+22
.tangled/workflows/deploy.yml
+22
.tangled/workflows/deploy.yml
···
···
1
+
## need this for commit idk what else to change
2
+
3
+
when:
4
+
- event: ["push"]
5
+
branch: ["main"]
6
+
7
+
engine: "nixery"
8
+
9
+
clone:
10
+
skip: true
11
+
12
+
dependencies:
13
+
nixpkgs:
14
+
- curl
15
+
16
+
steps:
17
+
- name: "Trigger Deploy"
18
+
command: |
19
+
curl -X POST \
20
+
-H "Authorization: Bearer $SCANS_HOST_API_KEY" \
21
+
-H "Authorization: Bearer $SCANS_HOST_API_KEY" \
22
+
https://free.scan.blue/api/v1/sites/jy35AeguTwaqDy_3ufq09/deploy?wait=true
+25
0001-ok.patch
+25
0001-ok.patch
···
···
1
+
From baf405c82fb23f9274a35384286bac2b901d45af Mon Sep 17 00:00:00 2001
2
+
From: scanash00 <scan@scanash.com>
3
+
Date: Tue, 30 Dec 2025 22:12:13 -0900
4
+
Subject: [PATCH] add ?wait=true
5
+
6
+
---
7
+
.tangled/workflows/deploy.yml | 3 ++-
8
+
1 file changed, 2 insertions(+), 1 deletion(-)
9
+
10
+
diff --git a/.tangled/workflows/deploy.yml b/.tangled/workflows/deploy.yml
11
+
index a44c51b..b7edfea 100644
12
+
--- a/.tangled/workflows/deploy.yml
13
+
+++ b/.tangled/workflows/deploy.yml
14
+
@@ -16,4 +16,5 @@ steps:
15
+
command: |
16
+
curl -X POST \
17
+
-H "Authorization: Bearer $SCANS_HOST_API_KEY" \
18
+
- https://free.scan.blue/api/v1/sites/jy35AeguTwaqDy_3ufq09/deploy
19
+
\ No newline at end of file
20
+
+ -H "Authorization: Bearer $SCANS_HOST_API_KEY" \
21
+
+ https://free.scan.blue/api/v1/sites/YOUR_SITE_ID/deploy?wait=true
22
+
\ No newline at end of file
23
+
--
24
+
2.50.1 (Apple Git-155)
25
+
+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>
+24
-23
package.json
+24
-23
package.json
···
9
"serve": "vite preview"
10
},
11
"devDependencies": {
12
-
"@iconify-json/lucide": "^1.2.77",
13
"@iconify/tailwind4": "^1.2.0",
14
-
"@tailwindcss/vite": "^4.1.17",
15
-
"prettier": "^3.7.3",
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.11",
26
-
"@atcute/client": "^4.1.0",
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.4",
33
"@atcute/lexicon-resolver": "^0.1.5",
34
-
"@atcute/lexicons": "^1.2.5",
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.82",
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.3.0",
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.2",
27
+
"@atcute/crypto": "^2.3.0",
28
+
"@atcute/did-plc": "^0.3.1",
29
"@atcute/identity": "^1.1.3",
30
+
"@atcute/identity-resolver": "^1.2.1",
31
+
"@atcute/leaflet": "^1.0.14",
32
+
"@atcute/lexicon-doc": "^2.0.6",
33
"@atcute/lexicon-resolver": "^0.1.5",
34
+
"@atcute/lexicons": "^1.2.6",
35
+
"@atcute/multibase": "^1.1.6",
36
+
"@atcute/oauth-browser-client": "^2.0.3",
37
+
"@atcute/repo": "^0.1.1",
38
+
"@atcute/tangled": "^1.0.13",
39
+
"@atcute/tid": "^1.1.0",
40
+
"@codemirror/commands": "^6.10.1",
41
"@codemirror/lang-json": "^6.0.2",
42
"@codemirror/lint": "^6.9.2",
43
+
"@codemirror/state": "^6.5.3",
44
+
"@codemirror/view": "^6.39.7",
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",
+564
-515
pnpm-lock.yaml
+564
-515
pnpm-lock.yaml
···
12
specifier: ^3.1.9
13
version: 3.1.9
14
'@atcute/bluesky':
15
-
specifier: ^3.2.11
16
-
version: 3.2.11
17
'@atcute/client':
18
-
specifier: ^4.1.0
19
-
version: 4.1.0
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
26
'@atcute/identity':
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.4
37
-
version: 2.0.4
38
'@atcute/lexicon-resolver':
39
specifier: ^0.1.5
40
-
version: 0.1.5(@atcute/identity-resolver@1.1.4(@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/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
56
'@codemirror/commands':
57
-
specifier: ^6.10.0
58
-
version: 6.10.0
59
'@codemirror/lang-json':
60
specifier: ^6.0.2
61
version: 6.0.2
···
63
specifier: ^6.9.2
64
version: 6.9.2
65
'@codemirror/state':
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.77
98
-
version: 1.2.77
99
'@iconify/tailwind4':
100
specifier: ^1.2.0
101
-
version: 1.2.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.7.3
107
-
version: 3.7.3
108
prettier-plugin-organize-imports:
109
specifier: ^4.3.0
110
-
version: 4.3.0(prettier@3.7.3)(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.7.3)(typescript@5.9.3))(prettier@3.7.3)
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
···
132
'@atcute/atproto@3.1.9':
133
resolution: {integrity: sha512-DyWwHCTdR4hY2BPNbLXgVmm7lI+fceOwWbE4LXbGvbvVtSn+ejSVFaAv01Ra3kWDha0whsOmbJL8JP0QPpf1+w==}
134
135
-
'@atcute/bluesky@3.2.11':
136
-
resolution: {integrity: sha512-AboS6y4t+zaxIq7E4noue10csSpIuk/Uwo30/l6GgGBDPXrd7STw8Yb5nGZQP+TdG/uC8/c2mm7UnY65SDOh6A==}
137
138
'@atcute/car@3.1.3':
139
resolution: {integrity: sha512-WJ13bAEt7TjDMVi09ubjLtvhdljbWInGm9Kfy7Y6NhrmiyC/aZYaA/zHX/bHI6xv1c/h3SQduWqxOr4ae49eqA==}
···
144
'@atcute/cbor@2.2.8':
145
resolution: {integrity: sha512-UzOAN9BuN6JCXgn0ryV8qZuRJUDrNqrbLd6EFM8jc6RYssjRyGRxNy6RZ1NU/07Hd8Tq/0pz8+nQiMu5Zai5uw==}
146
147
-
'@atcute/cid@2.2.6':
148
-
resolution: {integrity: sha512-bTAHHbJ24p+E//V4KCS4xdmd39o211jJswvqQOevj7vk+5IYcgDLx1ryZWZ1sEPOo9x875li/kj5gpKL14RDwQ==}
149
150
-
'@atcute/client@4.1.0':
151
-
resolution: {integrity: sha512-AYhSu3RSDA2VDkVGOmad320NRbUUUf5pCFWJcOzlk25YC/4kyzmMFfpzhf1jjjEcY+anNBXGGhav/kKB1evggQ==}
152
153
-
'@atcute/crypto@2.2.6':
154
-
resolution: {integrity: sha512-vkuexF+kmrKE1/Uqzub99Qi4QpnxA2jbu60E6PTgL4XypELQ6rb59MB/J1VbY2gs0kd3ET7+L3+NWpKD5nXyfA==}
155
156
-
'@atcute/did-plc@0.2.0':
157
-
resolution: {integrity: sha512-1sGek8GRM/Ph7nLVRREm8FqM7g4shGckItvdVwJcRbUa8Rh0zOsXQa0QyYWAC0k40BhkqO9FwKXhJEaXCmF5oQ==}
158
159
-
'@atcute/identity-resolver@1.1.4':
160
-
resolution: {integrity: sha512-/SVh8vf2cXFJenmBnGeYF2aY3WGQm3cJeew5NWTlkqoy3LvJ5wkvKq9PWu4Tv653VF40rPOp6LOdVr9Fa+q5rA==}
161
peerDependencies:
162
'@atcute/identity': ^1.0.0
163
164
'@atcute/identity@1.1.3':
165
resolution: {integrity: sha512-oIqPoI8TwWeQxvcLmFEZLdN2XdWcaLVtlm8pNk0E72As9HNzzD9pwKPrLr3rmTLRIoULPPFmq9iFNsTeCIU9ng==}
166
167
-
'@atcute/leaflet@1.0.12':
168
-
resolution: {integrity: sha512-T5laBTl8vwzy0eZXBy07IQSjsLqhbZmRJsffnNQ6XMSc+lnCZ/NHfuKy8TNJbDU6dc26Z7o5l0ELfWz5QESo+w==}
169
170
-
'@atcute/lexicon-doc@2.0.4':
171
-
resolution: {integrity: sha512-YfwlYFoYiBvRIYG0I1zsINCTFugFtS8l67uT3nQ04zdKVflzdg8uUj8cNZYRNY1V7okoOPdikhR4kPFhYGyemw==}
172
173
'@atcute/lexicon-resolver@0.1.5':
174
resolution: {integrity: sha512-0bx1/zdMQPuxvRcHW6ykAxRxktC2rEZLoAVSFoLSWDAA92Tf09F9QPK5wgXSF4MNODm1dvzMEdWSMIvlg8sr3A==}
···
176
'@atcute/identity': ^1.1.0
177
'@atcute/identity-resolver': ^1.1.3
178
179
-
'@atcute/lexicons@1.2.5':
180
-
resolution: {integrity: sha512-9yO9WdgxW8jZ7SbzUycH710z+JmsQ9W9n5S6i6eghYju32kkluFmgBeS47r8e8p2+Dv4DemS7o/3SUGsX9FR5Q==}
181
182
-
'@atcute/mst@0.1.0':
183
-
resolution: {integrity: sha512-h+iDToKEnBpigk2DOHjSqY63vJtjYKUIztqu1CZ0P+I54wV2SrgoqAXAT1xrW6A1Iup8cjTv+U2H5WVG4KxPLw==}
184
185
'@atcute/multibase@1.1.6':
186
resolution: {integrity: sha512-HBxuCgYLKPPxETV0Rot4VP9e24vKl8JdzGCZOVsDaOXJgbRZoRIF67Lp0H/OgnJeH/Xpva8Z5ReoTNJE5dn3kg==}
187
188
-
'@atcute/oauth-browser-client@2.0.1':
189
-
resolution: {integrity: sha512-lG021GkeORG06zfFf4bH85egObjBEKHNgAWHvbtY/E2dX4wxo88hf370pJDx8acdnuUJLJ2VKPikJtZwo4Heeg==}
190
191
-
'@atcute/repo@0.1.0':
192
-
resolution: {integrity: sha512-INiYAuma8dydBu7cqd2WVpcXh3mzhIepYBUqFWAK5MqMulPRLTRCc/9GW3G9pxYrOdlvLCVamG2Jf8XK0nuFEw==}
193
194
-
'@atcute/tangled@1.0.12':
195
-
resolution: {integrity: sha512-JKA5sOhd8SLhDFhY+PKHqLLytQBBKSiwcaEzfYUJBeyfvqXFPNNAwvRbe3VST4IQ3izoOu3O0R9/b1mjL45UzA==}
196
197
-
'@atcute/tid@1.0.3':
198
-
resolution: {integrity: sha512-wfMJx1IMdnu0CZgWl0uR4JO2s6PGT1YPhpytD4ZHzEYKKQVuqV6Eb/7vieaVo1eYNMp2FrY67FZObeR7utRl2w==}
199
200
-
'@atcute/uint8array@1.0.5':
201
-
resolution: {integrity: sha512-XLWWxoR2HNl2qU+FCr0rp1APwJXci7HnzbOQLxK55OaMNBXZ19+xNC5ii4QCsThsDxa4JS/JTzuiQLziITWf2Q==}
202
203
'@atcute/util-fetch@1.0.4':
204
resolution: {integrity: sha512-sIU9Qk0dE8PLEXSfhy+gIJV+HpiiknMytCI2SqLlqd0vgZUtEKI/EQfP+23LHWvP+CLCzVDOa6cpH045OlmNBg==}
205
206
'@atcute/varint@1.0.3':
207
resolution: {integrity: sha512-fdvMPyBB+McDT+Ai5e9RwEbwYV4yjZ60S2Dn5PTjGqUyxvoCH1z42viuheDZRUDkmfQehXJTZ5az7dSozVNtog==}
···
294
'@codemirror/autocomplete@6.20.0':
295
resolution: {integrity: sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==}
296
297
-
'@codemirror/commands@6.10.0':
298
-
resolution: {integrity: sha512-2xUIc5mHXQzT16JnyOFkh8PvfeXuIut3pslWGfsGOhxP/lpgRm9HOl/mpzLErgt5mXDovqA0d11P21gofRLb9w==}
299
300
'@codemirror/lang-json@6.0.2':
301
resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==}
302
303
-
'@codemirror/language@6.11.3':
304
-
resolution: {integrity: sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==}
305
306
'@codemirror/lint@6.9.2':
307
resolution: {integrity: sha512-sv3DylBiIyi+xKwRCJAAsBZZZWo82shJ/RTMymLabAdtbkV5cSKwWDeCgtUq3v8flTaXS2y1kKkICuRYtUswyQ==}
···
309
'@codemirror/search@6.5.11':
310
resolution: {integrity: sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==}
311
312
-
'@codemirror/state@6.5.2':
313
-
resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==}
314
315
-
'@codemirror/view@6.38.8':
316
-
resolution: {integrity: sha512-XcE9fcnkHCbWkjeKyi0lllwXmBLtyYb5dt89dJyx23I9+LSh5vZDIuk7OLG4VM1lgrXZQcY6cxyZyk5WVPRv/A==}
317
318
'@cyberalien/svg-utils@1.0.11':
319
resolution: {integrity: sha512-qEE9mnyI+avfGT3emKuRs3ucYkITeaV0Xi7VlYN41f+uGnZBecQP3jwz/AF437H9J4Q7qPClHKm4NiTYpNE6hA==}
···
324
cpu: [ppc64]
325
os: [aix]
326
327
-
'@esbuild/aix-ppc64@0.25.12':
328
-
resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
329
engines: {node: '>=18'}
330
cpu: [ppc64]
331
os: [aix]
···
336
cpu: [arm64]
337
os: [android]
338
339
-
'@esbuild/android-arm64@0.25.12':
340
-
resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==}
341
engines: {node: '>=18'}
342
cpu: [arm64]
343
os: [android]
···
348
cpu: [arm]
349
os: [android]
350
351
-
'@esbuild/android-arm@0.25.12':
352
-
resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==}
353
engines: {node: '>=18'}
354
cpu: [arm]
355
os: [android]
···
360
cpu: [x64]
361
os: [android]
362
363
-
'@esbuild/android-x64@0.25.12':
364
-
resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==}
365
engines: {node: '>=18'}
366
cpu: [x64]
367
os: [android]
···
372
cpu: [arm64]
373
os: [darwin]
374
375
-
'@esbuild/darwin-arm64@0.25.12':
376
-
resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==}
377
engines: {node: '>=18'}
378
cpu: [arm64]
379
os: [darwin]
···
384
cpu: [x64]
385
os: [darwin]
386
387
-
'@esbuild/darwin-x64@0.25.12':
388
-
resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==}
389
engines: {node: '>=18'}
390
cpu: [x64]
391
os: [darwin]
···
396
cpu: [arm64]
397
os: [freebsd]
398
399
-
'@esbuild/freebsd-arm64@0.25.12':
400
-
resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==}
401
engines: {node: '>=18'}
402
cpu: [arm64]
403
os: [freebsd]
···
408
cpu: [x64]
409
os: [freebsd]
410
411
-
'@esbuild/freebsd-x64@0.25.12':
412
-
resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==}
413
engines: {node: '>=18'}
414
cpu: [x64]
415
os: [freebsd]
···
420
cpu: [arm64]
421
os: [linux]
422
423
-
'@esbuild/linux-arm64@0.25.12':
424
-
resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==}
425
engines: {node: '>=18'}
426
cpu: [arm64]
427
os: [linux]
···
432
cpu: [arm]
433
os: [linux]
434
435
-
'@esbuild/linux-arm@0.25.12':
436
-
resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==}
437
engines: {node: '>=18'}
438
cpu: [arm]
439
os: [linux]
···
444
cpu: [ia32]
445
os: [linux]
446
447
-
'@esbuild/linux-ia32@0.25.12':
448
-
resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==}
449
engines: {node: '>=18'}
450
cpu: [ia32]
451
os: [linux]
···
456
cpu: [loong64]
457
os: [linux]
458
459
-
'@esbuild/linux-loong64@0.25.12':
460
-
resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==}
461
engines: {node: '>=18'}
462
cpu: [loong64]
463
os: [linux]
···
468
cpu: [mips64el]
469
os: [linux]
470
471
-
'@esbuild/linux-mips64el@0.25.12':
472
-
resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==}
473
engines: {node: '>=18'}
474
cpu: [mips64el]
475
os: [linux]
···
480
cpu: [ppc64]
481
os: [linux]
482
483
-
'@esbuild/linux-ppc64@0.25.12':
484
-
resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==}
485
engines: {node: '>=18'}
486
cpu: [ppc64]
487
os: [linux]
···
492
cpu: [riscv64]
493
os: [linux]
494
495
-
'@esbuild/linux-riscv64@0.25.12':
496
-
resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==}
497
engines: {node: '>=18'}
498
cpu: [riscv64]
499
os: [linux]
···
504
cpu: [s390x]
505
os: [linux]
506
507
-
'@esbuild/linux-s390x@0.25.12':
508
-
resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==}
509
engines: {node: '>=18'}
510
cpu: [s390x]
511
os: [linux]
···
516
cpu: [x64]
517
os: [linux]
518
519
-
'@esbuild/linux-x64@0.25.12':
520
-
resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==}
521
engines: {node: '>=18'}
522
cpu: [x64]
523
os: [linux]
524
525
-
'@esbuild/netbsd-arm64@0.25.12':
526
-
resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==}
527
engines: {node: '>=18'}
528
cpu: [arm64]
529
os: [netbsd]
···
534
cpu: [x64]
535
os: [netbsd]
536
537
-
'@esbuild/netbsd-x64@0.25.12':
538
-
resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==}
539
engines: {node: '>=18'}
540
cpu: [x64]
541
os: [netbsd]
···
546
cpu: [arm64]
547
os: [openbsd]
548
549
-
'@esbuild/openbsd-arm64@0.25.12':
550
-
resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==}
551
engines: {node: '>=18'}
552
cpu: [arm64]
553
os: [openbsd]
···
558
cpu: [x64]
559
os: [openbsd]
560
561
-
'@esbuild/openbsd-x64@0.25.12':
562
-
resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==}
563
engines: {node: '>=18'}
564
cpu: [x64]
565
os: [openbsd]
566
567
-
'@esbuild/openharmony-arm64@0.25.12':
568
-
resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==}
569
engines: {node: '>=18'}
570
cpu: [arm64]
571
os: [openharmony]
···
576
cpu: [x64]
577
os: [sunos]
578
579
-
'@esbuild/sunos-x64@0.25.12':
580
-
resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==}
581
engines: {node: '>=18'}
582
cpu: [x64]
583
os: [sunos]
···
588
cpu: [arm64]
589
os: [win32]
590
591
-
'@esbuild/win32-arm64@0.25.12':
592
-
resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==}
593
engines: {node: '>=18'}
594
cpu: [arm64]
595
os: [win32]
···
600
cpu: [ia32]
601
os: [win32]
602
603
-
'@esbuild/win32-ia32@0.25.12':
604
-
resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==}
605
engines: {node: '>=18'}
606
cpu: [ia32]
607
os: [win32]
···
612
cpu: [x64]
613
os: [win32]
614
615
-
'@esbuild/win32-x64@0.25.12':
616
-
resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==}
617
engines: {node: '>=18'}
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.77':
638
-
resolution: {integrity: sha512-FF3Z+np6Ksb0MaoQymhCHZ4xs5Oo8992Fw7By7bCgVCbBCClYV3wxpF8KzsI1FlxHD4ZXR42NVmXuqdW8YQGgA==}
639
640
'@iconify/tailwind4@1.2.0':
641
resolution: {integrity: sha512-+t7XqfojOB0zzZdd8gV7IQZGq1AaIHTlsxMVzagxYR0hAlJCLUD63o3iSlNKRMH3ZR7gZ8y5c9dJ7J431avRbA==}
642
peerDependencies:
643
tailwindcss: '>= 4.0.0'
644
645
-
'@iconify/tools@5.0.0':
646
-
resolution: {integrity: sha512-GY/FsuNdWA/FbkLqgQ8b1PHFkNvjMeSFWaVJdLldYGHBp0lZ64HJlcS0qzLfglacHTd8zYdfQjF74RxGqyGMgw==}
647
648
'@iconify/types@2.0.0':
649
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
···
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.4.0':
674
-
resolution: {integrity: sha512-DVeMRoGrgn/k45oQNu189BoW4SZwgZFzJ1+1TV5j2NJ/KFC83oa/enRqZSGshyeMk5cPWMhsKs9nx+8o0unwGg==}
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.4':
683
-
resolution: {integrity: sha512-LHL17Mq0OcFXm1pGQssuGTQFPPdxARjKM8f7GA5+sGtHi0K3R84YaSbmche0+RKWHnCsx9asEe5OWOI4FHfe4A==}
684
685
'@marijn/find-cluster-break@1.0.2':
686
resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
···
688
'@noble/secp256k1@3.0.0':
689
resolution: {integrity: sha512-NJBaR352KyIvj3t6sgT/+7xrNyF9Xk9QlLSIqUGVUYlsnDTAUqY8LOmwpcgEx4AMJXRITQ5XEVHD+mMaPfr3mg==}
690
691
-
'@rollup/rollup-android-arm-eabi@4.53.3':
692
-
resolution: {integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==}
693
cpu: [arm]
694
os: [android]
695
696
-
'@rollup/rollup-android-arm64@4.53.3':
697
-
resolution: {integrity: sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==}
698
cpu: [arm64]
699
os: [android]
700
701
-
'@rollup/rollup-darwin-arm64@4.53.3':
702
-
resolution: {integrity: sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==}
703
cpu: [arm64]
704
os: [darwin]
705
706
-
'@rollup/rollup-darwin-x64@4.53.3':
707
-
resolution: {integrity: sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==}
708
cpu: [x64]
709
os: [darwin]
710
711
-
'@rollup/rollup-freebsd-arm64@4.53.3':
712
-
resolution: {integrity: sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==}
713
cpu: [arm64]
714
os: [freebsd]
715
716
-
'@rollup/rollup-freebsd-x64@4.53.3':
717
-
resolution: {integrity: sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==}
718
cpu: [x64]
719
os: [freebsd]
720
721
-
'@rollup/rollup-linux-arm-gnueabihf@4.53.3':
722
-
resolution: {integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==}
723
cpu: [arm]
724
os: [linux]
725
726
-
'@rollup/rollup-linux-arm-musleabihf@4.53.3':
727
-
resolution: {integrity: sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==}
728
cpu: [arm]
729
os: [linux]
730
731
-
'@rollup/rollup-linux-arm64-gnu@4.53.3':
732
-
resolution: {integrity: sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==}
733
cpu: [arm64]
734
os: [linux]
735
736
-
'@rollup/rollup-linux-arm64-musl@4.53.3':
737
-
resolution: {integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==}
738
cpu: [arm64]
739
os: [linux]
740
741
-
'@rollup/rollup-linux-loong64-gnu@4.53.3':
742
-
resolution: {integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==}
743
cpu: [loong64]
744
os: [linux]
745
746
-
'@rollup/rollup-linux-ppc64-gnu@4.53.3':
747
-
resolution: {integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==}
748
cpu: [ppc64]
749
os: [linux]
750
751
-
'@rollup/rollup-linux-riscv64-gnu@4.53.3':
752
-
resolution: {integrity: sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==}
753
cpu: [riscv64]
754
os: [linux]
755
756
-
'@rollup/rollup-linux-riscv64-musl@4.53.3':
757
-
resolution: {integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==}
758
cpu: [riscv64]
759
os: [linux]
760
761
-
'@rollup/rollup-linux-s390x-gnu@4.53.3':
762
-
resolution: {integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==}
763
cpu: [s390x]
764
os: [linux]
765
766
-
'@rollup/rollup-linux-x64-gnu@4.53.3':
767
-
resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==}
768
cpu: [x64]
769
os: [linux]
770
771
-
'@rollup/rollup-linux-x64-musl@4.53.3':
772
-
resolution: {integrity: sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==}
773
cpu: [x64]
774
os: [linux]
775
776
-
'@rollup/rollup-openharmony-arm64@4.53.3':
777
-
resolution: {integrity: sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==}
778
cpu: [arm64]
779
os: [openharmony]
780
781
-
'@rollup/rollup-win32-arm64-msvc@4.53.3':
782
-
resolution: {integrity: sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==}
783
cpu: [arm64]
784
os: [win32]
785
786
-
'@rollup/rollup-win32-ia32-msvc@4.53.3':
787
-
resolution: {integrity: sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==}
788
cpu: [ia32]
789
os: [win32]
790
791
-
'@rollup/rollup-win32-x64-gnu@4.53.3':
792
-
resolution: {integrity: sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==}
793
cpu: [x64]
794
os: [win32]
795
796
-
'@rollup/rollup-win32-x64-msvc@4.53.3':
797
-
resolution: {integrity: sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==}
798
cpu: [x64]
799
os: [win32]
800
···
811
peerDependencies:
812
solid-js: ^1.8.6
813
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
···
919
'@types/estree@1.0.8':
920
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
921
922
'@types/node@24.10.1':
923
resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==}
924
···
941
solid-js:
942
optional: true
943
944
-
baseline-browser-mapping@2.8.32:
945
-
resolution: {integrity: sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==}
946
hasBin: true
947
948
boolbase@1.0.0:
949
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
950
951
-
browserslist@4.28.0:
952
-
resolution: {integrity: sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==}
953
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
954
hasBin: true
955
956
-
caniuse-lite@1.0.30001757:
957
-
resolution: {integrity: sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==}
958
959
codemirror@6.0.2:
960
resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==}
···
1020
domutils@3.2.2:
1021
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
1022
1023
-
electron-to-chromium@1.5.262:
1024
-
resolution: {integrity: sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==}
1025
1026
-
enhanced-resolve@5.18.3:
1027
-
resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==}
1028
engines: {node: '>=10.13.0'}
1029
1030
entities@4.5.0:
···
1040
engines: {node: '>=18'}
1041
hasBin: true
1042
1043
-
esbuild@0.25.12:
1044
-
resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
1045
engines: {node: '>=18'}
1046
hasBin: true
1047
···
1192
mlly@1.8.0:
1193
resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==}
1194
1195
-
modern-tar@0.7.2:
1196
-
resolution: {integrity: sha512-TGG1ZRk1TAQ3neuZwahAHke3rKsSlro+ooMYtjh9sl2gGPVMLMuWiHgwC7im9T5bSM566RSo2Dko56ETgEvZcA==}
1197
engines: {node: '>=18.0.0'}
1198
1199
ms@2.1.3:
···
1211
nanoid@5.1.6:
1212
resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==}
1213
engines: {node: ^18 || >=20}
1214
hasBin: true
1215
1216
node-releases@2.0.27:
···
1252
vue-tsc:
1253
optional: true
1254
1255
-
prettier-plugin-tailwindcss@0.7.1:
1256
-
resolution: {integrity: sha512-Bzv1LZcuiR1Sk02iJTS1QzlFNp/o5l2p3xkopwOrbPmtMeh3fK9rVW5M3neBQzHq+kGKj/4LGQMTNcTH4NGPtQ==}
1257
engines: {node: '>=20.19'}
1258
peerDependencies:
1259
'@ianvs/prettier-plugin-sort-imports': '*'
···
1307
prettier-plugin-svelte:
1308
optional: true
1309
1310
-
prettier@3.7.3:
1311
-
resolution: {integrity: sha512-QgODejq9K3OzoBbuyobZlUhznP5SKwPqp+6Q6xw6o8gnhr4O85L2U915iM2IDcfF2NPXVaM9zlo9tdwipnYwzg==}
1312
engines: {node: '>=14'}
1313
hasBin: true
1314
1315
resolve-pkg-maps@1.0.0:
1316
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
1317
1318
-
rollup@4.53.3:
1319
-
resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==}
1320
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
1321
hasBin: true
1322
···
1357
engines: {node: '>=16'}
1358
hasBin: true
1359
1360
-
tailwindcss@4.1.17:
1361
-
resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==}
1362
1363
tapable@2.3.0:
1364
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
···
1385
ufo@1.6.1:
1386
resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
1387
1388
undici-types@7.16.0:
1389
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
1390
1391
-
update-browserslist-db@1.1.4:
1392
-
resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==}
1393
hasBin: true
1394
peerDependencies:
1395
browserslist: '>= 4.21.0'
···
1404
'@testing-library/jest-dom':
1405
optional: true
1406
1407
-
vite@7.2.4:
1408
-
resolution: {integrity: sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==}
1409
engines: {node: ^20.19.0 || >=22.12.0}
1410
hasBin: true
1411
peerDependencies:
···
1471
1472
'@atcute/atproto@3.1.9':
1473
dependencies:
1474
-
'@atcute/lexicons': 1.2.5
1475
1476
-
'@atcute/bluesky@3.2.11':
1477
dependencies:
1478
'@atcute/atproto': 3.1.9
1479
-
'@atcute/lexicons': 1.2.5
1480
1481
'@atcute/car@3.1.3':
1482
dependencies:
1483
'@atcute/cbor': 2.2.8
1484
-
'@atcute/cid': 2.2.6
1485
-
'@atcute/uint8array': 1.0.5
1486
'@atcute/varint': 1.0.3
1487
yocto-queue: 1.2.2
1488
1489
'@atcute/car@5.0.0':
1490
dependencies:
1491
'@atcute/cbor': 2.2.8
1492
-
'@atcute/cid': 2.2.6
1493
-
'@atcute/uint8array': 1.0.5
1494
'@atcute/varint': 1.0.3
1495
1496
'@atcute/cbor@2.2.8':
1497
dependencies:
1498
-
'@atcute/cid': 2.2.6
1499
'@atcute/multibase': 1.1.6
1500
-
'@atcute/uint8array': 1.0.5
1501
1502
-
'@atcute/cid@2.2.6':
1503
dependencies:
1504
'@atcute/multibase': 1.1.6
1505
-
'@atcute/uint8array': 1.0.5
1506
1507
-
'@atcute/client@4.1.0':
1508
dependencies:
1509
'@atcute/identity': 1.1.3
1510
-
'@atcute/lexicons': 1.2.5
1511
1512
-
'@atcute/crypto@2.2.6':
1513
dependencies:
1514
'@atcute/multibase': 1.1.6
1515
-
'@atcute/uint8array': 1.0.5
1516
'@noble/secp256k1': 3.0.0
1517
1518
-
'@atcute/did-plc@0.2.0':
1519
dependencies:
1520
'@atcute/cbor': 2.2.8
1521
-
'@atcute/cid': 2.2.6
1522
-
'@atcute/crypto': 2.2.6
1523
'@atcute/identity': 1.1.3
1524
-
'@atcute/lexicons': 1.2.5
1525
'@atcute/multibase': 1.1.6
1526
-
'@atcute/uint8array': 1.0.5
1527
'@badrap/valita': 0.4.6
1528
1529
-
'@atcute/identity-resolver@1.1.4(@atcute/identity@1.1.3)':
1530
dependencies:
1531
'@atcute/identity': 1.1.3
1532
-
'@atcute/lexicons': 1.2.5
1533
'@atcute/util-fetch': 1.0.4
1534
'@badrap/valita': 0.4.6
1535
1536
'@atcute/identity@1.1.3':
1537
dependencies:
1538
-
'@atcute/lexicons': 1.2.5
1539
'@badrap/valita': 0.4.6
1540
1541
-
'@atcute/leaflet@1.0.12':
1542
dependencies:
1543
'@atcute/atproto': 3.1.9
1544
-
'@atcute/lexicons': 1.2.5
1545
1546
-
'@atcute/lexicon-doc@2.0.4':
1547
dependencies:
1548
'@atcute/identity': 1.1.3
1549
-
'@atcute/lexicons': 1.2.5
1550
'@badrap/valita': 0.4.6
1551
1552
-
'@atcute/lexicon-resolver@0.1.5(@atcute/identity-resolver@1.1.4(@atcute/identity@1.1.3))(@atcute/identity@1.1.3)':
1553
dependencies:
1554
-
'@atcute/crypto': 2.2.6
1555
'@atcute/identity': 1.1.3
1556
-
'@atcute/identity-resolver': 1.1.4(@atcute/identity@1.1.3)
1557
-
'@atcute/lexicon-doc': 2.0.4
1558
-
'@atcute/lexicons': 1.2.5
1559
-
'@atcute/repo': 0.1.0
1560
'@atcute/util-fetch': 1.0.4
1561
'@badrap/valita': 0.4.6
1562
1563
-
'@atcute/lexicons@1.2.5':
1564
dependencies:
1565
-
'@standard-schema/spec': 1.0.0
1566
esm-env: 1.2.2
1567
1568
-
'@atcute/mst@0.1.0':
1569
dependencies:
1570
'@atcute/cbor': 2.2.8
1571
-
'@atcute/cid': 2.2.6
1572
-
'@atcute/uint8array': 1.0.5
1573
1574
'@atcute/multibase@1.1.6':
1575
dependencies:
1576
-
'@atcute/uint8array': 1.0.5
1577
1578
-
'@atcute/oauth-browser-client@2.0.1':
1579
dependencies:
1580
-
'@atcute/client': 4.1.0
1581
-
'@atcute/identity': 1.1.3
1582
-
'@atcute/identity-resolver': 1.1.4(@atcute/identity@1.1.3)
1583
-
'@atcute/lexicons': 1.2.5
1584
'@atcute/multibase': 1.1.6
1585
-
'@atcute/uint8array': 1.0.5
1586
nanoid: 5.1.6
1587
1588
-
'@atcute/repo@0.1.0':
1589
dependencies:
1590
'@atcute/car': 5.0.0
1591
'@atcute/cbor': 2.2.8
1592
-
'@atcute/cid': 2.2.6
1593
-
'@atcute/crypto': 2.2.6
1594
-
'@atcute/lexicons': 1.2.5
1595
-
'@atcute/mst': 0.1.0
1596
-
'@atcute/uint8array': 1.0.5
1597
1598
-
'@atcute/tangled@1.0.12':
1599
dependencies:
1600
'@atcute/atproto': 3.1.9
1601
-
'@atcute/lexicons': 1.2.5
1602
1603
-
'@atcute/tid@1.0.3': {}
1604
1605
-
'@atcute/uint8array@1.0.5': {}
1606
1607
'@atcute/util-fetch@1.0.4':
1608
dependencies:
1609
'@badrap/valita': 0.4.6
1610
1611
'@atcute/varint@1.0.3': {}
1612
···
1650
dependencies:
1651
'@babel/compat-data': 7.28.5
1652
'@babel/helper-validator-option': 7.27.1
1653
-
browserslist: 4.28.0
1654
lru-cache: 5.1.1
1655
semver: 6.3.1
1656
···
1725
1726
'@codemirror/autocomplete@6.20.0':
1727
dependencies:
1728
-
'@codemirror/language': 6.11.3
1729
-
'@codemirror/state': 6.5.2
1730
-
'@codemirror/view': 6.38.8
1731
-
'@lezer/common': 1.4.0
1732
1733
-
'@codemirror/commands@6.10.0':
1734
dependencies:
1735
-
'@codemirror/language': 6.11.3
1736
-
'@codemirror/state': 6.5.2
1737
-
'@codemirror/view': 6.38.8
1738
-
'@lezer/common': 1.4.0
1739
1740
'@codemirror/lang-json@6.0.2':
1741
dependencies:
1742
-
'@codemirror/language': 6.11.3
1743
'@lezer/json': 1.0.3
1744
1745
-
'@codemirror/language@6.11.3':
1746
dependencies:
1747
-
'@codemirror/state': 6.5.2
1748
-
'@codemirror/view': 6.38.8
1749
-
'@lezer/common': 1.4.0
1750
'@lezer/highlight': 1.2.3
1751
-
'@lezer/lr': 1.4.4
1752
style-mod: 4.1.3
1753
1754
'@codemirror/lint@6.9.2':
1755
dependencies:
1756
-
'@codemirror/state': 6.5.2
1757
-
'@codemirror/view': 6.38.8
1758
crelt: 1.0.6
1759
1760
'@codemirror/search@6.5.11':
1761
dependencies:
1762
-
'@codemirror/state': 6.5.2
1763
-
'@codemirror/view': 6.38.8
1764
crelt: 1.0.6
1765
1766
-
'@codemirror/state@6.5.2':
1767
dependencies:
1768
'@marijn/find-cluster-break': 1.0.2
1769
1770
-
'@codemirror/view@6.38.8':
1771
dependencies:
1772
-
'@codemirror/state': 6.5.2
1773
crelt: 1.0.6
1774
style-mod: 4.1.3
1775
w3c-keyname: 2.2.8
···
1781
'@esbuild/aix-ppc64@0.23.1':
1782
optional: true
1783
1784
-
'@esbuild/aix-ppc64@0.25.12':
1785
optional: true
1786
1787
'@esbuild/android-arm64@0.23.1':
1788
optional: true
1789
1790
-
'@esbuild/android-arm64@0.25.12':
1791
optional: true
1792
1793
'@esbuild/android-arm@0.23.1':
1794
optional: true
1795
1796
-
'@esbuild/android-arm@0.25.12':
1797
optional: true
1798
1799
'@esbuild/android-x64@0.23.1':
1800
optional: true
1801
1802
-
'@esbuild/android-x64@0.25.12':
1803
optional: true
1804
1805
'@esbuild/darwin-arm64@0.23.1':
1806
optional: true
1807
1808
-
'@esbuild/darwin-arm64@0.25.12':
1809
optional: true
1810
1811
'@esbuild/darwin-x64@0.23.1':
1812
optional: true
1813
1814
-
'@esbuild/darwin-x64@0.25.12':
1815
optional: true
1816
1817
'@esbuild/freebsd-arm64@0.23.1':
1818
optional: true
1819
1820
-
'@esbuild/freebsd-arm64@0.25.12':
1821
optional: true
1822
1823
'@esbuild/freebsd-x64@0.23.1':
1824
optional: true
1825
1826
-
'@esbuild/freebsd-x64@0.25.12':
1827
optional: true
1828
1829
'@esbuild/linux-arm64@0.23.1':
1830
optional: true
1831
1832
-
'@esbuild/linux-arm64@0.25.12':
1833
optional: true
1834
1835
'@esbuild/linux-arm@0.23.1':
1836
optional: true
1837
1838
-
'@esbuild/linux-arm@0.25.12':
1839
optional: true
1840
1841
'@esbuild/linux-ia32@0.23.1':
1842
optional: true
1843
1844
-
'@esbuild/linux-ia32@0.25.12':
1845
optional: true
1846
1847
'@esbuild/linux-loong64@0.23.1':
1848
optional: true
1849
1850
-
'@esbuild/linux-loong64@0.25.12':
1851
optional: true
1852
1853
'@esbuild/linux-mips64el@0.23.1':
1854
optional: true
1855
1856
-
'@esbuild/linux-mips64el@0.25.12':
1857
optional: true
1858
1859
'@esbuild/linux-ppc64@0.23.1':
1860
optional: true
1861
1862
-
'@esbuild/linux-ppc64@0.25.12':
1863
optional: true
1864
1865
'@esbuild/linux-riscv64@0.23.1':
1866
optional: true
1867
1868
-
'@esbuild/linux-riscv64@0.25.12':
1869
optional: true
1870
1871
'@esbuild/linux-s390x@0.23.1':
1872
optional: true
1873
1874
-
'@esbuild/linux-s390x@0.25.12':
1875
optional: true
1876
1877
'@esbuild/linux-x64@0.23.1':
1878
optional: true
1879
1880
-
'@esbuild/linux-x64@0.25.12':
1881
optional: true
1882
1883
-
'@esbuild/netbsd-arm64@0.25.12':
1884
optional: true
1885
1886
'@esbuild/netbsd-x64@0.23.1':
1887
optional: true
1888
1889
-
'@esbuild/netbsd-x64@0.25.12':
1890
optional: true
1891
1892
'@esbuild/openbsd-arm64@0.23.1':
1893
optional: true
1894
1895
-
'@esbuild/openbsd-arm64@0.25.12':
1896
optional: true
1897
1898
'@esbuild/openbsd-x64@0.23.1':
1899
optional: true
1900
1901
-
'@esbuild/openbsd-x64@0.25.12':
1902
optional: true
1903
1904
-
'@esbuild/openharmony-arm64@0.25.12':
1905
optional: true
1906
1907
'@esbuild/sunos-x64@0.23.1':
1908
optional: true
1909
1910
-
'@esbuild/sunos-x64@0.25.12':
1911
optional: true
1912
1913
'@esbuild/win32-arm64@0.23.1':
1914
optional: true
1915
1916
-
'@esbuild/win32-arm64@0.25.12':
1917
optional: true
1918
1919
'@esbuild/win32-ia32@0.23.1':
1920
optional: true
1921
1922
-
'@esbuild/win32-ia32@0.25.12':
1923
optional: true
1924
1925
'@esbuild/win32-x64@0.23.1':
1926
optional: true
1927
1928
-
'@esbuild/win32-x64@0.25.12':
1929
optional: true
1930
1931
-
'@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)':
1932
dependencies:
1933
-
'@codemirror/language': 6.11.3
1934
-
'@codemirror/state': 6.5.2
1935
-
'@codemirror/view': 6.38.8
1936
'@lezer/highlight': 1.2.3
1937
1938
-
'@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)':
1939
dependencies:
1940
-
'@codemirror/language': 6.11.3
1941
-
'@codemirror/state': 6.5.2
1942
-
'@codemirror/view': 6.38.8
1943
'@lezer/highlight': 1.2.3
1944
1945
-
'@iconify-json/lucide@1.2.77':
1946
dependencies:
1947
'@iconify/types': 2.0.0
1948
1949
-
'@iconify/tailwind4@1.2.0(tailwindcss@4.1.17)':
1950
dependencies:
1951
-
'@iconify/tools': 5.0.0
1952
'@iconify/types': 2.0.0
1953
'@iconify/utils': 3.1.0
1954
-
tailwindcss: 4.1.17
1955
1956
-
'@iconify/tools@5.0.0':
1957
dependencies:
1958
'@cyberalien/svg-utils': 1.0.11
1959
'@iconify/types': 2.0.0
1960
'@iconify/utils': 3.1.0
1961
fflate: 0.8.2
1962
-
modern-tar: 0.7.2
1963
pathe: 2.0.3
1964
svgo: 4.0.0
1965
···
1992
1993
'@jsr/mary__exif-rm@0.2.2': {}
1994
1995
-
'@lezer/common@1.4.0': {}
1996
1997
'@lezer/highlight@1.2.3':
1998
dependencies:
1999
-
'@lezer/common': 1.4.0
2000
2001
'@lezer/json@1.0.3':
2002
dependencies:
2003
-
'@lezer/common': 1.4.0
2004
'@lezer/highlight': 1.2.3
2005
-
'@lezer/lr': 1.4.4
2006
2007
-
'@lezer/lr@1.4.4':
2008
dependencies:
2009
-
'@lezer/common': 1.4.0
2010
2011
'@marijn/find-cluster-break@1.0.2': {}
2012
2013
'@noble/secp256k1@3.0.0': {}
2014
2015
-
'@rollup/rollup-android-arm-eabi@4.53.3':
2016
optional: true
2017
2018
-
'@rollup/rollup-android-arm64@4.53.3':
2019
optional: true
2020
2021
-
'@rollup/rollup-darwin-arm64@4.53.3':
2022
optional: true
2023
2024
-
'@rollup/rollup-darwin-x64@4.53.3':
2025
optional: true
2026
2027
-
'@rollup/rollup-freebsd-arm64@4.53.3':
2028
optional: true
2029
2030
-
'@rollup/rollup-freebsd-x64@4.53.3':
2031
optional: true
2032
2033
-
'@rollup/rollup-linux-arm-gnueabihf@4.53.3':
2034
optional: true
2035
2036
-
'@rollup/rollup-linux-arm-musleabihf@4.53.3':
2037
optional: true
2038
2039
-
'@rollup/rollup-linux-arm64-gnu@4.53.3':
2040
optional: true
2041
2042
-
'@rollup/rollup-linux-arm64-musl@4.53.3':
2043
optional: true
2044
2045
-
'@rollup/rollup-linux-loong64-gnu@4.53.3':
2046
optional: true
2047
2048
-
'@rollup/rollup-linux-ppc64-gnu@4.53.3':
2049
optional: true
2050
2051
-
'@rollup/rollup-linux-riscv64-gnu@4.53.3':
2052
optional: true
2053
2054
-
'@rollup/rollup-linux-riscv64-musl@4.53.3':
2055
optional: true
2056
2057
-
'@rollup/rollup-linux-s390x-gnu@4.53.3':
2058
optional: true
2059
2060
-
'@rollup/rollup-linux-x64-gnu@4.53.3':
2061
optional: true
2062
2063
-
'@rollup/rollup-linux-x64-musl@4.53.3':
2064
optional: true
2065
2066
-
'@rollup/rollup-openharmony-arm64@4.53.3':
2067
optional: true
2068
2069
-
'@rollup/rollup-win32-arm64-msvc@4.53.3':
2070
optional: true
2071
2072
-
'@rollup/rollup-win32-ia32-msvc@4.53.3':
2073
optional: true
2074
2075
-
'@rollup/rollup-win32-x64-gnu@4.53.3':
2076
optional: true
2077
2078
-
'@rollup/rollup-win32-x64-msvc@4.53.3':
2079
optional: true
2080
2081
'@skyware/firehose@0.5.2':
···
2092
dependencies:
2093
solid-js: 1.9.10
2094
2095
-
'@standard-schema/spec@1.0.0': {}
2096
2097
-
'@tailwindcss/node@4.1.17':
2098
dependencies:
2099
'@jridgewell/remapping': 2.3.5
2100
-
enhanced-resolve: 5.18.3
2101
jiti: 2.6.1
2102
lightningcss: 1.30.2
2103
magic-string: 0.30.21
2104
source-map-js: 1.2.1
2105
-
tailwindcss: 4.1.17
2106
2107
-
'@tailwindcss/oxide-android-arm64@4.1.17':
2108
optional: true
2109
2110
-
'@tailwindcss/oxide-darwin-arm64@4.1.17':
2111
optional: true
2112
2113
-
'@tailwindcss/oxide-darwin-x64@4.1.17':
2114
optional: true
2115
2116
-
'@tailwindcss/oxide-freebsd-x64@4.1.17':
2117
optional: true
2118
2119
-
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17':
2120
optional: true
2121
2122
-
'@tailwindcss/oxide-linux-arm64-gnu@4.1.17':
2123
optional: true
2124
2125
-
'@tailwindcss/oxide-linux-arm64-musl@4.1.17':
2126
optional: true
2127
2128
-
'@tailwindcss/oxide-linux-x64-gnu@4.1.17':
2129
optional: true
2130
2131
-
'@tailwindcss/oxide-linux-x64-musl@4.1.17':
2132
optional: true
2133
2134
-
'@tailwindcss/oxide-wasm32-wasi@4.1.17':
2135
optional: true
2136
2137
-
'@tailwindcss/oxide-win32-arm64-msvc@4.1.17':
2138
optional: true
2139
2140
-
'@tailwindcss/oxide-win32-x64-msvc@4.1.17':
2141
optional: true
2142
2143
-
'@tailwindcss/oxide@4.1.17':
2144
optionalDependencies:
2145
-
'@tailwindcss/oxide-android-arm64': 4.1.17
2146
-
'@tailwindcss/oxide-darwin-arm64': 4.1.17
2147
-
'@tailwindcss/oxide-darwin-x64': 4.1.17
2148
-
'@tailwindcss/oxide-freebsd-x64': 4.1.17
2149
-
'@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.17
2150
-
'@tailwindcss/oxide-linux-arm64-gnu': 4.1.17
2151
-
'@tailwindcss/oxide-linux-arm64-musl': 4.1.17
2152
-
'@tailwindcss/oxide-linux-x64-gnu': 4.1.17
2153
-
'@tailwindcss/oxide-linux-x64-musl': 4.1.17
2154
-
'@tailwindcss/oxide-wasm32-wasi': 4.1.17
2155
-
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.17
2156
-
'@tailwindcss/oxide-win32-x64-msvc': 4.1.17
2157
2158
-
'@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))':
2159
dependencies:
2160
-
'@tailwindcss/node': 4.1.17
2161
-
'@tailwindcss/oxide': 4.1.17
2162
-
tailwindcss: 4.1.17
2163
-
vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)
2164
2165
'@types/babel__core@7.20.5':
2166
dependencies:
···
2185
2186
'@types/estree@1.0.8': {}
2187
2188
'@types/node@24.10.1':
2189
dependencies:
2190
undici-types: 7.16.0
···
2208
optionalDependencies:
2209
solid-js: 1.9.10
2210
2211
-
baseline-browser-mapping@2.8.32: {}
2212
2213
boolbase@1.0.0: {}
2214
2215
-
browserslist@4.28.0:
2216
dependencies:
2217
-
baseline-browser-mapping: 2.8.32
2218
-
caniuse-lite: 1.0.30001757
2219
-
electron-to-chromium: 1.5.262
2220
node-releases: 2.0.27
2221
-
update-browserslist-db: 1.1.4(browserslist@4.28.0)
2222
2223
-
caniuse-lite@1.0.30001757: {}
2224
2225
codemirror@6.0.2:
2226
dependencies:
2227
'@codemirror/autocomplete': 6.20.0
2228
-
'@codemirror/commands': 6.10.0
2229
-
'@codemirror/language': 6.11.3
2230
'@codemirror/lint': 6.9.2
2231
'@codemirror/search': 6.5.11
2232
-
'@codemirror/state': 6.5.2
2233
-
'@codemirror/view': 6.38.8
2234
2235
commander@11.1.0: {}
2236
···
2290
domelementtype: 2.3.0
2291
domhandler: 5.0.3
2292
2293
-
electron-to-chromium@1.5.262: {}
2294
2295
-
enhanced-resolve@5.18.3:
2296
dependencies:
2297
graceful-fs: 4.2.11
2298
tapable: 2.3.0
···
2329
'@esbuild/win32-x64': 0.23.1
2330
optional: true
2331
2332
-
esbuild@0.25.12:
2333
optionalDependencies:
2334
-
'@esbuild/aix-ppc64': 0.25.12
2335
-
'@esbuild/android-arm': 0.25.12
2336
-
'@esbuild/android-arm64': 0.25.12
2337
-
'@esbuild/android-x64': 0.25.12
2338
-
'@esbuild/darwin-arm64': 0.25.12
2339
-
'@esbuild/darwin-x64': 0.25.12
2340
-
'@esbuild/freebsd-arm64': 0.25.12
2341
-
'@esbuild/freebsd-x64': 0.25.12
2342
-
'@esbuild/linux-arm': 0.25.12
2343
-
'@esbuild/linux-arm64': 0.25.12
2344
-
'@esbuild/linux-ia32': 0.25.12
2345
-
'@esbuild/linux-loong64': 0.25.12
2346
-
'@esbuild/linux-mips64el': 0.25.12
2347
-
'@esbuild/linux-ppc64': 0.25.12
2348
-
'@esbuild/linux-riscv64': 0.25.12
2349
-
'@esbuild/linux-s390x': 0.25.12
2350
-
'@esbuild/linux-x64': 0.25.12
2351
-
'@esbuild/netbsd-arm64': 0.25.12
2352
-
'@esbuild/netbsd-x64': 0.25.12
2353
-
'@esbuild/openbsd-arm64': 0.25.12
2354
-
'@esbuild/openbsd-x64': 0.25.12
2355
-
'@esbuild/openharmony-arm64': 0.25.12
2356
-
'@esbuild/sunos-x64': 0.25.12
2357
-
'@esbuild/win32-arm64': 0.25.12
2358
-
'@esbuild/win32-ia32': 0.25.12
2359
-
'@esbuild/win32-x64': 0.25.12
2360
2361
escalade@3.2.0: {}
2362
···
2464
pkg-types: 1.3.1
2465
ufo: 1.6.1
2466
2467
-
modern-tar@0.7.2: {}
2468
2469
ms@2.1.3: {}
2470
···
2473
nanoid@3.3.11: {}
2474
2475
nanoid@5.1.6: {}
2476
2477
node-releases@2.0.27: {}
2478
···
2504
picocolors: 1.1.1
2505
source-map-js: 1.2.1
2506
2507
-
prettier-plugin-organize-imports@4.3.0(prettier@3.7.3)(typescript@5.9.3):
2508
dependencies:
2509
-
prettier: 3.7.3
2510
typescript: 5.9.3
2511
2512
-
prettier-plugin-tailwindcss@0.7.1(prettier-plugin-organize-imports@4.3.0(prettier@3.7.3)(typescript@5.9.3))(prettier@3.7.3):
2513
dependencies:
2514
-
prettier: 3.7.3
2515
optionalDependencies:
2516
-
prettier-plugin-organize-imports: 4.3.0(prettier@3.7.3)(typescript@5.9.3)
2517
2518
-
prettier@3.7.3: {}
2519
2520
resolve-pkg-maps@1.0.0:
2521
optional: true
2522
2523
-
rollup@4.53.3:
2524
dependencies:
2525
'@types/estree': 1.0.8
2526
optionalDependencies:
2527
-
'@rollup/rollup-android-arm-eabi': 4.53.3
2528
-
'@rollup/rollup-android-arm64': 4.53.3
2529
-
'@rollup/rollup-darwin-arm64': 4.53.3
2530
-
'@rollup/rollup-darwin-x64': 4.53.3
2531
-
'@rollup/rollup-freebsd-arm64': 4.53.3
2532
-
'@rollup/rollup-freebsd-x64': 4.53.3
2533
-
'@rollup/rollup-linux-arm-gnueabihf': 4.53.3
2534
-
'@rollup/rollup-linux-arm-musleabihf': 4.53.3
2535
-
'@rollup/rollup-linux-arm64-gnu': 4.53.3
2536
-
'@rollup/rollup-linux-arm64-musl': 4.53.3
2537
-
'@rollup/rollup-linux-loong64-gnu': 4.53.3
2538
-
'@rollup/rollup-linux-ppc64-gnu': 4.53.3
2539
-
'@rollup/rollup-linux-riscv64-gnu': 4.53.3
2540
-
'@rollup/rollup-linux-riscv64-musl': 4.53.3
2541
-
'@rollup/rollup-linux-s390x-gnu': 4.53.3
2542
-
'@rollup/rollup-linux-x64-gnu': 4.53.3
2543
-
'@rollup/rollup-linux-x64-musl': 4.53.3
2544
-
'@rollup/rollup-openharmony-arm64': 4.53.3
2545
-
'@rollup/rollup-win32-arm64-msvc': 4.53.3
2546
-
'@rollup/rollup-win32-ia32-msvc': 4.53.3
2547
-
'@rollup/rollup-win32-x64-gnu': 4.53.3
2548
-
'@rollup/rollup-win32-x64-msvc': 4.53.3
2549
fsevents: 2.3.3
2550
2551
sax@1.4.3: {}
···
2587
picocolors: 1.1.1
2588
sax: 1.4.3
2589
2590
-
tailwindcss@4.1.17: {}
2591
2592
tapable@2.3.0: {}
2593
···
2610
2611
ufo@1.6.1: {}
2612
2613
undici-types@7.16.0:
2614
optional: true
2615
2616
-
update-browserslist-db@1.1.4(browserslist@4.28.0):
2617
dependencies:
2618
-
browserslist: 4.28.0
2619
escalade: 3.2.0
2620
picocolors: 1.1.1
2621
2622
-
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)):
2623
dependencies:
2624
'@babel/core': 7.28.5
2625
'@types/babel__core': 7.20.5
···
2627
merge-anything: 5.1.7
2628
solid-js: 1.9.10
2629
solid-refresh: 0.6.3(solid-js@1.9.10)
2630
-
vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)
2631
-
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))
2632
transitivePeerDependencies:
2633
- supports-color
2634
2635
-
vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2):
2636
dependencies:
2637
-
esbuild: 0.25.12
2638
fdir: 6.5.0(picomatch@4.0.3)
2639
picomatch: 4.0.3
2640
postcss: 8.5.6
2641
-
rollup: 4.53.3
2642
tinyglobby: 0.2.15
2643
optionalDependencies:
2644
'@types/node': 24.10.1
···
2647
lightningcss: 1.30.2
2648
tsx: 4.19.2
2649
2650
-
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)):
2651
optionalDependencies:
2652
-
vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)
2653
2654
w3c-keyname@2.2.8: {}
2655
···
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.2
19
+
version: 4.1.2
20
'@atcute/crypto':
21
+
specifier: ^2.3.0
22
+
version: 2.3.0
23
'@atcute/did-plc':
24
+
specifier: ^0.3.1
25
+
version: 0.3.1
26
'@atcute/identity':
27
specifier: ^1.1.3
28
version: 1.1.3
29
'@atcute/identity-resolver':
30
+
specifier: ^1.2.1
31
+
version: 1.2.1(@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.6
37
+
version: 2.0.6
38
'@atcute/lexicon-resolver':
39
specifier: ^0.1.5
40
+
version: 0.1.5(@atcute/identity-resolver@1.2.1(@atcute/identity@1.1.3))(@atcute/identity@1.1.3)
41
'@atcute/lexicons':
42
+
specifier: ^1.2.6
43
+
version: 1.2.6
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.1
52
+
version: 0.1.1
53
'@atcute/tangled':
54
+
specifier: ^1.0.13
55
+
version: 1.0.13
56
'@atcute/tid':
57
+
specifier: ^1.1.0
58
+
version: 1.1.0
59
'@codemirror/commands':
60
+
specifier: ^6.10.1
61
+
version: 6.10.1
62
'@codemirror/lang-json':
63
specifier: ^6.0.2
64
version: 6.0.2
···
66
specifier: ^6.9.2
67
version: 6.9.2
68
'@codemirror/state':
69
+
specifier: ^6.5.3
70
+
version: 6.5.3
71
'@codemirror/view':
72
+
specifier: ^6.39.7
73
+
version: 6.39.7
74
'@fsegurai/codemirror-theme-basic-dark':
75
+
specifier: ^6.2.3
76
+
version: 6.2.3(@codemirror/language@6.12.1)(@codemirror/state@6.5.3)(@codemirror/view@6.39.7)(@lezer/highlight@1.2.3)
77
'@fsegurai/codemirror-theme-basic-light':
78
+
specifier: ^6.2.3
79
+
version: 6.2.3(@codemirror/language@6.12.1)(@codemirror/state@6.5.3)(@codemirror/view@6.39.7)(@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.82
101
+
version: 1.2.82
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.3.0(@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.3.0
125
+
version: 7.3.0(@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.3.0(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2))
129
130
packages:
131
···
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==}
···
147
'@atcute/cbor@2.2.8':
148
resolution: {integrity: sha512-UzOAN9BuN6JCXgn0ryV8qZuRJUDrNqrbLd6EFM8jc6RYssjRyGRxNy6RZ1NU/07Hd8Tq/0pz8+nQiMu5Zai5uw==}
149
150
+
'@atcute/cid@2.3.0':
151
+
resolution: {integrity: sha512-1SRdkTuMs/l5arQ+7Ag0F7JAueZqtzYE0d2gmbkuzi8EPweNU1kYlQs0CE4dSd81YF8PMDTOQty0K2ATq9CW9g==}
152
153
+
'@atcute/client@4.1.2':
154
+
resolution: {integrity: sha512-DOJ0hpdBA4QVl4SGUeOUyz5FfYhdjRW1h0XIH9YDgNTipeA0tnUbRs8hWh9Nb7nyn6zMKzO5RpaWyWWWSx9Yxw==}
155
156
+
'@atcute/crypto@2.3.0':
157
+
resolution: {integrity: sha512-w5pkJKCjbNMQu+F4JRHbR3ROQyhi1wbn+GSC6WDQamcYHkZmEZk1/eoI354bIQOOfkEM6aFLv718iskrkon4GQ==}
158
159
+
'@atcute/did-plc@0.3.1':
160
+
resolution: {integrity: sha512-KsuVdRtaaIPMmlcCDcxZzLg6OWm7rajczquhIHfA3s57+c34PFQbdY4Lsc2BvDwZ0fUjmbwzvQI3Zio2VcZa7w==}
161
162
+
'@atcute/identity-resolver@1.2.1':
163
+
resolution: {integrity: sha512-LqWFFf8D8bqW8l0zUV9oZxcXYZ8+uQTZfjURoxH1TLmtmZFSXredtQHsY70k/iSMNDPxWHJXebdlKxJm5ioNIg==}
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.6':
174
+
resolution: {integrity: sha512-iDYJkuom+tIw3zIvU1ggCEVFfReXKfOUtIhpY2kEg2kQeSfMB75F+8k1QOpeAQBetyWYmjsHqBuSUX9oQS6L1Q==}
175
176
'@atcute/lexicon-resolver@0.1.5':
177
resolution: {integrity: sha512-0bx1/zdMQPuxvRcHW6ykAxRxktC2rEZLoAVSFoLSWDAA92Tf09F9QPK5wgXSF4MNODm1dvzMEdWSMIvlg8sr3A==}
···
179
'@atcute/identity': ^1.1.0
180
'@atcute/identity-resolver': ^1.1.3
181
182
+
'@atcute/lexicons@1.2.6':
183
+
resolution: {integrity: sha512-s76UQd8D+XmHIzrjD9CJ9SOOeeLPHc+sMmcj7UFakAW/dDFXc579fcRdRfuUKvXBL5v1Gs2VgDdlh/IvvQZAwA==}
184
185
+
'@atcute/mst@0.1.1':
186
+
resolution: {integrity: sha512-NZ/lZ68GOjmAgBSeGf6WHyKM5wo1Hhc7PNt9uwsViswGPMNEEKNj9cw+0YGziXee/Qbnvc+CKqbRSPwruhXFQg==}
187
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.1':
195
+
resolution: {integrity: sha512-P5aWjt3bvcquUkUmGPslF0naAfLGRHse5Qdz9/RJYrFuoH0iiEMyRnW6M+3ksOe20GPsMnbq71WbzzFkRFPBtg==}
196
197
+
'@atcute/tangled@1.0.13':
198
+
resolution: {integrity: sha512-K95jmjDXl/f1FFzOJkk07ibNbFsPmn64sdrMACxQmUibO9WcfSjzjZLPXuH6WHFnCNtIBG3x1FQ7ndQgLoZAmw==}
199
+
200
+
'@atcute/tid@1.1.0':
201
+
resolution: {integrity: sha512-U/YKL9BsBi/bcVXaIwdUBfglnjFxRfqoPd2f1uLsEIDQk1EyxepwdDQYOQ5t/aQctmtywl7lQn6KESQNG+mdfg==}
202
203
+
'@atcute/time-ms@1.0.0':
204
+
resolution: {integrity: sha512-iWEOlMBcO3ktB+zQPC2kXka9H/798we+IWq2sjhb+hQJNNfcJrwejzvNi/68Q3jKo/hdfwZjRU9iF8U6D32/2Q==}
205
206
+
'@atcute/uint8array@1.0.6':
207
+
resolution: {integrity: sha512-ucfRBQc7BFT8n9eCyGOzDHEMKF/nZwhS2pPao4Xtab1ML3HdFYcX2DM1tadCzas85QTGxHe5urnUAAcNKGRi9A==}
208
209
'@atcute/util-fetch@1.0.4':
210
resolution: {integrity: sha512-sIU9Qk0dE8PLEXSfhy+gIJV+HpiiknMytCI2SqLlqd0vgZUtEKI/EQfP+23LHWvP+CLCzVDOa6cpH045OlmNBg==}
211
+
212
+
'@atcute/util-text@0.0.1':
213
+
resolution: {integrity: sha512-t1KZqvn0AYy+h2KcJyHnKF9aEqfRfMUmyY8j1ELtAEIgqN9CxINAjxnoRCJIFUlvWzb+oY3uElQL/Vyk3yss0g==}
214
215
'@atcute/varint@1.0.3':
216
resolution: {integrity: sha512-fdvMPyBB+McDT+Ai5e9RwEbwYV4yjZ60S2Dn5PTjGqUyxvoCH1z42viuheDZRUDkmfQehXJTZ5az7dSozVNtog==}
···
303
'@codemirror/autocomplete@6.20.0':
304
resolution: {integrity: sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==}
305
306
+
'@codemirror/commands@6.10.1':
307
+
resolution: {integrity: sha512-uWDWFypNdQmz2y1LaNJzK7fL7TYKLeUAU0npEC685OKTF3KcQ2Vu3klIM78D7I6wGhktme0lh3CuQLv0ZCrD9Q==}
308
309
'@codemirror/lang-json@6.0.2':
310
resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==}
311
312
+
'@codemirror/language@6.12.1':
313
+
resolution: {integrity: sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==}
314
315
'@codemirror/lint@6.9.2':
316
resolution: {integrity: sha512-sv3DylBiIyi+xKwRCJAAsBZZZWo82shJ/RTMymLabAdtbkV5cSKwWDeCgtUq3v8flTaXS2y1kKkICuRYtUswyQ==}
···
318
'@codemirror/search@6.5.11':
319
resolution: {integrity: sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==}
320
321
+
'@codemirror/state@6.5.3':
322
+
resolution: {integrity: sha512-MerMzJzlXogk2fxWFU1nKp36bY5orBG59HnPiz0G9nLRebWa0zXuv2siH6PLIHBvv5TH8CkQRqjBs0MlxCZu+A==}
323
324
+
'@codemirror/view@6.39.7':
325
+
resolution: {integrity: sha512-3Vif9hnNHJnl2YgOtkR/wzGzhYcQ8gy3LGdUhkLUU8xSBbgsTxrE8he/CMTpeINm5TgxLe2FmzvF6IYQL/BSAg==}
326
327
'@cyberalien/svg-utils@1.0.11':
328
resolution: {integrity: sha512-qEE9mnyI+avfGT3emKuRs3ucYkITeaV0Xi7VlYN41f+uGnZBecQP3jwz/AF437H9J4Q7qPClHKm4NiTYpNE6hA==}
···
333
cpu: [ppc64]
334
os: [aix]
335
336
+
'@esbuild/aix-ppc64@0.27.2':
337
+
resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==}
338
engines: {node: '>=18'}
339
cpu: [ppc64]
340
os: [aix]
···
345
cpu: [arm64]
346
os: [android]
347
348
+
'@esbuild/android-arm64@0.27.2':
349
+
resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==}
350
engines: {node: '>=18'}
351
cpu: [arm64]
352
os: [android]
···
357
cpu: [arm]
358
os: [android]
359
360
+
'@esbuild/android-arm@0.27.2':
361
+
resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==}
362
engines: {node: '>=18'}
363
cpu: [arm]
364
os: [android]
···
369
cpu: [x64]
370
os: [android]
371
372
+
'@esbuild/android-x64@0.27.2':
373
+
resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==}
374
engines: {node: '>=18'}
375
cpu: [x64]
376
os: [android]
···
381
cpu: [arm64]
382
os: [darwin]
383
384
+
'@esbuild/darwin-arm64@0.27.2':
385
+
resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==}
386
engines: {node: '>=18'}
387
cpu: [arm64]
388
os: [darwin]
···
393
cpu: [x64]
394
os: [darwin]
395
396
+
'@esbuild/darwin-x64@0.27.2':
397
+
resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==}
398
engines: {node: '>=18'}
399
cpu: [x64]
400
os: [darwin]
···
405
cpu: [arm64]
406
os: [freebsd]
407
408
+
'@esbuild/freebsd-arm64@0.27.2':
409
+
resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==}
410
engines: {node: '>=18'}
411
cpu: [arm64]
412
os: [freebsd]
···
417
cpu: [x64]
418
os: [freebsd]
419
420
+
'@esbuild/freebsd-x64@0.27.2':
421
+
resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==}
422
engines: {node: '>=18'}
423
cpu: [x64]
424
os: [freebsd]
···
429
cpu: [arm64]
430
os: [linux]
431
432
+
'@esbuild/linux-arm64@0.27.2':
433
+
resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==}
434
engines: {node: '>=18'}
435
cpu: [arm64]
436
os: [linux]
···
441
cpu: [arm]
442
os: [linux]
443
444
+
'@esbuild/linux-arm@0.27.2':
445
+
resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==}
446
engines: {node: '>=18'}
447
cpu: [arm]
448
os: [linux]
···
453
cpu: [ia32]
454
os: [linux]
455
456
+
'@esbuild/linux-ia32@0.27.2':
457
+
resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==}
458
engines: {node: '>=18'}
459
cpu: [ia32]
460
os: [linux]
···
465
cpu: [loong64]
466
os: [linux]
467
468
+
'@esbuild/linux-loong64@0.27.2':
469
+
resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==}
470
engines: {node: '>=18'}
471
cpu: [loong64]
472
os: [linux]
···
477
cpu: [mips64el]
478
os: [linux]
479
480
+
'@esbuild/linux-mips64el@0.27.2':
481
+
resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==}
482
engines: {node: '>=18'}
483
cpu: [mips64el]
484
os: [linux]
···
489
cpu: [ppc64]
490
os: [linux]
491
492
+
'@esbuild/linux-ppc64@0.27.2':
493
+
resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==}
494
engines: {node: '>=18'}
495
cpu: [ppc64]
496
os: [linux]
···
501
cpu: [riscv64]
502
os: [linux]
503
504
+
'@esbuild/linux-riscv64@0.27.2':
505
+
resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==}
506
engines: {node: '>=18'}
507
cpu: [riscv64]
508
os: [linux]
···
513
cpu: [s390x]
514
os: [linux]
515
516
+
'@esbuild/linux-s390x@0.27.2':
517
+
resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==}
518
engines: {node: '>=18'}
519
cpu: [s390x]
520
os: [linux]
···
525
cpu: [x64]
526
os: [linux]
527
528
+
'@esbuild/linux-x64@0.27.2':
529
+
resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==}
530
engines: {node: '>=18'}
531
cpu: [x64]
532
os: [linux]
533
534
+
'@esbuild/netbsd-arm64@0.27.2':
535
+
resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==}
536
engines: {node: '>=18'}
537
cpu: [arm64]
538
os: [netbsd]
···
543
cpu: [x64]
544
os: [netbsd]
545
546
+
'@esbuild/netbsd-x64@0.27.2':
547
+
resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==}
548
engines: {node: '>=18'}
549
cpu: [x64]
550
os: [netbsd]
···
555
cpu: [arm64]
556
os: [openbsd]
557
558
+
'@esbuild/openbsd-arm64@0.27.2':
559
+
resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==}
560
engines: {node: '>=18'}
561
cpu: [arm64]
562
os: [openbsd]
···
567
cpu: [x64]
568
os: [openbsd]
569
570
+
'@esbuild/openbsd-x64@0.27.2':
571
+
resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==}
572
engines: {node: '>=18'}
573
cpu: [x64]
574
os: [openbsd]
575
576
+
'@esbuild/openharmony-arm64@0.27.2':
577
+
resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==}
578
engines: {node: '>=18'}
579
cpu: [arm64]
580
os: [openharmony]
···
585
cpu: [x64]
586
os: [sunos]
587
588
+
'@esbuild/sunos-x64@0.27.2':
589
+
resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==}
590
engines: {node: '>=18'}
591
cpu: [x64]
592
os: [sunos]
···
597
cpu: [arm64]
598
os: [win32]
599
600
+
'@esbuild/win32-arm64@0.27.2':
601
+
resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==}
602
engines: {node: '>=18'}
603
cpu: [arm64]
604
os: [win32]
···
609
cpu: [ia32]
610
os: [win32]
611
612
+
'@esbuild/win32-ia32@0.27.2':
613
+
resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==}
614
engines: {node: '>=18'}
615
cpu: [ia32]
616
os: [win32]
···
621
cpu: [x64]
622
os: [win32]
623
624
+
'@esbuild/win32-x64@0.27.2':
625
+
resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==}
626
engines: {node: '>=18'}
627
cpu: [x64]
628
os: [win32]
629
630
+
'@fsegurai/codemirror-theme-basic-dark@6.2.3':
631
+
resolution: {integrity: sha512-08d09Yn9Ic8mjCzrBQQhtws/HM+8B00bRV9FqW+GaIQwSOFmn17FsvzuLJQyervcKAkTzmKaLPjp2D3Y+2K8EQ==}
632
peerDependencies:
633
'@codemirror/language': ^6.0.0
634
'@codemirror/state': ^6.0.0
635
'@codemirror/view': ^6.0.0
636
'@lezer/highlight': ^1.0.0
637
638
+
'@fsegurai/codemirror-theme-basic-light@6.2.3':
639
+
resolution: {integrity: sha512-rkHCj1U3OwNAqLLi2xti47u3Fq6gDiSEKmQsAOwIADJKnnwU2LeAwCPqSEa7sUVlavFusjDvt5L/SmGjb10vWg==}
640
peerDependencies:
641
'@codemirror/language': ^6.0.0
642
'@codemirror/state': ^6.0.0
643
'@codemirror/view': ^6.0.0
644
'@lezer/highlight': ^1.0.0
645
646
+
'@iconify-json/lucide@1.2.82':
647
+
resolution: {integrity: sha512-fHZWegspOZonl5GNTvOkHsjnTMdSslFh3EzpzUtRyLxO8bOonqk2OTU3hCl0k4VXzViMjqpRK3X1sotnuBXkFA==}
648
649
'@iconify/tailwind4@1.2.0':
650
resolution: {integrity: sha512-+t7XqfojOB0zzZdd8gV7IQZGq1AaIHTlsxMVzagxYR0hAlJCLUD63o3iSlNKRMH3ZR7gZ8y5c9dJ7J431avRbA==}
651
peerDependencies:
652
tailwindcss: '>= 4.0.0'
653
654
+
'@iconify/tools@5.0.1':
655
+
resolution: {integrity: sha512-/znhBN9WIpJd9UtKhyEDfRKwNo8rrOy8dShF8bwSZ1i27ukTSHjeS6bmVK4tTYBYriwFhBf70JT6g8GIRwFvbw==}
656
657
'@iconify/types@2.0.0':
658
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
···
679
'@jsr/mary__exif-rm@0.2.2':
680
resolution: {integrity: sha512-+ZpLaC+1CyqWhH608Sqd6/yTG0pOlokn2tCXha7s1SMQ+GLKo4Nn/PskTeeP9Pt+6gNYSu6ednoSlRvXb2ZGxg==, tarball: https://npm.jsr.io/~/11/@jsr/mary__exif-rm/0.2.2.tgz}
681
682
+
'@lezer/common@1.5.0':
683
+
resolution: {integrity: sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA==}
684
685
'@lezer/highlight@1.2.3':
686
resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==}
···
688
'@lezer/json@1.0.3':
689
resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==}
690
691
+
'@lezer/lr@1.4.5':
692
+
resolution: {integrity: sha512-/YTRKP5yPPSo1xImYQk7AZZMAgap0kegzqCSYHjAL9x1AZ0ZQW+IpcEzMKagCsbTsLnVeWkxYrCNeXG8xEPrjg==}
693
694
'@marijn/find-cluster-break@1.0.2':
695
resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
···
697
'@noble/secp256k1@3.0.0':
698
resolution: {integrity: sha512-NJBaR352KyIvj3t6sgT/+7xrNyF9Xk9QlLSIqUGVUYlsnDTAUqY8LOmwpcgEx4AMJXRITQ5XEVHD+mMaPfr3mg==}
699
700
+
'@rollup/rollup-android-arm-eabi@4.54.0':
701
+
resolution: {integrity: sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==}
702
cpu: [arm]
703
os: [android]
704
705
+
'@rollup/rollup-android-arm64@4.54.0':
706
+
resolution: {integrity: sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==}
707
cpu: [arm64]
708
os: [android]
709
710
+
'@rollup/rollup-darwin-arm64@4.54.0':
711
+
resolution: {integrity: sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==}
712
cpu: [arm64]
713
os: [darwin]
714
715
+
'@rollup/rollup-darwin-x64@4.54.0':
716
+
resolution: {integrity: sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==}
717
cpu: [x64]
718
os: [darwin]
719
720
+
'@rollup/rollup-freebsd-arm64@4.54.0':
721
+
resolution: {integrity: sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==}
722
cpu: [arm64]
723
os: [freebsd]
724
725
+
'@rollup/rollup-freebsd-x64@4.54.0':
726
+
resolution: {integrity: sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==}
727
cpu: [x64]
728
os: [freebsd]
729
730
+
'@rollup/rollup-linux-arm-gnueabihf@4.54.0':
731
+
resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==}
732
cpu: [arm]
733
os: [linux]
734
735
+
'@rollup/rollup-linux-arm-musleabihf@4.54.0':
736
+
resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==}
737
cpu: [arm]
738
os: [linux]
739
740
+
'@rollup/rollup-linux-arm64-gnu@4.54.0':
741
+
resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==}
742
cpu: [arm64]
743
os: [linux]
744
745
+
'@rollup/rollup-linux-arm64-musl@4.54.0':
746
+
resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==}
747
cpu: [arm64]
748
os: [linux]
749
750
+
'@rollup/rollup-linux-loong64-gnu@4.54.0':
751
+
resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==}
752
cpu: [loong64]
753
os: [linux]
754
755
+
'@rollup/rollup-linux-ppc64-gnu@4.54.0':
756
+
resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==}
757
cpu: [ppc64]
758
os: [linux]
759
760
+
'@rollup/rollup-linux-riscv64-gnu@4.54.0':
761
+
resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==}
762
cpu: [riscv64]
763
os: [linux]
764
765
+
'@rollup/rollup-linux-riscv64-musl@4.54.0':
766
+
resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==}
767
cpu: [riscv64]
768
os: [linux]
769
770
+
'@rollup/rollup-linux-s390x-gnu@4.54.0':
771
+
resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==}
772
cpu: [s390x]
773
os: [linux]
774
775
+
'@rollup/rollup-linux-x64-gnu@4.54.0':
776
+
resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==}
777
cpu: [x64]
778
os: [linux]
779
780
+
'@rollup/rollup-linux-x64-musl@4.54.0':
781
+
resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==}
782
cpu: [x64]
783
os: [linux]
784
785
+
'@rollup/rollup-openharmony-arm64@4.54.0':
786
+
resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==}
787
cpu: [arm64]
788
os: [openharmony]
789
790
+
'@rollup/rollup-win32-arm64-msvc@4.54.0':
791
+
resolution: {integrity: sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==}
792
cpu: [arm64]
793
os: [win32]
794
795
+
'@rollup/rollup-win32-ia32-msvc@4.54.0':
796
+
resolution: {integrity: sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==}
797
cpu: [ia32]
798
os: [win32]
799
800
+
'@rollup/rollup-win32-x64-gnu@4.54.0':
801
+
resolution: {integrity: sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==}
802
cpu: [x64]
803
os: [win32]
804
805
+
'@rollup/rollup-win32-x64-msvc@4.54.0':
806
+
resolution: {integrity: sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==}
807
cpu: [x64]
808
os: [win32]
809
···
820
peerDependencies:
821
solid-js: ^1.8.6
822
823
+
'@standard-schema/spec@1.1.0':
824
+
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
825
826
+
'@tailwindcss/node@4.1.18':
827
+
resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==}
828
829
+
'@tailwindcss/oxide-android-arm64@4.1.18':
830
+
resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==}
831
engines: {node: '>= 10'}
832
cpu: [arm64]
833
os: [android]
834
835
+
'@tailwindcss/oxide-darwin-arm64@4.1.18':
836
+
resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==}
837
engines: {node: '>= 10'}
838
cpu: [arm64]
839
os: [darwin]
840
841
+
'@tailwindcss/oxide-darwin-x64@4.1.18':
842
+
resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==}
843
engines: {node: '>= 10'}
844
cpu: [x64]
845
os: [darwin]
846
847
+
'@tailwindcss/oxide-freebsd-x64@4.1.18':
848
+
resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==}
849
engines: {node: '>= 10'}
850
cpu: [x64]
851
os: [freebsd]
852
853
+
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18':
854
+
resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==}
855
engines: {node: '>= 10'}
856
cpu: [arm]
857
os: [linux]
858
859
+
'@tailwindcss/oxide-linux-arm64-gnu@4.1.18':
860
+
resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==}
861
engines: {node: '>= 10'}
862
cpu: [arm64]
863
os: [linux]
864
865
+
'@tailwindcss/oxide-linux-arm64-musl@4.1.18':
866
+
resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==}
867
engines: {node: '>= 10'}
868
cpu: [arm64]
869
os: [linux]
870
871
+
'@tailwindcss/oxide-linux-x64-gnu@4.1.18':
872
+
resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==}
873
engines: {node: '>= 10'}
874
cpu: [x64]
875
os: [linux]
876
877
+
'@tailwindcss/oxide-linux-x64-musl@4.1.18':
878
+
resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==}
879
engines: {node: '>= 10'}
880
cpu: [x64]
881
os: [linux]
882
883
+
'@tailwindcss/oxide-wasm32-wasi@4.1.18':
884
+
resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==}
885
engines: {node: '>=14.0.0'}
886
cpu: [wasm32]
887
bundledDependencies:
···
892
- '@emnapi/wasi-threads'
893
- tslib
894
895
+
'@tailwindcss/oxide-win32-arm64-msvc@4.1.18':
896
+
resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==}
897
engines: {node: '>= 10'}
898
cpu: [arm64]
899
os: [win32]
900
901
+
'@tailwindcss/oxide-win32-x64-msvc@4.1.18':
902
+
resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==}
903
engines: {node: '>= 10'}
904
cpu: [x64]
905
os: [win32]
906
907
+
'@tailwindcss/oxide@4.1.18':
908
+
resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==}
909
engines: {node: '>= 10'}
910
911
+
'@tailwindcss/vite@4.1.18':
912
+
resolution: {integrity: sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==}
913
peerDependencies:
914
vite: ^5.2.0 || ^6 || ^7
915
···
928
'@types/estree@1.0.8':
929
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
930
931
+
'@types/node@22.19.3':
932
+
resolution: {integrity: sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==}
933
+
934
'@types/node@24.10.1':
935
resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==}
936
···
953
solid-js:
954
optional: true
955
956
+
baseline-browser-mapping@2.9.11:
957
+
resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==}
958
hasBin: true
959
960
boolbase@1.0.0:
961
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
962
963
+
browserslist@4.28.1:
964
+
resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==}
965
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
966
hasBin: true
967
968
+
caniuse-lite@1.0.30001761:
969
+
resolution: {integrity: sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==}
970
971
codemirror@6.0.2:
972
resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==}
···
1032
domutils@3.2.2:
1033
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
1034
1035
+
electron-to-chromium@1.5.267:
1036
+
resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==}
1037
1038
+
enhanced-resolve@5.18.4:
1039
+
resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==}
1040
engines: {node: '>=10.13.0'}
1041
1042
entities@4.5.0:
···
1052
engines: {node: '>=18'}
1053
hasBin: true
1054
1055
+
esbuild@0.27.2:
1056
+
resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==}
1057
engines: {node: '>=18'}
1058
hasBin: true
1059
···
1204
mlly@1.8.0:
1205
resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==}
1206
1207
+
modern-tar@0.7.3:
1208
+
resolution: {integrity: sha512-4W79zekKGyYU4JXVmB78DOscMFaJth2gGhgfTl2alWE4rNe3nf4N2pqenQ0rEtIewrnD79M687Ouba3YGTLOvg==}
1209
engines: {node: '>=18.0.0'}
1210
1211
ms@2.1.3:
···
1223
nanoid@5.1.6:
1224
resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==}
1225
engines: {node: ^18 || >=20}
1226
+
hasBin: true
1227
+
1228
+
node-gyp-build@4.8.4:
1229
+
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
1230
hasBin: true
1231
1232
node-releases@2.0.27:
···
1268
vue-tsc:
1269
optional: true
1270
1271
+
prettier-plugin-tailwindcss@0.7.2:
1272
+
resolution: {integrity: sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA==}
1273
engines: {node: '>=20.19'}
1274
peerDependencies:
1275
'@ianvs/prettier-plugin-sort-imports': '*'
···
1323
prettier-plugin-svelte:
1324
optional: true
1325
1326
+
prettier@3.7.4:
1327
+
resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==}
1328
engines: {node: '>=14'}
1329
hasBin: true
1330
1331
resolve-pkg-maps@1.0.0:
1332
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
1333
1334
+
rollup@4.54.0:
1335
+
resolution: {integrity: sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==}
1336
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
1337
hasBin: true
1338
···
1373
engines: {node: '>=16'}
1374
hasBin: true
1375
1376
+
tailwindcss@4.1.18:
1377
+
resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==}
1378
1379
tapable@2.3.0:
1380
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
···
1401
ufo@1.6.1:
1402
resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
1403
1404
+
undici-types@6.21.0:
1405
+
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
1406
+
1407
undici-types@7.16.0:
1408
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
1409
1410
+
unicode-segmenter@0.14.4:
1411
+
resolution: {integrity: sha512-pR5VCiCrLrKOL6FRW61jnk9+wyMtKKowq+jyFY9oc6uHbWKhDL4yVRiI4YZPksGMK72Pahh8m0cn/0JvbDDyJg==}
1412
+
1413
+
update-browserslist-db@1.2.3:
1414
+
resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
1415
hasBin: true
1416
peerDependencies:
1417
browserslist: '>= 4.21.0'
···
1426
'@testing-library/jest-dom':
1427
optional: true
1428
1429
+
vite@7.3.0:
1430
+
resolution: {integrity: sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==}
1431
engines: {node: ^20.19.0 || >=22.12.0}
1432
hasBin: true
1433
peerDependencies:
···
1493
1494
'@atcute/atproto@3.1.9':
1495
dependencies:
1496
+
'@atcute/lexicons': 1.2.6
1497
1498
+
'@atcute/bluesky@3.2.14':
1499
dependencies:
1500
'@atcute/atproto': 3.1.9
1501
+
'@atcute/lexicons': 1.2.6
1502
1503
'@atcute/car@3.1.3':
1504
dependencies:
1505
'@atcute/cbor': 2.2.8
1506
+
'@atcute/cid': 2.3.0
1507
+
'@atcute/uint8array': 1.0.6
1508
'@atcute/varint': 1.0.3
1509
yocto-queue: 1.2.2
1510
1511
'@atcute/car@5.0.0':
1512
dependencies:
1513
'@atcute/cbor': 2.2.8
1514
+
'@atcute/cid': 2.3.0
1515
+
'@atcute/uint8array': 1.0.6
1516
'@atcute/varint': 1.0.3
1517
1518
'@atcute/cbor@2.2.8':
1519
dependencies:
1520
+
'@atcute/cid': 2.3.0
1521
'@atcute/multibase': 1.1.6
1522
+
'@atcute/uint8array': 1.0.6
1523
1524
+
'@atcute/cid@2.3.0':
1525
dependencies:
1526
'@atcute/multibase': 1.1.6
1527
+
'@atcute/uint8array': 1.0.6
1528
1529
+
'@atcute/client@4.1.2':
1530
dependencies:
1531
'@atcute/identity': 1.1.3
1532
+
'@atcute/lexicons': 1.2.6
1533
1534
+
'@atcute/crypto@2.3.0':
1535
dependencies:
1536
'@atcute/multibase': 1.1.6
1537
+
'@atcute/uint8array': 1.0.6
1538
'@noble/secp256k1': 3.0.0
1539
1540
+
'@atcute/did-plc@0.3.1':
1541
dependencies:
1542
'@atcute/cbor': 2.2.8
1543
+
'@atcute/cid': 2.3.0
1544
+
'@atcute/crypto': 2.3.0
1545
'@atcute/identity': 1.1.3
1546
+
'@atcute/lexicons': 1.2.6
1547
'@atcute/multibase': 1.1.6
1548
+
'@atcute/uint8array': 1.0.6
1549
+
'@atcute/util-fetch': 1.0.4
1550
'@badrap/valita': 0.4.6
1551
1552
+
'@atcute/identity-resolver@1.2.1(@atcute/identity@1.1.3)':
1553
dependencies:
1554
'@atcute/identity': 1.1.3
1555
+
'@atcute/lexicons': 1.2.6
1556
'@atcute/util-fetch': 1.0.4
1557
'@badrap/valita': 0.4.6
1558
1559
'@atcute/identity@1.1.3':
1560
dependencies:
1561
+
'@atcute/lexicons': 1.2.6
1562
'@badrap/valita': 0.4.6
1563
1564
+
'@atcute/leaflet@1.0.14':
1565
dependencies:
1566
'@atcute/atproto': 3.1.9
1567
+
'@atcute/lexicons': 1.2.6
1568
1569
+
'@atcute/lexicon-doc@2.0.6':
1570
dependencies:
1571
'@atcute/identity': 1.1.3
1572
+
'@atcute/lexicons': 1.2.6
1573
+
'@atcute/uint8array': 1.0.6
1574
+
'@atcute/util-text': 0.0.1
1575
'@badrap/valita': 0.4.6
1576
1577
+
'@atcute/lexicon-resolver@0.1.5(@atcute/identity-resolver@1.2.1(@atcute/identity@1.1.3))(@atcute/identity@1.1.3)':
1578
dependencies:
1579
+
'@atcute/crypto': 2.3.0
1580
'@atcute/identity': 1.1.3
1581
+
'@atcute/identity-resolver': 1.2.1(@atcute/identity@1.1.3)
1582
+
'@atcute/lexicon-doc': 2.0.6
1583
+
'@atcute/lexicons': 1.2.6
1584
+
'@atcute/repo': 0.1.1
1585
'@atcute/util-fetch': 1.0.4
1586
'@badrap/valita': 0.4.6
1587
1588
+
'@atcute/lexicons@1.2.6':
1589
dependencies:
1590
+
'@atcute/uint8array': 1.0.6
1591
+
'@atcute/util-text': 0.0.1
1592
+
'@standard-schema/spec': 1.1.0
1593
esm-env: 1.2.2
1594
1595
+
'@atcute/mst@0.1.1':
1596
dependencies:
1597
'@atcute/cbor': 2.2.8
1598
+
'@atcute/cid': 2.3.0
1599
+
'@atcute/uint8array': 1.0.6
1600
1601
'@atcute/multibase@1.1.6':
1602
dependencies:
1603
+
'@atcute/uint8array': 1.0.6
1604
1605
+
'@atcute/oauth-browser-client@2.0.3(@atcute/identity@1.1.3)':
1606
dependencies:
1607
+
'@atcute/client': 4.1.2
1608
+
'@atcute/identity-resolver': 1.2.1(@atcute/identity@1.1.3)
1609
+
'@atcute/lexicons': 1.2.6
1610
'@atcute/multibase': 1.1.6
1611
+
'@atcute/uint8array': 1.0.6
1612
nanoid: 5.1.6
1613
+
transitivePeerDependencies:
1614
+
- '@atcute/identity'
1615
1616
+
'@atcute/repo@0.1.1':
1617
dependencies:
1618
'@atcute/car': 5.0.0
1619
'@atcute/cbor': 2.2.8
1620
+
'@atcute/cid': 2.3.0
1621
+
'@atcute/crypto': 2.3.0
1622
+
'@atcute/lexicons': 1.2.6
1623
+
'@atcute/mst': 0.1.1
1624
+
'@atcute/uint8array': 1.0.6
1625
1626
+
'@atcute/tangled@1.0.13':
1627
dependencies:
1628
'@atcute/atproto': 3.1.9
1629
+
'@atcute/lexicons': 1.2.6
1630
+
1631
+
'@atcute/tid@1.1.0':
1632
+
dependencies:
1633
+
'@atcute/time-ms': 1.0.0
1634
1635
+
'@atcute/time-ms@1.0.0':
1636
+
dependencies:
1637
+
'@types/node': 22.19.3
1638
+
node-gyp-build: 4.8.4
1639
1640
+
'@atcute/uint8array@1.0.6': {}
1641
1642
'@atcute/util-fetch@1.0.4':
1643
dependencies:
1644
'@badrap/valita': 0.4.6
1645
+
1646
+
'@atcute/util-text@0.0.1':
1647
+
dependencies:
1648
+
unicode-segmenter: 0.14.4
1649
1650
'@atcute/varint@1.0.3': {}
1651
···
1689
dependencies:
1690
'@babel/compat-data': 7.28.5
1691
'@babel/helper-validator-option': 7.27.1
1692
+
browserslist: 4.28.1
1693
lru-cache: 5.1.1
1694
semver: 6.3.1
1695
···
1764
1765
'@codemirror/autocomplete@6.20.0':
1766
dependencies:
1767
+
'@codemirror/language': 6.12.1
1768
+
'@codemirror/state': 6.5.3
1769
+
'@codemirror/view': 6.39.7
1770
+
'@lezer/common': 1.5.0
1771
1772
+
'@codemirror/commands@6.10.1':
1773
dependencies:
1774
+
'@codemirror/language': 6.12.1
1775
+
'@codemirror/state': 6.5.3
1776
+
'@codemirror/view': 6.39.7
1777
+
'@lezer/common': 1.5.0
1778
1779
'@codemirror/lang-json@6.0.2':
1780
dependencies:
1781
+
'@codemirror/language': 6.12.1
1782
'@lezer/json': 1.0.3
1783
1784
+
'@codemirror/language@6.12.1':
1785
dependencies:
1786
+
'@codemirror/state': 6.5.3
1787
+
'@codemirror/view': 6.39.7
1788
+
'@lezer/common': 1.5.0
1789
'@lezer/highlight': 1.2.3
1790
+
'@lezer/lr': 1.4.5
1791
style-mod: 4.1.3
1792
1793
'@codemirror/lint@6.9.2':
1794
dependencies:
1795
+
'@codemirror/state': 6.5.3
1796
+
'@codemirror/view': 6.39.7
1797
crelt: 1.0.6
1798
1799
'@codemirror/search@6.5.11':
1800
dependencies:
1801
+
'@codemirror/state': 6.5.3
1802
+
'@codemirror/view': 6.39.7
1803
crelt: 1.0.6
1804
1805
+
'@codemirror/state@6.5.3':
1806
dependencies:
1807
'@marijn/find-cluster-break': 1.0.2
1808
1809
+
'@codemirror/view@6.39.7':
1810
dependencies:
1811
+
'@codemirror/state': 6.5.3
1812
crelt: 1.0.6
1813
style-mod: 4.1.3
1814
w3c-keyname: 2.2.8
···
1820
'@esbuild/aix-ppc64@0.23.1':
1821
optional: true
1822
1823
+
'@esbuild/aix-ppc64@0.27.2':
1824
optional: true
1825
1826
'@esbuild/android-arm64@0.23.1':
1827
optional: true
1828
1829
+
'@esbuild/android-arm64@0.27.2':
1830
optional: true
1831
1832
'@esbuild/android-arm@0.23.1':
1833
optional: true
1834
1835
+
'@esbuild/android-arm@0.27.2':
1836
optional: true
1837
1838
'@esbuild/android-x64@0.23.1':
1839
optional: true
1840
1841
+
'@esbuild/android-x64@0.27.2':
1842
optional: true
1843
1844
'@esbuild/darwin-arm64@0.23.1':
1845
optional: true
1846
1847
+
'@esbuild/darwin-arm64@0.27.2':
1848
optional: true
1849
1850
'@esbuild/darwin-x64@0.23.1':
1851
optional: true
1852
1853
+
'@esbuild/darwin-x64@0.27.2':
1854
optional: true
1855
1856
'@esbuild/freebsd-arm64@0.23.1':
1857
optional: true
1858
1859
+
'@esbuild/freebsd-arm64@0.27.2':
1860
optional: true
1861
1862
'@esbuild/freebsd-x64@0.23.1':
1863
optional: true
1864
1865
+
'@esbuild/freebsd-x64@0.27.2':
1866
optional: true
1867
1868
'@esbuild/linux-arm64@0.23.1':
1869
optional: true
1870
1871
+
'@esbuild/linux-arm64@0.27.2':
1872
optional: true
1873
1874
'@esbuild/linux-arm@0.23.1':
1875
optional: true
1876
1877
+
'@esbuild/linux-arm@0.27.2':
1878
optional: true
1879
1880
'@esbuild/linux-ia32@0.23.1':
1881
optional: true
1882
1883
+
'@esbuild/linux-ia32@0.27.2':
1884
optional: true
1885
1886
'@esbuild/linux-loong64@0.23.1':
1887
optional: true
1888
1889
+
'@esbuild/linux-loong64@0.27.2':
1890
optional: true
1891
1892
'@esbuild/linux-mips64el@0.23.1':
1893
optional: true
1894
1895
+
'@esbuild/linux-mips64el@0.27.2':
1896
optional: true
1897
1898
'@esbuild/linux-ppc64@0.23.1':
1899
optional: true
1900
1901
+
'@esbuild/linux-ppc64@0.27.2':
1902
optional: true
1903
1904
'@esbuild/linux-riscv64@0.23.1':
1905
optional: true
1906
1907
+
'@esbuild/linux-riscv64@0.27.2':
1908
optional: true
1909
1910
'@esbuild/linux-s390x@0.23.1':
1911
optional: true
1912
1913
+
'@esbuild/linux-s390x@0.27.2':
1914
optional: true
1915
1916
'@esbuild/linux-x64@0.23.1':
1917
optional: true
1918
1919
+
'@esbuild/linux-x64@0.27.2':
1920
optional: true
1921
1922
+
'@esbuild/netbsd-arm64@0.27.2':
1923
optional: true
1924
1925
'@esbuild/netbsd-x64@0.23.1':
1926
optional: true
1927
1928
+
'@esbuild/netbsd-x64@0.27.2':
1929
optional: true
1930
1931
'@esbuild/openbsd-arm64@0.23.1':
1932
optional: true
1933
1934
+
'@esbuild/openbsd-arm64@0.27.2':
1935
optional: true
1936
1937
'@esbuild/openbsd-x64@0.23.1':
1938
optional: true
1939
1940
+
'@esbuild/openbsd-x64@0.27.2':
1941
optional: true
1942
1943
+
'@esbuild/openharmony-arm64@0.27.2':
1944
optional: true
1945
1946
'@esbuild/sunos-x64@0.23.1':
1947
optional: true
1948
1949
+
'@esbuild/sunos-x64@0.27.2':
1950
optional: true
1951
1952
'@esbuild/win32-arm64@0.23.1':
1953
optional: true
1954
1955
+
'@esbuild/win32-arm64@0.27.2':
1956
optional: true
1957
1958
'@esbuild/win32-ia32@0.23.1':
1959
optional: true
1960
1961
+
'@esbuild/win32-ia32@0.27.2':
1962
optional: true
1963
1964
'@esbuild/win32-x64@0.23.1':
1965
optional: true
1966
1967
+
'@esbuild/win32-x64@0.27.2':
1968
optional: true
1969
1970
+
'@fsegurai/codemirror-theme-basic-dark@6.2.3(@codemirror/language@6.12.1)(@codemirror/state@6.5.3)(@codemirror/view@6.39.7)(@lezer/highlight@1.2.3)':
1971
dependencies:
1972
+
'@codemirror/language': 6.12.1
1973
+
'@codemirror/state': 6.5.3
1974
+
'@codemirror/view': 6.39.7
1975
'@lezer/highlight': 1.2.3
1976
1977
+
'@fsegurai/codemirror-theme-basic-light@6.2.3(@codemirror/language@6.12.1)(@codemirror/state@6.5.3)(@codemirror/view@6.39.7)(@lezer/highlight@1.2.3)':
1978
dependencies:
1979
+
'@codemirror/language': 6.12.1
1980
+
'@codemirror/state': 6.5.3
1981
+
'@codemirror/view': 6.39.7
1982
'@lezer/highlight': 1.2.3
1983
1984
+
'@iconify-json/lucide@1.2.82':
1985
dependencies:
1986
'@iconify/types': 2.0.0
1987
1988
+
'@iconify/tailwind4@1.2.0(tailwindcss@4.1.18)':
1989
dependencies:
1990
+
'@iconify/tools': 5.0.1
1991
'@iconify/types': 2.0.0
1992
'@iconify/utils': 3.1.0
1993
+
tailwindcss: 4.1.18
1994
1995
+
'@iconify/tools@5.0.1':
1996
dependencies:
1997
'@cyberalien/svg-utils': 1.0.11
1998
'@iconify/types': 2.0.0
1999
'@iconify/utils': 3.1.0
2000
fflate: 0.8.2
2001
+
modern-tar: 0.7.3
2002
pathe: 2.0.3
2003
svgo: 4.0.0
2004
···
2031
2032
'@jsr/mary__exif-rm@0.2.2': {}
2033
2034
+
'@lezer/common@1.5.0': {}
2035
2036
'@lezer/highlight@1.2.3':
2037
dependencies:
2038
+
'@lezer/common': 1.5.0
2039
2040
'@lezer/json@1.0.3':
2041
dependencies:
2042
+
'@lezer/common': 1.5.0
2043
'@lezer/highlight': 1.2.3
2044
+
'@lezer/lr': 1.4.5
2045
2046
+
'@lezer/lr@1.4.5':
2047
dependencies:
2048
+
'@lezer/common': 1.5.0
2049
2050
'@marijn/find-cluster-break@1.0.2': {}
2051
2052
'@noble/secp256k1@3.0.0': {}
2053
2054
+
'@rollup/rollup-android-arm-eabi@4.54.0':
2055
optional: true
2056
2057
+
'@rollup/rollup-android-arm64@4.54.0':
2058
optional: true
2059
2060
+
'@rollup/rollup-darwin-arm64@4.54.0':
2061
optional: true
2062
2063
+
'@rollup/rollup-darwin-x64@4.54.0':
2064
optional: true
2065
2066
+
'@rollup/rollup-freebsd-arm64@4.54.0':
2067
optional: true
2068
2069
+
'@rollup/rollup-freebsd-x64@4.54.0':
2070
optional: true
2071
2072
+
'@rollup/rollup-linux-arm-gnueabihf@4.54.0':
2073
optional: true
2074
2075
+
'@rollup/rollup-linux-arm-musleabihf@4.54.0':
2076
optional: true
2077
2078
+
'@rollup/rollup-linux-arm64-gnu@4.54.0':
2079
optional: true
2080
2081
+
'@rollup/rollup-linux-arm64-musl@4.54.0':
2082
optional: true
2083
2084
+
'@rollup/rollup-linux-loong64-gnu@4.54.0':
2085
optional: true
2086
2087
+
'@rollup/rollup-linux-ppc64-gnu@4.54.0':
2088
optional: true
2089
2090
+
'@rollup/rollup-linux-riscv64-gnu@4.54.0':
2091
optional: true
2092
2093
+
'@rollup/rollup-linux-riscv64-musl@4.54.0':
2094
optional: true
2095
2096
+
'@rollup/rollup-linux-s390x-gnu@4.54.0':
2097
optional: true
2098
2099
+
'@rollup/rollup-linux-x64-gnu@4.54.0':
2100
optional: true
2101
2102
+
'@rollup/rollup-linux-x64-musl@4.54.0':
2103
optional: true
2104
2105
+
'@rollup/rollup-openharmony-arm64@4.54.0':
2106
optional: true
2107
2108
+
'@rollup/rollup-win32-arm64-msvc@4.54.0':
2109
optional: true
2110
2111
+
'@rollup/rollup-win32-ia32-msvc@4.54.0':
2112
optional: true
2113
2114
+
'@rollup/rollup-win32-x64-gnu@4.54.0':
2115
optional: true
2116
2117
+
'@rollup/rollup-win32-x64-msvc@4.54.0':
2118
optional: true
2119
2120
'@skyware/firehose@0.5.2':
···
2131
dependencies:
2132
solid-js: 1.9.10
2133
2134
+
'@standard-schema/spec@1.1.0': {}
2135
2136
+
'@tailwindcss/node@4.1.18':
2137
dependencies:
2138
'@jridgewell/remapping': 2.3.5
2139
+
enhanced-resolve: 5.18.4
2140
jiti: 2.6.1
2141
lightningcss: 1.30.2
2142
magic-string: 0.30.21
2143
source-map-js: 1.2.1
2144
+
tailwindcss: 4.1.18
2145
2146
+
'@tailwindcss/oxide-android-arm64@4.1.18':
2147
optional: true
2148
2149
+
'@tailwindcss/oxide-darwin-arm64@4.1.18':
2150
optional: true
2151
2152
+
'@tailwindcss/oxide-darwin-x64@4.1.18':
2153
optional: true
2154
2155
+
'@tailwindcss/oxide-freebsd-x64@4.1.18':
2156
optional: true
2157
2158
+
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18':
2159
optional: true
2160
2161
+
'@tailwindcss/oxide-linux-arm64-gnu@4.1.18':
2162
optional: true
2163
2164
+
'@tailwindcss/oxide-linux-arm64-musl@4.1.18':
2165
optional: true
2166
2167
+
'@tailwindcss/oxide-linux-x64-gnu@4.1.18':
2168
optional: true
2169
2170
+
'@tailwindcss/oxide-linux-x64-musl@4.1.18':
2171
optional: true
2172
2173
+
'@tailwindcss/oxide-wasm32-wasi@4.1.18':
2174
optional: true
2175
2176
+
'@tailwindcss/oxide-win32-arm64-msvc@4.1.18':
2177
optional: true
2178
2179
+
'@tailwindcss/oxide-win32-x64-msvc@4.1.18':
2180
optional: true
2181
2182
+
'@tailwindcss/oxide@4.1.18':
2183
optionalDependencies:
2184
+
'@tailwindcss/oxide-android-arm64': 4.1.18
2185
+
'@tailwindcss/oxide-darwin-arm64': 4.1.18
2186
+
'@tailwindcss/oxide-darwin-x64': 4.1.18
2187
+
'@tailwindcss/oxide-freebsd-x64': 4.1.18
2188
+
'@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18
2189
+
'@tailwindcss/oxide-linux-arm64-gnu': 4.1.18
2190
+
'@tailwindcss/oxide-linux-arm64-musl': 4.1.18
2191
+
'@tailwindcss/oxide-linux-x64-gnu': 4.1.18
2192
+
'@tailwindcss/oxide-linux-x64-musl': 4.1.18
2193
+
'@tailwindcss/oxide-wasm32-wasi': 4.1.18
2194
+
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.18
2195
+
'@tailwindcss/oxide-win32-x64-msvc': 4.1.18
2196
2197
+
'@tailwindcss/vite@4.1.18(vite@7.3.0(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2))':
2198
dependencies:
2199
+
'@tailwindcss/node': 4.1.18
2200
+
'@tailwindcss/oxide': 4.1.18
2201
+
tailwindcss: 4.1.18
2202
+
vite: 7.3.0(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)
2203
2204
'@types/babel__core@7.20.5':
2205
dependencies:
···
2224
2225
'@types/estree@1.0.8': {}
2226
2227
+
'@types/node@22.19.3':
2228
+
dependencies:
2229
+
undici-types: 6.21.0
2230
+
2231
'@types/node@24.10.1':
2232
dependencies:
2233
undici-types: 7.16.0
···
2251
optionalDependencies:
2252
solid-js: 1.9.10
2253
2254
+
baseline-browser-mapping@2.9.11: {}
2255
2256
boolbase@1.0.0: {}
2257
2258
+
browserslist@4.28.1:
2259
dependencies:
2260
+
baseline-browser-mapping: 2.9.11
2261
+
caniuse-lite: 1.0.30001761
2262
+
electron-to-chromium: 1.5.267
2263
node-releases: 2.0.27
2264
+
update-browserslist-db: 1.2.3(browserslist@4.28.1)
2265
2266
+
caniuse-lite@1.0.30001761: {}
2267
2268
codemirror@6.0.2:
2269
dependencies:
2270
'@codemirror/autocomplete': 6.20.0
2271
+
'@codemirror/commands': 6.10.1
2272
+
'@codemirror/language': 6.12.1
2273
'@codemirror/lint': 6.9.2
2274
'@codemirror/search': 6.5.11
2275
+
'@codemirror/state': 6.5.3
2276
+
'@codemirror/view': 6.39.7
2277
2278
commander@11.1.0: {}
2279
···
2333
domelementtype: 2.3.0
2334
domhandler: 5.0.3
2335
2336
+
electron-to-chromium@1.5.267: {}
2337
2338
+
enhanced-resolve@5.18.4:
2339
dependencies:
2340
graceful-fs: 4.2.11
2341
tapable: 2.3.0
···
2372
'@esbuild/win32-x64': 0.23.1
2373
optional: true
2374
2375
+
esbuild@0.27.2:
2376
optionalDependencies:
2377
+
'@esbuild/aix-ppc64': 0.27.2
2378
+
'@esbuild/android-arm': 0.27.2
2379
+
'@esbuild/android-arm64': 0.27.2
2380
+
'@esbuild/android-x64': 0.27.2
2381
+
'@esbuild/darwin-arm64': 0.27.2
2382
+
'@esbuild/darwin-x64': 0.27.2
2383
+
'@esbuild/freebsd-arm64': 0.27.2
2384
+
'@esbuild/freebsd-x64': 0.27.2
2385
+
'@esbuild/linux-arm': 0.27.2
2386
+
'@esbuild/linux-arm64': 0.27.2
2387
+
'@esbuild/linux-ia32': 0.27.2
2388
+
'@esbuild/linux-loong64': 0.27.2
2389
+
'@esbuild/linux-mips64el': 0.27.2
2390
+
'@esbuild/linux-ppc64': 0.27.2
2391
+
'@esbuild/linux-riscv64': 0.27.2
2392
+
'@esbuild/linux-s390x': 0.27.2
2393
+
'@esbuild/linux-x64': 0.27.2
2394
+
'@esbuild/netbsd-arm64': 0.27.2
2395
+
'@esbuild/netbsd-x64': 0.27.2
2396
+
'@esbuild/openbsd-arm64': 0.27.2
2397
+
'@esbuild/openbsd-x64': 0.27.2
2398
+
'@esbuild/openharmony-arm64': 0.27.2
2399
+
'@esbuild/sunos-x64': 0.27.2
2400
+
'@esbuild/win32-arm64': 0.27.2
2401
+
'@esbuild/win32-ia32': 0.27.2
2402
+
'@esbuild/win32-x64': 0.27.2
2403
2404
escalade@3.2.0: {}
2405
···
2507
pkg-types: 1.3.1
2508
ufo: 1.6.1
2509
2510
+
modern-tar@0.7.3: {}
2511
2512
ms@2.1.3: {}
2513
···
2516
nanoid@3.3.11: {}
2517
2518
nanoid@5.1.6: {}
2519
+
2520
+
node-gyp-build@4.8.4: {}
2521
2522
node-releases@2.0.27: {}
2523
···
2549
picocolors: 1.1.1
2550
source-map-js: 1.2.1
2551
2552
+
prettier-plugin-organize-imports@4.3.0(prettier@3.7.4)(typescript@5.9.3):
2553
dependencies:
2554
+
prettier: 3.7.4
2555
typescript: 5.9.3
2556
2557
+
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):
2558
dependencies:
2559
+
prettier: 3.7.4
2560
optionalDependencies:
2561
+
prettier-plugin-organize-imports: 4.3.0(prettier@3.7.4)(typescript@5.9.3)
2562
2563
+
prettier@3.7.4: {}
2564
2565
resolve-pkg-maps@1.0.0:
2566
optional: true
2567
2568
+
rollup@4.54.0:
2569
dependencies:
2570
'@types/estree': 1.0.8
2571
optionalDependencies:
2572
+
'@rollup/rollup-android-arm-eabi': 4.54.0
2573
+
'@rollup/rollup-android-arm64': 4.54.0
2574
+
'@rollup/rollup-darwin-arm64': 4.54.0
2575
+
'@rollup/rollup-darwin-x64': 4.54.0
2576
+
'@rollup/rollup-freebsd-arm64': 4.54.0
2577
+
'@rollup/rollup-freebsd-x64': 4.54.0
2578
+
'@rollup/rollup-linux-arm-gnueabihf': 4.54.0
2579
+
'@rollup/rollup-linux-arm-musleabihf': 4.54.0
2580
+
'@rollup/rollup-linux-arm64-gnu': 4.54.0
2581
+
'@rollup/rollup-linux-arm64-musl': 4.54.0
2582
+
'@rollup/rollup-linux-loong64-gnu': 4.54.0
2583
+
'@rollup/rollup-linux-ppc64-gnu': 4.54.0
2584
+
'@rollup/rollup-linux-riscv64-gnu': 4.54.0
2585
+
'@rollup/rollup-linux-riscv64-musl': 4.54.0
2586
+
'@rollup/rollup-linux-s390x-gnu': 4.54.0
2587
+
'@rollup/rollup-linux-x64-gnu': 4.54.0
2588
+
'@rollup/rollup-linux-x64-musl': 4.54.0
2589
+
'@rollup/rollup-openharmony-arm64': 4.54.0
2590
+
'@rollup/rollup-win32-arm64-msvc': 4.54.0
2591
+
'@rollup/rollup-win32-ia32-msvc': 4.54.0
2592
+
'@rollup/rollup-win32-x64-gnu': 4.54.0
2593
+
'@rollup/rollup-win32-x64-msvc': 4.54.0
2594
fsevents: 2.3.3
2595
2596
sax@1.4.3: {}
···
2632
picocolors: 1.1.1
2633
sax: 1.4.3
2634
2635
+
tailwindcss@4.1.18: {}
2636
2637
tapable@2.3.0: {}
2638
···
2655
2656
ufo@1.6.1: {}
2657
2658
+
undici-types@6.21.0: {}
2659
+
2660
undici-types@7.16.0:
2661
optional: true
2662
2663
+
unicode-segmenter@0.14.4: {}
2664
+
2665
+
update-browserslist-db@1.2.3(browserslist@4.28.1):
2666
dependencies:
2667
+
browserslist: 4.28.1
2668
escalade: 3.2.0
2669
picocolors: 1.1.1
2670
2671
+
vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.3.0(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)):
2672
dependencies:
2673
'@babel/core': 7.28.5
2674
'@types/babel__core': 7.20.5
···
2676
merge-anything: 5.1.7
2677
solid-js: 1.9.10
2678
solid-refresh: 0.6.3(solid-js@1.9.10)
2679
+
vite: 7.3.0(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)
2680
+
vitefu: 1.1.1(vite@7.3.0(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2))
2681
transitivePeerDependencies:
2682
- supports-color
2683
2684
+
vite@7.3.0(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2):
2685
dependencies:
2686
+
esbuild: 0.27.2
2687
fdir: 6.5.0(picomatch@4.0.3)
2688
picomatch: 4.0.3
2689
postcss: 8.5.6
2690
+
rollup: 4.54.0
2691
tinyglobby: 0.2.15
2692
optionalDependencies:
2693
'@types/node': 24.10.1
···
2696
lightningcss: 1.30.2
2697
tsx: 4.19.2
2698
2699
+
vitefu@1.1.1(vite@7.3.0(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)):
2700
optionalDependencies:
2701
+
vite: 7.3.0(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)
2702
2703
w3c-keyname@2.2.8: {}
2704
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--circle-check shrink-0 text-blue-500 dark:text-blue-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 flex grow items-center gap-2 rounded-lg bg-white px-2 outline-1 outline-neutral-200 focus-within:outline-[1.5px] focus-within:outline-neutral-600 dark:outline-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 w-full items-center justify-between">
104
-
<A
105
-
href={`/at://${did}`}
106
-
onClick={() => setOpenManager(false)}
107
-
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"
108
-
>
109
-
<Show
110
-
when={avatars[did as Did]}
111
-
fallback={<span class="iconify lucide--user-round m-0.5 size-5"></span>}
112
-
>
113
-
<img
114
-
src={avatars[did as Did].replace("img/avatar/", "img/avatar_thumbnail/")}
115
-
class="size-6 rounded-full"
116
-
/>
117
-
</Show>
118
-
</A>
119
-
<button
120
-
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"
121
-
onclick={() => resumeSession(did as Did)}
122
-
>
123
-
<span class="truncate">
124
-
{sessions[did]?.handle ? sessions[did].handle : did}
125
-
</span>
126
-
<Show when={did === agent()?.sub && sessions[did].signedIn}>
127
-
<span class="iconify lucide--check shrink-0 text-green-500 dark:text-green-400"></span>
128
-
</Show>
129
-
<Show when={!sessions[did].signedIn}>
130
-
<span class="iconify lucide--circle-alert shrink-0 text-red-500 dark:text-red-400"></span>
131
-
</Show>
132
-
</button>
133
-
<button
134
-
onclick={() => removeSession(did as Did)}
135
-
class="flex items-center rounded-md 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
-
};
···
+117
-173
src/components/backlinks.tsx
+117
-173
src/components/backlinks.tsx
···
1
import * as TID from "@atcute/tid";
2
import { createResource, createSignal, For, onMount, Show } from "solid-js";
3
-
import {
4
-
getAllBacklinks,
5
-
getDidBacklinks,
6
-
getRecordBacklinks,
7
-
LinksWithDids,
8
-
LinksWithRecords,
9
-
} from "../utils/api.js";
10
import { localDateFromTimestamp } from "../utils/date.js";
11
import { Button } from "./button.jsx";
12
13
-
type Backlink = {
14
path: string;
15
counts: { distinct_dids: number; records: number };
16
};
17
18
-
const linksBySource = (links: Record<string, any>) => {
19
-
let out: Record<string, Backlink[]> = {};
20
Object.keys(links)
21
.toSorted()
22
.forEach((collection) => {
···
24
Object.keys(paths)
25
.toSorted()
26
.forEach((path) => {
27
-
if (paths[path].records === 0) return;
28
-
if (out[collection]) out[collection].push({ path, counts: paths[path] });
29
-
else out[collection] = [{ path, counts: paths[path] }];
30
});
31
});
32
-
return out;
33
};
34
35
-
const Backlinks = (props: { target: string }) => {
36
-
const fetchBacklinks = async () => {
37
-
const res = await getAllBacklinks(props.target);
38
-
return linksBySource(res.links);
39
-
};
40
41
-
const [response] = createResource(fetchBacklinks);
42
-
43
-
const [show, setShow] = createSignal<{
44
-
collection: string;
45
-
path: string;
46
-
showDids: boolean;
47
-
} | null>();
48
49
return (
50
-
<div class="flex w-full flex-col gap-1 text-sm wrap-anywhere">
51
-
<Show
52
-
when={response() && Object.keys(response()!).length}
53
-
fallback={<p>No backlinks found.</p>}
54
-
>
55
-
<For each={Object.keys(response()!)}>
56
-
{(collection) => (
57
-
<div>
58
-
<div class="flex items-center gap-1">
59
-
<span class="iconify lucide--book-text shrink-0"></span>
60
-
{collection}
61
-
</div>
62
-
<For each={response()![collection]}>
63
-
{({ path, counts }) => (
64
-
<div class="ml-4.5">
65
-
<div class="flex items-center gap-1">
66
-
<span class="iconify lucide--route shrink-0"></span>
67
-
{path.slice(1)}
68
-
</div>
69
-
<div class="ml-4.5">
70
-
<p>
71
-
<button
72
-
class="text-blue-400 hover:underline active:underline"
73
-
onclick={() =>
74
-
(
75
-
show()?.collection === collection &&
76
-
show()?.path === path &&
77
-
!show()?.showDids
78
-
) ?
79
-
setShow(null)
80
-
: setShow({ collection, path, showDids: false })
81
-
}
82
-
>
83
-
{counts.records} record{counts.records < 2 ? "" : "s"}
84
-
</button>
85
-
{" from "}
86
-
<button
87
-
class="text-blue-400 hover:underline active:underline"
88
-
onclick={() =>
89
-
(
90
-
show()?.collection === collection &&
91
-
show()?.path === path &&
92
-
show()?.showDids
93
-
) ?
94
-
setShow(null)
95
-
: setShow({ collection, path, showDids: true })
96
-
}
97
-
>
98
-
{counts.distinct_dids} DID
99
-
{counts.distinct_dids < 2 ? "" : "s"}
100
-
</button>
101
-
</p>
102
-
<Show when={show()?.collection === collection && show()?.path === path}>
103
-
<Show when={show()?.showDids}>
104
-
<p class="w-full font-semibold">Distinct identities</p>
105
-
<BacklinkItems
106
-
target={props.target}
107
-
collection={collection}
108
-
path={path}
109
-
dids={true}
110
-
/>
111
-
</Show>
112
-
<Show when={!show()?.showDids}>
113
-
<p class="w-full font-semibold">Records</p>
114
-
<BacklinkItems
115
-
target={props.target}
116
-
collection={collection}
117
-
path={path}
118
-
dids={false}
119
-
/>
120
-
</Show>
121
-
</Show>
122
-
</div>
123
-
</div>
124
-
)}
125
-
</For>
126
</div>
127
-
)}
128
-
</For>
129
</Show>
130
-
</div>
131
);
132
};
133
134
-
// switching on !!did everywhere is pretty annoying, this could probably be two components
135
-
// but i don't want to duplicate or think about how to extract the paging logic
136
-
const BacklinkItems = ({
137
-
target,
138
-
collection,
139
-
path,
140
-
dids,
141
-
cursor,
142
-
}: {
143
-
target: string;
144
-
collection: string;
145
-
path: string;
146
-
dids: boolean;
147
-
cursor?: string;
148
-
}) => {
149
-
const [links, setLinks] = createSignal<LinksWithDids | LinksWithRecords>();
150
-
const [more, setMore] = createSignal<boolean>(false);
151
-
152
-
onMount(async () => {
153
-
const links = await (dids ? getDidBacklinks : getRecordBacklinks)(
154
-
target,
155
-
collection,
156
-
path,
157
-
cursor,
158
-
);
159
-
setLinks(links);
160
});
161
162
-
// TODO: could pass the `total` into this component, which can be checked against each call to this endpoint to find if it's stale.
163
-
// also hmm 'total' is misleading/wrong on that api
164
-
165
return (
166
-
<Show when={links()} fallback={<p>Loading…</p>}>
167
-
<Show when={dids}>
168
-
<For each={(links() as LinksWithDids).linking_dids}>
169
-
{(did) => (
170
-
<a
171
-
href={`/at://${did}`}
172
-
class="relative flex w-full font-mono text-blue-400 hover:underline active:underline"
173
-
>
174
-
{did}
175
-
</a>
176
)}
177
</For>
178
</Show>
179
-
<Show when={!dids}>
180
-
<For each={(links() as LinksWithRecords).linking_records}>
181
-
{({ did, collection, rkey }) => (
182
-
<p class="relative flex w-full items-center gap-1 font-mono">
183
-
<a
184
-
href={`/at://${did}/${collection}/${rkey}`}
185
-
class="text-blue-400 hover:underline active:underline"
186
-
>
187
-
{rkey}
188
-
</a>
189
-
<span class="text-xs text-neutral-500 dark:text-neutral-400">
190
-
{TID.validate(rkey) ?
191
-
localDateFromTimestamp(TID.parse(rkey).timestamp / 1000)
192
-
: undefined}
193
-
</span>
194
-
</p>
195
-
)}
196
-
</For>
197
-
</Show>
198
-
<Show when={links()?.cursor}>
199
-
<Show when={more()} fallback={<Button onClick={() => setMore(true)}>Load More</Button>}>
200
-
<BacklinkItems
201
-
target={target}
202
-
collection={collection}
203
-
path={path}
204
-
dids={dids}
205
-
cursor={links()!.cursor}
206
/>
207
-
</Show>
208
</Show>
209
-
</Show>
210
);
211
};
212
···
1
import * as TID from "@atcute/tid";
2
import { createResource, createSignal, For, onMount, Show } from "solid-js";
3
+
import { getAllBacklinks, getRecordBacklinks, LinksWithRecords } from "../utils/api.js";
4
import { localDateFromTimestamp } from "../utils/date.js";
5
import { Button } from "./button.jsx";
6
7
+
type BacklinksProps = {
8
+
target: string;
9
+
collection: string;
10
+
path: string;
11
+
};
12
+
13
+
type BacklinkEntry = {
14
+
collection: string;
15
path: string;
16
counts: { distinct_dids: number; records: number };
17
};
18
19
+
const flattenLinks = (links: Record<string, any>): BacklinkEntry[] => {
20
+
const entries: BacklinkEntry[] = [];
21
Object.keys(links)
22
.toSorted()
23
.forEach((collection) => {
···
25
Object.keys(paths)
26
.toSorted()
27
.forEach((path) => {
28
+
if (paths[path].records > 0) {
29
+
entries.push({ collection, path, counts: paths[path] });
30
+
}
31
});
32
});
33
+
return entries;
34
};
35
36
+
const BacklinkRecords = (props: BacklinksProps & { cursor?: string }) => {
37
+
const [links, setLinks] = createSignal<LinksWithRecords>();
38
+
const [more, setMore] = createSignal(false);
39
40
+
onMount(async () => {
41
+
const res = await getRecordBacklinks(props.target, props.collection, props.path, props.cursor);
42
+
setLinks(res);
43
+
});
44
45
return (
46
+
<Show when={links()} fallback={<p class="px-3 py-2 text-neutral-500">Loadingโฆ</p>}>
47
+
<For each={links()!.linking_records}>
48
+
{({ did, collection, rkey }) => {
49
+
const timestamp =
50
+
TID.validate(rkey) ? localDateFromTimestamp(TID.parse(rkey).timestamp / 1000) : null;
51
+
return (
52
+
<a
53
+
href={`/at://${did}/${collection}/${rkey}`}
54
+
class="grid grid-cols-[auto_1fr_auto] items-center gap-x-1 px-2 py-1.5 font-mono text-xs select-none hover:bg-neutral-200/50 active:bg-neutral-200/50 sm:gap-x-3 sm:px-3 dark:hover:bg-neutral-700/50 dark:active:bg-neutral-700/50"
55
+
>
56
+
<span class="text-blue-500 dark:text-blue-400">{rkey}</span>
57
+
<span class="truncate text-neutral-700 dark:text-neutral-300" title={did}>
58
+
{did}
59
+
</span>
60
+
<span class="text-neutral-500 tabular-nums dark:text-neutral-400">
61
+
{timestamp ?? ""}
62
+
</span>
63
+
</a>
64
+
);
65
+
}}
66
+
</For>
67
+
<Show when={links()?.cursor}>
68
+
<Show
69
+
when={more()}
70
+
fallback={
71
+
<div class="p-2">
72
+
<Button
73
+
onClick={() => setMore(true)}
74
+
class="dark:hover:bg-dark-200 dark:shadow-dark-700 dark:active:bg-dark-100 box-border flex h-7 w-full items-center justify-center gap-1 rounded border-[0.5px] border-neutral-300 bg-neutral-50 px-2 py-1.5 text-xs shadow-xs select-none hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800"
75
+
>
76
+
Load More
77
+
</Button>
78
</div>
79
+
}
80
+
>
81
+
<BacklinkRecords
82
+
target={props.target}
83
+
collection={props.collection}
84
+
path={props.path}
85
+
cursor={links()!.cursor}
86
+
/>
87
+
</Show>
88
</Show>
89
+
</Show>
90
);
91
};
92
93
+
const Backlinks = (props: { target: string }) => {
94
+
const [response] = createResource(async () => {
95
+
const res = await getAllBacklinks(props.target);
96
+
return flattenLinks(res.links);
97
});
98
99
return (
100
+
<div class="flex w-full flex-col gap-3 text-sm">
101
+
<Show when={response()} fallback={<p class="text-neutral-500">Loadingโฆ</p>}>
102
+
<Show when={response()!.length === 0}>
103
+
<p class="text-neutral-500">No backlinks found.</p>
104
+
</Show>
105
+
<For each={response()}>
106
+
{(entry) => (
107
+
<BacklinkSection
108
+
target={props.target}
109
+
collection={entry.collection}
110
+
path={entry.path}
111
+
counts={entry.counts}
112
+
/>
113
)}
114
</For>
115
</Show>
116
+
</div>
117
+
);
118
+
};
119
+
120
+
const BacklinkSection = (
121
+
props: BacklinksProps & { counts: { distinct_dids: number; records: number } },
122
+
) => {
123
+
const [expanded, setExpanded] = createSignal(false);
124
+
125
+
return (
126
+
<div class="overflow-hidden rounded-lg border border-neutral-200 dark:border-neutral-700">
127
+
<button
128
+
class="flex w-full items-center justify-between gap-3 px-3 py-2 text-left hover:bg-neutral-50 dark:hover:bg-neutral-800/50"
129
+
onClick={() => setExpanded(!expanded())}
130
+
>
131
+
<div class="flex min-w-0 flex-1 flex-col">
132
+
<span class="w-full truncate">{props.collection}</span>
133
+
<span class="w-full text-xs wrap-break-word text-neutral-500 dark:text-neutral-400">
134
+
{props.path.slice(1)}
135
+
</span>
136
+
</div>
137
+
<div class="flex shrink-0 items-center gap-2 text-neutral-700 dark:text-neutral-300">
138
+
<span class="text-xs">
139
+
{props.counts.records} from {props.counts.distinct_dids} repo
140
+
{props.counts.distinct_dids > 1 ? "s" : ""}
141
+
</span>
142
+
<span
143
+
class="iconify lucide--chevron-down transition-transform"
144
+
classList={{ "rotate-180": expanded() }}
145
/>
146
+
</div>
147
+
</button>
148
+
<Show when={expanded()}>
149
+
<div class="border-t border-neutral-200 bg-neutral-50/50 dark:border-neutral-700 dark:bg-neutral-800/30">
150
+
<BacklinkRecords target={props.target} collection={props.collection} path={props.path} />
151
+
</div>
152
</Show>
153
+
</div>
154
);
155
};
156
+90
src/components/create/confirm-submit.tsx
+90
src/components/create/confirm-submit.tsx
···
···
1
+
import { createSignal, Show } from "solid-js";
2
+
import { Button } from "../button.jsx";
3
+
4
+
export const ConfirmSubmit = (props: {
5
+
isCreate: boolean;
6
+
onConfirm: (validate: boolean | undefined, recreate: boolean) => void;
7
+
onClose: () => void;
8
+
}) => {
9
+
const [validate, setValidate] = createSignal<boolean | undefined>(undefined);
10
+
const [recreate, setRecreate] = createSignal(false);
11
+
12
+
const getValidateLabel = () => {
13
+
return (
14
+
validate() === true ? "True"
15
+
: validate() === false ? "False"
16
+
: "Unset"
17
+
);
18
+
};
19
+
20
+
const cycleValidate = () => {
21
+
setValidate(
22
+
validate() === undefined ? true
23
+
: validate() === true ? false
24
+
: undefined,
25
+
);
26
+
};
27
+
28
+
return (
29
+
<div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-70 left-[50%] w-[24rem] -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">
30
+
<div class="flex flex-col gap-3 text-sm">
31
+
<h2 class="font-semibold">{props.isCreate ? "Create" : "Edit"} record</h2>
32
+
<div class="flex flex-col gap-1.5">
33
+
<div class="flex items-center gap-2">
34
+
<button
35
+
type="button"
36
+
class="-ml-2 flex min-w-30 items-center gap-1.5 rounded-lg px-2 py-1 text-xs hover:bg-neutral-200/50 dark:hover:bg-neutral-700"
37
+
onClick={cycleValidate}
38
+
>
39
+
<span
40
+
classList={{
41
+
iconify: true,
42
+
"lucide--square-check text-green-500 dark:text-green-400": validate() === true,
43
+
"lucide--square-x text-red-500 dark:text-red-400": validate() === false,
44
+
"lucide--square text-neutral-500 dark:text-neutral-400": validate() === undefined,
45
+
}}
46
+
></span>
47
+
<span>Validate: {getValidateLabel()}</span>
48
+
</button>
49
+
</div>
50
+
<p class="text-xs text-neutral-600 dark:text-neutral-400">
51
+
Set to 'false' to skip lexicon schema validation by the PDS, 'true' to require it, or
52
+
leave unset to validate only for known lexicons.
53
+
</p>
54
+
</div>
55
+
<Show when={!props.isCreate}>
56
+
<div class="flex flex-col gap-1.5">
57
+
<div class="flex items-center gap-2">
58
+
<button
59
+
type="button"
60
+
class="-ml-2 flex items-center gap-1.5 rounded-lg px-2 py-1 text-xs hover:bg-neutral-200/50 dark:hover:bg-neutral-700"
61
+
onClick={() => setRecreate(!recreate())}
62
+
>
63
+
<span
64
+
classList={{
65
+
iconify: true,
66
+
"lucide--square-check text-green-500 dark:text-green-400": recreate(),
67
+
"lucide--square text-neutral-500 dark:text-neutral-400": !recreate(),
68
+
}}
69
+
></span>
70
+
<span>Recreate</span>
71
+
</button>
72
+
</div>
73
+
<p class="text-xs text-neutral-600 dark:text-neutral-400">
74
+
Delete the existing record and create a new one with the same record key.
75
+
</p>
76
+
</div>
77
+
</Show>
78
+
<div class="flex justify-between gap-2">
79
+
<Button onClick={props.onClose}>Cancel</Button>
80
+
<Button
81
+
onClick={() => props.onConfirm(validate(), recreate())}
82
+
class="dark:shadow-dark-700 min-w-12 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"
83
+
>
84
+
{props.isCreate ? "Create" : "Edit"}
85
+
</Button>
86
+
</div>
87
+
</div>
88
+
</div>
89
+
);
90
+
};
+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
+
};
+460
src/components/create/index.tsx
+460
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 { ConfirmSubmit } from "./confirm-submit";
24
+
import { FileUpload } from "./file-upload";
25
+
import { HandleInput } from "./handle-input";
26
+
import { MenuItem } from "./menu-item";
27
+
import { editorInstance, placeholder, setPlaceholder } from "./state";
28
+
29
+
const Editor = lazy(() => import("../editor.jsx").then((m) => ({ default: m.Editor })));
30
+
31
+
export { editorInstance, placeholder, setPlaceholder };
32
+
33
+
export const RecordEditor = (props: { create: boolean; record?: any; refetch?: any }) => {
34
+
const navigate = useNavigate();
35
+
const params = useParams();
36
+
const [openDialog, setOpenDialog] = createSignal(false);
37
+
const [notice, setNotice] = createSignal("");
38
+
const [openUpload, setOpenUpload] = createSignal(false);
39
+
const [openInsertMenu, setOpenInsertMenu] = createSignal(false);
40
+
const [openHandleDialog, setOpenHandleDialog] = createSignal(false);
41
+
const [openConfirmDialog, setOpenConfirmDialog] = createSignal(false);
42
+
const [isMaximized, setIsMaximized] = createSignal(false);
43
+
const [isMinimized, setIsMinimized] = createSignal(false);
44
+
const [collectionError, setCollectionError] = createSignal("");
45
+
const [rkeyError, setRkeyError] = createSignal("");
46
+
let blobInput!: HTMLInputElement;
47
+
let formRef!: HTMLFormElement;
48
+
let insertMenuRef!: HTMLDivElement;
49
+
50
+
createEffect(() => {
51
+
if (openInsertMenu()) {
52
+
const handleClickOutside = (e: MouseEvent) => {
53
+
if (insertMenuRef && !insertMenuRef.contains(e.target as Node)) {
54
+
setOpenInsertMenu(false);
55
+
}
56
+
};
57
+
document.addEventListener("mousedown", handleClickOutside);
58
+
onCleanup(() => document.removeEventListener("mousedown", handleClickOutside));
59
+
}
60
+
});
61
+
62
+
onMount(() => {
63
+
const keyEvent = (ev: KeyboardEvent) => {
64
+
if (ev.target instanceof HTMLInputElement || ev.target instanceof HTMLTextAreaElement) return;
65
+
if ((ev.target as HTMLElement).closest("[data-modal]")) return;
66
+
67
+
const key = props.create ? "n" : "e";
68
+
if (ev.key === key) {
69
+
ev.preventDefault();
70
+
71
+
if (openDialog() && isMinimized()) {
72
+
setIsMinimized(false);
73
+
} else if (!openDialog() && !document.querySelector("[data-modal]")) {
74
+
setOpenDialog(true);
75
+
}
76
+
}
77
+
};
78
+
79
+
window.addEventListener("keydown", keyEvent);
80
+
onCleanup(() => window.removeEventListener("keydown", keyEvent));
81
+
});
82
+
83
+
const defaultPlaceholder = () => {
84
+
return {
85
+
$type: "app.bsky.feed.post",
86
+
text: "This post was sent from PDSls",
87
+
embed: {
88
+
$type: "app.bsky.embed.external",
89
+
external: {
90
+
uri: "https://pdsls.dev",
91
+
title: "PDSls",
92
+
description: "Browse the public data on atproto",
93
+
},
94
+
},
95
+
langs: ["en"],
96
+
createdAt: new Date().toISOString(),
97
+
};
98
+
};
99
+
100
+
createEffect(() => {
101
+
if (openDialog()) {
102
+
setCollectionError("");
103
+
setRkeyError("");
104
+
}
105
+
});
106
+
107
+
const createRecord = async (validate: boolean | undefined) => {
108
+
const formData = new FormData(formRef);
109
+
const repo = formData.get("repo")?.toString();
110
+
if (!repo) return;
111
+
const rpc = new Client({ handler: new OAuthUserAgent(await getSession(repo as Did)) });
112
+
const collection = formData.get("collection");
113
+
const rkey = formData.get("rkey");
114
+
let record: any;
115
+
try {
116
+
record = JSON.parse(editorInstance.view.state.doc.toString());
117
+
} catch (e: any) {
118
+
setNotice(e.message);
119
+
return;
120
+
}
121
+
const res = await rpc.post("com.atproto.repo.createRecord", {
122
+
input: {
123
+
repo: repo as Did,
124
+
collection: collection ? collection.toString() : record.$type,
125
+
rkey: rkey?.toString().length ? rkey?.toString() : undefined,
126
+
record: record,
127
+
validate: validate,
128
+
},
129
+
});
130
+
if (!res.ok) {
131
+
setNotice(`${res.data.error}: ${res.data.message}`);
132
+
return;
133
+
}
134
+
setOpenConfirmDialog(false);
135
+
setOpenDialog(false);
136
+
const id = addNotification({
137
+
message: "Record created",
138
+
type: "success",
139
+
});
140
+
setTimeout(() => removeNotification(id), 3000);
141
+
navigate(`/${res.data.uri}`);
142
+
};
143
+
144
+
const editRecord = async (validate: boolean | undefined, recreate: boolean) => {
145
+
const record = editorInstance.view.state.doc.toString();
146
+
if (!record) return;
147
+
const rpc = new Client({ handler: agent()! });
148
+
try {
149
+
const editedRecord = JSON.parse(record);
150
+
if (recreate) {
151
+
const res = await rpc.post("com.atproto.repo.applyWrites", {
152
+
input: {
153
+
repo: agent()!.sub,
154
+
validate: validate,
155
+
writes: [
156
+
{
157
+
collection: params.collection as `${string}.${string}.${string}`,
158
+
rkey: params.rkey!,
159
+
$type: "com.atproto.repo.applyWrites#delete",
160
+
},
161
+
{
162
+
collection: params.collection as `${string}.${string}.${string}`,
163
+
rkey: params.rkey,
164
+
$type: "com.atproto.repo.applyWrites#create",
165
+
value: editedRecord,
166
+
},
167
+
],
168
+
},
169
+
});
170
+
if (!res.ok) {
171
+
setNotice(`${res.data.error}: ${res.data.message}`);
172
+
return;
173
+
}
174
+
} else {
175
+
const res = await rpc.post("com.atproto.repo.applyWrites", {
176
+
input: {
177
+
repo: agent()!.sub,
178
+
validate: validate,
179
+
writes: [
180
+
{
181
+
collection: params.collection as `${string}.${string}.${string}`,
182
+
rkey: params.rkey!,
183
+
$type: "com.atproto.repo.applyWrites#update",
184
+
value: editedRecord,
185
+
},
186
+
],
187
+
},
188
+
});
189
+
if (!res.ok) {
190
+
setNotice(`${res.data.error}: ${res.data.message}`);
191
+
return;
192
+
}
193
+
}
194
+
setOpenConfirmDialog(false);
195
+
setOpenDialog(false);
196
+
const id = addNotification({
197
+
message: "Record edited",
198
+
type: "success",
199
+
});
200
+
setTimeout(() => removeNotification(id), 3000);
201
+
props.refetch();
202
+
} catch (err: any) {
203
+
setNotice(err.message);
204
+
}
205
+
};
206
+
207
+
const insertTimestamp = () => {
208
+
const timestamp = new Date().toISOString();
209
+
editorInstance.view.dispatch({
210
+
changes: {
211
+
from: editorInstance.view.state.selection.main.head,
212
+
insert: `"${timestamp}"`,
213
+
},
214
+
});
215
+
setOpenInsertMenu(false);
216
+
};
217
+
218
+
const insertDidFromHandle = () => {
219
+
setOpenInsertMenu(false);
220
+
setOpenHandleDialog(true);
221
+
};
222
+
223
+
return (
224
+
<>
225
+
<Modal
226
+
open={openDialog()}
227
+
onClose={() => setOpenDialog(false)}
228
+
closeOnClick={false}
229
+
nonBlocking={isMinimized()}
230
+
>
231
+
<div
232
+
style="transform: translateX(-50%) translateZ(0);"
233
+
classList={{
234
+
"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,
235
+
"w-[calc(100%-1rem)] max-w-3xl h-[65vh]": !isMaximized(),
236
+
"w-[calc(100%-1rem)] max-w-7xl h-[85vh]": isMaximized(),
237
+
hidden: isMinimized(),
238
+
}}
239
+
>
240
+
<div class="mb-2 flex w-full justify-between text-base">
241
+
<div class="flex items-center gap-2">
242
+
<span class="font-semibold select-none">
243
+
{props.create ? "Creating" : "Editing"} record
244
+
</span>
245
+
</div>
246
+
<div class="flex items-center gap-1">
247
+
<button
248
+
type="button"
249
+
onclick={() => setIsMinimized(true)}
250
+
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"
251
+
>
252
+
<span class="iconify lucide--minus"></span>
253
+
</button>
254
+
<button
255
+
type="button"
256
+
onclick={() => setIsMaximized(!isMaximized())}
257
+
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"
258
+
>
259
+
<span
260
+
class={`iconify ${isMaximized() ? "lucide--minimize-2" : "lucide--maximize-2"}`}
261
+
></span>
262
+
</button>
263
+
<button
264
+
id="close"
265
+
onclick={() => setOpenDialog(false)}
266
+
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"
267
+
>
268
+
<span class="iconify lucide--x"></span>
269
+
</button>
270
+
</div>
271
+
</div>
272
+
<form ref={formRef} class="flex min-h-0 flex-1 flex-col gap-y-2">
273
+
<Show when={props.create}>
274
+
<div class="flex flex-wrap items-center gap-1 text-sm">
275
+
<span>at://</span>
276
+
<select
277
+
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"
278
+
name="repo"
279
+
id="repo"
280
+
>
281
+
<For each={Object.keys(sessions)}>
282
+
{(session) => (
283
+
<option value={session} selected={session === agent()?.sub}>
284
+
{sessions[session].handle ?? session}
285
+
</option>
286
+
)}
287
+
</For>
288
+
</select>
289
+
<span>/</span>
290
+
<TextInput
291
+
id="collection"
292
+
name="collection"
293
+
placeholder="Collection (default: $type)"
294
+
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" : ""}`}
295
+
onInput={(e) => {
296
+
const value = e.currentTarget.value;
297
+
if (!value || isNsid(value)) setCollectionError("");
298
+
else
299
+
setCollectionError(
300
+
"Invalid collection: use reverse domain format (e.g. app.bsky.feed.post)",
301
+
);
302
+
}}
303
+
/>
304
+
<span>/</span>
305
+
<TextInput
306
+
id="rkey"
307
+
name="rkey"
308
+
placeholder="Record key (default: TID)"
309
+
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" : ""}`}
310
+
onInput={(e) => {
311
+
const value = e.currentTarget.value;
312
+
if (!value || isRecordKey(value)) setRkeyError("");
313
+
else setRkeyError("Invalid record key: 1-512 chars, use a-z A-Z 0-9 . _ ~ : -");
314
+
}}
315
+
/>
316
+
</div>
317
+
<Show when={collectionError() || rkeyError()}>
318
+
<div class="text-xs text-red-500 dark:text-red-400">
319
+
<div>{collectionError()}</div>
320
+
<div>{rkeyError()}</div>
321
+
</div>
322
+
</Show>
323
+
</Show>
324
+
<div class="min-h-0 flex-1">
325
+
<Suspense
326
+
fallback={
327
+
<div class="flex h-full items-center justify-center">
328
+
<span class="iconify lucide--loader-circle animate-spin text-xl"></span>
329
+
</div>
330
+
}
331
+
>
332
+
<Editor
333
+
content={JSON.stringify(
334
+
!props.create ? props.record
335
+
: params.rkey ? placeholder()
336
+
: defaultPlaceholder(),
337
+
null,
338
+
2,
339
+
)}
340
+
/>
341
+
</Suspense>
342
+
</div>
343
+
<div class="flex flex-col gap-2">
344
+
<Show when={notice()}>
345
+
<div class="text-sm text-red-500 dark:text-red-400">{notice()}</div>
346
+
</Show>
347
+
<div class="flex justify-between gap-2">
348
+
<div class="relative" ref={insertMenuRef}>
349
+
<button
350
+
type="button"
351
+
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"
352
+
onClick={() => setOpenInsertMenu(!openInsertMenu())}
353
+
>
354
+
<span class="iconify lucide--plus select-none"></span>
355
+
</button>
356
+
<Show when={openInsertMenu()}>
357
+
<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">
358
+
<MenuItem
359
+
icon="lucide--id-card"
360
+
label="Insert DID"
361
+
onClick={insertDidFromHandle}
362
+
/>
363
+
<MenuItem
364
+
icon="lucide--clock"
365
+
label="Insert timestamp"
366
+
onClick={insertTimestamp}
367
+
/>
368
+
<Show when={hasUserScope("blob")}>
369
+
<MenuItem
370
+
icon="lucide--upload"
371
+
label="Upload blob"
372
+
onClick={() => {
373
+
setOpenInsertMenu(false);
374
+
blobInput.click();
375
+
}}
376
+
/>
377
+
</Show>
378
+
</div>
379
+
</Show>
380
+
<input
381
+
type="file"
382
+
id="blob"
383
+
class="sr-only"
384
+
ref={blobInput}
385
+
onChange={(e) => {
386
+
if (e.target.files !== null) setOpenUpload(true);
387
+
}}
388
+
/>
389
+
</div>
390
+
<Modal
391
+
open={openUpload()}
392
+
onClose={() => setOpenUpload(false)}
393
+
closeOnClick={false}
394
+
>
395
+
<FileUpload
396
+
file={blobInput.files![0]}
397
+
blobInput={blobInput}
398
+
onClose={() => setOpenUpload(false)}
399
+
/>
400
+
</Modal>
401
+
<Modal
402
+
open={openHandleDialog()}
403
+
onClose={() => setOpenHandleDialog(false)}
404
+
closeOnClick={false}
405
+
>
406
+
<HandleInput onClose={() => setOpenHandleDialog(false)} />
407
+
</Modal>
408
+
<Modal
409
+
open={openConfirmDialog()}
410
+
onClose={() => setOpenConfirmDialog(false)}
411
+
closeOnClick={false}
412
+
>
413
+
<ConfirmSubmit
414
+
isCreate={props.create}
415
+
onConfirm={(validate, recreate) => {
416
+
if (props.create) {
417
+
createRecord(validate);
418
+
} else {
419
+
editRecord(validate, recreate);
420
+
}
421
+
}}
422
+
onClose={() => setOpenConfirmDialog(false)}
423
+
/>
424
+
</Modal>
425
+
<div class="flex items-center justify-end gap-2">
426
+
<Button onClick={() => setOpenConfirmDialog(true)}>
427
+
{props.create ? "Create..." : "Edit..."}
428
+
</Button>
429
+
</div>
430
+
</div>
431
+
</div>
432
+
</form>
433
+
</div>
434
+
</Modal>
435
+
<Show when={isMinimized() && openDialog()}>
436
+
<button
437
+
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"
438
+
onclick={() => setIsMinimized(false)}
439
+
>
440
+
<span class="iconify lucide--square-pen text-lg"></span>
441
+
<span class="text-sm font-medium">{props.create ? "Creating" : "Editing"} record</span>
442
+
</button>
443
+
</Show>
444
+
<Tooltip text={props.create ? "Create record (n)" : "Edit record (e)"}>
445
+
<button
446
+
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"}`}
447
+
onclick={() => {
448
+
setNotice("");
449
+
setOpenDialog(true);
450
+
setIsMinimized(false);
451
+
}}
452
+
>
453
+
<div
454
+
class={props.create ? "iconify lucide--square-pen text-lg" : "iconify lucide--pencil"}
455
+
/>
456
+
</button>
457
+
</Tooltip>
458
+
</>
459
+
);
460
+
};
+4
src/components/create/state.ts
+4
src/components/create/state.ts
-529
src/components/create.tsx
-529
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, lazy, onCleanup, Show, Suspense } from "solid-js";
8
-
import { agent } from "../components/login.jsx";
9
-
import { sessions } from "./account.jsx";
10
-
import { Button } from "./button.jsx";
11
-
import { Modal } from "./modal.jsx";
12
-
import { addNotification, removeNotification } from "./notification.jsx";
13
-
import { TextInput } from "./text-input.jsx";
14
-
import Tooltip from "./tooltip.jsx";
15
-
16
-
const Editor = lazy(() => import("../components/editor.jsx").then((m) => ({ default: m.Editor })));
17
-
18
-
export const editorInstance = { view: null as any };
19
-
export const [placeholder, setPlaceholder] = createSignal<any>();
20
-
21
-
export const RecordEditor = (props: { create: boolean; record?: any; refetch?: any }) => {
22
-
const navigate = useNavigate();
23
-
const params = useParams();
24
-
const [openDialog, setOpenDialog] = createSignal(false);
25
-
const [notice, setNotice] = createSignal("");
26
-
const [openUpload, setOpenUpload] = createSignal(false);
27
-
const [openInsertMenu, setOpenInsertMenu] = createSignal(false);
28
-
const [validate, setValidate] = createSignal<boolean | undefined>(undefined);
29
-
const [isMaximized, setIsMaximized] = createSignal(false);
30
-
const [isMinimized, setIsMinimized] = createSignal(false);
31
-
const [collectionError, setCollectionError] = createSignal("");
32
-
const [rkeyError, setRkeyError] = createSignal("");
33
-
let blobInput!: HTMLInputElement;
34
-
let formRef!: HTMLFormElement;
35
-
let insertMenuRef!: HTMLDivElement;
36
-
37
-
createEffect(() => {
38
-
if (openInsertMenu()) {
39
-
const handleClickOutside = (e: MouseEvent) => {
40
-
if (insertMenuRef && !insertMenuRef.contains(e.target as Node)) {
41
-
setOpenInsertMenu(false);
42
-
}
43
-
};
44
-
document.addEventListener("mousedown", handleClickOutside);
45
-
onCleanup(() => document.removeEventListener("mousedown", handleClickOutside));
46
-
}
47
-
});
48
-
49
-
const defaultPlaceholder = () => {
50
-
return {
51
-
$type: "app.bsky.feed.post",
52
-
text: "This post was sent from PDSls",
53
-
embed: {
54
-
$type: "app.bsky.embed.external",
55
-
external: {
56
-
uri: "https://pdsls.dev",
57
-
title: "PDSls",
58
-
description: "Browse the public data on atproto",
59
-
},
60
-
},
61
-
langs: ["en"],
62
-
createdAt: new Date().toISOString(),
63
-
};
64
-
};
65
-
66
-
const getValidateIcon = () => {
67
-
return (
68
-
validate() === true ? "lucide--circle-check"
69
-
: validate() === false ? "lucide--circle-x"
70
-
: "lucide--circle"
71
-
);
72
-
};
73
-
74
-
const getValidateLabel = () => {
75
-
return (
76
-
validate() === true ? "True"
77
-
: validate() === false ? "False"
78
-
: "Unset"
79
-
);
80
-
};
81
-
82
-
createEffect(() => {
83
-
if (openDialog()) {
84
-
setValidate(undefined);
85
-
setCollectionError("");
86
-
setRkeyError("");
87
-
}
88
-
});
89
-
90
-
const createRecord = async (formData: FormData) => {
91
-
const repo = formData.get("repo")?.toString();
92
-
if (!repo) return;
93
-
const rpc = new Client({ handler: new OAuthUserAgent(await getSession(repo as Did)) });
94
-
const collection = formData.get("collection");
95
-
const rkey = formData.get("rkey");
96
-
let record: any;
97
-
try {
98
-
record = JSON.parse(editorInstance.view.state.doc.toString());
99
-
} catch (e: any) {
100
-
setNotice(e.message);
101
-
return;
102
-
}
103
-
const res = await rpc.post("com.atproto.repo.createRecord", {
104
-
input: {
105
-
repo: repo as Did,
106
-
collection: collection ? collection.toString() : record.$type,
107
-
rkey: rkey?.toString().length ? rkey?.toString() : undefined,
108
-
record: record,
109
-
validate: validate(),
110
-
},
111
-
});
112
-
if (!res.ok) {
113
-
setNotice(`${res.data.error}: ${res.data.message}`);
114
-
return;
115
-
}
116
-
setOpenDialog(false);
117
-
const id = addNotification({
118
-
message: "Record created",
119
-
type: "success",
120
-
});
121
-
setTimeout(() => removeNotification(id), 3000);
122
-
navigate(`/${res.data.uri}`);
123
-
};
124
-
125
-
const editRecord = async (recreate?: boolean) => {
126
-
const record = editorInstance.view.state.doc.toString();
127
-
if (!record) return;
128
-
const rpc = new Client({ handler: agent()! });
129
-
try {
130
-
const editedRecord = JSON.parse(record);
131
-
if (recreate) {
132
-
const res = await rpc.post("com.atproto.repo.applyWrites", {
133
-
input: {
134
-
repo: agent()!.sub,
135
-
validate: validate(),
136
-
writes: [
137
-
{
138
-
collection: params.collection as `${string}.${string}.${string}`,
139
-
rkey: params.rkey!,
140
-
$type: "com.atproto.repo.applyWrites#delete",
141
-
},
142
-
{
143
-
collection: params.collection as `${string}.${string}.${string}`,
144
-
rkey: params.rkey,
145
-
$type: "com.atproto.repo.applyWrites#create",
146
-
value: editedRecord,
147
-
},
148
-
],
149
-
},
150
-
});
151
-
if (!res.ok) {
152
-
setNotice(`${res.data.error}: ${res.data.message}`);
153
-
return;
154
-
}
155
-
} else {
156
-
const res = await rpc.post("com.atproto.repo.putRecord", {
157
-
input: {
158
-
repo: agent()!.sub,
159
-
collection: params.collection as `${string}.${string}.${string}`,
160
-
rkey: params.rkey!,
161
-
record: editedRecord,
162
-
validate: validate(),
163
-
},
164
-
});
165
-
if (!res.ok) {
166
-
setNotice(`${res.data.error}: ${res.data.message}`);
167
-
return;
168
-
}
169
-
}
170
-
setOpenDialog(false);
171
-
const id = addNotification({
172
-
message: "Record edited",
173
-
type: "success",
174
-
});
175
-
setTimeout(() => removeNotification(id), 3000);
176
-
props.refetch();
177
-
} catch (err: any) {
178
-
setNotice(err.message);
179
-
}
180
-
};
181
-
182
-
const insertTimestamp = () => {
183
-
const timestamp = new Date().toISOString();
184
-
editorInstance.view.dispatch({
185
-
changes: {
186
-
from: editorInstance.view.state.selection.main.head,
187
-
insert: `"${timestamp}"`,
188
-
},
189
-
});
190
-
setOpenInsertMenu(false);
191
-
};
192
-
193
-
const MenuItem = (props: { icon: string; label: string; onClick: () => void }) => {
194
-
return (
195
-
<button
196
-
type="button"
197
-
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"
198
-
onClick={props.onClick}
199
-
>
200
-
<span class={`iconify ${props.icon}`}></span>
201
-
<span>{props.label}</span>
202
-
</button>
203
-
);
204
-
};
205
-
206
-
const FileUpload = (props: { file: File }) => {
207
-
const [uploading, setUploading] = createSignal(false);
208
-
const [error, setError] = createSignal("");
209
-
210
-
onCleanup(() => (blobInput.value = ""));
211
-
212
-
const formatFileSize = (bytes: number) => {
213
-
if (bytes === 0) return "0 Bytes";
214
-
const k = 1024;
215
-
const sizes = ["Bytes", "KB", "MB", "GB"];
216
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
217
-
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
218
-
};
219
-
220
-
const uploadBlob = async () => {
221
-
let blob: Blob;
222
-
223
-
const mimetype = (document.getElementById("mimetype") as HTMLInputElement)?.value;
224
-
(document.getElementById("mimetype") as HTMLInputElement).value = "";
225
-
if (mimetype) blob = new Blob([props.file], { type: mimetype });
226
-
else blob = props.file;
227
-
228
-
if ((document.getElementById("exif-rm") as HTMLInputElement).checked) {
229
-
const exifRemoved = remove(new Uint8Array(await blob.arrayBuffer()));
230
-
if (exifRemoved !== null) blob = new Blob([exifRemoved], { type: blob.type });
231
-
}
232
-
233
-
const rpc = new Client({ handler: agent()! });
234
-
setUploading(true);
235
-
const res = await rpc.post("com.atproto.repo.uploadBlob", {
236
-
input: blob,
237
-
});
238
-
setUploading(false);
239
-
if (!res.ok) {
240
-
setError(res.data.error);
241
-
return;
242
-
}
243
-
editorInstance.view.dispatch({
244
-
changes: {
245
-
from: editorInstance.view.state.selection.main.head,
246
-
insert: JSON.stringify(res.data.blob, null, 2),
247
-
},
248
-
});
249
-
setOpenUpload(false);
250
-
};
251
-
252
-
return (
253
-
<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">
254
-
<h2 class="mb-2 font-semibold">Upload blob</h2>
255
-
<div class="flex flex-col gap-2 text-sm">
256
-
<div class="flex flex-col gap-1">
257
-
<p class="flex gap-1">
258
-
<span class="truncate">{props.file.name}</span>
259
-
<span class="shrink-0 text-neutral-600 dark:text-neutral-400">
260
-
({formatFileSize(props.file.size)})
261
-
</span>
262
-
</p>
263
-
</div>
264
-
<div class="flex items-center gap-x-2">
265
-
<label for="mimetype" class="shrink-0 select-none">
266
-
MIME type
267
-
</label>
268
-
<TextInput id="mimetype" placeholder={props.file.type} />
269
-
</div>
270
-
<div class="flex items-center gap-1">
271
-
<input id="exif-rm" type="checkbox" checked />
272
-
<label for="exif-rm" class="select-none">
273
-
Remove EXIF data
274
-
</label>
275
-
</div>
276
-
<p class="text-xs text-neutral-600 dark:text-neutral-400">
277
-
Metadata will be pasted after the cursor
278
-
</p>
279
-
<Show when={error()}>
280
-
<span class="text-red-500 dark:text-red-400">Error: {error()}</span>
281
-
</Show>
282
-
<div class="flex justify-between gap-2">
283
-
<Button onClick={() => setOpenUpload(false)}>Cancel</Button>
284
-
<Show when={uploading()}>
285
-
<div class="flex items-center gap-1">
286
-
<span class="iconify lucide--loader-circle animate-spin"></span>
287
-
<span>Uploading</span>
288
-
</div>
289
-
</Show>
290
-
<Show when={!uploading()}>
291
-
<Button
292
-
onClick={uploadBlob}
293
-
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"
294
-
>
295
-
Upload
296
-
</Button>
297
-
</Show>
298
-
</div>
299
-
</div>
300
-
</div>
301
-
);
302
-
};
303
-
304
-
return (
305
-
<>
306
-
<Modal
307
-
open={openDialog()}
308
-
onClose={() => setOpenDialog(false)}
309
-
closeOnClick={false}
310
-
nonBlocking={isMinimized()}
311
-
>
312
-
<div
313
-
style="transform: translateX(-50%) translateZ(0);"
314
-
classList={{
315
-
"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,
316
-
"w-[calc(100%-1rem)] max-w-3xl h-[65vh]": !isMaximized(),
317
-
"w-[calc(100%-1rem)] max-w-7xl h-[85vh]": isMaximized(),
318
-
hidden: isMinimized(),
319
-
}}
320
-
>
321
-
<div class="mb-2 flex w-full justify-between text-base">
322
-
<div class="flex items-center gap-2">
323
-
<span class="font-semibold select-none">
324
-
{props.create ? "Creating" : "Editing"} record
325
-
</span>
326
-
</div>
327
-
<div class="flex items-center gap-1">
328
-
<button
329
-
type="button"
330
-
onclick={() => setIsMinimized(true)}
331
-
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"
332
-
>
333
-
<span class="iconify lucide--minus"></span>
334
-
</button>
335
-
<button
336
-
type="button"
337
-
onclick={() => setIsMaximized(!isMaximized())}
338
-
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"
339
-
>
340
-
<span
341
-
class={`iconify ${isMaximized() ? "lucide--minimize-2" : "lucide--maximize-2"}`}
342
-
></span>
343
-
</button>
344
-
<button
345
-
id="close"
346
-
onclick={() => setOpenDialog(false)}
347
-
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"
348
-
>
349
-
<span class="iconify lucide--x"></span>
350
-
</button>
351
-
</div>
352
-
</div>
353
-
<form ref={formRef} class="flex min-h-0 flex-1 flex-col gap-y-2">
354
-
<Show when={props.create}>
355
-
<div class="flex flex-wrap items-center gap-1 text-sm">
356
-
<span>at://</span>
357
-
<select
358
-
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"
359
-
name="repo"
360
-
id="repo"
361
-
>
362
-
<For each={Object.keys(sessions)}>
363
-
{(session) => (
364
-
<option value={session} selected={session === agent()?.sub}>
365
-
{sessions[session].handle ?? session}
366
-
</option>
367
-
)}
368
-
</For>
369
-
</select>
370
-
<span>/</span>
371
-
<TextInput
372
-
id="collection"
373
-
name="collection"
374
-
placeholder="Collection (default: $type)"
375
-
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" : ""}`}
376
-
onInput={(e) => {
377
-
const value = e.currentTarget.value;
378
-
if (!value || isNsid(value)) setCollectionError("");
379
-
else
380
-
setCollectionError(
381
-
"Invalid collection: use reverse domain format (e.g. app.bsky.feed.post)",
382
-
);
383
-
}}
384
-
/>
385
-
<span>/</span>
386
-
<TextInput
387
-
id="rkey"
388
-
name="rkey"
389
-
placeholder="Record key (default: TID)"
390
-
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" : ""}`}
391
-
onInput={(e) => {
392
-
const value = e.currentTarget.value;
393
-
if (!value || isRecordKey(value)) setRkeyError("");
394
-
else setRkeyError("Invalid record key: 1-512 chars, use a-z A-Z 0-9 . _ ~ : -");
395
-
}}
396
-
/>
397
-
</div>
398
-
<Show when={collectionError() || rkeyError()}>
399
-
<div class="text-xs text-red-500 dark:text-red-400">
400
-
<div>{collectionError()}</div>
401
-
<div>{rkeyError()}</div>
402
-
</div>
403
-
</Show>
404
-
</Show>
405
-
<div class="min-h-0 flex-1">
406
-
<Suspense
407
-
fallback={
408
-
<div class="flex h-full items-center justify-center">
409
-
<span class="iconify lucide--loader-circle animate-spin text-xl"></span>
410
-
</div>
411
-
}
412
-
>
413
-
<Editor
414
-
content={JSON.stringify(
415
-
!props.create ? props.record
416
-
: params.rkey ? placeholder()
417
-
: defaultPlaceholder(),
418
-
null,
419
-
2,
420
-
)}
421
-
/>
422
-
</Suspense>
423
-
</div>
424
-
<div class="flex flex-col gap-2">
425
-
<Show when={notice()}>
426
-
<div class="text-sm text-red-500 dark:text-red-400">{notice()}</div>
427
-
</Show>
428
-
<div class="flex justify-between gap-2">
429
-
<div class="relative" ref={insertMenuRef}>
430
-
<button
431
-
type="button"
432
-
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"
433
-
onClick={() => setOpenInsertMenu(!openInsertMenu())}
434
-
>
435
-
<span class="iconify lucide--plus select-none"></span>
436
-
</button>
437
-
<Show when={openInsertMenu()}>
438
-
<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">
439
-
<MenuItem
440
-
icon="lucide--upload"
441
-
label="Upload blob"
442
-
onClick={() => {
443
-
setOpenInsertMenu(false);
444
-
blobInput.click();
445
-
}}
446
-
/>
447
-
<MenuItem
448
-
icon="lucide--clock"
449
-
label="Insert timestamp"
450
-
onClick={insertTimestamp}
451
-
/>
452
-
</div>
453
-
</Show>
454
-
<input
455
-
type="file"
456
-
id="blob"
457
-
class="sr-only"
458
-
ref={blobInput}
459
-
onChange={(e) => {
460
-
if (e.target.files !== null) setOpenUpload(true);
461
-
}}
462
-
/>
463
-
</div>
464
-
<Modal
465
-
open={openUpload()}
466
-
onClose={() => setOpenUpload(false)}
467
-
closeOnClick={false}
468
-
>
469
-
<FileUpload file={blobInput.files![0]} />
470
-
</Modal>
471
-
<div class="flex items-center justify-end gap-2">
472
-
<button
473
-
type="button"
474
-
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"
475
-
onClick={() =>
476
-
setValidate(
477
-
validate() === true ? false
478
-
: validate() === false ? undefined
479
-
: true,
480
-
)
481
-
}
482
-
>
483
-
<Tooltip text={getValidateLabel()}>
484
-
<span class={`iconify ${getValidateIcon()}`}></span>
485
-
</Tooltip>
486
-
<span>Validate</span>
487
-
</button>
488
-
<Show when={!props.create}>
489
-
<Button onClick={() => editRecord(true)}>Recreate</Button>
490
-
</Show>
491
-
<Button
492
-
onClick={() =>
493
-
props.create ? createRecord(new FormData(formRef)) : editRecord()
494
-
}
495
-
>
496
-
{props.create ? "Create" : "Edit"}
497
-
</Button>
498
-
</div>
499
-
</div>
500
-
</div>
501
-
</form>
502
-
</div>
503
-
</Modal>
504
-
<Show when={isMinimized() && openDialog()}>
505
-
<button
506
-
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"
507
-
onclick={() => setIsMinimized(false)}
508
-
>
509
-
<span class="iconify lucide--square-pen text-lg"></span>
510
-
<span class="text-sm font-medium">{props.create ? "Creating" : "Editing"} record</span>
511
-
</button>
512
-
</Show>
513
-
<Tooltip text={`${props.create ? "Create" : "Edit"} record`}>
514
-
<button
515
-
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"}`}
516
-
onclick={() => {
517
-
setNotice("");
518
-
setOpenDialog(true);
519
-
setIsMinimized(false);
520
-
}}
521
-
>
522
-
<div
523
-
class={props.create ? "iconify lucide--square-pen text-lg" : "iconify lucide--pencil"}
524
-
/>
525
-
</button>
526
-
</Tooltip>
527
-
</>
528
-
);
529
-
};
···
+45
-14
src/components/dropdown.tsx
+45
-14
src/components/dropdown.tsx
···
10
Show,
11
useContext,
12
} from "solid-js";
13
import { addToClipboard } from "../utils/copy";
14
15
const MenuContext = createContext<{
···
74
export const ActionMenu = (props: {
75
label: string;
76
icon: string;
77
-
onClick: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>;
78
}) => {
79
return (
80
<button
81
-
onClick={props.onClick}
82
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"
83
>
84
<Show when={props.icon}>
···
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 min-w-40 flex-col rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 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<{
···
75
export const ActionMenu = (props: {
76
label: string;
77
icon: string;
78
+
onClick: () => void;
79
}) => {
80
+
const ctx = useContext(MenuContext);
81
+
82
return (
83
<button
84
+
onClick={() => {
85
+
props.onClick();
86
+
ctx?.setShowMenu(false);
87
+
}}
88
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"
89
>
90
<Show when={props.icon}>
···
108
const ctx = useContext(MenuContext);
109
const [menu, setMenu] = createSignal<HTMLDivElement>();
110
const [menuButton, setMenuButton] = createSignal<HTMLButtonElement>();
111
+
const [buttonRect, setButtonRect] = createSignal<DOMRect>();
112
113
const clickEvent = (event: MouseEvent) => {
114
const target = event.target as Node;
115
if (!menuButton()?.contains(target) && !menu()?.contains(target)) ctx?.setShowMenu(false);
116
};
117
118
+
const updatePosition = () => {
119
+
const rect = menuButton()?.getBoundingClientRect();
120
+
if (rect) setButtonRect(rect);
121
+
};
122
+
123
+
onMount(() => {
124
+
window.addEventListener("click", clickEvent);
125
+
window.addEventListener("scroll", updatePosition, true);
126
+
window.addEventListener("resize", updatePosition);
127
+
});
128
+
129
+
onCleanup(() => {
130
+
window.removeEventListener("click", clickEvent);
131
+
window.removeEventListener("scroll", updatePosition, true);
132
+
window.removeEventListener("resize", updatePosition);
133
+
});
134
135
return (
136
<div class="relative">
···
140
props.buttonClass
141
}
142
ref={setMenuButton}
143
+
onClick={() => {
144
+
updatePosition();
145
+
ctx?.setShowMenu(!ctx?.showMenu());
146
+
}}
147
>
148
<span class={"iconify " + props.icon}></span>
149
</button>
150
<Show when={ctx?.showMenu()}>
151
+
<Portal>
152
+
<div
153
+
ref={setMenu}
154
+
style={{
155
+
position: "fixed",
156
+
top: `${(buttonRect()?.bottom ?? 0) + 4}px`,
157
+
left: `${(buttonRect()?.right ?? 0) - 160}px`,
158
+
}}
159
+
class={
160
+
"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 " +
161
+
props.menuClass
162
+
}
163
+
>
164
+
{props.children}
165
+
</div>
166
+
</Portal>
167
</Show>
168
</div>
169
);
+2
-1
src/components/editor.tsx
+2
-1
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
-
import { editorInstance } from "./create";
11
12
const Editor = (props: { content: string }) => {
13
let editorDiv!: HTMLDivElement;
···
48
keymap.of([indentWithTab]),
49
linter(jsonParseLinter()),
50
themeColor.of(document.documentElement.classList.contains("dark") ? basicDark : basicLight),
51
],
52
});
53
editorInstance.view = view;
···
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;
···
48
keymap.of([indentWithTab]),
49
linter(jsonParseLinter()),
50
themeColor.of(document.documentElement.classList.contains("dark") ? basicDark : basicLight),
51
+
EditorView.lineWrapping,
52
],
53
});
54
editorInstance.view = view;
+70
-56
src/components/json.tsx
+70
-56
src/components/json.tsx
···
1
import { isCid, isDid, isNsid, isResourceUri, Nsid } from "@atcute/lexicons/syntax";
2
import { A, useNavigate, useParams } from "@solidjs/router";
3
-
import { createEffect, createSignal, ErrorBoundary, For, on, Show } from "solid-js";
4
import { resolveLexiconAuthority } from "../utils/api";
5
import { hideMedia } from "../views/settings";
6
import { pds } from "./navbar";
7
import { addNotification, removeNotification } from "./notification";
8
import VideoPlayer from "./video-player";
9
10
interface AtBlob {
11
$type: string;
12
ref: { $link: string };
13
mimeType: string;
14
}
15
16
-
const JSONString = (props: {
17
-
data: string;
18
-
isType?: boolean;
19
-
isLink?: boolean;
20
-
parentIsBlob?: boolean;
21
-
}) => {
22
const navigate = useNavigate();
23
const params = useParams();
24
25
-
const isURL =
26
-
URL.canParse ??
27
-
((url, base) => {
28
-
try {
29
-
new URL(url, base);
30
-
return true;
31
-
} catch {
32
-
return false;
33
-
}
34
-
});
35
-
36
const handleClick = async (lex: string) => {
37
try {
38
const [nsid, anchor] = lex.split("#");
···
50
}
51
};
52
53
return (
54
<span>
55
"
56
-
<For each={props.data.split(/(\s)/)}>
57
{(part) => (
58
<>
59
{isResourceUri(part) ?
···
72
>
73
{part}
74
</button>
75
-
: isCid(part) && props.isLink && props.parentIsBlob && params.repo ?
76
<A
77
class="text-blue-400 hover:underline active:underline"
78
rel="noopener"
···
93
</>
94
)}
95
</For>
96
"
97
</span>
98
);
99
};
···
110
return <span>null</span>;
111
};
112
113
-
const JSONObject = (props: {
114
-
data: { [x: string]: JSONType };
115
-
repo: string;
116
-
parentIsBlob?: boolean;
117
-
}) => {
118
const params = useParams();
119
const [hide, setHide] = createSignal(
120
localStorage.hideMedia === "true" || params.rkey === undefined,
···
136
);
137
138
const isBlob = props.data.$type === "blob";
139
-
const isBlobContext = isBlob || props.parentIsBlob;
140
141
const Obj = ({ key, value }: { key: string; value: JSONType }) => {
142
const [show, setShow] = createSignal(true);
···
169
"self-center": value !== Object(value),
170
"pl-[calc(2ch-0.5px)] border-l-[0.5px] border-neutral-500/50 dark:border-neutral-400/50 has-hover:group-hover/indent:border-neutral-700 transition-colors dark:has-hover:group-hover/indent:border-neutral-300":
171
value === Object(value),
172
-
"invisible h-0": !show(),
173
}}
174
>
175
-
<JSONValue
176
-
data={value}
177
-
repo={props.repo}
178
-
isType={key === "$type"}
179
-
isLink={key === "$link"}
180
-
parentIsBlob={isBlobContext}
181
-
/>
182
</span>
183
</span>
184
);
···
200
<Show when={blob.mimeType.startsWith("image/")}>
201
<img
202
class="h-auto max-h-48 max-w-48 object-contain sm:max-h-64 sm:max-w-64"
203
-
src={`https://${pds()}/xrpc/com.atproto.sync.getBlob?did=${props.repo}&cid=${blob.ref.$link}`}
204
onLoad={() => setMediaLoaded(true)}
205
/>
206
</Show>
207
<Show when={blob.mimeType === "video/mp4"}>
208
<ErrorBoundary fallback={() => <span>Failed to load video</span>}>
209
<VideoPlayer
210
-
did={props.repo}
211
cid={blob.ref.$link}
212
onLoad={() => setMediaLoaded(true)}
213
/>
···
241
return rawObj;
242
};
243
244
-
const JSONArray = (props: { data: JSONType[]; repo: string; parentIsBlob?: boolean }) => {
245
return (
246
<For each={props.data}>
247
{(value, index) => (
···
252
}}
253
>
254
<span class="ml-[1ch] w-full">
255
-
<JSONValue data={value} repo={props.repo} parentIsBlob={props.parentIsBlob} />
256
</span>
257
</span>
258
)}
···
260
);
261
};
262
263
-
export const JSONValue = (props: {
264
-
data: JSONType;
265
-
repo: string;
266
-
isType?: boolean;
267
-
isLink?: boolean;
268
-
parentIsBlob?: boolean;
269
-
}) => {
270
const data = props.data;
271
if (typeof data === "string")
272
-
return (
273
-
<JSONString
274
-
data={data}
275
-
isType={props.isType}
276
-
isLink={props.isLink}
277
-
parentIsBlob={props.parentIsBlob}
278
-
/>
279
-
);
280
if (typeof data === "number") return <JSONNumber data={data} />;
281
if (typeof data === "boolean") return <JSONBoolean data={data} />;
282
if (data === null) return <JSONNull />;
283
-
if (Array.isArray(data))
284
-
return <JSONArray data={data} repo={props.repo} parentIsBlob={props.parentIsBlob} />;
285
-
return <JSONObject data={data} repo={props.repo} parentIsBlob={props.parentIsBlob} />;
286
};
287
288
export type JSONType = string | number | boolean | null | { [x: string]: JSONType } | JSONType[];
···
1
import { isCid, isDid, isNsid, isResourceUri, Nsid } from "@atcute/lexicons/syntax";
2
import { A, useNavigate, useParams } from "@solidjs/router";
3
+
import {
4
+
createContext,
5
+
createEffect,
6
+
createSignal,
7
+
ErrorBoundary,
8
+
For,
9
+
on,
10
+
Show,
11
+
useContext,
12
+
} from "solid-js";
13
import { resolveLexiconAuthority } from "../utils/api";
14
import { hideMedia } from "../views/settings";
15
import { pds } from "./navbar";
16
import { addNotification, removeNotification } from "./notification";
17
import VideoPlayer from "./video-player";
18
19
+
interface JSONContext {
20
+
repo: string;
21
+
truncate?: boolean;
22
+
parentIsBlob?: boolean;
23
+
}
24
+
25
+
const JSONCtx = createContext<JSONContext>();
26
+
const useJSONCtx = () => useContext(JSONCtx)!;
27
+
28
interface AtBlob {
29
$type: string;
30
ref: { $link: string };
31
mimeType: string;
32
}
33
34
+
const isURL =
35
+
URL.canParse ??
36
+
((url, base) => {
37
+
try {
38
+
new URL(url, base);
39
+
return true;
40
+
} catch {
41
+
return false;
42
+
}
43
+
});
44
+
45
+
const JSONString = (props: { data: string; isType?: boolean; isLink?: boolean }) => {
46
+
const ctx = useJSONCtx();
47
const navigate = useNavigate();
48
const params = useParams();
49
50
const handleClick = async (lex: string) => {
51
try {
52
const [nsid, anchor] = lex.split("#");
···
64
}
65
};
66
67
+
const MAX_LENGTH = 200;
68
+
const isTruncated = () => ctx.truncate && props.data.length > MAX_LENGTH;
69
+
const displayData = () => (isTruncated() ? props.data.slice(0, MAX_LENGTH) : props.data);
70
+
const remainingChars = () => props.data.length - MAX_LENGTH;
71
+
72
return (
73
<span>
74
"
75
+
<For each={displayData().split(/(\s)/)}>
76
{(part) => (
77
<>
78
{isResourceUri(part) ?
···
91
>
92
{part}
93
</button>
94
+
: isCid(part) && props.isLink && ctx.parentIsBlob && params.repo ?
95
<A
96
class="text-blue-400 hover:underline active:underline"
97
rel="noopener"
···
112
</>
113
)}
114
</For>
115
+
<Show when={isTruncated()}>
116
+
<span>โฆ</span>
117
+
</Show>
118
"
119
+
<Show when={isTruncated()}>
120
+
<span class="ml-1 text-neutral-500 dark:text-neutral-400">
121
+
(+{remainingChars().toLocaleString()})
122
+
</span>
123
+
</Show>
124
</span>
125
);
126
};
···
137
return <span>null</span>;
138
};
139
140
+
const JSONObject = (props: { data: { [x: string]: JSONType } }) => {
141
+
const ctx = useJSONCtx();
142
const params = useParams();
143
const [hide, setHide] = createSignal(
144
localStorage.hideMedia === "true" || params.rkey === undefined,
···
160
);
161
162
const isBlob = props.data.$type === "blob";
163
+
const isBlobContext = isBlob || ctx.parentIsBlob;
164
165
const Obj = ({ key, value }: { key: string; value: JSONType }) => {
166
const [show, setShow] = createSignal(true);
···
193
"self-center": value !== Object(value),
194
"pl-[calc(2ch-0.5px)] border-l-[0.5px] border-neutral-500/50 dark:border-neutral-400/50 has-hover:group-hover/indent:border-neutral-700 transition-colors dark:has-hover:group-hover/indent:border-neutral-300":
195
value === Object(value),
196
+
"invisible h-0 overflow-hidden": !show(),
197
}}
198
>
199
+
<JSONCtx.Provider value={{ ...ctx, parentIsBlob: isBlobContext }}>
200
+
<JSONValueInner data={value} isType={key === "$type"} isLink={key === "$link"} />
201
+
</JSONCtx.Provider>
202
</span>
203
</span>
204
);
···
220
<Show when={blob.mimeType.startsWith("image/")}>
221
<img
222
class="h-auto max-h-48 max-w-48 object-contain sm:max-h-64 sm:max-w-64"
223
+
src={`https://${pds()}/xrpc/com.atproto.sync.getBlob?did=${ctx.repo}&cid=${blob.ref.$link}`}
224
onLoad={() => setMediaLoaded(true)}
225
/>
226
</Show>
227
<Show when={blob.mimeType === "video/mp4"}>
228
<ErrorBoundary fallback={() => <span>Failed to load video</span>}>
229
<VideoPlayer
230
+
did={ctx.repo}
231
cid={blob.ref.$link}
232
onLoad={() => setMediaLoaded(true)}
233
/>
···
261
return rawObj;
262
};
263
264
+
const JSONArray = (props: { data: JSONType[] }) => {
265
return (
266
<For each={props.data}>
267
{(value, index) => (
···
272
}}
273
>
274
<span class="ml-[1ch] w-full">
275
+
<JSONValueInner data={value} />
276
</span>
277
</span>
278
)}
···
280
);
281
};
282
283
+
const JSONValueInner = (props: { data: JSONType; isType?: boolean; isLink?: boolean }) => {
284
const data = props.data;
285
if (typeof data === "string")
286
+
return <JSONString data={data} isType={props.isType} isLink={props.isLink} />;
287
if (typeof data === "number") return <JSONNumber data={data} />;
288
if (typeof data === "boolean") return <JSONBoolean data={data} />;
289
if (data === null) return <JSONNull />;
290
+
if (Array.isArray(data)) return <JSONArray data={data} />;
291
+
return <JSONObject data={data} />;
292
+
};
293
+
294
+
export const JSONValue = (props: { data: JSONType; repo: string; truncate?: boolean }) => {
295
+
return (
296
+
<JSONCtx.Provider value={{ repo: props.repo, truncate: props.truncate }}>
297
+
<JSONValueInner data={props.data} />
298
+
</JSONCtx.Provider>
299
+
);
300
};
301
302
export type JSONType = string | number | boolean | null | { [x: string]: JSONType } | JSONType[];
+211
-1
src/components/lexicon-schema.tsx
+211
-1
src/components/lexicon-schema.tsx
···
12
};
13
}
14
15
interface LexiconDef {
16
type: string;
17
description?: string;
···
40
maxSize?: number;
41
knownValues?: string[];
42
format?: string;
43
}
44
45
interface LexiconObject {
···
257
);
258
};
259
260
const DefSection = (props: { name: string; def: LexiconDef }) => {
261
const defTypeColor = () => {
262
switch (props.def.type) {
···
272
return "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300";
273
case "token":
274
return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300";
275
default:
276
return "bg-neutral-200 text-neutral-800 dark:bg-neutral-700 dark:text-neutral-300";
277
}
···
302
{props.name === "main" ? "Main Definition" : props.name}
303
</a>
304
<span class={`rounded px-2 py-0.5 text-xs font-semibold uppercase ${defTypeColor()}`}>
305
-
{props.def.type}
306
</span>
307
</div>
308
···
316
<span class="text-sm font-semibold">Record Key: </span>
317
<span class="font-mono text-sm">{props.def.key}</span>
318
</div>
319
</Show>
320
321
{/* Properties (for record/object types) */}
···
12
};
13
}
14
15
+
interface LexiconPermission {
16
+
type: "permission";
17
+
// NOTE: blob, account, and identity are not supported in lexicon schema context
18
+
resource: "repo" | "rpc" | "blob" | "account" | "identity";
19
+
collection?: string[];
20
+
action?: string[];
21
+
lxm?: string[];
22
+
aud?: string;
23
+
inheritAud?: boolean;
24
+
}
25
+
26
interface LexiconDef {
27
type: string;
28
description?: string;
···
51
maxSize?: number;
52
knownValues?: string[];
53
format?: string;
54
+
// Permission-set fields
55
+
title?: string;
56
+
"title:langs"?: { [lang: string]: string };
57
+
detail?: string;
58
+
"detail:langs"?: { [lang: string]: string };
59
+
permissions?: LexiconPermission[];
60
}
61
62
interface LexiconObject {
···
274
);
275
};
276
277
+
const NsidLink = (props: { nsid: string }) => {
278
+
const navigate = useNavigate();
279
+
280
+
const handleClick = async () => {
281
+
try {
282
+
const authority = await resolveLexiconAuthority(props.nsid as Nsid);
283
+
navigate(`/at://${authority}/com.atproto.lexicon.schema/${props.nsid}#schema`);
284
+
} catch (err) {
285
+
console.error("Failed to resolve lexicon authority:", err);
286
+
}
287
+
};
288
+
289
+
return (
290
+
<button
291
+
type="button"
292
+
onClick={handleClick}
293
+
class="cursor-pointer rounded bg-blue-100 px-1.5 py-0.5 font-mono text-xs text-blue-800 hover:bg-blue-200 hover:underline active:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-300 dark:hover:bg-blue-900/50 dark:active:bg-blue-900/50"
294
+
>
295
+
{props.nsid}
296
+
</button>
297
+
);
298
+
};
299
+
300
+
const resourceColor = (resource: string) => {
301
+
switch (resource) {
302
+
case "repo":
303
+
return "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300";
304
+
case "rpc":
305
+
return "bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300";
306
+
default:
307
+
return "bg-neutral-200 text-neutral-800 dark:bg-neutral-700 dark:text-neutral-300";
308
+
}
309
+
};
310
+
311
+
const PermissionRow = (props: { permission: LexiconPermission; index: number }) => {
312
+
return (
313
+
<div class="flex flex-col gap-2 py-3">
314
+
<div class="flex flex-wrap items-center gap-2">
315
+
<span class="font-mono text-sm font-semibold">#{props.index + 1}</span>
316
+
<span
317
+
class={`rounded px-1.5 py-0.5 font-mono text-xs font-semibold ${resourceColor(props.permission.resource)}`}
318
+
>
319
+
{props.permission.resource}
320
+
</span>
321
+
</div>
322
+
323
+
{/* Collections (for repo resource) */}
324
+
<Show when={props.permission.collection && props.permission.collection.length > 0}>
325
+
<div class="flex flex-col gap-1">
326
+
<span class="text-xs font-semibold text-neutral-500 dark:text-neutral-400">
327
+
Collections:
328
+
</span>
329
+
<div class="flex flex-wrap gap-1">
330
+
<For each={props.permission.collection}>{(col) => <NsidLink nsid={col} />}</For>
331
+
</div>
332
+
</div>
333
+
</Show>
334
+
335
+
{/* Actions */}
336
+
<Show when={props.permission.action && props.permission.action.length > 0}>
337
+
<div class="flex flex-col gap-1">
338
+
<span class="text-xs font-semibold text-neutral-500 dark:text-neutral-400">Actions:</span>
339
+
<div class="flex flex-wrap gap-1">
340
+
<For each={props.permission.action}>
341
+
{(action) => (
342
+
<span class="dark:bg-dark-200 rounded bg-neutral-200/50 px-1.5 py-0.5 font-mono text-xs">
343
+
{action}
344
+
</span>
345
+
)}
346
+
</For>
347
+
</div>
348
+
</div>
349
+
</Show>
350
+
351
+
{/* LXM (for rpc resource) */}
352
+
<Show when={props.permission.lxm && props.permission.lxm.length > 0}>
353
+
<div class="flex flex-col gap-1">
354
+
<span class="text-xs font-semibold text-neutral-500 dark:text-neutral-400">
355
+
Lexicon Methods:
356
+
</span>
357
+
<div class="flex flex-wrap gap-1">
358
+
<For each={props.permission.lxm}>{(method) => <NsidLink nsid={method} />}</For>
359
+
</div>
360
+
</div>
361
+
</Show>
362
+
363
+
{/* Audience */}
364
+
<Show when={props.permission.aud}>
365
+
<div class="flex items-center gap-2 text-xs">
366
+
<span class="font-semibold text-neutral-500 dark:text-neutral-400">Audience:</span>
367
+
<span class="font-mono">{props.permission.aud}</span>
368
+
</div>
369
+
</Show>
370
+
371
+
{/* Inherit Audience */}
372
+
<Show when={props.permission.inheritAud}>
373
+
<div class="flex items-center gap-1 text-xs">
374
+
<span class="font-semibold text-neutral-500 dark:text-neutral-400">
375
+
Inherit Audience:
376
+
</span>
377
+
<span>true</span>
378
+
</div>
379
+
</Show>
380
+
</div>
381
+
);
382
+
};
383
+
384
const DefSection = (props: { name: string; def: LexiconDef }) => {
385
const defTypeColor = () => {
386
switch (props.def.type) {
···
396
return "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300";
397
case "token":
398
return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300";
399
+
case "permission-set":
400
+
return "bg-cyan-100 text-cyan-800 dark:bg-cyan-900/30 dark:text-cyan-300";
401
default:
402
return "bg-neutral-200 text-neutral-800 dark:bg-neutral-700 dark:text-neutral-300";
403
}
···
428
{props.name === "main" ? "Main Definition" : props.name}
429
</a>
430
<span class={`rounded px-2 py-0.5 text-xs font-semibold uppercase ${defTypeColor()}`}>
431
+
{props.def.type.replace("-", " ")}
432
</span>
433
</div>
434
···
442
<span class="text-sm font-semibold">Record Key: </span>
443
<span class="font-mono text-sm">{props.def.key}</span>
444
</div>
445
+
</Show>
446
+
447
+
{/* Permission-set: Title and Detail */}
448
+
<Show when={props.def.type === "permission-set" && (props.def.title || props.def.detail)}>
449
+
<div class="flex flex-col gap-2 rounded-lg border border-neutral-200 bg-neutral-50/50 p-3 dark:border-neutral-700 dark:bg-neutral-800/30">
450
+
<Show when={props.def.title}>
451
+
<div class="flex flex-col gap-1">
452
+
<span class="text-xs font-semibold text-neutral-500 uppercase dark:text-neutral-400">
453
+
Title
454
+
</span>
455
+
<span class="text-sm font-medium">{props.def.title}</span>
456
+
</div>
457
+
</Show>
458
+
<Show when={props.def["title:langs"]}>
459
+
<div class="flex flex-col gap-1">
460
+
<span class="text-xs font-semibold text-neutral-500 uppercase dark:text-neutral-400">
461
+
Localized Titles
462
+
</span>
463
+
<div class="flex flex-col gap-1">
464
+
<For each={Object.entries(props.def["title:langs"]!)}>
465
+
{([lang, text]) => (
466
+
<div class="flex items-center gap-2 text-sm">
467
+
<span class="rounded bg-neutral-100 px-1.5 py-0.5 font-mono text-xs dark:bg-neutral-800">
468
+
{lang}
469
+
</span>
470
+
<span>{text}</span>
471
+
</div>
472
+
)}
473
+
</For>
474
+
</div>
475
+
</div>
476
+
</Show>
477
+
<Show when={props.def.detail}>
478
+
<div class="flex flex-col gap-1">
479
+
<span class="text-xs font-semibold text-neutral-500 uppercase dark:text-neutral-400">
480
+
Detail
481
+
</span>
482
+
<p class="text-sm text-neutral-700 dark:text-neutral-300">{props.def.detail}</p>
483
+
</div>
484
+
</Show>
485
+
<Show when={props.def["detail:langs"]}>
486
+
<div class="flex flex-col gap-1">
487
+
<span class="text-xs font-semibold text-neutral-500 uppercase dark:text-neutral-400">
488
+
Localized Details
489
+
</span>
490
+
<div class="flex flex-col gap-1">
491
+
<For each={Object.entries(props.def["detail:langs"]!)}>
492
+
{([lang, text]) => (
493
+
<div class="flex flex-col gap-1 text-sm">
494
+
<span class="w-fit rounded bg-neutral-100 px-1.5 py-0.5 font-mono text-xs dark:bg-neutral-800">
495
+
{lang}
496
+
</span>
497
+
<p class="text-neutral-700 dark:text-neutral-300">{text}</p>
498
+
</div>
499
+
)}
500
+
</For>
501
+
</div>
502
+
</div>
503
+
</Show>
504
+
</div>
505
+
</Show>
506
+
507
+
{/* Permission-set: Permissions list */}
508
+
<Show when={props.def.permissions && props.def.permissions.length > 0}>
509
+
{(() => {
510
+
const supportedPermissions = () =>
511
+
props.def.permissions!.filter((p) => p.resource === "repo" || p.resource === "rpc");
512
+
return (
513
+
<Show when={supportedPermissions().length > 0}>
514
+
<div class="flex flex-col gap-2">
515
+
<h4 class="text-sm font-semibold text-neutral-600 uppercase dark:text-neutral-400">
516
+
Permissions
517
+
</h4>
518
+
<div class="divide-y divide-neutral-200 rounded-lg border border-neutral-200 bg-neutral-50/50 px-3 dark:divide-neutral-700 dark:border-neutral-700 dark:bg-neutral-800/30">
519
+
<For each={supportedPermissions()}>
520
+
{(permission, index) => (
521
+
<PermissionRow permission={permission} index={index()} />
522
+
)}
523
+
</For>
524
+
</div>
525
+
</div>
526
+
</Show>
527
+
);
528
+
})()}
529
</Show>
530
531
{/* Properties (for record/object types) */}
-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-md 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 };
···
+1
-1
src/components/notification.tsx
+1
-1
src/components/notification.tsx
+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
};
···
168
<label for="input" class="hidden">
169
PDS URL, AT URI, NSID, DID, or handle
170
</label>
171
-
<div class="dark:bg-dark-100 dark:inset-shadow-dark-200 flex 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">
172
<label
173
for="input"
174
class="iconify lucide--search text-neutral-500 dark:text-neutral-400"
···
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
}}
···
355
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"
356
onClick={() => setOpenList(true)}
357
>
358
-
<span class="iconify lucide--help-circle"></span>
359
</button>
360
</>
361
);
···
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
};
···
188
<label for="input" class="hidden">
189
PDS URL, AT URI, NSID, DID, or handle
190
</label>
191
+
<div class="dark:bg-dark-100 flex items-center gap-2 rounded-lg bg-white px-2 outline-1 outline-neutral-200 focus-within:outline-[1.5px] focus-within:outline-neutral-600 dark:outline-neutral-600 dark:focus-within:outline-neutral-400">
192
<label
193
for="input"
194
class="iconify lucide--search text-neutral-500 dark:text-neutral-400"
···
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 min-w-0 flex-col">
316
+
<Show when={actor.displayName}>
317
+
<span class="truncate text-sm font-medium">{actor.displayName}</span>
318
+
</Show>
319
+
<span class="truncate text-xs text-neutral-600 dark:text-neutral-400">
320
+
@{actor.handle}
321
+
</span>
322
+
</div>
323
</A>
324
);
325
}}
···
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 text-neutral-600 dark:text-neutral-300"></span>
386
</button>
387
</>
388
);
+2
-2
src/components/sticky.tsx
+2
-2
src/components/sticky.tsx
···
29
/>
30
31
<div
32
-
class="sticky top-2 z-10 flex w-full flex-col items-center justify-center gap-2 rounded-lg p-3 transition-colors"
33
classList={{
34
-
"bg-neutral-50 dark:bg-dark-300 border-[0.5px] border-neutral-300 dark:border-neutral-700 shadow-md":
35
filterStuck(),
36
"bg-transparent border-transparent shadow-none": !filterStuck(),
37
}}
···
29
/>
30
31
<div
32
+
class="sticky top-2 z-10 flex w-full flex-col items-center justify-center gap-2 rounded-lg border-[0.5px] p-3 transition-colors"
33
classList={{
34
+
"bg-neutral-50 dark:bg-dark-300 border-neutral-300 dark:border-neutral-700 shadow-md":
35
filterStuck(),
36
"bg-transparent border-transparent shadow-none": !filterStuck(),
37
}}
+1
-1
src/components/text-input.tsx
+1
-1
src/components/text-input.tsx
···
25
disabled={props.disabled}
26
required={props.required}
27
class={
28
-
"dark:bg-dark-100 dark:inset-shadow-dark-200 rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 py-1 inset-shadow-xs select-none placeholder:text-sm focus:outline-[1px] focus:outline-neutral-600 dark:border-neutral-600 dark:focus:outline-neutral-400 " +
29
props.class
30
}
31
onInput={props.onInput}
···
25
disabled={props.disabled}
26
required={props.required}
27
class={
28
+
"dark:bg-dark-100 rounded-lg bg-white px-2 py-1 outline-1 outline-neutral-200 select-none placeholder:text-sm focus:outline-[1.5px] focus:outline-neutral-600 dark:outline-neutral-600 dark:focus:outline-neutral-400 " +
29
props.class
30
}
31
onInput={props.onInput}
+7
-1
src/components/video-player.tsx
+7
-1
src/components/video-player.tsx
+33
-19
src/layout.tsx
+33
-19
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";
···
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 text-sm"
146
-
>
147
<NavMenu href="/jetstream" label="Jetstream" icon="lucide--radio-tower" />
148
<NavMenu href="/firehose" label="Firehose" icon="lucide--droplet" />
149
-
<NavMenu href="/labels" label="Labels" icon="lucide--tags" />
150
<NavMenu href="/settings" label="Settings" icon="lucide--settings" />
151
<MenuSeparator />
152
<NavMenu
···
187
</Show>
188
</div>
189
<NotificationContainer />
190
</div>
191
);
192
};
···
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";
···
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-3">
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--tag" />
155
<NavMenu href="/settings" label="Settings" icon="lucide--settings" />
156
<MenuSeparator />
157
<NavMenu
···
192
</Show>
193
</div>
194
<NotificationContainer />
195
+
<Show
196
+
when={localStorage.plcDirectory && localStorage.plcDirectory !== "https://plc.directory"}
197
+
>
198
+
<div class="dark:bg-dark-500 fixed right-0 bottom-0 left-0 z-10 flex items-center justify-center bg-neutral-100 px-3 py-1 text-xs">
199
+
<span>
200
+
PLC directory: <span class="font-medium">{localStorage.plcDirectory}</span>
201
+
</span>
202
+
</div>
203
+
</Show>
204
</div>
205
);
206
};
+7
-1
src/styles/index.css
+7
-1
src/styles/index.css
···
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
-17
src/utils/api.ts
-17
src/utils/api.ts
···
133
linking_records: Array<{ did: string; collection: string; rkey: string }>;
134
};
135
136
-
type LinksWithDids = {
137
-
cursor: string;
138
-
total: number;
139
-
linking_dids: Array<string>;
140
-
};
141
-
142
const getConstellation = async (
143
endpoint: string,
144
target: string,
···
175
): Promise<LinksWithRecords> =>
176
getConstellation("/links", target, collection, path, cursor, limit || 100);
177
178
-
const getDidBacklinks = (
179
-
target: string,
180
-
collection: string,
181
-
path: string,
182
-
cursor?: string,
183
-
limit?: number,
184
-
): Promise<LinksWithDids> =>
185
-
getConstellation("/links/distinct-dids", target, collection, path, cursor, limit || 100);
186
-
187
export {
188
didDocCache,
189
getAllBacklinks,
190
-
getDidBacklinks,
191
getPDS,
192
getRecordBacklinks,
193
labelerCache,
···
198
resolvePDS,
199
validateHandle,
200
type LinkData,
201
-
type LinksWithDids,
202
type LinksWithRecords,
203
};
···
133
linking_records: Array<{ did: string; collection: string; rkey: string }>;
134
};
135
136
const getConstellation = async (
137
endpoint: string,
138
target: string,
···
169
): Promise<LinksWithRecords> =>
170
getConstellation("/links", target, collection, path, cursor, limit || 100);
171
172
export {
173
didDocCache,
174
getAllBacklinks,
175
getPDS,
176
getRecordBacklinks,
177
labelerCache,
···
182
resolvePDS,
183
validateHandle,
184
type LinkData,
185
type LinksWithRecords,
186
};
+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
+
};
+24
src/utils/route-cache.ts
+24
src/utils/route-cache.ts
···
···
1
+
import { createStore } from "solid-js/store";
2
+
3
+
export interface CollectionCacheEntry {
4
+
records: unknown[];
5
+
cursor: string | undefined;
6
+
scrollY: number;
7
+
reverse: boolean;
8
+
}
9
+
10
+
type RouteCache = Record<string, CollectionCacheEntry>;
11
+
12
+
const [routeCache, setRouteCache] = createStore<RouteCache>({});
13
+
14
+
export const getCollectionCache = (key: string): CollectionCacheEntry | undefined => {
15
+
return routeCache[key];
16
+
};
17
+
18
+
export const setCollectionCache = (key: string, entry: CollectionCacheEntry): void => {
19
+
setRouteCache(key, entry);
20
+
};
21
+
22
+
export const clearCollectionCache = (key: string): void => {
23
+
setRouteCache(key, undefined!);
24
+
};
+7
-6
src/views/blob.tsx
+7
-6
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 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>
43
)}
44
</For>
45
</div>
46
</Show>
47
-
<div class="dark:bg-dark-500 fixed bottom-0 z-5 flex w-screen justify-center bg-neutral-100 py-2">
48
<div class="flex flex-col items-center gap-1 pb-2">
49
<p>
50
{blobs()?.length} blob{(blobs()?.length ?? 0 > 1) ? "s" : ""}
···
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 pb-20 font-mono text-xs sm:text-sm">
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="truncate rounded px-0.5 text-left text-blue-400 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
40
+
dir="rtl"
41
>
42
+
{cid}
43
</a>
44
)}
45
</For>
46
</div>
47
</Show>
48
+
<div class="dark:bg-dark-500 fixed bottom-0 z-5 flex w-screen justify-center bg-neutral-100 pt-2 pb-4">
49
<div class="flex flex-col items-center gap-1 pb-2">
50
<p>
51
{blobs()?.length} blob{(blobs()?.length ?? 0 > 1) ? "s" : ""}
+97
-39
src/views/collection.tsx
+97
-39
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, createMemo, createResource, createSignal, For, Show } 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";
14
import { TextInput } from "../components/text-input.jsx";
15
import Tooltip from "../components/tooltip.jsx";
16
import { resolvePDS } from "../utils/api.js";
17
import { localDateFromTimestamp } from "../utils/date.js";
18
19
interface AtprotoRecord {
20
rkey: string;
···
41
42
return (
43
<span
44
-
class="relative flex w-full min-w-0 items-baseline rounded px-0.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
45
ref={rkeyRef}
46
-
onmouseover={() => setHover(true)}
47
-
onmouseleave={() => setHover(false)}
48
>
49
<span class="flex items-baseline truncate">
50
-
<span class="shrink-0 text-sm text-blue-400 sm:text-base">{props.record.rkey}</span>
51
<span class="ml-1 truncate text-xs text-neutral-500 dark:text-neutral-400" dir="rtl">
52
{props.record.cid}
53
</span>
···
65
<JSONValue
66
data={props.record.record.value as JSONType}
67
repo={props.record.record.uri.split("/")[2]}
68
/>
69
</span>
70
</Show>
···
82
const [reverse, setReverse] = createSignal(false);
83
const [recreate, setRecreate] = createSignal(false);
84
const [openDelete, setOpenDelete] = createSignal(false);
85
const did = params.repo;
86
let pds: string;
87
let rpc: Client;
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,
···
165
setCursor(undefined);
166
setOpenDelete(false);
167
setRecreate(false);
168
refetch();
169
};
170
···
198
<StickyOverlay>
199
<div class="flex w-full flex-col gap-2">
200
<div class="flex items-center gap-1">
201
-
<Show when={agent() && agent()?.sub === did}>
202
<div class="flex items-center">
203
<Tooltip
204
text={batchDelete() ? "Cancel" : "Delete"}
···
209
setLastSelected(undefined);
210
setBatchDelete(!batchDelete());
211
}}
212
-
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"
213
>
214
<span
215
-
class={`iconify text-lg ${batchDelete() ? "lucide--circle-x" : "lucide--trash-2"} `}
216
></span>
217
</button>
218
}
···
223
children={
224
<button
225
onclick={() => selectAll()}
226
-
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"
227
-
>
228
-
<span class="iconify lucide--copy-check text-lg"></span>
229
-
</button>
230
-
}
231
-
/>
232
-
<Tooltip
233
-
text="Recreate"
234
-
children={
235
-
<button
236
-
onclick={() => {
237
-
setRecreate(true);
238
-
setOpenDelete(true);
239
-
}}
240
-
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"
241
>
242
-
<span class="iconify lucide--recycle text-lg text-green-500 dark:text-green-400"></span>
243
</button>
244
}
245
/>
246
<Tooltip
247
text="Delete"
248
children={
···
251
setRecreate(false);
252
setOpenDelete(true);
253
}}
254
-
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"
255
>
256
-
<span class="iconify lucide--trash-2 text-lg text-red-500 dark:text-red-400"></span>
257
</button>
258
}
259
/>
···
277
</div>
278
</Modal>
279
</Show>
280
-
<Tooltip text="Jetstream">
281
-
<A
282
-
href={`/jetstream?collections=${params.collection}&dids=${params.repo}`}
283
-
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"
284
-
>
285
-
<span class="iconify lucide--radio-tower text-lg"></span>
286
-
</A>
287
-
</Tooltip>
288
<TextInput
289
name="Filter"
290
placeholder="Filter by substring"
291
onInput={(e) => setFilter(e.currentTarget.value)}
292
class="grow"
293
/>
294
</div>
295
<Show when={records.length > 1}>
296
<div class="flex items-center justify-between gap-x-2">
···
299
setReverse(!reverse());
300
setRecords([]);
301
setCursor(undefined);
302
refetch();
303
}}
304
>
···
346
</label>
347
</Show>
348
<Show when={!batchDelete()}>
349
-
<A href={`/at://${did}/${params.collection}/${record.rkey}`}>
350
<RecordLink record={record} />
351
</A>
352
</Show>
···
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, useBeforeLeave, useParams } from "@solidjs/router";
6
+
import {
7
+
createEffect,
8
+
createMemo,
9
+
createResource,
10
+
createSignal,
11
+
For,
12
+
onMount,
13
+
Show,
14
+
} from "solid-js";
15
import { createStore } from "solid-js/store";
16
+
import { hasUserScope } from "../auth/scope-utils";
17
+
import { agent } from "../auth/state";
18
import { Button } from "../components/button.jsx";
19
import { JSONType, JSONValue } from "../components/json.jsx";
20
import { Modal } from "../components/modal.jsx";
21
import { addNotification, removeNotification } from "../components/notification.jsx";
22
import { StickyOverlay } from "../components/sticky.jsx";
23
import { TextInput } from "../components/text-input.jsx";
24
import Tooltip from "../components/tooltip.jsx";
25
+
import { isTouchDevice } from "../layout.jsx";
26
import { resolvePDS } from "../utils/api.js";
27
import { localDateFromTimestamp } from "../utils/date.js";
28
+
import {
29
+
clearCollectionCache,
30
+
getCollectionCache,
31
+
setCollectionCache,
32
+
} from "../utils/route-cache.js";
33
34
interface AtprotoRecord {
35
rkey: string;
···
56
57
return (
58
<span
59
+
class="relative flex w-full min-w-0 items-baseline rounded p-0.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
60
ref={rkeyRef}
61
+
onmouseover={() => !isTouchDevice && setHover(true)}
62
+
onmouseleave={() => !isTouchDevice && setHover(false)}
63
>
64
<span class="flex items-baseline truncate">
65
+
<span class="shrink-0 text-sm text-blue-400">{props.record.rkey}</span>
66
<span class="ml-1 truncate text-xs text-neutral-500 dark:text-neutral-400" dir="rtl">
67
{props.record.cid}
68
</span>
···
80
<JSONValue
81
data={props.record.record.value as JSONType}
82
repo={props.record.record.uri.split("/")[2]}
83
+
truncate
84
/>
85
</span>
86
</Show>
···
98
const [reverse, setReverse] = createSignal(false);
99
const [recreate, setRecreate] = createSignal(false);
100
const [openDelete, setOpenDelete] = createSignal(false);
101
+
const [restoredFromCache, setRestoredFromCache] = createSignal(false);
102
const did = params.repo;
103
let pds: string;
104
let rpc: Client;
105
106
+
const cacheKey = () => `${params.pds}/${params.repo}/${params.collection}`;
107
+
108
+
onMount(() => {
109
+
const cached = getCollectionCache(cacheKey());
110
+
if (cached) {
111
+
setRecords(cached.records as AtprotoRecord[]);
112
+
setCursor(cached.cursor);
113
+
setReverse(cached.reverse);
114
+
setRestoredFromCache(true);
115
+
requestAnimationFrame(() => {
116
+
window.scrollTo(0, cached.scrollY);
117
+
});
118
+
}
119
+
});
120
+
121
+
useBeforeLeave((e) => {
122
+
const recordPathPrefix = `/at://${did}/${params.collection}/`;
123
+
const isNavigatingToRecord = typeof e.to === "string" && e.to.startsWith(recordPathPrefix);
124
+
125
+
if (isNavigatingToRecord && records.length > 0) {
126
+
setCollectionCache(cacheKey(), {
127
+
records: [...records],
128
+
cursor: cursor(),
129
+
scrollY: window.scrollY,
130
+
reverse: reverse(),
131
+
});
132
+
} else {
133
+
clearCollectionCache(cacheKey());
134
+
}
135
+
});
136
+
137
const fetchRecords = async () => {
138
+
if (restoredFromCache() && records.length > 0 && !cursor()) {
139
+
setRestoredFromCache(false);
140
+
return records;
141
+
}
142
+
if (restoredFromCache()) setRestoredFromCache(false);
143
+
144
if (!pds) pds = await resolvePDS(did!);
145
+
if (!rpc) rpc = new Client({ handler: simpleFetchHandler({ service: pds }) });
146
const res = await rpc.get("com.atproto.repo.listRecords", {
147
params: {
148
repo: did as ActorIdentifier,
···
219
setCursor(undefined);
220
setOpenDelete(false);
221
setRecreate(false);
222
+
clearCollectionCache(cacheKey());
223
refetch();
224
};
225
···
253
<StickyOverlay>
254
<div class="flex w-full flex-col gap-2">
255
<div class="flex items-center gap-1">
256
+
<Show when={agent() && agent()?.sub === did && hasUserScope("delete")}>
257
<div class="flex items-center">
258
<Tooltip
259
text={batchDelete() ? "Cancel" : "Delete"}
···
264
setLastSelected(undefined);
265
setBatchDelete(!batchDelete());
266
}}
267
+
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"
268
>
269
<span
270
+
class={`iconify ${batchDelete() ? "lucide--circle-x" : "lucide--trash-2"} `}
271
></span>
272
</button>
273
}
···
278
children={
279
<button
280
onclick={() => selectAll()}
281
+
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"
282
>
283
+
<span class="iconify lucide--copy-check"></span>
284
</button>
285
}
286
/>
287
+
<Show when={hasUserScope("create")}>
288
+
<Tooltip
289
+
text="Recreate"
290
+
children={
291
+
<button
292
+
onclick={() => {
293
+
setRecreate(true);
294
+
setOpenDelete(true);
295
+
}}
296
+
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"
297
+
>
298
+
<span class="iconify lucide--recycle text-green-500 dark:text-green-400"></span>
299
+
</button>
300
+
}
301
+
/>
302
+
</Show>
303
<Tooltip
304
text="Delete"
305
children={
···
308
setRecreate(false);
309
setOpenDelete(true);
310
}}
311
+
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"
312
>
313
+
<span class="iconify lucide--trash-2 text-red-500 dark:text-red-400"></span>
314
</button>
315
}
316
/>
···
334
</div>
335
</Modal>
336
</Show>
337
<TextInput
338
name="Filter"
339
placeholder="Filter by substring"
340
onInput={(e) => setFilter(e.currentTarget.value)}
341
class="grow"
342
/>
343
+
<Tooltip text="Jetstream">
344
+
<A
345
+
href={`/jetstream?collections=${params.collection}&dids=${params.repo}`}
346
+
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"
347
+
>
348
+
<span class="iconify lucide--radio-tower"></span>
349
+
</A>
350
+
</Tooltip>
351
</div>
352
<Show when={records.length > 1}>
353
<div class="flex items-center justify-between gap-x-2">
···
356
setReverse(!reverse());
357
setRecords([]);
358
setCursor(undefined);
359
+
clearCollectionCache(cacheKey());
360
refetch();
361
}}
362
>
···
404
</label>
405
</Show>
406
<Show when={!batchDelete()}>
407
+
<A href={`/at://${did}/${params.collection}/${record.rkey}`} class="select-none">
408
<RecordLink record={record} />
409
</A>
410
</Show>
+9
-9
src/views/home.tsx
+9
-9
src/views/home.tsx
···
1
export const Home = () => {
2
return (
3
<div class="flex w-full flex-col gap-3 wrap-break-word">
4
-
<div class="flex flex-col gap-0.5">
5
<div>
6
<span class="text-xl font-semibold">AT Protocol Explorer</span>
7
</div>
···
16
</span>
17
</div>
18
<div class="flex items-center gap-1">
19
-
<div class="iconify lucide--user-round" />
20
-
<span>Login to manage records in your repository.</span>
21
-
</div>
22
-
<div class="flex items-center gap-1">
23
-
<div class="iconify lucide--radio-tower" />
24
-
<span>Jetstream and firehose streaming.</span>
25
-
</div>
26
-
<div class="flex items-center gap-1">
27
<div class="iconify lucide--link" />
28
<span>
29
Backlinks support with{" "}
···
36
</a>
37
.
38
</span>
39
</div>
40
<div class="flex items-center gap-1">
41
<div class="iconify lucide--tag" />
···
1
export const Home = () => {
2
return (
3
<div class="flex w-full flex-col gap-3 wrap-break-word">
4
+
<div class="flex flex-col gap-1">
5
<div>
6
<span class="text-xl font-semibold">AT Protocol Explorer</span>
7
</div>
···
16
</span>
17
</div>
18
<div class="flex items-center gap-1">
19
<div class="iconify lucide--link" />
20
<span>
21
Backlinks support with{" "}
···
28
</a>
29
.
30
</span>
31
+
</div>
32
+
<div class="flex items-center gap-1">
33
+
<div class="iconify lucide--user-round" />
34
+
<span>Login to manage records in your repository.</span>
35
+
</div>
36
+
<div class="flex items-center gap-1">
37
+
<div class="iconify lucide--radio-tower" />
38
+
<span>Jetstream and firehose streaming.</span>
39
</div>
40
<div class="flex items-center gap-1">
41
<div class="iconify lucide--tag" />
+20
-22
src/views/labels.tsx
+20
-22
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 });
···
230
rows={2}
231
value={searchParams.uriPatterns ?? "*"}
232
placeholder="at://did:web:example.com/app.bsky.feed.post/*"
233
-
class="dark:bg-dark-100 dark:inset-shadow-dark-200 grow rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 py-1.5 text-sm inset-shadow-xs focus:outline-[1px] focus:outline-neutral-600 dark:border-neutral-600 dark:focus:outline-neutral-400"
234
/>
235
</label>
236
</div>
···
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 });
···
228
rows={2}
229
value={searchParams.uriPatterns ?? "*"}
230
placeholder="at://did:web:example.com/app.bsky.feed.post/*"
231
+
class="dark:bg-dark-100 grow rounded-lg bg-white px-2 py-1.5 text-sm outline-1 outline-neutral-200 focus:outline-[1.5px] focus:outline-neutral-600 dark:outline-neutral-600 dark:focus:outline-neutral-400"
232
/>
233
</label>
234
</div>
+11
-20
src/views/logs.tsx
+11
-20
src/views/logs.tsx
···
55
}
56
});
57
58
-
const FilterButton = (props: { icon: string; event: PlcEvent; label: string }) => {
59
const isActive = () => activePlcEvent() === props.event;
60
const toggleFilter = () => setActivePlcEvent(isActive() ? undefined : props.event);
61
62
return (
63
<button
64
classList={{
65
-
"flex items-center gap-1 sm:gap-1.5 rounded-lg px-3 py-2 sm:px-2 sm:py-1.5 text-base sm:text-sm transition-colors": true,
66
-
"bg-neutral-700 text-white dark:bg-neutral-200 dark:text-neutral-900": isActive(),
67
"bg-neutral-200 text-neutral-700 hover:bg-neutral-300 dark:bg-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-600":
68
!isActive(),
69
}}
70
onclick={toggleFilter}
71
>
72
-
<span class={props.icon}></span>
73
-
<span class="hidden font-medium sm:inline">{props.label}</span>
74
</button>
75
);
76
};
···
255
<div class="iconify lucide--filter" />
256
<p class="font-medium">Filter by type</p>
257
</div>
258
-
<div class="flex flex-wrap gap-1 sm:gap-2">
259
-
<FilterButton icon="iconify lucide--at-sign" event="handle" label="Alias" />
260
-
<FilterButton icon="iconify lucide--hard-drive" event="service" label="Service" />
261
-
<FilterButton
262
-
icon="iconify lucide--shield-check"
263
-
event="verification_method"
264
-
label="Verification"
265
-
/>
266
-
<FilterButton
267
-
icon="iconify lucide--key-round"
268
-
event="rotation_key"
269
-
label="Rotation Key"
270
-
/>
271
</div>
272
</div>
273
<div class="flex items-center gap-1.5 text-sm font-medium">
274
<Show when={validLog() === true}>
275
-
<span class="iconify lucide--check-circle-2 text-green-500 dark:text-green-400"></span>
276
<span>Valid log</span>
277
</Show>
278
<Show when={validLog() === false}>
279
-
<span class="iconify lucide--x-circle text-red-500 dark:text-red-400"></span>
280
<span>Log validation failed</span>
281
</Show>
282
<Show when={validLog() === undefined}>
···
55
}
56
});
57
58
+
const FilterButton = (props: { event: PlcEvent; label: string }) => {
59
const isActive = () => activePlcEvent() === props.event;
60
const toggleFilter = () => setActivePlcEvent(isActive() ? undefined : props.event);
61
62
return (
63
<button
64
classList={{
65
+
"font-medium rounded-lg px-2 py-1.5 text-xs sm:text-sm transition-colors": true,
66
+
"bg-neutral-700 text-white dark:bg-neutral-300 dark:text-neutral-900": isActive(),
67
"bg-neutral-200 text-neutral-700 hover:bg-neutral-300 dark:bg-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-600":
68
!isActive(),
69
}}
70
onclick={toggleFilter}
71
>
72
+
{props.label}
73
</button>
74
);
75
};
···
254
<div class="iconify lucide--filter" />
255
<p class="font-medium">Filter by type</p>
256
</div>
257
+
<div class="flex flex-wrap gap-1">
258
+
<FilterButton event="handle" label="Alias" />
259
+
<FilterButton event="service" label="Service" />
260
+
<FilterButton event="verification_method" label="Verification" />
261
+
<FilterButton event="rotation_key" label="Rotation Key" />
262
</div>
263
</div>
264
<div class="flex items-center gap-1.5 text-sm font-medium">
265
<Show when={validLog() === true}>
266
+
<span class="iconify lucide--check text-green-600 dark:text-green-400"></span>
267
<span>Valid log</span>
268
</Show>
269
<Show when={validLog() === false}>
270
+
<span class="iconify lucide--x text-red-500 dark:text-red-400"></span>
271
<span>Log validation failed</span>
272
</Show>
273
<Show when={validLog() === undefined}>
+57
-43
src/views/pds.tsx
+57
-43
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";
6
import { createResource, createSignal, For, Show } from "solid-js";
7
import { Button } from "../components/button";
8
-
import { CopyMenu, DropdownMenu, MenuProvider, NavMenu } from "../components/dropdown";
9
import { Modal } from "../components/modal";
10
import { setPDS } from "../components/navbar";
11
import Tooltip from "../components/tooltip";
12
import { localDateFromTimestamp } from "../utils/date";
13
14
const LIMIT = 1000;
···
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
···
54
55
const RepoCard = (repo: ComAtprotoSyncListRepos.Repo) => {
56
const [openInfo, setOpenInfo] = createSignal(false);
57
58
return (
59
<div class="flex items-center gap-0.5">
···
69
</Tooltip>
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">
···
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
···
122
);
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>
···
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
149
-
icon="lucide--ellipsis-vertical"
150
-
buttonClass="rounded-sm p-1.5"
151
-
menuClass="top-9 text-sm"
152
-
>
153
-
<CopyMenu content={params.pds!} label="Copy PDS" icon="lucide--copy" />
154
-
<NavMenu
155
-
href={`/firehose?instance=wss://${params.pds}`}
156
-
label="Firehose"
157
-
icon="lucide--radio-tower"
158
-
/>
159
-
</DropdownMenu>
160
-
</MenuProvider>
161
</div>
162
-
<div class="flex flex-col gap-1 px-2">
163
-
<Show when={!location.hash || location.hash === "#repos"}>
164
-
<div class="flex flex-col divide-y-[0.5px] divide-neutral-300 dark:divide-neutral-700">
165
-
<For each={repos()}>{(repo) => <RepoCard {...repo} />}</For>
166
-
</div>
167
-
</Show>
168
<Show when={location.hash === "#info"}>
169
<Show when={version()}>
170
{(version) => (
171
-
<div class="flex items-baseline gap-x-1">
172
<span class="font-semibold">Version</span>
173
-
<span class="truncate text-sm">{version()}</span>
174
</div>
175
)}
176
</Show>
177
<Show when={serverInfos()}>
178
{(server) => (
179
<>
180
-
<div class="flex items-baseline gap-x-1">
181
<span class="font-semibold">DID</span>
182
-
<span class="truncate text-sm">{server().did}</span>
183
</div>
184
-
<Show when={server().inviteCodeRequired}>
185
<span class="font-semibold">Invite Code Required</span>
186
-
</Show>
187
<Show when={server().phoneVerificationRequired}>
188
-
<span class="font-semibold">Phone Verification Required</span>
189
</Show>
190
<Show when={server().availableUserDomains.length}>
191
<div class="flex flex-col">
···
236
</div>
237
</div>
238
<Show when={!location.hash || location.hash === "#repos"}>
239
-
<div class="dark:bg-dark-500 fixed bottom-0 z-5 flex w-screen justify-center bg-neutral-100 py-2">
240
<div class="flex flex-col items-center gap-1 pb-2">
241
<p>{repos()?.length} loaded</p>
242
<Show when={!response.loading && cursor()}>
···
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";
6
import { createResource, createSignal, For, Show } from "solid-js";
7
import { Button } from "../components/button";
8
import { Modal } from "../components/modal";
9
import { setPDS } from "../components/navbar";
10
import Tooltip from "../components/tooltip";
11
+
import { resolveDidDoc } from "../utils/api";
12
import { localDateFromTimestamp } from "../utils/date";
13
14
const LIMIT = 1000;
···
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
···
54
55
const RepoCard = (repo: ComAtprotoSyncListRepos.Repo) => {
56
const [openInfo, setOpenInfo] = createSignal(false);
57
+
const [handle, setHandle] = createSignal<string>();
58
+
59
+
const fetchHandle = async () => {
60
+
try {
61
+
const doc = await resolveDidDoc(repo.did);
62
+
const aka = doc.alsoKnownAs?.find((a) => a.startsWith("at://"));
63
+
if (aka) setHandle(aka.replace("at://", ""));
64
+
} catch {}
65
+
};
66
67
return (
68
<div class="flex items-center gap-0.5">
···
78
</Tooltip>
79
</Show>
80
<button
81
+
onclick={() => {
82
+
setOpenInfo(true);
83
+
if (!handle()) fetchHandle();
84
+
}}
85
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"
86
>
87
+
<span class="iconify lucide--info text-neutral-600 dark:text-neutral-400"></span>
88
</button>
89
<Modal open={openInfo()} onClose={() => setOpenInfo(false)}>
90
<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">
···
98
</button>
99
</div>
100
<div class="grid grid-cols-[auto_1fr] items-baseline gap-x-1 gap-y-0.5 text-sm">
101
+
<span class="font-medium">Handle:</span>
102
+
<span class="text-neutral-700 dark:text-neutral-300">{handle()}</span>
103
<span class="font-medium">Head:</span>
104
<span class="wrap-anywhere text-neutral-700 dark:text-neutral-300">{repo.head}</span>
105
···
136
);
137
};
138
139
+
const Tab = (props: { tab: "repos" | "info" | "firehose"; label: string }) => (
140
<A
141
classList={{
142
+
"border-b-2 font-medium": true,
143
+
"border-transparent dark:text-neutral-300/80 text-neutral-600 hover:border-neutral-600 dark:hover:border-neutral-300/80":
144
(!!location.hash && location.hash !== `#${props.tab}`) ||
145
(!location.hash && props.tab !== "repos"),
146
}}
147
+
href={
148
+
props.tab === "firehose" ?
149
+
`/firehose?instance=wss://${params.pds}`
150
+
: `/${params.pds}#${props.tab}`
151
+
}
152
>
153
{props.label}
154
</A>
···
156
157
return (
158
<Show when={repos() || response()}>
159
+
<div class="flex w-full flex-col px-2">
160
+
<div class="mb-3 flex gap-4 text-sm sm:text-base">
161
+
<Tab tab="repos" label="Repositories" />
162
+
<Tab tab="info" label="Info" />
163
+
<Tab tab="firehose" label="Firehose" />
164
</div>
165
+
<Show when={!location.hash || location.hash === "#repos"}>
166
+
<div class="flex flex-col divide-y-[0.5px] divide-neutral-300 pb-20 dark:divide-neutral-700">
167
+
<For each={repos()}>{(repo) => <RepoCard {...repo} />}</For>
168
+
</div>
169
+
</Show>
170
+
<div class="flex flex-col gap-2">
171
<Show when={location.hash === "#info"}>
172
<Show when={version()}>
173
{(version) => (
174
+
<div class="flex flex-col">
175
<span class="font-semibold">Version</span>
176
+
<span class="text-sm text-neutral-700 dark:text-neutral-300">{version()}</span>
177
</div>
178
)}
179
</Show>
180
<Show when={serverInfos()}>
181
{(server) => (
182
<>
183
+
<div class="flex flex-col">
184
<span class="font-semibold">DID</span>
185
+
<span class="text-sm">{server().did}</span>
186
</div>
187
+
<div class="flex items-center gap-1">
188
<span class="font-semibold">Invite Code Required</span>
189
+
<span
190
+
classList={{
191
+
"iconify lucide--check text-green-500 dark:text-green-400":
192
+
server().inviteCodeRequired === true,
193
+
"iconify lucide--x text-red-500 dark:text-red-400":
194
+
!server().inviteCodeRequired,
195
+
}}
196
+
></span>
197
+
</div>
198
<Show when={server().phoneVerificationRequired}>
199
+
<div class="flex items-center gap-1">
200
+
<span class="font-semibold">Phone Verification Required</span>
201
+
<span class="iconify lucide--check text-green-500 dark:text-green-400"></span>
202
+
</div>
203
</Show>
204
<Show when={server().availableUserDomains.length}>
205
<div class="flex flex-col">
···
250
</div>
251
</div>
252
<Show when={!location.hash || location.hash === "#repos"}>
253
+
<div class="dark:bg-dark-500 fixed bottom-0 z-5 flex w-screen justify-center bg-neutral-100 pt-2 pb-4">
254
<div class="flex flex-col items-center gap-1 pb-2">
255
<p>{repos()?.length} loaded</p>
256
<Show when={!response.loading && cursor()}>
+40
-50
src/views/record.tsx
+40
-50
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
-
"border-b-2": true,
366
-
"border-transparent hover:border-neutral-400 dark:hover:border-neutral-600":
367
!isActive(),
368
}}
369
href={`/at://${did}/${params.collection}/${params.rkey}#${props.tab}`}
···
380
return (
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 items-center 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 text-sm"
421
-
>
422
<CopyMenu
423
content={JSON.stringify(record()?.value, null, 2)}
424
label="Copy record"
···
489
<Show when={location.hash === "#info"}>
490
<div class="flex w-full flex-col gap-2 px-2 text-sm">
491
<div>
492
-
<div class="flex items-center gap-1">
493
-
<span class="iconify lucide--at-sign"></span>
494
-
<p class="font-semibold">AT URI</p>
495
-
</div>
496
<div class="truncate text-xs">{record()?.uri}</div>
497
</div>
498
<Show when={record()?.cid}>
499
<div>
500
-
<div class="flex items-center gap-1">
501
-
<span class="iconify lucide--box"></span>
502
-
<p class="font-semibold">CID</p>
503
-
</div>
504
<div class="truncate text-left text-xs" dir="rtl">
505
{record()?.cid}
506
</div>
···
508
</Show>
509
<div>
510
<div class="flex items-center gap-1">
511
-
<span class="iconify lucide--lock-keyhole"></span>
512
<p class="font-semibold">Record verification</p>
513
<span
514
classList={{
···
525
</div>
526
<div>
527
<div class="flex items-center gap-1">
528
-
<span class="iconify lucide--file-check"></span>
529
<p class="font-semibold">Schema validation</p>
530
<span
531
classList={{
···
555
</div>
556
<Show when={lexiconUri()}>
557
<div>
558
-
<div class="flex items-center gap-1">
559
-
<span class="iconify lucide--scroll-text"></span>
560
-
<p class="font-semibold">Lexicon schema</p>
561
-
</div>
562
<div class="truncate text-xs">
563
<A
564
href={`/${lexiconUri()}`}
···
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 font-medium": true,
367
+
"border-transparent text-neutral-600 dark:text-neutral-300/80 hover:border-neutral-600 dark:hover:border-neutral-300/80":
368
!isActive(),
369
}}
370
href={`/at://${did}/${params.collection}/${params.rkey}#${props.tab}`}
···
381
return (
382
<Show when={record()} keyed>
383
<div class="flex w-full flex-col items-center">
384
+
<div class="mb-3 flex w-full justify-between px-2 text-sm sm:text-base">
385
+
<div class="flex items-center gap-4">
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"
···
490
<Show when={location.hash === "#info"}>
491
<div class="flex w-full flex-col gap-2 px-2 text-sm">
492
<div>
493
+
<p class="font-semibold">AT URI</p>
494
<div class="truncate text-xs">{record()?.uri}</div>
495
</div>
496
<Show when={record()?.cid}>
497
<div>
498
+
<p class="font-semibold">CID</p>
499
<div class="truncate text-left text-xs" dir="rtl">
500
{record()?.cid}
501
</div>
···
503
</Show>
504
<div>
505
<div class="flex items-center gap-1">
506
<p class="font-semibold">Record verification</p>
507
<span
508
classList={{
···
519
</div>
520
<div>
521
<div class="flex items-center gap-1">
522
<p class="font-semibold">Schema validation</p>
523
<span
524
classList={{
···
548
</div>
549
<Show when={lexiconUri()}>
550
<div>
551
+
<p class="font-semibold">Lexicon schema</p>
552
<div class="truncate text-xs">
553
<A
554
href={`/${lexiconUri()}`}
+184
-193
src/views/repo.tsx
+184
-193
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
···
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
>
···
113
if (!did.startsWith("did:")) {
114
try {
115
const did = await resolveHandle(params.repo as Handle);
116
-
navigate(location.pathname.replace(params.repo!, did));
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}`);
123
return;
124
} catch {
125
-
navigate(`/${did}`);
126
return;
127
}
128
}
···
139
return {};
140
}
141
142
-
rpc = new Client({ handler: new CredentialManager({ service: pds }) });
143
-
const res = await rpc.get("com.atproto.repo.describeRepo", {
144
-
params: { repo: did as ActorIdentifier },
145
-
});
146
-
if (res.ok) {
147
-
const collections: Record<string, { hidden: boolean; nsids: string[] }> = {};
148
-
res.data.collections.forEach((c) => {
149
-
const nsid = c.split(".");
150
-
if (nsid.length > 2) {
151
-
const authority = `${nsid[0]}.${nsid[1]}`;
152
-
collections[authority] = {
153
-
nsids: (collections[authority]?.nsids ?? []).concat(nsid.slice(2).join(".")),
154
-
hidden: false,
155
-
};
156
}
157
-
});
158
-
setNsids(collections);
159
-
} else {
160
-
console.error(res.data.error);
161
-
switch (res.data.error) {
162
-
case "RepoDeactivated":
163
-
setError("Deactivated");
164
-
break;
165
-
case "RepoTakendown":
166
-
setError("Takendown");
167
-
break;
168
-
default:
169
-
setError("Unreachable");
170
}
171
}
172
-
173
-
return res.data;
174
};
175
176
const [repo] = createResource(fetchRepo);
···
207
let loaded = 0;
208
209
const reader = response.body?.getReader();
210
-
const chunks: Uint8Array[] = [];
211
212
if (reader) {
213
while (true) {
···
271
return (
272
<Show when={repo()}>
273
<div class="flex w-full flex-col gap-3 wrap-break-word">
274
-
<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">
275
-
<div class="ml-1 flex items-center gap-2 text-xs sm:gap-4 sm:text-sm">
276
<Show when={!error()}>
277
<RepoTab tab="collections" label="Collections" />
278
</Show>
···
285
</Show>
286
<RepoTab tab="backlinks" label="Backlinks" />
287
</div>
288
-
<div class="flex gap-0.5">
289
<Show when={error() && error() !== "Missing PDS"}>
290
<div class="flex items-center gap-1 text-red-500 dark:text-red-400">
291
<span class="iconify lucide--alert-triangle"></span>
292
<span>{error()}</span>
293
</div>
294
</Show>
295
-
<Show when={!error() && (!location.hash || location.hash.startsWith("#collections"))}>
296
-
<Tooltip text="Filter collections">
297
-
<button
298
-
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"
299
-
onClick={() => setShowFilter(!showFilter())}
300
>
301
-
<span class="iconify lucide--filter"></span>
302
-
</button>
303
-
</Tooltip>
304
-
</Show>
305
-
<MenuProvider>
306
-
<DropdownMenu
307
-
icon="lucide--ellipsis-vertical"
308
-
buttonClass="rounded-sm p-1.5"
309
-
menuClass="top-9 text-sm"
310
-
>
311
<CopyMenu content={params.repo!} label="Copy DID" icon="lucide--copy" />
312
<NavMenu
313
href={`/jetstream?dids=${params.repo}`}
···
323
</Show>
324
<Show when={error()?.length === 0 || error() === undefined}>
325
<ActionMenu
326
-
label="Export Repo"
327
icon={downloading() ? "lucide--loader-circle animate-spin" : "lucide--download"}
328
onClick={() => downloadRepo()}
329
/>
···
336
: `https://${did.split("did:web:")[1]}/.well-known/did.json`
337
}
338
newTab
339
-
label="DID Document"
340
icon="lucide--external-link"
341
/>
342
<Show when={did.startsWith("did:plc")}>
343
<NavMenu
344
href={`${localStorage.plcDirectory ?? "https://plc.directory"}/${did}/log/audit`}
345
newTab
346
-
label="Audit Log"
347
icon="lucide--external-link"
348
/>
349
</Show>
···
404
/>
405
</Show>
406
<div class="flex flex-col text-sm wrap-anywhere" classList={{ "-mt-1": !showFilter() }}>
407
-
<For
408
-
each={Object.keys(nsids() ?? {}).filter((authority) =>
409
-
filter() ?
410
-
authority.includes(filter()!) ||
411
-
nsids()?.[authority].nsids.some((nsid) =>
412
-
`${authority}.${nsid}`.includes(filter()!),
413
-
)
414
-
: true,
415
-
)}
416
>
417
-
{(authority) => {
418
-
const reversedDomain = authority.split(".").reverse().join(".");
419
-
const [faviconLoaded, setFaviconLoaded] = createSignal(false);
420
421
-
const isHighlighted = () => location.hash === `#collections:${authority}`;
422
423
-
return (
424
-
<div
425
-
id={`collection-${authority}`}
426
-
class="group flex items-start gap-2 rounded-lg p-1 transition-colors"
427
-
classList={{
428
-
"dark:hover:bg-dark-200 hover:bg-neutral-200": !isHighlighted(),
429
-
"bg-blue-100 dark:bg-blue-500/25": isHighlighted(),
430
-
}}
431
-
>
432
-
<a
433
-
href={`#collections:${authority}`}
434
-
class="relative flex h-5 w-4 shrink-0 items-center justify-center hover:opacity-70"
435
>
436
-
<span class="absolute top-1/2 -left-5 flex -translate-y-1/2 items-center text-base opacity-0 transition-opacity group-hover:opacity-100">
437
-
<span class="iconify lucide--link absolute -left-2 w-7"></span>
438
-
</span>
439
-
<Show when={!faviconLoaded()}>
440
-
<span class="iconify lucide--globe size-4 text-neutral-400 dark:text-neutral-500" />
441
-
</Show>
442
-
<img
443
-
src={
444
-
["bsky.app", "bsky.chat"].includes(reversedDomain) ?
445
-
"https://web-cdn.bsky.app/static/apple-touch-icon.png"
446
-
: `https://${reversedDomain}/favicon.ico`
447
-
}
448
-
alt={`${reversedDomain} favicon`}
449
-
class="h-4 w-4"
450
-
classList={{ hidden: !faviconLoaded() }}
451
-
onLoad={() => setFaviconLoaded(true)}
452
-
onError={() => setFaviconLoaded(false)}
453
-
/>
454
-
</a>
455
-
<div class="flex flex-1 flex-col">
456
-
<For
457
-
each={nsids()?.[authority].nsids.filter((nsid) =>
458
-
filter() ? `${authority}.${nsid}`.includes(filter()!) : true,
459
-
)}
460
>
461
-
{(nsid) => (
462
-
<A
463
-
href={`/at://${did}/${authority}.${nsid}`}
464
-
class="hover:underline active:underline"
465
-
>
466
-
<span>{authority}</span>
467
-
<span class="text-neutral-500 dark:text-neutral-400">.{nsid}</span>
468
-
</A>
469
-
)}
470
-
</For>
471
</div>
472
-
</div>
473
-
);
474
-
}}
475
-
</For>
476
</div>
477
</Show>
478
<Show when={location.hash === "#identity" || (error() && !location.hash)}>
479
<Show when={didDoc()}>
480
{(didDocument) => (
481
-
<div class="flex flex-col gap-2 wrap-anywhere">
482
{/* ID Section */}
483
<div>
484
-
<div class="flex items-center gap-1">
485
-
<div class="iconify lucide--id-card" />
486
-
<p class="font-semibold">ID</p>
487
</div>
488
-
<div class="text-sm">{didDocument().id}</div>
489
</div>
490
491
{/* Aliases Section */}
492
<div>
493
-
<div class="flex items-center gap-1">
494
-
<div class="iconify lucide--at-sign" />
495
-
<p class="font-semibold">Aliases</p>
496
-
</div>
497
-
<div class="flex flex-col gap-0.5">
498
-
<For each={didDocument().alsoKnownAs}>
499
-
{(alias) => (
500
-
<div class="flex items-center gap-1 text-sm">
501
-
<span>{alias}</span>
502
-
<Show when={alias.startsWith("at://")}>
503
-
<Tooltip
504
-
text={
505
-
validHandles[alias] === true ? "Valid handle"
506
-
: validHandles[alias] === undefined ?
507
-
"Validating"
508
-
: "Invalid handle"
509
-
}
510
-
>
511
-
<span
512
-
classList={{
513
-
"iconify lucide--circle-check text-green-600 dark:text-green-400":
514
-
validHandles[alias] === true,
515
-
"iconify lucide--circle-x text-red-500 dark:text-red-400":
516
-
validHandles[alias] === false,
517
-
"iconify lucide--loader-circle animate-spin":
518
-
validHandles[alias] === undefined,
519
-
}}
520
-
></span>
521
-
</Tooltip>
522
-
</Show>
523
-
</div>
524
-
)}
525
-
</For>
526
-
</div>
527
</div>
528
529
{/* Services Section */}
530
<div>
531
-
<div class="flex items-center gap-1">
532
-
<div class="iconify lucide--hard-drive" />
533
-
<p class="font-semibold">Services</p>
534
-
</div>
535
-
<div class="flex flex-col gap-0.5">
536
<For each={didDocument().service}>
537
{(service) => (
538
-
<div class="text-sm">
539
-
<div class="font-medium text-neutral-700 dark:text-neutral-300">
540
-
#{service.id.split("#")[1]}
541
-
</div>
542
<a
543
-
class="underline hover:text-blue-400"
544
href={service.serviceEndpoint.toString()}
545
target="_blank"
546
rel="noopener"
···
555
556
{/* Verification Methods Section */}
557
<div>
558
-
<div class="flex items-center gap-1">
559
-
<div class="iconify lucide--shield-check" />
560
-
<p class="font-semibold">Verification Methods</p>
561
-
</div>
562
-
<div class="flex flex-col gap-0.5">
563
<For each={didDocument().verificationMethod}>
564
{(verif) => (
565
<Show when={verif.publicKeyMultibase}>
566
{(key) => (
567
-
<div class="text-sm">
568
-
<div class="flex items-baseline gap-1">
569
-
<span class="font-medium text-neutral-700 dark:text-neutral-300">
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
-
<ErrorBoundary fallback={<>unknown</>}>
574
-
{parsePublicMultikey(key()).type}
575
-
</ErrorBoundary>
576
-
</span>
577
</div>
578
<div class="font-mono break-all">{key()}</div>
579
</div>
580
)}
···
587
{/* Rotation Keys Section */}
588
<Show when={rotationKeys().length > 0}>
589
<div>
590
-
<div class="flex items-center gap-1">
591
-
<div class="iconify lucide--key-round" />
592
-
<p class="font-semibold">Rotation Keys</p>
593
-
</div>
594
-
<div class="flex flex-col gap-0.5">
595
<For each={rotationKeys()}>
596
{(key) => (
597
-
<div class="text-sm">
598
-
<span class="rounded bg-neutral-200 px-1 py-0.5 text-xs text-neutral-800 dark:bg-neutral-700 dark:text-neutral-300">
599
-
{parseDidKey(key).type}
600
</span>
601
<div class="font-mono break-all">{key.replace("did:key:", "")}</div>
602
</div>
603
)}
···
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
···
88
return (
89
<A
90
classList={{
91
+
"border-b-2 font-medium": true,
92
+
"border-transparent text-neutral-600 dark:text-neutral-300/80 hover:border-neutral-600 dark:hover:border-neutral-300/80":
93
+
!isActive(),
94
}}
95
href={`/at://${params.repo}#${props.tab}`}
96
>
···
114
if (!did.startsWith("did:")) {
115
try {
116
const did = await resolveHandle(params.repo as Handle);
117
+
navigate(location.pathname.replace(params.repo!, did), { replace: true });
118
return;
119
} catch {
120
try {
121
const nsid = params.repo as Nsid;
122
const res = await resolveLexiconAuthority(nsid);
123
+
navigate(`/at://${res}/com.atproto.lexicon.schema/${nsid}`, { replace: true });
124
return;
125
} catch {
126
+
navigate(`/${did}`, { replace: true });
127
return;
128
}
129
}
···
140
return {};
141
}
142
143
+
rpc = new Client({ handler: simpleFetchHandler({ service: pds }) });
144
+
try {
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
+
} catch {
177
+
return {};
178
}
179
};
180
181
const [repo] = createResource(fetchRepo);
···
212
let loaded = 0;
213
214
const reader = response.body?.getReader();
215
+
const chunks: BlobPart[] = [];
216
217
if (reader) {
218
while (true) {
···
276
return (
277
<Show when={repo()}>
278
<div class="flex w-full flex-col gap-3 wrap-break-word">
279
+
<div class="flex justify-between px-2 text-sm sm:text-base">
280
+
<div class="flex items-center gap-3 sm:gap-4">
281
<Show when={!error()}>
282
<RepoTab tab="collections" label="Collections" />
283
</Show>
···
290
</Show>
291
<RepoTab tab="backlinks" label="Backlinks" />
292
</div>
293
+
<div class="flex gap-1">
294
<Show when={error() && error() !== "Missing PDS"}>
295
<div class="flex items-center gap-1 text-red-500 dark:text-red-400">
296
<span class="iconify lucide--alert-triangle"></span>
297
<span>{error()}</span>
298
</div>
299
</Show>
300
+
<MenuProvider>
301
+
<DropdownMenu icon="lucide--ellipsis-vertical" buttonClass="rounded-sm p-1.5">
302
+
<Show
303
+
when={!error() && (!location.hash || location.hash.startsWith("#collections"))}
304
>
305
+
<ActionMenu
306
+
label="Filter collections"
307
+
icon="lucide--filter"
308
+
onClick={() => setShowFilter(!showFilter())}
309
+
/>
310
+
</Show>
311
<CopyMenu content={params.repo!} label="Copy DID" icon="lucide--copy" />
312
<NavMenu
313
href={`/jetstream?dids=${params.repo}`}
···
323
</Show>
324
<Show when={error()?.length === 0 || error() === undefined}>
325
<ActionMenu
326
+
label="Export repo"
327
icon={downloading() ? "lucide--loader-circle animate-spin" : "lucide--download"}
328
onClick={() => downloadRepo()}
329
/>
···
336
: `https://${did.split("did:web:")[1]}/.well-known/did.json`
337
}
338
newTab
339
+
label="DID document"
340
icon="lucide--external-link"
341
/>
342
<Show when={did.startsWith("did:plc")}>
343
<NavMenu
344
href={`${localStorage.plcDirectory ?? "https://plc.directory"}/${did}/log/audit`}
345
newTab
346
+
label="Audit log"
347
icon="lucide--external-link"
348
/>
349
</Show>
···
404
/>
405
</Show>
406
<div class="flex flex-col text-sm wrap-anywhere" classList={{ "-mt-1": !showFilter() }}>
407
+
<Show
408
+
when={Object.keys(nsids() ?? {}).length != 0}
409
+
fallback={<span class="mt-3 text-center text-base">No collections found.</span>}
410
>
411
+
<For
412
+
each={Object.keys(nsids() ?? {}).filter((authority) =>
413
+
filter() ?
414
+
authority.includes(filter()!) ||
415
+
nsids()?.[authority].nsids.some((nsid) =>
416
+
`${authority}.${nsid}`.includes(filter()!),
417
+
)
418
+
: true,
419
+
)}
420
+
>
421
+
{(authority) => {
422
+
const reversedDomain = authority.split(".").reverse().join(".");
423
+
const [faviconLoaded, setFaviconLoaded] = createSignal(false);
424
425
+
const isHighlighted = () => location.hash === `#collections:${authority}`;
426
427
+
return (
428
+
<div
429
+
id={`collection-${authority}`}
430
+
class="group flex items-start gap-2 rounded-lg p-1 transition-colors"
431
+
classList={{
432
+
"dark:hover:bg-dark-200 hover:bg-neutral-200": !isHighlighted(),
433
+
"bg-blue-100 dark:bg-blue-500/25": isHighlighted(),
434
+
}}
435
>
436
+
<a
437
+
href={`#collections:${authority}`}
438
+
class="relative flex h-5 w-4 shrink-0 items-center justify-center hover:opacity-70"
439
>
440
+
<span class="absolute top-1/2 -left-5 flex -translate-y-1/2 items-center text-base opacity-0 transition-opacity group-hover:opacity-100">
441
+
<span class="iconify lucide--link absolute -left-2 w-7"></span>
442
+
</span>
443
+
<Show when={!faviconLoaded()}>
444
+
<span class="iconify lucide--globe size-4 text-neutral-400 dark:text-neutral-500" />
445
+
</Show>
446
+
<img
447
+
src={
448
+
["bsky.app", "bsky.chat"].includes(reversedDomain) ?
449
+
"https://web-cdn.bsky.app/static/apple-touch-icon.png"
450
+
: `https://${reversedDomain}/favicon.ico`
451
+
}
452
+
alt={`${reversedDomain} favicon`}
453
+
class="h-4 w-4"
454
+
classList={{ hidden: !faviconLoaded() }}
455
+
onLoad={() => setFaviconLoaded(true)}
456
+
onError={() => setFaviconLoaded(false)}
457
+
/>
458
+
</a>
459
+
<div class="flex flex-1 flex-col">
460
+
<For
461
+
each={nsids()?.[authority].nsids.filter((nsid) =>
462
+
filter() ? `${authority}.${nsid}`.includes(filter()!) : true,
463
+
)}
464
+
>
465
+
{(nsid) => (
466
+
<A
467
+
href={`/at://${did}/${authority}.${nsid}`}
468
+
class="hover:underline active:underline"
469
+
>
470
+
<span>{authority}</span>
471
+
<span class="text-neutral-500 dark:text-neutral-400">.{nsid}</span>
472
+
</A>
473
+
)}
474
+
</For>
475
+
</div>
476
</div>
477
+
);
478
+
}}
479
+
</For>
480
+
</Show>
481
</div>
482
</Show>
483
<Show when={location.hash === "#identity" || (error() && !location.hash)}>
484
<Show when={didDoc()}>
485
{(didDocument) => (
486
+
<div class="flex flex-col gap-3 wrap-anywhere">
487
{/* ID Section */}
488
<div>
489
+
<div class="font-semibold">DID</div>
490
+
<div class="text-sm text-neutral-700 dark:text-neutral-300">
491
+
{didDocument().id}
492
</div>
493
</div>
494
495
{/* Aliases Section */}
496
<div>
497
+
<p class="font-semibold">Aliases</p>
498
+
<For each={didDocument().alsoKnownAs}>
499
+
{(alias) => (
500
+
<div class="flex items-center gap-1 text-sm text-neutral-700 dark:text-neutral-300">
501
+
<span>{alias}</span>
502
+
<Show when={alias.startsWith("at://")}>
503
+
<Tooltip
504
+
text={
505
+
validHandles[alias] === true ? "Valid handle"
506
+
: validHandles[alias] === undefined ?
507
+
"Validating"
508
+
: "Invalid handle"
509
+
}
510
+
>
511
+
<span
512
+
classList={{
513
+
"iconify lucide--check text-green-600 dark:text-green-400":
514
+
validHandles[alias] === true,
515
+
"iconify lucide--x text-red-500 dark:text-red-400":
516
+
validHandles[alias] === false,
517
+
"iconify lucide--loader-circle animate-spin":
518
+
validHandles[alias] === undefined,
519
+
}}
520
+
></span>
521
+
</Tooltip>
522
+
</Show>
523
+
</div>
524
+
)}
525
+
</For>
526
</div>
527
528
{/* Services Section */}
529
<div>
530
+
<p class="font-semibold">Services</p>
531
+
<div class="flex flex-col gap-1">
532
<For each={didDocument().service}>
533
{(service) => (
534
+
<div class="grid grid-cols-[auto_1fr] items-center gap-x-1 text-sm text-neutral-700 dark:text-neutral-300">
535
+
<span class="iconify lucide--hash"></span>
536
+
<span>{service.id.split("#")[1]}</span>
537
+
<span></span>
538
<a
539
+
class="w-fit underline hover:text-blue-400"
540
href={service.serviceEndpoint.toString()}
541
target="_blank"
542
rel="noopener"
···
551
552
{/* Verification Methods Section */}
553
<div>
554
+
<p class="font-semibold">Verification Methods</p>
555
+
<div class="flex flex-col gap-1">
556
<For each={didDocument().verificationMethod}>
557
{(verif) => (
558
<Show when={verif.publicKeyMultibase}>
559
{(key) => (
560
+
<div class="grid grid-cols-[auto_1fr] items-center gap-x-1 text-sm text-neutral-700 dark:text-neutral-300">
561
+
<span class="iconify lucide--hash"></span>
562
+
<div class="flex items-center gap-2">
563
+
<span>{verif.id.split("#")[1]}</span>
564
+
<div class="flex items-center gap-1 text-neutral-500 dark:text-neutral-400">
565
+
<span class="iconify lucide--key-round"></span>
566
+
<span>{detectKeyType(key())}</span>
567
+
</div>
568
</div>
569
+
<span></span>
570
<div class="font-mono break-all">{key()}</div>
571
</div>
572
)}
···
579
{/* Rotation Keys Section */}
580
<Show when={rotationKeys().length > 0}>
581
<div>
582
+
<p class="font-semibold">Rotation Keys</p>
583
+
<div class="flex flex-col gap-1">
584
<For each={rotationKeys()}>
585
{(key) => (
586
+
<div class="grid grid-cols-[auto_1fr] items-center gap-x-1 text-sm text-neutral-700 dark:text-neutral-300">
587
+
<span class="iconify lucide--key-round text-neutral-500 dark:text-neutral-400"></span>
588
+
<span class="text-neutral-500 dark:text-neutral-400">
589
+
{detectDidKeyType(key)}
590
</span>
591
+
<span></span>
592
<div class="font-mono break-all">{key.replace("did:key:", "")}</div>
593
</div>
594
)}
+8
-8
src/views/stream.tsx
+8
-8
src/views/stream.tsx
···
143
144
return (
145
<div class="flex w-full flex-col items-center">
146
-
<div class="flex gap-2 text-sm">
147
<A
148
-
class="flex items-center gap-1 border-b-2 p-1"
149
-
inactiveClass="border-transparent hover:border-neutral-400 dark:hover:border-neutral-600"
150
href="/jetstream"
151
>
152
Jetstream
153
</A>
154
<A
155
-
class="flex items-center gap-1 border-b-2 p-1"
156
-
inactiveClass="border-transparent hover:border-neutral-400 dark:hover:border-neutral-600"
157
href="/firehose"
158
>
159
Firehose
160
</A>
161
</div>
162
<StickyOverlay>
163
-
<form ref={formRef} class="flex w-full flex-col gap-1 text-sm">
164
<Show when={!connected()}>
165
<label class="flex items-center justify-end gap-x-1">
166
<span class="min-w-20">Instance</span>
···
183
spellcheck={false}
184
placeholder="Comma-separated list of collections"
185
value={searchParams.collections ?? ""}
186
-
class="dark:bg-dark-100 dark:inset-shadow-dark-200 grow rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 py-1 inset-shadow-xs focus:outline-[1px] focus:outline-neutral-600 dark:border-neutral-600 dark:focus:outline-neutral-400"
187
/>
188
</label>
189
</Show>
···
195
spellcheck={false}
196
placeholder="Comma-separated list of DIDs"
197
value={searchParams.dids ?? ""}
198
-
class="dark:bg-dark-100 dark:inset-shadow-dark-200 grow rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 py-1 inset-shadow-xs focus:outline-[1px] focus:outline-neutral-600 dark:border-neutral-600 dark:focus:outline-neutral-400"
199
/>
200
</label>
201
</Show>
···
143
144
return (
145
<div class="flex w-full flex-col items-center">
146
+
<div class="mb-1 flex gap-4 font-medium">
147
<A
148
+
class="flex items-center gap-1 border-b-2"
149
+
inactiveClass="border-transparent text-neutral-600 dark:text-neutral-400 hover:border-neutral-400 dark:hover:border-neutral-600"
150
href="/jetstream"
151
>
152
Jetstream
153
</A>
154
<A
155
+
class="flex items-center gap-1 border-b-2"
156
+
inactiveClass="border-transparent text-neutral-600 dark:text-neutral-400 hover:border-neutral-400 dark:hover:border-neutral-600"
157
href="/firehose"
158
>
159
Firehose
160
</A>
161
</div>
162
<StickyOverlay>
163
+
<form ref={formRef} class="flex w-full flex-col gap-1.5 text-sm">
164
<Show when={!connected()}>
165
<label class="flex items-center justify-end gap-x-1">
166
<span class="min-w-20">Instance</span>
···
183
spellcheck={false}
184
placeholder="Comma-separated list of collections"
185
value={searchParams.collections ?? ""}
186
+
class="dark:bg-dark-100 grow rounded-lg bg-white px-2 py-1 outline-1 outline-neutral-200 focus:outline-[1.5px] focus:outline-neutral-600 dark:outline-neutral-600 dark:focus:outline-neutral-400"
187
/>
188
</label>
189
</Show>
···
195
spellcheck={false}
196
placeholder="Comma-separated list of DIDs"
197
value={searchParams.dids ?? ""}
198
+
class="dark:bg-dark-100 grow rounded-lg bg-white px-2 py-1 outline-1 outline-neutral-200 focus:outline-[1.5px] focus:outline-neutral-600 dark:outline-neutral-600 dark:focus:outline-neutral-400"
199
/>
200
</label>
201
</Show>