Compare changes

Choose any two refs to compare.

+22
.tangled/workflows/deploy.yml
··· 1 + ## need this for commit idk what else to change 2 + 3 + when: 4 + - event: ["push"] 5 + branch: ["main"] 6 + 7 + engine: "nixery" 8 + 9 + clone: 10 + skip: true 11 + 12 + dependencies: 13 + nixpkgs: 14 + - curl 15 + 16 + steps: 17 + - name: "Trigger Deploy" 18 + command: | 19 + curl -X POST \ 20 + -H "Authorization: Bearer $SCANS_HOST_API_KEY" \ 21 + -H "Authorization: Bearer $SCANS_HOST_API_KEY" \ 22 + https://free.scan.blue/api/v1/sites/jy35AeguTwaqDy_3ufq09/deploy?wait=true
+25
0001-ok.patch
··· 1 + From baf405c82fb23f9274a35384286bac2b901d45af Mon Sep 17 00:00:00 2001 2 + From: scanash00 <scan@scanash.com> 3 + Date: Tue, 30 Dec 2025 22:12:13 -0900 4 + Subject: [PATCH] add ?wait=true 5 + 6 + --- 7 + .tangled/workflows/deploy.yml | 3 ++- 8 + 1 file changed, 2 insertions(+), 1 deletion(-) 9 + 10 + diff --git a/.tangled/workflows/deploy.yml b/.tangled/workflows/deploy.yml 11 + index a44c51b..b7edfea 100644 12 + --- a/.tangled/workflows/deploy.yml 13 + +++ b/.tangled/workflows/deploy.yml 14 + @@ -16,4 +16,5 @@ steps: 15 + command: | 16 + curl -X POST \ 17 + -H "Authorization: Bearer $SCANS_HOST_API_KEY" \ 18 + - https://free.scan.blue/api/v1/sites/jy35AeguTwaqDy_3ufq09/deploy 19 + \ No newline at end of file 20 + + -H "Authorization: Bearer $SCANS_HOST_API_KEY" \ 21 + + https://free.scan.blue/api/v1/sites/YOUR_SITE_ID/deploy?wait=true 22 + \ No newline at end of file 23 + -- 24 + 2.50.1 (Apple Git-155) 25 +
+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>
+26 -25
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.82", 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.3.0", 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", 28 - "@atcute/did-plc": "^0.2.0", 25 + "@atcute/bluesky": "^3.2.14", 26 + "@atcute/client": "^4.1.2", 27 + "@atcute/crypto": "^2.3.0", 28 + "@atcute/did-plc": "^0.3.1", 29 29 "@atcute/identity": "^1.1.3", 30 - "@atcute/identity-resolver": "^1.1.4", 31 - "@atcute/leaflet": "^1.0.12", 32 - "@atcute/lexicon-doc": "^2.0.1", 33 - "@atcute/lexicon-resolver": "^0.1.4", 34 - "@atcute/lexicons": "^1.2.4", 35 - "@atcute/oauth-browser-client": "^2.0.1", 36 - "@atcute/repo": "^0.1.0", 37 - "@atcute/tangled": "^1.0.12", 38 - "@atcute/tid": "^1.0.3", 39 - "@codemirror/commands": "^6.10.0", 30 + "@atcute/identity-resolver": "^1.2.1", 31 + "@atcute/leaflet": "^1.0.14", 32 + "@atcute/lexicon-doc": "^2.0.6", 33 + "@atcute/lexicon-resolver": "^0.1.5", 34 + "@atcute/lexicons": "^1.2.6", 35 + "@atcute/multibase": "^1.1.6", 36 + "@atcute/oauth-browser-client": "^2.0.3", 37 + "@atcute/repo": "^0.1.1", 38 + "@atcute/tangled": "^1.0.13", 39 + "@atcute/tid": "^1.1.0", 40 + "@codemirror/commands": "^6.10.1", 40 41 "@codemirror/lang-json": "^6.0.2", 41 42 "@codemirror/lint": "^6.9.2", 42 - "@codemirror/state": "^6.5.2", 43 - "@codemirror/view": "^6.38.8", 44 - "@fsegurai/codemirror-theme-basic-dark": "^6.2.2", 45 - "@fsegurai/codemirror-theme-basic-light": "^6.2.2", 43 + "@codemirror/state": "^6.5.3", 44 + "@codemirror/view": "^6.39.7", 45 + "@fsegurai/codemirror-theme-basic-dark": "^6.2.3", 46 + "@fsegurai/codemirror-theme-basic-light": "^6.2.3", 46 47 "@mary/exif-rm": "jsr:^0.2.2", 47 48 "@skyware/firehose": "^0.5.2", 48 49 "@solidjs/meta": "^0.29.4",
+622 -1077
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.2 19 + version: 4.1.2 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 - specifier: ^0.2.0 25 - version: 0.2.0 24 + specifier: ^0.3.1 25 + version: 0.3.1 26 26 '@atcute/identity': 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.1 31 + version: 1.2.1(@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.6 37 + version: 2.0.6 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.1(@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.6 43 + version: 1.2.6 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 - specifier: ^0.1.0 49 - version: 0.1.0 51 + specifier: ^0.1.1 52 + version: 0.1.1 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 - specifier: ^1.0.3 55 - version: 1.0.3 57 + specifier: ^1.1.0 58 + version: 1.1.0 56 59 '@codemirror/commands': 57 - specifier: ^6.10.0 58 - version: 6.10.0 60 + specifier: ^6.10.1 61 + version: 6.10.1 59 62 '@codemirror/lang-json': 60 63 specifier: ^6.0.2 61 64 version: 6.0.2 ··· 63 66 specifier: ^6.9.2 64 67 version: 6.9.2 65 68 '@codemirror/state': 66 - specifier: ^6.5.2 67 - version: 6.5.2 69 + specifier: ^6.5.3 70 + version: 6.5.3 68 71 '@codemirror/view': 69 - specifier: ^6.38.8 70 - version: 6.38.8 72 + specifier: ^6.39.7 73 + version: 6.39.7 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.12.1)(@codemirror/state@6.5.3)(@codemirror/view@6.39.7)(@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.12.1)(@codemirror/state@6.5.3)(@codemirror/view@6.39.7)(@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.82 101 + version: 1.2.82 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.3.0(@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.3.0 125 + version: 7.3.0(@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.3.0(@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==} ··· 147 147 '@atcute/cbor@2.2.8': 148 148 resolution: {integrity: sha512-UzOAN9BuN6JCXgn0ryV8qZuRJUDrNqrbLd6EFM8jc6RYssjRyGRxNy6RZ1NU/07Hd8Tq/0pz8+nQiMu5Zai5uw==} 149 149 150 - '@atcute/cid@2.2.6': 151 - resolution: {integrity: sha512-bTAHHbJ24p+E//V4KCS4xdmd39o211jJswvqQOevj7vk+5IYcgDLx1ryZWZ1sEPOo9x875li/kj5gpKL14RDwQ==} 150 + '@atcute/cid@2.3.0': 151 + resolution: {integrity: sha512-1SRdkTuMs/l5arQ+7Ag0F7JAueZqtzYE0d2gmbkuzi8EPweNU1kYlQs0CE4dSd81YF8PMDTOQty0K2ATq9CW9g==} 152 152 153 - '@atcute/client@4.0.5': 154 - resolution: {integrity: sha512-R8Qen8goGmEkynYGg2m6XFlVmz0GTDvQ+9w+4QqOob+XMk8/WDpF4aImev7WKEde/rV2gjcqW7zM8E6W9NShDA==} 153 + '@atcute/client@4.1.2': 154 + resolution: {integrity: sha512-DOJ0hpdBA4QVl4SGUeOUyz5FfYhdjRW1h0XIH9YDgNTipeA0tnUbRs8hWh9Nb7nyn6zMKzO5RpaWyWWWSx9Yxw==} 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 - '@atcute/did-plc@0.2.0': 160 - resolution: {integrity: sha512-1sGek8GRM/Ph7nLVRREm8FqM7g4shGckItvdVwJcRbUa8Rh0zOsXQa0QyYWAC0k40BhkqO9FwKXhJEaXCmF5oQ==} 159 + '@atcute/did-plc@0.3.1': 160 + resolution: {integrity: sha512-KsuVdRtaaIPMmlcCDcxZzLg6OWm7rajczquhIHfA3s57+c34PFQbdY4Lsc2BvDwZ0fUjmbwzvQI3Zio2VcZa7w==} 161 161 162 - '@atcute/identity-resolver@1.1.4': 163 - resolution: {integrity: sha512-/SVh8vf2cXFJenmBnGeYF2aY3WGQm3cJeew5NWTlkqoy3LvJ5wkvKq9PWu4Tv653VF40rPOp6LOdVr9Fa+q5rA==} 162 + '@atcute/identity-resolver@1.2.1': 163 + resolution: {integrity: sha512-LqWFFf8D8bqW8l0zUV9oZxcXYZ8+uQTZfjURoxH1TLmtmZFSXredtQHsY70k/iSMNDPxWHJXebdlKxJm5ioNIg==} 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.6': 174 + resolution: {integrity: sha512-iDYJkuom+tIw3zIvU1ggCEVFfReXKfOUtIhpY2kEg2kQeSfMB75F+8k1QOpeAQBetyWYmjsHqBuSUX9oQS6L1Q==} 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.6': 183 + resolution: {integrity: sha512-s76UQd8D+XmHIzrjD9CJ9SOOeeLPHc+sMmcj7UFakAW/dDFXc579fcRdRfuUKvXBL5v1Gs2VgDdlh/IvvQZAwA==} 184 184 185 - '@atcute/mst@0.1.0': 186 - resolution: {integrity: sha512-h+iDToKEnBpigk2DOHjSqY63vJtjYKUIztqu1CZ0P+I54wV2SrgoqAXAT1xrW6A1Iup8cjTv+U2H5WVG4KxPLw==} 185 + '@atcute/mst@0.1.1': 186 + resolution: {integrity: sha512-NZ/lZ68GOjmAgBSeGf6WHyKM5wo1Hhc7PNt9uwsViswGPMNEEKNj9cw+0YGziXee/Qbnvc+CKqbRSPwruhXFQg==} 187 187 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 - '@atcute/repo@0.1.0': 195 - resolution: {integrity: sha512-INiYAuma8dydBu7cqd2WVpcXh3mzhIepYBUqFWAK5MqMulPRLTRCc/9GW3G9pxYrOdlvLCVamG2Jf8XK0nuFEw==} 194 + '@atcute/repo@0.1.1': 195 + resolution: {integrity: sha512-P5aWjt3bvcquUkUmGPslF0naAfLGRHse5Qdz9/RJYrFuoH0iiEMyRnW6M+3ksOe20GPsMnbq71WbzzFkRFPBtg==} 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 - '@atcute/tid@1.0.3': 201 - resolution: {integrity: sha512-wfMJx1IMdnu0CZgWl0uR4JO2s6PGT1YPhpytD4ZHzEYKKQVuqV6Eb/7vieaVo1eYNMp2FrY67FZObeR7utRl2w==} 200 + '@atcute/tid@1.1.0': 201 + resolution: {integrity: sha512-U/YKL9BsBi/bcVXaIwdUBfglnjFxRfqoPd2f1uLsEIDQk1EyxepwdDQYOQ5t/aQctmtywl7lQn6KESQNG+mdfg==} 202 202 203 - '@atcute/uint8array@1.0.5': 204 - resolution: {integrity: sha512-XLWWxoR2HNl2qU+FCr0rp1APwJXci7HnzbOQLxK55OaMNBXZ19+xNC5ii4QCsThsDxa4JS/JTzuiQLziITWf2Q==} 203 + '@atcute/time-ms@1.0.0': 204 + resolution: {integrity: sha512-iWEOlMBcO3ktB+zQPC2kXka9H/798we+IWq2sjhb+hQJNNfcJrwejzvNi/68Q3jKo/hdfwZjRU9iF8U6D32/2Q==} 205 205 206 - '@atcute/util-fetch@1.0.3': 207 - resolution: {integrity: sha512-f8zzTb/xlKIwv2OQ31DhShPUNCmIIleX6p7qIXwWwEUjX6x8skUtpdISSjnImq01LXpltGV5y8yhV4/Mlb7CRQ==} 206 + '@atcute/uint8array@1.0.6': 207 + resolution: {integrity: sha512-ucfRBQc7BFT8n9eCyGOzDHEMKF/nZwhS2pPao4Xtab1ML3HdFYcX2DM1tadCzas85QTGxHe5urnUAAcNKGRi9A==} 208 + 209 + '@atcute/util-fetch@1.0.4': 210 + resolution: {integrity: sha512-sIU9Qk0dE8PLEXSfhy+gIJV+HpiiknMytCI2SqLlqd0vgZUtEKI/EQfP+23LHWvP+CLCzVDOa6cpH045OlmNBg==} 211 + 212 + '@atcute/util-text@0.0.1': 213 + resolution: {integrity: sha512-t1KZqvn0AYy+h2KcJyHnKF9aEqfRfMUmyY8j1ELtAEIgqN9CxINAjxnoRCJIFUlvWzb+oY3uElQL/Vyk3yss0g==} 208 214 209 215 '@atcute/varint@1.0.3': 210 216 resolution: {integrity: sha512-fdvMPyBB+McDT+Ai5e9RwEbwYV4yjZ60S2Dn5PTjGqUyxvoCH1z42viuheDZRUDkmfQehXJTZ5az7dSozVNtog==} ··· 297 303 '@codemirror/autocomplete@6.20.0': 298 304 resolution: {integrity: sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==} 299 305 300 - '@codemirror/commands@6.10.0': 301 - resolution: {integrity: sha512-2xUIc5mHXQzT16JnyOFkh8PvfeXuIut3pslWGfsGOhxP/lpgRm9HOl/mpzLErgt5mXDovqA0d11P21gofRLb9w==} 306 + '@codemirror/commands@6.10.1': 307 + resolution: {integrity: sha512-uWDWFypNdQmz2y1LaNJzK7fL7TYKLeUAU0npEC685OKTF3KcQ2Vu3klIM78D7I6wGhktme0lh3CuQLv0ZCrD9Q==} 302 308 303 309 '@codemirror/lang-json@6.0.2': 304 310 resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==} 305 311 306 - '@codemirror/language@6.11.3': 307 - resolution: {integrity: sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==} 312 + '@codemirror/language@6.12.1': 313 + resolution: {integrity: sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==} 308 314 309 315 '@codemirror/lint@6.9.2': 310 316 resolution: {integrity: sha512-sv3DylBiIyi+xKwRCJAAsBZZZWo82shJ/RTMymLabAdtbkV5cSKwWDeCgtUq3v8flTaXS2y1kKkICuRYtUswyQ==} ··· 312 318 '@codemirror/search@6.5.11': 313 319 resolution: {integrity: sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==} 314 320 315 - '@codemirror/state@6.5.2': 316 - resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==} 321 + '@codemirror/state@6.5.3': 322 + resolution: {integrity: sha512-MerMzJzlXogk2fxWFU1nKp36bY5orBG59HnPiz0G9nLRebWa0zXuv2siH6PLIHBvv5TH8CkQRqjBs0MlxCZu+A==} 323 + 324 + '@codemirror/view@6.39.7': 325 + resolution: {integrity: sha512-3Vif9hnNHJnl2YgOtkR/wzGzhYcQ8gy3LGdUhkLUU8xSBbgsTxrE8he/CMTpeINm5TgxLe2FmzvF6IYQL/BSAg==} 317 326 318 - '@codemirror/view@6.38.8': 319 - resolution: {integrity: sha512-XcE9fcnkHCbWkjeKyi0lllwXmBLtyYb5dt89dJyx23I9+LSh5vZDIuk7OLG4VM1lgrXZQcY6cxyZyk5WVPRv/A==} 327 + '@cyberalien/svg-utils@1.0.11': 328 + resolution: {integrity: sha512-qEE9mnyI+avfGT3emKuRs3ucYkITeaV0Xi7VlYN41f+uGnZBecQP3jwz/AF437H9J4Q7qPClHKm4NiTYpNE6hA==} 320 329 321 330 '@esbuild/aix-ppc64@0.23.1': 322 331 resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} ··· 324 333 cpu: [ppc64] 325 334 os: [aix] 326 335 327 - '@esbuild/aix-ppc64@0.25.12': 328 - resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} 336 + '@esbuild/aix-ppc64@0.27.2': 337 + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} 329 338 engines: {node: '>=18'} 330 339 cpu: [ppc64] 331 340 os: [aix] ··· 336 345 cpu: [arm64] 337 346 os: [android] 338 347 339 - '@esbuild/android-arm64@0.25.12': 340 - resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} 348 + '@esbuild/android-arm64@0.27.2': 349 + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} 341 350 engines: {node: '>=18'} 342 351 cpu: [arm64] 343 352 os: [android] ··· 348 357 cpu: [arm] 349 358 os: [android] 350 359 351 - '@esbuild/android-arm@0.25.12': 352 - resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} 360 + '@esbuild/android-arm@0.27.2': 361 + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} 353 362 engines: {node: '>=18'} 354 363 cpu: [arm] 355 364 os: [android] ··· 360 369 cpu: [x64] 361 370 os: [android] 362 371 363 - '@esbuild/android-x64@0.25.12': 364 - resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} 372 + '@esbuild/android-x64@0.27.2': 373 + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} 365 374 engines: {node: '>=18'} 366 375 cpu: [x64] 367 376 os: [android] ··· 372 381 cpu: [arm64] 373 382 os: [darwin] 374 383 375 - '@esbuild/darwin-arm64@0.25.12': 376 - resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} 384 + '@esbuild/darwin-arm64@0.27.2': 385 + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} 377 386 engines: {node: '>=18'} 378 387 cpu: [arm64] 379 388 os: [darwin] ··· 384 393 cpu: [x64] 385 394 os: [darwin] 386 395 387 - '@esbuild/darwin-x64@0.25.12': 388 - resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} 396 + '@esbuild/darwin-x64@0.27.2': 397 + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} 389 398 engines: {node: '>=18'} 390 399 cpu: [x64] 391 400 os: [darwin] ··· 396 405 cpu: [arm64] 397 406 os: [freebsd] 398 407 399 - '@esbuild/freebsd-arm64@0.25.12': 400 - resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} 408 + '@esbuild/freebsd-arm64@0.27.2': 409 + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} 401 410 engines: {node: '>=18'} 402 411 cpu: [arm64] 403 412 os: [freebsd] ··· 408 417 cpu: [x64] 409 418 os: [freebsd] 410 419 411 - '@esbuild/freebsd-x64@0.25.12': 412 - resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} 420 + '@esbuild/freebsd-x64@0.27.2': 421 + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} 413 422 engines: {node: '>=18'} 414 423 cpu: [x64] 415 424 os: [freebsd] ··· 420 429 cpu: [arm64] 421 430 os: [linux] 422 431 423 - '@esbuild/linux-arm64@0.25.12': 424 - resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} 432 + '@esbuild/linux-arm64@0.27.2': 433 + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} 425 434 engines: {node: '>=18'} 426 435 cpu: [arm64] 427 436 os: [linux] ··· 432 441 cpu: [arm] 433 442 os: [linux] 434 443 435 - '@esbuild/linux-arm@0.25.12': 436 - resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} 444 + '@esbuild/linux-arm@0.27.2': 445 + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} 437 446 engines: {node: '>=18'} 438 447 cpu: [arm] 439 448 os: [linux] ··· 444 453 cpu: [ia32] 445 454 os: [linux] 446 455 447 - '@esbuild/linux-ia32@0.25.12': 448 - resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} 456 + '@esbuild/linux-ia32@0.27.2': 457 + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} 449 458 engines: {node: '>=18'} 450 459 cpu: [ia32] 451 460 os: [linux] ··· 456 465 cpu: [loong64] 457 466 os: [linux] 458 467 459 - '@esbuild/linux-loong64@0.25.12': 460 - resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} 468 + '@esbuild/linux-loong64@0.27.2': 469 + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} 461 470 engines: {node: '>=18'} 462 471 cpu: [loong64] 463 472 os: [linux] ··· 468 477 cpu: [mips64el] 469 478 os: [linux] 470 479 471 - '@esbuild/linux-mips64el@0.25.12': 472 - resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} 480 + '@esbuild/linux-mips64el@0.27.2': 481 + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} 473 482 engines: {node: '>=18'} 474 483 cpu: [mips64el] 475 484 os: [linux] ··· 480 489 cpu: [ppc64] 481 490 os: [linux] 482 491 483 - '@esbuild/linux-ppc64@0.25.12': 484 - resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} 492 + '@esbuild/linux-ppc64@0.27.2': 493 + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} 485 494 engines: {node: '>=18'} 486 495 cpu: [ppc64] 487 496 os: [linux] ··· 492 501 cpu: [riscv64] 493 502 os: [linux] 494 503 495 - '@esbuild/linux-riscv64@0.25.12': 496 - resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} 504 + '@esbuild/linux-riscv64@0.27.2': 505 + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} 497 506 engines: {node: '>=18'} 498 507 cpu: [riscv64] 499 508 os: [linux] ··· 504 513 cpu: [s390x] 505 514 os: [linux] 506 515 507 - '@esbuild/linux-s390x@0.25.12': 508 - resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} 516 + '@esbuild/linux-s390x@0.27.2': 517 + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} 509 518 engines: {node: '>=18'} 510 519 cpu: [s390x] 511 520 os: [linux] ··· 516 525 cpu: [x64] 517 526 os: [linux] 518 527 519 - '@esbuild/linux-x64@0.25.12': 520 - resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} 528 + '@esbuild/linux-x64@0.27.2': 529 + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} 521 530 engines: {node: '>=18'} 522 531 cpu: [x64] 523 532 os: [linux] 524 533 525 - '@esbuild/netbsd-arm64@0.25.12': 526 - resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} 534 + '@esbuild/netbsd-arm64@0.27.2': 535 + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} 527 536 engines: {node: '>=18'} 528 537 cpu: [arm64] 529 538 os: [netbsd] ··· 534 543 cpu: [x64] 535 544 os: [netbsd] 536 545 537 - '@esbuild/netbsd-x64@0.25.12': 538 - resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} 546 + '@esbuild/netbsd-x64@0.27.2': 547 + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} 539 548 engines: {node: '>=18'} 540 549 cpu: [x64] 541 550 os: [netbsd] ··· 546 555 cpu: [arm64] 547 556 os: [openbsd] 548 557 549 - '@esbuild/openbsd-arm64@0.25.12': 550 - resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} 558 + '@esbuild/openbsd-arm64@0.27.2': 559 + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} 551 560 engines: {node: '>=18'} 552 561 cpu: [arm64] 553 562 os: [openbsd] ··· 558 567 cpu: [x64] 559 568 os: [openbsd] 560 569 561 - '@esbuild/openbsd-x64@0.25.12': 562 - resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} 570 + '@esbuild/openbsd-x64@0.27.2': 571 + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} 563 572 engines: {node: '>=18'} 564 573 cpu: [x64] 565 574 os: [openbsd] 566 575 567 - '@esbuild/openharmony-arm64@0.25.12': 568 - resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} 576 + '@esbuild/openharmony-arm64@0.27.2': 577 + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} 569 578 engines: {node: '>=18'} 570 579 cpu: [arm64] 571 580 os: [openharmony] ··· 576 585 cpu: [x64] 577 586 os: [sunos] 578 587 579 - '@esbuild/sunos-x64@0.25.12': 580 - resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} 588 + '@esbuild/sunos-x64@0.27.2': 589 + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} 581 590 engines: {node: '>=18'} 582 591 cpu: [x64] 583 592 os: [sunos] ··· 588 597 cpu: [arm64] 589 598 os: [win32] 590 599 591 - '@esbuild/win32-arm64@0.25.12': 592 - resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} 600 + '@esbuild/win32-arm64@0.27.2': 601 + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} 593 602 engines: {node: '>=18'} 594 603 cpu: [arm64] 595 604 os: [win32] ··· 600 609 cpu: [ia32] 601 610 os: [win32] 602 611 603 - '@esbuild/win32-ia32@0.25.12': 604 - resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} 612 + '@esbuild/win32-ia32@0.27.2': 613 + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} 605 614 engines: {node: '>=18'} 606 615 cpu: [ia32] 607 616 os: [win32] ··· 612 621 cpu: [x64] 613 622 os: [win32] 614 623 615 - '@esbuild/win32-x64@0.25.12': 616 - resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} 624 + '@esbuild/win32-x64@0.27.2': 625 + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} 617 626 engines: {node: '>=18'} 618 627 cpu: [x64] 619 628 os: [win32] 620 629 621 - '@fsegurai/codemirror-theme-basic-dark@6.2.2': 622 - resolution: {integrity: sha512-cVK4VheF7ZkuV0sfy20lmH2S7Q2xIfKoqN2HdU5rpGH8mZM2LVG9Tl+oHT0XNPpsWFqNAAKLzjYFw0IPX95Biw==} 630 + '@fsegurai/codemirror-theme-basic-dark@6.2.3': 631 + resolution: {integrity: sha512-08d09Yn9Ic8mjCzrBQQhtws/HM+8B00bRV9FqW+GaIQwSOFmn17FsvzuLJQyervcKAkTzmKaLPjp2D3Y+2K8EQ==} 623 632 peerDependencies: 624 633 '@codemirror/language': ^6.0.0 625 634 '@codemirror/state': ^6.0.0 626 635 '@codemirror/view': ^6.0.0 627 636 '@lezer/highlight': ^1.0.0 628 637 629 - '@fsegurai/codemirror-theme-basic-light@6.2.2': 630 - resolution: {integrity: sha512-zFtJ6VwwEeZ/HAXMYdcJz6+DdW1aQkngFwbD3diku79cctpTglCWH49KRFO8Mifjzwylsynm7dLyOUnGhIu0NQ==} 638 + '@fsegurai/codemirror-theme-basic-light@6.2.3': 639 + resolution: {integrity: sha512-rkHCj1U3OwNAqLLi2xti47u3Fq6gDiSEKmQsAOwIADJKnnwU2LeAwCPqSEa7sUVlavFusjDvt5L/SmGjb10vWg==} 631 640 peerDependencies: 632 641 '@codemirror/language': ^6.0.0 633 642 '@codemirror/state': ^6.0.0 634 643 '@codemirror/view': ^6.0.0 635 644 '@lezer/highlight': ^1.0.0 636 645 637 - '@iconify-json/lucide@1.2.75': 638 - resolution: {integrity: sha512-sWBN0t/rTo1FxWG/46xKgkIcDerHpsjyNgMH48nvtC4/kUG88sFQXI+7mxX3SD8eSUaQQ2kS9C7ZKWm2DKgBlw==} 646 + '@iconify-json/lucide@1.2.82': 647 + resolution: {integrity: sha512-fHZWegspOZonl5GNTvOkHsjnTMdSslFh3EzpzUtRyLxO8bOonqk2OTU3hCl0k4VXzViMjqpRK3X1sotnuBXkFA==} 639 648 640 - '@iconify/tailwind4@1.1.0': 641 - resolution: {integrity: sha512-HqgAYtYk4eFtLvdYfhQrBRT9ohToh+VJJVhHtJ7B4Qhw+J+mRPvGC9Wr6Cgtb36YbIWqBxWuBaAUw9TE/8m2/w==} 649 + '@iconify/tailwind4@1.2.0': 650 + resolution: {integrity: sha512-+t7XqfojOB0zzZdd8gV7IQZGq1AaIHTlsxMVzagxYR0hAlJCLUD63o3iSlNKRMH3ZR7gZ8y5c9dJ7J431avRbA==} 642 651 peerDependencies: 643 652 tailwindcss: '>= 4.0.0' 644 653 645 - '@iconify/tools@4.1.4': 646 - resolution: {integrity: sha512-s6BcNUcCxQ3S6cvhlsoWzOuBt8qKXdVyXB9rT57uSJ/ARHD7dVM43+5ERBWn3tmkMWXeJ/s9DPVc3dUasayzeA==} 654 + '@iconify/tools@5.0.1': 655 + resolution: {integrity: sha512-/znhBN9WIpJd9UtKhyEDfRKwNo8rrOy8dShF8bwSZ1i27ukTSHjeS6bmVK4tTYBYriwFhBf70JT6g8GIRwFvbw==} 647 656 648 657 '@iconify/types@2.0.0': 649 658 resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} 650 659 651 - '@iconify/utils@2.3.0': 652 - resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==} 660 + '@iconify/utils@3.1.0': 661 + resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==} 653 662 654 663 '@jridgewell/gen-mapping@0.3.13': 655 664 resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} ··· 670 679 '@jsr/mary__exif-rm@0.2.2': 671 680 resolution: {integrity: sha512-+ZpLaC+1CyqWhH608Sqd6/yTG0pOlokn2tCXha7s1SMQ+GLKo4Nn/PskTeeP9Pt+6gNYSu6ednoSlRvXb2ZGxg==, tarball: https://npm.jsr.io/~/11/@jsr/mary__exif-rm/0.2.2.tgz} 672 681 673 - '@lezer/common@1.3.0': 674 - resolution: {integrity: sha512-L9X8uHCYU310o99L3/MpJKYxPzXPOS7S0NmBaM7UO/x2Kb2WbmMLSkfvdr1KxRIFYOpbY0Jhn7CfLSUDzL8arQ==} 682 + '@lezer/common@1.5.0': 683 + resolution: {integrity: sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA==} 675 684 676 685 '@lezer/highlight@1.2.3': 677 686 resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} ··· 679 688 '@lezer/json@1.0.3': 680 689 resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==} 681 690 682 - '@lezer/lr@1.4.3': 683 - resolution: {integrity: sha512-yenN5SqAxAPv/qMnpWW0AT7l+SxVrgG+u0tNsRQWqbrz66HIl8DnEbBObvy21J5K7+I1v7gsAnlE2VQ5yYVSeA==} 691 + '@lezer/lr@1.4.5': 692 + resolution: {integrity: sha512-/YTRKP5yPPSo1xImYQk7AZZMAgap0kegzqCSYHjAL9x1AZ0ZQW+IpcEzMKagCsbTsLnVeWkxYrCNeXG8xEPrjg==} 684 693 685 694 '@marijn/find-cluster-break@1.0.2': 686 695 resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} ··· 688 697 '@noble/secp256k1@3.0.0': 689 698 resolution: {integrity: sha512-NJBaR352KyIvj3t6sgT/+7xrNyF9Xk9QlLSIqUGVUYlsnDTAUqY8LOmwpcgEx4AMJXRITQ5XEVHD+mMaPfr3mg==} 690 699 691 - '@rollup/rollup-android-arm-eabi@4.53.3': 692 - resolution: {integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==} 700 + '@rollup/rollup-android-arm-eabi@4.54.0': 701 + resolution: {integrity: sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==} 693 702 cpu: [arm] 694 703 os: [android] 695 704 696 - '@rollup/rollup-android-arm64@4.53.3': 697 - resolution: {integrity: sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==} 705 + '@rollup/rollup-android-arm64@4.54.0': 706 + resolution: {integrity: sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==} 698 707 cpu: [arm64] 699 708 os: [android] 700 709 701 - '@rollup/rollup-darwin-arm64@4.53.3': 702 - resolution: {integrity: sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==} 710 + '@rollup/rollup-darwin-arm64@4.54.0': 711 + resolution: {integrity: sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==} 703 712 cpu: [arm64] 704 713 os: [darwin] 705 714 706 - '@rollup/rollup-darwin-x64@4.53.3': 707 - resolution: {integrity: sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==} 715 + '@rollup/rollup-darwin-x64@4.54.0': 716 + resolution: {integrity: sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==} 708 717 cpu: [x64] 709 718 os: [darwin] 710 719 711 - '@rollup/rollup-freebsd-arm64@4.53.3': 712 - resolution: {integrity: sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==} 720 + '@rollup/rollup-freebsd-arm64@4.54.0': 721 + resolution: {integrity: sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==} 713 722 cpu: [arm64] 714 723 os: [freebsd] 715 724 716 - '@rollup/rollup-freebsd-x64@4.53.3': 717 - resolution: {integrity: sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==} 725 + '@rollup/rollup-freebsd-x64@4.54.0': 726 + resolution: {integrity: sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==} 718 727 cpu: [x64] 719 728 os: [freebsd] 720 729 721 - '@rollup/rollup-linux-arm-gnueabihf@4.53.3': 722 - resolution: {integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==} 730 + '@rollup/rollup-linux-arm-gnueabihf@4.54.0': 731 + resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==} 723 732 cpu: [arm] 724 733 os: [linux] 725 734 726 - '@rollup/rollup-linux-arm-musleabihf@4.53.3': 727 - resolution: {integrity: sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==} 735 + '@rollup/rollup-linux-arm-musleabihf@4.54.0': 736 + resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==} 728 737 cpu: [arm] 729 738 os: [linux] 730 739 731 - '@rollup/rollup-linux-arm64-gnu@4.53.3': 732 - resolution: {integrity: sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==} 740 + '@rollup/rollup-linux-arm64-gnu@4.54.0': 741 + resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==} 733 742 cpu: [arm64] 734 743 os: [linux] 735 744 736 - '@rollup/rollup-linux-arm64-musl@4.53.3': 737 - resolution: {integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==} 745 + '@rollup/rollup-linux-arm64-musl@4.54.0': 746 + resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==} 738 747 cpu: [arm64] 739 748 os: [linux] 740 749 741 - '@rollup/rollup-linux-loong64-gnu@4.53.3': 742 - resolution: {integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==} 750 + '@rollup/rollup-linux-loong64-gnu@4.54.0': 751 + resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==} 743 752 cpu: [loong64] 744 753 os: [linux] 745 754 746 - '@rollup/rollup-linux-ppc64-gnu@4.53.3': 747 - resolution: {integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==} 755 + '@rollup/rollup-linux-ppc64-gnu@4.54.0': 756 + resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==} 748 757 cpu: [ppc64] 749 758 os: [linux] 750 759 751 - '@rollup/rollup-linux-riscv64-gnu@4.53.3': 752 - resolution: {integrity: sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==} 760 + '@rollup/rollup-linux-riscv64-gnu@4.54.0': 761 + resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==} 753 762 cpu: [riscv64] 754 763 os: [linux] 755 764 756 - '@rollup/rollup-linux-riscv64-musl@4.53.3': 757 - resolution: {integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==} 765 + '@rollup/rollup-linux-riscv64-musl@4.54.0': 766 + resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==} 758 767 cpu: [riscv64] 759 768 os: [linux] 760 769 761 - '@rollup/rollup-linux-s390x-gnu@4.53.3': 762 - resolution: {integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==} 770 + '@rollup/rollup-linux-s390x-gnu@4.54.0': 771 + resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==} 763 772 cpu: [s390x] 764 773 os: [linux] 765 774 766 - '@rollup/rollup-linux-x64-gnu@4.53.3': 767 - resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==} 775 + '@rollup/rollup-linux-x64-gnu@4.54.0': 776 + resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==} 768 777 cpu: [x64] 769 778 os: [linux] 770 779 771 - '@rollup/rollup-linux-x64-musl@4.53.3': 772 - resolution: {integrity: sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==} 780 + '@rollup/rollup-linux-x64-musl@4.54.0': 781 + resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==} 773 782 cpu: [x64] 774 783 os: [linux] 775 784 776 - '@rollup/rollup-openharmony-arm64@4.53.3': 777 - resolution: {integrity: sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==} 785 + '@rollup/rollup-openharmony-arm64@4.54.0': 786 + resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==} 778 787 cpu: [arm64] 779 788 os: [openharmony] 780 789 781 - '@rollup/rollup-win32-arm64-msvc@4.53.3': 782 - resolution: {integrity: sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==} 790 + '@rollup/rollup-win32-arm64-msvc@4.54.0': 791 + resolution: {integrity: sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==} 783 792 cpu: [arm64] 784 793 os: [win32] 785 794 786 - '@rollup/rollup-win32-ia32-msvc@4.53.3': 787 - resolution: {integrity: sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==} 795 + '@rollup/rollup-win32-ia32-msvc@4.54.0': 796 + resolution: {integrity: sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==} 788 797 cpu: [ia32] 789 798 os: [win32] 790 799 791 - '@rollup/rollup-win32-x64-gnu@4.53.3': 792 - resolution: {integrity: sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==} 800 + '@rollup/rollup-win32-x64-gnu@4.54.0': 801 + resolution: {integrity: sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==} 793 802 cpu: [x64] 794 803 os: [win32] 795 804 796 - '@rollup/rollup-win32-x64-msvc@4.53.3': 797 - resolution: {integrity: sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==} 805 + '@rollup/rollup-win32-x64-msvc@4.54.0': 806 + resolution: {integrity: sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==} 798 807 cpu: [x64] 799 808 os: [win32] 800 809 ··· 811 820 peerDependencies: 812 821 solid-js: ^1.8.6 813 822 814 - '@standard-schema/spec@1.0.0': 815 - resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} 823 + '@standard-schema/spec@1.1.0': 824 + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} 816 825 817 - '@tailwindcss/node@4.1.17': 818 - resolution: {integrity: sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==} 826 + '@tailwindcss/node@4.1.18': 827 + resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} 819 828 820 - '@tailwindcss/oxide-android-arm64@4.1.17': 821 - resolution: {integrity: sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==} 829 + '@tailwindcss/oxide-android-arm64@4.1.18': 830 + resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==} 822 831 engines: {node: '>= 10'} 823 832 cpu: [arm64] 824 833 os: [android] 825 834 826 - '@tailwindcss/oxide-darwin-arm64@4.1.17': 827 - resolution: {integrity: sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==} 835 + '@tailwindcss/oxide-darwin-arm64@4.1.18': 836 + resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==} 828 837 engines: {node: '>= 10'} 829 838 cpu: [arm64] 830 839 os: [darwin] 831 840 832 - '@tailwindcss/oxide-darwin-x64@4.1.17': 833 - resolution: {integrity: sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==} 841 + '@tailwindcss/oxide-darwin-x64@4.1.18': 842 + resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==} 834 843 engines: {node: '>= 10'} 835 844 cpu: [x64] 836 845 os: [darwin] 837 846 838 - '@tailwindcss/oxide-freebsd-x64@4.1.17': 839 - resolution: {integrity: sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==} 847 + '@tailwindcss/oxide-freebsd-x64@4.1.18': 848 + resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==} 840 849 engines: {node: '>= 10'} 841 850 cpu: [x64] 842 851 os: [freebsd] 843 852 844 - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17': 845 - resolution: {integrity: sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==} 853 + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': 854 + resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==} 846 855 engines: {node: '>= 10'} 847 856 cpu: [arm] 848 857 os: [linux] 849 858 850 - '@tailwindcss/oxide-linux-arm64-gnu@4.1.17': 851 - resolution: {integrity: sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==} 859 + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': 860 + resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==} 852 861 engines: {node: '>= 10'} 853 862 cpu: [arm64] 854 863 os: [linux] 855 864 856 - '@tailwindcss/oxide-linux-arm64-musl@4.1.17': 857 - resolution: {integrity: sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==} 865 + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': 866 + resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} 858 867 engines: {node: '>= 10'} 859 868 cpu: [arm64] 860 869 os: [linux] 861 870 862 - '@tailwindcss/oxide-linux-x64-gnu@4.1.17': 863 - resolution: {integrity: sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==} 871 + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': 872 + resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} 864 873 engines: {node: '>= 10'} 865 874 cpu: [x64] 866 875 os: [linux] 867 876 868 - '@tailwindcss/oxide-linux-x64-musl@4.1.17': 869 - resolution: {integrity: sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==} 877 + '@tailwindcss/oxide-linux-x64-musl@4.1.18': 878 + resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} 870 879 engines: {node: '>= 10'} 871 880 cpu: [x64] 872 881 os: [linux] 873 882 874 - '@tailwindcss/oxide-wasm32-wasi@4.1.17': 875 - resolution: {integrity: sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==} 883 + '@tailwindcss/oxide-wasm32-wasi@4.1.18': 884 + resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} 876 885 engines: {node: '>=14.0.0'} 877 886 cpu: [wasm32] 878 887 bundledDependencies: ··· 883 892 - '@emnapi/wasi-threads' 884 893 - tslib 885 894 886 - '@tailwindcss/oxide-win32-arm64-msvc@4.1.17': 887 - resolution: {integrity: sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==} 895 + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': 896 + resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==} 888 897 engines: {node: '>= 10'} 889 898 cpu: [arm64] 890 899 os: [win32] 891 900 892 - '@tailwindcss/oxide-win32-x64-msvc@4.1.17': 893 - resolution: {integrity: sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==} 901 + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': 902 + resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==} 894 903 engines: {node: '>= 10'} 895 904 cpu: [x64] 896 905 os: [win32] 897 906 898 - '@tailwindcss/oxide@4.1.17': 899 - resolution: {integrity: sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==} 907 + '@tailwindcss/oxide@4.1.18': 908 + resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==} 900 909 engines: {node: '>= 10'} 901 910 902 - '@tailwindcss/vite@4.1.17': 903 - resolution: {integrity: sha512-4+9w8ZHOiGnpcGI6z1TVVfWaX/koK7fKeSYF3qlYg2xpBtbteP2ddBxiarL+HVgfSJGeK5RIxRQmKm4rTJJAwA==} 911 + '@tailwindcss/vite@4.1.18': 912 + resolution: {integrity: sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==} 904 913 peerDependencies: 905 914 vite: ^5.2.0 || ^6 || ^7 906 915 907 - '@trysound/sax@0.2.0': 908 - resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} 909 - engines: {node: '>=10.13.0'} 910 - 911 916 '@types/babel__core@7.20.5': 912 917 resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} 913 918 ··· 923 928 '@types/estree@1.0.8': 924 929 resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} 925 930 931 + '@types/node@22.19.3': 932 + resolution: {integrity: sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==} 933 + 926 934 '@types/node@24.10.1': 927 935 resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} 928 936 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 937 acorn@8.15.0: 936 938 resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} 937 939 engines: {node: '>=0.4.0'} 938 940 hasBin: true 939 941 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 942 babel-plugin-jsx-dom-expressions@0.40.3: 947 943 resolution: {integrity: sha512-5HOwwt0BYiv/zxl7j8Pf2bGL6rDXfV6nUhLs8ygBX+EFJXzBPHM/euj9j/6deMZ6wa52Wb2PBaAV5U/jKwIY1w==} 948 944 peerDependencies: ··· 957 953 solid-js: 958 954 optional: true 959 955 960 - baseline-browser-mapping@2.8.31: 961 - resolution: {integrity: sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==} 956 + baseline-browser-mapping@2.9.11: 957 + resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==} 962 958 hasBin: true 963 959 964 960 boolbase@1.0.0: 965 961 resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} 966 962 967 - browserslist@4.28.0: 968 - resolution: {integrity: sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==} 963 + browserslist@4.28.1: 964 + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} 969 965 engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} 970 966 hasBin: true 971 967 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'} 968 + caniuse-lite@1.0.30001761: 969 + resolution: {integrity: sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==} 992 970 993 971 codemirror@6.0.2: 994 972 resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} 995 973 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'} 974 + commander@11.1.0: 975 + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} 976 + engines: {node: '>=16'} 1003 977 1004 978 confbox@0.1.8: 1005 979 resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} 1006 - 1007 - confbox@0.2.2: 1008 - resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} 1009 980 1010 981 convert-source-map@2.0.0: 1011 982 resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} ··· 1020 991 resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} 1021 992 engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} 1022 993 1023 - css-tree@2.3.1: 1024 - resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} 994 + css-tree@3.1.0: 995 + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} 1025 996 engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} 1026 997 1027 998 css-what@6.2.2: ··· 1044 1015 supports-color: 1045 1016 optional: true 1046 1017 1047 - delayed-stream@1.0.0: 1048 - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} 1049 - engines: {node: '>=0.4.0'} 1050 - 1051 1018 detect-libc@2.1.2: 1052 1019 resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} 1053 1020 engines: {node: '>=8'} ··· 1065 1032 domutils@3.2.2: 1066 1033 resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} 1067 1034 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==} 1035 + electron-to-chromium@1.5.267: 1036 + resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} 1080 1037 1081 - enhanced-resolve@5.18.3: 1082 - resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} 1038 + enhanced-resolve@5.18.4: 1039 + resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} 1083 1040 engines: {node: '>=10.13.0'} 1084 1041 1085 1042 entities@4.5.0: ··· 1090 1047 resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} 1091 1048 engines: {node: '>=0.12'} 1092 1049 1093 - es-define-property@1.0.1: 1094 - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} 1095 - engines: {node: '>= 0.4'} 1096 - 1097 - es-errors@1.3.0: 1098 - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} 1099 - engines: {node: '>= 0.4'} 1100 - 1101 - es-object-atoms@1.1.1: 1102 - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} 1103 - engines: {node: '>= 0.4'} 1104 - 1105 - es-set-tostringtag@2.1.0: 1106 - resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} 1107 - engines: {node: '>= 0.4'} 1108 - 1109 1050 esbuild@0.23.1: 1110 1051 resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} 1111 1052 engines: {node: '>=18'} 1112 1053 hasBin: true 1113 1054 1114 - esbuild@0.25.12: 1115 - resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} 1055 + esbuild@0.27.2: 1056 + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} 1116 1057 engines: {node: '>=18'} 1117 1058 hasBin: true 1118 1059 ··· 1123 1064 esm-env@1.2.2: 1124 1065 resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} 1125 1066 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 1067 fdir@6.5.0: 1138 1068 resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} 1139 1069 engines: {node: '>=12.0.0'} ··· 1143 1073 picomatch: 1144 1074 optional: true 1145 1075 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'} 1076 + fflate@0.8.2: 1077 + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} 1162 1078 1163 1079 fsevents@2.3.3: 1164 1080 resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 1165 1081 engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 1166 1082 os: [darwin] 1167 1083 1168 - function-bind@1.1.2: 1169 - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 1170 - 1171 1084 gensync@1.0.0-beta.2: 1172 1085 resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} 1173 1086 engines: {node: '>=6.9.0'} 1174 1087 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 1088 get-tsconfig@4.13.0: 1188 1089 resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} 1189 1090 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 1091 graceful-fs@4.2.11: 1199 1092 resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 1200 1093 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 1094 html-entities@2.3.3: 1214 1095 resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} 1215 1096 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 1097 is-what@4.1.16: 1224 1098 resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} 1225 1099 engines: {node: '>=12.13'} ··· 1240 1114 resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} 1241 1115 engines: {node: '>=6'} 1242 1116 hasBin: true 1243 - 1244 - kolorist@1.8.0: 1245 - resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} 1246 1117 1247 1118 lightningcss-android-arm64@1.30.2: 1248 1119 resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} ··· 1314 1185 resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} 1315 1186 engines: {node: '>= 12.0.0'} 1316 1187 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 1188 lru-cache@5.1.1: 1326 1189 resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} 1327 1190 1328 1191 magic-string@0.30.21: 1329 1192 resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} 1330 1193 1331 - math-intrinsics@1.1.0: 1332 - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} 1333 - engines: {node: '>= 0.4'} 1334 - 1335 1194 mdn-data@2.0.28: 1336 1195 resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} 1337 1196 1338 - mdn-data@2.0.30: 1339 - resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} 1197 + mdn-data@2.12.2: 1198 + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} 1340 1199 1341 1200 merge-anything@5.1.7: 1342 1201 resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==} 1343 1202 engines: {node: '>=12.13'} 1344 1203 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 1204 mlly@1.8.0: 1375 1205 resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} 1376 1206 1207 + modern-tar@0.7.3: 1208 + resolution: {integrity: sha512-4W79zekKGyYU4JXVmB78DOscMFaJth2gGhgfTl2alWE4rNe3nf4N2pqenQ0rEtIewrnD79M687Ouba3YGTLOvg==} 1209 + engines: {node: '>=18.0.0'} 1210 + 1377 1211 ms@2.1.3: 1378 1212 resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 1379 1213 ··· 1391 1225 engines: {node: ^18 || >=20} 1392 1226 hasBin: true 1393 1227 1228 + node-gyp-build@4.8.4: 1229 + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} 1230 + hasBin: true 1231 + 1394 1232 node-releases@2.0.27: 1395 1233 resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} 1396 1234 1397 1235 nth-check@2.1.1: 1398 1236 resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} 1399 1237 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==} 1238 + package-manager-detector@1.6.0: 1239 + resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} 1411 1240 1412 1241 parse5@7.3.0: 1413 1242 resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} 1414 1243 1415 - pathe@1.1.2: 1416 - resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} 1417 - 1418 1244 pathe@2.0.3: 1419 1245 resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} 1420 1246 1421 - pend@1.2.0: 1422 - resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} 1423 - 1424 1247 picocolors@1.1.1: 1425 1248 resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} 1426 1249 ··· 1430 1253 1431 1254 pkg-types@1.3.1: 1432 1255 resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} 1433 - 1434 - pkg-types@2.3.0: 1435 - resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} 1436 1256 1437 1257 postcss@8.5.6: 1438 1258 resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} ··· 1448 1268 vue-tsc: 1449 1269 optional: true 1450 1270 1451 - prettier-plugin-tailwindcss@0.7.1: 1452 - resolution: {integrity: sha512-Bzv1LZcuiR1Sk02iJTS1QzlFNp/o5l2p3xkopwOrbPmtMeh3fK9rVW5M3neBQzHq+kGKj/4LGQMTNcTH4NGPtQ==} 1271 + prettier-plugin-tailwindcss@0.7.2: 1272 + resolution: {integrity: sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA==} 1453 1273 engines: {node: '>=20.19'} 1454 1274 peerDependencies: 1455 1275 '@ianvs/prettier-plugin-sort-imports': '*' ··· 1503 1323 prettier-plugin-svelte: 1504 1324 optional: true 1505 1325 1506 - prettier@3.6.2: 1507 - resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} 1326 + prettier@3.7.4: 1327 + resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} 1508 1328 engines: {node: '>=14'} 1509 1329 hasBin: true 1510 1330 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 1331 resolve-pkg-maps@1.0.0: 1521 1332 resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} 1522 1333 1523 - rollup@4.53.3: 1524 - resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==} 1334 + rollup@4.54.0: 1335 + resolution: {integrity: sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==} 1525 1336 engines: {node: '>=18.0.0', npm: '>=8.0.0'} 1526 1337 hasBin: true 1527 1338 1528 - safer-buffer@2.1.2: 1529 - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} 1339 + sax@1.4.3: 1340 + resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==} 1530 1341 1531 1342 semver@6.3.1: 1532 1343 resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} ··· 1557 1368 style-mod@4.1.3: 1558 1369 resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} 1559 1370 1560 - svgo@3.3.2: 1561 - resolution: {integrity: sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==} 1562 - engines: {node: '>=14.0.0'} 1371 + svgo@4.0.0: 1372 + resolution: {integrity: sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==} 1373 + engines: {node: '>=16'} 1563 1374 hasBin: true 1564 1375 1565 - tailwindcss@4.1.17: 1566 - resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==} 1376 + tailwindcss@4.1.18: 1377 + resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} 1567 1378 1568 1379 tapable@2.3.0: 1569 1380 resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} 1570 1381 engines: {node: '>=6'} 1571 1382 1572 - tar@6.2.1: 1573 - resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} 1574 - engines: {node: '>=10'} 1575 - 1576 1383 tinyexec@1.0.2: 1577 1384 resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} 1578 1385 engines: {node: '>=18'} ··· 1594 1401 ufo@1.6.1: 1595 1402 resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} 1596 1403 1404 + undici-types@6.21.0: 1405 + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} 1406 + 1597 1407 undici-types@7.16.0: 1598 1408 resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} 1599 1409 1600 - undici@6.22.0: 1601 - resolution: {integrity: sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==} 1602 - engines: {node: '>=18.17'} 1410 + unicode-segmenter@0.14.4: 1411 + resolution: {integrity: sha512-pR5VCiCrLrKOL6FRW61jnk9+wyMtKKowq+jyFY9oc6uHbWKhDL4yVRiI4YZPksGMK72Pahh8m0cn/0JvbDDyJg==} 1603 1412 1604 - update-browserslist-db@1.1.4: 1605 - resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} 1413 + update-browserslist-db@1.2.3: 1414 + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} 1606 1415 hasBin: true 1607 1416 peerDependencies: 1608 1417 browserslist: '>= 4.21.0' ··· 1617 1426 '@testing-library/jest-dom': 1618 1427 optional: true 1619 1428 1620 - vite@7.2.4: 1621 - resolution: {integrity: sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==} 1429 + vite@7.3.0: 1430 + resolution: {integrity: sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==} 1622 1431 engines: {node: ^20.19.0 || >=22.12.0} 1623 1432 hasBin: true 1624 1433 peerDependencies: ··· 1668 1477 w3c-keyname@2.2.8: 1669 1478 resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} 1670 1479 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 1480 yallist@3.1.1: 1683 1481 resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} 1684 1482 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 1483 yocto-queue@1.2.2: 1692 1484 resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} 1693 1485 engines: {node: '>=12.20'} ··· 1696 1488 1697 1489 '@antfu/install-pkg@1.1.0': 1698 1490 dependencies: 1699 - package-manager-detector: 1.5.0 1491 + package-manager-detector: 1.6.0 1700 1492 tinyexec: 1.0.2 1701 - 1702 - '@antfu/utils@8.1.1': {} 1703 1493 1704 1494 '@atcute/atproto@3.1.9': 1705 1495 dependencies: 1706 - '@atcute/lexicons': 1.2.4 1496 + '@atcute/lexicons': 1.2.6 1707 1497 1708 - '@atcute/bluesky@3.2.10': 1498 + '@atcute/bluesky@3.2.14': 1709 1499 dependencies: 1710 1500 '@atcute/atproto': 3.1.9 1711 - '@atcute/lexicons': 1.2.4 1501 + '@atcute/lexicons': 1.2.6 1712 1502 1713 1503 '@atcute/car@3.1.3': 1714 1504 dependencies: 1715 1505 '@atcute/cbor': 2.2.8 1716 - '@atcute/cid': 2.2.6 1717 - '@atcute/uint8array': 1.0.5 1506 + '@atcute/cid': 2.3.0 1507 + '@atcute/uint8array': 1.0.6 1718 1508 '@atcute/varint': 1.0.3 1719 1509 yocto-queue: 1.2.2 1720 1510 1721 1511 '@atcute/car@5.0.0': 1722 1512 dependencies: 1723 1513 '@atcute/cbor': 2.2.8 1724 - '@atcute/cid': 2.2.6 1725 - '@atcute/uint8array': 1.0.5 1514 + '@atcute/cid': 2.3.0 1515 + '@atcute/uint8array': 1.0.6 1726 1516 '@atcute/varint': 1.0.3 1727 1517 1728 1518 '@atcute/cbor@2.2.8': 1729 1519 dependencies: 1730 - '@atcute/cid': 2.2.6 1520 + '@atcute/cid': 2.3.0 1731 1521 '@atcute/multibase': 1.1.6 1732 - '@atcute/uint8array': 1.0.5 1522 + '@atcute/uint8array': 1.0.6 1733 1523 1734 - '@atcute/cid@2.2.6': 1524 + '@atcute/cid@2.3.0': 1735 1525 dependencies: 1736 1526 '@atcute/multibase': 1.1.6 1737 - '@atcute/uint8array': 1.0.5 1527 + '@atcute/uint8array': 1.0.6 1738 1528 1739 - '@atcute/client@4.0.5': 1529 + '@atcute/client@4.1.2': 1740 1530 dependencies: 1741 1531 '@atcute/identity': 1.1.3 1742 - '@atcute/lexicons': 1.2.4 1532 + '@atcute/lexicons': 1.2.6 1743 1533 1744 - '@atcute/crypto@2.2.6': 1534 + '@atcute/crypto@2.3.0': 1745 1535 dependencies: 1746 1536 '@atcute/multibase': 1.1.6 1747 - '@atcute/uint8array': 1.0.5 1537 + '@atcute/uint8array': 1.0.6 1748 1538 '@noble/secp256k1': 3.0.0 1749 1539 1750 - '@atcute/did-plc@0.2.0': 1540 + '@atcute/did-plc@0.3.1': 1751 1541 dependencies: 1752 1542 '@atcute/cbor': 2.2.8 1753 - '@atcute/cid': 2.2.6 1754 - '@atcute/crypto': 2.2.6 1543 + '@atcute/cid': 2.3.0 1544 + '@atcute/crypto': 2.3.0 1755 1545 '@atcute/identity': 1.1.3 1756 - '@atcute/lexicons': 1.2.4 1546 + '@atcute/lexicons': 1.2.6 1757 1547 '@atcute/multibase': 1.1.6 1758 - '@atcute/uint8array': 1.0.5 1548 + '@atcute/uint8array': 1.0.6 1549 + '@atcute/util-fetch': 1.0.4 1759 1550 '@badrap/valita': 0.4.6 1760 1551 1761 - '@atcute/identity-resolver@1.1.4(@atcute/identity@1.1.3)': 1552 + '@atcute/identity-resolver@1.2.1(@atcute/identity@1.1.3)': 1762 1553 dependencies: 1763 1554 '@atcute/identity': 1.1.3 1764 - '@atcute/lexicons': 1.2.4 1765 - '@atcute/util-fetch': 1.0.3 1555 + '@atcute/lexicons': 1.2.6 1556 + '@atcute/util-fetch': 1.0.4 1766 1557 '@badrap/valita': 0.4.6 1767 1558 1768 1559 '@atcute/identity@1.1.3': 1769 1560 dependencies: 1770 - '@atcute/lexicons': 1.2.4 1561 + '@atcute/lexicons': 1.2.6 1771 1562 '@badrap/valita': 0.4.6 1772 1563 1773 - '@atcute/leaflet@1.0.12': 1564 + '@atcute/leaflet@1.0.14': 1774 1565 dependencies: 1775 1566 '@atcute/atproto': 3.1.9 1776 - '@atcute/lexicons': 1.2.4 1567 + '@atcute/lexicons': 1.2.6 1777 1568 1778 - '@atcute/lexicon-doc@2.0.1': 1569 + '@atcute/lexicon-doc@2.0.6': 1779 1570 dependencies: 1780 1571 '@atcute/identity': 1.1.3 1781 - '@atcute/lexicons': 1.2.4 1572 + '@atcute/lexicons': 1.2.6 1573 + '@atcute/uint8array': 1.0.6 1574 + '@atcute/util-text': 0.0.1 1782 1575 '@badrap/valita': 0.4.6 1783 1576 1784 - '@atcute/lexicon-resolver@0.1.4(@atcute/identity-resolver@1.1.4(@atcute/identity@1.1.3))(@atcute/identity@1.1.3)': 1577 + '@atcute/lexicon-resolver@0.1.5(@atcute/identity-resolver@1.2.1(@atcute/identity@1.1.3))(@atcute/identity@1.1.3)': 1785 1578 dependencies: 1786 - '@atcute/crypto': 2.2.6 1579 + '@atcute/crypto': 2.3.0 1787 1580 '@atcute/identity': 1.1.3 1788 - '@atcute/identity-resolver': 1.1.4(@atcute/identity@1.1.3) 1789 - '@atcute/lexicon-doc': 2.0.1 1790 - '@atcute/lexicons': 1.2.4 1791 - '@atcute/repo': 0.1.0 1792 - '@atcute/util-fetch': 1.0.3 1581 + '@atcute/identity-resolver': 1.2.1(@atcute/identity@1.1.3) 1582 + '@atcute/lexicon-doc': 2.0.6 1583 + '@atcute/lexicons': 1.2.6 1584 + '@atcute/repo': 0.1.1 1585 + '@atcute/util-fetch': 1.0.4 1793 1586 '@badrap/valita': 0.4.6 1794 1587 1795 - '@atcute/lexicons@1.2.4': 1588 + '@atcute/lexicons@1.2.6': 1796 1589 dependencies: 1797 - '@standard-schema/spec': 1.0.0 1590 + '@atcute/uint8array': 1.0.6 1591 + '@atcute/util-text': 0.0.1 1592 + '@standard-schema/spec': 1.1.0 1798 1593 esm-env: 1.2.2 1799 1594 1800 - '@atcute/mst@0.1.0': 1595 + '@atcute/mst@0.1.1': 1801 1596 dependencies: 1802 1597 '@atcute/cbor': 2.2.8 1803 - '@atcute/cid': 2.2.6 1804 - '@atcute/uint8array': 1.0.5 1598 + '@atcute/cid': 2.3.0 1599 + '@atcute/uint8array': 1.0.6 1805 1600 1806 1601 '@atcute/multibase@1.1.6': 1807 1602 dependencies: 1808 - '@atcute/uint8array': 1.0.5 1603 + '@atcute/uint8array': 1.0.6 1809 1604 1810 - '@atcute/oauth-browser-client@2.0.1': 1605 + '@atcute/oauth-browser-client@2.0.3(@atcute/identity@1.1.3)': 1811 1606 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 1607 + '@atcute/client': 4.1.2 1608 + '@atcute/identity-resolver': 1.2.1(@atcute/identity@1.1.3) 1609 + '@atcute/lexicons': 1.2.6 1816 1610 '@atcute/multibase': 1.1.6 1817 - '@atcute/uint8array': 1.0.5 1611 + '@atcute/uint8array': 1.0.6 1818 1612 nanoid: 5.1.6 1613 + transitivePeerDependencies: 1614 + - '@atcute/identity' 1819 1615 1820 - '@atcute/repo@0.1.0': 1616 + '@atcute/repo@0.1.1': 1821 1617 dependencies: 1822 1618 '@atcute/car': 5.0.0 1823 1619 '@atcute/cbor': 2.2.8 1824 - '@atcute/cid': 2.2.6 1825 - '@atcute/crypto': 2.2.6 1826 - '@atcute/lexicons': 1.2.4 1827 - '@atcute/mst': 0.1.0 1828 - '@atcute/uint8array': 1.0.5 1620 + '@atcute/cid': 2.3.0 1621 + '@atcute/crypto': 2.3.0 1622 + '@atcute/lexicons': 1.2.6 1623 + '@atcute/mst': 0.1.1 1624 + '@atcute/uint8array': 1.0.6 1829 1625 1830 - '@atcute/tangled@1.0.12': 1626 + '@atcute/tangled@1.0.13': 1831 1627 dependencies: 1832 1628 '@atcute/atproto': 3.1.9 1833 - '@atcute/lexicons': 1.2.4 1629 + '@atcute/lexicons': 1.2.6 1834 1630 1835 - '@atcute/tid@1.0.3': {} 1631 + '@atcute/tid@1.1.0': 1632 + dependencies: 1633 + '@atcute/time-ms': 1.0.0 1836 1634 1837 - '@atcute/uint8array@1.0.5': {} 1635 + '@atcute/time-ms@1.0.0': 1636 + dependencies: 1637 + '@types/node': 22.19.3 1638 + node-gyp-build: 4.8.4 1639 + 1640 + '@atcute/uint8array@1.0.6': {} 1838 1641 1839 - '@atcute/util-fetch@1.0.3': 1642 + '@atcute/util-fetch@1.0.4': 1840 1643 dependencies: 1841 1644 '@badrap/valita': 0.4.6 1645 + 1646 + '@atcute/util-text@0.0.1': 1647 + dependencies: 1648 + unicode-segmenter: 0.14.4 1842 1649 1843 1650 '@atcute/varint@1.0.3': {} 1844 1651 ··· 1882 1689 dependencies: 1883 1690 '@babel/compat-data': 7.28.5 1884 1691 '@babel/helper-validator-option': 7.27.1 1885 - browserslist: 4.28.0 1692 + browserslist: 4.28.1 1886 1693 lru-cache: 5.1.1 1887 1694 semver: 6.3.1 1888 1695 ··· 1957 1764 1958 1765 '@codemirror/autocomplete@6.20.0': 1959 1766 dependencies: 1960 - '@codemirror/language': 6.11.3 1961 - '@codemirror/state': 6.5.2 1962 - '@codemirror/view': 6.38.8 1963 - '@lezer/common': 1.3.0 1767 + '@codemirror/language': 6.12.1 1768 + '@codemirror/state': 6.5.3 1769 + '@codemirror/view': 6.39.7 1770 + '@lezer/common': 1.5.0 1964 1771 1965 - '@codemirror/commands@6.10.0': 1772 + '@codemirror/commands@6.10.1': 1966 1773 dependencies: 1967 - '@codemirror/language': 6.11.3 1968 - '@codemirror/state': 6.5.2 1969 - '@codemirror/view': 6.38.8 1970 - '@lezer/common': 1.3.0 1774 + '@codemirror/language': 6.12.1 1775 + '@codemirror/state': 6.5.3 1776 + '@codemirror/view': 6.39.7 1777 + '@lezer/common': 1.5.0 1971 1778 1972 1779 '@codemirror/lang-json@6.0.2': 1973 1780 dependencies: 1974 - '@codemirror/language': 6.11.3 1781 + '@codemirror/language': 6.12.1 1975 1782 '@lezer/json': 1.0.3 1976 1783 1977 - '@codemirror/language@6.11.3': 1784 + '@codemirror/language@6.12.1': 1978 1785 dependencies: 1979 - '@codemirror/state': 6.5.2 1980 - '@codemirror/view': 6.38.8 1981 - '@lezer/common': 1.3.0 1786 + '@codemirror/state': 6.5.3 1787 + '@codemirror/view': 6.39.7 1788 + '@lezer/common': 1.5.0 1982 1789 '@lezer/highlight': 1.2.3 1983 - '@lezer/lr': 1.4.3 1790 + '@lezer/lr': 1.4.5 1984 1791 style-mod: 4.1.3 1985 1792 1986 1793 '@codemirror/lint@6.9.2': 1987 1794 dependencies: 1988 - '@codemirror/state': 6.5.2 1989 - '@codemirror/view': 6.38.8 1795 + '@codemirror/state': 6.5.3 1796 + '@codemirror/view': 6.39.7 1990 1797 crelt: 1.0.6 1991 1798 1992 1799 '@codemirror/search@6.5.11': 1993 1800 dependencies: 1994 - '@codemirror/state': 6.5.2 1995 - '@codemirror/view': 6.38.8 1801 + '@codemirror/state': 6.5.3 1802 + '@codemirror/view': 6.39.7 1996 1803 crelt: 1.0.6 1997 1804 1998 - '@codemirror/state@6.5.2': 1805 + '@codemirror/state@6.5.3': 1999 1806 dependencies: 2000 1807 '@marijn/find-cluster-break': 1.0.2 2001 1808 2002 - '@codemirror/view@6.38.8': 1809 + '@codemirror/view@6.39.7': 2003 1810 dependencies: 2004 - '@codemirror/state': 6.5.2 1811 + '@codemirror/state': 6.5.3 2005 1812 crelt: 1.0.6 2006 1813 style-mod: 4.1.3 2007 1814 w3c-keyname: 2.2.8 2008 1815 1816 + '@cyberalien/svg-utils@1.0.11': 1817 + dependencies: 1818 + '@iconify/types': 2.0.0 1819 + 2009 1820 '@esbuild/aix-ppc64@0.23.1': 2010 1821 optional: true 2011 1822 2012 - '@esbuild/aix-ppc64@0.25.12': 1823 + '@esbuild/aix-ppc64@0.27.2': 2013 1824 optional: true 2014 1825 2015 1826 '@esbuild/android-arm64@0.23.1': 2016 1827 optional: true 2017 1828 2018 - '@esbuild/android-arm64@0.25.12': 1829 + '@esbuild/android-arm64@0.27.2': 2019 1830 optional: true 2020 1831 2021 1832 '@esbuild/android-arm@0.23.1': 2022 1833 optional: true 2023 1834 2024 - '@esbuild/android-arm@0.25.12': 1835 + '@esbuild/android-arm@0.27.2': 2025 1836 optional: true 2026 1837 2027 1838 '@esbuild/android-x64@0.23.1': 2028 1839 optional: true 2029 1840 2030 - '@esbuild/android-x64@0.25.12': 1841 + '@esbuild/android-x64@0.27.2': 2031 1842 optional: true 2032 1843 2033 1844 '@esbuild/darwin-arm64@0.23.1': 2034 1845 optional: true 2035 1846 2036 - '@esbuild/darwin-arm64@0.25.12': 1847 + '@esbuild/darwin-arm64@0.27.2': 2037 1848 optional: true 2038 1849 2039 1850 '@esbuild/darwin-x64@0.23.1': 2040 1851 optional: true 2041 1852 2042 - '@esbuild/darwin-x64@0.25.12': 1853 + '@esbuild/darwin-x64@0.27.2': 2043 1854 optional: true 2044 1855 2045 1856 '@esbuild/freebsd-arm64@0.23.1': 2046 1857 optional: true 2047 1858 2048 - '@esbuild/freebsd-arm64@0.25.12': 1859 + '@esbuild/freebsd-arm64@0.27.2': 2049 1860 optional: true 2050 1861 2051 1862 '@esbuild/freebsd-x64@0.23.1': 2052 1863 optional: true 2053 1864 2054 - '@esbuild/freebsd-x64@0.25.12': 1865 + '@esbuild/freebsd-x64@0.27.2': 2055 1866 optional: true 2056 1867 2057 1868 '@esbuild/linux-arm64@0.23.1': 2058 1869 optional: true 2059 1870 2060 - '@esbuild/linux-arm64@0.25.12': 1871 + '@esbuild/linux-arm64@0.27.2': 2061 1872 optional: true 2062 1873 2063 1874 '@esbuild/linux-arm@0.23.1': 2064 1875 optional: true 2065 1876 2066 - '@esbuild/linux-arm@0.25.12': 1877 + '@esbuild/linux-arm@0.27.2': 2067 1878 optional: true 2068 1879 2069 1880 '@esbuild/linux-ia32@0.23.1': 2070 1881 optional: true 2071 1882 2072 - '@esbuild/linux-ia32@0.25.12': 1883 + '@esbuild/linux-ia32@0.27.2': 2073 1884 optional: true 2074 1885 2075 1886 '@esbuild/linux-loong64@0.23.1': 2076 1887 optional: true 2077 1888 2078 - '@esbuild/linux-loong64@0.25.12': 1889 + '@esbuild/linux-loong64@0.27.2': 2079 1890 optional: true 2080 1891 2081 1892 '@esbuild/linux-mips64el@0.23.1': 2082 1893 optional: true 2083 1894 2084 - '@esbuild/linux-mips64el@0.25.12': 1895 + '@esbuild/linux-mips64el@0.27.2': 2085 1896 optional: true 2086 1897 2087 1898 '@esbuild/linux-ppc64@0.23.1': 2088 1899 optional: true 2089 1900 2090 - '@esbuild/linux-ppc64@0.25.12': 1901 + '@esbuild/linux-ppc64@0.27.2': 2091 1902 optional: true 2092 1903 2093 1904 '@esbuild/linux-riscv64@0.23.1': 2094 1905 optional: true 2095 1906 2096 - '@esbuild/linux-riscv64@0.25.12': 1907 + '@esbuild/linux-riscv64@0.27.2': 2097 1908 optional: true 2098 1909 2099 1910 '@esbuild/linux-s390x@0.23.1': 2100 1911 optional: true 2101 1912 2102 - '@esbuild/linux-s390x@0.25.12': 1913 + '@esbuild/linux-s390x@0.27.2': 2103 1914 optional: true 2104 1915 2105 1916 '@esbuild/linux-x64@0.23.1': 2106 1917 optional: true 2107 1918 2108 - '@esbuild/linux-x64@0.25.12': 1919 + '@esbuild/linux-x64@0.27.2': 2109 1920 optional: true 2110 1921 2111 - '@esbuild/netbsd-arm64@0.25.12': 1922 + '@esbuild/netbsd-arm64@0.27.2': 2112 1923 optional: true 2113 1924 2114 1925 '@esbuild/netbsd-x64@0.23.1': 2115 1926 optional: true 2116 1927 2117 - '@esbuild/netbsd-x64@0.25.12': 1928 + '@esbuild/netbsd-x64@0.27.2': 2118 1929 optional: true 2119 1930 2120 1931 '@esbuild/openbsd-arm64@0.23.1': 2121 1932 optional: true 2122 1933 2123 - '@esbuild/openbsd-arm64@0.25.12': 1934 + '@esbuild/openbsd-arm64@0.27.2': 2124 1935 optional: true 2125 1936 2126 1937 '@esbuild/openbsd-x64@0.23.1': 2127 1938 optional: true 2128 1939 2129 - '@esbuild/openbsd-x64@0.25.12': 1940 + '@esbuild/openbsd-x64@0.27.2': 2130 1941 optional: true 2131 1942 2132 - '@esbuild/openharmony-arm64@0.25.12': 1943 + '@esbuild/openharmony-arm64@0.27.2': 2133 1944 optional: true 2134 1945 2135 1946 '@esbuild/sunos-x64@0.23.1': 2136 1947 optional: true 2137 1948 2138 - '@esbuild/sunos-x64@0.25.12': 1949 + '@esbuild/sunos-x64@0.27.2': 2139 1950 optional: true 2140 1951 2141 1952 '@esbuild/win32-arm64@0.23.1': 2142 1953 optional: true 2143 1954 2144 - '@esbuild/win32-arm64@0.25.12': 1955 + '@esbuild/win32-arm64@0.27.2': 2145 1956 optional: true 2146 1957 2147 1958 '@esbuild/win32-ia32@0.23.1': 2148 1959 optional: true 2149 1960 2150 - '@esbuild/win32-ia32@0.25.12': 1961 + '@esbuild/win32-ia32@0.27.2': 2151 1962 optional: true 2152 1963 2153 1964 '@esbuild/win32-x64@0.23.1': 2154 1965 optional: true 2155 1966 2156 - '@esbuild/win32-x64@0.25.12': 1967 + '@esbuild/win32-x64@0.27.2': 2157 1968 optional: true 2158 1969 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)': 1970 + '@fsegurai/codemirror-theme-basic-dark@6.2.3(@codemirror/language@6.12.1)(@codemirror/state@6.5.3)(@codemirror/view@6.39.7)(@lezer/highlight@1.2.3)': 2160 1971 dependencies: 2161 - '@codemirror/language': 6.11.3 2162 - '@codemirror/state': 6.5.2 2163 - '@codemirror/view': 6.38.8 1972 + '@codemirror/language': 6.12.1 1973 + '@codemirror/state': 6.5.3 1974 + '@codemirror/view': 6.39.7 2164 1975 '@lezer/highlight': 1.2.3 2165 1976 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)': 1977 + '@fsegurai/codemirror-theme-basic-light@6.2.3(@codemirror/language@6.12.1)(@codemirror/state@6.5.3)(@codemirror/view@6.39.7)(@lezer/highlight@1.2.3)': 2167 1978 dependencies: 2168 - '@codemirror/language': 6.11.3 2169 - '@codemirror/state': 6.5.2 2170 - '@codemirror/view': 6.38.8 1979 + '@codemirror/language': 6.12.1 1980 + '@codemirror/state': 6.5.3 1981 + '@codemirror/view': 6.39.7 2171 1982 '@lezer/highlight': 1.2.3 2172 1983 2173 - '@iconify-json/lucide@1.2.75': 1984 + '@iconify-json/lucide@1.2.82': 2174 1985 dependencies: 2175 1986 '@iconify/types': 2.0.0 2176 1987 2177 - '@iconify/tailwind4@1.1.0(tailwindcss@4.1.17)': 1988 + '@iconify/tailwind4@1.2.0(tailwindcss@4.1.18)': 2178 1989 dependencies: 2179 - '@iconify/tools': 4.1.4 1990 + '@iconify/tools': 5.0.1 2180 1991 '@iconify/types': 2.0.0 2181 - '@iconify/utils': 2.3.0 2182 - tailwindcss: 4.1.17 2183 - transitivePeerDependencies: 2184 - - debug 2185 - - supports-color 1992 + '@iconify/utils': 3.1.0 1993 + tailwindcss: 4.1.18 2186 1994 2187 - '@iconify/tools@4.1.4': 1995 + '@iconify/tools@5.0.1': 2188 1996 dependencies: 1997 + '@cyberalien/svg-utils': 1.0.11 2189 1998 '@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 1999 + '@iconify/utils': 3.1.0 2000 + fflate: 0.8.2 2001 + modern-tar: 0.7.3 2002 + pathe: 2.0.3 2003 + svgo: 4.0.0 2203 2004 2204 2005 '@iconify/types@2.0.0': {} 2205 2006 2206 - '@iconify/utils@2.3.0': 2007 + '@iconify/utils@3.1.0': 2207 2008 dependencies: 2208 2009 '@antfu/install-pkg': 1.1.0 2209 - '@antfu/utils': 8.1.1 2210 2010 '@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 2011 mlly: 1.8.0 2216 - transitivePeerDependencies: 2217 - - supports-color 2218 2012 2219 2013 '@jridgewell/gen-mapping@0.3.13': 2220 2014 dependencies: ··· 2237 2031 2238 2032 '@jsr/mary__exif-rm@0.2.2': {} 2239 2033 2240 - '@lezer/common@1.3.0': {} 2034 + '@lezer/common@1.5.0': {} 2241 2035 2242 2036 '@lezer/highlight@1.2.3': 2243 2037 dependencies: 2244 - '@lezer/common': 1.3.0 2038 + '@lezer/common': 1.5.0 2245 2039 2246 2040 '@lezer/json@1.0.3': 2247 2041 dependencies: 2248 - '@lezer/common': 1.3.0 2042 + '@lezer/common': 1.5.0 2249 2043 '@lezer/highlight': 1.2.3 2250 - '@lezer/lr': 1.4.3 2044 + '@lezer/lr': 1.4.5 2251 2045 2252 - '@lezer/lr@1.4.3': 2046 + '@lezer/lr@1.4.5': 2253 2047 dependencies: 2254 - '@lezer/common': 1.3.0 2048 + '@lezer/common': 1.5.0 2255 2049 2256 2050 '@marijn/find-cluster-break@1.0.2': {} 2257 2051 2258 2052 '@noble/secp256k1@3.0.0': {} 2259 2053 2260 - '@rollup/rollup-android-arm-eabi@4.53.3': 2054 + '@rollup/rollup-android-arm-eabi@4.54.0': 2261 2055 optional: true 2262 2056 2263 - '@rollup/rollup-android-arm64@4.53.3': 2057 + '@rollup/rollup-android-arm64@4.54.0': 2264 2058 optional: true 2265 2059 2266 - '@rollup/rollup-darwin-arm64@4.53.3': 2060 + '@rollup/rollup-darwin-arm64@4.54.0': 2267 2061 optional: true 2268 2062 2269 - '@rollup/rollup-darwin-x64@4.53.3': 2063 + '@rollup/rollup-darwin-x64@4.54.0': 2270 2064 optional: true 2271 2065 2272 - '@rollup/rollup-freebsd-arm64@4.53.3': 2066 + '@rollup/rollup-freebsd-arm64@4.54.0': 2273 2067 optional: true 2274 2068 2275 - '@rollup/rollup-freebsd-x64@4.53.3': 2069 + '@rollup/rollup-freebsd-x64@4.54.0': 2276 2070 optional: true 2277 2071 2278 - '@rollup/rollup-linux-arm-gnueabihf@4.53.3': 2072 + '@rollup/rollup-linux-arm-gnueabihf@4.54.0': 2279 2073 optional: true 2280 2074 2281 - '@rollup/rollup-linux-arm-musleabihf@4.53.3': 2075 + '@rollup/rollup-linux-arm-musleabihf@4.54.0': 2282 2076 optional: true 2283 2077 2284 - '@rollup/rollup-linux-arm64-gnu@4.53.3': 2078 + '@rollup/rollup-linux-arm64-gnu@4.54.0': 2285 2079 optional: true 2286 2080 2287 - '@rollup/rollup-linux-arm64-musl@4.53.3': 2081 + '@rollup/rollup-linux-arm64-musl@4.54.0': 2288 2082 optional: true 2289 2083 2290 - '@rollup/rollup-linux-loong64-gnu@4.53.3': 2084 + '@rollup/rollup-linux-loong64-gnu@4.54.0': 2291 2085 optional: true 2292 2086 2293 - '@rollup/rollup-linux-ppc64-gnu@4.53.3': 2087 + '@rollup/rollup-linux-ppc64-gnu@4.54.0': 2294 2088 optional: true 2295 2089 2296 - '@rollup/rollup-linux-riscv64-gnu@4.53.3': 2090 + '@rollup/rollup-linux-riscv64-gnu@4.54.0': 2297 2091 optional: true 2298 2092 2299 - '@rollup/rollup-linux-riscv64-musl@4.53.3': 2093 + '@rollup/rollup-linux-riscv64-musl@4.54.0': 2300 2094 optional: true 2301 2095 2302 - '@rollup/rollup-linux-s390x-gnu@4.53.3': 2096 + '@rollup/rollup-linux-s390x-gnu@4.54.0': 2303 2097 optional: true 2304 2098 2305 - '@rollup/rollup-linux-x64-gnu@4.53.3': 2099 + '@rollup/rollup-linux-x64-gnu@4.54.0': 2306 2100 optional: true 2307 2101 2308 - '@rollup/rollup-linux-x64-musl@4.53.3': 2102 + '@rollup/rollup-linux-x64-musl@4.54.0': 2309 2103 optional: true 2310 2104 2311 - '@rollup/rollup-openharmony-arm64@4.53.3': 2105 + '@rollup/rollup-openharmony-arm64@4.54.0': 2312 2106 optional: true 2313 2107 2314 - '@rollup/rollup-win32-arm64-msvc@4.53.3': 2108 + '@rollup/rollup-win32-arm64-msvc@4.54.0': 2315 2109 optional: true 2316 2110 2317 - '@rollup/rollup-win32-ia32-msvc@4.53.3': 2111 + '@rollup/rollup-win32-ia32-msvc@4.54.0': 2318 2112 optional: true 2319 2113 2320 - '@rollup/rollup-win32-x64-gnu@4.53.3': 2114 + '@rollup/rollup-win32-x64-gnu@4.54.0': 2321 2115 optional: true 2322 2116 2323 - '@rollup/rollup-win32-x64-msvc@4.53.3': 2117 + '@rollup/rollup-win32-x64-msvc@4.54.0': 2324 2118 optional: true 2325 2119 2326 2120 '@skyware/firehose@0.5.2': ··· 2337 2131 dependencies: 2338 2132 solid-js: 1.9.10 2339 2133 2340 - '@standard-schema/spec@1.0.0': {} 2134 + '@standard-schema/spec@1.1.0': {} 2341 2135 2342 - '@tailwindcss/node@4.1.17': 2136 + '@tailwindcss/node@4.1.18': 2343 2137 dependencies: 2344 2138 '@jridgewell/remapping': 2.3.5 2345 - enhanced-resolve: 5.18.3 2139 + enhanced-resolve: 5.18.4 2346 2140 jiti: 2.6.1 2347 2141 lightningcss: 1.30.2 2348 2142 magic-string: 0.30.21 2349 2143 source-map-js: 1.2.1 2350 - tailwindcss: 4.1.17 2144 + tailwindcss: 4.1.18 2351 2145 2352 - '@tailwindcss/oxide-android-arm64@4.1.17': 2146 + '@tailwindcss/oxide-android-arm64@4.1.18': 2353 2147 optional: true 2354 2148 2355 - '@tailwindcss/oxide-darwin-arm64@4.1.17': 2149 + '@tailwindcss/oxide-darwin-arm64@4.1.18': 2356 2150 optional: true 2357 2151 2358 - '@tailwindcss/oxide-darwin-x64@4.1.17': 2152 + '@tailwindcss/oxide-darwin-x64@4.1.18': 2359 2153 optional: true 2360 2154 2361 - '@tailwindcss/oxide-freebsd-x64@4.1.17': 2155 + '@tailwindcss/oxide-freebsd-x64@4.1.18': 2362 2156 optional: true 2363 2157 2364 - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17': 2158 + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': 2365 2159 optional: true 2366 2160 2367 - '@tailwindcss/oxide-linux-arm64-gnu@4.1.17': 2161 + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': 2368 2162 optional: true 2369 2163 2370 - '@tailwindcss/oxide-linux-arm64-musl@4.1.17': 2164 + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': 2371 2165 optional: true 2372 2166 2373 - '@tailwindcss/oxide-linux-x64-gnu@4.1.17': 2167 + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': 2374 2168 optional: true 2375 2169 2376 - '@tailwindcss/oxide-linux-x64-musl@4.1.17': 2170 + '@tailwindcss/oxide-linux-x64-musl@4.1.18': 2377 2171 optional: true 2378 2172 2379 - '@tailwindcss/oxide-wasm32-wasi@4.1.17': 2173 + '@tailwindcss/oxide-wasm32-wasi@4.1.18': 2380 2174 optional: true 2381 2175 2382 - '@tailwindcss/oxide-win32-arm64-msvc@4.1.17': 2176 + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': 2383 2177 optional: true 2384 2178 2385 - '@tailwindcss/oxide-win32-x64-msvc@4.1.17': 2179 + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': 2386 2180 optional: true 2387 2181 2388 - '@tailwindcss/oxide@4.1.17': 2182 + '@tailwindcss/oxide@4.1.18': 2389 2183 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 2184 + '@tailwindcss/oxide-android-arm64': 4.1.18 2185 + '@tailwindcss/oxide-darwin-arm64': 4.1.18 2186 + '@tailwindcss/oxide-darwin-x64': 4.1.18 2187 + '@tailwindcss/oxide-freebsd-x64': 4.1.18 2188 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18 2189 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.18 2190 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.18 2191 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.18 2192 + '@tailwindcss/oxide-linux-x64-musl': 4.1.18 2193 + '@tailwindcss/oxide-wasm32-wasi': 4.1.18 2194 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 2195 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 2402 2196 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))': 2197 + '@tailwindcss/vite@4.1.18(vite@7.3.0(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2))': 2404 2198 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': {} 2199 + '@tailwindcss/node': 4.1.18 2200 + '@tailwindcss/oxide': 4.1.18 2201 + tailwindcss: 4.1.18 2202 + vite: 7.3.0(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2) 2411 2203 2412 2204 '@types/babel__core@7.20.5': 2413 2205 dependencies: ··· 2432 2224 2433 2225 '@types/estree@1.0.8': {} 2434 2226 2227 + '@types/node@22.19.3': 2228 + dependencies: 2229 + undici-types: 6.21.0 2230 + 2435 2231 '@types/node@24.10.1': 2436 2232 dependencies: 2437 2233 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 2234 optional: true 2448 2235 2449 2236 acorn@8.15.0: {} 2450 2237 2451 - asynckit@0.4.0: {} 2452 - 2453 - axios@1.13.2: 2454 - dependencies: 2455 - follow-redirects: 1.15.11 2456 - form-data: 4.0.5 2457 - proxy-from-env: 1.1.0 2458 - transitivePeerDependencies: 2459 - - debug 2460 - 2461 2238 babel-plugin-jsx-dom-expressions@0.40.3(@babel/core@7.28.5): 2462 2239 dependencies: 2463 2240 '@babel/core': 7.28.5 ··· 2474 2251 optionalDependencies: 2475 2252 solid-js: 1.9.10 2476 2253 2477 - baseline-browser-mapping@2.8.31: {} 2254 + baseline-browser-mapping@2.9.11: {} 2478 2255 2479 2256 boolbase@1.0.0: {} 2480 2257 2481 - browserslist@4.28.0: 2258 + browserslist@4.28.1: 2482 2259 dependencies: 2483 - baseline-browser-mapping: 2.8.31 2484 - caniuse-lite: 1.0.30001757 2485 - electron-to-chromium: 1.5.259 2260 + baseline-browser-mapping: 2.9.11 2261 + caniuse-lite: 1.0.30001761 2262 + electron-to-chromium: 1.5.267 2486 2263 node-releases: 2.0.27 2487 - update-browserslist-db: 1.1.4(browserslist@4.28.0) 2488 - 2489 - buffer-crc32@0.2.13: {} 2490 - 2491 - call-bind-apply-helpers@1.0.2: 2492 - dependencies: 2493 - es-errors: 1.3.0 2494 - function-bind: 1.1.2 2264 + update-browserslist-db: 1.2.3(browserslist@4.28.1) 2495 2265 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: {} 2266 + caniuse-lite@1.0.30001761: {} 2522 2267 2523 2268 codemirror@6.0.2: 2524 2269 dependencies: 2525 2270 '@codemirror/autocomplete': 6.20.0 2526 - '@codemirror/commands': 6.10.0 2527 - '@codemirror/language': 6.11.3 2271 + '@codemirror/commands': 6.10.1 2272 + '@codemirror/language': 6.12.1 2528 2273 '@codemirror/lint': 6.9.2 2529 2274 '@codemirror/search': 6.5.11 2530 - '@codemirror/state': 6.5.2 2531 - '@codemirror/view': 6.38.8 2275 + '@codemirror/state': 6.5.3 2276 + '@codemirror/view': 6.39.7 2532 2277 2533 - combined-stream@1.0.8: 2534 - dependencies: 2535 - delayed-stream: 1.0.0 2536 - 2537 - commander@7.2.0: {} 2278 + commander@11.1.0: {} 2538 2279 2539 2280 confbox@0.1.8: {} 2540 2281 2541 - confbox@0.2.2: {} 2542 - 2543 2282 convert-source-map@2.0.0: {} 2544 2283 2545 2284 crelt@1.0.6: {} ··· 2557 2296 mdn-data: 2.0.28 2558 2297 source-map-js: 1.2.1 2559 2298 2560 - css-tree@2.3.1: 2299 + css-tree@3.1.0: 2561 2300 dependencies: 2562 - mdn-data: 2.0.30 2301 + mdn-data: 2.12.2 2563 2302 source-map-js: 1.2.1 2564 2303 2565 2304 css-what@6.2.2: {} ··· 2573 2312 debug@4.4.3: 2574 2313 dependencies: 2575 2314 ms: 2.1.3 2576 - 2577 - delayed-stream@1.0.0: {} 2578 2315 2579 2316 detect-libc@2.1.2: {} 2580 2317 ··· 2596 2333 domelementtype: 2.3.0 2597 2334 domhandler: 5.0.3 2598 2335 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 2336 + electron-to-chromium@1.5.267: {} 2604 2337 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: 2338 + enhanced-resolve@5.18.4: 2617 2339 dependencies: 2618 2340 graceful-fs: 4.2.11 2619 2341 tapable: 2.3.0 ··· 2622 2344 2623 2345 entities@6.0.1: {} 2624 2346 2625 - es-define-property@1.0.1: {} 2626 - 2627 - es-errors@1.3.0: {} 2628 - 2629 - es-object-atoms@1.1.1: 2630 - dependencies: 2631 - es-errors: 1.3.0 2632 - 2633 - es-set-tostringtag@2.1.0: 2634 - dependencies: 2635 - es-errors: 1.3.0 2636 - get-intrinsic: 1.3.0 2637 - has-tostringtag: 1.0.2 2638 - hasown: 2.0.2 2639 - 2640 2347 esbuild@0.23.1: 2641 2348 optionalDependencies: 2642 2349 '@esbuild/aix-ppc64': 0.23.1 ··· 2665 2372 '@esbuild/win32-x64': 0.23.1 2666 2373 optional: true 2667 2374 2668 - esbuild@0.25.12: 2375 + esbuild@0.27.2: 2669 2376 optionalDependencies: 2670 - '@esbuild/aix-ppc64': 0.25.12 2671 - '@esbuild/android-arm': 0.25.12 2672 - '@esbuild/android-arm64': 0.25.12 2673 - '@esbuild/android-x64': 0.25.12 2674 - '@esbuild/darwin-arm64': 0.25.12 2675 - '@esbuild/darwin-x64': 0.25.12 2676 - '@esbuild/freebsd-arm64': 0.25.12 2677 - '@esbuild/freebsd-x64': 0.25.12 2678 - '@esbuild/linux-arm': 0.25.12 2679 - '@esbuild/linux-arm64': 0.25.12 2680 - '@esbuild/linux-ia32': 0.25.12 2681 - '@esbuild/linux-loong64': 0.25.12 2682 - '@esbuild/linux-mips64el': 0.25.12 2683 - '@esbuild/linux-ppc64': 0.25.12 2684 - '@esbuild/linux-riscv64': 0.25.12 2685 - '@esbuild/linux-s390x': 0.25.12 2686 - '@esbuild/linux-x64': 0.25.12 2687 - '@esbuild/netbsd-arm64': 0.25.12 2688 - '@esbuild/netbsd-x64': 0.25.12 2689 - '@esbuild/openbsd-arm64': 0.25.12 2690 - '@esbuild/openbsd-x64': 0.25.12 2691 - '@esbuild/openharmony-arm64': 0.25.12 2692 - '@esbuild/sunos-x64': 0.25.12 2693 - '@esbuild/win32-arm64': 0.25.12 2694 - '@esbuild/win32-ia32': 0.25.12 2695 - '@esbuild/win32-x64': 0.25.12 2377 + '@esbuild/aix-ppc64': 0.27.2 2378 + '@esbuild/android-arm': 0.27.2 2379 + '@esbuild/android-arm64': 0.27.2 2380 + '@esbuild/android-x64': 0.27.2 2381 + '@esbuild/darwin-arm64': 0.27.2 2382 + '@esbuild/darwin-x64': 0.27.2 2383 + '@esbuild/freebsd-arm64': 0.27.2 2384 + '@esbuild/freebsd-x64': 0.27.2 2385 + '@esbuild/linux-arm': 0.27.2 2386 + '@esbuild/linux-arm64': 0.27.2 2387 + '@esbuild/linux-ia32': 0.27.2 2388 + '@esbuild/linux-loong64': 0.27.2 2389 + '@esbuild/linux-mips64el': 0.27.2 2390 + '@esbuild/linux-ppc64': 0.27.2 2391 + '@esbuild/linux-riscv64': 0.27.2 2392 + '@esbuild/linux-s390x': 0.27.2 2393 + '@esbuild/linux-x64': 0.27.2 2394 + '@esbuild/netbsd-arm64': 0.27.2 2395 + '@esbuild/netbsd-x64': 0.27.2 2396 + '@esbuild/openbsd-arm64': 0.27.2 2397 + '@esbuild/openbsd-x64': 0.27.2 2398 + '@esbuild/openharmony-arm64': 0.27.2 2399 + '@esbuild/sunos-x64': 0.27.2 2400 + '@esbuild/win32-arm64': 0.27.2 2401 + '@esbuild/win32-ia32': 0.27.2 2402 + '@esbuild/win32-x64': 0.27.2 2696 2403 2697 2404 escalade@3.2.0: {} 2698 2405 2699 2406 esm-env@1.2.2: {} 2700 2407 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 2408 fdir@6.5.0(picomatch@4.0.3): 2718 2409 optionalDependencies: 2719 2410 picomatch: 4.0.3 2720 2411 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 2412 + fflate@0.8.2: {} 2734 2413 2735 2414 fsevents@2.3.3: 2736 2415 optional: true 2737 - 2738 - function-bind@1.1.2: {} 2739 2416 2740 2417 gensync@1.0.0-beta.2: {} 2741 2418 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 2419 get-tsconfig@4.13.0: 2765 2420 dependencies: 2766 2421 resolve-pkg-maps: 1.0.0 2767 2422 optional: true 2768 - 2769 - globals@15.15.0: {} 2770 - 2771 - gopd@1.2.0: {} 2772 2423 2773 2424 graceful-fs@4.2.11: {} 2774 2425 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 2426 html-entities@2.3.3: {} 2786 2427 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 2428 is-what@4.1.16: {} 2799 2429 2800 2430 jiti@2.6.1: {} ··· 2804 2434 jsesc@3.1.0: {} 2805 2435 2806 2436 json5@2.2.3: {} 2807 - 2808 - kolorist@1.8.0: {} 2809 2437 2810 2438 lightningcss-android-arm64@1.30.2: 2811 2439 optional: true ··· 2856 2484 lightningcss-win32-arm64-msvc: 1.30.2 2857 2485 lightningcss-win32-x64-msvc: 1.30.2 2858 2486 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 2487 lru-cache@5.1.1: 2871 2488 dependencies: 2872 2489 yallist: 3.1.1 ··· 2875 2492 dependencies: 2876 2493 '@jridgewell/sourcemap-codec': 1.5.5 2877 2494 2878 - math-intrinsics@1.1.0: {} 2879 - 2880 2495 mdn-data@2.0.28: {} 2881 2496 2882 - mdn-data@2.0.30: {} 2497 + mdn-data@2.12.2: {} 2883 2498 2884 2499 merge-anything@5.1.7: 2885 2500 dependencies: 2886 2501 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 2502 2909 2503 mlly@1.8.0: 2910 2504 dependencies: ··· 2913 2507 pkg-types: 1.3.1 2914 2508 ufo: 1.6.1 2915 2509 2510 + modern-tar@0.7.3: {} 2511 + 2916 2512 ms@2.1.3: {} 2917 2513 2918 2514 nanoevents@9.1.0: {} ··· 2920 2516 nanoid@3.3.11: {} 2921 2517 2922 2518 nanoid@5.1.6: {} 2519 + 2520 + node-gyp-build@4.8.4: {} 2923 2521 2924 2522 node-releases@2.0.27: {} 2925 2523 ··· 2927 2525 dependencies: 2928 2526 boolbase: 1.0.0 2929 2527 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 2528 + package-manager-detector@1.6.0: {} 2944 2529 2945 2530 parse5@7.3.0: 2946 2531 dependencies: 2947 2532 entities: 6.0.1 2948 - 2949 - pathe@1.1.2: {} 2950 2533 2951 2534 pathe@2.0.3: {} 2952 2535 2953 - pend@1.2.0: {} 2954 - 2955 2536 picocolors@1.1.1: {} 2956 2537 2957 2538 picomatch@4.0.3: {} ··· 2962 2543 mlly: 1.8.0 2963 2544 pathe: 2.0.3 2964 2545 2965 - pkg-types@2.3.0: 2966 - dependencies: 2967 - confbox: 0.2.2 2968 - exsolve: 1.0.8 2969 - pathe: 2.0.3 2970 - 2971 2546 postcss@8.5.6: 2972 2547 dependencies: 2973 2548 nanoid: 3.3.11 2974 2549 picocolors: 1.1.1 2975 2550 source-map-js: 1.2.1 2976 2551 2977 - prettier-plugin-organize-imports@4.3.0(prettier@3.6.2)(typescript@5.9.3): 2552 + prettier-plugin-organize-imports@4.3.0(prettier@3.7.4)(typescript@5.9.3): 2978 2553 dependencies: 2979 - prettier: 3.6.2 2554 + prettier: 3.7.4 2980 2555 typescript: 5.9.3 2981 2556 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): 2557 + prettier-plugin-tailwindcss@0.7.2(prettier-plugin-organize-imports@4.3.0(prettier@3.7.4)(typescript@5.9.3))(prettier@3.7.4): 2983 2558 dependencies: 2984 - prettier: 3.6.2 2559 + prettier: 3.7.4 2985 2560 optionalDependencies: 2986 - prettier-plugin-organize-imports: 4.3.0(prettier@3.6.2)(typescript@5.9.3) 2987 - 2988 - prettier@3.6.2: {} 2561 + prettier-plugin-organize-imports: 4.3.0(prettier@3.7.4)(typescript@5.9.3) 2989 2562 2990 - proxy-from-env@1.1.0: {} 2991 - 2992 - pump@3.0.3: 2993 - dependencies: 2994 - end-of-stream: 1.4.5 2995 - once: 1.4.0 2996 - 2997 - quansync@0.2.11: {} 2563 + prettier@3.7.4: {} 2998 2564 2999 2565 resolve-pkg-maps@1.0.0: 3000 2566 optional: true 3001 2567 3002 - rollup@4.53.3: 2568 + rollup@4.54.0: 3003 2569 dependencies: 3004 2570 '@types/estree': 1.0.8 3005 2571 optionalDependencies: 3006 - '@rollup/rollup-android-arm-eabi': 4.53.3 3007 - '@rollup/rollup-android-arm64': 4.53.3 3008 - '@rollup/rollup-darwin-arm64': 4.53.3 3009 - '@rollup/rollup-darwin-x64': 4.53.3 3010 - '@rollup/rollup-freebsd-arm64': 4.53.3 3011 - '@rollup/rollup-freebsd-x64': 4.53.3 3012 - '@rollup/rollup-linux-arm-gnueabihf': 4.53.3 3013 - '@rollup/rollup-linux-arm-musleabihf': 4.53.3 3014 - '@rollup/rollup-linux-arm64-gnu': 4.53.3 3015 - '@rollup/rollup-linux-arm64-musl': 4.53.3 3016 - '@rollup/rollup-linux-loong64-gnu': 4.53.3 3017 - '@rollup/rollup-linux-ppc64-gnu': 4.53.3 3018 - '@rollup/rollup-linux-riscv64-gnu': 4.53.3 3019 - '@rollup/rollup-linux-riscv64-musl': 4.53.3 3020 - '@rollup/rollup-linux-s390x-gnu': 4.53.3 3021 - '@rollup/rollup-linux-x64-gnu': 4.53.3 3022 - '@rollup/rollup-linux-x64-musl': 4.53.3 3023 - '@rollup/rollup-openharmony-arm64': 4.53.3 3024 - '@rollup/rollup-win32-arm64-msvc': 4.53.3 3025 - '@rollup/rollup-win32-ia32-msvc': 4.53.3 3026 - '@rollup/rollup-win32-x64-gnu': 4.53.3 3027 - '@rollup/rollup-win32-x64-msvc': 4.53.3 2572 + '@rollup/rollup-android-arm-eabi': 4.54.0 2573 + '@rollup/rollup-android-arm64': 4.54.0 2574 + '@rollup/rollup-darwin-arm64': 4.54.0 2575 + '@rollup/rollup-darwin-x64': 4.54.0 2576 + '@rollup/rollup-freebsd-arm64': 4.54.0 2577 + '@rollup/rollup-freebsd-x64': 4.54.0 2578 + '@rollup/rollup-linux-arm-gnueabihf': 4.54.0 2579 + '@rollup/rollup-linux-arm-musleabihf': 4.54.0 2580 + '@rollup/rollup-linux-arm64-gnu': 4.54.0 2581 + '@rollup/rollup-linux-arm64-musl': 4.54.0 2582 + '@rollup/rollup-linux-loong64-gnu': 4.54.0 2583 + '@rollup/rollup-linux-ppc64-gnu': 4.54.0 2584 + '@rollup/rollup-linux-riscv64-gnu': 4.54.0 2585 + '@rollup/rollup-linux-riscv64-musl': 4.54.0 2586 + '@rollup/rollup-linux-s390x-gnu': 4.54.0 2587 + '@rollup/rollup-linux-x64-gnu': 4.54.0 2588 + '@rollup/rollup-linux-x64-musl': 4.54.0 2589 + '@rollup/rollup-openharmony-arm64': 4.54.0 2590 + '@rollup/rollup-win32-arm64-msvc': 4.54.0 2591 + '@rollup/rollup-win32-ia32-msvc': 4.54.0 2592 + '@rollup/rollup-win32-x64-gnu': 4.54.0 2593 + '@rollup/rollup-win32-x64-msvc': 4.54.0 3028 2594 fsevents: 2.3.3 3029 2595 3030 - safer-buffer@2.1.2: {} 2596 + sax@1.4.3: {} 3031 2597 3032 2598 semver@6.3.1: {} 3033 2599 ··· 3056 2622 3057 2623 style-mod@4.1.3: {} 3058 2624 3059 - svgo@3.3.2: 2625 + svgo@4.0.0: 3060 2626 dependencies: 3061 - '@trysound/sax': 0.2.0 3062 - commander: 7.2.0 2627 + commander: 11.1.0 3063 2628 css-select: 5.2.2 3064 - css-tree: 2.3.1 2629 + css-tree: 3.1.0 3065 2630 css-what: 6.2.2 3066 2631 csso: 5.0.5 3067 2632 picocolors: 1.1.1 2633 + sax: 1.4.3 3068 2634 3069 - tailwindcss@4.1.17: {} 2635 + tailwindcss@4.1.18: {} 3070 2636 3071 2637 tapable@2.3.0: {} 3072 - 3073 - tar@6.2.1: 3074 - dependencies: 3075 - chownr: 2.0.0 3076 - fs-minipass: 2.1.0 3077 - minipass: 5.0.0 3078 - minizlib: 2.1.2 3079 - mkdirp: 1.0.4 3080 - yallist: 4.0.0 3081 2638 3082 2639 tinyexec@1.0.2: {} 3083 2640 ··· 3098 2655 3099 2656 ufo@1.6.1: {} 3100 2657 3101 - undici-types@7.16.0: {} 2658 + undici-types@6.21.0: {} 2659 + 2660 + undici-types@7.16.0: 2661 + optional: true 3102 2662 3103 - undici@6.22.0: {} 2663 + unicode-segmenter@0.14.4: {} 3104 2664 3105 - update-browserslist-db@1.1.4(browserslist@4.28.0): 2665 + update-browserslist-db@1.2.3(browserslist@4.28.1): 3106 2666 dependencies: 3107 - browserslist: 4.28.0 2667 + browserslist: 4.28.1 3108 2668 escalade: 3.2.0 3109 2669 picocolors: 1.1.1 3110 2670 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)): 2671 + vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.3.0(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)): 3112 2672 dependencies: 3113 2673 '@babel/core': 7.28.5 3114 2674 '@types/babel__core': 7.20.5 ··· 3116 2676 merge-anything: 5.1.7 3117 2677 solid-js: 1.9.10 3118 2678 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)) 2679 + vite: 7.3.0(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2) 2680 + vitefu: 1.1.1(vite@7.3.0(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)) 3121 2681 transitivePeerDependencies: 3122 2682 - supports-color 3123 2683 3124 - vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2): 2684 + vite@7.3.0(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2): 3125 2685 dependencies: 3126 - esbuild: 0.25.12 2686 + esbuild: 0.27.2 3127 2687 fdir: 6.5.0(picomatch@4.0.3) 3128 2688 picomatch: 4.0.3 3129 2689 postcss: 8.5.6 3130 - rollup: 4.53.3 2690 + rollup: 4.54.0 3131 2691 tinyglobby: 0.2.15 3132 2692 optionalDependencies: 3133 2693 '@types/node': 24.10.1 ··· 3136 2696 lightningcss: 1.30.2 3137 2697 tsx: 4.19.2 3138 2698 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)): 2699 + vitefu@1.1.1(vite@7.3.0(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)): 3140 2700 optionalDependencies: 3141 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2) 2701 + vite: 7.3.0(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2) 3142 2702 3143 2703 w3c-keyname@2.2.8: {} 3144 2704 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 2705 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 2706 3162 2707 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--circle-check shrink-0 text-blue-500 dark:text-blue-400"></span> 144 + </Show> 145 + <Show when={!sessions[did].signedIn}> 146 + <span class="iconify lucide--circle-alert shrink-0 text-red-500 dark:text-red-400"></span> 147 + </Show> 148 + </button> 149 + <AccountDropdown 150 + did={did as Did} 151 + onEditPermissions={(accountDid) => scopeFlow.initiateWithRedirect(accountDid)} 152 + /> 153 + </div> 154 + )} 155 + </For> 156 + </div> 157 + <button 158 + onclick={() => setShowingAddAccount(true)} 159 + class="flex w-full items-center justify-center gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-100 px-3 py-2 hover:bg-neutral-200 active:bg-neutral-300 dark:border-neutral-600 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 160 + > 161 + <span class="iconify lucide--user-plus"></span> 162 + <span>Add account</span> 163 + </button> 164 + </Show> 165 + 166 + <Show when={showingAddAccount() && !scopeFlow.showScopeSelector()}> 167 + <Login onCancel={() => setShowingAddAccount(false)} /> 168 + </Show> 169 + 170 + <Show when={scopeFlow.showScopeSelector()}> 171 + <ScopeSelector 172 + initialScopes={parseScopeString( 173 + sessions[scopeFlow.pendingAccount()]?.grantedScopes || "", 174 + )} 175 + onConfirm={scopeFlow.complete} 176 + onCancel={() => { 177 + scopeFlow.cancel(); 178 + setShowingAddAccount(false); 179 + }} 180 + /> 181 + </Show> 182 + </div> 183 + </Modal> 184 + <button 185 + onclick={() => setOpenManager(true)} 186 + class={`flex items-center rounded-lg ${agent() && avatars[agent()!.sub] ? "p-1.25" : "p-1.5"} hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600`} 187 + > 188 + {agent() && avatars[agent()!.sub] ? 189 + <img src={getThumbnailUrl(avatars[agent()!.sub])} class="size-5 rounded-full" /> 190 + : <span class="iconify lucide--circle-user-round text-lg"></span>} 191 + </button> 192 + </> 193 + ); 194 + };
+88
src/auth/login.tsx
··· 1 + import { createSignal, Show } from "solid-js"; 2 + import "./oauth-config"; 3 + import { useOAuthScopeFlow } from "./scope-flow"; 4 + import { ScopeSelector } from "./scope-selector"; 5 + 6 + interface LoginProps { 7 + onCancel?: () => void; 8 + } 9 + 10 + export const Login = (props: LoginProps) => { 11 + const [notice, setNotice] = createSignal(""); 12 + const [loginInput, setLoginInput] = createSignal(""); 13 + 14 + const scopeFlow = useOAuthScopeFlow({ 15 + onError: (e) => setNotice(`${e}`), 16 + onRedirecting: () => { 17 + setNotice(`Contacting your data server...`); 18 + setTimeout(() => setNotice(`Redirecting...`), 0); 19 + }, 20 + }); 21 + 22 + const initiateLogin = (handle: string) => { 23 + setNotice(""); 24 + scopeFlow.initiate(handle); 25 + }; 26 + 27 + const handleCancel = () => { 28 + scopeFlow.cancel(); 29 + setLoginInput(""); 30 + setNotice(""); 31 + props.onCancel?.(); 32 + }; 33 + 34 + return ( 35 + <div class="flex flex-col gap-y-2 px-1"> 36 + <Show when={!scopeFlow.showScopeSelector()}> 37 + <Show when={props.onCancel}> 38 + <div class="mb-1 flex items-center gap-2"> 39 + <button 40 + onclick={handleCancel} 41 + class="flex items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 42 + > 43 + <span class="iconify lucide--arrow-left"></span> 44 + </button> 45 + <div class="font-semibold">Add account</div> 46 + </div> 47 + </Show> 48 + <form class="flex flex-col gap-2" onsubmit={(e) => e.preventDefault()}> 49 + <label for="username" class="hidden"> 50 + Add account 51 + </label> 52 + <div class="dark:bg-dark-100 flex grow items-center gap-2 rounded-lg bg-white px-2 outline-1 outline-neutral-200 focus-within:outline-[1.5px] focus-within:outline-neutral-600 dark:outline-neutral-600 dark:focus-within:outline-neutral-400"> 53 + <label 54 + for="username" 55 + class="iconify lucide--user-round-plus shrink-0 text-neutral-500 dark:text-neutral-400" 56 + ></label> 57 + <input 58 + type="text" 59 + spellcheck={false} 60 + placeholder="user.bsky.social" 61 + id="username" 62 + name="username" 63 + autocomplete="username" 64 + autofocus 65 + aria-label="Your AT Protocol handle" 66 + class="grow py-1 select-none placeholder:text-sm focus:outline-none" 67 + onInput={(e) => setLoginInput(e.currentTarget.value)} 68 + /> 69 + </div> 70 + <button 71 + onclick={() => initiateLogin(loginInput())} 72 + class="grow rounded-lg border-[0.5px] border-neutral-300 bg-neutral-100 px-3 py-2 hover:bg-neutral-200 active:bg-neutral-300 dark:border-neutral-600 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 73 + > 74 + Continue 75 + </button> 76 + </form> 77 + </Show> 78 + 79 + <Show when={scopeFlow.showScopeSelector()}> 80 + <ScopeSelector onConfirm={scopeFlow.complete} onCancel={handleCancel} /> 81 + </Show> 82 + 83 + <Show when={notice()}> 84 + <div class="text-sm">{notice()}</div> 85 + </Show> 86 + </div> 87 + ); 88 + };
+13
src/auth/oauth-config.ts
··· 1 + import { configureOAuth, defaultIdentityResolver } from "@atcute/oauth-browser-client"; 2 + import { didDocumentResolver, handleResolver } from "../utils/api"; 3 + 4 + configureOAuth({ 5 + metadata: { 6 + client_id: import.meta.env.VITE_OAUTH_CLIENT_ID, 7 + redirect_uri: import.meta.env.VITE_OAUTH_REDIRECT_URL, 8 + }, 9 + identityResolver: defaultIdentityResolver({ 10 + handleResolver: handleResolver, 11 + didDocumentResolver: didDocumentResolver, 12 + }), 13 + });
+77
src/auth/scope-flow.ts
··· 1 + import { isDid, isHandle } from "@atcute/lexicons/syntax"; 2 + import { createAuthorizationUrl } from "@atcute/oauth-browser-client"; 3 + import { createSignal } from "solid-js"; 4 + 5 + interface UseOAuthScopeFlowOptions { 6 + onError?: (error: unknown) => void; 7 + onRedirecting?: () => void; 8 + beforeRedirect?: (account: string) => Promise<void>; 9 + } 10 + 11 + export const useOAuthScopeFlow = (options: UseOAuthScopeFlowOptions = {}) => { 12 + const [showScopeSelector, setShowScopeSelector] = createSignal(false); 13 + const [pendingAccount, setPendingAccount] = createSignal(""); 14 + const [shouldForceRedirect, setShouldForceRedirect] = createSignal(false); 15 + 16 + const initiate = (account: string) => { 17 + if (!account) return; 18 + setPendingAccount(account); 19 + setShouldForceRedirect(false); 20 + setShowScopeSelector(true); 21 + }; 22 + 23 + const initiateWithRedirect = (account: string) => { 24 + if (!account) return; 25 + setPendingAccount(account); 26 + setShouldForceRedirect(true); 27 + setShowScopeSelector(true); 28 + }; 29 + 30 + const complete = async (scopeString: string, scopeIds: string) => { 31 + try { 32 + const account = pendingAccount(); 33 + 34 + if (options.beforeRedirect && !shouldForceRedirect()) { 35 + try { 36 + await options.beforeRedirect(account); 37 + setShowScopeSelector(false); 38 + return; 39 + } catch {} 40 + } 41 + 42 + localStorage.setItem("pendingScopes", scopeIds); 43 + 44 + options.onRedirecting?.(); 45 + 46 + const authUrl = await createAuthorizationUrl({ 47 + scope: scopeString, 48 + target: 49 + isHandle(account) || isDid(account) ? 50 + { type: "account", identifier: account } 51 + : { type: "pds", serviceUrl: account }, 52 + }); 53 + 54 + await new Promise((resolve) => setTimeout(resolve, 250)); 55 + location.assign(authUrl); 56 + } catch (e) { 57 + console.error(e); 58 + options.onError?.(e); 59 + setShowScopeSelector(false); 60 + } 61 + }; 62 + 63 + const cancel = () => { 64 + setShowScopeSelector(false); 65 + setPendingAccount(""); 66 + setShouldForceRedirect(false); 67 + }; 68 + 69 + return { 70 + showScopeSelector, 71 + pendingAccount, 72 + initiate, 73 + initiateWithRedirect, 74 + complete, 75 + cancel, 76 + }; 77 + };
+86
src/auth/scope-selector.tsx
··· 1 + import { createSignal, For } from "solid-js"; 2 + import { buildScopeString, GRANULAR_SCOPES, scopeIdsToString } from "./scope-utils"; 3 + 4 + interface ScopeSelectorProps { 5 + onConfirm: (scopeString: string, scopeIds: string) => void; 6 + onCancel: () => void; 7 + initialScopes?: Set<string>; 8 + } 9 + 10 + export const ScopeSelector = (props: ScopeSelectorProps) => { 11 + const [selectedScopes, setSelectedScopes] = createSignal<Set<string>>( 12 + props.initialScopes || new Set(["create", "update", "delete", "blob"]), 13 + ); 14 + 15 + const isBlobDisabled = () => { 16 + const scopes = selectedScopes(); 17 + return !scopes.has("create") && !scopes.has("update"); 18 + }; 19 + 20 + const toggleScope = (scopeId: string) => { 21 + setSelectedScopes((prev) => { 22 + const newSet = new Set(prev); 23 + if (newSet.has(scopeId)) { 24 + newSet.delete(scopeId); 25 + if ( 26 + (scopeId === "create" || scopeId === "update") && 27 + !newSet.has("create") && 28 + !newSet.has("update") 29 + ) { 30 + newSet.delete("blob"); 31 + } 32 + } else { 33 + newSet.add(scopeId); 34 + } 35 + return newSet; 36 + }); 37 + }; 38 + 39 + const handleConfirm = () => { 40 + const scopes = selectedScopes(); 41 + const scopeString = buildScopeString(scopes); 42 + const scopeIds = scopeIdsToString(scopes); 43 + props.onConfirm(scopeString, scopeIds); 44 + }; 45 + 46 + return ( 47 + <div class="flex flex-col gap-y-2"> 48 + <div class="mb-1 flex items-center gap-2"> 49 + <button 50 + onclick={props.onCancel} 51 + class="flex items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 52 + > 53 + <span class="iconify lucide--arrow-left"></span> 54 + </button> 55 + <div class="font-semibold">Select permissions</div> 56 + </div> 57 + <div class="flex flex-col gap-y-2 px-1"> 58 + <For each={GRANULAR_SCOPES}> 59 + {(scope) => ( 60 + <div 61 + class="flex items-center gap-2" 62 + classList={{ "opacity-50": scope.id === "blob" && isBlobDisabled() }} 63 + > 64 + <input 65 + id={`scope-${scope.id}`} 66 + type="checkbox" 67 + checked={selectedScopes().has(scope.id)} 68 + disabled={scope.id === "blob" && isBlobDisabled()} 69 + onChange={() => toggleScope(scope.id)} 70 + /> 71 + <label for={`scope-${scope.id}`} class="flex grow items-center gap-2 select-none"> 72 + <span>{scope.label}</span> 73 + </label> 74 + </div> 75 + )} 76 + </For> 77 + </div> 78 + <button 79 + onclick={handleConfirm} 80 + class="mt-2 grow rounded-lg border-[0.5px] border-neutral-300 bg-neutral-100 px-3 py-2 hover:bg-neutral-200 active:bg-neutral-300 dark:border-neutral-600 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 81 + > 82 + Continue 83 + </button> 84 + </div> 85 + ); 86 + };
+53
src/auth/scope-utils.ts
··· 1 + import { agent, sessions } from "./state"; 2 + 3 + export const GRANULAR_SCOPES = [ 4 + { 5 + id: "create", 6 + scope: "repo:*?action=create", 7 + label: "Create records", 8 + }, 9 + { 10 + id: "update", 11 + scope: "repo:*?action=update", 12 + label: "Update records", 13 + }, 14 + { 15 + id: "delete", 16 + scope: "repo:*?action=delete", 17 + label: "Delete records", 18 + }, 19 + { 20 + id: "blob", 21 + scope: "blob:*/*", 22 + label: "Upload blobs", 23 + }, 24 + ]; 25 + 26 + export const BASE_SCOPES = ["atproto"]; 27 + 28 + export const buildScopeString = (selected: Set<string>): string => { 29 + const granular = GRANULAR_SCOPES.filter((s) => selected.has(s.id)).map((s) => s.scope); 30 + return [...BASE_SCOPES, ...granular].join(" "); 31 + }; 32 + 33 + export const scopeIdsToString = (scopeIds: Set<string>): string => { 34 + return ["atproto", ...Array.from(scopeIds)].join(","); 35 + }; 36 + 37 + export const parseScopeString = (scopeIdsString: string): Set<string> => { 38 + if (!scopeIdsString) return new Set(); 39 + const ids = scopeIdsString.split(",").filter(Boolean); 40 + return new Set(ids.filter((id) => id !== "atproto")); 41 + }; 42 + 43 + export const hasScope = (grantedScopes: string | undefined, scopeId: string): boolean => { 44 + if (!grantedScopes) return false; 45 + return grantedScopes.split(",").includes(scopeId); 46 + }; 47 + 48 + export const hasUserScope = (scopeId: string): boolean => { 49 + if (!agent()) return false; 50 + const grantedScopes = sessions[agent()!.sub]?.grantedScopes; 51 + if (!grantedScopes) return true; 52 + return hasScope(grantedScopes, scopeId); 53 + };
+95
src/auth/session-manager.ts
··· 1 + import { Client, simpleFetchHandler } from "@atcute/client"; 2 + import { Did } from "@atcute/lexicons"; 3 + import { 4 + finalizeAuthorization, 5 + getSession, 6 + OAuthUserAgent, 7 + type Session, 8 + } from "@atcute/oauth-browser-client"; 9 + import { resolveDidDoc } from "../utils/api"; 10 + import { Sessions, setAgent, setSessions } from "./state"; 11 + 12 + export const saveSessionToStorage = (sessions: Sessions) => { 13 + localStorage.setItem("sessions", JSON.stringify(sessions)); 14 + }; 15 + 16 + export const loadSessionsFromStorage = (): Sessions | null => { 17 + const localSessions = localStorage.getItem("sessions"); 18 + return localSessions ? JSON.parse(localSessions) : null; 19 + }; 20 + 21 + export const getAvatar = async (did: Did): Promise<string | undefined> => { 22 + const rpc = new Client({ 23 + handler: simpleFetchHandler({ service: "https://public.api.bsky.app" }), 24 + }); 25 + const res = await rpc.get("app.bsky.actor.getProfile", { params: { actor: did } }); 26 + if (res.ok) { 27 + return res.data.avatar; 28 + } 29 + return undefined; 30 + }; 31 + 32 + export const loadHandleForSession = async (did: Did, storedSessions: Sessions) => { 33 + const doc = await resolveDidDoc(did); 34 + const alias = doc.alsoKnownAs?.find((alias) => alias.startsWith("at://")); 35 + if (alias) { 36 + setSessions(did, { 37 + signedIn: storedSessions[did].signedIn, 38 + handle: alias.replace("at://", ""), 39 + grantedScopes: storedSessions[did].grantedScopes, 40 + }); 41 + } 42 + }; 43 + 44 + export const retrieveSession = async (): Promise<void> => { 45 + const init = async (): Promise<Session | undefined> => { 46 + const params = new URLSearchParams(location.hash.slice(1)); 47 + 48 + if (params.has("state") && (params.has("code") || params.has("error"))) { 49 + history.replaceState(null, "", location.pathname + location.search); 50 + 51 + const auth = await finalizeAuthorization(params); 52 + const did = auth.session.info.sub; 53 + 54 + localStorage.setItem("lastSignedIn", did); 55 + 56 + const grantedScopes = localStorage.getItem("pendingScopes") || "atproto"; 57 + localStorage.removeItem("pendingScopes"); 58 + 59 + const sessions = loadSessionsFromStorage(); 60 + const newSessions: Sessions = sessions || {}; 61 + newSessions[did] = { signedIn: true, grantedScopes }; 62 + saveSessionToStorage(newSessions); 63 + return auth.session; 64 + } else { 65 + const lastSignedIn = localStorage.getItem("lastSignedIn"); 66 + 67 + if (lastSignedIn) { 68 + const sessions = loadSessionsFromStorage(); 69 + const newSessions: Sessions = sessions || {}; 70 + try { 71 + const session = await getSession(lastSignedIn as Did); 72 + const rpc = new Client({ handler: new OAuthUserAgent(session) }); 73 + const res = await rpc.get("com.atproto.server.getSession"); 74 + newSessions[lastSignedIn].signedIn = true; 75 + saveSessionToStorage(newSessions); 76 + if (!res.ok) throw res.data.error; 77 + return session; 78 + } catch (err) { 79 + newSessions[lastSignedIn].signedIn = false; 80 + saveSessionToStorage(newSessions); 81 + throw err; 82 + } 83 + } 84 + } 85 + }; 86 + 87 + const session = await init(); 88 + 89 + if (session) setAgent(new OAuthUserAgent(session)); 90 + }; 91 + 92 + export const resumeSession = async (did: Did): Promise<void> => { 93 + localStorage.setItem("lastSignedIn", did); 94 + await retrieveSession(); 95 + };
+14
src/auth/state.ts
··· 1 + import { OAuthUserAgent } from "@atcute/oauth-browser-client"; 2 + import { createSignal } from "solid-js"; 3 + import { createStore } from "solid-js/store"; 4 + 5 + export type Account = { 6 + signedIn: boolean; 7 + handle?: string; 8 + grantedScopes?: string; 9 + }; 10 + 11 + export type Sessions = Record<string, Account>; 12 + 13 + export const [agent, setAgent] = createSignal<OAuthUserAgent | undefined>(); 14 + export const [sessions, setSessions] = createStore<Sessions>();
-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 - };
+117 -173
src/components/backlinks.tsx
··· 1 1 import * as TID from "@atcute/tid"; 2 2 import { createResource, createSignal, For, onMount, Show } from "solid-js"; 3 - import { 4 - getAllBacklinks, 5 - getDidBacklinks, 6 - getRecordBacklinks, 7 - LinksWithDids, 8 - LinksWithRecords, 9 - } from "../utils/api.js"; 3 + import { getAllBacklinks, getRecordBacklinks, LinksWithRecords } from "../utils/api.js"; 10 4 import { localDateFromTimestamp } from "../utils/date.js"; 11 5 import { Button } from "./button.jsx"; 12 6 13 - type Backlink = { 7 + type BacklinksProps = { 8 + target: string; 9 + collection: string; 10 + path: string; 11 + }; 12 + 13 + type BacklinkEntry = { 14 + collection: string; 14 15 path: string; 15 16 counts: { distinct_dids: number; records: number }; 16 17 }; 17 18 18 - const linksBySource = (links: Record<string, any>) => { 19 - let out: Record<string, Backlink[]> = {}; 19 + const flattenLinks = (links: Record<string, any>): BacklinkEntry[] => { 20 + const entries: BacklinkEntry[] = []; 20 21 Object.keys(links) 21 22 .toSorted() 22 23 .forEach((collection) => { ··· 24 25 Object.keys(paths) 25 26 .toSorted() 26 27 .forEach((path) => { 27 - if (paths[path].records === 0) return; 28 - if (out[collection]) out[collection].push({ path, counts: paths[path] }); 29 - else out[collection] = [{ path, counts: paths[path] }]; 28 + if (paths[path].records > 0) { 29 + entries.push({ collection, path, counts: paths[path] }); 30 + } 30 31 }); 31 32 }); 32 - return out; 33 + return entries; 33 34 }; 34 35 35 - const Backlinks = (props: { target: string }) => { 36 - const fetchBacklinks = async () => { 37 - const res = await getAllBacklinks(props.target); 38 - return linksBySource(res.links); 39 - }; 36 + const BacklinkRecords = (props: BacklinksProps & { cursor?: string }) => { 37 + const [links, setLinks] = createSignal<LinksWithRecords>(); 38 + const [more, setMore] = createSignal(false); 40 39 41 - const [response] = createResource(fetchBacklinks); 42 - 43 - const [show, setShow] = createSignal<{ 44 - collection: string; 45 - path: string; 46 - showDids: boolean; 47 - } | null>(); 40 + onMount(async () => { 41 + const res = await getRecordBacklinks(props.target, props.collection, props.path, props.cursor); 42 + setLinks(res); 43 + }); 48 44 49 45 return ( 50 - <div class="flex w-full flex-col gap-1 text-sm wrap-anywhere"> 51 - <Show 52 - when={response() && Object.keys(response()!).length} 53 - fallback={<p>No backlinks found.</p>} 54 - > 55 - <For each={Object.keys(response()!)}> 56 - {(collection) => ( 57 - <div> 58 - <div class="flex items-center gap-1"> 59 - <span class="iconify lucide--book-text shrink-0"></span> 60 - {collection} 61 - </div> 62 - <For each={response()![collection]}> 63 - {({ path, counts }) => ( 64 - <div class="ml-4.5"> 65 - <div class="flex items-center gap-1"> 66 - <span class="iconify lucide--route shrink-0"></span> 67 - {path.slice(1)} 68 - </div> 69 - <div class="ml-4.5"> 70 - <p> 71 - <button 72 - class="text-blue-400 hover:underline active:underline" 73 - onclick={() => 74 - ( 75 - show()?.collection === collection && 76 - show()?.path === path && 77 - !show()?.showDids 78 - ) ? 79 - setShow(null) 80 - : setShow({ collection, path, showDids: false }) 81 - } 82 - > 83 - {counts.records} record{counts.records < 2 ? "" : "s"} 84 - </button> 85 - {" from "} 86 - <button 87 - class="text-blue-400 hover:underline active:underline" 88 - onclick={() => 89 - ( 90 - show()?.collection === collection && 91 - show()?.path === path && 92 - show()?.showDids 93 - ) ? 94 - setShow(null) 95 - : setShow({ collection, path, showDids: true }) 96 - } 97 - > 98 - {counts.distinct_dids} DID 99 - {counts.distinct_dids < 2 ? "" : "s"} 100 - </button> 101 - </p> 102 - <Show when={show()?.collection === collection && show()?.path === path}> 103 - <Show when={show()?.showDids}> 104 - <p class="w-full font-semibold">Distinct identities</p> 105 - <BacklinkItems 106 - target={props.target} 107 - collection={collection} 108 - path={path} 109 - dids={true} 110 - /> 111 - </Show> 112 - <Show when={!show()?.showDids}> 113 - <p class="w-full font-semibold">Records</p> 114 - <BacklinkItems 115 - target={props.target} 116 - collection={collection} 117 - path={path} 118 - dids={false} 119 - /> 120 - </Show> 121 - </Show> 122 - </div> 123 - </div> 124 - )} 125 - </For> 46 + <Show when={links()} fallback={<p class="px-3 py-2 text-neutral-500">Loadingโ€ฆ</p>}> 47 + <For each={links()!.linking_records}> 48 + {({ did, collection, rkey }) => { 49 + const timestamp = 50 + TID.validate(rkey) ? localDateFromTimestamp(TID.parse(rkey).timestamp / 1000) : null; 51 + return ( 52 + <a 53 + href={`/at://${did}/${collection}/${rkey}`} 54 + class="grid grid-cols-[auto_1fr_auto] items-center gap-x-1 px-2 py-1.5 font-mono text-xs select-none hover:bg-neutral-200/50 active:bg-neutral-200/50 sm:gap-x-3 sm:px-3 dark:hover:bg-neutral-700/50 dark:active:bg-neutral-700/50" 55 + > 56 + <span class="text-blue-500 dark:text-blue-400">{rkey}</span> 57 + <span class="truncate text-neutral-700 dark:text-neutral-300" title={did}> 58 + {did} 59 + </span> 60 + <span class="text-neutral-500 tabular-nums dark:text-neutral-400"> 61 + {timestamp ?? ""} 62 + </span> 63 + </a> 64 + ); 65 + }} 66 + </For> 67 + <Show when={links()?.cursor}> 68 + <Show 69 + when={more()} 70 + fallback={ 71 + <div class="p-2"> 72 + <Button 73 + onClick={() => setMore(true)} 74 + class="dark:hover:bg-dark-200 dark:shadow-dark-700 dark:active:bg-dark-100 box-border flex h-7 w-full items-center justify-center gap-1 rounded border-[0.5px] border-neutral-300 bg-neutral-50 px-2 py-1.5 text-xs shadow-xs select-none hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800" 75 + > 76 + Load More 77 + </Button> 126 78 </div> 127 - )} 128 - </For> 79 + } 80 + > 81 + <BacklinkRecords 82 + target={props.target} 83 + collection={props.collection} 84 + path={props.path} 85 + cursor={links()!.cursor} 86 + /> 87 + </Show> 129 88 </Show> 130 - </div> 89 + </Show> 131 90 ); 132 91 }; 133 92 134 - // switching on !!did everywhere is pretty annoying, this could probably be two components 135 - // but i don't want to duplicate or think about how to extract the paging logic 136 - const BacklinkItems = ({ 137 - target, 138 - collection, 139 - path, 140 - dids, 141 - cursor, 142 - }: { 143 - target: string; 144 - collection: string; 145 - path: string; 146 - dids: boolean; 147 - cursor?: string; 148 - }) => { 149 - const [links, setLinks] = createSignal<LinksWithDids | LinksWithRecords>(); 150 - const [more, setMore] = createSignal<boolean>(false); 151 - 152 - onMount(async () => { 153 - const links = await (dids ? getDidBacklinks : getRecordBacklinks)( 154 - target, 155 - collection, 156 - path, 157 - cursor, 158 - ); 159 - setLinks(links); 93 + const Backlinks = (props: { target: string }) => { 94 + const [response] = createResource(async () => { 95 + const res = await getAllBacklinks(props.target); 96 + return flattenLinks(res.links); 160 97 }); 161 98 162 - // TODO: could pass the `total` into this component, which can be checked against each call to this endpoint to find if it's stale. 163 - // also hmm 'total' is misleading/wrong on that api 164 - 165 99 return ( 166 - <Show when={links()} fallback={<p>Loading&hellip;</p>}> 167 - <Show when={dids}> 168 - <For each={(links() as LinksWithDids).linking_dids}> 169 - {(did) => ( 170 - <a 171 - href={`/at://${did}`} 172 - class="relative flex w-full font-mono text-blue-400 hover:underline active:underline" 173 - > 174 - {did} 175 - </a> 100 + <div class="flex w-full flex-col gap-3 text-sm"> 101 + <Show when={response()} fallback={<p class="text-neutral-500">Loadingโ€ฆ</p>}> 102 + <Show when={response()!.length === 0}> 103 + <p class="text-neutral-500">No backlinks found.</p> 104 + </Show> 105 + <For each={response()}> 106 + {(entry) => ( 107 + <BacklinkSection 108 + target={props.target} 109 + collection={entry.collection} 110 + path={entry.path} 111 + counts={entry.counts} 112 + /> 176 113 )} 177 114 </For> 178 115 </Show> 179 - <Show when={!dids}> 180 - <For each={(links() as LinksWithRecords).linking_records}> 181 - {({ did, collection, rkey }) => ( 182 - <p class="relative flex w-full items-center gap-1 font-mono"> 183 - <a 184 - href={`/at://${did}/${collection}/${rkey}`} 185 - class="text-blue-400 hover:underline active:underline" 186 - > 187 - {rkey} 188 - </a> 189 - <span class="text-xs text-neutral-500 dark:text-neutral-400"> 190 - {TID.validate(rkey) ? 191 - localDateFromTimestamp(TID.parse(rkey).timestamp / 1000) 192 - : undefined} 193 - </span> 194 - </p> 195 - )} 196 - </For> 197 - </Show> 198 - <Show when={links()?.cursor}> 199 - <Show when={more()} fallback={<Button onClick={() => setMore(true)}>Load More</Button>}> 200 - <BacklinkItems 201 - target={target} 202 - collection={collection} 203 - path={path} 204 - dids={dids} 205 - cursor={links()!.cursor} 116 + </div> 117 + ); 118 + }; 119 + 120 + const BacklinkSection = ( 121 + props: BacklinksProps & { counts: { distinct_dids: number; records: number } }, 122 + ) => { 123 + const [expanded, setExpanded] = createSignal(false); 124 + 125 + return ( 126 + <div class="overflow-hidden rounded-lg border border-neutral-200 dark:border-neutral-700"> 127 + <button 128 + class="flex w-full items-center justify-between gap-3 px-3 py-2 text-left hover:bg-neutral-50 dark:hover:bg-neutral-800/50" 129 + onClick={() => setExpanded(!expanded())} 130 + > 131 + <div class="flex min-w-0 flex-1 flex-col"> 132 + <span class="w-full truncate">{props.collection}</span> 133 + <span class="w-full text-xs wrap-break-word text-neutral-500 dark:text-neutral-400"> 134 + {props.path.slice(1)} 135 + </span> 136 + </div> 137 + <div class="flex shrink-0 items-center gap-2 text-neutral-700 dark:text-neutral-300"> 138 + <span class="text-xs"> 139 + {props.counts.records} from {props.counts.distinct_dids} repo 140 + {props.counts.distinct_dids > 1 ? "s" : ""} 141 + </span> 142 + <span 143 + class="iconify lucide--chevron-down transition-transform" 144 + classList={{ "rotate-180": expanded() }} 206 145 /> 207 - </Show> 146 + </div> 147 + </button> 148 + <Show when={expanded()}> 149 + <div class="border-t border-neutral-200 bg-neutral-50/50 dark:border-neutral-700 dark:bg-neutral-800/30"> 150 + <BacklinkRecords target={props.target} collection={props.collection} path={props.path} /> 151 + </div> 208 152 </Show> 209 - </Show> 153 + </div> 210 154 ); 211 155 }; 212 156
+90
src/components/create/confirm-submit.tsx
··· 1 + import { createSignal, Show } from "solid-js"; 2 + import { Button } from "../button.jsx"; 3 + 4 + export const ConfirmSubmit = (props: { 5 + isCreate: boolean; 6 + onConfirm: (validate: boolean | undefined, recreate: boolean) => void; 7 + onClose: () => void; 8 + }) => { 9 + const [validate, setValidate] = createSignal<boolean | undefined>(undefined); 10 + const [recreate, setRecreate] = createSignal(false); 11 + 12 + const getValidateLabel = () => { 13 + return ( 14 + validate() === true ? "True" 15 + : validate() === false ? "False" 16 + : "Unset" 17 + ); 18 + }; 19 + 20 + const cycleValidate = () => { 21 + setValidate( 22 + validate() === undefined ? true 23 + : validate() === true ? false 24 + : undefined, 25 + ); 26 + }; 27 + 28 + return ( 29 + <div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-70 left-[50%] w-[24rem] -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-opacity duration-200 dark:border-neutral-700 starting:opacity-0"> 30 + <div class="flex flex-col gap-3 text-sm"> 31 + <h2 class="font-semibold">{props.isCreate ? "Create" : "Edit"} record</h2> 32 + <div class="flex flex-col gap-1.5"> 33 + <div class="flex items-center gap-2"> 34 + <button 35 + type="button" 36 + class="-ml-2 flex min-w-30 items-center gap-1.5 rounded-lg px-2 py-1 text-xs hover:bg-neutral-200/50 dark:hover:bg-neutral-700" 37 + onClick={cycleValidate} 38 + > 39 + <span 40 + classList={{ 41 + iconify: true, 42 + "lucide--square-check text-green-500 dark:text-green-400": validate() === true, 43 + "lucide--square-x text-red-500 dark:text-red-400": validate() === false, 44 + "lucide--square text-neutral-500 dark:text-neutral-400": validate() === undefined, 45 + }} 46 + ></span> 47 + <span>Validate: {getValidateLabel()}</span> 48 + </button> 49 + </div> 50 + <p class="text-xs text-neutral-600 dark:text-neutral-400"> 51 + Set to 'false' to skip lexicon schema validation by the PDS, 'true' to require it, or 52 + leave unset to validate only for known lexicons. 53 + </p> 54 + </div> 55 + <Show when={!props.isCreate}> 56 + <div class="flex flex-col gap-1.5"> 57 + <div class="flex items-center gap-2"> 58 + <button 59 + type="button" 60 + class="-ml-2 flex items-center gap-1.5 rounded-lg px-2 py-1 text-xs hover:bg-neutral-200/50 dark:hover:bg-neutral-700" 61 + onClick={() => setRecreate(!recreate())} 62 + > 63 + <span 64 + classList={{ 65 + iconify: true, 66 + "lucide--square-check text-green-500 dark:text-green-400": recreate(), 67 + "lucide--square text-neutral-500 dark:text-neutral-400": !recreate(), 68 + }} 69 + ></span> 70 + <span>Recreate</span> 71 + </button> 72 + </div> 73 + <p class="text-xs text-neutral-600 dark:text-neutral-400"> 74 + Delete the existing record and create a new one with the same record key. 75 + </p> 76 + </div> 77 + </Show> 78 + <div class="flex justify-between gap-2"> 79 + <Button onClick={props.onClose}>Cancel</Button> 80 + <Button 81 + onClick={() => props.onConfirm(validate(), recreate())} 82 + class="dark:shadow-dark-700 min-w-12 rounded-lg bg-blue-500 px-2 py-1.5 text-xs text-white shadow-xs select-none hover:bg-blue-600 active:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 dark:active:bg-blue-400" 83 + > 84 + {props.isCreate ? "Create" : "Edit"} 85 + </Button> 86 + </div> 87 + </div> 88 + </div> 89 + ); 90 + };
+109
src/components/create/file-upload.tsx
··· 1 + import { Client } from "@atcute/client"; 2 + import { remove } from "@mary/exif-rm"; 3 + import { createSignal, onCleanup, Show } from "solid-js"; 4 + import { agent } from "../../auth/state"; 5 + import { Button } from "../button.jsx"; 6 + import { TextInput } from "../text-input.jsx"; 7 + import { editorInstance } from "./state"; 8 + 9 + export const FileUpload = (props: { 10 + file: File; 11 + blobInput: HTMLInputElement; 12 + onClose: () => void; 13 + }) => { 14 + const [uploading, setUploading] = createSignal(false); 15 + const [error, setError] = createSignal(""); 16 + 17 + onCleanup(() => (props.blobInput.value = "")); 18 + 19 + const formatFileSize = (bytes: number) => { 20 + if (bytes === 0) return "0 Bytes"; 21 + const k = 1024; 22 + const sizes = ["Bytes", "KB", "MB", "GB"]; 23 + const i = Math.floor(Math.log(bytes) / Math.log(k)); 24 + return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i]; 25 + }; 26 + 27 + const uploadBlob = async () => { 28 + let blob: Blob; 29 + 30 + const mimetype = (document.getElementById("mimetype") as HTMLInputElement)?.value; 31 + (document.getElementById("mimetype") as HTMLInputElement).value = ""; 32 + if (mimetype) blob = new Blob([props.file], { type: mimetype }); 33 + else blob = props.file; 34 + 35 + if ((document.getElementById("exif-rm") as HTMLInputElement).checked) { 36 + const exifRemoved = remove(new Uint8Array(await blob.arrayBuffer())); 37 + if (exifRemoved !== null) blob = new Blob([exifRemoved], { type: blob.type }); 38 + } 39 + 40 + const rpc = new Client({ handler: agent()! }); 41 + setUploading(true); 42 + const res = await rpc.post("com.atproto.repo.uploadBlob", { 43 + input: blob, 44 + }); 45 + setUploading(false); 46 + if (!res.ok) { 47 + setError(res.data.error); 48 + return; 49 + } 50 + editorInstance.view.dispatch({ 51 + changes: { 52 + from: editorInstance.view.state.selection.main.head, 53 + insert: JSON.stringify(res.data.blob, null, 2), 54 + }, 55 + }); 56 + props.onClose(); 57 + }; 58 + 59 + return ( 60 + <div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-70 left-[50%] w-[20rem] -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-opacity duration-200 dark:border-neutral-700 starting:opacity-0"> 61 + <h2 class="mb-2 font-semibold">Upload blob</h2> 62 + <div class="flex flex-col gap-2 text-sm"> 63 + <div class="flex flex-col gap-1"> 64 + <p class="flex gap-1"> 65 + <span class="truncate">{props.file.name}</span> 66 + <span class="shrink-0 text-neutral-600 dark:text-neutral-400"> 67 + ({formatFileSize(props.file.size)}) 68 + </span> 69 + </p> 70 + </div> 71 + <div class="flex items-center gap-x-2"> 72 + <label for="mimetype" class="shrink-0 select-none"> 73 + MIME type 74 + </label> 75 + <TextInput id="mimetype" placeholder={props.file.type} /> 76 + </div> 77 + <div class="flex items-center gap-1"> 78 + <input id="exif-rm" type="checkbox" checked /> 79 + <label for="exif-rm" class="select-none"> 80 + Remove EXIF data 81 + </label> 82 + </div> 83 + <p class="text-xs text-neutral-600 dark:text-neutral-400"> 84 + Metadata will be pasted after the cursor 85 + </p> 86 + <Show when={error()}> 87 + <span class="text-red-500 dark:text-red-400">Error: {error()}</span> 88 + </Show> 89 + <div class="flex justify-between gap-2"> 90 + <Button onClick={props.onClose}>Cancel</Button> 91 + <Show when={uploading()}> 92 + <div class="flex items-center gap-1"> 93 + <span class="iconify lucide--loader-circle animate-spin"></span> 94 + <span>Uploading</span> 95 + </div> 96 + </Show> 97 + <Show when={!uploading()}> 98 + <Button 99 + onClick={uploadBlob} 100 + class="dark:shadow-dark-700 flex items-center gap-1 rounded-lg bg-blue-500 px-2 py-1.5 text-xs text-white shadow-xs select-none hover:bg-blue-600 active:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 dark:active:bg-blue-400" 101 + > 102 + Upload 103 + </Button> 104 + </Show> 105 + </div> 106 + </div> 107 + </div> 108 + ); 109 + };
+87
src/components/create/handle-input.tsx
··· 1 + import { Handle } from "@atcute/lexicons"; 2 + import { createSignal, Show } from "solid-js"; 3 + import { resolveHandle } from "../../utils/api"; 4 + import { Button } from "../button.jsx"; 5 + import { TextInput } from "../text-input.jsx"; 6 + import { editorInstance } from "./state"; 7 + 8 + export const HandleInput = (props: { onClose: () => void }) => { 9 + const [resolving, setResolving] = createSignal(false); 10 + const [error, setError] = createSignal(""); 11 + let handleFormRef!: HTMLFormElement; 12 + 13 + const resolveDid = async (e: SubmitEvent) => { 14 + e.preventDefault(); 15 + const formData = new FormData(handleFormRef); 16 + const handleValue = formData.get("handle")?.toString().trim(); 17 + 18 + if (!handleValue) { 19 + setError("Please enter a handle"); 20 + return; 21 + } 22 + 23 + setResolving(true); 24 + setError(""); 25 + try { 26 + const did = await resolveHandle(handleValue as Handle); 27 + editorInstance.view.dispatch({ 28 + changes: { 29 + from: editorInstance.view.state.selection.main.head, 30 + insert: `"${did}"`, 31 + }, 32 + }); 33 + props.onClose(); 34 + handleFormRef.reset(); 35 + } catch (err: any) { 36 + setError(err.message || "Failed to resolve handle"); 37 + } finally { 38 + setResolving(false); 39 + } 40 + }; 41 + 42 + return ( 43 + <div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-70 left-[50%] w-[20rem] -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-opacity duration-200 dark:border-neutral-700 starting:opacity-0"> 44 + <h2 class="mb-2 font-semibold">Insert DID from handle</h2> 45 + <form ref={handleFormRef} onSubmit={resolveDid} class="flex flex-col gap-2 text-sm"> 46 + <div class="flex flex-col gap-1"> 47 + <label for="handle-input" class="select-none"> 48 + Handle 49 + </label> 50 + <TextInput id="handle-input" name="handle" placeholder="user.bsky.social" /> 51 + </div> 52 + <p class="text-xs text-neutral-600 dark:text-neutral-400"> 53 + DID will be pasted after the cursor 54 + </p> 55 + <Show when={error()}> 56 + <span class="text-red-500 dark:text-red-400">Error: {error()}</span> 57 + </Show> 58 + <div class="flex justify-between gap-2"> 59 + <Button 60 + type="button" 61 + onClick={() => { 62 + props.onClose(); 63 + handleFormRef.reset(); 64 + setError(""); 65 + }} 66 + > 67 + Cancel 68 + </Button> 69 + <Show when={resolving()}> 70 + <div class="flex items-center gap-1"> 71 + <span class="iconify lucide--loader-circle animate-spin"></span> 72 + <span>Resolving</span> 73 + </div> 74 + </Show> 75 + <Show when={!resolving()}> 76 + <Button 77 + type="submit" 78 + class="dark:shadow-dark-700 flex items-center gap-1 rounded-lg bg-blue-500 px-2 py-1.5 text-xs text-white shadow-xs select-none hover:bg-blue-600 active:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 dark:active:bg-blue-400" 79 + > 80 + Insert 81 + </Button> 82 + </Show> 83 + </div> 84 + </form> 85 + </div> 86 + ); 87 + };
+460
src/components/create/index.tsx
··· 1 + import { Client } from "@atcute/client"; 2 + import { Did } from "@atcute/lexicons"; 3 + import { isNsid, isRecordKey } from "@atcute/lexicons/syntax"; 4 + import { getSession, OAuthUserAgent } from "@atcute/oauth-browser-client"; 5 + import { useNavigate, useParams } from "@solidjs/router"; 6 + import { 7 + createEffect, 8 + createSignal, 9 + For, 10 + lazy, 11 + onCleanup, 12 + onMount, 13 + Show, 14 + Suspense, 15 + } from "solid-js"; 16 + import { hasUserScope } from "../../auth/scope-utils"; 17 + import { agent, sessions } from "../../auth/state"; 18 + import { Button } from "../button.jsx"; 19 + import { Modal } from "../modal.jsx"; 20 + import { addNotification, removeNotification } from "../notification.jsx"; 21 + import { TextInput } from "../text-input.jsx"; 22 + import Tooltip from "../tooltip.jsx"; 23 + import { ConfirmSubmit } from "./confirm-submit"; 24 + import { FileUpload } from "./file-upload"; 25 + import { HandleInput } from "./handle-input"; 26 + import { MenuItem } from "./menu-item"; 27 + import { editorInstance, placeholder, setPlaceholder } from "./state"; 28 + 29 + const Editor = lazy(() => import("../editor.jsx").then((m) => ({ default: m.Editor }))); 30 + 31 + export { editorInstance, placeholder, setPlaceholder }; 32 + 33 + export const RecordEditor = (props: { create: boolean; record?: any; refetch?: any }) => { 34 + const navigate = useNavigate(); 35 + const params = useParams(); 36 + const [openDialog, setOpenDialog] = createSignal(false); 37 + const [notice, setNotice] = createSignal(""); 38 + const [openUpload, setOpenUpload] = createSignal(false); 39 + const [openInsertMenu, setOpenInsertMenu] = createSignal(false); 40 + const [openHandleDialog, setOpenHandleDialog] = createSignal(false); 41 + const [openConfirmDialog, setOpenConfirmDialog] = createSignal(false); 42 + const [isMaximized, setIsMaximized] = createSignal(false); 43 + const [isMinimized, setIsMinimized] = createSignal(false); 44 + const [collectionError, setCollectionError] = createSignal(""); 45 + const [rkeyError, setRkeyError] = createSignal(""); 46 + let blobInput!: HTMLInputElement; 47 + let formRef!: HTMLFormElement; 48 + let insertMenuRef!: HTMLDivElement; 49 + 50 + createEffect(() => { 51 + if (openInsertMenu()) { 52 + const handleClickOutside = (e: MouseEvent) => { 53 + if (insertMenuRef && !insertMenuRef.contains(e.target as Node)) { 54 + setOpenInsertMenu(false); 55 + } 56 + }; 57 + document.addEventListener("mousedown", handleClickOutside); 58 + onCleanup(() => document.removeEventListener("mousedown", handleClickOutside)); 59 + } 60 + }); 61 + 62 + onMount(() => { 63 + const keyEvent = (ev: KeyboardEvent) => { 64 + if (ev.target instanceof HTMLInputElement || ev.target instanceof HTMLTextAreaElement) return; 65 + if ((ev.target as HTMLElement).closest("[data-modal]")) return; 66 + 67 + const key = props.create ? "n" : "e"; 68 + if (ev.key === key) { 69 + ev.preventDefault(); 70 + 71 + if (openDialog() && isMinimized()) { 72 + setIsMinimized(false); 73 + } else if (!openDialog() && !document.querySelector("[data-modal]")) { 74 + setOpenDialog(true); 75 + } 76 + } 77 + }; 78 + 79 + window.addEventListener("keydown", keyEvent); 80 + onCleanup(() => window.removeEventListener("keydown", keyEvent)); 81 + }); 82 + 83 + const defaultPlaceholder = () => { 84 + return { 85 + $type: "app.bsky.feed.post", 86 + text: "This post was sent from PDSls", 87 + embed: { 88 + $type: "app.bsky.embed.external", 89 + external: { 90 + uri: "https://pdsls.dev", 91 + title: "PDSls", 92 + description: "Browse the public data on atproto", 93 + }, 94 + }, 95 + langs: ["en"], 96 + createdAt: new Date().toISOString(), 97 + }; 98 + }; 99 + 100 + createEffect(() => { 101 + if (openDialog()) { 102 + setCollectionError(""); 103 + setRkeyError(""); 104 + } 105 + }); 106 + 107 + const createRecord = async (validate: boolean | undefined) => { 108 + const formData = new FormData(formRef); 109 + const repo = formData.get("repo")?.toString(); 110 + if (!repo) return; 111 + const rpc = new Client({ handler: new OAuthUserAgent(await getSession(repo as Did)) }); 112 + const collection = formData.get("collection"); 113 + const rkey = formData.get("rkey"); 114 + let record: any; 115 + try { 116 + record = JSON.parse(editorInstance.view.state.doc.toString()); 117 + } catch (e: any) { 118 + setNotice(e.message); 119 + return; 120 + } 121 + const res = await rpc.post("com.atproto.repo.createRecord", { 122 + input: { 123 + repo: repo as Did, 124 + collection: collection ? collection.toString() : record.$type, 125 + rkey: rkey?.toString().length ? rkey?.toString() : undefined, 126 + record: record, 127 + validate: validate, 128 + }, 129 + }); 130 + if (!res.ok) { 131 + setNotice(`${res.data.error}: ${res.data.message}`); 132 + return; 133 + } 134 + setOpenConfirmDialog(false); 135 + setOpenDialog(false); 136 + const id = addNotification({ 137 + message: "Record created", 138 + type: "success", 139 + }); 140 + setTimeout(() => removeNotification(id), 3000); 141 + navigate(`/${res.data.uri}`); 142 + }; 143 + 144 + const editRecord = async (validate: boolean | undefined, recreate: boolean) => { 145 + const record = editorInstance.view.state.doc.toString(); 146 + if (!record) return; 147 + const rpc = new Client({ handler: agent()! }); 148 + try { 149 + const editedRecord = JSON.parse(record); 150 + if (recreate) { 151 + const res = await rpc.post("com.atproto.repo.applyWrites", { 152 + input: { 153 + repo: agent()!.sub, 154 + validate: validate, 155 + writes: [ 156 + { 157 + collection: params.collection as `${string}.${string}.${string}`, 158 + rkey: params.rkey!, 159 + $type: "com.atproto.repo.applyWrites#delete", 160 + }, 161 + { 162 + collection: params.collection as `${string}.${string}.${string}`, 163 + rkey: params.rkey, 164 + $type: "com.atproto.repo.applyWrites#create", 165 + value: editedRecord, 166 + }, 167 + ], 168 + }, 169 + }); 170 + if (!res.ok) { 171 + setNotice(`${res.data.error}: ${res.data.message}`); 172 + return; 173 + } 174 + } else { 175 + const res = await rpc.post("com.atproto.repo.applyWrites", { 176 + input: { 177 + repo: agent()!.sub, 178 + validate: validate, 179 + writes: [ 180 + { 181 + collection: params.collection as `${string}.${string}.${string}`, 182 + rkey: params.rkey!, 183 + $type: "com.atproto.repo.applyWrites#update", 184 + value: editedRecord, 185 + }, 186 + ], 187 + }, 188 + }); 189 + if (!res.ok) { 190 + setNotice(`${res.data.error}: ${res.data.message}`); 191 + return; 192 + } 193 + } 194 + setOpenConfirmDialog(false); 195 + setOpenDialog(false); 196 + const id = addNotification({ 197 + message: "Record edited", 198 + type: "success", 199 + }); 200 + setTimeout(() => removeNotification(id), 3000); 201 + props.refetch(); 202 + } catch (err: any) { 203 + setNotice(err.message); 204 + } 205 + }; 206 + 207 + const insertTimestamp = () => { 208 + const timestamp = new Date().toISOString(); 209 + editorInstance.view.dispatch({ 210 + changes: { 211 + from: editorInstance.view.state.selection.main.head, 212 + insert: `"${timestamp}"`, 213 + }, 214 + }); 215 + setOpenInsertMenu(false); 216 + }; 217 + 218 + const insertDidFromHandle = () => { 219 + setOpenInsertMenu(false); 220 + setOpenHandleDialog(true); 221 + }; 222 + 223 + return ( 224 + <> 225 + <Modal 226 + open={openDialog()} 227 + onClose={() => setOpenDialog(false)} 228 + closeOnClick={false} 229 + nonBlocking={isMinimized()} 230 + > 231 + <div 232 + style="transform: translateX(-50%) translateZ(0);" 233 + classList={{ 234 + "dark:bg-dark-300 dark:shadow-dark-700 pointer-events-auto absolute top-18 left-1/2 flex flex-col rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-all duration-200 dark:border-neutral-700 starting:opacity-0": true, 235 + "w-[calc(100%-1rem)] max-w-3xl h-[65vh]": !isMaximized(), 236 + "w-[calc(100%-1rem)] max-w-7xl h-[85vh]": isMaximized(), 237 + hidden: isMinimized(), 238 + }} 239 + > 240 + <div class="mb-2 flex w-full justify-between text-base"> 241 + <div class="flex items-center gap-2"> 242 + <span class="font-semibold select-none"> 243 + {props.create ? "Creating" : "Editing"} record 244 + </span> 245 + </div> 246 + <div class="flex items-center gap-1"> 247 + <button 248 + type="button" 249 + onclick={() => setIsMinimized(true)} 250 + class="flex items-center rounded-lg p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 251 + > 252 + <span class="iconify lucide--minus"></span> 253 + </button> 254 + <button 255 + type="button" 256 + onclick={() => setIsMaximized(!isMaximized())} 257 + class="flex items-center rounded-lg p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 258 + > 259 + <span 260 + class={`iconify ${isMaximized() ? "lucide--minimize-2" : "lucide--maximize-2"}`} 261 + ></span> 262 + </button> 263 + <button 264 + id="close" 265 + onclick={() => setOpenDialog(false)} 266 + class="flex items-center rounded-lg p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 267 + > 268 + <span class="iconify lucide--x"></span> 269 + </button> 270 + </div> 271 + </div> 272 + <form ref={formRef} class="flex min-h-0 flex-1 flex-col gap-y-2"> 273 + <Show when={props.create}> 274 + <div class="flex flex-wrap items-center gap-1 text-sm"> 275 + <span>at://</span> 276 + <select 277 + class="dark:bg-dark-100 max-w-40 truncate rounded-lg border-[0.5px] border-neutral-300 bg-white px-1 py-1 select-none focus:outline-[1px] focus:outline-neutral-600 dark:border-neutral-600 dark:focus:outline-neutral-400" 278 + name="repo" 279 + id="repo" 280 + > 281 + <For each={Object.keys(sessions)}> 282 + {(session) => ( 283 + <option value={session} selected={session === agent()?.sub}> 284 + {sessions[session].handle ?? session} 285 + </option> 286 + )} 287 + </For> 288 + </select> 289 + <span>/</span> 290 + <TextInput 291 + id="collection" 292 + name="collection" 293 + placeholder="Collection (default: $type)" 294 + class={`w-40 placeholder:text-xs lg:w-52 ${collectionError() ? "border-red-500 focus:outline-red-500 dark:border-red-400 dark:focus:outline-red-400" : ""}`} 295 + onInput={(e) => { 296 + const value = e.currentTarget.value; 297 + if (!value || isNsid(value)) setCollectionError(""); 298 + else 299 + setCollectionError( 300 + "Invalid collection: use reverse domain format (e.g. app.bsky.feed.post)", 301 + ); 302 + }} 303 + /> 304 + <span>/</span> 305 + <TextInput 306 + id="rkey" 307 + name="rkey" 308 + placeholder="Record key (default: TID)" 309 + class={`w-40 placeholder:text-xs lg:w-52 ${rkeyError() ? "border-red-500 focus:outline-red-500 dark:border-red-400 dark:focus:outline-red-400" : ""}`} 310 + onInput={(e) => { 311 + const value = e.currentTarget.value; 312 + if (!value || isRecordKey(value)) setRkeyError(""); 313 + else setRkeyError("Invalid record key: 1-512 chars, use a-z A-Z 0-9 . _ ~ : -"); 314 + }} 315 + /> 316 + </div> 317 + <Show when={collectionError() || rkeyError()}> 318 + <div class="text-xs text-red-500 dark:text-red-400"> 319 + <div>{collectionError()}</div> 320 + <div>{rkeyError()}</div> 321 + </div> 322 + </Show> 323 + </Show> 324 + <div class="min-h-0 flex-1"> 325 + <Suspense 326 + fallback={ 327 + <div class="flex h-full items-center justify-center"> 328 + <span class="iconify lucide--loader-circle animate-spin text-xl"></span> 329 + </div> 330 + } 331 + > 332 + <Editor 333 + content={JSON.stringify( 334 + !props.create ? props.record 335 + : params.rkey ? placeholder() 336 + : defaultPlaceholder(), 337 + null, 338 + 2, 339 + )} 340 + /> 341 + </Suspense> 342 + </div> 343 + <div class="flex flex-col gap-2"> 344 + <Show when={notice()}> 345 + <div class="text-sm text-red-500 dark:text-red-400">{notice()}</div> 346 + </Show> 347 + <div class="flex justify-between gap-2"> 348 + <div class="relative" ref={insertMenuRef}> 349 + <button 350 + type="button" 351 + class="dark:hover:bg-dark-200 dark:shadow-dark-700 dark:active:bg-dark-100 flex w-fit rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-1.5 text-base shadow-xs hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800" 352 + onClick={() => setOpenInsertMenu(!openInsertMenu())} 353 + > 354 + <span class="iconify lucide--plus select-none"></span> 355 + </button> 356 + <Show when={openInsertMenu()}> 357 + <div class="dark:bg-dark-300 dark:shadow-dark-700 absolute bottom-full left-0 z-10 mb-1 flex w-40 flex-col rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-1.5 shadow-md dark:border-neutral-700"> 358 + <MenuItem 359 + icon="lucide--id-card" 360 + label="Insert DID" 361 + onClick={insertDidFromHandle} 362 + /> 363 + <MenuItem 364 + icon="lucide--clock" 365 + label="Insert timestamp" 366 + onClick={insertTimestamp} 367 + /> 368 + <Show when={hasUserScope("blob")}> 369 + <MenuItem 370 + icon="lucide--upload" 371 + label="Upload blob" 372 + onClick={() => { 373 + setOpenInsertMenu(false); 374 + blobInput.click(); 375 + }} 376 + /> 377 + </Show> 378 + </div> 379 + </Show> 380 + <input 381 + type="file" 382 + id="blob" 383 + class="sr-only" 384 + ref={blobInput} 385 + onChange={(e) => { 386 + if (e.target.files !== null) setOpenUpload(true); 387 + }} 388 + /> 389 + </div> 390 + <Modal 391 + open={openUpload()} 392 + onClose={() => setOpenUpload(false)} 393 + closeOnClick={false} 394 + > 395 + <FileUpload 396 + file={blobInput.files![0]} 397 + blobInput={blobInput} 398 + onClose={() => setOpenUpload(false)} 399 + /> 400 + </Modal> 401 + <Modal 402 + open={openHandleDialog()} 403 + onClose={() => setOpenHandleDialog(false)} 404 + closeOnClick={false} 405 + > 406 + <HandleInput onClose={() => setOpenHandleDialog(false)} /> 407 + </Modal> 408 + <Modal 409 + open={openConfirmDialog()} 410 + onClose={() => setOpenConfirmDialog(false)} 411 + closeOnClick={false} 412 + > 413 + <ConfirmSubmit 414 + isCreate={props.create} 415 + onConfirm={(validate, recreate) => { 416 + if (props.create) { 417 + createRecord(validate); 418 + } else { 419 + editRecord(validate, recreate); 420 + } 421 + }} 422 + onClose={() => setOpenConfirmDialog(false)} 423 + /> 424 + </Modal> 425 + <div class="flex items-center justify-end gap-2"> 426 + <Button onClick={() => setOpenConfirmDialog(true)}> 427 + {props.create ? "Create..." : "Edit..."} 428 + </Button> 429 + </div> 430 + </div> 431 + </div> 432 + </form> 433 + </div> 434 + </Modal> 435 + <Show when={isMinimized() && openDialog()}> 436 + <button 437 + class="dark:bg-dark-300 dark:hover:bg-dark-200 dark:active:bg-dark-100 fixed right-4 bottom-4 z-30 flex items-center gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 px-3 py-2 shadow-md hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700" 438 + onclick={() => setIsMinimized(false)} 439 + > 440 + <span class="iconify lucide--square-pen text-lg"></span> 441 + <span class="text-sm font-medium">{props.create ? "Creating" : "Editing"} record</span> 442 + </button> 443 + </Show> 444 + <Tooltip text={props.create ? "Create record (n)" : "Edit record (e)"}> 445 + <button 446 + class={`flex items-center p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600 ${props.create ? "rounded-lg" : "rounded-sm"}`} 447 + onclick={() => { 448 + setNotice(""); 449 + setOpenDialog(true); 450 + setIsMinimized(false); 451 + }} 452 + > 453 + <div 454 + class={props.create ? "iconify lucide--square-pen text-lg" : "iconify lucide--pencil"} 455 + /> 456 + </button> 457 + </Tooltip> 458 + </> 459 + ); 460 + };
+12
src/components/create/menu-item.tsx
··· 1 + export const MenuItem = (props: { icon: string; label: string; onClick: () => void }) => { 2 + return ( 3 + <button 4 + type="button" 5 + class="flex items-center gap-2 rounded-md p-2 text-left text-xs hover:bg-neutral-100 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 6 + onClick={props.onClick} 7 + > 8 + <span class={`iconify ${props.icon}`}></span> 9 + <span>{props.label}</span> 10 + </button> 11 + ); 12 + };
+4
src/components/create/state.ts
··· 1 + import { createSignal } from "solid-js"; 2 + 3 + export const editorInstance = { view: null as any }; 4 + export const [placeholder, setPlaceholder] = createSignal<any>();
-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 - };
+48 -17
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 > ··· 74 75 export const ActionMenu = (props: { 75 76 label: string; 76 77 icon: string; 77 - onClick: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>; 78 + onClick: () => void; 78 79 }) => { 80 + const ctx = useContext(MenuContext); 81 + 79 82 return ( 80 83 <button 81 - onClick={props.onClick} 82 - class="flex items-center gap-1.5 rounded-md p-1 whitespace-nowrap hover:bg-neutral-200/50 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 84 + onClick={() => { 85 + props.onClick(); 86 + ctx?.setShowMenu(false); 87 + }} 88 + class="flex items-center gap-2 rounded-md p-1.5 whitespace-nowrap hover:bg-neutral-200/50 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 83 89 > 84 90 <Show when={props.icon}> 85 91 <span class={"iconify shrink-0 " + props.icon}></span> ··· 102 108 const ctx = useContext(MenuContext); 103 109 const [menu, setMenu] = createSignal<HTMLDivElement>(); 104 110 const [menuButton, setMenuButton] = createSignal<HTMLButtonElement>(); 111 + const [buttonRect, setButtonRect] = createSignal<DOMRect>(); 105 112 106 113 const clickEvent = (event: MouseEvent) => { 107 114 const target = event.target as Node; 108 115 if (!menuButton()?.contains(target) && !menu()?.contains(target)) ctx?.setShowMenu(false); 109 116 }; 110 117 111 - onMount(() => window.addEventListener("click", clickEvent)); 112 - onCleanup(() => window.removeEventListener("click", clickEvent)); 118 + const updatePosition = () => { 119 + const rect = menuButton()?.getBoundingClientRect(); 120 + if (rect) setButtonRect(rect); 121 + }; 122 + 123 + onMount(() => { 124 + window.addEventListener("click", clickEvent); 125 + window.addEventListener("scroll", updatePosition, true); 126 + window.addEventListener("resize", updatePosition); 127 + }); 128 + 129 + onCleanup(() => { 130 + window.removeEventListener("click", clickEvent); 131 + window.removeEventListener("scroll", updatePosition, true); 132 + window.removeEventListener("resize", updatePosition); 133 + }); 113 134 114 135 return ( 115 136 <div class="relative"> ··· 119 140 props.buttonClass 120 141 } 121 142 ref={setMenuButton} 122 - onClick={() => ctx?.setShowMenu(!ctx?.showMenu())} 143 + onClick={() => { 144 + updatePosition(); 145 + ctx?.setShowMenu(!ctx?.showMenu()); 146 + }} 123 147 > 124 148 <span class={"iconify " + props.icon}></span> 125 149 </button> 126 150 <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> 151 + <Portal> 152 + <div 153 + ref={setMenu} 154 + style={{ 155 + position: "fixed", 156 + top: `${(buttonRect()?.bottom ?? 0) + 4}px`, 157 + left: `${(buttonRect()?.right ?? 0) - 160}px`, 158 + }} 159 + class={ 160 + "dark:bg-dark-300 dark:shadow-dark-700 z-50 flex min-w-40 flex-col rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 text-sm shadow-md dark:border-neutral-700 " + 161 + props.menuClass 162 + } 163 + > 164 + {props.children} 165 + </div> 166 + </Portal> 136 167 </Show> 137 168 </div> 138 169 );
+6 -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: [ ··· 48 48 keymap.of([indentWithTab]), 49 49 linter(jsonParseLinter()), 50 50 themeColor.of(document.documentElement.classList.contains("dark") ? basicDark : basicLight), 51 + EditorView.lineWrapping, 51 52 ], 52 53 }); 54 + editorInstance.view = view; 53 55 }); 54 56 55 57 onCleanup(() =>
+70 -56
src/components/json.tsx
··· 1 1 import { isCid, isDid, isNsid, isResourceUri, Nsid } from "@atcute/lexicons/syntax"; 2 2 import { A, useNavigate, useParams } from "@solidjs/router"; 3 - import { createEffect, createSignal, ErrorBoundary, For, on, Show } from "solid-js"; 3 + import { 4 + createContext, 5 + createEffect, 6 + createSignal, 7 + ErrorBoundary, 8 + For, 9 + on, 10 + Show, 11 + useContext, 12 + } from "solid-js"; 4 13 import { resolveLexiconAuthority } from "../utils/api"; 5 14 import { hideMedia } from "../views/settings"; 6 15 import { pds } from "./navbar"; 7 16 import { addNotification, removeNotification } from "./notification"; 8 17 import VideoPlayer from "./video-player"; 9 18 19 + interface JSONContext { 20 + repo: string; 21 + truncate?: boolean; 22 + parentIsBlob?: boolean; 23 + } 24 + 25 + const JSONCtx = createContext<JSONContext>(); 26 + const useJSONCtx = () => useContext(JSONCtx)!; 27 + 10 28 interface AtBlob { 11 29 $type: string; 12 30 ref: { $link: string }; 13 31 mimeType: string; 14 32 } 15 33 16 - const JSONString = (props: { 17 - data: string; 18 - isType?: boolean; 19 - isLink?: boolean; 20 - parentIsBlob?: boolean; 21 - }) => { 34 + const isURL = 35 + URL.canParse ?? 36 + ((url, base) => { 37 + try { 38 + new URL(url, base); 39 + return true; 40 + } catch { 41 + return false; 42 + } 43 + }); 44 + 45 + const JSONString = (props: { data: string; isType?: boolean; isLink?: boolean }) => { 46 + const ctx = useJSONCtx(); 22 47 const navigate = useNavigate(); 23 48 const params = useParams(); 24 49 25 - const isURL = 26 - URL.canParse ?? 27 - ((url, base) => { 28 - try { 29 - new URL(url, base); 30 - return true; 31 - } catch { 32 - return false; 33 - } 34 - }); 35 - 36 50 const handleClick = async (lex: string) => { 37 51 try { 38 52 const [nsid, anchor] = lex.split("#"); ··· 50 64 } 51 65 }; 52 66 67 + const MAX_LENGTH = 200; 68 + const isTruncated = () => ctx.truncate && props.data.length > MAX_LENGTH; 69 + const displayData = () => (isTruncated() ? props.data.slice(0, MAX_LENGTH) : props.data); 70 + const remainingChars = () => props.data.length - MAX_LENGTH; 71 + 53 72 return ( 54 73 <span> 55 74 " 56 - <For each={props.data.split(/(\s)/)}> 75 + <For each={displayData().split(/(\s)/)}> 57 76 {(part) => ( 58 77 <> 59 78 {isResourceUri(part) ? ··· 72 91 > 73 92 {part} 74 93 </button> 75 - : isCid(part) && props.isLink && props.parentIsBlob && params.repo ? 94 + : isCid(part) && props.isLink && ctx.parentIsBlob && params.repo ? 76 95 <A 77 96 class="text-blue-400 hover:underline active:underline" 78 97 rel="noopener" ··· 93 112 </> 94 113 )} 95 114 </For> 115 + <Show when={isTruncated()}> 116 + <span>โ€ฆ</span> 117 + </Show> 96 118 " 119 + <Show when={isTruncated()}> 120 + <span class="ml-1 text-neutral-500 dark:text-neutral-400"> 121 + (+{remainingChars().toLocaleString()}) 122 + </span> 123 + </Show> 97 124 </span> 98 125 ); 99 126 }; ··· 110 137 return <span>null</span>; 111 138 }; 112 139 113 - const JSONObject = (props: { 114 - data: { [x: string]: JSONType }; 115 - repo: string; 116 - parentIsBlob?: boolean; 117 - }) => { 140 + const JSONObject = (props: { data: { [x: string]: JSONType } }) => { 141 + const ctx = useJSONCtx(); 118 142 const params = useParams(); 119 143 const [hide, setHide] = createSignal( 120 144 localStorage.hideMedia === "true" || params.rkey === undefined, ··· 136 160 ); 137 161 138 162 const isBlob = props.data.$type === "blob"; 139 - const isBlobContext = isBlob || props.parentIsBlob; 163 + const isBlobContext = isBlob || ctx.parentIsBlob; 140 164 141 165 const Obj = ({ key, value }: { key: string; value: JSONType }) => { 142 166 const [show, setShow] = createSignal(true); ··· 169 193 "self-center": value !== Object(value), 170 194 "pl-[calc(2ch-0.5px)] border-l-[0.5px] border-neutral-500/50 dark:border-neutral-400/50 has-hover:group-hover/indent:border-neutral-700 transition-colors dark:has-hover:group-hover/indent:border-neutral-300": 171 195 value === Object(value), 172 - "invisible h-0": !show(), 196 + "invisible h-0 overflow-hidden": !show(), 173 197 }} 174 198 > 175 - <JSONValue 176 - data={value} 177 - repo={props.repo} 178 - isType={key === "$type"} 179 - isLink={key === "$link"} 180 - parentIsBlob={isBlobContext} 181 - /> 199 + <JSONCtx.Provider value={{ ...ctx, parentIsBlob: isBlobContext }}> 200 + <JSONValueInner data={value} isType={key === "$type"} isLink={key === "$link"} /> 201 + </JSONCtx.Provider> 182 202 </span> 183 203 </span> 184 204 ); ··· 200 220 <Show when={blob.mimeType.startsWith("image/")}> 201 221 <img 202 222 class="h-auto max-h-48 max-w-48 object-contain sm:max-h-64 sm:max-w-64" 203 - src={`https://${pds()}/xrpc/com.atproto.sync.getBlob?did=${props.repo}&cid=${blob.ref.$link}`} 223 + src={`https://${pds()}/xrpc/com.atproto.sync.getBlob?did=${ctx.repo}&cid=${blob.ref.$link}`} 204 224 onLoad={() => setMediaLoaded(true)} 205 225 /> 206 226 </Show> 207 227 <Show when={blob.mimeType === "video/mp4"}> 208 228 <ErrorBoundary fallback={() => <span>Failed to load video</span>}> 209 229 <VideoPlayer 210 - did={props.repo} 230 + did={ctx.repo} 211 231 cid={blob.ref.$link} 212 232 onLoad={() => setMediaLoaded(true)} 213 233 /> ··· 241 261 return rawObj; 242 262 }; 243 263 244 - const JSONArray = (props: { data: JSONType[]; repo: string; parentIsBlob?: boolean }) => { 264 + const JSONArray = (props: { data: JSONType[] }) => { 245 265 return ( 246 266 <For each={props.data}> 247 267 {(value, index) => ( ··· 252 272 }} 253 273 > 254 274 <span class="ml-[1ch] w-full"> 255 - <JSONValue data={value} repo={props.repo} parentIsBlob={props.parentIsBlob} /> 275 + <JSONValueInner data={value} /> 256 276 </span> 257 277 </span> 258 278 )} ··· 260 280 ); 261 281 }; 262 282 263 - export const JSONValue = (props: { 264 - data: JSONType; 265 - repo: string; 266 - isType?: boolean; 267 - isLink?: boolean; 268 - parentIsBlob?: boolean; 269 - }) => { 283 + const JSONValueInner = (props: { data: JSONType; isType?: boolean; isLink?: boolean }) => { 270 284 const data = props.data; 271 285 if (typeof data === "string") 272 - return ( 273 - <JSONString 274 - data={data} 275 - isType={props.isType} 276 - isLink={props.isLink} 277 - parentIsBlob={props.parentIsBlob} 278 - /> 279 - ); 286 + return <JSONString data={data} isType={props.isType} isLink={props.isLink} />; 280 287 if (typeof data === "number") return <JSONNumber data={data} />; 281 288 if (typeof data === "boolean") return <JSONBoolean data={data} />; 282 289 if (data === null) return <JSONNull />; 283 - if (Array.isArray(data)) 284 - return <JSONArray data={data} repo={props.repo} parentIsBlob={props.parentIsBlob} />; 285 - return <JSONObject data={data} repo={props.repo} parentIsBlob={props.parentIsBlob} />; 290 + if (Array.isArray(data)) return <JSONArray data={data} />; 291 + return <JSONObject data={data} />; 292 + }; 293 + 294 + export const JSONValue = (props: { data: JSONType; repo: string; truncate?: boolean }) => { 295 + return ( 296 + <JSONCtx.Provider value={{ repo: props.repo, truncate: props.truncate }}> 297 + <JSONValueInner data={props.data} /> 298 + </JSONCtx.Provider> 299 + ); 286 300 }; 287 301 288 302 export type JSONType = string | number | boolean | null | { [x: string]: JSONType } | JSONType[];
+211 -1
src/components/lexicon-schema.tsx
··· 12 12 }; 13 13 } 14 14 15 + interface LexiconPermission { 16 + type: "permission"; 17 + // NOTE: blob, account, and identity are not supported in lexicon schema context 18 + resource: "repo" | "rpc" | "blob" | "account" | "identity"; 19 + collection?: string[]; 20 + action?: string[]; 21 + lxm?: string[]; 22 + aud?: string; 23 + inheritAud?: boolean; 24 + } 25 + 15 26 interface LexiconDef { 16 27 type: string; 17 28 description?: string; ··· 40 51 maxSize?: number; 41 52 knownValues?: string[]; 42 53 format?: string; 54 + // Permission-set fields 55 + title?: string; 56 + "title:langs"?: { [lang: string]: string }; 57 + detail?: string; 58 + "detail:langs"?: { [lang: string]: string }; 59 + permissions?: LexiconPermission[]; 43 60 } 44 61 45 62 interface LexiconObject { ··· 257 274 ); 258 275 }; 259 276 277 + const NsidLink = (props: { nsid: string }) => { 278 + const navigate = useNavigate(); 279 + 280 + const handleClick = async () => { 281 + try { 282 + const authority = await resolveLexiconAuthority(props.nsid as Nsid); 283 + navigate(`/at://${authority}/com.atproto.lexicon.schema/${props.nsid}#schema`); 284 + } catch (err) { 285 + console.error("Failed to resolve lexicon authority:", err); 286 + } 287 + }; 288 + 289 + return ( 290 + <button 291 + type="button" 292 + onClick={handleClick} 293 + class="cursor-pointer rounded bg-blue-100 px-1.5 py-0.5 font-mono text-xs text-blue-800 hover:bg-blue-200 hover:underline active:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-300 dark:hover:bg-blue-900/50 dark:active:bg-blue-900/50" 294 + > 295 + {props.nsid} 296 + </button> 297 + ); 298 + }; 299 + 300 + const resourceColor = (resource: string) => { 301 + switch (resource) { 302 + case "repo": 303 + return "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300"; 304 + case "rpc": 305 + return "bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300"; 306 + default: 307 + return "bg-neutral-200 text-neutral-800 dark:bg-neutral-700 dark:text-neutral-300"; 308 + } 309 + }; 310 + 311 + const PermissionRow = (props: { permission: LexiconPermission; index: number }) => { 312 + return ( 313 + <div class="flex flex-col gap-2 py-3"> 314 + <div class="flex flex-wrap items-center gap-2"> 315 + <span class="font-mono text-sm font-semibold">#{props.index + 1}</span> 316 + <span 317 + class={`rounded px-1.5 py-0.5 font-mono text-xs font-semibold ${resourceColor(props.permission.resource)}`} 318 + > 319 + {props.permission.resource} 320 + </span> 321 + </div> 322 + 323 + {/* Collections (for repo resource) */} 324 + <Show when={props.permission.collection && props.permission.collection.length > 0}> 325 + <div class="flex flex-col gap-1"> 326 + <span class="text-xs font-semibold text-neutral-500 dark:text-neutral-400"> 327 + Collections: 328 + </span> 329 + <div class="flex flex-wrap gap-1"> 330 + <For each={props.permission.collection}>{(col) => <NsidLink nsid={col} />}</For> 331 + </div> 332 + </div> 333 + </Show> 334 + 335 + {/* Actions */} 336 + <Show when={props.permission.action && props.permission.action.length > 0}> 337 + <div class="flex flex-col gap-1"> 338 + <span class="text-xs font-semibold text-neutral-500 dark:text-neutral-400">Actions:</span> 339 + <div class="flex flex-wrap gap-1"> 340 + <For each={props.permission.action}> 341 + {(action) => ( 342 + <span class="dark:bg-dark-200 rounded bg-neutral-200/50 px-1.5 py-0.5 font-mono text-xs"> 343 + {action} 344 + </span> 345 + )} 346 + </For> 347 + </div> 348 + </div> 349 + </Show> 350 + 351 + {/* LXM (for rpc resource) */} 352 + <Show when={props.permission.lxm && props.permission.lxm.length > 0}> 353 + <div class="flex flex-col gap-1"> 354 + <span class="text-xs font-semibold text-neutral-500 dark:text-neutral-400"> 355 + Lexicon Methods: 356 + </span> 357 + <div class="flex flex-wrap gap-1"> 358 + <For each={props.permission.lxm}>{(method) => <NsidLink nsid={method} />}</For> 359 + </div> 360 + </div> 361 + </Show> 362 + 363 + {/* Audience */} 364 + <Show when={props.permission.aud}> 365 + <div class="flex items-center gap-2 text-xs"> 366 + <span class="font-semibold text-neutral-500 dark:text-neutral-400">Audience:</span> 367 + <span class="font-mono">{props.permission.aud}</span> 368 + </div> 369 + </Show> 370 + 371 + {/* Inherit Audience */} 372 + <Show when={props.permission.inheritAud}> 373 + <div class="flex items-center gap-1 text-xs"> 374 + <span class="font-semibold text-neutral-500 dark:text-neutral-400"> 375 + Inherit Audience: 376 + </span> 377 + <span>true</span> 378 + </div> 379 + </Show> 380 + </div> 381 + ); 382 + }; 383 + 260 384 const DefSection = (props: { name: string; def: LexiconDef }) => { 261 385 const defTypeColor = () => { 262 386 switch (props.def.type) { ··· 272 396 return "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300"; 273 397 case "token": 274 398 return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300"; 399 + case "permission-set": 400 + return "bg-cyan-100 text-cyan-800 dark:bg-cyan-900/30 dark:text-cyan-300"; 275 401 default: 276 402 return "bg-neutral-200 text-neutral-800 dark:bg-neutral-700 dark:text-neutral-300"; 277 403 } ··· 302 428 {props.name === "main" ? "Main Definition" : props.name} 303 429 </a> 304 430 <span class={`rounded px-2 py-0.5 text-xs font-semibold uppercase ${defTypeColor()}`}> 305 - {props.def.type} 431 + {props.def.type.replace("-", " ")} 306 432 </span> 307 433 </div> 308 434 ··· 316 442 <span class="text-sm font-semibold">Record Key: </span> 317 443 <span class="font-mono text-sm">{props.def.key}</span> 318 444 </div> 445 + </Show> 446 + 447 + {/* Permission-set: Title and Detail */} 448 + <Show when={props.def.type === "permission-set" && (props.def.title || props.def.detail)}> 449 + <div class="flex flex-col gap-2 rounded-lg border border-neutral-200 bg-neutral-50/50 p-3 dark:border-neutral-700 dark:bg-neutral-800/30"> 450 + <Show when={props.def.title}> 451 + <div class="flex flex-col gap-1"> 452 + <span class="text-xs font-semibold text-neutral-500 uppercase dark:text-neutral-400"> 453 + Title 454 + </span> 455 + <span class="text-sm font-medium">{props.def.title}</span> 456 + </div> 457 + </Show> 458 + <Show when={props.def["title:langs"]}> 459 + <div class="flex flex-col gap-1"> 460 + <span class="text-xs font-semibold text-neutral-500 uppercase dark:text-neutral-400"> 461 + Localized Titles 462 + </span> 463 + <div class="flex flex-col gap-1"> 464 + <For each={Object.entries(props.def["title:langs"]!)}> 465 + {([lang, text]) => ( 466 + <div class="flex items-center gap-2 text-sm"> 467 + <span class="rounded bg-neutral-100 px-1.5 py-0.5 font-mono text-xs dark:bg-neutral-800"> 468 + {lang} 469 + </span> 470 + <span>{text}</span> 471 + </div> 472 + )} 473 + </For> 474 + </div> 475 + </div> 476 + </Show> 477 + <Show when={props.def.detail}> 478 + <div class="flex flex-col gap-1"> 479 + <span class="text-xs font-semibold text-neutral-500 uppercase dark:text-neutral-400"> 480 + Detail 481 + </span> 482 + <p class="text-sm text-neutral-700 dark:text-neutral-300">{props.def.detail}</p> 483 + </div> 484 + </Show> 485 + <Show when={props.def["detail:langs"]}> 486 + <div class="flex flex-col gap-1"> 487 + <span class="text-xs font-semibold text-neutral-500 uppercase dark:text-neutral-400"> 488 + Localized Details 489 + </span> 490 + <div class="flex flex-col gap-1"> 491 + <For each={Object.entries(props.def["detail:langs"]!)}> 492 + {([lang, text]) => ( 493 + <div class="flex flex-col gap-1 text-sm"> 494 + <span class="w-fit rounded bg-neutral-100 px-1.5 py-0.5 font-mono text-xs dark:bg-neutral-800"> 495 + {lang} 496 + </span> 497 + <p class="text-neutral-700 dark:text-neutral-300">{text}</p> 498 + </div> 499 + )} 500 + </For> 501 + </div> 502 + </div> 503 + </Show> 504 + </div> 505 + </Show> 506 + 507 + {/* Permission-set: Permissions list */} 508 + <Show when={props.def.permissions && props.def.permissions.length > 0}> 509 + {(() => { 510 + const supportedPermissions = () => 511 + props.def.permissions!.filter((p) => p.resource === "repo" || p.resource === "rpc"); 512 + return ( 513 + <Show when={supportedPermissions().length > 0}> 514 + <div class="flex flex-col gap-2"> 515 + <h4 class="text-sm font-semibold text-neutral-600 uppercase dark:text-neutral-400"> 516 + Permissions 517 + </h4> 518 + <div class="divide-y divide-neutral-200 rounded-lg border border-neutral-200 bg-neutral-50/50 px-3 dark:divide-neutral-700 dark:border-neutral-700 dark:bg-neutral-800/30"> 519 + <For each={supportedPermissions()}> 520 + {(permission, index) => ( 521 + <PermissionRow permission={permission} index={index()} /> 522 + )} 523 + </For> 524 + </div> 525 + </div> 526 + </Show> 527 + ); 528 + })()} 319 529 </Show> 320 530 321 531 {/* Properties (for record/object types) */}
-143
src/components/login.tsx
··· 1 - import { Client } from "@atcute/client"; 2 - import { Did } from "@atcute/lexicons"; 3 - import { isDid, isHandle } from "@atcute/lexicons/syntax"; 4 - import { 5 - configureOAuth, 6 - createAuthorizationUrl, 7 - defaultIdentityResolver, 8 - finalizeAuthorization, 9 - getSession, 10 - OAuthUserAgent, 11 - type Session, 12 - } from "@atcute/oauth-browser-client"; 13 - import { createSignal, Show } from "solid-js"; 14 - import { didDocumentResolver, handleResolver } from "../utils/api"; 15 - 16 - configureOAuth({ 17 - metadata: { 18 - client_id: import.meta.env.VITE_OAUTH_CLIENT_ID, 19 - redirect_uri: import.meta.env.VITE_OAUTH_REDIRECT_URL, 20 - }, 21 - identityResolver: defaultIdentityResolver({ 22 - handleResolver: handleResolver, 23 - didDocumentResolver: didDocumentResolver, 24 - }), 25 - }); 26 - 27 - export const [agent, setAgent] = createSignal<OAuthUserAgent | undefined>(); 28 - 29 - type Account = { 30 - signedIn: boolean; 31 - handle?: string; 32 - }; 33 - 34 - export type Sessions = Record<string, Account>; 35 - 36 - const Login = () => { 37 - const [notice, setNotice] = createSignal(""); 38 - const [loginInput, setLoginInput] = createSignal(""); 39 - 40 - const login = async (handle: string) => { 41 - try { 42 - setNotice(""); 43 - if (!handle) return; 44 - setNotice(`Contacting your data server...`); 45 - const authUrl = await createAuthorizationUrl({ 46 - scope: import.meta.env.VITE_OAUTH_SCOPE, 47 - target: 48 - isHandle(handle) || isDid(handle) ? 49 - { type: "account", identifier: handle } 50 - : { type: "pds", serviceUrl: handle }, 51 - }); 52 - 53 - setNotice(`Redirecting...`); 54 - await new Promise((resolve) => setTimeout(resolve, 250)); 55 - 56 - location.assign(authUrl); 57 - } catch (e) { 58 - console.error(e); 59 - setNotice(`${e}`); 60 - } 61 - }; 62 - 63 - return ( 64 - <form class="flex flex-col gap-y-2 px-1" onsubmit={(e) => e.preventDefault()}> 65 - <label for="username" class="hidden"> 66 - Add account 67 - </label> 68 - <div class="dark:bg-dark-100 dark:inset-shadow-dark-200 flex grow items-center gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 inset-shadow-xs focus-within:outline-[1px] focus-within:outline-neutral-600 dark:border-neutral-600 dark:focus-within:outline-neutral-400"> 69 - <label 70 - for="username" 71 - class="iconify lucide--user-round-plus shrink-0 text-neutral-500 dark:text-neutral-400" 72 - ></label> 73 - <input 74 - type="text" 75 - spellcheck={false} 76 - placeholder="user.bsky.social" 77 - id="username" 78 - name="username" 79 - autocomplete="username" 80 - aria-label="Your AT Protocol handle" 81 - class="grow py-1 select-none placeholder:text-sm focus:outline-none" 82 - onInput={(e) => setLoginInput(e.currentTarget.value)} 83 - /> 84 - <button 85 - onclick={() => login(loginInput())} 86 - class="flex items-center rounded-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 };
+28 -28
src/components/navbar.tsx
··· 18 18 e.stopPropagation(); 19 19 addToClipboard(props.content); 20 20 }} 21 - class={`-mr-2 hidden items-center rounded px-2 py-1.5 text-neutral-500 transition-all duration-200 group-hover:flex hover:bg-neutral-200/70 hover:text-neutral-600 active:bg-neutral-300/70 dark:text-neutral-400 dark:hover:bg-neutral-700/70 dark:hover:text-neutral-300 dark:active:bg-neutral-600/70`} 21 + class={`-mr-2 hidden items-center rounded px-2 py-1 text-neutral-500 transition-all duration-200 group-hover:flex hover:bg-neutral-200/70 hover:text-neutral-600 active:bg-neutral-300/70 sm:py-1.5 dark:text-neutral-400 dark:hover:bg-neutral-700/70 dark:hover:text-neutral-300 dark:active:bg-neutral-600/70`} 22 22 aria-label="Copy to clipboard" 23 23 > 24 24 <span class="iconify lucide--link"></span> ··· 30 30 31 31 export const NavBar = (props: { params: Params }) => { 32 32 const [handle, setHandle] = createSignal(props.params.repo); 33 - const [showHandle, setShowHandle] = createSignal(localStorage.showHandle === "true"); 34 33 35 34 createEffect(() => { 36 35 if (pds() !== undefined && props.params.repo) { ··· 88 87 <Show when={props.params.repo}> 89 88 {/* Repository Level */} 90 89 <div class="group relative flex items-center justify-between gap-1 rounded-md border-[0.5px] border-transparent bg-transparent px-2 transition-all duration-200 hover:border-neutral-300 hover:bg-neutral-50/40 dark:hover:border-neutral-600 dark:hover:bg-neutral-800/40"> 91 - <div class="flex basis-full items-center gap-2"> 90 + <div class="flex min-w-0 basis-full items-center gap-2"> 92 91 <Tooltip text="Repository"> 93 92 <span class="iconify lucide--book-user shrink-0 text-neutral-500 transition-colors duration-200 group-hover:text-neutral-700 dark:text-neutral-400 dark:group-hover:text-neutral-200"></span> 94 93 </Tooltip> 95 - {props.params.collection ? 94 + <Show 95 + when={props.params.collection} 96 + fallback={ 97 + <span class="flex min-w-0 gap-1 py-0.5 font-medium"> 98 + <Show 99 + when={handle() !== props.params.repo} 100 + fallback={<span class="truncate">{props.params.repo}</span>} 101 + > 102 + <span class="shrink-0">{handle()}</span> 103 + <span class="truncate text-neutral-500 dark:text-neutral-400"> 104 + ({props.params.repo}) 105 + </span> 106 + </Show> 107 + </span> 108 + } 109 + > 96 110 <A 97 111 end 98 112 href={`/at://${props.params.repo}`} 99 - inactiveClass="text-blue-400 w-full py-0.5 font-medium hover:text-blue-500 transition-colors duration-150 dark:hover:text-blue-300" 113 + inactiveClass="flex grow min-w-0 gap-1 py-0.5 font-medium text-blue-400 hover:text-blue-500 transition-colors duration-150 dark:hover:text-blue-300" 100 114 > 101 - {showHandle() ? handle() : props.params.repo} 115 + <Show 116 + when={handle() !== props.params.repo} 117 + fallback={<span class="truncate">{props.params.repo}</span>} 118 + > 119 + <span class="shrink-0">{handle()}</span> 120 + <span class="truncate">({props.params.repo})</span> 121 + </Show> 102 122 </A> 103 - : <span class="py-0.5 font-medium"> 104 - {showHandle() ? handle() : props.params.repo} 105 - </span> 106 - } 107 - </div> 108 - <div class="flex"> 109 - <Tooltip text={showHandle() ? "Show DID" : "Show handle"}> 110 - <button 111 - type="button" 112 - class={`items-center rounded px-1.25 py-1.25 text-neutral-500 transition-all duration-200 hover:bg-neutral-200/70 hover:text-neutral-700 active:bg-neutral-300/70 sm:px-2 sm:py-1.5 dark:text-neutral-400 dark:hover:bg-neutral-700/70 dark:hover:text-neutral-200 dark:active:bg-neutral-600/70 ${isTouchDevice ? "flex" : "hidden group-hover:flex"}`} 113 - onclick={() => { 114 - localStorage.showHandle = !showHandle(); 115 - setShowHandle(!showHandle()); 116 - }} 117 - aria-label="Switch DID/Handle" 118 - > 119 - <span 120 - class={`iconify shrink-0 duration-200 ${showHandle() ? "rotate-y-180" : ""} lucide--arrow-left-right`} 121 - ></span> 122 - </button> 123 - </Tooltip> 124 - <CopyButton content={props.params.repo!} label="Copy DID" /> 123 + </Show> 125 124 </div> 125 + <CopyButton content={props.params.repo!} label="Copy DID" /> 126 126 </div> 127 127 </Show> 128 128
+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"
+44 -17
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; ··· 85 101 onCleanup(() => window.removeEventListener("paste", handlePaste)); 86 102 }); 87 103 104 + createEffect(() => { 105 + if (showSearch()) searchInput.focus(); 106 + }); 107 + 88 108 const fetchTypeahead = async (input: string) => { 89 109 const { prefix, query } = parsePrefix(input); 90 110 ··· 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 }; ··· 168 188 <label for="input" class="hidden"> 169 189 PDS URL, AT URI, NSID, DID, or handle 170 190 </label> 171 - <div class="dark:bg-dark-100 dark:inset-shadow-dark-200 flex items-center gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 inset-shadow-xs focus-within:outline-[1px] focus-within:outline-neutral-600 dark:border-neutral-600 dark:focus-within:outline-neutral-400"> 191 + <div class="dark:bg-dark-100 flex items-center gap-2 rounded-lg bg-white px-2 outline-1 outline-neutral-200 focus-within:outline-[1.5px] focus-within:outline-neutral-600 dark:outline-neutral-600 dark:focus-within:outline-neutral-400"> 172 192 <label 173 193 for="input" 174 194 class="iconify lucide--search text-neutral-500 dark:text-neutral-400" ··· 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 min-w-0 flex-col"> 316 + <Show when={actor.displayName}> 317 + <span class="truncate text-sm font-medium">{actor.displayName}</span> 318 + </Show> 319 + <span class="truncate text-xs text-neutral-600 dark:text-neutral-400"> 320 + @{actor.handle} 321 + </span> 322 + </div> 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 - <span class="iconify lucide--help-circle"></span> 385 + <span class="iconify lucide--help-circle text-neutral-600 dark:text-neutral-300"></span> 359 386 </button> 360 387 </> 361 388 );
+2 -2
src/components/sticky.tsx
··· 29 29 /> 30 30 31 31 <div 32 - class="sticky top-2 z-10 flex w-full flex-col items-center justify-center gap-2 rounded-lg p-3 transition-colors" 32 + class="sticky top-2 z-10 flex w-full flex-col items-center justify-center gap-2 rounded-lg border-[0.5px] p-3 transition-colors" 33 33 classList={{ 34 - "bg-neutral-50 dark:bg-dark-300 border-[0.5px] border-neutral-300 dark:border-neutral-700 shadow-md": 34 + "bg-neutral-50 dark:bg-dark-300 border-neutral-300 dark:border-neutral-700 shadow-md": 35 35 filterStuck(), 36 36 "bg-transparent border-transparent shadow-none": !filterStuck(), 37 37 }}
+1 -1
src/components/text-input.tsx
··· 25 25 disabled={props.disabled} 26 26 required={props.required} 27 27 class={ 28 - "dark:bg-dark-100 dark:inset-shadow-dark-200 rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 py-1 inset-shadow-xs select-none placeholder:text-sm focus:outline-[1px] focus:outline-neutral-600 dark:border-neutral-600 dark:focus:outline-neutral-400 " + 28 + "dark:bg-dark-100 rounded-lg bg-white px-2 py-1 outline-1 outline-neutral-200 select-none placeholder:text-sm focus:outline-[1.5px] focus:outline-neutral-600 dark:outline-neutral-600 dark:focus:outline-neutral-400 " + 29 29 props.class 30 30 } 31 31 onInput={props.onInput}
+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 );
+39 -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); 47 48 49 + const handleGoToRepo = (ev: KeyboardEvent) => { 50 + if (document.querySelector("[data-modal]")) return; 51 + if (ev.target instanceof HTMLInputElement || ev.target instanceof HTMLTextAreaElement) return; 52 + 53 + if (ev.key === "g" && agent()?.sub) { 54 + ev.preventDefault(); 55 + navigate(`/at://${agent()!.sub}`); 56 + } 57 + }; 58 + 59 + window.addEventListener("keydown", handleGoToRepo); 60 + onCleanup(() => window.removeEventListener("keydown", handleGoToRepo)); 61 + 48 62 if (localStorage.getItem("sailor") === "true") { 49 63 const style = document.createElement("style"); 50 64 style.textContent = ` ··· 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-3"> 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--tag" /> 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> ··· 188 192 </Show> 189 193 </div> 190 194 <NotificationContainer /> 195 + <Show 196 + when={localStorage.plcDirectory && localStorage.plcDirectory !== "https://plc.directory"} 197 + > 198 + <div class="dark:bg-dark-500 fixed right-0 bottom-0 left-0 z-10 flex items-center justify-center bg-neutral-100 px-3 py-1 text-xs"> 199 + <span> 200 + PLC directory: <span class="font-medium">{localStorage.plcDirectory}</span> 201 + </span> 202 + </div> 203 + </Show> 191 204 </div> 192 205 ); 193 206 };
+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 {
-17
src/utils/api.ts
··· 133 133 linking_records: Array<{ did: string; collection: string; rkey: string }>; 134 134 }; 135 135 136 - type LinksWithDids = { 137 - cursor: string; 138 - total: number; 139 - linking_dids: Array<string>; 140 - }; 141 - 142 136 const getConstellation = async ( 143 137 endpoint: string, 144 138 target: string, ··· 175 169 ): Promise<LinksWithRecords> => 176 170 getConstellation("/links", target, collection, path, cursor, limit || 100); 177 171 178 - const getDidBacklinks = ( 179 - target: string, 180 - collection: string, 181 - path: string, 182 - cursor?: string, 183 - limit?: number, 184 - ): Promise<LinksWithDids> => 185 - getConstellation("/links/distinct-dids", target, collection, path, cursor, limit || 100); 186 - 187 172 export { 188 173 didDocCache, 189 174 getAllBacklinks, 190 - getDidBacklinks, 191 175 getPDS, 192 176 getRecordBacklinks, 193 177 labelerCache, ··· 198 182 resolvePDS, 199 183 validateHandle, 200 184 type LinkData, 201 - type LinksWithDids, 202 185 type LinksWithRecords, 203 186 };
-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 + };
+24
src/utils/route-cache.ts
··· 1 + import { createStore } from "solid-js/store"; 2 + 3 + export interface CollectionCacheEntry { 4 + records: unknown[]; 5 + cursor: string | undefined; 6 + scrollY: number; 7 + reverse: boolean; 8 + } 9 + 10 + type RouteCache = Record<string, CollectionCacheEntry>; 11 + 12 + const [routeCache, setRouteCache] = createStore<RouteCache>({}); 13 + 14 + export const getCollectionCache = (key: string): CollectionCacheEntry | undefined => { 15 + return routeCache[key]; 16 + }; 17 + 18 + export const setCollectionCache = (key: string, entry: CollectionCacheEntry): void => { 19 + setRouteCache(key, entry); 20 + }; 21 + 22 + export const clearCollectionCache = (key: string): void => { 23 + setRouteCache(key, undefined!); 24 + };
+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 };
+7 -6
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 pb-20 font-mono text-xs sm:text-sm"> 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="truncate rounded px-0.5 text-left text-blue-400 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 40 + dir="rtl" 40 41 > 41 - <span class="text-blue-400">{cid}</span> 42 + {cid} 42 43 </a> 43 44 )} 44 45 </For> 45 46 </div> 46 47 </Show> 47 - <div class="dark:bg-dark-500 fixed bottom-0 z-5 flex w-screen justify-center bg-neutral-100 py-2"> 48 + <div class="dark:bg-dark-500 fixed bottom-0 z-5 flex w-screen justify-center bg-neutral-100 pt-2 pb-4"> 48 49 <div class="flex flex-col items-center gap-1 pb-2"> 49 50 <p> 50 51 {blobs()?.length} blob{(blobs()?.length ?? 0 > 1) ? "s" : ""}
+106 -50
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 - import { A, useParams } from "@solidjs/router"; 6 - import { createEffect, createResource, createSignal, For, Show, untrack } from "solid-js"; 5 + import { A, useBeforeLeave, useParams } from "@solidjs/router"; 6 + import { 7 + createEffect, 8 + createMemo, 9 + createResource, 10 + createSignal, 11 + For, 12 + onMount, 13 + Show, 14 + } from "solid-js"; 7 15 import { createStore } from "solid-js/store"; 16 + import { hasUserScope } from "../auth/scope-utils"; 17 + import { agent } from "../auth/state"; 8 18 import { Button } from "../components/button.jsx"; 9 19 import { JSONType, JSONValue } from "../components/json.jsx"; 10 - import { agent } from "../components/login.jsx"; 11 20 import { Modal } from "../components/modal.jsx"; 12 21 import { addNotification, removeNotification } from "../components/notification.jsx"; 13 22 import { StickyOverlay } from "../components/sticky.jsx"; 14 23 import { TextInput } from "../components/text-input.jsx"; 15 24 import Tooltip from "../components/tooltip.jsx"; 25 + import { isTouchDevice } from "../layout.jsx"; 16 26 import { resolvePDS } from "../utils/api.js"; 17 27 import { localDateFromTimestamp } from "../utils/date.js"; 28 + import { 29 + clearCollectionCache, 30 + getCollectionCache, 31 + setCollectionCache, 32 + } from "../utils/route-cache.js"; 18 33 19 34 interface AtprotoRecord { 20 35 rkey: string; ··· 41 56 42 57 return ( 43 58 <span 44 - class="relative flex w-full min-w-0 items-baseline rounded px-0.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 59 + class="relative flex w-full min-w-0 items-baseline rounded p-0.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 45 60 ref={rkeyRef} 46 - onmouseover={() => setHover(true)} 47 - onmouseleave={() => setHover(false)} 61 + onmouseover={() => !isTouchDevice && setHover(true)} 62 + onmouseleave={() => !isTouchDevice && setHover(false)} 48 63 > 49 64 <span class="flex items-baseline truncate"> 50 - <span class="shrink-0 text-sm text-blue-400 sm:text-base">{props.record.rkey}</span> 65 + <span class="shrink-0 text-sm text-blue-400">{props.record.rkey}</span> 51 66 <span class="ml-1 truncate text-xs text-neutral-500 dark:text-neutral-400" dir="rtl"> 52 67 {props.record.cid} 53 68 </span> ··· 65 80 <JSONValue 66 81 data={props.record.record.value as JSONType} 67 82 repo={props.record.record.uri.split("/")[2]} 83 + truncate 68 84 /> 69 85 </span> 70 86 </Show> ··· 82 98 const [reverse, setReverse] = createSignal(false); 83 99 const [recreate, setRecreate] = createSignal(false); 84 100 const [openDelete, setOpenDelete] = createSignal(false); 101 + const [restoredFromCache, setRestoredFromCache] = createSignal(false); 85 102 const did = params.repo; 86 103 let pds: string; 87 104 let rpc: Client; 88 105 106 + const cacheKey = () => `${params.pds}/${params.repo}/${params.collection}`; 107 + 108 + onMount(() => { 109 + const cached = getCollectionCache(cacheKey()); 110 + if (cached) { 111 + setRecords(cached.records as AtprotoRecord[]); 112 + setCursor(cached.cursor); 113 + setReverse(cached.reverse); 114 + setRestoredFromCache(true); 115 + requestAnimationFrame(() => { 116 + window.scrollTo(0, cached.scrollY); 117 + }); 118 + } 119 + }); 120 + 121 + useBeforeLeave((e) => { 122 + const recordPathPrefix = `/at://${did}/${params.collection}/`; 123 + const isNavigatingToRecord = typeof e.to === "string" && e.to.startsWith(recordPathPrefix); 124 + 125 + if (isNavigatingToRecord && records.length > 0) { 126 + setCollectionCache(cacheKey(), { 127 + records: [...records], 128 + cursor: cursor(), 129 + scrollY: window.scrollY, 130 + reverse: reverse(), 131 + }); 132 + } else { 133 + clearCollectionCache(cacheKey()); 134 + } 135 + }); 136 + 89 137 const fetchRecords = async () => { 138 + if (restoredFromCache() && records.length > 0 && !cursor()) { 139 + setRestoredFromCache(false); 140 + return records; 141 + } 142 + if (restoredFromCache()) setRestoredFromCache(false); 143 + 90 144 if (!pds) pds = await resolvePDS(did!); 91 - if (!rpc) rpc = new Client({ handler: new CredentialManager({ service: pds }) }); 145 + if (!rpc) rpc = new Client({ handler: simpleFetchHandler({ service: pds }) }); 92 146 const res = await rpc.get("com.atproto.repo.listRecords", { 93 147 params: { 94 148 repo: did as ActorIdentifier, ··· 116 170 }; 117 171 118 172 const [response, { refetch }] = createResource(fetchRecords); 173 + 174 + const filteredRecords = createMemo(() => 175 + records.filter((rec) => 176 + filter() ? JSON.stringify(rec.record.value).includes(filter()!) : true, 177 + ), 178 + ); 119 179 120 180 const deleteRecords = async () => { 121 181 const recsToDel = records.filter((record) => record.toDelete); ··· 159 219 setCursor(undefined); 160 220 setOpenDelete(false); 161 221 setRecreate(false); 222 + clearCollectionCache(cacheKey()); 162 223 refetch(); 163 224 }; 164 225 ··· 192 253 <StickyOverlay> 193 254 <div class="flex w-full flex-col gap-2"> 194 255 <div class="flex items-center gap-1"> 195 - <Show when={agent() && agent()?.sub === did}> 256 + <Show when={agent() && agent()?.sub === did && hasUserScope("delete")}> 196 257 <div class="flex items-center"> 197 258 <Tooltip 198 259 text={batchDelete() ? "Cancel" : "Delete"} 199 260 children={ 200 261 <button 201 262 onclick={() => { 202 - setRecords( 203 - { from: 0, to: untrack(() => records.length) - 1 }, 204 - "toDelete", 205 - false, 206 - ); 263 + setRecords({ from: 0, to: records.length - 1 }, "toDelete", false); 207 264 setLastSelected(undefined); 208 265 setBatchDelete(!batchDelete()); 209 266 }} 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" 267 + class="flex items-center rounded-md p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 211 268 > 212 269 <span 213 - class={`iconify text-lg ${batchDelete() ? "lucide--circle-x" : "lucide--trash-2"} `} 270 + class={`iconify ${batchDelete() ? "lucide--circle-x" : "lucide--trash-2"} `} 214 271 ></span> 215 272 </button> 216 273 } ··· 221 278 children={ 222 279 <button 223 280 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" 281 + class="flex items-center rounded-md p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 225 282 > 226 - <span class="iconify lucide--copy-check text-lg"></span> 283 + <span class="iconify lucide--copy-check"></span> 227 284 </button> 228 285 } 229 286 /> 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 - /> 287 + <Show when={hasUserScope("create")}> 288 + <Tooltip 289 + text="Recreate" 290 + children={ 291 + <button 292 + onclick={() => { 293 + setRecreate(true); 294 + setOpenDelete(true); 295 + }} 296 + class="flex items-center rounded-md p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 297 + > 298 + <span class="iconify lucide--recycle text-green-500 dark:text-green-400"></span> 299 + </button> 300 + } 301 + /> 302 + </Show> 244 303 <Tooltip 245 304 text="Delete" 246 305 children={ ··· 249 308 setRecreate(false); 250 309 setOpenDelete(true); 251 310 }} 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" 311 + class="flex items-center rounded-md p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 253 312 > 254 - <span class="iconify lucide--trash-2 text-lg text-red-500 dark:text-red-400"></span> 313 + <span class="iconify lucide--trash-2 text-red-500 dark:text-red-400"></span> 255 314 </button> 256 315 } 257 316 /> ··· 275 334 </div> 276 335 </Modal> 277 336 </Show> 337 + <TextInput 338 + name="Filter" 339 + placeholder="Filter by substring" 340 + onInput={(e) => setFilter(e.currentTarget.value)} 341 + class="grow" 342 + /> 278 343 <Tooltip text="Jetstream"> 279 344 <A 280 345 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" 346 + class="flex items-center rounded-md p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 282 347 > 283 - <span class="iconify lucide--radio-tower text-lg"></span> 348 + <span class="iconify lucide--radio-tower"></span> 284 349 </A> 285 350 </Tooltip> 286 - <TextInput 287 - name="Filter" 288 - placeholder="Filter by substring" 289 - onInput={(e) => setFilter(e.currentTarget.value)} 290 - class="grow" 291 - /> 292 351 </div> 293 352 <Show when={records.length > 1}> 294 353 <div class="flex items-center justify-between gap-x-2"> ··· 297 356 setReverse(!reverse()); 298 357 setRecords([]); 299 358 setCursor(undefined); 359 + clearCollectionCache(cacheKey()); 300 360 refetch(); 301 361 }} 302 362 > ··· 310 370 <span>{records.filter((rec) => rec.toDelete).length}</span> 311 371 <span>/</span> 312 372 </Show> 313 - <span>{records.length} records</span> 373 + <span>{filter() ? filteredRecords().length : records.length} records</span> 314 374 </div> 315 375 <div class="flex w-20 items-center justify-end"> 316 376 <Show when={cursor()}> ··· 327 387 </div> 328 388 </StickyOverlay> 329 389 <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 - > 390 + <For each={filteredRecords()}> 335 391 {(record, index) => ( 336 392 <> 337 393 <Show when={batchDelete()}> ··· 348 404 </label> 349 405 </Show> 350 406 <Show when={!batchDelete()}> 351 - <A href={`/at://${did}/${params.collection}/${record.rkey}`}> 407 + <A href={`/at://${did}/${params.collection}/${record.rkey}`} class="select-none"> 352 408 <RecordLink record={record} /> 353 409 </A> 354 410 </Show>
+9 -9
src/views/home.tsx
··· 1 1 export const Home = () => { 2 2 return ( 3 3 <div class="flex w-full flex-col gap-3 wrap-break-word"> 4 - <div class="flex flex-col gap-0.5"> 4 + <div class="flex flex-col gap-1"> 5 5 <div> 6 6 <span class="text-xl font-semibold">AT Protocol Explorer</span> 7 7 </div> ··· 16 16 </span> 17 17 </div> 18 18 <div class="flex items-center gap-1"> 19 - <div class="iconify lucide--user-round" /> 20 - <span>Login to manage records in your repository.</span> 21 - </div> 22 - <div class="flex items-center gap-1"> 23 - <div class="iconify lucide--radio-tower" /> 24 - <span>Jetstream and firehose streaming.</span> 25 - </div> 26 - <div class="flex items-center gap-1"> 27 19 <div class="iconify lucide--link" /> 28 20 <span> 29 21 Backlinks support with{" "} ··· 36 28 </a> 37 29 . 38 30 </span> 31 + </div> 32 + <div class="flex items-center gap-1"> 33 + <div class="iconify lucide--user-round" /> 34 + <span>Login to manage records in your repository.</span> 35 + </div> 36 + <div class="flex items-center gap-1"> 37 + <div class="iconify lucide--radio-tower" /> 38 + <span>Jetstream and firehose streaming.</span> 39 39 </div> 40 40 <div class="flex items-center gap-1"> 41 41 <div class="iconify lucide--tag" />
+20 -22
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 }); ··· 230 228 rows={2} 231 229 value={searchParams.uriPatterns ?? "*"} 232 230 placeholder="at://did:web:example.com/app.bsky.feed.post/*" 233 - class="dark:bg-dark-100 dark:inset-shadow-dark-200 grow rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 py-1.5 text-sm inset-shadow-xs focus:outline-[1px] focus:outline-neutral-600 dark:border-neutral-600 dark:focus:outline-neutral-400" 231 + class="dark:bg-dark-100 grow rounded-lg bg-white px-2 py-1.5 text-sm outline-1 outline-neutral-200 focus:outline-[1.5px] focus:outline-neutral-600 dark:outline-neutral-600 dark:focus:outline-neutral-400" 234 232 /> 235 233 </label> 236 234 </div>
+11 -20
src/views/logs.tsx
··· 55 55 } 56 56 }); 57 57 58 - const FilterButton = (props: { icon: string; event: PlcEvent; label: string }) => { 58 + const FilterButton = (props: { event: PlcEvent; label: string }) => { 59 59 const isActive = () => activePlcEvent() === props.event; 60 60 const toggleFilter = () => setActivePlcEvent(isActive() ? undefined : props.event); 61 61 62 62 return ( 63 63 <button 64 64 classList={{ 65 - "flex items-center gap-1 sm:gap-1.5 rounded-lg px-3 py-2 sm:px-2 sm:py-1.5 text-base sm:text-sm transition-colors": true, 66 - "bg-neutral-700 text-white dark:bg-neutral-200 dark:text-neutral-900": isActive(), 65 + "font-medium rounded-lg px-2 py-1.5 text-xs sm:text-sm transition-colors": true, 66 + "bg-neutral-700 text-white dark:bg-neutral-300 dark:text-neutral-900": isActive(), 67 67 "bg-neutral-200 text-neutral-700 hover:bg-neutral-300 dark:bg-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-600": 68 68 !isActive(), 69 69 }} 70 70 onclick={toggleFilter} 71 71 > 72 - <span class={props.icon}></span> 73 - <span class="hidden font-medium sm:inline">{props.label}</span> 72 + {props.label} 74 73 </button> 75 74 ); 76 75 }; ··· 255 254 <div class="iconify lucide--filter" /> 256 255 <p class="font-medium">Filter by type</p> 257 256 </div> 258 - <div class="flex flex-wrap gap-1 sm:gap-2"> 259 - <FilterButton icon="iconify lucide--at-sign" event="handle" label="Alias" /> 260 - <FilterButton icon="iconify lucide--hard-drive" event="service" label="Service" /> 261 - <FilterButton 262 - icon="iconify lucide--shield-check" 263 - event="verification_method" 264 - label="Verification" 265 - /> 266 - <FilterButton 267 - icon="iconify lucide--key-round" 268 - event="rotation_key" 269 - label="Rotation Key" 270 - /> 257 + <div class="flex flex-wrap gap-1"> 258 + <FilterButton event="handle" label="Alias" /> 259 + <FilterButton event="service" label="Service" /> 260 + <FilterButton event="verification_method" label="Verification" /> 261 + <FilterButton event="rotation_key" label="Rotation Key" /> 271 262 </div> 272 263 </div> 273 264 <div class="flex items-center gap-1.5 text-sm font-medium"> 274 265 <Show when={validLog() === true}> 275 - <span class="iconify lucide--check-circle-2 text-green-500 dark:text-green-400"></span> 266 + <span class="iconify lucide--check text-green-600 dark:text-green-400"></span> 276 267 <span>Valid log</span> 277 268 </Show> 278 269 <Show when={validLog() === false}> 279 - <span class="iconify lucide--x-circle text-red-500 dark:text-red-400"></span> 270 + <span class="iconify lucide--x text-red-500 dark:text-red-400"></span> 280 271 <span>Log validation failed</span> 281 272 </Show> 282 273 <Show when={validLog() === undefined}>
+96 -72
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"; 6 6 import { createResource, createSignal, For, Show } from "solid-js"; 7 7 import { Button } from "../components/button"; 8 - import { CopyMenu, DropdownMenu, MenuProvider, NavMenu } from "../components/dropdown"; 9 8 import { Modal } from "../components/modal"; 10 9 import { setPDS } from "../components/navbar"; 11 10 import Tooltip from "../components/tooltip"; 11 + import { resolveDidDoc } from "../utils/api"; 12 12 import { localDateFromTimestamp } from "../utils/date"; 13 13 14 14 const LIMIT = 1000; ··· 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 ··· 54 54 55 55 const RepoCard = (repo: ComAtprotoSyncListRepos.Repo) => { 56 56 const [openInfo, setOpenInfo] = createSignal(false); 57 + const [handle, setHandle] = createSignal<string>(); 58 + 59 + const fetchHandle = async () => { 60 + try { 61 + const doc = await resolveDidDoc(repo.did); 62 + const aka = doc.alsoKnownAs?.find((a) => a.startsWith("at://")); 63 + if (aka) setHandle(aka.replace("at://", "")); 64 + } catch {} 65 + }; 57 66 58 67 return ( 59 - <div class="flex items-center"> 68 + <div class="flex items-center gap-0.5"> 60 69 <A 61 70 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" 71 + 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 72 > 64 73 {repo.did} 65 74 </A> ··· 69 78 </Tooltip> 70 79 </Show> 71 80 <button 72 - onclick={() => setOpenInfo(true)} 73 - class="flex items-center rounded-lg p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 81 + onclick={() => { 82 + setOpenInfo(true); 83 + if (!handle()) fetchHandle(); 84 + }} 85 + class="flex items-center rounded-md p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 74 86 > 75 - <span class="iconify lucide--info"></span> 87 + <span class="iconify lucide--info text-neutral-600 dark:text-neutral-400"></span> 76 88 </button> 77 89 <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> 90 + <div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-70 left-[50%] w-max max-w-[90vw] -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-white p-3 shadow-md transition-opacity duration-200 sm:max-w-xl dark:border-neutral-700 starting:opacity-0"> 91 + <div class="mb-2 flex items-center justify-between gap-4"> 92 + <p class="truncate font-semibold">{repo.did}</p> 84 93 <button 85 94 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" 95 + 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 96 > 88 97 <span class="iconify lucide--x"></span> 89 98 </button> 90 99 </div> 91 - <div class="flex flex-col text-sm"> 92 - <span> 93 - Head: <span class="text-xs">{repo.head}</span> 94 - </span> 100 + <div class="grid grid-cols-[auto_1fr] items-baseline gap-x-1 gap-y-0.5 text-sm"> 101 + <span class="font-medium">Handle:</span> 102 + <span class="text-neutral-700 dark:text-neutral-300">{handle()}</span> 103 + <span class="font-medium">Head:</span> 104 + <span class="wrap-anywhere text-neutral-700 dark:text-neutral-300">{repo.head}</span> 105 + 95 106 <Show when={TID.validate(repo.rev)}> 96 - <span> 97 - Rev: {repo.rev} ({localDateFromTimestamp(TID.parse(repo.rev).timestamp / 1000)}) 98 - </span> 107 + <span class="font-medium">Rev:</span> 108 + <div class="flex gap-1"> 109 + <span class="text-neutral-700 dark:text-neutral-300">{repo.rev}</span> 110 + <span class="text-neutral-600 dark:text-neutral-400">ยท</span> 111 + <span class="text-neutral-600 dark:text-neutral-400"> 112 + {localDateFromTimestamp(TID.parse(repo.rev).timestamp / 1000)} 113 + </span> 114 + </div> 99 115 </Show> 116 + 100 117 <Show when={repo.active !== undefined}> 101 - <span>Active: {repo.active ? "true" : "false"}</span> 118 + <span class="font-medium">Active:</span> 119 + <span 120 + class={`iconify self-center ${ 121 + repo.active ? 122 + "lucide--check text-green-500 dark:text-green-400" 123 + : "lucide--x text-red-500 dark:text-red-400" 124 + }`} 125 + ></span> 102 126 </Show> 127 + 103 128 <Show when={repo.status}> 104 - <span>Status: {repo.status}</span> 129 + <span class="font-medium">Status:</span> 130 + <span class="text-neutral-700 dark:text-neutral-300">{repo.status}</span> 105 131 </Show> 106 132 </div> 107 133 </div> ··· 110 136 ); 111 137 }; 112 138 113 - const Tab = (props: { tab: "repos" | "info"; label: string }) => ( 114 - <div class="flex items-center gap-0.5"> 115 - <A 116 - classList={{ 117 - "flex items-center gap-1 border-b-2": true, 118 - "border-transparent hover:border-neutral-400 dark:hover:border-neutral-600": 119 - (!!location.hash && location.hash !== `#${props.tab}`) || 120 - (!location.hash && props.tab !== "repos"), 121 - }} 122 - href={`/${params.pds}#${props.tab}`} 123 - > 124 - {props.label} 125 - </A> 126 - </div> 139 + const Tab = (props: { tab: "repos" | "info" | "firehose"; label: string }) => ( 140 + <A 141 + classList={{ 142 + "border-b-2 font-medium": true, 143 + "border-transparent dark:text-neutral-300/80 text-neutral-600 hover:border-neutral-600 dark:hover:border-neutral-300/80": 144 + (!!location.hash && location.hash !== `#${props.tab}`) || 145 + (!location.hash && props.tab !== "repos"), 146 + }} 147 + href={ 148 + props.tab === "firehose" ? 149 + `/firehose?instance=wss://${params.pds}` 150 + : `/${params.pds}#${props.tab}` 151 + } 152 + > 153 + {props.label} 154 + </A> 127 155 ); 128 156 129 157 return ( 130 158 <Show when={repos() || response()}> 131 - <div class="flex w-full flex-col"> 132 - <div class="dark:shadow-dark-700 dark:bg-dark-300 mb-2 flex w-full justify-between rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 text-sm shadow-xs dark:border-neutral-700"> 133 - <div class="ml-1 flex gap-3"> 134 - <Tab tab="repos" label="Repositories" /> 135 - <Tab tab="info" label="Info" /> 136 - </div> 137 - <MenuProvider> 138 - <DropdownMenu 139 - icon="lucide--ellipsis-vertical" 140 - buttonClass="rounded-sm p-1.5" 141 - menuClass="top-9 p-2 text-sm" 142 - > 143 - <CopyMenu content={params.pds!} label="Copy PDS" icon="lucide--copy" /> 144 - <NavMenu 145 - href={`/firehose?instance=wss://${params.pds}`} 146 - label="Firehose" 147 - icon="lucide--radio-tower" 148 - /> 149 - </DropdownMenu> 150 - </MenuProvider> 159 + <div class="flex w-full flex-col px-2"> 160 + <div class="mb-3 flex gap-4 text-sm sm:text-base"> 161 + <Tab tab="repos" label="Repositories" /> 162 + <Tab tab="info" label="Info" /> 163 + <Tab tab="firehose" label="Firehose" /> 151 164 </div> 152 - <div class="flex flex-col gap-1 px-2"> 153 - <Show when={!location.hash || location.hash === "#repos"}> 154 - <div class="flex flex-col divide-y-[0.5px] divide-neutral-300 dark:divide-neutral-700"> 155 - <For each={repos()}>{(repo) => <RepoCard {...repo} />}</For> 156 - </div> 157 - </Show> 165 + <Show when={!location.hash || location.hash === "#repos"}> 166 + <div class="flex flex-col divide-y-[0.5px] divide-neutral-300 pb-20 dark:divide-neutral-700"> 167 + <For each={repos()}>{(repo) => <RepoCard {...repo} />}</For> 168 + </div> 169 + </Show> 170 + <div class="flex flex-col gap-2"> 158 171 <Show when={location.hash === "#info"}> 159 172 <Show when={version()}> 160 173 {(version) => ( 161 - <div class="flex items-baseline gap-x-1"> 174 + <div class="flex flex-col"> 162 175 <span class="font-semibold">Version</span> 163 - <span class="truncate text-sm">{version()}</span> 176 + <span class="text-sm text-neutral-700 dark:text-neutral-300">{version()}</span> 164 177 </div> 165 178 )} 166 179 </Show> 167 180 <Show when={serverInfos()}> 168 181 {(server) => ( 169 182 <> 170 - <div class="flex items-baseline gap-x-1"> 183 + <div class="flex flex-col"> 171 184 <span class="font-semibold">DID</span> 172 - <span class="truncate text-sm">{server().did}</span> 185 + <span class="text-sm">{server().did}</span> 173 186 </div> 174 - <Show when={server().inviteCodeRequired}> 187 + <div class="flex items-center gap-1"> 175 188 <span class="font-semibold">Invite Code Required</span> 176 - </Show> 189 + <span 190 + classList={{ 191 + "iconify lucide--check text-green-500 dark:text-green-400": 192 + server().inviteCodeRequired === true, 193 + "iconify lucide--x text-red-500 dark:text-red-400": 194 + !server().inviteCodeRequired, 195 + }} 196 + ></span> 197 + </div> 177 198 <Show when={server().phoneVerificationRequired}> 178 - <span class="font-semibold">Phone Verification Required</span> 199 + <div class="flex items-center gap-1"> 200 + <span class="font-semibold">Phone Verification Required</span> 201 + <span class="iconify lucide--check text-green-500 dark:text-green-400"></span> 202 + </div> 179 203 </Show> 180 204 <Show when={server().availableUserDomains.length}> 181 205 <div class="flex flex-col"> ··· 226 250 </div> 227 251 </div> 228 252 <Show when={!location.hash || location.hash === "#repos"}> 229 - <div class="dark:bg-dark-500 fixed bottom-0 z-5 flex w-screen justify-center bg-neutral-100 py-2"> 253 + <div class="dark:bg-dark-500 fixed bottom-0 z-5 flex w-screen justify-center bg-neutral-100 pt-2 pb-4"> 230 254 <div class="flex flex-col items-center gap-1 pb-2"> 231 255 <p>{repos()?.length} loaded</p> 232 256 <Show when={!response.loading && cursor()}>
+40 -50
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-transparent hover:border-neutral-400 dark:hover:border-neutral-600": 366 + "border-b-2 font-medium": true, 367 + "border-transparent text-neutral-600 dark:text-neutral-300/80 hover:border-neutral-600 dark:hover:border-neutral-300/80": 367 368 !isActive(), 368 369 }} 369 370 href={`/at://${did}/${params.collection}/${params.rkey}#${props.tab}`} ··· 380 381 return ( 381 382 <Show when={record()} keyed> 382 383 <div class="flex w-full flex-col items-center"> 383 - <div class="dark:shadow-dark-700 dark:bg-dark-300 mb-3 flex w-full justify-between rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 text-sm shadow-xs dark:border-neutral-700"> 384 - <div class="ml-1 flex gap-3"> 384 + <div class="mb-3 flex w-full justify-between px-2 text-sm sm:text-base"> 385 + <div class="flex items-center gap-4"> 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" ··· 489 490 <Show when={location.hash === "#info"}> 490 491 <div class="flex w-full flex-col gap-2 px-2 text-sm"> 491 492 <div> 492 - <div class="flex items-center gap-1"> 493 - <span class="iconify lucide--at-sign"></span> 494 - <p class="font-semibold">AT URI</p> 495 - </div> 493 + <p class="font-semibold">AT URI</p> 496 494 <div class="truncate text-xs">{record()?.uri}</div> 497 495 </div> 498 496 <Show when={record()?.cid}> 499 497 <div> 500 - <div class="flex items-center gap-1"> 501 - <span class="iconify lucide--box"></span> 502 - <p class="font-semibold">CID</p> 503 - </div> 498 + <p class="font-semibold">CID</p> 504 499 <div class="truncate text-left text-xs" dir="rtl"> 505 500 {record()?.cid} 506 501 </div> ··· 508 503 </Show> 509 504 <div> 510 505 <div class="flex items-center gap-1"> 511 - <span class="iconify lucide--lock-keyhole"></span> 512 506 <p class="font-semibold">Record verification</p> 513 507 <span 514 508 classList={{ ··· 525 519 </div> 526 520 <div> 527 521 <div class="flex items-center gap-1"> 528 - <span class="iconify lucide--file-check"></span> 529 522 <p class="font-semibold">Schema validation</p> 530 523 <span 531 524 classList={{ ··· 555 548 </div> 556 549 <Show when={lexiconUri()}> 557 550 <div> 558 - <div class="flex items-center gap-1"> 559 - <span class="iconify lucide--scroll-text"></span> 560 - <p class="font-semibold">Lexicon schema</p> 561 - </div> 551 + <p class="font-semibold">Lexicon schema</p> 562 552 <div class="truncate text-xs"> 563 553 <A 564 554 href={`/${lexiconUri()}`}
+190 -201
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 font-medium": true, 92 + "border-transparent text-neutral-600 dark:text-neutral-300/80 hover:border-neutral-600 dark:hover:border-neutral-300/80": 93 + !isActive(), 94 + }} 95 + href={`/at://${params.repo}#${props.tab}`} 96 + > 97 + {props.label} 99 98 </A> 100 99 ); 101 100 }; ··· 115 114 if (!did.startsWith("did:")) { 116 115 try { 117 116 const did = await resolveHandle(params.repo as Handle); 118 - navigate(location.pathname.replace(params.repo!, did)); 117 + navigate(location.pathname.replace(params.repo!, did), { replace: true }); 119 118 return; 120 119 } catch { 121 120 try { 122 121 const nsid = params.repo as Nsid; 123 122 const res = await resolveLexiconAuthority(nsid); 124 - navigate(`/at://${res}/com.atproto.lexicon.schema/${nsid}`); 123 + navigate(`/at://${res}/com.atproto.lexicon.schema/${nsid}`, { replace: true }); 125 124 return; 126 125 } catch { 127 - navigate(`/${did}`); 126 + navigate(`/${did}`, { replace: true }); 128 127 return; 129 128 } 130 129 } ··· 141 140 return {}; 142 141 } 143 142 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 - } 143 + rpc = new Client({ handler: simpleFetchHandler({ service: pds }) }); 144 + try { 145 + const res = await rpc.get("com.atproto.repo.describeRepo", { 146 + params: { repo: did as ActorIdentifier }, 159 147 }); 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"); 148 + if (res.ok) { 149 + const collections: Record<string, { hidden: boolean; nsids: string[] }> = {}; 150 + res.data.collections.forEach((c) => { 151 + const nsid = c.split("."); 152 + if (nsid.length > 2) { 153 + const authority = `${nsid[0]}.${nsid[1]}`; 154 + collections[authority] = { 155 + nsids: (collections[authority]?.nsids ?? []).concat(nsid.slice(2).join(".")), 156 + hidden: false, 157 + }; 158 + } 159 + }); 160 + setNsids(collections); 161 + } else { 162 + console.error(res.data.error); 163 + switch (res.data.error) { 164 + case "RepoDeactivated": 165 + setError("Deactivated"); 166 + break; 167 + case "RepoTakendown": 168 + setError("Takendown"); 169 + break; 170 + default: 171 + setError("Unreachable"); 172 + } 172 173 } 173 - } 174 174 175 - return res.data; 175 + return res.data; 176 + } catch { 177 + return {}; 178 + } 176 179 }; 177 180 178 181 const [repo] = createResource(fetchRepo); ··· 209 212 let loaded = 0; 210 213 211 214 const reader = response.body?.getReader(); 212 - const chunks: Uint8Array[] = []; 215 + const chunks: BlobPart[] = []; 213 216 214 217 if (reader) { 215 218 while (true) { ··· 273 276 return ( 274 277 <Show when={repo()}> 275 278 <div class="flex w-full flex-col gap-3 wrap-break-word"> 276 - <div class="dark:shadow-dark-700 dark:bg-dark-300 flex justify-between rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 text-sm shadow-xs dark:border-neutral-700"> 277 - <div class="ml-1 flex gap-2 text-xs sm:gap-4 sm:text-sm"> 279 + <div class="flex justify-between px-2 text-sm sm:text-base"> 280 + <div class="flex items-center gap-3 sm:gap-4"> 278 281 <Show when={!error()}> 279 282 <RepoTab tab="collections" label="Collections" /> 280 283 </Show> ··· 287 290 </Show> 288 291 <RepoTab tab="backlinks" label="Backlinks" /> 289 292 </div> 290 - <div class="flex gap-0.5"> 293 + <div class="flex gap-1"> 291 294 <Show when={error() && error() !== "Missing PDS"}> 292 295 <div class="flex items-center gap-1 text-red-500 dark:text-red-400"> 293 296 <span class="iconify lucide--alert-triangle"></span> 294 297 <span>{error()}</span> 295 298 </div> 296 299 </Show> 297 - <Show when={!error() && (!location.hash || location.hash.startsWith("#collections"))}> 298 - <Tooltip text="Filter collections"> 299 - <button 300 - 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" 301 - onClick={() => setShowFilter(!showFilter())} 300 + <MenuProvider> 301 + <DropdownMenu icon="lucide--ellipsis-vertical" buttonClass="rounded-sm p-1.5"> 302 + <Show 303 + when={!error() && (!location.hash || location.hash.startsWith("#collections"))} 302 304 > 303 - <span class="iconify lucide--filter"></span> 304 - </button> 305 - </Tooltip> 306 - </Show> 307 - <MenuProvider> 308 - <DropdownMenu 309 - icon="lucide--ellipsis-vertical" 310 - buttonClass="rounded-sm p-1.5" 311 - menuClass="top-9 p-2 text-sm" 312 - > 305 + <ActionMenu 306 + label="Filter collections" 307 + icon="lucide--filter" 308 + onClick={() => setShowFilter(!showFilter())} 309 + /> 310 + </Show> 313 311 <CopyMenu content={params.repo!} label="Copy DID" icon="lucide--copy" /> 314 312 <NavMenu 315 313 href={`/jetstream?dids=${params.repo}`} ··· 325 323 </Show> 326 324 <Show when={error()?.length === 0 || error() === undefined}> 327 325 <ActionMenu 328 - label="Export Repo" 326 + label="Export repo" 329 327 icon={downloading() ? "lucide--loader-circle animate-spin" : "lucide--download"} 330 328 onClick={() => downloadRepo()} 331 329 /> ··· 338 336 : `https://${did.split("did:web:")[1]}/.well-known/did.json` 339 337 } 340 338 newTab 341 - label="DID Document" 339 + label="DID document" 342 340 icon="lucide--external-link" 343 341 /> 344 342 <Show when={did.startsWith("did:plc")}> 345 343 <NavMenu 346 344 href={`${localStorage.plcDirectory ?? "https://plc.directory"}/${did}/log/audit`} 347 345 newTab 348 - label="Audit Log" 346 + label="Audit log" 349 347 icon="lucide--external-link" 350 348 /> 351 349 </Show> ··· 406 404 /> 407 405 </Show> 408 406 <div class="flex flex-col text-sm wrap-anywhere" classList={{ "-mt-1": !showFilter() }}> 409 - <For 410 - each={Object.keys(nsids() ?? {}).filter((authority) => 411 - filter() ? 412 - authority.includes(filter()!) || 413 - nsids()?.[authority].nsids.some((nsid) => 414 - `${authority}.${nsid}`.includes(filter()!), 415 - ) 416 - : true, 417 - )} 407 + <Show 408 + when={Object.keys(nsids() ?? {}).length != 0} 409 + fallback={<span class="mt-3 text-center text-base">No collections found.</span>} 418 410 > 419 - {(authority) => { 420 - const reversedDomain = authority.split(".").reverse().join("."); 421 - const [faviconLoaded, setFaviconLoaded] = createSignal(false); 411 + <For 412 + each={Object.keys(nsids() ?? {}).filter((authority) => 413 + filter() ? 414 + authority.includes(filter()!) || 415 + nsids()?.[authority].nsids.some((nsid) => 416 + `${authority}.${nsid}`.includes(filter()!), 417 + ) 418 + : true, 419 + )} 420 + > 421 + {(authority) => { 422 + const reversedDomain = authority.split(".").reverse().join("."); 423 + const [faviconLoaded, setFaviconLoaded] = createSignal(false); 422 424 423 - const isHighlighted = () => location.hash === `#collections:${authority}`; 425 + const isHighlighted = () => location.hash === `#collections:${authority}`; 424 426 425 - return ( 426 - <div 427 - id={`collection-${authority}`} 428 - class="group flex items-start gap-2 rounded-lg p-1 transition-colors" 429 - classList={{ 430 - "dark:hover:bg-dark-200 hover:bg-neutral-200": !isHighlighted(), 431 - "bg-blue-100 dark:bg-blue-500/25": isHighlighted(), 432 - }} 433 - > 434 - <a 435 - href={`#collections:${authority}`} 436 - class="relative flex h-5 w-4 shrink-0 items-center justify-center hover:opacity-70" 427 + return ( 428 + <div 429 + id={`collection-${authority}`} 430 + class="group flex items-start gap-2 rounded-lg p-1 transition-colors" 431 + classList={{ 432 + "dark:hover:bg-dark-200 hover:bg-neutral-200": !isHighlighted(), 433 + "bg-blue-100 dark:bg-blue-500/25": isHighlighted(), 434 + }} 437 435 > 438 - <span class="absolute top-1/2 -left-5 flex -translate-y-1/2 items-center text-base opacity-0 transition-opacity group-hover:opacity-100"> 439 - <span class="iconify lucide--link absolute -left-2 w-7"></span> 440 - </span> 441 - <Show when={!faviconLoaded()}> 442 - <span class="iconify lucide--globe size-4 text-neutral-400 dark:text-neutral-500" /> 443 - </Show> 444 - <img 445 - src={ 446 - ["bsky.app", "bsky.chat"].includes(reversedDomain) ? 447 - "https://web-cdn.bsky.app/static/apple-touch-icon.png" 448 - : `https://${reversedDomain}/favicon.ico` 449 - } 450 - alt={`${reversedDomain} favicon`} 451 - class="h-4 w-4" 452 - classList={{ hidden: !faviconLoaded() }} 453 - onLoad={() => setFaviconLoaded(true)} 454 - onError={() => setFaviconLoaded(false)} 455 - /> 456 - </a> 457 - <div class="flex flex-1 flex-col"> 458 - <For 459 - each={nsids()?.[authority].nsids.filter((nsid) => 460 - filter() ? `${authority}.${nsid}`.includes(filter()!) : true, 461 - )} 436 + <a 437 + href={`#collections:${authority}`} 438 + class="relative flex h-5 w-4 shrink-0 items-center justify-center hover:opacity-70" 462 439 > 463 - {(nsid) => ( 464 - <A 465 - href={`/at://${did}/${authority}.${nsid}`} 466 - class="hover:underline active:underline" 467 - > 468 - <span>{authority}</span> 469 - <span class="text-neutral-500 dark:text-neutral-400">.{nsid}</span> 470 - </A> 471 - )} 472 - </For> 440 + <span class="absolute top-1/2 -left-5 flex -translate-y-1/2 items-center text-base opacity-0 transition-opacity group-hover:opacity-100"> 441 + <span class="iconify lucide--link absolute -left-2 w-7"></span> 442 + </span> 443 + <Show when={!faviconLoaded()}> 444 + <span class="iconify lucide--globe size-4 text-neutral-400 dark:text-neutral-500" /> 445 + </Show> 446 + <img 447 + src={ 448 + ["bsky.app", "bsky.chat"].includes(reversedDomain) ? 449 + "https://web-cdn.bsky.app/static/apple-touch-icon.png" 450 + : `https://${reversedDomain}/favicon.ico` 451 + } 452 + alt={`${reversedDomain} favicon`} 453 + class="h-4 w-4" 454 + classList={{ hidden: !faviconLoaded() }} 455 + onLoad={() => setFaviconLoaded(true)} 456 + onError={() => setFaviconLoaded(false)} 457 + /> 458 + </a> 459 + <div class="flex flex-1 flex-col"> 460 + <For 461 + each={nsids()?.[authority].nsids.filter((nsid) => 462 + filter() ? `${authority}.${nsid}`.includes(filter()!) : true, 463 + )} 464 + > 465 + {(nsid) => ( 466 + <A 467 + href={`/at://${did}/${authority}.${nsid}`} 468 + class="hover:underline active:underline" 469 + > 470 + <span>{authority}</span> 471 + <span class="text-neutral-500 dark:text-neutral-400">.{nsid}</span> 472 + </A> 473 + )} 474 + </For> 475 + </div> 473 476 </div> 474 - </div> 475 - ); 476 - }} 477 - </For> 477 + ); 478 + }} 479 + </For> 480 + </Show> 478 481 </div> 479 482 </Show> 480 483 <Show when={location.hash === "#identity" || (error() && !location.hash)}> 481 484 <Show when={didDoc()}> 482 485 {(didDocument) => ( 483 - <div class="flex flex-col gap-2 wrap-anywhere"> 486 + <div class="flex flex-col gap-3 wrap-anywhere"> 484 487 {/* ID Section */} 485 488 <div> 486 - <div class="flex items-center gap-1"> 487 - <div class="iconify lucide--id-card" /> 488 - <p class="font-semibold">ID</p> 489 + <div class="font-semibold">DID</div> 490 + <div class="text-sm text-neutral-700 dark:text-neutral-300"> 491 + {didDocument().id} 489 492 </div> 490 - <div class="text-sm">{didDocument().id}</div> 491 493 </div> 492 494 493 495 {/* Aliases Section */} 494 496 <div> 495 - <div class="flex items-center gap-1"> 496 - <div class="iconify lucide--at-sign" /> 497 - <p class="font-semibold">Aliases</p> 498 - </div> 499 - <div class="flex flex-col gap-0.5"> 500 - <For each={didDocument().alsoKnownAs}> 501 - {(alias) => ( 502 - <div class="flex items-center gap-1 text-sm"> 503 - <span>{alias}</span> 504 - <Show when={alias.startsWith("at://")}> 505 - <Tooltip 506 - text={ 507 - validHandles[alias] === true ? "Valid handle" 508 - : validHandles[alias] === undefined ? 509 - "Validating" 510 - : "Invalid handle" 511 - } 512 - > 513 - <span 514 - classList={{ 515 - "iconify lucide--circle-check text-green-600 dark:text-green-400": 516 - validHandles[alias] === true, 517 - "iconify lucide--circle-x text-red-500 dark:text-red-400": 518 - validHandles[alias] === false, 519 - "iconify lucide--loader-circle animate-spin": 520 - validHandles[alias] === undefined, 521 - }} 522 - ></span> 523 - </Tooltip> 524 - </Show> 525 - </div> 526 - )} 527 - </For> 528 - </div> 497 + <p class="font-semibold">Aliases</p> 498 + <For each={didDocument().alsoKnownAs}> 499 + {(alias) => ( 500 + <div class="flex items-center gap-1 text-sm text-neutral-700 dark:text-neutral-300"> 501 + <span>{alias}</span> 502 + <Show when={alias.startsWith("at://")}> 503 + <Tooltip 504 + text={ 505 + validHandles[alias] === true ? "Valid handle" 506 + : validHandles[alias] === undefined ? 507 + "Validating" 508 + : "Invalid handle" 509 + } 510 + > 511 + <span 512 + classList={{ 513 + "iconify lucide--check text-green-600 dark:text-green-400": 514 + validHandles[alias] === true, 515 + "iconify lucide--x text-red-500 dark:text-red-400": 516 + validHandles[alias] === false, 517 + "iconify lucide--loader-circle animate-spin": 518 + validHandles[alias] === undefined, 519 + }} 520 + ></span> 521 + </Tooltip> 522 + </Show> 523 + </div> 524 + )} 525 + </For> 529 526 </div> 530 527 531 528 {/* Services Section */} 532 529 <div> 533 - <div class="flex items-center gap-1"> 534 - <div class="iconify lucide--hard-drive" /> 535 - <p class="font-semibold">Services</p> 536 - </div> 537 - <div class="flex flex-col gap-0.5"> 530 + <p class="font-semibold">Services</p> 531 + <div class="flex flex-col gap-1"> 538 532 <For each={didDocument().service}> 539 533 {(service) => ( 540 - <div class="text-sm"> 541 - <div class="font-medium text-neutral-700 dark:text-neutral-300"> 542 - #{service.id.split("#")[1]} 543 - </div> 534 + <div class="grid grid-cols-[auto_1fr] items-center gap-x-1 text-sm text-neutral-700 dark:text-neutral-300"> 535 + <span class="iconify lucide--hash"></span> 536 + <span>{service.id.split("#")[1]}</span> 537 + <span></span> 544 538 <a 545 - class="underline hover:text-blue-400" 539 + class="w-fit underline hover:text-blue-400" 546 540 href={service.serviceEndpoint.toString()} 547 541 target="_blank" 548 542 rel="noopener" ··· 557 551 558 552 {/* Verification Methods Section */} 559 553 <div> 560 - <div class="flex items-center gap-1"> 561 - <div class="iconify lucide--shield-check" /> 562 - <p class="font-semibold">Verification Methods</p> 563 - </div> 564 - <div class="flex flex-col gap-0.5"> 554 + <p class="font-semibold">Verification Methods</p> 555 + <div class="flex flex-col gap-1"> 565 556 <For each={didDocument().verificationMethod}> 566 557 {(verif) => ( 567 558 <Show when={verif.publicKeyMultibase}> 568 559 {(key) => ( 569 - <div class="text-sm"> 570 - <div class="flex items-baseline gap-1"> 571 - <span class="font-medium text-neutral-700 dark:text-neutral-300"> 572 - #{verif.id.split("#")[1]} 573 - </span> 574 - <span class="rounded bg-neutral-200 px-1 py-0.5 text-xs text-neutral-800 dark:bg-neutral-700 dark:text-neutral-300"> 575 - <ErrorBoundary fallback={<>unknown</>}> 576 - {parsePublicMultikey(key()).type} 577 - </ErrorBoundary> 578 - </span> 560 + <div class="grid grid-cols-[auto_1fr] items-center gap-x-1 text-sm text-neutral-700 dark:text-neutral-300"> 561 + <span class="iconify lucide--hash"></span> 562 + <div class="flex items-center gap-2"> 563 + <span>{verif.id.split("#")[1]}</span> 564 + <div class="flex items-center gap-1 text-neutral-500 dark:text-neutral-400"> 565 + <span class="iconify lucide--key-round"></span> 566 + <span>{detectKeyType(key())}</span> 567 + </div> 579 568 </div> 569 + <span></span> 580 570 <div class="font-mono break-all">{key()}</div> 581 571 </div> 582 572 )} ··· 589 579 {/* Rotation Keys Section */} 590 580 <Show when={rotationKeys().length > 0}> 591 581 <div> 592 - <div class="flex items-center gap-1"> 593 - <div class="iconify lucide--key-round" /> 594 - <p class="font-semibold">Rotation Keys</p> 595 - </div> 596 - <div class="flex flex-col gap-0.5"> 582 + <p class="font-semibold">Rotation Keys</p> 583 + <div class="flex flex-col gap-1"> 597 584 <For each={rotationKeys()}> 598 585 {(key) => ( 599 - <div class="text-sm"> 600 - <span class="rounded bg-neutral-200 px-1 py-0.5 text-xs text-neutral-800 dark:bg-neutral-700 dark:text-neutral-300"> 601 - {parseDidKey(key).type} 586 + <div class="grid grid-cols-[auto_1fr] items-center gap-x-1 text-sm text-neutral-700 dark:text-neutral-300"> 587 + <span class="iconify lucide--key-round text-neutral-500 dark:text-neutral-400"></span> 588 + <span class="text-neutral-500 dark:text-neutral-400"> 589 + {detectDidKeyType(key)} 602 590 </span> 591 + <span></span> 603 592 <div class="font-mono break-all">{key.replace("did:key:", "")}</div> 604 593 </div> 605 594 )}
+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
+8 -8
src/views/stream.tsx
··· 143 143 144 144 return ( 145 145 <div class="flex w-full flex-col items-center"> 146 - <div class="flex gap-2 text-sm"> 146 + <div class="mb-1 flex gap-4 font-medium"> 147 147 <A 148 - class="flex items-center gap-1 border-b-2 p-1" 149 - inactiveClass="border-transparent hover:border-neutral-400 dark:hover:border-neutral-600" 148 + class="flex items-center gap-1 border-b-2" 149 + inactiveClass="border-transparent text-neutral-600 dark:text-neutral-400 hover:border-neutral-400 dark:hover:border-neutral-600" 150 150 href="/jetstream" 151 151 > 152 152 Jetstream 153 153 </A> 154 154 <A 155 - class="flex items-center gap-1 border-b-2 p-1" 156 - inactiveClass="border-transparent hover:border-neutral-400 dark:hover:border-neutral-600" 155 + class="flex items-center gap-1 border-b-2" 156 + inactiveClass="border-transparent text-neutral-600 dark:text-neutral-400 hover:border-neutral-400 dark:hover:border-neutral-600" 157 157 href="/firehose" 158 158 > 159 159 Firehose 160 160 </A> 161 161 </div> 162 162 <StickyOverlay> 163 - <form ref={formRef} class="flex w-full flex-col gap-1 text-sm"> 163 + <form ref={formRef} class="flex w-full flex-col gap-1.5 text-sm"> 164 164 <Show when={!connected()}> 165 165 <label class="flex items-center justify-end gap-x-1"> 166 166 <span class="min-w-20">Instance</span> ··· 183 183 spellcheck={false} 184 184 placeholder="Comma-separated list of collections" 185 185 value={searchParams.collections ?? ""} 186 - class="dark:bg-dark-100 dark:inset-shadow-dark-200 grow rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 py-1 inset-shadow-xs focus:outline-[1px] focus:outline-neutral-600 dark:border-neutral-600 dark:focus:outline-neutral-400" 186 + class="dark:bg-dark-100 grow rounded-lg bg-white px-2 py-1 outline-1 outline-neutral-200 focus:outline-[1.5px] focus:outline-neutral-600 dark:outline-neutral-600 dark:focus:outline-neutral-400" 187 187 /> 188 188 </label> 189 189 </Show> ··· 195 195 spellcheck={false} 196 196 placeholder="Comma-separated list of DIDs" 197 197 value={searchParams.dids ?? ""} 198 - class="dark:bg-dark-100 dark:inset-shadow-dark-200 grow rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 py-1 inset-shadow-xs focus:outline-[1px] focus:outline-neutral-600 dark:border-neutral-600 dark:focus:outline-neutral-400" 198 + class="dark:bg-dark-100 grow rounded-lg bg-white px-2 py-1 outline-1 outline-neutral-200 focus:outline-[1.5px] focus:outline-neutral-600 dark:outline-neutral-600 dark:focus:outline-neutral-400" 199 199 /> 200 200 </label> 201 201 </Show>