minimal streamplace frontend
8
fork

Configure Feed

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

init

Juliet 3f3711be

+3261
+6
.gitignore
··· 1 + node_modules 2 + dist 3 + .env 4 + .DS_Store 5 + public/oauth-client-metadata.json 6 + .claude
+4
.oxfmtrc.json
··· 1 + { 2 + "sortImports": {}, 3 + "sortTailwindcss": {} 4 + }
+20
index.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="utf-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" /> 6 + <link rel="icon" href="/favicon.svg" type="image/svg+xml" /> 7 + <meta name="theme-color" content="#0a0a0b" /> 8 + <meta property="og:title" content="stream.place" /> 9 + <meta property="og:type" content="website" /> 10 + <meta property="og:description" content="Live streaming on the atmosphere" /> 11 + <title>stream.place</title> 12 + <link rel="preconnect" href="https://fonts.bunny.net" /> 13 + <link href="https://fonts.bunny.net/css?family=dm-sans:400,500,600,700" rel="stylesheet" /> 14 + <script src="/src/index.tsx" type="module"></script> 15 + </head> 16 + 17 + <body id="root" class="bg-sp-bg text-sp-text min-h-dvh"> 18 + <noscript>You need to enable JavaScript to run this app.</noscript> 19 + </body> 20 + </html>
+35
package.json
··· 1 + { 2 + "name": "streamplace-frontend", 3 + "private": true, 4 + "license": "0BSD", 5 + "type": "module", 6 + "scripts": { 7 + "predev": "node scripts/generate-metadata.js", 8 + "dev": "vite", 9 + "prebuild": "node scripts/generate-metadata.js", 10 + "build": "vite build", 11 + "serve": "vite preview", 12 + "format": "oxfmt ." 13 + }, 14 + "dependencies": { 15 + "@atcute/atproto": "^3.1.10", 16 + "@atcute/client": "^4.2.1", 17 + "@atcute/identity": "^1.1.4", 18 + "@atcute/identity-resolver": "^1.2.2", 19 + "@atcute/lexicons": "^1.2.9", 20 + "@atcute/oauth-browser-client": "^3.0.0", 21 + "@solidjs/router": "^0.16.1", 22 + "lucide-solid": "^1.7.0", 23 + "solid-js": "^1.9.11" 24 + }, 25 + "devDependencies": { 26 + "@tailwindcss/vite": "^4.2.2", 27 + "@types/node": "^25.5.0", 28 + "oxfmt": "^0.42.0", 29 + "tailwindcss": "^4.2.2", 30 + "typescript": "^5.9.3", 31 + "vite": "^8.0.1", 32 + "vite-plugin-solid": "^2.11.11" 33 + }, 34 + "packageManager": "pnpm@10.32.1" 35 + }
+1650
pnpm-lock.yaml
··· 1 + lockfileVersion: '9.0' 2 + 3 + settings: 4 + autoInstallPeers: true 5 + excludeLinksFromLockfile: false 6 + 7 + importers: 8 + 9 + .: 10 + dependencies: 11 + '@atcute/atproto': 12 + specifier: ^3.1.10 13 + version: 3.1.10 14 + '@atcute/client': 15 + specifier: ^4.2.1 16 + version: 4.2.1 17 + '@atcute/identity': 18 + specifier: ^1.1.4 19 + version: 1.1.4 20 + '@atcute/identity-resolver': 21 + specifier: ^1.2.2 22 + version: 1.2.2(@atcute/identity@1.1.4) 23 + '@atcute/lexicons': 24 + specifier: ^1.2.9 25 + version: 1.2.9 26 + '@atcute/oauth-browser-client': 27 + specifier: ^3.0.0 28 + version: 3.0.0(@atcute/identity@1.1.4) 29 + '@solidjs/router': 30 + specifier: ^0.16.1 31 + version: 0.16.1(solid-js@1.9.12) 32 + lucide-solid: 33 + specifier: ^1.7.0 34 + version: 1.7.0(solid-js@1.9.12) 35 + solid-js: 36 + specifier: ^1.9.11 37 + version: 1.9.12 38 + devDependencies: 39 + '@tailwindcss/vite': 40 + specifier: ^4.2.2 41 + version: 4.2.2(vite@8.0.3(@types/node@25.5.0)(jiti@2.6.1)) 42 + '@types/node': 43 + specifier: ^25.5.0 44 + version: 25.5.0 45 + oxfmt: 46 + specifier: ^0.42.0 47 + version: 0.42.0 48 + tailwindcss: 49 + specifier: ^4.2.2 50 + version: 4.2.2 51 + typescript: 52 + specifier: ^5.9.3 53 + version: 5.9.3 54 + vite: 55 + specifier: ^8.0.1 56 + version: 8.0.3(@types/node@25.5.0)(jiti@2.6.1) 57 + vite-plugin-solid: 58 + specifier: ^2.11.11 59 + version: 2.11.11(solid-js@1.9.12)(vite@8.0.3(@types/node@25.5.0)(jiti@2.6.1)) 60 + 61 + packages: 62 + 63 + '@atcute/atproto@3.1.10': 64 + resolution: {integrity: sha512-+GKZpOc0PJcdWMQEkTfg/rSNDAAHxmAUGBl60g2az15etqJn5WaUPNGFE2sB7hKpwi5Ue2h/L0OacINcE/JDDQ==} 65 + 66 + '@atcute/client@4.2.1': 67 + resolution: {integrity: sha512-ZBFM2pW075JtgGFu5g7HHZBecrClhlcNH8GVP9Zz1aViWR+cjjBsTpeE63rJs+FCOHFYlirUyo5L8SGZ4kMINw==} 68 + 69 + '@atcute/identity-resolver@1.2.2': 70 + resolution: {integrity: sha512-eUh/UH4bFvuXS0X7epYCeJC/kj4rbBXfSRumLEH4smMVwNOgTo7cL/0Srty+P/qVPoZEyXdfEbS0PHJyzoXmHw==} 71 + peerDependencies: 72 + '@atcute/identity': ^1.0.0 73 + 74 + '@atcute/identity@1.1.4': 75 + resolution: {integrity: sha512-RCw1IqflfuSYCxK5m0lZCm0UnvIzcUnuhngiBhJEJb9a9Mc2SEf1xP3H8N5r8pvEH1LoAYd6/zrvCNU+uy9esw==} 76 + 77 + '@atcute/lexicons@1.2.9': 78 + resolution: {integrity: sha512-/RRHm2Cw9o8Mcsrq0eo8fjS9okKYLGfuFwrQ0YoP/6sdSDsXshaTLJsvLlcUcaDaSJ1YFOuHIo3zr2Om2F/16g==} 79 + 80 + '@atcute/multibase@1.2.0': 81 + resolution: {integrity: sha512-ZK2GRra+qIYq9nNuQB52m2ul0hOmCQEtPobGfTSUxm7pF0OGEkWGkWHugFhNEDVzHzTwPxHp6VGotdZFue4lYQ==} 82 + 83 + '@atcute/oauth-browser-client@3.0.0': 84 + resolution: {integrity: sha512-7AbKV8tTe7aRJNJV7gCcWHSVEADb2nr58O1p7dQsf73HSe9pvlBkj/Vk1yjjtH691uAVYkwhHSh0bC7D8XdwJw==} 85 + 86 + '@atcute/oauth-crypto@0.1.0': 87 + resolution: {integrity: sha512-qZYDCNLF/4B6AndYT1rsQelN8621AC5u/sL5PHvlr/qqAbmmUwCBGjEgRSyZtHE1AqD60VNiSMlOgAuEQTSl3w==} 88 + 89 + '@atcute/oauth-keyset@0.1.0': 90 + resolution: {integrity: sha512-+wqT/+I5Lg9VzKnKY3g88+N45xbq+wsdT6bHDGqCVa2u57gRvolFF4dY+weMfc/OX641BIZO6/o+zFtKBsMQnQ==} 91 + 92 + '@atcute/oauth-types@0.1.1': 93 + resolution: {integrity: sha512-u+3KMjse3Uc/9hDyilu1QVN7IpcnjVXgRzhddzBB8Uh6wePHNVBDdi9wQvFTVVA3zmxtMJVptXRyLLg6Ou9bqg==} 94 + 95 + '@atcute/uint8array@1.1.1': 96 + resolution: {integrity: sha512-3LsC8XB8TKe9q/5hOA5sFuzGaIFdJZJNewC5OKa3o/eU6+K7JR6see9Zy2JbQERNVnRl11EzbNov1efgLMAs4g==} 97 + 98 + '@atcute/util-fetch@1.0.5': 99 + resolution: {integrity: sha512-qjHj01BGxjSjIFdPiAjSARnodJIIyKxnCMMEcXMESo9TAyND6XZQqrie5fia+LlYWVXdpsTds8uFQwc9jdKTig==} 100 + 101 + '@atcute/util-text@1.2.0': 102 + resolution: {integrity: sha512-b8WSh+Z7K601eUFFmTFj8QPKDO8Ic0VDDj63sdKzpkm+ySQKsYT5nXekViGqFVKbyKj1V5FyvZvgXad6/aI4QQ==} 103 + 104 + '@babel/code-frame@7.29.0': 105 + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} 106 + engines: {node: '>=6.9.0'} 107 + 108 + '@babel/compat-data@7.29.0': 109 + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} 110 + engines: {node: '>=6.9.0'} 111 + 112 + '@babel/core@7.29.0': 113 + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} 114 + engines: {node: '>=6.9.0'} 115 + 116 + '@babel/generator@7.29.1': 117 + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} 118 + engines: {node: '>=6.9.0'} 119 + 120 + '@babel/helper-compilation-targets@7.28.6': 121 + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} 122 + engines: {node: '>=6.9.0'} 123 + 124 + '@babel/helper-globals@7.28.0': 125 + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} 126 + engines: {node: '>=6.9.0'} 127 + 128 + '@babel/helper-module-imports@7.18.6': 129 + resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} 130 + engines: {node: '>=6.9.0'} 131 + 132 + '@babel/helper-module-imports@7.28.6': 133 + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} 134 + engines: {node: '>=6.9.0'} 135 + 136 + '@babel/helper-module-transforms@7.28.6': 137 + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} 138 + engines: {node: '>=6.9.0'} 139 + peerDependencies: 140 + '@babel/core': ^7.0.0 141 + 142 + '@babel/helper-plugin-utils@7.28.6': 143 + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} 144 + engines: {node: '>=6.9.0'} 145 + 146 + '@babel/helper-string-parser@7.27.1': 147 + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} 148 + engines: {node: '>=6.9.0'} 149 + 150 + '@babel/helper-validator-identifier@7.28.5': 151 + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} 152 + engines: {node: '>=6.9.0'} 153 + 154 + '@babel/helper-validator-option@7.27.1': 155 + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} 156 + engines: {node: '>=6.9.0'} 157 + 158 + '@babel/helpers@7.29.2': 159 + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} 160 + engines: {node: '>=6.9.0'} 161 + 162 + '@babel/parser@7.29.2': 163 + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} 164 + engines: {node: '>=6.0.0'} 165 + hasBin: true 166 + 167 + '@babel/plugin-syntax-jsx@7.28.6': 168 + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} 169 + engines: {node: '>=6.9.0'} 170 + peerDependencies: 171 + '@babel/core': ^7.0.0-0 172 + 173 + '@babel/template@7.28.6': 174 + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} 175 + engines: {node: '>=6.9.0'} 176 + 177 + '@babel/traverse@7.29.0': 178 + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} 179 + engines: {node: '>=6.9.0'} 180 + 181 + '@babel/types@7.29.0': 182 + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} 183 + engines: {node: '>=6.9.0'} 184 + 185 + '@badrap/valita@0.4.6': 186 + resolution: {integrity: sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg==} 187 + engines: {node: '>= 18'} 188 + 189 + '@emnapi/core@1.9.1': 190 + resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==} 191 + 192 + '@emnapi/runtime@1.9.1': 193 + resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==} 194 + 195 + '@emnapi/wasi-threads@1.2.0': 196 + resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} 197 + 198 + '@jridgewell/gen-mapping@0.3.13': 199 + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} 200 + 201 + '@jridgewell/remapping@2.3.5': 202 + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} 203 + 204 + '@jridgewell/resolve-uri@3.1.2': 205 + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 206 + engines: {node: '>=6.0.0'} 207 + 208 + '@jridgewell/sourcemap-codec@1.5.5': 209 + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} 210 + 211 + '@jridgewell/trace-mapping@0.3.31': 212 + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} 213 + 214 + '@napi-rs/wasm-runtime@1.1.1': 215 + resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} 216 + 217 + '@oxc-project/types@0.122.0': 218 + resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==} 219 + 220 + '@oxfmt/binding-android-arm-eabi@0.42.0': 221 + resolution: {integrity: sha512-dsqPTYsozeokRjlrt/b4E7Pj0z3eS3Eg74TWQuuKbjY4VttBmA88rB7d50Xrd+TZ986qdXCNeZRPEzZHAe+jow==} 222 + engines: {node: ^20.19.0 || >=22.12.0} 223 + cpu: [arm] 224 + os: [android] 225 + 226 + '@oxfmt/binding-android-arm64@0.42.0': 227 + resolution: {integrity: sha512-t+aAjHxcr5eOBphFHdg1ouQU9qmZZoRxnX7UOJSaTwSoKsb6TYezNKO0YbWytGXCECObRqNcUxPoPr0KaraAIg==} 228 + engines: {node: ^20.19.0 || >=22.12.0} 229 + cpu: [arm64] 230 + os: [android] 231 + 232 + '@oxfmt/binding-darwin-arm64@0.42.0': 233 + resolution: {integrity: sha512-ulpSEYMKg61C5bRMZinFHrKJYRoKGVbvMEXA5zM1puX3O9T6Q4XXDbft20yrDijpYWeuG59z3Nabt+npeTsM1A==} 234 + engines: {node: ^20.19.0 || >=22.12.0} 235 + cpu: [arm64] 236 + os: [darwin] 237 + 238 + '@oxfmt/binding-darwin-x64@0.42.0': 239 + resolution: {integrity: sha512-ttxLKhQYPdFiM8I/Ri37cvqChE4Xa562nNOsZFcv1CKTVLeEozXjKuYClNvxkXmNlcF55nzM80P+CQkdFBu+uQ==} 240 + engines: {node: ^20.19.0 || >=22.12.0} 241 + cpu: [x64] 242 + os: [darwin] 243 + 244 + '@oxfmt/binding-freebsd-x64@0.42.0': 245 + resolution: {integrity: sha512-Og7QS3yI3tdIKYZ58SXik0rADxIk2jmd+/YvuHRyKULWpG4V2fR5V4hvKm624Mc0cQET35waPXiCQWvjQEjwYQ==} 246 + engines: {node: ^20.19.0 || >=22.12.0} 247 + cpu: [x64] 248 + os: [freebsd] 249 + 250 + '@oxfmt/binding-linux-arm-gnueabihf@0.42.0': 251 + resolution: {integrity: sha512-jwLOw/3CW4H6Vxcry4/buQHk7zm9Ne2YsidzTL1kpiMe4qqrRCwev3dkyWe2YkFmP+iZCQ7zku4KwjcLRoh8ew==} 252 + engines: {node: ^20.19.0 || >=22.12.0} 253 + cpu: [arm] 254 + os: [linux] 255 + 256 + '@oxfmt/binding-linux-arm-musleabihf@0.42.0': 257 + resolution: {integrity: sha512-XwXu2vkMtiq2h7tfvN+WA/9/5/1IoGAVCFPiiQUvcAuG3efR97KNcRGM8BetmbYouFotQ2bDal3yyjUx6IPsTg==} 258 + engines: {node: ^20.19.0 || >=22.12.0} 259 + cpu: [arm] 260 + os: [linux] 261 + 262 + '@oxfmt/binding-linux-arm64-gnu@0.42.0': 263 + resolution: {integrity: sha512-ea7s/XUJoT7ENAtUQDudFe3nkSM3e3Qpz4nJFRdzO2wbgXEcjnchKLEsV3+t4ev3r8nWxIYr9NRjPWtnyIFJVA==} 264 + engines: {node: ^20.19.0 || >=22.12.0} 265 + cpu: [arm64] 266 + os: [linux] 267 + libc: [glibc] 268 + 269 + '@oxfmt/binding-linux-arm64-musl@0.42.0': 270 + resolution: {integrity: sha512-+JA0YMlSdDqmacygGi2REp57c3fN+tzARD8nwsukx9pkCHK+6DkbAA9ojS4lNKsiBjIW8WWa0pBrBWhdZEqfuw==} 271 + engines: {node: ^20.19.0 || >=22.12.0} 272 + cpu: [arm64] 273 + os: [linux] 274 + libc: [musl] 275 + 276 + '@oxfmt/binding-linux-ppc64-gnu@0.42.0': 277 + resolution: {integrity: sha512-VfnET0j4Y5mdfCzh5gBt0NK28lgn5DKx+8WgSMLYYeSooHhohdbzwAStLki9pNuGy51y4I7IoW8bqwAaCMiJQg==} 278 + engines: {node: ^20.19.0 || >=22.12.0} 279 + cpu: [ppc64] 280 + os: [linux] 281 + libc: [glibc] 282 + 283 + '@oxfmt/binding-linux-riscv64-gnu@0.42.0': 284 + resolution: {integrity: sha512-gVlCbmBkB0fxBWbhBj9rcxezPydsQHf4MFKeHoTSPicOQ+8oGeTQgQ8EeesSybWeiFPVRx3bgdt4IJnH6nOjAA==} 285 + engines: {node: ^20.19.0 || >=22.12.0} 286 + cpu: [riscv64] 287 + os: [linux] 288 + libc: [glibc] 289 + 290 + '@oxfmt/binding-linux-riscv64-musl@0.42.0': 291 + resolution: {integrity: sha512-zN5OfstL0avgt/IgvRu0zjQzVh/EPkcLzs33E9LMAzpqlLWiPWeMDZyMGFlSRGOdDjuNmlZBCgj0pFnK5u32TQ==} 292 + engines: {node: ^20.19.0 || >=22.12.0} 293 + cpu: [riscv64] 294 + os: [linux] 295 + libc: [musl] 296 + 297 + '@oxfmt/binding-linux-s390x-gnu@0.42.0': 298 + resolution: {integrity: sha512-9X6+H2L0qMc2sCAgO9HS03bkGLMKvOFjmEdchaFlany3vNZOjnVui//D8k/xZAtQv2vaCs1reD5KAgPoIU4msA==} 299 + engines: {node: ^20.19.0 || >=22.12.0} 300 + cpu: [s390x] 301 + os: [linux] 302 + libc: [glibc] 303 + 304 + '@oxfmt/binding-linux-x64-gnu@0.42.0': 305 + resolution: {integrity: sha512-BajxJ6KQvMMdpXGPWhBGyjb2Jvx4uec0w+wi6TJZ6Tv7+MzPwe0pO8g5h1U0jyFgoaF7mDl6yKPW3ykWcbUJRw==} 306 + engines: {node: ^20.19.0 || >=22.12.0} 307 + cpu: [x64] 308 + os: [linux] 309 + libc: [glibc] 310 + 311 + '@oxfmt/binding-linux-x64-musl@0.42.0': 312 + resolution: {integrity: sha512-0wV284I6vc5f0AqAhgAbHU2935B4bVpncPoe5n/WzVZY/KnHgqxC8iSFGeSyLWEgstFboIcWkOPck7tqbdHkzA==} 313 + engines: {node: ^20.19.0 || >=22.12.0} 314 + cpu: [x64] 315 + os: [linux] 316 + libc: [musl] 317 + 318 + '@oxfmt/binding-openharmony-arm64@0.42.0': 319 + resolution: {integrity: sha512-p4BG6HpGnhfgHk1rzZfyR6zcWkE7iLrWxyehHfXUy4Qa5j3e0roglFOdP/Nj5cJJ58MA3isQ5dlfkW2nNEpolw==} 320 + engines: {node: ^20.19.0 || >=22.12.0} 321 + cpu: [arm64] 322 + os: [openharmony] 323 + 324 + '@oxfmt/binding-win32-arm64-msvc@0.42.0': 325 + resolution: {integrity: sha512-mn//WV60A+IetORDxYieYGAoQso4KnVRRjORDewMcod4irlRe0OSC7YPhhwaexYNPQz/GCFk+v9iUcZ2W22yxQ==} 326 + engines: {node: ^20.19.0 || >=22.12.0} 327 + cpu: [arm64] 328 + os: [win32] 329 + 330 + '@oxfmt/binding-win32-ia32-msvc@0.42.0': 331 + resolution: {integrity: sha512-3gWltUrvuz4LPJXWivoAxZ28Of2O4N7OGuM5/X3ubPXCEV8hmgECLZzjz7UYvSDUS3grfdccQwmjynm+51EFpw==} 332 + engines: {node: ^20.19.0 || >=22.12.0} 333 + cpu: [ia32] 334 + os: [win32] 335 + 336 + '@oxfmt/binding-win32-x64-msvc@0.42.0': 337 + resolution: {integrity: sha512-Wg4TMAfQRL9J9AZevJ/ZNy3uyyDztDYQtGr4P8UyyzIhLhFrdSmz1J/9JT+rv0fiCDLaFOBQnj3f3K3+a5PzDQ==} 338 + engines: {node: ^20.19.0 || >=22.12.0} 339 + cpu: [x64] 340 + os: [win32] 341 + 342 + '@rolldown/binding-android-arm64@1.0.0-rc.12': 343 + resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==} 344 + engines: {node: ^20.19.0 || >=22.12.0} 345 + cpu: [arm64] 346 + os: [android] 347 + 348 + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': 349 + resolution: {integrity: sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==} 350 + engines: {node: ^20.19.0 || >=22.12.0} 351 + cpu: [arm64] 352 + os: [darwin] 353 + 354 + '@rolldown/binding-darwin-x64@1.0.0-rc.12': 355 + resolution: {integrity: sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==} 356 + engines: {node: ^20.19.0 || >=22.12.0} 357 + cpu: [x64] 358 + os: [darwin] 359 + 360 + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': 361 + resolution: {integrity: sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==} 362 + engines: {node: ^20.19.0 || >=22.12.0} 363 + cpu: [x64] 364 + os: [freebsd] 365 + 366 + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': 367 + resolution: {integrity: sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==} 368 + engines: {node: ^20.19.0 || >=22.12.0} 369 + cpu: [arm] 370 + os: [linux] 371 + 372 + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': 373 + resolution: {integrity: sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==} 374 + engines: {node: ^20.19.0 || >=22.12.0} 375 + cpu: [arm64] 376 + os: [linux] 377 + libc: [glibc] 378 + 379 + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': 380 + resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==} 381 + engines: {node: ^20.19.0 || >=22.12.0} 382 + cpu: [arm64] 383 + os: [linux] 384 + libc: [musl] 385 + 386 + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': 387 + resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==} 388 + engines: {node: ^20.19.0 || >=22.12.0} 389 + cpu: [ppc64] 390 + os: [linux] 391 + libc: [glibc] 392 + 393 + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': 394 + resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==} 395 + engines: {node: ^20.19.0 || >=22.12.0} 396 + cpu: [s390x] 397 + os: [linux] 398 + libc: [glibc] 399 + 400 + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': 401 + resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==} 402 + engines: {node: ^20.19.0 || >=22.12.0} 403 + cpu: [x64] 404 + os: [linux] 405 + libc: [glibc] 406 + 407 + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': 408 + resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==} 409 + engines: {node: ^20.19.0 || >=22.12.0} 410 + cpu: [x64] 411 + os: [linux] 412 + libc: [musl] 413 + 414 + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': 415 + resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==} 416 + engines: {node: ^20.19.0 || >=22.12.0} 417 + cpu: [arm64] 418 + os: [openharmony] 419 + 420 + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': 421 + resolution: {integrity: sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==} 422 + engines: {node: '>=14.0.0'} 423 + cpu: [wasm32] 424 + 425 + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': 426 + resolution: {integrity: sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==} 427 + engines: {node: ^20.19.0 || >=22.12.0} 428 + cpu: [arm64] 429 + os: [win32] 430 + 431 + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': 432 + resolution: {integrity: sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==} 433 + engines: {node: ^20.19.0 || >=22.12.0} 434 + cpu: [x64] 435 + os: [win32] 436 + 437 + '@rolldown/pluginutils@1.0.0-rc.12': 438 + resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==} 439 + 440 + '@solidjs/router@0.16.1': 441 + resolution: {integrity: sha512-IhyjedgC6LRpw/8CPGGI89FrV+r0xTHzOl2c4CRyzYQ1bLepJxbVI1LLKvsavMWY5TRBRacV7hAeOhuTXkjiqg==} 442 + peerDependencies: 443 + solid-js: ^1.8.6 444 + 445 + '@standard-schema/spec@1.1.0': 446 + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} 447 + 448 + '@tailwindcss/node@4.2.2': 449 + resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==} 450 + 451 + '@tailwindcss/oxide-android-arm64@4.2.2': 452 + resolution: {integrity: sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==} 453 + engines: {node: '>= 20'} 454 + cpu: [arm64] 455 + os: [android] 456 + 457 + '@tailwindcss/oxide-darwin-arm64@4.2.2': 458 + resolution: {integrity: sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==} 459 + engines: {node: '>= 20'} 460 + cpu: [arm64] 461 + os: [darwin] 462 + 463 + '@tailwindcss/oxide-darwin-x64@4.2.2': 464 + resolution: {integrity: sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==} 465 + engines: {node: '>= 20'} 466 + cpu: [x64] 467 + os: [darwin] 468 + 469 + '@tailwindcss/oxide-freebsd-x64@4.2.2': 470 + resolution: {integrity: sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==} 471 + engines: {node: '>= 20'} 472 + cpu: [x64] 473 + os: [freebsd] 474 + 475 + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2': 476 + resolution: {integrity: sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==} 477 + engines: {node: '>= 20'} 478 + cpu: [arm] 479 + os: [linux] 480 + 481 + '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': 482 + resolution: {integrity: sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==} 483 + engines: {node: '>= 20'} 484 + cpu: [arm64] 485 + os: [linux] 486 + libc: [glibc] 487 + 488 + '@tailwindcss/oxide-linux-arm64-musl@4.2.2': 489 + resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==} 490 + engines: {node: '>= 20'} 491 + cpu: [arm64] 492 + os: [linux] 493 + libc: [musl] 494 + 495 + '@tailwindcss/oxide-linux-x64-gnu@4.2.2': 496 + resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==} 497 + engines: {node: '>= 20'} 498 + cpu: [x64] 499 + os: [linux] 500 + libc: [glibc] 501 + 502 + '@tailwindcss/oxide-linux-x64-musl@4.2.2': 503 + resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==} 504 + engines: {node: '>= 20'} 505 + cpu: [x64] 506 + os: [linux] 507 + libc: [musl] 508 + 509 + '@tailwindcss/oxide-wasm32-wasi@4.2.2': 510 + resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==} 511 + engines: {node: '>=14.0.0'} 512 + cpu: [wasm32] 513 + bundledDependencies: 514 + - '@napi-rs/wasm-runtime' 515 + - '@emnapi/core' 516 + - '@emnapi/runtime' 517 + - '@tybys/wasm-util' 518 + - '@emnapi/wasi-threads' 519 + - tslib 520 + 521 + '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': 522 + resolution: {integrity: sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==} 523 + engines: {node: '>= 20'} 524 + cpu: [arm64] 525 + os: [win32] 526 + 527 + '@tailwindcss/oxide-win32-x64-msvc@4.2.2': 528 + resolution: {integrity: sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==} 529 + engines: {node: '>= 20'} 530 + cpu: [x64] 531 + os: [win32] 532 + 533 + '@tailwindcss/oxide@4.2.2': 534 + resolution: {integrity: sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==} 535 + engines: {node: '>= 20'} 536 + 537 + '@tailwindcss/vite@4.2.2': 538 + resolution: {integrity: sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==} 539 + peerDependencies: 540 + vite: ^5.2.0 || ^6 || ^7 || ^8 541 + 542 + '@tybys/wasm-util@0.10.1': 543 + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} 544 + 545 + '@types/babel__core@7.20.5': 546 + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} 547 + 548 + '@types/babel__generator@7.27.0': 549 + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} 550 + 551 + '@types/babel__template@7.4.4': 552 + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} 553 + 554 + '@types/babel__traverse@7.28.0': 555 + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} 556 + 557 + '@types/node@25.5.0': 558 + resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} 559 + 560 + babel-plugin-jsx-dom-expressions@0.40.6: 561 + resolution: {integrity: sha512-v3P1MW46Lm7VMpAkq0QfyzLWWkC8fh+0aE5Km4msIgDx5kjenHU0pF2s+4/NH8CQn/kla6+Hvws+2AF7bfV5qQ==} 562 + peerDependencies: 563 + '@babel/core': ^7.20.12 564 + 565 + babel-preset-solid@1.9.12: 566 + resolution: {integrity: sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg==} 567 + peerDependencies: 568 + '@babel/core': ^7.0.0 569 + solid-js: ^1.9.12 570 + peerDependenciesMeta: 571 + solid-js: 572 + optional: true 573 + 574 + baseline-browser-mapping@2.10.11: 575 + resolution: {integrity: sha512-DAKrHphkJyiGuau/cFieRYhcTFeK/lBuD++C7cZ6KZHbMhBrisoi+EvhQ5RZrIfV5qwsW8kgQ07JIC+MDJRAhg==} 576 + engines: {node: '>=6.0.0'} 577 + hasBin: true 578 + 579 + browserslist@4.28.1: 580 + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} 581 + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} 582 + hasBin: true 583 + 584 + caniuse-lite@1.0.30001781: 585 + resolution: {integrity: sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==} 586 + 587 + convert-source-map@2.0.0: 588 + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} 589 + 590 + csstype@3.2.3: 591 + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} 592 + 593 + debug@4.4.3: 594 + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} 595 + engines: {node: '>=6.0'} 596 + peerDependencies: 597 + supports-color: '*' 598 + peerDependenciesMeta: 599 + supports-color: 600 + optional: true 601 + 602 + detect-libc@2.1.2: 603 + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} 604 + engines: {node: '>=8'} 605 + 606 + electron-to-chromium@1.5.328: 607 + resolution: {integrity: sha512-QNQ5l45DzYytThO21403XN3FvK0hOkWDG8viNf6jqS42msJ8I4tGDSpBCgvDRRPnkffafiwAym2X2eHeGD2V0w==} 608 + 609 + enhanced-resolve@5.20.1: 610 + resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} 611 + engines: {node: '>=10.13.0'} 612 + 613 + entities@6.0.1: 614 + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} 615 + engines: {node: '>=0.12'} 616 + 617 + escalade@3.2.0: 618 + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} 619 + engines: {node: '>=6'} 620 + 621 + esm-env@1.2.2: 622 + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} 623 + 624 + fdir@6.5.0: 625 + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} 626 + engines: {node: '>=12.0.0'} 627 + peerDependencies: 628 + picomatch: ^3 || ^4 629 + peerDependenciesMeta: 630 + picomatch: 631 + optional: true 632 + 633 + fsevents@2.3.3: 634 + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 635 + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 636 + os: [darwin] 637 + 638 + gensync@1.0.0-beta.2: 639 + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} 640 + engines: {node: '>=6.9.0'} 641 + 642 + graceful-fs@4.2.11: 643 + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 644 + 645 + html-entities@2.3.3: 646 + resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} 647 + 648 + is-what@4.1.16: 649 + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} 650 + engines: {node: '>=12.13'} 651 + 652 + jiti@2.6.1: 653 + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} 654 + hasBin: true 655 + 656 + js-tokens@4.0.0: 657 + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 658 + 659 + jsesc@3.1.0: 660 + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} 661 + engines: {node: '>=6'} 662 + hasBin: true 663 + 664 + json5@2.2.3: 665 + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} 666 + engines: {node: '>=6'} 667 + hasBin: true 668 + 669 + lightningcss-android-arm64@1.32.0: 670 + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} 671 + engines: {node: '>= 12.0.0'} 672 + cpu: [arm64] 673 + os: [android] 674 + 675 + lightningcss-darwin-arm64@1.32.0: 676 + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} 677 + engines: {node: '>= 12.0.0'} 678 + cpu: [arm64] 679 + os: [darwin] 680 + 681 + lightningcss-darwin-x64@1.32.0: 682 + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} 683 + engines: {node: '>= 12.0.0'} 684 + cpu: [x64] 685 + os: [darwin] 686 + 687 + lightningcss-freebsd-x64@1.32.0: 688 + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} 689 + engines: {node: '>= 12.0.0'} 690 + cpu: [x64] 691 + os: [freebsd] 692 + 693 + lightningcss-linux-arm-gnueabihf@1.32.0: 694 + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} 695 + engines: {node: '>= 12.0.0'} 696 + cpu: [arm] 697 + os: [linux] 698 + 699 + lightningcss-linux-arm64-gnu@1.32.0: 700 + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} 701 + engines: {node: '>= 12.0.0'} 702 + cpu: [arm64] 703 + os: [linux] 704 + libc: [glibc] 705 + 706 + lightningcss-linux-arm64-musl@1.32.0: 707 + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} 708 + engines: {node: '>= 12.0.0'} 709 + cpu: [arm64] 710 + os: [linux] 711 + libc: [musl] 712 + 713 + lightningcss-linux-x64-gnu@1.32.0: 714 + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} 715 + engines: {node: '>= 12.0.0'} 716 + cpu: [x64] 717 + os: [linux] 718 + libc: [glibc] 719 + 720 + lightningcss-linux-x64-musl@1.32.0: 721 + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} 722 + engines: {node: '>= 12.0.0'} 723 + cpu: [x64] 724 + os: [linux] 725 + libc: [musl] 726 + 727 + lightningcss-win32-arm64-msvc@1.32.0: 728 + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} 729 + engines: {node: '>= 12.0.0'} 730 + cpu: [arm64] 731 + os: [win32] 732 + 733 + lightningcss-win32-x64-msvc@1.32.0: 734 + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} 735 + engines: {node: '>= 12.0.0'} 736 + cpu: [x64] 737 + os: [win32] 738 + 739 + lightningcss@1.32.0: 740 + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} 741 + engines: {node: '>= 12.0.0'} 742 + 743 + lru-cache@5.1.1: 744 + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} 745 + 746 + lucide-solid@1.7.0: 747 + resolution: {integrity: sha512-z+6Vmueb4W3Rf7ZQIEbcwWhQ/ci8vumLnixRzITXXs2uBjFPQvIvhotivUFAlgSj4xDvU822A1p0wioQlFyx8A==} 748 + peerDependencies: 749 + solid-js: ^1.4.7 750 + 751 + magic-string@0.30.21: 752 + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} 753 + 754 + merge-anything@5.1.7: 755 + resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==} 756 + engines: {node: '>=12.13'} 757 + 758 + ms@2.1.3: 759 + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 760 + 761 + nanoid@3.3.11: 762 + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} 763 + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 764 + hasBin: true 765 + 766 + nanoid@5.1.7: 767 + resolution: {integrity: sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ==} 768 + engines: {node: ^18 || >=20} 769 + hasBin: true 770 + 771 + node-releases@2.0.36: 772 + resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} 773 + 774 + oxfmt@0.42.0: 775 + resolution: {integrity: sha512-QhejGErLSMReNuZ6vxgFHDyGoPbjTRNi6uGHjy0cvIjOQFqD6xmr/T+3L41ixR3NIgzcNiJ6ylQKpvShTgDfqg==} 776 + engines: {node: ^20.19.0 || >=22.12.0} 777 + hasBin: true 778 + 779 + parse5@7.3.0: 780 + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} 781 + 782 + picocolors@1.1.1: 783 + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} 784 + 785 + picomatch@4.0.4: 786 + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} 787 + engines: {node: '>=12'} 788 + 789 + postcss@8.5.8: 790 + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} 791 + engines: {node: ^10 || ^12 || >=14} 792 + 793 + rolldown@1.0.0-rc.12: 794 + resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==} 795 + engines: {node: ^20.19.0 || >=22.12.0} 796 + hasBin: true 797 + 798 + semver@6.3.1: 799 + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} 800 + hasBin: true 801 + 802 + seroval-plugins@1.5.1: 803 + resolution: {integrity: sha512-4FbuZ/TMl02sqv0RTFexu0SP6V+ywaIe5bAWCCEik0fk17BhALgwvUDVF7e3Uvf9pxmwCEJsRPmlkUE6HdzLAw==} 804 + engines: {node: '>=10'} 805 + peerDependencies: 806 + seroval: ^1.0 807 + 808 + seroval@1.5.1: 809 + resolution: {integrity: sha512-OwrZRZAfhHww0WEnKHDY8OM0U/Qs8OTfIDWhUD4BLpNJUfXK4cGmjiagGze086m+mhI+V2nD0gfbHEnJjb9STA==} 810 + engines: {node: '>=10'} 811 + 812 + solid-js@1.9.12: 813 + resolution: {integrity: sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw==} 814 + 815 + solid-refresh@0.6.3: 816 + resolution: {integrity: sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==} 817 + peerDependencies: 818 + solid-js: ^1.3 819 + 820 + source-map-js@1.2.1: 821 + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 822 + engines: {node: '>=0.10.0'} 823 + 824 + tailwindcss@4.2.2: 825 + resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==} 826 + 827 + tapable@2.3.2: 828 + resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} 829 + engines: {node: '>=6'} 830 + 831 + tinyglobby@0.2.15: 832 + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} 833 + engines: {node: '>=12.0.0'} 834 + 835 + tinypool@2.1.0: 836 + resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} 837 + engines: {node: ^20.0.0 || >=22.0.0} 838 + 839 + tslib@2.8.1: 840 + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} 841 + 842 + typescript@5.9.3: 843 + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} 844 + engines: {node: '>=14.17'} 845 + hasBin: true 846 + 847 + undici-types@7.18.2: 848 + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} 849 + 850 + unicode-segmenter@0.14.5: 851 + resolution: {integrity: sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==} 852 + 853 + update-browserslist-db@1.2.3: 854 + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} 855 + hasBin: true 856 + peerDependencies: 857 + browserslist: '>= 4.21.0' 858 + 859 + vite-plugin-solid@2.11.11: 860 + resolution: {integrity: sha512-YMZCXsLw9kyuvQFEdwLP27fuTQJLmjNoHy90AOJnbRuJ6DwShUxKFo38gdFrWn9v11hnGicKCZEaeI/TFs6JKw==} 861 + peerDependencies: 862 + '@testing-library/jest-dom': ^5.16.6 || ^5.17.0 || ^6.* 863 + solid-js: ^1.7.2 864 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 865 + peerDependenciesMeta: 866 + '@testing-library/jest-dom': 867 + optional: true 868 + 869 + vite@8.0.3: 870 + resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==} 871 + engines: {node: ^20.19.0 || >=22.12.0} 872 + hasBin: true 873 + peerDependencies: 874 + '@types/node': ^20.19.0 || >=22.12.0 875 + '@vitejs/devtools': ^0.1.0 876 + esbuild: ^0.27.0 877 + jiti: '>=1.21.0' 878 + less: ^4.0.0 879 + sass: ^1.70.0 880 + sass-embedded: ^1.70.0 881 + stylus: '>=0.54.8' 882 + sugarss: ^5.0.0 883 + terser: ^5.16.0 884 + tsx: ^4.8.1 885 + yaml: ^2.4.2 886 + peerDependenciesMeta: 887 + '@types/node': 888 + optional: true 889 + '@vitejs/devtools': 890 + optional: true 891 + esbuild: 892 + optional: true 893 + jiti: 894 + optional: true 895 + less: 896 + optional: true 897 + sass: 898 + optional: true 899 + sass-embedded: 900 + optional: true 901 + stylus: 902 + optional: true 903 + sugarss: 904 + optional: true 905 + terser: 906 + optional: true 907 + tsx: 908 + optional: true 909 + yaml: 910 + optional: true 911 + 912 + vitefu@1.1.2: 913 + resolution: {integrity: sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==} 914 + peerDependencies: 915 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0 916 + peerDependenciesMeta: 917 + vite: 918 + optional: true 919 + 920 + yallist@3.1.1: 921 + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} 922 + 923 + snapshots: 924 + 925 + '@atcute/atproto@3.1.10': 926 + dependencies: 927 + '@atcute/lexicons': 1.2.9 928 + 929 + '@atcute/client@4.2.1': 930 + dependencies: 931 + '@atcute/identity': 1.1.4 932 + '@atcute/lexicons': 1.2.9 933 + 934 + '@atcute/identity-resolver@1.2.2(@atcute/identity@1.1.4)': 935 + dependencies: 936 + '@atcute/identity': 1.1.4 937 + '@atcute/lexicons': 1.2.9 938 + '@atcute/util-fetch': 1.0.5 939 + '@badrap/valita': 0.4.6 940 + 941 + '@atcute/identity@1.1.4': 942 + dependencies: 943 + '@atcute/lexicons': 1.2.9 944 + '@badrap/valita': 0.4.6 945 + 946 + '@atcute/lexicons@1.2.9': 947 + dependencies: 948 + '@atcute/uint8array': 1.1.1 949 + '@atcute/util-text': 1.2.0 950 + '@standard-schema/spec': 1.1.0 951 + esm-env: 1.2.2 952 + 953 + '@atcute/multibase@1.2.0': 954 + dependencies: 955 + '@atcute/uint8array': 1.1.1 956 + 957 + '@atcute/oauth-browser-client@3.0.0(@atcute/identity@1.1.4)': 958 + dependencies: 959 + '@atcute/client': 4.2.1 960 + '@atcute/identity-resolver': 1.2.2(@atcute/identity@1.1.4) 961 + '@atcute/lexicons': 1.2.9 962 + '@atcute/multibase': 1.2.0 963 + '@atcute/oauth-crypto': 0.1.0 964 + '@atcute/oauth-types': 0.1.1 965 + nanoid: 5.1.7 966 + transitivePeerDependencies: 967 + - '@atcute/identity' 968 + 969 + '@atcute/oauth-crypto@0.1.0': 970 + dependencies: 971 + '@atcute/multibase': 1.2.0 972 + '@atcute/uint8array': 1.1.1 973 + '@badrap/valita': 0.4.6 974 + nanoid: 5.1.7 975 + 976 + '@atcute/oauth-keyset@0.1.0': 977 + dependencies: 978 + '@atcute/oauth-crypto': 0.1.0 979 + 980 + '@atcute/oauth-types@0.1.1': 981 + dependencies: 982 + '@atcute/identity': 1.1.4 983 + '@atcute/lexicons': 1.2.9 984 + '@atcute/oauth-keyset': 0.1.0 985 + '@badrap/valita': 0.4.6 986 + 987 + '@atcute/uint8array@1.1.1': {} 988 + 989 + '@atcute/util-fetch@1.0.5': 990 + dependencies: 991 + '@badrap/valita': 0.4.6 992 + 993 + '@atcute/util-text@1.2.0': 994 + dependencies: 995 + unicode-segmenter: 0.14.5 996 + 997 + '@babel/code-frame@7.29.0': 998 + dependencies: 999 + '@babel/helper-validator-identifier': 7.28.5 1000 + js-tokens: 4.0.0 1001 + picocolors: 1.1.1 1002 + 1003 + '@babel/compat-data@7.29.0': {} 1004 + 1005 + '@babel/core@7.29.0': 1006 + dependencies: 1007 + '@babel/code-frame': 7.29.0 1008 + '@babel/generator': 7.29.1 1009 + '@babel/helper-compilation-targets': 7.28.6 1010 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) 1011 + '@babel/helpers': 7.29.2 1012 + '@babel/parser': 7.29.2 1013 + '@babel/template': 7.28.6 1014 + '@babel/traverse': 7.29.0 1015 + '@babel/types': 7.29.0 1016 + '@jridgewell/remapping': 2.3.5 1017 + convert-source-map: 2.0.0 1018 + debug: 4.4.3 1019 + gensync: 1.0.0-beta.2 1020 + json5: 2.2.3 1021 + semver: 6.3.1 1022 + transitivePeerDependencies: 1023 + - supports-color 1024 + 1025 + '@babel/generator@7.29.1': 1026 + dependencies: 1027 + '@babel/parser': 7.29.2 1028 + '@babel/types': 7.29.0 1029 + '@jridgewell/gen-mapping': 0.3.13 1030 + '@jridgewell/trace-mapping': 0.3.31 1031 + jsesc: 3.1.0 1032 + 1033 + '@babel/helper-compilation-targets@7.28.6': 1034 + dependencies: 1035 + '@babel/compat-data': 7.29.0 1036 + '@babel/helper-validator-option': 7.27.1 1037 + browserslist: 4.28.1 1038 + lru-cache: 5.1.1 1039 + semver: 6.3.1 1040 + 1041 + '@babel/helper-globals@7.28.0': {} 1042 + 1043 + '@babel/helper-module-imports@7.18.6': 1044 + dependencies: 1045 + '@babel/types': 7.29.0 1046 + 1047 + '@babel/helper-module-imports@7.28.6': 1048 + dependencies: 1049 + '@babel/traverse': 7.29.0 1050 + '@babel/types': 7.29.0 1051 + transitivePeerDependencies: 1052 + - supports-color 1053 + 1054 + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': 1055 + dependencies: 1056 + '@babel/core': 7.29.0 1057 + '@babel/helper-module-imports': 7.28.6 1058 + '@babel/helper-validator-identifier': 7.28.5 1059 + '@babel/traverse': 7.29.0 1060 + transitivePeerDependencies: 1061 + - supports-color 1062 + 1063 + '@babel/helper-plugin-utils@7.28.6': {} 1064 + 1065 + '@babel/helper-string-parser@7.27.1': {} 1066 + 1067 + '@babel/helper-validator-identifier@7.28.5': {} 1068 + 1069 + '@babel/helper-validator-option@7.27.1': {} 1070 + 1071 + '@babel/helpers@7.29.2': 1072 + dependencies: 1073 + '@babel/template': 7.28.6 1074 + '@babel/types': 7.29.0 1075 + 1076 + '@babel/parser@7.29.2': 1077 + dependencies: 1078 + '@babel/types': 7.29.0 1079 + 1080 + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': 1081 + dependencies: 1082 + '@babel/core': 7.29.0 1083 + '@babel/helper-plugin-utils': 7.28.6 1084 + 1085 + '@babel/template@7.28.6': 1086 + dependencies: 1087 + '@babel/code-frame': 7.29.0 1088 + '@babel/parser': 7.29.2 1089 + '@babel/types': 7.29.0 1090 + 1091 + '@babel/traverse@7.29.0': 1092 + dependencies: 1093 + '@babel/code-frame': 7.29.0 1094 + '@babel/generator': 7.29.1 1095 + '@babel/helper-globals': 7.28.0 1096 + '@babel/parser': 7.29.2 1097 + '@babel/template': 7.28.6 1098 + '@babel/types': 7.29.0 1099 + debug: 4.4.3 1100 + transitivePeerDependencies: 1101 + - supports-color 1102 + 1103 + '@babel/types@7.29.0': 1104 + dependencies: 1105 + '@babel/helper-string-parser': 7.27.1 1106 + '@babel/helper-validator-identifier': 7.28.5 1107 + 1108 + '@badrap/valita@0.4.6': {} 1109 + 1110 + '@emnapi/core@1.9.1': 1111 + dependencies: 1112 + '@emnapi/wasi-threads': 1.2.0 1113 + tslib: 2.8.1 1114 + optional: true 1115 + 1116 + '@emnapi/runtime@1.9.1': 1117 + dependencies: 1118 + tslib: 2.8.1 1119 + optional: true 1120 + 1121 + '@emnapi/wasi-threads@1.2.0': 1122 + dependencies: 1123 + tslib: 2.8.1 1124 + optional: true 1125 + 1126 + '@jridgewell/gen-mapping@0.3.13': 1127 + dependencies: 1128 + '@jridgewell/sourcemap-codec': 1.5.5 1129 + '@jridgewell/trace-mapping': 0.3.31 1130 + 1131 + '@jridgewell/remapping@2.3.5': 1132 + dependencies: 1133 + '@jridgewell/gen-mapping': 0.3.13 1134 + '@jridgewell/trace-mapping': 0.3.31 1135 + 1136 + '@jridgewell/resolve-uri@3.1.2': {} 1137 + 1138 + '@jridgewell/sourcemap-codec@1.5.5': {} 1139 + 1140 + '@jridgewell/trace-mapping@0.3.31': 1141 + dependencies: 1142 + '@jridgewell/resolve-uri': 3.1.2 1143 + '@jridgewell/sourcemap-codec': 1.5.5 1144 + 1145 + '@napi-rs/wasm-runtime@1.1.1': 1146 + dependencies: 1147 + '@emnapi/core': 1.9.1 1148 + '@emnapi/runtime': 1.9.1 1149 + '@tybys/wasm-util': 0.10.1 1150 + optional: true 1151 + 1152 + '@oxc-project/types@0.122.0': {} 1153 + 1154 + '@oxfmt/binding-android-arm-eabi@0.42.0': 1155 + optional: true 1156 + 1157 + '@oxfmt/binding-android-arm64@0.42.0': 1158 + optional: true 1159 + 1160 + '@oxfmt/binding-darwin-arm64@0.42.0': 1161 + optional: true 1162 + 1163 + '@oxfmt/binding-darwin-x64@0.42.0': 1164 + optional: true 1165 + 1166 + '@oxfmt/binding-freebsd-x64@0.42.0': 1167 + optional: true 1168 + 1169 + '@oxfmt/binding-linux-arm-gnueabihf@0.42.0': 1170 + optional: true 1171 + 1172 + '@oxfmt/binding-linux-arm-musleabihf@0.42.0': 1173 + optional: true 1174 + 1175 + '@oxfmt/binding-linux-arm64-gnu@0.42.0': 1176 + optional: true 1177 + 1178 + '@oxfmt/binding-linux-arm64-musl@0.42.0': 1179 + optional: true 1180 + 1181 + '@oxfmt/binding-linux-ppc64-gnu@0.42.0': 1182 + optional: true 1183 + 1184 + '@oxfmt/binding-linux-riscv64-gnu@0.42.0': 1185 + optional: true 1186 + 1187 + '@oxfmt/binding-linux-riscv64-musl@0.42.0': 1188 + optional: true 1189 + 1190 + '@oxfmt/binding-linux-s390x-gnu@0.42.0': 1191 + optional: true 1192 + 1193 + '@oxfmt/binding-linux-x64-gnu@0.42.0': 1194 + optional: true 1195 + 1196 + '@oxfmt/binding-linux-x64-musl@0.42.0': 1197 + optional: true 1198 + 1199 + '@oxfmt/binding-openharmony-arm64@0.42.0': 1200 + optional: true 1201 + 1202 + '@oxfmt/binding-win32-arm64-msvc@0.42.0': 1203 + optional: true 1204 + 1205 + '@oxfmt/binding-win32-ia32-msvc@0.42.0': 1206 + optional: true 1207 + 1208 + '@oxfmt/binding-win32-x64-msvc@0.42.0': 1209 + optional: true 1210 + 1211 + '@rolldown/binding-android-arm64@1.0.0-rc.12': 1212 + optional: true 1213 + 1214 + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': 1215 + optional: true 1216 + 1217 + '@rolldown/binding-darwin-x64@1.0.0-rc.12': 1218 + optional: true 1219 + 1220 + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': 1221 + optional: true 1222 + 1223 + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': 1224 + optional: true 1225 + 1226 + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': 1227 + optional: true 1228 + 1229 + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': 1230 + optional: true 1231 + 1232 + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': 1233 + optional: true 1234 + 1235 + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': 1236 + optional: true 1237 + 1238 + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': 1239 + optional: true 1240 + 1241 + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': 1242 + optional: true 1243 + 1244 + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': 1245 + optional: true 1246 + 1247 + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': 1248 + dependencies: 1249 + '@napi-rs/wasm-runtime': 1.1.1 1250 + optional: true 1251 + 1252 + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': 1253 + optional: true 1254 + 1255 + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': 1256 + optional: true 1257 + 1258 + '@rolldown/pluginutils@1.0.0-rc.12': {} 1259 + 1260 + '@solidjs/router@0.16.1(solid-js@1.9.12)': 1261 + dependencies: 1262 + solid-js: 1.9.12 1263 + 1264 + '@standard-schema/spec@1.1.0': {} 1265 + 1266 + '@tailwindcss/node@4.2.2': 1267 + dependencies: 1268 + '@jridgewell/remapping': 2.3.5 1269 + enhanced-resolve: 5.20.1 1270 + jiti: 2.6.1 1271 + lightningcss: 1.32.0 1272 + magic-string: 0.30.21 1273 + source-map-js: 1.2.1 1274 + tailwindcss: 4.2.2 1275 + 1276 + '@tailwindcss/oxide-android-arm64@4.2.2': 1277 + optional: true 1278 + 1279 + '@tailwindcss/oxide-darwin-arm64@4.2.2': 1280 + optional: true 1281 + 1282 + '@tailwindcss/oxide-darwin-x64@4.2.2': 1283 + optional: true 1284 + 1285 + '@tailwindcss/oxide-freebsd-x64@4.2.2': 1286 + optional: true 1287 + 1288 + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2': 1289 + optional: true 1290 + 1291 + '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': 1292 + optional: true 1293 + 1294 + '@tailwindcss/oxide-linux-arm64-musl@4.2.2': 1295 + optional: true 1296 + 1297 + '@tailwindcss/oxide-linux-x64-gnu@4.2.2': 1298 + optional: true 1299 + 1300 + '@tailwindcss/oxide-linux-x64-musl@4.2.2': 1301 + optional: true 1302 + 1303 + '@tailwindcss/oxide-wasm32-wasi@4.2.2': 1304 + optional: true 1305 + 1306 + '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': 1307 + optional: true 1308 + 1309 + '@tailwindcss/oxide-win32-x64-msvc@4.2.2': 1310 + optional: true 1311 + 1312 + '@tailwindcss/oxide@4.2.2': 1313 + optionalDependencies: 1314 + '@tailwindcss/oxide-android-arm64': 4.2.2 1315 + '@tailwindcss/oxide-darwin-arm64': 4.2.2 1316 + '@tailwindcss/oxide-darwin-x64': 4.2.2 1317 + '@tailwindcss/oxide-freebsd-x64': 4.2.2 1318 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.2 1319 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.2 1320 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.2 1321 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.2 1322 + '@tailwindcss/oxide-linux-x64-musl': 4.2.2 1323 + '@tailwindcss/oxide-wasm32-wasi': 4.2.2 1324 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2 1325 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.2 1326 + 1327 + '@tailwindcss/vite@4.2.2(vite@8.0.3(@types/node@25.5.0)(jiti@2.6.1))': 1328 + dependencies: 1329 + '@tailwindcss/node': 4.2.2 1330 + '@tailwindcss/oxide': 4.2.2 1331 + tailwindcss: 4.2.2 1332 + vite: 8.0.3(@types/node@25.5.0)(jiti@2.6.1) 1333 + 1334 + '@tybys/wasm-util@0.10.1': 1335 + dependencies: 1336 + tslib: 2.8.1 1337 + optional: true 1338 + 1339 + '@types/babel__core@7.20.5': 1340 + dependencies: 1341 + '@babel/parser': 7.29.2 1342 + '@babel/types': 7.29.0 1343 + '@types/babel__generator': 7.27.0 1344 + '@types/babel__template': 7.4.4 1345 + '@types/babel__traverse': 7.28.0 1346 + 1347 + '@types/babel__generator@7.27.0': 1348 + dependencies: 1349 + '@babel/types': 7.29.0 1350 + 1351 + '@types/babel__template@7.4.4': 1352 + dependencies: 1353 + '@babel/parser': 7.29.2 1354 + '@babel/types': 7.29.0 1355 + 1356 + '@types/babel__traverse@7.28.0': 1357 + dependencies: 1358 + '@babel/types': 7.29.0 1359 + 1360 + '@types/node@25.5.0': 1361 + dependencies: 1362 + undici-types: 7.18.2 1363 + 1364 + babel-plugin-jsx-dom-expressions@0.40.6(@babel/core@7.29.0): 1365 + dependencies: 1366 + '@babel/core': 7.29.0 1367 + '@babel/helper-module-imports': 7.18.6 1368 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) 1369 + '@babel/types': 7.29.0 1370 + html-entities: 2.3.3 1371 + parse5: 7.3.0 1372 + 1373 + babel-preset-solid@1.9.12(@babel/core@7.29.0)(solid-js@1.9.12): 1374 + dependencies: 1375 + '@babel/core': 7.29.0 1376 + babel-plugin-jsx-dom-expressions: 0.40.6(@babel/core@7.29.0) 1377 + optionalDependencies: 1378 + solid-js: 1.9.12 1379 + 1380 + baseline-browser-mapping@2.10.11: {} 1381 + 1382 + browserslist@4.28.1: 1383 + dependencies: 1384 + baseline-browser-mapping: 2.10.11 1385 + caniuse-lite: 1.0.30001781 1386 + electron-to-chromium: 1.5.328 1387 + node-releases: 2.0.36 1388 + update-browserslist-db: 1.2.3(browserslist@4.28.1) 1389 + 1390 + caniuse-lite@1.0.30001781: {} 1391 + 1392 + convert-source-map@2.0.0: {} 1393 + 1394 + csstype@3.2.3: {} 1395 + 1396 + debug@4.4.3: 1397 + dependencies: 1398 + ms: 2.1.3 1399 + 1400 + detect-libc@2.1.2: {} 1401 + 1402 + electron-to-chromium@1.5.328: {} 1403 + 1404 + enhanced-resolve@5.20.1: 1405 + dependencies: 1406 + graceful-fs: 4.2.11 1407 + tapable: 2.3.2 1408 + 1409 + entities@6.0.1: {} 1410 + 1411 + escalade@3.2.0: {} 1412 + 1413 + esm-env@1.2.2: {} 1414 + 1415 + fdir@6.5.0(picomatch@4.0.4): 1416 + optionalDependencies: 1417 + picomatch: 4.0.4 1418 + 1419 + fsevents@2.3.3: 1420 + optional: true 1421 + 1422 + gensync@1.0.0-beta.2: {} 1423 + 1424 + graceful-fs@4.2.11: {} 1425 + 1426 + html-entities@2.3.3: {} 1427 + 1428 + is-what@4.1.16: {} 1429 + 1430 + jiti@2.6.1: {} 1431 + 1432 + js-tokens@4.0.0: {} 1433 + 1434 + jsesc@3.1.0: {} 1435 + 1436 + json5@2.2.3: {} 1437 + 1438 + lightningcss-android-arm64@1.32.0: 1439 + optional: true 1440 + 1441 + lightningcss-darwin-arm64@1.32.0: 1442 + optional: true 1443 + 1444 + lightningcss-darwin-x64@1.32.0: 1445 + optional: true 1446 + 1447 + lightningcss-freebsd-x64@1.32.0: 1448 + optional: true 1449 + 1450 + lightningcss-linux-arm-gnueabihf@1.32.0: 1451 + optional: true 1452 + 1453 + lightningcss-linux-arm64-gnu@1.32.0: 1454 + optional: true 1455 + 1456 + lightningcss-linux-arm64-musl@1.32.0: 1457 + optional: true 1458 + 1459 + lightningcss-linux-x64-gnu@1.32.0: 1460 + optional: true 1461 + 1462 + lightningcss-linux-x64-musl@1.32.0: 1463 + optional: true 1464 + 1465 + lightningcss-win32-arm64-msvc@1.32.0: 1466 + optional: true 1467 + 1468 + lightningcss-win32-x64-msvc@1.32.0: 1469 + optional: true 1470 + 1471 + lightningcss@1.32.0: 1472 + dependencies: 1473 + detect-libc: 2.1.2 1474 + optionalDependencies: 1475 + lightningcss-android-arm64: 1.32.0 1476 + lightningcss-darwin-arm64: 1.32.0 1477 + lightningcss-darwin-x64: 1.32.0 1478 + lightningcss-freebsd-x64: 1.32.0 1479 + lightningcss-linux-arm-gnueabihf: 1.32.0 1480 + lightningcss-linux-arm64-gnu: 1.32.0 1481 + lightningcss-linux-arm64-musl: 1.32.0 1482 + lightningcss-linux-x64-gnu: 1.32.0 1483 + lightningcss-linux-x64-musl: 1.32.0 1484 + lightningcss-win32-arm64-msvc: 1.32.0 1485 + lightningcss-win32-x64-msvc: 1.32.0 1486 + 1487 + lru-cache@5.1.1: 1488 + dependencies: 1489 + yallist: 3.1.1 1490 + 1491 + lucide-solid@1.7.0(solid-js@1.9.12): 1492 + dependencies: 1493 + solid-js: 1.9.12 1494 + 1495 + magic-string@0.30.21: 1496 + dependencies: 1497 + '@jridgewell/sourcemap-codec': 1.5.5 1498 + 1499 + merge-anything@5.1.7: 1500 + dependencies: 1501 + is-what: 4.1.16 1502 + 1503 + ms@2.1.3: {} 1504 + 1505 + nanoid@3.3.11: {} 1506 + 1507 + nanoid@5.1.7: {} 1508 + 1509 + node-releases@2.0.36: {} 1510 + 1511 + oxfmt@0.42.0: 1512 + dependencies: 1513 + tinypool: 2.1.0 1514 + optionalDependencies: 1515 + '@oxfmt/binding-android-arm-eabi': 0.42.0 1516 + '@oxfmt/binding-android-arm64': 0.42.0 1517 + '@oxfmt/binding-darwin-arm64': 0.42.0 1518 + '@oxfmt/binding-darwin-x64': 0.42.0 1519 + '@oxfmt/binding-freebsd-x64': 0.42.0 1520 + '@oxfmt/binding-linux-arm-gnueabihf': 0.42.0 1521 + '@oxfmt/binding-linux-arm-musleabihf': 0.42.0 1522 + '@oxfmt/binding-linux-arm64-gnu': 0.42.0 1523 + '@oxfmt/binding-linux-arm64-musl': 0.42.0 1524 + '@oxfmt/binding-linux-ppc64-gnu': 0.42.0 1525 + '@oxfmt/binding-linux-riscv64-gnu': 0.42.0 1526 + '@oxfmt/binding-linux-riscv64-musl': 0.42.0 1527 + '@oxfmt/binding-linux-s390x-gnu': 0.42.0 1528 + '@oxfmt/binding-linux-x64-gnu': 0.42.0 1529 + '@oxfmt/binding-linux-x64-musl': 0.42.0 1530 + '@oxfmt/binding-openharmony-arm64': 0.42.0 1531 + '@oxfmt/binding-win32-arm64-msvc': 0.42.0 1532 + '@oxfmt/binding-win32-ia32-msvc': 0.42.0 1533 + '@oxfmt/binding-win32-x64-msvc': 0.42.0 1534 + 1535 + parse5@7.3.0: 1536 + dependencies: 1537 + entities: 6.0.1 1538 + 1539 + picocolors@1.1.1: {} 1540 + 1541 + picomatch@4.0.4: {} 1542 + 1543 + postcss@8.5.8: 1544 + dependencies: 1545 + nanoid: 3.3.11 1546 + picocolors: 1.1.1 1547 + source-map-js: 1.2.1 1548 + 1549 + rolldown@1.0.0-rc.12: 1550 + dependencies: 1551 + '@oxc-project/types': 0.122.0 1552 + '@rolldown/pluginutils': 1.0.0-rc.12 1553 + optionalDependencies: 1554 + '@rolldown/binding-android-arm64': 1.0.0-rc.12 1555 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.12 1556 + '@rolldown/binding-darwin-x64': 1.0.0-rc.12 1557 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.12 1558 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.12 1559 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.12 1560 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.12 1561 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.12 1562 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.12 1563 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.12 1564 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.12 1565 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.12 1566 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.12 1567 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12 1568 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12 1569 + 1570 + semver@6.3.1: {} 1571 + 1572 + seroval-plugins@1.5.1(seroval@1.5.1): 1573 + dependencies: 1574 + seroval: 1.5.1 1575 + 1576 + seroval@1.5.1: {} 1577 + 1578 + solid-js@1.9.12: 1579 + dependencies: 1580 + csstype: 3.2.3 1581 + seroval: 1.5.1 1582 + seroval-plugins: 1.5.1(seroval@1.5.1) 1583 + 1584 + solid-refresh@0.6.3(solid-js@1.9.12): 1585 + dependencies: 1586 + '@babel/generator': 7.29.1 1587 + '@babel/helper-module-imports': 7.28.6 1588 + '@babel/types': 7.29.0 1589 + solid-js: 1.9.12 1590 + transitivePeerDependencies: 1591 + - supports-color 1592 + 1593 + source-map-js@1.2.1: {} 1594 + 1595 + tailwindcss@4.2.2: {} 1596 + 1597 + tapable@2.3.2: {} 1598 + 1599 + tinyglobby@0.2.15: 1600 + dependencies: 1601 + fdir: 6.5.0(picomatch@4.0.4) 1602 + picomatch: 4.0.4 1603 + 1604 + tinypool@2.1.0: {} 1605 + 1606 + tslib@2.8.1: 1607 + optional: true 1608 + 1609 + typescript@5.9.3: {} 1610 + 1611 + undici-types@7.18.2: {} 1612 + 1613 + unicode-segmenter@0.14.5: {} 1614 + 1615 + update-browserslist-db@1.2.3(browserslist@4.28.1): 1616 + dependencies: 1617 + browserslist: 4.28.1 1618 + escalade: 3.2.0 1619 + picocolors: 1.1.1 1620 + 1621 + vite-plugin-solid@2.11.11(solid-js@1.9.12)(vite@8.0.3(@types/node@25.5.0)(jiti@2.6.1)): 1622 + dependencies: 1623 + '@babel/core': 7.29.0 1624 + '@types/babel__core': 7.20.5 1625 + babel-preset-solid: 1.9.12(@babel/core@7.29.0)(solid-js@1.9.12) 1626 + merge-anything: 5.1.7 1627 + solid-js: 1.9.12 1628 + solid-refresh: 0.6.3(solid-js@1.9.12) 1629 + vite: 8.0.3(@types/node@25.5.0)(jiti@2.6.1) 1630 + vitefu: 1.1.2(vite@8.0.3(@types/node@25.5.0)(jiti@2.6.1)) 1631 + transitivePeerDependencies: 1632 + - supports-color 1633 + 1634 + vite@8.0.3(@types/node@25.5.0)(jiti@2.6.1): 1635 + dependencies: 1636 + lightningcss: 1.32.0 1637 + picomatch: 4.0.4 1638 + postcss: 8.5.8 1639 + rolldown: 1.0.0-rc.12 1640 + tinyglobby: 0.2.15 1641 + optionalDependencies: 1642 + '@types/node': 25.5.0 1643 + fsevents: 2.3.3 1644 + jiti: 2.6.1 1645 + 1646 + vitefu@1.1.2(vite@8.0.3(@types/node@25.5.0)(jiti@2.6.1)): 1647 + optionalDependencies: 1648 + vite: 8.0.3(@types/node@25.5.0)(jiti@2.6.1) 1649 + 1650 + yallist@3.1.1: {}
+5
public/favicon.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 36 36"> 2 + <circle cx="16" cy="16" r="15" fill="none" stroke="#c4b5fd" stroke-opacity="0.15" stroke-width="3"/> 3 + <circle cx="16" cy="16" r="11" fill="none" stroke="#c4b5fd" stroke-opacity="0.3" stroke-width="3"/> 4 + <circle cx="16" cy="16" r="7.5" fill="#c4b5fd"/> 5 + </svg>
+35
scripts/generate-metadata.js
··· 1 + import { mkdirSync, writeFileSync } from "fs"; 2 + import { dirname } from "path"; 3 + import { fileURLToPath } from "url"; 4 + 5 + const __filename = fileURLToPath(import.meta.url); 6 + const __dirname = dirname(__filename); 7 + 8 + const domain = process.env.APP_DOMAIN || "stream.place"; 9 + const protocol = process.env.APP_PROTOCOL || "https"; 10 + const baseUrl = `${protocol}://${domain}`; 11 + 12 + const metadata = { 13 + client_id: `${baseUrl}/oauth-client-metadata.json`, 14 + client_name: "Streamplace", 15 + client_uri: baseUrl, 16 + logo_uri: `${baseUrl}/favicon.ico`, 17 + redirect_uris: [`${baseUrl}/`], 18 + scope: "atproto include:place.stream.authFull", 19 + grant_types: ["authorization_code", "refresh_token"], 20 + response_types: ["code"], 21 + token_endpoint_auth_method: "none", 22 + application_type: "web", 23 + dpop_bound_access_tokens: true, 24 + }; 25 + 26 + const outputPath = `${__dirname}/../public/oauth-client-metadata.json`; 27 + 28 + try { 29 + mkdirSync(dirname(outputPath), { recursive: true }); 30 + writeFileSync(outputPath, JSON.stringify(metadata, null, 2) + "\n"); 31 + console.log(`Generated OAuth metadata for ${baseUrl}`); 32 + } catch (error) { 33 + console.error("Failed to generate metadata:", error); 34 + process.exit(1); 35 + }
+3
src/auth/login-modal.ts
··· 1 + import { createSignal } from "solid-js"; 2 + 3 + export const [showLoginModal, setShowLoginModal] = createSignal(false);
+12
src/auth/login.ts
··· 1 + import { createAuthorizationUrl } from "@atcute/oauth-browser-client"; 2 + 3 + import "./oauth-config"; 4 + 5 + export const signIn = async (handle: string): Promise<void> => { 6 + const authUrl = await createAuthorizationUrl({ 7 + scope: import.meta.env.VITE_OAUTH_SCOPE, 8 + target: { type: "account", identifier: handle as `${string}.${string}` }, 9 + }); 10 + 11 + location.assign(authUrl); 12 + };
+15
src/auth/oauth-config.ts
··· 1 + import { LocalActorResolver } from "@atcute/identity-resolver"; 2 + import { configureOAuth } from "@atcute/oauth-browser-client"; 3 + 4 + import { didDocumentResolver, handleResolver } from "../lib/api"; 5 + 6 + configureOAuth({ 7 + metadata: { 8 + client_id: import.meta.env.VITE_OAUTH_CLIENT_ID, 9 + redirect_uri: import.meta.env.VITE_OAUTH_REDIRECT_URL, 10 + }, 11 + identityResolver: new LocalActorResolver({ 12 + handleResolver: handleResolver, 13 + didDocumentResolver: didDocumentResolver, 14 + }), 15 + });
+77
src/auth/session-manager.ts
··· 1 + import { Client } from "@atcute/client"; 2 + import { 3 + finalizeAuthorization, 4 + getSession, 5 + OAuthUserAgent, 6 + type Session, 7 + } from "@atcute/oauth-browser-client"; 8 + 9 + import { resolveDidDoc } from "../lib/api"; 10 + import { agent, setAgent, setLoggedInDid, setLoggedInHandle } from "./state"; 11 + import "./oauth-config"; 12 + 13 + const resolveOwnHandle = async (did: string): Promise<string | undefined> => { 14 + try { 15 + const doc = await resolveDidDoc(did); 16 + const alias = doc.alsoKnownAs?.find((a) => a.startsWith("at://")); 17 + return alias?.replace("at://", ""); 18 + } catch { 19 + return undefined; 20 + } 21 + }; 22 + 23 + export const initAuth = async (): Promise<void> => { 24 + const session = await (async (): Promise<Session | undefined> => { 25 + const params = new URLSearchParams(decodeURIComponent(location.hash.slice(1))); 26 + 27 + if (params.has("state") && (params.has("code") || params.has("error"))) { 28 + history.replaceState(null, "", location.pathname + location.search); 29 + 30 + const auth = await finalizeAuthorization(params); 31 + const did = auth.session.info.sub; 32 + 33 + localStorage.setItem("atproto_did", did); 34 + return auth.session; 35 + } else { 36 + const storedDid = localStorage.getItem("atproto_did"); 37 + 38 + if (storedDid) { 39 + try { 40 + const session = await getSession(storedDid as `did:${string}:${string}`); 41 + const rpc = new Client({ handler: new OAuthUserAgent(session) }); 42 + const res = await rpc.get("com.atproto.server.getSession"); 43 + if (!res.ok) throw new Error("Session verification failed"); 44 + return session; 45 + } catch (err) { 46 + console.warn("Failed to restore session:", err); 47 + return undefined; 48 + } 49 + } 50 + } 51 + })(); 52 + 53 + if (session) { 54 + const did = session.info.sub; 55 + const oauthAgent = new OAuthUserAgent(session); 56 + setAgent(oauthAgent); 57 + setLoggedInDid(did); 58 + 59 + const handle = await resolveOwnHandle(did); 60 + if (handle) setLoggedInHandle(handle); 61 + } 62 + }; 63 + 64 + export const signOut = async (): Promise<void> => { 65 + const currentAgent = agent(); 66 + if (currentAgent) { 67 + try { 68 + await currentAgent.signOut(); 69 + } catch { 70 + // ignore signout errors 71 + } 72 + } 73 + localStorage.removeItem("atproto_did"); 74 + setAgent(undefined); 75 + setLoggedInDid(undefined); 76 + setLoggedInHandle(undefined); 77 + };
+6
src/auth/state.ts
··· 1 + import { OAuthUserAgent } from "@atcute/oauth-browser-client"; 2 + import { createSignal } from "solid-js"; 3 + 4 + export const [agent, setAgent] = createSignal<OAuthUserAgent | undefined>(); 5 + export const [loggedInDid, setLoggedInDid] = createSignal<string | undefined>(); 6 + export const [loggedInHandle, setLoggedInHandle] = createSignal<string | undefined>();
+303
src/components/Chat.tsx
··· 1 + import { Camera, CornerDownRight, Reply, Sword, Star, X } from "lucide-solid"; 2 + import { createSignal, For, onCleanup, onMount, Show } from "solid-js"; 3 + 4 + import { setShowLoginModal } from "../auth/login-modal"; 5 + import { agent, loggedInDid } from "../auth/state"; 6 + import { resolveHandle } from "../lib/api"; 7 + import { 8 + connectChatWs, 9 + segmentRichText, 10 + sendChatMessage, 11 + type ChatConnection, 12 + type ChatMessage, 13 + type Facet, 14 + type StreamInfo, 15 + } from "../lib/chat"; 16 + 17 + export interface ChatProps { 18 + handle: string; 19 + streamerDid?: string; 20 + onStreamInfo?: (info: StreamInfo) => void; 21 + onViewerCount?: (count: number) => void; 22 + class?: string; 23 + } 24 + 25 + const MAX_MESSAGES = 500; 26 + 27 + function getAuthorColor(msg: ChatMessage): string { 28 + const color = msg.chatProfile?.color; 29 + if (color && color.red !== undefined) { 30 + return `rgb(${color.red}, ${color.green}, ${color.blue})`; 31 + } 32 + return "#4ade80"; 33 + } 34 + 35 + function ChatBadges(props: { badges?: ChatMessage["badges"] }) { 36 + if (!props.badges || props.badges.length === 0) return null; 37 + 38 + return ( 39 + <> 40 + <For each={props.badges}> 41 + {(badge) => { 42 + const type = badge.badgeType; 43 + if (type === "place.stream.badge.defs#mod") { 44 + return ( 45 + <span class="mr-0.5 inline-flex align-middle" title="Moderator"> 46 + <Sword size={12} class="text-blue-400" /> 47 + </span> 48 + ); 49 + } 50 + if (type === "place.stream.badge.defs#streamer") { 51 + return ( 52 + <span class="mr-0.5 inline-flex align-middle" title="Streamer"> 53 + <Camera size={12} class="text-sp-red" /> 54 + </span> 55 + ); 56 + } 57 + if (type === "place.stream.badge.defs#vip") { 58 + return ( 59 + <span class="mr-0.5 inline-flex align-middle" title="VIP"> 60 + <Star size={12} class="text-yellow-400" /> 61 + </span> 62 + ); 63 + } 64 + return null; 65 + }} 66 + </For> 67 + </> 68 + ); 69 + } 70 + 71 + function FacetSegment(props: { text: string; facet?: Facet }) { 72 + if (!props.facet) return <>{props.text}</>; 73 + 74 + for (const feature of props.facet.features) { 75 + if (feature.$type === "app.bsky.richtext.facet#link") { 76 + return ( 77 + <a 78 + href={feature.uri} 79 + target="_blank" 80 + rel="noopener noreferrer" 81 + class="text-sp-accent decoration-sp-accent/40 hover:decoration-sp-accent underline" 82 + > 83 + {props.text} 84 + </a> 85 + ); 86 + } 87 + if (feature.$type === "app.bsky.richtext.facet#mention") { 88 + return ( 89 + <a 90 + href={`https://bsky.app/profile/${feature.did}`} 91 + target="_blank" 92 + rel="noopener noreferrer" 93 + class="text-sp-accent font-medium" 94 + > 95 + {props.text} 96 + </a> 97 + ); 98 + } 99 + } 100 + 101 + return <>{props.text}</>; 102 + } 103 + 104 + export function Chat(props: ChatProps) { 105 + let messagesEl!: HTMLDivElement; 106 + let ws: ChatConnection | undefined; 107 + 108 + const [messages, setMessages] = createSignal<ChatMessage[]>([]); 109 + const [connected, setConnected] = createSignal(false); 110 + const [inputText, setInputText] = createSignal(""); 111 + const [sending, setSending] = createSignal(false); 112 + const [replyingTo, setReplyingTo] = createSignal<ChatMessage | undefined>(); 113 + let inputEl!: HTMLInputElement; 114 + 115 + const addMessage = (msg: ChatMessage) => { 116 + setMessages((prev) => { 117 + // Insert sorted by indexedAt to handle backfill arriving in reverse order 118 + const msgTime = new Date(msg.indexedAt).getTime(); 119 + let i = prev.length; 120 + while (i > 0 && new Date(prev[i - 1].indexedAt).getTime() > msgTime) { 121 + i--; 122 + } 123 + const next = [...prev.slice(0, i), msg, ...prev.slice(i)]; 124 + if (next.length > MAX_MESSAGES) return next.slice(next.length - MAX_MESSAGES); 125 + return next; 126 + }); 127 + 128 + // auto-scroll to bottom 129 + requestAnimationFrame(() => { 130 + if (messagesEl) { 131 + messagesEl.scrollTop = messagesEl.scrollHeight; 132 + } 133 + }); 134 + }; 135 + 136 + const connect = () => { 137 + ws = connectChatWs(props.handle, { 138 + onMessage: addMessage, 139 + onStreamInfo: (info) => props.onStreamInfo?.(info), 140 + onViewerCount: (count) => props.onViewerCount?.(count), 141 + onOpen: () => setConnected(true), 142 + onClose: () => setConnected(false), 143 + }); 144 + }; 145 + 146 + const send = async () => { 147 + const text = inputText().trim(); 148 + if (!text) return; 149 + 150 + const currentAgent = agent(); 151 + const did = loggedInDid(); 152 + const streamerDid = props.streamerDid; 153 + if (!currentAgent || !did || !streamerDid) return; 154 + 155 + const replyMsg = replyingTo(); 156 + const reply = replyMsg 157 + ? { 158 + root: { 159 + uri: replyMsg.record.reply?.root?.uri ?? replyMsg.uri, 160 + cid: replyMsg.record.reply?.root?.cid ?? replyMsg.cid, 161 + }, 162 + parent: { uri: replyMsg.uri, cid: replyMsg.cid }, 163 + } 164 + : undefined; 165 + 166 + setSending(true); 167 + try { 168 + await sendChatMessage(currentAgent, did, streamerDid, text, resolveHandle, reply); 169 + setInputText(""); 170 + setReplyingTo(undefined); 171 + } catch (err) { 172 + console.error("Failed to send chat:", err); 173 + } finally { 174 + setSending(false); 175 + } 176 + }; 177 + 178 + const handleKeyDown = (e: KeyboardEvent) => { 179 + if (e.key === "Enter" && !e.shiftKey) { 180 + e.preventDefault(); 181 + send(); 182 + } 183 + if (e.key === "Escape") { 184 + setReplyingTo(undefined); 185 + } 186 + }; 187 + 188 + onMount(() => { 189 + connect(); 190 + }); 191 + 192 + onCleanup(() => { 193 + if (ws) { 194 + ws.close(); 195 + ws = undefined; 196 + } 197 + }); 198 + 199 + return ( 200 + <div 201 + class={`border-sp-border bg-sp-surface flex min-h-0 flex-col border-l ${props.class ?? ""}`} 202 + > 203 + {/* Messages */} 204 + <div ref={messagesEl} class="min-h-0 flex-1 overflow-y-auto pt-2"> 205 + <Show 206 + when={messages().length > 0} 207 + fallback={ 208 + <div class="text-sp-dim flex h-full items-center justify-center text-sm"> 209 + {connected() ? "Waiting for messages..." : "Connecting..."} 210 + </div> 211 + } 212 + > 213 + <div class="space-y-1"> 214 + <For each={messages()}> 215 + {(msg) => ( 216 + <div class="group/msg hover:bg-sp-hover relative px-3 text-sm leading-relaxed"> 217 + <Show when={agent()}> 218 + <button 219 + class="text-sp-dim hover:text-sp-accent absolute top-0 right-0 hidden rounded p-0.5 transition-colors group-hover/msg:inline-flex" 220 + title="Reply" 221 + onClick={() => { 222 + setReplyingTo(msg); 223 + inputEl?.focus(); 224 + }} 225 + > 226 + <Reply size={14} /> 227 + </button> 228 + </Show> 229 + <Show when={msg.replyTo}> 230 + {(parent) => ( 231 + <div class="text-sp-dim flex items-center gap-1 text-[11px]"> 232 + <CornerDownRight size={10} class="shrink-0" /> 233 + <span class="font-medium" style={{ color: getAuthorColor(parent()) }}> 234 + {parent().author.handle} 235 + </span> 236 + <span class="truncate">{parent().record.text}</span> 237 + </div> 238 + )} 239 + </Show> 240 + <ChatBadges badges={msg.badges} /> 241 + <span class="font-medium" style={{ color: getAuthorColor(msg) }}> 242 + {msg.author.handle} 243 + </span> 244 + <span class="text-sp-dim">: </span> 245 + <span class="wrap-break-word"> 246 + <For each={segmentRichText(msg.record.text, msg.record.facets)}> 247 + {(seg) => <FacetSegment text={seg.text} facet={seg.facet} />} 248 + </For> 249 + </span> 250 + </div> 251 + )} 252 + </For> 253 + </div> 254 + </Show> 255 + </div> 256 + 257 + {/* Input */} 258 + <Show 259 + when={agent()} 260 + fallback={ 261 + <button 262 + class="text-sp-dim hover:text-sp-accent border-sp-border hover:bg-sp-hover mt-2 w-full border-t py-4 text-center text-xs italic transition-colors" 263 + onClick={() => setShowLoginModal(true)} 264 + > 265 + Sign in to chat 266 + </button> 267 + } 268 + > 269 + <div class="px-2 py-3"> 270 + <Show when={replyingTo()}> 271 + {(msg) => ( 272 + <div class="bg-sp-bg text-sp-dim mb-1.5 flex items-center gap-1.5 rounded px-2 py-1 text-xs"> 273 + <CornerDownRight size={10} class="shrink-0" /> 274 + <span class="font-medium" style={{ color: getAuthorColor(msg()) }}> 275 + {msg().author.handle} 276 + </span> 277 + <span class="min-w-0 flex-1 truncate">{msg().record.text}</span> 278 + <button 279 + class="hover:text-sp-text shrink-0 rounded p-0.5 transition-colors" 280 + onClick={() => setReplyingTo(undefined)} 281 + > 282 + <X size={12} /> 283 + </button> 284 + </div> 285 + )} 286 + </Show> 287 + <input 288 + ref={inputEl} 289 + type="text" 290 + placeholder={ 291 + replyingTo() ? `Reply to ${replyingTo()!.author.handle}...` : "Send a message..." 292 + } 293 + class="border-sp-border bg-sp-bg text-sp-text placeholder:text-sp-dim focus:border-sp-accent w-full rounded-sm border px-3 py-2.5 text-sm focus:outline-none" 294 + value={inputText()} 295 + onInput={(e) => setInputText(e.currentTarget.value)} 296 + onKeyDown={handleKeyDown} 297 + disabled={sending() || !props.streamerDid} 298 + /> 299 + </div> 300 + </Show> 301 + </div> 302 + ); 303 + }
+19
src/components/Header.tsx
··· 1 + import { A } from "@solidjs/router"; 2 + 3 + import { LoginButton } from "./LoginButton"; 4 + 5 + export function Header() { 6 + return ( 7 + <header class="border-sp-border flex items-center justify-between border-b px-4 py-3"> 8 + <A href="/" class="flex items-center gap-2 text-lg font-medium tracking-tight"> 9 + <svg viewBox="-2 -2 36 36" class="text-sp-accent h-7 w-7" aria-hidden="true"> 10 + <circle cx="16" cy="16" r="15" fill="none" stroke="currentColor" stroke-opacity="0.15" stroke-width="3" /> 11 + <circle cx="16" cy="16" r="11" fill="none" stroke="currentColor" stroke-opacity="0.3" stroke-width="3" /> 12 + <circle cx="16" cy="16" r="7.5" fill="currentColor" /> 13 + </svg> 14 + <span>streamplace</span> 15 + </A> 16 + <LoginButton /> 17 + </header> 18 + ); 19 + }
+98
src/components/LoginButton.tsx
··· 1 + import { Show, createEffect, createSignal, onCleanup } from "solid-js"; 2 + 3 + import { signIn } from "../auth/login"; 4 + import { showLoginModal, setShowLoginModal } from "../auth/login-modal"; 5 + import { signOut } from "../auth/session-manager"; 6 + import { agent, loggedInHandle } from "../auth/state"; 7 + 8 + export function LoginButton() { 9 + const [handle, setHandle] = createSignal(""); 10 + const [loading, setLoading] = createSignal(false); 11 + 12 + const handleSignIn = async () => { 13 + const h = handle().trim(); 14 + if (!h) return; 15 + setLoading(true); 16 + try { 17 + await signIn(h); 18 + } catch (err) { 19 + console.error("Sign in failed:", err); 20 + setLoading(false); 21 + } 22 + }; 23 + 24 + const handleKeyDown = (e: KeyboardEvent) => { 25 + if (e.key === "Enter") handleSignIn(); 26 + }; 27 + 28 + createEffect(() => { 29 + if (!showLoginModal()) return; 30 + const onEsc = (e: KeyboardEvent) => { 31 + if (e.key === "Escape") setShowLoginModal(false); 32 + }; 33 + document.addEventListener("keydown", onEsc); 34 + onCleanup(() => document.removeEventListener("keydown", onEsc)); 35 + }); 36 + 37 + return ( 38 + <div class="flex items-center gap-3"> 39 + <Show 40 + when={agent()} 41 + fallback={ 42 + <> 43 + <button 44 + class="bg-sp-accent text-sp-bg hover:bg-sp-accent/80 rounded-sm px-3 py-1.5 text-sm font-medium transition-colors" 45 + onClick={() => setShowLoginModal(true)} 46 + > 47 + Sign in 48 + </button> 49 + <Show when={showLoginModal()}> 50 + <div 51 + class="fixed inset-0 z-50 flex items-center justify-center bg-black/60" 52 + onClick={(e) => { 53 + if (e.target === e.currentTarget) setShowLoginModal(false); 54 + }} 55 + > 56 + <div class="bg-sp-surface border-sp-border mx-4 flex w-full max-w-md flex-col gap-4 rounded-lg border p-5 shadow-lg"> 57 + <h2 class="text-sp-text text-lg font-semibold">Sign in</h2> 58 + <input 59 + type="text" 60 + placeholder="handle.bsky.social" 61 + class="border-sp-border bg-sp-bg text-sp-text placeholder:text-sp-dim focus:border-sp-accent rounded-sm border px-3 py-1.5 text-sm focus:outline-none" 62 + value={handle()} 63 + onInput={(e) => setHandle(e.currentTarget.value)} 64 + onKeyDown={handleKeyDown} 65 + ref={(el: HTMLInputElement) => setTimeout(() => el.focus())} 66 + /> 67 + <div class="flex justify-end gap-2"> 68 + <button 69 + class="text-sp-dim hover:text-sp-text rounded-sm px-3 py-1.5 text-sm transition-colors" 70 + onClick={() => setShowLoginModal(false)} 71 + > 72 + Cancel 73 + </button> 74 + <button 75 + class="bg-sp-accent text-sp-bg hover:bg-sp-accent/80 rounded-sm px-3 py-1.5 text-sm font-medium transition-colors disabled:opacity-50" 76 + onClick={handleSignIn} 77 + disabled={loading() || !handle().trim()} 78 + > 79 + {loading() ? "..." : "Go"} 80 + </button> 81 + </div> 82 + </div> 83 + </div> 84 + </Show> 85 + </> 86 + } 87 + > 88 + <span class="text-sp-dim text-sm">@{loggedInHandle() || "..."}</span> 89 + <button 90 + class="border-sp-border text-sp-dim hover:border-sp-red hover:text-sp-red rounded-sm border px-3 py-1.5 text-sm transition-colors" 91 + onClick={() => signOut()} 92 + > 93 + Sign out 94 + </button> 95 + </Show> 96 + </div> 97 + ); 98 + }
+53
src/components/StreamCard.tsx
··· 1 + import { A } from "@solidjs/router"; 2 + 3 + export interface StreamCardProps { 4 + handle: string; 5 + did: string; 6 + title: string; 7 + viewerCount: number; 8 + avatarUrl?: string; 9 + thumbRef?: string; 10 + } 11 + 12 + function getThumbnailUrl(handle: string): string { 13 + return `https://stream.place/api/playback/${encodeURIComponent(handle)}/stream.jpg?ts=${Date.now()}`; 14 + } 15 + 16 + export function StreamCard(props: StreamCardProps) { 17 + const thumbUrl = () => getThumbnailUrl(props.handle); 18 + 19 + return ( 20 + <A 21 + href={`/${props.handle}`} 22 + class="group border-sp-border bg-sp-surface hover:border-sp-accent overflow-hidden rounded-lg border transition-colors" 23 + > 24 + <div class="bg-sp-bg aspect-video w-full overflow-hidden"> 25 + {thumbUrl() ? ( 26 + <img src={thumbUrl()} alt="" class="h-full w-full object-cover" loading="lazy" /> 27 + ) : ( 28 + <div class="text-sp-dim flex h-full items-center justify-center"> 29 + <span class="text-3xl">&#9654;</span> 30 + </div> 31 + )} 32 + </div> 33 + <div class="group-hover:bg-sp-accent/10 flex gap-3 p-3 transition-colors"> 34 + {props.avatarUrl ? ( 35 + <img src={props.avatarUrl} alt="" class="h-9 w-9 shrink-0 rounded-full" loading="lazy" /> 36 + ) : ( 37 + <div class="bg-sp-border h-9 w-9 shrink-0 rounded-full" /> 38 + )} 39 + <div class="min-w-0 flex-1"> 40 + <div class="truncate text-sm font-medium">{props.title}</div> 41 + <div class="text-sp-dim truncate text-xs">@{props.handle}</div> 42 + <div class="text-sp-dim mt-1 flex items-center gap-1.5 text-xs"> 43 + <span 44 + class="bg-sp-accent inline-block h-2 w-2 rounded-full" 45 + style={{ animation: "pulse-live 3s ease-in-out infinite" }} 46 + /> 47 + {props.viewerCount} watching 48 + </div> 49 + </div> 50 + </div> 51 + </A> 52 + ); 53 + }
+160
src/components/VideoPlayer.tsx
··· 1 + import { Maximize, Volume2, VolumeOff } from "lucide-solid"; 2 + import { createSignal, onCleanup, onMount, Show } from "solid-js"; 3 + 4 + import { connectWhep, type WhepConnection } from "../lib/whep"; 5 + 6 + export interface VideoPlayerProps { 7 + handle: string; 8 + } 9 + 10 + export function VideoPlayer(props: VideoPlayerProps) { 11 + let videoEl!: HTMLVideoElement; 12 + let containerEl!: HTMLDivElement; 13 + let connection: WhepConnection | undefined; 14 + 15 + const [status, setStatus] = createSignal<"connecting" | "live" | "error" | "idle">("idle"); 16 + const [muted, setMuted] = createSignal(true); 17 + const [volume, setVolume] = createSignal(1); 18 + const [errorMsg, setErrorMsg] = createSignal(""); 19 + 20 + const connect = async () => { 21 + setStatus("connecting"); 22 + setErrorMsg(""); 23 + 24 + try { 25 + connection = await connectWhep(props.handle); 26 + 27 + connection.pc.oniceconnectionstatechange = () => { 28 + const state = connection!.pc.iceConnectionState; 29 + if (state === "connected" || state === "completed") { 30 + setStatus("live"); 31 + } else if (state === "failed" || state === "disconnected") { 32 + setStatus("error"); 33 + setErrorMsg("Connection lost"); 34 + } 35 + }; 36 + 37 + connection.pc.onconnectionstatechange = () => { 38 + if (connection!.pc.connectionState === "failed") { 39 + setStatus("error"); 40 + setErrorMsg("Connection failed"); 41 + } 42 + }; 43 + 44 + videoEl.srcObject = connection.stream; 45 + videoEl.play().catch(() => {}); 46 + } catch (err) { 47 + setStatus("error"); 48 + setErrorMsg(err instanceof Error ? err.message : "Failed to connect"); 49 + } 50 + }; 51 + 52 + const disconnect = () => { 53 + if (connection) { 54 + connection.pc.close(); 55 + connection = undefined; 56 + } 57 + videoEl.srcObject = null; 58 + setStatus("idle"); 59 + }; 60 + 61 + const toggleMute = () => { 62 + const next = !muted(); 63 + setMuted(next); 64 + videoEl.muted = next; 65 + }; 66 + 67 + const handleVolume = (v: number) => { 68 + setVolume(v); 69 + videoEl.volume = v * v; 70 + if (v > 0 && muted()) { 71 + setMuted(false); 72 + videoEl.muted = false; 73 + } else if (v === 0) { 74 + setMuted(true); 75 + videoEl.muted = true; 76 + } 77 + }; 78 + 79 + const toggleFullscreen = () => { 80 + if (document.fullscreenElement) { 81 + document.exitFullscreen(); 82 + } else { 83 + containerEl.requestFullscreen().catch(() => {}); 84 + } 85 + }; 86 + 87 + onMount(() => { 88 + videoEl.muted = true; 89 + connect(); 90 + }); 91 + 92 + onCleanup(() => { 93 + disconnect(); 94 + }); 95 + 96 + return ( 97 + <div ref={containerEl} class="group/player relative w-full overflow-hidden bg-black"> 98 + <video ref={videoEl} class="aspect-video w-full" playsinline autoplay /> 99 + 100 + {/* Status overlay */} 101 + <Show when={status() !== "live"}> 102 + <div class="absolute inset-0 flex items-center justify-center bg-black/60"> 103 + <Show when={status() === "connecting"}> 104 + <div class="text-sp-dim flex items-center gap-2"> 105 + <div class="border-sp-dim border-t-sp-accent h-5 w-5 animate-spin rounded-full border-2" /> 106 + Connecting... 107 + </div> 108 + </Show> 109 + <Show when={status() === "error"}> 110 + <div class="text-center"> 111 + <div class="text-sp-red">{errorMsg() || "Error"}</div> 112 + <button 113 + class="bg-sp-surface text-sp-text hover:bg-sp-border mt-2 rounded-sm px-3 py-1.5 text-sm transition-colors" 114 + onClick={connect} 115 + > 116 + Retry 117 + </button> 118 + </div> 119 + </Show> 120 + <Show when={status() === "idle"}> 121 + <div class="text-sp-dim">Stream offline</div> 122 + </Show> 123 + </div> 124 + </Show> 125 + 126 + {/* Controls */} 127 + <div class="absolute right-0 bottom-0 left-0 flex items-center gap-2 bg-black/60 p-3 opacity-0 transition-opacity group-hover/player:opacity-100"> 128 + <Show when={status() === "live"}> 129 + <span class="bg-sp-accent mr-1 flex items-center gap-1.5 rounded-sm px-1.5 py-0.5 text-xs font-medium text-black"> 130 + LIVE 131 + </span> 132 + </Show> 133 + <div class="flex-1" /> 134 + <button 135 + class="rounded-sm p-1.5 text-white/70 transition-colors hover:text-white" 136 + onClick={toggleMute} 137 + title={muted() ? "Unmute" : "Mute"} 138 + > 139 + {muted() ? <VolumeOff size={18} /> : <Volume2 size={18} />} 140 + </button> 141 + <input 142 + type="range" 143 + min="0" 144 + max="1" 145 + step="0.01" 146 + value={muted() ? 0 : volume()} 147 + onInput={(e) => handleVolume(parseFloat(e.currentTarget.value))} 148 + class="h-1 w-20 cursor-pointer appearance-none rounded-full bg-white/30 accent-white" 149 + /> 150 + <button 151 + class="rounded-sm p-1.5 text-white/70 transition-colors hover:text-white" 152 + onClick={toggleFullscreen} 153 + title="Fullscreen" 154 + > 155 + <Maximize size={18} /> 156 + </button> 157 + </div> 158 + </div> 159 + ); 160 + }
+35
src/index.css
··· 1 + @import "tailwindcss"; 2 + 3 + @theme { 4 + --font-sans: "DM Sans", sans-serif; 5 + 6 + --color-sp-bg: #131316; 7 + --color-sp-surface: #1c1c20; 8 + --color-sp-border: #222228; 9 + --color-sp-text: #f0f0f4; 10 + --color-sp-dim: #8a8a96; 11 + --color-sp-accent: #c4b5fd; 12 + --color-sp-accent-dim: #c4b5fd15; 13 + --color-sp-red: #f87171; 14 + --color-sp-red-dim: #f8717118; 15 + --color-sp-hover: #242428; 16 + } 17 + 18 + html { 19 + scrollbar-gutter: stable both-edges; 20 + } 21 + 22 + * { 23 + scrollbar-width: thin; 24 + scrollbar-color: var(--color-sp-border) transparent; 25 + } 26 + 27 + @keyframes pulse-live { 28 + 0%, 29 + 100% { 30 + opacity: 1; 31 + } 32 + 50% { 33 + opacity: 0.6; 34 + } 35 + }
+19
src/index.tsx
··· 1 + /* @refresh reload */ 2 + import { Route, Router } from "@solidjs/router"; 3 + import { render } from "solid-js/web"; 4 + 5 + import { Layout } from "./layout"; 6 + import { Home } from "./pages/Home"; 7 + import { Watch } from "./pages/Watch"; 8 + 9 + import "./index.css"; 10 + 11 + render( 12 + () => ( 13 + <Router root={Layout}> 14 + <Route path="/" component={Home} /> 15 + <Route path="/:handle" component={Watch} /> 16 + </Router> 17 + ), 18 + document.getElementById("root")!, 19 + );
+19
src/layout.tsx
··· 1 + import { type ParentProps, onMount } from "solid-js"; 2 + 3 + import { initAuth } from "./auth/session-manager"; 4 + import { Header } from "./components/Header"; 5 + 6 + export function Layout(props: ParentProps) { 7 + onMount(() => { 8 + initAuth().catch((err) => { 9 + console.warn("Auth init failed:", err); 10 + }); 11 + }); 12 + 13 + return ( 14 + <div class="flex h-dvh flex-col"> 15 + <Header /> 16 + <main class="flex min-h-0 flex-1 flex-col">{props.children}</main> 17 + </div> 18 + ); 19 + }
+85
src/lib/api.ts
··· 1 + import "@atcute/atproto"; 2 + import { type DidDocument, getPdsEndpoint, isAtprotoDid } from "@atcute/identity"; 3 + import { 4 + AtprotoWebDidDocumentResolver, 5 + CompositeDidDocumentResolver, 6 + CompositeHandleResolver, 7 + DohJsonHandleResolver, 8 + PlcDidDocumentResolver, 9 + WellKnownHandleResolver, 10 + } from "@atcute/identity-resolver"; 11 + 12 + export const didDocumentResolver = new CompositeDidDocumentResolver({ 13 + methods: { 14 + plc: new PlcDidDocumentResolver(), 15 + web: new AtprotoWebDidDocumentResolver(), 16 + }, 17 + }); 18 + 19 + export const handleResolver = new CompositeHandleResolver({ 20 + strategy: "dns-first", 21 + methods: { 22 + dns: new DohJsonHandleResolver({ dohUrl: "https://cloudflare-dns.com/dns-query" }), 23 + http: new WellKnownHandleResolver(), 24 + }, 25 + }); 26 + 27 + export const resolveHandle = async (handle: string): Promise<string> => { 28 + return await handleResolver.resolve(handle as `${string}.${string}`); 29 + }; 30 + 31 + export const resolveDidDoc = async (did: string): Promise<DidDocument> => { 32 + if (!isAtprotoDid(did)) { 33 + throw new Error("Not a valid DID identifier"); 34 + } 35 + return await didDocumentResolver.resolve(did); 36 + }; 37 + 38 + const didPDSCache: Record<string, Promise<string>> = {}; 39 + 40 + export const getPDS = (did: string): Promise<string> => { 41 + if (did in didPDSCache) return didPDSCache[did]; 42 + 43 + if (!isAtprotoDid(did)) { 44 + return Promise.reject(new Error("Not a valid DID identifier")); 45 + } 46 + 47 + didPDSCache[did] = (async () => { 48 + const doc = await didDocumentResolver.resolve(did); 49 + const pds = getPdsEndpoint(doc); 50 + if (!pds) { 51 + delete didPDSCache[did]; 52 + throw new Error("No PDS found"); 53 + } 54 + return pds; 55 + })(); 56 + 57 + return didPDSCache[did]; 58 + }; 59 + 60 + export interface LiveUser { 61 + did: string; 62 + handle: string; 63 + title: string; 64 + viewerCount: number; 65 + thumbRef?: string; 66 + } 67 + 68 + // eslint-disable-next-line @typescript-eslint/no-explicit-any 69 + function mapStream(stream: any): LiveUser { 70 + return { 71 + did: stream.author?.did || "", 72 + handle: stream.author?.handle || "unknown", 73 + title: stream.record?.title || "Untitled stream", 74 + viewerCount: stream.viewerCount?.count ?? 0, 75 + thumbRef: stream.record?.thumb?.ref?.$link, 76 + }; 77 + } 78 + 79 + export const fetchLiveUsers = async (): Promise<LiveUser[]> => { 80 + const res = await fetch("https://stream.place/xrpc/place.stream.live.getLiveUsers"); 81 + if (!res.ok) throw new Error("Failed to fetch live users"); 82 + const data = await res.json(); 83 + const streams = data.streams || []; 84 + return streams.map(mapStream); 85 + };
+266
src/lib/chat.ts
··· 1 + import { Client } from "@atcute/client"; 2 + import { OAuthUserAgent } from "@atcute/oauth-browser-client"; 3 + 4 + export interface Facet { 5 + index: { byteStart: number; byteEnd: number }; 6 + features: Array< 7 + | { $type: "app.bsky.richtext.facet#mention"; did: string } 8 + | { $type: "app.bsky.richtext.facet#link"; uri: string } 9 + >; 10 + } 11 + 12 + export interface ChatMessage { 13 + uri: string; 14 + cid: string; 15 + author: { handle: string; did: string }; 16 + record: { 17 + text: string; 18 + facets?: Facet[]; 19 + reply?: { parent?: { uri: string; cid: string }; root?: { uri: string; cid: string } }; 20 + }; 21 + chatProfile?: { color?: { red: number; green: number; blue: number } }; 22 + indexedAt: string; 23 + badges?: Array<{ badgeType: string }>; 24 + replyTo?: ChatMessage; 25 + } 26 + 27 + export interface RichTextSegment { 28 + text: string; 29 + facet?: Facet; 30 + } 31 + 32 + export function segmentRichText(text: string, facets?: Facet[]): RichTextSegment[] { 33 + if (!facets || facets.length === 0) { 34 + return [{ text }]; 35 + } 36 + 37 + const encoder = new TextEncoder(); 38 + const decoder = new TextDecoder(); 39 + const bytes = encoder.encode(text); 40 + 41 + // Sort facets by byteStart 42 + const sorted = [...facets].sort((a, b) => a.index.byteStart - b.index.byteStart); 43 + 44 + const segments: RichTextSegment[] = []; 45 + let cursor = 0; 46 + 47 + for (const facet of sorted) { 48 + const { byteStart, byteEnd } = facet.index; 49 + if (byteStart < cursor || byteEnd > bytes.length) continue; 50 + 51 + // Text before this facet 52 + if (byteStart > cursor) { 53 + segments.push({ text: decoder.decode(bytes.slice(cursor, byteStart)) }); 54 + } 55 + 56 + // The facet text 57 + segments.push({ 58 + text: decoder.decode(bytes.slice(byteStart, byteEnd)), 59 + facet, 60 + }); 61 + 62 + cursor = byteEnd; 63 + } 64 + 65 + // Remaining text after last facet 66 + if (cursor < bytes.length) { 67 + segments.push({ text: decoder.decode(bytes.slice(cursor)) }); 68 + } 69 + 70 + return segments; 71 + } 72 + 73 + export interface StreamInfo { 74 + title: string; 75 + handle: string; 76 + } 77 + 78 + export interface ChatCallbacks { 79 + onMessage: (msg: ChatMessage) => void; 80 + onStreamInfo: (info: StreamInfo) => void; 81 + onViewerCount: (count: number) => void; 82 + onOpen: () => void; 83 + onClose: () => void; 84 + } 85 + 86 + export interface ChatConnection { 87 + close(): void; 88 + } 89 + 90 + export function connectChatWs(handle: string, callbacks: ChatCallbacks): ChatConnection { 91 + const wsUrl = `wss://stream.place/api/websocket/${encodeURIComponent(handle)}`; 92 + let ws: WebSocket | undefined; 93 + let closed = false; 94 + let retryDelay = 1000; 95 + let retryTimer: ReturnType<typeof setTimeout> | undefined; 96 + 97 + function connect() { 98 + if (closed) return; 99 + ws = new WebSocket(wsUrl); 100 + 101 + ws.onopen = () => { 102 + retryDelay = 1000; 103 + callbacks.onOpen(); 104 + }; 105 + 106 + ws.onclose = () => { 107 + callbacks.onClose(); 108 + scheduleReconnect(); 109 + }; 110 + 111 + ws.onerror = () => { 112 + ws?.close(); 113 + }; 114 + 115 + ws.onmessage = (event) => { 116 + try { 117 + const data = JSON.parse(event.data); 118 + 119 + if (data.$type === "place.stream.chat.defs#messageView") { 120 + callbacks.onMessage(data as ChatMessage); 121 + } else if (data.$type === "place.stream.livestream#livestreamView") { 122 + callbacks.onStreamInfo({ 123 + title: data.record?.title || "", 124 + handle: data.author?.handle || "", 125 + }); 126 + } else if (data.$type === "place.stream.livestream#viewerCount") { 127 + callbacks.onViewerCount(data.count ?? 0); 128 + } 129 + } catch { 130 + // ignore non-JSON messages 131 + } 132 + }; 133 + } 134 + 135 + function scheduleReconnect() { 136 + if (closed) return; 137 + retryTimer = setTimeout(() => { 138 + retryDelay = Math.min(retryDelay * 2, 30000); 139 + connect(); 140 + }, retryDelay); 141 + } 142 + 143 + connect(); 144 + 145 + return { 146 + close() { 147 + closed = true; 148 + clearTimeout(retryTimer); 149 + ws?.close(); 150 + }, 151 + }; 152 + } 153 + 154 + // URL regex - matches http:// and https:// URLs 155 + const URL_RE = /https?:\/\/[^\s\])<>]+/g; 156 + // Mention regex - matches @handle.tld patterns 157 + const MENTION_RE = /(?<![.\w])@([\w.-]+\.[\w.-]+)/g; 158 + 159 + export function detectFacets(text: string): Facet[] { 160 + const encoder = new TextEncoder(); 161 + const bytes = encoder.encode(text); 162 + const facets: Facet[] = []; 163 + 164 + // We need byte offsets, so we convert char indices to byte indices 165 + const charToByteOffset = (charIdx: number): number => { 166 + return encoder.encode(text.slice(0, charIdx)).byteLength; 167 + }; 168 + 169 + // Detect URLs 170 + for (const match of text.matchAll(URL_RE)) { 171 + const start = match.index!; 172 + // Trim trailing punctuation that's likely not part of the URL 173 + let uri = match[0]; 174 + while (uri.length > 0 && /[.,;:!?)}\]'"]+$/.test(uri)) { 175 + uri = uri.slice(0, -1); 176 + } 177 + const byteStart = charToByteOffset(start); 178 + const byteEnd = charToByteOffset(start + uri.length); 179 + if (byteEnd <= bytes.byteLength) { 180 + facets.push({ 181 + index: { byteStart, byteEnd }, 182 + features: [{ $type: "app.bsky.richtext.facet#link", uri }], 183 + }); 184 + } 185 + } 186 + 187 + // Detect mentions 188 + for (const match of text.matchAll(MENTION_RE)) { 189 + const start = match.index!; 190 + const end = start + match[0].length; 191 + const byteStart = charToByteOffset(start); 192 + const byteEnd = charToByteOffset(end); 193 + if (byteEnd <= bytes.byteLength) { 194 + // Store the handle; the DID will need to be resolved before sending 195 + facets.push({ 196 + index: { byteStart, byteEnd }, 197 + features: [{ $type: "app.bsky.richtext.facet#mention", did: match[1] }], 198 + }); 199 + } 200 + } 201 + 202 + return facets.sort((a, b) => a.index.byteStart - b.index.byteStart); 203 + } 204 + 205 + export interface ReplyRef { 206 + uri: string; 207 + cid: string; 208 + } 209 + 210 + export async function sendChatMessage( 211 + oauthAgent: OAuthUserAgent, 212 + repo: string, 213 + streamerDid: string, 214 + text: string, 215 + resolveHandle?: (handle: string) => Promise<string>, 216 + reply?: { root: ReplyRef; parent: ReplyRef }, 217 + ): Promise<void> { 218 + const rpc = new Client({ handler: oauthAgent }); 219 + 220 + let facets = detectFacets(text); 221 + 222 + // Resolve mention handles to DIDs 223 + if (resolveHandle) { 224 + facets = await Promise.all( 225 + facets.map(async (facet) => { 226 + const feature = facet.features[0]; 227 + if (feature.$type === "app.bsky.richtext.facet#mention") { 228 + try { 229 + const did = await resolveHandle(feature.did); 230 + return { 231 + ...facet, 232 + features: [{ $type: "app.bsky.richtext.facet#mention" as const, did }], 233 + }; 234 + } catch { 235 + // If resolution fails, drop the facet 236 + return null; 237 + } 238 + } 239 + return facet; 240 + }), 241 + ).then((results) => results.filter((f): f is Facet => f !== null)); 242 + } 243 + 244 + const record: Record<string, unknown> = { 245 + $type: "place.stream.chat.message", 246 + text, 247 + streamer: streamerDid, 248 + createdAt: new Date().toISOString(), 249 + }; 250 + 251 + if (facets.length > 0) { 252 + record.facets = facets; 253 + } 254 + 255 + if (reply) { 256 + record.reply = reply; 257 + } 258 + 259 + await rpc.post("com.atproto.repo.createRecord", { 260 + input: { 261 + repo: repo as `did:${string}:${string}`, 262 + collection: "place.stream.chat.message", 263 + record, 264 + }, 265 + }); 266 + }
+65
src/lib/whep.ts
··· 1 + function waitForIceGathering(pc: RTCPeerConnection, timeout: number): Promise<void> { 2 + return new Promise((resolve) => { 3 + if (pc.iceGatheringState === "complete") { 4 + resolve(); 5 + return; 6 + } 7 + const timer = setTimeout(() => resolve(), timeout); 8 + pc.onicegatheringstatechange = () => { 9 + if (pc.iceGatheringState === "complete") { 10 + clearTimeout(timer); 11 + resolve(); 12 + } 13 + }; 14 + }); 15 + } 16 + 17 + export interface WhepConnection { 18 + pc: RTCPeerConnection; 19 + stream: MediaStream; 20 + } 21 + 22 + export async function connectWhep(handle: string): Promise<WhepConnection> { 23 + const whepUrl = `https://stream.place/api/playback/${encodeURIComponent(handle)}/webrtc?rendition=source`; 24 + 25 + const pc = new RTCPeerConnection({ 26 + iceServers: [{ urls: "stun:stun.l.google.com:19302" }], 27 + bundlePolicy: "max-bundle", 28 + }); 29 + 30 + const stream = new MediaStream(); 31 + 32 + pc.addTransceiver("video", { direction: "recvonly" }); 33 + pc.addTransceiver("audio", { direction: "recvonly" }); 34 + 35 + pc.ontrack = (event) => { 36 + if (event.streams && event.streams[0]) { 37 + for (const track of event.streams[0].getTracks()) { 38 + stream.addTrack(track); 39 + } 40 + } else { 41 + stream.addTrack(event.track); 42 + } 43 + }; 44 + 45 + const offer = await pc.createOffer(); 46 + await pc.setLocalDescription(offer); 47 + await waitForIceGathering(pc, 500); 48 + 49 + const resp = await fetch(whepUrl, { 50 + method: "POST", 51 + headers: { "Content-Type": "application/sdp" }, 52 + body: pc.localDescription!.sdp, 53 + }); 54 + 55 + if (!resp.ok) { 56 + pc.close(); 57 + const errText = await resp.text(); 58 + throw new Error(`WHEP ${resp.status}: ${errText}`); 59 + } 60 + 61 + const answerSdp = await resp.text(); 62 + await pc.setRemoteDescription({ type: "answer", sdp: answerSdp }); 63 + 64 + return { pc, stream }; 65 + }
+100
src/pages/Home.tsx
··· 1 + import { For, onCleanup, onMount, Show } from "solid-js"; 2 + import { createStore, reconcile } from "solid-js/store"; 3 + 4 + import { StreamCard } from "../components/StreamCard"; 5 + import { fetchLiveUsers, type LiveUser } from "../lib/api"; 6 + 7 + const POLL_INTERVAL = 15_000; 8 + 9 + const avatarCache: Record<string, string> = {}; 10 + 11 + async function fetchNewAvatars(handles: string[]): Promise<void> { 12 + const missing = handles.filter((h) => !(h in avatarCache)); 13 + if (!missing.length) return; 14 + try { 15 + const params = missing.map((h) => `actors=${encodeURIComponent(h)}`).join("&"); 16 + const res = await fetch( 17 + `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfiles?${params}`, 18 + ); 19 + if (!res.ok) return; 20 + const data = await res.json(); 21 + for (const profile of data.profiles || []) { 22 + if (profile.avatar) { 23 + avatarCache[profile.handle] = profile.avatar; 24 + } 25 + } 26 + } catch { 27 + // ignore 28 + } 29 + } 30 + 31 + interface StreamEntry extends LiveUser { 32 + avatarUrl?: string; 33 + } 34 + 35 + export function Home() { 36 + const [streams, setStreams] = createStore<StreamEntry[]>([]); 37 + const [state, setState] = createStore({ loading: true }); 38 + 39 + const refresh = async () => { 40 + try { 41 + const users = await fetchLiveUsers(); 42 + users.sort((a, b) => (b.viewerCount ?? 0) - (a.viewerCount ?? 0)); 43 + 44 + const handles = users.map((s) => s.handle).filter(Boolean); 45 + await fetchNewAvatars(handles); 46 + 47 + const entries: StreamEntry[] = users.map((s) => ({ 48 + ...s, 49 + avatarUrl: avatarCache[s.handle], 50 + })); 51 + 52 + setStreams(reconcile(entries, { key: "did", merge: false })); 53 + } catch (err) { 54 + console.error("Failed to fetch streams:", err); 55 + } finally { 56 + setState("loading", false); 57 + } 58 + }; 59 + 60 + onMount(() => { 61 + refresh(); 62 + }); 63 + 64 + const interval = setInterval(refresh, POLL_INTERVAL); 65 + onCleanup(() => clearInterval(interval)); 66 + 67 + return ( 68 + <div class="mx-auto w-full max-w-6xl p-6"> 69 + <Show 70 + when={!state.loading} 71 + fallback={ 72 + <div class="text-sp-dim flex items-center gap-2"> 73 + <div class="border-sp-dim border-t-sp-accent h-4 w-4 animate-spin rounded-full border-2" /> 74 + Loading streams... 75 + </div> 76 + } 77 + > 78 + <Show 79 + when={streams.length > 0} 80 + fallback={<p class="text-sp-dim">No one is live right now. Check back later!</p>} 81 + > 82 + <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3"> 83 + <For each={streams}> 84 + {(stream) => ( 85 + <StreamCard 86 + handle={stream.handle} 87 + did={stream.did} 88 + title={stream.title} 89 + viewerCount={stream.viewerCount} 90 + avatarUrl={stream.avatarUrl} 91 + thumbRef={stream.thumbRef} 92 + /> 93 + )} 94 + </For> 95 + </div> 96 + </Show> 97 + </Show> 98 + </div> 99 + ); 100 + }
+62
src/pages/Watch.tsx
··· 1 + import { useParams } from "@solidjs/router"; 2 + import { createResource, createSignal, Show } from "solid-js"; 3 + 4 + import { Chat } from "../components/Chat"; 5 + import { VideoPlayer } from "../components/VideoPlayer"; 6 + import { resolveHandle } from "../lib/api"; 7 + import type { StreamInfo } from "../lib/chat"; 8 + 9 + export function Watch() { 10 + const params = useParams<{ handle: string }>(); 11 + 12 + const [streamerDid] = createResource( 13 + () => params.handle, 14 + async (handle) => { 15 + try { 16 + return await resolveHandle(handle); 17 + } catch (err) { 18 + console.error("Failed to resolve handle:", err); 19 + return undefined; 20 + } 21 + }, 22 + ); 23 + 24 + const [streamInfo, setStreamInfo] = createSignal<StreamInfo | undefined>(); 25 + const [viewerCount, setViewerCount] = createSignal(0); 26 + 27 + return ( 28 + <div class="flex min-h-0 flex-1 flex-col overflow-hidden lg:flex-row"> 29 + {/* Main content - video + info */} 30 + <div class="flex min-w-0 flex-1 flex-col pb-4"> 31 + <VideoPlayer handle={params.handle} /> 32 + 33 + {/* Stream info bar */} 34 + <div class="mt-3 flex items-start justify-between gap-4 px-4"> 35 + <div class="min-w-0"> 36 + <h1 class="truncate text-lg font-semibold"> 37 + {streamInfo()?.title || `@${params.handle}`} 38 + </h1> 39 + <Show when={streamInfo()?.title}> 40 + <div class="text-sp-dim text-sm">@{params.handle}</div> 41 + </Show> 42 + </div> 43 + <div class="text-sp-dim flex shrink-0 items-center gap-2 text-base"> 44 + <span class="bg-sp-accent inline-block h-2.5 w-2.5 rounded-full" /> 45 + {viewerCount()} watching 46 + </div> 47 + </div> 48 + </div> 49 + 50 + {/* Chat sidebar */} 51 + <div class="flex min-h-0 w-full flex-col lg:w-105"> 52 + <Chat 53 + class="flex-1" 54 + handle={params.handle} 55 + streamerDid={streamerDid()} 56 + onStreamInfo={setStreamInfo} 57 + onViewerCount={setViewerCount} 58 + /> 59 + </div> 60 + </div> 61 + ); 62 + }
+12
src/vite-env.d.ts
··· 1 + /// <reference types="vite/client" /> 2 + 3 + interface ImportMetaEnv { 4 + readonly VITE_CLIENT_URI: string; 5 + readonly VITE_OAUTH_CLIENT_ID: string; 6 + readonly VITE_OAUTH_REDIRECT_URL: string; 7 + readonly VITE_OAUTH_SCOPE: string; 8 + } 9 + 10 + interface ImportMeta { 11 + readonly env: ImportMetaEnv; 12 + }
+26
tsconfig.app.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "ESNext", 4 + "useDefineForClassFields": true, 5 + "module": "ESNext", 6 + "lib": ["ESNext", "DOM", "DOM.Iterable"], 7 + "types": [], 8 + "skipLibCheck": true, 9 + 10 + /* Bundler mode */ 11 + "moduleResolution": "bundler", 12 + "allowImportingTsExtensions": true, 13 + "isolatedModules": true, 14 + "moduleDetection": "force", 15 + "noEmit": true, 16 + "jsx": "preserve", 17 + "jsxImportSource": "solid-js", 18 + 19 + /* Linting */ 20 + "strict": true, 21 + "noUnusedLocals": true, 22 + "noUnusedParameters": true, 23 + "noFallthroughCasesInSwitch": true 24 + }, 25 + "include": ["src"] 26 + }
+4
tsconfig.json
··· 1 + { 2 + "files": [], 3 + "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }] 4 + }
+23
tsconfig.node.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "ESNext", 4 + "lib": ["ESNext"], 5 + "types": ["node"], 6 + "module": "ESNext", 7 + "skipLibCheck": true, 8 + 9 + /* Bundler mode */ 10 + "moduleResolution": "bundler", 11 + "allowImportingTsExtensions": true, 12 + "isolatedModules": true, 13 + "moduleDetection": "force", 14 + "noEmit": true, 15 + 16 + /* Linting */ 17 + "strict": true, 18 + "noUnusedLocals": true, 19 + "noUnusedParameters": true, 20 + "noFallthroughCasesInSwitch": true 21 + }, 22 + "include": ["vite.config.ts"] 23 + }
+44
vite.config.ts
··· 1 + import tailwindcss from "@tailwindcss/vite"; 2 + import { defineConfig } from "vite"; 3 + import solidPlugin from "vite-plugin-solid"; 4 + 5 + import metadata from "./public/oauth-client-metadata.json"; 6 + 7 + const SERVER_HOST = "127.0.0.1"; 8 + const SERVER_PORT = 5173; 9 + 10 + export default defineConfig({ 11 + plugins: [ 12 + tailwindcss(), 13 + solidPlugin(), 14 + { 15 + name: "oauth", 16 + config(_conf, { command }) { 17 + if (command === "build") { 18 + process.env.VITE_OAUTH_CLIENT_ID = metadata.client_id; 19 + process.env.VITE_OAUTH_REDIRECT_URL = metadata.redirect_uris[0]; 20 + } else { 21 + const redirectUri = `http://${SERVER_HOST}:${SERVER_PORT}/`; 22 + 23 + const clientId = 24 + `http://localhost` + 25 + `?redirect_uri=${encodeURIComponent(redirectUri)}` + 26 + `&scope=${encodeURIComponent(metadata.scope)}`; 27 + 28 + process.env.VITE_OAUTH_CLIENT_ID = clientId; 29 + process.env.VITE_OAUTH_REDIRECT_URL = redirectUri; 30 + } 31 + 32 + process.env.VITE_CLIENT_URI = metadata.client_uri; 33 + process.env.VITE_OAUTH_SCOPE = metadata.scope; 34 + }, 35 + }, 36 + ], 37 + server: { 38 + host: SERVER_HOST, 39 + port: SERVER_PORT, 40 + }, 41 + build: { 42 + target: "esnext", 43 + }, 44 + });