my pkgs monorepo

feat: initial web version

+4086
+23
packages/malachite-web/.gitignore
··· 1 + node_modules 2 + 3 + # Output 4 + .output 5 + .vercel 6 + .netlify 7 + .wrangler 8 + /.svelte-kit 9 + /build 10 + 11 + # OS 12 + .DS_Store 13 + Thumbs.db 14 + 15 + # Env 16 + .env 17 + .env.* 18 + !.env.example 19 + !.env.test 20 + 21 + # Vite 22 + vite.config.js.timestamp-* 23 + vite.config.ts.timestamp-*
+1
packages/malachite-web/.npmrc
··· 1 + engine-strict=true
+9
packages/malachite-web/.prettierignore
··· 1 + # Package Managers 2 + package-lock.json 3 + pnpm-lock.yaml 4 + yarn.lock 5 + bun.lock 6 + bun.lockb 7 + 8 + # Miscellaneous 9 + /static/
+16
packages/malachite-web/.prettierrc
··· 1 + { 2 + "useTabs": true, 3 + "singleQuote": true, 4 + "trailingComma": "none", 5 + "printWidth": 100, 6 + "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], 7 + "overrides": [ 8 + { 9 + "files": "*.svelte", 10 + "options": { 11 + "parser": "svelte" 12 + } 13 + } 14 + ], 15 + "tailwindStylesheet": "./src/routes/layout.css" 16 + }
+42
packages/malachite-web/README.md
··· 1 + # sv 2 + 3 + Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). 4 + 5 + ## Creating a project 6 + 7 + If you're seeing this, you've probably already done this step. Congrats! 8 + 9 + ```sh 10 + # create a new project 11 + npx sv create my-app 12 + ``` 13 + 14 + To recreate this project with the same configuration: 15 + 16 + ```sh 17 + # recreate this project 18 + pnpm dlx sv create --template minimal --types ts --add prettier tailwindcss="plugins:none" sveltekit-adapter="adapter:vercel" --install pnpm ./web 19 + ``` 20 + 21 + ## Developing 22 + 23 + Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: 24 + 25 + ```sh 26 + npm run dev 27 + 28 + # or start the server and open the app in a new browser tab 29 + npm run dev -- --open 30 + ``` 31 + 32 + ## Building 33 + 34 + To create a production version of your app: 35 + 36 + ```sh 37 + npm run build 38 + ``` 39 + 40 + You can preview the production build with `npm run preview`. 41 + 42 + > To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
+35
packages/malachite-web/package.json
··· 1 + { 2 + "name": "web", 3 + "private": true, 4 + "version": "0.1.0", 5 + "type": "module", 6 + "scripts": { 7 + "dev": "vite dev", 8 + "build": "vite build", 9 + "preview": "vite preview", 10 + "prepare": "svelte-kit sync || echo ''", 11 + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 12 + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 13 + "lint": "prettier --check .", 14 + "format": "prettier --write ." 15 + }, 16 + "dependencies": { 17 + "@atproto/api": "^0.18.13", 18 + "@atproto/common-web": "^0.4.12", 19 + "@lucide/svelte": "^0.575.0" 20 + }, 21 + "devDependencies": { 22 + "@sveltejs/adapter-vercel": "^6.3.1", 23 + "@sveltejs/kit": "^2.50.2", 24 + "@sveltejs/vite-plugin-svelte": "^6.2.4", 25 + "@tailwindcss/vite": "^4.1.18", 26 + "prettier": "^3.8.1", 27 + "prettier-plugin-svelte": "^3.4.1", 28 + "prettier-plugin-tailwindcss": "^0.7.2", 29 + "svelte": "^5.51.0", 30 + "svelte-check": "^4.3.6", 31 + "tailwindcss": "^4.1.18", 32 + "typescript": "^5.9.3", 33 + "vite": "^7.3.1" 34 + } 35 + }
+2110
packages/malachite-web/pnpm-lock.yaml
··· 1 + lockfileVersion: '9.0' 2 + 3 + settings: 4 + autoInstallPeers: true 5 + excludeLinksFromLockfile: false 6 + 7 + importers: 8 + 9 + .: 10 + dependencies: 11 + '@atproto/api': 12 + specifier: ^0.18.13 13 + version: 0.18.21 14 + '@atproto/common-web': 15 + specifier: ^0.4.12 16 + version: 0.4.17 17 + '@lucide/svelte': 18 + specifier: ^0.575.0 19 + version: 0.575.0(svelte@5.53.5) 20 + devDependencies: 21 + '@sveltejs/adapter-vercel': 22 + specifier: ^6.3.1 23 + version: 6.3.3(@sveltejs/kit@2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)))(rollup@4.59.0) 24 + '@sveltejs/kit': 25 + specifier: ^2.50.2 26 + version: 2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)) 27 + '@sveltejs/vite-plugin-svelte': 28 + specifier: ^6.2.4 29 + version: 6.2.4(svelte@5.53.5)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)) 30 + '@tailwindcss/vite': 31 + specifier: ^4.1.18 32 + version: 4.2.1(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)) 33 + prettier: 34 + specifier: ^3.8.1 35 + version: 3.8.1 36 + prettier-plugin-svelte: 37 + specifier: ^3.4.1 38 + version: 3.5.0(prettier@3.8.1)(svelte@5.53.5) 39 + prettier-plugin-tailwindcss: 40 + specifier: ^0.7.2 41 + version: 0.7.2(prettier-plugin-svelte@3.5.0(prettier@3.8.1)(svelte@5.53.5))(prettier@3.8.1) 42 + svelte: 43 + specifier: ^5.51.0 44 + version: 5.53.5 45 + svelte-check: 46 + specifier: ^4.3.6 47 + version: 4.4.3(picomatch@4.0.3)(svelte@5.53.5)(typescript@5.9.3) 48 + tailwindcss: 49 + specifier: ^4.1.18 50 + version: 4.2.1 51 + typescript: 52 + specifier: ^5.9.3 53 + version: 5.9.3 54 + vite: 55 + specifier: ^7.3.1 56 + version: 7.3.1(jiti@2.6.1)(lightningcss@1.31.1) 57 + 58 + packages: 59 + 60 + '@atproto/api@0.18.21': 61 + resolution: {integrity: sha512-s35MIJerGT/pKe2xJtKKswqlIr/ola2r2iURBKBL0Mk1OKe6jP4YvTMh1N2d2PEANFzNNTbKoDaLfJPo2Uvc/w==} 62 + 63 + '@atproto/common-web@0.4.17': 64 + resolution: {integrity: sha512-sfxD8NGxyoxhxmM9EUshEFbWcJ3+JHEOZF4Quk6HsCh1UxpHBmLabT/vEsAkDWl+C/8U0ine0+c/gHyE/OZiQQ==} 65 + 66 + '@atproto/lex-data@0.0.12': 67 + resolution: {integrity: sha512-aekJudcK1p6sbTqUv2bJMJBAGZaOJS0mgDclpK3U6VuBREK/au4B6ffunBFWgrDfg0Vwj2JGyEA7E51WZkJcRw==} 68 + 69 + '@atproto/lex-json@0.0.12': 70 + resolution: {integrity: sha512-XlEpnWWZdDJ5BIgG25GyH+6iBfyrFL18BI5JSE6rUfMObbFMrQRaCuRLQfryRXNysVz3L3U+Qb9y8KcXbE8AcA==} 71 + 72 + '@atproto/lexicon@0.6.1': 73 + resolution: {integrity: sha512-/vI1kVlY50Si+5MXpvOucelnYwb0UJ6Qto5mCp+7Q5C+Jtp+SoSykAPVvjVtTnQUH2vrKOFOwpb3C375vSKzXw==} 74 + 75 + '@atproto/syntax@0.4.3': 76 + resolution: {integrity: sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA==} 77 + 78 + '@atproto/xrpc@0.7.7': 79 + resolution: {integrity: sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA==} 80 + 81 + '@esbuild/aix-ppc64@0.25.12': 82 + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} 83 + engines: {node: '>=18'} 84 + cpu: [ppc64] 85 + os: [aix] 86 + 87 + '@esbuild/aix-ppc64@0.27.3': 88 + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} 89 + engines: {node: '>=18'} 90 + cpu: [ppc64] 91 + os: [aix] 92 + 93 + '@esbuild/android-arm64@0.25.12': 94 + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} 95 + engines: {node: '>=18'} 96 + cpu: [arm64] 97 + os: [android] 98 + 99 + '@esbuild/android-arm64@0.27.3': 100 + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} 101 + engines: {node: '>=18'} 102 + cpu: [arm64] 103 + os: [android] 104 + 105 + '@esbuild/android-arm@0.25.12': 106 + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} 107 + engines: {node: '>=18'} 108 + cpu: [arm] 109 + os: [android] 110 + 111 + '@esbuild/android-arm@0.27.3': 112 + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} 113 + engines: {node: '>=18'} 114 + cpu: [arm] 115 + os: [android] 116 + 117 + '@esbuild/android-x64@0.25.12': 118 + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} 119 + engines: {node: '>=18'} 120 + cpu: [x64] 121 + os: [android] 122 + 123 + '@esbuild/android-x64@0.27.3': 124 + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} 125 + engines: {node: '>=18'} 126 + cpu: [x64] 127 + os: [android] 128 + 129 + '@esbuild/darwin-arm64@0.25.12': 130 + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} 131 + engines: {node: '>=18'} 132 + cpu: [arm64] 133 + os: [darwin] 134 + 135 + '@esbuild/darwin-arm64@0.27.3': 136 + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} 137 + engines: {node: '>=18'} 138 + cpu: [arm64] 139 + os: [darwin] 140 + 141 + '@esbuild/darwin-x64@0.25.12': 142 + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} 143 + engines: {node: '>=18'} 144 + cpu: [x64] 145 + os: [darwin] 146 + 147 + '@esbuild/darwin-x64@0.27.3': 148 + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} 149 + engines: {node: '>=18'} 150 + cpu: [x64] 151 + os: [darwin] 152 + 153 + '@esbuild/freebsd-arm64@0.25.12': 154 + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} 155 + engines: {node: '>=18'} 156 + cpu: [arm64] 157 + os: [freebsd] 158 + 159 + '@esbuild/freebsd-arm64@0.27.3': 160 + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} 161 + engines: {node: '>=18'} 162 + cpu: [arm64] 163 + os: [freebsd] 164 + 165 + '@esbuild/freebsd-x64@0.25.12': 166 + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} 167 + engines: {node: '>=18'} 168 + cpu: [x64] 169 + os: [freebsd] 170 + 171 + '@esbuild/freebsd-x64@0.27.3': 172 + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} 173 + engines: {node: '>=18'} 174 + cpu: [x64] 175 + os: [freebsd] 176 + 177 + '@esbuild/linux-arm64@0.25.12': 178 + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} 179 + engines: {node: '>=18'} 180 + cpu: [arm64] 181 + os: [linux] 182 + 183 + '@esbuild/linux-arm64@0.27.3': 184 + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} 185 + engines: {node: '>=18'} 186 + cpu: [arm64] 187 + os: [linux] 188 + 189 + '@esbuild/linux-arm@0.25.12': 190 + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} 191 + engines: {node: '>=18'} 192 + cpu: [arm] 193 + os: [linux] 194 + 195 + '@esbuild/linux-arm@0.27.3': 196 + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} 197 + engines: {node: '>=18'} 198 + cpu: [arm] 199 + os: [linux] 200 + 201 + '@esbuild/linux-ia32@0.25.12': 202 + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} 203 + engines: {node: '>=18'} 204 + cpu: [ia32] 205 + os: [linux] 206 + 207 + '@esbuild/linux-ia32@0.27.3': 208 + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} 209 + engines: {node: '>=18'} 210 + cpu: [ia32] 211 + os: [linux] 212 + 213 + '@esbuild/linux-loong64@0.25.12': 214 + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} 215 + engines: {node: '>=18'} 216 + cpu: [loong64] 217 + os: [linux] 218 + 219 + '@esbuild/linux-loong64@0.27.3': 220 + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} 221 + engines: {node: '>=18'} 222 + cpu: [loong64] 223 + os: [linux] 224 + 225 + '@esbuild/linux-mips64el@0.25.12': 226 + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} 227 + engines: {node: '>=18'} 228 + cpu: [mips64el] 229 + os: [linux] 230 + 231 + '@esbuild/linux-mips64el@0.27.3': 232 + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} 233 + engines: {node: '>=18'} 234 + cpu: [mips64el] 235 + os: [linux] 236 + 237 + '@esbuild/linux-ppc64@0.25.12': 238 + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} 239 + engines: {node: '>=18'} 240 + cpu: [ppc64] 241 + os: [linux] 242 + 243 + '@esbuild/linux-ppc64@0.27.3': 244 + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} 245 + engines: {node: '>=18'} 246 + cpu: [ppc64] 247 + os: [linux] 248 + 249 + '@esbuild/linux-riscv64@0.25.12': 250 + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} 251 + engines: {node: '>=18'} 252 + cpu: [riscv64] 253 + os: [linux] 254 + 255 + '@esbuild/linux-riscv64@0.27.3': 256 + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} 257 + engines: {node: '>=18'} 258 + cpu: [riscv64] 259 + os: [linux] 260 + 261 + '@esbuild/linux-s390x@0.25.12': 262 + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} 263 + engines: {node: '>=18'} 264 + cpu: [s390x] 265 + os: [linux] 266 + 267 + '@esbuild/linux-s390x@0.27.3': 268 + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} 269 + engines: {node: '>=18'} 270 + cpu: [s390x] 271 + os: [linux] 272 + 273 + '@esbuild/linux-x64@0.25.12': 274 + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} 275 + engines: {node: '>=18'} 276 + cpu: [x64] 277 + os: [linux] 278 + 279 + '@esbuild/linux-x64@0.27.3': 280 + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} 281 + engines: {node: '>=18'} 282 + cpu: [x64] 283 + os: [linux] 284 + 285 + '@esbuild/netbsd-arm64@0.25.12': 286 + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} 287 + engines: {node: '>=18'} 288 + cpu: [arm64] 289 + os: [netbsd] 290 + 291 + '@esbuild/netbsd-arm64@0.27.3': 292 + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} 293 + engines: {node: '>=18'} 294 + cpu: [arm64] 295 + os: [netbsd] 296 + 297 + '@esbuild/netbsd-x64@0.25.12': 298 + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} 299 + engines: {node: '>=18'} 300 + cpu: [x64] 301 + os: [netbsd] 302 + 303 + '@esbuild/netbsd-x64@0.27.3': 304 + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} 305 + engines: {node: '>=18'} 306 + cpu: [x64] 307 + os: [netbsd] 308 + 309 + '@esbuild/openbsd-arm64@0.25.12': 310 + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} 311 + engines: {node: '>=18'} 312 + cpu: [arm64] 313 + os: [openbsd] 314 + 315 + '@esbuild/openbsd-arm64@0.27.3': 316 + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} 317 + engines: {node: '>=18'} 318 + cpu: [arm64] 319 + os: [openbsd] 320 + 321 + '@esbuild/openbsd-x64@0.25.12': 322 + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} 323 + engines: {node: '>=18'} 324 + cpu: [x64] 325 + os: [openbsd] 326 + 327 + '@esbuild/openbsd-x64@0.27.3': 328 + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} 329 + engines: {node: '>=18'} 330 + cpu: [x64] 331 + os: [openbsd] 332 + 333 + '@esbuild/openharmony-arm64@0.25.12': 334 + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} 335 + engines: {node: '>=18'} 336 + cpu: [arm64] 337 + os: [openharmony] 338 + 339 + '@esbuild/openharmony-arm64@0.27.3': 340 + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} 341 + engines: {node: '>=18'} 342 + cpu: [arm64] 343 + os: [openharmony] 344 + 345 + '@esbuild/sunos-x64@0.25.12': 346 + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} 347 + engines: {node: '>=18'} 348 + cpu: [x64] 349 + os: [sunos] 350 + 351 + '@esbuild/sunos-x64@0.27.3': 352 + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} 353 + engines: {node: '>=18'} 354 + cpu: [x64] 355 + os: [sunos] 356 + 357 + '@esbuild/win32-arm64@0.25.12': 358 + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} 359 + engines: {node: '>=18'} 360 + cpu: [arm64] 361 + os: [win32] 362 + 363 + '@esbuild/win32-arm64@0.27.3': 364 + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} 365 + engines: {node: '>=18'} 366 + cpu: [arm64] 367 + os: [win32] 368 + 369 + '@esbuild/win32-ia32@0.25.12': 370 + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} 371 + engines: {node: '>=18'} 372 + cpu: [ia32] 373 + os: [win32] 374 + 375 + '@esbuild/win32-ia32@0.27.3': 376 + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} 377 + engines: {node: '>=18'} 378 + cpu: [ia32] 379 + os: [win32] 380 + 381 + '@esbuild/win32-x64@0.25.12': 382 + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} 383 + engines: {node: '>=18'} 384 + cpu: [x64] 385 + os: [win32] 386 + 387 + '@esbuild/win32-x64@0.27.3': 388 + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} 389 + engines: {node: '>=18'} 390 + cpu: [x64] 391 + os: [win32] 392 + 393 + '@isaacs/fs-minipass@4.0.1': 394 + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} 395 + engines: {node: '>=18.0.0'} 396 + 397 + '@jridgewell/gen-mapping@0.3.13': 398 + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} 399 + 400 + '@jridgewell/remapping@2.3.5': 401 + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} 402 + 403 + '@jridgewell/resolve-uri@3.1.2': 404 + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 405 + engines: {node: '>=6.0.0'} 406 + 407 + '@jridgewell/sourcemap-codec@1.5.5': 408 + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} 409 + 410 + '@jridgewell/trace-mapping@0.3.31': 411 + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} 412 + 413 + '@lucide/svelte@0.575.0': 414 + resolution: {integrity: sha512-FEFp/0McZwsjBqh1Dn8H+UBm1yHFQYk+utuVMFDw57155+wz2XMoc1pw027ylCPzs+bi14UEXYKbekFhuJKtnw==} 415 + peerDependencies: 416 + svelte: ^5 417 + 418 + '@mapbox/node-pre-gyp@2.0.3': 419 + resolution: {integrity: sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==} 420 + engines: {node: '>=18'} 421 + hasBin: true 422 + 423 + '@polka/url@1.0.0-next.29': 424 + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} 425 + 426 + '@rollup/pluginutils@5.3.0': 427 + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} 428 + engines: {node: '>=14.0.0'} 429 + peerDependencies: 430 + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 431 + peerDependenciesMeta: 432 + rollup: 433 + optional: true 434 + 435 + '@rollup/rollup-android-arm-eabi@4.59.0': 436 + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} 437 + cpu: [arm] 438 + os: [android] 439 + 440 + '@rollup/rollup-android-arm64@4.59.0': 441 + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} 442 + cpu: [arm64] 443 + os: [android] 444 + 445 + '@rollup/rollup-darwin-arm64@4.59.0': 446 + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} 447 + cpu: [arm64] 448 + os: [darwin] 449 + 450 + '@rollup/rollup-darwin-x64@4.59.0': 451 + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} 452 + cpu: [x64] 453 + os: [darwin] 454 + 455 + '@rollup/rollup-freebsd-arm64@4.59.0': 456 + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} 457 + cpu: [arm64] 458 + os: [freebsd] 459 + 460 + '@rollup/rollup-freebsd-x64@4.59.0': 461 + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} 462 + cpu: [x64] 463 + os: [freebsd] 464 + 465 + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': 466 + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} 467 + cpu: [arm] 468 + os: [linux] 469 + 470 + '@rollup/rollup-linux-arm-musleabihf@4.59.0': 471 + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} 472 + cpu: [arm] 473 + os: [linux] 474 + 475 + '@rollup/rollup-linux-arm64-gnu@4.59.0': 476 + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} 477 + cpu: [arm64] 478 + os: [linux] 479 + 480 + '@rollup/rollup-linux-arm64-musl@4.59.0': 481 + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} 482 + cpu: [arm64] 483 + os: [linux] 484 + 485 + '@rollup/rollup-linux-loong64-gnu@4.59.0': 486 + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} 487 + cpu: [loong64] 488 + os: [linux] 489 + 490 + '@rollup/rollup-linux-loong64-musl@4.59.0': 491 + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} 492 + cpu: [loong64] 493 + os: [linux] 494 + 495 + '@rollup/rollup-linux-ppc64-gnu@4.59.0': 496 + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} 497 + cpu: [ppc64] 498 + os: [linux] 499 + 500 + '@rollup/rollup-linux-ppc64-musl@4.59.0': 501 + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} 502 + cpu: [ppc64] 503 + os: [linux] 504 + 505 + '@rollup/rollup-linux-riscv64-gnu@4.59.0': 506 + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} 507 + cpu: [riscv64] 508 + os: [linux] 509 + 510 + '@rollup/rollup-linux-riscv64-musl@4.59.0': 511 + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} 512 + cpu: [riscv64] 513 + os: [linux] 514 + 515 + '@rollup/rollup-linux-s390x-gnu@4.59.0': 516 + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} 517 + cpu: [s390x] 518 + os: [linux] 519 + 520 + '@rollup/rollup-linux-x64-gnu@4.59.0': 521 + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} 522 + cpu: [x64] 523 + os: [linux] 524 + 525 + '@rollup/rollup-linux-x64-musl@4.59.0': 526 + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} 527 + cpu: [x64] 528 + os: [linux] 529 + 530 + '@rollup/rollup-openbsd-x64@4.59.0': 531 + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} 532 + cpu: [x64] 533 + os: [openbsd] 534 + 535 + '@rollup/rollup-openharmony-arm64@4.59.0': 536 + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} 537 + cpu: [arm64] 538 + os: [openharmony] 539 + 540 + '@rollup/rollup-win32-arm64-msvc@4.59.0': 541 + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} 542 + cpu: [arm64] 543 + os: [win32] 544 + 545 + '@rollup/rollup-win32-ia32-msvc@4.59.0': 546 + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} 547 + cpu: [ia32] 548 + os: [win32] 549 + 550 + '@rollup/rollup-win32-x64-gnu@4.59.0': 551 + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} 552 + cpu: [x64] 553 + os: [win32] 554 + 555 + '@rollup/rollup-win32-x64-msvc@4.59.0': 556 + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} 557 + cpu: [x64] 558 + os: [win32] 559 + 560 + '@standard-schema/spec@1.1.0': 561 + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} 562 + 563 + '@sveltejs/acorn-typescript@1.0.9': 564 + resolution: {integrity: sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==} 565 + peerDependencies: 566 + acorn: ^8.9.0 567 + 568 + '@sveltejs/adapter-vercel@6.3.3': 569 + resolution: {integrity: sha512-jI7jT/XqRyFe9oqKvFcNPQfyNBi3pXqN1iQXa2lmeKT5Vzgr9iSOqJOD3pXf/9Q2Os6SXzqYYm6osRjHYEhkyw==} 570 + engines: {node: '>=20.0'} 571 + peerDependencies: 572 + '@sveltejs/kit': ^2.4.0 573 + 574 + '@sveltejs/kit@2.53.2': 575 + resolution: {integrity: sha512-M+MqAvFve12T1HWws/2npP/s3hFtyjw3GB/OXW/8a1jZBk48qnvPJrtgE+VOMc3RnjUMxc4mv/vQ73nvj2uNMg==} 576 + engines: {node: '>=18.13'} 577 + hasBin: true 578 + peerDependencies: 579 + '@opentelemetry/api': ^1.0.0 580 + '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0 581 + svelte: ^4.0.0 || ^5.0.0-next.0 582 + typescript: ^5.3.3 583 + vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0 584 + peerDependenciesMeta: 585 + '@opentelemetry/api': 586 + optional: true 587 + typescript: 588 + optional: true 589 + 590 + '@sveltejs/vite-plugin-svelte-inspector@5.0.2': 591 + resolution: {integrity: sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==} 592 + engines: {node: ^20.19 || ^22.12 || >=24} 593 + peerDependencies: 594 + '@sveltejs/vite-plugin-svelte': ^6.0.0-next.0 595 + svelte: ^5.0.0 596 + vite: ^6.3.0 || ^7.0.0 597 + 598 + '@sveltejs/vite-plugin-svelte@6.2.4': 599 + resolution: {integrity: sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==} 600 + engines: {node: ^20.19 || ^22.12 || >=24} 601 + peerDependencies: 602 + svelte: ^5.0.0 603 + vite: ^6.3.0 || ^7.0.0 604 + 605 + '@tailwindcss/node@4.2.1': 606 + resolution: {integrity: sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==} 607 + 608 + '@tailwindcss/oxide-android-arm64@4.2.1': 609 + resolution: {integrity: sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==} 610 + engines: {node: '>= 20'} 611 + cpu: [arm64] 612 + os: [android] 613 + 614 + '@tailwindcss/oxide-darwin-arm64@4.2.1': 615 + resolution: {integrity: sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==} 616 + engines: {node: '>= 20'} 617 + cpu: [arm64] 618 + os: [darwin] 619 + 620 + '@tailwindcss/oxide-darwin-x64@4.2.1': 621 + resolution: {integrity: sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==} 622 + engines: {node: '>= 20'} 623 + cpu: [x64] 624 + os: [darwin] 625 + 626 + '@tailwindcss/oxide-freebsd-x64@4.2.1': 627 + resolution: {integrity: sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==} 628 + engines: {node: '>= 20'} 629 + cpu: [x64] 630 + os: [freebsd] 631 + 632 + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': 633 + resolution: {integrity: sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==} 634 + engines: {node: '>= 20'} 635 + cpu: [arm] 636 + os: [linux] 637 + 638 + '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': 639 + resolution: {integrity: sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==} 640 + engines: {node: '>= 20'} 641 + cpu: [arm64] 642 + os: [linux] 643 + 644 + '@tailwindcss/oxide-linux-arm64-musl@4.2.1': 645 + resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==} 646 + engines: {node: '>= 20'} 647 + cpu: [arm64] 648 + os: [linux] 649 + 650 + '@tailwindcss/oxide-linux-x64-gnu@4.2.1': 651 + resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==} 652 + engines: {node: '>= 20'} 653 + cpu: [x64] 654 + os: [linux] 655 + 656 + '@tailwindcss/oxide-linux-x64-musl@4.2.1': 657 + resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==} 658 + engines: {node: '>= 20'} 659 + cpu: [x64] 660 + os: [linux] 661 + 662 + '@tailwindcss/oxide-wasm32-wasi@4.2.1': 663 + resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==} 664 + engines: {node: '>=14.0.0'} 665 + cpu: [wasm32] 666 + bundledDependencies: 667 + - '@napi-rs/wasm-runtime' 668 + - '@emnapi/core' 669 + - '@emnapi/runtime' 670 + - '@tybys/wasm-util' 671 + - '@emnapi/wasi-threads' 672 + - tslib 673 + 674 + '@tailwindcss/oxide-win32-arm64-msvc@4.2.1': 675 + resolution: {integrity: sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==} 676 + engines: {node: '>= 20'} 677 + cpu: [arm64] 678 + os: [win32] 679 + 680 + '@tailwindcss/oxide-win32-x64-msvc@4.2.1': 681 + resolution: {integrity: sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==} 682 + engines: {node: '>= 20'} 683 + cpu: [x64] 684 + os: [win32] 685 + 686 + '@tailwindcss/oxide@4.2.1': 687 + resolution: {integrity: sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==} 688 + engines: {node: '>= 20'} 689 + 690 + '@tailwindcss/vite@4.2.1': 691 + resolution: {integrity: sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==} 692 + peerDependencies: 693 + vite: ^5.2.0 || ^6 || ^7 694 + 695 + '@types/cookie@0.6.0': 696 + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} 697 + 698 + '@types/estree@1.0.8': 699 + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} 700 + 701 + '@types/trusted-types@2.0.7': 702 + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} 703 + 704 + '@vercel/nft@1.3.2': 705 + resolution: {integrity: sha512-HC8venRc4Ya7vNeBsJneKHHMDDWpQie7VaKhAIOst3MKO+DES+Y/SbzSp8mFkD7OzwAE2HhHkeSuSmwS20mz3A==} 706 + engines: {node: '>=20'} 707 + hasBin: true 708 + 709 + abbrev@3.0.1: 710 + resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==} 711 + engines: {node: ^18.17.0 || >=20.5.0} 712 + 713 + acorn-import-attributes@1.9.5: 714 + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} 715 + peerDependencies: 716 + acorn: ^8 717 + 718 + acorn@8.16.0: 719 + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} 720 + engines: {node: '>=0.4.0'} 721 + hasBin: true 722 + 723 + agent-base@7.1.4: 724 + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} 725 + engines: {node: '>= 14'} 726 + 727 + aria-query@5.3.1: 728 + resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} 729 + engines: {node: '>= 0.4'} 730 + 731 + async-sema@3.1.1: 732 + resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==} 733 + 734 + await-lock@2.2.2: 735 + resolution: {integrity: sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==} 736 + 737 + axobject-query@4.1.0: 738 + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} 739 + engines: {node: '>= 0.4'} 740 + 741 + balanced-match@4.0.4: 742 + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} 743 + engines: {node: 18 || 20 || >=22} 744 + 745 + bindings@1.5.0: 746 + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} 747 + 748 + brace-expansion@5.0.3: 749 + resolution: {integrity: sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==} 750 + engines: {node: 18 || 20 || >=22} 751 + 752 + chokidar@4.0.3: 753 + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} 754 + engines: {node: '>= 14.16.0'} 755 + 756 + chownr@3.0.0: 757 + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} 758 + engines: {node: '>=18'} 759 + 760 + clsx@2.1.1: 761 + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} 762 + engines: {node: '>=6'} 763 + 764 + consola@3.4.2: 765 + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} 766 + engines: {node: ^14.18.0 || >=16.10.0} 767 + 768 + cookie@0.6.0: 769 + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} 770 + engines: {node: '>= 0.6'} 771 + 772 + debug@4.4.3: 773 + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} 774 + engines: {node: '>=6.0'} 775 + peerDependencies: 776 + supports-color: '*' 777 + peerDependenciesMeta: 778 + supports-color: 779 + optional: true 780 + 781 + deepmerge@4.3.1: 782 + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} 783 + engines: {node: '>=0.10.0'} 784 + 785 + detect-libc@2.1.2: 786 + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} 787 + engines: {node: '>=8'} 788 + 789 + devalue@5.6.3: 790 + resolution: {integrity: sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==} 791 + 792 + enhanced-resolve@5.19.0: 793 + resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} 794 + engines: {node: '>=10.13.0'} 795 + 796 + esbuild@0.25.12: 797 + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} 798 + engines: {node: '>=18'} 799 + hasBin: true 800 + 801 + esbuild@0.27.3: 802 + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} 803 + engines: {node: '>=18'} 804 + hasBin: true 805 + 806 + esm-env@1.2.2: 807 + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} 808 + 809 + esrap@2.2.3: 810 + resolution: {integrity: sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==} 811 + 812 + estree-walker@2.0.2: 813 + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} 814 + 815 + fdir@6.5.0: 816 + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} 817 + engines: {node: '>=12.0.0'} 818 + peerDependencies: 819 + picomatch: ^3 || ^4 820 + peerDependenciesMeta: 821 + picomatch: 822 + optional: true 823 + 824 + file-uri-to-path@1.0.0: 825 + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} 826 + 827 + fsevents@2.3.3: 828 + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 829 + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 830 + os: [darwin] 831 + 832 + glob@13.0.6: 833 + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} 834 + engines: {node: 18 || 20 || >=22} 835 + 836 + graceful-fs@4.2.11: 837 + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 838 + 839 + https-proxy-agent@7.0.6: 840 + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} 841 + engines: {node: '>= 14'} 842 + 843 + is-reference@3.0.3: 844 + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} 845 + 846 + iso-datestring-validator@2.2.2: 847 + resolution: {integrity: sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==} 848 + 849 + jiti@2.6.1: 850 + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} 851 + hasBin: true 852 + 853 + kleur@4.1.5: 854 + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} 855 + engines: {node: '>=6'} 856 + 857 + lightningcss-android-arm64@1.31.1: 858 + resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==} 859 + engines: {node: '>= 12.0.0'} 860 + cpu: [arm64] 861 + os: [android] 862 + 863 + lightningcss-darwin-arm64@1.31.1: 864 + resolution: {integrity: sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==} 865 + engines: {node: '>= 12.0.0'} 866 + cpu: [arm64] 867 + os: [darwin] 868 + 869 + lightningcss-darwin-x64@1.31.1: 870 + resolution: {integrity: sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==} 871 + engines: {node: '>= 12.0.0'} 872 + cpu: [x64] 873 + os: [darwin] 874 + 875 + lightningcss-freebsd-x64@1.31.1: 876 + resolution: {integrity: sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==} 877 + engines: {node: '>= 12.0.0'} 878 + cpu: [x64] 879 + os: [freebsd] 880 + 881 + lightningcss-linux-arm-gnueabihf@1.31.1: 882 + resolution: {integrity: sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==} 883 + engines: {node: '>= 12.0.0'} 884 + cpu: [arm] 885 + os: [linux] 886 + 887 + lightningcss-linux-arm64-gnu@1.31.1: 888 + resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==} 889 + engines: {node: '>= 12.0.0'} 890 + cpu: [arm64] 891 + os: [linux] 892 + 893 + lightningcss-linux-arm64-musl@1.31.1: 894 + resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} 895 + engines: {node: '>= 12.0.0'} 896 + cpu: [arm64] 897 + os: [linux] 898 + 899 + lightningcss-linux-x64-gnu@1.31.1: 900 + resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} 901 + engines: {node: '>= 12.0.0'} 902 + cpu: [x64] 903 + os: [linux] 904 + 905 + lightningcss-linux-x64-musl@1.31.1: 906 + resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} 907 + engines: {node: '>= 12.0.0'} 908 + cpu: [x64] 909 + os: [linux] 910 + 911 + lightningcss-win32-arm64-msvc@1.31.1: 912 + resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} 913 + engines: {node: '>= 12.0.0'} 914 + cpu: [arm64] 915 + os: [win32] 916 + 917 + lightningcss-win32-x64-msvc@1.31.1: 918 + resolution: {integrity: sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==} 919 + engines: {node: '>= 12.0.0'} 920 + cpu: [x64] 921 + os: [win32] 922 + 923 + lightningcss@1.31.1: 924 + resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==} 925 + engines: {node: '>= 12.0.0'} 926 + 927 + locate-character@3.0.0: 928 + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} 929 + 930 + lru-cache@11.2.6: 931 + resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} 932 + engines: {node: 20 || >=22} 933 + 934 + magic-string@0.30.21: 935 + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} 936 + 937 + minimatch@10.2.4: 938 + resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} 939 + engines: {node: 18 || 20 || >=22} 940 + 941 + minipass@7.1.3: 942 + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} 943 + engines: {node: '>=16 || 14 >=14.17'} 944 + 945 + minizlib@3.1.0: 946 + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} 947 + engines: {node: '>= 18'} 948 + 949 + mri@1.2.0: 950 + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} 951 + engines: {node: '>=4'} 952 + 953 + mrmime@2.0.1: 954 + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} 955 + engines: {node: '>=10'} 956 + 957 + ms@2.1.3: 958 + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 959 + 960 + multiformats@9.9.0: 961 + resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==} 962 + 963 + nanoid@3.3.11: 964 + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} 965 + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 966 + hasBin: true 967 + 968 + node-fetch@2.7.0: 969 + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} 970 + engines: {node: 4.x || >=6.0.0} 971 + peerDependencies: 972 + encoding: ^0.1.0 973 + peerDependenciesMeta: 974 + encoding: 975 + optional: true 976 + 977 + node-gyp-build@4.8.4: 978 + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} 979 + hasBin: true 980 + 981 + nopt@8.1.0: 982 + resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==} 983 + engines: {node: ^18.17.0 || >=20.5.0} 984 + hasBin: true 985 + 986 + obug@2.1.1: 987 + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} 988 + 989 + path-scurry@2.0.2: 990 + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} 991 + engines: {node: 18 || 20 || >=22} 992 + 993 + picocolors@1.1.1: 994 + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} 995 + 996 + picomatch@4.0.3: 997 + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} 998 + engines: {node: '>=12'} 999 + 1000 + postcss@8.5.6: 1001 + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} 1002 + engines: {node: ^10 || ^12 || >=14} 1003 + 1004 + prettier-plugin-svelte@3.5.0: 1005 + resolution: {integrity: sha512-2lLO/7EupnjO/95t+XZesXs8Bf3nYLIDfCo270h5QWbj/vjLqmrQ1LiRk9LPggxSDsnVYfehamZNf+rgQYApZg==} 1006 + peerDependencies: 1007 + prettier: ^3.0.0 1008 + svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 1009 + 1010 + prettier-plugin-tailwindcss@0.7.2: 1011 + resolution: {integrity: sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA==} 1012 + engines: {node: '>=20.19'} 1013 + peerDependencies: 1014 + '@ianvs/prettier-plugin-sort-imports': '*' 1015 + '@prettier/plugin-hermes': '*' 1016 + '@prettier/plugin-oxc': '*' 1017 + '@prettier/plugin-pug': '*' 1018 + '@shopify/prettier-plugin-liquid': '*' 1019 + '@trivago/prettier-plugin-sort-imports': '*' 1020 + '@zackad/prettier-plugin-twig': '*' 1021 + prettier: ^3.0 1022 + prettier-plugin-astro: '*' 1023 + prettier-plugin-css-order: '*' 1024 + prettier-plugin-jsdoc: '*' 1025 + prettier-plugin-marko: '*' 1026 + prettier-plugin-multiline-arrays: '*' 1027 + prettier-plugin-organize-attributes: '*' 1028 + prettier-plugin-organize-imports: '*' 1029 + prettier-plugin-sort-imports: '*' 1030 + prettier-plugin-svelte: '*' 1031 + peerDependenciesMeta: 1032 + '@ianvs/prettier-plugin-sort-imports': 1033 + optional: true 1034 + '@prettier/plugin-hermes': 1035 + optional: true 1036 + '@prettier/plugin-oxc': 1037 + optional: true 1038 + '@prettier/plugin-pug': 1039 + optional: true 1040 + '@shopify/prettier-plugin-liquid': 1041 + optional: true 1042 + '@trivago/prettier-plugin-sort-imports': 1043 + optional: true 1044 + '@zackad/prettier-plugin-twig': 1045 + optional: true 1046 + prettier-plugin-astro: 1047 + optional: true 1048 + prettier-plugin-css-order: 1049 + optional: true 1050 + prettier-plugin-jsdoc: 1051 + optional: true 1052 + prettier-plugin-marko: 1053 + optional: true 1054 + prettier-plugin-multiline-arrays: 1055 + optional: true 1056 + prettier-plugin-organize-attributes: 1057 + optional: true 1058 + prettier-plugin-organize-imports: 1059 + optional: true 1060 + prettier-plugin-sort-imports: 1061 + optional: true 1062 + prettier-plugin-svelte: 1063 + optional: true 1064 + 1065 + prettier@3.8.1: 1066 + resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} 1067 + engines: {node: '>=14'} 1068 + hasBin: true 1069 + 1070 + readdirp@4.1.2: 1071 + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} 1072 + engines: {node: '>= 14.18.0'} 1073 + 1074 + resolve-from@5.0.0: 1075 + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} 1076 + engines: {node: '>=8'} 1077 + 1078 + rollup@4.59.0: 1079 + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} 1080 + engines: {node: '>=18.0.0', npm: '>=8.0.0'} 1081 + hasBin: true 1082 + 1083 + sade@1.8.1: 1084 + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} 1085 + engines: {node: '>=6'} 1086 + 1087 + semver@7.7.4: 1088 + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} 1089 + engines: {node: '>=10'} 1090 + hasBin: true 1091 + 1092 + set-cookie-parser@3.0.1: 1093 + resolution: {integrity: sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==} 1094 + 1095 + sirv@3.0.2: 1096 + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} 1097 + engines: {node: '>=18'} 1098 + 1099 + source-map-js@1.2.1: 1100 + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 1101 + engines: {node: '>=0.10.0'} 1102 + 1103 + svelte-check@4.4.3: 1104 + resolution: {integrity: sha512-4HtdEv2hOoLCEsSXI+RDELk9okP/4sImWa7X02OjMFFOWeSdFF3NFy3vqpw0z+eH9C88J9vxZfUXz/Uv2A1ANw==} 1105 + engines: {node: '>= 18.0.0'} 1106 + hasBin: true 1107 + peerDependencies: 1108 + svelte: ^4.0.0 || ^5.0.0-next.0 1109 + typescript: '>=5.0.0' 1110 + 1111 + svelte@5.53.5: 1112 + resolution: {integrity: sha512-YkqERnF05g8KLdDZwZrF8/i1eSbj6Eoat8Jjr2IfruZz9StLuBqo8sfCSzjosNKd+ZrQ8DkKZDjpO5y3ht1Pow==} 1113 + engines: {node: '>=18'} 1114 + 1115 + tailwindcss@4.2.1: 1116 + resolution: {integrity: sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==} 1117 + 1118 + tapable@2.3.0: 1119 + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} 1120 + engines: {node: '>=6'} 1121 + 1122 + tar@7.5.9: 1123 + resolution: {integrity: sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==} 1124 + engines: {node: '>=18'} 1125 + 1126 + tinyglobby@0.2.15: 1127 + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} 1128 + engines: {node: '>=12.0.0'} 1129 + 1130 + tlds@1.261.0: 1131 + resolution: {integrity: sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==} 1132 + hasBin: true 1133 + 1134 + totalist@3.0.1: 1135 + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} 1136 + engines: {node: '>=6'} 1137 + 1138 + tr46@0.0.3: 1139 + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} 1140 + 1141 + tslib@2.8.1: 1142 + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} 1143 + 1144 + typescript@5.9.3: 1145 + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} 1146 + engines: {node: '>=14.17'} 1147 + hasBin: true 1148 + 1149 + uint8arrays@3.0.0: 1150 + resolution: {integrity: sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==} 1151 + 1152 + unicode-segmenter@0.14.5: 1153 + resolution: {integrity: sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==} 1154 + 1155 + vite@7.3.1: 1156 + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} 1157 + engines: {node: ^20.19.0 || >=22.12.0} 1158 + hasBin: true 1159 + peerDependencies: 1160 + '@types/node': ^20.19.0 || >=22.12.0 1161 + jiti: '>=1.21.0' 1162 + less: ^4.0.0 1163 + lightningcss: ^1.21.0 1164 + sass: ^1.70.0 1165 + sass-embedded: ^1.70.0 1166 + stylus: '>=0.54.8' 1167 + sugarss: ^5.0.0 1168 + terser: ^5.16.0 1169 + tsx: ^4.8.1 1170 + yaml: ^2.4.2 1171 + peerDependenciesMeta: 1172 + '@types/node': 1173 + optional: true 1174 + jiti: 1175 + optional: true 1176 + less: 1177 + optional: true 1178 + lightningcss: 1179 + optional: true 1180 + sass: 1181 + optional: true 1182 + sass-embedded: 1183 + optional: true 1184 + stylus: 1185 + optional: true 1186 + sugarss: 1187 + optional: true 1188 + terser: 1189 + optional: true 1190 + tsx: 1191 + optional: true 1192 + yaml: 1193 + optional: true 1194 + 1195 + vitefu@1.1.2: 1196 + resolution: {integrity: sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==} 1197 + peerDependencies: 1198 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0 1199 + peerDependenciesMeta: 1200 + vite: 1201 + optional: true 1202 + 1203 + webidl-conversions@3.0.1: 1204 + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} 1205 + 1206 + whatwg-url@5.0.0: 1207 + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} 1208 + 1209 + yallist@5.0.0: 1210 + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} 1211 + engines: {node: '>=18'} 1212 + 1213 + zimmerframe@1.1.4: 1214 + resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} 1215 + 1216 + zod@3.25.76: 1217 + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} 1218 + 1219 + snapshots: 1220 + 1221 + '@atproto/api@0.18.21': 1222 + dependencies: 1223 + '@atproto/common-web': 0.4.17 1224 + '@atproto/lexicon': 0.6.1 1225 + '@atproto/syntax': 0.4.3 1226 + '@atproto/xrpc': 0.7.7 1227 + await-lock: 2.2.2 1228 + multiformats: 9.9.0 1229 + tlds: 1.261.0 1230 + zod: 3.25.76 1231 + 1232 + '@atproto/common-web@0.4.17': 1233 + dependencies: 1234 + '@atproto/lex-data': 0.0.12 1235 + '@atproto/lex-json': 0.0.12 1236 + '@atproto/syntax': 0.4.3 1237 + zod: 3.25.76 1238 + 1239 + '@atproto/lex-data@0.0.12': 1240 + dependencies: 1241 + multiformats: 9.9.0 1242 + tslib: 2.8.1 1243 + uint8arrays: 3.0.0 1244 + unicode-segmenter: 0.14.5 1245 + 1246 + '@atproto/lex-json@0.0.12': 1247 + dependencies: 1248 + '@atproto/lex-data': 0.0.12 1249 + tslib: 2.8.1 1250 + 1251 + '@atproto/lexicon@0.6.1': 1252 + dependencies: 1253 + '@atproto/common-web': 0.4.17 1254 + '@atproto/syntax': 0.4.3 1255 + iso-datestring-validator: 2.2.2 1256 + multiformats: 9.9.0 1257 + zod: 3.25.76 1258 + 1259 + '@atproto/syntax@0.4.3': 1260 + dependencies: 1261 + tslib: 2.8.1 1262 + 1263 + '@atproto/xrpc@0.7.7': 1264 + dependencies: 1265 + '@atproto/lexicon': 0.6.1 1266 + zod: 3.25.76 1267 + 1268 + '@esbuild/aix-ppc64@0.25.12': 1269 + optional: true 1270 + 1271 + '@esbuild/aix-ppc64@0.27.3': 1272 + optional: true 1273 + 1274 + '@esbuild/android-arm64@0.25.12': 1275 + optional: true 1276 + 1277 + '@esbuild/android-arm64@0.27.3': 1278 + optional: true 1279 + 1280 + '@esbuild/android-arm@0.25.12': 1281 + optional: true 1282 + 1283 + '@esbuild/android-arm@0.27.3': 1284 + optional: true 1285 + 1286 + '@esbuild/android-x64@0.25.12': 1287 + optional: true 1288 + 1289 + '@esbuild/android-x64@0.27.3': 1290 + optional: true 1291 + 1292 + '@esbuild/darwin-arm64@0.25.12': 1293 + optional: true 1294 + 1295 + '@esbuild/darwin-arm64@0.27.3': 1296 + optional: true 1297 + 1298 + '@esbuild/darwin-x64@0.25.12': 1299 + optional: true 1300 + 1301 + '@esbuild/darwin-x64@0.27.3': 1302 + optional: true 1303 + 1304 + '@esbuild/freebsd-arm64@0.25.12': 1305 + optional: true 1306 + 1307 + '@esbuild/freebsd-arm64@0.27.3': 1308 + optional: true 1309 + 1310 + '@esbuild/freebsd-x64@0.25.12': 1311 + optional: true 1312 + 1313 + '@esbuild/freebsd-x64@0.27.3': 1314 + optional: true 1315 + 1316 + '@esbuild/linux-arm64@0.25.12': 1317 + optional: true 1318 + 1319 + '@esbuild/linux-arm64@0.27.3': 1320 + optional: true 1321 + 1322 + '@esbuild/linux-arm@0.25.12': 1323 + optional: true 1324 + 1325 + '@esbuild/linux-arm@0.27.3': 1326 + optional: true 1327 + 1328 + '@esbuild/linux-ia32@0.25.12': 1329 + optional: true 1330 + 1331 + '@esbuild/linux-ia32@0.27.3': 1332 + optional: true 1333 + 1334 + '@esbuild/linux-loong64@0.25.12': 1335 + optional: true 1336 + 1337 + '@esbuild/linux-loong64@0.27.3': 1338 + optional: true 1339 + 1340 + '@esbuild/linux-mips64el@0.25.12': 1341 + optional: true 1342 + 1343 + '@esbuild/linux-mips64el@0.27.3': 1344 + optional: true 1345 + 1346 + '@esbuild/linux-ppc64@0.25.12': 1347 + optional: true 1348 + 1349 + '@esbuild/linux-ppc64@0.27.3': 1350 + optional: true 1351 + 1352 + '@esbuild/linux-riscv64@0.25.12': 1353 + optional: true 1354 + 1355 + '@esbuild/linux-riscv64@0.27.3': 1356 + optional: true 1357 + 1358 + '@esbuild/linux-s390x@0.25.12': 1359 + optional: true 1360 + 1361 + '@esbuild/linux-s390x@0.27.3': 1362 + optional: true 1363 + 1364 + '@esbuild/linux-x64@0.25.12': 1365 + optional: true 1366 + 1367 + '@esbuild/linux-x64@0.27.3': 1368 + optional: true 1369 + 1370 + '@esbuild/netbsd-arm64@0.25.12': 1371 + optional: true 1372 + 1373 + '@esbuild/netbsd-arm64@0.27.3': 1374 + optional: true 1375 + 1376 + '@esbuild/netbsd-x64@0.25.12': 1377 + optional: true 1378 + 1379 + '@esbuild/netbsd-x64@0.27.3': 1380 + optional: true 1381 + 1382 + '@esbuild/openbsd-arm64@0.25.12': 1383 + optional: true 1384 + 1385 + '@esbuild/openbsd-arm64@0.27.3': 1386 + optional: true 1387 + 1388 + '@esbuild/openbsd-x64@0.25.12': 1389 + optional: true 1390 + 1391 + '@esbuild/openbsd-x64@0.27.3': 1392 + optional: true 1393 + 1394 + '@esbuild/openharmony-arm64@0.25.12': 1395 + optional: true 1396 + 1397 + '@esbuild/openharmony-arm64@0.27.3': 1398 + optional: true 1399 + 1400 + '@esbuild/sunos-x64@0.25.12': 1401 + optional: true 1402 + 1403 + '@esbuild/sunos-x64@0.27.3': 1404 + optional: true 1405 + 1406 + '@esbuild/win32-arm64@0.25.12': 1407 + optional: true 1408 + 1409 + '@esbuild/win32-arm64@0.27.3': 1410 + optional: true 1411 + 1412 + '@esbuild/win32-ia32@0.25.12': 1413 + optional: true 1414 + 1415 + '@esbuild/win32-ia32@0.27.3': 1416 + optional: true 1417 + 1418 + '@esbuild/win32-x64@0.25.12': 1419 + optional: true 1420 + 1421 + '@esbuild/win32-x64@0.27.3': 1422 + optional: true 1423 + 1424 + '@isaacs/fs-minipass@4.0.1': 1425 + dependencies: 1426 + minipass: 7.1.3 1427 + 1428 + '@jridgewell/gen-mapping@0.3.13': 1429 + dependencies: 1430 + '@jridgewell/sourcemap-codec': 1.5.5 1431 + '@jridgewell/trace-mapping': 0.3.31 1432 + 1433 + '@jridgewell/remapping@2.3.5': 1434 + dependencies: 1435 + '@jridgewell/gen-mapping': 0.3.13 1436 + '@jridgewell/trace-mapping': 0.3.31 1437 + 1438 + '@jridgewell/resolve-uri@3.1.2': {} 1439 + 1440 + '@jridgewell/sourcemap-codec@1.5.5': {} 1441 + 1442 + '@jridgewell/trace-mapping@0.3.31': 1443 + dependencies: 1444 + '@jridgewell/resolve-uri': 3.1.2 1445 + '@jridgewell/sourcemap-codec': 1.5.5 1446 + 1447 + '@lucide/svelte@0.575.0(svelte@5.53.5)': 1448 + dependencies: 1449 + svelte: 5.53.5 1450 + 1451 + '@mapbox/node-pre-gyp@2.0.3': 1452 + dependencies: 1453 + consola: 3.4.2 1454 + detect-libc: 2.1.2 1455 + https-proxy-agent: 7.0.6 1456 + node-fetch: 2.7.0 1457 + nopt: 8.1.0 1458 + semver: 7.7.4 1459 + tar: 7.5.9 1460 + transitivePeerDependencies: 1461 + - encoding 1462 + - supports-color 1463 + 1464 + '@polka/url@1.0.0-next.29': {} 1465 + 1466 + '@rollup/pluginutils@5.3.0(rollup@4.59.0)': 1467 + dependencies: 1468 + '@types/estree': 1.0.8 1469 + estree-walker: 2.0.2 1470 + picomatch: 4.0.3 1471 + optionalDependencies: 1472 + rollup: 4.59.0 1473 + 1474 + '@rollup/rollup-android-arm-eabi@4.59.0': 1475 + optional: true 1476 + 1477 + '@rollup/rollup-android-arm64@4.59.0': 1478 + optional: true 1479 + 1480 + '@rollup/rollup-darwin-arm64@4.59.0': 1481 + optional: true 1482 + 1483 + '@rollup/rollup-darwin-x64@4.59.0': 1484 + optional: true 1485 + 1486 + '@rollup/rollup-freebsd-arm64@4.59.0': 1487 + optional: true 1488 + 1489 + '@rollup/rollup-freebsd-x64@4.59.0': 1490 + optional: true 1491 + 1492 + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': 1493 + optional: true 1494 + 1495 + '@rollup/rollup-linux-arm-musleabihf@4.59.0': 1496 + optional: true 1497 + 1498 + '@rollup/rollup-linux-arm64-gnu@4.59.0': 1499 + optional: true 1500 + 1501 + '@rollup/rollup-linux-arm64-musl@4.59.0': 1502 + optional: true 1503 + 1504 + '@rollup/rollup-linux-loong64-gnu@4.59.0': 1505 + optional: true 1506 + 1507 + '@rollup/rollup-linux-loong64-musl@4.59.0': 1508 + optional: true 1509 + 1510 + '@rollup/rollup-linux-ppc64-gnu@4.59.0': 1511 + optional: true 1512 + 1513 + '@rollup/rollup-linux-ppc64-musl@4.59.0': 1514 + optional: true 1515 + 1516 + '@rollup/rollup-linux-riscv64-gnu@4.59.0': 1517 + optional: true 1518 + 1519 + '@rollup/rollup-linux-riscv64-musl@4.59.0': 1520 + optional: true 1521 + 1522 + '@rollup/rollup-linux-s390x-gnu@4.59.0': 1523 + optional: true 1524 + 1525 + '@rollup/rollup-linux-x64-gnu@4.59.0': 1526 + optional: true 1527 + 1528 + '@rollup/rollup-linux-x64-musl@4.59.0': 1529 + optional: true 1530 + 1531 + '@rollup/rollup-openbsd-x64@4.59.0': 1532 + optional: true 1533 + 1534 + '@rollup/rollup-openharmony-arm64@4.59.0': 1535 + optional: true 1536 + 1537 + '@rollup/rollup-win32-arm64-msvc@4.59.0': 1538 + optional: true 1539 + 1540 + '@rollup/rollup-win32-ia32-msvc@4.59.0': 1541 + optional: true 1542 + 1543 + '@rollup/rollup-win32-x64-gnu@4.59.0': 1544 + optional: true 1545 + 1546 + '@rollup/rollup-win32-x64-msvc@4.59.0': 1547 + optional: true 1548 + 1549 + '@standard-schema/spec@1.1.0': {} 1550 + 1551 + '@sveltejs/acorn-typescript@1.0.9(acorn@8.16.0)': 1552 + dependencies: 1553 + acorn: 8.16.0 1554 + 1555 + '@sveltejs/adapter-vercel@6.3.3(@sveltejs/kit@2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)))(rollup@4.59.0)': 1556 + dependencies: 1557 + '@sveltejs/kit': 2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)) 1558 + '@vercel/nft': 1.3.2(rollup@4.59.0) 1559 + esbuild: 0.25.12 1560 + transitivePeerDependencies: 1561 + - encoding 1562 + - rollup 1563 + - supports-color 1564 + 1565 + '@sveltejs/kit@2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1))': 1566 + dependencies: 1567 + '@standard-schema/spec': 1.1.0 1568 + '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) 1569 + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.5)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)) 1570 + '@types/cookie': 0.6.0 1571 + acorn: 8.16.0 1572 + cookie: 0.6.0 1573 + devalue: 5.6.3 1574 + esm-env: 1.2.2 1575 + kleur: 4.1.5 1576 + magic-string: 0.30.21 1577 + mrmime: 2.0.1 1578 + set-cookie-parser: 3.0.1 1579 + sirv: 3.0.2 1580 + svelte: 5.53.5 1581 + vite: 7.3.1(jiti@2.6.1)(lightningcss@1.31.1) 1582 + optionalDependencies: 1583 + typescript: 5.9.3 1584 + 1585 + '@sveltejs/vite-plugin-svelte-inspector@5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1))': 1586 + dependencies: 1587 + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.5)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)) 1588 + obug: 2.1.1 1589 + svelte: 5.53.5 1590 + vite: 7.3.1(jiti@2.6.1)(lightningcss@1.31.1) 1591 + 1592 + '@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1))': 1593 + dependencies: 1594 + '@sveltejs/vite-plugin-svelte-inspector': 5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)) 1595 + deepmerge: 4.3.1 1596 + magic-string: 0.30.21 1597 + obug: 2.1.1 1598 + svelte: 5.53.5 1599 + vite: 7.3.1(jiti@2.6.1)(lightningcss@1.31.1) 1600 + vitefu: 1.1.2(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)) 1601 + 1602 + '@tailwindcss/node@4.2.1': 1603 + dependencies: 1604 + '@jridgewell/remapping': 2.3.5 1605 + enhanced-resolve: 5.19.0 1606 + jiti: 2.6.1 1607 + lightningcss: 1.31.1 1608 + magic-string: 0.30.21 1609 + source-map-js: 1.2.1 1610 + tailwindcss: 4.2.1 1611 + 1612 + '@tailwindcss/oxide-android-arm64@4.2.1': 1613 + optional: true 1614 + 1615 + '@tailwindcss/oxide-darwin-arm64@4.2.1': 1616 + optional: true 1617 + 1618 + '@tailwindcss/oxide-darwin-x64@4.2.1': 1619 + optional: true 1620 + 1621 + '@tailwindcss/oxide-freebsd-x64@4.2.1': 1622 + optional: true 1623 + 1624 + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': 1625 + optional: true 1626 + 1627 + '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': 1628 + optional: true 1629 + 1630 + '@tailwindcss/oxide-linux-arm64-musl@4.2.1': 1631 + optional: true 1632 + 1633 + '@tailwindcss/oxide-linux-x64-gnu@4.2.1': 1634 + optional: true 1635 + 1636 + '@tailwindcss/oxide-linux-x64-musl@4.2.1': 1637 + optional: true 1638 + 1639 + '@tailwindcss/oxide-wasm32-wasi@4.2.1': 1640 + optional: true 1641 + 1642 + '@tailwindcss/oxide-win32-arm64-msvc@4.2.1': 1643 + optional: true 1644 + 1645 + '@tailwindcss/oxide-win32-x64-msvc@4.2.1': 1646 + optional: true 1647 + 1648 + '@tailwindcss/oxide@4.2.1': 1649 + optionalDependencies: 1650 + '@tailwindcss/oxide-android-arm64': 4.2.1 1651 + '@tailwindcss/oxide-darwin-arm64': 4.2.1 1652 + '@tailwindcss/oxide-darwin-x64': 4.2.1 1653 + '@tailwindcss/oxide-freebsd-x64': 4.2.1 1654 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.1 1655 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.1 1656 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.1 1657 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.1 1658 + '@tailwindcss/oxide-linux-x64-musl': 4.2.1 1659 + '@tailwindcss/oxide-wasm32-wasi': 4.2.1 1660 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.1 1661 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.1 1662 + 1663 + '@tailwindcss/vite@4.2.1(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1))': 1664 + dependencies: 1665 + '@tailwindcss/node': 4.2.1 1666 + '@tailwindcss/oxide': 4.2.1 1667 + tailwindcss: 4.2.1 1668 + vite: 7.3.1(jiti@2.6.1)(lightningcss@1.31.1) 1669 + 1670 + '@types/cookie@0.6.0': {} 1671 + 1672 + '@types/estree@1.0.8': {} 1673 + 1674 + '@types/trusted-types@2.0.7': {} 1675 + 1676 + '@vercel/nft@1.3.2(rollup@4.59.0)': 1677 + dependencies: 1678 + '@mapbox/node-pre-gyp': 2.0.3 1679 + '@rollup/pluginutils': 5.3.0(rollup@4.59.0) 1680 + acorn: 8.16.0 1681 + acorn-import-attributes: 1.9.5(acorn@8.16.0) 1682 + async-sema: 3.1.1 1683 + bindings: 1.5.0 1684 + estree-walker: 2.0.2 1685 + glob: 13.0.6 1686 + graceful-fs: 4.2.11 1687 + node-gyp-build: 4.8.4 1688 + picomatch: 4.0.3 1689 + resolve-from: 5.0.0 1690 + transitivePeerDependencies: 1691 + - encoding 1692 + - rollup 1693 + - supports-color 1694 + 1695 + abbrev@3.0.1: {} 1696 + 1697 + acorn-import-attributes@1.9.5(acorn@8.16.0): 1698 + dependencies: 1699 + acorn: 8.16.0 1700 + 1701 + acorn@8.16.0: {} 1702 + 1703 + agent-base@7.1.4: {} 1704 + 1705 + aria-query@5.3.1: {} 1706 + 1707 + async-sema@3.1.1: {} 1708 + 1709 + await-lock@2.2.2: {} 1710 + 1711 + axobject-query@4.1.0: {} 1712 + 1713 + balanced-match@4.0.4: {} 1714 + 1715 + bindings@1.5.0: 1716 + dependencies: 1717 + file-uri-to-path: 1.0.0 1718 + 1719 + brace-expansion@5.0.3: 1720 + dependencies: 1721 + balanced-match: 4.0.4 1722 + 1723 + chokidar@4.0.3: 1724 + dependencies: 1725 + readdirp: 4.1.2 1726 + 1727 + chownr@3.0.0: {} 1728 + 1729 + clsx@2.1.1: {} 1730 + 1731 + consola@3.4.2: {} 1732 + 1733 + cookie@0.6.0: {} 1734 + 1735 + debug@4.4.3: 1736 + dependencies: 1737 + ms: 2.1.3 1738 + 1739 + deepmerge@4.3.1: {} 1740 + 1741 + detect-libc@2.1.2: {} 1742 + 1743 + devalue@5.6.3: {} 1744 + 1745 + enhanced-resolve@5.19.0: 1746 + dependencies: 1747 + graceful-fs: 4.2.11 1748 + tapable: 2.3.0 1749 + 1750 + esbuild@0.25.12: 1751 + optionalDependencies: 1752 + '@esbuild/aix-ppc64': 0.25.12 1753 + '@esbuild/android-arm': 0.25.12 1754 + '@esbuild/android-arm64': 0.25.12 1755 + '@esbuild/android-x64': 0.25.12 1756 + '@esbuild/darwin-arm64': 0.25.12 1757 + '@esbuild/darwin-x64': 0.25.12 1758 + '@esbuild/freebsd-arm64': 0.25.12 1759 + '@esbuild/freebsd-x64': 0.25.12 1760 + '@esbuild/linux-arm': 0.25.12 1761 + '@esbuild/linux-arm64': 0.25.12 1762 + '@esbuild/linux-ia32': 0.25.12 1763 + '@esbuild/linux-loong64': 0.25.12 1764 + '@esbuild/linux-mips64el': 0.25.12 1765 + '@esbuild/linux-ppc64': 0.25.12 1766 + '@esbuild/linux-riscv64': 0.25.12 1767 + '@esbuild/linux-s390x': 0.25.12 1768 + '@esbuild/linux-x64': 0.25.12 1769 + '@esbuild/netbsd-arm64': 0.25.12 1770 + '@esbuild/netbsd-x64': 0.25.12 1771 + '@esbuild/openbsd-arm64': 0.25.12 1772 + '@esbuild/openbsd-x64': 0.25.12 1773 + '@esbuild/openharmony-arm64': 0.25.12 1774 + '@esbuild/sunos-x64': 0.25.12 1775 + '@esbuild/win32-arm64': 0.25.12 1776 + '@esbuild/win32-ia32': 0.25.12 1777 + '@esbuild/win32-x64': 0.25.12 1778 + 1779 + esbuild@0.27.3: 1780 + optionalDependencies: 1781 + '@esbuild/aix-ppc64': 0.27.3 1782 + '@esbuild/android-arm': 0.27.3 1783 + '@esbuild/android-arm64': 0.27.3 1784 + '@esbuild/android-x64': 0.27.3 1785 + '@esbuild/darwin-arm64': 0.27.3 1786 + '@esbuild/darwin-x64': 0.27.3 1787 + '@esbuild/freebsd-arm64': 0.27.3 1788 + '@esbuild/freebsd-x64': 0.27.3 1789 + '@esbuild/linux-arm': 0.27.3 1790 + '@esbuild/linux-arm64': 0.27.3 1791 + '@esbuild/linux-ia32': 0.27.3 1792 + '@esbuild/linux-loong64': 0.27.3 1793 + '@esbuild/linux-mips64el': 0.27.3 1794 + '@esbuild/linux-ppc64': 0.27.3 1795 + '@esbuild/linux-riscv64': 0.27.3 1796 + '@esbuild/linux-s390x': 0.27.3 1797 + '@esbuild/linux-x64': 0.27.3 1798 + '@esbuild/netbsd-arm64': 0.27.3 1799 + '@esbuild/netbsd-x64': 0.27.3 1800 + '@esbuild/openbsd-arm64': 0.27.3 1801 + '@esbuild/openbsd-x64': 0.27.3 1802 + '@esbuild/openharmony-arm64': 0.27.3 1803 + '@esbuild/sunos-x64': 0.27.3 1804 + '@esbuild/win32-arm64': 0.27.3 1805 + '@esbuild/win32-ia32': 0.27.3 1806 + '@esbuild/win32-x64': 0.27.3 1807 + 1808 + esm-env@1.2.2: {} 1809 + 1810 + esrap@2.2.3: 1811 + dependencies: 1812 + '@jridgewell/sourcemap-codec': 1.5.5 1813 + 1814 + estree-walker@2.0.2: {} 1815 + 1816 + fdir@6.5.0(picomatch@4.0.3): 1817 + optionalDependencies: 1818 + picomatch: 4.0.3 1819 + 1820 + file-uri-to-path@1.0.0: {} 1821 + 1822 + fsevents@2.3.3: 1823 + optional: true 1824 + 1825 + glob@13.0.6: 1826 + dependencies: 1827 + minimatch: 10.2.4 1828 + minipass: 7.1.3 1829 + path-scurry: 2.0.2 1830 + 1831 + graceful-fs@4.2.11: {} 1832 + 1833 + https-proxy-agent@7.0.6: 1834 + dependencies: 1835 + agent-base: 7.1.4 1836 + debug: 4.4.3 1837 + transitivePeerDependencies: 1838 + - supports-color 1839 + 1840 + is-reference@3.0.3: 1841 + dependencies: 1842 + '@types/estree': 1.0.8 1843 + 1844 + iso-datestring-validator@2.2.2: {} 1845 + 1846 + jiti@2.6.1: {} 1847 + 1848 + kleur@4.1.5: {} 1849 + 1850 + lightningcss-android-arm64@1.31.1: 1851 + optional: true 1852 + 1853 + lightningcss-darwin-arm64@1.31.1: 1854 + optional: true 1855 + 1856 + lightningcss-darwin-x64@1.31.1: 1857 + optional: true 1858 + 1859 + lightningcss-freebsd-x64@1.31.1: 1860 + optional: true 1861 + 1862 + lightningcss-linux-arm-gnueabihf@1.31.1: 1863 + optional: true 1864 + 1865 + lightningcss-linux-arm64-gnu@1.31.1: 1866 + optional: true 1867 + 1868 + lightningcss-linux-arm64-musl@1.31.1: 1869 + optional: true 1870 + 1871 + lightningcss-linux-x64-gnu@1.31.1: 1872 + optional: true 1873 + 1874 + lightningcss-linux-x64-musl@1.31.1: 1875 + optional: true 1876 + 1877 + lightningcss-win32-arm64-msvc@1.31.1: 1878 + optional: true 1879 + 1880 + lightningcss-win32-x64-msvc@1.31.1: 1881 + optional: true 1882 + 1883 + lightningcss@1.31.1: 1884 + dependencies: 1885 + detect-libc: 2.1.2 1886 + optionalDependencies: 1887 + lightningcss-android-arm64: 1.31.1 1888 + lightningcss-darwin-arm64: 1.31.1 1889 + lightningcss-darwin-x64: 1.31.1 1890 + lightningcss-freebsd-x64: 1.31.1 1891 + lightningcss-linux-arm-gnueabihf: 1.31.1 1892 + lightningcss-linux-arm64-gnu: 1.31.1 1893 + lightningcss-linux-arm64-musl: 1.31.1 1894 + lightningcss-linux-x64-gnu: 1.31.1 1895 + lightningcss-linux-x64-musl: 1.31.1 1896 + lightningcss-win32-arm64-msvc: 1.31.1 1897 + lightningcss-win32-x64-msvc: 1.31.1 1898 + 1899 + locate-character@3.0.0: {} 1900 + 1901 + lru-cache@11.2.6: {} 1902 + 1903 + magic-string@0.30.21: 1904 + dependencies: 1905 + '@jridgewell/sourcemap-codec': 1.5.5 1906 + 1907 + minimatch@10.2.4: 1908 + dependencies: 1909 + brace-expansion: 5.0.3 1910 + 1911 + minipass@7.1.3: {} 1912 + 1913 + minizlib@3.1.0: 1914 + dependencies: 1915 + minipass: 7.1.3 1916 + 1917 + mri@1.2.0: {} 1918 + 1919 + mrmime@2.0.1: {} 1920 + 1921 + ms@2.1.3: {} 1922 + 1923 + multiformats@9.9.0: {} 1924 + 1925 + nanoid@3.3.11: {} 1926 + 1927 + node-fetch@2.7.0: 1928 + dependencies: 1929 + whatwg-url: 5.0.0 1930 + 1931 + node-gyp-build@4.8.4: {} 1932 + 1933 + nopt@8.1.0: 1934 + dependencies: 1935 + abbrev: 3.0.1 1936 + 1937 + obug@2.1.1: {} 1938 + 1939 + path-scurry@2.0.2: 1940 + dependencies: 1941 + lru-cache: 11.2.6 1942 + minipass: 7.1.3 1943 + 1944 + picocolors@1.1.1: {} 1945 + 1946 + picomatch@4.0.3: {} 1947 + 1948 + postcss@8.5.6: 1949 + dependencies: 1950 + nanoid: 3.3.11 1951 + picocolors: 1.1.1 1952 + source-map-js: 1.2.1 1953 + 1954 + prettier-plugin-svelte@3.5.0(prettier@3.8.1)(svelte@5.53.5): 1955 + dependencies: 1956 + prettier: 3.8.1 1957 + svelte: 5.53.5 1958 + 1959 + prettier-plugin-tailwindcss@0.7.2(prettier-plugin-svelte@3.5.0(prettier@3.8.1)(svelte@5.53.5))(prettier@3.8.1): 1960 + dependencies: 1961 + prettier: 3.8.1 1962 + optionalDependencies: 1963 + prettier-plugin-svelte: 3.5.0(prettier@3.8.1)(svelte@5.53.5) 1964 + 1965 + prettier@3.8.1: {} 1966 + 1967 + readdirp@4.1.2: {} 1968 + 1969 + resolve-from@5.0.0: {} 1970 + 1971 + rollup@4.59.0: 1972 + dependencies: 1973 + '@types/estree': 1.0.8 1974 + optionalDependencies: 1975 + '@rollup/rollup-android-arm-eabi': 4.59.0 1976 + '@rollup/rollup-android-arm64': 4.59.0 1977 + '@rollup/rollup-darwin-arm64': 4.59.0 1978 + '@rollup/rollup-darwin-x64': 4.59.0 1979 + '@rollup/rollup-freebsd-arm64': 4.59.0 1980 + '@rollup/rollup-freebsd-x64': 4.59.0 1981 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 1982 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 1983 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 1984 + '@rollup/rollup-linux-arm64-musl': 4.59.0 1985 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 1986 + '@rollup/rollup-linux-loong64-musl': 4.59.0 1987 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 1988 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 1989 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 1990 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 1991 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 1992 + '@rollup/rollup-linux-x64-gnu': 4.59.0 1993 + '@rollup/rollup-linux-x64-musl': 4.59.0 1994 + '@rollup/rollup-openbsd-x64': 4.59.0 1995 + '@rollup/rollup-openharmony-arm64': 4.59.0 1996 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 1997 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 1998 + '@rollup/rollup-win32-x64-gnu': 4.59.0 1999 + '@rollup/rollup-win32-x64-msvc': 4.59.0 2000 + fsevents: 2.3.3 2001 + 2002 + sade@1.8.1: 2003 + dependencies: 2004 + mri: 1.2.0 2005 + 2006 + semver@7.7.4: {} 2007 + 2008 + set-cookie-parser@3.0.1: {} 2009 + 2010 + sirv@3.0.2: 2011 + dependencies: 2012 + '@polka/url': 1.0.0-next.29 2013 + mrmime: 2.0.1 2014 + totalist: 3.0.1 2015 + 2016 + source-map-js@1.2.1: {} 2017 + 2018 + svelte-check@4.4.3(picomatch@4.0.3)(svelte@5.53.5)(typescript@5.9.3): 2019 + dependencies: 2020 + '@jridgewell/trace-mapping': 0.3.31 2021 + chokidar: 4.0.3 2022 + fdir: 6.5.0(picomatch@4.0.3) 2023 + picocolors: 1.1.1 2024 + sade: 1.8.1 2025 + svelte: 5.53.5 2026 + typescript: 5.9.3 2027 + transitivePeerDependencies: 2028 + - picomatch 2029 + 2030 + svelte@5.53.5: 2031 + dependencies: 2032 + '@jridgewell/remapping': 2.3.5 2033 + '@jridgewell/sourcemap-codec': 1.5.5 2034 + '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) 2035 + '@types/estree': 1.0.8 2036 + '@types/trusted-types': 2.0.7 2037 + acorn: 8.16.0 2038 + aria-query: 5.3.1 2039 + axobject-query: 4.1.0 2040 + clsx: 2.1.1 2041 + devalue: 5.6.3 2042 + esm-env: 1.2.2 2043 + esrap: 2.2.3 2044 + is-reference: 3.0.3 2045 + locate-character: 3.0.0 2046 + magic-string: 0.30.21 2047 + zimmerframe: 1.1.4 2048 + 2049 + tailwindcss@4.2.1: {} 2050 + 2051 + tapable@2.3.0: {} 2052 + 2053 + tar@7.5.9: 2054 + dependencies: 2055 + '@isaacs/fs-minipass': 4.0.1 2056 + chownr: 3.0.0 2057 + minipass: 7.1.3 2058 + minizlib: 3.1.0 2059 + yallist: 5.0.0 2060 + 2061 + tinyglobby@0.2.15: 2062 + dependencies: 2063 + fdir: 6.5.0(picomatch@4.0.3) 2064 + picomatch: 4.0.3 2065 + 2066 + tlds@1.261.0: {} 2067 + 2068 + totalist@3.0.1: {} 2069 + 2070 + tr46@0.0.3: {} 2071 + 2072 + tslib@2.8.1: {} 2073 + 2074 + typescript@5.9.3: {} 2075 + 2076 + uint8arrays@3.0.0: 2077 + dependencies: 2078 + multiformats: 9.9.0 2079 + 2080 + unicode-segmenter@0.14.5: {} 2081 + 2082 + vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1): 2083 + dependencies: 2084 + esbuild: 0.27.3 2085 + fdir: 6.5.0(picomatch@4.0.3) 2086 + picomatch: 4.0.3 2087 + postcss: 8.5.6 2088 + rollup: 4.59.0 2089 + tinyglobby: 0.2.15 2090 + optionalDependencies: 2091 + fsevents: 2.3.3 2092 + jiti: 2.6.1 2093 + lightningcss: 1.31.1 2094 + 2095 + vitefu@1.1.2(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)): 2096 + optionalDependencies: 2097 + vite: 7.3.1(jiti@2.6.1)(lightningcss@1.31.1) 2098 + 2099 + webidl-conversions@3.0.1: {} 2100 + 2101 + whatwg-url@5.0.0: 2102 + dependencies: 2103 + tr46: 0.0.3 2104 + webidl-conversions: 3.0.1 2105 + 2106 + yallist@5.0.0: {} 2107 + 2108 + zimmerframe@1.1.4: {} 2109 + 2110 + zod@3.25.76: {}
+2
packages/malachite-web/pnpm-workspace.yaml
··· 1 + onlyBuiltDependencies: 2 + - esbuild
+13
packages/malachite-web/src/app.d.ts
··· 1 + // See https://svelte.dev/docs/kit/types#app.d.ts 2 + // for information about these interfaces 3 + declare global { 4 + namespace App { 5 + // interface Error {} 6 + // interface Locals {} 7 + // interface PageData {} 8 + // interface PageState {} 9 + // interface Platform {} 10 + } 11 + } 12 + 13 + export {};
+19
packages/malachite-web/src/app.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="utf-8" /> 5 + <link rel="icon" href="%sveltekit.assets%/favicon.svg" /> 6 + <meta name="viewport" content="width=device-width, initial-scale=1" /> 7 + <meta name="theme-color" content="#090f0c" /> 8 + <meta name="description" content="Import your Last.fm and Spotify listening history into ATProto (Teal / Bluesky)." /> 9 + <meta property="og:title" content="Malachite" /> 10 + <meta property="og:description" content="Import your Last.fm and Spotify listening history into ATProto." /> 11 + <meta property="og:type" content="website" /> 12 + <meta property="og:url" content="https://malachite.ewancroft.uk" /> 13 + <title>Malachite</title> 14 + %sveltekit.head% 15 + </head> 16 + <body data-sveltekit-preload-data="hover"> 17 + <div style="display: contents">%sveltekit.body%</div> 18 + </body> 19 + </html>
+1
packages/malachite-web/src/lib/assets/favicon.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>
+7
packages/malachite-web/src/lib/config.ts
··· 1 + // Shared constants — mirrors src/config.ts without Node.js deps. 2 + 3 + export const RECORD_TYPE = 'fm.teal.alpha.feed.play'; 4 + export const CLIENT_AGENT = 'malachite/v0.9.3 (web)'; 5 + export const SLINGSHOT_RESOLVER = 'https://slingshot.microcosm.blue'; 6 + export const MAX_PDS_BATCH_SIZE = 200; 7 + export const POINTS_PER_RECORD = 3;
+43
packages/malachite-web/src/lib/core/auth.ts
··· 1 + /** 2 + * Browser-compatible ATProto authentication. 3 + * No CLI prompts — credentials come from the web form. 4 + */ 5 + 6 + import { AtpAgent } from '@atproto/api'; 7 + import { SLINGSHOT_RESOLVER } from '../config.js'; 8 + 9 + interface ResolvedIdentity { 10 + did: string; 11 + handle: string; 12 + pds: string; 13 + } 14 + 15 + export async function resolveIdentity(identifier: string): Promise<ResolvedIdentity> { 16 + const url = `${SLINGSHOT_RESOLVER}/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(identifier)}`; 17 + const res = await fetch(url); 18 + if (!res.ok) { 19 + throw new Error(`Failed to resolve identity: ${res.status} ${res.statusText}`); 20 + } 21 + const data = (await res.json()) as ResolvedIdentity; 22 + if (!data.did || !data.pds) { 23 + throw new Error('Invalid response from identity resolver'); 24 + } 25 + return data; 26 + } 27 + 28 + export async function login( 29 + identifier: string, 30 + password: string, 31 + pdsOverride?: string 32 + ): Promise<AtpAgent> { 33 + if (pdsOverride) { 34 + const agent = new AtpAgent({ service: pdsOverride }); 35 + await agent.login({ identifier, password }); 36 + return agent; 37 + } 38 + 39 + const identity = await resolveIdentity(identifier); 40 + const agent = new AtpAgent({ service: identity.pds }); 41 + await agent.login({ identifier: identity.did, password }); 42 + return agent; 43 + }
+144
packages/malachite-web/src/lib/core/csv.ts
··· 1 + /** 2 + * Browser-compatible Last.fm CSV parser. 3 + * 4 + * The parsing logic mirrors src/lib/csv.ts → parseLastFmCsvContent. 5 + * We avoid csv-parse (uses Node streams) and write a minimal parser instead. 6 + */ 7 + 8 + import type { LastFmCsvRecord, PlayRecord } from '../types.js'; 9 + import { RECORD_TYPE, CLIENT_AGENT } from '../config.js'; 10 + 11 + // ─── delimiter detection ─────────────────────────────────────────────────── 12 + 13 + function detectDelimiter(content: string): string { 14 + const firstLine = content.split('\n')[0]; 15 + const delimiters = [',', ';', '\t', '|']; 16 + let maxCount = 0; 17 + let best = ','; 18 + for (const d of delimiters) { 19 + const count = firstLine.split(d).length; 20 + if (count > maxCount) { maxCount = count; best = d; } 21 + } 22 + return best; 23 + } 24 + 25 + // ─── column normalisation ────────────────────────────────────────────────── 26 + 27 + const COLUMN_MAP: Record<string, string> = { 28 + uts: 'uts', date: 'uts', timestamp: 'uts', played_at: 'uts', time: 'uts', 29 + artist: 'artist', artist_name: 'artist', artistname: 'artist', 30 + artist_mbid: 'artist_mbid', artistmbid: 'artist_mbid', artist_id: 'artist_mbid', 31 + album: 'album', album_name: 'album', albumname: 'album', release: 'album', 32 + album_mbid: 'album_mbid', albummbid: 'album_mbid', albumid: 'album_mbid', album_id: 'album_mbid', 33 + track: 'track', track_name: 'track', trackname: 'track', song: 'track', title: 'track', 34 + track_mbid: 'track_mbid', trackmbid: 'track_mbid', track_id: 'track_mbid', 35 + utc_time: 'utc_time', utctime: 'utc_time', datetime: 'utc_time' 36 + }; 37 + 38 + function normalizeRecord(raw: Record<string, string>): LastFmCsvRecord { 39 + const normalized: Record<string, string> = {}; 40 + for (const [k, v] of Object.entries(raw)) { 41 + const mapped = COLUMN_MAP[k.toLowerCase()]; 42 + if (mapped) normalized[mapped] = v; 43 + } 44 + 45 + // Timestamp normalisation 46 + if (normalized.uts) { 47 + const ts = normalized.uts.toString(); 48 + if (ts.length >= 13) normalized.uts = Math.floor(parseInt(ts) / 1000).toString(); 49 + } 50 + if (normalized.uts && !normalized.utc_time) { 51 + normalized.utc_time = new Date(parseInt(normalized.uts) * 1000).toISOString(); 52 + } 53 + return normalized as unknown as LastFmCsvRecord; 54 + } 55 + 56 + // ─── minimal CSV parser ──────────────────────────────────────────────────── 57 + 58 + function parseCSV(content: string, delimiter: string): Record<string, string>[] { 59 + const lines = content.split(/\r?\n/).filter((l) => l.trim()); 60 + if (lines.length < 2) return []; 61 + 62 + const parseRow = (line: string): string[] => { 63 + const cells: string[] = []; 64 + let cur = ''; 65 + let inQuote = false; 66 + for (let i = 0; i < line.length; i++) { 67 + const ch = line[i]; 68 + if (ch === '"') { 69 + if (inQuote && line[i + 1] === '"') { cur += '"'; i++; } 70 + else inQuote = !inQuote; 71 + } else if (ch === delimiter && !inQuote) { 72 + cells.push(cur.trim()); cur = ''; 73 + } else { 74 + cur += ch; 75 + } 76 + } 77 + cells.push(cur.trim()); 78 + return cells; 79 + }; 80 + 81 + const headers = parseRow(lines[0]); 82 + const records: Record<string, string>[] = []; 83 + for (let i = 1; i < lines.length; i++) { 84 + const cells = parseRow(lines[i]); 85 + const record: Record<string, string> = {}; 86 + headers.forEach((h, idx) => { record[h] = cells[idx] ?? ''; }); 87 + records.push(record); 88 + } 89 + return records; 90 + } 91 + 92 + // ─── public API ─────────────────────────────────────────────────────────── 93 + 94 + export function parseLastFmCsvContent(rawContent: string): LastFmCsvRecord[] { 95 + let content = rawContent; 96 + // Strip BOM 97 + if (content.charCodeAt(0) === 0xfeff) content = content.slice(1); 98 + // Strip username comment from header 99 + const lines = content.split('\n'); 100 + lines[0] = lines[0].split('#')[0].trim(); 101 + content = lines.join('\n'); 102 + 103 + const delimiter = detectDelimiter(content); 104 + const raw = parseCSV(content, delimiter); 105 + const records = raw.map(normalizeRecord); 106 + return records.filter((r) => r.artist && r.track && r.uts); 107 + } 108 + 109 + export async function parseLastFmFile(file: File): Promise<LastFmCsvRecord[]> { 110 + const text = await file.text(); 111 + return parseLastFmCsvContent(text); 112 + } 113 + 114 + export function convertToPlayRecord(csv: LastFmCsvRecord): PlayRecord { 115 + const timestamp = parseInt(csv.uts); 116 + const playedTime = new Date(timestamp * 1000).toISOString(); 117 + 118 + const artists: PlayRecord['artists'] = []; 119 + if (csv.artist) { 120 + const a: PlayRecord['artists'][0] = { artistName: csv.artist }; 121 + if (csv.artist_mbid?.trim()) a.artistMbId = csv.artist_mbid; 122 + artists.push(a); 123 + } 124 + 125 + const record: PlayRecord = { 126 + $type: RECORD_TYPE, 127 + trackName: csv.track, 128 + artists, 129 + playedTime, 130 + submissionClientAgent: CLIENT_AGENT, 131 + musicServiceBaseDomain: 'last.fm', 132 + originUrl: '' 133 + }; 134 + 135 + if (csv.album?.trim()) record.releaseName = csv.album; 136 + if (csv.album_mbid?.trim()) record.releaseMbId = csv.album_mbid; 137 + if (csv.track_mbid?.trim()) record.recordingMbId = csv.track_mbid; 138 + 139 + const aEnc = encodeURIComponent(csv.artist); 140 + const tEnc = encodeURIComponent(csv.track); 141 + record.originUrl = `https://www.last.fm/music/${aEnc}/_/${tEnc}`; 142 + 143 + return record; 144 + }
+113
packages/malachite-web/src/lib/core/merge.ts
··· 1 + /** 2 + * Browser-compatible merge logic. 3 + * Mirrors src/lib/merge.ts without Node.js deps or file I/O. 4 + */ 5 + 6 + import type { PlayRecord } from '../types.js'; 7 + 8 + function normalizeString(s: string): string { 9 + return s.toLowerCase().replace(/[^\w\s]/g, '').replace(/\s+/g, ' ').trim(); 10 + } 11 + 12 + interface NormalizedRecord { 13 + original: PlayRecord; 14 + normalizedTrack: string; 15 + normalizedArtist: string; 16 + timestamp: number; 17 + source: 'lastfm' | 'spotify'; 18 + } 19 + 20 + function areDuplicates(a: NormalizedRecord, b: NormalizedRecord): boolean { 21 + return ( 22 + Math.abs(a.timestamp - b.timestamp) <= 300_000 && 23 + a.normalizedTrack === b.normalizedTrack && 24 + a.normalizedArtist === b.normalizedArtist 25 + ); 26 + } 27 + 28 + function betterRecord(a: NormalizedRecord, b: NormalizedRecord): PlayRecord { 29 + const hasMb = (n: NormalizedRecord) => 30 + n.source === 'lastfm' && 31 + (n.original.recordingMbId || n.original.releaseMbId || n.original.artists[0]?.artistMbId); 32 + if (hasMb(a) && !hasMb(b)) return a.original; 33 + if (hasMb(b) && !hasMb(a)) return b.original; 34 + return a.source === 'spotify' ? a.original : b.original; 35 + } 36 + 37 + export interface MergeStats { 38 + lastfmTotal: number; 39 + spotifyTotal: number; 40 + duplicatesRemoved: number; 41 + mergedTotal: number; 42 + } 43 + 44 + export function mergePlayRecords( 45 + lastfmRecords: PlayRecord[], 46 + spotifyRecords: PlayRecord[] 47 + ): { merged: PlayRecord[]; stats: MergeStats } { 48 + const toNorm = (r: PlayRecord, source: 'lastfm' | 'spotify'): NormalizedRecord => ({ 49 + original: r, 50 + normalizedTrack: normalizeString(r.trackName), 51 + normalizedArtist: normalizeString(r.artists[0]?.artistName ?? ''), 52 + timestamp: new Date(r.playedTime).getTime(), 53 + source 54 + }); 55 + 56 + const all = [ 57 + ...lastfmRecords.map((r) => toNorm(r, 'lastfm')), 58 + ...spotifyRecords.map((r) => toNorm(r, 'spotify')) 59 + ].sort((a, b) => a.timestamp - b.timestamp); 60 + 61 + const unique: PlayRecord[] = []; 62 + const seen = new Set<string>(); 63 + let dups = 0; 64 + 65 + for (const rec of all) { 66 + const key = `${rec.normalizedTrack}|${rec.normalizedArtist}|${Math.floor(rec.timestamp / 60_000)}`; 67 + if (seen.has(key)) { 68 + const idx = unique.findIndex((u) => { 69 + const n = toNorm(u, u.musicServiceBaseDomain === 'last.fm' ? 'lastfm' : 'spotify'); 70 + return areDuplicates(rec, n); 71 + }); 72 + if (idx !== -1) { 73 + const existing = toNorm(unique[idx], unique[idx].musicServiceBaseDomain === 'last.fm' ? 'lastfm' : 'spotify'); 74 + unique[idx] = betterRecord(existing, rec); 75 + dups++; 76 + continue; 77 + } 78 + } 79 + seen.add(key); 80 + unique.push(rec.original); 81 + } 82 + 83 + unique.sort((a, b) => new Date(a.playedTime).getTime() - new Date(b.playedTime).getTime()); 84 + 85 + return { 86 + merged: unique, 87 + stats: { 88 + lastfmTotal: lastfmRecords.length, 89 + spotifyTotal: spotifyRecords.length, 90 + duplicatesRemoved: dups, 91 + mergedTotal: unique.length 92 + } 93 + }; 94 + } 95 + 96 + /** Remove duplicate records within a single input set (keep first occurrence). */ 97 + export function deduplicateInputRecords(records: PlayRecord[]): { unique: PlayRecord[]; duplicates: number } { 98 + const seen = new Map<string, PlayRecord>(); 99 + let dups = 0; 100 + for (const r of records) { 101 + const key = `${(r.artists[0]?.artistName ?? '').toLowerCase()}|||${r.trackName.toLowerCase()}|||${r.playedTime}`; 102 + if (!seen.has(key)) seen.set(key, r); 103 + else dups++; 104 + } 105 + return { unique: Array.from(seen.values()), duplicates: dups }; 106 + } 107 + 108 + export function sortRecords(records: PlayRecord[], reverseChronological = false): PlayRecord[] { 109 + return [...records].sort((a, b) => { 110 + const diff = new Date(a.playedTime).getTime() - new Date(b.playedTime).getTime(); 111 + return reverseChronological ? -diff : diff; 112 + }); 113 + }
+168
packages/malachite-web/src/lib/core/publisher.ts
··· 1 + /** 2 + * Browser-compatible publisher. 3 + * Mirrors src/lib/publisher.ts without Node.js deps. 4 + * Uses in-memory rate limiting and progress callbacks instead of console.log. 5 + */ 6 + 7 + import type { AtpAgent } from '@atproto/api'; 8 + import type { PlayRecord } from '../types.js'; 9 + import { RECORD_TYPE, MAX_PDS_BATCH_SIZE, POINTS_PER_RECORD } from '../config.js'; 10 + import { BrowserRateLimiter } from './rate-limiter.js'; 11 + import { generateTIDFromISO } from './tid.js'; 12 + import { normalizeHeaders, isRateLimitError } from './rate-limit-headers.js'; 13 + 14 + export interface PublishProgress { 15 + batchIndex: number; 16 + totalBatches: number; 17 + recordsProcessed: number; 18 + totalRecords: number; 19 + successCount: number; 20 + errorCount: number; 21 + currentBatchSize: number; 22 + message: string; 23 + } 24 + 25 + export interface PublisherCallbacks { 26 + onProgress: (p: PublishProgress) => void; 27 + onLog: (level: 'info' | 'success' | 'warn' | 'error' | 'progress', msg: string) => void; 28 + isCancelled: () => boolean; 29 + } 30 + 31 + function normalizeResponseHeaders(response: any): Record<string, string> { 32 + const headers: Record<string, string> = {}; 33 + if (response?.headers) { 34 + if (typeof response.headers.forEach === 'function') { 35 + response.headers.forEach((v: string, k: string) => { headers[k] = v; }); 36 + } else { 37 + Object.assign(headers, response.headers); 38 + } 39 + } 40 + return headers; 41 + } 42 + 43 + export async function publishRecords( 44 + agent: AtpAgent, 45 + records: PlayRecord[], 46 + dryRun: boolean, 47 + callbacks: PublisherCallbacks 48 + ): Promise<{ successCount: number; errorCount: number; cancelled: boolean }> { 49 + const { onProgress, onLog, isCancelled } = callbacks; 50 + const total = records.length; 51 + 52 + if (dryRun) { 53 + onLog('info', `[DRY RUN] Would publish ${total} records`); 54 + const preview = records.slice(0, 5); 55 + preview.forEach((r, i) => { 56 + onLog('info', ` ${i + 1}. ${r.artists[0]?.artistName} – ${r.trackName} (${r.playedTime.slice(0, 10)})`); 57 + }); 58 + if (total > 5) onLog('info', ` …and ${total - 5} more`); 59 + return { successCount: total, errorCount: 0, cancelled: false }; 60 + } 61 + 62 + const rl = new BrowserRateLimiter({ headroom: 0.15 }); 63 + let currentBatchSize = 50; // probe batch 64 + let currentDelay = 500; 65 + let successCount = 0; 66 + let errorCount = 0; 67 + let batchCounter = 0; 68 + let i = 0; 69 + const startTime = Date.now(); 70 + 71 + onLog('info', `Publishing ${total.toLocaleString()} records to ATProto…`); 72 + onLog('warn', 'Do not close this tab during import.'); 73 + 74 + while (i < total) { 75 + if (isCancelled()) { 76 + onLog('warn', 'Import cancelled by user.'); 77 + return { successCount, errorCount, cancelled: true }; 78 + } 79 + 80 + const batch = records.slice(i, Math.min(i + currentBatchSize, total)); 81 + batchCounter++; 82 + const pct = ((i / total) * 100).toFixed(1); 83 + 84 + onProgress({ 85 + batchIndex: batchCounter, 86 + totalBatches: Math.ceil(total / currentBatchSize), 87 + recordsProcessed: i, 88 + totalRecords: total, 89 + successCount, 90 + errorCount, 91 + currentBatchSize: batch.length, 92 + message: `[${pct}%] Batch ${batchCounter} — records ${i + 1}–${Math.min(i + batch.length, total)}` 93 + }); 94 + 95 + const writes = await Promise.all( 96 + batch.map(async (record) => ({ 97 + $type: 'com.atproto.repo.applyWrites#create', 98 + collection: RECORD_TYPE, 99 + rkey: await generateTIDFromISO(record.playedTime, 'web:import'), 100 + value: record 101 + })) 102 + ); 103 + 104 + const batchPoints = batch.length * POINTS_PER_RECORD; 105 + await rl.waitForPermit(batchPoints); 106 + 107 + try { 108 + const response = await agent.com.atproto.repo.applyWrites({ 109 + repo: agent.session?.did ?? '', 110 + writes: writes as any 111 + }); 112 + 113 + successCount += response.data.results?.length ?? batch.length; 114 + 115 + // Learn rate limits from response headers 116 + const rawHeaders = normalizeResponseHeaders(response); 117 + if (Object.keys(rawHeaders).length > 0) { 118 + const norm = normalizeHeaders(rawHeaders); 119 + rl.updateFromHeaders(norm); 120 + 121 + // After first response, optimise batch size 122 + if (batchCounter === 1) { 123 + const cap = rl.getServerCapacity(); 124 + if (cap) { 125 + const remaining = rl.getActualRemaining(); 126 + const pointsPerSec = cap.limit / cap.windowSeconds; 127 + const recsPerSec = pointsPerSec / POINTS_PER_RECORD * 0.8; 128 + currentBatchSize = Math.min( 129 + MAX_PDS_BATCH_SIZE, 130 + Math.max(10, Math.floor(recsPerSec * 45)) 131 + ); 132 + currentDelay = Math.max(500, Math.floor((currentBatchSize / recsPerSec) * 1000)); 133 + onLog('info', `📊 Server: ${cap.limit} pts/${cap.windowSeconds}s — optimised to ${currentBatchSize} records/batch`); 134 + onLog('info', ` Remaining quota: ${remaining.toLocaleString()}/${cap.limit.toLocaleString()}`); 135 + } 136 + } 137 + } 138 + 139 + const elapsed = ((Date.now() - startTime) / 1000).toFixed(0); 140 + const rps = (successCount / ((Date.now() - startTime) / 1000)).toFixed(1); 141 + onLog('progress', `✓ Batch ${batchCounter} — ${successCount}/${total} records (${rps} rec/s, ${elapsed}s)`); 142 + 143 + i += batch.length; 144 + } catch (err: any) { 145 + if (isRateLimitError(err)) { 146 + onLog('warn', '⚠️ Rate limit hit — waiting for quota reset…'); 147 + // Extract headers from error if present 148 + const errHeaders = err?.response?.headers ?? err?.headers ?? {}; 149 + if (Object.keys(errHeaders).length > 0) { 150 + rl.updateFromHeaders(normalizeHeaders(errHeaders)); 151 + } 152 + await rl.waitForPermit(batchPoints); 153 + continue; // retry same batch 154 + } 155 + 156 + // Non-retryable error 157 + errorCount += batch.length; 158 + onLog('error', `✗ Batch ${batchCounter} failed: ${err.message ?? err}`); 159 + i += batch.length; 160 + } 161 + 162 + if (i < total) { 163 + await new Promise((r) => setTimeout(r, currentDelay)); 164 + } 165 + } 166 + 167 + return { successCount, errorCount, cancelled: false }; 168 + }
+47
packages/malachite-web/src/lib/core/rate-limit-headers.ts
··· 1 + /** 2 + * Minimal rate-limit header parsing — browser-safe. 3 + * Mirrors src/utils/rate-limit-headers.ts without the logger import. 4 + */ 5 + 6 + export interface RateLimitHeaders { 7 + limit?: number; 8 + remaining?: number; 9 + reset?: number; 10 + windowSeconds?: number; 11 + } 12 + 13 + export function normalizeHeaders(headers: Record<string, string>): Record<string, string> { 14 + const out: Record<string, string> = {}; 15 + for (const [k, v] of Object.entries(headers)) out[k.toLowerCase()] = v; 16 + return out; 17 + } 18 + 19 + export function parseRateLimitHeaders(headers: Record<string, string>): RateLimitHeaders { 20 + const h = normalizeHeaders(headers); 21 + const get = (k: string) => h[k] ?? h[`x-${k}`] ?? ''; 22 + 23 + const limit = parseInt(get('ratelimit-limit'), 10); 24 + const remaining = parseInt(get('ratelimit-remaining'), 10); 25 + const reset = parseInt(get('ratelimit-reset'), 10); 26 + const policy = get('ratelimit-policy'); 27 + 28 + let windowSeconds: number | undefined; 29 + const m = /;w=(\d+)/.exec(policy); 30 + if (m) windowSeconds = parseInt(m[1], 10); 31 + else if (!isNaN(reset)) { 32 + windowSeconds = Math.max(0, reset - Math.floor(Date.now() / 1000)); 33 + } 34 + 35 + return { 36 + limit: isNaN(limit) ? undefined : limit, 37 + remaining: isNaN(remaining) ? undefined : remaining, 38 + reset: isNaN(reset) ? undefined : reset, 39 + windowSeconds 40 + }; 41 + } 42 + 43 + export function isRateLimitError(err: any): boolean { 44 + if (err?.status === 429) return true; 45 + const msg = (err?.message ?? '').toLowerCase(); 46 + return msg.includes('rate limit') || msg.includes('too many requests') || msg.includes('ratelimit'); 47 + }
+86
packages/malachite-web/src/lib/core/rate-limiter.ts
··· 1 + /** 2 + * In-memory rate limiter for browser use. 3 + * Mirrors the interface of src/utils/rate-limiter.ts without any Node.js deps. 4 + */ 5 + 6 + interface State { 7 + limit: number; 8 + remaining: number; 9 + resetAt: number; // unix seconds 10 + windowSeconds: number; 11 + } 12 + 13 + export class BrowserRateLimiter { 14 + private state: State | null = null; 15 + private readonly headroom: number; 16 + 17 + constructor(opts?: { headroom?: number }) { 18 + this.headroom = opts?.headroom ?? 0.15; 19 + } 20 + 21 + updateFromHeaders(headers: Record<string, string>): void { 22 + const h = (k: string) => headers[k.toLowerCase()] ?? headers[k] ?? ''; 23 + const limitStr = h('ratelimit-limit') || h('x-ratelimit-limit'); 24 + const remainingStr = h('ratelimit-remaining') || h('x-ratelimit-remaining'); 25 + const resetStr = h('ratelimit-reset') || h('x-ratelimit-reset'); 26 + const policy = h('ratelimit-policy') || h('x-ratelimit-policy'); 27 + 28 + const limit = parseInt(limitStr, 10); 29 + const remaining = parseInt(remainingStr, 10); 30 + const reset = parseInt(resetStr, 10); 31 + 32 + if (!limit || isNaN(limit) || isNaN(remaining)) return; 33 + 34 + let windowSeconds = 3600; 35 + const m = /;w=(\d+)/.exec(policy); 36 + if (m) windowSeconds = parseInt(m[1], 10); 37 + 38 + const now = Math.floor(Date.now() / 1000); 39 + this.state = { 40 + limit, 41 + remaining, 42 + resetAt: isNaN(reset) ? now + windowSeconds : reset, 43 + windowSeconds 44 + }; 45 + } 46 + 47 + getActualRemaining(): number { 48 + if (!this.state) return 0; 49 + if (Math.floor(Date.now() / 1000) >= this.state.resetAt) return this.state.limit; 50 + return this.state.remaining; 51 + } 52 + 53 + getServerCapacity(): { limit: number; windowSeconds: number } | null { 54 + if (!this.state || this.state.limit === 0) return null; 55 + return { limit: this.state.limit, windowSeconds: this.state.windowSeconds }; 56 + } 57 + 58 + hasServerInfo(): boolean { 59 + return this.state !== null && this.state.limit > 0; 60 + } 61 + 62 + async waitForPermit(pointsNeeded: number): Promise<void> { 63 + if (!this.state) return; // no state yet — let first request probe 64 + 65 + const now = Math.floor(Date.now() / 1000); 66 + if (now >= this.state.resetAt) { 67 + this.state.remaining = this.state.limit; 68 + this.state.resetAt = now + this.state.windowSeconds; 69 + } 70 + 71 + const headroomPts = Math.floor(this.state.limit * this.headroom); 72 + const effective = this.state.remaining - headroomPts; 73 + 74 + if (effective < pointsNeeded) { 75 + // Wait until the window resets 76 + const waitMs = Math.max(0, (this.state.resetAt - Math.floor(Date.now() / 1000)) + 1) * 1000; 77 + await new Promise((r) => setTimeout(r, waitMs)); 78 + if (this.state) { 79 + this.state.remaining = this.state.limit; 80 + this.state.resetAt = Math.floor(Date.now() / 1000) + this.state.windowSeconds; 81 + } 82 + } 83 + 84 + if (this.state) this.state.remaining = Math.max(0, this.state.remaining - pointsNeeded); 85 + } 86 + }
+48
packages/malachite-web/src/lib/core/spotify.ts
··· 1 + /** 2 + * Browser-compatible Spotify JSON parser. 3 + * Mirrors src/lib/spotify.ts without any Node.js deps. 4 + */ 5 + 6 + import type { SpotifyRecord, PlayRecord } from '../types.js'; 7 + import { RECORD_TYPE, CLIENT_AGENT } from '../config.js'; 8 + 9 + export function parseSpotifyJsonContent(records: SpotifyRecord[]): SpotifyRecord[] { 10 + return records.filter( 11 + (r) => r.master_metadata_track_name && r.master_metadata_album_artist_name 12 + ); 13 + } 14 + 15 + export async function parseSpotifyFiles(files: File[]): Promise<SpotifyRecord[]> { 16 + let all: SpotifyRecord[] = []; 17 + for (const file of files) { 18 + const text = await file.text(); 19 + const parsed = JSON.parse(text) as SpotifyRecord[]; 20 + all = all.concat(parsed); 21 + } 22 + return parseSpotifyJsonContent(all); 23 + } 24 + 25 + export function convertSpotifyToPlayRecord(r: SpotifyRecord): PlayRecord { 26 + const artists: PlayRecord['artists'] = []; 27 + if (r.master_metadata_album_artist_name) { 28 + artists.push({ artistName: r.master_metadata_album_artist_name }); 29 + } 30 + 31 + const record: PlayRecord = { 32 + $type: RECORD_TYPE, 33 + trackName: r.master_metadata_track_name ?? 'Unknown Track', 34 + artists, 35 + playedTime: r.ts, 36 + submissionClientAgent: CLIENT_AGENT, 37 + musicServiceBaseDomain: 'spotify.com', 38 + originUrl: '' 39 + }; 40 + 41 + if (r.master_metadata_album_album_name) record.releaseName = r.master_metadata_album_album_name; 42 + if (r.spotify_track_uri) { 43 + const id = r.spotify_track_uri.replace('spotify:track:', ''); 44 + record.originUrl = `https://open.spotify.com/track/${id}`; 45 + } 46 + 47 + return record; 48 + }
+153
packages/malachite-web/src/lib/core/sync.ts
··· 1 + /** 2 + * Browser-compatible sync helpers. 3 + * Fetches existing records from ATProto and filters for new ones. 4 + */ 5 + 6 + import type { AtpAgent } from '@atproto/api'; 7 + import type { PlayRecord } from '../types.js'; 8 + import { RECORD_TYPE } from '../config.js'; 9 + 10 + export interface ExistingRecord { 11 + uri: string; 12 + cid: string; 13 + value: PlayRecord; 14 + } 15 + 16 + function recordKey(r: PlayRecord): string { 17 + const artist = (r.artists[0]?.artistName ?? '').toLowerCase().trim(); 18 + return `${artist}|||${r.trackName.toLowerCase().trim()}|||${r.playedTime}`; 19 + } 20 + 21 + /** In-session cache so repeat calls don't re-fetch. */ 22 + const sessionCache = new Map<string, Map<string, ExistingRecord>>(); 23 + 24 + export async function fetchExistingRecords( 25 + agent: AtpAgent, 26 + onProgress?: (fetched: number) => void, 27 + forceRefresh = false 28 + ): Promise<Map<string, ExistingRecord>> { 29 + const did = agent.session?.did; 30 + if (!did) throw new Error('No authenticated session'); 31 + 32 + if (!forceRefresh && sessionCache.has(did)) { 33 + return sessionCache.get(did)!; 34 + } 35 + 36 + const map = new Map<string, ExistingRecord>(); 37 + let cursor: string | undefined; 38 + let total = 0; 39 + let batchSize = 50; 40 + 41 + do { 42 + const res = await agent.com.atproto.repo.listRecords({ 43 + repo: did, 44 + collection: RECORD_TYPE, 45 + limit: batchSize, 46 + cursor 47 + }); 48 + 49 + for (const rec of res.data.records) { 50 + const value = rec.value as unknown as PlayRecord; 51 + const key = recordKey(value); 52 + map.set(key, { uri: rec.uri, cid: rec.cid, value }); 53 + } 54 + 55 + total += res.data.records.length; 56 + cursor = res.data.cursor; 57 + onProgress?.(total); 58 + 59 + // Simple adaptive sizing 60 + if (res.data.records.length === batchSize && batchSize < 100) { 61 + batchSize = Math.min(100, batchSize * 2); 62 + } 63 + } while (cursor); 64 + 65 + sessionCache.set(did, map); 66 + return map; 67 + } 68 + 69 + export function filterNewRecords( 70 + records: PlayRecord[], 71 + existing: Map<string, ExistingRecord> 72 + ): PlayRecord[] { 73 + return records.filter((r) => !existing.has(recordKey(r))); 74 + } 75 + 76 + export async function fetchAllRecordsForDedup( 77 + agent: AtpAgent, 78 + onProgress?: (fetched: number) => void 79 + ): Promise<ExistingRecord[]> { 80 + const did = agent.session?.did; 81 + if (!did) throw new Error('No authenticated session'); 82 + 83 + const all: ExistingRecord[] = []; 84 + let cursor: string | undefined; 85 + let batchSize = 50; 86 + 87 + do { 88 + const res = await agent.com.atproto.repo.listRecords({ 89 + repo: did, 90 + collection: RECORD_TYPE, 91 + limit: batchSize, 92 + cursor 93 + }); 94 + 95 + for (const rec of res.data.records) { 96 + const value = rec.value as unknown as PlayRecord; 97 + all.push({ uri: rec.uri, cid: rec.cid, value }); 98 + } 99 + 100 + cursor = res.data.cursor; 101 + onProgress?.(all.length); 102 + 103 + if (res.data.records.length === batchSize && batchSize < 100) { 104 + batchSize = Math.min(100, batchSize * 2); 105 + } 106 + } while (cursor); 107 + 108 + return all; 109 + } 110 + 111 + export interface DedupGroup { 112 + key: string; 113 + records: ExistingRecord[]; 114 + } 115 + 116 + export function findDuplicateGroups(records: ExistingRecord[]): DedupGroup[] { 117 + const groups = new Map<string, ExistingRecord[]>(); 118 + for (const rec of records) { 119 + const key = recordKey(rec.value); 120 + if (!groups.has(key)) groups.set(key, []); 121 + groups.get(key)!.push(rec); 122 + } 123 + const result: DedupGroup[] = []; 124 + for (const [key, recs] of groups) { 125 + if (recs.length > 1) result.push({ key, records: recs }); 126 + } 127 + return result; 128 + } 129 + 130 + export async function removeDuplicateRecords( 131 + agent: AtpAgent, 132 + groups: DedupGroup[], 133 + onProgress?: (removed: number) => void 134 + ): Promise<number> { 135 + let removed = 0; 136 + for (const group of groups) { 137 + for (const rec of group.records.slice(1)) { 138 + try { 139 + await agent.com.atproto.repo.deleteRecord({ 140 + repo: agent.session?.did ?? '', 141 + collection: RECORD_TYPE, 142 + rkey: rec.uri.split('/').pop()! 143 + }); 144 + removed++; 145 + onProgress?.(removed); 146 + await new Promise((r) => setTimeout(r, 100)); 147 + } catch { 148 + // continue on individual failures 149 + } 150 + } 151 + } 152 + return removed; 153 + }
+48
packages/malachite-web/src/lib/core/tid.ts
··· 1 + /** 2 + * Browser-compatible TID (Timestamp Identifier) generation for ATProto. 3 + * 4 + * Re-implements the monotonic TID clock from src/utils/tid-clock.ts without 5 + * any Node.js dependencies (no fs, no crypto module — uses crypto.getRandomValues). 6 + * 7 + * Spec: https://atproto.com/specs/tid 8 + */ 9 + 10 + // Base-32 alphabet used by AT Protocol (not standard base32) 11 + const S32_CHARS = '234567abcdefghijklmnopqrstuvwxyz'; 12 + 13 + function s32encode(n: number): string { 14 + if (n === 0) return '2'; 15 + let s = ''; 16 + let val = n; 17 + while (val > 0) { 18 + s = S32_CHARS[val % 32] + s; 19 + val = Math.floor(val / 32); 20 + } 21 + return s; 22 + } 23 + 24 + // In-memory monotonic state 25 + let lastTimestampUs = 0; 26 + const clockId = (() => { 27 + const buf = new Uint8Array(1); 28 + crypto.getRandomValues(buf); 29 + return buf[0] % 32; 30 + })(); 31 + 32 + export async function generateTIDFromISO(isoString: string, _context?: string): Promise<string> { 33 + let timestamp = new Date(isoString).getTime() * 1000; // ms → µs 34 + 35 + // Monotonicity: never go backwards 36 + if (timestamp <= lastTimestampUs) { 37 + timestamp = lastTimestampUs + 1; 38 + } 39 + lastTimestampUs = timestamp; 40 + 41 + const timestampStr = s32encode(timestamp).padStart(11, '2'); 42 + const clockIdStr = s32encode(clockId).padStart(2, '2'); 43 + return timestampStr + clockIdStr; 44 + } 45 + 46 + export function resetTidClock(): void { 47 + lastTimestampUs = 0; 48 + }
+4
packages/malachite-web/src/lib/index.ts
··· 1 + // Public re-exports — import from '$lib' instead of deep paths. 2 + export type { ImportMode, LogEntry, LogLevel, PlayRecord } from './types.js'; 3 + export { MODES, modeNeeds, stepLabelsFor } from './modes.js'; 4 + export type { ModeConfig, IconComponent } from './modes.js';
+36
packages/malachite-web/src/lib/modes.ts
··· 1 + import { Music2, Disc3, Layers2, RefreshCw, ListFilter } from '@lucide/svelte'; 2 + import type { ImportMode } from '$lib/types.js'; 3 + import type { Component } from 'svelte'; 4 + 5 + export type IconComponent = Component<{ size?: number; strokeWidth?: number; color?: string }>; 6 + 7 + export interface ModeConfig { 8 + id: ImportMode; 9 + icon: IconComponent; 10 + title: string; 11 + description: string; 12 + } 13 + 14 + export const MODES: ModeConfig[] = [ 15 + { id: 'lastfm', icon: Music2 as IconComponent, title: 'Last.fm', description: 'Import scrobble history from a Last.fm CSV export' }, 16 + { id: 'spotify', icon: Disc3 as IconComponent, title: 'Spotify', description: 'Import play history from a Spotify JSON export' }, 17 + { id: 'combined', icon: Layers2 as IconComponent, title: 'Combined', description: 'Merge Last.fm + Spotify with smart deduplication' }, 18 + { id: 'sync', icon: RefreshCw as IconComponent, title: 'Sync', description: 'Only import records not already in Teal' }, 19 + { id: 'deduplicate', icon: ListFilter as IconComponent, title: 'Deduplicate', description: 'Find and remove duplicate records from Teal' }, 20 + ]; 21 + 22 + /** Which file sources does a given mode require? */ 23 + export function modeNeeds(mode: ImportMode | null) { 24 + return { 25 + lastfm: mode === 'lastfm' || mode === 'combined' || mode === 'sync', 26 + spotify: mode === 'spotify' || mode === 'combined', 27 + files: mode !== 'deduplicate', 28 + }; 29 + } 30 + 31 + /** Wizard step labels for a given mode. */ 32 + export function stepLabelsFor(mode: ImportMode | null): string[] { 33 + return mode === 'deduplicate' 34 + ? ['Mode', 'Sign in', 'Options', 'Run'] 35 + : ['Mode', 'Sign in', 'Files', 'Options', 'Run']; 36 + }
+68
packages/malachite-web/src/lib/types.ts
··· 1 + // Shared type definitions for the Malachite web app. 2 + // These mirror src/types.ts but are free of Node.js dependencies. 3 + 4 + export interface LastFmCsvRecord { 5 + uts: string; 6 + utc_time: string; 7 + artist: string; 8 + artist_mbid?: string; 9 + album: string; 10 + album_mbid?: string; 11 + track: string; 12 + track_mbid?: string; 13 + } 14 + 15 + export interface PlayRecordArtist { 16 + artistName: string; 17 + artistMbId?: string; 18 + } 19 + 20 + export interface PlayRecord { 21 + $type: string; 22 + trackName: string; 23 + artists: PlayRecordArtist[]; 24 + playedTime: string; 25 + submissionClientAgent: string; 26 + musicServiceBaseDomain: string; 27 + releaseName?: string; 28 + releaseMbId?: string; 29 + recordingMbId?: string; 30 + originUrl: string; 31 + } 32 + 33 + export interface PublishResult { 34 + successCount: number; 35 + errorCount: number; 36 + cancelled: boolean; 37 + } 38 + 39 + export type ImportMode = 'lastfm' | 'spotify' | 'combined' | 'sync' | 'deduplicate'; 40 + 41 + export interface SpotifyRecord { 42 + ts: string; 43 + platform: string; 44 + ms_played: number; 45 + conn_country: string; 46 + master_metadata_track_name: string | null; 47 + master_metadata_album_artist_name: string | null; 48 + master_metadata_album_album_name: string | null; 49 + spotify_track_uri: string | null; 50 + episode_name: string | null; 51 + episode_show_name: string | null; 52 + spotify_episode_uri: string | null; 53 + reason_start: string; 54 + reason_end: string; 55 + shuffle: boolean; 56 + skipped: boolean; 57 + offline: boolean; 58 + offline_timestamp: number | null; 59 + incognito_mode: boolean; 60 + } 61 + 62 + export type LogLevel = 'info' | 'success' | 'warn' | 'error' | 'progress' | 'section'; 63 + 64 + export interface LogEntry { 65 + level: LogLevel; 66 + message: string; 67 + timestamp: number; 68 + }
+22
packages/malachite-web/src/routes/+layout.svelte
··· 1 + <script lang="ts"> 2 + import './layout.css'; 3 + let { children } = $props(); 4 + </script> 5 + 6 + <svelte:head> 7 + <link 8 + rel="preconnect" 9 + href="https://fonts.googleapis.com" 10 + /> 11 + <link 12 + rel="preconnect" 13 + href="https://fonts.gstatic.com" 14 + crossorigin="anonymous" 15 + /> 16 + <link 17 + href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" 18 + rel="stylesheet" 19 + /> 20 + </svelte:head> 21 + 22 + {@render children()}
+632
packages/malachite-web/src/routes/+page.svelte
··· 1 + <script lang="ts"> 2 + import { fly, scale } from 'svelte/transition'; 3 + import { cubicOut, backOut } from 'svelte/easing'; 4 + import { Check, Eye, EyeOff, Music2, Disc3, Layers2, RefreshCw, ListFilter, CheckCircle2 } from '@lucide/svelte'; 5 + 6 + import { parseLastFmFile, convertToPlayRecord } from '$lib/core/csv.js'; 7 + import { parseSpotifyFiles, convertSpotifyToPlayRecord } from '$lib/core/spotify.js'; 8 + import { mergePlayRecords, deduplicateInputRecords, sortRecords } from '$lib/core/merge.js'; 9 + import { fetchExistingRecords, filterNewRecords, fetchAllRecordsForDedup, findDuplicateGroups, removeDuplicateRecords } from '$lib/core/sync.js'; 10 + import { publishRecords, type PublishProgress } from '$lib/core/publisher.js'; 11 + import { login } from '$lib/core/auth.js'; 12 + import type { AtpAgent } from '@atproto/api'; 13 + import { modeNeeds, MODES } from '$lib'; 14 + import type { ImportMode, LogEntry, PlayRecord } from '$lib'; 15 + 16 + // ─── wizard state ──────────────────────────────────────────────────────────── 17 + 18 + let step = $state(0); 19 + let prevStep = $state(0); 20 + let mode = $state<ImportMode | null>(null); 21 + 22 + let handle = $state(''); 23 + let password = $state(''); 24 + let pdsOverride = $state(''); 25 + let agent = $state<AtpAgent | null>(null); 26 + 27 + let lastfmFiles = $state<File[]>([]); 28 + let spotifyFiles = $state<File[]>([]); 29 + 30 + let dryRun = $state(false); 31 + let reverseOrder = $state(false); 32 + let fresh = $state(false); 33 + 34 + let isRunning = $state(false); 35 + let cancelled = $state(false); 36 + let logs = $state<LogEntry[]>([]); 37 + let progress = $state<PublishProgress | null>(null); 38 + let result = $state<{ success: number; errors: number; cancelled: boolean } | null>(null); 39 + let importError = $state<string | null>(null); 40 + 41 + // ─── derived ────────────────────────────────────────────────────────────────── 42 + 43 + let needs = $derived(modeNeeds(mode)); 44 + 45 + let stepLabels = $derived( 46 + mode === 'deduplicate' 47 + ? ['Mode', 'Sign in', 'Options', 'Run'] 48 + : ['Mode', 'Sign in', 'Files', 'Options', 'Run'] 49 + ); 50 + 51 + let goingForward = $derived(step >= prevStep); 52 + 53 + // ─── nav ────────────────────────────────────────────────────────────────────── 54 + 55 + function goTo(n: number) { prevStep = step; step = n; } 56 + 57 + function selectMode(m: ImportMode) { mode = m; goTo(1); } 58 + 59 + function goBack() { 60 + if (step === 3 && mode === 'deduplicate') { goTo(1); return; } 61 + goTo(Math.max(0, step - 1)); 62 + } 63 + 64 + function afterAuth(a: AtpAgent) { 65 + agent = a; 66 + goTo(needs.files ? 2 : 3); 67 + } 68 + 69 + // ─── auth state ─────────────────────────────────────────────────────────────── 70 + 71 + let showPassword = $state(false); 72 + let showAdvanced = $state(false); 73 + let authError = $state<string | null>(null); 74 + let authLoading = $state(false); 75 + 76 + async function doAuth() { 77 + authError = null; 78 + authLoading = true; 79 + try { 80 + afterAuth(await login(handle.trim(), password, pdsOverride.trim() || undefined)); 81 + } catch (err: any) { 82 + authError = err.message ?? 'Login failed'; 83 + } finally { 84 + authLoading = false; 85 + } 86 + } 87 + 88 + // ─── files state ────────────────────────────────────────────────────────────── 89 + 90 + let lfDragging = $state(false); 91 + let spDragging = $state(false); 92 + 93 + function handleDrop(e: DragEvent, type: 'lf' | 'sp') { 94 + e.preventDefault(); 95 + if (type === 'lf') { lfDragging = false; lastfmFiles = Array.from(e.dataTransfer?.files ?? []).filter(f => f.name.endsWith('.csv')); } 96 + else { spDragging = false; spotifyFiles = Array.from(e.dataTransfer?.files ?? []).filter(f => f.name.endsWith('.json')); } 97 + } 98 + 99 + let canContinue = $derived( 100 + (needs.lastfm && needs.spotify && lastfmFiles.length > 0 && spotifyFiles.length > 0) || 101 + (needs.lastfm && !needs.spotify && lastfmFiles.length > 0) || 102 + (!needs.lastfm && needs.spotify && spotifyFiles.length > 0) 103 + ); 104 + 105 + // ─── log ────────────────────────────────────────────────────────────────────── 106 + 107 + let logEl = $state<HTMLElement | null>(null); 108 + $effect(() => { if (logs.length > 0 && logEl) logEl.scrollTop = logEl.scrollHeight; }); 109 + 110 + function addLog(level: LogEntry['level'], message: string) { 111 + logs = [...logs, { level, message, timestamp: Date.now() }]; 112 + } 113 + 114 + const LOG_COLORS: Record<string, string> = { 115 + info: 'var(--text)', success: 'var(--accent)', warn: 'var(--warn)', 116 + error: 'var(--error)', progress: 'var(--muted)', section: 'var(--accent)' 117 + }; 118 + 119 + let runTitle = $derived( 120 + result 121 + ? (result.cancelled ? 'Import stopped' : 'Import complete') 122 + : isRunning ? (dryRun ? 'Previewing…' : 'Importing…') 123 + : importError ? 'Something went wrong' : 'Starting…' 124 + ); 125 + 126 + let pct = $derived( 127 + progress && progress.totalRecords > 0 128 + ? (progress.recordsProcessed / progress.totalRecords * 100).toFixed(1) 129 + : '0' 130 + ); 131 + 132 + // ─── import ─────────────────────────────────────────────────────────────────── 133 + 134 + async function startImport() { 135 + if (!agent || !mode) return; 136 + isRunning = true; cancelled = false; logs = []; importError = null; result = null; 137 + goTo(4); 138 + try { 139 + if (mode === 'deduplicate') { 140 + addLog('section', '── Deduplication ──────────────────────────────────'); 141 + addLog('info', 'Fetching existing records from Teal…'); 142 + const all = await fetchAllRecordsForDedup(agent, n => addLog('progress', ` Fetched ${n.toLocaleString()} records…`)); 143 + addLog('success', `Fetched ${all.length.toLocaleString()} records`); 144 + const groups = findDuplicateGroups(all); 145 + const totalDups = groups.reduce((s, g) => s + g.records.length - 1, 0); 146 + if (totalDups === 0) { addLog('success', 'No duplicates found — your records are clean.'); result = { success: 0, errors: 0, cancelled: false }; return; } 147 + addLog('warn', `Found ${totalDups.toLocaleString()} duplicate(s) across ${groups.length} groups`); 148 + if (dryRun) { addLog('info', `[DRY RUN] Would remove ${totalDups} duplicate record(s).`); result = { success: totalDups, errors: 0, cancelled: false }; return; } 149 + addLog('info', 'Removing duplicates…'); 150 + const removed = await removeDuplicateRecords(agent, groups, n => { if (!cancelled) addLog('progress', ` Removed ${n}/${totalDups}…`); }); 151 + addLog('success', `Removed ${removed.toLocaleString()} duplicate(s)`); 152 + result = { success: removed, errors: 0, cancelled: false }; 153 + return; 154 + } 155 + 156 + addLog('section', '── Loading Records ─────────────────────────────────'); 157 + let records: PlayRecord[] = []; 158 + if (mode === 'combined') { 159 + const lfRaw = await parseLastFmFile(lastfmFiles[0]); 160 + addLog('info', `Last.fm: ${lfRaw.length.toLocaleString()} scrobbles`); 161 + const spRaw = await parseSpotifyFiles(spotifyFiles); 162 + addLog('info', `Spotify: ${spRaw.length.toLocaleString()} tracks`); 163 + const { merged, stats } = mergePlayRecords(lfRaw.map(r => convertToPlayRecord(r)), spRaw.map(r => convertSpotifyToPlayRecord(r))); 164 + records = merged; 165 + addLog('success', `Merged: ${stats.mergedTotal.toLocaleString()} unique records (${stats.duplicatesRemoved} removed)`); 166 + } else if (mode === 'spotify') { 167 + const spRaw = await parseSpotifyFiles(spotifyFiles); 168 + records = spRaw.map(r => convertSpotifyToPlayRecord(r)); 169 + addLog('success', `Loaded ${records.length.toLocaleString()} Spotify records`); 170 + } else { 171 + const lfRaw = await parseLastFmFile(lastfmFiles[0]); 172 + records = lfRaw.map(r => convertToPlayRecord(r)); 173 + addLog('success', `Loaded ${records.length.toLocaleString()} Last.fm records`); 174 + } 175 + 176 + const { unique, duplicates: inputDups } = deduplicateInputRecords(records); 177 + records = unique; 178 + if (inputDups > 0) addLog('warn', `Removed ${inputDups.toLocaleString()} duplicate(s) from input`); 179 + 180 + addLog('section', '── Sync Check ───────────────────────────────────────'); 181 + addLog('info', 'Fetching existing records from Teal…'); 182 + const existing = await fetchExistingRecords(agent, n => addLog('progress', ` Fetched ${n.toLocaleString()} existing records…`), fresh); 183 + const before = records.length; 184 + records = filterNewRecords(records, existing); 185 + const skipped = before - records.length; 186 + if (skipped > 0) addLog('info', `Skipped ${skipped.toLocaleString()} already-imported record(s)`); 187 + addLog('success', `${records.length.toLocaleString()} new record(s) to import`); 188 + 189 + if (records.length === 0) { addLog('success', '✓ Nothing to import — all records already exist in Teal!'); result = { success: 0, errors: 0, cancelled: false }; return; } 190 + if (mode !== 'combined') records = sortRecords(records, reverseOrder); 191 + 192 + addLog('section', '── Publishing ───────────────────────────────────────'); 193 + const res = await publishRecords(agent, records, dryRun, { 194 + onProgress: p => { progress = p; }, 195 + onLog: (level, msg) => addLog(level as LogEntry['level'], msg), 196 + isCancelled: () => cancelled 197 + }); 198 + result = { success: res.successCount, errors: res.errorCount, cancelled: res.cancelled }; 199 + if (res.cancelled) addLog('warn', `Stopped. ${res.successCount.toLocaleString()} records published.`); 200 + else if (dryRun) addLog('success', `Dry run complete — ${res.successCount.toLocaleString()} records would be imported.`); 201 + else { addLog('success', `Import complete! ${res.successCount.toLocaleString()} records published.`); if (res.errorCount > 0) addLog('warn', `${res.errorCount} record(s) failed.`); } 202 + } catch (err: any) { 203 + importError = err.message ?? 'An unexpected error occurred'; 204 + addLog('error', `Fatal: ${importError}`); 205 + } finally { 206 + isRunning = false; 207 + } 208 + } 209 + 210 + // ─── reset ──────────────────────────────────────────────────────────────────── 211 + 212 + function reset() { 213 + prevStep = step; step = 0; mode = null; agent = null; 214 + lastfmFiles = []; spotifyFiles = []; 215 + dryRun = false; reverseOrder = false; fresh = false; 216 + logs = []; progress = null; result = null; importError = null; cancelled = false; 217 + } 218 + </script> 219 + 220 + <svelte:head> 221 + <title>Malachite — Import to Teal</title> 222 + </svelte:head> 223 + 224 + <main> 225 + <header> 226 + <p class="logo-text">Malachite</p> 227 + <p class="tagline">Import Last.fm &amp; Spotify history to ATProto / Teal</p> 228 + </header> 229 + 230 + <!-- Step indicator --> 231 + {#if step > 0 && step < 5} 232 + <nav class="steps" aria-label="Progress"> 233 + {#each stepLabels as label, i} 234 + <div class="step-item" class:done={i < step} class:active={i === step}> 235 + <span class="step-dot"> 236 + {#key i < step} 237 + <span 238 + class="dot-inner" 239 + in:scale={{ duration: 250, start: 0.4, easing: backOut }} 240 + > 241 + {#if i < step}<Check size={12} strokeWidth={3} />{:else}{i + 1}{/if} 242 + </span> 243 + {/key} 244 + </span> 245 + <span class="step-label">{label}</span> 246 + </div> 247 + {#if i < stepLabels.length - 1} 248 + <div class="step-line" class:done={i < step}></div> 249 + {/if} 250 + {/each} 251 + </nav> 252 + {/if} 253 + 254 + <!-- Step content --> 255 + <div class="step-viewport"> 256 + {#key step} 257 + <div 258 + class="step-slide" 259 + in:fly={{ x: goingForward ? 40 : -40, duration: 280, easing: cubicOut }} 260 + out:fly={{ x: goingForward ? -40 : 40, duration: 200, easing: cubicOut }} 261 + > 262 + 263 + <!-- ── Step 0: Mode ─────────────────────────────────────────────────── --> 264 + {#if step === 0} 265 + <section class="card-section"> 266 + <h2 class="section-title">What would you like to do?</h2> 267 + <div class="mode-grid"> 268 + {#each MODES as m} 269 + <button class="mode-card" onclick={() => selectMode(m.id)}> 270 + <span class="mode-icon"><m.icon size={20} /></span> 271 + <span class="mode-title">{m.title}</span> 272 + <span class="mode-desc">{m.description}</span> 273 + </button> 274 + {/each} 275 + </div> 276 + <p class="footer-note"> 277 + <a href="https://github.com/ewanc26/malachite" target="_blank" rel="noopener">↗ View on GitHub</a> 278 + &nbsp;·&nbsp; 279 + <a href="https://ko-fi.com/ewancroft" target="_blank" rel="noopener">♥ Support Malachite</a> 280 + </p> 281 + </section> 282 + 283 + <!-- ── Step 1: Auth ─────────────────────────────────────────────────── --> 284 + {:else if step === 1} 285 + <section class="card-section"> 286 + <button class="back-btn" onclick={goBack}>← Back</button> 287 + <h2 class="section-title">Sign in to ATProto</h2> 288 + <p class="section-sub"> 289 + Use your Bluesky handle and an 290 + <a href="https://bsky.app/settings/app-passwords" target="_blank" rel="noopener">app password</a>. 291 + </p> 292 + <div class="form"> 293 + <label class="field"> 294 + <span class="field-label">Handle</span> 295 + <input type="text" bind:value={handle} placeholder="you.bsky.social" autocomplete="username" spellcheck="false" /> 296 + </label> 297 + <label class="field"> 298 + <span class="field-label">App password</span> 299 + <div class="password-wrap"> 300 + <input type={showPassword ? 'text' : 'password'} bind:value={password} placeholder="xxxx-xxxx-xxxx-xxxx" autocomplete="current-password" /> 301 + <button class="pw-toggle" onclick={() => (showPassword = !showPassword)} type="button" aria-label={showPassword ? 'Hide password' : 'Show password'}> 302 + {#if showPassword}<EyeOff size={14} />{:else}<Eye size={14} />{/if} 303 + </button> 304 + </div> 305 + </label> 306 + <button class="expand-btn" onclick={() => (showAdvanced = !showAdvanced)} type="button"> 307 + {showAdvanced ? '▾' : '▸'} Advanced options 308 + </button> 309 + {#if showAdvanced} 310 + <label class="field"> 311 + <span class="field-label">PDS URL override <span class="badge">optional</span></span> 312 + <input type="url" bind:value={pdsOverride} placeholder="https://your.pds.example" spellcheck="false" /> 313 + <span class="field-hint">Skip Slingshot identity resolution and connect directly to your PDS.</span> 314 + </label> 315 + {/if} 316 + {#if authError} 317 + <div class="alert alert-error">{authError}</div> 318 + {/if} 319 + <button class="btn-primary" onclick={doAuth} disabled={authLoading || !handle || !password}> 320 + {#if authLoading}<span class="spinner"></span> Signing in…{:else}Sign in →{/if} 321 + </button> 322 + </div> 323 + </section> 324 + 325 + <!-- ── Step 2: Files ────────────────────────────────────────────────── --> 326 + {:else if step === 2} 327 + <section class="card-section"> 328 + <button class="back-btn" onclick={goBack}>← Back</button> 329 + <h2 class="section-title">Upload your export{needs.lastfm && needs.spotify ? 's' : ''}</h2> 330 + <div class="drop-zones"> 331 + {#if needs.lastfm} 332 + <div 333 + class="drop-zone" class:dragging={lfDragging} class:filled={lastfmFiles.length > 0} 334 + role="button" tabindex="0" aria-label="Upload Last.fm CSV file" 335 + ondragover={e => { e.preventDefault(); lfDragging = true; }} 336 + ondragleave={() => (lfDragging = false)} 337 + ondrop={e => handleDrop(e, 'lf')} 338 + onclick={() => document.getElementById('lfInput')?.click()} 339 + onkeydown={e => e.key === 'Enter' && document.getElementById('lfInput')?.click()} 340 + > 341 + <input id="lfInput" type="file" accept=".csv" hidden onchange={e => { lastfmFiles = Array.from((e.target as HTMLInputElement).files ?? []); }} /> 342 + {#if lastfmFiles.length > 0} 343 + <span class="drop-icon drop-done"><CheckCircle2 size={28} /></span> 344 + <span class="drop-filename">{lastfmFiles[0].name}</span> 345 + <span class="drop-meta">{(lastfmFiles[0].size / 1024).toFixed(0)} KB · CSV</span> 346 + {:else} 347 + <span class="drop-icon"><Music2 size={28} /></span> 348 + <span class="drop-title">Last.fm CSV</span> 349 + <span class="drop-hint">Drag & drop or click to select</span> 350 + {/if} 351 + </div> 352 + {/if} 353 + {#if needs.spotify} 354 + <div 355 + class="drop-zone" class:dragging={spDragging} class:filled={spotifyFiles.length > 0} 356 + role="button" tabindex="0" aria-label="Upload Spotify JSON files" 357 + ondragover={e => { e.preventDefault(); spDragging = true; }} 358 + ondragleave={() => (spDragging = false)} 359 + ondrop={e => handleDrop(e, 'sp')} 360 + onclick={() => document.getElementById('spInput')?.click()} 361 + onkeydown={e => e.key === 'Enter' && document.getElementById('spInput')?.click()} 362 + > 363 + <input id="spInput" type="file" accept=".json" multiple hidden onchange={e => { spotifyFiles = Array.from((e.target as HTMLInputElement).files ?? []); }} /> 364 + {#if spotifyFiles.length > 0} 365 + <span class="drop-icon drop-done"><CheckCircle2 size={28} /></span> 366 + <span class="drop-filename">{spotifyFiles.length === 1 ? spotifyFiles[0].name : `${spotifyFiles.length} files selected`}</span> 367 + <span class="drop-meta">{spotifyFiles.length === 1 ? `${(spotifyFiles[0].size / 1024).toFixed(0)} KB · JSON` : 'JSON · Streaming_History_Audio_*'}</span> 368 + {:else} 369 + <span class="drop-icon"><Disc3 size={28} /></span> 370 + <span class="drop-title">Spotify JSON</span> 371 + <span class="drop-hint">Select one or more JSON files</span> 372 + {/if} 373 + </div> 374 + {/if} 375 + </div> 376 + <div class="how-to"> 377 + {#if needs.lastfm} 378 + <details> 379 + <summary>How to export from Last.fm</summary> 380 + <p>Go to <a href="https://www.last.fm/settings/dataexporter" target="_blank" rel="noopener">last.fm/settings/dataexporter</a>, request a data export, and download the CSV once ready.</p> 381 + </details> 382 + {/if} 383 + {#if needs.spotify} 384 + <details> 385 + <summary>How to export from Spotify</summary> 386 + <p>Go to <a href="https://www.spotify.com/account/privacy" target="_blank" rel="noopener">spotify.com/account/privacy</a>, request "Extended streaming history", and upload all <code>Streaming_History_Audio_*.json</code> files.</p> 387 + </details> 388 + {/if} 389 + </div> 390 + <button class="btn-primary" onclick={() => goTo(3)} disabled={!canContinue}>Continue →</button> 391 + </section> 392 + 393 + <!-- ── Step 3: Options ──────────────────────────────────────────────── --> 394 + {:else if step === 3} 395 + <section class="card-section"> 396 + <button class="back-btn" onclick={goBack}>← Back</button> 397 + <h2 class="section-title">Import options</h2> 398 + <div class="options"> 399 + <div class="option-row"> 400 + <div class="option-info"> 401 + <span class="option-name">Dry run</span> 402 + <span class="option-desc">Preview what would be imported without making changes</span> 403 + </div> 404 + <button class="toggle" class:on={dryRun} onclick={() => (dryRun = !dryRun)} type="button" aria-label="Toggle dry run" aria-pressed={dryRun}> 405 + <span class="toggle-thumb"></span> 406 + </button> 407 + </div> 408 + {#if mode !== 'deduplicate'} 409 + <div class="option-row"> 410 + <div class="option-info"> 411 + <span class="option-name">Reverse order</span> 412 + <span class="option-desc">Process newest records first (default: oldest first)</span> 413 + </div> 414 + <button class="toggle" class:on={reverseOrder} onclick={() => (reverseOrder = !reverseOrder)} type="button" aria-label="Toggle reverse order" aria-pressed={reverseOrder}> 415 + <span class="toggle-thumb"></span> 416 + </button> 417 + </div> 418 + <div class="option-row"> 419 + <div class="option-info"> 420 + <span class="option-name">Fresh start</span> 421 + <span class="option-desc">Re-fetch existing records instead of using the session cache</span> 422 + </div> 423 + <button class="toggle" class:on={fresh} onclick={() => (fresh = !fresh)} type="button" aria-label="Toggle fresh start" aria-pressed={fresh}> 424 + <span class="toggle-thumb"></span> 425 + </button> 426 + </div> 427 + {/if} 428 + </div> 429 + {#if dryRun} 430 + <div class="alert alert-info">Dry run enabled — no records will be written to Teal.</div> 431 + {/if} 432 + <button class="btn-primary" onclick={startImport}>{dryRun ? 'Preview import →' : 'Start import →'}</button> 433 + </section> 434 + 435 + <!-- ── Step 4: Run ──────────────────────────────────────────────────── --> 436 + {:else if step === 4} 437 + <section class="card-section run-section"> 438 + <div class="run-header"> 439 + <h2 class="section-title">{runTitle}</h2> 440 + {#if isRunning && !result} 441 + <button class="btn-cancel" onclick={() => (cancelled = true)}>Stop</button> 442 + {/if} 443 + </div> 444 + {#if progress && !result} 445 + <div class="progress-wrap"> 446 + <div class="progress-bar" style="width: {pct}%"></div> 447 + </div> 448 + <p class="progress-text"> 449 + {progress.recordsProcessed.toLocaleString()} / {progress.totalRecords.toLocaleString()} records 450 + &nbsp;·&nbsp; batch {progress.batchIndex} · {progress.currentBatchSize} per batch 451 + </p> 452 + {/if} 453 + <div class="log-terminal" bind:this={logEl}> 454 + {#each logs as entry} 455 + <div class="log-line" style="color: {LOG_COLORS[entry.level] ?? 'var(--text)'}"> 456 + {#if entry.level === 'section'} 457 + <span class="log-section">{entry.message}</span> 458 + {:else} 459 + <span class="log-ts">{new Date(entry.timestamp).toTimeString().slice(0, 8)}</span> 460 + <span>{entry.message}</span> 461 + {/if} 462 + </div> 463 + {/each} 464 + {#if isRunning && !result && logs.length === 0} 465 + <div class="log-line" style="color: var(--muted)">Initialising…</div> 466 + {/if} 467 + </div> 468 + {#if result} 469 + <div class="result-card" class:success={!result.cancelled && !importError}> 470 + {#if importError} 471 + <p class="result-label error">Error</p> 472 + <p class="result-detail">{importError}</p> 473 + {:else if result.cancelled} 474 + <p class="result-label warn">Stopped</p> 475 + <p class="result-detail">{result.success.toLocaleString()} record(s) published before stopping.</p> 476 + {:else} 477 + <p class="result-label success">{dryRun ? 'Preview complete' : '✓ Done'}</p> 478 + <p class="result-detail"> 479 + {#if dryRun}{result.success.toLocaleString()} record(s) would be imported. 480 + {:else}{result.success.toLocaleString()} record(s) published successfully.{#if result.errors > 0}&nbsp;{result.errors} failed.{/if} 481 + {/if} 482 + </p> 483 + {/if} 484 + </div> 485 + <button class="btn-secondary" onclick={reset}>← Start over</button> 486 + {/if} 487 + </section> 488 + {/if} 489 + 490 + </div> 491 + {/key} 492 + </div> 493 + </main> 494 + 495 + <style> 496 + main { 497 + max-width: 680px; 498 + margin: 0 auto; 499 + padding: 2.5rem 1.5rem 5rem; 500 + min-height: 100vh; 501 + } 502 + 503 + header { margin-bottom: 2.5rem; text-align: center; } 504 + .logo-text { 505 + font-family: 'JetBrains Mono', monospace; 506 + font-size: 1.5rem; font-weight: 500; letter-spacing: -0.02em; 507 + color: var(--text); margin: 0 0 0.25rem; 508 + } 509 + .tagline { color: var(--muted); font-size: 0.875rem; margin: 0; } 510 + 511 + /* ── Step indicator ───────────────────────────────────────────────────────── */ 512 + .steps { display: flex; align-items: center; justify-content: center; margin-bottom: 2rem; } 513 + .step-item { display: flex; flex-direction: column; align-items: center; gap: 0.25rem; flex-shrink: 0; } 514 + .step-dot { 515 + width: 28px; height: 28px; border-radius: 50%; 516 + display: flex; align-items: center; justify-content: center; 517 + font-size: 0.75rem; font-family: 'JetBrains Mono', monospace; font-weight: 500; 518 + border: 1.5px solid var(--border); background: var(--surface); color: var(--muted); 519 + transition: all 0.2s; overflow: hidden; 520 + } 521 + .dot-inner { display: flex; align-items: center; justify-content: center; } 522 + .step-item.done .step-dot { background: var(--accent); border-color: var(--accent); color: #000; } 523 + .step-item.active .step-dot { border-color: var(--accent); color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); } 524 + .step-label { font-size: 0.65rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; white-space: nowrap; } 525 + .step-item.active .step-label { color: var(--accent); } 526 + .step-line { flex: 1; height: 1.5px; background: var(--border); margin: 0 4px 18px; min-width: 16px; transition: background 0.2s; } 527 + .step-line.done { background: var(--accent); } 528 + @media (max-width: 480px) { .step-label { display: none; } } 529 + 530 + /* ── Slide viewport ───────────────────────────────────────────────────────── */ 531 + .step-viewport { display: grid; overflow: hidden; } 532 + .step-slide { grid-area: 1 / 1; min-width: 0; } 533 + 534 + /* ── Mode step ────────────────────────────────────────────────────────────── */ 535 + .mode-grid { 536 + display: grid; grid-template-columns: repeat(auto-fill, minmax(175px, 1fr)); 537 + gap: 0.75rem; margin-bottom: 1.5rem; 538 + } 539 + .mode-card { 540 + background: var(--surface-2); border: 1px solid var(--border); border-radius: 8px; 541 + padding: 1.25rem 1rem; cursor: pointer; text-align: left; 542 + display: flex; flex-direction: column; gap: 0.4rem; 543 + transition: border-color 0.15s, background 0.15s, transform 0.1s; 544 + } 545 + .mode-card:hover { 546 + border-color: var(--accent); 547 + background: linear-gradient(135deg, var(--surface-2), rgba(63,185,104,0.06)); 548 + transform: translateY(-1px); 549 + } 550 + .mode-icon { color: var(--accent); display: flex; } 551 + .mode-title { font-size: 0.9rem; font-weight: 500; color: var(--text); } 552 + .mode-desc { font-size: 0.75rem; color: var(--muted); line-height: 1.4; } 553 + .footer-note { text-align: center; font-size: 0.78rem; color: var(--muted); margin: 0; } 554 + .footer-note a { color: var(--muted); text-decoration: underline; text-underline-offset: 3px; } 555 + .footer-note a:hover { color: var(--accent); } 556 + @media (max-width: 480px) { .mode-grid { grid-template-columns: 1fr 1fr; } } 557 + 558 + /* ── Auth step ────────────────────────────────────────────────────────────── */ 559 + .pw-toggle { 560 + position: absolute; right: 0.75rem; top: 50%; transform: translateY(-50%); 561 + background: none; border: none; color: var(--muted); cursor: pointer; 562 + display: flex; align-items: center; padding: 0; 563 + } 564 + .pw-toggle:hover { color: var(--text); } 565 + 566 + /* ── Files step ───────────────────────────────────────────────────────────── */ 567 + .drop-zones { 568 + display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 569 + gap: 0.75rem; margin-bottom: 1.25rem; 570 + } 571 + .drop-zone { 572 + background: var(--surface-2); border: 1.5px dashed var(--border); border-radius: 8px; 573 + padding: 2rem 1.5rem; display: flex; flex-direction: column; align-items: center; 574 + gap: 0.4rem; cursor: pointer; transition: border-color 0.15s, background 0.15s; 575 + text-align: center; user-select: none; 576 + } 577 + .drop-zone:hover, .drop-zone.dragging { border-color: var(--accent); background: var(--accent-glow); } 578 + .drop-zone.filled { border-style: solid; border-color: var(--accent); } 579 + .drop-icon { color: var(--muted); display: flex; } 580 + .drop-icon.drop-done { color: var(--accent); } 581 + .drop-title { font-size: 0.875rem; font-weight: 500; color: var(--text); } 582 + .drop-filename { font-size: 0.825rem; color: var(--accent); font-family: 'JetBrains Mono', monospace; word-break: break-all; } 583 + .drop-meta { font-size: 0.7rem; color: var(--muted); } 584 + .drop-hint { font-size: 0.75rem; color: var(--muted); } 585 + 586 + /* ── Options step ─────────────────────────────────────────────────────────── */ 587 + .options { display: flex; flex-direction: column; margin-bottom: 1.25rem; } 588 + .option-row { 589 + display: flex; align-items: center; justify-content: space-between; 590 + gap: 1rem; padding: 0.875rem 0; border-bottom: 1px solid var(--border); 591 + } 592 + .option-row:last-child { border-bottom: none; } 593 + .option-info { flex: 1; } 594 + .option-name { font-size: 0.875rem; color: var(--text); display: block; } 595 + .option-desc { font-size: 0.75rem; color: var(--muted); display: block; margin-top: 0.15rem; } 596 + .toggle { 597 + width: 40px; height: 22px; border-radius: 11px; 598 + background: var(--surface-2); border: 1.5px solid var(--border); 599 + cursor: pointer; position: relative; flex-shrink: 0; 600 + transition: background 0.2s, border-color 0.2s; 601 + } 602 + .toggle.on { background: var(--accent); border-color: var(--accent); } 603 + .toggle-thumb { 604 + position: absolute; width: 14px; height: 14px; border-radius: 50%; 605 + background: var(--muted); top: 2px; left: 2px; 606 + transition: transform 0.2s, background 0.2s; 607 + } 608 + .toggle.on .toggle-thumb { transform: translateX(18px); background: #000; } 609 + 610 + /* ── Run step ─────────────────────────────────────────────────────────────── */ 611 + .run-section { padding: 1.5rem; } 612 + .run-header { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: 1rem; } 613 + .run-header .section-title { margin: 0; } 614 + .progress-wrap { height: 3px; background: var(--surface-2); border-radius: 2px; overflow: hidden; margin-bottom: 0.4rem; } 615 + .progress-bar { height: 100%; background: var(--accent); border-radius: 2px; transition: width 0.3s ease; box-shadow: 0 0 8px var(--accent); } 616 + .progress-text { font-size: 0.75rem; color: var(--muted); font-family: 'JetBrains Mono', monospace; margin: 0 0 0.75rem; } 617 + .log-terminal { 618 + background: #060d09; border: 1px solid var(--border); border-radius: 6px; 619 + padding: 0.75rem 1rem; height: 320px; overflow-y: auto; 620 + font-family: 'JetBrains Mono', monospace; font-size: 0.78rem; line-height: 1.65; 621 + } 622 + .log-line { display: flex; align-items: baseline; gap: 0.625rem; word-break: break-word; } 623 + .log-ts { color: var(--border); flex-shrink: 0; font-size: 0.7rem; } 624 + .log-section { color: var(--accent); font-weight: 500; letter-spacing: 0.04em; width: 100%; } 625 + .result-card { background: var(--surface-2); border: 1px solid var(--border); border-radius: 8px; padding: 1.25rem; margin-top: 1rem; text-align: center; } 626 + .result-card.success { border-color: rgba(63,185,104,0.4); background: var(--accent-glow); } 627 + .result-label { font-size: 1rem; font-weight: 600; margin: 0 0 0.25rem; } 628 + .result-label.success { color: var(--accent); } 629 + .result-label.warn { color: var(--warn); } 630 + .result-label.error { color: var(--error); } 631 + .result-detail { font-size: 0.875rem; color: var(--muted); margin: 0; } 632 + </style>
+3
packages/malachite-web/src/routes/+page.ts
··· 1 + // All logic runs client-side — avoid SSR for browser APIs (FileReader, crypto, etc.) 2 + export const ssr = false; 3 + export const prerender = false;
+137
packages/malachite-web/src/routes/layout.css
··· 1 + @import 'tailwindcss'; 2 + 3 + :root { 4 + --bg: #090f0c; 5 + --surface: #0e1a13; 6 + --surface-2: #152019; 7 + --border: #1f3328; 8 + --accent: #3fb968; 9 + --accent-dim: #2d8a4e; 10 + --accent-glow: rgba(63, 185, 104, 0.14); 11 + --text: #d8ede1; 12 + --muted: #6b9e7e; 13 + --error: #f87171; 14 + --warn: #fbbf24; 15 + } 16 + 17 + * { box-sizing: border-box; } 18 + 19 + html, body { 20 + background: var(--bg); 21 + color: var(--text); 22 + font-family: 'Inter', system-ui, -apple-system, sans-serif; 23 + min-height: 100vh; 24 + margin: 0; 25 + } 26 + 27 + a { color: var(--accent); text-decoration: none; } 28 + a:hover { text-decoration: underline; } 29 + 30 + ::-webkit-scrollbar { width: 6px; } 31 + ::-webkit-scrollbar-track { background: var(--surface); } 32 + ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; } 33 + 34 + /* ── shared card ─────────────────────────────────────────────────────────── */ 35 + .card-section { 36 + background: var(--surface); 37 + border: 1px solid var(--border); 38 + border-radius: 12px; 39 + padding: 2rem; 40 + animation: fadeUp 0.2s ease; 41 + } 42 + @keyframes fadeUp { 43 + from { opacity: 0; transform: translateY(8px); } 44 + to { opacity: 1; transform: translateY(0); } 45 + } 46 + 47 + .section-title { font-size: 1.125rem; font-weight: 500; margin: 0 0 0.5rem; color: var(--text); } 48 + .section-sub { color: var(--muted); font-size: 0.875rem; margin: 0 0 1.5rem; } 49 + .section-sub a { color: var(--accent); } 50 + 51 + .back-btn { 52 + background: none; border: none; color: var(--muted); font-size: 0.8rem; 53 + cursor: pointer; padding: 0; margin-bottom: 1rem; transition: color 0.15s; 54 + } 55 + .back-btn:hover { color: var(--text); } 56 + 57 + /* ── buttons ─────────────────────────────────────────────────────────────── */ 58 + .btn-primary { 59 + background: var(--accent); color: #000; border: none; border-radius: 6px; 60 + padding: 0.7rem 1.25rem; font-size: 0.9rem; font-weight: 600; cursor: pointer; 61 + transition: background 0.15s, transform 0.1s, opacity 0.15s; 62 + display: flex; align-items: center; justify-content: center; gap: 0.5rem; 63 + width: 100%; margin-top: 0.5rem; 64 + } 65 + .btn-primary:hover:not(:disabled) { background: #48d47a; transform: translateY(-1px); } 66 + .btn-primary:disabled { opacity: 0.45; cursor: not-allowed; } 67 + 68 + .btn-secondary { 69 + background: var(--surface-2); color: var(--text); border: 1px solid var(--border); 70 + border-radius: 6px; padding: 0.6rem 1.25rem; font-size: 0.875rem; cursor: pointer; 71 + transition: border-color 0.15s, color 0.15s; width: 100%; margin-top: 0.75rem; 72 + } 73 + .btn-secondary:hover { border-color: var(--accent); color: var(--accent); } 74 + 75 + .btn-cancel { 76 + background: transparent; color: var(--error); border: 1px solid rgba(248,113,113,0.4); 77 + border-radius: 6px; padding: 0.4rem 0.875rem; font-size: 0.8rem; 78 + cursor: pointer; transition: background 0.15s; flex-shrink: 0; 79 + } 80 + .btn-cancel:hover { background: rgba(248,113,113,0.1); } 81 + 82 + /* ── form fields ─────────────────────────────────────────────────────────── */ 83 + .form { display: flex; flex-direction: column; gap: 1rem; } 84 + .field { display: flex; flex-direction: column; gap: 0.375rem; } 85 + .field-label { 86 + font-size: 0.8rem; color: var(--muted); 87 + font-family: 'JetBrains Mono', monospace; 88 + display: flex; align-items: center; gap: 0.5rem; 89 + } 90 + .field input { 91 + background: var(--surface-2); border: 1px solid var(--border); border-radius: 6px; 92 + padding: 0.625rem 0.75rem; color: var(--text); font-size: 0.9rem; 93 + font-family: 'JetBrains Mono', monospace; transition: border-color 0.15s; width: 100%; 94 + } 95 + .field input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); } 96 + .field-hint { font-size: 0.75rem; color: var(--muted); } 97 + 98 + .password-wrap { position: relative; } 99 + .password-wrap input { padding-right: 2.5rem; } 100 + 101 + .expand-btn { 102 + background: none; border: none; color: var(--muted); font-size: 0.8rem; 103 + cursor: pointer; padding: 0; text-align: left; 104 + font-family: 'JetBrains Mono', monospace; 105 + } 106 + .expand-btn:hover { color: var(--text); } 107 + 108 + .badge { 109 + background: var(--surface-2); border: 1px solid var(--border); border-radius: 4px; 110 + padding: 1px 6px; font-size: 0.65rem; color: var(--muted); 111 + font-family: 'Inter', sans-serif; vertical-align: middle; 112 + } 113 + 114 + /* ── alerts ──────────────────────────────────────────────────────────────── */ 115 + .alert { border-radius: 6px; padding: 0.625rem 0.875rem; font-size: 0.825rem; border: 1px solid transparent; } 116 + .alert-error { background: rgba(248,113,113,0.1); border-color: rgba(248,113,113,0.3); color: var(--error); } 117 + .alert-info { background: var(--accent-glow); border-color: rgba(63,185,104,0.3); color: var(--accent); } 118 + 119 + /* ── spinner ─────────────────────────────────────────────────────────────── */ 120 + .spinner { 121 + width: 14px; height: 14px; border: 2px solid rgba(0,0,0,0.3); border-top-color: #000; 122 + border-radius: 50%; animation: spin 0.6s linear infinite; flex-shrink: 0; 123 + } 124 + @keyframes spin { to { transform: rotate(360deg); } } 125 + 126 + /* ── how-to accordions ───────────────────────────────────────────────────── */ 127 + .how-to { margin-bottom: 1.25rem; display: flex; flex-direction: column; gap: 0.5rem; } 128 + details { background: var(--surface-2); border: 1px solid var(--border); border-radius: 6px; padding: 0.5rem 0.75rem; font-size: 0.8rem; } 129 + summary { cursor: pointer; color: var(--muted); list-style: none; } 130 + summary:hover { color: var(--text); } 131 + details p { color: var(--muted); margin: 0.5rem 0 0; line-height: 1.5; } 132 + details code { font-family: 'JetBrains Mono', monospace; font-size: 0.8em; color: var(--accent); } 133 + 134 + /* ── responsive ──────────────────────────────────────────────────────────── */ 135 + @media (max-width: 480px) { 136 + .card-section { padding: 1.25rem; } 137 + }
+4
packages/malachite-web/static/favicon.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> 2 + <rect width="32" height="32" rx="8" fill="#0e1117"/> 3 + <text x="16" y="22" font-size="13" font-family="monospace" font-weight="500" text-anchor="middle" fill="#3fb968">mlc</text> 4 + </svg>
+3
packages/malachite-web/static/robots.txt
··· 1 + # allow crawling everything by default 2 + User-agent: * 3 + Disallow:
+6
packages/malachite-web/svelte.config.js
··· 1 + import adapter from '@sveltejs/adapter-vercel'; 2 + 3 + /** @type {import('@sveltejs/kit').Config} */ 4 + const config = { kit: { adapter: adapter() } }; 5 + 6 + export default config;
+20
packages/malachite-web/tsconfig.json
··· 1 + { 2 + "extends": "./.svelte-kit/tsconfig.json", 3 + "compilerOptions": { 4 + "rewriteRelativeImportExtensions": true, 5 + "allowJs": true, 6 + "checkJs": true, 7 + "esModuleInterop": true, 8 + "forceConsistentCasingInFileNames": true, 9 + "resolveJsonModule": true, 10 + "skipLibCheck": true, 11 + "sourceMap": true, 12 + "strict": true, 13 + "moduleResolution": "bundler" 14 + } 15 + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias 16 + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files 17 + // 18 + // To make changes to top-level options such as include and exclude, we recommend extending 19 + // the generated config; see https://svelte.dev/docs/kit/configuration#typescript 20 + }
+6
packages/malachite-web/vercel.json
··· 1 + { 2 + "framework": "sveltekit", 3 + "buildCommand": "pnpm build", 4 + "installCommand": "pnpm install", 5 + "outputDirectory": ".vercel/output" 6 + }
+17
packages/malachite-web/vite.config.ts
··· 1 + import tailwindcss from '@tailwindcss/vite'; 2 + import { sveltekit } from '@sveltejs/kit/vite'; 3 + import { defineConfig } from 'vite'; 4 + 5 + export default defineConfig({ 6 + plugins: [tailwindcss(), sveltekit()], 7 + // Ensure Buffer / global are polyfilled for @atproto/api in the browser 8 + define: { 9 + global: 'globalThis' 10 + }, 11 + optimizeDeps: { 12 + include: ['@atproto/api', '@atproto/common-web'] 13 + }, 14 + build: { 15 + target: 'es2022' 16 + } 17 + });