forked from pdsls.dev/pdsls
atproto explorer

Compare changes

Choose any two refs to compare.

+4 -3
index.html
··· 11 11 <meta property="description" content="Browse the public data on atproto" /> 12 12 <link rel="manifest" href="/manifest.json" /> 13 13 <title>PDSls</title> 14 - <link rel="preconnect" href="https://rsms.me/" /> 15 - <link rel="stylesheet" href="https://rsms.me/inter/inter.css" /> 16 14 <link rel="preconnect" href="https://fonts.bunny.net" /> 17 15 <link href="https://fonts.bunny.net/css?family=roboto-mono:400" rel="stylesheet" /> 18 16 <link href="https://fonts.cdnfonts.com/css/pecita" rel="stylesheet" /> ··· 26 24 <script src="/src/index.tsx" type="module"></script> 27 25 </head> 28 26 29 - <body id="root" class="dark:bg-dark-500 min-h-screen bg-neutral-100"> 27 + <body 28 + id="root" 29 + class="dark:bg-dark-500 min-h-screen bg-neutral-100 text-neutral-900 dark:text-neutral-200" 30 + > 30 31 <noscript>You need to enable JavaScript to run this app.</noscript> 31 32 </body> 32 33 </html>
+21 -20
package.json
··· 9 9 "serve": "vite preview" 10 10 }, 11 11 "devDependencies": { 12 - "@iconify-json/lucide": "^1.2.75", 13 - "@iconify/tailwind4": "^1.1.0", 14 - "@tailwindcss/vite": "^4.1.17", 15 - "prettier": "^3.6.2", 12 + "@iconify-json/lucide": "^1.2.81", 13 + "@iconify/tailwind4": "^1.2.0", 14 + "@tailwindcss/vite": "^4.1.18", 15 + "prettier": "^3.7.4", 16 16 "prettier-plugin-organize-imports": "^4.3.0", 17 - "prettier-plugin-tailwindcss": "^0.7.1", 18 - "tailwindcss": "^4.1.17", 17 + "prettier-plugin-tailwindcss": "^0.7.2", 18 + "tailwindcss": "^4.1.18", 19 19 "typescript": "^5.9.3", 20 - "vite": "^7.2.4", 20 + "vite": "^7.2.7", 21 21 "vite-plugin-solid": "^2.11.10" 22 22 }, 23 23 "dependencies": { 24 24 "@atcute/atproto": "^3.1.9", 25 - "@atcute/bluesky": "^3.2.10", 26 - "@atcute/client": "^4.0.5", 27 - "@atcute/crypto": "^2.2.6", 25 + "@atcute/bluesky": "^3.2.14", 26 + "@atcute/client": "^4.1.1", 27 + "@atcute/crypto": "^2.3.0", 28 28 "@atcute/did-plc": "^0.2.0", 29 29 "@atcute/identity": "^1.1.3", 30 - "@atcute/identity-resolver": "^1.1.4", 31 - "@atcute/leaflet": "^1.0.12", 32 - "@atcute/lexicon-doc": "^2.0.1", 33 - "@atcute/lexicon-resolver": "^0.1.4", 34 - "@atcute/lexicons": "^1.2.4", 35 - "@atcute/oauth-browser-client": "^2.0.1", 30 + "@atcute/identity-resolver": "^1.2.0", 31 + "@atcute/leaflet": "^1.0.14", 32 + "@atcute/lexicon-doc": "^2.0.5", 33 + "@atcute/lexicon-resolver": "^0.1.5", 34 + "@atcute/lexicons": "^1.2.5", 35 + "@atcute/multibase": "^1.1.6", 36 + "@atcute/oauth-browser-client": "^2.0.3", 36 37 "@atcute/repo": "^0.1.0", 37 - "@atcute/tangled": "^1.0.12", 38 + "@atcute/tangled": "^1.0.13", 38 39 "@atcute/tid": "^1.0.3", 39 40 "@codemirror/commands": "^6.10.0", 40 41 "@codemirror/lang-json": "^6.0.2", 41 42 "@codemirror/lint": "^6.9.2", 42 43 "@codemirror/state": "^6.5.2", 43 - "@codemirror/view": "^6.38.8", 44 - "@fsegurai/codemirror-theme-basic-dark": "^6.2.2", 45 - "@fsegurai/codemirror-theme-basic-light": "^6.2.2", 44 + "@codemirror/view": "^6.39.4", 45 + "@fsegurai/codemirror-theme-basic-dark": "^6.2.3", 46 + "@fsegurai/codemirror-theme-basic-light": "^6.2.3", 46 47 "@mary/exif-rm": "jsr:^0.2.2", 47 48 "@skyware/firehose": "^0.5.2", 48 49 "@solidjs/meta": "^0.29.4",
+317 -817
pnpm-lock.yaml
··· 12 12 specifier: ^3.1.9 13 13 version: 3.1.9 14 14 '@atcute/bluesky': 15 - specifier: ^3.2.10 16 - version: 3.2.10 15 + specifier: ^3.2.14 16 + version: 3.2.14 17 17 '@atcute/client': 18 - specifier: ^4.0.5 19 - version: 4.0.5 18 + specifier: ^4.1.1 19 + version: 4.1.1 20 20 '@atcute/crypto': 21 - specifier: ^2.2.6 22 - version: 2.2.6 21 + specifier: ^2.3.0 22 + version: 2.3.0 23 23 '@atcute/did-plc': 24 24 specifier: ^0.2.0 25 25 version: 0.2.0 ··· 27 27 specifier: ^1.1.3 28 28 version: 1.1.3 29 29 '@atcute/identity-resolver': 30 - specifier: ^1.1.4 31 - version: 1.1.4(@atcute/identity@1.1.3) 30 + specifier: ^1.2.0 31 + version: 1.2.0(@atcute/identity@1.1.3) 32 32 '@atcute/leaflet': 33 - specifier: ^1.0.12 34 - version: 1.0.12 33 + specifier: ^1.0.14 34 + version: 1.0.14 35 35 '@atcute/lexicon-doc': 36 - specifier: ^2.0.1 37 - version: 2.0.1 36 + specifier: ^2.0.5 37 + version: 2.0.5 38 38 '@atcute/lexicon-resolver': 39 - specifier: ^0.1.4 40 - version: 0.1.4(@atcute/identity-resolver@1.1.4(@atcute/identity@1.1.3))(@atcute/identity@1.1.3) 39 + specifier: ^0.1.5 40 + version: 0.1.5(@atcute/identity-resolver@1.2.0(@atcute/identity@1.1.3))(@atcute/identity@1.1.3) 41 41 '@atcute/lexicons': 42 - specifier: ^1.2.4 43 - version: 1.2.4 42 + specifier: ^1.2.5 43 + version: 1.2.5 44 + '@atcute/multibase': 45 + specifier: ^1.1.6 46 + version: 1.1.6 44 47 '@atcute/oauth-browser-client': 45 - specifier: ^2.0.1 46 - version: 2.0.1 48 + specifier: ^2.0.3 49 + version: 2.0.3(@atcute/identity@1.1.3) 47 50 '@atcute/repo': 48 51 specifier: ^0.1.0 49 52 version: 0.1.0 50 53 '@atcute/tangled': 51 - specifier: ^1.0.12 52 - version: 1.0.12 54 + specifier: ^1.0.13 55 + version: 1.0.13 53 56 '@atcute/tid': 54 57 specifier: ^1.0.3 55 58 version: 1.0.3 ··· 66 69 specifier: ^6.5.2 67 70 version: 6.5.2 68 71 '@codemirror/view': 69 - specifier: ^6.38.8 70 - version: 6.38.8 72 + specifier: ^6.39.4 73 + version: 6.39.4 71 74 '@fsegurai/codemirror-theme-basic-dark': 72 - specifier: ^6.2.2 73 - version: 6.2.2(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.8)(@lezer/highlight@1.2.3) 75 + specifier: ^6.2.3 76 + version: 6.2.3(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.39.4)(@lezer/highlight@1.2.3) 74 77 '@fsegurai/codemirror-theme-basic-light': 75 - specifier: ^6.2.2 76 - version: 6.2.2(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.8)(@lezer/highlight@1.2.3) 78 + specifier: ^6.2.3 79 + version: 6.2.3(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.39.4)(@lezer/highlight@1.2.3) 77 80 '@mary/exif-rm': 78 81 specifier: jsr:^0.2.2 79 82 version: '@jsr/mary__exif-rm@0.2.2' ··· 94 97 version: 1.9.10 95 98 devDependencies: 96 99 '@iconify-json/lucide': 97 - specifier: ^1.2.75 98 - version: 1.2.75 100 + specifier: ^1.2.81 101 + version: 1.2.81 99 102 '@iconify/tailwind4': 100 - specifier: ^1.1.0 101 - version: 1.1.0(tailwindcss@4.1.17) 103 + specifier: ^1.2.0 104 + version: 1.2.0(tailwindcss@4.1.18) 102 105 '@tailwindcss/vite': 103 - specifier: ^4.1.17 104 - version: 4.1.17(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)) 106 + specifier: ^4.1.18 107 + version: 4.1.18(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)) 105 108 prettier: 106 - specifier: ^3.6.2 107 - version: 3.6.2 109 + specifier: ^3.7.4 110 + version: 3.7.4 108 111 prettier-plugin-organize-imports: 109 112 specifier: ^4.3.0 110 - version: 4.3.0(prettier@3.6.2)(typescript@5.9.3) 113 + version: 4.3.0(prettier@3.7.4)(typescript@5.9.3) 111 114 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) 115 + specifier: ^0.7.2 116 + version: 0.7.2(prettier-plugin-organize-imports@4.3.0(prettier@3.7.4)(typescript@5.9.3))(prettier@3.7.4) 114 117 tailwindcss: 115 - specifier: ^4.1.17 116 - version: 4.1.17 118 + specifier: ^4.1.18 119 + version: 4.1.18 117 120 typescript: 118 121 specifier: ^5.9.3 119 122 version: 5.9.3 120 123 vite: 121 - specifier: ^7.2.4 122 - version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2) 124 + specifier: ^7.2.7 125 + version: 7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2) 123 126 vite-plugin-solid: 124 127 specifier: ^2.11.10 125 - version: 2.11.10(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)) 128 + version: 2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)) 126 129 127 130 packages: 128 131 129 132 '@antfu/install-pkg@1.1.0': 130 133 resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} 131 134 132 - '@antfu/utils@8.1.1': 133 - resolution: {integrity: sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==} 134 - 135 135 '@atcute/atproto@3.1.9': 136 136 resolution: {integrity: sha512-DyWwHCTdR4hY2BPNbLXgVmm7lI+fceOwWbE4LXbGvbvVtSn+ejSVFaAv01Ra3kWDha0whsOmbJL8JP0QPpf1+w==} 137 137 138 - '@atcute/bluesky@3.2.10': 139 - resolution: {integrity: sha512-qwQWTzRf3umnh2u41gdU+xWYkbzGlKDupc3zeOB+YjmuP1N9wEaUhwS8H7vgrqr0xC9SGNDjeUVcjC4m5BPLBg==} 138 + '@atcute/bluesky@3.2.14': 139 + resolution: {integrity: sha512-XlVuF55AYIyplmKvlGLlj+cUvk9ggxNRPczkTPIY991xJ4qDxDHpBJ39ekAV4dWcuBoRo2o9JynzpafPu2ljDA==} 140 140 141 141 '@atcute/car@3.1.3': 142 142 resolution: {integrity: sha512-WJ13bAEt7TjDMVi09ubjLtvhdljbWInGm9Kfy7Y6NhrmiyC/aZYaA/zHX/bHI6xv1c/h3SQduWqxOr4ae49eqA==} ··· 150 150 '@atcute/cid@2.2.6': 151 151 resolution: {integrity: sha512-bTAHHbJ24p+E//V4KCS4xdmd39o211jJswvqQOevj7vk+5IYcgDLx1ryZWZ1sEPOo9x875li/kj5gpKL14RDwQ==} 152 152 153 - '@atcute/client@4.0.5': 154 - resolution: {integrity: sha512-R8Qen8goGmEkynYGg2m6XFlVmz0GTDvQ+9w+4QqOob+XMk8/WDpF4aImev7WKEde/rV2gjcqW7zM8E6W9NShDA==} 153 + '@atcute/client@4.1.1': 154 + resolution: {integrity: sha512-FROCbTTCeL5u4tO/n72jDEKyKqjdlXMB56Ehve3W/gnnLGCYWvN42sS7tvL1Mgu6sbO3yZwsXKDrmM2No4XpjA==} 155 155 156 - '@atcute/crypto@2.2.6': 157 - resolution: {integrity: sha512-vkuexF+kmrKE1/Uqzub99Qi4QpnxA2jbu60E6PTgL4XypELQ6rb59MB/J1VbY2gs0kd3ET7+L3+NWpKD5nXyfA==} 156 + '@atcute/crypto@2.3.0': 157 + resolution: {integrity: sha512-w5pkJKCjbNMQu+F4JRHbR3ROQyhi1wbn+GSC6WDQamcYHkZmEZk1/eoI354bIQOOfkEM6aFLv718iskrkon4GQ==} 158 158 159 159 '@atcute/did-plc@0.2.0': 160 160 resolution: {integrity: sha512-1sGek8GRM/Ph7nLVRREm8FqM7g4shGckItvdVwJcRbUa8Rh0zOsXQa0QyYWAC0k40BhkqO9FwKXhJEaXCmF5oQ==} 161 161 162 - '@atcute/identity-resolver@1.1.4': 163 - resolution: {integrity: sha512-/SVh8vf2cXFJenmBnGeYF2aY3WGQm3cJeew5NWTlkqoy3LvJ5wkvKq9PWu4Tv653VF40rPOp6LOdVr9Fa+q5rA==} 162 + '@atcute/identity-resolver@1.2.0': 163 + resolution: {integrity: sha512-5UbSJfdV3JIkF8ksXz7g4nKBWasf2wROvzM66cfvTIWydWFO6/oS1KZd+zo9Eokje5Scf5+jsY9ZfgVARLepXg==} 164 164 peerDependencies: 165 165 '@atcute/identity': ^1.0.0 166 166 167 167 '@atcute/identity@1.1.3': 168 168 resolution: {integrity: sha512-oIqPoI8TwWeQxvcLmFEZLdN2XdWcaLVtlm8pNk0E72As9HNzzD9pwKPrLr3rmTLRIoULPPFmq9iFNsTeCIU9ng==} 169 169 170 - '@atcute/leaflet@1.0.12': 171 - resolution: {integrity: sha512-T5laBTl8vwzy0eZXBy07IQSjsLqhbZmRJsffnNQ6XMSc+lnCZ/NHfuKy8TNJbDU6dc26Z7o5l0ELfWz5QESo+w==} 170 + '@atcute/leaflet@1.0.14': 171 + resolution: {integrity: sha512-TWbtB7b73GChBaYwfd7aWFyGVObZ/DqrRtwkpWGm1GO8zZmQ9eJyKDUnXim7NOAs2hmKQ1u2wk2AM4AYzkF5Gg==} 172 172 173 - '@atcute/lexicon-doc@2.0.1': 174 - resolution: {integrity: sha512-yWgcBYkvifczVODZSgdVkIljzIfdh50t+QXjkDL/FSu2RP43NGBEZ5xfZqJcT68/UoyE+doSg0dhvOEIlVGU/A==} 173 + '@atcute/lexicon-doc@2.0.5': 174 + resolution: {integrity: sha512-fNCp94ehGjWFZMIqP6pWD1F9MOJogNCyqsaMVZluPSIclZ+lDL528iXB56aW4u0eSiD6Y9WJB1OI/lElG39cSA==} 175 175 176 - '@atcute/lexicon-resolver@0.1.4': 177 - resolution: {integrity: sha512-8MAN3wrlP+PvyAbzHgzavaGeNNHq/r0LDEV4ABqDozHIZ2pcLR3O3J40UdiGW6ldeC/YciSkkmpl6f/zP3sXzw==} 176 + '@atcute/lexicon-resolver@0.1.5': 177 + resolution: {integrity: sha512-0bx1/zdMQPuxvRcHW6ykAxRxktC2rEZLoAVSFoLSWDAA92Tf09F9QPK5wgXSF4MNODm1dvzMEdWSMIvlg8sr3A==} 178 178 peerDependencies: 179 179 '@atcute/identity': ^1.1.0 180 180 '@atcute/identity-resolver': ^1.1.3 181 181 182 - '@atcute/lexicons@1.2.4': 183 - resolution: {integrity: sha512-s6fl/SVjQMv7jiitLCcZ434X+VrTsJt7Fl9iJg8WXHJIELRz/U0sNUoP++oWd7bvPy1Vcd2Wnm+YtTm/Zn7AIQ==} 182 + '@atcute/lexicons@1.2.5': 183 + resolution: {integrity: sha512-9yO9WdgxW8jZ7SbzUycH710z+JmsQ9W9n5S6i6eghYju32kkluFmgBeS47r8e8p2+Dv4DemS7o/3SUGsX9FR5Q==} 184 184 185 185 '@atcute/mst@0.1.0': 186 186 resolution: {integrity: sha512-h+iDToKEnBpigk2DOHjSqY63vJtjYKUIztqu1CZ0P+I54wV2SrgoqAXAT1xrW6A1Iup8cjTv+U2H5WVG4KxPLw==} ··· 188 188 '@atcute/multibase@1.1.6': 189 189 resolution: {integrity: sha512-HBxuCgYLKPPxETV0Rot4VP9e24vKl8JdzGCZOVsDaOXJgbRZoRIF67Lp0H/OgnJeH/Xpva8Z5ReoTNJE5dn3kg==} 190 190 191 - '@atcute/oauth-browser-client@2.0.1': 192 - resolution: {integrity: sha512-lG021GkeORG06zfFf4bH85egObjBEKHNgAWHvbtY/E2dX4wxo88hf370pJDx8acdnuUJLJ2VKPikJtZwo4Heeg==} 191 + '@atcute/oauth-browser-client@2.0.3': 192 + resolution: {integrity: sha512-rzUjwhjE4LRRKdQnCFQag/zXRZMEAB1hhBoLfnoQuHwWbmDUCL7fzwC3jRhDPp3om8XaYNDj8a/iqRip0wRqoQ==} 193 193 194 194 '@atcute/repo@0.1.0': 195 195 resolution: {integrity: sha512-INiYAuma8dydBu7cqd2WVpcXh3mzhIepYBUqFWAK5MqMulPRLTRCc/9GW3G9pxYrOdlvLCVamG2Jf8XK0nuFEw==} 196 196 197 - '@atcute/tangled@1.0.12': 198 - resolution: {integrity: sha512-JKA5sOhd8SLhDFhY+PKHqLLytQBBKSiwcaEzfYUJBeyfvqXFPNNAwvRbe3VST4IQ3izoOu3O0R9/b1mjL45UzA==} 197 + '@atcute/tangled@1.0.13': 198 + resolution: {integrity: sha512-K95jmjDXl/f1FFzOJkk07ibNbFsPmn64sdrMACxQmUibO9WcfSjzjZLPXuH6WHFnCNtIBG3x1FQ7ndQgLoZAmw==} 199 199 200 200 '@atcute/tid@1.0.3': 201 201 resolution: {integrity: sha512-wfMJx1IMdnu0CZgWl0uR4JO2s6PGT1YPhpytD4ZHzEYKKQVuqV6Eb/7vieaVo1eYNMp2FrY67FZObeR7utRl2w==} 202 202 203 - '@atcute/uint8array@1.0.5': 204 - resolution: {integrity: sha512-XLWWxoR2HNl2qU+FCr0rp1APwJXci7HnzbOQLxK55OaMNBXZ19+xNC5ii4QCsThsDxa4JS/JTzuiQLziITWf2Q==} 203 + '@atcute/uint8array@1.0.6': 204 + resolution: {integrity: sha512-ucfRBQc7BFT8n9eCyGOzDHEMKF/nZwhS2pPao4Xtab1ML3HdFYcX2DM1tadCzas85QTGxHe5urnUAAcNKGRi9A==} 205 205 206 - '@atcute/util-fetch@1.0.3': 207 - resolution: {integrity: sha512-f8zzTb/xlKIwv2OQ31DhShPUNCmIIleX6p7qIXwWwEUjX6x8skUtpdISSjnImq01LXpltGV5y8yhV4/Mlb7CRQ==} 206 + '@atcute/util-fetch@1.0.4': 207 + resolution: {integrity: sha512-sIU9Qk0dE8PLEXSfhy+gIJV+HpiiknMytCI2SqLlqd0vgZUtEKI/EQfP+23LHWvP+CLCzVDOa6cpH045OlmNBg==} 208 208 209 209 '@atcute/varint@1.0.3': 210 210 resolution: {integrity: sha512-fdvMPyBB+McDT+Ai5e9RwEbwYV4yjZ60S2Dn5PTjGqUyxvoCH1z42viuheDZRUDkmfQehXJTZ5az7dSozVNtog==} ··· 315 315 '@codemirror/state@6.5.2': 316 316 resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==} 317 317 318 - '@codemirror/view@6.38.8': 319 - resolution: {integrity: sha512-XcE9fcnkHCbWkjeKyi0lllwXmBLtyYb5dt89dJyx23I9+LSh5vZDIuk7OLG4VM1lgrXZQcY6cxyZyk5WVPRv/A==} 318 + '@codemirror/view@6.39.4': 319 + resolution: {integrity: sha512-xMF6OfEAUVY5Waega4juo1QGACfNkNF+aJLqpd8oUJz96ms2zbfQ9Gh35/tI3y8akEV31FruKfj7hBnIU/nkqA==} 320 + 321 + '@cyberalien/svg-utils@1.0.11': 322 + resolution: {integrity: sha512-qEE9mnyI+avfGT3emKuRs3ucYkITeaV0Xi7VlYN41f+uGnZBecQP3jwz/AF437H9J4Q7qPClHKm4NiTYpNE6hA==} 320 323 321 324 '@esbuild/aix-ppc64@0.23.1': 322 325 resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} ··· 618 621 cpu: [x64] 619 622 os: [win32] 620 623 621 - '@fsegurai/codemirror-theme-basic-dark@6.2.2': 622 - resolution: {integrity: sha512-cVK4VheF7ZkuV0sfy20lmH2S7Q2xIfKoqN2HdU5rpGH8mZM2LVG9Tl+oHT0XNPpsWFqNAAKLzjYFw0IPX95Biw==} 624 + '@fsegurai/codemirror-theme-basic-dark@6.2.3': 625 + resolution: {integrity: sha512-08d09Yn9Ic8mjCzrBQQhtws/HM+8B00bRV9FqW+GaIQwSOFmn17FsvzuLJQyervcKAkTzmKaLPjp2D3Y+2K8EQ==} 623 626 peerDependencies: 624 627 '@codemirror/language': ^6.0.0 625 628 '@codemirror/state': ^6.0.0 626 629 '@codemirror/view': ^6.0.0 627 630 '@lezer/highlight': ^1.0.0 628 631 629 - '@fsegurai/codemirror-theme-basic-light@6.2.2': 630 - resolution: {integrity: sha512-zFtJ6VwwEeZ/HAXMYdcJz6+DdW1aQkngFwbD3diku79cctpTglCWH49KRFO8Mifjzwylsynm7dLyOUnGhIu0NQ==} 632 + '@fsegurai/codemirror-theme-basic-light@6.2.3': 633 + resolution: {integrity: sha512-rkHCj1U3OwNAqLLi2xti47u3Fq6gDiSEKmQsAOwIADJKnnwU2LeAwCPqSEa7sUVlavFusjDvt5L/SmGjb10vWg==} 631 634 peerDependencies: 632 635 '@codemirror/language': ^6.0.0 633 636 '@codemirror/state': ^6.0.0 634 637 '@codemirror/view': ^6.0.0 635 638 '@lezer/highlight': ^1.0.0 636 639 637 - '@iconify-json/lucide@1.2.75': 638 - resolution: {integrity: sha512-sWBN0t/rTo1FxWG/46xKgkIcDerHpsjyNgMH48nvtC4/kUG88sFQXI+7mxX3SD8eSUaQQ2kS9C7ZKWm2DKgBlw==} 640 + '@iconify-json/lucide@1.2.81': 641 + resolution: {integrity: sha512-6Kz/+SEuD5bkg0KImi0yFem9l6njKp4e1qF1LpQbgRfk7ngsJR/qjlB4y5rM8N1iKiDR/p19cqhmwZxyCWek+w==} 639 642 640 - '@iconify/tailwind4@1.1.0': 641 - resolution: {integrity: sha512-HqgAYtYk4eFtLvdYfhQrBRT9ohToh+VJJVhHtJ7B4Qhw+J+mRPvGC9Wr6Cgtb36YbIWqBxWuBaAUw9TE/8m2/w==} 643 + '@iconify/tailwind4@1.2.0': 644 + resolution: {integrity: sha512-+t7XqfojOB0zzZdd8gV7IQZGq1AaIHTlsxMVzagxYR0hAlJCLUD63o3iSlNKRMH3ZR7gZ8y5c9dJ7J431avRbA==} 642 645 peerDependencies: 643 646 tailwindcss: '>= 4.0.0' 644 647 645 - '@iconify/tools@4.1.4': 646 - resolution: {integrity: sha512-s6BcNUcCxQ3S6cvhlsoWzOuBt8qKXdVyXB9rT57uSJ/ARHD7dVM43+5ERBWn3tmkMWXeJ/s9DPVc3dUasayzeA==} 648 + '@iconify/tools@5.0.0': 649 + resolution: {integrity: sha512-GY/FsuNdWA/FbkLqgQ8b1PHFkNvjMeSFWaVJdLldYGHBp0lZ64HJlcS0qzLfglacHTd8zYdfQjF74RxGqyGMgw==} 647 650 648 651 '@iconify/types@2.0.0': 649 652 resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} 650 653 651 - '@iconify/utils@2.3.0': 652 - resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==} 654 + '@iconify/utils@3.1.0': 655 + resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==} 653 656 654 657 '@jridgewell/gen-mapping@0.3.13': 655 658 resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} ··· 670 673 '@jsr/mary__exif-rm@0.2.2': 671 674 resolution: {integrity: sha512-+ZpLaC+1CyqWhH608Sqd6/yTG0pOlokn2tCXha7s1SMQ+GLKo4Nn/PskTeeP9Pt+6gNYSu6ednoSlRvXb2ZGxg==, tarball: https://npm.jsr.io/~/11/@jsr/mary__exif-rm/0.2.2.tgz} 672 675 673 - '@lezer/common@1.3.0': 674 - resolution: {integrity: sha512-L9X8uHCYU310o99L3/MpJKYxPzXPOS7S0NmBaM7UO/x2Kb2WbmMLSkfvdr1KxRIFYOpbY0Jhn7CfLSUDzL8arQ==} 676 + '@lezer/common@1.4.0': 677 + resolution: {integrity: sha512-DVeMRoGrgn/k45oQNu189BoW4SZwgZFzJ1+1TV5j2NJ/KFC83oa/enRqZSGshyeMk5cPWMhsKs9nx+8o0unwGg==} 675 678 676 679 '@lezer/highlight@1.2.3': 677 680 resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} ··· 679 682 '@lezer/json@1.0.3': 680 683 resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==} 681 684 682 - '@lezer/lr@1.4.3': 683 - resolution: {integrity: sha512-yenN5SqAxAPv/qMnpWW0AT7l+SxVrgG+u0tNsRQWqbrz66HIl8DnEbBObvy21J5K7+I1v7gsAnlE2VQ5yYVSeA==} 685 + '@lezer/lr@1.4.5': 686 + resolution: {integrity: sha512-/YTRKP5yPPSo1xImYQk7AZZMAgap0kegzqCSYHjAL9x1AZ0ZQW+IpcEzMKagCsbTsLnVeWkxYrCNeXG8xEPrjg==} 684 687 685 688 '@marijn/find-cluster-break@1.0.2': 686 689 resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} ··· 814 817 '@standard-schema/spec@1.0.0': 815 818 resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} 816 819 817 - '@tailwindcss/node@4.1.17': 818 - resolution: {integrity: sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==} 820 + '@tailwindcss/node@4.1.18': 821 + resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} 819 822 820 - '@tailwindcss/oxide-android-arm64@4.1.17': 821 - resolution: {integrity: sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==} 823 + '@tailwindcss/oxide-android-arm64@4.1.18': 824 + resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==} 822 825 engines: {node: '>= 10'} 823 826 cpu: [arm64] 824 827 os: [android] 825 828 826 - '@tailwindcss/oxide-darwin-arm64@4.1.17': 827 - resolution: {integrity: sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==} 829 + '@tailwindcss/oxide-darwin-arm64@4.1.18': 830 + resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==} 828 831 engines: {node: '>= 10'} 829 832 cpu: [arm64] 830 833 os: [darwin] 831 834 832 - '@tailwindcss/oxide-darwin-x64@4.1.17': 833 - resolution: {integrity: sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==} 835 + '@tailwindcss/oxide-darwin-x64@4.1.18': 836 + resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==} 834 837 engines: {node: '>= 10'} 835 838 cpu: [x64] 836 839 os: [darwin] 837 840 838 - '@tailwindcss/oxide-freebsd-x64@4.1.17': 839 - resolution: {integrity: sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==} 841 + '@tailwindcss/oxide-freebsd-x64@4.1.18': 842 + resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==} 840 843 engines: {node: '>= 10'} 841 844 cpu: [x64] 842 845 os: [freebsd] 843 846 844 - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17': 845 - resolution: {integrity: sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==} 847 + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': 848 + resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==} 846 849 engines: {node: '>= 10'} 847 850 cpu: [arm] 848 851 os: [linux] 849 852 850 - '@tailwindcss/oxide-linux-arm64-gnu@4.1.17': 851 - resolution: {integrity: sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==} 853 + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': 854 + resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==} 852 855 engines: {node: '>= 10'} 853 856 cpu: [arm64] 854 857 os: [linux] 855 858 856 - '@tailwindcss/oxide-linux-arm64-musl@4.1.17': 857 - resolution: {integrity: sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==} 859 + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': 860 + resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} 858 861 engines: {node: '>= 10'} 859 862 cpu: [arm64] 860 863 os: [linux] 861 864 862 - '@tailwindcss/oxide-linux-x64-gnu@4.1.17': 863 - resolution: {integrity: sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==} 865 + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': 866 + resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} 864 867 engines: {node: '>= 10'} 865 868 cpu: [x64] 866 869 os: [linux] 867 870 868 - '@tailwindcss/oxide-linux-x64-musl@4.1.17': 869 - resolution: {integrity: sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==} 871 + '@tailwindcss/oxide-linux-x64-musl@4.1.18': 872 + resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} 870 873 engines: {node: '>= 10'} 871 874 cpu: [x64] 872 875 os: [linux] 873 876 874 - '@tailwindcss/oxide-wasm32-wasi@4.1.17': 875 - resolution: {integrity: sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==} 877 + '@tailwindcss/oxide-wasm32-wasi@4.1.18': 878 + resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} 876 879 engines: {node: '>=14.0.0'} 877 880 cpu: [wasm32] 878 881 bundledDependencies: ··· 883 886 - '@emnapi/wasi-threads' 884 887 - tslib 885 888 886 - '@tailwindcss/oxide-win32-arm64-msvc@4.1.17': 887 - resolution: {integrity: sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==} 889 + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': 890 + resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==} 888 891 engines: {node: '>= 10'} 889 892 cpu: [arm64] 890 893 os: [win32] 891 894 892 - '@tailwindcss/oxide-win32-x64-msvc@4.1.17': 893 - resolution: {integrity: sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==} 895 + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': 896 + resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==} 894 897 engines: {node: '>= 10'} 895 898 cpu: [x64] 896 899 os: [win32] 897 900 898 - '@tailwindcss/oxide@4.1.17': 899 - resolution: {integrity: sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==} 901 + '@tailwindcss/oxide@4.1.18': 902 + resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==} 900 903 engines: {node: '>= 10'} 901 904 902 - '@tailwindcss/vite@4.1.17': 903 - resolution: {integrity: sha512-4+9w8ZHOiGnpcGI6z1TVVfWaX/koK7fKeSYF3qlYg2xpBtbteP2ddBxiarL+HVgfSJGeK5RIxRQmKm4rTJJAwA==} 905 + '@tailwindcss/vite@4.1.18': 906 + resolution: {integrity: sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==} 904 907 peerDependencies: 905 908 vite: ^5.2.0 || ^6 || ^7 906 909 907 - '@trysound/sax@0.2.0': 908 - resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} 909 - engines: {node: '>=10.13.0'} 910 - 911 910 '@types/babel__core@7.20.5': 912 911 resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} 913 912 ··· 926 925 '@types/node@24.10.1': 927 926 resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} 928 927 929 - '@types/tar@6.1.13': 930 - resolution: {integrity: sha512-IznnlmU5f4WcGTh2ltRu/Ijpmk8wiWXfF0VA4s+HPjHZgvFggk1YaIkbo5krX/zUCzWF8N/l4+W/LNxnvAJ8nw==} 931 - 932 - '@types/yauzl@2.10.3': 933 - resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} 934 - 935 928 acorn@8.15.0: 936 929 resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} 937 930 engines: {node: '>=0.4.0'} 938 931 hasBin: true 939 932 940 - asynckit@0.4.0: 941 - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} 942 - 943 - axios@1.13.2: 944 - resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} 945 - 946 933 babel-plugin-jsx-dom-expressions@0.40.3: 947 934 resolution: {integrity: sha512-5HOwwt0BYiv/zxl7j8Pf2bGL6rDXfV6nUhLs8ygBX+EFJXzBPHM/euj9j/6deMZ6wa52Wb2PBaAV5U/jKwIY1w==} 948 935 peerDependencies: ··· 957 944 solid-js: 958 945 optional: true 959 946 960 - baseline-browser-mapping@2.8.31: 961 - resolution: {integrity: sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==} 947 + baseline-browser-mapping@2.9.7: 948 + resolution: {integrity: sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg==} 962 949 hasBin: true 963 950 964 951 boolbase@1.0.0: 965 952 resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} 966 953 967 - browserslist@4.28.0: 968 - resolution: {integrity: sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==} 954 + browserslist@4.28.1: 955 + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} 969 956 engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} 970 957 hasBin: true 971 958 972 - buffer-crc32@0.2.13: 973 - resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} 974 - 975 - call-bind-apply-helpers@1.0.2: 976 - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} 977 - engines: {node: '>= 0.4'} 978 - 979 - caniuse-lite@1.0.30001757: 980 - resolution: {integrity: sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==} 981 - 982 - cheerio-select@2.1.0: 983 - resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} 984 - 985 - cheerio@1.0.0: 986 - resolution: {integrity: sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==} 987 - engines: {node: '>=18.17'} 988 - 989 - chownr@2.0.0: 990 - resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} 991 - engines: {node: '>=10'} 959 + caniuse-lite@1.0.30001760: 960 + resolution: {integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==} 992 961 993 962 codemirror@6.0.2: 994 963 resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} 995 964 996 - combined-stream@1.0.8: 997 - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} 998 - engines: {node: '>= 0.8'} 999 - 1000 - commander@7.2.0: 1001 - resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} 1002 - engines: {node: '>= 10'} 965 + commander@11.1.0: 966 + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} 967 + engines: {node: '>=16'} 1003 968 1004 969 confbox@0.1.8: 1005 970 resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} 1006 971 1007 - confbox@0.2.2: 1008 - resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} 1009 - 1010 972 convert-source-map@2.0.0: 1011 973 resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} 1012 974 ··· 1020 982 resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} 1021 983 engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} 1022 984 1023 - css-tree@2.3.1: 1024 - resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} 985 + css-tree@3.1.0: 986 + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} 1025 987 engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} 1026 988 1027 989 css-what@6.2.2: ··· 1044 1006 supports-color: 1045 1007 optional: true 1046 1008 1047 - delayed-stream@1.0.0: 1048 - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} 1049 - engines: {node: '>=0.4.0'} 1050 - 1051 1009 detect-libc@2.1.2: 1052 1010 resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} 1053 1011 engines: {node: '>=8'} ··· 1065 1023 domutils@3.2.2: 1066 1024 resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} 1067 1025 1068 - dunder-proto@1.0.1: 1069 - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} 1070 - engines: {node: '>= 0.4'} 1071 - 1072 - electron-to-chromium@1.5.259: 1073 - resolution: {integrity: sha512-I+oLXgpEJzD6Cwuwt1gYjxsDmu/S/Kd41mmLA3O+/uH2pFRO/DvOjUyGozL8j3KeLV6WyZ7ssPwELMsXCcsJAQ==} 1074 - 1075 - encoding-sniffer@0.2.1: 1076 - resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==} 1077 - 1078 - end-of-stream@1.4.5: 1079 - resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} 1026 + electron-to-chromium@1.5.267: 1027 + resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} 1080 1028 1081 - enhanced-resolve@5.18.3: 1082 - resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} 1029 + enhanced-resolve@5.18.4: 1030 + resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} 1083 1031 engines: {node: '>=10.13.0'} 1084 1032 1085 1033 entities@4.5.0: ··· 1089 1037 entities@6.0.1: 1090 1038 resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} 1091 1039 engines: {node: '>=0.12'} 1092 - 1093 - es-define-property@1.0.1: 1094 - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} 1095 - engines: {node: '>= 0.4'} 1096 - 1097 - es-errors@1.3.0: 1098 - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} 1099 - engines: {node: '>= 0.4'} 1100 - 1101 - es-object-atoms@1.1.1: 1102 - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} 1103 - engines: {node: '>= 0.4'} 1104 - 1105 - es-set-tostringtag@2.1.0: 1106 - resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} 1107 - engines: {node: '>= 0.4'} 1108 1040 1109 1041 esbuild@0.23.1: 1110 1042 resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} ··· 1123 1055 esm-env@1.2.2: 1124 1056 resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} 1125 1057 1126 - exsolve@1.0.8: 1127 - resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} 1128 - 1129 - extract-zip@2.0.1: 1130 - resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} 1131 - engines: {node: '>= 10.17.0'} 1132 - hasBin: true 1133 - 1134 - fd-slicer@1.1.0: 1135 - resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} 1136 - 1137 1058 fdir@6.5.0: 1138 1059 resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} 1139 1060 engines: {node: '>=12.0.0'} ··· 1143 1064 picomatch: 1144 1065 optional: true 1145 1066 1146 - follow-redirects@1.15.11: 1147 - resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} 1148 - engines: {node: '>=4.0'} 1149 - peerDependencies: 1150 - debug: '*' 1151 - peerDependenciesMeta: 1152 - debug: 1153 - optional: true 1154 - 1155 - form-data@4.0.5: 1156 - resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} 1157 - engines: {node: '>= 6'} 1158 - 1159 - fs-minipass@2.1.0: 1160 - resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} 1161 - engines: {node: '>= 8'} 1067 + fflate@0.8.2: 1068 + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} 1162 1069 1163 1070 fsevents@2.3.3: 1164 1071 resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 1165 1072 engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 1166 1073 os: [darwin] 1167 1074 1168 - function-bind@1.1.2: 1169 - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 1170 - 1171 1075 gensync@1.0.0-beta.2: 1172 1076 resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} 1173 1077 engines: {node: '>=6.9.0'} 1174 1078 1175 - get-intrinsic@1.3.0: 1176 - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} 1177 - engines: {node: '>= 0.4'} 1178 - 1179 - get-proto@1.0.1: 1180 - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} 1181 - engines: {node: '>= 0.4'} 1182 - 1183 - get-stream@5.2.0: 1184 - resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} 1185 - engines: {node: '>=8'} 1186 - 1187 1079 get-tsconfig@4.13.0: 1188 1080 resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} 1189 1081 1190 - globals@15.15.0: 1191 - resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} 1192 - engines: {node: '>=18'} 1193 - 1194 - gopd@1.2.0: 1195 - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} 1196 - engines: {node: '>= 0.4'} 1197 - 1198 1082 graceful-fs@4.2.11: 1199 1083 resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 1200 1084 1201 - has-symbols@1.1.0: 1202 - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} 1203 - engines: {node: '>= 0.4'} 1204 - 1205 - has-tostringtag@1.0.2: 1206 - resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} 1207 - engines: {node: '>= 0.4'} 1208 - 1209 - hasown@2.0.2: 1210 - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} 1211 - engines: {node: '>= 0.4'} 1212 - 1213 1085 html-entities@2.3.3: 1214 1086 resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} 1215 1087 1216 - htmlparser2@9.1.0: 1217 - resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} 1218 - 1219 - iconv-lite@0.6.3: 1220 - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} 1221 - engines: {node: '>=0.10.0'} 1222 - 1223 1088 is-what@4.1.16: 1224 1089 resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} 1225 1090 engines: {node: '>=12.13'} ··· 1240 1105 resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} 1241 1106 engines: {node: '>=6'} 1242 1107 hasBin: true 1243 - 1244 - kolorist@1.8.0: 1245 - resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} 1246 1108 1247 1109 lightningcss-android-arm64@1.30.2: 1248 1110 resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} ··· 1314 1176 resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} 1315 1177 engines: {node: '>= 12.0.0'} 1316 1178 1317 - local-pkg@0.5.1: 1318 - resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==} 1319 - engines: {node: '>=14'} 1320 - 1321 - local-pkg@1.1.2: 1322 - resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} 1323 - engines: {node: '>=14'} 1324 - 1325 1179 lru-cache@5.1.1: 1326 1180 resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} 1327 1181 1328 1182 magic-string@0.30.21: 1329 1183 resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} 1330 1184 1331 - math-intrinsics@1.1.0: 1332 - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} 1333 - engines: {node: '>= 0.4'} 1334 - 1335 1185 mdn-data@2.0.28: 1336 1186 resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} 1337 1187 1338 - mdn-data@2.0.30: 1339 - resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} 1188 + mdn-data@2.12.2: 1189 + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} 1340 1190 1341 1191 merge-anything@5.1.7: 1342 1192 resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==} 1343 1193 engines: {node: '>=12.13'} 1344 1194 1345 - mime-db@1.52.0: 1346 - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} 1347 - engines: {node: '>= 0.6'} 1348 - 1349 - mime-types@2.1.35: 1350 - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} 1351 - engines: {node: '>= 0.6'} 1352 - 1353 - minipass@3.3.6: 1354 - resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} 1355 - engines: {node: '>=8'} 1356 - 1357 - minipass@4.2.8: 1358 - resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} 1359 - engines: {node: '>=8'} 1360 - 1361 - minipass@5.0.0: 1362 - resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} 1363 - engines: {node: '>=8'} 1364 - 1365 - minizlib@2.1.2: 1366 - resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} 1367 - engines: {node: '>= 8'} 1368 - 1369 - mkdirp@1.0.4: 1370 - resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} 1371 - engines: {node: '>=10'} 1372 - hasBin: true 1373 - 1374 1195 mlly@1.8.0: 1375 1196 resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} 1197 + 1198 + modern-tar@0.7.2: 1199 + resolution: {integrity: sha512-TGG1ZRk1TAQ3neuZwahAHke3rKsSlro+ooMYtjh9sl2gGPVMLMuWiHgwC7im9T5bSM566RSo2Dko56ETgEvZcA==} 1200 + engines: {node: '>=18.0.0'} 1376 1201 1377 1202 ms@2.1.3: 1378 1203 resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} ··· 1397 1222 nth-check@2.1.1: 1398 1223 resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} 1399 1224 1400 - once@1.4.0: 1401 - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 1402 - 1403 - package-manager-detector@1.5.0: 1404 - resolution: {integrity: sha512-uBj69dVlYe/+wxj8JOpr97XfsxH/eumMt6HqjNTmJDf/6NO9s+0uxeOneIz3AsPt2m6y9PqzDzd3ATcU17MNfw==} 1405 - 1406 - parse5-htmlparser2-tree-adapter@7.1.0: 1407 - resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} 1408 - 1409 - parse5-parser-stream@7.1.2: 1410 - resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==} 1225 + package-manager-detector@1.6.0: 1226 + resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} 1411 1227 1412 1228 parse5@7.3.0: 1413 1229 resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} 1414 1230 1415 - pathe@1.1.2: 1416 - resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} 1417 - 1418 1231 pathe@2.0.3: 1419 1232 resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} 1420 - 1421 - pend@1.2.0: 1422 - resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} 1423 1233 1424 1234 picocolors@1.1.1: 1425 1235 resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} ··· 1431 1241 pkg-types@1.3.1: 1432 1242 resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} 1433 1243 1434 - pkg-types@2.3.0: 1435 - resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} 1436 - 1437 1244 postcss@8.5.6: 1438 1245 resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} 1439 1246 engines: {node: ^10 || ^12 || >=14} ··· 1448 1255 vue-tsc: 1449 1256 optional: true 1450 1257 1451 - prettier-plugin-tailwindcss@0.7.1: 1452 - resolution: {integrity: sha512-Bzv1LZcuiR1Sk02iJTS1QzlFNp/o5l2p3xkopwOrbPmtMeh3fK9rVW5M3neBQzHq+kGKj/4LGQMTNcTH4NGPtQ==} 1258 + prettier-plugin-tailwindcss@0.7.2: 1259 + resolution: {integrity: sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA==} 1453 1260 engines: {node: '>=20.19'} 1454 1261 peerDependencies: 1455 1262 '@ianvs/prettier-plugin-sort-imports': '*' ··· 1503 1310 prettier-plugin-svelte: 1504 1311 optional: true 1505 1312 1506 - prettier@3.6.2: 1507 - resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} 1313 + prettier@3.7.4: 1314 + resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} 1508 1315 engines: {node: '>=14'} 1509 1316 hasBin: true 1510 1317 1511 - proxy-from-env@1.1.0: 1512 - resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} 1513 - 1514 - pump@3.0.3: 1515 - resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} 1516 - 1517 - quansync@0.2.11: 1518 - resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} 1519 - 1520 1318 resolve-pkg-maps@1.0.0: 1521 1319 resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} 1522 1320 ··· 1525 1323 engines: {node: '>=18.0.0', npm: '>=8.0.0'} 1526 1324 hasBin: true 1527 1325 1528 - safer-buffer@2.1.2: 1529 - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} 1326 + sax@1.4.3: 1327 + resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==} 1530 1328 1531 1329 semver@6.3.1: 1532 1330 resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} ··· 1557 1355 style-mod@4.1.3: 1558 1356 resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} 1559 1357 1560 - svgo@3.3.2: 1561 - resolution: {integrity: sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==} 1562 - engines: {node: '>=14.0.0'} 1358 + svgo@4.0.0: 1359 + resolution: {integrity: sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==} 1360 + engines: {node: '>=16'} 1563 1361 hasBin: true 1564 1362 1565 - tailwindcss@4.1.17: 1566 - resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==} 1363 + tailwindcss@4.1.18: 1364 + resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} 1567 1365 1568 1366 tapable@2.3.0: 1569 1367 resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} 1570 1368 engines: {node: '>=6'} 1571 1369 1572 - tar@6.2.1: 1573 - resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} 1574 - engines: {node: '>=10'} 1575 - 1576 1370 tinyexec@1.0.2: 1577 1371 resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} 1578 1372 engines: {node: '>=18'} ··· 1597 1391 undici-types@7.16.0: 1598 1392 resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} 1599 1393 1600 - undici@6.22.0: 1601 - resolution: {integrity: sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==} 1602 - engines: {node: '>=18.17'} 1603 - 1604 - update-browserslist-db@1.1.4: 1605 - resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} 1394 + update-browserslist-db@1.2.2: 1395 + resolution: {integrity: sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==} 1606 1396 hasBin: true 1607 1397 peerDependencies: 1608 1398 browserslist: '>= 4.21.0' ··· 1617 1407 '@testing-library/jest-dom': 1618 1408 optional: true 1619 1409 1620 - vite@7.2.4: 1621 - resolution: {integrity: sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==} 1410 + vite@7.2.7: 1411 + resolution: {integrity: sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==} 1622 1412 engines: {node: ^20.19.0 || >=22.12.0} 1623 1413 hasBin: true 1624 1414 peerDependencies: ··· 1668 1458 w3c-keyname@2.2.8: 1669 1459 resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} 1670 1460 1671 - whatwg-encoding@3.1.1: 1672 - resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} 1673 - engines: {node: '>=18'} 1674 - 1675 - whatwg-mimetype@4.0.0: 1676 - resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} 1677 - engines: {node: '>=18'} 1678 - 1679 - wrappy@1.0.2: 1680 - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 1681 - 1682 1461 yallist@3.1.1: 1683 1462 resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} 1684 1463 1685 - yallist@4.0.0: 1686 - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} 1687 - 1688 - yauzl@2.10.0: 1689 - resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} 1690 - 1691 1464 yocto-queue@1.2.2: 1692 1465 resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} 1693 1466 engines: {node: '>=12.20'} ··· 1696 1469 1697 1470 '@antfu/install-pkg@1.1.0': 1698 1471 dependencies: 1699 - package-manager-detector: 1.5.0 1472 + package-manager-detector: 1.6.0 1700 1473 tinyexec: 1.0.2 1701 1474 1702 - '@antfu/utils@8.1.1': {} 1703 - 1704 1475 '@atcute/atproto@3.1.9': 1705 1476 dependencies: 1706 - '@atcute/lexicons': 1.2.4 1477 + '@atcute/lexicons': 1.2.5 1707 1478 1708 - '@atcute/bluesky@3.2.10': 1479 + '@atcute/bluesky@3.2.14': 1709 1480 dependencies: 1710 1481 '@atcute/atproto': 3.1.9 1711 - '@atcute/lexicons': 1.2.4 1482 + '@atcute/lexicons': 1.2.5 1712 1483 1713 1484 '@atcute/car@3.1.3': 1714 1485 dependencies: 1715 1486 '@atcute/cbor': 2.2.8 1716 1487 '@atcute/cid': 2.2.6 1717 - '@atcute/uint8array': 1.0.5 1488 + '@atcute/uint8array': 1.0.6 1718 1489 '@atcute/varint': 1.0.3 1719 1490 yocto-queue: 1.2.2 1720 1491 ··· 1722 1493 dependencies: 1723 1494 '@atcute/cbor': 2.2.8 1724 1495 '@atcute/cid': 2.2.6 1725 - '@atcute/uint8array': 1.0.5 1496 + '@atcute/uint8array': 1.0.6 1726 1497 '@atcute/varint': 1.0.3 1727 1498 1728 1499 '@atcute/cbor@2.2.8': 1729 1500 dependencies: 1730 1501 '@atcute/cid': 2.2.6 1731 1502 '@atcute/multibase': 1.1.6 1732 - '@atcute/uint8array': 1.0.5 1503 + '@atcute/uint8array': 1.0.6 1733 1504 1734 1505 '@atcute/cid@2.2.6': 1735 1506 dependencies: 1736 1507 '@atcute/multibase': 1.1.6 1737 - '@atcute/uint8array': 1.0.5 1508 + '@atcute/uint8array': 1.0.6 1738 1509 1739 - '@atcute/client@4.0.5': 1510 + '@atcute/client@4.1.1': 1740 1511 dependencies: 1741 1512 '@atcute/identity': 1.1.3 1742 - '@atcute/lexicons': 1.2.4 1513 + '@atcute/lexicons': 1.2.5 1743 1514 1744 - '@atcute/crypto@2.2.6': 1515 + '@atcute/crypto@2.3.0': 1745 1516 dependencies: 1746 1517 '@atcute/multibase': 1.1.6 1747 - '@atcute/uint8array': 1.0.5 1518 + '@atcute/uint8array': 1.0.6 1748 1519 '@noble/secp256k1': 3.0.0 1749 1520 1750 1521 '@atcute/did-plc@0.2.0': 1751 1522 dependencies: 1752 1523 '@atcute/cbor': 2.2.8 1753 1524 '@atcute/cid': 2.2.6 1754 - '@atcute/crypto': 2.2.6 1525 + '@atcute/crypto': 2.3.0 1755 1526 '@atcute/identity': 1.1.3 1756 - '@atcute/lexicons': 1.2.4 1527 + '@atcute/lexicons': 1.2.5 1757 1528 '@atcute/multibase': 1.1.6 1758 - '@atcute/uint8array': 1.0.5 1529 + '@atcute/uint8array': 1.0.6 1759 1530 '@badrap/valita': 0.4.6 1760 1531 1761 - '@atcute/identity-resolver@1.1.4(@atcute/identity@1.1.3)': 1532 + '@atcute/identity-resolver@1.2.0(@atcute/identity@1.1.3)': 1762 1533 dependencies: 1763 1534 '@atcute/identity': 1.1.3 1764 - '@atcute/lexicons': 1.2.4 1765 - '@atcute/util-fetch': 1.0.3 1535 + '@atcute/lexicons': 1.2.5 1536 + '@atcute/util-fetch': 1.0.4 1766 1537 '@badrap/valita': 0.4.6 1767 1538 1768 1539 '@atcute/identity@1.1.3': 1769 1540 dependencies: 1770 - '@atcute/lexicons': 1.2.4 1541 + '@atcute/lexicons': 1.2.5 1771 1542 '@badrap/valita': 0.4.6 1772 1543 1773 - '@atcute/leaflet@1.0.12': 1544 + '@atcute/leaflet@1.0.14': 1774 1545 dependencies: 1775 1546 '@atcute/atproto': 3.1.9 1776 - '@atcute/lexicons': 1.2.4 1547 + '@atcute/lexicons': 1.2.5 1777 1548 1778 - '@atcute/lexicon-doc@2.0.1': 1549 + '@atcute/lexicon-doc@2.0.5': 1779 1550 dependencies: 1780 1551 '@atcute/identity': 1.1.3 1781 - '@atcute/lexicons': 1.2.4 1552 + '@atcute/lexicons': 1.2.5 1782 1553 '@badrap/valita': 0.4.6 1783 1554 1784 - '@atcute/lexicon-resolver@0.1.4(@atcute/identity-resolver@1.1.4(@atcute/identity@1.1.3))(@atcute/identity@1.1.3)': 1555 + '@atcute/lexicon-resolver@0.1.5(@atcute/identity-resolver@1.2.0(@atcute/identity@1.1.3))(@atcute/identity@1.1.3)': 1785 1556 dependencies: 1786 - '@atcute/crypto': 2.2.6 1557 + '@atcute/crypto': 2.3.0 1787 1558 '@atcute/identity': 1.1.3 1788 - '@atcute/identity-resolver': 1.1.4(@atcute/identity@1.1.3) 1789 - '@atcute/lexicon-doc': 2.0.1 1790 - '@atcute/lexicons': 1.2.4 1559 + '@atcute/identity-resolver': 1.2.0(@atcute/identity@1.1.3) 1560 + '@atcute/lexicon-doc': 2.0.5 1561 + '@atcute/lexicons': 1.2.5 1791 1562 '@atcute/repo': 0.1.0 1792 - '@atcute/util-fetch': 1.0.3 1563 + '@atcute/util-fetch': 1.0.4 1793 1564 '@badrap/valita': 0.4.6 1794 1565 1795 - '@atcute/lexicons@1.2.4': 1566 + '@atcute/lexicons@1.2.5': 1796 1567 dependencies: 1797 1568 '@standard-schema/spec': 1.0.0 1798 1569 esm-env: 1.2.2 ··· 1801 1572 dependencies: 1802 1573 '@atcute/cbor': 2.2.8 1803 1574 '@atcute/cid': 2.2.6 1804 - '@atcute/uint8array': 1.0.5 1575 + '@atcute/uint8array': 1.0.6 1805 1576 1806 1577 '@atcute/multibase@1.1.6': 1807 1578 dependencies: 1808 - '@atcute/uint8array': 1.0.5 1579 + '@atcute/uint8array': 1.0.6 1809 1580 1810 - '@atcute/oauth-browser-client@2.0.1': 1581 + '@atcute/oauth-browser-client@2.0.3(@atcute/identity@1.1.3)': 1811 1582 dependencies: 1812 - '@atcute/client': 4.0.5 1813 - '@atcute/identity': 1.1.3 1814 - '@atcute/identity-resolver': 1.1.4(@atcute/identity@1.1.3) 1815 - '@atcute/lexicons': 1.2.4 1583 + '@atcute/client': 4.1.1 1584 + '@atcute/identity-resolver': 1.2.0(@atcute/identity@1.1.3) 1585 + '@atcute/lexicons': 1.2.5 1816 1586 '@atcute/multibase': 1.1.6 1817 - '@atcute/uint8array': 1.0.5 1587 + '@atcute/uint8array': 1.0.6 1818 1588 nanoid: 5.1.6 1589 + transitivePeerDependencies: 1590 + - '@atcute/identity' 1819 1591 1820 1592 '@atcute/repo@0.1.0': 1821 1593 dependencies: 1822 1594 '@atcute/car': 5.0.0 1823 1595 '@atcute/cbor': 2.2.8 1824 1596 '@atcute/cid': 2.2.6 1825 - '@atcute/crypto': 2.2.6 1826 - '@atcute/lexicons': 1.2.4 1597 + '@atcute/crypto': 2.3.0 1598 + '@atcute/lexicons': 1.2.5 1827 1599 '@atcute/mst': 0.1.0 1828 - '@atcute/uint8array': 1.0.5 1600 + '@atcute/uint8array': 1.0.6 1829 1601 1830 - '@atcute/tangled@1.0.12': 1602 + '@atcute/tangled@1.0.13': 1831 1603 dependencies: 1832 1604 '@atcute/atproto': 3.1.9 1833 - '@atcute/lexicons': 1.2.4 1605 + '@atcute/lexicons': 1.2.5 1834 1606 1835 1607 '@atcute/tid@1.0.3': {} 1836 1608 1837 - '@atcute/uint8array@1.0.5': {} 1609 + '@atcute/uint8array@1.0.6': {} 1838 1610 1839 - '@atcute/util-fetch@1.0.3': 1611 + '@atcute/util-fetch@1.0.4': 1840 1612 dependencies: 1841 1613 '@badrap/valita': 0.4.6 1842 1614 ··· 1882 1654 dependencies: 1883 1655 '@babel/compat-data': 7.28.5 1884 1656 '@babel/helper-validator-option': 7.27.1 1885 - browserslist: 4.28.0 1657 + browserslist: 4.28.1 1886 1658 lru-cache: 5.1.1 1887 1659 semver: 6.3.1 1888 1660 ··· 1959 1731 dependencies: 1960 1732 '@codemirror/language': 6.11.3 1961 1733 '@codemirror/state': 6.5.2 1962 - '@codemirror/view': 6.38.8 1963 - '@lezer/common': 1.3.0 1734 + '@codemirror/view': 6.39.4 1735 + '@lezer/common': 1.4.0 1964 1736 1965 1737 '@codemirror/commands@6.10.0': 1966 1738 dependencies: 1967 1739 '@codemirror/language': 6.11.3 1968 1740 '@codemirror/state': 6.5.2 1969 - '@codemirror/view': 6.38.8 1970 - '@lezer/common': 1.3.0 1741 + '@codemirror/view': 6.39.4 1742 + '@lezer/common': 1.4.0 1971 1743 1972 1744 '@codemirror/lang-json@6.0.2': 1973 1745 dependencies: ··· 1977 1749 '@codemirror/language@6.11.3': 1978 1750 dependencies: 1979 1751 '@codemirror/state': 6.5.2 1980 - '@codemirror/view': 6.38.8 1981 - '@lezer/common': 1.3.0 1752 + '@codemirror/view': 6.39.4 1753 + '@lezer/common': 1.4.0 1982 1754 '@lezer/highlight': 1.2.3 1983 - '@lezer/lr': 1.4.3 1755 + '@lezer/lr': 1.4.5 1984 1756 style-mod: 4.1.3 1985 1757 1986 1758 '@codemirror/lint@6.9.2': 1987 1759 dependencies: 1988 1760 '@codemirror/state': 6.5.2 1989 - '@codemirror/view': 6.38.8 1761 + '@codemirror/view': 6.39.4 1990 1762 crelt: 1.0.6 1991 1763 1992 1764 '@codemirror/search@6.5.11': 1993 1765 dependencies: 1994 1766 '@codemirror/state': 6.5.2 1995 - '@codemirror/view': 6.38.8 1767 + '@codemirror/view': 6.39.4 1996 1768 crelt: 1.0.6 1997 1769 1998 1770 '@codemirror/state@6.5.2': 1999 1771 dependencies: 2000 1772 '@marijn/find-cluster-break': 1.0.2 2001 1773 2002 - '@codemirror/view@6.38.8': 1774 + '@codemirror/view@6.39.4': 2003 1775 dependencies: 2004 1776 '@codemirror/state': 6.5.2 2005 1777 crelt: 1.0.6 2006 1778 style-mod: 4.1.3 2007 1779 w3c-keyname: 2.2.8 1780 + 1781 + '@cyberalien/svg-utils@1.0.11': 1782 + dependencies: 1783 + '@iconify/types': 2.0.0 2008 1784 2009 1785 '@esbuild/aix-ppc64@0.23.1': 2010 1786 optional: true ··· 2156 1932 '@esbuild/win32-x64@0.25.12': 2157 1933 optional: true 2158 1934 2159 - '@fsegurai/codemirror-theme-basic-dark@6.2.2(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.8)(@lezer/highlight@1.2.3)': 1935 + '@fsegurai/codemirror-theme-basic-dark@6.2.3(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.39.4)(@lezer/highlight@1.2.3)': 2160 1936 dependencies: 2161 1937 '@codemirror/language': 6.11.3 2162 1938 '@codemirror/state': 6.5.2 2163 - '@codemirror/view': 6.38.8 1939 + '@codemirror/view': 6.39.4 2164 1940 '@lezer/highlight': 1.2.3 2165 1941 2166 - '@fsegurai/codemirror-theme-basic-light@6.2.2(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.8)(@lezer/highlight@1.2.3)': 1942 + '@fsegurai/codemirror-theme-basic-light@6.2.3(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.39.4)(@lezer/highlight@1.2.3)': 2167 1943 dependencies: 2168 1944 '@codemirror/language': 6.11.3 2169 1945 '@codemirror/state': 6.5.2 2170 - '@codemirror/view': 6.38.8 1946 + '@codemirror/view': 6.39.4 2171 1947 '@lezer/highlight': 1.2.3 2172 1948 2173 - '@iconify-json/lucide@1.2.75': 1949 + '@iconify-json/lucide@1.2.81': 2174 1950 dependencies: 2175 1951 '@iconify/types': 2.0.0 2176 1952 2177 - '@iconify/tailwind4@1.1.0(tailwindcss@4.1.17)': 1953 + '@iconify/tailwind4@1.2.0(tailwindcss@4.1.18)': 2178 1954 dependencies: 2179 - '@iconify/tools': 4.1.4 1955 + '@iconify/tools': 5.0.0 2180 1956 '@iconify/types': 2.0.0 2181 - '@iconify/utils': 2.3.0 2182 - tailwindcss: 4.1.17 2183 - transitivePeerDependencies: 2184 - - debug 2185 - - supports-color 1957 + '@iconify/utils': 3.1.0 1958 + tailwindcss: 4.1.18 2186 1959 2187 - '@iconify/tools@4.1.4': 1960 + '@iconify/tools@5.0.0': 2188 1961 dependencies: 1962 + '@cyberalien/svg-utils': 1.0.11 2189 1963 '@iconify/types': 2.0.0 2190 - '@iconify/utils': 2.3.0 2191 - '@types/tar': 6.1.13 2192 - axios: 1.13.2 2193 - cheerio: 1.0.0 2194 - domhandler: 5.0.3 2195 - extract-zip: 2.0.1 2196 - local-pkg: 0.5.1 2197 - pathe: 1.1.2 2198 - svgo: 3.3.2 2199 - tar: 6.2.1 2200 - transitivePeerDependencies: 2201 - - debug 2202 - - supports-color 1964 + '@iconify/utils': 3.1.0 1965 + fflate: 0.8.2 1966 + modern-tar: 0.7.2 1967 + pathe: 2.0.3 1968 + svgo: 4.0.0 2203 1969 2204 1970 '@iconify/types@2.0.0': {} 2205 1971 2206 - '@iconify/utils@2.3.0': 1972 + '@iconify/utils@3.1.0': 2207 1973 dependencies: 2208 1974 '@antfu/install-pkg': 1.1.0 2209 - '@antfu/utils': 8.1.1 2210 1975 '@iconify/types': 2.0.0 2211 - debug: 4.4.3 2212 - globals: 15.15.0 2213 - kolorist: 1.8.0 2214 - local-pkg: 1.1.2 2215 1976 mlly: 1.8.0 2216 - transitivePeerDependencies: 2217 - - supports-color 2218 1977 2219 1978 '@jridgewell/gen-mapping@0.3.13': 2220 1979 dependencies: ··· 2237 1996 2238 1997 '@jsr/mary__exif-rm@0.2.2': {} 2239 1998 2240 - '@lezer/common@1.3.0': {} 1999 + '@lezer/common@1.4.0': {} 2241 2000 2242 2001 '@lezer/highlight@1.2.3': 2243 2002 dependencies: 2244 - '@lezer/common': 1.3.0 2003 + '@lezer/common': 1.4.0 2245 2004 2246 2005 '@lezer/json@1.0.3': 2247 2006 dependencies: 2248 - '@lezer/common': 1.3.0 2007 + '@lezer/common': 1.4.0 2249 2008 '@lezer/highlight': 1.2.3 2250 - '@lezer/lr': 1.4.3 2009 + '@lezer/lr': 1.4.5 2251 2010 2252 - '@lezer/lr@1.4.3': 2011 + '@lezer/lr@1.4.5': 2253 2012 dependencies: 2254 - '@lezer/common': 1.3.0 2013 + '@lezer/common': 1.4.0 2255 2014 2256 2015 '@marijn/find-cluster-break@1.0.2': {} 2257 2016 ··· 2339 2098 2340 2099 '@standard-schema/spec@1.0.0': {} 2341 2100 2342 - '@tailwindcss/node@4.1.17': 2101 + '@tailwindcss/node@4.1.18': 2343 2102 dependencies: 2344 2103 '@jridgewell/remapping': 2.3.5 2345 - enhanced-resolve: 5.18.3 2104 + enhanced-resolve: 5.18.4 2346 2105 jiti: 2.6.1 2347 2106 lightningcss: 1.30.2 2348 2107 magic-string: 0.30.21 2349 2108 source-map-js: 1.2.1 2350 - tailwindcss: 4.1.17 2109 + tailwindcss: 4.1.18 2351 2110 2352 - '@tailwindcss/oxide-android-arm64@4.1.17': 2111 + '@tailwindcss/oxide-android-arm64@4.1.18': 2353 2112 optional: true 2354 2113 2355 - '@tailwindcss/oxide-darwin-arm64@4.1.17': 2114 + '@tailwindcss/oxide-darwin-arm64@4.1.18': 2356 2115 optional: true 2357 2116 2358 - '@tailwindcss/oxide-darwin-x64@4.1.17': 2117 + '@tailwindcss/oxide-darwin-x64@4.1.18': 2359 2118 optional: true 2360 2119 2361 - '@tailwindcss/oxide-freebsd-x64@4.1.17': 2120 + '@tailwindcss/oxide-freebsd-x64@4.1.18': 2362 2121 optional: true 2363 2122 2364 - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17': 2123 + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': 2365 2124 optional: true 2366 2125 2367 - '@tailwindcss/oxide-linux-arm64-gnu@4.1.17': 2126 + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': 2368 2127 optional: true 2369 2128 2370 - '@tailwindcss/oxide-linux-arm64-musl@4.1.17': 2129 + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': 2371 2130 optional: true 2372 2131 2373 - '@tailwindcss/oxide-linux-x64-gnu@4.1.17': 2132 + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': 2374 2133 optional: true 2375 2134 2376 - '@tailwindcss/oxide-linux-x64-musl@4.1.17': 2135 + '@tailwindcss/oxide-linux-x64-musl@4.1.18': 2377 2136 optional: true 2378 2137 2379 - '@tailwindcss/oxide-wasm32-wasi@4.1.17': 2138 + '@tailwindcss/oxide-wasm32-wasi@4.1.18': 2380 2139 optional: true 2381 2140 2382 - '@tailwindcss/oxide-win32-arm64-msvc@4.1.17': 2141 + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': 2383 2142 optional: true 2384 2143 2385 - '@tailwindcss/oxide-win32-x64-msvc@4.1.17': 2144 + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': 2386 2145 optional: true 2387 2146 2388 - '@tailwindcss/oxide@4.1.17': 2147 + '@tailwindcss/oxide@4.1.18': 2389 2148 optionalDependencies: 2390 - '@tailwindcss/oxide-android-arm64': 4.1.17 2391 - '@tailwindcss/oxide-darwin-arm64': 4.1.17 2392 - '@tailwindcss/oxide-darwin-x64': 4.1.17 2393 - '@tailwindcss/oxide-freebsd-x64': 4.1.17 2394 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.17 2395 - '@tailwindcss/oxide-linux-arm64-gnu': 4.1.17 2396 - '@tailwindcss/oxide-linux-arm64-musl': 4.1.17 2397 - '@tailwindcss/oxide-linux-x64-gnu': 4.1.17 2398 - '@tailwindcss/oxide-linux-x64-musl': 4.1.17 2399 - '@tailwindcss/oxide-wasm32-wasi': 4.1.17 2400 - '@tailwindcss/oxide-win32-arm64-msvc': 4.1.17 2401 - '@tailwindcss/oxide-win32-x64-msvc': 4.1.17 2149 + '@tailwindcss/oxide-android-arm64': 4.1.18 2150 + '@tailwindcss/oxide-darwin-arm64': 4.1.18 2151 + '@tailwindcss/oxide-darwin-x64': 4.1.18 2152 + '@tailwindcss/oxide-freebsd-x64': 4.1.18 2153 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18 2154 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.18 2155 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.18 2156 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.18 2157 + '@tailwindcss/oxide-linux-x64-musl': 4.1.18 2158 + '@tailwindcss/oxide-wasm32-wasi': 4.1.18 2159 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 2160 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 2402 2161 2403 - '@tailwindcss/vite@4.1.17(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2))': 2162 + '@tailwindcss/vite@4.1.18(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2))': 2404 2163 dependencies: 2405 - '@tailwindcss/node': 4.1.17 2406 - '@tailwindcss/oxide': 4.1.17 2407 - tailwindcss: 4.1.17 2408 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2) 2409 - 2410 - '@trysound/sax@0.2.0': {} 2164 + '@tailwindcss/node': 4.1.18 2165 + '@tailwindcss/oxide': 4.1.18 2166 + tailwindcss: 4.1.18 2167 + vite: 7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2) 2411 2168 2412 2169 '@types/babel__core@7.20.5': 2413 2170 dependencies: ··· 2435 2192 '@types/node@24.10.1': 2436 2193 dependencies: 2437 2194 undici-types: 7.16.0 2438 - 2439 - '@types/tar@6.1.13': 2440 - dependencies: 2441 - '@types/node': 24.10.1 2442 - minipass: 4.2.8 2443 - 2444 - '@types/yauzl@2.10.3': 2445 - dependencies: 2446 - '@types/node': 24.10.1 2447 2195 optional: true 2448 2196 2449 2197 acorn@8.15.0: {} 2450 - 2451 - asynckit@0.4.0: {} 2452 - 2453 - axios@1.13.2: 2454 - dependencies: 2455 - follow-redirects: 1.15.11 2456 - form-data: 4.0.5 2457 - proxy-from-env: 1.1.0 2458 - transitivePeerDependencies: 2459 - - debug 2460 2198 2461 2199 babel-plugin-jsx-dom-expressions@0.40.3(@babel/core@7.28.5): 2462 2200 dependencies: ··· 2474 2212 optionalDependencies: 2475 2213 solid-js: 1.9.10 2476 2214 2477 - baseline-browser-mapping@2.8.31: {} 2215 + baseline-browser-mapping@2.9.7: {} 2478 2216 2479 2217 boolbase@1.0.0: {} 2480 2218 2481 - browserslist@4.28.0: 2219 + browserslist@4.28.1: 2482 2220 dependencies: 2483 - baseline-browser-mapping: 2.8.31 2484 - caniuse-lite: 1.0.30001757 2485 - electron-to-chromium: 1.5.259 2221 + baseline-browser-mapping: 2.9.7 2222 + caniuse-lite: 1.0.30001760 2223 + electron-to-chromium: 1.5.267 2486 2224 node-releases: 2.0.27 2487 - update-browserslist-db: 1.1.4(browserslist@4.28.0) 2225 + update-browserslist-db: 1.2.2(browserslist@4.28.1) 2488 2226 2489 - buffer-crc32@0.2.13: {} 2490 - 2491 - call-bind-apply-helpers@1.0.2: 2492 - dependencies: 2493 - es-errors: 1.3.0 2494 - function-bind: 1.1.2 2495 - 2496 - caniuse-lite@1.0.30001757: {} 2497 - 2498 - cheerio-select@2.1.0: 2499 - dependencies: 2500 - boolbase: 1.0.0 2501 - css-select: 5.2.2 2502 - css-what: 6.2.2 2503 - domelementtype: 2.3.0 2504 - domhandler: 5.0.3 2505 - domutils: 3.2.2 2506 - 2507 - cheerio@1.0.0: 2508 - dependencies: 2509 - cheerio-select: 2.1.0 2510 - dom-serializer: 2.0.0 2511 - domhandler: 5.0.3 2512 - domutils: 3.2.2 2513 - encoding-sniffer: 0.2.1 2514 - htmlparser2: 9.1.0 2515 - parse5: 7.3.0 2516 - parse5-htmlparser2-tree-adapter: 7.1.0 2517 - parse5-parser-stream: 7.1.2 2518 - undici: 6.22.0 2519 - whatwg-mimetype: 4.0.0 2520 - 2521 - chownr@2.0.0: {} 2227 + caniuse-lite@1.0.30001760: {} 2522 2228 2523 2229 codemirror@6.0.2: 2524 2230 dependencies: ··· 2528 2234 '@codemirror/lint': 6.9.2 2529 2235 '@codemirror/search': 6.5.11 2530 2236 '@codemirror/state': 6.5.2 2531 - '@codemirror/view': 6.38.8 2237 + '@codemirror/view': 6.39.4 2532 2238 2533 - combined-stream@1.0.8: 2534 - dependencies: 2535 - delayed-stream: 1.0.0 2536 - 2537 - commander@7.2.0: {} 2239 + commander@11.1.0: {} 2538 2240 2539 2241 confbox@0.1.8: {} 2540 - 2541 - confbox@0.2.2: {} 2542 2242 2543 2243 convert-source-map@2.0.0: {} 2544 2244 ··· 2557 2257 mdn-data: 2.0.28 2558 2258 source-map-js: 1.2.1 2559 2259 2560 - css-tree@2.3.1: 2260 + css-tree@3.1.0: 2561 2261 dependencies: 2562 - mdn-data: 2.0.30 2262 + mdn-data: 2.12.2 2563 2263 source-map-js: 1.2.1 2564 2264 2565 2265 css-what@6.2.2: {} ··· 2573 2273 debug@4.4.3: 2574 2274 dependencies: 2575 2275 ms: 2.1.3 2576 - 2577 - delayed-stream@1.0.0: {} 2578 2276 2579 2277 detect-libc@2.1.2: {} 2580 2278 ··· 2596 2294 domelementtype: 2.3.0 2597 2295 domhandler: 5.0.3 2598 2296 2599 - dunder-proto@1.0.1: 2600 - dependencies: 2601 - call-bind-apply-helpers: 1.0.2 2602 - es-errors: 1.3.0 2603 - gopd: 1.2.0 2297 + electron-to-chromium@1.5.267: {} 2604 2298 2605 - electron-to-chromium@1.5.259: {} 2606 - 2607 - encoding-sniffer@0.2.1: 2608 - dependencies: 2609 - iconv-lite: 0.6.3 2610 - whatwg-encoding: 3.1.1 2611 - 2612 - end-of-stream@1.4.5: 2613 - dependencies: 2614 - once: 1.4.0 2615 - 2616 - enhanced-resolve@5.18.3: 2299 + enhanced-resolve@5.18.4: 2617 2300 dependencies: 2618 2301 graceful-fs: 4.2.11 2619 2302 tapable: 2.3.0 ··· 2621 2304 entities@4.5.0: {} 2622 2305 2623 2306 entities@6.0.1: {} 2624 - 2625 - es-define-property@1.0.1: {} 2626 - 2627 - es-errors@1.3.0: {} 2628 - 2629 - es-object-atoms@1.1.1: 2630 - dependencies: 2631 - es-errors: 1.3.0 2632 - 2633 - es-set-tostringtag@2.1.0: 2634 - dependencies: 2635 - es-errors: 1.3.0 2636 - get-intrinsic: 1.3.0 2637 - has-tostringtag: 1.0.2 2638 - hasown: 2.0.2 2639 2307 2640 2308 esbuild@0.23.1: 2641 2309 optionalDependencies: ··· 2698 2366 2699 2367 esm-env@1.2.2: {} 2700 2368 2701 - exsolve@1.0.8: {} 2702 - 2703 - extract-zip@2.0.1: 2704 - dependencies: 2705 - debug: 4.4.3 2706 - get-stream: 5.2.0 2707 - yauzl: 2.10.0 2708 - optionalDependencies: 2709 - '@types/yauzl': 2.10.3 2710 - transitivePeerDependencies: 2711 - - supports-color 2712 - 2713 - fd-slicer@1.1.0: 2714 - dependencies: 2715 - pend: 1.2.0 2716 - 2717 2369 fdir@6.5.0(picomatch@4.0.3): 2718 2370 optionalDependencies: 2719 2371 picomatch: 4.0.3 2720 2372 2721 - follow-redirects@1.15.11: {} 2722 - 2723 - form-data@4.0.5: 2724 - dependencies: 2725 - asynckit: 0.4.0 2726 - combined-stream: 1.0.8 2727 - es-set-tostringtag: 2.1.0 2728 - hasown: 2.0.2 2729 - mime-types: 2.1.35 2730 - 2731 - fs-minipass@2.1.0: 2732 - dependencies: 2733 - minipass: 3.3.6 2373 + fflate@0.8.2: {} 2734 2374 2735 2375 fsevents@2.3.3: 2736 2376 optional: true 2737 2377 2738 - function-bind@1.1.2: {} 2739 - 2740 2378 gensync@1.0.0-beta.2: {} 2741 2379 2742 - get-intrinsic@1.3.0: 2743 - dependencies: 2744 - call-bind-apply-helpers: 1.0.2 2745 - es-define-property: 1.0.1 2746 - es-errors: 1.3.0 2747 - es-object-atoms: 1.1.1 2748 - function-bind: 1.1.2 2749 - get-proto: 1.0.1 2750 - gopd: 1.2.0 2751 - has-symbols: 1.1.0 2752 - hasown: 2.0.2 2753 - math-intrinsics: 1.1.0 2754 - 2755 - get-proto@1.0.1: 2756 - dependencies: 2757 - dunder-proto: 1.0.1 2758 - es-object-atoms: 1.1.1 2759 - 2760 - get-stream@5.2.0: 2761 - dependencies: 2762 - pump: 3.0.3 2763 - 2764 2380 get-tsconfig@4.13.0: 2765 2381 dependencies: 2766 2382 resolve-pkg-maps: 1.0.0 2767 2383 optional: true 2768 2384 2769 - globals@15.15.0: {} 2770 - 2771 - gopd@1.2.0: {} 2772 - 2773 2385 graceful-fs@4.2.11: {} 2774 2386 2775 - has-symbols@1.1.0: {} 2776 - 2777 - has-tostringtag@1.0.2: 2778 - dependencies: 2779 - has-symbols: 1.1.0 2780 - 2781 - hasown@2.0.2: 2782 - dependencies: 2783 - function-bind: 1.1.2 2784 - 2785 2387 html-entities@2.3.3: {} 2786 2388 2787 - htmlparser2@9.1.0: 2788 - dependencies: 2789 - domelementtype: 2.3.0 2790 - domhandler: 5.0.3 2791 - domutils: 3.2.2 2792 - entities: 4.5.0 2793 - 2794 - iconv-lite@0.6.3: 2795 - dependencies: 2796 - safer-buffer: 2.1.2 2797 - 2798 2389 is-what@4.1.16: {} 2799 2390 2800 2391 jiti@2.6.1: {} ··· 2804 2395 jsesc@3.1.0: {} 2805 2396 2806 2397 json5@2.2.3: {} 2807 - 2808 - kolorist@1.8.0: {} 2809 2398 2810 2399 lightningcss-android-arm64@1.30.2: 2811 2400 optional: true ··· 2856 2445 lightningcss-win32-arm64-msvc: 1.30.2 2857 2446 lightningcss-win32-x64-msvc: 1.30.2 2858 2447 2859 - local-pkg@0.5.1: 2860 - dependencies: 2861 - mlly: 1.8.0 2862 - pkg-types: 1.3.1 2863 - 2864 - local-pkg@1.1.2: 2865 - dependencies: 2866 - mlly: 1.8.0 2867 - pkg-types: 2.3.0 2868 - quansync: 0.2.11 2869 - 2870 2448 lru-cache@5.1.1: 2871 2449 dependencies: 2872 2450 yallist: 3.1.1 ··· 2875 2453 dependencies: 2876 2454 '@jridgewell/sourcemap-codec': 1.5.5 2877 2455 2878 - math-intrinsics@1.1.0: {} 2879 - 2880 2456 mdn-data@2.0.28: {} 2881 2457 2882 - mdn-data@2.0.30: {} 2458 + mdn-data@2.12.2: {} 2883 2459 2884 2460 merge-anything@5.1.7: 2885 2461 dependencies: 2886 2462 is-what: 4.1.16 2887 - 2888 - mime-db@1.52.0: {} 2889 - 2890 - mime-types@2.1.35: 2891 - dependencies: 2892 - mime-db: 1.52.0 2893 - 2894 - minipass@3.3.6: 2895 - dependencies: 2896 - yallist: 4.0.0 2897 - 2898 - minipass@4.2.8: {} 2899 - 2900 - minipass@5.0.0: {} 2901 - 2902 - minizlib@2.1.2: 2903 - dependencies: 2904 - minipass: 3.3.6 2905 - yallist: 4.0.0 2906 - 2907 - mkdirp@1.0.4: {} 2908 2463 2909 2464 mlly@1.8.0: 2910 2465 dependencies: ··· 2913 2468 pkg-types: 1.3.1 2914 2469 ufo: 1.6.1 2915 2470 2471 + modern-tar@0.7.2: {} 2472 + 2916 2473 ms@2.1.3: {} 2917 2474 2918 2475 nanoevents@9.1.0: {} ··· 2927 2484 dependencies: 2928 2485 boolbase: 1.0.0 2929 2486 2930 - once@1.4.0: 2931 - dependencies: 2932 - wrappy: 1.0.2 2933 - 2934 - package-manager-detector@1.5.0: {} 2935 - 2936 - parse5-htmlparser2-tree-adapter@7.1.0: 2937 - dependencies: 2938 - domhandler: 5.0.3 2939 - parse5: 7.3.0 2940 - 2941 - parse5-parser-stream@7.1.2: 2942 - dependencies: 2943 - parse5: 7.3.0 2487 + package-manager-detector@1.6.0: {} 2944 2488 2945 2489 parse5@7.3.0: 2946 2490 dependencies: 2947 2491 entities: 6.0.1 2948 2492 2949 - pathe@1.1.2: {} 2950 - 2951 2493 pathe@2.0.3: {} 2952 - 2953 - pend@1.2.0: {} 2954 2494 2955 2495 picocolors@1.1.1: {} 2956 2496 ··· 2960 2500 dependencies: 2961 2501 confbox: 0.1.8 2962 2502 mlly: 1.8.0 2963 - pathe: 2.0.3 2964 - 2965 - pkg-types@2.3.0: 2966 - dependencies: 2967 - confbox: 0.2.2 2968 - exsolve: 1.0.8 2969 2503 pathe: 2.0.3 2970 2504 2971 2505 postcss@8.5.6: ··· 2974 2508 picocolors: 1.1.1 2975 2509 source-map-js: 1.2.1 2976 2510 2977 - prettier-plugin-organize-imports@4.3.0(prettier@3.6.2)(typescript@5.9.3): 2511 + prettier-plugin-organize-imports@4.3.0(prettier@3.7.4)(typescript@5.9.3): 2978 2512 dependencies: 2979 - prettier: 3.6.2 2513 + prettier: 3.7.4 2980 2514 typescript: 5.9.3 2981 2515 2982 - prettier-plugin-tailwindcss@0.7.1(prettier-plugin-organize-imports@4.3.0(prettier@3.6.2)(typescript@5.9.3))(prettier@3.6.2): 2516 + prettier-plugin-tailwindcss@0.7.2(prettier-plugin-organize-imports@4.3.0(prettier@3.7.4)(typescript@5.9.3))(prettier@3.7.4): 2983 2517 dependencies: 2984 - prettier: 3.6.2 2518 + prettier: 3.7.4 2985 2519 optionalDependencies: 2986 - prettier-plugin-organize-imports: 4.3.0(prettier@3.6.2)(typescript@5.9.3) 2987 - 2988 - prettier@3.6.2: {} 2989 - 2990 - proxy-from-env@1.1.0: {} 2991 - 2992 - pump@3.0.3: 2993 - dependencies: 2994 - end-of-stream: 1.4.5 2995 - once: 1.4.0 2520 + prettier-plugin-organize-imports: 4.3.0(prettier@3.7.4)(typescript@5.9.3) 2996 2521 2997 - quansync@0.2.11: {} 2522 + prettier@3.7.4: {} 2998 2523 2999 2524 resolve-pkg-maps@1.0.0: 3000 2525 optional: true ··· 3027 2552 '@rollup/rollup-win32-x64-msvc': 4.53.3 3028 2553 fsevents: 2.3.3 3029 2554 3030 - safer-buffer@2.1.2: {} 2555 + sax@1.4.3: {} 3031 2556 3032 2557 semver@6.3.1: {} 3033 2558 ··· 3056 2581 3057 2582 style-mod@4.1.3: {} 3058 2583 3059 - svgo@3.3.2: 2584 + svgo@4.0.0: 3060 2585 dependencies: 3061 - '@trysound/sax': 0.2.0 3062 - commander: 7.2.0 2586 + commander: 11.1.0 3063 2587 css-select: 5.2.2 3064 - css-tree: 2.3.1 2588 + css-tree: 3.1.0 3065 2589 css-what: 6.2.2 3066 2590 csso: 5.0.5 3067 2591 picocolors: 1.1.1 2592 + sax: 1.4.3 3068 2593 3069 - tailwindcss@4.1.17: {} 2594 + tailwindcss@4.1.18: {} 3070 2595 3071 2596 tapable@2.3.0: {} 3072 2597 3073 - tar@6.2.1: 3074 - dependencies: 3075 - chownr: 2.0.0 3076 - fs-minipass: 2.1.0 3077 - minipass: 5.0.0 3078 - minizlib: 2.1.2 3079 - mkdirp: 1.0.4 3080 - yallist: 4.0.0 3081 - 3082 2598 tinyexec@1.0.2: {} 3083 2599 3084 2600 tinyglobby@0.2.15: ··· 3098 2614 3099 2615 ufo@1.6.1: {} 3100 2616 3101 - undici-types@7.16.0: {} 2617 + undici-types@7.16.0: 2618 + optional: true 3102 2619 3103 - undici@6.22.0: {} 3104 - 3105 - update-browserslist-db@1.1.4(browserslist@4.28.0): 2620 + update-browserslist-db@1.2.2(browserslist@4.28.1): 3106 2621 dependencies: 3107 - browserslist: 4.28.0 2622 + browserslist: 4.28.1 3108 2623 escalade: 3.2.0 3109 2624 picocolors: 1.1.1 3110 2625 3111 - vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)): 2626 + vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)): 3112 2627 dependencies: 3113 2628 '@babel/core': 7.28.5 3114 2629 '@types/babel__core': 7.20.5 ··· 3116 2631 merge-anything: 5.1.7 3117 2632 solid-js: 1.9.10 3118 2633 solid-refresh: 0.6.3(solid-js@1.9.10) 3119 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2) 3120 - vitefu: 1.1.1(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)) 2634 + vite: 7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2) 2635 + vitefu: 1.1.1(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)) 3121 2636 transitivePeerDependencies: 3122 2637 - supports-color 3123 2638 3124 - vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2): 2639 + vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2): 3125 2640 dependencies: 3126 2641 esbuild: 0.25.12 3127 2642 fdir: 6.5.0(picomatch@4.0.3) ··· 3136 2651 lightningcss: 1.30.2 3137 2652 tsx: 4.19.2 3138 2653 3139 - vitefu@1.1.1(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)): 2654 + vitefu@1.1.1(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)): 3140 2655 optionalDependencies: 3141 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2) 2656 + vite: 7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2) 3142 2657 3143 2658 w3c-keyname@2.2.8: {} 3144 2659 3145 - whatwg-encoding@3.1.1: 3146 - dependencies: 3147 - iconv-lite: 0.6.3 3148 - 3149 - whatwg-mimetype@4.0.0: {} 3150 - 3151 - wrappy@1.0.2: {} 3152 - 3153 2660 yallist@3.1.1: {} 3154 - 3155 - yallist@4.0.0: {} 3156 - 3157 - yauzl@2.10.0: 3158 - dependencies: 3159 - buffer-crc32: 0.2.13 3160 - fd-slicer: 1.1.0 3161 2661 3162 2662 yocto-queue@1.2.2: {}
public/favicon.ico

This is a binary file and will not be displayed.

public/fonts/Figtree[wght].woff2

This is a binary file and will not be displayed.

+1 -1
public/oauth-client-metadata.json
··· 4 4 "client_uri": "https://pdsls.dev", 5 5 "logo_uri": "https://pdsls.dev/favicon.ico", 6 6 "redirect_uris": ["https://pdsls.dev/"], 7 - "scope": "atproto transition:generic", 7 + "scope": "atproto repo:*?action=create repo:*?action=update repo:*?action=delete blob:*/*", 8 8 "grant_types": ["authorization_code", "refresh_token"], 9 9 "response_types": ["code"], 10 10 "token_endpoint_auth_method": "none",
+194
src/auth/account.tsx
··· 1 + import { Did } from "@atcute/lexicons"; 2 + import { deleteStoredSession, getSession, OAuthUserAgent } from "@atcute/oauth-browser-client"; 3 + import { A } from "@solidjs/router"; 4 + import { createSignal, For, onMount, Show } from "solid-js"; 5 + import { createStore, produce } from "solid-js/store"; 6 + import { ActionMenu, DropdownMenu, MenuProvider, NavMenu } from "../components/dropdown.jsx"; 7 + import { Modal } from "../components/modal.jsx"; 8 + import { Login } from "./login.jsx"; 9 + import { useOAuthScopeFlow } from "./scope-flow.js"; 10 + import { ScopeSelector } from "./scope-selector.jsx"; 11 + import { parseScopeString } from "./scope-utils.js"; 12 + import { 13 + getAvatar, 14 + loadHandleForSession, 15 + loadSessionsFromStorage, 16 + resumeSession, 17 + retrieveSession, 18 + saveSessionToStorage, 19 + } from "./session-manager.js"; 20 + import { agent, sessions, setAgent, setSessions } from "./state.js"; 21 + 22 + const AccountDropdown = (props: { did: Did; onEditPermissions: (did: Did) => void }) => { 23 + const removeSession = async (did: Did) => { 24 + const currentSession = agent()?.sub; 25 + try { 26 + const session = await getSession(did, { allowStale: true }); 27 + const agent = new OAuthUserAgent(session); 28 + await agent.signOut(); 29 + } catch { 30 + deleteStoredSession(did); 31 + } 32 + setSessions( 33 + produce((accs) => { 34 + delete accs[did]; 35 + }), 36 + ); 37 + saveSessionToStorage(sessions); 38 + if (currentSession === did) setAgent(undefined); 39 + }; 40 + 41 + return ( 42 + <MenuProvider> 43 + <DropdownMenu icon="lucide--ellipsis" buttonClass="rounded-md p-2"> 44 + <NavMenu 45 + href={`/at://${props.did}`} 46 + label={agent()?.sub === props.did ? "Go to repo (g)" : "Go to repo"} 47 + icon="lucide--user-round" 48 + /> 49 + <ActionMenu 50 + icon="lucide--settings" 51 + label="Edit permissions" 52 + onClick={() => props.onEditPermissions(props.did)} 53 + /> 54 + <ActionMenu 55 + icon="lucide--x" 56 + label="Remove account" 57 + onClick={() => removeSession(props.did)} 58 + /> 59 + </DropdownMenu> 60 + </MenuProvider> 61 + ); 62 + }; 63 + 64 + export const AccountManager = () => { 65 + const [openManager, setOpenManager] = createSignal(false); 66 + const [avatars, setAvatars] = createStore<Record<Did, string>>(); 67 + const [showingAddAccount, setShowingAddAccount] = createSignal(false); 68 + 69 + const getThumbnailUrl = (avatarUrl: string) => { 70 + return avatarUrl.replace("img/avatar/", "img/avatar_thumbnail/"); 71 + }; 72 + 73 + const scopeFlow = useOAuthScopeFlow({ 74 + beforeRedirect: (account) => resumeSession(account as Did), 75 + }); 76 + 77 + const handleAccountClick = async (did: Did) => { 78 + try { 79 + await resumeSession(did); 80 + } catch { 81 + scopeFlow.initiate(did); 82 + } 83 + }; 84 + 85 + onMount(async () => { 86 + try { 87 + await retrieveSession(); 88 + } catch {} 89 + 90 + const storedSessions = loadSessionsFromStorage(); 91 + if (storedSessions) { 92 + const sessionDids = Object.keys(storedSessions) as Did[]; 93 + sessionDids.forEach(async (did) => { 94 + await loadHandleForSession(did, storedSessions); 95 + }); 96 + sessionDids.forEach(async (did) => { 97 + const avatar = await getAvatar(did); 98 + if (avatar) setAvatars(did, avatar); 99 + }); 100 + } 101 + }); 102 + 103 + return ( 104 + <> 105 + <Modal 106 + open={openManager()} 107 + onClose={() => { 108 + setOpenManager(false); 109 + setShowingAddAccount(false); 110 + scopeFlow.cancel(); 111 + }} 112 + > 113 + <div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-18 left-[50%] w-88 -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-opacity duration-200 dark:border-neutral-700 starting:opacity-0"> 114 + <Show when={!scopeFlow.showScopeSelector() && !showingAddAccount()}> 115 + <div class="mb-2 px-1 font-semibold"> 116 + <span>Manage accounts</span> 117 + </div> 118 + <div class="mb-3 max-h-80 overflow-y-auto md:max-h-100"> 119 + <For each={Object.keys(sessions)}> 120 + {(did) => ( 121 + <div class="flex w-full items-center justify-between"> 122 + <A 123 + href={`/at://${did}`} 124 + onClick={() => setOpenManager(false)} 125 + class="flex items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 126 + > 127 + <Show 128 + when={avatars[did as Did]} 129 + fallback={<span class="iconify lucide--user-round m-0.5 size-5"></span>} 130 + > 131 + <img 132 + src={getThumbnailUrl(avatars[did as Did])} 133 + class="size-6 rounded-full" 134 + /> 135 + </Show> 136 + </A> 137 + <button 138 + class="flex grow items-center justify-between gap-1 truncate rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 139 + onclick={() => handleAccountClick(did as Did)} 140 + > 141 + <span class="truncate">{sessions[did]?.handle || did}</span> 142 + <Show when={did === agent()?.sub && sessions[did].signedIn}> 143 + <span class="iconify lucide--check shrink-0 text-green-500 dark:text-green-400"></span> 144 + </Show> 145 + <Show when={!sessions[did].signedIn}> 146 + <span class="iconify lucide--circle-alert shrink-0 text-red-500 dark:text-red-400"></span> 147 + </Show> 148 + </button> 149 + <AccountDropdown 150 + did={did as Did} 151 + onEditPermissions={(accountDid) => scopeFlow.initiateWithRedirect(accountDid)} 152 + /> 153 + </div> 154 + )} 155 + </For> 156 + </div> 157 + <button 158 + onclick={() => setShowingAddAccount(true)} 159 + class="flex w-full items-center justify-center gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-100 px-3 py-2 hover:bg-neutral-200 active:bg-neutral-300 dark:border-neutral-600 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 160 + > 161 + <span class="iconify lucide--user-plus"></span> 162 + <span>Add account</span> 163 + </button> 164 + </Show> 165 + 166 + <Show when={showingAddAccount() && !scopeFlow.showScopeSelector()}> 167 + <Login onCancel={() => setShowingAddAccount(false)} /> 168 + </Show> 169 + 170 + <Show when={scopeFlow.showScopeSelector()}> 171 + <ScopeSelector 172 + initialScopes={parseScopeString( 173 + sessions[scopeFlow.pendingAccount()]?.grantedScopes || "", 174 + )} 175 + onConfirm={scopeFlow.complete} 176 + onCancel={() => { 177 + scopeFlow.cancel(); 178 + setShowingAddAccount(false); 179 + }} 180 + /> 181 + </Show> 182 + </div> 183 + </Modal> 184 + <button 185 + onclick={() => setOpenManager(true)} 186 + class={`flex items-center rounded-lg ${agent() && avatars[agent()!.sub] ? "p-1.25" : "p-1.5"} hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600`} 187 + > 188 + {agent() && avatars[agent()!.sub] ? 189 + <img src={getThumbnailUrl(avatars[agent()!.sub])} class="size-5 rounded-full" /> 190 + : <span class="iconify lucide--circle-user-round text-lg"></span>} 191 + </button> 192 + </> 193 + ); 194 + };
+88
src/auth/login.tsx
··· 1 + import { createSignal, Show } from "solid-js"; 2 + import "./oauth-config"; 3 + import { useOAuthScopeFlow } from "./scope-flow"; 4 + import { ScopeSelector } from "./scope-selector"; 5 + 6 + interface LoginProps { 7 + onCancel?: () => void; 8 + } 9 + 10 + export const Login = (props: LoginProps) => { 11 + const [notice, setNotice] = createSignal(""); 12 + const [loginInput, setLoginInput] = createSignal(""); 13 + 14 + const scopeFlow = useOAuthScopeFlow({ 15 + onError: (e) => setNotice(`${e}`), 16 + onRedirecting: () => { 17 + setNotice(`Contacting your data server...`); 18 + setTimeout(() => setNotice(`Redirecting...`), 0); 19 + }, 20 + }); 21 + 22 + const initiateLogin = (handle: string) => { 23 + setNotice(""); 24 + scopeFlow.initiate(handle); 25 + }; 26 + 27 + const handleCancel = () => { 28 + scopeFlow.cancel(); 29 + setLoginInput(""); 30 + setNotice(""); 31 + props.onCancel?.(); 32 + }; 33 + 34 + return ( 35 + <div class="flex flex-col gap-y-2 px-1"> 36 + <Show when={!scopeFlow.showScopeSelector()}> 37 + <Show when={props.onCancel}> 38 + <div class="mb-1 flex items-center gap-2"> 39 + <button 40 + onclick={handleCancel} 41 + class="flex items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 42 + > 43 + <span class="iconify lucide--arrow-left"></span> 44 + </button> 45 + <div class="font-semibold">Add account</div> 46 + </div> 47 + </Show> 48 + <form class="flex flex-col gap-2" onsubmit={(e) => e.preventDefault()}> 49 + <label for="username" class="hidden"> 50 + Add account 51 + </label> 52 + <div class="dark:bg-dark-100 dark:inset-shadow-dark-200 flex grow items-center gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 inset-shadow-xs focus-within:outline-[1px] focus-within:outline-neutral-600 dark:border-neutral-600 dark:focus-within:outline-neutral-400"> 53 + <label 54 + for="username" 55 + class="iconify lucide--user-round-plus shrink-0 text-neutral-500 dark:text-neutral-400" 56 + ></label> 57 + <input 58 + type="text" 59 + spellcheck={false} 60 + placeholder="user.bsky.social" 61 + id="username" 62 + name="username" 63 + autocomplete="username" 64 + autofocus 65 + aria-label="Your AT Protocol handle" 66 + class="grow py-1 select-none placeholder:text-sm focus:outline-none" 67 + onInput={(e) => setLoginInput(e.currentTarget.value)} 68 + /> 69 + </div> 70 + <button 71 + onclick={() => initiateLogin(loginInput())} 72 + class="grow rounded-lg border-[0.5px] border-neutral-300 bg-neutral-100 px-3 py-2 hover:bg-neutral-200 active:bg-neutral-300 dark:border-neutral-600 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 73 + > 74 + Continue 75 + </button> 76 + </form> 77 + </Show> 78 + 79 + <Show when={scopeFlow.showScopeSelector()}> 80 + <ScopeSelector onConfirm={scopeFlow.complete} onCancel={handleCancel} /> 81 + </Show> 82 + 83 + <Show when={notice()}> 84 + <div class="text-sm">{notice()}</div> 85 + </Show> 86 + </div> 87 + ); 88 + };
+13
src/auth/oauth-config.ts
··· 1 + import { configureOAuth, defaultIdentityResolver } from "@atcute/oauth-browser-client"; 2 + import { didDocumentResolver, handleResolver } from "../utils/api"; 3 + 4 + configureOAuth({ 5 + metadata: { 6 + client_id: import.meta.env.VITE_OAUTH_CLIENT_ID, 7 + redirect_uri: import.meta.env.VITE_OAUTH_REDIRECT_URL, 8 + }, 9 + identityResolver: defaultIdentityResolver({ 10 + handleResolver: handleResolver, 11 + didDocumentResolver: didDocumentResolver, 12 + }), 13 + });
+77
src/auth/scope-flow.ts
··· 1 + import { isDid, isHandle } from "@atcute/lexicons/syntax"; 2 + import { createAuthorizationUrl } from "@atcute/oauth-browser-client"; 3 + import { createSignal } from "solid-js"; 4 + 5 + interface UseOAuthScopeFlowOptions { 6 + onError?: (error: unknown) => void; 7 + onRedirecting?: () => void; 8 + beforeRedirect?: (account: string) => Promise<void>; 9 + } 10 + 11 + export const useOAuthScopeFlow = (options: UseOAuthScopeFlowOptions = {}) => { 12 + const [showScopeSelector, setShowScopeSelector] = createSignal(false); 13 + const [pendingAccount, setPendingAccount] = createSignal(""); 14 + const [shouldForceRedirect, setShouldForceRedirect] = createSignal(false); 15 + 16 + const initiate = (account: string) => { 17 + if (!account) return; 18 + setPendingAccount(account); 19 + setShouldForceRedirect(false); 20 + setShowScopeSelector(true); 21 + }; 22 + 23 + const initiateWithRedirect = (account: string) => { 24 + if (!account) return; 25 + setPendingAccount(account); 26 + setShouldForceRedirect(true); 27 + setShowScopeSelector(true); 28 + }; 29 + 30 + const complete = async (scopeString: string, scopeIds: string) => { 31 + try { 32 + const account = pendingAccount(); 33 + 34 + if (options.beforeRedirect && !shouldForceRedirect()) { 35 + try { 36 + await options.beforeRedirect(account); 37 + setShowScopeSelector(false); 38 + return; 39 + } catch {} 40 + } 41 + 42 + localStorage.setItem("pendingScopes", scopeIds); 43 + 44 + options.onRedirecting?.(); 45 + 46 + const authUrl = await createAuthorizationUrl({ 47 + scope: scopeString, 48 + target: 49 + isHandle(account) || isDid(account) ? 50 + { type: "account", identifier: account } 51 + : { type: "pds", serviceUrl: account }, 52 + }); 53 + 54 + await new Promise((resolve) => setTimeout(resolve, 250)); 55 + location.assign(authUrl); 56 + } catch (e) { 57 + console.error(e); 58 + options.onError?.(e); 59 + setShowScopeSelector(false); 60 + } 61 + }; 62 + 63 + const cancel = () => { 64 + setShowScopeSelector(false); 65 + setPendingAccount(""); 66 + setShouldForceRedirect(false); 67 + }; 68 + 69 + return { 70 + showScopeSelector, 71 + pendingAccount, 72 + initiate, 73 + initiateWithRedirect, 74 + complete, 75 + cancel, 76 + }; 77 + };
+86
src/auth/scope-selector.tsx
··· 1 + import { createSignal, For } from "solid-js"; 2 + import { buildScopeString, GRANULAR_SCOPES, scopeIdsToString } from "./scope-utils"; 3 + 4 + interface ScopeSelectorProps { 5 + onConfirm: (scopeString: string, scopeIds: string) => void; 6 + onCancel: () => void; 7 + initialScopes?: Set<string>; 8 + } 9 + 10 + export const ScopeSelector = (props: ScopeSelectorProps) => { 11 + const [selectedScopes, setSelectedScopes] = createSignal<Set<string>>( 12 + props.initialScopes || new Set(["create", "update", "delete", "blob"]), 13 + ); 14 + 15 + const isBlobDisabled = () => { 16 + const scopes = selectedScopes(); 17 + return !scopes.has("create") && !scopes.has("update"); 18 + }; 19 + 20 + const toggleScope = (scopeId: string) => { 21 + setSelectedScopes((prev) => { 22 + const newSet = new Set(prev); 23 + if (newSet.has(scopeId)) { 24 + newSet.delete(scopeId); 25 + if ( 26 + (scopeId === "create" || scopeId === "update") && 27 + !newSet.has("create") && 28 + !newSet.has("update") 29 + ) { 30 + newSet.delete("blob"); 31 + } 32 + } else { 33 + newSet.add(scopeId); 34 + } 35 + return newSet; 36 + }); 37 + }; 38 + 39 + const handleConfirm = () => { 40 + const scopes = selectedScopes(); 41 + const scopeString = buildScopeString(scopes); 42 + const scopeIds = scopeIdsToString(scopes); 43 + props.onConfirm(scopeString, scopeIds); 44 + }; 45 + 46 + return ( 47 + <div class="flex flex-col gap-y-2"> 48 + <div class="mb-1 flex items-center gap-2"> 49 + <button 50 + onclick={props.onCancel} 51 + class="flex items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 52 + > 53 + <span class="iconify lucide--arrow-left"></span> 54 + </button> 55 + <div class="font-semibold">Select permissions</div> 56 + </div> 57 + <div class="flex flex-col gap-y-2 px-1"> 58 + <For each={GRANULAR_SCOPES}> 59 + {(scope) => ( 60 + <div 61 + class="flex items-center gap-2" 62 + classList={{ "opacity-50": scope.id === "blob" && isBlobDisabled() }} 63 + > 64 + <input 65 + id={`scope-${scope.id}`} 66 + type="checkbox" 67 + checked={selectedScopes().has(scope.id)} 68 + disabled={scope.id === "blob" && isBlobDisabled()} 69 + onChange={() => toggleScope(scope.id)} 70 + /> 71 + <label for={`scope-${scope.id}`} class="flex grow items-center gap-2 select-none"> 72 + <span>{scope.label}</span> 73 + </label> 74 + </div> 75 + )} 76 + </For> 77 + </div> 78 + <button 79 + onclick={handleConfirm} 80 + class="mt-2 grow rounded-lg border-[0.5px] border-neutral-300 bg-neutral-100 px-3 py-2 hover:bg-neutral-200 active:bg-neutral-300 dark:border-neutral-600 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 81 + > 82 + Continue 83 + </button> 84 + </div> 85 + ); 86 + };
+53
src/auth/scope-utils.ts
··· 1 + import { agent, sessions } from "./state"; 2 + 3 + export const GRANULAR_SCOPES = [ 4 + { 5 + id: "create", 6 + scope: "repo:*?action=create", 7 + label: "Create records", 8 + }, 9 + { 10 + id: "update", 11 + scope: "repo:*?action=update", 12 + label: "Update records", 13 + }, 14 + { 15 + id: "delete", 16 + scope: "repo:*?action=delete", 17 + label: "Delete records", 18 + }, 19 + { 20 + id: "blob", 21 + scope: "blob:*/*", 22 + label: "Upload blobs", 23 + }, 24 + ]; 25 + 26 + export const BASE_SCOPES = ["atproto"]; 27 + 28 + export const buildScopeString = (selected: Set<string>): string => { 29 + const granular = GRANULAR_SCOPES.filter((s) => selected.has(s.id)).map((s) => s.scope); 30 + return [...BASE_SCOPES, ...granular].join(" "); 31 + }; 32 + 33 + export const scopeIdsToString = (scopeIds: Set<string>): string => { 34 + return ["atproto", ...Array.from(scopeIds)].join(","); 35 + }; 36 + 37 + export const parseScopeString = (scopeIdsString: string): Set<string> => { 38 + if (!scopeIdsString) return new Set(); 39 + const ids = scopeIdsString.split(",").filter(Boolean); 40 + return new Set(ids.filter((id) => id !== "atproto")); 41 + }; 42 + 43 + export const hasScope = (grantedScopes: string | undefined, scopeId: string): boolean => { 44 + if (!grantedScopes) return false; 45 + return grantedScopes.split(",").includes(scopeId); 46 + }; 47 + 48 + export const hasUserScope = (scopeId: string): boolean => { 49 + if (!agent()) return false; 50 + const grantedScopes = sessions[agent()!.sub]?.grantedScopes; 51 + if (!grantedScopes) return true; 52 + return hasScope(grantedScopes, scopeId); 53 + };
+95
src/auth/session-manager.ts
··· 1 + import { Client, simpleFetchHandler } from "@atcute/client"; 2 + import { Did } from "@atcute/lexicons"; 3 + import { 4 + finalizeAuthorization, 5 + getSession, 6 + OAuthUserAgent, 7 + type Session, 8 + } from "@atcute/oauth-browser-client"; 9 + import { resolveDidDoc } from "../utils/api"; 10 + import { Sessions, setAgent, setSessions } from "./state"; 11 + 12 + export const saveSessionToStorage = (sessions: Sessions) => { 13 + localStorage.setItem("sessions", JSON.stringify(sessions)); 14 + }; 15 + 16 + export const loadSessionsFromStorage = (): Sessions | null => { 17 + const localSessions = localStorage.getItem("sessions"); 18 + return localSessions ? JSON.parse(localSessions) : null; 19 + }; 20 + 21 + export const getAvatar = async (did: Did): Promise<string | undefined> => { 22 + const rpc = new Client({ 23 + handler: simpleFetchHandler({ service: "https://public.api.bsky.app" }), 24 + }); 25 + const res = await rpc.get("app.bsky.actor.getProfile", { params: { actor: did } }); 26 + if (res.ok) { 27 + return res.data.avatar; 28 + } 29 + return undefined; 30 + }; 31 + 32 + export const loadHandleForSession = async (did: Did, storedSessions: Sessions) => { 33 + const doc = await resolveDidDoc(did); 34 + const alias = doc.alsoKnownAs?.find((alias) => alias.startsWith("at://")); 35 + if (alias) { 36 + setSessions(did, { 37 + signedIn: storedSessions[did].signedIn, 38 + handle: alias.replace("at://", ""), 39 + grantedScopes: storedSessions[did].grantedScopes, 40 + }); 41 + } 42 + }; 43 + 44 + export const retrieveSession = async (): Promise<void> => { 45 + const init = async (): Promise<Session | undefined> => { 46 + const params = new URLSearchParams(location.hash.slice(1)); 47 + 48 + if (params.has("state") && (params.has("code") || params.has("error"))) { 49 + history.replaceState(null, "", location.pathname + location.search); 50 + 51 + const auth = await finalizeAuthorization(params); 52 + const did = auth.session.info.sub; 53 + 54 + localStorage.setItem("lastSignedIn", did); 55 + 56 + const grantedScopes = localStorage.getItem("pendingScopes") || "atproto"; 57 + localStorage.removeItem("pendingScopes"); 58 + 59 + const sessions = loadSessionsFromStorage(); 60 + const newSessions: Sessions = sessions || {}; 61 + newSessions[did] = { signedIn: true, grantedScopes }; 62 + saveSessionToStorage(newSessions); 63 + return auth.session; 64 + } else { 65 + const lastSignedIn = localStorage.getItem("lastSignedIn"); 66 + 67 + if (lastSignedIn) { 68 + const sessions = loadSessionsFromStorage(); 69 + const newSessions: Sessions = sessions || {}; 70 + try { 71 + const session = await getSession(lastSignedIn as Did); 72 + const rpc = new Client({ handler: new OAuthUserAgent(session) }); 73 + const res = await rpc.get("com.atproto.server.getSession"); 74 + newSessions[lastSignedIn].signedIn = true; 75 + saveSessionToStorage(newSessions); 76 + if (!res.ok) throw res.data.error; 77 + return session; 78 + } catch (err) { 79 + newSessions[lastSignedIn].signedIn = false; 80 + saveSessionToStorage(newSessions); 81 + throw err; 82 + } 83 + } 84 + } 85 + }; 86 + 87 + const session = await init(); 88 + 89 + if (session) setAgent(new OAuthUserAgent(session)); 90 + }; 91 + 92 + export const resumeSession = async (did: Did): Promise<void> => { 93 + localStorage.setItem("lastSignedIn", did); 94 + await retrieveSession(); 95 + };
+14
src/auth/state.ts
··· 1 + import { OAuthUserAgent } from "@atcute/oauth-browser-client"; 2 + import { createSignal } from "solid-js"; 3 + import { createStore } from "solid-js/store"; 4 + 5 + export type Account = { 6 + signedIn: boolean; 7 + handle?: string; 8 + grantedScopes?: string; 9 + }; 10 + 11 + export type Sessions = Record<string, Account>; 12 + 13 + export const [agent, setAgent] = createSignal<OAuthUserAgent | undefined>(); 14 + export const [sessions, setSessions] = createStore<Sessions>();
-159
src/components/account.tsx
··· 1 - import { Client, CredentialManager } from "@atcute/client"; 2 - import { Did } from "@atcute/lexicons"; 3 - import { 4 - createAuthorizationUrl, 5 - deleteStoredSession, 6 - getSession, 7 - OAuthUserAgent, 8 - } from "@atcute/oauth-browser-client"; 9 - import { A } from "@solidjs/router"; 10 - import { createSignal, For, onMount, Show } from "solid-js"; 11 - import { createStore, produce } from "solid-js/store"; 12 - import { resolveDidDoc } from "../utils/api.js"; 13 - import { agent, Login, retrieveSession, Sessions, setAgent } from "./login.jsx"; 14 - import { Modal } from "./modal.jsx"; 15 - 16 - export const [sessions, setSessions] = createStore<Sessions>(); 17 - 18 - export const AccountManager = () => { 19 - const [openManager, setOpenManager] = createSignal(false); 20 - const [avatars, setAvatars] = createStore<Record<Did, string>>(); 21 - 22 - onMount(async () => { 23 - try { 24 - await retrieveSession(); 25 - } catch {} 26 - 27 - const localSessions = localStorage.getItem("sessions"); 28 - if (localSessions) { 29 - const storedSessions: Sessions = JSON.parse(localSessions); 30 - const sessionDids = Object.keys(storedSessions) as Did[]; 31 - sessionDids.forEach(async (did) => { 32 - const doc = await resolveDidDoc(did); 33 - const alias = doc.alsoKnownAs?.find((alias) => alias.startsWith("at://")); 34 - if (alias) { 35 - setSessions(did, { 36 - signedIn: storedSessions[did].signedIn, 37 - handle: alias.replace("at://", ""), 38 - }); 39 - } 40 - }); 41 - sessionDids.forEach(async (did) => { 42 - const avatar = await getAvatar(did); 43 - if (avatar) setAvatars(did, avatar); 44 - }); 45 - } 46 - }); 47 - 48 - const resumeSession = async (did: Did) => { 49 - try { 50 - localStorage.setItem("lastSignedIn", did); 51 - await retrieveSession(); 52 - } catch { 53 - const authUrl = await createAuthorizationUrl({ 54 - scope: import.meta.env.VITE_OAUTH_SCOPE, 55 - target: { type: "account", identifier: did }, 56 - }); 57 - 58 - await new Promise((resolve) => setTimeout(resolve, 250)); 59 - 60 - location.assign(authUrl); 61 - } 62 - }; 63 - 64 - const removeSession = async (did: Did) => { 65 - const currentSession = agent()?.sub; 66 - try { 67 - const session = await getSession(did, { allowStale: true }); 68 - const agent = new OAuthUserAgent(session); 69 - await agent.signOut(); 70 - } catch { 71 - deleteStoredSession(did); 72 - } 73 - setSessions( 74 - produce((accs) => { 75 - delete accs[did]; 76 - }), 77 - ); 78 - localStorage.setItem("sessions", JSON.stringify(sessions)); 79 - if (currentSession === did) setAgent(undefined); 80 - }; 81 - 82 - const getAvatar = async (did: Did) => { 83 - const rpc = new Client({ 84 - handler: new CredentialManager({ service: "https://public.api.bsky.app" }), 85 - }); 86 - const res = await rpc.get("app.bsky.actor.getProfile", { params: { actor: did } }); 87 - if (res.ok) { 88 - return res.data.avatar; 89 - } 90 - return undefined; 91 - }; 92 - 93 - return ( 94 - <> 95 - <Modal open={openManager()} onClose={() => setOpenManager(false)}> 96 - <div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-18 left-[50%] w-88 -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-opacity duration-200 dark:border-neutral-700 starting:opacity-0"> 97 - <div class="mb-2 px-1 font-semibold"> 98 - <span>Manage accounts</span> 99 - </div> 100 - <div class="mb-3 max-h-80 overflow-y-auto md:max-h-100"> 101 - <For each={Object.keys(sessions)}> 102 - {(did) => ( 103 - <div class="flex items-center"> 104 - <button 105 - class="flex w-full items-center justify-between gap-1 truncate rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 106 - onclick={() => resumeSession(did as Did)} 107 - > 108 - <span class="flex items-center gap-2 truncate"> 109 - <Show when={avatars[did as Did]}> 110 - <img 111 - src={avatars[did as Did].replace("img/avatar/", "img/avatar_thumbnail/")} 112 - class="size-6 rounded-full" 113 - /> 114 - </Show> 115 - <span class="truncate"> 116 - {sessions[did]?.handle ? sessions[did].handle : did} 117 - </span> 118 - </span> 119 - <Show when={did === agent()?.sub && sessions[did].signedIn}> 120 - <span class="iconify lucide--check shrink-0 text-green-500 dark:text-green-400"></span> 121 - </Show> 122 - <Show when={!sessions[did].signedIn}> 123 - <span class="iconify lucide--circle-alert shrink-0 text-red-500 dark:text-red-400"></span> 124 - </Show> 125 - </button> 126 - <A 127 - href={`/at://${did}`} 128 - onClick={() => setOpenManager(false)} 129 - class="flex items-center rounded-lg p-2 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 130 - > 131 - <span class="iconify lucide--user-round"></span> 132 - </A> 133 - <button 134 - onclick={() => removeSession(did as Did)} 135 - class="flex items-center rounded-lg p-2 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 136 - > 137 - <span class="iconify lucide--x"></span> 138 - </button> 139 - </div> 140 - )} 141 - </For> 142 - </div> 143 - <Login /> 144 - </div> 145 - </Modal> 146 - <button 147 - onclick={() => setOpenManager(true)} 148 - class={`flex items-center rounded-lg ${agent() && avatars[agent()!.sub] ? "p-1.25" : "p-1.5"} hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600`} 149 - > 150 - {agent() && avatars[agent()!.sub] ? 151 - <img 152 - src={avatars[agent()!.sub].replace("img/avatar/", "img/avatar_thumbnail/")} 153 - class="size-5 rounded-full" 154 - /> 155 - : <span class="iconify lucide--circle-user-round text-lg"></span>} 156 - </button> 157 - </> 158 - ); 159 - };
+109
src/components/create/file-upload.tsx
··· 1 + import { Client } from "@atcute/client"; 2 + import { remove } from "@mary/exif-rm"; 3 + import { createSignal, onCleanup, Show } from "solid-js"; 4 + import { agent } from "../../auth/state"; 5 + import { Button } from "../button.jsx"; 6 + import { TextInput } from "../text-input.jsx"; 7 + import { editorInstance } from "./state"; 8 + 9 + export const FileUpload = (props: { 10 + file: File; 11 + blobInput: HTMLInputElement; 12 + onClose: () => void; 13 + }) => { 14 + const [uploading, setUploading] = createSignal(false); 15 + const [error, setError] = createSignal(""); 16 + 17 + onCleanup(() => (props.blobInput.value = "")); 18 + 19 + const formatFileSize = (bytes: number) => { 20 + if (bytes === 0) return "0 Bytes"; 21 + const k = 1024; 22 + const sizes = ["Bytes", "KB", "MB", "GB"]; 23 + const i = Math.floor(Math.log(bytes) / Math.log(k)); 24 + return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i]; 25 + }; 26 + 27 + const uploadBlob = async () => { 28 + let blob: Blob; 29 + 30 + const mimetype = (document.getElementById("mimetype") as HTMLInputElement)?.value; 31 + (document.getElementById("mimetype") as HTMLInputElement).value = ""; 32 + if (mimetype) blob = new Blob([props.file], { type: mimetype }); 33 + else blob = props.file; 34 + 35 + if ((document.getElementById("exif-rm") as HTMLInputElement).checked) { 36 + const exifRemoved = remove(new Uint8Array(await blob.arrayBuffer())); 37 + if (exifRemoved !== null) blob = new Blob([exifRemoved], { type: blob.type }); 38 + } 39 + 40 + const rpc = new Client({ handler: agent()! }); 41 + setUploading(true); 42 + const res = await rpc.post("com.atproto.repo.uploadBlob", { 43 + input: blob, 44 + }); 45 + setUploading(false); 46 + if (!res.ok) { 47 + setError(res.data.error); 48 + return; 49 + } 50 + editorInstance.view.dispatch({ 51 + changes: { 52 + from: editorInstance.view.state.selection.main.head, 53 + insert: JSON.stringify(res.data.blob, null, 2), 54 + }, 55 + }); 56 + props.onClose(); 57 + }; 58 + 59 + return ( 60 + <div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-70 left-[50%] w-[20rem] -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-opacity duration-200 dark:border-neutral-700 starting:opacity-0"> 61 + <h2 class="mb-2 font-semibold">Upload blob</h2> 62 + <div class="flex flex-col gap-2 text-sm"> 63 + <div class="flex flex-col gap-1"> 64 + <p class="flex gap-1"> 65 + <span class="truncate">{props.file.name}</span> 66 + <span class="shrink-0 text-neutral-600 dark:text-neutral-400"> 67 + ({formatFileSize(props.file.size)}) 68 + </span> 69 + </p> 70 + </div> 71 + <div class="flex items-center gap-x-2"> 72 + <label for="mimetype" class="shrink-0 select-none"> 73 + MIME type 74 + </label> 75 + <TextInput id="mimetype" placeholder={props.file.type} /> 76 + </div> 77 + <div class="flex items-center gap-1"> 78 + <input id="exif-rm" type="checkbox" checked /> 79 + <label for="exif-rm" class="select-none"> 80 + Remove EXIF data 81 + </label> 82 + </div> 83 + <p class="text-xs text-neutral-600 dark:text-neutral-400"> 84 + Metadata will be pasted after the cursor 85 + </p> 86 + <Show when={error()}> 87 + <span class="text-red-500 dark:text-red-400">Error: {error()}</span> 88 + </Show> 89 + <div class="flex justify-between gap-2"> 90 + <Button onClick={props.onClose}>Cancel</Button> 91 + <Show when={uploading()}> 92 + <div class="flex items-center gap-1"> 93 + <span class="iconify lucide--loader-circle animate-spin"></span> 94 + <span>Uploading</span> 95 + </div> 96 + </Show> 97 + <Show when={!uploading()}> 98 + <Button 99 + onClick={uploadBlob} 100 + class="dark:shadow-dark-700 flex items-center gap-1 rounded-lg bg-blue-500 px-2 py-1.5 text-xs text-white shadow-xs select-none hover:bg-blue-600 active:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 dark:active:bg-blue-400" 101 + > 102 + Upload 103 + </Button> 104 + </Show> 105 + </div> 106 + </div> 107 + </div> 108 + ); 109 + };
+87
src/components/create/handle-input.tsx
··· 1 + import { Handle } from "@atcute/lexicons"; 2 + import { createSignal, Show } from "solid-js"; 3 + import { resolveHandle } from "../../utils/api"; 4 + import { Button } from "../button.jsx"; 5 + import { TextInput } from "../text-input.jsx"; 6 + import { editorInstance } from "./state"; 7 + 8 + export const HandleInput = (props: { onClose: () => void }) => { 9 + const [resolving, setResolving] = createSignal(false); 10 + const [error, setError] = createSignal(""); 11 + let handleFormRef!: HTMLFormElement; 12 + 13 + const resolveDid = async (e: SubmitEvent) => { 14 + e.preventDefault(); 15 + const formData = new FormData(handleFormRef); 16 + const handleValue = formData.get("handle")?.toString().trim(); 17 + 18 + if (!handleValue) { 19 + setError("Please enter a handle"); 20 + return; 21 + } 22 + 23 + setResolving(true); 24 + setError(""); 25 + try { 26 + const did = await resolveHandle(handleValue as Handle); 27 + editorInstance.view.dispatch({ 28 + changes: { 29 + from: editorInstance.view.state.selection.main.head, 30 + insert: `"${did}"`, 31 + }, 32 + }); 33 + props.onClose(); 34 + handleFormRef.reset(); 35 + } catch (err: any) { 36 + setError(err.message || "Failed to resolve handle"); 37 + } finally { 38 + setResolving(false); 39 + } 40 + }; 41 + 42 + return ( 43 + <div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-70 left-[50%] w-[20rem] -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-opacity duration-200 dark:border-neutral-700 starting:opacity-0"> 44 + <h2 class="mb-2 font-semibold">Insert DID from handle</h2> 45 + <form ref={handleFormRef} onSubmit={resolveDid} class="flex flex-col gap-2 text-sm"> 46 + <div class="flex flex-col gap-1"> 47 + <label for="handle-input" class="select-none"> 48 + Handle 49 + </label> 50 + <TextInput id="handle-input" name="handle" placeholder="user.bsky.social" /> 51 + </div> 52 + <p class="text-xs text-neutral-600 dark:text-neutral-400"> 53 + DID will be pasted after the cursor 54 + </p> 55 + <Show when={error()}> 56 + <span class="text-red-500 dark:text-red-400">Error: {error()}</span> 57 + </Show> 58 + <div class="flex justify-between gap-2"> 59 + <Button 60 + type="button" 61 + onClick={() => { 62 + props.onClose(); 63 + handleFormRef.reset(); 64 + setError(""); 65 + }} 66 + > 67 + Cancel 68 + </Button> 69 + <Show when={resolving()}> 70 + <div class="flex items-center gap-1"> 71 + <span class="iconify lucide--loader-circle animate-spin"></span> 72 + <span>Resolving</span> 73 + </div> 74 + </Show> 75 + <Show when={!resolving()}> 76 + <Button 77 + type="submit" 78 + class="dark:shadow-dark-700 flex items-center gap-1 rounded-lg bg-blue-500 px-2 py-1.5 text-xs text-white shadow-xs select-none hover:bg-blue-600 active:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 dark:active:bg-blue-400" 79 + > 80 + Insert 81 + </Button> 82 + </Show> 83 + </div> 84 + </form> 85 + </div> 86 + ); 87 + };
+479
src/components/create/index.tsx
··· 1 + import { Client } from "@atcute/client"; 2 + import { Did } from "@atcute/lexicons"; 3 + import { isNsid, isRecordKey } from "@atcute/lexicons/syntax"; 4 + import { getSession, OAuthUserAgent } from "@atcute/oauth-browser-client"; 5 + import { useNavigate, useParams } from "@solidjs/router"; 6 + import { 7 + createEffect, 8 + createSignal, 9 + For, 10 + lazy, 11 + onCleanup, 12 + onMount, 13 + Show, 14 + Suspense, 15 + } from "solid-js"; 16 + import { hasUserScope } from "../../auth/scope-utils"; 17 + import { agent, sessions } from "../../auth/state"; 18 + import { Button } from "../button.jsx"; 19 + import { Modal } from "../modal.jsx"; 20 + import { addNotification, removeNotification } from "../notification.jsx"; 21 + import { TextInput } from "../text-input.jsx"; 22 + import Tooltip from "../tooltip.jsx"; 23 + import { FileUpload } from "./file-upload"; 24 + import { HandleInput } from "./handle-input"; 25 + import { MenuItem } from "./menu-item"; 26 + import { editorInstance, placeholder, setPlaceholder } from "./state"; 27 + 28 + const Editor = lazy(() => import("../editor.jsx").then((m) => ({ default: m.Editor }))); 29 + 30 + export { editorInstance, placeholder, setPlaceholder }; 31 + 32 + export const RecordEditor = (props: { create: boolean; record?: any; refetch?: any }) => { 33 + const navigate = useNavigate(); 34 + const params = useParams(); 35 + const [openDialog, setOpenDialog] = createSignal(false); 36 + const [notice, setNotice] = createSignal(""); 37 + const [openUpload, setOpenUpload] = createSignal(false); 38 + const [openInsertMenu, setOpenInsertMenu] = createSignal(false); 39 + const [openHandleDialog, setOpenHandleDialog] = createSignal(false); 40 + const [validate, setValidate] = createSignal<boolean | undefined>(undefined); 41 + const [isMaximized, setIsMaximized] = createSignal(false); 42 + const [isMinimized, setIsMinimized] = createSignal(false); 43 + const [collectionError, setCollectionError] = createSignal(""); 44 + const [rkeyError, setRkeyError] = createSignal(""); 45 + let blobInput!: HTMLInputElement; 46 + let formRef!: HTMLFormElement; 47 + let insertMenuRef!: HTMLDivElement; 48 + 49 + createEffect(() => { 50 + if (openInsertMenu()) { 51 + const handleClickOutside = (e: MouseEvent) => { 52 + if (insertMenuRef && !insertMenuRef.contains(e.target as Node)) { 53 + setOpenInsertMenu(false); 54 + } 55 + }; 56 + document.addEventListener("mousedown", handleClickOutside); 57 + onCleanup(() => document.removeEventListener("mousedown", handleClickOutside)); 58 + } 59 + }); 60 + 61 + onMount(() => { 62 + const keyEvent = (ev: KeyboardEvent) => { 63 + if (ev.target instanceof HTMLInputElement || ev.target instanceof HTMLTextAreaElement) return; 64 + if ((ev.target as HTMLElement).closest("[data-modal]")) return; 65 + 66 + const key = props.create ? "n" : "e"; 67 + if (ev.key === key) { 68 + ev.preventDefault(); 69 + 70 + if (openDialog() && isMinimized()) { 71 + setIsMinimized(false); 72 + } else if (!openDialog() && !document.querySelector("[data-modal]")) { 73 + setOpenDialog(true); 74 + } 75 + } 76 + }; 77 + 78 + window.addEventListener("keydown", keyEvent); 79 + onCleanup(() => window.removeEventListener("keydown", keyEvent)); 80 + }); 81 + 82 + const defaultPlaceholder = () => { 83 + return { 84 + $type: "app.bsky.feed.post", 85 + text: "This post was sent from PDSls", 86 + embed: { 87 + $type: "app.bsky.embed.external", 88 + external: { 89 + uri: "https://pdsls.dev", 90 + title: "PDSls", 91 + description: "Browse the public data on atproto", 92 + }, 93 + }, 94 + langs: ["en"], 95 + createdAt: new Date().toISOString(), 96 + }; 97 + }; 98 + 99 + const getValidateIcon = () => { 100 + return ( 101 + validate() === true ? "lucide--circle-check" 102 + : validate() === false ? "lucide--circle-x" 103 + : "lucide--circle" 104 + ); 105 + }; 106 + 107 + const getValidateLabel = () => { 108 + return ( 109 + validate() === true ? "True" 110 + : validate() === false ? "False" 111 + : "Unset" 112 + ); 113 + }; 114 + 115 + createEffect(() => { 116 + if (openDialog()) { 117 + setValidate(undefined); 118 + setCollectionError(""); 119 + setRkeyError(""); 120 + } 121 + }); 122 + 123 + const createRecord = async (formData: FormData) => { 124 + const repo = formData.get("repo")?.toString(); 125 + if (!repo) return; 126 + const rpc = new Client({ handler: new OAuthUserAgent(await getSession(repo as Did)) }); 127 + const collection = formData.get("collection"); 128 + const rkey = formData.get("rkey"); 129 + let record: any; 130 + try { 131 + record = JSON.parse(editorInstance.view.state.doc.toString()); 132 + } catch (e: any) { 133 + setNotice(e.message); 134 + return; 135 + } 136 + const res = await rpc.post("com.atproto.repo.createRecord", { 137 + input: { 138 + repo: repo as Did, 139 + collection: collection ? collection.toString() : record.$type, 140 + rkey: rkey?.toString().length ? rkey?.toString() : undefined, 141 + record: record, 142 + validate: validate(), 143 + }, 144 + }); 145 + if (!res.ok) { 146 + setNotice(`${res.data.error}: ${res.data.message}`); 147 + return; 148 + } 149 + setOpenDialog(false); 150 + const id = addNotification({ 151 + message: "Record created", 152 + type: "success", 153 + }); 154 + setTimeout(() => removeNotification(id), 3000); 155 + navigate(`/${res.data.uri}`); 156 + }; 157 + 158 + const editRecord = async (recreate?: boolean) => { 159 + const record = editorInstance.view.state.doc.toString(); 160 + if (!record) return; 161 + const rpc = new Client({ handler: agent()! }); 162 + try { 163 + const editedRecord = JSON.parse(record); 164 + if (recreate) { 165 + const res = await rpc.post("com.atproto.repo.applyWrites", { 166 + input: { 167 + repo: agent()!.sub, 168 + validate: validate(), 169 + writes: [ 170 + { 171 + collection: params.collection as `${string}.${string}.${string}`, 172 + rkey: params.rkey!, 173 + $type: "com.atproto.repo.applyWrites#delete", 174 + }, 175 + { 176 + collection: params.collection as `${string}.${string}.${string}`, 177 + rkey: params.rkey, 178 + $type: "com.atproto.repo.applyWrites#create", 179 + value: editedRecord, 180 + }, 181 + ], 182 + }, 183 + }); 184 + if (!res.ok) { 185 + setNotice(`${res.data.error}: ${res.data.message}`); 186 + return; 187 + } 188 + } else { 189 + const res = await rpc.post("com.atproto.repo.applyWrites", { 190 + input: { 191 + repo: agent()!.sub, 192 + validate: validate(), 193 + writes: [ 194 + { 195 + collection: params.collection as `${string}.${string}.${string}`, 196 + rkey: params.rkey!, 197 + $type: "com.atproto.repo.applyWrites#update", 198 + value: editedRecord, 199 + }, 200 + ], 201 + }, 202 + }); 203 + if (!res.ok) { 204 + setNotice(`${res.data.error}: ${res.data.message}`); 205 + return; 206 + } 207 + } 208 + setOpenDialog(false); 209 + const id = addNotification({ 210 + message: "Record edited", 211 + type: "success", 212 + }); 213 + setTimeout(() => removeNotification(id), 3000); 214 + props.refetch(); 215 + } catch (err: any) { 216 + setNotice(err.message); 217 + } 218 + }; 219 + 220 + const insertTimestamp = () => { 221 + const timestamp = new Date().toISOString(); 222 + editorInstance.view.dispatch({ 223 + changes: { 224 + from: editorInstance.view.state.selection.main.head, 225 + insert: `"${timestamp}"`, 226 + }, 227 + }); 228 + setOpenInsertMenu(false); 229 + }; 230 + 231 + const insertDidFromHandle = () => { 232 + setOpenInsertMenu(false); 233 + setOpenHandleDialog(true); 234 + }; 235 + 236 + return ( 237 + <> 238 + <Modal 239 + open={openDialog()} 240 + onClose={() => setOpenDialog(false)} 241 + closeOnClick={false} 242 + nonBlocking={isMinimized()} 243 + > 244 + <div 245 + style="transform: translateX(-50%) translateZ(0);" 246 + classList={{ 247 + "dark:bg-dark-300 dark:shadow-dark-700 pointer-events-auto absolute top-18 left-1/2 flex flex-col rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-all duration-200 dark:border-neutral-700 starting:opacity-0": true, 248 + "w-[calc(100%-1rem)] max-w-3xl h-[65vh]": !isMaximized(), 249 + "w-[calc(100%-1rem)] max-w-7xl h-[85vh]": isMaximized(), 250 + hidden: isMinimized(), 251 + }} 252 + > 253 + <div class="mb-2 flex w-full justify-between text-base"> 254 + <div class="flex items-center gap-2"> 255 + <span class="font-semibold select-none"> 256 + {props.create ? "Creating" : "Editing"} record 257 + </span> 258 + </div> 259 + <div class="flex items-center gap-1"> 260 + <button 261 + type="button" 262 + onclick={() => setIsMinimized(true)} 263 + class="flex items-center rounded-lg p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 264 + > 265 + <span class="iconify lucide--minus"></span> 266 + </button> 267 + <button 268 + type="button" 269 + onclick={() => setIsMaximized(!isMaximized())} 270 + class="flex items-center rounded-lg p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 271 + > 272 + <span 273 + class={`iconify ${isMaximized() ? "lucide--minimize-2" : "lucide--maximize-2"}`} 274 + ></span> 275 + </button> 276 + <button 277 + id="close" 278 + onclick={() => setOpenDialog(false)} 279 + class="flex items-center rounded-lg p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 280 + > 281 + <span class="iconify lucide--x"></span> 282 + </button> 283 + </div> 284 + </div> 285 + <form ref={formRef} class="flex min-h-0 flex-1 flex-col gap-y-2"> 286 + <Show when={props.create}> 287 + <div class="flex flex-wrap items-center gap-1 text-sm"> 288 + <span>at://</span> 289 + <select 290 + class="dark:bg-dark-100 max-w-40 truncate rounded-lg border-[0.5px] border-neutral-300 bg-white px-1 py-1 select-none focus:outline-[1px] focus:outline-neutral-600 dark:border-neutral-600 dark:focus:outline-neutral-400" 291 + name="repo" 292 + id="repo" 293 + > 294 + <For each={Object.keys(sessions)}> 295 + {(session) => ( 296 + <option value={session} selected={session === agent()?.sub}> 297 + {sessions[session].handle ?? session} 298 + </option> 299 + )} 300 + </For> 301 + </select> 302 + <span>/</span> 303 + <TextInput 304 + id="collection" 305 + name="collection" 306 + placeholder="Collection (default: $type)" 307 + class={`w-40 placeholder:text-xs lg:w-52 ${collectionError() ? "border-red-500 focus:outline-red-500 dark:border-red-400 dark:focus:outline-red-400" : ""}`} 308 + onInput={(e) => { 309 + const value = e.currentTarget.value; 310 + if (!value || isNsid(value)) setCollectionError(""); 311 + else 312 + setCollectionError( 313 + "Invalid collection: use reverse domain format (e.g. app.bsky.feed.post)", 314 + ); 315 + }} 316 + /> 317 + <span>/</span> 318 + <TextInput 319 + id="rkey" 320 + name="rkey" 321 + placeholder="Record key (default: TID)" 322 + class={`w-40 placeholder:text-xs lg:w-52 ${rkeyError() ? "border-red-500 focus:outline-red-500 dark:border-red-400 dark:focus:outline-red-400" : ""}`} 323 + onInput={(e) => { 324 + const value = e.currentTarget.value; 325 + if (!value || isRecordKey(value)) setRkeyError(""); 326 + else setRkeyError("Invalid record key: 1-512 chars, use a-z A-Z 0-9 . _ ~ : -"); 327 + }} 328 + /> 329 + </div> 330 + <Show when={collectionError() || rkeyError()}> 331 + <div class="text-xs text-red-500 dark:text-red-400"> 332 + <div>{collectionError()}</div> 333 + <div>{rkeyError()}</div> 334 + </div> 335 + </Show> 336 + </Show> 337 + <div class="min-h-0 flex-1"> 338 + <Suspense 339 + fallback={ 340 + <div class="flex h-full items-center justify-center"> 341 + <span class="iconify lucide--loader-circle animate-spin text-xl"></span> 342 + </div> 343 + } 344 + > 345 + <Editor 346 + content={JSON.stringify( 347 + !props.create ? props.record 348 + : params.rkey ? placeholder() 349 + : defaultPlaceholder(), 350 + null, 351 + 2, 352 + )} 353 + /> 354 + </Suspense> 355 + </div> 356 + <div class="flex flex-col gap-2"> 357 + <Show when={notice()}> 358 + <div class="text-sm text-red-500 dark:text-red-400">{notice()}</div> 359 + </Show> 360 + <div class="flex justify-between gap-2"> 361 + <div class="relative" ref={insertMenuRef}> 362 + <button 363 + type="button" 364 + class="dark:hover:bg-dark-200 dark:shadow-dark-700 dark:active:bg-dark-100 flex w-fit rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-1.5 text-base shadow-xs hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800" 365 + onClick={() => setOpenInsertMenu(!openInsertMenu())} 366 + > 367 + <span class="iconify lucide--plus select-none"></span> 368 + </button> 369 + <Show when={openInsertMenu()}> 370 + <div class="dark:bg-dark-300 dark:shadow-dark-700 absolute bottom-full left-0 z-10 mb-1 flex w-40 flex-col rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-1.5 shadow-md dark:border-neutral-700"> 371 + <MenuItem 372 + icon="lucide--id-card" 373 + label="Insert DID" 374 + onClick={insertDidFromHandle} 375 + /> 376 + <MenuItem 377 + icon="lucide--clock" 378 + label="Insert timestamp" 379 + onClick={insertTimestamp} 380 + /> 381 + <Show when={hasUserScope("blob")}> 382 + <MenuItem 383 + icon="lucide--upload" 384 + label="Upload blob" 385 + onClick={() => { 386 + setOpenInsertMenu(false); 387 + blobInput.click(); 388 + }} 389 + /> 390 + </Show> 391 + </div> 392 + </Show> 393 + <input 394 + type="file" 395 + id="blob" 396 + class="sr-only" 397 + ref={blobInput} 398 + onChange={(e) => { 399 + if (e.target.files !== null) setOpenUpload(true); 400 + }} 401 + /> 402 + </div> 403 + <Modal 404 + open={openUpload()} 405 + onClose={() => setOpenUpload(false)} 406 + closeOnClick={false} 407 + > 408 + <FileUpload 409 + file={blobInput.files![0]} 410 + blobInput={blobInput} 411 + onClose={() => setOpenUpload(false)} 412 + /> 413 + </Modal> 414 + <Modal 415 + open={openHandleDialog()} 416 + onClose={() => setOpenHandleDialog(false)} 417 + closeOnClick={false} 418 + > 419 + <HandleInput onClose={() => setOpenHandleDialog(false)} /> 420 + </Modal> 421 + <div class="flex items-center justify-end gap-2"> 422 + <button 423 + type="button" 424 + class="flex items-center gap-1 rounded-sm p-1.5 text-xs hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 425 + onClick={() => 426 + setValidate( 427 + validate() === true ? false 428 + : validate() === false ? undefined 429 + : true, 430 + ) 431 + } 432 + > 433 + <Tooltip text={getValidateLabel()}> 434 + <span class={`iconify ${getValidateIcon()}`}></span> 435 + </Tooltip> 436 + <span>Validate</span> 437 + </button> 438 + <Show when={!props.create && hasUserScope("create") && hasUserScope("delete")}> 439 + <Button onClick={() => editRecord(true)}>Recreate</Button> 440 + </Show> 441 + <Button 442 + onClick={() => 443 + props.create ? createRecord(new FormData(formRef)) : editRecord() 444 + } 445 + > 446 + {props.create ? "Create" : "Edit"} 447 + </Button> 448 + </div> 449 + </div> 450 + </div> 451 + </form> 452 + </div> 453 + </Modal> 454 + <Show when={isMinimized() && openDialog()}> 455 + <button 456 + class="dark:bg-dark-300 dark:hover:bg-dark-200 dark:active:bg-dark-100 fixed right-4 bottom-4 z-30 flex items-center gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 px-3 py-2 shadow-md hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700" 457 + onclick={() => setIsMinimized(false)} 458 + > 459 + <span class="iconify lucide--square-pen text-lg"></span> 460 + <span class="text-sm font-medium">{props.create ? "Creating" : "Editing"} record</span> 461 + </button> 462 + </Show> 463 + <Tooltip text={props.create ? "Create record (n)" : "Edit record (e)"}> 464 + <button 465 + class={`flex items-center p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600 ${props.create ? "rounded-lg" : "rounded-sm"}`} 466 + onclick={() => { 467 + setNotice(""); 468 + setOpenDialog(true); 469 + setIsMinimized(false); 470 + }} 471 + > 472 + <div 473 + class={props.create ? "iconify lucide--square-pen text-lg" : "iconify lucide--pencil"} 474 + /> 475 + </button> 476 + </Tooltip> 477 + </> 478 + ); 479 + };
+12
src/components/create/menu-item.tsx
··· 1 + export const MenuItem = (props: { icon: string; label: string; onClick: () => void }) => { 2 + return ( 3 + <button 4 + type="button" 5 + class="flex items-center gap-2 rounded-md p-2 text-left text-xs hover:bg-neutral-100 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 6 + onClick={props.onClick} 7 + > 8 + <span class={`iconify ${props.icon}`}></span> 9 + <span>{props.label}</span> 10 + </button> 11 + ); 12 + };
+4
src/components/create/state.ts
··· 1 + import { createSignal } from "solid-js"; 2 + 3 + export const editorInstance = { view: null as any }; 4 + export const [placeholder, setPlaceholder] = createSignal<any>();
-519
src/components/create.tsx
··· 1 - import { Client } from "@atcute/client"; 2 - import { Did } from "@atcute/lexicons"; 3 - import { isNsid, isRecordKey } from "@atcute/lexicons/syntax"; 4 - import { getSession, OAuthUserAgent } from "@atcute/oauth-browser-client"; 5 - import { remove } from "@mary/exif-rm"; 6 - import { useNavigate, useParams } from "@solidjs/router"; 7 - import { createEffect, createSignal, For, onCleanup, Show } from "solid-js"; 8 - import { Editor, editorView } from "../components/editor.jsx"; 9 - import { agent } from "../components/login.jsx"; 10 - import { sessions } from "./account.jsx"; 11 - import { Button } from "./button.jsx"; 12 - import { Modal } from "./modal.jsx"; 13 - import { addNotification, removeNotification } from "./notification.jsx"; 14 - import { TextInput } from "./text-input.jsx"; 15 - import Tooltip from "./tooltip.jsx"; 16 - 17 - export const [placeholder, setPlaceholder] = createSignal<any>(); 18 - 19 - export const RecordEditor = (props: { create: boolean; record?: any; refetch?: any }) => { 20 - const navigate = useNavigate(); 21 - const params = useParams(); 22 - const [openDialog, setOpenDialog] = createSignal(false); 23 - const [notice, setNotice] = createSignal(""); 24 - const [openUpload, setOpenUpload] = createSignal(false); 25 - const [openInsertMenu, setOpenInsertMenu] = createSignal(false); 26 - const [validate, setValidate] = createSignal<boolean | undefined>(undefined); 27 - const [isMaximized, setIsMaximized] = createSignal(false); 28 - const [isMinimized, setIsMinimized] = createSignal(false); 29 - const [collectionError, setCollectionError] = createSignal(""); 30 - const [rkeyError, setRkeyError] = createSignal(""); 31 - let blobInput!: HTMLInputElement; 32 - let formRef!: HTMLFormElement; 33 - let insertMenuRef!: HTMLDivElement; 34 - 35 - createEffect(() => { 36 - if (openInsertMenu()) { 37 - const handleClickOutside = (e: MouseEvent) => { 38 - if (insertMenuRef && !insertMenuRef.contains(e.target as Node)) { 39 - setOpenInsertMenu(false); 40 - } 41 - }; 42 - document.addEventListener("mousedown", handleClickOutside); 43 - onCleanup(() => document.removeEventListener("mousedown", handleClickOutside)); 44 - } 45 - }); 46 - 47 - const defaultPlaceholder = () => { 48 - return { 49 - $type: "app.bsky.feed.post", 50 - text: "This post was sent from PDSls", 51 - embed: { 52 - $type: "app.bsky.embed.external", 53 - external: { 54 - uri: "https://pdsls.dev", 55 - title: "PDSls", 56 - description: "Browse the public data on atproto", 57 - }, 58 - }, 59 - langs: ["en"], 60 - createdAt: new Date().toISOString(), 61 - }; 62 - }; 63 - 64 - const getValidateIcon = () => { 65 - return ( 66 - validate() === true ? "lucide--circle-check" 67 - : validate() === false ? "lucide--circle-x" 68 - : "lucide--circle" 69 - ); 70 - }; 71 - 72 - const getValidateLabel = () => { 73 - return ( 74 - validate() === true ? "True" 75 - : validate() === false ? "False" 76 - : "Unset" 77 - ); 78 - }; 79 - 80 - createEffect(() => { 81 - if (openDialog()) { 82 - setValidate(undefined); 83 - setCollectionError(""); 84 - setRkeyError(""); 85 - } 86 - }); 87 - 88 - const createRecord = async (formData: FormData) => { 89 - const repo = formData.get("repo")?.toString(); 90 - if (!repo) return; 91 - const rpc = new Client({ handler: new OAuthUserAgent(await getSession(repo as Did)) }); 92 - const collection = formData.get("collection"); 93 - const rkey = formData.get("rkey"); 94 - let record: any; 95 - try { 96 - record = JSON.parse(editorView.state.doc.toString()); 97 - } catch (e: any) { 98 - setNotice(e.message); 99 - return; 100 - } 101 - const res = await rpc.post("com.atproto.repo.createRecord", { 102 - input: { 103 - repo: repo as Did, 104 - collection: collection ? collection.toString() : record.$type, 105 - rkey: rkey?.toString().length ? rkey?.toString() : undefined, 106 - record: record, 107 - validate: validate(), 108 - }, 109 - }); 110 - if (!res.ok) { 111 - setNotice(`${res.data.error}: ${res.data.message}`); 112 - return; 113 - } 114 - setOpenDialog(false); 115 - const id = addNotification({ 116 - message: "Record created", 117 - type: "success", 118 - }); 119 - setTimeout(() => removeNotification(id), 3000); 120 - navigate(`/${res.data.uri}`); 121 - }; 122 - 123 - const editRecord = async (recreate?: boolean) => { 124 - const record = editorView.state.doc.toString(); 125 - if (!record) return; 126 - const rpc = new Client({ handler: agent()! }); 127 - try { 128 - const editedRecord = JSON.parse(record); 129 - if (recreate) { 130 - const res = await rpc.post("com.atproto.repo.applyWrites", { 131 - input: { 132 - repo: agent()!.sub, 133 - validate: validate(), 134 - writes: [ 135 - { 136 - collection: params.collection as `${string}.${string}.${string}`, 137 - rkey: params.rkey!, 138 - $type: "com.atproto.repo.applyWrites#delete", 139 - }, 140 - { 141 - collection: params.collection as `${string}.${string}.${string}`, 142 - rkey: params.rkey, 143 - $type: "com.atproto.repo.applyWrites#create", 144 - value: editedRecord, 145 - }, 146 - ], 147 - }, 148 - }); 149 - if (!res.ok) { 150 - setNotice(`${res.data.error}: ${res.data.message}`); 151 - return; 152 - } 153 - } else { 154 - const res = await rpc.post("com.atproto.repo.putRecord", { 155 - input: { 156 - repo: agent()!.sub, 157 - collection: params.collection as `${string}.${string}.${string}`, 158 - rkey: params.rkey!, 159 - record: editedRecord, 160 - validate: validate(), 161 - }, 162 - }); 163 - if (!res.ok) { 164 - setNotice(`${res.data.error}: ${res.data.message}`); 165 - return; 166 - } 167 - } 168 - setOpenDialog(false); 169 - const id = addNotification({ 170 - message: "Record edited", 171 - type: "success", 172 - }); 173 - setTimeout(() => removeNotification(id), 3000); 174 - props.refetch(); 175 - } catch (err: any) { 176 - setNotice(err.message); 177 - } 178 - }; 179 - 180 - const insertTimestamp = () => { 181 - const timestamp = new Date().toISOString(); 182 - editorView.dispatch({ 183 - changes: { 184 - from: editorView.state.selection.main.head, 185 - insert: `"${timestamp}"`, 186 - }, 187 - }); 188 - setOpenInsertMenu(false); 189 - }; 190 - 191 - const MenuItem = (props: { icon: string; label: string; onClick: () => void }) => { 192 - return ( 193 - <button 194 - type="button" 195 - class="flex items-center gap-2 rounded-md p-2 text-left text-xs hover:bg-neutral-100 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 196 - onClick={props.onClick} 197 - > 198 - <span class={`iconify ${props.icon}`}></span> 199 - <span>{props.label}</span> 200 - </button> 201 - ); 202 - }; 203 - 204 - const FileUpload = (props: { file: File }) => { 205 - const [uploading, setUploading] = createSignal(false); 206 - const [error, setError] = createSignal(""); 207 - 208 - onCleanup(() => (blobInput.value = "")); 209 - 210 - const formatFileSize = (bytes: number) => { 211 - if (bytes === 0) return "0 Bytes"; 212 - const k = 1024; 213 - const sizes = ["Bytes", "KB", "MB", "GB"]; 214 - const i = Math.floor(Math.log(bytes) / Math.log(k)); 215 - return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i]; 216 - }; 217 - 218 - const uploadBlob = async () => { 219 - let blob: Blob; 220 - 221 - const mimetype = (document.getElementById("mimetype") as HTMLInputElement)?.value; 222 - (document.getElementById("mimetype") as HTMLInputElement).value = ""; 223 - if (mimetype) blob = new Blob([props.file], { type: mimetype }); 224 - else blob = props.file; 225 - 226 - if ((document.getElementById("exif-rm") as HTMLInputElement).checked) { 227 - const exifRemoved = remove(new Uint8Array(await blob.arrayBuffer())); 228 - if (exifRemoved !== null) blob = new Blob([exifRemoved], { type: blob.type }); 229 - } 230 - 231 - const rpc = new Client({ handler: agent()! }); 232 - setUploading(true); 233 - const res = await rpc.post("com.atproto.repo.uploadBlob", { 234 - input: blob, 235 - }); 236 - setUploading(false); 237 - if (!res.ok) { 238 - setError(res.data.error); 239 - return; 240 - } 241 - editorView.dispatch({ 242 - changes: { 243 - from: editorView.state.selection.main.head, 244 - insert: JSON.stringify(res.data.blob, null, 2), 245 - }, 246 - }); 247 - setOpenUpload(false); 248 - }; 249 - 250 - return ( 251 - <div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-70 left-[50%] w-[20rem] -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-opacity duration-200 dark:border-neutral-700 starting:opacity-0"> 252 - <h2 class="mb-2 font-semibold">Upload blob</h2> 253 - <div class="flex flex-col gap-2 text-sm"> 254 - <div class="flex flex-col gap-1"> 255 - <p class="flex gap-1"> 256 - <span class="truncate">{props.file.name}</span> 257 - <span class="shrink-0 text-neutral-600 dark:text-neutral-400"> 258 - ({formatFileSize(props.file.size)}) 259 - </span> 260 - </p> 261 - </div> 262 - <div class="flex items-center gap-x-2"> 263 - <label for="mimetype" class="shrink-0 select-none"> 264 - MIME type 265 - </label> 266 - <TextInput id="mimetype" placeholder={props.file.type} /> 267 - </div> 268 - <div class="flex items-center gap-1"> 269 - <input id="exif-rm" type="checkbox" checked /> 270 - <label for="exif-rm" class="select-none"> 271 - Remove EXIF data 272 - </label> 273 - </div> 274 - <p class="text-xs text-neutral-600 dark:text-neutral-400"> 275 - Metadata will be pasted after the cursor 276 - </p> 277 - <Show when={error()}> 278 - <span class="text-red-500 dark:text-red-400">Error: {error()}</span> 279 - </Show> 280 - <div class="flex justify-between gap-2"> 281 - <Button onClick={() => setOpenUpload(false)}>Cancel</Button> 282 - <Show when={uploading()}> 283 - <div class="flex items-center gap-1"> 284 - <span class="iconify lucide--loader-circle animate-spin"></span> 285 - <span>Uploading</span> 286 - </div> 287 - </Show> 288 - <Show when={!uploading()}> 289 - <Button 290 - onClick={uploadBlob} 291 - class="dark:shadow-dark-700 flex items-center gap-1 rounded-lg bg-blue-500 px-2 py-1.5 text-xs text-white shadow-xs select-none hover:bg-blue-600 active:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 dark:active:bg-blue-400" 292 - > 293 - Upload 294 - </Button> 295 - </Show> 296 - </div> 297 - </div> 298 - </div> 299 - ); 300 - }; 301 - 302 - return ( 303 - <> 304 - <Modal 305 - open={openDialog()} 306 - onClose={() => setOpenDialog(false)} 307 - closeOnClick={false} 308 - nonBlocking={isMinimized()} 309 - > 310 - <div 311 - style="transform: translateX(-50%) translateZ(0);" 312 - classList={{ 313 - "dark:bg-dark-300 dark:shadow-dark-700 pointer-events-auto absolute top-18 left-1/2 flex flex-col rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-all duration-200 dark:border-neutral-700 starting:opacity-0": true, 314 - "w-[calc(100%-1rem)] max-w-3xl h-[65vh]": !isMaximized(), 315 - "w-[calc(100%-1rem)] max-w-7xl h-[85vh]": isMaximized(), 316 - hidden: isMinimized(), 317 - }} 318 - > 319 - <div class="mb-2 flex w-full justify-between text-base"> 320 - <div class="flex items-center gap-2"> 321 - <span class="font-semibold select-none"> 322 - {props.create ? "Creating" : "Editing"} record 323 - </span> 324 - </div> 325 - <div class="flex items-center gap-1"> 326 - <button 327 - type="button" 328 - onclick={() => setIsMinimized(true)} 329 - class="flex items-center rounded-lg p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 330 - > 331 - <span class="iconify lucide--minus"></span> 332 - </button> 333 - <button 334 - type="button" 335 - onclick={() => setIsMaximized(!isMaximized())} 336 - class="flex items-center rounded-lg p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 337 - > 338 - <span 339 - class={`iconify ${isMaximized() ? "lucide--minimize-2" : "lucide--maximize-2"}`} 340 - ></span> 341 - </button> 342 - <button 343 - id="close" 344 - onclick={() => setOpenDialog(false)} 345 - class="flex items-center rounded-lg p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 346 - > 347 - <span class="iconify lucide--x"></span> 348 - </button> 349 - </div> 350 - </div> 351 - <form ref={formRef} class="flex min-h-0 flex-1 flex-col gap-y-2"> 352 - <Show when={props.create}> 353 - <div class="flex flex-wrap items-center gap-1 text-sm"> 354 - <span>at://</span> 355 - <select 356 - class="dark:bg-dark-100 max-w-40 truncate rounded-lg border-[0.5px] border-neutral-300 bg-white px-1 py-1 select-none focus:outline-[1px] focus:outline-neutral-600 dark:border-neutral-600 dark:focus:outline-neutral-400" 357 - name="repo" 358 - id="repo" 359 - > 360 - <For each={Object.keys(sessions)}> 361 - {(session) => ( 362 - <option value={session} selected={session === agent()?.sub}> 363 - {sessions[session].handle ?? session} 364 - </option> 365 - )} 366 - </For> 367 - </select> 368 - <span>/</span> 369 - <TextInput 370 - id="collection" 371 - name="collection" 372 - placeholder="Collection (default: $type)" 373 - class={`w-40 placeholder:text-xs lg:w-52 ${collectionError() ? "border-red-500 focus:outline-red-500 dark:border-red-400 dark:focus:outline-red-400" : ""}`} 374 - onInput={(e) => { 375 - const value = e.currentTarget.value; 376 - if (!value || isNsid(value)) setCollectionError(""); 377 - else 378 - setCollectionError( 379 - "Invalid collection: use reverse domain format (e.g. app.bsky.feed.post)", 380 - ); 381 - }} 382 - /> 383 - <span>/</span> 384 - <TextInput 385 - id="rkey" 386 - name="rkey" 387 - placeholder="Record key (default: TID)" 388 - class={`w-40 placeholder:text-xs lg:w-52 ${rkeyError() ? "border-red-500 focus:outline-red-500 dark:border-red-400 dark:focus:outline-red-400" : ""}`} 389 - onInput={(e) => { 390 - const value = e.currentTarget.value; 391 - if (!value || isRecordKey(value)) setRkeyError(""); 392 - else setRkeyError("Invalid record key: 1-512 chars, use a-z A-Z 0-9 . _ ~ : -"); 393 - }} 394 - /> 395 - </div> 396 - <Show when={collectionError() || rkeyError()}> 397 - <div class="text-xs text-red-500 dark:text-red-400"> 398 - <div>{collectionError()}</div> 399 - <div>{rkeyError()}</div> 400 - </div> 401 - </Show> 402 - </Show> 403 - <div class="min-h-0 flex-1"> 404 - <Editor 405 - content={JSON.stringify( 406 - !props.create ? props.record 407 - : params.rkey ? placeholder() 408 - : defaultPlaceholder(), 409 - null, 410 - 2, 411 - )} 412 - /> 413 - </div> 414 - <div class="flex flex-col gap-2"> 415 - <Show when={notice()}> 416 - <div class="text-sm text-red-500 dark:text-red-400">{notice()}</div> 417 - </Show> 418 - <div class="flex justify-between gap-2"> 419 - <div class="relative" ref={insertMenuRef}> 420 - <button 421 - type="button" 422 - class="dark:hover:bg-dark-200 dark:shadow-dark-700 dark:active:bg-dark-100 flex w-fit rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-1.5 text-base shadow-xs hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800" 423 - onClick={() => setOpenInsertMenu(!openInsertMenu())} 424 - > 425 - <span class="iconify lucide--plus select-none"></span> 426 - </button> 427 - <Show when={openInsertMenu()}> 428 - <div class="dark:bg-dark-300 dark:shadow-dark-700 absolute bottom-full left-0 z-10 mb-1 flex w-40 flex-col rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-1.5 shadow-md dark:border-neutral-700"> 429 - <MenuItem 430 - icon="lucide--upload" 431 - label="Upload blob" 432 - onClick={() => { 433 - setOpenInsertMenu(false); 434 - blobInput.click(); 435 - }} 436 - /> 437 - <MenuItem 438 - icon="lucide--clock" 439 - label="Insert timestamp" 440 - onClick={insertTimestamp} 441 - /> 442 - </div> 443 - </Show> 444 - <input 445 - type="file" 446 - id="blob" 447 - class="sr-only" 448 - ref={blobInput} 449 - onChange={(e) => { 450 - if (e.target.files !== null) setOpenUpload(true); 451 - }} 452 - /> 453 - </div> 454 - <Modal 455 - open={openUpload()} 456 - onClose={() => setOpenUpload(false)} 457 - closeOnClick={false} 458 - > 459 - <FileUpload file={blobInput.files![0]} /> 460 - </Modal> 461 - <div class="flex items-center justify-end gap-2"> 462 - <button 463 - type="button" 464 - class="flex items-center gap-1 rounded-sm p-1.5 text-xs hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 465 - onClick={() => 466 - setValidate( 467 - validate() === true ? false 468 - : validate() === false ? undefined 469 - : true, 470 - ) 471 - } 472 - > 473 - <Tooltip text={getValidateLabel()}> 474 - <span class={`iconify ${getValidateIcon()}`}></span> 475 - </Tooltip> 476 - <span>Validate</span> 477 - </button> 478 - <Show when={!props.create}> 479 - <Button onClick={() => editRecord(true)}>Recreate</Button> 480 - </Show> 481 - <Button 482 - onClick={() => 483 - props.create ? createRecord(new FormData(formRef)) : editRecord() 484 - } 485 - > 486 - {props.create ? "Create" : "Edit"} 487 - </Button> 488 - </div> 489 - </div> 490 - </div> 491 - </form> 492 - </div> 493 - </Modal> 494 - <Show when={isMinimized() && openDialog()}> 495 - <button 496 - class="dark:bg-dark-300 dark:hover:bg-dark-200 dark:active:bg-dark-100 fixed right-4 bottom-4 z-30 flex items-center gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 px-3 py-2 shadow-md hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700" 497 - onclick={() => setIsMinimized(false)} 498 - > 499 - <span class="iconify lucide--square-pen text-lg"></span> 500 - <span class="text-sm font-medium">{props.create ? "Creating" : "Editing"} record</span> 501 - </button> 502 - </Show> 503 - <Tooltip text={`${props.create ? "Create" : "Edit"} record`}> 504 - <button 505 - class={`flex items-center p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600 ${props.create ? "rounded-lg" : "rounded-sm"}`} 506 - onclick={() => { 507 - setNotice(""); 508 - setOpenDialog(true); 509 - setIsMinimized(false); 510 - }} 511 - > 512 - <div 513 - class={props.create ? "iconify lucide--square-pen text-lg" : "iconify lucide--pencil"} 514 - /> 515 - </button> 516 - </Tooltip> 517 - </> 518 - ); 519 - };
+41 -15
src/components/dropdown.tsx
··· 10 10 Show, 11 11 useContext, 12 12 } from "solid-js"; 13 + import { Portal } from "solid-js/web"; 13 14 import { addToClipboard } from "../utils/copy"; 14 15 15 16 const MenuContext = createContext<{ ··· 33 34 addToClipboard(props.content); 34 35 ctx?.setShowMenu(false); 35 36 }} 36 - class="flex items-center gap-1.5 rounded-md p-1 whitespace-nowrap hover:bg-neutral-200/50 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 37 + class="flex items-center gap-2 rounded-md p-1.5 whitespace-nowrap hover:bg-neutral-200/50 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 37 38 > 38 39 <Show when={props.icon}> 39 40 <span class={"iconify shrink-0 " + props.icon}></span> ··· 56 57 <A 57 58 href={props.href} 58 59 onClick={() => ctx?.setShowMenu(false)} 59 - class="flex items-center gap-1.5 rounded-md p-1 hover:bg-neutral-200/50 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 60 + class="flex items-center gap-2 rounded-md p-1.5 hover:bg-neutral-200/50 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 60 61 classList={{ "justify-between": props.external }} 61 62 target={props.newTab ? "_blank" : undefined} 62 63 > ··· 79 80 return ( 80 81 <button 81 82 onClick={props.onClick} 82 - class="flex items-center gap-1.5 rounded-md p-1 whitespace-nowrap hover:bg-neutral-200/50 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 83 + class="flex items-center gap-2 rounded-md p-1.5 whitespace-nowrap hover:bg-neutral-200/50 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 83 84 > 84 85 <Show when={props.icon}> 85 86 <span class={"iconify shrink-0 " + props.icon}></span> ··· 102 103 const ctx = useContext(MenuContext); 103 104 const [menu, setMenu] = createSignal<HTMLDivElement>(); 104 105 const [menuButton, setMenuButton] = createSignal<HTMLButtonElement>(); 106 + const [buttonRect, setButtonRect] = createSignal<DOMRect>(); 105 107 106 108 const clickEvent = (event: MouseEvent) => { 107 109 const target = event.target as Node; 108 110 if (!menuButton()?.contains(target) && !menu()?.contains(target)) ctx?.setShowMenu(false); 109 111 }; 110 112 111 - onMount(() => window.addEventListener("click", clickEvent)); 112 - onCleanup(() => window.removeEventListener("click", clickEvent)); 113 + const updatePosition = () => { 114 + const rect = menuButton()?.getBoundingClientRect(); 115 + if (rect) setButtonRect(rect); 116 + }; 117 + 118 + onMount(() => { 119 + window.addEventListener("click", clickEvent); 120 + window.addEventListener("scroll", updatePosition, true); 121 + window.addEventListener("resize", updatePosition); 122 + }); 123 + 124 + onCleanup(() => { 125 + window.removeEventListener("click", clickEvent); 126 + window.removeEventListener("scroll", updatePosition, true); 127 + window.removeEventListener("resize", updatePosition); 128 + }); 113 129 114 130 return ( 115 131 <div class="relative"> ··· 119 135 props.buttonClass 120 136 } 121 137 ref={setMenuButton} 122 - onClick={() => ctx?.setShowMenu(!ctx?.showMenu())} 138 + onClick={() => { 139 + updatePosition(); 140 + ctx?.setShowMenu(!ctx?.showMenu()); 141 + }} 123 142 > 124 143 <span class={"iconify " + props.icon}></span> 125 144 </button> 126 145 <Show when={ctx?.showMenu()}> 127 - <div 128 - ref={setMenu} 129 - class={ 130 - "dark:bg-dark-300 dark:shadow-dark-700 absolute right-0 z-40 flex flex-col rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 shadow-md dark:border-neutral-700 " + 131 - props.menuClass 132 - } 133 - > 134 - {props.children} 135 - </div> 146 + <Portal> 147 + <div 148 + ref={setMenu} 149 + style={{ 150 + position: "fixed", 151 + top: `${(buttonRect()?.bottom ?? 0) + 4}px`, 152 + left: `${(buttonRect()?.right ?? 0) - 160}px`, 153 + }} 154 + class={ 155 + "dark:bg-dark-300 dark:shadow-dark-700 z-50 flex min-w-40 flex-col rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 text-sm shadow-md dark:border-neutral-700 " + 156 + props.menuClass 157 + } 158 + > 159 + {props.children} 160 + </div> 161 + </Portal> 136 162 </Show> 137 163 </div> 138 164 );
+5 -4
src/components/editor.tsx
··· 7 7 import { basicLight } from "@fsegurai/codemirror-theme-basic-light"; 8 8 import { basicSetup, EditorView } from "codemirror"; 9 9 import { onCleanup, onMount } from "solid-js"; 10 - 11 - export let editorView: EditorView; 10 + import { editorInstance } from "./create/state"; 12 11 13 12 const Editor = (props: { content: string }) => { 14 13 let editorDiv!: HTMLDivElement; 15 14 let themeColor = new Compartment(); 15 + let view: EditorView; 16 16 17 17 const themeEvent = () => { 18 - editorView.dispatch({ 18 + view.dispatch({ 19 19 effects: themeColor.reconfigure( 20 20 window.matchMedia("(prefers-color-scheme: dark)").matches ? basicDark : basicLight, 21 21 ), ··· 38 38 39 39 window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", themeEvent); 40 40 41 - editorView = new EditorView({ 41 + view = new EditorView({ 42 42 doc: props.content, 43 43 parent: editorDiv, 44 44 extensions: [ ··· 50 50 themeColor.of(document.documentElement.classList.contains("dark") ? basicDark : basicLight), 51 51 ], 52 52 }); 53 + editorInstance.view = view; 53 54 }); 54 55 55 56 onCleanup(() =>
-143
src/components/login.tsx
··· 1 - import { Client } from "@atcute/client"; 2 - import { Did } from "@atcute/lexicons"; 3 - import { isDid, isHandle } from "@atcute/lexicons/syntax"; 4 - import { 5 - configureOAuth, 6 - createAuthorizationUrl, 7 - defaultIdentityResolver, 8 - finalizeAuthorization, 9 - getSession, 10 - OAuthUserAgent, 11 - type Session, 12 - } from "@atcute/oauth-browser-client"; 13 - import { createSignal, Show } from "solid-js"; 14 - import { didDocumentResolver, handleResolver } from "../utils/api"; 15 - 16 - configureOAuth({ 17 - metadata: { 18 - client_id: import.meta.env.VITE_OAUTH_CLIENT_ID, 19 - redirect_uri: import.meta.env.VITE_OAUTH_REDIRECT_URL, 20 - }, 21 - identityResolver: defaultIdentityResolver({ 22 - handleResolver: handleResolver, 23 - didDocumentResolver: didDocumentResolver, 24 - }), 25 - }); 26 - 27 - export const [agent, setAgent] = createSignal<OAuthUserAgent | undefined>(); 28 - 29 - type Account = { 30 - signedIn: boolean; 31 - handle?: string; 32 - }; 33 - 34 - export type Sessions = Record<string, Account>; 35 - 36 - const Login = () => { 37 - const [notice, setNotice] = createSignal(""); 38 - const [loginInput, setLoginInput] = createSignal(""); 39 - 40 - const login = async (handle: string) => { 41 - try { 42 - setNotice(""); 43 - if (!handle) return; 44 - setNotice(`Contacting your data server...`); 45 - const authUrl = await createAuthorizationUrl({ 46 - scope: import.meta.env.VITE_OAUTH_SCOPE, 47 - target: 48 - isHandle(handle) || isDid(handle) ? 49 - { type: "account", identifier: handle } 50 - : { type: "pds", serviceUrl: handle }, 51 - }); 52 - 53 - setNotice(`Redirecting...`); 54 - await new Promise((resolve) => setTimeout(resolve, 250)); 55 - 56 - location.assign(authUrl); 57 - } catch (e) { 58 - console.error(e); 59 - setNotice(`${e}`); 60 - } 61 - }; 62 - 63 - return ( 64 - <form class="flex flex-col gap-y-2 px-1" onsubmit={(e) => e.preventDefault()}> 65 - <label for="username" class="hidden"> 66 - Add account 67 - </label> 68 - <div class="dark:bg-dark-100 dark:inset-shadow-dark-200 flex grow items-center gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 inset-shadow-xs focus-within:outline-[1px] focus-within:outline-neutral-600 dark:border-neutral-600 dark:focus-within:outline-neutral-400"> 69 - <label 70 - for="username" 71 - class="iconify lucide--user-round-plus shrink-0 text-neutral-500 dark:text-neutral-400" 72 - ></label> 73 - <input 74 - type="text" 75 - spellcheck={false} 76 - placeholder="user.bsky.social" 77 - id="username" 78 - name="username" 79 - autocomplete="username" 80 - aria-label="Your AT Protocol handle" 81 - class="grow py-1 select-none placeholder:text-sm focus:outline-none" 82 - onInput={(e) => setLoginInput(e.currentTarget.value)} 83 - /> 84 - <button 85 - onclick={() => login(loginInput())} 86 - class="flex items-center rounded-lg p-1 hover:bg-neutral-100 active:bg-neutral-200 dark:hover:bg-neutral-600 dark:active:bg-neutral-500" 87 - > 88 - <span class="iconify lucide--log-in"></span> 89 - </button> 90 - </div> 91 - <Show when={notice()}> 92 - <div class="text-sm">{notice()}</div> 93 - </Show> 94 - </form> 95 - ); 96 - }; 97 - 98 - const retrieveSession = async () => { 99 - const init = async (): Promise<Session | undefined> => { 100 - const params = new URLSearchParams(location.hash.slice(1)); 101 - 102 - if (params.has("state") && (params.has("code") || params.has("error"))) { 103 - history.replaceState(null, "", location.pathname + location.search); 104 - 105 - const auth = await finalizeAuthorization(params); 106 - const did = auth.session.info.sub; 107 - 108 - localStorage.setItem("lastSignedIn", did); 109 - 110 - const sessions = localStorage.getItem("sessions"); 111 - const newSessions: Sessions = sessions ? JSON.parse(sessions) : { [did]: {} }; 112 - newSessions[did] = { signedIn: true }; 113 - localStorage.setItem("sessions", JSON.stringify(newSessions)); 114 - return auth.session; 115 - } else { 116 - const lastSignedIn = localStorage.getItem("lastSignedIn"); 117 - 118 - if (lastSignedIn) { 119 - const sessions = localStorage.getItem("sessions"); 120 - const newSessions: Sessions = sessions ? JSON.parse(sessions) : {}; 121 - try { 122 - const session = await getSession(lastSignedIn as Did); 123 - const rpc = new Client({ handler: new OAuthUserAgent(session) }); 124 - const res = await rpc.get("com.atproto.server.getSession"); 125 - newSessions[lastSignedIn].signedIn = true; 126 - localStorage.setItem("sessions", JSON.stringify(newSessions)); 127 - if (!res.ok) throw res.data.error; 128 - return session; 129 - } catch (err) { 130 - newSessions[lastSignedIn].signedIn = false; 131 - localStorage.setItem("sessions", JSON.stringify(newSessions)); 132 - throw err; 133 - } 134 - } 135 - } 136 - }; 137 - 138 - const session = await init(); 139 - 140 - if (session) setAgent(new OAuthUserAgent(session)); 141 - }; 142 - 143 - export { Login, retrieveSession };
+7 -6
src/components/notification.tsx
··· 1 1 import { createSignal, For, Show } from "solid-js"; 2 + import { createStore } from "solid-js/store"; 2 3 3 4 export type Notification = { 4 5 id: string; ··· 8 9 type?: "info" | "success" | "error"; 9 10 }; 10 11 11 - const [notifications, setNotifications] = createSignal<Notification[]>([]); 12 + const [notifications, setNotifications] = createStore<Notification[]>([]); 12 13 const [removingIds, setRemovingIds] = createSignal<Set<string>>(new Set()); 13 14 14 15 export const addNotification = (notification: Omit<Notification, "id">) => { 15 16 const id = `notification-${Date.now()}-${Math.random()}`; 16 - setNotifications([...notifications(), { ...notification, id }]); 17 + setNotifications(notifications.length, { ...notification, id }); 17 18 return id; 18 19 }; 19 20 20 21 export const updateNotification = (id: string, updates: Partial<Notification>) => { 21 - setNotifications(notifications().map((n) => (n.id === id ? { ...n, ...updates } : n))); 22 + setNotifications((n) => n.id === id, updates); 22 23 }; 23 24 24 25 export const removeNotification = (id: string) => { 25 26 setRemovingIds(new Set([...removingIds(), id])); 26 27 setTimeout(() => { 27 - setNotifications(notifications().filter((n) => n.id !== id)); 28 + setNotifications((n) => n.filter((notification) => notification.id !== id)); 28 29 setRemovingIds((ids) => { 29 30 const newIds = new Set(ids); 30 31 newIds.delete(id); ··· 35 36 36 37 export const NotificationContainer = () => { 37 38 return ( 38 - <div class="pointer-events-none fixed bottom-4 left-4 z-50 flex flex-col gap-2"> 39 - <For each={notifications()}> 39 + <div class="pointer-events-none fixed bottom-4 left-4 z-60 flex flex-col gap-2"> 40 + <For each={notifications}> 40 41 {(notification) => ( 41 42 <div 42 43 class="dark:bg-dark-300 dark:shadow-dark-700 pointer-events-auto flex min-w-64 flex-col gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-3 shadow-md select-none dark:border-neutral-700"
+42 -15
src/components/search.tsx
··· 1 - import { Client, CredentialManager } from "@atcute/client"; 1 + import { Client, simpleFetchHandler } from "@atcute/client"; 2 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"; 3 + import { A, useNavigate } from "@solidjs/router"; 4 + import { 5 + createEffect, 6 + createResource, 7 + createSignal, 8 + For, 9 + onCleanup, 10 + onMount, 11 + Show, 12 + } from "solid-js"; 5 13 import { isTouchDevice } from "../layout"; 6 14 import { resolveLexiconAuthority } from "../utils/api"; 7 15 import { appHandleLink, appList, appName, AppUrl } from "../utils/app-urls"; ··· 38 46 39 47 if ((ev.ctrlKey || ev.metaKey) && ev.key == "k") { 40 48 ev.preventDefault(); 41 - setShowSearch(!showSearch()); 49 + 50 + if (showSearch()) { 51 + const searchInput = document.querySelector("#input") as HTMLInputElement; 52 + if (searchInput && document.activeElement !== searchInput) { 53 + searchInput.focus(); 54 + } else { 55 + setShowSearch(false); 56 + } 57 + } else { 58 + setShowSearch(true); 59 + } 42 60 } else if (ev.key == "Escape") { 43 61 ev.preventDefault(); 44 62 setShowSearch(false); ··· 67 85 const navigate = useNavigate(); 68 86 let searchInput!: HTMLInputElement; 69 87 const rpc = new Client({ 70 - handler: new CredentialManager({ service: "https://public.api.bsky.app" }), 88 + handler: simpleFetchHandler({ service: "https://public.api.bsky.app" }), 71 89 }); 72 90 73 91 onMount(() => { 74 - if (useLocation().pathname !== "/") searchInput.focus(); 75 - 76 92 const handlePaste = (e: ClipboardEvent) => { 77 93 if (e.target === searchInput) return; 78 94 if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return; ··· 83 99 84 100 window.addEventListener("paste", handlePaste); 85 101 onCleanup(() => window.removeEventListener("paste", handlePaste)); 102 + }); 103 + 104 + createEffect(() => { 105 + if (showSearch()) searchInput.focus(); 86 106 }); 87 107 88 108 const fetchTypeahead = async (input: string) => { ··· 111 131 const currentInput = input(); 112 132 if (!currentInput) return SEARCH_PREFIXES; 113 133 114 - const { prefix } = parsePrefix(currentInput); 115 - if (prefix) return []; 134 + const { prefix, query } = parsePrefix(currentInput); 135 + if (prefix && query.length > 0) return []; 116 136 117 137 return SEARCH_PREFIXES.filter((p) => p.prefix.startsWith(currentInput.toLowerCase())); 118 138 }; ··· 238 258 <Show when={input()} fallback={ListUrlsTooltip()}> 239 259 <button 240 260 type="button" 241 - class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-600 dark:active:bg-neutral-500" 261 + class="flex items-center rounded-md p-1 hover:bg-neutral-100 active:bg-neutral-200 dark:hover:bg-neutral-600 dark:active:bg-neutral-500" 242 262 onClick={() => setInput(undefined)} 243 263 > 244 264 <span class="iconify lucide--x"></span> ··· 255 275 {(prefixItem, index) => ( 256 276 <button 257 277 type="button" 258 - class={`flex items-center rounded-lg p-2 ${ 278 + class={`flex items-center rounded-md p-2 ${ 259 279 index() === selectedIndex() ? 260 280 "bg-neutral-200 dark:bg-neutral-700" 261 281 : "hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" ··· 280 300 const adjustedIndex = getPrefixSuggestions().length + index(); 281 301 return ( 282 302 <A 283 - class={`flex items-center gap-2 rounded-lg p-2 ${ 303 + class={`flex items-center gap-2 rounded-md p-2 ${ 284 304 adjustedIndex === selectedIndex() ? 285 305 "bg-neutral-200 dark:bg-neutral-700" 286 306 : "hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" ··· 290 310 > 291 311 <img 292 312 src={actor.avatar?.replace("img/avatar/", "img/avatar_thumbnail/")} 293 - class="size-8 rounded-full" 313 + class="size-9 rounded-full" 294 314 /> 295 - <span>{actor.handle}</span> 315 + <div class="flex flex-col"> 316 + <Show when={actor.displayName}> 317 + <span class="text-sm font-medium">{actor.displayName}</span> 318 + </Show> 319 + <span class="text-xs text-neutral-600 dark:text-neutral-400"> 320 + @{actor.handle} 321 + </span> 322 + </div> 296 323 </A> 297 324 ); 298 325 }} ··· 352 379 </Modal> 353 380 <button 354 381 type="button" 355 - class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-600 dark:active:bg-neutral-500" 382 + class="flex items-center rounded-md p-1 hover:bg-neutral-100 active:bg-neutral-200 dark:hover:bg-neutral-600 dark:active:bg-neutral-500" 356 383 onClick={() => setOpenList(true)} 357 384 > 358 385 <span class="iconify lucide--help-circle"></span>
+13 -8
src/components/theme.tsx
··· 24 24 else localStorage.theme = newTheme; 25 25 }; 26 26 27 - const ThemeButton = (props: { theme: string; icon: string }) => { 27 + const ThemeOption = (props: { theme: string; icon: string; label: string }) => { 28 28 return ( 29 29 <button 30 30 classList={{ 31 - "p-1.5 flex items-center rounded-full border-[0.5px]": true, 32 - "bg-neutral-200/60 border-neutral-300/60 dark:border-neutral-500/60 dark:bg-neutral-600": 31 + "flex items-center gap-2 rounded-xl border px-3 py-2": true, 32 + "bg-neutral-200/60 border-neutral-300 dark:border-neutral-500 dark:bg-neutral-700": 33 33 theme() === props.theme, 34 - "border-transparent": theme() !== props.theme, 34 + "border-neutral-200 dark:border-neutral-600 hover:bg-neutral-200/30 dark:hover:bg-neutral-800": 35 + theme() !== props.theme, 35 36 }} 36 37 onclick={() => updateTheme(props.theme)} 37 38 > 38 39 <span class={"iconify " + props.icon}></span> 40 + <span>{props.label}</span> 39 41 </button> 40 42 ); 41 43 }; 42 44 43 45 return ( 44 - <div class="dark:bg-dark-100 dark:inset-shadow-dark-200 mt-2 flex items-center justify-between gap-1 rounded-full border-[0.5px] border-neutral-200/60 bg-white p-1 text-base text-neutral-800 inset-shadow-sm dark:border-neutral-600 dark:text-neutral-300"> 45 - <ThemeButton theme="system" icon="lucide--monitor" /> 46 - <ThemeButton theme="light" icon="lucide--sun" /> 47 - <ThemeButton theme="dark" icon="lucide--moon" /> 46 + <div class="flex flex-col gap-0.5"> 47 + <label class="select-none">Theme</label> 48 + <div class="flex gap-2"> 49 + <ThemeOption theme="system" icon="lucide--monitor" label="System" /> 50 + <ThemeOption theme="light" icon="lucide--sun" label="Light" /> 51 + <ThemeOption theme="dark" icon="lucide--moon" label="Dark" /> 52 + </div> 48 53 </div> 49 54 ); 50 55 };
+7 -1
src/components/video-player.tsx
··· 22 22 }); 23 23 24 24 return ( 25 - <video ref={video} class="max-h-80 max-w-[20rem]" controls playsinline onLoadedData={props.onLoad}> 25 + <video 26 + ref={video} 27 + class="max-h-80 max-w-[20rem]" 28 + controls 29 + playsinline 30 + onLoadedData={props.onLoad} 31 + > 26 32 <source type="video/mp4" /> 27 33 </video> 28 34 );
+30 -26
src/layout.tsx
··· 1 1 import { Handle } from "@atcute/lexicons"; 2 2 import { Meta, MetaProvider } from "@solidjs/meta"; 3 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"; 4 + import { createEffect, ErrorBoundary, onCleanup, onMount, Show, Suspense } from "solid-js"; 5 + import { AccountManager } from "./auth/account.jsx"; 6 + import { hasUserScope } from "./auth/scope-utils"; 7 + import { agent } from "./auth/state.js"; 8 + import { RecordEditor } from "./components/create"; 7 9 import { DropdownMenu, MenuProvider, MenuSeparator, NavMenu } from "./components/dropdown.jsx"; 8 - import { agent } from "./components/login.jsx"; 9 10 import { NavBar } from "./components/navbar.jsx"; 10 11 import { NotificationContainer } from "./components/notification.jsx"; 11 12 import { Search, SearchButton, showSearch } from "./components/search.jsx"; 12 - import { themeEvent, ThemeSelection } from "./components/theme.jsx"; 13 + import { themeEvent } from "./components/theme.jsx"; 13 14 import { resolveHandle } from "./utils/api.js"; 14 15 15 16 export const isTouchDevice = "ontouchstart" in window || navigator.maxTouchPoints > 1; ··· 38 39 createEffect(async () => { 39 40 if (props.params.repo && !props.params.repo.startsWith("did:")) { 40 41 const did = await resolveHandle(props.params.repo as Handle); 41 - navigate(location.pathname.replace(props.params.repo, did)); 42 + navigate(location.pathname.replace(props.params.repo, did), { replace: true }); 42 43 } 43 44 }); 44 45 45 46 onMount(() => { 46 47 window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", themeEvent); 48 + 49 + const handleGoToRepo = (ev: KeyboardEvent) => { 50 + if (document.querySelector("[data-modal]")) return; 51 + if (ev.target instanceof HTMLInputElement || ev.target instanceof HTMLTextAreaElement) return; 52 + 53 + if (ev.key === "g" && agent()?.sub) { 54 + ev.preventDefault(); 55 + navigate(`/at://${agent()!.sub}`); 56 + } 57 + }; 58 + 59 + window.addEventListener("keydown", handleGoToRepo); 60 + onCleanup(() => window.removeEventListener("keydown", handleGoToRepo)); 47 61 48 62 if (localStorage.getItem("sailor") === "true") { 49 63 const style = document.createElement("style"); ··· 104 118 }); 105 119 106 120 return ( 107 - <div 108 - id="main" 109 - class="mx-auto mb-8 flex max-w-lg flex-col items-center p-4 text-neutral-900 dark:text-neutral-200" 110 - > 121 + <div id="main" class="mx-auto mb-8 flex max-w-lg flex-col items-center p-4"> 111 122 <MetaProvider> 112 123 <Show when={location.pathname !== "/"}> 113 124 <Meta name="robots" content="noindex, nofollow" /> ··· 131 142 <span>PDSls</span> 132 143 </A> 133 144 <div class="dark:bg-dark-300/60 relative flex items-center gap-0.5 rounded-lg bg-neutral-50/60 px-1 py-0.5"> 134 - <Show when={location.pathname !== "/"}> 135 - <SearchButton /> 136 - </Show> 137 - <Show when={agent()}> 145 + <SearchButton /> 146 + <Show when={hasUserScope("create")}> 138 147 <RecordEditor create={true} /> 139 148 </Show> 140 149 <AccountManager /> 141 150 <MenuProvider> 142 - <DropdownMenu 143 - icon="lucide--menu text-lg" 144 - buttonClass="rounded-lg p-1.5" 145 - menuClass="top-11 p-3 text-sm" 146 - > 147 - <NavMenu href="/jetstream" label="Jetstream" /> 148 - <NavMenu href="/firehose" label="Firehose" /> 149 - <NavMenu href="/labels" label="Labels" /> 150 - <NavMenu href="/settings" label="Settings" /> 151 + <DropdownMenu icon="lucide--menu text-lg" buttonClass="rounded-lg p-1.5"> 152 + <NavMenu href="/jetstream" label="Jetstream" icon="lucide--radio-tower" /> 153 + <NavMenu href="/firehose" label="Firehose" icon="lucide--droplet" /> 154 + <NavMenu href="/labels" label="Labels" icon="lucide--tags" /> 155 + <NavMenu href="/settings" label="Settings" icon="lucide--settings" /> 151 156 <MenuSeparator /> 152 157 <NavMenu 153 158 href="https://bsky.app/profile/did:plc:6q5daed5gutiyerimlrnojnz" 154 159 label="Bluesky" 160 + icon="simple-icons--bluesky text-[#0085ff]" 155 161 newTab 156 - external 157 162 /> 158 163 <NavMenu 159 164 href="https://tangled.org/@pdsls.dev/pdsls/" 160 165 label="Source" 166 + icon="lucide--code" 161 167 newTab 162 - external 163 168 /> 164 - <ThemeSelection /> 165 169 </DropdownMenu> 166 170 </MenuProvider> 167 171 </div>
+13 -3
src/styles/index.css
··· 6 6 7 7 @custom-variant dark (&:where(.dark, .dark *)); 8 8 9 + @font-face { 10 + font-family: "Figtree"; 11 + src: url("/fonts/Figtree[wght].woff2") format("woff2"); 12 + font-display: swap; 13 + } 14 + 9 15 @theme { 10 - --font-sans: "Inter", sans-serif; 16 + --font-sans: "Figtree", sans-serif; 11 17 --font-mono: "Roboto Mono", monospace; 12 18 --font-pecita: "Pecita", serif; 13 19 ··· 40 46 --svg: url("data:image/svg+xml,%3Csvg%20width%3D%22800%22%20height%3D%22800%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22m3%206%203.106-1.553a2%202%200%200%201%201.788%200l1.423.711a6%206%200%200%200%205.366%200l1.423-.71a2%202%200%200%201%201.788%200L21%206M3%2010.5l3.106-1.553a2%202%200%200%201%201.788%200l1.423.711a6%206%200%200%200%205.366%200l1.423-.71a2%202%200%200%201%201.788%200L21%2010.5M3%2015l3.106-1.553a2%202%200%200%201%201.788%200l1.423.711a6%206%200%200%200%205.366%200l1.423-.71a2%202%200%200%201%201.788%200L21%2015M3%2019.5l3.106-1.553a2%202%200%200%201%201.788%200l1.423.711a6%206%200%200%200%205.366%200l1.423-.71a2%202%200%200%201%201.788%200L21%2019.5%22%20stroke%3D%22%23ffe5ea%22%20stroke-width%3D%223%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E"); 41 47 } 42 48 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"); 49 + .simple-icons--bluesky { 50 + --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M5.202 2.857C7.954 4.922 10.913 9.11 12 11.358c1.087-2.247 4.046-6.436 6.798-8.501C20.783 1.366 24 .213 24 3.883c0 .732-.42 6.156-.667 7.037c-.856 3.061-3.978 3.842-6.755 3.37c4.854.826 6.089 3.562 3.422 6.299c-5.065 5.196-7.28-1.304-7.847-2.97c-.104-.305-.152-.448-.153-.327c0-.121-.05.022-.153.327c-.568 1.666-2.782 8.166-7.847 2.97c-2.667-2.737-1.432-5.473 3.422-6.3c-2.777.473-5.899-.308-6.755-3.369C.42 10.04 0 4.615 0 3.883c0-3.67 3.217-2.517 5.202-1.026'/%3E%3C/svg%3E"); 51 + } 52 + 53 + .i-leaflet { 54 + --svg: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%20fill%3D%22none%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M1.468%2010.977c-.55%201.061-.961%201.751-1.359%202.741a1.508%201.508%200%201%200%202.8%201.124l.227-.574v-.002c.28-.71.52-1.316.81-1.862.328-.018.702-.02%201.125-.023h.053c.77-.005%201.697-.01%202.497-.172s1.791-.545%202.229-1.57c.119-.278.239-.688.134-1.105h.151c.422%200%201.017.001%201.548-.143.62-.17%201.272-.569%201.558-1.41a1.52%201.52%200%200%200%20.034-.925l.084-.015.042-.007c.363-.063.849-.148%201.264-.304.404-.15%201.068-.488%201.267-1.262.113-.44.1-.908-.154-1.33a1.7%201.7%200%200%200-.36-.414c.112-.14.253-.333.35-.547.17-.371.257-.916-.089-1.45-.393-.604-1.066-.71-1.4-.737a6%206%200%200%200-.985.026%201.2%201.2%200%200%200-.156-.275c-.371-.496-.947-.538-1.272-.53-.655.018-1.167.31-1.538.61-.194.159-.657.806-.808.974%200-.603-.581-.91-.99-.973-.794-.123-1.285.388-1.742.973-.57.73-1.01%201.668-1.531%202.373-.18-.117-.393-.39-.733-.375-.56.026-.932.406-1.173.666-.419.452-.685%201.273-.867%201.885-.197.885-.332%201.258-.491%202.228a9.4%209.4%200%200%200-.144%201.677c-.109.213-.234.443-.381.728%22%20fill%3D%22%23639431%22%2F%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M4.714%204.78c.16.14.349.306.755.165.266-.093.61-.695.993-1.367.56-.982%201.205-2.114%201.816-2.02.738.114.693.523.658.837-.025.22-.044.394.216.387.264-.006.521-.317.82-.678.413-.498.904-1.092%201.602-1.11.492-.014.484.198.476.413-.005.138-.01.276.116.358.123.08.434.053.79.02.573-.052%201.265-.114%201.497.243.204.314-.056.626-.305.925-.21.254-.414.498-.321.726.076.186.231.291.383.394.25.168.491.33.361.834-.136.533-.96.677-1.732.812-.646.113-1.257.22-1.397.544-.088.203.058.297.222.403.195.127.415.27.292.633-.29.85-1.254.85-2.16.85-.777%200-1.51%200-1.735.537-.13.31.067.365.282.425.264.074.557.155.315.723-.464%201.087-2.195%201.096-3.78%201.105-.58.004-1.141.007-1.613.063a.18.18%200%200%200-.13.083c-.434.713-.742%201.496-1.07%202.332l-.221.559a.486.486%200%201%201-.903-.363c.373-.928.803-1.781%201.273-2.564.767-1.413%202.28-3.147%203.88-4.45%201.423-1.184%202.782-2.071%204.364-2.744.198-.084.139-.316-.068-.256-1.403.405-2.643%201.21-3.928%202.02-1.399.881-2.57%202.073-3.291%202.94-.127.153-.405.027-.365-.168.313-1.523.636-2.92%201.11-3.432.45-.485.603-.35.798-.18%22%20fill%3D%22%23d9ea72%22%2F%3E%3C%2Fsvg%3E"); 45 55 } 46 56 47 57 @keyframes slideIn {
-10
src/utils/app-urls.ts
··· 3 3 export enum App { 4 4 Bluesky, 5 5 Tangled, 6 - Whitewind, 7 6 Frontpage, 8 7 Pinksea, 9 8 Linkat, ··· 12 11 export const appName = { 13 12 [App.Bluesky]: "Bluesky", 14 13 [App.Tangled]: "Tangled", 15 - [App.Whitewind]: "Whitewind", 16 14 [App.Frontpage]: "Frontpage", 17 15 [App.Pinksea]: "Pinksea", 18 16 [App.Linkat]: "Linkat", ··· 29 27 "main.bsky.dev": App.Bluesky, 30 28 "social.daniela.lol": App.Bluesky, 31 29 "tangled.org": App.Tangled, 32 - "whtwnd.com": App.Whitewind, 33 30 "frontpage.fyi": App.Frontpage, 34 31 "pinksea.art": App.Pinksea, 35 32 "linkat.blue": App.Linkat, ··· 91 88 } 92 89 93 90 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 91 }, 102 92 [App.Frontpage]: (path) => { 103 93 if (path.length === 3) {
+15 -15
src/utils/hooks/debounced.ts
··· 1 - import { type Accessor, createEffect, createSignal, onCleanup } from 'solid-js'; 1 + import { type Accessor, createEffect, createSignal, onCleanup } from "solid-js"; 2 2 3 3 export const createDebouncedValue = <T>( 4 - accessor: Accessor<T>, 5 - delay: number, 6 - equals?: false | ((prev: T, next: T) => boolean), 4 + accessor: Accessor<T>, 5 + delay: number, 6 + equals?: false | ((prev: T, next: T) => boolean), 7 7 ): Accessor<T> => { 8 - const initial = accessor(); 9 - const [state, setState] = createSignal(initial, { equals }); 8 + const initial = accessor(); 9 + const [state, setState] = createSignal(initial, { equals }); 10 10 11 - createEffect((prev: T) => { 12 - const next = accessor(); 11 + createEffect((prev: T) => { 12 + const next = accessor(); 13 13 14 - if (prev !== next) { 15 - const timeout = setTimeout(() => setState(() => next), delay); 16 - onCleanup(() => clearTimeout(timeout)); 17 - } 14 + if (prev !== next) { 15 + const timeout = setTimeout(() => setState(() => next), delay); 16 + onCleanup(() => clearTimeout(timeout)); 17 + } 18 18 19 - return next; 20 - }, initial); 19 + return next; 20 + }, initial); 21 21 22 - return state; 22 + return state; 23 23 };
+30
src/utils/key.ts
··· 1 + import { parseDidKey, parsePublicMultikey } from "@atcute/crypto"; 2 + import { fromBase58Btc } from "@atcute/multibase"; 3 + 4 + export const detectKeyType = (key: string): string => { 5 + try { 6 + return parsePublicMultikey(key).type; 7 + } catch (e) { 8 + try { 9 + const bytes = fromBase58Btc(key.startsWith("z") ? key.slice(1) : key); 10 + if (bytes.length >= 2) { 11 + const type = (bytes[0] << 8) | bytes[1]; 12 + if (type === 0xed01) { 13 + return "ed25519"; 14 + } 15 + } 16 + } catch {} 17 + return "unknown"; 18 + } 19 + }; 20 + 21 + export const detectDidKeyType = (key: string): string => { 22 + try { 23 + return parseDidKey(key).type; 24 + } catch (e) { 25 + if (key.startsWith("did:key:")) { 26 + return detectKeyType(key.slice(8)); 27 + } 28 + return "unknown"; 29 + } 30 + };
+14 -8
src/utils/templates.ts
··· 6 6 "app.bsky.actor.profile": (uri) => ({ 7 7 label: "Bluesky", 8 8 link: `https://bsky.app/profile/${uri.repo}`, 9 - icon: "ri--bluesky", 9 + icon: "simple-icons--bluesky text-[#0085ff]", 10 10 }), 11 11 "app.bsky.feed.post": (uri) => ({ 12 12 label: "Bluesky", 13 13 link: `https://bsky.app/profile/${uri.repo}/post/${uri.rkey}`, 14 - icon: "ri--bluesky", 14 + icon: "simple-icons--bluesky text-[#0085ff]", 15 15 }), 16 16 "app.bsky.graph.list": (uri) => ({ 17 17 label: "Bluesky", 18 18 link: `https://bsky.app/profile/${uri.repo}/lists/${uri.rkey}`, 19 - icon: "ri--bluesky", 19 + icon: "simple-icons--bluesky text-[#0085ff]", 20 20 }), 21 21 "app.bsky.feed.generator": (uri) => ({ 22 22 label: "Bluesky", 23 23 link: `https://bsky.app/profile/${uri.repo}/feed/${uri.rkey}`, 24 - icon: "ri--bluesky", 24 + icon: "simple-icons--bluesky text-[#0085ff]", 25 25 }), 26 26 "fyi.unravel.frontpage.post": (uri) => ({ 27 27 label: "Frontpage", 28 28 link: `https://frontpage.fyi/post/${uri.repo}/${uri.rkey}`, 29 29 }), 30 - "com.whtwnd.blog.entry": (uri) => ({ 31 - label: "WhiteWind", 32 - link: `https://whtwnd.com/${uri.repo}/${uri.rkey}`, 33 - }), 34 30 "com.shinolabs.pinksea.oekaki": (uri) => ({ 35 31 label: "PinkSea", 36 32 link: `https://pinksea.art/${uri.repo}/oekaki/${uri.rkey}`, ··· 54 50 label: "Tangled", 55 51 link: `https://tangled.org/${uri.repo}/${record.name}`, 56 52 icon: "i-tangled", 53 + }), 54 + "pub.leaflet.document": (uri) => ({ 55 + label: "Leaflet", 56 + link: `https://leaflet.pub/p/${uri.repo}/${uri.rkey}`, 57 + icon: "iconify-color i-leaflet", 58 + }), 59 + "pub.leaflet.publication": (uri) => ({ 60 + label: "Leaflet", 61 + link: `https://leaflet.pub/lish/${uri.repo}/${uri.rkey}`, 62 + icon: "iconify-color i-leaflet", 57 63 }), 58 64 };
+4 -4
src/views/blob.tsx
··· 1 - import { Client, CredentialManager } from "@atcute/client"; 1 + import { Client, simpleFetchHandler } from "@atcute/client"; 2 2 import { createResource, createSignal, For, Show } from "solid-js"; 3 3 import { Button } from "../components/button"; 4 4 ··· 9 9 let rpc: Client; 10 10 11 11 const fetchBlobs = async () => { 12 - if (!rpc) rpc = new Client({ handler: new CredentialManager({ service: props.pds }) }); 12 + if (!rpc) rpc = new Client({ handler: simpleFetchHandler({ service: props.pds }) }); 13 13 const res = await rpc.get("com.atproto.sync.listBlobs", { 14 14 params: { 15 15 did: props.repo as `did:${string}:${string}`, ··· 30 30 return ( 31 31 <div class="flex flex-col items-center gap-2"> 32 32 <Show when={blobs() || response()}> 33 - <div class="flex flex-col gap-0.5 font-mono text-sm wrap-anywhere lg:break-normal"> 33 + <div class="flex w-full flex-col gap-0.5 font-mono text-xs wrap-anywhere"> 34 34 <For each={blobs()}> 35 35 {(cid) => ( 36 36 <a 37 37 href={`${props.pds}/xrpc/com.atproto.sync.getBlob?did=${props.repo}&cid=${cid}`} 38 38 target="_blank" 39 - class="rounded px-0.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 39 + class="w-fit rounded px-0.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 40 40 > 41 41 <span class="text-blue-400">{cid}</span> 42 42 </a>
+35 -34
src/views/collection.tsx
··· 1 1 import { ComAtprotoRepoApplyWrites, ComAtprotoRepoGetRecord } from "@atcute/atproto"; 2 - import { Client, CredentialManager } from "@atcute/client"; 2 + import { Client, simpleFetchHandler } from "@atcute/client"; 3 3 import { $type, ActorIdentifier, InferXRPCBodyOutput } from "@atcute/lexicons"; 4 4 import * as TID from "@atcute/tid"; 5 5 import { A, useParams } from "@solidjs/router"; 6 - import { createEffect, createResource, createSignal, For, Show, untrack } from "solid-js"; 6 + import { createEffect, createMemo, createResource, createSignal, For, Show } from "solid-js"; 7 7 import { createStore } from "solid-js/store"; 8 + import { hasUserScope } from "../auth/scope-utils"; 9 + import { agent } from "../auth/state"; 8 10 import { Button } from "../components/button.jsx"; 9 11 import { JSONType, JSONValue } from "../components/json.jsx"; 10 - import { agent } from "../components/login.jsx"; 11 12 import { Modal } from "../components/modal.jsx"; 12 13 import { addNotification, removeNotification } from "../components/notification.jsx"; 13 14 import { StickyOverlay } from "../components/sticky.jsx"; ··· 88 89 89 90 const fetchRecords = async () => { 90 91 if (!pds) pds = await resolvePDS(did!); 91 - if (!rpc) rpc = new Client({ handler: new CredentialManager({ service: pds }) }); 92 + if (!rpc) rpc = new Client({ handler: simpleFetchHandler({ service: pds }) }); 92 93 const res = await rpc.get("com.atproto.repo.listRecords", { 93 94 params: { 94 95 repo: did as ActorIdentifier, ··· 117 118 118 119 const [response, { refetch }] = createResource(fetchRecords); 119 120 121 + const filteredRecords = createMemo(() => 122 + records.filter((rec) => 123 + filter() ? JSON.stringify(rec.record.value).includes(filter()!) : true, 124 + ), 125 + ); 126 + 120 127 const deleteRecords = async () => { 121 128 const recsToDel = records.filter((record) => record.toDelete); 122 129 let writes: Array< ··· 192 199 <StickyOverlay> 193 200 <div class="flex w-full flex-col gap-2"> 194 201 <div class="flex items-center gap-1"> 195 - <Show when={agent() && agent()?.sub === did}> 202 + <Show when={agent() && agent()?.sub === did && hasUserScope("delete")}> 196 203 <div class="flex items-center"> 197 204 <Tooltip 198 205 text={batchDelete() ? "Cancel" : "Delete"} 199 206 children={ 200 207 <button 201 208 onclick={() => { 202 - setRecords( 203 - { from: 0, to: untrack(() => records.length) - 1 }, 204 - "toDelete", 205 - false, 206 - ); 209 + setRecords({ from: 0, to: records.length - 1 }, "toDelete", false); 207 210 setLastSelected(undefined); 208 211 setBatchDelete(!batchDelete()); 209 212 }} 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" 213 + class="flex items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 211 214 > 212 215 <span 213 216 class={`iconify text-lg ${batchDelete() ? "lucide--circle-x" : "lucide--trash-2"} `} ··· 221 224 children={ 222 225 <button 223 226 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" 227 + class="flex items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 225 228 > 226 229 <span class="iconify lucide--copy-check text-lg"></span> 227 230 </button> 228 231 } 229 232 /> 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 - /> 233 + <Show when={hasUserScope("create")}> 234 + <Tooltip 235 + text="Recreate" 236 + children={ 237 + <button 238 + onclick={() => { 239 + setRecreate(true); 240 + setOpenDelete(true); 241 + }} 242 + class="flex items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 243 + > 244 + <span class="iconify lucide--recycle text-lg text-green-500 dark:text-green-400"></span> 245 + </button> 246 + } 247 + /> 248 + </Show> 244 249 <Tooltip 245 250 text="Delete" 246 251 children={ ··· 249 254 setRecreate(false); 250 255 setOpenDelete(true); 251 256 }} 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" 257 + class="flex items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 253 258 > 254 259 <span class="iconify lucide--trash-2 text-lg text-red-500 dark:text-red-400"></span> 255 260 </button> ··· 278 283 <Tooltip text="Jetstream"> 279 284 <A 280 285 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" 286 + class="flex items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 282 287 > 283 288 <span class="iconify lucide--radio-tower text-lg"></span> 284 289 </A> ··· 310 315 <span>{records.filter((rec) => rec.toDelete).length}</span> 311 316 <span>/</span> 312 317 </Show> 313 - <span>{records.length} records</span> 318 + <span>{filter() ? filteredRecords().length : records.length} records</span> 314 319 </div> 315 320 <div class="flex w-20 items-center justify-end"> 316 321 <Show when={cursor()}> ··· 327 332 </div> 328 333 </StickyOverlay> 329 334 <div class="flex max-w-full flex-col px-2 font-mono"> 330 - <For 331 - each={records.filter((rec) => 332 - filter() ? JSON.stringify(rec.record.value).includes(filter()!) : true, 333 - )} 334 - > 335 + <For each={filteredRecords()}> 335 336 {(record, index) => ( 336 337 <> 337 338 <Show when={batchDelete()}>
+19 -21
src/views/labels.tsx
··· 1 1 import { ComAtprotoLabelDefs } from "@atcute/atproto"; 2 - import { Client, CredentialManager } from "@atcute/client"; 2 + import { Client, simpleFetchHandler } from "@atcute/client"; 3 3 import { isAtprotoDid } from "@atcute/identity"; 4 4 import { Handle } from "@atcute/lexicons"; 5 5 import { A, useSearchParams } from "@solidjs/router"; ··· 17 17 18 18 return ( 19 19 <div class="flex flex-col gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-3 dark:border-neutral-700 dark:bg-neutral-800"> 20 - <div class="flex flex-wrap items-center gap-x-2 gap-y-2"> 21 - <div class="inline-flex items-center gap-x-1 text-sm font-medium"> 22 - <span class="iconify lucide--tag shrink-0" /> 23 - {label.val} 24 - </div> 25 - <Show when={label.neg}> 26 - <div class="inline-flex items-center gap-x-1 text-xs font-medium text-red-500 dark:text-red-400"> 27 - <span>negated</span> 28 - </div> 29 - </Show> 30 - <div class="flex flex-wrap gap-3 text-xs text-neutral-600 dark:text-neutral-400"> 31 - <span>{localDateFromTimestamp(new Date(label.cts).getTime())}</span> 32 - <Show when={label.exp}> 33 - {(exp) => ( 34 - <div class="flex items-center gap-x-1"> 35 - <span class="iconify lucide--clock-fading shrink-0" /> 36 - <span>{localDateFromTimestamp(new Date(exp()).getTime())}</span> 37 - </div> 38 - )} 20 + <div class="flex gap-1 text-sm"> 21 + <span class="iconify lucide--tag shrink-0 self-center" /> 22 + <div class="flex flex-wrap items-baseline gap-2"> 23 + <span class="font-medium">{label.val}</span> 24 + <Show when={label.neg}> 25 + <span class="text-xs font-medium text-red-500 dark:text-red-400">negated</span> 39 26 </Show> 27 + <div class="flex flex-wrap gap-2 text-xs text-neutral-600 dark:text-neutral-400"> 28 + <span>{localDateFromTimestamp(new Date(label.cts).getTime())}</span> 29 + <Show when={label.exp}> 30 + {(exp) => ( 31 + <div class="flex items-center gap-x-1"> 32 + <span class="iconify lucide--clock-fading shrink-0" /> 33 + <span>{localDateFromTimestamp(new Date(exp()).getTime())}</span> 34 + </div> 35 + )} 36 + </Show> 37 + </div> 40 38 </div> 41 39 </div> 42 40 ··· 160 158 await resolvePDS(did); 161 159 if (!labelerCache[did]) throw new Error("Repository is not a labeler"); 162 160 rpc = new Client({ 163 - handler: new CredentialManager({ service: labelerCache[did] }), 161 + handler: simpleFetchHandler({ service: labelerCache[did] }), 164 162 }); 165 163 166 164 setSearchParams({ did, uriPatterns });
+46 -40
src/views/pds.tsx
··· 1 1 import { ComAtprotoServerDescribeServer, ComAtprotoSyncListRepos } from "@atcute/atproto"; 2 - import { Client, CredentialManager } from "@atcute/client"; 2 + import { Client, simpleFetchHandler } from "@atcute/client"; 3 3 import { InferXRPCBodyOutput } from "@atcute/lexicons"; 4 4 import * as TID from "@atcute/tid"; 5 5 import { A, useLocation, useParams } from "@solidjs/router"; ··· 23 23 setPDS(params.pds); 24 24 const pds = 25 25 params.pds!.startsWith("localhost") ? `http://${params.pds}` : `https://${params.pds}`; 26 - const rpc = new Client({ handler: new CredentialManager({ service: pds }) }); 26 + const rpc = new Client({ handler: simpleFetchHandler({ service: pds }) }); 27 27 28 28 const getVersion = async () => { 29 29 // @ts-expect-error: undocumented endpoint ··· 56 56 const [openInfo, setOpenInfo] = createSignal(false); 57 57 58 58 return ( 59 - <div class="flex items-center"> 59 + <div class="flex items-center gap-0.5"> 60 60 <A 61 61 href={`/at://${repo.did}`} 62 - class="grow truncate rounded py-0.5 font-mono hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 62 + class="grow truncate rounded-md p-0.5 font-mono hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 63 63 > 64 64 {repo.did} 65 65 </A> ··· 70 70 </Show> 71 71 <button 72 72 onclick={() => setOpenInfo(true)} 73 - class="flex items-center rounded-lg p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 73 + class="flex items-center rounded-md p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 74 74 > 75 75 <span class="iconify lucide--info"></span> 76 76 </button> 77 77 <Modal open={openInfo()} onClose={() => setOpenInfo(false)}> 78 - <div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-70 left-[50%] w-max max-w-full -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-3 wrap-break-word shadow-md transition-opacity duration-200 sm:max-w-lg dark:border-neutral-700 starting:opacity-0"> 79 - <div class="mb-1 flex justify-between gap-2"> 80 - <div class="flex items-center gap-1"> 81 - <span class="iconify lucide--info"></span> 82 - <span class="font-semibold">{repo.did}</span> 83 - </div> 78 + <div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-70 left-[50%] w-max max-w-[90vw] -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-white p-3 shadow-md transition-opacity duration-200 sm:max-w-xl dark:border-neutral-700 starting:opacity-0"> 79 + <div class="mb-2 flex items-center justify-between gap-4"> 80 + <p class="truncate font-semibold">{repo.did}</p> 84 81 <button 85 82 onclick={() => setOpenInfo(false)} 86 - class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 83 + class="flex shrink-0 items-center rounded-md p-1.5 text-neutral-500 hover:bg-neutral-100 hover:text-neutral-700 active:bg-neutral-200 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:hover:text-neutral-200 dark:active:bg-neutral-600" 87 84 > 88 85 <span class="iconify lucide--x"></span> 89 86 </button> 90 87 </div> 91 - <div class="flex flex-col text-sm"> 92 - <span> 93 - Head: <span class="text-xs">{repo.head}</span> 94 - </span> 88 + <div class="grid grid-cols-[auto_1fr] items-baseline gap-x-1 gap-y-0.5 text-sm"> 89 + <span class="font-medium">Head:</span> 90 + <span class="wrap-anywhere text-neutral-700 dark:text-neutral-300">{repo.head}</span> 91 + 95 92 <Show when={TID.validate(repo.rev)}> 96 - <span> 97 - Rev: {repo.rev} ({localDateFromTimestamp(TID.parse(repo.rev).timestamp / 1000)}) 98 - </span> 93 + <span class="font-medium">Rev:</span> 94 + <div class="flex gap-1"> 95 + <span class="text-neutral-700 dark:text-neutral-300">{repo.rev}</span> 96 + <span class="text-neutral-600 dark:text-neutral-400">ยท</span> 97 + <span class="text-neutral-600 dark:text-neutral-400"> 98 + {localDateFromTimestamp(TID.parse(repo.rev).timestamp / 1000)} 99 + </span> 100 + </div> 99 101 </Show> 102 + 100 103 <Show when={repo.active !== undefined}> 101 - <span>Active: {repo.active ? "true" : "false"}</span> 104 + <span class="font-medium">Active:</span> 105 + <span 106 + class={`iconify self-center ${ 107 + repo.active ? 108 + "lucide--check text-green-500 dark:text-green-400" 109 + : "lucide--x text-red-500 dark:text-red-400" 110 + }`} 111 + ></span> 102 112 </Show> 113 + 103 114 <Show when={repo.status}> 104 - <span>Status: {repo.status}</span> 115 + <span class="font-medium">Status:</span> 116 + <span class="text-neutral-700 dark:text-neutral-300">{repo.status}</span> 105 117 </Show> 106 118 </div> 107 119 </div> ··· 111 123 }; 112 124 113 125 const Tab = (props: { tab: "repos" | "info"; label: string }) => ( 114 - <div class="flex items-center gap-0.5"> 115 - <A 116 - classList={{ 117 - "flex items-center gap-1 border-b-2": true, 118 - "border-transparent hover:border-neutral-400 dark:hover:border-neutral-600": 119 - (!!location.hash && location.hash !== `#${props.tab}`) || 120 - (!location.hash && props.tab !== "repos"), 121 - }} 122 - href={`/${params.pds}#${props.tab}`} 123 - > 124 - {props.label} 125 - </A> 126 - </div> 126 + <A 127 + classList={{ 128 + "border-b-2": true, 129 + "border-transparent hover:border-neutral-400 dark:hover:border-neutral-600": 130 + (!!location.hash && location.hash !== `#${props.tab}`) || 131 + (!location.hash && props.tab !== "repos"), 132 + }} 133 + href={`/${params.pds}#${props.tab}`} 134 + > 135 + {props.label} 136 + </A> 127 137 ); 128 138 129 139 return ( 130 140 <Show when={repos() || response()}> 131 141 <div class="flex w-full flex-col"> 132 142 <div class="dark:shadow-dark-700 dark:bg-dark-300 mb-2 flex w-full justify-between rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 text-sm shadow-xs dark:border-neutral-700"> 133 - <div class="ml-1 flex gap-3"> 143 + <div class="ml-1 flex items-center gap-3"> 134 144 <Tab tab="repos" label="Repositories" /> 135 145 <Tab tab="info" label="Info" /> 136 146 </div> 137 147 <MenuProvider> 138 - <DropdownMenu 139 - icon="lucide--ellipsis-vertical" 140 - buttonClass="rounded-sm p-1.5" 141 - menuClass="top-9 p-2 text-sm" 142 - > 148 + <DropdownMenu icon="lucide--ellipsis-vertical" buttonClass="rounded-sm p-1.5"> 143 149 <CopyMenu content={params.pds!} label="Copy PDS" icon="lucide--copy" /> 144 150 <NavMenu 145 151 href={`/firehose?instance=wss://${params.pds}`}
+35 -34
src/views/record.tsx
··· 1 - import { Client, CredentialManager } from "@atcute/client"; 1 + import { Client, simpleFetchHandler } from "@atcute/client"; 2 2 import { DidDocument, getPdsEndpoint } from "@atcute/identity"; 3 3 import { lexiconDoc } from "@atcute/lexicon-doc"; 4 4 import { RecordValidator } from "@atcute/lexicon-doc/validations"; ··· 8 8 import { verifyRecord } from "@atcute/repo"; 9 9 import { A, useLocation, useNavigate, useParams } from "@solidjs/router"; 10 10 import { createResource, createSignal, ErrorBoundary, Show, Suspense } from "solid-js"; 11 + import { hasUserScope } from "../auth/scope-utils"; 12 + import { agent } from "../auth/state"; 11 13 import { Backlinks } from "../components/backlinks.jsx"; 12 14 import { Button } from "../components/button.jsx"; 13 - import { RecordEditor, setPlaceholder } from "../components/create.jsx"; 15 + import { RecordEditor, setPlaceholder } from "../components/create"; 14 16 import { 15 17 CopyMenu, 16 18 DropdownMenu, ··· 20 22 } from "../components/dropdown.jsx"; 21 23 import { JSONValue } from "../components/json.jsx"; 22 24 import { LexiconSchemaView } from "../components/lexicon-schema.jsx"; 23 - import { agent } from "../components/login.jsx"; 24 25 import { Modal } from "../components/modal.jsx"; 25 26 import { pds } from "../components/navbar.jsx"; 26 27 import { addNotification, removeNotification } from "../components/notification.jsx"; ··· 67 68 }); 68 69 } 69 70 70 - const rpc = new Client({ handler: new CredentialManager({ service: pdsEndpoint }) }); 71 + const rpc = new Client({ handler: simpleFetchHandler({ service: pdsEndpoint }) }); 71 72 const response = await rpc.get("com.atproto.repo.getRecord", { 72 73 params: { 73 74 repo: authority, ··· 207 208 setValidSchema(undefined); 208 209 setLexiconUri(undefined); 209 210 const pds = await resolvePDS(did!); 210 - rpc = new Client({ handler: new CredentialManager({ service: pds }) }); 211 + rpc = new Client({ handler: simpleFetchHandler({ service: pds }) }); 211 212 const res = await rpc.get("com.atproto.repo.getRecord", { 212 213 params: { 213 214 repo: did as ActorIdentifier, ··· 362 363 <div class="flex items-center gap-0.5"> 363 364 <A 364 365 classList={{ 365 - "flex items-center gap-1 border-b-2": true, 366 + "border-b-2": true, 366 367 "border-transparent hover:border-neutral-400 dark:hover:border-neutral-600": 367 368 !isActive(), 368 369 }} ··· 381 382 <Show when={record()} keyed> 382 383 <div class="flex w-full flex-col items-center"> 383 384 <div class="dark:shadow-dark-700 dark:bg-dark-300 mb-3 flex w-full justify-between rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 text-sm shadow-xs dark:border-neutral-700"> 384 - <div class="ml-1 flex gap-3"> 385 + <div class="ml-1 flex items-center gap-3"> 385 386 <RecordTab tab="record" label="Record" /> 386 387 <RecordTab tab="schema" label="Schema" /> 387 388 <RecordTab tab="backlinks" label="Backlinks" /> ··· 389 390 </div> 390 391 <div class="flex gap-0.5"> 391 392 <Show when={agent() && agent()?.sub === record()?.uri.split("/")[2]}> 392 - <RecordEditor create={false} record={record()?.value} refetch={refetch} /> 393 - <Tooltip text="Delete"> 394 - <button 395 - class="flex items-center rounded-sm p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 396 - onclick={() => setOpenDelete(true)} 397 - > 398 - <span class="iconify lucide--trash-2"></span> 399 - </button> 400 - </Tooltip> 401 - <Modal open={openDelete()} onClose={() => setOpenDelete(false)}> 402 - <div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-70 left-[50%] -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-opacity duration-200 dark:border-neutral-700 starting:opacity-0"> 403 - <h2 class="mb-2 font-semibold">Delete this record?</h2> 404 - <div class="flex justify-end gap-2"> 405 - <Button onClick={() => setOpenDelete(false)}>Cancel</Button> 406 - <Button 407 - onClick={deleteRecord} 408 - class="dark:shadow-dark-700 rounded-lg bg-red-500 px-2 py-1.5 text-xs text-white shadow-xs select-none hover:bg-red-400 active:bg-red-400" 409 - > 410 - Delete 411 - </Button> 393 + <Show when={hasUserScope("update")}> 394 + <RecordEditor create={false} record={record()?.value} refetch={refetch} /> 395 + </Show> 396 + <Show when={hasUserScope("delete")}> 397 + <Tooltip text="Delete"> 398 + <button 399 + class="flex items-center rounded-sm p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 400 + onclick={() => setOpenDelete(true)} 401 + > 402 + <span class="iconify lucide--trash-2"></span> 403 + </button> 404 + </Tooltip> 405 + <Modal open={openDelete()} onClose={() => setOpenDelete(false)}> 406 + <div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-70 left-[50%] -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-opacity duration-200 dark:border-neutral-700 starting:opacity-0"> 407 + <h2 class="mb-2 font-semibold">Delete this record?</h2> 408 + <div class="flex justify-end gap-2"> 409 + <Button onClick={() => setOpenDelete(false)}>Cancel</Button> 410 + <Button 411 + onClick={deleteRecord} 412 + class="dark:shadow-dark-700 rounded-lg bg-red-500 px-2 py-1.5 text-xs text-white shadow-xs select-none hover:bg-red-400 active:bg-red-400" 413 + > 414 + Delete 415 + </Button> 416 + </div> 412 417 </div> 413 - </div> 414 - </Modal> 418 + </Modal> 419 + </Show> 415 420 </Show> 416 421 <MenuProvider> 417 - <DropdownMenu 418 - icon="lucide--ellipsis-vertical" 419 - buttonClass="rounded-sm p-1.5" 420 - menuClass="top-9 p-2 text-sm" 421 - > 422 + <DropdownMenu icon="lucide--ellipsis-vertical" buttonClass="rounded-sm p-1.5"> 422 423 <CopyMenu 423 424 content={JSON.stringify(record()?.value, null, 2)} 424 425 label="Copy record"
+51 -55
src/views/repo.tsx
··· 1 - import { Client, CredentialManager } from "@atcute/client"; 2 - import { parseDidKey, parsePublicMultikey } from "@atcute/crypto"; 1 + import { Client, simpleFetchHandler } from "@atcute/client"; 3 2 import { DidDocument } from "@atcute/identity"; 4 3 import { ActorIdentifier, Did, Handle, Nsid } from "@atcute/lexicons"; 5 4 import { A, useLocation, useNavigate, useParams } from "@solidjs/router"; ··· 39 38 resolvePDS, 40 39 validateHandle, 41 40 } from "../utils/api.js"; 41 + import { detectDidKeyType, detectKeyType } from "../utils/key.js"; 42 42 import { BlobView } from "./blob.jsx"; 43 43 import { PlcLogView } from "./logs.jsx"; 44 44 ··· 86 86 }; 87 87 88 88 return ( 89 - <A class="flex items-center" href={`/at://${params.repo}#${props.tab}`}> 90 - <span 91 - classList={{ 92 - "flex items-center border-b-2": true, 93 - "border-transparent hover:border-neutral-400 dark:hover:border-neutral-600": 94 - !isActive(), 95 - }} 96 - > 97 - {props.label} 98 - </span> 89 + <A 90 + classList={{ 91 + "border-b-2": true, 92 + "border-transparent hover:border-neutral-400 dark:hover:border-neutral-600": !isActive(), 93 + }} 94 + href={`/at://${params.repo}#${props.tab}`} 95 + > 96 + {props.label} 99 97 </A> 100 98 ); 101 99 }; ··· 115 113 if (!did.startsWith("did:")) { 116 114 try { 117 115 const did = await resolveHandle(params.repo as Handle); 118 - navigate(location.pathname.replace(params.repo!, did)); 116 + navigate(location.pathname.replace(params.repo!, did), { replace: true }); 119 117 return; 120 118 } catch { 121 119 try { 122 120 const nsid = params.repo as Nsid; 123 121 const res = await resolveLexiconAuthority(nsid); 124 - navigate(`/at://${res}/com.atproto.lexicon.schema/${nsid}`); 122 + navigate(`/at://${res}/com.atproto.lexicon.schema/${nsid}`, { replace: true }); 125 123 return; 126 124 } catch { 127 - navigate(`/${did}`); 125 + navigate(`/${did}`, { replace: true }); 128 126 return; 129 127 } 130 128 } ··· 141 139 return {}; 142 140 } 143 141 144 - rpc = new Client({ handler: new CredentialManager({ service: pds }) }); 145 - const res = await rpc.get("com.atproto.repo.describeRepo", { 146 - params: { repo: did as ActorIdentifier }, 147 - }); 148 - if (res.ok) { 149 - const collections: Record<string, { hidden: boolean; nsids: string[] }> = {}; 150 - res.data.collections.forEach((c) => { 151 - const nsid = c.split("."); 152 - if (nsid.length > 2) { 153 - const authority = `${nsid[0]}.${nsid[1]}`; 154 - collections[authority] = { 155 - nsids: (collections[authority]?.nsids ?? []).concat(nsid.slice(2).join(".")), 156 - hidden: false, 157 - }; 158 - } 142 + rpc = new Client({ handler: simpleFetchHandler({ service: pds }) }); 143 + try { 144 + const res = await rpc.get("com.atproto.repo.describeRepo", { 145 + params: { repo: did as ActorIdentifier }, 159 146 }); 160 - setNsids(collections); 161 - } else { 162 - console.error(res.data.error); 163 - switch (res.data.error) { 164 - case "RepoDeactivated": 165 - setError("Deactivated"); 166 - break; 167 - case "RepoTakendown": 168 - setError("Takendown"); 169 - break; 170 - default: 171 - setError("Unreachable"); 147 + if (res.ok) { 148 + const collections: Record<string, { hidden: boolean; nsids: string[] }> = {}; 149 + res.data.collections.forEach((c) => { 150 + const nsid = c.split("."); 151 + if (nsid.length > 2) { 152 + const authority = `${nsid[0]}.${nsid[1]}`; 153 + collections[authority] = { 154 + nsids: (collections[authority]?.nsids ?? []).concat(nsid.slice(2).join(".")), 155 + hidden: false, 156 + }; 157 + } 158 + }); 159 + setNsids(collections); 160 + } else { 161 + console.error(res.data.error); 162 + switch (res.data.error) { 163 + case "RepoDeactivated": 164 + setError("Deactivated"); 165 + break; 166 + case "RepoTakendown": 167 + setError("Takendown"); 168 + break; 169 + default: 170 + setError("Unreachable"); 171 + } 172 172 } 173 - } 174 173 175 - return res.data; 174 + return res.data; 175 + } catch { 176 + return {}; 177 + } 176 178 }; 177 179 178 180 const [repo] = createResource(fetchRepo); ··· 274 276 <Show when={repo()}> 275 277 <div class="flex w-full flex-col gap-3 wrap-break-word"> 276 278 <div class="dark:shadow-dark-700 dark:bg-dark-300 flex justify-between rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 text-sm shadow-xs dark:border-neutral-700"> 277 - <div class="ml-1 flex gap-2 text-xs sm:gap-4 sm:text-sm"> 279 + <div class="ml-1 flex items-center gap-2 text-xs sm:gap-4 sm:text-sm"> 278 280 <Show when={!error()}> 279 281 <RepoTab tab="collections" label="Collections" /> 280 282 </Show> ··· 305 307 </Tooltip> 306 308 </Show> 307 309 <MenuProvider> 308 - <DropdownMenu 309 - icon="lucide--ellipsis-vertical" 310 - buttonClass="rounded-sm p-1.5" 311 - menuClass="top-9 p-2 text-sm" 312 - > 310 + <DropdownMenu icon="lucide--ellipsis-vertical" buttonClass="rounded-sm p-1.5"> 313 311 <CopyMenu content={params.repo!} label="Copy DID" icon="lucide--copy" /> 314 312 <NavMenu 315 313 href={`/jetstream?dids=${params.repo}`} ··· 480 478 <Show when={location.hash === "#identity" || (error() && !location.hash)}> 481 479 <Show when={didDoc()}> 482 480 {(didDocument) => ( 483 - <div class="flex flex-col gap-2 wrap-anywhere"> 481 + <div class="flex flex-col gap-3 wrap-anywhere"> 484 482 {/* ID Section */} 485 483 <div> 486 484 <div class="flex items-center gap-1"> ··· 572 570 #{verif.id.split("#")[1]} 573 571 </span> 574 572 <span class="rounded bg-neutral-200 px-1 py-0.5 text-xs text-neutral-800 dark:bg-neutral-700 dark:text-neutral-300"> 575 - <ErrorBoundary fallback={<>unknown</>}> 576 - {parsePublicMultikey(key()).type} 577 - </ErrorBoundary> 573 + {detectKeyType(key())} 578 574 </span> 579 575 </div> 580 576 <div class="font-mono break-all">{key()}</div> ··· 598 594 {(key) => ( 599 595 <div class="text-sm"> 600 596 <span class="rounded bg-neutral-200 px-1 py-0.5 text-xs text-neutral-800 dark:bg-neutral-700 dark:text-neutral-300"> 601 - {parseDidKey(key).type} 597 + {detectDidKeyType(key)} 602 598 </span> 603 599 <div class="font-mono break-all">{key.replace("did:key:", "")}</div> 604 600 </div>
+3 -1
src/views/settings.tsx
··· 1 1 import { createSignal } from "solid-js"; 2 2 import { TextInput } from "../components/text-input.jsx"; 3 + import { ThemeSelection } from "../components/theme.jsx"; 3 4 4 5 export const [hideMedia, setHideMedia] = createSignal(localStorage.hideMedia === "true"); 5 6 ··· 9 10 <div class="flex items-center gap-1 font-semibold"> 10 11 <span>Settings</span> 11 12 </div> 12 - <div class="flex flex-col gap-2"> 13 + <div class="flex flex-col gap-3"> 13 14 <div class="flex flex-col gap-0.5"> 14 15 <label for="plcDirectory" class="select-none"> 15 16 PLC Directory ··· 24 25 }} 25 26 /> 26 27 </div> 28 + <ThemeSelection /> 27 29 <div class="flex justify-between"> 28 30 <div class="flex items-center gap-1"> 29 31 <input