JavaScript-optional public web frontend for Bluesky anartia.kelinci.net
sveltekit atcute bluesky typescript svelte

initial commit

mary.my.id c0b6f643

Changed files
+6206
src
static
+3
.env
··· 1 + PUBLIC_APP_NAME=Anartia 2 + 3 + PUBLIC_APPVIEW_URL=https://public.api.bsky.app
+15
.gitignore
··· 1 + node_modules 2 + 3 + .output 4 + .vercel 5 + .netlify 6 + .wrangler 7 + /.svelte-kit 8 + /build 9 + 10 + *.local 11 + .idea 12 + .DS_Store 13 + Thumbs.db 14 + 15 + vite.config.*.timestamp-*
+1
.npmrc
··· 1 + engine-strict=true
+4
.prettierignore
··· 1 + # Package Managers 2 + package-lock.json 3 + pnpm-lock.yaml 4 + yarn.lock
+31
.prettierrc
··· 1 + { 2 + "trailingComma": "all", 3 + "useTabs": true, 4 + "tabWidth": 2, 5 + "printWidth": 110, 6 + "semi": true, 7 + "singleQuote": true, 8 + "bracketSpacing": true, 9 + "plugins": ["prettier-plugin-svelte", "prettier-plugin-css-order"], 10 + "overrides": [ 11 + { 12 + "files": "*.svelte", 13 + "options": { 14 + "parser": "svelte" 15 + } 16 + }, 17 + { 18 + "files": ["tsconfig.json", "jsconfig.json", "tsconfig.*.json"], 19 + "options": { 20 + "parser": "jsonc" 21 + } 22 + }, 23 + { 24 + "files": ["*.md"], 25 + "options": { 26 + "printWidth": 100, 27 + "proseWrap": "always" 28 + } 29 + } 30 + ] 31 + }
+3
README.md
··· 1 + # Anartia 2 + 3 + JavaScript-optional public web frontend for Bluesky.
+35
package.json
··· 1 + { 2 + "private": true, 3 + "name": "anartia", 4 + "type": "module", 5 + "scripts": { 6 + "dev": "vite dev", 7 + "build": "vite build", 8 + "preview": "vite preview", 9 + "prepare": "svelte-kit sync || echo ''", 10 + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 11 + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 12 + "format": "prettier --write .", 13 + "lint": "prettier --check ." 14 + }, 15 + "devDependencies": { 16 + "@sveltejs/adapter-cloudflare": "^5.0.2", 17 + "@sveltejs/kit": "^2.17.1", 18 + "@sveltejs/vite-plugin-svelte": "^5.0.3", 19 + "prettier": "^3.4.2", 20 + "prettier-plugin-css-order": "^2.1.2", 21 + "prettier-plugin-svelte": "^3.3.3", 22 + "svelte": "^5.19.8", 23 + "svelte-check": "^4.1.4", 24 + "typescript": "~5.7.3", 25 + "vite": "^6.1.0", 26 + "wrangler": "^3.107.3" 27 + }, 28 + "dependencies": { 29 + "@atcute/bluesky": "^1.0.12", 30 + "@atcute/bluesky-richtext-parser": "^1.0.7", 31 + "@atcute/bluesky-richtext-segmenter": "^1.0.5", 32 + "@atcute/client": "^2.0.7", 33 + "@badrap/valita": "^0.4.2" 34 + } 35 + }
+1706
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/bluesky': 12 + specifier: ^1.0.12 13 + version: 1.0.12(@atcute/client@2.0.7) 14 + '@atcute/bluesky-richtext-parser': 15 + specifier: ^1.0.7 16 + version: 1.0.7 17 + '@atcute/bluesky-richtext-segmenter': 18 + specifier: ^1.0.5 19 + version: 1.0.5(@atcute/bluesky@1.0.12(@atcute/client@2.0.7))(@atcute/client@2.0.7) 20 + '@atcute/client': 21 + specifier: ^2.0.7 22 + version: 2.0.7 23 + '@badrap/valita': 24 + specifier: ^0.4.2 25 + version: 0.4.2 26 + devDependencies: 27 + '@sveltejs/adapter-cloudflare': 28 + specifier: ^5.0.2 29 + version: 5.0.2(@sveltejs/kit@2.17.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.19.8)(vite@6.1.0))(svelte@5.19.8)(vite@6.1.0))(wrangler@3.107.3(@cloudflare/workers-types@4.20250204.0)) 30 + '@sveltejs/kit': 31 + specifier: ^2.17.1 32 + version: 2.17.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.19.8)(vite@6.1.0))(svelte@5.19.8)(vite@6.1.0) 33 + '@sveltejs/vite-plugin-svelte': 34 + specifier: ^5.0.3 35 + version: 5.0.3(svelte@5.19.8)(vite@6.1.0) 36 + prettier: 37 + specifier: ^3.4.2 38 + version: 3.4.2 39 + prettier-plugin-css-order: 40 + specifier: ^2.1.2 41 + version: 2.1.2(postcss@8.5.1)(prettier@3.4.2) 42 + prettier-plugin-svelte: 43 + specifier: ^3.3.3 44 + version: 3.3.3(prettier@3.4.2)(svelte@5.19.8) 45 + svelte: 46 + specifier: ^5.19.8 47 + version: 5.19.8 48 + svelte-check: 49 + specifier: ^4.1.4 50 + version: 4.1.4(svelte@5.19.8)(typescript@5.7.3) 51 + typescript: 52 + specifier: ~5.7.3 53 + version: 5.7.3 54 + vite: 55 + specifier: ^6.1.0 56 + version: 6.1.0 57 + wrangler: 58 + specifier: ^3.107.3 59 + version: 3.107.3(@cloudflare/workers-types@4.20250204.0) 60 + 61 + packages: 62 + 63 + '@ampproject/remapping@2.3.0': 64 + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} 65 + engines: {node: '>=6.0.0'} 66 + 67 + '@atcute/bluesky-richtext-parser@1.0.7': 68 + resolution: {integrity: sha512-nOvU699OXiGMbyswao7JJnY0C9WkwE7PVC/m5WWt0UN9fsXSOor9IZWw+v9SATp+94BTJoG38XyUomUaJnoQRA==} 69 + 70 + '@atcute/bluesky-richtext-segmenter@1.0.5': 71 + resolution: {integrity: sha512-D0FfmJVwppky9naL1OGKcQjtgO0lDLhkG4iCQHpShuHhEZ9FUdf3eUb/eQpVRNJGaJ4ShjEOHA6FlAizcjMkGQ==} 72 + peerDependencies: 73 + '@atcute/bluesky': ^1.0.0 74 + '@atcute/client': ^1.0.0 || ^2.0.0 75 + 76 + '@atcute/bluesky@1.0.12': 77 + resolution: {integrity: sha512-oUM+MxD5asGYyQDOHBGay7f9ryhsBpQ8LTUmsEZvp4t/WG0ZV2AcFRWsG0DxB+CsmSTbP2DHLMZCatE3usmt+g==} 78 + peerDependencies: 79 + '@atcute/client': ^1.0.0 || ^2.0.0 80 + 81 + '@atcute/client@2.0.7': 82 + resolution: {integrity: sha512-bvNahrCGvhZw/EIx0HU/GOoKZEnUaAppbuZh7cu+VsOFA2tdFLnZJed9Hagh5Yz/eUX7QUh5NB4dRTRUdggSLQ==} 83 + 84 + '@badrap/valita@0.4.2': 85 + resolution: {integrity: sha512-Mwmr7k2iK0Yy0POLnAFUgab2mxKYeIsYXHY7sg3jo8XFsFHbG0SBmTcktXD0uW8N4WZePKf8s68QV7QDTGSdHA==} 86 + engines: {node: '>= 18'} 87 + 88 + '@cloudflare/kv-asset-handler@0.3.4': 89 + resolution: {integrity: sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==} 90 + engines: {node: '>=16.13'} 91 + 92 + '@cloudflare/workerd-darwin-64@1.20250129.0': 93 + resolution: {integrity: sha512-M+xETVnl+xy2dfDDWmp0XXr2rttl70a6bljQygl0EmYmNswFTcYbQWCaBuNBo9kabU59rLKr4a/b3QZ07NoL/g==} 94 + engines: {node: '>=16'} 95 + cpu: [x64] 96 + os: [darwin] 97 + 98 + '@cloudflare/workerd-darwin-arm64@1.20250129.0': 99 + resolution: {integrity: sha512-c4PQUyIMp+bCMxZkAMBzXgTHjRZxeYCujDbb3staestqgRbenzcfauXsMd6np35ng+EE1uBgHNPV4+7fC0ZBfg==} 100 + engines: {node: '>=16'} 101 + cpu: [arm64] 102 + os: [darwin] 103 + 104 + '@cloudflare/workerd-linux-64@1.20250129.0': 105 + resolution: {integrity: sha512-xJx8LwWFxsm5U3DETJwRuOmT5RWBqm4FmA4itYXvcEICca9pWJDB641kT4PnpypwDNmYOebhU7A+JUrCRucG0w==} 106 + engines: {node: '>=16'} 107 + cpu: [x64] 108 + os: [linux] 109 + 110 + '@cloudflare/workerd-linux-arm64@1.20250129.0': 111 + resolution: {integrity: sha512-dR//npbaX5p323huBVNIy5gaWubQx6CC3aiXeK0yX4aD5ar8AjxQFb2U/Sgjeo65Rkt53hJWqC7IwRpK/eOxrA==} 112 + engines: {node: '>=16'} 113 + cpu: [arm64] 114 + os: [linux] 115 + 116 + '@cloudflare/workerd-windows-64@1.20250129.0': 117 + resolution: {integrity: sha512-OeO+1nPj/ocAE3adFar/tRFGRkbCrBnrOYXq0FUBSpyNHpDdA9/U3PAw5CN4zvjfTnqXZfTxTFeqoruqzRzbtg==} 118 + engines: {node: '>=16'} 119 + cpu: [x64] 120 + os: [win32] 121 + 122 + '@cloudflare/workers-types@4.20250204.0': 123 + resolution: {integrity: sha512-mWoQbYaP+nYztx9I7q9sgaiNlT54Cypszz0RfzMxYnT5W3NXDuwGcjGB+5B5H5VB8tEC2dYnBRpa70lX94ueaQ==} 124 + 125 + '@cspotcode/source-map-support@0.8.1': 126 + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} 127 + engines: {node: '>=12'} 128 + 129 + '@esbuild-plugins/node-globals-polyfill@0.2.3': 130 + resolution: {integrity: sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==} 131 + peerDependencies: 132 + esbuild: '*' 133 + 134 + '@esbuild-plugins/node-modules-polyfill@0.2.2': 135 + resolution: {integrity: sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==} 136 + peerDependencies: 137 + esbuild: '*' 138 + 139 + '@esbuild/aix-ppc64@0.24.2': 140 + resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} 141 + engines: {node: '>=18'} 142 + cpu: [ppc64] 143 + os: [aix] 144 + 145 + '@esbuild/android-arm64@0.17.19': 146 + resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==} 147 + engines: {node: '>=12'} 148 + cpu: [arm64] 149 + os: [android] 150 + 151 + '@esbuild/android-arm64@0.24.2': 152 + resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==} 153 + engines: {node: '>=18'} 154 + cpu: [arm64] 155 + os: [android] 156 + 157 + '@esbuild/android-arm@0.17.19': 158 + resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==} 159 + engines: {node: '>=12'} 160 + cpu: [arm] 161 + os: [android] 162 + 163 + '@esbuild/android-arm@0.24.2': 164 + resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==} 165 + engines: {node: '>=18'} 166 + cpu: [arm] 167 + os: [android] 168 + 169 + '@esbuild/android-x64@0.17.19': 170 + resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==} 171 + engines: {node: '>=12'} 172 + cpu: [x64] 173 + os: [android] 174 + 175 + '@esbuild/android-x64@0.24.2': 176 + resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==} 177 + engines: {node: '>=18'} 178 + cpu: [x64] 179 + os: [android] 180 + 181 + '@esbuild/darwin-arm64@0.17.19': 182 + resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==} 183 + engines: {node: '>=12'} 184 + cpu: [arm64] 185 + os: [darwin] 186 + 187 + '@esbuild/darwin-arm64@0.24.2': 188 + resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==} 189 + engines: {node: '>=18'} 190 + cpu: [arm64] 191 + os: [darwin] 192 + 193 + '@esbuild/darwin-x64@0.17.19': 194 + resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==} 195 + engines: {node: '>=12'} 196 + cpu: [x64] 197 + os: [darwin] 198 + 199 + '@esbuild/darwin-x64@0.24.2': 200 + resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==} 201 + engines: {node: '>=18'} 202 + cpu: [x64] 203 + os: [darwin] 204 + 205 + '@esbuild/freebsd-arm64@0.17.19': 206 + resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==} 207 + engines: {node: '>=12'} 208 + cpu: [arm64] 209 + os: [freebsd] 210 + 211 + '@esbuild/freebsd-arm64@0.24.2': 212 + resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==} 213 + engines: {node: '>=18'} 214 + cpu: [arm64] 215 + os: [freebsd] 216 + 217 + '@esbuild/freebsd-x64@0.17.19': 218 + resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==} 219 + engines: {node: '>=12'} 220 + cpu: [x64] 221 + os: [freebsd] 222 + 223 + '@esbuild/freebsd-x64@0.24.2': 224 + resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==} 225 + engines: {node: '>=18'} 226 + cpu: [x64] 227 + os: [freebsd] 228 + 229 + '@esbuild/linux-arm64@0.17.19': 230 + resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==} 231 + engines: {node: '>=12'} 232 + cpu: [arm64] 233 + os: [linux] 234 + 235 + '@esbuild/linux-arm64@0.24.2': 236 + resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==} 237 + engines: {node: '>=18'} 238 + cpu: [arm64] 239 + os: [linux] 240 + 241 + '@esbuild/linux-arm@0.17.19': 242 + resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==} 243 + engines: {node: '>=12'} 244 + cpu: [arm] 245 + os: [linux] 246 + 247 + '@esbuild/linux-arm@0.24.2': 248 + resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==} 249 + engines: {node: '>=18'} 250 + cpu: [arm] 251 + os: [linux] 252 + 253 + '@esbuild/linux-ia32@0.17.19': 254 + resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==} 255 + engines: {node: '>=12'} 256 + cpu: [ia32] 257 + os: [linux] 258 + 259 + '@esbuild/linux-ia32@0.24.2': 260 + resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==} 261 + engines: {node: '>=18'} 262 + cpu: [ia32] 263 + os: [linux] 264 + 265 + '@esbuild/linux-loong64@0.17.19': 266 + resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==} 267 + engines: {node: '>=12'} 268 + cpu: [loong64] 269 + os: [linux] 270 + 271 + '@esbuild/linux-loong64@0.24.2': 272 + resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==} 273 + engines: {node: '>=18'} 274 + cpu: [loong64] 275 + os: [linux] 276 + 277 + '@esbuild/linux-mips64el@0.17.19': 278 + resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==} 279 + engines: {node: '>=12'} 280 + cpu: [mips64el] 281 + os: [linux] 282 + 283 + '@esbuild/linux-mips64el@0.24.2': 284 + resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==} 285 + engines: {node: '>=18'} 286 + cpu: [mips64el] 287 + os: [linux] 288 + 289 + '@esbuild/linux-ppc64@0.17.19': 290 + resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==} 291 + engines: {node: '>=12'} 292 + cpu: [ppc64] 293 + os: [linux] 294 + 295 + '@esbuild/linux-ppc64@0.24.2': 296 + resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==} 297 + engines: {node: '>=18'} 298 + cpu: [ppc64] 299 + os: [linux] 300 + 301 + '@esbuild/linux-riscv64@0.17.19': 302 + resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==} 303 + engines: {node: '>=12'} 304 + cpu: [riscv64] 305 + os: [linux] 306 + 307 + '@esbuild/linux-riscv64@0.24.2': 308 + resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==} 309 + engines: {node: '>=18'} 310 + cpu: [riscv64] 311 + os: [linux] 312 + 313 + '@esbuild/linux-s390x@0.17.19': 314 + resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==} 315 + engines: {node: '>=12'} 316 + cpu: [s390x] 317 + os: [linux] 318 + 319 + '@esbuild/linux-s390x@0.24.2': 320 + resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==} 321 + engines: {node: '>=18'} 322 + cpu: [s390x] 323 + os: [linux] 324 + 325 + '@esbuild/linux-x64@0.17.19': 326 + resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==} 327 + engines: {node: '>=12'} 328 + cpu: [x64] 329 + os: [linux] 330 + 331 + '@esbuild/linux-x64@0.24.2': 332 + resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==} 333 + engines: {node: '>=18'} 334 + cpu: [x64] 335 + os: [linux] 336 + 337 + '@esbuild/netbsd-arm64@0.24.2': 338 + resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==} 339 + engines: {node: '>=18'} 340 + cpu: [arm64] 341 + os: [netbsd] 342 + 343 + '@esbuild/netbsd-x64@0.17.19': 344 + resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==} 345 + engines: {node: '>=12'} 346 + cpu: [x64] 347 + os: [netbsd] 348 + 349 + '@esbuild/netbsd-x64@0.24.2': 350 + resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==} 351 + engines: {node: '>=18'} 352 + cpu: [x64] 353 + os: [netbsd] 354 + 355 + '@esbuild/openbsd-arm64@0.24.2': 356 + resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==} 357 + engines: {node: '>=18'} 358 + cpu: [arm64] 359 + os: [openbsd] 360 + 361 + '@esbuild/openbsd-x64@0.17.19': 362 + resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==} 363 + engines: {node: '>=12'} 364 + cpu: [x64] 365 + os: [openbsd] 366 + 367 + '@esbuild/openbsd-x64@0.24.2': 368 + resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==} 369 + engines: {node: '>=18'} 370 + cpu: [x64] 371 + os: [openbsd] 372 + 373 + '@esbuild/sunos-x64@0.17.19': 374 + resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==} 375 + engines: {node: '>=12'} 376 + cpu: [x64] 377 + os: [sunos] 378 + 379 + '@esbuild/sunos-x64@0.24.2': 380 + resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==} 381 + engines: {node: '>=18'} 382 + cpu: [x64] 383 + os: [sunos] 384 + 385 + '@esbuild/win32-arm64@0.17.19': 386 + resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==} 387 + engines: {node: '>=12'} 388 + cpu: [arm64] 389 + os: [win32] 390 + 391 + '@esbuild/win32-arm64@0.24.2': 392 + resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==} 393 + engines: {node: '>=18'} 394 + cpu: [arm64] 395 + os: [win32] 396 + 397 + '@esbuild/win32-ia32@0.17.19': 398 + resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==} 399 + engines: {node: '>=12'} 400 + cpu: [ia32] 401 + os: [win32] 402 + 403 + '@esbuild/win32-ia32@0.24.2': 404 + resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==} 405 + engines: {node: '>=18'} 406 + cpu: [ia32] 407 + os: [win32] 408 + 409 + '@esbuild/win32-x64@0.17.19': 410 + resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==} 411 + engines: {node: '>=12'} 412 + cpu: [x64] 413 + os: [win32] 414 + 415 + '@esbuild/win32-x64@0.24.2': 416 + resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==} 417 + engines: {node: '>=18'} 418 + cpu: [x64] 419 + os: [win32] 420 + 421 + '@fastify/busboy@2.1.1': 422 + resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} 423 + engines: {node: '>=14'} 424 + 425 + '@jridgewell/gen-mapping@0.3.8': 426 + resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} 427 + engines: {node: '>=6.0.0'} 428 + 429 + '@jridgewell/resolve-uri@3.1.2': 430 + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 431 + engines: {node: '>=6.0.0'} 432 + 433 + '@jridgewell/set-array@1.2.1': 434 + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} 435 + engines: {node: '>=6.0.0'} 436 + 437 + '@jridgewell/sourcemap-codec@1.5.0': 438 + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} 439 + 440 + '@jridgewell/trace-mapping@0.3.25': 441 + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} 442 + 443 + '@jridgewell/trace-mapping@0.3.9': 444 + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} 445 + 446 + '@polka/url@1.0.0-next.28': 447 + resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} 448 + 449 + '@rollup/rollup-android-arm-eabi@4.34.4': 450 + resolution: {integrity: sha512-gGi5adZWvjtJU7Axs//CWaQbQd/vGy8KGcnEaCWiyCqxWYDxwIlAHFuSe6Guoxtd0SRvSfVTDMPd5H+4KE2kKA==} 451 + cpu: [arm] 452 + os: [android] 453 + 454 + '@rollup/rollup-android-arm64@4.34.4': 455 + resolution: {integrity: sha512-1aRlh1gqtF7vNPMnlf1vJKk72Yshw5zknR/ZAVh7zycRAGF2XBMVDAHmFQz/Zws5k++nux3LOq/Ejj1WrDR6xg==} 456 + cpu: [arm64] 457 + os: [android] 458 + 459 + '@rollup/rollup-darwin-arm64@4.34.4': 460 + resolution: {integrity: sha512-drHl+4qhFj+PV/jrQ78p9ch6A0MfNVZScl/nBps5a7u01aGf/GuBRrHnRegA9bP222CBDfjYbFdjkIJ/FurvSQ==} 461 + cpu: [arm64] 462 + os: [darwin] 463 + 464 + '@rollup/rollup-darwin-x64@4.34.4': 465 + resolution: {integrity: sha512-hQqq/8QALU6t1+fbNmm6dwYsa0PDD4L5r3TpHx9dNl+aSEMnIksHZkSO3AVH+hBMvZhpumIGrTFj8XCOGuIXjw==} 466 + cpu: [x64] 467 + os: [darwin] 468 + 469 + '@rollup/rollup-freebsd-arm64@4.34.4': 470 + resolution: {integrity: sha512-/L0LixBmbefkec1JTeAQJP0ETzGjFtNml2gpQXA8rpLo7Md+iXQzo9kwEgzyat5Q+OG/C//2B9Fx52UxsOXbzw==} 471 + cpu: [arm64] 472 + os: [freebsd] 473 + 474 + '@rollup/rollup-freebsd-x64@4.34.4': 475 + resolution: {integrity: sha512-6Rk3PLRK+b8L/M6m/x6Mfj60LhAUcLJ34oPaxufA+CfqkUrDoUPQYFdRrhqyOvtOKXLJZJwxlOLbQjNYQcRQfw==} 476 + cpu: [x64] 477 + os: [freebsd] 478 + 479 + '@rollup/rollup-linux-arm-gnueabihf@4.34.4': 480 + resolution: {integrity: sha512-kmT3x0IPRuXY/tNoABp2nDvI9EvdiS2JZsd4I9yOcLCCViKsP0gB38mVHOhluzx+SSVnM1KNn9k6osyXZhLoCA==} 481 + cpu: [arm] 482 + os: [linux] 483 + 484 + '@rollup/rollup-linux-arm-musleabihf@4.34.4': 485 + resolution: {integrity: sha512-3iSA9tx+4PZcJH/Wnwsvx/BY4qHpit/u2YoZoXugWVfc36/4mRkgGEoRbRV7nzNBSCOgbWMeuQ27IQWgJ7tRzw==} 486 + cpu: [arm] 487 + os: [linux] 488 + 489 + '@rollup/rollup-linux-arm64-gnu@4.34.4': 490 + resolution: {integrity: sha512-7CwSJW+sEhM9sESEk+pEREF2JL0BmyCro8UyTq0Kyh0nu1v0QPNY3yfLPFKChzVoUmaKj8zbdgBxUhBRR+xGxg==} 491 + cpu: [arm64] 492 + os: [linux] 493 + 494 + '@rollup/rollup-linux-arm64-musl@4.34.4': 495 + resolution: {integrity: sha512-GZdafB41/4s12j8Ss2izofjeFXRAAM7sHCb+S4JsI9vaONX/zQ8cXd87B9MRU/igGAJkKvmFmJJBeeT9jJ5Cbw==} 496 + cpu: [arm64] 497 + os: [linux] 498 + 499 + '@rollup/rollup-linux-loongarch64-gnu@4.34.4': 500 + resolution: {integrity: sha512-uuphLuw1X6ur11675c2twC6YxbzyLSpWggvdawTUamlsoUv81aAXRMPBC1uvQllnBGls0Qt5Siw8reSIBnbdqQ==} 501 + cpu: [loong64] 502 + os: [linux] 503 + 504 + '@rollup/rollup-linux-powerpc64le-gnu@4.34.4': 505 + resolution: {integrity: sha512-KvLEw1os2gSmD6k6QPCQMm2T9P2GYvsMZMRpMz78QpSoEevHbV/KOUbI/46/JRalhtSAYZBYLAnT9YE4i/l4vg==} 506 + cpu: [ppc64] 507 + os: [linux] 508 + 509 + '@rollup/rollup-linux-riscv64-gnu@4.34.4': 510 + resolution: {integrity: sha512-wcpCLHGM9yv+3Dql/CI4zrY2mpQ4WFergD3c9cpRowltEh5I84pRT/EuHZsG0In4eBPPYthXnuR++HrFkeqwkA==} 511 + cpu: [riscv64] 512 + os: [linux] 513 + 514 + '@rollup/rollup-linux-s390x-gnu@4.34.4': 515 + resolution: {integrity: sha512-nLbfQp2lbJYU8obhRQusXKbuiqm4jSJteLwfjnunDT5ugBKdxqw1X9KWwk8xp1OMC6P5d0WbzxzhWoznuVK6XA==} 516 + cpu: [s390x] 517 + os: [linux] 518 + 519 + '@rollup/rollup-linux-x64-gnu@4.34.4': 520 + resolution: {integrity: sha512-JGejzEfVzqc/XNiCKZj14eb6s5w8DdWlnQ5tWUbs99kkdvfq9btxxVX97AaxiUX7xJTKFA0LwoS0KU8C2faZRg==} 521 + cpu: [x64] 522 + os: [linux] 523 + 524 + '@rollup/rollup-linux-x64-musl@4.34.4': 525 + resolution: {integrity: sha512-/iFIbhzeyZZy49ozAWJ1ZR2KW6ZdYUbQXLT4O5n1cRZRoTpwExnHLjlurDXXPKEGxiAg0ujaR9JDYKljpr2fDg==} 526 + cpu: [x64] 527 + os: [linux] 528 + 529 + '@rollup/rollup-win32-arm64-msvc@4.34.4': 530 + resolution: {integrity: sha512-qORc3UzoD5UUTneiP2Afg5n5Ti1GAW9Gp5vHPxzvAFFA3FBaum9WqGvYXGf+c7beFdOKNos31/41PRMUwh1tpA==} 531 + cpu: [arm64] 532 + os: [win32] 533 + 534 + '@rollup/rollup-win32-ia32-msvc@4.34.4': 535 + resolution: {integrity: sha512-5g7E2PHNK2uvoD5bASBD9aelm44nf1w4I5FEI7MPHLWcCSrR8JragXZWgKPXk5i2FU3JFfa6CGZLw2RrGBHs2Q==} 536 + cpu: [ia32] 537 + os: [win32] 538 + 539 + '@rollup/rollup-win32-x64-msvc@4.34.4': 540 + resolution: {integrity: sha512-p0scwGkR4kZ242xLPBuhSckrJ734frz6v9xZzD+kHVYRAkSUmdSLCIJRfql6H5//aF8Q10K+i7q8DiPfZp0b7A==} 541 + cpu: [x64] 542 + os: [win32] 543 + 544 + '@sveltejs/adapter-cloudflare@5.0.2': 545 + resolution: {integrity: sha512-jlNRYFQ5mfmHmBqF79GhbFty/BTU+HZvgaT1uDREQmUcngT5j8yV85pxCOORl1r7rIVd0silRNBD5RJFsU5UUg==} 546 + peerDependencies: 547 + '@sveltejs/kit': ^2.0.0 548 + wrangler: ^3.87.0 549 + 550 + '@sveltejs/kit@2.17.1': 551 + resolution: {integrity: sha512-CpoGSLqE2MCmcQwA2CWJvOsZ9vW+p/1H3itrFykdgajUNAEyQPbsaSn7fZb6PLHQwe+07njxje9ss0fjZoCAyw==} 552 + engines: {node: '>=18.13'} 553 + hasBin: true 554 + peerDependencies: 555 + '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 556 + svelte: ^4.0.0 || ^5.0.0-next.0 557 + vite: ^5.0.3 || ^6.0.0 558 + 559 + '@sveltejs/vite-plugin-svelte-inspector@4.0.1': 560 + resolution: {integrity: sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==} 561 + engines: {node: ^18.0.0 || ^20.0.0 || >=22} 562 + peerDependencies: 563 + '@sveltejs/vite-plugin-svelte': ^5.0.0 564 + svelte: ^5.0.0 565 + vite: ^6.0.0 566 + 567 + '@sveltejs/vite-plugin-svelte@5.0.3': 568 + resolution: {integrity: sha512-MCFS6CrQDu1yGwspm4qtli0e63vaPCehf6V7pIMP15AsWgMKrqDGCPFF/0kn4SP0ii4aySu4Pa62+fIRGFMjgw==} 569 + engines: {node: ^18.0.0 || ^20.0.0 || >=22} 570 + peerDependencies: 571 + svelte: ^5.0.0 572 + vite: ^6.0.0 573 + 574 + '@types/cookie@0.6.0': 575 + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} 576 + 577 + '@types/estree@1.0.6': 578 + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} 579 + 580 + acorn-typescript@1.4.13: 581 + resolution: {integrity: sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==} 582 + peerDependencies: 583 + acorn: '>=8.9.0' 584 + 585 + acorn-walk@8.3.4: 586 + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} 587 + engines: {node: '>=0.4.0'} 588 + 589 + acorn@8.14.0: 590 + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} 591 + engines: {node: '>=0.4.0'} 592 + hasBin: true 593 + 594 + aria-query@5.3.2: 595 + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} 596 + engines: {node: '>= 0.4'} 597 + 598 + as-table@1.0.55: 599 + resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==} 600 + 601 + axobject-query@4.1.0: 602 + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} 603 + engines: {node: '>= 0.4'} 604 + 605 + blake3-wasm@2.1.5: 606 + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} 607 + 608 + chokidar@4.0.3: 609 + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} 610 + engines: {node: '>= 14.16.0'} 611 + 612 + clsx@2.1.1: 613 + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} 614 + engines: {node: '>=6'} 615 + 616 + confbox@0.1.8: 617 + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} 618 + 619 + cookie@0.6.0: 620 + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} 621 + engines: {node: '>= 0.6'} 622 + 623 + cookie@0.7.2: 624 + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} 625 + engines: {node: '>= 0.6'} 626 + 627 + css-declaration-sorter@7.2.0: 628 + resolution: {integrity: sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==} 629 + engines: {node: ^14 || ^16 || >=18} 630 + peerDependencies: 631 + postcss: ^8.0.9 632 + 633 + data-uri-to-buffer@2.0.2: 634 + resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==} 635 + 636 + debug@4.4.0: 637 + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} 638 + engines: {node: '>=6.0'} 639 + peerDependencies: 640 + supports-color: '*' 641 + peerDependenciesMeta: 642 + supports-color: 643 + optional: true 644 + 645 + deepmerge@4.3.1: 646 + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} 647 + engines: {node: '>=0.10.0'} 648 + 649 + defu@6.1.4: 650 + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} 651 + 652 + devalue@5.1.1: 653 + resolution: {integrity: sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==} 654 + 655 + esbuild@0.17.19: 656 + resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} 657 + engines: {node: '>=12'} 658 + hasBin: true 659 + 660 + esbuild@0.24.2: 661 + resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} 662 + engines: {node: '>=18'} 663 + hasBin: true 664 + 665 + escape-string-regexp@4.0.0: 666 + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} 667 + engines: {node: '>=10'} 668 + 669 + esm-env@1.2.2: 670 + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} 671 + 672 + esrap@1.4.3: 673 + resolution: {integrity: sha512-Xddc1RsoFJ4z9nR7W7BFaEPIp4UXoeQ0+077UdWLxbafMQFyU79sQJMk7kxNgRwQ9/aVgaKacCHC2pUACGwmYw==} 674 + 675 + estree-walker@0.6.1: 676 + resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} 677 + 678 + exit-hook@2.2.1: 679 + resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} 680 + engines: {node: '>=6'} 681 + 682 + fdir@6.4.3: 683 + resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==} 684 + peerDependencies: 685 + picomatch: ^3 || ^4 686 + peerDependenciesMeta: 687 + picomatch: 688 + optional: true 689 + 690 + fsevents@2.3.3: 691 + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 692 + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 693 + os: [darwin] 694 + 695 + get-source@2.0.12: 696 + resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==} 697 + 698 + glob-to-regexp@0.4.1: 699 + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} 700 + 701 + import-meta-resolve@4.1.0: 702 + resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} 703 + 704 + is-reference@3.0.3: 705 + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} 706 + 707 + kleur@4.1.5: 708 + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} 709 + engines: {node: '>=6'} 710 + 711 + locate-character@3.0.0: 712 + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} 713 + 714 + magic-string@0.25.9: 715 + resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} 716 + 717 + magic-string@0.30.17: 718 + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} 719 + 720 + mime@3.0.0: 721 + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} 722 + engines: {node: '>=10.0.0'} 723 + hasBin: true 724 + 725 + miniflare@3.20250129.0: 726 + resolution: {integrity: sha512-qYlGEjMl/2kJdgNaztj4hpA64d6Dl79Lx/NL61p/v5XZRiWanBOTgkQqdPxCKZOj6KQnioqhC7lfd6jDXKSs2A==} 727 + engines: {node: '>=16.13'} 728 + hasBin: true 729 + 730 + mlly@1.7.4: 731 + resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} 732 + 733 + mri@1.2.0: 734 + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} 735 + engines: {node: '>=4'} 736 + 737 + mrmime@2.0.0: 738 + resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} 739 + engines: {node: '>=10'} 740 + 741 + ms@2.1.3: 742 + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 743 + 744 + mustache@4.2.0: 745 + resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} 746 + hasBin: true 747 + 748 + nanoid@3.3.8: 749 + resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} 750 + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 751 + hasBin: true 752 + 753 + ohash@1.1.4: 754 + resolution: {integrity: sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==} 755 + 756 + path-to-regexp@6.3.0: 757 + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} 758 + 759 + pathe@1.1.2: 760 + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} 761 + 762 + pathe@2.0.2: 763 + resolution: {integrity: sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==} 764 + 765 + picocolors@1.1.1: 766 + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} 767 + 768 + pkg-types@1.3.1: 769 + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} 770 + 771 + postcss-less@6.0.0: 772 + resolution: {integrity: sha512-FPX16mQLyEjLzEuuJtxA8X3ejDLNGGEG503d2YGZR5Ask1SpDN8KmZUMpzCvyalWRywAn1n1VOA5dcqfCLo5rg==} 773 + engines: {node: '>=12'} 774 + peerDependencies: 775 + postcss: ^8.3.5 776 + 777 + postcss-scss@4.0.9: 778 + resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==} 779 + engines: {node: '>=12.0'} 780 + peerDependencies: 781 + postcss: ^8.4.29 782 + 783 + postcss@8.5.1: 784 + resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==} 785 + engines: {node: ^10 || ^12 || >=14} 786 + 787 + prettier-plugin-css-order@2.1.2: 788 + resolution: {integrity: sha512-vomxPjHI6pOMYcBuouSJHxxQClJXaUpU9rsV9IAO2wrSTZILRRlrxAAR8t9UF6wtczLkLfNRFUwM+ZbGXOONUA==} 789 + engines: {node: '>=16'} 790 + peerDependencies: 791 + prettier: 3.x 792 + 793 + prettier-plugin-svelte@3.3.3: 794 + resolution: {integrity: sha512-yViK9zqQ+H2qZD1w/bH7W8i+bVfKrD8GIFjkFe4Thl6kCT9SlAsXVNmt3jCvQOCsnOhcvYgsoVlRV/Eu6x5nNw==} 795 + peerDependencies: 796 + prettier: ^3.0.0 797 + svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 798 + 799 + prettier@3.4.2: 800 + resolution: {integrity: sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==} 801 + engines: {node: '>=14'} 802 + hasBin: true 803 + 804 + printable-characters@1.0.42: 805 + resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} 806 + 807 + readdirp@4.1.1: 808 + resolution: {integrity: sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==} 809 + engines: {node: '>= 14.18.0'} 810 + 811 + regexparam@3.0.0: 812 + resolution: {integrity: sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==} 813 + engines: {node: '>=8'} 814 + 815 + rollup-plugin-inject@3.0.2: 816 + resolution: {integrity: sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==} 817 + deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject. 818 + 819 + rollup-plugin-node-polyfills@0.2.1: 820 + resolution: {integrity: sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==} 821 + 822 + rollup-pluginutils@2.8.2: 823 + resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} 824 + 825 + rollup@4.34.4: 826 + resolution: {integrity: sha512-spF66xoyD7rz3o08sHP7wogp1gZ6itSq22SGa/IZTcUDXDlOyrShwMwkVSB+BUxFRZZCUYqdb3KWDEOMVQZxuw==} 827 + engines: {node: '>=18.0.0', npm: '>=8.0.0'} 828 + hasBin: true 829 + 830 + sade@1.8.1: 831 + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} 832 + engines: {node: '>=6'} 833 + 834 + set-cookie-parser@2.7.1: 835 + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} 836 + 837 + sirv@3.0.0: 838 + resolution: {integrity: sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==} 839 + engines: {node: '>=18'} 840 + 841 + source-map-js@1.2.1: 842 + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 843 + engines: {node: '>=0.10.0'} 844 + 845 + source-map@0.6.1: 846 + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 847 + engines: {node: '>=0.10.0'} 848 + 849 + sourcemap-codec@1.4.8: 850 + resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} 851 + deprecated: Please use @jridgewell/sourcemap-codec instead 852 + 853 + stacktracey@2.1.8: 854 + resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==} 855 + 856 + stoppable@1.1.0: 857 + resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} 858 + engines: {node: '>=4', npm: '>=6'} 859 + 860 + svelte-check@4.1.4: 861 + resolution: {integrity: sha512-v0j7yLbT29MezzaQJPEDwksybTE2Ups9rUxEXy92T06TiA0cbqcO8wAOwNUVkFW6B0hsYHA+oAX3BS8b/2oHtw==} 862 + engines: {node: '>= 18.0.0'} 863 + hasBin: true 864 + peerDependencies: 865 + svelte: ^4.0.0 || ^5.0.0-next.0 866 + typescript: '>=5.0.0' 867 + 868 + svelte@5.19.8: 869 + resolution: {integrity: sha512-56Vd/nwJrljV0w7RCV1A8sB4/yjSbWW5qrGDTAzp7q42OxwqEWT+6obWzDt41tHjIW+C9Fs2ygtejjJrXR+ZPA==} 870 + engines: {node: '>=18'} 871 + 872 + totalist@3.0.1: 873 + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} 874 + engines: {node: '>=6'} 875 + 876 + typescript@5.7.3: 877 + resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} 878 + engines: {node: '>=14.17'} 879 + hasBin: true 880 + 881 + ufo@1.5.4: 882 + resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} 883 + 884 + undici@5.28.5: 885 + resolution: {integrity: sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==} 886 + engines: {node: '>=14.0'} 887 + 888 + unenv@2.0.0-rc.1: 889 + resolution: {integrity: sha512-PU5fb40H8X149s117aB4ytbORcCvlASdtF97tfls4BPIyj4PeVxvpSuy1jAptqYHqB0vb2w2sHvzM0XWcp2OKg==} 890 + 891 + vite@6.1.0: 892 + resolution: {integrity: sha512-RjjMipCKVoR4hVfPY6GQTgveinjNuyLw+qruksLDvA5ktI1150VmcMBKmQaEWJhg/j6Uaf6dNCNA0AfdzUb/hQ==} 893 + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} 894 + hasBin: true 895 + peerDependencies: 896 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 897 + jiti: '>=1.21.0' 898 + less: '*' 899 + lightningcss: ^1.21.0 900 + sass: '*' 901 + sass-embedded: '*' 902 + stylus: '*' 903 + sugarss: '*' 904 + terser: ^5.16.0 905 + tsx: ^4.8.1 906 + yaml: ^2.4.2 907 + peerDependenciesMeta: 908 + '@types/node': 909 + optional: true 910 + jiti: 911 + optional: true 912 + less: 913 + optional: true 914 + lightningcss: 915 + optional: true 916 + sass: 917 + optional: true 918 + sass-embedded: 919 + optional: true 920 + stylus: 921 + optional: true 922 + sugarss: 923 + optional: true 924 + terser: 925 + optional: true 926 + tsx: 927 + optional: true 928 + yaml: 929 + optional: true 930 + 931 + vitefu@1.0.5: 932 + resolution: {integrity: sha512-h4Vflt9gxODPFNGPwp4zAMZRpZR7eslzwH2c5hn5kNZ5rhnKyRJ50U+yGCdc2IRaBs8O4haIgLNGrV5CrpMsCA==} 933 + peerDependencies: 934 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 935 + peerDependenciesMeta: 936 + vite: 937 + optional: true 938 + 939 + workerd@1.20250129.0: 940 + resolution: {integrity: sha512-Rprz8rxKTF4l6q/nYYI07lBetJnR19mGipx+u/a27GZOPKMG5SLIzA2NciZlJaB2Qd5YY+4p/eHOeKqo5keVWA==} 941 + engines: {node: '>=16'} 942 + hasBin: true 943 + 944 + worktop@0.8.0-next.18: 945 + resolution: {integrity: sha512-+TvsA6VAVoMC3XDKR5MoC/qlLqDixEfOBysDEKnPIPou/NvoPWCAuXHXMsswwlvmEuvX56lQjvELLyLuzTKvRw==} 946 + engines: {node: '>=12'} 947 + 948 + wrangler@3.107.3: 949 + resolution: {integrity: sha512-N9ZMDHZ+DI5/B0yclr3bG57U/Zw7wSzGdpO2l7j6+3q8yUf+4Fk0Rvneo2t8rjLewKlvqgt9D9siFuo8MXJ55Q==} 950 + engines: {node: '>=16.17.0'} 951 + hasBin: true 952 + peerDependencies: 953 + '@cloudflare/workers-types': ^4.20250129.0 954 + peerDependenciesMeta: 955 + '@cloudflare/workers-types': 956 + optional: true 957 + 958 + ws@8.18.0: 959 + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} 960 + engines: {node: '>=10.0.0'} 961 + peerDependencies: 962 + bufferutil: ^4.0.1 963 + utf-8-validate: '>=5.0.2' 964 + peerDependenciesMeta: 965 + bufferutil: 966 + optional: true 967 + utf-8-validate: 968 + optional: true 969 + 970 + youch@3.3.4: 971 + resolution: {integrity: sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==} 972 + 973 + zimmerframe@1.1.2: 974 + resolution: {integrity: sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==} 975 + 976 + zod@3.24.1: 977 + resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==} 978 + 979 + snapshots: 980 + 981 + '@ampproject/remapping@2.3.0': 982 + dependencies: 983 + '@jridgewell/gen-mapping': 0.3.8 984 + '@jridgewell/trace-mapping': 0.3.25 985 + 986 + '@atcute/bluesky-richtext-parser@1.0.7': {} 987 + 988 + '@atcute/bluesky-richtext-segmenter@1.0.5(@atcute/bluesky@1.0.12(@atcute/client@2.0.7))(@atcute/client@2.0.7)': 989 + dependencies: 990 + '@atcute/bluesky': 1.0.12(@atcute/client@2.0.7) 991 + '@atcute/client': 2.0.7 992 + 993 + '@atcute/bluesky@1.0.12(@atcute/client@2.0.7)': 994 + dependencies: 995 + '@atcute/client': 2.0.7 996 + 997 + '@atcute/client@2.0.7': {} 998 + 999 + '@badrap/valita@0.4.2': {} 1000 + 1001 + '@cloudflare/kv-asset-handler@0.3.4': 1002 + dependencies: 1003 + mime: 3.0.0 1004 + 1005 + '@cloudflare/workerd-darwin-64@1.20250129.0': 1006 + optional: true 1007 + 1008 + '@cloudflare/workerd-darwin-arm64@1.20250129.0': 1009 + optional: true 1010 + 1011 + '@cloudflare/workerd-linux-64@1.20250129.0': 1012 + optional: true 1013 + 1014 + '@cloudflare/workerd-linux-arm64@1.20250129.0': 1015 + optional: true 1016 + 1017 + '@cloudflare/workerd-windows-64@1.20250129.0': 1018 + optional: true 1019 + 1020 + '@cloudflare/workers-types@4.20250204.0': {} 1021 + 1022 + '@cspotcode/source-map-support@0.8.1': 1023 + dependencies: 1024 + '@jridgewell/trace-mapping': 0.3.9 1025 + 1026 + '@esbuild-plugins/node-globals-polyfill@0.2.3(esbuild@0.17.19)': 1027 + dependencies: 1028 + esbuild: 0.17.19 1029 + 1030 + '@esbuild-plugins/node-modules-polyfill@0.2.2(esbuild@0.17.19)': 1031 + dependencies: 1032 + esbuild: 0.17.19 1033 + escape-string-regexp: 4.0.0 1034 + rollup-plugin-node-polyfills: 0.2.1 1035 + 1036 + '@esbuild/aix-ppc64@0.24.2': 1037 + optional: true 1038 + 1039 + '@esbuild/android-arm64@0.17.19': 1040 + optional: true 1041 + 1042 + '@esbuild/android-arm64@0.24.2': 1043 + optional: true 1044 + 1045 + '@esbuild/android-arm@0.17.19': 1046 + optional: true 1047 + 1048 + '@esbuild/android-arm@0.24.2': 1049 + optional: true 1050 + 1051 + '@esbuild/android-x64@0.17.19': 1052 + optional: true 1053 + 1054 + '@esbuild/android-x64@0.24.2': 1055 + optional: true 1056 + 1057 + '@esbuild/darwin-arm64@0.17.19': 1058 + optional: true 1059 + 1060 + '@esbuild/darwin-arm64@0.24.2': 1061 + optional: true 1062 + 1063 + '@esbuild/darwin-x64@0.17.19': 1064 + optional: true 1065 + 1066 + '@esbuild/darwin-x64@0.24.2': 1067 + optional: true 1068 + 1069 + '@esbuild/freebsd-arm64@0.17.19': 1070 + optional: true 1071 + 1072 + '@esbuild/freebsd-arm64@0.24.2': 1073 + optional: true 1074 + 1075 + '@esbuild/freebsd-x64@0.17.19': 1076 + optional: true 1077 + 1078 + '@esbuild/freebsd-x64@0.24.2': 1079 + optional: true 1080 + 1081 + '@esbuild/linux-arm64@0.17.19': 1082 + optional: true 1083 + 1084 + '@esbuild/linux-arm64@0.24.2': 1085 + optional: true 1086 + 1087 + '@esbuild/linux-arm@0.17.19': 1088 + optional: true 1089 + 1090 + '@esbuild/linux-arm@0.24.2': 1091 + optional: true 1092 + 1093 + '@esbuild/linux-ia32@0.17.19': 1094 + optional: true 1095 + 1096 + '@esbuild/linux-ia32@0.24.2': 1097 + optional: true 1098 + 1099 + '@esbuild/linux-loong64@0.17.19': 1100 + optional: true 1101 + 1102 + '@esbuild/linux-loong64@0.24.2': 1103 + optional: true 1104 + 1105 + '@esbuild/linux-mips64el@0.17.19': 1106 + optional: true 1107 + 1108 + '@esbuild/linux-mips64el@0.24.2': 1109 + optional: true 1110 + 1111 + '@esbuild/linux-ppc64@0.17.19': 1112 + optional: true 1113 + 1114 + '@esbuild/linux-ppc64@0.24.2': 1115 + optional: true 1116 + 1117 + '@esbuild/linux-riscv64@0.17.19': 1118 + optional: true 1119 + 1120 + '@esbuild/linux-riscv64@0.24.2': 1121 + optional: true 1122 + 1123 + '@esbuild/linux-s390x@0.17.19': 1124 + optional: true 1125 + 1126 + '@esbuild/linux-s390x@0.24.2': 1127 + optional: true 1128 + 1129 + '@esbuild/linux-x64@0.17.19': 1130 + optional: true 1131 + 1132 + '@esbuild/linux-x64@0.24.2': 1133 + optional: true 1134 + 1135 + '@esbuild/netbsd-arm64@0.24.2': 1136 + optional: true 1137 + 1138 + '@esbuild/netbsd-x64@0.17.19': 1139 + optional: true 1140 + 1141 + '@esbuild/netbsd-x64@0.24.2': 1142 + optional: true 1143 + 1144 + '@esbuild/openbsd-arm64@0.24.2': 1145 + optional: true 1146 + 1147 + '@esbuild/openbsd-x64@0.17.19': 1148 + optional: true 1149 + 1150 + '@esbuild/openbsd-x64@0.24.2': 1151 + optional: true 1152 + 1153 + '@esbuild/sunos-x64@0.17.19': 1154 + optional: true 1155 + 1156 + '@esbuild/sunos-x64@0.24.2': 1157 + optional: true 1158 + 1159 + '@esbuild/win32-arm64@0.17.19': 1160 + optional: true 1161 + 1162 + '@esbuild/win32-arm64@0.24.2': 1163 + optional: true 1164 + 1165 + '@esbuild/win32-ia32@0.17.19': 1166 + optional: true 1167 + 1168 + '@esbuild/win32-ia32@0.24.2': 1169 + optional: true 1170 + 1171 + '@esbuild/win32-x64@0.17.19': 1172 + optional: true 1173 + 1174 + '@esbuild/win32-x64@0.24.2': 1175 + optional: true 1176 + 1177 + '@fastify/busboy@2.1.1': {} 1178 + 1179 + '@jridgewell/gen-mapping@0.3.8': 1180 + dependencies: 1181 + '@jridgewell/set-array': 1.2.1 1182 + '@jridgewell/sourcemap-codec': 1.5.0 1183 + '@jridgewell/trace-mapping': 0.3.25 1184 + 1185 + '@jridgewell/resolve-uri@3.1.2': {} 1186 + 1187 + '@jridgewell/set-array@1.2.1': {} 1188 + 1189 + '@jridgewell/sourcemap-codec@1.5.0': {} 1190 + 1191 + '@jridgewell/trace-mapping@0.3.25': 1192 + dependencies: 1193 + '@jridgewell/resolve-uri': 3.1.2 1194 + '@jridgewell/sourcemap-codec': 1.5.0 1195 + 1196 + '@jridgewell/trace-mapping@0.3.9': 1197 + dependencies: 1198 + '@jridgewell/resolve-uri': 3.1.2 1199 + '@jridgewell/sourcemap-codec': 1.5.0 1200 + 1201 + '@polka/url@1.0.0-next.28': {} 1202 + 1203 + '@rollup/rollup-android-arm-eabi@4.34.4': 1204 + optional: true 1205 + 1206 + '@rollup/rollup-android-arm64@4.34.4': 1207 + optional: true 1208 + 1209 + '@rollup/rollup-darwin-arm64@4.34.4': 1210 + optional: true 1211 + 1212 + '@rollup/rollup-darwin-x64@4.34.4': 1213 + optional: true 1214 + 1215 + '@rollup/rollup-freebsd-arm64@4.34.4': 1216 + optional: true 1217 + 1218 + '@rollup/rollup-freebsd-x64@4.34.4': 1219 + optional: true 1220 + 1221 + '@rollup/rollup-linux-arm-gnueabihf@4.34.4': 1222 + optional: true 1223 + 1224 + '@rollup/rollup-linux-arm-musleabihf@4.34.4': 1225 + optional: true 1226 + 1227 + '@rollup/rollup-linux-arm64-gnu@4.34.4': 1228 + optional: true 1229 + 1230 + '@rollup/rollup-linux-arm64-musl@4.34.4': 1231 + optional: true 1232 + 1233 + '@rollup/rollup-linux-loongarch64-gnu@4.34.4': 1234 + optional: true 1235 + 1236 + '@rollup/rollup-linux-powerpc64le-gnu@4.34.4': 1237 + optional: true 1238 + 1239 + '@rollup/rollup-linux-riscv64-gnu@4.34.4': 1240 + optional: true 1241 + 1242 + '@rollup/rollup-linux-s390x-gnu@4.34.4': 1243 + optional: true 1244 + 1245 + '@rollup/rollup-linux-x64-gnu@4.34.4': 1246 + optional: true 1247 + 1248 + '@rollup/rollup-linux-x64-musl@4.34.4': 1249 + optional: true 1250 + 1251 + '@rollup/rollup-win32-arm64-msvc@4.34.4': 1252 + optional: true 1253 + 1254 + '@rollup/rollup-win32-ia32-msvc@4.34.4': 1255 + optional: true 1256 + 1257 + '@rollup/rollup-win32-x64-msvc@4.34.4': 1258 + optional: true 1259 + 1260 + '@sveltejs/adapter-cloudflare@5.0.2(@sveltejs/kit@2.17.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.19.8)(vite@6.1.0))(svelte@5.19.8)(vite@6.1.0))(wrangler@3.107.3(@cloudflare/workers-types@4.20250204.0))': 1261 + dependencies: 1262 + '@cloudflare/workers-types': 4.20250204.0 1263 + '@sveltejs/kit': 2.17.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.19.8)(vite@6.1.0))(svelte@5.19.8)(vite@6.1.0) 1264 + esbuild: 0.24.2 1265 + worktop: 0.8.0-next.18 1266 + wrangler: 3.107.3(@cloudflare/workers-types@4.20250204.0) 1267 + 1268 + '@sveltejs/kit@2.17.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.19.8)(vite@6.1.0))(svelte@5.19.8)(vite@6.1.0)': 1269 + dependencies: 1270 + '@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.19.8)(vite@6.1.0) 1271 + '@types/cookie': 0.6.0 1272 + cookie: 0.6.0 1273 + devalue: 5.1.1 1274 + esm-env: 1.2.2 1275 + import-meta-resolve: 4.1.0 1276 + kleur: 4.1.5 1277 + magic-string: 0.30.17 1278 + mrmime: 2.0.0 1279 + sade: 1.8.1 1280 + set-cookie-parser: 2.7.1 1281 + sirv: 3.0.0 1282 + svelte: 5.19.8 1283 + vite: 6.1.0 1284 + 1285 + '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.19.8)(vite@6.1.0))(svelte@5.19.8)(vite@6.1.0)': 1286 + dependencies: 1287 + '@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.19.8)(vite@6.1.0) 1288 + debug: 4.4.0 1289 + svelte: 5.19.8 1290 + vite: 6.1.0 1291 + transitivePeerDependencies: 1292 + - supports-color 1293 + 1294 + '@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.19.8)(vite@6.1.0)': 1295 + dependencies: 1296 + '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.19.8)(vite@6.1.0))(svelte@5.19.8)(vite@6.1.0) 1297 + debug: 4.4.0 1298 + deepmerge: 4.3.1 1299 + kleur: 4.1.5 1300 + magic-string: 0.30.17 1301 + svelte: 5.19.8 1302 + vite: 6.1.0 1303 + vitefu: 1.0.5(vite@6.1.0) 1304 + transitivePeerDependencies: 1305 + - supports-color 1306 + 1307 + '@types/cookie@0.6.0': {} 1308 + 1309 + '@types/estree@1.0.6': {} 1310 + 1311 + acorn-typescript@1.4.13(acorn@8.14.0): 1312 + dependencies: 1313 + acorn: 8.14.0 1314 + 1315 + acorn-walk@8.3.4: 1316 + dependencies: 1317 + acorn: 8.14.0 1318 + 1319 + acorn@8.14.0: {} 1320 + 1321 + aria-query@5.3.2: {} 1322 + 1323 + as-table@1.0.55: 1324 + dependencies: 1325 + printable-characters: 1.0.42 1326 + 1327 + axobject-query@4.1.0: {} 1328 + 1329 + blake3-wasm@2.1.5: {} 1330 + 1331 + chokidar@4.0.3: 1332 + dependencies: 1333 + readdirp: 4.1.1 1334 + 1335 + clsx@2.1.1: {} 1336 + 1337 + confbox@0.1.8: {} 1338 + 1339 + cookie@0.6.0: {} 1340 + 1341 + cookie@0.7.2: {} 1342 + 1343 + css-declaration-sorter@7.2.0(postcss@8.5.1): 1344 + dependencies: 1345 + postcss: 8.5.1 1346 + 1347 + data-uri-to-buffer@2.0.2: {} 1348 + 1349 + debug@4.4.0: 1350 + dependencies: 1351 + ms: 2.1.3 1352 + 1353 + deepmerge@4.3.1: {} 1354 + 1355 + defu@6.1.4: {} 1356 + 1357 + devalue@5.1.1: {} 1358 + 1359 + esbuild@0.17.19: 1360 + optionalDependencies: 1361 + '@esbuild/android-arm': 0.17.19 1362 + '@esbuild/android-arm64': 0.17.19 1363 + '@esbuild/android-x64': 0.17.19 1364 + '@esbuild/darwin-arm64': 0.17.19 1365 + '@esbuild/darwin-x64': 0.17.19 1366 + '@esbuild/freebsd-arm64': 0.17.19 1367 + '@esbuild/freebsd-x64': 0.17.19 1368 + '@esbuild/linux-arm': 0.17.19 1369 + '@esbuild/linux-arm64': 0.17.19 1370 + '@esbuild/linux-ia32': 0.17.19 1371 + '@esbuild/linux-loong64': 0.17.19 1372 + '@esbuild/linux-mips64el': 0.17.19 1373 + '@esbuild/linux-ppc64': 0.17.19 1374 + '@esbuild/linux-riscv64': 0.17.19 1375 + '@esbuild/linux-s390x': 0.17.19 1376 + '@esbuild/linux-x64': 0.17.19 1377 + '@esbuild/netbsd-x64': 0.17.19 1378 + '@esbuild/openbsd-x64': 0.17.19 1379 + '@esbuild/sunos-x64': 0.17.19 1380 + '@esbuild/win32-arm64': 0.17.19 1381 + '@esbuild/win32-ia32': 0.17.19 1382 + '@esbuild/win32-x64': 0.17.19 1383 + 1384 + esbuild@0.24.2: 1385 + optionalDependencies: 1386 + '@esbuild/aix-ppc64': 0.24.2 1387 + '@esbuild/android-arm': 0.24.2 1388 + '@esbuild/android-arm64': 0.24.2 1389 + '@esbuild/android-x64': 0.24.2 1390 + '@esbuild/darwin-arm64': 0.24.2 1391 + '@esbuild/darwin-x64': 0.24.2 1392 + '@esbuild/freebsd-arm64': 0.24.2 1393 + '@esbuild/freebsd-x64': 0.24.2 1394 + '@esbuild/linux-arm': 0.24.2 1395 + '@esbuild/linux-arm64': 0.24.2 1396 + '@esbuild/linux-ia32': 0.24.2 1397 + '@esbuild/linux-loong64': 0.24.2 1398 + '@esbuild/linux-mips64el': 0.24.2 1399 + '@esbuild/linux-ppc64': 0.24.2 1400 + '@esbuild/linux-riscv64': 0.24.2 1401 + '@esbuild/linux-s390x': 0.24.2 1402 + '@esbuild/linux-x64': 0.24.2 1403 + '@esbuild/netbsd-arm64': 0.24.2 1404 + '@esbuild/netbsd-x64': 0.24.2 1405 + '@esbuild/openbsd-arm64': 0.24.2 1406 + '@esbuild/openbsd-x64': 0.24.2 1407 + '@esbuild/sunos-x64': 0.24.2 1408 + '@esbuild/win32-arm64': 0.24.2 1409 + '@esbuild/win32-ia32': 0.24.2 1410 + '@esbuild/win32-x64': 0.24.2 1411 + 1412 + escape-string-regexp@4.0.0: {} 1413 + 1414 + esm-env@1.2.2: {} 1415 + 1416 + esrap@1.4.3: 1417 + dependencies: 1418 + '@jridgewell/sourcemap-codec': 1.5.0 1419 + 1420 + estree-walker@0.6.1: {} 1421 + 1422 + exit-hook@2.2.1: {} 1423 + 1424 + fdir@6.4.3: {} 1425 + 1426 + fsevents@2.3.3: 1427 + optional: true 1428 + 1429 + get-source@2.0.12: 1430 + dependencies: 1431 + data-uri-to-buffer: 2.0.2 1432 + source-map: 0.6.1 1433 + 1434 + glob-to-regexp@0.4.1: {} 1435 + 1436 + import-meta-resolve@4.1.0: {} 1437 + 1438 + is-reference@3.0.3: 1439 + dependencies: 1440 + '@types/estree': 1.0.6 1441 + 1442 + kleur@4.1.5: {} 1443 + 1444 + locate-character@3.0.0: {} 1445 + 1446 + magic-string@0.25.9: 1447 + dependencies: 1448 + sourcemap-codec: 1.4.8 1449 + 1450 + magic-string@0.30.17: 1451 + dependencies: 1452 + '@jridgewell/sourcemap-codec': 1.5.0 1453 + 1454 + mime@3.0.0: {} 1455 + 1456 + miniflare@3.20250129.0: 1457 + dependencies: 1458 + '@cspotcode/source-map-support': 0.8.1 1459 + acorn: 8.14.0 1460 + acorn-walk: 8.3.4 1461 + exit-hook: 2.2.1 1462 + glob-to-regexp: 0.4.1 1463 + stoppable: 1.1.0 1464 + undici: 5.28.5 1465 + workerd: 1.20250129.0 1466 + ws: 8.18.0 1467 + youch: 3.3.4 1468 + zod: 3.24.1 1469 + transitivePeerDependencies: 1470 + - bufferutil 1471 + - utf-8-validate 1472 + 1473 + mlly@1.7.4: 1474 + dependencies: 1475 + acorn: 8.14.0 1476 + pathe: 2.0.2 1477 + pkg-types: 1.3.1 1478 + ufo: 1.5.4 1479 + 1480 + mri@1.2.0: {} 1481 + 1482 + mrmime@2.0.0: {} 1483 + 1484 + ms@2.1.3: {} 1485 + 1486 + mustache@4.2.0: {} 1487 + 1488 + nanoid@3.3.8: {} 1489 + 1490 + ohash@1.1.4: {} 1491 + 1492 + path-to-regexp@6.3.0: {} 1493 + 1494 + pathe@1.1.2: {} 1495 + 1496 + pathe@2.0.2: {} 1497 + 1498 + picocolors@1.1.1: {} 1499 + 1500 + pkg-types@1.3.1: 1501 + dependencies: 1502 + confbox: 0.1.8 1503 + mlly: 1.7.4 1504 + pathe: 2.0.2 1505 + 1506 + postcss-less@6.0.0(postcss@8.5.1): 1507 + dependencies: 1508 + postcss: 8.5.1 1509 + 1510 + postcss-scss@4.0.9(postcss@8.5.1): 1511 + dependencies: 1512 + postcss: 8.5.1 1513 + 1514 + postcss@8.5.1: 1515 + dependencies: 1516 + nanoid: 3.3.8 1517 + picocolors: 1.1.1 1518 + source-map-js: 1.2.1 1519 + 1520 + prettier-plugin-css-order@2.1.2(postcss@8.5.1)(prettier@3.4.2): 1521 + dependencies: 1522 + css-declaration-sorter: 7.2.0(postcss@8.5.1) 1523 + postcss-less: 6.0.0(postcss@8.5.1) 1524 + postcss-scss: 4.0.9(postcss@8.5.1) 1525 + prettier: 3.4.2 1526 + transitivePeerDependencies: 1527 + - postcss 1528 + 1529 + prettier-plugin-svelte@3.3.3(prettier@3.4.2)(svelte@5.19.8): 1530 + dependencies: 1531 + prettier: 3.4.2 1532 + svelte: 5.19.8 1533 + 1534 + prettier@3.4.2: {} 1535 + 1536 + printable-characters@1.0.42: {} 1537 + 1538 + readdirp@4.1.1: {} 1539 + 1540 + regexparam@3.0.0: {} 1541 + 1542 + rollup-plugin-inject@3.0.2: 1543 + dependencies: 1544 + estree-walker: 0.6.1 1545 + magic-string: 0.25.9 1546 + rollup-pluginutils: 2.8.2 1547 + 1548 + rollup-plugin-node-polyfills@0.2.1: 1549 + dependencies: 1550 + rollup-plugin-inject: 3.0.2 1551 + 1552 + rollup-pluginutils@2.8.2: 1553 + dependencies: 1554 + estree-walker: 0.6.1 1555 + 1556 + rollup@4.34.4: 1557 + dependencies: 1558 + '@types/estree': 1.0.6 1559 + optionalDependencies: 1560 + '@rollup/rollup-android-arm-eabi': 4.34.4 1561 + '@rollup/rollup-android-arm64': 4.34.4 1562 + '@rollup/rollup-darwin-arm64': 4.34.4 1563 + '@rollup/rollup-darwin-x64': 4.34.4 1564 + '@rollup/rollup-freebsd-arm64': 4.34.4 1565 + '@rollup/rollup-freebsd-x64': 4.34.4 1566 + '@rollup/rollup-linux-arm-gnueabihf': 4.34.4 1567 + '@rollup/rollup-linux-arm-musleabihf': 4.34.4 1568 + '@rollup/rollup-linux-arm64-gnu': 4.34.4 1569 + '@rollup/rollup-linux-arm64-musl': 4.34.4 1570 + '@rollup/rollup-linux-loongarch64-gnu': 4.34.4 1571 + '@rollup/rollup-linux-powerpc64le-gnu': 4.34.4 1572 + '@rollup/rollup-linux-riscv64-gnu': 4.34.4 1573 + '@rollup/rollup-linux-s390x-gnu': 4.34.4 1574 + '@rollup/rollup-linux-x64-gnu': 4.34.4 1575 + '@rollup/rollup-linux-x64-musl': 4.34.4 1576 + '@rollup/rollup-win32-arm64-msvc': 4.34.4 1577 + '@rollup/rollup-win32-ia32-msvc': 4.34.4 1578 + '@rollup/rollup-win32-x64-msvc': 4.34.4 1579 + fsevents: 2.3.3 1580 + 1581 + sade@1.8.1: 1582 + dependencies: 1583 + mri: 1.2.0 1584 + 1585 + set-cookie-parser@2.7.1: {} 1586 + 1587 + sirv@3.0.0: 1588 + dependencies: 1589 + '@polka/url': 1.0.0-next.28 1590 + mrmime: 2.0.0 1591 + totalist: 3.0.1 1592 + 1593 + source-map-js@1.2.1: {} 1594 + 1595 + source-map@0.6.1: {} 1596 + 1597 + sourcemap-codec@1.4.8: {} 1598 + 1599 + stacktracey@2.1.8: 1600 + dependencies: 1601 + as-table: 1.0.55 1602 + get-source: 2.0.12 1603 + 1604 + stoppable@1.1.0: {} 1605 + 1606 + svelte-check@4.1.4(svelte@5.19.8)(typescript@5.7.3): 1607 + dependencies: 1608 + '@jridgewell/trace-mapping': 0.3.25 1609 + chokidar: 4.0.3 1610 + fdir: 6.4.3 1611 + picocolors: 1.1.1 1612 + sade: 1.8.1 1613 + svelte: 5.19.8 1614 + typescript: 5.7.3 1615 + transitivePeerDependencies: 1616 + - picomatch 1617 + 1618 + svelte@5.19.8: 1619 + dependencies: 1620 + '@ampproject/remapping': 2.3.0 1621 + '@jridgewell/sourcemap-codec': 1.5.0 1622 + '@types/estree': 1.0.6 1623 + acorn: 8.14.0 1624 + acorn-typescript: 1.4.13(acorn@8.14.0) 1625 + aria-query: 5.3.2 1626 + axobject-query: 4.1.0 1627 + clsx: 2.1.1 1628 + esm-env: 1.2.2 1629 + esrap: 1.4.3 1630 + is-reference: 3.0.3 1631 + locate-character: 3.0.0 1632 + magic-string: 0.30.17 1633 + zimmerframe: 1.1.2 1634 + 1635 + totalist@3.0.1: {} 1636 + 1637 + typescript@5.7.3: {} 1638 + 1639 + ufo@1.5.4: {} 1640 + 1641 + undici@5.28.5: 1642 + dependencies: 1643 + '@fastify/busboy': 2.1.1 1644 + 1645 + unenv@2.0.0-rc.1: 1646 + dependencies: 1647 + defu: 6.1.4 1648 + mlly: 1.7.4 1649 + ohash: 1.1.4 1650 + pathe: 1.1.2 1651 + ufo: 1.5.4 1652 + 1653 + vite@6.1.0: 1654 + dependencies: 1655 + esbuild: 0.24.2 1656 + postcss: 8.5.1 1657 + rollup: 4.34.4 1658 + optionalDependencies: 1659 + fsevents: 2.3.3 1660 + 1661 + vitefu@1.0.5(vite@6.1.0): 1662 + optionalDependencies: 1663 + vite: 6.1.0 1664 + 1665 + workerd@1.20250129.0: 1666 + optionalDependencies: 1667 + '@cloudflare/workerd-darwin-64': 1.20250129.0 1668 + '@cloudflare/workerd-darwin-arm64': 1.20250129.0 1669 + '@cloudflare/workerd-linux-64': 1.20250129.0 1670 + '@cloudflare/workerd-linux-arm64': 1.20250129.0 1671 + '@cloudflare/workerd-windows-64': 1.20250129.0 1672 + 1673 + worktop@0.8.0-next.18: 1674 + dependencies: 1675 + mrmime: 2.0.0 1676 + regexparam: 3.0.0 1677 + 1678 + wrangler@3.107.3(@cloudflare/workers-types@4.20250204.0): 1679 + dependencies: 1680 + '@cloudflare/kv-asset-handler': 0.3.4 1681 + '@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19) 1682 + '@esbuild-plugins/node-modules-polyfill': 0.2.2(esbuild@0.17.19) 1683 + blake3-wasm: 2.1.5 1684 + esbuild: 0.17.19 1685 + miniflare: 3.20250129.0 1686 + path-to-regexp: 6.3.0 1687 + unenv: 2.0.0-rc.1 1688 + workerd: 1.20250129.0 1689 + optionalDependencies: 1690 + '@cloudflare/workers-types': 4.20250204.0 1691 + fsevents: 2.3.3 1692 + transitivePeerDependencies: 1693 + - bufferutil 1694 + - utf-8-validate 1695 + 1696 + ws@8.18.0: {} 1697 + 1698 + youch@3.3.4: 1699 + dependencies: 1700 + cookie: 0.7.2 1701 + mustache: 4.2.0 1702 + stacktracey: 2.1.8 1703 + 1704 + zimmerframe@1.1.2: {} 1705 + 1706 + zod@3.24.1: {}
+15
src/app.d.ts
··· 1 + import '@atcute/bluesky/lexicons'; 2 + 3 + // See https://svelte.dev/docs/kit/types#app.d.ts 4 + // for information about these interfaces 5 + declare global { 6 + namespace App { 7 + // interface Error {} 8 + // interface Locals {} 9 + // interface PageData {} 10 + // interface PageState {} 11 + // interface Platform {} 12 + } 13 + } 14 + 15 + export {};
+18
src/app.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="utf-8" /> 5 + <link rel="icon" href="%sveltekit.assets%/favicon.png" /> 6 + <meta name="viewport" content="width=device-width, initial-scale=1" /> 7 + <style> 8 + .sv-img-blurred { 9 + scale: 125%; 10 + filter: blur(4px); 11 + } 12 + </style> 13 + %sveltekit.head% 14 + </head> 15 + <body data-sveltekit-preload-data="hover" data-sveltekit-reload> 16 + %sveltekit.body% 17 + </body> 18 + </html>
+9
src/lib/components/central-icons/arrows-repeat-right-left-outlined.svelte
··· 1 + <svg class="sv-icon" fill="none" viewBox="0 0 24 24"> 2 + <path 3 + stroke="currentColor" 4 + stroke-linecap="round" 5 + stroke-linejoin="round" 6 + stroke-width="2" 7 + d="m17 3 3 3-3 3M7 21l-3-3 3-3m-2 3h15v-5M4 11V6h15" 8 + /> 9 + </svg>
+9
src/lib/components/central-icons/bubble-2-outlined.svelte
··· 1 + <svg class="sv-icon" fill="none" viewBox="0 0 24 24"> 2 + <path 3 + stroke="currentColor" 4 + stroke-linecap="square" 5 + stroke-linejoin="round" 6 + stroke-width="2" 7 + d="M3.002 4h18v14h-9l-5 3v-3h-4V4Z" 8 + /> 9 + </svg>
+9
src/lib/components/central-icons/dot-grid-1x3-horizontal-outlined.svelte
··· 1 + <svg class="sv-icon" fill="none" viewBox="0 0 24 24"> 2 + <path 3 + stroke="currentColor" 4 + stroke-linecap="round" 5 + stroke-linejoin="round" 6 + stroke-width="2" 7 + d="M12 13a1 1 0 1 0 0-2 1 1 0 0 0 0 2ZM20 13a1 1 0 1 0 0-2 1 1 0 0 0 0 2ZM4 13a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z" 8 + /> 9 + </svg>
+6
src/lib/components/central-icons/earth-outlined.svelte
··· 1 + <svg class="sv-icon" fill="none" viewBox="0 0 24 24"> 2 + <path 3 + fill="currentColor" 4 + d="M14.922 16.865v1a1 1 0 0 0 .894-.553l-.894-.447Zm.973-1.946.894.447a1 1 0 0 0-.337-1.277l-.557.83Zm-2.869-1.928.558-.83a1 1 0 0 0-.494-.168l-.064.998Zm-1.89-.12.064-.998a1 1 0 0 0-.77.29l.706.708Zm-1.08 1.075-.705-.708a1 1 0 0 0-.126 1.263l.832-.555Zm1.947 2.919-.832.555a1 1 0 0 0 .832.445v-1ZM4.772 7.27a1 1 0 1 0-1.2 1.6l1.2-1.6Zm3.34 3.757-.601.8a1 1 0 0 0 1.493-.35l-.893-.45Zm.977-1.941-.244-.97a1 1 0 0 0-.65.52l.894.45Zm3.887-.978.244.97a1 1 0 0 0 .726-.727l-.97-.243Zm2.12-4.357a1 1 0 0 0-1.94-.485l1.94.485ZM20 12a8 8 0 0 1-8 8v2c5.523 0 10-4.477 10-10h-2Zm-8 8a8 8 0 0 1-8-8H2c0 5.523 4.477 10 10 10v-2Zm-8-8a8 8 0 0 1 8-8V2C6.477 2 2 6.477 2 12h2Zm8-8a8 8 0 0 1 8 8h2c0-5.523-4.477-10-10-10v2Zm3.816 13.312.973-1.946L15 14.472l-.973 1.946 1.79.894Zm.636-3.223-2.868-1.928-1.115 1.66 2.868 1.928 1.116-1.66Zm-3.362-2.096-1.89-.12-.128 1.996 1.89.12.128-1.996Zm-2.66.17-1.079 1.075 1.412 1.416 1.079-1.075-1.412-1.417ZM9.225 14.5l1.946 2.919 1.664-1.11-1.946-2.919-1.664 1.11Zm2.778 3.364h2.919v-2h-2.92v2ZM3.57 8.87l3.94 2.957 1.2-1.6-3.94-2.957-1.2 1.6Zm5.433 2.607.978-1.941-1.786-.9-.978 1.941 1.786.9Zm.329-1.421 3.887-.978-.488-1.94-3.887.978.488 1.94Zm4.613-1.705 1.15-4.6-1.94-.485-1.15 4.6 1.94.485Z" 5 + /> 6 + </svg>
+8
src/lib/components/central-icons/heart-outlined.svelte
··· 1 + <svg class="sv-icon" fill="none" viewBox="0 0 24 24"> 2 + <path 3 + stroke="currentColor" 4 + stroke-linejoin="round" 5 + stroke-width="2" 6 + d="M21 10c0 5.75-8.25 10-9 10s-9-4.25-9-10c0-4 2.5-6 5-6s4 1.5 4 1.5S13.5 4 16 4s5 2 5 6Z" 7 + /> 8 + </svg>
+9
src/lib/components/central-icons/pin-outlined.svelte
··· 1 + <svg class="sv-icon" fill="none" viewBox="0 0 24 24"> 2 + <path 3 + stroke="currentColor" 4 + stroke-linecap="round" 5 + stroke-linejoin="round" 6 + stroke-width="2" 7 + d="m4 20 4.5-4.5m3-8L14 3l7 7-4.5 2.5L14 20 4 10l7.5-2.5Z" 8 + /> 9 + </svg>
+6
src/lib/components/central-icons/play-solid.svelte
··· 1 + <svg class="sv-icon" fill="none" viewBox="0 0 24 24"> 2 + <path 3 + fill="currentColor" 4 + d="M6.514 2.143A1 1 0 0 0 5 3v18a1 1 0 0 0 1.514.858l15-9a1 1 0 0 0 0-1.716l-15-9Z" 5 + /> 6 + </svg>
+118
src/lib/components/embeds/embeds.svelte
··· 1 + <script lang="ts" module> 2 + const collectionToLabel = (collection: string): string | null => { 3 + switch (collection) { 4 + case 'app.bsky.feed.post': 5 + return 'post'; 6 + case 'app.bsky.feed.generator': 7 + return 'feed'; 8 + case 'app.bsky.graph.list': 9 + return 'list'; 10 + case 'app.bsky.graph.starterpack': 11 + return 'starter pack'; 12 + case 'app.bsky.labeler.service': 13 + return 'labeler'; 14 + } 15 + 16 + return null; 17 + }; 18 + </script> 19 + 20 + <script lang="ts"> 21 + import type { 22 + AppBskyEmbedExternal, 23 + AppBskyEmbedImages, 24 + AppBskyEmbedRecord, 25 + AppBskyEmbedVideo, 26 + AppBskyFeedDefs, 27 + Brand, 28 + } from '@atcute/client/lexicons'; 29 + 30 + import { parseAtUri } from '$lib/types/at-uri'; 31 + 32 + import ExternalEmbed from './external-embed.svelte'; 33 + import FeedEmbed from './feed-embed.svelte'; 34 + import ImageEmbed from './image-embed.svelte'; 35 + import ListEmbed from './list-embed.svelte'; 36 + import QuoteEmbed from './quote-embed.svelte'; 37 + import StarterpackEmbed from './starterpack-embed.svelte'; 38 + import VideoEmbed from './video-embed.svelte'; 39 + 40 + type Embed = NonNullable<AppBskyFeedDefs.PostView['embed']>; 41 + type MediaEmbed = Brand.Union<AppBskyEmbedExternal.View | AppBskyEmbedImages.View | AppBskyEmbedVideo.View>; 42 + type RecordEmbed = AppBskyEmbedRecord.View; 43 + 44 + interface Props { 45 + embed: Embed; 46 + large?: boolean; 47 + } 48 + 49 + const { embed, large = false }: Props = $props(); 50 + </script> 51 + 52 + <div class="embeds"> 53 + {#if embed.$type === 'app.bsky.embed.recordWithMedia#view'} 54 + {@render Media(embed.media)} 55 + {@render Record(embed.record)} 56 + {:else if embed.$type === 'app.bsky.embed.record#view'} 57 + {@render Record(embed)} 58 + {:else} 59 + {@render Media(embed)} 60 + {/if} 61 + </div> 62 + 63 + {#snippet Media(embed: MediaEmbed)} 64 + {#if embed.$type === 'app.bsky.embed.external#view'} 65 + <ExternalEmbed {embed} /> 66 + {:else if embed.$type === 'app.bsky.embed.images#view'} 67 + <ImageEmbed {embed} standalone /> 68 + {:else if embed.$type === 'app.bsky.embed.video#view'} 69 + <VideoEmbed {embed} standalone /> 70 + {:else} 71 + {@render Message(`Unsupported media embed`)} 72 + {/if} 73 + {/snippet} 74 + 75 + {#snippet Record(embed: RecordEmbed)} 76 + {@const record = embed.record} 77 + 78 + {#if record.$type === 'app.bsky.embed.record#viewRecord'} 79 + <QuoteEmbed embed={record} {large} /> 80 + {:else if record.$type === 'app.bsky.feed.defs#generatorView'} 81 + <FeedEmbed embed={record} /> 82 + {:else if record.$type === 'app.bsky.graph.defs#listView'} 83 + <ListEmbed embed={record} /> 84 + {:else if record.$type === 'app.bsky.graph.defs#starterPackViewBasic'} 85 + <StarterpackEmbed embed={record} {large} /> 86 + {:else} 87 + {@const uri = parseAtUri(record.uri)} 88 + {@const resource = collectionToLabel(uri.collection)} 89 + 90 + {@const isUnavailable = 91 + resource && 92 + (record.$type === 'app.bsky.embed.record#viewNotFound' || 93 + record.$type === 'app.bsky.embed.record#viewBlocked' || 94 + record.$type === 'app.bsky.embed.record#viewDetached')} 95 + 96 + {@render Message(isUnavailable ? `This ${resource} is unavailable` : `Unsupported record embed`)} 97 + {/if} 98 + {/snippet} 99 + 100 + {#snippet Message(message: string)} 101 + <div class="message">{message}</div> 102 + {/snippet} 103 + 104 + <style> 105 + .embeds { 106 + display: flex; 107 + flex-direction: column; 108 + gap: 12px; 109 + margin: 12px 0 0 0; 110 + } 111 + 112 + .message { 113 + border: 1px solid var(--divider-sm); 114 + border-radius: 6px; 115 + padding: 12px; 116 + color: var(--text-blurb); 117 + } 118 + </style>
+121
src/lib/components/embeds/external-embed.svelte
··· 1 + <script lang="ts" module> 2 + const safeParseUrl = (str: string): URL | null => { 3 + let url: URL | null | undefined; 4 + if ('parse' in URL) { 5 + url = URL.parse(str); 6 + } else { 7 + try { 8 + // @ts-expect-error: `'parse' in URL` is giving truthy 9 + url = new URL(str); 10 + } catch {} 11 + } 12 + 13 + if (url && (url.protocol === 'https:' || url.protocol === 'http:')) { 14 + return url; 15 + } 16 + 17 + return null; 18 + }; 19 + </script> 20 + 21 + <script lang="ts"> 22 + import type { AppBskyEmbedExternal } from '@atcute/client/lexicons'; 23 + 24 + import EarthOutlined from '$lib/components/central-icons/earth-outlined.svelte'; 25 + 26 + interface Props { 27 + embed: AppBskyEmbedExternal.View; 28 + } 29 + 30 + const { embed }: Props = $props(); 31 + 32 + const external = embed.external; 33 + 34 + const domain = safeParseUrl(external.uri)?.host; 35 + </script> 36 + 37 + <a target="_blank" href={domain && external.uri} rel="noopener noreferrer nofollow" class="external-embed"> 38 + {#if external.thumb} 39 + <img loading="lazy" src={external.thumb} alt="" class="thumbnail" /> 40 + {/if} 41 + 42 + <div class="meta"> 43 + <p class="title">{external.title}</p> 44 + <p class="description">{external.description}</p> 45 + 46 + {#if domain} 47 + <div class="domain"> 48 + <EarthOutlined /> 49 + 50 + <span class="domain-name">{domain}</span> 51 + </div> 52 + {/if} 53 + </div> 54 + </a> 55 + 56 + <style> 57 + .external-embed { 58 + display: block; 59 + border: 1px solid var(--divider-md); 60 + border-radius: 6px; 61 + overflow: hidden; 62 + color: var(--text-primary); 63 + 64 + &:hover { 65 + background: var(--tap-sm); 66 + } 67 + } 68 + 69 + .thumbnail { 70 + display: block; 71 + border-bottom: 1px solid var(--divider-md); 72 + background: var(--bg-primary); 73 + aspect-ratio: 1.91; 74 + width: 100%; 75 + object-fit: cover; 76 + } 77 + 78 + .meta { 79 + padding: 12px; 80 + } 81 + 82 + .title { 83 + display: -webkit-box; 84 + overflow: hidden; 85 + font-weight: 700; 86 + white-space: pre-wrap; 87 + -webkit-box-orient: vertical; 88 + -webkit-line-clamp: 2; 89 + line-clamp: 2; 90 + overflow-wrap: break-word; 91 + 92 + &:empty { 93 + display: none; 94 + } 95 + } 96 + .description { 97 + display: -webkit-box; 98 + overflow: hidden; 99 + color: var(--text-blurb); 100 + font-size: 0.8125rem; 101 + white-space: pre-wrap; 102 + -webkit-box-orient: vertical; 103 + -webkit-line-clamp: 2; 104 + line-clamp: 2; 105 + overflow-wrap: break-word; 106 + 107 + &:empty { 108 + display: none; 109 + } 110 + } 111 + 112 + .domain { 113 + display: flex; 114 + align-items: center; 115 + gap: 6px; 116 + margin: 6px 0 0 0; 117 + color: var(--text-blurb); 118 + font-weight: 500; 119 + font-size: 0.75rem; 120 + } 121 + </style>
+101
src/lib/components/embeds/feed-embed.svelte
··· 1 + <script lang="ts"> 2 + import type { AppBskyFeedDefs } from '@atcute/client/lexicons'; 3 + 4 + import { base } from '$app/paths'; 5 + 6 + import { parseAtUri } from '$lib/types/at-uri'; 7 + 8 + interface Props { 9 + embed: AppBskyFeedDefs.GeneratorView; 10 + } 11 + 12 + const { embed: feed }: Props = $props(); 13 + 14 + const creator = $derived(feed.creator); 15 + </script> 16 + 17 + <a href="{base}/{creator.did}/feeds/{parseAtUri(feed.uri).rkey}" class="feed-embed"> 18 + <div class="main"> 19 + <div class="avatar-wrapper"> 20 + {#if feed.avatar} 21 + <img loading="lazy" src={feed.avatar} alt="" class="avatar" /> 22 + {:else} 23 + <svg viewBox="0 0 32 32" class="avatar"> 24 + <path fill="#0070FF" d="M0 0h32v32H0z" /> 25 + <path 26 + fill="#fff" 27 + d="M22.153 22.354a9.328 9.328 0 0 0 3.837-.491 3.076 3.076 0 0 0-4.802-2.79m.965 3.281a6.128 6.128 0 0 0-.965-3.28Zm-11.342-3.28a3.077 3.077 0 0 0-4.801 2.79 9.21 9.21 0 0 0 3.835.49m.966-3.28a6.127 6.127 0 0 0-.966 3.28Zm8.265-8.997a3.076 3.076 0 1 1-6.153 0 3.076 3.076 0 0 1 6.153 0Zm6.154 3.077a2.307 2.307 0 1 1-4.615 0 2.307 2.307 0 0 1 4.615 0Zm-13.847 0a2.307 2.307 0 1 1-4.614 0 2.307 2.307 0 0 1 4.614 0Z" 28 + /> 29 + <path fill="#fff" d="M22 22c0 3.314-2.686 3.5-6 3.5s-6-.186-6-3.5a6 6 0 0 1 12 0Z" /> 30 + </svg> 31 + {/if} 32 + </div> 33 + 34 + <div class="info"> 35 + <p class="name">{feed.displayName}</p> 36 + <p class="creator">Feed by @{creator.handle}</p> 37 + </div> 38 + </div> 39 + 40 + <p class="description">{feed.description}</p> 41 + </a> 42 + 43 + <style> 44 + .feed-embed { 45 + display: flex; 46 + flex-direction: column; 47 + gap: 12px; 48 + border: 1px solid var(--divider-md); 49 + border-radius: 6px; 50 + padding: 12px; 51 + color: var(--text-primary); 52 + 53 + &:hover { 54 + background: var(--tap-sm); 55 + } 56 + } 57 + 58 + .main { 59 + display: flex; 60 + gap: 12px; 61 + } 62 + 63 + .avatar-wrapper { 64 + margin: 2px 0 0 0; 65 + border-radius: 6px; 66 + background: var(--bg-secondary); 67 + width: 36px; 68 + height: 36px; 69 + overflow: hidden; 70 + } 71 + .avatar { 72 + width: 100%; 73 + height: 100%; 74 + object-fit: cover; 75 + font-size: 0; 76 + } 77 + 78 + .name { 79 + font-weight: 700; 80 + } 81 + 82 + .creator { 83 + color: var(--text-blurb); 84 + font-size: 0.8125rem; 85 + } 86 + 87 + .description { 88 + display: -webkit-box; 89 + overflow: hidden; 90 + font-size: 0.8125rem; 91 + white-space: pre-wrap; 92 + -webkit-box-orient: vertical; 93 + -webkit-line-clamp: 2; 94 + line-clamp: 2; 95 + overflow-wrap: break-word; 96 + 97 + &:empty { 98 + display: none; 99 + } 100 + } 101 + </style>
+178
src/lib/components/embeds/image-embed.svelte
··· 1 + <script lang="ts" module> 2 + const DEFAULT_RATIO = { width: 16, height: 9 }; 3 + </script> 4 + 5 + <script lang="ts"> 6 + import type { AppBskyEmbedImages } from '@atcute/client/lexicons'; 7 + 8 + interface Props { 9 + embed: AppBskyEmbedImages.View; 10 + borderless?: boolean; 11 + standalone?: boolean; 12 + blur?: boolean; 13 + } 14 + 15 + const { embed, borderless, standalone, blur }: Props = $props(); 16 + 17 + const images = $derived(embed.images); 18 + const length = $derived(images.length); 19 + </script> 20 + 21 + <div class={['image-embed', !borderless && 'is-bordered', standalone && length === 1 && 'is-aligned']}> 22 + {#if length === 4} 23 + <div class="grid"> 24 + <div class="col"> 25 + <div class="item wide tl"> 26 + {@render Image(0)} 27 + </div> 28 + <div class="item wide bl"> 29 + {@render Image(2)} 30 + </div> 31 + </div> 32 + <div class="col"> 33 + <div class="item wide tr"> 34 + {@render Image(1)} 35 + </div> 36 + <div class="item wide br"> 37 + {@render Image(3)} 38 + </div> 39 + </div> 40 + </div> 41 + {:else if length === 3} 42 + <div class="grid"> 43 + <div class="col square"> 44 + <div class="item tl bl"> 45 + {@render Image(0)} 46 + </div> 47 + </div> 48 + <div class="col square"> 49 + <div class="item tr"> 50 + {@render Image(1)} 51 + </div> 52 + <div class="item br"> 53 + {@render Image(2)} 54 + </div> 55 + </div> 56 + </div> 57 + {:else if length === 2} 58 + <div class="grid"> 59 + <div class="col"> 60 + <div class="item square tl bl"> 61 + {@render Image(0)} 62 + </div> 63 + </div> 64 + <div class="col"> 65 + <div class="item square tr br"> 66 + {@render Image(1)} 67 + </div> 68 + </div> 69 + </div> 70 + {:else if length === 1} 71 + {@const ratio = standalone && (images[0].aspectRatio || DEFAULT_RATIO)} 72 + 73 + <div 74 + class={['single-item tl tr bl br', ratio && 'is-standalone', ratio === DEFAULT_RATIO && 'is-defaulted']} 75 + style={ratio ? `aspect-ratio: ${ratio.width}/${ratio.height}` : ``} 76 + > 77 + {@render Image(0)} 78 + 79 + {#if ratio} 80 + <div class="placeholder"></div> 81 + {/if} 82 + </div> 83 + {/if} 84 + </div> 85 + 86 + {#snippet Image(index: number)} 87 + {@const image = images[index]} 88 + 89 + <img loading="lazy" src={image.thumb} alt={image.alt} class={`image` + (blur ? ` is-blurred` : ``)} /> 90 + {/snippet} 91 + 92 + <style> 93 + .is-aligned { 94 + align-self: baseline; 95 + max-width: 100%; 96 + } 97 + 98 + .grid { 99 + display: flex; 100 + gap: 2px; 101 + } 102 + .col { 103 + display: flex; 104 + flex: 1; 105 + flex-direction: column; 106 + gap: 2px; 107 + } 108 + 109 + .square { 110 + aspect-ratio: 1; 111 + } 112 + .wide { 113 + aspect-ratio: 1.5; 114 + } 115 + 116 + .item { 117 + position: relative; 118 + flex-grow: 1; 119 + flex-shrink: 0; 120 + overflow: hidden; 121 + } 122 + 123 + .is-bordered { 124 + .tl, 125 + .tr, 126 + .bl, 127 + .br { 128 + border: 1px solid var(--divider-md); 129 + } 130 + 131 + .tl { 132 + border-top-left-radius: 6px; 133 + } 134 + .tr { 135 + border-top-right-radius: 6px; 136 + } 137 + .bl { 138 + border-bottom-left-radius: 6px; 139 + } 140 + .br { 141 + border-bottom-right-radius: 6px; 142 + } 143 + } 144 + 145 + .single-item { 146 + position: relative; 147 + aspect-ratio: 16 / 9; 148 + overflow: hidden; 149 + } 150 + .is-standalone { 151 + min-width: 64px; 152 + max-width: 100%; 153 + min-height: 64px; 154 + max-height: 320px; 155 + } 156 + 157 + .image { 158 + position: absolute; 159 + inset: 0; 160 + background: var(--bg-secondary); 161 + width: 100%; 162 + height: 100%; 163 + object-fit: cover; 164 + font-size: 0px; 165 + } 166 + .is-defaulted .image { 167 + object-fit: contain; 168 + } 169 + .is-blurred { 170 + scale: 125%; 171 + filter: blur(24px); 172 + } 173 + 174 + .placeholder { 175 + width: 100vw; 176 + height: 100vh; 177 + } 178 + </style>
+114
src/lib/components/embeds/list-embed.svelte
··· 1 + <script lang="ts" module> 2 + const getPurpose = (purpose: AppBskyGraphDefs.ListPurpose) => { 3 + switch (purpose) { 4 + case 'app.bsky.graph.defs#curatelist': 5 + return `User list`; 6 + case 'app.bsky.graph.defs#modlist': 7 + return `Moderation list`; 8 + } 9 + 10 + return `Unknown list`; 11 + }; 12 + </script> 13 + 14 + <script lang="ts"> 15 + import type { AppBskyGraphDefs } from '@atcute/client/lexicons'; 16 + 17 + import { base } from '$app/paths'; 18 + 19 + import { parseAtUri } from '$lib/types/at-uri'; 20 + 21 + interface Props { 22 + embed: AppBskyGraphDefs.ListView; 23 + } 24 + 25 + const { embed: list }: Props = $props(); 26 + 27 + const creator = $derived(list.creator); 28 + </script> 29 + 30 + <a href="{base}/{creator.did}/lists/{parseAtUri(list.uri).rkey}" class="list-embed"> 31 + <div class="main"> 32 + <div class="avatar-wrapper"> 33 + {#if list.avatar} 34 + <img loading="lazy" src={list.avatar} alt="" class="avatar" /> 35 + {:else} 36 + <svg viewBox="0 0 32 32" class="avatar"> 37 + <path fill="#0070FF" d="M0 0h32v32H0z" /> 38 + <path 39 + fill="#fff" 40 + d="M22.153 22.354a9.328 9.328 0 0 0 3.837-.491 3.076 3.076 0 0 0-4.802-2.79m.965 3.281a6.128 6.128 0 0 0-.965-3.28Zm-11.342-3.28a3.077 3.077 0 0 0-4.801 2.79 9.21 9.21 0 0 0 3.835.49m.966-3.28a6.127 6.127 0 0 0-.966 3.28Zm8.265-8.997a3.076 3.076 0 1 1-6.153 0 3.076 3.076 0 0 1 6.153 0Zm6.154 3.077a2.307 2.307 0 1 1-4.615 0 2.307 2.307 0 0 1 4.615 0Zm-13.847 0a2.307 2.307 0 1 1-4.614 0 2.307 2.307 0 0 1 4.614 0Z" 41 + /> 42 + <path fill="#fff" d="M22 22c0 3.314-2.686 3.5-6 3.5s-6-.186-6-3.5a6 6 0 0 1 12 0Z" /> 43 + </svg> 44 + {/if} 45 + </div> 46 + 47 + <div class="info"> 48 + <p class="name">{list.name}</p> 49 + <p class="creator">{getPurpose(list.purpose)} by @{creator.handle}</p> 50 + </div> 51 + </div> 52 + 53 + <p class="description">{list.description}</p> 54 + </a> 55 + 56 + <style> 57 + .list-embed { 58 + display: flex; 59 + flex-direction: column; 60 + gap: 12px; 61 + border: 1px solid var(--divider-md); 62 + border-radius: 6px; 63 + padding: 12px; 64 + color: var(--text-primary); 65 + 66 + &:hover { 67 + background: var(--tap-sm); 68 + } 69 + } 70 + 71 + .main { 72 + display: flex; 73 + gap: 12px; 74 + } 75 + 76 + .avatar-wrapper { 77 + margin: 2px 0 0 0; 78 + border-radius: 6px; 79 + background: var(--bg-secondary); 80 + width: 36px; 81 + height: 36px; 82 + overflow: hidden; 83 + } 84 + .avatar { 85 + width: 100%; 86 + height: 100%; 87 + object-fit: cover; 88 + font-size: 0; 89 + } 90 + 91 + .name { 92 + font-weight: 700; 93 + } 94 + 95 + .creator { 96 + color: var(--text-blurb); 97 + font-size: 0.8125rem; 98 + } 99 + 100 + .description { 101 + display: -webkit-box; 102 + overflow: hidden; 103 + font-size: 0.8125rem; 104 + white-space: pre-wrap; 105 + -webkit-box-orient: vertical; 106 + -webkit-line-clamp: 2; 107 + line-clamp: 2; 108 + overflow-wrap: break-word; 109 + 110 + &:empty { 111 + display: none; 112 + } 113 + } 114 + </style>
+212
src/lib/components/embeds/quote-embed.svelte
··· 1 + <script lang="ts" module> 2 + const getPostImage = (embed: AppBskyFeedDefs.PostView['embed']): AppBskyEmbedImages.View | undefined => { 3 + if (embed) { 4 + if (embed.$type === 'app.bsky.embed.images#view') { 5 + return embed; 6 + } 7 + 8 + if (embed.$type === 'app.bsky.embed.recordWithMedia#view') { 9 + return getPostImage(embed.media); 10 + } 11 + } 12 + }; 13 + 14 + const getPostVideo = (embed: AppBskyFeedDefs.PostView['embed']): AppBskyEmbedVideo.View | undefined => { 15 + if (embed) { 16 + if (embed.$type === 'app.bsky.embed.video#view') { 17 + return embed; 18 + } 19 + 20 + if (embed.$type === 'app.bsky.embed.recordWithMedia#view') { 21 + return getPostVideo(embed.media); 22 + } 23 + } 24 + }; 25 + </script> 26 + 27 + <script lang="ts"> 28 + import type { 29 + AppBskyEmbedImages, 30 + AppBskyEmbedRecord, 31 + AppBskyEmbedVideo, 32 + AppBskyFeedDefs, 33 + AppBskyFeedPost, 34 + } from '@atcute/client/lexicons'; 35 + 36 + import { base } from '$app/paths'; 37 + 38 + import { parseAtUri } from '$lib/types/at-uri'; 39 + import { formatRelativeTime } from '$lib/utils/intl/date'; 40 + 41 + import ImageEmbed from './image-embed.svelte'; 42 + import VideoEmbed from './video-embed.svelte'; 43 + 44 + interface Props { 45 + embed: AppBskyEmbedRecord.ViewRecord; 46 + large?: boolean; 47 + } 48 + 49 + const { embed: quote, large = false }: Props = $props(); 50 + 51 + const record = $derived(quote.value as AppBskyFeedPost.Record); 52 + const text = $derived(record.text.trim()); 53 + 54 + const author = $derived(quote.author); 55 + const authorName = $derived(author.displayName?.trim()); 56 + 57 + const embed = $derived(quote.embeds?.[0]); 58 + const image = $derived(getPostImage(embed)); 59 + const video = $derived(getPostVideo(embed)); 60 + </script> 61 + 62 + <a href="{base}/{author.did}/{parseAtUri(quote.uri).rkey}#main" class="quote-embed"> 63 + <div class="meta"> 64 + <div class="avatar-wrapper"> 65 + {#if author.avatar} 66 + <img loading="lazy" src={author.avatar} alt="" class="avatar" /> 67 + {/if} 68 + </div> 69 + 70 + <span class="name-wrapper"> 71 + {#if authorName} 72 + <bdi class="display-name-wrapper"> 73 + <span class="display-name">{authorName}</span> 74 + </bdi> 75 + {/if} 76 + 77 + <span class="handle">@{author.handle}</span> 78 + </span> 79 + 80 + <span aria-hidden="true" class="dot">·</span> 81 + 82 + <time datetime={record.createdAt} class="date"> 83 + {formatRelativeTime(record.createdAt)} 84 + </time> 85 + </div> 86 + 87 + {#if text} 88 + <div class="body"> 89 + {#if !large} 90 + {#if image} 91 + <div class="aside"> 92 + <ImageEmbed embed={image} blur={false} /> 93 + </div> 94 + {:else if video} 95 + <div class="aside"> 96 + <VideoEmbed embed={video} blur={false} /> 97 + </div> 98 + {/if} 99 + {/if} 100 + 101 + <p class="text">{text}</p> 102 + </div> 103 + {:else} 104 + <div class="divide"></div> 105 + {/if} 106 + 107 + {#if large || !text} 108 + {#if image} 109 + <ImageEmbed embed={image} borderless blur={false} /> 110 + {:else if video} 111 + <VideoEmbed embed={video} borderless blur={false} /> 112 + {/if} 113 + {/if} 114 + </a> 115 + 116 + <style> 117 + .quote-embed { 118 + display: block; 119 + border: 1px solid var(--divider-md); 120 + border-radius: 6px; 121 + overflow: hidden; 122 + color: var(--text-primary); 123 + 124 + &:hover { 125 + background: var(--tap-sm); 126 + } 127 + } 128 + 129 + .meta { 130 + display: flex; 131 + padding: 12px 12px 0 12px; 132 + color: var(--text-blurb); 133 + 134 + .avatar-wrapper { 135 + flex-shrink: 0; 136 + margin: 0 8px 0 0; 137 + border-radius: 9999px; 138 + background: var(--bg-secondary); 139 + width: 20px; 140 + height: 20px; 141 + overflow: hidden; 142 + } 143 + .avatar { 144 + width: 100%; 145 + height: 100%; 146 + object-fit: cover; 147 + font-size: 0; 148 + } 149 + 150 + .name-wrapper { 151 + display: flex; 152 + gap: 4px; 153 + max-width: 100%; 154 + overflow: hidden; 155 + text-overflow: ellipsis; 156 + white-space: nowrap; 157 + } 158 + .display-name-wrapper { 159 + overflow: hidden; 160 + text-overflow: ellipsis; 161 + } 162 + .display-name { 163 + color: var(--text-primary); 164 + font-weight: 700; 165 + } 166 + .handle { 167 + display: block; 168 + overflow: hidden; 169 + text-overflow: ellipsis; 170 + white-space: nowrap; 171 + } 172 + 173 + .dot { 174 + flex-shrink: 0; 175 + margin: 0 6px; 176 + } 177 + 178 + .date { 179 + white-space: nowrap; 180 + } 181 + } 182 + 183 + .body { 184 + display: flex; 185 + align-items: flex-start; 186 + } 187 + 188 + .aside { 189 + flex-grow: 1; 190 + flex-basis: 0; 191 + margin: 8px 0 12px 12px; 192 + max-width: 20%; 193 + } 194 + 195 + .text { 196 + display: -webkit-box; 197 + margin: 8px 12px 12px 12px; 198 + overflow: hidden; 199 + -webkit-box-orient: vertical; 200 + -webkit-line-clamp: 6; 201 + line-clamp: 6; 202 + flex-grow: 4; 203 + flex-basis: 0px; 204 + min-width: 0px; 205 + white-space: pre-wrap; 206 + overflow-wrap: break-word; 207 + } 208 + 209 + .divide { 210 + padding: 6px 0; 211 + } 212 + </style>
+125
src/lib/components/embeds/starterpack-embed.svelte
··· 1 + <script lang="ts"> 2 + import type { AppBskyGraphDefs, AppBskyGraphStarterpack } from '@atcute/client/lexicons'; 3 + 4 + import { base } from '$app/paths'; 5 + 6 + import { parseAtUri } from '$lib/types/at-uri'; 7 + 8 + interface Props { 9 + embed: AppBskyGraphDefs.StarterPackViewBasic; 10 + large?: boolean; 11 + } 12 + 13 + const { embed: pack, large = false }: Props = $props(); 14 + 15 + const record = pack.record as AppBskyGraphStarterpack.Record; 16 + 17 + const creator = $derived(pack.creator); 18 + 19 + const rkey = $derived(parseAtUri(pack.uri).rkey); 20 + </script> 21 + 22 + <a href="{base}/{creator.did}/packs/{rkey}" class="starterpack-embed"> 23 + {#if large} 24 + <img 25 + loading="lazy" 26 + src="https://ogcard.cdn.bsky.app/start/${creator.did}/${rkey}" 27 + alt="" 28 + class="banner" 29 + /> 30 + {/if} 31 + 32 + <div class="meta"> 33 + <div class="main"> 34 + <svg fill="none" viewBox="0 0 24 24" class="avatar"> 35 + <defs> 36 + <linearGradient id="a" x1="0" x2="100%" y1="0" y2="0" gradientTransform="rotate(45)"> 37 + <stop offset="0" stop-color="#0A7AFF" /> 38 + <stop offset="1" stop-color="#59B9FF" /> 39 + </linearGradient> 40 + </defs> 41 + <path 42 + fill="url(#a)" 43 + fill-rule="evenodd" 44 + d="M11.26 5.227 5.02 6.899c-.734.197-1.17.95-.973 1.685l1.672 6.24c.197.734.951 1.17 1.685.973l6.24-1.672a1.376 1.376 0 0 0 .973-1.685L12.945 6.2a1.375 1.375 0 0 0-1.685-.973Zm-6.566.459a2.632 2.632 0 0 0-1.86 3.223l1.672 6.24a2.632 2.632 0 0 0 3.223 1.861l6.24-1.672a2.631 2.631 0 0 0 1.861-3.223l-1.672-6.24a2.632 2.632 0 0 0-3.223-1.861l-6.24 1.672Z" 45 + clip-rule="evenodd" 46 + /> 47 + <path 48 + fill="url(#a)" 49 + fill-rule="evenodd" 50 + d="M15.138 18.411a4.606 4.606 0 1 0 0-9.211 4.606 4.606 0 0 0 0 9.211Zm0 1.257a5.862 5.862 0 1 0 0-11.724 5.862 5.862 0 0 0 0 11.724Z" 51 + clip-rule="evenodd" 52 + /> 53 + </svg> 54 + 55 + <div class="info"> 56 + <p class="name">{record.name}</p> 57 + <p class="creator">Starter pack by @{creator.handle}</p> 58 + </div> 59 + </div> 60 + 61 + <p class="description">{record.description}</p> 62 + </div> 63 + </a> 64 + 65 + <style> 66 + .starterpack-embed { 67 + display: block; 68 + border: 1px solid var(--divider-md); 69 + border-radius: 6px; 70 + overflow: hidden; 71 + color: var(--text-primary); 72 + 73 + &:hover { 74 + background: var(--tap-sm); 75 + } 76 + } 77 + 78 + .banner { 79 + display: block; 80 + aspect-ratio: 1.91; 81 + width: 100%; 82 + } 83 + 84 + .meta { 85 + display: flex; 86 + flex-direction: column; 87 + gap: 12px; 88 + padding: 12px; 89 + } 90 + 91 + .main { 92 + display: flex; 93 + gap: 12px; 94 + } 95 + 96 + .avatar { 97 + margin: 2px; 98 + width: 36px; 99 + height: 36px; 100 + } 101 + 102 + .name { 103 + font-weight: 700; 104 + } 105 + 106 + .creator { 107 + color: var(--text-blurb); 108 + font-size: 0.8125rem; 109 + } 110 + 111 + .description { 112 + display: -webkit-box; 113 + overflow: hidden; 114 + font-size: 0.8125rem; 115 + white-space: pre-wrap; 116 + -webkit-box-orient: vertical; 117 + -webkit-line-clamp: 2; 118 + line-clamp: 2; 119 + overflow-wrap: break-word; 120 + 121 + &:empty { 122 + display: none; 123 + } 124 + } 125 + </style>
+108
src/lib/components/embeds/video-embed.svelte
··· 1 + <script lang="ts"> 2 + import type { AppBskyEmbedVideo } from '@atcute/client/lexicons'; 3 + 4 + import PlaySolid from '$lib/components/central-icons/play-solid.svelte'; 5 + 6 + interface Props { 7 + embed: AppBskyEmbedVideo.View; 8 + borderless?: boolean; 9 + standalone?: boolean; 10 + blur?: boolean; 11 + } 12 + 13 + const { embed: video, borderless, standalone, blur }: Props = $props(); 14 + 15 + const ratio = standalone && video.aspectRatio; 16 + </script> 17 + 18 + {#if standalone} 19 + <div class={['video-embed', !borderless && 'is-bordered', standalone && 'is-standalone']}> 20 + <div class="constrainer" style={ratio ? `aspect-ratio: ${ratio.width}/${ratio.height}` : ``}> 21 + {@render Content()} 22 + </div> 23 + </div> 24 + {:else} 25 + <div 26 + class={['video-embed', !borderless && 'is-bordered']} 27 + style={ratio ? `aspect-ratio: ${ratio.width}/${ratio.height}` : ``} 28 + > 29 + {@render Content()} 30 + </div> 31 + {/if} 32 + 33 + {#snippet Content()} 34 + <img loading="lazy" src={video.thumbnail} alt="" class={['thumbnail', blur && 'is-blurred']} /> 35 + 36 + {#if ratio} 37 + <div class="placeholder"></div> 38 + {/if} 39 + 40 + <div class="play"> 41 + <PlaySolid /> 42 + </div> 43 + {/snippet} 44 + 45 + <style> 46 + .video-embed { 47 + position: relative; 48 + background: var(--bg-secondary); 49 + aspect-ratio: 16 / 9; 50 + overflow: hidden; 51 + } 52 + .is-bordered { 53 + border: 1px solid var(--divider-md); 54 + border-radius: 6px; 55 + } 56 + .is-standalone { 57 + align-self: baseline; 58 + aspect-ratio: auto; 59 + max-width: 100%; 60 + } 61 + 62 + .constrainer { 63 + min-width: 64px; 64 + max-width: 100%; 65 + min-height: 64px; 66 + max-height: 320px; 67 + } 68 + 69 + .thumbnail { 70 + width: 100%; 71 + height: 100%; 72 + object-fit: contain; 73 + } 74 + .is-blurred { 75 + scale: 125%; 76 + filter: blur(24px); 77 + } 78 + 79 + .placeholder { 80 + width: 100vw; 81 + height: 100vh; 82 + } 83 + 84 + .play { 85 + display: grid; 86 + position: absolute; 87 + top: 50%; 88 + left: 50%; 89 + place-items: center; 90 + translate: -50% -50%; 91 + border-radius: 50%; 92 + background: rgba(64, 64, 64, 0.6); 93 + aspect-ratio: 1 / 1; 94 + height: 40%; 95 + max-height: 48px; 96 + color: #ffffff; 97 + font-size: 20px; 98 + 99 + :global(.sv-icon) { 100 + width: 40%; 101 + height: 40%; 102 + } 103 + 104 + .is-standalone &:hover { 105 + background: rgba(64, 64, 64, 0.8); 106 + } 107 + } 108 + </style>
+23
src/lib/components/page/page-container.svelte
··· 1 + <script lang="ts"> 2 + import type { Snippet } from 'svelte'; 3 + 4 + interface Props { 5 + children: Snippet<[]>; 6 + } 7 + 8 + const { children }: Props = $props(); 9 + </script> 10 + 11 + <div class="page-container"> 12 + {@render children()} 13 + </div> 14 + 15 + <style> 16 + .page-container { 17 + display: flex; 18 + flex-direction: column; 19 + margin: 24px auto; 20 + width: 100%; 21 + max-width: 600px; 22 + } 23 + </style>
+28
src/lib/components/page/page-header.svelte
··· 1 + <script lang="ts"> 2 + interface Props { 3 + title: string; 4 + } 5 + 6 + const { title }: Props = $props(); 7 + </script> 8 + 9 + <div class="page-header"> 10 + <h2 class="title">{title}</h2> 11 + </div> 12 + 13 + <style> 14 + .page-header { 15 + position: sticky; 16 + top: 0; 17 + border-bottom: 1px solid var(--divider-sm); 18 + background: var(--bg-primary); 19 + padding: 12px 16px; 20 + z-index: 1; 21 + } 22 + 23 + .title { 24 + font-weight: 600; 25 + font-size: 1rem; 26 + line-height: 1.5rem; 27 + } 28 + </style>
+59
src/lib/components/page/page-listing.svelte
··· 1 + <script lang="ts"> 2 + import type { Snippet } from 'svelte'; 3 + 4 + interface Props { 5 + subject: 'timeline' | 'posts' | 'profiles' | 'reposts' | 'likes'; 6 + root: boolean; 7 + cursor: string | undefined; 8 + children: Snippet<[]>; 9 + } 10 + 11 + const { subject, root, cursor, children }: Props = $props(); 12 + </script> 13 + 14 + <div class="page-listing"> 15 + {#if !root} 16 + <a href="?" class="button latest-button">Show latest {subject}</a> 17 + {/if} 18 + 19 + {@render children()} 20 + 21 + {#if cursor} 22 + <a href="?cursor={encodeURIComponent(cursor)}" class="button more-button"> 23 + {subject === 'timeline' ? `Show older posts` : `Show more ${subject}`} 24 + </a> 25 + {:else} 26 + <div class="end-marker"> 27 + {subject === 'timeline' ? `No more posts.` : `No more ${subject}.`} 28 + </div> 29 + {/if} 30 + </div> 31 + 32 + <style> 33 + .page-listing { 34 + background: var(--bg-primary); 35 + } 36 + 37 + .button { 38 + display: grid; 39 + place-items: center; 40 + height: 53px; 41 + font-weight: 500; 42 + 43 + &:hover { 44 + background: var(--tap-sm); 45 + } 46 + } 47 + .latest-button { 48 + border-bottom: 1px solid var(--divider-sm); 49 + } 50 + 51 + .end-marker { 52 + display: grid; 53 + place-items: center; 54 + height: 53px; 55 + color: var(--text-blurb); 56 + font-style: italic; 57 + font-weight: 500; 58 + } 59 + </style>
+127
src/lib/components/profiles/profile-item.svelte
··· 1 + <script lang="ts"> 2 + import type { AppBskyActorDefs } from '@atcute/client/lexicons'; 3 + 4 + import { base } from '$app/paths'; 5 + 6 + interface Props { 7 + item: AppBskyActorDefs.ProfileView | AppBskyActorDefs.ProfileViewBasic; 8 + } 9 + 10 + const { item: profile }: Props = $props(); 11 + 12 + const href = $derived(`${base}/${profile.did}`); 13 + </script> 14 + 15 + <div class="profile-item"> 16 + <div class="aside"> 17 + <a {href} tabindex={-1} class="avatar-wrapper"> 18 + {#if profile.avatar} 19 + <img loading="lazy" src={profile.avatar} alt="" class="avatar" /> 20 + {/if} 21 + </a> 22 + </div> 23 + 24 + <div class="main"> 25 + <div class="content"> 26 + <a {href} class="name-wrapper"> 27 + <p class="display-name">{profile.displayName}</p> 28 + <p class="handle">@{profile.handle}</p> 29 + </a> 30 + </div> 31 + 32 + {#if 'description' in profile && profile.description?.trim()} 33 + <p class="bio">{profile.description}</p> 34 + {/if} 35 + </div> 36 + </div> 37 + 38 + <style> 39 + .profile-item { 40 + display: flex; 41 + gap: 12px; 42 + contain: content; 43 + padding: 12px 16px; 44 + 45 + @media (hover: hover) { 46 + &:hover { 47 + background: var(--tap-sm); 48 + } 49 + } 50 + } 51 + 52 + .aside { 53 + display: flex; 54 + flex-shrink: 0; 55 + flex-direction: column; 56 + align-items: center; 57 + } 58 + 59 + .avatar-wrapper { 60 + display: block; 61 + margin: 2px 0 0; 62 + border-radius: 9999px; 63 + background: var(--bg-secondary); 64 + width: 36px; 65 + height: 36px; 66 + overflow: hidden; 67 + 68 + &:hover { 69 + filter: brightness(0.85); 70 + } 71 + } 72 + .avatar { 73 + width: 100%; 74 + height: 100%; 75 + object-fit: cover; 76 + font-size: 0; 77 + } 78 + 79 + .main { 80 + display: flex; 81 + flex-grow: 1; 82 + flex-direction: column; 83 + gap: 4px; 84 + min-width: 0; 85 + } 86 + 87 + .content { 88 + display: flex; 89 + justify-content: space-between; 90 + align-items: center; 91 + gap: 12px; 92 + margin: auto 0; 93 + height: 40px; 94 + } 95 + 96 + .name-wrapper { 97 + min-width: 0; 98 + } 99 + .display-name { 100 + overflow: hidden; 101 + color: var(--text-primary); 102 + font-weight: 600; 103 + text-overflow: ellipsis; 104 + white-space: nowrap; 105 + 106 + &:empty { 107 + color: var(--text-muted); 108 + } 109 + 110 + .name-wrapper:hover & { 111 + text-decoration: underline; 112 + } 113 + } 114 + .handle { 115 + overflow: hidden; 116 + color: var(--text-blurb); 117 + text-overflow: ellipsis; 118 + white-space: nowrap; 119 + } 120 + 121 + .bio { 122 + display: -webkit-box; 123 + overflow: hidden; 124 + -webkit-line-clamp: 2; 125 + -webkit-box-orient: vertical; 126 + } 127 + </style>
+56
src/lib/components/richtext-raw-renderer.svelte
··· 1 + <script lang="ts" module> 2 + const HTTP_RE = /^https?:\/\//; 3 + </script> 4 + 5 + <script lang="ts"> 6 + import { tokenize } from '@atcute/bluesky-richtext-parser'; 7 + 8 + interface Props { 9 + text: string; 10 + large?: boolean; 11 + } 12 + 13 + const { text, large }: Props = $props(); 14 + </script> 15 + 16 + <p class={`rich-text` + (large ? ` is-large` : ` is-small`)}> 17 + {#each tokenize(text) as token} 18 + {#if token.type === 'autolink'} 19 + <a target="_blank" href={token.url} rel="noopener nofollow" class="link"> 20 + {token.raw.replace(HTTP_RE, '')} 21 + </a> 22 + {:else if token.type === 'mention'} 23 + <a href="/{token.handle}" class="mention">{token.raw}</a> 24 + {:else if token.type === 'topic'} 25 + <span class="hashtag">{token.raw}</span> 26 + {:else} 27 + {token.raw} 28 + {/if} 29 + {/each} 30 + </p> 31 + 32 + <style> 33 + .rich-text { 34 + overflow: hidden; 35 + white-space: pre-wrap; 36 + overflow-wrap: break-word; 37 + 38 + &:empty { 39 + display: none; 40 + } 41 + } 42 + .is-large { 43 + font-size: 1rem; 44 + line-height: 1.5rem; 45 + } 46 + 47 + .link, 48 + .mention, 49 + .hashtag { 50 + color: var(--text-link); 51 + 52 + &:hover { 53 + text-decoration: underline; 54 + } 55 + } 56 + </style>
+66
src/lib/components/richtext-renderer.svelte
··· 1 + <script lang="ts" module> 2 + import { segmentize, type Facet, type FacetFeature } from '@atcute/bluesky-richtext-segmenter'; 3 + 4 + const grabFirstSupported = (features: FacetFeature[] | undefined): FacetFeature | undefined => { 5 + return features?.find( 6 + (feature) => 7 + feature.$type === 'app.bsky.richtext.facet#link' || 8 + feature.$type === 'app.bsky.richtext.facet#mention' || 9 + feature.$type === 'app.bsky.richtext.facet#tag', 10 + ); 11 + }; 12 + </script> 13 + 14 + <script lang="ts"> 15 + import { base } from '$app/paths'; 16 + 17 + interface Props { 18 + text: string; 19 + facets?: Facet[]; 20 + large?: boolean; 21 + } 22 + 23 + const { text, facets, large }: Props = $props(); 24 + </script> 25 + 26 + <p class={`rich-text` + (large ? ` is-large` : ` is-small`)}> 27 + {#each segmentize(text, facets) as segment} 28 + {@const feature = grabFirstSupported(segment.features)} 29 + 30 + {#if !feature} 31 + {segment.text} 32 + {:else if feature.$type === 'app.bsky.richtext.facet#link'} 33 + <a target="_blank" href={feature.uri} rel="noopener nofollow" class="link">{segment.text}</a> 34 + {:else if feature.$type === 'app.bsky.richtext.facet#mention'} 35 + <a href="{base}/{feature.did}" class="mention">{segment.text}</a> 36 + {:else if feature.$type === 'app.bsky.richtext.facet#tag'} 37 + <span class="hashtag">{segment.text}</span> 38 + {/if} 39 + {/each} 40 + </p> 41 + 42 + <style> 43 + .rich-text { 44 + overflow: hidden; 45 + white-space: pre-wrap; 46 + overflow-wrap: break-word; 47 + 48 + &:empty { 49 + display: none; 50 + } 51 + } 52 + .is-large { 53 + font-size: 1rem; 54 + line-height: 1.5rem; 55 + } 56 + 57 + .link, 58 + .mention, 59 + .hashtag { 60 + color: var(--text-link); 61 + 62 + &:hover { 63 + text-decoration: underline; 64 + } 65 + } 66 + </style>
+62
src/lib/components/threads/main-post-metrics.svelte
··· 1 + <script lang="ts"> 2 + import type { AppBskyFeedDefs } from '@atcute/client/lexicons'; 3 + 4 + import { base } from '$app/paths'; 5 + 6 + import { parseAtUri } from '$lib/types/at-uri'; 7 + import { formatCompactNumber } from '$lib/utils/intl/number'; 8 + 9 + interface Props { 10 + post: AppBskyFeedDefs.PostView; 11 + } 12 + 13 + const { post }: Props = $props(); 14 + 15 + const baseUrl = $derived(`${base}/${post.author.did}/${parseAtUri(post.uri).rkey}`); 16 + </script> 17 + 18 + {#snippet Stat(count: number | undefined, one: string, many: string, href: string)} 19 + {#if count !== undefined && count > 0} 20 + <a {href} class="stat"> 21 + <span class="count">{formatCompactNumber(count)}</span> 22 + <span class="label"> {count === 1 ? one : many}</span> 23 + </a> 24 + {/if} 25 + {/snippet} 26 + 27 + {#if post.repostCount || post.quoteCount || post.likeCount} 28 + <div class="main-post-metrics"> 29 + {@render Stat(post.repostCount, 'repost', 'reposts', `${baseUrl}/reposts`)} 30 + {@render Stat(post.quoteCount, 'quote', 'quotes', `${baseUrl}/quotes`)} 31 + {@render Stat(post.likeCount, 'like', 'likes', `${baseUrl}/likes`)} 32 + </div> 33 + {/if} 34 + 35 + <style> 36 + .main-post-metrics { 37 + display: flex; 38 + flex-wrap: wrap; 39 + gap: 16px; 40 + border-top: 1px solid var(--divider-md); 41 + padding: 16px 0; 42 + 43 + &:empty { 44 + display: none; 45 + } 46 + } 47 + 48 + .stat { 49 + color: var(--text-primary); 50 + 51 + &:hover { 52 + text-decoration: underline; 53 + } 54 + } 55 + 56 + .count { 57 + font-weight: 600; 58 + } 59 + .label { 60 + color: var(--text-blurb); 61 + } 62 + </style>
+167
src/lib/components/threads/main-post.svelte
··· 1 + <script lang="ts"> 2 + import type { AppBskyFeedDefs, AppBskyFeedPost } from '@atcute/client/lexicons'; 3 + 4 + import { base } from '$app/paths'; 5 + 6 + import { parseAtUri } from '$lib/types/at-uri'; 7 + import { formatLongDate } from '$lib/utils/intl/date'; 8 + 9 + import Embeds from '../embeds/embeds.svelte'; 10 + import RichTextRenderer from '../richtext-renderer.svelte'; 11 + 12 + import MainPostMetrics from './main-post-metrics.svelte'; 13 + 14 + interface Props { 15 + post: AppBskyFeedDefs.PostView; 16 + prev?: boolean; 17 + } 18 + 19 + const { post, prev = false }: Props = $props(); 20 + 21 + const author = $derived(post.author); 22 + const authorUrl = $derived(`/${author.did}`); 23 + const authorName = $derived(author.displayName?.trim()); 24 + 25 + const record = $derived(post.record as AppBskyFeedPost.Record); 26 + const postUrl = $derived(`${base}/${author.did}/${parseAtUri(post.uri).rkey}#main`); 27 + </script> 28 + 29 + <div class="highlighted-post"> 30 + <div class="meta"> 31 + {#if prev} 32 + <div class="ancestor-line-wrapper"> 33 + <div class="ancestor-line"></div> 34 + </div> 35 + {/if} 36 + 37 + <a href={authorUrl} class="avatar-wrapper"> 38 + {#if author.avatar} 39 + <img loading="lazy" src={author.avatar} alt="" class={`avatar`} /> 40 + {/if} 41 + </a> 42 + 43 + <a href={authorUrl} class="name-wrapper"> 44 + {#if authorName} 45 + <bdi class="display-name-wrapper"> 46 + <span class="display-name">{authorName}</span> 47 + </bdi> 48 + {/if} 49 + 50 + <span class="handle">@{author.handle}</span> 51 + </a> 52 + </div> 53 + 54 + <RichTextRenderer text={record.text} facets={record.facets} large /> 55 + 56 + {#if post.embed} 57 + <Embeds embed={post.embed} large /> 58 + {/if} 59 + 60 + <div class="footer"> 61 + <a href={postUrl} class="date"> 62 + <time datetime={record.createdAt}> 63 + {formatLongDate(record.createdAt)} 64 + </time> 65 + </a> 66 + </div> 67 + 68 + <MainPostMetrics {post} /> 69 + </div> 70 + 71 + <style> 72 + .highlighted-post { 73 + padding: 12px 16px 0 16px; 74 + } 75 + 76 + .meta { 77 + display: flex; 78 + position: relative; 79 + align-items: center; 80 + gap: 12px; 81 + margin: 0 0 12px 0; 82 + color: var(--text-blurb); 83 + } 84 + 85 + .avatar-wrapper { 86 + display: block; 87 + flex-shrink: 0; 88 + border-radius: 9999px; 89 + background: var(--bg-secondary); 90 + width: 40px; 91 + height: 40px; 92 + overflow: hidden; 93 + 94 + &:hover { 95 + filter: brightness(0.85); 96 + } 97 + } 98 + 99 + .avatar { 100 + width: 100%; 101 + height: 100%; 102 + object-fit: cover; 103 + } 104 + .is-blurred { 105 + scale: 125%; 106 + filter: blur(4px); 107 + } 108 + 109 + .name-wrapper { 110 + display: block; 111 + flex-grow: 1; 112 + max-width: 100%; 113 + overflow: hidden; 114 + color: inherit; 115 + text-overflow: ellipsis; 116 + white-space: nowrap; 117 + } 118 + .display-name-wrapper { 119 + overflow: hidden; 120 + text-overflow: ellipsis; 121 + 122 + .name-wrapper:hover & { 123 + text-decoration: underline; 124 + } 125 + } 126 + .display-name { 127 + color: var(--text-primary); 128 + font-weight: 700; 129 + } 130 + .handle { 131 + display: block; 132 + overflow: hidden; 133 + text-overflow: ellipsis; 134 + white-space: nowrap; 135 + } 136 + 137 + .footer { 138 + display: flex; 139 + flex-wrap: wrap; 140 + align-items: center; 141 + gap: 8px; 142 + margin: 12px 0 0; 143 + padding: 0 0 12px 0; 144 + } 145 + .date { 146 + color: var(--text-blurb); 147 + 148 + &:hover { 149 + text-decoration: underline; 150 + } 151 + } 152 + 153 + .ancestor-line-wrapper { 154 + display: flex; 155 + position: absolute; 156 + bottom: 100%; 157 + flex-direction: column; 158 + align-items: center; 159 + margin: 0 0 4px 0; 160 + width: 36px; 161 + height: 12px; 162 + } 163 + .ancestor-line { 164 + flex-grow: 1; 165 + border-left: 2px solid var(--divider-md); 166 + } 167 + </style>
+107
src/lib/components/threads/overflow-thread-item.svelte
··· 1 + <script lang="ts"> 2 + import type { OverflowAncestorItem, OverflowDescendantItem } from '$lib/models/thread'; 3 + import { parseAtUri } from '$lib/types/at-uri'; 4 + 5 + import { base } from '$app/paths'; 6 + 7 + import DotGrid_1x3HorizontalOutlined from '$lib/components/central-icons/dot-grid-1x3-horizontal-outlined.svelte'; 8 + 9 + import TreeLines from './tree-lines.svelte'; 10 + 11 + interface Props { 12 + item: OverflowAncestorItem | OverflowDescendantItem; 13 + treeView: boolean; 14 + descendant: boolean; 15 + } 16 + 17 + const { item, treeView, descendant }: Props = $props(); 18 + 19 + const postUrl = $derived.by(() => { 20 + const uri = parseAtUri(item.uri); 21 + return `${base}/${uri.repo}/${uri.rkey}#main`; 22 + }); 23 + </script> 24 + 25 + <a 26 + href={postUrl} 27 + class={['overflow-thread-item', treeView ? 'is-tree' : 'is-flat', !descendant && 'has-next']} 28 + > 29 + {#if treeView} 30 + <TreeLines lines={item.lines} /> 31 + {:else} 32 + <div class="dots"> 33 + <div class="dot"></div> 34 + <div class="dot"></div> 35 + <div class="dot"></div> 36 + </div> 37 + {/if} 38 + 39 + <div class="content"> 40 + {#if treeView} 41 + <div class="circle"> 42 + <DotGrid_1x3HorizontalOutlined /> 43 + </div> 44 + {/if} 45 + 46 + <span class="label"> 47 + {!descendant ? `See parent replies` : `Continue thread`} 48 + </span> 49 + </div> 50 + </a> 51 + 52 + <style> 53 + .overflow-thread-item { 54 + display: flex; 55 + user-select: none; 56 + 57 + @media (hover: hover) { 58 + &:hover { 59 + background: var(--tap-sm); 60 + } 61 + } 62 + } 63 + .is-tree { 64 + padding: 0 12px; 65 + } 66 + 67 + .is-flat { 68 + padding: 0 16px; 69 + 70 + &:not(.has-next) { 71 + border-bottom: 2px solid var(--divider-sm); 72 + } 73 + } 74 + 75 + .dots { 76 + display: flex; 77 + flex-shrink: 0; 78 + flex-direction: column; 79 + justify-content: center; 80 + align-items: center; 81 + gap: 6px; 82 + margin: 0 12px 0 0; 83 + width: 36px; 84 + } 85 + .dot { 86 + border-left: 2px solid var(--divider-md); 87 + height: 2px; 88 + } 89 + 90 + .content { 91 + display: flex; 92 + align-items: center; 93 + gap: 12px; 94 + padding: 12px 0; 95 + } 96 + 97 + .circle { 98 + display: grid; 99 + place-items: center; 100 + border-radius: 9999px; 101 + background: var(--divider-md); 102 + width: 20px; 103 + height: 20px; 104 + color: var(--text-blurb); 105 + font-size: 12px; 106 + } 107 + </style>
+168
src/lib/components/threads/post-thread-item.svelte
··· 1 + <script lang="ts"> 2 + import type { AppBskyFeedPost } from '@atcute/client/lexicons'; 3 + 4 + import { base } from '$app/paths'; 5 + 6 + import { type PostAncestorItem, type PostDescendantItem } from '$lib/models/thread'; 7 + import { parseAtUri } from '$lib/types/at-uri'; 8 + 9 + import Embeds from '../embeds/embeds.svelte'; 10 + import RichtextRenderer from '../richtext-renderer.svelte'; 11 + import PostMeta from '../timeline/post-meta.svelte'; 12 + import PostMetrics from '../timeline/post-metrics.svelte'; 13 + 14 + import TreeLines from './tree-lines.svelte'; 15 + 16 + interface Props { 17 + item: PostAncestorItem | PostDescendantItem; 18 + treeView: boolean; 19 + } 20 + 21 + const { item, treeView }: Props = $props(); 22 + 23 + const post = $derived(item.post); 24 + 25 + const author = $derived(post.author); 26 + const authorUrl = $derived(`${base}/${author.did}`); 27 + 28 + const record = $derived(post.record as AppBskyFeedPost.Record); 29 + const postUrl = $derived(`${base}/${author.did}/${parseAtUri(post.uri).rkey}#main`); 30 + </script> 31 + 32 + <div 33 + class={[ 34 + 'post-thread-item', 35 + treeView ? 'is-tree' : 'is-flat', 36 + item.prev && 'has-prev', 37 + item.next && 'has-next', 38 + ]} 39 + > 40 + {#if treeView} 41 + <TreeLines lines={item.lines} /> 42 + {/if} 43 + 44 + <div class="content"> 45 + <div class="aside"> 46 + <div class="ascendant-line"></div> 47 + 48 + <a href={authorUrl} class="avatar-wrapper"> 49 + {#if author.avatar} 50 + <img loading="lazy" src={author.avatar} alt="" class="avatar" /> 51 + {/if} 52 + </a> 53 + 54 + <div class="descendant-line"></div> 55 + </div> 56 + 57 + <div class="main"> 58 + <PostMeta {post} {postUrl} {authorUrl} gutterBottom /> 59 + 60 + <RichtextRenderer text={record.text} facets={record.facets} /> 61 + 62 + {#if post.embed} 63 + <Embeds embed={post.embed} /> 64 + {/if} 65 + 66 + <PostMetrics {post} /> 67 + </div> 68 + </div> 69 + </div> 70 + 71 + <style> 72 + .post-thread-item { 73 + display: flex; 74 + contain: content; 75 + 76 + @media (hover: hover) { 77 + &:hover { 78 + background: var(--tap-sm); 79 + } 80 + } 81 + } 82 + .is-tree { 83 + padding: 0 12px; 84 + } 85 + 86 + .is-flat { 87 + padding: 0 16px; 88 + 89 + &:not(.has-next) { 90 + border-bottom: 2px solid var(--divider-sm); 91 + } 92 + } 93 + 94 + .content { 95 + display: flex; 96 + flex-grow: 1; 97 + gap: 12px; 98 + min-width: 0; 99 + 100 + .is-tree & { 101 + gap: 8px; 102 + } 103 + } 104 + 105 + .ascendant-line { 106 + display: none; 107 + position: absolute; 108 + top: 0; 109 + border-left: 2px solid var(--divider-md); 110 + height: 8px; 111 + 112 + .is-flat.has-prev & { 113 + display: block; 114 + } 115 + } 116 + .descendant-line { 117 + display: none; 118 + flex-grow: 1; 119 + margin: 4px 0 0 0; 120 + border-left: 2px solid var(--divider-md); 121 + 122 + .is-tree & { 123 + margin: 2px 0 0 0; 124 + } 125 + .has-next & { 126 + display: block; 127 + } 128 + } 129 + 130 + .aside { 131 + display: flex; 132 + position: relative; 133 + flex-shrink: 0; 134 + flex-direction: column; 135 + align-items: center; 136 + padding-top: 12px; 137 + } 138 + 139 + .avatar-wrapper { 140 + display: block; 141 + border-radius: 9999px; 142 + background: var(--bg-secondary); 143 + width: 36px; 144 + height: 36px; 145 + overflow: hidden; 146 + 147 + .is-tree & { 148 + width: 20px; 149 + height: 20px; 150 + } 151 + 152 + &:hover { 153 + filter: brightness(0.85); 154 + } 155 + } 156 + .avatar { 157 + width: 100%; 158 + height: 100%; 159 + object-fit: cover; 160 + font-size: 0; 161 + } 162 + 163 + .main { 164 + flex-grow: 1; 165 + padding: 12px 0; 166 + min-width: 0; 167 + } 168 + </style>
+51
src/lib/components/threads/tree-lines.svelte
··· 1 + <script lang="ts"> 2 + import { LineType } from '$lib/models/thread'; 3 + 4 + interface Props { 5 + lines: LineType[] | undefined; 6 + } 7 + 8 + const { lines = [] }: Props = $props(); 9 + </script> 10 + 11 + <div class="tree-lines"> 12 + {#each lines as line} 13 + <div class="line"> 14 + {#if line === LineType.UP_RIGHT || line === LineType.VERTICAL_RIGHT} 15 + <div class="line-draw-right"></div> 16 + {/if} 17 + {#if line === LineType.VERTICAL || line === LineType.VERTICAL_RIGHT} 18 + <div class="line-draw-vertical"></div> 19 + {/if} 20 + </div> 21 + {/each} 22 + </div> 23 + 24 + <style> 25 + .tree-lines { 26 + display: contents; 27 + } 28 + 29 + .line { 30 + position: relative; 31 + flex-shrink: 0; 32 + width: 20px; 33 + } 34 + .line-draw-right { 35 + position: absolute; 36 + top: 0; 37 + right: 2px; 38 + border-bottom: 2px solid var(--divider-md); 39 + border-left: 2px solid var(--divider-md); 40 + border-bottom-left-radius: 9px; 41 + width: 9px; 42 + height: 22px; 43 + } 44 + .line-draw-vertical { 45 + position: absolute; 46 + top: 0; 47 + bottom: 0; 48 + left: 9px; 49 + border-left: 2px solid var(--divider-md); 50 + } 51 + </style>
+245
src/lib/components/timeline/post-feed-item.svelte
··· 1 + <script lang="ts"> 2 + import type { AppBskyFeedPost } from '@atcute/client/lexicons'; 3 + 4 + import { base } from '$app/paths'; 5 + 6 + import type { UiTimelineItem } from '$lib/models/timeline'; 7 + import { parseAtUri } from '$lib/types/at-uri'; 8 + 9 + import ArrowsRepeatRightLeftOutlined from '$lib/components/central-icons/arrows-repeat-right-left-outlined.svelte'; 10 + import PinOutlined from '$lib/components/central-icons/pin-outlined.svelte'; 11 + import Embeds from '$lib/components/embeds/embeds.svelte'; 12 + import RichtextRenderer from '$lib/components/richtext-renderer.svelte'; 13 + 14 + import PostMeta from './post-meta.svelte'; 15 + import PostMetrics from './post-metrics.svelte'; 16 + 17 + interface Props { 18 + item: UiTimelineItem; 19 + } 20 + 21 + const { item }: Props = $props(); 22 + 23 + const post = $derived(item.post); 24 + 25 + const author = $derived(post.author); 26 + const authorUrl = $derived(`${base}/${author.did}`); 27 + 28 + const record = $derived(post.record as AppBskyFeedPost.Record); 29 + const postUrl = $derived(`${base}/${author.did}/${parseAtUri(post.uri).rkey}#main`); 30 + </script> 31 + 32 + <div class={['post-feed-item', !item.next && `is-leaf`]}> 33 + <div class="contexts"> 34 + {#if item.prev} 35 + <div class="ascendant-line-wrapper"> 36 + <div class="line"></div> 37 + </div> 38 + {/if} 39 + 40 + {#if item.reason} 41 + {@const reason = item.reason} 42 + 43 + {#if reason.$type === 'app.bsky.feed.defs#reasonRepost'} 44 + {@const by = reason.by} 45 + 46 + <div class="context"> 47 + <div class="aside"> 48 + <ArrowsRepeatRightLeftOutlined /> 49 + </div> 50 + <a href="/{by.did}" class="main"> 51 + <span dir="auto" class="name">{by.displayName?.trim() || by.handle.slice(0, 64)}</span> 52 + <span class="affix">{' '}reposted</span> 53 + </a> 54 + </div> 55 + {:else if reason.$type === 'app.bsky.feed.defs#reasonPin'} 56 + <div class="context"> 57 + <div class="aside"> 58 + <PinOutlined /> 59 + </div> 60 + <span class="main">Pinned</span> 61 + </div> 62 + {/if} 63 + {/if} 64 + </div> 65 + 66 + <div class="content"> 67 + <div class="aside"> 68 + <a tabindex={-1} href={authorUrl} class="avatar-wrapper"> 69 + {#if author.avatar} 70 + <img loading="lazy" src={author.avatar} alt="" class="avatar" /> 71 + {/if} 72 + </a> 73 + 74 + {#if item.next} 75 + <div class="descendant-line"></div> 76 + {/if} 77 + </div> 78 + 79 + <div class="main"> 80 + <PostMeta {post} {postUrl} {authorUrl} gutterBottom /> 81 + 82 + {#if !item.prev && record.reply} 83 + {@const parent = item.reply?.parent} 84 + 85 + <p class="reply-context"> 86 + {#if parent && parent.$type === 'app.bsky.feed.defs#postView'} 87 + {@const author = parent.author} 88 + 89 + Replying to 90 + <a href="/{author.did}" dir="auto"> 91 + {author.displayName?.trim() || `@${author.handle}`} 92 + </a> 93 + {:else} 94 + Replying to an unknown post 95 + {/if} 96 + </p> 97 + {/if} 98 + 99 + <RichtextRenderer text={record.text} facets={record.facets} /> 100 + 101 + {#if post.embed} 102 + <Embeds embed={post.embed} /> 103 + {/if} 104 + 105 + <PostMetrics {post} /> 106 + </div> 107 + </div> 108 + </div> 109 + 110 + <style> 111 + .post-feed-item { 112 + contain: content; 113 + padding: 0 16px; 114 + 115 + /* content-visibility: auto; */ 116 + /* contain-intrinsic-height: 99px; */ 117 + 118 + @media (hover: hover) { 119 + &:hover { 120 + background: var(--tap-sm); 121 + } 122 + } 123 + } 124 + .is-leaf { 125 + border-bottom: 1px solid var(--divider-sm); 126 + } 127 + 128 + .ascendant-line-wrapper { 129 + display: flex; 130 + flex-direction: column; 131 + align-items: center; 132 + width: 36px; 133 + 134 + .line { 135 + position: absolute; 136 + top: 0; 137 + bottom: 4px; 138 + flex-grow: 1; 139 + border-left: 2px solid var(--divider-md); 140 + } 141 + } 142 + .descendant-line { 143 + flex-grow: 1; 144 + margin-top: 4px; 145 + border-left: 2px solid var(--divider-md); 146 + } 147 + 148 + .contexts { 149 + display: flex; 150 + position: relative; 151 + flex-direction: column; 152 + padding: 8px 0 4px 0; 153 + } 154 + .context { 155 + display: flex; 156 + align-items: center; 157 + gap: 12px; 158 + color: var(--text-blurb); 159 + font-size: 0.8125rem; 160 + line-height: 1.25rem; 161 + 162 + .aside { 163 + display: flex; 164 + flex-shrink: 0; 165 + justify-content: flex-end; 166 + width: 36px; 167 + } 168 + 169 + .main { 170 + display: flex; 171 + min-width: 0px; 172 + color: inherit; 173 + 174 + &[href]:hover { 175 + text-decoration-line: underline; 176 + } 177 + } 178 + 179 + .name { 180 + overflow: hidden; 181 + font-weight: 500; 182 + text-overflow: ellipsis; 183 + white-space: nowrap; 184 + } 185 + 186 + .affix { 187 + flex-shrink: 0; 188 + white-space: pre; 189 + } 190 + } 191 + 192 + .content { 193 + display: flex; 194 + gap: 12px; 195 + 196 + .aside { 197 + display: flex; 198 + flex-shrink: 0; 199 + flex-direction: column; 200 + align-items: center; 201 + } 202 + 203 + .main { 204 + flex-grow: 1; 205 + padding-bottom: 12px; 206 + min-width: 0; 207 + } 208 + } 209 + 210 + .avatar-wrapper { 211 + display: block; 212 + border-radius: 9999px; 213 + background: var(--bg-secondary); 214 + width: 36px; 215 + height: 36px; 216 + overflow: hidden; 217 + 218 + &:hover { 219 + filter: brightness(0.85); 220 + } 221 + } 222 + .avatar { 223 + width: 100%; 224 + height: 100%; 225 + object-fit: cover; 226 + font-size: 0; 227 + } 228 + 229 + .reply-context { 230 + overflow: hidden; 231 + color: var(--text-blurb); 232 + font-size: 0.8125rem; 233 + text-overflow: ellipsis; 234 + white-space: nowrap; 235 + 236 + a { 237 + color: inherit; 238 + font-weight: 500; 239 + 240 + &:hover { 241 + text-decoration: underline; 242 + } 243 + } 244 + } 245 + </style>
+94
src/lib/components/timeline/post-meta.svelte
··· 1 + <script lang="ts"> 2 + import type { AppBskyFeedDefs, AppBskyFeedPost } from '@atcute/client/lexicons'; 3 + 4 + import { formatLongDate, formatRelativeTime } from '$lib/utils/intl/date'; 5 + 6 + interface Props { 7 + post: AppBskyFeedDefs.PostView; 8 + postUrl: string; 9 + authorUrl: string; 10 + gutterBottom?: boolean; 11 + } 12 + 13 + const { post, postUrl, authorUrl, gutterBottom = false }: Props = $props(); 14 + 15 + const author = $derived(post.author); 16 + const authorName = $derived(author.displayName?.trim()); 17 + 18 + const createdAt = $derived((post.record as AppBskyFeedPost.Record).createdAt); 19 + </script> 20 + 21 + <div class={['post-meta', gutterBottom && 'has-bottom-gutter']}> 22 + <a href={authorUrl} class="name-wrapper"> 23 + {#if authorName} 24 + <bdi class="display-name-wrapper"> 25 + <span class="display-name">{authorName}</span> 26 + </bdi> 27 + {/if} 28 + 29 + <span class="handle">@{author.handle}</span> 30 + </a> 31 + 32 + <span aria-hidden="true" class="dot"> · </span> 33 + 34 + <a href={postUrl} title={formatLongDate(createdAt)} class="date"> 35 + <time datetime={createdAt}>{formatRelativeTime(createdAt)}</time> 36 + </a> 37 + </div> 38 + 39 + <style> 40 + .post-meta { 41 + display: flex; 42 + align-items: center; 43 + color: var(--text-blurb); 44 + } 45 + .has-bottom-gutter { 46 + margin-bottom: 2px; 47 + } 48 + 49 + .name-wrapper { 50 + display: flex; 51 + gap: 4px; 52 + max-width: 100%; 53 + overflow: hidden; 54 + color: inherit; 55 + text-decoration: none; 56 + text-overflow: ellipsis; 57 + white-space: nowrap; 58 + } 59 + 60 + .display-name-wrapper { 61 + overflow: hidden; 62 + text-overflow: ellipsis; 63 + 64 + .name-wrapper:hover & { 65 + text-decoration: underline; 66 + } 67 + } 68 + .display-name { 69 + color: var(--text-primary); 70 + font-weight: 700; 71 + } 72 + 73 + .handle { 74 + display: block; 75 + overflow: hidden; 76 + text-overflow: ellipsis; 77 + white-space: nowrap; 78 + } 79 + 80 + .dot { 81 + flex-shrink: 0; 82 + margin: 0 6px; 83 + } 84 + 85 + .date { 86 + color: inherit; 87 + text-decoration: none; 88 + white-space: nowrap; 89 + 90 + &:hover { 91 + text-decoration: underline; 92 + } 93 + } 94 + </style>
+67
src/lib/components/timeline/post-metrics.svelte
··· 1 + <script lang="ts"> 2 + import type { Component } from 'svelte'; 3 + 4 + import type { AppBskyFeedDefs } from '@atcute/client/lexicons'; 5 + 6 + import { formatCompactNumber, formatLongNumber } from '$lib/utils/intl/number'; 7 + 8 + import ArrowsRepeatRightLeftOutlined from '$lib/components/central-icons/arrows-repeat-right-left-outlined.svelte'; 9 + import Bubble_2Outlined from '$lib/components/central-icons/bubble-2-outlined.svelte'; 10 + import HeartOutlined from '$lib/components/central-icons/heart-outlined.svelte'; 11 + 12 + interface Props { 13 + post: AppBskyFeedDefs.PostView; 14 + } 15 + 16 + const { post }: Props = $props(); 17 + 18 + const replyCount = $derived(post.replyCount || 0); 19 + const likeCount = $derived(post.likeCount || 0); 20 + const repostCount = $derived((post.repostCount || 0) + (post.quoteCount || 0)); 21 + </script> 22 + 23 + {#snippet Stat(count: number, Icon: Component, one: string, many: string)} 24 + <div 25 + title={count === 1 ? `${formatLongNumber(count)} ${one}` : `${formatLongNumber(count)} ${many}`} 26 + class="stat" 27 + > 28 + <Icon /> 29 + 30 + <span class="count"> 31 + {formatCompactNumber(count)} 32 + </span> 33 + </div> 34 + {/snippet} 35 + 36 + <div class="post-metrics"> 37 + {@render Stat(replyCount, Bubble_2Outlined, 'reply', 'replies')} 38 + {@render Stat(repostCount, ArrowsRepeatRightLeftOutlined, 'repost', 'reposts')} 39 + {@render Stat(likeCount, HeartOutlined, 'like', 'likes')} 40 + </div> 41 + 42 + <style> 43 + .post-metrics { 44 + display: flex; 45 + align-items: center; 46 + gap: 16px; 47 + margin-top: 12px; 48 + color: var(--text-blurb); 49 + } 50 + 51 + .stat { 52 + display: flex; 53 + align-items: center; 54 + gap: 8px; 55 + min-width: 0px; 56 + max-width: 100%; 57 + } 58 + 59 + .count { 60 + padding-right: 8px; 61 + overflow: hidden; 62 + font-size: 0.8125rem; 63 + line-height: 1.25rem; 64 + text-overflow: ellipsis; 65 + white-space: nowrap; 66 + } 67 + </style>
+269
src/lib/models/thread.ts
··· 1 + import type { Brand, AppBskyFeedDefs, AppBskyFeedPost } from '@atcute/client/lexicons'; 2 + 3 + export const enum LineType { 4 + // <empty> 5 + NONE, 6 + // │ 7 + VERTICAL, 8 + // ├ 9 + VERTICAL_RIGHT, 10 + // └ 11 + UP_RIGHT, 12 + } 13 + 14 + interface BaseAncestor { 15 + id: string; 16 + lines?: undefined; 17 + } 18 + 19 + export interface BlockedAncestorItem extends BaseAncestor { 20 + type: 'blocked'; 21 + uri: string; 22 + } 23 + export interface NonexistentAncestorItem extends BaseAncestor { 24 + type: 'nonexistent'; 25 + uri: string; 26 + } 27 + export interface OverflowAncestorItem extends BaseAncestor { 28 + type: 'overflow'; 29 + uri: string; 30 + } 31 + export interface PostAncestorItem extends BaseAncestor { 32 + type: 'post'; 33 + post: AppBskyFeedDefs.PostView; 34 + prev: boolean; 35 + next: boolean; 36 + } 37 + 38 + export type AncestorItem = 39 + | BlockedAncestorItem 40 + | NonexistentAncestorItem 41 + | OverflowAncestorItem 42 + | PostAncestorItem; 43 + 44 + interface BaseDescendant { 45 + id: string; 46 + lines: LineType[]; 47 + } 48 + 49 + export interface BlockedDescendantItem extends BaseDescendant { 50 + type: 'blocked'; 51 + uri: string; 52 + } 53 + export interface OverflowDescendantItem extends BaseDescendant { 54 + type: 'overflow'; 55 + uri: string; 56 + } 57 + export interface PostDescendantItem extends BaseDescendant { 58 + type: 'post'; 59 + post: AppBskyFeedDefs.PostView; 60 + prev: boolean; 61 + next: boolean; 62 + } 63 + 64 + export type DescendantItem = BlockedDescendantItem | OverflowDescendantItem | PostDescendantItem; 65 + 66 + export interface ThreadData { 67 + post: AppBskyFeedDefs.PostView; 68 + ancestors: AncestorItem[]; 69 + descendants: DescendantItem[]; 70 + } 71 + 72 + export const createThreadData = ({ 73 + thread, 74 + treeView, 75 + }: { 76 + thread: Brand.Union<AppBskyFeedDefs.ThreadViewPost>; 77 + treeView?: boolean; 78 + }): ThreadData => { 79 + let ancestors: AncestorItem[]; 80 + let descendants: DescendantItem[]; 81 + 82 + { 83 + let parent = thread.parent; 84 + 85 + ancestors = []; 86 + 87 + while (parent) { 88 + const type = parent.$type; 89 + if (type === 'app.bsky.feed.defs#blockedPost') { 90 + const uri = parent.uri; 91 + 92 + ancestors.push({ id: uri, type: 'blocked', uri: uri }); 93 + } else if (type === 'app.bsky.feed.defs#notFoundPost') { 94 + const uri = parent.uri; 95 + 96 + ancestors.push({ id: uri, type: 'nonexistent', uri: uri }); 97 + } else if (type === 'app.bsky.feed.defs#threadViewPost') { 98 + const post = parent.post; 99 + 100 + ancestors.push({ id: post.uri, type: 'post', post: post, prev: true, next: true }); 101 + parent = parent.parent; 102 + 103 + continue; 104 + } 105 + 106 + break; 107 + } 108 + 109 + { 110 + const last = ancestors[ancestors.length - 1]; 111 + 112 + if (last && last.type === 'post') { 113 + const post = last.post; 114 + const reply = (post.record as AppBskyFeedPost.Record).reply; 115 + 116 + if (reply) { 117 + const uri = reply.parent.uri; 118 + 119 + ancestors.push({ id: uri, type: 'overflow', uri: uri }); 120 + } else { 121 + last.prev = false; 122 + } 123 + } 124 + } 125 + 126 + ancestors.reverse(); 127 + } 128 + 129 + { 130 + const traverse = ( 131 + parent: AppBskyFeedDefs.PostView, 132 + replies: AppBskyFeedDefs.ThreadViewPost['replies'] | undefined, 133 + depth: number, 134 + lines: LineType[], 135 + ): DescendantItem[] => { 136 + if (!replies || replies.length === 0) { 137 + if (depth !== 0 && parent.replyCount) { 138 + return [ 139 + { 140 + id: 'overflow-' + parent.uri, 141 + type: 'overflow', 142 + uri: parent.uri, 143 + lines: treeView ? lines.concat(LineType.UP_RIGHT) : lines, 144 + }, 145 + ]; 146 + } 147 + 148 + return []; 149 + } 150 + 151 + // Filter the replies to only what we want 152 + const items = replies.filter( 153 + (x): x is Brand.Union<AppBskyFeedDefs.ThreadViewPost | AppBskyFeedDefs.BlockedPost> => { 154 + const type = x.$type; 155 + 156 + return type === 'app.bsky.feed.defs#threadViewPost' || type === 'app.bsky.feed.defs#blockedPost'; 157 + }, 158 + ); 159 + 160 + // Sort the replies 161 + const did = parent.author.did; 162 + items.sort((a, b) => { 163 + if (a.$type !== 'app.bsky.feed.defs#threadViewPost') { 164 + return 1; 165 + } 166 + if (b.$type !== 'app.bsky.feed.defs#threadViewPost') { 167 + return -1; 168 + } 169 + 170 + const aPost = a.post; 171 + const aAuthor = aPost.author; 172 + const aIndexed = new Date(aPost.indexedAt).getTime(); 173 + 174 + const bPost = b.post; 175 + const bAuthor = bPost.author; 176 + const bIndexed = new Date(aPost.indexedAt).getTime(); 177 + 178 + // Prioritize replies from parent's author 179 + { 180 + const aIsByOp = aAuthor.did === did; 181 + const bIsByOp = bAuthor.did === did; 182 + 183 + if (aIsByOp && bIsByOp) { 184 + // Prioritize oldest first for own reply 185 + return aIndexed - bIndexed; 186 + } else if (aIsByOp) { 187 + return -1; 188 + } else if (bIsByOp) { 189 + return 1; 190 + } 191 + } 192 + 193 + return getHotness(bPost, bIndexed) - getHotness(aPost, aIndexed); 194 + }); 195 + 196 + // Iterate through the replies 197 + const array: DescendantItem[] = []; 198 + for (let idx = 0, len = items.length; idx < len; idx++) { 199 + const reply = items[idx]; 200 + const type = reply.$type; 201 + 202 + const end = idx === len - 1; 203 + const nlines = 204 + treeView && depth !== 0 ? lines.concat(end ? LineType.UP_RIGHT : LineType.VERTICAL_RIGHT) : lines; 205 + 206 + if (type === 'app.bsky.feed.defs#threadViewPost') { 207 + const post = reply.post; 208 + const children = traverse( 209 + post, 210 + reply.replies, 211 + depth + 1, 212 + treeView && depth !== 0 ? lines.concat(end ? LineType.NONE : LineType.VERTICAL) : lines, 213 + ); 214 + 215 + array.push({ 216 + id: post.uri, 217 + type: 'post', 218 + post: post, 219 + prev: depth !== 0, 220 + next: children.length !== 0, 221 + lines: nlines, 222 + }); 223 + 224 + push(array, children); 225 + } else if (type === 'app.bsky.feed.defs#blockedPost') { 226 + array.push({ 227 + id: reply.uri, 228 + type: 'blocked', 229 + uri: reply.uri, 230 + lines: nlines, 231 + }); 232 + } 233 + 234 + if (!treeView && depth !== 0) { 235 + break; 236 + } 237 + } 238 + 239 + return array; 240 + }; 241 + 242 + descendants = traverse(thread.post, thread.replies, 0, []); 243 + } 244 + 245 + return { 246 + post: thread.post, 247 + ancestors: ancestors, 248 + descendants: descendants, 249 + }; 250 + }; 251 + 252 + const push = <T>(target: T[], source: T[]) => { 253 + for (let idx = 0, len = source.length; idx < len; idx++) { 254 + const item = source[idx]; 255 + target.push(item); 256 + } 257 + }; 258 + 259 + // https://github.com/bluesky-social/social-app/blob/e9a792e4c1e85760fd073def21aa9e921e3afa3c/src/state/queries/post-thread.ts#L276 260 + const getHotness = (post: AppBskyFeedDefs.PostView, indexedAt: number) => { 261 + const hoursAgo = (Date.now() - indexedAt) / (1000 * 60 * 60); 262 + 263 + const likeCount = post.likeCount ?? 0; 264 + const likeOrder = Math.log(3 + likeCount); 265 + const timePenaltyExponent = 1.5 + 1.5 / (1 + Math.log(1 + likeCount)); 266 + const timePenalty = Math.pow(hoursAgo + 2, timePenaltyExponent); 267 + 268 + return likeOrder / timePenalty; 269 + };
+155
src/lib/models/timeline.ts
··· 1 + import type { AppBskyFeedDefs } from '@atcute/client/lexicons'; 2 + 3 + export type TimelineItem = AppBskyFeedDefs.FeedViewPost; 4 + 5 + // #region TimelineSlice 6 + export interface TimelineSlice { 7 + items: TimelineItem[]; 8 + } 9 + 10 + // #region UiTimelineItem 11 + const enum TimelineFlags { 12 + HAS_PREV = 1 << 0, 13 + HAS_NEXT = 1 << 1, 14 + IS_REPOSTED = 1 << 2, 15 + IS_PINNED = 1 << 3, 16 + } 17 + 18 + export interface UiTimelineItem extends TimelineItem { 19 + id: string; 20 + prev: boolean; 21 + next: boolean; 22 + } 23 + 24 + // #region Filters 25 + export type SliceFilter = (slice: TimelineSlice) => boolean | TimelineSlice[]; 26 + export type PostFilter = (item: TimelineItem) => boolean; 27 + 28 + const isNextInThread = (slice: TimelineSlice, item: TimelineItem) => { 29 + const items = slice.items; 30 + const last = items[items.length - 1]; 31 + 32 + const parent = item.reply?.parent; 33 + 34 + return parent?.$type === 'app.bsky.feed.defs#postView' && last.post.cid == parent.cid; 35 + }; 36 + 37 + const isFirstInThread = (slice: TimelineSlice, item: TimelineItem) => { 38 + const items = slice.items; 39 + const first = items[0]; 40 + 41 + const parent = first.reply?.parent; 42 + 43 + return parent?.$type === 'app.bsky.feed.defs#postView' && parent.cid === item.post.cid; 44 + }; 45 + 46 + export const createJoinedItems = ( 47 + arr: TimelineItem[], 48 + filterSlice?: SliceFilter, 49 + filterPost?: PostFilter, 50 + ): UiTimelineItem[] => { 51 + let slices: TimelineSlice[] = []; 52 + let jlen = 0; 53 + 54 + // arrange the posts into connected slices 55 + loop: for (let i = arr.length - 1; i >= 0; i--) { 56 + const item = arr[i]; 57 + 58 + if (filterPost && !filterPost(item)) { 59 + continue; 60 + } 61 + 62 + // if we find a matching slice and it's currently not in front, then bump 63 + // it to the front. this is so that new reply don't get buried away because 64 + // there's multiple posts separating it and the parent post. 65 + for (let j = 0; j < jlen; j++) { 66 + const slice = slices[j]; 67 + 68 + // skip, we already have too much. 69 + if (slice.items.length >= 7) { 70 + continue; 71 + } 72 + 73 + if (isFirstInThread(slice, item)) { 74 + slice.items.unshift(item); 75 + 76 + if (j !== 0) { 77 + slices.splice(j, 1); 78 + slices.unshift(slice); 79 + } 80 + 81 + continue loop; 82 + } else if (isNextInThread(slice, item)) { 83 + slice.items.push(item); 84 + 85 + if (j !== 0) { 86 + slices.splice(j, 1); 87 + slices.unshift(slice); 88 + } 89 + 90 + continue loop; 91 + } 92 + } 93 + 94 + slices.unshift({ items: [item] }); 95 + jlen++; 96 + } 97 + 98 + if (filterSlice && jlen > 0) { 99 + const unfiltered = slices; 100 + slices = []; 101 + 102 + for (let j = 0; j < jlen; j++) { 103 + const slice = unfiltered[j]; 104 + const result = filterSlice(slice); 105 + 106 + if (result) { 107 + if (Array.isArray(result)) { 108 + for (let k = 0, klen = result.length; k < klen; k++) { 109 + const slice = result[k]; 110 + slices.push(slice); 111 + } 112 + } else { 113 + slices.push(slice); 114 + } 115 + } 116 + } 117 + } 118 + 119 + return slices.flatMap((slice) => { 120 + const arr = slice.items; 121 + const len = arr.length; 122 + 123 + return arr.map((item, idx): UiTimelineItem => { 124 + const post = item.post; 125 + const reason = item.reason; 126 + 127 + let flags = 0; 128 + 129 + if (idx !== 0) { 130 + flags |= TimelineFlags.HAS_PREV; 131 + } 132 + if (idx !== len - 1) { 133 + flags |= TimelineFlags.HAS_NEXT; 134 + } 135 + 136 + switch (reason?.$type) { 137 + case 'app.bsky.feed.defs#reasonRepost': { 138 + flags |= TimelineFlags.IS_REPOSTED; 139 + break; 140 + } 141 + case 'app.bsky.feed.defs#reasonPin': { 142 + flags |= TimelineFlags.IS_PINNED; 143 + break; 144 + } 145 + } 146 + 147 + return { 148 + ...item, 149 + id: `${post.author.did}-${post.cid}-${flags}`, 150 + prev: !!(flags & TimelineFlags.HAS_PREV), 151 + next: !!(flags & TimelineFlags.HAS_NEXT), 152 + }; 153 + }); 154 + }); 155 + };
+12
src/lib/queries/handle.ts
··· 1 + import type { XRPC } from '@atcute/client'; 2 + 3 + import type { Did } from '$lib/types/identity'; 4 + 5 + export const resolveHandle = async ({ rpc, handle }: { rpc: XRPC; handle: string }): Promise<Did> => { 6 + const { data } = await rpc.get('com.atproto.identity.resolveHandle', { 7 + params: { handle }, 8 + }); 9 + 10 + // because my types are stricter than atcute's 11 + return data.did as Did; 12 + };
+236
src/lib/queries/timeline.ts
··· 1 + import type { XRPC } from '@atcute/client'; 2 + import type { 3 + AppBskyActorDefs, 4 + AppBskyEmbedRecord, 5 + AppBskyFeedDefs, 6 + AppBskyFeedGetTimeline, 7 + AppBskyFeedPost, 8 + } from '@atcute/client/lexicons'; 9 + 10 + import { 11 + createJoinedItems, 12 + type PostFilter, 13 + type SliceFilter, 14 + type TimelineItem, 15 + type TimelineSlice, 16 + type UiTimelineItem, 17 + } from '$lib/models/timeline'; 18 + import type { AtUri } from '$lib/types/at-uri'; 19 + import type { Did } from '$lib/types/identity'; 20 + import { assertNever } from '$lib/utils/invariant'; 21 + 22 + type PostRecord = AppBskyFeedPost.Record; 23 + 24 + export const enum TimelineType { 25 + PROFILE, 26 + CUSTOM_FEED, 27 + USER_LIST, 28 + } 29 + 30 + export const enum ProfileFilter { 31 + POSTS, 32 + POSTS_WITH_REPLIES, 33 + MEDIA, 34 + } 35 + 36 + export interface ProfileTimelineParams { 37 + type: TimelineType.PROFILE; 38 + actor: Did; 39 + filter: ProfileFilter; 40 + cursor?: string; 41 + } 42 + 43 + export interface CustomFeedTimelineParams { 44 + type: TimelineType.CUSTOM_FEED; 45 + feed: AtUri; 46 + cursor?: string; 47 + } 48 + 49 + export interface UserListTimelineParams { 50 + type: TimelineType.USER_LIST; 51 + list: AtUri; 52 + cursor?: string; 53 + } 54 + 55 + export type TimelineParams = ProfileTimelineParams | CustomFeedTimelineParams | UserListTimelineParams; 56 + 57 + export interface TimelinePage { 58 + cursor: string | undefined; 59 + items: UiTimelineItem[]; 60 + } 61 + 62 + const PAGE_LIMIT = 50; 63 + 64 + export const fetchTimeline = async ({ 65 + rpc, 66 + params, 67 + }: { 68 + rpc: XRPC; 69 + params: TimelineParams; 70 + }): Promise<TimelinePage> => { 71 + let sliceFilter: SliceFilter | undefined; 72 + let postFilter: PostFilter | undefined; 73 + 74 + let timeline: AppBskyFeedGetTimeline.Output; 75 + 76 + switch (params.type) { 77 + case TimelineType.PROFILE: { 78 + const { data } = await rpc.get('app.bsky.feed.getAuthorFeed', { 79 + params: { 80 + actor: params.actor, 81 + cursor: params.cursor, 82 + limit: PAGE_LIMIT, 83 + includePins: params.filter !== ProfileFilter.MEDIA, 84 + filter: 85 + params.filter === ProfileFilter.MEDIA 86 + ? 'posts_with_media' 87 + : params.filter === ProfileFilter.POSTS_WITH_REPLIES 88 + ? 'posts_with_replies' 89 + : 'posts_and_author_threads', 90 + }, 91 + }); 92 + 93 + timeline = data; 94 + 95 + if (params.filter === ProfileFilter.POSTS) { 96 + sliceFilter = createProfileSliceFilter(params.actor); 97 + } 98 + 99 + break; 100 + } 101 + case TimelineType.CUSTOM_FEED: { 102 + const { data } = await rpc.get('app.bsky.feed.getFeed', { 103 + params: { 104 + feed: params.feed, 105 + cursor: params.cursor, 106 + limit: PAGE_LIMIT, 107 + }, 108 + }); 109 + 110 + timeline = { 111 + // Discover feed, wooo. 112 + cursor: data.cursor && data.cursor.length <= 5_000 ? data.cursor : undefined, 113 + feed: data.feed, 114 + }; 115 + 116 + break; 117 + } 118 + case TimelineType.USER_LIST: { 119 + const { data } = await rpc.get('app.bsky.feed.getListFeed', { 120 + params: { 121 + list: params.list, 122 + cursor: params.cursor, 123 + limit: PAGE_LIMIT, 124 + }, 125 + }); 126 + 127 + timeline = data; 128 + break; 129 + } 130 + default: { 131 + assertNever(params); 132 + } 133 + } 134 + 135 + const page: TimelinePage = { 136 + // Prevent fetching the same data over and over 137 + cursor: timeline.cursor !== params.cursor ? timeline.cursor : undefined, 138 + items: createJoinedItems(timeline.feed, sliceFilter, postFilter), 139 + }; 140 + 141 + return page; 142 + }; 143 + 144 + // #region Post filters 145 + 146 + // #region Slice filters 147 + const createProfileSliceFilter = (did: Did): SliceFilter | undefined => { 148 + return (slice) => { 149 + const items = slice.items; 150 + const first = items[0]; 151 + 152 + const reply = first.reply; 153 + const reason = first.reason; 154 + 155 + // Skip any posts that doesn't seem to look like a self-thread 156 + if (reply && (!reason || reason.$type !== 'app.bsky.feed.defs#reasonRepost')) { 157 + for (const author of getReplyAuthors(reply)) { 158 + if (!author) { 159 + continue; 160 + } 161 + 162 + if (author.did !== did) { 163 + return yankReposts(items); 164 + } 165 + } 166 + } 167 + 168 + return true; 169 + }; 170 + }; 171 + 172 + // #region Utilities 173 + /** Get the reposts out of the gutter */ 174 + const yankReposts = (items: TimelineItem[]): TimelineSlice[] | false => { 175 + let slices: TimelineSlice[] | false = false; 176 + let last: TimelineItem[] | undefined; 177 + 178 + for (let idx = 0, len = items.length; idx < len; idx++) { 179 + const item = items[idx]; 180 + const reason = item.reason; 181 + 182 + if (reason && reason.$type === 'app.bsky.feed.defs#reasonRepost') { 183 + if (last) { 184 + last.push(item); 185 + } else { 186 + (slices ||= []).push({ items: (last = [item]) }); 187 + } 188 + } else { 189 + last = undefined; 190 + } 191 + } 192 + 193 + return slices; 194 + }; 195 + 196 + const getReplyAuthors = ({ root, grandparentAuthor, parent }: AppBskyFeedDefs.ReplyRef) => { 197 + const authors: AppBskyActorDefs.ProfileViewBasic[] = []; 198 + 199 + if (root.$type === 'app.bsky.feed.defs#postView') { 200 + authors.push(root.author); 201 + } 202 + 203 + if (grandparentAuthor) { 204 + authors.push(grandparentAuthor); 205 + } 206 + 207 + if (parent.$type === 'app.bsky.feed.defs#postView') { 208 + authors.push(parent.author); 209 + } 210 + 211 + return authors; 212 + }; 213 + 214 + const getRecordEmbed = (embed: PostRecord['embed']): AppBskyEmbedRecord.Main | undefined => { 215 + if (embed) { 216 + if (embed.$type === 'app.bsky.embed.record') { 217 + return embed; 218 + } 219 + 220 + if (embed.$type === 'app.bsky.embed.recordWithMedia') { 221 + return embed.record; 222 + } 223 + } 224 + }; 225 + 226 + const getRecordEmbedView = (embed: AppBskyFeedDefs.PostView['embed']) => { 227 + if (embed) { 228 + if (embed.$type === 'app.bsky.embed.record#view') { 229 + return embed.record; 230 + } 231 + 232 + if (embed.$type === 'app.bsky.embed.recordWithMedia#view') { 233 + return embed.record.record; 234 + } 235 + } 236 + };
+50
src/lib/styles/app.css
··· 1 + :root { 2 + --accent: #1d9bf0; 3 + --accent-text: #ffffff; 4 + 5 + --text-primary: #0f1419; 6 + --text-blurb: #536471; 7 + --text-link: var(--accent); 8 + 9 + --bg-slate: #e6ecf0; 10 + --bg-primary: #ffffff; 11 + --bg-secondary: #cfd9de; 12 + 13 + --divider-sm: #eff3f4; 14 + --divider-md: #cfd9de; 15 + 16 + --tap: var(--text-primary); 17 + } 18 + 19 + :root { 20 + --tap-sm: rgb(from var(--tap) r g b / 0.03); 21 + --tap-sm-pressed: rgb(from var(--tap) r g b / 0.07); 22 + --tap-md: rgb(from var(--tap) r g b / 0.1); 23 + --tap-md-pressed: rgb(from var(--tap) r g b / 0.2); 24 + } 25 + 26 + body { 27 + background: var(--bg-slate); 28 + overflow-y: scroll; 29 + color: var(--text-primary); 30 + font-size: 0.875rem; 31 + line-height: 1.25rem; 32 + font-family: sans-serif; 33 + } 34 + 35 + :where(*, *::before, *::after) { 36 + box-sizing: border-box; 37 + margin: 0; 38 + padding: 0; 39 + } 40 + 41 + :where(a) { 42 + color: var(--text-link); 43 + text-decoration: none; 44 + } 45 + 46 + .sv-icon { 47 + flex-shrink: 0; 48 + width: 1em; 49 + height: 1em; 50 + }
+33
src/lib/types/at-uri.ts
··· 1 + import type { Records } from '@atcute/client/lexicons'; 2 + 3 + import { assert } from '$lib/utils/invariant'; 4 + 5 + import type { Did } from './identity'; 6 + 7 + export type AtUri = `at://${string}`; 8 + 9 + export const ATURI_RE = 10 + /^at:\/\/(did:[a-z]+:[a-zA-Z0-9._:%\-]*[a-zA-Z0-9._\-]|(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])\/([a-zA-Z0-9-.]+)\/((?!\.{1,2}$)[a-zA-Z0-9_~.:-]{1,512})(?:#(\/[a-zA-Z0-9._~:@!$&%')(*+,;=\-[\]/\\]*))?$/; 11 + 12 + export interface ParsedAtUri { 13 + repo: string; 14 + collection: string; 15 + rkey: string; 16 + fragment: string | undefined; 17 + } 18 + 19 + export const parseAtUri = (str: string): ParsedAtUri => { 20 + const match = ATURI_RE.exec(str); 21 + assert(match !== null, `failed to parse at-uri for ${str}`); 22 + 23 + return { 24 + repo: match[1] as Did, 25 + collection: match[2], 26 + rkey: match[3], 27 + fragment: match[4], 28 + }; 29 + }; 30 + 31 + export const makeAtUri = (repo: string, collection: keyof Records | (string & {}), rkey: string) => { 32 + return `at://${repo}/${collection}/${rkey}`; 33 + };
+17
src/lib/types/identity.ts
··· 1 + export type Handle = `${string}.${string}`; 2 + 3 + const HANDLE_RE = 4 + /^(?=.{4,253}$)(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+([a-zA-Z][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])$/; 5 + 6 + export const isHandle = (input: string): input is Handle => { 7 + return input.length >= 3 && input.length <= 253 && HANDLE_RE.test(input); 8 + }; 9 + 10 + export type Did<TMethod extends string = string> = `did:${TMethod}:${string}`; 11 + export type AtprotoDid = Did<'plc' | 'web'>; 12 + 13 + export const DID_RE = /^(?=.{7,2048}$)did:([a-z]+):([a-zA-Z0-9._:%\-]*[a-zA-Z0-9._\-])$/; 14 + 15 + export const isDid = (input: string): input is Did => { 16 + return input.length >= 7 && input.length <= 2048 && DID_RE.test(input); 17 + };
+11
src/lib/types/rkey.ts
··· 1 + export const RECORD_KEY_RE = /(?!\.{1,2}$)[a-zA-Z0-9_~.:-]{1,512}/; 2 + 3 + export const isRecordKey = (input: string) => { 4 + return input.length >= 1 && input.length <= 512 && RECORD_KEY_RE.test(input); 5 + }; 6 + 7 + export const TID_RE = /^[234567abcdefghij][234567abcdefghijklmnopqrstuvwxyz]{12}$/; 8 + 9 + export const isTid = (input: string) => { 10 + return input.length === 13 && TID_RE.test(input); 11 + };
+142
src/lib/utils/intl/date.ts
··· 1 + import { createSubscriber } from 'svelte/reactivity'; 2 + 3 + const reactiveNow = (() => { 4 + let subscribed = false; 5 + let now = 0; 6 + 7 + const subscribe = createSubscriber((update) => { 8 + // updates every ~minute 9 + const interval = setInterval(() => { 10 + now = Date.now(); 11 + update(); 12 + }, 60_000); 13 + 14 + subscribed = true; 15 + now = Date.now(); 16 + 17 + return () => { 18 + clearInterval(interval); 19 + subscribed = false; 20 + }; 21 + }); 22 + 23 + return () => { 24 + subscribe(); 25 + return subscribed ? now : Date.now(); 26 + }; 27 + })(); 28 + 29 + let startOfYear = 0; 30 + let endOfYear = 0; 31 + 32 + const fmtAbsoluteLong = new Intl.DateTimeFormat('en-US', { dateStyle: 'long', timeStyle: 'short' }); 33 + const fmtAbsShortWithYear = new Intl.DateTimeFormat('en-US', { dateStyle: 'medium' }); 34 + const fmtAbsShort = new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric' }); 35 + 36 + export const formatShortDate = (date: string | number): string => { 37 + const inst = new Date(date); 38 + const time = inst.getTime(); 39 + 40 + if (isNaN(time)) { 41 + return 'N/A'; 42 + } 43 + 44 + const now = Date.now(); 45 + if (now > endOfYear) { 46 + const date = new Date(now); 47 + 48 + date.setMonth(0, 1); 49 + date.setHours(0, 0, 0); 50 + startOfYear = date.getTime(); 51 + 52 + date.setFullYear(date.getFullYear() + 1, 0, 0); 53 + date.setHours(23, 59, 59, 999); 54 + endOfYear = date.getTime(); 55 + } 56 + 57 + if (time >= startOfYear && time <= endOfYear) { 58 + return fmtAbsShort.format(inst); 59 + } 60 + 61 + return fmtAbsShortWithYear.format(inst); 62 + }; 63 + 64 + export const formatLongDate = (date: string | number): string => { 65 + const inst = new Date(date); 66 + 67 + if (isNaN(inst.getTime())) { 68 + return 'N/A'; 69 + } 70 + 71 + return fmtAbsoluteLong.format(inst); 72 + }; 73 + 74 + const relativeFormatters: Record<string, Intl.NumberFormat> = {}; 75 + 76 + const SECOND = 1e3; 77 + const NOW = SECOND * 10; 78 + const MINUTE = SECOND * 60; 79 + const HOUR = MINUTE * 60; 80 + const DAY = HOUR * 24; 81 + const WEEK = DAY * 7; 82 + 83 + export const formatRelativeTime = (date: string | number): string => { 84 + const time = new Date(date).getTime(); 85 + 86 + const now = reactiveNow(); 87 + const delta = now - time; 88 + 89 + if (delta < -NOW || delta > WEEK) { 90 + if (now > endOfYear) { 91 + const date = new Date(); 92 + 93 + date.setMonth(0, 1); 94 + date.setHours(0, 0, 0); 95 + startOfYear = date.getTime(); 96 + 97 + date.setFullYear(date.getFullYear() + 1, 0, 0); 98 + date.setHours(23, 59, 59, 999); 99 + endOfYear = date.getTime(); 100 + } 101 + 102 + // if it happened this year, don't show the year. 103 + if (time >= startOfYear && time <= endOfYear) { 104 + return fmtAbsShort.format(time); 105 + } 106 + 107 + return fmtAbsShortWithYear.format(time); 108 + } 109 + 110 + if (delta < NOW) { 111 + return `now`; 112 + } 113 + 114 + { 115 + let value: number; 116 + let unit: Intl.RelativeTimeFormatUnit; 117 + 118 + if (delta < MINUTE) { 119 + value = Math.floor(delta / SECOND); 120 + unit = 'second'; 121 + } else if (delta < HOUR) { 122 + value = Math.floor(delta / MINUTE); 123 + unit = 'minute'; 124 + } else if (delta < DAY) { 125 + value = Math.floor(delta / HOUR); 126 + unit = 'hour'; 127 + } else { 128 + // use rounding, this handles the following scenario: 129 + // - 2024-02-13T09:00Z <- 2024-02-15T07:00Z = 2d 130 + value = Math.round(delta / DAY); 131 + unit = 'day'; 132 + } 133 + 134 + const formatter = (relativeFormatters[unit] ||= new Intl.NumberFormat('en-US', { 135 + style: 'unit', 136 + unit: unit, 137 + unitDisplay: 'narrow', 138 + })); 139 + 140 + return formatter.format(Math.abs(value)); 141 + } 142 + };
+18
src/lib/utils/intl/number.ts
··· 1 + const long = new Intl.NumberFormat('en-US'); 2 + const compact = new Intl.NumberFormat('en-US', { notation: 'compact' }); 3 + 4 + export const formatCompactNumber = (value: number) => { 5 + if (value < 1_000) { 6 + return '' + value; 7 + } 8 + 9 + if (value < 100_000) { 10 + return long.format(value); 11 + } 12 + 13 + return compact.format(value); 14 + }; 15 + 16 + export const formatLongNumber = (value: number) => { 17 + return long.format(value); 18 + };
+13
src/lib/utils/invariant.ts
··· 1 + export function assert(condition: any, message?: string): asserts condition { 2 + if (!condition) { 3 + if (import.meta.env.DEV) { 4 + throw new Error(`Assertion failed` + (message ? `: ${message}` : ``)); 5 + } 6 + 7 + throw new Error(`Assertion failed`); 8 + } 9 + } 10 + 11 + export function assertNever(value: never, message?: string): never { 12 + assert(false, message); 13 + }
+20
src/lib/utils/strings.ts
··· 1 + export const truncateMiddle = (text: string, max: number): string => { 2 + const len = text.length; 3 + 4 + if (len <= max) { 5 + return text; 6 + } 7 + 8 + const left = Math.ceil((max - 1) / 2); 9 + const right = Math.floor((max - 1) / 2); 10 + 11 + return text.slice(0, left) + '…' + text.slice(len - right); 12 + }; 13 + 14 + export const truncateRight = (text: string, max: number): string => { 15 + if (text.length <= max) { 16 + return text; 17 + } 18 + 19 + return text.slice(0, max - 1) + '…'; 20 + };
+5
src/params/did.ts
··· 1 + import type { ParamMatcher } from '@sveltejs/kit'; 2 + 3 + import { isDid } from '$lib/types/identity'; 4 + 5 + export const match = isDid satisfies ParamMatcher;
+7
src/params/didOrHandle.ts
··· 1 + import type { ParamMatcher } from '@sveltejs/kit'; 2 + 3 + import { isDid, isHandle, type Did, type Handle } from '$lib/types/identity'; 4 + 5 + export const match = ((param: string): param is Did | Handle => { 6 + return isDid(param) || isHandle(param); 7 + }) satisfies ParamMatcher;
+5
src/params/handle.ts
··· 1 + import type { ParamMatcher } from '@sveltejs/kit'; 2 + 3 + import { isHandle } from '$lib/types/identity'; 4 + 5 + export const match = isHandle satisfies ParamMatcher;
+5
src/params/rkey.ts
··· 1 + import type { ParamMatcher } from '@sveltejs/kit'; 2 + 3 + import { isRecordKey } from '$lib/types/rkey'; 4 + 5 + export const match = isRecordKey satisfies ParamMatcher;
+5
src/params/tid.ts
··· 1 + import type { ParamMatcher } from '@sveltejs/kit'; 2 + 3 + import { isTid } from '$lib/types/rkey'; 4 + 5 + export const match = isTid satisfies ParamMatcher;
+53
src/routes/(app)/(profile)/[actor=didOrHandle]/(timeline)/+layout.svelte
··· 1 + <script lang="ts"> 2 + import type { ClassValue } from 'svelte/elements'; 3 + 4 + import { base } from '$app/paths'; 5 + import { page } from '$app/state'; 6 + 7 + import type { LayoutProps } from './$types'; 8 + 9 + const { data, children }: LayoutProps = $props(); 10 + 11 + const did = $derived(data.profile.did); 12 + const currentRouteId = $derived(page.route.id); 13 + 14 + const cn = (routeId: string): ClassValue => { 15 + const id = `/(app)/(profile)/[actor=didOrHandle]/(timeline)${routeId}`; 16 + return ['tab', currentRouteId === id && 'is-active']; 17 + }; 18 + </script> 19 + 20 + <div class="profile-tabs" data-sveltekit-keepfocus> 21 + <a class={cn('')} href="{base}/{did}">Posts</a> 22 + <a class={cn('/with_replies')} href="{base}/{did}/with_replies">Replies</a> 23 + <a class={cn('/media')} href="{base}/{did}/media">Media</a> 24 + </div> 25 + 26 + {@render children()} 27 + 28 + <style> 29 + .profile-tabs { 30 + display: flex; 31 + position: sticky; 32 + top: 0; 33 + flex-wrap: wrap; 34 + z-index: 1; 35 + border-bottom: 1px solid var(--divider-sm); 36 + background: var(--bg-primary); 37 + } 38 + 39 + .tab { 40 + padding: 12px 16px; 41 + font-weight: 600; 42 + font-size: 1rem; 43 + line-height: 1.5rem; 44 + 45 + &:hover { 46 + text-decoration: underline; 47 + } 48 + 49 + &.is-active { 50 + color: var(--text-primary); 51 + } 52 + } 53 + </style>
+20
src/routes/(app)/(profile)/[actor=didOrHandle]/(timeline)/+page.svelte
··· 1 + <script lang="ts"> 2 + import { page } from '$app/state'; 3 + import { PUBLIC_APP_NAME } from '$env/static/public'; 4 + import type { PageProps } from './$types'; 5 + 6 + import PageListing from '$lib/components/page/page-listing.svelte'; 7 + import PostFeedItem from '$lib/components/timeline/post-feed-item.svelte'; 8 + 9 + const { data }: PageProps = $props(); 10 + </script> 11 + 12 + <svelte:head> 13 + <title>@{data.profile.handle} — {PUBLIC_APP_NAME}</title> 14 + </svelte:head> 15 + 16 + <PageListing subject="timeline" root={!page.url.searchParams.get('cursor')} cursor={data.timeline.cursor}> 17 + {#each data.timeline.items as item (item.id)} 18 + <PostFeedItem {item} /> 19 + {/each} 20 + </PageListing>
+31
src/routes/(app)/(profile)/[actor=didOrHandle]/(timeline)/+page.ts
··· 1 + import { simpleFetchHandler, XRPC } from '@atcute/client'; 2 + 3 + import { PUBLIC_APPVIEW_URL } from '$env/static/public'; 4 + import { fetchTimeline, ProfileFilter, TimelineType } from '$lib/queries/timeline'; 5 + import { isDid, type Did } from '$lib/types/identity'; 6 + 7 + import type { PageLoad } from './$types'; 8 + 9 + export const load: PageLoad = async ({ url, params, fetch, parent }) => { 10 + const rpc = new XRPC({ handler: simpleFetchHandler({ service: PUBLIC_APPVIEW_URL }) }); 11 + 12 + let did: Did; 13 + if (isDid(params.actor)) { 14 + did = params.actor; 15 + } else { 16 + const parentData = await parent(); 17 + did = parentData.profile.did as Did; 18 + } 19 + 20 + const timeline = await fetchTimeline({ 21 + rpc, 22 + params: { 23 + type: TimelineType.PROFILE, 24 + actor: did, 25 + filter: ProfileFilter.POSTS, 26 + cursor: url.searchParams.get('cursor') || undefined, 27 + }, 28 + }); 29 + 30 + return { timeline }; 31 + };
+20
src/routes/(app)/(profile)/[actor=didOrHandle]/(timeline)/media/+page.svelte
··· 1 + <script lang="ts"> 2 + import { page } from '$app/state'; 3 + import { PUBLIC_APP_NAME } from '$env/static/public'; 4 + import type { PageProps } from './$types'; 5 + 6 + import PageListing from '$lib/components/page/page-listing.svelte'; 7 + import PostFeedItem from '$lib/components/timeline/post-feed-item.svelte'; 8 + 9 + const { data }: PageProps = $props(); 10 + </script> 11 + 12 + <svelte:head> 13 + <title>@{data.profile.handle} — {PUBLIC_APP_NAME}</title> 14 + </svelte:head> 15 + 16 + <PageListing subject="timeline" root={!page.url.searchParams.get('cursor')} cursor={data.timeline.cursor}> 17 + {#each data.timeline.items as item (item.id)} 18 + <PostFeedItem {item} /> 19 + {/each} 20 + </PageListing>
+31
src/routes/(app)/(profile)/[actor=didOrHandle]/(timeline)/media/+page.ts
··· 1 + import { simpleFetchHandler, XRPC } from '@atcute/client'; 2 + 3 + import { PUBLIC_APPVIEW_URL } from '$env/static/public'; 4 + import { fetchTimeline, ProfileFilter, TimelineType } from '$lib/queries/timeline'; 5 + import { isDid, type Did } from '$lib/types/identity'; 6 + 7 + import type { PageLoad } from './$types'; 8 + 9 + export const load: PageLoad = async ({ url, params, fetch, parent }) => { 10 + const rpc = new XRPC({ handler: simpleFetchHandler({ service: PUBLIC_APPVIEW_URL }) }); 11 + 12 + let did: Did; 13 + if (isDid(params.actor)) { 14 + did = params.actor; 15 + } else { 16 + const parentData = await parent(); 17 + did = parentData.profile.did as Did; 18 + } 19 + 20 + const timeline = await fetchTimeline({ 21 + rpc, 22 + params: { 23 + type: TimelineType.PROFILE, 24 + actor: did, 25 + filter: ProfileFilter.MEDIA, 26 + cursor: url.searchParams.get('cursor') || undefined, 27 + }, 28 + }); 29 + 30 + return { timeline }; 31 + };
+20
src/routes/(app)/(profile)/[actor=didOrHandle]/(timeline)/with_replies/+page.svelte
··· 1 + <script lang="ts"> 2 + import { page } from '$app/state'; 3 + import { PUBLIC_APP_NAME } from '$env/static/public'; 4 + import type { PageProps } from './$types'; 5 + 6 + import PageListing from '$lib/components/page/page-listing.svelte'; 7 + import PostFeedItem from '$lib/components/timeline/post-feed-item.svelte'; 8 + 9 + const { data }: PageProps = $props(); 10 + </script> 11 + 12 + <svelte:head> 13 + <title>@{data.profile.handle} — {PUBLIC_APP_NAME}</title> 14 + </svelte:head> 15 + 16 + <PageListing subject="timeline" root={!page.url.searchParams.get('cursor')} cursor={data.timeline.cursor}> 17 + {#each data.timeline.items as item (item.id)} 18 + <PostFeedItem {item} /> 19 + {/each} 20 + </PageListing>
+31
src/routes/(app)/(profile)/[actor=didOrHandle]/(timeline)/with_replies/+page.ts
··· 1 + import { simpleFetchHandler, XRPC } from '@atcute/client'; 2 + 3 + import { PUBLIC_APPVIEW_URL } from '$env/static/public'; 4 + import { fetchTimeline, ProfileFilter, TimelineType } from '$lib/queries/timeline'; 5 + import { isDid, type Did } from '$lib/types/identity'; 6 + 7 + import type { PageLoad } from './$types'; 8 + 9 + export const load: PageLoad = async ({ url, params, fetch, parent }) => { 10 + const rpc = new XRPC({ handler: simpleFetchHandler({ service: PUBLIC_APPVIEW_URL }) }); 11 + 12 + let did: Did; 13 + if (isDid(params.actor)) { 14 + did = params.actor; 15 + } else { 16 + const parentData = await parent(); 17 + did = parentData.profile.did as Did; 18 + } 19 + 20 + const timeline = await fetchTimeline({ 21 + rpc, 22 + params: { 23 + type: TimelineType.PROFILE, 24 + actor: did, 25 + filter: ProfileFilter.POSTS_WITH_REPLIES, 26 + cursor: url.searchParams.get('cursor') || undefined, 27 + }, 28 + }); 29 + 30 + return { timeline }; 31 + };
+135
src/routes/(app)/(profile)/[actor=didOrHandle]/+layout.svelte
··· 1 + <script lang="ts"> 2 + import { base } from '$app/paths'; 3 + import { formatCompactNumber } from '$lib/utils/intl/number'; 4 + import type { LayoutProps } from './$types'; 5 + 6 + import ProfileAside from './components/profile-aside.svelte'; 7 + 8 + const { children, data }: LayoutProps = $props(); 9 + 10 + const profile = $derived(data.profile); 11 + const did = $derived(profile.did); 12 + 13 + const postCount = $derived(profile.postsCount ?? 0); 14 + </script> 15 + 16 + {#key profile.did} 17 + <div class="profile-layout"> 18 + <div class="banner"> 19 + {#if profile.banner} 20 + <img loading="lazy" src={profile.banner} alt="" class="banner-image" /> 21 + {/if} 22 + </div> 23 + 24 + <div class="aside"> 25 + <ProfileAside {profile} /> 26 + 27 + <div class="associations"> 28 + <a class="association" href="{base}/{did}"> 29 + <span class="association-count">{formatCompactNumber(postCount)}</span> 30 + {postCount === 1 ? `post` : `posts`} 31 + </a> 32 + 33 + {#if profile.associated?.feedgens} 34 + {@const count = profile.associated.feedgens} 35 + 36 + <a class="association" href="{base}/{did}/feeds"> 37 + <span class="association-count">{formatCompactNumber(count)}</span> 38 + {count === 1 ? `feed` : `feeds`} 39 + </a> 40 + {/if} 41 + {#if profile.associated?.lists} 42 + {@const count = profile.associated.lists} 43 + 44 + <a class="association" href="{base}/{did}/lists"> 45 + <span class="association-count">{formatCompactNumber(count)}</span> 46 + {count === 1 ? `list` : `lists`} 47 + </a> 48 + {/if} 49 + {#if profile.associated?.starterPacks} 50 + {@const count = profile.associated.starterPacks} 51 + 52 + <a class="association" href="{base}/{did}/packs"> 53 + <span class="association-count">{formatCompactNumber(count)}</span> 54 + {count === 1 ? `starter pack` : `starter packs`} 55 + </a> 56 + {/if} 57 + </div> 58 + </div> 59 + 60 + <div class="main"> 61 + {@render children()} 62 + </div> 63 + </div> 64 + {/key} 65 + 66 + <style> 67 + .profile-layout { 68 + display: grid; 69 + grid-template-columns: minmax(0, 1fr); 70 + grid-template-areas: 'banner' 'aside' 'main'; 71 + justify-content: center; 72 + gap: 8px; 73 + margin: 24px auto 0; 74 + max-width: 480px; 75 + 76 + @media (width >= 640px) { 77 + grid-template-columns: minmax(255px, 320px) minmax(0, 600px); 78 + grid-template-areas: 'banner banner' 'aside main'; 79 + max-width: 960px; 80 + } 81 + } 82 + 83 + .banner { 84 + grid-area: banner; 85 + background: var(--bg-secondary); 86 + aspect-ratio: 3 / 1; 87 + overflow: hidden; 88 + } 89 + .banner-image { 90 + width: 100%; 91 + height: 100%; 92 + font-size: 0; 93 + } 94 + 95 + .aside { 96 + display: flex; 97 + grid-area: aside; 98 + flex-direction: column; 99 + gap: 8px; 100 + 101 + @media (width >= 640px) { 102 + position: sticky; 103 + top: 0; 104 + max-height: 100dvh; 105 + overflow-y: auto; 106 + } 107 + } 108 + 109 + .associations { 110 + display: flex; 111 + flex-direction: column; 112 + background: var(--bg-primary); 113 + } 114 + .association { 115 + padding: 10px 16px; 116 + color: var(--text-blurb); 117 + 118 + & + & { 119 + border-top: 1px solid var(--divider-sm); 120 + } 121 + 122 + &:hover { 123 + background: var(--tap-sm); 124 + } 125 + } 126 + .association-count { 127 + color: var(--text-primary); 128 + font-weight: 600; 129 + } 130 + 131 + .main { 132 + grid-area: main; 133 + padding-bottom: 24px; 134 + } 135 + </style>
+19
src/routes/(app)/(profile)/[actor=didOrHandle]/+layout.ts
··· 1 + import { XRPC, simpleFetchHandler } from '@atcute/client'; 2 + 3 + import { PUBLIC_APPVIEW_URL } from '$env/static/public'; 4 + 5 + import type { LayoutLoad } from './$types'; 6 + 7 + export const load: LayoutLoad = async ({ params, fetch }) => { 8 + const rpc = new XRPC({ handler: simpleFetchHandler({ service: PUBLIC_APPVIEW_URL }) }); 9 + 10 + const { data } = await rpc.get('app.bsky.actor.getProfile', { 11 + params: { 12 + actor: params.actor, 13 + }, 14 + }); 15 + 16 + return { 17 + profile: data, 18 + }; 19 + };
+99
src/routes/(app)/(profile)/[actor=didOrHandle]/components/profile-aside.svelte
··· 1 + <script lang="ts"> 2 + import type { AppBskyActorDefs } from '@atcute/client/lexicons'; 3 + 4 + import { base } from '$app/paths'; 5 + 6 + import RichtextRawRenderer from '$lib/components/richtext-raw-renderer.svelte'; 7 + import { formatCompactNumber } from '$lib/utils/intl/number'; 8 + 9 + interface Props { 10 + profile: AppBskyActorDefs.ProfileViewDetailed; 11 + } 12 + 13 + const { profile }: Props = $props(); 14 + 15 + const did = $derived(profile.did); 16 + </script> 17 + 18 + <div class="profile-aside"> 19 + <div class="avatar-wrapper"> 20 + <img loading="lazy" src={profile.avatar} alt="" class="avatar" /> 21 + </div> 22 + 23 + <div class="name-wrapper"> 24 + <p dir="auto" class="display-name">{profile.displayName?.trim() || profile.handle.slice(0, 64)}</p> 25 + <p class="handle">@{profile.handle}</p> 26 + </div> 27 + 28 + {#if profile.description?.trim()} 29 + <RichtextRawRenderer text={profile.description} /> 30 + {/if} 31 + 32 + <div class="stats"> 33 + <a class="stat-entry" href="{base}/{did}/followers"> 34 + <span class="stat-count">{formatCompactNumber(profile.followersCount || 0)}</span> 35 + <span> {profile.followersCount === 1 ? `Follower` : `Followers`}</span> 36 + </a> 37 + 38 + <a class="stat-entry" href="{base}/{did}/following"> 39 + <span class="stat-count">{formatCompactNumber(profile.followsCount || 0)}</span> 40 + <span> Following</span> 41 + </a> 42 + </div> 43 + </div> 44 + 45 + <style> 46 + .profile-aside { 47 + display: flex; 48 + flex-direction: column; 49 + gap: 8px; 50 + background: var(--bg-primary); 51 + padding: 16px; 52 + min-width: 0; 53 + } 54 + 55 + .avatar-wrapper { 56 + flex-shrink: 0; 57 + border-radius: 50%; 58 + background: var(--bg-secondary); 59 + aspect-ratio: 1 / 1; 60 + width: 100%; 61 + max-width: 90px; 62 + overflow: hidden; 63 + } 64 + .avatar { 65 + width: 100%; 66 + height: 100%; 67 + object-fit: cover; 68 + } 69 + 70 + .display-name { 71 + font-weight: 700; 72 + font-size: 1.25rem; 73 + line-height: 1.75rem; 74 + overflow-wrap: break-word; 75 + } 76 + .handle { 77 + color: var(--text-blurb); 78 + overflow-wrap: break-word; 79 + } 80 + 81 + .stats { 82 + display: flex; 83 + flex-wrap: wrap; 84 + gap: 20px; 85 + 86 + min-width: 0; 87 + } 88 + .stat-entry { 89 + color: var(--text-blurb); 90 + 91 + &:hover { 92 + text-decoration: underline; 93 + } 94 + } 95 + .stat-count { 96 + color: var(--text-primary); 97 + font-weight: 700; 98 + } 99 + </style>
+23
src/routes/(app)/(profile)/[actor=didOrHandle]/followers/+page.svelte
··· 1 + <script lang="ts"> 2 + import { page } from '$app/state'; 3 + import { PUBLIC_APP_NAME } from '$env/static/public'; 4 + import type { PageProps } from './$types'; 5 + 6 + import PageHeader from '$lib/components/page/page-header.svelte'; 7 + import PageListing from '$lib/components/page/page-listing.svelte'; 8 + import ProfileItem from '$lib/components/profiles/profile-item.svelte'; 9 + 10 + const { data }: PageProps = $props(); 11 + </script> 12 + 13 + <svelte:head> 14 + <title>Users following @{data.profile.handle} — {PUBLIC_APP_NAME}</title> 15 + </svelte:head> 16 + 17 + <PageHeader title="Followers" /> 18 + 19 + <PageListing subject="profiles" root={!page.url.searchParams.get('cursor')} cursor={data.followers.cursor}> 20 + {#each data.followers.items as profile (profile.did)} 21 + <ProfileItem item={profile} /> 22 + {/each} 23 + </PageListing>
+18
src/routes/(app)/(profile)/[actor=didOrHandle]/followers/+page.ts
··· 1 + import { simpleFetchHandler, XRPC } from '@atcute/client'; 2 + 3 + import { PUBLIC_APPVIEW_URL } from '$env/static/public'; 4 + import type { PageLoad } from './$types'; 5 + 6 + export const load: PageLoad = async ({ url, params, fetch }) => { 7 + const rpc = new XRPC({ handler: simpleFetchHandler({ service: PUBLIC_APPVIEW_URL }) }); 8 + 9 + const { data } = await rpc.get('app.bsky.graph.getFollowers', { 10 + params: { 11 + actor: params.actor, 12 + limit: 50, 13 + cursor: url.searchParams.get('cursor') || undefined, 14 + }, 15 + }); 16 + 17 + return { followers: { cursor: data.cursor, items: data.followers } }; 18 + };
+23
src/routes/(app)/(profile)/[actor=didOrHandle]/following/+page.svelte
··· 1 + <script lang="ts"> 2 + import { page } from '$app/state'; 3 + import { PUBLIC_APP_NAME } from '$env/static/public'; 4 + import type { PageProps } from './$types'; 5 + 6 + import PageHeader from '$lib/components/page/page-header.svelte'; 7 + import PageListing from '$lib/components/page/page-listing.svelte'; 8 + import ProfileItem from '$lib/components/profiles/profile-item.svelte'; 9 + 10 + const { data }: PageProps = $props(); 11 + </script> 12 + 13 + <svelte:head> 14 + <title>Users followed by @{data.profile.handle} — {PUBLIC_APP_NAME}</title> 15 + </svelte:head> 16 + 17 + <PageHeader title="Following" /> 18 + 19 + <PageListing subject="profiles" root={!page.url.searchParams.get('cursor')} cursor={data.following.cursor}> 20 + {#each data.following.items as profile (profile.did)} 21 + <ProfileItem item={profile} /> 22 + {/each} 23 + </PageListing>
+18
src/routes/(app)/(profile)/[actor=didOrHandle]/following/+page.ts
··· 1 + import { simpleFetchHandler, XRPC } from '@atcute/client'; 2 + 3 + import { PUBLIC_APPVIEW_URL } from '$env/static/public'; 4 + import type { PageLoad } from './$types'; 5 + 6 + export const load: PageLoad = async ({ url, params, fetch }) => { 7 + const rpc = new XRPC({ handler: simpleFetchHandler({ service: PUBLIC_APPVIEW_URL }) }); 8 + 9 + const { data } = await rpc.get('app.bsky.graph.getFollows', { 10 + params: { 11 + actor: params.actor, 12 + limit: 50, 13 + cursor: url.searchParams.get('cursor') || undefined, 14 + }, 15 + }); 16 + 17 + return { following: { cursor: data.cursor, items: data.follows } }; 18 + };
+13
src/routes/(app)/+layout.svelte
··· 1 + <script lang="ts"> 2 + import type { Snippet } from 'svelte'; 3 + 4 + import '$lib/styles/app.css'; 5 + 6 + interface Props { 7 + children: Snippet<[]>; 8 + } 9 + 10 + const { children }: Props = $props(); 11 + </script> 12 + 13 + {@render children()}
+1
src/routes/(app)/+page.svelte
··· 1 + <div>Hello</div>
+107
src/routes/(app)/[actor=didOrHandle]/[rkey=tid]/+page.svelte
··· 1 + <script lang="ts"> 2 + import type { AppBskyFeedPost } from '@atcute/client/lexicons'; 3 + 4 + import { PUBLIC_APP_NAME } from '$env/static/public'; 5 + import type { PageProps } from './$types'; 6 + 7 + import { createThreadData } from '$lib/models/thread'; 8 + import { truncateMiddle, truncateRight } from '$lib/utils/strings'; 9 + 10 + import MainPost from '$lib/components/threads/main-post.svelte'; 11 + import OverflowThreadItem from '$lib/components/threads/overflow-thread-item.svelte'; 12 + import PostThreadItem from '$lib/components/threads/post-thread-item.svelte'; 13 + 14 + const { data }: PageProps = $props(); 15 + 16 + const treeView = true; 17 + const thread = $derived.by(() => { 18 + return createThreadData({ thread: data.thread, treeView }); 19 + }); 20 + 21 + const title = $derived.by(() => { 22 + const post = thread.post; 23 + 24 + const author = `@${truncateMiddle(post.author.handle, 29)}`; 25 + const content = truncateRight((post.record as AppBskyFeedPost.Record).text.trim(), 70); 26 + 27 + return `${author}: "${content}" — ${PUBLIC_APP_NAME}`; 28 + }); 29 + </script> 30 + 31 + <svelte:head> 32 + <title>{title}</title> 33 + </svelte:head> 34 + 35 + <div class={['thread-page', treeView ? 'is-tree' : 'is-flat']}> 36 + <div class="ancestors"> 37 + {#each thread.ancestors as item (item.id)} 38 + {#if item.type === 'post'} 39 + <PostThreadItem {item} treeView={false} /> 40 + {:else if item.type === 'blocked'} 41 + <div>blocked</div> 42 + {:else if item.type === 'overflow'} 43 + <OverflowThreadItem {item} treeView={false} descendant={false} /> 44 + {/if} 45 + {/each} 46 + </div> 47 + 48 + <div class="thread"> 49 + <div class="main" id="main"> 50 + <MainPost post={thread.post} prev={thread.ancestors.length > 0} /> 51 + </div> 52 + 53 + <div class="descendants"> 54 + {#each thread.descendants as item (item.id)} 55 + {#if item.type === 'post'} 56 + <PostThreadItem {item} {treeView} /> 57 + {:else if item.type === 'blocked'} 58 + <div>blocked</div> 59 + {:else if item.type === 'overflow'} 60 + <OverflowThreadItem {item} {treeView} descendant={true} /> 61 + {/if} 62 + {/each} 63 + </div> 64 + </div> 65 + </div> 66 + 67 + <style> 68 + .thread-page { 69 + display: flex; 70 + flex-direction: column; 71 + margin: 24px auto; 72 + width: 100%; 73 + max-width: 600px; 74 + } 75 + 76 + .thread { 77 + min-height: calc(100dvh - (24px * 2)); 78 + } 79 + 80 + .main, 81 + .ancestors, 82 + .descendants { 83 + background: var(--bg-primary); 84 + } 85 + 86 + .main { 87 + scroll-margin: 24px; 88 + } 89 + 90 + .ancestors { 91 + &:empty { 92 + display: none; 93 + } 94 + } 95 + 96 + .descendants { 97 + border-top: 1px solid var(--divider-md); 98 + 99 + &:empty { 100 + display: none; 101 + } 102 + 103 + .is-tree & { 104 + padding: 4px 0; 105 + } 106 + } 107 + </style>
+53
src/routes/(app)/[actor=didOrHandle]/[rkey=tid]/+page.ts
··· 1 + import { simpleFetchHandler, XRPC, XRPCError } from '@atcute/client'; 2 + 3 + import { PUBLIC_APPVIEW_URL } from '$env/static/public'; 4 + import type { PageLoad } from './$types'; 5 + 6 + import { resolveHandle } from '$lib/queries/handle'; 7 + import { makeAtUri } from '$lib/types/at-uri'; 8 + import { isDid, type Did } from '$lib/types/identity'; 9 + 10 + export const load: PageLoad = async ({ params }) => { 11 + const rpc = new XRPC({ handler: simpleFetchHandler({ service: PUBLIC_APPVIEW_URL }) }); 12 + 13 + let did: Did; 14 + if (!isDid(params.actor)) { 15 + did = await resolveHandle({ rpc, handle: params.actor }); 16 + } else { 17 + did = params.actor; 18 + } 19 + 20 + const uri = makeAtUri(did, 'app.bsky.feed.post', params.rkey); 21 + 22 + // TODO: look previous pages for an existing post 23 + { 24 + } 25 + 26 + const { data } = await rpc.get('app.bsky.feed.getPostThread', { 27 + params: { 28 + uri: uri, 29 + depth: 4, 30 + parentHeight: 10, 31 + }, 32 + }); 33 + 34 + const thread = data.thread; 35 + 36 + switch (thread.$type) { 37 + case 'app.bsky.feed.defs#notFoundPost': { 38 + throw new XRPCError(400, { 39 + kind: 'NotFound', 40 + description: `Post not found: ${uri}`, 41 + }); 42 + } 43 + case 'app.bsky.feed.defs#blockedPost': { 44 + // shouldn't happen? 45 + throw new XRPCError(400, { 46 + kind: 'NotFound', 47 + description: `Blocked post: ${uri}`, 48 + }); 49 + } 50 + } 51 + 52 + return { thread }; 53 + };
+1
src/routes/(app)/[actor=didOrHandle]/feeds/[rkey=rkey]/+page.svelte
··· 1 + <div>feeds</div>
+1
src/routes/(app)/[actor=didOrHandle]/lists/[rkey=rkey]/+page.svelte
··· 1 + <div>lists</div>
+26
src/routes/(app)/[actor=did]/[rkey=tid]/likes/+page.svelte
··· 1 + <script lang="ts"> 2 + import { page } from '$app/state'; 3 + import { PUBLIC_APP_NAME } from '$env/static/public'; 4 + import type { PageProps } from './$types'; 5 + 6 + import PageContainer from '$lib/components/page/page-container.svelte'; 7 + import PageHeader from '$lib/components/page/page-header.svelte'; 8 + import PageListing from '$lib/components/page/page-listing.svelte'; 9 + import ProfileItem from '$lib/components/profiles/profile-item.svelte'; 10 + 11 + const { data }: PageProps = $props(); 12 + </script> 13 + 14 + <svelte:head> 15 + <title>Post liked by — {PUBLIC_APP_NAME}</title> 16 + </svelte:head> 17 + 18 + <PageContainer> 19 + <PageHeader title="Liked by" /> 20 + 21 + <PageListing subject="profiles" root={!page.url.searchParams.get('cursor')} cursor={data.likes.cursor}> 22 + {#each data.likes.items as profile (profile.did)} 23 + <ProfileItem item={profile} /> 24 + {/each} 25 + </PageListing> 26 + </PageContainer>
+22
src/routes/(app)/[actor=did]/[rkey=tid]/likes/+page.ts
··· 1 + import { simpleFetchHandler, XRPC } from '@atcute/client'; 2 + 3 + import { PUBLIC_APPVIEW_URL } from '$env/static/public'; 4 + import type { PageLoad } from './$types'; 5 + 6 + import { makeAtUri } from '$lib/types/at-uri'; 7 + 8 + export const load: PageLoad = async ({ url, params, fetch }) => { 9 + const rpc = new XRPC({ handler: simpleFetchHandler({ service: PUBLIC_APPVIEW_URL }) }); 10 + 11 + const uri = makeAtUri(params.actor, 'app.bsky.feed.post', params.rkey); 12 + 13 + const { data } = await rpc.get('app.bsky.feed.getLikes', { 14 + params: { 15 + uri, 16 + limit: 50, 17 + cursor: url.searchParams.get('cursor') || undefined, 18 + }, 19 + }); 20 + 21 + return { likes: { cursor: data.cursor, items: data.likes.map((like) => like.actor) } }; 22 + };
+35
src/routes/(app)/[actor=did]/[rkey=tid]/quotes/+page.svelte
··· 1 + <script lang="ts"> 2 + import { page } from '$app/state'; 3 + import { PUBLIC_APP_NAME } from '$env/static/public'; 4 + import type { PageProps } from './$types'; 5 + 6 + import PageContainer from '$lib/components/page/page-container.svelte'; 7 + import PageHeader from '$lib/components/page/page-header.svelte'; 8 + import PageListing from '$lib/components/page/page-listing.svelte'; 9 + import PostFeedItem from '$lib/components/timeline/post-feed-item.svelte'; 10 + 11 + const { data }: PageProps = $props(); 12 + </script> 13 + 14 + <svelte:head> 15 + <title>Quotes — {PUBLIC_APP_NAME}</title> 16 + </svelte:head> 17 + 18 + <PageContainer> 19 + <PageHeader title="Quotes" /> 20 + 21 + <PageListing subject="posts" root={!page.url.searchParams.get('cursor')} cursor={data.quotes.cursor}> 22 + {#each data.quotes.items as post (post.uri)} 23 + <PostFeedItem 24 + item={{ 25 + id: post.uri, 26 + post, 27 + reply: undefined, 28 + reason: undefined, 29 + next: false, 30 + prev: false, 31 + }} 32 + /> 33 + {/each} 34 + </PageListing> 35 + </PageContainer>
+22
src/routes/(app)/[actor=did]/[rkey=tid]/quotes/+page.ts
··· 1 + import { simpleFetchHandler, XRPC } from '@atcute/client'; 2 + 3 + import { PUBLIC_APPVIEW_URL } from '$env/static/public'; 4 + import type { PageLoad } from './$types'; 5 + 6 + import { makeAtUri } from '$lib/types/at-uri'; 7 + 8 + export const load: PageLoad = async ({ url, params, fetch }) => { 9 + const rpc = new XRPC({ handler: simpleFetchHandler({ service: PUBLIC_APPVIEW_URL }) }); 10 + 11 + const uri = makeAtUri(params.actor, 'app.bsky.feed.post', params.rkey); 12 + 13 + const { data } = await rpc.get('app.bsky.feed.getQuotes', { 14 + params: { 15 + uri, 16 + limit: 50, 17 + cursor: url.searchParams.get('cursor') || undefined, 18 + }, 19 + }); 20 + 21 + return { quotes: { cursor: data.cursor, items: data.posts } }; 22 + };
+26
src/routes/(app)/[actor=did]/[rkey=tid]/reposts/+page.svelte
··· 1 + <script lang="ts"> 2 + import { page } from '$app/state'; 3 + import { PUBLIC_APP_NAME } from '$env/static/public'; 4 + import type { PageProps } from './$types'; 5 + 6 + import PageContainer from '$lib/components/page/page-container.svelte'; 7 + import PageHeader from '$lib/components/page/page-header.svelte'; 8 + import PageListing from '$lib/components/page/page-listing.svelte'; 9 + import ProfileItem from '$lib/components/profiles/profile-item.svelte'; 10 + 11 + const { data }: PageProps = $props(); 12 + </script> 13 + 14 + <svelte:head> 15 + <title>Post reposted by — {PUBLIC_APP_NAME}</title> 16 + </svelte:head> 17 + 18 + <PageContainer> 19 + <PageHeader title="Reposted by" /> 20 + 21 + <PageListing subject="profiles" root={!page.url.searchParams.get('cursor')} cursor={data.reposts.cursor}> 22 + {#each data.reposts.items as profile (profile.did)} 23 + <ProfileItem item={profile} /> 24 + {/each} 25 + </PageListing> 26 + </PageContainer>
+22
src/routes/(app)/[actor=did]/[rkey=tid]/reposts/+page.ts
··· 1 + import { simpleFetchHandler, XRPC } from '@atcute/client'; 2 + 3 + import { PUBLIC_APPVIEW_URL } from '$env/static/public'; 4 + import type { PageLoad } from './$types'; 5 + 6 + import { makeAtUri } from '$lib/types/at-uri'; 7 + 8 + export const load: PageLoad = async ({ url, params, fetch }) => { 9 + const rpc = new XRPC({ handler: simpleFetchHandler({ service: PUBLIC_APPVIEW_URL }) }); 10 + 11 + const uri = makeAtUri(params.actor, 'app.bsky.feed.post', params.rkey); 12 + 13 + const { data } = await rpc.get('app.bsky.feed.getRepostedBy', { 14 + params: { 15 + uri, 16 + limit: 50, 17 + cursor: url.searchParams.get('cursor') || undefined, 18 + }, 19 + }); 20 + 21 + return { reposts: { cursor: data.cursor, items: data.repostedBy } }; 22 + };
+3
src/routes/+layout.ts
··· 1 + import { dev } from '$app/environment'; 2 + 3 + export const csr = dev;
static/favicon.png

This is a binary file and will not be displayed.

+21
svelte.config.js
··· 1 + import adapter from '@sveltejs/adapter-cloudflare'; 2 + import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 + 4 + /** @type {import('@sveltejs/kit').Config} */ 5 + const config = { 6 + // Consult https://svelte.dev/docs/kit/integrations 7 + // for more information about preprocessors 8 + preprocess: vitePreprocess(), 9 + compilerOptions: { 10 + runes: true, 11 + }, 12 + 13 + kit: { 14 + // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. 15 + // If your environment is not supported, or you settled on a specific environment, switch out the adapter. 16 + // See https://svelte.dev/docs/kit/adapters for more information about adapters. 17 + adapter: adapter(), 18 + }, 19 + }; 20 + 21 + export default config;
+19
tsconfig.json
··· 1 + { 2 + "extends": "./.svelte-kit/tsconfig.json", 3 + "compilerOptions": { 4 + "allowJs": true, 5 + "checkJs": true, 6 + "esModuleInterop": true, 7 + "forceConsistentCasingInFileNames": true, 8 + "resolveJsonModule": true, 9 + "skipLibCheck": true, 10 + "sourceMap": true, 11 + "strict": true, 12 + "moduleResolution": "bundler" 13 + } 14 + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias 15 + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files 16 + // 17 + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 18 + // from the referenced tsconfig.json - TypeScript does not merge them in 19 + }
+6
vite.config.ts
··· 1 + import { sveltekit } from '@sveltejs/kit/vite'; 2 + import { defineConfig } from 'vite'; 3 + 4 + export default defineConfig({ 5 + plugins: [sveltekit()] 6 + });