forked from pdsls.dev/pdsls
this repo has no description

Compare changes

Choose any two refs to compare.

+1
.gitignore
··· 1 node_modules 2 dist 3 .env
··· 1 node_modules 2 dist 3 .env 4 + .DS_Store
+3 -1
index.html
··· 9 <meta property="og:url" content="https://pdsls.dev" /> 10 <meta property="og:description" content="Browse the public data on atproto" /> 11 <meta property="description" content="Browse the public data on atproto" /> 12 <title>PDSls</title> 13 <link rel="preconnect" href="https://rsms.me/" /> 14 <link rel="stylesheet" href="https://rsms.me/inter/inter.css" /> 15 <link rel="preconnect" href="https://fonts.bunny.net" /> 16 <link href="https://fonts.bunny.net/css?family=roboto-mono:400" rel="stylesheet" /> 17 <script> 18 document.documentElement.classList.toggle( 19 "dark", ··· 24 <script src="/src/index.tsx" type="module"></script> 25 </head> 26 27 - <body id="root" class="dark:bg-dark-500 bg-neutral-100"> 28 <noscript>You need to enable JavaScript to run this app.</noscript> 29 </body> 30 </html>
··· 9 <meta property="og:url" content="https://pdsls.dev" /> 10 <meta property="og:description" content="Browse the public data on atproto" /> 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" /> 19 <script> 20 document.documentElement.classList.toggle( 21 "dark", ··· 26 <script src="/src/index.tsx" type="module"></script> 27 </head> 28 29 + <body id="root" class="dark:bg-dark-500 min-h-screen bg-neutral-100"> 30 <noscript>You need to enable JavaScript to run this app.</noscript> 31 </body> 32 </html>
+28 -31
package.json
··· 9 "serve": "vite preview" 10 }, 11 "devDependencies": { 12 - "@iconify-json/lucide": "^1.2.67", 13 "@iconify/tailwind4": "^1.0.6", 14 - "@tailwindcss/vite": "^4.1.13", 15 "prettier": "^3.6.2", 16 - "prettier-plugin-organize-imports": "^4.2.0", 17 - "prettier-plugin-tailwindcss": "^0.6.14", 18 - "tailwindcss": "^4.1.13", 19 - "typescript": "^5.9.2", 20 - "vite": "^7.1.5", 21 - "vite-plugin-solid": "^2.11.8" 22 }, 23 "dependencies": { 24 - "@atcute/atproto": "^3.1.4", 25 - "@atcute/bluesky": "^3.2.2", 26 - "@atcute/car": "^3.1.1", 27 - "@atcute/cbor": "^2.2.5", 28 - "@atcute/cid": "^2.2.3", 29 - "@atcute/client": "^4.0.3", 30 - "@atcute/crypto": "^2.2.4", 31 - "@atcute/did-plc": "^0.1.6", 32 - "@atcute/identity": "^1.1.0", 33 - "@atcute/identity-resolver": "^1.1.3", 34 - "@atcute/leaflet": "^1.0.7", 35 - "@atcute/lexicon-doc": "^1.1.1", 36 - "@atcute/lexicons": "^1.1.1", 37 - "@atcute/oauth-browser-client": "^1.0.26", 38 - "@atcute/tangled": "^1.0.6", 39 - "@atcute/tid": "^1.0.2", 40 - "@atcute/uint8array": "^1.0.4", 41 - "@codemirror/commands": "^6.8.1", 42 "@codemirror/lang-json": "^6.0.2", 43 - "@codemirror/lint": "^6.8.5", 44 "@codemirror/state": "^6.5.2", 45 - "@codemirror/view": "^6.38.2", 46 "@fsegurai/codemirror-theme-basic-dark": "^6.2.2", 47 "@fsegurai/codemirror-theme-basic-light": "^6.2.2", 48 "@mary/exif-rm": "jsr:^0.2.2", ··· 50 "@solidjs/meta": "^0.29.4", 51 "@solidjs/router": "^0.15.3", 52 "codemirror": "^6.0.2", 53 - "hls.js": "^1.6.12", 54 - "solid-js": "^1.9.9" 55 }, 56 - "packageManager": "pnpm@10.12.2+sha512.a32540185b964ee30bb4e979e405adc6af59226b438ee4cc19f9e8773667a66d302f5bfee60a39d3cac69e35e4b96e708a71dd002b7e9359c4112a1722ac323f" 57 }
··· 9 "serve": "vite preview" 10 }, 11 "devDependencies": { 12 + "@iconify-json/lucide": "^1.2.71", 13 "@iconify/tailwind4": "^1.0.6", 14 + "@tailwindcss/vite": "^4.1.16", 15 "prettier": "^3.6.2", 16 + "prettier-plugin-organize-imports": "^4.3.0", 17 + "prettier-plugin-tailwindcss": "^0.7.1", 18 + "tailwindcss": "^4.1.16", 19 + "typescript": "^5.9.3", 20 + "vite": "^7.1.12", 21 + "vite-plugin-solid": "^2.11.10" 22 }, 23 "dependencies": { 24 + "@atcute/atproto": "^3.1.8", 25 + "@atcute/bluesky": "^3.2.9", 26 + "@atcute/client": "^4.0.5", 27 + "@atcute/crypto": "^2.2.6", 28 + "@atcute/did-plc": "^0.1.7", 29 + "@atcute/identity": "^1.1.1", 30 + "@atcute/identity-resolver": "^1.1.4", 31 + "@atcute/leaflet": "^1.0.11", 32 + "@atcute/lexicon-doc": "^1.1.4", 33 + "@atcute/lexicon-resolver": "^0.1.3", 34 + "@atcute/lexicons": "^1.2.2", 35 + "@atcute/oauth-browser-client": "^2.0.1", 36 + "@atcute/repo": "^0.1.0", 37 + "@atcute/tangled": "^1.0.10", 38 + "@atcute/tid": "^1.0.3", 39 + "@codemirror/commands": "^6.10.0", 40 "@codemirror/lang-json": "^6.0.2", 41 + "@codemirror/lint": "^6.9.1", 42 "@codemirror/state": "^6.5.2", 43 + "@codemirror/view": "^6.38.6", 44 "@fsegurai/codemirror-theme-basic-dark": "^6.2.2", 45 "@fsegurai/codemirror-theme-basic-light": "^6.2.2", 46 "@mary/exif-rm": "jsr:^0.2.2", ··· 48 "@solidjs/meta": "^0.29.4", 49 "@solidjs/router": "^0.15.3", 50 "codemirror": "^6.0.2", 51 + "solid-js": "^1.9.10" 52 }, 53 + "packageManager": "pnpm@10.17.1+sha512.17c560fca4867ae9473a3899ad84a88334914f379be46d455cbf92e5cf4b39d34985d452d2583baf19967fa76cb5c17bc9e245529d0b98745721aa7200ecaf7a" 54 }
+757 -761
pnpm-lock.yaml
··· 9 .: 10 dependencies: 11 '@atcute/atproto': 12 - specifier: ^3.1.4 13 - version: 3.1.4 14 '@atcute/bluesky': 15 - specifier: ^3.2.2 16 - version: 3.2.2 17 - '@atcute/car': 18 - specifier: ^3.1.1 19 - version: 3.1.1 20 - '@atcute/cbor': 21 - specifier: ^2.2.5 22 - version: 2.2.5 23 - '@atcute/cid': 24 - specifier: ^2.2.3 25 - version: 2.2.3 26 '@atcute/client': 27 - specifier: ^4.0.3 28 - version: 4.0.3 29 '@atcute/crypto': 30 - specifier: ^2.2.4 31 - version: 2.2.4 32 '@atcute/did-plc': 33 - specifier: ^0.1.6 34 - version: 0.1.6 35 '@atcute/identity': 36 - specifier: ^1.1.0 37 - version: 1.1.0 38 '@atcute/identity-resolver': 39 - specifier: ^1.1.3 40 - version: 1.1.3(@atcute/identity@1.1.0) 41 '@atcute/leaflet': 42 - specifier: ^1.0.7 43 - version: 1.0.7 44 '@atcute/lexicon-doc': 45 - specifier: ^1.1.1 46 - version: 1.1.1 47 '@atcute/lexicons': 48 - specifier: ^1.1.1 49 - version: 1.1.1 50 '@atcute/oauth-browser-client': 51 - specifier: ^1.0.26 52 - version: 1.0.26 53 '@atcute/tangled': 54 - specifier: ^1.0.6 55 - version: 1.0.6 56 '@atcute/tid': 57 - specifier: ^1.0.2 58 - version: 1.0.2 59 - '@atcute/uint8array': 60 - specifier: ^1.0.4 61 - version: 1.0.4 62 '@codemirror/commands': 63 - specifier: ^6.8.1 64 - version: 6.8.1 65 '@codemirror/lang-json': 66 specifier: ^6.0.2 67 version: 6.0.2 68 '@codemirror/lint': 69 - specifier: ^6.8.5 70 - version: 6.8.5 71 '@codemirror/state': 72 specifier: ^6.5.2 73 version: 6.5.2 74 '@codemirror/view': 75 - specifier: ^6.38.2 76 - version: 6.38.2 77 '@fsegurai/codemirror-theme-basic-dark': 78 specifier: ^6.2.2 79 - version: 6.2.2(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.2)(@lezer/highlight@1.2.1) 80 '@fsegurai/codemirror-theme-basic-light': 81 specifier: ^6.2.2 82 - version: 6.2.2(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.2)(@lezer/highlight@1.2.1) 83 '@mary/exif-rm': 84 specifier: jsr:^0.2.2 85 version: '@jsr/mary__exif-rm@0.2.2' ··· 88 version: 0.5.2 89 '@solidjs/meta': 90 specifier: ^0.29.4 91 - version: 0.29.4(solid-js@1.9.9) 92 '@solidjs/router': 93 specifier: ^0.15.3 94 - version: 0.15.3(solid-js@1.9.9) 95 codemirror: 96 specifier: ^6.0.2 97 version: 6.0.2 98 - hls.js: 99 - specifier: ^1.6.12 100 - version: 1.6.12 101 solid-js: 102 - specifier: ^1.9.9 103 - version: 1.9.9 104 devDependencies: 105 '@iconify-json/lucide': 106 - specifier: ^1.2.67 107 - version: 1.2.67 108 '@iconify/tailwind4': 109 specifier: ^1.0.6 110 - version: 1.0.6(tailwindcss@4.1.13) 111 '@tailwindcss/vite': 112 - specifier: ^4.1.13 113 - version: 4.1.13(vite@7.1.5(@types/node@22.13.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.19.2)) 114 prettier: 115 specifier: ^3.6.2 116 version: 3.6.2 117 prettier-plugin-organize-imports: 118 - specifier: ^4.2.0 119 - version: 4.2.0(prettier@3.6.2)(typescript@5.9.2) 120 prettier-plugin-tailwindcss: 121 - specifier: ^0.6.14 122 - version: 0.6.14(prettier-plugin-organize-imports@4.2.0(prettier@3.6.2)(typescript@5.9.2))(prettier@3.6.2) 123 tailwindcss: 124 - specifier: ^4.1.13 125 - version: 4.1.13 126 typescript: 127 - specifier: ^5.9.2 128 - version: 5.9.2 129 vite: 130 - specifier: ^7.1.5 131 - version: 7.1.5(@types/node@22.13.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.19.2) 132 vite-plugin-solid: 133 - specifier: ^2.11.8 134 - version: 2.11.8(solid-js@1.9.9)(vite@7.1.5(@types/node@22.13.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.19.2)) 135 136 packages: 137 ··· 141 '@antfu/utils@8.1.1': 142 resolution: {integrity: sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==} 143 144 - '@atcute/atproto@3.1.4': 145 - resolution: {integrity: sha512-v0/ue7mZYtjYw4vWbtda51bLwW88mqsUQB8F/UZNO18ANAQWmKq1HDceVqjvruaLe2QPqE43XM3WkEyZ2FhOrA==} 146 147 - '@atcute/bluesky@3.2.2': 148 - resolution: {integrity: sha512-L8RrMNeRLGvSHMq2KDIAGXrpuNGA87YOXpXHY1yhmovVCjQ5n55FrR6JoQaxhprdXdKKQiefxNwQQQybDrfgFQ==} 149 150 - '@atcute/car@3.1.1': 151 - resolution: {integrity: sha512-yhez/LqIl0zHubG6z/G/gqWYHmg7wJ5L4jNkbXj5FvZ4eOvmzsw8+ojbdq6wfMU4p5NhP0pUJNLkTZHbYSPmLg==} 152 153 - '@atcute/cbor@2.2.5': 154 - resolution: {integrity: sha512-sBT8+6qau0mC3kwgmjl+nzqGn02xsE9b+kSgXm4/BRd9w8fwdRQYwcC9ApDlfaojrljJfcEkimppl/IcPOF3CA==} 155 156 - '@atcute/cid@2.2.3': 157 - resolution: {integrity: sha512-WEzNSL1EuCVtCQDFYEBIm4dEP6PcMEwi8IYUVIWvT77eO5EjY58F63z5T4qMABxSBM0+L4kqMxypdL1Fzf6LZw==} 158 159 - '@atcute/client@4.0.3': 160 - resolution: {integrity: sha512-RIOZWFVLca/HiPAAUDqQPOdOreCxTbL5cb+WUf5yqQOKIu5yEAP3eksinmlLmgIrlr5qVOE7brazUUzaskFCfw==} 161 162 - '@atcute/crypto@2.2.4': 163 - resolution: {integrity: sha512-88LbuJr63bbdJywd949YgbEiaaW4UU5iXJcFE1WqY/5ItYuoHWVdmL3XsqehiM0AfzvrYEfd5ox2wm9CK9dyQQ==} 164 165 - '@atcute/did-plc@0.1.6': 166 - resolution: {integrity: sha512-CaKZpl3UHHUczE4Co7gNi2CR3TPmQgBM0xEkKJJ6Vk4Lu9d+i9GcZQY/VBjmZntfIxHFJgZNdEkMk30lCUVpyw==} 167 168 - '@atcute/identity-resolver@1.1.3': 169 - resolution: {integrity: sha512-KZgGgg99CWaV7Df3+h3X/WMrDzTPQVfsaoIVbTNLx2B56BvCL2EmaxPSVw/7BFUJMZHlVU4rtoEB4lyvNyMswA==} 170 peerDependencies: 171 '@atcute/identity': ^1.0.0 172 173 - '@atcute/identity@1.1.0': 174 - resolution: {integrity: sha512-6vRvRqJatDB+JUQsb+UswYmtBGQnSZcqC3a2y6H5DB/v5KcIh+6nFFtc17G0+3W9rxdk7k9M4KkgkdKf/YDNoQ==} 175 176 - '@atcute/leaflet@1.0.7': 177 - resolution: {integrity: sha512-hYuClvJCagaSu2kJJeKa9OEORn1A96jkn7WYwUmo4o0z8D83AdzHZ6Ta0bzhMzZoIKAnJw9E6FHfND4BrR9t6g==} 178 179 - '@atcute/lexicon-doc@1.1.1': 180 - resolution: {integrity: sha512-/6M4K34ICZWa9rRiM9YWSsDZs0Vjv05SFJBb03mD+x14W01gIVLPK2UXiP0yIodgzWHhPQ79LLdyMJ8LirC84w==} 181 182 - '@atcute/lexicons@1.1.1': 183 - resolution: {integrity: sha512-k6qy5p3j9fJJ6ekaMPfEfp3ni4TW/XNuH9ZmsuwC0fi0tOjp+Fa8ZQakHwnqOzFt/cVBfGcmYE/lKNAbeTjgUg==} 184 185 - '@atcute/multibase@1.1.5': 186 - resolution: {integrity: sha512-vbmEFxgpntMuRqVZOCBgf6bgq69UGrlznQCZirVSit/mlcgyFVkSGbSEfkRnpIcrM8SnaySwuKbVvL+EPLh2dw==} 187 188 - '@atcute/oauth-browser-client@1.0.26': 189 - resolution: {integrity: sha512-z8VUmwRO1sFu5Dq1qYQOQLenkTSNaOyzlUZhVwFR41ru+AP84MS5UHHW/NsdC1xJAq1v6mlLySJ+pjxdDW8IYA==} 190 191 - '@atcute/tangled@1.0.6': 192 - resolution: {integrity: sha512-eEOtrKRbjKfeLYtb5hmkhE45w8h4sV6mT4E2CQzJmhOMGCiK31GX7Vqfh59rhNLb9AlbW72RcQTV737pxx+ksw==} 193 194 - '@atcute/tid@1.0.2': 195 - resolution: {integrity: sha512-ahmjroNyeDPJhtuf3+HTJropaH04HmJ8fhntDu73Gpz/RkAF7+nkz6kcP2QTgfvMCgMPAJUdskAAP82GPDTY9w==} 196 197 - '@atcute/uint8array@1.0.4': 198 - resolution: {integrity: sha512-9jASMDghzhhDwjF3+eW+ZIauvytnUWDPfUVGUValRwnf9AZ7Yqqkc76tC89HVzVDAlJdhwQIUG2dQBsiVqumUA==} 199 200 - '@atcute/util-fetch@1.0.1': 201 - resolution: {integrity: sha512-Clc0E/5ufyGBVfYBUwWNlHONlZCoblSr4Ho50l1LhmRPGB1Wu/AQ9Sz+rsBg7fdaW/auve8ulmwhRhnX2cGRow==} 202 203 - '@atcute/varint@1.0.2': 204 - resolution: {integrity: sha512-0O31hePzzr4O3NGWHUKKOyta6CGSL+AtN8iir8grGxu9jXyI7DBARlw6PbgKA6uTAvsXdpmRmF8MX+p0TsLnNg==} 205 206 '@babel/code-frame@7.27.1': 207 resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} 208 engines: {node: '>=6.9.0'} 209 210 - '@babel/compat-data@7.28.4': 211 - resolution: {integrity: sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==} 212 engines: {node: '>=6.9.0'} 213 214 - '@babel/core@7.28.4': 215 - resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==} 216 engines: {node: '>=6.9.0'} 217 218 - '@babel/generator@7.28.3': 219 - resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} 220 engines: {node: '>=6.9.0'} 221 222 '@babel/helper-compilation-targets@7.27.2': ··· 249 resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} 250 engines: {node: '>=6.9.0'} 251 252 - '@babel/helper-validator-identifier@7.27.1': 253 - resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} 254 engines: {node: '>=6.9.0'} 255 256 '@babel/helper-validator-option@7.27.1': ··· 261 resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} 262 engines: {node: '>=6.9.0'} 263 264 - '@babel/parser@7.28.4': 265 - resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} 266 engines: {node: '>=6.0.0'} 267 hasBin: true 268 ··· 276 resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} 277 engines: {node: '>=6.9.0'} 278 279 - '@babel/traverse@7.28.4': 280 - resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} 281 engines: {node: '>=6.9.0'} 282 283 - '@babel/types@7.28.4': 284 - resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} 285 engines: {node: '>=6.9.0'} 286 287 '@badrap/valita@0.4.6': 288 resolution: {integrity: sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg==} 289 engines: {node: '>= 18'} 290 291 - '@codemirror/autocomplete@6.18.7': 292 - resolution: {integrity: sha512-8EzdeIoWPJDsMBwz3zdzwXnUpCzMiCyz5/A3FIPpriaclFCGDkAzK13sMcnsu5rowqiyeQN2Vs2TsOcoDPZirQ==} 293 294 - '@codemirror/commands@6.8.1': 295 - resolution: {integrity: sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==} 296 297 '@codemirror/lang-json@6.0.2': 298 resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==} ··· 300 '@codemirror/language@6.11.3': 301 resolution: {integrity: sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==} 302 303 - '@codemirror/lint@6.8.5': 304 - resolution: {integrity: sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==} 305 306 '@codemirror/search@6.5.11': 307 resolution: {integrity: sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==} ··· 309 '@codemirror/state@6.5.2': 310 resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==} 311 312 - '@codemirror/view@6.38.2': 313 - resolution: {integrity: sha512-bTWAJxL6EOFLPzTx+O5P5xAO3gTqpatQ2b/ARQ8itfU/v2LlpS3pH2fkL0A3E/Fx8Y2St2KES7ZEV0sHTsSW/A==} 314 315 '@esbuild/aix-ppc64@0.23.1': 316 resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} ··· 318 cpu: [ppc64] 319 os: [aix] 320 321 - '@esbuild/aix-ppc64@0.25.9': 322 - resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==} 323 engines: {node: '>=18'} 324 cpu: [ppc64] 325 os: [aix] ··· 330 cpu: [arm64] 331 os: [android] 332 333 - '@esbuild/android-arm64@0.25.9': 334 - resolution: {integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==} 335 engines: {node: '>=18'} 336 cpu: [arm64] 337 os: [android] ··· 342 cpu: [arm] 343 os: [android] 344 345 - '@esbuild/android-arm@0.25.9': 346 - resolution: {integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==} 347 engines: {node: '>=18'} 348 cpu: [arm] 349 os: [android] ··· 354 cpu: [x64] 355 os: [android] 356 357 - '@esbuild/android-x64@0.25.9': 358 - resolution: {integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==} 359 engines: {node: '>=18'} 360 cpu: [x64] 361 os: [android] ··· 366 cpu: [arm64] 367 os: [darwin] 368 369 - '@esbuild/darwin-arm64@0.25.9': 370 - resolution: {integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==} 371 engines: {node: '>=18'} 372 cpu: [arm64] 373 os: [darwin] ··· 378 cpu: [x64] 379 os: [darwin] 380 381 - '@esbuild/darwin-x64@0.25.9': 382 - resolution: {integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==} 383 engines: {node: '>=18'} 384 cpu: [x64] 385 os: [darwin] ··· 390 cpu: [arm64] 391 os: [freebsd] 392 393 - '@esbuild/freebsd-arm64@0.25.9': 394 - resolution: {integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==} 395 engines: {node: '>=18'} 396 cpu: [arm64] 397 os: [freebsd] ··· 402 cpu: [x64] 403 os: [freebsd] 404 405 - '@esbuild/freebsd-x64@0.25.9': 406 - resolution: {integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==} 407 engines: {node: '>=18'} 408 cpu: [x64] 409 os: [freebsd] ··· 414 cpu: [arm64] 415 os: [linux] 416 417 - '@esbuild/linux-arm64@0.25.9': 418 - resolution: {integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==} 419 engines: {node: '>=18'} 420 cpu: [arm64] 421 os: [linux] ··· 426 cpu: [arm] 427 os: [linux] 428 429 - '@esbuild/linux-arm@0.25.9': 430 - resolution: {integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==} 431 engines: {node: '>=18'} 432 cpu: [arm] 433 os: [linux] ··· 438 cpu: [ia32] 439 os: [linux] 440 441 - '@esbuild/linux-ia32@0.25.9': 442 - resolution: {integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==} 443 engines: {node: '>=18'} 444 cpu: [ia32] 445 os: [linux] ··· 450 cpu: [loong64] 451 os: [linux] 452 453 - '@esbuild/linux-loong64@0.25.9': 454 - resolution: {integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==} 455 engines: {node: '>=18'} 456 cpu: [loong64] 457 os: [linux] ··· 462 cpu: [mips64el] 463 os: [linux] 464 465 - '@esbuild/linux-mips64el@0.25.9': 466 - resolution: {integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==} 467 engines: {node: '>=18'} 468 cpu: [mips64el] 469 os: [linux] ··· 474 cpu: [ppc64] 475 os: [linux] 476 477 - '@esbuild/linux-ppc64@0.25.9': 478 - resolution: {integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==} 479 engines: {node: '>=18'} 480 cpu: [ppc64] 481 os: [linux] ··· 486 cpu: [riscv64] 487 os: [linux] 488 489 - '@esbuild/linux-riscv64@0.25.9': 490 - resolution: {integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==} 491 engines: {node: '>=18'} 492 cpu: [riscv64] 493 os: [linux] ··· 498 cpu: [s390x] 499 os: [linux] 500 501 - '@esbuild/linux-s390x@0.25.9': 502 - resolution: {integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==} 503 engines: {node: '>=18'} 504 cpu: [s390x] 505 os: [linux] ··· 510 cpu: [x64] 511 os: [linux] 512 513 - '@esbuild/linux-x64@0.25.9': 514 - resolution: {integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==} 515 engines: {node: '>=18'} 516 cpu: [x64] 517 os: [linux] 518 519 - '@esbuild/netbsd-arm64@0.25.9': 520 - resolution: {integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==} 521 engines: {node: '>=18'} 522 cpu: [arm64] 523 os: [netbsd] ··· 528 cpu: [x64] 529 os: [netbsd] 530 531 - '@esbuild/netbsd-x64@0.25.9': 532 - resolution: {integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==} 533 engines: {node: '>=18'} 534 cpu: [x64] 535 os: [netbsd] ··· 540 cpu: [arm64] 541 os: [openbsd] 542 543 - '@esbuild/openbsd-arm64@0.25.9': 544 - resolution: {integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==} 545 engines: {node: '>=18'} 546 cpu: [arm64] 547 os: [openbsd] ··· 552 cpu: [x64] 553 os: [openbsd] 554 555 - '@esbuild/openbsd-x64@0.25.9': 556 - resolution: {integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==} 557 engines: {node: '>=18'} 558 cpu: [x64] 559 os: [openbsd] 560 561 - '@esbuild/openharmony-arm64@0.25.9': 562 - resolution: {integrity: sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==} 563 engines: {node: '>=18'} 564 cpu: [arm64] 565 os: [openharmony] ··· 570 cpu: [x64] 571 os: [sunos] 572 573 - '@esbuild/sunos-x64@0.25.9': 574 - resolution: {integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==} 575 engines: {node: '>=18'} 576 cpu: [x64] 577 os: [sunos] ··· 582 cpu: [arm64] 583 os: [win32] 584 585 - '@esbuild/win32-arm64@0.25.9': 586 - resolution: {integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==} 587 engines: {node: '>=18'} 588 cpu: [arm64] 589 os: [win32] ··· 594 cpu: [ia32] 595 os: [win32] 596 597 - '@esbuild/win32-ia32@0.25.9': 598 - resolution: {integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==} 599 engines: {node: '>=18'} 600 cpu: [ia32] 601 os: [win32] ··· 606 cpu: [x64] 607 os: [win32] 608 609 - '@esbuild/win32-x64@0.25.9': 610 - resolution: {integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==} 611 engines: {node: '>=18'} 612 cpu: [x64] 613 os: [win32] ··· 628 '@codemirror/view': ^6.0.0 629 '@lezer/highlight': ^1.0.0 630 631 - '@iconify-json/lucide@1.2.67': 632 - resolution: {integrity: sha512-YVLDYbFbX0cPxl6p9l55igm+UljlOCj0CQKwN3BoEbD3KOmDMtQumw2TGuWAE9JWURL5U6v/1oFXyKgEZDGSkQ==} 633 634 '@iconify/tailwind4@1.0.6': 635 resolution: {integrity: sha512-43ZXe+bC7CuE2LCgROdqbQeFYJi/J7L/k1UpSy8KDQlWVsWxPzLSWbWhlJx4uRYLOh1NRyw02YlDOgzBOFNd+A==} ··· 642 '@iconify/utils@2.3.0': 643 resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==} 644 645 - '@isaacs/fs-minipass@4.0.1': 646 - resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} 647 - engines: {node: '>=18.0.0'} 648 - 649 '@jridgewell/gen-mapping@0.3.13': 650 resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} 651 ··· 665 '@jsr/mary__exif-rm@0.2.2': 666 resolution: {integrity: sha512-+ZpLaC+1CyqWhH608Sqd6/yTG0pOlokn2tCXha7s1SMQ+GLKo4Nn/PskTeeP9Pt+6gNYSu6ednoSlRvXb2ZGxg==, tarball: https://npm.jsr.io/~/11/@jsr/mary__exif-rm/0.2.2.tgz} 667 668 - '@lezer/common@1.2.3': 669 - resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==} 670 671 - '@lezer/highlight@1.2.1': 672 - resolution: {integrity: sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==} 673 674 '@lezer/json@1.0.3': 675 resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==} ··· 680 '@marijn/find-cluster-break@1.0.2': 681 resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} 682 683 - '@noble/secp256k1@2.3.0': 684 - resolution: {integrity: sha512-0TQed2gcBbIrh7Ccyw+y/uZQvbJwm7Ao4scBUxqpBCcsOlZG0O4KGfjtNAy/li4W8n1xt3dxrwJ0beZ2h2G6Kw==} 685 686 - '@rollup/rollup-android-arm-eabi@4.50.1': 687 - resolution: {integrity: sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==} 688 cpu: [arm] 689 os: [android] 690 691 - '@rollup/rollup-android-arm64@4.50.1': 692 - resolution: {integrity: sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw==} 693 cpu: [arm64] 694 os: [android] 695 696 - '@rollup/rollup-darwin-arm64@4.50.1': 697 - resolution: {integrity: sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw==} 698 cpu: [arm64] 699 os: [darwin] 700 701 - '@rollup/rollup-darwin-x64@4.50.1': 702 - resolution: {integrity: sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw==} 703 cpu: [x64] 704 os: [darwin] 705 706 - '@rollup/rollup-freebsd-arm64@4.50.1': 707 - resolution: {integrity: sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA==} 708 cpu: [arm64] 709 os: [freebsd] 710 711 - '@rollup/rollup-freebsd-x64@4.50.1': 712 - resolution: {integrity: sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ==} 713 cpu: [x64] 714 os: [freebsd] 715 716 - '@rollup/rollup-linux-arm-gnueabihf@4.50.1': 717 - resolution: {integrity: sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==} 718 cpu: [arm] 719 os: [linux] 720 721 - '@rollup/rollup-linux-arm-musleabihf@4.50.1': 722 - resolution: {integrity: sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==} 723 cpu: [arm] 724 os: [linux] 725 726 - '@rollup/rollup-linux-arm64-gnu@4.50.1': 727 - resolution: {integrity: sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==} 728 cpu: [arm64] 729 os: [linux] 730 731 - '@rollup/rollup-linux-arm64-musl@4.50.1': 732 - resolution: {integrity: sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==} 733 cpu: [arm64] 734 os: [linux] 735 736 - '@rollup/rollup-linux-loongarch64-gnu@4.50.1': 737 - resolution: {integrity: sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==} 738 cpu: [loong64] 739 os: [linux] 740 741 - '@rollup/rollup-linux-ppc64-gnu@4.50.1': 742 - resolution: {integrity: sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==} 743 cpu: [ppc64] 744 os: [linux] 745 746 - '@rollup/rollup-linux-riscv64-gnu@4.50.1': 747 - resolution: {integrity: sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==} 748 cpu: [riscv64] 749 os: [linux] 750 751 - '@rollup/rollup-linux-riscv64-musl@4.50.1': 752 - resolution: {integrity: sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==} 753 cpu: [riscv64] 754 os: [linux] 755 756 - '@rollup/rollup-linux-s390x-gnu@4.50.1': 757 - resolution: {integrity: sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==} 758 cpu: [s390x] 759 os: [linux] 760 761 - '@rollup/rollup-linux-x64-gnu@4.50.1': 762 - resolution: {integrity: sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==} 763 cpu: [x64] 764 os: [linux] 765 766 - '@rollup/rollup-linux-x64-musl@4.50.1': 767 - resolution: {integrity: sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==} 768 cpu: [x64] 769 os: [linux] 770 771 - '@rollup/rollup-openharmony-arm64@4.50.1': 772 - resolution: {integrity: sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==} 773 cpu: [arm64] 774 os: [openharmony] 775 776 - '@rollup/rollup-win32-arm64-msvc@4.50.1': 777 - resolution: {integrity: sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ==} 778 cpu: [arm64] 779 os: [win32] 780 781 - '@rollup/rollup-win32-ia32-msvc@4.50.1': 782 - resolution: {integrity: sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A==} 783 cpu: [ia32] 784 os: [win32] 785 786 - '@rollup/rollup-win32-x64-msvc@4.50.1': 787 - resolution: {integrity: sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA==} 788 cpu: [x64] 789 os: [win32] 790 ··· 801 peerDependencies: 802 solid-js: ^1.8.6 803 804 - '@tailwindcss/node@4.1.13': 805 - resolution: {integrity: sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw==} 806 807 - '@tailwindcss/oxide-android-arm64@4.1.13': 808 - resolution: {integrity: sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew==} 809 engines: {node: '>= 10'} 810 cpu: [arm64] 811 os: [android] 812 813 - '@tailwindcss/oxide-darwin-arm64@4.1.13': 814 - resolution: {integrity: sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ==} 815 engines: {node: '>= 10'} 816 cpu: [arm64] 817 os: [darwin] 818 819 - '@tailwindcss/oxide-darwin-x64@4.1.13': 820 - resolution: {integrity: sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw==} 821 engines: {node: '>= 10'} 822 cpu: [x64] 823 os: [darwin] 824 825 - '@tailwindcss/oxide-freebsd-x64@4.1.13': 826 - resolution: {integrity: sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ==} 827 engines: {node: '>= 10'} 828 cpu: [x64] 829 os: [freebsd] 830 831 - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.13': 832 - resolution: {integrity: sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw==} 833 engines: {node: '>= 10'} 834 cpu: [arm] 835 os: [linux] 836 837 - '@tailwindcss/oxide-linux-arm64-gnu@4.1.13': 838 - resolution: {integrity: sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ==} 839 engines: {node: '>= 10'} 840 cpu: [arm64] 841 os: [linux] 842 843 - '@tailwindcss/oxide-linux-arm64-musl@4.1.13': 844 - resolution: {integrity: sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg==} 845 engines: {node: '>= 10'} 846 cpu: [arm64] 847 os: [linux] 848 849 - '@tailwindcss/oxide-linux-x64-gnu@4.1.13': 850 - resolution: {integrity: sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ==} 851 engines: {node: '>= 10'} 852 cpu: [x64] 853 os: [linux] 854 855 - '@tailwindcss/oxide-linux-x64-musl@4.1.13': 856 - resolution: {integrity: sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ==} 857 engines: {node: '>= 10'} 858 cpu: [x64] 859 os: [linux] 860 861 - '@tailwindcss/oxide-wasm32-wasi@4.1.13': 862 - resolution: {integrity: sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA==} 863 engines: {node: '>=14.0.0'} 864 cpu: [wasm32] 865 bundledDependencies: ··· 870 - '@emnapi/wasi-threads' 871 - tslib 872 873 - '@tailwindcss/oxide-win32-arm64-msvc@4.1.13': 874 - resolution: {integrity: sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg==} 875 engines: {node: '>= 10'} 876 cpu: [arm64] 877 os: [win32] 878 879 - '@tailwindcss/oxide-win32-x64-msvc@4.1.13': 880 - resolution: {integrity: sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw==} 881 engines: {node: '>= 10'} 882 cpu: [x64] 883 os: [win32] 884 885 - '@tailwindcss/oxide@4.1.13': 886 - resolution: {integrity: sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA==} 887 engines: {node: '>= 10'} 888 889 - '@tailwindcss/vite@4.1.13': 890 - resolution: {integrity: sha512-0PmqLQ010N58SbMTJ7BVJ4I2xopiQn/5i6nlb4JmxzQf8zcS5+m2Cv6tqh+sfDwtIdjoEnOvwsGQ1hkUi8QEHQ==} 891 peerDependencies: 892 vite: ^5.2.0 || ^6 || ^7 893 ··· 914 engines: {node: '>=0.4.0'} 915 hasBin: true 916 917 - babel-plugin-jsx-dom-expressions@0.40.1: 918 - resolution: {integrity: sha512-b4iHuirqK7RgaMzB2Lsl7MqrlDgQtVRSSazyrmx7wB3T759ggGjod5Rkok5MfHjQXhR7tRPmdwoeGPqBnW2KfA==} 919 peerDependencies: 920 '@babel/core': ^7.20.12 921 922 - babel-preset-solid@1.9.9: 923 - resolution: {integrity: sha512-pCnxWrciluXCeli/dj5PIEHgbNzim3evtTn12snjqqg8QZWJNMjH1AWIp4iG/tbVjqQ72aBEymMSagvmgxubXw==} 924 peerDependencies: 925 '@babel/core': ^7.0.0 926 - solid-js: ^1.9.8 927 peerDependenciesMeta: 928 solid-js: 929 optional: true 930 931 - baseline-browser-mapping@2.8.3: 932 - resolution: {integrity: sha512-mcE+Wr2CAhHNWxXN/DdTI+n4gsPc5QpXpWnyCQWiQYIYZX+ZMJ8juXZgjRa/0/YPJo/NSsgW15/YgmI4nbysYw==} 933 hasBin: true 934 935 - browserslist@4.26.0: 936 - resolution: {integrity: sha512-P9go2WrP9FiPwLv3zqRD/Uoxo0RSHjzFCiQz7d4vbmwNqQFo9T9WCeP/Qn5EbcKQY6DBbkxEXNcpJOmncNrb7A==} 937 engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} 938 hasBin: true 939 940 - caniuse-lite@1.0.30001741: 941 - resolution: {integrity: sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==} 942 - 943 - chownr@3.0.0: 944 - resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} 945 - engines: {node: '>=18'} 946 947 codemirror@6.0.2: 948 resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} ··· 971 supports-color: 972 optional: true 973 974 - detect-libc@2.0.4: 975 - resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} 976 engines: {node: '>=8'} 977 978 - electron-to-chromium@1.5.218: 979 - resolution: {integrity: sha512-uwwdN0TUHs8u6iRgN8vKeWZMRll4gBkz+QMqdS7DDe49uiK68/UX92lFb61oiFPrpYZNeZIqa4bA7O6Aiasnzg==} 980 981 enhanced-resolve@5.18.3: 982 resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} ··· 991 engines: {node: '>=18'} 992 hasBin: true 993 994 - esbuild@0.25.9: 995 - resolution: {integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==} 996 engines: {node: '>=18'} 997 hasBin: true 998 ··· 1024 resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} 1025 engines: {node: '>=6.9.0'} 1026 1027 - get-tsconfig@4.10.1: 1028 - resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} 1029 1030 globals@15.15.0: 1031 resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} ··· 1034 graceful-fs@4.2.11: 1035 resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 1036 1037 - hls.js@1.6.12: 1038 - resolution: {integrity: sha512-Pz+7IzvkbAht/zXvwLzA/stUHNqztqKvlLbfpq6ZYU68+gZ+CZMlsbQBPUviRap+3IQ41E39ke7Ia+yvhsehEQ==} 1039 - 1040 html-entities@2.3.3: 1041 resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} 1042 ··· 1044 resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} 1045 engines: {node: '>=12.13'} 1046 1047 - jiti@2.5.1: 1048 - resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==} 1049 hasBin: true 1050 1051 js-tokens@4.0.0: ··· 1064 kolorist@1.8.0: 1065 resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} 1066 1067 - lightningcss-darwin-arm64@1.30.1: 1068 - resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} 1069 engines: {node: '>= 12.0.0'} 1070 cpu: [arm64] 1071 os: [darwin] 1072 1073 - lightningcss-darwin-x64@1.30.1: 1074 - resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==} 1075 engines: {node: '>= 12.0.0'} 1076 cpu: [x64] 1077 os: [darwin] 1078 1079 - lightningcss-freebsd-x64@1.30.1: 1080 - resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==} 1081 engines: {node: '>= 12.0.0'} 1082 cpu: [x64] 1083 os: [freebsd] 1084 1085 - lightningcss-linux-arm-gnueabihf@1.30.1: 1086 - resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==} 1087 engines: {node: '>= 12.0.0'} 1088 cpu: [arm] 1089 os: [linux] 1090 1091 - lightningcss-linux-arm64-gnu@1.30.1: 1092 - resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==} 1093 engines: {node: '>= 12.0.0'} 1094 cpu: [arm64] 1095 os: [linux] 1096 1097 - lightningcss-linux-arm64-musl@1.30.1: 1098 - resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} 1099 engines: {node: '>= 12.0.0'} 1100 cpu: [arm64] 1101 os: [linux] 1102 1103 - lightningcss-linux-x64-gnu@1.30.1: 1104 - resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} 1105 engines: {node: '>= 12.0.0'} 1106 cpu: [x64] 1107 os: [linux] 1108 1109 - lightningcss-linux-x64-musl@1.30.1: 1110 - resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} 1111 engines: {node: '>= 12.0.0'} 1112 cpu: [x64] 1113 os: [linux] 1114 1115 - lightningcss-win32-arm64-msvc@1.30.1: 1116 - resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} 1117 engines: {node: '>= 12.0.0'} 1118 cpu: [arm64] 1119 os: [win32] 1120 1121 - lightningcss-win32-x64-msvc@1.30.1: 1122 - resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==} 1123 engines: {node: '>= 12.0.0'} 1124 cpu: [x64] 1125 os: [win32] 1126 1127 - lightningcss@1.30.1: 1128 - resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} 1129 engines: {node: '>= 12.0.0'} 1130 1131 local-pkg@1.1.2: ··· 1135 lru-cache@5.1.1: 1136 resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} 1137 1138 - magic-string@0.30.19: 1139 - resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} 1140 1141 merge-anything@5.1.7: 1142 resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==} 1143 engines: {node: '>=12.13'} 1144 1145 - minipass@7.1.2: 1146 - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} 1147 - engines: {node: '>=16 || 14 >=14.17'} 1148 - 1149 - minizlib@3.0.2: 1150 - resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} 1151 - engines: {node: '>= 18'} 1152 - 1153 - mkdirp@3.0.1: 1154 - resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} 1155 - engines: {node: '>=10'} 1156 - hasBin: true 1157 - 1158 mlly@1.8.0: 1159 resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} 1160 ··· 1170 engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 1171 hasBin: true 1172 1173 - nanoid@5.1.5: 1174 - resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==} 1175 engines: {node: ^18 || >=20} 1176 hasBin: true 1177 1178 - node-releases@2.0.21: 1179 - resolution: {integrity: sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==} 1180 1181 - package-manager-detector@1.3.0: 1182 - resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==} 1183 1184 parse5@7.3.0: 1185 resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} ··· 1204 resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} 1205 engines: {node: ^10 || ^12 || >=14} 1206 1207 - prettier-plugin-organize-imports@4.2.0: 1208 - resolution: {integrity: sha512-Zdy27UhlmyvATZi67BTnLcKTo8fm6Oik59Sz6H64PgZJVs6NJpPD1mT240mmJn62c98/QaL+r3kx9Q3gRpDajg==} 1209 peerDependencies: 1210 prettier: '>=2.0' 1211 typescript: '>=2.9' ··· 1214 vue-tsc: 1215 optional: true 1216 1217 - prettier-plugin-tailwindcss@0.6.14: 1218 - resolution: {integrity: sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg==} 1219 - engines: {node: '>=14.21.3'} 1220 peerDependencies: 1221 '@ianvs/prettier-plugin-sort-imports': '*' 1222 '@prettier/plugin-hermes': '*' ··· 1228 prettier: ^3.0 1229 prettier-plugin-astro: '*' 1230 prettier-plugin-css-order: '*' 1231 - prettier-plugin-import-sort: '*' 1232 prettier-plugin-jsdoc: '*' 1233 prettier-plugin-marko: '*' 1234 prettier-plugin-multiline-arrays: '*' 1235 prettier-plugin-organize-attributes: '*' 1236 prettier-plugin-organize-imports: '*' 1237 prettier-plugin-sort-imports: '*' 1238 - prettier-plugin-style-order: '*' 1239 prettier-plugin-svelte: '*' 1240 peerDependenciesMeta: 1241 '@ianvs/prettier-plugin-sort-imports': ··· 1256 optional: true 1257 prettier-plugin-css-order: 1258 optional: true 1259 - prettier-plugin-import-sort: 1260 - optional: true 1261 prettier-plugin-jsdoc: 1262 optional: true 1263 prettier-plugin-marko: ··· 1270 optional: true 1271 prettier-plugin-sort-imports: 1272 optional: true 1273 - prettier-plugin-style-order: 1274 - optional: true 1275 prettier-plugin-svelte: 1276 optional: true 1277 ··· 1286 resolve-pkg-maps@1.0.0: 1287 resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} 1288 1289 - rollup@4.50.1: 1290 - resolution: {integrity: sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==} 1291 engines: {node: '>=18.0.0', npm: '>=8.0.0'} 1292 hasBin: true 1293 ··· 1305 resolution: {integrity: sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==} 1306 engines: {node: '>=10'} 1307 1308 - solid-js@1.9.9: 1309 - resolution: {integrity: sha512-A0ZBPJQldAeGCTW0YRYJmt7RCeh5rbFfPZ2aOttgYnctHE7HgKeHCBB/PVc2P7eOfmNXqMFFFoYYdm3S4dcbkA==} 1310 1311 solid-refresh@0.6.3: 1312 resolution: {integrity: sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==} ··· 1317 resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 1318 engines: {node: '>=0.10.0'} 1319 1320 - style-mod@4.1.2: 1321 - resolution: {integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==} 1322 1323 - tailwindcss@4.1.13: 1324 - resolution: {integrity: sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==} 1325 1326 - tapable@2.2.3: 1327 - resolution: {integrity: sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==} 1328 engines: {node: '>=6'} 1329 1330 - tar@7.4.3: 1331 - resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} 1332 - engines: {node: '>=18'} 1333 - 1334 tinyexec@1.0.1: 1335 resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} 1336 ··· 1343 engines: {node: '>=18.0.0'} 1344 hasBin: true 1345 1346 - typescript@5.9.2: 1347 - resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} 1348 engines: {node: '>=14.17'} 1349 hasBin: true 1350 ··· 1354 undici-types@6.20.0: 1355 resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} 1356 1357 - update-browserslist-db@1.1.3: 1358 - resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} 1359 hasBin: true 1360 peerDependencies: 1361 browserslist: '>= 4.21.0' 1362 1363 - validate-html-nesting@1.2.3: 1364 - resolution: {integrity: sha512-kdkWdCl6eCeLlRShJKbjVOU2kFKxMF8Ghu50n+crEoyx+VKm3FxAxF9z4DCy6+bbTOqNW0+jcIYRnjoIRzigRw==} 1365 - 1366 - vite-plugin-solid@2.11.8: 1367 - resolution: {integrity: sha512-hFrCxBfv3B1BmFqnJF4JOCYpjrmi/zwyeKjcomQ0khh8HFyQ8SbuBWQ7zGojfrz6HUOBFrJBNySDi/JgAHytWg==} 1368 peerDependencies: 1369 '@testing-library/jest-dom': ^5.16.6 || ^5.17.0 || ^6.* 1370 solid-js: ^1.7.2 ··· 1373 '@testing-library/jest-dom': 1374 optional: true 1375 1376 - vite@7.1.5: 1377 - resolution: {integrity: sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==} 1378 engines: {node: ^20.19.0 || >=22.12.0} 1379 hasBin: true 1380 peerDependencies: ··· 1427 yallist@3.1.1: 1428 resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} 1429 1430 - yallist@5.0.0: 1431 - resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} 1432 - engines: {node: '>=18'} 1433 - 1434 yocto-queue@1.2.1: 1435 resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} 1436 engines: {node: '>=12.20'} ··· 1439 1440 '@antfu/install-pkg@1.1.0': 1441 dependencies: 1442 - package-manager-detector: 1.3.0 1443 tinyexec: 1.0.1 1444 1445 '@antfu/utils@8.1.1': {} 1446 1447 - '@atcute/atproto@3.1.4': 1448 dependencies: 1449 - '@atcute/lexicons': 1.1.1 1450 1451 - '@atcute/bluesky@3.2.2': 1452 dependencies: 1453 - '@atcute/atproto': 3.1.4 1454 - '@atcute/lexicons': 1.1.1 1455 1456 - '@atcute/car@3.1.1': 1457 dependencies: 1458 - '@atcute/cbor': 2.2.5 1459 - '@atcute/cid': 2.2.3 1460 - '@atcute/uint8array': 1.0.4 1461 - '@atcute/varint': 1.0.2 1462 yocto-queue: 1.2.1 1463 1464 - '@atcute/cbor@2.2.5': 1465 dependencies: 1466 - '@atcute/cid': 2.2.3 1467 - '@atcute/multibase': 1.1.5 1468 - '@atcute/uint8array': 1.0.4 1469 1470 - '@atcute/cid@2.2.3': 1471 dependencies: 1472 - '@atcute/multibase': 1.1.5 1473 - '@atcute/uint8array': 1.0.4 1474 1475 - '@atcute/client@4.0.3': 1476 dependencies: 1477 - '@atcute/identity': 1.1.0 1478 - '@atcute/lexicons': 1.1.1 1479 1480 - '@atcute/crypto@2.2.4': 1481 dependencies: 1482 - '@atcute/multibase': 1.1.5 1483 - '@atcute/uint8array': 1.0.4 1484 - '@noble/secp256k1': 2.3.0 1485 1486 - '@atcute/did-plc@0.1.6': 1487 dependencies: 1488 - '@atcute/cbor': 2.2.5 1489 - '@atcute/cid': 2.2.3 1490 - '@atcute/crypto': 2.2.4 1491 - '@atcute/identity': 1.1.0 1492 - '@atcute/lexicons': 1.1.1 1493 - '@atcute/multibase': 1.1.5 1494 - '@atcute/uint8array': 1.0.4 1495 '@badrap/valita': 0.4.6 1496 1497 - '@atcute/identity-resolver@1.1.3(@atcute/identity@1.1.0)': 1498 dependencies: 1499 - '@atcute/identity': 1.1.0 1500 - '@atcute/lexicons': 1.1.1 1501 - '@atcute/util-fetch': 1.0.1 1502 '@badrap/valita': 0.4.6 1503 1504 - '@atcute/identity@1.1.0': 1505 dependencies: 1506 - '@atcute/lexicons': 1.1.1 1507 '@badrap/valita': 0.4.6 1508 1509 - '@atcute/leaflet@1.0.7': 1510 dependencies: 1511 - '@atcute/atproto': 3.1.4 1512 - '@atcute/lexicons': 1.1.1 1513 1514 - '@atcute/lexicon-doc@1.1.1': 1515 dependencies: 1516 '@badrap/valita': 0.4.6 1517 1518 - '@atcute/lexicons@1.1.1': 1519 dependencies: 1520 esm-env: 1.2.2 1521 1522 - '@atcute/multibase@1.1.5': 1523 dependencies: 1524 - '@atcute/uint8array': 1.0.4 1525 1526 - '@atcute/oauth-browser-client@1.0.26': 1527 dependencies: 1528 - '@atcute/client': 4.0.3 1529 - '@atcute/identity': 1.1.0 1530 - '@atcute/lexicons': 1.1.1 1531 - '@atcute/multibase': 1.1.5 1532 - '@atcute/uint8array': 1.0.4 1533 - nanoid: 5.1.5 1534 1535 - '@atcute/tangled@1.0.6': 1536 dependencies: 1537 - '@atcute/atproto': 3.1.4 1538 - '@atcute/lexicons': 1.1.1 1539 1540 - '@atcute/tid@1.0.2': {} 1541 1542 - '@atcute/uint8array@1.0.4': {} 1543 1544 - '@atcute/util-fetch@1.0.1': 1545 dependencies: 1546 '@badrap/valita': 0.4.6 1547 1548 - '@atcute/varint@1.0.2': {} 1549 1550 '@babel/code-frame@7.27.1': 1551 dependencies: 1552 - '@babel/helper-validator-identifier': 7.27.1 1553 js-tokens: 4.0.0 1554 picocolors: 1.1.1 1555 1556 - '@babel/compat-data@7.28.4': {} 1557 1558 - '@babel/core@7.28.4': 1559 dependencies: 1560 '@babel/code-frame': 7.27.1 1561 - '@babel/generator': 7.28.3 1562 '@babel/helper-compilation-targets': 7.27.2 1563 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) 1564 '@babel/helpers': 7.28.4 1565 - '@babel/parser': 7.28.4 1566 '@babel/template': 7.27.2 1567 - '@babel/traverse': 7.28.4 1568 - '@babel/types': 7.28.4 1569 '@jridgewell/remapping': 2.3.5 1570 convert-source-map: 2.0.0 1571 debug: 4.4.3 ··· 1575 transitivePeerDependencies: 1576 - supports-color 1577 1578 - '@babel/generator@7.28.3': 1579 dependencies: 1580 - '@babel/parser': 7.28.4 1581 - '@babel/types': 7.28.4 1582 '@jridgewell/gen-mapping': 0.3.13 1583 '@jridgewell/trace-mapping': 0.3.31 1584 jsesc: 3.1.0 1585 1586 '@babel/helper-compilation-targets@7.27.2': 1587 dependencies: 1588 - '@babel/compat-data': 7.28.4 1589 '@babel/helper-validator-option': 7.27.1 1590 - browserslist: 4.26.0 1591 lru-cache: 5.1.1 1592 semver: 6.3.1 1593 ··· 1595 1596 '@babel/helper-module-imports@7.18.6': 1597 dependencies: 1598 - '@babel/types': 7.28.4 1599 1600 '@babel/helper-module-imports@7.27.1': 1601 dependencies: 1602 - '@babel/traverse': 7.28.4 1603 - '@babel/types': 7.28.4 1604 transitivePeerDependencies: 1605 - supports-color 1606 1607 - '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.4)': 1608 dependencies: 1609 - '@babel/core': 7.28.4 1610 '@babel/helper-module-imports': 7.27.1 1611 - '@babel/helper-validator-identifier': 7.27.1 1612 - '@babel/traverse': 7.28.4 1613 transitivePeerDependencies: 1614 - supports-color 1615 ··· 1617 1618 '@babel/helper-string-parser@7.27.1': {} 1619 1620 - '@babel/helper-validator-identifier@7.27.1': {} 1621 1622 '@babel/helper-validator-option@7.27.1': {} 1623 1624 '@babel/helpers@7.28.4': 1625 dependencies: 1626 '@babel/template': 7.27.2 1627 - '@babel/types': 7.28.4 1628 1629 - '@babel/parser@7.28.4': 1630 dependencies: 1631 - '@babel/types': 7.28.4 1632 1633 - '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.4)': 1634 dependencies: 1635 - '@babel/core': 7.28.4 1636 '@babel/helper-plugin-utils': 7.27.1 1637 1638 '@babel/template@7.27.2': 1639 dependencies: 1640 '@babel/code-frame': 7.27.1 1641 - '@babel/parser': 7.28.4 1642 - '@babel/types': 7.28.4 1643 1644 - '@babel/traverse@7.28.4': 1645 dependencies: 1646 '@babel/code-frame': 7.27.1 1647 - '@babel/generator': 7.28.3 1648 '@babel/helper-globals': 7.28.0 1649 - '@babel/parser': 7.28.4 1650 '@babel/template': 7.27.2 1651 - '@babel/types': 7.28.4 1652 debug: 4.4.3 1653 transitivePeerDependencies: 1654 - supports-color 1655 1656 - '@babel/types@7.28.4': 1657 dependencies: 1658 '@babel/helper-string-parser': 7.27.1 1659 - '@babel/helper-validator-identifier': 7.27.1 1660 1661 '@badrap/valita@0.4.6': {} 1662 1663 - '@codemirror/autocomplete@6.18.7': 1664 dependencies: 1665 '@codemirror/language': 6.11.3 1666 '@codemirror/state': 6.5.2 1667 - '@codemirror/view': 6.38.2 1668 - '@lezer/common': 1.2.3 1669 1670 - '@codemirror/commands@6.8.1': 1671 dependencies: 1672 '@codemirror/language': 6.11.3 1673 '@codemirror/state': 6.5.2 1674 - '@codemirror/view': 6.38.2 1675 - '@lezer/common': 1.2.3 1676 1677 '@codemirror/lang-json@6.0.2': 1678 dependencies: ··· 1682 '@codemirror/language@6.11.3': 1683 dependencies: 1684 '@codemirror/state': 6.5.2 1685 - '@codemirror/view': 6.38.2 1686 - '@lezer/common': 1.2.3 1687 - '@lezer/highlight': 1.2.1 1688 '@lezer/lr': 1.4.2 1689 - style-mod: 4.1.2 1690 1691 - '@codemirror/lint@6.8.5': 1692 dependencies: 1693 '@codemirror/state': 6.5.2 1694 - '@codemirror/view': 6.38.2 1695 crelt: 1.0.6 1696 1697 '@codemirror/search@6.5.11': 1698 dependencies: 1699 '@codemirror/state': 6.5.2 1700 - '@codemirror/view': 6.38.2 1701 crelt: 1.0.6 1702 1703 '@codemirror/state@6.5.2': 1704 dependencies: 1705 '@marijn/find-cluster-break': 1.0.2 1706 1707 - '@codemirror/view@6.38.2': 1708 dependencies: 1709 '@codemirror/state': 6.5.2 1710 crelt: 1.0.6 1711 - style-mod: 4.1.2 1712 w3c-keyname: 2.2.8 1713 1714 '@esbuild/aix-ppc64@0.23.1': 1715 optional: true 1716 1717 - '@esbuild/aix-ppc64@0.25.9': 1718 optional: true 1719 1720 '@esbuild/android-arm64@0.23.1': 1721 optional: true 1722 1723 - '@esbuild/android-arm64@0.25.9': 1724 optional: true 1725 1726 '@esbuild/android-arm@0.23.1': 1727 optional: true 1728 1729 - '@esbuild/android-arm@0.25.9': 1730 optional: true 1731 1732 '@esbuild/android-x64@0.23.1': 1733 optional: true 1734 1735 - '@esbuild/android-x64@0.25.9': 1736 optional: true 1737 1738 '@esbuild/darwin-arm64@0.23.1': 1739 optional: true 1740 1741 - '@esbuild/darwin-arm64@0.25.9': 1742 optional: true 1743 1744 '@esbuild/darwin-x64@0.23.1': 1745 optional: true 1746 1747 - '@esbuild/darwin-x64@0.25.9': 1748 optional: true 1749 1750 '@esbuild/freebsd-arm64@0.23.1': 1751 optional: true 1752 1753 - '@esbuild/freebsd-arm64@0.25.9': 1754 optional: true 1755 1756 '@esbuild/freebsd-x64@0.23.1': 1757 optional: true 1758 1759 - '@esbuild/freebsd-x64@0.25.9': 1760 optional: true 1761 1762 '@esbuild/linux-arm64@0.23.1': 1763 optional: true 1764 1765 - '@esbuild/linux-arm64@0.25.9': 1766 optional: true 1767 1768 '@esbuild/linux-arm@0.23.1': 1769 optional: true 1770 1771 - '@esbuild/linux-arm@0.25.9': 1772 optional: true 1773 1774 '@esbuild/linux-ia32@0.23.1': 1775 optional: true 1776 1777 - '@esbuild/linux-ia32@0.25.9': 1778 optional: true 1779 1780 '@esbuild/linux-loong64@0.23.1': 1781 optional: true 1782 1783 - '@esbuild/linux-loong64@0.25.9': 1784 optional: true 1785 1786 '@esbuild/linux-mips64el@0.23.1': 1787 optional: true 1788 1789 - '@esbuild/linux-mips64el@0.25.9': 1790 optional: true 1791 1792 '@esbuild/linux-ppc64@0.23.1': 1793 optional: true 1794 1795 - '@esbuild/linux-ppc64@0.25.9': 1796 optional: true 1797 1798 '@esbuild/linux-riscv64@0.23.1': 1799 optional: true 1800 1801 - '@esbuild/linux-riscv64@0.25.9': 1802 optional: true 1803 1804 '@esbuild/linux-s390x@0.23.1': 1805 optional: true 1806 1807 - '@esbuild/linux-s390x@0.25.9': 1808 optional: true 1809 1810 '@esbuild/linux-x64@0.23.1': 1811 optional: true 1812 1813 - '@esbuild/linux-x64@0.25.9': 1814 optional: true 1815 1816 - '@esbuild/netbsd-arm64@0.25.9': 1817 optional: true 1818 1819 '@esbuild/netbsd-x64@0.23.1': 1820 optional: true 1821 1822 - '@esbuild/netbsd-x64@0.25.9': 1823 optional: true 1824 1825 '@esbuild/openbsd-arm64@0.23.1': 1826 optional: true 1827 1828 - '@esbuild/openbsd-arm64@0.25.9': 1829 optional: true 1830 1831 '@esbuild/openbsd-x64@0.23.1': 1832 optional: true 1833 1834 - '@esbuild/openbsd-x64@0.25.9': 1835 optional: true 1836 1837 - '@esbuild/openharmony-arm64@0.25.9': 1838 optional: true 1839 1840 '@esbuild/sunos-x64@0.23.1': 1841 optional: true 1842 1843 - '@esbuild/sunos-x64@0.25.9': 1844 optional: true 1845 1846 '@esbuild/win32-arm64@0.23.1': 1847 optional: true 1848 1849 - '@esbuild/win32-arm64@0.25.9': 1850 optional: true 1851 1852 '@esbuild/win32-ia32@0.23.1': 1853 optional: true 1854 1855 - '@esbuild/win32-ia32@0.25.9': 1856 optional: true 1857 1858 '@esbuild/win32-x64@0.23.1': 1859 optional: true 1860 1861 - '@esbuild/win32-x64@0.25.9': 1862 optional: true 1863 1864 - '@fsegurai/codemirror-theme-basic-dark@6.2.2(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.2)(@lezer/highlight@1.2.1)': 1865 dependencies: 1866 '@codemirror/language': 6.11.3 1867 '@codemirror/state': 6.5.2 1868 - '@codemirror/view': 6.38.2 1869 - '@lezer/highlight': 1.2.1 1870 1871 - '@fsegurai/codemirror-theme-basic-light@6.2.2(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.2)(@lezer/highlight@1.2.1)': 1872 dependencies: 1873 '@codemirror/language': 6.11.3 1874 '@codemirror/state': 6.5.2 1875 - '@codemirror/view': 6.38.2 1876 - '@lezer/highlight': 1.2.1 1877 1878 - '@iconify-json/lucide@1.2.67': 1879 dependencies: 1880 '@iconify/types': 2.0.0 1881 1882 - '@iconify/tailwind4@1.0.6(tailwindcss@4.1.13)': 1883 dependencies: 1884 '@iconify/types': 2.0.0 1885 '@iconify/utils': 2.3.0 1886 - tailwindcss: 4.1.13 1887 transitivePeerDependencies: 1888 - supports-color 1889 ··· 1902 transitivePeerDependencies: 1903 - supports-color 1904 1905 - '@isaacs/fs-minipass@4.0.1': 1906 - dependencies: 1907 - minipass: 7.1.2 1908 - 1909 '@jridgewell/gen-mapping@0.3.13': 1910 dependencies: 1911 '@jridgewell/sourcemap-codec': 1.5.5 ··· 1927 1928 '@jsr/mary__exif-rm@0.2.2': {} 1929 1930 - '@lezer/common@1.2.3': {} 1931 1932 - '@lezer/highlight@1.2.1': 1933 dependencies: 1934 - '@lezer/common': 1.2.3 1935 1936 '@lezer/json@1.0.3': 1937 dependencies: 1938 - '@lezer/common': 1.2.3 1939 - '@lezer/highlight': 1.2.1 1940 '@lezer/lr': 1.4.2 1941 1942 '@lezer/lr@1.4.2': 1943 dependencies: 1944 - '@lezer/common': 1.2.3 1945 1946 '@marijn/find-cluster-break@1.0.2': {} 1947 1948 - '@noble/secp256k1@2.3.0': {} 1949 1950 - '@rollup/rollup-android-arm-eabi@4.50.1': 1951 optional: true 1952 1953 - '@rollup/rollup-android-arm64@4.50.1': 1954 optional: true 1955 1956 - '@rollup/rollup-darwin-arm64@4.50.1': 1957 optional: true 1958 1959 - '@rollup/rollup-darwin-x64@4.50.1': 1960 optional: true 1961 1962 - '@rollup/rollup-freebsd-arm64@4.50.1': 1963 optional: true 1964 1965 - '@rollup/rollup-freebsd-x64@4.50.1': 1966 optional: true 1967 1968 - '@rollup/rollup-linux-arm-gnueabihf@4.50.1': 1969 optional: true 1970 1971 - '@rollup/rollup-linux-arm-musleabihf@4.50.1': 1972 optional: true 1973 1974 - '@rollup/rollup-linux-arm64-gnu@4.50.1': 1975 optional: true 1976 1977 - '@rollup/rollup-linux-arm64-musl@4.50.1': 1978 optional: true 1979 1980 - '@rollup/rollup-linux-loongarch64-gnu@4.50.1': 1981 optional: true 1982 1983 - '@rollup/rollup-linux-ppc64-gnu@4.50.1': 1984 optional: true 1985 1986 - '@rollup/rollup-linux-riscv64-gnu@4.50.1': 1987 optional: true 1988 1989 - '@rollup/rollup-linux-riscv64-musl@4.50.1': 1990 optional: true 1991 1992 - '@rollup/rollup-linux-s390x-gnu@4.50.1': 1993 optional: true 1994 1995 - '@rollup/rollup-linux-x64-gnu@4.50.1': 1996 optional: true 1997 1998 - '@rollup/rollup-linux-x64-musl@4.50.1': 1999 optional: true 2000 2001 - '@rollup/rollup-openharmony-arm64@4.50.1': 2002 optional: true 2003 2004 - '@rollup/rollup-win32-arm64-msvc@4.50.1': 2005 optional: true 2006 2007 - '@rollup/rollup-win32-ia32-msvc@4.50.1': 2008 optional: true 2009 2010 - '@rollup/rollup-win32-x64-msvc@4.50.1': 2011 optional: true 2012 2013 '@skyware/firehose@0.5.2': 2014 dependencies: 2015 - '@atcute/car': 3.1.1 2016 - '@atcute/cbor': 2.2.5 2017 nanoevents: 9.1.0 2018 2019 - '@solidjs/meta@0.29.4(solid-js@1.9.9)': 2020 dependencies: 2021 - solid-js: 1.9.9 2022 2023 - '@solidjs/router@0.15.3(solid-js@1.9.9)': 2024 dependencies: 2025 - solid-js: 1.9.9 2026 2027 - '@tailwindcss/node@4.1.13': 2028 dependencies: 2029 '@jridgewell/remapping': 2.3.5 2030 enhanced-resolve: 5.18.3 2031 - jiti: 2.5.1 2032 - lightningcss: 1.30.1 2033 - magic-string: 0.30.19 2034 source-map-js: 1.2.1 2035 - tailwindcss: 4.1.13 2036 2037 - '@tailwindcss/oxide-android-arm64@4.1.13': 2038 optional: true 2039 2040 - '@tailwindcss/oxide-darwin-arm64@4.1.13': 2041 optional: true 2042 2043 - '@tailwindcss/oxide-darwin-x64@4.1.13': 2044 optional: true 2045 2046 - '@tailwindcss/oxide-freebsd-x64@4.1.13': 2047 optional: true 2048 2049 - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.13': 2050 optional: true 2051 2052 - '@tailwindcss/oxide-linux-arm64-gnu@4.1.13': 2053 optional: true 2054 2055 - '@tailwindcss/oxide-linux-arm64-musl@4.1.13': 2056 optional: true 2057 2058 - '@tailwindcss/oxide-linux-x64-gnu@4.1.13': 2059 optional: true 2060 2061 - '@tailwindcss/oxide-linux-x64-musl@4.1.13': 2062 optional: true 2063 2064 - '@tailwindcss/oxide-wasm32-wasi@4.1.13': 2065 optional: true 2066 2067 - '@tailwindcss/oxide-win32-arm64-msvc@4.1.13': 2068 optional: true 2069 2070 - '@tailwindcss/oxide-win32-x64-msvc@4.1.13': 2071 optional: true 2072 2073 - '@tailwindcss/oxide@4.1.13': 2074 - dependencies: 2075 - detect-libc: 2.0.4 2076 - tar: 7.4.3 2077 optionalDependencies: 2078 - '@tailwindcss/oxide-android-arm64': 4.1.13 2079 - '@tailwindcss/oxide-darwin-arm64': 4.1.13 2080 - '@tailwindcss/oxide-darwin-x64': 4.1.13 2081 - '@tailwindcss/oxide-freebsd-x64': 4.1.13 2082 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.13 2083 - '@tailwindcss/oxide-linux-arm64-gnu': 4.1.13 2084 - '@tailwindcss/oxide-linux-arm64-musl': 4.1.13 2085 - '@tailwindcss/oxide-linux-x64-gnu': 4.1.13 2086 - '@tailwindcss/oxide-linux-x64-musl': 4.1.13 2087 - '@tailwindcss/oxide-wasm32-wasi': 4.1.13 2088 - '@tailwindcss/oxide-win32-arm64-msvc': 4.1.13 2089 - '@tailwindcss/oxide-win32-x64-msvc': 4.1.13 2090 2091 - '@tailwindcss/vite@4.1.13(vite@7.1.5(@types/node@22.13.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.19.2))': 2092 dependencies: 2093 - '@tailwindcss/node': 4.1.13 2094 - '@tailwindcss/oxide': 4.1.13 2095 - tailwindcss: 4.1.13 2096 - vite: 7.1.5(@types/node@22.13.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.19.2) 2097 2098 '@types/babel__core@7.20.5': 2099 dependencies: 2100 - '@babel/parser': 7.28.4 2101 - '@babel/types': 7.28.4 2102 '@types/babel__generator': 7.27.0 2103 '@types/babel__template': 7.4.4 2104 '@types/babel__traverse': 7.28.0 2105 2106 '@types/babel__generator@7.27.0': 2107 dependencies: 2108 - '@babel/types': 7.28.4 2109 2110 '@types/babel__template@7.4.4': 2111 dependencies: 2112 - '@babel/parser': 7.28.4 2113 - '@babel/types': 7.28.4 2114 2115 '@types/babel__traverse@7.28.0': 2116 dependencies: 2117 - '@babel/types': 7.28.4 2118 2119 '@types/estree@1.0.8': {} 2120 ··· 2125 2126 acorn@8.15.0: {} 2127 2128 - babel-plugin-jsx-dom-expressions@0.40.1(@babel/core@7.28.4): 2129 dependencies: 2130 - '@babel/core': 7.28.4 2131 '@babel/helper-module-imports': 7.18.6 2132 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4) 2133 - '@babel/types': 7.28.4 2134 html-entities: 2.3.3 2135 parse5: 7.3.0 2136 - validate-html-nesting: 1.2.3 2137 2138 - babel-preset-solid@1.9.9(@babel/core@7.28.4)(solid-js@1.9.9): 2139 dependencies: 2140 - '@babel/core': 7.28.4 2141 - babel-plugin-jsx-dom-expressions: 0.40.1(@babel/core@7.28.4) 2142 optionalDependencies: 2143 - solid-js: 1.9.9 2144 2145 - baseline-browser-mapping@2.8.3: {} 2146 2147 - browserslist@4.26.0: 2148 dependencies: 2149 - baseline-browser-mapping: 2.8.3 2150 - caniuse-lite: 1.0.30001741 2151 - electron-to-chromium: 1.5.218 2152 - node-releases: 2.0.21 2153 - update-browserslist-db: 1.1.3(browserslist@4.26.0) 2154 - 2155 - caniuse-lite@1.0.30001741: {} 2156 2157 - chownr@3.0.0: {} 2158 2159 codemirror@6.0.2: 2160 dependencies: 2161 - '@codemirror/autocomplete': 6.18.7 2162 - '@codemirror/commands': 6.8.1 2163 '@codemirror/language': 6.11.3 2164 - '@codemirror/lint': 6.8.5 2165 '@codemirror/search': 6.5.11 2166 '@codemirror/state': 6.5.2 2167 - '@codemirror/view': 6.38.2 2168 2169 confbox@0.1.8: {} 2170 ··· 2180 dependencies: 2181 ms: 2.1.3 2182 2183 - detect-libc@2.0.4: {} 2184 2185 - electron-to-chromium@1.5.218: {} 2186 2187 enhanced-resolve@5.18.3: 2188 dependencies: 2189 graceful-fs: 4.2.11 2190 - tapable: 2.2.3 2191 2192 entities@6.0.1: {} 2193 ··· 2219 '@esbuild/win32-x64': 0.23.1 2220 optional: true 2221 2222 - esbuild@0.25.9: 2223 optionalDependencies: 2224 - '@esbuild/aix-ppc64': 0.25.9 2225 - '@esbuild/android-arm': 0.25.9 2226 - '@esbuild/android-arm64': 0.25.9 2227 - '@esbuild/android-x64': 0.25.9 2228 - '@esbuild/darwin-arm64': 0.25.9 2229 - '@esbuild/darwin-x64': 0.25.9 2230 - '@esbuild/freebsd-arm64': 0.25.9 2231 - '@esbuild/freebsd-x64': 0.25.9 2232 - '@esbuild/linux-arm': 0.25.9 2233 - '@esbuild/linux-arm64': 0.25.9 2234 - '@esbuild/linux-ia32': 0.25.9 2235 - '@esbuild/linux-loong64': 0.25.9 2236 - '@esbuild/linux-mips64el': 0.25.9 2237 - '@esbuild/linux-ppc64': 0.25.9 2238 - '@esbuild/linux-riscv64': 0.25.9 2239 - '@esbuild/linux-s390x': 0.25.9 2240 - '@esbuild/linux-x64': 0.25.9 2241 - '@esbuild/netbsd-arm64': 0.25.9 2242 - '@esbuild/netbsd-x64': 0.25.9 2243 - '@esbuild/openbsd-arm64': 0.25.9 2244 - '@esbuild/openbsd-x64': 0.25.9 2245 - '@esbuild/openharmony-arm64': 0.25.9 2246 - '@esbuild/sunos-x64': 0.25.9 2247 - '@esbuild/win32-arm64': 0.25.9 2248 - '@esbuild/win32-ia32': 0.25.9 2249 - '@esbuild/win32-x64': 0.25.9 2250 2251 escalade@3.2.0: {} 2252 ··· 2263 2264 gensync@1.0.0-beta.2: {} 2265 2266 - get-tsconfig@4.10.1: 2267 dependencies: 2268 resolve-pkg-maps: 1.0.0 2269 optional: true ··· 2272 2273 graceful-fs@4.2.11: {} 2274 2275 - hls.js@1.6.12: {} 2276 - 2277 html-entities@2.3.3: {} 2278 2279 is-what@4.1.16: {} 2280 2281 - jiti@2.5.1: {} 2282 2283 js-tokens@4.0.0: {} 2284 ··· 2288 2289 kolorist@1.8.0: {} 2290 2291 - lightningcss-darwin-arm64@1.30.1: 2292 optional: true 2293 2294 - lightningcss-darwin-x64@1.30.1: 2295 optional: true 2296 2297 - lightningcss-freebsd-x64@1.30.1: 2298 optional: true 2299 2300 - lightningcss-linux-arm-gnueabihf@1.30.1: 2301 optional: true 2302 2303 - lightningcss-linux-arm64-gnu@1.30.1: 2304 optional: true 2305 2306 - lightningcss-linux-arm64-musl@1.30.1: 2307 optional: true 2308 2309 - lightningcss-linux-x64-gnu@1.30.1: 2310 optional: true 2311 2312 - lightningcss-linux-x64-musl@1.30.1: 2313 optional: true 2314 2315 - lightningcss-win32-arm64-msvc@1.30.1: 2316 optional: true 2317 2318 - lightningcss-win32-x64-msvc@1.30.1: 2319 optional: true 2320 2321 - lightningcss@1.30.1: 2322 dependencies: 2323 - detect-libc: 2.0.4 2324 optionalDependencies: 2325 - lightningcss-darwin-arm64: 1.30.1 2326 - lightningcss-darwin-x64: 1.30.1 2327 - lightningcss-freebsd-x64: 1.30.1 2328 - lightningcss-linux-arm-gnueabihf: 1.30.1 2329 - lightningcss-linux-arm64-gnu: 1.30.1 2330 - lightningcss-linux-arm64-musl: 1.30.1 2331 - lightningcss-linux-x64-gnu: 1.30.1 2332 - lightningcss-linux-x64-musl: 1.30.1 2333 - lightningcss-win32-arm64-msvc: 1.30.1 2334 - lightningcss-win32-x64-msvc: 1.30.1 2335 2336 local-pkg@1.1.2: 2337 dependencies: ··· 2343 dependencies: 2344 yallist: 3.1.1 2345 2346 - magic-string@0.30.19: 2347 dependencies: 2348 '@jridgewell/sourcemap-codec': 1.5.5 2349 ··· 2351 dependencies: 2352 is-what: 4.1.16 2353 2354 - minipass@7.1.2: {} 2355 - 2356 - minizlib@3.0.2: 2357 - dependencies: 2358 - minipass: 7.1.2 2359 - 2360 - mkdirp@3.0.1: {} 2361 - 2362 mlly@1.8.0: 2363 dependencies: 2364 acorn: 8.15.0 ··· 2372 2373 nanoid@3.3.11: {} 2374 2375 - nanoid@5.1.5: {} 2376 2377 - node-releases@2.0.21: {} 2378 2379 - package-manager-detector@1.3.0: {} 2380 2381 parse5@7.3.0: 2382 dependencies: ··· 2406 picocolors: 1.1.1 2407 source-map-js: 1.2.1 2408 2409 - prettier-plugin-organize-imports@4.2.0(prettier@3.6.2)(typescript@5.9.2): 2410 dependencies: 2411 prettier: 3.6.2 2412 - typescript: 5.9.2 2413 2414 - prettier-plugin-tailwindcss@0.6.14(prettier-plugin-organize-imports@4.2.0(prettier@3.6.2)(typescript@5.9.2))(prettier@3.6.2): 2415 dependencies: 2416 prettier: 3.6.2 2417 optionalDependencies: 2418 - prettier-plugin-organize-imports: 4.2.0(prettier@3.6.2)(typescript@5.9.2) 2419 2420 prettier@3.6.2: {} 2421 ··· 2424 resolve-pkg-maps@1.0.0: 2425 optional: true 2426 2427 - rollup@4.50.1: 2428 dependencies: 2429 '@types/estree': 1.0.8 2430 optionalDependencies: 2431 - '@rollup/rollup-android-arm-eabi': 4.50.1 2432 - '@rollup/rollup-android-arm64': 4.50.1 2433 - '@rollup/rollup-darwin-arm64': 4.50.1 2434 - '@rollup/rollup-darwin-x64': 4.50.1 2435 - '@rollup/rollup-freebsd-arm64': 4.50.1 2436 - '@rollup/rollup-freebsd-x64': 4.50.1 2437 - '@rollup/rollup-linux-arm-gnueabihf': 4.50.1 2438 - '@rollup/rollup-linux-arm-musleabihf': 4.50.1 2439 - '@rollup/rollup-linux-arm64-gnu': 4.50.1 2440 - '@rollup/rollup-linux-arm64-musl': 4.50.1 2441 - '@rollup/rollup-linux-loongarch64-gnu': 4.50.1 2442 - '@rollup/rollup-linux-ppc64-gnu': 4.50.1 2443 - '@rollup/rollup-linux-riscv64-gnu': 4.50.1 2444 - '@rollup/rollup-linux-riscv64-musl': 4.50.1 2445 - '@rollup/rollup-linux-s390x-gnu': 4.50.1 2446 - '@rollup/rollup-linux-x64-gnu': 4.50.1 2447 - '@rollup/rollup-linux-x64-musl': 4.50.1 2448 - '@rollup/rollup-openharmony-arm64': 4.50.1 2449 - '@rollup/rollup-win32-arm64-msvc': 4.50.1 2450 - '@rollup/rollup-win32-ia32-msvc': 4.50.1 2451 - '@rollup/rollup-win32-x64-msvc': 4.50.1 2452 fsevents: 2.3.3 2453 2454 semver@6.3.1: {} ··· 2459 2460 seroval@1.3.2: {} 2461 2462 - solid-js@1.9.9: 2463 dependencies: 2464 csstype: 3.1.3 2465 seroval: 1.3.2 2466 seroval-plugins: 1.3.3(seroval@1.3.2) 2467 2468 - solid-refresh@0.6.3(solid-js@1.9.9): 2469 dependencies: 2470 - '@babel/generator': 7.28.3 2471 '@babel/helper-module-imports': 7.27.1 2472 - '@babel/types': 7.28.4 2473 - solid-js: 1.9.9 2474 transitivePeerDependencies: 2475 - supports-color 2476 2477 source-map-js@1.2.1: {} 2478 2479 - style-mod@4.1.2: {} 2480 2481 - tailwindcss@4.1.13: {} 2482 2483 - tapable@2.2.3: {} 2484 - 2485 - tar@7.4.3: 2486 - dependencies: 2487 - '@isaacs/fs-minipass': 4.0.1 2488 - chownr: 3.0.0 2489 - minipass: 7.1.2 2490 - minizlib: 3.0.2 2491 - mkdirp: 3.0.1 2492 - yallist: 5.0.0 2493 2494 tinyexec@1.0.1: {} 2495 ··· 2501 tsx@4.19.2: 2502 dependencies: 2503 esbuild: 0.23.1 2504 - get-tsconfig: 4.10.1 2505 optionalDependencies: 2506 fsevents: 2.3.3 2507 optional: true 2508 2509 - typescript@5.9.2: {} 2510 2511 ufo@1.6.1: {} 2512 2513 undici-types@6.20.0: 2514 optional: true 2515 2516 - update-browserslist-db@1.1.3(browserslist@4.26.0): 2517 dependencies: 2518 - browserslist: 4.26.0 2519 escalade: 3.2.0 2520 picocolors: 1.1.1 2521 2522 - validate-html-nesting@1.2.3: {} 2523 - 2524 - vite-plugin-solid@2.11.8(solid-js@1.9.9)(vite@7.1.5(@types/node@22.13.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.19.2)): 2525 dependencies: 2526 - '@babel/core': 7.28.4 2527 '@types/babel__core': 7.20.5 2528 - babel-preset-solid: 1.9.9(@babel/core@7.28.4)(solid-js@1.9.9) 2529 merge-anything: 5.1.7 2530 - solid-js: 1.9.9 2531 - solid-refresh: 0.6.3(solid-js@1.9.9) 2532 - vite: 7.1.5(@types/node@22.13.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.19.2) 2533 - vitefu: 1.1.1(vite@7.1.5(@types/node@22.13.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.19.2)) 2534 transitivePeerDependencies: 2535 - supports-color 2536 2537 - vite@7.1.5(@types/node@22.13.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.19.2): 2538 dependencies: 2539 - esbuild: 0.25.9 2540 fdir: 6.5.0(picomatch@4.0.3) 2541 picomatch: 4.0.3 2542 postcss: 8.5.6 2543 - rollup: 4.50.1 2544 tinyglobby: 0.2.15 2545 optionalDependencies: 2546 '@types/node': 22.13.1 2547 fsevents: 2.3.3 2548 - jiti: 2.5.1 2549 - lightningcss: 1.30.1 2550 tsx: 4.19.2 2551 2552 - vitefu@1.1.1(vite@7.1.5(@types/node@22.13.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.19.2)): 2553 optionalDependencies: 2554 - vite: 7.1.5(@types/node@22.13.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.19.2) 2555 2556 w3c-keyname@2.2.8: {} 2557 2558 yallist@3.1.1: {} 2559 - 2560 - yallist@5.0.0: {} 2561 2562 yocto-queue@1.2.1: {}
··· 9 .: 10 dependencies: 11 '@atcute/atproto': 12 + specifier: ^3.1.8 13 + version: 3.1.8 14 '@atcute/bluesky': 15 + specifier: ^3.2.9 16 + version: 3.2.9 17 '@atcute/client': 18 + specifier: ^4.0.5 19 + version: 4.0.5 20 '@atcute/crypto': 21 + specifier: ^2.2.6 22 + version: 2.2.6 23 '@atcute/did-plc': 24 + specifier: ^0.1.7 25 + version: 0.1.7 26 '@atcute/identity': 27 + specifier: ^1.1.1 28 + version: 1.1.1 29 '@atcute/identity-resolver': 30 + specifier: ^1.1.4 31 + version: 1.1.4(@atcute/identity@1.1.1) 32 '@atcute/leaflet': 33 + specifier: ^1.0.11 34 + version: 1.0.11 35 '@atcute/lexicon-doc': 36 + specifier: ^1.1.4 37 + version: 1.1.4 38 + '@atcute/lexicon-resolver': 39 + specifier: ^0.1.3 40 + version: 0.1.3(@atcute/identity-resolver@1.1.4(@atcute/identity@1.1.1))(@atcute/identity@1.1.1) 41 '@atcute/lexicons': 42 + specifier: ^1.2.2 43 + version: 1.2.2 44 '@atcute/oauth-browser-client': 45 + specifier: ^2.0.1 46 + version: 2.0.1 47 + '@atcute/repo': 48 + specifier: ^0.1.0 49 + version: 0.1.0 50 '@atcute/tangled': 51 + specifier: ^1.0.10 52 + version: 1.0.10 53 '@atcute/tid': 54 + specifier: ^1.0.3 55 + version: 1.0.3 56 '@codemirror/commands': 57 + specifier: ^6.10.0 58 + version: 6.10.0 59 '@codemirror/lang-json': 60 specifier: ^6.0.2 61 version: 6.0.2 62 '@codemirror/lint': 63 + specifier: ^6.9.1 64 + version: 6.9.1 65 '@codemirror/state': 66 specifier: ^6.5.2 67 version: 6.5.2 68 '@codemirror/view': 69 + specifier: ^6.38.6 70 + version: 6.38.6 71 '@fsegurai/codemirror-theme-basic-dark': 72 specifier: ^6.2.2 73 + version: 6.2.2(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.6)(@lezer/highlight@1.2.3) 74 '@fsegurai/codemirror-theme-basic-light': 75 specifier: ^6.2.2 76 + version: 6.2.2(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.6)(@lezer/highlight@1.2.3) 77 '@mary/exif-rm': 78 specifier: jsr:^0.2.2 79 version: '@jsr/mary__exif-rm@0.2.2' ··· 82 version: 0.5.2 83 '@solidjs/meta': 84 specifier: ^0.29.4 85 + version: 0.29.4(solid-js@1.9.10) 86 '@solidjs/router': 87 specifier: ^0.15.3 88 + version: 0.15.3(solid-js@1.9.10) 89 codemirror: 90 specifier: ^6.0.2 91 version: 6.0.2 92 solid-js: 93 + specifier: ^1.9.10 94 + version: 1.9.10 95 devDependencies: 96 '@iconify-json/lucide': 97 + specifier: ^1.2.71 98 + version: 1.2.71 99 '@iconify/tailwind4': 100 specifier: ^1.0.6 101 + version: 1.0.6(tailwindcss@4.1.16) 102 '@tailwindcss/vite': 103 + specifier: ^4.1.16 104 + version: 4.1.16(vite@7.1.12(@types/node@22.13.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)) 105 prettier: 106 specifier: ^3.6.2 107 version: 3.6.2 108 prettier-plugin-organize-imports: 109 + specifier: ^4.3.0 110 + version: 4.3.0(prettier@3.6.2)(typescript@5.9.3) 111 prettier-plugin-tailwindcss: 112 + specifier: ^0.7.1 113 + version: 0.7.1(prettier-plugin-organize-imports@4.3.0(prettier@3.6.2)(typescript@5.9.3))(prettier@3.6.2) 114 tailwindcss: 115 + specifier: ^4.1.16 116 + version: 4.1.16 117 typescript: 118 + specifier: ^5.9.3 119 + version: 5.9.3 120 vite: 121 + specifier: ^7.1.12 122 + version: 7.1.12(@types/node@22.13.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2) 123 vite-plugin-solid: 124 + specifier: ^2.11.10 125 + version: 2.11.10(solid-js@1.9.10)(vite@7.1.12(@types/node@22.13.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)) 126 127 packages: 128 ··· 132 '@antfu/utils@8.1.1': 133 resolution: {integrity: sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==} 134 135 + '@atcute/atproto@3.1.8': 136 + resolution: {integrity: sha512-Miu+S7RSgAYbmQWtHJKfSFUN5Kliqoo4YH0rILPmBtfmlZieORJgXNj9oO/Uive0/ulWkiRse07ATIcK8JxMnw==} 137 138 + '@atcute/bluesky@3.2.9': 139 + resolution: {integrity: sha512-69+mAnnH/uyMoT3/jHLBNILHa3+dm8utDKbm/2xqSPMLvRK47Wo5COlpchu8Xq+NGisHqukhHYT8NYdQFfSJhA==} 140 141 + '@atcute/car@3.1.3': 142 + resolution: {integrity: sha512-WJ13bAEt7TjDMVi09ubjLtvhdljbWInGm9Kfy7Y6NhrmiyC/aZYaA/zHX/bHI6xv1c/h3SQduWqxOr4ae49eqA==} 143 + 144 + '@atcute/car@5.0.0': 145 + resolution: {integrity: sha512-OIY2xTXv8lSpZsDSn/UYQtJSMvDw5Hi4Q+uyvmiqSM+fht08QRAEq/nxa5YFciPZ3nfDFnZ3//EgJw7QhkSXLQ==} 146 147 + '@atcute/cbor@2.2.7': 148 + resolution: {integrity: sha512-/mwAF0gnokOphceZqFq3uzMGdd8sbw5y6bxF8CRutRkCCUcpjjpJc5fkLwhxyGgOveF3mZuHE6p7t/+IAqb7Aw==} 149 150 + '@atcute/cid@2.2.6': 151 + resolution: {integrity: sha512-bTAHHbJ24p+E//V4KCS4xdmd39o211jJswvqQOevj7vk+5IYcgDLx1ryZWZ1sEPOo9x875li/kj5gpKL14RDwQ==} 152 153 + '@atcute/client@4.0.5': 154 + resolution: {integrity: sha512-R8Qen8goGmEkynYGg2m6XFlVmz0GTDvQ+9w+4QqOob+XMk8/WDpF4aImev7WKEde/rV2gjcqW7zM8E6W9NShDA==} 155 156 + '@atcute/crypto@2.2.6': 157 + resolution: {integrity: sha512-vkuexF+kmrKE1/Uqzub99Qi4QpnxA2jbu60E6PTgL4XypELQ6rb59MB/J1VbY2gs0kd3ET7+L3+NWpKD5nXyfA==} 158 159 + '@atcute/did-plc@0.1.7': 160 + resolution: {integrity: sha512-a7yOQNqViae3rB5/xa3U0EPJbFD9l8zOHXx6XASZ5F8+Vy2uTgXK3omurpNZ5UxRpy1ni1AMhSohXr61cqWbkg==} 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.1': 168 + resolution: {integrity: sha512-zax42n693VEhnC+5tndvO2KLDTMkHOz8UExwmklvJv7R9VujfEwiSWhcv6Jgwb3ellaG8wjiQ1lMOIjLLvwh0Q==} 169 170 + '@atcute/leaflet@1.0.11': 171 + resolution: {integrity: sha512-PmhBIltPD4DqR737J412ePwtfyBeC/Xr9R+s/d55Aq0kLPo4aBWt+JDb40Mh4JCGGxiz6gu9HFCCZeaVLrL4Dw==} 172 173 + '@atcute/lexicon-doc@1.1.4': 174 + resolution: {integrity: sha512-OL0fsXtbnN/KwCq/L3nWGvOCdSHV0NWTatgLUIPt+T9AhcziFNaXAbbjvVHdflr3ZaLh3ksleHK0J789UBhlWQ==} 175 176 + '@atcute/lexicon-resolver@0.1.3': 177 + resolution: {integrity: sha512-4AOS3KKm60GtBfl7ue/35xwZlylAuX5V2xmXnAmNoiN3vIauNkYawwRqgtni5q+EIV9R7p4D8tzkv58NaZ8fEQ==} 178 + peerDependencies: 179 + '@atcute/identity': ^1.1.0 180 + '@atcute/identity-resolver': ^1.1.3 181 182 + '@atcute/lexicons@1.2.2': 183 + resolution: {integrity: sha512-bgEhJq5Z70/0TbK5sx+tAkrR8FsCODNiL2gUEvS5PuJfPxmFmRYNWaMGehxSPaXWpU2+Oa9ckceHiYbrItDTkA==} 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.10': 198 + resolution: {integrity: sha512-DGconZIN5TpLBah+aHGbWI1tMsL7XzyVEbr/fW4CbcLWYKICU6SAUZ0YnZ+5GvltjlORWHUy7hfftvoh4zodIA==} 199 200 + '@atcute/tid@1.0.3': 201 + resolution: {integrity: sha512-wfMJx1IMdnu0CZgWl0uR4JO2s6PGT1YPhpytD4ZHzEYKKQVuqV6Eb/7vieaVo1eYNMp2FrY67FZObeR7utRl2w==} 202 + 203 + '@atcute/uint8array@1.0.5': 204 + resolution: {integrity: sha512-XLWWxoR2HNl2qU+FCr0rp1APwJXci7HnzbOQLxK55OaMNBXZ19+xNC5ii4QCsThsDxa4JS/JTzuiQLziITWf2Q==} 205 + 206 + '@atcute/util-fetch@1.0.3': 207 + resolution: {integrity: sha512-f8zzTb/xlKIwv2OQ31DhShPUNCmIIleX6p7qIXwWwEUjX6x8skUtpdISSjnImq01LXpltGV5y8yhV4/Mlb7CRQ==} 208 + 209 + '@atcute/varint@1.0.3': 210 + resolution: {integrity: sha512-fdvMPyBB+McDT+Ai5e9RwEbwYV4yjZ60S2Dn5PTjGqUyxvoCH1z42viuheDZRUDkmfQehXJTZ5az7dSozVNtog==} 211 212 '@babel/code-frame@7.27.1': 213 resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} 214 engines: {node: '>=6.9.0'} 215 216 + '@babel/compat-data@7.28.5': 217 + resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==} 218 engines: {node: '>=6.9.0'} 219 220 + '@babel/core@7.28.5': 221 + resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==} 222 engines: {node: '>=6.9.0'} 223 224 + '@babel/generator@7.28.5': 225 + resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} 226 engines: {node: '>=6.9.0'} 227 228 '@babel/helper-compilation-targets@7.27.2': ··· 255 resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} 256 engines: {node: '>=6.9.0'} 257 258 + '@babel/helper-validator-identifier@7.28.5': 259 + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} 260 engines: {node: '>=6.9.0'} 261 262 '@babel/helper-validator-option@7.27.1': ··· 267 resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} 268 engines: {node: '>=6.9.0'} 269 270 + '@babel/parser@7.28.5': 271 + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} 272 engines: {node: '>=6.0.0'} 273 hasBin: true 274 ··· 282 resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} 283 engines: {node: '>=6.9.0'} 284 285 + '@babel/traverse@7.28.5': 286 + resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} 287 engines: {node: '>=6.9.0'} 288 289 + '@babel/types@7.28.5': 290 + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} 291 engines: {node: '>=6.9.0'} 292 293 '@badrap/valita@0.4.6': 294 resolution: {integrity: sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg==} 295 engines: {node: '>= 18'} 296 297 + '@codemirror/autocomplete@6.19.1': 298 + resolution: {integrity: sha512-q6NenYkEy2fn9+JyjIxMWcNjzTL/IhwqfzOut1/G3PrIFkrbl4AL7Wkse5tLrQUUyqGoAKU5+Pi5jnnXxH5HGw==} 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==} ··· 306 '@codemirror/language@6.11.3': 307 resolution: {integrity: sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==} 308 309 + '@codemirror/lint@6.9.1': 310 + resolution: {integrity: sha512-te7To1EQHePBQQzasDKWmK2xKINIXpk+xAiSYr9ZN+VB4KaT+/Hi2PEkeErTk5BV3PTz1TLyQL4MtJfPkKZ9sw==} 311 312 '@codemirror/search@6.5.11': 313 resolution: {integrity: sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==} ··· 315 '@codemirror/state@6.5.2': 316 resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==} 317 318 + '@codemirror/view@6.38.6': 319 + resolution: {integrity: sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==} 320 321 '@esbuild/aix-ppc64@0.23.1': 322 resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} ··· 324 cpu: [ppc64] 325 os: [aix] 326 327 + '@esbuild/aix-ppc64@0.25.11': 328 + resolution: {integrity: sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==} 329 engines: {node: '>=18'} 330 cpu: [ppc64] 331 os: [aix] ··· 336 cpu: [arm64] 337 os: [android] 338 339 + '@esbuild/android-arm64@0.25.11': 340 + resolution: {integrity: sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==} 341 engines: {node: '>=18'} 342 cpu: [arm64] 343 os: [android] ··· 348 cpu: [arm] 349 os: [android] 350 351 + '@esbuild/android-arm@0.25.11': 352 + resolution: {integrity: sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==} 353 engines: {node: '>=18'} 354 cpu: [arm] 355 os: [android] ··· 360 cpu: [x64] 361 os: [android] 362 363 + '@esbuild/android-x64@0.25.11': 364 + resolution: {integrity: sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==} 365 engines: {node: '>=18'} 366 cpu: [x64] 367 os: [android] ··· 372 cpu: [arm64] 373 os: [darwin] 374 375 + '@esbuild/darwin-arm64@0.25.11': 376 + resolution: {integrity: sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==} 377 engines: {node: '>=18'} 378 cpu: [arm64] 379 os: [darwin] ··· 384 cpu: [x64] 385 os: [darwin] 386 387 + '@esbuild/darwin-x64@0.25.11': 388 + resolution: {integrity: sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==} 389 engines: {node: '>=18'} 390 cpu: [x64] 391 os: [darwin] ··· 396 cpu: [arm64] 397 os: [freebsd] 398 399 + '@esbuild/freebsd-arm64@0.25.11': 400 + resolution: {integrity: sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==} 401 engines: {node: '>=18'} 402 cpu: [arm64] 403 os: [freebsd] ··· 408 cpu: [x64] 409 os: [freebsd] 410 411 + '@esbuild/freebsd-x64@0.25.11': 412 + resolution: {integrity: sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==} 413 engines: {node: '>=18'} 414 cpu: [x64] 415 os: [freebsd] ··· 420 cpu: [arm64] 421 os: [linux] 422 423 + '@esbuild/linux-arm64@0.25.11': 424 + resolution: {integrity: sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==} 425 engines: {node: '>=18'} 426 cpu: [arm64] 427 os: [linux] ··· 432 cpu: [arm] 433 os: [linux] 434 435 + '@esbuild/linux-arm@0.25.11': 436 + resolution: {integrity: sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==} 437 engines: {node: '>=18'} 438 cpu: [arm] 439 os: [linux] ··· 444 cpu: [ia32] 445 os: [linux] 446 447 + '@esbuild/linux-ia32@0.25.11': 448 + resolution: {integrity: sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==} 449 engines: {node: '>=18'} 450 cpu: [ia32] 451 os: [linux] ··· 456 cpu: [loong64] 457 os: [linux] 458 459 + '@esbuild/linux-loong64@0.25.11': 460 + resolution: {integrity: sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==} 461 engines: {node: '>=18'} 462 cpu: [loong64] 463 os: [linux] ··· 468 cpu: [mips64el] 469 os: [linux] 470 471 + '@esbuild/linux-mips64el@0.25.11': 472 + resolution: {integrity: sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==} 473 engines: {node: '>=18'} 474 cpu: [mips64el] 475 os: [linux] ··· 480 cpu: [ppc64] 481 os: [linux] 482 483 + '@esbuild/linux-ppc64@0.25.11': 484 + resolution: {integrity: sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==} 485 engines: {node: '>=18'} 486 cpu: [ppc64] 487 os: [linux] ··· 492 cpu: [riscv64] 493 os: [linux] 494 495 + '@esbuild/linux-riscv64@0.25.11': 496 + resolution: {integrity: sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==} 497 engines: {node: '>=18'} 498 cpu: [riscv64] 499 os: [linux] ··· 504 cpu: [s390x] 505 os: [linux] 506 507 + '@esbuild/linux-s390x@0.25.11': 508 + resolution: {integrity: sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==} 509 engines: {node: '>=18'} 510 cpu: [s390x] 511 os: [linux] ··· 516 cpu: [x64] 517 os: [linux] 518 519 + '@esbuild/linux-x64@0.25.11': 520 + resolution: {integrity: sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==} 521 engines: {node: '>=18'} 522 cpu: [x64] 523 os: [linux] 524 525 + '@esbuild/netbsd-arm64@0.25.11': 526 + resolution: {integrity: sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==} 527 engines: {node: '>=18'} 528 cpu: [arm64] 529 os: [netbsd] ··· 534 cpu: [x64] 535 os: [netbsd] 536 537 + '@esbuild/netbsd-x64@0.25.11': 538 + resolution: {integrity: sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==} 539 engines: {node: '>=18'} 540 cpu: [x64] 541 os: [netbsd] ··· 546 cpu: [arm64] 547 os: [openbsd] 548 549 + '@esbuild/openbsd-arm64@0.25.11': 550 + resolution: {integrity: sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==} 551 engines: {node: '>=18'} 552 cpu: [arm64] 553 os: [openbsd] ··· 558 cpu: [x64] 559 os: [openbsd] 560 561 + '@esbuild/openbsd-x64@0.25.11': 562 + resolution: {integrity: sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==} 563 engines: {node: '>=18'} 564 cpu: [x64] 565 os: [openbsd] 566 567 + '@esbuild/openharmony-arm64@0.25.11': 568 + resolution: {integrity: sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==} 569 engines: {node: '>=18'} 570 cpu: [arm64] 571 os: [openharmony] ··· 576 cpu: [x64] 577 os: [sunos] 578 579 + '@esbuild/sunos-x64@0.25.11': 580 + resolution: {integrity: sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==} 581 engines: {node: '>=18'} 582 cpu: [x64] 583 os: [sunos] ··· 588 cpu: [arm64] 589 os: [win32] 590 591 + '@esbuild/win32-arm64@0.25.11': 592 + resolution: {integrity: sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==} 593 engines: {node: '>=18'} 594 cpu: [arm64] 595 os: [win32] ··· 600 cpu: [ia32] 601 os: [win32] 602 603 + '@esbuild/win32-ia32@0.25.11': 604 + resolution: {integrity: sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==} 605 engines: {node: '>=18'} 606 cpu: [ia32] 607 os: [win32] ··· 612 cpu: [x64] 613 os: [win32] 614 615 + '@esbuild/win32-x64@0.25.11': 616 + resolution: {integrity: sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==} 617 engines: {node: '>=18'} 618 cpu: [x64] 619 os: [win32] ··· 634 '@codemirror/view': ^6.0.0 635 '@lezer/highlight': ^1.0.0 636 637 + '@iconify-json/lucide@1.2.71': 638 + resolution: {integrity: sha512-KL+3JHW+wN8QqT3CN+7e1SzTe+gIunFBuUICtVmdCmdVRx+MdGNkX4xJhXoYHfhYO2azrEhoGPG+It9k30aZkw==} 639 640 '@iconify/tailwind4@1.0.6': 641 resolution: {integrity: sha512-43ZXe+bC7CuE2LCgROdqbQeFYJi/J7L/k1UpSy8KDQlWVsWxPzLSWbWhlJx4uRYLOh1NRyw02YlDOgzBOFNd+A==} ··· 648 '@iconify/utils@2.3.0': 649 resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==} 650 651 '@jridgewell/gen-mapping@0.3.13': 652 resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} 653 ··· 667 '@jsr/mary__exif-rm@0.2.2': 668 resolution: {integrity: sha512-+ZpLaC+1CyqWhH608Sqd6/yTG0pOlokn2tCXha7s1SMQ+GLKo4Nn/PskTeeP9Pt+6gNYSu6ednoSlRvXb2ZGxg==, tarball: https://npm.jsr.io/~/11/@jsr/mary__exif-rm/0.2.2.tgz} 669 670 + '@lezer/common@1.3.0': 671 + resolution: {integrity: sha512-L9X8uHCYU310o99L3/MpJKYxPzXPOS7S0NmBaM7UO/x2Kb2WbmMLSkfvdr1KxRIFYOpbY0Jhn7CfLSUDzL8arQ==} 672 673 + '@lezer/highlight@1.2.3': 674 + resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} 675 676 '@lezer/json@1.0.3': 677 resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==} ··· 682 '@marijn/find-cluster-break@1.0.2': 683 resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} 684 685 + '@noble/secp256k1@3.0.0': 686 + resolution: {integrity: sha512-NJBaR352KyIvj3t6sgT/+7xrNyF9Xk9QlLSIqUGVUYlsnDTAUqY8LOmwpcgEx4AMJXRITQ5XEVHD+mMaPfr3mg==} 687 688 + '@rollup/rollup-android-arm-eabi@4.52.5': 689 + resolution: {integrity: sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==} 690 cpu: [arm] 691 os: [android] 692 693 + '@rollup/rollup-android-arm64@4.52.5': 694 + resolution: {integrity: sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==} 695 cpu: [arm64] 696 os: [android] 697 698 + '@rollup/rollup-darwin-arm64@4.52.5': 699 + resolution: {integrity: sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==} 700 cpu: [arm64] 701 os: [darwin] 702 703 + '@rollup/rollup-darwin-x64@4.52.5': 704 + resolution: {integrity: sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==} 705 cpu: [x64] 706 os: [darwin] 707 708 + '@rollup/rollup-freebsd-arm64@4.52.5': 709 + resolution: {integrity: sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==} 710 cpu: [arm64] 711 os: [freebsd] 712 713 + '@rollup/rollup-freebsd-x64@4.52.5': 714 + resolution: {integrity: sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==} 715 cpu: [x64] 716 os: [freebsd] 717 718 + '@rollup/rollup-linux-arm-gnueabihf@4.52.5': 719 + resolution: {integrity: sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==} 720 cpu: [arm] 721 os: [linux] 722 723 + '@rollup/rollup-linux-arm-musleabihf@4.52.5': 724 + resolution: {integrity: sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==} 725 cpu: [arm] 726 os: [linux] 727 728 + '@rollup/rollup-linux-arm64-gnu@4.52.5': 729 + resolution: {integrity: sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==} 730 cpu: [arm64] 731 os: [linux] 732 733 + '@rollup/rollup-linux-arm64-musl@4.52.5': 734 + resolution: {integrity: sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==} 735 cpu: [arm64] 736 os: [linux] 737 738 + '@rollup/rollup-linux-loong64-gnu@4.52.5': 739 + resolution: {integrity: sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==} 740 cpu: [loong64] 741 os: [linux] 742 743 + '@rollup/rollup-linux-ppc64-gnu@4.52.5': 744 + resolution: {integrity: sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==} 745 cpu: [ppc64] 746 os: [linux] 747 748 + '@rollup/rollup-linux-riscv64-gnu@4.52.5': 749 + resolution: {integrity: sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==} 750 cpu: [riscv64] 751 os: [linux] 752 753 + '@rollup/rollup-linux-riscv64-musl@4.52.5': 754 + resolution: {integrity: sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==} 755 cpu: [riscv64] 756 os: [linux] 757 758 + '@rollup/rollup-linux-s390x-gnu@4.52.5': 759 + resolution: {integrity: sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==} 760 cpu: [s390x] 761 os: [linux] 762 763 + '@rollup/rollup-linux-x64-gnu@4.52.5': 764 + resolution: {integrity: sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==} 765 cpu: [x64] 766 os: [linux] 767 768 + '@rollup/rollup-linux-x64-musl@4.52.5': 769 + resolution: {integrity: sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==} 770 cpu: [x64] 771 os: [linux] 772 773 + '@rollup/rollup-openharmony-arm64@4.52.5': 774 + resolution: {integrity: sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==} 775 cpu: [arm64] 776 os: [openharmony] 777 778 + '@rollup/rollup-win32-arm64-msvc@4.52.5': 779 + resolution: {integrity: sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==} 780 cpu: [arm64] 781 os: [win32] 782 783 + '@rollup/rollup-win32-ia32-msvc@4.52.5': 784 + resolution: {integrity: sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==} 785 cpu: [ia32] 786 os: [win32] 787 788 + '@rollup/rollup-win32-x64-gnu@4.52.5': 789 + resolution: {integrity: sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==} 790 + cpu: [x64] 791 + os: [win32] 792 + 793 + '@rollup/rollup-win32-x64-msvc@4.52.5': 794 + resolution: {integrity: sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==} 795 cpu: [x64] 796 os: [win32] 797 ··· 808 peerDependencies: 809 solid-js: ^1.8.6 810 811 + '@standard-schema/spec@1.0.0': 812 + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} 813 814 + '@tailwindcss/node@4.1.16': 815 + resolution: {integrity: sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw==} 816 + 817 + '@tailwindcss/oxide-android-arm64@4.1.16': 818 + resolution: {integrity: sha512-8+ctzkjHgwDJ5caq9IqRSgsP70xhdhJvm+oueS/yhD5ixLhqTw9fSL1OurzMUhBwE5zK26FXLCz2f/RtkISqHA==} 819 engines: {node: '>= 10'} 820 cpu: [arm64] 821 os: [android] 822 823 + '@tailwindcss/oxide-darwin-arm64@4.1.16': 824 + resolution: {integrity: sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA==} 825 engines: {node: '>= 10'} 826 cpu: [arm64] 827 os: [darwin] 828 829 + '@tailwindcss/oxide-darwin-x64@4.1.16': 830 + resolution: {integrity: sha512-vjrl/1Ub9+JwU6BP0emgipGjowzYZMjbWCDqwA2Z4vCa+HBSpP4v6U2ddejcHsolsYxwL5r4bPNoamlV0xDdLg==} 831 engines: {node: '>= 10'} 832 cpu: [x64] 833 os: [darwin] 834 835 + '@tailwindcss/oxide-freebsd-x64@4.1.16': 836 + resolution: {integrity: sha512-TSMpPYpQLm+aR1wW5rKuUuEruc/oOX3C7H0BTnPDn7W/eMw8W+MRMpiypKMkXZfwH8wqPIRKppuZoedTtNj2tg==} 837 engines: {node: '>= 10'} 838 cpu: [x64] 839 os: [freebsd] 840 841 + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.16': 842 + resolution: {integrity: sha512-p0GGfRg/w0sdsFKBjMYvvKIiKy/LNWLWgV/plR4lUgrsxFAoQBFrXkZ4C0w8IOXfslB9vHK/JGASWD2IefIpvw==} 843 engines: {node: '>= 10'} 844 cpu: [arm] 845 os: [linux] 846 847 + '@tailwindcss/oxide-linux-arm64-gnu@4.1.16': 848 + resolution: {integrity: sha512-DoixyMmTNO19rwRPdqviTrG1rYzpxgyYJl8RgQvdAQUzxC1ToLRqtNJpU/ATURSKgIg6uerPw2feW0aS8SNr/w==} 849 engines: {node: '>= 10'} 850 cpu: [arm64] 851 os: [linux] 852 853 + '@tailwindcss/oxide-linux-arm64-musl@4.1.16': 854 + resolution: {integrity: sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==} 855 engines: {node: '>= 10'} 856 cpu: [arm64] 857 os: [linux] 858 859 + '@tailwindcss/oxide-linux-x64-gnu@4.1.16': 860 + resolution: {integrity: sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==} 861 engines: {node: '>= 10'} 862 cpu: [x64] 863 os: [linux] 864 865 + '@tailwindcss/oxide-linux-x64-musl@4.1.16': 866 + resolution: {integrity: sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==} 867 engines: {node: '>= 10'} 868 cpu: [x64] 869 os: [linux] 870 871 + '@tailwindcss/oxide-wasm32-wasi@4.1.16': 872 + resolution: {integrity: sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==} 873 engines: {node: '>=14.0.0'} 874 cpu: [wasm32] 875 bundledDependencies: ··· 880 - '@emnapi/wasi-threads' 881 - tslib 882 883 + '@tailwindcss/oxide-win32-arm64-msvc@4.1.16': 884 + resolution: {integrity: sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A==} 885 engines: {node: '>= 10'} 886 cpu: [arm64] 887 os: [win32] 888 889 + '@tailwindcss/oxide-win32-x64-msvc@4.1.16': 890 + resolution: {integrity: sha512-m5dDFJUEejbFqP+UXVstd4W/wnxA4F61q8SoL+mqTypId2T2ZpuxosNSgowiCnLp2+Z+rivdU0AqpfgiD7yCBg==} 891 engines: {node: '>= 10'} 892 cpu: [x64] 893 os: [win32] 894 895 + '@tailwindcss/oxide@4.1.16': 896 + resolution: {integrity: sha512-2OSv52FRuhdlgyOQqgtQHuCgXnS8nFSYRp2tJ+4WZXKgTxqPy7SMSls8c3mPT5pkZ17SBToGM5LHEJBO7miEdg==} 897 engines: {node: '>= 10'} 898 899 + '@tailwindcss/vite@4.1.16': 900 + resolution: {integrity: sha512-bbguNBcDxsRmi9nnlWJxhfDWamY3lmcyACHcdO1crxfzuLpOhHLLtEIN/nCbbAtj5rchUgQD17QVAKi1f7IsKg==} 901 peerDependencies: 902 vite: ^5.2.0 || ^6 || ^7 903 ··· 924 engines: {node: '>=0.4.0'} 925 hasBin: true 926 927 + babel-plugin-jsx-dom-expressions@0.40.3: 928 + resolution: {integrity: sha512-5HOwwt0BYiv/zxl7j8Pf2bGL6rDXfV6nUhLs8ygBX+EFJXzBPHM/euj9j/6deMZ6wa52Wb2PBaAV5U/jKwIY1w==} 929 peerDependencies: 930 '@babel/core': ^7.20.12 931 932 + babel-preset-solid@1.9.10: 933 + resolution: {integrity: sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ==} 934 peerDependencies: 935 '@babel/core': ^7.0.0 936 + solid-js: ^1.9.10 937 peerDependenciesMeta: 938 solid-js: 939 optional: true 940 941 + baseline-browser-mapping@2.8.21: 942 + resolution: {integrity: sha512-JU0h5APyQNsHOlAM7HnQnPToSDQoEBZqzu/YBlqDnEeymPnZDREeXJA3KBMQee+dKteAxZ2AtvQEvVYdZf241Q==} 943 hasBin: true 944 945 + browserslist@4.27.0: 946 + resolution: {integrity: sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==} 947 engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} 948 hasBin: true 949 950 + caniuse-lite@1.0.30001751: 951 + resolution: {integrity: sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==} 952 953 codemirror@6.0.2: 954 resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} ··· 977 supports-color: 978 optional: true 979 980 + detect-libc@2.1.2: 981 + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} 982 engines: {node: '>=8'} 983 984 + electron-to-chromium@1.5.243: 985 + resolution: {integrity: sha512-ZCphxFW3Q1TVhcgS9blfut1PX8lusVi2SvXQgmEEnK4TCmE1JhH2JkjJN+DNt0pJJwfBri5AROBnz2b/C+YU9g==} 986 987 enhanced-resolve@5.18.3: 988 resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} ··· 997 engines: {node: '>=18'} 998 hasBin: true 999 1000 + esbuild@0.25.11: 1001 + resolution: {integrity: sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==} 1002 engines: {node: '>=18'} 1003 hasBin: true 1004 ··· 1030 resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} 1031 engines: {node: '>=6.9.0'} 1032 1033 + get-tsconfig@4.13.0: 1034 + resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} 1035 1036 globals@15.15.0: 1037 resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} ··· 1040 graceful-fs@4.2.11: 1041 resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 1042 1043 html-entities@2.3.3: 1044 resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} 1045 ··· 1047 resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} 1048 engines: {node: '>=12.13'} 1049 1050 + jiti@2.6.1: 1051 + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} 1052 hasBin: true 1053 1054 js-tokens@4.0.0: ··· 1067 kolorist@1.8.0: 1068 resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} 1069 1070 + lightningcss-android-arm64@1.30.2: 1071 + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} 1072 + engines: {node: '>= 12.0.0'} 1073 + cpu: [arm64] 1074 + os: [android] 1075 + 1076 + lightningcss-darwin-arm64@1.30.2: 1077 + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} 1078 engines: {node: '>= 12.0.0'} 1079 cpu: [arm64] 1080 os: [darwin] 1081 1082 + lightningcss-darwin-x64@1.30.2: 1083 + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} 1084 engines: {node: '>= 12.0.0'} 1085 cpu: [x64] 1086 os: [darwin] 1087 1088 + lightningcss-freebsd-x64@1.30.2: 1089 + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} 1090 engines: {node: '>= 12.0.0'} 1091 cpu: [x64] 1092 os: [freebsd] 1093 1094 + lightningcss-linux-arm-gnueabihf@1.30.2: 1095 + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} 1096 engines: {node: '>= 12.0.0'} 1097 cpu: [arm] 1098 os: [linux] 1099 1100 + lightningcss-linux-arm64-gnu@1.30.2: 1101 + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} 1102 engines: {node: '>= 12.0.0'} 1103 cpu: [arm64] 1104 os: [linux] 1105 1106 + lightningcss-linux-arm64-musl@1.30.2: 1107 + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} 1108 engines: {node: '>= 12.0.0'} 1109 cpu: [arm64] 1110 os: [linux] 1111 1112 + lightningcss-linux-x64-gnu@1.30.2: 1113 + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} 1114 engines: {node: '>= 12.0.0'} 1115 cpu: [x64] 1116 os: [linux] 1117 1118 + lightningcss-linux-x64-musl@1.30.2: 1119 + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} 1120 engines: {node: '>= 12.0.0'} 1121 cpu: [x64] 1122 os: [linux] 1123 1124 + lightningcss-win32-arm64-msvc@1.30.2: 1125 + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} 1126 engines: {node: '>= 12.0.0'} 1127 cpu: [arm64] 1128 os: [win32] 1129 1130 + lightningcss-win32-x64-msvc@1.30.2: 1131 + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} 1132 engines: {node: '>= 12.0.0'} 1133 cpu: [x64] 1134 os: [win32] 1135 1136 + lightningcss@1.30.2: 1137 + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} 1138 engines: {node: '>= 12.0.0'} 1139 1140 local-pkg@1.1.2: ··· 1144 lru-cache@5.1.1: 1145 resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} 1146 1147 + magic-string@0.30.21: 1148 + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} 1149 1150 merge-anything@5.1.7: 1151 resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==} 1152 engines: {node: '>=12.13'} 1153 1154 mlly@1.8.0: 1155 resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} 1156 ··· 1166 engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 1167 hasBin: true 1168 1169 + nanoid@5.1.6: 1170 + resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==} 1171 engines: {node: ^18 || >=20} 1172 hasBin: true 1173 1174 + node-releases@2.0.27: 1175 + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} 1176 1177 + package-manager-detector@1.5.0: 1178 + resolution: {integrity: sha512-uBj69dVlYe/+wxj8JOpr97XfsxH/eumMt6HqjNTmJDf/6NO9s+0uxeOneIz3AsPt2m6y9PqzDzd3ATcU17MNfw==} 1179 1180 parse5@7.3.0: 1181 resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} ··· 1200 resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} 1201 engines: {node: ^10 || ^12 || >=14} 1202 1203 + prettier-plugin-organize-imports@4.3.0: 1204 + resolution: {integrity: sha512-FxFz0qFhyBsGdIsb697f/EkvHzi5SZOhWAjxcx2dLt+Q532bAlhswcXGYB1yzjZ69kW8UoadFBw7TyNwlq96Iw==} 1205 peerDependencies: 1206 prettier: '>=2.0' 1207 typescript: '>=2.9' ··· 1210 vue-tsc: 1211 optional: true 1212 1213 + prettier-plugin-tailwindcss@0.7.1: 1214 + resolution: {integrity: sha512-Bzv1LZcuiR1Sk02iJTS1QzlFNp/o5l2p3xkopwOrbPmtMeh3fK9rVW5M3neBQzHq+kGKj/4LGQMTNcTH4NGPtQ==} 1215 + engines: {node: '>=20.19'} 1216 peerDependencies: 1217 '@ianvs/prettier-plugin-sort-imports': '*' 1218 '@prettier/plugin-hermes': '*' ··· 1224 prettier: ^3.0 1225 prettier-plugin-astro: '*' 1226 prettier-plugin-css-order: '*' 1227 prettier-plugin-jsdoc: '*' 1228 prettier-plugin-marko: '*' 1229 prettier-plugin-multiline-arrays: '*' 1230 prettier-plugin-organize-attributes: '*' 1231 prettier-plugin-organize-imports: '*' 1232 prettier-plugin-sort-imports: '*' 1233 prettier-plugin-svelte: '*' 1234 peerDependenciesMeta: 1235 '@ianvs/prettier-plugin-sort-imports': ··· 1250 optional: true 1251 prettier-plugin-css-order: 1252 optional: true 1253 prettier-plugin-jsdoc: 1254 optional: true 1255 prettier-plugin-marko: ··· 1262 optional: true 1263 prettier-plugin-sort-imports: 1264 optional: true 1265 prettier-plugin-svelte: 1266 optional: true 1267 ··· 1276 resolve-pkg-maps@1.0.0: 1277 resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} 1278 1279 + rollup@4.52.5: 1280 + resolution: {integrity: sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==} 1281 engines: {node: '>=18.0.0', npm: '>=8.0.0'} 1282 hasBin: true 1283 ··· 1295 resolution: {integrity: sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==} 1296 engines: {node: '>=10'} 1297 1298 + solid-js@1.9.10: 1299 + resolution: {integrity: sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew==} 1300 1301 solid-refresh@0.6.3: 1302 resolution: {integrity: sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==} ··· 1307 resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 1308 engines: {node: '>=0.10.0'} 1309 1310 + style-mod@4.1.3: 1311 + resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} 1312 1313 + tailwindcss@4.1.16: 1314 + resolution: {integrity: sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==} 1315 1316 + tapable@2.3.0: 1317 + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} 1318 engines: {node: '>=6'} 1319 1320 tinyexec@1.0.1: 1321 resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} 1322 ··· 1329 engines: {node: '>=18.0.0'} 1330 hasBin: true 1331 1332 + typescript@5.9.3: 1333 + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} 1334 engines: {node: '>=14.17'} 1335 hasBin: true 1336 ··· 1340 undici-types@6.20.0: 1341 resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} 1342 1343 + update-browserslist-db@1.1.4: 1344 + resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} 1345 hasBin: true 1346 peerDependencies: 1347 browserslist: '>= 4.21.0' 1348 1349 + vite-plugin-solid@2.11.10: 1350 + resolution: {integrity: sha512-Yr1dQybmtDtDAHkii6hXuc1oVH9CPcS/Zb2jN/P36qqcrkNnVPsMTzQ06jyzFPFjj3U1IYKMVt/9ZqcwGCEbjw==} 1351 peerDependencies: 1352 '@testing-library/jest-dom': ^5.16.6 || ^5.17.0 || ^6.* 1353 solid-js: ^1.7.2 ··· 1356 '@testing-library/jest-dom': 1357 optional: true 1358 1359 + vite@7.1.12: 1360 + resolution: {integrity: sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==} 1361 engines: {node: ^20.19.0 || >=22.12.0} 1362 hasBin: true 1363 peerDependencies: ··· 1410 yallist@3.1.1: 1411 resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} 1412 1413 yocto-queue@1.2.1: 1414 resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} 1415 engines: {node: '>=12.20'} ··· 1418 1419 '@antfu/install-pkg@1.1.0': 1420 dependencies: 1421 + package-manager-detector: 1.5.0 1422 tinyexec: 1.0.1 1423 1424 '@antfu/utils@8.1.1': {} 1425 1426 + '@atcute/atproto@3.1.8': 1427 dependencies: 1428 + '@atcute/lexicons': 1.2.2 1429 1430 + '@atcute/bluesky@3.2.9': 1431 dependencies: 1432 + '@atcute/atproto': 3.1.8 1433 + '@atcute/lexicons': 1.2.2 1434 1435 + '@atcute/car@3.1.3': 1436 dependencies: 1437 + '@atcute/cbor': 2.2.7 1438 + '@atcute/cid': 2.2.6 1439 + '@atcute/uint8array': 1.0.5 1440 + '@atcute/varint': 1.0.3 1441 yocto-queue: 1.2.1 1442 1443 + '@atcute/car@5.0.0': 1444 dependencies: 1445 + '@atcute/cbor': 2.2.7 1446 + '@atcute/cid': 2.2.6 1447 + '@atcute/uint8array': 1.0.5 1448 + '@atcute/varint': 1.0.3 1449 1450 + '@atcute/cbor@2.2.7': 1451 dependencies: 1452 + '@atcute/cid': 2.2.6 1453 + '@atcute/multibase': 1.1.6 1454 + '@atcute/uint8array': 1.0.5 1455 1456 + '@atcute/cid@2.2.6': 1457 dependencies: 1458 + '@atcute/multibase': 1.1.6 1459 + '@atcute/uint8array': 1.0.5 1460 1461 + '@atcute/client@4.0.5': 1462 dependencies: 1463 + '@atcute/identity': 1.1.1 1464 + '@atcute/lexicons': 1.2.2 1465 1466 + '@atcute/crypto@2.2.6': 1467 dependencies: 1468 + '@atcute/multibase': 1.1.6 1469 + '@atcute/uint8array': 1.0.5 1470 + '@noble/secp256k1': 3.0.0 1471 + 1472 + '@atcute/did-plc@0.1.7': 1473 + dependencies: 1474 + '@atcute/cbor': 2.2.7 1475 + '@atcute/cid': 2.2.6 1476 + '@atcute/crypto': 2.2.6 1477 + '@atcute/identity': 1.1.1 1478 + '@atcute/lexicons': 1.2.2 1479 + '@atcute/multibase': 1.1.6 1480 + '@atcute/uint8array': 1.0.5 1481 '@badrap/valita': 0.4.6 1482 1483 + '@atcute/identity-resolver@1.1.4(@atcute/identity@1.1.1)': 1484 dependencies: 1485 + '@atcute/identity': 1.1.1 1486 + '@atcute/lexicons': 1.2.2 1487 + '@atcute/util-fetch': 1.0.3 1488 '@badrap/valita': 0.4.6 1489 1490 + '@atcute/identity@1.1.1': 1491 dependencies: 1492 + '@atcute/lexicons': 1.2.2 1493 '@badrap/valita': 0.4.6 1494 1495 + '@atcute/leaflet@1.0.11': 1496 dependencies: 1497 + '@atcute/atproto': 3.1.8 1498 + '@atcute/lexicons': 1.2.2 1499 + 1500 + '@atcute/lexicon-doc@1.1.4': 1501 + dependencies: 1502 + '@badrap/valita': 0.4.6 1503 1504 + '@atcute/lexicon-resolver@0.1.3(@atcute/identity-resolver@1.1.4(@atcute/identity@1.1.1))(@atcute/identity@1.1.1)': 1505 dependencies: 1506 + '@atcute/car': 5.0.0 1507 + '@atcute/cbor': 2.2.7 1508 + '@atcute/cid': 2.2.6 1509 + '@atcute/crypto': 2.2.6 1510 + '@atcute/identity': 1.1.1 1511 + '@atcute/identity-resolver': 1.1.4(@atcute/identity@1.1.1) 1512 + '@atcute/lexicon-doc': 1.1.4 1513 + '@atcute/lexicons': 1.2.2 1514 + '@atcute/repo': 0.1.0 1515 + '@atcute/uint8array': 1.0.5 1516 + '@atcute/util-fetch': 1.0.3 1517 '@badrap/valita': 0.4.6 1518 1519 + '@atcute/lexicons@1.2.2': 1520 dependencies: 1521 + '@standard-schema/spec': 1.0.0 1522 esm-env: 1.2.2 1523 1524 + '@atcute/mst@0.1.0': 1525 dependencies: 1526 + '@atcute/cbor': 2.2.7 1527 + '@atcute/cid': 2.2.6 1528 + '@atcute/uint8array': 1.0.5 1529 1530 + '@atcute/multibase@1.1.6': 1531 dependencies: 1532 + '@atcute/uint8array': 1.0.5 1533 1534 + '@atcute/oauth-browser-client@2.0.1': 1535 dependencies: 1536 + '@atcute/client': 4.0.5 1537 + '@atcute/identity': 1.1.1 1538 + '@atcute/identity-resolver': 1.1.4(@atcute/identity@1.1.1) 1539 + '@atcute/lexicons': 1.2.2 1540 + '@atcute/multibase': 1.1.6 1541 + '@atcute/uint8array': 1.0.5 1542 + nanoid: 5.1.6 1543 1544 + '@atcute/repo@0.1.0': 1545 + dependencies: 1546 + '@atcute/car': 5.0.0 1547 + '@atcute/cbor': 2.2.7 1548 + '@atcute/cid': 2.2.6 1549 + '@atcute/crypto': 2.2.6 1550 + '@atcute/lexicons': 1.2.2 1551 + '@atcute/mst': 0.1.0 1552 + '@atcute/uint8array': 1.0.5 1553 1554 + '@atcute/tangled@1.0.10': 1555 + dependencies: 1556 + '@atcute/atproto': 3.1.8 1557 + '@atcute/lexicons': 1.2.2 1558 1559 + '@atcute/tid@1.0.3': {} 1560 + 1561 + '@atcute/uint8array@1.0.5': {} 1562 + 1563 + '@atcute/util-fetch@1.0.3': 1564 dependencies: 1565 '@badrap/valita': 0.4.6 1566 1567 + '@atcute/varint@1.0.3': {} 1568 1569 '@babel/code-frame@7.27.1': 1570 dependencies: 1571 + '@babel/helper-validator-identifier': 7.28.5 1572 js-tokens: 4.0.0 1573 picocolors: 1.1.1 1574 1575 + '@babel/compat-data@7.28.5': {} 1576 1577 + '@babel/core@7.28.5': 1578 dependencies: 1579 '@babel/code-frame': 7.27.1 1580 + '@babel/generator': 7.28.5 1581 '@babel/helper-compilation-targets': 7.27.2 1582 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) 1583 '@babel/helpers': 7.28.4 1584 + '@babel/parser': 7.28.5 1585 '@babel/template': 7.27.2 1586 + '@babel/traverse': 7.28.5 1587 + '@babel/types': 7.28.5 1588 '@jridgewell/remapping': 2.3.5 1589 convert-source-map: 2.0.0 1590 debug: 4.4.3 ··· 1594 transitivePeerDependencies: 1595 - supports-color 1596 1597 + '@babel/generator@7.28.5': 1598 dependencies: 1599 + '@babel/parser': 7.28.5 1600 + '@babel/types': 7.28.5 1601 '@jridgewell/gen-mapping': 0.3.13 1602 '@jridgewell/trace-mapping': 0.3.31 1603 jsesc: 3.1.0 1604 1605 '@babel/helper-compilation-targets@7.27.2': 1606 dependencies: 1607 + '@babel/compat-data': 7.28.5 1608 '@babel/helper-validator-option': 7.27.1 1609 + browserslist: 4.27.0 1610 lru-cache: 5.1.1 1611 semver: 6.3.1 1612 ··· 1614 1615 '@babel/helper-module-imports@7.18.6': 1616 dependencies: 1617 + '@babel/types': 7.28.5 1618 1619 '@babel/helper-module-imports@7.27.1': 1620 dependencies: 1621 + '@babel/traverse': 7.28.5 1622 + '@babel/types': 7.28.5 1623 transitivePeerDependencies: 1624 - supports-color 1625 1626 + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)': 1627 dependencies: 1628 + '@babel/core': 7.28.5 1629 '@babel/helper-module-imports': 7.27.1 1630 + '@babel/helper-validator-identifier': 7.28.5 1631 + '@babel/traverse': 7.28.5 1632 transitivePeerDependencies: 1633 - supports-color 1634 ··· 1636 1637 '@babel/helper-string-parser@7.27.1': {} 1638 1639 + '@babel/helper-validator-identifier@7.28.5': {} 1640 1641 '@babel/helper-validator-option@7.27.1': {} 1642 1643 '@babel/helpers@7.28.4': 1644 dependencies: 1645 '@babel/template': 7.27.2 1646 + '@babel/types': 7.28.5 1647 1648 + '@babel/parser@7.28.5': 1649 dependencies: 1650 + '@babel/types': 7.28.5 1651 1652 + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.5)': 1653 dependencies: 1654 + '@babel/core': 7.28.5 1655 '@babel/helper-plugin-utils': 7.27.1 1656 1657 '@babel/template@7.27.2': 1658 dependencies: 1659 '@babel/code-frame': 7.27.1 1660 + '@babel/parser': 7.28.5 1661 + '@babel/types': 7.28.5 1662 1663 + '@babel/traverse@7.28.5': 1664 dependencies: 1665 '@babel/code-frame': 7.27.1 1666 + '@babel/generator': 7.28.5 1667 '@babel/helper-globals': 7.28.0 1668 + '@babel/parser': 7.28.5 1669 '@babel/template': 7.27.2 1670 + '@babel/types': 7.28.5 1671 debug: 4.4.3 1672 transitivePeerDependencies: 1673 - supports-color 1674 1675 + '@babel/types@7.28.5': 1676 dependencies: 1677 '@babel/helper-string-parser': 7.27.1 1678 + '@babel/helper-validator-identifier': 7.28.5 1679 1680 '@badrap/valita@0.4.6': {} 1681 1682 + '@codemirror/autocomplete@6.19.1': 1683 dependencies: 1684 '@codemirror/language': 6.11.3 1685 '@codemirror/state': 6.5.2 1686 + '@codemirror/view': 6.38.6 1687 + '@lezer/common': 1.3.0 1688 1689 + '@codemirror/commands@6.10.0': 1690 dependencies: 1691 '@codemirror/language': 6.11.3 1692 '@codemirror/state': 6.5.2 1693 + '@codemirror/view': 6.38.6 1694 + '@lezer/common': 1.3.0 1695 1696 '@codemirror/lang-json@6.0.2': 1697 dependencies: ··· 1701 '@codemirror/language@6.11.3': 1702 dependencies: 1703 '@codemirror/state': 6.5.2 1704 + '@codemirror/view': 6.38.6 1705 + '@lezer/common': 1.3.0 1706 + '@lezer/highlight': 1.2.3 1707 '@lezer/lr': 1.4.2 1708 + style-mod: 4.1.3 1709 1710 + '@codemirror/lint@6.9.1': 1711 dependencies: 1712 '@codemirror/state': 6.5.2 1713 + '@codemirror/view': 6.38.6 1714 crelt: 1.0.6 1715 1716 '@codemirror/search@6.5.11': 1717 dependencies: 1718 '@codemirror/state': 6.5.2 1719 + '@codemirror/view': 6.38.6 1720 crelt: 1.0.6 1721 1722 '@codemirror/state@6.5.2': 1723 dependencies: 1724 '@marijn/find-cluster-break': 1.0.2 1725 1726 + '@codemirror/view@6.38.6': 1727 dependencies: 1728 '@codemirror/state': 6.5.2 1729 crelt: 1.0.6 1730 + style-mod: 4.1.3 1731 w3c-keyname: 2.2.8 1732 1733 '@esbuild/aix-ppc64@0.23.1': 1734 optional: true 1735 1736 + '@esbuild/aix-ppc64@0.25.11': 1737 optional: true 1738 1739 '@esbuild/android-arm64@0.23.1': 1740 optional: true 1741 1742 + '@esbuild/android-arm64@0.25.11': 1743 optional: true 1744 1745 '@esbuild/android-arm@0.23.1': 1746 optional: true 1747 1748 + '@esbuild/android-arm@0.25.11': 1749 optional: true 1750 1751 '@esbuild/android-x64@0.23.1': 1752 optional: true 1753 1754 + '@esbuild/android-x64@0.25.11': 1755 optional: true 1756 1757 '@esbuild/darwin-arm64@0.23.1': 1758 optional: true 1759 1760 + '@esbuild/darwin-arm64@0.25.11': 1761 optional: true 1762 1763 '@esbuild/darwin-x64@0.23.1': 1764 optional: true 1765 1766 + '@esbuild/darwin-x64@0.25.11': 1767 optional: true 1768 1769 '@esbuild/freebsd-arm64@0.23.1': 1770 optional: true 1771 1772 + '@esbuild/freebsd-arm64@0.25.11': 1773 optional: true 1774 1775 '@esbuild/freebsd-x64@0.23.1': 1776 optional: true 1777 1778 + '@esbuild/freebsd-x64@0.25.11': 1779 optional: true 1780 1781 '@esbuild/linux-arm64@0.23.1': 1782 optional: true 1783 1784 + '@esbuild/linux-arm64@0.25.11': 1785 optional: true 1786 1787 '@esbuild/linux-arm@0.23.1': 1788 optional: true 1789 1790 + '@esbuild/linux-arm@0.25.11': 1791 optional: true 1792 1793 '@esbuild/linux-ia32@0.23.1': 1794 optional: true 1795 1796 + '@esbuild/linux-ia32@0.25.11': 1797 optional: true 1798 1799 '@esbuild/linux-loong64@0.23.1': 1800 optional: true 1801 1802 + '@esbuild/linux-loong64@0.25.11': 1803 optional: true 1804 1805 '@esbuild/linux-mips64el@0.23.1': 1806 optional: true 1807 1808 + '@esbuild/linux-mips64el@0.25.11': 1809 optional: true 1810 1811 '@esbuild/linux-ppc64@0.23.1': 1812 optional: true 1813 1814 + '@esbuild/linux-ppc64@0.25.11': 1815 optional: true 1816 1817 '@esbuild/linux-riscv64@0.23.1': 1818 optional: true 1819 1820 + '@esbuild/linux-riscv64@0.25.11': 1821 optional: true 1822 1823 '@esbuild/linux-s390x@0.23.1': 1824 optional: true 1825 1826 + '@esbuild/linux-s390x@0.25.11': 1827 optional: true 1828 1829 '@esbuild/linux-x64@0.23.1': 1830 optional: true 1831 1832 + '@esbuild/linux-x64@0.25.11': 1833 optional: true 1834 1835 + '@esbuild/netbsd-arm64@0.25.11': 1836 optional: true 1837 1838 '@esbuild/netbsd-x64@0.23.1': 1839 optional: true 1840 1841 + '@esbuild/netbsd-x64@0.25.11': 1842 optional: true 1843 1844 '@esbuild/openbsd-arm64@0.23.1': 1845 optional: true 1846 1847 + '@esbuild/openbsd-arm64@0.25.11': 1848 optional: true 1849 1850 '@esbuild/openbsd-x64@0.23.1': 1851 optional: true 1852 1853 + '@esbuild/openbsd-x64@0.25.11': 1854 optional: true 1855 1856 + '@esbuild/openharmony-arm64@0.25.11': 1857 optional: true 1858 1859 '@esbuild/sunos-x64@0.23.1': 1860 optional: true 1861 1862 + '@esbuild/sunos-x64@0.25.11': 1863 optional: true 1864 1865 '@esbuild/win32-arm64@0.23.1': 1866 optional: true 1867 1868 + '@esbuild/win32-arm64@0.25.11': 1869 optional: true 1870 1871 '@esbuild/win32-ia32@0.23.1': 1872 optional: true 1873 1874 + '@esbuild/win32-ia32@0.25.11': 1875 optional: true 1876 1877 '@esbuild/win32-x64@0.23.1': 1878 optional: true 1879 1880 + '@esbuild/win32-x64@0.25.11': 1881 optional: true 1882 1883 + '@fsegurai/codemirror-theme-basic-dark@6.2.2(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.6)(@lezer/highlight@1.2.3)': 1884 dependencies: 1885 '@codemirror/language': 6.11.3 1886 '@codemirror/state': 6.5.2 1887 + '@codemirror/view': 6.38.6 1888 + '@lezer/highlight': 1.2.3 1889 1890 + '@fsegurai/codemirror-theme-basic-light@6.2.2(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.6)(@lezer/highlight@1.2.3)': 1891 dependencies: 1892 '@codemirror/language': 6.11.3 1893 '@codemirror/state': 6.5.2 1894 + '@codemirror/view': 6.38.6 1895 + '@lezer/highlight': 1.2.3 1896 1897 + '@iconify-json/lucide@1.2.71': 1898 dependencies: 1899 '@iconify/types': 2.0.0 1900 1901 + '@iconify/tailwind4@1.0.6(tailwindcss@4.1.16)': 1902 dependencies: 1903 '@iconify/types': 2.0.0 1904 '@iconify/utils': 2.3.0 1905 + tailwindcss: 4.1.16 1906 transitivePeerDependencies: 1907 - supports-color 1908 ··· 1921 transitivePeerDependencies: 1922 - supports-color 1923 1924 '@jridgewell/gen-mapping@0.3.13': 1925 dependencies: 1926 '@jridgewell/sourcemap-codec': 1.5.5 ··· 1942 1943 '@jsr/mary__exif-rm@0.2.2': {} 1944 1945 + '@lezer/common@1.3.0': {} 1946 1947 + '@lezer/highlight@1.2.3': 1948 dependencies: 1949 + '@lezer/common': 1.3.0 1950 1951 '@lezer/json@1.0.3': 1952 dependencies: 1953 + '@lezer/common': 1.3.0 1954 + '@lezer/highlight': 1.2.3 1955 '@lezer/lr': 1.4.2 1956 1957 '@lezer/lr@1.4.2': 1958 dependencies: 1959 + '@lezer/common': 1.3.0 1960 1961 '@marijn/find-cluster-break@1.0.2': {} 1962 1963 + '@noble/secp256k1@3.0.0': {} 1964 1965 + '@rollup/rollup-android-arm-eabi@4.52.5': 1966 optional: true 1967 1968 + '@rollup/rollup-android-arm64@4.52.5': 1969 optional: true 1970 1971 + '@rollup/rollup-darwin-arm64@4.52.5': 1972 + optional: true 1973 + 1974 + '@rollup/rollup-darwin-x64@4.52.5': 1975 optional: true 1976 1977 + '@rollup/rollup-freebsd-arm64@4.52.5': 1978 optional: true 1979 1980 + '@rollup/rollup-freebsd-x64@4.52.5': 1981 optional: true 1982 1983 + '@rollup/rollup-linux-arm-gnueabihf@4.52.5': 1984 optional: true 1985 1986 + '@rollup/rollup-linux-arm-musleabihf@4.52.5': 1987 optional: true 1988 1989 + '@rollup/rollup-linux-arm64-gnu@4.52.5': 1990 optional: true 1991 1992 + '@rollup/rollup-linux-arm64-musl@4.52.5': 1993 optional: true 1994 1995 + '@rollup/rollup-linux-loong64-gnu@4.52.5': 1996 optional: true 1997 1998 + '@rollup/rollup-linux-ppc64-gnu@4.52.5': 1999 optional: true 2000 2001 + '@rollup/rollup-linux-riscv64-gnu@4.52.5': 2002 optional: true 2003 2004 + '@rollup/rollup-linux-riscv64-musl@4.52.5': 2005 optional: true 2006 2007 + '@rollup/rollup-linux-s390x-gnu@4.52.5': 2008 optional: true 2009 2010 + '@rollup/rollup-linux-x64-gnu@4.52.5': 2011 optional: true 2012 2013 + '@rollup/rollup-linux-x64-musl@4.52.5': 2014 optional: true 2015 2016 + '@rollup/rollup-openharmony-arm64@4.52.5': 2017 optional: true 2018 2019 + '@rollup/rollup-win32-arm64-msvc@4.52.5': 2020 optional: true 2021 2022 + '@rollup/rollup-win32-ia32-msvc@4.52.5': 2023 optional: true 2024 2025 + '@rollup/rollup-win32-x64-gnu@4.52.5': 2026 optional: true 2027 2028 + '@rollup/rollup-win32-x64-msvc@4.52.5': 2029 optional: true 2030 2031 '@skyware/firehose@0.5.2': 2032 dependencies: 2033 + '@atcute/car': 3.1.3 2034 + '@atcute/cbor': 2.2.7 2035 nanoevents: 9.1.0 2036 2037 + '@solidjs/meta@0.29.4(solid-js@1.9.10)': 2038 dependencies: 2039 + solid-js: 1.9.10 2040 2041 + '@solidjs/router@0.15.3(solid-js@1.9.10)': 2042 dependencies: 2043 + solid-js: 1.9.10 2044 2045 + '@standard-schema/spec@1.0.0': {} 2046 + 2047 + '@tailwindcss/node@4.1.16': 2048 dependencies: 2049 '@jridgewell/remapping': 2.3.5 2050 enhanced-resolve: 5.18.3 2051 + jiti: 2.6.1 2052 + lightningcss: 1.30.2 2053 + magic-string: 0.30.21 2054 source-map-js: 1.2.1 2055 + tailwindcss: 4.1.16 2056 2057 + '@tailwindcss/oxide-android-arm64@4.1.16': 2058 optional: true 2059 2060 + '@tailwindcss/oxide-darwin-arm64@4.1.16': 2061 optional: true 2062 2063 + '@tailwindcss/oxide-darwin-x64@4.1.16': 2064 optional: true 2065 2066 + '@tailwindcss/oxide-freebsd-x64@4.1.16': 2067 optional: true 2068 2069 + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.16': 2070 optional: true 2071 2072 + '@tailwindcss/oxide-linux-arm64-gnu@4.1.16': 2073 optional: true 2074 2075 + '@tailwindcss/oxide-linux-arm64-musl@4.1.16': 2076 optional: true 2077 2078 + '@tailwindcss/oxide-linux-x64-gnu@4.1.16': 2079 optional: true 2080 2081 + '@tailwindcss/oxide-linux-x64-musl@4.1.16': 2082 optional: true 2083 2084 + '@tailwindcss/oxide-wasm32-wasi@4.1.16': 2085 optional: true 2086 2087 + '@tailwindcss/oxide-win32-arm64-msvc@4.1.16': 2088 optional: true 2089 2090 + '@tailwindcss/oxide-win32-x64-msvc@4.1.16': 2091 optional: true 2092 2093 + '@tailwindcss/oxide@4.1.16': 2094 optionalDependencies: 2095 + '@tailwindcss/oxide-android-arm64': 4.1.16 2096 + '@tailwindcss/oxide-darwin-arm64': 4.1.16 2097 + '@tailwindcss/oxide-darwin-x64': 4.1.16 2098 + '@tailwindcss/oxide-freebsd-x64': 4.1.16 2099 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.16 2100 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.16 2101 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.16 2102 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.16 2103 + '@tailwindcss/oxide-linux-x64-musl': 4.1.16 2104 + '@tailwindcss/oxide-wasm32-wasi': 4.1.16 2105 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.16 2106 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.16 2107 2108 + '@tailwindcss/vite@4.1.16(vite@7.1.12(@types/node@22.13.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2))': 2109 dependencies: 2110 + '@tailwindcss/node': 4.1.16 2111 + '@tailwindcss/oxide': 4.1.16 2112 + tailwindcss: 4.1.16 2113 + vite: 7.1.12(@types/node@22.13.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2) 2114 2115 '@types/babel__core@7.20.5': 2116 dependencies: 2117 + '@babel/parser': 7.28.5 2118 + '@babel/types': 7.28.5 2119 '@types/babel__generator': 7.27.0 2120 '@types/babel__template': 7.4.4 2121 '@types/babel__traverse': 7.28.0 2122 2123 '@types/babel__generator@7.27.0': 2124 dependencies: 2125 + '@babel/types': 7.28.5 2126 2127 '@types/babel__template@7.4.4': 2128 dependencies: 2129 + '@babel/parser': 7.28.5 2130 + '@babel/types': 7.28.5 2131 2132 '@types/babel__traverse@7.28.0': 2133 dependencies: 2134 + '@babel/types': 7.28.5 2135 2136 '@types/estree@1.0.8': {} 2137 ··· 2142 2143 acorn@8.15.0: {} 2144 2145 + babel-plugin-jsx-dom-expressions@0.40.3(@babel/core@7.28.5): 2146 dependencies: 2147 + '@babel/core': 7.28.5 2148 '@babel/helper-module-imports': 7.18.6 2149 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) 2150 + '@babel/types': 7.28.5 2151 html-entities: 2.3.3 2152 parse5: 7.3.0 2153 2154 + babel-preset-solid@1.9.10(@babel/core@7.28.5)(solid-js@1.9.10): 2155 dependencies: 2156 + '@babel/core': 7.28.5 2157 + babel-plugin-jsx-dom-expressions: 0.40.3(@babel/core@7.28.5) 2158 optionalDependencies: 2159 + solid-js: 1.9.10 2160 2161 + baseline-browser-mapping@2.8.21: {} 2162 2163 + browserslist@4.27.0: 2164 dependencies: 2165 + baseline-browser-mapping: 2.8.21 2166 + caniuse-lite: 1.0.30001751 2167 + electron-to-chromium: 1.5.243 2168 + node-releases: 2.0.27 2169 + update-browserslist-db: 1.1.4(browserslist@4.27.0) 2170 2171 + caniuse-lite@1.0.30001751: {} 2172 2173 codemirror@6.0.2: 2174 dependencies: 2175 + '@codemirror/autocomplete': 6.19.1 2176 + '@codemirror/commands': 6.10.0 2177 '@codemirror/language': 6.11.3 2178 + '@codemirror/lint': 6.9.1 2179 '@codemirror/search': 6.5.11 2180 '@codemirror/state': 6.5.2 2181 + '@codemirror/view': 6.38.6 2182 2183 confbox@0.1.8: {} 2184 ··· 2194 dependencies: 2195 ms: 2.1.3 2196 2197 + detect-libc@2.1.2: {} 2198 2199 + electron-to-chromium@1.5.243: {} 2200 2201 enhanced-resolve@5.18.3: 2202 dependencies: 2203 graceful-fs: 4.2.11 2204 + tapable: 2.3.0 2205 2206 entities@6.0.1: {} 2207 ··· 2233 '@esbuild/win32-x64': 0.23.1 2234 optional: true 2235 2236 + esbuild@0.25.11: 2237 optionalDependencies: 2238 + '@esbuild/aix-ppc64': 0.25.11 2239 + '@esbuild/android-arm': 0.25.11 2240 + '@esbuild/android-arm64': 0.25.11 2241 + '@esbuild/android-x64': 0.25.11 2242 + '@esbuild/darwin-arm64': 0.25.11 2243 + '@esbuild/darwin-x64': 0.25.11 2244 + '@esbuild/freebsd-arm64': 0.25.11 2245 + '@esbuild/freebsd-x64': 0.25.11 2246 + '@esbuild/linux-arm': 0.25.11 2247 + '@esbuild/linux-arm64': 0.25.11 2248 + '@esbuild/linux-ia32': 0.25.11 2249 + '@esbuild/linux-loong64': 0.25.11 2250 + '@esbuild/linux-mips64el': 0.25.11 2251 + '@esbuild/linux-ppc64': 0.25.11 2252 + '@esbuild/linux-riscv64': 0.25.11 2253 + '@esbuild/linux-s390x': 0.25.11 2254 + '@esbuild/linux-x64': 0.25.11 2255 + '@esbuild/netbsd-arm64': 0.25.11 2256 + '@esbuild/netbsd-x64': 0.25.11 2257 + '@esbuild/openbsd-arm64': 0.25.11 2258 + '@esbuild/openbsd-x64': 0.25.11 2259 + '@esbuild/openharmony-arm64': 0.25.11 2260 + '@esbuild/sunos-x64': 0.25.11 2261 + '@esbuild/win32-arm64': 0.25.11 2262 + '@esbuild/win32-ia32': 0.25.11 2263 + '@esbuild/win32-x64': 0.25.11 2264 2265 escalade@3.2.0: {} 2266 ··· 2277 2278 gensync@1.0.0-beta.2: {} 2279 2280 + get-tsconfig@4.13.0: 2281 dependencies: 2282 resolve-pkg-maps: 1.0.0 2283 optional: true ··· 2286 2287 graceful-fs@4.2.11: {} 2288 2289 html-entities@2.3.3: {} 2290 2291 is-what@4.1.16: {} 2292 2293 + jiti@2.6.1: {} 2294 2295 js-tokens@4.0.0: {} 2296 ··· 2300 2301 kolorist@1.8.0: {} 2302 2303 + lightningcss-android-arm64@1.30.2: 2304 optional: true 2305 2306 + lightningcss-darwin-arm64@1.30.2: 2307 optional: true 2308 2309 + lightningcss-darwin-x64@1.30.2: 2310 optional: true 2311 2312 + lightningcss-freebsd-x64@1.30.2: 2313 optional: true 2314 2315 + lightningcss-linux-arm-gnueabihf@1.30.2: 2316 optional: true 2317 2318 + lightningcss-linux-arm64-gnu@1.30.2: 2319 optional: true 2320 2321 + lightningcss-linux-arm64-musl@1.30.2: 2322 optional: true 2323 2324 + lightningcss-linux-x64-gnu@1.30.2: 2325 optional: true 2326 2327 + lightningcss-linux-x64-musl@1.30.2: 2328 optional: true 2329 2330 + lightningcss-win32-arm64-msvc@1.30.2: 2331 optional: true 2332 2333 + lightningcss-win32-x64-msvc@1.30.2: 2334 + optional: true 2335 + 2336 + lightningcss@1.30.2: 2337 dependencies: 2338 + detect-libc: 2.1.2 2339 optionalDependencies: 2340 + lightningcss-android-arm64: 1.30.2 2341 + lightningcss-darwin-arm64: 1.30.2 2342 + lightningcss-darwin-x64: 1.30.2 2343 + lightningcss-freebsd-x64: 1.30.2 2344 + lightningcss-linux-arm-gnueabihf: 1.30.2 2345 + lightningcss-linux-arm64-gnu: 1.30.2 2346 + lightningcss-linux-arm64-musl: 1.30.2 2347 + lightningcss-linux-x64-gnu: 1.30.2 2348 + lightningcss-linux-x64-musl: 1.30.2 2349 + lightningcss-win32-arm64-msvc: 1.30.2 2350 + lightningcss-win32-x64-msvc: 1.30.2 2351 2352 local-pkg@1.1.2: 2353 dependencies: ··· 2359 dependencies: 2360 yallist: 3.1.1 2361 2362 + magic-string@0.30.21: 2363 dependencies: 2364 '@jridgewell/sourcemap-codec': 1.5.5 2365 ··· 2367 dependencies: 2368 is-what: 4.1.16 2369 2370 mlly@1.8.0: 2371 dependencies: 2372 acorn: 8.15.0 ··· 2380 2381 nanoid@3.3.11: {} 2382 2383 + nanoid@5.1.6: {} 2384 2385 + node-releases@2.0.27: {} 2386 2387 + package-manager-detector@1.5.0: {} 2388 2389 parse5@7.3.0: 2390 dependencies: ··· 2414 picocolors: 1.1.1 2415 source-map-js: 1.2.1 2416 2417 + prettier-plugin-organize-imports@4.3.0(prettier@3.6.2)(typescript@5.9.3): 2418 dependencies: 2419 prettier: 3.6.2 2420 + typescript: 5.9.3 2421 2422 + prettier-plugin-tailwindcss@0.7.1(prettier-plugin-organize-imports@4.3.0(prettier@3.6.2)(typescript@5.9.3))(prettier@3.6.2): 2423 dependencies: 2424 prettier: 3.6.2 2425 optionalDependencies: 2426 + prettier-plugin-organize-imports: 4.3.0(prettier@3.6.2)(typescript@5.9.3) 2427 2428 prettier@3.6.2: {} 2429 ··· 2432 resolve-pkg-maps@1.0.0: 2433 optional: true 2434 2435 + rollup@4.52.5: 2436 dependencies: 2437 '@types/estree': 1.0.8 2438 optionalDependencies: 2439 + '@rollup/rollup-android-arm-eabi': 4.52.5 2440 + '@rollup/rollup-android-arm64': 4.52.5 2441 + '@rollup/rollup-darwin-arm64': 4.52.5 2442 + '@rollup/rollup-darwin-x64': 4.52.5 2443 + '@rollup/rollup-freebsd-arm64': 4.52.5 2444 + '@rollup/rollup-freebsd-x64': 4.52.5 2445 + '@rollup/rollup-linux-arm-gnueabihf': 4.52.5 2446 + '@rollup/rollup-linux-arm-musleabihf': 4.52.5 2447 + '@rollup/rollup-linux-arm64-gnu': 4.52.5 2448 + '@rollup/rollup-linux-arm64-musl': 4.52.5 2449 + '@rollup/rollup-linux-loong64-gnu': 4.52.5 2450 + '@rollup/rollup-linux-ppc64-gnu': 4.52.5 2451 + '@rollup/rollup-linux-riscv64-gnu': 4.52.5 2452 + '@rollup/rollup-linux-riscv64-musl': 4.52.5 2453 + '@rollup/rollup-linux-s390x-gnu': 4.52.5 2454 + '@rollup/rollup-linux-x64-gnu': 4.52.5 2455 + '@rollup/rollup-linux-x64-musl': 4.52.5 2456 + '@rollup/rollup-openharmony-arm64': 4.52.5 2457 + '@rollup/rollup-win32-arm64-msvc': 4.52.5 2458 + '@rollup/rollup-win32-ia32-msvc': 4.52.5 2459 + '@rollup/rollup-win32-x64-gnu': 4.52.5 2460 + '@rollup/rollup-win32-x64-msvc': 4.52.5 2461 fsevents: 2.3.3 2462 2463 semver@6.3.1: {} ··· 2468 2469 seroval@1.3.2: {} 2470 2471 + solid-js@1.9.10: 2472 dependencies: 2473 csstype: 3.1.3 2474 seroval: 1.3.2 2475 seroval-plugins: 1.3.3(seroval@1.3.2) 2476 2477 + solid-refresh@0.6.3(solid-js@1.9.10): 2478 dependencies: 2479 + '@babel/generator': 7.28.5 2480 '@babel/helper-module-imports': 7.27.1 2481 + '@babel/types': 7.28.5 2482 + solid-js: 1.9.10 2483 transitivePeerDependencies: 2484 - supports-color 2485 2486 source-map-js@1.2.1: {} 2487 2488 + style-mod@4.1.3: {} 2489 2490 + tailwindcss@4.1.16: {} 2491 2492 + tapable@2.3.0: {} 2493 2494 tinyexec@1.0.1: {} 2495 ··· 2501 tsx@4.19.2: 2502 dependencies: 2503 esbuild: 0.23.1 2504 + get-tsconfig: 4.13.0 2505 optionalDependencies: 2506 fsevents: 2.3.3 2507 optional: true 2508 2509 + typescript@5.9.3: {} 2510 2511 ufo@1.6.1: {} 2512 2513 undici-types@6.20.0: 2514 optional: true 2515 2516 + update-browserslist-db@1.1.4(browserslist@4.27.0): 2517 dependencies: 2518 + browserslist: 4.27.0 2519 escalade: 3.2.0 2520 picocolors: 1.1.1 2521 2522 + vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.1.12(@types/node@22.13.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)): 2523 dependencies: 2524 + '@babel/core': 7.28.5 2525 '@types/babel__core': 7.20.5 2526 + babel-preset-solid: 1.9.10(@babel/core@7.28.5)(solid-js@1.9.10) 2527 merge-anything: 5.1.7 2528 + solid-js: 1.9.10 2529 + solid-refresh: 0.6.3(solid-js@1.9.10) 2530 + vite: 7.1.12(@types/node@22.13.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2) 2531 + vitefu: 1.1.1(vite@7.1.12(@types/node@22.13.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)) 2532 transitivePeerDependencies: 2533 - supports-color 2534 2535 + vite@7.1.12(@types/node@22.13.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2): 2536 dependencies: 2537 + esbuild: 0.25.11 2538 fdir: 6.5.0(picomatch@4.0.3) 2539 picomatch: 4.0.3 2540 postcss: 8.5.6 2541 + rollup: 4.52.5 2542 tinyglobby: 0.2.15 2543 optionalDependencies: 2544 '@types/node': 22.13.1 2545 fsevents: 2.3.3 2546 + jiti: 2.6.1 2547 + lightningcss: 1.30.2 2548 tsx: 4.19.2 2549 2550 + vitefu@1.1.1(vite@7.1.12(@types/node@22.13.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)): 2551 optionalDependencies: 2552 + vite: 7.1.12(@types/node@22.13.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2) 2553 2554 w3c-keyname@2.2.8: {} 2555 2556 yallist@3.1.1: {} 2557 2558 yocto-queue@1.2.1: {}
public/88x31.webp

This is a binary file and will not be displayed.

public/cursor.cur

This is a binary file and will not be displayed.

public/headers/almaty.webp

This is a binary file and will not be displayed.

public/headers/aurora.jpg

This is a binary file and will not be displayed.

public/headers/bridge.jpg

This is a binary file and will not be displayed.

public/headers/bunny.jpg

This is a binary file and will not be displayed.

public/headers/city.webp

This is a binary file and will not be displayed.

public/headers/forest.jpg

This is a binary file and will not be displayed.

public/headers/puppy.jpg

This is a binary file and will not be displayed.

public/headers/water.webp

This is a binary file and will not be displayed.

+22
public/manifest.json
···
··· 1 + { 2 + "name": "PDSls", 3 + "description": "", 4 + "id": "dev.pdsls", 5 + "start_url": "/", 6 + "display": "standalone", 7 + "background_color": "#1f1f1f", 8 + "theme_color": "#1f1f1f", 9 + "icons": [ 10 + { 11 + "src": "/favicon.png", 12 + "type": "image/png", 13 + "sizes": "512x512" 14 + }, 15 + { 16 + "src": "/pwa-maskable.png", 17 + "type": "image/png", 18 + "sizes": "512x512", 19 + "purpose": "maskable" 20 + } 21 + ] 22 + }
public/pwa-maskable.png

This is a binary file and will not be displayed.

+85 -47
src/components/account.tsx
··· 1 import { Client, CredentialManager } from "@atcute/client"; 2 import { Did } from "@atcute/lexicons"; 3 - import { deleteStoredSession, getSession, OAuthUserAgent } from "@atcute/oauth-browser-client"; 4 import { A } from "@solidjs/router"; 5 import { createSignal, For, onMount, Show } from "solid-js"; 6 - import { createStore } from "solid-js/store"; 7 import { resolveDidDoc } from "../utils/api.js"; 8 - import { agent, Login, retrieveSession, setAgent } from "./login.jsx"; 9 import { Modal } from "./modal.jsx"; 10 11 - const AccountManager = () => { 12 const [openManager, setOpenManager] = createSignal(false); 13 - const [sessions, setSessions] = createStore<Record<string, string | undefined>>(); 14 - const [avatar, setAvatar] = createSignal<string>(); 15 16 onMount(async () => { 17 - await retrieveSession(); 18 19 - const storedSessions = localStorage.getItem("atcute-oauth:sessions"); 20 - if (storedSessions) { 21 - const sessionDids = Object.keys(JSON.parse(storedSessions)) as Did[]; 22 - sessionDids.forEach((did) => setSessions(did, "")); 23 sessionDids.forEach(async (did) => { 24 const doc = await resolveDidDoc(did); 25 doc.alsoKnownAs?.forEach((alias) => { 26 if (alias.startsWith("at://")) { 27 - setSessions(did, alias.replace("at://", "")); 28 return; 29 } 30 }); 31 }); 32 } 33 - 34 - const repo = localStorage.getItem("lastSignedIn"); 35 - if (repo) setAvatar(await getAvatar(repo as Did)); 36 }); 37 38 const resumeSession = async (did: Did) => { 39 - localStorage.setItem("lastSignedIn", did); 40 - retrieveSession(); 41 - setAvatar(await getAvatar(did)); 42 }; 43 44 const removeSession = async (did: Did) => { ··· 50 } catch { 51 deleteStoredSession(did); 52 } 53 - setSessions(did, undefined); 54 if (currentSession === did) setAgent(undefined); 55 }; 56 ··· 68 return ( 69 <> 70 <Modal open={openManager()} onClose={() => setOpenManager(false)}> 71 - <div class="dark:bg-dark-300 dark:shadow-dark-800 absolute top-12 left-[50%] w-[22rem] -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-opacity duration-300 sm:w-[24rem] dark:border-neutral-700 starting:opacity-0"> 72 - <div class="mb-2 flex items-center gap-1 font-semibold"> 73 - <span class="iconify lucide--user-round"></span> 74 <span>Manage accounts</span> 75 </div> 76 - <div class="mb-3 max-h-[20rem] overflow-y-auto md:max-h-[25rem]"> 77 <For each={Object.keys(sessions)}> 78 {(did) => ( 79 - <div class="flex w-full items-center justify-between gap-x-2 rounded-lg hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"> 80 <button 81 - class="flex basis-full items-center justify-between gap-1 truncate p-1" 82 onclick={() => resumeSession(did as Did)} 83 > 84 - <span class="truncate">{sessions[did]?.length ? sessions[did] : did}</span> 85 - <Show when={did === agent()?.sub}> 86 - <span class="iconify lucide--check shrink-0"></span> 87 </Show> 88 </button> 89 - <div class="flex items-center gap-1"> 90 - <A 91 - href={`/at://${did}`} 92 - onClick={() => setOpenManager(false)} 93 - class="flex items-center p-1" 94 - > 95 - <span class="iconify lucide--book-user"></span> 96 - </A> 97 - <button 98 - onclick={() => removeSession(did as Did)} 99 - class="flex items-center p-1 hover:text-red-500 hover:dark:text-red-400" 100 - > 101 - <span class="iconify lucide--user-round-x"></span> 102 - </button> 103 - </div> 104 </div> 105 )} 106 </For> ··· 112 onclick={() => setOpenManager(true)} 113 class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 114 > 115 - {agent() && avatar() ? 116 - <img src={avatar()} class="dark:shadow-dark-800 size-5 rounded-full shadow-xs" /> 117 : <span class="iconify lucide--circle-user-round text-xl"></span>} 118 </button> 119 </> 120 ); 121 }; 122 - 123 - export { AccountManager };
··· 1 import { Client, CredentialManager } from "@atcute/client"; 2 import { Did } from "@atcute/lexicons"; 3 + import { 4 + createAuthorizationUrl, 5 + deleteStoredSession, 6 + getSession, 7 + OAuthUserAgent, 8 + } from "@atcute/oauth-browser-client"; 9 import { A } from "@solidjs/router"; 10 import { createSignal, For, onMount, Show } from "solid-js"; 11 + import { createStore, produce } from "solid-js/store"; 12 import { resolveDidDoc } from "../utils/api.js"; 13 + import { agent, Login, retrieveSession, Sessions, setAgent } from "./login.jsx"; 14 import { Modal } from "./modal.jsx"; 15 16 + export const [sessions, setSessions] = createStore<Sessions>(); 17 + 18 + export const AccountManager = () => { 19 const [openManager, setOpenManager] = createSignal(false); 20 + const [avatars, setAvatars] = createStore<Record<Did, string>>(); 21 22 onMount(async () => { 23 + try { 24 + await retrieveSession(); 25 + } catch {} 26 27 + const localSessions = localStorage.getItem("sessions"); 28 + if (localSessions) { 29 + const storedSessions: Sessions = JSON.parse(localSessions); 30 + const sessionDids = Object.keys(storedSessions) as Did[]; 31 sessionDids.forEach(async (did) => { 32 const doc = await resolveDidDoc(did); 33 doc.alsoKnownAs?.forEach((alias) => { 34 if (alias.startsWith("at://")) { 35 + setSessions(did, { 36 + signedIn: storedSessions[did].signedIn, 37 + handle: alias.replace("at://", ""), 38 + }); 39 return; 40 } 41 }); 42 }); 43 + sessionDids.forEach(async (did) => { 44 + const avatar = await getAvatar(did); 45 + if (avatar) setAvatars(did, avatar); 46 + }); 47 } 48 }); 49 50 const resumeSession = async (did: Did) => { 51 + try { 52 + localStorage.setItem("lastSignedIn", did); 53 + await retrieveSession(); 54 + } catch { 55 + const authUrl = await createAuthorizationUrl({ 56 + scope: import.meta.env.VITE_OAUTH_SCOPE, 57 + target: { type: "account", identifier: did }, 58 + }); 59 + 60 + await new Promise((resolve) => setTimeout(resolve, 250)); 61 + 62 + location.assign(authUrl); 63 + } 64 }; 65 66 const removeSession = async (did: Did) => { ··· 72 } catch { 73 deleteStoredSession(did); 74 } 75 + setSessions( 76 + produce((accs) => { 77 + delete accs[did]; 78 + }), 79 + ); 80 + localStorage.setItem("sessions", JSON.stringify(sessions)); 81 if (currentSession === did) setAgent(undefined); 82 }; 83 ··· 95 return ( 96 <> 97 <Modal open={openManager()} onClose={() => setOpenManager(false)}> 98 + <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"> 99 + <div class="mb-2 px-1 font-semibold"> 100 <span>Manage accounts</span> 101 </div> 102 + <div class="mb-3 max-h-80 overflow-y-auto md:max-h-100"> 103 <For each={Object.keys(sessions)}> 104 {(did) => ( 105 + <div class="flex items-center"> 106 <button 107 + class="flex w-full items-center justify-between gap-1 truncate rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 108 onclick={() => resumeSession(did as Did)} 109 > 110 + <span class="flex items-center gap-2 truncate"> 111 + <Show when={avatars[did as Did]}> 112 + <img 113 + src={avatars[did as Did].replace("img/avatar/", "img/avatar_thumbnail/")} 114 + class="size-6 rounded-full" 115 + /> 116 + </Show> 117 + <span class="truncate"> 118 + {sessions[did]?.handle ? sessions[did].handle : did} 119 + </span> 120 + </span> 121 + <Show when={did === agent()?.sub && sessions[did].signedIn}> 122 + <span class="iconify lucide--check shrink-0 text-green-500 dark:text-green-400"></span> 123 + </Show> 124 + <Show when={!sessions[did].signedIn}> 125 + <span class="iconify lucide--circle-alert shrink-0 text-red-500 dark:text-red-400"></span> 126 </Show> 127 </button> 128 + <A 129 + href={`/at://${did}`} 130 + onClick={() => setOpenManager(false)} 131 + class="flex items-center rounded-lg p-2 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 132 + > 133 + <span class="iconify lucide--user-round"></span> 134 + </A> 135 + <button 136 + onclick={() => removeSession(did as Did)} 137 + class="flex items-center rounded-lg p-2 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 138 + > 139 + <span class="iconify lucide--x"></span> 140 + </button> 141 </div> 142 )} 143 </For> ··· 149 onclick={() => setOpenManager(true)} 150 class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 151 > 152 + {agent() && avatars[agent()!.sub] ? 153 + <img 154 + src={avatars[agent()!.sub].replace("img/avatar/", "img/avatar_thumbnail/")} 155 + class="size-5 rounded-full" 156 + /> 157 : <span class="iconify lucide--circle-user-round text-xl"></span>} 158 </button> 159 </> 160 ); 161 };
+105 -84
src/components/backlinks.tsx
··· 1 import * as TID from "@atcute/tid"; 2 import { createResource, createSignal, For, onMount, Show } from "solid-js"; 3 - import { getAllBacklinks, getDidBacklinks, getRecordBacklinks } from "../utils/api.js"; 4 import { localDateFromTimestamp } from "../utils/date.js"; 5 import { Button } from "./button.jsx"; 6 7 - // the actual backlink api will probably become closer to this 8 const linksBySource = (links: Record<string, any>) => { 9 - let out: any[] = []; 10 Object.keys(links) 11 .toSorted() 12 .forEach((collection) => { ··· 15 .toSorted() 16 .forEach((path) => { 17 if (paths[path].records === 0) return; 18 - out.push({ collection, path, counts: paths[path] }); 19 }); 20 }); 21 return out; ··· 24 const Backlinks = (props: { target: string }) => { 25 const fetchBacklinks = async () => { 26 const res = await getAllBacklinks(props.target); 27 - setBacklinks(linksBySource(res.links)); 28 - return res; 29 }; 30 31 const [response] = createResource(fetchBacklinks); 32 - const [backlinks, setBacklinks] = createSignal<any>(); 33 34 const [show, setShow] = createSignal<{ 35 collection: string; ··· 38 } | null>(); 39 40 return ( 41 - <Show when={response()}> 42 - <div class="flex w-full flex-col gap-1 text-sm wrap-anywhere"> 43 - <For each={backlinks()}> 44 - {({ collection, path, counts }) => ( 45 <div> 46 - <div> 47 - <div title="Collection containing linking records" class="flex items-center gap-1"> 48 - <span class="iconify lucide--book-text shrink-0"></span> 49 - {collection} 50 - </div> 51 - <div title="Record path where the link is found" class="flex items-center gap-1"> 52 - <span class="iconify lucide--route shrink-0"></span> 53 - {path.slice(1)} 54 - </div> 55 </div> 56 - <div class="ml-4.5"> 57 - <p> 58 - <button 59 - class="text-blue-400 hover:underline active:underline" 60 - title="Show linking records" 61 - onclick={() => 62 - ( 63 - show()?.collection === collection && 64 - show()?.path === path && 65 - !show()?.showDids 66 - ) ? 67 - setShow(null) 68 - : setShow({ collection, path, showDids: false }) 69 - } 70 - > 71 - {counts.records} record{counts.records < 2 ? "" : "s"} 72 - </button> 73 - {" from "} 74 - <button 75 - class="text-blue-400 hover:underline active:underline" 76 - title="Show linking DIDs" 77 - onclick={() => 78 - ( 79 - show()?.collection === collection && 80 - show()?.path === path && 81 - show()?.showDids 82 - ) ? 83 - setShow(null) 84 - : setShow({ collection, path, showDids: true }) 85 - } 86 - > 87 - {counts.distinct_dids} DID 88 - {counts.distinct_dids < 2 ? "" : "s"} 89 - </button> 90 - </p> 91 - <Show when={show()?.collection === collection && show()?.path === path}> 92 - <Show when={show()?.showDids}> 93 - {/* putting this in the `dids` prop directly failed to re-render. idk how to solidjs. */} 94 - <p class="w-full font-semibold">Distinct identities</p> 95 - <BacklinkItems 96 - target={props.target} 97 - collection={collection} 98 - path={path} 99 - dids={true} 100 - /> 101 - </Show> 102 - <Show when={!show()?.showDids}> 103 - <p class="w-full font-semibold">Records</p> 104 - <BacklinkItems 105 - target={props.target} 106 - collection={collection} 107 - path={path} 108 - dids={false} 109 - /> 110 - </Show> 111 - </Show> 112 - </div> 113 </div> 114 )} 115 </For> 116 - </div> 117 - </Show> 118 ); 119 }; 120 ··· 133 dids: boolean; 134 cursor?: string; 135 }) => { 136 - const [links, setLinks] = createSignal<any>(); 137 const [more, setMore] = createSignal<boolean>(false); 138 139 onMount(async () => { ··· 152 return ( 153 <Show when={links()} fallback={<p>Loading&hellip;</p>}> 154 <Show when={dids}> 155 - <For each={links().linking_dids}> 156 {(did) => ( 157 <a 158 href={`/at://${did}`} ··· 164 </For> 165 </Show> 166 <Show when={!dids}> 167 - <For each={links().linking_records}> 168 {({ did, collection, rkey }) => ( 169 <p class="relative flex w-full items-center gap-1 font-mono"> 170 <a ··· 182 )} 183 </For> 184 </Show> 185 - <Show when={links().cursor}> 186 <Show when={more()} fallback={<Button onClick={() => setMore(true)}>Load More</Button>}> 187 <BacklinkItems 188 target={target} 189 collection={collection} 190 path={path} 191 dids={dids} 192 - cursor={links().cursor} 193 /> 194 </Show> 195 </Show>
··· 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) => { ··· 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; ··· 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; ··· 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 60 + title="Collection containing linking records" 61 + class="iconify lucide--book-text shrink-0" 62 + ></span> 63 + {collection} 64 </div> 65 + <For each={response()![collection]}> 66 + {({ path, counts }) => ( 67 + <div class="ml-4.5"> 68 + <div class="flex items-center gap-1"> 69 + <span 70 + title="Record path where the link is found" 71 + class="iconify lucide--route shrink-0" 72 + ></span> 73 + {path.slice(1)} 74 + </div> 75 + <div class="ml-4.5"> 76 + <p> 77 + <button 78 + class="text-blue-400 hover:underline active:underline" 79 + title="Show linking records" 80 + onclick={() => 81 + ( 82 + show()?.collection === collection && 83 + show()?.path === path && 84 + !show()?.showDids 85 + ) ? 86 + setShow(null) 87 + : setShow({ collection, path, showDids: false }) 88 + } 89 + > 90 + {counts.records} record{counts.records < 2 ? "" : "s"} 91 + </button> 92 + {" from "} 93 + <button 94 + class="text-blue-400 hover:underline active:underline" 95 + title="Show linking DIDs" 96 + onclick={() => 97 + ( 98 + show()?.collection === collection && 99 + show()?.path === path && 100 + show()?.showDids 101 + ) ? 102 + setShow(null) 103 + : setShow({ collection, path, showDids: true }) 104 + } 105 + > 106 + {counts.distinct_dids} DID 107 + {counts.distinct_dids < 2 ? "" : "s"} 108 + </button> 109 + </p> 110 + <Show when={show()?.collection === collection && show()?.path === path}> 111 + <Show when={show()?.showDids}> 112 + <p class="w-full font-semibold">Distinct identities</p> 113 + <BacklinkItems 114 + target={props.target} 115 + collection={collection} 116 + path={path} 117 + dids={true} 118 + /> 119 + </Show> 120 + <Show when={!show()?.showDids}> 121 + <p class="w-full font-semibold">Records</p> 122 + <BacklinkItems 123 + target={props.target} 124 + collection={collection} 125 + path={path} 126 + dids={false} 127 + /> 128 + </Show> 129 + </Show> 130 + </div> 131 + </div> 132 + )} 133 + </For> 134 </div> 135 )} 136 </For> 137 + </Show> 138 + </div> 139 ); 140 }; 141 ··· 154 dids: boolean; 155 cursor?: string; 156 }) => { 157 + const [links, setLinks] = createSignal<LinksWithDids | LinksWithRecords>(); 158 const [more, setMore] = createSignal<boolean>(false); 159 160 onMount(async () => { ··· 173 return ( 174 <Show when={links()} fallback={<p>Loading&hellip;</p>}> 175 <Show when={dids}> 176 + <For each={(links() as LinksWithDids).linking_dids}> 177 {(did) => ( 178 <a 179 href={`/at://${did}`} ··· 185 </For> 186 </Show> 187 <Show when={!dids}> 188 + <For each={(links() as LinksWithRecords).linking_records}> 189 {({ did, collection, rkey }) => ( 190 <p class="relative flex w-full items-center gap-1 font-mono"> 191 <a ··· 203 )} 204 </For> 205 </Show> 206 + <Show when={links()?.cursor}> 207 <Show when={more()} fallback={<Button onClick={() => setMore(true)}>Load More</Button>}> 208 <BacklinkItems 209 target={target} 210 collection={collection} 211 path={path} 212 dids={dids} 213 + cursor={links()!.cursor} 214 /> 215 </Show> 216 </Show>
+5 -2
src/components/button.tsx
··· 1 import { JSX } from "solid-js"; 2 3 export interface ButtonProps { 4 class?: string; 5 classList?: Record<string, boolean | undefined>; 6 onClick?: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>; ··· 10 export const Button = (props: ButtonProps) => { 11 return ( 12 <button 13 - type="button" 14 class={ 15 props.class ?? 16 - "dark:hover:bg-dark-200 dark:shadow-dark-800 dark:active:bg-dark-100 box-border flex h-7 items-center gap-1 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 px-2 py-1.5 text-xs font-semibold shadow-xs select-none hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800" 17 } 18 classList={props.classList} 19 onClick={props.onClick}
··· 1 import { JSX } from "solid-js"; 2 3 export interface ButtonProps { 4 + type?: "button" | "submit" | "reset" | "menu" | undefined; 5 + disabled?: boolean; 6 class?: string; 7 classList?: Record<string, boolean | undefined>; 8 onClick?: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>; ··· 12 export const Button = (props: ButtonProps) => { 13 return ( 14 <button 15 + type={props.type ?? "button"} 16 + disabled={props.disabled ?? false} 17 class={ 18 props.class ?? 19 + "dark:hover:bg-dark-200 dark:shadow-dark-700 dark:active:bg-dark-100 box-border flex h-7 items-center gap-1 rounded-lg 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" 20 } 21 classList={props.classList} 22 onClick={props.onClick}
+345 -145
src/components/create.tsx
··· 1 import { Client } from "@atcute/client"; 2 import { remove } from "@mary/exif-rm"; 3 import { useNavigate, useParams } from "@solidjs/router"; 4 - import { createSignal, Show } from "solid-js"; 5 import { Editor, editorView } from "../components/editor.jsx"; 6 import { agent } from "../components/login.jsx"; 7 - import { setNotif } from "../layout.jsx"; 8 import { Button } from "./button.jsx"; 9 import { Modal } from "./modal.jsx"; 10 import { TextInput } from "./text-input.jsx"; 11 import Tooltip from "./tooltip.jsx"; 12 13 export const RecordEditor = (props: { create: boolean; record?: any; refetch?: any }) => { 14 const navigate = useNavigate(); 15 const params = useParams(); 16 const [openDialog, setOpenDialog] = createSignal(false); 17 const [notice, setNotice] = createSignal(""); 18 - const [uploading, setUploading] = createSignal(false); 19 let formRef!: HTMLFormElement; 20 21 - const placeholder = () => { 22 return { 23 $type: "app.bsky.feed.post", 24 text: "This post was sent from PDSls", ··· 35 }; 36 }; 37 38 const createRecord = async (formData: FormData) => { 39 - const rpc = new Client({ handler: agent()! }); 40 const collection = formData.get("collection"); 41 const rkey = formData.get("rkey"); 42 - const validate = formData.get("validate")?.toString(); 43 let record: any; 44 try { 45 record = JSON.parse(editorView.state.doc.toString()); ··· 49 } 50 const res = await rpc.post("com.atproto.repo.createRecord", { 51 input: { 52 - repo: agent()!.sub, 53 collection: collection ? collection.toString() : record.$type, 54 rkey: rkey?.toString().length ? rkey?.toString() : undefined, 55 record: record, 56 - validate: 57 - validate === "true" ? true 58 - : validate === "false" ? false 59 - : undefined, 60 }, 61 }); 62 if (!res.ok) { ··· 64 return; 65 } 66 setOpenDialog(false); 67 - setNotif({ show: true, icon: "lucide--file-check", text: "Record created" }); 68 navigate(`/${res.data.uri}`); 69 }; 70 71 - const editRecord = async (formData: FormData) => { 72 const record = editorView.state.doc.toString(); 73 - const validate = 74 - formData.get("validate")?.toString() === "true" ? true 75 - : formData.get("validate")?.toString() === "false" ? false 76 - : undefined; 77 if (!record) return; 78 const rpc = new Client({ handler: agent()! }); 79 try { 80 const editedRecord = JSON.parse(record); 81 - if (formData.get("recreate")) { 82 const res = await rpc.post("com.atproto.repo.applyWrites", { 83 input: { 84 repo: agent()!.sub, 85 - validate: validate, 86 writes: [ 87 { 88 collection: params.collection as `${string}.${string}.${string}`, ··· 109 collection: params.collection as `${string}.${string}.${string}`, 110 rkey: params.rkey, 111 record: editedRecord, 112 - validate: validate, 113 }, 114 }); 115 if (!res.ok) { ··· 118 } 119 } 120 setOpenDialog(false); 121 - setNotif({ show: true, icon: "lucide--file-check", text: "Record edited" }); 122 props.refetch(); 123 } catch (err: any) { 124 setNotice(err.message); 125 } 126 }; 127 128 - const uploadBlob = async () => { 129 - setNotice(""); 130 - let blob: Blob; 131 132 - const file = (document.getElementById("blob") as HTMLInputElement)?.files?.[0]; 133 - if (!file) return; 134 135 - const mimetype = (document.getElementById("mimetype") as HTMLInputElement)?.value; 136 - (document.getElementById("mimetype") as HTMLInputElement).value = ""; 137 - if (mimetype) blob = new Blob([file], { type: mimetype }); 138 - else blob = file; 139 140 - if ((document.getElementById("exif-rm") as HTMLInputElement).checked) { 141 - const exifRemoved = remove(new Uint8Array(await blob.arrayBuffer())); 142 - if (exifRemoved !== null) blob = new Blob([exifRemoved], { type: blob.type }); 143 - } 144 145 - const rpc = new Client({ handler: agent()! }); 146 - setUploading(true); 147 - const res = await rpc.post("com.atproto.repo.uploadBlob", { 148 - input: blob, 149 }); 150 - setUploading(false); 151 - (document.getElementById("blob") as HTMLInputElement).value = ""; 152 - if (!res.ok) { 153 - setNotice(res.data.error); 154 - return; 155 - } 156 - editorView.dispatch({ 157 - changes: { 158 - from: editorView.state.selection.main.head, 159 - insert: JSON.stringify(res.data.blob, null, 2), 160 - }, 161 }); 162 }; 163 164 return ( 165 <> 166 - <Modal open={openDialog()} onClose={() => setOpenDialog(false)} closeOnClick={false}> 167 - <div class="dark:bg-dark-300 dark:shadow-dark-800 absolute top-12 left-[50%] w-[22rem] -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 shadow-md transition-opacity duration-300 sm:w-xl sm:p-4 lg:w-[48rem] dark:border-neutral-700 starting:opacity-0"> 168 - <div class="mb-2 flex w-full justify-between"> 169 - <div class="flex items-center gap-1 font-semibold"> 170 - <span 171 - class={`iconify ${props.create ? "lucide--square-pen" : "lucide--pencil"}`} 172 - ></span> 173 - <span>{props.create ? "Creating" : "Editing"} record</span> 174 </div> 175 - <button onclick={() => setOpenDialog(false)} class="flex items-center"> 176 - <span class="iconify lucide--x text-lg hover:text-neutral-500 dark:hover:text-neutral-400"></span> 177 </button> 178 </div> 179 <form ref={formRef} class="flex flex-col gap-y-2"> 180 - <div class="flex w-fit flex-col gap-y-1 text-xs sm:text-sm"> 181 - <Show when={props.create}> 182 - <div class="flex items-center gap-x-2"> 183 - <label for="collection" class="min-w-20 select-none"> 184 - Collection 185 - </label> 186 - <TextInput 187 - id="collection" 188 - name="collection" 189 - placeholder="Optional (default: record type)" 190 - class="w-[15rem]" 191 - /> 192 - </div> 193 - <div class="flex items-center gap-x-2"> 194 - <label for="rkey" class="min-w-20 select-none"> 195 - Record key 196 - </label> 197 - <TextInput 198 - id="rkey" 199 - name="rkey" 200 - placeholder="Optional (default: TID)" 201 - class="w-[15rem]" 202 - /> 203 - </div> 204 - </Show> 205 - <div class="flex items-center gap-x-2"> 206 - <label for="validate" class="min-w-20 select-none"> 207 - Validate 208 - </label> 209 <select 210 - name="validate" 211 - id="validate" 212 - class="dark:bg-dark-100 dark:shadow-dark-800 rounded-lg border-[0.5px] border-neutral-300 bg-white px-1 py-1 shadow-xs focus:outline-[1.5px] focus:outline-neutral-900 dark:border-neutral-700 dark:focus:outline-neutral-200" 213 > 214 - <option value="unset">Unset</option> 215 - <option value="true">True</option> 216 - <option value="false">False</option> 217 </select> 218 </div> 219 - <div class="flex items-center gap-2"> 220 - <Show when={!uploading()}> 221 - <div class="dark:hover:bg-dark-200 dark:shadow-dark-800 dark:active:bg-dark-100 flex rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 text-xs font-semibold shadow-xs hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800"> 222 - <input type="file" id="blob" hidden onChange={() => uploadBlob()} /> 223 - <label class="flex items-center gap-1 px-2 py-1.5 select-none" for="blob"> 224 - <span class="iconify lucide--upload text-sm"></span> 225 - Upload 226 - </label> 227 - </div> 228 - <p class="text-xs">Metadata will be pasted after the cursor</p> 229 - </Show> 230 - <Show when={uploading()}> 231 - <span class="iconify lucide--loader-circle animate-spin text-xl"></span> 232 - <p>Uploading...</p> 233 - </Show> 234 - </div> 235 - <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"> 236 - <div class="flex items-center gap-x-2"> 237 - <label for="mimetype" class="min-w-20 select-none"> 238 - MIME type 239 - </label> 240 - <TextInput id="mimetype" placeholder="Optional" class="w-[15rem]" /> 241 - </div> 242 - <div class="flex items-center gap-1"> 243 - <input id="exif-rm" type="checkbox" checked /> 244 - <label for="exif-rm" class="select-none"> 245 - Remove EXIF data 246 - </label> 247 - </div> 248 - </div> 249 - </div> 250 <Editor 251 - content={JSON.stringify(props.create ? placeholder() : props.record, null, 2)} 252 /> 253 <div class="flex flex-col gap-2"> 254 <Show when={notice()}> 255 - <div class="text-red-500 dark:text-red-400">{notice()}</div> 256 </Show> 257 - <div class="flex items-center justify-end gap-2"> 258 - <Show when={!props.create}> 259 - <div class="flex items-center gap-1"> 260 - <input id="recreate" name="recreate" type="checkbox" /> 261 - <label for="recreate" class="text-sm select-none"> 262 - Recreate record 263 - </label> 264 - </div> 265 - </Show> 266 - <Button 267 - onClick={() => 268 - props.create ? 269 - createRecord(new FormData(formRef)) 270 - : editRecord(new FormData(formRef)) 271 - } 272 > 273 - {props.create ? "Create" : "Edit"} 274 - </Button> 275 </div> 276 </div> 277 </form> ··· 279 </Modal> 280 <Tooltip text={`${props.create ? "Create" : "Edit"} record`}> 281 <button 282 - class={`flex items-center p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600 ${props.create ? "rounded-lg" : "rounded-sm"}`} 283 onclick={() => { 284 setNotice(""); 285 setOpenDialog(true);
··· 1 import { Client } from "@atcute/client"; 2 + import { Did } from "@atcute/lexicons"; 3 + import { getSession, OAuthUserAgent } from "@atcute/oauth-browser-client"; 4 import { remove } from "@mary/exif-rm"; 5 import { useNavigate, useParams } from "@solidjs/router"; 6 + import { createEffect, createSignal, For, onCleanup, onMount, Show } from "solid-js"; 7 import { Editor, editorView } from "../components/editor.jsx"; 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 + export const [placeholder, setPlaceholder] = createSignal<any>(); 17 + 18 export const RecordEditor = (props: { create: boolean; record?: any; refetch?: any }) => { 19 const navigate = useNavigate(); 20 const params = useParams(); 21 const [openDialog, setOpenDialog] = createSignal(false); 22 const [notice, setNotice] = createSignal(""); 23 + const [openUpload, setOpenUpload] = createSignal(false); 24 + const [validate, setValidate] = createSignal<boolean | undefined>(undefined); 25 + const [nonBlocking, setNonBlocking] = createSignal(false); 26 + let blobInput!: HTMLInputElement; 27 let formRef!: HTMLFormElement; 28 29 + const defaultPlaceholder = () => { 30 return { 31 $type: "app.bsky.feed.post", 32 text: "This post was sent from PDSls", ··· 43 }; 44 }; 45 46 + const getValidateIcon = () => { 47 + return ( 48 + validate() === true ? "lucide--circle-check" 49 + : validate() === false ? "lucide--circle-x" 50 + : "lucide--circle" 51 + ); 52 + }; 53 + 54 + const getValidateLabel = () => { 55 + return ( 56 + validate() === true ? "True" 57 + : validate() === false ? "False" 58 + : "Unset" 59 + ); 60 + }; 61 + 62 + createEffect(() => { 63 + if (openDialog()) { 64 + setValidate(undefined); 65 + setNonBlocking(false); 66 + } 67 + }); 68 + 69 const createRecord = async (formData: FormData) => { 70 + const repo = formData.get("repo")?.toString(); 71 + if (!repo) return; 72 + const rpc = new Client({ handler: new OAuthUserAgent(await getSession(repo as Did)) }); 73 const collection = formData.get("collection"); 74 const rkey = formData.get("rkey"); 75 let record: any; 76 try { 77 record = JSON.parse(editorView.state.doc.toString()); ··· 81 } 82 const res = await rpc.post("com.atproto.repo.createRecord", { 83 input: { 84 + repo: repo as Did, 85 collection: collection ? collection.toString() : record.$type, 86 rkey: rkey?.toString().length ? rkey?.toString() : undefined, 87 record: record, 88 + validate: validate(), 89 }, 90 }); 91 if (!res.ok) { ··· 93 return; 94 } 95 setOpenDialog(false); 96 + const id = addNotification({ 97 + message: "Record created", 98 + type: "success", 99 + }); 100 + setTimeout(() => removeNotification(id), 3000); 101 navigate(`/${res.data.uri}`); 102 }; 103 104 + const editRecord = async (recreate?: boolean) => { 105 const record = editorView.state.doc.toString(); 106 if (!record) return; 107 const rpc = new Client({ handler: agent()! }); 108 try { 109 const editedRecord = JSON.parse(record); 110 + if (recreate) { 111 const res = await rpc.post("com.atproto.repo.applyWrites", { 112 input: { 113 repo: agent()!.sub, 114 + validate: validate(), 115 writes: [ 116 { 117 collection: params.collection as `${string}.${string}.${string}`, ··· 138 collection: params.collection as `${string}.${string}.${string}`, 139 rkey: params.rkey, 140 record: editedRecord, 141 + validate: validate(), 142 }, 143 }); 144 if (!res.ok) { ··· 147 } 148 } 149 setOpenDialog(false); 150 + const id = addNotification({ 151 + message: "Record edited", 152 + type: "success", 153 + }); 154 + setTimeout(() => removeNotification(id), 3000); 155 props.refetch(); 156 } catch (err: any) { 157 setNotice(err.message); 158 } 159 }; 160 161 + const dragBox = (box: HTMLDivElement) => { 162 + let isDragging = false; 163 + let offsetX: number; 164 + let offsetY: number; 165 + 166 + const handleMouseDown = (e: MouseEvent) => { 167 + if (!(e.target instanceof HTMLElement)) return; 168 + 169 + const closestDraggable = e.target.closest("[data-draggable]") as HTMLElement; 170 + if (closestDraggable && closestDraggable !== box) return; 171 + 172 + if ( 173 + ["INPUT", "SELECT", "BUTTON", "LABEL"].includes(e.target.tagName) || 174 + e.target.closest("#editor, #close") 175 + ) 176 + return; 177 + 178 + e.preventDefault(); 179 + isDragging = true; 180 + box.classList.add("cursor-grabbing"); 181 + 182 + const rect = box.getBoundingClientRect(); 183 + 184 + box.style.left = rect.left + "px"; 185 + box.style.top = rect.top + "px"; 186 + 187 + box.classList.remove("-translate-x-1/2"); 188 189 + offsetX = e.clientX - rect.left; 190 + offsetY = e.clientY - rect.top; 191 + }; 192 + 193 + const handleMouseMove = (e: MouseEvent) => { 194 + if (isDragging) { 195 + let newLeft = e.clientX - offsetX; 196 + let newTop = e.clientY - offsetY; 197 + 198 + const boxWidth = box.offsetWidth; 199 + const boxHeight = box.offsetHeight; 200 + 201 + const viewportWidth = window.innerWidth; 202 + const viewportHeight = window.innerHeight; 203 + 204 + newLeft = Math.max(0, Math.min(newLeft, viewportWidth - boxWidth)); 205 + newTop = Math.max(0, Math.min(newTop, viewportHeight - boxHeight)); 206 207 + box.style.left = newLeft + "px"; 208 + box.style.top = newTop + "px"; 209 + } 210 + }; 211 212 + const handleMouseUp = () => { 213 + if (isDragging) { 214 + isDragging = false; 215 + box.classList.remove("cursor-grabbing"); 216 + } 217 + }; 218 219 + onMount(() => { 220 + box.addEventListener("mousedown", handleMouseDown); 221 + document.addEventListener("mousemove", handleMouseMove); 222 + document.addEventListener("mouseup", handleMouseUp); 223 }); 224 + 225 + onCleanup(() => { 226 + box.removeEventListener("mousedown", handleMouseDown); 227 + document.removeEventListener("mousemove", handleMouseMove); 228 + document.removeEventListener("mouseup", handleMouseUp); 229 }); 230 }; 231 232 + const FileUpload = (props: { file: File }) => { 233 + const [uploading, setUploading] = createSignal(false); 234 + const [error, setError] = createSignal(""); 235 + 236 + onCleanup(() => (blobInput.value = "")); 237 + 238 + const formatFileSize = (bytes: number) => { 239 + if (bytes === 0) return "0 Bytes"; 240 + const k = 1024; 241 + const sizes = ["Bytes", "KB", "MB", "GB"]; 242 + const i = Math.floor(Math.log(bytes) / Math.log(k)); 243 + return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i]; 244 + }; 245 + 246 + const uploadBlob = async () => { 247 + let blob: Blob; 248 + 249 + const mimetype = (document.getElementById("mimetype") as HTMLInputElement)?.value; 250 + (document.getElementById("mimetype") as HTMLInputElement).value = ""; 251 + if (mimetype) blob = new Blob([props.file], { type: mimetype }); 252 + else blob = props.file; 253 + 254 + if ((document.getElementById("exif-rm") as HTMLInputElement).checked) { 255 + const exifRemoved = remove(new Uint8Array(await blob.arrayBuffer())); 256 + if (exifRemoved !== null) blob = new Blob([exifRemoved], { type: blob.type }); 257 + } 258 + 259 + const rpc = new Client({ handler: agent()! }); 260 + setUploading(true); 261 + const res = await rpc.post("com.atproto.repo.uploadBlob", { 262 + input: blob, 263 + }); 264 + setUploading(false); 265 + if (!res.ok) { 266 + setError(res.data.error); 267 + return; 268 + } 269 + editorView.dispatch({ 270 + changes: { 271 + from: editorView.state.selection.main.head, 272 + insert: JSON.stringify(res.data.blob, null, 2), 273 + }, 274 + }); 275 + setOpenUpload(false); 276 + }; 277 + 278 + return ( 279 + <div 280 + data-draggable 281 + 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" 282 + ref={dragBox} 283 + > 284 + <h2 class="mb-2 font-semibold">Upload blob</h2> 285 + <div class="flex flex-col gap-2 text-sm"> 286 + <div class="flex flex-col gap-1"> 287 + <p class="flex gap-1"> 288 + <span class="truncate">{props.file.name}</span> 289 + <span class="shrink-0 text-neutral-600 dark:text-neutral-400"> 290 + ({formatFileSize(props.file.size)}) 291 + </span> 292 + </p> 293 + </div> 294 + <div class="flex items-center gap-x-2"> 295 + <label for="mimetype" class="shrink-0 select-none"> 296 + MIME type 297 + </label> 298 + <TextInput id="mimetype" placeholder={props.file.type} /> 299 + </div> 300 + <div class="flex items-center gap-1"> 301 + <input id="exif-rm" type="checkbox" checked /> 302 + <label for="exif-rm" class="select-none"> 303 + Remove EXIF data 304 + </label> 305 + </div> 306 + <p class="text-xs text-neutral-600 dark:text-neutral-400"> 307 + Metadata will be pasted after the cursor 308 + </p> 309 + <Show when={error()}> 310 + <span class="text-red-500 dark:text-red-400">Error: {error()}</span> 311 + </Show> 312 + <div class="flex justify-between gap-2"> 313 + <Button onClick={() => setOpenUpload(false)}>Cancel</Button> 314 + <Show when={uploading()}> 315 + <div class="flex items-center gap-1"> 316 + <span class="iconify lucide--loader-circle animate-spin"></span> 317 + <span>Uploading</span> 318 + </div> 319 + </Show> 320 + <Show when={!uploading()}> 321 + <Button 322 + onClick={uploadBlob} 323 + 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" 324 + > 325 + Upload 326 + </Button> 327 + </Show> 328 + </div> 329 + </div> 330 + </div> 331 + ); 332 + }; 333 + 334 return ( 335 <> 336 + <Modal 337 + open={openDialog()} 338 + onClose={() => setOpenDialog(false)} 339 + closeOnClick={false} 340 + nonBlocking={nonBlocking()} 341 + > 342 + <div 343 + data-draggable 344 + classList={{ 345 + "dark:bg-dark-300 dark:shadow-dark-700 pointer-events-auto absolute top-18 left-[50%] w-screen -translate-x-1/2 cursor-grab rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-opacity duration-200 sm:w-xl lg:w-3xl dark:border-neutral-700 starting:opacity-0": true, 346 + "opacity-60 hover:opacity-100": nonBlocking(), 347 + }} 348 + ref={dragBox} 349 + > 350 + <div class="mb-2 flex w-full justify-between text-base"> 351 + <div class="flex items-center gap-2"> 352 + <span class="font-semibold select-none"> 353 + {props.create ? "Creating" : "Editing"} record 354 + </span> 355 + <Tooltip text={nonBlocking() ? "Lock" : "Unlock"}> 356 + <button 357 + type="button" 358 + onclick={() => setNonBlocking(!nonBlocking())} 359 + class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 360 + > 361 + <span 362 + class={`iconify ${nonBlocking() ? "lucide--lock-open" : "lucide--lock"}`} 363 + ></span> 364 + </button> 365 + </Tooltip> 366 </div> 367 + <button 368 + id="close" 369 + onclick={() => setOpenDialog(false)} 370 + class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 371 + > 372 + <span class="iconify lucide--x"></span> 373 </button> 374 </div> 375 <form ref={formRef} class="flex flex-col gap-y-2"> 376 + <Show when={props.create}> 377 + <div class="flex flex-wrap items-center gap-1 text-sm"> 378 + <span>at://</span> 379 <select 380 + class="dark:bg-dark-100 dark:shadow-dark-700 max-w-40 truncate rounded-lg border-[0.5px] border-neutral-300 bg-white px-1 py-1 shadow-xs select-none focus:outline-[1px] focus:outline-neutral-600 dark:border-neutral-600 dark:focus:outline-neutral-400" 381 + name="repo" 382 + id="repo" 383 > 384 + <For each={Object.keys(sessions)}> 385 + {(session) => ( 386 + <option value={session} selected={session === agent()?.sub}> 387 + {sessions[session].handle ?? session} 388 + </option> 389 + )} 390 + </For> 391 </select> 392 + <span>/</span> 393 + <TextInput 394 + id="collection" 395 + name="collection" 396 + placeholder="Collection (default: $type)" 397 + class="w-40 placeholder:text-xs lg:w-52" 398 + /> 399 + <span>/</span> 400 + <TextInput 401 + id="rkey" 402 + name="rkey" 403 + placeholder="Record key (default: TID)" 404 + class="w-40 placeholder:text-xs lg:w-52" 405 + /> 406 </div> 407 + </Show> 408 <Editor 409 + content={JSON.stringify( 410 + !props.create ? props.record 411 + : params.rkey ? placeholder() 412 + : defaultPlaceholder(), 413 + null, 414 + 2, 415 + )} 416 /> 417 <div class="flex flex-col gap-2"> 418 <Show when={notice()}> 419 + <div class="text-sm text-red-500 dark:text-red-400">{notice()}</div> 420 </Show> 421 + <div class="flex justify-between gap-2"> 422 + <button 423 + type="button" 424 + 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 text-xs shadow-xs hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800" 425 + > 426 + <input 427 + type="file" 428 + id="blob" 429 + class="sr-only" 430 + ref={blobInput} 431 + onChange={(e) => { 432 + if (e.target.files !== null) setOpenUpload(true); 433 + }} 434 + /> 435 + <label class="flex items-center gap-1 px-2 py-1.5 select-none" for="blob"> 436 + <span class="iconify lucide--upload"></span> 437 + Upload 438 + </label> 439 + </button> 440 + <Modal 441 + open={openUpload()} 442 + onClose={() => setOpenUpload(false)} 443 + closeOnClick={false} 444 > 445 + <FileUpload file={blobInput.files![0]} /> 446 + </Modal> 447 + <div class="flex items-center justify-end gap-2"> 448 + <button 449 + type="button" 450 + class="flex items-center gap-1 rounded-sm p-1 text-sm hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 451 + onClick={() => 452 + setValidate( 453 + validate() === true ? false 454 + : validate() === false ? undefined 455 + : true, 456 + ) 457 + } 458 + > 459 + <Tooltip text={getValidateLabel()}> 460 + <span class={`iconify ${getValidateIcon()}`}></span> 461 + </Tooltip> 462 + <span>Validate</span> 463 + </button> 464 + <Show when={!props.create}> 465 + <Button onClick={() => editRecord(true)}>Recreate</Button> 466 + </Show> 467 + <Button 468 + onClick={() => 469 + props.create ? createRecord(new FormData(formRef)) : editRecord() 470 + } 471 + > 472 + {props.create ? "Create" : "Edit"} 473 + </Button> 474 + </div> 475 </div> 476 </div> 477 </form> ··· 479 </Modal> 480 <Tooltip text={`${props.create ? "Create" : "Edit"} record`}> 481 <button 482 + class={`flex items-center ${props.create ? "p-1" : "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"}`} 483 onclick={() => { 484 setNotice(""); 485 setOpenDialog(true);
+39 -5
src/components/dropdown.tsx
··· 24 return <MenuContext.Provider value={value}>{props.children}</MenuContext.Provider>; 25 }; 26 27 - export const CopyMenu = (props: { copyContent: string; label: string; icon?: string }) => { 28 const ctx = useContext(MenuContext); 29 30 return ( 31 <button 32 onClick={() => { 33 - addToClipboard(props.copyContent); 34 ctx?.setShowMenu(false); 35 }} 36 class="flex items-center gap-1.5 rounded-lg p-1 whitespace-nowrap hover:bg-neutral-200/50 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" ··· 43 ); 44 }; 45 46 - export const NavMenu = (props: { href: string; label: string; icon: string; newTab?: boolean }) => { 47 const ctx = useContext(MenuContext); 48 49 return ( ··· 51 href={props.href} 52 onClick={() => ctx?.setShowMenu(false)} 53 class="flex items-center gap-1.5 rounded-lg p-1 hover:bg-neutral-200/50 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 54 target={props.newTab ? "_blank" : undefined} 55 > 56 - <span class={"iconify shrink-0 " + props.icon}></span> 57 <span class="whitespace-nowrap">{props.label}</span> 58 </A> 59 ); 60 }; 61 62 export const DropdownMenu = (props: { 63 icon: string; 64 buttonClass?: string; ··· 93 <div 94 ref={setMenu} 95 class={ 96 - "dark:bg-dark-300 dark:shadow-dark-800 absolute right-0 z-20 flex flex-col rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 shadow-md dark:border-neutral-700 " + 97 props.menuClass 98 } 99 >
··· 24 return <MenuContext.Provider value={value}>{props.children}</MenuContext.Provider>; 25 }; 26 27 + export const CopyMenu = (props: { content: string; label: string; icon?: string }) => { 28 const ctx = useContext(MenuContext); 29 30 return ( 31 <button 32 onClick={() => { 33 + addToClipboard(props.content); 34 ctx?.setShowMenu(false); 35 }} 36 class="flex items-center gap-1.5 rounded-lg p-1 whitespace-nowrap hover:bg-neutral-200/50 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" ··· 43 ); 44 }; 45 46 + export const NavMenu = (props: { 47 + href: string; 48 + label: string; 49 + icon?: string; 50 + newTab?: boolean; 51 + external?: boolean; 52 + }) => { 53 const ctx = useContext(MenuContext); 54 55 return ( ··· 57 href={props.href} 58 onClick={() => ctx?.setShowMenu(false)} 59 class="flex items-center gap-1.5 rounded-lg p-1 hover:bg-neutral-200/50 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 60 + classList={{ "justify-between": props.external }} 61 target={props.newTab ? "_blank" : undefined} 62 > 63 + <Show when={props.icon}> 64 + <span class={"iconify shrink-0 " + props.icon}></span> 65 + </Show> 66 <span class="whitespace-nowrap">{props.label}</span> 67 + <Show when={props.external}> 68 + <span class="iconify lucide--external-link"></span> 69 + </Show> 70 </A> 71 ); 72 }; 73 74 + export const ActionMenu = (props: { 75 + label: string; 76 + icon: string; 77 + onClick: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>; 78 + }) => { 79 + return ( 80 + <button 81 + onClick={props.onClick} 82 + class="flex items-center gap-1.5 rounded-lg p-1 whitespace-nowrap hover:bg-neutral-200/50 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 83 + > 84 + <Show when={props.icon}> 85 + <span class={"iconify shrink-0 " + props.icon}></span> 86 + </Show> 87 + <span class="whitespace-nowrap">{props.label}</span> 88 + </button> 89 + ); 90 + }; 91 + 92 + export const MenuSeparator = () => { 93 + return <div class="my-1 h-[0.5px] bg-neutral-300 dark:bg-neutral-600" />; 94 + }; 95 + 96 export const DropdownMenu = (props: { 97 icon: string; 98 buttonClass?: string; ··· 127 <div 128 ref={setMenu} 129 class={ 130 + "dark:bg-dark-300 dark:shadow-dark-700 absolute right-0 z-40 flex flex-col rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 shadow-md dark:border-neutral-700 " + 131 props.menuClass 132 } 133 >
+2 -1
src/components/editor.tsx
··· 57 return ( 58 <div 59 ref={editorDiv} 60 - class="dark:shadow-dark-800 border-[0.5px] border-neutral-300 shadow-xs dark:border-neutral-700" 61 ></div> 62 ); 63 };
··· 57 return ( 58 <div 59 ref={editorDiv} 60 + id="editor" 61 + class="dark:shadow-dark-700 cursor-auto border-[0.5px] border-neutral-300 shadow-xs dark:border-neutral-700" 62 ></div> 63 ); 64 };
+148 -71
src/components/json.tsx
··· 1 - import { A } from "@solidjs/router"; 2 - import { createEffect, createSignal, For, Show } from "solid-js"; 3 import { hideMedia } from "../views/settings"; 4 import { pds } from "./navbar"; 5 - import Tooltip from "./tooltip"; 6 import VideoPlayer from "./video-player"; 7 8 interface AtBlob { ··· 11 mimeType: string; 12 } 13 14 - const ATURI_RE = 15 - /^at:\/\/([a-zA-Z0-9._:%-]+)(?:\/([a-zA-Z0-9-.]+)(?:\/([a-zA-Z0-9._~:@!$&%')(*+,;=-]+))?)?(?:#(\/[a-zA-Z0-9._~:@!$&%')(*+,;=\-[\]/\\]*))?$/; 16 - 17 - const DID_RE = /^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$/; 18 19 - const JSONString = ({ data }: { data: string }) => { 20 const isURL = 21 URL.canParse ?? 22 ((url, base) => { ··· 28 } 29 }); 30 31 return ( 32 <span> 33 " 34 - <For each={data.split(/(\s)/)}> 35 {(part) => ( 36 <> 37 {ATURI_RE.test(part) ? 38 <A class="text-blue-400 hover:underline active:underline" href={`/${part}`}> 39 {part} 40 </A> 41 - : DID_RE.test(part) ? 42 <A class="text-blue-400 hover:underline active:underline" href={`/at://${part}`}> 43 {part} 44 </A> 45 : ( 46 isURL(part) && 47 ["http:", "https:", "web+at:"].includes(new URL(part).protocol) && 48 part.split("\n").length === 1 49 ) ? 50 - <a 51 - class="text-blue-400 hover:underline active:underline" 52 - href={part} 53 - target="_blank" 54 - rel="noopener noreferrer" 55 - > 56 {part} 57 </a> 58 : part} ··· 76 return <span>null</span>; 77 }; 78 79 - const JSONObject = ({ data, repo }: { data: { [x: string]: JSONType }; repo: string }) => { 80 - const [hide, setHide] = createSignal(localStorage.hideMedia === "true"); 81 82 - createEffect(() => setHide(hideMedia())); 83 84 const Obj = ({ key, value }: { key: string; value: JSONType }) => { 85 const [show, setShow] = createSignal(true); ··· 110 <span 111 classList={{ 112 "self-center": value !== Object(value), 113 - "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 dark:has-hover:group-hover/indent:border-neutral-300": 114 value === Object(value), 115 "invisible h-0": !show(), 116 }} 117 > 118 - <JSONValue data={value} repo={repo} /> 119 </span> 120 </span> 121 ); 122 }; 123 124 const rawObj = ( 125 - <For each={Object.entries(data)}>{([key, value]) => <Obj key={key} value={value} />}</For> 126 ); 127 128 - const blob: AtBlob = data as any; 129 130 if (blob.$type === "blob") { 131 return ( 132 <> 133 - <span class="flex gap-x-1"> 134 - <Show when={blob.mimeType.startsWith("image/") && !hide()}> 135 - <a 136 - href={`https://cdn.bsky.app/img/feed_thumbnail/plain/${repo}/${blob.ref.$link}@jpeg`} 137 - target="_blank" 138 - > 139 - <img 140 - class="max-h-[16rem] w-full max-w-[16rem]" 141 - src={`https://cdn.bsky.app/img/feed_thumbnail/plain/${repo}/${blob.ref.$link}@jpeg`} 142 - /> 143 - </a> 144 - </Show> 145 - <Show when={blob.mimeType === "video/mp4" && !hide()}> 146 - <VideoPlayer did={repo} cid={blob.ref.$link} /> 147 - </Show> 148 - <span 149 - classList={{ "flex items-center justify-between gap-1": true, "flex-col": !hide() }} 150 - > 151 - <Show when={blob.mimeType.startsWith("image/") || blob.mimeType === "video/mp4"}> 152 - <Tooltip text={hide() ? "Show" : "Hide"}> 153 <button 154 - onclick={() => setHide(!hide())} 155 - class={`${!hide() ? "-mt-1 -ml-0.5" : ""} flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600`} 156 > 157 - <span 158 - class={`iconify text-base ${hide() ? "lucide--eye-off" : "lucide--eye"}`} 159 - ></span> 160 </button> 161 - </Tooltip> 162 - </Show> 163 - <Show when={pds()}> 164 - <Tooltip text="Blob PDS link"> 165 - <a 166 - href={`https://${pds()}/xrpc/com.atproto.sync.getBlob?did=${repo}&cid=${blob.ref.$link}`} 167 - target="_blank" 168 - class={`${!hide() ? "-mb-1 -ml-0.5" : ""} flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600`} 169 - > 170 - <span class="iconify lucide--external-link text-base"></span> 171 - </a> 172 - </Tooltip> 173 - </Show> 174 - </span> 175 - </span> 176 {rawObj} 177 </> 178 ); ··· 181 return rawObj; 182 }; 183 184 - const JSONArray = ({ data, repo }: { data: JSONType[]; repo: string }) => { 185 return ( 186 - <For each={data}> 187 {(value, index) => ( 188 <span 189 classList={{ 190 "flex before:content-['-']": true, 191 - "mb-2": value === Object(value) && index() !== data.length - 1, 192 }} 193 > 194 <span class="ml-[1ch] w-full"> 195 - <JSONValue data={value} repo={repo} /> 196 </span> 197 </span> 198 )} ··· 200 ); 201 }; 202 203 - export const JSONValue = ({ data, repo }: { data: JSONType; repo: string }) => { 204 - if (typeof data === "string") return <JSONString data={data} />; 205 if (typeof data === "number") return <JSONNumber data={data} />; 206 if (typeof data === "boolean") return <JSONBoolean data={data} />; 207 if (data === null) return <JSONNull />; 208 - if (Array.isArray(data)) return <JSONArray data={data} repo={repo} />; 209 - return <JSONObject data={data} repo={repo} />; 210 }; 211 212 export type JSONType = string | number | boolean | null | { [x: string]: JSONType } | JSONType[];
··· 1 + import { isCid, isDid, isNsid, 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 { ATURI_RE } from "../utils/types/at-uri"; 6 import { hideMedia } from "../views/settings"; 7 import { pds } from "./navbar"; 8 + import { addNotification, removeNotification } from "./notification"; 9 import VideoPlayer from "./video-player"; 10 11 interface AtBlob { ··· 14 mimeType: string; 15 } 16 17 + const JSONString = (props: { 18 + data: string; 19 + isType?: boolean; 20 + isLink?: boolean; 21 + parentIsBlob?: boolean; 22 + }) => { 23 + const navigate = useNavigate(); 24 + const params = useParams(); 25 26 const isURL = 27 URL.canParse ?? 28 ((url, base) => { ··· 34 } 35 }); 36 37 + const handleClick = async (lex: string) => { 38 + try { 39 + const [nsid, anchor] = lex.split("#"); 40 + const authority = await resolveLexiconAuthority(nsid as Nsid); 41 + 42 + const hash = anchor ? `#schema:${anchor}` : "#schema"; 43 + navigate(`/at://${authority}/com.atproto.lexicon.schema/${nsid}${hash}`); 44 + } catch (err) { 45 + console.error("Failed to resolve lexicon authority:", err); 46 + const id = addNotification({ 47 + message: "Could not resolve schema", 48 + type: "error", 49 + }); 50 + setTimeout(() => removeNotification(id), 5000); 51 + } 52 + }; 53 + 54 return ( 55 <span> 56 " 57 + <For each={props.data.split(/(\s)/)}> 58 {(part) => ( 59 <> 60 {ATURI_RE.test(part) ? 61 <A class="text-blue-400 hover:underline active:underline" href={`/${part}`}> 62 {part} 63 </A> 64 + : isDid(part) ? 65 <A class="text-blue-400 hover:underline active:underline" href={`/at://${part}`}> 66 {part} 67 </A> 68 + : isNsid(part.split("#")[0]) && props.isType ? 69 + <button 70 + type="button" 71 + onClick={() => handleClick(part)} 72 + class="cursor-pointer text-blue-400 hover:underline active:underline" 73 + > 74 + {part} 75 + </button> 76 + : isCid(part) && props.isLink && props.parentIsBlob && params.repo ? 77 + <A 78 + class="text-blue-400 hover:underline active:underline" 79 + rel="noopener" 80 + target="_blank" 81 + href={`https://${pds()}/xrpc/com.atproto.sync.getBlob?did=${params.repo}&cid=${part}`} 82 + > 83 + {part} 84 + </A> 85 : ( 86 isURL(part) && 87 ["http:", "https:", "web+at:"].includes(new URL(part).protocol) && 88 part.split("\n").length === 1 89 ) ? 90 + <a class="underline hover:text-blue-400" href={part} target="_blank" rel="noopener"> 91 {part} 92 </a> 93 : part} ··· 111 return <span>null</span>; 112 }; 113 114 + const JSONObject = (props: { 115 + data: { [x: string]: JSONType }; 116 + repo: string; 117 + parentIsBlob?: boolean; 118 + }) => { 119 + const params = useParams(); 120 + const [hide, setHide] = createSignal( 121 + localStorage.hideMedia === "true" || params.rkey === undefined, 122 + ); 123 + const [mediaLoaded, setMediaLoaded] = createSignal(false); 124 + 125 + createEffect(() => { 126 + if (hideMedia()) setHide(hideMedia()); 127 + }); 128 + 129 + createEffect( 130 + on( 131 + hide, 132 + (value) => { 133 + if (value === false) setMediaLoaded(false); 134 + }, 135 + { defer: true }, 136 + ), 137 + ); 138 139 + const isBlob = props.data.$type === "blob"; 140 + const isBlobContext = isBlob || props.parentIsBlob; 141 142 const Obj = ({ key, value }: { key: string; value: JSONType }) => { 143 const [show, setShow] = createSignal(true); ··· 168 <span 169 classList={{ 170 "self-center": value !== Object(value), 171 + "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": 172 value === Object(value), 173 "invisible h-0": !show(), 174 }} 175 > 176 + <JSONValue 177 + data={value} 178 + repo={props.repo} 179 + isType={key === "$type"} 180 + isLink={key === "$link"} 181 + parentIsBlob={isBlobContext} 182 + /> 183 </span> 184 </span> 185 ); 186 }; 187 188 const rawObj = ( 189 + <For each={Object.entries(props.data)}>{([key, value]) => <Obj key={key} value={value} />}</For> 190 ); 191 192 + const blob: AtBlob = props.data as any; 193 194 if (blob.$type === "blob") { 195 return ( 196 <> 197 + <Show when={pds() && params.rkey}> 198 + <Show when={blob.mimeType.startsWith("image/") || blob.mimeType === "video/mp4"}> 199 + <span class="group/media relative flex w-fit"> 200 + <Show when={!hide()}> 201 + <Show when={blob.mimeType.startsWith("image/")}> 202 + <img 203 + class="h-auto max-h-64 max-w-[16rem] object-contain" 204 + src={`https://${pds()}/xrpc/com.atproto.sync.getBlob?did=${props.repo}&cid=${blob.ref.$link}`} 205 + onLoad={() => setMediaLoaded(true)} 206 + /> 207 + </Show> 208 + <Show when={blob.mimeType === "video/mp4"}> 209 + <ErrorBoundary fallback={() => <span>Failed to load video</span>}> 210 + <VideoPlayer 211 + did={props.repo} 212 + cid={blob.ref.$link} 213 + onLoad={() => setMediaLoaded(true)} 214 + /> 215 + </ErrorBoundary> 216 + </Show> 217 + <Show when={mediaLoaded()}> 218 + <button 219 + onclick={() => setHide(true)} 220 + class="absolute top-1 right-1 flex items-center rounded-lg bg-neutral-900/70 p-1.5 text-white opacity-100 backdrop-blur-sm transition-opacity hover:bg-neutral-900/80 active:bg-neutral-900/90 sm:opacity-0 sm:group-hover/media:opacity-100 dark:bg-neutral-100/70 dark:text-neutral-900 dark:hover:bg-neutral-100/80 dark:active:bg-neutral-100/90" 221 + > 222 + <span class="iconify lucide--eye-off text-base"></span> 223 + </button> 224 + </Show> 225 + </Show> 226 + <Show when={hide()}> 227 <button 228 + onclick={() => setHide(false)} 229 + class="flex items-center rounded-lg bg-neutral-200 p-1.5 transition-colors hover:bg-neutral-300 active:bg-neutral-400 dark:bg-neutral-700 dark:hover:bg-neutral-600 dark:active:bg-neutral-500" 230 > 231 + <span class="iconify lucide--eye text-base"></span> 232 </button> 233 + </Show> 234 + </span> 235 + </Show> 236 + </Show> 237 {rawObj} 238 </> 239 ); ··· 242 return rawObj; 243 }; 244 245 + const JSONArray = (props: { data: JSONType[]; repo: string; parentIsBlob?: boolean }) => { 246 return ( 247 + <For each={props.data}> 248 {(value, index) => ( 249 <span 250 classList={{ 251 "flex before:content-['-']": true, 252 + "mb-2": value === Object(value) && index() !== props.data.length - 1, 253 }} 254 > 255 <span class="ml-[1ch] w-full"> 256 + <JSONValue data={value} repo={props.repo} parentIsBlob={props.parentIsBlob} /> 257 </span> 258 </span> 259 )} ··· 261 ); 262 }; 263 264 + export const JSONValue = (props: { 265 + data: JSONType; 266 + repo: string; 267 + isType?: boolean; 268 + isLink?: boolean; 269 + parentIsBlob?: boolean; 270 + }) => { 271 + const data = props.data; 272 + if (typeof data === "string") 273 + return ( 274 + <JSONString 275 + data={data} 276 + isType={props.isType} 277 + isLink={props.isLink} 278 + parentIsBlob={props.parentIsBlob} 279 + /> 280 + ); 281 if (typeof data === "number") return <JSONNumber data={data} />; 282 if (typeof data === "boolean") return <JSONBoolean data={data} />; 283 if (data === null) return <JSONNull />; 284 + if (Array.isArray(data)) 285 + return <JSONArray data={data} repo={props.repo} parentIsBlob={props.parentIsBlob} />; 286 + return <JSONObject data={data} repo={props.repo} parentIsBlob={props.parentIsBlob} />; 287 }; 288 289 export type JSONType = string | number | boolean | null | { [x: string]: JSONType } | JSONType[];
+554
src/components/lexicon-schema.tsx
···
··· 1 + import { Nsid } from "@atcute/lexicons"; 2 + import { useLocation, useNavigate } from "@solidjs/router"; 3 + import { createEffect, For, Show } from "solid-js"; 4 + import { resolveLexiconAuthority } from "../utils/api.js"; 5 + 6 + interface LexiconSchema { 7 + lexicon: number; 8 + id: string; 9 + description?: string; 10 + defs: { 11 + [key: string]: LexiconDef; 12 + }; 13 + } 14 + 15 + interface LexiconDef { 16 + type: string; 17 + description?: string; 18 + key?: string; 19 + record?: LexiconObject; 20 + parameters?: LexiconObject; 21 + input?: { encoding: string; schema?: LexiconObject }; 22 + output?: { encoding: string; schema?: LexiconObject }; 23 + errors?: Array<{ name: string; description?: string }>; 24 + properties?: { [key: string]: LexiconProperty }; 25 + required?: string[]; 26 + nullable?: string[]; 27 + maxLength?: number; 28 + minLength?: number; 29 + maxGraphemes?: number; 30 + minGraphemes?: number; 31 + items?: LexiconProperty; 32 + refs?: string[]; 33 + closed?: boolean; 34 + enum?: string[]; 35 + const?: string; 36 + default?: any; 37 + minimum?: number; 38 + maximum?: number; 39 + accept?: string[]; 40 + maxSize?: number; 41 + knownValues?: string[]; 42 + format?: string; 43 + } 44 + 45 + interface LexiconObject { 46 + type: string; 47 + description?: string; 48 + ref?: string; 49 + refs?: string[]; 50 + closed?: boolean; 51 + properties?: { [key: string]: LexiconProperty }; 52 + required?: string[]; 53 + nullable?: string[]; 54 + } 55 + 56 + interface LexiconProperty { 57 + type: string; 58 + description?: string; 59 + ref?: string; 60 + refs?: string[]; 61 + closed?: boolean; 62 + format?: string; 63 + items?: LexiconProperty; 64 + minLength?: number; 65 + maxLength?: number; 66 + maxGraphemes?: number; 67 + minGraphemes?: number; 68 + minimum?: number; 69 + maximum?: number; 70 + enum?: string[]; 71 + const?: string | boolean | number; 72 + default?: any; 73 + knownValues?: string[]; 74 + accept?: string[]; 75 + maxSize?: number; 76 + } 77 + 78 + const TypeBadge = (props: { type: string; format?: string; refType?: string }) => { 79 + const navigate = useNavigate(); 80 + const displayType = 81 + props.refType ? props.refType.replace(/^#/, "") 82 + : props.format ? `${props.type}:${props.format}` 83 + : props.type; 84 + 85 + const isLocalRef = () => props.refType?.startsWith("#"); 86 + const isExternalRef = () => props.refType && !props.refType.startsWith("#"); 87 + 88 + const handleClick = async () => { 89 + if (isLocalRef()) { 90 + const defName = props.refType!.slice(1); 91 + window.history.replaceState(null, "", `#schema:${defName}`); 92 + const element = document.getElementById(`def-${defName}`); 93 + if (element) { 94 + element.scrollIntoView({ behavior: "instant", block: "start" }); 95 + } 96 + } else if (isExternalRef()) { 97 + try { 98 + const [nsid, anchor] = props.refType!.split("#"); 99 + const authority = await resolveLexiconAuthority(nsid as Nsid); 100 + 101 + const hash = anchor ? `#schema:${anchor}` : "#schema"; 102 + navigate(`/at://${authority}/com.atproto.lexicon.schema/${nsid}${hash}`); 103 + } catch (err) { 104 + console.error("Failed to resolve lexicon authority:", err); 105 + } 106 + } 107 + }; 108 + 109 + return ( 110 + <> 111 + <Show when={props.refType}> 112 + <button 113 + type="button" 114 + onClick={handleClick} 115 + class="inline-block 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" 116 + > 117 + {displayType} 118 + </button> 119 + </Show> 120 + <Show when={!props.refType}> 121 + <span class="inline-block rounded bg-blue-100 px-1.5 py-0.5 font-mono text-xs text-blue-800 dark:bg-blue-900/30 dark:text-blue-300"> 122 + {displayType} 123 + </span> 124 + </Show> 125 + </> 126 + ); 127 + }; 128 + 129 + const UnionBadges = (props: { refs: string[] }) => ( 130 + <div class="flex flex-wrap gap-2"> 131 + <For each={props.refs}>{(refType) => <TypeBadge type="union" refType={refType} />}</For> 132 + </div> 133 + ); 134 + 135 + const ConstraintsList = (props: { property: LexiconProperty }) => ( 136 + <div class="flex flex-wrap gap-x-4 gap-y-1 text-xs text-neutral-500 dark:text-neutral-400"> 137 + <Show when={props.property.minLength !== undefined}> 138 + <span>minLength: {props.property.minLength}</span> 139 + </Show> 140 + <Show when={props.property.maxLength !== undefined}> 141 + <span>maxLength: {props.property.maxLength}</span> 142 + </Show> 143 + <Show when={props.property.maxGraphemes !== undefined}> 144 + <span>maxGraphemes: {props.property.maxGraphemes}</span> 145 + </Show> 146 + <Show when={props.property.minGraphemes !== undefined}> 147 + <span>minGraphemes: {props.property.minGraphemes}</span> 148 + </Show> 149 + <Show when={props.property.minimum !== undefined}> 150 + <span>min: {props.property.minimum}</span> 151 + </Show> 152 + <Show when={props.property.maximum !== undefined}> 153 + <span>max: {props.property.maximum}</span> 154 + </Show> 155 + <Show when={props.property.maxSize !== undefined}> 156 + <span>maxSize: {props.property.maxSize}</span> 157 + </Show> 158 + <Show when={props.property.accept}> 159 + <span>accept: [{props.property.accept!.join(", ")}]</span> 160 + </Show> 161 + <Show when={props.property.enum}> 162 + <span>enum: [{props.property.enum!.join(", ")}]</span> 163 + </Show> 164 + <Show when={props.property.const}> 165 + <span>const: {props.property.const?.toString()}</span> 166 + </Show> 167 + <Show when={props.property.default !== undefined}> 168 + <span>default: {JSON.stringify(props.property.default)}</span> 169 + </Show> 170 + <Show when={props.property.knownValues}> 171 + <span>knownValues: [{props.property.knownValues!.join(", ")}]</span> 172 + </Show> 173 + <Show when={props.property.closed}> 174 + <span>closed: true</span> 175 + </Show> 176 + </div> 177 + ); 178 + 179 + const PropertyRow = (props: { 180 + name: string; 181 + property: LexiconProperty; 182 + required?: boolean; 183 + hideNameType?: boolean; 184 + }) => { 185 + const hasConstraints = (property: LexiconProperty) => 186 + property.minLength !== undefined || 187 + property.maxLength !== undefined || 188 + property.maxGraphemes !== undefined || 189 + property.minGraphemes !== undefined || 190 + property.minimum !== undefined || 191 + property.maximum !== undefined || 192 + property.maxSize !== undefined || 193 + property.accept || 194 + property.enum || 195 + property.const || 196 + property.default !== undefined || 197 + property.knownValues || 198 + property.closed; 199 + 200 + return ( 201 + <div class="flex flex-col gap-2 py-3"> 202 + <Show when={!props.hideNameType}> 203 + <div class="flex flex-wrap items-center gap-2"> 204 + <span class="font-mono text-sm font-semibold">{props.name}</span> 205 + <Show when={!props.property.refs}> 206 + <TypeBadge 207 + type={props.property.type} 208 + format={props.property.format} 209 + refType={props.property.ref} 210 + /> 211 + </Show> 212 + <Show when={props.property.refs}> 213 + <span class="inline-block rounded bg-blue-100 px-1.5 py-0.5 font-mono text-xs text-blue-800 dark:bg-blue-900/30 dark:text-blue-300"> 214 + union 215 + </span> 216 + </Show> 217 + <Show when={props.required}> 218 + <span class="text-xs font-semibold text-red-500 dark:text-red-400">required</span> 219 + </Show> 220 + </div> 221 + </Show> 222 + <Show when={props.property.refs}> 223 + <UnionBadges refs={props.property.refs!} /> 224 + </Show> 225 + <Show when={hasConstraints(props.property)}> 226 + <ConstraintsList property={props.property} /> 227 + </Show> 228 + <Show when={props.property.items}> 229 + <div class="flex flex-col gap-2"> 230 + <div class="flex items-center gap-2 text-xs text-neutral-500 dark:text-neutral-400"> 231 + <span class="font-semibold">items:</span> 232 + <Show when={!props.property.items!.refs}> 233 + <TypeBadge 234 + type={props.property.items!.type} 235 + format={props.property.items!.format} 236 + refType={props.property.items!.ref} 237 + /> 238 + </Show> 239 + <Show when={props.property.items!.refs}> 240 + <span class="inline-block rounded bg-blue-100 px-1.5 py-0.5 font-mono text-xs text-blue-800 dark:bg-blue-900/30 dark:text-blue-300"> 241 + union 242 + </span> 243 + </Show> 244 + </div> 245 + <Show when={props.property.items!.refs}> 246 + <UnionBadges refs={props.property.items!.refs!} /> 247 + </Show> 248 + </div> 249 + </Show> 250 + <Show when={props.property.items && hasConstraints(props.property.items)}> 251 + <ConstraintsList property={props.property.items!} /> 252 + </Show> 253 + <Show when={props.property.description && !props.hideNameType}> 254 + <p class="text-sm text-neutral-700 dark:text-neutral-300">{props.property.description}</p> 255 + </Show> 256 + </div> 257 + ); 258 + }; 259 + 260 + const DefSection = (props: { name: string; def: LexiconDef }) => { 261 + const defTypeColor = () => { 262 + switch (props.def.type) { 263 + case "record": 264 + return "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300"; 265 + case "query": 266 + return "bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300"; 267 + case "procedure": 268 + return "bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300"; 269 + case "subscription": 270 + return "bg-pink-100 text-pink-800 dark:bg-pink-900/30 dark:text-pink-300"; 271 + case "object": 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 + } 278 + }; 279 + 280 + const hasDefContent = () => 281 + props.def.refs || 282 + props.def.minLength !== undefined || 283 + props.def.maxLength !== undefined || 284 + props.def.maxGraphemes !== undefined || 285 + props.def.minGraphemes !== undefined || 286 + props.def.minimum !== undefined || 287 + props.def.maximum !== undefined || 288 + props.def.maxSize !== undefined || 289 + props.def.accept || 290 + props.def.enum || 291 + props.def.const || 292 + props.def.default !== undefined || 293 + props.def.closed || 294 + props.def.items || 295 + props.def.knownValues; 296 + 297 + const handleHeaderClick = () => { 298 + window.history.replaceState(null, "", `#schema:${props.name}`); 299 + const element = document.getElementById(`def-${props.name}`); 300 + if (element) { 301 + element.scrollIntoView({ behavior: "instant", block: "start" }); 302 + } 303 + }; 304 + 305 + return ( 306 + <div class="flex flex-col gap-3" id={`def-${props.name}`}> 307 + <div class="flex items-center gap-2"> 308 + <button 309 + type="button" 310 + onClick={handleHeaderClick} 311 + class="cursor-pointer text-lg font-semibold hover:underline" 312 + > 313 + {props.name === "main" ? "Main Definition" : props.name} 314 + </button> 315 + <span class={`rounded px-2 py-0.5 text-xs font-semibold uppercase ${defTypeColor()}`}> 316 + {props.def.type} 317 + </span> 318 + </div> 319 + 320 + <Show when={props.def.description}> 321 + <p class="text-sm text-neutral-700 dark:text-neutral-300">{props.def.description}</p> 322 + </Show> 323 + 324 + {/* Record key */} 325 + <Show when={props.def.key}> 326 + <div> 327 + <span class="text-sm font-semibold">Record Key: </span> 328 + <span class="font-mono text-sm">{props.def.key}</span> 329 + </div> 330 + </Show> 331 + 332 + {/* Properties (for record/object types) */} 333 + <Show 334 + when={Object.keys(props.def.properties || props.def.record?.properties || {}).length > 0} 335 + > 336 + <div class="flex flex-col gap-2"> 337 + <h4 class="text-sm font-semibold text-neutral-600 uppercase dark:text-neutral-400"> 338 + Properties 339 + </h4> 340 + <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"> 341 + <For each={Object.entries(props.def.properties || props.def.record?.properties || {})}> 342 + {([name, property]) => ( 343 + <PropertyRow 344 + name={name} 345 + property={property} 346 + required={(props.def.required || props.def.record?.required || []).includes(name)} 347 + /> 348 + )} 349 + </For> 350 + </div> 351 + </div> 352 + </Show> 353 + 354 + {/* Parameters (for query/procedure) */} 355 + <Show 356 + when={ 357 + props.def.parameters?.properties && 358 + Object.keys(props.def.parameters.properties).length > 0 359 + } 360 + > 361 + <div class="flex flex-col gap-2"> 362 + <h4 class="text-sm font-semibold text-neutral-600 uppercase dark:text-neutral-400"> 363 + Parameters 364 + </h4> 365 + <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"> 366 + <For each={Object.entries(props.def.parameters!.properties!)}> 367 + {([name, property]) => ( 368 + <PropertyRow 369 + name={name} 370 + property={property} 371 + required={(props.def.parameters?.required || []).includes(name)} 372 + /> 373 + )} 374 + </For> 375 + </div> 376 + </div> 377 + </Show> 378 + 379 + {/* Input */} 380 + <Show when={props.def.input}> 381 + <div class="flex flex-col gap-2"> 382 + <h4 class="text-sm font-semibold text-neutral-600 uppercase dark:text-neutral-400"> 383 + Input 384 + </h4> 385 + <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"> 386 + <div class="text-sm"> 387 + <span class="font-semibold">Encoding: </span> 388 + <span class="font-mono">{props.def.input!.encoding}</span> 389 + </div> 390 + <Show when={props.def.input!.schema?.ref}> 391 + <div class="flex items-center gap-2"> 392 + <span class="text-sm font-semibold">Schema:</span> 393 + <TypeBadge type="ref" refType={props.def.input!.schema!.ref} /> 394 + </div> 395 + </Show> 396 + <Show when={props.def.input!.schema?.refs}> 397 + <div class="flex flex-col gap-2"> 398 + <div class="flex items-center gap-2"> 399 + <span class="text-sm font-semibold">Schema (union):</span> 400 + </div> 401 + <UnionBadges refs={props.def.input!.schema!.refs!} /> 402 + </div> 403 + </Show> 404 + <Show 405 + when={ 406 + props.def.input!.schema?.properties && 407 + Object.keys(props.def.input!.schema.properties).length > 0 408 + } 409 + > 410 + <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"> 411 + <For each={Object.entries(props.def.input!.schema!.properties!)}> 412 + {([name, property]) => ( 413 + <PropertyRow 414 + name={name} 415 + property={property} 416 + required={(props.def.input!.schema?.required || []).includes(name)} 417 + /> 418 + )} 419 + </For> 420 + </div> 421 + </Show> 422 + </div> 423 + </div> 424 + </Show> 425 + 426 + {/* Output */} 427 + <Show when={props.def.output}> 428 + <div class="flex flex-col gap-2"> 429 + <h4 class="text-sm font-semibold text-neutral-600 uppercase dark:text-neutral-400"> 430 + Output 431 + </h4> 432 + <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"> 433 + <div class="text-sm"> 434 + <span class="font-semibold">Encoding: </span> 435 + <span class="font-mono">{props.def.output!.encoding}</span> 436 + </div> 437 + <Show when={props.def.output!.schema?.ref}> 438 + <div class="flex items-center gap-2"> 439 + <span class="text-sm font-semibold">Schema:</span> 440 + <TypeBadge type="ref" refType={props.def.output!.schema!.ref} /> 441 + </div> 442 + </Show> 443 + <Show when={props.def.output!.schema?.refs}> 444 + <div class="flex flex-col gap-2"> 445 + <div class="flex items-center gap-2"> 446 + <span class="text-sm font-semibold">Schema (union):</span> 447 + </div> 448 + <UnionBadges refs={props.def.output!.schema!.refs!} /> 449 + </div> 450 + </Show> 451 + <Show 452 + when={ 453 + props.def.output!.schema?.properties && 454 + Object.keys(props.def.output!.schema.properties).length > 0 455 + } 456 + > 457 + <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"> 458 + <For each={Object.entries(props.def.output!.schema!.properties!)}> 459 + {([name, property]) => ( 460 + <PropertyRow 461 + name={name} 462 + property={property} 463 + required={(props.def.output!.schema?.required || []).includes(name)} 464 + /> 465 + )} 466 + </For> 467 + </div> 468 + </Show> 469 + </div> 470 + </div> 471 + </Show> 472 + 473 + {/* Errors */} 474 + <Show when={props.def.errors && props.def.errors.length > 0}> 475 + <div class="flex flex-col gap-2"> 476 + <h4 class="text-sm font-semibold text-neutral-600 uppercase dark:text-neutral-400"> 477 + Errors 478 + </h4> 479 + <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"> 480 + <For each={props.def.errors}> 481 + {(error) => ( 482 + <div class="flex flex-col gap-1 py-2"> 483 + <div class="font-mono text-sm font-semibold">{error.name}</div> 484 + <Show when={error.description}> 485 + <p class="text-sm text-neutral-700 dark:text-neutral-300"> 486 + {error.description} 487 + </p> 488 + </Show> 489 + </div> 490 + )} 491 + </For> 492 + </div> 493 + </div> 494 + </Show> 495 + 496 + {/* Other Definitions */} 497 + <Show 498 + when={ 499 + !( 500 + props.def.properties || 501 + props.def.parameters || 502 + props.def.input || 503 + props.def.output || 504 + props.def.errors || 505 + props.def.record 506 + ) && hasDefContent() 507 + } 508 + > 509 + <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"> 510 + <PropertyRow name={props.name} property={props.def} hideNameType /> 511 + </div> 512 + </Show> 513 + </div> 514 + ); 515 + }; 516 + 517 + export const LexiconSchemaView = (props: { schema: LexiconSchema }) => { 518 + const location = useLocation(); 519 + 520 + // Handle scrolling to a definition when hash is like #schema:definitionName 521 + createEffect(() => { 522 + const hash = location.hash; 523 + if (hash.startsWith("#schema:")) { 524 + const defName = hash.slice(8); 525 + const element = document.getElementById(`def-${defName}`); 526 + if (element) element.scrollIntoView({ behavior: "instant", block: "start" }); 527 + } 528 + }); 529 + 530 + return ( 531 + <div class="w-full max-w-4xl px-2"> 532 + {/* Header */} 533 + <div class="flex flex-col gap-2 border-b border-neutral-300 pb-4 dark:border-neutral-700"> 534 + <h2 class="text-lg font-semibold">{props.schema.id}</h2> 535 + <div class="flex gap-4 text-sm text-neutral-600 dark:text-neutral-400"> 536 + <span> 537 + <span class="font-semibold">Lexicon version: </span> 538 + <span class="font-mono">{props.schema.lexicon}</span> 539 + </span> 540 + </div> 541 + <Show when={props.schema.description}> 542 + <p class="text-sm text-neutral-700 dark:text-neutral-300">{props.schema.description}</p> 543 + </Show> 544 + </div> 545 + 546 + {/* Definitions */} 547 + <div class="flex flex-col gap-6 pt-4"> 548 + <For each={Object.entries(props.schema.defs)}> 549 + {([name, def]) => <DefSection name={name} def={def} />} 550 + </For> 551 + </div> 552 + </div> 553 + ); 554 + };
+60 -34
src/components/login.tsx
··· 1 import { Did } from "@atcute/lexicons"; 2 - import { isHandle } from "@atcute/lexicons/syntax"; 3 import { 4 configureOAuth, 5 createAuthorizationUrl, 6 - deleteStoredSession, 7 finalizeAuthorization, 8 getSession, 9 OAuthUserAgent, 10 - resolveFromIdentity, 11 - resolveFromService, 12 type Session, 13 } from "@atcute/oauth-browser-client"; 14 - import { createSignal } from "solid-js"; 15 - import { TextInput } from "./text-input"; 16 17 configureOAuth({ 18 metadata: { 19 client_id: import.meta.env.VITE_OAUTH_CLIENT_ID, 20 redirect_uri: import.meta.env.VITE_OAUTH_REDIRECT_URL, 21 }, 22 }); 23 24 export const [agent, setAgent] = createSignal<OAuthUserAgent | undefined>(); 25 26 const Login = () => { 27 const [notice, setNotice] = createSignal(""); 28 const [loginInput, setLoginInput] = createSignal(""); 29 30 const login = async (handle: string) => { 31 try { 32 if (!handle) return; 33 - let resolved; 34 - if (!isHandle(handle)) { 35 - setNotice(`Resolving your service...`); 36 - resolved = await resolveFromService(handle); 37 - } else { 38 - setNotice(`Resolving your identity...`); 39 - resolved = await resolveFromIdentity(handle); 40 - } 41 - 42 setNotice(`Contacting your data server...`); 43 const authUrl = await createAuthorizationUrl({ 44 scope: import.meta.env.VITE_OAUTH_SCOPE, 45 - ...resolved, 46 }); 47 48 setNotice(`Redirecting...`); ··· 56 }; 57 58 return ( 59 - <form class="flex flex-col gap-y-2" onsubmit={(e) => e.preventDefault()}> 60 - <div class="flex items-center gap-1"> 61 - <label for="handle" class="mr-1 flex items-center"> 62 - <span class="iconify lucide--user-round-plus text-lg"></span> 63 - </label> 64 - <TextInput 65 - id="handle" 66 placeholder="user.bsky.social" 67 onInput={(e) => setLoginInput(e.currentTarget.value)} 68 - class="grow" 69 /> 70 <button 71 onclick={() => login(loginInput())} 72 - class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 73 > 74 - <span class="iconify lucide--log-in text-lg"></span> 75 </button> 76 </div> 77 - <div>{notice()}</div> 78 </form> 79 ); 80 }; ··· 86 if (params.has("state") && (params.has("code") || params.has("error"))) { 87 history.replaceState(null, "", location.pathname + location.search); 88 89 - const session = await finalizeAuthorization(params); 90 - const did = session.info.sub; 91 92 localStorage.setItem("lastSignedIn", did); 93 - return session; 94 } else { 95 const lastSignedIn = localStorage.getItem("lastSignedIn"); 96 97 if (lastSignedIn) { 98 try { 99 - return await getSession(lastSignedIn as Did); 100 } catch (err) { 101 - deleteStoredSession(lastSignedIn as Did); 102 - localStorage.removeItem("lastSignedIn"); 103 throw err; 104 } 105 } 106 } 107 }; 108 109 - const session = await init().catch(() => {}); 110 111 if (session) setAgent(new OAuthUserAgent(session)); 112 };
··· 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...`); ··· 61 }; 62 63 return ( 64 + <form class="flex flex-col gap-y-2 px-1" onsubmit={(e) => e.preventDefault()}> 65 + <label for="handle" class="hidden"> 66 + Add account 67 + </label> 68 + <div class="dark:bg-dark-100 dark:shadow-dark-700 flex grow items-center gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 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="handle" 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="handle" 78 + class="grow py-1 select-none placeholder:text-sm focus:outline-none" 79 onInput={(e) => setLoginInput(e.currentTarget.value)} 80 /> 81 <button 82 onclick={() => login(loginInput())} 83 + class="flex items-center rounded-lg p-1 hover:bg-neutral-100 active:bg-neutral-200 dark:hover:bg-neutral-600 dark:active:bg-neutral-500" 84 > 85 + <span class="iconify lucide--log-in"></span> 86 </button> 87 </div> 88 + <Show when={notice()}> 89 + <div class="text-sm">{notice()}</div> 90 + </Show> 91 </form> 92 ); 93 }; ··· 99 if (params.has("state") && (params.has("code") || params.has("error"))) { 100 history.replaceState(null, "", location.pathname + location.search); 101 102 + const auth = await finalizeAuthorization(params); 103 + const did = auth.session.info.sub; 104 105 localStorage.setItem("lastSignedIn", did); 106 + 107 + const sessions = localStorage.getItem("sessions"); 108 + const newSessions: Sessions = sessions ? JSON.parse(sessions) : { [did]: {} }; 109 + newSessions[did] = { signedIn: true }; 110 + localStorage.setItem("sessions", JSON.stringify(newSessions)); 111 + return auth.session; 112 } else { 113 const lastSignedIn = localStorage.getItem("lastSignedIn"); 114 115 if (lastSignedIn) { 116 + const sessions = localStorage.getItem("sessions"); 117 + const newSessions: Sessions = sessions ? JSON.parse(sessions) : {}; 118 try { 119 + const session = await getSession(lastSignedIn as Did); 120 + const rpc = new Client({ handler: new OAuthUserAgent(session) }); 121 + const res = await rpc.get("com.atproto.server.getSession"); 122 + newSessions[lastSignedIn].signedIn = true; 123 + localStorage.setItem("sessions", JSON.stringify(newSessions)); 124 + if (!res.ok) throw res.data.error; 125 + return session; 126 } catch (err) { 127 + newSessions[lastSignedIn].signedIn = false; 128 + localStorage.setItem("sessions", JSON.stringify(newSessions)); 129 throw err; 130 } 131 } 132 } 133 }; 134 135 + const session = await init(); 136 137 if (session) setAgent(new OAuthUserAgent(session)); 138 };
+36 -14
src/components/modal.tsx
··· 1 - import { ComponentProps, onCleanup, onMount, Show } from "solid-js"; 2 3 export interface ModalProps extends Pick<ComponentProps<"svg">, "children"> { 4 open?: boolean; 5 onClose?: () => void; 6 closeOnClick?: boolean; 7 } 8 9 export const Modal = (props: ModalProps) => { 10 return ( 11 <Show when={props.open}> 12 - <dialog 13 ref={(node) => { 14 - onMount(() => { 15 - document.body.style.overflow = "hidden"; 16 - node.showModal(); 17 - (document.activeElement as any).blur(); 18 }); 19 - onCleanup(() => node.close()); 20 }} 21 onClick={(ev) => { 22 - if ((props.closeOnClick ?? true) && ev.target === ev.currentTarget) { 23 if (props.onClose) props.onClose(); 24 } 25 }} 26 - onClose={() => { 27 - document.body.style.overflow = "auto"; 28 - if (props.onClose) props.onClose(); 29 - }} 30 - class="h-full max-h-none w-full max-w-none bg-transparent text-neutral-900 backdrop:bg-transparent dark:text-neutral-200" 31 > 32 {props.children} 33 - </dialog> 34 </Show> 35 ); 36 };
··· 1 + import { ComponentProps, createEffect, onCleanup, Show } from "solid-js"; 2 3 export interface ModalProps extends Pick<ComponentProps<"svg">, "children"> { 4 open?: boolean; 5 onClose?: () => void; 6 closeOnClick?: boolean; 7 + nonBlocking?: boolean; 8 } 9 10 export const Modal = (props: ModalProps) => { 11 return ( 12 <Show when={props.open}> 13 + <div 14 + data-modal 15 + class="fixed inset-0 z-50 h-full max-h-none w-full max-w-none bg-transparent text-neutral-900 dark:text-neutral-200" 16 + classList={{ 17 + "pointer-events-none": props.nonBlocking, 18 + }} 19 ref={(node) => { 20 + const handleEscape = (e: KeyboardEvent) => { 21 + if (e.key === "Escape") { 22 + const modals = document.querySelectorAll("[data-modal]"); 23 + const lastModal = modals[modals.length - 1]; 24 + if (lastModal === node) { 25 + e.preventDefault(); 26 + e.stopPropagation(); 27 + if (props.onClose) props.onClose(); 28 + } 29 + } 30 + }; 31 + 32 + createEffect(() => { 33 + if (!props.nonBlocking) document.body.style.overflow = "hidden"; 34 + else document.body.style.overflow = "auto"; 35 }); 36 + 37 + document.addEventListener("keydown", handleEscape); 38 + 39 + onCleanup(() => { 40 + document.body.style.overflow = "auto"; 41 + document.removeEventListener("keydown", handleEscape); 42 + }); 43 }} 44 onClick={(ev) => { 45 + if ( 46 + (props.closeOnClick ?? true) && 47 + ev.target === ev.currentTarget && 48 + !props.nonBlocking 49 + ) { 50 if (props.onClose) props.onClose(); 51 } 52 }} 53 > 54 {props.children} 55 + </div> 56 </Show> 57 ); 58 };
+128 -200
src/components/navbar.tsx
··· 1 - import { Did, Handle } from "@atcute/lexicons"; 2 - import { A, Params, useLocation } from "@solidjs/router"; 3 import { createEffect, createSignal, Show } from "solid-js"; 4 - import { didDocCache, labelerCache, validateHandle } from "../utils/api"; 5 - import { CopyMenu, DropdownMenu, MenuProvider } from "./dropdown"; 6 import Tooltip from "./tooltip"; 7 8 export const [pds, setPDS] = createSignal<string>(); 9 - export const [cid, setCID] = createSignal<string>(); 10 - export const [isLabeler, setIsLabeler] = createSignal(false); 11 - export const [validRecord, setValidRecord] = createSignal<boolean | undefined>(undefined); 12 - export const [validSchema, setValidSchema] = createSignal<boolean | undefined>(undefined); 13 14 - const swapIcons: Record<string, string> = { 15 - "did:plc:vwzwgnygau7ed7b7wt5ux7y2": "lucide--microchip", 16 - "did:plc:oisofpd7lj26yvgiivf3lxsi": "lucide--bone", 17 - "did:plc:uu5axsmbm2or2dngy4gwchec": "lucide--train-track", 18 - "did:plc:7x6rtuenkuvxq3zsvffp2ide": "lucide--rabbit", 19 - "did:plc:ia76kvnndjutgedggx2ibrem": "lucide--rabbit", 20 - "did:plc:hvakvedv6byxhufjl23mfmsd": "lucide--rat", 21 - "did:plc:ezhjhbzqt32bqprrn6qjlkri": "lucide--film", 22 - "did:plc:6v6jqsy7swpzuu53rmzaybjy": "lucide--fish", 23 - "did:plc:hx53snho72xoj7zqt5uice4u": "lucide--rose", 24 - "did:plc:wzsilnxf24ehtmmc3gssy5bu": "lucide--music-2", 25 - "did:plc:bnqkww7bjxaacajzvu5gswdf": "lucide--gem", 26 - "did:plc:hdhoaan3xa3jiuq4fg4mefid": "lucide--sparkles", 27 }; 28 29 - const NavBar = (props: { params: Params }) => { 30 - const location = useLocation(); 31 const [handle, setHandle] = createSignal(props.params.repo); 32 - const [validHandle, setValidHandle] = createSignal<boolean | undefined>(undefined); 33 - const [fullCid, setFullCid] = createSignal(false); 34 const [showHandle, setShowHandle] = createSignal(localStorage.showHandle === "true"); 35 36 createEffect(() => { 37 - if (cid() !== undefined) setFullCid(false); 38 - }); 39 - 40 - createEffect(async () => { 41 if (pds() !== undefined && props.params.repo) { 42 const hdl = 43 didDocCache[props.params.repo]?.alsoKnownAs 44 ?.filter((alias) => alias.startsWith("at://"))[0] 45 .split("at://")[1] ?? props.params.repo; 46 - if (hdl !== handle()) { 47 - setValidHandle(undefined); 48 - setHandle(hdl); 49 - setValidHandle(await validateHandle(hdl as Handle, props.params.repo as Did)); 50 - } 51 } 52 }); 53 54 return ( 55 - <nav class="flex w-[22rem] flex-col text-sm wrap-anywhere sm:w-[24rem]"> 56 - <div class="relative flex items-center justify-between gap-1"> 57 - <div class="flex min-h-[1.25rem] basis-full items-center gap-2"> 58 <Tooltip text="PDS"> 59 - <span class="iconify lucide--hard-drive shrink-0 text-lg"></span> 60 </Tooltip> 61 <Show when={pds()}> 62 - <Show when={props.params.repo}> 63 - <A 64 - end 65 - href={pds()!} 66 - inactiveClass="text-blue-400 w-full hover:underline active:underline" 67 - > 68 - {pds()} 69 - </A> 70 - </Show> 71 - <Show when={!props.params.repo}> 72 - <span>{pds()}</span> 73 </Show> 74 </Show> 75 </div> 76 - <MenuProvider> 77 - <DropdownMenu 78 - icon="lucide--copy text-base" 79 - buttonClass="rounded p-0.5" 80 - menuClass="top-6 p-2 text-xs" 81 - > 82 - <Show when={pds()}> 83 - <CopyMenu copyContent={pds()!} label="Copy PDS" /> 84 - </Show> 85 - <Show when={props.params.repo}> 86 - <CopyMenu copyContent={props.params.repo} label="Copy DID" /> 87 - <CopyMenu 88 - copyContent={`at://${props.params.repo}${props.params.collection ? `/${props.params.collection}` : ""}${props.params.rkey ? `/${props.params.rkey}` : ""}`} 89 - label="Copy AT URI" 90 - /> 91 - </Show> 92 - <Show when={props.params.rkey && cid()}> 93 - <CopyMenu copyContent={cid()!} label="Copy CID" /> 94 - </Show> 95 - </DropdownMenu> 96 - </MenuProvider> 97 </div> 98 - <div class="flex flex-col flex-wrap"> 99 <Show when={props.params.repo}> 100 - <div class="relative mt-1 flex items-center justify-between gap-1"> 101 <div class="flex basis-full items-center gap-2"> 102 <Tooltip text="Repository"> 103 - <span class="iconify lucide--book-user text-lg"></span> 104 </Tooltip> 105 - <div class="flex w-full gap-1"> 106 - {props.params.collection || location.pathname.includes("/labels") ? 107 - <A 108 - end 109 - href={`/at://${props.params.repo}`} 110 - inactiveClass={`text-blue-400 hover:underline active:underline ${!showHandle() ? "w-full" : ""}`} 111 - > 112 - {showHandle() ? handle() : props.params.repo} 113 - </A> 114 - : <span>{showHandle() ? handle() : props.params.repo}</span>} 115 - <Show when={showHandle()}> 116 - <Tooltip 117 - text={ 118 - validHandle() === true ? "Valid handle" 119 - : validHandle() === undefined ? 120 - "Validating" 121 - : "Invalid handle" 122 - } 123 - > 124 - <span 125 - classList={{ 126 - "iconify lucide--circle-check": validHandle() === true, 127 - "iconify lucide--circle-x text-red-500 dark:text-red-400": 128 - validHandle() === false, 129 - "iconify lucide--loader-circle animate-spin": validHandle() === undefined, 130 - }} 131 - ></span> 132 - </Tooltip> 133 - </Show> 134 - </div> 135 </div> 136 - <Tooltip text={showHandle() ? "Show DID" : "Show handle"}> 137 - <button 138 - class="flex items-center rounded p-0.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 139 - onclick={() => { 140 - localStorage.showHandle = !showHandle(); 141 - setShowHandle(!showHandle()); 142 - }} 143 - > 144 - <span 145 - class={ 146 - `iconify shrink-0 text-base transition-transform duration-400 ${showHandle() ? "rotate-y-180" : ""} ` + 147 - (swapIcons[props.params.repo] ?? "lucide--arrow-left-right") 148 - } 149 - ></span> 150 - </button> 151 - </Tooltip> 152 - </div> 153 - </Show> 154 - <Show 155 - when={ 156 - !props.params.collection && 157 - (props.params.repo in labelerCache || location.pathname.endsWith("/labels")) 158 - } 159 - > 160 - <div class="mt-1 flex items-center gap-2"> 161 - <span class="iconify lucide--tag text-lg"></span> 162 - <A 163 - end 164 - href={`/at://${props.params.repo}/labels`} 165 - inactiveClass="text-blue-400 grow hover:underline active:underline" 166 - > 167 - labels 168 - </A> 169 </div> 170 </Show> 171 <Show when={props.params.collection}> 172 - <div class="mt-1 flex items-center gap-2"> 173 - <Tooltip text="Collection"> 174 - <span class="iconify lucide--folder-open text-lg"></span> 175 - </Tooltip> 176 - <Show when={props.params.rkey}> 177 - <A 178 - end 179 - href={`/at://${props.params.repo}/${props.params.collection}`} 180 - inactiveClass="text-blue-400 w-full hover:underline active:underline" 181 > 182 - {props.params.collection} 183 - </A> 184 - </Show> 185 - <Show when={!props.params.rkey}> 186 - <span>{props.params.collection}</span> 187 - </Show> 188 </div> 189 </Show> 190 <Show when={props.params.rkey}> 191 - <div class="mt-1 flex items-center gap-2"> 192 - <Tooltip text="Record"> 193 - <span class="iconify lucide--file-json text-lg"></span> 194 - </Tooltip> 195 - <div class="flex gap-1"> 196 - <span>{props.params.rkey}</span> 197 - <Show when={validRecord()}> 198 - <Tooltip text="Valid record"> 199 - <span class="iconify lucide--lock-keyhole"></span> 200 - </Tooltip> 201 - </Show> 202 - <Show when={validRecord() === false}> 203 - <Tooltip text="Invalid record"> 204 - <span class="iconify lucide--lock-keyhole-open text-red-500 dark:text-red-400"></span> 205 - </Tooltip> 206 - </Show> 207 - <Show when={validRecord() === undefined}> 208 - <Tooltip text="Validating"> 209 - <span class="iconify lucide--loader-circle animate-spin"></span> 210 - </Tooltip> 211 - </Show> 212 - <Show when={validSchema()}> 213 - <Tooltip text="Valid schema"> 214 - <span class="iconify lucide--file-check"></span> 215 - </Tooltip> 216 - </Show> 217 - <Show when={validSchema() === false}> 218 - <Tooltip text="Invalid schema"> 219 - <span class="iconify lucide--file-x text-red-500 dark:text-red-400"></span> 220 - </Tooltip> 221 - </Show> 222 </div> 223 </div> 224 </Show> 225 </div> 226 - <Show when={props.params.rkey && cid()}> 227 - {(cid) => ( 228 - <div class="mt-1 flex gap-2"> 229 - <Tooltip text="CID"> 230 - <span class="iconify lucide--box text-lg"></span> 231 - </Tooltip> 232 - <button 233 - dir="rtl" 234 - classList={{ "bg-transparent text-left": true, truncate: !fullCid() }} 235 - onclick={() => setFullCid(!fullCid())} 236 - > 237 - {cid()} 238 - </button> 239 - </div> 240 - )} 241 - </Show> 242 </nav> 243 ); 244 }; 245 - 246 - export { NavBar };
··· 1 + import { A, Params } from "@solidjs/router"; 2 import { createEffect, createSignal, Show } from "solid-js"; 3 + import { isTouchDevice } from "../layout"; 4 + import { didDocCache } from "../utils/api"; 5 + import { addToClipboard } from "../utils/copy"; 6 import Tooltip from "./tooltip"; 7 8 export const [pds, setPDS] = createSignal<string>(); 9 10 + const CopyButton = (props: { content: string; label: string }) => { 11 + return ( 12 + <Show when={!isTouchDevice}> 13 + <Tooltip text={props.label}> 14 + <button 15 + type="button" 16 + onclick={(e) => { 17 + e.preventDefault(); 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> 25 + </button> 26 + </Tooltip> 27 + </Show> 28 + ); 29 }; 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) { 37 const hdl = 38 didDocCache[props.params.repo]?.alsoKnownAs 39 ?.filter((alias) => alias.startsWith("at://"))[0] 40 .split("at://")[1] ?? props.params.repo; 41 + if (hdl !== handle()) setHandle(hdl); 42 } 43 }); 44 45 return ( 46 + <nav class="flex w-full flex-col text-sm wrap-anywhere sm:text-base"> 47 + {/* PDS Level */} 48 + <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"> 49 + <div class="flex min-h-6 basis-full items-center gap-2 sm:min-h-7"> 50 <Tooltip text="PDS"> 51 + <span 52 + classList={{ 53 + "iconify shrink-0 transition-colors duration-200": true, 54 + "lucide--alert-triangle text-red-500 dark:text-red-400": pds() === "Missing PDS", 55 + "lucide--hard-drive text-neutral-500 group-hover:text-neutral-700 dark:text-neutral-400 dark:group-hover:text-neutral-200": 56 + pds() !== "Missing PDS", 57 + }} 58 + ></span> 59 </Tooltip> 60 <Show when={pds()}> 61 + <Show 62 + when={pds() === "Missing PDS"} 63 + fallback={ 64 + <Show 65 + when={props.params.repo} 66 + fallback={<span class="py-0.5 font-medium">{pds()}</span>} 67 + > 68 + <A 69 + end 70 + href={pds()!} 71 + inactiveClass="text-blue-400 py-0.5 w-full font-medium hover:text-blue-500 transition-colors duration-150 dark:hover:text-blue-300" 72 + > 73 + {pds()} 74 + </A> 75 + </Show> 76 + } 77 + > 78 + <span class="py-0.5 font-medium text-red-500 dark:text-red-400">{pds()}</span> 79 </Show> 80 </Show> 81 </div> 82 + <Show when={pds() && pds() !== "Missing PDS"}> 83 + <CopyButton content={pds()!} label="Copy PDS" /> 84 + </Show> 85 </div> 86 + 87 + <div class="flex flex-col"> 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 + 129 + {/* Collection Level */} 130 <Show when={props.params.collection}> 131 + <div class="group flex items-center justify-between gap-2 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"> 132 + <div class="flex basis-full items-center gap-2"> 133 + <Tooltip text="Collection"> 134 + <span class="iconify lucide--folder-open text-neutral-500 transition-colors duration-200 group-hover:text-neutral-700 dark:text-neutral-400 dark:group-hover:text-neutral-200"></span> 135 + </Tooltip> 136 + <Show 137 + when={props.params.rkey} 138 + fallback={<span class="py-0.5 font-medium">{props.params.collection}</span>} 139 > 140 + <A 141 + end 142 + href={`/at://${props.params.repo}/${props.params.collection}`} 143 + inactiveClass="text-blue-400 grow py-0.5 font-medium hover:text-blue-500 transition-colors duration-150 dark:hover:text-blue-300" 144 + > 145 + {props.params.collection} 146 + </A> 147 + </Show> 148 + </div> 149 + <CopyButton 150 + content={`at://${props.params.repo}/${props.params.collection}`} 151 + label="Copy AT URI" 152 + /> 153 </div> 154 </Show> 155 + 156 + {/* Record Level */} 157 <Show when={props.params.rkey}> 158 + <div class="group flex items-center justify-between gap-2 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"> 159 + <div class="flex basis-full items-center gap-2"> 160 + <Tooltip text="Record"> 161 + <span class="iconify lucide--file-json text-neutral-500 transition-colors duration-200 group-hover:text-neutral-700 dark:text-neutral-400 dark:group-hover:text-neutral-200"></span> 162 + </Tooltip> 163 + <span class="py-0.5 font-medium">{props.params.rkey}</span> 164 </div> 165 + <CopyButton 166 + content={`at://${props.params.repo}/${props.params.collection}/${props.params.rkey}`} 167 + label="Copy AT URI" 168 + /> 169 </div> 170 </Show> 171 </div> 172 </nav> 173 ); 174 };
+91
src/components/notification.tsx
···
··· 1 + import { createSignal, For, Show } from "solid-js"; 2 + 3 + export type Notification = { 4 + id: string; 5 + message: string; 6 + progress?: number; 7 + total?: number; 8 + type?: "info" | "success" | "error"; 9 + }; 10 + 11 + const [notifications, setNotifications] = createSignal<Notification[]>([]); 12 + const [removingIds, setRemovingIds] = createSignal<Set<string>>(new Set()); 13 + 14 + export const addNotification = (notification: Omit<Notification, "id">) => { 15 + const id = `notification-${Date.now()}-${Math.random()}`; 16 + setNotifications([...notifications(), { ...notification, id }]); 17 + return id; 18 + }; 19 + 20 + export const updateNotification = (id: string, updates: Partial<Notification>) => { 21 + setNotifications(notifications().map((n) => (n.id === id ? { ...n, ...updates } : n))); 22 + }; 23 + 24 + export const removeNotification = (id: string) => { 25 + setRemovingIds(new Set([...removingIds(), id])); 26 + setTimeout(() => { 27 + setNotifications(notifications().filter((n) => n.id !== id)); 28 + setRemovingIds((ids) => { 29 + const newIds = new Set(ids); 30 + newIds.delete(id); 31 + return newIds; 32 + }); 33 + }, 250); 34 + }; 35 + 36 + export const NotificationContainer = () => { 37 + return ( 38 + <div class="pointer-events-none fixed bottom-4 left-4 z-50 flex flex-col gap-2"> 39 + <For each={notifications()}> 40 + {(notification) => ( 41 + <div 42 + class="dark:bg-dark-300 dark:shadow-dark-700 pointer-events-auto flex min-w-64 flex-col gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-3 shadow-md select-none dark:border-neutral-700" 43 + classList={{ 44 + "border-blue-500 dark:border-blue-400": notification.type === "info", 45 + "border-green-500 dark:border-green-400": notification.type === "success", 46 + "border-red-500 dark:border-red-400": notification.type === "error", 47 + "animate-[slideIn_0.25s_ease-in]": !removingIds().has(notification.id), 48 + "animate-[slideOut_0.25s_ease-in]": removingIds().has(notification.id), 49 + }} 50 + onClick={() => removeNotification(notification.id)} 51 + > 52 + <div class="flex items-center gap-2 text-sm"> 53 + <Show when={notification.progress !== undefined}> 54 + <span class="iconify lucide--download" /> 55 + </Show> 56 + <Show when={notification.type === "success"}> 57 + <span class="iconify lucide--check-circle text-green-600 dark:text-green-400" /> 58 + </Show> 59 + <Show when={notification.type === "error"}> 60 + <span class="iconify lucide--x-circle text-red-500 dark:text-red-400" /> 61 + </Show> 62 + <span>{notification.message}</span> 63 + </div> 64 + <Show when={notification.progress !== undefined}> 65 + <div class="flex flex-col gap-1"> 66 + <Show 67 + when={notification.total !== undefined && notification.total > 0} 68 + fallback={ 69 + <div class="text-xs text-neutral-600 dark:text-neutral-400"> 70 + {notification.progress} MB 71 + </div> 72 + } 73 + > 74 + <div class="h-2 w-full overflow-hidden rounded-full bg-neutral-200 dark:bg-neutral-700"> 75 + <div 76 + class="h-full rounded-full bg-blue-500 transition-all dark:bg-blue-400" 77 + style={{ width: `${notification.progress}%` }} 78 + /> 79 + </div> 80 + <div class="text-xs text-neutral-600 dark:text-neutral-400"> 81 + {notification.progress}% 82 + </div> 83 + </Show> 84 + </div> 85 + </Show> 86 + </div> 87 + )} 88 + </For> 89 + </div> 90 + ); 91 + };
+172 -28
src/components/search.tsx
··· 1 - import { useLocation, useNavigate } from "@solidjs/router"; 2 - import { createSignal, onCleanup, onMount, Show } from "solid-js"; 3 import { isTouchDevice } from "../layout"; 4 5 export const [showSearch, setShowSearch] = createSignal(false); 6 ··· 9 onCleanup(() => window.removeEventListener("keydown", keyEvent)); 10 11 const keyEvent = (ev: KeyboardEvent) => { 12 - if (document.querySelector("dialog")) return; 13 14 if ((ev.ctrlKey || ev.metaKey) && ev.key == "k") { 15 ev.preventDefault(); ··· 23 return ( 24 <button 25 onclick={() => setShowSearch(!showSearch())} 26 - class={`flex items-center gap-0.5 rounded-lg ${isTouchDevice ? "p-1 text-xl hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" : "dark:bg-dark-200 bg-neutral-200 p-1.5 text-xs hover:bg-neutral-300/80 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"}`} 27 > 28 <span class="iconify lucide--search"></span> 29 <Show when={!isTouchDevice}> ··· 38 const Search = () => { 39 const navigate = useNavigate(); 40 let searchInput!: HTMLInputElement; 41 42 onMount(() => { 43 - if (useLocation().pathname !== "/") searchInput.focus(); 44 }); 45 46 - const processInput = (input: string) => { 47 input = input.trim().replace(/^@/, ""); 48 if (!input.length) return; 49 setShowSearch(false); 50 - if ( 51 - !input.startsWith("https://bsky.app/") && 52 - (input.startsWith("https://") || input.startsWith("http://")) 53 - ) { 54 - navigate(`/${input.replace("https://", "").replace("http://", "").replace("/", "")}`); 55 - return; 56 - } 57 58 - const uri = input 59 - .replace("at://", "") 60 - .replace("https://bsky.app/profile/", "") 61 - .replace("/post/", "/app.bsky.feed.post/"); 62 - const uriParts = uri.split("/"); 63 - navigate(`/at://${uriParts[0]}${uriParts.length > 1 ? `/${uriParts.slice(1).join("/")}` : ""}`); 64 }; 65 66 return ( 67 <form 68 - class="w-[22rem] sm:w-[24rem]" 69 onsubmit={(e) => { 70 e.preventDefault(); 71 processInput(searchInput.value); 72 }} 73 > 74 <label for="input" class="hidden"> 75 - PDS URL, AT URI, or handle 76 </label> 77 - <div class="dark:bg-dark-100 dark:shadow-dark-800 flex items-center gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 py-1 shadow-xs focus-within:outline-[1.5px] focus-within:outline-neutral-900 dark:border-neutral-700 dark:focus-within:outline-neutral-200"> 78 <input 79 type="text" 80 spellcheck={false} 81 - placeholder="PDS URL, AT URI, or handle" 82 ref={searchInput} 83 id="input" 84 - class="grow select-none placeholder:text-sm focus:outline-none" 85 /> 86 - <button 87 - type="submit" 88 - class="iconify lucide--arrow-right text-lg text-neutral-500 dark:text-neutral-400" 89 - ></button> 90 </div> 91 </form> 92 ); 93 }; 94
··· 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"; 8 + import { createDebouncedValue } from "../utils/hooks/debounced"; 9 + import { Modal } from "./modal"; 10 11 export const [showSearch, setShowSearch] = createSignal(false); 12 ··· 15 onCleanup(() => window.removeEventListener("keydown", keyEvent)); 16 17 const keyEvent = (ev: KeyboardEvent) => { 18 + if (document.querySelector("[data-modal]")) return; 19 20 if ((ev.ctrlKey || ev.metaKey) && ev.key == "k") { 21 ev.preventDefault(); ··· 29 return ( 30 <button 31 onclick={() => setShowSearch(!showSearch())} 32 + class={`flex items-center gap-0.5 rounded-lg ${isTouchDevice ? "p-1 text-xl hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" : "dark:bg-dark-100/70 box-border h-7 border-[0.5px] border-neutral-300 bg-neutral-100/70 p-1.5 text-xs hover:bg-neutral-200 active:bg-neutral-300 dark:border-neutral-600 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"}`} 33 > 34 <span class="iconify lucide--search"></span> 35 <Show when={!isTouchDevice}> ··· 44 const Search = () => { 45 const navigate = useNavigate(); 46 let searchInput!: HTMLInputElement; 47 + const rpc = new Client({ 48 + handler: new CredentialManager({ service: "https://public.api.bsky.app" }), 49 + }); 50 51 onMount(() => { 52 + if (!isTouchDevice || useLocation().pathname !== "/") searchInput.focus(); 53 }); 54 55 + const fetchTypeahead = async (input: string) => { 56 + if (!input.length) return []; 57 + const res = await rpc.get("app.bsky.actor.searchActorsTypeahead", { 58 + params: { q: input, limit: 5 }, 59 + }); 60 + if (res.ok) { 61 + return res.data.actors; 62 + } 63 + return []; 64 + }; 65 + 66 + const [input, setInput] = createSignal<string>(); 67 + const [selectedIndex, setSelectedIndex] = createSignal(-1); 68 + const [search] = createResource(createDebouncedValue(input, 250), fetchTypeahead); 69 + 70 + const processInput = async (input: string) => { 71 input = input.trim().replace(/^@/, ""); 72 if (!input.length) return; 73 + const index = selectedIndex() >= 0 ? selectedIndex() : 0; 74 setShowSearch(false); 75 + setInput(undefined); 76 + if (search()?.length && selectedIndex() !== -1) { 77 + navigate(`/at://${search()![index].did}`); 78 + } else if (input.startsWith("https://") || input.startsWith("http://")) { 79 + const hostLength = input.indexOf("/", 8); 80 + const host = input.slice(0, hostLength).replace("https://", "").replace("http://", ""); 81 82 + if (!(host in appList)) { 83 + navigate(`/${input.replace("https://", "").replace("http://", "").replace("/", "")}`); 84 + } else { 85 + const app = appList[host as AppUrl]; 86 + const path = input.slice(hostLength + 1).split("/"); 87 + 88 + const uri = appHandleLink[app](path); 89 + navigate(`/${uri}`); 90 + } 91 + } else if (input.startsWith("lex:")) { 92 + const nsid = input.replace("lex:", "") as Nsid; 93 + const res = await resolveLexiconAuthority(nsid); 94 + navigate(`/at://${res}/com.atproto.lexicon.schema/${nsid}`); 95 + } else { 96 + navigate(`/at://${input.replace("at://", "")}`); 97 + } 98 + setSelectedIndex(-1); 99 }; 100 101 return ( 102 <form 103 + class="relative w-full" 104 onsubmit={(e) => { 105 e.preventDefault(); 106 processInput(searchInput.value); 107 }} 108 > 109 <label for="input" class="hidden"> 110 + PDS URL, AT URI, NSID, DID, or handle 111 </label> 112 + <div class="dark:bg-dark-100 dark:shadow-dark-700 flex items-center gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 shadow-xs focus-within:outline-[1px] focus-within:outline-neutral-600 dark:border-neutral-600 dark:focus-within:outline-neutral-400"> 113 + <label 114 + for="input" 115 + class="iconify lucide--search text-neutral-500 dark:text-neutral-400" 116 + ></label> 117 <input 118 type="text" 119 spellcheck={false} 120 + placeholder="PDS, AT URI, NSID, DID, or handle" 121 ref={searchInput} 122 id="input" 123 + class="grow py-1 select-none placeholder:text-sm focus:outline-none" 124 + value={input() ?? ""} 125 + onInput={(e) => { 126 + setInput(e.currentTarget.value); 127 + setSelectedIndex(-1); 128 + }} 129 + onKeyDown={(e) => { 130 + const results = search(); 131 + if (!results?.length) return; 132 + 133 + if (e.key === "ArrowDown") { 134 + e.preventDefault(); 135 + setSelectedIndex((prev) => (prev === -1 ? 0 : (prev + 1) % results.length)); 136 + } else if (e.key === "ArrowUp") { 137 + e.preventDefault(); 138 + setSelectedIndex((prev) => 139 + prev === -1 ? results.length - 1 : (prev - 1 + results.length) % results.length, 140 + ); 141 + } 142 + }} 143 /> 144 + <Show when={input()} fallback={ListUrlsTooltip()}> 145 + <button 146 + type="button" 147 + class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-600 dark:active:bg-neutral-500" 148 + onClick={() => setInput(undefined)} 149 + > 150 + <span class="iconify lucide--x"></span> 151 + </button> 152 + </Show> 153 </div> 154 + <Show when={search()?.length && input()}> 155 + <div class="dark:bg-dark-300 dark:shadow-dark-700 absolute z-30 mt-1 flex w-full flex-col rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 shadow-md transition-opacity duration-200 dark:border-neutral-700 starting:opacity-0"> 156 + <For each={search()}> 157 + {(actor, index) => ( 158 + <A 159 + class={`flex items-center gap-2 rounded-lg p-1 transition-colors duration-150 ${ 160 + index() === selectedIndex() ? 161 + "bg-neutral-200 dark:bg-neutral-700" 162 + : "hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 163 + }`} 164 + href={`/at://${actor.did}`} 165 + onClick={() => setShowSearch(false)} 166 + > 167 + <img 168 + src={actor.avatar?.replace("img/avatar/", "img/avatar_thumbnail/")} 169 + class="size-8 rounded-full" 170 + /> 171 + <span>{actor.handle}</span> 172 + </A> 173 + )} 174 + </For> 175 + </div> 176 + </Show> 177 </form> 178 + ); 179 + }; 180 + 181 + const ListUrlsTooltip = () => { 182 + const [openList, setOpenList] = createSignal(false); 183 + 184 + let urls: Record<string, AppUrl[]> = {}; 185 + for (const [appUrl, appView] of Object.entries(appList)) { 186 + if (!urls[appView]) urls[appView] = [appUrl as AppUrl]; 187 + else urls[appView].push(appUrl as AppUrl); 188 + } 189 + 190 + return ( 191 + <> 192 + <Modal open={openList()} onClose={() => setOpenList(false)}> 193 + <div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-16 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 sm:w-104 dark:border-neutral-700 starting:opacity-0"> 194 + <div class="mb-2 flex items-center gap-1 font-semibold"> 195 + <span class="iconify lucide--link"></span> 196 + <span>Supported URLs</span> 197 + </div> 198 + <div class="mb-2 text-sm text-neutral-600 dark:text-neutral-400"> 199 + Links that will be parsed automatically, as long as all the data necessary is on the 200 + URL. 201 + </div> 202 + <div class="flex flex-col gap-2 text-sm"> 203 + <For each={Object.entries(appName)}> 204 + {([appView, name]) => { 205 + return ( 206 + <div> 207 + <p class="font-semibold">{name}</p> 208 + <div class="grid grid-cols-2 gap-x-4 text-neutral-600 dark:text-neutral-400"> 209 + <For each={urls[appView]}> 210 + {(url) => ( 211 + <a 212 + href={`${url.startsWith("localhost:") ? "http://" : "https://"}${url}`} 213 + target="_blank" 214 + class="hover:underline active:underline" 215 + > 216 + {url} 217 + </a> 218 + )} 219 + </For> 220 + </div> 221 + </div> 222 + ); 223 + }} 224 + </For> 225 + </div> 226 + </div> 227 + </Modal> 228 + <button 229 + type="button" 230 + class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-600 dark:active:bg-neutral-500" 231 + onClick={() => setOpenList(true)} 232 + > 233 + <span class="iconify lucide--help-circle"></span> 234 + </button> 235 + </> 236 ); 237 }; 238
+1 -1
src/components/sticky.tsx
··· 29 /> 30 31 <div 32 - class="sticky top-2 z-10 flex 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(),
··· 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(),
+2 -2
src/components/text-input.tsx
··· 1 export interface TextInputProps { 2 - ref?: HTMLInputElement; 3 class?: string; 4 id?: string; 5 type?: "text" | "email" | "password" | "search" | "tel" | "url"; ··· 25 disabled={props.disabled} 26 required={props.required} 27 class={ 28 - "dark:bg-dark-100 dark:shadow-dark-800 rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 py-1 shadow-xs select-none placeholder:text-sm focus:outline-[1.5px] focus:outline-neutral-900 dark:border-neutral-700 dark:focus:outline-neutral-200 " + 29 props.class 30 } 31 onInput={props.onInput}
··· 1 export interface TextInputProps { 2 + ref?: HTMLInputElement | ((el: HTMLInputElement) => void); 3 class?: string; 4 id?: string; 5 type?: "text" | "email" | "password" | "search" | "tel" | "url"; ··· 25 disabled={props.disabled} 26 required={props.required} 27 class={ 28 + "dark:bg-dark-100 dark:shadow-dark-700 rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 py-1 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}
+1 -1
src/components/tooltip.tsx
··· 7 <Show when={!isTouchDevice}> 8 <span 9 style={`transform: translate(-50%, 28px)`} 10 - class={`dark:shadow-dark-800 pointer-events-none absolute left-[50%] z-20 hidden min-w-fit rounded border-[0.5px] border-neutral-300 bg-neutral-50 p-1 text-center font-sans text-xs whitespace-nowrap text-neutral-900 shadow-md select-none group-hover/tooltip:inline first-letter:capitalize dark:border-neutral-600 dark:bg-neutral-800 dark:text-neutral-200`} 11 > 12 {props.text} 13 </span>
··· 7 <Show when={!isTouchDevice}> 8 <span 9 style={`transform: translate(-50%, 28px)`} 10 + class={`dark:shadow-dark-700 pointer-events-none absolute left-[50%] z-20 hidden min-w-fit rounded border-[0.5px] border-neutral-300 bg-neutral-50 p-1 text-center font-sans text-xs whitespace-nowrap text-neutral-900 shadow-md select-none group-hover/tooltip:inline first-letter:capitalize dark:border-neutral-600 dark:bg-neutral-800 dark:text-neutral-200`} 11 > 12 {props.text} 13 </span>
+17 -69
src/components/video-player.tsx
··· 1 - // courtesy of the best ๐Ÿ‡, my lovely sister mary 2 - import Hls from "hls.js"; 3 - import { createEffect, createSignal, onCleanup, Show } from "solid-js"; 4 5 export interface VideoPlayerProps { 6 - /** Expected to be static */ 7 did: string; 8 cid: string; 9 } 10 11 - const VideoPlayer = ({ did, cid }: VideoPlayerProps) => { 12 - const [playing, setPlaying] = createSignal(false); 13 - const [error, setError] = createSignal(false); 14 15 - const hls = new Hls({ 16 - capLevelToPlayerSize: true, 17 - startLevel: 1, 18 - xhrSetup(xhr, urlString) { 19 - const url = new URL(urlString); 20 - 21 - // Just in case it fails, we'll remove `session_id` everywhere 22 - url.searchParams.delete("session_id"); 23 - 24 - xhr.open("get", url.toString()); 25 - }, 26 }); 27 28 - onCleanup(() => hls.destroy()); 29 - 30 - hls.loadSource(`https://video.cdn.bsky.app/hls/${did}/${cid}/playlist.m3u8`); 31 - hls.on(Hls.Events.ERROR, () => setError(true)); 32 - 33 return ( 34 - <div class="max-w-xs"> 35 - <Show when={!error()}> 36 - <video 37 - ref={(node) => { 38 - hls.attachMedia(node); 39 - 40 - createEffect(() => { 41 - if (!playing()) { 42 - return; 43 - } 44 - 45 - const observer = new IntersectionObserver( 46 - (entries) => { 47 - const entry = entries[0]; 48 - if (!entry.isIntersecting) { 49 - node.pause(); 50 - } 51 - }, 52 - { threshold: 0.5 }, 53 - ); 54 - 55 - onCleanup(() => observer.disconnect()); 56 - 57 - observer.observe(node); 58 - }); 59 - }} 60 - controls 61 - playsinline 62 - onPlay={() => setPlaying(true)} 63 - onPause={() => setPlaying(false)} 64 - onLoadedMetadata={(ev) => { 65 - const video = ev.currentTarget; 66 - 67 - const hasAudio = 68 - // @ts-expect-error: Mozilla-specific 69 - video.mozHasAudio || 70 - // @ts-expect-error: WebKit/Blink-specific 71 - !!video.webkitAudioDecodedByteCount || 72 - // @ts-expect-error: WebKit-specific 73 - !!(video.audioTracks && video.audioTracks.length); 74 - 75 - video.loop = !hasAudio || video.duration <= 6; 76 - }} 77 - /> 78 - </Show> 79 - </div> 80 ); 81 }; 82
··· 1 + import { onMount } from "solid-js"; 2 + import { pds } from "./navbar"; 3 4 export interface VideoPlayerProps { 5 did: string; 6 cid: string; 7 + onLoad: () => void; 8 } 9 10 + const VideoPlayer = (props: VideoPlayerProps) => { 11 + let video!: HTMLVideoElement; 12 13 + onMount(async () => { 14 + // thanks bf <3 15 + const res = await fetch( 16 + `https://${pds()}/xrpc/com.atproto.sync.getBlob?did=${props.did}&cid=${props.cid}`, 17 + ); 18 + if (!res.ok) throw new Error(res.statusText); 19 + const blob = await res.blob(); 20 + const url = URL.createObjectURL(blob); 21 + if (video) video.src = url; 22 }); 23 24 return ( 25 + <video ref={video} class="max-h-80 max-w-[20rem]" controls playsinline onLoadedData={props.onLoad}> 26 + <source type="video/mp4" /> 27 + </video> 28 ); 29 }; 30
+1 -1
src/index.tsx
··· 17 <Router root={Layout}> 18 <Route path="/" component={Home} /> 19 <Route path={["/jetstream", "/firehose"]} component={StreamView} /> 20 <Route path="/settings" component={Settings} /> 21 <Route path="/:pds" component={PdsView} /> 22 <Route path="/:pds/:repo" component={RepoView} /> 23 - <Route path="/:pds/:repo/labels" component={LabelView} /> 24 <Route path="/:pds/:repo/:collection" component={CollectionView} /> 25 <Route path="/:pds/:repo/:collection/:rkey" component={RecordView} /> 26 </Router>
··· 17 <Router root={Layout}> 18 <Route path="/" component={Home} /> 19 <Route path={["/jetstream", "/firehose"]} component={StreamView} /> 20 + <Route path="/labels" component={LabelView} /> 21 <Route path="/settings" component={Settings} /> 22 <Route path="/:pds" component={PdsView} /> 23 <Route path="/:pds/:repo" component={RepoView} /> 24 <Route path="/:pds/:repo/:collection" component={CollectionView} /> 25 <Route path="/:pds/:repo/:collection/:rkey" component={RecordView} /> 26 </Router>
+107 -33
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, createSignal, 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, NavMenu } from "./components/dropdown.jsx"; 8 import { agent } from "./components/login.jsx"; 9 import { NavBar } from "./components/navbar.jsx"; 10 import { Search, SearchButton, showSearch } from "./components/search.jsx"; 11 import { themeEvent, ThemeSelection } from "./components/theme.jsx"; 12 import { resolveHandle } from "./utils/api.js"; 13 14 export const isTouchDevice = "ontouchstart" in window || navigator.maxTouchPoints > 1; 15 16 - export const [notif, setNotif] = createSignal<{ 17 - show: boolean; 18 - icon?: string; 19 - text?: string; 20 - }>({ show: false }); 21 22 const Layout = (props: RouteSectionProps<unknown>) => { 23 const location = useLocation(); 24 const navigate = useNavigate(); 25 - let timeout: number; 26 27 createEffect(async () => { 28 if (props.params.repo && !props.params.repo.startsWith("did:")) { ··· 31 } 32 }); 33 34 - createEffect(() => { 35 - if (notif().show) { 36 - clearTimeout(timeout); 37 - timeout = setTimeout(() => setNotif({ show: false }), 3000); 38 - } 39 - }); 40 - 41 onMount(() => { 42 window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", themeEvent); 43 }); 44 45 return ( 46 <div 47 id="main" 48 - class="m-4 mb-8 flex flex-col items-center text-neutral-900 dark:text-neutral-200" 49 > 50 <MetaProvider> 51 <Show when={location.pathname !== "/"}> 52 <Meta name="robots" content="noindex, nofollow" /> 53 </Show> 54 </MetaProvider> 55 - <header class="mb-4 flex w-[22rem] items-center justify-between sm:w-[24rem]"> 56 <A 57 href="/" 58 style='font-feature-settings: "cv05"' ··· 61 <span class="iconify tabler--binary-tree-filled text-[#76c4e5]"></span> 62 <span>PDSls</span> 63 </A> 64 - <div class="relative -mr-1 flex items-center gap-1"> 65 <Show when={location.pathname !== "/"}> 66 <SearchButton /> 67 </Show> ··· 73 <DropdownMenu 74 icon="lucide--menu text-xl" 75 buttonClass="rounded-lg p-1" 76 - menuClass="top-8 p-3 text-sm" 77 > 78 - <NavMenu href="/jetstream" label="Jetstream" icon="lucide--radio-tower" /> 79 - <NavMenu href="/firehose" label="Firehose" icon="lucide--waves" /> 80 - <NavMenu href="/settings" label="Settings" icon="lucide--settings" /> 81 <ThemeSelection /> 82 </DropdownMenu> 83 </MenuProvider> 84 </div> 85 </header> 86 - <div class="flex max-w-full min-w-[22rem] flex-col items-center gap-4 text-pretty sm:min-w-[24rem] md:max-w-[48rem]"> 87 <Show when={showSearch() || location.pathname === "/"}> 88 <Search /> 89 </Show> ··· 92 </Show> 93 <Show keyed when={location.pathname}> 94 <ErrorBoundary 95 - fallback={(err) => <div class="mt-3 break-words">Error: {err.message}</div>} 96 > 97 <Suspense 98 fallback={ ··· 104 </ErrorBoundary> 105 </Show> 106 </div> 107 - <Show when={notif().show}> 108 - <button 109 - class="dark:shadow-dark-800 dark:bg-dark-100 fixed bottom-10 z-50 flex items-center rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 shadow-md dark:border-neutral-700" 110 - onClick={() => setNotif({ show: false })} 111 - > 112 - <span class={`iconify ${notif().icon} mr-1`}></span> 113 - {notif().text} 114 - </button> 115 - </Show> 116 </div> 117 ); 118 };
··· 1 import { Handle } from "@atcute/lexicons"; 2 import { Meta, MetaProvider } from "@solidjs/meta"; 3 import { A, RouteSectionProps, useLocation, useNavigate } from "@solidjs/router"; 4 + import { createEffect, ErrorBoundary, onMount, Show, Suspense } from "solid-js"; 5 import { AccountManager } from "./components/account.jsx"; 6 import { RecordEditor } from "./components/create.jsx"; 7 + import { DropdownMenu, MenuProvider, MenuSeparator, NavMenu } from "./components/dropdown.jsx"; 8 import { agent } from "./components/login.jsx"; 9 import { NavBar } from "./components/navbar.jsx"; 10 + import { NotificationContainer } from "./components/notification.jsx"; 11 import { Search, SearchButton, showSearch } from "./components/search.jsx"; 12 import { themeEvent, ThemeSelection } from "./components/theme.jsx"; 13 import { resolveHandle } from "./utils/api.js"; 14 15 export const isTouchDevice = "ontouchstart" in window || navigator.maxTouchPoints > 1; 16 17 + const headers: Record<string, string> = { 18 + "did:plc:ia76kvnndjutgedggx2ibrem": "bunny.jpg", 19 + "did:plc:oisofpd7lj26yvgiivf3lxsi": "puppy.jpg", 20 + "did:plc:vwzwgnygau7ed7b7wt5ux7y2": "water.webp", 21 + "did:plc:uu5axsmbm2or2dngy4gwchec": "city.webp", 22 + "did:plc:aokggmp5jzj4nc5jifhiplqc": "bridge.jpg", 23 + "did:plc:bnqkww7bjxaacajzvu5gswdf": "forest.jpg", 24 + "did:plc:p2cp5gopk7mgjegy6wadk3ep": "aurora.jpg", 25 + "did:plc:ucaezectmpny7l42baeyooxi": "almaty.webp", 26 + }; 27 28 const Layout = (props: RouteSectionProps<unknown>) => { 29 const location = useLocation(); 30 const navigate = useNavigate(); 31 + 32 + if (location.search.includes("hrt=true")) localStorage.setItem("hrt", "true"); 33 + else if (location.search.includes("hrt=false")) localStorage.setItem("hrt", "false"); 34 + if (location.search.includes("sailor=true")) localStorage.setItem("sailor", "true"); 35 + else if (location.search.includes("sailor=false")) localStorage.setItem("sailor", "false"); 36 37 createEffect(async () => { 38 if (props.params.repo && !props.params.repo.startsWith("did:")) { ··· 41 } 42 }); 43 44 onMount(() => { 45 window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", themeEvent); 46 + 47 + if (localStorage.getItem("sailor") === "true") { 48 + const style = document.createElement("style"); 49 + style.textContent = ` 50 + html, * { 51 + cursor: url(/cursor.cur), pointer; 52 + } 53 + 54 + .star { 55 + position: fixed; 56 + pointer-events: none; 57 + z-index: 9999; 58 + font-size: 20px; 59 + animation: sparkle 0.8s ease-out forwards; 60 + } 61 + 62 + @keyframes sparkle { 63 + 0% { 64 + opacity: 1; 65 + transform: translate(0, 0) rotate(var(--ttheta1)) scale(1); 66 + } 67 + 100% { 68 + opacity: 0; 69 + transform: translate(var(--tx), var(--ty)) rotate(var(--ttheta2)) scale(0); 70 + } 71 + } 72 + `; 73 + document.head.appendChild(style); 74 + 75 + let lastTime = 0; 76 + const throttleDelay = 30; 77 + 78 + document.addEventListener("mousemove", (e) => { 79 + const now = Date.now(); 80 + if (now - lastTime < throttleDelay) return; 81 + lastTime = now; 82 + 83 + const star = document.createElement("div"); 84 + star.className = "star"; 85 + star.textContent = "โœจ"; 86 + star.style.left = e.clientX + "px"; 87 + star.style.top = e.clientY + "px"; 88 + 89 + const tx = (Math.random() - 0.5) * 50; 90 + const ty = (Math.random() - 0.5) * 50; 91 + const ttheta1 = Math.random() * 360; 92 + const ttheta2 = ttheta1 + (Math.random() - 0.5) * 540; 93 + star.style.setProperty("--tx", tx + "px"); 94 + star.style.setProperty("--ty", ty + "px"); 95 + star.style.setProperty("--ttheta1", ttheta1 + "deg"); 96 + star.style.setProperty("--ttheta2", ttheta2 + "deg"); 97 + 98 + document.body.appendChild(star); 99 + 100 + setTimeout(() => star.remove(), 800); 101 + }); 102 + } 103 }); 104 105 return ( 106 <div 107 id="main" 108 + class="mx-auto mb-8 flex max-w-lg flex-col items-center p-4 text-neutral-900 dark:text-neutral-200" 109 > 110 <MetaProvider> 111 <Show when={location.pathname !== "/"}> 112 <Meta name="robots" content="noindex, nofollow" /> 113 </Show> 114 </MetaProvider> 115 + <header 116 + class={`dark:shadow-dark-700 dark:bg-dark-300 mb-3 flex w-full items-center justify-between rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 bg-size-[95%] bg-right bg-no-repeat p-3 shadow-xs [--header-bg:#fafafa] dark:border-neutral-700 dark:[--header-bg:#2d2d2d] ${localStorage.getItem("hrt") === "true" ? "bg-[linear-gradient(to_left,transparent_10%,var(--header-bg)_85%),linear-gradient(to_bottom,#5BCEFA90_0%,#5BCEFA90_20%,#F5A9B890_20%,#F5A9B890_40%,#FFFFFF90_40%,#FFFFFF90_60%,#F5A9B890_60%,#F5A9B890_80%,#5BCEFA90_80%,#5BCEFA90_100%)]" : ""}`} 117 + style={{ 118 + "background-image": 119 + props.params.repo in headers ? 120 + `linear-gradient(to left, transparent 10%, var(--header-bg) 85%), url(/headers/${headers[props.params.repo]})` 121 + : undefined, 122 + }} 123 + > 124 <A 125 href="/" 126 style='font-feature-settings: "cv05"' ··· 129 <span class="iconify tabler--binary-tree-filled text-[#76c4e5]"></span> 130 <span>PDSls</span> 131 </A> 132 + <div class="dark:bg-dark-300/60 relative flex items-center gap-1 rounded-lg bg-neutral-50/60"> 133 <Show when={location.pathname !== "/"}> 134 <SearchButton /> 135 </Show> ··· 141 <DropdownMenu 142 icon="lucide--menu text-xl" 143 buttonClass="rounded-lg p-1" 144 + menuClass="top-11 p-3 text-sm" 145 > 146 + <NavMenu href="/jetstream" label="Jetstream" /> 147 + <NavMenu href="/firehose" label="Firehose" /> 148 + <NavMenu href="/labels" label="Labels" /> 149 + <NavMenu href="/settings" label="Settings" /> 150 + <MenuSeparator /> 151 + <NavMenu 152 + href="https://bsky.app/profile/did:plc:6q5daed5gutiyerimlrnojnz" 153 + label="Bluesky" 154 + newTab 155 + external 156 + /> 157 + <NavMenu 158 + href="https://tangled.org/@pdsls.dev/pdsls/" 159 + label="Source" 160 + newTab 161 + external 162 + /> 163 <ThemeSelection /> 164 </DropdownMenu> 165 </MenuProvider> 166 </div> 167 </header> 168 + <div class="flex w-full flex-col items-center gap-3 text-pretty"> 169 <Show when={showSearch() || location.pathname === "/"}> 170 <Search /> 171 </Show> ··· 174 </Show> 175 <Show keyed when={location.pathname}> 176 <ErrorBoundary 177 + fallback={(err) => <div class="mt-3 wrap-break-word">Error: {err.message}</div>} 178 > 179 <Suspense 180 fallback={ ··· 186 </ErrorBoundary> 187 </Show> 188 </div> 189 + <NotificationContainer /> 190 </div> 191 ); 192 };
+23
src/styles/index.css
··· 9 @theme { 10 --font-sans: "Inter", sans-serif; 11 --font-mono: "Roboto Mono", monospace; 12 13 --color-dark-50: oklch(40.91% 0 0); 14 --color-dark-100: oklch(35.62% 0 0); ··· 42 .ri--bluesky { 43 --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M12 11.388c-.906-1.761-3.372-5.044-5.665-6.662c-2.197-1.55-3.034-1.283-3.583-1.033C2.116 3.978 2 4.955 2 5.528c0 .575.315 4.709.52 5.4c.68 2.28 3.094 3.05 5.32 2.803c-3.26.483-6.157 1.67-2.36 5.898c4.178 4.325 5.726-.927 6.52-3.59c.794 2.663 1.708 7.726 6.444 3.59c3.556-3.59.977-5.415-2.283-5.898c2.225.247 4.64-.523 5.319-2.803c.205-.69.52-4.825.52-5.399c0-.575-.116-1.55-.752-1.838c-.549-.248-1.386-.517-3.583 1.033c-2.293 1.621-4.76 4.904-5.665 6.664'/%3E%3C/svg%3E"); 44 }
··· 9 @theme { 10 --font-sans: "Inter", sans-serif; 11 --font-mono: "Roboto Mono", monospace; 12 + --font-pecita: "Pecita", serif; 13 14 --color-dark-50: oklch(40.91% 0 0); 15 --color-dark-100: oklch(35.62% 0 0); ··· 43 .ri--bluesky { 44 --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M12 11.388c-.906-1.761-3.372-5.044-5.665-6.662c-2.197-1.55-3.034-1.283-3.583-1.033C2.116 3.978 2 4.955 2 5.528c0 .575.315 4.709.52 5.4c.68 2.28 3.094 3.05 5.32 2.803c-3.26.483-6.157 1.67-2.36 5.898c4.178 4.325 5.726-.927 6.52-3.59c.794 2.663 1.708 7.726 6.444 3.59c3.556-3.59.977-5.415-2.283-5.898c2.225.247 4.64-.523 5.319-2.803c.205-.69.52-4.825.52-5.399c0-.575-.116-1.55-.752-1.838c-.549-.248-1.386-.517-3.583 1.033c-2.293 1.621-4.76 4.904-5.665 6.664'/%3E%3C/svg%3E"); 45 } 46 + 47 + @keyframes slideIn { 48 + 0% { 49 + transform: translateY(20px); 50 + opacity: 0; 51 + } 52 + 100% { 53 + transform: translateY(0); 54 + opacity: 1; 55 + } 56 + } 57 + 58 + @keyframes slideOut { 59 + 0% { 60 + transform: translateY(0); 61 + opacity: 1; 62 + } 63 + 100% { 64 + transform: translateY(20px); 65 + opacity: 0; 66 + } 67 + }
+46 -16
src/utils/api.ts
··· 12 DohJsonHandleResolver, 13 PlcDidDocumentResolver, 14 WellKnownHandleResolver, 15 - XrpcHandleResolver, 16 } from "@atcute/identity-resolver"; 17 import { Did, Handle } from "@atcute/lexicons"; 18 - import { isHandle } from "@atcute/lexicons/syntax"; 19 import { createStore } from "solid-js/store"; 20 import { setPDS } from "../components/navbar"; 21 22 - const didDocumentResolver = new CompositeDidDocumentResolver({ 23 methods: { 24 plc: new PlcDidDocumentResolver({ 25 - apiUrl: localStorage.plcDirectory ?? "https://plc.directory", 26 }), 27 web: new AtprotoWebDidDocumentResolver(), 28 }, 29 }); 30 31 - const handleResolver = new XrpcHandleResolver({ 32 - serviceUrl: "https://public.api.bsky.app", 33 }); 34 35 const didPDSCache: Record<string, string> = {}; ··· 77 const validateHandle = async (handle: Handle, did: Did) => { 78 if (!isHandle(handle)) return false; 79 80 - const handleResolver = new CompositeHandleResolver({ 81 - strategy: "dns-first", 82 - methods: { 83 - dns: new DohJsonHandleResolver({ dohUrl: "https://dns.google/resolve?" }), 84 - http: new WellKnownHandleResolver(), 85 - }, 86 - }); 87 - 88 let resolvedDid: string; 89 try { 90 resolvedDid = await handleResolver.resolve(handle); ··· 104 return pds; 105 }; 106 107 interface LinkData { 108 links: { 109 [key: string]: { ··· 115 }; 116 } 117 118 const getConstellation = async ( 119 endpoint: string, 120 target: string, ··· 148 path: string, 149 cursor?: string, 150 limit?: number, 151 - ) => getConstellation("/links", target, collection, path, cursor, limit || 100); 152 153 const getDidBacklinks = ( 154 target: string, ··· 156 path: string, 157 cursor?: string, 158 limit?: number, 159 - ) => getConstellation("/links/distinct-dids", target, collection, path, cursor, limit || 100); 160 161 export { 162 didDocCache, ··· 167 labelerCache, 168 resolveDidDoc, 169 resolveHandle, 170 resolvePDS, 171 validateHandle, 172 type LinkData, 173 };
··· 12 DohJsonHandleResolver, 13 PlcDidDocumentResolver, 14 WellKnownHandleResolver, 15 } from "@atcute/identity-resolver"; 16 + import { DohJsonLexiconAuthorityResolver, LexiconSchemaResolver } from "@atcute/lexicon-resolver"; 17 import { Did, Handle } from "@atcute/lexicons"; 18 + import { AtprotoDid, isHandle, Nsid } from "@atcute/lexicons/syntax"; 19 import { createStore } from "solid-js/store"; 20 import { setPDS } from "../components/navbar"; 21 22 + export const didDocumentResolver = new CompositeDidDocumentResolver({ 23 methods: { 24 plc: new PlcDidDocumentResolver({ 25 + apiUrl: localStorage.getItem("plcDirectory") ?? "https://plc.directory", 26 }), 27 web: new AtprotoWebDidDocumentResolver(), 28 }, 29 }); 30 31 + export const handleResolver = new CompositeHandleResolver({ 32 + strategy: "dns-first", 33 + methods: { 34 + dns: new DohJsonHandleResolver({ dohUrl: "https://dns.google/resolve?" }), 35 + http: new WellKnownHandleResolver(), 36 + }, 37 + }); 38 + 39 + const authorityResolver = new DohJsonLexiconAuthorityResolver({ 40 + dohUrl: "https://mozilla.cloudflare-dns.com/dns-query", 41 + }); 42 + 43 + const schemaResolver = new LexiconSchemaResolver({ 44 + didDocumentResolver: didDocumentResolver, 45 }); 46 47 const didPDSCache: Record<string, string> = {}; ··· 89 const validateHandle = async (handle: Handle, did: Did) => { 90 if (!isHandle(handle)) return false; 91 92 let resolvedDid: string; 93 try { 94 resolvedDid = await handleResolver.resolve(handle); ··· 108 return pds; 109 }; 110 111 + const resolveLexiconAuthority = async (nsid: Nsid) => { 112 + return await authorityResolver.resolve(nsid); 113 + }; 114 + 115 + const resolveLexiconSchema = async (authority: AtprotoDid, nsid: Nsid) => { 116 + return await schemaResolver.resolve(authority, nsid); 117 + }; 118 + 119 interface LinkData { 120 links: { 121 [key: string]: { ··· 127 }; 128 } 129 130 + type LinksWithRecords = { 131 + cursor: string; 132 + total: number; 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, ··· 172 path: string, 173 cursor?: string, 174 limit?: number, 175 + ): Promise<LinksWithRecords> => 176 + getConstellation("/links", target, collection, path, cursor, limit || 100); 177 178 const getDidBacklinks = ( 179 target: 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, ··· 193 labelerCache, 194 resolveDidDoc, 195 resolveHandle, 196 + resolveLexiconAuthority, 197 + resolveLexiconSchema, 198 resolvePDS, 199 validateHandle, 200 type LinkData, 201 + type LinksWithDids, 202 + type LinksWithRecords, 203 };
+119
src/utils/app-urls.ts
···
··· 1 + export type AppUrl = `${string}.${string}` | `localhost:${number}`; 2 + 3 + export enum App { 4 + Bluesky, 5 + Tangled, 6 + Whitewind, 7 + Frontpage, 8 + Pinksea, 9 + Linkat, 10 + } 11 + 12 + export const appName = { 13 + [App.Bluesky]: "Bluesky", 14 + [App.Tangled]: "Tangled", 15 + [App.Whitewind]: "Whitewind", 16 + [App.Frontpage]: "Frontpage", 17 + [App.Pinksea]: "Pinksea", 18 + [App.Linkat]: "Linkat", 19 + }; 20 + 21 + export const appList: Record<AppUrl, App> = { 22 + "localhost:19006": App.Bluesky, 23 + "blacksky.community": App.Bluesky, 24 + "bsky.app": App.Bluesky, 25 + "catsky.social": App.Bluesky, 26 + "deer.aylac.top": App.Bluesky, 27 + "deer-social-ayla.pages.dev": App.Bluesky, 28 + "deer.social": App.Bluesky, 29 + "main.bsky.dev": App.Bluesky, 30 + "social.daniela.lol": App.Bluesky, 31 + "tangled.org": App.Tangled, 32 + "whtwnd.com": App.Whitewind, 33 + "frontpage.fyi": App.Frontpage, 34 + "pinksea.art": App.Pinksea, 35 + "linkat.blue": App.Linkat, 36 + }; 37 + 38 + export const appHandleLink: Record<App, (url: string[]) => string> = { 39 + [App.Bluesky]: (path) => { 40 + const baseType = path[0]; 41 + const user = path[1]; 42 + 43 + if (baseType === "profile") { 44 + if (path[2]) { 45 + const type = path[2]; 46 + const rkey = path[3]; 47 + 48 + if (type === "post") { 49 + return `at://${user}/app.bsky.feed.post/${rkey}`; 50 + } else if (type === "lists") { 51 + return `at://${user}/app.bsky.graph.list/${rkey}`; 52 + } else if (type === "feed") { 53 + return `at://${user}/app.bsky.feed.generator/${rkey}`; 54 + } else if (type === "follows") { 55 + return `at://${user}/app.bsky.graph.follow/${rkey}`; 56 + } 57 + } else { 58 + return `at://${user}`; 59 + } 60 + } else if (baseType === "starter-pack") { 61 + return `at://${user}/app.bsky.graph.starterpack/${path[2]}`; 62 + } 63 + return `at://${user}`; 64 + }, 65 + [App.Tangled]: (path) => { 66 + if (path[0] === "strings") { 67 + return `at://${path[1]}/sh.tangled.string/${path[2]}`; 68 + } 69 + 70 + let query: string | undefined; 71 + if (path[path.length - 1].includes("?")) { 72 + const split = path[path.length - 1].split("?"); 73 + query = split[1]; 74 + path[path.length - 1] = split[0]; 75 + } 76 + 77 + const user = path[0].replace("@", ""); 78 + 79 + if (path.length === 1) { 80 + if (query === "tab=repos") { 81 + return `at://${user}/sh.tangled.repo`; 82 + } else if (query === "tab=starred") { 83 + return `at://${user}/sh.tangled.feed.star`; 84 + } else if (query === "tab=strings") { 85 + return `at://${user}/sh.tangled.string`; 86 + } 87 + } else if (path.length === 2) { 88 + // no way to convert the repo name to an rkey afaik 89 + // same reason why there's nothing related to issues in here 90 + return `at://${user}/sh.tangled.repo`; 91 + } 92 + 93 + return `at://${user}`; 94 + }, 95 + [App.Whitewind]: (path) => { 96 + if (path.length === 2) { 97 + return `at://${path[0]}/com.whtwnd.blog.entry/${path[1]}`; 98 + } 99 + 100 + return `at://${path[0]}/com.whtwnd.blog.entry`; 101 + }, 102 + [App.Frontpage]: (path) => { 103 + if (path.length === 3) { 104 + return `at://${path[1]}/fyi.unravel.frontpage.post/${path[2]}`; 105 + } else if (path.length === 5) { 106 + return `at://${path[3]}/fyi.unravel.frontpage.comment/${path[4]}`; 107 + } 108 + 109 + return `at://${path[0]}`; 110 + }, 111 + [App.Pinksea]: (path) => { 112 + if (path.length === 2) { 113 + return `at://${path[0]}/com.shinolabs.pinksea.oekaki/${path[1]}`; 114 + } 115 + 116 + return `at://${path[0]}`; 117 + }, 118 + [App.Linkat]: (path) => `at://${path[0]}/blue.linkat.board/self`, 119 + };
+6 -2
src/utils/copy.ts
··· 1 - import { setNotif } from "../layout"; 2 3 export const addToClipboard = (text: string) => { 4 navigator.clipboard.writeText(text); 5 - setNotif({ show: true, icon: "lucide--clipboard-check", text: "Copied to clipboard" }); 6 };
··· 1 + import { addNotification, removeNotification } from "../components/notification"; 2 3 export const addToClipboard = (text: string) => { 4 navigator.clipboard.writeText(text); 5 + const id = addNotification({ 6 + message: "Copied to clipboard", 7 + type: "success", 8 + }); 9 + setTimeout(() => removeNotification(id), 3000); 10 };
+23
src/utils/hooks/debounced.ts
···
··· 1 + import { type Accessor, createEffect, createSignal, onCleanup } from 'solid-js'; 2 + 3 + export const createDebouncedValue = <T>( 4 + accessor: Accessor<T>, 5 + delay: number, 6 + equals?: false | ((prev: T, next: T) => boolean), 7 + ): Accessor<T> => { 8 + const initial = accessor(); 9 + const [state, setState] = createSignal(initial, { equals }); 10 + 11 + createEffect((prev: T) => { 12 + const next = accessor(); 13 + 14 + if (prev !== next) { 15 + const timeout = setTimeout(() => setState(() => next), delay); 16 + onCleanup(() => clearTimeout(timeout)); 17 + } 18 + 19 + return next; 20 + }, initial); 21 + 22 + return state; 23 + };
+2 -2
src/utils/templates.ts
··· 47 }), 48 "sh.tangled.actor.profile": (uri) => ({ 49 label: "Tangled", 50 - link: `https://tangled.sh/${uri.repo}`, 51 icon: "i-tangled", 52 }), 53 "sh.tangled.repo": (uri, record) => ({ 54 label: "Tangled", 55 - link: `https://tangled.sh/${uri.repo}/${record.name}`, 56 icon: "i-tangled", 57 }), 58 };
··· 47 }), 48 "sh.tangled.actor.profile": (uri) => ({ 49 label: "Tangled", 50 + link: `https://tangled.org/${uri.repo}`, 51 icon: "i-tangled", 52 }), 53 "sh.tangled.repo": (uri, record) => ({ 54 label: "Tangled", 55 + link: `https://tangled.org/${uri.repo}/${record.name}`, 56 icon: "i-tangled", 57 }), 58 };
+1 -1
src/utils/types/at-uri.ts
··· 11 12 const RECORD_KEY_RE = /^(?!\.{1,2}$)[a-zA-Z0-9_~.:-]{1,512}$/; 13 14 - const ATURI_RE = 15 /^at:\/\/([a-zA-Z0-9._:%-]+)(?:\/([a-zA-Z0-9-.]+)(?:\/([a-zA-Z0-9._~:@!$&%')(*+,;=-]+))?)?(?:#(\/[a-zA-Z0-9._~:@!$&%')(*+,;=\-[\]/\\]*))?$/; 16 17 const isDid = (input: unknown): input is Did => {
··· 11 12 const RECORD_KEY_RE = /^(?!\.{1,2}$)[a-zA-Z0-9_~.:-]{1,512}$/; 13 14 + export const ATURI_RE = 15 /^at:\/\/([a-zA-Z0-9._:%-]+)(?:\/([a-zA-Z0-9-.]+)(?:\/([a-zA-Z0-9._~:@!$&%')(*+,;=-]+))?)?(?:#(\/[a-zA-Z0-9._~:@!$&%')(*+,;=\-[\]/\\]*))?$/; 16 17 const isDid = (input: unknown): input is Did => {
-306
src/utils/verify.ts
··· 1 - import * as CAR from "@atcute/car"; 2 - import { CarReader } from "@atcute/car/v4"; 3 - import * as CBOR from "@atcute/cbor"; 4 - import * as CID from "@atcute/cid"; 5 - import { Client } from "@atcute/client"; 6 - import { type FoundPublicKey, getPublicKeyFromDidController, verifySig } from "@atcute/crypto"; 7 - import { type DidDocument, getAtprotoVerificationMaterial } from "@atcute/identity"; 8 - import { Did } from "@atcute/lexicons"; 9 - import { toSha256 } from "@atcute/uint8array"; 10 - 11 - import { type AddressedAtUri, parseAddressedAtUri } from "./types/at-uri"; 12 - 13 - export interface VerifyError { 14 - message: string; 15 - detail?: unknown; 16 - } 17 - 18 - export interface VerifyResult { 19 - errors: VerifyError[]; 20 - } 21 - 22 - export interface VerifyOptions { 23 - rpc: Client; 24 - uri: string; 25 - cid: string; 26 - record: unknown; 27 - didDoc: DidDocument; 28 - } 29 - 30 - export const verifyRecord = async (opts: VerifyOptions): Promise<VerifyResult> => { 31 - const errors: VerifyError[] = []; 32 - 33 - // verify cid can be parsed 34 - try { 35 - CID.fromString(opts.cid); 36 - } catch (e) { 37 - errors.push({ message: `provided cid is invalid`, detail: e }); 38 - } 39 - 40 - // verify record content matches cid 41 - let cbor: Uint8Array; 42 - { 43 - cbor = CBOR.encode(opts.record); 44 - 45 - const cid = await CID.create(CID.CODEC_DCBOR, cbor); 46 - const cidString = CID.toString(cid); 47 - 48 - if (cidString !== opts.cid) { 49 - errors.push({ message: `record content does not match cid` }); 50 - } 51 - } 52 - 53 - // verify at-uri is valid 54 - let uri: AddressedAtUri; 55 - try { 56 - uri = parseAddressedAtUri(opts.uri); 57 - 58 - if (uri.repo !== opts.didDoc.id) { 59 - errors.push({ message: `repo in at-uri does not match did document` }); 60 - } 61 - } catch (err) { 62 - errors.push({ message: `provided at-uri is invalid`, detail: err }); 63 - return { errors }; 64 - } 65 - 66 - // grab public key from did document 67 - let publicKey: FoundPublicKey; 68 - try { 69 - const controller = getAtprotoVerificationMaterial(opts.didDoc); 70 - if (!controller) { 71 - errors.push({ 72 - message: `did document does not contain verification material`, 73 - }); 74 - return { errors }; 75 - } 76 - 77 - publicKey = getPublicKeyFromDidController(controller); 78 - } catch (err) { 79 - errors.push({ 80 - message: `failed to get public key from did document`, 81 - detail: err, 82 - }); 83 - return { errors }; 84 - } 85 - 86 - // grab the raw record blocks from the pds 87 - let car: Uint8Array; 88 - const { ok, data } = await opts.rpc.get("com.atproto.sync.getRecord", { 89 - params: { 90 - did: opts.didDoc.id as Did, 91 - collection: uri.collection, 92 - rkey: uri.rkey, 93 - }, 94 - as: "bytes", 95 - }); 96 - if (!ok) { 97 - errors.push({ message: `failed to fetch car from pds`, detail: data.error }); 98 - return { errors }; 99 - } else { 100 - car = data; 101 - } 102 - 103 - // read the car 104 - let blockmap: CAR.BlockMap; 105 - let commit: CAR.Commit; 106 - 107 - try { 108 - const reader = CarReader.fromUint8Array(car); 109 - if (reader.header.data.roots.length !== 1) { 110 - errors.push({ message: `car must have exactly one root` }); 111 - return { errors }; 112 - } 113 - 114 - blockmap = new Map(); 115 - for (const entry of reader) { 116 - const cidString = CID.toString(entry.cid); 117 - 118 - // Verify that `bytes` matches its associated CID 119 - const expectedCid = CID.toString(await CID.create(entry.cid.codec as 85 | 113, entry.bytes)); 120 - if (cidString !== expectedCid) { 121 - errors.push({ 122 - message: `cid does not match bytes`, 123 - detail: { cid: cidString, expectedCid }, 124 - }); 125 - } 126 - 127 - blockmap.set(cidString, entry); 128 - } 129 - 130 - if (blockmap.size === 0) { 131 - errors.push({ message: `car must have at least one block` }); 132 - return { errors }; 133 - } 134 - 135 - commit = CAR.readBlock(blockmap, reader.header.data.roots[0], CAR.isCommit); 136 - } catch (err) { 137 - errors.push({ message: `failed to read car`, detail: err }); 138 - return { errors }; 139 - } 140 - 141 - // verify did in commit matches the did in the at-uri 142 - if (commit.did !== opts.didDoc.id) { 143 - errors.push({ message: `did in commit does not match did document` }); 144 - } 145 - 146 - // verify signature contained in commit is valid 147 - { 148 - const { sig, ...unsigned } = commit; 149 - 150 - const data = CBOR.encode(unsigned); 151 - const valid = await verifySig( 152 - publicKey, 153 - CBOR.fromBytes(sig) as Uint8Array<ArrayBuffer>, 154 - data as Uint8Array<ArrayBuffer>, 155 - ); 156 - 157 - if (!valid) { 158 - errors.push({ message: `signature verification failed` }); 159 - } 160 - } 161 - 162 - // verify the commit is a valid commit 163 - try { 164 - const result = await dfs(blockmap, commit.data.$link, opts.cid); 165 - if (!result.found) { 166 - errors.push({ message: `could not find record in car` }); 167 - } 168 - } catch (err) { 169 - errors.push({ message: `failed to iterate over car`, detail: err }); 170 - } 171 - 172 - return { errors }; 173 - }; 174 - 175 - interface DfsResult { 176 - found: boolean; 177 - min?: string; 178 - max?: string; 179 - depth?: number; 180 - } 181 - 182 - const encoder = new TextEncoder(); 183 - const decoder = new TextDecoder(); 184 - 185 - const dfs = async ( 186 - blockmap: CAR.BlockMap, 187 - from: string | undefined, 188 - target: string, 189 - visited = new Set<string>(), 190 - ): Promise<DfsResult> => { 191 - // If there's no starting point, return empty state 192 - if (from == null) { 193 - return { found: false }; 194 - } 195 - 196 - // Check for cycles 197 - { 198 - if (visited.has(from)) { 199 - throw new Error(`cycle detected; cid=${from}`); 200 - } 201 - 202 - visited.add(from); 203 - } 204 - 205 - // Get the block data 206 - let node: CAR.MstNode; 207 - { 208 - const entry = blockmap.get(from); 209 - if (!entry) { 210 - return { found: false }; 211 - } 212 - 213 - const decoded = CBOR.decode(entry.bytes); 214 - if (!CAR.isMstNode(decoded)) { 215 - throw new Error(`invalid mst node; cid=${from}`); 216 - } 217 - 218 - node = decoded; 219 - } 220 - 221 - // Recursively process the left child 222 - const left = await dfs(blockmap, node.l?.$link, target, visited); 223 - 224 - let key = ""; 225 - let found = left.found; 226 - let depth: number | undefined; 227 - let firstKey: string | undefined; 228 - let lastKey: string | undefined; 229 - 230 - // Process all entries in this node 231 - for (const entry of node.e) { 232 - if (entry.v.$link === target) { 233 - found = true; 234 - } 235 - 236 - // Construct the key by truncating and appending 237 - key = key.substring(0, entry.p) + decoder.decode(CBOR.fromBytes(entry.k)); 238 - 239 - // Calculate depth based on leading zeros in the hash 240 - const keyDigest = await toSha256(encoder.encode(key) as Uint8Array<ArrayBuffer>); 241 - let zeroCount = 0; 242 - 243 - outerLoop: for (const byte of keyDigest) { 244 - for (let bit = 7; bit >= 0; bit--) { 245 - if (((byte >> bit) & 1) !== 0) { 246 - break outerLoop; 247 - } 248 - zeroCount++; 249 - } 250 - } 251 - 252 - const thisDepth = Math.floor(zeroCount / 2); 253 - 254 - // Ensure consistent depth 255 - if (depth === undefined) { 256 - depth = thisDepth; 257 - } else if (depth !== thisDepth) { 258 - throw new Error(`node has entries with different depths; cid=${from}`); 259 - } 260 - 261 - // Track first and last keys 262 - if (lastKey === undefined) { 263 - firstKey = key; 264 - lastKey = key; 265 - } 266 - 267 - // Check key ordering 268 - if (lastKey > key) { 269 - throw new Error(`entries are out of order; cid=${from}`); 270 - } 271 - 272 - // Process right child 273 - const right = await dfs(blockmap, entry.t?.$link, target, visited); 274 - 275 - // Check ordering with right subtree 276 - if (right.min && right.min < lastKey) { 277 - throw new Error(`entries are out of order; cid=${from}`); 278 - } 279 - 280 - found ||= right.found; 281 - 282 - // Check depth ordering 283 - if (left.depth !== undefined && left.depth >= thisDepth) { 284 - throw new Error(`depths are out of order; cid=${from}`); 285 - } 286 - 287 - if (right.depth !== undefined && right.depth >= thisDepth) { 288 - throw new Error(`depths are out of order; cid=${from}`); 289 - } 290 - 291 - // Update last key based on right subtree 292 - lastKey = right.max ?? key; 293 - } 294 - 295 - // Check ordering with left subtree 296 - if (left.max && firstKey && left.max > firstKey) { 297 - throw new Error(`entries are out of order; cid=${from}`); 298 - } 299 - 300 - return { 301 - found, 302 - min: firstKey, 303 - max: lastKey, 304 - depth, 305 - }; 306 - };
···
+7 -7
src/views/blob.tsx
··· 30 return ( 31 <div class="flex flex-col items-center gap-2"> 32 <Show when={blobs() || response()}> 33 - <p> 34 - {blobs()?.length} blob{(blobs()?.length ?? 0 > 1) ? "s" : ""} 35 - </p> 36 <div class="flex flex-col gap-0.5 font-mono text-sm wrap-anywhere lg:break-normal"> 37 <For each={blobs()}> 38 {(cid) => ( ··· 47 </For> 48 </div> 49 </Show> 50 - <Show when={cursor()}> 51 - <div class="dark:bg-dark-500 fixed bottom-0 z-5 flex w-screen justify-center bg-neutral-100 py-3"> 52 - <Show when={!response.loading}> 53 <Button onClick={() => refetch()}>Load More</Button> 54 </Show> 55 <Show when={response.loading}> 56 <span class="iconify lucide--loader-circle animate-spin py-3.5 text-xl"></span> 57 </Show> 58 </div> 59 - </Show> 60 </div> 61 ); 62 };
··· 30 return ( 31 <div class="flex flex-col items-center gap-2"> 32 <Show when={blobs() || response()}> 33 <div class="flex flex-col gap-0.5 font-mono text-sm wrap-anywhere lg:break-normal"> 34 <For each={blobs()}> 35 {(cid) => ( ··· 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" : ""} 51 + </p> 52 + <Show when={!response.loading && cursor()}> 53 <Button onClick={() => refetch()}>Load More</Button> 54 </Show> 55 <Show when={response.loading}> 56 <span class="iconify lucide--loader-circle animate-spin py-3.5 text-xl"></span> 57 </Show> 58 </div> 59 + </div> 60 </div> 61 ); 62 };
+140 -130
src/views/collection.tsx
··· 9 import { JSONType, JSONValue } from "../components/json.jsx"; 10 import { agent } from "../components/login.jsx"; 11 import { Modal } from "../components/modal.jsx"; 12 import { StickyOverlay } from "../components/sticky.jsx"; 13 import { TextInput } from "../components/text-input.jsx"; 14 import Tooltip from "../components/tooltip.jsx"; 15 - import { setNotif } from "../layout.jsx"; 16 import { resolvePDS } from "../utils/api.js"; 17 import { localDateFromTimestamp } from "../utils/date.js"; 18 19 interface AtprotoRecord { 20 rkey: string; 21 record: InferXRPCBodyOutput<ComAtprotoRepoGetRecord.mainSchema["output"]>; 22 timestamp: number | undefined; 23 toDelete: boolean; ··· 40 41 return ( 42 <span 43 - class="relative flex items-baseline rounded px-0.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 44 ref={rkeyRef} 45 onmouseover={() => setHover(true)} 46 onmouseleave={() => setHover(false)} 47 > 48 - <span class="text-sm text-blue-400 sm:text-base">{props.record.rkey}</span> 49 - <Show when={props.record.timestamp && props.record.timestamp <= Date.now()}> 50 - <span class="ml-1 text-xs text-neutral-500 dark:text-neutral-400"> 51 - {localDateFromTimestamp(props.record.timestamp!)} 52 </span> 53 - </Show> 54 <Show when={hover()}> 55 <span 56 ref={previewRef} 57 - class={`dark:bg-dark-300 dark:shadow-dark-800 pointer-events-none absolute left-[50%] z-25 block max-h-[20rem] w-max max-w-sm -translate-x-1/2 overflow-hidden rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 text-xs whitespace-pre-wrap shadow-md sm:max-h-[28rem] lg:max-w-lg dark:border-neutral-700 ${isOverflowing(previewHeight()) ? "bottom-7" : "top-7"}`} 58 > 59 <JSONValue 60 data={props.record.record.value as JSONType} ··· 99 const rkey = record.uri.split("/").pop()!; 100 tmpRecords.push({ 101 rkey: rkey, 102 record: record, 103 timestamp: TID.validate(rkey) ? TID.parse(rkey).timestamp / 1000 : undefined, 104 toDelete: false, ··· 142 }, 143 }); 144 } 145 - setNotif({ 146 - show: true, 147 - icon: "lucide--trash-2", 148 - text: `${recsToDel.length} records ${recreate() ? "recreated" : "deleted"}`, 149 }); 150 setBatchDelete(false); 151 setRecords([]); 152 setCursor(undefined); ··· 183 <Show when={records.length || response()}> 184 <div class="-mt-2 flex w-full flex-col items-center"> 185 <StickyOverlay> 186 - <div class="flex w-[22rem] items-center gap-1 sm:w-[24rem]"> 187 - <Show when={agent() && agent()?.sub === did}> 188 - <div class="flex items-center"> 189 - <Tooltip 190 - text={batchDelete() ? "Cancel" : "Delete"} 191 - children={ 192 - <button 193 - onclick={() => { 194 - setRecords( 195 - { from: 0, to: untrack(() => records.length) - 1 }, 196 - "toDelete", 197 - false, 198 - ); 199 - setLastSelected(undefined); 200 - setBatchDelete(!batchDelete()); 201 - }} 202 - class="-ml-1 flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 203 - > 204 - <span 205 - class={`iconify text-lg ${batchDelete() ? "lucide--circle-x" : "lucide--trash-2"} `} 206 - ></span> 207 - </button> 208 - } 209 - /> 210 - <Show when={batchDelete()}> 211 <Tooltip 212 - text="Select all" 213 - children={ 214 - <button 215 - onclick={() => selectAll()} 216 - class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 217 - > 218 - <span class="iconify lucide--copy-check text-lg"></span> 219 - </button> 220 - } 221 - /> 222 - <Tooltip 223 - text="Recreate" 224 children={ 225 <button 226 onclick={() => { 227 - setRecreate(true); 228 - setOpenDelete(true); 229 }} 230 class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 231 > 232 - <span class="iconify lucide--recycle text-lg text-green-500 dark:text-green-400"></span> 233 </button> 234 } 235 /> 236 - <Tooltip 237 - text="Delete" 238 - children={ 239 - <button 240 - onclick={() => { 241 - setRecreate(false); 242 - setOpenDelete(true); 243 - }} 244 - class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 245 > 246 - <span class="iconify lucide--trash-2 text-lg text-red-500 dark:text-red-400"></span> 247 - </button> 248 - } 249 - /> 250 - </Show> 251 - </div> 252 - <Modal open={openDelete()} onClose={() => setOpenDelete(false)}> 253 - <div class="dark:bg-dark-300 dark:shadow-dark-800 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-300 dark:border-neutral-700 starting:opacity-0"> 254 - <h2 class="mb-2 font-semibold"> 255 - {recreate() ? "Recreate" : "Delete"} {records.filter((r) => r.toDelete).length}{" "} 256 - records? 257 - </h2> 258 - <div class="flex justify-end gap-2"> 259 - <Button onClick={() => setOpenDelete(false)}>Cancel</Button> 260 - <Button 261 - onClick={deleteRecords} 262 - class={`dark:shadow-dark-800 rounded-lg px-2 py-1.5 text-xs font-semibold text-neutral-200 shadow-xs select-none ${recreate() ? "bg-green-500 hover:bg-green-400 dark:bg-green-600 dark:hover:bg-green-500" : "bg-red-500 hover:bg-red-400 active:bg-red-400"}`} 263 - > 264 - {recreate() ? "Recreate" : "Delete"} 265 - </Button> 266 </div> 267 </div> 268 - </Modal> 269 </Show> 270 - <Tooltip text="Jetstream"> 271 - <A 272 - href={`/jetstream?collections=${params.collection}&dids=${params.repo}`} 273 - class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 274 - > 275 - <span class="iconify lucide--radio-tower text-lg"></span> 276 - </A> 277 - </Tooltip> 278 - <TextInput 279 - placeholder="Filter by substring" 280 - onInput={(e) => setFilter(e.currentTarget.value)} 281 - class="grow" 282 - /> 283 </div> 284 - <Show when={records.length > 1}> 285 - <div class="flex w-[22rem] items-center justify-between gap-x-2 sm:w-[24rem]"> 286 - <Button 287 - onClick={() => { 288 - setReverse(!reverse()); 289 - setRecords([]); 290 - setCursor(undefined); 291 - refetch(); 292 - }} 293 - > 294 - <span 295 - class={`iconify ${reverse() ? "lucide--rotate-ccw" : "lucide--rotate-cw"} text-sm`} 296 - ></span> 297 - Reverse 298 - </Button> 299 - <div> 300 - <Show when={batchDelete()}> 301 - <span>{records.filter((rec) => rec.toDelete).length}</span> 302 - <span>/</span> 303 - </Show> 304 - <span>{records.length} records</span> 305 - </div> 306 - <div class="flex w-[5rem] items-center justify-end"> 307 - <Show when={cursor()}> 308 - <Show when={!response.loading}> 309 - <Button onClick={() => refetch()}>Load More</Button> 310 - </Show> 311 - <Show when={response.loading}> 312 - <div class="iconify lucide--loader-circle w-[5rem] animate-spin text-xl" /> 313 - </Show> 314 - </Show> 315 - </div> 316 - </div> 317 - </Show> 318 </StickyOverlay> 319 - <div class="flex max-w-full flex-col font-mono"> 320 <For 321 each={records.filter((rec) => 322 filter() ? JSON.stringify(rec.record.value).includes(filter()!) : true,
··· 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; 21 + cid: string; 22 record: InferXRPCBodyOutput<ComAtprotoRepoGetRecord.mainSchema["output"]>; 23 timestamp: number | undefined; 24 toDelete: boolean; ··· 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> 54 + <Show when={props.record.timestamp && props.record.timestamp <= Date.now()}> 55 + <span class="ml-1 shrink-0 text-xs"> 56 + {localDateFromTimestamp(props.record.timestamp!)} 57 + </span> 58 + </Show> 59 + </span> 60 <Show when={hover()}> 61 <span 62 ref={previewRef} 63 + class={`dark:bg-dark-300 dark:shadow-dark-700 pointer-events-none absolute left-[50%] z-25 block max-h-80 w-max max-w-sm -translate-x-1/2 overflow-hidden rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 text-xs whitespace-pre-wrap shadow-md sm:max-h-112 lg:max-w-lg dark:border-neutral-700 ${isOverflowing(previewHeight()) ? "bottom-7" : "top-7"}`} 64 > 65 <JSONValue 66 data={props.record.record.value as JSONType} ··· 105 const rkey = record.uri.split("/").pop()!; 106 tmpRecords.push({ 107 rkey: rkey, 108 + cid: record.cid, 109 record: record, 110 timestamp: TID.validate(rkey) ? TID.parse(rkey).timestamp / 1000 : undefined, 111 toDelete: false, ··· 149 }, 150 }); 151 } 152 + const id = addNotification({ 153 + message: `${recsToDel.length} records ${recreate() ? "recreated" : "deleted"}`, 154 + type: "success", 155 }); 156 + setTimeout(() => removeNotification(id), 3000); 157 setBatchDelete(false); 158 setRecords([]); 159 setCursor(undefined); ··· 190 <Show when={records.length || response()}> 191 <div class="-mt-2 flex w-full flex-col items-center"> 192 <StickyOverlay> 193 + <div class="flex w-full flex-col gap-2"> 194 + <div class="flex items-center gap-1"> 195 + <Show when={agent() && agent()?.sub === did}> 196 + <div class="flex items-center"> 197 <Tooltip 198 + text={batchDelete() ? "Cancel" : "Delete"} 199 children={ 200 <button 201 onclick={() => { 202 + setRecords( 203 + { from: 0, to: untrack(() => records.length) - 1 }, 204 + "toDelete", 205 + false, 206 + ); 207 + setLastSelected(undefined); 208 + setBatchDelete(!batchDelete()); 209 }} 210 class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 211 > 212 + <span 213 + class={`iconify text-lg ${batchDelete() ? "lucide--circle-x" : "lucide--trash-2"} `} 214 + ></span> 215 </button> 216 } 217 /> 218 + <Show when={batchDelete()}> 219 + <Tooltip 220 + text="Select all" 221 + children={ 222 + <button 223 + onclick={() => selectAll()} 224 + class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 225 + > 226 + <span class="iconify lucide--copy-check text-lg"></span> 227 + </button> 228 + } 229 + /> 230 + <Tooltip 231 + text="Recreate" 232 + children={ 233 + <button 234 + onclick={() => { 235 + setRecreate(true); 236 + setOpenDelete(true); 237 + }} 238 + class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 239 + > 240 + <span class="iconify lucide--recycle text-lg text-green-500 dark:text-green-400"></span> 241 + </button> 242 + } 243 + /> 244 + <Tooltip 245 + text="Delete" 246 + children={ 247 + <button 248 + onclick={() => { 249 + setRecreate(false); 250 + setOpenDelete(true); 251 + }} 252 + class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 253 + > 254 + <span class="iconify lucide--trash-2 text-lg text-red-500 dark:text-red-400"></span> 255 + </button> 256 + } 257 + /> 258 + </Show> 259 + </div> 260 + <Modal open={openDelete()} onClose={() => setOpenDelete(false)}> 261 + <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"> 262 + <h2 class="mb-2 font-semibold"> 263 + {recreate() ? "Recreate" : "Delete"}{" "} 264 + {records.filter((r) => r.toDelete).length} records? 265 + </h2> 266 + <div class="flex justify-end gap-2"> 267 + <Button onClick={() => setOpenDelete(false)}>Cancel</Button> 268 + <Button 269 + onClick={deleteRecords} 270 + class={`dark:shadow-dark-700 rounded-lg px-2 py-1.5 text-xs text-white shadow-xs select-none ${recreate() ? "bg-green-500 hover:bg-green-400 dark:bg-green-600 dark:hover:bg-green-500" : "bg-red-500 hover:bg-red-400 active:bg-red-400"}`} 271 > 272 + {recreate() ? "Recreate" : "Delete"} 273 + </Button> 274 + </div> 275 </div> 276 + </Modal> 277 + </Show> 278 + <Tooltip text="Jetstream"> 279 + <A 280 + href={`/jetstream?collections=${params.collection}&dids=${params.repo}`} 281 + class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 282 + > 283 + <span class="iconify lucide--radio-tower text-lg"></span> 284 + </A> 285 + </Tooltip> 286 + <TextInput 287 + name="Filter" 288 + placeholder="Filter by substring" 289 + onInput={(e) => setFilter(e.currentTarget.value)} 290 + class="grow" 291 + /> 292 + </div> 293 + <Show when={records.length > 1}> 294 + <div class="flex items-center justify-between gap-x-2"> 295 + <Button 296 + onClick={() => { 297 + setReverse(!reverse()); 298 + setRecords([]); 299 + setCursor(undefined); 300 + refetch(); 301 + }} 302 + > 303 + <span 304 + class={`iconify ${reverse() ? "lucide--rotate-ccw" : "lucide--rotate-cw"}`} 305 + ></span> 306 + Reverse 307 + </Button> 308 + <div> 309 + <Show when={batchDelete()}> 310 + <span>{records.filter((rec) => rec.toDelete).length}</span> 311 + <span>/</span> 312 + </Show> 313 + <span>{records.length} records</span> 314 </div> 315 + <div class="flex w-20 items-center justify-end"> 316 + <Show when={cursor()}> 317 + <Show when={!response.loading}> 318 + <Button onClick={() => refetch()}>Load More</Button> 319 + </Show> 320 + <Show when={response.loading}> 321 + <div class="iconify lucide--loader-circle w-20 animate-spin text-xl" /> 322 + </Show> 323 + </Show> 324 + </div> 325 + </div> 326 </Show> 327 </div> 328 </StickyOverlay> 329 + <div class="flex max-w-full flex-col px-2 font-mono"> 330 <For 331 each={records.filter((rec) => 332 filter() ? JSON.stringify(rec.record.value).includes(filter()!) : true,
+20 -75
src/views/home.tsx
··· 1 - import { A } from "@solidjs/router"; 2 - 3 - const Home = () => { 4 return ( 5 - <div class="flex w-[22rem] flex-col gap-4 break-words sm:w-[24rem]"> 6 - <div> 7 <div> 8 - <span class="text-lg font-semibold">AT Protocol Explorer</span> 9 </div> 10 <div class="flex items-center gap-1"> 11 <div class="iconify lucide--search" /> 12 <span> 13 Browse the public data on{" "} 14 - <a 15 - class="text-blue-400 hover:underline active:underline" 16 - href="https://atproto.com" 17 - target="_blank" 18 - > 19 atproto 20 </a> 21 . ··· 23 </div> 24 <div class="flex items-center gap-1"> 25 <div class="iconify lucide--user-round" /> 26 - <span>Login to manage records in your repo.</span> 27 </div> 28 <div class="flex items-center gap-1"> 29 <div class="iconify lucide--radio-tower" /> 30 - <div> 31 - <A href="/jetstream" class="text-blue-400 hover:underline active:underline"> 32 - Jetstream 33 - </A>{" "} 34 - and{" "} 35 - <A href="/firehose" class="text-blue-400 hover:underline active:underline"> 36 - firehose 37 - </A>{" "} 38 - streaming. 39 - </div> 40 </div> 41 <div class="flex items-center gap-1"> 42 - <div class="iconify lucide--send-to-back" /> 43 <span> 44 Backlinks support with{" "} 45 - <A 46 href="https://constellation.microcosm.blue" 47 - class="text-blue-400 hover:underline active:underline" 48 target="_blank" 49 > 50 constellation 51 - </A> 52 . 53 </span> 54 </div> 55 - </div> 56 - <div class="text-sm"> 57 - <span class="text-base font-semibold">Examples</span> 58 - <div class="flex items-center gap-1"> 59 - <div class="iconify lucide--hard-drive" /> 60 - <A href="/pds.kelinci.net" class="text-blue-400 hover:underline active:underline"> 61 - https://pds.kelinci.net 62 - </A> 63 - </div> 64 - <div class="flex items-center gap-1"> 65 - <div class="iconify lucide--book-user" /> 66 - <A 67 - href="/at://did:plc:vwzwgnygau7ed7b7wt5ux7y2" 68 - class="text-blue-400 hover:underline active:underline" 69 - > 70 - did:plc:vwzwgnygau7ed7b7wt5ux7y2 71 - </A> 72 - </div> 73 - <div class="flex items-center gap-1"> 74 - <div class="iconify lucide--file-json shrink-0" /> 75 - <A 76 - href="/at://did:plc:oisofpd7lj26yvgiivf3lxsi/app.bsky.feed.post/3l2zpbbhuvw2h" 77 - class="text-blue-400 hover:underline active:underline" 78 - > 79 - at://hailey.at/app.bsky.feed.post/3l2zpbbhuvw2h 80 - </A> 81 - </div> 82 <div class="flex items-center gap-1"> 83 <div class="iconify lucide--tag" /> 84 - <A 85 - href="/at://did:plc:wkoofae5uytcm7bjncmev6n6/labels" 86 - class="text-blue-400 hover:underline active:underline" 87 - > 88 - at://pronouns.diy/labels 89 - </A> 90 </div> 91 </div> 92 - <div class="flex gap-2 text-xl"> 93 - <A 94 - href="https://tangled.sh/@pdsls.dev/pdsls/" 95 - target="_blank" 96 - class="flex rounded-full bg-neutral-200 p-1.5 transition-colors duration-300 hover:bg-neutral-700 hover:text-neutral-200 dark:bg-neutral-700 dark:hover:bg-neutral-200 dark:hover:text-neutral-700" 97 > 98 - <span class="iconify i-tangled"></span> 99 - </A> 100 - <A 101 - href="https://bsky.app/profile/did:plc:6q5daed5gutiyerimlrnojnz" 102 - target="_blank" 103 - class="flex rounded-full bg-neutral-200 p-1.5 transition-colors duration-300 hover:bg-neutral-700 hover:text-neutral-200 dark:bg-neutral-700 dark:hover:bg-neutral-200 dark:hover:text-neutral-700" 104 - > 105 - <span class="iconify ri--bluesky"></span> 106 - </A> 107 </div> 108 </div> 109 ); 110 }; 111 - 112 - export { Home };
··· 1 + export const Home = () => { 2 return ( 3 + <div class="flex w-full flex-col gap-4 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> 8 <div class="flex items-center gap-1"> 9 <div class="iconify lucide--search" /> 10 <span> 11 Browse the public data on{" "} 12 + <a class="underline hover:text-blue-400" href="https://atproto.com" target="_blank"> 13 atproto 14 </a> 15 . ··· 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{" "} 30 + <a 31 href="https://constellation.microcosm.blue" 32 + class="underline hover:text-blue-400" 33 target="_blank" 34 > 35 constellation 36 + </a> 37 . 38 </span> 39 </div> 40 <div class="flex items-center gap-1"> 41 <div class="iconify lucide--tag" /> 42 + <span>Query labels from moderation services.</span> 43 </div> 44 </div> 45 + <div class="text-center text-sm italic"> 46 + Made by{" "} 47 + <a 48 + href="https://juli.ee" 49 + class="font-pecita relative after:absolute after:bottom-0 after:left-0 after:h-px after:w-0 after:bg-current after:transition-[width] after:duration-300 after:ease-out hover:after:w-full" 50 > 51 + Juliet 52 + </a>{" "} 53 + with love 54 </div> 55 </div> 56 ); 57 };
+244 -142
src/views/labels.tsx
··· 1 import { ComAtprotoLabelDefs } from "@atcute/atproto"; 2 import { Client, CredentialManager } from "@atcute/client"; 3 - import { A, useParams, useSearchParams } from "@solidjs/router"; 4 - import { createResource, createSignal, For, onMount, Show } from "solid-js"; 5 import { Button } from "../components/button.jsx"; 6 import { StickyOverlay } from "../components/sticky.jsx"; 7 import { TextInput } from "../components/text-input.jsx"; 8 - import { labelerCache, resolvePDS } from "../utils/api.js"; 9 import { localDateFromTimestamp } from "../utils/date.js"; 10 11 - const LabelView = () => { 12 - const params = useParams(); 13 const [searchParams, setSearchParams] = useSearchParams(); 14 const [cursor, setCursor] = createSignal<string>(); 15 const [labels, setLabels] = createSignal<ComAtprotoLabelDefs.Label[]>([]); 16 - const [filter, setFilter] = createSignal<string>(); 17 - const [labelCount, setLabelCount] = createSignal(0); 18 - const did = params.repo; 19 - let rpc: Client; 20 21 onMount(async () => { 22 - await resolvePDS(did); 23 - rpc = new Client({ 24 - handler: new CredentialManager({ service: labelerCache[did] }), 25 - }); 26 - refetch(); 27 }); 28 29 - const fetchLabels = async () => { 30 - const uriPatterns = (document.getElementById("patterns") as HTMLInputElement).value; 31 - if (!uriPatterns) return; 32 - const res = await rpc.get("com.atproto.label.queryLabels", { 33 - params: { 34 - uriPatterns: uriPatterns.toString().trim().split(","), 35 - sources: [did as `did:${string}:${string}`], 36 - cursor: cursor(), 37 - }, 38 - }); 39 - if (!res.ok) throw new Error(res.data.error); 40 - setCursor(res.data.labels.length < 50 ? undefined : res.data.cursor); 41 - setLabels(labels().concat(res.data.labels) ?? res.data.labels); 42 - return res.data.labels; 43 - }; 44 45 - const [response, { refetch }] = createResource(fetchLabels); 46 47 - const initQuery = async () => { 48 - setLabels([]); 49 - setCursor(""); 50 - setSearchParams({ 51 - uriPatterns: (document.getElementById("patterns") as HTMLInputElement).value, 52 - }); 53 - refetch(); 54 }; 55 56 - const filterLabels = () => { 57 - const newFilter = labels().filter((label) => (filter() ? filter() === label.val : true)); 58 - setLabelCount(newFilter.length); 59 - return newFilter; 60 }; 61 62 return ( 63 <div class="flex w-full flex-col items-center"> 64 <form 65 - class="flex w-[22rem] flex-col items-center gap-y-1 sm:w-[24rem]" 66 - onsubmit={(e) => { 67 e.preventDefault(); 68 - initQuery(); 69 }} 70 > 71 - <div class="w-full"> 72 - <label for="patterns" class="ml-0.5 text-sm"> 73 - URI Patterns (comma-separated) 74 </label> 75 </div> 76 - <div class="flex w-full items-center gap-x-1"> 77 - <textarea 78 - id="patterns" 79 - name="patterns" 80 - spellcheck={false} 81 - rows={3} 82 - value={searchParams.uriPatterns ?? "*"} 83 - class="dark:bg-dark-100 dark:shadow-dark-800 grow rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 py-1 shadow-xs focus:outline-[1.5px] focus:outline-neutral-900 dark:border-neutral-700 dark:focus:outline-neutral-200" 84 - /> 85 - <div class="flex justify-center"> 86 - <Show when={!response.loading}> 87 - <button 88 - type="submit" 89 - class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 90 - > 91 - <span class="iconify lucide--search text-lg"></span> 92 - </button> 93 - </Show> 94 - <Show when={response.loading}> 95 - <div class="m-1 flex items-center"> 96 - <span class="iconify lucide--loader-circle animate-spin text-lg"></span> 97 - </div> 98 - </Show> 99 </div> 100 - </div> 101 </form> 102 - <StickyOverlay> 103 - <TextInput 104 - placeholder="Filter by label" 105 - onInput={(e) => setFilter(e.currentTarget.value)} 106 - class="w-[22rem] sm:w-[24rem]" 107 - /> 108 - <div class="flex items-center gap-x-2"> 109 - <Show when={labelCount() && labels().length}> 110 - <div> 111 - <span> 112 - {labelCount()} label{labelCount() > 1 ? "s" : ""} 113 - </span> 114 - </div> 115 - </Show> 116 - <Show when={cursor()}> 117 - <div class="flex h-[2rem] w-[5.5rem] items-center justify-center text-nowrap"> 118 - <Show when={!response.loading}> 119 - <Button onClick={() => refetch()}>Load More</Button> 120 </Show> 121 - <Show when={response.loading}> 122 - <div class="iconify lucide--loader-circle animate-spin text-xl" /> 123 </Show> 124 </div> 125 </Show> 126 - </div> 127 - </StickyOverlay> 128 - <Show when={labels().length}> 129 - <div class="flex max-w-full min-w-[22rem] flex-col gap-2 divide-y-[0.5px] divide-neutral-400 text-sm wrap-anywhere whitespace-pre-wrap sm:min-w-[24rem] dark:divide-neutral-600"> 130 - <For each={filterLabels()}> 131 - {(label) => ( 132 - <div class="flex items-center justify-between gap-2 pb-2"> 133 - <div class="flex flex-col"> 134 - <div class="flex items-center gap-x-2"> 135 - <div class="min-w-[5rem] font-semibold">URI</div> 136 - <A 137 - href={`/at://${label.uri.replace("at://", "")}`} 138 - target="_blank" 139 - class="text-blue-400 hover:underline active:underline" 140 - > 141 - {label.uri} 142 - </A> 143 - </div> 144 - <Show when={label.cid}> 145 - <div class="flex items-center gap-x-2"> 146 - <div class="min-w-[5rem] font-semibold">CID</div> 147 - {label.cid} 148 - </div> 149 - </Show> 150 - <div class="flex items-center gap-x-2"> 151 - <div class="min-w-[5rem] font-semibold">Label</div> 152 - {label.val} 153 - </div> 154 - <div class="flex items-center gap-x-2"> 155 - <div class="min-w-[5rem] font-semibold">Created</div> 156 - {localDateFromTimestamp(new Date(label.cts).getTime())} 157 - </div> 158 - <Show when={label.exp}> 159 - {(exp) => ( 160 - <div class="flex items-center gap-x-2"> 161 - <div class="min-w-[5rem] font-semibold">Expires</div> 162 - {localDateFromTimestamp(new Date(exp()).getTime())} 163 - </div> 164 - )} 165 - </Show> 166 - </div> 167 - <Show when={label.neg}> 168 - <div class="iconify lucide--minus shrink-0 text-lg text-red-500 dark:text-red-400" /> 169 - </Show> 170 </div> 171 - )} 172 - </For> 173 </div> 174 </Show> 175 - <Show when={!labels().length && !response.loading && searchParams.uriPatterns}> 176 - <div class="mt-2">No results</div> 177 - </Show> 178 </div> 179 ); 180 }; 181 - 182 - export { LabelView };
··· 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"; 6 + import { createMemo, createSignal, For, onMount, Show } from "solid-js"; 7 import { Button } from "../components/button.jsx"; 8 import { StickyOverlay } from "../components/sticky.jsx"; 9 import { TextInput } from "../components/text-input.jsx"; 10 + import { labelerCache, resolveHandle, resolvePDS } from "../utils/api.js"; 11 import { localDateFromTimestamp } from "../utils/date.js"; 12 13 + const LABELS_PER_PAGE = 50; 14 + 15 + const LabelCard = (props: { label: ComAtprotoLabelDefs.Label }) => { 16 + const label = props.label; 17 + 18 + return ( 19 + <div class="flex flex-col gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-3 dark:border-neutral-700 dark:bg-neutral-800"> 20 + <div class="flex flex-wrap items-center gap-x-2 gap-y-2"> 21 + <div class="inline-flex items-center gap-x-1 rounded-full bg-neutral-200 px-2 py-0.5 text-sm font-medium text-neutral-800 dark:bg-neutral-700 dark:text-neutral-200"> 22 + <span class="iconify lucide--tag shrink-0" /> 23 + {label.val} 24 + </div> 25 + <Show when={label.neg}> 26 + <div class="inline-flex items-center gap-x-1 rounded-full border border-orange-400 bg-orange-100 px-2 py-0.5 text-xs font-medium text-orange-700 dark:border-orange-600 dark:bg-orange-900/30 dark:text-orange-400"> 27 + <span class="iconify lucide--minus shrink-0 text-sm" /> 28 + <span>Negated</span> 29 + </div> 30 + </Show> 31 + <div class="flex flex-wrap gap-3 text-xs text-neutral-600 dark:text-neutral-400"> 32 + <div class="flex items-center gap-x-1"> 33 + <span class="iconify lucide--calendar shrink-0" /> 34 + <span>{localDateFromTimestamp(new Date(label.cts).getTime())}</span> 35 + </div> 36 + <Show when={label.exp}> 37 + {(exp) => ( 38 + <div class="flex items-center gap-x-1"> 39 + <span class="iconify lucide--clock-fading shrink-0" /> 40 + <span>e{localDateFromTimestamp(new Date(exp()).getTime())}</span> 41 + </div> 42 + )} 43 + </Show> 44 + </div> 45 + </div> 46 + 47 + <div class="flex flex-col gap-y-0.5"> 48 + <div class="text-xs font-medium tracking-wide text-neutral-500 uppercase dark:text-neutral-400"> 49 + URI 50 + </div> 51 + <A 52 + href={`/at://${label.uri.replace("at://", "")}`} 53 + class="text-sm break-all text-blue-600 hover:underline dark:text-blue-400" 54 + > 55 + {label.uri} 56 + </A> 57 + </div> 58 + 59 + <Show when={label.cid}> 60 + <div class="flex flex-col gap-y-0.5"> 61 + <div class="text-xs font-medium tracking-wide text-neutral-500 uppercase dark:text-neutral-400"> 62 + CID 63 + </div> 64 + <div class="text-sm break-all text-neutral-700 dark:text-neutral-300">{label.cid}</div> 65 + </div> 66 + </Show> 67 + </div> 68 + ); 69 + }; 70 + 71 + export const LabelView = () => { 72 const [searchParams, setSearchParams] = useSearchParams(); 73 const [cursor, setCursor] = createSignal<string>(); 74 const [labels, setLabels] = createSignal<ComAtprotoLabelDefs.Label[]>([]); 75 + const [filter, setFilter] = createSignal(""); 76 + const [loading, setLoading] = createSignal(false); 77 + const [error, setError] = createSignal<string>(); 78 + const [didInput, setDidInput] = createSignal(searchParams.did ?? ""); 79 + 80 + let rpc: Client | undefined; 81 + let formRef!: HTMLFormElement; 82 + 83 + const filteredLabels = createMemo(() => { 84 + const filterValue = filter().trim().toLowerCase(); 85 + if (!filterValue) return labels(); 86 + return labels().filter((label) => label.val.toLowerCase().includes(filterValue)); 87 + }); 88 + 89 + const hasSearched = createMemo(() => Boolean(searchParams.uriPatterns)); 90 91 onMount(async () => { 92 + if (searchParams.did && searchParams.uriPatterns) { 93 + const formData = new FormData(); 94 + formData.append("did", searchParams.did.toString()); 95 + formData.append("uriPatterns", searchParams.uriPatterns.toString()); 96 + await fetchLabels(formData); 97 + } 98 }); 99 100 + const fetchLabels = async (formData: FormData, reset?: boolean) => { 101 + let did = formData.get("did")?.toString()?.trim(); 102 + const uriPatterns = formData.get("uriPatterns")?.toString()?.trim(); 103 104 + if (!did || !uriPatterns) { 105 + setError("Please provide both DID and URI patterns"); 106 + return; 107 + } 108 109 + if (reset) { 110 + setLabels([]); 111 + setCursor(undefined); 112 + setError(undefined); 113 + } 114 + 115 + try { 116 + setLoading(true); 117 + setError(undefined); 118 + 119 + if (!isAtprotoDid(did)) did = await resolveHandle(did as Handle); 120 + await resolvePDS(did); 121 + if (!labelerCache[did]) throw new Error("Repository is not a labeler"); 122 + rpc = new Client({ 123 + handler: new CredentialManager({ service: labelerCache[did] }), 124 + }); 125 + 126 + setSearchParams({ did, uriPatterns }); 127 + setDidInput(did); 128 + 129 + const res = await rpc.get("com.atproto.label.queryLabels", { 130 + params: { 131 + uriPatterns: uriPatterns.split(",").map((p) => p.trim()), 132 + sources: [did as `did:${string}:${string}`], 133 + cursor: cursor(), 134 + }, 135 + }); 136 + 137 + if (!res.ok) throw new Error(res.data.error || "Failed to fetch labels"); 138 + 139 + const newLabels = res.data.labels || []; 140 + setCursor(newLabels.length < LABELS_PER_PAGE ? undefined : res.data.cursor); 141 + setLabels(reset ? newLabels : [...labels(), ...newLabels]); 142 + } catch (err) { 143 + setError(err instanceof Error ? err.message : "An error occurred"); 144 + console.error("Failed to fetch labels:", err); 145 + } finally { 146 + setLoading(false); 147 + } 148 }; 149 150 + const handleSearch = () => { 151 + fetchLabels(new FormData(formRef), true); 152 + }; 153 + 154 + const handleLoadMore = () => { 155 + fetchLabels(new FormData(formRef)); 156 }; 157 158 return ( 159 <div class="flex w-full flex-col items-center"> 160 <form 161 + ref={formRef} 162 + class="flex w-full max-w-3xl flex-col gap-y-2 px-3 pb-2" 163 + onSubmit={(e) => { 164 e.preventDefault(); 165 + handleSearch(); 166 }} 167 > 168 + <div class="flex flex-col gap-y-1.5"> 169 + <label class="flex w-full flex-col gap-y-1"> 170 + <span class="text-sm font-medium text-neutral-700 dark:text-neutral-300"> 171 + Labeler DID/Handle 172 + </span> 173 + <TextInput 174 + name="did" 175 + value={didInput()} 176 + onInput={(e) => setDidInput(e.currentTarget.value)} 177 + placeholder="did:plc:..." 178 + class="w-full" 179 + /> 180 + </label> 181 + 182 + <label class="flex w-full flex-col gap-y-1"> 183 + <span class="text-sm font-medium text-neutral-700 dark:text-neutral-300"> 184 + URI Patterns (comma-separated) 185 + </span> 186 + <textarea 187 + id="uriPatterns" 188 + name="uriPatterns" 189 + spellcheck={false} 190 + rows={2} 191 + value={searchParams.uriPatterns ?? "*"} 192 + placeholder="at://did:web:example.com/app.bsky.feed.post/*" 193 + class="dark:bg-dark-100 dark:shadow-dark-700 grow rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 py-1.5 text-sm shadow-xs focus:outline-[1px] focus:outline-neutral-600 dark:border-neutral-600 dark:focus:outline-neutral-400" 194 + /> 195 </label> 196 </div> 197 + 198 + <Button 199 + type="submit" 200 + disabled={loading()} 201 + class="dark:hover:bg-dark-200 dark:shadow-dark-700 dark:active:bg-dark-100 box-border flex h-7 w-fit items-center justify-center gap-1 rounded-lg 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" 202 + > 203 + <span class="iconify lucide--search" /> 204 + <span>Search Labels</span> 205 + </Button> 206 + 207 + <Show when={error()}> 208 + <div class="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-800 dark:border-red-800 dark:bg-red-900/20 dark:text-red-300"> 209 + {error()} 210 </div> 211 + </Show> 212 </form> 213 + 214 + <Show when={hasSearched()}> 215 + <StickyOverlay> 216 + <div class="flex w-full items-center gap-x-2"> 217 + <TextInput 218 + placeholder="Filter by label value" 219 + name="filter" 220 + value={filter()} 221 + onInput={(e) => setFilter(e.currentTarget.value)} 222 + class="min-w-0 grow text-sm" 223 + /> 224 + <div class="flex shrink-0 items-center gap-x-2 text-sm"> 225 + <Show when={labels().length > 0}> 226 + <span class="whitespace-nowrap text-neutral-600 dark:text-neutral-400"> 227 + {filteredLabels().length}/{labels().length} 228 + </span> 229 </Show> 230 + 231 + <Show when={cursor()}> 232 + <Button 233 + onClick={handleLoadMore} 234 + disabled={loading()} 235 + class="dark:hover:bg-dark-200 dark:shadow-dark-700 dark:active:bg-dark-100 box-border flex h-7 w-20 items-center justify-center gap-1 rounded-lg 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" 236 + > 237 + <Show 238 + when={!loading()} 239 + fallback={<span class="iconify lucide--loader-circle animate-spin" />} 240 + > 241 + Load More 242 + </Show> 243 + </Button> 244 </Show> 245 </div> 246 + </div> 247 + </StickyOverlay> 248 + 249 + <div class="w-full max-w-3xl px-3 py-2"> 250 + <Show when={loading() && labels().length === 0}> 251 + <div class="flex flex-col items-center justify-center py-12 text-center"> 252 + <span class="iconify lucide--loader-circle mb-3 animate-spin text-4xl text-neutral-400" /> 253 + <p class="text-sm text-neutral-600 dark:text-neutral-400">Loading labels...</p> 254 + </div> 255 </Show> 256 + 257 + <Show when={!loading() || labels().length > 0}> 258 + <Show when={filteredLabels().length > 0}> 259 + <div class="grid gap-2"> 260 + <For each={filteredLabels()}>{(label) => <LabelCard label={label} />}</For> 261 </div> 262 + </Show> 263 + 264 + <Show when={labels().length > 0 && filteredLabels().length === 0}> 265 + <div class="flex flex-col items-center justify-center py-8 text-center"> 266 + <span class="iconify lucide--search-x mb-2 text-3xl text-neutral-400" /> 267 + <p class="text-sm text-neutral-600 dark:text-neutral-400"> 268 + No labels match your filter 269 + </p> 270 + </div> 271 + </Show> 272 + 273 + <Show when={labels().length === 0 && !loading()}> 274 + <div class="flex flex-col items-center justify-center py-8 text-center"> 275 + <span class="iconify lucide--tags mb-2 text-3xl text-neutral-400" /> 276 + <p class="text-sm text-neutral-600 dark:text-neutral-400">No labels found</p> 277 + </div> 278 + </Show> 279 + </Show> 280 </div> 281 </Show> 282 </div> 283 ); 284 };
+278
src/views/logs.tsx
···
··· 1 + import { 2 + CompatibleOperationOrTombstone, 3 + defs, 4 + IndexedEntry, 5 + processIndexedEntryLog, 6 + } from "@atcute/did-plc"; 7 + import { createResource, createSignal, For, Show } from "solid-js"; 8 + import { localDateFromTimestamp } from "../utils/date.js"; 9 + import { createOperationHistory, DiffEntry, groupBy } from "../utils/plc-logs.js"; 10 + 11 + type PlcEvent = "handle" | "rotation_key" | "service" | "verification_method"; 12 + 13 + export const PlcLogView = (props: { did: string }) => { 14 + const [activePlcEvent, setActivePlcEvent] = createSignal<PlcEvent | undefined>(); 15 + 16 + const shouldShowDiff = (diff: DiffEntry) => 17 + !activePlcEvent() || diff.type.startsWith(activePlcEvent()!); 18 + 19 + const shouldShowEntry = (diffs: DiffEntry[]) => 20 + !activePlcEvent() || diffs.some((d) => d.type.startsWith(activePlcEvent()!)); 21 + 22 + const fetchPlcLogs = async () => { 23 + const res = await fetch( 24 + `${localStorage.plcDirectory ?? "https://plc.directory"}/${props.did}/log/audit`, 25 + ); 26 + const json = await res.json(); 27 + const logs = defs.indexedEntryLog.parse(json); 28 + try { 29 + await processIndexedEntryLog(props.did as any, logs); 30 + } catch (e) { 31 + console.error(e); 32 + } 33 + const opHistory = createOperationHistory(logs).reverse(); 34 + return Array.from(groupBy(opHistory, (item) => item.orig)); 35 + }; 36 + 37 + const [plcOps] = 38 + createResource<[IndexedEntry<CompatibleOperationOrTombstone>, DiffEntry[]][]>(fetchPlcLogs); 39 + 40 + const FilterButton = (props: { icon: string; event: PlcEvent; label: string }) => { 41 + const isActive = () => activePlcEvent() === props.event; 42 + const toggleFilter = () => setActivePlcEvent(isActive() ? undefined : props.event); 43 + 44 + return ( 45 + <button 46 + classList={{ 47 + "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, 48 + "bg-neutral-700 text-white dark:bg-neutral-200 dark:text-neutral-900": isActive(), 49 + "bg-neutral-200 text-neutral-700 hover:bg-neutral-300 dark:bg-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-600": 50 + !isActive(), 51 + }} 52 + onclick={toggleFilter} 53 + > 54 + <span class={props.icon}></span> 55 + <span class="hidden font-medium sm:inline">{props.label}</span> 56 + </button> 57 + ); 58 + }; 59 + 60 + const DiffItem = (props: { diff: DiffEntry }) => { 61 + const diff = props.diff; 62 + 63 + const getDiffConfig = () => { 64 + switch (diff.type) { 65 + case "identity_created": 66 + return { icon: "lucide--bell", title: "Identity created" }; 67 + case "identity_tombstoned": 68 + return { icon: "lucide--skull", title: "Identity tombstoned" }; 69 + case "handle_added": 70 + return { 71 + icon: "lucide--at-sign", 72 + title: "Alias added", 73 + value: diff.handle, 74 + isAddition: true, 75 + }; 76 + case "handle_removed": 77 + return { 78 + icon: "lucide--at-sign", 79 + title: "Alias removed", 80 + value: diff.handle, 81 + isRemoval: true, 82 + }; 83 + case "handle_changed": 84 + return { 85 + icon: "lucide--at-sign", 86 + title: "Alias updated", 87 + oldValue: diff.prev_handle, 88 + newValue: diff.next_handle, 89 + }; 90 + case "rotation_key_added": 91 + return { 92 + icon: "lucide--key-round", 93 + title: "Rotation key added", 94 + value: diff.rotation_key, 95 + isAddition: true, 96 + }; 97 + case "rotation_key_removed": 98 + return { 99 + icon: "lucide--key-round", 100 + title: "Rotation key removed", 101 + value: diff.rotation_key, 102 + isRemoval: true, 103 + }; 104 + case "service_added": 105 + return { 106 + icon: "lucide--hard-drive", 107 + title: "Service added", 108 + badge: diff.service_id, 109 + value: diff.service_endpoint, 110 + isAddition: true, 111 + }; 112 + case "service_removed": 113 + return { 114 + icon: "lucide--hard-drive", 115 + title: "Service removed", 116 + badge: diff.service_id, 117 + value: diff.service_endpoint, 118 + isRemoval: true, 119 + }; 120 + case "service_changed": 121 + return { 122 + icon: "lucide--hard-drive", 123 + title: "Service updated", 124 + badge: diff.service_id, 125 + oldValue: diff.prev_service_endpoint, 126 + newValue: diff.next_service_endpoint, 127 + }; 128 + case "verification_method_added": 129 + return { 130 + icon: "lucide--shield-check", 131 + title: "Verification method added", 132 + badge: diff.method_id, 133 + value: diff.method_key, 134 + isAddition: true, 135 + }; 136 + case "verification_method_removed": 137 + return { 138 + icon: "lucide--shield-check", 139 + title: "Verification method removed", 140 + badge: diff.method_id, 141 + value: diff.method_key, 142 + isRemoval: true, 143 + }; 144 + case "verification_method_changed": 145 + return { 146 + icon: "lucide--shield-check", 147 + title: "Verification method updated", 148 + badge: diff.method_id, 149 + oldValue: diff.prev_method_key, 150 + newValue: diff.next_method_key, 151 + }; 152 + default: 153 + return { icon: "lucide--circle-help", title: "Unknown log entry" }; 154 + } 155 + }; 156 + 157 + const config = getDiffConfig(); 158 + const { 159 + icon, 160 + title, 161 + value = "", 162 + oldValue = "", 163 + newValue = "", 164 + badge = "", 165 + isAddition = false, 166 + isRemoval = false, 167 + } = config; 168 + 169 + return ( 170 + <div 171 + classList={{ 172 + "grid grid-cols-[auto_1fr] gap-y-0.5 gap-x-2": true, 173 + "opacity-60": diff.orig.nullified, 174 + }} 175 + > 176 + <div class={`${icon} iconify shrink-0 self-center`} /> 177 + <div class="flex min-w-0 items-center gap-1.5"> 178 + <p 179 + classList={{ 180 + "font-semibold text-sm": true, 181 + "line-through": diff.orig.nullified, 182 + }} 183 + > 184 + {title} 185 + </p> 186 + <Show when={badge}> 187 + <span class="shrink-0 rounded bg-neutral-200 px-1.5 py-0.5 text-xs font-medium dark:bg-neutral-700"> 188 + #{badge} 189 + </span> 190 + </Show> 191 + <Show when={diff.orig.nullified}> 192 + <span class="ml-auto rounded bg-neutral-200 px-2 py-0.5 text-xs font-medium dark:bg-neutral-700"> 193 + Nullified 194 + </span> 195 + </Show> 196 + </div> 197 + <Show when={value}> 198 + <div></div> 199 + <div 200 + classList={{ 201 + "text-sm break-all flex items-start gap-2 min-w-0": true, 202 + "text-green-500 dark:text-green-300": isAddition, 203 + "text-red-400 dark:text-red-300": isRemoval, 204 + "text-neutral-600 dark:text-neutral-400": !isAddition && !isRemoval, 205 + }} 206 + > 207 + <Show when={isAddition}> 208 + <span class="shrink-0">+</span> 209 + </Show> 210 + <Show when={isRemoval}> 211 + <span class="shrink-0">โˆ’</span> 212 + </Show> 213 + <span class="break-all">{value}</span> 214 + </div> 215 + </Show> 216 + <Show when={oldValue && newValue}> 217 + <div></div> 218 + <div class="flex min-w-0 flex-col text-sm"> 219 + <div class="flex items-start gap-2 text-red-400 dark:text-red-300"> 220 + <span class="shrink-0">โˆ’</span> 221 + <span class="break-all">{oldValue}</span> 222 + </div> 223 + <div class="flex items-start gap-2 text-green-500 dark:text-green-300"> 224 + <span class="shrink-0">+</span> 225 + <span class="break-all">{newValue}</span> 226 + </div> 227 + </div> 228 + </Show> 229 + </div> 230 + ); 231 + }; 232 + 233 + return ( 234 + <div class="flex w-full flex-col gap-4 wrap-anywhere"> 235 + <div class="flex flex-col gap-2"> 236 + <div class="flex items-center gap-1.5 text-sm"> 237 + <div class="iconify lucide--filter" /> 238 + <p class="font-semibold">Filter by type</p> 239 + </div> 240 + <div class="flex flex-wrap gap-1 sm:gap-2"> 241 + <FilterButton icon="iconify lucide--at-sign" event="handle" label="Alias" /> 242 + <FilterButton 243 + icon="iconify lucide--key-round" 244 + event="rotation_key" 245 + label="Rotation Key" 246 + /> 247 + <FilterButton icon="iconify lucide--hard-drive" event="service" label="Service" /> 248 + <FilterButton 249 + icon="iconify lucide--shield-check" 250 + event="verification_method" 251 + label="Verification" 252 + /> 253 + </div> 254 + </div> 255 + <div class="flex flex-col gap-3"> 256 + <For each={plcOps()}> 257 + {([entry, diffs]) => ( 258 + <Show when={shouldShowEntry(diffs)}> 259 + <div class="flex flex-col gap-1"> 260 + <div class="flex items-center gap-2 text-sm"> 261 + <div class="iconify lucide--clock text-neutral-600 dark:text-neutral-400" /> 262 + <span class="font-medium text-neutral-700 dark:text-neutral-300"> 263 + {localDateFromTimestamp(new Date(entry.createdAt).getTime())} 264 + </span> 265 + </div> 266 + <div class="flex flex-col gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-3 text-sm dark:border-neutral-700 dark:bg-neutral-800"> 267 + <For each={diffs.filter(shouldShowDiff)}> 268 + {(diff) => <DiffItem diff={diff} />} 269 + </For> 270 + </div> 271 + </div> 272 + </Show> 273 + )} 274 + </For> 275 + </div> 276 + </div> 277 + ); 278 + };
+189 -67
src/views/pds.tsx
··· 2 import { Client, CredentialManager } from "@atcute/client"; 3 import { InferXRPCBodyOutput } from "@atcute/lexicons"; 4 import * as TID from "@atcute/tid"; 5 - import { A, useParams } from "@solidjs/router"; 6 import { createResource, createSignal, For, Show } from "solid-js"; 7 import { Button } from "../components/button"; 8 import { setPDS } from "../components/navbar"; 9 import Tooltip from "../components/tooltip"; 10 import { localDateFromTimestamp } from "../utils/date"; ··· 13 14 const PdsView = () => { 15 const params = useParams(); 16 - if (params.pds.startsWith("web%2Bat%3A%2F%2F")) return; 17 const [version, setVersion] = createSignal<string>(); 18 const [serverInfos, setServerInfos] = 19 createSignal<InferXRPCBodyOutput<ComAtprotoServerDescribeServer.mainSchema["output"]>>(); ··· 28 setVersion((res.data as any).version); 29 }; 30 31 const fetchRepos = async () => { 32 - await getVersion(); 33 - const describeRes = await rpc.get("com.atproto.server.describeServer"); 34 - if (!describeRes.ok) console.error(describeRes.data.error); 35 - else setServerInfos(describeRes.data); 36 const res = await rpc.get("com.atproto.sync.listRepos", { 37 params: { limit: LIMIT, cursor: cursor() }, 38 }); 39 if (!res.ok) throw new Error(res.data.error); 40 setCursor(res.data.repos.length < LIMIT ? undefined : res.data.cursor); 41 setRepos(repos()?.concat(res.data.repos) ?? res.data.repos); 42 - await getVersion(); 43 return res.data; 44 }; 45 46 const [response, { refetch }] = createResource(fetchRepos); 47 const [repos, setRepos] = createSignal<ComAtprotoSyncListRepos.Repo[]>(); 48 49 - return ( 50 - <Show when={repos() || response()}> 51 - <div class="flex w-[22rem] flex-col sm:w-[24rem]"> 52 - <Show when={version()}> 53 - {(version) => ( 54 - <div class="flex items-baseline gap-x-1"> 55 - <span class="font-semibold">Version</span> 56 - <span class="truncate text-sm">{version()}</span> 57 - </div> 58 - )} 59 - </Show> 60 - <Show when={serverInfos()}> 61 - {(server) => ( 62 - <> 63 - <Show when={server().inviteCodeRequired}> 64 - <span class="font-semibold">Invite Code Required</span> 65 - </Show> 66 - <Show when={server().phoneVerificationRequired}> 67 - <span class="font-semibold">Phone Verification Required</span> 68 - </Show> 69 - <Show when={server().availableUserDomains.length}> 70 - <div class="flex flex-col"> 71 - <span class="font-semibold">Available User Domains</span> 72 - <For each={server().availableUserDomains}> 73 - {(domain) => <span class="text-sm wrap-anywhere">{domain}</span>} 74 - </For> 75 - </div> 76 - </Show> 77 - </> 78 - )} 79 </Show> 80 - <p class="w-full font-semibold">{repos()?.length} Repositories</p> 81 - <For each={repos()}> 82 - {(repo) => ( 83 - <A 84 - href={`/at://${repo.did}`} 85 - classList={{ 86 - "rounded items-center text-sm gap-1 flex justify-between font-mono relative hover:bg-neutral-200 dark:hover:bg-neutral-700 active:bg-neutral-300 dark:active:bg-neutral-600": true, 87 - "text-blue-400": repo.active, 88 - "text-neutral-400 dark:text-neutral-500": !repo.active, 89 - }} 90 - > 91 - <Show when={!repo.active}> 92 - <div class="absolute -left-4"> 93 - <Tooltip text={repo.status ?? "Unknown status"}> 94 - <span class="iconify lucide--unplug"></span> 95 - </Tooltip> 96 - </div> 97 - </Show> 98 - <span class="text-sm">{repo.did}</span> 99 <Show when={TID.validate(repo.rev)}> 100 - <span class="text-xs text-neutral-500 dark:text-neutral-400"> 101 - {localDateFromTimestamp(TID.parse(repo.rev).timestamp / 1000).split(" ")[0]} 102 </span> 103 </Show> 104 - </A> 105 - )} 106 - </For> 107 </div> 108 - <Show when={cursor()}> 109 - <div class="dark:bg-dark-500 fixed bottom-0 z-5 flex w-screen justify-center bg-neutral-100 py-3"> 110 - <Show when={!response.loading}> 111 - <Button onClick={() => refetch()}>Load More</Button> 112 </Show> 113 - <Show when={response.loading}> 114 - <span class="iconify lucide--loader-circle animate-spin py-3.5 text-xl"></span> 115 </Show> 116 </div> 117 </Show> 118 </Show>
··· 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"; ··· 15 16 const PdsView = () => { 17 const params = useParams(); 18 + const location = useLocation(); 19 const [version, setVersion] = createSignal<string>(); 20 const [serverInfos, setServerInfos] = 21 createSignal<InferXRPCBodyOutput<ComAtprotoServerDescribeServer.mainSchema["output"]>>(); ··· 30 setVersion((res.data as any).version); 31 }; 32 33 + const describeServer = async () => { 34 + const res = await rpc.get("com.atproto.server.describeServer"); 35 + if (!res.ok) console.error(res.data.error); 36 + else setServerInfos(res.data); 37 + }; 38 + 39 const fetchRepos = async () => { 40 + getVersion(); 41 + describeServer(); 42 const res = await rpc.get("com.atproto.sync.listRepos", { 43 params: { limit: LIMIT, cursor: cursor() }, 44 }); 45 if (!res.ok) throw new Error(res.data.error); 46 setCursor(res.data.repos.length < LIMIT ? undefined : res.data.cursor); 47 setRepos(repos()?.concat(res.data.repos) ?? res.data.repos); 48 return res.data; 49 }; 50 51 const [response, { refetch }] = createResource(fetchRepos); 52 const [repos, setRepos] = createSignal<ComAtprotoSyncListRepos.Repo[]>(); 53 54 + const RepoCard = (repo: ComAtprotoSyncListRepos.Repo) => { 55 + const [openInfo, setOpenInfo] = createSignal(false); 56 + 57 + return ( 58 + <div class="flex items-center"> 59 + <A 60 + href={`/at://${repo.did}`} 61 + class="grow truncate rounded py-0.5 font-mono hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 62 + > 63 + {repo.did} 64 + </A> 65 + <Show when={!repo.active}> 66 + <Tooltip text={repo.status ?? "Unknown status"}> 67 + <span class="iconify lucide--unplug text-red-500 dark:text-red-400"></span> 68 + </Tooltip> 69 </Show> 70 + <button 71 + onclick={() => setOpenInfo(true)} 72 + 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" 73 + > 74 + <span class="iconify lucide--info"></span> 75 + </button> 76 + <Modal open={openInfo()} onClose={() => setOpenInfo(false)}> 77 + <div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-70 left-[50%] w-max max-w-full -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-3 wrap-break-word shadow-md transition-opacity duration-200 sm:max-w-lg dark:border-neutral-700 starting:opacity-0"> 78 + <div class="mb-1 flex justify-between gap-2"> 79 + <div class="flex items-center gap-1"> 80 + <span class="iconify lucide--info"></span> 81 + <span class="font-semibold">{repo.did}</span> 82 + </div> 83 + <button 84 + onclick={() => setOpenInfo(false)} 85 + class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 86 + > 87 + <span class="iconify lucide--x"></span> 88 + </button> 89 + </div> 90 + <div class="flex flex-col text-sm"> 91 + <span> 92 + Head: <span class="text-xs">{repo.head}</span> 93 + </span> 94 <Show when={TID.validate(repo.rev)}> 95 + <span> 96 + Rev: {repo.rev} ({localDateFromTimestamp(TID.parse(repo.rev).timestamp / 1000)}) 97 </span> 98 </Show> 99 + <Show when={repo.active !== undefined}> 100 + <span>Active: {repo.active ? "true" : "false"}</span> 101 + </Show> 102 + <Show when={repo.status}> 103 + <span>Status: {repo.status}</span> 104 + </Show> 105 + </div> 106 + </div> 107 + </Modal> 108 </div> 109 + ); 110 + }; 111 + 112 + const Tab = (props: { tab: "repos" | "info"; label: string }) => ( 113 + <div class="flex items-center gap-0.5"> 114 + <A 115 + classList={{ 116 + "flex items-center gap-1 border-b-2": true, 117 + "border-transparent hover:border-neutral-400 dark:hover:border-neutral-600": 118 + (!!location.hash && location.hash !== `#${props.tab}`) || 119 + (!location.hash && props.tab !== "repos"), 120 + }} 121 + href={`/${params.pds}#${props.tab}`} 122 + > 123 + {props.label} 124 + </A> 125 + </div> 126 + ); 127 + 128 + return ( 129 + <Show when={repos() || response()}> 130 + <div class="flex w-full flex-col"> 131 + <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"> 132 + <div class="flex gap-3"> 133 + <Tab tab="repos" label="Repositories" /> 134 + <Tab tab="info" label="Info" /> 135 + </div> 136 + <MenuProvider> 137 + <DropdownMenu 138 + icon="lucide--ellipsis-vertical" 139 + buttonClass="rounded-sm p-1.5" 140 + menuClass="top-9 p-2 text-sm" 141 + > 142 + <CopyMenu content={params.pds} label="Copy PDS" icon="lucide--copy" /> 143 + <NavMenu 144 + href={`/firehose?instance=wss://${params.pds}`} 145 + label="Firehose" 146 + icon="lucide--radio-tower" 147 + /> 148 + </DropdownMenu> 149 + </MenuProvider> 150 + </div> 151 + <div class="flex flex-col gap-1 px-2"> 152 + <Show when={!location.hash || location.hash === "#repos"}> 153 + <div class="flex flex-col divide-y-[0.5px] divide-neutral-300 dark:divide-neutral-700"> 154 + <For each={repos()}>{(repo) => <RepoCard {...repo} />}</For> 155 + </div> 156 </Show> 157 + <Show when={location.hash === "#info"}> 158 + <Show when={version()}> 159 + {(version) => ( 160 + <div class="flex items-baseline gap-x-1"> 161 + <span class="font-semibold">Version</span> 162 + <span class="truncate text-sm">{version()}</span> 163 + </div> 164 + )} 165 + </Show> 166 + <Show when={serverInfos()}> 167 + {(server) => ( 168 + <> 169 + <div class="flex items-baseline gap-x-1"> 170 + <span class="font-semibold">DID</span> 171 + <span class="truncate text-sm">{server().did}</span> 172 + </div> 173 + <Show when={server().inviteCodeRequired}> 174 + <span class="font-semibold">Invite Code Required</span> 175 + </Show> 176 + <Show when={server().phoneVerificationRequired}> 177 + <span class="font-semibold">Phone Verification Required</span> 178 + </Show> 179 + <Show when={server().availableUserDomains.length}> 180 + <div class="flex flex-col"> 181 + <span class="font-semibold">Available User Domains</span> 182 + <For each={server().availableUserDomains}> 183 + {(domain) => <span class="text-sm wrap-anywhere">{domain}</span>} 184 + </For> 185 + </div> 186 + </Show> 187 + <Show when={server().links?.privacyPolicy}> 188 + <div class="flex flex-col"> 189 + <span class="font-semibold">Privacy Policy</span> 190 + <a 191 + href={server().links?.privacyPolicy} 192 + class="text-sm hover:underline" 193 + target="_blank" 194 + rel="noopener" 195 + > 196 + {server().links?.privacyPolicy} 197 + </a> 198 + </div> 199 + </Show> 200 + <Show when={server().links?.termsOfService}> 201 + <div class="flex flex-col"> 202 + <span class="font-semibold">Terms of Service</span> 203 + <a 204 + href={server().links?.termsOfService} 205 + class="text-sm hover:underline" 206 + target="_blank" 207 + rel="noopener" 208 + > 209 + {server().links?.termsOfService} 210 + </a> 211 + </div> 212 + </Show> 213 + <Show when={server().contact?.email}> 214 + <div class="flex flex-col"> 215 + <span class="font-semibold">Contact</span> 216 + <a href={`mailto:${server().contact?.email}`} class="text-sm hover:underline"> 217 + {server().contact?.email} 218 + </a> 219 + </div> 220 + </Show> 221 + </> 222 + )} 223 + </Show> 224 </Show> 225 + </div> 226 + </div> 227 + <Show when={!location.hash || location.hash === "#repos"}> 228 + <div class="dark:bg-dark-500 fixed bottom-0 z-5 flex w-screen justify-center bg-neutral-100 py-2"> 229 + <div class="flex flex-col items-center gap-1 pb-2"> 230 + <p>{repos()?.length} loaded</p> 231 + <Show when={!response.loading && cursor()}> 232 + <Button onClick={() => refetch()}>Load More</Button> 233 + </Show> 234 + <Show when={response.loading}> 235 + <span class="iconify lucide--loader-circle animate-spin py-3.5 text-xl"></span> 236 + </Show> 237 + </div> 238 </div> 239 </Show> 240 </Show>
+202 -61
src/views/record.tsx
··· 1 import { Client, CredentialManager } from "@atcute/client"; 2 import { lexiconDoc } from "@atcute/lexicon-doc"; 3 - import { ActorIdentifier, is, ResourceUri } from "@atcute/lexicons"; 4 import { A, useLocation, useNavigate, useParams } from "@solidjs/router"; 5 import { createResource, createSignal, ErrorBoundary, Show, Suspense } from "solid-js"; 6 import { Backlinks } from "../components/backlinks.jsx"; 7 import { Button } from "../components/button.jsx"; 8 - import { RecordEditor } from "../components/create.jsx"; 9 - import { CopyMenu, DropdownMenu, MenuProvider, NavMenu } from "../components/dropdown.jsx"; 10 import { JSONValue } from "../components/json.jsx"; 11 import { agent } from "../components/login.jsx"; 12 import { Modal } from "../components/modal.jsx"; 13 - import { pds, setCID, setValidRecord, setValidSchema, validRecord } from "../components/navbar.jsx"; 14 import Tooltip from "../components/tooltip.jsx"; 15 - import { setNotif } from "../layout.jsx"; 16 - import { didDocCache, resolvePDS } from "../utils/api.js"; 17 import { AtUri, uriTemplates } from "../utils/templates.js"; 18 import { lexicons } from "../utils/types/lexicons.js"; 19 - import { verifyRecord } from "../utils/verify.js"; 20 21 export const RecordView = () => { 22 const location = useLocation(); ··· 27 const [externalLink, setExternalLink] = createSignal< 28 { label: string; link: string; icon?: string } | undefined 29 >(); 30 const did = params.repo; 31 let rpc: Client; 32 33 const fetchRecord = async () => { 34 - setCID(undefined); 35 setValidRecord(undefined); 36 setValidSchema(undefined); 37 const pds = await resolvePDS(did); 38 rpc = new Client({ handler: new CredentialManager({ service: pds }) }); 39 const res = await rpc.get("com.atproto.repo.getRecord", { ··· 48 setNotice(res.data.error); 49 throw new Error(res.data.error); 50 } 51 - setCID(res.data.cid); 52 setExternalLink(checkUri(res.data.uri, res.data.value)); 53 verify(res.data); 54 55 return res.data; 56 }; 57 58 const verify = async (record: { 59 uri: ResourceUri; 60 value: Record<string, unknown>; ··· 65 if (is(lexicons[params.collection], record.value)) setValidSchema(true); 66 else setValidSchema(false); 67 } else if (params.collection === "com.atproto.lexicon.schema") { 68 try { 69 lexiconDoc.parse(record.value, { mode: "passthrough" }); 70 setValidSchema(true); ··· 73 setValidSchema(false); 74 } 75 } 76 - const { errors } = await verifyRecord({ 77 - rpc: rpc, 78 - uri: record.uri, 79 - cid: record.cid!, 80 - record: record.value, 81 - didDoc: didDocCache[record.uri.split("/")[2]], 82 }); 83 84 - if (errors.length > 0) { 85 - console.warn(errors); 86 - setNotice(`Invalid record: ${errors.map((e) => e.message).join("\n")}`); 87 - } 88 - setValidRecord(errors.length === 0); 89 - } catch (err) { 90 console.error(err); 91 setValidRecord(false); 92 } 93 }; 94 95 - const [record, { refetch }] = createResource(fetchRecord); 96 97 const deleteRecord = async () => { 98 rpc = new Client({ handler: agent()! }); ··· 103 rkey: params.rkey, 104 }, 105 }); 106 - setNotif({ show: true, icon: "lucide--trash-2", text: "Record deleted" }); 107 navigate(`/at://${params.repo}/${params.collection}`); 108 }; 109 ··· 117 return template(parsedUri, record); 118 }; 119 120 return ( 121 <Show when={record()} keyed> 122 <div class="flex w-full flex-col items-center"> 123 - <div class="dark:shadow-dark-800 dark:bg-dark-300 mb-3 flex w-[22rem] justify-between rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 px-2 py-1.5 shadow-xs sm:w-[24rem] dark:border-neutral-700"> 124 - <div class="flex gap-3 text-sm"> 125 - <A 126 - classList={{ 127 - "flex items-center gap-1 border-b-2": true, 128 - "border-transparent hover:border-neutral-400 dark:hover:border-neutral-600": 129 - !!location.hash && location.hash !== "#record", 130 - }} 131 - href={`/at://${did}/${params.collection}/${params.rkey}#record`} 132 - > 133 - <div class="iconify lucide--file-json" /> 134 - Record 135 - </A> 136 - <A 137 - classList={{ 138 - "flex items-center gap-1 border-b-2": true, 139 - "border-transparent hover:border-neutral-400 dark:hover:border-neutral-600": 140 - location.hash !== "#backlinks", 141 - }} 142 - href={`/at://${did}/${params.collection}/${params.rkey}#backlinks`} 143 - > 144 - <div class="iconify lucide--send-to-back" /> 145 - Backlinks 146 - </A> 147 </div> 148 - <div class="flex gap-1"> 149 <Show when={agent() && agent()?.sub === record()?.uri.split("/")[2]}> 150 <RecordEditor create={false} record={record()?.value} refetch={refetch} /> 151 <Tooltip text="Delete"> 152 <button 153 - class="flex items-center rounded-sm p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 154 onclick={() => setOpenDelete(true)} 155 > 156 <span class="iconify lucide--trash-2"></span> 157 </button> 158 </Tooltip> 159 <Modal open={openDelete()} onClose={() => setOpenDelete(false)}> 160 - <div class="dark:bg-dark-300 dark:shadow-dark-800 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-300 dark:border-neutral-700 starting:opacity-0"> 161 <h2 class="mb-2 font-semibold">Delete this record?</h2> 162 <div class="flex justify-end gap-2"> 163 <Button onClick={() => setOpenDelete(false)}>Cancel</Button> 164 <Button 165 onClick={deleteRecord} 166 - class="dark:shadow-dark-800 rounded-lg bg-red-500 px-2 py-1.5 text-xs font-semibold text-neutral-200 shadow-xs select-none hover:bg-red-400 active:bg-red-400" 167 > 168 Delete 169 </Button> ··· 173 </Show> 174 <MenuProvider> 175 <DropdownMenu 176 - icon="lucide--ellipsis-vertical " 177 - buttonClass="rounded-sm p-1" 178 - menuClass="top-8 p-2 text-sm" 179 > 180 <CopyMenu 181 - copyContent={JSON.stringify(record()?.value, null, 2)} 182 label="Copy record" 183 icon="lucide--copy" 184 /> 185 <Show when={externalLink()}> 186 {(externalLink) => ( 187 <NavMenu ··· 203 </div> 204 </div> 205 <Show when={!location.hash || location.hash === "#record"}> 206 - <Show when={validRecord() === false}> 207 - <div class="mb-2 break-words text-red-500 dark:text-red-400">{notice()}</div> 208 - </Show> 209 - <div class="w-[22rem] font-mono text-xs wrap-anywhere whitespace-pre-wrap sm:w-full sm:text-sm"> 210 <JSONValue data={record()?.value as any} repo={record()!.uri.split("/")[2]} /> 211 </div> 212 </Show> 213 <Show when={location.hash === "#backlinks"}> 214 - <ErrorBoundary fallback={(err) => <div class="break-words">Error: {err.message}</div>}> 215 <Suspense 216 fallback={ 217 <div class="iconify lucide--loader-circle animate-spin self-center text-xl" /> 218 } 219 > 220 - <Backlinks target={`at://${did}/${params.collection}/${params.rkey}`} /> 221 </Suspense> 222 </ErrorBoundary> 223 </Show> 224 </div> 225 </Show>
··· 1 import { Client, CredentialManager } from "@atcute/client"; 2 import { lexiconDoc } from "@atcute/lexicon-doc"; 3 + import { ResolvedSchema } from "@atcute/lexicon-resolver"; 4 + import { ActorIdentifier, is, Nsid, ResourceUri } from "@atcute/lexicons"; 5 + import { AtprotoDid, Did } from "@atcute/lexicons/syntax"; 6 + import { verifyRecord } from "@atcute/repo"; 7 import { A, useLocation, useNavigate, useParams } from "@solidjs/router"; 8 import { createResource, createSignal, ErrorBoundary, Show, Suspense } from "solid-js"; 9 import { Backlinks } from "../components/backlinks.jsx"; 10 import { Button } from "../components/button.jsx"; 11 + import { RecordEditor, setPlaceholder } from "../components/create.jsx"; 12 + import { 13 + CopyMenu, 14 + DropdownMenu, 15 + MenuProvider, 16 + MenuSeparator, 17 + NavMenu, 18 + } from "../components/dropdown.jsx"; 19 import { JSONValue } from "../components/json.jsx"; 20 + import { LexiconSchemaView } from "../components/lexicon-schema.jsx"; 21 import { agent } from "../components/login.jsx"; 22 import { Modal } from "../components/modal.jsx"; 23 + import { pds } from "../components/navbar.jsx"; 24 + import { addNotification, removeNotification } from "../components/notification.jsx"; 25 import Tooltip from "../components/tooltip.jsx"; 26 + import { resolveLexiconAuthority, resolveLexiconSchema, resolvePDS } from "../utils/api.js"; 27 import { AtUri, uriTemplates } from "../utils/templates.js"; 28 import { lexicons } from "../utils/types/lexicons.js"; 29 30 export const RecordView = () => { 31 const location = useLocation(); ··· 36 const [externalLink, setExternalLink] = createSignal< 37 { label: string; link: string; icon?: string } | undefined 38 >(); 39 + const [lexiconUri, setLexiconUri] = createSignal<string>(); 40 + const [validRecord, setValidRecord] = createSignal<boolean | undefined>(undefined); 41 + const [validSchema, setValidSchema] = createSignal<boolean | undefined>(undefined); 42 + const [schema, setSchema] = createSignal<ResolvedSchema>(); 43 + const [lexiconNotFound, setLexiconNotFound] = createSignal<boolean>(); 44 const did = params.repo; 45 let rpc: Client; 46 47 const fetchRecord = async () => { 48 setValidRecord(undefined); 49 setValidSchema(undefined); 50 + setLexiconUri(undefined); 51 const pds = await resolvePDS(did); 52 rpc = new Client({ handler: new CredentialManager({ service: pds }) }); 53 const res = await rpc.get("com.atproto.repo.getRecord", { ··· 62 setNotice(res.data.error); 63 throw new Error(res.data.error); 64 } 65 + setPlaceholder(res.data.value); 66 setExternalLink(checkUri(res.data.uri, res.data.value)); 67 + resolveLexicon(params.collection as Nsid); 68 verify(res.data); 69 70 return res.data; 71 }; 72 73 + const [record, { refetch }] = createResource(fetchRecord); 74 + 75 const verify = async (record: { 76 uri: ResourceUri; 77 value: Record<string, unknown>; ··· 82 if (is(lexicons[params.collection], record.value)) setValidSchema(true); 83 else setValidSchema(false); 84 } else if (params.collection === "com.atproto.lexicon.schema") { 85 + setLexiconNotFound(false); 86 try { 87 lexiconDoc.parse(record.value, { mode: "passthrough" }); 88 setValidSchema(true); ··· 91 setValidSchema(false); 92 } 93 } 94 + 95 + const { ok, data } = await rpc.get("com.atproto.sync.getRecord", { 96 + params: { 97 + did: did as Did, 98 + collection: params.collection as Nsid, 99 + rkey: params.rkey, 100 + }, 101 + as: "bytes", 102 + }); 103 + if (!ok) throw data.error; 104 + 105 + await verifyRecord({ 106 + did: did as AtprotoDid, 107 + collection: params.collection, 108 + rkey: params.rkey, 109 + carBytes: data, 110 }); 111 112 + setValidRecord(true); 113 + } catch (err: any) { 114 console.error(err); 115 + setNotice(err.message); 116 setValidRecord(false); 117 } 118 }; 119 120 + const resolveLexicon = async (nsid: Nsid) => { 121 + try { 122 + const authority = await resolveLexiconAuthority(nsid); 123 + setLexiconUri(`at://${authority}/com.atproto.lexicon.schema/${nsid}`); 124 + if (params.collection !== "com.atproto.lexicon.schema") { 125 + const schema = await resolveLexiconSchema(authority, nsid); 126 + setSchema(schema); 127 + setLexiconNotFound(false); 128 + } 129 + } catch { 130 + setLexiconNotFound(true); 131 + } 132 + }; 133 134 const deleteRecord = async () => { 135 rpc = new Client({ handler: agent()! }); ··· 140 rkey: params.rkey, 141 }, 142 }); 143 + const id = addNotification({ 144 + message: "Record deleted", 145 + type: "success", 146 + }); 147 + setTimeout(() => removeNotification(id), 3000); 148 navigate(`/at://${params.repo}/${params.collection}`); 149 }; 150 ··· 158 return template(parsedUri, record); 159 }; 160 161 + const RecordTab = (props: { 162 + tab: "record" | "backlinks" | "info" | "schema"; 163 + label: string; 164 + error?: boolean; 165 + }) => { 166 + const isActive = () => { 167 + if (!location.hash && props.tab === "record") return true; 168 + if (location.hash === `#${props.tab}`) return true; 169 + if (props.tab === "schema" && location.hash.startsWith("#schema:")) return true; 170 + return false; 171 + }; 172 + 173 + return ( 174 + <div class="flex items-center gap-0.5"> 175 + <A 176 + classList={{ 177 + "flex items-center gap-1 border-b-2": true, 178 + "border-transparent hover:border-neutral-400 dark:hover:border-neutral-600": 179 + !isActive(), 180 + }} 181 + href={`/at://${did}/${params.collection}/${params.rkey}#${props.tab}`} 182 + > 183 + {props.label} 184 + </A> 185 + <Show when={props.error && (validRecord() === false || validSchema() === false)}> 186 + <span class="iconify lucide--x text-red-500 dark:text-red-400"></span> 187 + </Show> 188 + </div> 189 + ); 190 + }; 191 + 192 return ( 193 <Show when={record()} keyed> 194 <div class="flex w-full flex-col items-center"> 195 + <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"> 196 + <div class="flex gap-3"> 197 + <RecordTab tab="record" label="Record" /> 198 + <RecordTab tab="schema" label="Schema" /> 199 + <RecordTab tab="backlinks" label="Backlinks" /> 200 + <RecordTab tab="info" label="Info" error /> 201 </div> 202 + <div class="flex gap-0.5"> 203 <Show when={agent() && agent()?.sub === record()?.uri.split("/")[2]}> 204 <RecordEditor create={false} record={record()?.value} refetch={refetch} /> 205 <Tooltip text="Delete"> 206 <button 207 + 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" 208 onclick={() => setOpenDelete(true)} 209 > 210 <span class="iconify lucide--trash-2"></span> 211 </button> 212 </Tooltip> 213 <Modal open={openDelete()} onClose={() => setOpenDelete(false)}> 214 + <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"> 215 <h2 class="mb-2 font-semibold">Delete this record?</h2> 216 <div class="flex justify-end gap-2"> 217 <Button onClick={() => setOpenDelete(false)}>Cancel</Button> 218 <Button 219 onClick={deleteRecord} 220 + 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" 221 > 222 Delete 223 </Button> ··· 227 </Show> 228 <MenuProvider> 229 <DropdownMenu 230 + icon="lucide--ellipsis-vertical" 231 + buttonClass="rounded-sm p-1.5" 232 + menuClass="top-9 p-2 text-sm" 233 > 234 <CopyMenu 235 + content={JSON.stringify(record()?.value, null, 2)} 236 label="Copy record" 237 icon="lucide--copy" 238 /> 239 + <CopyMenu 240 + content={`at://${params.repo}/${params.collection}/${params.rkey}`} 241 + label="Copy AT URI" 242 + icon="lucide--copy" 243 + /> 244 + <Show when={record()?.cid}> 245 + {(cid) => <CopyMenu content={cid()} label="Copy CID" icon="lucide--copy" />} 246 + </Show> 247 + <MenuSeparator /> 248 <Show when={externalLink()}> 249 {(externalLink) => ( 250 <NavMenu ··· 266 </div> 267 </div> 268 <Show when={!location.hash || location.hash === "#record"}> 269 + <div class="w-max max-w-screen min-w-full px-4 font-mono text-xs wrap-anywhere whitespace-pre-wrap sm:px-2 sm:text-sm md:max-w-3xl"> 270 <JSONValue data={record()?.value as any} repo={record()!.uri.split("/")[2]} /> 271 </div> 272 </Show> 273 + <Show when={location.hash === "#schema" || location.hash.startsWith("#schema:")}> 274 + <Show when={lexiconNotFound() === true}> 275 + <span class="w-full px-2 text-sm">Lexicon schema could not be resolved.</span> 276 + </Show> 277 + <Show when={lexiconNotFound() === undefined}> 278 + <span class="w-full px-2 text-sm">Resolving lexicon schema...</span> 279 + </Show> 280 + <Show when={schema() || params.collection === "com.atproto.lexicon.schema"}> 281 + <ErrorBoundary fallback={(err) => <div>Error: {err.message}</div>}> 282 + <LexiconSchemaView schema={schema()?.rawSchema ?? (record()?.value as any)} /> 283 + </ErrorBoundary> 284 + </Show> 285 + </Show> 286 <Show when={location.hash === "#backlinks"}> 287 + <ErrorBoundary 288 + fallback={(err) => <div class="wrap-break-word">Error: {err.message}</div>} 289 + > 290 <Suspense 291 fallback={ 292 <div class="iconify lucide--loader-circle animate-spin self-center text-xl" /> 293 } 294 > 295 + <div class="w-full px-2"> 296 + <Backlinks target={`at://${did}/${params.collection}/${params.rkey}`} /> 297 + </div> 298 </Suspense> 299 </ErrorBoundary> 300 + </Show> 301 + <Show when={location.hash === "#info"}> 302 + <div class="flex w-full flex-col gap-2 px-2 text-sm"> 303 + <div> 304 + <div class="flex items-center gap-1"> 305 + <span class="iconify lucide--at-sign"></span> 306 + <p class="font-semibold">AT URI</p> 307 + </div> 308 + <div class="truncate text-xs">{record()?.uri}</div> 309 + </div> 310 + <Show when={record()?.cid}> 311 + <div> 312 + <div class="flex items-center gap-1"> 313 + <span class="iconify lucide--box"></span> 314 + <p class="font-semibold">CID</p> 315 + </div> 316 + <div class="truncate text-left text-xs" dir="rtl"> 317 + {record()?.cid} 318 + </div> 319 + </div> 320 + </Show> 321 + <div> 322 + <div class="flex items-center gap-1"> 323 + <span class="iconify lucide--lock-keyhole"></span> 324 + <p class="font-semibold">Record verification</p> 325 + <span 326 + classList={{ 327 + "iconify lucide--check text-green-500 dark:text-green-400": 328 + validRecord() === true, 329 + "iconify lucide--x text-red-500 dark:text-red-400": validRecord() === false, 330 + "iconify lucide--loader-circle animate-spin": validRecord() === undefined, 331 + }} 332 + ></span> 333 + </div> 334 + <Show when={validRecord() === false}> 335 + <div class="wrap-break-word">{notice()}</div> 336 + </Show> 337 + </div> 338 + <Show when={validSchema() !== undefined}> 339 + <div class="flex items-center gap-1"> 340 + <span class="iconify lucide--file-check"></span> 341 + <p class="font-semibold">Schema validation</p> 342 + <span 343 + class={`iconify ${validSchema() ? "lucide--check text-green-500 dark:text-green-400" : "lucide--x text-red-500 dark:text-red-400"}`} 344 + ></span> 345 + </div> 346 + </Show> 347 + <Show when={lexiconUri()}> 348 + <div> 349 + <div class="flex items-center gap-1"> 350 + <span class="iconify lucide--scroll-text"></span> 351 + <p class="font-semibold">Lexicon schema</p> 352 + </div> 353 + <div class="truncate text-xs"> 354 + <A 355 + href={`/${lexiconUri()}`} 356 + class="text-blue-400 hover:underline active:underline" 357 + > 358 + {lexiconUri()} 359 + </A> 360 + </div> 361 + </div> 362 + </Show> 363 + </div> 364 </Show> 365 </div> 366 </Show>
+403 -381
src/views/repo.tsx
··· 1 import { Client, CredentialManager } from "@atcute/client"; 2 - import { parsePublicMultikey } from "@atcute/crypto"; 3 - import { 4 - CompatibleOperationOrTombstone, 5 - defs, 6 - IndexedEntry, 7 - processIndexedEntryLog, 8 - } from "@atcute/did-plc"; 9 import { DidDocument } from "@atcute/identity"; 10 - import { ActorIdentifier, Handle } from "@atcute/lexicons"; 11 - import { resolveHandle } from "@atcute/oauth-browser-client"; 12 import { A, useLocation, useNavigate, useParams } from "@solidjs/router"; 13 - import { createResource, createSignal, ErrorBoundary, For, Show, Suspense } from "solid-js"; 14 import { Backlinks } from "../components/backlinks.jsx"; 15 - import { Button } from "../components/button.jsx"; 16 import { TextInput } from "../components/text-input.jsx"; 17 import Tooltip from "../components/tooltip.jsx"; 18 - import { didDocCache, resolvePDS } from "../utils/api.js"; 19 - import { localDateFromTimestamp } from "../utils/date.js"; 20 - import { createOperationHistory, DiffEntry, groupBy } from "../utils/plc-logs.js"; 21 import { BlobView } from "./blob.jsx"; 22 - 23 - type Tab = "collections" | "backlinks" | "identity" | "blobs"; 24 - type PlcEvent = "handle" | "rotation_key" | "service" | "verification_method"; 25 - 26 - const PlcLogView = (props: { 27 - did: string; 28 - plcOps: [IndexedEntry<CompatibleOperationOrTombstone>, DiffEntry[]][]; 29 - }) => { 30 - const [activePlcEvent, setActivePlcEvent] = createSignal<PlcEvent | undefined>(); 31 - 32 - const FilterButton = (props: { icon: string; event: PlcEvent }) => ( 33 - <button 34 - classList={{ 35 - "flex items-center rounded-full p-1.5": true, 36 - "bg-neutral-700 dark:bg-neutral-200": activePlcEvent() === props.event, 37 - }} 38 - onclick={() => setActivePlcEvent(activePlcEvent() === props.event ? undefined : props.event)} 39 - > 40 - <span 41 - class={`${props.icon} ${activePlcEvent() === props.event ? "text-neutral-200 dark:text-neutral-900" : ""}`} 42 - ></span> 43 - </button> 44 - ); 45 - 46 - const DiffItem = (props: { diff: DiffEntry }) => { 47 - const diff = props.diff; 48 - let title = "Unknown log entry"; 49 - let icon = "lucide--circle-help"; 50 - let value = ""; 51 - 52 - if (diff.type === "identity_created") { 53 - icon = "lucide--bell"; 54 - title = `Identity created`; 55 - } else if (diff.type === "identity_tombstoned") { 56 - icon = "lucide--skull"; 57 - title = `Identity tombstoned`; 58 - } else if (diff.type === "handle_added" || diff.type === "handle_removed") { 59 - icon = "lucide--at-sign"; 60 - title = diff.type === "handle_added" ? "Alias added" : "Alias removed"; 61 - value = diff.handle; 62 - } else if (diff.type === "handle_changed") { 63 - icon = "lucide--at-sign"; 64 - title = "Alias updated"; 65 - value = `${diff.prev_handle} โ†’ ${diff.next_handle}`; 66 - } else if (diff.type === "rotation_key_added" || diff.type === "rotation_key_removed") { 67 - icon = "lucide--key-round"; 68 - title = diff.type === "rotation_key_added" ? "Rotation key added" : "Rotation key removed"; 69 - value = diff.rotation_key; 70 - } else if (diff.type === "service_added" || diff.type === "service_removed") { 71 - icon = "lucide--hard-drive"; 72 - title = `Service ${diff.service_id} ${diff.type === "service_added" ? "added" : "removed"}`; 73 - value = `${diff.service_endpoint}`; 74 - } else if (diff.type === "service_changed") { 75 - icon = "lucide--hard-drive"; 76 - title = `Service ${diff.service_id} updated`; 77 - value = `${diff.prev_service_endpoint} โ†’ ${diff.next_service_endpoint}`; 78 - } else if ( 79 - diff.type === "verification_method_added" || 80 - diff.type === "verification_method_removed" 81 - ) { 82 - icon = "lucide--shield-check"; 83 - title = `Verification method ${diff.method_id} ${diff.type === "verification_method_added" ? "added" : "removed"}`; 84 - value = `${diff.method_key}`; 85 - } else if (diff.type === "verification_method_changed") { 86 - icon = "lucide--shield-check"; 87 - title = `Verification method ${diff.method_id} updated`; 88 - value = `${diff.prev_method_key} โ†’ ${diff.next_method_key}`; 89 - } 90 - 91 - return ( 92 - <div class="grid grid-cols-[min-content_1fr] items-center gap-x-1"> 93 - <div class={icon + ` iconify shrink-0`} /> 94 - <p 95 - classList={{ 96 - "font-semibold": true, 97 - "text-neutral-400 line-through dark:text-neutral-600": diff.orig.nullified, 98 - }} 99 - > 100 - {title} 101 - </p> 102 - <div></div> 103 - {value} 104 - </div> 105 - ); 106 - }; 107 - 108 - return ( 109 - <> 110 - <div class="flex items-center justify-between"> 111 - <div class="flex items-center gap-1"> 112 - <div class="iconify lucide--filter" /> 113 - <div class="dark:shadow-dark-800 dark:bg-dark-300 flex w-fit items-center rounded-full border-[0.5px] border-neutral-300 bg-neutral-50 shadow-xs dark:border-neutral-700"> 114 - <FilterButton icon="iconify lucide--at-sign" event="handle" /> 115 - <FilterButton icon="iconify lucide--key-round" event="rotation_key" /> 116 - <FilterButton icon="iconify lucide--hard-drive" event="service" /> 117 - <FilterButton icon="iconify lucide--shield-check" event="verification_method" /> 118 - </div> 119 - </div> 120 - <Tooltip text="Audit log"> 121 - <a 122 - href={`${localStorage.plcDirectory ?? "https://plc.directory"}/${props.did}/log/audit`} 123 - target="_blank" 124 - class="-mr-1 flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 125 - > 126 - <span class="iconify lucide--external-link"></span> 127 - </a> 128 - </Tooltip> 129 - </div> 130 - <div class="flex flex-col gap-1 text-sm"> 131 - <For each={props.plcOps}> 132 - {([entry, diffs]) => ( 133 - <Show 134 - when={!activePlcEvent() || diffs.find((d) => d.type.startsWith(activePlcEvent()!))} 135 - > 136 - <div class="flex flex-col"> 137 - <span class="text-neutral-500 dark:text-neutral-400"> 138 - {localDateFromTimestamp(new Date(entry.createdAt).getTime())} 139 - </span> 140 - {diffs.map((diff) => ( 141 - <Show when={!activePlcEvent() || diff.type.startsWith(activePlcEvent()!)}> 142 - <DiffItem diff={diff} /> 143 - </Show> 144 - ))} 145 - </div> 146 - </Show> 147 - )} 148 - </For> 149 - </div> 150 - </> 151 - ); 152 - }; 153 154 - const RepoView = () => { 155 const params = useParams(); 156 const location = useLocation(); 157 const navigate = useNavigate(); ··· 160 const [didDoc, setDidDoc] = createSignal<DidDocument>(); 161 const [nsids, setNsids] = createSignal<Record<string, { hidden: boolean; nsids: string[] }>>(); 162 const [filter, setFilter] = createSignal<string>(); 163 - const [plcOps, setPlcOps] = 164 - createSignal<[IndexedEntry<CompatibleOperationOrTombstone>, DiffEntry[]][]>(); 165 - const [showPlcLogs, setShowPlcLogs] = createSignal(false); 166 - const [loading, setLoading] = createSignal(false); 167 - const [notice, setNotice] = createSignal<string>(); 168 let rpc: Client; 169 let pds: string; 170 const did = params.repo; 171 172 - const RepoTab = (props: { tab: Tab; label: string; icon: string }) => ( 173 - <A 174 - classList={{ 175 - "flex items-center border-b-2 gap-1": true, 176 - "border-transparent hover:border-neutral-400 dark:hover:border-neutral-600": 177 - (location.hash !== `#${props.tab}` && !!location.hash) || 178 - (!location.hash && props.tab !== "collections"), 179 - }} 180 - href={`/at://${params.repo}#${props.tab}`} 181 - > 182 - <div class={"iconify " + props.icon} /> 183 - {props.label} 184 </A> 185 ); 186 187 const fetchRepo = async () => { 188 try { 189 pds = await resolvePDS(did); 190 } catch { 191 - try { 192 - const did = await resolveHandle(params.repo as Handle); 193 - navigate(location.pathname.replace(params.repo, did)); 194 - } catch { 195 - navigate(`/${did}`); 196 } 197 } 198 setDidDoc(didDocCache[did] as DidDocument); 199 200 rpc = new Client({ handler: new CredentialManager({ service: pds }) }); 201 const res = await rpc.get("com.atproto.repo.describeRepo", { ··· 218 console.error(res.data.error); 219 switch (res.data.error) { 220 case "RepoDeactivated": 221 - setError("This repository has been deactivated"); 222 break; 223 case "RepoTakendown": 224 - setError("This repository has been taken down"); 225 break; 226 default: 227 - setError("This repository is unreachable"); 228 } 229 - navigate(`/at://${params.repo}#identity`); 230 } 231 232 return res.data; ··· 234 235 const [repo] = createResource(fetchRepo); 236 237 const downloadRepo = async () => { 238 try { 239 setDownloading(true); 240 const response = await fetch(`${pds}/xrpc/com.atproto.sync.getRepo?did=${did}`); 241 if (!response.ok) { 242 throw new Error(`HTTP error status: ${response.status}`); 243 } 244 245 - const blob = await response.blob(); 246 const url = window.URL.createObjectURL(blob); 247 const a = document.createElement("a"); 248 a.href = url; ··· 252 253 window.URL.revokeObjectURL(url); 254 document.body.removeChild(a); 255 } catch (error) { 256 console.error("Download failed:", error); 257 } 258 setDownloading(false); 259 }; 260 261 - const toggleCollection = (authority: string) => { 262 - setNsids({ 263 - ...nsids(), 264 - [authority]: { ...nsids()![authority], hidden: !nsids()![authority].hidden }, 265 - }); 266 - }; 267 - 268 return ( 269 <Show when={repo()}> 270 - <div class="flex w-[22rem] flex-col gap-2 break-words sm:w-[24rem]"> 271 - <Show when={error()}> 272 - <div class="rounded-lg bg-red-100 p-2 text-sm text-red-700 dark:bg-red-200 dark:text-red-600"> 273 - {error()} 274 - </div> 275 - </Show> 276 <div 277 - class={`dark:shadow-dark-800 dark:bg-dark-300 flex ${error() ? "justify-around" : "justify-between"} rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 px-2 py-1.5 text-sm shadow-xs dark:border-neutral-700`} 278 > 279 - <Show when={!error()}> 280 - <RepoTab tab="collections" label="Collections" icon="lucide--folder-open" /> 281 - </Show> 282 - <RepoTab tab="identity" label="Identity" icon="lucide--id-card" /> 283 - <Show when={!error()}> 284 - <RepoTab tab="blobs" label="Blobs" icon="lucide--file-digit" /> 285 - </Show> 286 - <RepoTab tab="backlinks" label="Backlinks" icon="lucide--send-to-back" /> 287 </div> 288 - <Show when={location.hash === "#backlinks"}> 289 - <ErrorBoundary fallback={(err) => <div class="break-words">Error: {err.message}</div>}> 290 - <Suspense 291 - fallback={ 292 - <div class="iconify lucide--loader-circle animate-spin self-center text-xl" /> 293 - } 294 > 295 - <Backlinks target={did} /> 296 - </Suspense> 297 - </ErrorBoundary> 298 - </Show> 299 - <Show when={location.hash === "#blobs"}> 300 - <ErrorBoundary fallback={(err) => <div class="break-words">Error: {err.message}</div>}> 301 - <Suspense 302 - fallback={ 303 - <div class="iconify lucide--loader-circle animate-spin self-center text-xl" /> 304 - } 305 > 306 - <BlobView pds={pds!} repo={did} /> 307 - </Suspense> 308 - </ErrorBoundary> 309 - </Show> 310 - <Show when={nsids() && (!location.hash || location.hash === "#collections")}> 311 - <div class="flex items-center gap-1"> 312 - <Tooltip text="Jetstream"> 313 - <A 314 - href={`/jetstream?dids=${params.repo}`} 315 - class="-ml-1 flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 316 > 317 - <span class="iconify lucide--radio-tower text-lg"></span> 318 - </A> 319 - </Tooltip> 320 - <TextInput 321 - placeholder="Filter collections" 322 - onInput={(e) => setFilter(e.currentTarget.value)} 323 - class="grow" 324 - /> 325 - </div> 326 - <div class="flex flex-col font-mono"> 327 - <div class="grid grid-cols-[min-content_1fr] items-center gap-x-2 overflow-hidden text-sm"> 328 <For 329 each={Object.keys(nsids() ?? {}).filter((authority) => 330 filter() ? ··· 333 )} 334 > 335 {(authority) => ( 336 - <> 337 - <button onclick={() => toggleCollection(authority)} class="flex items-center"> 338 - <span 339 - classList={{ 340 - "iconify lucide--chevron-down text-lg transition-transform": true, 341 - "-rotate-90": nsids()?.[authority].hidden, 342 - }} 343 - ></span> 344 - </button> 345 - <button 346 - class="bg-transparent text-left wrap-anywhere" 347 - onclick={() => toggleCollection(authority)} 348 > 349 - {authority} 350 - </button> 351 - <Show when={!nsids()?.[authority].hidden}> 352 - <div></div> 353 - <div class="flex flex-col"> 354 - <For 355 - each={nsids()?.[authority].nsids.filter((nsid) => 356 - filter() ? 357 - nsid.startsWith(filter()!.split(".").slice(2).join(".")) 358 - : true, 359 - )} 360 > 361 - {(nsid) => ( 362 - <A 363 - href={`/at://${did}/${authority}.${nsid}`} 364 - class="text-blue-400 hover:underline active:underline" 365 - > 366 - {authority}.{nsid} 367 - </A> 368 - )} 369 - </For> 370 - </div> 371 - </Show> 372 - </> 373 )} 374 </For> 375 </div> 376 - </div> 377 - </Show> 378 - <Show when={location.hash === "#identity"}> 379 - <Show when={didDoc()}> 380 - {(didDocument) => ( 381 - <div class="flex flex-col gap-y-2 wrap-anywhere"> 382 - <div class="flex flex-col gap-y-1"> 383 - <div class="flex items-baseline justify-between gap-2"> 384 - <div> 385 - <div class="flex items-center gap-1"> 386 - <div class="iconify lucide--id-card" /> 387 - <p class="font-semibold">ID</p> 388 - </div> 389 - <div class="text-sm">{didDocument().id}</div> 390 </div> 391 - <Tooltip text="DID document"> 392 - <a 393 - href={ 394 - did.startsWith("did:plc") ? 395 - `${localStorage.plcDirectory ?? "https://plc.directory"}/${did}` 396 - : `https://${did.split("did:web:")[1]}/.well-known/did.json` 397 - } 398 - target="_blank" 399 - class="-mr-1 flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 400 - > 401 - <span class="iconify lucide--external-link"></span> 402 - </a> 403 - </Tooltip> 404 </div> 405 <div> 406 <div class="flex items-center gap-1"> 407 <div class="iconify lucide--at-sign" /> 408 <p class="font-semibold">Aliases</p> 409 </div> 410 - <ul> 411 <For each={didDocument().alsoKnownAs}> 412 - {(alias) => <li class="text-sm">{alias}</li>} 413 </For> 414 - </ul> 415 </div> 416 <div> 417 <div class="flex items-center gap-1"> 418 <div class="iconify lucide--hard-drive" /> 419 <p class="font-semibold">Services</p> 420 </div> 421 - <ul> 422 <For each={didDocument().service}> 423 {(service) => ( 424 - <li class="flex flex-col text-sm"> 425 - <span>#{service.id.split("#")[1]}</span> 426 <a 427 - class="w-fit text-blue-400 hover:underline active:underline" 428 href={service.serviceEndpoint.toString()} 429 target="_blank" 430 > 431 {service.serviceEndpoint.toString()} 432 </a> 433 - </li> 434 )} 435 </For> 436 - </ul> 437 </div> 438 <div> 439 <div class="flex items-center gap-1"> 440 <div class="iconify lucide--shield-check" /> 441 - <p class="font-semibold">Verification methods</p> 442 </div> 443 - <ul> 444 <For each={didDocument().verificationMethod}> 445 {(verif) => ( 446 <Show when={verif.publicKeyMultibase}> 447 {(key) => ( 448 - <li class="flex flex-col text-sm"> 449 - <span class="flex justify-between gap-1"> 450 - <span>#{verif.id.split("#")[1]}</span> 451 - <span class="flex items-center gap-0.5"> 452 - <div class="iconify lucide--key-round" /> 453 - <ErrorBoundary fallback={<>unknown</>}> 454 - {parsePublicMultikey(key()).type} 455 - </ErrorBoundary> 456 </span> 457 - </span> 458 - <span class="truncate text-xs">{key()}</span> 459 - </li> 460 )} 461 </Show> 462 )} 463 </For> 464 - </ul> 465 </div> 466 - </div> 467 - <div class="flex justify-between"> 468 - <Show when={did.startsWith("did:plc")}> 469 - <div class="flex items-center gap-1"> 470 - <Button 471 - onClick={async () => { 472 - if (!plcOps()) { 473 - setLoading(true); 474 - const response = await fetch( 475 - `${localStorage.plcDirectory ?? "https://plc.directory"}/${did}/log/audit`, 476 - ); 477 - const json = await response.json(); 478 - try { 479 - const logs = defs.indexedEntryLog.parse(json); 480 - try { 481 - await processIndexedEntryLog(did as any, logs); 482 - } catch (e) { 483 - console.error(e); 484 - } 485 - const opHistory = createOperationHistory(logs).reverse(); 486 - setPlcOps(Array.from(groupBy(opHistory, (item) => item.orig))); 487 - setLoading(false); 488 - } catch (e: any) { 489 - setNotice(e); 490 - console.error(e); 491 - setLoading(false); 492 - } 493 - } 494 495 - setShowPlcLogs(!showPlcLogs()); 496 - }} 497 - > 498 - <span class="iconify lucide--logs text-sm"></span> 499 - {showPlcLogs() ? "Hide" : "Show"} PLC Logs 500 - </Button> 501 - <Show when={loading()}> 502 - <div class="iconify lucide--loader-circle animate-spin text-xl" /> 503 - </Show> 504 - </div> 505 - </Show> 506 - <Show when={error()?.length === 0 || error() === undefined}> 507 - <div 508 - classList={{ 509 - "flex items-center gap-1": true, 510 - "flex-row-reverse": did.startsWith("did:web"), 511 - }} 512 - > 513 - <Show when={downloading()}> 514 - <div class="iconify lucide--loader-circle animate-spin text-xl" /> 515 - </Show> 516 - <Button onClick={() => downloadRepo()}> 517 - <span class="iconify lucide--download text-sm"></span> 518 - Export Repo 519 - </Button> 520 </div> 521 </Show> 522 </div> 523 - <Show when={showPlcLogs()}> 524 - <Show when={notice()}> 525 - <div>{notice()}</div> 526 - </Show> 527 - <PlcLogView plcOps={plcOps() ?? []} did={did} /> 528 - </Show> 529 - </div> 530 - )} 531 </Show> 532 - </Show> 533 </div> 534 </Show> 535 ); 536 }; 537 - 538 - export { RepoView };
··· 1 import { Client, CredentialManager } from "@atcute/client"; 2 + import { parseDidKey, parsePublicMultikey } from "@atcute/crypto"; 3 import { DidDocument } from "@atcute/identity"; 4 + import { ActorIdentifier, Did, Handle, Nsid } from "@atcute/lexicons"; 5 import { A, useLocation, useNavigate, useParams } from "@solidjs/router"; 6 + import { 7 + createResource, 8 + createSignal, 9 + ErrorBoundary, 10 + For, 11 + onMount, 12 + Show, 13 + Suspense, 14 + } from "solid-js"; 15 + import { createStore } from "solid-js/store"; 16 import { Backlinks } from "../components/backlinks.jsx"; 17 + import { 18 + ActionMenu, 19 + CopyMenu, 20 + DropdownMenu, 21 + MenuProvider, 22 + MenuSeparator, 23 + NavMenu, 24 + } from "../components/dropdown.jsx"; 25 + import { setPDS } from "../components/navbar.jsx"; 26 + import { 27 + addNotification, 28 + removeNotification, 29 + updateNotification, 30 + } from "../components/notification.jsx"; 31 import { TextInput } from "../components/text-input.jsx"; 32 import Tooltip from "../components/tooltip.jsx"; 33 + import { 34 + didDocCache, 35 + labelerCache, 36 + resolveHandle, 37 + resolveLexiconAuthority, 38 + resolvePDS, 39 + validateHandle, 40 + } from "../utils/api.js"; 41 import { BlobView } from "./blob.jsx"; 42 + import { PlcLogView } from "./logs.jsx"; 43 44 + export const RepoView = () => { 45 const params = useParams(); 46 const location = useLocation(); 47 const navigate = useNavigate(); ··· 50 const [didDoc, setDidDoc] = createSignal<DidDocument>(); 51 const [nsids, setNsids] = createSignal<Record<string, { hidden: boolean; nsids: string[] }>>(); 52 const [filter, setFilter] = createSignal<string>(); 53 + const [showFilter, setShowFilter] = createSignal(false); 54 + const [validHandles, setValidHandles] = createStore<Record<string, boolean>>({}); 55 + const [rotationKeys, setRotationKeys] = createSignal<Array<string>>([]); 56 let rpc: Client; 57 let pds: string; 58 const did = params.repo; 59 60 + const RepoTab = (props: { 61 + tab: "collections" | "backlinks" | "identity" | "blobs" | "logs"; 62 + label: string; 63 + }) => ( 64 + <A class="group flex justify-center" href={`/at://${params.repo}#${props.tab}`}> 65 + <span 66 + classList={{ 67 + "flex flex-1 items-center border-b-2": true, 68 + "border-transparent group-hover:border-neutral-400 dark:group-hover:border-neutral-600": 69 + (location.hash !== `#${props.tab}` && !!location.hash) || 70 + (!location.hash && 71 + ((!error() && props.tab !== "collections") || 72 + (!!error() && props.tab !== "identity"))), 73 + }} 74 + > 75 + {props.label} 76 + </span> 77 </A> 78 ); 79 80 + const getRotationKeys = async () => { 81 + const res = await fetch( 82 + `${localStorage.plcDirectory ?? "https://plc.directory"}/${did}/log/last`, 83 + ); 84 + const json = await res.json(); 85 + setRotationKeys(json.rotationKeys ?? []); 86 + }; 87 + 88 const fetchRepo = async () => { 89 try { 90 pds = await resolvePDS(did); 91 } catch { 92 + if (!did.startsWith("did:")) { 93 + try { 94 + const did = await resolveHandle(params.repo as Handle); 95 + navigate(location.pathname.replace(params.repo, did)); 96 + return; 97 + } catch { 98 + try { 99 + const nsid = params.repo as Nsid; 100 + const res = await resolveLexiconAuthority(nsid); 101 + navigate(`/at://${res}/com.atproto.lexicon.schema/${nsid}`); 102 + return; 103 + } catch { 104 + navigate(`/${did}`); 105 + return; 106 + } 107 + } 108 } 109 } 110 setDidDoc(didDocCache[did] as DidDocument); 111 + getRotationKeys(); 112 + 113 + validateHandles(); 114 + 115 + if (!pds) { 116 + setError("Missing PDS"); 117 + setPDS("Missing PDS"); 118 + return {}; 119 + } 120 121 rpc = new Client({ handler: new CredentialManager({ service: pds }) }); 122 const res = await rpc.get("com.atproto.repo.describeRepo", { ··· 139 console.error(res.data.error); 140 switch (res.data.error) { 141 case "RepoDeactivated": 142 + setError("Deactivated"); 143 break; 144 case "RepoTakendown": 145 + setError("Takendown"); 146 break; 147 default: 148 + setError("Unreachable"); 149 } 150 } 151 152 return res.data; ··· 154 155 const [repo] = createResource(fetchRepo); 156 157 + const validateHandles = async () => { 158 + for (const alias of didDoc()?.alsoKnownAs ?? []) { 159 + if (alias.startsWith("at://")) 160 + setValidHandles( 161 + alias, 162 + await validateHandle(alias.replace("at://", "") as Handle, did as Did), 163 + ); 164 + } 165 + }; 166 + 167 const downloadRepo = async () => { 168 + let notificationId: string | null = null; 169 + 170 try { 171 setDownloading(true); 172 + notificationId = addNotification({ 173 + message: "Downloading repository...", 174 + progress: 0, 175 + total: 0, 176 + type: "info", 177 + }); 178 + 179 const response = await fetch(`${pds}/xrpc/com.atproto.sync.getRepo?did=${did}`); 180 if (!response.ok) { 181 throw new Error(`HTTP error status: ${response.status}`); 182 } 183 184 + const contentLength = response.headers.get("content-length"); 185 + const total = contentLength ? parseInt(contentLength, 10) : 0; 186 + let loaded = 0; 187 + 188 + const reader = response.body?.getReader(); 189 + const chunks: Uint8Array[] = []; 190 + 191 + if (reader) { 192 + while (true) { 193 + const { done, value } = await reader.read(); 194 + if (done) break; 195 + 196 + chunks.push(value); 197 + loaded += value.length; 198 + 199 + if (total > 0) { 200 + const progress = Math.round((loaded / total) * 100); 201 + updateNotification(notificationId, { 202 + progress, 203 + total, 204 + }); 205 + } else { 206 + const progressMB = Math.round((loaded / (1024 * 1024)) * 10) / 10; 207 + updateNotification(notificationId, { 208 + progress: progressMB, 209 + total: 0, 210 + }); 211 + } 212 + } 213 + } 214 + 215 + const blob = new Blob(chunks); 216 const url = window.URL.createObjectURL(blob); 217 const a = document.createElement("a"); 218 a.href = url; ··· 222 223 window.URL.revokeObjectURL(url); 224 document.body.removeChild(a); 225 + 226 + updateNotification(notificationId, { 227 + message: "Repository downloaded successfully", 228 + type: "success", 229 + progress: undefined, 230 + }); 231 + setTimeout(() => { 232 + if (notificationId) removeNotification(notificationId); 233 + }, 3000); 234 } catch (error) { 235 console.error("Download failed:", error); 236 + if (notificationId) { 237 + updateNotification(notificationId, { 238 + message: "Download failed", 239 + type: "error", 240 + progress: undefined, 241 + }); 242 + setTimeout(() => { 243 + if (notificationId) removeNotification(notificationId); 244 + }, 5000); 245 + } 246 } 247 setDownloading(false); 248 }; 249 250 return ( 251 <Show when={repo()}> 252 + <div class="flex w-full flex-col gap-3 wrap-break-word"> 253 <div 254 + 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`} 255 > 256 + <div class="flex gap-2 text-xs sm:gap-4 sm:text-sm"> 257 + <Show when={!error()}> 258 + <RepoTab tab="collections" label="Collections" /> 259 + </Show> 260 + <RepoTab tab="identity" label="Identity" /> 261 + <Show when={did.startsWith("did:plc")}> 262 + <RepoTab tab="logs" label="Logs" /> 263 + </Show> 264 + <Show when={!error()}> 265 + <RepoTab tab="blobs" label="Blobs" /> 266 + </Show> 267 + <RepoTab tab="backlinks" label="Backlinks" /> 268 + </div> 269 + <div class="flex gap-0.5"> 270 + <Show when={error() && error() !== "Missing PDS"}> 271 + <div class="flex items-center gap-1 text-red-500 dark:text-red-400"> 272 + <span class="iconify lucide--alert-triangle"></span> 273 + <span>{error()}</span> 274 + </div> 275 + </Show> 276 + <Show when={!error() && (!location.hash || location.hash === "#collections")}> 277 + <Tooltip text="Filter collections"> 278 + <button 279 + 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" 280 + onClick={() => setShowFilter(!showFilter())} 281 + > 282 + <span class="iconify lucide--filter"></span> 283 + </button> 284 + </Tooltip> 285 + </Show> 286 + <MenuProvider> 287 + <DropdownMenu 288 + icon="lucide--ellipsis-vertical" 289 + buttonClass="rounded-sm p-1.5" 290 + menuClass="top-9 p-2 text-sm" 291 + > 292 + <CopyMenu content={params.repo} label="Copy DID" icon="lucide--copy" /> 293 + <NavMenu 294 + href={`/jetstream?dids=${params.repo}`} 295 + label="Jetstream" 296 + icon="lucide--radio-tower" 297 + /> 298 + <Show when={params.repo in labelerCache}> 299 + <NavMenu 300 + href={`/labels?did=${params.repo}&uriPatterns=*`} 301 + label="Labels" 302 + icon="lucide--tag" 303 + /> 304 + </Show> 305 + <Show when={error()?.length === 0 || error() === undefined}> 306 + <ActionMenu 307 + label="Export Repo" 308 + icon={downloading() ? "lucide--loader-circle animate-spin" : "lucide--download"} 309 + onClick={() => downloadRepo()} 310 + /> 311 + </Show> 312 + <MenuSeparator /> 313 + <NavMenu 314 + href={ 315 + did.startsWith("did:plc") ? 316 + `${localStorage.plcDirectory ?? "https://plc.directory"}/${did}` 317 + : `https://${did.split("did:web:")[1]}/.well-known/did.json` 318 + } 319 + newTab 320 + label="DID Document" 321 + icon="lucide--external-link" 322 + /> 323 + <Show when={did.startsWith("did:plc")}> 324 + <NavMenu 325 + href={`${localStorage.plcDirectory ?? "https://plc.directory"}/${did}/log/audit`} 326 + newTab 327 + label="Audit Log" 328 + icon="lucide--external-link" 329 + /> 330 + </Show> 331 + </DropdownMenu> 332 + </MenuProvider> 333 + </div> 334 </div> 335 + <div class="flex w-full flex-col gap-1 px-2"> 336 + <Show when={location.hash === "#logs"}> 337 + <ErrorBoundary 338 + fallback={(err) => <div class="wrap-break-word">Error: {err.message}</div>} 339 > 340 + <Suspense 341 + fallback={ 342 + <div class="iconify lucide--loader-circle mt-2 animate-spin self-center text-xl" /> 343 + } 344 + > 345 + <PlcLogView did={did} /> 346 + </Suspense> 347 + </ErrorBoundary> 348 + </Show> 349 + <Show when={location.hash === "#backlinks"}> 350 + <ErrorBoundary 351 + fallback={(err) => <div class="wrap-break-word">Error: {err.message}</div>} 352 > 353 + <Suspense 354 + fallback={ 355 + <div class="iconify lucide--loader-circle mt-2 animate-spin self-center text-xl" /> 356 + } 357 > 358 + <Backlinks target={did} /> 359 + </Suspense> 360 + </ErrorBoundary> 361 + </Show> 362 + <Show when={location.hash === "#blobs"}> 363 + <ErrorBoundary 364 + fallback={(err) => <div class="wrap-break-word">Error: {err.message}</div>} 365 + > 366 + <Suspense 367 + fallback={ 368 + <div class="iconify lucide--loader-circle mt-2 animate-spin self-center text-xl" /> 369 + } 370 + > 371 + <BlobView pds={pds!} repo={did} /> 372 + </Suspense> 373 + </ErrorBoundary> 374 + </Show> 375 + <Show when={nsids() && (!location.hash || location.hash === "#collections")}> 376 + <Show when={showFilter()}> 377 + <TextInput 378 + name="filter" 379 + placeholder="Filter collections" 380 + onInput={(e) => setFilter(e.currentTarget.value.toLowerCase())} 381 + class="grow" 382 + ref={(node) => { 383 + onMount(() => node.focus()); 384 + }} 385 + /> 386 + </Show> 387 + <div 388 + class="flex flex-col overflow-hidden text-sm" 389 + classList={{ "-mt-1": !showFilter() }} 390 + > 391 <For 392 each={Object.keys(nsids() ?? {}).filter((authority) => 393 filter() ? ··· 396 )} 397 > 398 {(authority) => ( 399 + <div class="dark:hover:bg-dark-200 flex flex-col rounded-lg p-1 hover:bg-neutral-200"> 400 + <For 401 + each={nsids()?.[authority].nsids.filter((nsid) => 402 + filter() ? nsid.startsWith(filter()!.split(".").slice(2).join(".")) : true, 403 + )} 404 > 405 + {(nsid) => ( 406 + <A 407 + href={`/at://${did}/${authority}.${nsid}`} 408 + class="hover:underline active:underline" 409 > 410 + <span>{authority}</span> 411 + <span class="text-neutral-500 dark:text-neutral-400">.{nsid}</span> 412 + </A> 413 + )} 414 + </For> 415 + </div> 416 )} 417 </For> 418 </div> 419 + </Show> 420 + <Show when={location.hash === "#identity" || (error() && !location.hash)}> 421 + <Show when={didDoc()}> 422 + {(didDocument) => ( 423 + <div class="flex flex-col gap-1 wrap-anywhere"> 424 + {/* ID Section */} 425 + <div> 426 + <div class="flex items-center gap-1"> 427 + <div class="iconify lucide--id-card" /> 428 + <p class="font-semibold">ID</p> 429 </div> 430 + <div class="text-sm">{didDocument().id}</div> 431 </div> 432 + 433 + {/* Aliases Section */} 434 <div> 435 <div class="flex items-center gap-1"> 436 <div class="iconify lucide--at-sign" /> 437 <p class="font-semibold">Aliases</p> 438 </div> 439 + <div class="flex flex-col gap-0.5"> 440 <For each={didDocument().alsoKnownAs}> 441 + {(alias) => ( 442 + <div class="flex items-center gap-1 text-sm"> 443 + <span>{alias}</span> 444 + <Show when={alias.startsWith("at://")}> 445 + <Tooltip 446 + text={ 447 + validHandles[alias] === true ? "Valid handle" 448 + : validHandles[alias] === undefined ? 449 + "Validating" 450 + : "Invalid handle" 451 + } 452 + > 453 + <span 454 + classList={{ 455 + "iconify lucide--circle-check text-green-600 dark:text-green-400": 456 + validHandles[alias] === true, 457 + "iconify lucide--circle-x text-red-500 dark:text-red-400": 458 + validHandles[alias] === false, 459 + "iconify lucide--loader-circle animate-spin": 460 + validHandles[alias] === undefined, 461 + }} 462 + ></span> 463 + </Tooltip> 464 + </Show> 465 + </div> 466 + )} 467 </For> 468 + </div> 469 </div> 470 + 471 + {/* Services Section */} 472 <div> 473 <div class="flex items-center gap-1"> 474 <div class="iconify lucide--hard-drive" /> 475 <p class="font-semibold">Services</p> 476 </div> 477 + <div class="flex flex-col gap-0.5"> 478 <For each={didDocument().service}> 479 {(service) => ( 480 + <div class="text-sm"> 481 + <div class="text-neutral-600 dark:text-neutral-400"> 482 + #{service.id.split("#")[1]} 483 + </div> 484 <a 485 + class="underline hover:text-blue-400" 486 href={service.serviceEndpoint.toString()} 487 target="_blank" 488 + rel="noopener" 489 > 490 {service.serviceEndpoint.toString()} 491 </a> 492 + </div> 493 )} 494 </For> 495 + </div> 496 </div> 497 + 498 + {/* Verification Methods Section */} 499 <div> 500 <div class="flex items-center gap-1"> 501 <div class="iconify lucide--shield-check" /> 502 + <p class="font-semibold">Verification Methods</p> 503 </div> 504 + <div class="flex flex-col gap-0.5"> 505 <For each={didDocument().verificationMethod}> 506 {(verif) => ( 507 <Show when={verif.publicKeyMultibase}> 508 {(key) => ( 509 + <div class="text-sm"> 510 + <div class="flex items-baseline gap-1"> 511 + <span class="text-neutral-600 dark:text-neutral-400"> 512 + #{verif.id.split("#")[1]} 513 </span> 514 + <ErrorBoundary 515 + fallback={<span class="text-neutral-500">unknown</span>} 516 + > 517 + <span class="dark:bg-dark-100 rounded bg-neutral-200 px-1 py-0.5 font-mono text-xs"> 518 + {parsePublicMultikey(key()).type} 519 + </span> 520 + </ErrorBoundary> 521 + </div> 522 + <div class="font-mono break-all">{key()}</div> 523 + </div> 524 )} 525 </Show> 526 )} 527 </For> 528 + </div> 529 </div> 530 531 + {/* Rotation Keys Section */} 532 + <Show when={rotationKeys().length > 0}> 533 + <div> 534 + <div class="flex items-center gap-1"> 535 + <div class="iconify lucide--key-round" /> 536 + <p class="font-semibold">Rotation Keys</p> 537 + </div> 538 + <div class="flex flex-col gap-0.5"> 539 + <For each={rotationKeys()}> 540 + {(key) => ( 541 + <div class="text-sm"> 542 + <span class="dark:bg-dark-100 rounded bg-neutral-200 px-1 py-0.5 font-mono text-xs"> 543 + {parseDidKey(key).type} 544 + </span> 545 + <div class="font-mono break-all">{key.replace("did:key:", "")}</div> 546 + </div> 547 + )} 548 + </For> 549 + </div> 550 </div> 551 </Show> 552 </div> 553 + )} 554 + </Show> 555 </Show> 556 + </div> 557 </div> 558 </Show> 559 ); 560 };
+1 -1
src/views/settings.tsx
··· 5 6 const Settings = () => { 7 return ( 8 - <div class="flex w-[22rem] flex-col gap-3 sm:w-[24rem]"> 9 <div class="flex items-center gap-1 font-semibold"> 10 <span>Settings</span> 11 </div>
··· 5 6 const Settings = () => { 7 return ( 8 + <div class="flex w-full flex-col gap-3"> 9 <div class="flex items-center gap-1 font-semibold"> 10 <span>Settings</span> 11 </div>
+12 -12
src/views/stream.tsx
··· 37 url = url.concat("?"); 38 } else { 39 url = formData.get("instance")?.toString() ?? "wss://bsky.network"; 40 } 41 42 const collections = formData.get("collections")?.toString().split(","); ··· 140 onCleanup(() => socket?.close()); 141 142 return ( 143 - <div class="flex flex-col items-center"> 144 <div class="flex gap-2 text-sm"> 145 <A 146 class="flex items-center gap-1 border-b-2 p-1" 147 inactiveClass="border-transparent hover:border-neutral-400 dark:hover:border-neutral-600" 148 href="/jetstream" 149 > 150 - <span class="iconify lucide--radio-tower"></span> 151 Jetstream 152 </A> 153 <A ··· 155 inactiveClass="border-transparent hover:border-neutral-400 dark:hover:border-neutral-600" 156 href="/firehose" 157 > 158 - <span class="iconify lucide--waves"></span> 159 Firehose 160 </A> 161 </div> 162 <StickyOverlay> 163 - <form ref={formRef} class="flex w-[22rem] flex-col gap-1 text-sm sm:w-[24rem]"> 164 <Show when={!connected()}> 165 <label class="flex items-center justify-end gap-x-1"> 166 - <span class="min-w-[5rem]">Instance</span> 167 <TextInput 168 name="instance" 169 value={ ··· 177 </label> 178 <Show when={streamType === "jetstream"}> 179 <label class="flex items-center justify-end gap-x-1"> 180 - <span class="min-w-[5rem]">Collections</span> 181 <textarea 182 name="collections" 183 spellcheck={false} 184 placeholder="Comma-separated list of collections" 185 value={searchParams.collections ?? ""} 186 - class="dark:bg-dark-100 dark:shadow-dark-800 grow rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 py-1 shadow-xs focus:outline-[1.5px] focus:outline-neutral-900 dark:border-neutral-700 dark:focus:outline-neutral-200" 187 /> 188 </label> 189 </Show> 190 <Show when={streamType === "jetstream"}> 191 <label class="flex items-center justify-end gap-x-1"> 192 - <span class="min-w-[5rem]">DIDs</span> 193 <textarea 194 name="dids" 195 spellcheck={false} 196 placeholder="Comma-separated list of DIDs" 197 value={searchParams.dids ?? ""} 198 - class="dark:bg-dark-100 dark:shadow-dark-800 grow rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 py-1 shadow-xs focus:outline-[1.5px] focus:outline-neutral-900 dark:border-neutral-700 dark:focus:outline-neutral-200" 199 /> 200 </label> 201 </Show> 202 <label class="flex items-center justify-end gap-x-1"> 203 - <span class="min-w-[5rem]">Cursor</span> 204 <TextInput 205 name="cursor" 206 placeholder="Leave empty for live-tail" ··· 228 {(param) => ( 229 <Show when={param.param}> 230 <div class="flex"> 231 - <div class="min-w-[6rem] font-semibold">{param.name}</div> 232 {param.param} 233 </div> 234 </Show> ··· 246 <Show when={notice().length}> 247 <div class="text-red-500 dark:text-red-400">{notice()}</div> 248 </Show> 249 - <div class="flex w-full flex-col gap-2 divide-y-[0.5px] divide-neutral-500 px-4 font-mono text-sm wrap-anywhere whitespace-pre-wrap md:w-[48rem]"> 250 <For each={records().toReversed()}> 251 {(rec) => ( 252 <div class="pb-2">
··· 37 url = url.concat("?"); 38 } else { 39 url = formData.get("instance")?.toString() ?? "wss://bsky.network"; 40 + url = url.replace("/xrpc/com.atproto.sync.subscribeRepos", ""); 41 + if (!(url.startsWith("wss://") || url.startsWith("ws://"))) url = "wss://" + url; 42 } 43 44 const collections = formData.get("collections")?.toString().split(","); ··· 142 onCleanup(() => socket?.close()); 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 ··· 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> 167 <TextInput 168 name="instance" 169 value={ ··· 177 </label> 178 <Show when={streamType === "jetstream"}> 179 <label class="flex items-center justify-end gap-x-1"> 180 + <span class="min-w-20">Collections</span> 181 <textarea 182 name="collections" 183 spellcheck={false} 184 placeholder="Comma-separated list of collections" 185 value={searchParams.collections ?? ""} 186 + class="dark:bg-dark-100 dark:shadow-dark-700 grow rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 py-1 shadow-xs focus:outline-[1px] focus:outline-neutral-600 dark:border-neutral-600 dark:focus:outline-neutral-400" 187 /> 188 </label> 189 </Show> 190 <Show when={streamType === "jetstream"}> 191 <label class="flex items-center justify-end gap-x-1"> 192 + <span class="min-w-20">DIDs</span> 193 <textarea 194 name="dids" 195 spellcheck={false} 196 placeholder="Comma-separated list of DIDs" 197 value={searchParams.dids ?? ""} 198 + class="dark:bg-dark-100 dark:shadow-dark-700 grow rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 py-1 shadow-xs focus:outline-[1px] focus:outline-neutral-600 dark:border-neutral-600 dark:focus:outline-neutral-400" 199 /> 200 </label> 201 </Show> 202 <label class="flex items-center justify-end gap-x-1"> 203 + <span class="min-w-20">Cursor</span> 204 <TextInput 205 name="cursor" 206 placeholder="Leave empty for live-tail" ··· 228 {(param) => ( 229 <Show when={param.param}> 230 <div class="flex"> 231 + <div class="min-w-24 font-semibold">{param.name}</div> 232 {param.param} 233 </div> 234 </Show> ··· 246 <Show when={notice().length}> 247 <div class="text-red-500 dark:text-red-400">{notice()}</div> 248 </Show> 249 + <div class="flex w-full flex-col gap-2 divide-y-[0.5px] divide-neutral-500 font-mono text-sm wrap-anywhere whitespace-pre-wrap md:w-3xl"> 250 <For each={records().toReversed()}> 251 {(rec) => ( 252 <div class="pb-2">