slack status without the slack status.zzstoatzz.io
hatk statusphere
0
fork

Configure Feed

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

rewrite status app from quickslice to hatk

replace quickslice backend + vanilla JS SPA (split across Fly.io and
Cloudflare Pages) with hatk — SvelteKit frontend + typed XRPC backend,
single Fly.io deployment.

- svelte 5 components with tanstack query for data fetching
- server-side feeds (recent, actor) with hydration
- AT Protocol native OAuth via hatk
- SSR link previews with OG tags (no edge function needed)
- bufo emoji og:image support for status permalinks
- local dev env via docker-compose (PLC + PDS + postgres)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+7232 -3312
+5
.dockerignore
··· 1 + node_modules/ 2 + .svelte-kit/ 3 + .git/ 4 + data/ 5 + *.db
+7 -7
.gitignore
··· 1 - # wrangler/cloudflare 2 - .wrangler/ 3 - 4 - # node 5 1 node_modules/ 6 - package-lock.json 2 + .svelte-kit/ 3 + build/ 4 + *.db 5 + data/ 6 + hatk.generated.* 7 7 8 - # notes 9 - oauth-experience.md 8 + .DS_Store 9 + .env
+194
CHANGELOG.md
··· 1 + # status app: quickslice → hatk migration 2 + 3 + _march 2026_ 4 + 5 + ## what changed 6 + 7 + rewrote the status app from a **quickslice** backend + **vanilla JS SPA** frontend (split across Fly.io and Cloudflare Pages) into a single **hatk** app (SvelteKit + typed XRPC backend) deployed to Fly.io only. 8 + 9 + ## architecture: before and after 10 + 11 + ### before (quickslice) 12 + 13 + ``` 14 + cloudflare pages fly.io 15 + ┌─────────────────┐ ┌──────────────────────┐ 16 + │ site/ │ │ quickslice (pre-built)│ 17 + │ index.html │ ──→ │ graphql api │ 18 + │ app.js (1695L) │ │ sqlite @ /data/ │ 19 + │ styles.css │ │ firehose ingestion │ 20 + │ functions/ │ └──────────────────────┘ 21 + │ status/[did]/ │ 22 + │ [rkey].js │ ← cloudflare pages function 23 + └─────────────────┘ for OG tag injection 24 + ``` 25 + 26 + - **backend**: quickslice — "appview in a bottle" by chad. pre-built docker image, graphql API, no code generation 27 + - **frontend**: vanilla JS SPA. one 1695-line `app.js`, one 1152-line `styles.css`, no build step 28 + - **link previews**: cloudflare pages function (`site/functions/status/[did]/[rkey].js`) intercepted social bot user agents, fetched status via graphql, returned HTML with OG tags 29 + - **deploy**: two targets — `fly deploy` for backend, cloudflare pages for frontend 30 + - **data access**: raw graphql queries inline in app.js 31 + 32 + ### after (hatk) 33 + 34 + ``` 35 + fly.io (single deploy) 36 + ┌──────────────────────────────────┐ 37 + │ hatk │ 38 + │ sveltekit frontend (app/) │ 39 + │ typed XRPC backend (server/) │ 40 + │ auto-generated from lexicons │ 41 + │ sqlite @ /data/status.db │ 42 + │ firehose ingestion + backfill │ 43 + │ oauth (AT Protocol native) │ 44 + │ SSR → link previews built-in │ 45 + └──────────────────────────────────┘ 46 + ``` 47 + 48 + - **framework**: hatk — full-stack AT Protocol framework. SvelteKit frontend + typed XRPC backend, types auto-generated from lexicons 49 + - **frontend**: svelte 5 components (30 files, 2248 lines total). tanstack svelte query for data fetching 50 + - **link previews**: SSR via `+page.server.ts` — no separate function needed, OG tags rendered server-side 51 + - **deploy**: single target — `fly deploy` 52 + - **data access**: `callXrpc('dev.hatk.getFeed', ...)` with full type safety 53 + 54 + ## infra changes 55 + 56 + | | before | after | 57 + |---|---|---| 58 + | **backend** | `ghcr.io/bigmoves/quickslice:latest` (pre-built) | custom Dockerfile, `node:25-slim`, `vp build` | 59 + | **API** | graphql | XRPC (AT Protocol native) | 60 + | **port** | 8080 | 3000 | 61 + | **frontend hosting** | cloudflare pages | same fly.io app (SvelteKit SSR) | 62 + | **OG previews** | cloudflare pages function | SvelteKit SSR (`+page.server.ts`) | 63 + | **domain** | `zzstoatzz-quickslice-status.fly.dev` (backend) + cloudflare pages (frontend) | `status.zzstoatzz.io` → fly.io (CNAME) | 64 + | **db** | `/data/quickslice.db` | `/data/status.db` (same volume, new file) | 65 + | **data migration** | n/a | none needed — AT Protocol repos are source of truth, hatk backfills from firehose | 66 + | **oauth** | quickslice client SDK | hatk built-in AT Protocol OAuth | 67 + | **dev env** | none documented | `docker-compose.yml` (PLC + PDS + postgres) | 68 + 69 + ## file structure comparison 70 + 71 + ### removed 72 + - `site/` — entire vanilla JS SPA (app.js, styles.css, index.html, functions/) 73 + - `notes/quickslice-migration.md` — old migration notes 74 + 75 + ### added 76 + 77 + ``` 78 + app/ # sveltekit frontend 79 + app.css # global styles (740L, was 1152L) 80 + app.html # shell with OG fallback tags 81 + lib/ 82 + components/ 83 + Header.svelte # nav, theme toggle, login/logout 84 + CreateStatusForm.svelte # emoji picker + text + expiration 85 + EmojiPicker.svelte # unicode + bufo tabs, semantic search 86 + StatusCard.svelte # single status display 87 + StatusFeed.svelte # paginated feed with "load more" 88 + LoginCard.svelte # handle input with typeahead 89 + SettingsModal.svelte # accent color, font, theme 90 + utils/ 91 + emoji.ts # bufo image URLs, emoji search, parseLinks 92 + time.ts # relativeTime, formatExpiration 93 + queries.ts # tanstack query options wrapping callXrpc 94 + preferences.ts # accent color, font, theme as CSS vars 95 + stores.ts # loginModalOpen (just one store now) 96 + auth.ts # re-export from hatk 97 + routes/ 98 + +layout.server.ts # parse viewer from session cookie 99 + +layout.ts # create QueryClient 100 + +layout.svelte # shell: QueryClientProvider + Header 101 + +page.svelte # home: login or current status + form 102 + +page.ts # prefetch actor feed 103 + feed/+page.svelte # global feed (all users) 104 + feed/+page.ts # prefetch recent feed 105 + profile/[did]/+page.svelte # user profile with history 106 + profile/[did]/+page.ts # prefetch actor feed 107 + status/[did]/[rkey]/ 108 + +page.server.ts # fetch status for SSR + OG tags 109 + +page.svelte # status permalink with OG meta 110 + oauth/callback/+page.svelte # OAuth redirect handler 111 + 112 + server/ # hatk backend 113 + feeds/ 114 + recent.ts # global feed — all statuses DESC 115 + actor.ts # per-user feed — single DID 116 + _hydrate.ts # DB rows → statusView objects 117 + on-login.ts # backfill user's repo on first login 118 + 119 + hatk.config.ts # relay, plc, OAuth, SQLite, backfill 120 + vite.config.ts # hatk() + sveltekit() plugins 121 + svelte.config.js # adapter-node, files.src: "app" 122 + docker-compose.yml # local dev: PLC + PDS + postgres 123 + Dockerfile # node:25-slim, vp build, hatk start 124 + ``` 125 + 126 + ## key differences in practice 127 + 128 + ### data fetching 129 + 130 + before (graphql in vanilla JS): 131 + ```js 132 + const res = await fetch(`${CONFIG.server}/graphql`, { 133 + method: 'POST', 134 + headers: { 'Content-Type': 'application/json' }, 135 + body: JSON.stringify({ 136 + query: `query { ioZzstoatzzStatusRecord(first: 20, sortBy: [...]) { edges { node { ... } } } }`, 137 + }) 138 + }); 139 + ``` 140 + 141 + after (typed XRPC via tanstack query): 142 + ```ts 143 + const feed = createQuery(() => recentFeedQuery()) 144 + // recentFeedQuery = () => callXrpc("dev.hatk.getFeed", { feed: "recent", limit: 50 }) 145 + ``` 146 + 147 + ### link previews 148 + 149 + before (cloudflare pages function): 150 + ```js 151 + // site/functions/status/[did]/[rkey].js 152 + export async function onRequest(context) { 153 + if (!isSocialBot(userAgent)) return next(); 154 + const status = await fetchStatus(did, rkey); // graphql 155 + return new Response(generateOgHtml(status, ...)); 156 + } 157 + ``` 158 + 159 + after (SvelteKit SSR): 160 + ```ts 161 + // +page.server.ts — fetches on server, svelte:head renders OG tags 162 + export const load = async ({ params }) => { 163 + const res = await callXrpc("dev.hatk.getRecord", { uri }); 164 + return { status: res.record }; 165 + }; 166 + ``` 167 + ```svelte 168 + <!-- +page.svelte — OG tags in svelte:head, rendered during SSR --> 169 + <svelte:head> 170 + <meta property="og:image" content={ogImage} /> 171 + </svelte:head> 172 + ``` 173 + 174 + ### auth 175 + 176 + before: quickslice client SDK (`QuicksliceClient.createQuicksliceClient()`), manual OAuth flow 177 + after: hatk built-in OAuth, session cookie parsed by `parseViewer(cookies)` 178 + 179 + ## bugs fixed during migration 180 + 181 + - **`store.subscribe is not a function` (SSR 500)**: tanstack svelte query v5 returns reactive objects, not svelte stores. `$feed.data` → `feed.data` 182 + - **OOM crashes**: backfill parallelism 5 on 1GB VM caused heap exhaustion. reduced to 2 + added `--max-old-space-size=768` 183 + - **header handle sticking**: header showed viewer handle on all pages. now shows contextual titles (home: `@handle`, feed: `global feed`, etc.) 184 + - **link previews missing**: `+page.ts` was overwriting `+page.server.ts` data, dropping the status. removed redundant `+page.ts` 185 + 186 + ## what we kept 187 + 188 + - same Fly.io app (`zzstoatzz-quickslice-status`) and volume (`quickslice_data`) 189 + - same lexicons (`io.zzstoatzz.status.record`, `io.zzstoatzz.status.preferences`) 190 + - bufo emoji support (custom:name → `all-the.bufo.zone/{name}.png`) 191 + - semantic bufo search via `find-bufo.fly.dev` 192 + - handle typeahead via `typeahead.waow.tech` 193 + - dark/light theme, accent colors, font preferences 194 + - same UX: home (your status + form), global feed, profile, permalink
+12
Dockerfile
··· 1 + FROM node:25-slim 2 + RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl sqlite3 \ 3 + && rm -rf /var/lib/apt/lists/* 4 + WORKDIR /app 5 + COPY package.json package-lock.json ./ 6 + RUN npm ci 7 + COPY . . 8 + RUN npx vp build 9 + RUN npm prune --omit=dev 10 + ENV NODE_ENV=production 11 + EXPOSE 3000 12 + CMD ["node", "--max-old-space-size=768", "--experimental-strip-types", "node_modules/@hatk/hatk/dist/main.js", "hatk.config.ts"]
+740
app/app.css
··· 1 + :root { 2 + --bg: #0a0a0a; 3 + --bg-card: #1a1a1a; 4 + --text: #ffffff; 5 + --text-secondary: #888; 6 + --accent: #4a9eff; 7 + --border: #2a2a2a; 8 + --radius: 12px; 9 + --font-family: ui-monospace, "SF Mono", Monaco, monospace; 10 + } 11 + 12 + [data-theme="light"] { 13 + --bg: #ffffff; 14 + --bg-card: #f5f5f5; 15 + --text: #1a1a1a; 16 + --text-secondary: #666; 17 + --border: #e0e0e0; 18 + } 19 + 20 + *, 21 + *::before, 22 + *::after { 23 + box-sizing: border-box; 24 + margin: 0; 25 + padding: 0; 26 + } 27 + 28 + ::-webkit-scrollbar { width: 8px; height: 8px; } 29 + ::-webkit-scrollbar-track { background: var(--bg); } 30 + ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; } 31 + ::-webkit-scrollbar-thumb:hover { background: var(--text-secondary); } 32 + * { scrollbar-width: thin; scrollbar-color: var(--border) var(--bg); } 33 + 34 + html { 35 + background: var(--bg); 36 + color: var(--text); 37 + } 38 + 39 + body { 40 + font-family: var(--font-family); 41 + background: var(--bg); 42 + color: var(--text); 43 + line-height: 1.6; 44 + min-height: 100vh; 45 + } 46 + 47 + a { color: inherit; text-decoration: none; } 48 + 49 + .app-shell { 50 + max-width: 600px; 51 + margin: 0 auto; 52 + padding: 2rem 1rem; 53 + } 54 + 55 + /* Header */ 56 + header { 57 + display: flex; 58 + justify-content: space-between; 59 + align-items: center; 60 + margin-bottom: 2rem; 61 + padding-bottom: 1rem; 62 + border-bottom: 1px solid var(--border); 63 + } 64 + 65 + header h1 { 66 + font-size: 1.5rem; 67 + font-weight: 600; 68 + } 69 + 70 + header h1 a { color: var(--text); } 71 + header h1 a:hover { color: var(--accent); } 72 + 73 + nav { 74 + display: flex; 75 + gap: 1rem; 76 + align-items: center; 77 + } 78 + 79 + .nav-btn { 80 + display: flex; 81 + align-items: center; 82 + justify-content: center; 83 + padding: 0.5rem; 84 + border-radius: 8px; 85 + transition: background 0.15s, color 0.15s; 86 + color: var(--text-secondary); 87 + background: none; 88 + border: none; 89 + cursor: pointer; 90 + } 91 + 92 + .nav-btn:hover { background: var(--bg-card); color: var(--accent); } 93 + .nav-btn svg { display: block; } 94 + 95 + .theme-toggle { 96 + background: none; 97 + border: 1px solid var(--border); 98 + border-radius: 8px; 99 + padding: 0.5rem; 100 + cursor: pointer; 101 + font-size: 1rem; 102 + color: var(--text); 103 + } 104 + 105 + /* Login */ 106 + .login-container { 107 + display: flex; 108 + flex-direction: column; 109 + align-items: center; 110 + justify-content: center; 111 + min-height: 50vh; 112 + padding: 2rem 1rem; 113 + } 114 + 115 + .login-card { 116 + background: var(--bg-card); 117 + border: 1px solid var(--border); 118 + border-radius: var(--radius); 119 + padding: 2.5rem 2rem; 120 + width: 100%; 121 + max-width: 380px; 122 + text-align: center; 123 + } 124 + 125 + .login-title { font-size: 1.75rem; font-weight: 600; margin-bottom: 0.5rem; } 126 + .login-tagline { color: var(--text-secondary); font-size: 1rem; margin-bottom: 1.5rem; } 127 + 128 + .login-form { 129 + display: flex; 130 + flex-direction: column; 131 + gap: 1.25rem; 132 + } 133 + 134 + .input-group { 135 + display: flex; 136 + flex-direction: column; 137 + gap: 0.5rem; 138 + } 139 + 140 + .input-group label { 141 + color: var(--text-secondary); 142 + font-size: 0.9rem; 143 + } 144 + 145 + .handle-input-wrapper { 146 + position: relative; 147 + width: 100%; 148 + } 149 + 150 + .handle-input-wrapper input { 151 + width: 100%; 152 + padding: 0.875rem 1rem; 153 + border: 1px solid var(--border); 154 + border-radius: var(--radius); 155 + background: var(--bg); 156 + color: var(--text); 157 + font-family: inherit; 158 + font-size: 1rem; 159 + transition: border-color 0.15s; 160 + } 161 + 162 + .handle-input-wrapper input:focus { 163 + outline: none; 164 + border-color: var(--accent); 165 + } 166 + 167 + /* Suggestions dropdown */ 168 + .suggestions-dropdown { 169 + position: absolute; 170 + top: 100%; 171 + left: 0; 172 + right: 0; 173 + margin-top: 4px; 174 + background: var(--bg-card); 175 + border: 1px solid var(--border); 176 + border-radius: 8px; 177 + overflow: hidden; 178 + z-index: 100; 179 + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); 180 + } 181 + 182 + .suggestion-item { 183 + display: flex; 184 + align-items: center; 185 + gap: 10px; 186 + width: 100%; 187 + padding: 10px 12px; 188 + background: transparent; 189 + border: none; 190 + border-bottom: 1px solid var(--border); 191 + color: var(--text); 192 + cursor: pointer; 193 + text-align: left; 194 + font-family: inherit; 195 + transition: background 0.15s; 196 + } 197 + 198 + .suggestion-item:last-child { border-bottom: none; } 199 + .suggestion-item:hover, 200 + .suggestion-item.selected { background: var(--bg); } 201 + 202 + .suggestion-avatar { 203 + width: 32px; 204 + height: 32px; 205 + border-radius: 50%; 206 + object-fit: cover; 207 + flex-shrink: 0; 208 + } 209 + 210 + .suggestion-avatar-placeholder { 211 + width: 32px; 212 + height: 32px; 213 + border-radius: 50%; 214 + background: var(--border); 215 + flex-shrink: 0; 216 + } 217 + 218 + .suggestion-info { display: flex; flex-direction: column; min-width: 0; } 219 + .suggestion-name { font-size: 14px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } 220 + .suggestion-handle { font-size: 12px; color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } 221 + 222 + /* FAQ */ 223 + .login-faq { 224 + margin-top: 1.5rem; 225 + border-top: 1px solid var(--border); 226 + padding-top: 1rem; 227 + } 228 + 229 + .faq-toggle { 230 + width: 100%; 231 + display: flex; 232 + justify-content: space-between; 233 + align-items: center; 234 + padding: 0.75rem 0; 235 + background: none; 236 + border: none; 237 + color: var(--text-secondary); 238 + font-family: inherit; 239 + font-size: 0.9rem; 240 + cursor: pointer; 241 + text-align: left; 242 + } 243 + 244 + .faq-toggle:hover { color: var(--text); } 245 + 246 + .faq-content { 247 + padding: 0 0 1rem 0; 248 + color: var(--text-secondary); 249 + font-size: 0.875rem; 250 + line-height: 1.6; 251 + } 252 + 253 + .faq-content p { margin: 0 0 0.75rem 0; text-align: left; } 254 + .faq-content p:last-child { margin-bottom: 0; } 255 + .faq-content a { color: var(--accent); } 256 + .faq-content a:hover { text-decoration: underline; } 257 + .faq-content code { background: var(--bg); padding: 0.15rem 0.4rem; border-radius: 4px; font-size: 0.85em; } 258 + 259 + /* Buttons */ 260 + button[type="submit"], 261 + .btn-primary { 262 + padding: 0.75rem 1.5rem; 263 + background: var(--accent); 264 + color: white; 265 + border: none; 266 + border-radius: var(--radius); 267 + cursor: pointer; 268 + font-family: inherit; 269 + font-size: 1rem; 270 + } 271 + 272 + button[type="submit"]:hover, 273 + .btn-primary:hover { opacity: 0.9; } 274 + 275 + .login-card button[type="submit"] { 276 + width: 100%; 277 + padding: 0.875rem 1rem; 278 + } 279 + 280 + /* Profile card */ 281 + .profile-card { 282 + background: var(--bg-card); 283 + border: 1px solid var(--border); 284 + border-radius: var(--radius); 285 + padding: 2rem; 286 + margin-bottom: 1.5rem; 287 + } 288 + 289 + .current-status { 290 + display: flex; 291 + flex-direction: column; 292 + align-items: center; 293 + gap: 1rem; 294 + text-align: center; 295 + } 296 + 297 + .big-emoji { font-size: 4rem; line-height: 1; } 298 + .big-emoji img { width: 4rem; height: 4rem; object-fit: contain; } 299 + .status-info { display: flex; flex-direction: column; gap: 0.25rem; } 300 + .current-text { font-size: 1.25rem; } 301 + .meta { color: var(--text-secondary); font-size: 0.875rem; } 302 + 303 + .current-status-actions { 304 + display: flex; 305 + gap: 0.5rem; 306 + margin-top: 0.5rem; 307 + } 308 + 309 + .current-status-actions .share-btn, 310 + .current-status-actions .delete-btn, 311 + .current-status-actions .embed-toggle-btn { 312 + opacity: 1; 313 + background: transparent; 314 + border: 1px solid var(--border); 315 + color: var(--text-secondary); 316 + cursor: pointer; 317 + padding: 0.375rem; 318 + border-radius: 6px; 319 + transition: color 0.15s, border-color 0.15s; 320 + } 321 + 322 + .current-status-actions button:hover { 323 + color: var(--accent); 324 + border-color: var(--accent); 325 + } 326 + 327 + .current-status-actions .delete-btn:hover { color: #ef4444; border-color: #ef4444; } 328 + 329 + /* Status form */ 330 + .status-form { 331 + background: var(--bg-card); 332 + border: 1px solid var(--border); 333 + border-radius: var(--radius); 334 + padding: 1rem; 335 + margin-bottom: 1.5rem; 336 + } 337 + 338 + .emoji-input-row { 339 + display: flex; 340 + gap: 0.5rem; 341 + margin-bottom: 0.75rem; 342 + } 343 + 344 + .emoji-trigger { 345 + width: 48px; 346 + height: 48px; 347 + border: 1px solid var(--border); 348 + border-radius: 8px; 349 + background: var(--bg); 350 + cursor: pointer; 351 + display: flex; 352 + align-items: center; 353 + justify-content: center; 354 + font-size: 1.5rem; 355 + flex-shrink: 0; 356 + transition: border-color 0.15s; 357 + } 358 + 359 + .emoji-trigger:hover { border-color: var(--accent); } 360 + .emoji-trigger img { width: 1.5rem; height: 1.5rem; object-fit: contain; } 361 + 362 + .emoji-input-row input[type="text"] { 363 + flex: 1; 364 + padding: 0.75rem; 365 + border: 1px solid var(--border); 366 + border-radius: 8px; 367 + background: var(--bg); 368 + color: var(--text); 369 + font-family: inherit; 370 + font-size: 1rem; 371 + } 372 + 373 + .emoji-input-row input[type="text"]:focus { 374 + outline: none; 375 + border-color: var(--accent); 376 + } 377 + 378 + .form-actions { 379 + display: flex; 380 + gap: 0.5rem; 381 + justify-content: flex-end; 382 + } 383 + 384 + .form-actions select, 385 + .form-actions .custom-datetime { 386 + padding: 0.75rem; 387 + border: 1px solid var(--border); 388 + border-radius: 8px; 389 + background: var(--bg); 390 + color: var(--text); 391 + font-family: inherit; 392 + } 393 + 394 + /* History */ 395 + .history { margin-bottom: 2rem; } 396 + 397 + .history h2 { 398 + font-size: 0.875rem; 399 + text-transform: uppercase; 400 + letter-spacing: 0.05em; 401 + color: var(--text-secondary); 402 + margin-bottom: 1rem; 403 + } 404 + 405 + /* Feed / status list */ 406 + .feed-list { 407 + display: flex; 408 + flex-direction: column; 409 + gap: 1rem; 410 + } 411 + 412 + .status-item { 413 + display: flex; 414 + gap: 1rem; 415 + padding: 1rem; 416 + background: var(--bg-card); 417 + border: 1px solid var(--border); 418 + border-radius: var(--radius); 419 + align-items: flex-start; 420 + transition: border-color 0.15s; 421 + } 422 + 423 + .status-item:hover { border-color: var(--accent); } 424 + .status-item .emoji { font-size: 1.5rem; line-height: 1; flex-shrink: 0; } 425 + .status-item .emoji img { width: 1.5rem; height: 1.5rem; object-fit: contain; } 426 + .status-item .content { flex: 1; min-width: 0; } 427 + .status-item .author { color: var(--text-secondary); font-weight: 600; } 428 + .status-item .author:hover { color: var(--accent); } 429 + .status-item .text { margin-left: 0.5rem; } 430 + .status-item .text a { color: var(--accent); } 431 + .status-item .text a:hover { text-decoration: underline; } 432 + .status-item .time { display: block; font-size: 0.875rem; color: var(--text-secondary); margin-top: 0.25rem; } 433 + 434 + .status-actions { 435 + display: flex; 436 + gap: 0.25rem; 437 + flex-shrink: 0; 438 + } 439 + 440 + .share-btn, 441 + .delete-btn { 442 + background: transparent; 443 + border: none; 444 + color: var(--text-secondary); 445 + cursor: pointer; 446 + padding: 0.25rem; 447 + border-radius: 4px; 448 + opacity: 0; 449 + transition: opacity 0.15s, color 0.15s; 450 + flex-shrink: 0; 451 + position: relative; 452 + } 453 + 454 + .status-item:hover .share-btn, 455 + .status-item:hover .delete-btn { opacity: 1; } 456 + .share-btn:hover { color: var(--accent); } 457 + .delete-btn:hover { color: #ef4444; } 458 + .share-btn.copied { color: var(--accent); opacity: 1; } 459 + 460 + /* Load more */ 461 + .load-more { 462 + text-align: center; 463 + padding: 1rem; 464 + } 465 + 466 + .load-more button { 467 + padding: 0.5rem 1.5rem; 468 + background: var(--bg-card); 469 + border: 1px solid var(--border); 470 + border-radius: var(--radius); 471 + color: var(--text); 472 + cursor: pointer; 473 + font-family: inherit; 474 + } 475 + 476 + .load-more button:hover { border-color: var(--accent); color: var(--accent); } 477 + .end-of-feed { text-align: center; padding: 1rem; color: var(--text-secondary); font-size: 0.875rem; } 478 + 479 + /* Emoji picker */ 480 + .emoji-picker-overlay { 481 + position: fixed; 482 + inset: 0; 483 + background: rgba(0, 0, 0, 0.6); 484 + z-index: 1000; 485 + display: flex; 486 + align-items: center; 487 + justify-content: center; 488 + } 489 + 490 + .emoji-picker { 491 + background: var(--bg-card); 492 + border: 1px solid var(--border); 493 + border-radius: var(--radius); 494 + width: 360px; 495 + max-height: 480px; 496 + display: flex; 497 + flex-direction: column; 498 + } 499 + 500 + .emoji-picker-header { 501 + display: flex; 502 + justify-content: space-between; 503 + align-items: center; 504 + padding: 1rem; 505 + border-bottom: 1px solid var(--border); 506 + } 507 + 508 + .emoji-picker-header h3 { font-size: 1rem; } 509 + 510 + .emoji-picker-close { 511 + background: none; 512 + border: none; 513 + color: var(--text-secondary); 514 + cursor: pointer; 515 + font-size: 1.25rem; 516 + } 517 + 518 + .emoji-search { 519 + margin: 0.75rem; 520 + padding: 0.5rem 0.75rem; 521 + border: 1px solid var(--border); 522 + border-radius: 8px; 523 + background: var(--bg); 524 + color: var(--text); 525 + font-family: inherit; 526 + font-size: 0.875rem; 527 + } 528 + 529 + .emoji-search:focus { outline: none; border-color: var(--accent); } 530 + 531 + .emoji-categories { 532 + display: flex; 533 + gap: 0.25rem; 534 + padding: 0 0.75rem; 535 + border-bottom: 1px solid var(--border); 536 + padding-bottom: 0.5rem; 537 + } 538 + 539 + .category-btn { 540 + background: none; 541 + border: none; 542 + padding: 0.375rem; 543 + border-radius: 4px; 544 + cursor: pointer; 545 + font-size: 1rem; 546 + opacity: 0.5; 547 + transition: opacity 0.15s; 548 + } 549 + 550 + .category-btn:hover { opacity: 0.8; } 551 + .category-btn.active { opacity: 1; background: var(--bg); } 552 + 553 + .emoji-grid { 554 + display: grid; 555 + grid-template-columns: repeat(8, 1fr); 556 + gap: 2px; 557 + padding: 0.75rem; 558 + overflow-y: auto; 559 + flex: 1; 560 + } 561 + 562 + .emoji-grid.bufo-grid { 563 + grid-template-columns: repeat(5, 1fr); 564 + gap: 4px; 565 + } 566 + 567 + .emoji-btn { 568 + background: none; 569 + border: none; 570 + padding: 0.375rem; 571 + border-radius: 4px; 572 + cursor: pointer; 573 + font-size: 1.25rem; 574 + transition: background 0.15s; 575 + display: flex; 576 + align-items: center; 577 + justify-content: center; 578 + } 579 + 580 + .emoji-btn:hover { background: var(--bg); } 581 + 582 + .bufo-btn { 583 + flex-direction: column; 584 + gap: 2px; 585 + padding: 4px; 586 + } 587 + 588 + .bufo-btn img { 589 + width: 36px; 590 + height: 36px; 591 + object-fit: contain; 592 + } 593 + 594 + .bufo-score { 595 + font-size: 0.6rem; 596 + color: var(--text-secondary); 597 + } 598 + 599 + .bufo-helper { 600 + padding: 0.5rem 0.75rem; 601 + border-top: 1px solid var(--border); 602 + font-size: 0.75rem; 603 + text-align: center; 604 + } 605 + 606 + .bufo-helper a { color: var(--accent); } 607 + 608 + .no-results, 609 + .loading { 610 + grid-column: 1 / -1; 611 + text-align: center; 612 + padding: 2rem 1rem; 613 + color: var(--text-secondary); 614 + font-size: 0.875rem; 615 + } 616 + 617 + /* Settings modal */ 618 + .settings-overlay { 619 + position: fixed; 620 + inset: 0; 621 + background: rgba(0, 0, 0, 0.6); 622 + z-index: 1000; 623 + display: flex; 624 + align-items: center; 625 + justify-content: center; 626 + } 627 + 628 + .settings-modal { 629 + background: var(--bg-card); 630 + border: 1px solid var(--border); 631 + border-radius: var(--radius); 632 + width: 360px; 633 + padding: 1.5rem; 634 + } 635 + 636 + .settings-header { 637 + display: flex; 638 + justify-content: space-between; 639 + align-items: center; 640 + margin-bottom: 1.5rem; 641 + } 642 + 643 + .settings-header h3 { font-size: 1rem; } 644 + 645 + .settings-close { 646 + background: none; 647 + border: none; 648 + color: var(--text-secondary); 649 + cursor: pointer; 650 + font-size: 1.25rem; 651 + } 652 + 653 + .setting-group { 654 + margin-bottom: 1.25rem; 655 + } 656 + 657 + .setting-group label { 658 + display: block; 659 + color: var(--text-secondary); 660 + font-size: 0.875rem; 661 + margin-bottom: 0.5rem; 662 + } 663 + 664 + .color-picker { 665 + display: flex; 666 + gap: 0.5rem; 667 + flex-wrap: wrap; 668 + align-items: center; 669 + } 670 + 671 + .color-btn { 672 + width: 28px; 673 + height: 28px; 674 + border-radius: 50%; 675 + border: 2px solid transparent; 676 + cursor: pointer; 677 + transition: border-color 0.15s; 678 + } 679 + 680 + .color-btn:hover { border-color: var(--text-secondary); } 681 + .color-btn.active { border-color: var(--text); } 682 + 683 + .custom-color-input { 684 + width: 28px; 685 + height: 28px; 686 + border: none; 687 + border-radius: 50%; 688 + cursor: pointer; 689 + padding: 0; 690 + } 691 + 692 + .setting-group select { 693 + width: 100%; 694 + padding: 0.5rem; 695 + border: 1px solid var(--border); 696 + border-radius: 8px; 697 + background: var(--bg); 698 + color: var(--text); 699 + font-family: inherit; 700 + } 701 + 702 + .settings-footer { 703 + margin-top: 1rem; 704 + } 705 + 706 + .save-btn { 707 + width: 100%; 708 + padding: 0.75rem; 709 + background: var(--accent); 710 + color: white; 711 + border: none; 712 + border-radius: var(--radius); 713 + cursor: pointer; 714 + font-family: inherit; 715 + font-size: 1rem; 716 + } 717 + 718 + .save-btn:hover { opacity: 0.9; } 719 + .save-btn:disabled { opacity: 0.5; cursor: not-allowed; } 720 + 721 + /* View profile link */ 722 + .view-profile-link { 723 + color: var(--accent); 724 + font-size: 0.875rem; 725 + } 726 + 727 + .view-profile-link:hover { text-decoration: underline; } 728 + 729 + /* Utility */ 730 + .center { text-align: center; padding: 2rem; } 731 + .hidden { display: none !important; } 732 + 733 + @media (max-width: 480px) { 734 + .app-shell { padding: 1rem 0.75rem; } 735 + .profile-card { padding: 1.5rem 1rem; } 736 + .big-emoji { font-size: 3rem; } 737 + .big-emoji img { width: 3rem; height: 3rem; } 738 + .emoji-picker { width: calc(100vw - 2rem); } 739 + .settings-modal { width: calc(100vw - 2rem); } 740 + }
+22
app/app.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="utf-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1" /> 6 + <link rel="icon" type="image/svg+xml" href="%sveltekit.assets%/favicon.svg" /> 7 + <meta name="theme-color" content="#0a0a0a" /> 8 + <meta property="og:type" content="website" /> 9 + <meta property="og:title" content="status" /> 10 + <meta property="og:description" content="share your status" /> 11 + <meta property="og:url" content="https://status.zzstoatzz.io" /> 12 + <meta property="og:site_name" content="status" /> 13 + <meta name="twitter:card" content="summary" /> 14 + <meta name="twitter:title" content="status" /> 15 + <meta name="twitter:description" content="share your status" /> 16 + <title>status</title> 17 + %sveltekit.head% 18 + </head> 19 + <body data-sveltekit-preload-data="hover"> 20 + <div style="display: contents">%sveltekit.body%</div> 21 + </body> 22 + </html>
+1
app/lib/auth.ts
··· 1 + export { login, logout, viewerDid } from "$hatk/client";
+101
app/lib/components/CreateStatusForm.svelte
··· 1 + <script lang="ts"> 2 + import { page } from '$app/stores' 3 + import { callXrpc } from '$hatk/client' 4 + import { isCustomEmoji, customEmojiName, bufoImageUrl, bufoFallbackUrl } from '$lib/utils/emoji' 5 + import EmojiPicker from './EmojiPicker.svelte' 6 + 7 + let { currentEmoji = '😊', oncreated }: { currentEmoji?: string; oncreated?: () => void } = $props() 8 + 9 + let selectedEmoji = $derived(currentEmoji) 10 + let text = $state('') 11 + let expiresValue = $state('') 12 + let customDatetime = $state('') 13 + let showPicker = $state(false) 14 + let submitting = $state(false) 15 + 16 + function toLocalDatetimeString(date: Date) { 17 + const offset = date.getTimezoneOffset() 18 + const local = new Date(date.getTime() - offset * 60 * 1000) 19 + return local.toISOString().slice(0, 16) 20 + } 21 + 22 + function onExpiresChange() { 23 + if (expiresValue === 'custom') { 24 + const defaultTime = new Date(Date.now() + 60 * 60 * 1000) 25 + customDatetime = toLocalDatetimeString(defaultTime) 26 + } 27 + } 28 + 29 + async function submit(e: Event) { 30 + e.preventDefault() 31 + if (!selectedEmoji || !$page.data.viewer) return 32 + 33 + submitting = true 34 + try { 35 + const record: Record<string, string> = { 36 + emoji: selectedEmoji, 37 + createdAt: new Date().toISOString(), 38 + } 39 + if (text.trim()) record.text = text.trim() 40 + if (expiresValue === 'custom' && customDatetime) { 41 + record.expires = new Date(customDatetime).toISOString() 42 + } else if (expiresValue && expiresValue !== 'custom') { 43 + record.expires = new Date(Date.now() + parseInt(expiresValue) * 60 * 1000).toISOString() 44 + } 45 + 46 + await callXrpc('dev.hatk.createRecord', { 47 + collection: 'io.zzstoatzz.status.record', 48 + repo: $page.data.viewer.did, 49 + record, 50 + }) 51 + 52 + text = '' 53 + expiresValue = '' 54 + oncreated?.() 55 + } catch (err: any) { 56 + alert('Failed to set status: ' + (err?.message ?? err)) 57 + } finally { 58 + submitting = false 59 + } 60 + } 61 + </script> 62 + 63 + <form class="status-form" onsubmit={submit}> 64 + <div class="emoji-input-row"> 65 + <button type="button" class="emoji-trigger" onclick={() => showPicker = true}> 66 + {#if isCustomEmoji(selectedEmoji)} 67 + {@const name = customEmojiName(selectedEmoji)} 68 + <img src={bufoImageUrl(name)} alt={name} onerror={(e) => { (e.currentTarget as HTMLImageElement).src = bufoFallbackUrl(name) }} /> 69 + {:else} 70 + {selectedEmoji} 71 + {/if} 72 + </button> 73 + <input type="text" placeholder="what's happening?" maxlength="256" bind:value={text} /> 74 + </div> 75 + <div class="form-actions"> 76 + <select bind:value={expiresValue} onchange={onExpiresChange}> 77 + <option value="">don't clear</option> 78 + <option value="30">30 min</option> 79 + <option value="60">1 hour</option> 80 + <option value="120">2 hours</option> 81 + <option value="240">4 hours</option> 82 + <option value="480">8 hours</option> 83 + <option value="1440">1 day</option> 84 + <option value="10080">1 week</option> 85 + <option value="custom">custom...</option> 86 + </select> 87 + {#if expiresValue === 'custom'} 88 + <input type="datetime-local" class="custom-datetime" bind:value={customDatetime} min={toLocalDatetimeString(new Date())} /> 89 + {/if} 90 + <button type="submit" disabled={submitting}> 91 + {submitting ? 'setting...' : 'set status'} 92 + </button> 93 + </div> 94 + </form> 95 + 96 + {#if showPicker} 97 + <EmojiPicker 98 + onselect={(emoji) => { selectedEmoji = emoji; showPicker = false }} 99 + onclose={() => showPicker = false} 100 + /> 101 + {/if}
+151
app/lib/components/EmojiPicker.svelte
··· 1 + <script lang="ts"> 2 + import { loadBufoList, searchBufos, loadEmojiData, searchEmojis, DEFAULT_FREQUENT } from '$lib/utils/emoji' 3 + 4 + let { onselect, onclose }: { onselect: (emoji: string) => void; onclose: () => void } = $props() 5 + 6 + let currentCategory = $state('frequent') 7 + let searchQuery = $state('') 8 + let gridItems: Array<{ type: 'emoji' | 'bufo'; value: string; name?: string; score?: number }> = $state([]) 9 + let loading = $state(false) 10 + let bufoSearchTimer: ReturnType<typeof setTimeout> | undefined 11 + 12 + const categories = [ 13 + { id: 'frequent', icon: '\u2B50' }, 14 + { id: 'custom', icon: '\uD83D\uDC38' }, 15 + { id: 'people', icon: '\uD83D\uDE0A' }, 16 + { id: 'nature', icon: '\uD83C\uDF3F' }, 17 + { id: 'food', icon: '\uD83C\uDF54' }, 18 + { id: 'activity', icon: '\u26BD' }, 19 + { id: 'travel', icon: '\u2708\uFE0F' }, 20 + { id: 'objects', icon: '\uD83D\uDCA1' }, 21 + { id: 'symbols', icon: '\uD83D\uDC95' }, 22 + { id: 'flags', icon: '\uD83C\uDFC1' }, 23 + ] 24 + 25 + async function renderCategory(cat: string) { 26 + currentCategory = cat 27 + searchQuery = '' 28 + loading = true 29 + 30 + if (cat === 'custom') { 31 + try { 32 + const bufos = await loadBufoList() 33 + gridItems = bufos.map(name => ({ type: 'bufo', value: `custom:${name}`, name })) 34 + } catch { 35 + gridItems = [] 36 + } 37 + } else if (cat === 'frequent') { 38 + gridItems = DEFAULT_FREQUENT.map(e => ({ type: 'emoji', value: e })) 39 + } else { 40 + try { 41 + const data = await loadEmojiData() 42 + const emojis = data.categories[cat] || [] 43 + gridItems = emojis.map(e => ({ type: 'emoji', value: e })) 44 + } catch { 45 + gridItems = [] 46 + } 47 + } 48 + loading = false 49 + } 50 + 51 + async function handleSearch() { 52 + const q = searchQuery.trim() 53 + if (!q) { 54 + renderCategory(currentCategory) 55 + return 56 + } 57 + 58 + if (currentCategory === 'custom') { 59 + clearTimeout(bufoSearchTimer) 60 + bufoSearchTimer = setTimeout(async () => { 61 + loading = true 62 + try { 63 + const results = await searchBufos(q, 30) 64 + if (searchQuery.trim() !== q) return 65 + gridItems = results.map(r => ({ type: 'bufo', value: `custom:${r.name}`, name: r.name, score: r.score })) 66 + } catch { 67 + gridItems = [] 68 + } 69 + loading = false 70 + }, 300) 71 + return 72 + } 73 + 74 + loading = true 75 + try { 76 + const data = await loadEmojiData() 77 + const emojiResults = searchEmojis(q, data) 78 + const bufos = await loadBufoList().catch(() => [] as string[]) 79 + const qLower = q.toLowerCase() 80 + const bufoResults = bufos.filter(name => name.toLowerCase().includes(qLower)).slice(0, 30) 81 + 82 + gridItems = [ 83 + ...emojiResults.map(e => ({ type: 'emoji' as const, value: e })), 84 + ...bufoResults.map(name => ({ type: 'bufo' as const, value: `custom:${name}`, name })), 85 + ] 86 + } catch { 87 + gridItems = [] 88 + } 89 + loading = false 90 + } 91 + 92 + function select(value: string) { 93 + onselect(value) 94 + onclose() 95 + } 96 + 97 + $effect(() => { 98 + renderCategory('frequent') 99 + }) 100 + </script> 101 + 102 + <!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions --> 103 + <div class="emoji-picker-overlay" onclick={(e) => { if (e.target === e.currentTarget) onclose() }}> 104 + <div class="emoji-picker"> 105 + <div class="emoji-picker-header"> 106 + <h3>pick an emoji</h3> 107 + <button class="emoji-picker-close" onclick={onclose}>&#x2715;</button> 108 + </div> 109 + <input 110 + type="text" 111 + class="emoji-search" 112 + placeholder={currentCategory === 'custom' ? 'describe a bufo... try "happy" or "apocalyptic"' : 'search emojis...'} 113 + bind:value={searchQuery} 114 + oninput={handleSearch} 115 + /> 116 + <div class="emoji-categories"> 117 + {#each categories as cat (cat.id)} 118 + <button 119 + class="category-btn" 120 + class:active={currentCategory === cat.id} 121 + onclick={() => renderCategory(cat.id)} 122 + >{cat.icon}</button> 123 + {/each} 124 + </div> 125 + <div class="emoji-grid" class:bufo-grid={currentCategory === 'custom' || gridItems.some(i => i.type === 'bufo')}> 126 + {#if loading} 127 + <div class="loading">loading...</div> 128 + {:else if gridItems.length === 0} 129 + <div class="no-results">no emojis found</div> 130 + {:else} 131 + {#each gridItems as item (item.value)} 132 + {#if item.type === 'bufo'} 133 + <button class="emoji-btn bufo-btn" onclick={() => select(item.value)} title={item.name}> 134 + <img src="https://all-the.bufo.zone/{item.name}.png" alt={item.name ?? ''} loading="lazy" onerror={(e) => { const img = e.currentTarget as HTMLImageElement; img.src = img.src.replace('.png', '.gif') }} /> 135 + {#if item.score != null} 136 + <span class="bufo-score">{Math.round(item.score * 100)}%</span> 137 + {/if} 138 + </button> 139 + {:else} 140 + <button class="emoji-btn" onclick={() => select(item.value)}>{item.value}</button> 141 + {/if} 142 + {/each} 143 + {/if} 144 + </div> 145 + {#if currentCategory === 'custom'} 146 + <div class="bufo-helper"> 147 + <a href="https://find-bufo.com" target="_blank" rel="noopener">powered by find-bufo.com</a> 148 + </div> 149 + {/if} 150 + </div> 151 + </div>
+85
app/lib/components/Header.svelte
··· 1 + <script lang="ts"> 2 + import { page } from '$app/stores' 3 + import { logout } from '$lib/auth' 4 + import { House, Rss, Settings, LogOut } from 'lucide-svelte' 5 + import SettingsModal from './SettingsModal.svelte' 6 + 7 + let settingsOpen = $state(false) 8 + 9 + const viewer = $derived($page.data.viewer) 10 + const currentPage = $derived($page.url.pathname) 11 + 12 + function toggleTheme() { 13 + const current = document.documentElement.getAttribute('data-theme') 14 + const next = current === 'dark' ? 'light' : 'dark' 15 + document.documentElement.setAttribute('data-theme', next) 16 + localStorage.setItem('theme', next) 17 + } 18 + 19 + async function handleLogout() { 20 + await logout() 21 + window.location.href = '/' 22 + } 23 + 24 + function initTheme() { 25 + if (typeof document === 'undefined') return 26 + const saved = localStorage.getItem('theme') || 'dark' 27 + document.documentElement.setAttribute('data-theme', saved) 28 + } 29 + 30 + $effect(() => { 31 + initTheme() 32 + }) 33 + </script> 34 + 35 + <header> 36 + <h1> 37 + {#if currentPage === '/' && viewer} 38 + <a href="https://bsky.app/profile/{viewer.handle ?? viewer.did}" target="_blank"> 39 + @{viewer.handle ?? viewer.did.slice(0, 18)} 40 + </a> 41 + {:else if currentPage.startsWith('/feed')} 42 + global feed 43 + {:else if currentPage.startsWith('/profile/')} 44 + profile 45 + {:else} 46 + status 47 + {/if} 48 + </h1> 49 + <nav> 50 + {#if currentPage !== '/'} 51 + <a href="/" class="nav-btn" aria-label="home" title="home"> 52 + <House size={20} /> 53 + </a> 54 + {/if} 55 + {#if currentPage !== '/feed'} 56 + <a href="/feed" class="nav-btn" aria-label="feed" title="global feed"> 57 + <Rss size={20} /> 58 + </a> 59 + {/if} 60 + {#if viewer} 61 + <button class="nav-btn" onclick={() => settingsOpen = true} aria-label="settings" title="settings"> 62 + <Settings size={20} /> 63 + </button> 64 + {/if} 65 + <button class="theme-toggle" onclick={toggleTheme} aria-label="toggle theme"> 66 + <span class="sun">☀</span><span class="moon">☾</span> 67 + </button> 68 + {#if viewer} 69 + <button class="nav-btn" onclick={handleLogout} aria-label="log out" title="log out"> 70 + <LogOut size={20} /> 71 + </button> 72 + {/if} 73 + </nav> 74 + </header> 75 + 76 + {#if settingsOpen} 77 + <SettingsModal onclose={() => settingsOpen = false} /> 78 + {/if} 79 + 80 + <style> 81 + .sun { display: none; } 82 + .moon { display: inline; } 83 + :global([data-theme="light"]) .sun { display: inline; } 84 + :global([data-theme="light"]) .moon { display: none; } 85 + </style>
+147
app/lib/components/LoginCard.svelte
··· 1 + <script lang="ts"> 2 + import { login } from '$lib/auth' 3 + import { ChevronDown } from 'lucide-svelte' 4 + 5 + let handle = $state('') 6 + let suggestions: Array<{ handle: string; displayName?: string; avatar?: string }> = $state([]) 7 + let selectedIndex = $state(-1) 8 + let showDropdown = $state(false) 9 + let debounceTimer: ReturnType<typeof setTimeout> | undefined 10 + let abortController: AbortController | null = null 11 + let faqOpen: Record<string, boolean> = $state({}) 12 + 13 + async function fetchSuggestions(query: string) { 14 + if (abortController) abortController.abort() 15 + abortController = new AbortController() 16 + try { 17 + const url = `https://typeahead.waow.tech/xrpc/app.bsky.actor.searchActorsTypeahead?q=${encodeURIComponent(query)}&limit=5` 18 + const res = await fetch(url, { signal: abortController.signal }) 19 + if (!res.ok) return [] 20 + const data = await res.json() 21 + return data.actors || [] 22 + } catch { 23 + return [] 24 + } 25 + } 26 + 27 + function oninput() { 28 + const q = handle.trim() 29 + clearTimeout(debounceTimer) 30 + if (q.length < 3) { 31 + suggestions = [] 32 + showDropdown = false 33 + return 34 + } 35 + debounceTimer = setTimeout(async () => { 36 + suggestions = await fetchSuggestions(q) 37 + selectedIndex = -1 38 + showDropdown = suggestions.length > 0 39 + }, 300) 40 + } 41 + 42 + function selectSuggestion(h: string) { 43 + handle = h 44 + showDropdown = false 45 + suggestions = [] 46 + } 47 + 48 + function onkeydown(e: KeyboardEvent) { 49 + if (!showDropdown || suggestions.length === 0) return 50 + if (e.key === 'ArrowDown') { 51 + e.preventDefault() 52 + selectedIndex = Math.min(selectedIndex + 1, suggestions.length - 1) 53 + } else if (e.key === 'ArrowUp') { 54 + e.preventDefault() 55 + selectedIndex = Math.max(selectedIndex - 1, -1) 56 + } else if (e.key === 'Enter' && selectedIndex >= 0) { 57 + e.preventDefault() 58 + selectSuggestion(suggestions[selectedIndex].handle) 59 + } else if (e.key === 'Escape') { 60 + showDropdown = false 61 + } 62 + } 63 + 64 + async function submit(e: Event) { 65 + e.preventDefault() 66 + const h = handle.trim() 67 + if (h) await login(h) 68 + } 69 + 70 + function toggleFaq(id: string) { 71 + faqOpen[id] = !faqOpen[id] 72 + } 73 + </script> 74 + 75 + <div class="login-container"> 76 + <div class="login-card"> 77 + <h2 class="login-title">what's happening?</h2> 78 + <p class="login-tagline">share what you're up to</p> 79 + <form class="login-form" onsubmit={submit}> 80 + <div class="input-group"> 81 + <label for="handle-input">internet handle</label> 82 + <div class="handle-input-wrapper"> 83 + <input 84 + id="handle-input" 85 + type="text" 86 + placeholder="you.bsky.social" 87 + autocomplete="off" 88 + spellcheck="false" 89 + required 90 + bind:value={handle} 91 + {oninput} 92 + {onkeydown} 93 + onblur={() => setTimeout(() => showDropdown = false, 200)} 94 + onfocus={() => { if (handle.trim().length >= 3 && suggestions.length > 0) showDropdown = true }} 95 + /> 96 + {#if showDropdown} 97 + <div class="suggestions-dropdown"> 98 + {#each suggestions as s, i (s.handle)} 99 + <button type="button" class="suggestion-item" class:selected={i === selectedIndex} onclick={() => selectSuggestion(s.handle)}> 100 + {#if s.avatar} 101 + <img src={s.avatar} class="suggestion-avatar" alt="" /> 102 + {:else} 103 + <div class="suggestion-avatar-placeholder"></div> 104 + {/if} 105 + <div class="suggestion-info"> 106 + <span class="suggestion-name">{s.displayName || s.handle}</span> 107 + <span class="suggestion-handle">@{s.handle}</span> 108 + </div> 109 + </button> 110 + {/each} 111 + </div> 112 + {/if} 113 + </div> 114 + </div> 115 + <button type="submit">sign in</button> 116 + </form> 117 + <div class="login-faq"> 118 + <button type="button" class="faq-toggle" onclick={() => toggleFaq('handle')}> 119 + <span>what is an internet handle?</span> 120 + <ChevronDown size={16} style={faqOpen.handle ? 'transform: rotate(180deg)' : ''} /> 121 + </button> 122 + {#if faqOpen.handle} 123 + <div class="faq-content"> 124 + <p> 125 + your internet handle is a domain that identifies you across apps built on 126 + <a href="https://atproto.com" target="_blank" rel="noopener">AT Protocol</a>. 127 + if you signed up for Bluesky or another ATProto service, you already have one 128 + (like <code>yourname.bsky.social</code>). 129 + </p> 130 + <p>read more at <a href="https://internethandle.org" target="_blank" rel="noopener">internethandle.org</a>.</p> 131 + </div> 132 + {/if} 133 + <button type="button" class="faq-toggle" onclick={() => toggleFaq('signup')}> 134 + <span>don't have one?</span> 135 + <ChevronDown size={16} style={faqOpen.signup ? 'transform: rotate(180deg)' : ''} /> 136 + </button> 137 + {#if faqOpen.signup} 138 + <div class="faq-content"> 139 + <p> 140 + the easiest way to get one is to sign up for <a href="https://bsky.app" target="_blank" rel="noopener">Bluesky</a>. 141 + once you have an account, you can use that handle here. 142 + </p> 143 + </div> 144 + {/if} 145 + </div> 146 + </div> 147 + </div>
+70
app/lib/components/SettingsModal.svelte
··· 1 + <script lang="ts"> 2 + import { preferences, savePreferences, ACCENT_COLORS, FONTS, type Preferences } from '$lib/preferences' 3 + import { get } from 'svelte/store' 4 + 5 + let { onclose }: { onclose: () => void } = $props() 6 + 7 + let current: Preferences = $state({ ...get(preferences) }) 8 + let saving = $state(false) 9 + 10 + function selectColor(color: string) { 11 + current.accentColor = color 12 + } 13 + 14 + async function save() { 15 + saving = true 16 + try { 17 + await savePreferences(current) 18 + onclose() 19 + } finally { 20 + saving = false 21 + } 22 + } 23 + </script> 24 + 25 + <!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions --> 26 + <div class="settings-overlay" onclick={(e) => { if (e.target === e.currentTarget) onclose() }}> 27 + <div class="settings-modal"> 28 + <div class="settings-header"> 29 + <h3>settings</h3> 30 + <button class="settings-close" onclick={onclose}>&#x2715;</button> 31 + </div> 32 + <div class="settings-content"> 33 + <div class="setting-group"> 34 + <label>accent color</label> 35 + <div class="color-picker"> 36 + {#each ACCENT_COLORS as color} 37 + <button 38 + class="color-btn" 39 + class:active={current.accentColor === color} 40 + style="background: {color}" 41 + onclick={() => selectColor(color)} 42 + ></button> 43 + {/each} 44 + <input type="color" class="custom-color-input" bind:value={current.accentColor} /> 45 + </div> 46 + </div> 47 + <div class="setting-group"> 48 + <label>font</label> 49 + <select bind:value={current.font}> 50 + {#each FONTS as f} 51 + <option value={f.value}>{f.label}</option> 52 + {/each} 53 + </select> 54 + </div> 55 + <div class="setting-group"> 56 + <label>theme</label> 57 + <select bind:value={current.theme}> 58 + <option value="dark">dark</option> 59 + <option value="light">light</option> 60 + <option value="system">system</option> 61 + </select> 62 + </div> 63 + </div> 64 + <div class="settings-footer"> 65 + <button class="save-btn" onclick={save} disabled={saving}> 66 + {saving ? 'saving...' : 'save'} 67 + </button> 68 + </div> 69 + </div> 70 + </div>
+85
app/lib/components/StatusCard.svelte
··· 1 + <script lang="ts"> 2 + import { isCustomEmoji, customEmojiName, bufoImageUrl, bufoFallbackUrl, parseLinks, parseStatusUri } from '$lib/utils/emoji' 3 + import { relativeTime, formatExpiration } from '$lib/utils/time' 4 + import { Link, X } from 'lucide-svelte' 5 + 6 + interface StatusItem { 7 + uri: string 8 + emoji: string 9 + text?: string 10 + handle?: string 11 + did?: string 12 + createdAt: string 13 + expires?: string 14 + expired?: boolean 15 + } 16 + 17 + let { 18 + status, 19 + showAuthor = false, 20 + showDelete = false, 21 + ondelete, 22 + }: { 23 + status: StatusItem 24 + showAuthor?: boolean 25 + showDelete?: boolean 26 + ondelete?: (rkey: string) => void 27 + } = $props() 28 + 29 + let copied = $state(false) 30 + 31 + function getPermalink() { 32 + const { did, rkey } = parseStatusUri(status.uri) 33 + return `${window.location.origin}/status/${did}/${rkey}` 34 + } 35 + 36 + async function share() { 37 + try { 38 + await navigator.clipboard.writeText(getPermalink()) 39 + copied = true 40 + setTimeout(() => copied = false, 1500) 41 + } catch {} 42 + } 43 + 44 + function handleDelete() { 45 + const { rkey } = parseStatusUri(status.uri) 46 + ondelete?.(rkey) 47 + } 48 + </script> 49 + 50 + <div class="status-item"> 51 + <span class="emoji"> 52 + {#if isCustomEmoji(status.emoji)} 53 + {@const name = customEmojiName(status.emoji)} 54 + <img src={bufoImageUrl(name)} alt={name} onerror={(e) => { e.currentTarget.src = bufoFallbackUrl(name) }} /> 55 + {:else} 56 + {status.emoji} 57 + {/if} 58 + </span> 59 + <div class="content"> 60 + <div> 61 + {#if showAuthor && (status.handle || status.did)} 62 + <a href="/profile/{status.did}" class="author">@{status.handle ?? status.did?.slice(0, 18)}</a> 63 + {/if} 64 + {#if status.text} 65 + <span class="text">{@html parseLinks(status.text)}</span> 66 + {/if} 67 + </div> 68 + <span class="time"> 69 + {relativeTime(status.createdAt)} 70 + {#if status.expires} 71 + &middot; {formatExpiration(status.expires)} 72 + {/if} 73 + </span> 74 + </div> 75 + <div class="status-actions"> 76 + <button class="share-btn" class:copied onclick={share} title="copy link"> 77 + <Link size={14} /> 78 + </button> 79 + {#if showDelete && ondelete} 80 + <button class="delete-btn" onclick={handleDelete} title="delete"> 81 + <X size={14} /> 82 + </button> 83 + {/if} 84 + </div> 85 + </div>
+69
app/lib/components/StatusFeed.svelte
··· 1 + <script lang="ts"> 2 + import { untrack } from 'svelte' 3 + import { callXrpc } from '$hatk/client' 4 + import StatusCard from './StatusCard.svelte' 5 + 6 + interface StatusItem { 7 + uri: string 8 + cid: string 9 + did: string 10 + handle: string 11 + emoji: string 12 + text?: string 13 + expires?: string 14 + createdAt: string 15 + indexedAt: string 16 + expired: boolean 17 + } 18 + 19 + let { 20 + feed, 21 + initialItems = [], 22 + initialCursor, 23 + showAuthor = false, 24 + showDelete = false, 25 + ondelete, 26 + }: { 27 + feed: string 28 + initialItems?: StatusItem[] 29 + initialCursor?: string 30 + showAuthor?: boolean 31 + showDelete?: boolean 32 + ondelete?: (rkey: string) => void 33 + } = $props() 34 + 35 + let items: StatusItem[] = $state(untrack(() => [...initialItems])) 36 + let cursor: string | undefined = $state(untrack(() => initialCursor)) 37 + let loadingMore = $state(false) 38 + let hasMore = $derived(!!cursor) 39 + 40 + async function loadMore() { 41 + if (!cursor || loadingMore) return 42 + loadingMore = true 43 + try { 44 + const res = await callXrpc('dev.hatk.getFeed', { feed, cursor, limit: 20 }) 45 + items = [...items, ...(res.items ?? [])] 46 + cursor = res.cursor 47 + } catch (err) { 48 + console.error('Failed to load more:', err) 49 + } finally { 50 + loadingMore = false 51 + } 52 + } 53 + </script> 54 + 55 + <div class="feed-list"> 56 + {#each items as status (status.uri)} 57 + <StatusCard {status} {showAuthor} {showDelete} {ondelete} /> 58 + {/each} 59 + </div> 60 + 61 + {#if hasMore} 62 + <div class="load-more"> 63 + <button onclick={loadMore} disabled={loadingMore}> 64 + {loadingMore ? 'loading...' : 'load more'} 65 + </button> 66 + </div> 67 + {:else if items.length > 0} 68 + <div class="end-of-feed">you've reached the end</div> 69 + {/if}
+57
app/lib/preferences.ts
··· 1 + import { writable, get } from "svelte/store"; 2 + import { callXrpc } from "$hatk/client"; 3 + 4 + export interface Preferences { 5 + accentColor: string; 6 + font: string; 7 + theme: string; 8 + } 9 + 10 + const DEFAULT: Preferences = { 11 + accentColor: "#4a9eff", 12 + font: "mono", 13 + theme: "dark", 14 + }; 15 + 16 + export const FONTS = [ 17 + { value: "system", label: "system", css: "system-ui, -apple-system, sans-serif" }, 18 + { value: "mono", label: "mono", css: "ui-monospace, SF Mono, Monaco, monospace" }, 19 + { value: "serif", label: "serif", css: "ui-serif, Georgia, serif" }, 20 + { value: "comic", label: "comic", css: "Comic Sans MS, Comic Sans, cursive" }, 21 + ]; 22 + 23 + export const ACCENT_COLORS = [ 24 + "#4a9eff", "#10b981", "#f59e0b", "#ef4444", 25 + "#8b5cf6", "#ec4899", "#06b6d4", "#f97316", 26 + ]; 27 + 28 + export const preferences = writable<Preferences>({ ...DEFAULT }); 29 + 30 + export function loadPreferences(prefs: Record<string, unknown> | null): void { 31 + if (!prefs) return; 32 + const merged = { ...DEFAULT }; 33 + if (typeof prefs.accentColor === "string") merged.accentColor = prefs.accentColor; 34 + if (typeof prefs.font === "string") merged.font = prefs.font; 35 + if (typeof prefs.theme === "string") merged.theme = prefs.theme; 36 + preferences.set(merged); 37 + applyPreferences(merged); 38 + } 39 + 40 + export function applyPreferences(prefs: Preferences): void { 41 + if (typeof document === "undefined") return; 42 + document.documentElement.style.setProperty("--accent", prefs.accentColor); 43 + const font = FONTS.find((f) => f.value === prefs.font) ?? FONTS[1]; 44 + document.documentElement.style.setProperty("--font-family", font.css); 45 + document.documentElement.setAttribute("data-theme", prefs.theme); 46 + localStorage.setItem("theme", prefs.theme); 47 + } 48 + 49 + export async function savePreferences(prefs: Preferences): Promise<void> { 50 + preferences.set(prefs); 51 + applyPreferences(prefs); 52 + await Promise.all([ 53 + callXrpc("dev.hatk.putPreference", { key: "accentColor", value: prefs.accentColor }), 54 + callXrpc("dev.hatk.putPreference", { key: "font", value: prefs.font }), 55 + callXrpc("dev.hatk.putPreference", { key: "theme", value: prefs.theme }), 56 + ]); 57 + }
+19
app/lib/queries.ts
··· 1 + import { queryOptions } from "@tanstack/svelte-query"; 2 + import { callXrpc } from "$hatk/client"; 3 + 4 + type Fetch = typeof fetch; 5 + 6 + export const recentFeedQuery = (limit = 50, f?: Fetch) => 7 + queryOptions({ 8 + queryKey: ["getFeed", "recent"], 9 + queryFn: () => callXrpc("dev.hatk.getFeed", { feed: "recent", limit }, f), 10 + staleTime: 60_000, 11 + }); 12 + 13 + export const actorFeedQuery = (did: string, limit = 50, f?: Fetch) => 14 + queryOptions({ 15 + queryKey: ["getFeed", "actor", did], 16 + queryFn: () => 17 + callXrpc("dev.hatk.getFeed", { feed: "actor", actor: did, limit }, f), 18 + staleTime: 60_000, 19 + });
+3
app/lib/stores.ts
··· 1 + import { writable } from "svelte/store"; 2 + 3 + export const loginModalOpen = writable(false);
+129
app/lib/utils/emoji.ts
··· 1 + export function isCustomEmoji(emoji: string): boolean { 2 + return emoji?.startsWith("custom:") ?? false; 3 + } 4 + 5 + export function customEmojiName(emoji: string): string { 6 + return emoji.slice(7); 7 + } 8 + 9 + export function bufoImageUrl(name: string): string { 10 + return `https://all-the.bufo.zone/${name}.png`; 11 + } 12 + 13 + export function bufoFallbackUrl(name: string): string { 14 + return `https://all-the.bufo.zone/${name}.gif`; 15 + } 16 + 17 + export function parseLinks(text: string): string { 18 + if (!text) return ""; 19 + const escaped = text 20 + .replace(/&/g, "&amp;") 21 + .replace(/</g, "&lt;") 22 + .replace(/>/g, "&gt;") 23 + .replace(/"/g, "&quot;"); 24 + return escaped.replace( 25 + /\[([^\]]+)\]\(([^)]+)\)/g, 26 + (_match: string, linkText: string, url: string) => { 27 + if (url.startsWith("http://") || url.startsWith("https://")) { 28 + return `<a href="${url}" target="_blank" rel="noopener">${linkText}</a>`; 29 + } 30 + return _match; 31 + }, 32 + ); 33 + } 34 + 35 + export function parseStatusUri(uri: string): { did: string; rkey: string } { 36 + const parts = uri.split("/"); 37 + return { did: parts[2], rkey: parts[parts.length - 1] }; 38 + } 39 + 40 + let bufoListCache: string[] | null = null; 41 + 42 + export async function loadBufoList(): Promise<string[]> { 43 + if (bufoListCache) return bufoListCache; 44 + const res = await fetch("/bufos.json"); 45 + if (!res.ok) throw new Error("Failed to load bufos"); 46 + bufoListCache = await res.json(); 47 + return bufoListCache!; 48 + } 49 + 50 + export async function searchBufos( 51 + query: string, 52 + topK = 20, 53 + ): Promise<Array<{ name: string; score: number }>> { 54 + const params = new URLSearchParams({ 55 + query, 56 + top_k: String(topK), 57 + }); 58 + const res = await fetch(`https://find-bufo.fly.dev/api/search?${params}`); 59 + if (!res.ok) throw new Error("bufo search failed"); 60 + const data = await res.json(); 61 + return data.results; 62 + } 63 + 64 + let emojiDataCache: { 65 + emojis: Record<string, string[]>; 66 + categories: Record<string, string[]>; 67 + } | null = null; 68 + 69 + const DEFAULT_FREQUENT = [ 70 + "😊", "👍", "❤️", "😂", "🎉", "🔥", "✨", "💯", 71 + "🚀", "💪", "🙏", "👏", "😴", "🤔", "👀", "💻", 72 + ]; 73 + 74 + export { DEFAULT_FREQUENT }; 75 + 76 + export async function loadEmojiData() { 77 + if (emojiDataCache) return emojiDataCache; 78 + const response = await fetch( 79 + "https://cdn.jsdelivr.net/npm/emoji-datasource@15.1.0/emoji.json", 80 + ); 81 + if (!response.ok) throw new Error("Failed to fetch emoji data"); 82 + const data = await response.json(); 83 + 84 + const emojis: Record<string, string[]> = {}; 85 + const categories: Record<string, string[]> = { 86 + people: [], nature: [], food: [], activity: [], 87 + travel: [], objects: [], symbols: [], flags: [], 88 + }; 89 + const categoryMap: Record<string, string> = { 90 + "Smileys & Emotion": "people", 91 + "People & Body": "people", 92 + "Animals & Nature": "nature", 93 + "Food & Drink": "food", 94 + Activities: "activity", 95 + "Travel & Places": "travel", 96 + Objects: "objects", 97 + Symbols: "symbols", 98 + Flags: "flags", 99 + }; 100 + 101 + for (const emoji of data) { 102 + const char = emoji.unified 103 + .split("-") 104 + .map((u: string) => String.fromCodePoint(parseInt(u, 16))) 105 + .join(""); 106 + const keywords = [ 107 + ...(emoji.short_names || []), 108 + ...(emoji.name ? emoji.name.toLowerCase().split(/[\s_-]+/) : []), 109 + ]; 110 + emojis[char] = keywords; 111 + const cat = categoryMap[emoji.category]; 112 + if (cat && categories[cat]) categories[cat].push(char); 113 + } 114 + 115 + emojiDataCache = { emojis, categories }; 116 + return emojiDataCache; 117 + } 118 + 119 + export function searchEmojis( 120 + query: string, 121 + data: { emojis: Record<string, string[]> }, 122 + ): string[] { 123 + if (!query) return []; 124 + const q = query.toLowerCase(); 125 + return Object.entries(data.emojis) 126 + .filter(([, keywords]) => keywords.some((k) => k.includes(q))) 127 + .map(([char]) => char) 128 + .slice(0, 50); 129 + }
+109
app/lib/utils/time.ts
··· 1 + export function relativeTime(dateStr: string): string { 2 + const date = new Date(dateStr); 3 + const now = new Date(); 4 + const diffMs = now.getTime() - date.getTime(); 5 + const diffMins = Math.floor(diffMs / 60000); 6 + const diffHours = Math.floor(diffMs / 3600000); 7 + const diffDays = Math.floor(diffMs / 86400000); 8 + 9 + if (diffMs < 30000) return "just now"; 10 + if (diffMins < 60) return `${diffMins}m ago`; 11 + if (diffHours < 24) { 12 + const remainingMins = diffMins % 60; 13 + return remainingMins === 0 14 + ? `${diffHours}h ago` 15 + : `${diffHours}h ${remainingMins}m ago`; 16 + } 17 + if (diffDays < 7) { 18 + const remainingHours = diffHours % 24; 19 + return remainingHours === 0 20 + ? `${diffDays}d ago` 21 + : `${diffDays}d ${remainingHours}h ago`; 22 + } 23 + 24 + const timeStr = date 25 + .toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", hour12: true }) 26 + .toLowerCase(); 27 + if (date.getFullYear() === now.getFullYear()) { 28 + return ( 29 + date.toLocaleDateString("en-US", { month: "short", day: "numeric" }) + 30 + ", " + 31 + timeStr 32 + ); 33 + } 34 + return ( 35 + date.toLocaleDateString("en-US", { 36 + month: "short", 37 + day: "numeric", 38 + year: "numeric", 39 + }) + 40 + ", " + 41 + timeStr 42 + ); 43 + } 44 + 45 + export function formatExpiration(dateStr: string): string { 46 + const date = new Date(dateStr); 47 + const now = new Date(); 48 + const diffMs = date.getTime() - now.getTime(); 49 + 50 + if (diffMs <= 0) { 51 + const agoMs = Math.abs(diffMs); 52 + const agoMins = Math.floor(agoMs / 60000); 53 + if (agoMins < 1) return "expired"; 54 + if (agoMins < 60) return `expired ${agoMins}m ago`; 55 + const agoHours = Math.floor(agoMs / 3600000); 56 + if (agoHours < 24) return `expired ${agoHours}h ago`; 57 + const agoDays = Math.floor(agoMs / 86400000); 58 + return `expired ${agoDays}d ago`; 59 + } 60 + 61 + return `clears ${relativeTimeFuture(dateStr)}`; 62 + } 63 + 64 + export function relativeTimeFuture(dateStr: string): string { 65 + const date = new Date(dateStr); 66 + const now = new Date(); 67 + const diffMs = date.getTime() - now.getTime(); 68 + 69 + if (diffMs <= 0) return "now"; 70 + 71 + const diffMins = Math.floor(diffMs / 60000); 72 + const diffHours = Math.floor(diffMs / 3600000); 73 + const diffDays = Math.floor(diffMs / 86400000); 74 + 75 + if (diffMins < 1) return "in less than a minute"; 76 + if (diffMins < 60) return `in ${diffMins}m`; 77 + if (diffHours < 24) { 78 + const remainingMins = diffMins % 60; 79 + return remainingMins === 0 80 + ? `in ${diffHours}h` 81 + : `in ${diffHours}h ${remainingMins}m`; 82 + } 83 + if (diffDays < 7) { 84 + const remainingHours = diffHours % 24; 85 + return remainingHours === 0 86 + ? `in ${diffDays}d` 87 + : `in ${diffDays}d ${remainingHours}h`; 88 + } 89 + 90 + const timeStr = date 91 + .toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", hour12: true }) 92 + .toLowerCase(); 93 + if (date.getFullYear() === now.getFullYear()) { 94 + return ( 95 + date.toLocaleDateString("en-US", { month: "short", day: "numeric" }) + 96 + ", " + 97 + timeStr 98 + ); 99 + } 100 + return ( 101 + date.toLocaleDateString("en-US", { 102 + month: "short", 103 + day: "numeric", 104 + year: "numeric", 105 + }) + 106 + ", " + 107 + timeStr 108 + ); 109 + }
+13
app/routes/+layout.server.ts
··· 1 + import { callXrpc, parseViewer } from "$hatk/client"; 2 + import type { LayoutServerLoad } from "./$types"; 3 + 4 + export const load: LayoutServerLoad = async ({ cookies }) => { 5 + const viewer = await parseViewer(cookies); 6 + 7 + return { 8 + viewer, 9 + preferences: viewer 10 + ? callXrpc("dev.hatk.getPreferences").catch(() => null) 11 + : null, 12 + }; 13 + };
+23
app/routes/+layout.svelte
··· 1 + <script lang="ts"> 2 + import type { Snippet } from 'svelte' 3 + import '../app.css' 4 + import '$lib/auth' 5 + import Header from '$lib/components/Header.svelte' 6 + import { QueryClientProvider } from '@tanstack/svelte-query' 7 + import { loadPreferences } from '$lib/preferences' 8 + 9 + let { data, children }: { data: any; children: Snippet } = $props() 10 + 11 + $effect(() => { 12 + if (data.preferences) { 13 + Promise.resolve(data.preferences).then((prefs: any) => loadPreferences(prefs?.preferences ?? prefs)) 14 + } 15 + }) 16 + </script> 17 + 18 + <QueryClientProvider client={data.queryClient}> 19 + <div class="app-shell"> 20 + <Header /> 21 + {@render children()} 22 + </div> 23 + </QueryClientProvider>
+36
app/routes/+layout.ts
··· 1 + import { browser } from "$app/environment"; 2 + import { QueryClient } from "@tanstack/svelte-query"; 3 + import type { LayoutLoad } from "./$types"; 4 + 5 + let browserClient: QueryClient; 6 + 7 + function getQueryClient() { 8 + if (browser) { 9 + if (!browserClient) { 10 + browserClient = new QueryClient({ 11 + defaultOptions: { 12 + queries: { 13 + staleTime: 60_000, 14 + gcTime: 5 * 60_000, 15 + refetchOnWindowFocus: true, 16 + }, 17 + }, 18 + }); 19 + } 20 + return browserClient; 21 + } 22 + 23 + return new QueryClient({ 24 + defaultOptions: { 25 + queries: { 26 + enabled: false, 27 + staleTime: 60_000, 28 + }, 29 + }, 30 + }); 31 + } 32 + 33 + export const load: LayoutLoad = async ({ data }) => { 34 + const queryClient = getQueryClient(); 35 + return { ...data, queryClient }; 36 + };
+110
app/routes/+page.svelte
··· 1 + <script lang="ts"> 2 + import { page } from '$app/stores' 3 + import { createQuery, useQueryClient } from '@tanstack/svelte-query' 4 + import { actorFeedQuery } from '$lib/queries' 5 + import { callXrpc } from '$hatk/client' 6 + import { isCustomEmoji, customEmojiName, bufoImageUrl, bufoFallbackUrl, parseLinks, parseStatusUri } from '$lib/utils/emoji' 7 + import { relativeTime, formatExpiration } from '$lib/utils/time' 8 + import LoginCard from '$lib/components/LoginCard.svelte' 9 + import CreateStatusForm from '$lib/components/CreateStatusForm.svelte' 10 + import StatusCard from '$lib/components/StatusCard.svelte' 11 + import { Link, Code, X } from 'lucide-svelte' 12 + 13 + const queryClient = useQueryClient() 14 + const viewer = $derived($page.data.viewer) 15 + 16 + const feed = createQuery(() => { 17 + if (!viewer) return { queryKey: ['noop'], queryFn: () => null, enabled: false } 18 + return actorFeedQuery(viewer.did) 19 + }) 20 + 21 + const statuses = $derived((feed.data?.items ?? []) as any[]) 22 + const current = $derived(statuses[0] ?? null) 23 + const history = $derived(statuses.slice(1)) 24 + 25 + let copied = $state(false) 26 + let showEmbed = $state(false) 27 + 28 + function refresh() { 29 + queryClient.invalidateQueries({ queryKey: ['getFeed', 'actor'] }) 30 + } 31 + 32 + async function deleteStatus(rkey: string) { 33 + if (!confirm('Delete this status?')) return 34 + try { 35 + await callXrpc('dev.hatk.deleteRecord', { 36 + collection: 'io.zzstoatzz.status.record', 37 + rkey, 38 + }) 39 + refresh() 40 + } catch (err: any) { 41 + alert('Failed to delete: ' + (err?.message ?? err)) 42 + } 43 + } 44 + 45 + async function shareStatus(uri: string) { 46 + const { did, rkey } = parseStatusUri(uri) 47 + const permalink = `${window.location.origin}/status/${did}/${rkey}` 48 + try { 49 + await navigator.clipboard.writeText(permalink) 50 + copied = true 51 + setTimeout(() => copied = false, 1500) 52 + } catch {} 53 + } 54 + </script> 55 + 56 + {#if !viewer} 57 + <LoginCard /> 58 + {:else} 59 + <div class="profile-card"> 60 + <div class="current-status"> 61 + {#if current} 62 + <span class="big-emoji"> 63 + {#if isCustomEmoji(current.emoji)} 64 + {@const name = customEmojiName(current.emoji)} 65 + <img src={bufoImageUrl(name)} alt={name} onerror={(e) => { (e.currentTarget as HTMLImageElement).src = bufoFallbackUrl(name) }} /> 66 + {:else} 67 + {current.emoji} 68 + {/if} 69 + </span> 70 + <div class="status-info"> 71 + {#if current.text} 72 + <span class="current-text">{@html parseLinks(current.text)}</span> 73 + {/if} 74 + <span class="meta"> 75 + since {relativeTime(current.createdAt)} 76 + {#if current.expires} 77 + &middot; {formatExpiration(current.expires)} 78 + {/if} 79 + </span> 80 + </div> 81 + <div class="current-status-actions"> 82 + <button class="share-btn" onclick={() => shareStatus(current.uri)} title="copy link"> 83 + <Link size={16} /> 84 + </button> 85 + <button class="embed-toggle-btn" onclick={() => showEmbed = !showEmbed} title="get embed code"> 86 + <Code size={16} /> 87 + </button> 88 + <button class="delete-btn" onclick={() => deleteStatus(parseStatusUri(current.uri).rkey)} title="delete"> 89 + <X size={16} /> 90 + </button> 91 + </div> 92 + {:else} 93 + <span class="big-emoji">-</span> 94 + {/if} 95 + </div> 96 + </div> 97 + 98 + <CreateStatusForm currentEmoji={current?.emoji ?? '😊'} oncreated={refresh} /> 99 + 100 + {#if history.length > 0} 101 + <section class="history"> 102 + <h2>history</h2> 103 + <div class="feed-list"> 104 + {#each history as status (status.uri)} 105 + <StatusCard {status} showDelete ondelete={deleteStatus} /> 106 + {/each} 107 + </div> 108 + </section> 109 + {/if} 110 + {/if}
+11
app/routes/+page.ts
··· 1 + import { browser } from "$app/environment"; 2 + import { actorFeedQuery } from "$lib/queries"; 3 + import type { PageLoad } from "./$types"; 4 + 5 + export const load: PageLoad = async ({ parent }) => { 6 + const { queryClient, viewer } = await parent(); 7 + if (viewer) { 8 + const prefetch = queryClient.prefetchQuery(actorFeedQuery(viewer.did)); 9 + if (!browser) await prefetch; 10 + } 11 + };
+22
app/routes/feed/+page.svelte
··· 1 + <script lang="ts"> 2 + import { createQuery } from '@tanstack/svelte-query' 3 + import { recentFeedQuery } from '$lib/queries' 4 + import StatusFeed from '$lib/components/StatusFeed.svelte' 5 + 6 + const feed = createQuery(() => recentFeedQuery()) 7 + </script> 8 + 9 + <svelte:head> 10 + <title>global feed — status</title> 11 + </svelte:head> 12 + 13 + {#if feed.isLoading} 14 + <div class="center">loading...</div> 15 + {:else} 16 + <StatusFeed 17 + feed="recent" 18 + initialItems={feed.data?.items ?? []} 19 + initialCursor={feed.data?.cursor} 20 + showAuthor 21 + /> 22 + {/if}
+9
app/routes/feed/+page.ts
··· 1 + import { browser } from "$app/environment"; 2 + import { recentFeedQuery } from "$lib/queries"; 3 + import type { PageLoad } from "./$types"; 4 + 5 + export const load: PageLoad = async ({ parent, fetch }) => { 6 + const { queryClient } = await parent(); 7 + const prefetch = queryClient.prefetchQuery(recentFeedQuery(50, fetch)); 8 + if (!browser) await prefetch; 9 + };
+8
app/routes/oauth/callback/+page.svelte
··· 1 + <script lang="ts"> 2 + import { onMount } from 'svelte' 3 + import { goto } from '$app/navigation' 4 + 5 + onMount(() => { 6 + goto('/', { replaceState: true }) 7 + }) 8 + </script>
+59
app/routes/profile/[did]/+page.svelte
··· 1 + <script lang="ts"> 2 + import { createQuery } from '@tanstack/svelte-query' 3 + import { actorFeedQuery } from '$lib/queries' 4 + import StatusFeed from '$lib/components/StatusFeed.svelte' 5 + import { isCustomEmoji, customEmojiName, bufoImageUrl, bufoFallbackUrl, parseLinks } from '$lib/utils/emoji' 6 + import { relativeTime, formatExpiration } from '$lib/utils/time' 7 + 8 + let { data } = $props() 9 + 10 + const feed = createQuery(() => actorFeedQuery(data.did)) 11 + const statuses = $derived((feed.data?.items ?? []) as any[]) 12 + const current = $derived(statuses[0] ?? null) 13 + const handle = $derived(current?.handle ?? data.did.slice(0, 18)) 14 + </script> 15 + 16 + <svelte:head> 17 + <title>@{handle} — status</title> 18 + </svelte:head> 19 + 20 + {#if feed.isLoading} 21 + <div class="center">loading...</div> 22 + {:else if !current} 23 + <div class="center">no statuses yet</div> 24 + {:else} 25 + <div class="profile-card"> 26 + <div class="current-status"> 27 + <span class="big-emoji"> 28 + {#if isCustomEmoji(current.emoji)} 29 + {@const name = customEmojiName(current.emoji)} 30 + <img src={bufoImageUrl(name)} alt={name} onerror={(e) => { (e.currentTarget as HTMLImageElement).src = bufoFallbackUrl(name) }} /> 31 + {:else} 32 + {current.emoji} 33 + {/if} 34 + </span> 35 + <div class="status-info"> 36 + {#if current.text} 37 + <span class="current-text">{@html parseLinks(current.text)}</span> 38 + {/if} 39 + <span class="meta"> 40 + {relativeTime(current.createdAt)} 41 + {#if current.expires} 42 + &middot; {formatExpiration(current.expires)} 43 + {/if} 44 + </span> 45 + </div> 46 + </div> 47 + </div> 48 + 49 + {#if statuses.length > 1} 50 + <section class="history"> 51 + <h2>history</h2> 52 + <StatusFeed 53 + feed="actor" 54 + initialItems={statuses.slice(1)} 55 + showAuthor={false} 56 + /> 57 + </section> 58 + {/if} 59 + {/if}
+11
app/routes/profile/[did]/+page.ts
··· 1 + import { browser } from "$app/environment"; 2 + import { actorFeedQuery } from "$lib/queries"; 3 + import type { PageLoad } from "./$types"; 4 + 5 + export const load: PageLoad = async ({ params, parent, fetch }) => { 6 + const did = decodeURIComponent(params.did); 7 + const { queryClient } = await parent(); 8 + const prefetch = queryClient.prefetchQuery(actorFeedQuery(did, 50, fetch)); 9 + if (!browser) await prefetch; 10 + return { did }; 11 + };
+17
app/routes/status/[did]/[rkey]/+page.server.ts
··· 1 + import { callXrpc } from "$hatk/client"; 2 + import type { PageServerLoad } from "./$types"; 3 + 4 + export const load: PageServerLoad = async ({ params }) => { 5 + const did = decodeURIComponent(params.did); 6 + const rkey = decodeURIComponent(params.rkey); 7 + const uri = `at://${did}/io.zzstoatzz.status.record/${rkey}`; 8 + 9 + try { 10 + const res = await callXrpc("dev.hatk.getRecord", { uri }); 11 + if (res.record) { 12 + return { did, rkey, status: res.record }; 13 + } 14 + } catch {} 15 + 16 + return { did, rkey, status: null }; 17 + };
+69
app/routes/status/[did]/[rkey]/+page.svelte
··· 1 + <script lang="ts"> 2 + import { isCustomEmoji, customEmojiName, bufoImageUrl, bufoFallbackUrl, parseLinks } from '$lib/utils/emoji' 3 + import { relativeTime, formatExpiration } from '$lib/utils/time' 4 + 5 + let { data } = $props() 6 + 7 + let status = $derived(data.status) 8 + let emoji = $derived(status?.emoji ?? status?.value?.emoji) 9 + let text = $derived(status?.text ?? status?.value?.text) 10 + let handle = $derived(status?.handle ?? status?.value?.handle ?? data.did) 11 + let createdAt = $derived(status?.createdAt ?? status?.value?.createdAt) 12 + let expires = $derived(status?.expires ?? status?.value?.expires) 13 + 14 + let ogTitle = $derived(`@${handle}'s status`) 15 + let ogDescription = $derived(text || (emoji && isCustomEmoji(emoji) ? customEmojiName(emoji).replace(/-/g, ' ') : emoji) || 'share your status') 16 + let ogUrl = $derived(`https://status.zzstoatzz.io/status/${data.did}/${data.rkey}`) 17 + let ogImage = $derived(emoji && isCustomEmoji(emoji) ? bufoImageUrl(customEmojiName(emoji)) : null) 18 + </script> 19 + 20 + <svelte:head> 21 + <title>{status ? ogTitle : 'status not found'} | status</title> 22 + {#if status} 23 + <meta property="og:type" content="website" /> 24 + <meta property="og:title" content={ogTitle} /> 25 + <meta property="og:description" content={ogDescription} /> 26 + <meta property="og:url" content={ogUrl} /> 27 + <meta property="og:site_name" content="status" /> 28 + {#if ogImage} 29 + <meta property="og:image" content={ogImage} /> 30 + <meta name="twitter:image" content={ogImage} /> 31 + <meta name="twitter:card" content="summary_large_image" /> 32 + {:else} 33 + <meta name="twitter:card" content="summary" /> 34 + {/if} 35 + <meta name="twitter:title" content={ogTitle} /> 36 + <meta name="twitter:description" content={ogDescription} /> 37 + {/if} 38 + </svelte:head> 39 + 40 + {#if status} 41 + <div class="profile-card"> 42 + <div class="current-status"> 43 + <span class="big-emoji"> 44 + {#if emoji && isCustomEmoji(emoji)} 45 + {@const name = customEmojiName(emoji)} 46 + <img src={bufoImageUrl(name)} alt={name} onerror={(e) => { (e.currentTarget as HTMLImageElement).src = bufoFallbackUrl(name) }} /> 47 + {:else} 48 + {emoji ?? '-'} 49 + {/if} 50 + </span> 51 + <div class="status-info"> 52 + {#if text} 53 + <span class="current-text">{@html parseLinks(text)}</span> 54 + {/if} 55 + <span class="meta"> 56 + {relativeTime(createdAt)} 57 + {#if expires} 58 + &middot; {formatExpiration(expires)} 59 + {/if} 60 + </span> 61 + </div> 62 + </div> 63 + </div> 64 + <div class="center"> 65 + <a href="/profile/{data.did}" class="view-profile-link">view all statuses</a> 66 + </div> 67 + {:else} 68 + <div class="center">status not found</div> 69 + {/if}
+46
docker-compose.yml
··· 1 + services: 2 + plc: 3 + build: https://github.com/did-method-plc/did-method-plc.git 4 + ports: 5 + - "2582:2582" 6 + environment: 7 + - DATABASE_URL=postgres://plc:plc@postgres:5432/plc 8 + - PORT=2582 9 + depends_on: 10 + - postgres 11 + healthcheck: 12 + test: ["CMD", "curl", "-f", "http://localhost:2582/_health"] 13 + interval: 5s 14 + timeout: 3s 15 + retries: 5 16 + 17 + pds: 18 + image: ghcr.io/bluesky-social/pds:latest 19 + ports: 20 + - "2583:2583" 21 + environment: 22 + - PDS_HOSTNAME=localhost 23 + - PDS_PORT=2583 24 + - PDS_DID_PLC_URL=http://plc:2582 25 + - PDS_JWT_SECRET=dev-jwt-secret 26 + - PDS_ADMIN_PASSWORD=dev-admin 27 + - PDS_INVITE_REQUIRED=false 28 + - PDS_DEV_MODE=true 29 + depends_on: 30 + plc: 31 + condition: service_healthy 32 + volumes: 33 + - pds_data:/pds 34 + 35 + postgres: 36 + image: postgres:16-alpine 37 + environment: 38 + - POSTGRES_USER=plc 39 + - POSTGRES_PASSWORD=plc 40 + - POSTGRES_DB=plc 41 + volumes: 42 + - plc_data:/var/lib/postgresql/data 43 + 44 + volumes: 45 + pds_data: 46 + plc_data:
+11 -11
fly.toml
··· 1 - # fly.toml app configuration file generated for zzstoatzz-quickslice-status on 2025-12-13T16:42:55-06:00 2 - # 3 - # See https://fly.io/docs/reference/configuration/ for information about how to use this file. 4 - # 5 - 6 1 app = 'zzstoatzz-quickslice-status' 7 2 primary_region = 'ewr' 8 3 9 4 [build] 10 - image = 'ghcr.io/bigmoves/quickslice:latest' 5 + dockerfile = "Dockerfile" 11 6 12 7 [env] 13 - DATABASE_URL = 'sqlite:/data/quickslice.db' 14 - HOST = '0.0.0.0' 15 - PORT = '8080' 16 - EXTERNAL_BASE_URL = 'https://zzstoatzz-quickslice-status.fly.dev' 8 + NODE_ENV = 'production' 9 + EXTERNAL_BASE_URL = 'https://status.zzstoatzz.io' 17 10 18 11 [[mounts]] 19 12 source = 'quickslice_data' 20 13 destination = '/data' 21 14 22 15 [http_service] 23 - internal_port = 8080 16 + internal_port = 3000 24 17 force_https = true 25 18 auto_stop_machines = 'stop' 26 19 auto_start_machines = true 27 20 min_machines_running = 1 21 + 22 + [checks.health] 23 + type = "http" 24 + port = 3000 25 + path = "/_health" 26 + interval = "30s" 27 + timeout = "5s" 28 28 29 29 [[vm]] 30 30 memory = '1gb'
+49
hatk.config.ts
··· 1 + import { defineConfig } from "@hatk/hatk/config"; 2 + 3 + const isProd = process.env.NODE_ENV === "production"; 4 + 5 + const scopes = [ 6 + "atproto", 7 + "repo:io.zzstoatzz.status.record", 8 + "repo:io.zzstoatzz.status.preferences", 9 + ].join(" "); 10 + 11 + export default defineConfig({ 12 + relay: isProd ? "wss://bsky.network" : "ws://localhost:2583", 13 + plc: isProd ? "https://plc.directory" : "http://localhost:2582", 14 + port: 3000, 15 + databaseEngine: "sqlite", 16 + database: isProd ? "/data/status.db" : "data/status.db", 17 + backfill: { 18 + signalCollections: ["io.zzstoatzz.status.record"], 19 + fullNetwork: false, 20 + parallelism: 2, 21 + }, 22 + oauth: { 23 + issuer: isProd 24 + ? "https://status.zzstoatzz.io" 25 + : undefined, 26 + scopes: scopes.split(" "), 27 + clients: [ 28 + ...(isProd 29 + ? [ 30 + { 31 + client_id: 32 + "https://status.zzstoatzz.io/oauth-client-metadata.json", 33 + client_name: "status", 34 + scope: scopes, 35 + redirect_uris: [ 36 + "https://status.zzstoatzz.io/oauth/callback", 37 + ], 38 + }, 39 + ] 40 + : []), 41 + { 42 + client_id: "http://127.0.0.1:3000/oauth-client-metadata.json", 43 + client_name: "status", 44 + scope: scopes, 45 + redirect_uris: ["http://127.0.0.1:3000/oauth/callback"], 46 + }, 47 + ], 48 + }, 49 + });
+41
lexicons/com/atproto/label/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.label.defs", 4 + "defs": { 5 + "label": { 6 + "type": "object", 7 + "description": "Metadata tag on an atproto resource (eg, repo or record).", 8 + "required": ["src", "uri", "val", "cts"], 9 + "properties": { 10 + "ver": { "type": "integer" }, 11 + "src": { "type": "string", "format": "did" }, 12 + "uri": { "type": "string", "format": "uri" }, 13 + "cid": { "type": "string", "format": "cid" }, 14 + "val": { "type": "string", "maxLength": 128 }, 15 + "neg": { "type": "boolean" }, 16 + "cts": { "type": "string", "format": "datetime" }, 17 + "exp": { "type": "string", "format": "datetime" }, 18 + "sig": { "type": "bytes" } 19 + } 20 + }, 21 + "selfLabels": { 22 + "type": "object", 23 + "description": "Metadata tags on an atproto record, published by the author within the record.", 24 + "required": ["values"], 25 + "properties": { 26 + "values": { 27 + "type": "array", 28 + "items": { "type": "ref", "ref": "#selfLabel" }, 29 + "maxLength": 10 30 + } 31 + } 32 + }, 33 + "selfLabel": { 34 + "type": "object", 35 + "required": ["val"], 36 + "properties": { 37 + "val": { "type": "string", "maxLength": 128 } 38 + } 39 + } 40 + } 41 + }
+25
lexicons/com/atproto/moderation/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.moderation.defs", 4 + "defs": { 5 + "reasonType": { 6 + "type": "string", 7 + "knownValues": [ 8 + "com.atproto.moderation.defs#reasonSpam", 9 + "com.atproto.moderation.defs#reasonViolation", 10 + "com.atproto.moderation.defs#reasonMisleading", 11 + "com.atproto.moderation.defs#reasonSexual", 12 + "com.atproto.moderation.defs#reasonRude", 13 + "com.atproto.moderation.defs#reasonOther", 14 + "com.atproto.moderation.defs#reasonAppeal" 15 + ] 16 + }, 17 + "reasonSpam": { "type": "token" }, 18 + "reasonViolation": { "type": "token" }, 19 + "reasonMisleading": { "type": "token" }, 20 + "reasonSexual": { "type": "token" }, 21 + "reasonRude": { "type": "token" }, 22 + "reasonOther": { "type": "token" }, 23 + "reasonAppeal": { "type": "token" } 24 + } 25 + }
+15
lexicons/com/atproto/repo/strongRef.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.repo.strongRef", 4 + "description": "A URI with a content-hash fingerprint.", 5 + "defs": { 6 + "main": { 7 + "type": "object", 8 + "required": ["uri", "cid"], 9 + "properties": { 10 + "uri": { "type": "string", "format": "at-uri" }, 11 + "cid": { "type": "string", "format": "cid" } 12 + } 13 + } 14 + } 15 + }
+32
lexicons/dev/hatk/createRecord.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "dev.hatk.createRecord", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Create a record via the user's PDS.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["collection", "repo", "record"], 13 + "properties": { 14 + "collection": { "type": "string" }, 15 + "repo": { "type": "string", "format": "did" }, 16 + "record": { "type": "unknown" } 17 + } 18 + } 19 + }, 20 + "output": { 21 + "encoding": "application/json", 22 + "schema": { 23 + "type": "object", 24 + "properties": { 25 + "uri": { "type": "string", "format": "at-uri" }, 26 + "cid": { "type": "string", "format": "cid" } 27 + } 28 + } 29 + } 30 + } 31 + } 32 + }
+48
lexicons/dev/hatk/createReport.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "dev.hatk.createReport", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Report an account or record for moderation review.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["subject", "label"], 13 + "properties": { 14 + "subject": { 15 + "type": "union", 16 + "description": "The account or record being reported.", 17 + "refs": ["#repoRef", "com.atproto.repo.strongRef"] 18 + }, 19 + "label": { "type": "string" }, 20 + "reason": { "type": "string", "maxLength": 2000 } 21 + } 22 + } 23 + }, 24 + "output": { 25 + "encoding": "application/json", 26 + "schema": { 27 + "type": "object", 28 + "required": ["id", "subject", "label", "reportedBy", "createdAt"], 29 + "properties": { 30 + "id": { "type": "integer" }, 31 + "subject": { "type": "unknown" }, 32 + "label": { "type": "string" }, 33 + "reason": { "type": "string" }, 34 + "reportedBy": { "type": "string", "format": "did" }, 35 + "createdAt": { "type": "string", "format": "datetime" } 36 + } 37 + } 38 + } 39 + }, 40 + "repoRef": { 41 + "type": "object", 42 + "required": ["did"], 43 + "properties": { 44 + "did": { "type": "string", "format": "did" } 45 + } 46 + } 47 + } 48 + }
+28
lexicons/dev/hatk/deleteRecord.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "dev.hatk.deleteRecord", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Delete a record via the user's PDS.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["collection", "rkey"], 13 + "properties": { 14 + "collection": { "type": "string" }, 15 + "rkey": { "type": "string" } 16 + } 17 + } 18 + }, 19 + "output": { 20 + "encoding": "application/json", 21 + "schema": { 22 + "type": "object", 23 + "properties": {} 24 + } 25 + } 26 + } 27 + } 28 + }
+41
lexicons/dev/hatk/describeCollections.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "dev.hatk.describeCollections", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "List indexed collections and their schemas.", 8 + "output": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "properties": { 13 + "collections": { 14 + "type": "array", 15 + "items": { 16 + "type": "object", 17 + "required": ["collection"], 18 + "properties": { 19 + "collection": { "type": "string" }, 20 + "columns": { 21 + "type": "array", 22 + "items": { 23 + "type": "object", 24 + "required": ["name", "originalName", "type", "required"], 25 + "properties": { 26 + "name": { "type": "string" }, 27 + "originalName": { "type": "string" }, 28 + "type": { "type": "string" }, 29 + "required": { "type": "boolean" } 30 + } 31 + } 32 + } 33 + } 34 + } 35 + } 36 + } 37 + } 38 + } 39 + } 40 + } 41 + }
+29
lexicons/dev/hatk/describeFeeds.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "dev.hatk.describeFeeds", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "List available feeds.", 8 + "output": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "properties": { 13 + "feeds": { 14 + "type": "array", 15 + "items": { 16 + "type": "object", 17 + "required": ["name", "label"], 18 + "properties": { 19 + "name": { "type": "string" }, 20 + "label": { "type": "string" } 21 + } 22 + } 23 + } 24 + } 25 + } 26 + } 27 + } 28 + } 29 + }
+45
lexicons/dev/hatk/describeLabels.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "dev.hatk.describeLabels", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "List available label definitions.", 8 + "output": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "properties": { 13 + "definitions": { 14 + "type": "array", 15 + "items": { "type": "ref", "ref": "#labelDefinition" } 16 + } 17 + } 18 + } 19 + } 20 + }, 21 + "labelDefinition": { 22 + "type": "object", 23 + "required": ["identifier", "severity", "blurs", "defaultSetting"], 24 + "properties": { 25 + "identifier": { "type": "string" }, 26 + "severity": { "type": "string" }, 27 + "blurs": { "type": "string" }, 28 + "defaultSetting": { "type": "string" }, 29 + "locales": { 30 + "type": "array", 31 + "items": { "type": "ref", "ref": "#labelLocale" } 32 + } 33 + } 34 + }, 35 + "labelLocale": { 36 + "type": "object", 37 + "required": ["lang", "name", "description"], 38 + "properties": { 39 + "lang": { "type": "string" }, 40 + "name": { "type": "string" }, 41 + "description": { "type": "string" } 42 + } 43 + } 44 + } 45 + }
+33
lexicons/dev/hatk/getFeed.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "dev.hatk.getFeed", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Retrieve a named feed of items.", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["feed"], 11 + "properties": { 12 + "feed": { "type": "string", "description": "Feed name" }, 13 + "limit": { "type": "integer", "minimum": 1, "maximum": 100, "default": 30 }, 14 + "cursor": { "type": "string" }, 15 + "actor": { "type": "string", "format": "did", "description": "Filter by actor DID" } 16 + } 17 + }, 18 + "output": { 19 + "encoding": "application/json", 20 + "schema": { 21 + "type": "object", 22 + "properties": { 23 + "items": { 24 + "type": "array", 25 + "items": { "type": "ref", "ref": "io.zzstoatzz.status.defs#statusView" } 26 + }, 27 + "cursor": { "type": "string" } 28 + } 29 + } 30 + } 31 + } 32 + } 33 + }
+19
lexicons/dev/hatk/getPreferences.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "dev.hatk.getPreferences", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get all preferences for the authenticated user.", 8 + "output": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "properties": { 13 + "preferences": { "type": "unknown" } 14 + } 15 + } 16 + } 17 + } 18 + } 19 + }
+26
lexicons/dev/hatk/getRecord.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "dev.hatk.getRecord", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Fetch a single record by AT URI.", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["uri"], 11 + "properties": { 12 + "uri": { "type": "string", "format": "at-uri" } 13 + } 14 + }, 15 + "output": { 16 + "encoding": "application/json", 17 + "schema": { 18 + "type": "object", 19 + "properties": { 20 + "record": { "type": "unknown" } 21 + } 22 + } 23 + } 24 + } 25 + } 26 + }
+31
lexicons/dev/hatk/getRecords.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "dev.hatk.getRecords", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "List records from a collection with optional filters.", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["collection"], 11 + "properties": { 12 + "collection": { "type": "string" }, 13 + "limit": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 }, 14 + "cursor": { "type": "string" }, 15 + "sort": { "type": "string" }, 16 + "order": { "type": "string" } 17 + } 18 + }, 19 + "output": { 20 + "encoding": "application/json", 21 + "schema": { 22 + "type": "object", 23 + "properties": { 24 + "items": { "type": "array", "items": { "type": "unknown" } }, 25 + "cursor": { "type": "string" } 26 + } 27 + } 28 + } 29 + } 30 + } 31 + }
+28
lexicons/dev/hatk/putPreference.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "dev.hatk.putPreference", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Set a single preference by key.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["key", "value"], 13 + "properties": { 14 + "key": { "type": "string" }, 15 + "value": { "type": "unknown" } 16 + } 17 + } 18 + }, 19 + "output": { 20 + "encoding": "application/json", 21 + "schema": { 22 + "type": "object", 23 + "properties": {} 24 + } 25 + } 26 + } 27 + } 28 + }
+33
lexicons/dev/hatk/putRecord.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "dev.hatk.putRecord", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Create or update a record via the user's PDS.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["collection", "rkey", "record"], 13 + "properties": { 14 + "collection": { "type": "string" }, 15 + "rkey": { "type": "string" }, 16 + "record": { "type": "unknown" }, 17 + "repo": { "type": "string", "format": "did" } 18 + } 19 + } 20 + }, 21 + "output": { 22 + "encoding": "application/json", 23 + "schema": { 24 + "type": "object", 25 + "properties": { 26 + "uri": { "type": "string", "format": "at-uri" }, 27 + "cid": { "type": "string", "format": "cid" } 28 + } 29 + } 30 + } 31 + } 32 + } 33 + }
+31
lexicons/dev/hatk/searchRecords.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "dev.hatk.searchRecords", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Full-text search across a collection.", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["collection", "q"], 11 + "properties": { 12 + "collection": { "type": "string" }, 13 + "q": { "type": "string", "description": "Search query" }, 14 + "limit": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 }, 15 + "cursor": { "type": "string" }, 16 + "fuzzy": { "type": "boolean", "default": true } 17 + } 18 + }, 19 + "output": { 20 + "encoding": "application/json", 21 + "schema": { 22 + "type": "object", 23 + "properties": { 24 + "items": { "type": "array", "items": { "type": "unknown" } }, 25 + "cursor": { "type": "string" } 26 + } 27 + } 28 + } 29 + } 30 + } 31 + }
+23
lexicons/dev/hatk/uploadBlob.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "dev.hatk.uploadBlob", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Upload a blob via the user's PDS.", 8 + "input": { 9 + "encoding": "*/*" 10 + }, 11 + "output": { 12 + "encoding": "application/json", 13 + "schema": { 14 + "type": "object", 15 + "required": ["blob"], 16 + "properties": { 17 + "blob": { "type": "blob" } 18 + } 19 + } 20 + } 21 + } 22 + } 23 + }
+22
lexicons/io/zzstoatzz/status/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "io.zzstoatzz.status.defs", 4 + "defs": { 5 + "statusView": { 6 + "type": "object", 7 + "required": ["uri", "cid", "did", "handle", "emoji", "createdAt", "indexedAt", "expired"], 8 + "properties": { 9 + "uri": { "type": "string", "format": "at-uri" }, 10 + "cid": { "type": "string", "format": "cid" }, 11 + "did": { "type": "string", "format": "did" }, 12 + "handle": { "type": "string" }, 13 + "emoji": { "type": "string" }, 14 + "text": { "type": "string" }, 15 + "expires": { "type": "string", "format": "datetime" }, 16 + "createdAt": { "type": "string", "format": "datetime" }, 17 + "indexedAt": { "type": "string", "format": "datetime" }, 18 + "expired": { "type": "boolean" } 19 + } 20 + } 21 + } 22 + }
lexicons/preferences.json lexicons/io/zzstoatzz/status/preferences.json
lexicons/status.json lexicons/io/zzstoatzz/status/record.json
-235
notes/quickslice-migration.md
··· 1 - # migrating to quickslice: a status app rewrite 2 - 3 - ## what we built 4 - 5 - a bluesky status app that lets users set emoji statuses (like slack status) stored in their AT protocol repository. the app has two parts: 6 - 7 - - **backend**: [quickslice](https://github.com/bigmoves/quickslice) on fly.io - handles OAuth, GraphQL API, and jetstream ingestion 8 - - **frontend**: vanilla JS SPA on cloudflare pages 9 - 10 - live at https://status.zzstoatzz.io 11 - 12 - ## why quickslice 13 - 14 - the original implementation was a custom rust backend using atrium-rs. it worked, but maintaining OAuth, jetstream ingestion, and all the AT protocol plumbing was a lot. quickslice handles all of that out of the box: 15 - 16 - - OAuth 2.0 with PKCE + DPoP (the hard part of AT protocol) 17 - - GraphQL API auto-generated from your lexicons 18 - - jetstream consumer for real-time firehose data 19 - - admin UI for managing OAuth clients 20 - 21 - ## the migration 22 - 23 - ### 1. lexicon design 24 - 25 - quickslice ingests data based on lexicons you define. we have two: 26 - 27 - **io.zzstoatzz.status.record** - the actual status 28 - ```json 29 - { 30 - "emoji": "🔥", 31 - "text": "shipping code", 32 - "createdAt": "2025-12-13T12:00:00Z" 33 - } 34 - ``` 35 - 36 - **io.zzstoatzz.status.preferences** - user display preferences 37 - ```json 38 - { 39 - "accentColor": "#4a9eff", 40 - "theme": "dark" 41 - } 42 - ``` 43 - 44 - ### 2. frontend architecture 45 - 46 - since quickslice serves its own admin UI at the root path, we couldn't bundle our frontend into the same container. this led to a clean separation: 47 - 48 - - quickslice backend on fly.io (`zzstoatzz-quickslice-status.fly.dev`) 49 - - static frontend on cloudflare pages (`status.zzstoatzz.io`) 50 - 51 - the frontend uses the `quickslice-client-js` library for OAuth: 52 - ```html 53 - <script src="https://cdn.jsdelivr.net/gh/bigmoves/quickslice@v0.17.3/quickslice-client-js/dist/quickslice-client.min.js"></script> 54 - ``` 55 - 56 - ### 3. the UI 57 - 58 - since quickslice serves its own admin UI at the root path, we host our frontend separately on cloudflare pages. the frontend is vanilla JS - no framework, just a single `app.js` file. 59 - 60 - **OAuth with quickslice-client-js** 61 - 62 - the `quickslice-client-js` library handles the OAuth flow in the browser: 63 - 64 - ```javascript 65 - const client = await QuicksliceClient.create({ 66 - server: 'https://your-app.fly.dev', // your quickslice instance 67 - clientId: 'client_xxx', // from quickslice admin UI 68 - redirectUri: window.location.origin + '/', // where OAuth redirects back 69 - }); 70 - 71 - // start login 72 - await client.signIn(handle); 73 - 74 - // after redirect, client.agent is authenticated 75 - const { data } = await client.agent.getProfile({ actor: client.agent.session.did }); 76 - ``` 77 - 78 - the `clientId` comes from registering an OAuth client in the quickslice admin UI. the redirect URI should match what you registered. 79 - 80 - **GraphQL queries** 81 - 82 - quickslice auto-generates a GraphQL API from your lexicons. querying status records looks like: 83 - 84 - ```javascript 85 - const response = await fetch(`https://your-app.fly.dev/api/graphql`, { 86 - method: 'POST', 87 - headers: { 'Content-Type': 'application/json' }, 88 - body: JSON.stringify({ 89 - query: ` 90 - query GetStatuses($did: String!) { 91 - ioZzstoatzzStatusRecords( 92 - where: { did: { eq: $did } } 93 - orderBy: { createdAt: DESC } 94 - first: 50 95 - ) { 96 - nodes { uri did emoji text createdAt } 97 - } 98 - } 99 - `, 100 - variables: { did } 101 - }) 102 - }); 103 - ``` 104 - 105 - the query name is auto-generated from your lexicon ID - dots become camelCase (e.g., `io.zzstoatzz.status.record` → `ioZzstoatzzStatusRecords`). 106 - 107 - no need to write resolvers or schema - it's all generated from the lexicon definitions. 108 - 109 - ## problems we hit 110 - 111 - ### the `sub` claim fix 112 - 113 - the biggest issue: after OAuth login, the app would redirect loop infinitely. the AT protocol SDK needs a `sub` claim in the OAuth token response to identify the user, but quickslice v0.17.2 didn't include it. 114 - 115 - the fix was in v0.17.3 (commit `0b2d54a`), but `ghcr.io/bigmoves/quickslice:latest` still pointed to v0.17.2. we had to build from source: 116 - 117 - ```dockerfile 118 - # Clone quickslice at the v0.17.3 tag (includes sub claim fix) 119 - RUN git clone --depth 1 --branch v0.17.3 https://github.com/bigmoves/quickslice.git /build 120 - ``` 121 - 122 - ### secrets configuration 123 - 124 - quickslice needs two secrets for OAuth to work: 125 - 126 - ```bash 127 - fly secrets set SECRET_KEY_BASE="$(openssl rand -base64 64 | tr -d '\n')" 128 - fly secrets set OAUTH_SIGNING_KEY="$(goat key generate -t p256 | tail -1)" 129 - ``` 130 - 131 - the `OAUTH_SIGNING_KEY` must be just the multibase key (starts with `z`), not the full output from goat. 132 - 133 - ### EXTERNAL_BASE_URL 134 - 135 - without this, quickslice uses `0.0.0.0:8080` in its OAuth client metadata, which breaks the flow. set it to your public URL: 136 - 137 - ```toml 138 - [env] 139 - EXTERNAL_BASE_URL = 'https://your-app.fly.dev' 140 - ``` 141 - 142 - ### PDS caching 143 - 144 - when debugging OAuth issues, be aware that your PDS caches OAuth client metadata. if you fix something on the server, the PDS might still have the old metadata cached. this caused some confusion during debugging. 145 - 146 - ## deployment architecture 147 - 148 - ``` 149 - ┌─────────────────────────────────────────────────────────┐ 150 - │ cloudflare pages │ 151 - │ your-frontend.com │ 152 - │ │ 153 - │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ 154 - │ │ index.html │ │ app.js │ │ styles.css │ │ 155 - │ └─────────────┘ └─────────────┘ └─────────────┘ │ 156 - └─────────────────────────────────────────────────────────┘ 157 - 158 - │ GraphQL + OAuth 159 - 160 - ┌─────────────────────────────────────────────────────────┐ 161 - │ fly.io │ 162 - │ your-app.fly.dev │ 163 - │ │ 164 - │ ┌─────────────────────────────────────────────────┐ │ 165 - │ │ quickslice │ │ 166 - │ │ • OAuth server (PKCE + DPoP) │ │ 167 - │ │ • GraphQL API (auto-generated from lexicons) │ │ 168 - │ │ • Jetstream consumer │ │ 169 - │ │ • SQLite database │ │ 170 - │ └─────────────────────────────────────────────────┘ │ 171 - └─────────────────────────────────────────────────────────┘ 172 - 173 - │ Jetstream 174 - 175 - ┌─────────────────────────────────────────────────────────┐ 176 - │ AT Protocol │ 177 - │ (bluesky PDS, jetstream firehose) │ 178 - └─────────────────────────────────────────────────────────┘ 179 - ``` 180 - 181 - ## what quickslice eliminated 182 - 183 - the rust backend was ~2000 lines of code handling: 184 - 185 - - OAuth server implementation (PKCE + DPoP) 186 - - jetstream consumer for firehose ingestion 187 - - custom API endpoints for reading/writing statuses 188 - - session management 189 - - database queries 190 - 191 - with quickslice, all of that is replaced by: 192 - 193 - - a Dockerfile that builds quickslice from source 194 - - a fly.toml with env vars 195 - - two secrets 196 - 197 - the frontend is still custom (~1200 lines), but the backend complexity is gone. 198 - 199 - ## deployment checklist 200 - 201 - when deploying quickslice: 202 - 203 - ```bash 204 - # 1. set required secrets 205 - fly secrets set SECRET_KEY_BASE="$(openssl rand -base64 64 | tr -d '\n')" 206 - fly secrets set OAUTH_SIGNING_KEY="$(goat key generate -t p256 | tail -1)" 207 - 208 - # 2. deploy (builds from source, takes ~3 min) 209 - fly deploy 210 - 211 - # 3. in quickslice admin UI: 212 - # - set domain authority (e.g., io.zzstoatzz) 213 - # - add supported lexicons 214 - # - register OAuth client with redirect URI 215 - ``` 216 - 217 - ## key takeaways 218 - 219 - 1. **quickslice eliminates the hard parts** - OAuth and jetstream are notoriously tricky. quickslice handles them so you can focus on your app logic. 220 - 221 - 2. **separate frontend and backend** - quickslice serves its own admin UI, so host your frontend elsewhere. cloudflare pages is free and fast. 222 - 223 - 3. **pin your dependencies** - we got bit by `:latest` not being latest. pin to specific versions/tags. 224 - 225 - 4. **check the image version** - `fly image show` tells you exactly what's deployed. don't assume. 226 - 227 - 5. **GraphQL is your API** - quickslice auto-generates a GraphQL API from your lexicons. no need to write endpoints. 228 - 229 - 6. **the sub claim matters** - AT protocol OAuth needs the `sub` claim in token responses. this was the root cause of our redirect loop. 230 - 231 - ## resources 232 - 233 - - [quickslice](https://github.com/bigmoves/quickslice) - the framework 234 - - [AT protocol OAuth](https://atproto.com/specs/oauth) - the spec 235 - - [quickslice-client-js](https://github.com/bigmoves/quickslice/tree/main/quickslice-client-js) - frontend OAuth helper
+3997
package-lock.json
··· 1 + { 2 + "name": "zzstoatzz-status", 3 + "lockfileVersion": 3, 4 + "requires": true, 5 + "packages": { 6 + "": { 7 + "name": "zzstoatzz-status", 8 + "dependencies": { 9 + "@hatk/hatk": "^0.0.1-alpha.44", 10 + "@sveltejs/adapter-node": "^5.5.4", 11 + "@sveltejs/kit": "^2.55.0", 12 + "@tanstack/svelte-query": "^6.1.0", 13 + "lucide-svelte": "^0.576.0" 14 + }, 15 + "devDependencies": { 16 + "@voidzero-dev/vite-plus-core": "^0.1.11", 17 + "svelte": "^5", 18 + "svelte-check": "^4", 19 + "typescript": "^5", 20 + "vite-plus": "^0.1.11" 21 + } 22 + }, 23 + "node_modules/@bigmoves/lexicon": { 24 + "version": "0.2.2", 25 + "resolved": "https://registry.npmjs.org/@bigmoves/lexicon/-/lexicon-0.2.2.tgz", 26 + "integrity": "sha512-2g6wR6xMvtAyYtkg6146eA0rkYvtYU0Ov6UfPDvyVt0rB8qkAsehUEO5rgNtVhzjuwuS2H9BIwx6bDZc943Qtg==", 27 + "license": "MIT" 28 + }, 29 + "node_modules/@duckdb/node-api": { 30 + "version": "1.4.4-r.3", 31 + "resolved": "https://registry.npmjs.org/@duckdb/node-api/-/node-api-1.4.4-r.3.tgz", 32 + "integrity": "sha512-zsTihV7WTGp+n6nI0rNY+cNamKiN9lj0YTS4jrjBMd5ESJpDIJNJOB3o0xBbBghalgGu6PZfZOIcNofUbUHAYg==", 33 + "license": "MIT", 34 + "dependencies": { 35 + "@duckdb/node-bindings": "1.4.4-r.3" 36 + } 37 + }, 38 + "node_modules/@duckdb/node-bindings": { 39 + "version": "1.4.4-r.3", 40 + "resolved": "https://registry.npmjs.org/@duckdb/node-bindings/-/node-bindings-1.4.4-r.3.tgz", 41 + "integrity": "sha512-ucvhCF3IK60BzxTFwYTjyQao9zavEwXJ1JdGk70UEXa85JQlos3pJslEG3lIczhdtSPu0aum+hv67aBmpH21cw==", 42 + "license": "MIT", 43 + "optionalDependencies": { 44 + "@duckdb/node-bindings-darwin-arm64": "1.4.4-r.3", 45 + "@duckdb/node-bindings-darwin-x64": "1.4.4-r.3", 46 + "@duckdb/node-bindings-linux-arm64": "1.4.4-r.3", 47 + "@duckdb/node-bindings-linux-x64": "1.4.4-r.3", 48 + "@duckdb/node-bindings-win32-arm64": "1.4.4-r.3", 49 + "@duckdb/node-bindings-win32-x64": "1.4.4-r.3" 50 + } 51 + }, 52 + "node_modules/@duckdb/node-bindings-darwin-arm64": { 53 + "version": "1.4.4-r.3", 54 + "resolved": "https://registry.npmjs.org/@duckdb/node-bindings-darwin-arm64/-/node-bindings-darwin-arm64-1.4.4-r.3.tgz", 55 + "integrity": "sha512-8tfUVcbkNhpCezUNFlrMOwNZL8p1XfE9jxhjlPine1zKM9mNNtKW1pwLXId6MZ/BDMpVfQmlxnu97NYtPmpgRw==", 56 + "cpu": [ 57 + "arm64" 58 + ], 59 + "license": "MIT", 60 + "optional": true, 61 + "os": [ 62 + "darwin" 63 + ] 64 + }, 65 + "node_modules/@duckdb/node-bindings-darwin-x64": { 66 + "version": "1.4.4-r.3", 67 + "resolved": "https://registry.npmjs.org/@duckdb/node-bindings-darwin-x64/-/node-bindings-darwin-x64-1.4.4-r.3.tgz", 68 + "integrity": "sha512-7pqSajMX/qb2vRnCMxOaM/cNkUX4kzHKAfdfQT+7dglvuIND2xy8+7EQ4+wHzk+gkPJsWUHZF0l0n6X6tyKFEA==", 69 + "cpu": [ 70 + "x64" 71 + ], 72 + "license": "MIT", 73 + "optional": true, 74 + "os": [ 75 + "darwin" 76 + ] 77 + }, 78 + "node_modules/@duckdb/node-bindings-linux-arm64": { 79 + "version": "1.4.4-r.3", 80 + "resolved": "https://registry.npmjs.org/@duckdb/node-bindings-linux-arm64/-/node-bindings-linux-arm64-1.4.4-r.3.tgz", 81 + "integrity": "sha512-qF2/WhB+i+8is60RrpTY/5ZnywIUayuBiVXSuQ4rrRn1m1gvfKqPvjySbiDjYgQo/6OXu/fax0KC1gXnNWoLEA==", 82 + "cpu": [ 83 + "arm64" 84 + ], 85 + "license": "MIT", 86 + "optional": true, 87 + "os": [ 88 + "linux" 89 + ] 90 + }, 91 + "node_modules/@duckdb/node-bindings-linux-x64": { 92 + "version": "1.4.4-r.3", 93 + "resolved": "https://registry.npmjs.org/@duckdb/node-bindings-linux-x64/-/node-bindings-linux-x64-1.4.4-r.3.tgz", 94 + "integrity": "sha512-Z59zpUIcZilHBmcZpmwfFwi6/OAnKlVQCE9/NGgZCFSqYeb+3ij5HQy26hp10GIiv3HxUQGId8HVs92x2jvckQ==", 95 + "cpu": [ 96 + "x64" 97 + ], 98 + "license": "MIT", 99 + "optional": true, 100 + "os": [ 101 + "linux" 102 + ] 103 + }, 104 + "node_modules/@duckdb/node-bindings-win32-arm64": { 105 + "version": "1.4.4-r.3", 106 + "resolved": "https://registry.npmjs.org/@duckdb/node-bindings-win32-arm64/-/node-bindings-win32-arm64-1.4.4-r.3.tgz", 107 + "integrity": "sha512-ejjMqDm2UbGIKq+4wBE27cqcZjxBzLnc1dniEE/xs9CVs4iYsiFP1BJLDAh4188P9pqgN9+VR3O69QfoZQ8rqQ==", 108 + "cpu": [ 109 + "arm64" 110 + ], 111 + "license": "MIT", 112 + "optional": true, 113 + "os": [ 114 + "win32" 115 + ] 116 + }, 117 + "node_modules/@duckdb/node-bindings-win32-x64": { 118 + "version": "1.4.4-r.3", 119 + "resolved": "https://registry.npmjs.org/@duckdb/node-bindings-win32-x64/-/node-bindings-win32-x64-1.4.4-r.3.tgz", 120 + "integrity": "sha512-5CzkI3jPkwALPYS66/vj/3wgsICxALkqRdOEJc8wHqPPlpM/Ez7jFAovI7+S8JrTaxUANkiIAcCtYYAJPElbRQ==", 121 + "cpu": [ 122 + "x64" 123 + ], 124 + "license": "MIT", 125 + "optional": true, 126 + "os": [ 127 + "win32" 128 + ] 129 + }, 130 + "node_modules/@hatk/hatk": { 131 + "version": "0.0.1-alpha.45", 132 + "resolved": "https://registry.npmjs.org/@hatk/hatk/-/hatk-0.0.1-alpha.45.tgz", 133 + "integrity": "sha512-y1+JB3uPr1tyUfu+XW4UXV4QuSJ+To6j+RyuINHlqjJ6j5OxaUZVVy5ZkzoEqQjqz4+H/RK1DrmB1KyYbjVBGQ==", 134 + "license": "MIT", 135 + "dependencies": { 136 + "@bigmoves/lexicon": "^0.2.2", 137 + "@duckdb/node-api": "^1.4.4-r.1", 138 + "@hatk/oauth-client": "*", 139 + "@resvg/resvg-js": "^2.6.2", 140 + "better-sqlite3": "^12.6.2", 141 + "satori": "^0.19.2", 142 + "vitest": "^4", 143 + "yaml": "^2.7.0" 144 + }, 145 + "bin": { 146 + "hatk": "dist/cli.js" 147 + }, 148 + "peerDependencies": { 149 + "vite": "^8.0.0" 150 + } 151 + }, 152 + "node_modules/@hatk/oauth-client": { 153 + "version": "0.0.1-alpha.0", 154 + "resolved": "https://registry.npmjs.org/@hatk/oauth-client/-/oauth-client-0.0.1-alpha.0.tgz", 155 + "integrity": "sha512-WSGCfW9fzZ4zu3xAEtm4C5WV6f72SDVloIFS6EoHeNhs/IswKOUGRvgiy6ibAmErBqcrid4euiDF1/IphW5XzQ==" 156 + }, 157 + "node_modules/@jridgewell/gen-mapping": { 158 + "version": "0.3.13", 159 + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", 160 + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", 161 + "license": "MIT", 162 + "dependencies": { 163 + "@jridgewell/sourcemap-codec": "^1.5.0", 164 + "@jridgewell/trace-mapping": "^0.3.24" 165 + } 166 + }, 167 + "node_modules/@jridgewell/remapping": { 168 + "version": "2.3.5", 169 + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", 170 + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", 171 + "license": "MIT", 172 + "dependencies": { 173 + "@jridgewell/gen-mapping": "^0.3.5", 174 + "@jridgewell/trace-mapping": "^0.3.24" 175 + } 176 + }, 177 + "node_modules/@jridgewell/resolve-uri": { 178 + "version": "3.1.2", 179 + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", 180 + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", 181 + "license": "MIT", 182 + "engines": { 183 + "node": ">=6.0.0" 184 + } 185 + }, 186 + "node_modules/@jridgewell/sourcemap-codec": { 187 + "version": "1.5.5", 188 + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", 189 + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", 190 + "license": "MIT" 191 + }, 192 + "node_modules/@jridgewell/trace-mapping": { 193 + "version": "0.3.31", 194 + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", 195 + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", 196 + "license": "MIT", 197 + "dependencies": { 198 + "@jridgewell/resolve-uri": "^3.1.0", 199 + "@jridgewell/sourcemap-codec": "^1.4.14" 200 + } 201 + }, 202 + "node_modules/@oxc-project/runtime": { 203 + "version": "0.121.0", 204 + "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.121.0.tgz", 205 + "integrity": "sha512-p0bQukD8OEHxzY4T9OlANBbEFGnOnjo1CYi50HES7OD36UO2yPh6T+uOJKLtlg06eclxroipRCpQGMpeH8EJ/g==", 206 + "license": "MIT", 207 + "engines": { 208 + "node": "^20.19.0 || >=22.12.0" 209 + } 210 + }, 211 + "node_modules/@oxc-project/types": { 212 + "version": "0.122.0", 213 + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", 214 + "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", 215 + "license": "MIT", 216 + "funding": { 217 + "url": "https://github.com/sponsors/Boshen" 218 + } 219 + }, 220 + "node_modules/@oxfmt/binding-android-arm-eabi": { 221 + "version": "0.42.0", 222 + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.42.0.tgz", 223 + "integrity": "sha512-dsqPTYsozeokRjlrt/b4E7Pj0z3eS3Eg74TWQuuKbjY4VttBmA88rB7d50Xrd+TZ986qdXCNeZRPEzZHAe+jow==", 224 + "cpu": [ 225 + "arm" 226 + ], 227 + "dev": true, 228 + "license": "MIT", 229 + "optional": true, 230 + "os": [ 231 + "android" 232 + ], 233 + "engines": { 234 + "node": "^20.19.0 || >=22.12.0" 235 + } 236 + }, 237 + "node_modules/@oxfmt/binding-android-arm64": { 238 + "version": "0.42.0", 239 + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.42.0.tgz", 240 + "integrity": "sha512-t+aAjHxcr5eOBphFHdg1ouQU9qmZZoRxnX7UOJSaTwSoKsb6TYezNKO0YbWytGXCECObRqNcUxPoPr0KaraAIg==", 241 + "cpu": [ 242 + "arm64" 243 + ], 244 + "dev": true, 245 + "license": "MIT", 246 + "optional": true, 247 + "os": [ 248 + "android" 249 + ], 250 + "engines": { 251 + "node": "^20.19.0 || >=22.12.0" 252 + } 253 + }, 254 + "node_modules/@oxfmt/binding-darwin-arm64": { 255 + "version": "0.42.0", 256 + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.42.0.tgz", 257 + "integrity": "sha512-ulpSEYMKg61C5bRMZinFHrKJYRoKGVbvMEXA5zM1puX3O9T6Q4XXDbft20yrDijpYWeuG59z3Nabt+npeTsM1A==", 258 + "cpu": [ 259 + "arm64" 260 + ], 261 + "dev": true, 262 + "license": "MIT", 263 + "optional": true, 264 + "os": [ 265 + "darwin" 266 + ], 267 + "engines": { 268 + "node": "^20.19.0 || >=22.12.0" 269 + } 270 + }, 271 + "node_modules/@oxfmt/binding-darwin-x64": { 272 + "version": "0.42.0", 273 + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.42.0.tgz", 274 + "integrity": "sha512-ttxLKhQYPdFiM8I/Ri37cvqChE4Xa562nNOsZFcv1CKTVLeEozXjKuYClNvxkXmNlcF55nzM80P+CQkdFBu+uQ==", 275 + "cpu": [ 276 + "x64" 277 + ], 278 + "dev": true, 279 + "license": "MIT", 280 + "optional": true, 281 + "os": [ 282 + "darwin" 283 + ], 284 + "engines": { 285 + "node": "^20.19.0 || >=22.12.0" 286 + } 287 + }, 288 + "node_modules/@oxfmt/binding-freebsd-x64": { 289 + "version": "0.42.0", 290 + "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.42.0.tgz", 291 + "integrity": "sha512-Og7QS3yI3tdIKYZ58SXik0rADxIk2jmd+/YvuHRyKULWpG4V2fR5V4hvKm624Mc0cQET35waPXiCQWvjQEjwYQ==", 292 + "cpu": [ 293 + "x64" 294 + ], 295 + "dev": true, 296 + "license": "MIT", 297 + "optional": true, 298 + "os": [ 299 + "freebsd" 300 + ], 301 + "engines": { 302 + "node": "^20.19.0 || >=22.12.0" 303 + } 304 + }, 305 + "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { 306 + "version": "0.42.0", 307 + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.42.0.tgz", 308 + "integrity": "sha512-jwLOw/3CW4H6Vxcry4/buQHk7zm9Ne2YsidzTL1kpiMe4qqrRCwev3dkyWe2YkFmP+iZCQ7zku4KwjcLRoh8ew==", 309 + "cpu": [ 310 + "arm" 311 + ], 312 + "dev": true, 313 + "license": "MIT", 314 + "optional": true, 315 + "os": [ 316 + "linux" 317 + ], 318 + "engines": { 319 + "node": "^20.19.0 || >=22.12.0" 320 + } 321 + }, 322 + "node_modules/@oxfmt/binding-linux-arm-musleabihf": { 323 + "version": "0.42.0", 324 + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.42.0.tgz", 325 + "integrity": "sha512-XwXu2vkMtiq2h7tfvN+WA/9/5/1IoGAVCFPiiQUvcAuG3efR97KNcRGM8BetmbYouFotQ2bDal3yyjUx6IPsTg==", 326 + "cpu": [ 327 + "arm" 328 + ], 329 + "dev": true, 330 + "license": "MIT", 331 + "optional": true, 332 + "os": [ 333 + "linux" 334 + ], 335 + "engines": { 336 + "node": "^20.19.0 || >=22.12.0" 337 + } 338 + }, 339 + "node_modules/@oxfmt/binding-linux-arm64-gnu": { 340 + "version": "0.42.0", 341 + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.42.0.tgz", 342 + "integrity": "sha512-ea7s/XUJoT7ENAtUQDudFe3nkSM3e3Qpz4nJFRdzO2wbgXEcjnchKLEsV3+t4ev3r8nWxIYr9NRjPWtnyIFJVA==", 343 + "cpu": [ 344 + "arm64" 345 + ], 346 + "dev": true, 347 + "license": "MIT", 348 + "optional": true, 349 + "os": [ 350 + "linux" 351 + ], 352 + "engines": { 353 + "node": "^20.19.0 || >=22.12.0" 354 + } 355 + }, 356 + "node_modules/@oxfmt/binding-linux-arm64-musl": { 357 + "version": "0.42.0", 358 + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.42.0.tgz", 359 + "integrity": "sha512-+JA0YMlSdDqmacygGi2REp57c3fN+tzARD8nwsukx9pkCHK+6DkbAA9ojS4lNKsiBjIW8WWa0pBrBWhdZEqfuw==", 360 + "cpu": [ 361 + "arm64" 362 + ], 363 + "dev": true, 364 + "license": "MIT", 365 + "optional": true, 366 + "os": [ 367 + "linux" 368 + ], 369 + "engines": { 370 + "node": "^20.19.0 || >=22.12.0" 371 + } 372 + }, 373 + "node_modules/@oxfmt/binding-linux-ppc64-gnu": { 374 + "version": "0.42.0", 375 + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.42.0.tgz", 376 + "integrity": "sha512-VfnET0j4Y5mdfCzh5gBt0NK28lgn5DKx+8WgSMLYYeSooHhohdbzwAStLki9pNuGy51y4I7IoW8bqwAaCMiJQg==", 377 + "cpu": [ 378 + "ppc64" 379 + ], 380 + "dev": true, 381 + "license": "MIT", 382 + "optional": true, 383 + "os": [ 384 + "linux" 385 + ], 386 + "engines": { 387 + "node": "^20.19.0 || >=22.12.0" 388 + } 389 + }, 390 + "node_modules/@oxfmt/binding-linux-riscv64-gnu": { 391 + "version": "0.42.0", 392 + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.42.0.tgz", 393 + "integrity": "sha512-gVlCbmBkB0fxBWbhBj9rcxezPydsQHf4MFKeHoTSPicOQ+8oGeTQgQ8EeesSybWeiFPVRx3bgdt4IJnH6nOjAA==", 394 + "cpu": [ 395 + "riscv64" 396 + ], 397 + "dev": true, 398 + "license": "MIT", 399 + "optional": true, 400 + "os": [ 401 + "linux" 402 + ], 403 + "engines": { 404 + "node": "^20.19.0 || >=22.12.0" 405 + } 406 + }, 407 + "node_modules/@oxfmt/binding-linux-riscv64-musl": { 408 + "version": "0.42.0", 409 + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.42.0.tgz", 410 + "integrity": "sha512-zN5OfstL0avgt/IgvRu0zjQzVh/EPkcLzs33E9LMAzpqlLWiPWeMDZyMGFlSRGOdDjuNmlZBCgj0pFnK5u32TQ==", 411 + "cpu": [ 412 + "riscv64" 413 + ], 414 + "dev": true, 415 + "license": "MIT", 416 + "optional": true, 417 + "os": [ 418 + "linux" 419 + ], 420 + "engines": { 421 + "node": "^20.19.0 || >=22.12.0" 422 + } 423 + }, 424 + "node_modules/@oxfmt/binding-linux-s390x-gnu": { 425 + "version": "0.42.0", 426 + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.42.0.tgz", 427 + "integrity": "sha512-9X6+H2L0qMc2sCAgO9HS03bkGLMKvOFjmEdchaFlany3vNZOjnVui//D8k/xZAtQv2vaCs1reD5KAgPoIU4msA==", 428 + "cpu": [ 429 + "s390x" 430 + ], 431 + "dev": true, 432 + "license": "MIT", 433 + "optional": true, 434 + "os": [ 435 + "linux" 436 + ], 437 + "engines": { 438 + "node": "^20.19.0 || >=22.12.0" 439 + } 440 + }, 441 + "node_modules/@oxfmt/binding-linux-x64-gnu": { 442 + "version": "0.42.0", 443 + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.42.0.tgz", 444 + "integrity": "sha512-BajxJ6KQvMMdpXGPWhBGyjb2Jvx4uec0w+wi6TJZ6Tv7+MzPwe0pO8g5h1U0jyFgoaF7mDl6yKPW3ykWcbUJRw==", 445 + "cpu": [ 446 + "x64" 447 + ], 448 + "dev": true, 449 + "license": "MIT", 450 + "optional": true, 451 + "os": [ 452 + "linux" 453 + ], 454 + "engines": { 455 + "node": "^20.19.0 || >=22.12.0" 456 + } 457 + }, 458 + "node_modules/@oxfmt/binding-linux-x64-musl": { 459 + "version": "0.42.0", 460 + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.42.0.tgz", 461 + "integrity": "sha512-0wV284I6vc5f0AqAhgAbHU2935B4bVpncPoe5n/WzVZY/KnHgqxC8iSFGeSyLWEgstFboIcWkOPck7tqbdHkzA==", 462 + "cpu": [ 463 + "x64" 464 + ], 465 + "dev": true, 466 + "license": "MIT", 467 + "optional": true, 468 + "os": [ 469 + "linux" 470 + ], 471 + "engines": { 472 + "node": "^20.19.0 || >=22.12.0" 473 + } 474 + }, 475 + "node_modules/@oxfmt/binding-openharmony-arm64": { 476 + "version": "0.42.0", 477 + "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.42.0.tgz", 478 + "integrity": "sha512-p4BG6HpGnhfgHk1rzZfyR6zcWkE7iLrWxyehHfXUy4Qa5j3e0roglFOdP/Nj5cJJ58MA3isQ5dlfkW2nNEpolw==", 479 + "cpu": [ 480 + "arm64" 481 + ], 482 + "dev": true, 483 + "license": "MIT", 484 + "optional": true, 485 + "os": [ 486 + "openharmony" 487 + ], 488 + "engines": { 489 + "node": "^20.19.0 || >=22.12.0" 490 + } 491 + }, 492 + "node_modules/@oxfmt/binding-win32-arm64-msvc": { 493 + "version": "0.42.0", 494 + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.42.0.tgz", 495 + "integrity": "sha512-mn//WV60A+IetORDxYieYGAoQso4KnVRRjORDewMcod4irlRe0OSC7YPhhwaexYNPQz/GCFk+v9iUcZ2W22yxQ==", 496 + "cpu": [ 497 + "arm64" 498 + ], 499 + "dev": true, 500 + "license": "MIT", 501 + "optional": true, 502 + "os": [ 503 + "win32" 504 + ], 505 + "engines": { 506 + "node": "^20.19.0 || >=22.12.0" 507 + } 508 + }, 509 + "node_modules/@oxfmt/binding-win32-ia32-msvc": { 510 + "version": "0.42.0", 511 + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.42.0.tgz", 512 + "integrity": "sha512-3gWltUrvuz4LPJXWivoAxZ28Of2O4N7OGuM5/X3ubPXCEV8hmgECLZzjz7UYvSDUS3grfdccQwmjynm+51EFpw==", 513 + "cpu": [ 514 + "ia32" 515 + ], 516 + "dev": true, 517 + "license": "MIT", 518 + "optional": true, 519 + "os": [ 520 + "win32" 521 + ], 522 + "engines": { 523 + "node": "^20.19.0 || >=22.12.0" 524 + } 525 + }, 526 + "node_modules/@oxfmt/binding-win32-x64-msvc": { 527 + "version": "0.42.0", 528 + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.42.0.tgz", 529 + "integrity": "sha512-Wg4TMAfQRL9J9AZevJ/ZNy3uyyDztDYQtGr4P8UyyzIhLhFrdSmz1J/9JT+rv0fiCDLaFOBQnj3f3K3+a5PzDQ==", 530 + "cpu": [ 531 + "x64" 532 + ], 533 + "dev": true, 534 + "license": "MIT", 535 + "optional": true, 536 + "os": [ 537 + "win32" 538 + ], 539 + "engines": { 540 + "node": "^20.19.0 || >=22.12.0" 541 + } 542 + }, 543 + "node_modules/@oxlint-tsgolint/darwin-arm64": { 544 + "version": "0.17.3", 545 + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-arm64/-/darwin-arm64-0.17.3.tgz", 546 + "integrity": "sha512-5aDl4mxXWs+Bj02pNrX6YY6v9KMZjLIytXoqolLEo0dfBNVeZUonZgJAa/w0aUmijwIRrBhxEzb42oLuUtfkGw==", 547 + "cpu": [ 548 + "arm64" 549 + ], 550 + "dev": true, 551 + "license": "MIT", 552 + "optional": true, 553 + "os": [ 554 + "darwin" 555 + ] 556 + }, 557 + "node_modules/@oxlint-tsgolint/darwin-x64": { 558 + "version": "0.17.3", 559 + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-x64/-/darwin-x64-0.17.3.tgz", 560 + "integrity": "sha512-gPBy4DS5ueCgXzko20XsNZzDe/Cxde056B+QuPLGvz05CGEAtmRfpImwnyY2lAXXjPL+SmnC/OYexu8zI12yHQ==", 561 + "cpu": [ 562 + "x64" 563 + ], 564 + "dev": true, 565 + "license": "MIT", 566 + "optional": true, 567 + "os": [ 568 + "darwin" 569 + ] 570 + }, 571 + "node_modules/@oxlint-tsgolint/linux-arm64": { 572 + "version": "0.17.3", 573 + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-arm64/-/linux-arm64-0.17.3.tgz", 574 + "integrity": "sha512-+pkunvCfB6pB0G9qHVVXUao3nqzXQPo4O3DReIi+5nGa+bOU3J3Srgy+Zb8VyOL+WDsSMJ+U7+r09cKHWhz3hg==", 575 + "cpu": [ 576 + "arm64" 577 + ], 578 + "dev": true, 579 + "license": "MIT", 580 + "optional": true, 581 + "os": [ 582 + "linux" 583 + ] 584 + }, 585 + "node_modules/@oxlint-tsgolint/linux-x64": { 586 + "version": "0.17.3", 587 + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-x64/-/linux-x64-0.17.3.tgz", 588 + "integrity": "sha512-/kW5oXtBThu4FjmgIBthdmMjWLzT3M1TEDQhxDu7hQU5xDeTd60CDXb2SSwKCbue9xu7MbiFoJu83LN0Z/d38g==", 589 + "cpu": [ 590 + "x64" 591 + ], 592 + "dev": true, 593 + "license": "MIT", 594 + "optional": true, 595 + "os": [ 596 + "linux" 597 + ] 598 + }, 599 + "node_modules/@oxlint-tsgolint/win32-arm64": { 600 + "version": "0.17.3", 601 + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-arm64/-/win32-arm64-0.17.3.tgz", 602 + "integrity": "sha512-NMELRvbz4Ed4dxg8WiqZxtu3k4OJEp2B9KInZW+BMfqEqbwZdEJY83tbqz2hD1EjKO2akrqBQ0GpRUJEkd8kKw==", 603 + "cpu": [ 604 + "arm64" 605 + ], 606 + "dev": true, 607 + "license": "MIT", 608 + "optional": true, 609 + "os": [ 610 + "win32" 611 + ] 612 + }, 613 + "node_modules/@oxlint-tsgolint/win32-x64": { 614 + "version": "0.17.3", 615 + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-x64/-/win32-x64-0.17.3.tgz", 616 + "integrity": "sha512-+pJ7r8J3SLPws5uoidVplZc8R/lpKyKPE6LoPGv9BME00Y1VjT6jWGx/dtUN8PWvcu3iTC6k+8u3ojFSJNmWTg==", 617 + "cpu": [ 618 + "x64" 619 + ], 620 + "dev": true, 621 + "license": "MIT", 622 + "optional": true, 623 + "os": [ 624 + "win32" 625 + ] 626 + }, 627 + "node_modules/@oxlint/binding-android-arm-eabi": { 628 + "version": "1.57.0", 629 + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.57.0.tgz", 630 + "integrity": "sha512-C7EiyfAJG4B70496eV543nKiq5cH0o/xIh/ufbjQz3SIvHhlDDsyn+mRFh+aW8KskTyUpyH2LGWL8p2oN6bl1A==", 631 + "cpu": [ 632 + "arm" 633 + ], 634 + "dev": true, 635 + "license": "MIT", 636 + "optional": true, 637 + "os": [ 638 + "android" 639 + ], 640 + "engines": { 641 + "node": "^20.19.0 || >=22.12.0" 642 + } 643 + }, 644 + "node_modules/@oxlint/binding-android-arm64": { 645 + "version": "1.57.0", 646 + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.57.0.tgz", 647 + "integrity": "sha512-9i80AresjZ/FZf5xK8tKFbhQnijD4s1eOZw6/FHUwD59HEZbVLRc2C88ADYJfLZrF5XofWDiRX/Ja9KefCLy7w==", 648 + "cpu": [ 649 + "arm64" 650 + ], 651 + "dev": true, 652 + "license": "MIT", 653 + "optional": true, 654 + "os": [ 655 + "android" 656 + ], 657 + "engines": { 658 + "node": "^20.19.0 || >=22.12.0" 659 + } 660 + }, 661 + "node_modules/@oxlint/binding-darwin-arm64": { 662 + "version": "1.57.0", 663 + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.57.0.tgz", 664 + "integrity": "sha512-0eUfhRz5L2yKa9I8k3qpyl37XK3oBS5BvrgdVIx599WZK63P8sMbg+0s4IuxmIiZuBK68Ek+Z+gcKgeYf0otsg==", 665 + "cpu": [ 666 + "arm64" 667 + ], 668 + "dev": true, 669 + "license": "MIT", 670 + "optional": true, 671 + "os": [ 672 + "darwin" 673 + ], 674 + "engines": { 675 + "node": "^20.19.0 || >=22.12.0" 676 + } 677 + }, 678 + "node_modules/@oxlint/binding-darwin-x64": { 679 + "version": "1.57.0", 680 + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.57.0.tgz", 681 + "integrity": "sha512-UvrSuzBaYOue+QMAcuDITe0k/Vhj6KZGjfnI6x+NkxBTke/VoM7ZisaxgNY0LWuBkTnd1OmeQfEQdQ48fRjkQg==", 682 + "cpu": [ 683 + "x64" 684 + ], 685 + "dev": true, 686 + "license": "MIT", 687 + "optional": true, 688 + "os": [ 689 + "darwin" 690 + ], 691 + "engines": { 692 + "node": "^20.19.0 || >=22.12.0" 693 + } 694 + }, 695 + "node_modules/@oxlint/binding-freebsd-x64": { 696 + "version": "1.57.0", 697 + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.57.0.tgz", 698 + "integrity": "sha512-wtQq0dCoiw4bUwlsNVDJJ3pxJA218fOezpgtLKrbQqUtQJcM9yP8z+I9fu14aHg0uyAxIY+99toL6uBa2r7nxA==", 699 + "cpu": [ 700 + "x64" 701 + ], 702 + "dev": true, 703 + "license": "MIT", 704 + "optional": true, 705 + "os": [ 706 + "freebsd" 707 + ], 708 + "engines": { 709 + "node": "^20.19.0 || >=22.12.0" 710 + } 711 + }, 712 + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { 713 + "version": "1.57.0", 714 + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.57.0.tgz", 715 + "integrity": "sha512-qxFWl2BBBFcT4djKa+OtMdnLgoHEJXpqjyGwz8OhW35ImoCwR5qtAGqApNYce5260FQqoAHW8S8eZTjiX67Tsg==", 716 + "cpu": [ 717 + "arm" 718 + ], 719 + "dev": true, 720 + "license": "MIT", 721 + "optional": true, 722 + "os": [ 723 + "linux" 724 + ], 725 + "engines": { 726 + "node": "^20.19.0 || >=22.12.0" 727 + } 728 + }, 729 + "node_modules/@oxlint/binding-linux-arm-musleabihf": { 730 + "version": "1.57.0", 731 + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.57.0.tgz", 732 + "integrity": "sha512-SQoIsBU7J0bDW15/f0/RvxHfY3Y0+eB/caKBQtNFbuerTiA6JCYx9P1MrrFTwY2dTm/lMgTSgskvCEYk2AtG/Q==", 733 + "cpu": [ 734 + "arm" 735 + ], 736 + "dev": true, 737 + "license": "MIT", 738 + "optional": true, 739 + "os": [ 740 + "linux" 741 + ], 742 + "engines": { 743 + "node": "^20.19.0 || >=22.12.0" 744 + } 745 + }, 746 + "node_modules/@oxlint/binding-linux-arm64-gnu": { 747 + "version": "1.57.0", 748 + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.57.0.tgz", 749 + "integrity": "sha512-jqxYd1W6WMeozsCmqe9Rzbu3SRrGTyGDAipRlRggetyYbUksJqJKvUNTQtZR/KFoJPb+grnSm5SHhdWrywv3RQ==", 750 + "cpu": [ 751 + "arm64" 752 + ], 753 + "dev": true, 754 + "license": "MIT", 755 + "optional": true, 756 + "os": [ 757 + "linux" 758 + ], 759 + "engines": { 760 + "node": "^20.19.0 || >=22.12.0" 761 + } 762 + }, 763 + "node_modules/@oxlint/binding-linux-arm64-musl": { 764 + "version": "1.57.0", 765 + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.57.0.tgz", 766 + "integrity": "sha512-i66WyEPVEvq9bxRUCJ/MP5EBfnTDN3nhwEdFZFTO5MmLLvzngfWEG3NSdXQzTT3vk5B9i6C2XSIYBh+aG6uqyg==", 767 + "cpu": [ 768 + "arm64" 769 + ], 770 + "dev": true, 771 + "license": "MIT", 772 + "optional": true, 773 + "os": [ 774 + "linux" 775 + ], 776 + "engines": { 777 + "node": "^20.19.0 || >=22.12.0" 778 + } 779 + }, 780 + "node_modules/@oxlint/binding-linux-ppc64-gnu": { 781 + "version": "1.57.0", 782 + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.57.0.tgz", 783 + "integrity": "sha512-oMZDCwz4NobclZU3pH+V1/upVlJZiZvne4jQP+zhJwt+lmio4XXr4qG47CehvrW1Lx2YZiIHuxM2D4YpkG3KVA==", 784 + "cpu": [ 785 + "ppc64" 786 + ], 787 + "dev": true, 788 + "license": "MIT", 789 + "optional": true, 790 + "os": [ 791 + "linux" 792 + ], 793 + "engines": { 794 + "node": "^20.19.0 || >=22.12.0" 795 + } 796 + }, 797 + "node_modules/@oxlint/binding-linux-riscv64-gnu": { 798 + "version": "1.57.0", 799 + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.57.0.tgz", 800 + "integrity": "sha512-uoBnjJ3MMEBbfnWC1jSFr7/nSCkcQYa72NYoNtLl1imshDnWSolYCjzb8LVCwYCCfLJXD+0gBLD7fyC14c0+0g==", 801 + "cpu": [ 802 + "riscv64" 803 + ], 804 + "dev": true, 805 + "license": "MIT", 806 + "optional": true, 807 + "os": [ 808 + "linux" 809 + ], 810 + "engines": { 811 + "node": "^20.19.0 || >=22.12.0" 812 + } 813 + }, 814 + "node_modules/@oxlint/binding-linux-riscv64-musl": { 815 + "version": "1.57.0", 816 + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.57.0.tgz", 817 + "integrity": "sha512-BdrwD7haPZ8a9KrZhKJRSj6jwCor+Z8tHFZ3PT89Y3Jq5v3LfMfEePeAmD0LOTWpiTmzSzdmyw9ijneapiVHKQ==", 818 + "cpu": [ 819 + "riscv64" 820 + ], 821 + "dev": true, 822 + "license": "MIT", 823 + "optional": true, 824 + "os": [ 825 + "linux" 826 + ], 827 + "engines": { 828 + "node": "^20.19.0 || >=22.12.0" 829 + } 830 + }, 831 + "node_modules/@oxlint/binding-linux-s390x-gnu": { 832 + "version": "1.57.0", 833 + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.57.0.tgz", 834 + "integrity": "sha512-BNs+7ZNsRstVg2tpNxAXfMX/Iv5oZh204dVyb8Z37+/gCh+yZqNTlg6YwCLIMPSk5wLWIGOaQjT0GUOahKYImw==", 835 + "cpu": [ 836 + "s390x" 837 + ], 838 + "dev": true, 839 + "license": "MIT", 840 + "optional": true, 841 + "os": [ 842 + "linux" 843 + ], 844 + "engines": { 845 + "node": "^20.19.0 || >=22.12.0" 846 + } 847 + }, 848 + "node_modules/@oxlint/binding-linux-x64-gnu": { 849 + "version": "1.57.0", 850 + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.57.0.tgz", 851 + "integrity": "sha512-AghS18w+XcENcAX0+BQGLiqjpqpaxKJa4cWWP0OWNLacs27vHBxu7TYkv9LUSGe5w8lOJHeMxcYfZNOAPqw2bg==", 852 + "cpu": [ 853 + "x64" 854 + ], 855 + "dev": true, 856 + "license": "MIT", 857 + "optional": true, 858 + "os": [ 859 + "linux" 860 + ], 861 + "engines": { 862 + "node": "^20.19.0 || >=22.12.0" 863 + } 864 + }, 865 + "node_modules/@oxlint/binding-linux-x64-musl": { 866 + "version": "1.57.0", 867 + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.57.0.tgz", 868 + "integrity": "sha512-E/FV3GB8phu/Rpkhz5T96hAiJlGzn91qX5yj5gU754P5cmVGXY1Jw/VSjDSlZBCY3VHjsVLdzgdkJaomEmcNOg==", 869 + "cpu": [ 870 + "x64" 871 + ], 872 + "dev": true, 873 + "license": "MIT", 874 + "optional": true, 875 + "os": [ 876 + "linux" 877 + ], 878 + "engines": { 879 + "node": "^20.19.0 || >=22.12.0" 880 + } 881 + }, 882 + "node_modules/@oxlint/binding-openharmony-arm64": { 883 + "version": "1.57.0", 884 + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.57.0.tgz", 885 + "integrity": "sha512-xvZ2yZt0nUVfU14iuGv3V25jpr9pov5N0Wr28RXnHFxHCRxNDMtYPHV61gGLhN9IlXM96gI4pyYpLSJC5ClLCQ==", 886 + "cpu": [ 887 + "arm64" 888 + ], 889 + "dev": true, 890 + "license": "MIT", 891 + "optional": true, 892 + "os": [ 893 + "openharmony" 894 + ], 895 + "engines": { 896 + "node": "^20.19.0 || >=22.12.0" 897 + } 898 + }, 899 + "node_modules/@oxlint/binding-win32-arm64-msvc": { 900 + "version": "1.57.0", 901 + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.57.0.tgz", 902 + "integrity": "sha512-Z4D8Pd0AyHBKeazhdIXeUUy5sIS3Mo0veOlzlDECg6PhRRKgEsBJCCV1n+keUZtQ04OP+i7+itS3kOykUyNhDg==", 903 + "cpu": [ 904 + "arm64" 905 + ], 906 + "dev": true, 907 + "license": "MIT", 908 + "optional": true, 909 + "os": [ 910 + "win32" 911 + ], 912 + "engines": { 913 + "node": "^20.19.0 || >=22.12.0" 914 + } 915 + }, 916 + "node_modules/@oxlint/binding-win32-ia32-msvc": { 917 + "version": "1.57.0", 918 + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.57.0.tgz", 919 + "integrity": "sha512-StOZ9nFMVKvevicbQfql6Pouu9pgbeQnu60Fvhz2S6yfMaii+wnueLnqQ5I1JPgNF0Syew4voBlAaHD13wH6tw==", 920 + "cpu": [ 921 + "ia32" 922 + ], 923 + "dev": true, 924 + "license": "MIT", 925 + "optional": true, 926 + "os": [ 927 + "win32" 928 + ], 929 + "engines": { 930 + "node": "^20.19.0 || >=22.12.0" 931 + } 932 + }, 933 + "node_modules/@oxlint/binding-win32-x64-msvc": { 934 + "version": "1.57.0", 935 + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.57.0.tgz", 936 + "integrity": "sha512-6PuxhYgth8TuW0+ABPOIkGdBYw+qYGxgIdXPHSVpiCDm+hqTTWCmC739St1Xni0DJBt8HnSHTG67i1y6gr8qrA==", 937 + "cpu": [ 938 + "x64" 939 + ], 940 + "dev": true, 941 + "license": "MIT", 942 + "optional": true, 943 + "os": [ 944 + "win32" 945 + ], 946 + "engines": { 947 + "node": "^20.19.0 || >=22.12.0" 948 + } 949 + }, 950 + "node_modules/@polka/url": { 951 + "version": "1.0.0-next.29", 952 + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", 953 + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", 954 + "license": "MIT" 955 + }, 956 + "node_modules/@resvg/resvg-js": { 957 + "version": "2.6.2", 958 + "resolved": "https://registry.npmjs.org/@resvg/resvg-js/-/resvg-js-2.6.2.tgz", 959 + "integrity": "sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==", 960 + "license": "MPL-2.0", 961 + "engines": { 962 + "node": ">= 10" 963 + }, 964 + "optionalDependencies": { 965 + "@resvg/resvg-js-android-arm-eabi": "2.6.2", 966 + "@resvg/resvg-js-android-arm64": "2.6.2", 967 + "@resvg/resvg-js-darwin-arm64": "2.6.2", 968 + "@resvg/resvg-js-darwin-x64": "2.6.2", 969 + "@resvg/resvg-js-linux-arm-gnueabihf": "2.6.2", 970 + "@resvg/resvg-js-linux-arm64-gnu": "2.6.2", 971 + "@resvg/resvg-js-linux-arm64-musl": "2.6.2", 972 + "@resvg/resvg-js-linux-x64-gnu": "2.6.2", 973 + "@resvg/resvg-js-linux-x64-musl": "2.6.2", 974 + "@resvg/resvg-js-win32-arm64-msvc": "2.6.2", 975 + "@resvg/resvg-js-win32-ia32-msvc": "2.6.2", 976 + "@resvg/resvg-js-win32-x64-msvc": "2.6.2" 977 + } 978 + }, 979 + "node_modules/@resvg/resvg-js-android-arm-eabi": { 980 + "version": "2.6.2", 981 + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm-eabi/-/resvg-js-android-arm-eabi-2.6.2.tgz", 982 + "integrity": "sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==", 983 + "cpu": [ 984 + "arm" 985 + ], 986 + "license": "MPL-2.0", 987 + "optional": true, 988 + "os": [ 989 + "android" 990 + ], 991 + "engines": { 992 + "node": ">= 10" 993 + } 994 + }, 995 + "node_modules/@resvg/resvg-js-android-arm64": { 996 + "version": "2.6.2", 997 + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm64/-/resvg-js-android-arm64-2.6.2.tgz", 998 + "integrity": "sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==", 999 + "cpu": [ 1000 + "arm64" 1001 + ], 1002 + "license": "MPL-2.0", 1003 + "optional": true, 1004 + "os": [ 1005 + "android" 1006 + ], 1007 + "engines": { 1008 + "node": ">= 10" 1009 + } 1010 + }, 1011 + "node_modules/@resvg/resvg-js-darwin-arm64": { 1012 + "version": "2.6.2", 1013 + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-arm64/-/resvg-js-darwin-arm64-2.6.2.tgz", 1014 + "integrity": "sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==", 1015 + "cpu": [ 1016 + "arm64" 1017 + ], 1018 + "license": "MPL-2.0", 1019 + "optional": true, 1020 + "os": [ 1021 + "darwin" 1022 + ], 1023 + "engines": { 1024 + "node": ">= 10" 1025 + } 1026 + }, 1027 + "node_modules/@resvg/resvg-js-darwin-x64": { 1028 + "version": "2.6.2", 1029 + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-x64/-/resvg-js-darwin-x64-2.6.2.tgz", 1030 + "integrity": "sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==", 1031 + "cpu": [ 1032 + "x64" 1033 + ], 1034 + "license": "MPL-2.0", 1035 + "optional": true, 1036 + "os": [ 1037 + "darwin" 1038 + ], 1039 + "engines": { 1040 + "node": ">= 10" 1041 + } 1042 + }, 1043 + "node_modules/@resvg/resvg-js-linux-arm-gnueabihf": { 1044 + "version": "2.6.2", 1045 + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm-gnueabihf/-/resvg-js-linux-arm-gnueabihf-2.6.2.tgz", 1046 + "integrity": "sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==", 1047 + "cpu": [ 1048 + "arm" 1049 + ], 1050 + "license": "MPL-2.0", 1051 + "optional": true, 1052 + "os": [ 1053 + "linux" 1054 + ], 1055 + "engines": { 1056 + "node": ">= 10" 1057 + } 1058 + }, 1059 + "node_modules/@resvg/resvg-js-linux-arm64-gnu": { 1060 + "version": "2.6.2", 1061 + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-gnu/-/resvg-js-linux-arm64-gnu-2.6.2.tgz", 1062 + "integrity": "sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==", 1063 + "cpu": [ 1064 + "arm64" 1065 + ], 1066 + "license": "MPL-2.0", 1067 + "optional": true, 1068 + "os": [ 1069 + "linux" 1070 + ], 1071 + "engines": { 1072 + "node": ">= 10" 1073 + } 1074 + }, 1075 + "node_modules/@resvg/resvg-js-linux-arm64-musl": { 1076 + "version": "2.6.2", 1077 + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-musl/-/resvg-js-linux-arm64-musl-2.6.2.tgz", 1078 + "integrity": "sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==", 1079 + "cpu": [ 1080 + "arm64" 1081 + ], 1082 + "license": "MPL-2.0", 1083 + "optional": true, 1084 + "os": [ 1085 + "linux" 1086 + ], 1087 + "engines": { 1088 + "node": ">= 10" 1089 + } 1090 + }, 1091 + "node_modules/@resvg/resvg-js-linux-x64-gnu": { 1092 + "version": "2.6.2", 1093 + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-gnu/-/resvg-js-linux-x64-gnu-2.6.2.tgz", 1094 + "integrity": "sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==", 1095 + "cpu": [ 1096 + "x64" 1097 + ], 1098 + "license": "MPL-2.0", 1099 + "optional": true, 1100 + "os": [ 1101 + "linux" 1102 + ], 1103 + "engines": { 1104 + "node": ">= 10" 1105 + } 1106 + }, 1107 + "node_modules/@resvg/resvg-js-linux-x64-musl": { 1108 + "version": "2.6.2", 1109 + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-musl/-/resvg-js-linux-x64-musl-2.6.2.tgz", 1110 + "integrity": "sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==", 1111 + "cpu": [ 1112 + "x64" 1113 + ], 1114 + "license": "MPL-2.0", 1115 + "optional": true, 1116 + "os": [ 1117 + "linux" 1118 + ], 1119 + "engines": { 1120 + "node": ">= 10" 1121 + } 1122 + }, 1123 + "node_modules/@resvg/resvg-js-win32-arm64-msvc": { 1124 + "version": "2.6.2", 1125 + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-arm64-msvc/-/resvg-js-win32-arm64-msvc-2.6.2.tgz", 1126 + "integrity": "sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==", 1127 + "cpu": [ 1128 + "arm64" 1129 + ], 1130 + "license": "MPL-2.0", 1131 + "optional": true, 1132 + "os": [ 1133 + "win32" 1134 + ], 1135 + "engines": { 1136 + "node": ">= 10" 1137 + } 1138 + }, 1139 + "node_modules/@resvg/resvg-js-win32-ia32-msvc": { 1140 + "version": "2.6.2", 1141 + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-ia32-msvc/-/resvg-js-win32-ia32-msvc-2.6.2.tgz", 1142 + "integrity": "sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==", 1143 + "cpu": [ 1144 + "ia32" 1145 + ], 1146 + "license": "MPL-2.0", 1147 + "optional": true, 1148 + "os": [ 1149 + "win32" 1150 + ], 1151 + "engines": { 1152 + "node": ">= 10" 1153 + } 1154 + }, 1155 + "node_modules/@resvg/resvg-js-win32-x64-msvc": { 1156 + "version": "2.6.2", 1157 + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-x64-msvc/-/resvg-js-win32-x64-msvc-2.6.2.tgz", 1158 + "integrity": "sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==", 1159 + "cpu": [ 1160 + "x64" 1161 + ], 1162 + "license": "MPL-2.0", 1163 + "optional": true, 1164 + "os": [ 1165 + "win32" 1166 + ], 1167 + "engines": { 1168 + "node": ">= 10" 1169 + } 1170 + }, 1171 + "node_modules/@rollup/plugin-commonjs": { 1172 + "version": "29.0.2", 1173 + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-29.0.2.tgz", 1174 + "integrity": "sha512-S/ggWH1LU7jTyi9DxZOKyxpVd4hF/OZ0JrEbeLjXk/DFXwRny0tjD2c992zOUYQobLrVkRVMDdmHP16HKP7GRg==", 1175 + "license": "MIT", 1176 + "dependencies": { 1177 + "@rollup/pluginutils": "^5.0.1", 1178 + "commondir": "^1.0.1", 1179 + "estree-walker": "^2.0.2", 1180 + "fdir": "^6.2.0", 1181 + "is-reference": "1.2.1", 1182 + "magic-string": "^0.30.3", 1183 + "picomatch": "^4.0.2" 1184 + }, 1185 + "engines": { 1186 + "node": ">=16.0.0 || 14 >= 14.17" 1187 + }, 1188 + "peerDependencies": { 1189 + "rollup": "^2.68.0||^3.0.0||^4.0.0" 1190 + }, 1191 + "peerDependenciesMeta": { 1192 + "rollup": { 1193 + "optional": true 1194 + } 1195 + } 1196 + }, 1197 + "node_modules/@rollup/plugin-json": { 1198 + "version": "6.1.0", 1199 + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", 1200 + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", 1201 + "license": "MIT", 1202 + "dependencies": { 1203 + "@rollup/pluginutils": "^5.1.0" 1204 + }, 1205 + "engines": { 1206 + "node": ">=14.0.0" 1207 + }, 1208 + "peerDependencies": { 1209 + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" 1210 + }, 1211 + "peerDependenciesMeta": { 1212 + "rollup": { 1213 + "optional": true 1214 + } 1215 + } 1216 + }, 1217 + "node_modules/@rollup/plugin-node-resolve": { 1218 + "version": "16.0.3", 1219 + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", 1220 + "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==", 1221 + "license": "MIT", 1222 + "dependencies": { 1223 + "@rollup/pluginutils": "^5.0.1", 1224 + "@types/resolve": "1.20.2", 1225 + "deepmerge": "^4.2.2", 1226 + "is-module": "^1.0.0", 1227 + "resolve": "^1.22.1" 1228 + }, 1229 + "engines": { 1230 + "node": ">=14.0.0" 1231 + }, 1232 + "peerDependencies": { 1233 + "rollup": "^2.78.0||^3.0.0||^4.0.0" 1234 + }, 1235 + "peerDependenciesMeta": { 1236 + "rollup": { 1237 + "optional": true 1238 + } 1239 + } 1240 + }, 1241 + "node_modules/@rollup/pluginutils": { 1242 + "version": "5.3.0", 1243 + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", 1244 + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", 1245 + "license": "MIT", 1246 + "dependencies": { 1247 + "@types/estree": "^1.0.0", 1248 + "estree-walker": "^2.0.2", 1249 + "picomatch": "^4.0.2" 1250 + }, 1251 + "engines": { 1252 + "node": ">=14.0.0" 1253 + }, 1254 + "peerDependencies": { 1255 + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" 1256 + }, 1257 + "peerDependenciesMeta": { 1258 + "rollup": { 1259 + "optional": true 1260 + } 1261 + } 1262 + }, 1263 + "node_modules/@rollup/rollup-android-arm-eabi": { 1264 + "version": "4.60.0", 1265 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", 1266 + "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==", 1267 + "cpu": [ 1268 + "arm" 1269 + ], 1270 + "license": "MIT", 1271 + "optional": true, 1272 + "os": [ 1273 + "android" 1274 + ] 1275 + }, 1276 + "node_modules/@rollup/rollup-android-arm64": { 1277 + "version": "4.60.0", 1278 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz", 1279 + "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==", 1280 + "cpu": [ 1281 + "arm64" 1282 + ], 1283 + "license": "MIT", 1284 + "optional": true, 1285 + "os": [ 1286 + "android" 1287 + ] 1288 + }, 1289 + "node_modules/@rollup/rollup-darwin-arm64": { 1290 + "version": "4.60.0", 1291 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz", 1292 + "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==", 1293 + "cpu": [ 1294 + "arm64" 1295 + ], 1296 + "license": "MIT", 1297 + "optional": true, 1298 + "os": [ 1299 + "darwin" 1300 + ] 1301 + }, 1302 + "node_modules/@rollup/rollup-darwin-x64": { 1303 + "version": "4.60.0", 1304 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz", 1305 + "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==", 1306 + "cpu": [ 1307 + "x64" 1308 + ], 1309 + "license": "MIT", 1310 + "optional": true, 1311 + "os": [ 1312 + "darwin" 1313 + ] 1314 + }, 1315 + "node_modules/@rollup/rollup-freebsd-arm64": { 1316 + "version": "4.60.0", 1317 + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz", 1318 + "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==", 1319 + "cpu": [ 1320 + "arm64" 1321 + ], 1322 + "license": "MIT", 1323 + "optional": true, 1324 + "os": [ 1325 + "freebsd" 1326 + ] 1327 + }, 1328 + "node_modules/@rollup/rollup-freebsd-x64": { 1329 + "version": "4.60.0", 1330 + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz", 1331 + "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==", 1332 + "cpu": [ 1333 + "x64" 1334 + ], 1335 + "license": "MIT", 1336 + "optional": true, 1337 + "os": [ 1338 + "freebsd" 1339 + ] 1340 + }, 1341 + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { 1342 + "version": "4.60.0", 1343 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz", 1344 + "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==", 1345 + "cpu": [ 1346 + "arm" 1347 + ], 1348 + "license": "MIT", 1349 + "optional": true, 1350 + "os": [ 1351 + "linux" 1352 + ] 1353 + }, 1354 + "node_modules/@rollup/rollup-linux-arm-musleabihf": { 1355 + "version": "4.60.0", 1356 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz", 1357 + "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==", 1358 + "cpu": [ 1359 + "arm" 1360 + ], 1361 + "license": "MIT", 1362 + "optional": true, 1363 + "os": [ 1364 + "linux" 1365 + ] 1366 + }, 1367 + "node_modules/@rollup/rollup-linux-arm64-gnu": { 1368 + "version": "4.60.0", 1369 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz", 1370 + "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==", 1371 + "cpu": [ 1372 + "arm64" 1373 + ], 1374 + "license": "MIT", 1375 + "optional": true, 1376 + "os": [ 1377 + "linux" 1378 + ] 1379 + }, 1380 + "node_modules/@rollup/rollup-linux-arm64-musl": { 1381 + "version": "4.60.0", 1382 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz", 1383 + "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==", 1384 + "cpu": [ 1385 + "arm64" 1386 + ], 1387 + "license": "MIT", 1388 + "optional": true, 1389 + "os": [ 1390 + "linux" 1391 + ] 1392 + }, 1393 + "node_modules/@rollup/rollup-linux-loong64-gnu": { 1394 + "version": "4.60.0", 1395 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz", 1396 + "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==", 1397 + "cpu": [ 1398 + "loong64" 1399 + ], 1400 + "license": "MIT", 1401 + "optional": true, 1402 + "os": [ 1403 + "linux" 1404 + ] 1405 + }, 1406 + "node_modules/@rollup/rollup-linux-loong64-musl": { 1407 + "version": "4.60.0", 1408 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz", 1409 + "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==", 1410 + "cpu": [ 1411 + "loong64" 1412 + ], 1413 + "license": "MIT", 1414 + "optional": true, 1415 + "os": [ 1416 + "linux" 1417 + ] 1418 + }, 1419 + "node_modules/@rollup/rollup-linux-ppc64-gnu": { 1420 + "version": "4.60.0", 1421 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz", 1422 + "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==", 1423 + "cpu": [ 1424 + "ppc64" 1425 + ], 1426 + "license": "MIT", 1427 + "optional": true, 1428 + "os": [ 1429 + "linux" 1430 + ] 1431 + }, 1432 + "node_modules/@rollup/rollup-linux-ppc64-musl": { 1433 + "version": "4.60.0", 1434 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz", 1435 + "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==", 1436 + "cpu": [ 1437 + "ppc64" 1438 + ], 1439 + "license": "MIT", 1440 + "optional": true, 1441 + "os": [ 1442 + "linux" 1443 + ] 1444 + }, 1445 + "node_modules/@rollup/rollup-linux-riscv64-gnu": { 1446 + "version": "4.60.0", 1447 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz", 1448 + "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==", 1449 + "cpu": [ 1450 + "riscv64" 1451 + ], 1452 + "license": "MIT", 1453 + "optional": true, 1454 + "os": [ 1455 + "linux" 1456 + ] 1457 + }, 1458 + "node_modules/@rollup/rollup-linux-riscv64-musl": { 1459 + "version": "4.60.0", 1460 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz", 1461 + "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==", 1462 + "cpu": [ 1463 + "riscv64" 1464 + ], 1465 + "license": "MIT", 1466 + "optional": true, 1467 + "os": [ 1468 + "linux" 1469 + ] 1470 + }, 1471 + "node_modules/@rollup/rollup-linux-s390x-gnu": { 1472 + "version": "4.60.0", 1473 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz", 1474 + "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==", 1475 + "cpu": [ 1476 + "s390x" 1477 + ], 1478 + "license": "MIT", 1479 + "optional": true, 1480 + "os": [ 1481 + "linux" 1482 + ] 1483 + }, 1484 + "node_modules/@rollup/rollup-linux-x64-gnu": { 1485 + "version": "4.60.0", 1486 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz", 1487 + "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==", 1488 + "cpu": [ 1489 + "x64" 1490 + ], 1491 + "license": "MIT", 1492 + "optional": true, 1493 + "os": [ 1494 + "linux" 1495 + ] 1496 + }, 1497 + "node_modules/@rollup/rollup-linux-x64-musl": { 1498 + "version": "4.60.0", 1499 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz", 1500 + "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==", 1501 + "cpu": [ 1502 + "x64" 1503 + ], 1504 + "license": "MIT", 1505 + "optional": true, 1506 + "os": [ 1507 + "linux" 1508 + ] 1509 + }, 1510 + "node_modules/@rollup/rollup-openbsd-x64": { 1511 + "version": "4.60.0", 1512 + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz", 1513 + "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==", 1514 + "cpu": [ 1515 + "x64" 1516 + ], 1517 + "license": "MIT", 1518 + "optional": true, 1519 + "os": [ 1520 + "openbsd" 1521 + ] 1522 + }, 1523 + "node_modules/@rollup/rollup-openharmony-arm64": { 1524 + "version": "4.60.0", 1525 + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz", 1526 + "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==", 1527 + "cpu": [ 1528 + "arm64" 1529 + ], 1530 + "license": "MIT", 1531 + "optional": true, 1532 + "os": [ 1533 + "openharmony" 1534 + ] 1535 + }, 1536 + "node_modules/@rollup/rollup-win32-arm64-msvc": { 1537 + "version": "4.60.0", 1538 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz", 1539 + "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==", 1540 + "cpu": [ 1541 + "arm64" 1542 + ], 1543 + "license": "MIT", 1544 + "optional": true, 1545 + "os": [ 1546 + "win32" 1547 + ] 1548 + }, 1549 + "node_modules/@rollup/rollup-win32-ia32-msvc": { 1550 + "version": "4.60.0", 1551 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz", 1552 + "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==", 1553 + "cpu": [ 1554 + "ia32" 1555 + ], 1556 + "license": "MIT", 1557 + "optional": true, 1558 + "os": [ 1559 + "win32" 1560 + ] 1561 + }, 1562 + "node_modules/@rollup/rollup-win32-x64-gnu": { 1563 + "version": "4.60.0", 1564 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz", 1565 + "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==", 1566 + "cpu": [ 1567 + "x64" 1568 + ], 1569 + "license": "MIT", 1570 + "optional": true, 1571 + "os": [ 1572 + "win32" 1573 + ] 1574 + }, 1575 + "node_modules/@rollup/rollup-win32-x64-msvc": { 1576 + "version": "4.60.0", 1577 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz", 1578 + "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==", 1579 + "cpu": [ 1580 + "x64" 1581 + ], 1582 + "license": "MIT", 1583 + "optional": true, 1584 + "os": [ 1585 + "win32" 1586 + ] 1587 + }, 1588 + "node_modules/@shuding/opentype.js": { 1589 + "version": "1.4.0-beta.0", 1590 + "resolved": "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz", 1591 + "integrity": "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==", 1592 + "license": "MIT", 1593 + "dependencies": { 1594 + "fflate": "^0.7.3", 1595 + "string.prototype.codepointat": "^0.2.1" 1596 + }, 1597 + "bin": { 1598 + "ot": "bin/ot" 1599 + }, 1600 + "engines": { 1601 + "node": ">= 8.0.0" 1602 + } 1603 + }, 1604 + "node_modules/@standard-schema/spec": { 1605 + "version": "1.1.0", 1606 + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", 1607 + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", 1608 + "license": "MIT" 1609 + }, 1610 + "node_modules/@sveltejs/acorn-typescript": { 1611 + "version": "1.0.9", 1612 + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", 1613 + "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==", 1614 + "license": "MIT", 1615 + "peerDependencies": { 1616 + "acorn": "^8.9.0" 1617 + } 1618 + }, 1619 + "node_modules/@sveltejs/adapter-node": { 1620 + "version": "5.5.4", 1621 + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.5.4.tgz", 1622 + "integrity": "sha512-45X92CXW+2J8ZUzPv3eLlKWEzINKiiGeFWTjyER4ZN4sGgNoaoeSkCY/QYNxHpPXy71QPsctwccBo9jJs0ySPQ==", 1623 + "license": "MIT", 1624 + "dependencies": { 1625 + "@rollup/plugin-commonjs": "^29.0.0", 1626 + "@rollup/plugin-json": "^6.1.0", 1627 + "@rollup/plugin-node-resolve": "^16.0.0", 1628 + "rollup": "^4.59.0" 1629 + }, 1630 + "peerDependencies": { 1631 + "@sveltejs/kit": "^2.4.0" 1632 + } 1633 + }, 1634 + "node_modules/@sveltejs/kit": { 1635 + "version": "2.55.0", 1636 + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.55.0.tgz", 1637 + "integrity": "sha512-MdFRjevVxmAknf2NbaUkDF16jSIzXMWd4Nfah0Qp8TtQVoSp3bV4jKt8mX7z7qTUTWvgSaxtR0EG5WJf53gcuA==", 1638 + "license": "MIT", 1639 + "dependencies": { 1640 + "@standard-schema/spec": "^1.0.0", 1641 + "@sveltejs/acorn-typescript": "^1.0.5", 1642 + "@types/cookie": "^0.6.0", 1643 + "acorn": "^8.14.1", 1644 + "cookie": "^0.6.0", 1645 + "devalue": "^5.6.4", 1646 + "esm-env": "^1.2.2", 1647 + "kleur": "^4.1.5", 1648 + "magic-string": "^0.30.5", 1649 + "mrmime": "^2.0.0", 1650 + "set-cookie-parser": "^3.0.0", 1651 + "sirv": "^3.0.0" 1652 + }, 1653 + "bin": { 1654 + "svelte-kit": "svelte-kit.js" 1655 + }, 1656 + "engines": { 1657 + "node": ">=18.13" 1658 + }, 1659 + "peerDependencies": { 1660 + "@opentelemetry/api": "^1.0.0", 1661 + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", 1662 + "svelte": "^4.0.0 || ^5.0.0-next.0", 1663 + "typescript": "^5.3.3", 1664 + "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" 1665 + }, 1666 + "peerDependenciesMeta": { 1667 + "@opentelemetry/api": { 1668 + "optional": true 1669 + }, 1670 + "typescript": { 1671 + "optional": true 1672 + } 1673 + } 1674 + }, 1675 + "node_modules/@sveltejs/vite-plugin-svelte": { 1676 + "version": "7.0.0", 1677 + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.0.0.tgz", 1678 + "integrity": "sha512-ILXmxC7HAsnkK2eslgPetrqqW1BKSL7LktsFgqzNj83MaivMGZzluWq32m25j2mDOjmSKX7GGWahePhuEs7P/g==", 1679 + "license": "MIT", 1680 + "peer": true, 1681 + "dependencies": { 1682 + "deepmerge": "^4.3.1", 1683 + "magic-string": "^0.30.21", 1684 + "obug": "^2.1.0", 1685 + "vitefu": "^1.1.2" 1686 + }, 1687 + "engines": { 1688 + "node": "^20.19 || ^22.12 || >=24" 1689 + }, 1690 + "peerDependencies": { 1691 + "svelte": "^5.46.4", 1692 + "vite": "^8.0.0-beta.7 || ^8.0.0" 1693 + } 1694 + }, 1695 + "node_modules/@tanstack/query-core": { 1696 + "version": "5.95.2", 1697 + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.95.2.tgz", 1698 + "integrity": "sha512-o4T8vZHZET4Bib3jZ/tCW9/7080urD4c+0/AUaYVpIqOsr7y0reBc1oX3ttNaSW5mYyvZHctiQ/UOP2PfdmFEQ==", 1699 + "license": "MIT", 1700 + "funding": { 1701 + "type": "github", 1702 + "url": "https://github.com/sponsors/tannerlinsley" 1703 + } 1704 + }, 1705 + "node_modules/@tanstack/svelte-query": { 1706 + "version": "6.1.10", 1707 + "resolved": "https://registry.npmjs.org/@tanstack/svelte-query/-/svelte-query-6.1.10.tgz", 1708 + "integrity": "sha512-YrRIIlTfmkkCiLwQ+bxFg/Ltil9RoRjiBlJHx0CBh5+9ZRbvKzBqYBncpGTEwW2AdSJfYimgoBABFmCa5q+6qw==", 1709 + "license": "MIT", 1710 + "dependencies": { 1711 + "@tanstack/query-core": "5.95.2" 1712 + }, 1713 + "funding": { 1714 + "type": "github", 1715 + "url": "https://github.com/sponsors/tannerlinsley" 1716 + }, 1717 + "peerDependencies": { 1718 + "svelte": "^5.25.0" 1719 + } 1720 + }, 1721 + "node_modules/@types/chai": { 1722 + "version": "5.2.3", 1723 + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", 1724 + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", 1725 + "license": "MIT", 1726 + "dependencies": { 1727 + "@types/deep-eql": "*", 1728 + "assertion-error": "^2.0.1" 1729 + } 1730 + }, 1731 + "node_modules/@types/cookie": { 1732 + "version": "0.6.0", 1733 + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", 1734 + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", 1735 + "license": "MIT" 1736 + }, 1737 + "node_modules/@types/deep-eql": { 1738 + "version": "4.0.2", 1739 + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", 1740 + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", 1741 + "license": "MIT" 1742 + }, 1743 + "node_modules/@types/estree": { 1744 + "version": "1.0.8", 1745 + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", 1746 + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", 1747 + "license": "MIT" 1748 + }, 1749 + "node_modules/@types/resolve": { 1750 + "version": "1.20.2", 1751 + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", 1752 + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", 1753 + "license": "MIT" 1754 + }, 1755 + "node_modules/@types/trusted-types": { 1756 + "version": "2.0.7", 1757 + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", 1758 + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", 1759 + "license": "MIT" 1760 + }, 1761 + "node_modules/@typescript-eslint/types": { 1762 + "version": "8.57.2", 1763 + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", 1764 + "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", 1765 + "license": "MIT", 1766 + "engines": { 1767 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1768 + }, 1769 + "funding": { 1770 + "type": "opencollective", 1771 + "url": "https://opencollective.com/typescript-eslint" 1772 + } 1773 + }, 1774 + "node_modules/@voidzero-dev/vite-plus-core": { 1775 + "version": "0.1.14", 1776 + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-core/-/vite-plus-core-0.1.14.tgz", 1777 + "integrity": "sha512-CCWzdkfW0fo0cQNlIsYp5fOuH2IwKuPZEb2UY2Z8gXcp5pG74A82H2Pthj0heAuvYTAnfT7kEC6zM+RbiBgQbg==", 1778 + "license": "MIT", 1779 + "dependencies": { 1780 + "@oxc-project/runtime": "=0.121.0", 1781 + "@oxc-project/types": "=0.122.0", 1782 + "lightningcss": "^1.30.2", 1783 + "postcss": "^8.5.6" 1784 + }, 1785 + "engines": { 1786 + "node": "^20.19.0 || >=22.12.0" 1787 + }, 1788 + "optionalDependencies": { 1789 + "fsevents": "~2.3.3" 1790 + }, 1791 + "peerDependencies": { 1792 + "@arethetypeswrong/core": "^0.18.1", 1793 + "@tsdown/css": "0.21.4", 1794 + "@tsdown/exe": "0.21.4", 1795 + "@types/node": "^20.19.0 || >=22.12.0", 1796 + "@vitejs/devtools": "^0.1.0", 1797 + "esbuild": "^0.27.0", 1798 + "jiti": ">=1.21.0", 1799 + "less": "^4.0.0", 1800 + "publint": "^0.3.0", 1801 + "sass": "^1.70.0", 1802 + "sass-embedded": "^1.70.0", 1803 + "stylus": ">=0.54.8", 1804 + "sugarss": "^5.0.0", 1805 + "terser": "^5.16.0", 1806 + "tsx": "^4.8.1", 1807 + "typescript": "^5.0.0", 1808 + "unplugin-unused": "^0.5.0", 1809 + "yaml": "^2.4.2" 1810 + }, 1811 + "peerDependenciesMeta": { 1812 + "@arethetypeswrong/core": { 1813 + "optional": true 1814 + }, 1815 + "@tsdown/css": { 1816 + "optional": true 1817 + }, 1818 + "@tsdown/exe": { 1819 + "optional": true 1820 + }, 1821 + "@types/node": { 1822 + "optional": true 1823 + }, 1824 + "@vitejs/devtools": { 1825 + "optional": true 1826 + }, 1827 + "esbuild": { 1828 + "optional": true 1829 + }, 1830 + "jiti": { 1831 + "optional": true 1832 + }, 1833 + "less": { 1834 + "optional": true 1835 + }, 1836 + "publint": { 1837 + "optional": true 1838 + }, 1839 + "sass": { 1840 + "optional": true 1841 + }, 1842 + "sass-embedded": { 1843 + "optional": true 1844 + }, 1845 + "stylus": { 1846 + "optional": true 1847 + }, 1848 + "sugarss": { 1849 + "optional": true 1850 + }, 1851 + "terser": { 1852 + "optional": true 1853 + }, 1854 + "tsx": { 1855 + "optional": true 1856 + }, 1857 + "typescript": { 1858 + "optional": true 1859 + }, 1860 + "unplugin-unused": { 1861 + "optional": true 1862 + }, 1863 + "yaml": { 1864 + "optional": true 1865 + } 1866 + } 1867 + }, 1868 + "node_modules/@voidzero-dev/vite-plus-darwin-arm64": { 1869 + "version": "0.1.14", 1870 + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-darwin-arm64/-/vite-plus-darwin-arm64-0.1.14.tgz", 1871 + "integrity": "sha512-q2ESUSbapwsxVRe/KevKATahNRraoX5nti3HT9S3266OHT5sMroBY14jaxTv74ekjQc9E6EPhyLGQWuWQuuBRw==", 1872 + "cpu": [ 1873 + "arm64" 1874 + ], 1875 + "dev": true, 1876 + "license": "MIT", 1877 + "optional": true, 1878 + "os": [ 1879 + "darwin" 1880 + ], 1881 + "engines": { 1882 + "node": "^20.19.0 || >=22.12.0" 1883 + } 1884 + }, 1885 + "node_modules/@voidzero-dev/vite-plus-darwin-x64": { 1886 + "version": "0.1.14", 1887 + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-darwin-x64/-/vite-plus-darwin-x64-0.1.14.tgz", 1888 + "integrity": "sha512-UpcDZc9G99E/4HDRoobvYHxMvFOG5uv3RwEcq0HF70u4DsnEMl1z8RaJLeWV7a09LGwj9Q+YWC3Z4INWnTLs8g==", 1889 + "cpu": [ 1890 + "x64" 1891 + ], 1892 + "dev": true, 1893 + "license": "MIT", 1894 + "optional": true, 1895 + "os": [ 1896 + "darwin" 1897 + ], 1898 + "engines": { 1899 + "node": "^20.19.0 || >=22.12.0" 1900 + } 1901 + }, 1902 + "node_modules/@voidzero-dev/vite-plus-linux-arm64-gnu": { 1903 + "version": "0.1.14", 1904 + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-linux-arm64-gnu/-/vite-plus-linux-arm64-gnu-0.1.14.tgz", 1905 + "integrity": "sha512-GIjn35RABUEDB9gHD26nRq7T72Te+Qy2+NIzogwEaUE728PvPkatF5gMCeF4sigCoc8c4qxDwsG+A2A2LYGnDg==", 1906 + "cpu": [ 1907 + "arm64" 1908 + ], 1909 + "dev": true, 1910 + "license": "MIT", 1911 + "optional": true, 1912 + "os": [ 1913 + "linux" 1914 + ], 1915 + "engines": { 1916 + "node": "^20.19.0 || >=22.12.0" 1917 + } 1918 + }, 1919 + "node_modules/@voidzero-dev/vite-plus-linux-arm64-musl": { 1920 + "version": "0.1.14", 1921 + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-linux-arm64-musl/-/vite-plus-linux-arm64-musl-0.1.14.tgz", 1922 + "integrity": "sha512-qo2RToGirG0XCcxZ2AEOuonLM256z6dNbJzDDIo5gWYA+cIKigFQJbkPyr25zsT1tsP2aY0OTxt2038XbVlRkQ==", 1923 + "cpu": [ 1924 + "arm64" 1925 + ], 1926 + "dev": true, 1927 + "license": "MIT", 1928 + "optional": true, 1929 + "os": [ 1930 + "linux" 1931 + ], 1932 + "engines": { 1933 + "node": "^20.19.0 || >=22.12.0" 1934 + } 1935 + }, 1936 + "node_modules/@voidzero-dev/vite-plus-linux-x64-gnu": { 1937 + "version": "0.1.14", 1938 + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-linux-x64-gnu/-/vite-plus-linux-x64-gnu-0.1.14.tgz", 1939 + "integrity": "sha512-BsMWKZfdfGcYLxxLyaePpg6NW54xqzzcfq8sFUwKfwby0kgOKQ4WymUXyBvO9nnBb0ZPsJQrV0sx+Onac/LTaw==", 1940 + "cpu": [ 1941 + "x64" 1942 + ], 1943 + "dev": true, 1944 + "license": "MIT", 1945 + "optional": true, 1946 + "os": [ 1947 + "linux" 1948 + ], 1949 + "engines": { 1950 + "node": "^20.19.0 || >=22.12.0" 1951 + } 1952 + }, 1953 + "node_modules/@voidzero-dev/vite-plus-linux-x64-musl": { 1954 + "version": "0.1.14", 1955 + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-linux-x64-musl/-/vite-plus-linux-x64-musl-0.1.14.tgz", 1956 + "integrity": "sha512-mOrEpj7ntW9RopGbcOYG/L0pOs0qHzUG4Vz7NXbuf4dbOSlY4JjyoMOIWxjKQORQht02Hzuf8YrMGNwa6AjVSQ==", 1957 + "cpu": [ 1958 + "x64" 1959 + ], 1960 + "dev": true, 1961 + "license": "MIT", 1962 + "optional": true, 1963 + "os": [ 1964 + "linux" 1965 + ], 1966 + "engines": { 1967 + "node": "^20.19.0 || >=22.12.0" 1968 + } 1969 + }, 1970 + "node_modules/@voidzero-dev/vite-plus-test": { 1971 + "version": "0.1.14", 1972 + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-test/-/vite-plus-test-0.1.14.tgz", 1973 + "integrity": "sha512-rjF+qpYD+5+THOJZ3gbE3+cxsk5sW7nJ0ODK7y6ZKeS4amREUMedEDYykzKBwR7OZDC/WwE90A0iLWCr6qAXhA==", 1974 + "dev": true, 1975 + "license": "MIT", 1976 + "dependencies": { 1977 + "@standard-schema/spec": "^1.1.0", 1978 + "@types/chai": "^5.2.2", 1979 + "@voidzero-dev/vite-plus-core": "0.1.14", 1980 + "es-module-lexer": "^1.7.0", 1981 + "obug": "^2.1.1", 1982 + "pixelmatch": "^7.1.0", 1983 + "pngjs": "^7.0.0", 1984 + "sirv": "^3.0.2", 1985 + "std-env": "^4.0.0", 1986 + "tinybench": "^2.9.0", 1987 + "tinyexec": "^1.0.2", 1988 + "tinyglobby": "^0.2.15", 1989 + "ws": "^8.18.3" 1990 + }, 1991 + "engines": { 1992 + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" 1993 + }, 1994 + "peerDependencies": { 1995 + "@edge-runtime/vm": "*", 1996 + "@opentelemetry/api": "^1.9.0", 1997 + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", 1998 + "@vitest/ui": "4.1.1", 1999 + "happy-dom": "*", 2000 + "jsdom": "*", 2001 + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" 2002 + }, 2003 + "peerDependenciesMeta": { 2004 + "@edge-runtime/vm": { 2005 + "optional": true 2006 + }, 2007 + "@opentelemetry/api": { 2008 + "optional": true 2009 + }, 2010 + "@types/node": { 2011 + "optional": true 2012 + }, 2013 + "@vitest/ui": { 2014 + "optional": true 2015 + }, 2016 + "happy-dom": { 2017 + "optional": true 2018 + }, 2019 + "jsdom": { 2020 + "optional": true 2021 + }, 2022 + "vite": { 2023 + "optional": false 2024 + } 2025 + } 2026 + }, 2027 + "node_modules/@voidzero-dev/vite-plus-win32-arm64-msvc": { 2028 + "version": "0.1.14", 2029 + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-win32-arm64-msvc/-/vite-plus-win32-arm64-msvc-0.1.14.tgz", 2030 + "integrity": "sha512-7iC+Ig+8D/zACy0IJf7w/vQ7duTjux9Ttmm3KOBdVWH4dl3JihydA7+SQVMhz71a4WiqJ6nPidoG8D6hUP4MVQ==", 2031 + "cpu": [ 2032 + "arm64" 2033 + ], 2034 + "dev": true, 2035 + "license": "MIT", 2036 + "optional": true, 2037 + "os": [ 2038 + "win32" 2039 + ], 2040 + "engines": { 2041 + "node": "^20.19.0 || >=22.12.0" 2042 + } 2043 + }, 2044 + "node_modules/@voidzero-dev/vite-plus-win32-x64-msvc": { 2045 + "version": "0.1.14", 2046 + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-win32-x64-msvc/-/vite-plus-win32-x64-msvc-0.1.14.tgz", 2047 + "integrity": "sha512-yRJ/8yAYFluNHx0Ej6Kevx65MIeM3wFKklnxosVZRlz2ZRL1Ea1Qh3tWATr3Ipk1ciRxBv8KJgp6zXqjxtZSoQ==", 2048 + "cpu": [ 2049 + "x64" 2050 + ], 2051 + "dev": true, 2052 + "license": "MIT", 2053 + "optional": true, 2054 + "os": [ 2055 + "win32" 2056 + ], 2057 + "engines": { 2058 + "node": "^20.19.0 || >=22.12.0" 2059 + } 2060 + }, 2061 + "node_modules/acorn": { 2062 + "version": "8.16.0", 2063 + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", 2064 + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", 2065 + "license": "MIT", 2066 + "bin": { 2067 + "acorn": "bin/acorn" 2068 + }, 2069 + "engines": { 2070 + "node": ">=0.4.0" 2071 + } 2072 + }, 2073 + "node_modules/aria-query": { 2074 + "version": "5.3.1", 2075 + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", 2076 + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", 2077 + "license": "Apache-2.0", 2078 + "engines": { 2079 + "node": ">= 0.4" 2080 + } 2081 + }, 2082 + "node_modules/assertion-error": { 2083 + "version": "2.0.1", 2084 + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", 2085 + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", 2086 + "license": "MIT", 2087 + "engines": { 2088 + "node": ">=12" 2089 + } 2090 + }, 2091 + "node_modules/axobject-query": { 2092 + "version": "4.1.0", 2093 + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", 2094 + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", 2095 + "license": "Apache-2.0", 2096 + "engines": { 2097 + "node": ">= 0.4" 2098 + } 2099 + }, 2100 + "node_modules/base64-js": { 2101 + "version": "0.0.8", 2102 + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", 2103 + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", 2104 + "license": "MIT", 2105 + "engines": { 2106 + "node": ">= 0.4" 2107 + } 2108 + }, 2109 + "node_modules/better-sqlite3": { 2110 + "version": "12.8.0", 2111 + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.8.0.tgz", 2112 + "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", 2113 + "hasInstallScript": true, 2114 + "license": "MIT", 2115 + "dependencies": { 2116 + "bindings": "^1.5.0", 2117 + "prebuild-install": "^7.1.1" 2118 + }, 2119 + "engines": { 2120 + "node": "20.x || 22.x || 23.x || 24.x || 25.x" 2121 + } 2122 + }, 2123 + "node_modules/bindings": { 2124 + "version": "1.5.0", 2125 + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", 2126 + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", 2127 + "license": "MIT", 2128 + "dependencies": { 2129 + "file-uri-to-path": "1.0.0" 2130 + } 2131 + }, 2132 + "node_modules/bl": { 2133 + "version": "4.1.0", 2134 + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", 2135 + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", 2136 + "license": "MIT", 2137 + "dependencies": { 2138 + "buffer": "^5.5.0", 2139 + "inherits": "^2.0.4", 2140 + "readable-stream": "^3.4.0" 2141 + } 2142 + }, 2143 + "node_modules/buffer": { 2144 + "version": "5.7.1", 2145 + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", 2146 + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", 2147 + "funding": [ 2148 + { 2149 + "type": "github", 2150 + "url": "https://github.com/sponsors/feross" 2151 + }, 2152 + { 2153 + "type": "patreon", 2154 + "url": "https://www.patreon.com/feross" 2155 + }, 2156 + { 2157 + "type": "consulting", 2158 + "url": "https://feross.org/support" 2159 + } 2160 + ], 2161 + "license": "MIT", 2162 + "dependencies": { 2163 + "base64-js": "^1.3.1", 2164 + "ieee754": "^1.1.13" 2165 + } 2166 + }, 2167 + "node_modules/buffer/node_modules/base64-js": { 2168 + "version": "1.5.1", 2169 + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 2170 + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", 2171 + "funding": [ 2172 + { 2173 + "type": "github", 2174 + "url": "https://github.com/sponsors/feross" 2175 + }, 2176 + { 2177 + "type": "patreon", 2178 + "url": "https://www.patreon.com/feross" 2179 + }, 2180 + { 2181 + "type": "consulting", 2182 + "url": "https://feross.org/support" 2183 + } 2184 + ], 2185 + "license": "MIT" 2186 + }, 2187 + "node_modules/cac": { 2188 + "version": "7.0.0", 2189 + "resolved": "https://registry.npmjs.org/cac/-/cac-7.0.0.tgz", 2190 + "integrity": "sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==", 2191 + "dev": true, 2192 + "license": "MIT", 2193 + "engines": { 2194 + "node": ">=20.19.0" 2195 + } 2196 + }, 2197 + "node_modules/camelize": { 2198 + "version": "1.0.1", 2199 + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", 2200 + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", 2201 + "license": "MIT", 2202 + "funding": { 2203 + "url": "https://github.com/sponsors/ljharb" 2204 + } 2205 + }, 2206 + "node_modules/chokidar": { 2207 + "version": "4.0.3", 2208 + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", 2209 + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", 2210 + "dev": true, 2211 + "license": "MIT", 2212 + "dependencies": { 2213 + "readdirp": "^4.0.1" 2214 + }, 2215 + "engines": { 2216 + "node": ">= 14.16.0" 2217 + }, 2218 + "funding": { 2219 + "url": "https://paulmillr.com/funding/" 2220 + } 2221 + }, 2222 + "node_modules/chownr": { 2223 + "version": "1.1.4", 2224 + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", 2225 + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", 2226 + "license": "ISC" 2227 + }, 2228 + "node_modules/clsx": { 2229 + "version": "2.1.1", 2230 + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", 2231 + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", 2232 + "license": "MIT", 2233 + "engines": { 2234 + "node": ">=6" 2235 + } 2236 + }, 2237 + "node_modules/color-name": { 2238 + "version": "1.1.4", 2239 + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 2240 + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 2241 + "license": "MIT" 2242 + }, 2243 + "node_modules/commondir": { 2244 + "version": "1.0.1", 2245 + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", 2246 + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", 2247 + "license": "MIT" 2248 + }, 2249 + "node_modules/cookie": { 2250 + "version": "0.6.0", 2251 + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", 2252 + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", 2253 + "license": "MIT", 2254 + "engines": { 2255 + "node": ">= 0.6" 2256 + } 2257 + }, 2258 + "node_modules/cross-spawn": { 2259 + "version": "7.0.6", 2260 + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", 2261 + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", 2262 + "dev": true, 2263 + "license": "MIT", 2264 + "dependencies": { 2265 + "path-key": "^3.1.0", 2266 + "shebang-command": "^2.0.0", 2267 + "which": "^2.0.1" 2268 + }, 2269 + "engines": { 2270 + "node": ">= 8" 2271 + } 2272 + }, 2273 + "node_modules/css-background-parser": { 2274 + "version": "0.1.0", 2275 + "resolved": "https://registry.npmjs.org/css-background-parser/-/css-background-parser-0.1.0.tgz", 2276 + "integrity": "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==", 2277 + "license": "MIT" 2278 + }, 2279 + "node_modules/css-box-shadow": { 2280 + "version": "1.0.0-3", 2281 + "resolved": "https://registry.npmjs.org/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz", 2282 + "integrity": "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==", 2283 + "license": "MIT" 2284 + }, 2285 + "node_modules/css-color-keywords": { 2286 + "version": "1.0.0", 2287 + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", 2288 + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", 2289 + "license": "ISC", 2290 + "engines": { 2291 + "node": ">=4" 2292 + } 2293 + }, 2294 + "node_modules/css-gradient-parser": { 2295 + "version": "0.0.17", 2296 + "resolved": "https://registry.npmjs.org/css-gradient-parser/-/css-gradient-parser-0.0.17.tgz", 2297 + "integrity": "sha512-w2Xy9UMMwlKtou0vlRnXvWglPAceXCTtcmVSo8ZBUvqCV5aXEFP/PC6d+I464810I9FT++UACwTD5511bmGPUg==", 2298 + "license": "MIT", 2299 + "engines": { 2300 + "node": ">=16" 2301 + } 2302 + }, 2303 + "node_modules/css-to-react-native": { 2304 + "version": "3.2.0", 2305 + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", 2306 + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", 2307 + "license": "MIT", 2308 + "dependencies": { 2309 + "camelize": "^1.0.0", 2310 + "css-color-keywords": "^1.0.0", 2311 + "postcss-value-parser": "^4.0.2" 2312 + } 2313 + }, 2314 + "node_modules/decompress-response": { 2315 + "version": "6.0.0", 2316 + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", 2317 + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", 2318 + "license": "MIT", 2319 + "dependencies": { 2320 + "mimic-response": "^3.1.0" 2321 + }, 2322 + "engines": { 2323 + "node": ">=10" 2324 + }, 2325 + "funding": { 2326 + "url": "https://github.com/sponsors/sindresorhus" 2327 + } 2328 + }, 2329 + "node_modules/deep-extend": { 2330 + "version": "0.6.0", 2331 + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", 2332 + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", 2333 + "license": "MIT", 2334 + "engines": { 2335 + "node": ">=4.0.0" 2336 + } 2337 + }, 2338 + "node_modules/deepmerge": { 2339 + "version": "4.3.1", 2340 + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", 2341 + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", 2342 + "license": "MIT", 2343 + "engines": { 2344 + "node": ">=0.10.0" 2345 + } 2346 + }, 2347 + "node_modules/detect-libc": { 2348 + "version": "2.1.2", 2349 + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", 2350 + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", 2351 + "license": "Apache-2.0", 2352 + "engines": { 2353 + "node": ">=8" 2354 + } 2355 + }, 2356 + "node_modules/devalue": { 2357 + "version": "5.6.4", 2358 + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz", 2359 + "integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==", 2360 + "license": "MIT" 2361 + }, 2362 + "node_modules/emoji-regex-xs": { 2363 + "version": "2.0.1", 2364 + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-2.0.1.tgz", 2365 + "integrity": "sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==", 2366 + "license": "MIT", 2367 + "engines": { 2368 + "node": ">=10.0.0" 2369 + } 2370 + }, 2371 + "node_modules/end-of-stream": { 2372 + "version": "1.4.5", 2373 + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", 2374 + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", 2375 + "license": "MIT", 2376 + "dependencies": { 2377 + "once": "^1.4.0" 2378 + } 2379 + }, 2380 + "node_modules/es-module-lexer": { 2381 + "version": "1.7.0", 2382 + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", 2383 + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", 2384 + "license": "MIT" 2385 + }, 2386 + "node_modules/escape-html": { 2387 + "version": "1.0.3", 2388 + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 2389 + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", 2390 + "license": "MIT" 2391 + }, 2392 + "node_modules/esm-env": { 2393 + "version": "1.2.2", 2394 + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", 2395 + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", 2396 + "license": "MIT" 2397 + }, 2398 + "node_modules/esrap": { 2399 + "version": "2.2.4", 2400 + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.4.tgz", 2401 + "integrity": "sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg==", 2402 + "license": "MIT", 2403 + "dependencies": { 2404 + "@jridgewell/sourcemap-codec": "^1.4.15", 2405 + "@typescript-eslint/types": "^8.2.0" 2406 + } 2407 + }, 2408 + "node_modules/estree-walker": { 2409 + "version": "2.0.2", 2410 + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", 2411 + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", 2412 + "license": "MIT" 2413 + }, 2414 + "node_modules/expand-template": { 2415 + "version": "2.0.3", 2416 + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", 2417 + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", 2418 + "license": "(MIT OR WTFPL)", 2419 + "engines": { 2420 + "node": ">=6" 2421 + } 2422 + }, 2423 + "node_modules/fdir": { 2424 + "version": "6.5.0", 2425 + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", 2426 + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", 2427 + "license": "MIT", 2428 + "engines": { 2429 + "node": ">=12.0.0" 2430 + }, 2431 + "peerDependencies": { 2432 + "picomatch": "^3 || ^4" 2433 + }, 2434 + "peerDependenciesMeta": { 2435 + "picomatch": { 2436 + "optional": true 2437 + } 2438 + } 2439 + }, 2440 + "node_modules/fflate": { 2441 + "version": "0.7.4", 2442 + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz", 2443 + "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==", 2444 + "license": "MIT" 2445 + }, 2446 + "node_modules/file-uri-to-path": { 2447 + "version": "1.0.0", 2448 + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", 2449 + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", 2450 + "license": "MIT" 2451 + }, 2452 + "node_modules/fs-constants": { 2453 + "version": "1.0.0", 2454 + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", 2455 + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", 2456 + "license": "MIT" 2457 + }, 2458 + "node_modules/fsevents": { 2459 + "version": "2.3.3", 2460 + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 2461 + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 2462 + "hasInstallScript": true, 2463 + "license": "MIT", 2464 + "optional": true, 2465 + "os": [ 2466 + "darwin" 2467 + ], 2468 + "engines": { 2469 + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 2470 + } 2471 + }, 2472 + "node_modules/function-bind": { 2473 + "version": "1.1.2", 2474 + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 2475 + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 2476 + "license": "MIT", 2477 + "funding": { 2478 + "url": "https://github.com/sponsors/ljharb" 2479 + } 2480 + }, 2481 + "node_modules/github-from-package": { 2482 + "version": "0.0.0", 2483 + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", 2484 + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", 2485 + "license": "MIT" 2486 + }, 2487 + "node_modules/hasown": { 2488 + "version": "2.0.2", 2489 + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 2490 + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 2491 + "license": "MIT", 2492 + "dependencies": { 2493 + "function-bind": "^1.1.2" 2494 + }, 2495 + "engines": { 2496 + "node": ">= 0.4" 2497 + } 2498 + }, 2499 + "node_modules/hex-rgb": { 2500 + "version": "4.3.0", 2501 + "resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz", 2502 + "integrity": "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==", 2503 + "license": "MIT", 2504 + "engines": { 2505 + "node": ">=6" 2506 + }, 2507 + "funding": { 2508 + "url": "https://github.com/sponsors/sindresorhus" 2509 + } 2510 + }, 2511 + "node_modules/ieee754": { 2512 + "version": "1.2.1", 2513 + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 2514 + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", 2515 + "funding": [ 2516 + { 2517 + "type": "github", 2518 + "url": "https://github.com/sponsors/feross" 2519 + }, 2520 + { 2521 + "type": "patreon", 2522 + "url": "https://www.patreon.com/feross" 2523 + }, 2524 + { 2525 + "type": "consulting", 2526 + "url": "https://feross.org/support" 2527 + } 2528 + ], 2529 + "license": "BSD-3-Clause" 2530 + }, 2531 + "node_modules/inherits": { 2532 + "version": "2.0.4", 2533 + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 2534 + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 2535 + "license": "ISC" 2536 + }, 2537 + "node_modules/ini": { 2538 + "version": "1.3.8", 2539 + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", 2540 + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", 2541 + "license": "ISC" 2542 + }, 2543 + "node_modules/is-core-module": { 2544 + "version": "2.16.1", 2545 + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", 2546 + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", 2547 + "license": "MIT", 2548 + "dependencies": { 2549 + "hasown": "^2.0.2" 2550 + }, 2551 + "engines": { 2552 + "node": ">= 0.4" 2553 + }, 2554 + "funding": { 2555 + "url": "https://github.com/sponsors/ljharb" 2556 + } 2557 + }, 2558 + "node_modules/is-module": { 2559 + "version": "1.0.0", 2560 + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", 2561 + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", 2562 + "license": "MIT" 2563 + }, 2564 + "node_modules/is-reference": { 2565 + "version": "1.2.1", 2566 + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", 2567 + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", 2568 + "license": "MIT", 2569 + "dependencies": { 2570 + "@types/estree": "*" 2571 + } 2572 + }, 2573 + "node_modules/isexe": { 2574 + "version": "2.0.0", 2575 + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 2576 + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 2577 + "dev": true, 2578 + "license": "ISC" 2579 + }, 2580 + "node_modules/kleur": { 2581 + "version": "4.1.5", 2582 + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", 2583 + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", 2584 + "license": "MIT", 2585 + "engines": { 2586 + "node": ">=6" 2587 + } 2588 + }, 2589 + "node_modules/lightningcss": { 2590 + "version": "1.32.0", 2591 + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", 2592 + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", 2593 + "license": "MPL-2.0", 2594 + "dependencies": { 2595 + "detect-libc": "^2.0.3" 2596 + }, 2597 + "engines": { 2598 + "node": ">= 12.0.0" 2599 + }, 2600 + "funding": { 2601 + "type": "opencollective", 2602 + "url": "https://opencollective.com/parcel" 2603 + }, 2604 + "optionalDependencies": { 2605 + "lightningcss-android-arm64": "1.32.0", 2606 + "lightningcss-darwin-arm64": "1.32.0", 2607 + "lightningcss-darwin-x64": "1.32.0", 2608 + "lightningcss-freebsd-x64": "1.32.0", 2609 + "lightningcss-linux-arm-gnueabihf": "1.32.0", 2610 + "lightningcss-linux-arm64-gnu": "1.32.0", 2611 + "lightningcss-linux-arm64-musl": "1.32.0", 2612 + "lightningcss-linux-x64-gnu": "1.32.0", 2613 + "lightningcss-linux-x64-musl": "1.32.0", 2614 + "lightningcss-win32-arm64-msvc": "1.32.0", 2615 + "lightningcss-win32-x64-msvc": "1.32.0" 2616 + } 2617 + }, 2618 + "node_modules/lightningcss-android-arm64": { 2619 + "version": "1.32.0", 2620 + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", 2621 + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", 2622 + "cpu": [ 2623 + "arm64" 2624 + ], 2625 + "license": "MPL-2.0", 2626 + "optional": true, 2627 + "os": [ 2628 + "android" 2629 + ], 2630 + "engines": { 2631 + "node": ">= 12.0.0" 2632 + }, 2633 + "funding": { 2634 + "type": "opencollective", 2635 + "url": "https://opencollective.com/parcel" 2636 + } 2637 + }, 2638 + "node_modules/lightningcss-darwin-arm64": { 2639 + "version": "1.32.0", 2640 + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", 2641 + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", 2642 + "cpu": [ 2643 + "arm64" 2644 + ], 2645 + "license": "MPL-2.0", 2646 + "optional": true, 2647 + "os": [ 2648 + "darwin" 2649 + ], 2650 + "engines": { 2651 + "node": ">= 12.0.0" 2652 + }, 2653 + "funding": { 2654 + "type": "opencollective", 2655 + "url": "https://opencollective.com/parcel" 2656 + } 2657 + }, 2658 + "node_modules/lightningcss-darwin-x64": { 2659 + "version": "1.32.0", 2660 + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", 2661 + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", 2662 + "cpu": [ 2663 + "x64" 2664 + ], 2665 + "license": "MPL-2.0", 2666 + "optional": true, 2667 + "os": [ 2668 + "darwin" 2669 + ], 2670 + "engines": { 2671 + "node": ">= 12.0.0" 2672 + }, 2673 + "funding": { 2674 + "type": "opencollective", 2675 + "url": "https://opencollective.com/parcel" 2676 + } 2677 + }, 2678 + "node_modules/lightningcss-freebsd-x64": { 2679 + "version": "1.32.0", 2680 + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", 2681 + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", 2682 + "cpu": [ 2683 + "x64" 2684 + ], 2685 + "license": "MPL-2.0", 2686 + "optional": true, 2687 + "os": [ 2688 + "freebsd" 2689 + ], 2690 + "engines": { 2691 + "node": ">= 12.0.0" 2692 + }, 2693 + "funding": { 2694 + "type": "opencollective", 2695 + "url": "https://opencollective.com/parcel" 2696 + } 2697 + }, 2698 + "node_modules/lightningcss-linux-arm-gnueabihf": { 2699 + "version": "1.32.0", 2700 + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", 2701 + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", 2702 + "cpu": [ 2703 + "arm" 2704 + ], 2705 + "license": "MPL-2.0", 2706 + "optional": true, 2707 + "os": [ 2708 + "linux" 2709 + ], 2710 + "engines": { 2711 + "node": ">= 12.0.0" 2712 + }, 2713 + "funding": { 2714 + "type": "opencollective", 2715 + "url": "https://opencollective.com/parcel" 2716 + } 2717 + }, 2718 + "node_modules/lightningcss-linux-arm64-gnu": { 2719 + "version": "1.32.0", 2720 + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", 2721 + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", 2722 + "cpu": [ 2723 + "arm64" 2724 + ], 2725 + "license": "MPL-2.0", 2726 + "optional": true, 2727 + "os": [ 2728 + "linux" 2729 + ], 2730 + "engines": { 2731 + "node": ">= 12.0.0" 2732 + }, 2733 + "funding": { 2734 + "type": "opencollective", 2735 + "url": "https://opencollective.com/parcel" 2736 + } 2737 + }, 2738 + "node_modules/lightningcss-linux-arm64-musl": { 2739 + "version": "1.32.0", 2740 + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", 2741 + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", 2742 + "cpu": [ 2743 + "arm64" 2744 + ], 2745 + "license": "MPL-2.0", 2746 + "optional": true, 2747 + "os": [ 2748 + "linux" 2749 + ], 2750 + "engines": { 2751 + "node": ">= 12.0.0" 2752 + }, 2753 + "funding": { 2754 + "type": "opencollective", 2755 + "url": "https://opencollective.com/parcel" 2756 + } 2757 + }, 2758 + "node_modules/lightningcss-linux-x64-gnu": { 2759 + "version": "1.32.0", 2760 + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", 2761 + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", 2762 + "cpu": [ 2763 + "x64" 2764 + ], 2765 + "license": "MPL-2.0", 2766 + "optional": true, 2767 + "os": [ 2768 + "linux" 2769 + ], 2770 + "engines": { 2771 + "node": ">= 12.0.0" 2772 + }, 2773 + "funding": { 2774 + "type": "opencollective", 2775 + "url": "https://opencollective.com/parcel" 2776 + } 2777 + }, 2778 + "node_modules/lightningcss-linux-x64-musl": { 2779 + "version": "1.32.0", 2780 + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", 2781 + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", 2782 + "cpu": [ 2783 + "x64" 2784 + ], 2785 + "license": "MPL-2.0", 2786 + "optional": true, 2787 + "os": [ 2788 + "linux" 2789 + ], 2790 + "engines": { 2791 + "node": ">= 12.0.0" 2792 + }, 2793 + "funding": { 2794 + "type": "opencollective", 2795 + "url": "https://opencollective.com/parcel" 2796 + } 2797 + }, 2798 + "node_modules/lightningcss-win32-arm64-msvc": { 2799 + "version": "1.32.0", 2800 + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", 2801 + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", 2802 + "cpu": [ 2803 + "arm64" 2804 + ], 2805 + "license": "MPL-2.0", 2806 + "optional": true, 2807 + "os": [ 2808 + "win32" 2809 + ], 2810 + "engines": { 2811 + "node": ">= 12.0.0" 2812 + }, 2813 + "funding": { 2814 + "type": "opencollective", 2815 + "url": "https://opencollective.com/parcel" 2816 + } 2817 + }, 2818 + "node_modules/lightningcss-win32-x64-msvc": { 2819 + "version": "1.32.0", 2820 + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", 2821 + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", 2822 + "cpu": [ 2823 + "x64" 2824 + ], 2825 + "license": "MPL-2.0", 2826 + "optional": true, 2827 + "os": [ 2828 + "win32" 2829 + ], 2830 + "engines": { 2831 + "node": ">= 12.0.0" 2832 + }, 2833 + "funding": { 2834 + "type": "opencollective", 2835 + "url": "https://opencollective.com/parcel" 2836 + } 2837 + }, 2838 + "node_modules/linebreak": { 2839 + "version": "1.1.0", 2840 + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", 2841 + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", 2842 + "license": "MIT", 2843 + "dependencies": { 2844 + "base64-js": "0.0.8", 2845 + "unicode-trie": "^2.0.0" 2846 + } 2847 + }, 2848 + "node_modules/locate-character": { 2849 + "version": "3.0.0", 2850 + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", 2851 + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", 2852 + "license": "MIT" 2853 + }, 2854 + "node_modules/lucide-svelte": { 2855 + "version": "0.576.0", 2856 + "resolved": "https://registry.npmjs.org/lucide-svelte/-/lucide-svelte-0.576.0.tgz", 2857 + "integrity": "sha512-bm7RCoptI8unoEyo9H9sRHTHgnleuBW8npge05ZtxHkNsDNnO3p/BQEU79sshf4k+MSrjqlWvsCN5vVZtgV7ww==", 2858 + "license": "ISC", 2859 + "peerDependencies": { 2860 + "svelte": "^3 || ^4 || ^5.0.0-next.42" 2861 + } 2862 + }, 2863 + "node_modules/magic-string": { 2864 + "version": "0.30.21", 2865 + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", 2866 + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", 2867 + "license": "MIT", 2868 + "dependencies": { 2869 + "@jridgewell/sourcemap-codec": "^1.5.5" 2870 + } 2871 + }, 2872 + "node_modules/mimic-response": { 2873 + "version": "3.1.0", 2874 + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", 2875 + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", 2876 + "license": "MIT", 2877 + "engines": { 2878 + "node": ">=10" 2879 + }, 2880 + "funding": { 2881 + "url": "https://github.com/sponsors/sindresorhus" 2882 + } 2883 + }, 2884 + "node_modules/minimist": { 2885 + "version": "1.2.8", 2886 + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", 2887 + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", 2888 + "license": "MIT", 2889 + "funding": { 2890 + "url": "https://github.com/sponsors/ljharb" 2891 + } 2892 + }, 2893 + "node_modules/mkdirp-classic": { 2894 + "version": "0.5.3", 2895 + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", 2896 + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", 2897 + "license": "MIT" 2898 + }, 2899 + "node_modules/mri": { 2900 + "version": "1.2.0", 2901 + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", 2902 + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", 2903 + "dev": true, 2904 + "license": "MIT", 2905 + "engines": { 2906 + "node": ">=4" 2907 + } 2908 + }, 2909 + "node_modules/mrmime": { 2910 + "version": "2.0.1", 2911 + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", 2912 + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", 2913 + "license": "MIT", 2914 + "engines": { 2915 + "node": ">=10" 2916 + } 2917 + }, 2918 + "node_modules/nanoid": { 2919 + "version": "3.3.11", 2920 + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", 2921 + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", 2922 + "funding": [ 2923 + { 2924 + "type": "github", 2925 + "url": "https://github.com/sponsors/ai" 2926 + } 2927 + ], 2928 + "license": "MIT", 2929 + "bin": { 2930 + "nanoid": "bin/nanoid.cjs" 2931 + }, 2932 + "engines": { 2933 + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 2934 + } 2935 + }, 2936 + "node_modules/napi-build-utils": { 2937 + "version": "2.0.0", 2938 + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", 2939 + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", 2940 + "license": "MIT" 2941 + }, 2942 + "node_modules/node-abi": { 2943 + "version": "3.89.0", 2944 + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", 2945 + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", 2946 + "license": "MIT", 2947 + "dependencies": { 2948 + "semver": "^7.3.5" 2949 + }, 2950 + "engines": { 2951 + "node": ">=10" 2952 + } 2953 + }, 2954 + "node_modules/obug": { 2955 + "version": "2.1.1", 2956 + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", 2957 + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", 2958 + "funding": [ 2959 + "https://github.com/sponsors/sxzz", 2960 + "https://opencollective.com/debug" 2961 + ], 2962 + "license": "MIT" 2963 + }, 2964 + "node_modules/once": { 2965 + "version": "1.4.0", 2966 + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 2967 + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 2968 + "license": "ISC", 2969 + "dependencies": { 2970 + "wrappy": "1" 2971 + } 2972 + }, 2973 + "node_modules/oxfmt": { 2974 + "version": "0.42.0", 2975 + "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.42.0.tgz", 2976 + "integrity": "sha512-QhejGErLSMReNuZ6vxgFHDyGoPbjTRNi6uGHjy0cvIjOQFqD6xmr/T+3L41ixR3NIgzcNiJ6ylQKpvShTgDfqg==", 2977 + "dev": true, 2978 + "license": "MIT", 2979 + "dependencies": { 2980 + "tinypool": "2.1.0" 2981 + }, 2982 + "bin": { 2983 + "oxfmt": "bin/oxfmt" 2984 + }, 2985 + "engines": { 2986 + "node": "^20.19.0 || >=22.12.0" 2987 + }, 2988 + "funding": { 2989 + "url": "https://github.com/sponsors/Boshen" 2990 + }, 2991 + "optionalDependencies": { 2992 + "@oxfmt/binding-android-arm-eabi": "0.42.0", 2993 + "@oxfmt/binding-android-arm64": "0.42.0", 2994 + "@oxfmt/binding-darwin-arm64": "0.42.0", 2995 + "@oxfmt/binding-darwin-x64": "0.42.0", 2996 + "@oxfmt/binding-freebsd-x64": "0.42.0", 2997 + "@oxfmt/binding-linux-arm-gnueabihf": "0.42.0", 2998 + "@oxfmt/binding-linux-arm-musleabihf": "0.42.0", 2999 + "@oxfmt/binding-linux-arm64-gnu": "0.42.0", 3000 + "@oxfmt/binding-linux-arm64-musl": "0.42.0", 3001 + "@oxfmt/binding-linux-ppc64-gnu": "0.42.0", 3002 + "@oxfmt/binding-linux-riscv64-gnu": "0.42.0", 3003 + "@oxfmt/binding-linux-riscv64-musl": "0.42.0", 3004 + "@oxfmt/binding-linux-s390x-gnu": "0.42.0", 3005 + "@oxfmt/binding-linux-x64-gnu": "0.42.0", 3006 + "@oxfmt/binding-linux-x64-musl": "0.42.0", 3007 + "@oxfmt/binding-openharmony-arm64": "0.42.0", 3008 + "@oxfmt/binding-win32-arm64-msvc": "0.42.0", 3009 + "@oxfmt/binding-win32-ia32-msvc": "0.42.0", 3010 + "@oxfmt/binding-win32-x64-msvc": "0.42.0" 3011 + } 3012 + }, 3013 + "node_modules/oxlint": { 3014 + "version": "1.57.0", 3015 + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.57.0.tgz", 3016 + "integrity": "sha512-DGFsuBX5MFZX9yiDdtKjTrYPq45CZ8Fft6qCltJITYZxfwYjVdGf/6wycGYTACloauwIPxUnYhBVeZbHvleGhw==", 3017 + "dev": true, 3018 + "license": "MIT", 3019 + "bin": { 3020 + "oxlint": "bin/oxlint" 3021 + }, 3022 + "engines": { 3023 + "node": "^20.19.0 || >=22.12.0" 3024 + }, 3025 + "funding": { 3026 + "url": "https://github.com/sponsors/Boshen" 3027 + }, 3028 + "optionalDependencies": { 3029 + "@oxlint/binding-android-arm-eabi": "1.57.0", 3030 + "@oxlint/binding-android-arm64": "1.57.0", 3031 + "@oxlint/binding-darwin-arm64": "1.57.0", 3032 + "@oxlint/binding-darwin-x64": "1.57.0", 3033 + "@oxlint/binding-freebsd-x64": "1.57.0", 3034 + "@oxlint/binding-linux-arm-gnueabihf": "1.57.0", 3035 + "@oxlint/binding-linux-arm-musleabihf": "1.57.0", 3036 + "@oxlint/binding-linux-arm64-gnu": "1.57.0", 3037 + "@oxlint/binding-linux-arm64-musl": "1.57.0", 3038 + "@oxlint/binding-linux-ppc64-gnu": "1.57.0", 3039 + "@oxlint/binding-linux-riscv64-gnu": "1.57.0", 3040 + "@oxlint/binding-linux-riscv64-musl": "1.57.0", 3041 + "@oxlint/binding-linux-s390x-gnu": "1.57.0", 3042 + "@oxlint/binding-linux-x64-gnu": "1.57.0", 3043 + "@oxlint/binding-linux-x64-musl": "1.57.0", 3044 + "@oxlint/binding-openharmony-arm64": "1.57.0", 3045 + "@oxlint/binding-win32-arm64-msvc": "1.57.0", 3046 + "@oxlint/binding-win32-ia32-msvc": "1.57.0", 3047 + "@oxlint/binding-win32-x64-msvc": "1.57.0" 3048 + }, 3049 + "peerDependencies": { 3050 + "oxlint-tsgolint": ">=0.15.0" 3051 + }, 3052 + "peerDependenciesMeta": { 3053 + "oxlint-tsgolint": { 3054 + "optional": true 3055 + } 3056 + } 3057 + }, 3058 + "node_modules/oxlint-tsgolint": { 3059 + "version": "0.17.3", 3060 + "resolved": "https://registry.npmjs.org/oxlint-tsgolint/-/oxlint-tsgolint-0.17.3.tgz", 3061 + "integrity": "sha512-1eh4bcpOMw0e7+YYVxmhFc2mo/V6hJ2+zfukqf+GprvVn3y94b69M/xNrYLmx5A+VdYe0i/bJ2xOs6Hp/jRmRA==", 3062 + "dev": true, 3063 + "license": "MIT", 3064 + "bin": { 3065 + "tsgolint": "bin/tsgolint.js" 3066 + }, 3067 + "optionalDependencies": { 3068 + "@oxlint-tsgolint/darwin-arm64": "0.17.3", 3069 + "@oxlint-tsgolint/darwin-x64": "0.17.3", 3070 + "@oxlint-tsgolint/linux-arm64": "0.17.3", 3071 + "@oxlint-tsgolint/linux-x64": "0.17.3", 3072 + "@oxlint-tsgolint/win32-arm64": "0.17.3", 3073 + "@oxlint-tsgolint/win32-x64": "0.17.3" 3074 + } 3075 + }, 3076 + "node_modules/pako": { 3077 + "version": "0.2.9", 3078 + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", 3079 + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", 3080 + "license": "MIT" 3081 + }, 3082 + "node_modules/parse-css-color": { 3083 + "version": "0.2.1", 3084 + "resolved": "https://registry.npmjs.org/parse-css-color/-/parse-css-color-0.2.1.tgz", 3085 + "integrity": "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==", 3086 + "license": "MIT", 3087 + "dependencies": { 3088 + "color-name": "^1.1.4", 3089 + "hex-rgb": "^4.1.0" 3090 + } 3091 + }, 3092 + "node_modules/path-key": { 3093 + "version": "3.1.1", 3094 + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 3095 + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 3096 + "dev": true, 3097 + "license": "MIT", 3098 + "engines": { 3099 + "node": ">=8" 3100 + } 3101 + }, 3102 + "node_modules/path-parse": { 3103 + "version": "1.0.7", 3104 + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", 3105 + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", 3106 + "license": "MIT" 3107 + }, 3108 + "node_modules/picocolors": { 3109 + "version": "1.1.1", 3110 + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", 3111 + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", 3112 + "license": "ISC" 3113 + }, 3114 + "node_modules/picomatch": { 3115 + "version": "4.0.4", 3116 + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", 3117 + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", 3118 + "license": "MIT", 3119 + "engines": { 3120 + "node": ">=12" 3121 + }, 3122 + "funding": { 3123 + "url": "https://github.com/sponsors/jonschlinkert" 3124 + } 3125 + }, 3126 + "node_modules/pixelmatch": { 3127 + "version": "7.1.0", 3128 + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-7.1.0.tgz", 3129 + "integrity": "sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==", 3130 + "license": "ISC", 3131 + "dependencies": { 3132 + "pngjs": "^7.0.0" 3133 + }, 3134 + "bin": { 3135 + "pixelmatch": "bin/pixelmatch" 3136 + } 3137 + }, 3138 + "node_modules/pngjs": { 3139 + "version": "7.0.0", 3140 + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", 3141 + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", 3142 + "license": "MIT", 3143 + "engines": { 3144 + "node": ">=14.19.0" 3145 + } 3146 + }, 3147 + "node_modules/postcss": { 3148 + "version": "8.5.8", 3149 + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", 3150 + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", 3151 + "funding": [ 3152 + { 3153 + "type": "opencollective", 3154 + "url": "https://opencollective.com/postcss/" 3155 + }, 3156 + { 3157 + "type": "tidelift", 3158 + "url": "https://tidelift.com/funding/github/npm/postcss" 3159 + }, 3160 + { 3161 + "type": "github", 3162 + "url": "https://github.com/sponsors/ai" 3163 + } 3164 + ], 3165 + "license": "MIT", 3166 + "dependencies": { 3167 + "nanoid": "^3.3.11", 3168 + "picocolors": "^1.1.1", 3169 + "source-map-js": "^1.2.1" 3170 + }, 3171 + "engines": { 3172 + "node": "^10 || ^12 || >=14" 3173 + } 3174 + }, 3175 + "node_modules/postcss-value-parser": { 3176 + "version": "4.2.0", 3177 + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", 3178 + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", 3179 + "license": "MIT" 3180 + }, 3181 + "node_modules/prebuild-install": { 3182 + "version": "7.1.3", 3183 + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", 3184 + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", 3185 + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", 3186 + "license": "MIT", 3187 + "dependencies": { 3188 + "detect-libc": "^2.0.0", 3189 + "expand-template": "^2.0.3", 3190 + "github-from-package": "0.0.0", 3191 + "minimist": "^1.2.3", 3192 + "mkdirp-classic": "^0.5.3", 3193 + "napi-build-utils": "^2.0.0", 3194 + "node-abi": "^3.3.0", 3195 + "pump": "^3.0.0", 3196 + "rc": "^1.2.7", 3197 + "simple-get": "^4.0.0", 3198 + "tar-fs": "^2.0.0", 3199 + "tunnel-agent": "^0.6.0" 3200 + }, 3201 + "bin": { 3202 + "prebuild-install": "bin.js" 3203 + }, 3204 + "engines": { 3205 + "node": ">=10" 3206 + } 3207 + }, 3208 + "node_modules/pump": { 3209 + "version": "3.0.4", 3210 + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", 3211 + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", 3212 + "license": "MIT", 3213 + "dependencies": { 3214 + "end-of-stream": "^1.1.0", 3215 + "once": "^1.3.1" 3216 + } 3217 + }, 3218 + "node_modules/rc": { 3219 + "version": "1.2.8", 3220 + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", 3221 + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", 3222 + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", 3223 + "dependencies": { 3224 + "deep-extend": "^0.6.0", 3225 + "ini": "~1.3.0", 3226 + "minimist": "^1.2.0", 3227 + "strip-json-comments": "~2.0.1" 3228 + }, 3229 + "bin": { 3230 + "rc": "cli.js" 3231 + } 3232 + }, 3233 + "node_modules/readable-stream": { 3234 + "version": "3.6.2", 3235 + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", 3236 + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", 3237 + "license": "MIT", 3238 + "dependencies": { 3239 + "inherits": "^2.0.3", 3240 + "string_decoder": "^1.1.1", 3241 + "util-deprecate": "^1.0.1" 3242 + }, 3243 + "engines": { 3244 + "node": ">= 6" 3245 + } 3246 + }, 3247 + "node_modules/readdirp": { 3248 + "version": "4.1.2", 3249 + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", 3250 + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", 3251 + "dev": true, 3252 + "license": "MIT", 3253 + "engines": { 3254 + "node": ">= 14.18.0" 3255 + }, 3256 + "funding": { 3257 + "type": "individual", 3258 + "url": "https://paulmillr.com/funding/" 3259 + } 3260 + }, 3261 + "node_modules/resolve": { 3262 + "version": "1.22.11", 3263 + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", 3264 + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", 3265 + "license": "MIT", 3266 + "dependencies": { 3267 + "is-core-module": "^2.16.1", 3268 + "path-parse": "^1.0.7", 3269 + "supports-preserve-symlinks-flag": "^1.0.0" 3270 + }, 3271 + "bin": { 3272 + "resolve": "bin/resolve" 3273 + }, 3274 + "engines": { 3275 + "node": ">= 0.4" 3276 + }, 3277 + "funding": { 3278 + "url": "https://github.com/sponsors/ljharb" 3279 + } 3280 + }, 3281 + "node_modules/rollup": { 3282 + "version": "4.60.0", 3283 + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", 3284 + "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", 3285 + "license": "MIT", 3286 + "dependencies": { 3287 + "@types/estree": "1.0.8" 3288 + }, 3289 + "bin": { 3290 + "rollup": "dist/bin/rollup" 3291 + }, 3292 + "engines": { 3293 + "node": ">=18.0.0", 3294 + "npm": ">=8.0.0" 3295 + }, 3296 + "optionalDependencies": { 3297 + "@rollup/rollup-android-arm-eabi": "4.60.0", 3298 + "@rollup/rollup-android-arm64": "4.60.0", 3299 + "@rollup/rollup-darwin-arm64": "4.60.0", 3300 + "@rollup/rollup-darwin-x64": "4.60.0", 3301 + "@rollup/rollup-freebsd-arm64": "4.60.0", 3302 + "@rollup/rollup-freebsd-x64": "4.60.0", 3303 + "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", 3304 + "@rollup/rollup-linux-arm-musleabihf": "4.60.0", 3305 + "@rollup/rollup-linux-arm64-gnu": "4.60.0", 3306 + "@rollup/rollup-linux-arm64-musl": "4.60.0", 3307 + "@rollup/rollup-linux-loong64-gnu": "4.60.0", 3308 + "@rollup/rollup-linux-loong64-musl": "4.60.0", 3309 + "@rollup/rollup-linux-ppc64-gnu": "4.60.0", 3310 + "@rollup/rollup-linux-ppc64-musl": "4.60.0", 3311 + "@rollup/rollup-linux-riscv64-gnu": "4.60.0", 3312 + "@rollup/rollup-linux-riscv64-musl": "4.60.0", 3313 + "@rollup/rollup-linux-s390x-gnu": "4.60.0", 3314 + "@rollup/rollup-linux-x64-gnu": "4.60.0", 3315 + "@rollup/rollup-linux-x64-musl": "4.60.0", 3316 + "@rollup/rollup-openbsd-x64": "4.60.0", 3317 + "@rollup/rollup-openharmony-arm64": "4.60.0", 3318 + "@rollup/rollup-win32-arm64-msvc": "4.60.0", 3319 + "@rollup/rollup-win32-ia32-msvc": "4.60.0", 3320 + "@rollup/rollup-win32-x64-gnu": "4.60.0", 3321 + "@rollup/rollup-win32-x64-msvc": "4.60.0", 3322 + "fsevents": "~2.3.2" 3323 + } 3324 + }, 3325 + "node_modules/sade": { 3326 + "version": "1.8.1", 3327 + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", 3328 + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", 3329 + "dev": true, 3330 + "license": "MIT", 3331 + "dependencies": { 3332 + "mri": "^1.1.0" 3333 + }, 3334 + "engines": { 3335 + "node": ">=6" 3336 + } 3337 + }, 3338 + "node_modules/safe-buffer": { 3339 + "version": "5.2.1", 3340 + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 3341 + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 3342 + "funding": [ 3343 + { 3344 + "type": "github", 3345 + "url": "https://github.com/sponsors/feross" 3346 + }, 3347 + { 3348 + "type": "patreon", 3349 + "url": "https://www.patreon.com/feross" 3350 + }, 3351 + { 3352 + "type": "consulting", 3353 + "url": "https://feross.org/support" 3354 + } 3355 + ], 3356 + "license": "MIT" 3357 + }, 3358 + "node_modules/satori": { 3359 + "version": "0.19.3", 3360 + "resolved": "https://registry.npmjs.org/satori/-/satori-0.19.3.tgz", 3361 + "integrity": "sha512-dKr8TNYSyceWqBoTHWntjy25xaiWMw5GF+f8QOqFsov9OpTswLs7xdbvZudGRp9jkzbhv/4mVjVZYFtpruGKiA==", 3362 + "license": "MPL-2.0", 3363 + "dependencies": { 3364 + "@shuding/opentype.js": "1.4.0-beta.0", 3365 + "css-background-parser": "^0.1.0", 3366 + "css-box-shadow": "1.0.0-3", 3367 + "css-gradient-parser": "^0.0.17", 3368 + "css-to-react-native": "^3.0.0", 3369 + "emoji-regex-xs": "^2.0.1", 3370 + "escape-html": "^1.0.3", 3371 + "linebreak": "^1.1.0", 3372 + "parse-css-color": "^0.2.1", 3373 + "postcss-value-parser": "^4.2.0", 3374 + "yoga-layout": "^3.2.1" 3375 + }, 3376 + "engines": { 3377 + "node": ">=16" 3378 + } 3379 + }, 3380 + "node_modules/semver": { 3381 + "version": "7.7.4", 3382 + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", 3383 + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", 3384 + "license": "ISC", 3385 + "bin": { 3386 + "semver": "bin/semver.js" 3387 + }, 3388 + "engines": { 3389 + "node": ">=10" 3390 + } 3391 + }, 3392 + "node_modules/set-cookie-parser": { 3393 + "version": "3.1.0", 3394 + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz", 3395 + "integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==", 3396 + "license": "MIT" 3397 + }, 3398 + "node_modules/shebang-command": { 3399 + "version": "2.0.0", 3400 + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 3401 + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 3402 + "dev": true, 3403 + "license": "MIT", 3404 + "dependencies": { 3405 + "shebang-regex": "^3.0.0" 3406 + }, 3407 + "engines": { 3408 + "node": ">=8" 3409 + } 3410 + }, 3411 + "node_modules/shebang-regex": { 3412 + "version": "3.0.0", 3413 + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 3414 + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 3415 + "dev": true, 3416 + "license": "MIT", 3417 + "engines": { 3418 + "node": ">=8" 3419 + } 3420 + }, 3421 + "node_modules/simple-concat": { 3422 + "version": "1.0.1", 3423 + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", 3424 + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", 3425 + "funding": [ 3426 + { 3427 + "type": "github", 3428 + "url": "https://github.com/sponsors/feross" 3429 + }, 3430 + { 3431 + "type": "patreon", 3432 + "url": "https://www.patreon.com/feross" 3433 + }, 3434 + { 3435 + "type": "consulting", 3436 + "url": "https://feross.org/support" 3437 + } 3438 + ], 3439 + "license": "MIT" 3440 + }, 3441 + "node_modules/simple-get": { 3442 + "version": "4.0.1", 3443 + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", 3444 + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", 3445 + "funding": [ 3446 + { 3447 + "type": "github", 3448 + "url": "https://github.com/sponsors/feross" 3449 + }, 3450 + { 3451 + "type": "patreon", 3452 + "url": "https://www.patreon.com/feross" 3453 + }, 3454 + { 3455 + "type": "consulting", 3456 + "url": "https://feross.org/support" 3457 + } 3458 + ], 3459 + "license": "MIT", 3460 + "dependencies": { 3461 + "decompress-response": "^6.0.0", 3462 + "once": "^1.3.1", 3463 + "simple-concat": "^1.0.0" 3464 + } 3465 + }, 3466 + "node_modules/sirv": { 3467 + "version": "3.0.2", 3468 + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", 3469 + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", 3470 + "license": "MIT", 3471 + "dependencies": { 3472 + "@polka/url": "^1.0.0-next.24", 3473 + "mrmime": "^2.0.0", 3474 + "totalist": "^3.0.0" 3475 + }, 3476 + "engines": { 3477 + "node": ">=18" 3478 + } 3479 + }, 3480 + "node_modules/source-map-js": { 3481 + "version": "1.2.1", 3482 + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", 3483 + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", 3484 + "license": "BSD-3-Clause", 3485 + "engines": { 3486 + "node": ">=0.10.0" 3487 + } 3488 + }, 3489 + "node_modules/std-env": { 3490 + "version": "4.0.0", 3491 + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", 3492 + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", 3493 + "license": "MIT" 3494 + }, 3495 + "node_modules/string_decoder": { 3496 + "version": "1.3.0", 3497 + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 3498 + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 3499 + "license": "MIT", 3500 + "dependencies": { 3501 + "safe-buffer": "~5.2.0" 3502 + } 3503 + }, 3504 + "node_modules/string.prototype.codepointat": { 3505 + "version": "0.2.1", 3506 + "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz", 3507 + "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==", 3508 + "license": "MIT" 3509 + }, 3510 + "node_modules/strip-json-comments": { 3511 + "version": "2.0.1", 3512 + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", 3513 + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", 3514 + "license": "MIT", 3515 + "engines": { 3516 + "node": ">=0.10.0" 3517 + } 3518 + }, 3519 + "node_modules/supports-preserve-symlinks-flag": { 3520 + "version": "1.0.0", 3521 + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", 3522 + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", 3523 + "license": "MIT", 3524 + "engines": { 3525 + "node": ">= 0.4" 3526 + }, 3527 + "funding": { 3528 + "url": "https://github.com/sponsors/ljharb" 3529 + } 3530 + }, 3531 + "node_modules/svelte": { 3532 + "version": "5.55.0", 3533 + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.0.tgz", 3534 + "integrity": "sha512-SThllKq6TRMBwPtat7ASnm/9CDXnIhBR0NPGw0ujn2DVYx9rVwsPZxDaDQcYGdUz/3BYVsCzdq7pZarRQoGvtw==", 3535 + "license": "MIT", 3536 + "dependencies": { 3537 + "@jridgewell/remapping": "^2.3.4", 3538 + "@jridgewell/sourcemap-codec": "^1.5.0", 3539 + "@sveltejs/acorn-typescript": "^1.0.5", 3540 + "@types/estree": "^1.0.5", 3541 + "@types/trusted-types": "^2.0.7", 3542 + "acorn": "^8.12.1", 3543 + "aria-query": "5.3.1", 3544 + "axobject-query": "^4.1.0", 3545 + "clsx": "^2.1.1", 3546 + "devalue": "^5.6.4", 3547 + "esm-env": "^1.2.1", 3548 + "esrap": "^2.2.2", 3549 + "is-reference": "^3.0.3", 3550 + "locate-character": "^3.0.0", 3551 + "magic-string": "^0.30.11", 3552 + "zimmerframe": "^1.1.2" 3553 + }, 3554 + "engines": { 3555 + "node": ">=18" 3556 + } 3557 + }, 3558 + "node_modules/svelte-check": { 3559 + "version": "4.4.5", 3560 + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.5.tgz", 3561 + "integrity": "sha512-1bSwIRCvvmSHrlK52fOlZmVtUZgil43jNL/2H18pRpa+eQjzGt6e3zayxhp1S7GajPFKNM/2PMCG+DZFHlG9fw==", 3562 + "dev": true, 3563 + "license": "MIT", 3564 + "dependencies": { 3565 + "@jridgewell/trace-mapping": "^0.3.25", 3566 + "chokidar": "^4.0.1", 3567 + "fdir": "^6.2.0", 3568 + "picocolors": "^1.0.0", 3569 + "sade": "^1.7.4" 3570 + }, 3571 + "bin": { 3572 + "svelte-check": "bin/svelte-check" 3573 + }, 3574 + "engines": { 3575 + "node": ">= 18.0.0" 3576 + }, 3577 + "peerDependencies": { 3578 + "svelte": "^4.0.0 || ^5.0.0-next.0", 3579 + "typescript": ">=5.0.0" 3580 + } 3581 + }, 3582 + "node_modules/svelte/node_modules/is-reference": { 3583 + "version": "3.0.3", 3584 + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", 3585 + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", 3586 + "license": "MIT", 3587 + "dependencies": { 3588 + "@types/estree": "^1.0.6" 3589 + } 3590 + }, 3591 + "node_modules/tar-fs": { 3592 + "version": "2.1.4", 3593 + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", 3594 + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", 3595 + "license": "MIT", 3596 + "dependencies": { 3597 + "chownr": "^1.1.1", 3598 + "mkdirp-classic": "^0.5.2", 3599 + "pump": "^3.0.0", 3600 + "tar-stream": "^2.1.4" 3601 + } 3602 + }, 3603 + "node_modules/tar-stream": { 3604 + "version": "2.2.0", 3605 + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", 3606 + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", 3607 + "license": "MIT", 3608 + "dependencies": { 3609 + "bl": "^4.0.3", 3610 + "end-of-stream": "^1.4.1", 3611 + "fs-constants": "^1.0.0", 3612 + "inherits": "^2.0.3", 3613 + "readable-stream": "^3.1.1" 3614 + }, 3615 + "engines": { 3616 + "node": ">=6" 3617 + } 3618 + }, 3619 + "node_modules/tiny-inflate": { 3620 + "version": "1.0.3", 3621 + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", 3622 + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", 3623 + "license": "MIT" 3624 + }, 3625 + "node_modules/tinybench": { 3626 + "version": "2.9.0", 3627 + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", 3628 + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", 3629 + "license": "MIT" 3630 + }, 3631 + "node_modules/tinyexec": { 3632 + "version": "1.0.4", 3633 + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", 3634 + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", 3635 + "license": "MIT", 3636 + "engines": { 3637 + "node": ">=18" 3638 + } 3639 + }, 3640 + "node_modules/tinyglobby": { 3641 + "version": "0.2.15", 3642 + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", 3643 + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", 3644 + "license": "MIT", 3645 + "dependencies": { 3646 + "fdir": "^6.5.0", 3647 + "picomatch": "^4.0.3" 3648 + }, 3649 + "engines": { 3650 + "node": ">=12.0.0" 3651 + }, 3652 + "funding": { 3653 + "url": "https://github.com/sponsors/SuperchupuDev" 3654 + } 3655 + }, 3656 + "node_modules/tinypool": { 3657 + "version": "2.1.0", 3658 + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.1.0.tgz", 3659 + "integrity": "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==", 3660 + "dev": true, 3661 + "license": "MIT", 3662 + "engines": { 3663 + "node": "^20.0.0 || >=22.0.0" 3664 + } 3665 + }, 3666 + "node_modules/totalist": { 3667 + "version": "3.0.1", 3668 + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", 3669 + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", 3670 + "license": "MIT", 3671 + "engines": { 3672 + "node": ">=6" 3673 + } 3674 + }, 3675 + "node_modules/tunnel-agent": { 3676 + "version": "0.6.0", 3677 + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 3678 + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", 3679 + "license": "Apache-2.0", 3680 + "dependencies": { 3681 + "safe-buffer": "^5.0.1" 3682 + }, 3683 + "engines": { 3684 + "node": "*" 3685 + } 3686 + }, 3687 + "node_modules/typescript": { 3688 + "version": "5.9.3", 3689 + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", 3690 + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", 3691 + "devOptional": true, 3692 + "license": "Apache-2.0", 3693 + "bin": { 3694 + "tsc": "bin/tsc", 3695 + "tsserver": "bin/tsserver" 3696 + }, 3697 + "engines": { 3698 + "node": ">=14.17" 3699 + } 3700 + }, 3701 + "node_modules/unicode-trie": { 3702 + "version": "2.0.0", 3703 + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", 3704 + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", 3705 + "license": "MIT", 3706 + "dependencies": { 3707 + "pako": "^0.2.5", 3708 + "tiny-inflate": "^1.0.0" 3709 + } 3710 + }, 3711 + "node_modules/util-deprecate": { 3712 + "version": "1.0.2", 3713 + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 3714 + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", 3715 + "license": "MIT" 3716 + }, 3717 + "node_modules/vite": { 3718 + "name": "@voidzero-dev/vite-plus-core", 3719 + "version": "0.1.14", 3720 + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-core/-/vite-plus-core-0.1.14.tgz", 3721 + "integrity": "sha512-CCWzdkfW0fo0cQNlIsYp5fOuH2IwKuPZEb2UY2Z8gXcp5pG74A82H2Pthj0heAuvYTAnfT7kEC6zM+RbiBgQbg==", 3722 + "license": "MIT", 3723 + "peer": true, 3724 + "dependencies": { 3725 + "@oxc-project/runtime": "=0.121.0", 3726 + "@oxc-project/types": "=0.122.0", 3727 + "lightningcss": "^1.30.2", 3728 + "postcss": "^8.5.6" 3729 + }, 3730 + "engines": { 3731 + "node": "^20.19.0 || >=22.12.0" 3732 + }, 3733 + "optionalDependencies": { 3734 + "fsevents": "~2.3.3" 3735 + }, 3736 + "peerDependencies": { 3737 + "@arethetypeswrong/core": "^0.18.1", 3738 + "@tsdown/css": "0.21.4", 3739 + "@tsdown/exe": "0.21.4", 3740 + "@types/node": "^20.19.0 || >=22.12.0", 3741 + "@vitejs/devtools": "^0.1.0", 3742 + "esbuild": "^0.27.0", 3743 + "jiti": ">=1.21.0", 3744 + "less": "^4.0.0", 3745 + "publint": "^0.3.0", 3746 + "sass": "^1.70.0", 3747 + "sass-embedded": "^1.70.0", 3748 + "stylus": ">=0.54.8", 3749 + "sugarss": "^5.0.0", 3750 + "terser": "^5.16.0", 3751 + "tsx": "^4.8.1", 3752 + "typescript": "^5.0.0", 3753 + "unplugin-unused": "^0.5.0", 3754 + "yaml": "^2.4.2" 3755 + }, 3756 + "peerDependenciesMeta": { 3757 + "@arethetypeswrong/core": { 3758 + "optional": true 3759 + }, 3760 + "@tsdown/css": { 3761 + "optional": true 3762 + }, 3763 + "@tsdown/exe": { 3764 + "optional": true 3765 + }, 3766 + "@types/node": { 3767 + "optional": true 3768 + }, 3769 + "@vitejs/devtools": { 3770 + "optional": true 3771 + }, 3772 + "esbuild": { 3773 + "optional": true 3774 + }, 3775 + "jiti": { 3776 + "optional": true 3777 + }, 3778 + "less": { 3779 + "optional": true 3780 + }, 3781 + "publint": { 3782 + "optional": true 3783 + }, 3784 + "sass": { 3785 + "optional": true 3786 + }, 3787 + "sass-embedded": { 3788 + "optional": true 3789 + }, 3790 + "stylus": { 3791 + "optional": true 3792 + }, 3793 + "sugarss": { 3794 + "optional": true 3795 + }, 3796 + "terser": { 3797 + "optional": true 3798 + }, 3799 + "tsx": { 3800 + "optional": true 3801 + }, 3802 + "typescript": { 3803 + "optional": true 3804 + }, 3805 + "unplugin-unused": { 3806 + "optional": true 3807 + }, 3808 + "yaml": { 3809 + "optional": true 3810 + } 3811 + } 3812 + }, 3813 + "node_modules/vite-plus": { 3814 + "version": "0.1.14", 3815 + "resolved": "https://registry.npmjs.org/vite-plus/-/vite-plus-0.1.14.tgz", 3816 + "integrity": "sha512-p4pWlpZZNiEsHxPWNdeIU9iuPix3ydm3ficb0dXPggoyIkdotfXtvn2NPX9KwfiQImU72EVEs4+VYBZYNcUYrw==", 3817 + "dev": true, 3818 + "license": "MIT", 3819 + "dependencies": { 3820 + "@oxc-project/types": "=0.122.0", 3821 + "@voidzero-dev/vite-plus-core": "0.1.14", 3822 + "@voidzero-dev/vite-plus-test": "0.1.14", 3823 + "cac": "^7.0.0", 3824 + "cross-spawn": "^7.0.5", 3825 + "oxfmt": "=0.42.0", 3826 + "oxlint": "=1.57.0", 3827 + "oxlint-tsgolint": "=0.17.3", 3828 + "picocolors": "^1.1.1" 3829 + }, 3830 + "bin": { 3831 + "oxfmt": "bin/oxfmt", 3832 + "oxlint": "bin/oxlint", 3833 + "vp": "bin/vp" 3834 + }, 3835 + "engines": { 3836 + "node": "^20.19.0 || >=22.12.0" 3837 + }, 3838 + "optionalDependencies": { 3839 + "@voidzero-dev/vite-plus-darwin-arm64": "0.1.14", 3840 + "@voidzero-dev/vite-plus-darwin-x64": "0.1.14", 3841 + "@voidzero-dev/vite-plus-linux-arm64-gnu": "0.1.14", 3842 + "@voidzero-dev/vite-plus-linux-arm64-musl": "0.1.14", 3843 + "@voidzero-dev/vite-plus-linux-x64-gnu": "0.1.14", 3844 + "@voidzero-dev/vite-plus-linux-x64-musl": "0.1.14", 3845 + "@voidzero-dev/vite-plus-win32-arm64-msvc": "0.1.14", 3846 + "@voidzero-dev/vite-plus-win32-x64-msvc": "0.1.14" 3847 + } 3848 + }, 3849 + "node_modules/vitefu": { 3850 + "version": "1.1.2", 3851 + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.2.tgz", 3852 + "integrity": "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==", 3853 + "license": "MIT", 3854 + "peer": true, 3855 + "workspaces": [ 3856 + "tests/deps/*", 3857 + "tests/projects/*", 3858 + "tests/projects/workspace/packages/*" 3859 + ], 3860 + "peerDependencies": { 3861 + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0" 3862 + }, 3863 + "peerDependenciesMeta": { 3864 + "vite": { 3865 + "optional": true 3866 + } 3867 + } 3868 + }, 3869 + "node_modules/vitest": { 3870 + "name": "@voidzero-dev/vite-plus-test", 3871 + "version": "0.1.14", 3872 + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-test/-/vite-plus-test-0.1.14.tgz", 3873 + "integrity": "sha512-rjF+qpYD+5+THOJZ3gbE3+cxsk5sW7nJ0ODK7y6ZKeS4amREUMedEDYykzKBwR7OZDC/WwE90A0iLWCr6qAXhA==", 3874 + "license": "MIT", 3875 + "dependencies": { 3876 + "@standard-schema/spec": "^1.1.0", 3877 + "@types/chai": "^5.2.2", 3878 + "@voidzero-dev/vite-plus-core": "0.1.14", 3879 + "es-module-lexer": "^1.7.0", 3880 + "obug": "^2.1.1", 3881 + "pixelmatch": "^7.1.0", 3882 + "pngjs": "^7.0.0", 3883 + "sirv": "^3.0.2", 3884 + "std-env": "^4.0.0", 3885 + "tinybench": "^2.9.0", 3886 + "tinyexec": "^1.0.2", 3887 + "tinyglobby": "^0.2.15", 3888 + "ws": "^8.18.3" 3889 + }, 3890 + "engines": { 3891 + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" 3892 + }, 3893 + "peerDependencies": { 3894 + "@edge-runtime/vm": "*", 3895 + "@opentelemetry/api": "^1.9.0", 3896 + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", 3897 + "@vitest/ui": "4.1.1", 3898 + "happy-dom": "*", 3899 + "jsdom": "*", 3900 + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" 3901 + }, 3902 + "peerDependenciesMeta": { 3903 + "@edge-runtime/vm": { 3904 + "optional": true 3905 + }, 3906 + "@opentelemetry/api": { 3907 + "optional": true 3908 + }, 3909 + "@types/node": { 3910 + "optional": true 3911 + }, 3912 + "@vitest/ui": { 3913 + "optional": true 3914 + }, 3915 + "happy-dom": { 3916 + "optional": true 3917 + }, 3918 + "jsdom": { 3919 + "optional": true 3920 + }, 3921 + "vite": { 3922 + "optional": false 3923 + } 3924 + } 3925 + }, 3926 + "node_modules/which": { 3927 + "version": "2.0.2", 3928 + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 3929 + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 3930 + "dev": true, 3931 + "license": "ISC", 3932 + "dependencies": { 3933 + "isexe": "^2.0.0" 3934 + }, 3935 + "bin": { 3936 + "node-which": "bin/node-which" 3937 + }, 3938 + "engines": { 3939 + "node": ">= 8" 3940 + } 3941 + }, 3942 + "node_modules/wrappy": { 3943 + "version": "1.0.2", 3944 + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 3945 + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 3946 + "license": "ISC" 3947 + }, 3948 + "node_modules/ws": { 3949 + "version": "8.20.0", 3950 + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", 3951 + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", 3952 + "license": "MIT", 3953 + "engines": { 3954 + "node": ">=10.0.0" 3955 + }, 3956 + "peerDependencies": { 3957 + "bufferutil": "^4.0.1", 3958 + "utf-8-validate": ">=5.0.2" 3959 + }, 3960 + "peerDependenciesMeta": { 3961 + "bufferutil": { 3962 + "optional": true 3963 + }, 3964 + "utf-8-validate": { 3965 + "optional": true 3966 + } 3967 + } 3968 + }, 3969 + "node_modules/yaml": { 3970 + "version": "2.8.3", 3971 + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", 3972 + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", 3973 + "license": "ISC", 3974 + "bin": { 3975 + "yaml": "bin.mjs" 3976 + }, 3977 + "engines": { 3978 + "node": ">= 14.6" 3979 + }, 3980 + "funding": { 3981 + "url": "https://github.com/sponsors/eemeli" 3982 + } 3983 + }, 3984 + "node_modules/yoga-layout": { 3985 + "version": "3.2.1", 3986 + "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", 3987 + "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", 3988 + "license": "MIT" 3989 + }, 3990 + "node_modules/zimmerframe": { 3991 + "version": "1.1.4", 3992 + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", 3993 + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", 3994 + "license": "MIT" 3995 + } 3996 + } 3997 + }
+29
package.json
··· 1 + { 2 + "name": "zzstoatzz-status", 3 + "private": true, 4 + "type": "module", 5 + "scripts": { 6 + "start": "hatk start", 7 + "dev": "vp dev", 8 + "build": "vp build", 9 + "check": "vp check && svelte-check" 10 + }, 11 + "dependencies": { 12 + "@hatk/hatk": "^0.0.1-alpha.44", 13 + "@sveltejs/adapter-node": "^5.5.4", 14 + "@sveltejs/kit": "^2.55.0", 15 + "@tanstack/svelte-query": "^6.1.0", 16 + "lucide-svelte": "^0.576.0" 17 + }, 18 + "devDependencies": { 19 + "@voidzero-dev/vite-plus-core": "^0.1.11", 20 + "svelte": "^5", 21 + "svelte-check": "^4", 22 + "typescript": "^5", 23 + "vite-plus": "^0.1.11" 24 + }, 25 + "overrides": { 26 + "vite": "npm:@voidzero-dev/vite-plus-core@latest", 27 + "vitest": "npm:@voidzero-dev/vite-plus-test@latest" 28 + } 29 + }
+27
server/feeds/_hydrate.ts
··· 1 + import { views } from "$hatk"; 2 + import type { StatusRecord, StatusView } from "$hatk"; 3 + import type { BaseContext, Row } from "$hatk"; 4 + 5 + export async function hydrateStatuses( 6 + ctx: BaseContext, 7 + items: Row<StatusRecord>[], 8 + ): Promise<StatusView[]> { 9 + const now = new Date(); 10 + 11 + return items.map((item) => { 12 + const expiresDate = item.value.expires ? new Date(item.value.expires) : null; 13 + 14 + return views.statusView({ 15 + uri: item.uri, 16 + cid: item.cid, 17 + did: item.did, 18 + handle: item.handle ?? item.did, 19 + emoji: item.value.emoji, 20 + text: item.value.text, 21 + expires: item.value.expires, 22 + createdAt: item.value.createdAt, 23 + indexedAt: item.indexed_at ?? item.value.createdAt, 24 + expired: expiresDate ? expiresDate < now : false, 25 + }); 26 + }); 27 + }
+41
server/feeds/actor.ts
··· 1 + import { defineFeed } from "$hatk"; 2 + import { hydrateStatuses } from "./_hydrate.ts"; 3 + 4 + export default defineFeed({ 5 + collection: "io.zzstoatzz.status.record", 6 + label: "Actor Statuses", 7 + 8 + hydrate: hydrateStatuses, 9 + 10 + async generate(ctx) { 11 + const { params, ok, isTakendown } = ctx; 12 + 13 + let actor = params.actor; 14 + if (!actor) { 15 + return ok({ uris: [], cursor: undefined }); 16 + } 17 + 18 + if (!actor.startsWith("did:")) { 19 + const rows = (await ctx.db.query( 20 + `SELECT did FROM _repos WHERE handle = $1`, 21 + [actor], 22 + )) as { did: string }[]; 23 + if (rows[0]?.did) { 24 + actor = rows[0].did; 25 + } 26 + } 27 + 28 + if (await isTakendown(actor)) { 29 + return ok({ uris: [], cursor: undefined }); 30 + } 31 + 32 + const { rows, cursor } = await ctx.paginate<{ uri: string }>( 33 + `SELECT t.uri, t.cid, t.created_at 34 + FROM "io.zzstoatzz.status.record" t 35 + WHERE t.did = $1`, 36 + { params: [actor], orderBy: "t.created_at" }, 37 + ); 38 + 39 + return ok({ uris: rows.map((r) => r.uri), cursor }); 40 + }, 41 + });
+20
server/feeds/recent.ts
··· 1 + import { defineFeed } from "$hatk"; 2 + import { hydrateStatuses } from "./_hydrate.ts"; 3 + 4 + export default defineFeed({ 5 + collection: "io.zzstoatzz.status.record", 6 + label: "Recent Statuses", 7 + 8 + hydrate: hydrateStatuses, 9 + 10 + async generate(ctx) { 11 + const { rows, cursor } = await ctx.paginate<{ uri: string }>( 12 + `SELECT t.uri, t.cid, t.created_at FROM "io.zzstoatzz.status.record" t 13 + LEFT JOIN _repos r ON t.did = r.did 14 + WHERE (r.status IS NULL OR r.status != 'takendown')`, 15 + { orderBy: "t.created_at" }, 16 + ); 17 + 18 + return ctx.ok({ uris: rows.map((r) => r.uri), cursor }); 19 + }, 20 + });
+6
server/on-login.ts
··· 1 + import { defineHook } from "$hatk"; 2 + 3 + export default defineHook("on-login", async (ctx) => { 4 + const { did, ensureRepo } = ctx; 5 + await ensureRepo(did); 6 + });
-1695
site/app.js
··· 1 - // Configuration 2 - const CONFIG = { 3 - server: 'https://zzstoatzz-quickslice-status.fly.dev', 4 - clientId: 'client_2mP9AwgVHkg1vaSpcWSsKw', 5 - }; 6 - 7 - // Base path for routing (empty for root domain, '/subpath' for subdirectory) 8 - // Auto-detect from pathname for wisp.place style hosting (/did:xxx/sitename) 9 - const BASE_PATH = (() => { 10 - const match = window.location.pathname.match(/^(\/did:[^/]+\/[^/]+)/); 11 - return match ? match[1] : ''; 12 - })(); 13 - 14 - let client = null; 15 - let userPreferences = null; 16 - 17 - // Default preferences 18 - const DEFAULT_PREFERENCES = { 19 - accentColor: '#4a9eff', 20 - font: 'mono', 21 - theme: 'dark' 22 - }; 23 - 24 - // Available fonts - use simple keys, map to actual CSS in applyPreferences 25 - const FONTS = [ 26 - { value: 'system', label: 'system' }, 27 - { value: 'mono', label: 'mono' }, 28 - { value: 'serif', label: 'serif' }, 29 - { value: 'comic', label: 'comic' }, 30 - ]; 31 - 32 - const FONT_CSS = { 33 - 'system': 'system-ui, -apple-system, sans-serif', 34 - 'mono': 'ui-monospace, SF Mono, Monaco, monospace', 35 - 'serif': 'ui-serif, Georgia, serif', 36 - 'comic': 'Comic Sans MS, Comic Sans, cursive', 37 - }; 38 - 39 - // Preset accent colors 40 - const ACCENT_COLORS = [ 41 - '#4a9eff', // blue (default) 42 - '#10b981', // green 43 - '#f59e0b', // amber 44 - '#ef4444', // red 45 - '#8b5cf6', // purple 46 - '#ec4899', // pink 47 - '#06b6d4', // cyan 48 - '#f97316', // orange 49 - ]; 50 - 51 - // Apply preferences to the page 52 - function applyPreferences(prefs) { 53 - const { accentColor, font, theme } = { ...DEFAULT_PREFERENCES, ...prefs }; 54 - 55 - document.documentElement.style.setProperty('--accent', accentColor); 56 - // Map simple font key to actual CSS font-family 57 - const fontCSS = FONT_CSS[font] || FONT_CSS['mono']; 58 - document.documentElement.style.setProperty('--font-family', fontCSS); 59 - document.documentElement.setAttribute('data-theme', theme); 60 - 61 - localStorage.setItem('theme', theme); 62 - } 63 - 64 - // Load preferences from server 65 - async function loadPreferences() { 66 - if (!client) return DEFAULT_PREFERENCES; 67 - 68 - try { 69 - const user = client.getUser(); 70 - if (!user) return DEFAULT_PREFERENCES; 71 - 72 - const res = await fetch(`${CONFIG.server}/graphql`, { 73 - method: 'POST', 74 - headers: { 'Content-Type': 'application/json' }, 75 - body: JSON.stringify({ 76 - query: ` 77 - query GetPreferences($did: String!) { 78 - ioZzstoatzzStatusPreferences( 79 - where: { did: { eq: $did } } 80 - first: 1 81 - ) { 82 - edges { node { accentColor font theme } } 83 - } 84 - } 85 - `, 86 - variables: { did: user.did } 87 - }) 88 - }); 89 - const json = await res.json(); 90 - const edges = json.data?.ioZzstoatzzStatusPreferences?.edges || []; 91 - 92 - if (edges.length > 0) { 93 - userPreferences = edges[0].node; 94 - return userPreferences; 95 - } 96 - return DEFAULT_PREFERENCES; 97 - } catch (e) { 98 - console.error('Failed to load preferences:', e); 99 - return DEFAULT_PREFERENCES; 100 - } 101 - } 102 - 103 - // Save preferences to server 104 - async function savePreferences(prefs) { 105 - if (!client) return; 106 - 107 - try { 108 - const user = client.getUser(); 109 - if (!user) return; 110 - 111 - // First, delete any existing preferences records for this user 112 - const res = await fetch(`${CONFIG.server}/graphql`, { 113 - method: 'POST', 114 - headers: { 'Content-Type': 'application/json' }, 115 - body: JSON.stringify({ 116 - query: ` 117 - query GetExistingPrefs($did: String!) { 118 - ioZzstoatzzStatusPreferences(where: { did: { eq: $did } }, first: 50) { 119 - edges { node { uri } } 120 - } 121 - } 122 - `, 123 - variables: { did: user.did } 124 - }) 125 - }); 126 - const json = await res.json(); 127 - const existing = json.data?.ioZzstoatzzStatusPreferences?.edges || []; 128 - 129 - // Delete all existing preference records 130 - for (const edge of existing) { 131 - const rkey = edge.node.uri.split('/').pop(); 132 - try { 133 - await client.mutate(` 134 - mutation DeletePref($rkey: String!) { 135 - deleteIoZzstoatzzStatusPreferences(rkey: $rkey) { uri } 136 - } 137 - `, { rkey }); 138 - } catch (e) { 139 - console.warn('Failed to delete old pref:', e); 140 - } 141 - } 142 - 143 - // Create new preferences record 144 - await client.mutate(` 145 - mutation SavePreferences($input: CreateIoZzstoatzzStatusPreferencesInput!) { 146 - createIoZzstoatzzStatusPreferences(input: $input) { uri } 147 - } 148 - `, { 149 - input: { 150 - accentColor: prefs.accentColor, 151 - font: prefs.font, 152 - theme: prefs.theme 153 - } 154 - }); 155 - 156 - userPreferences = prefs; 157 - applyPreferences(prefs); 158 - } catch (e) { 159 - console.error('Failed to save preferences:', e); 160 - alert('Failed to save preferences: ' + e.message); 161 - } 162 - } 163 - 164 - // Create settings modal 165 - function createSettingsModal() { 166 - const overlay = document.createElement('div'); 167 - overlay.className = 'settings-overlay hidden'; 168 - overlay.innerHTML = ` 169 - <div class="settings-modal"> 170 - <div class="settings-header"> 171 - <h3>settings</h3> 172 - <button class="settings-close" aria-label="close">✕</button> 173 - </div> 174 - <div class="settings-content"> 175 - <div class="setting-group"> 176 - <label>accent color</label> 177 - <div class="color-picker"> 178 - ${ACCENT_COLORS.map(c => ` 179 - <button class="color-btn" data-color="${c}" style="background: ${c}" title="${c}"></button> 180 - `).join('')} 181 - <input type="color" id="custom-color" class="custom-color-input" title="custom color"> 182 - </div> 183 - </div> 184 - <div class="setting-group"> 185 - <label>font</label> 186 - <select id="font-select"> 187 - ${FONTS.map(f => `<option value="${f.value}">${f.label}</option>`).join('')} 188 - </select> 189 - </div> 190 - <div class="setting-group"> 191 - <label>theme</label> 192 - <select id="theme-select"> 193 - <option value="dark">dark</option> 194 - <option value="light">light</option> 195 - <option value="system">system</option> 196 - </select> 197 - </div> 198 - </div> 199 - <div class="settings-footer"> 200 - <button id="save-settings" class="save-btn">save</button> 201 - </div> 202 - </div> 203 - `; 204 - 205 - const modal = overlay.querySelector('.settings-modal'); 206 - const closeBtn = overlay.querySelector('.settings-close'); 207 - const colorBtns = overlay.querySelectorAll('.color-btn'); 208 - const customColor = overlay.querySelector('#custom-color'); 209 - const fontSelect = overlay.querySelector('#font-select'); 210 - const themeSelect = overlay.querySelector('#theme-select'); 211 - const saveBtn = overlay.querySelector('#save-settings'); 212 - 213 - let currentPrefs = { ...DEFAULT_PREFERENCES }; 214 - 215 - function updateColorSelection(color) { 216 - colorBtns.forEach(btn => btn.classList.toggle('active', btn.dataset.color === color)); 217 - customColor.value = color; 218 - currentPrefs.accentColor = color; 219 - } 220 - 221 - function open(prefs) { 222 - currentPrefs = { ...DEFAULT_PREFERENCES, ...prefs }; 223 - updateColorSelection(currentPrefs.accentColor); 224 - fontSelect.value = currentPrefs.font; 225 - themeSelect.value = currentPrefs.theme; 226 - overlay.classList.remove('hidden'); 227 - } 228 - 229 - function close() { 230 - overlay.classList.add('hidden'); 231 - } 232 - 233 - overlay.addEventListener('click', e => { if (e.target === overlay) close(); }); 234 - closeBtn.addEventListener('click', close); 235 - 236 - colorBtns.forEach(btn => { 237 - btn.addEventListener('click', () => updateColorSelection(btn.dataset.color)); 238 - }); 239 - 240 - customColor.addEventListener('input', () => { 241 - updateColorSelection(customColor.value); 242 - }); 243 - 244 - fontSelect.addEventListener('change', () => { 245 - currentPrefs.font = fontSelect.value; 246 - }); 247 - 248 - themeSelect.addEventListener('change', () => { 249 - currentPrefs.theme = themeSelect.value; 250 - }); 251 - 252 - saveBtn.addEventListener('click', async () => { 253 - saveBtn.disabled = true; 254 - saveBtn.textContent = 'saving...'; 255 - await savePreferences(currentPrefs); 256 - saveBtn.disabled = false; 257 - saveBtn.textContent = 'save'; 258 - close(); 259 - }); 260 - 261 - document.body.appendChild(overlay); 262 - return { open, close }; 263 - } 264 - 265 - // Theme (fallback for non-logged-in users) 266 - function initTheme() { 267 - const saved = localStorage.getItem('theme') || 'dark'; 268 - document.documentElement.setAttribute('data-theme', saved); 269 - } 270 - 271 - function toggleTheme() { 272 - const current = document.documentElement.getAttribute('data-theme'); 273 - const next = current === 'dark' ? 'light' : 'dark'; 274 - document.documentElement.setAttribute('data-theme', next); 275 - localStorage.setItem('theme', next); 276 - 277 - // If logged in, also update preferences 278 - if (userPreferences) { 279 - userPreferences.theme = next; 280 - savePreferences(userPreferences); 281 - } 282 - } 283 - 284 - // Timestamp formatting (ported from original status app) 285 - const TimestampFormatter = { 286 - formatRelative(date, now = new Date()) { 287 - const diffMs = now - date; 288 - const diffMins = Math.floor(diffMs / 60000); 289 - const diffHours = Math.floor(diffMs / 3600000); 290 - const diffDays = Math.floor(diffMs / 86400000); 291 - 292 - if (diffMs < 30000) return 'just now'; 293 - if (diffMins < 60) return `${diffMins}m ago`; 294 - if (diffHours < 24) { 295 - const remainingMins = diffMins % 60; 296 - return remainingMins === 0 ? `${diffHours}h ago` : `${diffHours}h ${remainingMins}m ago`; 297 - } 298 - if (diffDays < 7) { 299 - const remainingHours = diffHours % 24; 300 - return remainingHours === 0 ? `${diffDays}d ago` : `${diffDays}d ${remainingHours}h ago`; 301 - } 302 - 303 - const timeStr = date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }).toLowerCase(); 304 - if (date.getFullYear() === now.getFullYear()) { 305 - return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) + ', ' + timeStr; 306 - } 307 - return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) + ', ' + timeStr; 308 - }, 309 - 310 - formatCompact(date, now = new Date()) { 311 - const diffMs = now - date; 312 - const diffDays = Math.floor(diffMs / 86400000); 313 - 314 - if (date.toDateString() === now.toDateString()) { 315 - return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }).toLowerCase(); 316 - } 317 - const yesterday = new Date(now); 318 - yesterday.setDate(yesterday.getDate() - 1); 319 - if (date.toDateString() === yesterday.toDateString()) { 320 - return 'yesterday, ' + date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }).toLowerCase(); 321 - } 322 - if (diffDays < 7) { 323 - const dayName = date.toLocaleDateString('en-US', { weekday: 'short' }).toLowerCase(); 324 - const time = date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }).toLowerCase(); 325 - return `${dayName}, ${time}`; 326 - } 327 - if (date.getFullYear() === now.getFullYear()) { 328 - return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true }).toLowerCase(); 329 - } 330 - return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true }).toLowerCase(); 331 - }, 332 - 333 - getFullTimestamp(date) { 334 - const dayName = date.toLocaleDateString('en-US', { weekday: 'long' }); 335 - const monthDay = date.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }); 336 - const time = date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', second: '2-digit', hour12: true }); 337 - const tzAbbr = date.toLocaleTimeString('en-US', { timeZoneName: 'short' }).split(' ').pop(); 338 - return `${dayName}, ${monthDay} at ${time} ${tzAbbr}`; 339 - } 340 - }; 341 - 342 - function relativeTime(dateStr, format = 'relative') { 343 - const date = new Date(dateStr); 344 - return format === 'compact' 345 - ? TimestampFormatter.formatCompact(date) 346 - : TimestampFormatter.formatRelative(date); 347 - } 348 - 349 - function formatExpiration(dateStr) { 350 - const date = new Date(dateStr); 351 - const now = new Date(); 352 - const diffMs = date - now; 353 - 354 - // Already expired - show how long ago 355 - if (diffMs <= 0) { 356 - const agoMs = Math.abs(diffMs); 357 - const agoMins = Math.floor(agoMs / 60000); 358 - if (agoMins < 1) return 'expired'; 359 - if (agoMins < 60) return `expired ${agoMins}m ago`; 360 - const agoHours = Math.floor(agoMs / 3600000); 361 - if (agoHours < 24) return `expired ${agoHours}h ago`; 362 - const agoDays = Math.floor(agoMs / 86400000); 363 - return `expired ${agoDays}d ago`; 364 - } 365 - 366 - // Future - show when it clears 367 - return `clears ${relativeTimeFuture(dateStr)}`; 368 - } 369 - 370 - function relativeTimeFuture(dateStr) { 371 - const date = new Date(dateStr); 372 - const now = new Date(); 373 - const diffMs = date - now; 374 - 375 - if (diffMs <= 0) return 'now'; 376 - 377 - const diffMins = Math.floor(diffMs / 60000); 378 - const diffHours = Math.floor(diffMs / 3600000); 379 - const diffDays = Math.floor(diffMs / 86400000); 380 - 381 - if (diffMins < 1) return 'in less than a minute'; 382 - if (diffMins < 60) return `in ${diffMins}m`; 383 - if (diffHours < 24) { 384 - const remainingMins = diffMins % 60; 385 - return remainingMins === 0 ? `in ${diffHours}h` : `in ${diffHours}h ${remainingMins}m`; 386 - } 387 - if (diffDays < 7) { 388 - const remainingHours = diffHours % 24; 389 - return remainingHours === 0 ? `in ${diffDays}d` : `in ${diffDays}d ${remainingHours}h`; 390 - } 391 - 392 - // For longer times, show the date 393 - const timeStr = date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }).toLowerCase(); 394 - if (date.getFullYear() === now.getFullYear()) { 395 - return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) + ', ' + timeStr; 396 - } 397 - return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) + ', ' + timeStr; 398 - } 399 - 400 - function fullTimestamp(dateStr) { 401 - return TimestampFormatter.getFullTimestamp(new Date(dateStr)); 402 - } 403 - 404 - // Emoji picker 405 - let emojiData = null; 406 - let bufoList = null; 407 - let userFrequentEmojis = null; 408 - const DEFAULT_FREQUENT_EMOJIS = ['😊', '👍', '❤️', '😂', '🎉', '🔥', '✨', '💯', '🚀', '💪', '🙏', '👏', '😴', '🤔', '👀', '💻']; 409 - 410 - async function loadUserFrequentEmojis() { 411 - if (userFrequentEmojis) return userFrequentEmojis; 412 - if (!client) return DEFAULT_FREQUENT_EMOJIS; 413 - 414 - try { 415 - const user = client.getUser(); 416 - if (!user) return DEFAULT_FREQUENT_EMOJIS; 417 - 418 - // Fetch user's status history to count emoji usage 419 - const res = await fetch(`${CONFIG.server}/graphql`, { 420 - method: 'POST', 421 - headers: { 'Content-Type': 'application/json' }, 422 - body: JSON.stringify({ 423 - query: ` 424 - query GetUserEmojis($did: String!) { 425 - ioZzstoatzzStatusRecord( 426 - first: 100 427 - where: { did: { eq: $did } } 428 - ) { 429 - edges { node { emoji } } 430 - } 431 - } 432 - `, 433 - variables: { did: user.did } 434 - }) 435 - }); 436 - const json = await res.json(); 437 - const emojis = json.data?.ioZzstoatzzStatusRecord?.edges?.map(e => e.node.emoji) || []; 438 - 439 - if (emojis.length === 0) return DEFAULT_FREQUENT_EMOJIS; 440 - 441 - // Count emoji frequency 442 - const counts = {}; 443 - emojis.forEach(e => { counts[e] = (counts[e] || 0) + 1; }); 444 - 445 - // Sort by frequency and take top 16 446 - const sorted = Object.entries(counts) 447 - .sort((a, b) => b[1] - a[1]) 448 - .slice(0, 16) 449 - .map(([emoji]) => emoji); 450 - 451 - userFrequentEmojis = sorted.length > 0 ? sorted : DEFAULT_FREQUENT_EMOJIS; 452 - return userFrequentEmojis; 453 - } catch (e) { 454 - console.error('Failed to load frequent emojis:', e); 455 - return DEFAULT_FREQUENT_EMOJIS; 456 - } 457 - } 458 - 459 - async function loadBufoList() { 460 - if (bufoList) return bufoList; 461 - const res = await fetch('/bufos.json'); 462 - if (!res.ok) throw new Error('Failed to load bufos'); 463 - bufoList = await res.json(); 464 - return bufoList; 465 - } 466 - 467 - async function searchBufos(query, topK = 20) { 468 - const params = new URLSearchParams({ query, top_k: topK }); 469 - const res = await fetch(`https://find-bufo.fly.dev/api/search?${params}`); 470 - if (!res.ok) throw new Error('bufo search failed'); 471 - const data = await res.json(); 472 - return data.results; 473 - } 474 - 475 - async function loadEmojiData() { 476 - if (emojiData) return emojiData; 477 - try { 478 - const response = await fetch('https://cdn.jsdelivr.net/npm/emoji-datasource@15.1.0/emoji.json'); 479 - if (!response.ok) throw new Error('Failed to fetch'); 480 - const data = await response.json(); 481 - 482 - const emojis = {}; 483 - const categories = { frequent: DEFAULT_FREQUENT_EMOJIS, people: [], nature: [], food: [], activity: [], travel: [], objects: [], symbols: [], flags: [] }; 484 - const categoryMap = { 485 - 'Smileys & Emotion': 'people', 'People & Body': 'people', 'Animals & Nature': 'nature', 486 - 'Food & Drink': 'food', 'Activities': 'activity', 'Travel & Places': 'travel', 487 - 'Objects': 'objects', 'Symbols': 'symbols', 'Flags': 'flags' 488 - }; 489 - 490 - data.forEach(emoji => { 491 - const char = emoji.unified.split('-').map(u => String.fromCodePoint(parseInt(u, 16))).join(''); 492 - const keywords = [...(emoji.short_names || []), ...(emoji.name ? emoji.name.toLowerCase().split(/[\s_-]+/) : [])]; 493 - emojis[char] = keywords; 494 - const cat = categoryMap[emoji.category]; 495 - if (cat && categories[cat]) categories[cat].push(char); 496 - }); 497 - 498 - emojiData = { emojis, categories }; 499 - return emojiData; 500 - } catch (e) { 501 - console.error('Failed to load emoji data:', e); 502 - return { emojis: {}, categories: { frequent: DEFAULT_FREQUENT_EMOJIS, people: [], nature: [], food: [], activity: [], travel: [], objects: [], symbols: [], flags: [] } }; 503 - } 504 - } 505 - 506 - function searchEmojis(query, data) { 507 - if (!query) return []; 508 - const q = query.toLowerCase(); 509 - return Object.entries(data.emojis) 510 - .filter(([char, keywords]) => keywords.some(k => k.includes(q))) 511 - .map(([char]) => char) 512 - .slice(0, 50); 513 - } 514 - 515 - function createEmojiPicker(onSelect) { 516 - const overlay = document.createElement('div'); 517 - overlay.className = 'emoji-picker-overlay hidden'; 518 - overlay.innerHTML = ` 519 - <div class="emoji-picker"> 520 - <div class="emoji-picker-header"> 521 - <h3>pick an emoji</h3> 522 - <button class="emoji-picker-close" aria-label="close">✕</button> 523 - </div> 524 - <input type="text" class="emoji-search" placeholder="search emojis..."> 525 - <div class="emoji-categories"> 526 - <button class="category-btn active" data-category="frequent">⭐</button> 527 - <button class="category-btn" data-category="custom">🐸</button> 528 - <button class="category-btn" data-category="people">😊</button> 529 - <button class="category-btn" data-category="nature">🌿</button> 530 - <button class="category-btn" data-category="food">🍔</button> 531 - <button class="category-btn" data-category="activity">⚽</button> 532 - <button class="category-btn" data-category="travel">✈️</button> 533 - <button class="category-btn" data-category="objects">💡</button> 534 - <button class="category-btn" data-category="symbols">💕</button> 535 - <button class="category-btn" data-category="flags">🏁</button> 536 - </div> 537 - <div class="emoji-grid"></div> 538 - <div class="bufo-helper hidden"><a href="https://find-bufo.com" target="_blank">powered by find-bufo.com</a></div> 539 - </div> 540 - `; 541 - 542 - const picker = overlay.querySelector('.emoji-picker'); 543 - const grid = overlay.querySelector('.emoji-grid'); 544 - const search = overlay.querySelector('.emoji-search'); 545 - const closeBtn = overlay.querySelector('.emoji-picker-close'); 546 - const categoryBtns = overlay.querySelectorAll('.category-btn'); 547 - const bufoHelper = overlay.querySelector('.bufo-helper'); 548 - 549 - let currentCategory = 'frequent'; 550 - let data = null; 551 - let bufoSearchTimer = null; 552 - 553 - async function renderCategory(cat) { 554 - currentCategory = cat; 555 - categoryBtns.forEach(b => b.classList.toggle('active', b.dataset.category === cat)); 556 - bufoHelper.classList.toggle('hidden', cat !== 'custom'); 557 - 558 - if (cat === 'custom') { 559 - search.placeholder = 'describe a bufo... try "happy" or "apocalyptic"'; 560 - grid.classList.add('bufo-grid'); 561 - grid.innerHTML = '<div class="loading">loading bufos...</div>'; 562 - try { 563 - const bufos = await loadBufoList(); 564 - grid.innerHTML = bufos.map(name => ` 565 - <button class="emoji-btn bufo-btn" data-emoji="custom:${name}" title="${name}"> 566 - <img src="https://all-the.bufo.zone/${name}.png" alt="${name}" loading="lazy" onerror="this.src='https://all-the.bufo.zone/${name}.gif'"> 567 - </button> 568 - `).join(''); 569 - } catch (e) { 570 - grid.innerHTML = '<div class="no-results">failed to load bufos</div>'; 571 - } 572 - return; 573 - } 574 - 575 - search.placeholder = 'search emojis...'; 576 - 577 - grid.classList.remove('bufo-grid'); 578 - 579 - // Load user's frequent emojis for the frequent category 580 - if (cat === 'frequent') { 581 - grid.innerHTML = '<div class="loading">loading...</div>'; 582 - const frequentEmojis = await loadUserFrequentEmojis(); 583 - grid.innerHTML = frequentEmojis.map(e => { 584 - if (e.startsWith('custom:')) { 585 - const name = e.replace('custom:', ''); 586 - return `<button class="emoji-btn bufo-btn" data-emoji="${e}" title="${name}"> 587 - <img src="https://all-the.bufo.zone/${name}.png" alt="${name}" onerror="this.src='https://all-the.bufo.zone/${name}.gif'"> 588 - </button>`; 589 - } 590 - return `<button class="emoji-btn" data-emoji="${e}">${e}</button>`; 591 - }).join(''); 592 - return; 593 - } 594 - 595 - if (!data) data = await loadEmojiData(); 596 - const emojis = data.categories[cat] || []; 597 - grid.innerHTML = emojis.map(e => `<button class="emoji-btn" data-emoji="${e}">${e}</button>`).join(''); 598 - } 599 - 600 - function close() { 601 - overlay.classList.add('hidden'); 602 - search.value = ''; 603 - clearTimeout(bufoSearchTimer); 604 - } 605 - 606 - function open() { 607 - overlay.classList.remove('hidden'); 608 - renderCategory('frequent'); 609 - search.focus(); 610 - } 611 - 612 - overlay.addEventListener('click', e => { if (e.target === overlay) close(); }); 613 - closeBtn.addEventListener('click', close); 614 - categoryBtns.forEach(btn => btn.addEventListener('click', () => renderCategory(btn.dataset.category))); 615 - 616 - grid.addEventListener('click', e => { 617 - const btn = e.target.closest('.emoji-btn'); 618 - if (btn) { 619 - onSelect(btn.dataset.emoji); 620 - close(); 621 - } 622 - }); 623 - 624 - search.addEventListener('input', async () => { 625 - const q = search.value.trim(); 626 - if (!q) { renderCategory(currentCategory); return; } 627 - 628 - // When on the custom tab, use the findbufo semantic search API 629 - if (currentCategory === 'custom') { 630 - clearTimeout(bufoSearchTimer); 631 - bufoSearchTimer = setTimeout(async () => { 632 - grid.classList.add('bufo-grid'); 633 - bufoHelper.classList.remove('hidden'); 634 - grid.innerHTML = '<div class="loading">searching bufos...</div>'; 635 - try { 636 - const results = await searchBufos(q, 30); 637 - if (search.value.trim() !== q) return; // stale 638 - if (results.length === 0) { 639 - grid.innerHTML = '<div class="no-results">no bufos found</div>'; 640 - return; 641 - } 642 - grid.innerHTML = results.map(r => ` 643 - <button class="emoji-btn bufo-btn" data-emoji="custom:${r.name}" title="${r.name} (${Math.round(r.score * 100)}%)"> 644 - <img src="https://all-the.bufo.zone/${r.name}.png" alt="${r.name}" loading="lazy" onerror="this.src='https://all-the.bufo.zone/${r.name}.gif'"> 645 - <span class="bufo-score">${Math.round(r.score * 100)}%</span> 646 - </button> 647 - `).join(''); 648 - } catch (e) { 649 - grid.innerHTML = '<div class="no-results">search failed — try again</div>'; 650 - } 651 - }, 300); 652 - return; 653 - } 654 - 655 - // Default: search both emojis and bufos by name 656 - if (!data) data = await loadEmojiData(); 657 - const emojiResults = searchEmojis(q, data); 658 - 659 - let bufoResults = []; 660 - try { 661 - const bufos = await loadBufoList(); 662 - const qLower = q.toLowerCase(); 663 - bufoResults = bufos.filter(name => name.toLowerCase().includes(qLower)).slice(0, 30); 664 - } catch (e) { /* ignore */ } 665 - 666 - grid.classList.remove('bufo-grid'); 667 - bufoHelper.classList.add('hidden'); 668 - 669 - if (emojiResults.length === 0 && bufoResults.length === 0) { 670 - grid.innerHTML = '<div class="no-results">no emojis found</div>'; 671 - return; 672 - } 673 - 674 - let html = ''; 675 - html += emojiResults.map(e => `<button class="emoji-btn" data-emoji="${e}">${e}</button>`).join(''); 676 - html += bufoResults.map(name => ` 677 - <button class="emoji-btn bufo-btn" data-emoji="custom:${name}" title="${name}"> 678 - <img src="https://all-the.bufo.zone/${name}.png" alt="${name}" onerror="this.src='https://all-the.bufo.zone/${name}.gif'"> 679 - </button> 680 - `).join(''); 681 - 682 - grid.innerHTML = html; 683 - }); 684 - 685 - document.body.appendChild(overlay); 686 - return { open, close }; 687 - } 688 - 689 - // Render emoji (handles custom:name format) 690 - function renderEmoji(emoji) { 691 - if (emoji && emoji.startsWith('custom:')) { 692 - const name = emoji.slice(7); 693 - return `<img src="https://all-the.bufo.zone/${name}.png" alt="${name}" title="${name}" onerror="this.src='https://all-the.bufo.zone/${name}.gif'">`; 694 - } 695 - return emoji || '-'; 696 - } 697 - 698 - function escapeHtml(str) { 699 - if (!str) return ''; 700 - const div = document.createElement('div'); 701 - div.textContent = str; 702 - return div.innerHTML; 703 - } 704 - 705 - // Extract did and rkey from status uri (at://did/collection/rkey) 706 - function parseStatusUri(uri) { 707 - const parts = uri.split('/'); 708 - const did = parts[2]; 709 - const rkey = parts[parts.length - 1]; 710 - return { did, rkey }; 711 - } 712 - 713 - // Build permalink for a status 714 - function getStatusPermalink(uri) { 715 - const { did, rkey } = parseStatusUri(uri); 716 - return `${window.location.origin}/status/${did}/${rkey}`; 717 - } 718 - 719 - // Copy text to clipboard with visual feedback 720 - async function copyToClipboard(text, button) { 721 - try { 722 - await navigator.clipboard.writeText(text); 723 - button.classList.add('copied'); 724 - setTimeout(() => button.classList.remove('copied'), 1500); 725 - } catch (e) { 726 - console.error('Failed to copy:', e); 727 - } 728 - } 729 - 730 - // Parse markdown links [text](url) and return HTML 731 - function parseLinks(text) { 732 - if (!text) return ''; 733 - // First escape HTML, then parse markdown links 734 - const escaped = escapeHtml(text); 735 - // Match [text](url) pattern 736 - return escaped.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, linkText, url) => { 737 - // Validate URL (basic check) 738 - if (url.startsWith('http://') || url.startsWith('https://')) { 739 - return `<a href="${url}" target="_blank" rel="noopener">${linkText}</a>`; 740 - } 741 - return match; 742 - }); 743 - } 744 - 745 - // Handle typeahead state 746 - let handleSuggestions = []; 747 - let selectedSuggestionIndex = -1; 748 - let typeaheadDebounceTimer = null; 749 - let typeaheadAbortController = null; 750 - 751 - // Fetch handle suggestions from Bluesky 752 - async function fetchHandleSuggestions(query) { 753 - if (typeaheadAbortController) typeaheadAbortController.abort(); 754 - typeaheadAbortController = new AbortController(); 755 - 756 - try { 757 - const url = `https://typeahead.waow.tech/xrpc/app.bsky.actor.searchActorsTypeahead?q=${encodeURIComponent(query)}&limit=5`; 758 - const res = await fetch(url, { signal: typeaheadAbortController.signal }); 759 - if (!res.ok) throw new Error(`HTTP ${res.status}`); 760 - const data = await res.json(); 761 - return data.actors || []; 762 - } catch (e) { 763 - if (e.name === 'AbortError') return []; 764 - console.error('Typeahead error:', e); 765 - return []; 766 - } 767 - } 768 - 769 - // Render suggestions dropdown 770 - function renderSuggestions(suggestions, dropdown, input) { 771 - handleSuggestions = suggestions; 772 - selectedSuggestionIndex = -1; 773 - 774 - if (suggestions.length === 0) { 775 - dropdown.classList.add('hidden'); 776 - dropdown.innerHTML = ''; 777 - return; 778 - } 779 - 780 - dropdown.innerHTML = suggestions.map((s, i) => ` 781 - <button type="button" class="suggestion-item" data-handle="${escapeHtml(s.handle)}" data-index="${i}"> 782 - ${s.avatar ? `<img src="${escapeHtml(s.avatar)}" class="suggestion-avatar" alt="" />` : '<div class="suggestion-avatar-placeholder"></div>'} 783 - <div class="suggestion-info"> 784 - <span class="suggestion-name">${escapeHtml(s.displayName || s.handle)}</span> 785 - <span class="suggestion-handle">@${escapeHtml(s.handle)}</span> 786 - </div> 787 - </button> 788 - `).join(''); 789 - 790 - dropdown.classList.remove('hidden'); 791 - 792 - // Attach click handlers 793 - dropdown.querySelectorAll('.suggestion-item').forEach(btn => { 794 - btn.addEventListener('click', () => { 795 - input.value = btn.dataset.handle; 796 - dropdown.classList.add('hidden'); 797 - handleSuggestions = []; 798 - }); 799 - }); 800 - } 801 - 802 - // Handle keyboard navigation in suggestions 803 - function handleSuggestionKeydown(e, dropdown, input) { 804 - if (handleSuggestions.length === 0) return false; 805 - 806 - const items = dropdown.querySelectorAll('.suggestion-item'); 807 - 808 - switch (e.key) { 809 - case 'ArrowDown': 810 - e.preventDefault(); 811 - selectedSuggestionIndex = Math.min(selectedSuggestionIndex + 1, handleSuggestions.length - 1); 812 - items.forEach((item, i) => item.classList.toggle('selected', i === selectedSuggestionIndex)); 813 - return true; 814 - 815 - case 'ArrowUp': 816 - e.preventDefault(); 817 - selectedSuggestionIndex = Math.max(selectedSuggestionIndex - 1, -1); 818 - items.forEach((item, i) => item.classList.toggle('selected', i === selectedSuggestionIndex)); 819 - return true; 820 - 821 - case 'Enter': 822 - if (selectedSuggestionIndex >= 0) { 823 - e.preventDefault(); 824 - input.value = handleSuggestions[selectedSuggestionIndex].handle; 825 - dropdown.classList.add('hidden'); 826 - handleSuggestions = []; 827 - return true; 828 - } 829 - return false; 830 - 831 - case 'Escape': 832 - dropdown.classList.add('hidden'); 833 - handleSuggestions = []; 834 - return true; 835 - } 836 - return false; 837 - } 838 - 839 - // Handle input for typeahead 840 - function handleTypeaheadInput(input, dropdown) { 841 - const query = input.value.trim(); 842 - 843 - if (typeaheadDebounceTimer) clearTimeout(typeaheadDebounceTimer); 844 - 845 - if (query.length < 3) { 846 - dropdown.classList.add('hidden'); 847 - handleSuggestions = []; 848 - return; 849 - } 850 - 851 - typeaheadDebounceTimer = setTimeout(async () => { 852 - const suggestions = await fetchHandleSuggestions(query); 853 - renderSuggestions(suggestions, dropdown, input); 854 - }, 300); 855 - } 856 - 857 - // Resolve handle to DID 858 - async function resolveHandle(handle) { 859 - const res = await fetch(`https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`); 860 - if (!res.ok) return null; 861 - const data = await res.json(); 862 - return data.did; 863 - } 864 - 865 - // Resolve DID to handle 866 - async function resolveDidToHandle(did) { 867 - const res = await fetch(`https://plc.directory/${did}`); 868 - if (!res.ok) return null; 869 - const data = await res.json(); 870 - // alsoKnownAs is like ["at://handle"] 871 - if (data.alsoKnownAs && data.alsoKnownAs.length > 0) { 872 - return data.alsoKnownAs[0].replace('at://', ''); 873 - } 874 - return null; 875 - } 876 - 877 - // Router 878 - function getRoute() { 879 - let path = window.location.pathname; 880 - // Strip base path if present (for wisp.place or other subdirectory hosting) 881 - if (BASE_PATH && path.startsWith(BASE_PATH)) { 882 - path = path.slice(BASE_PATH.length) || '/'; 883 - } 884 - if (path === '/' || path === '/index.html') return { page: 'home' }; 885 - if (path === '/feed' || path === '/feed.html') return { page: 'feed' }; 886 - if (path.startsWith('/@')) { 887 - const handle = path.slice(2); 888 - return { page: 'profile', handle }; 889 - } 890 - // Match /status/{did}/{rkey} 891 - const statusMatch = path.match(/^\/status\/(did:[^/]+)\/([^/]+)$/); 892 - if (statusMatch) { 893 - return { page: 'status', did: statusMatch[1], rkey: statusMatch[2] }; 894 - } 895 - return { page: '404' }; 896 - } 897 - 898 - // Render home page 899 - async function renderHome() { 900 - const main = document.getElementById('main-content'); 901 - document.getElementById('page-title').textContent = 'status'; 902 - 903 - if (typeof QuicksliceClient === 'undefined') { 904 - main.innerHTML = '<div class="center">failed to load. check console.</div>'; 905 - return; 906 - } 907 - 908 - try { 909 - client = await QuicksliceClient.createQuicksliceClient({ 910 - server: CONFIG.server, 911 - clientId: CONFIG.clientId, 912 - redirectUri: window.location.origin + '/', 913 - }); 914 - console.log('Client created with server:', CONFIG.server, 'clientId:', CONFIG.clientId); 915 - 916 - if (window.location.search.includes('code=')) { 917 - console.log('Got OAuth callback with code, handling...'); 918 - try { 919 - const result = await client.handleRedirectCallback(); 920 - console.log('handleRedirectCallback result:', result); 921 - } catch (err) { 922 - console.error('handleRedirectCallback error:', err); 923 - } 924 - window.history.replaceState({}, document.title, '/'); 925 - } 926 - 927 - const isAuthed = await client.isAuthenticated(); 928 - 929 - if (!isAuthed) { 930 - main.innerHTML = ` 931 - <div class="login-container"> 932 - <div class="login-card"> 933 - <h2 class="login-title">what's happening?</h2> 934 - <p class="login-tagline">share what you're up to</p> 935 - <form id="login-form"> 936 - <div class="input-group"> 937 - <label for="handle-input">internet handle</label> 938 - <div class="handle-input-wrapper"> 939 - <input type="text" id="handle-input" placeholder="you.bsky.social" autocomplete="off" spellcheck="false" required> 940 - <div id="suggestions-dropdown" class="suggestions-dropdown hidden"></div> 941 - </div> 942 - </div> 943 - <button type="submit">sign in</button> 944 - </form> 945 - <div class="login-faq"> 946 - <button type="button" class="faq-toggle" data-faq="handle"> 947 - <span>what is an internet handle?</span> 948 - <svg class="chevron" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 949 - <polyline points="6 9 12 15 18 9"></polyline> 950 - </svg> 951 - </button> 952 - <div id="faq-handle" class="faq-content hidden"> 953 - <p> 954 - your internet handle is a domain that identifies you across apps built on 955 - <a href="https://atproto.com" target="_blank" rel="noopener">AT Protocol</a>. 956 - if you signed up for Bluesky or another ATProto service, you already have one 957 - (like <code>yourname.bsky.social</code>). 958 - </p> 959 - <p> 960 - read more at <a href="https://internethandle.org" target="_blank" rel="noopener">internethandle.org</a>. 961 - </p> 962 - </div> 963 - <button type="button" class="faq-toggle" data-faq="signup"> 964 - <span>don't have one?</span> 965 - <svg class="chevron" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 966 - <polyline points="6 9 12 15 18 9"></polyline> 967 - </svg> 968 - </button> 969 - <div id="faq-signup" class="faq-content hidden"> 970 - <p> 971 - the easiest way to get one is to sign up for <a href="https://bsky.app" target="_blank" rel="noopener">Bluesky</a>. 972 - once you have an account, you can use that handle here. 973 - </p> 974 - </div> 975 - </div> 976 - </div> 977 - </div> 978 - `; 979 - 980 - const loginForm = document.getElementById('login-form'); 981 - const handleInput = document.getElementById('handle-input'); 982 - const suggestionsDropdown = document.getElementById('suggestions-dropdown'); 983 - 984 - // Typeahead input handler 985 - handleInput.addEventListener('input', () => { 986 - handleTypeaheadInput(handleInput, suggestionsDropdown); 987 - }); 988 - 989 - // Keyboard navigation 990 - handleInput.addEventListener('keydown', (e) => { 991 - handleSuggestionKeydown(e, suggestionsDropdown, handleInput); 992 - }); 993 - 994 - // Close dropdown on blur (with delay for click events) 995 - handleInput.addEventListener('blur', () => { 996 - setTimeout(() => { 997 - suggestionsDropdown.classList.add('hidden'); 998 - }, 200); 999 - }); 1000 - 1001 - // Reopen on focus if there's content 1002 - handleInput.addEventListener('focus', () => { 1003 - if (handleInput.value.trim().length >= 3 && handleSuggestions.length > 0) { 1004 - suggestionsDropdown.classList.remove('hidden'); 1005 - } 1006 - }); 1007 - 1008 - loginForm.addEventListener('submit', async (e) => { 1009 - e.preventDefault(); 1010 - const handle = handleInput.value.trim(); 1011 - if (handle && client) { 1012 - await client.loginWithRedirect({ handle }); 1013 - } 1014 - }); 1015 - 1016 - // FAQ toggle handlers 1017 - document.querySelectorAll('.faq-toggle').forEach(btn => { 1018 - btn.addEventListener('click', () => { 1019 - const faqId = btn.dataset.faq; 1020 - const content = document.getElementById(`faq-${faqId}`); 1021 - const chevron = btn.querySelector('.chevron'); 1022 - if (content) { 1023 - content.classList.toggle('hidden'); 1024 - chevron?.classList.toggle('open'); 1025 - } 1026 - }); 1027 - }); 1028 - } else { 1029 - const user = client.getUser(); 1030 - if (!user) { 1031 - // Token might be invalid, log out 1032 - await client.logout(); 1033 - window.location.reload(); 1034 - return; 1035 - } 1036 - 1037 - // Load statuses first (includes actorHandle to avoid PLC lookup) 1038 - const res = await fetch(`${CONFIG.server}/graphql`, { 1039 - method: 'POST', 1040 - headers: { 'Content-Type': 'application/json' }, 1041 - body: JSON.stringify({ 1042 - query: ` 1043 - query GetUserStatuses($did: String!) { 1044 - ioZzstoatzzStatusRecord( 1045 - first: 100 1046 - where: { did: { eq: $did } } 1047 - sortBy: [{ field: "createdAt", direction: DESC }] 1048 - ) { 1049 - edges { node { uri did actorHandle emoji text createdAt expires } } 1050 - } 1051 - } 1052 - `, 1053 - variables: { did: user.did } 1054 - }) 1055 - }); 1056 - const json = await res.json(); 1057 - const statuses = json.data.ioZzstoatzzStatusRecord.edges.map(e => e.node); 1058 - 1059 - // Get handle from statuses if available, otherwise fall back to PLC lookup 1060 - const handle = statuses.length > 0 && statuses[0].actorHandle 1061 - ? statuses[0].actorHandle 1062 - : (await resolveDidToHandle(user.did) || user.did); 1063 - 1064 - // Load and apply preferences, set up settings/logout buttons 1065 - const prefs = await loadPreferences(); 1066 - applyPreferences(prefs); 1067 - 1068 - // Show settings button and set up modal 1069 - const settingsBtn = document.getElementById('settings-btn'); 1070 - settingsBtn.classList.remove('hidden'); 1071 - const settingsModal = createSettingsModal(); 1072 - settingsBtn.addEventListener('click', () => settingsModal.open(userPreferences || prefs)); 1073 - 1074 - // Add logout button to header nav (if not already there) 1075 - if (!document.getElementById('logout-btn')) { 1076 - const nav = document.querySelector('header nav'); 1077 - const logoutBtn = document.createElement('button'); 1078 - logoutBtn.id = 'logout-btn'; 1079 - logoutBtn.className = 'nav-btn'; 1080 - logoutBtn.setAttribute('aria-label', 'log out'); 1081 - logoutBtn.setAttribute('title', 'log out'); 1082 - logoutBtn.innerHTML = ` 1083 - <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 1084 - <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path> 1085 - <polyline points="16 17 21 12 16 7"></polyline> 1086 - <line x1="21" y1="12" x2="9" y2="12"></line> 1087 - </svg> 1088 - `; 1089 - logoutBtn.addEventListener('click', async () => { 1090 - await client.logout(); 1091 - window.location.href = '/'; 1092 - }); 1093 - nav.appendChild(logoutBtn); 1094 - } 1095 - 1096 - // Set page title with Bluesky profile link 1097 - document.getElementById('page-title').innerHTML = `<a href="https://bsky.app/profile/${handle}" target="_blank">@${handle}</a>`; 1098 - 1099 - let currentHtml = '<span class="big-emoji">-</span>'; 1100 - let historyHtml = ''; 1101 - 1102 - if (statuses.length > 0) { 1103 - const current = statuses[0]; 1104 - const expiresHtml = current.expires ? ` • ${formatExpiration(current.expires)}` : ''; 1105 - const currentRkey = current.uri.split('/').pop(); 1106 - currentHtml = ` 1107 - <span class="big-emoji">${renderEmoji(current.emoji)}</span> 1108 - <div class="status-info"> 1109 - ${current.text ? `<span id="current-text">${parseLinks(current.text)}</span>` : ''} 1110 - <span class="meta">since ${relativeTime(current.createdAt)}${expiresHtml}</span> 1111 - </div> 1112 - <div class="current-status-actions"> 1113 - <button class="share-btn current-share-btn" data-uri="${escapeHtml(current.uri)}" title="copy link"> 1114 - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 1115 - <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path> 1116 - <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path> 1117 - </svg> 1118 - </button> 1119 - <button class="embed-toggle-btn" title="get embed code"> 1120 - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 1121 - <polyline points="16 18 22 12 16 6"></polyline> 1122 - <polyline points="8 6 2 12 8 18"></polyline> 1123 - </svg> 1124 - </button> 1125 - <button class="delete-btn" data-rkey="${escapeHtml(currentRkey)}" title="delete"> 1126 - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 1127 - <line x1="18" y1="6" x2="6" y2="18"></line> 1128 - <line x1="6" y1="6" x2="18" y2="18"></line> 1129 - </svg> 1130 - </button> 1131 - </div> 1132 - `; 1133 - if (statuses.length > 1) { 1134 - historyHtml = '<section class="history"><h2>history</h2><div id="history-list">'; 1135 - statuses.slice(1).forEach(s => { 1136 - // Extract rkey from URI (at://did/collection/rkey) 1137 - const rkey = s.uri.split('/').pop(); 1138 - historyHtml += ` 1139 - <div class="status-item"> 1140 - <span class="emoji">${renderEmoji(s.emoji)}</span> 1141 - <div class="content"> 1142 - <div>${s.text ? `<span class="text">${parseLinks(s.text)}</span>` : ''}</div> 1143 - <span class="time">${relativeTime(s.createdAt)}</span> 1144 - </div> 1145 - <div class="status-actions"> 1146 - <button class="share-btn" data-uri="${escapeHtml(s.uri)}" title="copy link"> 1147 - <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 1148 - <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path> 1149 - <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path> 1150 - </svg> 1151 - </button> 1152 - <button class="delete-btn" data-rkey="${escapeHtml(rkey)}" title="delete"> 1153 - <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 1154 - <line x1="18" y1="6" x2="6" y2="18"></line> 1155 - <line x1="6" y1="6" x2="18" y2="18"></line> 1156 - </svg> 1157 - </button> 1158 - </div> 1159 - </div> 1160 - `; 1161 - }); 1162 - historyHtml += '</div></section>'; 1163 - } 1164 - } 1165 - 1166 - const currentEmoji = statuses.length > 0 ? statuses[0].emoji : '😊'; 1167 - 1168 - const embedCode = `<div id="status-embed"></div> 1169 - <script> 1170 - (async function() { 1171 - const did = '${user.did}'; 1172 - const handle = '${handle}'; 1173 - try { 1174 - const res = await fetch('https://pds.zzstoatzz.io/xrpc/com.atproto.repo.listRecords?repo=' + did + '&collection=io.zzstoatzz.status.record&limit=1'); 1175 - const data = await res.json(); 1176 - const record = data.records?.[0]?.value; 1177 - if (!record) return; 1178 - const emoji = record.emoji || ''; 1179 - const text = record.text || ''; 1180 - const isCustom = emoji.startsWith('custom:'); 1181 - const emojiHtml = isCustom 1182 - ? '<img src="https://all-the.bufo.zone/' + emoji.slice(7) + '.png" style="width:1.25em;height:1.25em;vertical-align:middle" onerror="this.src=this.src.replace(\\'.png\\',\\'.gif\\')">' 1183 - : emoji; 1184 - const displayText = text || (isCustom ? emoji.slice(7).replace(/-/g, ' ') : 'vibing'); 1185 - document.getElementById('status-embed').innerHTML = '<a href="https://status.zzstoatzz.io/@' + handle + '" target="_blank" style="text-decoration:none;color:inherit">' + emojiHtml + ' ' + displayText + '</a>'; 1186 - } catch(e) { console.error('status embed error:', e); } 1187 - })(); 1188 - </` + `script>`; 1189 - 1190 - main.innerHTML = ` 1191 - <div class="profile-card"> 1192 - <div class="current-status">${currentHtml}</div> 1193 - </div> 1194 - <div class="embed-section hidden" id="embed-section"> 1195 - <div class="embed-code-container"> 1196 - <pre class="embed-code"><code>${escapeHtml(embedCode)}</code></pre> 1197 - <button class="copy-embed-btn" title="copy code"> 1198 - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 1199 - <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect> 1200 - <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path> 1201 - </svg> 1202 - </button> 1203 - </div> 1204 - </div> 1205 - <form id="status-form" class="status-form"> 1206 - <div class="emoji-input-row"> 1207 - <button type="button" id="emoji-trigger" class="emoji-trigger"> 1208 - <span id="selected-emoji">${renderEmoji(currentEmoji)}</span> 1209 - </button> 1210 - <input type="hidden" id="emoji-input" value="${escapeHtml(currentEmoji)}"> 1211 - <input type="text" id="text-input" placeholder="what's happening?" maxlength="256"> 1212 - </div> 1213 - <div class="form-actions"> 1214 - <select id="expires-select"> 1215 - <option value="">don't clear</option> 1216 - <option value="30">30 min</option> 1217 - <option value="60">1 hour</option> 1218 - <option value="120">2 hours</option> 1219 - <option value="240">4 hours</option> 1220 - <option value="480">8 hours</option> 1221 - <option value="1440">1 day</option> 1222 - <option value="10080">1 week</option> 1223 - <option value="custom">custom...</option> 1224 - </select> 1225 - <input type="datetime-local" id="custom-datetime" class="custom-datetime hidden"> 1226 - <button type="submit">set status</button> 1227 - </div> 1228 - </form> 1229 - ${historyHtml} 1230 - `; 1231 - 1232 - // Set up emoji picker 1233 - const emojiInput = document.getElementById('emoji-input'); 1234 - const selectedEmojiEl = document.getElementById('selected-emoji'); 1235 - const emojiPicker = createEmojiPicker((emoji) => { 1236 - emojiInput.value = emoji; 1237 - selectedEmojiEl.innerHTML = renderEmoji(emoji); 1238 - }); 1239 - document.getElementById('emoji-trigger').addEventListener('click', () => emojiPicker.open()); 1240 - 1241 - // Custom datetime toggle 1242 - const expiresSelect = document.getElementById('expires-select'); 1243 - const customDatetime = document.getElementById('custom-datetime'); 1244 - 1245 - // Helper to format date for datetime-local input (local timezone) 1246 - function toLocalDatetimeString(date) { 1247 - const offset = date.getTimezoneOffset(); 1248 - const local = new Date(date.getTime() - offset * 60 * 1000); 1249 - return local.toISOString().slice(0, 16); 1250 - } 1251 - 1252 - expiresSelect.addEventListener('change', () => { 1253 - if (expiresSelect.value === 'custom') { 1254 - customDatetime.classList.remove('hidden'); 1255 - // Set min to now (prevent past dates) 1256 - const now = new Date(); 1257 - customDatetime.min = toLocalDatetimeString(now); 1258 - // Default to 1 hour from now 1259 - const defaultTime = new Date(Date.now() + 60 * 60 * 1000); 1260 - customDatetime.value = toLocalDatetimeString(defaultTime); 1261 - } else { 1262 - customDatetime.classList.add('hidden'); 1263 - } 1264 - }); 1265 - 1266 - document.getElementById('status-form').addEventListener('submit', async (e) => { 1267 - e.preventDefault(); 1268 - const emoji = document.getElementById('emoji-input').value.trim(); 1269 - const text = document.getElementById('text-input').value.trim(); 1270 - const expiresVal = document.getElementById('expires-select').value; 1271 - const customDt = document.getElementById('custom-datetime').value; 1272 - 1273 - if (!emoji) return; 1274 - 1275 - const input = { emoji, createdAt: new Date().toISOString() }; 1276 - if (text) input.text = text; 1277 - if (expiresVal === 'custom' && customDt) { 1278 - input.expires = new Date(customDt).toISOString(); 1279 - } else if (expiresVal && expiresVal !== 'custom') { 1280 - input.expires = new Date(Date.now() + parseInt(expiresVal) * 60 * 1000).toISOString(); 1281 - } 1282 - 1283 - try { 1284 - await client.mutate(` 1285 - mutation CreateStatus($input: CreateIoZzstoatzzStatusRecordInput!) { 1286 - createIoZzstoatzzStatusRecord(input: $input) { uri } 1287 - } 1288 - `, { input }); 1289 - window.location.reload(); 1290 - } catch (err) { 1291 - console.error('Failed to create status:', err); 1292 - alert('Failed to set status: ' + err.message); 1293 - } 1294 - }); 1295 - 1296 - // Delete buttons 1297 - document.querySelectorAll('.delete-btn').forEach(btn => { 1298 - btn.addEventListener('click', async () => { 1299 - const rkey = btn.dataset.rkey; 1300 - if (!confirm('Delete this status?')) return; 1301 - 1302 - try { 1303 - await client.mutate(` 1304 - mutation DeleteStatus($rkey: String!) { 1305 - deleteIoZzstoatzzStatusRecord(rkey: $rkey) { uri } 1306 - } 1307 - `, { rkey }); 1308 - window.location.reload(); 1309 - } catch (err) { 1310 - console.error('Failed to delete status:', err); 1311 - alert('Failed to delete: ' + err.message); 1312 - } 1313 - }); 1314 - }); 1315 - 1316 - // Share buttons 1317 - document.querySelectorAll('.share-btn').forEach(btn => { 1318 - btn.addEventListener('click', () => { 1319 - const uri = btn.dataset.uri; 1320 - const permalink = getStatusPermalink(uri); 1321 - copyToClipboard(permalink, btn); 1322 - }); 1323 - }); 1324 - 1325 - // Embed toggle button 1326 - const embedToggleBtn = document.querySelector('.embed-toggle-btn'); 1327 - const embedSection = document.getElementById('embed-section'); 1328 - if (embedToggleBtn && embedSection) { 1329 - embedToggleBtn.addEventListener('click', () => { 1330 - embedSection.classList.toggle('hidden'); 1331 - embedToggleBtn.classList.toggle('active'); 1332 - }); 1333 - } 1334 - 1335 - // Copy embed button 1336 - const copyEmbedBtn = document.querySelector('.copy-embed-btn'); 1337 - if (copyEmbedBtn) { 1338 - copyEmbedBtn.addEventListener('click', () => { 1339 - copyToClipboard(embedCode, copyEmbedBtn); 1340 - }); 1341 - } 1342 - } 1343 - } catch (e) { 1344 - console.error('Failed to init:', e); 1345 - main.innerHTML = '<div class="center">failed to initialize. check console.</div>'; 1346 - } 1347 - } 1348 - 1349 - // Render feed page 1350 - let feedCursor = null; 1351 - let feedHasMore = true; 1352 - 1353 - async function renderFeed(append = false) { 1354 - const main = document.getElementById('main-content'); 1355 - document.getElementById('page-title').textContent = 'global feed'; 1356 - 1357 - if (!append) { 1358 - // Initialize auth UI for header elements 1359 - await initAuthUI(); 1360 - main.innerHTML = '<div id="feed-list" class="feed-list"><div class="center">loading...</div></div><div id="load-more" class="center hidden"><button id="load-more-btn">load more</button></div><div id="end-of-feed" class="center hidden"><span class="meta">you\'ve reached the end</span></div>'; 1361 - } 1362 - 1363 - const feedList = document.getElementById('feed-list'); 1364 - 1365 - try { 1366 - const res = await fetch(`${CONFIG.server}/graphql`, { 1367 - method: 'POST', 1368 - headers: { 'Content-Type': 'application/json' }, 1369 - body: JSON.stringify({ 1370 - query: ` 1371 - query GetFeed($after: String) { 1372 - ioZzstoatzzStatusRecord(first: 20, after: $after, sortBy: [{ field: "createdAt", direction: DESC }]) { 1373 - edges { node { uri did actorHandle emoji text createdAt } cursor } 1374 - pageInfo { hasNextPage endCursor } 1375 - } 1376 - } 1377 - `, 1378 - variables: { after: append ? feedCursor : null } 1379 - }) 1380 - }); 1381 - 1382 - const json = await res.json(); 1383 - const data = json.data.ioZzstoatzzStatusRecord; 1384 - const statuses = data.edges.map(e => e.node); 1385 - feedCursor = data.pageInfo.endCursor; 1386 - feedHasMore = data.pageInfo.hasNextPage; 1387 - 1388 - if (!append) { 1389 - feedList.innerHTML = ''; 1390 - } 1391 - 1392 - statuses.forEach((status) => { 1393 - const handle = status.actorHandle || status.did.slice(8, 28); 1394 - const div = document.createElement('div'); 1395 - div.className = 'status-item'; 1396 - div.innerHTML = ` 1397 - <span class="emoji">${renderEmoji(status.emoji)}</span> 1398 - <div class="content"> 1399 - <div> 1400 - <a href="/@${handle}" class="author">@${handle}</a> 1401 - ${status.text ? `<span class="text">${parseLinks(status.text)}</span>` : ''} 1402 - </div> 1403 - <span class="time">${relativeTime(status.createdAt)}</span> 1404 - </div> 1405 - <button class="share-btn" data-uri="${escapeHtml(status.uri)}" title="copy link"> 1406 - <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 1407 - <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path> 1408 - <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path> 1409 - </svg> 1410 - </button> 1411 - `; 1412 - // Attach share button handler 1413 - div.querySelector('.share-btn').addEventListener('click', (e) => { 1414 - const permalink = getStatusPermalink(status.uri); 1415 - copyToClipboard(permalink, e.currentTarget); 1416 - }); 1417 - feedList.appendChild(div); 1418 - }); 1419 - 1420 - const loadMore = document.getElementById('load-more'); 1421 - const endOfFeed = document.getElementById('end-of-feed'); 1422 - if (feedHasMore) { 1423 - loadMore.classList.remove('hidden'); 1424 - endOfFeed.classList.add('hidden'); 1425 - } else { 1426 - loadMore.classList.add('hidden'); 1427 - endOfFeed.classList.remove('hidden'); 1428 - } 1429 - 1430 - // Attach load more handler 1431 - const btn = document.getElementById('load-more-btn'); 1432 - if (btn && !btn.dataset.bound) { 1433 - btn.dataset.bound = 'true'; 1434 - btn.addEventListener('click', () => renderFeed(true)); 1435 - } 1436 - } catch (e) { 1437 - console.error('Failed to load feed:', e); 1438 - if (!append) { 1439 - feedList.innerHTML = '<div class="center">failed to load feed</div>'; 1440 - } 1441 - } 1442 - } 1443 - 1444 - // Render profile page 1445 - async function renderProfile(handle) { 1446 - const main = document.getElementById('main-content'); 1447 - const pageTitle = document.getElementById('page-title'); 1448 - 1449 - // Initialize auth UI for header elements 1450 - await initAuthUI(); 1451 - 1452 - pageTitle.innerHTML = `<a href="https://bsky.app/profile/${handle}" target="_blank">@${handle}</a>`; 1453 - 1454 - main.innerHTML = '<div class="center">loading...</div>'; 1455 - 1456 - try { 1457 - // Resolve handle to DID 1458 - const did = await resolveHandle(handle); 1459 - if (!did) { 1460 - main.innerHTML = '<div class="center">user not found</div>'; 1461 - return; 1462 - } 1463 - 1464 - const res = await fetch(`${CONFIG.server}/graphql`, { 1465 - method: 'POST', 1466 - headers: { 'Content-Type': 'application/json' }, 1467 - body: JSON.stringify({ 1468 - query: ` 1469 - query GetUserStatuses($did: String!) { 1470 - ioZzstoatzzStatusRecord(first: 20, where: { did: { eq: $did } }, sortBy: [{ field: "createdAt", direction: DESC }]) { 1471 - edges { node { uri did emoji text createdAt expires } } 1472 - } 1473 - } 1474 - `, 1475 - variables: { did } 1476 - }) 1477 - }); 1478 - 1479 - const json = await res.json(); 1480 - const statuses = json.data.ioZzstoatzzStatusRecord.edges.map(e => e.node); 1481 - 1482 - if (statuses.length === 0) { 1483 - main.innerHTML = '<div class="center">no statuses yet</div>'; 1484 - return; 1485 - } 1486 - 1487 - const current = statuses[0]; 1488 - const expiresHtml = current.expires ? ` • ${formatExpiration(current.expires)}` : ''; 1489 - let html = ` 1490 - <div class="profile-card"> 1491 - <div class="current-status"> 1492 - <span class="big-emoji">${renderEmoji(current.emoji)}</span> 1493 - <div class="status-info"> 1494 - ${current.text ? `<span id="current-text">${parseLinks(current.text)}</span>` : ''} 1495 - <span class="meta">${relativeTime(current.createdAt)}${expiresHtml}</span> 1496 - </div> 1497 - </div> 1498 - </div> 1499 - `; 1500 - 1501 - if (statuses.length > 1) { 1502 - html += '<section class="history"><h2>history</h2><div class="feed-list">'; 1503 - statuses.slice(1).forEach(status => { 1504 - html += ` 1505 - <div class="status-item"> 1506 - <span class="emoji">${renderEmoji(status.emoji)}</span> 1507 - <div class="content"> 1508 - <div>${status.text ? `<span class="text">${parseLinks(status.text)}</span>` : ''}</div> 1509 - <span class="time">${relativeTime(status.createdAt)}</span> 1510 - </div> 1511 - <button class="share-btn" data-uri="${escapeHtml(status.uri)}" title="copy link"> 1512 - <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 1513 - <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path> 1514 - <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path> 1515 - </svg> 1516 - </button> 1517 - </div> 1518 - `; 1519 - }); 1520 - html += '</div></section>'; 1521 - } 1522 - 1523 - main.innerHTML = html; 1524 - 1525 - // Share buttons 1526 - document.querySelectorAll('.share-btn').forEach(btn => { 1527 - btn.addEventListener('click', () => { 1528 - const uri = btn.dataset.uri; 1529 - const permalink = getStatusPermalink(uri); 1530 - copyToClipboard(permalink, btn); 1531 - }); 1532 - }); 1533 - } catch (e) { 1534 - console.error('Failed to load profile:', e); 1535 - main.innerHTML = '<div class="center">failed to load profile</div>'; 1536 - } 1537 - } 1538 - 1539 - // Render single status permalink page 1540 - async function renderStatus(did, rkey) { 1541 - const main = document.getElementById('main-content'); 1542 - const pageTitle = document.getElementById('page-title'); 1543 - 1544 - // Initialize auth UI for header elements 1545 - await initAuthUI(); 1546 - 1547 - pageTitle.textContent = 'status'; 1548 - main.innerHTML = '<div class="center">loading...</div>'; 1549 - 1550 - try { 1551 - // Fetch the specific status by did and rkey 1552 - const res = await fetch(`${CONFIG.server}/graphql`, { 1553 - method: 'POST', 1554 - headers: { 'Content-Type': 'application/json' }, 1555 - body: JSON.stringify({ 1556 - query: ` 1557 - query GetStatus($did: String!, $rkey: String!) { 1558 - ioZzstoatzzStatusRecord( 1559 - first: 1 1560 - where: { 1561 - did: { eq: $did } 1562 - uri: { endsWith: $rkey } 1563 - } 1564 - ) { 1565 - edges { node { uri did actorHandle emoji text createdAt expires } } 1566 - } 1567 - } 1568 - `, 1569 - variables: { did, rkey: `/${rkey}` } 1570 - }) 1571 - }); 1572 - 1573 - const json = await res.json(); 1574 - const statuses = json.data?.ioZzstoatzzStatusRecord?.edges?.map(e => e.node) || []; 1575 - 1576 - if (statuses.length === 0) { 1577 - main.innerHTML = '<div class="center">status not found</div>'; 1578 - return; 1579 - } 1580 - 1581 - const status = statuses[0]; 1582 - const handle = status.actorHandle || await resolveDidToHandle(status.did) || status.did.slice(8, 28); 1583 - const expiresHtml = status.expires ? ` • ${formatExpiration(status.expires)}` : ''; 1584 - 1585 - pageTitle.innerHTML = `<a href="/@${handle}" target="_blank">@${handle}</a>`; 1586 - 1587 - main.innerHTML = ` 1588 - <div class="profile-card"> 1589 - <div class="current-status"> 1590 - <span class="big-emoji">${renderEmoji(status.emoji)}</span> 1591 - <div class="status-info"> 1592 - ${status.text ? `<span id="current-text">${parseLinks(status.text)}</span>` : ''} 1593 - <span class="meta">${relativeTime(status.createdAt)}${expiresHtml}</span> 1594 - </div> 1595 - </div> 1596 - </div> 1597 - <div class="center"> 1598 - <a href="/@${handle}" class="view-profile-link">view all statuses from @${handle}</a> 1599 - </div> 1600 - `; 1601 - } catch (e) { 1602 - console.error('Failed to load status:', e); 1603 - main.innerHTML = '<div class="center">failed to load status</div>'; 1604 - } 1605 - } 1606 - 1607 - // Update nav active state - hide current page icon, show the other 1608 - function updateNavActive(page) { 1609 - const navHome = document.getElementById('nav-home'); 1610 - const navFeed = document.getElementById('nav-feed'); 1611 - // Hide the nav icon for the current page, show the other 1612 - if (navHome) navHome.classList.toggle('hidden', page === 'home'); 1613 - if (navFeed) navFeed.classList.toggle('hidden', page === 'feed'); 1614 - } 1615 - 1616 - // Initialize auth state for header (settings, logout) - used by all pages 1617 - async function initAuthUI() { 1618 - if (typeof QuicksliceClient === 'undefined') return; 1619 - 1620 - try { 1621 - client = await QuicksliceClient.createQuicksliceClient({ 1622 - server: CONFIG.server, 1623 - clientId: CONFIG.clientId, 1624 - redirectUri: window.location.origin + '/', 1625 - }); 1626 - 1627 - const isAuthed = await client.isAuthenticated(); 1628 - if (!isAuthed) return; 1629 - 1630 - const user = client.getUser(); 1631 - if (!user) return; 1632 - 1633 - // Load and apply preferences 1634 - const prefs = await loadPreferences(); 1635 - applyPreferences(prefs); 1636 - 1637 - // Show settings button and set up modal 1638 - const settingsBtn = document.getElementById('settings-btn'); 1639 - settingsBtn.classList.remove('hidden'); 1640 - const settingsModal = createSettingsModal(); 1641 - settingsBtn.addEventListener('click', () => settingsModal.open(userPreferences || prefs)); 1642 - 1643 - // Add logout button to header nav (if not already there) 1644 - if (!document.getElementById('logout-btn')) { 1645 - const nav = document.querySelector('header nav'); 1646 - const logoutBtn = document.createElement('button'); 1647 - logoutBtn.id = 'logout-btn'; 1648 - logoutBtn.className = 'nav-btn'; 1649 - logoutBtn.setAttribute('aria-label', 'log out'); 1650 - logoutBtn.setAttribute('title', 'log out'); 1651 - logoutBtn.innerHTML = ` 1652 - <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 1653 - <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path> 1654 - <polyline points="16 17 21 12 16 7"></polyline> 1655 - <line x1="21" y1="12" x2="9" y2="12"></line> 1656 - </svg> 1657 - `; 1658 - logoutBtn.addEventListener('click', async () => { 1659 - await client.logout(); 1660 - window.location.href = '/'; 1661 - }); 1662 - nav.appendChild(logoutBtn); 1663 - } 1664 - 1665 - return { user, prefs }; 1666 - } catch (e) { 1667 - console.error('Failed to init auth UI:', e); 1668 - return null; 1669 - } 1670 - } 1671 - 1672 - // Init 1673 - document.addEventListener('DOMContentLoaded', () => { 1674 - initTheme(); 1675 - 1676 - const themeBtn = document.getElementById('theme-toggle'); 1677 - if (themeBtn) { 1678 - themeBtn.addEventListener('click', toggleTheme); 1679 - } 1680 - 1681 - const route = getRoute(); 1682 - updateNavActive(route.page); 1683 - 1684 - if (route.page === 'home') { 1685 - renderHome(); 1686 - } else if (route.page === 'feed') { 1687 - renderFeed(); 1688 - } else if (route.page === 'profile') { 1689 - renderProfile(route.handle); 1690 - } else if (route.page === 'status') { 1691 - renderStatus(route.did, route.rkey); 1692 - } else { 1693 - document.getElementById('main-content').innerHTML = '<div class="center">page not found</div>'; 1694 - } 1695 - });
site/bufos.json static/bufos.json
site/favicon.svg static/favicon.svg
-143
site/functions/status/[did]/[rkey].js
··· 1 - // CloudFlare Pages Function to handle /status/:did/:rkey routes 2 - // Injects OG meta tags for social media crawlers 3 - 4 - const GRAPHQL_ENDPOINT = 'https://zzstoatzz-quickslice-status.fly.dev/graphql'; 5 - const SITE_URL = 'https://status.zzstoatzz.io'; 6 - 7 - // Social media bot user agents 8 - const BOT_USER_AGENTS = [ 9 - 'Twitterbot', 10 - 'facebookexternalhit', 11 - 'LinkedInBot', 12 - 'Slackbot', 13 - 'Discordbot', 14 - 'TelegramBot', 15 - 'WhatsApp', 16 - 'Bluesky', 17 - ]; 18 - 19 - function isSocialBot(userAgent) { 20 - if (!userAgent) return false; 21 - return BOT_USER_AGENTS.some(bot => userAgent.includes(bot)); 22 - } 23 - 24 - async function fetchStatus(did, rkey) { 25 - const response = await fetch(GRAPHQL_ENDPOINT, { 26 - method: 'POST', 27 - headers: { 'Content-Type': 'application/json' }, 28 - body: JSON.stringify({ 29 - query: ` 30 - query GetStatus($did: String!, $rkey: String!) { 31 - ioZzstoatzzStatusRecord( 32 - first: 1 33 - where: { 34 - did: { eq: $did } 35 - uri: { endsWith: $rkey } 36 - } 37 - ) { 38 - edges { node { uri did actorHandle emoji text createdAt } } 39 - } 40 - } 41 - `, 42 - variables: { did, rkey: `/${rkey}` } 43 - }) 44 - }); 45 - 46 - const json = await response.json(); 47 - const statuses = json.data?.ioZzstoatzzStatusRecord?.edges?.map(e => e.node) || []; 48 - return statuses[0] || null; 49 - } 50 - 51 - function getEmojiDisplay(emoji) { 52 - if (emoji && emoji.startsWith('custom:')) { 53 - return emoji.slice(7).replace(/-/g, ' '); // "bufo-stab" -> "bufo stab" 54 - } 55 - return emoji || ''; 56 - } 57 - 58 - function getOgImageUrl(emoji) { 59 - // For custom emojis (bufos), use the bufo.zone image directly 60 - if (emoji && emoji.startsWith('custom:')) { 61 - const name = emoji.slice(7); 62 - return `https://all-the.bufo.zone/${name}.png`; 63 - } 64 - // For standard emojis, no image (social platforms will use text) 65 - return null; 66 - } 67 - 68 - function generateOgHtml(status, did, rkey, handle) { 69 - const emojiDisplay = getEmojiDisplay(status.emoji); 70 - const title = `@${handle}'s status`; 71 - // prioritize user's text, fall back to emoji name if no text 72 - const description = status.text || emojiDisplay; 73 - const url = `${SITE_URL}/status/${did}/${rkey}`; 74 - const imageUrl = getOgImageUrl(status.emoji); 75 - 76 - const imageMetaTags = imageUrl ? ` 77 - <meta property="og:image" content="${imageUrl}"> 78 - <meta name="twitter:image" content="${imageUrl}">` : ''; 79 - 80 - const twitterCard = imageUrl ? 'summary_large_image' : 'summary'; 81 - 82 - return `<!DOCTYPE html> 83 - <html lang="en"> 84 - <head> 85 - <meta charset="utf-8"> 86 - <meta name="viewport" content="width=device-width, initial-scale=1.0"> 87 - <title>${title} | status</title> 88 - 89 - <!-- Open Graph --> 90 - <meta property="og:type" content="website"> 91 - <meta property="og:title" content="${title}"> 92 - <meta property="og:description" content="${description}"> 93 - <meta property="og:url" content="${url}"> 94 - <meta property="og:site_name" content="status">${imageMetaTags} 95 - 96 - <!-- Twitter Card --> 97 - <meta name="twitter:card" content="${twitterCard}"> 98 - <meta name="twitter:title" content="${title}"> 99 - <meta name="twitter:description" content="${description}"> 100 - 101 - <!-- Redirect browsers to the actual page --> 102 - <meta http-equiv="refresh" content="0;url=${url}"> 103 - <link rel="canonical" href="${url}"> 104 - </head> 105 - <body> 106 - <p>Redirecting to <a href="${url}">${url}</a></p> 107 - </body> 108 - </html>`; 109 - } 110 - 111 - export async function onRequest(context) { 112 - const { request, params, next } = context; 113 - const { did, rkey } = params; 114 - 115 - const userAgent = request.headers.get('user-agent') || ''; 116 - 117 - // If not a social bot, pass through to the SPA 118 - if (!isSocialBot(userAgent)) { 119 - return next(); 120 - } 121 - 122 - try { 123 - const status = await fetchStatus(did, rkey); 124 - 125 - if (!status) { 126 - // Status not found, let the SPA handle it 127 - return next(); 128 - } 129 - 130 - const handle = status.actorHandle || did.slice(8, 28); 131 - const html = generateOgHtml(status, did, rkey, handle); 132 - 133 - return new Response(html, { 134 - headers: { 135 - 'Content-Type': 'text/html;charset=UTF-8', 136 - 'Cache-Control': 'public, max-age=3600', // Cache for 1 hour 137 - }, 138 - }); 139 - } catch (error) { 140 - console.error('Error fetching status for OG tags:', error); 141 - return next(); 142 - } 143 - }
-62
site/index.html
··· 1 - <!DOCTYPE html> 2 - <html lang="en"> 3 - <head> 4 - <meta charset="utf-8"> 5 - <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 - <title>status</title> 7 - 8 - <!-- Open Graph (fallback) --> 9 - <meta property="og:type" content="website"> 10 - <meta property="og:title" content="status"> 11 - <meta property="og:description" content="share your status"> 12 - <meta property="og:url" content="https://status.zzstoatzz.io"> 13 - <meta property="og:site_name" content="status"> 14 - 15 - <!-- Twitter Card (fallback) --> 16 - <meta name="twitter:card" content="summary"> 17 - <meta name="twitter:title" content="status"> 18 - <meta name="twitter:description" content="share your status"> 19 - 20 - <link rel="icon" type="image/svg+xml" href="/favicon.svg"> 21 - <link rel="stylesheet" href="/styles.css"> 22 - <script src="https://cdn.jsdelivr.net/gh/bigmoves/quickslice@v0.17.3/quickslice-client-js/dist/quickslice-client.min.js"></script> 23 - </head> 24 - <body> 25 - <div id="app"> 26 - <header> 27 - <h1 id="page-title">status</h1> 28 - <nav> 29 - <a href="/" id="nav-home" class="nav-btn" aria-label="home" title="home"> 30 - <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 31 - <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path> 32 - <polyline points="9 22 9 12 15 12 15 22"></polyline> 33 - </svg> 34 - </a> 35 - <a href="/feed" id="nav-feed" class="nav-btn" aria-label="global feed" title="global feed"> 36 - <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 37 - <circle cx="12" cy="12" r="10"></circle> 38 - <line x1="2" y1="12" x2="22" y2="12"></line> 39 - <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path> 40 - </svg> 41 - </a> 42 - <button id="settings-btn" class="nav-btn hidden" aria-label="settings" title="settings"> 43 - <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 44 - <circle cx="12" cy="12" r="3"></circle> 45 - <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path> 46 - </svg> 47 - </button> 48 - <button id="theme-toggle" aria-label="toggle theme"> 49 - <span class="sun">☀</span> 50 - <span class="moon">☾</span> 51 - </button> 52 - </nav> 53 - </header> 54 - 55 - <main id="main-content"> 56 - <div class="center">loading...</div> 57 - </main> 58 - </div> 59 - 60 - <script src="/app.js"></script> 61 - </body> 62 - </html>
-7
site/package.json
··· 1 - { 2 - "name": "status-site", 3 - "private": true, 4 - "dependencies": { 5 - "@cloudflare/pages-plugin-vercel-og": "^0.1.2" 6 - } 7 - }
-1152
site/styles.css
··· 1 - :root { 2 - --bg: #0a0a0a; 3 - --bg-card: #1a1a1a; 4 - --text: #ffffff; 5 - --text-secondary: #888; 6 - --accent: #4a9eff; 7 - --border: #2a2a2a; 8 - --radius: 12px; 9 - --font-family: ui-monospace, "SF Mono", Monaco, monospace; 10 - } 11 - 12 - [data-theme="light"] { 13 - --bg: #ffffff; 14 - --bg-card: #f5f5f5; 15 - --text: #1a1a1a; 16 - --text-secondary: #666; 17 - --border: #e0e0e0; 18 - } 19 - 20 - * { 21 - margin: 0; 22 - padding: 0; 23 - box-sizing: border-box; 24 - } 25 - 26 - /* Theme-aware scrollbars */ 27 - ::-webkit-scrollbar { 28 - width: 8px; 29 - height: 8px; 30 - } 31 - 32 - ::-webkit-scrollbar-track { 33 - background: var(--bg); 34 - } 35 - 36 - ::-webkit-scrollbar-thumb { 37 - background: var(--border); 38 - border-radius: 4px; 39 - } 40 - 41 - ::-webkit-scrollbar-thumb:hover { 42 - background: var(--text-secondary); 43 - } 44 - 45 - /* Firefox */ 46 - * { 47 - scrollbar-width: thin; 48 - scrollbar-color: var(--border) var(--bg); 49 - } 50 - 51 - body { 52 - font-family: var(--font-family); 53 - background: var(--bg); 54 - color: var(--text); 55 - line-height: 1.6; 56 - min-height: 100vh; 57 - } 58 - 59 - #app { 60 - max-width: 600px; 61 - margin: 0 auto; 62 - padding: 2rem 1rem; 63 - } 64 - 65 - header { 66 - display: flex; 67 - justify-content: space-between; 68 - align-items: center; 69 - margin-bottom: 2rem; 70 - padding-bottom: 1rem; 71 - border-bottom: 1px solid var(--border); 72 - } 73 - 74 - header h1 { 75 - font-size: 1.5rem; 76 - font-weight: 600; 77 - } 78 - 79 - nav { 80 - display: flex; 81 - gap: 1rem; 82 - align-items: center; 83 - } 84 - 85 - nav a { 86 - color: var(--text-secondary); 87 - text-decoration: none; 88 - } 89 - 90 - nav a:hover { 91 - color: var(--accent); 92 - } 93 - 94 - .nav-btn { 95 - display: flex; 96 - align-items: center; 97 - justify-content: center; 98 - padding: 0.5rem; 99 - border-radius: 8px; 100 - transition: background 0.15s, color 0.15s; 101 - color: var(--text-secondary); 102 - background: none; 103 - border: none; 104 - cursor: pointer; 105 - } 106 - 107 - .nav-btn:hover { 108 - background: var(--bg-card); 109 - color: var(--accent); 110 - } 111 - 112 - .nav-btn.active { 113 - color: var(--accent); 114 - } 115 - 116 - .nav-btn svg { 117 - display: block; 118 - } 119 - 120 - #theme-toggle { 121 - background: none; 122 - border: 1px solid var(--border); 123 - border-radius: 8px; 124 - padding: 0.5rem; 125 - cursor: pointer; 126 - font-size: 1rem; 127 - } 128 - 129 - #theme-toggle .sun { display: none; } 130 - #theme-toggle .moon { display: inline; color: var(--text); } 131 - [data-theme="light"] #theme-toggle .sun { display: inline; color: var(--text); } 132 - [data-theme="light"] #theme-toggle .moon { display: none; } 133 - 134 - .hidden { display: none !important; } 135 - .center { text-align: center; padding: 2rem; } 136 - 137 - /* Login form - base button styles */ 138 - button[type="submit"] { 139 - padding: 0.75rem 1.5rem; 140 - background: var(--accent); 141 - color: white; 142 - border: none; 143 - border-radius: var(--radius); 144 - cursor: pointer; 145 - font-family: inherit; 146 - font-size: 1rem; 147 - } 148 - 149 - button[type="submit"]:hover { 150 - opacity: 0.9; 151 - } 152 - 153 - /* Login container - centered layout */ 154 - .login-container { 155 - display: flex; 156 - flex-direction: column; 157 - align-items: center; 158 - justify-content: center; 159 - min-height: 50vh; 160 - padding: 2rem 1rem; 161 - } 162 - 163 - .login-card { 164 - background: var(--bg-card); 165 - border: 1px solid var(--border); 166 - border-radius: var(--radius); 167 - padding: 2.5rem 2rem; 168 - width: 100%; 169 - max-width: 380px; 170 - text-align: center; 171 - } 172 - 173 - .login-title { 174 - font-size: 1.75rem; 175 - font-weight: 600; 176 - margin-bottom: 0.5rem; 177 - } 178 - 179 - .login-tagline { 180 - color: var(--text-secondary); 181 - font-size: 1rem; 182 - margin-bottom: 1.5rem; 183 - } 184 - 185 - .login-card #login-form { 186 - display: flex; 187 - flex-direction: column; 188 - gap: 1.25rem; 189 - } 190 - 191 - .login-card .input-group { 192 - display: flex; 193 - flex-direction: column; 194 - gap: 0.5rem; 195 - } 196 - 197 - .login-card .input-group label { 198 - color: var(--text-secondary); 199 - font-size: 0.9rem; 200 - } 201 - 202 - .handle-input-wrapper { 203 - position: relative; 204 - width: 100%; 205 - } 206 - 207 - .handle-input-wrapper input { 208 - width: 100%; 209 - padding: 0.875rem 1rem; 210 - border: 1px solid var(--border); 211 - border-radius: var(--radius); 212 - background: var(--bg); 213 - color: var(--text); 214 - font-family: inherit; 215 - font-size: 1rem; 216 - transition: border-color 0.15s; 217 - box-sizing: border-box; 218 - } 219 - 220 - .handle-input-wrapper input:focus { 221 - outline: none; 222 - border-color: var(--accent); 223 - } 224 - 225 - .login-card button[type="submit"] { 226 - width: 100%; 227 - padding: 0.875rem 1rem; 228 - box-sizing: border-box; 229 - } 230 - 231 - /* Suggestions dropdown */ 232 - .suggestions-dropdown { 233 - position: absolute; 234 - top: 100%; 235 - left: 0; 236 - right: 0; 237 - margin-top: 4px; 238 - background: var(--bg-card); 239 - border: 1px solid var(--border); 240 - border-radius: 8px; 241 - overflow: hidden; 242 - z-index: 100; 243 - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); 244 - } 245 - 246 - .suggestion-item { 247 - display: flex; 248 - align-items: center; 249 - gap: 10px; 250 - width: 100%; 251 - padding: 10px 12px; 252 - background: transparent; 253 - border: none; 254 - border-bottom: 1px solid var(--border); 255 - color: var(--text); 256 - cursor: pointer; 257 - text-align: left; 258 - font-family: inherit; 259 - transition: background 0.15s; 260 - } 261 - 262 - .suggestion-item:last-child { 263 - border-bottom: none; 264 - } 265 - 266 - .suggestion-item:hover, 267 - .suggestion-item.selected { 268 - background: var(--bg); 269 - } 270 - 271 - .suggestion-avatar { 272 - width: 32px; 273 - height: 32px; 274 - border-radius: 50%; 275 - object-fit: cover; 276 - flex-shrink: 0; 277 - } 278 - 279 - .suggestion-avatar-placeholder { 280 - width: 32px; 281 - height: 32px; 282 - border-radius: 50%; 283 - background: var(--border); 284 - display: flex; 285 - align-items: center; 286 - justify-content: center; 287 - font-size: 12px; 288 - color: var(--text-secondary); 289 - flex-shrink: 0; 290 - } 291 - 292 - .suggestion-info { 293 - display: flex; 294 - flex-direction: column; 295 - min-width: 0; 296 - } 297 - 298 - .suggestion-name { 299 - font-size: 14px; 300 - font-weight: 500; 301 - white-space: nowrap; 302 - overflow: hidden; 303 - text-overflow: ellipsis; 304 - } 305 - 306 - .suggestion-handle { 307 - font-size: 12px; 308 - color: var(--text-secondary); 309 - white-space: nowrap; 310 - overflow: hidden; 311 - text-overflow: ellipsis; 312 - } 313 - 314 - /* FAQ accordions */ 315 - .login-faq { 316 - margin-top: 1.5rem; 317 - border-top: 1px solid var(--border); 318 - padding-top: 1rem; 319 - } 320 - 321 - .faq-toggle { 322 - width: 100%; 323 - display: flex; 324 - justify-content: space-between; 325 - align-items: center; 326 - padding: 0.75rem 0; 327 - background: none; 328 - border: none; 329 - color: var(--text-secondary); 330 - font-family: inherit; 331 - font-size: 0.9rem; 332 - cursor: pointer; 333 - text-align: left; 334 - } 335 - 336 - .faq-toggle:hover { 337 - color: var(--text); 338 - } 339 - 340 - .faq-toggle .chevron { 341 - transition: transform 0.2s; 342 - flex-shrink: 0; 343 - } 344 - 345 - .faq-toggle .chevron.open { 346 - transform: rotate(180deg); 347 - } 348 - 349 - .faq-content { 350 - padding: 0 0 1rem 0; 351 - color: var(--text-secondary); 352 - font-size: 0.875rem; 353 - line-height: 1.6; 354 - } 355 - 356 - .faq-content p { 357 - margin: 0 0 0.75rem 0; 358 - text-align: left; 359 - } 360 - 361 - .faq-content p:last-child { 362 - margin-bottom: 0; 363 - } 364 - 365 - .faq-content a { 366 - color: var(--accent); 367 - text-decoration: none; 368 - } 369 - 370 - .faq-content a:hover { 371 - text-decoration: underline; 372 - } 373 - 374 - .faq-content code { 375 - background: var(--bg); 376 - padding: 0.15rem 0.4rem; 377 - border-radius: 4px; 378 - font-size: 0.85em; 379 - } 380 - 381 - /* Profile card */ 382 - .profile-card { 383 - background: var(--bg-card); 384 - border: 1px solid var(--border); 385 - border-radius: var(--radius); 386 - padding: 2rem; 387 - margin-bottom: 1.5rem; 388 - } 389 - 390 - .current-status { 391 - display: flex; 392 - flex-direction: column; 393 - align-items: center; 394 - gap: 1rem; 395 - text-align: center; 396 - } 397 - 398 - .big-emoji { 399 - font-size: 4rem; 400 - line-height: 1; 401 - } 402 - 403 - .big-emoji img { 404 - width: 4rem; 405 - height: 4rem; 406 - object-fit: contain; 407 - } 408 - 409 - .status-info { 410 - display: flex; 411 - flex-direction: column; 412 - gap: 0.25rem; 413 - } 414 - 415 - #current-text { 416 - font-size: 1.25rem; 417 - } 418 - 419 - .meta { 420 - color: var(--text-secondary); 421 - font-size: 0.875rem; 422 - } 423 - 424 - /* Status form */ 425 - .status-form { 426 - background: var(--bg-card); 427 - border: 1px solid var(--border); 428 - border-radius: var(--radius); 429 - padding: 1rem; 430 - margin-bottom: 1.5rem; 431 - } 432 - 433 - .emoji-input-row { 434 - display: flex; 435 - gap: 0.5rem; 436 - margin-bottom: 0.75rem; 437 - } 438 - 439 - .emoji-input-row input { 440 - flex: 1; 441 - padding: 0.75rem; 442 - border: 1px solid var(--border); 443 - border-radius: 8px; 444 - background: var(--bg); 445 - color: var(--text); 446 - font-family: inherit; 447 - font-size: 1rem; 448 - } 449 - 450 - #emoji-input { 451 - max-width: 150px; 452 - } 453 - 454 - .form-actions { 455 - display: flex; 456 - gap: 0.5rem; 457 - justify-content: flex-end; 458 - } 459 - 460 - .form-actions select { 461 - padding: 0.75rem; 462 - border: 1px solid var(--border); 463 - border-radius: 8px; 464 - background: var(--bg); 465 - color: var(--text); 466 - font-family: inherit; 467 - } 468 - 469 - .custom-datetime { 470 - padding: 0.75rem; 471 - border: 1px solid var(--border); 472 - border-radius: 8px; 473 - background: var(--bg); 474 - color: var(--text); 475 - font-family: inherit; 476 - } 477 - 478 - /* History */ 479 - .history { 480 - margin-bottom: 2rem; 481 - } 482 - 483 - .history h2 { 484 - font-size: 0.875rem; 485 - text-transform: uppercase; 486 - letter-spacing: 0.05em; 487 - color: var(--text-secondary); 488 - margin-bottom: 1rem; 489 - } 490 - 491 - #history-list { 492 - display: flex; 493 - flex-direction: column; 494 - gap: 0.75rem; 495 - } 496 - 497 - /* Feed list */ 498 - .feed-list { 499 - display: flex; 500 - flex-direction: column; 501 - gap: 1rem; 502 - } 503 - 504 - /* Status item (used in both history and feed) */ 505 - .status-item { 506 - display: flex; 507 - gap: 1rem; 508 - padding: 1rem; 509 - background: var(--bg-card); 510 - border: 1px solid var(--border); 511 - border-radius: var(--radius); 512 - align-items: flex-start; 513 - } 514 - 515 - .status-item:hover { 516 - border-color: var(--accent); 517 - } 518 - 519 - .status-item .emoji { 520 - font-size: 1.5rem; 521 - line-height: 1; 522 - flex-shrink: 0; 523 - } 524 - 525 - .status-item .emoji img { 526 - width: 1.5rem; 527 - height: 1.5rem; 528 - object-fit: contain; 529 - } 530 - 531 - .status-item .content { 532 - flex: 1; 533 - min-width: 0; 534 - } 535 - 536 - .status-item .author { 537 - color: var(--text-secondary); 538 - font-weight: 600; 539 - text-decoration: none; 540 - } 541 - 542 - .status-item .author:hover { 543 - color: var(--accent); 544 - } 545 - 546 - .status-item .text { 547 - margin-left: 0.5rem; 548 - } 549 - 550 - .status-item .time { 551 - display: block; 552 - font-size: 0.875rem; 553 - color: var(--text-secondary); 554 - margin-top: 0.25rem; 555 - } 556 - 557 - .status-actions { 558 - display: flex; 559 - gap: 0.25rem; 560 - flex-shrink: 0; 561 - } 562 - 563 - .share-btn, 564 - .delete-btn { 565 - background: transparent; 566 - border: none; 567 - color: var(--text-secondary); 568 - cursor: pointer; 569 - padding: 0.25rem; 570 - border-radius: 4px; 571 - opacity: 0; 572 - transition: opacity 0.15s, color 0.15s; 573 - flex-shrink: 0; 574 - } 575 - 576 - .status-item:hover .share-btn, 577 - .status-item:hover .delete-btn { 578 - opacity: 1; 579 - } 580 - 581 - .share-btn:hover { 582 - color: var(--accent); 583 - } 584 - 585 - .share-btn.copied { 586 - color: var(--accent); 587 - opacity: 1; 588 - } 589 - 590 - .share-btn.copied::after { 591 - content: 'copied!'; 592 - position: absolute; 593 - font-size: 0.75rem; 594 - background: var(--bg-card); 595 - border: 1px solid var(--border); 596 - padding: 0.25rem 0.5rem; 597 - border-radius: 4px; 598 - transform: translateY(-100%) translateX(-50%); 599 - left: 50%; 600 - top: -4px; 601 - white-space: nowrap; 602 - } 603 - 604 - .share-btn { 605 - position: relative; 606 - } 607 - 608 - /* Current status action buttons */ 609 - .current-status-actions { 610 - display: flex; 611 - gap: 0.5rem; 612 - margin-top: 0.75rem; 613 - } 614 - 615 - .current-share-btn, 616 - .embed-toggle-btn, 617 - .current-status-actions .delete-btn { 618 - opacity: 0.6; 619 - background: transparent; 620 - border: 1px solid var(--border); 621 - color: var(--text-secondary); 622 - cursor: pointer; 623 - padding: 0.5rem; 624 - border-radius: 6px; 625 - transition: opacity 0.15s, color 0.15s, border-color 0.15s; 626 - } 627 - 628 - .current-share-btn:hover, 629 - .embed-toggle-btn:hover, 630 - .embed-toggle-btn.active { 631 - opacity: 1; 632 - color: var(--accent); 633 - border-color: var(--accent); 634 - } 635 - 636 - .current-status-actions .delete-btn:hover { 637 - opacity: 1; 638 - color: #e74c3c; 639 - border-color: #e74c3c; 640 - } 641 - 642 - .delete-btn:hover { 643 - color: #e74c3c; 644 - } 645 - 646 - /* View profile link */ 647 - .view-profile-link { 648 - color: var(--accent); 649 - text-decoration: none; 650 - font-size: 0.875rem; 651 - } 652 - 653 - .view-profile-link:hover { 654 - text-decoration: underline; 655 - } 656 - 657 - /* Embed section */ 658 - .embed-section { 659 - margin-bottom: 1.5rem; 660 - } 661 - 662 - .embed-code-container { 663 - position: relative; 664 - background: var(--bg-card); 665 - border: 1px solid var(--border); 666 - border-radius: var(--radius); 667 - } 668 - 669 - .embed-code { 670 - padding: 1rem; 671 - padding-right: 3rem; 672 - overflow-x: auto; 673 - font-size: 0.75rem; 674 - line-height: 1.5; 675 - margin: 0; 676 - } 677 - 678 - .embed-code code { 679 - white-space: pre-wrap; 680 - word-break: break-all; 681 - } 682 - 683 - .copy-embed-btn { 684 - position: absolute; 685 - top: 0.5rem; 686 - right: 0.5rem; 687 - background: var(--bg); 688 - border: 1px solid var(--border); 689 - color: var(--text-secondary); 690 - cursor: pointer; 691 - padding: 0.5rem; 692 - border-radius: 6px; 693 - transition: color 0.15s, border-color 0.15s; 694 - } 695 - 696 - .copy-embed-btn:hover { 697 - color: var(--accent); 698 - border-color: var(--accent); 699 - } 700 - 701 - .copy-embed-btn.copied { 702 - color: var(--accent); 703 - border-color: var(--accent); 704 - } 705 - 706 - /* Logout */ 707 - .logout-btn { 708 - display: block; 709 - margin: 0 auto; 710 - padding: 0.5rem 1rem; 711 - background: none; 712 - border: 1px solid var(--border); 713 - border-radius: 8px; 714 - color: var(--text-secondary); 715 - cursor: pointer; 716 - font-family: inherit; 717 - } 718 - 719 - .logout-btn:hover { 720 - border-color: var(--text); 721 - color: var(--text); 722 - } 723 - 724 - /* Load more */ 725 - #load-more-btn { 726 - padding: 0.75rem 1.5rem; 727 - background: var(--bg-card); 728 - border: 1px solid var(--border); 729 - border-radius: var(--radius); 730 - color: var(--text); 731 - cursor: pointer; 732 - font-family: inherit; 733 - } 734 - 735 - #load-more-btn:hover { 736 - border-color: var(--accent); 737 - } 738 - 739 - /* Emoji trigger button */ 740 - .emoji-trigger { 741 - width: 3rem; 742 - height: 3rem; 743 - border: none; 744 - border-radius: 8px; 745 - background: transparent; 746 - cursor: pointer; 747 - display: flex; 748 - align-items: center; 749 - justify-content: center; 750 - font-size: 1.75rem; 751 - flex-shrink: 0; 752 - } 753 - 754 - .emoji-trigger:hover { 755 - background: var(--bg-card); 756 - } 757 - 758 - .emoji-trigger img { 759 - width: 2.5rem; 760 - height: 2.5rem; 761 - object-fit: contain; 762 - } 763 - 764 - /* Emoji picker overlay */ 765 - .emoji-picker-overlay { 766 - position: fixed; 767 - inset: 0; 768 - background: rgba(0, 0, 0, 0.7); 769 - display: flex; 770 - align-items: center; 771 - justify-content: center; 772 - z-index: 1000; 773 - padding: 1rem; 774 - } 775 - 776 - .emoji-picker { 777 - background: var(--bg-card); 778 - border: 1px solid var(--border); 779 - border-radius: var(--radius); 780 - width: 100%; 781 - max-width: 600px; 782 - height: 90vh; 783 - max-height: 700px; 784 - display: flex; 785 - flex-direction: column; 786 - overflow: hidden; 787 - } 788 - 789 - .emoji-picker-header { 790 - display: flex; 791 - justify-content: space-between; 792 - align-items: center; 793 - padding: 1rem; 794 - border-bottom: 1px solid var(--border); 795 - } 796 - 797 - .emoji-picker-header h3 { 798 - font-size: 1rem; 799 - font-weight: 600; 800 - } 801 - 802 - .emoji-picker-close { 803 - background: none; 804 - border: none; 805 - color: var(--text-secondary); 806 - cursor: pointer; 807 - font-size: 1.25rem; 808 - padding: 0.25rem; 809 - } 810 - 811 - .emoji-picker-close:hover { 812 - color: var(--text); 813 - } 814 - 815 - .emoji-search { 816 - margin: 0.75rem; 817 - padding: 0.5rem 0.75rem; 818 - border: 1px solid var(--border); 819 - border-radius: 8px; 820 - background: var(--bg); 821 - color: var(--text); 822 - font-family: inherit; 823 - font-size: 0.875rem; 824 - } 825 - 826 - .emoji-categories { 827 - display: flex; 828 - gap: 0.25rem; 829 - padding: 0 0.75rem; 830 - overflow-x: auto; 831 - flex-shrink: 0; 832 - } 833 - 834 - .category-btn { 835 - padding: 0.5rem; 836 - border: none; 837 - background: none; 838 - cursor: pointer; 839 - font-size: 1.25rem; 840 - border-radius: 8px; 841 - opacity: 0.5; 842 - transition: opacity 0.15s; 843 - } 844 - 845 - .category-btn:hover, .category-btn.active { 846 - opacity: 1; 847 - background: var(--bg); 848 - } 849 - 850 - .emoji-grid { 851 - padding: 0.75rem; 852 - display: grid; 853 - grid-template-columns: repeat(auto-fill, minmax(48px, 1fr)); 854 - gap: 0.25rem; 855 - overflow-y: auto; 856 - flex: 1; 857 - min-height: 200px; 858 - align-content: start; 859 - } 860 - 861 - .emoji-grid.bufo-grid { 862 - grid-template-columns: repeat(auto-fill, minmax(64px, 1fr)); 863 - gap: 0.5rem; 864 - } 865 - 866 - .emoji-btn { 867 - padding: 0.5rem; 868 - border: none; 869 - background: none; 870 - cursor: pointer; 871 - font-size: 1.5rem; 872 - border-radius: 8px; 873 - transition: background 0.15s; 874 - } 875 - 876 - .emoji-btn:hover { 877 - background: var(--bg); 878 - } 879 - 880 - /* Consistent sizing for mixed emoji/bufo grids (frequent tab) */ 881 - .emoji-grid .emoji-btn { 882 - width: 48px; 883 - height: 48px; 884 - display: flex; 885 - align-items: center; 886 - justify-content: center; 887 - font-size: 1.75rem; 888 - } 889 - 890 - .bufo-btn { 891 - padding: 0.25rem; 892 - position: relative; 893 - } 894 - 895 - .bufo-grid .bufo-btn { 896 - width: 64px; 897 - height: 64px; 898 - } 899 - 900 - .bufo-btn img { 901 - width: 100%; 902 - height: 100%; 903 - max-width: 48px; 904 - max-height: 48px; 905 - object-fit: contain; 906 - } 907 - 908 - .bufo-score { 909 - position: absolute; 910 - bottom: 1px; 911 - right: 1px; 912 - font-size: 0.6rem; 913 - background: rgba(0, 0, 0, 0.6); 914 - color: #fff; 915 - padding: 1px 3px; 916 - border-radius: 3px; 917 - line-height: 1; 918 - pointer-events: none; 919 - } 920 - 921 - .loading { 922 - grid-column: 1 / -1; 923 - text-align: center; 924 - color: var(--text-secondary); 925 - padding: 2rem; 926 - } 927 - 928 - .no-results { 929 - grid-column: 1 / -1; 930 - text-align: center; 931 - color: var(--text-secondary); 932 - padding: 2rem; 933 - } 934 - 935 - /* Custom emoji input */ 936 - .custom-emoji-input { 937 - grid-column: 1 / -1; 938 - display: flex; 939 - gap: 0.5rem; 940 - margin-bottom: 1rem; 941 - } 942 - 943 - .custom-emoji-input input { 944 - flex: 1; 945 - padding: 0.5rem 0.75rem; 946 - border: 1px solid var(--border); 947 - border-radius: 8px; 948 - background: var(--bg); 949 - color: var(--text); 950 - font-family: inherit; 951 - } 952 - 953 - .custom-emoji-input button { 954 - padding: 0.5rem 1rem; 955 - background: var(--accent); 956 - color: white; 957 - border: none; 958 - border-radius: 8px; 959 - cursor: pointer; 960 - font-family: inherit; 961 - } 962 - 963 - .custom-emoji-preview { 964 - grid-column: 1 / -1; 965 - display: flex; 966 - justify-content: center; 967 - min-height: 80px; 968 - align-items: center; 969 - } 970 - 971 - .bufo-helper { 972 - padding: 0.5rem 0.75rem; 973 - text-align: center; 974 - border-top: 1px solid var(--border); 975 - } 976 - 977 - .bufo-helper a { 978 - color: var(--text-secondary); 979 - font-size: 0.75rem; 980 - text-decoration: none; 981 - opacity: 0.7; 982 - transition: opacity 0.15s; 983 - } 984 - 985 - .bufo-helper a:hover { 986 - opacity: 1; 987 - color: var(--accent); 988 - } 989 - 990 - /* Settings Modal */ 991 - .settings-overlay { 992 - position: fixed; 993 - top: 0; 994 - left: 0; 995 - right: 0; 996 - bottom: 0; 997 - background: rgba(0, 0, 0, 0.7); 998 - display: flex; 999 - align-items: center; 1000 - justify-content: center; 1001 - z-index: 1000; 1002 - padding: 1rem; 1003 - } 1004 - 1005 - .settings-modal { 1006 - background: var(--bg-card); 1007 - border: 1px solid var(--border); 1008 - border-radius: var(--radius); 1009 - width: 100%; 1010 - max-width: 400px; 1011 - display: flex; 1012 - flex-direction: column; 1013 - } 1014 - 1015 - .settings-header { 1016 - display: flex; 1017 - justify-content: space-between; 1018 - align-items: center; 1019 - padding: 1rem; 1020 - border-bottom: 1px solid var(--border); 1021 - } 1022 - 1023 - .settings-header h3 { 1024 - font-size: 1.1rem; 1025 - font-weight: 500; 1026 - } 1027 - 1028 - .settings-close { 1029 - background: none; 1030 - border: none; 1031 - color: var(--text-secondary); 1032 - cursor: pointer; 1033 - font-size: 1.25rem; 1034 - padding: 0.25rem; 1035 - } 1036 - 1037 - .settings-close:hover { 1038 - color: var(--text); 1039 - } 1040 - 1041 - .settings-content { 1042 - padding: 1rem; 1043 - display: flex; 1044 - flex-direction: column; 1045 - gap: 1.25rem; 1046 - } 1047 - 1048 - .setting-group { 1049 - display: flex; 1050 - flex-direction: column; 1051 - gap: 0.5rem; 1052 - } 1053 - 1054 - .setting-group label { 1055 - font-size: 0.875rem; 1056 - color: var(--text-secondary); 1057 - } 1058 - 1059 - .setting-group select { 1060 - padding: 0.75rem; 1061 - border: 1px solid var(--border); 1062 - border-radius: 8px; 1063 - background: var(--bg); 1064 - color: var(--text); 1065 - font-family: inherit; 1066 - font-size: 1rem; 1067 - } 1068 - 1069 - .color-picker { 1070 - display: flex; 1071 - flex-wrap: wrap; 1072 - gap: 0.5rem; 1073 - align-items: center; 1074 - } 1075 - 1076 - .color-btn { 1077 - width: 32px; 1078 - height: 32px; 1079 - border-radius: 50%; 1080 - border: 2px solid transparent; 1081 - cursor: pointer; 1082 - transition: border-color 0.15s, transform 0.15s; 1083 - } 1084 - 1085 - .color-btn:hover { 1086 - transform: scale(1.1); 1087 - } 1088 - 1089 - .color-btn.active { 1090 - border-color: var(--text); 1091 - } 1092 - 1093 - .custom-color-input { 1094 - width: 32px; 1095 - height: 32px; 1096 - border: none; 1097 - border-radius: 50%; 1098 - cursor: pointer; 1099 - background: none; 1100 - padding: 0; 1101 - } 1102 - 1103 - .custom-color-input::-webkit-color-swatch-wrapper { 1104 - padding: 0; 1105 - } 1106 - 1107 - .custom-color-input::-webkit-color-swatch { 1108 - border: 2px solid var(--border); 1109 - border-radius: 50%; 1110 - } 1111 - 1112 - .settings-footer { 1113 - padding: 1rem; 1114 - border-top: 1px solid var(--border); 1115 - display: flex; 1116 - justify-content: flex-end; 1117 - } 1118 - 1119 - .settings-footer .save-btn { 1120 - padding: 0.75rem 1.5rem; 1121 - background: var(--accent); 1122 - color: white; 1123 - border: none; 1124 - border-radius: 8px; 1125 - cursor: pointer; 1126 - font-family: inherit; 1127 - font-size: 1rem; 1128 - } 1129 - 1130 - .settings-footer .save-btn:hover { 1131 - opacity: 0.9; 1132 - } 1133 - 1134 - .settings-footer .save-btn:disabled { 1135 - opacity: 0.5; 1136 - cursor: not-allowed; 1137 - } 1138 - 1139 - /* Mobile */ 1140 - @media (max-width: 480px) { 1141 - .emoji-input-row { 1142 - flex-direction: row; 1143 - } 1144 - 1145 - .form-actions { 1146 - flex-direction: column; 1147 - } 1148 - 1149 - .emoji-grid { 1150 - grid-template-columns: repeat(6, 1fr); 1151 - } 1152 - }
+14
svelte.config.js
··· 1 + import adapter from "@sveltejs/adapter-node"; 2 + 3 + export default { 4 + kit: { 5 + adapter: adapter(), 6 + files: { 7 + src: "app", 8 + }, 9 + alias: { 10 + $hatk: "./hatk.generated.ts", 11 + "$hatk/client": "./hatk.generated.client.ts", 12 + }, 13 + }, 14 + };
+16
tsconfig.json
··· 1 + { 2 + "extends": "./.svelte-kit/tsconfig.json", 3 + "compilerOptions": { 4 + "target": "ESNext", 5 + "module": "ESNext", 6 + "moduleResolution": "bundler", 7 + "allowImportingTsExtensions": true, 8 + "verbatimModuleSyntax": true, 9 + "noEmit": true, 10 + "strict": true, 11 + "skipLibCheck": true, 12 + "resolveJsonModule": true 13 + }, 14 + "include": ["app", "hatk.generated.ts", "hatk.config.ts"], 15 + "references": [{ "path": "./tsconfig.server.json" }] 16 + }
+19
tsconfig.server.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "ES2022", 4 + "module": "Node16", 5 + "moduleResolution": "Node16", 6 + "strict": true, 7 + "esModuleInterop": true, 8 + "skipLibCheck": true, 9 + "noEmit": true, 10 + "composite": true, 11 + "allowImportingTsExtensions": true, 12 + "resolveJsonModule": true, 13 + "paths": { 14 + "$hatk": ["./hatk.generated.ts"], 15 + "$hatk/client": ["./hatk.generated.client.ts"] 16 + } 17 + }, 18 + "include": ["server", "hatk.generated.ts", "hatk.config.ts"] 19 + }
+13
vite.config.ts
··· 1 + import { sveltekit } from "@sveltejs/kit/vite"; 2 + import { hatk } from "@hatk/hatk/vite-plugin"; 3 + import { defineConfig } from "vite-plus"; 4 + 5 + export default defineConfig({ 6 + plugins: [hatk(), sveltekit()], 7 + lint: { 8 + ignorePatterns: ["hatk.generated.ts", "hatk.generated.client.ts"], 9 + }, 10 + fmt: { 11 + ignorePatterns: ["hatk.generated.ts", "hatk.generated.client.ts"], 12 + }, 13 + });