my pkgs monorepo
fork

Configure Feed

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

Add 'packages/svelte-standard-site/' from commit '86f3057e84ec12fe01728947f3508d49d0a6d85e'

git-subtree-dir: packages/svelte-standard-site
git-subtree-mainline: d68efed13c3482d40190912f9e30f674c79c0575
git-subtree-split: 86f3057e84ec12fe01728947f3508d49d0a6d85e

+12364
+8
packages/svelte-standard-site/.env.example
··· 1 + # Required: Your AT Protocol DID 2 + PUBLIC_ATPROTO_DID=did:plc:revjuqmkvrw6fnkxppqtszpv 3 + 4 + # Optional: Custom PDS endpoint (will be auto-resolved if not provided) 5 + # PUBLIC_ATPROTO_PDS=https://cortinarius.us-west.host.bsky.network 6 + 7 + # Optional: Cache TTL in milliseconds (default: 300000 = 5 minutes) 8 + # PUBLIC_CACHE_TTL=300000
+24
packages/svelte-standard-site/.gitignore
··· 1 + node_modules 2 + 3 + # Output 4 + .output 5 + .vercel 6 + .netlify 7 + .wrangler 8 + /.svelte-kit 9 + /build 10 + /dist 11 + 12 + # OS 13 + .DS_Store 14 + Thumbs.db 15 + 16 + # Env 17 + .env 18 + .env.* 19 + !.env.example 20 + !.env.test 21 + 22 + # Vite 23 + vite.config.js.timestamp-* 24 + vite.config.ts.timestamp-*
+1
packages/svelte-standard-site/.npmrc
··· 1 + engine-strict=true
+9
packages/svelte-standard-site/.prettierignore
··· 1 + # Package Managers 2 + package-lock.json 3 + pnpm-lock.yaml 4 + yarn.lock 5 + bun.lock 6 + bun.lockb 7 + 8 + # Miscellaneous 9 + /static/
+16
packages/svelte-standard-site/.prettierrc
··· 1 + { 2 + "useTabs": true, 3 + "singleQuote": true, 4 + "trailingComma": "none", 5 + "printWidth": 100, 6 + "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], 7 + "overrides": [ 8 + { 9 + "files": "*.svelte", 10 + "options": { 11 + "parser": "svelte" 12 + } 13 + } 14 + ], 15 + "tailwindStylesheet": "./src/routes/layout.css" 16 + }
+5
packages/svelte-standard-site/.vscode/settings.json
··· 1 + { 2 + "files.associations": { 3 + "*.css": "tailwindcss" 4 + } 5 + }
+740
packages/svelte-standard-site/API.md
··· 1 + # API Reference 2 + 3 + Complete API documentation for svelte-standard-site. 4 + 5 + ## Table of Contents 6 + 7 + - [Components](#components) 8 + - [Stores](#stores) 9 + - [Client](#client) 10 + - [Types](#types) 11 + - [Utilities](#utilities) 12 + 13 + ## Components 14 + 15 + ### StandardSiteLayout 16 + 17 + A complete page layout with header, footer, and built-in theme management. 18 + 19 + ```typescript 20 + interface StandardSiteLayoutProps { 21 + /** Site title displayed in header */ 22 + title?: string; 23 + /** Custom header content (replaces default header) */ 24 + header?: Snippet; 25 + /** Custom footer content (replaces default footer) */ 26 + footer?: Snippet; 27 + /** Main content */ 28 + children: Snippet; 29 + /** Additional CSS classes for main container */ 30 + class?: string; 31 + /** Show theme toggle button in default header */ 32 + showThemeToggle?: boolean; 33 + } 34 + ``` 35 + 36 + **Default Values:** 37 + 38 + - `title`: `"My Site"` 39 + - `showThemeToggle`: `true` 40 + 41 + **Usage:** 42 + 43 + ```svelte 44 + <StandardSiteLayout title="My Site"> 45 + <h1>Content here</h1> 46 + </StandardSiteLayout> 47 + ``` 48 + 49 + **With Custom Header:** 50 + 51 + ```svelte 52 + <StandardSiteLayout title="My Site"> 53 + {#snippet header()} 54 + <nav>Custom header</nav> 55 + {/snippet} 56 + 57 + <h1>Content</h1> 58 + </StandardSiteLayout> 59 + ``` 60 + 61 + --- 62 + 63 + ### ThemeToggle 64 + 65 + A button component for toggling between light and dark modes with smooth animations. 66 + 67 + ```typescript 68 + interface ThemeToggleProps { 69 + /** Additional CSS classes */ 70 + class?: string; 71 + } 72 + ``` 73 + 74 + **Usage:** 75 + 76 + ```svelte 77 + <ThemeToggle class="ml-auto" /> 78 + ``` 79 + 80 + **Features:** 81 + 82 + - Smooth icon transitions 83 + - System preference detection 84 + - Persistent theme storage 85 + - Loading state indicator 86 + - Accessible ARIA labels 87 + 88 + --- 89 + 90 + ### DocumentCard 91 + 92 + Displays a `site.standard.document` record as a styled card with cover image, metadata, and tags. 93 + 94 + ```typescript 95 + interface DocumentCardProps { 96 + /** The document record to display */ 97 + document: AtProtoRecord<Document>; 98 + /** Additional CSS classes */ 99 + class?: string; 100 + /** Whether to show the cover image */ 101 + showCover?: boolean; 102 + /** Custom href override (defaults to /[pub_rkey]/[doc_rkey]) */ 103 + href?: string; 104 + } 105 + ``` 106 + 107 + **Default Values:** 108 + 109 + - `showCover`: `true` 110 + - `href`: Auto-generated from document URI 111 + 112 + **Usage:** 113 + 114 + ```svelte 115 + <DocumentCard {document} showCover={true} class="shadow-lg" /> 116 + ``` 117 + 118 + **Features:** 119 + 120 + - Responsive design 121 + - Cover image display 122 + - Date formatting 123 + - Tag pills 124 + - Hover states 125 + - Dark mode support 126 + 127 + --- 128 + 129 + ### PublicationCard 130 + 131 + Displays a `site.standard.publication` record with icon, description, and external link. 132 + 133 + ```typescript 134 + interface PublicationCardProps { 135 + /** The publication record to display */ 136 + publication: AtProtoRecord<Publication>; 137 + /** Additional CSS classes */ 138 + class?: string; 139 + /** Whether to show external link icon */ 140 + showExternalIcon?: boolean; 141 + } 142 + ``` 143 + 144 + **Default Values:** 145 + 146 + - `showExternalIcon`: `true` 147 + 148 + **Usage:** 149 + 150 + ```svelte 151 + <PublicationCard {publication} showExternalIcon={true} /> 152 + ``` 153 + 154 + **Features:** 155 + 156 + - Icon display 157 + - Theme color support 158 + - External link indicator 159 + - Hover states 160 + - Dark mode support 161 + 162 + --- 163 + 164 + ## Stores 165 + 166 + ### themeStore 167 + 168 + A Svelte store for managing light/dark theme state. 169 + 170 + ```typescript 171 + interface ThemeState { 172 + isDark: boolean; 173 + mounted: boolean; 174 + } 175 + 176 + interface ThemeStore { 177 + subscribe: (callback: (state: ThemeState) => void) => () => void; 178 + init: () => void | (() => void); 179 + toggle: () => void; 180 + setTheme: (isDark: boolean) => void; 181 + } 182 + ``` 183 + 184 + **Methods:** 185 + 186 + #### `init()` 187 + 188 + Initialize the theme store. Automatically detects saved preference and system preference. 189 + 190 + ```typescript 191 + import { themeStore } from 'svelte-standard-site'; 192 + import { onMount } from 'svelte'; 193 + 194 + onMount(() => { 195 + themeStore.init(); 196 + }); 197 + ``` 198 + 199 + **Returns:** Optional cleanup function 200 + 201 + #### `toggle()` 202 + 203 + Toggle between light and dark modes. 204 + 205 + ```typescript 206 + themeStore.toggle(); 207 + ``` 208 + 209 + #### `setTheme(isDark: boolean)` 210 + 211 + Set a specific theme mode. 212 + 213 + ```typescript 214 + themeStore.setTheme(true); // Dark mode 215 + themeStore.setTheme(false); // Light mode 216 + ``` 217 + 218 + #### `subscribe(callback)` 219 + 220 + Subscribe to theme changes. 221 + 222 + ```typescript 223 + const unsubscribe = themeStore.subscribe((state) => { 224 + console.log('Dark mode:', state.isDark); 225 + console.log('Mounted:', state.mounted); 226 + }); 227 + 228 + // Don't forget to unsubscribe 229 + unsubscribe(); 230 + ``` 231 + 232 + **State Properties:** 233 + 234 + - `isDark: boolean` - Current theme state (true = dark, false = light) 235 + - `mounted: boolean` - Whether the store has been initialized 236 + 237 + --- 238 + 239 + ## Client 240 + 241 + ### SiteStandardClient 242 + 243 + Main client for fetching AT Protocol records. 244 + 245 + ```typescript 246 + class SiteStandardClient { 247 + constructor(config: SiteStandardConfig); 248 + 249 + // Fetch methods 250 + fetchPublication( 251 + rkey: string, 252 + fetchFn?: typeof fetch 253 + ): Promise<AtProtoRecord<Publication> | null>; 254 + fetchAllPublications(fetchFn?: typeof fetch): Promise<AtProtoRecord<Publication>[]>; 255 + fetchDocument(rkey: string, fetchFn?: typeof fetch): Promise<AtProtoRecord<Document> | null>; 256 + fetchAllDocuments(fetchFn?: typeof fetch): Promise<AtProtoRecord<Document>[]>; 257 + fetchDocumentsByPublication( 258 + publicationUri: string, 259 + fetchFn?: typeof fetch 260 + ): Promise<AtProtoRecord<Document>[]>; 261 + fetchByAtUri<T>(atUri: string, fetchFn?: typeof fetch): Promise<AtProtoRecord<T> | null>; 262 + 263 + // Utility methods 264 + clearCache(): void; 265 + getPDS(fetchFn?: typeof fetch): Promise<string>; 266 + } 267 + ``` 268 + 269 + ### createClient() 270 + 271 + Factory function for creating a client instance. 272 + 273 + ```typescript 274 + function createClient(config: SiteStandardConfig): SiteStandardClient; 275 + ``` 276 + 277 + **Usage:** 278 + 279 + ```typescript 280 + import { createClient } from 'svelte-standard-site'; 281 + 282 + const client = createClient({ 283 + did: 'did:plc:your-did-here', 284 + pds: 'https://your-pds.example.com', // optional 285 + cacheTTL: 300000 // optional, 5 minutes 286 + }); 287 + ``` 288 + 289 + --- 290 + 291 + ### Methods 292 + 293 + #### `fetchPublication(rkey, fetchFn?)` 294 + 295 + Fetch a single publication by record key. 296 + 297 + **Parameters:** 298 + 299 + - `rkey: string` - The record key 300 + - `fetchFn?: typeof fetch` - Optional custom fetch function (for SSR) 301 + 302 + **Returns:** `Promise<AtProtoRecord<Publication> | null>` 303 + 304 + **Example:** 305 + 306 + ```typescript 307 + const pub = await client.fetchPublication('3lwafzkjqm25s'); 308 + if (pub) { 309 + console.log(pub.value.name); 310 + } 311 + ``` 312 + 313 + --- 314 + 315 + #### `fetchAllPublications(fetchFn?)` 316 + 317 + Fetch all publications for the configured DID. 318 + 319 + **Parameters:** 320 + 321 + - `fetchFn?: typeof fetch` - Optional custom fetch function 322 + 323 + **Returns:** `Promise<AtProtoRecord<Publication>[]>` 324 + 325 + **Example:** 326 + 327 + ```typescript 328 + const pubs = await client.fetchAllPublications(); 329 + console.log(`Found ${pubs.length} publications`); 330 + ``` 331 + 332 + --- 333 + 334 + #### `fetchDocument(rkey, fetchFn?)` 335 + 336 + Fetch a single document by record key. 337 + 338 + **Parameters:** 339 + 340 + - `rkey: string` - The record key 341 + - `fetchFn?: typeof fetch` - Optional custom fetch function 342 + 343 + **Returns:** `Promise<AtProtoRecord<Document> | null>` 344 + 345 + --- 346 + 347 + #### `fetchAllDocuments(fetchFn?)` 348 + 349 + Fetch all documents, sorted by `publishedAt` (newest first). 350 + 351 + **Parameters:** 352 + 353 + - `fetchFn?: typeof fetch` - Optional custom fetch function 354 + 355 + **Returns:** `Promise<AtProtoRecord<Document>[]>` 356 + 357 + **Example:** 358 + 359 + ```typescript 360 + const docs = await client.fetchAllDocuments(); 361 + const latest = docs[0]; // Most recent document 362 + ``` 363 + 364 + --- 365 + 366 + #### `fetchDocumentsByPublication(publicationUri, fetchFn?)` 367 + 368 + Fetch all documents belonging to a specific publication. 369 + 370 + **Parameters:** 371 + 372 + - `publicationUri: string` - AT URI of the publication 373 + - `fetchFn?: typeof fetch` - Optional custom fetch function 374 + 375 + **Returns:** `Promise<AtProtoRecord<Document>[]>` 376 + 377 + **Example:** 378 + 379 + ```typescript 380 + const docs = await client.fetchDocumentsByPublication( 381 + 'at://did:plc:xxx/site.standard.publication/rkey' 382 + ); 383 + ``` 384 + 385 + --- 386 + 387 + #### `fetchByAtUri<T>(atUri, fetchFn?)` 388 + 389 + Fetch any record by its AT URI. 390 + 391 + **Parameters:** 392 + 393 + - `atUri: string` - The AT URI 394 + - `fetchFn?: typeof fetch` - Optional custom fetch function 395 + 396 + **Returns:** `Promise<AtProtoRecord<T> | null>` 397 + 398 + **Example:** 399 + 400 + ```typescript 401 + const record = await client.fetchByAtUri<Document>('at://did:plc:xxx/site.standard.document/rkey'); 402 + ``` 403 + 404 + --- 405 + 406 + #### `clearCache()` 407 + 408 + Clear all cached data. 409 + 410 + **Example:** 411 + 412 + ```typescript 413 + client.clearCache(); 414 + ``` 415 + 416 + --- 417 + 418 + #### `getPDS(fetchFn?)` 419 + 420 + Get the resolved PDS endpoint for the configured DID. 421 + 422 + **Parameters:** 423 + 424 + - `fetchFn?: typeof fetch` - Optional custom fetch function 425 + 426 + **Returns:** `Promise<string>` 427 + 428 + **Example:** 429 + 430 + ```typescript 431 + const pds = await client.getPDS(); 432 + console.log(`Using PDS: ${pds}`); 433 + ``` 434 + 435 + --- 436 + 437 + ## Types 438 + 439 + ### Core Types 440 + 441 + ```typescript 442 + interface AtProtoRecord<T> { 443 + uri: string; 444 + cid: string; 445 + value: T; 446 + } 447 + 448 + interface AtProtoBlob { 449 + $type: 'blob'; 450 + ref: { $link: string }; 451 + mimeType: string; 452 + size: number; 453 + } 454 + 455 + interface StrongRef { 456 + uri: string; 457 + cid: string; 458 + } 459 + ``` 460 + 461 + ### Publication Types 462 + 463 + ```typescript 464 + interface Publication { 465 + $type: 'site.standard.publication'; 466 + url: string; 467 + name: string; 468 + icon?: AtProtoBlob; 469 + description?: string; 470 + basicTheme?: BasicTheme; 471 + preferences?: PublicationPreferences; 472 + } 473 + 474 + interface BasicTheme { 475 + primary: RGBColor; 476 + secondary: RGBColor; 477 + accent: RGBColor; 478 + } 479 + 480 + interface RGBColor { 481 + r: number; // 0-255 482 + g: number; // 0-255 483 + b: number; // 0-255 484 + } 485 + 486 + interface PublicationPreferences { 487 + defaultShowCoverImage?: boolean; 488 + allowComments?: boolean; 489 + allowReactions?: boolean; 490 + } 491 + ``` 492 + 493 + ### Document Types 494 + 495 + ```typescript 496 + interface Document { 497 + $type: 'site.standard.document'; 498 + site: string; // AT URI or HTTPS URL to publication 499 + title: string; 500 + path?: string; 501 + description?: string; 502 + coverImage?: AtProtoBlob; 503 + content?: any; // Rich content object 504 + textContent?: string; // Plain text fallback 505 + bskyPostRef?: StrongRef; // Reference to Bluesky post 506 + tags?: string[]; 507 + publishedAt: string; // ISO 8601 datetime 508 + updatedAt?: string; // ISO 8601 datetime 509 + } 510 + ``` 511 + 512 + ### Configuration Types 513 + 514 + ```typescript 515 + interface SiteStandardConfig { 516 + did: string; 517 + pds?: string; // Optional, auto-resolved if not provided 518 + cacheTTL?: number; // Cache time-to-live in milliseconds 519 + } 520 + 521 + interface ResolvedIdentity { 522 + did: string; 523 + pds: string; 524 + handle?: string; 525 + } 526 + ``` 527 + 528 + --- 529 + 530 + ## Utilities 531 + 532 + ### AT URI Utilities 533 + 534 + #### `parseAtUri(uri: string)` 535 + 536 + Parse an AT URI into its components. 537 + 538 + ```typescript 539 + const parsed = parseAtUri('at://did:plc:xxx/site.standard.publication/rkey'); 540 + // Returns: { did: 'did:plc:xxx', collection: 'site.standard.publication', rkey: 'rkey' } 541 + ``` 542 + 543 + #### `buildAtUri(did: string, collection: string, rkey: string)` 544 + 545 + Build an AT URI from components. 546 + 547 + ```typescript 548 + const uri = buildAtUri('did:plc:xxx', 'site.standard.publication', 'rkey'); 549 + // Returns: 'at://did:plc:xxx/site.standard.publication/rkey' 550 + ``` 551 + 552 + #### `extractRkey(uri: string)` 553 + 554 + Extract the record key from an AT URI. 555 + 556 + ```typescript 557 + const rkey = extractRkey('at://did:plc:xxx/site.standard.publication/rkey'); 558 + // Returns: 'rkey' 559 + ``` 560 + 561 + #### `isAtUri(value: string)` 562 + 563 + Check if a string is a valid AT URI. 564 + 565 + ```typescript 566 + const valid = isAtUri('at://did:plc:xxx/site.standard.publication/rkey'); 567 + // Returns: true 568 + ``` 569 + 570 + #### `atUriToHttps(atUri: string, pds: string)` 571 + 572 + Convert an AT URI to an HTTPS URL for API calls. 573 + 574 + ```typescript 575 + const url = atUriToHttps( 576 + 'at://did:plc:xxx/site.standard.publication/rkey', 577 + 'https://pds.example.com' 578 + ); 579 + // Returns: 'https://pds.example.com/xrpc/com.atproto.repo.getRecord?repo=...' 580 + ``` 581 + 582 + --- 583 + 584 + ### Identity Resolution 585 + 586 + #### `resolveIdentity(did: string, fetchFn?: typeof fetch)` 587 + 588 + Resolve a DID to its PDS endpoint and handle. 589 + 590 + ```typescript 591 + const identity = await resolveIdentity('did:plc:xxx'); 592 + // Returns: { did: 'did:plc:xxx', pds: 'https://...', handle: 'user.bsky.social' } 593 + ``` 594 + 595 + --- 596 + 597 + ### Blob Utilities 598 + 599 + #### `buildPdsBlobUrl(pds: string, did: string, cid: string)` 600 + 601 + Build a URL for fetching blob content. 602 + 603 + ```typescript 604 + const url = buildPdsBlobUrl('https://pds.example.com', 'did:plc:xxx', 'bafyrei...'); 605 + ``` 606 + 607 + --- 608 + 609 + ### Theme Utilities 610 + 611 + #### `rgbToCSS(color: RGBColor)` 612 + 613 + Convert an RGB color object to CSS rgb() string. 614 + 615 + ```typescript 616 + const css = rgbToCSS({ r: 100, g: 150, b: 200 }); 617 + // Returns: 'rgb(100, 150, 200)' 618 + ``` 619 + 620 + #### `rgbToHex(color: RGBColor)` 621 + 622 + Convert an RGB color object to hex string. 623 + 624 + ```typescript 625 + const hex = rgbToHex({ r: 100, g: 150, b: 200 }); 626 + // Returns: '#6496c8' 627 + ``` 628 + 629 + #### `getThemeVars(theme: BasicTheme)` 630 + 631 + Convert a BasicTheme to CSS custom properties object. 632 + 633 + ```typescript 634 + const vars = getThemeVars({ 635 + primary: { r: 100, g: 150, b: 200 }, 636 + secondary: { r: 150, g: 100, b: 200 }, 637 + accent: { r: 200, g: 100, b: 150 } 638 + }); 639 + // Returns: { '--theme-primary': 'rgb(100, 150, 200)', ... } 640 + ``` 641 + 642 + --- 643 + 644 + ### Document Utilities 645 + 646 + #### `getDocumentSlug(document: AtProtoRecord<Document>)` 647 + 648 + Generate a URL slug from a document's path or URI. 649 + 650 + ```typescript 651 + const slug = getDocumentSlug(document); 652 + ``` 653 + 654 + #### `getDocumentUrl(document: AtProtoRecord<Document>, publicationRkey?: string)` 655 + 656 + Generate a full URL path for a document. 657 + 658 + ```typescript 659 + const url = getDocumentUrl(document, 'pub123'); 660 + // Returns: '/pub123/doc456' or custom path 661 + ``` 662 + 663 + --- 664 + 665 + ### Cache Utilities 666 + 667 + The library includes built-in caching with automatic expiration. 668 + 669 + ```typescript 670 + import { cache } from 'svelte-standard-site'; 671 + 672 + // Get cached value 673 + const value = cache.get<MyType>('my-key'); 674 + 675 + // Set cached value 676 + cache.set('my-key', myValue, 300000); // 5 minutes TTL 677 + 678 + // Delete cached value 679 + cache.delete('my-key'); 680 + 681 + // Clear all cache 682 + cache.clear(); 683 + ``` 684 + 685 + --- 686 + 687 + ## Environment Configuration 688 + 689 + ### getConfigFromEnv() 690 + 691 + Read configuration from environment variables. 692 + 693 + ```typescript 694 + import { getConfigFromEnv } from 'svelte-standard-site/config/env'; 695 + 696 + const config = getConfigFromEnv(); 697 + // Returns: { did: '...', pds: '...', cacheTTL: ... } or null 698 + ``` 699 + 700 + **Environment Variables:** 701 + 702 + - `PUBLIC_ATPROTO_DID` - Required DID 703 + - `PUBLIC_ATPROTO_PDS` - Optional PDS endpoint 704 + - `PUBLIC_CACHE_TTL` - Optional cache TTL in milliseconds 705 + 706 + ### validateEnv() 707 + 708 + Validate that required environment variables are set. Throws if missing. 709 + 710 + ```typescript 711 + import { validateEnv } from 'svelte-standard-site/config/env'; 712 + 713 + validateEnv(); // Throws if PUBLIC_ATPROTO_DID is not set 714 + ``` 715 + 716 + --- 717 + 718 + ## Error Handling 719 + 720 + All async methods can throw errors. Always use try-catch: 721 + 722 + ```typescript 723 + try { 724 + const docs = await client.fetchAllDocuments(); 725 + // Handle success 726 + } catch (error) { 727 + console.error('Failed to fetch documents:', error); 728 + // Handle error 729 + } 730 + ``` 731 + 732 + ## Performance Tips 733 + 734 + 1. **Pass fetch function in SSR** for proper request tracking 735 + 2. **Use caching** - it's built-in and automatic 736 + 3. **Batch requests** with `Promise.all()` when possible 737 + 4. **Clear cache** strategically if data changes frequently 738 + 5. **Pre-render static pages** for better performance 739 + 740 + For more examples, see [EXAMPLES.md](./EXAMPLES.md).
+212
packages/svelte-standard-site/CLAUDE.md
··· 1 + # CLAUDE.md 2 + 3 + ## What 4 + 5 + SvelteKit library for ATProto longform publishing via the `standard.site` lexicon. Provides both read and write capabilities: display content from ATProto (Leaflet/WhiteWind), publish content TO ATProto, and aggregate federated comments. 6 + 7 + **Package:** `svelte-standard-site` 8 + 9 + ## Project Structure 10 + 11 + ``` 12 + src/lib/ 13 + client.ts # Read from ATProto (fetch documents/publications) 14 + publisher.ts # Write to ATProto (publish documents/publications) 15 + schemas.ts # Zod schemas for validation 16 + types.ts # TypeScript type definitions 17 + components/ 18 + Comments.svelte # Federated comments from Bluesky 19 + DocumentCard.svelte 20 + PublicationCard.svelte 21 + StandardSiteLayout.svelte 22 + ThemeToggle.svelte 23 + common/ # Reusable utility components 24 + document/ # Document rendering components 25 + utils/ 26 + content.ts # Markdown transformation (sidenotes, links, etc.) 27 + comments.ts # Fetch Bluesky replies 28 + verification.ts # Ownership verification helpers 29 + at-uri.ts # AT-URI parsing and conversion 30 + theme.ts # Theme utilities 31 + cache.ts # Caching layer 32 + stores/ 33 + theme.ts # Dark/light mode store 34 + styles/ 35 + base.css # Core design system 36 + themes.css # Theme definitions 37 + ``` 38 + 39 + ## Commands 40 + 41 + ```bash 42 + pnpm dev # Start dev server 43 + pnpm build # Build package 44 + pnpm test # Run tests 45 + pnpm check # Type check 46 + ``` 47 + 48 + ## Critical: TID Format 49 + 50 + Record keys for `site.standard.document` and `site.standard.publication` MUST be TIDs. Schema validation will reject anything else. 51 + 52 + **TID requirements:** 53 + - 13 characters, base32-sortable charset: `234567abcdefghijklmnopqrstuvwxyz` 54 + - First char must be `234567abcdefghij` (top bit = 0) 55 + - Regex: `/^[234567abcdefghij][234567abcdefghijklmnopqrstuvwxyz]{12}$/` 56 + 57 + See `generateTid()` in `src/lib/publisher.ts` — do not modify without reading https://atproto.com/specs/tid 58 + 59 + ## Critical: ES Modules 60 + 61 + `package.json` must have `"type": "module"`. Without this, imports break. 62 + 63 + ## Key Concepts 64 + 65 + ### Read vs Write 66 + 67 + - **SiteStandardClient** (`client.ts`): Read-only. Fetches content from ATProto. 68 + - **StandardSitePublisher** (`publisher.ts`): Write operations. Publishes content to ATProto. 69 + 70 + ### Content Transformation 71 + 72 + The `content.ts` utilities transform markdown for ATProto compatibility: 73 + - Convert HTML sidenotes → markdown blockquotes 74 + - Resolve relative links → absolute URLs 75 + - Extract plain text for search indexing 76 + - Calculate word count and reading time 77 + 78 + ### Comments System 79 + 80 + The Comments component fetches Bluesky replies and displays them as comments on blog posts. It uses the ATProto API to recursively fetch threaded conversations. 81 + 82 + ### Verification 83 + 84 + Verification helpers generate `.well-known` endpoints and `<link>` tags to prove content ownership. This allows platforms to verify that you control the content you've published. 85 + 86 + ## Testing Against Real PDS 87 + 88 + ```bash 89 + # Set your app password 90 + export ATPROTO_APP_PASSWORD="xxxx-xxxx-xxxx-xxxx" 91 + 92 + # Run publisher test 93 + node scripts/test-publisher.js 94 + ``` 95 + 96 + For integration testing, use `pds.rip` (throwaway test accounts). 97 + 98 + ## Design System 99 + 100 + The library uses semantic color tokens that automatically adapt to light/dark mode: 101 + 102 + - **Ink**: Text colors (ink-50 to ink-950) 103 + - **Canvas**: Background colors (canvas-50 to canvas-950) 104 + - **Primary**: Brand colors (primary-50 to primary-950) 105 + - **Secondary**: Secondary brand (secondary-50 to secondary-950) 106 + - **Accent**: Accent colors (accent-50 to accent-950) 107 + 108 + All styled using Tailwind v4 with `light-dark()` function. 109 + 110 + ## Publishing to ATProto 111 + 112 + ```typescript 113 + import { StandardSitePublisher } from 'svelte-standard-site/publisher'; 114 + 115 + const publisher = new StandardSitePublisher({ 116 + identifier: 'you.bsky.social', 117 + password: process.env.ATPROTO_APP_PASSWORD!, 118 + }); 119 + 120 + await publisher.login(); 121 + 122 + await publisher.publishDocument({ 123 + site: 'https://yourblog.com', 124 + title: 'My Post', 125 + publishedAt: new Date().toISOString(), 126 + content: { 127 + $type: 'site.standard.content.markdown', 128 + text: markdownContent, 129 + version: '1.0', 130 + }, 131 + textContent: plainTextContent, 132 + }); 133 + ``` 134 + 135 + ## Reading from ATProto 136 + 137 + ```typescript 138 + import { createClient } from 'svelte-standard-site'; 139 + import { getConfigFromEnv } from 'svelte-standard-site/config/env'; 140 + 141 + const config = getConfigFromEnv(); 142 + const client = createClient(config); 143 + 144 + const documents = await client.fetchAllDocuments(fetch); 145 + const publications = await client.fetchAllPublications(fetch); 146 + ``` 147 + 148 + ## Comments 149 + 150 + ```svelte 151 + <script> 152 + import { Comments } from 'svelte-standard-site'; 153 + </script> 154 + 155 + <Comments 156 + bskyPostUri="at://did:plc:xxx/app.bsky.feed.post/abc123" 157 + canonicalUrl="https://yourblog.com/posts/my-post" 158 + maxDepth={3} 159 + /> 160 + ``` 161 + 162 + ## Content Transformation 163 + 164 + ```typescript 165 + import { transformContent } from 'svelte-standard-site/content'; 166 + 167 + const result = transformContent(rawMarkdown, { 168 + baseUrl: 'https://yourblog.com', 169 + }); 170 + 171 + // result.markdown - cleaned for ATProto 172 + // result.textContent - plain text for search 173 + // result.wordCount 174 + // result.readingTime 175 + ``` 176 + 177 + ## Verification 178 + 179 + ```typescript 180 + // src/routes/.well-known/site.standard.publication/+server.ts 181 + import { generatePublicationWellKnown } from 'svelte-standard-site/verification'; 182 + import { text } from '@sveltejs/kit'; 183 + 184 + export function GET() { 185 + return text( 186 + generatePublicationWellKnown({ 187 + did: 'did:plc:xxx', 188 + publicationRkey: '3abc123xyz', 189 + }) 190 + ); 191 + } 192 + ``` 193 + 194 + ## Important Notes 195 + 196 + 1. **App Passwords**: Always use app passwords, never main account passwords 197 + 2. **PDS Resolution**: The publisher auto-resolves PDS from DID documents 198 + 3. **Caching**: The client has built-in caching (5-minute TTL by default) 199 + 4. **SSR**: All fetch operations support SvelteKit's `fetch` for SSR 200 + 5. **Theme Store**: Call `themeStore.init()` in `onMount()` to enable theme toggle 201 + 6. **Blob URLs**: Cover images and icons are converted from blob refs to HTTPS URLs 202 + 203 + ## External References 204 + 205 + - ATProto specs: https://atproto.com/ 206 + - standard.site: https://standard.site/ 207 + - Lexicon explorer: https://pdsls.dev/ 208 + - Bluesky: https://bsky.app/ 209 + 210 + ## License 211 + 212 + AGPL-3.0 (stricter than Astro version's MIT)
+203
packages/svelte-standard-site/CONTRIBUTING.md
··· 1 + # Contributing to svelte-standard-site 2 + 3 + Thank you for your interest in contributing! This document provides guidelines and information for contributors. 4 + 5 + ## Getting Started 6 + 7 + ### Prerequisites 8 + 9 + - Node.js 18 or higher 10 + - pnpm 8 or higher 11 + 12 + ### Setup 13 + 14 + 1. Fork and clone the repository: 15 + 16 + ```bash 17 + git clone https://github.com/ewanc26/svelte-standard-site.git 18 + cd svelte-standard-site 19 + ``` 20 + 21 + 2. Install dependencies: 22 + 23 + ```bash 24 + pnpm install 25 + ``` 26 + 27 + 3. Create a `.env` file: 28 + 29 + ```bash 30 + cp .env.example .env 31 + # Edit .env and add your PUBLIC_ATPROTO_DID 32 + ``` 33 + 34 + 4. Start the development server: 35 + 36 + ```bash 37 + pnpm dev 38 + ``` 39 + 40 + ## Development Workflow 41 + 42 + ### Project Structure 43 + 44 + ``` 45 + src/ 46 + ├── lib/ # Library source code 47 + │ ├── client.ts # Main client implementation 48 + │ ├── types.ts # TypeScript type definitions 49 + │ ├── index.ts # Public API exports 50 + │ ├── components/ # Reusable Svelte components 51 + │ │ ├── PublicationCard.svelte 52 + │ │ └── DocumentCard.svelte 53 + │ ├── config/ # Configuration utilities 54 + │ │ └── env.ts # Environment variable handling 55 + │ └── utils/ # Utility functions 56 + │ ├── agents.ts # AT Protocol agent utilities 57 + │ ├── at-uri.ts # AT URI parsing utilities 58 + │ └── cache.ts # Caching implementation 59 + └── routes/ # Demo/showcase pages 60 + ├── +page.svelte 61 + └── +page.server.ts 62 + ``` 63 + 64 + ### Commands 65 + 66 + - `pnpm dev` - Start development server 67 + - `pnpm build` - Build the library 68 + - `pnpm check` - Run type checking 69 + - `pnpm format` - Format code with Prettier 70 + - `pnpm lint` - Check code formatting 71 + - `pnpm prepack` - Prepare package for publishing 72 + 73 + ## Making Changes 74 + 75 + ### Code Style 76 + 77 + - We use Prettier for code formatting 78 + - Run `pnpm format` before committing 79 + - TypeScript strict mode is enabled 80 + - Follow the existing code structure and patterns 81 + 82 + ### Commit Messages 83 + 84 + We follow the [Conventional Commits](https://www.conventionalcommits.org/) specification: 85 + 86 + - `feat:` - New features 87 + - `fix:` - Bug fixes 88 + - `docs:` - Documentation changes 89 + - `style:` - Code style changes (formatting, etc.) 90 + - `refactor:` - Code refactoring 91 + - `test:` - Test additions or changes 92 + - `chore:` - Build process or tooling changes 93 + 94 + Example: 95 + 96 + ``` 97 + feat: add support for custom PDS endpoints 98 + fix: resolve caching issue with blob URLs 99 + docs: update README with new examples 100 + ``` 101 + 102 + ### Pull Request Process 103 + 104 + 1. Create a new branch: 105 + 106 + ```bash 107 + git checkout -b feat/your-feature-name 108 + ``` 109 + 110 + 2. Make your changes and commit them: 111 + 112 + ```bash 113 + git add . 114 + git commit -m "feat: add your feature" 115 + ``` 116 + 117 + 3. Push to your fork: 118 + 119 + ```bash 120 + git push origin feat/your-feature-name 121 + ``` 122 + 123 + 4. Open a Pull Request on GitHub 124 + 125 + 5. Ensure: 126 + - Code passes type checking (`pnpm check`) 127 + - Code is properly formatted (`pnpm format`) 128 + - Documentation is updated if needed 129 + - Examples are added for new features 130 + 131 + ## What to Contribute 132 + 133 + ### Good First Issues 134 + 135 + Look for issues labeled `good first issue` for beginner-friendly tasks. 136 + 137 + ### Areas for Contribution 138 + 139 + - **Bug Fixes**: Report and fix bugs 140 + - **Features**: Implement new features (discuss in an issue first) 141 + - **Documentation**: Improve or expand documentation 142 + - **Examples**: Add new usage examples 143 + - **Components**: Create new reusable components 144 + - **Tests**: Add or improve test coverage 145 + - **Performance**: Optimize existing code 146 + 147 + ## Reporting Bugs 148 + 149 + When reporting bugs, please include: 150 + 151 + 1. A clear description of the issue 152 + 2. Steps to reproduce 153 + 3. Expected behavior 154 + 4. Actual behavior 155 + 5. Environment details (Node version, OS, etc.) 156 + 6. Code samples if applicable 157 + 158 + ## Feature Requests 159 + 160 + For feature requests: 161 + 162 + 1. Check if the feature already exists or is planned 163 + 2. Open an issue describing: 164 + - The problem you're trying to solve 165 + - Your proposed solution 166 + - Any alternatives you've considered 167 + - Examples of the desired behavior 168 + 169 + ## Code of Conduct 170 + 171 + ### Our Pledge 172 + 173 + We are committed to providing a friendly, safe, and welcoming environment for all contributors. 174 + 175 + ### Expected Behavior 176 + 177 + - Be respectful and inclusive 178 + - Welcome newcomers 179 + - Accept constructive criticism gracefully 180 + - Focus on what's best for the community 181 + - Show empathy towards others 182 + 183 + ### Unacceptable Behavior 184 + 185 + - Harassment of any kind 186 + - Discriminatory language or actions 187 + - Personal attacks 188 + - Publishing others' private information 189 + - Other conduct which could reasonably be considered inappropriate 190 + 191 + ## Questions? 192 + 193 + Feel free to: 194 + 195 + - Open an issue for questions 196 + - Start a discussion in GitHub Discussions 197 + - Reach out to maintainers 198 + 199 + ## License 200 + 201 + By contributing, you agree that your contributions will be licensed under the [AGPL-3.0](./LICENSE). 202 + 203 + Thank you for contributing to svelte-standard-site! 🎉
+925
packages/svelte-standard-site/EXAMPLES.md
··· 1 + # svelte-standard-site Examples 2 + 3 + This file contains comprehensive examples of using svelte-standard-site in various scenarios. 4 + 5 + ## Table of Contents 6 + 7 + - [Basic Setup](#basic-setup) 8 + - [Simple Blog](#simple-blog) 9 + - [Using Utility Components](#using-utility-components) 10 + - [Building Custom Cards](#building-custom-cards) 11 + - [Multi-Publication Site](#multi-publication-site) 12 + - [Custom Styling](#custom-styling) 13 + - [Custom Layout](#custom-layout) 14 + - [Programmatic Theme Control](#programmatic-theme-control) 15 + - [Internationalization](#internationalization) 16 + - [Server-Side Rendering](#server-side-rendering) 17 + 18 + ## Basic Setup 19 + 20 + ### 1. Install and Configure 21 + 22 + ```bash 23 + pnpm add svelte-standard-site 24 + ``` 25 + 26 + Create `.env`: 27 + 28 + ```env 29 + PUBLIC_ATPROTO_DID=did:plc:your-did-here 30 + ``` 31 + 32 + ### 2. Root Layout 33 + 34 + ```svelte 35 + <!-- src/routes/+layout.svelte --> 36 + <script lang="ts"> 37 + import 'svelte-standard-site/styles/base.css'; 38 + import type { Snippet } from 'svelte'; 39 + 40 + interface Props { 41 + children: Snippet; 42 + } 43 + 44 + let { children }: Props = $props(); 45 + </script> 46 + 47 + {@render children()} 48 + ``` 49 + 50 + ### 3. Home Page 51 + 52 + ```svelte 53 + <!-- src/routes/+page.svelte --> 54 + <script lang="ts"> 55 + import { StandardSiteLayout, PublicationCard } from 'svelte-standard-site'; 56 + import type { PageData } from './$types'; 57 + 58 + const { data }: { data: PageData } = $props(); 59 + </script> 60 + 61 + <StandardSiteLayout title="My Site"> 62 + <h1>Welcome!</h1> 63 + 64 + <div class="grid gap-6 md:grid-cols-3"> 65 + {#each data.publications as publication} 66 + <PublicationCard {publication} /> 67 + {/each} 68 + </div> 69 + </StandardSiteLayout> 70 + ``` 71 + 72 + ## Simple Blog 73 + 74 + ### Load Function 75 + 76 + ```typescript 77 + // src/routes/blog/+page.server.ts 78 + import { createClient } from 'svelte-standard-site'; 79 + import { getConfigFromEnv } from 'svelte-standard-site/config/env'; 80 + import type { PageServerLoad } from './$types'; 81 + 82 + export const load: PageServerLoad = async ({ fetch }) => { 83 + const config = getConfigFromEnv(); 84 + if (!config) { 85 + throw new Error('Missing AT Proto configuration'); 86 + } 87 + 88 + const client = createClient(config); 89 + const documents = await client.fetchAllDocuments(fetch); 90 + 91 + return { 92 + posts: documents 93 + }; 94 + }; 95 + ``` 96 + 97 + ### Blog Index Page 98 + 99 + ```svelte 100 + <!-- src/routes/blog/+page.svelte --> 101 + <script lang="ts"> 102 + import { StandardSiteLayout, DocumentCard } from 'svelte-standard-site'; 103 + import type { PageData } from './$types'; 104 + 105 + const { data }: { data: PageData } = $props(); 106 + </script> 107 + 108 + <StandardSiteLayout title="My Blog"> 109 + <div class="mb-8"> 110 + <h1 class="text-ink-900 dark:text-ink-50 text-4xl font-bold">Blog Posts</h1> 111 + <p class="text-ink-700 dark:text-ink-200 mt-2">Thoughts, tutorials, and updates</p> 112 + </div> 113 + 114 + <div class="space-y-6"> 115 + {#each data.posts as post} 116 + <DocumentCard document={post} showCover={true} /> 117 + {/each} 118 + </div> 119 + </StandardSiteLayout> 120 + ``` 121 + 122 + ### Individual Post Page 123 + 124 + ```typescript 125 + // src/routes/blog/[pub_rkey]/[doc_rkey]/+page.server.ts 126 + import { createClient } from 'svelte-standard-site'; 127 + import { getConfigFromEnv } from 'svelte-standard-site/config/env'; 128 + import { error } from '@sveltejs/kit'; 129 + import type { PageServerLoad } from './$types'; 130 + 131 + export const load: PageServerLoad = async ({ params, fetch }) => { 132 + const config = getConfigFromEnv(); 133 + if (!config) { 134 + throw new Error('Missing configuration'); 135 + } 136 + 137 + const client = createClient(config); 138 + const document = await client.fetchDocument(params.doc_rkey, fetch); 139 + 140 + if (!document) { 141 + throw error(404, 'Post not found'); 142 + } 143 + 144 + return { 145 + post: document 146 + }; 147 + }; 148 + ``` 149 + 150 + ```svelte 151 + <!-- src/routes/blog/[pub_rkey]/[doc_rkey]/+page.svelte --> 152 + <script lang="ts"> 153 + import { StandardSiteLayout } from 'svelte-standard-site'; 154 + import type { PageData } from './$types'; 155 + 156 + const { data }: { data: PageData } = $props(); 157 + 158 + const post = $derived(data.post.value); 159 + 160 + function formatDate(date: string) { 161 + return new Date(date).toLocaleDateString('en-US', { 162 + year: 'numeric', 163 + month: 'long', 164 + day: 'numeric' 165 + }); 166 + } 167 + </script> 168 + 169 + <svelte:head> 170 + <title>{post.title} - My Blog</title> 171 + {#if post.description} 172 + <meta name="description" content={post.description} /> 173 + {/if} 174 + </svelte:head> 175 + 176 + <StandardSiteLayout title="My Blog"> 177 + <article class="mx-auto prose prose-lg dark:prose-invert"> 178 + {#if post.coverImage} 179 + <img src={post.coverImage} alt={post.title} class="w-full rounded-xl" /> 180 + {/if} 181 + 182 + <header class="mb-8"> 183 + <h1>{post.title}</h1> 184 + <div class="text-ink-600 dark:text-ink-400 flex gap-4 text-sm"> 185 + <time datetime={post.publishedAt}> 186 + {formatDate(post.publishedAt)} 187 + </time> 188 + {#if post.updatedAt} 189 + <span>Updated: {formatDate(post.updatedAt)}</span> 190 + {/if} 191 + </div> 192 + {#if post.tags && post.tags.length > 0} 193 + <div class="mt-4 flex flex-wrap gap-2"> 194 + {#each post.tags as tag} 195 + <span 196 + class="bg-primary-100 text-primary-800 dark:bg-primary-900 dark:text-primary-200 rounded-full px-3 py-1 text-xs font-medium" 197 + > 198 + {tag} 199 + </span> 200 + {/each} 201 + </div> 202 + {/if} 203 + </header> 204 + 205 + {@html post.content || post.textContent || ''} 206 + </article> 207 + </StandardSiteLayout> 208 + ``` 209 + 210 + ## Using Utility Components 211 + 212 + The modular utility components make it easy to build consistent, theme-aware UIs. 213 + 214 + ### Using DateDisplay 215 + 216 + ```svelte 217 + <script lang="ts"> 218 + import { DateDisplay } from 'svelte-standard-site'; 219 + import type { PageData } from './$types'; 220 + 221 + const { data }: { data: PageData } = $props(); 222 + </script> 223 + 224 + <article> 225 + <h1>{data.post.value.title}</h1> 226 + 227 + <!-- Simple date display --> 228 + <DateDisplay date={data.post.value.publishedAt} /> 229 + 230 + <!-- With label and icon --> 231 + <DateDisplay 232 + date={data.post.value.updatedAt} 233 + label="Last updated: " 234 + showIcon={true} 235 + class="text-ink-600 dark:text-ink-400 text-sm" 236 + /> 237 + 238 + <!-- Custom locale --> 239 + <DateDisplay date={data.post.value.publishedAt} locale="fr-FR" /> 240 + </article> 241 + ``` 242 + 243 + ### Using TagList 244 + 245 + ```svelte 246 + <script lang="ts"> 247 + import { TagList } from 'svelte-standard-site'; 248 + import type { PageData } from './$types'; 249 + 250 + const { data }: { data: PageData } = $props(); 251 + const hasTheme = $derived(!!data.publication?.value.basicTheme); 252 + </script> 253 + 254 + <!-- Simple tag list --> 255 + <TagList tags={data.post.value.tags || []} /> 256 + 257 + <!-- With theme support --> 258 + <TagList tags={data.post.value.tags || []} {hasTheme} class="mt-4" /> 259 + ``` 260 + 261 + ### Using ThemedText 262 + 263 + ```svelte 264 + <script lang="ts"> 265 + import { ThemedText } from 'svelte-standard-site'; 266 + import type { PageData } from './$types'; 267 + 268 + const { data }: { data: PageData } = $props(); 269 + const hasTheme = $derived(!!data.publication?.value.basicTheme); 270 + </script> 271 + 272 + <!-- Title with theme --> 273 + <ThemedText {hasTheme} element="h1" class="mb-4 text-4xl font-bold"> 274 + {data.post.value.title} 275 + </ThemedText> 276 + 277 + <!-- Semi-transparent description --> 278 + <ThemedText {hasTheme} opacity={70} element="p" class="text-lg"> 279 + {data.post.value.description} 280 + </ThemedText> 281 + 282 + <!-- Accent color for links --> 283 + <ThemedText {hasTheme} variant="accent" element="span">Read more →</ThemedText> 284 + ``` 285 + 286 + ### Combining Utility Components 287 + 288 + ```svelte 289 + <script lang="ts"> 290 + import { ThemedContainer, ThemedText, DateDisplay, TagList } from 'svelte-standard-site'; 291 + import type { PageData } from './$types'; 292 + 293 + const { data }: { data: PageData } = $props(); 294 + const theme = $derived(data.publication?.value.basicTheme); 295 + const hasTheme = $derived(!!theme); 296 + </script> 297 + 298 + <ThemedContainer {theme} element="article" class="p-8"> 299 + <!-- Title --> 300 + <ThemedText {hasTheme} element="h1" class="mb-2 text-4xl font-bold"> 301 + {data.post.value.title} 302 + </ThemedText> 303 + 304 + <!-- Description --> 305 + <ThemedText {hasTheme} opacity={70} element="p" class="mb-4 text-lg"> 306 + {data.post.value.description} 307 + </ThemedText> 308 + 309 + <!-- Metadata --> 310 + <div class="mb-6 flex gap-4"> 311 + <DateDisplay date={data.post.value.publishedAt} /> 312 + {#if data.post.value.updatedAt} 313 + <DateDisplay date={data.post.value.updatedAt} label="Updated " showIcon={true} /> 314 + {/if} 315 + </div> 316 + 317 + <!-- Tags --> 318 + <TagList tags={data.post.value.tags || []} {hasTheme} /> 319 + 320 + <!-- Content --> 321 + <div class="prose mt-8 max-w-none"> 322 + {@html data.post.value.content} 323 + </div> 324 + </ThemedContainer> 325 + ``` 326 + 327 + ## Building Custom Cards 328 + 329 + Use ThemedCard and utility components to build custom card layouts. 330 + 331 + ### Blog Post Card 332 + 333 + ```svelte 334 + <script lang="ts"> 335 + import { ThemedCard, ThemedText, DateDisplay, TagList } from 'svelte-standard-site'; 336 + import type { Document, Publication, AtProtoRecord } from 'svelte-standard-site'; 337 + 338 + interface Props { 339 + document: AtProtoRecord<Document>; 340 + publication?: AtProtoRecord<Publication>; 341 + } 342 + 343 + let { document, publication }: Props = $props(); 344 + 345 + const theme = $derived(publication?.value.basicTheme); 346 + const hasTheme = $derived(!!theme); 347 + const value = $derived(document.value); 348 + </script> 349 + 350 + <ThemedCard 351 + {theme} 352 + href="/blog/{document.uri.split('/').pop()}" 353 + class="transition-shadow hover:shadow-lg" 354 + > 355 + <div class="flex gap-6"> 356 + {#if value.coverImage} 357 + <img src={value.coverImage} alt={value.title} class="h-32 w-32 rounded-lg object-cover" /> 358 + {/if} 359 + 360 + <div class="flex-1"> 361 + <ThemedText {hasTheme} element="h3" class="mb-2 text-2xl font-bold"> 362 + {value.title} 363 + </ThemedText> 364 + 365 + {#if value.description} 366 + <ThemedText {hasTheme} opacity={70} element="p" class="mb-4 line-clamp-2"> 367 + {value.description} 368 + </ThemedText> 369 + {/if} 370 + 371 + <div class="mb-3 flex items-center gap-4"> 372 + <DateDisplay date={value.publishedAt} class="text-sm" /> 373 + </div> 374 + 375 + {#if value.tags?.length} 376 + <TagList tags={value.tags} {hasTheme} /> 377 + {/if} 378 + </div> 379 + </div> 380 + </ThemedCard> 381 + ``` 382 + 383 + ### Author Card 384 + 385 + ```svelte 386 + <script lang="ts"> 387 + import { ThemedCard, ThemedText } from 'svelte-standard-site'; 388 + 389 + interface Props { 390 + name: string; 391 + bio: string; 392 + avatar?: string; 393 + theme?: any; 394 + } 395 + 396 + let { name, bio, avatar, theme }: Props = $props(); 397 + const hasTheme = $derived(!!theme); 398 + </script> 399 + 400 + <ThemedCard {theme} class="p-6"> 401 + <div class="flex items-start gap-4"> 402 + {#if avatar} 403 + <img src={avatar} alt={name} class="h-16 w-16 rounded-full" /> 404 + {/if} 405 + 406 + <div> 407 + <ThemedText {hasTheme} element="h3" class="mb-2 text-xl font-bold"> 408 + {name} 409 + </ThemedText> 410 + 411 + <ThemedText {hasTheme} opacity={70} element="p"> 412 + {bio} 413 + </ThemedText> 414 + </div> 415 + </div> 416 + </ThemedCard> 417 + ``` 418 + 419 + ### Feature Card with Icon 420 + 421 + ```svelte 422 + <script lang="ts"> 423 + import { ThemedCard, ThemedText } from 'svelte-standard-site'; 424 + import type { Snippet } from 'svelte'; 425 + 426 + interface Props { 427 + title: string; 428 + description: string; 429 + icon: Snippet; 430 + theme?: any; 431 + } 432 + 433 + let { title, description, icon, theme }: Props = $props(); 434 + const hasTheme = $derived(!!theme); 435 + </script> 436 + 437 + <ThemedCard {theme} class="p-6 text-center"> 438 + <div 439 + class="bg-primary-100 dark:bg-primary-900 mb-4 inline-flex h-16 w-16 items-center justify-center rounded-full" 440 + > 441 + {@render icon()} 442 + </div> 443 + 444 + <ThemedText {hasTheme} element="h3" class="mb-2 text-xl font-bold"> 445 + {title} 446 + </ThemedText> 447 + 448 + <ThemedText {hasTheme} opacity={70} element="p"> 449 + {description} 450 + </ThemedText> 451 + </ThemedCard> 452 + ``` 453 + 454 + ## Internationalization 455 + 456 + ### Automatic Locale Detection 457 + 458 + The DateDisplay component automatically detects the user's browser locale. 459 + 460 + ```svelte 461 + <script lang="ts"> 462 + import { DateDisplay } from 'svelte-standard-site'; 463 + </script> 464 + 465 + <!-- Automatically formats based on user's locale --> 466 + <DateDisplay date="2026-01-19T12:00:00Z" /> 467 + 468 + <!-- 469 + Results: 470 + - en-US: "January 19, 2026" 471 + - fr-FR: "19 janvier 2026" 472 + - de-DE: "19. Januar 2026" 473 + - ja-JP: "2026年1月19日" 474 + - es-ES: "19 de enero de 2026" 475 + --> 476 + ``` 477 + 478 + ### Explicit Locale Override 479 + 480 + ```svelte 481 + <script lang="ts"> 482 + import { DateDisplay } from 'svelte-standard-site'; 483 + 484 + // User preference from settings or profile 485 + let userLocale = $state('fr-FR'); 486 + </script> 487 + 488 + <DateDisplay date="2026-01-19T12:00:00Z" locale={userLocale} /> 489 + ``` 490 + 491 + ### Multi-Language Blog 492 + 493 + ```svelte 494 + <script lang="ts"> 495 + import { 496 + StandardSiteLayout, 497 + ThemedContainer, 498 + ThemedText, 499 + DateDisplay 500 + } from 'svelte-standard-site'; 501 + import type { PageData } from './$types'; 502 + 503 + const { data }: { data: PageData } = $props(); 504 + 505 + // Detect user's language 506 + let locale = $state('en-US'); 507 + 508 + $effect(() => { 509 + if (typeof navigator !== 'undefined') { 510 + locale = navigator.language || 'en-US'; 511 + } 512 + }); 513 + 514 + const theme = $derived(data.publication?.value.basicTheme); 515 + const hasTheme = $derived(!!theme); 516 + </script> 517 + 518 + <StandardSiteLayout title={data.publication?.value.name}> 519 + <ThemedContainer {theme}> 520 + <ThemedText {hasTheme} element="h1" class="mb-4 text-4xl font-bold"> 521 + {data.post.value.title} 522 + </ThemedText> 523 + 524 + <!-- Date automatically formats to user's locale --> 525 + <DateDisplay 526 + date={data.post.value.publishedAt} 527 + {locale} 528 + class="text-ink-600 dark:text-ink-400 text-sm" 529 + /> 530 + 531 + <div class="prose mt-8 max-w-none"> 532 + {@html data.post.value.content} 533 + </div> 534 + </ThemedContainer> 535 + </StandardSiteLayout> 536 + ``` 537 + 538 + ## Multi-Publication Site 539 + 540 + ```typescript 541 + // src/routes/+page.server.ts 542 + import { createClient } from 'svelte-standard-site'; 543 + import { getConfigFromEnv } from 'svelte-standard-site/config/env'; 544 + import type { PageServerLoad } from './$types'; 545 + 546 + export const load: PageServerLoad = async ({ fetch }) => { 547 + const config = getConfigFromEnv(); 548 + if (!config) { 549 + return { error: 'Configuration missing', publications: [], documents: [] }; 550 + } 551 + 552 + const client = createClient(config); 553 + 554 + const [publications, documents] = await Promise.all([ 555 + client.fetchAllPublications(fetch), 556 + client.fetchAllDocuments(fetch) 557 + ]); 558 + 559 + // Group documents by publication 560 + const documentsByPub = new Map(); 561 + for (const doc of documents) { 562 + const pubUri = doc.value.site; 563 + if (!documentsByPub.has(pubUri)) { 564 + documentsByPub.set(pubUri, []); 565 + } 566 + documentsByPub.get(pubUri).push(doc); 567 + } 568 + 569 + return { 570 + publications, 571 + documents, 572 + documentsByPub: Object.fromEntries(documentsByPub) 573 + }; 574 + }; 575 + ``` 576 + 577 + ```svelte 578 + <!-- src/routes/+page.svelte --> 579 + <script lang="ts"> 580 + import { StandardSiteLayout, PublicationCard, DocumentCard } from 'svelte-standard-site'; 581 + import type { PageData } from './$types'; 582 + 583 + const { data }: { data: PageData } = $props(); 584 + </script> 585 + 586 + <StandardSiteLayout title="My Publications"> 587 + {#each data.publications as publication} 588 + <section class="mb-16"> 589 + <div class="mb-8"> 590 + <PublicationCard {publication} /> 591 + </div> 592 + 593 + <h2 class="text-ink-900 dark:text-ink-50 mb-6 text-2xl font-bold"> 594 + Recent from {publication.value.name} 595 + </h2> 596 + 597 + {#if data.documentsByPub[publication.uri]} 598 + <div class="space-y-6"> 599 + {#each data.documentsByPub[publication.uri].slice(0, 5) as document} 600 + <DocumentCard {document} /> 601 + {/each} 602 + </div> 603 + {:else} 604 + <p class="text-ink-600 dark:text-ink-400">No documents yet</p> 605 + {/if} 606 + </section> 607 + {/each} 608 + </StandardSiteLayout> 609 + ``` 610 + 611 + ## Custom Styling 612 + 613 + ### Override Theme Colors 614 + 615 + ```css 616 + /* src/app.css or src/lib/styles/custom.css */ 617 + @import 'svelte-standard-site/styles/base.css'; 618 + 619 + /* Override primary color */ 620 + :root { 621 + --color-primary-50: oklch(18.2% 0.018 280); 622 + --color-primary-100: oklch(26.5% 0.03 280); 623 + --color-primary-200: oklch(40.5% 0.048 280); 624 + --color-primary-300: oklch(54% 0.065 280); 625 + --color-primary-400: oklch(66.5% 0.08 280); 626 + --color-primary-500: oklch(78.5% 0.095 280); 627 + --color-primary-600: oklch(82.2% 0.078 280); 628 + --color-primary-700: oklch(86.5% 0.062 280); 629 + --color-primary-800: oklch(91% 0.042 280); 630 + --color-primary-900: oklch(95.8% 0.022 280); 631 + --color-primary-950: oklch(98% 0.012 280); 632 + } 633 + ``` 634 + 635 + ### Custom Component Styles 636 + 637 + ```svelte 638 + <script lang="ts"> 639 + import { DocumentCard } from 'svelte-standard-site'; 640 + </script> 641 + 642 + <DocumentCard 643 + {document} 644 + class=" 645 + border-primary-500 646 + border-2 647 + shadow-2xl 648 + transition-transform 649 + hover:scale-105 650 + " 651 + /> 652 + ``` 653 + 654 + ## Custom Layout 655 + 656 + ### Full Custom Layout with Theme Support 657 + 658 + ```svelte 659 + <!-- src/routes/+layout.svelte --> 660 + <script lang="ts"> 661 + import 'svelte-standard-site/styles/base.css'; 662 + import { ThemeToggle, themeStore } from 'svelte-standard-site'; 663 + import { onMount } from 'svelte'; 664 + import { page } from '$app/stores'; 665 + import type { Snippet } from 'svelte'; 666 + 667 + interface Props { 668 + children: Snippet; 669 + } 670 + 671 + let { children }: Props = $props(); 672 + 673 + // Navigation items 674 + const navItems = [ 675 + { href: '/', label: 'Home' }, 676 + { href: '/blog', label: 'Blog' }, 677 + { href: '/about', label: 'About' } 678 + ]; 679 + 680 + onMount(() => { 681 + themeStore.init(); 682 + }); 683 + </script> 684 + 685 + <svelte:head> 686 + <script> 687 + // Prevent FOUC 688 + (function () { 689 + const stored = localStorage.getItem('theme'); 690 + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; 691 + const isDark = stored === 'dark' || (!stored && prefersDark); 692 + const htmlElement = document.documentElement; 693 + 694 + if (isDark) { 695 + htmlElement.classList.add('dark'); 696 + htmlElement.style.colorScheme = 'dark'; 697 + } else { 698 + htmlElement.classList.remove('dark'); 699 + htmlElement.style.colorScheme = 'light'; 700 + } 701 + })(); 702 + </script> 703 + </svelte:head> 704 + 705 + <div class="bg-canvas-50 text-ink-900 dark:bg-canvas-950 dark:text-ink-50 min-h-screen"> 706 + <!-- Custom Header --> 707 + <header 708 + class="border-canvas-200 bg-canvas-50/90 dark:border-canvas-800 dark:bg-canvas-950/90 border-b backdrop-blur-md" 709 + > 710 + <nav class="container mx-auto flex items-center justify-between px-4 py-4"> 711 + <a href="/" class="text-primary-600 dark:text-primary-400 text-2xl font-bold"> MyBrand </a> 712 + 713 + <ul class="flex items-center gap-6"> 714 + {#each navItems as item} 715 + <li> 716 + <a 717 + href={item.href} 718 + class="hover:text-primary-600 dark:hover:text-primary-400 font-medium transition-colors 719 + {$page.url.pathname === item.href 720 + ? 'text-primary-600 dark:text-primary-400' 721 + : 'text-ink-700 dark:text-ink-200'}" 722 + > 723 + {item.label} 724 + </a> 725 + </li> 726 + {/each} 727 + <li> 728 + <ThemeToggle /> 729 + </li> 730 + </ul> 731 + </nav> 732 + </header> 733 + 734 + <!-- Main Content --> 735 + <main class="container mx-auto px-4 py-12"> 736 + {@render children()} 737 + </main> 738 + 739 + <!-- Custom Footer --> 740 + <footer 741 + class="border-canvas-200 bg-canvas-50 dark:border-canvas-800 dark:bg-canvas-950 border-t py-8" 742 + > 743 + <div class="text-ink-700 dark:text-ink-200 container mx-auto px-4 text-center text-sm"> 744 + <p>&copy; {new Date().getFullYear()} MyBrand. All rights reserved.</p> 745 + <p class="mt-2"> 746 + Powered by 747 + <a 748 + href="https://github.com/ewanc26/svelte-standard-site" 749 + class="text-primary-600 dark:text-primary-400 hover:underline" 750 + > 751 + svelte-standard-site 752 + </a> 753 + </p> 754 + </div> 755 + </footer> 756 + </div> 757 + ``` 758 + 759 + ## Programmatic Theme Control 760 + 761 + ### Theme Toggle Button 762 + 763 + ```svelte 764 + <script lang="ts"> 765 + import { themeStore } from 'svelte-standard-site'; 766 + import { onMount } from 'svelte'; 767 + 768 + let isDark = $state(false); 769 + 770 + onMount(() => { 771 + themeStore.init(); 772 + 773 + const unsubscribe = themeStore.subscribe((state) => { 774 + isDark = state.isDark; 775 + }); 776 + 777 + return unsubscribe; 778 + }); 779 + </script> 780 + 781 + <button onclick={() => themeStore.toggle()} class="bg-primary-600 rounded-lg px-4 py-2 text-white"> 782 + Switch to {isDark ? 'Light' : 'Dark'} Mode 783 + </button> 784 + ``` 785 + 786 + ### System Preference Detection 787 + 788 + ```svelte 789 + <script lang="ts"> 790 + import { themeStore } from 'svelte-standard-site'; 791 + import { onMount } from 'svelte'; 792 + 793 + let systemPreference = $state<'light' | 'dark'>('light'); 794 + let currentTheme = $state<'light' | 'dark' | 'system'>('system'); 795 + 796 + onMount(() => { 797 + themeStore.init(); 798 + 799 + // Detect system preference 800 + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); 801 + systemPreference = mediaQuery.matches ? 'dark' : 'light'; 802 + 803 + // Check if user has overridden 804 + const stored = localStorage.getItem('theme'); 805 + currentTheme = stored ? (stored as 'light' | 'dark') : 'system'; 806 + 807 + // Listen for changes 808 + mediaQuery.addEventListener('change', (e) => { 809 + systemPreference = e.matches ? 'dark' : 'light'; 810 + }); 811 + }); 812 + 813 + function setTheme(theme: 'light' | 'dark' | 'system') { 814 + if (theme === 'system') { 815 + localStorage.removeItem('theme'); 816 + themeStore.setTheme(systemPreference === 'dark'); 817 + } else { 818 + themeStore.setTheme(theme === 'dark'); 819 + } 820 + currentTheme = theme; 821 + } 822 + </script> 823 + 824 + <div class="flex gap-2"> 825 + <button 826 + onclick={() => setTheme('light')} 827 + class="rounded px-4 py-2 {currentTheme === 'light' 828 + ? 'bg-primary-600 text-white' 829 + : 'bg-canvas-200'}" 830 + > 831 + Light 832 + </button> 833 + <button 834 + onclick={() => setTheme('dark')} 835 + class="rounded px-4 py-2 {currentTheme === 'dark' 836 + ? 'bg-primary-600 text-white' 837 + : 'bg-canvas-200'}" 838 + > 839 + Dark 840 + </button> 841 + <button 842 + onclick={() => setTheme('system')} 843 + class="rounded px-4 py-2 {currentTheme === 'system' 844 + ? 'bg-primary-600 text-white' 845 + : 'bg-canvas-200'}" 846 + > 847 + System 848 + </button> 849 + </div> 850 + ``` 851 + 852 + ## Server-Side Rendering 853 + 854 + ### Pre-render Static Pages 855 + 856 + ```typescript 857 + // svelte.config.js 858 + import adapter from '@sveltejs/adapter-static'; 859 + 860 + export default { 861 + kit: { 862 + adapter: adapter({ 863 + pages: 'build', 864 + assets: 'build', 865 + fallback: null, 866 + precompress: false 867 + }), 868 + prerender: { 869 + entries: ['*'] 870 + } 871 + } 872 + }; 873 + ``` 874 + 875 + ### Generate Dynamic Routes 876 + 877 + ```typescript 878 + // src/routes/blog/[pub_rkey]/[doc_rkey]/+page.server.ts 879 + import { createClient } from 'svelte-standard-site'; 880 + import { getConfigFromEnv } from 'svelte-standard-site/config/env'; 881 + import { error } from '@sveltejs/kit'; 882 + import type { PageServerLoad, EntryGenerator } from './$types'; 883 + 884 + export const load: PageServerLoad = async ({ params, fetch }) => { 885 + const config = getConfigFromEnv(); 886 + if (!config) throw error(500, 'Configuration missing'); 887 + 888 + const client = createClient(config); 889 + const document = await client.fetchDocument(params.doc_rkey, fetch); 890 + 891 + if (!document) throw error(404, 'Post not found'); 892 + 893 + return { post: document }; 894 + }; 895 + 896 + export const entries: EntryGenerator = async () => { 897 + const config = getConfigFromEnv(); 898 + if (!config) return []; 899 + 900 + const client = createClient(config); 901 + const documents = await client.fetchAllDocuments(); 902 + 903 + return documents.map((doc) => { 904 + const pubRkey = doc.value.site.split('/').pop() || ''; 905 + const docRkey = doc.uri.split('/').pop() || ''; 906 + return { pub_rkey: pubRkey, doc_rkey: docRkey }; 907 + }); 908 + }; 909 + ``` 910 + 911 + ## Tips and Best Practices 912 + 913 + 1. **Always import base.css** in your root layout for consistent styling 914 + 2. **Use the ThemeToggle component** or manage theme with themeStore 915 + 3. **Leverage utility components** - Use DateDisplay, TagList, ThemedText, etc. for consistency 916 + 4. **Follow DRY principles** - Don't manually format dates or apply theme colors repeatedly 917 + 5. **Leverage the design tokens** (ink, canvas, primary, etc.) for consistency 918 + 6. **Pass custom classes** to components for one-off customizations 919 + 7. **Use server-side rendering** for better SEO and performance 920 + 8. **Cache aggressively** - the library has built-in caching 921 + 9. **Handle errors gracefully** - always check for null/undefined data 922 + 10. **Test dark mode** - all components support it out of the box 923 + 11. **Embrace locale-aware dates** - DateDisplay automatically formats for user's locale 924 + 925 + For more examples and detailed documentation, visit the [GitHub repository](https://github.com/ewanc26/svelte-standard-site).
+661
packages/svelte-standard-site/LICENSE
··· 1 + GNU AFFERO GENERAL PUBLIC LICENSE 2 + Version 3, 19 November 2007 3 + 4 + Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> 5 + Everyone is permitted to copy and distribute verbatim copies 6 + of this license document, but changing it is not allowed. 7 + 8 + Preamble 9 + 10 + The GNU Affero General Public License is a free, copyleft license for 11 + software and other kinds of works, specifically designed to ensure 12 + cooperation with the community in the case of network server software. 13 + 14 + The licenses for most software and other practical works are designed 15 + to take away your freedom to share and change the works. By contrast, 16 + our General Public Licenses are intended to guarantee your freedom to 17 + share and change all versions of a program--to make sure it remains free 18 + software for all its users. 19 + 20 + When we speak of free software, we are referring to freedom, not 21 + price. Our General Public Licenses are designed to make sure that you 22 + have the freedom to distribute copies of free software (and charge for 23 + them if you wish), that you receive source code or can get it if you 24 + want it, that you can change the software or use pieces of it in new 25 + free programs, and that you know you can do these things. 26 + 27 + Developers that use our General Public Licenses protect your rights 28 + with two steps: (1) assert copyright on the software, and (2) offer 29 + you this License which gives you legal permission to copy, distribute 30 + and/or modify the software. 31 + 32 + A secondary benefit of defending all users' freedom is that 33 + improvements made in alternate versions of the program, if they 34 + receive widespread use, become available for other developers to 35 + incorporate. Many developers of free software are heartened and 36 + encouraged by the resulting cooperation. However, in the case of 37 + software used on network servers, this result may fail to come about. 38 + The GNU General Public License permits making a modified version and 39 + letting the public access it on a server without ever releasing its 40 + source code to the public. 41 + 42 + The GNU Affero General Public License is designed specifically to 43 + ensure that, in such cases, the modified source code becomes available 44 + to the community. It requires the operator of a network server to 45 + provide the source code of the modified version running there to the 46 + users of that server. Therefore, public use of a modified version, on 47 + a publicly accessible server, gives the public access to the source 48 + code of the modified version. 49 + 50 + An older license, called the Affero General Public License and 51 + published by Affero, was designed to accomplish similar goals. This is 52 + a different license, not a version of the Affero GPL, but Affero has 53 + released a new version of the Affero GPL which permits relicensing under 54 + this license. 55 + 56 + The precise terms and conditions for copying, distribution and 57 + modification follow. 58 + 59 + TERMS AND CONDITIONS 60 + 61 + 0. Definitions. 62 + 63 + "This License" refers to version 3 of the GNU Affero General Public License. 64 + 65 + "Copyright" also means copyright-like laws that apply to other kinds of 66 + works, such as semiconductor masks. 67 + 68 + "The Program" refers to any copyrightable work licensed under this 69 + License. Each licensee is addressed as "you". "Licensees" and 70 + "recipients" may be individuals or organizations. 71 + 72 + To "modify" a work means to copy from or adapt all or part of the work 73 + in a fashion requiring copyright permission, other than the making of an 74 + exact copy. The resulting work is called a "modified version" of the 75 + earlier work or a work "based on" the earlier work. 76 + 77 + A "covered work" means either the unmodified Program or a work based 78 + on the Program. 79 + 80 + To "propagate" a work means to do anything with it that, without 81 + permission, would make you directly or secondarily liable for 82 + infringement under applicable copyright law, except executing it on a 83 + computer or modifying a private copy. Propagation includes copying, 84 + distribution (with or without modification), making available to the 85 + public, and in some countries other activities as well. 86 + 87 + To "convey" a work means any kind of propagation that enables other 88 + parties to make or receive copies. Mere interaction with a user through 89 + a computer network, with no transfer of a copy, is not conveying. 90 + 91 + An interactive user interface displays "Appropriate Legal Notices" 92 + to the extent that it includes a convenient and prominently visible 93 + feature that (1) displays an appropriate copyright notice, and (2) 94 + tells the user that there is no warranty for the work (except to the 95 + extent that warranties are provided), that licensees may convey the 96 + work under this License, and how to view a copy of this License. If 97 + the interface presents a list of user commands or options, such as a 98 + menu, a prominent item in the list meets this criterion. 99 + 100 + 1. Source Code. 101 + 102 + The "source code" for a work means the preferred form of the work 103 + for making modifications to it. "Object code" means any non-source 104 + form of a work. 105 + 106 + A "Standard Interface" means an interface that either is an official 107 + standard defined by a recognized standards body, or, in the case of 108 + interfaces specified for a particular programming language, one that 109 + is widely used among developers working in that language. 110 + 111 + The "System Libraries" of an executable work include anything, other 112 + than the work as a whole, that (a) is included in the normal form of 113 + packaging a Major Component, but which is not part of that Major 114 + Component, and (b) serves only to enable use of the work with that 115 + Major Component, or to implement a Standard Interface for which an 116 + implementation is available to the public in source code form. A 117 + "Major Component", in this context, means a major essential component 118 + (kernel, window system, and so on) of the specific operating system 119 + (if any) on which the executable work runs, or a compiler used to 120 + produce the work, or an object code interpreter used to run it. 121 + 122 + The "Corresponding Source" for a work in object code form means all 123 + the source code needed to generate, install, and (for an executable 124 + work) run the object code and to modify the work, including scripts to 125 + control those activities. However, it does not include the work's 126 + System Libraries, or general-purpose tools or generally available free 127 + programs which are used unmodified in performing those activities but 128 + which are not part of the work. For example, Corresponding Source 129 + includes interface definition files associated with source files for 130 + the work, and the source code for shared libraries and dynamically 131 + linked subprograms that the work is specifically designed to require, 132 + such as by intimate data communication or control flow between those 133 + subprograms and other parts of the work. 134 + 135 + The Corresponding Source need not include anything that users 136 + can regenerate automatically from other parts of the Corresponding 137 + Source. 138 + 139 + The Corresponding Source for a work in source code form is that 140 + same work. 141 + 142 + 2. Basic Permissions. 143 + 144 + All rights granted under this License are granted for the term of 145 + copyright on the Program, and are irrevocable provided the stated 146 + conditions are met. This License explicitly affirms your unlimited 147 + permission to run the unmodified Program. The output from running a 148 + covered work is covered by this License only if the output, given its 149 + content, constitutes a covered work. This License acknowledges your 150 + rights of fair use or other equivalent, as provided by copyright law. 151 + 152 + You may make, run and propagate covered works that you do not 153 + convey, without conditions so long as your license otherwise remains 154 + in force. You may convey covered works to others for the sole purpose 155 + of having them make modifications exclusively for you, or provide you 156 + with facilities for running those works, provided that you comply with 157 + the terms of this License in conveying all material for which you do 158 + not control copyright. Those thus making or running the covered works 159 + for you must do so exclusively on your behalf, under your direction 160 + and control, on terms that prohibit them from making any copies of 161 + your copyrighted material outside their relationship with you. 162 + 163 + Conveying under any other circumstances is permitted solely under 164 + the conditions stated below. Sublicensing is not allowed; section 10 165 + makes it unnecessary. 166 + 167 + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 + 169 + No covered work shall be deemed part of an effective technological 170 + measure under any applicable law fulfilling obligations under article 171 + 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 + similar laws prohibiting or restricting circumvention of such 173 + measures. 174 + 175 + When you convey a covered work, you waive any legal power to forbid 176 + circumvention of technological measures to the extent such circumvention 177 + is effected by exercising rights under this License with respect to 178 + the covered work, and you disclaim any intention to limit operation or 179 + modification of the work as a means of enforcing, against the work's 180 + users, your or third parties' legal rights to forbid circumvention of 181 + technological measures. 182 + 183 + 4. Conveying Verbatim Copies. 184 + 185 + You may convey verbatim copies of the Program's source code as you 186 + receive it, in any medium, provided that you conspicuously and 187 + appropriately publish on each copy an appropriate copyright notice; 188 + keep intact all notices stating that this License and any 189 + non-permissive terms added in accord with section 7 apply to the code; 190 + keep intact all notices of the absence of any warranty; and give all 191 + recipients a copy of this License along with the Program. 192 + 193 + You may charge any price or no price for each copy that you convey, 194 + and you may offer support or warranty protection for a fee. 195 + 196 + 5. Conveying Modified Source Versions. 197 + 198 + You may convey a work based on the Program, or the modifications to 199 + produce it from the Program, in the form of source code under the 200 + terms of section 4, provided that you also meet all of these conditions: 201 + 202 + a) The work must carry prominent notices stating that you modified 203 + it, and giving a relevant date. 204 + 205 + b) The work must carry prominent notices stating that it is 206 + released under this License and any conditions added under section 207 + 7. This requirement modifies the requirement in section 4 to 208 + "keep intact all notices". 209 + 210 + c) You must license the entire work, as a whole, under this 211 + License to anyone who comes into possession of a copy. This 212 + License will therefore apply, along with any applicable section 7 213 + additional terms, to the whole of the work, and all its parts, 214 + regardless of how they are packaged. This License gives no 215 + permission to license the work in any other way, but it does not 216 + invalidate such permission if you have separately received it. 217 + 218 + d) If the work has interactive user interfaces, each must display 219 + Appropriate Legal Notices; however, if the Program has interactive 220 + interfaces that do not display Appropriate Legal Notices, your 221 + work need not make them do so. 222 + 223 + A compilation of a covered work with other separate and independent 224 + works, which are not by their nature extensions of the covered work, 225 + and which are not combined with it such as to form a larger program, 226 + in or on a volume of a storage or distribution medium, is called an 227 + "aggregate" if the compilation and its resulting copyright are not 228 + used to limit the access or legal rights of the compilation's users 229 + beyond what the individual works permit. Inclusion of a covered work 230 + in an aggregate does not cause this License to apply to the other 231 + parts of the aggregate. 232 + 233 + 6. Conveying Non-Source Forms. 234 + 235 + You may convey a covered work in object code form under the terms 236 + of sections 4 and 5, provided that you also convey the 237 + machine-readable Corresponding Source under the terms of this License, 238 + in one of these ways: 239 + 240 + a) Convey the object code in, or embodied in, a physical product 241 + (including a physical distribution medium), accompanied by the 242 + Corresponding Source fixed on a durable physical medium 243 + customarily used for software interchange. 244 + 245 + b) Convey the object code in, or embodied in, a physical product 246 + (including a physical distribution medium), accompanied by a 247 + written offer, valid for at least three years and valid for as 248 + long as you offer spare parts or customer support for that product 249 + model, to give anyone who possesses the object code either (1) a 250 + copy of the Corresponding Source for all the software in the 251 + product that is covered by this License, on a durable physical 252 + medium customarily used for software interchange, for a price no 253 + more than your reasonable cost of physically performing this 254 + conveying of source, or (2) access to copy the 255 + Corresponding Source from a network server at no charge. 256 + 257 + c) Convey individual copies of the object code with a copy of the 258 + written offer to provide the Corresponding Source. This 259 + alternative is allowed only occasionally and noncommercially, and 260 + only if you received the object code with such an offer, in accord 261 + with subsection 6b. 262 + 263 + d) Convey the object code by offering access from a designated 264 + place (gratis or for a charge), and offer equivalent access to the 265 + Corresponding Source in the same way through the same place at no 266 + further charge. You need not require recipients to copy the 267 + Corresponding Source along with the object code. If the place to 268 + copy the object code is a network server, the Corresponding Source 269 + may be on a different server (operated by you or a third party) 270 + that supports equivalent copying facilities, provided you maintain 271 + clear directions next to the object code saying where to find the 272 + Corresponding Source. Regardless of what server hosts the 273 + Corresponding Source, you remain obligated to ensure that it is 274 + available for as long as needed to satisfy these requirements. 275 + 276 + e) Convey the object code using peer-to-peer transmission, provided 277 + you inform other peers where the object code and Corresponding 278 + Source of the work are being offered to the general public at no 279 + charge under subsection 6d. 280 + 281 + A separable portion of the object code, whose source code is excluded 282 + from the Corresponding Source as a System Library, need not be 283 + included in conveying the object code work. 284 + 285 + A "User Product" is either (1) a "consumer product", which means any 286 + tangible personal property which is normally used for personal, family, 287 + or household purposes, or (2) anything designed or sold for incorporation 288 + into a dwelling. In determining whether a product is a consumer product, 289 + doubtful cases shall be resolved in favor of coverage. For a particular 290 + product received by a particular user, "normally used" refers to a 291 + typical or common use of that class of product, regardless of the status 292 + of the particular user or of the way in which the particular user 293 + actually uses, or expects or is expected to use, the product. A product 294 + is a consumer product regardless of whether the product has substantial 295 + commercial, industrial or non-consumer uses, unless such uses represent 296 + the only significant mode of use of the product. 297 + 298 + "Installation Information" for a User Product means any methods, 299 + procedures, authorization keys, or other information required to install 300 + and execute modified versions of a covered work in that User Product from 301 + a modified version of its Corresponding Source. The information must 302 + suffice to ensure that the continued functioning of the modified object 303 + code is in no case prevented or interfered with solely because 304 + modification has been made. 305 + 306 + If you convey an object code work under this section in, or with, or 307 + specifically for use in, a User Product, and the conveying occurs as 308 + part of a transaction in which the right of possession and use of the 309 + User Product is transferred to the recipient in perpetuity or for a 310 + fixed term (regardless of how the transaction is characterized), the 311 + Corresponding Source conveyed under this section must be accompanied 312 + by the Installation Information. But this requirement does not apply 313 + if neither you nor any third party retains the ability to install 314 + modified object code on the User Product (for example, the work has 315 + been installed in ROM). 316 + 317 + The requirement to provide Installation Information does not include a 318 + requirement to continue to provide support service, warranty, or updates 319 + for a work that has been modified or installed by the recipient, or for 320 + the User Product in which it has been modified or installed. Access to a 321 + network may be denied when the modification itself materially and 322 + adversely affects the operation of the network or violates the rules and 323 + protocols for communication across the network. 324 + 325 + Corresponding Source conveyed, and Installation Information provided, 326 + in accord with this section must be in a format that is publicly 327 + documented (and with an implementation available to the public in 328 + source code form), and must require no special password or key for 329 + unpacking, reading or copying. 330 + 331 + 7. Additional Terms. 332 + 333 + "Additional permissions" are terms that supplement the terms of this 334 + License by making exceptions from one or more of its conditions. 335 + Additional permissions that are applicable to the entire Program shall 336 + be treated as though they were included in this License, to the extent 337 + that they are valid under applicable law. If additional permissions 338 + apply only to part of the Program, that part may be used separately 339 + under those permissions, but the entire Program remains governed by 340 + this License without regard to the additional permissions. 341 + 342 + When you convey a copy of a covered work, you may at your option 343 + remove any additional permissions from that copy, or from any part of 344 + it. (Additional permissions may be written to require their own 345 + removal in certain cases when you modify the work.) You may place 346 + additional permissions on material, added by you to a covered work, 347 + for which you have or can give appropriate copyright permission. 348 + 349 + Notwithstanding any other provision of this License, for material you 350 + add to a covered work, you may (if authorized by the copyright holders of 351 + that material) supplement the terms of this License with terms: 352 + 353 + a) Disclaiming warranty or limiting liability differently from the 354 + terms of sections 15 and 16 of this License; or 355 + 356 + b) Requiring preservation of specified reasonable legal notices or 357 + author attributions in that material or in the Appropriate Legal 358 + Notices displayed by works containing it; or 359 + 360 + c) Prohibiting misrepresentation of the origin of that material, or 361 + requiring that modified versions of such material be marked in 362 + reasonable ways as different from the original version; or 363 + 364 + d) Limiting the use for publicity purposes of names of licensors or 365 + authors of the material; or 366 + 367 + e) Declining to grant rights under trademark law for use of some 368 + trade names, trademarks, or service marks; or 369 + 370 + f) Requiring indemnification of licensors and authors of that 371 + material by anyone who conveys the material (or modified versions of 372 + it) with contractual assumptions of liability to the recipient, for 373 + any liability that these contractual assumptions directly impose on 374 + those licensors and authors. 375 + 376 + All other non-permissive additional terms are considered "further 377 + restrictions" within the meaning of section 10. If the Program as you 378 + received it, or any part of it, contains a notice stating that it is 379 + governed by this License along with a term that is a further 380 + restriction, you may remove that term. If a license document contains 381 + a further restriction but permits relicensing or conveying under this 382 + License, you may add to a covered work material governed by the terms 383 + of that license document, provided that the further restriction does 384 + not survive such relicensing or conveying. 385 + 386 + If you add terms to a covered work in accord with this section, you 387 + must place, in the relevant source files, a statement of the 388 + additional terms that apply to those files, or a notice indicating 389 + where to find the applicable terms. 390 + 391 + Additional terms, permissive or non-permissive, may be stated in the 392 + form of a separately written license, or stated as exceptions; 393 + the above requirements apply either way. 394 + 395 + 8. Termination. 396 + 397 + You may not propagate or modify a covered work except as expressly 398 + provided under this License. Any attempt otherwise to propagate or 399 + modify it is void, and will automatically terminate your rights under 400 + this License (including any patent licenses granted under the third 401 + paragraph of section 11). 402 + 403 + However, if you cease all violation of this License, then your 404 + license from a particular copyright holder is reinstated (a) 405 + provisionally, unless and until the copyright holder explicitly and 406 + finally terminates your license, and (b) permanently, if the copyright 407 + holder fails to notify you of the violation by some reasonable means 408 + prior to 60 days after the cessation. 409 + 410 + Moreover, your license from a particular copyright holder is 411 + reinstated permanently if the copyright holder notifies you of the 412 + violation by some reasonable means, this is the first time you have 413 + received notice of violation of this License (for any work) from that 414 + copyright holder, and you cure the violation prior to 30 days after 415 + your receipt of the notice. 416 + 417 + Termination of your rights under this section does not terminate the 418 + licenses of parties who have received copies or rights from you under 419 + this License. If your rights have been terminated and not permanently 420 + reinstated, you do not qualify to receive new licenses for the same 421 + material under section 10. 422 + 423 + 9. Acceptance Not Required for Having Copies. 424 + 425 + You are not required to accept this License in order to receive or 426 + run a copy of the Program. Ancillary propagation of a covered work 427 + occurring solely as a consequence of using peer-to-peer transmission 428 + to receive a copy likewise does not require acceptance. However, 429 + nothing other than this License grants you permission to propagate or 430 + modify any covered work. These actions infringe copyright if you do 431 + not accept this License. Therefore, by modifying or propagating a 432 + covered work, you indicate your acceptance of this License to do so. 433 + 434 + 10. Automatic Licensing of Downstream Recipients. 435 + 436 + Each time you convey a covered work, the recipient automatically 437 + receives a license from the original licensors, to run, modify and 438 + propagate that work, subject to this License. You are not responsible 439 + for enforcing compliance by third parties with this License. 440 + 441 + An "entity transaction" is a transaction transferring control of an 442 + organization, or substantially all assets of one, or subdividing an 443 + organization, or merging organizations. If propagation of a covered 444 + work results from an entity transaction, each party to that 445 + transaction who receives a copy of the work also receives whatever 446 + licenses to the work the party's predecessor in interest had or could 447 + give under the previous paragraph, plus a right to possession of the 448 + Corresponding Source of the work from the predecessor in interest, if 449 + the predecessor has it or can get it with reasonable efforts. 450 + 451 + You may not impose any further restrictions on the exercise of the 452 + rights granted or affirmed under this License. For example, you may 453 + not impose a license fee, royalty, or other charge for exercise of 454 + rights granted under this License, and you may not initiate litigation 455 + (including a cross-claim or counterclaim in a lawsuit) alleging that 456 + any patent claim is infringed by making, using, selling, offering for 457 + sale, or importing the Program or any portion of it. 458 + 459 + 11. Patents. 460 + 461 + A "contributor" is a copyright holder who authorizes use under this 462 + License of the Program or a work on which the Program is based. The 463 + work thus licensed is called the contributor's "contributor version". 464 + 465 + A contributor's "essential patent claims" are all patent claims 466 + owned or controlled by the contributor, whether already acquired or 467 + hereafter acquired, that would be infringed by some manner, permitted 468 + by this License, of making, using, or selling its contributor version, 469 + but do not include claims that would be infringed only as a 470 + consequence of further modification of the contributor version. For 471 + purposes of this definition, "control" includes the right to grant 472 + patent sublicenses in a manner consistent with the requirements of 473 + this License. 474 + 475 + Each contributor grants you a non-exclusive, worldwide, royalty-free 476 + patent license under the contributor's essential patent claims, to 477 + make, use, sell, offer for sale, import and otherwise run, modify and 478 + propagate the contents of its contributor version. 479 + 480 + In the following three paragraphs, a "patent license" is any express 481 + agreement or commitment, however denominated, not to enforce a patent 482 + (such as an express permission to practice a patent or covenant not to 483 + sue for patent infringement). To "grant" such a patent license to a 484 + party means to make such an agreement or commitment not to enforce a 485 + patent against the party. 486 + 487 + If you convey a covered work, knowingly relying on a patent license, 488 + and the Corresponding Source of the work is not available for anyone 489 + to copy, free of charge and under the terms of this License, through a 490 + publicly available network server or other readily accessible means, 491 + then you must either (1) cause the Corresponding Source to be so 492 + available, or (2) arrange to deprive yourself of the benefit of the 493 + patent license for this particular work, or (3) arrange, in a manner 494 + consistent with the requirements of this License, to extend the patent 495 + license to downstream recipients. "Knowingly relying" means you have 496 + actual knowledge that, but for the patent license, your conveying the 497 + covered work in a country, or your recipient's use of the covered work 498 + in a country, would infringe one or more identifiable patents in that 499 + country that you have reason to believe are valid. 500 + 501 + If, pursuant to or in connection with a single transaction or 502 + arrangement, you convey, or propagate by procuring conveyance of, a 503 + covered work, and grant a patent license to some of the parties 504 + receiving the covered work authorizing them to use, propagate, modify 505 + or convey a specific copy of the covered work, then the patent license 506 + you grant is automatically extended to all recipients of the covered 507 + work and works based on it. 508 + 509 + A patent license is "discriminatory" if it does not include within 510 + the scope of its coverage, prohibits the exercise of, or is 511 + conditioned on the non-exercise of one or more of the rights that are 512 + specifically granted under this License. You may not convey a covered 513 + work if you are a party to an arrangement with a third party that is 514 + in the business of distributing software, under which you make payment 515 + to the third party based on the extent of your activity of conveying 516 + the work, and under which the third party grants, to any of the 517 + parties who would receive the covered work from you, a discriminatory 518 + patent license (a) in connection with copies of the covered work 519 + conveyed by you (or copies made from those copies), or (b) primarily 520 + for and in connection with specific products or compilations that 521 + contain the covered work, unless you entered into that arrangement, 522 + or that patent license was granted, prior to 28 March 2007. 523 + 524 + Nothing in this License shall be construed as excluding or limiting 525 + any implied license or other defenses to infringement that may 526 + otherwise be available to you under applicable patent law. 527 + 528 + 12. No Surrender of Others' Freedom. 529 + 530 + If conditions are imposed on you (whether by court order, agreement or 531 + otherwise) that contradict the conditions of this License, they do not 532 + excuse you from the conditions of this License. If you cannot convey a 533 + covered work so as to satisfy simultaneously your obligations under this 534 + License and any other pertinent obligations, then as a consequence you may 535 + not convey it at all. For example, if you agree to terms that obligate you 536 + to collect a royalty for further conveying from those to whom you convey 537 + the Program, the only way you could satisfy both those terms and this 538 + License would be to refrain entirely from conveying the Program. 539 + 540 + 13. Remote Network Interaction; Use with the GNU General Public License. 541 + 542 + Notwithstanding any other provision of this License, if you modify the 543 + Program, your modified version must prominently offer all users 544 + interacting with it remotely through a computer network (if your version 545 + supports such interaction) an opportunity to receive the Corresponding 546 + Source of your version by providing access to the Corresponding Source 547 + from a network server at no charge, through some standard or customary 548 + means of facilitating copying of software. This Corresponding Source 549 + shall include the Corresponding Source for any work covered by version 3 550 + of the GNU General Public License that is incorporated pursuant to the 551 + following paragraph. 552 + 553 + Notwithstanding any other provision of this License, you have 554 + permission to link or combine any covered work with a work licensed 555 + under version 3 of the GNU General Public License into a single 556 + combined work, and to convey the resulting work. The terms of this 557 + License will continue to apply to the part which is the covered work, 558 + but the work with which it is combined will remain governed by version 559 + 3 of the GNU General Public License. 560 + 561 + 14. Revised Versions of this License. 562 + 563 + The Free Software Foundation may publish revised and/or new versions of 564 + the GNU Affero General Public License from time to time. Such new versions 565 + will be similar in spirit to the present version, but may differ in detail to 566 + address new problems or concerns. 567 + 568 + Each version is given a distinguishing version number. If the 569 + Program specifies that a certain numbered version of the GNU Affero General 570 + Public License "or any later version" applies to it, you have the 571 + option of following the terms and conditions either of that numbered 572 + version or of any later version published by the Free Software 573 + Foundation. If the Program does not specify a version number of the 574 + GNU Affero General Public License, you may choose any version ever published 575 + by the Free Software Foundation. 576 + 577 + If the Program specifies that a proxy can decide which future 578 + versions of the GNU Affero General Public License can be used, that proxy's 579 + public statement of acceptance of a version permanently authorizes you 580 + to choose that version for the Program. 581 + 582 + Later license versions may give you additional or different 583 + permissions. However, no additional obligations are imposed on any 584 + author or copyright holder as a result of your choosing to follow a 585 + later version. 586 + 587 + 15. Disclaimer of Warranty. 588 + 589 + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 + APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 + HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 + OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 + THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 + PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 + IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 + ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 + 598 + 16. Limitation of Liability. 599 + 600 + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 + WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 + THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 + GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 + USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 + DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 + PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 + EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 + SUCH DAMAGES. 609 + 610 + 17. Interpretation of Sections 15 and 16. 611 + 612 + If the disclaimer of warranty and limitation of liability provided 613 + above cannot be given local legal effect according to their terms, 614 + reviewing courts shall apply local law that most closely approximates 615 + an absolute waiver of all civil liability in connection with the 616 + Program, unless a warranty or assumption of liability accompanies a 617 + copy of the Program in return for a fee. 618 + 619 + END OF TERMS AND CONDITIONS 620 + 621 + How to Apply These Terms to Your New Programs 622 + 623 + If you develop a new program, and you want it to be of the greatest 624 + possible use to the public, the best way to achieve this is to make it 625 + free software which everyone can redistribute and change under these terms. 626 + 627 + To do so, attach the following notices to the program. It is safest 628 + to attach them to the start of each source file to most effectively 629 + state the exclusion of warranty; and each file should have at least 630 + the "copyright" line and a pointer to where the full notice is found. 631 + 632 + <one line to give the program's name and a brief idea of what it does.> 633 + Copyright (C) <year> <name of author> 634 + 635 + This program is free software: you can redistribute it and/or modify 636 + it under the terms of the GNU Affero General Public License as published 637 + by the Free Software Foundation, either version 3 of the License, or 638 + (at your option) any later version. 639 + 640 + This program is distributed in the hope that it will be useful, 641 + but WITHOUT ANY WARRANTY; without even the implied warranty of 642 + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 + GNU Affero General Public License for more details. 644 + 645 + You should have received a copy of the GNU Affero General Public License 646 + along with this program. If not, see <https://www.gnu.org/licenses/>. 647 + 648 + Also add information on how to contact you by electronic and paper mail. 649 + 650 + If your software can interact with users remotely through a computer 651 + network, you should also make sure that it provides a way for users to 652 + get its source. For example, if your program is a web application, its 653 + interface could display a "Source" link that leads users to an archive 654 + of the code. There are many ways you could offer source, and different 655 + solutions will be better for different programs; see section 13 for the 656 + specific requirements. 657 + 658 + You should also get your employer (if you work as a programmer) or school, 659 + if any, to sign a "copyright disclaimer" for the program, if necessary. 660 + For more information on this, and how to apply and follow the GNU AGPL, see 661 + <https://www.gnu.org/licenses/>.
+108
packages/svelte-standard-site/README.md
··· 1 + # @ewanc26/svelte-standard-site 2 + 3 + > **Canonical source:** This package is now maintained in the [`@ewanc26/pkgs`](https://github.com/ewanc26/pkgs) monorepo under [`packages/svelte-standard-site`](https://github.com/ewanc26/pkgs/tree/main/packages/svelte-standard-site). This copy exists for historical context — please open issues and PRs there. 4 + 5 + A SvelteKit library for reading and writing AT Protocol longform content via `site.standard.*` records. Includes a complete design system, publishing tools, federated Bluesky comments, content verification helpers, and pre-built components. 6 + 7 + Part of the [`@ewanc26/pkgs`](https://github.com/ewanc26/pkgs) monorepo. 8 + 9 + ## Installation 10 + 11 + ```bash 12 + pnpm add @ewanc26/svelte-standard-site zod 13 + ``` 14 + 15 + Requires `svelte >= 5` and `@sveltejs/kit >= 2` as peer dependencies. 16 + 17 + ## Features 18 + 19 + - **Reading** — Fetch `site.standard.document` and `site.standard.publication` records from AT Protocol 20 + - **Writing** — Publish and manage documents via `StandardSitePublisher` 21 + - **Comments** — Federated Bluesky replies as comments via the `<Comments />` component 22 + - **Verification** — `.well-known` endpoint helpers to prove content ownership 23 + - **Design system** — Semantic colour tokens (ink, canvas, primary, secondary, accent) with automatic light/dark mode via Tailwind CSS 4 24 + - **Type-safe** — Full TypeScript with Zod validation 25 + - **SSR-ready** — Works with SvelteKit's `fetch` for prerendering 26 + - **Caching** — In-memory cache with configurable TTL 27 + 28 + ## Quick Start 29 + 30 + ### Reading 31 + 32 + ```typescript 33 + // src/routes/+page.server.ts 34 + import { createClient } from '@ewanc26/svelte-standard-site'; 35 + 36 + export const load = async ({ fetch }) => { 37 + const client = createClient({ did: 'did:plc:your-did' }); 38 + const documents = await client.fetchAllDocuments(fetch); 39 + return { documents }; 40 + }; 41 + ``` 42 + 43 + ### Publishing 44 + 45 + ```typescript 46 + import { StandardSitePublisher } from '@ewanc26/svelte-standard-site/publisher'; 47 + 48 + const publisher = new StandardSitePublisher({ 49 + identifier: 'you.bsky.social', 50 + password: process.env.ATPROTO_APP_PASSWORD 51 + }); 52 + await publisher.login(); 53 + await publisher.publishDocument({ site, title, content, publishedAt }); 54 + ``` 55 + 56 + ### Comments 57 + 58 + ```svelte 59 + <script> 60 + import { Comments } from '@ewanc26/svelte-standard-site'; 61 + </script> 62 + 63 + <Comments 64 + bskyPostUri="at://did:plc:xxx/app.bsky.feed.post/abc" 65 + canonicalUrl="https://yourblog.com/posts/my-post" 66 + /> 67 + ``` 68 + 69 + ## Entry Points 70 + 71 + | Import | Description | 72 + |--------|-------------| 73 + | `@ewanc26/svelte-standard-site` | Components, client, stores, types, utilities | 74 + | `@ewanc26/svelte-standard-site/publisher` | `StandardSitePublisher` for writing records | 75 + | `@ewanc26/svelte-standard-site/content` | Markdown transformation utilities | 76 + | `@ewanc26/svelte-standard-site/comments` | Comment fetching utilities | 77 + | `@ewanc26/svelte-standard-site/verification` | `.well-known` and ownership verification | 78 + | `@ewanc26/svelte-standard-site/schemas` | Zod schemas and `COLLECTIONS` constant | 79 + | `@ewanc26/svelte-standard-site/config/env` | `getConfigFromEnv()` SvelteKit helper | 80 + | `@ewanc26/svelte-standard-site/styles/base.css` | Base CSS | 81 + | `@ewanc26/svelte-standard-site/styles/themes.css` | Theme CSS | 82 + 83 + ## Development 84 + 85 + Development happens in the [`@ewanc26/pkgs`](https://github.com/ewanc26/pkgs) monorepo. Local commands (from `packages/svelte-standard-site`): 86 + 87 + ```bash 88 + pnpm build # svelte-package → dist/ 89 + pnpm dev # svelte-package --watch 90 + pnpm dev:app # vite dev (demo routes) 91 + pnpm check # svelte-check 92 + pnpm test # vitest run 93 + ``` 94 + 95 + ## Environment Variables 96 + 97 + ```env 98 + PUBLIC_ATPROTO_DID=did:plc:your-did-here 99 + PUBLIC_PUBLICATION_RKEY=3abc123xyz 100 + 101 + # For publishing (never commit) 102 + ATPROTO_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx 103 + ATPROTO_HANDLE=you.bsky.social 104 + ``` 105 + 106 + ## Licence 107 + 108 + AGPL-3.0-only — see the [pkgs monorepo licence](https://github.com/ewanc26/pkgs/blob/main/LICENSE).
+489
packages/svelte-standard-site/docs/comments.md
··· 1 + # Federated Comments 2 + 3 + Display Bluesky replies as comments on your blog posts using the Comments component. 4 + 5 + ## How It Works 6 + 7 + 1. You publish a blog post 8 + 2. You share it on Bluesky (creating an "announcement post") 9 + 3. People reply to that Bluesky post 10 + 4. The Comments component fetches those replies and displays them as comments 11 + 12 + ## Quick Start 13 + 14 + ### 1. Install 15 + 16 + ```bash 17 + pnpm add svelte-standard-site 18 + ``` 19 + 20 + ### 2. Add to Your Blog Post 21 + 22 + ```svelte 23 + <script lang="ts"> 24 + import { Comments } from 'svelte-standard-site'; 25 + import type { PageData } from './$types'; 26 + 27 + const { data }: { data: PageData } = $props(); 28 + </script> 29 + 30 + <article> 31 + <h1>{data.post.title}</h1> 32 + {@html data.post.content} 33 + </article> 34 + 35 + {#if data.post.bskyPostUri} 36 + <Comments 37 + bskyPostUri={data.post.bskyPostUri} 38 + canonicalUrl="https://yourblog.com/posts/{data.post.slug}" 39 + /> 40 + {/if} 41 + ``` 42 + 43 + ### 3. Get the AT-URI 44 + 45 + When you share your post on Bluesky: 46 + 47 + 1. Click on your post 48 + 2. Click the "..." menu 49 + 3. Click "Copy post link" 50 + 4. Convert to AT-URI format 51 + 52 + ``` 53 + URL: https://bsky.app/profile/you.bsky.social/post/abc123xyz 54 + AT-URI: at://did:plc:YOUR_DID/app.bsky.feed.post/abc123xyz 55 + ``` 56 + 57 + ### 4. Store the AT-URI 58 + 59 + Add it to your post's frontmatter or database: 60 + 61 + ```yaml 62 + --- 63 + title: My Blog Post 64 + date: 2026-01-25 65 + bskyPostUri: at://did:plc:xxx/app.bsky.feed.post/abc123xyz 66 + --- 67 + ``` 68 + 69 + ## Component Props 70 + 71 + ```svelte 72 + <Comments 73 + bskyPostUri="at://..." // Required: AT-URI of announcement post 74 + canonicalUrl="https://..." // Required: URL of your blog post 75 + maxDepth={3} // Optional: Max reply nesting (default: 3) 76 + title="Comments" // Optional: Section heading 77 + showReplyLink={true} // Optional: Show "Reply on Bluesky" link 78 + class="my-custom-class" // Optional: Additional CSS classes 79 + /> 80 + ``` 81 + 82 + ## Workflow 83 + 84 + ### Complete Example 85 + 86 + 1. **Write and publish your blog post** 87 + 88 + ```typescript 89 + // scripts/publish-post.ts 90 + import { StandardSitePublisher } from 'svelte-standard-site/publisher'; 91 + 92 + const publisher = new StandardSitePublisher({ 93 + identifier: 'you.bsky.social', 94 + password: process.env.ATPROTO_APP_PASSWORD! 95 + }); 96 + 97 + await publisher.login(); 98 + 99 + const result = await publisher.publishDocument({ 100 + site: 'https://yourblog.com', 101 + title: 'Understanding ATProto', 102 + publishedAt: new Date().toISOString(), 103 + path: '/posts/understanding-atproto' 104 + // ... 105 + }); 106 + ``` 107 + 108 + 2. **Share on Bluesky** 109 + 110 + ```typescript 111 + // Create announcement post 112 + const agent = publisher.getAtpAgent(); 113 + 114 + const postResult = await agent.post({ 115 + text: `New blog post: Understanding ATProto 116 + 117 + Read it at: https://yourblog.com/posts/understanding-atproto`, 118 + langs: ['en'] 119 + }); 120 + 121 + console.log('Post URI:', postResult.uri); 122 + // Save this: at://did:plc:xxx/app.bsky.feed.post/abc123 123 + ``` 124 + 125 + 3. **Update your post with the AT-URI** 126 + 127 + ```typescript 128 + await publisher.updateDocument(rkey, { 129 + // ... all original fields 130 + bskyPostRef: { 131 + uri: postResult.uri, 132 + cid: postResult.cid 133 + } 134 + }); 135 + ``` 136 + 137 + 4. **Comments appear automatically** 138 + 139 + The Comments component fetches replies from Bluesky when users visit your post. 140 + 141 + ## Programmatic Usage 142 + 143 + If you want to fetch comments in your load function instead of client-side: 144 + 145 + ```typescript 146 + // src/routes/blog/[slug]/+page.server.ts 147 + import { fetchComments } from 'svelte-standard-site/comments'; 148 + import type { PageServerLoad } from './$types'; 149 + 150 + export const load: PageServerLoad = async ({ params }) => { 151 + const post = await getPost(params.slug); // Your database/CMS 152 + 153 + let comments = []; 154 + if (post.bskyPostUri) { 155 + comments = await fetchComments({ 156 + bskyPostUri: post.bskyPostUri, 157 + canonicalUrl: `https://yourblog.com/blog/${params.slug}`, 158 + maxDepth: 3 159 + }); 160 + } 161 + 162 + return { 163 + post, 164 + comments 165 + }; 166 + }; 167 + ``` 168 + 169 + Then render them manually: 170 + 171 + ```svelte 172 + <script lang="ts"> 173 + import type { PageData } from './$types'; 174 + 175 + const { data }: { data: PageData } = $props(); 176 + </script> 177 + 178 + <div class="comments"> 179 + {#each data.comments as comment} 180 + <div class="comment"> 181 + <img src={comment.author.avatar} alt={comment.author.handle} /> 182 + <p>{comment.text}</p> 183 + </div> 184 + {/each} 185 + </div> 186 + ``` 187 + 188 + ## Functions 189 + 190 + ### fetchComments 191 + 192 + ```typescript 193 + import { fetchComments } from 'svelte-standard-site/comments'; 194 + 195 + const comments = await fetchComments({ 196 + bskyPostUri: 'at://did:plc:xxx/app.bsky.feed.post/abc123', 197 + canonicalUrl: 'https://yourblog.com/posts/my-post', 198 + maxDepth: 3 199 + }); 200 + 201 + // Returns array of Comment objects 202 + ``` 203 + 204 + ### fetchMentionComments 205 + 206 + Fetch posts that mention your blog post URL (even if not replies): 207 + 208 + ```typescript 209 + import { fetchMentionComments } from 'svelte-standard-site/comments'; 210 + 211 + const mentions = await fetchMentionComments('https://yourblog.com/posts/my-post', 3); 212 + ``` 213 + 214 + ### formatRelativeTime 215 + 216 + ```typescript 217 + import { formatRelativeTime } from 'svelte-standard-site/comments'; 218 + 219 + formatRelativeTime('2026-01-25T10:00:00Z'); 220 + // "2 hours ago" 221 + ``` 222 + 223 + ## Types 224 + 225 + ```typescript 226 + interface Comment { 227 + uri: string; // AT-URI of the reply 228 + cid: string; // Content hash 229 + author: CommentAuthor; 230 + text: string; // Comment text 231 + createdAt: string; // ISO date 232 + likeCount: number; 233 + replyCount: number; 234 + replies?: Comment[]; // Nested replies 235 + depth: number; // Nesting level (0 = top-level) 236 + } 237 + 238 + interface CommentAuthor { 239 + did: string; 240 + handle: string; 241 + displayName?: string; 242 + avatar?: string; 243 + } 244 + ``` 245 + 246 + ## Styling 247 + 248 + The Comments component uses your site's design system classes. You can customize: 249 + 250 + ```svelte 251 + <Comments 252 + {bskyPostUri} 253 + {canonicalUrl} 254 + class="my-12 rounded-xl border-2 p-6" 255 + /> 256 + 257 + <style> 258 + :global(.comments-section) { 259 + /* Custom styles */ 260 + } 261 + </style> 262 + ``` 263 + 264 + ## Advanced Usage 265 + 266 + ### Custom Comment Renderer 267 + 268 + Build your own comment UI: 269 + 270 + ```svelte 271 + <script lang="ts"> 272 + import { fetchComments, formatRelativeTime } from 'svelte-standard-site/comments'; 273 + import { onMount } from 'svelte'; 274 + 275 + let comments = $state([]); 276 + 277 + onMount(async () => { 278 + comments = await fetchComments({ 279 + bskyPostUri: 'at://...', 280 + canonicalUrl: 'https://...' 281 + }); 282 + }); 283 + </script> 284 + 285 + <div class="comments"> 286 + {#each comments as comment} 287 + <article> 288 + <header> 289 + <a href="https://bsky.app/profile/{comment.author.handle}"> 290 + {comment.author.displayName || comment.author.handle} 291 + </a> 292 + <time>{formatRelativeTime(comment.createdAt)}</time> 293 + </header> 294 + 295 + <p>{comment.text}</p> 296 + 297 + {#if comment.replies} 298 + <!-- Recursively render replies --> 299 + {#each comment.replies as reply} 300 + <!-- ... --> 301 + {/each} 302 + {/if} 303 + </article> 304 + {/each} 305 + </div> 306 + ``` 307 + 308 + ### Combine with Mentions 309 + 310 + Show both replies and mentions: 311 + 312 + ```typescript 313 + const [replies, mentions] = await Promise.all([ 314 + fetchComments({ 315 + bskyPostUri: post.bskyPostUri, 316 + canonicalUrl: post.url 317 + }), 318 + fetchMentionComments(post.url) 319 + ]); 320 + 321 + const allComments = [...replies, ...mentions]; 322 + ``` 323 + 324 + ### Filter by Language 325 + 326 + ```typescript 327 + const comments = await fetchComments({ 328 + bskyPostUri, 329 + canonicalUrl 330 + }); 331 + 332 + const englishComments = comments.filter((c) => { 333 + // You'd need to add language detection 334 + return detectLanguage(c.text) === 'en'; 335 + }); 336 + ``` 337 + 338 + ### Moderation 339 + 340 + Since these are from Bluesky, you can use their moderation tools: 341 + 342 + ```typescript 343 + const comments = await fetchComments({ 344 + bskyPostUri, 345 + canonicalUrl 346 + }); 347 + 348 + // Filter out blocked users 349 + const moderated = comments.filter((c) => { 350 + return !isUserBlocked(c.author.did); 351 + }); 352 + ``` 353 + 354 + ## Best Practices 355 + 356 + 1. **Always include canonical URL** - Helps with mention detection 357 + 2. **Set appropriate maxDepth** - Too deep can be overwhelming (3 is good) 358 + 3. **Show "Reply on Bluesky" link** - Encourages engagement 359 + 4. **Handle loading states** - Comments load async 360 + 5. **Cache on server** - Fetch in load() for better performance 361 + 6. **Respect privacy** - Remember these are public Bluesky posts 362 + 7. **Test thoroughly** - Ensure AT-URI is correct 363 + 364 + ## Troubleshooting 365 + 366 + ### Comments Not Loading 367 + 368 + 1. **Check the AT-URI format** 369 + ``` 370 + ✅ at://did:plc:xxx/app.bsky.feed.post/abc123 371 + ❌ https://bsky.app/profile/you.bsky.social/post/abc123 372 + ``` 373 + 2. **Verify the post exists** - Visit it on bsky.app 374 + 3. **Check console** - Look for error messages 375 + 4. **Ensure post is public** - Private posts won't be accessible 376 + 377 + ### Wrong Comments Showing 378 + 379 + - Double-check the AT-URI 380 + - Make sure you're using the announcement post URI, not a reply URI 381 + 382 + ### Missing Nested Replies 383 + 384 + - Increase `maxDepth` prop 385 + - Check if replies are actually nested (some clients flatten threads) 386 + 387 + ### Performance Issues 388 + 389 + - Fetch comments server-side in `load()` 390 + - Implement pagination for posts with many comments 391 + - Cache results 392 + 393 + ## Static Sites 394 + 395 + For static sites (using adapter-static): 396 + 397 + 1. **Pre-build comments** 398 + 399 + ```typescript 400 + // scripts/prebuild-comments.ts 401 + const posts = await getAllPosts(); 402 + 403 + for (const post of posts) { 404 + if (post.bskyPostUri) { 405 + const comments = await fetchComments({ 406 + bskyPostUri: post.bskyPostUri, 407 + canonicalUrl: post.url 408 + }); 409 + 410 + fs.writeFileSync(`static/comments/${post.slug}.json`, JSON.stringify(comments)); 411 + } 412 + } 413 + ``` 414 + 415 + 2. **Load from static file** 416 + 417 + ```typescript 418 + // +page.server.ts 419 + export const load = async ({ params }) => { 420 + const comments = JSON.parse(fs.readFileSync(`static/comments/${params.slug}.json`, 'utf-8')); 421 + 422 + return { comments }; 423 + }; 424 + ``` 425 + 426 + 3. **Rebuild on schedule** - Use GitHub Actions or similar to rebuild daily/weekly 427 + 428 + ## Examples 429 + 430 + ### Basic Blog Post 431 + 432 + ```svelte 433 + <script lang="ts"> 434 + import { Comments } from 'svelte-standard-site'; 435 + 436 + const post = { 437 + title: 'My Post', 438 + content: '...', 439 + bskyPostUri: 'at://did:plc:xxx/app.bsky.feed.post/abc123' 440 + }; 441 + </script> 442 + 443 + <article> 444 + <h1>{post.title}</h1> 445 + {@html post.content} 446 + </article> 447 + 448 + <Comments 449 + bskyPostUri={post.bskyPostUri} 450 + canonicalUrl="https://yourblog.com/posts/my-post" 451 + /> 452 + ``` 453 + 454 + ### With Loading State 455 + 456 + ```svelte 457 + <script lang="ts"> 458 + import { Comments } from 'svelte-standard-site'; 459 + import { page } from '$app/stores'; 460 + 461 + const { data } = $props(); 462 + 463 + let commentsLoaded = $state(false); 464 + </script> 465 + 466 + <article> 467 + <!-- Post content --> 468 + </article> 469 + 470 + {#if data.post.bskyPostUri} 471 + <div class="comments-wrapper"> 472 + {#if !commentsLoaded} 473 + <div class="loading">Loading comments...</div> 474 + {/if} 475 + 476 + <Comments 477 + bskyPostUri={data.post.bskyPostUri} 478 + canonicalUrl={$page.url.href} 479 + on:load={() => (commentsLoaded = true)} 480 + /> 481 + </div> 482 + {/if} 483 + ``` 484 + 485 + ## Next Steps 486 + 487 + - [Publishing](./publishing.md) 488 + - [Content Transformation](./content-transformation.md) 489 + - [Verification](./verification.md)
+363
packages/svelte-standard-site/docs/content-transformation.md
··· 1 + # Content Transformation 2 + 3 + Content transformation utilities convert your markdown content into formats suitable for ATProto publishing. 4 + 5 + ## Why Transform Content? 6 + 7 + When publishing to ATProto, you need to: 8 + 9 + 1. **Convert sidenotes** - HTML sidenotes → markdown blockquotes 10 + 2. **Resolve links** - Relative URLs → absolute URLs 11 + 3. **Extract plain text** - For search indexing (`textContent` field) 12 + 4. **Calculate metadata** - Word count and reading time 13 + 14 + ## Quick Start 15 + 16 + ```typescript 17 + import { transformContent } from 'svelte-standard-site/content'; 18 + 19 + const markdown = ` 20 + # My Blog Post 21 + 22 + This is [a link](/about) with some content. 23 + 24 + <div class="sidenote"> 25 + <span class="sidenote-label">Tip</span> 26 + <p>This is helpful information</p> 27 + </div> 28 + `; 29 + 30 + const result = transformContent(markdown, { 31 + baseUrl: 'https://yourblog.com' 32 + }); 33 + 34 + // result.markdown - Clean markdown for ATProto 35 + // result.textContent - Plain text for search 36 + // result.wordCount - Number of words 37 + // result.readingTime - Estimated minutes to read 38 + ``` 39 + 40 + ## Individual Functions 41 + 42 + ### Convert Sidenotes 43 + 44 + Transform HTML sidenotes into markdown blockquotes: 45 + 46 + ```typescript 47 + import { convertSidenotes, convertComplexSidenotes } from 'svelte-standard-site/content'; 48 + 49 + const input = ` 50 + <div class="sidenote sidenote--tip"> 51 + <span class="sidenote-label">Tip</span> 52 + <p>This is a helpful tip</p> 53 + </div> 54 + `; 55 + 56 + const output = convertSidenotes(input); 57 + // > **Tip:** This is a helpful tip 58 + 59 + // For complex sidenotes with multiple paragraphs: 60 + const complex = convertComplexSidenotes(input); 61 + ``` 62 + 63 + ### Resolve Relative Links 64 + 65 + Convert relative URLs to absolute: 66 + 67 + ```typescript 68 + import { resolveRelativeLinks } from 'svelte-standard-site/content'; 69 + 70 + const input = ` 71 + [About page](/about) 72 + ![Image](/images/photo.jpg) 73 + `; 74 + 75 + const output = resolveRelativeLinks(input, 'https://yourblog.com'); 76 + // [About page](https://yourblog.com/about) 77 + // ![Image](https://yourblog.com/images/photo.jpg) 78 + ``` 79 + 80 + ### Strip to Plain Text 81 + 82 + Extract plain text from markdown: 83 + 84 + ```typescript 85 + import { stripToPlainText } from 'svelte-standard-site/content'; 86 + 87 + const markdown = ` 88 + # Heading 89 + 90 + This is **bold** and *italic*. 91 + 92 + [Link](https://example.com) 93 + `; 94 + 95 + const plain = stripToPlainText(markdown); 96 + // Heading 97 + // This is bold and italic. 98 + // Link 99 + ``` 100 + 101 + ### Calculate Metadata 102 + 103 + ```typescript 104 + import { countWords, calculateReadingTime } from 'svelte-standard-site/content'; 105 + 106 + const text = 'Your blog post content here...'; 107 + const words = countWords(text); // 42 108 + const minutes = calculateReadingTime(words); // 1 (assumes 200 wpm) 109 + 110 + // Custom reading speed 111 + const slowRead = calculateReadingTime(words, 150); // Slower pace 112 + ``` 113 + 114 + ## Complete Pipeline 115 + 116 + The `transformContent` function runs all transformations: 117 + 118 + ```typescript 119 + import { transformContent } from 'svelte-standard-site/content'; 120 + 121 + const result = transformContent(rawMarkdown, { 122 + baseUrl: 'https://yourblog.com', 123 + postPath: '/blog/my-post' // Optional 124 + }); 125 + 126 + // Use in publisher 127 + await publisher.publishDocument({ 128 + site: 'https://yourblog.com', 129 + title: 'My Post', 130 + publishedAt: new Date().toISOString(), 131 + content: { 132 + $type: 'site.standard.content.markdown', 133 + text: result.markdown, 134 + version: '1.0' 135 + }, 136 + textContent: result.textContent 137 + }); 138 + ``` 139 + 140 + ## Use Cases 141 + 142 + ### Publishing from Markdown Files 143 + 144 + ```typescript 145 + import fs from 'fs'; 146 + import matter from 'gray-matter'; 147 + import { transformContent } from 'svelte-standard-site/content'; 148 + 149 + const file = fs.readFileSync('./posts/my-post.md', 'utf-8'); 150 + const { data, content } = matter(file); 151 + 152 + const transformed = transformContent(content, { 153 + baseUrl: 'https://yourblog.com' 154 + }); 155 + 156 + // Now publish... 157 + ``` 158 + 159 + ### SvelteKit Form Actions 160 + 161 + ```typescript 162 + // src/routes/admin/publish/+page.server.ts 163 + import { transformContent } from 'svelte-standard-site/content'; 164 + import type { Actions } from './$types'; 165 + 166 + export const actions = { 167 + publish: async ({ request }) => { 168 + const formData = await request.formData(); 169 + const markdown = formData.get('content') as string; 170 + 171 + const transformed = transformContent(markdown, { 172 + baseUrl: 'https://yourblog.com' 173 + }); 174 + 175 + // Publish using transformed content 176 + } 177 + } satisfies Actions; 178 + ``` 179 + 180 + ### Preview with Metadata 181 + 182 + ```svelte 183 + <script lang="ts"> 184 + import { transformContent } from 'svelte-standard-site/content'; 185 + 186 + let markdown = $state(''); 187 + let preview = $derived( 188 + transformContent(markdown, { 189 + baseUrl: 'https://yourblog.com' 190 + }) 191 + ); 192 + </script> 193 + 194 + <div> 195 + <textarea bind:value={markdown} /> 196 + 197 + <div class="stats"> 198 + <p>Words: {preview.wordCount}</p> 199 + <p>Reading time: {preview.readingTime} min</p> 200 + </div> 201 + 202 + <div class="preview"> 203 + {@html marked(preview.markdown)} 204 + </div> 205 + </div> 206 + ``` 207 + 208 + ## Advanced Examples 209 + 210 + ### Custom Sidenote Formats 211 + 212 + If you have custom sidenote HTML, create your own converter: 213 + 214 + ```typescript 215 + function convertCustomSidenotes(markdown: string): string { 216 + const regex = /<aside class="note">([\s\S]*?)<\/aside>/gi; 217 + 218 + return markdown.replace(regex, (match, content) => { 219 + const clean = content.replace(/<[^>]+>/g, '').trim(); 220 + return `\n> ${clean}\n`; 221 + }); 222 + } 223 + 224 + // Use in pipeline 225 + import { resolveRelativeLinks, stripToPlainText } from 'svelte-standard-site/content'; 226 + 227 + let transformed = convertCustomSidenotes(markdown); 228 + transformed = resolveRelativeLinks(transformed, baseUrl); 229 + const textContent = stripToPlainText(transformed); 230 + ``` 231 + 232 + ### Preserve Certain HTML 233 + 234 + If you want to keep some HTML in the markdown: 235 + 236 + ```typescript 237 + function stripToPlainTextPreserveCode(markdown: string): string { 238 + // Extract code blocks first 239 + const codeBlocks: string[] = []; 240 + let text = markdown.replace(/```[\s\S]*?```/g, (match) => { 241 + codeBlocks.push(match); 242 + return `__CODE_BLOCK_${codeBlocks.length - 1}__`; 243 + }); 244 + 245 + // Strip other markdown 246 + text = stripToPlainText(text); 247 + 248 + // Restore code blocks 249 + text = text.replace(/__CODE_BLOCK_(\d+)__/g, (_, index) => codeBlocks[parseInt(index)]); 250 + 251 + return text; 252 + } 253 + ``` 254 + 255 + ### Image Alt Text Extraction 256 + 257 + Extract all image alt text for accessibility metadata: 258 + 259 + ```typescript 260 + function extractImageAltText(markdown: string): string[] { 261 + const regex = /!\[([^\]]*)\]/g; 262 + const matches = []; 263 + let match; 264 + 265 + while ((match = regex.exec(markdown)) !== null) { 266 + if (match[1]) { 267 + matches.push(match[1]); 268 + } 269 + } 270 + 271 + return matches; 272 + } 273 + 274 + const altTexts = extractImageAltText(markdown); 275 + // ['Photo of sunset', 'Diagram showing architecture'] 276 + ``` 277 + 278 + ## Best Practices 279 + 280 + 1. **Always transform before publishing** - Don't skip transformation 281 + 2. **Include textContent** - Essential for search and accessibility 282 + 3. **Use absolute URLs** - Prevents broken links on other platforms 283 + 4. **Test transformations** - Write tests for custom sidenotes 284 + 5. **Validate output** - Ensure markdown is valid before publishing 285 + 6. **Consider locale** - If using date formatting, respect user locale 286 + 287 + ## Common Issues 288 + 289 + ### Sidenotes Not Converting 290 + 291 + Make sure the HTML structure matches exactly: 292 + 293 + ```html 294 + <!-- This works --> 295 + <div class="sidenote"> 296 + <span class="sidenote-label">Note</span> 297 + <p>Content</p> 298 + </div> 299 + 300 + <!-- This won't work (missing class) --> 301 + <div> 302 + <span>Note</span> 303 + <p>Content</p> 304 + </div> 305 + ``` 306 + 307 + ### Links Not Resolving 308 + 309 + Ensure you're passing the base URL correctly: 310 + 311 + ```typescript 312 + // ❌ Wrong - missing protocol 313 + transformContent(md, { baseUrl: 'yourblog.com' }); 314 + 315 + // ✅ Correct 316 + transformContent(md, { baseUrl: 'https://yourblog.com' }); 317 + 318 + // ✅ Also correct - trailing slash is OK 319 + transformContent(md, { baseUrl: 'https://yourblog.com/' }); 320 + ``` 321 + 322 + ### Plain Text Too Long 323 + 324 + The `textContent` field should be shorter than the markdown. If it's the same length, check that your markdown is being processed: 325 + 326 + ```typescript 327 + const result = transformContent(markdown, options); 328 + 329 + console.log('Markdown length:', result.markdown.length); 330 + console.log('Text length:', result.textContent.length); 331 + // Text should be shorter 332 + ``` 333 + 334 + ## Performance Tips 335 + 336 + For large documents, transformation can be slow. Consider: 337 + 338 + 1. **Cache results** - Don't re-transform unchanged content 339 + 2. **Transform on build** - Pre-transform content at build time 340 + 3. **Lazy load** - Transform on-demand for preview 341 + 342 + ```typescript 343 + // Cache example 344 + const cache = new Map(); 345 + 346 + function getCachedTransform(markdown: string, options: TransformOptions) { 347 + const key = `${markdown.substring(0, 100)}:${options.baseUrl}`; 348 + 349 + if (cache.has(key)) { 350 + return cache.get(key); 351 + } 352 + 353 + const result = transformContent(markdown, options); 354 + cache.set(key, result); 355 + return result; 356 + } 357 + ``` 358 + 359 + ## Next Steps 360 + 361 + - [Publishing](./publishing.md) 362 + - [Verification](./verification.md) 363 + - [Comments](./comments.md)
+304
packages/svelte-standard-site/docs/publishing.md
··· 1 + # Publishing to ATProto 2 + 3 + This guide explains how to publish content FROM your SvelteKit site TO the ATProto network (Bluesky, Leaflet, WhiteWind, etc.). 4 + 5 + ## Prerequisites 6 + 7 + 1. A Bluesky account (or any ATProto account) 8 + 2. An app password (NOT your main password) 9 + - Get one at: https://bsky.app/settings/app-passwords 10 + 3. Your DID (Decentralized Identifier) 11 + - Find it at: https://bsky.app/settings 12 + 13 + ## Quick Start 14 + 15 + ### 1. Install Dependencies 16 + 17 + ```bash 18 + pnpm add svelte-standard-site zod 19 + ``` 20 + 21 + ### 2. Create a Publication 22 + 23 + A publication represents your blog/site on ATProto. 24 + 25 + ```typescript 26 + // scripts/create-publication.ts 27 + import { StandardSitePublisher } from 'svelte-standard-site/publisher'; 28 + 29 + const publisher = new StandardSitePublisher({ 30 + identifier: 'you.bsky.social', // or your DID 31 + password: process.env.ATPROTO_APP_PASSWORD! 32 + }); 33 + 34 + await publisher.login(); 35 + 36 + const result = await publisher.publishPublication({ 37 + name: 'My Awesome Blog', 38 + url: 'https://yourblog.com', 39 + description: 'Thoughts on code, life, and everything', 40 + basicTheme: { 41 + background: { r: 255, g: 245, b: 235 }, 42 + foreground: { r: 30, g: 30, b: 30 }, 43 + accent: { r: 74, g: 124, b: 155 }, 44 + accentForeground: { r: 255, g: 255, b: 255 } 45 + } 46 + }); 47 + 48 + console.log('Publication created!'); 49 + console.log('AT-URI:', result.uri); 50 + console.log('Save this rkey:', result.uri.split('/').pop()); 51 + ``` 52 + 53 + Run it: 54 + 55 + ```bash 56 + ATPROTO_APP_PASSWORD="xxxx-xxxx-xxxx-xxxx" node scripts/create-publication.ts 57 + ``` 58 + 59 + ### 3. Publish Documents 60 + 61 + Create a script to sync your blog posts to ATProto: 62 + 63 + ```typescript 64 + // scripts/publish-posts.ts 65 + import { StandardSitePublisher } from 'svelte-standard-site/publisher'; 66 + import { transformContent } from 'svelte-standard-site/content'; 67 + import fs from 'fs'; 68 + import matter from 'gray-matter'; 69 + 70 + const publisher = new StandardSitePublisher({ 71 + identifier: 'you.bsky.social', 72 + password: process.env.ATPROTO_APP_PASSWORD! 73 + }); 74 + 75 + await publisher.login(); 76 + 77 + // Read your markdown files 78 + const files = fs.readdirSync('./content/posts'); 79 + 80 + for (const file of files) { 81 + const content = fs.readFileSync(`./content/posts/${file}`, 'utf-8'); 82 + const { data, content: markdown } = matter(content); 83 + 84 + // Transform content for ATProto 85 + const transformed = transformContent(markdown, { 86 + baseUrl: 'https://yourblog.com' 87 + }); 88 + 89 + // Publish to ATProto 90 + const result = await publisher.publishDocument({ 91 + site: 'https://yourblog.com', // or AT-URI of your publication 92 + title: data.title, 93 + description: data.description, 94 + publishedAt: data.date.toISOString(), 95 + path: `/posts/${file.replace('.md', '')}`, 96 + tags: data.tags, 97 + content: { 98 + $type: 'site.standard.content.markdown', 99 + text: transformed.markdown, 100 + version: '1.0' 101 + }, 102 + textContent: transformed.textContent 103 + }); 104 + 105 + console.log(`Published: ${data.title}`); 106 + console.log(` → ${result.uri}`); 107 + } 108 + ``` 109 + 110 + ## Advanced Usage 111 + 112 + ### Update Existing Documents 113 + 114 + ```typescript 115 + // Get the rkey from the original publish result 116 + const rkey = '3abc123xyz789'; 117 + 118 + await publisher.updateDocument(rkey, { 119 + site: 'https://yourblog.com', 120 + title: 'Updated Title', 121 + publishedAt: originalDate.toISOString(), 122 + updatedAt: new Date().toISOString(), 123 + content: { 124 + $type: 'site.standard.content.markdown', 125 + text: updatedMarkdown 126 + } 127 + }); 128 + ``` 129 + 130 + ### Delete Documents 131 + 132 + ```typescript 133 + await publisher.deleteDocument('3abc123xyz789'); 134 + ``` 135 + 136 + ### List Your Published Documents 137 + 138 + ```typescript 139 + const documents = await publisher.listDocuments(); 140 + 141 + for (const doc of documents) { 142 + console.log(`${doc.value.title} - ${doc.uri}`); 143 + } 144 + ``` 145 + 146 + ### Custom Themes 147 + 148 + ```typescript 149 + await publisher.publishPublication({ 150 + name: 'Dark Mode Blog', 151 + url: 'https://yourblog.com', 152 + basicTheme: { 153 + background: { r: 13, g: 17, b: 23 }, // Dark 154 + foreground: { r: 230, g: 237, b: 243 }, // Light text 155 + accent: { r: 136, g: 58, b: 234 }, // Purple 156 + accentForeground: { r: 255, g: 255, b: 255 } 157 + } 158 + }); 159 + ``` 160 + 161 + ### With Cover Images 162 + 163 + First, upload the image as a blob: 164 + 165 + ```typescript 166 + const agent = publisher.getAtpAgent(); 167 + 168 + const imageBuffer = fs.readFileSync('./cover.jpg'); 169 + const uploadResult = await agent.uploadBlob(imageBuffer, { 170 + encoding: 'image/jpeg' 171 + }); 172 + 173 + await publisher.publishDocument({ 174 + // ...other fields 175 + coverImage: { 176 + $type: 'blob', 177 + ref: { $link: uploadResult.data.blob.ref.$link }, 178 + mimeType: 'image/jpeg', 179 + size: imageBuffer.length 180 + } 181 + }); 182 + ``` 183 + 184 + ## SvelteKit Integration 185 + 186 + ### Create an Admin Route 187 + 188 + ```typescript 189 + // src/routes/admin/publish/+page.server.ts 190 + import { StandardSitePublisher } from 'svelte-standard-site/publisher'; 191 + import { env } from '$env/dynamic/private'; 192 + import { error } from '@sveltejs/kit'; 193 + import type { PageServerLoad, Actions } from './$types'; 194 + 195 + export const load: PageServerLoad = async () => { 196 + // List existing documents 197 + const publisher = new StandardSitePublisher({ 198 + identifier: env.ATPROTO_HANDLE!, 199 + password: env.ATPROTO_APP_PASSWORD! 200 + }); 201 + 202 + await publisher.login(); 203 + const documents = await publisher.listDocuments(); 204 + 205 + return { 206 + documents 207 + }; 208 + }; 209 + 210 + export const actions = { 211 + publish: async ({ request }) => { 212 + const data = await request.formData(); 213 + const title = data.get('title') as string; 214 + const content = data.get('content') as string; 215 + 216 + const publisher = new StandardSitePublisher({ 217 + identifier: env.ATPROTO_HANDLE!, 218 + password: env.ATPROTO_APP_PASSWORD! 219 + }); 220 + 221 + await publisher.login(); 222 + 223 + const result = await publisher.publishDocument({ 224 + site: env.PUBLIC_SITE_URL!, 225 + title, 226 + publishedAt: new Date().toISOString(), 227 + content: { 228 + $type: 'site.standard.content.markdown', 229 + text: content 230 + } 231 + }); 232 + 233 + return { success: true, uri: result.uri }; 234 + } 235 + } satisfies Actions; 236 + ``` 237 + 238 + ## Important Notes 239 + 240 + ### Security 241 + 242 + 1. **Never commit app passwords** - Use environment variables 243 + 2. **Never use main password** - Always use app passwords 244 + 3. **Validate input** - Always validate data before publishing 245 + 4. **Rate limiting** - Be mindful of API rate limits 246 + 247 + ### TID Format 248 + 249 + Record keys (rkeys) MUST be TIDs (Timestamp Identifiers). The publisher generates these automatically. Do not manually create rkeys. 250 + 251 + ### PDS Resolution 252 + 253 + The publisher automatically resolves your PDS from your DID document. You don't need to specify it unless you're using a custom PDS. 254 + 255 + ### Content Types 256 + 257 + The `content` field is an open union. Different platforms support different types: 258 + 259 + - `site.standard.content.markdown` - Markdown content 260 + - `site.standard.content.html` - HTML content 261 + - Platform-specific types 262 + 263 + Always include `textContent` for search/indexing. 264 + 265 + ## Troubleshooting 266 + 267 + ### "Failed to resolve handle" 268 + 269 + - Check your handle is correct 270 + - Verify your PDS is reachable 271 + - Ensure you're using an app password 272 + 273 + ### "Schema validation failed" 274 + 275 + - Check your data matches the schema 276 + - Ensure dates are ISO 8601 format 277 + - Verify URLs are valid 278 + 279 + ### "Invalid TID" 280 + 281 + - Don't manually create rkeys 282 + - Let the publisher generate TIDs automatically 283 + 284 + ### "Authentication failed" 285 + 286 + - Verify your app password is correct 287 + - Check it hasn't been revoked 288 + - Ensure you're not using your main password 289 + 290 + ## Best Practices 291 + 292 + 1. **Use content transformation** - Always run markdown through `transformContent()` 293 + 2. **Include textContent** - Provides plain text for search 294 + 3. **Add descriptions** - Helps with discovery 295 + 4. **Use tags** - Categorize your content 296 + 5. **Set updatedAt** - Track when content changes 297 + 6. **Link Bluesky posts** - Use `bskyPostRef` for engagement 298 + 7. **Verify ownership** - Set up `.well-known` endpoints 299 + 300 + ## Next Steps 301 + 302 + - [Content Transformation](./content-transformation.md) 303 + - [Verification](./verification.md) 304 + - [Comments](./comments.md)
+412
packages/svelte-standard-site/docs/verification.md
··· 1 + # Content Verification 2 + 3 + Verification proves that you own the content you've published to ATProto. This is done through `.well-known` endpoints and `<link>` tags. 4 + 5 + ## Why Verify? 6 + 7 + Verification allows platforms like Leaflet and WhiteWind to: 8 + 9 + 1. Confirm you control the content you claim to have published 10 + 2. Prevent impersonation 11 + 3. Enable features that require ownership proof 12 + 4. Build trust in the federated ecosystem 13 + 14 + ## Quick Start 15 + 16 + ### 1. Create .well-known Endpoint 17 + 18 + Create a SvelteKit endpoint at `.well-known/site.standard.publication`: 19 + 20 + ```typescript 21 + // src/routes/.well-known/site.standard.publication/+server.ts 22 + import { text } from '@sveltejs/kit'; 23 + import { generatePublicationWellKnown } from 'svelte-standard-site/verification'; 24 + 25 + export function GET() { 26 + return text( 27 + generatePublicationWellKnown({ 28 + did: 'did:plc:your-did-here', 29 + publicationRkey: '3abc123xyz789' // From publication creation 30 + }) 31 + ); 32 + } 33 + ``` 34 + 35 + ### 2. Verify It Works 36 + 37 + ```bash 38 + curl https://yourblog.com/.well-known/site.standard.publication 39 + # Should output: at://did:plc:xxx/site.standard.publication/3abc123xyz789 40 + ``` 41 + 42 + ### 3. Add Link Tag (Optional) 43 + 44 + Add verification to individual documents: 45 + 46 + ```svelte 47 + <!-- src/routes/blog/[slug]/+page.svelte --> 48 + <script lang="ts"> 49 + import { generateDocumentLinkTag } from 'svelte-standard-site/verification'; 50 + 51 + const { data } = $props(); 52 + </script> 53 + 54 + <svelte:head> 55 + {@html generateDocumentLinkTag({ 56 + did: 'did:plc:xxx', 57 + documentRkey: data.rkey 58 + })} 59 + </svelte:head> 60 + ``` 61 + 62 + ## Functions 63 + 64 + ### generatePublicationWellKnown 65 + 66 + Generate content for the `.well-known` endpoint: 67 + 68 + ```typescript 69 + import { generatePublicationWellKnown } from 'svelte-standard-site/verification'; 70 + 71 + const content = generatePublicationWellKnown({ 72 + did: 'did:plc:xxx', 73 + publicationRkey: '3abc123xyz' 74 + }); 75 + // Returns: "at://did:plc:xxx/site.standard.publication/3abc123xyz" 76 + ``` 77 + 78 + ### generateDocumentLinkTag 79 + 80 + Generate a `<link>` tag for a specific document: 81 + 82 + ```typescript 83 + import { generateDocumentLinkTag } from 'svelte-standard-site/verification'; 84 + 85 + const tag = generateDocumentLinkTag({ 86 + did: 'did:plc:xxx', 87 + documentRkey: '3xyz789abc' 88 + }); 89 + // Returns: '<link rel="site.standard.document" href="at://...">' 90 + ``` 91 + 92 + ### generatePublicationLinkTag 93 + 94 + Generate a `<link>` tag for your publication: 95 + 96 + ```typescript 97 + import { generatePublicationLinkTag } from 'svelte-standard-site/verification'; 98 + 99 + const tag = generatePublicationLinkTag({ 100 + did: 'did:plc:xxx', 101 + publicationRkey: '3abc123xyz' 102 + }); 103 + ``` 104 + 105 + ### verifyPublicationWellKnown 106 + 107 + Programmatically verify a site's `.well-known` endpoint: 108 + 109 + ```typescript 110 + import { verifyPublicationWellKnown } from 'svelte-standard-site/verification'; 111 + 112 + const isValid = await verifyPublicationWellKnown( 113 + 'https://example.com', 114 + 'did:plc:xxx', 115 + '3abc123xyz' 116 + ); 117 + 118 + if (isValid) { 119 + console.log('Site is verified!'); 120 + } 121 + ``` 122 + 123 + ## Complete Setup 124 + 125 + ### Get Your Publication Rkey 126 + 127 + When you create a publication, save the rkey: 128 + 129 + ```typescript 130 + import { StandardSitePublisher } from 'svelte-standard-site/publisher'; 131 + 132 + const publisher = new StandardSitePublisher({ 133 + identifier: 'you.bsky.social', 134 + password: process.env.ATPROTO_APP_PASSWORD! 135 + }); 136 + 137 + await publisher.login(); 138 + 139 + const result = await publisher.publishPublication({ 140 + name: 'My Blog', 141 + url: 'https://yourblog.com' 142 + }); 143 + 144 + // Save these! 145 + console.log('DID:', publisher.getDid()); 146 + console.log('Rkey:', result.uri.split('/').pop()); 147 + ``` 148 + 149 + ### Store in Environment Variables 150 + 151 + ```env 152 + # .env 153 + PUBLIC_ATPROTO_DID=did:plc:xxx 154 + PUBLIC_PUBLICATION_RKEY=3abc123xyz 155 + ``` 156 + 157 + ### Create Endpoint 158 + 159 + ```typescript 160 + // src/routes/.well-known/site.standard.publication/+server.ts 161 + import { text } from '@sveltejs/kit'; 162 + import { generatePublicationWellKnown } from 'svelte-standard-site/verification'; 163 + import { PUBLIC_ATPROTO_DID, PUBLIC_PUBLICATION_RKEY } from '$env/static/public'; 164 + 165 + export function GET() { 166 + return text( 167 + generatePublicationWellKnown({ 168 + did: PUBLIC_ATPROTO_DID, 169 + publicationRkey: PUBLIC_PUBLICATION_RKEY 170 + }), 171 + { 172 + headers: { 173 + 'Content-Type': 'text/plain', 174 + 'Cache-Control': 'public, max-age=3600' 175 + } 176 + } 177 + ); 178 + } 179 + ``` 180 + 181 + ### Add to Site Header 182 + 183 + ```svelte 184 + <!-- src/routes/+layout.svelte --> 185 + <script lang="ts"> 186 + import { generatePublicationLinkTag } from 'svelte-standard-site/verification'; 187 + import { PUBLIC_ATPROTO_DID, PUBLIC_PUBLICATION_RKEY } from '$env/static/public'; 188 + </script> 189 + 190 + <svelte:head> 191 + {@html generatePublicationLinkTag({ 192 + did: PUBLIC_ATPROTO_DID, 193 + publicationRkey: PUBLIC_PUBLICATION_RKEY 194 + })} 195 + </svelte:head> 196 + ``` 197 + 198 + ## Document Verification 199 + 200 + For individual blog posts: 201 + 202 + ### Store Document Rkeys 203 + 204 + When publishing, save the mapping: 205 + 206 + ```typescript 207 + // In your publish script 208 + const result = await publisher.publishDocument({ 209 + // ... document data 210 + }); 211 + 212 + // Save mapping: slug -> rkey 213 + const mapping = { 214 + 'my-first-post': result.uri.split('/').pop(), 215 + 'another-post': '3xyz789abc' 216 + // etc. 217 + }; 218 + 219 + fs.writeFileSync('document-rkeys.json', JSON.stringify(mapping)); 220 + ``` 221 + 222 + ### Add to Document Pages 223 + 224 + ```svelte 225 + <!-- src/routes/blog/[slug]/+page.svelte --> 226 + <script lang="ts"> 227 + import { generateDocumentLinkTag } from 'svelte-standard-site/verification'; 228 + import documentRkeys from '$lib/document-rkeys.json'; 229 + 230 + const { data } = $props(); 231 + const rkey = documentRkeys[data.slug]; 232 + </script> 233 + 234 + <svelte:head> 235 + {#if rkey} 236 + {@html generateDocumentLinkTag({ 237 + did: 'did:plc:xxx', 238 + documentRkey: rkey 239 + })} 240 + {/if} 241 + </svelte:head> 242 + ``` 243 + 244 + ## AT-URI Utilities 245 + 246 + ### Build AT-URIs 247 + 248 + ```typescript 249 + import { getDocumentAtUri, getPublicationAtUri } from 'svelte-standard-site/verification'; 250 + 251 + const docUri = getDocumentAtUri('did:plc:xxx', '3xyz789abc'); 252 + // "at://did:plc:xxx/site.standard.document/3xyz789abc" 253 + 254 + const pubUri = getPublicationAtUri('did:plc:xxx', '3abc123xyz'); 255 + // "at://did:plc:xxx/site.standard.publication/3abc123xyz" 256 + ``` 257 + 258 + ### Parse AT-URIs 259 + 260 + ```typescript 261 + import { parseAtUri } from 'svelte-standard-site/verification'; 262 + 263 + const parsed = parseAtUri('at://did:plc:xxx/site.standard.document/3xyz'); 264 + 265 + if (parsed) { 266 + console.log(parsed.did); // "did:plc:xxx" 267 + console.log(parsed.collection); // "site.standard.document" 268 + console.log(parsed.rkey); // "3xyz" 269 + } 270 + ``` 271 + 272 + ### Extract from HTML 273 + 274 + ```typescript 275 + import { extractDocumentLinkFromHtml, extractPublicationLinkFromHtml } from 'svelte-standard-site/verification'; 276 + 277 + const html = await fetch('https://example.com/post').then((r) => r.text()); 278 + 279 + const docUri = extractDocumentLinkFromHtml(html); 280 + // "at://did:plc:xxx/site.standard.document/3xyz" 281 + 282 + const pubUri = extractPublicationLinkFromHtml(html); 283 + // "at://did:plc:xxx/site.standard.publication/3abc" 284 + ``` 285 + 286 + ## Verification Flow 287 + 288 + 1. **Publish** a publication to ATProto 289 + 2. **Save** the DID and rkey 290 + 3. **Create** `.well-known` endpoint returning the AT-URI 291 + 4. **Optionally** add `<link>` tags to your HTML 292 + 5. **Platforms** fetch your `.well-known` endpoint to verify ownership 293 + 294 + ```mermaid 295 + sequenceDiagram 296 + participant You 297 + participant ATProto 298 + participant Platform 299 + 300 + You->>ATProto: Publish publication 301 + ATProto->>You: Return AT-URI 302 + You->>Your Site: Add .well-known endpoint 303 + Platform->>Your Site: Fetch .well-known 304 + Your Site->>Platform: Return AT-URI 305 + Platform->>ATProto: Verify AT-URI exists 306 + ATProto->>Platform: Confirmed 307 + Platform->>Platform: Mark as verified ✓ 308 + ``` 309 + 310 + ## Troubleshooting 311 + 312 + ### .well-known Returning 404 313 + 314 + SvelteKit requires special handling for `.well-known`: 315 + 316 + ```typescript 317 + // Option 1: Create the exact path 318 + // src/routes/.well-known/site.standard.publication/+server.ts 319 + 320 + // Option 2: Use static files 321 + // static/.well-known/site.standard.publication 322 + ``` 323 + 324 + If using static files, make sure your hosting platform allows `.well-known`. 325 + 326 + ### Wrong MIME Type 327 + 328 + Ensure you're returning `text/plain`: 329 + 330 + ```typescript 331 + export function GET() { 332 + return text(content, { 333 + headers: { 334 + 'Content-Type': 'text/plain' 335 + } 336 + }); 337 + } 338 + ``` 339 + 340 + ### CORS Issues 341 + 342 + If platforms can't access your endpoint: 343 + 344 + ```typescript 345 + export function GET() { 346 + return text(content, { 347 + headers: { 348 + 'Content-Type': 'text/plain', 349 + 'Access-Control-Allow-Origin': '*' 350 + } 351 + }); 352 + } 353 + ``` 354 + 355 + ### Verification Failing 356 + 357 + Use the verification utility to test: 358 + 359 + ```typescript 360 + const isValid = await verifyPublicationWellKnown( 361 + 'https://yourblog.com', 362 + 'did:plc:xxx', 363 + '3abc123xyz' 364 + ); 365 + 366 + if (!isValid) { 367 + // Check: 368 + // 1. .well-known endpoint is accessible 369 + // 2. Returns exact AT-URI 370 + // 3. No extra whitespace 371 + // 4. Correct MIME type 372 + } 373 + ``` 374 + 375 + ## Best Practices 376 + 377 + 1. **Use environment variables** - Don't hardcode DIDs/rkeys 378 + 2. **Add caching headers** - `.well-known` content doesn't change often 379 + 3. **Test before deploying** - Verify the endpoint works 380 + 4. **Keep rkeys secure** - Don't expose in client code unnecessarily 381 + 5. **Monitor** - Check that verification keeps working after deploys 382 + 383 + ## Hosting Platform Notes 384 + 385 + ### Vercel 386 + 387 + Works out of the box. No special configuration needed. 388 + 389 + ### Netlify 390 + 391 + Add to `netlify.toml`: 392 + 393 + ```toml 394 + [[redirects]] 395 + from = "/.well-known/site.standard.publication" 396 + to = "/.well-known/site.standard.publication/index.html" 397 + status = 200 398 + ``` 399 + 400 + ### Cloudflare Pages 401 + 402 + Works by default. Consider adding a cache rule for `.well-known`. 403 + 404 + ### GitHub Pages 405 + 406 + Static files work, but SvelteKit endpoints don't. Use the static file approach. 407 + 408 + ## Next Steps 409 + 410 + - [Publishing](./publishing.md) 411 + - [Content Transformation](./content-transformation.md) 412 + - [Comments](./comments.md)
+133
packages/svelte-standard-site/package.json
··· 1 + { 2 + "name": "@ewanc26/svelte-standard-site", 3 + "version": "0.1.0", 4 + "description": "SvelteKit library for reading and writing AT Protocol longform content via site.standard.* records — with a complete design system, federated comments, publishing tools, and content verification.", 5 + "license": "AGPL-3.0-only", 6 + "author": { 7 + "name": "Ewan Croft", 8 + "url": "https://github.com/ewanc26" 9 + }, 10 + "type": "module", 11 + "svelte": "./dist/index.js", 12 + "types": "./dist/index.d.ts", 13 + "exports": { 14 + ".": { 15 + "types": "./dist/index.d.ts", 16 + "svelte": "./dist/index.js", 17 + "default": "./dist/index.js" 18 + }, 19 + "./publisher": { 20 + "types": "./dist/publisher.d.ts", 21 + "default": "./dist/publisher.js" 22 + }, 23 + "./content": { 24 + "types": "./dist/utils/content.d.ts", 25 + "default": "./dist/utils/content.js" 26 + }, 27 + "./comments": { 28 + "types": "./dist/utils/comments.d.ts", 29 + "default": "./dist/utils/comments.js" 30 + }, 31 + "./verification": { 32 + "types": "./dist/utils/verification.d.ts", 33 + "default": "./dist/utils/verification.js" 34 + }, 35 + "./schemas": { 36 + "types": "./dist/schemas.d.ts", 37 + "default": "./dist/schemas.js" 38 + }, 39 + "./config/env": { 40 + "types": "./dist/config/env.d.ts", 41 + "default": "./dist/config/env.js" 42 + }, 43 + "./styles/base.css": { 44 + "default": "./dist/styles/base.css" 45 + }, 46 + "./styles/themes.css": { 47 + "default": "./dist/styles/themes.css" 48 + } 49 + }, 50 + "files": [ 51 + "dist", 52 + "src/lib", 53 + "README.md" 54 + ], 55 + "sideEffects": [ 56 + "**/*.css" 57 + ], 58 + "publishConfig": { 59 + "access": "public" 60 + }, 61 + "scripts": { 62 + "build": "svelte-kit sync && svelte-package -i src/lib -o dist && publint", 63 + "dev": "svelte-kit sync && svelte-package -i src/lib -o dist --watch", 64 + "dev:app": "vite dev", 65 + "preview": "vite preview", 66 + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 67 + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 68 + "test": "vitest run", 69 + "test:watch": "vitest", 70 + "format": "prettier --write .", 71 + "lint": "prettier --check ." 72 + }, 73 + "peerDependencies": { 74 + "@sveltejs/kit": "^2.0.0", 75 + "svelte": "^5.0.0" 76 + }, 77 + "dependencies": { 78 + "@atproto/api": "^0.18.16", 79 + "@ewanc26/atproto": "workspace:*", 80 + "@ewanc26/tid": "workspace:*", 81 + "@ewanc26/utils": "workspace:*", 82 + "@lucide/svelte": "^0.562.0", 83 + "katex": "^0.16.27", 84 + "shiki": "^3.21.0", 85 + "zod": "^3.24.0" 86 + }, 87 + "devDependencies": { 88 + "@sveltejs/adapter-auto": "^7.0.0", 89 + "@sveltejs/kit": "^2.49.1", 90 + "@sveltejs/package": "^2.5.7", 91 + "@sveltejs/vite-plugin-svelte": "^6.2.1", 92 + "@tailwindcss/typography": "^0.5.19", 93 + "@tailwindcss/vite": "^4.1.17", 94 + "@types/node": "^22.0.0", 95 + "prettier": "^3.7.4", 96 + "prettier-plugin-svelte": "^3.4.0", 97 + "prettier-plugin-tailwindcss": "^0.7.2", 98 + "publint": "^0.3.15", 99 + "svelte": "^5.45.6", 100 + "svelte-check": "^4.3.4", 101 + "tailwindcss": "^4.1.17", 102 + "typescript": "^5.9.3", 103 + "vite": "^7.2.6", 104 + "vitest": "^4.0.16" 105 + }, 106 + "keywords": [ 107 + "svelte", 108 + "sveltekit", 109 + "atproto", 110 + "at-protocol", 111 + "bluesky", 112 + "site-standard", 113 + "blog", 114 + "cms", 115 + "design-system", 116 + "components", 117 + "dark-mode", 118 + "light-mode", 119 + "theme", 120 + "publishing", 121 + "federation", 122 + "comments" 123 + ], 124 + "repository": { 125 + "type": "git", 126 + "url": "git+https://github.com/ewanc26/pkgs.git", 127 + "directory": "packages/svelte-standard-site" 128 + }, 129 + "homepage": "https://github.com/ewanc26/pkgs/tree/main/packages/svelte-standard-site", 130 + "bugs": { 131 + "url": "https://github.com/ewanc26/pkgs/issues" 132 + } 133 + }
+1997
packages/svelte-standard-site/pnpm-lock.yaml
··· 1 + lockfileVersion: '9.0' 2 + 3 + settings: 4 + autoInstallPeers: true 5 + excludeLinksFromLockfile: false 6 + 7 + importers: 8 + 9 + .: 10 + dependencies: 11 + '@atproto/api': 12 + specifier: ^0.18.16 13 + version: 0.18.16 14 + '@lucide/svelte': 15 + specifier: ^0.562.0 16 + version: 0.562.0(svelte@5.46.4) 17 + katex: 18 + specifier: ^0.16.27 19 + version: 0.16.27 20 + shiki: 21 + specifier: ^3.21.0 22 + version: 3.21.0 23 + devDependencies: 24 + '@sveltejs/adapter-auto': 25 + specifier: ^7.0.0 26 + version: 7.0.0(@sveltejs/kit@2.50.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.46.4)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.30.2))) 27 + '@sveltejs/kit': 28 + specifier: ^2.49.1 29 + version: 2.50.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.46.4)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.30.2)) 30 + '@sveltejs/package': 31 + specifier: ^2.5.7 32 + version: 2.5.7(svelte@5.46.4)(typescript@5.9.3) 33 + '@sveltejs/vite-plugin-svelte': 34 + specifier: ^6.2.1 35 + version: 6.2.4(svelte@5.46.4)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.30.2)) 36 + '@tailwindcss/typography': 37 + specifier: ^0.5.19 38 + version: 0.5.19(tailwindcss@4.1.18) 39 + '@tailwindcss/vite': 40 + specifier: ^4.1.17 41 + version: 4.1.18(vite@7.3.1(jiti@2.6.1)(lightningcss@1.30.2)) 42 + prettier: 43 + specifier: ^3.7.4 44 + version: 3.8.0 45 + prettier-plugin-svelte: 46 + specifier: ^3.4.0 47 + version: 3.4.1(prettier@3.8.0)(svelte@5.46.4) 48 + prettier-plugin-tailwindcss: 49 + specifier: ^0.7.2 50 + version: 0.7.2(prettier-plugin-svelte@3.4.1(prettier@3.8.0)(svelte@5.46.4))(prettier@3.8.0) 51 + publint: 52 + specifier: ^0.3.15 53 + version: 0.3.16 54 + svelte: 55 + specifier: ^5.45.6 56 + version: 5.46.4 57 + svelte-check: 58 + specifier: ^4.3.4 59 + version: 4.3.5(picomatch@4.0.3)(svelte@5.46.4)(typescript@5.9.3) 60 + tailwindcss: 61 + specifier: ^4.1.17 62 + version: 4.1.18 63 + typescript: 64 + specifier: ^5.9.3 65 + version: 5.9.3 66 + vite: 67 + specifier: ^7.2.6 68 + version: 7.3.1(jiti@2.6.1)(lightningcss@1.30.2) 69 + 70 + packages: 71 + 72 + '@atproto/api@0.18.16': 73 + resolution: {integrity: sha512-tRGKSWr83pP5CQpSboePU21pE+GqLDYy1XHae4HH4hjaT0pr5V8wNgu70kbKB0B02GVUumeDRpJnlHKD+eMzLg==} 74 + 75 + '@atproto/common-web@0.4.12': 76 + resolution: {integrity: sha512-3aCJemqM/fkHQrVPbTCHCdiVstKFI+2LkFLvUhO6XZP0EqUZa/rg/CIZBKTFUWu9I5iYiaEiXL9VwcDRpEevSw==} 77 + 78 + '@atproto/lex-data@0.0.8': 79 + resolution: {integrity: sha512-1Y5tz7BkS7380QuLNXaE8GW8Xba+mRWugt8BKM4BUFYjjUZdmirU8lr72iM4XlEBrzRu8Cfvj+MbsbYaZv+IgA==} 80 + 81 + '@atproto/lex-json@0.0.8': 82 + resolution: {integrity: sha512-w1Qmkae1QhmNz+i1Zm3xr3jp0UPPRENmdlpU0qIrdxWDo9W4Mzkeyc3eSoa+Zs+zN8xkRSQw7RLZte/B7Ipdwg==} 83 + 84 + '@atproto/lexicon@0.6.0': 85 + resolution: {integrity: sha512-5veb8aD+J5M0qszLJ+73KSFsFrJBgAY/nM1TSAJvGY7fNc9ZAT+PSUlmIyrdye9YznAZ07yktalls/TwNV7cHQ==} 86 + 87 + '@atproto/syntax@0.4.2': 88 + resolution: {integrity: sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA==} 89 + 90 + '@atproto/xrpc@0.7.7': 91 + resolution: {integrity: sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA==} 92 + 93 + '@esbuild/aix-ppc64@0.27.2': 94 + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} 95 + engines: {node: '>=18'} 96 + cpu: [ppc64] 97 + os: [aix] 98 + 99 + '@esbuild/android-arm64@0.27.2': 100 + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} 101 + engines: {node: '>=18'} 102 + cpu: [arm64] 103 + os: [android] 104 + 105 + '@esbuild/android-arm@0.27.2': 106 + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} 107 + engines: {node: '>=18'} 108 + cpu: [arm] 109 + os: [android] 110 + 111 + '@esbuild/android-x64@0.27.2': 112 + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} 113 + engines: {node: '>=18'} 114 + cpu: [x64] 115 + os: [android] 116 + 117 + '@esbuild/darwin-arm64@0.27.2': 118 + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} 119 + engines: {node: '>=18'} 120 + cpu: [arm64] 121 + os: [darwin] 122 + 123 + '@esbuild/darwin-x64@0.27.2': 124 + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} 125 + engines: {node: '>=18'} 126 + cpu: [x64] 127 + os: [darwin] 128 + 129 + '@esbuild/freebsd-arm64@0.27.2': 130 + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} 131 + engines: {node: '>=18'} 132 + cpu: [arm64] 133 + os: [freebsd] 134 + 135 + '@esbuild/freebsd-x64@0.27.2': 136 + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} 137 + engines: {node: '>=18'} 138 + cpu: [x64] 139 + os: [freebsd] 140 + 141 + '@esbuild/linux-arm64@0.27.2': 142 + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} 143 + engines: {node: '>=18'} 144 + cpu: [arm64] 145 + os: [linux] 146 + 147 + '@esbuild/linux-arm@0.27.2': 148 + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} 149 + engines: {node: '>=18'} 150 + cpu: [arm] 151 + os: [linux] 152 + 153 + '@esbuild/linux-ia32@0.27.2': 154 + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} 155 + engines: {node: '>=18'} 156 + cpu: [ia32] 157 + os: [linux] 158 + 159 + '@esbuild/linux-loong64@0.27.2': 160 + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} 161 + engines: {node: '>=18'} 162 + cpu: [loong64] 163 + os: [linux] 164 + 165 + '@esbuild/linux-mips64el@0.27.2': 166 + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} 167 + engines: {node: '>=18'} 168 + cpu: [mips64el] 169 + os: [linux] 170 + 171 + '@esbuild/linux-ppc64@0.27.2': 172 + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} 173 + engines: {node: '>=18'} 174 + cpu: [ppc64] 175 + os: [linux] 176 + 177 + '@esbuild/linux-riscv64@0.27.2': 178 + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} 179 + engines: {node: '>=18'} 180 + cpu: [riscv64] 181 + os: [linux] 182 + 183 + '@esbuild/linux-s390x@0.27.2': 184 + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} 185 + engines: {node: '>=18'} 186 + cpu: [s390x] 187 + os: [linux] 188 + 189 + '@esbuild/linux-x64@0.27.2': 190 + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} 191 + engines: {node: '>=18'} 192 + cpu: [x64] 193 + os: [linux] 194 + 195 + '@esbuild/netbsd-arm64@0.27.2': 196 + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} 197 + engines: {node: '>=18'} 198 + cpu: [arm64] 199 + os: [netbsd] 200 + 201 + '@esbuild/netbsd-x64@0.27.2': 202 + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} 203 + engines: {node: '>=18'} 204 + cpu: [x64] 205 + os: [netbsd] 206 + 207 + '@esbuild/openbsd-arm64@0.27.2': 208 + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} 209 + engines: {node: '>=18'} 210 + cpu: [arm64] 211 + os: [openbsd] 212 + 213 + '@esbuild/openbsd-x64@0.27.2': 214 + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} 215 + engines: {node: '>=18'} 216 + cpu: [x64] 217 + os: [openbsd] 218 + 219 + '@esbuild/openharmony-arm64@0.27.2': 220 + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} 221 + engines: {node: '>=18'} 222 + cpu: [arm64] 223 + os: [openharmony] 224 + 225 + '@esbuild/sunos-x64@0.27.2': 226 + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} 227 + engines: {node: '>=18'} 228 + cpu: [x64] 229 + os: [sunos] 230 + 231 + '@esbuild/win32-arm64@0.27.2': 232 + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} 233 + engines: {node: '>=18'} 234 + cpu: [arm64] 235 + os: [win32] 236 + 237 + '@esbuild/win32-ia32@0.27.2': 238 + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} 239 + engines: {node: '>=18'} 240 + cpu: [ia32] 241 + os: [win32] 242 + 243 + '@esbuild/win32-x64@0.27.2': 244 + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} 245 + engines: {node: '>=18'} 246 + cpu: [x64] 247 + os: [win32] 248 + 249 + '@jridgewell/gen-mapping@0.3.13': 250 + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} 251 + 252 + '@jridgewell/remapping@2.3.5': 253 + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} 254 + 255 + '@jridgewell/resolve-uri@3.1.2': 256 + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 257 + engines: {node: '>=6.0.0'} 258 + 259 + '@jridgewell/sourcemap-codec@1.5.5': 260 + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} 261 + 262 + '@jridgewell/trace-mapping@0.3.31': 263 + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} 264 + 265 + '@lucide/svelte@0.562.0': 266 + resolution: {integrity: sha512-wDMULwtTFN2Sc/TFBm6gfuVCNb4Y5P9LDrwxNnUbV52+IEU7NXZmvxwXoz+vrrpad6Xupq+Hw5eUlqIHEGhouw==} 267 + peerDependencies: 268 + svelte: ^5 269 + 270 + '@polka/url@1.0.0-next.29': 271 + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} 272 + 273 + '@publint/pack@0.1.2': 274 + resolution: {integrity: sha512-S+9ANAvUmjutrshV4jZjaiG8XQyuJIZ8a4utWmN/vW1sgQ9IfBnPndwkmQYw53QmouOIytT874u65HEmu6H5jw==} 275 + engines: {node: '>=18'} 276 + 277 + '@rollup/rollup-android-arm-eabi@4.55.1': 278 + resolution: {integrity: sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==} 279 + cpu: [arm] 280 + os: [android] 281 + 282 + '@rollup/rollup-android-arm64@4.55.1': 283 + resolution: {integrity: sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==} 284 + cpu: [arm64] 285 + os: [android] 286 + 287 + '@rollup/rollup-darwin-arm64@4.55.1': 288 + resolution: {integrity: sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==} 289 + cpu: [arm64] 290 + os: [darwin] 291 + 292 + '@rollup/rollup-darwin-x64@4.55.1': 293 + resolution: {integrity: sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==} 294 + cpu: [x64] 295 + os: [darwin] 296 + 297 + '@rollup/rollup-freebsd-arm64@4.55.1': 298 + resolution: {integrity: sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==} 299 + cpu: [arm64] 300 + os: [freebsd] 301 + 302 + '@rollup/rollup-freebsd-x64@4.55.1': 303 + resolution: {integrity: sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==} 304 + cpu: [x64] 305 + os: [freebsd] 306 + 307 + '@rollup/rollup-linux-arm-gnueabihf@4.55.1': 308 + resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==} 309 + cpu: [arm] 310 + os: [linux] 311 + 312 + '@rollup/rollup-linux-arm-musleabihf@4.55.1': 313 + resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==} 314 + cpu: [arm] 315 + os: [linux] 316 + 317 + '@rollup/rollup-linux-arm64-gnu@4.55.1': 318 + resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==} 319 + cpu: [arm64] 320 + os: [linux] 321 + 322 + '@rollup/rollup-linux-arm64-musl@4.55.1': 323 + resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==} 324 + cpu: [arm64] 325 + os: [linux] 326 + 327 + '@rollup/rollup-linux-loong64-gnu@4.55.1': 328 + resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==} 329 + cpu: [loong64] 330 + os: [linux] 331 + 332 + '@rollup/rollup-linux-loong64-musl@4.55.1': 333 + resolution: {integrity: sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==} 334 + cpu: [loong64] 335 + os: [linux] 336 + 337 + '@rollup/rollup-linux-ppc64-gnu@4.55.1': 338 + resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==} 339 + cpu: [ppc64] 340 + os: [linux] 341 + 342 + '@rollup/rollup-linux-ppc64-musl@4.55.1': 343 + resolution: {integrity: sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==} 344 + cpu: [ppc64] 345 + os: [linux] 346 + 347 + '@rollup/rollup-linux-riscv64-gnu@4.55.1': 348 + resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==} 349 + cpu: [riscv64] 350 + os: [linux] 351 + 352 + '@rollup/rollup-linux-riscv64-musl@4.55.1': 353 + resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==} 354 + cpu: [riscv64] 355 + os: [linux] 356 + 357 + '@rollup/rollup-linux-s390x-gnu@4.55.1': 358 + resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==} 359 + cpu: [s390x] 360 + os: [linux] 361 + 362 + '@rollup/rollup-linux-x64-gnu@4.55.1': 363 + resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==} 364 + cpu: [x64] 365 + os: [linux] 366 + 367 + '@rollup/rollup-linux-x64-musl@4.55.1': 368 + resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==} 369 + cpu: [x64] 370 + os: [linux] 371 + 372 + '@rollup/rollup-openbsd-x64@4.55.1': 373 + resolution: {integrity: sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==} 374 + cpu: [x64] 375 + os: [openbsd] 376 + 377 + '@rollup/rollup-openharmony-arm64@4.55.1': 378 + resolution: {integrity: sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==} 379 + cpu: [arm64] 380 + os: [openharmony] 381 + 382 + '@rollup/rollup-win32-arm64-msvc@4.55.1': 383 + resolution: {integrity: sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==} 384 + cpu: [arm64] 385 + os: [win32] 386 + 387 + '@rollup/rollup-win32-ia32-msvc@4.55.1': 388 + resolution: {integrity: sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==} 389 + cpu: [ia32] 390 + os: [win32] 391 + 392 + '@rollup/rollup-win32-x64-gnu@4.55.1': 393 + resolution: {integrity: sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==} 394 + cpu: [x64] 395 + os: [win32] 396 + 397 + '@rollup/rollup-win32-x64-msvc@4.55.1': 398 + resolution: {integrity: sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==} 399 + cpu: [x64] 400 + os: [win32] 401 + 402 + '@shikijs/core@3.21.0': 403 + resolution: {integrity: sha512-AXSQu/2n1UIQekY8euBJlvFYZIw0PHY63jUzGbrOma4wPxzznJXTXkri+QcHeBNaFxiiOljKxxJkVSoB3PjbyA==} 404 + 405 + '@shikijs/engine-javascript@3.21.0': 406 + resolution: {integrity: sha512-ATwv86xlbmfD9n9gKRiwuPpWgPENAWCLwYCGz9ugTJlsO2kOzhOkvoyV/UD+tJ0uT7YRyD530x6ugNSffmvIiQ==} 407 + 408 + '@shikijs/engine-oniguruma@3.21.0': 409 + resolution: {integrity: sha512-OYknTCct6qiwpQDqDdf3iedRdzj6hFlOPv5hMvI+hkWfCKs5mlJ4TXziBG9nyabLwGulrUjHiCq3xCspSzErYQ==} 410 + 411 + '@shikijs/langs@3.21.0': 412 + resolution: {integrity: sha512-g6mn5m+Y6GBJ4wxmBYqalK9Sp0CFkUqfNzUy2pJglUginz6ZpWbaWjDB4fbQ/8SHzFjYbtU6Ddlp1pc+PPNDVA==} 413 + 414 + '@shikijs/themes@3.21.0': 415 + resolution: {integrity: sha512-BAE4cr9EDiZyYzwIHEk7JTBJ9CzlPuM4PchfcA5ao1dWXb25nv6hYsoDiBq2aZK9E3dlt3WB78uI96UESD+8Mw==} 416 + 417 + '@shikijs/types@3.21.0': 418 + resolution: {integrity: sha512-zGrWOxZ0/+0ovPY7PvBU2gIS9tmhSUUt30jAcNV0Bq0gb2S98gwfjIs1vxlmH5zM7/4YxLamT6ChlqqAJmPPjA==} 419 + 420 + '@shikijs/vscode-textmate@10.0.2': 421 + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} 422 + 423 + '@standard-schema/spec@1.1.0': 424 + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} 425 + 426 + '@sveltejs/acorn-typescript@1.0.8': 427 + resolution: {integrity: sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA==} 428 + peerDependencies: 429 + acorn: ^8.9.0 430 + 431 + '@sveltejs/adapter-auto@7.0.0': 432 + resolution: {integrity: sha512-ImDWaErTOCkRS4Gt+5gZuymKFBobnhChXUZ9lhUZLahUgvA4OOvRzi3sahzYgbxGj5nkA6OV0GAW378+dl/gyw==} 433 + peerDependencies: 434 + '@sveltejs/kit': ^2.0.0 435 + 436 + '@sveltejs/kit@2.50.0': 437 + resolution: {integrity: sha512-Hj8sR8O27p2zshFEIJzsvfhLzxga/hWw6tRLnBjMYw70m1aS9BSYCqAUtzDBjRREtX1EvLMYgaC0mYE3Hz4KWA==} 438 + engines: {node: '>=18.13'} 439 + hasBin: true 440 + peerDependencies: 441 + '@opentelemetry/api': ^1.0.0 442 + '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 443 + svelte: ^4.0.0 || ^5.0.0-next.0 444 + typescript: ^5.3.3 445 + vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 446 + peerDependenciesMeta: 447 + '@opentelemetry/api': 448 + optional: true 449 + typescript: 450 + optional: true 451 + 452 + '@sveltejs/package@2.5.7': 453 + resolution: {integrity: sha512-qqD9xa9H7TDiGFrF6rz7AirOR8k15qDK/9i4MIE8te4vWsv5GEogPks61rrZcLy+yWph+aI6pIj2MdoK3YI8AQ==} 454 + engines: {node: ^16.14 || >=18} 455 + hasBin: true 456 + peerDependencies: 457 + svelte: ^3.44.0 || ^4.0.0 || ^5.0.0-next.1 458 + 459 + '@sveltejs/vite-plugin-svelte-inspector@5.0.2': 460 + resolution: {integrity: sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==} 461 + engines: {node: ^20.19 || ^22.12 || >=24} 462 + peerDependencies: 463 + '@sveltejs/vite-plugin-svelte': ^6.0.0-next.0 464 + svelte: ^5.0.0 465 + vite: ^6.3.0 || ^7.0.0 466 + 467 + '@sveltejs/vite-plugin-svelte@6.2.4': 468 + resolution: {integrity: sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==} 469 + engines: {node: ^20.19 || ^22.12 || >=24} 470 + peerDependencies: 471 + svelte: ^5.0.0 472 + vite: ^6.3.0 || ^7.0.0 473 + 474 + '@tailwindcss/node@4.1.18': 475 + resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} 476 + 477 + '@tailwindcss/oxide-android-arm64@4.1.18': 478 + resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==} 479 + engines: {node: '>= 10'} 480 + cpu: [arm64] 481 + os: [android] 482 + 483 + '@tailwindcss/oxide-darwin-arm64@4.1.18': 484 + resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==} 485 + engines: {node: '>= 10'} 486 + cpu: [arm64] 487 + os: [darwin] 488 + 489 + '@tailwindcss/oxide-darwin-x64@4.1.18': 490 + resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==} 491 + engines: {node: '>= 10'} 492 + cpu: [x64] 493 + os: [darwin] 494 + 495 + '@tailwindcss/oxide-freebsd-x64@4.1.18': 496 + resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==} 497 + engines: {node: '>= 10'} 498 + cpu: [x64] 499 + os: [freebsd] 500 + 501 + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': 502 + resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==} 503 + engines: {node: '>= 10'} 504 + cpu: [arm] 505 + os: [linux] 506 + 507 + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': 508 + resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==} 509 + engines: {node: '>= 10'} 510 + cpu: [arm64] 511 + os: [linux] 512 + 513 + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': 514 + resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} 515 + engines: {node: '>= 10'} 516 + cpu: [arm64] 517 + os: [linux] 518 + 519 + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': 520 + resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} 521 + engines: {node: '>= 10'} 522 + cpu: [x64] 523 + os: [linux] 524 + 525 + '@tailwindcss/oxide-linux-x64-musl@4.1.18': 526 + resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} 527 + engines: {node: '>= 10'} 528 + cpu: [x64] 529 + os: [linux] 530 + 531 + '@tailwindcss/oxide-wasm32-wasi@4.1.18': 532 + resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} 533 + engines: {node: '>=14.0.0'} 534 + cpu: [wasm32] 535 + bundledDependencies: 536 + - '@napi-rs/wasm-runtime' 537 + - '@emnapi/core' 538 + - '@emnapi/runtime' 539 + - '@tybys/wasm-util' 540 + - '@emnapi/wasi-threads' 541 + - tslib 542 + 543 + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': 544 + resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==} 545 + engines: {node: '>= 10'} 546 + cpu: [arm64] 547 + os: [win32] 548 + 549 + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': 550 + resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==} 551 + engines: {node: '>= 10'} 552 + cpu: [x64] 553 + os: [win32] 554 + 555 + '@tailwindcss/oxide@4.1.18': 556 + resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==} 557 + engines: {node: '>= 10'} 558 + 559 + '@tailwindcss/typography@0.5.19': 560 + resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==} 561 + peerDependencies: 562 + tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' 563 + 564 + '@tailwindcss/vite@4.1.18': 565 + resolution: {integrity: sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==} 566 + peerDependencies: 567 + vite: ^5.2.0 || ^6 || ^7 568 + 569 + '@types/cookie@0.6.0': 570 + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} 571 + 572 + '@types/estree@1.0.8': 573 + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} 574 + 575 + '@types/hast@3.0.4': 576 + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} 577 + 578 + '@types/mdast@4.0.4': 579 + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} 580 + 581 + '@types/unist@3.0.3': 582 + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} 583 + 584 + '@ungap/structured-clone@1.3.0': 585 + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} 586 + 587 + acorn@8.15.0: 588 + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} 589 + engines: {node: '>=0.4.0'} 590 + hasBin: true 591 + 592 + aria-query@5.3.2: 593 + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} 594 + engines: {node: '>= 0.4'} 595 + 596 + await-lock@2.2.2: 597 + resolution: {integrity: sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==} 598 + 599 + axobject-query@4.1.0: 600 + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} 601 + engines: {node: '>= 0.4'} 602 + 603 + ccount@2.0.1: 604 + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} 605 + 606 + character-entities-html4@2.1.0: 607 + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} 608 + 609 + character-entities-legacy@3.0.0: 610 + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} 611 + 612 + chokidar@4.0.3: 613 + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} 614 + engines: {node: '>= 14.16.0'} 615 + 616 + chokidar@5.0.0: 617 + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} 618 + engines: {node: '>= 20.19.0'} 619 + 620 + clsx@2.1.1: 621 + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} 622 + engines: {node: '>=6'} 623 + 624 + comma-separated-tokens@2.0.3: 625 + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} 626 + 627 + commander@8.3.0: 628 + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} 629 + engines: {node: '>= 12'} 630 + 631 + cookie@0.6.0: 632 + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} 633 + engines: {node: '>= 0.6'} 634 + 635 + cssesc@3.0.0: 636 + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} 637 + engines: {node: '>=4'} 638 + hasBin: true 639 + 640 + dedent-js@1.0.1: 641 + resolution: {integrity: sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ==} 642 + 643 + deepmerge@4.3.1: 644 + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} 645 + engines: {node: '>=0.10.0'} 646 + 647 + dequal@2.0.3: 648 + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} 649 + engines: {node: '>=6'} 650 + 651 + detect-libc@2.1.2: 652 + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} 653 + engines: {node: '>=8'} 654 + 655 + devalue@5.6.2: 656 + resolution: {integrity: sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==} 657 + 658 + devlop@1.1.0: 659 + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} 660 + 661 + enhanced-resolve@5.18.4: 662 + resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} 663 + engines: {node: '>=10.13.0'} 664 + 665 + esbuild@0.27.2: 666 + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} 667 + engines: {node: '>=18'} 668 + hasBin: true 669 + 670 + esm-env@1.2.2: 671 + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} 672 + 673 + esrap@2.2.1: 674 + resolution: {integrity: sha512-GiYWG34AN/4CUyaWAgunGt0Rxvr1PTMlGC0vvEov/uOQYWne2bpN03Um+k8jT+q3op33mKouP2zeJ6OlM+qeUg==} 675 + 676 + fdir@6.5.0: 677 + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} 678 + engines: {node: '>=12.0.0'} 679 + peerDependencies: 680 + picomatch: ^3 || ^4 681 + peerDependenciesMeta: 682 + picomatch: 683 + optional: true 684 + 685 + fsevents@2.3.3: 686 + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 687 + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 688 + os: [darwin] 689 + 690 + graceful-fs@4.2.11: 691 + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 692 + 693 + hast-util-to-html@9.0.5: 694 + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} 695 + 696 + hast-util-whitespace@3.0.0: 697 + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} 698 + 699 + html-void-elements@3.0.0: 700 + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} 701 + 702 + is-reference@3.0.3: 703 + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} 704 + 705 + iso-datestring-validator@2.2.2: 706 + resolution: {integrity: sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==} 707 + 708 + jiti@2.6.1: 709 + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} 710 + hasBin: true 711 + 712 + katex@0.16.27: 713 + resolution: {integrity: sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw==} 714 + hasBin: true 715 + 716 + kleur@4.1.5: 717 + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} 718 + engines: {node: '>=6'} 719 + 720 + lightningcss-android-arm64@1.30.2: 721 + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} 722 + engines: {node: '>= 12.0.0'} 723 + cpu: [arm64] 724 + os: [android] 725 + 726 + lightningcss-darwin-arm64@1.30.2: 727 + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} 728 + engines: {node: '>= 12.0.0'} 729 + cpu: [arm64] 730 + os: [darwin] 731 + 732 + lightningcss-darwin-x64@1.30.2: 733 + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} 734 + engines: {node: '>= 12.0.0'} 735 + cpu: [x64] 736 + os: [darwin] 737 + 738 + lightningcss-freebsd-x64@1.30.2: 739 + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} 740 + engines: {node: '>= 12.0.0'} 741 + cpu: [x64] 742 + os: [freebsd] 743 + 744 + lightningcss-linux-arm-gnueabihf@1.30.2: 745 + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} 746 + engines: {node: '>= 12.0.0'} 747 + cpu: [arm] 748 + os: [linux] 749 + 750 + lightningcss-linux-arm64-gnu@1.30.2: 751 + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} 752 + engines: {node: '>= 12.0.0'} 753 + cpu: [arm64] 754 + os: [linux] 755 + 756 + lightningcss-linux-arm64-musl@1.30.2: 757 + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} 758 + engines: {node: '>= 12.0.0'} 759 + cpu: [arm64] 760 + os: [linux] 761 + 762 + lightningcss-linux-x64-gnu@1.30.2: 763 + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} 764 + engines: {node: '>= 12.0.0'} 765 + cpu: [x64] 766 + os: [linux] 767 + 768 + lightningcss-linux-x64-musl@1.30.2: 769 + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} 770 + engines: {node: '>= 12.0.0'} 771 + cpu: [x64] 772 + os: [linux] 773 + 774 + lightningcss-win32-arm64-msvc@1.30.2: 775 + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} 776 + engines: {node: '>= 12.0.0'} 777 + cpu: [arm64] 778 + os: [win32] 779 + 780 + lightningcss-win32-x64-msvc@1.30.2: 781 + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} 782 + engines: {node: '>= 12.0.0'} 783 + cpu: [x64] 784 + os: [win32] 785 + 786 + lightningcss@1.30.2: 787 + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} 788 + engines: {node: '>= 12.0.0'} 789 + 790 + locate-character@3.0.0: 791 + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} 792 + 793 + magic-string@0.30.21: 794 + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} 795 + 796 + mdast-util-to-hast@13.2.1: 797 + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} 798 + 799 + micromark-util-character@2.1.1: 800 + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} 801 + 802 + micromark-util-encode@2.0.1: 803 + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} 804 + 805 + micromark-util-sanitize-uri@2.0.1: 806 + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} 807 + 808 + micromark-util-symbol@2.0.1: 809 + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} 810 + 811 + micromark-util-types@2.0.2: 812 + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} 813 + 814 + mri@1.2.0: 815 + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} 816 + engines: {node: '>=4'} 817 + 818 + mrmime@2.0.1: 819 + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} 820 + engines: {node: '>=10'} 821 + 822 + multiformats@9.9.0: 823 + resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==} 824 + 825 + nanoid@3.3.11: 826 + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} 827 + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 828 + hasBin: true 829 + 830 + obug@2.1.1: 831 + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} 832 + 833 + oniguruma-parser@0.12.1: 834 + resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} 835 + 836 + oniguruma-to-es@4.3.4: 837 + resolution: {integrity: sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==} 838 + 839 + package-manager-detector@1.6.0: 840 + resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} 841 + 842 + picocolors@1.1.1: 843 + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} 844 + 845 + picomatch@4.0.3: 846 + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} 847 + engines: {node: '>=12'} 848 + 849 + postcss-selector-parser@6.0.10: 850 + resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} 851 + engines: {node: '>=4'} 852 + 853 + postcss@8.5.6: 854 + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} 855 + engines: {node: ^10 || ^12 || >=14} 856 + 857 + prettier-plugin-svelte@3.4.1: 858 + resolution: {integrity: sha512-xL49LCloMoZRvSwa6IEdN2GV6cq2IqpYGstYtMT+5wmml1/dClEoI0MZR78MiVPpu6BdQFfN0/y73yO6+br5Pg==} 859 + peerDependencies: 860 + prettier: ^3.0.0 861 + svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 862 + 863 + prettier-plugin-tailwindcss@0.7.2: 864 + resolution: {integrity: sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA==} 865 + engines: {node: '>=20.19'} 866 + peerDependencies: 867 + '@ianvs/prettier-plugin-sort-imports': '*' 868 + '@prettier/plugin-hermes': '*' 869 + '@prettier/plugin-oxc': '*' 870 + '@prettier/plugin-pug': '*' 871 + '@shopify/prettier-plugin-liquid': '*' 872 + '@trivago/prettier-plugin-sort-imports': '*' 873 + '@zackad/prettier-plugin-twig': '*' 874 + prettier: ^3.0 875 + prettier-plugin-astro: '*' 876 + prettier-plugin-css-order: '*' 877 + prettier-plugin-jsdoc: '*' 878 + prettier-plugin-marko: '*' 879 + prettier-plugin-multiline-arrays: '*' 880 + prettier-plugin-organize-attributes: '*' 881 + prettier-plugin-organize-imports: '*' 882 + prettier-plugin-sort-imports: '*' 883 + prettier-plugin-svelte: '*' 884 + peerDependenciesMeta: 885 + '@ianvs/prettier-plugin-sort-imports': 886 + optional: true 887 + '@prettier/plugin-hermes': 888 + optional: true 889 + '@prettier/plugin-oxc': 890 + optional: true 891 + '@prettier/plugin-pug': 892 + optional: true 893 + '@shopify/prettier-plugin-liquid': 894 + optional: true 895 + '@trivago/prettier-plugin-sort-imports': 896 + optional: true 897 + '@zackad/prettier-plugin-twig': 898 + optional: true 899 + prettier-plugin-astro: 900 + optional: true 901 + prettier-plugin-css-order: 902 + optional: true 903 + prettier-plugin-jsdoc: 904 + optional: true 905 + prettier-plugin-marko: 906 + optional: true 907 + prettier-plugin-multiline-arrays: 908 + optional: true 909 + prettier-plugin-organize-attributes: 910 + optional: true 911 + prettier-plugin-organize-imports: 912 + optional: true 913 + prettier-plugin-sort-imports: 914 + optional: true 915 + prettier-plugin-svelte: 916 + optional: true 917 + 918 + prettier@3.8.0: 919 + resolution: {integrity: sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA==} 920 + engines: {node: '>=14'} 921 + hasBin: true 922 + 923 + property-information@7.1.0: 924 + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} 925 + 926 + publint@0.3.16: 927 + resolution: {integrity: sha512-MFqyfRLAExPVZdTQFwkAQELzA8idyXzROVOytg6nEJ/GEypXBUmMGrVaID8cTuzRS1U5L8yTOdOJtMXgFUJAeA==} 928 + engines: {node: '>=18'} 929 + hasBin: true 930 + 931 + readdirp@4.1.2: 932 + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} 933 + engines: {node: '>= 14.18.0'} 934 + 935 + readdirp@5.0.0: 936 + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} 937 + engines: {node: '>= 20.19.0'} 938 + 939 + regex-recursion@6.0.2: 940 + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} 941 + 942 + regex-utilities@2.3.0: 943 + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} 944 + 945 + regex@6.1.0: 946 + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} 947 + 948 + rollup@4.55.1: 949 + resolution: {integrity: sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==} 950 + engines: {node: '>=18.0.0', npm: '>=8.0.0'} 951 + hasBin: true 952 + 953 + sade@1.8.1: 954 + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} 955 + engines: {node: '>=6'} 956 + 957 + scule@1.3.0: 958 + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} 959 + 960 + semver@7.7.3: 961 + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} 962 + engines: {node: '>=10'} 963 + hasBin: true 964 + 965 + set-cookie-parser@2.7.2: 966 + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} 967 + 968 + shiki@3.21.0: 969 + resolution: {integrity: sha512-N65B/3bqL/TI2crrXr+4UivctrAGEjmsib5rPMMPpFp1xAx/w03v8WZ9RDDFYteXoEgY7qZ4HGgl5KBIu1153w==} 970 + 971 + sirv@3.0.2: 972 + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} 973 + engines: {node: '>=18'} 974 + 975 + source-map-js@1.2.1: 976 + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 977 + engines: {node: '>=0.10.0'} 978 + 979 + space-separated-tokens@2.0.2: 980 + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} 981 + 982 + stringify-entities@4.0.4: 983 + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} 984 + 985 + svelte-check@4.3.5: 986 + resolution: {integrity: sha512-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q==} 987 + engines: {node: '>= 18.0.0'} 988 + hasBin: true 989 + peerDependencies: 990 + svelte: ^4.0.0 || ^5.0.0-next.0 991 + typescript: '>=5.0.0' 992 + 993 + svelte2tsx@0.7.46: 994 + resolution: {integrity: sha512-S++Vw3w47a8rBuhbz4JK0fcGea8tOoX1boT53Aib8+oUO2EKeOG+geXprJVTDfBlvR+IJdf3jIpR2RGwT6paQA==} 995 + peerDependencies: 996 + svelte: ^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0 997 + typescript: ^4.9.4 || ^5.0.0 998 + 999 + svelte@5.46.4: 1000 + resolution: {integrity: sha512-VJwdXrmv9L8L7ZasJeWcCjoIuMRVbhuxbss0fpVnR8yorMmjNDwcjIH08vS6wmSzzzgAG5CADQ1JuXPS2nwt9w==} 1001 + engines: {node: '>=18'} 1002 + 1003 + tailwindcss@4.1.18: 1004 + resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} 1005 + 1006 + tapable@2.3.0: 1007 + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} 1008 + engines: {node: '>=6'} 1009 + 1010 + tinyglobby@0.2.15: 1011 + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} 1012 + engines: {node: '>=12.0.0'} 1013 + 1014 + tlds@1.261.0: 1015 + resolution: {integrity: sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==} 1016 + hasBin: true 1017 + 1018 + totalist@3.0.1: 1019 + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} 1020 + engines: {node: '>=6'} 1021 + 1022 + trim-lines@3.0.1: 1023 + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} 1024 + 1025 + tslib@2.8.1: 1026 + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} 1027 + 1028 + typescript@5.9.3: 1029 + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} 1030 + engines: {node: '>=14.17'} 1031 + hasBin: true 1032 + 1033 + uint8arrays@3.0.0: 1034 + resolution: {integrity: sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==} 1035 + 1036 + unicode-segmenter@0.14.5: 1037 + resolution: {integrity: sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==} 1038 + 1039 + unist-util-is@6.0.1: 1040 + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} 1041 + 1042 + unist-util-position@5.0.0: 1043 + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} 1044 + 1045 + unist-util-stringify-position@4.0.0: 1046 + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} 1047 + 1048 + unist-util-visit-parents@6.0.2: 1049 + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} 1050 + 1051 + unist-util-visit@5.1.0: 1052 + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} 1053 + 1054 + util-deprecate@1.0.2: 1055 + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} 1056 + 1057 + vfile-message@4.0.3: 1058 + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} 1059 + 1060 + vfile@6.0.3: 1061 + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} 1062 + 1063 + vite@7.3.1: 1064 + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} 1065 + engines: {node: ^20.19.0 || >=22.12.0} 1066 + hasBin: true 1067 + peerDependencies: 1068 + '@types/node': ^20.19.0 || >=22.12.0 1069 + jiti: '>=1.21.0' 1070 + less: ^4.0.0 1071 + lightningcss: ^1.21.0 1072 + sass: ^1.70.0 1073 + sass-embedded: ^1.70.0 1074 + stylus: '>=0.54.8' 1075 + sugarss: ^5.0.0 1076 + terser: ^5.16.0 1077 + tsx: ^4.8.1 1078 + yaml: ^2.4.2 1079 + peerDependenciesMeta: 1080 + '@types/node': 1081 + optional: true 1082 + jiti: 1083 + optional: true 1084 + less: 1085 + optional: true 1086 + lightningcss: 1087 + optional: true 1088 + sass: 1089 + optional: true 1090 + sass-embedded: 1091 + optional: true 1092 + stylus: 1093 + optional: true 1094 + sugarss: 1095 + optional: true 1096 + terser: 1097 + optional: true 1098 + tsx: 1099 + optional: true 1100 + yaml: 1101 + optional: true 1102 + 1103 + vitefu@1.1.1: 1104 + resolution: {integrity: sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==} 1105 + peerDependencies: 1106 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 1107 + peerDependenciesMeta: 1108 + vite: 1109 + optional: true 1110 + 1111 + zimmerframe@1.1.4: 1112 + resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} 1113 + 1114 + zod@3.25.76: 1115 + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} 1116 + 1117 + zwitch@2.0.4: 1118 + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} 1119 + 1120 + snapshots: 1121 + 1122 + '@atproto/api@0.18.16': 1123 + dependencies: 1124 + '@atproto/common-web': 0.4.12 1125 + '@atproto/lexicon': 0.6.0 1126 + '@atproto/syntax': 0.4.2 1127 + '@atproto/xrpc': 0.7.7 1128 + await-lock: 2.2.2 1129 + multiformats: 9.9.0 1130 + tlds: 1.261.0 1131 + zod: 3.25.76 1132 + 1133 + '@atproto/common-web@0.4.12': 1134 + dependencies: 1135 + '@atproto/lex-data': 0.0.8 1136 + '@atproto/lex-json': 0.0.8 1137 + zod: 3.25.76 1138 + 1139 + '@atproto/lex-data@0.0.8': 1140 + dependencies: 1141 + '@atproto/syntax': 0.4.2 1142 + multiformats: 9.9.0 1143 + tslib: 2.8.1 1144 + uint8arrays: 3.0.0 1145 + unicode-segmenter: 0.14.5 1146 + 1147 + '@atproto/lex-json@0.0.8': 1148 + dependencies: 1149 + '@atproto/lex-data': 0.0.8 1150 + tslib: 2.8.1 1151 + 1152 + '@atproto/lexicon@0.6.0': 1153 + dependencies: 1154 + '@atproto/common-web': 0.4.12 1155 + '@atproto/syntax': 0.4.2 1156 + iso-datestring-validator: 2.2.2 1157 + multiformats: 9.9.0 1158 + zod: 3.25.76 1159 + 1160 + '@atproto/syntax@0.4.2': {} 1161 + 1162 + '@atproto/xrpc@0.7.7': 1163 + dependencies: 1164 + '@atproto/lexicon': 0.6.0 1165 + zod: 3.25.76 1166 + 1167 + '@esbuild/aix-ppc64@0.27.2': 1168 + optional: true 1169 + 1170 + '@esbuild/android-arm64@0.27.2': 1171 + optional: true 1172 + 1173 + '@esbuild/android-arm@0.27.2': 1174 + optional: true 1175 + 1176 + '@esbuild/android-x64@0.27.2': 1177 + optional: true 1178 + 1179 + '@esbuild/darwin-arm64@0.27.2': 1180 + optional: true 1181 + 1182 + '@esbuild/darwin-x64@0.27.2': 1183 + optional: true 1184 + 1185 + '@esbuild/freebsd-arm64@0.27.2': 1186 + optional: true 1187 + 1188 + '@esbuild/freebsd-x64@0.27.2': 1189 + optional: true 1190 + 1191 + '@esbuild/linux-arm64@0.27.2': 1192 + optional: true 1193 + 1194 + '@esbuild/linux-arm@0.27.2': 1195 + optional: true 1196 + 1197 + '@esbuild/linux-ia32@0.27.2': 1198 + optional: true 1199 + 1200 + '@esbuild/linux-loong64@0.27.2': 1201 + optional: true 1202 + 1203 + '@esbuild/linux-mips64el@0.27.2': 1204 + optional: true 1205 + 1206 + '@esbuild/linux-ppc64@0.27.2': 1207 + optional: true 1208 + 1209 + '@esbuild/linux-riscv64@0.27.2': 1210 + optional: true 1211 + 1212 + '@esbuild/linux-s390x@0.27.2': 1213 + optional: true 1214 + 1215 + '@esbuild/linux-x64@0.27.2': 1216 + optional: true 1217 + 1218 + '@esbuild/netbsd-arm64@0.27.2': 1219 + optional: true 1220 + 1221 + '@esbuild/netbsd-x64@0.27.2': 1222 + optional: true 1223 + 1224 + '@esbuild/openbsd-arm64@0.27.2': 1225 + optional: true 1226 + 1227 + '@esbuild/openbsd-x64@0.27.2': 1228 + optional: true 1229 + 1230 + '@esbuild/openharmony-arm64@0.27.2': 1231 + optional: true 1232 + 1233 + '@esbuild/sunos-x64@0.27.2': 1234 + optional: true 1235 + 1236 + '@esbuild/win32-arm64@0.27.2': 1237 + optional: true 1238 + 1239 + '@esbuild/win32-ia32@0.27.2': 1240 + optional: true 1241 + 1242 + '@esbuild/win32-x64@0.27.2': 1243 + optional: true 1244 + 1245 + '@jridgewell/gen-mapping@0.3.13': 1246 + dependencies: 1247 + '@jridgewell/sourcemap-codec': 1.5.5 1248 + '@jridgewell/trace-mapping': 0.3.31 1249 + 1250 + '@jridgewell/remapping@2.3.5': 1251 + dependencies: 1252 + '@jridgewell/gen-mapping': 0.3.13 1253 + '@jridgewell/trace-mapping': 0.3.31 1254 + 1255 + '@jridgewell/resolve-uri@3.1.2': {} 1256 + 1257 + '@jridgewell/sourcemap-codec@1.5.5': {} 1258 + 1259 + '@jridgewell/trace-mapping@0.3.31': 1260 + dependencies: 1261 + '@jridgewell/resolve-uri': 3.1.2 1262 + '@jridgewell/sourcemap-codec': 1.5.5 1263 + 1264 + '@lucide/svelte@0.562.0(svelte@5.46.4)': 1265 + dependencies: 1266 + svelte: 5.46.4 1267 + 1268 + '@polka/url@1.0.0-next.29': {} 1269 + 1270 + '@publint/pack@0.1.2': {} 1271 + 1272 + '@rollup/rollup-android-arm-eabi@4.55.1': 1273 + optional: true 1274 + 1275 + '@rollup/rollup-android-arm64@4.55.1': 1276 + optional: true 1277 + 1278 + '@rollup/rollup-darwin-arm64@4.55.1': 1279 + optional: true 1280 + 1281 + '@rollup/rollup-darwin-x64@4.55.1': 1282 + optional: true 1283 + 1284 + '@rollup/rollup-freebsd-arm64@4.55.1': 1285 + optional: true 1286 + 1287 + '@rollup/rollup-freebsd-x64@4.55.1': 1288 + optional: true 1289 + 1290 + '@rollup/rollup-linux-arm-gnueabihf@4.55.1': 1291 + optional: true 1292 + 1293 + '@rollup/rollup-linux-arm-musleabihf@4.55.1': 1294 + optional: true 1295 + 1296 + '@rollup/rollup-linux-arm64-gnu@4.55.1': 1297 + optional: true 1298 + 1299 + '@rollup/rollup-linux-arm64-musl@4.55.1': 1300 + optional: true 1301 + 1302 + '@rollup/rollup-linux-loong64-gnu@4.55.1': 1303 + optional: true 1304 + 1305 + '@rollup/rollup-linux-loong64-musl@4.55.1': 1306 + optional: true 1307 + 1308 + '@rollup/rollup-linux-ppc64-gnu@4.55.1': 1309 + optional: true 1310 + 1311 + '@rollup/rollup-linux-ppc64-musl@4.55.1': 1312 + optional: true 1313 + 1314 + '@rollup/rollup-linux-riscv64-gnu@4.55.1': 1315 + optional: true 1316 + 1317 + '@rollup/rollup-linux-riscv64-musl@4.55.1': 1318 + optional: true 1319 + 1320 + '@rollup/rollup-linux-s390x-gnu@4.55.1': 1321 + optional: true 1322 + 1323 + '@rollup/rollup-linux-x64-gnu@4.55.1': 1324 + optional: true 1325 + 1326 + '@rollup/rollup-linux-x64-musl@4.55.1': 1327 + optional: true 1328 + 1329 + '@rollup/rollup-openbsd-x64@4.55.1': 1330 + optional: true 1331 + 1332 + '@rollup/rollup-openharmony-arm64@4.55.1': 1333 + optional: true 1334 + 1335 + '@rollup/rollup-win32-arm64-msvc@4.55.1': 1336 + optional: true 1337 + 1338 + '@rollup/rollup-win32-ia32-msvc@4.55.1': 1339 + optional: true 1340 + 1341 + '@rollup/rollup-win32-x64-gnu@4.55.1': 1342 + optional: true 1343 + 1344 + '@rollup/rollup-win32-x64-msvc@4.55.1': 1345 + optional: true 1346 + 1347 + '@shikijs/core@3.21.0': 1348 + dependencies: 1349 + '@shikijs/types': 3.21.0 1350 + '@shikijs/vscode-textmate': 10.0.2 1351 + '@types/hast': 3.0.4 1352 + hast-util-to-html: 9.0.5 1353 + 1354 + '@shikijs/engine-javascript@3.21.0': 1355 + dependencies: 1356 + '@shikijs/types': 3.21.0 1357 + '@shikijs/vscode-textmate': 10.0.2 1358 + oniguruma-to-es: 4.3.4 1359 + 1360 + '@shikijs/engine-oniguruma@3.21.0': 1361 + dependencies: 1362 + '@shikijs/types': 3.21.0 1363 + '@shikijs/vscode-textmate': 10.0.2 1364 + 1365 + '@shikijs/langs@3.21.0': 1366 + dependencies: 1367 + '@shikijs/types': 3.21.0 1368 + 1369 + '@shikijs/themes@3.21.0': 1370 + dependencies: 1371 + '@shikijs/types': 3.21.0 1372 + 1373 + '@shikijs/types@3.21.0': 1374 + dependencies: 1375 + '@shikijs/vscode-textmate': 10.0.2 1376 + '@types/hast': 3.0.4 1377 + 1378 + '@shikijs/vscode-textmate@10.0.2': {} 1379 + 1380 + '@standard-schema/spec@1.1.0': {} 1381 + 1382 + '@sveltejs/acorn-typescript@1.0.8(acorn@8.15.0)': 1383 + dependencies: 1384 + acorn: 8.15.0 1385 + 1386 + '@sveltejs/adapter-auto@7.0.0(@sveltejs/kit@2.50.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.46.4)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.30.2)))': 1387 + dependencies: 1388 + '@sveltejs/kit': 2.50.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.46.4)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.30.2)) 1389 + 1390 + '@sveltejs/kit@2.50.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.46.4)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.30.2))': 1391 + dependencies: 1392 + '@standard-schema/spec': 1.1.0 1393 + '@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0) 1394 + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.46.4)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.30.2)) 1395 + '@types/cookie': 0.6.0 1396 + acorn: 8.15.0 1397 + cookie: 0.6.0 1398 + devalue: 5.6.2 1399 + esm-env: 1.2.2 1400 + kleur: 4.1.5 1401 + magic-string: 0.30.21 1402 + mrmime: 2.0.1 1403 + sade: 1.8.1 1404 + set-cookie-parser: 2.7.2 1405 + sirv: 3.0.2 1406 + svelte: 5.46.4 1407 + vite: 7.3.1(jiti@2.6.1)(lightningcss@1.30.2) 1408 + optionalDependencies: 1409 + typescript: 5.9.3 1410 + 1411 + '@sveltejs/package@2.5.7(svelte@5.46.4)(typescript@5.9.3)': 1412 + dependencies: 1413 + chokidar: 5.0.0 1414 + kleur: 4.1.5 1415 + sade: 1.8.1 1416 + semver: 7.7.3 1417 + svelte: 5.46.4 1418 + svelte2tsx: 0.7.46(svelte@5.46.4)(typescript@5.9.3) 1419 + transitivePeerDependencies: 1420 + - typescript 1421 + 1422 + '@sveltejs/vite-plugin-svelte-inspector@5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.46.4)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.46.4)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.30.2))': 1423 + dependencies: 1424 + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.46.4)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.30.2)) 1425 + obug: 2.1.1 1426 + svelte: 5.46.4 1427 + vite: 7.3.1(jiti@2.6.1)(lightningcss@1.30.2) 1428 + 1429 + '@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.46.4)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.30.2))': 1430 + dependencies: 1431 + '@sveltejs/vite-plugin-svelte-inspector': 5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.46.4)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.46.4)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.30.2)) 1432 + deepmerge: 4.3.1 1433 + magic-string: 0.30.21 1434 + obug: 2.1.1 1435 + svelte: 5.46.4 1436 + vite: 7.3.1(jiti@2.6.1)(lightningcss@1.30.2) 1437 + vitefu: 1.1.1(vite@7.3.1(jiti@2.6.1)(lightningcss@1.30.2)) 1438 + 1439 + '@tailwindcss/node@4.1.18': 1440 + dependencies: 1441 + '@jridgewell/remapping': 2.3.5 1442 + enhanced-resolve: 5.18.4 1443 + jiti: 2.6.1 1444 + lightningcss: 1.30.2 1445 + magic-string: 0.30.21 1446 + source-map-js: 1.2.1 1447 + tailwindcss: 4.1.18 1448 + 1449 + '@tailwindcss/oxide-android-arm64@4.1.18': 1450 + optional: true 1451 + 1452 + '@tailwindcss/oxide-darwin-arm64@4.1.18': 1453 + optional: true 1454 + 1455 + '@tailwindcss/oxide-darwin-x64@4.1.18': 1456 + optional: true 1457 + 1458 + '@tailwindcss/oxide-freebsd-x64@4.1.18': 1459 + optional: true 1460 + 1461 + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': 1462 + optional: true 1463 + 1464 + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': 1465 + optional: true 1466 + 1467 + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': 1468 + optional: true 1469 + 1470 + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': 1471 + optional: true 1472 + 1473 + '@tailwindcss/oxide-linux-x64-musl@4.1.18': 1474 + optional: true 1475 + 1476 + '@tailwindcss/oxide-wasm32-wasi@4.1.18': 1477 + optional: true 1478 + 1479 + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': 1480 + optional: true 1481 + 1482 + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': 1483 + optional: true 1484 + 1485 + '@tailwindcss/oxide@4.1.18': 1486 + optionalDependencies: 1487 + '@tailwindcss/oxide-android-arm64': 4.1.18 1488 + '@tailwindcss/oxide-darwin-arm64': 4.1.18 1489 + '@tailwindcss/oxide-darwin-x64': 4.1.18 1490 + '@tailwindcss/oxide-freebsd-x64': 4.1.18 1491 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18 1492 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.18 1493 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.18 1494 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.18 1495 + '@tailwindcss/oxide-linux-x64-musl': 4.1.18 1496 + '@tailwindcss/oxide-wasm32-wasi': 4.1.18 1497 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 1498 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 1499 + 1500 + '@tailwindcss/typography@0.5.19(tailwindcss@4.1.18)': 1501 + dependencies: 1502 + postcss-selector-parser: 6.0.10 1503 + tailwindcss: 4.1.18 1504 + 1505 + '@tailwindcss/vite@4.1.18(vite@7.3.1(jiti@2.6.1)(lightningcss@1.30.2))': 1506 + dependencies: 1507 + '@tailwindcss/node': 4.1.18 1508 + '@tailwindcss/oxide': 4.1.18 1509 + tailwindcss: 4.1.18 1510 + vite: 7.3.1(jiti@2.6.1)(lightningcss@1.30.2) 1511 + 1512 + '@types/cookie@0.6.0': {} 1513 + 1514 + '@types/estree@1.0.8': {} 1515 + 1516 + '@types/hast@3.0.4': 1517 + dependencies: 1518 + '@types/unist': 3.0.3 1519 + 1520 + '@types/mdast@4.0.4': 1521 + dependencies: 1522 + '@types/unist': 3.0.3 1523 + 1524 + '@types/unist@3.0.3': {} 1525 + 1526 + '@ungap/structured-clone@1.3.0': {} 1527 + 1528 + acorn@8.15.0: {} 1529 + 1530 + aria-query@5.3.2: {} 1531 + 1532 + await-lock@2.2.2: {} 1533 + 1534 + axobject-query@4.1.0: {} 1535 + 1536 + ccount@2.0.1: {} 1537 + 1538 + character-entities-html4@2.1.0: {} 1539 + 1540 + character-entities-legacy@3.0.0: {} 1541 + 1542 + chokidar@4.0.3: 1543 + dependencies: 1544 + readdirp: 4.1.2 1545 + 1546 + chokidar@5.0.0: 1547 + dependencies: 1548 + readdirp: 5.0.0 1549 + 1550 + clsx@2.1.1: {} 1551 + 1552 + comma-separated-tokens@2.0.3: {} 1553 + 1554 + commander@8.3.0: {} 1555 + 1556 + cookie@0.6.0: {} 1557 + 1558 + cssesc@3.0.0: {} 1559 + 1560 + dedent-js@1.0.1: {} 1561 + 1562 + deepmerge@4.3.1: {} 1563 + 1564 + dequal@2.0.3: {} 1565 + 1566 + detect-libc@2.1.2: {} 1567 + 1568 + devalue@5.6.2: {} 1569 + 1570 + devlop@1.1.0: 1571 + dependencies: 1572 + dequal: 2.0.3 1573 + 1574 + enhanced-resolve@5.18.4: 1575 + dependencies: 1576 + graceful-fs: 4.2.11 1577 + tapable: 2.3.0 1578 + 1579 + esbuild@0.27.2: 1580 + optionalDependencies: 1581 + '@esbuild/aix-ppc64': 0.27.2 1582 + '@esbuild/android-arm': 0.27.2 1583 + '@esbuild/android-arm64': 0.27.2 1584 + '@esbuild/android-x64': 0.27.2 1585 + '@esbuild/darwin-arm64': 0.27.2 1586 + '@esbuild/darwin-x64': 0.27.2 1587 + '@esbuild/freebsd-arm64': 0.27.2 1588 + '@esbuild/freebsd-x64': 0.27.2 1589 + '@esbuild/linux-arm': 0.27.2 1590 + '@esbuild/linux-arm64': 0.27.2 1591 + '@esbuild/linux-ia32': 0.27.2 1592 + '@esbuild/linux-loong64': 0.27.2 1593 + '@esbuild/linux-mips64el': 0.27.2 1594 + '@esbuild/linux-ppc64': 0.27.2 1595 + '@esbuild/linux-riscv64': 0.27.2 1596 + '@esbuild/linux-s390x': 0.27.2 1597 + '@esbuild/linux-x64': 0.27.2 1598 + '@esbuild/netbsd-arm64': 0.27.2 1599 + '@esbuild/netbsd-x64': 0.27.2 1600 + '@esbuild/openbsd-arm64': 0.27.2 1601 + '@esbuild/openbsd-x64': 0.27.2 1602 + '@esbuild/openharmony-arm64': 0.27.2 1603 + '@esbuild/sunos-x64': 0.27.2 1604 + '@esbuild/win32-arm64': 0.27.2 1605 + '@esbuild/win32-ia32': 0.27.2 1606 + '@esbuild/win32-x64': 0.27.2 1607 + 1608 + esm-env@1.2.2: {} 1609 + 1610 + esrap@2.2.1: 1611 + dependencies: 1612 + '@jridgewell/sourcemap-codec': 1.5.5 1613 + 1614 + fdir@6.5.0(picomatch@4.0.3): 1615 + optionalDependencies: 1616 + picomatch: 4.0.3 1617 + 1618 + fsevents@2.3.3: 1619 + optional: true 1620 + 1621 + graceful-fs@4.2.11: {} 1622 + 1623 + hast-util-to-html@9.0.5: 1624 + dependencies: 1625 + '@types/hast': 3.0.4 1626 + '@types/unist': 3.0.3 1627 + ccount: 2.0.1 1628 + comma-separated-tokens: 2.0.3 1629 + hast-util-whitespace: 3.0.0 1630 + html-void-elements: 3.0.0 1631 + mdast-util-to-hast: 13.2.1 1632 + property-information: 7.1.0 1633 + space-separated-tokens: 2.0.2 1634 + stringify-entities: 4.0.4 1635 + zwitch: 2.0.4 1636 + 1637 + hast-util-whitespace@3.0.0: 1638 + dependencies: 1639 + '@types/hast': 3.0.4 1640 + 1641 + html-void-elements@3.0.0: {} 1642 + 1643 + is-reference@3.0.3: 1644 + dependencies: 1645 + '@types/estree': 1.0.8 1646 + 1647 + iso-datestring-validator@2.2.2: {} 1648 + 1649 + jiti@2.6.1: {} 1650 + 1651 + katex@0.16.27: 1652 + dependencies: 1653 + commander: 8.3.0 1654 + 1655 + kleur@4.1.5: {} 1656 + 1657 + lightningcss-android-arm64@1.30.2: 1658 + optional: true 1659 + 1660 + lightningcss-darwin-arm64@1.30.2: 1661 + optional: true 1662 + 1663 + lightningcss-darwin-x64@1.30.2: 1664 + optional: true 1665 + 1666 + lightningcss-freebsd-x64@1.30.2: 1667 + optional: true 1668 + 1669 + lightningcss-linux-arm-gnueabihf@1.30.2: 1670 + optional: true 1671 + 1672 + lightningcss-linux-arm64-gnu@1.30.2: 1673 + optional: true 1674 + 1675 + lightningcss-linux-arm64-musl@1.30.2: 1676 + optional: true 1677 + 1678 + lightningcss-linux-x64-gnu@1.30.2: 1679 + optional: true 1680 + 1681 + lightningcss-linux-x64-musl@1.30.2: 1682 + optional: true 1683 + 1684 + lightningcss-win32-arm64-msvc@1.30.2: 1685 + optional: true 1686 + 1687 + lightningcss-win32-x64-msvc@1.30.2: 1688 + optional: true 1689 + 1690 + lightningcss@1.30.2: 1691 + dependencies: 1692 + detect-libc: 2.1.2 1693 + optionalDependencies: 1694 + lightningcss-android-arm64: 1.30.2 1695 + lightningcss-darwin-arm64: 1.30.2 1696 + lightningcss-darwin-x64: 1.30.2 1697 + lightningcss-freebsd-x64: 1.30.2 1698 + lightningcss-linux-arm-gnueabihf: 1.30.2 1699 + lightningcss-linux-arm64-gnu: 1.30.2 1700 + lightningcss-linux-arm64-musl: 1.30.2 1701 + lightningcss-linux-x64-gnu: 1.30.2 1702 + lightningcss-linux-x64-musl: 1.30.2 1703 + lightningcss-win32-arm64-msvc: 1.30.2 1704 + lightningcss-win32-x64-msvc: 1.30.2 1705 + 1706 + locate-character@3.0.0: {} 1707 + 1708 + magic-string@0.30.21: 1709 + dependencies: 1710 + '@jridgewell/sourcemap-codec': 1.5.5 1711 + 1712 + mdast-util-to-hast@13.2.1: 1713 + dependencies: 1714 + '@types/hast': 3.0.4 1715 + '@types/mdast': 4.0.4 1716 + '@ungap/structured-clone': 1.3.0 1717 + devlop: 1.1.0 1718 + micromark-util-sanitize-uri: 2.0.1 1719 + trim-lines: 3.0.1 1720 + unist-util-position: 5.0.0 1721 + unist-util-visit: 5.1.0 1722 + vfile: 6.0.3 1723 + 1724 + micromark-util-character@2.1.1: 1725 + dependencies: 1726 + micromark-util-symbol: 2.0.1 1727 + micromark-util-types: 2.0.2 1728 + 1729 + micromark-util-encode@2.0.1: {} 1730 + 1731 + micromark-util-sanitize-uri@2.0.1: 1732 + dependencies: 1733 + micromark-util-character: 2.1.1 1734 + micromark-util-encode: 2.0.1 1735 + micromark-util-symbol: 2.0.1 1736 + 1737 + micromark-util-symbol@2.0.1: {} 1738 + 1739 + micromark-util-types@2.0.2: {} 1740 + 1741 + mri@1.2.0: {} 1742 + 1743 + mrmime@2.0.1: {} 1744 + 1745 + multiformats@9.9.0: {} 1746 + 1747 + nanoid@3.3.11: {} 1748 + 1749 + obug@2.1.1: {} 1750 + 1751 + oniguruma-parser@0.12.1: {} 1752 + 1753 + oniguruma-to-es@4.3.4: 1754 + dependencies: 1755 + oniguruma-parser: 0.12.1 1756 + regex: 6.1.0 1757 + regex-recursion: 6.0.2 1758 + 1759 + package-manager-detector@1.6.0: {} 1760 + 1761 + picocolors@1.1.1: {} 1762 + 1763 + picomatch@4.0.3: {} 1764 + 1765 + postcss-selector-parser@6.0.10: 1766 + dependencies: 1767 + cssesc: 3.0.0 1768 + util-deprecate: 1.0.2 1769 + 1770 + postcss@8.5.6: 1771 + dependencies: 1772 + nanoid: 3.3.11 1773 + picocolors: 1.1.1 1774 + source-map-js: 1.2.1 1775 + 1776 + prettier-plugin-svelte@3.4.1(prettier@3.8.0)(svelte@5.46.4): 1777 + dependencies: 1778 + prettier: 3.8.0 1779 + svelte: 5.46.4 1780 + 1781 + prettier-plugin-tailwindcss@0.7.2(prettier-plugin-svelte@3.4.1(prettier@3.8.0)(svelte@5.46.4))(prettier@3.8.0): 1782 + dependencies: 1783 + prettier: 3.8.0 1784 + optionalDependencies: 1785 + prettier-plugin-svelte: 3.4.1(prettier@3.8.0)(svelte@5.46.4) 1786 + 1787 + prettier@3.8.0: {} 1788 + 1789 + property-information@7.1.0: {} 1790 + 1791 + publint@0.3.16: 1792 + dependencies: 1793 + '@publint/pack': 0.1.2 1794 + package-manager-detector: 1.6.0 1795 + picocolors: 1.1.1 1796 + sade: 1.8.1 1797 + 1798 + readdirp@4.1.2: {} 1799 + 1800 + readdirp@5.0.0: {} 1801 + 1802 + regex-recursion@6.0.2: 1803 + dependencies: 1804 + regex-utilities: 2.3.0 1805 + 1806 + regex-utilities@2.3.0: {} 1807 + 1808 + regex@6.1.0: 1809 + dependencies: 1810 + regex-utilities: 2.3.0 1811 + 1812 + rollup@4.55.1: 1813 + dependencies: 1814 + '@types/estree': 1.0.8 1815 + optionalDependencies: 1816 + '@rollup/rollup-android-arm-eabi': 4.55.1 1817 + '@rollup/rollup-android-arm64': 4.55.1 1818 + '@rollup/rollup-darwin-arm64': 4.55.1 1819 + '@rollup/rollup-darwin-x64': 4.55.1 1820 + '@rollup/rollup-freebsd-arm64': 4.55.1 1821 + '@rollup/rollup-freebsd-x64': 4.55.1 1822 + '@rollup/rollup-linux-arm-gnueabihf': 4.55.1 1823 + '@rollup/rollup-linux-arm-musleabihf': 4.55.1 1824 + '@rollup/rollup-linux-arm64-gnu': 4.55.1 1825 + '@rollup/rollup-linux-arm64-musl': 4.55.1 1826 + '@rollup/rollup-linux-loong64-gnu': 4.55.1 1827 + '@rollup/rollup-linux-loong64-musl': 4.55.1 1828 + '@rollup/rollup-linux-ppc64-gnu': 4.55.1 1829 + '@rollup/rollup-linux-ppc64-musl': 4.55.1 1830 + '@rollup/rollup-linux-riscv64-gnu': 4.55.1 1831 + '@rollup/rollup-linux-riscv64-musl': 4.55.1 1832 + '@rollup/rollup-linux-s390x-gnu': 4.55.1 1833 + '@rollup/rollup-linux-x64-gnu': 4.55.1 1834 + '@rollup/rollup-linux-x64-musl': 4.55.1 1835 + '@rollup/rollup-openbsd-x64': 4.55.1 1836 + '@rollup/rollup-openharmony-arm64': 4.55.1 1837 + '@rollup/rollup-win32-arm64-msvc': 4.55.1 1838 + '@rollup/rollup-win32-ia32-msvc': 4.55.1 1839 + '@rollup/rollup-win32-x64-gnu': 4.55.1 1840 + '@rollup/rollup-win32-x64-msvc': 4.55.1 1841 + fsevents: 2.3.3 1842 + 1843 + sade@1.8.1: 1844 + dependencies: 1845 + mri: 1.2.0 1846 + 1847 + scule@1.3.0: {} 1848 + 1849 + semver@7.7.3: {} 1850 + 1851 + set-cookie-parser@2.7.2: {} 1852 + 1853 + shiki@3.21.0: 1854 + dependencies: 1855 + '@shikijs/core': 3.21.0 1856 + '@shikijs/engine-javascript': 3.21.0 1857 + '@shikijs/engine-oniguruma': 3.21.0 1858 + '@shikijs/langs': 3.21.0 1859 + '@shikijs/themes': 3.21.0 1860 + '@shikijs/types': 3.21.0 1861 + '@shikijs/vscode-textmate': 10.0.2 1862 + '@types/hast': 3.0.4 1863 + 1864 + sirv@3.0.2: 1865 + dependencies: 1866 + '@polka/url': 1.0.0-next.29 1867 + mrmime: 2.0.1 1868 + totalist: 3.0.1 1869 + 1870 + source-map-js@1.2.1: {} 1871 + 1872 + space-separated-tokens@2.0.2: {} 1873 + 1874 + stringify-entities@4.0.4: 1875 + dependencies: 1876 + character-entities-html4: 2.1.0 1877 + character-entities-legacy: 3.0.0 1878 + 1879 + svelte-check@4.3.5(picomatch@4.0.3)(svelte@5.46.4)(typescript@5.9.3): 1880 + dependencies: 1881 + '@jridgewell/trace-mapping': 0.3.31 1882 + chokidar: 4.0.3 1883 + fdir: 6.5.0(picomatch@4.0.3) 1884 + picocolors: 1.1.1 1885 + sade: 1.8.1 1886 + svelte: 5.46.4 1887 + typescript: 5.9.3 1888 + transitivePeerDependencies: 1889 + - picomatch 1890 + 1891 + svelte2tsx@0.7.46(svelte@5.46.4)(typescript@5.9.3): 1892 + dependencies: 1893 + dedent-js: 1.0.1 1894 + scule: 1.3.0 1895 + svelte: 5.46.4 1896 + typescript: 5.9.3 1897 + 1898 + svelte@5.46.4: 1899 + dependencies: 1900 + '@jridgewell/remapping': 2.3.5 1901 + '@jridgewell/sourcemap-codec': 1.5.5 1902 + '@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0) 1903 + '@types/estree': 1.0.8 1904 + acorn: 8.15.0 1905 + aria-query: 5.3.2 1906 + axobject-query: 4.1.0 1907 + clsx: 2.1.1 1908 + devalue: 5.6.2 1909 + esm-env: 1.2.2 1910 + esrap: 2.2.1 1911 + is-reference: 3.0.3 1912 + locate-character: 3.0.0 1913 + magic-string: 0.30.21 1914 + zimmerframe: 1.1.4 1915 + 1916 + tailwindcss@4.1.18: {} 1917 + 1918 + tapable@2.3.0: {} 1919 + 1920 + tinyglobby@0.2.15: 1921 + dependencies: 1922 + fdir: 6.5.0(picomatch@4.0.3) 1923 + picomatch: 4.0.3 1924 + 1925 + tlds@1.261.0: {} 1926 + 1927 + totalist@3.0.1: {} 1928 + 1929 + trim-lines@3.0.1: {} 1930 + 1931 + tslib@2.8.1: {} 1932 + 1933 + typescript@5.9.3: {} 1934 + 1935 + uint8arrays@3.0.0: 1936 + dependencies: 1937 + multiformats: 9.9.0 1938 + 1939 + unicode-segmenter@0.14.5: {} 1940 + 1941 + unist-util-is@6.0.1: 1942 + dependencies: 1943 + '@types/unist': 3.0.3 1944 + 1945 + unist-util-position@5.0.0: 1946 + dependencies: 1947 + '@types/unist': 3.0.3 1948 + 1949 + unist-util-stringify-position@4.0.0: 1950 + dependencies: 1951 + '@types/unist': 3.0.3 1952 + 1953 + unist-util-visit-parents@6.0.2: 1954 + dependencies: 1955 + '@types/unist': 3.0.3 1956 + unist-util-is: 6.0.1 1957 + 1958 + unist-util-visit@5.1.0: 1959 + dependencies: 1960 + '@types/unist': 3.0.3 1961 + unist-util-is: 6.0.1 1962 + unist-util-visit-parents: 6.0.2 1963 + 1964 + util-deprecate@1.0.2: {} 1965 + 1966 + vfile-message@4.0.3: 1967 + dependencies: 1968 + '@types/unist': 3.0.3 1969 + unist-util-stringify-position: 4.0.0 1970 + 1971 + vfile@6.0.3: 1972 + dependencies: 1973 + '@types/unist': 3.0.3 1974 + vfile-message: 4.0.3 1975 + 1976 + vite@7.3.1(jiti@2.6.1)(lightningcss@1.30.2): 1977 + dependencies: 1978 + esbuild: 0.27.2 1979 + fdir: 6.5.0(picomatch@4.0.3) 1980 + picomatch: 4.0.3 1981 + postcss: 8.5.6 1982 + rollup: 4.55.1 1983 + tinyglobby: 0.2.15 1984 + optionalDependencies: 1985 + fsevents: 2.3.3 1986 + jiti: 2.6.1 1987 + lightningcss: 1.30.2 1988 + 1989 + vitefu@1.1.1(vite@7.3.1(jiti@2.6.1)(lightningcss@1.30.2)): 1990 + optionalDependencies: 1991 + vite: 7.3.1(jiti@2.6.1)(lightningcss@1.30.2) 1992 + 1993 + zimmerframe@1.1.4: {} 1994 + 1995 + zod@3.25.76: {} 1996 + 1997 + zwitch@2.0.4: {}
+2
packages/svelte-standard-site/pnpm-workspace.yaml
··· 1 + onlyBuiltDependencies: 2 + - esbuild
+181
packages/svelte-standard-site/scripts/test-publisher.js
··· 1 + /** 2 + * Example script to test publishing to ATProto 3 + * 4 + * Usage: 5 + * ATPROTO_APP_PASSWORD="xxxx-xxxx-xxxx-xxxx" node scripts/test-publisher.js 6 + * 7 + * This script demonstrates: 8 + * 1. Creating a publisher instance 9 + * 2. Logging in 10 + * 3. Publishing a publication 11 + * 4. Publishing a document 12 + * 5. Listing your content 13 + */ 14 + 15 + import { StandardSitePublisher } from '../src/lib/publisher.js'; 16 + import { transformContent } from '../src/lib/utils/content.js'; 17 + 18 + async function main() { 19 + // Check for app password 20 + if (!process.env.ATPROTO_APP_PASSWORD) { 21 + console.error('Error: ATPROTO_APP_PASSWORD environment variable not set'); 22 + console.error(''); 23 + console.error('Get an app password at: https://bsky.app/settings/app-passwords'); 24 + console.error(''); 25 + console.error('Usage:'); 26 + console.error(' ATPROTO_APP_PASSWORD="xxxx-xxxx-xxxx-xxxx" node scripts/test-publisher.js'); 27 + process.exit(1); 28 + } 29 + 30 + console.log('🚀 Testing svelte-standard-site Publisher\n'); 31 + 32 + // Step 1: Create publisher 33 + console.log('Step 1: Creating publisher instance...'); 34 + const publisher = new StandardSitePublisher({ 35 + identifier: process.env.ATPROTO_HANDLE || 'your-handle.bsky.social', 36 + password: process.env.ATPROTO_APP_PASSWORD 37 + }); 38 + 39 + // Step 2: Login 40 + console.log('Step 2: Logging in...'); 41 + try { 42 + await publisher.login(); 43 + console.log('✅ Logged in successfully'); 44 + console.log(' DID:', publisher.getDid()); 45 + console.log(' PDS:', publisher.getPdsUrl()); 46 + } catch (error) { 47 + console.error('❌ Login failed:', error.message); 48 + process.exit(1); 49 + } 50 + 51 + // Step 3: Create a publication 52 + console.log('\nStep 3: Creating publication...'); 53 + try { 54 + const pubResult = await publisher.publishPublication({ 55 + name: 'Test Blog', 56 + url: 'https://example.com', 57 + description: 'A test publication created by svelte-standard-site', 58 + basicTheme: { 59 + background: { r: 255, g: 250, b: 240 }, 60 + foreground: { r: 30, g: 30, b: 30 }, 61 + accent: { r: 74, g: 124, b: 155 }, 62 + accentForeground: { r: 255, g: 255, b: 255 } 63 + } 64 + }); 65 + 66 + console.log('✅ Publication created'); 67 + console.log(' URI:', pubResult.uri); 68 + console.log(' CID:', pubResult.cid); 69 + console.log(' Rkey:', pubResult.uri.split('/').pop()); 70 + console.log(''); 71 + console.log(' 💡 Save the rkey for verification!'); 72 + } catch (error) { 73 + console.error('❌ Failed to create publication:', error.message); 74 + } 75 + 76 + // Step 4: Transform and publish a document 77 + console.log('\nStep 4: Publishing a test document...'); 78 + 79 + const sampleMarkdown = ` 80 + # Test Blog Post 81 + 82 + This is a test blog post published using \`svelte-standard-site\`. 83 + 84 + ## Features 85 + 86 + - **Markdown support** - Full markdown formatting 87 + - [Links](/about) are automatically resolved 88 + - Content is transformed for ATProto compatibility 89 + 90 + <div class="sidenote"> 91 + <span class="sidenote-label">Note</span> 92 + <p>This sidenote will be converted to a markdown blockquote!</p> 93 + </div> 94 + 95 + ## Code Example 96 + 97 + \`\`\`typescript 98 + import { StandardSitePublisher } from 'svelte-standard-site/publisher'; 99 + 100 + const publisher = new StandardSitePublisher({ 101 + identifier: 'you.bsky.social', 102 + password: process.env.ATPROTO_APP_PASSWORD! 103 + }); 104 + 105 + await publisher.login(); 106 + \`\`\` 107 + 108 + Pretty cool, right? 109 + `.trim(); 110 + 111 + try { 112 + // Transform the content 113 + const transformed = transformContent(sampleMarkdown, { 114 + baseUrl: 'https://example.com' 115 + }); 116 + 117 + console.log(' Content stats:'); 118 + console.log(' - Word count:', transformed.wordCount); 119 + console.log(' - Reading time:', transformed.readingTime, 'min'); 120 + 121 + // Publish the document 122 + const docResult = await publisher.publishDocument({ 123 + site: 'https://example.com', 124 + title: 'Test Blog Post from svelte-standard-site', 125 + publishedAt: new Date().toISOString(), 126 + path: '/posts/test-post', 127 + description: 'A test document to verify publishing works', 128 + tags: ['test', 'example', 'svelte-standard-site'], 129 + content: { 130 + $type: 'site.standard.content.markdown', 131 + text: transformed.markdown, 132 + version: '1.0' 133 + }, 134 + textContent: transformed.textContent 135 + }); 136 + 137 + console.log('\n✅ Document published'); 138 + console.log(' URI:', docResult.uri); 139 + console.log(' CID:', docResult.cid); 140 + console.log(' Rkey:', docResult.uri.split('/').pop()); 141 + } catch (error) { 142 + console.error('❌ Failed to publish document:', error.message); 143 + } 144 + 145 + // Step 5: List all your content 146 + console.log('\nStep 5: Listing your published content...'); 147 + 148 + try { 149 + const [publications, documents] = await Promise.all([ 150 + publisher.listPublications(), 151 + publisher.listDocuments() 152 + ]); 153 + 154 + console.log(`\n📚 Publications (${publications.length}):`); 155 + publications.forEach((pub, i) => { 156 + console.log(` ${i + 1}. ${pub.value.name}`); 157 + console.log(` URL: ${pub.value.url}`); 158 + console.log(` URI: ${pub.uri}`); 159 + }); 160 + 161 + console.log(`\n📝 Documents (${documents.length}):`); 162 + documents.forEach((doc, i) => { 163 + console.log(` ${i + 1}. ${doc.value.title}`); 164 + console.log(` Published: ${new Date(doc.value.publishedAt).toLocaleDateString()}`); 165 + console.log(` URI: ${doc.uri}`); 166 + }); 167 + } catch (error) { 168 + console.error('❌ Failed to list content:', error.message); 169 + } 170 + 171 + console.log('\n✨ Test complete!'); 172 + console.log('\nNext steps:'); 173 + console.log('1. View your content on pdsls.dev:'); 174 + console.log(` https://pdsls.dev/at://${publisher.getDid()}/site.standard.publication`); 175 + console.log(` https://pdsls.dev/at://${publisher.getDid()}/site.standard.document`); 176 + console.log('2. Set up verification with .well-known endpoints'); 177 + console.log('3. Add Comments component to your blog'); 178 + console.log('\nSee docs/ for more information!'); 179 + } 180 + 181 + main().catch(console.error);
+13
packages/svelte-standard-site/src/app.d.ts
··· 1 + // See https://svelte.dev/docs/kit/types#app.d.ts 2 + // for information about these interfaces 3 + declare global { 4 + namespace App { 5 + // interface Error {} 6 + // interface Locals {} 7 + // interface PageData {} 8 + // interface PageState {} 9 + // interface Platform {} 10 + } 11 + } 12 + 13 + export {};
+12
packages/svelte-standard-site/src/app.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="utf-8" /> 5 + <link rel="icon" href="%sveltekit.assets%/favicon.svg" /> 6 + <meta name="viewport" content="width=device-width, initial-scale=1" /> 7 + %sveltekit.head% 8 + </head> 9 + <body data-sveltekit-preload-data="hover"> 10 + <div style="display: contents">%sveltekit.body%</div> 11 + </body> 12 + </html>
+155
packages/svelte-standard-site/src/lib/__tests__/content.test.ts
··· 1 + /** 2 + * Tests for content transformation utilities 3 + */ 4 + 5 + import { describe, it, expect } from 'vitest'; 6 + import { 7 + convertSidenotes, 8 + resolveRelativeLinks, 9 + stripToPlainText, 10 + countWords, 11 + calculateReadingTime, 12 + transformContent 13 + } from '../utils/content.js'; 14 + 15 + describe('convertSidenotes', () => { 16 + it('converts simple sidenotes to blockquotes', () => { 17 + const input = ` 18 + Some text before. 19 + 20 + <div class="sidenote sidenote--tip"> 21 + <span class="sidenote-label">Tip</span> 22 + <p>This is a helpful tip.</p> 23 + </div> 24 + 25 + Some text after. 26 + `.trim(); 27 + 28 + const expected = ` 29 + Some text before. 30 + 31 + > **Tip:** This is a helpful tip. 32 + 33 + Some text after. 34 + `.trim(); 35 + 36 + expect(convertSidenotes(input)).toBe(expected); 37 + }); 38 + }); 39 + 40 + describe('resolveRelativeLinks', () => { 41 + it('converts relative links to absolute', () => { 42 + const input = ` 43 + [Link to about](/about) 44 + ![Image](/images/photo.jpg) 45 + [External link](https://example.com) should stay the same 46 + `.trim(); 47 + 48 + const expected = ` 49 + [Link to about](https://myblog.com/about) 50 + ![Image](https://myblog.com/images/photo.jpg) 51 + [External link](https://example.com) should stay the same 52 + `.trim(); 53 + 54 + expect(resolveRelativeLinks(input, 'https://myblog.com')).toBe(expected); 55 + }); 56 + 57 + it('handles trailing slash in base URL', () => { 58 + const input = '[Link](/page)'; 59 + const expected = '[Link](https://myblog.com/page)'; 60 + expect(resolveRelativeLinks(input, 'https://myblog.com/')).toBe(expected); 61 + }); 62 + }); 63 + 64 + describe('stripToPlainText', () => { 65 + it('removes markdown formatting', () => { 66 + const input = ` 67 + # Heading 68 + 69 + This is **bold** and *italic* text. 70 + 71 + [Link text](https://example.com) 72 + 73 + \`code\` 74 + 75 + \`\`\` 76 + code block 77 + \`\`\` 78 + `.trim(); 79 + 80 + const result = stripToPlainText(input); 81 + 82 + expect(result).not.toContain('#'); 83 + expect(result).not.toContain('**'); 84 + expect(result).not.toContain('*'); 85 + expect(result).not.toContain('['); 86 + expect(result).not.toContain(']'); 87 + expect(result).not.toContain('`'); 88 + expect(result).toContain('Heading'); 89 + expect(result).toContain('bold'); 90 + expect(result).toContain('italic'); 91 + expect(result).toContain('Link text'); 92 + }); 93 + 94 + it('removes images', () => { 95 + const input = '![Alt text](/image.jpg)'; 96 + const result = stripToPlainText(input); 97 + expect(result).toBe(''); 98 + }); 99 + 100 + it('preserves link text', () => { 101 + const input = '[Click here](https://example.com)'; 102 + const result = stripToPlainText(input); 103 + expect(result).toBe('Click here'); 104 + }); 105 + }); 106 + 107 + describe('countWords', () => { 108 + it('counts words correctly', () => { 109 + expect(countWords('Hello world')).toBe(2); 110 + expect(countWords('One two three four five')).toBe(5); 111 + expect(countWords(' Spaces around words ')).toBe(3); 112 + expect(countWords('')).toBe(0); 113 + }); 114 + }); 115 + 116 + describe('calculateReadingTime', () => { 117 + it('calculates reading time', () => { 118 + expect(calculateReadingTime(200)).toBe(1); // 200 words = 1 minute 119 + expect(calculateReadingTime(400)).toBe(2); // 400 words = 2 minutes 120 + expect(calculateReadingTime(100)).toBe(1); // Always at least 1 minute 121 + expect(calculateReadingTime(0)).toBe(1); // Always at least 1 minute 122 + }); 123 + }); 124 + 125 + describe('transformContent', () => { 126 + it('performs full transformation pipeline', () => { 127 + const input = ` 128 + # My Blog Post 129 + 130 + This is some **markdown** content with [relative links](/about). 131 + 132 + <div class="sidenote"> 133 + <span class="sidenote-label">Note</span> 134 + <p>Important information</p> 135 + </div> 136 + `.trim(); 137 + 138 + const result = transformContent(input, { 139 + baseUrl: 'https://myblog.com' 140 + }); 141 + 142 + // Should have transformed markdown 143 + expect(result.markdown).toContain('(https://myblog.com/about)'); 144 + expect(result.markdown).toContain('> **Note:**'); 145 + 146 + // Should have plain text version 147 + expect(result.textContent).not.toContain('['); 148 + expect(result.textContent).not.toContain('**'); 149 + expect(result.textContent).toContain('markdown'); 150 + 151 + // Should have metadata 152 + expect(result.wordCount).toBeGreaterThan(0); 153 + expect(result.readingTime).toBeGreaterThan(0); 154 + }); 155 + });
+368
packages/svelte-standard-site/src/lib/client.ts
··· 1 + import type { 2 + SiteStandardConfig, 3 + Publication, 4 + Document, 5 + AtProtoRecord, 6 + ResolvedIdentity 7 + } from './types.js'; 8 + import { cache } from './utils/cache.js'; 9 + import { resolveIdentity, withFallback, buildPdsBlobUrl } from './utils/agents.js'; 10 + import { parseAtUri, atUriToHttps } from './utils/at-uri.js'; 11 + 12 + /** 13 + * Main client for interacting with site.standard.* records 14 + */ 15 + export class SiteStandardClient { 16 + private config: Required<SiteStandardConfig>; 17 + private pdsEndpoint: string | null = null; 18 + 19 + constructor(config: SiteStandardConfig) { 20 + this.config = { 21 + did: config.did, 22 + pds: config.pds ?? '', 23 + cacheTTL: config.cacheTTL ?? 5 * 60 * 1000 24 + }; 25 + 26 + // Set cache TTL 27 + cache.setDefaultTTL(this.config.cacheTTL); 28 + } 29 + 30 + /** 31 + * Resolve and cache PDS endpoint 32 + */ 33 + private async resolvePDS(fetchFn?: typeof fetch): Promise<string> { 34 + if (this.pdsEndpoint) return this.pdsEndpoint; 35 + 36 + if (this.config.pds) { 37 + this.pdsEndpoint = this.config.pds; 38 + return this.pdsEndpoint; 39 + } 40 + 41 + const identity = await resolveIdentity(this.config.did, fetchFn); 42 + this.pdsEndpoint = identity.pds; 43 + return this.pdsEndpoint; 44 + } 45 + 46 + /** 47 + * Convert a blob object to a URL string 48 + */ 49 + private async getBlobUrl(blob: any, fetchFn?: typeof fetch): Promise<string | undefined> { 50 + try { 51 + const cid = blob?.ref?.$link || blob?.cid; 52 + if (!cid) return undefined; 53 + 54 + const pds = await this.resolvePDS(fetchFn); 55 + return buildPdsBlobUrl(pds, this.config.did, cid); 56 + } catch (error) { 57 + console.warn('Failed to resolve blob URL:', error); 58 + return undefined; 59 + } 60 + } 61 + 62 + /** 63 + * Fetch a single publication by rkey 64 + * @param rkey - Record key for the publication 65 + * @param fetchFn - Optional fetch function for SSR 66 + * @returns Publication record or null if not found 67 + */ 68 + async fetchPublication( 69 + rkey: string, 70 + fetchFn?: typeof fetch 71 + ): Promise<AtProtoRecord<Publication> | null> { 72 + const cacheKey = `publication:${this.config.did}:${rkey}`; 73 + const cached = cache.get<AtProtoRecord<Publication>>(cacheKey); 74 + if (cached) return cached; 75 + 76 + try { 77 + const result = await withFallback( 78 + this.config.did, 79 + async (agent) => { 80 + const response = await agent.com.atproto.repo.getRecord({ 81 + repo: this.config.did, 82 + collection: 'site.standard.publication', 83 + rkey 84 + }); 85 + return response.data; 86 + }, 87 + fetchFn 88 + ); 89 + 90 + if (!result || !result.value) return null; 91 + 92 + const pubValue = result.value as any; 93 + 94 + // Build the publication object with converted blob URLs 95 + const record: AtProtoRecord<Publication> = { 96 + uri: result.uri, 97 + cid: result.cid || '', 98 + value: { 99 + $type: 'site.standard.publication', 100 + url: pubValue.url, 101 + name: pubValue.name, 102 + icon: pubValue.icon ? await this.getBlobUrl(pubValue.icon, fetchFn) : undefined, 103 + description: pubValue.description, 104 + basicTheme: pubValue.basicTheme, 105 + preferences: pubValue.preferences 106 + } 107 + }; 108 + 109 + cache.set(cacheKey, record); 110 + return record; 111 + } catch (error) { 112 + console.error(`Failed to fetch publication ${rkey}:`, error); 113 + return null; 114 + } 115 + } 116 + 117 + /** 118 + * Fetch all publications for the configured DID 119 + * @param fetchFn - Optional fetch function for SSR 120 + * @returns Array of publication records 121 + */ 122 + async fetchAllPublications(fetchFn?: typeof fetch): Promise<AtProtoRecord<Publication>[]> { 123 + const cacheKey = `publications:${this.config.did}:all`; 124 + const cached = cache.get<AtProtoRecord<Publication>[]>(cacheKey); 125 + if (cached) return cached; 126 + 127 + try { 128 + const allRecords: AtProtoRecord<Publication>[] = []; 129 + let cursor: string | undefined; 130 + 131 + do { 132 + const records = await withFallback( 133 + this.config.did, 134 + async (agent) => { 135 + const response = await agent.com.atproto.repo.listRecords({ 136 + repo: this.config.did, 137 + collection: 'site.standard.publication', 138 + limit: 100, 139 + cursor 140 + }); 141 + cursor = response.data.cursor; 142 + return response.data.records; 143 + }, 144 + fetchFn 145 + ); 146 + 147 + // Convert each record with blob URLs 148 + for (const record of records) { 149 + const pubValue = record.value as any; 150 + const pub: AtProtoRecord<Publication> = { 151 + uri: record.uri, 152 + cid: record.cid || '', 153 + value: { 154 + $type: 'site.standard.publication', 155 + url: pubValue.url, 156 + name: pubValue.name, 157 + icon: pubValue.icon ? await this.getBlobUrl(pubValue.icon, fetchFn) : undefined, 158 + description: pubValue.description, 159 + basicTheme: pubValue.basicTheme, 160 + preferences: pubValue.preferences 161 + } 162 + }; 163 + allRecords.push(pub); 164 + } 165 + } while (cursor); 166 + 167 + cache.set(cacheKey, allRecords); 168 + return allRecords; 169 + } catch (error) { 170 + console.error('Failed to fetch publications:', error); 171 + return []; 172 + } 173 + } 174 + 175 + /** 176 + * Fetch a single document by rkey 177 + * @param rkey - Record key for the document 178 + * @param fetchFn - Optional fetch function for SSR 179 + * @returns Document record or null if not found 180 + */ 181 + async fetchDocument( 182 + rkey: string, 183 + fetchFn?: typeof fetch 184 + ): Promise<AtProtoRecord<Document> | null> { 185 + const cacheKey = `document:${this.config.did}:${rkey}`; 186 + const cached = cache.get<AtProtoRecord<Document>>(cacheKey); 187 + if (cached) return cached; 188 + 189 + try { 190 + const result = await withFallback( 191 + this.config.did, 192 + async (agent) => { 193 + const response = await agent.com.atproto.repo.getRecord({ 194 + repo: this.config.did, 195 + collection: 'site.standard.document', 196 + rkey 197 + }); 198 + return response.data; 199 + }, 200 + fetchFn 201 + ); 202 + 203 + if (!result || !result.value) return null; 204 + 205 + const docValue = result.value as any; 206 + 207 + // Build the document object with converted blob URLs 208 + const record: AtProtoRecord<Document> = { 209 + uri: result.uri, 210 + cid: result.cid || '', 211 + value: { 212 + $type: 'site.standard.document', 213 + site: docValue.site, 214 + title: docValue.title, 215 + path: docValue.path, 216 + description: docValue.description, 217 + coverImage: docValue.coverImage 218 + ? await this.getBlobUrl(docValue.coverImage, fetchFn) 219 + : undefined, 220 + content: docValue.content, 221 + textContent: docValue.textContent, 222 + bskyPostRef: docValue.bskyPostRef, 223 + tags: docValue.tags, 224 + publishedAt: docValue.publishedAt, 225 + updatedAt: docValue.updatedAt 226 + } 227 + }; 228 + 229 + cache.set(cacheKey, record); 230 + return record; 231 + } catch (error) { 232 + console.error(`Failed to fetch document ${rkey}:`, error); 233 + return null; 234 + } 235 + } 236 + 237 + /** 238 + * Fetch all documents for the configured DID 239 + * @param fetchFn - Optional fetch function for SSR 240 + * @returns Array of document records 241 + */ 242 + async fetchAllDocuments(fetchFn?: typeof fetch): Promise<AtProtoRecord<Document>[]> { 243 + const cacheKey = `documents:${this.config.did}:all`; 244 + const cached = cache.get<AtProtoRecord<Document>[]>(cacheKey); 245 + if (cached) return cached; 246 + 247 + try { 248 + const allRecords: AtProtoRecord<Document>[] = []; 249 + let cursor: string | undefined; 250 + 251 + do { 252 + const records = await withFallback( 253 + this.config.did, 254 + async (agent) => { 255 + const response = await agent.com.atproto.repo.listRecords({ 256 + repo: this.config.did, 257 + collection: 'site.standard.document', 258 + limit: 100, 259 + cursor 260 + }); 261 + cursor = response.data.cursor; 262 + return response.data.records; 263 + }, 264 + fetchFn 265 + ); 266 + 267 + // Convert each record with blob URLs 268 + for (const record of records) { 269 + const docValue = record.value as any; 270 + const doc: AtProtoRecord<Document> = { 271 + uri: record.uri, 272 + cid: record.cid || '', 273 + value: { 274 + $type: 'site.standard.document', 275 + site: docValue.site, 276 + title: docValue.title, 277 + path: docValue.path, 278 + description: docValue.description, 279 + coverImage: docValue.coverImage 280 + ? await this.getBlobUrl(docValue.coverImage, fetchFn) 281 + : undefined, 282 + content: docValue.content, 283 + textContent: docValue.textContent, 284 + bskyPostRef: docValue.bskyPostRef, 285 + tags: docValue.tags, 286 + publishedAt: docValue.publishedAt, 287 + updatedAt: docValue.updatedAt 288 + } 289 + }; 290 + allRecords.push(doc); 291 + } 292 + } while (cursor); 293 + 294 + // Sort by publishedAt, newest first 295 + allRecords.sort( 296 + (a, b) => new Date(b.value.publishedAt).getTime() - new Date(a.value.publishedAt).getTime() 297 + ); 298 + 299 + cache.set(cacheKey, allRecords); 300 + return allRecords; 301 + } catch (error) { 302 + console.error('Failed to fetch documents:', error); 303 + return []; 304 + } 305 + } 306 + 307 + /** 308 + * Fetch documents for a specific publication 309 + * @param publicationUri - AT URI of the publication 310 + * @param fetchFn - Optional fetch function for SSR 311 + * @returns Array of document records belonging to the publication 312 + */ 313 + async fetchDocumentsByPublication( 314 + publicationUri: string, 315 + fetchFn?: typeof fetch 316 + ): Promise<AtProtoRecord<Document>[]> { 317 + const allDocs = await this.fetchAllDocuments(fetchFn); 318 + return allDocs.filter((doc) => doc.value.site === publicationUri); 319 + } 320 + 321 + /** 322 + * Fetch a record by AT URI 323 + * @param atUri - Full AT URI (e.g., at://did:plc:xxx/site.standard.publication/rkey) 324 + * @param fetchFn - Optional fetch function for SSR 325 + * @returns Record or null if not found 326 + */ 327 + async fetchByAtUri<T = Publication | Document>( 328 + atUri: string, 329 + fetchFn?: typeof fetch 330 + ): Promise<AtProtoRecord<T> | null> { 331 + const parsed = parseAtUri(atUri); 332 + if (!parsed) { 333 + console.error('Invalid AT URI:', atUri); 334 + return null; 335 + } 336 + 337 + if (parsed.collection === 'site.standard.publication') { 338 + return this.fetchPublication(parsed.rkey, fetchFn) as Promise<AtProtoRecord<T> | null>; 339 + } else if (parsed.collection === 'site.standard.document') { 340 + return this.fetchDocument(parsed.rkey, fetchFn) as Promise<AtProtoRecord<T> | null>; 341 + } 342 + 343 + return null; 344 + } 345 + 346 + /** 347 + * Clear all cached data 348 + */ 349 + clearCache(): void { 350 + cache.clear(); 351 + } 352 + 353 + /** 354 + * Get the resolved PDS endpoint 355 + */ 356 + async getPDS(fetchFn?: typeof fetch): Promise<string> { 357 + return this.resolvePDS(fetchFn); 358 + } 359 + } 360 + 361 + /** 362 + * Create a new SiteStandardClient instance 363 + * @param config - Configuration object 364 + * @returns Configured client instance 365 + */ 366 + export function createClient(config: SiteStandardConfig): SiteStandardClient { 367 + return new SiteStandardClient(config); 368 + }
+277
packages/svelte-standard-site/src/lib/components/Comments.svelte
··· 1 + <script lang="ts"> 2 + /** 3 + * Comments component for displaying Bluesky replies 4 + * 5 + * Fetches and displays threaded replies from Bluesky as comments on your blog. 6 + * 7 + * @example 8 + * ```svelte 9 + * <Comments 10 + * bskyPostUri="at://did:plc:xxx/app.bsky.feed.post/abc123" 11 + * canonicalUrl="https://yourblog.com/posts/my-post" 12 + * maxDepth={3} 13 + * /> 14 + * ``` 15 + */ 16 + 17 + import { onMount } from 'svelte'; 18 + import { fetchComments, formatRelativeTime, type Comment } from '../utils/comments.js'; 19 + import { MessageSquare, ExternalLink, Heart } from '@lucide/svelte'; 20 + 21 + interface Props { 22 + /** AT-URI of the Bluesky announcement post */ 23 + bskyPostUri: string; 24 + /** Canonical URL of your blog post */ 25 + canonicalUrl: string; 26 + /** Maximum nesting depth for replies (default: 3) */ 27 + maxDepth?: number; 28 + /** Section title (default: "Comments") */ 29 + title?: string; 30 + /** Show "Reply on Bluesky" link (default: true) */ 31 + showReplyLink?: boolean; 32 + /** Additional CSS classes */ 33 + class?: string; 34 + } 35 + 36 + let { 37 + bskyPostUri, 38 + canonicalUrl, 39 + maxDepth = 3, 40 + title = 'Comments', 41 + showReplyLink = true, 42 + class: className = '' 43 + }: Props = $props(); 44 + 45 + let comments = $state<Comment[]>([]); 46 + let loading = $state(true); 47 + let error = $state<string | null>(null); 48 + 49 + // Convert AT-URI to HTTPS URL for the reply link 50 + const bskyUrl = $derived.by(() => { 51 + const match = bskyPostUri.match(/^at:\/\/([^/]+)\/app\.bsky\.feed\.post\/(.+)$/); 52 + if (!match) return null; 53 + const [, did, rkey] = match; 54 + // We'll need to resolve the DID to a handle, but for now use DID 55 + return `https://bsky.app/profile/${did}/post/${rkey}`; 56 + }); 57 + 58 + onMount(async () => { 59 + try { 60 + comments = await fetchComments({ 61 + bskyPostUri, 62 + canonicalUrl, 63 + maxDepth 64 + }); 65 + } catch (err) { 66 + error = err instanceof Error ? err.message : 'Failed to load comments'; 67 + console.error('Failed to fetch comments:', err); 68 + } finally { 69 + loading = false; 70 + } 71 + }); 72 + 73 + function CommentItem(comment: Comment) { 74 + const authorUrl = `https://bsky.app/profile/${comment.author.handle}`; 75 + const postUrl = $derived.by(() => { 76 + const match = comment.uri.match(/^at:\/\/([^/]+)\/app\.bsky\.feed\.post\/(.+)$/); 77 + if (!match) return null; 78 + return `https://bsky.app/profile/${comment.author.handle}/post/${match[2]}`; 79 + }); 80 + 81 + return { 82 + comment, 83 + authorUrl, 84 + postUrl 85 + }; 86 + } 87 + </script> 88 + 89 + <section class="comments-section {className}"> 90 + <header class="mb-6"> 91 + <h2 class="text-ink-900 dark:text-ink-50 flex items-center gap-2 text-2xl font-bold"> 92 + <MessageSquare size={24} /> 93 + {title} 94 + </h2> 95 + {#if showReplyLink && bskyUrl} 96 + <a 97 + href={bskyUrl} 98 + target="_blank" 99 + rel="noopener noreferrer" 100 + class="text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 mt-2 inline-flex items-center gap-1 text-sm transition-colors" 101 + > 102 + Reply on Bluesky 103 + <ExternalLink size={14} /> 104 + </a> 105 + {/if} 106 + </header> 107 + 108 + {#if loading} 109 + <div class="text-ink-600 dark:text-ink-400 py-8 text-center"> 110 + <div class="inline-block h-8 w-8 animate-spin rounded-full border-4 border-primary-600 border-t-transparent"></div> 111 + <p class="mt-2">Loading comments...</p> 112 + </div> 113 + {:else if error} 114 + <div class="border-canvas-200 bg-canvas-100 dark:border-canvas-700 dark:bg-canvas-800 rounded-lg border p-4"> 115 + <p class="text-ink-900 dark:text-ink-50 font-medium">Failed to load comments</p> 116 + <p class="text-ink-600 dark:text-ink-400 mt-1 text-sm">{error}</p> 117 + </div> 118 + {:else if comments.length === 0} 119 + <div class="text-ink-600 dark:text-ink-400 py-8 text-center"> 120 + <MessageSquare size={48} class="mx-auto mb-2 opacity-50" /> 121 + <p>No comments yet. Be the first to comment!</p> 122 + {#if showReplyLink && bskyUrl} 123 + <a 124 + href={bskyUrl} 125 + target="_blank" 126 + rel="noopener noreferrer" 127 + class="text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 mt-2 inline-flex items-center gap-1 transition-colors" 128 + > 129 + Start the conversation on Bluesky 130 + <ExternalLink size={14} /> 131 + </a> 132 + {/if} 133 + </div> 134 + {:else} 135 + <div class="space-y-4"> 136 + {#each comments as comment} 137 + {@const item = CommentItem(comment)} 138 + <article 139 + class="border-canvas-200 bg-canvas-50 dark:border-canvas-700 dark:bg-canvas-900 rounded-lg border p-4" 140 + style="margin-left: {comment.depth * 1.5}rem" 141 + > 142 + <header class="mb-2 flex items-start justify-between"> 143 + <div class="flex items-center gap-3"> 144 + {#if comment.author.avatar} 145 + <img 146 + src={comment.author.avatar} 147 + alt={comment.author.displayName || comment.author.handle} 148 + class="h-10 w-10 rounded-full" 149 + /> 150 + {:else} 151 + <div class="bg-primary-200 dark:bg-primary-800 flex h-10 w-10 items-center justify-center rounded-full"> 152 + <span class="text-primary-900 dark:text-primary-100 text-sm font-bold"> 153 + {(comment.author.displayName || comment.author.handle)[0].toUpperCase()} 154 + </span> 155 + </div> 156 + {/if} 157 + 158 + <div> 159 + <a 160 + href={item.authorUrl} 161 + target="_blank" 162 + rel="noopener noreferrer" 163 + class="text-ink-900 dark:text-ink-50 hover:text-primary-600 dark:hover:text-primary-400 font-medium transition-colors" 164 + > 165 + {comment.author.displayName || comment.author.handle} 166 + </a> 167 + <p class="text-ink-600 dark:text-ink-400 text-sm">@{comment.author.handle}</p> 168 + </div> 169 + </div> 170 + 171 + <time class="text-ink-600 dark:text-ink-400 text-sm" datetime={comment.createdAt}> 172 + {formatRelativeTime(comment.createdAt)} 173 + </time> 174 + </header> 175 + 176 + <p class="text-ink-900 dark:text-ink-50 mb-3 whitespace-pre-wrap">{comment.text}</p> 177 + 178 + <footer class="text-ink-600 dark:text-ink-400 flex items-center gap-4 text-sm"> 179 + {#if comment.likeCount > 0} 180 + <span class="flex items-center gap-1"> 181 + <Heart size={14} /> 182 + {comment.likeCount} 183 + </span> 184 + {/if} 185 + 186 + {#if item.postUrl} 187 + <a 188 + href={item.postUrl} 189 + target="_blank" 190 + rel="noopener noreferrer" 191 + class="hover:text-primary-600 dark:hover:text-primary-400 flex items-center gap-1 transition-colors" 192 + > 193 + View on Bluesky 194 + <ExternalLink size={12} /> 195 + </a> 196 + {/if} 197 + </footer> 198 + 199 + {#if comment.replies && comment.replies.length > 0} 200 + <div class="mt-4 space-y-4"> 201 + {#each comment.replies as reply} 202 + {@const replyItem = CommentItem(reply)} 203 + <article 204 + class="border-canvas-200 bg-canvas-100 dark:border-canvas-600 dark:bg-canvas-800 rounded-lg border p-4" 205 + > 206 + <header class="mb-2 flex items-start justify-between"> 207 + <div class="flex items-center gap-3"> 208 + {#if reply.author.avatar} 209 + <img 210 + src={reply.author.avatar} 211 + alt={reply.author.displayName || reply.author.handle} 212 + class="h-8 w-8 rounded-full" 213 + /> 214 + {:else} 215 + <div class="bg-primary-200 dark:bg-primary-800 flex h-8 w-8 items-center justify-center rounded-full"> 216 + <span class="text-primary-900 dark:text-primary-100 text-xs font-bold"> 217 + {(reply.author.displayName || reply.author.handle)[0].toUpperCase()} 218 + </span> 219 + </div> 220 + {/if} 221 + 222 + <div> 223 + <a 224 + href={replyItem.authorUrl} 225 + target="_blank" 226 + rel="noopener noreferrer" 227 + class="text-ink-900 dark:text-ink-50 hover:text-primary-600 dark:hover:text-primary-400 text-sm font-medium transition-colors" 228 + > 229 + {reply.author.displayName || reply.author.handle} 230 + </a> 231 + <p class="text-ink-600 dark:text-ink-400 text-xs">@{reply.author.handle}</p> 232 + </div> 233 + </div> 234 + 235 + <time class="text-ink-600 dark:text-ink-400 text-xs" datetime={reply.createdAt}> 236 + {formatRelativeTime(reply.createdAt)} 237 + </time> 238 + </header> 239 + 240 + <p class="text-ink-900 dark:text-ink-50 mb-2 whitespace-pre-wrap text-sm">{reply.text}</p> 241 + 242 + <footer class="text-ink-600 dark:text-ink-400 flex items-center gap-4 text-xs"> 243 + {#if reply.likeCount > 0} 244 + <span class="flex items-center gap-1"> 245 + <Heart size={12} /> 246 + {reply.likeCount} 247 + </span> 248 + {/if} 249 + 250 + {#if replyItem.postUrl} 251 + <a 252 + href={replyItem.postUrl} 253 + target="_blank" 254 + rel="noopener noreferrer" 255 + class="hover:text-primary-600 dark:hover:text-primary-400 flex items-center gap-1 transition-colors" 256 + > 257 + View on Bluesky 258 + <ExternalLink size={10} /> 259 + </a> 260 + {/if} 261 + </footer> 262 + </article> 263 + {/each} 264 + </div> 265 + {/if} 266 + </article> 267 + {/each} 268 + </div> 269 + {/if} 270 + </section> 271 + 272 + <style> 273 + .comments-section { 274 + margin-top: 3rem; 275 + margin-bottom: 3rem; 276 + } 277 + </style>
+95
packages/svelte-standard-site/src/lib/components/DocumentCard.svelte
··· 1 + <script lang="ts"> 2 + import type { Document, Publication, AtProtoRecord } from '../types.js'; 3 + import { extractRkey } from '$lib/index.js'; 4 + import { ThemedCard, ThemedText, DateDisplay, TagList } from './index.js'; 5 + 6 + interface Props { 7 + document: AtProtoRecord<Document>; 8 + publication?: AtProtoRecord<Publication>; 9 + class?: string; 10 + showCover?: boolean; 11 + href?: string; 12 + } 13 + 14 + const { 15 + document, 16 + publication, 17 + class: className = '', 18 + showCover = true, 19 + href: customHref 20 + }: Props = $props(); 21 + 22 + const value = $derived(document.value); 23 + const hasTheme = $derived(!!publication?.value.basicTheme); 24 + 25 + const pubRkey = $derived(extractRkey(value.site)); 26 + const docRkey = $derived(extractRkey(document.uri)); 27 + const href = $derived(customHref || `/${pubRkey}/${docRkey}`); 28 + </script> 29 + 30 + <ThemedCard 31 + theme={publication?.value.basicTheme} 32 + {href} 33 + class="flex gap-6 duration-200 focus-within:shadow-md hover:shadow-md {className}" 34 + > 35 + {#if showCover && value.coverImage} 36 + <img 37 + src={value.coverImage} 38 + alt="{value.title} cover" 39 + class="h-48 w-32 shrink-0 rounded-lg object-cover shadow-sm" 40 + /> 41 + {/if} 42 + 43 + <div class="flex-1"> 44 + <ThemedText 45 + {hasTheme} 46 + element="h3" 47 + class="group-hover:text-primary-600 dark:group-hover:text-primary-400 mb-2 text-2xl font-bold transition-colors" 48 + > 49 + {value.title} 50 + </ThemedText> 51 + 52 + {#if value.description} 53 + <ThemedText 54 + {hasTheme} 55 + opacity={70} 56 + element="p" 57 + class="mb-4 line-clamp-3 text-sm leading-relaxed" 58 + > 59 + {value.description} 60 + </ThemedText> 61 + {/if} 62 + 63 + <div class="mb-4 flex flex-wrap items-center gap-x-4 gap-y-2 text-sm"> 64 + <DateDisplay 65 + date={value.publishedAt} 66 + class="font-medium" 67 + style={hasTheme 68 + ? `color: ${hasTheme ? 'color-mix(in srgb, var(--theme-foreground) 80%, transparent)' : ''}` 69 + : ''} 70 + /> 71 + {#if value.updatedAt} 72 + <span 73 + class="flex items-center gap-1" 74 + style:color={hasTheme 75 + ? 'color-mix(in srgb, var(--theme-foreground) 60%, transparent)' 76 + : undefined} 77 + > 78 + <span 79 + class="h-1 w-1 rounded-full" 80 + class:bg-ink-400={!hasTheme} 81 + class:dark:bg-ink-600={!hasTheme} 82 + style:background-color={hasTheme 83 + ? 'color-mix(in srgb, var(--theme-foreground) 40%, transparent)' 84 + : undefined} 85 + ></span> 86 + Updated <DateDisplay date={value.updatedAt} /> 87 + </span> 88 + {/if} 89 + </div> 90 + 91 + {#if value.tags && value.tags.length > 0} 92 + <TagList tags={value.tags} {hasTheme} /> 93 + {/if} 94 + </div> 95 + </ThemedCard>
+54
packages/svelte-standard-site/src/lib/components/PublicationCard.svelte
··· 1 + <script lang="ts"> 2 + import type { Publication, AtProtoRecord } from '../types.js'; 3 + import { ThemedCard, ThemedText } from './index.js'; 4 + import { ExternalLink } from '@lucide/svelte'; 5 + 6 + interface Props { 7 + publication: AtProtoRecord<Publication>; 8 + class?: string; 9 + showExternalIcon?: boolean; 10 + } 11 + 12 + const { publication, class: className = '', showExternalIcon = true }: Props = $props(); 13 + 14 + const value = $derived(publication.value); 15 + const hasTheme = $derived(!!value.basicTheme); 16 + </script> 17 + 18 + <ThemedCard theme={value.basicTheme} class="focus-within:shadow-lg hover:shadow-lg {className}"> 19 + <div class="mb-4 flex gap-4"> 20 + {#if value.icon} 21 + <img 22 + src={value.icon} 23 + alt="{value.name} icon" 24 + class="size-16 shrink-0 rounded-lg object-cover" 25 + /> 26 + {/if} 27 + <div class="min-w-0 flex-1"> 28 + <ThemedText {hasTheme} element="h3" class="mb-2 text-xl font-semibold"> 29 + {value.name} 30 + </ThemedText> 31 + {#if value.description} 32 + <ThemedText {hasTheme} opacity={70} element="p" class="text-sm"> 33 + {value.description} 34 + </ThemedText> 35 + {/if} 36 + </div> 37 + </div> 38 + <a 39 + href={value.url} 40 + target="_blank" 41 + rel="noopener noreferrer" 42 + class="inline-flex items-center gap-2 font-medium transition-opacity hover:opacity-80 focus-visible:opacity-80 focus-visible:outline-2 focus-visible:outline-offset-2" 43 + class:text-primary-600={!hasTheme} 44 + class:dark:text-primary-400={!hasTheme} 45 + class:focus-visible:outline-primary-600={!hasTheme} 46 + style:color={hasTheme ? 'var(--theme-accent)' : undefined} 47 + style:outline-color={hasTheme ? 'var(--theme-accent)' : undefined} 48 + > 49 + Visit Site 50 + {#if showExternalIcon} 51 + <ExternalLink class="size-4" aria-hidden="true" /> 52 + {/if} 53 + </a> 54 + </ThemedCard>
+102
packages/svelte-standard-site/src/lib/components/StandardSiteLayout.svelte
··· 1 + <script lang="ts"> 2 + import type { Snippet } from 'svelte'; 3 + import ThemeToggle from './ThemeToggle.svelte'; 4 + 5 + interface Props { 6 + /** Site title */ 7 + title?: string; 8 + /** Header slot for custom header content */ 9 + header?: Snippet; 10 + /** Footer slot for custom footer content */ 11 + footer?: Snippet; 12 + /** Main content */ 13 + children: Snippet; 14 + /** Additional CSS classes for the main container */ 15 + class?: string; 16 + /** Show theme toggle in default header */ 17 + showThemeToggle?: boolean; 18 + } 19 + 20 + let { 21 + title = 'My Site', 22 + header, 23 + footer, 24 + children, 25 + class: className = '', 26 + showThemeToggle = true 27 + }: Props = $props(); 28 + </script> 29 + 30 + <svelte:head> 31 + <script> 32 + // Prevent flash of unstyled content (FOUC) by applying theme before page renders 33 + (function () { 34 + const stored = localStorage.getItem('theme'); 35 + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; 36 + const isDark = stored === 'dark' || (!stored && prefersDark); 37 + const htmlElement = document.documentElement; 38 + 39 + if (isDark) { 40 + htmlElement.classList.add('dark'); 41 + htmlElement.style.colorScheme = 'dark'; 42 + } else { 43 + htmlElement.classList.remove('dark'); 44 + htmlElement.style.colorScheme = 'light'; 45 + } 46 + })(); 47 + </script> 48 + </svelte:head> 49 + 50 + <div 51 + class="bg-canvas-50 text-ink-900 dark:bg-canvas-950 dark:text-ink-50 flex min-h-screen flex-col overflow-x-hidden" 52 + > 53 + {#if header} 54 + {@render header()} 55 + {:else} 56 + <header 57 + class="border-canvas-200 bg-canvas-50/90 dark:border-canvas-800 dark:bg-canvas-950/90 sticky top-0 z-50 w-full border-b backdrop-blur-md" 58 + > 59 + <nav 60 + class="container mx-auto flex items-center justify-between px-4 py-4" 61 + aria-label="Main navigation" 62 + > 63 + <a 64 + href="/" 65 + class="text-ink-900 hover:text-primary-600 focus-visible:text-primary-600 focus-visible:outline-primary-600 dark:text-ink-50 dark:hover:text-primary-400 dark:focus-visible:text-primary-400 text-xl font-bold transition-colors focus-visible:outline-2 focus-visible:outline-offset-2" 66 + > 67 + {title} 68 + </a> 69 + 70 + {#if showThemeToggle} 71 + <ThemeToggle /> 72 + {/if} 73 + </nav> 74 + </header> 75 + {/if} 76 + 77 + <main id="main-content" class="container mx-auto grow px-4 py-8 {className}" tabindex="-1"> 78 + {@render children()} 79 + </main> 80 + 81 + {#if footer} 82 + {@render footer()} 83 + {:else} 84 + <footer 85 + class="border-canvas-200 bg-canvas-50 dark:border-canvas-800 dark:bg-canvas-950 mt-auto w-full border-t py-6" 86 + > 87 + <div class="text-ink-700 dark:text-ink-200 container mx-auto px-4 text-center text-sm"> 88 + <p> 89 + &copy; {new Date().getFullYear()} 90 + {title}. Powered by svelte-standard-site and the AT Protocol. Created by 91 + <a 92 + href="https://ewancroft.uk" 93 + target="_blank" 94 + rel="noopener noreferrer" 95 + class="text-primary-600 hover:text-primary-700 focus-visible:outline-primary-600 dark:text-primary-400 dark:hover:text-primary-500 font-semibold transition focus-visible:outline-2 focus-visible:outline-offset-2" 96 + >Ewan Croft</a 97 + >. 98 + </p> 99 + </div> 100 + </footer> 101 + {/if} 102 + </div>
+55
packages/svelte-standard-site/src/lib/components/ThemeToggle.svelte
··· 1 + <script lang="ts"> 2 + import { onMount } from 'svelte'; 3 + import { Sun, Moon } from '@lucide/svelte'; 4 + import { themeStore } from '../stores/index.js'; 5 + 6 + interface Props { 7 + class?: string; 8 + } 9 + 10 + let { class: className = '' }: Props = $props(); 11 + 12 + let isDark = $state(false); 13 + let mounted = $state(false); 14 + 15 + onMount(() => { 16 + themeStore.init(); 17 + 18 + const unsubscribe = themeStore.subscribe((state) => { 19 + isDark = state.isDark; 20 + mounted = state.mounted; 21 + }); 22 + 23 + return unsubscribe; 24 + }); 25 + 26 + function toggle() { 27 + themeStore.toggle(); 28 + } 29 + </script> 30 + 31 + <button 32 + onclick={toggle} 33 + class="bg-canvas-200 text-ink-900 hover:bg-canvas-300 focus-visible:outline-primary-600 dark:bg-canvas-800 dark:text-ink-50 dark:hover:bg-canvas-700 relative flex h-10 w-10 items-center justify-center rounded-lg transition-all focus-visible:outline-2 focus-visible:outline-offset-2 {className}" 34 + aria-label={isDark ? 'Switch to light mode' : 'Switch to dark mode'} 35 + type="button" 36 + > 37 + {#if mounted} 38 + <div class="relative h-5 w-5"> 39 + <Sun 40 + class="absolute inset-0 h-5 w-5 transition-all duration-300 {isDark 41 + ? 'scale-0 -rotate-90 opacity-0' 42 + : 'scale-100 rotate-0 opacity-100'}" 43 + aria-hidden="true" 44 + /> 45 + <Moon 46 + class="absolute inset-0 h-5 w-5 transition-all duration-300 {isDark 47 + ? 'scale-100 rotate-0 opacity-100' 48 + : 'scale-0 rotate-90 opacity-0'}" 49 + aria-hidden="true" 50 + /> 51 + </div> 52 + {:else} 53 + <div class="bg-canvas-300 dark:bg-canvas-700 h-5 w-5 animate-pulse rounded"></div> 54 + {/if} 55 + </button>
+38
packages/svelte-standard-site/src/lib/components/common/DateDisplay.svelte
··· 1 + <script lang="ts"> 2 + interface Props { 3 + date: string; 4 + label?: string; 5 + class?: string; 6 + showIcon?: boolean; 7 + style?: string; 8 + locale?: string; 9 + } 10 + 11 + let { date, label, class: className = '', showIcon = false, style, locale }: Props = $props(); 12 + 13 + function formatDate(dateString: string): string { 14 + // Use browser's locale if not specified 15 + const userLocale = locale || navigator.language || 'en-US'; 16 + 17 + return new Date(dateString).toLocaleDateString(userLocale, { 18 + year: 'numeric', 19 + month: 'long', 20 + day: 'numeric' 21 + }); 22 + } 23 + </script> 24 + 25 + <time datetime={date} class={className} {style}> 26 + {#if showIcon} 27 + <svg class="inline size-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 28 + <path 29 + stroke-linecap="round" 30 + stroke-linejoin="round" 31 + stroke-width="2" 32 + d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" 33 + /> 34 + </svg> 35 + {/if} 36 + {#if label}{label}{/if} 37 + {formatDate(date)} 38 + </time>
+31
packages/svelte-standard-site/src/lib/components/common/TagList.svelte
··· 1 + <script lang="ts"> 2 + import { getThemedAccent } from '$lib/utils/theme-helpers.js'; 3 + 4 + interface Props { 5 + tags: string[]; 6 + hasTheme?: boolean; 7 + class?: string; 8 + } 9 + 10 + let { tags, hasTheme = false, class: className = '' }: Props = $props(); 11 + 12 + const accentStyles = $derived(getThemedAccent(hasTheme, 20)); 13 + </script> 14 + 15 + {#if tags.length > 0} 16 + <div class="flex flex-wrap gap-2 {className}"> 17 + {#each tags as tag} 18 + <span 19 + class="rounded-full px-4 py-2 text-sm font-medium transition-all hover:scale-105" 20 + class:bg-primary-50={!hasTheme} 21 + class:text-primary-700={!hasTheme} 22 + class:dark:bg-primary-950={!hasTheme} 23 + class:dark:text-primary-300={!hasTheme} 24 + style:background-color={accentStyles.backgroundColor} 25 + style:color={hasTheme ? accentStyles.color : undefined} 26 + > 27 + #{tag} 28 + </span> 29 + {/each} 30 + </div> 31 + {/if}
+65
packages/svelte-standard-site/src/lib/components/common/ThemedCard.svelte
··· 1 + <script lang="ts"> 2 + import type { BasicTheme } from '$lib/types.js'; 3 + import { getThemeVars } from '$lib/utils/theme.js'; 4 + import { getThemedBorder } from '$lib/utils/theme-helpers.js'; 5 + import type { Snippet } from 'svelte'; 6 + 7 + interface Props { 8 + theme?: BasicTheme; 9 + children: Snippet; 10 + class?: string; 11 + href?: string; 12 + } 13 + 14 + let { theme, children, class: className = '', href }: Props = $props(); 15 + 16 + const themeVars = $derived(theme ? getThemeVars(theme) : {}); 17 + const hasTheme = $derived(!!theme); 18 + const borderStyles = $derived(getThemedBorder(hasTheme)); 19 + 20 + const styles = $derived( 21 + Object.entries(themeVars) 22 + .map(([k, v]) => `${k}:${v}`) 23 + .join(';') 24 + ); 25 + 26 + const allStyles = $derived(() => { 27 + const base = styles; 28 + if (borderStyles.borderColor) { 29 + return `${base};border-color:${borderStyles.borderColor}`; 30 + } 31 + return base; 32 + }); 33 + </script> 34 + 35 + {#if href} 36 + <a {href} class="group block"> 37 + <article 38 + class="rounded-lg border p-6 transition-all {className}" 39 + class:bg-canvas-50={!hasTheme} 40 + class:dark:bg-canvas-950={!hasTheme} 41 + class:border-canvas-200={!hasTheme} 42 + class:dark:border-canvas-800={!hasTheme} 43 + class:hover:border-primary-300={!hasTheme} 44 + class:dark:hover:border-primary-700={!hasTheme} 45 + class:focus-within:border-primary-300={!hasTheme} 46 + class:dark:focus-within:border-primary-700={!hasTheme} 47 + style:background-color={hasTheme ? 'var(--theme-background)' : undefined} 48 + style={allStyles()} 49 + > 50 + {@render children()} 51 + </article> 52 + </a> 53 + {:else} 54 + <article 55 + class="rounded-lg border p-6 transition-all {className}" 56 + class:bg-canvas-50={!hasTheme} 57 + class:dark:bg-canvas-950={!hasTheme} 58 + class:border-canvas-200={!hasTheme} 59 + class:dark:border-canvas-800={!hasTheme} 60 + style:background-color={hasTheme ? 'var(--theme-background)' : undefined} 61 + style={allStyles()} 62 + > 63 + {@render children()} 64 + </article> 65 + {/if}
+55
packages/svelte-standard-site/src/lib/components/common/ThemedContainer.svelte
··· 1 + <script lang="ts"> 2 + import type { BasicTheme } from '$lib/types.js'; 3 + import { getThemeVars } from '$lib/utils/theme.js'; 4 + import type { Snippet } from 'svelte'; 5 + 6 + interface Props { 7 + theme?: BasicTheme; 8 + children: Snippet; 9 + class?: string; 10 + element?: 'div' | 'article' | 'section'; 11 + } 12 + 13 + let { theme, children, class: className = '', element = 'div' }: Props = $props(); 14 + 15 + const themeVars = $derived(theme ? getThemeVars(theme) : {}); 16 + const hasTheme = $derived(!!theme); 17 + 18 + const styles = $derived( 19 + Object.entries(themeVars) 20 + .map(([k, v]) => `${k}:${v}`) 21 + .join(';') 22 + ); 23 + </script> 24 + 25 + {#if element === 'article'} 26 + <article 27 + class={className} 28 + class:bg-canvas-50={!hasTheme} 29 + class:dark:bg-canvas-950={!hasTheme} 30 + style:background-color={hasTheme ? 'var(--theme-background)' : undefined} 31 + style={styles} 32 + > 33 + {@render children()} 34 + </article> 35 + {:else if element === 'section'} 36 + <section 37 + class={className} 38 + class:bg-canvas-50={!hasTheme} 39 + class:dark:bg-canvas-950={!hasTheme} 40 + style:background-color={hasTheme ? 'var(--theme-background)' : undefined} 41 + style={styles} 42 + > 43 + {@render children()} 44 + </section> 45 + {:else} 46 + <div 47 + class={className} 48 + class:bg-canvas-50={!hasTheme} 49 + class:dark:bg-canvas-950={!hasTheme} 50 + style:background-color={hasTheme ? 'var(--theme-background)' : undefined} 51 + style={styles} 52 + > 53 + {@render children()} 54 + </div> 55 + {/if}
+75
packages/svelte-standard-site/src/lib/components/common/ThemedText.svelte
··· 1 + <script lang="ts"> 2 + import { getThemedTextColor, getThemedAccent } from '$lib/utils/theme-helpers.js'; 3 + 4 + interface Props { 5 + hasTheme?: boolean; 6 + opacity?: number; 7 + variant?: 'foreground' | 'accent'; 8 + element?: 'span' | 'p' | 'h1' | 'h2' | 'h3' | 'div'; 9 + class?: string; 10 + children?: any; 11 + } 12 + 13 + let { 14 + hasTheme = false, 15 + opacity = 100, 16 + variant = 'foreground', 17 + element = 'span', 18 + class: className = '', 19 + children 20 + }: Props = $props(); 21 + 22 + const colorStyles = $derived( 23 + variant === 'accent' ? getThemedAccent(hasTheme) : getThemedTextColor(hasTheme, opacity) 24 + ); 25 + 26 + // Default Tailwind classes when no theme 27 + const defaultClasses = $derived(() => { 28 + if (hasTheme) return ''; 29 + 30 + if (variant === 'accent') { 31 + return 'text-primary-600 dark:text-primary-400'; 32 + } 33 + 34 + switch (opacity) { 35 + case 100: 36 + return 'text-ink-900 dark:text-ink-50'; 37 + case 80: 38 + return 'text-ink-800 dark:text-ink-100'; 39 + case 70: 40 + return 'text-ink-700 dark:text-ink-200'; 41 + case 60: 42 + return 'text-ink-600 dark:text-ink-400'; 43 + case 50: 44 + return 'text-ink-500 dark:text-ink-500'; 45 + default: 46 + return 'text-ink-700 dark:text-ink-200'; 47 + } 48 + }); 49 + </script> 50 + 51 + {#if element === 'p'} 52 + <p class="{defaultClasses()} {className}" style:color={colorStyles.color}> 53 + {@render children?.()} 54 + </p> 55 + {:else if element === 'h1'} 56 + <h1 class="{defaultClasses()} {className}" style:color={colorStyles.color}> 57 + {@render children?.()} 58 + </h1> 59 + {:else if element === 'h2'} 60 + <h2 class="{defaultClasses()} {className}" style:color={colorStyles.color}> 61 + {@render children?.()} 62 + </h2> 63 + {:else if element === 'h3'} 64 + <h3 class="{defaultClasses()} {className}" style:color={colorStyles.color}> 65 + {@render children?.()} 66 + </h3> 67 + {:else if element === 'div'} 68 + <div class="{defaultClasses()} {className}" style:color={colorStyles.color}> 69 + {@render children?.()} 70 + </div> 71 + {:else} 72 + <span class="{defaultClasses()} {className}" style:color={colorStyles.color}> 73 + {@render children?.()} 74 + </span> 75 + {/if}
+66
packages/svelte-standard-site/src/lib/components/document/BlockRenderer.svelte
··· 1 + <script lang="ts"> 2 + import TextBlock from './blocks/TextBlock.svelte'; 3 + import HeaderBlock from './blocks/HeaderBlock.svelte'; 4 + import BlockquoteBlock from './blocks/BlockquoteBlock.svelte'; 5 + import ImageBlock from './blocks/ImageBlock.svelte'; 6 + import CodeBlock from './blocks/CodeBlock.svelte'; 7 + import MathBlock from './blocks/MathBlock.svelte'; 8 + import UnorderedListBlock from './blocks/UnorderedListBlock.svelte'; 9 + import HorizontalRuleBlock from './blocks/HorizontalRuleBlock.svelte'; 10 + import IframeBlock from './blocks/IframeBlock.svelte'; 11 + import WebsiteBlock from './blocks/WebsiteBlock.svelte'; 12 + import ButtonBlock from './blocks/ButtonBlock.svelte'; 13 + import BskyPostBlock from './blocks/BskyPostBlock.svelte'; 14 + import PollBlock from './blocks/PollBlock.svelte'; 15 + import PageBlock from './blocks/PageBlock.svelte'; 16 + 17 + interface Props { 18 + block: any; 19 + hasTheme?: boolean; 20 + } 21 + 22 + const { block, hasTheme = false }: Props = $props(); 23 + </script> 24 + 25 + {#if block.$type === 'pub.leaflet.blocks.text'} 26 + <TextBlock {block} {hasTheme} /> 27 + {:else if block.$type === 'pub.leaflet.blocks.header'} 28 + <HeaderBlock {block} {hasTheme} /> 29 + {:else if block.$type === 'pub.leaflet.blocks.blockquote'} 30 + <BlockquoteBlock {block} {hasTheme} /> 31 + {:else if block.$type === 'pub.leaflet.blocks.image'} 32 + <ImageBlock {block} {hasTheme} /> 33 + {:else if block.$type === 'pub.leaflet.blocks.code'} 34 + <CodeBlock {block} {hasTheme} /> 35 + {:else if block.$type === 'pub.leaflet.blocks.math'} 36 + <MathBlock {block} {hasTheme} /> 37 + {:else if block.$type === 'pub.leaflet.blocks.unorderedList'} 38 + <UnorderedListBlock {block} {hasTheme} /> 39 + {:else if block.$type === 'pub.leaflet.blocks.horizontalRule'} 40 + <HorizontalRuleBlock {hasTheme} /> 41 + {:else if block.$type === 'pub.leaflet.blocks.iframe'} 42 + <IframeBlock {block} {hasTheme} /> 43 + {:else if block.$type === 'pub.leaflet.blocks.website'} 44 + <WebsiteBlock {block} {hasTheme} /> 45 + {:else if block.$type === 'pub.leaflet.blocks.button'} 46 + <ButtonBlock {block} {hasTheme} /> 47 + {:else if block.$type === 'pub.leaflet.blocks.bskyPost'} 48 + <BskyPostBlock {block} {hasTheme} /> 49 + {:else if block.$type === 'pub.leaflet.blocks.poll'} 50 + <PollBlock {block} {hasTheme} /> 51 + {:else if block.$type === 'pub.leaflet.blocks.page'} 52 + <PageBlock {block} {hasTheme} /> 53 + {:else} 54 + <!-- Unknown block type --> 55 + <div class="my-2 rounded-lg border border-orange-500/20 bg-orange-500/5 p-3"> 56 + <p class="text-sm text-orange-600 dark:text-orange-400"> 57 + Unknown block type: <code class="font-mono text-xs">{block.$type}</code> 58 + </p> 59 + <details class="mt-2"> 60 + <summary class="cursor-pointer text-xs text-orange-600/70 dark:text-orange-400/70"> 61 + Show block data 62 + </summary> 63 + <pre class="mt-2 overflow-x-auto text-xs">{JSON.stringify(block, null, 2)}</pre> 64 + </details> 65 + </div> 66 + {/if}
+40
packages/svelte-standard-site/src/lib/components/document/CanvasRenderer.svelte
··· 1 + <script lang="ts"> 2 + import BlockRenderer from './BlockRenderer.svelte'; 3 + 4 + interface CanvasPage { 5 + $type: 'pub.leaflet.pages.canvas'; 6 + id?: string; 7 + blocks: Array<{ 8 + $type: 'pub.leaflet.pages.canvas#block'; 9 + block: any; 10 + x: number; 11 + y: number; 12 + width: number; 13 + height?: number; 14 + rotation?: number; 15 + }>; 16 + } 17 + 18 + interface Props { 19 + page: CanvasPage; 20 + hasTheme?: boolean; 21 + } 22 + 23 + const { page, hasTheme = false }: Props = $props(); 24 + </script> 25 + 26 + <!-- Canvas layout uses absolute positioning --> 27 + <div class="relative min-h-screen w-full"> 28 + {#each page.blocks as blockWrapper, index} 29 + <div 30 + class="absolute" 31 + style:left="{blockWrapper.x}px" 32 + style:top="{blockWrapper.y}px" 33 + style:width="{blockWrapper.width}px" 34 + style:height={blockWrapper.height ? `${blockWrapper.height}px` : 'auto'} 35 + style:transform={blockWrapper.rotation ? `rotate(${blockWrapper.rotation}deg)` : undefined} 36 + > 37 + <BlockRenderer block={blockWrapper.block} {hasTheme} /> 38 + </div> 39 + {/each} 40 + </div>
+55
packages/svelte-standard-site/src/lib/components/document/DocumentRenderer.svelte
··· 1 + <script lang="ts"> 2 + import type { Document } from '$lib/types.js'; 3 + import LeafletContentRenderer from './LeafletContentRenderer.svelte'; 4 + import { mixThemeColor } from '$lib/utils/theme-helpers.js'; 5 + 6 + interface Props { 7 + document: Document; 8 + hasTheme?: boolean; 9 + } 10 + 11 + const { document, hasTheme = false }: Props = $props(); 12 + 13 + // Determine if we should render textContent or content 14 + const shouldRenderTextContent = $derived(!!document.textContent); 15 + const shouldRenderLeafletContent = $derived( 16 + !document.textContent && document.content && document.content.$type === 'pub.leaflet.content' 17 + ); 18 + </script> 19 + 20 + <div 21 + class="prose prose-lg max-w-none" 22 + style:color={hasTheme ? 'var(--theme-foreground)' : undefined} 23 + > 24 + {#if shouldRenderTextContent} 25 + <!-- Simple text content with proper whitespace handling --> 26 + <div class="leading-relaxed whitespace-pre-wrap">{document.textContent}</div> 27 + {:else if shouldRenderLeafletContent} 28 + <!-- Render the rich Leaflet content --> 29 + <LeafletContentRenderer content={document.content} {hasTheme} /> 30 + {:else if document.content} 31 + <!-- Fallback: show raw content for unknown types --> 32 + <div 33 + class="rounded-xl border p-6" 34 + style:border-color={hasTheme ? mixThemeColor('--theme-foreground', 20) : undefined} 35 + style:background-color={hasTheme ? mixThemeColor('--theme-foreground', 5) : undefined} 36 + > 37 + <p 38 + class="mb-3 text-sm font-semibold tracking-wider uppercase" 39 + style:color={hasTheme ? mixThemeColor('--theme-foreground', 60) : undefined} 40 + > 41 + Raw Content 42 + </p> 43 + <pre 44 + class="overflow-x-auto text-xs leading-relaxed" 45 + style:color={hasTheme ? 'var(--theme-foreground)' : undefined}>{JSON.stringify( 46 + document.content, 47 + null, 48 + 2 49 + )}</pre> 50 + </div> 51 + {:else} 52 + <!-- No content at all --> 53 + <p class="italic opacity-50">No content available</p> 54 + {/if} 55 + </div>
+41
packages/svelte-standard-site/src/lib/components/document/InlineMath.svelte
··· 1 + <script lang="ts"> 2 + import { onMount } from 'svelte'; 3 + 4 + interface Props { 5 + tex: string; 6 + hasTheme?: boolean; 7 + } 8 + 9 + const { tex, hasTheme = false }: Props = $props(); 10 + 11 + let mathContainer = $state<HTMLSpanElement>(); 12 + let renderError = $state<string | null>(null); 13 + 14 + onMount(async () => { 15 + try { 16 + // Dynamically import KaTeX 17 + const katex = await import('katex'); 18 + await import('katex/dist/katex.min.css'); 19 + 20 + if (mathContainer) { 21 + katex.default.render(tex, mathContainer, { 22 + displayMode: false, // Inline mode 23 + throwOnError: false, 24 + errorColor: '#ef4444', 25 + trust: false 26 + }); 27 + } 28 + } catch (error) { 29 + console.error('Failed to render inline LaTeX:', error); 30 + renderError = error instanceof Error ? error.message : 'Failed to render LaTeX'; 31 + } 32 + }); 33 + </script> 34 + 35 + {#if renderError} 36 + <span class="rounded bg-red-100 px-1 text-xs text-red-700 dark:bg-red-950/50 dark:text-red-400" 37 + >Error: {tex}</span 38 + > 39 + {:else} 40 + <span bind:this={mathContainer} class="inline-block align-middle"></span> 41 + {/if}
+63
packages/svelte-standard-site/src/lib/components/document/LeafletContentRenderer.svelte
··· 1 + <script lang="ts"> 2 + import LinearDocumentRenderer from './LinearDocumentRenderer.svelte'; 3 + import CanvasRenderer from './CanvasRenderer.svelte'; 4 + 5 + interface LinearDocumentPage { 6 + $type: 'pub.leaflet.pages.linearDocument'; 7 + id?: string; 8 + blocks: Array<{ 9 + $type: 'pub.leaflet.pages.linearDocument#block'; 10 + block: any; 11 + alignment?: string; 12 + }>; 13 + } 14 + 15 + interface CanvasPage { 16 + $type: 'pub.leaflet.pages.canvas'; 17 + id?: string; 18 + blocks: Array<{ 19 + $type: 'pub.leaflet.pages.canvas#block'; 20 + block: any; 21 + x: number; 22 + y: number; 23 + width: number; 24 + height?: number; 25 + rotation?: number; 26 + }>; 27 + } 28 + 29 + type Page = LinearDocumentPage | CanvasPage; 30 + 31 + interface LeafletContent { 32 + $type: 'pub.leaflet.content'; 33 + pages: Page[]; 34 + } 35 + 36 + interface Props { 37 + content: LeafletContent; 38 + hasTheme?: boolean; 39 + } 40 + 41 + const { content, hasTheme = false }: Props = $props(); 42 + </script> 43 + 44 + {#if content.pages && content.pages.length > 0} 45 + {#each content.pages as page, index} 46 + {#if page.$type === 'pub.leaflet.pages.linearDocument'} 47 + <LinearDocumentRenderer {page} {hasTheme} /> 48 + {:else if page.$type === 'pub.leaflet.pages.canvas'} 49 + <CanvasRenderer {page} {hasTheme} /> 50 + {:else} 51 + <!-- Unknown page type --> 52 + <div class="my-4 rounded-lg border border-yellow-500/20 bg-yellow-500/5 p-4"> 53 + <p class="text-sm text-yellow-600 dark:text-yellow-400"> 54 + Unknown page type: <code class="font-mono text-xs" 55 + >{(page as any).$type || 'unknown'}</code 56 + > 57 + </p> 58 + </div> 59 + {/if} 60 + {/each} 61 + {:else} 62 + <p class="italic opacity-50">No pages found in content</p> 63 + {/if}
+44
packages/svelte-standard-site/src/lib/components/document/LinearDocumentRenderer.svelte
··· 1 + <script lang="ts"> 2 + import BlockRenderer from './BlockRenderer.svelte'; 3 + 4 + interface LinearDocumentPage { 5 + $type: 'pub.leaflet.pages.linearDocument'; 6 + id?: string; 7 + blocks: Array<{ 8 + $type: 'pub.leaflet.pages.linearDocument#block'; 9 + block: any; 10 + alignment?: string; 11 + }>; 12 + } 13 + 14 + interface Props { 15 + page: LinearDocumentPage; 16 + hasTheme?: boolean; 17 + } 18 + 19 + const { page, hasTheme = false }: Props = $props(); 20 + 21 + function getAlignmentClass(alignment?: string): string { 22 + if (!alignment) return ''; 23 + switch (alignment) { 24 + case '#textAlignLeft': 25 + return 'text-left'; 26 + case '#textAlignCenter': 27 + return 'text-center'; 28 + case '#textAlignRight': 29 + return 'text-right'; 30 + case '#textAlignJustify': 31 + return 'text-justify'; 32 + default: 33 + return ''; 34 + } 35 + } 36 + </script> 37 + 38 + <div class="space-y-6"> 39 + {#each page.blocks as blockWrapper, index} 40 + <div class={getAlignmentClass(blockWrapper.alignment)}> 41 + <BlockRenderer block={blockWrapper.block} {hasTheme} /> 42 + </div> 43 + {/each} 44 + </div>
+272
packages/svelte-standard-site/src/lib/components/document/RichText.svelte
··· 1 + <script lang="ts"> 2 + import InlineMath from './InlineMath.svelte'; 3 + import { UnicodeString } from '@atproto/api'; 4 + 5 + interface Facet { 6 + index: { 7 + byteStart: number; 8 + byteEnd: number; 9 + }; 10 + features: Array<{ 11 + $type: string; 12 + [key: string]: any; 13 + }>; 14 + } 15 + 16 + interface Props { 17 + plaintext: string; 18 + facets?: Facet[]; 19 + hasTheme?: boolean; 20 + } 21 + 22 + const { plaintext, facets = [], hasTheme = false }: Props = $props(); 23 + 24 + interface RichTextSegment { 25 + text: string; 26 + facet?: Array<{ $type: string; [key: string]: any }>; 27 + } 28 + 29 + class RichText { 30 + unicodeText: UnicodeString; 31 + facets: Facet[]; 32 + 33 + constructor(props: { text: string; facets: Facet[] }) { 34 + this.unicodeText = new UnicodeString(props.text || ''); 35 + this.facets = props.facets || []; 36 + if (this.facets) { 37 + this.facets = this.facets 38 + .filter((facet) => facet.index.byteStart <= facet.index.byteEnd) 39 + .sort((a, b) => a.index.byteStart - b.index.byteStart); 40 + } 41 + } 42 + 43 + *segments(): Generator<RichTextSegment, void, void> { 44 + const facets = this.facets || []; 45 + if (!facets.length) { 46 + yield { text: this.unicodeText.utf16 || '' }; 47 + return; 48 + } 49 + 50 + let textCursor = 0; 51 + let facetCursor = 0; 52 + do { 53 + const currFacet = facets[facetCursor]; 54 + if (textCursor < currFacet.index.byteStart) { 55 + const sliced = this.unicodeText.slice(textCursor, currFacet.index.byteStart); 56 + yield { 57 + text: sliced || '' 58 + }; 59 + } else if (textCursor > currFacet.index.byteStart) { 60 + facetCursor++; 61 + continue; 62 + } 63 + if (currFacet.index.byteStart < currFacet.index.byteEnd) { 64 + const subtext = this.unicodeText.slice( 65 + currFacet.index.byteStart, 66 + currFacet.index.byteEnd 67 + ); 68 + const subtextStr = subtext || ''; 69 + if (!subtextStr.trim()) { 70 + // don't emit empty string entities 71 + yield { text: subtextStr }; 72 + } else { 73 + yield { text: subtextStr, facet: currFacet.features }; 74 + } 75 + } 76 + textCursor = currFacet.index.byteEnd; 77 + facetCursor++; 78 + } while (facetCursor < facets.length); 79 + if (textCursor < this.unicodeText.length) { 80 + const sliced = this.unicodeText.slice(textCursor, this.unicodeText.length); 81 + yield { 82 + text: sliced || '' 83 + }; 84 + } 85 + } 86 + } 87 + 88 + interface ProcessedSegment { 89 + parts: Array<{ text: string; isBr: boolean }>; 90 + isBold: boolean; 91 + isItalic: boolean; 92 + isUnderline: boolean; 93 + isStrikethrough: boolean; 94 + isCode: boolean; 95 + isHighlighted: boolean; 96 + isMath: boolean; 97 + isDidMention: boolean; 98 + isAtMention: boolean; 99 + link?: string; 100 + id?: string; 101 + did?: string; 102 + atURI?: string; 103 + } 104 + 105 + function processSegments(): ProcessedSegment[] { 106 + // Handle undefined or empty plaintext 107 + const text = plaintext || ''; 108 + const richText = new RichText({ text, facets }); 109 + const result: ProcessedSegment[] = []; 110 + 111 + for (const segment of richText.segments()) { 112 + const id = segment.facet?.find((f) => f.$type === 'pub.leaflet.richtext.facet#id'); 113 + const link = segment.facet?.find((f) => f.$type === 'pub.leaflet.richtext.facet#link'); 114 + const isBold = segment.facet?.some((f) => f.$type === 'pub.leaflet.richtext.facet#bold'); 115 + const isCode = segment.facet?.some((f) => f.$type === 'pub.leaflet.richtext.facet#code'); 116 + const isStrikethrough = segment.facet?.some( 117 + (f) => f.$type === 'pub.leaflet.richtext.facet#strikethrough' 118 + ); 119 + const isDidMention = segment.facet?.find( 120 + (f) => f.$type === 'pub.leaflet.richtext.facet#didMention' 121 + ); 122 + const isAtMention = segment.facet?.find( 123 + (f) => f.$type === 'pub.leaflet.richtext.facet#atMention' 124 + ); 125 + const isUnderline = segment.facet?.some( 126 + (f) => f.$type === 'pub.leaflet.richtext.facet#underline' 127 + ); 128 + const isItalic = segment.facet?.some((f) => f.$type === 'pub.leaflet.richtext.facet#italic'); 129 + const isHighlighted = segment.facet?.some( 130 + (f) => f.$type === 'pub.leaflet.richtext.facet#highlight' 131 + ); 132 + const isMath = segment.facet?.some((f) => f.$type === 'pub.leaflet.richtext.facet#math'); 133 + 134 + // Split text by newlines and mark br elements - handle undefined segment.text 135 + const segmentText = segment.text || ''; 136 + const textParts = segmentText.split('\n'); 137 + const parts = textParts.flatMap((part, i) => 138 + i < textParts.length - 1 139 + ? [ 140 + { text: part, isBr: false }, 141 + { text: '', isBr: true } 142 + ] 143 + : [{ text: part, isBr: false }] 144 + ); 145 + 146 + result.push({ 147 + parts, 148 + isBold: isBold || false, 149 + isItalic: isItalic || false, 150 + isUnderline: isUnderline || false, 151 + isStrikethrough: isStrikethrough || false, 152 + isCode: isCode || false, 153 + isHighlighted: isHighlighted || false, 154 + isMath: isMath || false, 155 + isDidMention: !!isDidMention, 156 + isAtMention: !!isAtMention, 157 + link: link?.uri, 158 + id: id?.id, 159 + did: isDidMention?.did, 160 + atURI: isAtMention?.atURI 161 + }); 162 + } 163 + 164 + return result; 165 + } 166 + 167 + const segments = $derived(processSegments()); 168 + </script> 169 + 170 + {#each segments as segment, i} 171 + {#each segment.parts as part, j} 172 + {#if part.isBr} 173 + <br /> 174 + {:else if segment.isMath} 175 + <InlineMath tex={part.text} {hasTheme} /> 176 + {:else} 177 + {@const classes = [ 178 + segment.isCode ? 'inline-code' : '', 179 + segment.id ? 'scroll-mt-12 scroll-mb-10' : '', 180 + segment.isBold ? 'font-bold' : '', 181 + segment.isItalic ? 'italic' : '', 182 + segment.isUnderline ? 'underline' : '', 183 + segment.isStrikethrough ? 'line-through decoration-tertiary' : '', 184 + segment.isHighlighted ? 'highlight' : '' 185 + ] 186 + .filter(Boolean) 187 + .join(' ')} 188 + 189 + {#if segment.isCode} 190 + <code class={classes} id={segment.id}>{part.text}</code> 191 + {:else if segment.isDidMention} 192 + <a 193 + href={`https://leaflet.pub/p/${segment.did}`} 194 + target="_blank" 195 + rel="noopener noreferrer" 196 + class="no-underline" 197 + > 198 + <span class="mention {classes}" class:themed={hasTheme}>{part.text}</span> 199 + </a> 200 + {:else if segment.isAtMention} 201 + <a 202 + href={segment.atURI} 203 + target="_blank" 204 + rel="noopener noreferrer" 205 + class="hover:underline {classes}" 206 + class:themed={hasTheme} 207 + > 208 + {part.text} 209 + </a> 210 + {:else if segment.link} 211 + <a 212 + href={segment.link.trim()} 213 + class="hover:underline {classes}" 214 + class:themed={hasTheme} 215 + target="_blank" 216 + rel="noopener noreferrer" 217 + > 218 + {part.text} 219 + </a> 220 + {:else} 221 + <span class={classes} id={segment.id}>{part.text}</span> 222 + {/if} 223 + {/if} 224 + {/each} 225 + {/each} 226 + 227 + <style> 228 + .mention { 229 + cursor: pointer; 230 + color: rgb(0 0 225); 231 + padding: 0 0.125rem; 232 + border-radius: 0.25rem; 233 + background-color: color-mix(in oklab, rgb(0 0 225), transparent 80%); 234 + border: 1px solid transparent; 235 + display: inline; 236 + white-space: normal; 237 + } 238 + 239 + .mention.themed { 240 + color: var(--theme-accent); 241 + background-color: color-mix(in oklab, var(--theme-accent), transparent 80%); 242 + } 243 + 244 + a { 245 + color: rgb(0 0 225); 246 + } 247 + 248 + a.themed { 249 + color: var(--theme-accent); 250 + } 251 + 252 + .inline-code { 253 + display: inline; 254 + font-size: 1em; 255 + background-color: color-mix(in oklab, currentColor, transparent 90%); 256 + font-family: ui-monospace, monospace; 257 + padding: 1px; 258 + margin: -1px; 259 + border-radius: 4px; 260 + box-decoration-break: clone; 261 + -webkit-box-decoration-break: clone; 262 + } 263 + 264 + .highlight { 265 + padding: 1px; 266 + margin: -1px; 267 + border-radius: 4px; 268 + box-decoration-break: clone; 269 + -webkit-box-decoration-break: clone; 270 + background-color: rgb(255, 177, 177); 271 + } 272 + </style>
+29
packages/svelte-standard-site/src/lib/components/document/blocks/BlockquoteBlock.svelte
··· 1 + <script lang="ts"> 2 + import RichText from '../RichText.svelte'; 3 + 4 + interface Props { 5 + block: { 6 + plaintext: string; 7 + facets?: any[]; 8 + }; 9 + hasTheme?: boolean; 10 + } 11 + 12 + const { block, hasTheme = false }: Props = $props(); 13 + </script> 14 + 15 + <blockquote class="blockquote mt-1 mb-2" class:themed={hasTheme}> 16 + <RichText plaintext={block.plaintext} facets={block.facets} {hasTheme} /> 17 + </blockquote> 18 + 19 + <style> 20 + .blockquote { 21 + border-left: 2px solid rgb(107 114 128); /* Default gray color */ 22 + padding-left: 0.75rem; 23 + margin-left: 0.5rem; 24 + } 25 + 26 + .blockquote.themed { 27 + border-color: var(--theme-accent); 28 + } 29 + </style>
+202
packages/svelte-standard-site/src/lib/components/document/blocks/BskyPostBlock.svelte
··· 1 + <script lang="ts"> 2 + import { onMount } from 'svelte'; 3 + 4 + interface Props { 5 + block: { 6 + postRef: { 7 + uri: string; 8 + cid: string; 9 + }; 10 + }; 11 + hasTheme?: boolean; 12 + postData?: any; // The full post data if already fetched 13 + } 14 + 15 + const { block, hasTheme = false, postData }: Props = $props(); 16 + 17 + let post = $state<any>(null); 18 + let loading = $state(true); 19 + let error = $state<string | null>(null); 20 + 21 + // Extract post info from AT URI 22 + function extractPostInfo(uri: string): { did: string; rkey: string } | null { 23 + const match = uri.match(/^at:\/\/([^/]+)\/app\.bsky\.feed\.post\/(.+)$/); 24 + if (!match) return null; 25 + return { did: match[1], rkey: match[2] }; 26 + } 27 + 28 + const postInfo = $derived(extractPostInfo(block.postRef.uri)); 29 + const postUrl = $derived( 30 + postInfo ? `https://bsky.app/profile/${postInfo.did}/post/${postInfo.rkey}` : null 31 + ); 32 + 33 + onMount(async () => { 34 + // Use postData if provided 35 + if (postData) { 36 + post = postData; 37 + loading = false; 38 + return; 39 + } 40 + 41 + // You would fetch the post data here from your API or Bluesky API 42 + // For now, we'll just show a simple link 43 + loading = false; 44 + }); 45 + </script> 46 + 47 + {#if loading} 48 + <div 49 + class="relative my-2 flex w-full flex-col gap-2 overflow-hidden rounded-md border bg-white p-3 text-sm dark:bg-gray-900" 50 + style:border-color={hasTheme ? 'var(--theme-accent)' : undefined} 51 + class:border-gray-200={!hasTheme} 52 + class:dark:border-gray-700={!hasTheme} 53 + > 54 + <div class="animate-pulse"> 55 + <div class="mb-2 h-8 w-8 rounded-full bg-gray-200 dark:bg-gray-700"></div> 56 + <div class="mb-2 h-4 w-3/4 rounded bg-gray-200 dark:bg-gray-700"></div> 57 + <div class="h-4 w-1/2 rounded bg-gray-200 dark:bg-gray-700"></div> 58 + </div> 59 + </div> 60 + {:else if error} 61 + <div 62 + class="relative my-2 flex w-full flex-col gap-2 overflow-hidden rounded-md border border-red-200 bg-red-50 p-3 text-sm dark:border-red-800 dark:bg-red-950" 63 + > 64 + <p class="text-red-600 dark:text-red-400">Failed to load Bluesky post: {error}</p> 65 + </div> 66 + {:else if post && post.author && post.record} 67 + <div 68 + class="relative my-2 flex w-full flex-col gap-2 overflow-hidden rounded-md border bg-white p-3 text-sm dark:bg-gray-900" 69 + style:border-color={hasTheme ? 'var(--theme-accent)' : undefined} 70 + class:border-gray-200={!hasTheme} 71 + class:dark:border-gray-700={!hasTheme} 72 + > 73 + <div class="flex w-full items-center gap-2"> 74 + {#if post.author.avatar} 75 + <img 76 + src={post.author.avatar} 77 + alt="{post.author.displayName}'s avatar" 78 + class="h-8 w-8 shrink-0 rounded-full border border-gray-200 dark:border-gray-700" 79 + /> 80 + {/if} 81 + <div class="flex grow flex-col gap-0.5 leading-tight"> 82 + <div class="font-bold text-gray-900 dark:text-gray-100"> 83 + {post.author.displayName} 84 + </div> 85 + <a 86 + href="https://bsky.app/profile/{post.author.handle}" 87 + target="_blank" 88 + rel="noopener noreferrer" 89 + class="text-xs text-gray-600 hover:underline dark:text-gray-400" 90 + > 91 + @{post.author.handle} 92 + </a> 93 + </div> 94 + </div> 95 + 96 + <div class="flex flex-col gap-2"> 97 + {#if post.record.text} 98 + <pre class="whitespace-pre-wrap text-gray-900 dark:text-gray-100">{post.record.text}</pre> 99 + {/if} 100 + 101 + {#if post.embed} 102 + <div> 103 + <!-- Embed rendering would go here --> 104 + <div class="text-sm text-gray-600 italic dark:text-gray-400">[Embedded content]</div> 105 + </div> 106 + {/if} 107 + </div> 108 + 109 + <div class="flex w-full items-center justify-between gap-2 text-gray-600 dark:text-gray-400"> 110 + {#if post.record.createdAt} 111 + <div class="text-xs"> 112 + {new Date(post.record.createdAt).toLocaleDateString('en-US', { 113 + month: 'short', 114 + day: 'numeric', 115 + year: 'numeric', 116 + hour: 'numeric', 117 + minute: 'numeric', 118 + hour12: true 119 + })} 120 + </div> 121 + {/if} 122 + <div class="flex items-center gap-2"> 123 + {#if post.replyCount != null && post.replyCount > 0} 124 + <span class="flex items-center gap-1 text-xs"> 125 + <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 126 + <path 127 + stroke-linecap="round" 128 + stroke-linejoin="round" 129 + stroke-width="2" 130 + d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" 131 + /> 132 + </svg> 133 + {post.replyCount} 134 + </span> 135 + {/if} 136 + {#if post.quoteCount != null && post.quoteCount > 0} 137 + <span class="flex items-center gap-1 text-xs"> 138 + <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 139 + <path 140 + stroke-linecap="round" 141 + stroke-linejoin="round" 142 + stroke-width="2" 143 + d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" 144 + /> 145 + </svg> 146 + {post.quoteCount} 147 + </span> 148 + {/if} 149 + <a 150 + href={postUrl} 151 + target="_blank" 152 + rel="noopener noreferrer" 153 + class="transition-opacity hover:opacity-70" 154 + title="View on Bluesky" 155 + > 156 + <svg class="h-4 w-4" fill="currentColor" viewBox="0 0 24 24"> 157 + <path 158 + d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.039.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 0 1-.415-.056c.14.017.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8Z" 159 + /> 160 + </svg> 161 + </a> 162 + </div> 163 + </div> 164 + </div> 165 + {:else if postUrl} 166 + <div 167 + class="my-2 rounded-md border bg-white p-6 dark:bg-gray-900" 168 + style:border-color={hasTheme ? 'var(--theme-accent)' : undefined} 169 + class:border-gray-200={!hasTheme} 170 + class:dark:border-gray-700={!hasTheme} 171 + > 172 + <div class="mb-3 flex items-center gap-2 text-sm font-medium"> 173 + <svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"> 174 + <path 175 + d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.039.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 0 1-.415-.056c.14.017.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8Z" 176 + /> 177 + </svg> 178 + <span style:color={hasTheme ? 'var(--theme-accent)' : undefined}> Bluesky Post </span> 179 + </div> 180 + <a 181 + href={postUrl} 182 + target="_blank" 183 + rel="noopener noreferrer" 184 + class="inline-flex items-center gap-2 text-sm font-medium transition-all hover:gap-3" 185 + style:color={hasTheme ? 'var(--theme-accent)' : undefined} 186 + > 187 + View on Bluesky 188 + <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 189 + <path 190 + stroke-linecap="round" 191 + stroke-linejoin="round" 192 + stroke-width="2" 193 + d="M14 5l7 7m0 0l-7 7m7-7H3" 194 + /> 195 + </svg> 196 + </a> 197 + </div> 198 + {:else} 199 + <div class="my-4 rounded-lg border border-yellow-500/20 bg-yellow-500/5 p-4"> 200 + <p class="text-sm text-yellow-600 dark:text-yellow-400">Invalid Bluesky post reference</p> 201 + </div> 202 + {/if}
+24
packages/svelte-standard-site/src/lib/components/document/blocks/ButtonBlock.svelte
··· 1 + <script lang="ts"> 2 + interface Props { 3 + block: { 4 + text: string; 5 + url: string; 6 + }; 7 + hasTheme?: boolean; 8 + } 9 + 10 + const { block, hasTheme = false }: Props = $props(); 11 + </script> 12 + 13 + <div class="my-2"> 14 + <a 15 + href={block.url.trim()} 16 + target="_blank" 17 + rel="noopener noreferrer" 18 + class="inline-block rounded-md px-6 py-3 font-semibold transition-all hover:opacity-90" 19 + style:background-color={hasTheme ? 'var(--theme-accent)' : '#3b82f6'} 20 + style:color="white" 21 + > 22 + {block.text} 23 + </a> 24 + </div>
+68
packages/svelte-standard-site/src/lib/components/document/blocks/CodeBlock.svelte
··· 1 + <script lang="ts"> 2 + import { onMount } from 'svelte'; 3 + 4 + interface Props { 5 + block: { 6 + plaintext: string; 7 + language?: string; 8 + syntaxHighlightingTheme?: string; 9 + }; 10 + hasTheme?: boolean; 11 + prerenderedCode?: string; 12 + } 13 + 14 + const { block, hasTheme = false, prerenderedCode }: Props = $props(); 15 + 16 + let html = $state<string | null>(null); 17 + 18 + onMount(async () => { 19 + // Use prerendered code if available 20 + if (prerenderedCode) { 21 + html = prerenderedCode; 22 + return; 23 + } 24 + 25 + try { 26 + const { codeToHtml, bundledLanguagesInfo, bundledThemesInfo } = await import('shiki'); 27 + 28 + const lang = 29 + bundledLanguagesInfo.find((l: any) => l.id === block.language)?.id || 'plaintext'; 30 + const theme = 31 + bundledThemesInfo.find((t: any) => t.id === block.syntaxHighlightingTheme)?.id || 32 + 'github-light'; 33 + 34 + html = await codeToHtml(block.plaintext, { lang, theme }); 35 + } catch (error) { 36 + console.error('Failed to highlight code:', error); 37 + // Fallback to plain text 38 + html = `<pre><code>${block.plaintext}</code></pre>`; 39 + } 40 + }); 41 + </script> 42 + 43 + {#if html} 44 + <div 45 + class="my-2 min-h-[42px] w-full rounded-md border" 46 + style:border-color={hasTheme ? 'var(--theme-accent)' : undefined} 47 + class:border-gray-200={!hasTheme} 48 + class:dark:border-gray-700={!hasTheme} 49 + > 50 + {@html html} 51 + </div> 52 + {:else} 53 + <div 54 + class="my-2 min-h-[42px] w-full rounded-md border" 55 + style:border-color={hasTheme ? 'var(--theme-accent)' : undefined} 56 + class:border-gray-200={!hasTheme} 57 + class:dark:border-gray-700={!hasTheme} 58 + > 59 + <pre class="p-4"><code>{block.plaintext}</code></pre> 60 + </div> 61 + {/if} 62 + 63 + <style> 64 + :global(.shiki) { 65 + padding: 1rem; 66 + overflow-x: auto; 67 + } 68 + </style>
+56
packages/svelte-standard-site/src/lib/components/document/blocks/HeaderBlock.svelte
··· 1 + <script lang="ts"> 2 + import RichText from '../RichText.svelte'; 3 + 4 + interface Props { 5 + block: { 6 + plaintext: string; 7 + level?: number; 8 + facets?: any[]; 9 + }; 10 + hasTheme?: boolean; 11 + } 12 + 13 + const { block, hasTheme = false }: Props = $props(); 14 + 15 + const level = $derived(block.level || 1); 16 + </script> 17 + 18 + {#if level === 1} 19 + <h2 class="h1Block mt-1 mb-2"> 20 + <RichText plaintext={block.plaintext} facets={block.facets} {hasTheme} /> 21 + </h2> 22 + {:else if level === 2} 23 + <h3 class="h2Block mt-1 mb-2"> 24 + <RichText plaintext={block.plaintext} facets={block.facets} {hasTheme} /> 25 + </h3> 26 + {:else if level === 3} 27 + <h4 class="h3Block mt-1 mb-2"> 28 + <RichText plaintext={block.plaintext} facets={block.facets} {hasTheme} /> 29 + </h4> 30 + {:else} 31 + <h6 class="h6Block mt-1 mb-2"> 32 + <RichText plaintext={block.plaintext} facets={block.facets} {hasTheme} /> 33 + </h6> 34 + {/if} 35 + 36 + <style> 37 + .h1Block { 38 + font-size: 2rem; 39 + font-weight: bold; 40 + } 41 + 42 + .h2Block { 43 + font-size: 1.625rem; 44 + font-weight: bold; 45 + } 46 + 47 + .h3Block { 48 + font-size: 1.125rem; 49 + font-weight: bold; 50 + } 51 + 52 + .h6Block { 53 + font-size: 1rem; 54 + font-weight: bold; 55 + } 56 + </style>
+14
packages/svelte-standard-site/src/lib/components/document/blocks/HorizontalRuleBlock.svelte
··· 1 + <script lang="ts"> 2 + interface Props { 3 + hasTheme?: boolean; 4 + } 5 + 6 + const { hasTheme = false }: Props = $props(); 7 + </script> 8 + 9 + <hr 10 + class="my-2 border-t" 11 + style:border-color={hasTheme ? 'var(--theme-accent)' : undefined} 12 + class:border-gray-200={!hasTheme} 13 + class:dark:border-gray-700={!hasTheme} 14 + />
+32
packages/svelte-standard-site/src/lib/components/document/blocks/IframeBlock.svelte
··· 1 + <script lang="ts"> 2 + interface Props { 3 + block: { 4 + url: string; 5 + height?: number; 6 + }; 7 + hasTheme?: boolean; 8 + } 9 + 10 + const { block, hasTheme = false }: Props = $props(); 11 + 12 + const height = $derived(block.height || 360); 13 + </script> 14 + 15 + <div class="my-2 w-full"> 16 + <div 17 + class="overflow-hidden rounded-md border" 18 + style:border-color={hasTheme ? 'var(--theme-accent)' : undefined} 19 + class:border-gray-200={!hasTheme} 20 + class:dark:border-gray-700={!hasTheme} 21 + > 22 + <iframe 23 + width="100%" 24 + {height} 25 + src={block.url} 26 + allow="fullscreen" 27 + loading="lazy" 28 + title="Embedded content" 29 + class="border-0" 30 + ></iframe> 31 + </div> 32 + </div>
+37
packages/svelte-standard-site/src/lib/components/document/blocks/ImageBlock.svelte
··· 1 + <script lang="ts"> 2 + interface Props { 3 + block: { 4 + image: { 5 + src: string; 6 + height?: number; 7 + width?: number; 8 + fallback?: string; 9 + local?: boolean; 10 + }; 11 + alt?: string; 12 + }; 13 + hasTheme?: boolean; 14 + } 15 + 16 + const { block, hasTheme = false }: Props = $props(); 17 + 18 + const imageSrc = $derived(block.image.fallback || block.image.src); 19 + const altText = $derived(block.alt || ''); 20 + </script> 21 + 22 + <div class="my-2 w-fit"> 23 + <img 24 + src={imageSrc} 25 + alt={altText} 26 + loading="lazy" 27 + decoding="async" 28 + height={block.image.height} 29 + width={block.image.width} 30 + class="h-auto max-w-full" 31 + /> 32 + {#if altText} 33 + <div class="mt-1 text-xs text-gray-600 italic dark:text-gray-400"> 34 + {altText} 35 + </div> 36 + {/if} 37 + </div>
+34
packages/svelte-standard-site/src/lib/components/document/blocks/MathBlock.svelte
··· 1 + <script lang="ts"> 2 + import { onMount } from 'svelte'; 3 + import katex from 'katex'; 4 + import 'katex/dist/katex.min.css'; 5 + 6 + interface Props { 7 + block: { 8 + tex: string; 9 + }; 10 + hasTheme?: boolean; 11 + } 12 + 13 + const { block, hasTheme = false }: Props = $props(); 14 + 15 + let html = $state(''); 16 + 17 + onMount(() => { 18 + html = katex.renderToString(block.tex, { 19 + displayMode: true, 20 + output: 'html', 21 + throwOnError: false 22 + }); 23 + }); 24 + </script> 25 + 26 + <div class="math-block my-2"> 27 + {@html html} 28 + </div> 29 + 30 + <style> 31 + .math-block :global(.katex-display) { 32 + margin: 0; 33 + } 34 + </style>
+66
packages/svelte-standard-site/src/lib/components/document/blocks/PageBlock.svelte
··· 1 + <script lang="ts"> 2 + interface Props { 3 + block: { 4 + pageId: string; 5 + }; 6 + hasTheme?: boolean; 7 + pages?: any[]; // Array of page data if available 8 + } 9 + 10 + const { block, hasTheme = false, pages }: Props = $props(); 11 + 12 + // Find the referenced page 13 + const referencedPage = $derived(pages?.find((p) => p.id === block.pageId)); 14 + </script> 15 + 16 + {#if referencedPage} 17 + <div 18 + class="relative my-2 flex h-[104px] w-full overflow-clip rounded-md border bg-white dark:bg-gray-900" 19 + style:border-color={hasTheme ? 'var(--theme-accent)' : undefined} 20 + class:border-gray-200={!hasTheme} 21 + class:dark:border-gray-700={!hasTheme} 22 + > 23 + <div class="flex h-full w-full overflow-clip"> 24 + <div class="my-2 ml-3 flex min-w-0 grow flex-col overflow-clip bg-transparent text-sm"> 25 + <div class="grow"> 26 + {#if referencedPage.title} 27 + <div class="line-clamp-1 text-base font-bold"> 28 + {referencedPage.title} 29 + </div> 30 + {/if} 31 + {#if referencedPage.description} 32 + <div class="line-clamp-2 text-sm text-gray-600 dark:text-gray-400"> 33 + {referencedPage.description} 34 + </div> 35 + {/if} 36 + </div> 37 + </div> 38 + <div 39 + class="m-2 -mb-2 w-[120px] shrink-0 origin-center rotate-[4deg] rounded-t-md border border-gray-200 bg-gradient-to-br from-gray-50 to-gray-100 dark:border-gray-700 dark:from-gray-800 dark:to-gray-900" 40 + ></div> 41 + </div> 42 + </div> 43 + {:else} 44 + <div 45 + class="my-2 rounded-md border bg-white p-6 dark:bg-gray-900" 46 + style:border-color={hasTheme ? 'var(--theme-accent)' : undefined} 47 + class:border-gray-200={!hasTheme} 48 + class:dark:border-gray-700={!hasTheme} 49 + > 50 + <div class="mb-3 flex items-center gap-2 text-sm font-medium"> 51 + <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> 52 + <path 53 + stroke-linecap="round" 54 + stroke-linejoin="round" 55 + d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" 56 + /> 57 + </svg> 58 + <span>Page Reference</span> 59 + </div> 60 + <p class="text-sm opacity-70"> 61 + Links to page: <code class="rounded bg-gray-100 px-1 py-0.5 text-xs dark:bg-gray-800" 62 + >{block.pageId}</code 63 + > 64 + </p> 65 + </div> 66 + {/if}
+122
packages/svelte-standard-site/src/lib/components/document/blocks/PollBlock.svelte
··· 1 + <script lang="ts"> 2 + interface PollOption { 3 + text: string; 4 + } 5 + 6 + interface PollVote { 7 + voter_did: string; 8 + record: { 9 + option: string[]; 10 + }; 11 + } 12 + 13 + interface Props { 14 + block: { 15 + pollRef: { 16 + uri: string; 17 + cid: string; 18 + }; 19 + }; 20 + pollData?: { 21 + record: { 22 + options: PollOption[]; 23 + }; 24 + atp_poll_votes: PollVote[]; 25 + }; 26 + hasTheme?: boolean; 27 + } 28 + 29 + const { block, pollData, hasTheme = false }: Props = $props(); 30 + 31 + const totalVotes = $derived(pollData?.atp_poll_votes.length || 0); 32 + 33 + function getVoteOption(voteRecord: any): string | null { 34 + try { 35 + return voteRecord.option && voteRecord.option.length > 0 ? voteRecord.option[0] : null; 36 + } catch { 37 + return null; 38 + } 39 + } 40 + 41 + function getVotesForOption(optionIndex: number): number { 42 + if (!pollData) return 0; 43 + return pollData.atp_poll_votes.filter((v) => getVoteOption(v.record) === optionIndex.toString()) 44 + .length; 45 + } 46 + 47 + function getHighestVotes(): number { 48 + if (!pollData) return 0; 49 + const options = pollData.record.options; 50 + return Math.max(...options.map((_, i) => getVotesForOption(i))); 51 + } 52 + </script> 53 + 54 + {#if !pollData} 55 + <div 56 + class="my-2 rounded-md border bg-white p-6 dark:bg-gray-900" 57 + style:border-color={hasTheme ? 'var(--theme-accent)' : undefined} 58 + class:border-gray-200={!hasTheme} 59 + class:dark:border-gray-700={!hasTheme} 60 + > 61 + <div class="mb-3 flex items-center gap-2 text-sm font-medium"> 62 + <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> 63 + <path 64 + stroke-linecap="round" 65 + stroke-linejoin="round" 66 + d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" 67 + /> 68 + </svg> 69 + <span>Poll Reference</span> 70 + </div> 71 + <p class="text-sm opacity-70"> 72 + Poll: <code class="rounded bg-gray-100 px-1 py-0.5 text-xs dark:bg-gray-800" 73 + >{block.pollRef.uri}</code 74 + > 75 + </p> 76 + </div> 77 + {:else} 78 + <div 79 + class="poll my-2 flex w-full flex-col gap-2 rounded-md border p-3" 80 + style:border-color={hasTheme ? 'var(--theme-accent)' : undefined} 81 + style:background-color={hasTheme 82 + ? 'color-mix(in oklab, var(--theme-accent), white 85%)' 83 + : '#f0f9ff'} 84 + class:border-gray-200={!hasTheme} 85 + class:dark:border-gray-700={!hasTheme} 86 + > 87 + <!-- Poll Results (Read-only) --> 88 + {#each pollData.record.options as option, index} 89 + {@const votes = getVotesForOption(index)} 90 + {@const isWinner = totalVotes > 0 && votes === getHighestVotes()} 91 + {@const percentage = totalVotes > 0 ? (votes / totalVotes) * 100 : 0} 92 + 93 + <div 94 + class="pollResult relative grow overflow-hidden rounded-md px-2 py-0.5 {isWinner 95 + ? 'border-2 border-blue-600 font-bold dark:border-blue-400' 96 + : 'border border-blue-600 dark:border-blue-400'}" 97 + > 98 + <div 99 + class="pollResultContent relative z-10 flex justify-between gap-2" 100 + style="color: {hasTheme ? 'var(--theme-accent)' : '#1e40af'};" 101 + > 102 + <div class="max-w-full grow truncate">{option.text}</div> 103 + <div class="tabular-nums">{votes}</div> 104 + </div> 105 + <div class="pollResultBG absolute top-0 right-0 bottom-0 left-0 z-0 flex w-full flex-row"> 106 + <div 107 + class="m-0.5 rounded-[2px]" 108 + style="background-color: {hasTheme 109 + ? 'var(--theme-accent)' 110 + : '#3b82f6'}; width: {percentage}%; min-width: 4px;" 111 + ></div> 112 + <div></div> 113 + </div> 114 + </div> 115 + {/each} 116 + 117 + <div class="pt-2 text-center text-sm text-gray-600 dark:text-gray-400"> 118 + {totalVotes} 119 + {totalVotes === 1 ? 'vote' : 'votes'} 120 + </div> 121 + </div> 122 + {/if}
+26
packages/svelte-standard-site/src/lib/components/document/blocks/TextBlock.svelte
··· 1 + <script lang="ts"> 2 + import RichText from '../RichText.svelte'; 3 + 4 + interface Props { 5 + block: { 6 + plaintext: string; 7 + facets?: any[]; 8 + textSize?: 'default' | 'small' | 'large'; 9 + }; 10 + hasTheme?: boolean; 11 + } 12 + 13 + const { block, hasTheme = false }: Props = $props(); 14 + 15 + const textSizeClass = $derived( 16 + block.textSize === 'small' 17 + ? 'text-sm text-secondary' 18 + : block.textSize === 'large' 19 + ? 'text-lg' 20 + : '' 21 + ); 22 + </script> 23 + 24 + <p class="textBlock mt-1 mb-2 {textSizeClass}"> 25 + <RichText plaintext={block.plaintext} facets={block.facets} {hasTheme} /> 26 + </p>
+71
packages/svelte-standard-site/src/lib/components/document/blocks/UnorderedListBlock.svelte
··· 1 + <script lang="ts"> 2 + import RichText from '../RichText.svelte'; 3 + import UnorderedListBlock from './UnorderedListBlock.svelte'; 4 + 5 + interface ListItem { 6 + content?: { 7 + plaintext: string; 8 + facets?: any[]; 9 + }; 10 + children?: ListItem[]; 11 + } 12 + 13 + interface Props { 14 + block: { 15 + children: ListItem[]; 16 + }; 17 + hasTheme?: boolean; 18 + } 19 + 20 + const { block, hasTheme = false }: Props = $props(); 21 + </script> 22 + 23 + <ul class="unordered-list pb-2"> 24 + {#each block.children as item} 25 + <li class="flex flex-row gap-2 pb-0"> 26 + <div 27 + class="listMarker z-1 mx-2 mt-3.5 h-1.25 w-1.25 shrink-0" 28 + class:has-content={item.content} 29 + class:themed={hasTheme} 30 + ></div> 31 + 32 + <div class="flex w-full flex-col"> 33 + {#if item.content} 34 + <div class="textBlock mt-1 mb-2"> 35 + <RichText plaintext={item.content.plaintext} facets={item.content.facets} {hasTheme} /> 36 + </div> 37 + {/if} 38 + {#if item.children && item.children.length > 0} 39 + <UnorderedListBlock block={{ children: item.children }} {hasTheme} /> 40 + {/if} 41 + </div> 42 + </li> 43 + {/each} 44 + </ul> 45 + 46 + <style> 47 + .unordered-list { 48 + list-style: none; 49 + padding-left: 0; 50 + margin-left: -1px; 51 + } 52 + 53 + @media (min-width: 640px) { 54 + .unordered-list { 55 + margin-left: 9px; 56 + } 57 + } 58 + 59 + .listMarker { 60 + background-color: transparent; 61 + } 62 + 63 + .listMarker.has-content { 64 + border-radius: 9999px; 65 + background-color: rgb(107 114 128); /* Default gray color */ 66 + } 67 + 68 + .listMarker.has-content.themed { 69 + background-color: var(--theme-accent); 70 + } 71 + </style>
+66
packages/svelte-standard-site/src/lib/components/document/blocks/WebsiteBlock.svelte
··· 1 + <script lang="ts"> 2 + interface Props { 3 + block: { 4 + url: string; 5 + title?: string; 6 + description?: string; 7 + preview?: { 8 + src: string; 9 + }; 10 + }; 11 + hasTheme?: boolean; 12 + } 13 + 14 + const { block, hasTheme = false }: Props = $props(); 15 + </script> 16 + 17 + <div 18 + class="group/linkBlock relative my-2 flex h-[104px] overflow-hidden rounded-md border bg-white transition-colors dark:bg-gray-900" 19 + style:border-color={hasTheme ? 'var(--theme-accent)' : undefined} 20 + class:border-gray-200={!hasTheme} 21 + class:hover:border-gray-300={!hasTheme} 22 + class:dark:border-gray-700={!hasTheme} 23 + class:dark:hover:border-gray-600={!hasTheme} 24 + > 25 + <a 26 + href={block.url} 27 + target="_blank" 28 + rel="noopener noreferrer" 29 + class="flex h-full w-full text-inherit no-underline hover:no-underline" 30 + > 31 + <div class="min-w-0 grow px-3 pt-2 pb-2"> 32 + <div class="flex h-full w-full min-w-0 flex-col"> 33 + {#if block.title} 34 + <div 35 + class="mb-0.5 line-clamp-1 text-base font-bold" 36 + style="overflow: hidden; text-overflow: ellipsis; word-break: break-all;" 37 + > 38 + {block.title} 39 + </div> 40 + {/if} 41 + 42 + {#if block.description} 43 + <div class="line-clamp-2 grow text-sm text-gray-600 dark:text-gray-400"> 44 + {block.description} 45 + </div> 46 + {/if} 47 + 48 + <div 49 + class="line-clamp-1 w-full min-w-0 text-xs text-gray-500 italic group-hover/linkBlock:text-blue-600 dark:text-gray-500 dark:group-hover/linkBlock:text-blue-400" 50 + style="word-break: break-word;" 51 + style:color={hasTheme ? 'var(--theme-accent)' : undefined} 52 + > 53 + {block.url} 54 + </div> 55 + </div> 56 + </div> 57 + 58 + {#if block.preview?.src} 59 + <div 60 + class="m-2 -mb-2 w-[120px] shrink-0 origin-center rotate-[4deg] rounded-t-md border border-gray-200 bg-cover dark:border-gray-700" 61 + style:background-image="url({block.preview.src})" 62 + style:background-position="center" 63 + ></div> 64 + {/if} 65 + </a> 66 + </div>
+14
packages/svelte-standard-site/src/lib/components/index.ts
··· 1 + export { default as DocumentCard } from './DocumentCard.svelte'; 2 + export { default as PublicationCard } from './PublicationCard.svelte'; 3 + export { default as ThemeToggle } from './ThemeToggle.svelte'; 4 + export { default as StandardSiteLayout } from './StandardSiteLayout.svelte'; 5 + 6 + // Common reusable components 7 + export { default as DateDisplay } from './common/DateDisplay.svelte'; 8 + export { default as TagList } from './common/TagList.svelte'; 9 + export { default as ThemedContainer } from './common/ThemedContainer.svelte'; 10 + export { default as ThemedText } from './common/ThemedText.svelte'; 11 + export { default as ThemedCard } from './common/ThemedCard.svelte'; 12 + 13 + // Document rendering 14 + export { default as DocumentRenderer } from './document/DocumentRenderer.svelte';
+31
packages/svelte-standard-site/src/lib/config/env.ts
··· 1 + import { env } from '$env/dynamic/public'; 2 + import type { SiteStandardConfig } from '../types.js'; 3 + 4 + /** 5 + * Get configuration from environment variables 6 + * @returns Configuration object or null if required vars are missing 7 + */ 8 + export function getConfigFromEnv(): SiteStandardConfig | null { 9 + const did = env.PUBLIC_ATPROTO_DID; 10 + 11 + if (!did) { 12 + console.error('Missing required environment variable: PUBLIC_ATPROTO_DID'); 13 + return null; 14 + } 15 + 16 + return { 17 + did, 18 + pds: env.PUBLIC_ATPROTO_PDS || undefined, 19 + cacheTTL: env.PUBLIC_CACHE_TTL ? parseInt(env.PUBLIC_CACHE_TTL) : undefined 20 + }; 21 + } 22 + 23 + /** 24 + * Validate that required environment variables are set 25 + * @throws Error if required variables are missing 26 + */ 27 + export function validateEnv(): void { 28 + if (!env.PUBLIC_ATPROTO_DID) { 29 + throw new Error('Missing required environment variable: PUBLIC_ATPROTO_DID'); 30 + } 31 + }
+102
packages/svelte-standard-site/src/lib/index.ts
··· 1 + // Main exports 2 + export { SiteStandardClient, createClient } from './client.js'; 3 + export { StandardSitePublisher } from './publisher.js'; 4 + 5 + // Component exports 6 + export { 7 + DocumentCard, 8 + PublicationCard, 9 + ThemeToggle, 10 + StandardSiteLayout, 11 + DateDisplay, 12 + TagList, 13 + ThemedContainer, 14 + ThemedText, 15 + ThemedCard 16 + } from './components/index.js'; 17 + 18 + // Comments component 19 + export { default as Comments } from './components/Comments.svelte'; 20 + 21 + // Store exports 22 + export { themeStore } from './stores/index.js'; 23 + 24 + // Type exports 25 + export type { 26 + AtProtoBlob, 27 + StrongRef, 28 + RGBColor, 29 + BasicTheme, 30 + PublicationPreferences, 31 + Publication, 32 + Document, 33 + AtProtoRecord, 34 + ResolvedIdentity, 35 + SiteStandardConfig 36 + } from './types.js'; 37 + 38 + // Schema exports 39 + export type { 40 + PublisherConfig, 41 + ReaderConfig, 42 + LoaderConfig 43 + } from './schemas.js'; 44 + 45 + export { COLLECTIONS } from './schemas.js'; 46 + 47 + // Utility exports 48 + export { parseAtUri, atUriToHttps, buildAtUri, extractRkey, isAtUri } from './utils/at-uri.js'; 49 + 50 + export { resolveIdentity, buildPdsBlobUrl } from './utils/agents.js'; 51 + 52 + export { cache } from './utils/cache.js'; 53 + 54 + export { rgbToCSS, rgbToHex, getThemeVars } from './utils/theme.js'; 55 + 56 + export { 57 + mixThemeColor, 58 + getThemedTextColor, 59 + getThemedBackground, 60 + getThemedBorder, 61 + getThemedAccent, 62 + themeToCssVars 63 + } from './utils/theme-helpers.js'; 64 + 65 + export { 66 + getDocumentSlug, 67 + getDocumentUrl, 68 + extractRkey as extractRkeyFromUri 69 + } from './utils/document.js'; 70 + 71 + // Content transformation exports 72 + export { 73 + transformContent, 74 + convertSidenotes, 75 + convertComplexSidenotes, 76 + resolveRelativeLinks, 77 + stripToPlainText, 78 + countWords, 79 + calculateReadingTime 80 + } from './utils/content.js'; 81 + 82 + export type { TransformOptions, TransformResult } from './utils/content.js'; 83 + 84 + // Comments exports 85 + export { fetchComments, fetchMentionComments, formatRelativeTime } from './utils/comments.js'; 86 + 87 + export type { Comment, CommentAuthor, FetchCommentsOptions } from './utils/comments.js'; 88 + 89 + // Verification exports 90 + export { 91 + generatePublicationWellKnown, 92 + generateDocumentLinkTag, 93 + generatePublicationLinkTag, 94 + getDocumentAtUri, 95 + getPublicationAtUri, 96 + verifyPublicationWellKnown, 97 + extractDocumentLinkFromHtml, 98 + extractPublicationLinkFromHtml 99 + } from './utils/verification.js'; 100 + 101 + // Publisher types 102 + export type { PublishDocumentInput, PublishPublicationInput, PublishResult } from './publisher.js';
+475
packages/svelte-standard-site/src/lib/publisher.ts
··· 1 + /** 2 + * Publisher for standard.site documents 3 + * 4 + * Publishes documents to ATProto repositories using the standard.site lexicon, 5 + * enabling your SvelteKit site to sync content to Leaflet, WhiteWind, or any 6 + * compatible platform. 7 + * 8 + * The publisher automatically resolves the correct PDS from your DID document, 9 + * so it works with any PDS (bsky.app, Blacksky, self-hosted, etc.). 10 + * 11 + * @example 12 + * ```ts 13 + * import { StandardSitePublisher } from 'svelte-standard-site/publisher'; 14 + * 15 + * const publisher = new StandardSitePublisher({ 16 + * identifier: 'your-handle.bsky.social', 17 + * password: process.env.ATPROTO_APP_PASSWORD!, 18 + * }); 19 + * 20 + * await publisher.login(); 21 + * 22 + * await publisher.publishDocument({ 23 + * site: 'https://myblog.com', 24 + * title: 'My Blog Post', 25 + * publishedAt: new Date().toISOString(), 26 + * }); 27 + * ``` 28 + */ 29 + 30 + import { AtpAgent } from '@atproto/api'; 31 + import type { PublisherConfig, Document, Publication } from './schemas.js'; 32 + import { PublisherConfigSchema, COLLECTIONS } from './schemas.js'; 33 + 34 + /** 35 + * Resolve a handle to a DID using the public API 36 + */ 37 + async function resolveHandle(handle: string): Promise<string> { 38 + const res = await fetch( 39 + `https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}` 40 + ); 41 + if (!res.ok) throw new Error(`Failed to resolve handle: ${handle}`); 42 + const data = (await res.json()) as { did: string }; 43 + return data.did; 44 + } 45 + 46 + /** 47 + * Get the PDS endpoint from a DID document 48 + */ 49 + async function getPdsFromDid(did: string): Promise<string> { 50 + let didDoc: any; 51 + 52 + if (did.startsWith('did:plc:')) { 53 + // Resolve from plc.directory 54 + const res = await fetch(`https://plc.directory/${did}`); 55 + if (!res.ok) throw new Error(`Failed to resolve DID: ${did}`); 56 + didDoc = await res.json(); 57 + } else if (did.startsWith('did:web:')) { 58 + // Resolve from the domain 59 + const domain = did.replace('did:web:', ''); 60 + const res = await fetch(`https://${domain}/.well-known/did.json`); 61 + if (!res.ok) throw new Error(`Failed to resolve DID: ${did}`); 62 + didDoc = await res.json(); 63 + } else { 64 + throw new Error(`Unsupported DID method: ${did}`); 65 + } 66 + 67 + // Find the AtprotoPersonalDataServer service 68 + const pdsService = didDoc.service?.find( 69 + (s: any) => s.type === 'AtprotoPersonalDataServer' || s.id === '#atproto_pds' 70 + ); 71 + 72 + if (!pdsService?.serviceEndpoint) { 73 + throw new Error(`No PDS found in DID document for ${did}`); 74 + } 75 + 76 + return pdsService.serviceEndpoint; 77 + } 78 + 79 + /** 80 + * Generate a TID (Timestamp Identifier) per ATProto spec 81 + * @see https://atproto.com/specs/tid 82 + * 83 + * Structure: 84 + * - 64-bit integer, big-endian 85 + * - Top bit always 0 86 + * - Next 53 bits: microseconds since UNIX epoch 87 + * - Final 10 bits: random clock identifier 88 + * - Encoded as base32-sortable (chars: 234567abcdefghijklmnopqrstuvwxyz) 89 + * - Always 13 characters 90 + */ 91 + const BASE32_SORTABLE = '234567abcdefghijklmnopqrstuvwxyz'; 92 + 93 + function generateTid(): string { 94 + const now = Date.now() * 1000; // Convert to microseconds 95 + const clockId = Math.floor(Math.random() * 1024); // 10 bits 96 + 97 + // Combine: (timestamp << 10) | clockId 98 + // Ensure top bit is 0 by masking with 0x7FFFFFFFFFFFFFFF 99 + const tid = ((BigInt(now) << 10n) | BigInt(clockId)) & 0x7fffffffffffffffn; 100 + 101 + // Encode as base32-sortable 102 + let encoded = ''; 103 + let remaining = tid; 104 + for (let i = 0; i < 13; i++) { 105 + const index = Number(remaining & 31n); 106 + encoded = BASE32_SORTABLE[index] + encoded; 107 + remaining = remaining >> 5n; 108 + } 109 + 110 + return encoded; 111 + } 112 + 113 + export interface PublishDocumentInput { 114 + /** Site/publication URI (https or at-uri) - REQUIRED */ 115 + site: string; 116 + /** Document title - REQUIRED */ 117 + title: string; 118 + /** When the document was published (ISO 8601) - REQUIRED */ 119 + publishedAt: string; 120 + /** Path to combine with site URL */ 121 + path?: string; 122 + /** Document description/excerpt */ 123 + description?: string; 124 + /** When the document was last updated (ISO 8601) */ 125 + updatedAt?: string; 126 + /** Tags/categories */ 127 + tags?: string[]; 128 + /** Plain text content for indexing */ 129 + textContent?: string; 130 + /** Platform-specific content */ 131 + content?: unknown; 132 + /** Reference to associated Bluesky post */ 133 + bskyPostRef?: { uri: string; cid: string }; 134 + /** Cover image blob */ 135 + coverImage?: { 136 + $type: 'blob'; 137 + ref: { $link: string }; 138 + mimeType: string; 139 + size: number; 140 + }; 141 + } 142 + 143 + export interface PublishPublicationInput { 144 + /** Publication name */ 145 + name: string; 146 + /** Base URL */ 147 + url: string; 148 + /** Description */ 149 + description?: string; 150 + /** Icon blob */ 151 + icon?: { 152 + $type: 'blob'; 153 + ref: { $link: string }; 154 + mimeType: string; 155 + size: number; 156 + }; 157 + /** Basic theme colors */ 158 + basicTheme?: { 159 + background: { r: number; g: number; b: number }; 160 + foreground: { r: number; g: number; b: number }; 161 + accent: { r: number; g: number; b: number }; 162 + accentForeground: { r: number; g: number; b: number }; 163 + }; 164 + /** Publication preferences */ 165 + preferences?: { 166 + showInDiscover?: boolean; 167 + }; 168 + } 169 + 170 + export interface PublishResult { 171 + uri: string; 172 + cid: string; 173 + } 174 + 175 + /** 176 + * Publisher for standard.site documents on ATProto 177 + */ 178 + export class StandardSitePublisher { 179 + private agent: AtpAgent | null = null; 180 + private config: PublisherConfig; 181 + private did: string | null = null; 182 + private pdsUrl: string | null = null; 183 + 184 + constructor(config: Partial<PublisherConfig>) { 185 + this.config = PublisherConfigSchema.parse(config); 186 + } 187 + 188 + /** 189 + * Authenticate with the PDS 190 + * Automatically resolves the correct PDS from the DID document 191 + */ 192 + async login(): Promise<void> { 193 + // Resolve handle to DID if needed 194 + let did = this.config.identifier; 195 + if (!did.startsWith('did:')) { 196 + did = await resolveHandle(this.config.identifier); 197 + } 198 + this.did = did; 199 + 200 + // Get PDS URL from DID document (unless manually overridden) 201 + if (this.config.service) { 202 + this.pdsUrl = this.config.service; 203 + } else { 204 + this.pdsUrl = await getPdsFromDid(did); 205 + } 206 + 207 + // Create agent and login 208 + this.agent = new AtpAgent({ service: this.pdsUrl }); 209 + await this.agent.login({ 210 + identifier: this.config.identifier, 211 + password: this.config.password 212 + }); 213 + } 214 + 215 + /** 216 + * Get the authenticated DID 217 + */ 218 + getDid(): string { 219 + if (!this.did) { 220 + throw new Error('Not logged in. Call login() first.'); 221 + } 222 + return this.did; 223 + } 224 + 225 + /** 226 + * Get the PDS URL being used 227 + */ 228 + getPdsUrl(): string { 229 + if (!this.pdsUrl) { 230 + throw new Error('Not logged in. Call login() first.'); 231 + } 232 + return this.pdsUrl; 233 + } 234 + 235 + private getAgent(): AtpAgent { 236 + if (!this.agent) { 237 + throw new Error('Not logged in. Call login() first.'); 238 + } 239 + return this.agent; 240 + } 241 + 242 + /** 243 + * Publish a document record 244 + */ 245 + async publishDocument(input: PublishDocumentInput): Promise<PublishResult> { 246 + const did = this.getDid(); 247 + const agent = this.getAgent(); 248 + 249 + const record: Document = { 250 + $type: 'site.standard.document', 251 + site: input.site, 252 + title: input.title, 253 + publishedAt: input.publishedAt, 254 + path: input.path, 255 + description: input.description, 256 + updatedAt: input.updatedAt, 257 + tags: input.tags, 258 + textContent: input.textContent, 259 + content: input.content, 260 + bskyPostRef: input.bskyPostRef, 261 + coverImage: input.coverImage 262 + }; 263 + 264 + // Remove undefined values 265 + const cleanRecord = Object.fromEntries( 266 + Object.entries(record).filter(([_, v]) => v !== undefined) 267 + ) as Document; 268 + 269 + // Generate TID for record key per lexicon spec (key: "tid") 270 + const rkey = generateTid(); 271 + 272 + const response = await agent.api.com.atproto.repo.createRecord({ 273 + repo: did, 274 + collection: COLLECTIONS.DOCUMENT, 275 + rkey, 276 + record: cleanRecord 277 + }); 278 + 279 + return { 280 + uri: response.data.uri, 281 + cid: response.data.cid 282 + }; 283 + } 284 + 285 + /** 286 + * Update an existing document 287 + */ 288 + async updateDocument(rkey: string, input: PublishDocumentInput): Promise<PublishResult> { 289 + const did = this.getDid(); 290 + const agent = this.getAgent(); 291 + 292 + const record: Document = { 293 + $type: 'site.standard.document', 294 + site: input.site, 295 + title: input.title, 296 + publishedAt: input.publishedAt, 297 + path: input.path, 298 + description: input.description, 299 + updatedAt: input.updatedAt ?? new Date().toISOString(), 300 + tags: input.tags, 301 + textContent: input.textContent, 302 + content: input.content, 303 + bskyPostRef: input.bskyPostRef, 304 + coverImage: input.coverImage 305 + }; 306 + 307 + const cleanRecord = Object.fromEntries( 308 + Object.entries(record).filter(([_, v]) => v !== undefined) 309 + ) as Document; 310 + 311 + const response = await agent.api.com.atproto.repo.putRecord({ 312 + repo: did, 313 + collection: COLLECTIONS.DOCUMENT, 314 + rkey, 315 + record: cleanRecord 316 + }); 317 + 318 + return { 319 + uri: response.data.uri, 320 + cid: response.data.cid 321 + }; 322 + } 323 + 324 + /** 325 + * Delete a document 326 + */ 327 + async deleteDocument(rkey: string): Promise<void> { 328 + const did = this.getDid(); 329 + const agent = this.getAgent(); 330 + 331 + await agent.api.com.atproto.repo.deleteRecord({ 332 + repo: did, 333 + collection: COLLECTIONS.DOCUMENT, 334 + rkey 335 + }); 336 + } 337 + 338 + /** 339 + * Publish a publication record 340 + */ 341 + async publishPublication(input: PublishPublicationInput): Promise<PublishResult> { 342 + const did = this.getDid(); 343 + const agent = this.getAgent(); 344 + 345 + const record: Publication = { 346 + $type: 'site.standard.publication', 347 + name: input.name, 348 + url: input.url, 349 + description: input.description, 350 + icon: input.icon, 351 + basicTheme: input.basicTheme, 352 + preferences: input.preferences 353 + }; 354 + 355 + const cleanRecord = Object.fromEntries( 356 + Object.entries(record).filter(([_, v]) => v !== undefined) 357 + ) as Publication; 358 + 359 + // Generate TID for record key per lexicon spec (key: "tid") 360 + const rkey = generateTid(); 361 + 362 + const response = await agent.api.com.atproto.repo.createRecord({ 363 + repo: did, 364 + collection: COLLECTIONS.PUBLICATION, 365 + rkey, 366 + record: cleanRecord 367 + }); 368 + 369 + return { 370 + uri: response.data.uri, 371 + cid: response.data.cid 372 + }; 373 + } 374 + 375 + /** 376 + * Update an existing publication 377 + */ 378 + async updatePublication(rkey: string, input: PublishPublicationInput): Promise<PublishResult> { 379 + const did = this.getDid(); 380 + const agent = this.getAgent(); 381 + 382 + const record: Publication = { 383 + $type: 'site.standard.publication', 384 + name: input.name, 385 + url: input.url, 386 + description: input.description, 387 + icon: input.icon, 388 + basicTheme: input.basicTheme, 389 + preferences: input.preferences 390 + }; 391 + 392 + const cleanRecord = Object.fromEntries( 393 + Object.entries(record).filter(([_, v]) => v !== undefined) 394 + ) as Publication; 395 + 396 + const response = await agent.api.com.atproto.repo.putRecord({ 397 + repo: did, 398 + collection: COLLECTIONS.PUBLICATION, 399 + rkey, 400 + record: cleanRecord 401 + }); 402 + 403 + return { 404 + uri: response.data.uri, 405 + cid: response.data.cid 406 + }; 407 + } 408 + 409 + /** 410 + * Delete a publication 411 + */ 412 + async deletePublication(rkey: string): Promise<void> { 413 + const did = this.getDid(); 414 + const agent = this.getAgent(); 415 + 416 + await agent.api.com.atproto.repo.deleteRecord({ 417 + repo: did, 418 + collection: COLLECTIONS.PUBLICATION, 419 + rkey 420 + }); 421 + } 422 + 423 + /** 424 + * Get all documents for the current account 425 + */ 426 + async listDocuments( 427 + limit = 100 428 + ): Promise<Array<{ uri: string; cid: string; value: Document }>> { 429 + const did = this.getDid(); 430 + const agent = this.getAgent(); 431 + 432 + const response = await agent.api.com.atproto.repo.listRecords({ 433 + repo: did, 434 + collection: COLLECTIONS.DOCUMENT, 435 + limit 436 + }); 437 + 438 + return response.data.records.map((r) => ({ 439 + uri: r.uri, 440 + cid: r.cid, 441 + value: r.value as Document 442 + })); 443 + } 444 + 445 + /** 446 + * Get all publications for the current account 447 + */ 448 + async listPublications( 449 + limit = 100 450 + ): Promise<Array<{ uri: string; cid: string; value: Publication }>> { 451 + const did = this.getDid(); 452 + const agent = this.getAgent(); 453 + 454 + const response = await agent.api.com.atproto.repo.listRecords({ 455 + repo: did, 456 + collection: COLLECTIONS.PUBLICATION, 457 + limit 458 + }); 459 + 460 + return response.data.records.map((r) => ({ 461 + uri: r.uri, 462 + cid: r.cid, 463 + value: r.value as Publication 464 + })); 465 + } 466 + 467 + /** 468 + * Get the underlying ATP agent for advanced operations 469 + */ 470 + getAtpAgent(): AtpAgent { 471 + return this.getAgent(); 472 + } 473 + } 474 + 475 + export type { PublisherConfig };
+132
packages/svelte-standard-site/src/lib/schemas.ts
··· 1 + /** 2 + * Zod schemas for standard.site lexicons and configuration 3 + */ 4 + 5 + import { z } from 'zod'; 6 + 7 + /** 8 + * AT Protocol collections 9 + */ 10 + export const COLLECTIONS = { 11 + DOCUMENT: 'site.standard.document', 12 + PUBLICATION: 'site.standard.publication' 13 + } as const; 14 + 15 + /** 16 + * RGB Color schema 17 + */ 18 + export const RGBColorSchema = z.object({ 19 + r: z.number().int().min(0).max(255), 20 + g: z.number().int().min(0).max(255), 21 + b: z.number().int().min(0).max(255) 22 + }); 23 + 24 + /** 25 + * Basic Theme schema 26 + */ 27 + export const BasicThemeSchema = z.object({ 28 + $type: z.literal('site.standard.theme.basic').optional(), 29 + background: RGBColorSchema, 30 + foreground: RGBColorSchema, 31 + accent: RGBColorSchema, 32 + accentForeground: RGBColorSchema 33 + }); 34 + 35 + /** 36 + * Publication Preferences schema 37 + */ 38 + export const PublicationPreferencesSchema = z.object({ 39 + showInDiscover: z.boolean().optional() 40 + }); 41 + 42 + /** 43 + * AT Protocol Blob schema 44 + */ 45 + export const AtProtoBlobSchema = z.object({ 46 + $type: z.literal('blob'), 47 + ref: z.object({ 48 + $link: z.string() 49 + }), 50 + mimeType: z.string(), 51 + size: z.number().int().positive() 52 + }); 53 + 54 + /** 55 + * Strong Reference schema 56 + */ 57 + export const StrongRefSchema = z.object({ 58 + uri: z.string(), 59 + cid: z.string() 60 + }); 61 + 62 + /** 63 + * Publication schema 64 + */ 65 + export const PublicationSchema = z.object({ 66 + $type: z.literal('site.standard.publication'), 67 + name: z.string(), 68 + url: z.string().url(), 69 + description: z.string().optional(), 70 + icon: AtProtoBlobSchema.optional(), 71 + basicTheme: BasicThemeSchema.optional(), 72 + preferences: PublicationPreferencesSchema.optional() 73 + }); 74 + 75 + /** 76 + * Document schema 77 + */ 78 + export const DocumentSchema = z.object({ 79 + $type: z.literal('site.standard.document'), 80 + site: z.string(), 81 + title: z.string(), 82 + publishedAt: z.string().datetime(), 83 + path: z.string().optional(), 84 + description: z.string().optional(), 85 + updatedAt: z.string().datetime().optional(), 86 + tags: z.array(z.string()).optional(), 87 + coverImage: AtProtoBlobSchema.optional(), 88 + textContent: z.string().optional(), 89 + content: z.unknown().optional(), 90 + bskyPostRef: StrongRefSchema.optional() 91 + }); 92 + 93 + /** 94 + * Publisher configuration schema 95 + */ 96 + export const PublisherConfigSchema = z.object({ 97 + identifier: z.string(), // handle or DID 98 + password: z.string(), 99 + service: z.string().url().optional() 100 + }); 101 + 102 + /** 103 + * Reader/Client configuration schema 104 + */ 105 + export const ReaderConfigSchema = z.object({ 106 + did: z.string(), 107 + pds: z.string().url().optional(), 108 + cacheTTL: z.number().int().positive().optional() 109 + }); 110 + 111 + /** 112 + * Loader configuration schema 113 + */ 114 + export const LoaderConfigSchema = z.object({ 115 + repo: z.string(), 116 + excludeSite: z.string().url().optional(), 117 + publication: z.string().optional(), 118 + limit: z.number().int().positive().default(100), 119 + service: z.string().url().default('https://public.api.bsky.app') 120 + }); 121 + 122 + // Type exports 123 + export type RGBColor = z.infer<typeof RGBColorSchema>; 124 + export type BasicTheme = z.infer<typeof BasicThemeSchema>; 125 + export type PublicationPreferences = z.infer<typeof PublicationPreferencesSchema>; 126 + export type AtProtoBlob = z.infer<typeof AtProtoBlobSchema>; 127 + export type StrongRef = z.infer<typeof StrongRefSchema>; 128 + export type Publication = z.infer<typeof PublicationSchema>; 129 + export type Document = z.infer<typeof DocumentSchema>; 130 + export type PublisherConfig = z.infer<typeof PublisherConfigSchema>; 131 + export type ReaderConfig = z.infer<typeof ReaderConfigSchema>; 132 + export type LoaderConfig = z.infer<typeof LoaderConfigSchema>;
+1
packages/svelte-standard-site/src/lib/stores/index.ts
··· 1 + export { themeStore } from './theme.js';
+80
packages/svelte-standard-site/src/lib/stores/theme.ts
··· 1 + import { writable } from 'svelte/store'; 2 + import { browser } from '$app/environment'; 3 + 4 + interface ThemeState { 5 + isDark: boolean; 6 + mounted: boolean; 7 + } 8 + 9 + function createThemeStore() { 10 + const { subscribe, set, update } = writable<ThemeState>({ 11 + isDark: false, 12 + mounted: false 13 + }); 14 + 15 + return { 16 + subscribe, 17 + init: () => { 18 + if (!browser) return; 19 + 20 + const stored = localStorage.getItem('theme'); 21 + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; 22 + const isDark = stored === 'dark' || (!stored && prefersDark); 23 + 24 + update((state) => ({ 25 + ...state, 26 + isDark, 27 + mounted: true 28 + })); 29 + 30 + applyTheme(isDark); 31 + 32 + // Listen for system preference changes 33 + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); 34 + const handleChange = (e: MediaQueryListEvent) => { 35 + if (!localStorage.getItem('theme')) { 36 + update((state) => ({ ...state, isDark: e.matches })); 37 + applyTheme(e.matches); 38 + } 39 + }; 40 + mediaQuery.addEventListener('change', handleChange); 41 + 42 + return () => { 43 + mediaQuery.removeEventListener('change', handleChange); 44 + }; 45 + }, 46 + toggle: () => { 47 + if (!browser) return; 48 + 49 + update((state) => { 50 + const newIsDark = !state.isDark; 51 + localStorage.setItem('theme', newIsDark ? 'dark' : 'light'); 52 + applyTheme(newIsDark); 53 + return { ...state, isDark: newIsDark }; 54 + }); 55 + }, 56 + setTheme: (isDark: boolean) => { 57 + if (!browser) return; 58 + 59 + localStorage.setItem('theme', isDark ? 'dark' : 'light'); 60 + applyTheme(isDark); 61 + update((state) => ({ ...state, isDark })); 62 + } 63 + }; 64 + } 65 + 66 + function applyTheme(isDark: boolean) { 67 + if (!browser) return; 68 + 69 + const htmlElement = document.documentElement; 70 + 71 + if (isDark) { 72 + htmlElement.classList.add('dark'); 73 + htmlElement.style.colorScheme = 'dark'; 74 + } else { 75 + htmlElement.classList.remove('dark'); 76 + htmlElement.style.colorScheme = 'light'; 77 + } 78 + } 79 + 80 + export const themeStore = createThemeStore();
+188
packages/svelte-standard-site/src/lib/styles/base.css
··· 1 + @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); 2 + @import 'tailwindcss'; 3 + @import './themes.css'; 4 + 5 + @theme { 6 + /* Font Family */ 7 + --font-family-sans: 8 + 'Inter', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 9 + 'Segoe UI Symbol', 'Noto Color Emoji'; 10 + 11 + /* Ink - Slate-tinted text (230°) */ 12 + --color-ink-50: light-dark(oklch(17.5% 0.012 230), oklch(97.6% 0.008 230)); 13 + --color-ink-100: light-dark(oklch(25% 0.022 230), oklch(93.2% 0.017 230)); 14 + --color-ink-200: light-dark(oklch(38.5% 0.037 230), oklch(85.2% 0.032 230)); 15 + --color-ink-300: light-dark(oklch(50.5% 0.052 230), oklch(75.2% 0.048 230)); 16 + --color-ink-400: light-dark(oklch(62% 0.065 230), oklch(65.2% 0.062 230)); 17 + --color-ink-500: light-dark(oklch(73% 0.078 230), oklch(55.2% 0.078 230)); 18 + --color-ink-600: light-dark(oklch(78% 0.062 230), oklch(45.2% 0.065 230)); 19 + --color-ink-700: light-dark(oklch(83.5% 0.048 230), oklch(35.2% 0.052 230)); 20 + --color-ink-800: light-dark(oklch(89% 0.032 230), oklch(25.2% 0.037 230)); 21 + --color-ink-900: light-dark(oklch(94.5% 0.017 230), oklch(18.2% 0.022 230)); 22 + --color-ink-950: light-dark(oklch(97.6% 0.008 230), oklch(12.5% 0.012 230)); 23 + 24 + /* Canvas - Slate-tinted backgrounds (230°) */ 25 + --color-canvas-50: light-dark(oklch(17.8% 0.014 230), oklch(98.6% 0.005 230)); 26 + --color-canvas-100: light-dark(oklch(25.8% 0.025 230), oklch(96.6% 0.011 230)); 27 + --color-canvas-200: light-dark(oklch(39.5% 0.042 230), oklch(92.5% 0.024 230)); 28 + --color-canvas-300: light-dark(oklch(52% 0.058 230), oklch(86.5% 0.038 230)); 29 + --color-canvas-400: light-dark(oklch(64% 0.072 230), oklch(80.5% 0.055 230)); 30 + --color-canvas-500: light-dark(oklch(75.5% 0.085 230), oklch(75.5% 0.068 230)); 31 + --color-canvas-600: light-dark(oklch(80.5% 0.055 230), oklch(64% 0.072 230)); 32 + --color-canvas-700: light-dark(oklch(86.5% 0.038 230), oklch(52% 0.058 230)); 33 + --color-canvas-800: light-dark(oklch(92.5% 0.024 230), oklch(39.5% 0.042 230)); 34 + --color-canvas-900: light-dark(oklch(96.6% 0.011 230), oklch(25.8% 0.025 230)); 35 + --color-canvas-950: light-dark(oklch(98.6% 0.005 230), oklch(17.8% 0.014 230)); 36 + 37 + /* Slate - Primary colors (230°) */ 38 + --color-primary-50: light-dark(oklch(18.2% 0.018 230), oklch(97.8% 0.012 230)); 39 + --color-primary-100: light-dark(oklch(26.5% 0.03 230), oklch(94.8% 0.022 230)); 40 + --color-primary-200: light-dark(oklch(40.5% 0.048 230), oklch(89.5% 0.042 230)); 41 + --color-primary-300: light-dark(oklch(54% 0.065 230), oklch(79.5% 0.062 230)); 42 + --color-primary-400: light-dark(oklch(66.5% 0.08 230), oklch(69.5% 0.078 230)); 43 + --color-primary-500: light-dark(oklch(78.5% 0.095 230), oklch(59.5% 0.095 230)); 44 + --color-primary-600: light-dark(oklch(82.2% 0.078 230), oklch(49.5% 0.08 230)); 45 + --color-primary-700: light-dark(oklch(86.5% 0.062 230), oklch(39.5% 0.065 230)); 46 + --color-primary-800: light-dark(oklch(91% 0.042 230), oklch(29.5% 0.048 230)); 47 + --color-primary-900: light-dark(oklch(95.8% 0.022 230), oklch(21.5% 0.03 230)); 48 + --color-primary-950: light-dark(oklch(98% 0.012 230), oklch(15.2% 0.018 230)); 49 + 50 + /* Steel Grey - Secondary colors (215°) */ 51 + --color-secondary-50: light-dark(oklch(18.5% 0.02 215), oklch(97.9% 0.013 215)); 52 + --color-secondary-100: light-dark(oklch(26.8% 0.033 215), oklch(95% 0.024 215)); 53 + --color-secondary-200: light-dark(oklch(41% 0.052 215), oklch(89.8% 0.045 215)); 54 + --color-secondary-300: light-dark(oklch(54.5% 0.07 215), oklch(80.2% 0.065 215)); 55 + --color-secondary-400: light-dark(oklch(67% 0.087 215), oklch(70.2% 0.082 215)); 56 + --color-secondary-500: light-dark(oklch(79% 0.103 215), oklch(60.2% 0.103 215)); 57 + --color-secondary-600: light-dark(oklch(82.8% 0.082 215), oklch(50.2% 0.087 215)); 58 + --color-secondary-700: light-dark(oklch(87% 0.065 215), oklch(40.2% 0.07 215)); 59 + --color-secondary-800: light-dark(oklch(91.5% 0.045 215), oklch(30.5% 0.052 215)); 60 + --color-secondary-900: light-dark(oklch(96% 0.024 215), oklch(22.2% 0.033 215)); 61 + --color-secondary-950: light-dark(oklch(98.2% 0.013 215), oklch(15.8% 0.02 215)); 62 + 63 + /* Charcoal - Accent colors (240°) */ 64 + --color-accent-50: light-dark(oklch(18.5% 0.022 240), oklch(98% 0.014 240)); 65 + --color-accent-100: light-dark(oklch(26.8% 0.036 240), oklch(95.2% 0.026 240)); 66 + --color-accent-200: light-dark(oklch(41% 0.058 240), oklch(90% 0.048 240)); 67 + --color-accent-300: light-dark(oklch(54.5% 0.078 240), oklch(80.8% 0.072 240)); 68 + --color-accent-400: light-dark(oklch(67% 0.097 240), oklch(71% 0.092 240)); 69 + --color-accent-500: light-dark(oklch(79% 0.115 240), oklch(61% 0.115 240)); 70 + --color-accent-600: light-dark(oklch(82.8% 0.092 240), oklch(51% 0.097 240)); 71 + --color-accent-700: light-dark(oklch(87% 0.072 240), oklch(41% 0.078 240)); 72 + --color-accent-800: light-dark(oklch(91.5% 0.048 240), oklch(31% 0.058 240)); 73 + --color-accent-900: light-dark(oklch(96% 0.026 240), oklch(22.5% 0.036 240)); 74 + --color-accent-950: light-dark(oklch(98.2% 0.014 240), oklch(16.2% 0.022 240)); 75 + } 76 + 77 + @layer base { 78 + /* Base styles for consistent typography and accessibility */ 79 + html { 80 + scroll-behavior: smooth; 81 + overflow-x: hidden; 82 + width: 100%; 83 + } 84 + 85 + @media (prefers-reduced-motion: reduce) { 86 + html { 87 + scroll-behavior: auto; 88 + } 89 + 90 + *, 91 + *::before, 92 + *::after { 93 + animation-duration: 0.01ms !important; 94 + animation-iteration-count: 1 !important; 95 + transition-duration: 0.01ms !important; 96 + } 97 + } 98 + 99 + body { 100 + font-family: var(--font-family-sans); 101 + text-rendering: optimizeLegibility; 102 + -webkit-font-smoothing: antialiased; 103 + -moz-osx-font-smoothing: grayscale; 104 + overflow-x: hidden; 105 + width: 100%; 106 + max-width: 100vw; 107 + background-color: var(--color-canvas-50); 108 + } 109 + 110 + @media (prefers-color-scheme: dark) { 111 + body { 112 + background-color: var(--color-canvas-950); 113 + } 114 + } 115 + 116 + /* Skip to content link for keyboard navigation */ 117 + .skip-to-content { 118 + position: absolute; 119 + left: -9999px; 120 + z-index: 999; 121 + padding: 1rem 1.5rem; 122 + background-color: var(--color-primary-600); 123 + color: white; 124 + font-weight: 600; 125 + text-decoration: none; 126 + border-radius: 0.5rem; 127 + } 128 + 129 + .skip-to-content:focus { 130 + left: 1rem; 131 + top: 1rem; 132 + outline: 2px solid var(--color-primary-800); 133 + outline-offset: 2px; 134 + } 135 + 136 + /* Focus visible styles for accessibility - Enhanced for better visibility */ 137 + *:focus-visible { 138 + outline: 3px solid var(--color-primary-600); 139 + outline-offset: 2px; 140 + border-radius: 0.25rem; 141 + } 142 + 143 + /* High contrast mode support */ 144 + @media (prefers-contrast: high) { 145 + *:focus-visible { 146 + outline-width: 4px; 147 + } 148 + } 149 + 150 + /* Ensure all elements stay within viewport */ 151 + * { 152 + min-width: 0; 153 + } 154 + 155 + img, 156 + video, 157 + iframe, 158 + embed, 159 + object { 160 + max-width: 100%; 161 + height: auto; 162 + } 163 + 164 + /* Improve link accessibility */ 165 + a { 166 + text-decoration-skip-ink: auto; 167 + } 168 + 169 + /* Better button accessibility */ 170 + button:disabled { 171 + cursor: not-allowed; 172 + } 173 + 174 + /* Screen reader only utility */ 175 + .sr-only { 176 + position: absolute; 177 + width: 1px; 178 + height: 1px; 179 + padding: 0; 180 + margin: -1px; 181 + overflow: hidden; 182 + clip: rect(0, 0, 0, 0); 183 + white-space: nowrap; 184 + border-width: 0; 185 + } 186 + } 187 + 188 + @plugin '@tailwindcss/typography';
+5
packages/svelte-standard-site/src/lib/styles/themes.css
··· 1 + /* Color Theme System - Modular Theme Imports */ 2 + /* Slate theme is the default and included in base.css */ 3 + /* Add additional themes here as needed */ 4 + 5 + /* Default theme is Slate (230°) defined in base.css */
+111
packages/svelte-standard-site/src/lib/types.ts
··· 1 + /** 2 + * Core types for site.standard.* lexicons 3 + */ 4 + 5 + /** 6 + * AT Protocol blob reference 7 + */ 8 + export interface AtProtoBlob { 9 + $type: 'blob'; 10 + ref: { 11 + $link: string; 12 + }; 13 + mimeType: string; 14 + size: number; 15 + } 16 + 17 + /** 18 + * Strong reference to another AT Protocol record 19 + */ 20 + export interface StrongRef { 21 + uri: string; 22 + cid: string; 23 + } 24 + 25 + /** 26 + * RGB Color 27 + */ 28 + export interface RGBColor { 29 + r: number; // 0-255 30 + g: number; // 0-255 31 + b: number; // 0-255 32 + } 33 + 34 + /** 35 + * Basic theme for publications 36 + */ 37 + export interface BasicTheme { 38 + $type?: 'site.standard.theme.basic'; 39 + background: RGBColor; 40 + foreground: RGBColor; 41 + accent: RGBColor; 42 + accentForeground: RGBColor; 43 + } 44 + 45 + /** 46 + * Publication preferences 47 + */ 48 + export interface PublicationPreferences { 49 + showInDiscover?: boolean; 50 + } 51 + 52 + /** 53 + * Site Standard Publication record 54 + */ 55 + export interface Publication { 56 + $type: 'site.standard.publication'; 57 + url: string; 58 + name: string; 59 + icon?: string; // Blob URL converted to string 60 + description?: string; 61 + basicTheme?: BasicTheme; 62 + preferences?: PublicationPreferences; 63 + } 64 + 65 + /** 66 + * Site Standard Document record 67 + */ 68 + export interface Document { 69 + $type: 'site.standard.document'; 70 + site: string; // AT URI or HTTPS URL 71 + title: string; 72 + path?: string; 73 + description?: string; 74 + coverImage?: string; // Blob URL converted to string 75 + content?: any; // Open union 76 + textContent?: string; 77 + bskyPostRef?: StrongRef; 78 + tags?: string[]; 79 + publishedAt: string; 80 + updatedAt?: string; 81 + } 82 + 83 + /** 84 + * AT Protocol record response 85 + */ 86 + export interface AtProtoRecord<T = any> { 87 + uri: string; 88 + cid: string; 89 + value: T; 90 + } 91 + 92 + /** 93 + * Resolved identity from PDS resolution 94 + */ 95 + export interface ResolvedIdentity { 96 + did: string; 97 + pds: string; 98 + handle?: string; 99 + } 100 + 101 + /** 102 + * Configuration for the library 103 + */ 104 + export interface SiteStandardConfig { 105 + /** The DID to fetch records from */ 106 + did: string; 107 + /** Optional custom PDS endpoint */ 108 + pds?: string; 109 + /** Cache TTL in milliseconds (default: 5 minutes) */ 110 + cacheTTL?: number; 111 + }
+124
packages/svelte-standard-site/src/lib/utils/agents.ts
··· 1 + import { AtpAgent } from '@atproto/api'; 2 + import type { ResolvedIdentity } from '../types.js'; 3 + import { cache } from './cache.js'; 4 + 5 + /** 6 + * Creates an AtpAgent with optional fetch function injection 7 + */ 8 + export function createAgent(service: string, fetchFn?: typeof fetch): AtpAgent { 9 + const wrappedFetch = fetchFn 10 + ? async (url: URL | RequestInfo, init?: RequestInit) => { 11 + const urlStr = url instanceof URL ? url.toString() : url; 12 + const response = await fetchFn(urlStr, init); 13 + 14 + const headers = new Headers(response.headers); 15 + if (!headers.has('content-type')) { 16 + headers.set('content-type', 'application/json'); 17 + } 18 + 19 + return new Response(response.body, { 20 + status: response.status, 21 + statusText: response.statusText, 22 + headers 23 + }); 24 + } 25 + : undefined; 26 + 27 + return new AtpAgent({ 28 + service, 29 + ...(wrappedFetch && { fetch: wrappedFetch }) 30 + }); 31 + } 32 + 33 + /** 34 + * Resolves a DID to find its PDS endpoint using Slingshot 35 + * @param did - DID to resolve 36 + * @param fetchFn - Optional fetch function for SSR 37 + * @returns Resolved identity with PDS endpoint 38 + */ 39 + export async function resolveIdentity( 40 + did: string, 41 + fetchFn?: typeof fetch 42 + ): Promise<ResolvedIdentity> { 43 + const cacheKey = `identity:${did}`; 44 + const cached = cache.get<ResolvedIdentity>(cacheKey); 45 + if (cached) return cached; 46 + 47 + const _fetch = fetchFn ?? globalThis.fetch; 48 + 49 + const response = await _fetch( 50 + `https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(did)}` 51 + ); 52 + 53 + if (!response.ok) { 54 + throw new Error(`Failed to resolve DID: ${response.status} ${response.statusText}`); 55 + } 56 + 57 + const rawText = await response.text(); 58 + const data = JSON.parse(rawText); 59 + 60 + if (!data.did || !data.pds) { 61 + throw new Error('Invalid response from identity resolver'); 62 + } 63 + 64 + cache.set(cacheKey, data); 65 + return data; 66 + } 67 + 68 + /** 69 + * Gets or creates a PDS-specific agent 70 + * @param did - DID to resolve PDS for 71 + * @param fetchFn - Optional fetch function for SSR 72 + * @returns AtpAgent configured for the user's PDS 73 + */ 74 + export async function getPDSAgent(did: string, fetchFn?: typeof fetch): Promise<AtpAgent> { 75 + const resolved = await resolveIdentity(did, fetchFn); 76 + return createAgent(resolved.pds, fetchFn); 77 + } 78 + 79 + /** 80 + * Executes a function with automatic fallback 81 + * @param did - The DID to resolve 82 + * @param operation - The operation to execute 83 + * @param fetchFn - Optional fetch function for SSR 84 + */ 85 + export async function withFallback<T>( 86 + did: string, 87 + operation: (agent: AtpAgent) => Promise<T>, 88 + fetchFn?: typeof fetch 89 + ): Promise<T> { 90 + const agents = [ 91 + () => getPDSAgent(did, fetchFn), 92 + () => 93 + Promise.resolve( 94 + fetchFn 95 + ? createAgent('https://public.api.bsky.app', fetchFn) 96 + : createAgent('https://public.api.bsky.app') 97 + ) 98 + ]; 99 + 100 + let lastError: any; 101 + 102 + for (const getAgent of agents) { 103 + try { 104 + const agent = await getAgent(); 105 + return await operation(agent); 106 + } catch (error) { 107 + lastError = error; 108 + } 109 + } 110 + 111 + throw lastError; 112 + } 113 + 114 + /** 115 + * Build a PDS blob URL 116 + * @param pds - PDS endpoint 117 + * @param did - Repository DID 118 + * @param cid - Blob CID 119 + * @returns Full blob URL 120 + */ 121 + export function buildPdsBlobUrl(pds: string, did: string, cid: string): string { 122 + const pdsBase = pds.replace(/\/$/, ''); 123 + return `${pdsBase}/xrpc/com.atproto.sync.getBlob?did=${encodeURIComponent(did)}&cid=${encodeURIComponent(cid)}`; 124 + }
+89
packages/svelte-standard-site/src/lib/utils/at-uri.ts
··· 1 + /** 2 + * AT URI parsing and conversion utilities 3 + */ 4 + 5 + /** 6 + * Parsed AT URI components 7 + */ 8 + export interface ParsedAtUri { 9 + did: string; 10 + collection: string; 11 + rkey: string; 12 + } 13 + 14 + /** 15 + * Parse an AT URI into its components 16 + * @param atUri - AT URI in format: at://did:plc:xxx/collection/rkey 17 + * @returns Parsed components or null if invalid 18 + */ 19 + export function parseAtUri(atUri: string): ParsedAtUri | null { 20 + if (!atUri.startsWith('at://')) return null; 21 + 22 + const withoutProtocol = atUri.slice(5); // Remove "at://" 23 + const parts = withoutProtocol.split('/'); 24 + 25 + if (parts.length !== 3) return null; 26 + 27 + const [did, collection, rkey] = parts; 28 + 29 + if (!did.startsWith('did:') || !collection || !rkey) return null; 30 + 31 + return { did, collection, rkey }; 32 + } 33 + 34 + /** 35 + * Convert AT URI to HTTPS URL for com.atproto.repo.getRecord 36 + * @param atUri - AT URI in format: at://did:plc:xxx/collection/rkey 37 + * @param pdsEndpoint - PDS endpoint (e.g., "https://cortinarius.us-west.host.bsky.network") 38 + * @returns HTTPS URL for getRecord XRPC call 39 + */ 40 + export function atUriToHttps(atUri: string, pdsEndpoint: string): string | null { 41 + const parsed = parseAtUri(atUri); 42 + if (!parsed) return null; 43 + 44 + // Ensure PDS endpoint doesn't have trailing slash 45 + const pds = pdsEndpoint.replace(/\/$/, ''); 46 + 47 + return `${pds}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(parsed.did)}&collection=${encodeURIComponent(parsed.collection)}&rkey=${encodeURIComponent(parsed.rkey)}`; 48 + } 49 + 50 + /** 51 + * Construct an AT URI from components 52 + * @param did - DID of the repository 53 + * @param collection - Collection name 54 + * @param rkey - Record key 55 + * @returns AT URI string 56 + */ 57 + export function buildAtUri(did: string, collection: string, rkey: string): string { 58 + return `at://${did}/${collection}/${rkey}`; 59 + } 60 + 61 + /** 62 + * Extract rkey from an AT URI 63 + * @param atUri - AT URI 64 + * @returns rkey or null if invalid 65 + */ 66 + export function extractRkey(atUri: string): string | null { 67 + const parsed = parseAtUri(atUri); 68 + return parsed?.rkey ?? null; 69 + } 70 + 71 + /** 72 + * Validate if string is a valid AT URI 73 + * @param uri - String to validate 74 + * @returns true if valid AT URI 75 + */ 76 + export function isAtUri(uri: string): boolean { 77 + return parseAtUri(uri) !== null; 78 + } 79 + 80 + /** 81 + * Convert DID to PDS hostname format 82 + * @param did - DID to convert 83 + * @returns PDS hostname or null if unable to derive 84 + */ 85 + export function didToPdsHostname(did: string): string | null { 86 + // This is a simplified version - real implementation should use DID resolution 87 + // For now, we'll return null and rely on explicit PDS configuration 88 + return null; 89 + }
+46
packages/svelte-standard-site/src/lib/utils/cache.ts
··· 1 + /** 2 + * Simple in-memory cache with TTL support 3 + */ 4 + 5 + interface CacheEntry<T> { 6 + value: T; 7 + expires: number; 8 + } 9 + 10 + class Cache { 11 + private store = new Map<string, CacheEntry<any>>(); 12 + private defaultTTL = 5 * 60 * 1000; // 5 minutes 13 + 14 + get<T>(key: string): T | null { 15 + const entry = this.store.get(key); 16 + if (!entry) return null; 17 + 18 + if (Date.now() > entry.expires) { 19 + this.store.delete(key); 20 + return null; 21 + } 22 + 23 + return entry.value; 24 + } 25 + 26 + set<T>(key: string, value: T, ttl?: number): void { 27 + this.store.set(key, { 28 + value, 29 + expires: Date.now() + (ttl ?? this.defaultTTL) 30 + }); 31 + } 32 + 33 + delete(key: string): void { 34 + this.store.delete(key); 35 + } 36 + 37 + clear(): void { 38 + this.store.clear(); 39 + } 40 + 41 + setDefaultTTL(ttl: number): void { 42 + this.defaultTTL = ttl; 43 + } 44 + } 45 + 46 + export const cache = new Cache();
+217
packages/svelte-standard-site/src/lib/utils/comments.ts
··· 1 + /** 2 + * Comments utilities for fetching Bluesky replies 3 + * 4 + * Fetches threaded replies from Bluesky to display as comments on your blog. 5 + * 6 + * @example 7 + * ```ts 8 + * import { fetchComments } from 'svelte-standard-site/comments'; 9 + * 10 + * const comments = await fetchComments({ 11 + * bskyPostUri: 'at://did:plc:xxx/app.bsky.feed.post/abc123', 12 + * canonicalUrl: 'https://yourblog.com/posts/my-post', 13 + * maxDepth: 3, 14 + * }); 15 + * ``` 16 + */ 17 + 18 + import { AtpAgent } from '@atproto/api'; 19 + 20 + export interface CommentAuthor { 21 + did: string; 22 + handle: string; 23 + displayName?: string; 24 + avatar?: string; 25 + } 26 + 27 + export interface Comment { 28 + uri: string; 29 + cid: string; 30 + author: CommentAuthor; 31 + text: string; 32 + createdAt: string; 33 + likeCount: number; 34 + replyCount: number; 35 + replies?: Comment[]; 36 + depth: number; 37 + } 38 + 39 + export interface FetchCommentsOptions { 40 + /** AT-URI of the announcement post (e.g., at://did:plc:xxx/app.bsky.feed.post/abc123) */ 41 + bskyPostUri: string; 42 + /** Optional canonical URL to search for mentions */ 43 + canonicalUrl?: string; 44 + /** Maximum depth for nested replies (default: 3) */ 45 + maxDepth?: number; 46 + /** Optional fetch function for SSR */ 47 + fetchFn?: typeof fetch; 48 + } 49 + 50 + /** 51 + * Parse an AT-URI to extract components 52 + */ 53 + function parseAtUri(uri: string): { did: string; collection: string; rkey: string } | null { 54 + const match = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/); 55 + if (!match) return null; 56 + return { 57 + did: match[1], 58 + collection: match[2], 59 + rkey: match[3] 60 + }; 61 + } 62 + 63 + /** 64 + * Fetch a single thread of replies 65 + */ 66 + async function fetchThread( 67 + agent: AtpAgent, 68 + uri: string, 69 + maxDepth: number, 70 + currentDepth = 0 71 + ): Promise<Comment | null> { 72 + try { 73 + const response = await agent.getPostThread({ 74 + uri, 75 + depth: maxDepth - currentDepth, 76 + parentHeight: 0 77 + }); 78 + 79 + const thread = response.data.thread; 80 + 81 + if (thread.$type !== 'app.bsky.feed.defs#threadViewPost') { 82 + return null; 83 + } 84 + 85 + const post = thread.post; 86 + 87 + // Build comment object 88 + const comment: Comment = { 89 + uri: post.uri, 90 + cid: post.cid, 91 + author: { 92 + did: post.author.did, 93 + handle: post.author.handle, 94 + displayName: post.author.displayName, 95 + avatar: post.author.avatar 96 + }, 97 + text: (post.record as any)?.text || '', 98 + createdAt: post.indexedAt, 99 + likeCount: post.likeCount || 0, 100 + replyCount: post.replyCount || 0, 101 + depth: currentDepth, 102 + replies: [] 103 + }; 104 + 105 + // Process replies if within depth limit 106 + if (thread.replies && currentDepth < maxDepth) { 107 + for (const reply of thread.replies) { 108 + if (reply.$type === 'app.bsky.feed.defs#threadViewPost') { 109 + const replyComment = await fetchThread( 110 + agent, 111 + reply.post.uri, 112 + maxDepth, 113 + currentDepth + 1 114 + ); 115 + if (replyComment) { 116 + comment.replies!.push(replyComment); 117 + } 118 + } 119 + } 120 + } 121 + 122 + return comment; 123 + } catch (error) { 124 + console.error(`Failed to fetch thread for ${uri}:`, error); 125 + return null; 126 + } 127 + } 128 + 129 + /** 130 + * Fetch comments for a blog post 131 + * 132 + * @param options - Configuration options 133 + * @returns Array of top-level comments with nested replies 134 + */ 135 + export async function fetchComments(options: FetchCommentsOptions): Promise<Comment[]> { 136 + const { bskyPostUri, canonicalUrl, maxDepth = 3 } = options; 137 + 138 + // Parse the post URI 139 + const parsed = parseAtUri(bskyPostUri); 140 + if (!parsed) { 141 + console.error('Invalid AT-URI:', bskyPostUri); 142 + return []; 143 + } 144 + 145 + try { 146 + // Create agent 147 + const agent = new AtpAgent({ service: 'https://public.api.bsky.app' }); 148 + 149 + // Fetch the main thread 150 + const mainComment = await fetchThread(agent, bskyPostUri, maxDepth, 0); 151 + 152 + if (!mainComment || !mainComment.replies) { 153 + return []; 154 + } 155 + 156 + // Return only the replies (not the original post) 157 + return mainComment.replies; 158 + } catch (error) { 159 + console.error('Failed to fetch comments:', error); 160 + return []; 161 + } 162 + } 163 + 164 + /** 165 + * Search for mentions of a URL and fetch those threads as comments 166 + * 167 + * This is useful if people share your blog post on Bluesky without 168 + * replying to a specific announcement post. 169 + */ 170 + export async function fetchMentionComments( 171 + canonicalUrl: string, 172 + maxDepth = 3 173 + ): Promise<Comment[]> { 174 + try { 175 + const agent = new AtpAgent({ service: 'https://public.api.bsky.app' }); 176 + 177 + // Search for posts mentioning the URL 178 + const searchResponse = await agent.app.bsky.feed.searchPosts({ 179 + q: canonicalUrl, 180 + limit: 25 181 + }); 182 + 183 + const comments: Comment[] = []; 184 + 185 + for (const post of searchResponse.data.posts) { 186 + const comment = await fetchThread(agent, post.uri, maxDepth, 0); 187 + if (comment) { 188 + comments.push(comment); 189 + } 190 + } 191 + 192 + return comments; 193 + } catch (error) { 194 + console.error('Failed to fetch mention comments:', error); 195 + return []; 196 + } 197 + } 198 + 199 + /** 200 + * Format a relative time string (e.g., "2 hours ago") 201 + */ 202 + export function formatRelativeTime(dateString: string): string { 203 + const date = new Date(dateString); 204 + const now = new Date(); 205 + const diffMs = now.getTime() - date.getTime(); 206 + const diffSecs = Math.floor(diffMs / 1000); 207 + const diffMins = Math.floor(diffSecs / 60); 208 + const diffHours = Math.floor(diffMins / 60); 209 + const diffDays = Math.floor(diffHours / 24); 210 + 211 + if (diffSecs < 60) return 'just now'; 212 + if (diffMins < 60) return `${diffMins} minute${diffMins === 1 ? '' : 's'} ago`; 213 + if (diffHours < 24) return `${diffHours} hour${diffHours === 1 ? '' : 's'} ago`; 214 + if (diffDays < 7) return `${diffDays} day${diffDays === 1 ? '' : 's'} ago`; 215 + 216 + return date.toLocaleDateString(); 217 + }
+234
packages/svelte-standard-site/src/lib/utils/content.ts
··· 1 + /** 2 + * Content transformation utilities for standard.site 3 + * 4 + * Converts blog post content into formats suitable for ATProto: 5 + * - Strips markdown to plain text for `textContent` (search/indexing) 6 + * - Converts custom sidenotes to standard markdown 7 + * - Resolves relative links to absolute URLs 8 + * 9 + * @example 10 + * ```ts 11 + * import { transformContent, stripToPlainText } from 'svelte-standard-site/content'; 12 + * 13 + * const result = transformContent(markdown, { 14 + * baseUrl: 'https://yourblog.com', 15 + * }); 16 + * 17 + * // result.markdown - cleaned markdown for ATProto 18 + * // result.textContent - plain text for search 19 + * ``` 20 + */ 21 + 22 + export interface TransformOptions { 23 + /** Base URL of your site (e.g., 'https://yourblog.com') */ 24 + baseUrl: string; 25 + /** Optional path to the current post (e.g., '/blog/my-post') */ 26 + postPath?: string; 27 + } 28 + 29 + export interface TransformResult { 30 + /** Transformed markdown suitable for ATProto */ 31 + markdown: string; 32 + /** Plain text version for textContent field */ 33 + textContent: string; 34 + /** Word count */ 35 + wordCount: number; 36 + /** Estimated reading time in minutes */ 37 + readingTime: number; 38 + } 39 + 40 + /** 41 + * Convert HTML sidenotes to markdown blockquotes 42 + * 43 + * Transforms: 44 + * ```html 45 + * <div class="sidenote sidenote--tip"> 46 + * <span class="sidenote-label">Tip</span> 47 + * <p>Content here</p> 48 + * </div> 49 + * ``` 50 + * 51 + * Into: 52 + * ```markdown 53 + * > **Tip:** Content here 54 + * ``` 55 + */ 56 + export function convertSidenotes(markdown: string): string { 57 + // Match sidenote divs with various formats 58 + const sidenoteRegex = 59 + /<div\s+class="sidenote(?:\s+sidenote--(warning|tip))?">\s*<span\s+class="sidenote-label">([^<]+)<\/span>\s*<p>([^<]+)<\/p>\s*<\/div>/gi; 60 + 61 + return markdown.replace(sidenoteRegex, (_, type, label, content) => { 62 + // Clean up the content 63 + const cleanContent = content.trim(); 64 + const cleanLabel = label.trim(); 65 + 66 + // Convert to blockquote with label 67 + return `\n> **${cleanLabel}:** ${cleanContent}\n`; 68 + }); 69 + } 70 + 71 + /** 72 + * Convert HTML sidenotes (multi-paragraph) to markdown 73 + * Handles more complex sidenote structures 74 + */ 75 + export function convertComplexSidenotes(markdown: string): string { 76 + // First pass: simple sidenotes 77 + let result = convertSidenotes(markdown); 78 + 79 + // Second pass: sidenotes with multiple paragraphs or nested content 80 + const complexSidenoteRegex = /<div\s+class="sidenote[^"]*">([\s\S]*?)<\/div>/gi; 81 + 82 + result = result.replace(complexSidenoteRegex, (match, innerContent) => { 83 + // Extract label if present 84 + const labelMatch = innerContent.match(/<span\s+class="sidenote-label">([^<]+)<\/span>/i); 85 + const label = labelMatch ? labelMatch[1].trim() : 'Note'; 86 + 87 + // Remove the label span 88 + let content = innerContent.replace(/<span\s+class="sidenote-label">[^<]+<\/span>/gi, ''); 89 + 90 + // Convert remaining HTML to plain text with basic formatting 91 + content = content 92 + .replace(/<p>/gi, '') 93 + .replace(/<\/p>/gi, '\n') 94 + .replace(/<strong>/gi, '**') 95 + .replace(/<\/strong>/gi, '**') 96 + .replace(/<em>/gi, '*') 97 + .replace(/<\/em>/gi, '*') 98 + .replace(/<a\s+href="([^"]+)"[^>]*>([^<]+)<\/a>/gi, '[$2]($1)') 99 + .replace(/<[^>]+>/g, '') // Remove any remaining HTML tags 100 + .trim(); 101 + 102 + // Format as blockquote 103 + const lines = content.split('\n').filter((line: string) => line.trim()); 104 + const quotedLines = lines.map((line: string, i: number) => 105 + i === 0 ? `> **${label}:** ${line}` : `> ${line}` 106 + ); 107 + 108 + return '\n' + quotedLines.join('\n') + '\n'; 109 + }); 110 + 111 + return result; 112 + } 113 + 114 + /** 115 + * Resolve relative URLs to absolute URLs 116 + * 117 + * Converts: 118 + * - `[Link](/page)` → `[Link](https://example.com/page)` 119 + * - `![Image](/img.png)` → `![Image](https://example.com/img.png)` 120 + */ 121 + export function resolveRelativeLinks(markdown: string, baseUrl: string): string { 122 + const cleanBaseUrl = baseUrl.replace(/\/$/, ''); 123 + 124 + // Match markdown links and images with relative URLs 125 + // [text](/path) or ![alt](/path) 126 + const linkRegex = /(!?\[[^\]]*\])\((?!https?:\/\/|mailto:|#)([^)]+)\)/g; 127 + 128 + return markdown.replace(linkRegex, (_, prefix, path) => { 129 + // Ensure path starts with / 130 + const absolutePath = path.startsWith('/') ? path : `/${path}`; 131 + return `${prefix}(${cleanBaseUrl}${absolutePath})`; 132 + }); 133 + } 134 + 135 + /** 136 + * Strip markdown to plain text for indexing/search 137 + * 138 + * Removes: 139 + * - Markdown formatting (**, *, #, etc.) 140 + * - Links (keeps link text) 141 + * - Images 142 + * - Code blocks 143 + * - HTML tags 144 + */ 145 + export function stripToPlainText(markdown: string): string { 146 + let text = markdown; 147 + 148 + // Remove code blocks first (preserve nothing) 149 + text = text.replace(/```[\s\S]*?```/g, ''); 150 + text = text.replace(/`[^`]+`/g, ''); 151 + 152 + // Remove images 153 + text = text.replace(/!\[[^\]]*\]\([^)]+\)/g, ''); 154 + 155 + // Convert links to just their text 156 + text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1'); 157 + 158 + // Remove HTML tags 159 + text = text.replace(/<[^>]+>/g, ''); 160 + 161 + // Remove heading markers 162 + text = text.replace(/^#{1,6}\s+/gm, ''); 163 + 164 + // Remove bold/italic markers 165 + text = text.replace(/\*\*([^*]+)\*\*/g, '$1'); 166 + text = text.replace(/\*([^*]+)\*/g, '$1'); 167 + text = text.replace(/__([^_]+)__/g, '$1'); 168 + text = text.replace(/_([^_]+)_/g, '$1'); 169 + 170 + // Remove blockquote markers 171 + text = text.replace(/^>\s*/gm, ''); 172 + 173 + // Remove list markers 174 + text = text.replace(/^[\s]*[-*+]\s+/gm, ''); 175 + text = text.replace(/^[\s]*\d+\.\s+/gm, ''); 176 + 177 + // Remove horizontal rules 178 + text = text.replace(/^[-*_]{3,}$/gm, ''); 179 + 180 + // Collapse multiple newlines 181 + text = text.replace(/\n{3,}/g, '\n\n'); 182 + 183 + // Trim whitespace 184 + text = text.trim(); 185 + 186 + return text; 187 + } 188 + 189 + /** 190 + * Calculate word count from text 191 + */ 192 + export function countWords(text: string): number { 193 + return text.split(/\s+/).filter((word) => word.length > 0).length; 194 + } 195 + 196 + /** 197 + * Calculate reading time in minutes (assuming 200 words per minute) 198 + */ 199 + export function calculateReadingTime(wordCount: number, wordsPerMinute = 200): number { 200 + return Math.max(1, Math.ceil(wordCount / wordsPerMinute)); 201 + } 202 + 203 + /** 204 + * Full content transformation pipeline 205 + * 206 + * Takes raw markdown (with HTML sidenotes) and produces: 207 + * - Clean markdown suitable for ATProto platforms 208 + * - Plain text for search/indexing 209 + * - Metadata (word count, reading time) 210 + */ 211 + export function transformContent(rawMarkdown: string, options: TransformOptions): TransformResult { 212 + // Step 1: Convert sidenotes from HTML to markdown blockquotes 213 + let markdown = convertComplexSidenotes(rawMarkdown); 214 + 215 + // Step 2: Resolve relative links to absolute URLs 216 + markdown = resolveRelativeLinks(markdown, options.baseUrl); 217 + 218 + // Step 3: Clean up extra whitespace 219 + markdown = markdown.replace(/\n{3,}/g, '\n\n').trim(); 220 + 221 + // Step 4: Generate plain text version 222 + const textContent = stripToPlainText(markdown); 223 + 224 + // Step 5: Calculate metadata 225 + const wordCount = countWords(textContent); 226 + const readingTime = calculateReadingTime(wordCount); 227 + 228 + return { 229 + markdown, 230 + textContent, 231 + wordCount, 232 + readingTime 233 + }; 234 + }
+41
packages/svelte-standard-site/src/lib/utils/document.ts
··· 1 + /** 2 + * Extract rkey from AT URI 3 + */ 4 + export function extractRkey(uri: string): string | null { 5 + const match = uri.match(/at:\/\/[^\/]+\/[^\/]+\/(.+)$/); 6 + return match ? match[1] : null; 7 + } 8 + 9 + /** 10 + * Get document slug (uses path if available, otherwise rkey) 11 + */ 12 + export function getDocumentSlug(document: { uri: string; value: { path?: string } }): string { 13 + if (document.value.path) { 14 + // Remove leading slash if present 15 + return document.value.path.replace(/^\//, ''); 16 + } 17 + return extractRkey(document.uri) || ''; 18 + } 19 + 20 + /** 21 + * Get document canonical URL 22 + */ 23 + export function getDocumentUrl(document: { 24 + value: { site: string; path?: string }; 25 + uri: string; 26 + }): string { 27 + const { site, path } = document.value; 28 + 29 + // If site is an AT URI, we'll use our local route 30 + if (site.startsWith('at://')) { 31 + return `/documents/${getDocumentSlug(document as any)}`; 32 + } 33 + 34 + // If site is HTTPS and we have a path, combine them 35 + if (path) { 36 + return `${site.replace(/\/$/, '')}/${path.replace(/^\//, '')}`; 37 + } 38 + 39 + // Fallback to local route 40 + return `/documents/${extractRkey(document.uri)}`; 41 + }
+87
packages/svelte-standard-site/src/lib/utils/theme-helpers.ts
··· 1 + import type { BasicTheme } from '../types.js'; 2 + import { rgbToCSS } from './theme.js'; 3 + 4 + /** 5 + * Generate color-mix CSS for theme colors with transparency 6 + */ 7 + export function mixThemeColor(variable: string, opacity: number, fallback = 'transparent'): string { 8 + return `color-mix(in srgb, var(${variable}) ${opacity}%, ${fallback})`; 9 + } 10 + 11 + /** 12 + * Get theme-aware color styles for text 13 + */ 14 + export function getThemedTextColor( 15 + hasTheme: boolean, 16 + opacity = 100 17 + ): { 18 + color?: string; 19 + } { 20 + if (!hasTheme) return {}; 21 + return opacity === 100 22 + ? { color: 'var(--theme-foreground)' } 23 + : { color: mixThemeColor('--theme-foreground', opacity) }; 24 + } 25 + 26 + /** 27 + * Get theme-aware background color 28 + */ 29 + export function getThemedBackground( 30 + hasTheme: boolean, 31 + opacity?: number 32 + ): { 33 + backgroundColor?: string; 34 + } { 35 + if (!hasTheme) return {}; 36 + if (opacity === undefined) { 37 + return { backgroundColor: 'var(--theme-background)' }; 38 + } 39 + return { backgroundColor: mixThemeColor('--theme-background', opacity) }; 40 + } 41 + 42 + /** 43 + * Get theme-aware border color 44 + */ 45 + export function getThemedBorder( 46 + hasTheme: boolean, 47 + opacity = 20 48 + ): { 49 + borderColor?: string; 50 + } { 51 + if (!hasTheme) return {}; 52 + return { borderColor: mixThemeColor('--theme-foreground', opacity) }; 53 + } 54 + 55 + /** 56 + * Get theme-aware accent color 57 + */ 58 + export function getThemedAccent( 59 + hasTheme: boolean, 60 + opacity?: number 61 + ): { 62 + color?: string; 63 + backgroundColor?: string; 64 + } { 65 + if (!hasTheme) return {}; 66 + if (opacity === undefined) { 67 + return { color: 'var(--theme-accent)' }; 68 + } 69 + return { 70 + backgroundColor: mixThemeColor('--theme-accent', opacity), 71 + color: 'var(--theme-accent)' 72 + }; 73 + } 74 + 75 + /** 76 + * Convert BasicTheme to CSS custom properties 77 + */ 78 + export function themeToCssVars(theme?: BasicTheme): Record<string, string> { 79 + if (!theme) return {}; 80 + 81 + return { 82 + '--theme-background': rgbToCSS(theme.background), 83 + '--theme-foreground': rgbToCSS(theme.foreground), 84 + '--theme-accent': rgbToCSS(theme.accent), 85 + '--theme-accent-foreground': rgbToCSS(theme.accentForeground) 86 + }; 87 + }
+33
packages/svelte-standard-site/src/lib/utils/theme.ts
··· 1 + import type { RGBColor } from '../types.js'; 2 + 3 + /** 4 + * Convert RGB color object to CSS rgb() string 5 + */ 6 + export function rgbToCSS(color: RGBColor): string { 7 + return `rgb(${color.r}, ${color.g}, ${color.b})`; 8 + } 9 + 10 + /** 11 + * Convert RGB color object to hex string 12 + */ 13 + export function rgbToHex(color: RGBColor): string { 14 + const toHex = (n: number) => n.toString(16).padStart(2, '0'); 15 + return `#${toHex(color.r)}${toHex(color.g)}${toHex(color.b)}`; 16 + } 17 + 18 + /** 19 + * Get theme CSS variables from BasicTheme 20 + */ 21 + export function getThemeVars(theme: { 22 + background: RGBColor; 23 + foreground: RGBColor; 24 + accent: RGBColor; 25 + accentForeground: RGBColor; 26 + }): Record<string, string> { 27 + return { 28 + '--theme-background': rgbToCSS(theme.background), 29 + '--theme-foreground': rgbToCSS(theme.foreground), 30 + '--theme-accent': rgbToCSS(theme.accent), 31 + '--theme-accent-foreground': rgbToCSS(theme.accentForeground) 32 + }; 33 + }
+180
packages/svelte-standard-site/src/lib/utils/verification.ts
··· 1 + /** 2 + * Verification utilities for proving content ownership 3 + * 4 + * Creates `.well-known` endpoints and `<link>` tags to verify that 5 + * you own the content published to ATProto. 6 + * 7 + * @example 8 + * ```ts 9 + * // SvelteKit endpoint: src/routes/.well-known/site.standard.publication/+server.ts 10 + * import { generatePublicationWellKnown } from 'svelte-standard-site/verification'; 11 + * import { text } from '@sveltejs/kit'; 12 + * 13 + * export function GET() { 14 + * return text( 15 + * generatePublicationWellKnown({ 16 + * did: 'did:plc:xxx', 17 + * publicationRkey: '3abc123xyz', 18 + * }) 19 + * ); 20 + * } 21 + * ``` 22 + */ 23 + 24 + /** 25 + * Parse an AT-URI to extract its components 26 + */ 27 + export function parseAtUri(uri: string): { did: string; collection: string; rkey: string } | null { 28 + const match = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/); 29 + if (!match) return null; 30 + return { 31 + did: match[1], 32 + collection: match[2], 33 + rkey: match[3] 34 + }; 35 + } 36 + 37 + /** 38 + * Build an AT-URI for a document 39 + */ 40 + export function getDocumentAtUri(did: string, rkey: string): string { 41 + return `at://${did}/site.standard.document/${rkey}`; 42 + } 43 + 44 + /** 45 + * Build an AT-URI for a publication 46 + */ 47 + export function getPublicationAtUri(did: string, rkey: string): string { 48 + return `at://${did}/site.standard.publication/${rkey}`; 49 + } 50 + 51 + /** 52 + * Generate content for /.well-known/site.standard.publication endpoint 53 + * 54 + * This endpoint proves you own your publication record on ATProto. 55 + * 56 + * @example 57 + * ```ts 58 + * // src/routes/.well-known/site.standard.publication/+server.ts 59 + * import { generatePublicationWellKnown } from 'svelte-standard-site/verification'; 60 + * import { text } from '@sveltejs/kit'; 61 + * 62 + * export function GET() { 63 + * return text( 64 + * generatePublicationWellKnown({ 65 + * did: 'did:plc:xxx', 66 + * publicationRkey: '3abc123xyz', 67 + * }) 68 + * ); 69 + * } 70 + * ``` 71 + */ 72 + export function generatePublicationWellKnown(options: { 73 + did: string; 74 + publicationRkey: string; 75 + }): string { 76 + return getPublicationAtUri(options.did, options.publicationRkey); 77 + } 78 + 79 + /** 80 + * Generate a <link> tag for document verification 81 + * 82 + * Add this to your document's <head> to verify ownership. 83 + * 84 + * @example 85 + * ```svelte 86 + * <svelte:head> 87 + * {@html generateDocumentLinkTag({ 88 + * did: 'did:plc:xxx', 89 + * documentRkey: '3xyz789abc', 90 + * })} 91 + * </svelte:head> 92 + * ``` 93 + */ 94 + export function generateDocumentLinkTag(options: { did: string; documentRkey: string }): string { 95 + const atUri = getDocumentAtUri(options.did, options.documentRkey); 96 + return `<link rel="site.standard.document" href="${atUri}">`; 97 + } 98 + 99 + /** 100 + * Generate a <link> tag for publication verification 101 + * 102 + * Add this to your site's <head> to verify publication ownership. 103 + * 104 + * @example 105 + * ```svelte 106 + * <svelte:head> 107 + * {@html generatePublicationLinkTag({ 108 + * did: 'did:plc:xxx', 109 + * publicationRkey: '3abc123xyz', 110 + * })} 111 + * </svelte:head> 112 + * ``` 113 + */ 114 + export function generatePublicationLinkTag(options: { 115 + did: string; 116 + publicationRkey: string; 117 + }): string { 118 + const atUri = getPublicationAtUri(options.did, options.publicationRkey); 119 + return `<link rel="site.standard.publication" href="${atUri}">`; 120 + } 121 + 122 + /** 123 + * Verify that a well-known endpoint returns the expected AT-URI 124 + * 125 + * @example 126 + * ```ts 127 + * const isValid = await verifyPublicationWellKnown( 128 + * 'https://yourblog.com', 129 + * 'did:plc:xxx', 130 + * '3abc123xyz' 131 + * ); 132 + * ``` 133 + */ 134 + export async function verifyPublicationWellKnown( 135 + siteUrl: string, 136 + did: string, 137 + publicationRkey: string 138 + ): Promise<boolean> { 139 + try { 140 + const cleanUrl = siteUrl.replace(/\/$/, ''); 141 + const response = await fetch(`${cleanUrl}/.well-known/site.standard.publication`); 142 + 143 + if (!response.ok) return false; 144 + 145 + const content = await response.text(); 146 + const expectedUri = getPublicationAtUri(did, publicationRkey); 147 + 148 + return content.trim() === expectedUri; 149 + } catch (error) { 150 + console.error('Failed to verify publication well-known:', error); 151 + return false; 152 + } 153 + } 154 + 155 + /** 156 + * Extract AT-URI from a <link> tag in HTML 157 + * 158 + * @example 159 + * ```ts 160 + * const html = '<link rel="site.standard.document" href="at://did:plc:xxx/site.standard.document/3xyz">'; 161 + * const uri = extractDocumentLinkFromHtml(html); 162 + * // => 'at://did:plc:xxx/site.standard.document/3xyz' 163 + * ``` 164 + */ 165 + export function extractDocumentLinkFromHtml(html: string): string | null { 166 + const match = html.match( 167 + /<link\s+rel="site\.standard\.document"\s+href="(at:\/\/[^"]+)"\s*\/?>/i 168 + ); 169 + return match ? match[1] : null; 170 + } 171 + 172 + /** 173 + * Extract publication AT-URI from a <link> tag in HTML 174 + */ 175 + export function extractPublicationLinkFromHtml(html: string): string | null { 176 + const match = html.match( 177 + /<link\s+rel="site\.standard\.publication"\s+href="(at:\/\/[^"]+)"\s*\/?>/i 178 + ); 179 + return match ? match[1] : null; 180 + }
+12
packages/svelte-standard-site/src/routes/+layout.svelte
··· 1 + <script lang="ts"> 2 + import '../lib/styles/base.css'; 3 + import type { Snippet } from 'svelte'; 4 + 5 + interface Props { 6 + children: Snippet; 7 + } 8 + 9 + let { children }: Props = $props(); 10 + </script> 11 + 12 + {@render children()}
+40
packages/svelte-standard-site/src/routes/+page.server.ts
··· 1 + import { createClient } from '$lib/index.js'; 2 + import { getConfigFromEnv } from '$lib/config/env.js'; 3 + import type { PageServerLoad } from './$types.js'; 4 + 5 + export const load: PageServerLoad = async ({ fetch }) => { 6 + const config = getConfigFromEnv(); 7 + 8 + if (!config) { 9 + return { 10 + error: 'Missing configuration. Please set PUBLIC_ATPROTO_DID in your .env file.', 11 + publications: [], 12 + documents: [] 13 + }; 14 + } 15 + 16 + const client = createClient(config); 17 + 18 + try { 19 + const [publications, documents] = await Promise.all([ 20 + client.fetchAllPublications(fetch), 21 + client.fetchAllDocuments(fetch) 22 + ]); 23 + 24 + const pds = await client.getPDS(fetch); 25 + 26 + return { 27 + config, 28 + publications: JSON.parse(JSON.stringify(publications)), 29 + documents: JSON.parse(JSON.stringify(documents)), 30 + pds 31 + }; 32 + } catch (error) { 33 + console.error('Failed to load data:', error); 34 + return { 35 + error: error instanceof Error ? error.message : 'Failed to load data', 36 + publications: [], 37 + documents: [] 38 + }; 39 + } 40 + };
+211
packages/svelte-standard-site/src/routes/+page.svelte
··· 1 + <script lang="ts"> 2 + import type { PageData } from './$types.js'; 3 + import { PublicationCard, DocumentCard, StandardSiteLayout } from '$lib/components/index.js'; 4 + 5 + const { data }: { data: PageData } = $props(); 6 + </script> 7 + 8 + <svelte:head> 9 + <title>svelte-standard-site - Showcase</title> 10 + <meta 11 + name="description" 12 + content="A SvelteKit library for fetching site.standard.* records from AT Protocol" 13 + /> 14 + </svelte:head> 15 + 16 + <StandardSiteLayout title="svelte-standard-site" showThemeToggle={true}> 17 + <div class="mx-auto max-w-7xl"> 18 + <!-- Hero Section --> 19 + <header class="mb-16 text-center"> 20 + <h1 class="text-ink-900 dark:text-ink-50 mb-4 text-5xl font-bold tracking-tight"> 21 + svelte-standard-site 22 + </h1> 23 + <p class="text-ink-700 dark:text-ink-200 mx-auto mb-6 max-w-2xl text-xl"> 24 + A powerful SvelteKit library for fetching <code 25 + class="bg-canvas-200 dark:bg-canvas-800 rounded px-2 py-1 text-base">site.standard.*</code 26 + > records from the AT Protocol ecosystem 27 + </p> 28 + <div class="flex items-center justify-center gap-4"> 29 + <a 30 + href="https://github.com/ewanc26/svelte-standard-site" 31 + target="_blank" 32 + rel="noopener noreferrer" 33 + class="bg-primary-600 hover:bg-primary-700 focus-visible:outline-primary-600 dark:bg-primary-500 dark:hover:bg-primary-600 rounded-lg px-6 py-3 font-semibold text-white transition focus-visible:outline-2 focus-visible:outline-offset-2" 34 + > 35 + View on GitHub 36 + </a> 37 + </div> 38 + </header> 39 + 40 + {#if data.error} 41 + <!-- Error State --> 42 + <div 43 + class="border-accent-300 bg-accent-50 dark:border-accent-700 dark:bg-accent-950 mb-12 rounded-xl border-2 p-8" 44 + > 45 + <div class="flex items-start gap-4"> 46 + <svg 47 + class="text-accent-600 dark:text-accent-400 size-6 shrink-0" 48 + fill="none" 49 + viewBox="0 0 24 24" 50 + stroke="currentColor" 51 + > 52 + <path 53 + stroke-linecap="round" 54 + stroke-linejoin="round" 55 + stroke-width="2" 56 + d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" 57 + /> 58 + </svg> 59 + <div class="flex-1"> 60 + <h2 class="text-accent-900 dark:text-accent-50 mb-2 text-xl font-bold"> 61 + Configuration Error 62 + </h2> 63 + <p class="text-accent-800 dark:text-accent-200 mb-4">{data.error}</p> 64 + <div class="bg-canvas-50 dark:bg-canvas-950 rounded-lg p-4"> 65 + <p class="text-ink-900 dark:text-ink-50 mb-2 font-semibold">Quick Fix:</p> 66 + <ol class="text-ink-700 dark:text-ink-200 list-inside list-decimal space-y-2 text-sm"> 67 + <li> 68 + Create a <code class="bg-canvas-200 dark:bg-canvas-800 rounded px-2 py-1" 69 + >.env</code 70 + > file in your project root 71 + </li> 72 + <li> 73 + Add: <code class="bg-canvas-200 dark:bg-canvas-800 rounded px-2 py-1" 74 + >PUBLIC_ATPROTO_DID=your-did-here</code 75 + > 76 + </li> 77 + <li>Restart your dev server</li> 78 + </ol> 79 + </div> 80 + </div> 81 + </div> 82 + </div> 83 + {:else} 84 + <!-- Configuration Info --> 85 + <section class="bg-primary-50 dark:bg-primary-950 mb-16 rounded-xl p-8"> 86 + <h2 class="text-ink-900 dark:text-ink-50 mb-6 text-2xl font-bold">Active Configuration</h2> 87 + <div class="grid gap-6 md:grid-cols-2"> 88 + <div class="bg-canvas-50 dark:bg-canvas-950 rounded-lg p-6 shadow-sm"> 89 + <dt class="text-ink-700 dark:text-ink-200 mb-2 text-sm font-medium">DID</dt> 90 + <dd class="text-ink-900 dark:text-ink-50 font-mono text-sm break-all"> 91 + {data.config?.did} 92 + </dd> 93 + </div> 94 + <div class="bg-canvas-50 dark:bg-canvas-950 rounded-lg p-6 shadow-sm"> 95 + <dt class="text-ink-700 dark:text-ink-200 mb-2 text-sm font-medium">PDS Endpoint</dt> 96 + <dd class="text-ink-900 dark:text-ink-50 font-mono text-sm break-all"> 97 + {data.pds || 'Auto-resolved'} 98 + </dd> 99 + </div> 100 + </div> 101 + </section> 102 + 103 + <!-- Publications Section --> 104 + <section class="mb-16"> 105 + <div class="mb-8 flex items-end justify-between gap-4"> 106 + <div class="min-w-0 flex-1"> 107 + <h2 class="text-ink-900 dark:text-ink-50 text-3xl font-bold">Publications</h2> 108 + <p class="text-ink-700 dark:text-ink-200 mt-2"> 109 + site.standard.publication records from the configured repository 110 + </p> 111 + </div> 112 + <div 113 + class="bg-primary-100 text-primary-800 dark:bg-primary-900 dark:text-primary-100 rounded-full px-4 py-2 text-sm font-semibold whitespace-nowrap" 114 + > 115 + {data.publications.length} found 116 + </div> 117 + </div> 118 + {#if data.publications.length === 0} 119 + <div 120 + class="border-canvas-300 dark:border-canvas-700 rounded-xl border-2 border-dashed p-12 text-center" 121 + > 122 + <svg 123 + class="text-ink-500 dark:text-ink-400 mx-auto mb-4 size-12" 124 + fill="none" 125 + viewBox="0 0 24 24" 126 + stroke="currentColor" 127 + > 128 + <path 129 + stroke-linecap="round" 130 + stroke-linejoin="round" 131 + stroke-width="2" 132 + d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" 133 + /> 134 + </svg> 135 + <h3 class="text-ink-900 dark:text-ink-50 mb-2 text-lg font-semibold"> 136 + No Publications Found 137 + </h3> 138 + <p class="text-ink-700 dark:text-ink-200"> 139 + This repository doesn't have any <code 140 + class="bg-canvas-200 dark:bg-canvas-800 rounded px-2 py-1" 141 + >site.standard.publication</code 142 + > records yet. 143 + </p> 144 + </div> 145 + {:else} 146 + <div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3"> 147 + {#each data.publications as publication} 148 + <PublicationCard {publication} /> 149 + {/each} 150 + </div> 151 + {/if} 152 + </section> 153 + 154 + <!-- Documents Section --> 155 + <section> 156 + <div class="mb-8 flex items-end justify-between gap-4"> 157 + <div class="min-w-0 flex-1"> 158 + <h2 class="text-ink-900 dark:text-ink-50 text-3xl font-bold">Documents</h2> 159 + <p class="text-ink-700 dark:text-ink-200 mt-2"> 160 + <code class="bg-canvas-200 dark:bg-canvas-800 rounded px-2 py-1" 161 + >site.standard.document</code 162 + > records, sorted by publication date 163 + </p> 164 + </div> 165 + <div 166 + class="bg-secondary-100 text-secondary-800 dark:bg-secondary-900 dark:text-secondary-100 rounded-full px-4 py-2 text-sm font-semibold whitespace-nowrap" 167 + > 168 + {data.documents.length} found 169 + </div> 170 + </div> 171 + {#if data.documents.length === 0} 172 + <div 173 + class="border-canvas-300 dark:border-canvas-700 rounded-xl border-2 border-dashed p-12 text-center" 174 + > 175 + <svg 176 + class="text-ink-500 dark:text-ink-400 mx-auto mb-4 size-12" 177 + fill="none" 178 + viewBox="0 0 24 24" 179 + stroke="currentColor" 180 + > 181 + <path 182 + stroke-linecap="round" 183 + stroke-linejoin="round" 184 + stroke-width="2" 185 + d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" 186 + /> 187 + </svg> 188 + <h3 class="text-ink-900 dark:text-ink-50 mb-2 text-lg font-semibold"> 189 + No Documents Found 190 + </h3> 191 + <p class="text-ink-700 dark:text-ink-200"> 192 + This repository doesn't have any <code 193 + class="bg-canvas-200 dark:bg-canvas-800 rounded px-2 py-1" 194 + >site.standard.document</code 195 + > records yet. 196 + </p> 197 + </div> 198 + {:else} 199 + <div class="space-y-6"> 200 + {#each data.documents as document} 201 + {@const publication = 202 + data.publications.find((pub: { uri: any }) => pub.uri === document.value.site) ?? 203 + undefined} 204 + <DocumentCard {document} {publication} /> 205 + {/each} 206 + </div> 207 + {/if} 208 + </section> 209 + {/if} 210 + </div> 211 + </StandardSiteLayout>
+53
packages/svelte-standard-site/src/routes/[pub_rkey]/[doc_rkey]/+page.server.ts
··· 1 + import { createClient } from '$lib/index.js'; 2 + import { getConfigFromEnv } from '$lib/config/env.js'; 3 + import { extractRkey } from '$lib/utils/document.js'; 4 + import { error } from '@sveltejs/kit'; 5 + import type { PageServerLoad } from './$types.js'; 6 + 7 + export const load: PageServerLoad = async ({ params, fetch }) => { 8 + const config = getConfigFromEnv(); 9 + 10 + if (!config) { 11 + throw error(500, 'Missing configuration. Please set PUBLIC_ATPROTO_DID in your .env file.'); 12 + } 13 + 14 + const client = createClient(config); 15 + const { pub_rkey, doc_rkey } = params; 16 + 17 + try { 18 + // 1. Fetch the specific document by its rkey 19 + // Assuming client.fetchDocument exists; if not, use fetchAllDocuments and find 20 + const documents = await client.fetchAllDocuments(fetch); 21 + 22 + const document = documents.find((doc) => { 23 + const rkey = extractRkey(doc.uri); 24 + const parentPubRkey = extractRkey(doc.value.site); 25 + // Validate both the document rkey AND that it belongs to the publication in the URL 26 + return rkey === doc_rkey && parentPubRkey === pub_rkey; 27 + }); 28 + 29 + if (!document) { 30 + throw error(404, 'Document not found under this publication'); 31 + } 32 + 33 + // 2. Fetch the publication metadata 34 + const publication = await client.fetchPublication(pub_rkey, fetch); 35 + 36 + if (!publication) { 37 + throw error(404, 'Publication not found'); 38 + } 39 + 40 + // 3. Return sanitized data to prevent "non-POJO" serialization errors 41 + return { 42 + document: JSON.parse(JSON.stringify(document)), 43 + publication: JSON.parse(JSON.stringify(publication)), 44 + config 45 + }; 46 + } catch (err) { 47 + console.error('Failed to load document:', err); 48 + if (err && typeof err === 'object' && 'status' in err) { 49 + throw err; 50 + } 51 + throw error(500, 'Failed to load document'); 52 + } 53 + };
+191
packages/svelte-standard-site/src/routes/[pub_rkey]/[doc_rkey]/+page.svelte
··· 1 + <script lang="ts"> 2 + import type { PageData } from './$types.js'; 3 + import { getThemeVars } from '$lib/utils/theme.js'; 4 + import { extractRkey } from '$lib/utils/document.js'; 5 + import { ThemedContainer, ThemedText, DateDisplay, TagList } from '$lib/components/index.js'; 6 + import { mixThemeColor } from '$lib/utils/theme-helpers.js'; 7 + import DocumentRenderer from '$lib/components/document/DocumentRenderer.svelte'; 8 + 9 + const { data }: { data: PageData } = $props(); 10 + 11 + const themeVars = $derived( 12 + data.publication?.value.basicTheme ? getThemeVars(data.publication.value.basicTheme) : {} 13 + ); 14 + const hasTheme = $derived(!!data.publication?.value.basicTheme); 15 + </script> 16 + 17 + <svelte:head> 18 + <title>{data.document.value.title}</title> 19 + {#if data.document.value.description} 20 + <meta name="description" content={data.document.value.description} /> 21 + {/if} 22 + {#if hasTheme} 23 + <!-- prettier-ignore --> 24 + <style> 25 + body {{ 26 + background-color: {themeVars['--theme-background'] || 'var(--color-canvas-50)'} !important; 27 + }} 28 + </style> 29 + {/if} 30 + </svelte:head> 31 + 32 + <ThemedContainer theme={data.publication?.value.basicTheme} class="flex flex-col"> 33 + <!-- Header Bar --> 34 + <header 35 + class="sticky top-0 z-10 border-b backdrop-blur-sm" 36 + style:border-color={hasTheme ? mixThemeColor('--theme-foreground', 15) : undefined} 37 + style:background-color={hasTheme ? mixThemeColor('--theme-background', 80) : undefined} 38 + > 39 + <nav class="mx-auto flex max-w-4xl items-center justify-between px-6 py-4"> 40 + <a 41 + href="/" 42 + class="group flex items-center gap-2 text-sm font-medium transition-all hover:gap-3" 43 + style:color={hasTheme ? 'var(--theme-accent)' : undefined} 44 + > 45 + <svg 46 + class="size-4 transition-transform group-hover:-translate-x-0.5" 47 + fill="none" 48 + viewBox="0 0 24 24" 49 + stroke="currentColor" 50 + stroke-width="2.5" 51 + > 52 + <path stroke-linecap="round" stroke-linejoin="round" d="M10 19l-7-7m0 0l7-7m-7 7h18" /> 53 + </svg> 54 + Back to Home 55 + </a> 56 + 57 + {#if data.publication} 58 + <a 59 + href={data.publication.value.url || '/'} 60 + target={data.publication.value.url ? '_blank' : undefined} 61 + rel={data.publication.value.url ? 'noopener noreferrer' : undefined} 62 + class="flex items-center gap-2 transition-opacity hover:opacity-70" 63 + > 64 + {#if data.publication.value.icon} 65 + <img 66 + src={data.publication.value.icon} 67 + alt="{data.publication.value.name} icon" 68 + class="size-6 rounded object-cover" 69 + /> 70 + {/if} 71 + <ThemedText {hasTheme} element="span" class="text-sm font-semibold"> 72 + {data.publication.value.name} 73 + </ThemedText> 74 + </a> 75 + {/if} 76 + </nav> 77 + </header> 78 + 79 + <!-- Main Content --> 80 + <article class="mx-auto max-w-3xl px-6 py-12"> 81 + <!-- Title Section --> 82 + <header class="mb-12"> 83 + <ThemedText {hasTheme} element="h1" class="mb-6 text-5xl font-bold leading-tight tracking-tight"> 84 + {data.document.value.title} 85 + </ThemedText> 86 + 87 + <div 88 + class="flex flex-wrap items-center gap-4 text-sm" 89 + style:color={hasTheme ? mixThemeColor('--theme-foreground', 60) : undefined} 90 + > 91 + <DateDisplay date={data.document.value.publishedAt} /> 92 + {#if data.document.value.updatedAt} 93 + <span 94 + class="flex items-center gap-1.5" 95 + style:color={hasTheme ? mixThemeColor('--theme-foreground', 50) : undefined} 96 + > 97 + <svg class="size-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 98 + <path 99 + stroke-linecap="round" 100 + stroke-linejoin="round" 101 + stroke-width="2" 102 + d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" 103 + /> 104 + </svg> 105 + Updated <DateDisplay date={data.document.value.updatedAt} /> 106 + </span> 107 + {/if} 108 + </div> 109 + </header> 110 + 111 + <!-- Cover Image --> 112 + {#if data.document.value.coverImage} 113 + <div class="-mx-6 mb-12 sm:mx-0"> 114 + <img 115 + src={data.document.value.coverImage} 116 + alt="{data.document.value.title} cover" 117 + class="w-full rounded-none object-cover sm:rounded-2xl" 118 + style="max-height: 28rem; object-position: center;" 119 + /> 120 + </div> 121 + {/if} 122 + 123 + <!-- Document Content --> 124 + <DocumentRenderer document={data.document.value} {hasTheme} /> 125 + 126 + <!-- Tags --> 127 + {#if data.document.value.tags && data.document.value.tags.length > 0} 128 + <div class="mt-16 pt-8"> 129 + <TagList tags={data.document.value.tags} {hasTheme} /> 130 + </div> 131 + {/if} 132 + 133 + <!-- Bluesky Reference --> 134 + {#if data.document.value.bskyPostRef} 135 + <div 136 + class="mt-12 rounded-2xl border p-6" 137 + style:border-color={hasTheme ? mixThemeColor('--theme-accent', 30) : undefined} 138 + style:background-color={hasTheme ? mixThemeColor('--theme-accent', 10) : undefined} 139 + > 140 + <a 141 + href="https://bsky.app/profile/{data.config?.did}/post/{extractRkey( 142 + data.document.value.bskyPostRef.uri 143 + )}" 144 + target="_blank" 145 + rel="noopener noreferrer" 146 + class="flex items-center gap-3 font-medium transition-all hover:gap-4" 147 + style:color={hasTheme ? 'var(--theme-accent)' : undefined} 148 + > 149 + <svg class="size-5 shrink-0" fill="currentColor" viewBox="0 0 24 24"> 150 + <path 151 + d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.039.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 0 1-.415-.056c.14.017.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8Z" 152 + /> 153 + </svg> 154 + <span>Continue the conversation on Bluesky</span> 155 + <svg class="size-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 156 + <path 157 + stroke-linecap="round" 158 + stroke-linejoin="round" 159 + stroke-width="2" 160 + d="M14 5l7 7m0 0l-7 7m7-7H3" 161 + /> 162 + </svg> 163 + </a> 164 + </div> 165 + {/if} 166 + </article> 167 + 168 + <!-- Footer --> 169 + <footer 170 + class="mt-auto border-t py-8" 171 + style:border-color={hasTheme ? mixThemeColor('--theme-foreground', 15) : undefined} 172 + > 173 + <div class="mx-auto max-w-3xl px-6 text-center"> 174 + <p 175 + class="text-sm" 176 + style:color={hasTheme ? mixThemeColor('--theme-foreground', 50) : undefined} 177 + > 178 + Powered by 179 + <a 180 + href="https://atproto.com" 181 + target="_blank" 182 + rel="noopener noreferrer" 183 + class="font-medium underline decoration-1 underline-offset-2 transition-all hover:decoration-2" 184 + style:color={hasTheme ? 'var(--theme-accent)' : undefined} 185 + > 186 + AT Protocol 187 + </a> 188 + </p> 189 + </div> 190 + </footer> 191 + </ThemedContainer>
+2
packages/svelte-standard-site/src/routes/layout.css
··· 1 + @import 'tailwindcss'; 2 + @plugin '@tailwindcss/typography';
+1
packages/svelte-standard-site/static/favicon.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>
+18
packages/svelte-standard-site/svelte.config.js
··· 1 + import adapter from '@sveltejs/adapter-auto'; 2 + import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 + 4 + /** @type {import('@sveltejs/kit').Config} */ 5 + const config = { 6 + // Consult https://svelte.dev/docs/kit/integrations 7 + // for more information about preprocessors 8 + preprocess: vitePreprocess(), 9 + 10 + kit: { 11 + // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. 12 + // If your environment is not supported, or you settled on a specific environment, switch out the adapter. 13 + // See https://svelte.dev/docs/kit/adapters for more information about adapters. 14 + adapter: adapter() 15 + } 16 + }; 17 + 18 + export default config;
+15
packages/svelte-standard-site/tsconfig.json
··· 1 + { 2 + "extends": "./.svelte-kit/tsconfig.json", 3 + "compilerOptions": { 4 + "rewriteRelativeImportExtensions": true, 5 + "allowJs": true, 6 + "checkJs": true, 7 + "forceConsistentCasingInFileNames": true, 8 + "resolveJsonModule": true, 9 + "skipLibCheck": true, 10 + "sourceMap": true, 11 + "strict": true, 12 + "module": "NodeNext", 13 + "moduleResolution": "NodeNext" 14 + } 15 + }
+5
packages/svelte-standard-site/vite.config.ts
··· 1 + import tailwindcss from '@tailwindcss/vite'; 2 + import { sveltekit } from '@sveltejs/kit/vite'; 3 + import { defineConfig } from 'vite'; 4 + 5 + export default defineConfig({ plugins: [tailwindcss(), sveltekit()] });
+10
packages/svelte-standard-site/vitest.config.ts
··· 1 + import { defineConfig } from 'vitest/config'; 2 + import { svelte } from '@sveltejs/vite-plugin-svelte'; 3 + 4 + export default defineConfig({ 5 + plugins: [svelte()], 6 + test: { 7 + globals: true, 8 + environment: 'jsdom' 9 + } 10 + });