Compare changes

Choose any two refs to compare.

+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
···
··· 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 +
-2
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" />
··· 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" />
+19 -19
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.4", 16 "prettier-plugin-organize-imports": "^4.3.0", 17 "prettier-plugin-tailwindcss": "^0.7.2", 18 - "tailwindcss": "^4.1.17", 19 "typescript": "^5.9.3", 20 - "vite": "^7.2.6", 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/multibase": "^1.1.6", 36 - "@atcute/oauth-browser-client": "^2.0.1", 37 - "@atcute/repo": "^0.1.0", 38 - "@atcute/tangled": "^1.0.12", 39 - "@atcute/tid": "^1.0.3", 40 - "@codemirror/commands": "^6.10.0", 41 "@codemirror/lang-json": "^6.0.2", 42 "@codemirror/lint": "^6.9.2", 43 - "@codemirror/state": "^6.5.2", 44 - "@codemirror/view": "^6.38.8", 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",
··· 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",
+535 -489
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/multibase': 45 specifier: ^1.1.6 46 version: 1.1.6 47 '@atcute/oauth-browser-client': 48 - specifier: ^2.0.1 49 - version: 2.0.1 50 '@atcute/repo': 51 - specifier: ^0.1.0 52 - version: 0.1.0 53 '@atcute/tangled': 54 - specifier: ^1.0.12 55 - version: 1.0.12 56 '@atcute/tid': 57 - specifier: ^1.0.3 58 - version: 1.0.3 59 '@codemirror/commands': 60 - specifier: ^6.10.0 61 - version: 6.10.0 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.2 70 - version: 6.5.2 71 '@codemirror/view': 72 - specifier: ^6.38.8 73 - version: 6.38.8 74 '@fsegurai/codemirror-theme-basic-dark': 75 specifier: ^6.2.3 76 - version: 6.2.3(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.8)(@lezer/highlight@1.2.3) 77 '@fsegurai/codemirror-theme-basic-light': 78 specifier: ^6.2.3 79 - version: 6.2.3(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.8)(@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.77 101 - version: 1.2.77 102 '@iconify/tailwind4': 103 specifier: ^1.2.0 104 - version: 1.2.0(tailwindcss@4.1.17) 105 '@tailwindcss/vite': 106 - specifier: ^4.1.17 107 - version: 4.1.17(vite@7.2.6(@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 ··· 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.17 119 - version: 4.1.17 120 typescript: 121 specifier: ^5.9.3 122 version: 5.9.3 123 vite: 124 - specifier: ^7.2.6 125 - version: 7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2) 126 vite-plugin-solid: 127 specifier: ^2.11.10 128 - version: 2.11.10(solid-js@1.9.10)(vite@7.2.6(@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.11': 139 - resolution: {integrity: sha512-AboS6y4t+zaxIq7E4noue10csSpIuk/Uwo30/l6GgGBDPXrd7STw8Yb5nGZQP+TdG/uC8/c2mm7UnY65SDOh6A==} 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.2.6': 151 - resolution: {integrity: sha512-bTAHHbJ24p+E//V4KCS4xdmd39o211jJswvqQOevj7vk+5IYcgDLx1ryZWZ1sEPOo9x875li/kj5gpKL14RDwQ==} 152 153 - '@atcute/client@4.1.0': 154 - resolution: {integrity: sha512-AYhSu3RSDA2VDkVGOmad320NRbUUUf5pCFWJcOzlk25YC/4kyzmMFfpzhf1jjjEcY+anNBXGGhav/kKB1evggQ==} 155 156 - '@atcute/crypto@2.2.6': 157 - resolution: {integrity: sha512-vkuexF+kmrKE1/Uqzub99Qi4QpnxA2jbu60E6PTgL4XypELQ6rb59MB/J1VbY2gs0kd3ET7+L3+NWpKD5nXyfA==} 158 159 - '@atcute/did-plc@0.2.0': 160 - resolution: {integrity: sha512-1sGek8GRM/Ph7nLVRREm8FqM7g4shGckItvdVwJcRbUa8Rh0zOsXQa0QyYWAC0k40BhkqO9FwKXhJEaXCmF5oQ==} 161 162 - '@atcute/identity-resolver@1.1.4': 163 - resolution: {integrity: sha512-/SVh8vf2cXFJenmBnGeYF2aY3WGQm3cJeew5NWTlkqoy3LvJ5wkvKq9PWu4Tv653VF40rPOp6LOdVr9Fa+q5rA==} 164 peerDependencies: 165 '@atcute/identity': ^1.0.0 166 167 '@atcute/identity@1.1.3': 168 resolution: {integrity: sha512-oIqPoI8TwWeQxvcLmFEZLdN2XdWcaLVtlm8pNk0E72As9HNzzD9pwKPrLr3rmTLRIoULPPFmq9iFNsTeCIU9ng==} 169 170 - '@atcute/leaflet@1.0.12': 171 - resolution: {integrity: sha512-T5laBTl8vwzy0eZXBy07IQSjsLqhbZmRJsffnNQ6XMSc+lnCZ/NHfuKy8TNJbDU6dc26Z7o5l0ELfWz5QESo+w==} 172 173 - '@atcute/lexicon-doc@2.0.4': 174 - resolution: {integrity: sha512-YfwlYFoYiBvRIYG0I1zsINCTFugFtS8l67uT3nQ04zdKVflzdg8uUj8cNZYRNY1V7okoOPdikhR4kPFhYGyemw==} 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.5': 183 - resolution: {integrity: sha512-9yO9WdgxW8jZ7SbzUycH710z+JmsQ9W9n5S6i6eghYju32kkluFmgBeS47r8e8p2+Dv4DemS7o/3SUGsX9FR5Q==} 184 185 - '@atcute/mst@0.1.0': 186 - resolution: {integrity: sha512-h+iDToKEnBpigk2DOHjSqY63vJtjYKUIztqu1CZ0P+I54wV2SrgoqAXAT1xrW6A1Iup8cjTv+U2H5WVG4KxPLw==} 187 188 '@atcute/multibase@1.1.6': 189 resolution: {integrity: sha512-HBxuCgYLKPPxETV0Rot4VP9e24vKl8JdzGCZOVsDaOXJgbRZoRIF67Lp0H/OgnJeH/Xpva8Z5ReoTNJE5dn3kg==} 190 191 - '@atcute/oauth-browser-client@2.0.1': 192 - resolution: {integrity: sha512-lG021GkeORG06zfFf4bH85egObjBEKHNgAWHvbtY/E2dX4wxo88hf370pJDx8acdnuUJLJ2VKPikJtZwo4Heeg==} 193 194 - '@atcute/repo@0.1.0': 195 - resolution: {integrity: sha512-INiYAuma8dydBu7cqd2WVpcXh3mzhIepYBUqFWAK5MqMulPRLTRCc/9GW3G9pxYrOdlvLCVamG2Jf8XK0nuFEw==} 196 197 - '@atcute/tangled@1.0.12': 198 - resolution: {integrity: sha512-JKA5sOhd8SLhDFhY+PKHqLLytQBBKSiwcaEzfYUJBeyfvqXFPNNAwvRbe3VST4IQ3izoOu3O0R9/b1mjL45UzA==} 199 200 - '@atcute/tid@1.0.3': 201 - resolution: {integrity: sha512-wfMJx1IMdnu0CZgWl0uR4JO2s6PGT1YPhpytD4ZHzEYKKQVuqV6Eb/7vieaVo1eYNMp2FrY67FZObeR7utRl2w==} 202 203 - '@atcute/uint8array@1.0.5': 204 - resolution: {integrity: sha512-XLWWxoR2HNl2qU+FCr0rp1APwJXci7HnzbOQLxK55OaMNBXZ19+xNC5ii4QCsThsDxa4JS/JTzuiQLziITWf2Q==} 205 206 '@atcute/util-fetch@1.0.4': 207 resolution: {integrity: sha512-sIU9Qk0dE8PLEXSfhy+gIJV+HpiiknMytCI2SqLlqd0vgZUtEKI/EQfP+23LHWvP+CLCzVDOa6cpH045OlmNBg==} 208 209 '@atcute/varint@1.0.3': 210 resolution: {integrity: sha512-fdvMPyBB+McDT+Ai5e9RwEbwYV4yjZ60S2Dn5PTjGqUyxvoCH1z42viuheDZRUDkmfQehXJTZ5az7dSozVNtog==} ··· 297 '@codemirror/autocomplete@6.20.0': 298 resolution: {integrity: sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==} 299 300 - '@codemirror/commands@6.10.0': 301 - resolution: {integrity: sha512-2xUIc5mHXQzT16JnyOFkh8PvfeXuIut3pslWGfsGOhxP/lpgRm9HOl/mpzLErgt5mXDovqA0d11P21gofRLb9w==} 302 303 '@codemirror/lang-json@6.0.2': 304 resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==} 305 306 - '@codemirror/language@6.11.3': 307 - resolution: {integrity: sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==} 308 309 '@codemirror/lint@6.9.2': 310 resolution: {integrity: sha512-sv3DylBiIyi+xKwRCJAAsBZZZWo82shJ/RTMymLabAdtbkV5cSKwWDeCgtUq3v8flTaXS2y1kKkICuRYtUswyQ==} ··· 312 '@codemirror/search@6.5.11': 313 resolution: {integrity: sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==} 314 315 - '@codemirror/state@6.5.2': 316 - resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==} 317 318 - '@codemirror/view@6.38.8': 319 - resolution: {integrity: sha512-XcE9fcnkHCbWkjeKyi0lllwXmBLtyYb5dt89dJyx23I9+LSh5vZDIuk7OLG4VM1lgrXZQcY6cxyZyk5WVPRv/A==} 320 321 '@cyberalien/svg-utils@1.0.11': 322 resolution: {integrity: sha512-qEE9mnyI+avfGT3emKuRs3ucYkITeaV0Xi7VlYN41f+uGnZBecQP3jwz/AF437H9J4Q7qPClHKm4NiTYpNE6hA==} ··· 327 cpu: [ppc64] 328 os: [aix] 329 330 - '@esbuild/aix-ppc64@0.25.12': 331 - resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} 332 engines: {node: '>=18'} 333 cpu: [ppc64] 334 os: [aix] ··· 339 cpu: [arm64] 340 os: [android] 341 342 - '@esbuild/android-arm64@0.25.12': 343 - resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} 344 engines: {node: '>=18'} 345 cpu: [arm64] 346 os: [android] ··· 351 cpu: [arm] 352 os: [android] 353 354 - '@esbuild/android-arm@0.25.12': 355 - resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} 356 engines: {node: '>=18'} 357 cpu: [arm] 358 os: [android] ··· 363 cpu: [x64] 364 os: [android] 365 366 - '@esbuild/android-x64@0.25.12': 367 - resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} 368 engines: {node: '>=18'} 369 cpu: [x64] 370 os: [android] ··· 375 cpu: [arm64] 376 os: [darwin] 377 378 - '@esbuild/darwin-arm64@0.25.12': 379 - resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} 380 engines: {node: '>=18'} 381 cpu: [arm64] 382 os: [darwin] ··· 387 cpu: [x64] 388 os: [darwin] 389 390 - '@esbuild/darwin-x64@0.25.12': 391 - resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} 392 engines: {node: '>=18'} 393 cpu: [x64] 394 os: [darwin] ··· 399 cpu: [arm64] 400 os: [freebsd] 401 402 - '@esbuild/freebsd-arm64@0.25.12': 403 - resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} 404 engines: {node: '>=18'} 405 cpu: [arm64] 406 os: [freebsd] ··· 411 cpu: [x64] 412 os: [freebsd] 413 414 - '@esbuild/freebsd-x64@0.25.12': 415 - resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} 416 engines: {node: '>=18'} 417 cpu: [x64] 418 os: [freebsd] ··· 423 cpu: [arm64] 424 os: [linux] 425 426 - '@esbuild/linux-arm64@0.25.12': 427 - resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} 428 engines: {node: '>=18'} 429 cpu: [arm64] 430 os: [linux] ··· 435 cpu: [arm] 436 os: [linux] 437 438 - '@esbuild/linux-arm@0.25.12': 439 - resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} 440 engines: {node: '>=18'} 441 cpu: [arm] 442 os: [linux] ··· 447 cpu: [ia32] 448 os: [linux] 449 450 - '@esbuild/linux-ia32@0.25.12': 451 - resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} 452 engines: {node: '>=18'} 453 cpu: [ia32] 454 os: [linux] ··· 459 cpu: [loong64] 460 os: [linux] 461 462 - '@esbuild/linux-loong64@0.25.12': 463 - resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} 464 engines: {node: '>=18'} 465 cpu: [loong64] 466 os: [linux] ··· 471 cpu: [mips64el] 472 os: [linux] 473 474 - '@esbuild/linux-mips64el@0.25.12': 475 - resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} 476 engines: {node: '>=18'} 477 cpu: [mips64el] 478 os: [linux] ··· 483 cpu: [ppc64] 484 os: [linux] 485 486 - '@esbuild/linux-ppc64@0.25.12': 487 - resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} 488 engines: {node: '>=18'} 489 cpu: [ppc64] 490 os: [linux] ··· 495 cpu: [riscv64] 496 os: [linux] 497 498 - '@esbuild/linux-riscv64@0.25.12': 499 - resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} 500 engines: {node: '>=18'} 501 cpu: [riscv64] 502 os: [linux] ··· 507 cpu: [s390x] 508 os: [linux] 509 510 - '@esbuild/linux-s390x@0.25.12': 511 - resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} 512 engines: {node: '>=18'} 513 cpu: [s390x] 514 os: [linux] ··· 519 cpu: [x64] 520 os: [linux] 521 522 - '@esbuild/linux-x64@0.25.12': 523 - resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} 524 engines: {node: '>=18'} 525 cpu: [x64] 526 os: [linux] 527 528 - '@esbuild/netbsd-arm64@0.25.12': 529 - resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} 530 engines: {node: '>=18'} 531 cpu: [arm64] 532 os: [netbsd] ··· 537 cpu: [x64] 538 os: [netbsd] 539 540 - '@esbuild/netbsd-x64@0.25.12': 541 - resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} 542 engines: {node: '>=18'} 543 cpu: [x64] 544 os: [netbsd] ··· 549 cpu: [arm64] 550 os: [openbsd] 551 552 - '@esbuild/openbsd-arm64@0.25.12': 553 - resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} 554 engines: {node: '>=18'} 555 cpu: [arm64] 556 os: [openbsd] ··· 561 cpu: [x64] 562 os: [openbsd] 563 564 - '@esbuild/openbsd-x64@0.25.12': 565 - resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} 566 engines: {node: '>=18'} 567 cpu: [x64] 568 os: [openbsd] 569 570 - '@esbuild/openharmony-arm64@0.25.12': 571 - resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} 572 engines: {node: '>=18'} 573 cpu: [arm64] 574 os: [openharmony] ··· 579 cpu: [x64] 580 os: [sunos] 581 582 - '@esbuild/sunos-x64@0.25.12': 583 - resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} 584 engines: {node: '>=18'} 585 cpu: [x64] 586 os: [sunos] ··· 591 cpu: [arm64] 592 os: [win32] 593 594 - '@esbuild/win32-arm64@0.25.12': 595 - resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} 596 engines: {node: '>=18'} 597 cpu: [arm64] 598 os: [win32] ··· 603 cpu: [ia32] 604 os: [win32] 605 606 - '@esbuild/win32-ia32@0.25.12': 607 - resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} 608 engines: {node: '>=18'} 609 cpu: [ia32] 610 os: [win32] ··· 615 cpu: [x64] 616 os: [win32] 617 618 - '@esbuild/win32-x64@0.25.12': 619 - resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} 620 engines: {node: '>=18'} 621 cpu: [x64] 622 os: [win32] ··· 637 '@codemirror/view': ^6.0.0 638 '@lezer/highlight': ^1.0.0 639 640 - '@iconify-json/lucide@1.2.77': 641 - resolution: {integrity: sha512-FF3Z+np6Ksb0MaoQymhCHZ4xs5Oo8992Fw7By7bCgVCbBCClYV3wxpF8KzsI1FlxHD4ZXR42NVmXuqdW8YQGgA==} 642 643 '@iconify/tailwind4@1.2.0': 644 resolution: {integrity: sha512-+t7XqfojOB0zzZdd8gV7IQZGq1AaIHTlsxMVzagxYR0hAlJCLUD63o3iSlNKRMH3ZR7gZ8y5c9dJ7J431avRbA==} 645 peerDependencies: 646 tailwindcss: '>= 4.0.0' 647 648 - '@iconify/tools@5.0.0': 649 - resolution: {integrity: sha512-GY/FsuNdWA/FbkLqgQ8b1PHFkNvjMeSFWaVJdLldYGHBp0lZ64HJlcS0qzLfglacHTd8zYdfQjF74RxGqyGMgw==} 650 651 '@iconify/types@2.0.0': 652 resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} ··· 673 '@jsr/mary__exif-rm@0.2.2': 674 resolution: {integrity: sha512-+ZpLaC+1CyqWhH608Sqd6/yTG0pOlokn2tCXha7s1SMQ+GLKo4Nn/PskTeeP9Pt+6gNYSu6ednoSlRvXb2ZGxg==, tarball: https://npm.jsr.io/~/11/@jsr/mary__exif-rm/0.2.2.tgz} 675 676 - '@lezer/common@1.4.0': 677 - resolution: {integrity: sha512-DVeMRoGrgn/k45oQNu189BoW4SZwgZFzJ1+1TV5j2NJ/KFC83oa/enRqZSGshyeMk5cPWMhsKs9nx+8o0unwGg==} 678 679 '@lezer/highlight@1.2.3': 680 resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} ··· 682 '@lezer/json@1.0.3': 683 resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==} 684 685 - '@lezer/lr@1.4.4': 686 - resolution: {integrity: sha512-LHL17Mq0OcFXm1pGQssuGTQFPPdxARjKM8f7GA5+sGtHi0K3R84YaSbmche0+RKWHnCsx9asEe5OWOI4FHfe4A==} 687 688 '@marijn/find-cluster-break@1.0.2': 689 resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} ··· 691 '@noble/secp256k1@3.0.0': 692 resolution: {integrity: sha512-NJBaR352KyIvj3t6sgT/+7xrNyF9Xk9QlLSIqUGVUYlsnDTAUqY8LOmwpcgEx4AMJXRITQ5XEVHD+mMaPfr3mg==} 693 694 - '@rollup/rollup-android-arm-eabi@4.53.3': 695 - resolution: {integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==} 696 cpu: [arm] 697 os: [android] 698 699 - '@rollup/rollup-android-arm64@4.53.3': 700 - resolution: {integrity: sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==} 701 cpu: [arm64] 702 os: [android] 703 704 - '@rollup/rollup-darwin-arm64@4.53.3': 705 - resolution: {integrity: sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==} 706 cpu: [arm64] 707 os: [darwin] 708 709 - '@rollup/rollup-darwin-x64@4.53.3': 710 - resolution: {integrity: sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==} 711 cpu: [x64] 712 os: [darwin] 713 714 - '@rollup/rollup-freebsd-arm64@4.53.3': 715 - resolution: {integrity: sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==} 716 cpu: [arm64] 717 os: [freebsd] 718 719 - '@rollup/rollup-freebsd-x64@4.53.3': 720 - resolution: {integrity: sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==} 721 cpu: [x64] 722 os: [freebsd] 723 724 - '@rollup/rollup-linux-arm-gnueabihf@4.53.3': 725 - resolution: {integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==} 726 cpu: [arm] 727 os: [linux] 728 729 - '@rollup/rollup-linux-arm-musleabihf@4.53.3': 730 - resolution: {integrity: sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==} 731 cpu: [arm] 732 os: [linux] 733 734 - '@rollup/rollup-linux-arm64-gnu@4.53.3': 735 - resolution: {integrity: sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==} 736 cpu: [arm64] 737 os: [linux] 738 739 - '@rollup/rollup-linux-arm64-musl@4.53.3': 740 - resolution: {integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==} 741 cpu: [arm64] 742 os: [linux] 743 744 - '@rollup/rollup-linux-loong64-gnu@4.53.3': 745 - resolution: {integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==} 746 cpu: [loong64] 747 os: [linux] 748 749 - '@rollup/rollup-linux-ppc64-gnu@4.53.3': 750 - resolution: {integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==} 751 cpu: [ppc64] 752 os: [linux] 753 754 - '@rollup/rollup-linux-riscv64-gnu@4.53.3': 755 - resolution: {integrity: sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==} 756 cpu: [riscv64] 757 os: [linux] 758 759 - '@rollup/rollup-linux-riscv64-musl@4.53.3': 760 - resolution: {integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==} 761 cpu: [riscv64] 762 os: [linux] 763 764 - '@rollup/rollup-linux-s390x-gnu@4.53.3': 765 - resolution: {integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==} 766 cpu: [s390x] 767 os: [linux] 768 769 - '@rollup/rollup-linux-x64-gnu@4.53.3': 770 - resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==} 771 cpu: [x64] 772 os: [linux] 773 774 - '@rollup/rollup-linux-x64-musl@4.53.3': 775 - resolution: {integrity: sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==} 776 cpu: [x64] 777 os: [linux] 778 779 - '@rollup/rollup-openharmony-arm64@4.53.3': 780 - resolution: {integrity: sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==} 781 cpu: [arm64] 782 os: [openharmony] 783 784 - '@rollup/rollup-win32-arm64-msvc@4.53.3': 785 - resolution: {integrity: sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==} 786 cpu: [arm64] 787 os: [win32] 788 789 - '@rollup/rollup-win32-ia32-msvc@4.53.3': 790 - resolution: {integrity: sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==} 791 cpu: [ia32] 792 os: [win32] 793 794 - '@rollup/rollup-win32-x64-gnu@4.53.3': 795 - resolution: {integrity: sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==} 796 cpu: [x64] 797 os: [win32] 798 799 - '@rollup/rollup-win32-x64-msvc@4.53.3': 800 - resolution: {integrity: sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==} 801 cpu: [x64] 802 os: [win32] 803 ··· 814 peerDependencies: 815 solid-js: ^1.8.6 816 817 - '@standard-schema/spec@1.0.0': 818 - resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} 819 820 - '@tailwindcss/node@4.1.17': 821 - resolution: {integrity: sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==} 822 823 - '@tailwindcss/oxide-android-arm64@4.1.17': 824 - resolution: {integrity: sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==} 825 engines: {node: '>= 10'} 826 cpu: [arm64] 827 os: [android] 828 829 - '@tailwindcss/oxide-darwin-arm64@4.1.17': 830 - resolution: {integrity: sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==} 831 engines: {node: '>= 10'} 832 cpu: [arm64] 833 os: [darwin] 834 835 - '@tailwindcss/oxide-darwin-x64@4.1.17': 836 - resolution: {integrity: sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==} 837 engines: {node: '>= 10'} 838 cpu: [x64] 839 os: [darwin] 840 841 - '@tailwindcss/oxide-freebsd-x64@4.1.17': 842 - resolution: {integrity: sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==} 843 engines: {node: '>= 10'} 844 cpu: [x64] 845 os: [freebsd] 846 847 - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17': 848 - resolution: {integrity: sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==} 849 engines: {node: '>= 10'} 850 cpu: [arm] 851 os: [linux] 852 853 - '@tailwindcss/oxide-linux-arm64-gnu@4.1.17': 854 - resolution: {integrity: sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==} 855 engines: {node: '>= 10'} 856 cpu: [arm64] 857 os: [linux] 858 859 - '@tailwindcss/oxide-linux-arm64-musl@4.1.17': 860 - resolution: {integrity: sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==} 861 engines: {node: '>= 10'} 862 cpu: [arm64] 863 os: [linux] 864 865 - '@tailwindcss/oxide-linux-x64-gnu@4.1.17': 866 - resolution: {integrity: sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==} 867 engines: {node: '>= 10'} 868 cpu: [x64] 869 os: [linux] 870 871 - '@tailwindcss/oxide-linux-x64-musl@4.1.17': 872 - resolution: {integrity: sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==} 873 engines: {node: '>= 10'} 874 cpu: [x64] 875 os: [linux] 876 877 - '@tailwindcss/oxide-wasm32-wasi@4.1.17': 878 - resolution: {integrity: sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==} 879 engines: {node: '>=14.0.0'} 880 cpu: [wasm32] 881 bundledDependencies: ··· 886 - '@emnapi/wasi-threads' 887 - tslib 888 889 - '@tailwindcss/oxide-win32-arm64-msvc@4.1.17': 890 - resolution: {integrity: sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==} 891 engines: {node: '>= 10'} 892 cpu: [arm64] 893 os: [win32] 894 895 - '@tailwindcss/oxide-win32-x64-msvc@4.1.17': 896 - resolution: {integrity: sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==} 897 engines: {node: '>= 10'} 898 cpu: [x64] 899 os: [win32] 900 901 - '@tailwindcss/oxide@4.1.17': 902 - resolution: {integrity: sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==} 903 engines: {node: '>= 10'} 904 905 - '@tailwindcss/vite@4.1.17': 906 - resolution: {integrity: sha512-4+9w8ZHOiGnpcGI6z1TVVfWaX/koK7fKeSYF3qlYg2xpBtbteP2ddBxiarL+HVgfSJGeK5RIxRQmKm4rTJJAwA==} 907 peerDependencies: 908 vite: ^5.2.0 || ^6 || ^7 909 ··· 922 '@types/estree@1.0.8': 923 resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} 924 925 '@types/node@24.10.1': 926 resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} 927 ··· 944 solid-js: 945 optional: true 946 947 - baseline-browser-mapping@2.9.0: 948 - resolution: {integrity: sha512-Mh++g+2LPfzZToywfE1BUzvZbfOY52Nil0rn9H1CPC5DJ7fX+Vir7nToBeoiSbB1zTNeGYbELEvJESujgGrzXw==} 949 hasBin: true 950 951 boolbase@1.0.0: ··· 956 engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} 957 hasBin: true 958 959 - caniuse-lite@1.0.30001759: 960 - resolution: {integrity: sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==} 961 962 codemirror@6.0.2: 963 resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} ··· 1023 domutils@3.2.2: 1024 resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} 1025 1026 - electron-to-chromium@1.5.263: 1027 - resolution: {integrity: sha512-DrqJ11Knd+lo+dv+lltvfMDLU27g14LMdH2b0O3Pio4uk0x+z7OR+JrmyacTPN2M8w3BrZ7/RTwG3R9B7irPlg==} 1028 1029 - enhanced-resolve@5.18.3: 1030 - resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} 1031 engines: {node: '>=10.13.0'} 1032 1033 entities@4.5.0: ··· 1043 engines: {node: '>=18'} 1044 hasBin: true 1045 1046 - esbuild@0.25.12: 1047 - resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} 1048 engines: {node: '>=18'} 1049 hasBin: true 1050 ··· 1195 mlly@1.8.0: 1196 resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} 1197 1198 - modern-tar@0.7.2: 1199 - resolution: {integrity: sha512-TGG1ZRk1TAQ3neuZwahAHke3rKsSlro+ooMYtjh9sl2gGPVMLMuWiHgwC7im9T5bSM566RSo2Dko56ETgEvZcA==} 1200 engines: {node: '>=18.0.0'} 1201 1202 ms@2.1.3: ··· 1214 nanoid@5.1.6: 1215 resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==} 1216 engines: {node: ^18 || >=20} 1217 hasBin: true 1218 1219 node-releases@2.0.27: ··· 1318 resolve-pkg-maps@1.0.0: 1319 resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} 1320 1321 - rollup@4.53.3: 1322 - resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==} 1323 engines: {node: '>=18.0.0', npm: '>=8.0.0'} 1324 hasBin: true 1325 ··· 1360 engines: {node: '>=16'} 1361 hasBin: true 1362 1363 - tailwindcss@4.1.17: 1364 - resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==} 1365 1366 tapable@2.3.0: 1367 resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} ··· 1388 ufo@1.6.1: 1389 resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} 1390 1391 undici-types@7.16.0: 1392 resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} 1393 1394 - update-browserslist-db@1.2.1: 1395 - resolution: {integrity: sha512-R9NcHbbZ45RoWfTdhn1J9SS7zxNvlddv4YRrHTUaFdtjbmfncfedB45EC9IaqJQ97iAR1GZgOfyRQO+ExIF6EQ==} 1396 hasBin: true 1397 peerDependencies: 1398 browserslist: '>= 4.21.0' ··· 1407 '@testing-library/jest-dom': 1408 optional: true 1409 1410 - vite@7.2.6: 1411 - resolution: {integrity: sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==} 1412 engines: {node: ^20.19.0 || >=22.12.0} 1413 hasBin: true 1414 peerDependencies: ··· 1474 1475 '@atcute/atproto@3.1.9': 1476 dependencies: 1477 - '@atcute/lexicons': 1.2.5 1478 1479 - '@atcute/bluesky@3.2.11': 1480 dependencies: 1481 '@atcute/atproto': 3.1.9 1482 - '@atcute/lexicons': 1.2.5 1483 1484 '@atcute/car@3.1.3': 1485 dependencies: 1486 '@atcute/cbor': 2.2.8 1487 - '@atcute/cid': 2.2.6 1488 - '@atcute/uint8array': 1.0.5 1489 '@atcute/varint': 1.0.3 1490 yocto-queue: 1.2.2 1491 1492 '@atcute/car@5.0.0': 1493 dependencies: 1494 '@atcute/cbor': 2.2.8 1495 - '@atcute/cid': 2.2.6 1496 - '@atcute/uint8array': 1.0.5 1497 '@atcute/varint': 1.0.3 1498 1499 '@atcute/cbor@2.2.8': 1500 dependencies: 1501 - '@atcute/cid': 2.2.6 1502 '@atcute/multibase': 1.1.6 1503 - '@atcute/uint8array': 1.0.5 1504 1505 - '@atcute/cid@2.2.6': 1506 dependencies: 1507 '@atcute/multibase': 1.1.6 1508 - '@atcute/uint8array': 1.0.5 1509 1510 - '@atcute/client@4.1.0': 1511 dependencies: 1512 '@atcute/identity': 1.1.3 1513 - '@atcute/lexicons': 1.2.5 1514 1515 - '@atcute/crypto@2.2.6': 1516 dependencies: 1517 '@atcute/multibase': 1.1.6 1518 - '@atcute/uint8array': 1.0.5 1519 '@noble/secp256k1': 3.0.0 1520 1521 - '@atcute/did-plc@0.2.0': 1522 dependencies: 1523 '@atcute/cbor': 2.2.8 1524 - '@atcute/cid': 2.2.6 1525 - '@atcute/crypto': 2.2.6 1526 '@atcute/identity': 1.1.3 1527 - '@atcute/lexicons': 1.2.5 1528 '@atcute/multibase': 1.1.6 1529 - '@atcute/uint8array': 1.0.5 1530 '@badrap/valita': 0.4.6 1531 1532 - '@atcute/identity-resolver@1.1.4(@atcute/identity@1.1.3)': 1533 dependencies: 1534 '@atcute/identity': 1.1.3 1535 - '@atcute/lexicons': 1.2.5 1536 '@atcute/util-fetch': 1.0.4 1537 '@badrap/valita': 0.4.6 1538 1539 '@atcute/identity@1.1.3': 1540 dependencies: 1541 - '@atcute/lexicons': 1.2.5 1542 '@badrap/valita': 0.4.6 1543 1544 - '@atcute/leaflet@1.0.12': 1545 dependencies: 1546 '@atcute/atproto': 3.1.9 1547 - '@atcute/lexicons': 1.2.5 1548 1549 - '@atcute/lexicon-doc@2.0.4': 1550 dependencies: 1551 '@atcute/identity': 1.1.3 1552 - '@atcute/lexicons': 1.2.5 1553 '@badrap/valita': 0.4.6 1554 1555 - '@atcute/lexicon-resolver@0.1.5(@atcute/identity-resolver@1.1.4(@atcute/identity@1.1.3))(@atcute/identity@1.1.3)': 1556 dependencies: 1557 - '@atcute/crypto': 2.2.6 1558 '@atcute/identity': 1.1.3 1559 - '@atcute/identity-resolver': 1.1.4(@atcute/identity@1.1.3) 1560 - '@atcute/lexicon-doc': 2.0.4 1561 - '@atcute/lexicons': 1.2.5 1562 - '@atcute/repo': 0.1.0 1563 '@atcute/util-fetch': 1.0.4 1564 '@badrap/valita': 0.4.6 1565 1566 - '@atcute/lexicons@1.2.5': 1567 dependencies: 1568 - '@standard-schema/spec': 1.0.0 1569 esm-env: 1.2.2 1570 1571 - '@atcute/mst@0.1.0': 1572 dependencies: 1573 '@atcute/cbor': 2.2.8 1574 - '@atcute/cid': 2.2.6 1575 - '@atcute/uint8array': 1.0.5 1576 1577 '@atcute/multibase@1.1.6': 1578 dependencies: 1579 - '@atcute/uint8array': 1.0.5 1580 1581 - '@atcute/oauth-browser-client@2.0.1': 1582 dependencies: 1583 - '@atcute/client': 4.1.0 1584 - '@atcute/identity': 1.1.3 1585 - '@atcute/identity-resolver': 1.1.4(@atcute/identity@1.1.3) 1586 - '@atcute/lexicons': 1.2.5 1587 '@atcute/multibase': 1.1.6 1588 - '@atcute/uint8array': 1.0.5 1589 nanoid: 5.1.6 1590 1591 - '@atcute/repo@0.1.0': 1592 dependencies: 1593 '@atcute/car': 5.0.0 1594 '@atcute/cbor': 2.2.8 1595 - '@atcute/cid': 2.2.6 1596 - '@atcute/crypto': 2.2.6 1597 - '@atcute/lexicons': 1.2.5 1598 - '@atcute/mst': 0.1.0 1599 - '@atcute/uint8array': 1.0.5 1600 1601 - '@atcute/tangled@1.0.12': 1602 dependencies: 1603 '@atcute/atproto': 3.1.9 1604 - '@atcute/lexicons': 1.2.5 1605 1606 - '@atcute/tid@1.0.3': {} 1607 1608 - '@atcute/uint8array@1.0.5': {} 1609 1610 '@atcute/util-fetch@1.0.4': 1611 dependencies: 1612 '@badrap/valita': 0.4.6 1613 1614 '@atcute/varint@1.0.3': {} 1615 ··· 1728 1729 '@codemirror/autocomplete@6.20.0': 1730 dependencies: 1731 - '@codemirror/language': 6.11.3 1732 - '@codemirror/state': 6.5.2 1733 - '@codemirror/view': 6.38.8 1734 - '@lezer/common': 1.4.0 1735 1736 - '@codemirror/commands@6.10.0': 1737 dependencies: 1738 - '@codemirror/language': 6.11.3 1739 - '@codemirror/state': 6.5.2 1740 - '@codemirror/view': 6.38.8 1741 - '@lezer/common': 1.4.0 1742 1743 '@codemirror/lang-json@6.0.2': 1744 dependencies: 1745 - '@codemirror/language': 6.11.3 1746 '@lezer/json': 1.0.3 1747 1748 - '@codemirror/language@6.11.3': 1749 dependencies: 1750 - '@codemirror/state': 6.5.2 1751 - '@codemirror/view': 6.38.8 1752 - '@lezer/common': 1.4.0 1753 '@lezer/highlight': 1.2.3 1754 - '@lezer/lr': 1.4.4 1755 style-mod: 4.1.3 1756 1757 '@codemirror/lint@6.9.2': 1758 dependencies: 1759 - '@codemirror/state': 6.5.2 1760 - '@codemirror/view': 6.38.8 1761 crelt: 1.0.6 1762 1763 '@codemirror/search@6.5.11': 1764 dependencies: 1765 - '@codemirror/state': 6.5.2 1766 - '@codemirror/view': 6.38.8 1767 crelt: 1.0.6 1768 1769 - '@codemirror/state@6.5.2': 1770 dependencies: 1771 '@marijn/find-cluster-break': 1.0.2 1772 1773 - '@codemirror/view@6.38.8': 1774 dependencies: 1775 - '@codemirror/state': 6.5.2 1776 crelt: 1.0.6 1777 style-mod: 4.1.3 1778 w3c-keyname: 2.2.8 ··· 1784 '@esbuild/aix-ppc64@0.23.1': 1785 optional: true 1786 1787 - '@esbuild/aix-ppc64@0.25.12': 1788 optional: true 1789 1790 '@esbuild/android-arm64@0.23.1': 1791 optional: true 1792 1793 - '@esbuild/android-arm64@0.25.12': 1794 optional: true 1795 1796 '@esbuild/android-arm@0.23.1': 1797 optional: true 1798 1799 - '@esbuild/android-arm@0.25.12': 1800 optional: true 1801 1802 '@esbuild/android-x64@0.23.1': 1803 optional: true 1804 1805 - '@esbuild/android-x64@0.25.12': 1806 optional: true 1807 1808 '@esbuild/darwin-arm64@0.23.1': 1809 optional: true 1810 1811 - '@esbuild/darwin-arm64@0.25.12': 1812 optional: true 1813 1814 '@esbuild/darwin-x64@0.23.1': 1815 optional: true 1816 1817 - '@esbuild/darwin-x64@0.25.12': 1818 optional: true 1819 1820 '@esbuild/freebsd-arm64@0.23.1': 1821 optional: true 1822 1823 - '@esbuild/freebsd-arm64@0.25.12': 1824 optional: true 1825 1826 '@esbuild/freebsd-x64@0.23.1': 1827 optional: true 1828 1829 - '@esbuild/freebsd-x64@0.25.12': 1830 optional: true 1831 1832 '@esbuild/linux-arm64@0.23.1': 1833 optional: true 1834 1835 - '@esbuild/linux-arm64@0.25.12': 1836 optional: true 1837 1838 '@esbuild/linux-arm@0.23.1': 1839 optional: true 1840 1841 - '@esbuild/linux-arm@0.25.12': 1842 optional: true 1843 1844 '@esbuild/linux-ia32@0.23.1': 1845 optional: true 1846 1847 - '@esbuild/linux-ia32@0.25.12': 1848 optional: true 1849 1850 '@esbuild/linux-loong64@0.23.1': 1851 optional: true 1852 1853 - '@esbuild/linux-loong64@0.25.12': 1854 optional: true 1855 1856 '@esbuild/linux-mips64el@0.23.1': 1857 optional: true 1858 1859 - '@esbuild/linux-mips64el@0.25.12': 1860 optional: true 1861 1862 '@esbuild/linux-ppc64@0.23.1': 1863 optional: true 1864 1865 - '@esbuild/linux-ppc64@0.25.12': 1866 optional: true 1867 1868 '@esbuild/linux-riscv64@0.23.1': 1869 optional: true 1870 1871 - '@esbuild/linux-riscv64@0.25.12': 1872 optional: true 1873 1874 '@esbuild/linux-s390x@0.23.1': 1875 optional: true 1876 1877 - '@esbuild/linux-s390x@0.25.12': 1878 optional: true 1879 1880 '@esbuild/linux-x64@0.23.1': 1881 optional: true 1882 1883 - '@esbuild/linux-x64@0.25.12': 1884 optional: true 1885 1886 - '@esbuild/netbsd-arm64@0.25.12': 1887 optional: true 1888 1889 '@esbuild/netbsd-x64@0.23.1': 1890 optional: true 1891 1892 - '@esbuild/netbsd-x64@0.25.12': 1893 optional: true 1894 1895 '@esbuild/openbsd-arm64@0.23.1': 1896 optional: true 1897 1898 - '@esbuild/openbsd-arm64@0.25.12': 1899 optional: true 1900 1901 '@esbuild/openbsd-x64@0.23.1': 1902 optional: true 1903 1904 - '@esbuild/openbsd-x64@0.25.12': 1905 optional: true 1906 1907 - '@esbuild/openharmony-arm64@0.25.12': 1908 optional: true 1909 1910 '@esbuild/sunos-x64@0.23.1': 1911 optional: true 1912 1913 - '@esbuild/sunos-x64@0.25.12': 1914 optional: true 1915 1916 '@esbuild/win32-arm64@0.23.1': 1917 optional: true 1918 1919 - '@esbuild/win32-arm64@0.25.12': 1920 optional: true 1921 1922 '@esbuild/win32-ia32@0.23.1': 1923 optional: true 1924 1925 - '@esbuild/win32-ia32@0.25.12': 1926 optional: true 1927 1928 '@esbuild/win32-x64@0.23.1': 1929 optional: true 1930 1931 - '@esbuild/win32-x64@0.25.12': 1932 optional: true 1933 1934 - '@fsegurai/codemirror-theme-basic-dark@6.2.3(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.8)(@lezer/highlight@1.2.3)': 1935 dependencies: 1936 - '@codemirror/language': 6.11.3 1937 - '@codemirror/state': 6.5.2 1938 - '@codemirror/view': 6.38.8 1939 '@lezer/highlight': 1.2.3 1940 1941 - '@fsegurai/codemirror-theme-basic-light@6.2.3(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.8)(@lezer/highlight@1.2.3)': 1942 dependencies: 1943 - '@codemirror/language': 6.11.3 1944 - '@codemirror/state': 6.5.2 1945 - '@codemirror/view': 6.38.8 1946 '@lezer/highlight': 1.2.3 1947 1948 - '@iconify-json/lucide@1.2.77': 1949 dependencies: 1950 '@iconify/types': 2.0.0 1951 1952 - '@iconify/tailwind4@1.2.0(tailwindcss@4.1.17)': 1953 dependencies: 1954 - '@iconify/tools': 5.0.0 1955 '@iconify/types': 2.0.0 1956 '@iconify/utils': 3.1.0 1957 - tailwindcss: 4.1.17 1958 1959 - '@iconify/tools@5.0.0': 1960 dependencies: 1961 '@cyberalien/svg-utils': 1.0.11 1962 '@iconify/types': 2.0.0 1963 '@iconify/utils': 3.1.0 1964 fflate: 0.8.2 1965 - modern-tar: 0.7.2 1966 pathe: 2.0.3 1967 svgo: 4.0.0 1968 ··· 1995 1996 '@jsr/mary__exif-rm@0.2.2': {} 1997 1998 - '@lezer/common@1.4.0': {} 1999 2000 '@lezer/highlight@1.2.3': 2001 dependencies: 2002 - '@lezer/common': 1.4.0 2003 2004 '@lezer/json@1.0.3': 2005 dependencies: 2006 - '@lezer/common': 1.4.0 2007 '@lezer/highlight': 1.2.3 2008 - '@lezer/lr': 1.4.4 2009 2010 - '@lezer/lr@1.4.4': 2011 dependencies: 2012 - '@lezer/common': 1.4.0 2013 2014 '@marijn/find-cluster-break@1.0.2': {} 2015 2016 '@noble/secp256k1@3.0.0': {} 2017 2018 - '@rollup/rollup-android-arm-eabi@4.53.3': 2019 optional: true 2020 2021 - '@rollup/rollup-android-arm64@4.53.3': 2022 optional: true 2023 2024 - '@rollup/rollup-darwin-arm64@4.53.3': 2025 optional: true 2026 2027 - '@rollup/rollup-darwin-x64@4.53.3': 2028 optional: true 2029 2030 - '@rollup/rollup-freebsd-arm64@4.53.3': 2031 optional: true 2032 2033 - '@rollup/rollup-freebsd-x64@4.53.3': 2034 optional: true 2035 2036 - '@rollup/rollup-linux-arm-gnueabihf@4.53.3': 2037 optional: true 2038 2039 - '@rollup/rollup-linux-arm-musleabihf@4.53.3': 2040 optional: true 2041 2042 - '@rollup/rollup-linux-arm64-gnu@4.53.3': 2043 optional: true 2044 2045 - '@rollup/rollup-linux-arm64-musl@4.53.3': 2046 optional: true 2047 2048 - '@rollup/rollup-linux-loong64-gnu@4.53.3': 2049 optional: true 2050 2051 - '@rollup/rollup-linux-ppc64-gnu@4.53.3': 2052 optional: true 2053 2054 - '@rollup/rollup-linux-riscv64-gnu@4.53.3': 2055 optional: true 2056 2057 - '@rollup/rollup-linux-riscv64-musl@4.53.3': 2058 optional: true 2059 2060 - '@rollup/rollup-linux-s390x-gnu@4.53.3': 2061 optional: true 2062 2063 - '@rollup/rollup-linux-x64-gnu@4.53.3': 2064 optional: true 2065 2066 - '@rollup/rollup-linux-x64-musl@4.53.3': 2067 optional: true 2068 2069 - '@rollup/rollup-openharmony-arm64@4.53.3': 2070 optional: true 2071 2072 - '@rollup/rollup-win32-arm64-msvc@4.53.3': 2073 optional: true 2074 2075 - '@rollup/rollup-win32-ia32-msvc@4.53.3': 2076 optional: true 2077 2078 - '@rollup/rollup-win32-x64-gnu@4.53.3': 2079 optional: true 2080 2081 - '@rollup/rollup-win32-x64-msvc@4.53.3': 2082 optional: true 2083 2084 '@skyware/firehose@0.5.2': ··· 2095 dependencies: 2096 solid-js: 1.9.10 2097 2098 - '@standard-schema/spec@1.0.0': {} 2099 2100 - '@tailwindcss/node@4.1.17': 2101 dependencies: 2102 '@jridgewell/remapping': 2.3.5 2103 - enhanced-resolve: 5.18.3 2104 jiti: 2.6.1 2105 lightningcss: 1.30.2 2106 magic-string: 0.30.21 2107 source-map-js: 1.2.1 2108 - tailwindcss: 4.1.17 2109 2110 - '@tailwindcss/oxide-android-arm64@4.1.17': 2111 optional: true 2112 2113 - '@tailwindcss/oxide-darwin-arm64@4.1.17': 2114 optional: true 2115 2116 - '@tailwindcss/oxide-darwin-x64@4.1.17': 2117 optional: true 2118 2119 - '@tailwindcss/oxide-freebsd-x64@4.1.17': 2120 optional: true 2121 2122 - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17': 2123 optional: true 2124 2125 - '@tailwindcss/oxide-linux-arm64-gnu@4.1.17': 2126 optional: true 2127 2128 - '@tailwindcss/oxide-linux-arm64-musl@4.1.17': 2129 optional: true 2130 2131 - '@tailwindcss/oxide-linux-x64-gnu@4.1.17': 2132 optional: true 2133 2134 - '@tailwindcss/oxide-linux-x64-musl@4.1.17': 2135 optional: true 2136 2137 - '@tailwindcss/oxide-wasm32-wasi@4.1.17': 2138 optional: true 2139 2140 - '@tailwindcss/oxide-win32-arm64-msvc@4.1.17': 2141 optional: true 2142 2143 - '@tailwindcss/oxide-win32-x64-msvc@4.1.17': 2144 optional: true 2145 2146 - '@tailwindcss/oxide@4.1.17': 2147 optionalDependencies: 2148 - '@tailwindcss/oxide-android-arm64': 4.1.17 2149 - '@tailwindcss/oxide-darwin-arm64': 4.1.17 2150 - '@tailwindcss/oxide-darwin-x64': 4.1.17 2151 - '@tailwindcss/oxide-freebsd-x64': 4.1.17 2152 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.17 2153 - '@tailwindcss/oxide-linux-arm64-gnu': 4.1.17 2154 - '@tailwindcss/oxide-linux-arm64-musl': 4.1.17 2155 - '@tailwindcss/oxide-linux-x64-gnu': 4.1.17 2156 - '@tailwindcss/oxide-linux-x64-musl': 4.1.17 2157 - '@tailwindcss/oxide-wasm32-wasi': 4.1.17 2158 - '@tailwindcss/oxide-win32-arm64-msvc': 4.1.17 2159 - '@tailwindcss/oxide-win32-x64-msvc': 4.1.17 2160 2161 - '@tailwindcss/vite@4.1.17(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2))': 2162 dependencies: 2163 - '@tailwindcss/node': 4.1.17 2164 - '@tailwindcss/oxide': 4.1.17 2165 - tailwindcss: 4.1.17 2166 - vite: 7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2) 2167 2168 '@types/babel__core@7.20.5': 2169 dependencies: ··· 2187 '@babel/types': 7.28.5 2188 2189 '@types/estree@1.0.8': {} 2190 2191 '@types/node@24.10.1': 2192 dependencies: ··· 2211 optionalDependencies: 2212 solid-js: 1.9.10 2213 2214 - baseline-browser-mapping@2.9.0: {} 2215 2216 boolbase@1.0.0: {} 2217 2218 browserslist@4.28.1: 2219 dependencies: 2220 - baseline-browser-mapping: 2.9.0 2221 - caniuse-lite: 1.0.30001759 2222 - electron-to-chromium: 1.5.263 2223 node-releases: 2.0.27 2224 - update-browserslist-db: 1.2.1(browserslist@4.28.1) 2225 2226 - caniuse-lite@1.0.30001759: {} 2227 2228 codemirror@6.0.2: 2229 dependencies: 2230 '@codemirror/autocomplete': 6.20.0 2231 - '@codemirror/commands': 6.10.0 2232 - '@codemirror/language': 6.11.3 2233 '@codemirror/lint': 6.9.2 2234 '@codemirror/search': 6.5.11 2235 - '@codemirror/state': 6.5.2 2236 - '@codemirror/view': 6.38.8 2237 2238 commander@11.1.0: {} 2239 ··· 2293 domelementtype: 2.3.0 2294 domhandler: 5.0.3 2295 2296 - electron-to-chromium@1.5.263: {} 2297 2298 - enhanced-resolve@5.18.3: 2299 dependencies: 2300 graceful-fs: 4.2.11 2301 tapable: 2.3.0 ··· 2332 '@esbuild/win32-x64': 0.23.1 2333 optional: true 2334 2335 - esbuild@0.25.12: 2336 optionalDependencies: 2337 - '@esbuild/aix-ppc64': 0.25.12 2338 - '@esbuild/android-arm': 0.25.12 2339 - '@esbuild/android-arm64': 0.25.12 2340 - '@esbuild/android-x64': 0.25.12 2341 - '@esbuild/darwin-arm64': 0.25.12 2342 - '@esbuild/darwin-x64': 0.25.12 2343 - '@esbuild/freebsd-arm64': 0.25.12 2344 - '@esbuild/freebsd-x64': 0.25.12 2345 - '@esbuild/linux-arm': 0.25.12 2346 - '@esbuild/linux-arm64': 0.25.12 2347 - '@esbuild/linux-ia32': 0.25.12 2348 - '@esbuild/linux-loong64': 0.25.12 2349 - '@esbuild/linux-mips64el': 0.25.12 2350 - '@esbuild/linux-ppc64': 0.25.12 2351 - '@esbuild/linux-riscv64': 0.25.12 2352 - '@esbuild/linux-s390x': 0.25.12 2353 - '@esbuild/linux-x64': 0.25.12 2354 - '@esbuild/netbsd-arm64': 0.25.12 2355 - '@esbuild/netbsd-x64': 0.25.12 2356 - '@esbuild/openbsd-arm64': 0.25.12 2357 - '@esbuild/openbsd-x64': 0.25.12 2358 - '@esbuild/openharmony-arm64': 0.25.12 2359 - '@esbuild/sunos-x64': 0.25.12 2360 - '@esbuild/win32-arm64': 0.25.12 2361 - '@esbuild/win32-ia32': 0.25.12 2362 - '@esbuild/win32-x64': 0.25.12 2363 2364 escalade@3.2.0: {} 2365 ··· 2467 pkg-types: 1.3.1 2468 ufo: 1.6.1 2469 2470 - modern-tar@0.7.2: {} 2471 2472 ms@2.1.3: {} 2473 ··· 2476 nanoid@3.3.11: {} 2477 2478 nanoid@5.1.6: {} 2479 2480 node-releases@2.0.27: {} 2481 ··· 2523 resolve-pkg-maps@1.0.0: 2524 optional: true 2525 2526 - rollup@4.53.3: 2527 dependencies: 2528 '@types/estree': 1.0.8 2529 optionalDependencies: 2530 - '@rollup/rollup-android-arm-eabi': 4.53.3 2531 - '@rollup/rollup-android-arm64': 4.53.3 2532 - '@rollup/rollup-darwin-arm64': 4.53.3 2533 - '@rollup/rollup-darwin-x64': 4.53.3 2534 - '@rollup/rollup-freebsd-arm64': 4.53.3 2535 - '@rollup/rollup-freebsd-x64': 4.53.3 2536 - '@rollup/rollup-linux-arm-gnueabihf': 4.53.3 2537 - '@rollup/rollup-linux-arm-musleabihf': 4.53.3 2538 - '@rollup/rollup-linux-arm64-gnu': 4.53.3 2539 - '@rollup/rollup-linux-arm64-musl': 4.53.3 2540 - '@rollup/rollup-linux-loong64-gnu': 4.53.3 2541 - '@rollup/rollup-linux-ppc64-gnu': 4.53.3 2542 - '@rollup/rollup-linux-riscv64-gnu': 4.53.3 2543 - '@rollup/rollup-linux-riscv64-musl': 4.53.3 2544 - '@rollup/rollup-linux-s390x-gnu': 4.53.3 2545 - '@rollup/rollup-linux-x64-gnu': 4.53.3 2546 - '@rollup/rollup-linux-x64-musl': 4.53.3 2547 - '@rollup/rollup-openharmony-arm64': 4.53.3 2548 - '@rollup/rollup-win32-arm64-msvc': 4.53.3 2549 - '@rollup/rollup-win32-ia32-msvc': 4.53.3 2550 - '@rollup/rollup-win32-x64-gnu': 4.53.3 2551 - '@rollup/rollup-win32-x64-msvc': 4.53.3 2552 fsevents: 2.3.3 2553 2554 sax@1.4.3: {} ··· 2590 picocolors: 1.1.1 2591 sax: 1.4.3 2592 2593 - tailwindcss@4.1.17: {} 2594 2595 tapable@2.3.0: {} 2596 ··· 2613 2614 ufo@1.6.1: {} 2615 2616 undici-types@7.16.0: 2617 optional: true 2618 2619 - update-browserslist-db@1.2.1(browserslist@4.28.1): 2620 dependencies: 2621 browserslist: 4.28.1 2622 escalade: 3.2.0 2623 picocolors: 1.1.1 2624 2625 - vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)): 2626 dependencies: 2627 '@babel/core': 7.28.5 2628 '@types/babel__core': 7.20.5 ··· 2630 merge-anything: 5.1.7 2631 solid-js: 1.9.10 2632 solid-refresh: 0.6.3(solid-js@1.9.10) 2633 - vite: 7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2) 2634 - vitefu: 1.1.1(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)) 2635 transitivePeerDependencies: 2636 - supports-color 2637 2638 - vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2): 2639 dependencies: 2640 - esbuild: 0.25.12 2641 fdir: 6.5.0(picomatch@4.0.3) 2642 picomatch: 4.0.3 2643 postcss: 8.5.6 2644 - rollup: 4.53.3 2645 tinyglobby: 0.2.15 2646 optionalDependencies: 2647 '@types/node': 24.10.1 ··· 2650 lightningcss: 1.30.2 2651 tsx: 4.19.2 2652 2653 - vitefu@1.1.1(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)): 2654 optionalDependencies: 2655 - vite: 7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2) 2656 2657 w3c-keyname@2.2.8: {} 2658
··· 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 ··· 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] ··· 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: ··· 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: ··· 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 ··· 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: ··· 2223 '@babel/types': 7.28.5 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: ··· 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 ··· 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

