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

Configure Feed

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

initial commit

+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 + });