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

Compare changes

Choose any two refs to compare.

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

This is a binary file and will not be displayed.

public/cursor.cur

This is a binary file and will not be displayed.

public/headers/almaty.webp

This is a binary file and will not be displayed.

public/headers/aurora.jpg

This is a binary file and will not be displayed.

public/headers/bridge.jpg

This is a binary file and will not be displayed.

public/headers/bunny.jpg

This is a binary file and will not be displayed.

public/headers/city.webp

This is a binary file and will not be displayed.

public/headers/forest.jpg

This is a binary file and will not be displayed.

public/headers/puppy.jpg

This is a binary file and will not be displayed.

public/headers/water.webp

This is a binary file and will not be displayed.

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

This is a binary file and will not be displayed.

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