This is a binary file and will not be displayed.

public/fonts/Figtree[wght].woff2

This is a binary file and will not be displayed.

+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
···
··· 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
···
··· 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
···
··· 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
···
··· 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
···
··· 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
···
··· 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
···
··· 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
···
··· 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>();
-170
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 { ActionMenu, DropdownMenu, MenuProvider, NavMenu } from "./dropdown.jsx"; 14 - import { agent, Login, retrieveSession, Sessions, setAgent } from "./login.jsx"; 15 - import { Modal } from "./modal.jsx"; 16 - 17 - export const [sessions, setSessions] = createStore<Sessions>(); 18 - 19 - const AccountDropdown = (props: { did: Did }) => { 20 - const removeSession = async (did: Did) => { 21 - const currentSession = agent()?.sub; 22 - try { 23 - const session = await getSession(did, { allowStale: true }); 24 - const agent = new OAuthUserAgent(session); 25 - await agent.signOut(); 26 - } catch { 27 - deleteStoredSession(did); 28 - } 29 - setSessions( 30 - produce((accs) => { 31 - delete accs[did]; 32 - }), 33 - ); 34 - localStorage.setItem("sessions", JSON.stringify(sessions)); 35 - if (currentSession === did) setAgent(undefined); 36 - }; 37 - 38 - return ( 39 - <MenuProvider> 40 - <DropdownMenu icon="lucide--ellipsis" buttonClass="rounded-md p-2"> 41 - <NavMenu href={`/at://${props.did}`} label="Go to repo" icon="lucide--user-round" /> 42 - <ActionMenu 43 - icon="lucide--x" 44 - label="Remove account" 45 - onClick={() => removeSession(props.did)} 46 - /> 47 - </DropdownMenu> 48 - </MenuProvider> 49 - ); 50 - }; 51 - 52 - export const AccountManager = () => { 53 - const [openManager, setOpenManager] = createSignal(false); 54 - const [avatars, setAvatars] = createStore<Record<Did, string>>(); 55 - 56 - onMount(async () => { 57 - try { 58 - await retrieveSession(); 59 - } catch {} 60 - 61 - const localSessions = localStorage.getItem("sessions"); 62 - if (localSessions) { 63 - const storedSessions: Sessions = JSON.parse(localSessions); 64 - const sessionDids = Object.keys(storedSessions) as Did[]; 65 - sessionDids.forEach(async (did) => { 66 - const doc = await resolveDidDoc(did); 67 - const alias = doc.alsoKnownAs?.find((alias) => alias.startsWith("at://")); 68 - if (alias) { 69 - setSessions(did, { 70 - signedIn: storedSessions[did].signedIn, 71 - handle: alias.replace("at://", ""), 72 - }); 73 - } 74 - }); 75 - sessionDids.forEach(async (did) => { 76 - const avatar = await getAvatar(did); 77 - if (avatar) setAvatars(did, avatar); 78 - }); 79 - } 80 - }); 81 - 82 - const resumeSession = async (did: Did) => { 83 - try { 84 - localStorage.setItem("lastSignedIn", did); 85 - await retrieveSession(); 86 - } catch { 87 - const authUrl = await createAuthorizationUrl({ 88 - scope: import.meta.env.VITE_OAUTH_SCOPE, 89 - target: { type: "account", identifier: did }, 90 - }); 91 - 92 - await new Promise((resolve) => setTimeout(resolve, 250)); 93 - 94 - location.assign(authUrl); 95 - } 96 - }; 97 - 98 - const getAvatar = async (did: Did) => { 99 - const rpc = new Client({ 100 - handler: new CredentialManager({ service: "https://public.api.bsky.app" }), 101 - }); 102 - const res = await rpc.get("app.bsky.actor.getProfile", { params: { actor: did } }); 103 - if (res.ok) { 104 - return res.data.avatar; 105 - } 106 - return undefined; 107 - }; 108 - 109 - return ( 110 - <> 111 - <Modal open={openManager()} onClose={() => setOpenManager(false)}> 112 - <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"> 113 - <div class="mb-2 px-1 font-semibold"> 114 - <span>Manage accounts</span> 115 - </div> 116 - <div class="mb-3 max-h-80 overflow-y-auto md:max-h-100"> 117 - <For each={Object.keys(sessions)}> 118 - {(did) => ( 119 - <div class="flex w-full items-center justify-between"> 120 - <A 121 - href={`/at://${did}`} 122 - onClick={() => setOpenManager(false)} 123 - 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" 124 - > 125 - <Show 126 - when={avatars[did as Did]} 127 - fallback={<span class="iconify lucide--user-round m-0.5 size-5"></span>} 128 - > 129 - <img 130 - src={avatars[did as Did].replace("img/avatar/", "img/avatar_thumbnail/")} 131 - class="size-6 rounded-full" 132 - /> 133 - </Show> 134 - </A> 135 - <button 136 - 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" 137 - onclick={() => resumeSession(did as Did)} 138 - > 139 - <span class="truncate"> 140 - {sessions[did]?.handle ? sessions[did].handle : did} 141 - </span> 142 - <Show when={did === agent()?.sub && sessions[did].signedIn}> 143 - <span class="iconify lucide--check shrink-0 text-green-500 dark:text-green-400"></span> 144 - </Show> 145 - <Show when={!sessions[did].signedIn}> 146 - <span class="iconify lucide--circle-alert shrink-0 text-red-500 dark:text-red-400"></span> 147 - </Show> 148 - </button> 149 - <AccountDropdown did={did as Did} /> 150 - </div> 151 - )} 152 - </For> 153 - </div> 154 - <Login /> 155 - </div> 156 - </Modal> 157 - <button 158 - onclick={() => setOpenManager(true)} 159 - 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`} 160 - > 161 - {agent() && avatars[agent()!.sub] ? 162 - <img 163 - src={avatars[agent()!.sub].replace("img/avatar/", "img/avatar_thumbnail/")} 164 - class="size-5 rounded-full" 165 - /> 166 - : <span class="iconify lucide--circle-user-round text-lg"></span>} 167 - </button> 168 - </> 169 - ); 170 - };
···
+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&hellip;</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
···
··· 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
···
··· 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
···
··· 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
···
··· 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 + };
+12
src/components/create/menu-item.tsx
···
··· 1 + export const MenuItem = (props: { icon: string; label: string; onClick: () => void }) => { 2 + return ( 3 + <button 4 + type="button" 5 + 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" 6 + onClick={props.onClick} 7 + > 8 + <span class={`iconify ${props.icon}`}></span> 9 + <span>{props.label}</span> 10 + </button> 11 + ); 12 + };
+4
src/components/create/state.ts
···
··· 1 + import { createSignal } from "solid-js"; 2 + 3 + export const editorInstance = { view: null as any }; 4 + export const [placeholder, setPlaceholder] = createSignal<any>();
-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 - };
···
+7 -2
src/components/dropdown.tsx
··· 75 export const ActionMenu = (props: { 76 label: string; 77 icon: string; 78 - onClick: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>; 79 }) => { 80 return ( 81 <button 82 - onClick={props.onClick} 83 class="flex items-center gap-2 rounded-md p-1.5 whitespace-nowrap hover:bg-neutral-200/50 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 84 > 85 <Show when={props.icon}>
··· 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}>
+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
··· 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
··· 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
··· 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 };
···
+28 -28
src/components/navbar.tsx
··· 18 e.stopPropagation(); 19 addToClipboard(props.content); 20 }} 21 - class={`-mr-2 hidden items-center rounded px-2 py-1.5 text-neutral-500 transition-all duration-200 group-hover:flex hover:bg-neutral-200/70 hover:text-neutral-600 active:bg-neutral-300/70 dark:text-neutral-400 dark:hover:bg-neutral-700/70 dark:hover:text-neutral-300 dark:active:bg-neutral-600/70`} 22 aria-label="Copy to clipboard" 23 > 24 <span class="iconify lucide--link"></span> ··· 30 31 export const NavBar = (props: { params: Params }) => { 32 const [handle, setHandle] = createSignal(props.params.repo); 33 - const [showHandle, setShowHandle] = createSignal(localStorage.showHandle === "true"); 34 35 createEffect(() => { 36 if (pds() !== undefined && props.params.repo) { ··· 88 <Show when={props.params.repo}> 89 {/* Repository Level */} 90 <div class="group relative flex items-center justify-between gap-1 rounded-md border-[0.5px] border-transparent bg-transparent px-2 transition-all duration-200 hover:border-neutral-300 hover:bg-neutral-50/40 dark:hover:border-neutral-600 dark:hover:bg-neutral-800/40"> 91 - <div class="flex basis-full items-center gap-2"> 92 <Tooltip text="Repository"> 93 <span class="iconify lucide--book-user shrink-0 text-neutral-500 transition-colors duration-200 group-hover:text-neutral-700 dark:text-neutral-400 dark:group-hover:text-neutral-200"></span> 94 </Tooltip> 95 - {props.params.collection ? 96 <A 97 end 98 href={`/at://${props.params.repo}`} 99 - inactiveClass="text-blue-400 w-full py-0.5 font-medium hover:text-blue-500 transition-colors duration-150 dark:hover:text-blue-300" 100 > 101 - {showHandle() ? handle() : props.params.repo} 102 </A> 103 - : <span class="py-0.5 font-medium"> 104 - {showHandle() ? handle() : props.params.repo} 105 - </span> 106 - } 107 - </div> 108 - <div class="flex"> 109 - <Tooltip text={showHandle() ? "Show DID" : "Show handle"}> 110 - <button 111 - type="button" 112 - class={`items-center rounded px-1.25 py-1.25 text-neutral-500 transition-all duration-200 hover:bg-neutral-200/70 hover:text-neutral-700 active:bg-neutral-300/70 sm:px-2 sm:py-1.5 dark:text-neutral-400 dark:hover:bg-neutral-700/70 dark:hover:text-neutral-200 dark:active:bg-neutral-600/70 ${isTouchDevice ? "flex" : "hidden group-hover:flex"}`} 113 - onclick={() => { 114 - localStorage.showHandle = !showHandle(); 115 - setShowHandle(!showHandle()); 116 - }} 117 - aria-label="Switch DID/Handle" 118 - > 119 - <span 120 - class={`iconify shrink-0 duration-200 ${showHandle() ? "rotate-y-180" : ""} lucide--arrow-left-right`} 121 - ></span> 122 - </button> 123 - </Tooltip> 124 - <CopyButton content={props.params.repo!} label="Copy DID" /> 125 </div> 126 </div> 127 </Show> 128
··· 18 e.stopPropagation(); 19 addToClipboard(props.content); 20 }} 21 + class={`-mr-2 hidden items-center rounded px-2 py-1 text-neutral-500 transition-all duration-200 group-hover:flex hover:bg-neutral-200/70 hover:text-neutral-600 active:bg-neutral-300/70 sm:py-1.5 dark:text-neutral-400 dark:hover:bg-neutral-700/70 dark:hover:text-neutral-300 dark:active:bg-neutral-600/70`} 22 aria-label="Copy to clipboard" 23 > 24 <span class="iconify lucide--link"></span> ··· 30 31 export const NavBar = (props: { params: Params }) => { 32 const [handle, setHandle] = createSignal(props.params.repo); 33 34 createEffect(() => { 35 if (pds() !== undefined && props.params.repo) { ··· 87 <Show when={props.params.repo}> 88 {/* Repository Level */} 89 <div class="group relative flex items-center justify-between gap-1 rounded-md border-[0.5px] border-transparent bg-transparent px-2 transition-all duration-200 hover:border-neutral-300 hover:bg-neutral-50/40 dark:hover:border-neutral-600 dark:hover:bg-neutral-800/40"> 90 + <div class="flex min-w-0 basis-full items-center gap-2"> 91 <Tooltip text="Repository"> 92 <span class="iconify lucide--book-user shrink-0 text-neutral-500 transition-colors duration-200 group-hover:text-neutral-700 dark:text-neutral-400 dark:group-hover:text-neutral-200"></span> 93 </Tooltip> 94 + <Show 95 + when={props.params.collection} 96 + fallback={ 97 + <span class="flex min-w-0 gap-1 py-0.5 font-medium"> 98 + <Show 99 + when={handle() !== props.params.repo} 100 + fallback={<span class="truncate">{props.params.repo}</span>} 101 + > 102 + <span class="shrink-0">{handle()}</span> 103 + <span class="truncate text-neutral-500 dark:text-neutral-400"> 104 + ({props.params.repo}) 105 + </span> 106 + </Show> 107 + </span> 108 + } 109 + > 110 <A 111 end 112 href={`/at://${props.params.repo}`} 113 + inactiveClass="flex grow min-w-0 gap-1 py-0.5 font-medium text-blue-400 hover:text-blue-500 transition-colors duration-150 dark:hover:text-blue-300" 114 > 115 + <Show 116 + when={handle() !== props.params.repo} 117 + fallback={<span class="truncate">{props.params.repo}</span>} 118 + > 119 + <span class="shrink-0">{handle()}</span> 120 + <span class="truncate">({props.params.repo})</span> 121 + </Show> 122 </A> 123 + </Show> 124 </div> 125 + <CopyButton content={props.params.repo!} label="Copy DID" /> 126 </div> 127 </Show> 128
+32 -12
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; ··· 85 onCleanup(() => window.removeEventListener("paste", handlePaste)); 86 }); 87 88 const fetchTypeahead = async (input: string) => { 89 const { prefix, query } = parsePrefix(input); 90 ··· 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" ··· 292 src={actor.avatar?.replace("img/avatar/", "img/avatar_thumbnail/")} 293 class="size-9 rounded-full" 294 /> 295 - <div class="flex flex-col"> 296 <Show when={actor.displayName}> 297 - <span class="text-sm font-medium">{actor.displayName}</span> 298 </Show> 299 - <span class="text-xs text-neutral-600 dark:text-neutral-400"> 300 @{actor.handle} 301 </span> 302 </div> ··· 362 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" 363 onClick={() => setOpenList(true)} 364 > 365 - <span class="iconify lucide--help-circle"></span> 366 </button> 367 </> 368 );
··· 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; ··· 101 onCleanup(() => window.removeEventListener("paste", handlePaste)); 102 }); 103 104 + createEffect(() => { 105 + if (showSearch()) searchInput.focus(); 106 + }); 107 + 108 const fetchTypeahead = async (input: string) => { 109 const { prefix, query } = parsePrefix(input); 110 ··· 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" ··· 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> ··· 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
··· 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
··· 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}
+31 -10
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"; ··· 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"); 50 style.textContent = ` ··· 104 }); 105 106 return ( 107 - <div id="main" class="mx-auto mb-8 flex max-w-lg flex-col items-center p-4"> 108 <MetaProvider> 109 <Show when={location.pathname !== "/"}> 110 <Meta name="robots" content="noindex, nofollow" /> ··· 128 <span>PDSls</span> 129 </A> 130 <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"> 131 - <Show when={location.pathname !== "/"}> 132 - <SearchButton /> 133 - </Show> 134 - <Show when={agent()}> 135 <RecordEditor create={true} /> 136 </Show> 137 <AccountManager /> ··· 139 <DropdownMenu icon="lucide--menu text-lg" buttonClass="rounded-lg p-1.5"> 140 <NavMenu href="/jetstream" label="Jetstream" icon="lucide--radio-tower" /> 141 <NavMenu href="/firehose" label="Firehose" icon="lucide--droplet" /> 142 - <NavMenu href="/labels" label="Labels" icon="lucide--tags" /> 143 <NavMenu href="/settings" label="Settings" icon="lucide--settings" /> 144 <MenuSeparator /> 145 <NavMenu ··· 180 </Show> 181 </div> 182 <NotificationContainer /> 183 </div> 184 ); 185 };
··· 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"; ··· 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"); 64 style.textContent = ` ··· 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 /> ··· 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
··· 6 7 @custom-variant dark (&:where(.dark, .dark *)); 8 9 @theme { 10 - --font-sans: "Inter", sans-serif; 11 --font-mono: "Roboto Mono", monospace; 12 --font-pecita: "Pecita", serif; 13
··· 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
··· 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 };
+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
··· 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
··· 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
··· 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" />
+3 -3
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"; ··· 158 await resolvePDS(did); 159 if (!labelerCache[did]) throw new Error("Repository is not a labeler"); 160 rpc = new Client({ 161 - handler: new CredentialManager({ 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 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" 232 /> 233 </label> 234 </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"; ··· 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
··· 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 -39
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 icon="lucide--ellipsis-vertical" buttonClass="rounded-sm p-1.5"> 149 - <CopyMenu content={params.pds!} label="Copy PDS" icon="lucide--copy" /> 150 - <NavMenu 151 - href={`/firehose?instance=wss://${params.pds}`} 152 - label="Firehose" 153 - icon="lucide--radio-tower" 154 - /> 155 - </DropdownMenu> 156 - </MenuProvider> 157 </div> 158 - <div class="flex flex-col gap-1 px-2"> 159 - <Show when={!location.hash || location.hash === "#repos"}> 160 - <div class="flex flex-col divide-y-[0.5px] divide-neutral-300 dark:divide-neutral-700"> 161 - <For each={repos()}>{(repo) => <RepoCard {...repo} />}</For> 162 - </div> 163 - </Show> 164 <Show when={location.hash === "#info"}> 165 <Show when={version()}> 166 {(version) => ( 167 - <div class="flex items-baseline gap-x-1"> 168 <span class="font-semibold">Version</span> 169 - <span class="truncate text-sm">{version()}</span> 170 </div> 171 )} 172 </Show> 173 <Show when={serverInfos()}> 174 {(server) => ( 175 <> 176 - <div class="flex items-baseline gap-x-1"> 177 <span class="font-semibold">DID</span> 178 - <span class="truncate text-sm">{server().did}</span> 179 </div> 180 - <Show when={server().inviteCodeRequired}> 181 <span class="font-semibold">Invite Code Required</span> 182 - </Show> 183 <Show when={server().phoneVerificationRequired}> 184 - <span class="font-semibold">Phone Verification Required</span> 185 </Show> 186 <Show when={server().availableUserDomains.length}> 187 <div class="flex flex-col"> ··· 232 </div> 233 </div> 234 <Show when={!location.hash || location.hash === "#repos"}> 235 - <div class="dark:bg-dark-500 fixed bottom-0 z-5 flex w-screen justify-center bg-neutral-100 py-2"> 236 <div class="flex flex-col items-center gap-1 pb-2"> 237 <p>{repos()?.length} loaded</p> 238 <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()}>
+39 -45
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 icon="lucide--ellipsis-vertical" buttonClass="rounded-sm p-1.5"> ··· 485 <Show when={location.hash === "#info"}> 486 <div class="flex w-full flex-col gap-2 px-2 text-sm"> 487 <div> 488 - <div class="flex items-center gap-1"> 489 - <span class="iconify lucide--at-sign"></span> 490 - <p class="font-semibold">AT URI</p> 491 - </div> 492 <div class="truncate text-xs">{record()?.uri}</div> 493 </div> 494 <Show when={record()?.cid}> 495 <div> 496 - <div class="flex items-center gap-1"> 497 - <span class="iconify lucide--box"></span> 498 - <p class="font-semibold">CID</p> 499 - </div> 500 <div class="truncate text-left text-xs" dir="rtl"> 501 {record()?.cid} 502 </div> ··· 504 </Show> 505 <div> 506 <div class="flex items-center gap-1"> 507 - <span class="iconify lucide--lock-keyhole"></span> 508 <p class="font-semibold">Record verification</p> 509 <span 510 classList={{ ··· 521 </div> 522 <div> 523 <div class="flex items-center gap-1"> 524 - <span class="iconify lucide--file-check"></span> 525 <p class="font-semibold">Schema validation</p> 526 <span 527 classList={{ ··· 551 </div> 552 <Show when={lexiconUri()}> 553 <div> 554 - <div class="flex items-center gap-1"> 555 - <span class="iconify lucide--scroll-text"></span> 556 - <p class="font-semibold">Lexicon schema</p> 557 - </div> 558 <div class="truncate text-xs"> 559 <A 560 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"> ··· 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()}`}
+145 -152
src/views/repo.tsx
··· 1 - import { Client, CredentialManager } 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"; ··· 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 > ··· 139 return {}; 140 } 141 142 - rpc = new Client({ handler: new CredentialManager({ service: pds }) }); 143 try { 144 const res = await rpc.get("com.atproto.repo.describeRepo", { 145 params: { repo: did as ActorIdentifier }, ··· 211 let loaded = 0; 212 213 const reader = response.body?.getReader(); 214 - const chunks: Uint8Array[] = []; 215 216 if (reader) { 217 while (true) { ··· 275 return ( 276 <Show when={repo()}> 277 <div class="flex w-full flex-col gap-3 wrap-break-word"> 278 - <div class="dark:shadow-dark-700 dark:bg-dark-300 flex justify-between rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 text-sm shadow-xs dark:border-neutral-700"> 279 - <div class="ml-1 flex items-center gap-2 text-xs sm:gap-4 sm:text-sm"> 280 <Show when={!error()}> 281 <RepoTab tab="collections" label="Collections" /> 282 </Show> ··· 289 </Show> 290 <RepoTab tab="backlinks" label="Backlinks" /> 291 </div> 292 - <div class="flex gap-0.5"> 293 <Show when={error() && error() !== "Missing PDS"}> 294 <div class="flex items-center gap-1 text-red-500 dark:text-red-400"> 295 <span class="iconify lucide--alert-triangle"></span> 296 <span>{error()}</span> 297 </div> 298 </Show> 299 - <Show when={!error() && (!location.hash || location.hash.startsWith("#collections"))}> 300 - <Tooltip text="Filter collections"> 301 - <button 302 - 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" 303 - onClick={() => setShowFilter(!showFilter())} 304 - > 305 - <span class="iconify lucide--filter"></span> 306 - </button> 307 - </Tooltip> 308 - </Show> 309 <MenuProvider> 310 <DropdownMenu icon="lucide--ellipsis-vertical" buttonClass="rounded-sm p-1.5"> 311 <CopyMenu content={params.repo!} label="Copy DID" icon="lucide--copy" /> 312 <NavMenu 313 href={`/jetstream?dids=${params.repo}`} ··· 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)}> ··· 481 <div class="flex flex-col gap-3 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 - {detectKeyType(key())} 574 - </span> 575 </div> 576 <div class="font-mono break-all">{key()}</div> 577 </div> 578 )} ··· 585 {/* Rotation Keys Section */} 586 <Show when={rotationKeys().length > 0}> 587 <div> 588 - <div class="flex items-center gap-1"> 589 - <div class="iconify lucide--key-round" /> 590 - <p class="font-semibold">Rotation Keys</p> 591 - </div> 592 - <div class="flex flex-col gap-0.5"> 593 <For each={rotationKeys()}> 594 {(key) => ( 595 - <div class="text-sm"> 596 - <span class="rounded bg-neutral-200 px-1 py-0.5 text-xs text-neutral-800 dark:bg-neutral-700 dark:text-neutral-300"> 597 {detectDidKeyType(key)} 598 </span> 599 <div class="font-mono break-all">{key.replace("did:key:", "")}</div> 600 </div> 601 )}
··· 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"; ··· 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 > ··· 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 }, ··· 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)}> ··· 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
··· 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>