Compare changes

Choose any two refs to compare.

+22
.tangled/workflows/deploy.yml
··· 1 + ## need this for commit idk what else to change 2 + 3 + when: 4 + - event: ["push"] 5 + branch: ["main"] 6 + 7 + engine: "nixery" 8 + 9 + clone: 10 + skip: true 11 + 12 + dependencies: 13 + nixpkgs: 14 + - curl 15 + 16 + steps: 17 + - name: "Trigger Deploy" 18 + command: | 19 + curl -X POST \ 20 + -H "Authorization: Bearer $SCANS_HOST_API_KEY" \ 21 + -H "Authorization: Bearer $SCANS_HOST_API_KEY" \ 22 + https://free.scan.blue/api/v1/sites/jy35AeguTwaqDy_3ufq09/deploy?wait=true
+25
0001-ok.patch
··· 1 + From baf405c82fb23f9274a35384286bac2b901d45af Mon Sep 17 00:00:00 2001 2 + From: scanash00 <scan@scanash.com> 3 + Date: Tue, 30 Dec 2025 22:12:13 -0900 4 + Subject: [PATCH] add ?wait=true 5 + 6 + --- 7 + .tangled/workflows/deploy.yml | 3 ++- 8 + 1 file changed, 2 insertions(+), 1 deletion(-) 9 + 10 + diff --git a/.tangled/workflows/deploy.yml b/.tangled/workflows/deploy.yml 11 + index a44c51b..b7edfea 100644 12 + --- a/.tangled/workflows/deploy.yml 13 + +++ b/.tangled/workflows/deploy.yml 14 + @@ -16,4 +16,5 @@ steps: 15 + command: | 16 + curl -X POST \ 17 + -H "Authorization: Bearer $SCANS_HOST_API_KEY" \ 18 + - https://free.scan.blue/api/v1/sites/jy35AeguTwaqDy_3ufq09/deploy 19 + \ No newline at end of file 20 + + -H "Authorization: Bearer $SCANS_HOST_API_KEY" \ 21 + + https://free.scan.blue/api/v1/sites/YOUR_SITE_ID/deploy?wait=true 22 + \ No newline at end of file 23 + -- 24 + 2.50.1 (Apple Git-155) 25 +
-2
index.html
··· 11 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" />
+19 -19
package.json
··· 9 9 "serve": "vite preview" 10 10 }, 11 11 "devDependencies": { 12 - "@iconify-json/lucide": "^1.2.77", 12 + "@iconify-json/lucide": "^1.2.82", 13 13 "@iconify/tailwind4": "^1.2.0", 14 - "@tailwindcss/vite": "^4.1.17", 14 + "@tailwindcss/vite": "^4.1.18", 15 15 "prettier": "^3.7.4", 16 16 "prettier-plugin-organize-imports": "^4.3.0", 17 17 "prettier-plugin-tailwindcss": "^0.7.2", 18 - "tailwindcss": "^4.1.17", 18 + "tailwindcss": "^4.1.18", 19 19 "typescript": "^5.9.3", 20 - "vite": "^7.2.6", 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.11", 26 - "@atcute/client": "^4.1.0", 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.4", 30 + "@atcute/identity-resolver": "^1.2.1", 31 + "@atcute/leaflet": "^1.0.14", 32 + "@atcute/lexicon-doc": "^2.0.6", 33 33 "@atcute/lexicon-resolver": "^0.1.5", 34 - "@atcute/lexicons": "^1.2.5", 34 + "@atcute/lexicons": "^1.2.6", 35 35 "@atcute/multibase": "^1.1.6", 36 - "@atcute/oauth-browser-client": "^2.0.1", 37 - "@atcute/repo": "^0.1.0", 38 - "@atcute/tangled": "^1.0.12", 39 - "@atcute/tid": "^1.0.3", 40 - "@codemirror/commands": "^6.10.0", 36 + "@atcute/oauth-browser-client": "^2.0.3", 37 + "@atcute/repo": "^0.1.1", 38 + "@atcute/tangled": "^1.0.13", 39 + "@atcute/tid": "^1.1.0", 40 + "@codemirror/commands": "^6.10.1", 41 41 "@codemirror/lang-json": "^6.0.2", 42 42 "@codemirror/lint": "^6.9.2", 43 - "@codemirror/state": "^6.5.2", 44 - "@codemirror/view": "^6.38.8", 43 + "@codemirror/state": "^6.5.3", 44 + "@codemirror/view": "^6.39.7", 45 45 "@fsegurai/codemirror-theme-basic-dark": "^6.2.3", 46 46 "@fsegurai/codemirror-theme-basic-light": "^6.2.3", 47 47 "@mary/exif-rm": "jsr:^0.2.2",
+535 -489
pnpm-lock.yaml
··· 12 12 specifier: ^3.1.9 13 13 version: 3.1.9 14 14 '@atcute/bluesky': 15 - specifier: ^3.2.11 16 - version: 3.2.11 15 + specifier: ^3.2.14 16 + version: 3.2.14 17 17 '@atcute/client': 18 - specifier: ^4.1.0 19 - version: 4.1.0 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.4 37 - version: 2.0.4 36 + specifier: ^2.0.6 37 + version: 2.0.6 38 38 '@atcute/lexicon-resolver': 39 39 specifier: ^0.1.5 40 - version: 0.1.5(@atcute/identity-resolver@1.1.4(@atcute/identity@1.1.3))(@atcute/identity@1.1.3) 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.5 43 - version: 1.2.5 42 + specifier: ^1.2.6 43 + version: 1.2.6 44 44 '@atcute/multibase': 45 45 specifier: ^1.1.6 46 46 version: 1.1.6 47 47 '@atcute/oauth-browser-client': 48 - specifier: ^2.0.1 49 - version: 2.0.1 48 + specifier: ^2.0.3 49 + version: 2.0.3(@atcute/identity@1.1.3) 50 50 '@atcute/repo': 51 - specifier: ^0.1.0 52 - version: 0.1.0 51 + specifier: ^0.1.1 52 + version: 0.1.1 53 53 '@atcute/tangled': 54 - specifier: ^1.0.12 55 - version: 1.0.12 54 + specifier: ^1.0.13 55 + version: 1.0.13 56 56 '@atcute/tid': 57 - specifier: ^1.0.3 58 - version: 1.0.3 57 + specifier: ^1.1.0 58 + version: 1.1.0 59 59 '@codemirror/commands': 60 - specifier: ^6.10.0 61 - version: 6.10.0 60 + specifier: ^6.10.1 61 + version: 6.10.1 62 62 '@codemirror/lang-json': 63 63 specifier: ^6.0.2 64 64 version: 6.0.2 ··· 66 66 specifier: ^6.9.2 67 67 version: 6.9.2 68 68 '@codemirror/state': 69 - specifier: ^6.5.2 70 - version: 6.5.2 69 + specifier: ^6.5.3 70 + version: 6.5.3 71 71 '@codemirror/view': 72 - specifier: ^6.38.8 73 - version: 6.38.8 72 + specifier: ^6.39.7 73 + version: 6.39.7 74 74 '@fsegurai/codemirror-theme-basic-dark': 75 75 specifier: ^6.2.3 76 - version: 6.2.3(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.8)(@lezer/highlight@1.2.3) 76 + version: 6.2.3(@codemirror/language@6.12.1)(@codemirror/state@6.5.3)(@codemirror/view@6.39.7)(@lezer/highlight@1.2.3) 77 77 '@fsegurai/codemirror-theme-basic-light': 78 78 specifier: ^6.2.3 79 - version: 6.2.3(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.8)(@lezer/highlight@1.2.3) 79 + version: 6.2.3(@codemirror/language@6.12.1)(@codemirror/state@6.5.3)(@codemirror/view@6.39.7)(@lezer/highlight@1.2.3) 80 80 '@mary/exif-rm': 81 81 specifier: jsr:^0.2.2 82 82 version: '@jsr/mary__exif-rm@0.2.2' ··· 97 97 version: 1.9.10 98 98 devDependencies: 99 99 '@iconify-json/lucide': 100 - specifier: ^1.2.77 101 - version: 1.2.77 100 + specifier: ^1.2.82 101 + version: 1.2.82 102 102 '@iconify/tailwind4': 103 103 specifier: ^1.2.0 104 - version: 1.2.0(tailwindcss@4.1.17) 104 + version: 1.2.0(tailwindcss@4.1.18) 105 105 '@tailwindcss/vite': 106 - specifier: ^4.1.17 107 - version: 4.1.17(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)) 106 + specifier: ^4.1.18 107 + version: 4.1.18(vite@7.3.0(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)) 108 108 prettier: 109 109 specifier: ^3.7.4 110 110 version: 3.7.4 ··· 115 115 specifier: ^0.7.2 116 116 version: 0.7.2(prettier-plugin-organize-imports@4.3.0(prettier@3.7.4)(typescript@5.9.3))(prettier@3.7.4) 117 117 tailwindcss: 118 - specifier: ^4.1.17 119 - version: 4.1.17 118 + specifier: ^4.1.18 119 + version: 4.1.18 120 120 typescript: 121 121 specifier: ^5.9.3 122 122 version: 5.9.3 123 123 vite: 124 - specifier: ^7.2.6 125 - version: 7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2) 124 + specifier: ^7.3.0 125 + version: 7.3.0(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2) 126 126 vite-plugin-solid: 127 127 specifier: ^2.11.10 128 - version: 2.11.10(solid-js@1.9.10)(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)) 128 + version: 2.11.10(solid-js@1.9.10)(vite@7.3.0(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)) 129 129 130 130 packages: 131 131 ··· 135 135 '@atcute/atproto@3.1.9': 136 136 resolution: {integrity: sha512-DyWwHCTdR4hY2BPNbLXgVmm7lI+fceOwWbE4LXbGvbvVtSn+ejSVFaAv01Ra3kWDha0whsOmbJL8JP0QPpf1+w==} 137 137 138 - '@atcute/bluesky@3.2.11': 139 - resolution: {integrity: sha512-AboS6y4t+zaxIq7E4noue10csSpIuk/Uwo30/l6GgGBDPXrd7STw8Yb5nGZQP+TdG/uC8/c2mm7UnY65SDOh6A==} 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.1.0': 154 - resolution: {integrity: sha512-AYhSu3RSDA2VDkVGOmad320NRbUUUf5pCFWJcOzlk25YC/4kyzmMFfpzhf1jjjEcY+anNBXGGhav/kKB1evggQ==} 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.4': 174 - resolution: {integrity: sha512-YfwlYFoYiBvRIYG0I1zsINCTFugFtS8l67uT3nQ04zdKVflzdg8uUj8cNZYRNY1V7okoOPdikhR4kPFhYGyemw==} 173 + '@atcute/lexicon-doc@2.0.6': 174 + resolution: {integrity: sha512-iDYJkuom+tIw3zIvU1ggCEVFfReXKfOUtIhpY2kEg2kQeSfMB75F+8k1QOpeAQBetyWYmjsHqBuSUX9oQS6L1Q==} 175 175 176 176 '@atcute/lexicon-resolver@0.1.5': 177 177 resolution: {integrity: sha512-0bx1/zdMQPuxvRcHW6ykAxRxktC2rEZLoAVSFoLSWDAA92Tf09F9QPK5wgXSF4MNODm1dvzMEdWSMIvlg8sr3A==} ··· 179 179 '@atcute/identity': ^1.1.0 180 180 '@atcute/identity-resolver': ^1.1.3 181 181 182 - '@atcute/lexicons@1.2.5': 183 - resolution: {integrity: sha512-9yO9WdgxW8jZ7SbzUycH710z+JmsQ9W9n5S6i6eghYju32kkluFmgBeS47r8e8p2+Dv4DemS7o/3SUGsX9FR5Q==} 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 + 206 + '@atcute/uint8array@1.0.6': 207 + resolution: {integrity: sha512-ucfRBQc7BFT8n9eCyGOzDHEMKF/nZwhS2pPao4Xtab1ML3HdFYcX2DM1tadCzas85QTGxHe5urnUAAcNKGRi9A==} 205 208 206 209 '@atcute/util-fetch@1.0.4': 207 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==} 317 323 318 - '@codemirror/view@6.38.8': 319 - resolution: {integrity: sha512-XcE9fcnkHCbWkjeKyi0lllwXmBLtyYb5dt89dJyx23I9+LSh5vZDIuk7OLG4VM1lgrXZQcY6cxyZyk5WVPRv/A==} 324 + '@codemirror/view@6.39.7': 325 + resolution: {integrity: sha512-3Vif9hnNHJnl2YgOtkR/wzGzhYcQ8gy3LGdUhkLUU8xSBbgsTxrE8he/CMTpeINm5TgxLe2FmzvF6IYQL/BSAg==} 320 326 321 327 '@cyberalien/svg-utils@1.0.11': 322 328 resolution: {integrity: sha512-qEE9mnyI+avfGT3emKuRs3ucYkITeaV0Xi7VlYN41f+uGnZBecQP3jwz/AF437H9J4Q7qPClHKm4NiTYpNE6hA==} ··· 327 333 cpu: [ppc64] 328 334 os: [aix] 329 335 330 - '@esbuild/aix-ppc64@0.25.12': 331 - resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} 336 + '@esbuild/aix-ppc64@0.27.2': 337 + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} 332 338 engines: {node: '>=18'} 333 339 cpu: [ppc64] 334 340 os: [aix] ··· 339 345 cpu: [arm64] 340 346 os: [android] 341 347 342 - '@esbuild/android-arm64@0.25.12': 343 - resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} 348 + '@esbuild/android-arm64@0.27.2': 349 + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} 344 350 engines: {node: '>=18'} 345 351 cpu: [arm64] 346 352 os: [android] ··· 351 357 cpu: [arm] 352 358 os: [android] 353 359 354 - '@esbuild/android-arm@0.25.12': 355 - resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} 360 + '@esbuild/android-arm@0.27.2': 361 + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} 356 362 engines: {node: '>=18'} 357 363 cpu: [arm] 358 364 os: [android] ··· 363 369 cpu: [x64] 364 370 os: [android] 365 371 366 - '@esbuild/android-x64@0.25.12': 367 - resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} 372 + '@esbuild/android-x64@0.27.2': 373 + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} 368 374 engines: {node: '>=18'} 369 375 cpu: [x64] 370 376 os: [android] ··· 375 381 cpu: [arm64] 376 382 os: [darwin] 377 383 378 - '@esbuild/darwin-arm64@0.25.12': 379 - resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} 384 + '@esbuild/darwin-arm64@0.27.2': 385 + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} 380 386 engines: {node: '>=18'} 381 387 cpu: [arm64] 382 388 os: [darwin] ··· 387 393 cpu: [x64] 388 394 os: [darwin] 389 395 390 - '@esbuild/darwin-x64@0.25.12': 391 - resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} 396 + '@esbuild/darwin-x64@0.27.2': 397 + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} 392 398 engines: {node: '>=18'} 393 399 cpu: [x64] 394 400 os: [darwin] ··· 399 405 cpu: [arm64] 400 406 os: [freebsd] 401 407 402 - '@esbuild/freebsd-arm64@0.25.12': 403 - resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} 408 + '@esbuild/freebsd-arm64@0.27.2': 409 + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} 404 410 engines: {node: '>=18'} 405 411 cpu: [arm64] 406 412 os: [freebsd] ··· 411 417 cpu: [x64] 412 418 os: [freebsd] 413 419 414 - '@esbuild/freebsd-x64@0.25.12': 415 - resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} 420 + '@esbuild/freebsd-x64@0.27.2': 421 + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} 416 422 engines: {node: '>=18'} 417 423 cpu: [x64] 418 424 os: [freebsd] ··· 423 429 cpu: [arm64] 424 430 os: [linux] 425 431 426 - '@esbuild/linux-arm64@0.25.12': 427 - resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} 432 + '@esbuild/linux-arm64@0.27.2': 433 + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} 428 434 engines: {node: '>=18'} 429 435 cpu: [arm64] 430 436 os: [linux] ··· 435 441 cpu: [arm] 436 442 os: [linux] 437 443 438 - '@esbuild/linux-arm@0.25.12': 439 - resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} 444 + '@esbuild/linux-arm@0.27.2': 445 + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} 440 446 engines: {node: '>=18'} 441 447 cpu: [arm] 442 448 os: [linux] ··· 447 453 cpu: [ia32] 448 454 os: [linux] 449 455 450 - '@esbuild/linux-ia32@0.25.12': 451 - 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==} 452 458 engines: {node: '>=18'} 453 459 cpu: [ia32] 454 460 os: [linux] ··· 459 465 cpu: [loong64] 460 466 os: [linux] 461 467 462 - '@esbuild/linux-loong64@0.25.12': 463 - resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} 468 + '@esbuild/linux-loong64@0.27.2': 469 + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} 464 470 engines: {node: '>=18'} 465 471 cpu: [loong64] 466 472 os: [linux] ··· 471 477 cpu: [mips64el] 472 478 os: [linux] 473 479 474 - '@esbuild/linux-mips64el@0.25.12': 475 - resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} 480 + '@esbuild/linux-mips64el@0.27.2': 481 + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} 476 482 engines: {node: '>=18'} 477 483 cpu: [mips64el] 478 484 os: [linux] ··· 483 489 cpu: [ppc64] 484 490 os: [linux] 485 491 486 - '@esbuild/linux-ppc64@0.25.12': 487 - resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} 492 + '@esbuild/linux-ppc64@0.27.2': 493 + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} 488 494 engines: {node: '>=18'} 489 495 cpu: [ppc64] 490 496 os: [linux] ··· 495 501 cpu: [riscv64] 496 502 os: [linux] 497 503 498 - '@esbuild/linux-riscv64@0.25.12': 499 - resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} 504 + '@esbuild/linux-riscv64@0.27.2': 505 + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} 500 506 engines: {node: '>=18'} 501 507 cpu: [riscv64] 502 508 os: [linux] ··· 507 513 cpu: [s390x] 508 514 os: [linux] 509 515 510 - '@esbuild/linux-s390x@0.25.12': 511 - resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} 516 + '@esbuild/linux-s390x@0.27.2': 517 + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} 512 518 engines: {node: '>=18'} 513 519 cpu: [s390x] 514 520 os: [linux] ··· 519 525 cpu: [x64] 520 526 os: [linux] 521 527 522 - '@esbuild/linux-x64@0.25.12': 523 - resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} 528 + '@esbuild/linux-x64@0.27.2': 529 + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} 524 530 engines: {node: '>=18'} 525 531 cpu: [x64] 526 532 os: [linux] 527 533 528 - '@esbuild/netbsd-arm64@0.25.12': 529 - resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} 534 + '@esbuild/netbsd-arm64@0.27.2': 535 + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} 530 536 engines: {node: '>=18'} 531 537 cpu: [arm64] 532 538 os: [netbsd] ··· 537 543 cpu: [x64] 538 544 os: [netbsd] 539 545 540 - '@esbuild/netbsd-x64@0.25.12': 541 - resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} 546 + '@esbuild/netbsd-x64@0.27.2': 547 + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} 542 548 engines: {node: '>=18'} 543 549 cpu: [x64] 544 550 os: [netbsd] ··· 549 555 cpu: [arm64] 550 556 os: [openbsd] 551 557 552 - '@esbuild/openbsd-arm64@0.25.12': 553 - resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} 558 + '@esbuild/openbsd-arm64@0.27.2': 559 + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} 554 560 engines: {node: '>=18'} 555 561 cpu: [arm64] 556 562 os: [openbsd] ··· 561 567 cpu: [x64] 562 568 os: [openbsd] 563 569 564 - '@esbuild/openbsd-x64@0.25.12': 565 - 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==} 566 572 engines: {node: '>=18'} 567 573 cpu: [x64] 568 574 os: [openbsd] 569 575 570 - '@esbuild/openharmony-arm64@0.25.12': 571 - resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} 576 + '@esbuild/openharmony-arm64@0.27.2': 577 + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} 572 578 engines: {node: '>=18'} 573 579 cpu: [arm64] 574 580 os: [openharmony] ··· 579 585 cpu: [x64] 580 586 os: [sunos] 581 587 582 - '@esbuild/sunos-x64@0.25.12': 583 - resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} 588 + '@esbuild/sunos-x64@0.27.2': 589 + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} 584 590 engines: {node: '>=18'} 585 591 cpu: [x64] 586 592 os: [sunos] ··· 591 597 cpu: [arm64] 592 598 os: [win32] 593 599 594 - '@esbuild/win32-arm64@0.25.12': 595 - resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} 600 + '@esbuild/win32-arm64@0.27.2': 601 + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} 596 602 engines: {node: '>=18'} 597 603 cpu: [arm64] 598 604 os: [win32] ··· 603 609 cpu: [ia32] 604 610 os: [win32] 605 611 606 - '@esbuild/win32-ia32@0.25.12': 607 - resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} 612 + '@esbuild/win32-ia32@0.27.2': 613 + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} 608 614 engines: {node: '>=18'} 609 615 cpu: [ia32] 610 616 os: [win32] ··· 615 621 cpu: [x64] 616 622 os: [win32] 617 623 618 - '@esbuild/win32-x64@0.25.12': 619 - resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} 624 + '@esbuild/win32-x64@0.27.2': 625 + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} 620 626 engines: {node: '>=18'} 621 627 cpu: [x64] 622 628 os: [win32] ··· 637 643 '@codemirror/view': ^6.0.0 638 644 '@lezer/highlight': ^1.0.0 639 645 640 - '@iconify-json/lucide@1.2.77': 641 - resolution: {integrity: sha512-FF3Z+np6Ksb0MaoQymhCHZ4xs5Oo8992Fw7By7bCgVCbBCClYV3wxpF8KzsI1FlxHD4ZXR42NVmXuqdW8YQGgA==} 646 + '@iconify-json/lucide@1.2.82': 647 + resolution: {integrity: sha512-fHZWegspOZonl5GNTvOkHsjnTMdSslFh3EzpzUtRyLxO8bOonqk2OTU3hCl0k4VXzViMjqpRK3X1sotnuBXkFA==} 642 648 643 649 '@iconify/tailwind4@1.2.0': 644 650 resolution: {integrity: sha512-+t7XqfojOB0zzZdd8gV7IQZGq1AaIHTlsxMVzagxYR0hAlJCLUD63o3iSlNKRMH3ZR7gZ8y5c9dJ7J431avRbA==} 645 651 peerDependencies: 646 652 tailwindcss: '>= 4.0.0' 647 653 648 - '@iconify/tools@5.0.0': 649 - resolution: {integrity: sha512-GY/FsuNdWA/FbkLqgQ8b1PHFkNvjMeSFWaVJdLldYGHBp0lZ64HJlcS0qzLfglacHTd8zYdfQjF74RxGqyGMgw==} 654 + '@iconify/tools@5.0.1': 655 + resolution: {integrity: sha512-/znhBN9WIpJd9UtKhyEDfRKwNo8rrOy8dShF8bwSZ1i27ukTSHjeS6bmVK4tTYBYriwFhBf70JT6g8GIRwFvbw==} 650 656 651 657 '@iconify/types@2.0.0': 652 658 resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} ··· 673 679 '@jsr/mary__exif-rm@0.2.2': 674 680 resolution: {integrity: sha512-+ZpLaC+1CyqWhH608Sqd6/yTG0pOlokn2tCXha7s1SMQ+GLKo4Nn/PskTeeP9Pt+6gNYSu6ednoSlRvXb2ZGxg==, tarball: https://npm.jsr.io/~/11/@jsr/mary__exif-rm/0.2.2.tgz} 675 681 676 - '@lezer/common@1.4.0': 677 - resolution: {integrity: sha512-DVeMRoGrgn/k45oQNu189BoW4SZwgZFzJ1+1TV5j2NJ/KFC83oa/enRqZSGshyeMk5cPWMhsKs9nx+8o0unwGg==} 682 + '@lezer/common@1.5.0': 683 + resolution: {integrity: sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA==} 678 684 679 685 '@lezer/highlight@1.2.3': 680 686 resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} ··· 682 688 '@lezer/json@1.0.3': 683 689 resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==} 684 690 685 - '@lezer/lr@1.4.4': 686 - resolution: {integrity: sha512-LHL17Mq0OcFXm1pGQssuGTQFPPdxARjKM8f7GA5+sGtHi0K3R84YaSbmche0+RKWHnCsx9asEe5OWOI4FHfe4A==} 691 + '@lezer/lr@1.4.5': 692 + resolution: {integrity: sha512-/YTRKP5yPPSo1xImYQk7AZZMAgap0kegzqCSYHjAL9x1AZ0ZQW+IpcEzMKagCsbTsLnVeWkxYrCNeXG8xEPrjg==} 687 693 688 694 '@marijn/find-cluster-break@1.0.2': 689 695 resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} ··· 691 697 '@noble/secp256k1@3.0.0': 692 698 resolution: {integrity: sha512-NJBaR352KyIvj3t6sgT/+7xrNyF9Xk9QlLSIqUGVUYlsnDTAUqY8LOmwpcgEx4AMJXRITQ5XEVHD+mMaPfr3mg==} 693 699 694 - '@rollup/rollup-android-arm-eabi@4.53.3': 695 - resolution: {integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==} 700 + '@rollup/rollup-android-arm-eabi@4.54.0': 701 + resolution: {integrity: sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==} 696 702 cpu: [arm] 697 703 os: [android] 698 704 699 - '@rollup/rollup-android-arm64@4.53.3': 700 - resolution: {integrity: sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==} 705 + '@rollup/rollup-android-arm64@4.54.0': 706 + resolution: {integrity: sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==} 701 707 cpu: [arm64] 702 708 os: [android] 703 709 704 - '@rollup/rollup-darwin-arm64@4.53.3': 705 - resolution: {integrity: sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==} 710 + '@rollup/rollup-darwin-arm64@4.54.0': 711 + resolution: {integrity: sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==} 706 712 cpu: [arm64] 707 713 os: [darwin] 708 714 709 - '@rollup/rollup-darwin-x64@4.53.3': 710 - resolution: {integrity: sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==} 715 + '@rollup/rollup-darwin-x64@4.54.0': 716 + resolution: {integrity: sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==} 711 717 cpu: [x64] 712 718 os: [darwin] 713 719 714 - '@rollup/rollup-freebsd-arm64@4.53.3': 715 - resolution: {integrity: sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==} 720 + '@rollup/rollup-freebsd-arm64@4.54.0': 721 + resolution: {integrity: sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==} 716 722 cpu: [arm64] 717 723 os: [freebsd] 718 724 719 - '@rollup/rollup-freebsd-x64@4.53.3': 720 - resolution: {integrity: sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==} 725 + '@rollup/rollup-freebsd-x64@4.54.0': 726 + resolution: {integrity: sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==} 721 727 cpu: [x64] 722 728 os: [freebsd] 723 729 724 - '@rollup/rollup-linux-arm-gnueabihf@4.53.3': 725 - resolution: {integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==} 730 + '@rollup/rollup-linux-arm-gnueabihf@4.54.0': 731 + resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==} 726 732 cpu: [arm] 727 733 os: [linux] 728 734 729 - '@rollup/rollup-linux-arm-musleabihf@4.53.3': 730 - 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==} 731 737 cpu: [arm] 732 738 os: [linux] 733 739 734 - '@rollup/rollup-linux-arm64-gnu@4.53.3': 735 - 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==} 736 742 cpu: [arm64] 737 743 os: [linux] 738 744 739 - '@rollup/rollup-linux-arm64-musl@4.53.3': 740 - resolution: {integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==} 745 + '@rollup/rollup-linux-arm64-musl@4.54.0': 746 + resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==} 741 747 cpu: [arm64] 742 748 os: [linux] 743 749 744 - '@rollup/rollup-linux-loong64-gnu@4.53.3': 745 - resolution: {integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==} 750 + '@rollup/rollup-linux-loong64-gnu@4.54.0': 751 + resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==} 746 752 cpu: [loong64] 747 753 os: [linux] 748 754 749 - '@rollup/rollup-linux-ppc64-gnu@4.53.3': 750 - resolution: {integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==} 755 + '@rollup/rollup-linux-ppc64-gnu@4.54.0': 756 + resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==} 751 757 cpu: [ppc64] 752 758 os: [linux] 753 759 754 - '@rollup/rollup-linux-riscv64-gnu@4.53.3': 755 - 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==} 756 762 cpu: [riscv64] 757 763 os: [linux] 758 764 759 - '@rollup/rollup-linux-riscv64-musl@4.53.3': 760 - resolution: {integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==} 765 + '@rollup/rollup-linux-riscv64-musl@4.54.0': 766 + resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==} 761 767 cpu: [riscv64] 762 768 os: [linux] 763 769 764 - '@rollup/rollup-linux-s390x-gnu@4.53.3': 765 - resolution: {integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==} 770 + '@rollup/rollup-linux-s390x-gnu@4.54.0': 771 + resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==} 766 772 cpu: [s390x] 767 773 os: [linux] 768 774 769 - '@rollup/rollup-linux-x64-gnu@4.53.3': 770 - 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==} 771 777 cpu: [x64] 772 778 os: [linux] 773 779 774 - '@rollup/rollup-linux-x64-musl@4.53.3': 775 - 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==} 776 782 cpu: [x64] 777 783 os: [linux] 778 784 779 - '@rollup/rollup-openharmony-arm64@4.53.3': 780 - 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==} 781 787 cpu: [arm64] 782 788 os: [openharmony] 783 789 784 - '@rollup/rollup-win32-arm64-msvc@4.53.3': 785 - 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==} 786 792 cpu: [arm64] 787 793 os: [win32] 788 794 789 - '@rollup/rollup-win32-ia32-msvc@4.53.3': 790 - resolution: {integrity: sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==} 795 + '@rollup/rollup-win32-ia32-msvc@4.54.0': 796 + resolution: {integrity: sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==} 791 797 cpu: [ia32] 792 798 os: [win32] 793 799 794 - '@rollup/rollup-win32-x64-gnu@4.53.3': 795 - resolution: {integrity: sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==} 800 + '@rollup/rollup-win32-x64-gnu@4.54.0': 801 + resolution: {integrity: sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==} 796 802 cpu: [x64] 797 803 os: [win32] 798 804 799 - '@rollup/rollup-win32-x64-msvc@4.53.3': 800 - resolution: {integrity: sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==} 805 + '@rollup/rollup-win32-x64-msvc@4.54.0': 806 + resolution: {integrity: sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==} 801 807 cpu: [x64] 802 808 os: [win32] 803 809 ··· 814 820 peerDependencies: 815 821 solid-js: ^1.8.6 816 822 817 - '@standard-schema/spec@1.0.0': 818 - resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} 823 + '@standard-schema/spec@1.1.0': 824 + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} 819 825 820 - '@tailwindcss/node@4.1.17': 821 - resolution: {integrity: sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==} 826 + '@tailwindcss/node@4.1.18': 827 + resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} 822 828 823 - '@tailwindcss/oxide-android-arm64@4.1.17': 824 - 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==} 825 831 engines: {node: '>= 10'} 826 832 cpu: [arm64] 827 833 os: [android] 828 834 829 - '@tailwindcss/oxide-darwin-arm64@4.1.17': 830 - 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==} 831 837 engines: {node: '>= 10'} 832 838 cpu: [arm64] 833 839 os: [darwin] 834 840 835 - '@tailwindcss/oxide-darwin-x64@4.1.17': 836 - 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==} 837 843 engines: {node: '>= 10'} 838 844 cpu: [x64] 839 845 os: [darwin] 840 846 841 - '@tailwindcss/oxide-freebsd-x64@4.1.17': 842 - resolution: {integrity: sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==} 847 + '@tailwindcss/oxide-freebsd-x64@4.1.18': 848 + resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==} 843 849 engines: {node: '>= 10'} 844 850 cpu: [x64] 845 851 os: [freebsd] 846 852 847 - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17': 848 - 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==} 849 855 engines: {node: '>= 10'} 850 856 cpu: [arm] 851 857 os: [linux] 852 858 853 - '@tailwindcss/oxide-linux-arm64-gnu@4.1.17': 854 - 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==} 855 861 engines: {node: '>= 10'} 856 862 cpu: [arm64] 857 863 os: [linux] 858 864 859 - '@tailwindcss/oxide-linux-arm64-musl@4.1.17': 860 - resolution: {integrity: sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==} 865 + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': 866 + resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} 861 867 engines: {node: '>= 10'} 862 868 cpu: [arm64] 863 869 os: [linux] 864 870 865 - '@tailwindcss/oxide-linux-x64-gnu@4.1.17': 866 - resolution: {integrity: sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==} 871 + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': 872 + resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} 867 873 engines: {node: '>= 10'} 868 874 cpu: [x64] 869 875 os: [linux] 870 876 871 - '@tailwindcss/oxide-linux-x64-musl@4.1.17': 872 - resolution: {integrity: sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==} 877 + '@tailwindcss/oxide-linux-x64-musl@4.1.18': 878 + resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} 873 879 engines: {node: '>= 10'} 874 880 cpu: [x64] 875 881 os: [linux] 876 882 877 - '@tailwindcss/oxide-wasm32-wasi@4.1.17': 878 - resolution: {integrity: sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==} 883 + '@tailwindcss/oxide-wasm32-wasi@4.1.18': 884 + resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} 879 885 engines: {node: '>=14.0.0'} 880 886 cpu: [wasm32] 881 887 bundledDependencies: ··· 886 892 - '@emnapi/wasi-threads' 887 893 - tslib 888 894 889 - '@tailwindcss/oxide-win32-arm64-msvc@4.1.17': 890 - resolution: {integrity: sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==} 895 + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': 896 + resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==} 891 897 engines: {node: '>= 10'} 892 898 cpu: [arm64] 893 899 os: [win32] 894 900 895 - '@tailwindcss/oxide-win32-x64-msvc@4.1.17': 896 - resolution: {integrity: sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==} 901 + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': 902 + resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==} 897 903 engines: {node: '>= 10'} 898 904 cpu: [x64] 899 905 os: [win32] 900 906 901 - '@tailwindcss/oxide@4.1.17': 902 - resolution: {integrity: sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==} 907 + '@tailwindcss/oxide@4.1.18': 908 + resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==} 903 909 engines: {node: '>= 10'} 904 910 905 - '@tailwindcss/vite@4.1.17': 906 - resolution: {integrity: sha512-4+9w8ZHOiGnpcGI6z1TVVfWaX/koK7fKeSYF3qlYg2xpBtbteP2ddBxiarL+HVgfSJGeK5RIxRQmKm4rTJJAwA==} 911 + '@tailwindcss/vite@4.1.18': 912 + resolution: {integrity: sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==} 907 913 peerDependencies: 908 914 vite: ^5.2.0 || ^6 || ^7 909 915 ··· 922 928 '@types/estree@1.0.8': 923 929 resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} 924 930 931 + '@types/node@22.19.3': 932 + resolution: {integrity: sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==} 933 + 925 934 '@types/node@24.10.1': 926 935 resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} 927 936 ··· 944 953 solid-js: 945 954 optional: true 946 955 947 - baseline-browser-mapping@2.9.0: 948 - resolution: {integrity: sha512-Mh++g+2LPfzZToywfE1BUzvZbfOY52Nil0rn9H1CPC5DJ7fX+Vir7nToBeoiSbB1zTNeGYbELEvJESujgGrzXw==} 956 + baseline-browser-mapping@2.9.11: 957 + resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==} 949 958 hasBin: true 950 959 951 960 boolbase@1.0.0: ··· 956 965 engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} 957 966 hasBin: true 958 967 959 - caniuse-lite@1.0.30001759: 960 - resolution: {integrity: sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==} 968 + caniuse-lite@1.0.30001761: 969 + resolution: {integrity: sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==} 961 970 962 971 codemirror@6.0.2: 963 972 resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} ··· 1023 1032 domutils@3.2.2: 1024 1033 resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} 1025 1034 1026 - electron-to-chromium@1.5.263: 1027 - resolution: {integrity: sha512-DrqJ11Knd+lo+dv+lltvfMDLU27g14LMdH2b0O3Pio4uk0x+z7OR+JrmyacTPN2M8w3BrZ7/RTwG3R9B7irPlg==} 1035 + electron-to-chromium@1.5.267: 1036 + resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} 1028 1037 1029 - enhanced-resolve@5.18.3: 1030 - resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} 1038 + enhanced-resolve@5.18.4: 1039 + resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} 1031 1040 engines: {node: '>=10.13.0'} 1032 1041 1033 1042 entities@4.5.0: ··· 1043 1052 engines: {node: '>=18'} 1044 1053 hasBin: true 1045 1054 1046 - esbuild@0.25.12: 1047 - resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} 1055 + esbuild@0.27.2: 1056 + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} 1048 1057 engines: {node: '>=18'} 1049 1058 hasBin: true 1050 1059 ··· 1195 1204 mlly@1.8.0: 1196 1205 resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} 1197 1206 1198 - modern-tar@0.7.2: 1199 - resolution: {integrity: sha512-TGG1ZRk1TAQ3neuZwahAHke3rKsSlro+ooMYtjh9sl2gGPVMLMuWiHgwC7im9T5bSM566RSo2Dko56ETgEvZcA==} 1207 + modern-tar@0.7.3: 1208 + resolution: {integrity: sha512-4W79zekKGyYU4JXVmB78DOscMFaJth2gGhgfTl2alWE4rNe3nf4N2pqenQ0rEtIewrnD79M687Ouba3YGTLOvg==} 1200 1209 engines: {node: '>=18.0.0'} 1201 1210 1202 1211 ms@2.1.3: ··· 1214 1223 nanoid@5.1.6: 1215 1224 resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==} 1216 1225 engines: {node: ^18 || >=20} 1226 + hasBin: true 1227 + 1228 + node-gyp-build@4.8.4: 1229 + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} 1217 1230 hasBin: true 1218 1231 1219 1232 node-releases@2.0.27: ··· 1318 1331 resolve-pkg-maps@1.0.0: 1319 1332 resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} 1320 1333 1321 - rollup@4.53.3: 1322 - resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==} 1334 + rollup@4.54.0: 1335 + resolution: {integrity: sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==} 1323 1336 engines: {node: '>=18.0.0', npm: '>=8.0.0'} 1324 1337 hasBin: true 1325 1338 ··· 1360 1373 engines: {node: '>=16'} 1361 1374 hasBin: true 1362 1375 1363 - tailwindcss@4.1.17: 1364 - resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==} 1376 + tailwindcss@4.1.18: 1377 + resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} 1365 1378 1366 1379 tapable@2.3.0: 1367 1380 resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} ··· 1388 1401 ufo@1.6.1: 1389 1402 resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} 1390 1403 1404 + undici-types@6.21.0: 1405 + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} 1406 + 1391 1407 undici-types@7.16.0: 1392 1408 resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} 1393 1409 1394 - update-browserslist-db@1.2.1: 1395 - resolution: {integrity: sha512-R9NcHbbZ45RoWfTdhn1J9SS7zxNvlddv4YRrHTUaFdtjbmfncfedB45EC9IaqJQ97iAR1GZgOfyRQO+ExIF6EQ==} 1410 + unicode-segmenter@0.14.4: 1411 + resolution: {integrity: sha512-pR5VCiCrLrKOL6FRW61jnk9+wyMtKKowq+jyFY9oc6uHbWKhDL4yVRiI4YZPksGMK72Pahh8m0cn/0JvbDDyJg==} 1412 + 1413 + update-browserslist-db@1.2.3: 1414 + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} 1396 1415 hasBin: true 1397 1416 peerDependencies: 1398 1417 browserslist: '>= 4.21.0' ··· 1407 1426 '@testing-library/jest-dom': 1408 1427 optional: true 1409 1428 1410 - vite@7.2.6: 1411 - resolution: {integrity: sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==} 1429 + vite@7.3.0: 1430 + resolution: {integrity: sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==} 1412 1431 engines: {node: ^20.19.0 || >=22.12.0} 1413 1432 hasBin: true 1414 1433 peerDependencies: ··· 1474 1493 1475 1494 '@atcute/atproto@3.1.9': 1476 1495 dependencies: 1477 - '@atcute/lexicons': 1.2.5 1496 + '@atcute/lexicons': 1.2.6 1478 1497 1479 - '@atcute/bluesky@3.2.11': 1498 + '@atcute/bluesky@3.2.14': 1480 1499 dependencies: 1481 1500 '@atcute/atproto': 3.1.9 1482 - '@atcute/lexicons': 1.2.5 1501 + '@atcute/lexicons': 1.2.6 1483 1502 1484 1503 '@atcute/car@3.1.3': 1485 1504 dependencies: 1486 1505 '@atcute/cbor': 2.2.8 1487 - '@atcute/cid': 2.2.6 1488 - '@atcute/uint8array': 1.0.5 1506 + '@atcute/cid': 2.3.0 1507 + '@atcute/uint8array': 1.0.6 1489 1508 '@atcute/varint': 1.0.3 1490 1509 yocto-queue: 1.2.2 1491 1510 1492 1511 '@atcute/car@5.0.0': 1493 1512 dependencies: 1494 1513 '@atcute/cbor': 2.2.8 1495 - '@atcute/cid': 2.2.6 1496 - '@atcute/uint8array': 1.0.5 1514 + '@atcute/cid': 2.3.0 1515 + '@atcute/uint8array': 1.0.6 1497 1516 '@atcute/varint': 1.0.3 1498 1517 1499 1518 '@atcute/cbor@2.2.8': 1500 1519 dependencies: 1501 - '@atcute/cid': 2.2.6 1520 + '@atcute/cid': 2.3.0 1502 1521 '@atcute/multibase': 1.1.6 1503 - '@atcute/uint8array': 1.0.5 1522 + '@atcute/uint8array': 1.0.6 1504 1523 1505 - '@atcute/cid@2.2.6': 1524 + '@atcute/cid@2.3.0': 1506 1525 dependencies: 1507 1526 '@atcute/multibase': 1.1.6 1508 - '@atcute/uint8array': 1.0.5 1527 + '@atcute/uint8array': 1.0.6 1509 1528 1510 - '@atcute/client@4.1.0': 1529 + '@atcute/client@4.1.2': 1511 1530 dependencies: 1512 1531 '@atcute/identity': 1.1.3 1513 - '@atcute/lexicons': 1.2.5 1532 + '@atcute/lexicons': 1.2.6 1514 1533 1515 - '@atcute/crypto@2.2.6': 1534 + '@atcute/crypto@2.3.0': 1516 1535 dependencies: 1517 1536 '@atcute/multibase': 1.1.6 1518 - '@atcute/uint8array': 1.0.5 1537 + '@atcute/uint8array': 1.0.6 1519 1538 '@noble/secp256k1': 3.0.0 1520 1539 1521 - '@atcute/did-plc@0.2.0': 1540 + '@atcute/did-plc@0.3.1': 1522 1541 dependencies: 1523 1542 '@atcute/cbor': 2.2.8 1524 - '@atcute/cid': 2.2.6 1525 - '@atcute/crypto': 2.2.6 1543 + '@atcute/cid': 2.3.0 1544 + '@atcute/crypto': 2.3.0 1526 1545 '@atcute/identity': 1.1.3 1527 - '@atcute/lexicons': 1.2.5 1546 + '@atcute/lexicons': 1.2.6 1528 1547 '@atcute/multibase': 1.1.6 1529 - '@atcute/uint8array': 1.0.5 1548 + '@atcute/uint8array': 1.0.6 1549 + '@atcute/util-fetch': 1.0.4 1530 1550 '@badrap/valita': 0.4.6 1531 1551 1532 - '@atcute/identity-resolver@1.1.4(@atcute/identity@1.1.3)': 1552 + '@atcute/identity-resolver@1.2.1(@atcute/identity@1.1.3)': 1533 1553 dependencies: 1534 1554 '@atcute/identity': 1.1.3 1535 - '@atcute/lexicons': 1.2.5 1555 + '@atcute/lexicons': 1.2.6 1536 1556 '@atcute/util-fetch': 1.0.4 1537 1557 '@badrap/valita': 0.4.6 1538 1558 1539 1559 '@atcute/identity@1.1.3': 1540 1560 dependencies: 1541 - '@atcute/lexicons': 1.2.5 1561 + '@atcute/lexicons': 1.2.6 1542 1562 '@badrap/valita': 0.4.6 1543 1563 1544 - '@atcute/leaflet@1.0.12': 1564 + '@atcute/leaflet@1.0.14': 1545 1565 dependencies: 1546 1566 '@atcute/atproto': 3.1.9 1547 - '@atcute/lexicons': 1.2.5 1567 + '@atcute/lexicons': 1.2.6 1548 1568 1549 - '@atcute/lexicon-doc@2.0.4': 1569 + '@atcute/lexicon-doc@2.0.6': 1550 1570 dependencies: 1551 1571 '@atcute/identity': 1.1.3 1552 - '@atcute/lexicons': 1.2.5 1572 + '@atcute/lexicons': 1.2.6 1573 + '@atcute/uint8array': 1.0.6 1574 + '@atcute/util-text': 0.0.1 1553 1575 '@badrap/valita': 0.4.6 1554 1576 1555 - '@atcute/lexicon-resolver@0.1.5(@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)': 1556 1578 dependencies: 1557 - '@atcute/crypto': 2.2.6 1579 + '@atcute/crypto': 2.3.0 1558 1580 '@atcute/identity': 1.1.3 1559 - '@atcute/identity-resolver': 1.1.4(@atcute/identity@1.1.3) 1560 - '@atcute/lexicon-doc': 2.0.4 1561 - '@atcute/lexicons': 1.2.5 1562 - '@atcute/repo': 0.1.0 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 1563 1585 '@atcute/util-fetch': 1.0.4 1564 1586 '@badrap/valita': 0.4.6 1565 1587 1566 - '@atcute/lexicons@1.2.5': 1588 + '@atcute/lexicons@1.2.6': 1567 1589 dependencies: 1568 - '@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 1569 1593 esm-env: 1.2.2 1570 1594 1571 - '@atcute/mst@0.1.0': 1595 + '@atcute/mst@0.1.1': 1572 1596 dependencies: 1573 1597 '@atcute/cbor': 2.2.8 1574 - '@atcute/cid': 2.2.6 1575 - '@atcute/uint8array': 1.0.5 1598 + '@atcute/cid': 2.3.0 1599 + '@atcute/uint8array': 1.0.6 1576 1600 1577 1601 '@atcute/multibase@1.1.6': 1578 1602 dependencies: 1579 - '@atcute/uint8array': 1.0.5 1603 + '@atcute/uint8array': 1.0.6 1580 1604 1581 - '@atcute/oauth-browser-client@2.0.1': 1605 + '@atcute/oauth-browser-client@2.0.3(@atcute/identity@1.1.3)': 1582 1606 dependencies: 1583 - '@atcute/client': 4.1.0 1584 - '@atcute/identity': 1.1.3 1585 - '@atcute/identity-resolver': 1.1.4(@atcute/identity@1.1.3) 1586 - '@atcute/lexicons': 1.2.5 1607 + '@atcute/client': 4.1.2 1608 + '@atcute/identity-resolver': 1.2.1(@atcute/identity@1.1.3) 1609 + '@atcute/lexicons': 1.2.6 1587 1610 '@atcute/multibase': 1.1.6 1588 - '@atcute/uint8array': 1.0.5 1611 + '@atcute/uint8array': 1.0.6 1589 1612 nanoid: 5.1.6 1613 + transitivePeerDependencies: 1614 + - '@atcute/identity' 1590 1615 1591 - '@atcute/repo@0.1.0': 1616 + '@atcute/repo@0.1.1': 1592 1617 dependencies: 1593 1618 '@atcute/car': 5.0.0 1594 1619 '@atcute/cbor': 2.2.8 1595 - '@atcute/cid': 2.2.6 1596 - '@atcute/crypto': 2.2.6 1597 - '@atcute/lexicons': 1.2.5 1598 - '@atcute/mst': 0.1.0 1599 - '@atcute/uint8array': 1.0.5 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 1600 1625 1601 - '@atcute/tangled@1.0.12': 1626 + '@atcute/tangled@1.0.13': 1602 1627 dependencies: 1603 1628 '@atcute/atproto': 3.1.9 1604 - '@atcute/lexicons': 1.2.5 1629 + '@atcute/lexicons': 1.2.6 1605 1630 1606 - '@atcute/tid@1.0.3': {} 1631 + '@atcute/tid@1.1.0': 1632 + dependencies: 1633 + '@atcute/time-ms': 1.0.0 1607 1634 1608 - '@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': {} 1609 1641 1610 1642 '@atcute/util-fetch@1.0.4': 1611 1643 dependencies: 1612 1644 '@badrap/valita': 0.4.6 1645 + 1646 + '@atcute/util-text@0.0.1': 1647 + dependencies: 1648 + unicode-segmenter: 0.14.4 1613 1649 1614 1650 '@atcute/varint@1.0.3': {} 1615 1651 ··· 1728 1764 1729 1765 '@codemirror/autocomplete@6.20.0': 1730 1766 dependencies: 1731 - '@codemirror/language': 6.11.3 1732 - '@codemirror/state': 6.5.2 1733 - '@codemirror/view': 6.38.8 1734 - '@lezer/common': 1.4.0 1767 + '@codemirror/language': 6.12.1 1768 + '@codemirror/state': 6.5.3 1769 + '@codemirror/view': 6.39.7 1770 + '@lezer/common': 1.5.0 1735 1771 1736 - '@codemirror/commands@6.10.0': 1772 + '@codemirror/commands@6.10.1': 1737 1773 dependencies: 1738 - '@codemirror/language': 6.11.3 1739 - '@codemirror/state': 6.5.2 1740 - '@codemirror/view': 6.38.8 1741 - '@lezer/common': 1.4.0 1774 + '@codemirror/language': 6.12.1 1775 + '@codemirror/state': 6.5.3 1776 + '@codemirror/view': 6.39.7 1777 + '@lezer/common': 1.5.0 1742 1778 1743 1779 '@codemirror/lang-json@6.0.2': 1744 1780 dependencies: 1745 - '@codemirror/language': 6.11.3 1781 + '@codemirror/language': 6.12.1 1746 1782 '@lezer/json': 1.0.3 1747 1783 1748 - '@codemirror/language@6.11.3': 1784 + '@codemirror/language@6.12.1': 1749 1785 dependencies: 1750 - '@codemirror/state': 6.5.2 1751 - '@codemirror/view': 6.38.8 1752 - '@lezer/common': 1.4.0 1786 + '@codemirror/state': 6.5.3 1787 + '@codemirror/view': 6.39.7 1788 + '@lezer/common': 1.5.0 1753 1789 '@lezer/highlight': 1.2.3 1754 - '@lezer/lr': 1.4.4 1790 + '@lezer/lr': 1.4.5 1755 1791 style-mod: 4.1.3 1756 1792 1757 1793 '@codemirror/lint@6.9.2': 1758 1794 dependencies: 1759 - '@codemirror/state': 6.5.2 1760 - '@codemirror/view': 6.38.8 1795 + '@codemirror/state': 6.5.3 1796 + '@codemirror/view': 6.39.7 1761 1797 crelt: 1.0.6 1762 1798 1763 1799 '@codemirror/search@6.5.11': 1764 1800 dependencies: 1765 - '@codemirror/state': 6.5.2 1766 - '@codemirror/view': 6.38.8 1801 + '@codemirror/state': 6.5.3 1802 + '@codemirror/view': 6.39.7 1767 1803 crelt: 1.0.6 1768 1804 1769 - '@codemirror/state@6.5.2': 1805 + '@codemirror/state@6.5.3': 1770 1806 dependencies: 1771 1807 '@marijn/find-cluster-break': 1.0.2 1772 1808 1773 - '@codemirror/view@6.38.8': 1809 + '@codemirror/view@6.39.7': 1774 1810 dependencies: 1775 - '@codemirror/state': 6.5.2 1811 + '@codemirror/state': 6.5.3 1776 1812 crelt: 1.0.6 1777 1813 style-mod: 4.1.3 1778 1814 w3c-keyname: 2.2.8 ··· 1784 1820 '@esbuild/aix-ppc64@0.23.1': 1785 1821 optional: true 1786 1822 1787 - '@esbuild/aix-ppc64@0.25.12': 1823 + '@esbuild/aix-ppc64@0.27.2': 1788 1824 optional: true 1789 1825 1790 1826 '@esbuild/android-arm64@0.23.1': 1791 1827 optional: true 1792 1828 1793 - '@esbuild/android-arm64@0.25.12': 1829 + '@esbuild/android-arm64@0.27.2': 1794 1830 optional: true 1795 1831 1796 1832 '@esbuild/android-arm@0.23.1': 1797 1833 optional: true 1798 1834 1799 - '@esbuild/android-arm@0.25.12': 1835 + '@esbuild/android-arm@0.27.2': 1800 1836 optional: true 1801 1837 1802 1838 '@esbuild/android-x64@0.23.1': 1803 1839 optional: true 1804 1840 1805 - '@esbuild/android-x64@0.25.12': 1841 + '@esbuild/android-x64@0.27.2': 1806 1842 optional: true 1807 1843 1808 1844 '@esbuild/darwin-arm64@0.23.1': 1809 1845 optional: true 1810 1846 1811 - '@esbuild/darwin-arm64@0.25.12': 1847 + '@esbuild/darwin-arm64@0.27.2': 1812 1848 optional: true 1813 1849 1814 1850 '@esbuild/darwin-x64@0.23.1': 1815 1851 optional: true 1816 1852 1817 - '@esbuild/darwin-x64@0.25.12': 1853 + '@esbuild/darwin-x64@0.27.2': 1818 1854 optional: true 1819 1855 1820 1856 '@esbuild/freebsd-arm64@0.23.1': 1821 1857 optional: true 1822 1858 1823 - '@esbuild/freebsd-arm64@0.25.12': 1859 + '@esbuild/freebsd-arm64@0.27.2': 1824 1860 optional: true 1825 1861 1826 1862 '@esbuild/freebsd-x64@0.23.1': 1827 1863 optional: true 1828 1864 1829 - '@esbuild/freebsd-x64@0.25.12': 1865 + '@esbuild/freebsd-x64@0.27.2': 1830 1866 optional: true 1831 1867 1832 1868 '@esbuild/linux-arm64@0.23.1': 1833 1869 optional: true 1834 1870 1835 - '@esbuild/linux-arm64@0.25.12': 1871 + '@esbuild/linux-arm64@0.27.2': 1836 1872 optional: true 1837 1873 1838 1874 '@esbuild/linux-arm@0.23.1': 1839 1875 optional: true 1840 1876 1841 - '@esbuild/linux-arm@0.25.12': 1877 + '@esbuild/linux-arm@0.27.2': 1842 1878 optional: true 1843 1879 1844 1880 '@esbuild/linux-ia32@0.23.1': 1845 1881 optional: true 1846 1882 1847 - '@esbuild/linux-ia32@0.25.12': 1883 + '@esbuild/linux-ia32@0.27.2': 1848 1884 optional: true 1849 1885 1850 1886 '@esbuild/linux-loong64@0.23.1': 1851 1887 optional: true 1852 1888 1853 - '@esbuild/linux-loong64@0.25.12': 1889 + '@esbuild/linux-loong64@0.27.2': 1854 1890 optional: true 1855 1891 1856 1892 '@esbuild/linux-mips64el@0.23.1': 1857 1893 optional: true 1858 1894 1859 - '@esbuild/linux-mips64el@0.25.12': 1895 + '@esbuild/linux-mips64el@0.27.2': 1860 1896 optional: true 1861 1897 1862 1898 '@esbuild/linux-ppc64@0.23.1': 1863 1899 optional: true 1864 1900 1865 - '@esbuild/linux-ppc64@0.25.12': 1901 + '@esbuild/linux-ppc64@0.27.2': 1866 1902 optional: true 1867 1903 1868 1904 '@esbuild/linux-riscv64@0.23.1': 1869 1905 optional: true 1870 1906 1871 - '@esbuild/linux-riscv64@0.25.12': 1907 + '@esbuild/linux-riscv64@0.27.2': 1872 1908 optional: true 1873 1909 1874 1910 '@esbuild/linux-s390x@0.23.1': 1875 1911 optional: true 1876 1912 1877 - '@esbuild/linux-s390x@0.25.12': 1913 + '@esbuild/linux-s390x@0.27.2': 1878 1914 optional: true 1879 1915 1880 1916 '@esbuild/linux-x64@0.23.1': 1881 1917 optional: true 1882 1918 1883 - '@esbuild/linux-x64@0.25.12': 1919 + '@esbuild/linux-x64@0.27.2': 1884 1920 optional: true 1885 1921 1886 - '@esbuild/netbsd-arm64@0.25.12': 1922 + '@esbuild/netbsd-arm64@0.27.2': 1887 1923 optional: true 1888 1924 1889 1925 '@esbuild/netbsd-x64@0.23.1': 1890 1926 optional: true 1891 1927 1892 - '@esbuild/netbsd-x64@0.25.12': 1928 + '@esbuild/netbsd-x64@0.27.2': 1893 1929 optional: true 1894 1930 1895 1931 '@esbuild/openbsd-arm64@0.23.1': 1896 1932 optional: true 1897 1933 1898 - '@esbuild/openbsd-arm64@0.25.12': 1934 + '@esbuild/openbsd-arm64@0.27.2': 1899 1935 optional: true 1900 1936 1901 1937 '@esbuild/openbsd-x64@0.23.1': 1902 1938 optional: true 1903 1939 1904 - '@esbuild/openbsd-x64@0.25.12': 1940 + '@esbuild/openbsd-x64@0.27.2': 1905 1941 optional: true 1906 1942 1907 - '@esbuild/openharmony-arm64@0.25.12': 1943 + '@esbuild/openharmony-arm64@0.27.2': 1908 1944 optional: true 1909 1945 1910 1946 '@esbuild/sunos-x64@0.23.1': 1911 1947 optional: true 1912 1948 1913 - '@esbuild/sunos-x64@0.25.12': 1949 + '@esbuild/sunos-x64@0.27.2': 1914 1950 optional: true 1915 1951 1916 1952 '@esbuild/win32-arm64@0.23.1': 1917 1953 optional: true 1918 1954 1919 - '@esbuild/win32-arm64@0.25.12': 1955 + '@esbuild/win32-arm64@0.27.2': 1920 1956 optional: true 1921 1957 1922 1958 '@esbuild/win32-ia32@0.23.1': 1923 1959 optional: true 1924 1960 1925 - '@esbuild/win32-ia32@0.25.12': 1961 + '@esbuild/win32-ia32@0.27.2': 1926 1962 optional: true 1927 1963 1928 1964 '@esbuild/win32-x64@0.23.1': 1929 1965 optional: true 1930 1966 1931 - '@esbuild/win32-x64@0.25.12': 1967 + '@esbuild/win32-x64@0.27.2': 1932 1968 optional: true 1933 1969 1934 - '@fsegurai/codemirror-theme-basic-dark@6.2.3(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.8)(@lezer/highlight@1.2.3)': 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)': 1935 1971 dependencies: 1936 - '@codemirror/language': 6.11.3 1937 - '@codemirror/state': 6.5.2 1938 - '@codemirror/view': 6.38.8 1972 + '@codemirror/language': 6.12.1 1973 + '@codemirror/state': 6.5.3 1974 + '@codemirror/view': 6.39.7 1939 1975 '@lezer/highlight': 1.2.3 1940 1976 1941 - '@fsegurai/codemirror-theme-basic-light@6.2.3(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.8)(@lezer/highlight@1.2.3)': 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)': 1942 1978 dependencies: 1943 - '@codemirror/language': 6.11.3 1944 - '@codemirror/state': 6.5.2 1945 - '@codemirror/view': 6.38.8 1979 + '@codemirror/language': 6.12.1 1980 + '@codemirror/state': 6.5.3 1981 + '@codemirror/view': 6.39.7 1946 1982 '@lezer/highlight': 1.2.3 1947 1983 1948 - '@iconify-json/lucide@1.2.77': 1984 + '@iconify-json/lucide@1.2.82': 1949 1985 dependencies: 1950 1986 '@iconify/types': 2.0.0 1951 1987 1952 - '@iconify/tailwind4@1.2.0(tailwindcss@4.1.17)': 1988 + '@iconify/tailwind4@1.2.0(tailwindcss@4.1.18)': 1953 1989 dependencies: 1954 - '@iconify/tools': 5.0.0 1990 + '@iconify/tools': 5.0.1 1955 1991 '@iconify/types': 2.0.0 1956 1992 '@iconify/utils': 3.1.0 1957 - tailwindcss: 4.1.17 1993 + tailwindcss: 4.1.18 1958 1994 1959 - '@iconify/tools@5.0.0': 1995 + '@iconify/tools@5.0.1': 1960 1996 dependencies: 1961 1997 '@cyberalien/svg-utils': 1.0.11 1962 1998 '@iconify/types': 2.0.0 1963 1999 '@iconify/utils': 3.1.0 1964 2000 fflate: 0.8.2 1965 - modern-tar: 0.7.2 2001 + modern-tar: 0.7.3 1966 2002 pathe: 2.0.3 1967 2003 svgo: 4.0.0 1968 2004 ··· 1995 2031 1996 2032 '@jsr/mary__exif-rm@0.2.2': {} 1997 2033 1998 - '@lezer/common@1.4.0': {} 2034 + '@lezer/common@1.5.0': {} 1999 2035 2000 2036 '@lezer/highlight@1.2.3': 2001 2037 dependencies: 2002 - '@lezer/common': 1.4.0 2038 + '@lezer/common': 1.5.0 2003 2039 2004 2040 '@lezer/json@1.0.3': 2005 2041 dependencies: 2006 - '@lezer/common': 1.4.0 2042 + '@lezer/common': 1.5.0 2007 2043 '@lezer/highlight': 1.2.3 2008 - '@lezer/lr': 1.4.4 2044 + '@lezer/lr': 1.4.5 2009 2045 2010 - '@lezer/lr@1.4.4': 2046 + '@lezer/lr@1.4.5': 2011 2047 dependencies: 2012 - '@lezer/common': 1.4.0 2048 + '@lezer/common': 1.5.0 2013 2049 2014 2050 '@marijn/find-cluster-break@1.0.2': {} 2015 2051 2016 2052 '@noble/secp256k1@3.0.0': {} 2017 2053 2018 - '@rollup/rollup-android-arm-eabi@4.53.3': 2054 + '@rollup/rollup-android-arm-eabi@4.54.0': 2019 2055 optional: true 2020 2056 2021 - '@rollup/rollup-android-arm64@4.53.3': 2057 + '@rollup/rollup-android-arm64@4.54.0': 2022 2058 optional: true 2023 2059 2024 - '@rollup/rollup-darwin-arm64@4.53.3': 2060 + '@rollup/rollup-darwin-arm64@4.54.0': 2025 2061 optional: true 2026 2062 2027 - '@rollup/rollup-darwin-x64@4.53.3': 2063 + '@rollup/rollup-darwin-x64@4.54.0': 2028 2064 optional: true 2029 2065 2030 - '@rollup/rollup-freebsd-arm64@4.53.3': 2066 + '@rollup/rollup-freebsd-arm64@4.54.0': 2031 2067 optional: true 2032 2068 2033 - '@rollup/rollup-freebsd-x64@4.53.3': 2069 + '@rollup/rollup-freebsd-x64@4.54.0': 2034 2070 optional: true 2035 2071 2036 - '@rollup/rollup-linux-arm-gnueabihf@4.53.3': 2072 + '@rollup/rollup-linux-arm-gnueabihf@4.54.0': 2037 2073 optional: true 2038 2074 2039 - '@rollup/rollup-linux-arm-musleabihf@4.53.3': 2075 + '@rollup/rollup-linux-arm-musleabihf@4.54.0': 2040 2076 optional: true 2041 2077 2042 - '@rollup/rollup-linux-arm64-gnu@4.53.3': 2078 + '@rollup/rollup-linux-arm64-gnu@4.54.0': 2043 2079 optional: true 2044 2080 2045 - '@rollup/rollup-linux-arm64-musl@4.53.3': 2081 + '@rollup/rollup-linux-arm64-musl@4.54.0': 2046 2082 optional: true 2047 2083 2048 - '@rollup/rollup-linux-loong64-gnu@4.53.3': 2084 + '@rollup/rollup-linux-loong64-gnu@4.54.0': 2049 2085 optional: true 2050 2086 2051 - '@rollup/rollup-linux-ppc64-gnu@4.53.3': 2087 + '@rollup/rollup-linux-ppc64-gnu@4.54.0': 2052 2088 optional: true 2053 2089 2054 - '@rollup/rollup-linux-riscv64-gnu@4.53.3': 2090 + '@rollup/rollup-linux-riscv64-gnu@4.54.0': 2055 2091 optional: true 2056 2092 2057 - '@rollup/rollup-linux-riscv64-musl@4.53.3': 2093 + '@rollup/rollup-linux-riscv64-musl@4.54.0': 2058 2094 optional: true 2059 2095 2060 - '@rollup/rollup-linux-s390x-gnu@4.53.3': 2096 + '@rollup/rollup-linux-s390x-gnu@4.54.0': 2061 2097 optional: true 2062 2098 2063 - '@rollup/rollup-linux-x64-gnu@4.53.3': 2099 + '@rollup/rollup-linux-x64-gnu@4.54.0': 2064 2100 optional: true 2065 2101 2066 - '@rollup/rollup-linux-x64-musl@4.53.3': 2102 + '@rollup/rollup-linux-x64-musl@4.54.0': 2067 2103 optional: true 2068 2104 2069 - '@rollup/rollup-openharmony-arm64@4.53.3': 2105 + '@rollup/rollup-openharmony-arm64@4.54.0': 2070 2106 optional: true 2071 2107 2072 - '@rollup/rollup-win32-arm64-msvc@4.53.3': 2108 + '@rollup/rollup-win32-arm64-msvc@4.54.0': 2073 2109 optional: true 2074 2110 2075 - '@rollup/rollup-win32-ia32-msvc@4.53.3': 2111 + '@rollup/rollup-win32-ia32-msvc@4.54.0': 2076 2112 optional: true 2077 2113 2078 - '@rollup/rollup-win32-x64-gnu@4.53.3': 2114 + '@rollup/rollup-win32-x64-gnu@4.54.0': 2079 2115 optional: true 2080 2116 2081 - '@rollup/rollup-win32-x64-msvc@4.53.3': 2117 + '@rollup/rollup-win32-x64-msvc@4.54.0': 2082 2118 optional: true 2083 2119 2084 2120 '@skyware/firehose@0.5.2': ··· 2095 2131 dependencies: 2096 2132 solid-js: 1.9.10 2097 2133 2098 - '@standard-schema/spec@1.0.0': {} 2134 + '@standard-schema/spec@1.1.0': {} 2099 2135 2100 - '@tailwindcss/node@4.1.17': 2136 + '@tailwindcss/node@4.1.18': 2101 2137 dependencies: 2102 2138 '@jridgewell/remapping': 2.3.5 2103 - enhanced-resolve: 5.18.3 2139 + enhanced-resolve: 5.18.4 2104 2140 jiti: 2.6.1 2105 2141 lightningcss: 1.30.2 2106 2142 magic-string: 0.30.21 2107 2143 source-map-js: 1.2.1 2108 - tailwindcss: 4.1.17 2144 + tailwindcss: 4.1.18 2109 2145 2110 - '@tailwindcss/oxide-android-arm64@4.1.17': 2146 + '@tailwindcss/oxide-android-arm64@4.1.18': 2111 2147 optional: true 2112 2148 2113 - '@tailwindcss/oxide-darwin-arm64@4.1.17': 2149 + '@tailwindcss/oxide-darwin-arm64@4.1.18': 2114 2150 optional: true 2115 2151 2116 - '@tailwindcss/oxide-darwin-x64@4.1.17': 2152 + '@tailwindcss/oxide-darwin-x64@4.1.18': 2117 2153 optional: true 2118 2154 2119 - '@tailwindcss/oxide-freebsd-x64@4.1.17': 2155 + '@tailwindcss/oxide-freebsd-x64@4.1.18': 2120 2156 optional: true 2121 2157 2122 - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17': 2158 + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': 2123 2159 optional: true 2124 2160 2125 - '@tailwindcss/oxide-linux-arm64-gnu@4.1.17': 2161 + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': 2126 2162 optional: true 2127 2163 2128 - '@tailwindcss/oxide-linux-arm64-musl@4.1.17': 2164 + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': 2129 2165 optional: true 2130 2166 2131 - '@tailwindcss/oxide-linux-x64-gnu@4.1.17': 2167 + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': 2132 2168 optional: true 2133 2169 2134 - '@tailwindcss/oxide-linux-x64-musl@4.1.17': 2170 + '@tailwindcss/oxide-linux-x64-musl@4.1.18': 2135 2171 optional: true 2136 2172 2137 - '@tailwindcss/oxide-wasm32-wasi@4.1.17': 2173 + '@tailwindcss/oxide-wasm32-wasi@4.1.18': 2138 2174 optional: true 2139 2175 2140 - '@tailwindcss/oxide-win32-arm64-msvc@4.1.17': 2176 + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': 2141 2177 optional: true 2142 2178 2143 - '@tailwindcss/oxide-win32-x64-msvc@4.1.17': 2179 + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': 2144 2180 optional: true 2145 2181 2146 - '@tailwindcss/oxide@4.1.17': 2182 + '@tailwindcss/oxide@4.1.18': 2147 2183 optionalDependencies: 2148 - '@tailwindcss/oxide-android-arm64': 4.1.17 2149 - '@tailwindcss/oxide-darwin-arm64': 4.1.17 2150 - '@tailwindcss/oxide-darwin-x64': 4.1.17 2151 - '@tailwindcss/oxide-freebsd-x64': 4.1.17 2152 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.17 2153 - '@tailwindcss/oxide-linux-arm64-gnu': 4.1.17 2154 - '@tailwindcss/oxide-linux-arm64-musl': 4.1.17 2155 - '@tailwindcss/oxide-linux-x64-gnu': 4.1.17 2156 - '@tailwindcss/oxide-linux-x64-musl': 4.1.17 2157 - '@tailwindcss/oxide-wasm32-wasi': 4.1.17 2158 - '@tailwindcss/oxide-win32-arm64-msvc': 4.1.17 2159 - '@tailwindcss/oxide-win32-x64-msvc': 4.1.17 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 2160 2196 2161 - '@tailwindcss/vite@4.1.17(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2))': 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))': 2162 2198 dependencies: 2163 - '@tailwindcss/node': 4.1.17 2164 - '@tailwindcss/oxide': 4.1.17 2165 - tailwindcss: 4.1.17 2166 - vite: 7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2) 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) 2167 2203 2168 2204 '@types/babel__core@7.20.5': 2169 2205 dependencies: ··· 2187 2223 '@babel/types': 7.28.5 2188 2224 2189 2225 '@types/estree@1.0.8': {} 2226 + 2227 + '@types/node@22.19.3': 2228 + dependencies: 2229 + undici-types: 6.21.0 2190 2230 2191 2231 '@types/node@24.10.1': 2192 2232 dependencies: ··· 2211 2251 optionalDependencies: 2212 2252 solid-js: 1.9.10 2213 2253 2214 - baseline-browser-mapping@2.9.0: {} 2254 + baseline-browser-mapping@2.9.11: {} 2215 2255 2216 2256 boolbase@1.0.0: {} 2217 2257 2218 2258 browserslist@4.28.1: 2219 2259 dependencies: 2220 - baseline-browser-mapping: 2.9.0 2221 - caniuse-lite: 1.0.30001759 2222 - electron-to-chromium: 1.5.263 2260 + baseline-browser-mapping: 2.9.11 2261 + caniuse-lite: 1.0.30001761 2262 + electron-to-chromium: 1.5.267 2223 2263 node-releases: 2.0.27 2224 - update-browserslist-db: 1.2.1(browserslist@4.28.1) 2264 + update-browserslist-db: 1.2.3(browserslist@4.28.1) 2225 2265 2226 - caniuse-lite@1.0.30001759: {} 2266 + caniuse-lite@1.0.30001761: {} 2227 2267 2228 2268 codemirror@6.0.2: 2229 2269 dependencies: 2230 2270 '@codemirror/autocomplete': 6.20.0 2231 - '@codemirror/commands': 6.10.0 2232 - '@codemirror/language': 6.11.3 2271 + '@codemirror/commands': 6.10.1 2272 + '@codemirror/language': 6.12.1 2233 2273 '@codemirror/lint': 6.9.2 2234 2274 '@codemirror/search': 6.5.11 2235 - '@codemirror/state': 6.5.2 2236 - '@codemirror/view': 6.38.8 2275 + '@codemirror/state': 6.5.3 2276 + '@codemirror/view': 6.39.7 2237 2277 2238 2278 commander@11.1.0: {} 2239 2279 ··· 2293 2333 domelementtype: 2.3.0 2294 2334 domhandler: 5.0.3 2295 2335 2296 - electron-to-chromium@1.5.263: {} 2336 + electron-to-chromium@1.5.267: {} 2297 2337 2298 - enhanced-resolve@5.18.3: 2338 + enhanced-resolve@5.18.4: 2299 2339 dependencies: 2300 2340 graceful-fs: 4.2.11 2301 2341 tapable: 2.3.0 ··· 2332 2372 '@esbuild/win32-x64': 0.23.1 2333 2373 optional: true 2334 2374 2335 - esbuild@0.25.12: 2375 + esbuild@0.27.2: 2336 2376 optionalDependencies: 2337 - '@esbuild/aix-ppc64': 0.25.12 2338 - '@esbuild/android-arm': 0.25.12 2339 - '@esbuild/android-arm64': 0.25.12 2340 - '@esbuild/android-x64': 0.25.12 2341 - '@esbuild/darwin-arm64': 0.25.12 2342 - '@esbuild/darwin-x64': 0.25.12 2343 - '@esbuild/freebsd-arm64': 0.25.12 2344 - '@esbuild/freebsd-x64': 0.25.12 2345 - '@esbuild/linux-arm': 0.25.12 2346 - '@esbuild/linux-arm64': 0.25.12 2347 - '@esbuild/linux-ia32': 0.25.12 2348 - '@esbuild/linux-loong64': 0.25.12 2349 - '@esbuild/linux-mips64el': 0.25.12 2350 - '@esbuild/linux-ppc64': 0.25.12 2351 - '@esbuild/linux-riscv64': 0.25.12 2352 - '@esbuild/linux-s390x': 0.25.12 2353 - '@esbuild/linux-x64': 0.25.12 2354 - '@esbuild/netbsd-arm64': 0.25.12 2355 - '@esbuild/netbsd-x64': 0.25.12 2356 - '@esbuild/openbsd-arm64': 0.25.12 2357 - '@esbuild/openbsd-x64': 0.25.12 2358 - '@esbuild/openharmony-arm64': 0.25.12 2359 - '@esbuild/sunos-x64': 0.25.12 2360 - '@esbuild/win32-arm64': 0.25.12 2361 - '@esbuild/win32-ia32': 0.25.12 2362 - '@esbuild/win32-x64': 0.25.12 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 2363 2403 2364 2404 escalade@3.2.0: {} 2365 2405 ··· 2467 2507 pkg-types: 1.3.1 2468 2508 ufo: 1.6.1 2469 2509 2470 - modern-tar@0.7.2: {} 2510 + modern-tar@0.7.3: {} 2471 2511 2472 2512 ms@2.1.3: {} 2473 2513 ··· 2476 2516 nanoid@3.3.11: {} 2477 2517 2478 2518 nanoid@5.1.6: {} 2519 + 2520 + node-gyp-build@4.8.4: {} 2479 2521 2480 2522 node-releases@2.0.27: {} 2481 2523 ··· 2523 2565 resolve-pkg-maps@1.0.0: 2524 2566 optional: true 2525 2567 2526 - rollup@4.53.3: 2568 + rollup@4.54.0: 2527 2569 dependencies: 2528 2570 '@types/estree': 1.0.8 2529 2571 optionalDependencies: 2530 - '@rollup/rollup-android-arm-eabi': 4.53.3 2531 - '@rollup/rollup-android-arm64': 4.53.3 2532 - '@rollup/rollup-darwin-arm64': 4.53.3 2533 - '@rollup/rollup-darwin-x64': 4.53.3 2534 - '@rollup/rollup-freebsd-arm64': 4.53.3 2535 - '@rollup/rollup-freebsd-x64': 4.53.3 2536 - '@rollup/rollup-linux-arm-gnueabihf': 4.53.3 2537 - '@rollup/rollup-linux-arm-musleabihf': 4.53.3 2538 - '@rollup/rollup-linux-arm64-gnu': 4.53.3 2539 - '@rollup/rollup-linux-arm64-musl': 4.53.3 2540 - '@rollup/rollup-linux-loong64-gnu': 4.53.3 2541 - '@rollup/rollup-linux-ppc64-gnu': 4.53.3 2542 - '@rollup/rollup-linux-riscv64-gnu': 4.53.3 2543 - '@rollup/rollup-linux-riscv64-musl': 4.53.3 2544 - '@rollup/rollup-linux-s390x-gnu': 4.53.3 2545 - '@rollup/rollup-linux-x64-gnu': 4.53.3 2546 - '@rollup/rollup-linux-x64-musl': 4.53.3 2547 - '@rollup/rollup-openharmony-arm64': 4.53.3 2548 - '@rollup/rollup-win32-arm64-msvc': 4.53.3 2549 - '@rollup/rollup-win32-ia32-msvc': 4.53.3 2550 - '@rollup/rollup-win32-x64-gnu': 4.53.3 2551 - '@rollup/rollup-win32-x64-msvc': 4.53.3 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 2552 2594 fsevents: 2.3.3 2553 2595 2554 2596 sax@1.4.3: {} ··· 2590 2632 picocolors: 1.1.1 2591 2633 sax: 1.4.3 2592 2634 2593 - tailwindcss@4.1.17: {} 2635 + tailwindcss@4.1.18: {} 2594 2636 2595 2637 tapable@2.3.0: {} 2596 2638 ··· 2613 2655 2614 2656 ufo@1.6.1: {} 2615 2657 2658 + undici-types@6.21.0: {} 2659 + 2616 2660 undici-types@7.16.0: 2617 2661 optional: true 2618 2662 2619 - update-browserslist-db@1.2.1(browserslist@4.28.1): 2663 + unicode-segmenter@0.14.4: {} 2664 + 2665 + update-browserslist-db@1.2.3(browserslist@4.28.1): 2620 2666 dependencies: 2621 2667 browserslist: 4.28.1 2622 2668 escalade: 3.2.0 2623 2669 picocolors: 1.1.1 2624 2670 2625 - vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)): 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)): 2626 2672 dependencies: 2627 2673 '@babel/core': 7.28.5 2628 2674 '@types/babel__core': 7.20.5 ··· 2630 2676 merge-anything: 5.1.7 2631 2677 solid-js: 1.9.10 2632 2678 solid-refresh: 0.6.3(solid-js@1.9.10) 2633 - vite: 7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2) 2634 - vitefu: 1.1.1(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)) 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)) 2635 2681 transitivePeerDependencies: 2636 2682 - supports-color 2637 2683 2638 - vite@7.2.6(@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): 2639 2685 dependencies: 2640 - esbuild: 0.25.12 2686 + esbuild: 0.27.2 2641 2687 fdir: 6.5.0(picomatch@4.0.3) 2642 2688 picomatch: 4.0.3 2643 2689 postcss: 8.5.6 2644 - rollup: 4.53.3 2690 + rollup: 4.54.0 2645 2691 tinyglobby: 0.2.15 2646 2692 optionalDependencies: 2647 2693 '@types/node': 24.10.1 ··· 2650 2696 lightningcss: 1.30.2 2651 2697 tsx: 4.19.2 2652 2698 2653 - vitefu@1.1.1(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)): 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)): 2654 2700 optionalDependencies: 2655 - vite: 7.2.6(@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) 2656 2702 2657 2703 w3c-keyname@2.2.8: {} 2658 2704
public/favicon.ico

This is a binary file and will not be displayed.

public/fonts/Figtree[wght].woff2

This is a binary file and will not be displayed.

+1 -1
public/oauth-client-metadata.json
··· 4 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>();
-170
src/components/account.tsx
··· 1 - import { Client, CredentialManager } from "@atcute/client"; 2 - import { Did } from "@atcute/lexicons"; 3 - import { 4 - createAuthorizationUrl, 5 - deleteStoredSession, 6 - getSession, 7 - OAuthUserAgent, 8 - } from "@atcute/oauth-browser-client"; 9 - import { A } from "@solidjs/router"; 10 - import { createSignal, For, onMount, Show } from "solid-js"; 11 - import { createStore, produce } from "solid-js/store"; 12 - import { resolveDidDoc } from "../utils/api.js"; 13 - import { ActionMenu, DropdownMenu, MenuProvider, NavMenu } from "./dropdown.jsx"; 14 - import { agent, Login, retrieveSession, Sessions, setAgent } from "./login.jsx"; 15 - import { Modal } from "./modal.jsx"; 16 - 17 - export const [sessions, setSessions] = createStore<Sessions>(); 18 - 19 - const AccountDropdown = (props: { did: Did }) => { 20 - const removeSession = async (did: Did) => { 21 - const currentSession = agent()?.sub; 22 - try { 23 - const session = await getSession(did, { allowStale: true }); 24 - const agent = new OAuthUserAgent(session); 25 - await agent.signOut(); 26 - } catch { 27 - deleteStoredSession(did); 28 - } 29 - setSessions( 30 - produce((accs) => { 31 - delete accs[did]; 32 - }), 33 - ); 34 - localStorage.setItem("sessions", JSON.stringify(sessions)); 35 - if (currentSession === did) setAgent(undefined); 36 - }; 37 - 38 - return ( 39 - <MenuProvider> 40 - <DropdownMenu icon="lucide--ellipsis" buttonClass="rounded-md p-2"> 41 - <NavMenu href={`/at://${props.did}`} label="Go to repo" icon="lucide--user-round" /> 42 - <ActionMenu 43 - icon="lucide--x" 44 - label="Remove account" 45 - onClick={() => removeSession(props.did)} 46 - /> 47 - </DropdownMenu> 48 - </MenuProvider> 49 - ); 50 - }; 51 - 52 - export const AccountManager = () => { 53 - const [openManager, setOpenManager] = createSignal(false); 54 - const [avatars, setAvatars] = createStore<Record<Did, string>>(); 55 - 56 - onMount(async () => { 57 - try { 58 - await retrieveSession(); 59 - } catch {} 60 - 61 - const localSessions = localStorage.getItem("sessions"); 62 - if (localSessions) { 63 - const storedSessions: Sessions = JSON.parse(localSessions); 64 - const sessionDids = Object.keys(storedSessions) as Did[]; 65 - sessionDids.forEach(async (did) => { 66 - const doc = await resolveDidDoc(did); 67 - const alias = doc.alsoKnownAs?.find((alias) => alias.startsWith("at://")); 68 - if (alias) { 69 - setSessions(did, { 70 - signedIn: storedSessions[did].signedIn, 71 - handle: alias.replace("at://", ""), 72 - }); 73 - } 74 - }); 75 - sessionDids.forEach(async (did) => { 76 - const avatar = await getAvatar(did); 77 - if (avatar) setAvatars(did, avatar); 78 - }); 79 - } 80 - }); 81 - 82 - const resumeSession = async (did: Did) => { 83 - try { 84 - localStorage.setItem("lastSignedIn", did); 85 - await retrieveSession(); 86 - } catch { 87 - const authUrl = await createAuthorizationUrl({ 88 - scope: import.meta.env.VITE_OAUTH_SCOPE, 89 - target: { type: "account", identifier: did }, 90 - }); 91 - 92 - await new Promise((resolve) => setTimeout(resolve, 250)); 93 - 94 - location.assign(authUrl); 95 - } 96 - }; 97 - 98 - const getAvatar = async (did: Did) => { 99 - const rpc = new Client({ 100 - handler: new CredentialManager({ service: "https://public.api.bsky.app" }), 101 - }); 102 - const res = await rpc.get("app.bsky.actor.getProfile", { params: { actor: did } }); 103 - if (res.ok) { 104 - return res.data.avatar; 105 - } 106 - return undefined; 107 - }; 108 - 109 - return ( 110 - <> 111 - <Modal open={openManager()} onClose={() => setOpenManager(false)}> 112 - <div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-18 left-[50%] w-88 -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-opacity duration-200 dark:border-neutral-700 starting:opacity-0"> 113 - <div class="mb-2 px-1 font-semibold"> 114 - <span>Manage accounts</span> 115 - </div> 116 - <div class="mb-3 max-h-80 overflow-y-auto md:max-h-100"> 117 - <For each={Object.keys(sessions)}> 118 - {(did) => ( 119 - <div class="flex w-full items-center justify-between"> 120 - <A 121 - href={`/at://${did}`} 122 - onClick={() => setOpenManager(false)} 123 - class="flex items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 124 - > 125 - <Show 126 - when={avatars[did as Did]} 127 - fallback={<span class="iconify lucide--user-round m-0.5 size-5"></span>} 128 - > 129 - <img 130 - src={avatars[did as Did].replace("img/avatar/", "img/avatar_thumbnail/")} 131 - class="size-6 rounded-full" 132 - /> 133 - </Show> 134 - </A> 135 - <button 136 - class="flex grow items-center justify-between gap-1 truncate rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 137 - onclick={() => resumeSession(did as Did)} 138 - > 139 - <span class="truncate"> 140 - {sessions[did]?.handle ? sessions[did].handle : did} 141 - </span> 142 - <Show when={did === agent()?.sub && sessions[did].signedIn}> 143 - <span class="iconify lucide--check shrink-0 text-green-500 dark:text-green-400"></span> 144 - </Show> 145 - <Show when={!sessions[did].signedIn}> 146 - <span class="iconify lucide--circle-alert shrink-0 text-red-500 dark:text-red-400"></span> 147 - </Show> 148 - </button> 149 - <AccountDropdown did={did as Did} /> 150 - </div> 151 - )} 152 - </For> 153 - </div> 154 - <Login /> 155 - </div> 156 - </Modal> 157 - <button 158 - onclick={() => setOpenManager(true)} 159 - class={`flex items-center rounded-lg ${agent() && avatars[agent()!.sub] ? "p-1.25" : "p-1.5"} hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600`} 160 - > 161 - {agent() && avatars[agent()!.sub] ? 162 - <img 163 - src={avatars[agent()!.sub].replace("img/avatar/", "img/avatar_thumbnail/")} 164 - class="size-5 rounded-full" 165 - /> 166 - : <span class="iconify lucide--circle-user-round text-lg"></span>} 167 - </button> 168 - </> 169 - ); 170 - };
+117 -173
src/components/backlinks.tsx
··· 1 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>();
-529
src/components/create.tsx
··· 1 - import { Client } from "@atcute/client"; 2 - import { Did } from "@atcute/lexicons"; 3 - import { isNsid, isRecordKey } from "@atcute/lexicons/syntax"; 4 - import { getSession, OAuthUserAgent } from "@atcute/oauth-browser-client"; 5 - import { remove } from "@mary/exif-rm"; 6 - import { useNavigate, useParams } from "@solidjs/router"; 7 - import { createEffect, createSignal, For, lazy, onCleanup, Show, Suspense } from "solid-js"; 8 - import { agent } from "../components/login.jsx"; 9 - import { sessions } from "./account.jsx"; 10 - import { Button } from "./button.jsx"; 11 - import { Modal } from "./modal.jsx"; 12 - import { addNotification, removeNotification } from "./notification.jsx"; 13 - import { TextInput } from "./text-input.jsx"; 14 - import Tooltip from "./tooltip.jsx"; 15 - 16 - const Editor = lazy(() => import("../components/editor.jsx").then((m) => ({ default: m.Editor }))); 17 - 18 - export const editorInstance = { view: null as any }; 19 - export const [placeholder, setPlaceholder] = createSignal<any>(); 20 - 21 - export const RecordEditor = (props: { create: boolean; record?: any; refetch?: any }) => { 22 - const navigate = useNavigate(); 23 - const params = useParams(); 24 - const [openDialog, setOpenDialog] = createSignal(false); 25 - const [notice, setNotice] = createSignal(""); 26 - const [openUpload, setOpenUpload] = createSignal(false); 27 - const [openInsertMenu, setOpenInsertMenu] = createSignal(false); 28 - const [validate, setValidate] = createSignal<boolean | undefined>(undefined); 29 - const [isMaximized, setIsMaximized] = createSignal(false); 30 - const [isMinimized, setIsMinimized] = createSignal(false); 31 - const [collectionError, setCollectionError] = createSignal(""); 32 - const [rkeyError, setRkeyError] = createSignal(""); 33 - let blobInput!: HTMLInputElement; 34 - let formRef!: HTMLFormElement; 35 - let insertMenuRef!: HTMLDivElement; 36 - 37 - createEffect(() => { 38 - if (openInsertMenu()) { 39 - const handleClickOutside = (e: MouseEvent) => { 40 - if (insertMenuRef && !insertMenuRef.contains(e.target as Node)) { 41 - setOpenInsertMenu(false); 42 - } 43 - }; 44 - document.addEventListener("mousedown", handleClickOutside); 45 - onCleanup(() => document.removeEventListener("mousedown", handleClickOutside)); 46 - } 47 - }); 48 - 49 - const defaultPlaceholder = () => { 50 - return { 51 - $type: "app.bsky.feed.post", 52 - text: "This post was sent from PDSls", 53 - embed: { 54 - $type: "app.bsky.embed.external", 55 - external: { 56 - uri: "https://pdsls.dev", 57 - title: "PDSls", 58 - description: "Browse the public data on atproto", 59 - }, 60 - }, 61 - langs: ["en"], 62 - createdAt: new Date().toISOString(), 63 - }; 64 - }; 65 - 66 - const getValidateIcon = () => { 67 - return ( 68 - validate() === true ? "lucide--circle-check" 69 - : validate() === false ? "lucide--circle-x" 70 - : "lucide--circle" 71 - ); 72 - }; 73 - 74 - const getValidateLabel = () => { 75 - return ( 76 - validate() === true ? "True" 77 - : validate() === false ? "False" 78 - : "Unset" 79 - ); 80 - }; 81 - 82 - createEffect(() => { 83 - if (openDialog()) { 84 - setValidate(undefined); 85 - setCollectionError(""); 86 - setRkeyError(""); 87 - } 88 - }); 89 - 90 - const createRecord = async (formData: FormData) => { 91 - const repo = formData.get("repo")?.toString(); 92 - if (!repo) return; 93 - const rpc = new Client({ handler: new OAuthUserAgent(await getSession(repo as Did)) }); 94 - const collection = formData.get("collection"); 95 - const rkey = formData.get("rkey"); 96 - let record: any; 97 - try { 98 - record = JSON.parse(editorInstance.view.state.doc.toString()); 99 - } catch (e: any) { 100 - setNotice(e.message); 101 - return; 102 - } 103 - const res = await rpc.post("com.atproto.repo.createRecord", { 104 - input: { 105 - repo: repo as Did, 106 - collection: collection ? collection.toString() : record.$type, 107 - rkey: rkey?.toString().length ? rkey?.toString() : undefined, 108 - record: record, 109 - validate: validate(), 110 - }, 111 - }); 112 - if (!res.ok) { 113 - setNotice(`${res.data.error}: ${res.data.message}`); 114 - return; 115 - } 116 - setOpenDialog(false); 117 - const id = addNotification({ 118 - message: "Record created", 119 - type: "success", 120 - }); 121 - setTimeout(() => removeNotification(id), 3000); 122 - navigate(`/${res.data.uri}`); 123 - }; 124 - 125 - const editRecord = async (recreate?: boolean) => { 126 - const record = editorInstance.view.state.doc.toString(); 127 - if (!record) return; 128 - const rpc = new Client({ handler: agent()! }); 129 - try { 130 - const editedRecord = JSON.parse(record); 131 - if (recreate) { 132 - const res = await rpc.post("com.atproto.repo.applyWrites", { 133 - input: { 134 - repo: agent()!.sub, 135 - validate: validate(), 136 - writes: [ 137 - { 138 - collection: params.collection as `${string}.${string}.${string}`, 139 - rkey: params.rkey!, 140 - $type: "com.atproto.repo.applyWrites#delete", 141 - }, 142 - { 143 - collection: params.collection as `${string}.${string}.${string}`, 144 - rkey: params.rkey, 145 - $type: "com.atproto.repo.applyWrites#create", 146 - value: editedRecord, 147 - }, 148 - ], 149 - }, 150 - }); 151 - if (!res.ok) { 152 - setNotice(`${res.data.error}: ${res.data.message}`); 153 - return; 154 - } 155 - } else { 156 - const res = await rpc.post("com.atproto.repo.putRecord", { 157 - input: { 158 - repo: agent()!.sub, 159 - collection: params.collection as `${string}.${string}.${string}`, 160 - rkey: params.rkey!, 161 - record: editedRecord, 162 - validate: validate(), 163 - }, 164 - }); 165 - if (!res.ok) { 166 - setNotice(`${res.data.error}: ${res.data.message}`); 167 - return; 168 - } 169 - } 170 - setOpenDialog(false); 171 - const id = addNotification({ 172 - message: "Record edited", 173 - type: "success", 174 - }); 175 - setTimeout(() => removeNotification(id), 3000); 176 - props.refetch(); 177 - } catch (err: any) { 178 - setNotice(err.message); 179 - } 180 - }; 181 - 182 - const insertTimestamp = () => { 183 - const timestamp = new Date().toISOString(); 184 - editorInstance.view.dispatch({ 185 - changes: { 186 - from: editorInstance.view.state.selection.main.head, 187 - insert: `"${timestamp}"`, 188 - }, 189 - }); 190 - setOpenInsertMenu(false); 191 - }; 192 - 193 - const MenuItem = (props: { icon: string; label: string; onClick: () => void }) => { 194 - return ( 195 - <button 196 - type="button" 197 - class="flex items-center gap-2 rounded-md p-2 text-left text-xs hover:bg-neutral-100 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 198 - onClick={props.onClick} 199 - > 200 - <span class={`iconify ${props.icon}`}></span> 201 - <span>{props.label}</span> 202 - </button> 203 - ); 204 - }; 205 - 206 - const FileUpload = (props: { file: File }) => { 207 - const [uploading, setUploading] = createSignal(false); 208 - const [error, setError] = createSignal(""); 209 - 210 - onCleanup(() => (blobInput.value = "")); 211 - 212 - const formatFileSize = (bytes: number) => { 213 - if (bytes === 0) return "0 Bytes"; 214 - const k = 1024; 215 - const sizes = ["Bytes", "KB", "MB", "GB"]; 216 - const i = Math.floor(Math.log(bytes) / Math.log(k)); 217 - return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i]; 218 - }; 219 - 220 - const uploadBlob = async () => { 221 - let blob: Blob; 222 - 223 - const mimetype = (document.getElementById("mimetype") as HTMLInputElement)?.value; 224 - (document.getElementById("mimetype") as HTMLInputElement).value = ""; 225 - if (mimetype) blob = new Blob([props.file], { type: mimetype }); 226 - else blob = props.file; 227 - 228 - if ((document.getElementById("exif-rm") as HTMLInputElement).checked) { 229 - const exifRemoved = remove(new Uint8Array(await blob.arrayBuffer())); 230 - if (exifRemoved !== null) blob = new Blob([exifRemoved], { type: blob.type }); 231 - } 232 - 233 - const rpc = new Client({ handler: agent()! }); 234 - setUploading(true); 235 - const res = await rpc.post("com.atproto.repo.uploadBlob", { 236 - input: blob, 237 - }); 238 - setUploading(false); 239 - if (!res.ok) { 240 - setError(res.data.error); 241 - return; 242 - } 243 - editorInstance.view.dispatch({ 244 - changes: { 245 - from: editorInstance.view.state.selection.main.head, 246 - insert: JSON.stringify(res.data.blob, null, 2), 247 - }, 248 - }); 249 - setOpenUpload(false); 250 - }; 251 - 252 - return ( 253 - <div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-70 left-[50%] w-[20rem] -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-opacity duration-200 dark:border-neutral-700 starting:opacity-0"> 254 - <h2 class="mb-2 font-semibold">Upload blob</h2> 255 - <div class="flex flex-col gap-2 text-sm"> 256 - <div class="flex flex-col gap-1"> 257 - <p class="flex gap-1"> 258 - <span class="truncate">{props.file.name}</span> 259 - <span class="shrink-0 text-neutral-600 dark:text-neutral-400"> 260 - ({formatFileSize(props.file.size)}) 261 - </span> 262 - </p> 263 - </div> 264 - <div class="flex items-center gap-x-2"> 265 - <label for="mimetype" class="shrink-0 select-none"> 266 - MIME type 267 - </label> 268 - <TextInput id="mimetype" placeholder={props.file.type} /> 269 - </div> 270 - <div class="flex items-center gap-1"> 271 - <input id="exif-rm" type="checkbox" checked /> 272 - <label for="exif-rm" class="select-none"> 273 - Remove EXIF data 274 - </label> 275 - </div> 276 - <p class="text-xs text-neutral-600 dark:text-neutral-400"> 277 - Metadata will be pasted after the cursor 278 - </p> 279 - <Show when={error()}> 280 - <span class="text-red-500 dark:text-red-400">Error: {error()}</span> 281 - </Show> 282 - <div class="flex justify-between gap-2"> 283 - <Button onClick={() => setOpenUpload(false)}>Cancel</Button> 284 - <Show when={uploading()}> 285 - <div class="flex items-center gap-1"> 286 - <span class="iconify lucide--loader-circle animate-spin"></span> 287 - <span>Uploading</span> 288 - </div> 289 - </Show> 290 - <Show when={!uploading()}> 291 - <Button 292 - onClick={uploadBlob} 293 - class="dark:shadow-dark-700 flex items-center gap-1 rounded-lg bg-blue-500 px-2 py-1.5 text-xs text-white shadow-xs select-none hover:bg-blue-600 active:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 dark:active:bg-blue-400" 294 - > 295 - Upload 296 - </Button> 297 - </Show> 298 - </div> 299 - </div> 300 - </div> 301 - ); 302 - }; 303 - 304 - return ( 305 - <> 306 - <Modal 307 - open={openDialog()} 308 - onClose={() => setOpenDialog(false)} 309 - closeOnClick={false} 310 - nonBlocking={isMinimized()} 311 - > 312 - <div 313 - style="transform: translateX(-50%) translateZ(0);" 314 - classList={{ 315 - "dark:bg-dark-300 dark:shadow-dark-700 pointer-events-auto absolute top-18 left-1/2 flex flex-col rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-all duration-200 dark:border-neutral-700 starting:opacity-0": true, 316 - "w-[calc(100%-1rem)] max-w-3xl h-[65vh]": !isMaximized(), 317 - "w-[calc(100%-1rem)] max-w-7xl h-[85vh]": isMaximized(), 318 - hidden: isMinimized(), 319 - }} 320 - > 321 - <div class="mb-2 flex w-full justify-between text-base"> 322 - <div class="flex items-center gap-2"> 323 - <span class="font-semibold select-none"> 324 - {props.create ? "Creating" : "Editing"} record 325 - </span> 326 - </div> 327 - <div class="flex items-center gap-1"> 328 - <button 329 - type="button" 330 - onclick={() => setIsMinimized(true)} 331 - class="flex items-center rounded-lg p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 332 - > 333 - <span class="iconify lucide--minus"></span> 334 - </button> 335 - <button 336 - type="button" 337 - onclick={() => setIsMaximized(!isMaximized())} 338 - class="flex items-center rounded-lg p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 339 - > 340 - <span 341 - class={`iconify ${isMaximized() ? "lucide--minimize-2" : "lucide--maximize-2"}`} 342 - ></span> 343 - </button> 344 - <button 345 - id="close" 346 - onclick={() => setOpenDialog(false)} 347 - class="flex items-center rounded-lg p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 348 - > 349 - <span class="iconify lucide--x"></span> 350 - </button> 351 - </div> 352 - </div> 353 - <form ref={formRef} class="flex min-h-0 flex-1 flex-col gap-y-2"> 354 - <Show when={props.create}> 355 - <div class="flex flex-wrap items-center gap-1 text-sm"> 356 - <span>at://</span> 357 - <select 358 - class="dark:bg-dark-100 max-w-40 truncate rounded-lg border-[0.5px] border-neutral-300 bg-white px-1 py-1 select-none focus:outline-[1px] focus:outline-neutral-600 dark:border-neutral-600 dark:focus:outline-neutral-400" 359 - name="repo" 360 - id="repo" 361 - > 362 - <For each={Object.keys(sessions)}> 363 - {(session) => ( 364 - <option value={session} selected={session === agent()?.sub}> 365 - {sessions[session].handle ?? session} 366 - </option> 367 - )} 368 - </For> 369 - </select> 370 - <span>/</span> 371 - <TextInput 372 - id="collection" 373 - name="collection" 374 - placeholder="Collection (default: $type)" 375 - class={`w-40 placeholder:text-xs lg:w-52 ${collectionError() ? "border-red-500 focus:outline-red-500 dark:border-red-400 dark:focus:outline-red-400" : ""}`} 376 - onInput={(e) => { 377 - const value = e.currentTarget.value; 378 - if (!value || isNsid(value)) setCollectionError(""); 379 - else 380 - setCollectionError( 381 - "Invalid collection: use reverse domain format (e.g. app.bsky.feed.post)", 382 - ); 383 - }} 384 - /> 385 - <span>/</span> 386 - <TextInput 387 - id="rkey" 388 - name="rkey" 389 - placeholder="Record key (default: TID)" 390 - class={`w-40 placeholder:text-xs lg:w-52 ${rkeyError() ? "border-red-500 focus:outline-red-500 dark:border-red-400 dark:focus:outline-red-400" : ""}`} 391 - onInput={(e) => { 392 - const value = e.currentTarget.value; 393 - if (!value || isRecordKey(value)) setRkeyError(""); 394 - else setRkeyError("Invalid record key: 1-512 chars, use a-z A-Z 0-9 . _ ~ : -"); 395 - }} 396 - /> 397 - </div> 398 - <Show when={collectionError() || rkeyError()}> 399 - <div class="text-xs text-red-500 dark:text-red-400"> 400 - <div>{collectionError()}</div> 401 - <div>{rkeyError()}</div> 402 - </div> 403 - </Show> 404 - </Show> 405 - <div class="min-h-0 flex-1"> 406 - <Suspense 407 - fallback={ 408 - <div class="flex h-full items-center justify-center"> 409 - <span class="iconify lucide--loader-circle animate-spin text-xl"></span> 410 - </div> 411 - } 412 - > 413 - <Editor 414 - content={JSON.stringify( 415 - !props.create ? props.record 416 - : params.rkey ? placeholder() 417 - : defaultPlaceholder(), 418 - null, 419 - 2, 420 - )} 421 - /> 422 - </Suspense> 423 - </div> 424 - <div class="flex flex-col gap-2"> 425 - <Show when={notice()}> 426 - <div class="text-sm text-red-500 dark:text-red-400">{notice()}</div> 427 - </Show> 428 - <div class="flex justify-between gap-2"> 429 - <div class="relative" ref={insertMenuRef}> 430 - <button 431 - type="button" 432 - class="dark:hover:bg-dark-200 dark:shadow-dark-700 dark:active:bg-dark-100 flex w-fit rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-1.5 text-base shadow-xs hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800" 433 - onClick={() => setOpenInsertMenu(!openInsertMenu())} 434 - > 435 - <span class="iconify lucide--plus select-none"></span> 436 - </button> 437 - <Show when={openInsertMenu()}> 438 - <div class="dark:bg-dark-300 dark:shadow-dark-700 absolute bottom-full left-0 z-10 mb-1 flex w-40 flex-col rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-1.5 shadow-md dark:border-neutral-700"> 439 - <MenuItem 440 - icon="lucide--upload" 441 - label="Upload blob" 442 - onClick={() => { 443 - setOpenInsertMenu(false); 444 - blobInput.click(); 445 - }} 446 - /> 447 - <MenuItem 448 - icon="lucide--clock" 449 - label="Insert timestamp" 450 - onClick={insertTimestamp} 451 - /> 452 - </div> 453 - </Show> 454 - <input 455 - type="file" 456 - id="blob" 457 - class="sr-only" 458 - ref={blobInput} 459 - onChange={(e) => { 460 - if (e.target.files !== null) setOpenUpload(true); 461 - }} 462 - /> 463 - </div> 464 - <Modal 465 - open={openUpload()} 466 - onClose={() => setOpenUpload(false)} 467 - closeOnClick={false} 468 - > 469 - <FileUpload file={blobInput.files![0]} /> 470 - </Modal> 471 - <div class="flex items-center justify-end gap-2"> 472 - <button 473 - type="button" 474 - class="flex items-center gap-1 rounded-sm p-1.5 text-xs hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 475 - onClick={() => 476 - setValidate( 477 - validate() === true ? false 478 - : validate() === false ? undefined 479 - : true, 480 - ) 481 - } 482 - > 483 - <Tooltip text={getValidateLabel()}> 484 - <span class={`iconify ${getValidateIcon()}`}></span> 485 - </Tooltip> 486 - <span>Validate</span> 487 - </button> 488 - <Show when={!props.create}> 489 - <Button onClick={() => editRecord(true)}>Recreate</Button> 490 - </Show> 491 - <Button 492 - onClick={() => 493 - props.create ? createRecord(new FormData(formRef)) : editRecord() 494 - } 495 - > 496 - {props.create ? "Create" : "Edit"} 497 - </Button> 498 - </div> 499 - </div> 500 - </div> 501 - </form> 502 - </div> 503 - </Modal> 504 - <Show when={isMinimized() && openDialog()}> 505 - <button 506 - class="dark:bg-dark-300 dark:hover:bg-dark-200 dark:active:bg-dark-100 fixed right-4 bottom-4 z-30 flex items-center gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 px-3 py-2 shadow-md hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700" 507 - onclick={() => setIsMinimized(false)} 508 - > 509 - <span class="iconify lucide--square-pen text-lg"></span> 510 - <span class="text-sm font-medium">{props.create ? "Creating" : "Editing"} record</span> 511 - </button> 512 - </Show> 513 - <Tooltip text={`${props.create ? "Create" : "Edit"} record`}> 514 - <button 515 - class={`flex items-center p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600 ${props.create ? "rounded-lg" : "rounded-sm"}`} 516 - onclick={() => { 517 - setNotice(""); 518 - setOpenDialog(true); 519 - setIsMinimized(false); 520 - }} 521 - > 522 - <div 523 - class={props.create ? "iconify lucide--square-pen text-lg" : "iconify lucide--pencil"} 524 - /> 525 - </button> 526 - </Tooltip> 527 - </> 528 - ); 529 - };
+7 -2
src/components/dropdown.tsx
··· 75 75 export const ActionMenu = (props: { 76 76 label: string; 77 77 icon: string; 78 - onClick: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>; 78 + onClick: () => void; 79 79 }) => { 80 + const ctx = useContext(MenuContext); 81 + 80 82 return ( 81 83 <button 82 - onClick={props.onClick} 84 + onClick={() => { 85 + props.onClick(); 86 + ctx?.setShowMenu(false); 87 + }} 83 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" 84 89 > 85 90 <Show when={props.icon}>
+2 -1
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 - import { editorInstance } from "./create"; 10 + import { editorInstance } from "./create/state"; 11 11 12 12 const Editor = (props: { content: string }) => { 13 13 let editorDiv!: HTMLDivElement; ··· 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 }); 53 54 editorInstance.view = view;
+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-md p-1 hover:bg-neutral-100 active:bg-neutral-200 dark:hover:bg-neutral-600 dark:active:bg-neutral-500" 87 - > 88 - <span class="iconify lucide--log-in"></span> 89 - </button> 90 - </div> 91 - <Show when={notice()}> 92 - <div class="text-sm">{notice()}</div> 93 - </Show> 94 - </form> 95 - ); 96 - }; 97 - 98 - const retrieveSession = async () => { 99 - const init = async (): Promise<Session | undefined> => { 100 - const params = new URLSearchParams(location.hash.slice(1)); 101 - 102 - if (params.has("state") && (params.has("code") || params.has("error"))) { 103 - history.replaceState(null, "", location.pathname + location.search); 104 - 105 - const auth = await finalizeAuthorization(params); 106 - const did = auth.session.info.sub; 107 - 108 - localStorage.setItem("lastSignedIn", did); 109 - 110 - const sessions = localStorage.getItem("sessions"); 111 - const newSessions: Sessions = sessions ? JSON.parse(sessions) : { [did]: {} }; 112 - newSessions[did] = { signedIn: true }; 113 - localStorage.setItem("sessions", JSON.stringify(newSessions)); 114 - return auth.session; 115 - } else { 116 - const lastSignedIn = localStorage.getItem("lastSignedIn"); 117 - 118 - if (lastSignedIn) { 119 - const sessions = localStorage.getItem("sessions"); 120 - const newSessions: Sessions = sessions ? JSON.parse(sessions) : {}; 121 - try { 122 - const session = await getSession(lastSignedIn as Did); 123 - const rpc = new Client({ handler: new OAuthUserAgent(session) }); 124 - const res = await rpc.get("com.atproto.server.getSession"); 125 - newSessions[lastSignedIn].signedIn = true; 126 - localStorage.setItem("sessions", JSON.stringify(newSessions)); 127 - if (!res.ok) throw res.data.error; 128 - return session; 129 - } catch (err) { 130 - newSessions[lastSignedIn].signedIn = false; 131 - localStorage.setItem("sessions", JSON.stringify(newSessions)); 132 - throw err; 133 - } 134 - } 135 - } 136 - }; 137 - 138 - const session = await init(); 139 - 140 - if (session) setAgent(new OAuthUserAgent(session)); 141 - }; 142 - 143 - export { Login, retrieveSession };
+28 -28
src/components/navbar.tsx
··· 18 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
+32 -12
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 ··· 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" ··· 292 312 src={actor.avatar?.replace("img/avatar/", "img/avatar_thumbnail/")} 293 313 class="size-9 rounded-full" 294 314 /> 295 - <div class="flex flex-col"> 315 + <div class="flex min-w-0 flex-col"> 296 316 <Show when={actor.displayName}> 297 - <span class="text-sm font-medium">{actor.displayName}</span> 317 + <span class="truncate text-sm font-medium">{actor.displayName}</span> 298 318 </Show> 299 - <span class="text-xs text-neutral-600 dark:text-neutral-400"> 319 + <span class="truncate text-xs text-neutral-600 dark:text-neutral-400"> 300 320 @{actor.handle} 301 321 </span> 302 322 </div> ··· 362 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" 363 383 onClick={() => setOpenList(true)} 364 384 > 365 - <span class="iconify lucide--help-circle"></span> 385 + <span class="iconify lucide--help-circle text-neutral-600 dark:text-neutral-300"></span> 366 386 </button> 367 387 </> 368 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}
+31 -10
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"; ··· 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 id="main" class="mx-auto mb-8 flex max-w-lg flex-col items-center p-4"> 121 + <div id="main" class="mx-auto mb-8 flex max-w-lg flex-col items-center p-3"> 108 122 <MetaProvider> 109 123 <Show when={location.pathname !== "/"}> 110 124 <Meta name="robots" content="noindex, nofollow" /> ··· 128 142 <span>PDSls</span> 129 143 </A> 130 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"> 131 - <Show when={location.pathname !== "/"}> 132 - <SearchButton /> 133 - </Show> 134 - <Show when={agent()}> 145 + <SearchButton /> 146 + <Show when={hasUserScope("create")}> 135 147 <RecordEditor create={true} /> 136 148 </Show> 137 149 <AccountManager /> ··· 139 151 <DropdownMenu icon="lucide--menu text-lg" buttonClass="rounded-lg p-1.5"> 140 152 <NavMenu href="/jetstream" label="Jetstream" icon="lucide--radio-tower" /> 141 153 <NavMenu href="/firehose" label="Firehose" icon="lucide--droplet" /> 142 - <NavMenu href="/labels" label="Labels" icon="lucide--tags" /> 154 + <NavMenu href="/labels" label="Labels" icon="lucide--tag" /> 143 155 <NavMenu href="/settings" label="Settings" icon="lucide--settings" /> 144 156 <MenuSeparator /> 145 157 <NavMenu ··· 180 192 </Show> 181 193 </div> 182 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> 183 204 </div> 184 205 ); 185 206 };
+7 -1
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
-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 };
+24
src/utils/route-cache.ts
··· 1 + import { createStore } from "solid-js/store"; 2 + 3 + export interface CollectionCacheEntry { 4 + records: unknown[]; 5 + cursor: string | undefined; 6 + scrollY: number; 7 + reverse: boolean; 8 + } 9 + 10 + type RouteCache = Record<string, CollectionCacheEntry>; 11 + 12 + const [routeCache, setRouteCache] = createStore<RouteCache>({}); 13 + 14 + export const getCollectionCache = (key: string): CollectionCacheEntry | undefined => { 15 + return routeCache[key]; 16 + }; 17 + 18 + export const setCollectionCache = (key: string, entry: CollectionCacheEntry): void => { 19 + setRouteCache(key, entry); 20 + }; 21 + 22 + export const clearCollectionCache = (key: string): void => { 23 + setRouteCache(key, undefined!); 24 + };
+7 -6
src/views/blob.tsx
··· 1 - import { Client, CredentialManager } from "@atcute/client"; 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 w-full flex-col gap-0.5 font-mono text-xs wrap-anywhere"> 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="w-fit 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" : ""}
+97 -39
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, createMemo, createResource, createSignal, For, Show } 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, ··· 165 219 setCursor(undefined); 166 220 setOpenDelete(false); 167 221 setRecreate(false); 222 + clearCollectionCache(cacheKey()); 168 223 refetch(); 169 224 }; 170 225 ··· 198 253 <StickyOverlay> 199 254 <div class="flex w-full flex-col gap-2"> 200 255 <div class="flex items-center gap-1"> 201 - <Show when={agent() && agent()?.sub === did}> 256 + <Show when={agent() && agent()?.sub === did && hasUserScope("delete")}> 202 257 <div class="flex items-center"> 203 258 <Tooltip 204 259 text={batchDelete() ? "Cancel" : "Delete"} ··· 209 264 setLastSelected(undefined); 210 265 setBatchDelete(!batchDelete()); 211 266 }} 212 - class="flex items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 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" 213 268 > 214 269 <span 215 - class={`iconify text-lg ${batchDelete() ? "lucide--circle-x" : "lucide--trash-2"} `} 270 + class={`iconify ${batchDelete() ? "lucide--circle-x" : "lucide--trash-2"} `} 216 271 ></span> 217 272 </button> 218 273 } ··· 223 278 children={ 224 279 <button 225 280 onclick={() => selectAll()} 226 - class="flex items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 227 - > 228 - <span class="iconify lucide--copy-check text-lg"></span> 229 - </button> 230 - } 231 - /> 232 - <Tooltip 233 - text="Recreate" 234 - children={ 235 - <button 236 - onclick={() => { 237 - setRecreate(true); 238 - setOpenDelete(true); 239 - }} 240 - class="flex items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 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" 241 282 > 242 - <span class="iconify lucide--recycle text-lg text-green-500 dark:text-green-400"></span> 283 + <span class="iconify lucide--copy-check"></span> 243 284 </button> 244 285 } 245 286 /> 287 + <Show when={hasUserScope("create")}> 288 + <Tooltip 289 + text="Recreate" 290 + children={ 291 + <button 292 + onclick={() => { 293 + setRecreate(true); 294 + setOpenDelete(true); 295 + }} 296 + class="flex items-center rounded-md p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 297 + > 298 + <span class="iconify lucide--recycle text-green-500 dark:text-green-400"></span> 299 + </button> 300 + } 301 + /> 302 + </Show> 246 303 <Tooltip 247 304 text="Delete" 248 305 children={ ··· 251 308 setRecreate(false); 252 309 setOpenDelete(true); 253 310 }} 254 - class="flex items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 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" 255 312 > 256 - <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> 257 314 </button> 258 315 } 259 316 /> ··· 277 334 </div> 278 335 </Modal> 279 336 </Show> 280 - <Tooltip text="Jetstream"> 281 - <A 282 - href={`/jetstream?collections=${params.collection}&dids=${params.repo}`} 283 - class="flex items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 284 - > 285 - <span class="iconify lucide--radio-tower text-lg"></span> 286 - </A> 287 - </Tooltip> 288 337 <TextInput 289 338 name="Filter" 290 339 placeholder="Filter by substring" 291 340 onInput={(e) => setFilter(e.currentTarget.value)} 292 341 class="grow" 293 342 /> 343 + <Tooltip text="Jetstream"> 344 + <A 345 + href={`/jetstream?collections=${params.collection}&dids=${params.repo}`} 346 + class="flex items-center rounded-md p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 347 + > 348 + <span class="iconify lucide--radio-tower"></span> 349 + </A> 350 + </Tooltip> 294 351 </div> 295 352 <Show when={records.length > 1}> 296 353 <div class="flex items-center justify-between gap-x-2"> ··· 299 356 setReverse(!reverse()); 300 357 setRecords([]); 301 358 setCursor(undefined); 359 + clearCollectionCache(cacheKey()); 302 360 refetch(); 303 361 }} 304 362 > ··· 346 404 </label> 347 405 </Show> 348 406 <Show when={!batchDelete()}> 349 - <A href={`/at://${did}/${params.collection}/${record.rkey}`}> 407 + <A href={`/at://${did}/${params.collection}/${record.rkey}`} class="select-none"> 350 408 <RecordLink record={record} /> 351 409 </A> 352 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" />
+3 -3
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"; ··· 158 158 await resolvePDS(did); 159 159 if (!labelerCache[did]) throw new Error("Repository is not a labeler"); 160 160 rpc = new Client({ 161 - handler: new CredentialManager({ service: labelerCache[did] }), 161 + handler: simpleFetchHandler({ service: labelerCache[did] }), 162 162 }); 163 163 164 164 setSearchParams({ did, uriPatterns }); ··· 228 228 rows={2} 229 229 value={searchParams.uriPatterns ?? "*"} 230 230 placeholder="at://did:web:example.com/app.bsky.feed.post/*" 231 - class="dark:bg-dark-100 dark:inset-shadow-dark-200 grow rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 py-1.5 text-sm inset-shadow-xs focus:outline-[1px] focus:outline-neutral-600 dark:border-neutral-600 dark:focus:outline-neutral-400" 231 + class="dark:bg-dark-100 grow rounded-lg bg-white px-2 py-1.5 text-sm outline-1 outline-neutral-200 focus:outline-[1.5px] focus:outline-neutral-600 dark:outline-neutral-600 dark:focus:outline-neutral-400" 232 232 /> 233 233 </label> 234 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}>
+57 -39
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 68 <div class="flex items-center gap-0.5"> ··· 69 78 </Tooltip> 70 79 </Show> 71 80 <button 72 - onclick={() => setOpenInfo(true)} 81 + onclick={() => { 82 + setOpenInfo(true); 83 + if (!handle()) fetchHandle(); 84 + }} 73 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 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"> ··· 86 98 </button> 87 99 </div> 88 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> 89 103 <span class="font-medium">Head:</span> 90 104 <span class="wrap-anywhere text-neutral-700 dark:text-neutral-300">{repo.head}</span> 91 105 ··· 122 136 ); 123 137 }; 124 138 125 - const Tab = (props: { tab: "repos" | "info"; label: string }) => ( 139 + const Tab = (props: { tab: "repos" | "info" | "firehose"; label: string }) => ( 126 140 <A 127 141 classList={{ 128 - "border-b-2": true, 129 - "border-transparent hover:border-neutral-400 dark:hover:border-neutral-600": 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": 130 144 (!!location.hash && location.hash !== `#${props.tab}`) || 131 145 (!location.hash && props.tab !== "repos"), 132 146 }} 133 - href={`/${params.pds}#${props.tab}`} 147 + href={ 148 + props.tab === "firehose" ? 149 + `/firehose?instance=wss://${params.pds}` 150 + : `/${params.pds}#${props.tab}` 151 + } 134 152 > 135 153 {props.label} 136 154 </A> ··· 138 156 139 157 return ( 140 158 <Show when={repos() || response()}> 141 - <div class="flex w-full flex-col"> 142 - <div class="dark:shadow-dark-700 dark:bg-dark-300 mb-2 flex w-full justify-between rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 text-sm shadow-xs dark:border-neutral-700"> 143 - <div class="ml-1 flex items-center gap-3"> 144 - <Tab tab="repos" label="Repositories" /> 145 - <Tab tab="info" label="Info" /> 146 - </div> 147 - <MenuProvider> 148 - <DropdownMenu icon="lucide--ellipsis-vertical" buttonClass="rounded-sm p-1.5"> 149 - <CopyMenu content={params.pds!} label="Copy PDS" icon="lucide--copy" /> 150 - <NavMenu 151 - href={`/firehose?instance=wss://${params.pds}`} 152 - label="Firehose" 153 - icon="lucide--radio-tower" 154 - /> 155 - </DropdownMenu> 156 - </MenuProvider> 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" /> 157 164 </div> 158 - <div class="flex flex-col gap-1 px-2"> 159 - <Show when={!location.hash || location.hash === "#repos"}> 160 - <div class="flex flex-col divide-y-[0.5px] divide-neutral-300 dark:divide-neutral-700"> 161 - <For each={repos()}>{(repo) => <RepoCard {...repo} />}</For> 162 - </div> 163 - </Show> 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"> 164 171 <Show when={location.hash === "#info"}> 165 172 <Show when={version()}> 166 173 {(version) => ( 167 - <div class="flex items-baseline gap-x-1"> 174 + <div class="flex flex-col"> 168 175 <span class="font-semibold">Version</span> 169 - <span class="truncate text-sm">{version()}</span> 176 + <span class="text-sm text-neutral-700 dark:text-neutral-300">{version()}</span> 170 177 </div> 171 178 )} 172 179 </Show> 173 180 <Show when={serverInfos()}> 174 181 {(server) => ( 175 182 <> 176 - <div class="flex items-baseline gap-x-1"> 183 + <div class="flex flex-col"> 177 184 <span class="font-semibold">DID</span> 178 - <span class="truncate text-sm">{server().did}</span> 185 + <span class="text-sm">{server().did}</span> 179 186 </div> 180 - <Show when={server().inviteCodeRequired}> 187 + <div class="flex items-center gap-1"> 181 188 <span class="font-semibold">Invite Code Required</span> 182 - </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> 183 198 <Show when={server().phoneVerificationRequired}> 184 - <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> 185 203 </Show> 186 204 <Show when={server().availableUserDomains.length}> 187 205 <div class="flex flex-col"> ··· 232 250 </div> 233 251 </div> 234 252 <Show when={!location.hash || location.hash === "#repos"}> 235 - <div class="dark:bg-dark-500 fixed bottom-0 z-5 flex w-screen justify-center bg-neutral-100 py-2"> 253 + <div class="dark:bg-dark-500 fixed bottom-0 z-5 flex w-screen justify-center bg-neutral-100 pt-2 pb-4"> 236 254 <div class="flex flex-col items-center gap-1 pb-2"> 237 255 <p>{repos()?.length} loaded</p> 238 256 <Show when={!response.loading && cursor()}>
+39 -45
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 - "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 items-center 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 422 <DropdownMenu icon="lucide--ellipsis-vertical" buttonClass="rounded-sm p-1.5"> ··· 485 490 <Show when={location.hash === "#info"}> 486 491 <div class="flex w-full flex-col gap-2 px-2 text-sm"> 487 492 <div> 488 - <div class="flex items-center gap-1"> 489 - <span class="iconify lucide--at-sign"></span> 490 - <p class="font-semibold">AT URI</p> 491 - </div> 493 + <p class="font-semibold">AT URI</p> 492 494 <div class="truncate text-xs">{record()?.uri}</div> 493 495 </div> 494 496 <Show when={record()?.cid}> 495 497 <div> 496 - <div class="flex items-center gap-1"> 497 - <span class="iconify lucide--box"></span> 498 - <p class="font-semibold">CID</p> 499 - </div> 498 + <p class="font-semibold">CID</p> 500 499 <div class="truncate text-left text-xs" dir="rtl"> 501 500 {record()?.cid} 502 501 </div> ··· 504 503 </Show> 505 504 <div> 506 505 <div class="flex items-center gap-1"> 507 - <span class="iconify lucide--lock-keyhole"></span> 508 506 <p class="font-semibold">Record verification</p> 509 507 <span 510 508 classList={{ ··· 521 519 </div> 522 520 <div> 523 521 <div class="flex items-center gap-1"> 524 - <span class="iconify lucide--file-check"></span> 525 522 <p class="font-semibold">Schema validation</p> 526 523 <span 527 524 classList={{ ··· 551 548 </div> 552 549 <Show when={lexiconUri()}> 553 550 <div> 554 - <div class="flex items-center gap-1"> 555 - <span class="iconify lucide--scroll-text"></span> 556 - <p class="font-semibold">Lexicon schema</p> 557 - </div> 551 + <p class="font-semibold">Lexicon schema</p> 558 552 <div class="truncate text-xs"> 559 553 <A 560 554 href={`/${lexiconUri()}`}
+145 -152
src/views/repo.tsx
··· 1 - import { Client, CredentialManager } from "@atcute/client"; 1 + import { Client, simpleFetchHandler } from "@atcute/client"; 2 2 import { DidDocument } from "@atcute/identity"; 3 3 import { ActorIdentifier, Did, Handle, Nsid } from "@atcute/lexicons"; 4 4 import { A, useLocation, useNavigate, useParams } from "@solidjs/router"; ··· 88 88 return ( 89 89 <A 90 90 classList={{ 91 - "border-b-2": true, 92 - "border-transparent hover:border-neutral-400 dark:hover:border-neutral-600": !isActive(), 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(), 93 94 }} 94 95 href={`/at://${params.repo}#${props.tab}`} 95 96 > ··· 139 140 return {}; 140 141 } 141 142 142 - rpc = new Client({ handler: new CredentialManager({ service: pds }) }); 143 + rpc = new Client({ handler: simpleFetchHandler({ service: pds }) }); 143 144 try { 144 145 const res = await rpc.get("com.atproto.repo.describeRepo", { 145 146 params: { repo: did as ActorIdentifier }, ··· 211 212 let loaded = 0; 212 213 213 214 const reader = response.body?.getReader(); 214 - const chunks: Uint8Array[] = []; 215 + const chunks: BlobPart[] = []; 215 216 216 217 if (reader) { 217 218 while (true) { ··· 275 276 return ( 276 277 <Show when={repo()}> 277 278 <div class="flex w-full flex-col gap-3 wrap-break-word"> 278 - <div class="dark:shadow-dark-700 dark:bg-dark-300 flex justify-between rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 text-sm shadow-xs dark:border-neutral-700"> 279 - <div class="ml-1 flex items-center gap-2 text-xs sm:gap-4 sm:text-sm"> 279 + <div class="flex justify-between px-2 text-sm sm:text-base"> 280 + <div class="flex items-center gap-3 sm:gap-4"> 280 281 <Show when={!error()}> 281 282 <RepoTab tab="collections" label="Collections" /> 282 283 </Show> ··· 289 290 </Show> 290 291 <RepoTab tab="backlinks" label="Backlinks" /> 291 292 </div> 292 - <div class="flex gap-0.5"> 293 + <div class="flex gap-1"> 293 294 <Show when={error() && error() !== "Missing PDS"}> 294 295 <div class="flex items-center gap-1 text-red-500 dark:text-red-400"> 295 296 <span class="iconify lucide--alert-triangle"></span> 296 297 <span>{error()}</span> 297 298 </div> 298 299 </Show> 299 - <Show when={!error() && (!location.hash || location.hash.startsWith("#collections"))}> 300 - <Tooltip text="Filter collections"> 301 - <button 302 - class="flex items-center rounded-sm p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 303 - onClick={() => setShowFilter(!showFilter())} 304 - > 305 - <span class="iconify lucide--filter"></span> 306 - </button> 307 - </Tooltip> 308 - </Show> 309 300 <MenuProvider> 310 301 <DropdownMenu icon="lucide--ellipsis-vertical" buttonClass="rounded-sm p-1.5"> 302 + <Show 303 + when={!error() && (!location.hash || location.hash.startsWith("#collections"))} 304 + > 305 + <ActionMenu 306 + label="Filter collections" 307 + icon="lucide--filter" 308 + onClick={() => setShowFilter(!showFilter())} 309 + /> 310 + </Show> 311 311 <CopyMenu content={params.repo!} label="Copy DID" icon="lucide--copy" /> 312 312 <NavMenu 313 313 href={`/jetstream?dids=${params.repo}`} ··· 323 323 </Show> 324 324 <Show when={error()?.length === 0 || error() === undefined}> 325 325 <ActionMenu 326 - label="Export Repo" 326 + label="Export repo" 327 327 icon={downloading() ? "lucide--loader-circle animate-spin" : "lucide--download"} 328 328 onClick={() => downloadRepo()} 329 329 /> ··· 336 336 : `https://${did.split("did:web:")[1]}/.well-known/did.json` 337 337 } 338 338 newTab 339 - label="DID Document" 339 + label="DID document" 340 340 icon="lucide--external-link" 341 341 /> 342 342 <Show when={did.startsWith("did:plc")}> 343 343 <NavMenu 344 344 href={`${localStorage.plcDirectory ?? "https://plc.directory"}/${did}/log/audit`} 345 345 newTab 346 - label="Audit Log" 346 + label="Audit log" 347 347 icon="lucide--external-link" 348 348 /> 349 349 </Show> ··· 404 404 /> 405 405 </Show> 406 406 <div class="flex flex-col text-sm wrap-anywhere" classList={{ "-mt-1": !showFilter() }}> 407 - <For 408 - each={Object.keys(nsids() ?? {}).filter((authority) => 409 - filter() ? 410 - authority.includes(filter()!) || 411 - nsids()?.[authority].nsids.some((nsid) => 412 - `${authority}.${nsid}`.includes(filter()!), 413 - ) 414 - : true, 415 - )} 407 + <Show 408 + when={Object.keys(nsids() ?? {}).length != 0} 409 + fallback={<span class="mt-3 text-center text-base">No collections found.</span>} 416 410 > 417 - {(authority) => { 418 - const reversedDomain = authority.split(".").reverse().join("."); 419 - 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); 420 424 421 - const isHighlighted = () => location.hash === `#collections:${authority}`; 425 + const isHighlighted = () => location.hash === `#collections:${authority}`; 422 426 423 - return ( 424 - <div 425 - id={`collection-${authority}`} 426 - class="group flex items-start gap-2 rounded-lg p-1 transition-colors" 427 - classList={{ 428 - "dark:hover:bg-dark-200 hover:bg-neutral-200": !isHighlighted(), 429 - "bg-blue-100 dark:bg-blue-500/25": isHighlighted(), 430 - }} 431 - > 432 - <a 433 - href={`#collections:${authority}`} 434 - class="relative flex h-5 w-4 shrink-0 items-center justify-center hover:opacity-70" 427 + return ( 428 + <div 429 + id={`collection-${authority}`} 430 + class="group flex items-start gap-2 rounded-lg p-1 transition-colors" 431 + classList={{ 432 + "dark:hover:bg-dark-200 hover:bg-neutral-200": !isHighlighted(), 433 + "bg-blue-100 dark:bg-blue-500/25": isHighlighted(), 434 + }} 435 435 > 436 - <span class="absolute top-1/2 -left-5 flex -translate-y-1/2 items-center text-base opacity-0 transition-opacity group-hover:opacity-100"> 437 - <span class="iconify lucide--link absolute -left-2 w-7"></span> 438 - </span> 439 - <Show when={!faviconLoaded()}> 440 - <span class="iconify lucide--globe size-4 text-neutral-400 dark:text-neutral-500" /> 441 - </Show> 442 - <img 443 - src={ 444 - ["bsky.app", "bsky.chat"].includes(reversedDomain) ? 445 - "https://web-cdn.bsky.app/static/apple-touch-icon.png" 446 - : `https://${reversedDomain}/favicon.ico` 447 - } 448 - alt={`${reversedDomain} favicon`} 449 - class="h-4 w-4" 450 - classList={{ hidden: !faviconLoaded() }} 451 - onLoad={() => setFaviconLoaded(true)} 452 - onError={() => setFaviconLoaded(false)} 453 - /> 454 - </a> 455 - <div class="flex flex-1 flex-col"> 456 - <For 457 - each={nsids()?.[authority].nsids.filter((nsid) => 458 - filter() ? `${authority}.${nsid}`.includes(filter()!) : true, 459 - )} 436 + <a 437 + href={`#collections:${authority}`} 438 + class="relative flex h-5 w-4 shrink-0 items-center justify-center hover:opacity-70" 460 439 > 461 - {(nsid) => ( 462 - <A 463 - href={`/at://${did}/${authority}.${nsid}`} 464 - class="hover:underline active:underline" 465 - > 466 - <span>{authority}</span> 467 - <span class="text-neutral-500 dark:text-neutral-400">.{nsid}</span> 468 - </A> 469 - )} 470 - </For> 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> 471 476 </div> 472 - </div> 473 - ); 474 - }} 475 - </For> 477 + ); 478 + }} 479 + </For> 480 + </Show> 476 481 </div> 477 482 </Show> 478 483 <Show when={location.hash === "#identity" || (error() && !location.hash)}> ··· 481 486 <div class="flex flex-col gap-3 wrap-anywhere"> 482 487 {/* ID Section */} 483 488 <div> 484 - <div class="flex items-center gap-1"> 485 - <div class="iconify lucide--id-card" /> 486 - <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} 487 492 </div> 488 - <div class="text-sm">{didDocument().id}</div> 489 493 </div> 490 494 491 495 {/* Aliases Section */} 492 496 <div> 493 - <div class="flex items-center gap-1"> 494 - <div class="iconify lucide--at-sign" /> 495 - <p class="font-semibold">Aliases</p> 496 - </div> 497 - <div class="flex flex-col gap-0.5"> 498 - <For each={didDocument().alsoKnownAs}> 499 - {(alias) => ( 500 - <div class="flex items-center gap-1 text-sm"> 501 - <span>{alias}</span> 502 - <Show when={alias.startsWith("at://")}> 503 - <Tooltip 504 - text={ 505 - validHandles[alias] === true ? "Valid handle" 506 - : validHandles[alias] === undefined ? 507 - "Validating" 508 - : "Invalid handle" 509 - } 510 - > 511 - <span 512 - classList={{ 513 - "iconify lucide--circle-check text-green-600 dark:text-green-400": 514 - validHandles[alias] === true, 515 - "iconify lucide--circle-x text-red-500 dark:text-red-400": 516 - validHandles[alias] === false, 517 - "iconify lucide--loader-circle animate-spin": 518 - validHandles[alias] === undefined, 519 - }} 520 - ></span> 521 - </Tooltip> 522 - </Show> 523 - </div> 524 - )} 525 - </For> 526 - </div> 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> 527 526 </div> 528 527 529 528 {/* Services Section */} 530 529 <div> 531 - <div class="flex items-center gap-1"> 532 - <div class="iconify lucide--hard-drive" /> 533 - <p class="font-semibold">Services</p> 534 - </div> 535 - <div class="flex flex-col gap-0.5"> 530 + <p class="font-semibold">Services</p> 531 + <div class="flex flex-col gap-1"> 536 532 <For each={didDocument().service}> 537 533 {(service) => ( 538 - <div class="text-sm"> 539 - <div class="font-medium text-neutral-700 dark:text-neutral-300"> 540 - #{service.id.split("#")[1]} 541 - </div> 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> 542 538 <a 543 - class="underline hover:text-blue-400" 539 + class="w-fit underline hover:text-blue-400" 544 540 href={service.serviceEndpoint.toString()} 545 541 target="_blank" 546 542 rel="noopener" ··· 555 551 556 552 {/* Verification Methods Section */} 557 553 <div> 558 - <div class="flex items-center gap-1"> 559 - <div class="iconify lucide--shield-check" /> 560 - <p class="font-semibold">Verification Methods</p> 561 - </div> 562 - <div class="flex flex-col gap-0.5"> 554 + <p class="font-semibold">Verification Methods</p> 555 + <div class="flex flex-col gap-1"> 563 556 <For each={didDocument().verificationMethod}> 564 557 {(verif) => ( 565 558 <Show when={verif.publicKeyMultibase}> 566 559 {(key) => ( 567 - <div class="text-sm"> 568 - <div class="flex items-baseline gap-1"> 569 - <span class="font-medium text-neutral-700 dark:text-neutral-300"> 570 - #{verif.id.split("#")[1]} 571 - </span> 572 - <span class="rounded bg-neutral-200 px-1 py-0.5 text-xs text-neutral-800 dark:bg-neutral-700 dark:text-neutral-300"> 573 - {detectKeyType(key())} 574 - </span> 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> 575 568 </div> 569 + <span></span> 576 570 <div class="font-mono break-all">{key()}</div> 577 571 </div> 578 572 )} ··· 585 579 {/* Rotation Keys Section */} 586 580 <Show when={rotationKeys().length > 0}> 587 581 <div> 588 - <div class="flex items-center gap-1"> 589 - <div class="iconify lucide--key-round" /> 590 - <p class="font-semibold">Rotation Keys</p> 591 - </div> 592 - <div class="flex flex-col gap-0.5"> 582 + <p class="font-semibold">Rotation Keys</p> 583 + <div class="flex flex-col gap-1"> 593 584 <For each={rotationKeys()}> 594 585 {(key) => ( 595 - <div class="text-sm"> 596 - <span class="rounded bg-neutral-200 px-1 py-0.5 text-xs text-neutral-800 dark:bg-neutral-700 dark:text-neutral-300"> 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"> 597 589 {detectDidKeyType(key)} 598 590 </span> 591 + <span></span> 599 592 <div class="font-mono break-all">{key.replace("did:key:", "")}</div> 600 593 </div> 601 594 )}
+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>