handy online tools for AT Protocol boat.kelinci.net
atproto bluesky atcute typescript solidjs
20
fork

Configure Feed

Select the types of activity you want to include in your feed.

initial commit

Mary 0470e518

+6268
+4
.gitignore
··· 1 + node_modules/ 2 + dist/ 3 + 4 + *.tsbuildinfo
+4
.npmrc
··· 1 + auto-install-peers=false 2 + public-hoist-pattern[]=workbox-window 3 + 4 + @jsr:registry=https://npm.jsr.io
+1
.prettierignore
··· 1 + pnpm-lock.yaml
+19
.prettierrc
··· 1 + { 2 + "trailingComma": "all", 3 + "useTabs": true, 4 + "tabWidth": 2, 5 + "printWidth": 110, 6 + "semi": true, 7 + "singleQuote": true, 8 + "bracketSpacing": true, 9 + "plugins": ["prettier-plugin-tailwindcss"], 10 + "tailwindFunctions": ["tw"], 11 + "overrides": [ 12 + { 13 + "files": ["tsconfig.json", "tsconfig.*.json", "jsconfig.json"], 14 + "options": { 15 + "parser": "jsonc" 16 + } 17 + } 18 + ] 19 + }
+3
.vscode/extensions.json
··· 1 + { 2 + "recommendations": ["bradlc.vscode-tailwindcss", "esbenp.prettier-vscode"] 3 + }
+5
.vscode/settings.json
··· 1 + { 2 + "editor.defaultFormatter": "esbenp.prettier-vscode", 3 + "typescript.tsdk": "node_modules/typescript/lib", 4 + "tailwindCSS.experimental.classRegex": ["tw`([^`]*)"] 5 + }
+28
README.md
··· 1 + ## Usage 2 + 3 + ```bash 4 + $ npm install # or pnpm install or yarn install 5 + ``` 6 + 7 + ### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs) 8 + 9 + ## Available Scripts 10 + 11 + In the project directory, you can run: 12 + 13 + ### `npm run dev` 14 + 15 + Runs the app in the development mode.<br> 16 + Open [http://localhost:5173](http://localhost:5173) to view it in the browser. 17 + 18 + ### `npm run build` 19 + 20 + Builds the app for production to the `dist` folder.<br> 21 + It correctly bundles Solid in production mode and optimizes the build for the best performance. 22 + 23 + The build is minified and the filenames include the hashes.<br> 24 + Your app is ready to be deployed! 25 + 26 + ## Deployment 27 + 28 + Learn more about deploying your application with the [documentations](https://vite.dev/guide/static-deploy.html)
+11
index.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="utf-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 + <title>boat</title> 7 + <script type="module" src="/src/main.tsx"></script> 8 + </head> 9 + <body> 10 + </body> 11 + </html>
+34
package.json
··· 1 + { 2 + "type": "module", 3 + "scripts": { 4 + "dev": "vite", 5 + "build": "tsc -b && vite build", 6 + "preview": "vite preview", 7 + "fmt": "prettier --cache --write ." 8 + }, 9 + "dependencies": { 10 + "@atcute/bluesky": "^1.0.7", 11 + "@atcute/car": "^1.1.0", 12 + "@atcute/cbor": "^1.0.5", 13 + "@atcute/cid": "^1.0.1", 14 + "@atcute/client": "^2.0.3", 15 + "@mary/events": "npm:@jsr/mary__events@^0.1.0", 16 + "@mary/solid-freeze": "npm:@externdefs/solid-freeze@^0.1.1", 17 + "@mary/tar": "npm:@jsr/mary__tar@^0.2.4", 18 + "nanoid": "^5.0.8", 19 + "native-file-system-adapter": "^3.0.1", 20 + "solid-js": "^1.9.2", 21 + "valibot": "1.0.0-beta.2" 22 + }, 23 + "devDependencies": { 24 + "@types/node": "^22.8.2", 25 + "autoprefixer": "^10.4.20", 26 + "prettier": "^3.3.3", 27 + "prettier-plugin-tailwindcss": "^0.6.8", 28 + "tailwindcss": "^3.4.14", 29 + "terser": "^5.36.0", 30 + "typescript": "5.7.0-beta", 31 + "vite": "^5.4.9", 32 + "vite-plugin-solid": "^2.10.2" 33 + } 34 + }
+2129
pnpm-lock.yaml
··· 1 + lockfileVersion: '9.0' 2 + 3 + settings: 4 + autoInstallPeers: false 5 + excludeLinksFromLockfile: false 6 + 7 + importers: 8 + 9 + .: 10 + dependencies: 11 + '@atcute/bluesky': 12 + specifier: ^1.0.7 13 + version: 1.0.7(@atcute/client@2.0.3) 14 + '@atcute/car': 15 + specifier: ^1.1.0 16 + version: 1.1.0 17 + '@atcute/cbor': 18 + specifier: ^1.0.5 19 + version: 1.0.5 20 + '@atcute/cid': 21 + specifier: ^1.0.1 22 + version: 1.0.1 23 + '@atcute/client': 24 + specifier: ^2.0.3 25 + version: 2.0.3 26 + '@mary/events': 27 + specifier: npm:@jsr/mary__events@^0.1.0 28 + version: '@jsr/mary__events@0.1.0' 29 + '@mary/solid-freeze': 30 + specifier: npm:@externdefs/solid-freeze@^0.1.1 31 + version: '@externdefs/solid-freeze@0.1.1(solid-js@1.9.3)' 32 + '@mary/tar': 33 + specifier: npm:@jsr/mary__tar@^0.2.4 34 + version: '@jsr/mary__tar@0.2.4' 35 + nanoid: 36 + specifier: ^5.0.8 37 + version: 5.0.8 38 + native-file-system-adapter: 39 + specifier: ^3.0.1 40 + version: 3.0.1 41 + solid-js: 42 + specifier: ^1.9.2 43 + version: 1.9.3 44 + valibot: 45 + specifier: 1.0.0-beta.2 46 + version: 1.0.0-beta.2(typescript@5.7.0-beta) 47 + devDependencies: 48 + '@types/node': 49 + specifier: ^22.8.2 50 + version: 22.8.2 51 + autoprefixer: 52 + specifier: ^10.4.20 53 + version: 10.4.20(postcss@8.4.47) 54 + prettier: 55 + specifier: ^3.3.3 56 + version: 3.3.3 57 + prettier-plugin-tailwindcss: 58 + specifier: ^0.6.8 59 + version: 0.6.8(prettier@3.3.3) 60 + tailwindcss: 61 + specifier: ^3.4.14 62 + version: 3.4.14 63 + terser: 64 + specifier: ^5.36.0 65 + version: 5.36.0 66 + typescript: 67 + specifier: 5.7.0-beta 68 + version: 5.7.0-beta 69 + vite: 70 + specifier: ^5.4.9 71 + version: 5.4.10(@types/node@22.8.2)(terser@5.36.0) 72 + vite-plugin-solid: 73 + specifier: ^2.10.2 74 + version: 2.10.2(solid-js@1.9.3)(vite@5.4.10(@types/node@22.8.2)(terser@5.36.0)) 75 + 76 + packages: 77 + 78 + '@alloc/quick-lru@5.2.0': 79 + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} 80 + engines: {node: '>=10'} 81 + 82 + '@ampproject/remapping@2.3.0': 83 + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} 84 + engines: {node: '>=6.0.0'} 85 + 86 + '@atcute/base32@1.0.0': 87 + resolution: {integrity: sha512-Mbjsv6kd/ymvDMGjCoh9eqhlpFsoJ6zYguU6xtKxqh1wGhe5rvBOfMRXsEqcp7srn8Bfp8QhevqLgmwrWvzqrA==} 88 + 89 + '@atcute/bluesky@1.0.7': 90 + resolution: {integrity: sha512-2jPHzl7WbcqRtcAXanJy4Lp638ujqnoGmPCPmBlmpEDP34D7EVKQqjN/mlvglb5n539dThA9xlSgIS8yOxwzDA==} 91 + peerDependencies: 92 + '@atcute/client': ^1.0.0 || ^2.0.0 93 + 94 + '@atcute/car@1.1.0': 95 + resolution: {integrity: sha512-Ayi8gilzgMMYZ1sqbHqqP52OOJlxrbsAxgAB3Kgz/NJvl9StlYKKlUQN580gZebsG0B+EYbhToQJYoCs3ioW+A==} 96 + 97 + '@atcute/cbor@1.0.5': 98 + resolution: {integrity: sha512-ckWn+ZErzeTgKBuklQfUpsOb5+uAtSJi68Z7+1wJMMEP7iO/V90IIlyTm+19ACuGEuY8SGrfUIWyZvBjhgCTYw==} 99 + 100 + '@atcute/cid@1.0.1': 101 + resolution: {integrity: sha512-92Cor2ruS7y+/wdFutp2qFDjJj4mTcO7HdZ/BhTQRg/nzWdAnTann5DAmYjD+IWRaXd5SYk4dOZnDt4lsGofzg==} 102 + 103 + '@atcute/client@2.0.3': 104 + resolution: {integrity: sha512-j9GryA5l+4F0BTQWa6/1XmsuSPSq+bqNCY3mrHUGD592hMqUZxgpYDLgRWL+719V287AW/56AwvFYlbjlENp7A==} 105 + 106 + '@atcute/varint@1.0.0': 107 + resolution: {integrity: sha512-NEBOGkdaDY8cjlDg49kefIsRM7iv/4oReEnOr3bN4tF3IxBGdc6Io1NCJz1xNBNdUL+3VDG3CKHiRji91HXaTg==} 108 + 109 + '@babel/code-frame@7.26.0': 110 + resolution: {integrity: sha512-INCKxTtbXtcNbUZ3YXutwMpEleqttcswhAdee7dhuoVrD2cnuc3PqtERBtxkX5nziX9vnBL8WXmSGwv8CuPV6g==} 111 + engines: {node: '>=6.9.0'} 112 + 113 + '@babel/compat-data@7.26.0': 114 + resolution: {integrity: sha512-qETICbZSLe7uXv9VE8T/RWOdIE5qqyTucOt4zLYMafj2MRO271VGgLd4RACJMeBO37UPWhXiKMBk7YlJ0fOzQA==} 115 + engines: {node: '>=6.9.0'} 116 + 117 + '@babel/core@7.26.0': 118 + resolution: {integrity: sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==} 119 + engines: {node: '>=6.9.0'} 120 + 121 + '@babel/generator@7.26.0': 122 + resolution: {integrity: sha512-/AIkAmInnWwgEAJGQr9vY0c66Mj6kjkE2ZPB1PurTRaRAh3U+J45sAQMjQDJdh4WbR3l0x5xkimXBKyBXXAu2w==} 123 + engines: {node: '>=6.9.0'} 124 + 125 + '@babel/helper-compilation-targets@7.25.9': 126 + resolution: {integrity: sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==} 127 + engines: {node: '>=6.9.0'} 128 + 129 + '@babel/helper-module-imports@7.18.6': 130 + resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} 131 + engines: {node: '>=6.9.0'} 132 + 133 + '@babel/helper-module-imports@7.25.9': 134 + resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} 135 + engines: {node: '>=6.9.0'} 136 + 137 + '@babel/helper-module-transforms@7.26.0': 138 + resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} 139 + engines: {node: '>=6.9.0'} 140 + peerDependencies: 141 + '@babel/core': ^7.0.0 142 + 143 + '@babel/helper-plugin-utils@7.25.9': 144 + resolution: {integrity: sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==} 145 + engines: {node: '>=6.9.0'} 146 + 147 + '@babel/helper-string-parser@7.25.9': 148 + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} 149 + engines: {node: '>=6.9.0'} 150 + 151 + '@babel/helper-validator-identifier@7.25.9': 152 + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} 153 + engines: {node: '>=6.9.0'} 154 + 155 + '@babel/helper-validator-option@7.25.9': 156 + resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} 157 + engines: {node: '>=6.9.0'} 158 + 159 + '@babel/helpers@7.26.0': 160 + resolution: {integrity: sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==} 161 + engines: {node: '>=6.9.0'} 162 + 163 + '@babel/parser@7.26.1': 164 + resolution: {integrity: sha512-reoQYNiAJreZNsJzyrDNzFQ+IQ5JFiIzAHJg9bn94S3l+4++J7RsIhNMoB+lgP/9tpmiAQqspv+xfdxTSzREOw==} 165 + engines: {node: '>=6.0.0'} 166 + hasBin: true 167 + 168 + '@babel/plugin-syntax-jsx@7.25.9': 169 + resolution: {integrity: sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==} 170 + engines: {node: '>=6.9.0'} 171 + peerDependencies: 172 + '@babel/core': ^7.0.0-0 173 + 174 + '@babel/template@7.25.9': 175 + resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} 176 + engines: {node: '>=6.9.0'} 177 + 178 + '@babel/traverse@7.25.9': 179 + resolution: {integrity: sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==} 180 + engines: {node: '>=6.9.0'} 181 + 182 + '@babel/types@7.26.0': 183 + resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==} 184 + engines: {node: '>=6.9.0'} 185 + 186 + '@esbuild/aix-ppc64@0.21.5': 187 + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} 188 + engines: {node: '>=12'} 189 + cpu: [ppc64] 190 + os: [aix] 191 + 192 + '@esbuild/android-arm64@0.21.5': 193 + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} 194 + engines: {node: '>=12'} 195 + cpu: [arm64] 196 + os: [android] 197 + 198 + '@esbuild/android-arm@0.21.5': 199 + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} 200 + engines: {node: '>=12'} 201 + cpu: [arm] 202 + os: [android] 203 + 204 + '@esbuild/android-x64@0.21.5': 205 + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} 206 + engines: {node: '>=12'} 207 + cpu: [x64] 208 + os: [android] 209 + 210 + '@esbuild/darwin-arm64@0.21.5': 211 + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} 212 + engines: {node: '>=12'} 213 + cpu: [arm64] 214 + os: [darwin] 215 + 216 + '@esbuild/darwin-x64@0.21.5': 217 + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} 218 + engines: {node: '>=12'} 219 + cpu: [x64] 220 + os: [darwin] 221 + 222 + '@esbuild/freebsd-arm64@0.21.5': 223 + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} 224 + engines: {node: '>=12'} 225 + cpu: [arm64] 226 + os: [freebsd] 227 + 228 + '@esbuild/freebsd-x64@0.21.5': 229 + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} 230 + engines: {node: '>=12'} 231 + cpu: [x64] 232 + os: [freebsd] 233 + 234 + '@esbuild/linux-arm64@0.21.5': 235 + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} 236 + engines: {node: '>=12'} 237 + cpu: [arm64] 238 + os: [linux] 239 + 240 + '@esbuild/linux-arm@0.21.5': 241 + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} 242 + engines: {node: '>=12'} 243 + cpu: [arm] 244 + os: [linux] 245 + 246 + '@esbuild/linux-ia32@0.21.5': 247 + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} 248 + engines: {node: '>=12'} 249 + cpu: [ia32] 250 + os: [linux] 251 + 252 + '@esbuild/linux-loong64@0.21.5': 253 + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} 254 + engines: {node: '>=12'} 255 + cpu: [loong64] 256 + os: [linux] 257 + 258 + '@esbuild/linux-mips64el@0.21.5': 259 + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} 260 + engines: {node: '>=12'} 261 + cpu: [mips64el] 262 + os: [linux] 263 + 264 + '@esbuild/linux-ppc64@0.21.5': 265 + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} 266 + engines: {node: '>=12'} 267 + cpu: [ppc64] 268 + os: [linux] 269 + 270 + '@esbuild/linux-riscv64@0.21.5': 271 + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} 272 + engines: {node: '>=12'} 273 + cpu: [riscv64] 274 + os: [linux] 275 + 276 + '@esbuild/linux-s390x@0.21.5': 277 + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} 278 + engines: {node: '>=12'} 279 + cpu: [s390x] 280 + os: [linux] 281 + 282 + '@esbuild/linux-x64@0.21.5': 283 + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} 284 + engines: {node: '>=12'} 285 + cpu: [x64] 286 + os: [linux] 287 + 288 + '@esbuild/netbsd-x64@0.21.5': 289 + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} 290 + engines: {node: '>=12'} 291 + cpu: [x64] 292 + os: [netbsd] 293 + 294 + '@esbuild/openbsd-x64@0.21.5': 295 + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} 296 + engines: {node: '>=12'} 297 + cpu: [x64] 298 + os: [openbsd] 299 + 300 + '@esbuild/sunos-x64@0.21.5': 301 + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} 302 + engines: {node: '>=12'} 303 + cpu: [x64] 304 + os: [sunos] 305 + 306 + '@esbuild/win32-arm64@0.21.5': 307 + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} 308 + engines: {node: '>=12'} 309 + cpu: [arm64] 310 + os: [win32] 311 + 312 + '@esbuild/win32-ia32@0.21.5': 313 + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} 314 + engines: {node: '>=12'} 315 + cpu: [ia32] 316 + os: [win32] 317 + 318 + '@esbuild/win32-x64@0.21.5': 319 + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} 320 + engines: {node: '>=12'} 321 + cpu: [x64] 322 + os: [win32] 323 + 324 + '@externdefs/solid-freeze@0.1.1': 325 + resolution: {integrity: sha512-duvZBfJB9oOLphx04ckKF534hP186xIBFaw4GHJ5fGeZY5syZs59UeumV5NC6aiEU9hVhAFMOnDDGkQrFqHrnQ==} 326 + peerDependencies: 327 + solid-js: ^1.8.5 328 + 329 + '@isaacs/cliui@8.0.2': 330 + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} 331 + engines: {node: '>=12'} 332 + 333 + '@jridgewell/gen-mapping@0.3.5': 334 + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} 335 + engines: {node: '>=6.0.0'} 336 + 337 + '@jridgewell/resolve-uri@3.1.2': 338 + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 339 + engines: {node: '>=6.0.0'} 340 + 341 + '@jridgewell/set-array@1.2.1': 342 + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} 343 + engines: {node: '>=6.0.0'} 344 + 345 + '@jridgewell/source-map@0.3.6': 346 + resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} 347 + 348 + '@jridgewell/sourcemap-codec@1.5.0': 349 + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} 350 + 351 + '@jridgewell/trace-mapping@0.3.25': 352 + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} 353 + 354 + '@jsr/mary__events@0.1.0': 355 + resolution: {integrity: sha512-oS6jVOaXTaNEa6avRncwrEtUYaBKrq/HEybPa9Z3aoeMs+RSly0vn0KcOj/fy2H6iTBkeh3wa8+/9nFjhKyKIg==, tarball: https://npm.jsr.io/~/11/@jsr/mary__events/0.1.0.tgz} 356 + 357 + '@jsr/mary__tar@0.2.4': 358 + resolution: {integrity: sha512-jFjPcZj8DRSukPLZOt6+h74cVFdfdTMG9gzbW67YByCJTD52PEpe2sNcfCSw4mQ8hZBNgwiufCPyYL8hR9yicA==, tarball: https://npm.jsr.io/~/11/@jsr/mary__tar/0.2.4.tgz} 359 + 360 + '@nodelib/fs.scandir@2.1.5': 361 + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} 362 + engines: {node: '>= 8'} 363 + 364 + '@nodelib/fs.stat@2.0.5': 365 + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} 366 + engines: {node: '>= 8'} 367 + 368 + '@nodelib/fs.walk@1.2.8': 369 + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} 370 + engines: {node: '>= 8'} 371 + 372 + '@pkgjs/parseargs@0.11.0': 373 + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} 374 + engines: {node: '>=14'} 375 + 376 + '@rollup/rollup-android-arm-eabi@4.24.2': 377 + resolution: {integrity: sha512-ufoveNTKDg9t/b7nqI3lwbCG/9IJMhADBNjjz/Jn6LxIZxD7T5L8l2uO/wD99945F1Oo8FvgbbZJRguyk/BdzA==} 378 + cpu: [arm] 379 + os: [android] 380 + 381 + '@rollup/rollup-android-arm64@4.24.2': 382 + resolution: {integrity: sha512-iZoYCiJz3Uek4NI0J06/ZxUgwAfNzqltK0MptPDO4OR0a88R4h0DSELMsflS6ibMCJ4PnLvq8f7O1d7WexUvIA==} 383 + cpu: [arm64] 384 + os: [android] 385 + 386 + '@rollup/rollup-darwin-arm64@4.24.2': 387 + resolution: {integrity: sha512-/UhrIxobHYCBfhi5paTkUDQ0w+jckjRZDZ1kcBL132WeHZQ6+S5v9jQPVGLVrLbNUebdIRpIt00lQ+4Z7ys4Rg==} 388 + cpu: [arm64] 389 + os: [darwin] 390 + 391 + '@rollup/rollup-darwin-x64@4.24.2': 392 + resolution: {integrity: sha512-1F/jrfhxJtWILusgx63WeTvGTwE4vmsT9+e/z7cZLKU8sBMddwqw3UV5ERfOV+H1FuRK3YREZ46J4Gy0aP3qDA==} 393 + cpu: [x64] 394 + os: [darwin] 395 + 396 + '@rollup/rollup-freebsd-arm64@4.24.2': 397 + resolution: {integrity: sha512-1YWOpFcGuC6iGAS4EI+o3BV2/6S0H+m9kFOIlyFtp4xIX5rjSnL3AwbTBxROX0c8yWtiWM7ZI6mEPTI7VkSpZw==} 398 + cpu: [arm64] 399 + os: [freebsd] 400 + 401 + '@rollup/rollup-freebsd-x64@4.24.2': 402 + resolution: {integrity: sha512-3qAqTewYrCdnOD9Gl9yvPoAoFAVmPJsBvleabvx4bnu1Kt6DrB2OALeRVag7BdWGWLhP1yooeMLEi6r2nYSOjg==} 403 + cpu: [x64] 404 + os: [freebsd] 405 + 406 + '@rollup/rollup-linux-arm-gnueabihf@4.24.2': 407 + resolution: {integrity: sha512-ArdGtPHjLqWkqQuoVQ6a5UC5ebdX8INPuJuJNWRe0RGa/YNhVvxeWmCTFQ7LdmNCSUzVZzxAvUznKaYx645Rig==} 408 + cpu: [arm] 409 + os: [linux] 410 + 411 + '@rollup/rollup-linux-arm-musleabihf@4.24.2': 412 + resolution: {integrity: sha512-B6UHHeNnnih8xH6wRKB0mOcJGvjZTww1FV59HqJoTJ5da9LCG6R4SEBt6uPqzlawv1LoEXSS0d4fBlHNWl6iYw==} 413 + cpu: [arm] 414 + os: [linux] 415 + 416 + '@rollup/rollup-linux-arm64-gnu@4.24.2': 417 + resolution: {integrity: sha512-kr3gqzczJjSAncwOS6i7fpb4dlqcvLidqrX5hpGBIM1wtt0QEVtf4wFaAwVv8QygFU8iWUMYEoJZWuWxyua4GQ==} 418 + cpu: [arm64] 419 + os: [linux] 420 + 421 + '@rollup/rollup-linux-arm64-musl@4.24.2': 422 + resolution: {integrity: sha512-TDdHLKCWgPuq9vQcmyLrhg/bgbOvIQ8rtWQK7MRxJ9nvaxKx38NvY7/Lo6cYuEnNHqf6rMqnivOIPIQt6H2AoA==} 423 + cpu: [arm64] 424 + os: [linux] 425 + 426 + '@rollup/rollup-linux-powerpc64le-gnu@4.24.2': 427 + resolution: {integrity: sha512-xv9vS648T3X4AxFFZGWeB5Dou8ilsv4VVqJ0+loOIgDO20zIhYfDLkk5xoQiej2RiSQkld9ijF/fhLeonrz2mw==} 428 + cpu: [ppc64] 429 + os: [linux] 430 + 431 + '@rollup/rollup-linux-riscv64-gnu@4.24.2': 432 + resolution: {integrity: sha512-tbtXwnofRoTt223WUZYiUnbxhGAOVul/3StZ947U4A5NNjnQJV5irKMm76G0LGItWs6y+SCjUn/Q0WaMLkEskg==} 433 + cpu: [riscv64] 434 + os: [linux] 435 + 436 + '@rollup/rollup-linux-s390x-gnu@4.24.2': 437 + resolution: {integrity: sha512-gc97UebApwdsSNT3q79glOSPdfwgwj5ELuiyuiMY3pEWMxeVqLGKfpDFoum4ujivzxn6veUPzkGuSYoh5deQ2Q==} 438 + cpu: [s390x] 439 + os: [linux] 440 + 441 + '@rollup/rollup-linux-x64-gnu@4.24.2': 442 + resolution: {integrity: sha512-jOG/0nXb3z+EM6SioY8RofqqmZ+9NKYvJ6QQaa9Mvd3RQxlH68/jcB/lpyVt4lCiqr04IyaC34NzhUqcXbB5FQ==} 443 + cpu: [x64] 444 + os: [linux] 445 + 446 + '@rollup/rollup-linux-x64-musl@4.24.2': 447 + resolution: {integrity: sha512-XAo7cJec80NWx9LlZFEJQxqKOMz/lX3geWs2iNT5CHIERLFfd90f3RYLLjiCBm1IMaQ4VOX/lTC9lWfzzQm14Q==} 448 + cpu: [x64] 449 + os: [linux] 450 + 451 + '@rollup/rollup-win32-arm64-msvc@4.24.2': 452 + resolution: {integrity: sha512-A+JAs4+EhsTjnPQvo9XY/DC0ztaws3vfqzrMNMKlwQXuniBKOIIvAAI8M0fBYiTCxQnElYu7mLk7JrhlQ+HeOw==} 453 + cpu: [arm64] 454 + os: [win32] 455 + 456 + '@rollup/rollup-win32-ia32-msvc@4.24.2': 457 + resolution: {integrity: sha512-ZhcrakbqA1SCiJRMKSU64AZcYzlZ/9M5LaYil9QWxx9vLnkQ9Vnkve17Qn4SjlipqIIBFKjBES6Zxhnvh0EAEw==} 458 + cpu: [ia32] 459 + os: [win32] 460 + 461 + '@rollup/rollup-win32-x64-msvc@4.24.2': 462 + resolution: {integrity: sha512-2mLH46K1u3r6uwc95hU+OR9q/ggYMpnS7pSp83Ece1HUQgF9Nh/QwTK5rcgbFnV9j+08yBrU5sA/P0RK2MSBNA==} 463 + cpu: [x64] 464 + os: [win32] 465 + 466 + '@types/babel__core@7.20.5': 467 + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} 468 + 469 + '@types/babel__generator@7.6.8': 470 + resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} 471 + 472 + '@types/babel__template@7.4.4': 473 + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} 474 + 475 + '@types/babel__traverse@7.20.6': 476 + resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} 477 + 478 + '@types/estree@1.0.6': 479 + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} 480 + 481 + '@types/node@22.8.2': 482 + resolution: {integrity: sha512-NzaRNFV+FZkvK/KLCsNdTvID0SThyrs5SHB6tsD/lajr22FGC73N2QeDPM2wHtVde8mgcXuSsHQkH5cX1pbPLw==} 483 + 484 + acorn@8.14.0: 485 + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} 486 + engines: {node: '>=0.4.0'} 487 + hasBin: true 488 + 489 + ansi-regex@5.0.1: 490 + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 491 + engines: {node: '>=8'} 492 + 493 + ansi-regex@6.1.0: 494 + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} 495 + engines: {node: '>=12'} 496 + 497 + ansi-styles@4.3.0: 498 + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 499 + engines: {node: '>=8'} 500 + 501 + ansi-styles@6.2.1: 502 + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} 503 + engines: {node: '>=12'} 504 + 505 + any-promise@1.3.0: 506 + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} 507 + 508 + anymatch@3.1.3: 509 + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} 510 + engines: {node: '>= 8'} 511 + 512 + arg@5.0.2: 513 + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} 514 + 515 + autoprefixer@10.4.20: 516 + resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} 517 + engines: {node: ^10 || ^12 || >=14} 518 + hasBin: true 519 + peerDependencies: 520 + postcss: ^8.1.0 521 + 522 + babel-plugin-jsx-dom-expressions@0.39.3: 523 + resolution: {integrity: sha512-6RzmSu21zYPlV2gNwzjGG9FgODtt9hIWnx7L//OIioIEuRcnpDZoY8Tr+I81Cy1SrH4qoDyKpwHHo6uAMAeyPA==} 524 + peerDependencies: 525 + '@babel/core': ^7.20.12 526 + 527 + babel-preset-solid@1.9.3: 528 + resolution: {integrity: sha512-jvlx5wDp8s+bEF9sGFw/84SInXOA51ttkUEroQziKMbxplXThVKt83qB6bDTa1HuLNatdU9FHpFOiQWs1tLQIg==} 529 + peerDependencies: 530 + '@babel/core': ^7.0.0 531 + 532 + balanced-match@1.0.2: 533 + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 534 + 535 + binary-extensions@2.3.0: 536 + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} 537 + engines: {node: '>=8'} 538 + 539 + brace-expansion@2.0.1: 540 + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} 541 + 542 + braces@3.0.3: 543 + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} 544 + engines: {node: '>=8'} 545 + 546 + browserslist@4.24.2: 547 + resolution: {integrity: sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==} 548 + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} 549 + hasBin: true 550 + 551 + buffer-from@1.1.2: 552 + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} 553 + 554 + camelcase-css@2.0.1: 555 + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} 556 + engines: {node: '>= 6'} 557 + 558 + caniuse-lite@1.0.30001673: 559 + resolution: {integrity: sha512-WTrjUCSMp3LYX0nE12ECkV0a+e6LC85E0Auz75555/qr78Oc8YWhEPNfDd6SHdtlCMSzqtuXY0uyEMNRcsKpKw==} 560 + 561 + chokidar@3.6.0: 562 + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} 563 + engines: {node: '>= 8.10.0'} 564 + 565 + color-convert@2.0.1: 566 + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 567 + engines: {node: '>=7.0.0'} 568 + 569 + color-name@1.1.4: 570 + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 571 + 572 + commander@2.20.3: 573 + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} 574 + 575 + commander@4.1.1: 576 + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} 577 + engines: {node: '>= 6'} 578 + 579 + convert-source-map@2.0.0: 580 + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} 581 + 582 + cross-spawn@7.0.3: 583 + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} 584 + engines: {node: '>= 8'} 585 + 586 + cssesc@3.0.0: 587 + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} 588 + engines: {node: '>=4'} 589 + hasBin: true 590 + 591 + csstype@3.1.3: 592 + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} 593 + 594 + debug@4.3.7: 595 + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} 596 + engines: {node: '>=6.0'} 597 + peerDependencies: 598 + supports-color: '*' 599 + peerDependenciesMeta: 600 + supports-color: 601 + optional: true 602 + 603 + didyoumean@1.2.2: 604 + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} 605 + 606 + dlv@1.1.3: 607 + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} 608 + 609 + eastasianwidth@0.2.0: 610 + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} 611 + 612 + electron-to-chromium@1.5.48: 613 + resolution: {integrity: sha512-FXULnNK7ACNI9MTMOVAzUGiz/YrK9Kcb0s/JT4aJgsam7Eh6XYe7Y6q95lPq+VdBe1DpT2eTnfXFtnuPGCks4w==} 614 + 615 + emoji-regex@8.0.0: 616 + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 617 + 618 + emoji-regex@9.2.2: 619 + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} 620 + 621 + entities@4.5.0: 622 + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} 623 + engines: {node: '>=0.12'} 624 + 625 + esbuild@0.21.5: 626 + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} 627 + engines: {node: '>=12'} 628 + hasBin: true 629 + 630 + escalade@3.2.0: 631 + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} 632 + engines: {node: '>=6'} 633 + 634 + fast-glob@3.3.2: 635 + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} 636 + engines: {node: '>=8.6.0'} 637 + 638 + fastq@1.17.1: 639 + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} 640 + 641 + fetch-blob@3.2.0: 642 + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} 643 + engines: {node: ^12.20 || >= 14.13} 644 + 645 + fill-range@7.1.1: 646 + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} 647 + engines: {node: '>=8'} 648 + 649 + foreground-child@3.3.0: 650 + resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} 651 + engines: {node: '>=14'} 652 + 653 + fraction.js@4.3.7: 654 + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} 655 + 656 + fsevents@2.3.3: 657 + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 658 + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 659 + os: [darwin] 660 + 661 + function-bind@1.1.2: 662 + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 663 + 664 + gensync@1.0.0-beta.2: 665 + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} 666 + engines: {node: '>=6.9.0'} 667 + 668 + glob-parent@5.1.2: 669 + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 670 + engines: {node: '>= 6'} 671 + 672 + glob-parent@6.0.2: 673 + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} 674 + engines: {node: '>=10.13.0'} 675 + 676 + glob@10.4.5: 677 + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} 678 + hasBin: true 679 + 680 + globals@11.12.0: 681 + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} 682 + engines: {node: '>=4'} 683 + 684 + hasown@2.0.2: 685 + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} 686 + engines: {node: '>= 0.4'} 687 + 688 + html-entities@2.3.3: 689 + resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} 690 + 691 + is-binary-path@2.1.0: 692 + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} 693 + engines: {node: '>=8'} 694 + 695 + is-core-module@2.15.1: 696 + resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} 697 + engines: {node: '>= 0.4'} 698 + 699 + is-extglob@2.1.1: 700 + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 701 + engines: {node: '>=0.10.0'} 702 + 703 + is-fullwidth-code-point@3.0.0: 704 + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} 705 + engines: {node: '>=8'} 706 + 707 + is-glob@4.0.3: 708 + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 709 + engines: {node: '>=0.10.0'} 710 + 711 + is-number@7.0.0: 712 + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 713 + engines: {node: '>=0.12.0'} 714 + 715 + is-what@4.1.16: 716 + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} 717 + engines: {node: '>=12.13'} 718 + 719 + isexe@2.0.0: 720 + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 721 + 722 + jackspeak@3.4.3: 723 + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} 724 + 725 + jiti@1.21.6: 726 + resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} 727 + hasBin: true 728 + 729 + js-tokens@4.0.0: 730 + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 731 + 732 + jsesc@3.0.2: 733 + resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} 734 + engines: {node: '>=6'} 735 + hasBin: true 736 + 737 + json5@2.2.3: 738 + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} 739 + engines: {node: '>=6'} 740 + hasBin: true 741 + 742 + lilconfig@2.1.0: 743 + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} 744 + engines: {node: '>=10'} 745 + 746 + lilconfig@3.1.2: 747 + resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==} 748 + engines: {node: '>=14'} 749 + 750 + lines-and-columns@1.2.4: 751 + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} 752 + 753 + lru-cache@10.4.3: 754 + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} 755 + 756 + lru-cache@5.1.1: 757 + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} 758 + 759 + merge-anything@5.1.7: 760 + resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==} 761 + engines: {node: '>=12.13'} 762 + 763 + merge2@1.4.1: 764 + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} 765 + engines: {node: '>= 8'} 766 + 767 + micromatch@4.0.8: 768 + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} 769 + engines: {node: '>=8.6'} 770 + 771 + minimatch@9.0.5: 772 + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} 773 + engines: {node: '>=16 || 14 >=14.17'} 774 + 775 + minipass@7.1.2: 776 + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} 777 + engines: {node: '>=16 || 14 >=14.17'} 778 + 779 + ms@2.1.3: 780 + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 781 + 782 + mz@2.7.0: 783 + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} 784 + 785 + nanoid@3.3.7: 786 + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} 787 + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 788 + hasBin: true 789 + 790 + nanoid@5.0.8: 791 + resolution: {integrity: sha512-TcJPw+9RV9dibz1hHUzlLVy8N4X9TnwirAjrU08Juo6BNKggzVfP2ZJ/3ZUSq15Xl5i85i+Z89XBO90pB2PghQ==} 792 + engines: {node: ^18 || >=20} 793 + hasBin: true 794 + 795 + native-file-system-adapter@3.0.1: 796 + resolution: {integrity: sha512-ocuhsYk2SY0906LPc3QIMW+rCV3MdhqGiy7wV5Bf0e8/5TsMjDdyIwhNiVPiKxzTJLDrLT6h8BoV9ERfJscKhw==} 797 + engines: {node: '>=14.8.0'} 798 + 799 + node-domexception@1.0.0: 800 + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} 801 + engines: {node: '>=10.5.0'} 802 + 803 + node-releases@2.0.18: 804 + resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} 805 + 806 + normalize-path@3.0.0: 807 + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} 808 + engines: {node: '>=0.10.0'} 809 + 810 + normalize-range@0.1.2: 811 + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} 812 + engines: {node: '>=0.10.0'} 813 + 814 + object-assign@4.1.1: 815 + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} 816 + engines: {node: '>=0.10.0'} 817 + 818 + object-hash@3.0.0: 819 + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} 820 + engines: {node: '>= 6'} 821 + 822 + package-json-from-dist@1.0.1: 823 + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} 824 + 825 + parse5@7.2.1: 826 + resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} 827 + 828 + path-key@3.1.1: 829 + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 830 + engines: {node: '>=8'} 831 + 832 + path-parse@1.0.7: 833 + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} 834 + 835 + path-scurry@1.11.1: 836 + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} 837 + engines: {node: '>=16 || 14 >=14.18'} 838 + 839 + picocolors@1.1.1: 840 + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} 841 + 842 + picomatch@2.3.1: 843 + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 844 + engines: {node: '>=8.6'} 845 + 846 + pify@2.3.0: 847 + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} 848 + engines: {node: '>=0.10.0'} 849 + 850 + pirates@4.0.6: 851 + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} 852 + engines: {node: '>= 6'} 853 + 854 + postcss-import@15.1.0: 855 + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} 856 + engines: {node: '>=14.0.0'} 857 + peerDependencies: 858 + postcss: ^8.0.0 859 + 860 + postcss-js@4.0.1: 861 + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} 862 + engines: {node: ^12 || ^14 || >= 16} 863 + peerDependencies: 864 + postcss: ^8.4.21 865 + 866 + postcss-load-config@4.0.2: 867 + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} 868 + engines: {node: '>= 14'} 869 + peerDependencies: 870 + postcss: '>=8.0.9' 871 + ts-node: '>=9.0.0' 872 + peerDependenciesMeta: 873 + postcss: 874 + optional: true 875 + ts-node: 876 + optional: true 877 + 878 + postcss-nested@6.2.0: 879 + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} 880 + engines: {node: '>=12.0'} 881 + peerDependencies: 882 + postcss: ^8.2.14 883 + 884 + postcss-selector-parser@6.1.2: 885 + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} 886 + engines: {node: '>=4'} 887 + 888 + postcss-value-parser@4.2.0: 889 + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} 890 + 891 + postcss@8.4.47: 892 + resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==} 893 + engines: {node: ^10 || ^12 || >=14} 894 + 895 + prettier-plugin-tailwindcss@0.6.8: 896 + resolution: {integrity: sha512-dGu3kdm7SXPkiW4nzeWKCl3uoImdd5CTZEJGxyypEPL37Wj0HT2pLqjrvSei1nTeuQfO4PUfjeW5cTUNRLZ4sA==} 897 + engines: {node: '>=14.21.3'} 898 + peerDependencies: 899 + '@ianvs/prettier-plugin-sort-imports': '*' 900 + '@prettier/plugin-pug': '*' 901 + '@shopify/prettier-plugin-liquid': '*' 902 + '@trivago/prettier-plugin-sort-imports': '*' 903 + '@zackad/prettier-plugin-twig-melody': '*' 904 + prettier: ^3.0 905 + prettier-plugin-astro: '*' 906 + prettier-plugin-css-order: '*' 907 + prettier-plugin-import-sort: '*' 908 + prettier-plugin-jsdoc: '*' 909 + prettier-plugin-marko: '*' 910 + prettier-plugin-multiline-arrays: '*' 911 + prettier-plugin-organize-attributes: '*' 912 + prettier-plugin-organize-imports: '*' 913 + prettier-plugin-sort-imports: '*' 914 + prettier-plugin-style-order: '*' 915 + prettier-plugin-svelte: '*' 916 + peerDependenciesMeta: 917 + '@ianvs/prettier-plugin-sort-imports': 918 + optional: true 919 + '@prettier/plugin-pug': 920 + optional: true 921 + '@shopify/prettier-plugin-liquid': 922 + optional: true 923 + '@trivago/prettier-plugin-sort-imports': 924 + optional: true 925 + '@zackad/prettier-plugin-twig-melody': 926 + optional: true 927 + prettier-plugin-astro: 928 + optional: true 929 + prettier-plugin-css-order: 930 + optional: true 931 + prettier-plugin-import-sort: 932 + optional: true 933 + prettier-plugin-jsdoc: 934 + optional: true 935 + prettier-plugin-marko: 936 + optional: true 937 + prettier-plugin-multiline-arrays: 938 + optional: true 939 + prettier-plugin-organize-attributes: 940 + optional: true 941 + prettier-plugin-organize-imports: 942 + optional: true 943 + prettier-plugin-sort-imports: 944 + optional: true 945 + prettier-plugin-style-order: 946 + optional: true 947 + prettier-plugin-svelte: 948 + optional: true 949 + 950 + prettier@3.3.3: 951 + resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} 952 + engines: {node: '>=14'} 953 + hasBin: true 954 + 955 + queue-microtask@1.2.3: 956 + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 957 + 958 + read-cache@1.0.0: 959 + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} 960 + 961 + readdirp@3.6.0: 962 + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} 963 + engines: {node: '>=8.10.0'} 964 + 965 + resolve@1.22.8: 966 + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} 967 + hasBin: true 968 + 969 + reusify@1.0.4: 970 + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} 971 + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 972 + 973 + rollup@4.24.2: 974 + resolution: {integrity: sha512-do/DFGq5g6rdDhdpPq5qb2ecoczeK6y+2UAjdJ5trjQJj5f1AiVdLRWRc9A9/fFukfvJRgM0UXzxBIYMovm5ww==} 975 + engines: {node: '>=18.0.0', npm: '>=8.0.0'} 976 + hasBin: true 977 + 978 + run-parallel@1.2.0: 979 + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} 980 + 981 + semver@6.3.1: 982 + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} 983 + hasBin: true 984 + 985 + seroval-plugins@1.1.1: 986 + resolution: {integrity: sha512-qNSy1+nUj7hsCOon7AO4wdAIo9P0jrzAMp18XhiOzA6/uO5TKtP7ScozVJ8T293oRIvi5wyCHSM4TrJo/c/GJA==} 987 + engines: {node: '>=10'} 988 + peerDependencies: 989 + seroval: ^1.0 990 + 991 + seroval@1.1.1: 992 + resolution: {integrity: sha512-rqEO6FZk8mv7Hyv4UCj3FD3b6Waqft605TLfsCe/BiaylRpyyMC0b+uA5TJKawX3KzMrdi3wsLbCaLplrQmBvQ==} 993 + engines: {node: '>=10'} 994 + 995 + shebang-command@2.0.0: 996 + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 997 + engines: {node: '>=8'} 998 + 999 + shebang-regex@3.0.0: 1000 + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 1001 + engines: {node: '>=8'} 1002 + 1003 + signal-exit@4.1.0: 1004 + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} 1005 + engines: {node: '>=14'} 1006 + 1007 + solid-js@1.9.3: 1008 + resolution: {integrity: sha512-5ba3taPoZGt9GY3YlsCB24kCg0Lv/rie/HTD4kG6h4daZZz7+yK02xn8Vx8dLYBc9i6Ps5JwAbEiqjmKaLB3Ag==} 1009 + 1010 + solid-refresh@0.6.3: 1011 + resolution: {integrity: sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==} 1012 + peerDependencies: 1013 + solid-js: ^1.3 1014 + 1015 + source-map-js@1.2.1: 1016 + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 1017 + engines: {node: '>=0.10.0'} 1018 + 1019 + source-map-support@0.5.21: 1020 + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} 1021 + 1022 + source-map@0.6.1: 1023 + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 1024 + engines: {node: '>=0.10.0'} 1025 + 1026 + string-width@4.2.3: 1027 + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 1028 + engines: {node: '>=8'} 1029 + 1030 + string-width@5.1.2: 1031 + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} 1032 + engines: {node: '>=12'} 1033 + 1034 + strip-ansi@6.0.1: 1035 + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 1036 + engines: {node: '>=8'} 1037 + 1038 + strip-ansi@7.1.0: 1039 + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} 1040 + engines: {node: '>=12'} 1041 + 1042 + sucrase@3.35.0: 1043 + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} 1044 + engines: {node: '>=16 || 14 >=14.17'} 1045 + hasBin: true 1046 + 1047 + supports-preserve-symlinks-flag@1.0.0: 1048 + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} 1049 + engines: {node: '>= 0.4'} 1050 + 1051 + tailwindcss@3.4.14: 1052 + resolution: {integrity: sha512-IcSvOcTRcUtQQ7ILQL5quRDg7Xs93PdJEk1ZLbhhvJc7uj/OAhYOnruEiwnGgBvUtaUAJ8/mhSw1o8L2jCiENA==} 1053 + engines: {node: '>=14.0.0'} 1054 + hasBin: true 1055 + 1056 + terser@5.36.0: 1057 + resolution: {integrity: sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==} 1058 + engines: {node: '>=10'} 1059 + hasBin: true 1060 + 1061 + thenify-all@1.6.0: 1062 + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} 1063 + engines: {node: '>=0.8'} 1064 + 1065 + thenify@3.3.1: 1066 + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} 1067 + 1068 + to-regex-range@5.0.1: 1069 + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 1070 + engines: {node: '>=8.0'} 1071 + 1072 + ts-interface-checker@0.1.13: 1073 + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} 1074 + 1075 + typescript@5.7.0-beta: 1076 + resolution: {integrity: sha512-opDlmEnzKdl082N5piLS43lsyugg0aORdv+XnNzMv5yP5VtBWuZhFDxU8lizmhW+PEFa/fZiShYRBxKsrkTDMQ==} 1077 + engines: {node: '>=14.17'} 1078 + hasBin: true 1079 + 1080 + undici-types@6.19.8: 1081 + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} 1082 + 1083 + update-browserslist-db@1.1.1: 1084 + resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} 1085 + hasBin: true 1086 + peerDependencies: 1087 + browserslist: '>= 4.21.0' 1088 + 1089 + util-deprecate@1.0.2: 1090 + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} 1091 + 1092 + valibot@1.0.0-beta.2: 1093 + resolution: {integrity: sha512-XwAXmUPdB0Hikm6M18dD/a+j+57KA0dFlna5Yh77LBeeItSIQMwyZOjf8E2nWkhDtQ+Ie4GiaZPkLmaszH/4+w==} 1094 + peerDependencies: 1095 + typescript: '>=5' 1096 + peerDependenciesMeta: 1097 + typescript: 1098 + optional: true 1099 + 1100 + validate-html-nesting@1.2.2: 1101 + resolution: {integrity: sha512-hGdgQozCsQJMyfK5urgFcWEqsSSrK63Awe0t/IMR0bZ0QMtnuaiHzThW81guu3qx9abLi99NEuiaN6P9gVYsNg==} 1102 + 1103 + vite-plugin-solid@2.10.2: 1104 + resolution: {integrity: sha512-AOEtwMe2baBSXMXdo+BUwECC8IFHcKS6WQV/1NEd+Q7vHPap5fmIhLcAzr+DUJ04/KHx/1UBU0l1/GWP+rMAPQ==} 1105 + peerDependencies: 1106 + '@testing-library/jest-dom': ^5.16.6 || ^5.17.0 || ^6.* 1107 + solid-js: ^1.7.2 1108 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 1109 + peerDependenciesMeta: 1110 + '@testing-library/jest-dom': 1111 + optional: true 1112 + 1113 + vite@5.4.10: 1114 + resolution: {integrity: sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==} 1115 + engines: {node: ^18.0.0 || >=20.0.0} 1116 + hasBin: true 1117 + peerDependencies: 1118 + '@types/node': ^18.0.0 || >=20.0.0 1119 + less: '*' 1120 + lightningcss: ^1.21.0 1121 + sass: '*' 1122 + sass-embedded: '*' 1123 + stylus: '*' 1124 + sugarss: '*' 1125 + terser: ^5.4.0 1126 + peerDependenciesMeta: 1127 + '@types/node': 1128 + optional: true 1129 + less: 1130 + optional: true 1131 + lightningcss: 1132 + optional: true 1133 + sass: 1134 + optional: true 1135 + sass-embedded: 1136 + optional: true 1137 + stylus: 1138 + optional: true 1139 + sugarss: 1140 + optional: true 1141 + terser: 1142 + optional: true 1143 + 1144 + vitefu@0.2.5: 1145 + resolution: {integrity: sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==} 1146 + peerDependencies: 1147 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 1148 + peerDependenciesMeta: 1149 + vite: 1150 + optional: true 1151 + 1152 + web-streams-polyfill@3.3.3: 1153 + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} 1154 + engines: {node: '>= 8'} 1155 + 1156 + which@2.0.2: 1157 + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 1158 + engines: {node: '>= 8'} 1159 + hasBin: true 1160 + 1161 + wrap-ansi@7.0.0: 1162 + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} 1163 + engines: {node: '>=10'} 1164 + 1165 + wrap-ansi@8.1.0: 1166 + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} 1167 + engines: {node: '>=12'} 1168 + 1169 + yallist@3.1.1: 1170 + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} 1171 + 1172 + yaml@2.6.0: 1173 + resolution: {integrity: sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==} 1174 + engines: {node: '>= 14'} 1175 + hasBin: true 1176 + 1177 + snapshots: 1178 + 1179 + '@alloc/quick-lru@5.2.0': {} 1180 + 1181 + '@ampproject/remapping@2.3.0': 1182 + dependencies: 1183 + '@jridgewell/gen-mapping': 0.3.5 1184 + '@jridgewell/trace-mapping': 0.3.25 1185 + 1186 + '@atcute/base32@1.0.0': {} 1187 + 1188 + '@atcute/bluesky@1.0.7(@atcute/client@2.0.3)': 1189 + dependencies: 1190 + '@atcute/client': 2.0.3 1191 + 1192 + '@atcute/car@1.1.0': 1193 + dependencies: 1194 + '@atcute/cbor': 1.0.5 1195 + '@atcute/cid': 1.0.1 1196 + '@atcute/varint': 1.0.0 1197 + 1198 + '@atcute/cbor@1.0.5': 1199 + dependencies: 1200 + '@atcute/base32': 1.0.0 1201 + '@atcute/cid': 1.0.1 1202 + 1203 + '@atcute/cid@1.0.1': 1204 + dependencies: 1205 + '@atcute/base32': 1.0.0 1206 + '@atcute/varint': 1.0.0 1207 + 1208 + '@atcute/client@2.0.3': {} 1209 + 1210 + '@atcute/varint@1.0.0': {} 1211 + 1212 + '@babel/code-frame@7.26.0': 1213 + dependencies: 1214 + '@babel/helper-validator-identifier': 7.25.9 1215 + js-tokens: 4.0.0 1216 + picocolors: 1.1.1 1217 + 1218 + '@babel/compat-data@7.26.0': {} 1219 + 1220 + '@babel/core@7.26.0': 1221 + dependencies: 1222 + '@ampproject/remapping': 2.3.0 1223 + '@babel/code-frame': 7.26.0 1224 + '@babel/generator': 7.26.0 1225 + '@babel/helper-compilation-targets': 7.25.9 1226 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) 1227 + '@babel/helpers': 7.26.0 1228 + '@babel/parser': 7.26.1 1229 + '@babel/template': 7.25.9 1230 + '@babel/traverse': 7.25.9 1231 + '@babel/types': 7.26.0 1232 + convert-source-map: 2.0.0 1233 + debug: 4.3.7 1234 + gensync: 1.0.0-beta.2 1235 + json5: 2.2.3 1236 + semver: 6.3.1 1237 + transitivePeerDependencies: 1238 + - supports-color 1239 + 1240 + '@babel/generator@7.26.0': 1241 + dependencies: 1242 + '@babel/parser': 7.26.1 1243 + '@babel/types': 7.26.0 1244 + '@jridgewell/gen-mapping': 0.3.5 1245 + '@jridgewell/trace-mapping': 0.3.25 1246 + jsesc: 3.0.2 1247 + 1248 + '@babel/helper-compilation-targets@7.25.9': 1249 + dependencies: 1250 + '@babel/compat-data': 7.26.0 1251 + '@babel/helper-validator-option': 7.25.9 1252 + browserslist: 4.24.2 1253 + lru-cache: 5.1.1 1254 + semver: 6.3.1 1255 + 1256 + '@babel/helper-module-imports@7.18.6': 1257 + dependencies: 1258 + '@babel/types': 7.26.0 1259 + 1260 + '@babel/helper-module-imports@7.25.9': 1261 + dependencies: 1262 + '@babel/traverse': 7.25.9 1263 + '@babel/types': 7.26.0 1264 + transitivePeerDependencies: 1265 + - supports-color 1266 + 1267 + '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.0)': 1268 + dependencies: 1269 + '@babel/core': 7.26.0 1270 + '@babel/helper-module-imports': 7.25.9 1271 + '@babel/helper-validator-identifier': 7.25.9 1272 + '@babel/traverse': 7.25.9 1273 + transitivePeerDependencies: 1274 + - supports-color 1275 + 1276 + '@babel/helper-plugin-utils@7.25.9': {} 1277 + 1278 + '@babel/helper-string-parser@7.25.9': {} 1279 + 1280 + '@babel/helper-validator-identifier@7.25.9': {} 1281 + 1282 + '@babel/helper-validator-option@7.25.9': {} 1283 + 1284 + '@babel/helpers@7.26.0': 1285 + dependencies: 1286 + '@babel/template': 7.25.9 1287 + '@babel/types': 7.26.0 1288 + 1289 + '@babel/parser@7.26.1': 1290 + dependencies: 1291 + '@babel/types': 7.26.0 1292 + 1293 + '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.0)': 1294 + dependencies: 1295 + '@babel/core': 7.26.0 1296 + '@babel/helper-plugin-utils': 7.25.9 1297 + 1298 + '@babel/template@7.25.9': 1299 + dependencies: 1300 + '@babel/code-frame': 7.26.0 1301 + '@babel/parser': 7.26.1 1302 + '@babel/types': 7.26.0 1303 + 1304 + '@babel/traverse@7.25.9': 1305 + dependencies: 1306 + '@babel/code-frame': 7.26.0 1307 + '@babel/generator': 7.26.0 1308 + '@babel/parser': 7.26.1 1309 + '@babel/template': 7.25.9 1310 + '@babel/types': 7.26.0 1311 + debug: 4.3.7 1312 + globals: 11.12.0 1313 + transitivePeerDependencies: 1314 + - supports-color 1315 + 1316 + '@babel/types@7.26.0': 1317 + dependencies: 1318 + '@babel/helper-string-parser': 7.25.9 1319 + '@babel/helper-validator-identifier': 7.25.9 1320 + 1321 + '@esbuild/aix-ppc64@0.21.5': 1322 + optional: true 1323 + 1324 + '@esbuild/android-arm64@0.21.5': 1325 + optional: true 1326 + 1327 + '@esbuild/android-arm@0.21.5': 1328 + optional: true 1329 + 1330 + '@esbuild/android-x64@0.21.5': 1331 + optional: true 1332 + 1333 + '@esbuild/darwin-arm64@0.21.5': 1334 + optional: true 1335 + 1336 + '@esbuild/darwin-x64@0.21.5': 1337 + optional: true 1338 + 1339 + '@esbuild/freebsd-arm64@0.21.5': 1340 + optional: true 1341 + 1342 + '@esbuild/freebsd-x64@0.21.5': 1343 + optional: true 1344 + 1345 + '@esbuild/linux-arm64@0.21.5': 1346 + optional: true 1347 + 1348 + '@esbuild/linux-arm@0.21.5': 1349 + optional: true 1350 + 1351 + '@esbuild/linux-ia32@0.21.5': 1352 + optional: true 1353 + 1354 + '@esbuild/linux-loong64@0.21.5': 1355 + optional: true 1356 + 1357 + '@esbuild/linux-mips64el@0.21.5': 1358 + optional: true 1359 + 1360 + '@esbuild/linux-ppc64@0.21.5': 1361 + optional: true 1362 + 1363 + '@esbuild/linux-riscv64@0.21.5': 1364 + optional: true 1365 + 1366 + '@esbuild/linux-s390x@0.21.5': 1367 + optional: true 1368 + 1369 + '@esbuild/linux-x64@0.21.5': 1370 + optional: true 1371 + 1372 + '@esbuild/netbsd-x64@0.21.5': 1373 + optional: true 1374 + 1375 + '@esbuild/openbsd-x64@0.21.5': 1376 + optional: true 1377 + 1378 + '@esbuild/sunos-x64@0.21.5': 1379 + optional: true 1380 + 1381 + '@esbuild/win32-arm64@0.21.5': 1382 + optional: true 1383 + 1384 + '@esbuild/win32-ia32@0.21.5': 1385 + optional: true 1386 + 1387 + '@esbuild/win32-x64@0.21.5': 1388 + optional: true 1389 + 1390 + '@externdefs/solid-freeze@0.1.1(solid-js@1.9.3)': 1391 + dependencies: 1392 + solid-js: 1.9.3 1393 + 1394 + '@isaacs/cliui@8.0.2': 1395 + dependencies: 1396 + string-width: 5.1.2 1397 + string-width-cjs: string-width@4.2.3 1398 + strip-ansi: 7.1.0 1399 + strip-ansi-cjs: strip-ansi@6.0.1 1400 + wrap-ansi: 8.1.0 1401 + wrap-ansi-cjs: wrap-ansi@7.0.0 1402 + 1403 + '@jridgewell/gen-mapping@0.3.5': 1404 + dependencies: 1405 + '@jridgewell/set-array': 1.2.1 1406 + '@jridgewell/sourcemap-codec': 1.5.0 1407 + '@jridgewell/trace-mapping': 0.3.25 1408 + 1409 + '@jridgewell/resolve-uri@3.1.2': {} 1410 + 1411 + '@jridgewell/set-array@1.2.1': {} 1412 + 1413 + '@jridgewell/source-map@0.3.6': 1414 + dependencies: 1415 + '@jridgewell/gen-mapping': 0.3.5 1416 + '@jridgewell/trace-mapping': 0.3.25 1417 + 1418 + '@jridgewell/sourcemap-codec@1.5.0': {} 1419 + 1420 + '@jridgewell/trace-mapping@0.3.25': 1421 + dependencies: 1422 + '@jridgewell/resolve-uri': 3.1.2 1423 + '@jridgewell/sourcemap-codec': 1.5.0 1424 + 1425 + '@jsr/mary__events@0.1.0': {} 1426 + 1427 + '@jsr/mary__tar@0.2.4': {} 1428 + 1429 + '@nodelib/fs.scandir@2.1.5': 1430 + dependencies: 1431 + '@nodelib/fs.stat': 2.0.5 1432 + run-parallel: 1.2.0 1433 + 1434 + '@nodelib/fs.stat@2.0.5': {} 1435 + 1436 + '@nodelib/fs.walk@1.2.8': 1437 + dependencies: 1438 + '@nodelib/fs.scandir': 2.1.5 1439 + fastq: 1.17.1 1440 + 1441 + '@pkgjs/parseargs@0.11.0': 1442 + optional: true 1443 + 1444 + '@rollup/rollup-android-arm-eabi@4.24.2': 1445 + optional: true 1446 + 1447 + '@rollup/rollup-android-arm64@4.24.2': 1448 + optional: true 1449 + 1450 + '@rollup/rollup-darwin-arm64@4.24.2': 1451 + optional: true 1452 + 1453 + '@rollup/rollup-darwin-x64@4.24.2': 1454 + optional: true 1455 + 1456 + '@rollup/rollup-freebsd-arm64@4.24.2': 1457 + optional: true 1458 + 1459 + '@rollup/rollup-freebsd-x64@4.24.2': 1460 + optional: true 1461 + 1462 + '@rollup/rollup-linux-arm-gnueabihf@4.24.2': 1463 + optional: true 1464 + 1465 + '@rollup/rollup-linux-arm-musleabihf@4.24.2': 1466 + optional: true 1467 + 1468 + '@rollup/rollup-linux-arm64-gnu@4.24.2': 1469 + optional: true 1470 + 1471 + '@rollup/rollup-linux-arm64-musl@4.24.2': 1472 + optional: true 1473 + 1474 + '@rollup/rollup-linux-powerpc64le-gnu@4.24.2': 1475 + optional: true 1476 + 1477 + '@rollup/rollup-linux-riscv64-gnu@4.24.2': 1478 + optional: true 1479 + 1480 + '@rollup/rollup-linux-s390x-gnu@4.24.2': 1481 + optional: true 1482 + 1483 + '@rollup/rollup-linux-x64-gnu@4.24.2': 1484 + optional: true 1485 + 1486 + '@rollup/rollup-linux-x64-musl@4.24.2': 1487 + optional: true 1488 + 1489 + '@rollup/rollup-win32-arm64-msvc@4.24.2': 1490 + optional: true 1491 + 1492 + '@rollup/rollup-win32-ia32-msvc@4.24.2': 1493 + optional: true 1494 + 1495 + '@rollup/rollup-win32-x64-msvc@4.24.2': 1496 + optional: true 1497 + 1498 + '@types/babel__core@7.20.5': 1499 + dependencies: 1500 + '@babel/parser': 7.26.1 1501 + '@babel/types': 7.26.0 1502 + '@types/babel__generator': 7.6.8 1503 + '@types/babel__template': 7.4.4 1504 + '@types/babel__traverse': 7.20.6 1505 + 1506 + '@types/babel__generator@7.6.8': 1507 + dependencies: 1508 + '@babel/types': 7.26.0 1509 + 1510 + '@types/babel__template@7.4.4': 1511 + dependencies: 1512 + '@babel/parser': 7.26.1 1513 + '@babel/types': 7.26.0 1514 + 1515 + '@types/babel__traverse@7.20.6': 1516 + dependencies: 1517 + '@babel/types': 7.26.0 1518 + 1519 + '@types/estree@1.0.6': {} 1520 + 1521 + '@types/node@22.8.2': 1522 + dependencies: 1523 + undici-types: 6.19.8 1524 + 1525 + acorn@8.14.0: {} 1526 + 1527 + ansi-regex@5.0.1: {} 1528 + 1529 + ansi-regex@6.1.0: {} 1530 + 1531 + ansi-styles@4.3.0: 1532 + dependencies: 1533 + color-convert: 2.0.1 1534 + 1535 + ansi-styles@6.2.1: {} 1536 + 1537 + any-promise@1.3.0: {} 1538 + 1539 + anymatch@3.1.3: 1540 + dependencies: 1541 + normalize-path: 3.0.0 1542 + picomatch: 2.3.1 1543 + 1544 + arg@5.0.2: {} 1545 + 1546 + autoprefixer@10.4.20(postcss@8.4.47): 1547 + dependencies: 1548 + browserslist: 4.24.2 1549 + caniuse-lite: 1.0.30001673 1550 + fraction.js: 4.3.7 1551 + normalize-range: 0.1.2 1552 + picocolors: 1.1.1 1553 + postcss: 8.4.47 1554 + postcss-value-parser: 4.2.0 1555 + 1556 + babel-plugin-jsx-dom-expressions@0.39.3(@babel/core@7.26.0): 1557 + dependencies: 1558 + '@babel/core': 7.26.0 1559 + '@babel/helper-module-imports': 7.18.6 1560 + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0) 1561 + '@babel/types': 7.26.0 1562 + html-entities: 2.3.3 1563 + parse5: 7.2.1 1564 + validate-html-nesting: 1.2.2 1565 + 1566 + babel-preset-solid@1.9.3(@babel/core@7.26.0): 1567 + dependencies: 1568 + '@babel/core': 7.26.0 1569 + babel-plugin-jsx-dom-expressions: 0.39.3(@babel/core@7.26.0) 1570 + 1571 + balanced-match@1.0.2: {} 1572 + 1573 + binary-extensions@2.3.0: {} 1574 + 1575 + brace-expansion@2.0.1: 1576 + dependencies: 1577 + balanced-match: 1.0.2 1578 + 1579 + braces@3.0.3: 1580 + dependencies: 1581 + fill-range: 7.1.1 1582 + 1583 + browserslist@4.24.2: 1584 + dependencies: 1585 + caniuse-lite: 1.0.30001673 1586 + electron-to-chromium: 1.5.48 1587 + node-releases: 2.0.18 1588 + update-browserslist-db: 1.1.1(browserslist@4.24.2) 1589 + 1590 + buffer-from@1.1.2: {} 1591 + 1592 + camelcase-css@2.0.1: {} 1593 + 1594 + caniuse-lite@1.0.30001673: {} 1595 + 1596 + chokidar@3.6.0: 1597 + dependencies: 1598 + anymatch: 3.1.3 1599 + braces: 3.0.3 1600 + glob-parent: 5.1.2 1601 + is-binary-path: 2.1.0 1602 + is-glob: 4.0.3 1603 + normalize-path: 3.0.0 1604 + readdirp: 3.6.0 1605 + optionalDependencies: 1606 + fsevents: 2.3.3 1607 + 1608 + color-convert@2.0.1: 1609 + dependencies: 1610 + color-name: 1.1.4 1611 + 1612 + color-name@1.1.4: {} 1613 + 1614 + commander@2.20.3: {} 1615 + 1616 + commander@4.1.1: {} 1617 + 1618 + convert-source-map@2.0.0: {} 1619 + 1620 + cross-spawn@7.0.3: 1621 + dependencies: 1622 + path-key: 3.1.1 1623 + shebang-command: 2.0.0 1624 + which: 2.0.2 1625 + 1626 + cssesc@3.0.0: {} 1627 + 1628 + csstype@3.1.3: {} 1629 + 1630 + debug@4.3.7: 1631 + dependencies: 1632 + ms: 2.1.3 1633 + 1634 + didyoumean@1.2.2: {} 1635 + 1636 + dlv@1.1.3: {} 1637 + 1638 + eastasianwidth@0.2.0: {} 1639 + 1640 + electron-to-chromium@1.5.48: {} 1641 + 1642 + emoji-regex@8.0.0: {} 1643 + 1644 + emoji-regex@9.2.2: {} 1645 + 1646 + entities@4.5.0: {} 1647 + 1648 + esbuild@0.21.5: 1649 + optionalDependencies: 1650 + '@esbuild/aix-ppc64': 0.21.5 1651 + '@esbuild/android-arm': 0.21.5 1652 + '@esbuild/android-arm64': 0.21.5 1653 + '@esbuild/android-x64': 0.21.5 1654 + '@esbuild/darwin-arm64': 0.21.5 1655 + '@esbuild/darwin-x64': 0.21.5 1656 + '@esbuild/freebsd-arm64': 0.21.5 1657 + '@esbuild/freebsd-x64': 0.21.5 1658 + '@esbuild/linux-arm': 0.21.5 1659 + '@esbuild/linux-arm64': 0.21.5 1660 + '@esbuild/linux-ia32': 0.21.5 1661 + '@esbuild/linux-loong64': 0.21.5 1662 + '@esbuild/linux-mips64el': 0.21.5 1663 + '@esbuild/linux-ppc64': 0.21.5 1664 + '@esbuild/linux-riscv64': 0.21.5 1665 + '@esbuild/linux-s390x': 0.21.5 1666 + '@esbuild/linux-x64': 0.21.5 1667 + '@esbuild/netbsd-x64': 0.21.5 1668 + '@esbuild/openbsd-x64': 0.21.5 1669 + '@esbuild/sunos-x64': 0.21.5 1670 + '@esbuild/win32-arm64': 0.21.5 1671 + '@esbuild/win32-ia32': 0.21.5 1672 + '@esbuild/win32-x64': 0.21.5 1673 + 1674 + escalade@3.2.0: {} 1675 + 1676 + fast-glob@3.3.2: 1677 + dependencies: 1678 + '@nodelib/fs.stat': 2.0.5 1679 + '@nodelib/fs.walk': 1.2.8 1680 + glob-parent: 5.1.2 1681 + merge2: 1.4.1 1682 + micromatch: 4.0.8 1683 + 1684 + fastq@1.17.1: 1685 + dependencies: 1686 + reusify: 1.0.4 1687 + 1688 + fetch-blob@3.2.0: 1689 + dependencies: 1690 + node-domexception: 1.0.0 1691 + web-streams-polyfill: 3.3.3 1692 + optional: true 1693 + 1694 + fill-range@7.1.1: 1695 + dependencies: 1696 + to-regex-range: 5.0.1 1697 + 1698 + foreground-child@3.3.0: 1699 + dependencies: 1700 + cross-spawn: 7.0.3 1701 + signal-exit: 4.1.0 1702 + 1703 + fraction.js@4.3.7: {} 1704 + 1705 + fsevents@2.3.3: 1706 + optional: true 1707 + 1708 + function-bind@1.1.2: {} 1709 + 1710 + gensync@1.0.0-beta.2: {} 1711 + 1712 + glob-parent@5.1.2: 1713 + dependencies: 1714 + is-glob: 4.0.3 1715 + 1716 + glob-parent@6.0.2: 1717 + dependencies: 1718 + is-glob: 4.0.3 1719 + 1720 + glob@10.4.5: 1721 + dependencies: 1722 + foreground-child: 3.3.0 1723 + jackspeak: 3.4.3 1724 + minimatch: 9.0.5 1725 + minipass: 7.1.2 1726 + package-json-from-dist: 1.0.1 1727 + path-scurry: 1.11.1 1728 + 1729 + globals@11.12.0: {} 1730 + 1731 + hasown@2.0.2: 1732 + dependencies: 1733 + function-bind: 1.1.2 1734 + 1735 + html-entities@2.3.3: {} 1736 + 1737 + is-binary-path@2.1.0: 1738 + dependencies: 1739 + binary-extensions: 2.3.0 1740 + 1741 + is-core-module@2.15.1: 1742 + dependencies: 1743 + hasown: 2.0.2 1744 + 1745 + is-extglob@2.1.1: {} 1746 + 1747 + is-fullwidth-code-point@3.0.0: {} 1748 + 1749 + is-glob@4.0.3: 1750 + dependencies: 1751 + is-extglob: 2.1.1 1752 + 1753 + is-number@7.0.0: {} 1754 + 1755 + is-what@4.1.16: {} 1756 + 1757 + isexe@2.0.0: {} 1758 + 1759 + jackspeak@3.4.3: 1760 + dependencies: 1761 + '@isaacs/cliui': 8.0.2 1762 + optionalDependencies: 1763 + '@pkgjs/parseargs': 0.11.0 1764 + 1765 + jiti@1.21.6: {} 1766 + 1767 + js-tokens@4.0.0: {} 1768 + 1769 + jsesc@3.0.2: {} 1770 + 1771 + json5@2.2.3: {} 1772 + 1773 + lilconfig@2.1.0: {} 1774 + 1775 + lilconfig@3.1.2: {} 1776 + 1777 + lines-and-columns@1.2.4: {} 1778 + 1779 + lru-cache@10.4.3: {} 1780 + 1781 + lru-cache@5.1.1: 1782 + dependencies: 1783 + yallist: 3.1.1 1784 + 1785 + merge-anything@5.1.7: 1786 + dependencies: 1787 + is-what: 4.1.16 1788 + 1789 + merge2@1.4.1: {} 1790 + 1791 + micromatch@4.0.8: 1792 + dependencies: 1793 + braces: 3.0.3 1794 + picomatch: 2.3.1 1795 + 1796 + minimatch@9.0.5: 1797 + dependencies: 1798 + brace-expansion: 2.0.1 1799 + 1800 + minipass@7.1.2: {} 1801 + 1802 + ms@2.1.3: {} 1803 + 1804 + mz@2.7.0: 1805 + dependencies: 1806 + any-promise: 1.3.0 1807 + object-assign: 4.1.1 1808 + thenify-all: 1.6.0 1809 + 1810 + nanoid@3.3.7: {} 1811 + 1812 + nanoid@5.0.8: {} 1813 + 1814 + native-file-system-adapter@3.0.1: 1815 + optionalDependencies: 1816 + fetch-blob: 3.2.0 1817 + 1818 + node-domexception@1.0.0: 1819 + optional: true 1820 + 1821 + node-releases@2.0.18: {} 1822 + 1823 + normalize-path@3.0.0: {} 1824 + 1825 + normalize-range@0.1.2: {} 1826 + 1827 + object-assign@4.1.1: {} 1828 + 1829 + object-hash@3.0.0: {} 1830 + 1831 + package-json-from-dist@1.0.1: {} 1832 + 1833 + parse5@7.2.1: 1834 + dependencies: 1835 + entities: 4.5.0 1836 + 1837 + path-key@3.1.1: {} 1838 + 1839 + path-parse@1.0.7: {} 1840 + 1841 + path-scurry@1.11.1: 1842 + dependencies: 1843 + lru-cache: 10.4.3 1844 + minipass: 7.1.2 1845 + 1846 + picocolors@1.1.1: {} 1847 + 1848 + picomatch@2.3.1: {} 1849 + 1850 + pify@2.3.0: {} 1851 + 1852 + pirates@4.0.6: {} 1853 + 1854 + postcss-import@15.1.0(postcss@8.4.47): 1855 + dependencies: 1856 + postcss: 8.4.47 1857 + postcss-value-parser: 4.2.0 1858 + read-cache: 1.0.0 1859 + resolve: 1.22.8 1860 + 1861 + postcss-js@4.0.1(postcss@8.4.47): 1862 + dependencies: 1863 + camelcase-css: 2.0.1 1864 + postcss: 8.4.47 1865 + 1866 + postcss-load-config@4.0.2(postcss@8.4.47): 1867 + dependencies: 1868 + lilconfig: 3.1.2 1869 + yaml: 2.6.0 1870 + optionalDependencies: 1871 + postcss: 8.4.47 1872 + 1873 + postcss-nested@6.2.0(postcss@8.4.47): 1874 + dependencies: 1875 + postcss: 8.4.47 1876 + postcss-selector-parser: 6.1.2 1877 + 1878 + postcss-selector-parser@6.1.2: 1879 + dependencies: 1880 + cssesc: 3.0.0 1881 + util-deprecate: 1.0.2 1882 + 1883 + postcss-value-parser@4.2.0: {} 1884 + 1885 + postcss@8.4.47: 1886 + dependencies: 1887 + nanoid: 3.3.7 1888 + picocolors: 1.1.1 1889 + source-map-js: 1.2.1 1890 + 1891 + prettier-plugin-tailwindcss@0.6.8(prettier@3.3.3): 1892 + dependencies: 1893 + prettier: 3.3.3 1894 + 1895 + prettier@3.3.3: {} 1896 + 1897 + queue-microtask@1.2.3: {} 1898 + 1899 + read-cache@1.0.0: 1900 + dependencies: 1901 + pify: 2.3.0 1902 + 1903 + readdirp@3.6.0: 1904 + dependencies: 1905 + picomatch: 2.3.1 1906 + 1907 + resolve@1.22.8: 1908 + dependencies: 1909 + is-core-module: 2.15.1 1910 + path-parse: 1.0.7 1911 + supports-preserve-symlinks-flag: 1.0.0 1912 + 1913 + reusify@1.0.4: {} 1914 + 1915 + rollup@4.24.2: 1916 + dependencies: 1917 + '@types/estree': 1.0.6 1918 + optionalDependencies: 1919 + '@rollup/rollup-android-arm-eabi': 4.24.2 1920 + '@rollup/rollup-android-arm64': 4.24.2 1921 + '@rollup/rollup-darwin-arm64': 4.24.2 1922 + '@rollup/rollup-darwin-x64': 4.24.2 1923 + '@rollup/rollup-freebsd-arm64': 4.24.2 1924 + '@rollup/rollup-freebsd-x64': 4.24.2 1925 + '@rollup/rollup-linux-arm-gnueabihf': 4.24.2 1926 + '@rollup/rollup-linux-arm-musleabihf': 4.24.2 1927 + '@rollup/rollup-linux-arm64-gnu': 4.24.2 1928 + '@rollup/rollup-linux-arm64-musl': 4.24.2 1929 + '@rollup/rollup-linux-powerpc64le-gnu': 4.24.2 1930 + '@rollup/rollup-linux-riscv64-gnu': 4.24.2 1931 + '@rollup/rollup-linux-s390x-gnu': 4.24.2 1932 + '@rollup/rollup-linux-x64-gnu': 4.24.2 1933 + '@rollup/rollup-linux-x64-musl': 4.24.2 1934 + '@rollup/rollup-win32-arm64-msvc': 4.24.2 1935 + '@rollup/rollup-win32-ia32-msvc': 4.24.2 1936 + '@rollup/rollup-win32-x64-msvc': 4.24.2 1937 + fsevents: 2.3.3 1938 + 1939 + run-parallel@1.2.0: 1940 + dependencies: 1941 + queue-microtask: 1.2.3 1942 + 1943 + semver@6.3.1: {} 1944 + 1945 + seroval-plugins@1.1.1(seroval@1.1.1): 1946 + dependencies: 1947 + seroval: 1.1.1 1948 + 1949 + seroval@1.1.1: {} 1950 + 1951 + shebang-command@2.0.0: 1952 + dependencies: 1953 + shebang-regex: 3.0.0 1954 + 1955 + shebang-regex@3.0.0: {} 1956 + 1957 + signal-exit@4.1.0: {} 1958 + 1959 + solid-js@1.9.3: 1960 + dependencies: 1961 + csstype: 3.1.3 1962 + seroval: 1.1.1 1963 + seroval-plugins: 1.1.1(seroval@1.1.1) 1964 + 1965 + solid-refresh@0.6.3(solid-js@1.9.3): 1966 + dependencies: 1967 + '@babel/generator': 7.26.0 1968 + '@babel/helper-module-imports': 7.25.9 1969 + '@babel/types': 7.26.0 1970 + solid-js: 1.9.3 1971 + transitivePeerDependencies: 1972 + - supports-color 1973 + 1974 + source-map-js@1.2.1: {} 1975 + 1976 + source-map-support@0.5.21: 1977 + dependencies: 1978 + buffer-from: 1.1.2 1979 + source-map: 0.6.1 1980 + 1981 + source-map@0.6.1: {} 1982 + 1983 + string-width@4.2.3: 1984 + dependencies: 1985 + emoji-regex: 8.0.0 1986 + is-fullwidth-code-point: 3.0.0 1987 + strip-ansi: 6.0.1 1988 + 1989 + string-width@5.1.2: 1990 + dependencies: 1991 + eastasianwidth: 0.2.0 1992 + emoji-regex: 9.2.2 1993 + strip-ansi: 7.1.0 1994 + 1995 + strip-ansi@6.0.1: 1996 + dependencies: 1997 + ansi-regex: 5.0.1 1998 + 1999 + strip-ansi@7.1.0: 2000 + dependencies: 2001 + ansi-regex: 6.1.0 2002 + 2003 + sucrase@3.35.0: 2004 + dependencies: 2005 + '@jridgewell/gen-mapping': 0.3.5 2006 + commander: 4.1.1 2007 + glob: 10.4.5 2008 + lines-and-columns: 1.2.4 2009 + mz: 2.7.0 2010 + pirates: 4.0.6 2011 + ts-interface-checker: 0.1.13 2012 + 2013 + supports-preserve-symlinks-flag@1.0.0: {} 2014 + 2015 + tailwindcss@3.4.14: 2016 + dependencies: 2017 + '@alloc/quick-lru': 5.2.0 2018 + arg: 5.0.2 2019 + chokidar: 3.6.0 2020 + didyoumean: 1.2.2 2021 + dlv: 1.1.3 2022 + fast-glob: 3.3.2 2023 + glob-parent: 6.0.2 2024 + is-glob: 4.0.3 2025 + jiti: 1.21.6 2026 + lilconfig: 2.1.0 2027 + micromatch: 4.0.8 2028 + normalize-path: 3.0.0 2029 + object-hash: 3.0.0 2030 + picocolors: 1.1.1 2031 + postcss: 8.4.47 2032 + postcss-import: 15.1.0(postcss@8.4.47) 2033 + postcss-js: 4.0.1(postcss@8.4.47) 2034 + postcss-load-config: 4.0.2(postcss@8.4.47) 2035 + postcss-nested: 6.2.0(postcss@8.4.47) 2036 + postcss-selector-parser: 6.1.2 2037 + resolve: 1.22.8 2038 + sucrase: 3.35.0 2039 + transitivePeerDependencies: 2040 + - ts-node 2041 + 2042 + terser@5.36.0: 2043 + dependencies: 2044 + '@jridgewell/source-map': 0.3.6 2045 + acorn: 8.14.0 2046 + commander: 2.20.3 2047 + source-map-support: 0.5.21 2048 + 2049 + thenify-all@1.6.0: 2050 + dependencies: 2051 + thenify: 3.3.1 2052 + 2053 + thenify@3.3.1: 2054 + dependencies: 2055 + any-promise: 1.3.0 2056 + 2057 + to-regex-range@5.0.1: 2058 + dependencies: 2059 + is-number: 7.0.0 2060 + 2061 + ts-interface-checker@0.1.13: {} 2062 + 2063 + typescript@5.7.0-beta: {} 2064 + 2065 + undici-types@6.19.8: {} 2066 + 2067 + update-browserslist-db@1.1.1(browserslist@4.24.2): 2068 + dependencies: 2069 + browserslist: 4.24.2 2070 + escalade: 3.2.0 2071 + picocolors: 1.1.1 2072 + 2073 + util-deprecate@1.0.2: {} 2074 + 2075 + valibot@1.0.0-beta.2(typescript@5.7.0-beta): 2076 + optionalDependencies: 2077 + typescript: 5.7.0-beta 2078 + 2079 + validate-html-nesting@1.2.2: {} 2080 + 2081 + vite-plugin-solid@2.10.2(solid-js@1.9.3)(vite@5.4.10(@types/node@22.8.2)(terser@5.36.0)): 2082 + dependencies: 2083 + '@babel/core': 7.26.0 2084 + '@types/babel__core': 7.20.5 2085 + babel-preset-solid: 1.9.3(@babel/core@7.26.0) 2086 + merge-anything: 5.1.7 2087 + solid-js: 1.9.3 2088 + solid-refresh: 0.6.3(solid-js@1.9.3) 2089 + vite: 5.4.10(@types/node@22.8.2)(terser@5.36.0) 2090 + vitefu: 0.2.5(vite@5.4.10(@types/node@22.8.2)(terser@5.36.0)) 2091 + transitivePeerDependencies: 2092 + - supports-color 2093 + 2094 + vite@5.4.10(@types/node@22.8.2)(terser@5.36.0): 2095 + dependencies: 2096 + esbuild: 0.21.5 2097 + postcss: 8.4.47 2098 + rollup: 4.24.2 2099 + optionalDependencies: 2100 + '@types/node': 22.8.2 2101 + fsevents: 2.3.3 2102 + terser: 5.36.0 2103 + 2104 + vitefu@0.2.5(vite@5.4.10(@types/node@22.8.2)(terser@5.36.0)): 2105 + optionalDependencies: 2106 + vite: 5.4.10(@types/node@22.8.2)(terser@5.36.0) 2107 + 2108 + web-streams-polyfill@3.3.3: 2109 + optional: true 2110 + 2111 + which@2.0.2: 2112 + dependencies: 2113 + isexe: 2.0.0 2114 + 2115 + wrap-ansi@7.0.0: 2116 + dependencies: 2117 + ansi-styles: 4.3.0 2118 + string-width: 4.2.3 2119 + strip-ansi: 6.0.1 2120 + 2121 + wrap-ansi@8.1.0: 2122 + dependencies: 2123 + ansi-styles: 6.2.1 2124 + string-width: 5.1.2 2125 + strip-ansi: 7.1.0 2126 + 2127 + yallist@3.1.1: {} 2128 + 2129 + yaml@2.6.0: {}
+5
postcss.config.js
··· 1 + export default { 2 + plugins: { 3 + tailwindcss: {}, 4 + }, 5 + };
+2
public/_headers
··· 1 + /assets/* 2 + cache-control: public, max-age=31536000, immutable
+53
src/api/queries/did-doc.ts
··· 1 + import * as v from 'valibot'; 2 + 3 + import { At } from '@atcute/client/lexicons'; 4 + 5 + import { didDocument, DidDocument } from '../types/did-doc'; 6 + import { DID_WEB_RE } from '../utils/strings'; 7 + 8 + export const getDidDocument = async ({ 9 + did, 10 + signal, 11 + }: { 12 + did: At.DID; 13 + signal?: AbortSignal; 14 + }): Promise<DidDocument> => { 15 + const colon_index = did.indexOf(':', 4); 16 + 17 + const type = did.slice(4, colon_index); 18 + const ident = did.slice(colon_index + 1); 19 + 20 + let rawDoc: any; 21 + 22 + if (type === 'plc') { 23 + const response = await fetch(`https://plc.directory/${did}`, { signal }); 24 + 25 + if (response.status === 404) { 26 + throw new Error(`did not found in directory`); 27 + } else if (!response.ok) { 28 + throw new Error(`directory is unreachable`); 29 + } 30 + 31 + const json = await response.json(); 32 + 33 + rawDoc = json; 34 + } else if (type === 'web') { 35 + if (!DID_WEB_RE.test(ident)) { 36 + throw new Error(`invalid identifier`); 37 + } 38 + 39 + const response = await fetch(`https://${ident}/.well-known/did.json`, { signal }); 40 + 41 + if (!response.ok) { 42 + throw new Error(`did document is unreachable`); 43 + } 44 + 45 + const json = await response.json(); 46 + 47 + rawDoc = json; 48 + } else { 49 + throw new Error(`unsupported did method`); 50 + } 51 + 52 + return v.parse(didDocument, rawDoc); 53 + };
+38
src/api/queries/handle.ts
··· 1 + import { simpleFetchHandler, XRPC } from '@atcute/client'; 2 + import { At } from '@atcute/client/lexicons'; 3 + 4 + import { appViewRpc } from '~/globals/rpc'; 5 + 6 + export const resolveHandleViaAppView = async ({ 7 + handle, 8 + signal, 9 + }: { 10 + handle: string; 11 + signal?: AbortSignal; 12 + }): Promise<At.DID> => { 13 + const { data } = await appViewRpc.get('com.atproto.identity.resolveHandle', { 14 + signal: signal, 15 + params: { handle: handle }, 16 + }); 17 + 18 + return data.did; 19 + }; 20 + 21 + export const resolveHandleViaPds = async ({ 22 + service, 23 + handle: handle, 24 + signal, 25 + }: { 26 + service: string; 27 + handle: string; 28 + signal?: AbortSignal; 29 + }): Promise<At.DID> => { 30 + const rpc = new XRPC({ handler: simpleFetchHandler({ service }) }); 31 + 32 + const { data } = await rpc.get('com.atproto.identity.resolveHandle', { 33 + signal, 34 + params: { handle }, 35 + }); 36 + 37 + return data.did; 38 + };
+49
src/api/types/did-doc.ts
··· 1 + import * as v from 'valibot'; 2 + 3 + import { didString, serviceUrlString } from './strings'; 4 + 5 + const verificationMethod = v.object({ 6 + id: v.string(), 7 + type: v.string(), 8 + controller: v.string(), 9 + publicKeyMultibase: v.optional(v.string()), 10 + }); 11 + 12 + const service = v.object({ 13 + id: v.string(), 14 + type: v.string(), 15 + serviceEndpoint: v.union([v.string(), v.record(v.string(), v.unknown())]), 16 + }); 17 + 18 + export const didDocument = v.object({ 19 + id: didString, 20 + alsoKnownAs: v.optional(v.array(v.pipe(v.string(), v.url())), []), 21 + verificationMethod: v.optional(v.array(verificationMethod), []), 22 + service: v.optional(v.array(service), []), 23 + }); 24 + 25 + export type DidDocument = v.InferOutput<typeof didDocument>; 26 + 27 + export const getPdsEndpoint = (doc: DidDocument): string | undefined => { 28 + return getServiceEndpoint(doc, '#atproto_pds', 'AtprotoPersonalDataServer'); 29 + }; 30 + 31 + export const getServiceEndpoint = ( 32 + doc: DidDocument, 33 + serviceId: string, 34 + serviceType: string, 35 + ): string | undefined => { 36 + const did = doc.id; 37 + 38 + const didServiceId = did + serviceId; 39 + const found = doc.service?.find((service) => service.id === serviceId || service.id === didServiceId); 40 + 41 + if (!found || found.type !== serviceType || typeof found.serviceEndpoint !== 'string') { 42 + return undefined; 43 + } 44 + 45 + const endpoint = found.serviceEndpoint; 46 + if (v.is(serviceUrlString, found.serviceEndpoint)) { 47 + return endpoint; 48 + } 49 + };
+21
src/api/types/strings.ts
··· 1 + import * as v from 'valibot'; 2 + 3 + import { DID_RE, HANDLE_RE } from '../utils/strings'; 4 + 5 + export const didString = v.pipe(v.string(), v.regex(DID_RE, 'must be a valid did')); 6 + export const handleString = v.pipe(v.string(), v.regex(HANDLE_RE, 'must be a valid handle')); 7 + 8 + export const serviceUrlString = v.pipe( 9 + v.string(), 10 + v.check((urlString) => { 11 + const url = URL.parse(urlString); 12 + 13 + return ( 14 + url !== null && 15 + (url.protocol === 'https:' || url.protocol === 'http:') && 16 + url.pathname === '/' && 17 + url.search === '' && 18 + url.hash === '' 19 + ); 20 + }, 'must be a valid service url'), 21 + );
+34
src/api/utils/error.ts
··· 1 + import { XRPCError } from '@atcute/client'; 2 + 3 + export const formatXRPCError = (err: XRPCError): string => { 4 + const name = err.kind; 5 + return (name ? name + ': ' : '') + err.message; 6 + }; 7 + 8 + export const formatQueryError = (err: unknown) => { 9 + if (err instanceof XRPCError) { 10 + const kind = err.kind; 11 + 12 + if (kind === 'InvalidToken' || kind === 'ExpiredToken') { 13 + return `Account session is no longer valid`; 14 + } 15 + 16 + if (kind === 'UpstreamFailure') { 17 + return `Server appears to be experiencing issues, try again later`; 18 + } 19 + 20 + if (kind === 'InternalServerError') { 21 + return `Server is having issues processing this request, try again later`; 22 + } 23 + 24 + return formatXRPCError(err); 25 + } 26 + 27 + if (err instanceof Error) { 28 + if (/NetworkError|Failed to fetch|timed out|abort/.test(err.message)) { 29 + return `Unable to access the internet, try again later`; 30 + } 31 + } 32 + 33 + return '' + err; 34 + };
+42
src/api/utils/strings.ts
··· 1 + import type { At, Records } from '@atcute/client/lexicons'; 2 + 3 + import { assert } from '~/lib/utils/invariant'; 4 + 5 + export const ATURI_RE = 6 + /^at:\/\/(did:[a-zA-Z0-9._:%\-]+|[a-zA-Z0-9-.]+)\/([a-zA-Z0-9-.]+)\/([a-zA-Z0-9._~:@!$&%')(*+,;=\-]+)(?:#(\/[a-zA-Z0-9._~:@!$&%')(*+,;=\-[\]/\\]*))?$/; 7 + 8 + export const DID_RE = /^did:([a-z]+):([a-zA-Z0-9._:%\-]*[a-zA-Z0-9._\-])$/; 9 + 10 + export const DID_WEB_RE = /^([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*(?:\.[a-zA-Z]{2,}))$/; 11 + 12 + export const HANDLE_RE = /^[a-zA-Z0-9\-]+(?:\.[a-zA-Z0-9\-]+)*(?:\.[a-zA-Z]{2,})$/; 13 + 14 + export const DID_OR_HANDLE_RE = 15 + /^[a-zA-Z0-9\-]+(?:\.[a-zA-Z0-9\-]+)*(?:\.[a-zA-Z]{2,})$|^did:[a-z]+:[a-zA-Z0-9._:%\-]*[a-zA-Z0-9._\-]$/; 16 + 17 + export interface AtUri { 18 + repo: string; 19 + collection: string; 20 + rkey: string; 21 + fragment: string | undefined; 22 + } 23 + 24 + export const isDid = (value: string): value is At.DID => { 25 + return DID_RE.test(value); 26 + }; 27 + 28 + export const parseAtUri = (str: string): AtUri => { 29 + const match = ATURI_RE.exec(str); 30 + assert(match !== null, `Failed to parse AT URI for ${str}`); 31 + 32 + return { 33 + repo: match[1], 34 + collection: match[2], 35 + rkey: match[3], 36 + fragment: match[4], 37 + }; 38 + }; 39 + 40 + export const makeAtUri = (repo: string, collection: keyof Records | (string & {}), rkey: string) => { 41 + return `at://${repo}/${collection}/${rkey}`; 42 + };
+1
src/assets/dotted-background.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" width="800" height="800" opacity=".39" viewBox="0 0 800 800"><g fill="hsl(261, 60%, 38%)" stroke="none" stroke-width="2.25"><circle r="5.263" opacity=".35"/><circle cx="66.667" r="5.263" opacity=".35"/><circle cx="133.333" r="5.263" opacity=".35"/><circle cx="200" r="5.263" opacity=".35"/><circle cx="266.667" r="5.263" opacity=".35"/><circle cx="333.333" r="5.263" opacity=".35"/><circle cx="400" r="5.263" opacity=".35"/><circle cx="466.667" r="5.263" opacity=".35"/><circle cx="533.333" r="5.263" opacity=".35"/><circle cx="600" r="5.263" opacity=".35"/><circle cx="666.667" r="5.263" opacity=".35"/><circle cx="733.333" r="5.263" opacity=".35"/><circle cx="800" r="5.263" opacity=".35"/><circle cy="66.667" r="5.263" opacity=".35"/><circle cx="66.667" cy="66.667" r="5.263" opacity=".35"/><circle cx="133.333" cy="66.667" r="5.263"/><circle cx="200" cy="66.667" r="5.263" opacity=".35"/><circle cx="266.667" cy="66.667" r="5.263" opacity=".35"/><circle cx="333.333" cy="66.667" r="5.263" opacity=".35"/><circle cx="400" cy="66.667" r="5.263" opacity=".35"/><circle cx="466.667" cy="66.667" r="5.263"/><circle cx="533.333" cy="66.667" r="5.263" opacity=".35"/><circle cx="600" cy="66.667" r="5.263" opacity=".35"/><circle cx="666.667" cy="66.667" r="5.263" opacity=".35"/><circle cx="733.333" cy="66.667" r="5.263" opacity=".35"/><circle cx="800" cy="66.667" r="5.263" opacity=".35"/><circle cy="133.333" r="5.263" opacity=".35"/><circle cx="66.667" cy="133.333" r="5.263"/><circle cx="133.333" cy="133.333" r="5.263" opacity=".35"/><circle cx="200" cy="133.333" r="5.263" opacity=".35"/><circle cx="266.667" cy="133.333" r="5.263" opacity=".35"/><circle cx="333.333" cy="133.333" r="5.263"/><circle cx="400" cy="133.333" r="5.263"/><circle cx="466.667" cy="133.333" r="5.263"/><circle cx="533.333" cy="133.333" r="5.263" opacity=".35"/><circle cx="600" cy="133.333" r="5.263"/><circle cx="666.667" cy="133.333" r="5.263" opacity=".35"/><circle cx="733.333" cy="133.333" r="5.263" opacity=".35"/><circle cx="800" cy="133.333" r="5.263" opacity=".35"/><circle cy="200" r="5.263" opacity=".35"/><circle cx="66.667" cy="200" r="5.263" opacity=".35"/><circle cx="133.333" cy="200" r="5.263" opacity=".35"/><circle cx="200" cy="200" r="5.263" opacity=".35"/><circle cx="266.667" cy="200" r="5.263" opacity=".35"/><circle cx="333.333" cy="200" r="5.263" opacity=".35"/><circle cx="400" cy="200" r="5.263" opacity=".35"/><circle cx="466.667" cy="200" r="5.263" opacity=".35"/><circle cx="533.333" cy="200" r="5.263" opacity=".35"/><circle cx="600" cy="200" r="5.263" opacity=".35"/><circle cx="666.667" cy="200" r="5.263" opacity=".35"/><circle cx="733.333" cy="200" r="5.263" opacity=".35"/><circle cx="800" cy="200" r="5.263" opacity=".35"/><circle cy="266.667" r="5.263" opacity=".35"/><circle cx="66.667" cy="266.667" r="5.263" opacity=".35"/><circle cx="133.333" cy="266.667" r="5.263" opacity=".35"/><circle cx="200" cy="266.667" r="5.263" opacity=".35"/><circle cx="266.667" cy="266.667" r="5.263" opacity=".35"/><circle cx="333.333" cy="266.667" r="5.263" opacity=".35"/><circle cx="400" cy="266.667" r="5.263" opacity=".35"/><circle cx="466.667" cy="266.667" r="5.263" opacity=".35"/><circle cx="533.333" cy="266.667" r="5.263" opacity=".35"/><circle cx="600" cy="266.667" r="5.263" opacity=".35"/><circle cx="666.667" cy="266.667" r="5.263" opacity=".35"/><circle cx="733.333" cy="266.667" r="5.263" opacity=".35"/><circle cx="800" cy="266.667" r="5.263" opacity=".35"/><circle cy="333.333" r="5.263" opacity=".35"/><circle cx="66.667" cy="333.333" r="5.263"/><circle cx="133.333" cy="333.333" r="5.263" opacity=".35"/><circle cx="200" cy="333.333" r="5.263" opacity=".35"/><circle cx="266.667" cy="333.333" r="5.263"/><circle cx="333.333" cy="333.333" r="5.263" opacity=".35"/><circle cx="400" cy="333.333" r="5.263"/><circle cx="466.667" cy="333.333" r="5.263" opacity=".35"/><circle cx="533.333" cy="333.333" r="5.263" opacity=".35"/><circle cx="600" cy="333.333" r="5.263" opacity=".35"/><circle cx="666.667" cy="333.333" r="5.263"/><circle cx="733.333" cy="333.333" r="5.263"/><circle cx="800" cy="333.333" r="5.263" opacity=".35"/><circle cy="400" r="5.263" opacity=".35"/><circle cx="66.667" cy="400" r="5.263" opacity=".35"/><circle cx="133.333" cy="400" r="5.263"/><circle cx="200" cy="400" r="5.263" opacity=".35"/><circle cx="266.667" cy="400" r="5.263" opacity=".35"/><circle cx="333.333" cy="400" r="5.263"/><circle cx="400" cy="400" r="5.263" opacity=".35"/><circle cx="466.667" cy="400" r="5.263" opacity=".35"/><circle cx="533.333" cy="400" r="5.263" opacity=".35"/><circle cx="600" cy="400" r="5.263" opacity=".35"/><circle cx="666.667" cy="400" r="5.263" opacity=".35"/><circle cx="733.333" cy="400" r="5.263" opacity=".35"/><circle cx="800" cy="400" r="5.263" opacity=".35"/><circle cy="466.667" r="5.263" opacity=".35"/><circle cx="66.667" cy="466.667" r="5.263"/><circle cx="133.333" cy="466.667" r="5.263" opacity=".35"/><circle cx="200" cy="466.667" r="5.263" opacity=".35"/><circle cx="266.667" cy="466.667" r="5.263" opacity=".35"/><circle cx="333.333" cy="466.667" r="5.263" opacity=".35"/><circle cx="400" cy="466.667" r="5.263" opacity=".35"/><circle cx="466.667" cy="466.667" r="5.263" opacity=".35"/><circle cx="533.333" cy="466.667" r="5.263" opacity=".35"/><circle cx="600" cy="466.667" r="5.263"/><circle cx="666.667" cy="466.667" r="5.263" opacity=".35"/><circle cx="733.333" cy="466.667" r="5.263"/><circle cx="800" cy="466.667" r="5.263" opacity=".35"/><circle cy="533.333" r="5.263" opacity=".35"/><circle cx="66.667" cy="533.333" r="5.263" opacity=".35"/><circle cx="133.333" cy="533.333" r="5.263" opacity=".35"/><circle cx="200" cy="533.333" r="5.263" opacity=".35"/><circle cx="266.667" cy="533.333" r="5.263"/><circle cx="333.333" cy="533.333" r="5.263" opacity=".35"/><circle cx="400" cy="533.333" r="5.263"/><circle cx="466.667" cy="533.333" r="5.263" opacity=".35"/><circle cx="533.333" cy="533.333" r="5.263" opacity=".35"/><circle cx="600" cy="533.333" r="5.263" opacity=".35"/><circle cx="666.667" cy="533.333" r="5.263"/><circle cx="733.333" cy="533.333" r="5.263" opacity=".35"/><circle cx="800" cy="533.333" r="5.263" opacity=".35"/><circle cy="600" r="5.263" opacity=".35"/><circle cx="66.667" cy="600" r="5.263" opacity=".35"/><circle cx="133.333" cy="600" r="5.263" opacity=".35"/><circle cx="200" cy="600" r="5.263"/><circle cx="266.667" cy="600" r="5.263" opacity=".35"/><circle cx="333.333" cy="600" r="5.263" opacity=".35"/><circle cx="400" cy="600" r="5.263" opacity=".35"/><circle cx="466.667" cy="600" r="5.263" opacity=".35"/><circle cx="533.333" cy="600" r="5.263"/><circle cx="600" cy="600" r="5.263" opacity=".35"/><circle cx="666.667" cy="600" r="5.263" opacity=".35"/><circle cx="733.333" cy="600" r="5.263" opacity=".35"/><circle cx="800" cy="600" r="5.263" opacity=".35"/><circle cy="666.667" r="5.263" opacity=".35"/><circle cx="66.667" cy="666.667" r="5.263"/><circle cx="133.333" cy="666.667" r="5.263" opacity=".35"/><circle cx="200" cy="666.667" r="5.263" opacity=".35"/><circle cx="266.667" cy="666.667" r="5.263" opacity=".35"/><circle cx="333.333" cy="666.667" r="5.263"/><circle cx="400" cy="666.667" r="5.263"/><circle cx="466.667" cy="666.667" r="5.263" opacity=".35"/><circle cx="533.333" cy="666.667" r="5.263" opacity=".35"/><circle cx="600" cy="666.667" r="5.263" opacity=".35"/><circle cx="666.667" cy="666.667" r="5.263" opacity=".35"/><circle cx="733.333" cy="666.667" r="5.263" opacity=".35"/><circle cx="800" cy="666.667" r="5.263" opacity=".35"/><circle cy="733.333" r="5.263" opacity=".35"/><circle cx="66.667" cy="733.333" r="5.263" opacity=".35"/><circle cx="133.333" cy="733.333" r="5.263" opacity=".35"/><circle cx="200" cy="733.333" r="5.263"/><circle cx="266.667" cy="733.333" r="5.263" opacity=".35"/><circle cx="333.333" cy="733.333" r="5.263" opacity=".35"/><circle cx="400" cy="733.333" r="5.263"/><circle cx="466.667" cy="733.333" r="5.263" opacity=".35"/><circle cx="533.333" cy="733.333" r="5.263" opacity=".35"/><circle cx="600" cy="733.333" r="5.263" opacity=".35"/><circle cx="666.667" cy="733.333" r="5.263"/><circle cx="733.333" cy="733.333" r="5.263" opacity=".35"/><circle cx="800" cy="733.333" r="5.263" opacity=".35"/><circle cy="800" r="5.263" opacity=".35"/><circle cx="66.667" cy="800" r="5.263" opacity=".35"/><circle cx="133.333" cy="800" r="5.263" opacity=".35"/><circle cx="200" cy="800" r="5.263" opacity=".35"/><circle cx="266.667" cy="800" r="5.263" opacity=".35"/><circle cx="333.333" cy="800" r="5.263" opacity=".35"/><circle cx="400" cy="800" r="5.263" opacity=".35"/><circle cx="466.667" cy="800" r="5.263" opacity=".35"/><circle cx="533.333" cy="800" r="5.263" opacity=".35"/><circle cx="600" cy="800" r="5.263" opacity=".35"/><circle cx="666.667" cy="800" r="5.263" opacity=".35"/><circle cx="733.333" cy="800" r="5.263" opacity=".35"/><circle cx="800" cy="800" r="5.263" opacity=".35"/></g></svg>
+11
src/components/circular-progress-view.tsx
··· 1 + import CircularProgress from './circular-progress'; 2 + 3 + const CircularProgressView = () => { 4 + return ( 5 + <div class="grid place-items-center p-4"> 6 + <CircularProgress /> 7 + </div> 8 + ); 9 + }; 10 + 11 + export default CircularProgressView;
+27
src/components/circular-progress.tsx
··· 1 + export interface CircularProgressProps { 2 + size?: number; 3 + } 4 + 5 + const CircularProgress = (props: CircularProgressProps) => { 6 + return ( 7 + <svg 8 + viewBox="0 0 32 32" 9 + class="animate-spin" 10 + style={`height:${props.size ?? 24}px;width:${props.size ?? 24}px`} 11 + > 12 + <circle cx="16" cy="16" fill="none" r="14" stroke-width="4" class="stroke-purple-600 opacity-20" /> 13 + <circle 14 + cx="16" 15 + cy="16" 16 + fill="none" 17 + r="14" 18 + stroke-width="4" 19 + stroke-dasharray="80px" 20 + stroke-dashoffset="60px" 21 + class="stroke-purple-600" 22 + /> 23 + </svg> 24 + ); 25 + }; 26 + 27 + export default CircularProgress;
+34
src/components/diff-table.tsx
··· 1 + export interface DiffTableProps { 2 + fields: { title: string; prev?: string | null; next: string | null }[]; 3 + } 4 + 5 + const DiffTable = (props: DiffTableProps) => { 6 + return ( 7 + <div class="grid grid-cols-[min-content_minmax(0,1fr)]"> 8 + {props.fields.map(({ title, prev, next }) => { 9 + if (prev === undefined) { 10 + prev = next; 11 + } 12 + 13 + return ( 14 + <> 15 + <div class="w-20 py-1 pr-2 align-top font-medium text-gray-600">{`${title}:`}</div> 16 + <div class="font-mono"> 17 + <div hidden={prev !== next} class="px-2 py-1"> 18 + {next} 19 + </div> 20 + <div hidden={prev === next || prev === null} class="bg-red-200 px-2 py-1"> 21 + {prev} 22 + </div> 23 + <div hidden={prev === next || next === null} class="bg-green-200 px-2 py-1"> 24 + {next} 25 + </div> 26 + </div> 27 + </> 28 + ); 29 + })} 30 + </div> 31 + ); 32 + }; 33 + 34 + export default DiffTable;
+33
src/components/error-view.tsx
··· 1 + import { formatQueryError } from '~/api/utils/error'; 2 + 3 + export interface ErrorViewProps { 4 + error: unknown; 5 + onRetry?: () => void; 6 + } 7 + 8 + const ErrorView = (props: ErrorViewProps) => { 9 + const onRetry = props.onRetry; 10 + 11 + return ( 12 + <div class="flex flex-col gap-4 p-4"> 13 + <div> 14 + <p class="font-bold">Something went wrong</p> 15 + <p class="text-gray-600">{formatQueryError(props.error)}</p> 16 + </div> 17 + 18 + <div class="empty:hidden"> 19 + {onRetry && ( 20 + <button 21 + type="button" 22 + onClick={onRetry} 23 + class="flex h-9 items-center rounded bg-purple-800 px-4 text-sm font-semibold text-white hover:bg-purple-700 active:bg-purple-700" 24 + > 25 + Try again 26 + </button> 27 + )} 28 + </div> 29 + </div> 30 + ); 31 + }; 32 + 33 + export default ErrorView;
+68
src/components/ic-icons/_icon.tsx
··· 1 + import { type ComponentProps } from 'solid-js'; 2 + import { spread, template } from 'solid-js/web'; 3 + 4 + const SVG_NS = 'http://www.w3.org/2000/svg'; 5 + const ICON_NS = 'icons'; 6 + 7 + let uid = 0; 8 + let defs: SVGDefsElement; 9 + 10 + interface PathAttrs { 11 + fill?: 'currentColor' | (string & {}); 12 + 'fill-rule'?: 'evenodd' | (string & {}); 13 + } 14 + 15 + type PathDef = [d: string, attrs?: PathAttrs]; 16 + 17 + const DEFAULT_ATTRS: PathAttrs = { 18 + fill: 'currentColor', 19 + }; 20 + 21 + const pushIcon = (paths: PathDef[]) => { 22 + const id = ICON_NS + uid++; 23 + const symbol = document.createElementNS(SVG_NS, 'symbol'); 24 + 25 + symbol.id = id; 26 + 27 + for (let idx = 0, len = paths.length; idx < len; idx++) { 28 + const path = paths[idx]; 29 + 30 + const d = path[0]; 31 + const attrs = path[1] ?? DEFAULT_ATTRS; 32 + 33 + const node = document.createElementNS(SVG_NS, 'path'); 34 + node.setAttribute('d', d); 35 + 36 + for (const key in attrs) { 37 + // @ts-expect-error 38 + const val = attrs[key]; 39 + node.setAttribute(key, val); 40 + } 41 + 42 + symbol.appendChild(node); 43 + } 44 + 45 + if (!defs) { 46 + const svg = document.head.appendChild(document.createElementNS(SVG_NS, 'svg')); 47 + svg.appendChild((defs = document.createElementNS(SVG_NS, 'defs'))); 48 + } 49 + 50 + defs.appendChild(symbol); 51 + 52 + return id; 53 + }; 54 + 55 + /*#__NO_SIDE_EFFECTS__*/ 56 + export const createIcon = (paths: PathDef[], viewBox = '0 0 24 24') => { 57 + const href = '#' + pushIcon(paths); 58 + 59 + const tmpl = template(`<svg height=1em width=1em viewBox="${viewBox}"><use href=${href}>`); 60 + return Icon.bind(tmpl); 61 + }; 62 + 63 + function Icon(this: () => Element, props: ComponentProps<'svg'>) { 64 + const svg = this(); 65 + spread(svg, props, true, true); 66 + 67 + return svg; 68 + }
+9
src/components/ic-icons/baseline-history.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + const HistoryIcon = createIcon([ 4 + [ 5 + 'M13 3a9 9 0 0 0-9 9H1l3.89 3.89l.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7s-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42A8.954 8.954 0 0 0 13 21a9 9 0 0 0 0-18zm-1 5v5l4.28 2.54l.72-1.21l-3.5-2.08V8H12z', 6 + ], 7 + ]); 8 + 9 + export default HistoryIcon;
+9
src/components/ic-icons/outline-archive.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + const ArchiveOutlinedIcon = createIcon([ 4 + [ 5 + 'm20.54 5.23l-1.39-1.68C18.88 3.21 18.47 3 18 3H6c-.47 0-.88.21-1.16.55L3.46 5.23C3.17 5.57 3 6.02 3 6.5V19c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6.5c0-.48-.17-.93-.46-1.27M6.24 5h11.52l.81.97H5.44zM5 19V8h14v11zm8.45-9h-2.9v3H8l4 4l4-4h-2.55z', 6 + ], 7 + ]); 8 + 9 + export default ArchiveOutlinedIcon;
+9
src/components/ic-icons/outline-bookmarks.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + const BookmarksOutlinedIcon = createIcon([ 4 + [ 5 + 'M15 7v12.97l-4.21-1.81l-.79-.34l-.79.34L5 19.97V7zm4-6H8.99C7.89 1 7 1.9 7 3h10c1.1 0 2 .9 2 2v13l2 1V3c0-1.1-.9-2-2-2m-4 4H5c-1.1 0-2 .9-2 2v16l7-3l7 3V7c0-1.1-.9-2-2-2', 6 + ], 7 + ]); 8 + 9 + export default BookmarksOutlinedIcon;
+9
src/components/ic-icons/outline-directions-car.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + const DirectionsCarOutlinedIcon = createIcon([ 4 + [ 5 + 'M18.92 6.01C18.72 5.42 18.16 5 17.5 5h-11c-.66 0-1.21.42-1.42 1.01L3 12v8c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h12v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-8zM6.85 7h10.29l1.08 3.11H5.77zM19 17H5v-5h14z', 6 + ], 7 + ]); 8 + 9 + export default DirectionsCarOutlinedIcon;
+9
src/components/ic-icons/outline-explore.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + const ExploreOutlinedIcon = createIcon([ 4 + [ 5 + 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10s10-4.48 10-10S17.52 2 12 2m0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8s8 3.59 8 8s-3.59 8-8 8m-5.5-2.5l7.51-3.49L17.5 6.5L9.99 9.99zm5.5-6.6c.61 0 1.1.49 1.1 1.1s-.49 1.1-1.1 1.1s-1.1-.49-1.1-1.1s.49-1.1 1.1-1.1', 6 + ], 7 + ]); 8 + 9 + export default ExploreOutlinedIcon;
+9
src/components/ic-icons/outline-move-up.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + const MoveUpOutlinedIcon = createIcon([ 4 + [ 5 + 'M3 13c0-2.45 1.76-4.47 4.08-4.91l-1.49 1.5L7 11l4-4.01L7 3L5.59 4.41l1.58 1.58v.06A7.007 7.007 0 0 0 1 13c0 3.87 3.13 7 7 7h3v-2H8c-2.76 0-5-2.24-5-5m10 0v7h9v-7zm7 5h-5v-3h5zM13 4h9v7h-9z', 6 + ], 7 + ]); 8 + 9 + export default MoveUpOutlinedIcon;
+9
src/components/ic-icons/outline-visibility.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + const VisibilityOutlinedIcon = createIcon([ 4 + [ 5 + 'M12 6a9.77 9.77 0 0 1 8.82 5.5C19.17 14.87 15.79 17 12 17s-7.17-2.13-8.82-5.5A9.77 9.77 0 0 1 12 6m0-2C7 4 2.73 7.11 1 11.5C2.73 15.89 7 19 12 19s9.27-3.11 11-7.5C21.27 7.11 17 4 12 4m0 5a2.5 2.5 0 0 1 0 5a2.5 2.5 0 0 1 0-5m0-2c-2.48 0-4.5 2.02-4.5 4.5S9.52 16 12 16s4.5-2.02 4.5-4.5S14.48 7 12 7', 6 + ], 7 + ]); 8 + 9 + export default VisibilityOutlinedIcon;
+161
src/components/logger.tsx
··· 1 + import { For } from 'solid-js'; 2 + import { createMutable } from 'solid-js/store'; 3 + import { assert } from '~/lib/utils/invariant'; 4 + 5 + interface LogEntry { 6 + typ: 'log' | 'info' | 'warn' | 'error'; 7 + at: number; 8 + msg: string; 9 + } 10 + 11 + interface PendingLogEntry { 12 + msg: string; 13 + } 14 + 15 + export const createLogger = () => { 16 + const pending = createMutable<PendingLogEntry[]>([]); 17 + 18 + let backlog: LogEntry[] | undefined = []; 19 + let push = (entry: LogEntry) => { 20 + backlog!.push(entry); 21 + }; 22 + 23 + return { 24 + internal: { 25 + get pending() { 26 + return pending; 27 + }, 28 + attach(fn: (entry: LogEntry) => void) { 29 + if (backlog !== undefined) { 30 + for (let idx = 0, len = backlog.length; idx < len; idx++) { 31 + fn(backlog[idx]); 32 + } 33 + 34 + backlog = undefined; 35 + } 36 + 37 + push = fn; 38 + }, 39 + }, 40 + log(msg: string) { 41 + push({ typ: 'log', at: Date.now(), msg }); 42 + }, 43 + info(msg: string) { 44 + push({ typ: 'info', at: Date.now(), msg }); 45 + }, 46 + warn(msg: string) { 47 + push({ typ: 'warn', at: Date.now(), msg }); 48 + }, 49 + error(msg: string) { 50 + push({ typ: 'error', at: Date.now(), msg }); 51 + }, 52 + progress(initialMsg: string, throttleMs = 500) { 53 + pending.unshift({ msg: initialMsg }); 54 + 55 + let entry: PendingLogEntry | undefined = pending[0]; 56 + 57 + return { 58 + update: throttle((msg: string) => { 59 + if (entry !== undefined) { 60 + entry.msg = msg; 61 + } 62 + }, throttleMs), 63 + destroy() { 64 + if (entry !== undefined) { 65 + const index = pending.indexOf(entry); 66 + 67 + pending.splice(index, 1); 68 + entry = undefined; 69 + } 70 + }, 71 + [Symbol.dispose]() { 72 + this.destroy(); 73 + }, 74 + }; 75 + }, 76 + }; 77 + }; 78 + 79 + export interface LoggerProps { 80 + logger: ReturnType<typeof createLogger>; 81 + } 82 + 83 + const Logger = ({ logger }: LoggerProps) => { 84 + const formatter = new Intl.DateTimeFormat('en-US', { timeStyle: 'short', hour12: false }); 85 + 86 + return ( 87 + <ul class="flex flex-col py-3 font-mono text-xs empty:hidden"> 88 + <For each={logger.internal.pending}> 89 + {(entry) => ( 90 + <li class="flex gap-2 whitespace-pre-wrap px-4 py-1"> 91 + <span class="shrink-0 whitespace-pre-wrap font-medium text-gray-400">-----</span> 92 + <span class="break-words">{entry.msg}</span> 93 + </li> 94 + )} 95 + </For> 96 + 97 + <div 98 + ref={(node) => { 99 + logger.internal.attach(({ typ, at, msg }) => { 100 + let ecn = `flex gap-2 whitespace-pre-wrap px-4 py-1`; 101 + let tcn = `shrink-0 whitespace-pre-wrap font-medium`; 102 + if (typ === 'log') { 103 + tcn += ` text-gray-500`; 104 + } else if (typ === 'info') { 105 + ecn += ` bg-blue-200 text-blue-800`; 106 + tcn += ` text-blue-500`; 107 + } else if (typ === 'warn') { 108 + ecn += ` bg-amber-200 text-amber-800`; 109 + tcn += ` text-amber-500`; 110 + } else if (typ === 'error') { 111 + ecn += ` bg-red-200 text-red-800`; 112 + tcn += ` text-red-500`; 113 + } 114 + 115 + const item = ( 116 + <li class={ecn}> 117 + <span class={tcn}>{/* @once */ formatter.format(at)}</span> 118 + <span class="break-words">{msg}</span> 119 + </li> 120 + ); 121 + 122 + assert(item instanceof Node); 123 + node.after(item); 124 + }); 125 + }} 126 + ></div> 127 + </ul> 128 + ); 129 + }; 130 + 131 + export default Logger; 132 + 133 + const throttle = <T extends (...args: any[]) => void>(func: T, wait: number) => { 134 + let timeout: ReturnType<typeof setTimeout> | null = null; 135 + 136 + let lastArgs: Parameters<T> | null = null; 137 + let lastCallTime = 0; 138 + 139 + const invoke = () => { 140 + func(...lastArgs!); 141 + lastCallTime = Date.now(); 142 + timeout = null; 143 + }; 144 + 145 + return (...args: Parameters<T>) => { 146 + const now = Date.now(); 147 + const timeSinceLastCall = now - lastCallTime; 148 + 149 + lastArgs = args; 150 + 151 + if (timeSinceLastCall >= wait) { 152 + if (timeout !== null) { 153 + clearTimeout(timeout); 154 + } 155 + 156 + invoke(); 157 + } else if (timeout === null) { 158 + timeout = setTimeout(invoke, wait - timeSinceLastCall); 159 + } 160 + }; 161 + };
+1
src/components/page.tsx
··· 1 + export const Header = () => {};
+21
src/globals/multiagent.ts
··· 1 + import * as v from 'valibot'; 2 + 3 + import { didString, serviceUrlString } from '~/api/types/strings'; 4 + 5 + const hexColorString = v.pipe(v.string(), v.regex(v.HEX_COLOR_REGEX)); 6 + 7 + const multiagentAccountData = v.object({ 8 + did: v.pipe(didString, v.readonly()), 9 + service: serviceUrlString, 10 + session: v.unknown(), 11 + scope: v.union([v.literal('full'), v.literal('privileged'), v.literal('limited')]), 12 + name: v.string(), 13 + color: hexColorString, 14 + }); 15 + 16 + const multiagentStorage = v.object({ 17 + active: v.optional(didString), 18 + accounts: v.array(multiagentAccountData), 19 + }); 20 + 21 + console.log(multiagentStorage);
+9
src/globals/navigation.ts
··· 1 + import { createBrowserHistory } from '~/lib/navigation/history'; 2 + import { createHistoryLogger } from '~/lib/navigation/logger'; 3 + 4 + export const history = createBrowserHistory(); 5 + export const logger = createHistoryLogger(history); 6 + 7 + export const getEntryAt = (delta: number) => { 8 + return logger.entries[logger.active + delta]; 9 + };
src/globals/preferences.ts

This is a binary file and will not be displayed.

+5
src/globals/rpc.ts
··· 1 + import { simpleFetchHandler, XRPC } from '@atcute/client'; 2 + 3 + export const APPVIEW_URL = 'https://public.api.bsky.app'; 4 + 5 + export const appViewRpc = new XRPC({ handler: simpleFetchHandler({ service: APPVIEW_URL }) });
+120
src/lib/hooks/dropzone.ts
··· 1 + import { createEffect, createSignal } from 'solid-js'; 2 + 3 + import { createEventListener } from './event-listener'; 4 + 5 + const enum EventType { 6 + ENTER, 7 + OVER, 8 + LEAVE, 9 + DROP, 10 + } 11 + 12 + export interface CreateDropZoneOptions { 13 + dataTypes?: string[] | ((types: readonly string[]) => boolean); 14 + onDrop?: (files: File[] | null, event: DragEvent) => void; 15 + onEnter?: (files: File[] | null, event: DragEvent) => void; 16 + onLeave?: (files: File[] | null, event: DragEvent) => void; 17 + onOver?: (files: File[] | null, event: DragEvent) => void; 18 + multiple?: boolean; 19 + preventDefaultForUnhandled?: boolean; 20 + } 21 + 22 + export const createDropZone = ({ 23 + dataTypes, 24 + onDrop, 25 + onEnter, 26 + onLeave, 27 + onOver, 28 + multiple = true, 29 + preventDefaultForUnhandled = false, 30 + }: CreateDropZoneOptions = {}) => { 31 + let counter = 0; 32 + let isValid = true; 33 + 34 + const [targetEl, setTargetEl] = createSignal<HTMLElement>(); 35 + const [isDropping, setIsDropping] = createSignal(false); 36 + 37 + const getFiles = (event: DragEvent) => { 38 + const list = Array.from(event.dataTransfer?.files ?? []); 39 + return list.length === 0 ? null : multiple ? list : [list[0]]; 40 + }; 41 + 42 + const checkDataTypes: (types: string[]) => boolean = dataTypes 43 + ? typeof dataTypes === 'function' 44 + ? dataTypes 45 + : (types) => types.every((type) => dataTypes.includes(type)) 46 + : () => true; 47 + 48 + const checkValidity = (event: DragEvent) => { 49 + const items = Array.from(event.dataTransfer?.items ?? []); 50 + const types = items.map((item) => item.type); 51 + 52 + const dataTypesValid = checkDataTypes(types); 53 + const multipleFilesValid = multiple || items.length <= 1; 54 + 55 + return dataTypesValid && multipleFilesValid; 56 + }; 57 + 58 + const handleDragEvent = (type: EventType, event: DragEvent) => { 59 + if (counter === 0) { 60 + isValid = checkValidity(event); 61 + } 62 + 63 + if (!isValid) { 64 + if (preventDefaultForUnhandled) { 65 + event.preventDefault(); 66 + } 67 + if (event.dataTransfer) { 68 + event.dataTransfer.dropEffect = 'none'; 69 + } 70 + 71 + return; 72 + } 73 + 74 + event.preventDefault(); 75 + if (event.dataTransfer) { 76 + event.dataTransfer.dropEffect = 'copy'; 77 + } 78 + 79 + const currentFiles = getFiles(event); 80 + 81 + if (type === EventType.ENTER) { 82 + counter += 1; 83 + if (counter === 1) { 84 + setIsDropping(true); 85 + } 86 + 87 + onEnter?.(null, event); 88 + } else if (type === EventType.OVER) { 89 + onOver?.(null, event); 90 + } else if (type === EventType.LEAVE) { 91 + counter -= 1; 92 + if (counter === 0) { 93 + setIsDropping(false); 94 + } 95 + 96 + onLeave?.(null, event); 97 + } else if (type === EventType.DROP) { 98 + counter = 0; 99 + setIsDropping(false); 100 + 101 + if (isValid) { 102 + onDrop?.(currentFiles, event); 103 + } 104 + } 105 + }; 106 + 107 + createEffect(() => { 108 + const target = targetEl(); 109 + if (!target) { 110 + return; 111 + } 112 + 113 + createEventListener(target, 'dragenter', (event) => handleDragEvent(EventType.ENTER, event)); 114 + createEventListener(target, 'dragover', (event) => handleDragEvent(EventType.OVER, event)); 115 + createEventListener(target, 'dragleave', (event) => handleDragEvent(EventType.LEAVE, event)); 116 + createEventListener(target, 'drop', (event) => handleDragEvent(EventType.DROP, event)); 117 + }); 118 + 119 + return { ref: setTargetEl, isDropping }; 120 + };
+42
src/lib/hooks/event-listener.ts
··· 1 + import { onCleanup } from 'solid-js'; 2 + 3 + type UnknownFunction = (...args: any[]) => any; 4 + 5 + type InferEventType<TTarget> = TTarget extends { 6 + // we infer from 2 overloads which are super common for event targets in the DOM lib 7 + // we "prioritize" the first one as the first one is always more specific 8 + addEventListener(type: infer P, ...args: any): void; 9 + // we can ignore the second one as it's usually just a fallback that allows bare `string` here 10 + // we use `infer P2` over `any` as we really don't care about this type value 11 + // and we don't want to accidentally fail a type assignability check, remember that `any` isn't assignable to `never` 12 + addEventListener(type: infer _P2, ...args: any): void; 13 + } 14 + ? P & string 15 + : never; 16 + 17 + type InferEvent<TTarget, TType extends string> = `on${TType}` extends keyof TTarget 18 + ? Parameters<Extract<TTarget[`on${TType}`], UnknownFunction>>[0] 19 + : Event; 20 + 21 + // For listener objects, the handleEvent function has the object as the `this` binding 22 + type ListenerObject<TEvent extends Event> = { 23 + handleEvent(this: ListenerObject<TEvent>, event: TEvent): void; 24 + }; 25 + 26 + // event listeners can be an object or a function 27 + export type Listener<TTarget extends EventTarget, TType extends string> = 28 + | ListenerObject<InferEvent<TTarget, TType>> 29 + | { (this: TTarget, ev: InferEvent<TTarget, TType>): void }; 30 + 31 + export const createEventListener = < 32 + TTarget extends EventTarget, 33 + TType extends InferEventType<TTarget> | (string & {}), 34 + >( 35 + target: TTarget, 36 + type: TType, 37 + listener: Listener<TTarget, TType>, 38 + options?: boolean | AddEventListenerOptions, 39 + ) => { 40 + onCleanup(target.removeEventListener.bind(target, type, listener, options)); 41 + target.addEventListener(type, listener, options); 42 + };
+52
src/lib/hooks/local-storage.ts
··· 1 + import { createEffect } from 'solid-js'; 2 + import { type StoreNode, createMutable, modifyMutable, reconcile } from 'solid-js/store'; 3 + 4 + import { createEventListener } from './event-listener'; 5 + 6 + type ProduceFn<T> = (prev: unknown) => T; 7 + 8 + /** Useful for knowing whether an effect occured by external writes */ 9 + export let isExternalWriting = false; 10 + 11 + const parse = <T>(raw: string | null, produce: ProduceFn<T>): T => { 12 + if (raw !== null) { 13 + try { 14 + const persisted = JSON.parse(raw); 15 + 16 + if (persisted != null) { 17 + return produce(persisted); 18 + } 19 + } catch {} 20 + } 21 + 22 + return produce(null); 23 + }; 24 + 25 + export const createReactiveLocalStorage = <T extends StoreNode>(name: string, produce: ProduceFn<T>) => { 26 + const mutable = createMutable<T>(parse(localStorage.getItem(name), produce)); 27 + 28 + createEffect((inited) => { 29 + const json = JSON.stringify(mutable); 30 + 31 + if (inited && !isExternalWriting) { 32 + localStorage.setItem(name, json); 33 + } 34 + 35 + return true; 36 + }, false); 37 + 38 + createEventListener(window, 'storage', (ev) => { 39 + if (ev.key === name) { 40 + // Prevent our own effects from running, since this is already persisted. 41 + 42 + try { 43 + isExternalWriting = true; 44 + modifyMutable(mutable, reconcile(parse(ev.newValue, produce), { merge: true })); 45 + } finally { 46 + isExternalWriting = false; 47 + } 48 + } 49 + }); 50 + 51 + return mutable; 52 + };
+368
src/lib/navigation/history.ts
··· 1 + // Fork of `history` npm package 2 + // Repository: github.com/remix-run/history 3 + // Commit: 3e9dab413f4eda8d6bce565388c5ddb7aeff9f7e 4 + // Most of the changes are just trimming it down to only include the browser 5 + // history implementation. 6 + import { nanoid } from 'nanoid/non-secure'; 7 + 8 + export type Action = 'traverse' | 'push' | 'replace' | 'update'; 9 + 10 + export interface Path { 11 + /** A URL pathname, beginning with a /. */ 12 + pathname: string; 13 + /** A URL search string, beginning with a ?. */ 14 + search: string; 15 + /** A URL fragment identifier, beginning with a #. */ 16 + hash: string; 17 + } 18 + 19 + export interface Location extends Path { 20 + /** Position of this history */ 21 + index: number; 22 + /** A value of arbitrary data associated with this location. */ 23 + state: unknown; 24 + /** A unique string associated with this location */ 25 + key: string; 26 + } 27 + 28 + export interface Update { 29 + action: Action; 30 + location: Location; 31 + } 32 + 33 + export type Listener = (update: Update) => void; 34 + 35 + export interface Transition extends Update { 36 + retry(): void; 37 + } 38 + 39 + export type Blocker = (tx: Transition) => void; 40 + 41 + export type To = string | Partial<Path>; 42 + 43 + export interface History { 44 + readonly location: Location; 45 + 46 + createHref(to: To): string; 47 + 48 + navigate(to: To, options?: NavigateOptions): void; 49 + update(state: any): void; 50 + 51 + go(delta: number): void; 52 + back(): void; 53 + forward(): void; 54 + 55 + listen(listener: Listener): () => void; 56 + block(blocker: Blocker): () => void; 57 + } 58 + 59 + export interface NavigateOptions { 60 + replace?: boolean; 61 + state?: unknown; 62 + } 63 + 64 + /** 65 + * A browser history stores the current location in regular URLs in a web 66 + * browser environment. This is the standard for most web apps and provides the 67 + * cleanest URLs the browser's address bar. 68 + */ 69 + export interface BrowserHistory extends History {} 70 + 71 + const warning = (cond: any, message: string) => { 72 + if (!import.meta.env.PROD && !cond) { 73 + console.warn(message); 74 + } 75 + }; 76 + 77 + interface HistoryState { 78 + usr: any; 79 + key?: string; 80 + idx: number; 81 + } 82 + 83 + const BeforeUnloadEventType = 'beforeunload'; 84 + const PopStateEventType = 'popstate'; 85 + 86 + export interface BrowserHistoryOptions { 87 + window?: Window; 88 + } 89 + 90 + /** 91 + * Browser history stores the location in regular URLs. This is the standard for 92 + * most web apps, but it requires some configuration on the server to ensure you 93 + * serve the same app at multiple URLs. 94 + */ 95 + export const createBrowserHistory = (options: BrowserHistoryOptions = {}): BrowserHistory => { 96 + const { window = document.defaultView! } = options; 97 + const globalHistory = window.history; 98 + 99 + const getCurrentLocation = (): Location => { 100 + const { pathname, search, hash } = window.location; 101 + const state = globalHistory.state || {}; 102 + return { 103 + pathname, 104 + search, 105 + hash, 106 + index: state.idx, 107 + state: state.usr || null, 108 + key: state.key || 'default', 109 + }; 110 + }; 111 + 112 + let blockedPopTx: Transition | null = null; 113 + const handlePop = () => { 114 + if (blockedPopTx) { 115 + blockers.call(blockedPopTx); 116 + blockedPopTx = null; 117 + } else { 118 + const nextAction: Action = 'traverse'; 119 + const nextLocation = getCurrentLocation(); 120 + const nextIndex = nextLocation.index; 121 + 122 + if (blockers.length) { 123 + if (nextIndex != null) { 124 + const delta = location.index - nextIndex; 125 + if (delta) { 126 + // Revert the POP 127 + blockedPopTx = { 128 + action: nextAction, 129 + location: nextLocation, 130 + retry() { 131 + go(delta * -1); 132 + }, 133 + }; 134 + 135 + go(delta); 136 + } 137 + } else { 138 + // Trying to POP to a location with no index. We did not create 139 + // this location, so we can't effectively block the navigation. 140 + warning( 141 + false, 142 + // TODO: Write up a doc that explains our blocking strategy in 143 + // detail and link to it here so people can understand better what 144 + // is going on and how to avoid it. 145 + `You are trying to block a POP navigation to a location that was not ` + 146 + `created by the history library. The block will fail silently in ` + 147 + `production, but in general you should do all navigation with the ` + 148 + `history library (instead of using window.history.pushState directly) ` + 149 + `to avoid this situation.`, 150 + ); 151 + } 152 + } else { 153 + applyTx(nextAction); 154 + } 155 + } 156 + }; 157 + 158 + const listeners = createEvents<Listener>(); 159 + const blockers = createEvents<Blocker>(); 160 + 161 + let location = getCurrentLocation(); 162 + 163 + window.addEventListener(PopStateEventType, handlePop); 164 + 165 + if (location.index == null) { 166 + globalHistory.replaceState({ ...globalHistory.state, idx: (location.index = 0) }, ''); 167 + } 168 + 169 + const createHref = (to: To): string => { 170 + return typeof to === 'string' ? to : createPath(to); 171 + }; 172 + 173 + // state defaults to `null` because `window.history.state` does 174 + const getNextLocation = (to: To, index: number, state: any = null): Location => { 175 + return { 176 + pathname: location.pathname, 177 + hash: '', 178 + search: '', 179 + ...(typeof to === 'string' ? parsePath(to) : to), 180 + index, 181 + state, 182 + key: createKey(), 183 + }; 184 + }; 185 + 186 + const getHistoryStateAndUrl = (nextLocation: Location): [HistoryState, string] => { 187 + return [ 188 + { 189 + usr: nextLocation.state, 190 + key: nextLocation.key, 191 + idx: nextLocation.index, 192 + }, 193 + createHref(nextLocation), 194 + ]; 195 + }; 196 + 197 + const allowTx = (action: Action, location: Location, retry: () => void): boolean => { 198 + return !blockers.length || (blockers.call({ action, location, retry }), false); 199 + }; 200 + 201 + const applyTx = (nextAction: Action): void => { 202 + location = getCurrentLocation(); 203 + listeners.call({ action: nextAction, location }); 204 + }; 205 + 206 + const navigate = (to: To, { replace, state }: NavigateOptions = {}): void => { 207 + const nextAction: Action = !replace ? 'push' : 'replace'; 208 + const nextIndex = location.index + (!replace ? 1 : 0); 209 + const nextLocation = getNextLocation(to, nextIndex, state); 210 + 211 + const retry = () => { 212 + navigate(to, { replace, state }); 213 + }; 214 + 215 + if (allowTx(nextAction, nextLocation, retry)) { 216 + const [historyState, url] = getHistoryStateAndUrl(nextLocation); 217 + 218 + // TODO: Support forced reloading 219 + if (!replace) { 220 + // try...catch because iOS limits us to 100 pushState calls :/ 221 + try { 222 + globalHistory.pushState(historyState, '', url); 223 + } catch { 224 + // They are going to lose state here, but there is no real 225 + // way to warn them about it since the page will refresh... 226 + window.location.assign(url); 227 + } 228 + } else { 229 + globalHistory.replaceState(historyState, '', url); 230 + } 231 + 232 + applyTx(nextAction); 233 + } 234 + }; 235 + 236 + const update = (state: any): void => { 237 + const nextAction: Action = 'update'; 238 + const nextLocation = { ...location, state }; 239 + 240 + const [historyState, url] = getHistoryStateAndUrl(nextLocation); 241 + 242 + // TODO: Support forced reloading 243 + globalHistory.replaceState(historyState, '', url); 244 + 245 + applyTx(nextAction); 246 + }; 247 + 248 + const go = (delta: number): void => { 249 + globalHistory.go(delta); 250 + }; 251 + 252 + const history: BrowserHistory = { 253 + get location() { 254 + return location; 255 + }, 256 + createHref, 257 + navigate, 258 + update, 259 + go, 260 + back: () => { 261 + return go(-1); 262 + }, 263 + forward: () => { 264 + return go(1); 265 + }, 266 + listen: (listener) => { 267 + return listeners.push(listener); 268 + }, 269 + block: (blocker) => { 270 + const unblock = blockers.push(blocker); 271 + 272 + if (blockers.length === 1) { 273 + window.addEventListener(BeforeUnloadEventType, promptBeforeUnload); 274 + } 275 + 276 + return () => { 277 + unblock(); 278 + 279 + // Remove the beforeunload listener so the document may 280 + // still be salvageable in the pagehide event. 281 + // See https://html.spec.whatwg.org/#unloading-documents 282 + if (!blockers.length) { 283 + window.removeEventListener(BeforeUnloadEventType, promptBeforeUnload); 284 + } 285 + }; 286 + }, 287 + }; 288 + 289 + return history; 290 + }; 291 + 292 + const promptBeforeUnload = (event: BeforeUnloadEvent): void => { 293 + // Cancel the event. 294 + event.preventDefault(); 295 + }; 296 + 297 + interface Events<F extends (arg: any) => void> { 298 + length: number; 299 + push: (fn: F) => () => void; 300 + call: (arg: Parameters<F>[0]) => void; 301 + } 302 + 303 + const createEvents = <F extends (arg: any) => void>(): Events<F> => { 304 + const handlers: F[] = []; 305 + 306 + return { 307 + get length() { 308 + return handlers.length; 309 + }, 310 + push(fn: F) { 311 + handlers.push(fn); 312 + 313 + return () => { 314 + const index = handlers.indexOf(fn); 315 + handlers.splice(index, 1); 316 + }; 317 + }, 318 + call(arg) { 319 + for (let idx = 0, len = handlers.length; idx < len; idx++) { 320 + (0, handlers[idx])(arg); 321 + } 322 + }, 323 + }; 324 + }; 325 + 326 + const createKey = () => { 327 + return nanoid(); 328 + }; 329 + 330 + /** 331 + * Creates a string URL path from the given pathname, search, and hash components. 332 + */ 333 + export const createPath = ({ pathname = '/', search = '', hash = '' }: Partial<Path>) => { 334 + if (search && search !== '?') { 335 + pathname += search.charAt(0) === '?' ? search : '?' + search; 336 + } 337 + if (hash && hash !== '#') { 338 + pathname += hash.charAt(0) === '#' ? hash : '#' + hash; 339 + } 340 + return pathname; 341 + }; 342 + 343 + /** 344 + * Parses a string URL path into its separate pathname, search, and hash components. 345 + */ 346 + export const parsePath = (path: string): Partial<Path> => { 347 + const parsedPath: Partial<Path> = {}; 348 + 349 + if (path) { 350 + const hashIndex = path.indexOf('#'); 351 + if (hashIndex >= 0) { 352 + parsedPath.hash = path.substr(hashIndex); 353 + path = path.substr(0, hashIndex); 354 + } 355 + 356 + const searchIndex = path.indexOf('?'); 357 + if (searchIndex >= 0) { 358 + parsedPath.search = path.substr(searchIndex); 359 + path = path.substr(0, searchIndex); 360 + } 361 + 362 + if (path) { 363 + parsedPath.pathname = path; 364 + } 365 + } 366 + 367 + return parsedPath; 368 + };
+89
src/lib/navigation/logger.ts
··· 1 + // Keeps a best-effort log of history entries. 2 + // To simplify our stack router implementation on dealing with PWA-specific 3 + // aspects, `createHistoryLogger` is set to drop forward entries by default. 4 + import type { History, Location } from './history'; 5 + 6 + export interface HistoryLogger { 7 + readonly current: Location; 8 + readonly active: number; 9 + readonly entries: (Location | null)[]; 10 + readonly canGoBack: boolean; 11 + readonly canGoForward: boolean; 12 + } 13 + 14 + export const createHistoryLogger = (history: History, keepForwardEntries = false): HistoryLogger => { 15 + const loc = history.location; 16 + 17 + let active = loc.index; 18 + let entries = arr(active + 1, (i) => (i === active ? loc : null)); 19 + 20 + history.listen(({ action, location }) => { 21 + const index = location.index; 22 + 23 + if (action === 'push') { 24 + // New page pushed 25 + 26 + entries = entries.toSpliced(active + 1, entries.length, location); 27 + } else if (action === 'replace' || action === 'update') { 28 + // Current page replaced, or updated with new state 29 + 30 + entries = entries.with(active, location); 31 + } else if (action === 'traverse') { 32 + // Traversal happened 33 + 34 + if (keepForwardEntries) { 35 + if (index >= entries.length) { 36 + const length = entries.length; 37 + const delta = index - length; 38 + 39 + const extras = arr(delta + 1, (i) => (i === delta ? location : null)); 40 + 41 + entries = entries.concat(extras); 42 + } else if (entries[index] === null) { 43 + entries = entries.with(index, location); 44 + } 45 + } else { 46 + if (index < active) { 47 + if (entries[index] !== null) { 48 + entries = entries.slice(0, index + 1); 49 + } else { 50 + entries = entries.toSpliced(index, entries.length, location); 51 + } 52 + } else if (index >= entries.length) { 53 + const length = entries.length; 54 + const delta = index - length; 55 + 56 + const extras = arr(delta + 1, (i) => (i === delta ? location : null)); 57 + 58 + entries = entries.concat(extras); 59 + } 60 + } 61 + } 62 + 63 + active = index; 64 + }); 65 + 66 + return { 67 + get current() { 68 + // Current entry is guaranteed to exist 69 + return entries[active]!; 70 + }, 71 + get active() { 72 + return active; 73 + }, 74 + get entries() { 75 + return entries; 76 + }, 77 + 78 + get canGoBack() { 79 + return active !== 0; 80 + }, 81 + get canGoForward() { 82 + return active !== entries.length - 1; 83 + }, 84 + }; 85 + }; 86 + 87 + const arr = <T>(length: number, map: (index: number) => T): T[] => { 88 + return Array.from({ length }, (_, idx) => map(idx)); 89 + };
+373
src/lib/navigation/router.tsx
··· 1 + /* @refresh reload */ 2 + import { 3 + type Component, 4 + For, 5 + type JSX, 6 + type Owner, 7 + createContext, 8 + createMemo, 9 + createRoot, 10 + createSignal, 11 + getOwner, 12 + onCleanup, 13 + useContext, 14 + } from 'solid-js'; 15 + import { delegateEvents } from 'solid-js/web'; 16 + 17 + import { EventEmitter } from '@mary/events'; 18 + import { Freeze } from '@mary/solid-freeze'; 19 + 20 + import { createEventListener } from '../hooks/event-listener'; 21 + 22 + import type { History, Location } from './history'; 23 + import type { HistoryLogger } from './logger'; 24 + 25 + // This is the only application-specific code we have here, might need to 26 + // move it elsewhere, maybe as a separate package? 27 + export interface RouteMeta { 28 + name?: string; 29 + main?: boolean; 30 + public?: boolean; 31 + } 32 + 33 + export interface RouteDefinition { 34 + path: string; 35 + component: Component; 36 + single?: boolean; 37 + meta?: RouteMeta; 38 + validate?: (params: Record<string, string>) => boolean; 39 + } 40 + 41 + interface InternalRouteDefinition extends RouteDefinition { 42 + _regex?: RegExp; 43 + } 44 + 45 + export interface RouterOptions { 46 + history: History; 47 + logger: HistoryLogger; 48 + routes: RouteDefinition[]; 49 + } 50 + 51 + interface MatchedRoute { 52 + readonly id: string | undefined; 53 + readonly def: RouteDefinition; 54 + readonly params: Record<string, string>; 55 + } 56 + 57 + export interface MatchedRouteState extends MatchedRoute { 58 + readonly id: string; 59 + } 60 + 61 + interface RouterState { 62 + active: string; 63 + views: Record<string, MatchedRouteState>; 64 + singles: Record<string, MatchedRouteState>; 65 + } 66 + 67 + interface ViewContextObject { 68 + owner: Owner | null; 69 + route: MatchedRouteState; 70 + } 71 + 72 + let _entry: Location; 73 + 74 + let _routes: InternalRouteDefinition[] | undefined; 75 + let _cleanup: (() => void) | undefined; 76 + 77 + const [state, setState] = createSignal<RouterState>({ 78 + active: '', 79 + views: {}, 80 + singles: {}, 81 + }); 82 + 83 + interface RouteEvent { 84 + focus: boolean; 85 + enter: boolean; 86 + } 87 + 88 + const routerEvents = new EventEmitter<{ [key: string]: (event: RouteEvent) => void }>(); 89 + 90 + export { routerEvents as UNSAFE_routerEvents }; 91 + 92 + export const configureRouter = ({ history, logger: log, routes }: RouterOptions) => { 93 + _cleanup?.(); 94 + 95 + _routes = routes; 96 + 97 + { 98 + _entry = log.current; 99 + 100 + const pathname = _entry.pathname; 101 + const matched = matchRoute(pathname); 102 + 103 + if (matched) { 104 + const nextKey = matched.id || _entry.key; 105 + 106 + const isSingle = !!matched.id; 107 + const matchedState: MatchedRouteState = { ...matched, id: nextKey }; 108 + 109 + const next: Record<string, MatchedRouteState> = { [nextKey]: matchedState }; 110 + 111 + setState({ 112 + active: nextKey, 113 + views: isSingle ? {} : next, 114 + singles: isSingle ? next : {}, 115 + }); 116 + } 117 + } 118 + 119 + _cleanup = createRoot((cleanup) => { 120 + createEventListener; 121 + 122 + onCleanup( 123 + history.listen(({ action, location: nextEntry }) => { 124 + const currentEntry = _entry; 125 + _entry = nextEntry; 126 + 127 + if (action !== 'update') { 128 + const pathname = nextEntry.pathname; 129 + let matched = matchRoute(pathname); 130 + 131 + if (!matched) { 132 + return; 133 + } 134 + 135 + const current = state(); 136 + 137 + let views = current.views; 138 + let singles = current.singles; 139 + let isNew = false; 140 + 141 + const nextId = matched.id || nextEntry.key; 142 + const matchedState: MatchedRouteState = { ...matched, id: nextId }; 143 + 144 + let nextViews: typeof views | undefined; 145 + 146 + // Recreate the views object to remove no longer reachable views if: 147 + // - We're pushing a new page, or replacing the current page 148 + // - We're traversing and the intended index is lower than current 149 + if (action !== 'traverse' || nextEntry.index < currentEntry.index) { 150 + const entries = log.entries; 151 + 152 + nextViews = {}; 153 + 154 + for (let idx = 0, len = entries.length; idx < len; idx++) { 155 + const entry = entries[idx]; 156 + const key = entry?.key; 157 + 158 + if (key !== undefined && key in views) { 159 + nextViews[key] = views[key]; 160 + } 161 + } 162 + } 163 + 164 + if (!matched.id) { 165 + // Add this view, if it's already present, set `shouldCall` to true 166 + if (!(nextId in views)) { 167 + if (nextViews) { 168 + nextViews[nextId] = matchedState; 169 + } else { 170 + nextViews = { ...views, [nextId]: matchedState }; 171 + isNew = true; 172 + } 173 + } 174 + } else { 175 + // Add this view, if it's already present, set `shouldCall` to true 176 + if (!(nextId in singles)) { 177 + singles = { ...singles, [nextId]: matchedState }; 178 + isNew = true; 179 + } 180 + } 181 + 182 + if (nextViews) { 183 + views = nextViews; 184 + } 185 + 186 + routerEvents.emit(current.active, { focus: false, enter: false }); 187 + setState({ active: nextId, views: views, singles: singles }); 188 + 189 + if (!isNew) { 190 + routerEvents.emit(nextId, { 191 + focus: true, 192 + enter: action !== 'traverse' || nextEntry.index > currentEntry.index, 193 + }); 194 + } 195 + 196 + // Scroll to top if we're pushing or replacing, it's a new page. 197 + if (!matched.id && (action === 'push' || action === 'replace')) { 198 + window.scrollTo({ top: 0, behavior: 'instant' }); 199 + } 200 + } 201 + }), 202 + ); 203 + 204 + delegateEvents(['click']); 205 + createEventListener(document, 'click', (evt) => { 206 + if ( 207 + evt.defaultPrevented || 208 + evt.button !== 0 || 209 + evt.metaKey || 210 + evt.altKey || 211 + evt.ctrlKey || 212 + evt.shiftKey 213 + ) { 214 + return; 215 + } 216 + 217 + const a = evt.composedPath().find((el): el is HTMLAnchorElement => el instanceof HTMLAnchorElement); 218 + 219 + if (!a) { 220 + return; 221 + } 222 + 223 + const href = a.href; 224 + const target = a.target; 225 + 226 + if (!href || (target !== '' && target !== '_self')) { 227 + return; 228 + } 229 + 230 + const { origin, pathname, search, hash } = new URL(href); 231 + 232 + if (location.origin !== origin) { 233 + return; 234 + } 235 + 236 + evt.preventDefault(); 237 + 238 + if (location.pathname !== pathname || location.search !== search || location.hash !== hash) { 239 + history.navigate({ pathname, search, hash }); 240 + } 241 + }); 242 + 243 + return cleanup; 244 + }); 245 + }; 246 + 247 + const ViewContext = createContext<ViewContextObject>(); 248 + 249 + const getMatchedRoute = () => { 250 + const current = state(); 251 + const active = current.active; 252 + 253 + const match = current.singles[active] || current.views[active]; 254 + 255 + if (match) { 256 + return match; 257 + } 258 + }; 259 + 260 + export const useMatchedRoute = () => { 261 + return createMemo(getMatchedRoute); 262 + }; 263 + 264 + const useViewContext = () => { 265 + return useContext(ViewContext)!; 266 + }; 267 + 268 + export { useViewContext as UNSAFE_useViewContext }; 269 + 270 + export const useParams = <T extends Record<string, string>>() => { 271 + return useViewContext().route.params as T; 272 + }; 273 + 274 + export const onRouteEnter = (cb: () => void) => { 275 + const { route } = useViewContext(); 276 + 277 + cb(); 278 + onCleanup(routerEvents.on(route.id, (e) => e.enter && cb())); 279 + }; 280 + 281 + export interface RouterViewProps { 282 + render: (matched: MatchedRouteState) => JSX.Element; 283 + } 284 + 285 + export const RouterView = (props: RouterViewProps) => { 286 + const render = props.render; 287 + 288 + const renderView = (matched: MatchedRouteState) => { 289 + const def = matched.def; 290 + const id = matched.id; 291 + 292 + const active = createMemo((): boolean => state().active === id); 293 + 294 + const context: ViewContextObject = { 295 + owner: getOwner(), 296 + route: matched, 297 + }; 298 + 299 + if (def.single) { 300 + let storedHeight: number | undefined; 301 + 302 + onCleanup( 303 + routerEvents.on(id, (ev) => { 304 + if (!ev.focus) { 305 + storedHeight = document.documentElement.scrollTop; 306 + } else if (storedHeight !== undefined) { 307 + window.scrollTo({ top: storedHeight, behavior: 'instant' }); 308 + } 309 + }), 310 + ); 311 + } 312 + 313 + return ( 314 + <Freeze freeze={!active()}> 315 + <ViewContext.Provider value={context}>{render(matched)}</ViewContext.Provider> 316 + </Freeze> 317 + ); 318 + }; 319 + 320 + return ( 321 + <> 322 + <For each={Object.values(state().views)}>{renderView}</For> 323 + <For each={Object.values(state().singles)}>{renderView}</For> 324 + </> 325 + ); 326 + }; 327 + 328 + const matchRoute = (path: string): MatchedRoute | null => { 329 + for (let idx = 0, len = _routes!.length; idx < len; idx++) { 330 + const route = _routes![idx]; 331 + 332 + const validate = route.validate; 333 + const pattern = (route._regex ||= buildPathRegex(route.path)); 334 + 335 + const match = pattern.exec(path); 336 + 337 + if (!match || (validate && !validate(match.groups!))) { 338 + continue; 339 + } 340 + 341 + const params = match.groups!; 342 + 343 + let id: string | undefined; 344 + if (route.single) { 345 + id = '@' + idx; 346 + for (const param in params) { 347 + id += '/' + params[param]; 348 + } 349 + } 350 + 351 + return { id: id, def: route, params: params }; 352 + } 353 + 354 + return null; 355 + }; 356 + 357 + const buildPathRegex = (path: string) => { 358 + let source = 359 + '^' + 360 + path 361 + .replace(/\/*\*?$/, '') 362 + .replace(/^\/*/, '/') 363 + .replace(/[\\.*+^${}|()[\]]/g, '\\$&') 364 + .replace(/\/:([\w-]+)(\?)?/g, '/$2(?<$1>[^\\/]+)$2'); 365 + 366 + source += path.endsWith('*') 367 + ? path === '*' || path === '/*' 368 + ? '(?<$>.*)$' 369 + : '(?:\\/(?<$>.+)|\\/*)$' 370 + : '\\/*$'; 371 + 372 + return new RegExp(source, 'i'); 373 + };
+23
src/lib/utils/abortable.ts
··· 1 + import { onCleanup } from 'solid-js'; 2 + 3 + type Abortable = [signal: () => AbortSignal, cleanup: () => void]; 4 + 5 + export const makeAbortable = (): Abortable => { 6 + let controller: AbortController | undefined; 7 + 8 + const cleanup = (): void => { 9 + controller?.abort(); 10 + return (controller = undefined); 11 + }; 12 + 13 + const signal = (): AbortSignal => { 14 + cleanup(); 15 + 16 + controller = new AbortController(); 17 + return controller.signal; 18 + }; 19 + 20 + onCleanup(cleanup); 21 + 22 + return [signal, cleanup]; 23 + };
+54
src/lib/utils/dequal.ts
··· 1 + const keys = Object.keys; 2 + 3 + export const dequal = (a: any, b: any): boolean => { 4 + let ctor: any; 5 + let len: number; 6 + 7 + if (a === b) { 8 + return true; 9 + } 10 + 11 + if (a && b && (ctor = a.constructor) === b.constructor) { 12 + if (ctor === Array) { 13 + if ((len = a.length) === b.length) { 14 + while (len--) { 15 + if (!dequal(a[len], b[len])) { 16 + return false; 17 + } 18 + } 19 + } 20 + 21 + return len === -1; 22 + } else if (!ctor || ctor === Object) { 23 + len = 0; 24 + 25 + for (ctor in a) { 26 + len++; 27 + 28 + if (!(ctor in b) || !dequal(a[ctor], b[ctor])) { 29 + return false; 30 + } 31 + } 32 + 33 + return keys(b).length === len; 34 + } 35 + } 36 + 37 + return a !== a && b !== b; 38 + }; 39 + 40 + export const EQUALS_DEQUAL = { equals: dequal } as const; 41 + 42 + export const sequal = (a: any[], b: any[]): boolean => { 43 + let len = a.length; 44 + 45 + if (len === b.length) { 46 + while (len--) { 47 + if (a[len] !== b[len]) { 48 + return false; 49 + } 50 + } 51 + } 52 + 53 + return len === -1; 54 + };
+38
src/lib/utils/intl/bytes.ts
··· 1 + const BYTE = 1; 2 + const KILOBYTE = BYTE * 1000; 3 + const MEGABYTE = KILOBYTE * 1000; 4 + const GIGABYTE = MEGABYTE * 1000; 5 + 6 + export function formatBytes(size: number) { 7 + let num = size; 8 + let fractions = 0; 9 + let unit: string; 10 + 11 + if (size < KILOBYTE) { 12 + unit = 'byte'; 13 + } else if (size < MEGABYTE) { 14 + num /= KILOBYTE; 15 + unit = 'kilobyte'; 16 + } else if (size < GIGABYTE) { 17 + num /= MEGABYTE; 18 + unit = 'megabyte'; 19 + } else { 20 + num /= GIGABYTE; 21 + unit = 'gigabyte'; 22 + } 23 + 24 + if (num > 100) { 25 + fractions = 0; 26 + } else if (num > 10) { 27 + fractions = 1; 28 + } else if (num > 1) { 29 + fractions = 2; 30 + } 31 + 32 + return num.toLocaleString('en-US', { 33 + style: 'unit', 34 + unit: unit, 35 + unitDisplay: 'short', 36 + maximumFractionDigits: fractions, 37 + }); 38 + }
+9
src/lib/utils/invariant.ts
··· 1 + export function assert(condition: any, message?: string): asserts condition { 2 + if (!condition) { 3 + if (import.meta.env.DEV) { 4 + throw new Error(`Assertion failed` + (message ? `: ${message}` : ``)); 5 + } else { 6 + throw new Error(`Assertion failed`); 7 + } 8 + } 9 + }
+129
src/lib/utils/query.ts
··· 1 + import { createRenderEffect, createSignal } from 'solid-js'; 2 + 3 + import { makeAbortable } from './abortable'; 4 + import { dequal } from './dequal'; 5 + 6 + export interface SuccessQueryReturn<R> { 7 + data: R; 8 + error: undefined; 9 + isSuccess: true; 10 + isError: false; 11 + isPending: false; 12 + isIdle: false; 13 + refetch(): void; 14 + } 15 + export interface ErrorQueryReturn { 16 + data: undefined; 17 + error: unknown; 18 + isSuccess: false; 19 + isError: true; 20 + isPending: false; 21 + isIdle: false; 22 + refetch(): void; 23 + } 24 + export interface PendingQueryReturn { 25 + data: undefined; 26 + error: undefined; 27 + isSuccess: false; 28 + isError: false; 29 + isPending: true; 30 + isIdle: false; 31 + refetch(): void; 32 + } 33 + export interface IdleQueryReturn { 34 + data: undefined; 35 + error: undefined; 36 + isSuccess: false; 37 + isError: false; 38 + isPending: false; 39 + isIdle: true; 40 + refetch(): void; 41 + } 42 + 43 + export type QueryReturn<R> = SuccessQueryReturn<R> | ErrorQueryReturn | PendingQueryReturn; 44 + 45 + const enum QueryState { 46 + IDLE, 47 + PENDING, 48 + SUCCESS, 49 + ERROR, 50 + } 51 + 52 + const UNSET_QUERY_KEY = Symbol(); 53 + 54 + export const createQuery = <K, R>( 55 + keyFn: () => K | null | undefined, 56 + queryFn: (key: K, signal: AbortSignal) => Promise<R>, 57 + ): QueryReturn<R> => { 58 + let currKey: any = UNSET_QUERY_KEY; 59 + 60 + const [getSignal, cleanup] = makeAbortable(); 61 + const [state, setState] = createSignal<{ s: QueryState; d?: any; e?: any }>( 62 + { s: QueryState.IDLE }, 63 + { equals: (prev, next) => prev.s === next.s }, 64 + ); 65 + 66 + const refetch = (force: boolean) => { 67 + const nextKey = keyFn(); 68 + const prevKey = currKey; 69 + currKey = nextKey; 70 + 71 + if (nextKey == null) { 72 + cleanup(); 73 + setState({ s: QueryState.IDLE }); 74 + } else if (force || !dequal(nextKey, prevKey)) { 75 + setState({ s: QueryState.PENDING }); 76 + 77 + const signal = getSignal(); 78 + 79 + new Promise((resolve) => resolve(queryFn(nextKey, signal))).then( 80 + (data) => { 81 + if (signal.aborted) { 82 + return; 83 + } 84 + 85 + setState({ s: QueryState.SUCCESS, d: data }); 86 + }, 87 + (err) => { 88 + if (signal.aborted) { 89 + return; 90 + } 91 + 92 + setState({ s: QueryState.ERROR, e: err }); 93 + }, 94 + ); 95 + } 96 + }; 97 + 98 + createRenderEffect(() => refetch(false)); 99 + 100 + return { 101 + get data() { 102 + const $state = state(); 103 + if ($state.s === QueryState.SUCCESS) { 104 + return $state.d; 105 + } 106 + }, 107 + get error() { 108 + const $state = state(); 109 + if ($state.s === QueryState.ERROR) { 110 + return $state.e; 111 + } 112 + }, 113 + get isSuccess() { 114 + return state().s === QueryState.SUCCESS; 115 + }, 116 + get isError() { 117 + return state().s === QueryState.ERROR; 118 + }, 119 + get isPending() { 120 + return state().s === QueryState.PENDING; 121 + }, 122 + get isIdle() { 123 + return state().s === QueryState.IDLE; 124 + }, 125 + refetch() { 126 + refetch(true); 127 + }, 128 + } as any; 129 + };
+245
src/lib/utils/search-params.ts
··· 1 + import { batch, createSignal } from 'solid-js'; 2 + 3 + import { At } from '@atcute/client/lexicons'; 4 + 5 + import { DID_OR_HANDLE_RE, DID_RE, HANDLE_RE } from '~/api/utils/strings'; 6 + 7 + export interface ParamParser<T> { 8 + parse: (value: string | string[] | null) => T | null; 9 + serialize: (value: T) => string | string[] | null; 10 + } 11 + 12 + export interface BuiltParamParser<T> extends ParamParser<T> { 13 + equals(a: T, b: T): boolean; 14 + withDefault(value: NonNullable<T>): BuiltParamParser<T> & { readonly defaultValue: NonNullable<T> }; 15 + } 16 + 17 + type Nullable<T> = { 18 + [K in keyof T]: T[K] | null; 19 + }; 20 + 21 + type Prettify<T> = { 22 + [K in keyof T]: T[K]; 23 + } & {}; 24 + 25 + export type ParamParserWithDefault<T> = ParamParser<T> & { defaultValue?: T }; 26 + 27 + export type ParamParserMap<Map = any> = { 28 + [Key in keyof Map]: ParamParserWithDefault<Map[Key]>; 29 + }; 30 + 31 + export type ParamValues<T extends ParamParserMap> = Prettify<{ 32 + [K in keyof T]: T[K]['defaultValue'] extends NonNullable<ReturnType<T[K]['parse']>> 33 + ? NonNullable<ReturnType<T[K]['parse']>> 34 + : ReturnType<T[K]['parse']> | null; 35 + }>; 36 + 37 + export type SetParamValues<M extends ParamParserMap> = ( 38 + values: Partial<Nullable<ParamValues<M>>> | null, 39 + ) => void; 40 + 41 + export type UseSearchParamsReturn<M extends ParamParserMap> = [ParamValues<M>, SetParamValues<M>]; 42 + 43 + export const useSearchParams = <M extends ParamParserMap>(map: M): UseSearchParamsReturn<M> => { 44 + let searchParams = new URLSearchParams(location.search); 45 + let mappedSearchParams: ParamValues<M>; 46 + 47 + { 48 + const mapped: any = {}; 49 + 50 + for (const key in map) { 51 + const parser = map[key]; 52 + 53 + let rawValue = null; 54 + if (searchParams.has(key)) { 55 + rawValue = searchParams.getAll(key); 56 + 57 + if (rawValue.length === 1) { 58 + rawValue = rawValue[0]; 59 + } 60 + } 61 + 62 + let value = parser.parse(rawValue); 63 + if (value === null && 'defaultValue' in parser) { 64 + value = parser.defaultValue; 65 + } 66 + 67 + mapped[key] = value; 68 + } 69 + 70 + mappedSearchParams = createStateObject(mapped); 71 + } 72 + 73 + const update = () => { 74 + const search = searchParams.toString(); 75 + const path = location.pathname + (search ? '?' + search : '') + location.hash; 76 + 77 + history.replaceState(history.state, '', path); 78 + }; 79 + 80 + const setParams: SetParamValues<M> = (values: any) => { 81 + return batch(() => { 82 + if (values === null) { 83 + for (const key in map) { 84 + const parser = map[key]; 85 + 86 + mappedSearchParams[key] = parser.defaultValue ?? null; 87 + searchParams.delete(key); 88 + } 89 + 90 + update(); 91 + return; 92 + } 93 + 94 + for (const key in values) { 95 + const parser = map[key]; 96 + if (!parser) { 97 + continue; 98 + } 99 + 100 + const value = values[key]; 101 + if (value === undefined) { 102 + continue; 103 + } 104 + 105 + const serialized = value !== null ? parser.serialize(value) : null; 106 + if (serialized !== null) { 107 + // @ts-expect-error 108 + mappedSearchParams[key] = value; 109 + 110 + if (Array.isArray(serialized)) { 111 + for (let idx = 0, len = serialized.length; idx < len; idx++) { 112 + if (idx === 0) { 113 + searchParams.set(key, serialized[idx]); 114 + } else { 115 + searchParams.append(key, serialized[idx]); 116 + } 117 + } 118 + } else { 119 + searchParams.set(key, serialized); 120 + } 121 + } else { 122 + // @ts-expect-error 123 + mappedSearchParams[key] = parser.defaultValue ?? null; 124 + searchParams.delete(key); 125 + } 126 + } 127 + 128 + update(); 129 + }); 130 + }; 131 + 132 + return [mappedSearchParams, setParams]; 133 + }; 134 + 135 + const createStateObject = <T extends Record<string, any>>(obj: T): T => { 136 + const state = {} as T; 137 + 138 + for (const key in obj) { 139 + const [value, setValue] = createSignal(obj[key]); 140 + 141 + Object.defineProperty(state, key, { 142 + get: value, 143 + set: (next) => setValue(typeof next === 'function' ? () => next : next), 144 + }); 145 + } 146 + 147 + return state; 148 + }; 149 + 150 + /*#__NO_SIDE_EFFECTS__*/ 151 + const createParser = <T>(parser: ParamParser<T>): BuiltParamParser<T> => { 152 + return { 153 + ...parser, 154 + equals(a, b) { 155 + return a === b; 156 + }, 157 + withDefault(value) { 158 + return { ...this, defaultValue: value }; 159 + }, 160 + }; 161 + }; 162 + 163 + export const asString = createParser({ 164 + parse(value) { 165 + if (typeof value === 'string') { 166 + return value; 167 + } 168 + 169 + return null; 170 + }, 171 + serialize(value) { 172 + return value; 173 + }, 174 + }); 175 + 176 + export const asInteger = createParser({ 177 + parse(value) { 178 + if (typeof value === 'string') { 179 + const num = +value; 180 + if (Number.isSafeInteger(num) && num >= 0) { 181 + return num; 182 + } 183 + } 184 + 185 + return null; 186 + }, 187 + serialize(value) { 188 + return '' + value; 189 + }, 190 + }); 191 + 192 + export const asBoolean = createParser({ 193 + parse(value) { 194 + if (value === null) { 195 + return false; 196 + } 197 + if (value === '') { 198 + return true; 199 + } 200 + 201 + return null; 202 + }, 203 + serialize(value) { 204 + return value ? '' : null; 205 + }, 206 + }); 207 + 208 + export const asDID = createParser({ 209 + parse(value) { 210 + if (typeof value === 'string' && DID_RE.test(value)) { 211 + return value as At.DID; 212 + } 213 + 214 + return null; 215 + }, 216 + serialize(value) { 217 + return value; 218 + }, 219 + }); 220 + 221 + export const asHandle = createParser({ 222 + parse(value) { 223 + if (typeof value === 'string' && HANDLE_RE.test(value)) { 224 + return value; 225 + } 226 + 227 + return null; 228 + }, 229 + serialize(value) { 230 + return value; 231 + }, 232 + }); 233 + 234 + export const asIdentifier = createParser({ 235 + parse(value) { 236 + if (typeof value === 'string' && DID_OR_HANDLE_RE.test(value)) { 237 + return value; 238 + } 239 + 240 + return null; 241 + }, 242 + serialize(value) { 243 + return value; 244 + }, 245 + });
+22
src/main.tsx
··· 1 + /* @refresh reload */ 2 + import { render } from 'solid-js/web'; 3 + 4 + import * as navigation from '~/globals/navigation'; 5 + import { configureRouter } from '~/lib/navigation/router'; 6 + 7 + import './styles/app.css'; 8 + 9 + import routes from './routes'; 10 + import Shell from './shell'; 11 + 12 + configureRouter({ 13 + history: navigation.history, 14 + logger: navigation.logger, 15 + routes: routes, 16 + }); 17 + 18 + const App = () => { 19 + return <Shell />; 20 + }; 21 + 22 + render(App, document.body);
+35
src/routes.ts
··· 1 + import { lazy } from 'solid-js'; 2 + 3 + import type { RouteDefinition } from '~/lib/navigation/router'; 4 + 5 + const routes: RouteDefinition[] = [ 6 + { 7 + path: '/', 8 + component: lazy(() => import('./views/frontpage')), 9 + }, 10 + 11 + { 12 + path: '/did-lookup', 13 + component: lazy(() => import('./views/identity/did-lookup')), 14 + }, 15 + { 16 + path: '/plc-oplogs', 17 + component: lazy(() => import('./views/identity/plc-oplogs')), 18 + }, 19 + 20 + { 21 + path: '/repo-export', 22 + component: lazy(() => import('./views/repository/repo-export')), 23 + }, 24 + { 25 + path: '/car-unpack', 26 + component: lazy(() => import('./views/repository/car-unpack')), 27 + }, 28 + 29 + { 30 + path: '*', 31 + component: lazy(() => import('./views/_404')), 32 + }, 33 + ]; 34 + 35 + export default routes;
+27
src/shell.tsx
··· 1 + import { ErrorBoundary, Suspense } from 'solid-js'; 2 + 3 + import { RouterView } from '~/lib/navigation/router'; 4 + 5 + import ErrorPage from './views/_error'; 6 + 7 + const Shell = () => { 8 + return ( 9 + <div class="relative z-10 mx-auto flex min-h-dvh max-w-xl flex-col-reverse"> 10 + <div class="z-0 box-content flex min-h-0 grow flex-col overflow-clip bg-white shadow"> 11 + <RouterView 12 + render={({ def }) => { 13 + return ( 14 + <ErrorBoundary fallback={(error, reset) => <ErrorPage error={error} reset={reset} />}> 15 + <Suspense> 16 + <def.component /> 17 + </Suspense> 18 + </ErrorBoundary> 19 + ); 20 + }} 21 + /> 22 + </div> 23 + </div> 24 + ); 25 + }; 26 + 27 + export default Shell;
+17
src/styles/app.css
··· 1 + @tailwind base; 2 + @tailwind components; 3 + @tailwind utilities; 4 + 5 + @layer base { 6 + body { 7 + @apply z-0 min-h-dvh overflow-y-scroll bg-gray-100 text-sm; 8 + background-image: url(../assets/dotted-background.svg); 9 + background-size: 220px 220px; 10 + background-position: center; 11 + background-attachment: fixed; 12 + } 13 + 14 + [hidden] { 15 + display: none !important; 16 + } 17 + }
+5
src/views/_404.tsx
··· 1 + const NotFoundPage = () => { 2 + return <div>uh oh</div>; 3 + }; 4 + 5 + export default NotFoundPage;
+11
src/views/_error.tsx
··· 1 + export interface ErrorPageProps { 2 + error: unknown; 3 + reset: () => void; 4 + } 5 + 6 + const ErrorPage = ({ error, reset: _retry }: ErrorPageProps) => { 7 + console.error(error); 8 + return <div>something went wrong</div>; 9 + }; 10 + 11 + export default ErrorPage;
+149
src/views/frontpage.tsx
··· 1 + import { Component, ComponentProps } from 'solid-js'; 2 + 3 + import HistoryIcon from '~/components/ic-icons/baseline-history'; 4 + import ArchiveOutlinedIcon from '~/components/ic-icons/outline-archive'; 5 + import BookmarksOutlinedIcon from '~/components/ic-icons/outline-bookmarks'; 6 + import DirectionsCarOutlinedIcon from '~/components/ic-icons/outline-directions-car'; 7 + import ExploreOutlinedIcon from '~/components/ic-icons/outline-explore'; 8 + import MoveUpOutlinedIcon from '~/components/ic-icons/outline-move-up'; 9 + import VisibilityOutlinedIcon from '~/components/ic-icons/outline-visibility'; 10 + 11 + interface Group { 12 + name: string; 13 + items: Item[]; 14 + } 15 + 16 + interface Item { 17 + name: string; 18 + description: string; 19 + href: string | null; 20 + icon?: Component<ComponentProps<'svg'>>; 21 + } 22 + 23 + const Frontpage = () => { 24 + const catalogue: Group[] = [ 25 + { 26 + name: `Identity`, 27 + items: [ 28 + { 29 + name: `View identity info`, 30 + description: `Look up an account's DID document`, 31 + href: '/did-lookup', 32 + icon: VisibilityOutlinedIcon, 33 + }, 34 + { 35 + name: `View PLC operation logs`, 36 + description: `Show history of a did:plc identity`, 37 + href: '/plc-oplogs', 38 + icon: HistoryIcon, 39 + }, 40 + ], 41 + }, 42 + { 43 + name: `Repository`, 44 + items: [ 45 + { 46 + name: `Export repository`, 47 + description: `Download an archive of an account's repository`, 48 + href: '/repo-export', 49 + icon: ArchiveOutlinedIcon, 50 + }, 51 + { 52 + name: `Unpack CAR file`, 53 + description: `Extract a repository archive into a folder`, 54 + href: '/car-unpack', 55 + icon: DirectionsCarOutlinedIcon, 56 + }, 57 + { 58 + name: `Repository explorer`, 59 + description: `Explore an account's public records`, 60 + href: null, 61 + icon: ExploreOutlinedIcon, 62 + }, 63 + ], 64 + }, 65 + { 66 + name: `Blobs`, 67 + items: [ 68 + { 69 + name: `Export blobs`, 70 + description: `Download all blobs from an account`, 71 + href: null, 72 + icon: ArchiveOutlinedIcon, 73 + }, 74 + ], 75 + }, 76 + { 77 + name: `Labeling`, 78 + items: [ 79 + { 80 + name: `View emitted labels`, 81 + description: `Show moderation actions taken by a labeler`, 82 + href: null, 83 + icon: BookmarksOutlinedIcon, 84 + }, 85 + ], 86 + }, 87 + { 88 + name: `Account`, 89 + items: [ 90 + { 91 + name: `Migrate account`, 92 + description: `Move your account data to another server`, 93 + href: null, 94 + icon: MoveUpOutlinedIcon, 95 + }, 96 + ], 97 + }, 98 + ]; 99 + 100 + const nodes = catalogue.map(({ name: groupName, items }) => { 101 + const childNodes = items.map(({ name: itemName, description, href, icon: Icon }) => { 102 + return ( 103 + <a 104 + href={href || undefined} 105 + class={ 106 + `flex select-none gap-4 px-4 py-3` + 107 + (href ? ` hover:bg-gray-50 active:bg-gray-100` : ` opacity-40`) 108 + } 109 + > 110 + <div class="grid h-10 w-10 shrink-0 place-items-center rounded bg-purple-100 text-purple-600"> 111 + {Icon && <Icon class="h-5 w-5" />} 112 + </div> 113 + <div class="grow"> 114 + <p class="font-semibold">{itemName + (!href ? ` (wip)` : ``)}</p> 115 + <p class="text-pretty text-[13px] leading-5 text-gray-600">{description}</p> 116 + </div> 117 + </a> 118 + ); 119 + }); 120 + 121 + return ( 122 + <> 123 + <p class="px-4 pb-1 pt-4 text-[13px] font-semibold leading-5 text-purple-800">{groupName}</p> 124 + {childNodes} 125 + </> 126 + ); 127 + }); 128 + 129 + return ( 130 + <> 131 + <div class="p-4"> 132 + <h1 class="text-lg font-bold text-purple-800">boat</h1> 133 + <p class="text-gray-600">handy online tools for AT Protocol</p> 134 + </div> 135 + <hr class="mx-4 border-gray-200" /> 136 + 137 + <div class="flex grow flex-col pb-2">{nodes}</div> 138 + 139 + <hr class="mx-4 border-gray-200" /> 140 + <div class="p-4 pb-8"> 141 + <a href="https://github.com/atcute-js/boat" class="font-medium text-purple-800 underline"> 142 + source code 143 + </a> 144 + </div> 145 + </> 146 + ); 147 + }; 148 + 149 + export default Frontpage;
+216
src/views/identity/did-lookup.tsx
··· 1 + import { Match, Switch } from 'solid-js'; 2 + import * as v from 'valibot'; 3 + 4 + import { At } from '@atcute/client/lexicons'; 5 + 6 + import { getDidDocument } from '~/api/queries/did-doc'; 7 + import { resolveHandleViaAppView } from '~/api/queries/handle'; 8 + import { DID_OR_HANDLE_RE, isDid } from '~/api/utils/strings'; 9 + 10 + import { createQuery } from '~/lib/utils/query'; 11 + import { asIdentifier, useSearchParams } from '~/lib/utils/search-params'; 12 + 13 + import CircularProgressView from '~/components/circular-progress-view'; 14 + import ErrorView from '~/components/error-view'; 15 + import { serviceUrlString } from '~/api/types/strings'; 16 + 17 + const DidLookupPage = () => { 18 + const [params, setParams] = useSearchParams({ 19 + q: asIdentifier, 20 + }); 21 + 22 + const query = createQuery( 23 + () => params.q, 24 + async (identifier, signal) => { 25 + let did: At.DID; 26 + if (isDid(identifier)) { 27 + did = identifier; 28 + } else { 29 + did = await resolveHandleViaAppView({ handle: identifier, signal }); 30 + } 31 + 32 + const doc = await getDidDocument({ did, signal }); 33 + 34 + return doc; 35 + }, 36 + ); 37 + 38 + return ( 39 + <> 40 + <div class="p-4"> 41 + <h1 class="text-lg font-bold text-purple-800">View identity info</h1> 42 + <p class="text-gray-600">Look up an account's DID document</p> 43 + </div> 44 + <hr class="mx-4 border-gray-200" /> 45 + 46 + <form 47 + onSubmit={(ev) => { 48 + const formData = new FormData(ev.currentTarget); 49 + ev.preventDefault(); 50 + 51 + const ident = formData.get('ident') as string; 52 + setParams({ q: ident }); 53 + }} 54 + class="m-4 flex flex-col gap-4" 55 + > 56 + <label class="flex flex-col gap-2"> 57 + <span class="font-semibold text-gray-600">Handle or DID identifier*</span> 58 + <input 59 + type="text" 60 + name="ident" 61 + required 62 + pattern={DID_OR_HANDLE_RE.source} 63 + placeholder="paul.bsky.social" 64 + value={params.q ?? ''} 65 + class="rounded border border-gray-300 px-3 py-2 text-sm outline-2 -outline-offset-1 outline-purple-600 placeholder:text-gray-400 focus:outline" 66 + /> 67 + </label> 68 + 69 + <div> 70 + <button 71 + type="submit" 72 + class="flex h-9 select-none items-center rounded bg-purple-800 px-4 text-sm font-semibold text-white hover:bg-purple-700 active:bg-purple-700" 73 + > 74 + Look up! 75 + </button> 76 + </div> 77 + </form> 78 + 79 + <hr class="mx-4 border-gray-200" /> 80 + 81 + <Switch> 82 + <Match when={query.isPending}> 83 + <CircularProgressView /> 84 + </Match> 85 + 86 + <Match when={query.isError}> 87 + <ErrorView error={query.error} onRetry={query.refetch} /> 88 + </Match> 89 + 90 + <Match when={query.data} keyed> 91 + {(doc) => { 92 + const isDidPlc = doc.id.startsWith('did:plc:'); 93 + 94 + return ( 95 + <> 96 + <div class="flex flex-col gap-6 break-words p-4 text-gray-900"> 97 + <div> 98 + <p class="font-semibold text-gray-600">DID identifier</p> 99 + <span>{doc.id}</span> 100 + </div> 101 + 102 + <div> 103 + <p class="font-semibold text-gray-600">Identifies as</p> 104 + <ol class="list-disc pl-4"> 105 + {doc.alsoKnownAs.map((ident) => ( 106 + <li>{ident}</li> 107 + ))} 108 + </ol> 109 + </div> 110 + 111 + <div> 112 + <p class="font-semibold text-gray-600">Services</p> 113 + <ol class="list-disc pl-4"> 114 + {doc.service.map(({ id, type, serviceEndpoint }, idx) => { 115 + const isString = typeof serviceEndpoint === 'string'; 116 + const isURL = isString && URL.canParse('' + serviceEndpoint); 117 + const isServiceUrl = isString && v.is(serviceUrlString, serviceEndpoint); 118 + 119 + const isPDS = type === 'AtprotoPersonalDataServer'; 120 + const isLabeler = type === 'AtprotoLabeler'; 121 + 122 + return ( 123 + <li class={idx !== 0 ? `mt-3` : ``}> 124 + <p class="font-medium">{id}</p> 125 + <p class="text-gray-600">{type}</p> 126 + 127 + {isURL ? ( 128 + <a target="_blank" href={serviceEndpoint} class="text-purple-600 underline"> 129 + {serviceEndpoint} 130 + </a> 131 + ) : isString ? ( 132 + <p class="text-gray-600">{serviceEndpoint}</p> 133 + ) : null} 134 + 135 + <div class="mt-2 flex flex-wrap gap-2 empty:hidden"> 136 + {isPDS && isServiceUrl && ( 137 + <button 138 + disabled 139 + class="flex h-9 select-none items-center rounded border border-gray-300 px-4 text-sm font-semibold text-gray-800 hover:bg-gray-100 active:bg-gray-100 disabled:pointer-events-none disabled:opacity-50" 140 + > 141 + View PDS info 142 + </button> 143 + )} 144 + 145 + {isPDS && isServiceUrl && ( 146 + <button 147 + disabled 148 + class="flex h-9 select-none items-center rounded border border-gray-300 px-4 text-sm font-semibold text-gray-800 hover:bg-gray-100 active:bg-gray-100 disabled:pointer-events-none disabled:opacity-50" 149 + > 150 + Explore account repository 151 + </button> 152 + )} 153 + 154 + {isLabeler && isServiceUrl && ( 155 + <button 156 + disabled 157 + class="flex h-9 select-none items-center rounded border border-gray-300 px-4 text-sm font-semibold text-gray-800 hover:bg-gray-100 active:bg-gray-100 disabled:pointer-events-none disabled:opacity-50" 158 + > 159 + View emitted labels 160 + </button> 161 + )} 162 + </div> 163 + </li> 164 + ); 165 + })} 166 + </ol> 167 + </div> 168 + 169 + <div> 170 + <p class="font-semibold text-gray-600">Verification methods</p> 171 + <ol class="list-disc pl-4"> 172 + {doc.verificationMethod.map(({ id, type, publicKeyMultibase }, idx) => { 173 + return ( 174 + <li class={idx !== 0 ? `mt-3` : ``}> 175 + <p class="font-medium">{id.replace(doc.id, '')}</p> 176 + <p class="text-gray-600">{type}</p> 177 + 178 + {publicKeyMultibase && ( 179 + <p class="font-mono text-gray-600">{publicKeyMultibase}</p> 180 + )} 181 + </li> 182 + ); 183 + })} 184 + </ol> 185 + </div> 186 + </div> 187 + 188 + <div class="flex flex-wrap gap-4 p-4 pt-2"> 189 + <button 190 + onClick={() => { 191 + navigator.clipboard.writeText(JSON.stringify(doc, null, 2)); 192 + }} 193 + class="flex h-9 select-none items-center rounded border border-gray-300 px-4 text-sm font-semibold text-gray-800 hover:bg-gray-100 active:bg-gray-100" 194 + > 195 + Copy DID document 196 + </button> 197 + 198 + {isDidPlc && ( 199 + <a 200 + href={`/plc-oplogs?q=${params.q!}`} 201 + class="flex h-9 select-none items-center rounded border border-gray-300 px-4 text-sm font-semibold text-gray-800 hover:bg-gray-100 active:bg-gray-100" 202 + > 203 + View PLC operation logs 204 + </a> 205 + )} 206 + </div> 207 + </> 208 + ); 209 + }} 210 + </Match> 211 + </Switch> 212 + </> 213 + ); 214 + }; 215 + 216 + export default DidLookupPage;
+729
src/views/identity/plc-oplogs.tsx
··· 1 + import { JSX, Match, Switch } from 'solid-js'; 2 + import * as v from 'valibot'; 3 + 4 + import { At } from '@atcute/client/lexicons'; 5 + 6 + import { resolveHandleViaAppView } from '~/api/queries/handle'; 7 + import { didString, handleString, serviceUrlString } from '~/api/types/strings'; 8 + import { DID_OR_HANDLE_RE, isDid } from '~/api/utils/strings'; 9 + 10 + import { createQuery } from '~/lib/utils/query'; 11 + import { asIdentifier, useSearchParams } from '~/lib/utils/search-params'; 12 + 13 + import CircularProgressView from '~/components/circular-progress-view'; 14 + import DiffTable from '~/components/diff-table'; 15 + import ErrorView from '~/components/error-view'; 16 + import { dequal } from '~/lib/utils/dequal'; 17 + 18 + const PlcOperationLogPage = () => { 19 + const [params, setParams] = useSearchParams({ 20 + q: asIdentifier, 21 + }); 22 + 23 + const query = createQuery( 24 + () => params.q, 25 + async (identifier, signal) => { 26 + let did: At.DID; 27 + if (isDid(identifier)) { 28 + did = identifier; 29 + } else { 30 + did = await resolveHandleViaAppView({ handle: identifier, signal }); 31 + } 32 + 33 + if (!did.startsWith('did:plc:')) { 34 + throw new Error(`${did} is not plc`); 35 + } 36 + 37 + const response = await fetch(`https://plc.directory/${did}/log/audit`); 38 + if (!response.ok) { 39 + throw new Error(`got resposne ${response.status}`); 40 + } 41 + 42 + const json = await response.json(); 43 + return v.parse(plcLogEntries, json); 44 + }, 45 + ); 46 + 47 + return ( 48 + <> 49 + <div class="p-4"> 50 + <h1 class="text-lg font-bold text-purple-800">View PLC operation logs</h1> 51 + <p class="text-gray-600">Show history of a did:plc identity</p> 52 + </div> 53 + <hr class="mx-4 border-gray-200" /> 54 + 55 + <form 56 + onSubmit={(ev) => { 57 + const formData = new FormData(ev.currentTarget); 58 + ev.preventDefault(); 59 + 60 + const ident = formData.get('ident') as string; 61 + setParams({ q: ident }); 62 + }} 63 + class="m-4 flex flex-col gap-4" 64 + > 65 + <label class="flex flex-col gap-2"> 66 + <span class="font-semibold text-gray-600">Handle or DID identifier*</span> 67 + <input 68 + type="text" 69 + name="ident" 70 + required 71 + pattern={DID_OR_HANDLE_RE.source} 72 + placeholder="paul.bsky.social" 73 + value={params.q ?? ''} 74 + class="rounded border border-gray-300 px-3 py-2 text-sm outline-2 -outline-offset-1 outline-purple-600 placeholder:text-gray-400 focus:outline" 75 + /> 76 + </label> 77 + 78 + <div> 79 + <button 80 + type="submit" 81 + class="flex h-9 select-none items-center rounded bg-purple-800 px-4 text-sm font-semibold text-white hover:bg-purple-700 active:bg-purple-700" 82 + > 83 + Look up! 84 + </button> 85 + </div> 86 + </form> 87 + 88 + <hr class="mx-4 border-gray-200" /> 89 + 90 + <Switch> 91 + <Match when={query.isPending}> 92 + <CircularProgressView /> 93 + </Match> 94 + 95 + <Match when={query.isError}> 96 + <ErrorView error={query.error} onRetry={query.refetch} /> 97 + </Match> 98 + 99 + <Match when={query.data} keyed> 100 + {(plcLogs) => { 101 + const lastActiveEntry = plcLogs.findLast((entry) => !entry.nullified); 102 + 103 + const grouped = Map.groupBy(createOperationHistory(plcLogs).reverse(), (item) => item.orig) 104 + .entries() 105 + .toArray(); 106 + 107 + const renderDiffItem = (diff: DiffEntry) => { 108 + const nullified = diff.orig.nullified; 109 + 110 + let title = 'Unknown log entry'; 111 + let node: JSX.Element; 112 + 113 + if (diff.type === 'account_created') { 114 + title = `Account created`; 115 + } else if (diff.type === 'account_tombstoned') { 116 + title = `Account tombstoned`; 117 + } else if (diff.type === 'handle_added') { 118 + const handle = diff.handle; 119 + 120 + title = `Alias added`; 121 + node = <DiffTable fields={[{ title: `URI`, prev: null, next: handle }]} />; 122 + } else if (diff.type === 'handle_changed') { 123 + const prevHandle = diff.prev_handle; 124 + const nextHandle = diff.next_handle; 125 + 126 + title = `Alias updated`; 127 + node = <DiffTable fields={[{ title: `URI`, prev: prevHandle, next: nextHandle }]} />; 128 + } else if (diff.type === 'handle_removed') { 129 + const handle = diff.handle; 130 + 131 + title = `Alias removed`; 132 + node = <DiffTable fields={[{ title: `URI`, prev: handle, next: null }]} />; 133 + } else if (diff.type === 'rotation_key_added') { 134 + const key = diff.rotation_key; 135 + 136 + title = `Rotation key added`; 137 + node = <DiffTable fields={[{ title: `Key`, prev: null, next: key }]} />; 138 + } else if (diff.type === 'rotation_key_removed') { 139 + const key = diff.rotation_key; 140 + 141 + title = `Rotation key removed`; 142 + node = <DiffTable fields={[{ title: `Key`, prev: key, next: null }]} />; 143 + } else if (diff.type === 'service_added') { 144 + const id = diff.service_id; 145 + const type = diff.service_type; 146 + const endpoint = diff.service_endpoint; 147 + 148 + title = `Service added`; 149 + node = ( 150 + <DiffTable 151 + fields={[ 152 + { title: `ID`, prev: null, next: id }, 153 + { title: `Type`, prev: null, next: type }, 154 + { title: `Endpoint`, prev: null, next: endpoint }, 155 + ]} 156 + /> 157 + ); 158 + } else if (diff.type === 'service_changed') { 159 + const id = diff.service_id; 160 + 161 + const prevType = diff.prev_service_type; 162 + const prevEndpoint = diff.prev_service_endpoint; 163 + 164 + const nextType = diff.next_service_type; 165 + const nextEndpoint = diff.next_service_endpoint; 166 + 167 + title = `Service updated`; 168 + node = ( 169 + <DiffTable 170 + fields={[ 171 + { title: `ID`, next: id }, 172 + { title: `Type`, prev: prevType, next: nextType }, 173 + { title: `Endpoint`, prev: prevEndpoint, next: nextEndpoint }, 174 + ]} 175 + /> 176 + ); 177 + } else if (diff.type === 'service_removed') { 178 + const id = diff.service_id; 179 + const type = diff.service_type; 180 + const endpoint = diff.service_endpoint; 181 + 182 + title = `Service removed`; 183 + node = ( 184 + <DiffTable 185 + fields={[ 186 + { title: `ID`, prev: id, next: null }, 187 + { title: `Type`, prev: type, next: null }, 188 + { title: `Endpoint`, prev: endpoint, next: null }, 189 + ]} 190 + /> 191 + ); 192 + } else if (diff.type === 'verification_method_added') { 193 + const id = diff.method_id; 194 + const key = diff.method_key; 195 + 196 + title = `Verification method added`; 197 + node = ( 198 + <DiffTable 199 + fields={[ 200 + { title: `ID`, prev: null, next: id }, 201 + { title: `Key`, prev: null, next: key }, 202 + ]} 203 + /> 204 + ); 205 + } else if (diff.type === 'verification_method_changed') { 206 + const id = diff.method_id; 207 + 208 + const prevKey = diff.prev_method_key; 209 + const nextKey = diff.next_method_key; 210 + 211 + title = `Verification method updated`; 212 + 213 + node = ( 214 + <DiffTable 215 + fields={[ 216 + { title: `ID`, next: id }, 217 + { title: `Key`, prev: prevKey, next: nextKey }, 218 + ]} 219 + /> 220 + ); 221 + } else if (diff.type === 'verification_method_removed') { 222 + const id = diff.method_id; 223 + const key = diff.method_key; 224 + 225 + title = `Verification method removed`; 226 + node = ( 227 + <DiffTable 228 + fields={[ 229 + { title: `ID`, prev: id, next: null }, 230 + { title: `Key`, prev: key, next: null }, 231 + ]} 232 + /> 233 + ); 234 + } 235 + 236 + return ( 237 + <div class="flex min-w-0 grow flex-col gap-1 py-2"> 238 + <p class={`font-bold` + (!nullified ? ` ` : ` text-gray-600 line-through`)}>{title}</p> 239 + {node} 240 + </div> 241 + ); 242 + }; 243 + 244 + return ( 245 + <ol class="break-words px-4"> 246 + {grouped.map(([entry, diffs], idx) => { 247 + const last = idx === grouped.length - 1; 248 + const lastActive = entry === lastActiveEntry; 249 + 250 + const nullified = entry.nullified; 251 + const multiple = diffs.length > 1; 252 + 253 + const node = multiple ? ( 254 + <ol> 255 + {diffs.map((diff, idx) => { 256 + const last = idx === diffs.length - 1; 257 + 258 + return ( 259 + <li class="flex gap-4"> 260 + <div class="relative flex flex-col items-center"> 261 + <div class="mt-3.5 h-2 w-2 rounded-full bg-gray-600" /> 262 + 263 + {!last && ( 264 + <div class="absolute bottom-[-0.875rem] top-[1.375rem] border-l border-gray-300" /> 265 + )} 266 + </div> 267 + {/* @once */ renderDiffItem(diff)} 268 + </li> 269 + ); 270 + })} 271 + </ol> 272 + ) : diffs.length === 1 ? ( 273 + renderDiffItem(diffs[0]) 274 + ) : null; 275 + 276 + return ( 277 + <li class="flex gap-4"> 278 + <div class="relative flex flex-col items-center"> 279 + <div 280 + class={ 281 + `mt-[1.375rem] h-2 w-2 rounded-full` + 282 + (lastActive ? ` bg-purple-600` : ` bg-gray-600`) 283 + } 284 + /> 285 + 286 + {!last && ( 287 + <div class="absolute bottom-[-1.875rem] top-[1.875rem] border-l border-gray-300" /> 288 + )} 289 + {multiple && ( 290 + <div class="absolute left-[0.21875rem] top-[1.875rem] h-[1.5rem] w-[1.375rem] rounded-bl-2xl border-b border-l border-gray-300" /> 291 + )} 292 + </div> 293 + 294 + <div class="flex min-w-0 grow flex-col py-4"> 295 + <p class="font-mono text-[0.8125rem] leading-5 text-gray-600"> 296 + <span class={!nullified ? `` : `line-through`}>{/* @once */ entry.createdAt}</span> 297 + {nullified && <span> (nullified)</span>} 298 + </p> 299 + 300 + {node} 301 + </div> 302 + </li> 303 + ); 304 + })} 305 + </ol> 306 + ); 307 + }} 308 + </Match> 309 + </Switch> 310 + </> 311 + ); 312 + }; 313 + 314 + export default PlcOperationLogPage; 315 + 316 + type DiffEntry = 317 + | { 318 + type: 'account_created'; 319 + orig: PlcLogEntry; 320 + nullified: boolean; 321 + at: string; 322 + rotationKeys: string[]; 323 + verificationMethods: Record<string, string>; 324 + alsoKnownAs: string[]; 325 + services: Record<string, { type: string; endpoint: string }>; 326 + } 327 + | { 328 + type: 'account_tombstoned'; 329 + orig: PlcLogEntry; 330 + nullified: boolean; 331 + at: string; 332 + } 333 + | { 334 + type: 'rotation_key_added'; 335 + orig: PlcLogEntry; 336 + nullified: boolean; 337 + at: string; 338 + rotation_key: string; 339 + } 340 + | { 341 + type: 'rotation_key_removed'; 342 + orig: PlcLogEntry; 343 + nullified: boolean; 344 + at: string; 345 + rotation_key: string; 346 + } 347 + | { 348 + type: 'verification_method_added'; 349 + orig: PlcLogEntry; 350 + nullified: boolean; 351 + at: string; 352 + method_id: string; 353 + method_key: string; 354 + } 355 + | { 356 + type: 'verification_method_removed'; 357 + orig: PlcLogEntry; 358 + nullified: boolean; 359 + at: string; 360 + method_id: string; 361 + method_key: string; 362 + } 363 + | { 364 + type: 'verification_method_changed'; 365 + orig: PlcLogEntry; 366 + nullified: boolean; 367 + at: string; 368 + method_id: string; 369 + prev_method_key: string; 370 + next_method_key: string; 371 + } 372 + | { 373 + type: 'handle_added'; 374 + orig: PlcLogEntry; 375 + nullified: boolean; 376 + at: string; 377 + handle: string; 378 + } 379 + | { 380 + type: 'handle_removed'; 381 + orig: PlcLogEntry; 382 + nullified: boolean; 383 + at: string; 384 + handle: string; 385 + } 386 + | { 387 + type: 'handle_changed'; 388 + orig: PlcLogEntry; 389 + nullified: boolean; 390 + at: string; 391 + prev_handle: string; 392 + next_handle: string; 393 + } 394 + | { 395 + type: 'service_added'; 396 + orig: PlcLogEntry; 397 + nullified: boolean; 398 + at: string; 399 + service_id: string; 400 + service_type: string; 401 + service_endpoint: string; 402 + } 403 + | { 404 + type: 'service_removed'; 405 + orig: PlcLogEntry; 406 + nullified: boolean; 407 + at: string; 408 + service_id: string; 409 + service_type: string; 410 + service_endpoint: string; 411 + } 412 + | { 413 + type: 'service_changed'; 414 + orig: PlcLogEntry; 415 + nullified: boolean; 416 + at: string; 417 + service_id: string; 418 + prev_service_type: string; 419 + next_service_type: string; 420 + prev_service_endpoint: string; 421 + next_service_endpoint: string; 422 + }; 423 + 424 + const createOperationHistory = (entries: PlcLogEntry[]): DiffEntry[] => { 425 + const history: DiffEntry[] = []; 426 + 427 + for (let idx = 0, len = entries.length; idx < len; idx++) { 428 + const entry = entries[idx]; 429 + const op = entry.operation; 430 + 431 + if (op.type === 'create') { 432 + history.push({ 433 + type: 'account_created', 434 + orig: entry, 435 + nullified: entry.nullified, 436 + at: entry.createdAt, 437 + rotationKeys: [op.recoveryKey, op.signingKey], 438 + verificationMethods: { atproto: op.signingKey }, 439 + alsoKnownAs: [`at://${op.handle}`], 440 + services: { 441 + atproto_pds: { 442 + type: 'AtprotoPersonalDataServer', 443 + endpoint: op.service, 444 + }, 445 + }, 446 + }); 447 + } else if (op.type === 'plc_operation') { 448 + const prevOp = findLastMatching(entries, (entry) => !entry.nullified, idx - 1)?.operation; 449 + 450 + let oldRotationKeys: string[]; 451 + let oldVerificationMethods: Record<string, string>; 452 + let oldAlsoKnownAs: string[]; 453 + let oldServices: Record<string, Service>; 454 + 455 + if (!prevOp) { 456 + history.push({ 457 + type: 'account_created', 458 + orig: entry, 459 + nullified: entry.nullified, 460 + at: entry.createdAt, 461 + rotationKeys: op.rotationKeys, 462 + verificationMethods: op.verificationMethods, 463 + alsoKnownAs: op.alsoKnownAs, 464 + services: op.services, 465 + }); 466 + 467 + continue; 468 + } else if (prevOp.type === 'create') { 469 + oldRotationKeys = [prevOp.recoveryKey, prevOp.signingKey]; 470 + oldVerificationMethods = { atproto: prevOp.signingKey }; 471 + oldAlsoKnownAs = [`at://${prevOp.handle}`]; 472 + oldServices = { 473 + atproto_pds: { 474 + type: 'AtprotoPersonalDataServer', 475 + endpoint: prevOp.service, 476 + }, 477 + }; 478 + } else if (prevOp.type === 'plc_operation') { 479 + oldRotationKeys = prevOp.rotationKeys; 480 + oldVerificationMethods = prevOp.verificationMethods; 481 + oldAlsoKnownAs = prevOp.alsoKnownAs; 482 + oldServices = prevOp.services; 483 + } else { 484 + continue; 485 + } 486 + 487 + // Check for rotation key changes 488 + { 489 + const additions = difference(op.rotationKeys, oldRotationKeys); 490 + const removals = difference(oldRotationKeys, op.rotationKeys); 491 + 492 + for (const key of additions) { 493 + history.push({ 494 + type: 'rotation_key_added', 495 + orig: entry, 496 + nullified: entry.nullified, 497 + at: entry.createdAt, 498 + rotation_key: key, 499 + }); 500 + } 501 + 502 + for (const key of removals) { 503 + history.push({ 504 + type: 'rotation_key_removed', 505 + orig: entry, 506 + nullified: entry.nullified, 507 + at: entry.createdAt, 508 + rotation_key: key, 509 + }); 510 + } 511 + } 512 + 513 + // Check for verification method changes 514 + { 515 + for (const id in op.verificationMethods) { 516 + if (!(id in oldVerificationMethods)) { 517 + history.push({ 518 + type: 'verification_method_added', 519 + orig: entry, 520 + nullified: entry.nullified, 521 + at: entry.createdAt, 522 + method_id: id, 523 + method_key: op.verificationMethods[id], 524 + }); 525 + } else if (op.verificationMethods[id] !== oldVerificationMethods[id]) { 526 + history.push({ 527 + type: 'verification_method_changed', 528 + orig: entry, 529 + nullified: entry.nullified, 530 + at: entry.createdAt, 531 + method_id: id, 532 + prev_method_key: oldVerificationMethods[id], 533 + next_method_key: op.verificationMethods[id], 534 + }); 535 + } 536 + } 537 + 538 + for (const id in oldVerificationMethods) { 539 + if (!(id in op.verificationMethods)) { 540 + history.push({ 541 + type: 'verification_method_removed', 542 + orig: entry, 543 + nullified: entry.nullified, 544 + at: entry.createdAt, 545 + method_id: id, 546 + method_key: oldVerificationMethods[id], 547 + }); 548 + } 549 + } 550 + } 551 + 552 + // Check for handle changes 553 + if (op.alsoKnownAs.length === 1 && oldAlsoKnownAs.length === 1) { 554 + if (op.alsoKnownAs[0] !== oldAlsoKnownAs[0]) { 555 + history.push({ 556 + type: 'handle_changed', 557 + orig: entry, 558 + nullified: entry.nullified, 559 + at: entry.createdAt, 560 + prev_handle: oldAlsoKnownAs[0], 561 + next_handle: op.alsoKnownAs[0], 562 + }); 563 + } 564 + } else { 565 + const additions = difference(op.alsoKnownAs, oldAlsoKnownAs); 566 + const removals = difference(oldAlsoKnownAs, op.alsoKnownAs); 567 + 568 + for (const handle of additions) { 569 + history.push({ 570 + type: 'handle_added', 571 + orig: entry, 572 + nullified: entry.nullified, 573 + at: entry.createdAt, 574 + handle: handle, 575 + }); 576 + } 577 + 578 + for (const handle of removals) { 579 + history.push({ 580 + type: 'handle_removed', 581 + orig: entry, 582 + nullified: entry.nullified, 583 + at: entry.createdAt, 584 + handle: handle, 585 + }); 586 + } 587 + } 588 + 589 + // Check for service changes 590 + { 591 + for (const id in op.services) { 592 + if (!(id in oldServices)) { 593 + history.push({ 594 + type: 'service_added', 595 + orig: entry, 596 + nullified: entry.nullified, 597 + at: entry.createdAt, 598 + service_id: id, 599 + service_type: op.services[id].type, 600 + service_endpoint: op.services[id].endpoint, 601 + }); 602 + } else if (!dequal(op.services[id], oldServices[id])) { 603 + history.push({ 604 + type: 'service_changed', 605 + orig: entry, 606 + nullified: entry.nullified, 607 + at: entry.createdAt, 608 + service_id: id, 609 + prev_service_type: oldServices[id].type, 610 + next_service_type: op.services[id].type, 611 + prev_service_endpoint: oldServices[id].endpoint, 612 + next_service_endpoint: op.services[id].endpoint, 613 + }); 614 + } 615 + } 616 + 617 + for (const id in oldServices) { 618 + if (!(id in op.services)) { 619 + history.push({ 620 + type: 'service_removed', 621 + orig: entry, 622 + nullified: entry.nullified, 623 + at: entry.createdAt, 624 + service_id: id, 625 + service_type: oldServices[id].type, 626 + service_endpoint: oldServices[id].endpoint, 627 + }); 628 + } 629 + } 630 + } 631 + } else if (op.type === 'plc_tombstone') { 632 + history.push({ 633 + type: 'account_tombstoned', 634 + orig: entry, 635 + nullified: entry.nullified, 636 + at: entry.createdAt, 637 + }); 638 + } 639 + } 640 + 641 + return history; 642 + }; 643 + 644 + const didKeyString = v.pipe( 645 + v.string(), 646 + v.check((str) => str.startsWith('did:key:')), 647 + ); 648 + 649 + const legacyGenesisOp = v.object({ 650 + type: v.literal('create'), 651 + signingKey: didKeyString, 652 + recoveryKey: didKeyString, 653 + handle: handleString, 654 + service: serviceUrlString, 655 + prev: v.null(), 656 + sig: v.string(), 657 + }); 658 + 659 + const tombstoneOp = v.object({ 660 + type: v.literal('plc_tombstone'), 661 + prev: v.string(), 662 + sig: v.string(), 663 + }); 664 + 665 + const service = v.object({ 666 + type: v.string(), 667 + endpoint: v.pipe(v.string(), v.url()), 668 + }); 669 + type Service = v.InferOutput<typeof service>; 670 + 671 + const plcOp = v.object({ 672 + type: v.literal('plc_operation'), 673 + rotationKeys: v.array(didKeyString), 674 + verificationMethods: v.record(v.string(), didKeyString), 675 + alsoKnownAs: v.array(v.pipe(v.string(), v.url())), 676 + services: v.record( 677 + v.string(), 678 + v.object({ 679 + type: v.string(), 680 + endpoint: v.pipe(v.string(), v.url()), 681 + }), 682 + ), 683 + prev: v.nullable(v.string()), 684 + sig: v.string(), 685 + }); 686 + 687 + const plcOperation = v.union([legacyGenesisOp, tombstoneOp, plcOp]); 688 + 689 + const plcLogEntry = v.object({ 690 + did: didString, 691 + cid: v.string(), 692 + operation: plcOperation, 693 + nullified: v.boolean(), 694 + createdAt: v.pipe( 695 + v.string(), 696 + v.check((dateString) => { 697 + const date = new Date(dateString); 698 + return !Number.isNaN(date.getTime()); 699 + }), 700 + ), 701 + }); 702 + type PlcLogEntry = v.InferOutput<typeof plcLogEntry>; 703 + 704 + const plcLogEntries = v.array(plcLogEntry); 705 + 706 + function findLastMatching<T, S extends T>( 707 + arr: T[], 708 + predicate: (item: T) => item is S, 709 + start?: number, 710 + ): S | undefined; 711 + function findLastMatching<T>(arr: T[], predicate: (item: T) => boolean, start?: number): T | undefined; 712 + function findLastMatching<T>( 713 + arr: T[], 714 + predicate: (item: T) => boolean, 715 + start: number = arr.length - 1, 716 + ): T | undefined { 717 + for (let i = start, v: any; i >= 0; i--) { 718 + if (predicate((v = arr[i]))) { 719 + return v; 720 + } 721 + } 722 + 723 + return undefined; 724 + } 725 + 726 + function difference<T>(a: readonly T[], b: readonly T[]): T[] { 727 + const set = new Set(b); 728 + return a.filter((value) => !set.has(value)); 729 + }
+202
src/views/repository/car-unpack.tsx
··· 1 + import { FileSystemWritableFileStream, showSaveFilePicker } from 'native-file-system-adapter'; 2 + import { createSignal } from 'solid-js'; 3 + 4 + import { iterateAtpRepo } from '@atcute/car'; 5 + import { writeTarEntry } from '@mary/tar'; 6 + 7 + import { createDropZone } from '~/lib/hooks/dropzone'; 8 + import { makeAbortable } from '~/lib/utils/abortable'; 9 + 10 + import Logger, { createLogger } from '~/components/logger'; 11 + 12 + // @ts-expect-error: new API 13 + const yieldToScheduler: () => Promise<void> = window?.scheduler?.yield 14 + ? // @ts-expect-error: whatever 15 + window.scheduler.yield.bind(window.scheduler) 16 + : undefined; 17 + 18 + const yieldToIdle = 19 + typeof requestIdleCallback === 'function' 20 + ? () => new Promise((resolve) => requestIdleCallback(resolve)) 21 + : () => new Promise((resolve) => setTimeout(resolve, 1)); 22 + 23 + const UnpackCarPage = () => { 24 + const logger = createLogger(); 25 + 26 + const [getSignal] = makeAbortable(); 27 + const [pending, setPending] = createSignal(false); 28 + 29 + const { ref: dropRef, isDropping } = createDropZone({ 30 + // Checked, the mime type for CAR files is blank. 31 + dataTypes: [''], 32 + multiple: false, 33 + onDrop(files) { 34 + if (files) { 35 + onFileDrop(files); 36 + } 37 + }, 38 + }); 39 + 40 + const mutate = async (file: File, signal: AbortSignal) => { 41 + logger.info(`Starting extraction for ${file.name}`); 42 + 43 + const buf = await file.arrayBuffer(); 44 + const ui8 = new Uint8Array(buf); 45 + 46 + let currentCollection: string | undefined; 47 + let count = 0; 48 + 49 + let writable: FileSystemWritableFileStream | undefined; 50 + 51 + for (const { collection, rkey, record } of iterateAtpRepo(ui8)) { 52 + if (writable === undefined) { 53 + const progress = logger.progress(`Waiting for the user`); 54 + 55 + try { 56 + const fd = await showSaveFilePicker({ 57 + suggestedName: `${file.name.replace(/\.car$/, '')}.tar`, 58 + 59 + // @ts-expect-error: ponyfill doesn't have the full typings 60 + id: 'car-unpack', 61 + startIn: 'downloads', 62 + types: [ 63 + { 64 + description: 'Tarball archive', 65 + accept: { 'application/tar': ['.tar'] }, 66 + }, 67 + ], 68 + }).catch((err) => { 69 + console.warn(err); 70 + 71 + if (err instanceof DOMException && err.name === 'AbortError') { 72 + logger.warn(`Opened the file picker, but it was aborted`); 73 + } else { 74 + logger.warn(`Something went wrong when opening the file picker`); 75 + } 76 + 77 + return undefined; 78 + }); 79 + 80 + writable = await fd?.createWritable(); 81 + } finally { 82 + progress.destroy(); 83 + } 84 + 85 + if (writable === undefined) { 86 + // We already handled the errors above 87 + return; 88 + } 89 + } 90 + 91 + signal.throwIfAborted(); 92 + 93 + if (currentCollection !== collection) { 94 + logger.log(`Current progress: ${collection}`); 95 + currentCollection = collection; 96 + 97 + if (yieldToScheduler === undefined) { 98 + await yieldToIdle(); 99 + } 100 + } 101 + 102 + const entry = writeTarEntry({ 103 + filename: `${collection}/${filenamify(rkey)}.json`, 104 + data: JSON.stringify(record, null, 2), 105 + }); 106 + 107 + writable.write(entry); 108 + count++; 109 + 110 + if (yieldToScheduler !== undefined) { 111 + await yieldToScheduler(); 112 + } 113 + } 114 + 115 + signal.throwIfAborted(); 116 + 117 + if (writable === undefined) { 118 + // If we got here it means the above loop never iterated 119 + logger.log(`CAR file has no records`); 120 + } else { 121 + logger.log(`${count} records extracted`); 122 + await writable.close(); 123 + 124 + logger.log(`Finished!`); 125 + } 126 + }; 127 + 128 + const onFileDrop = (files: File[]) => { 129 + if (pending() || files.length < 1) { 130 + return; 131 + } 132 + 133 + const signal = getSignal(); 134 + 135 + setPending(true); 136 + mutate(files[0], signal).then( 137 + () => { 138 + if (signal.aborted) { 139 + return; 140 + } 141 + 142 + setPending(false); 143 + }, 144 + (err) => { 145 + if (signal.aborted) { 146 + return; 147 + } 148 + 149 + setPending(false); 150 + logger.error(`Critical error: ${err}\nFile might be malformed, or might not be a CAR archive`); 151 + }, 152 + ); 153 + }; 154 + 155 + return ( 156 + <> 157 + <div class="p-4"> 158 + <h1 class="text-lg font-bold text-purple-800">Unpack CAR file</h1> 159 + <p class="text-gray-600">Extract a repository archive into a folder</p> 160 + </div> 161 + <hr class="mx-4 border-gray-200" /> 162 + 163 + <div class="p-4"> 164 + <fieldset 165 + ref={dropRef} 166 + disabled={pending()} 167 + class={ 168 + `grid place-items-center rounded border border-gray-300 px-6 py-12 disabled:opacity-50` + 169 + (pending() || !isDropping() ? ` bg-gray-100` : ` bg-green-100`) 170 + } 171 + > 172 + <div class="flex flex-col items-center gap-4"> 173 + <button 174 + onClick={() => { 175 + const input = document.createElement('input'); 176 + input.type = 'file'; 177 + input.accept = '.car,application/vnd.ipld.car'; 178 + input.oninput = () => onFileDrop(Array.from(input.files!)); 179 + 180 + input.click(); 181 + }} 182 + class="flex h-9 select-none items-center rounded border border-gray-400 px-4 text-sm font-semibold text-gray-800 hover:bg-gray-200 active:bg-gray-200 disabled:pointer-events-none" 183 + > 184 + Browse files 185 + </button> 186 + <p class="select-none font-medium text-gray-600">or drop your file here</p> 187 + </div> 188 + </fieldset> 189 + </div> 190 + <hr class="mx-4 border-gray-200" /> 191 + 192 + <Logger logger={logger} /> 193 + </> 194 + ); 195 + }; 196 + 197 + export default UnpackCarPage; 198 + 199 + const INVALID_CHAR_RE = /[<>:"/\\|?*\x00-\x1F]/g; 200 + const filenamify = (name: string) => { 201 + return name.replace(INVALID_CHAR_RE, '~'); 202 + };
+247
src/views/repository/repo-export.tsx
··· 1 + import { type FileSystemFileHandle, showSaveFilePicker } from 'native-file-system-adapter'; 2 + import { createSignal } from 'solid-js'; 3 + import * as v from 'valibot'; 4 + 5 + import { At } from '@atcute/client/lexicons'; 6 + import { getPdsEndpoint } from '@atcute/client/utils/did'; 7 + 8 + import { getDidDocument } from '~/api/queries/did-doc'; 9 + import { resolveHandleViaAppView, resolveHandleViaPds } from '~/api/queries/handle'; 10 + import { serviceUrlString } from '~/api/types/strings'; 11 + import { DID_OR_HANDLE_RE, isDid } from '~/api/utils/strings'; 12 + 13 + import { makeAbortable } from '~/lib/utils/abortable'; 14 + import { formatBytes } from '~/lib/utils/intl/bytes'; 15 + 16 + import Logger, { createLogger } from '~/components/logger'; 17 + 18 + const RepoExportPage = () => { 19 + const logger = createLogger(); 20 + 21 + const [getSignal] = makeAbortable(); 22 + const [pending, setPending] = createSignal(false); 23 + 24 + const mutate = async ({ 25 + identifier, 26 + service, 27 + signal, 28 + }: { 29 + identifier: string; 30 + service?: string; 31 + signal?: AbortSignal; 32 + }) => { 33 + logger.info(`Starting export for ${identifier}`); 34 + 35 + let did: At.DID; 36 + if (isDid(identifier)) { 37 + did = identifier; 38 + } else if (service) { 39 + did = await resolveHandleViaPds({ service, handle: identifier, signal }); 40 + logger.log(`Resolved handle to ${did}`); 41 + } else { 42 + did = await resolveHandleViaAppView({ handle: identifier, signal }); 43 + logger.log(`Resolved handle to ${did}`); 44 + } 45 + 46 + if (!service) { 47 + const didDoc = await getDidDocument({ did, signal }); 48 + logger.log(`Retrieved DID document`); 49 + 50 + const endpoint = getPdsEndpoint(didDoc); 51 + if (!endpoint) { 52 + logger.error(`Identity does not have a PDS server set`); 53 + return; 54 + } 55 + 56 + logger.log(`PDS located at ${endpoint}`); 57 + service = endpoint; 58 + } 59 + 60 + let fd: FileSystemFileHandle | undefined; 61 + 62 + { 63 + const progress = logger.progress(`Waiting for the user`); 64 + 65 + try { 66 + fd = await showSaveFilePicker({ 67 + suggestedName: `${identifier}-${new Date().toISOString()}.car`, 68 + 69 + // @ts-expect-error: ponyfill doesn't have the full typings 70 + id: 'repo-export', 71 + startIn: 'downloads', 72 + types: [ 73 + { 74 + description: 'CAR archive file', 75 + accept: { 'application/vnd.ipld.car': ['.car'] }, 76 + }, 77 + ], 78 + }).catch((err) => { 79 + console.warn(err); 80 + 81 + if (err instanceof DOMException && err.name === 'AbortError') { 82 + logger.warn(`Opened the file picker, but it was aborted`); 83 + } else { 84 + logger.warn(`Something went wrong when opening the file picker`); 85 + } 86 + 87 + return undefined; 88 + }); 89 + } finally { 90 + progress.destroy(); 91 + } 92 + 93 + if (fd === undefined) { 94 + // We already handled the errors above 95 + return; 96 + } 97 + } 98 + 99 + const writable = await fd.createWritable(); 100 + 101 + { 102 + const progress = logger.progress(`Downloading CAR file`); 103 + 104 + let size = 0; 105 + try { 106 + const repoUrl = new URL(`/xrpc/com.atproto.sync.getRepo?did=${did}`, service); 107 + const response = await fetch(repoUrl, { signal: signal }); 108 + 109 + if (!response.ok || !response.body) { 110 + logger.error(`Failed to retrieve CAR file`); 111 + return; 112 + } 113 + 114 + for await (const chunk of iterateStream(response.body)) { 115 + size += chunk.length; 116 + writable.write(chunk); 117 + 118 + progress.update(`Downloading CAR file (${formatBytes(size)})`); 119 + } 120 + } finally { 121 + progress.destroy(); 122 + } 123 + 124 + logger.log(`CAR file downloaded (${formatBytes(size)})`); 125 + await writable.close(); 126 + 127 + logger.log(`Finished`); 128 + } 129 + }; 130 + 131 + return ( 132 + <> 133 + <div class="p-4"> 134 + <h1 class="text-lg font-bold text-purple-800">Export repository</h1> 135 + <p class="text-gray-600">Download an archive of an account's repository</p> 136 + </div> 137 + <hr class="mx-4 border-gray-200" /> 138 + 139 + <form 140 + onSubmit={(ev) => { 141 + const formEl = ev.currentTarget; 142 + const formData = new FormData(formEl); 143 + ev.preventDefault(); 144 + 145 + const signal = getSignal(); 146 + 147 + const ident = formData.get('ident') as string; 148 + const service = formData.get('service') as string; 149 + 150 + const promise = mutate({ 151 + identifier: ident, 152 + service: service || undefined, 153 + signal, 154 + }); 155 + 156 + setPending(true); 157 + 158 + promise.then( 159 + () => { 160 + if (signal.aborted) { 161 + return; 162 + } 163 + 164 + setPending(false); 165 + }, 166 + (err) => { 167 + if (signal.aborted) { 168 + return; 169 + } 170 + 171 + setPending(false); 172 + logger.error(`Critical error: ${err}`); 173 + }, 174 + ); 175 + }} 176 + class="m-4 flex flex-col gap-4" 177 + > 178 + <fieldset disabled={pending()} class="contents"> 179 + <label class="flex flex-col gap-2"> 180 + <span class="font-semibold text-gray-600">Handle or DID identifier*</span> 181 + <input 182 + type="text" 183 + name="ident" 184 + required 185 + pattern={DID_OR_HANDLE_RE.source} 186 + placeholder="paul.bsky.social" 187 + class="rounded border border-gray-300 px-3 py-2 text-sm outline-2 -outline-offset-1 outline-purple-600 placeholder:text-gray-400 focus:outline" 188 + /> 189 + </label> 190 + 191 + <label class="flex flex-col gap-2"> 192 + <span class="font-semibold text-gray-600">PDS service</span> 193 + <input 194 + type="url" 195 + name="service" 196 + placeholder="https://bsky.social" 197 + onInput={(ev) => { 198 + const input = ev.currentTarget; 199 + const value = input.value; 200 + 201 + if (value !== '' && !v.is(serviceUrlString, value)) { 202 + input.setCustomValidity('Must be a valid service URL'); 203 + } else { 204 + input.setCustomValidity(''); 205 + } 206 + }} 207 + class="rounded border border-gray-300 px-3 py-2 text-sm outline-2 -outline-offset-1 outline-purple-600 placeholder:text-gray-400 focus:outline" 208 + /> 209 + </label> 210 + 211 + <div> 212 + <button 213 + type="submit" 214 + class="flex h-9 select-none items-center rounded bg-purple-800 px-4 text-sm font-semibold text-white hover:bg-purple-700 active:bg-purple-700 disabled:pointer-events-none disabled:opacity-50" 215 + > 216 + Export! 217 + </button> 218 + </div> 219 + </fieldset> 220 + </form> 221 + <hr class="mx-4 border-gray-200" /> 222 + 223 + <Logger logger={logger} /> 224 + </> 225 + ); 226 + }; 227 + 228 + export default RepoExportPage; 229 + 230 + export async function* iterateStream<T>(stream: ReadableStream<T>) { 231 + // Get a lock on the stream 232 + const reader = stream.getReader(); 233 + 234 + try { 235 + while (true) { 236 + const { done, value } = await reader.read(); 237 + 238 + if (done) { 239 + return; 240 + } 241 + 242 + yield value; 243 + } 244 + } finally { 245 + reader.releaseLock(); 246 + } 247 + }
+2
src/vite-env.d.ts
··· 1 + /// <reference types="vite/client" /> 2 + /// <reference types="@atcute/bluesky" />
+43
tailwind.config.js
··· 1 + import plugin from 'tailwindcss/plugin'; 2 + 3 + /** @type {import('tailwindcss').Config} */ 4 + export default { 5 + content: ['./src/**/*.tsx'], 6 + theme: { 7 + fontFamily: { 8 + sans: `"Inter", "Roboto", ui-sans-serif, sans-serif, "Noto Color Emoji", "Twemoji Mozilla"`, 9 + mono: `"JetBrains Mono NL", ui-monospace, monospace`, 10 + }, 11 + }, 12 + corePlugins: { 13 + outlineStyle: false, 14 + }, 15 + future: { 16 + hoverOnlyWhenSupported: true, 17 + }, 18 + plugins: [ 19 + plugin(({ addVariant, addUtilities }) => { 20 + addVariant('modal', '&:modal'); 21 + addVariant('focus-within', '&:has(:focus-visible)'); 22 + // addVariant('hover', '.is-mouse &:hover'); 23 + // addVariant('group-hover', '.is-mouse .group &:hover'); 24 + 25 + addUtilities({ 26 + '.scrollbar-hide': { 27 + '-ms-overflow-style': 'none', 28 + 'scrollbar-width': 'none', 29 + 30 + '&::-webkit-scrollbar': { 31 + display: 'none', 32 + }, 33 + }, 34 + 35 + '.outline-none': { 'outline-style': 'none' }, 36 + '.outline': { 'outline-style': 'solid' }, 37 + '.outline-dashed': { 'outline-style': 'dashed' }, 38 + '.outline-dotted': { 'outline-style': 'dotted' }, 39 + '.outline-double': { 'outline-style': 'double' }, 40 + }); 41 + }), 42 + ], 43 + };
+29
tsconfig.app.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "ESNext", 4 + "useDefineForClassFields": false, 5 + "module": "ESNext", 6 + "lib": ["ESNext", "DOM", "DOM.Iterable"], 7 + "types": [], 8 + "skipLibCheck": true, 9 + 10 + "moduleResolution": "Bundler", 11 + "allowImportingTsExtensions": true, 12 + "isolatedModules": true, 13 + "moduleDetection": "force", 14 + "noEmit": true, 15 + "jsx": "preserve", 16 + "jsxImportSource": "solid-js", 17 + 18 + "strict": true, 19 + "noUnusedLocals": true, 20 + "noUnusedParameters": true, 21 + "noFallthroughCasesInSwitch": true, 22 + "noUncheckedSideEffectImports": true, 23 + 24 + "paths": { 25 + "~/*": ["./src/*"], 26 + }, 27 + }, 28 + "include": ["src"], 29 + }
+4
tsconfig.json
··· 1 + { 2 + "files": [], 3 + "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }], 4 + }
+23
tsconfig.node.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "ESNext", 4 + "useDefineForClassFields": false, 5 + "module": "NodeNext", 6 + "lib": ["ESNext"], 7 + "types": ["node"], 8 + "skipLibCheck": true, 9 + 10 + "moduleResolution": "NodeNext", 11 + "allowImportingTsExtensions": true, 12 + "isolatedModules": true, 13 + "moduleDetection": "force", 14 + "noEmit": true, 15 + 16 + "strict": true, 17 + "noUnusedLocals": true, 18 + "noUnusedParameters": true, 19 + "noFallthroughCasesInSwitch": true, 20 + "noUncheckedSideEffectImports": true, 21 + }, 22 + "include": ["vite.config.ts"], 23 + }
+49
vite.config.ts
··· 1 + import path from 'node:path'; 2 + 3 + import { defineConfig } from 'vite'; 4 + import solid from 'vite-plugin-solid'; 5 + 6 + export default defineConfig({ 7 + build: { 8 + target: 'esnext', 9 + modulePreload: false, 10 + sourcemap: true, 11 + assetsInlineLimit: 0, 12 + minify: 'terser', 13 + rollupOptions: { 14 + output: { 15 + chunkFileNames: 'assets/[hash].js', 16 + manualChunks: { 17 + common: [ 18 + 'solid-js', 19 + 'solid-js/store', 20 + 'solid-js/web', 21 + 22 + '@atcute/client', 23 + '@mary/events', 24 + 25 + 'src/globals/multiagent.ts', 26 + 'src/globals/navigation.ts', 27 + 'src/globals/preferences.ts', 28 + 'src/globals/rpc.ts', 29 + ], 30 + shell: ['src/shell.tsx'], 31 + }, 32 + }, 33 + }, 34 + terserOptions: { 35 + compress: { 36 + passes: 3, 37 + }, 38 + }, 39 + }, 40 + server: { 41 + port: 10555, 42 + }, 43 + resolve: { 44 + alias: { 45 + '~': path.join(__dirname, './src'), 46 + }, 47 + }, 48 + plugins: [solid()], 49 + });