my website at ewancroft.uk
6
fork

Configure Feed

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

style: format codebase with Prettier

Apply consistent formatting across the entire codebase:
- Normalize indentation to tabs
- Format arrays and objects for readability
- Adjust whitespace and line breaks
- Standardize quote usage
- Format CSS color values consistently

No functional changes - formatting only.

+868 -714
+3 -13
.cspell.json
··· 157 157 "xrpc" 158 158 ], 159 159 "flagWords": [], 160 - "ignorePaths": [ 161 - "node_modules", 162 - "package-lock.json", 163 - "dist", 164 - "build" 165 - ], 166 - "ignoreRegExpList": [ 167 - "/(\\w+)'s/g" 168 - ], 160 + "ignorePaths": ["node_modules", "package-lock.json", "dist", "build"], 161 + "ignoreRegExpList": ["/(\\w+)'s/g"], 169 162 "overrides": [ 170 163 { 171 164 "filename": "**/*.svelte", 172 - "ignoreRegExpList": [ 173 - "/>.*</", 174 - "/(\\w+)'s/g" 175 - ] 165 + "ignoreRegExpList": ["/>.*</", "/(\\w+)'s/g"] 176 166 } 177 167 ] 178 168 }
+13 -13
README.md
··· 215 215 216 216 ```typescript 217 217 import { 218 - fetchProfile, 219 - fetchBlogPosts, 220 - fetchLatestBlueskyPost, 221 - fetchMusicStatus, 222 - fetchTangledRepos 218 + fetchProfile, 219 + fetchBlogPosts, 220 + fetchLatestBlueskyPost, 221 + fetchMusicStatus, 222 + fetchTangledRepos 223 223 } from '$lib/services/atproto'; 224 224 225 225 // Fetch profile data ··· 248 248 249 249 ```typescript 250 250 export const slugMappings: SlugMapping[] = [ 251 - { 252 - slug: 'blog', // Access via /blog 253 - publicationRkey: '3m3x4bgbsh22k' // Leaflet publication rkey 254 - }, 255 - { 256 - slug: 'notes', // Access via /notes 257 - publicationRkey: 'xyz123abc' 258 - } 251 + { 252 + slug: 'blog', // Access via /blog 253 + publicationRkey: '3m3x4bgbsh22k' // Leaflet publication rkey 254 + }, 255 + { 256 + slug: 'notes', // Access via /notes 257 + publicationRkey: 'xyz123abc' 258 + } 259 259 ]; 260 260 ``` 261 261
+27
docs/INTEGRATION_SUMMARY.md
··· 7 7 ## Changes Made 8 8 9 9 ### 1. Type Definitions (`/src/lib/services/atproto/types.ts`) 10 + 10 11 Added comprehensive TypeScript types for Standard.site: 12 + 11 13 - `StandardSiteThemeColor` - RGB/RGBA color values 12 14 - `StandardSiteBasicTheme` - Theme with background, foreground, accent colors 13 15 - `StandardSitePublication` - Publication metadata ··· 17 19 - Updated `BlogPost` platform type to include `'standard.site'` 18 20 19 21 ### 2. Fetch Service (`/src/lib/services/atproto/standard.ts`) 22 + 20 23 Created new service module with two main functions: 21 24 22 25 **`fetchStandardSitePublications()`** 26 + 23 27 - Fetches all `site.standard.publication` records 24 28 - Resolves publication icons from AT Protocol blobs 25 29 - Extracts theme colors and preferences 26 30 - Caches results with key `standard-site:publications:{DID}` 27 31 28 32 **`fetchStandardSiteDocuments()`** 33 + 29 34 - Fetches all `site.standard.document` records 30 35 - Maps documents to their publications 31 36 - Resolves cover images from blobs ··· 35 40 - Caches results with key `standard-site:documents:{DID}` 36 41 37 42 ### 3. Blog Feed Integration (`/src/lib/services/atproto/posts.ts`) 43 + 38 44 Updated `fetchBlogPosts()` to include Standard.site documents: 45 + 39 46 - Fetches documents from Standard.site alongside WhiteWind and Leaflet 40 47 - Adds documents to unified feed with platform `'standard.site'` 41 48 - Preserves existing sorting and top 5 limitation 42 49 43 50 ### 4. Exports (`/src/lib/services/atproto/index.ts`) 51 + 44 52 Exported new types and functions: 53 + 45 54 - All Standard.site types 46 55 - `fetchStandardSitePublications` 47 56 - `fetchStandardSiteDocuments` 48 57 49 58 ### 5. Slug Mappings (`/src/lib/data/slug-mappings.ts`) 59 + 50 60 Enhanced slug mapping configuration: 61 + 51 62 - Added `PublicationPlatform` type: `'leaflet' | 'standard.site'` 52 63 - Extended `SlugMapping` interface with optional `platform` field 53 64 - Updated all existing mappings to explicitly use `platform: 'leaflet'` ··· 55 66 - Defaults to `'leaflet'` for backwards compatibility 56 67 57 68 ### 6. Slug Configuration (`/src/lib/config/slugs.ts`) 69 + 58 70 Added platform-aware slug resolution: 71 + 59 72 - New `getPublicationFromSlug()` returns `{ rkey, platform }` 60 73 - Maintained `getPublicationRkeyFromSlug()` for backwards compatibility 61 74 - Imports `PublicationPlatform` type 62 75 63 76 ### 7. Slug Route Handler (`/src/routes/[slug=slug]/+server.ts`) 77 + 64 78 Updated to support both platforms: 79 + 65 80 - Uses `getPublicationFromSlug()` instead of `getPublicationRkeyFromSlug()` 66 81 - Branches logic based on platform 67 82 - For Standard.site: Uses publication URL directly ··· 69 84 - Updated error messages to reference correct config file 70 85 71 86 ### 8. Document Route Handler (`/src/routes/[slug=slug]/[rkey]/+server.ts`) 87 + 72 88 Enhanced document detection and routing: 89 + 73 90 - Updated `detectPostPlatform()` to accept and prioritize `platform` parameter 74 91 - Added Standard.site document detection via `site.standard.document` collection 75 92 - Verifies document belongs to requested publication ··· 78 95 - Updated error messages with platform-specific information 79 96 80 97 ### 9. Documentation (`/docs/standard-site-integration.md`) 98 + 81 99 Created comprehensive documentation covering: 100 + 82 101 - Overview of Standard.site lexicons 83 102 - Supported lexicon collections and their fields 84 103 - File structure and changes ··· 93 112 ## Lexicon Collections Supported 94 113 95 114 ### `site.standard.publication` 115 + 96 116 - Fetched from AT Protocol records 97 117 - Provides publication metadata, theme, and URL 98 118 - Used for slug-based routing 99 119 100 120 ### `site.standard.document` 121 + 101 122 - Fetched from AT Protocol records 102 123 - Contains full document metadata 103 124 - Supports both publication-based and standalone documents ··· 106 127 ## URL Patterns 107 128 108 129 ### Publication Redirects 130 + 109 131 - `/{slug}` → `{publication.url}` 110 132 111 133 ### Document Redirects 134 + 112 135 - `/{slug}/{rkey}` → `{publication.url}{document.path}` 113 136 114 137 ## Backwards Compatibility 115 138 116 139 All changes maintain full backwards compatibility: 140 + 117 141 - Existing Leaflet slugs work without modification 118 142 - `getPublicationRkeyFromSlug()` still works 119 143 - Default platform is `'leaflet'` if not specified ··· 141 165 ## Reference 142 166 143 167 The lexicons were integrated based on the schema definitions in: 168 + 144 169 - `/Volumes/Storage/Developer/clones/standard-site-lexicons/src/lexicons/` 145 170 146 171 Key lexicon files referenced: 172 + 147 173 - `site.standard.publication.ts` 148 174 - `site.standard.document.ts` 149 175 - `site.standard.theme.basic.ts` ··· 153 179 ## Architecture 154 180 155 181 The integration follows the same patterns as Leaflet: 182 + 156 183 - Fetch functions in dedicated service module 157 184 - Type definitions in shared types file 158 185 - Caching via existing cache infrastructure
+106 -85
docs/MIGRATION_GUIDE.md
··· 5 5 ## Should You Migrate? 6 6 7 7 **Consider Standard.site if you want:** 8 + 8 9 - More flexible content models (open union for content types) 9 10 - Direct theme integration (colors defined in publication) 10 11 - Better document organization (tags, paths, cover images) ··· 12 13 - Bluesky integration (post references for comments) 13 14 14 15 **Stay with Leaflet if you prefer:** 16 + 15 17 - The Leaflet ecosystem and community 16 18 - The /lish hosted format 17 19 - Simpler publication structure ··· 23 25 The easiest approach is to use both platforms together: 24 26 25 27 ### 1. Keep Existing Leaflet Setup 28 + 26 29 Your existing Leaflet publications continue to work: 27 30 28 31 ```typescript 29 32 // src/lib/data/slug-mappings.ts 30 33 export const slugMappings: SlugMapping[] = [ 31 - { 32 - slug: 'blog', 33 - publicationRkey: '3m3x4bgbsh22k', 34 - platform: 'leaflet' // Existing Leaflet blog 35 - }, 36 - // ... other Leaflet mappings 34 + { 35 + slug: 'blog', 36 + publicationRkey: '3m3x4bgbsh22k', 37 + platform: 'leaflet' // Existing Leaflet blog 38 + } 39 + // ... other Leaflet mappings 37 40 ]; 38 41 ``` 39 42 40 43 ### 2. Add Standard.site Publications 44 + 41 45 Add new Standard.site publications alongside: 42 46 43 47 ```typescript 44 48 export const slugMappings: SlugMapping[] = [ 45 - // Existing Leaflet 46 - { 47 - slug: 'blog', 48 - publicationRkey: '3m3x4bgbsh22k', 49 - platform: 'leaflet' 50 - }, 51 - // New Standard.site 52 - { 53 - slug: 'articles', 54 - publicationRkey: '3labc123xyz', 55 - platform: 'standard.site' 56 - } 49 + // Existing Leaflet 50 + { 51 + slug: 'blog', 52 + publicationRkey: '3m3x4bgbsh22k', 53 + platform: 'leaflet' 54 + }, 55 + // New Standard.site 56 + { 57 + slug: 'articles', 58 + publicationRkey: '3labc123xyz', 59 + platform: 'standard.site' 60 + } 57 61 ]; 58 62 ``` 59 63 60 64 ### 3. Content Appears in Unified Feed 65 + 61 66 Both platforms' content automatically appears in `fetchBlogPosts()`: 62 67 63 68 ```typescript ··· 75 80 76 81 ```json 77 82 { 78 - "$type": "site.standard.publication", 79 - "name": "My Blog", 80 - "url": "https://myblog.com", 81 - "description": "My personal blog about technology", 82 - "icon": { /* blob reference */ }, 83 - "basicTheme": { 84 - "background": { "r": 255, "g": 255, "b": 255 }, 85 - "foreground": { "r": 0, "g": 0, "b": 0 }, 86 - "accent": { "r": 59, "g": 130, "b": 246 }, 87 - "accentForeground": { "r": 255, "g": 255, "b": 255 } 88 - }, 89 - "preferences": { 90 - "showInDiscover": true 91 - } 83 + "$type": "site.standard.publication", 84 + "name": "My Blog", 85 + "url": "https://myblog.com", 86 + "description": "My personal blog about technology", 87 + "icon": { 88 + /* blob reference */ 89 + }, 90 + "basicTheme": { 91 + "background": { "r": 255, "g": 255, "b": 255 }, 92 + "foreground": { "r": 0, "g": 0, "b": 0 }, 93 + "accent": { "r": 59, "g": 130, "b": 246 }, 94 + "accentForeground": { "r": 255, "g": 255, "b": 255 } 95 + }, 96 + "preferences": { 97 + "showInDiscover": true 98 + } 92 99 } 93 100 ``` 94 101 ··· 97 104 For each Leaflet document, create a corresponding Standard.site document: 98 105 99 106 **Leaflet Document:** 107 + 100 108 ```json 101 109 { 102 - "$type": "pub.leaflet.document", 103 - "title": "My Post", 104 - "publication": "at://did:plc:abc/pub.leaflet.publication/xyz", 105 - "content": { /* markdown or HTML */ }, 106 - "createdAt": "2024-01-01T12:00:00Z" 110 + "$type": "pub.leaflet.document", 111 + "title": "My Post", 112 + "publication": "at://did:plc:abc/pub.leaflet.publication/xyz", 113 + "content": { 114 + /* markdown or HTML */ 115 + }, 116 + "createdAt": "2024-01-01T12:00:00Z" 107 117 } 108 118 ``` 109 119 110 120 **Standard.site Document:** 121 + 111 122 ```json 112 123 { 113 - "$type": "site.standard.document", 114 - "title": "My Post", 115 - "site": "at://did:plc:abc/site.standard.publication/xyz", 116 - "path": "/my-post", 117 - "description": "A brief description of my post", 118 - "content": { /* open union - flexible format */ }, 119 - "textContent": "Plain text version...", 120 - "tags": ["technology", "tutorial"], 121 - "publishedAt": "2024-01-01T12:00:00Z", 122 - "updatedAt": "2024-01-15T14:30:00Z" 124 + "$type": "site.standard.document", 125 + "title": "My Post", 126 + "site": "at://did:plc:abc/site.standard.publication/xyz", 127 + "path": "/my-post", 128 + "description": "A brief description of my post", 129 + "content": { 130 + /* open union - flexible format */ 131 + }, 132 + "textContent": "Plain text version...", 133 + "tags": ["technology", "tutorial"], 134 + "publishedAt": "2024-01-01T12:00:00Z", 135 + "updatedAt": "2024-01-15T14:30:00Z" 123 136 } 124 137 ``` 125 138 ··· 159 172 160 173 ## Field Mapping Reference 161 174 162 - | Leaflet Field | Standard.site Field | Notes | 163 - |---------------|---------------------|-------| 164 - | `title` | `title` | Direct mapping | 165 - | `publication` | `site` | Both use AT URI reference | 166 - | `content` | `content` | Standard.site uses open union | 167 - | - | `textContent` | New: plaintext version | 168 - | - | `description` | New: excerpt/summary | 169 - | - | `path` | New: document URL path | 170 - | - | `coverImage` | New: cover/thumbnail | 171 - | `createdAt` | `publishedAt` | Rename for clarity | 172 - | - | `updatedAt` | New: track edits | 173 - | - | `tags` | New: categorization | 174 - | - | `bskyPostRef` | New: link to Bluesky post | 175 + | Leaflet Field | Standard.site Field | Notes | 176 + | ------------- | ------------------- | ----------------------------- | 177 + | `title` | `title` | Direct mapping | 178 + | `publication` | `site` | Both use AT URI reference | 179 + | `content` | `content` | Standard.site uses open union | 180 + | - | `textContent` | New: plaintext version | 181 + | - | `description` | New: excerpt/summary | 182 + | - | `path` | New: document URL path | 183 + | - | `coverImage` | New: cover/thumbnail | 184 + | `createdAt` | `publishedAt` | Rename for clarity | 185 + | - | `updatedAt` | New: track edits | 186 + | - | `tags` | New: categorization | 187 + | - | `bskyPostRef` | New: link to Bluesky post | 175 188 176 189 ## Theme Migration 177 190 ··· 180 193 ```typescript 181 194 // Define your theme in the publication 182 195 const theme = { 183 - background: { r: 255, g: 255, b: 255 }, // White 184 - foreground: { r: 17, g: 24, b: 39 }, // Dark gray 185 - accent: { r: 59, g: 130, b: 246 }, // Blue 186 - accentForeground: { r: 255, g: 255, b: 255 } // White 196 + background: { r: 255, g: 255, b: 255 }, // White 197 + foreground: { r: 17, g: 24, b: 39 }, // Dark gray 198 + accent: { r: 59, g: 130, b: 246 }, // Blue 199 + accentForeground: { r: 255, g: 255, b: 255 } // White 187 200 }; 188 201 189 202 // Use in your site ··· 195 208 ### Leaflet URLs 196 209 197 210 **Publication:** 211 + 198 212 - With base_path: `https://myblog.com` 199 213 - Without: `https://leaflet.pub/lish/{DID}/{publicationRkey}` 200 214 201 215 **Document:** 216 + 202 217 - With base_path: `https://myblog.com/{rkey}` 203 218 - Without: `https://leaflet.pub/lish/{DID}/{publicationRkey}/{rkey}` 204 219 205 220 ### Standard.site URLs 206 221 207 222 **Publication:** 223 + 208 224 - `{publication.url}` (e.g., `https://myblog.com`) 209 225 210 226 **Document:** 227 + 211 228 - `{publication.url}{document.path}` (e.g., `https://myblog.com/my-post`) 212 229 213 230 ## Migration Tools ··· 219 236 import { fetchLeafletPublications, fetchStandardSitePublications } from '$lib/services/atproto'; 220 237 221 238 async function migratePublication(leafletRkey: string) { 222 - // 1. Fetch Leaflet publication 223 - const { publications: leafletPubs } = await fetchLeafletPublications(); 224 - const leafletPub = leafletPubs.find(p => p.rkey === leafletRkey); 225 - 226 - if (!leafletPub) { 227 - throw new Error('Leaflet publication not found'); 228 - } 229 - 230 - // 2. Create Standard.site publication 231 - const standardPub = { 232 - $type: 'site.standard.publication', 233 - name: leafletPub.name, 234 - url: leafletPub.basePath || 'https://example.com', 235 - description: leafletPub.description, 236 - // ... map other fields 237 - }; 238 - 239 - // 3. Create record via AT Protocol 240 - // (use @atproto/api to create the record) 241 - 242 - console.log('Migration complete!'); 239 + // 1. Fetch Leaflet publication 240 + const { publications: leafletPubs } = await fetchLeafletPublications(); 241 + const leafletPub = leafletPubs.find((p) => p.rkey === leafletRkey); 242 + 243 + if (!leafletPub) { 244 + throw new Error('Leaflet publication not found'); 245 + } 246 + 247 + // 2. Create Standard.site publication 248 + const standardPub = { 249 + $type: 'site.standard.publication', 250 + name: leafletPub.name, 251 + url: leafletPub.basePath || 'https://example.com', 252 + description: leafletPub.description 253 + // ... map other fields 254 + }; 255 + 256 + // 3. Create record via AT Protocol 257 + // (use @atproto/api to create the record) 258 + 259 + console.log('Migration complete!'); 243 260 } 244 261 ``` 245 262 ··· 291 308 ## Example Migration Timeline 292 309 293 310 **Week 1: Preparation** 311 + 294 312 - Read documentation 295 313 - Test Standard.site locally 296 314 - Create test publication 297 315 298 316 **Week 2: Pilot Migration** 317 + 299 318 - Migrate one small publication 300 319 - Test thoroughly 301 320 - Gather feedback 302 321 303 322 **Week 3-4: Full Migration** 323 + 304 324 - Migrate remaining publications 305 325 - Update all slug mappings 306 326 - Monitor for issues 307 327 308 328 **Week 5: Cleanup** 329 + 309 330 - Archive Leaflet records 310 331 - Update documentation 311 332 - Celebrate! 🎉
+1
docs/README.md
··· 42 42 **Read this if you want to use Standard.site for long-form content.** 43 43 44 44 **Quick References:** 45 + 45 46 - [Integration Summary](./INTEGRATION_SUMMARY.md) - What was changed and why 46 47 - [Quick Reference](./STANDARD_SITE_QUICK_REF.md) - Common patterns and snippets 47 48 - [Migration Guide](./MIGRATION_GUIDE.md) - Migrate from Leaflet to Standard.site
+52 -47
docs/STANDARD_SITE_QUICK_REF.md
··· 4 4 5 5 ```typescript 6 6 import { 7 - fetchStandardSitePublications, 8 - fetchStandardSiteDocuments, 9 - type StandardSitePublication, 10 - type StandardSiteDocument 7 + fetchStandardSitePublications, 8 + fetchStandardSiteDocuments, 9 + type StandardSitePublication, 10 + type StandardSiteDocument 11 11 } from '$lib/services/atproto'; 12 12 ``` 13 13 ··· 16 16 ```typescript 17 17 const { publications } = await fetchStandardSitePublications(); 18 18 19 - publications.forEach(pub => { 20 - console.log(pub.name); // Publication name 21 - console.log(pub.url); // Base URL 22 - console.log(pub.icon); // Icon blob URL 23 - console.log(pub.basicTheme); // Theme colors 19 + publications.forEach((pub) => { 20 + console.log(pub.name); // Publication name 21 + console.log(pub.url); // Base URL 22 + console.log(pub.icon); // Icon blob URL 23 + console.log(pub.basicTheme); // Theme colors 24 24 }); 25 25 ``` 26 26 ··· 29 29 ```typescript 30 30 const { documents } = await fetchStandardSiteDocuments(); 31 31 32 - documents.forEach(doc => { 33 - console.log(doc.title); // Document title 34 - console.log(doc.url); // Full canonical URL 35 - console.log(doc.publishedAt); // ISO timestamp 36 - console.log(doc.coverImage); // Cover image blob URL 37 - console.log(doc.tags); // Array of tags 38 - console.log(doc.publicationName); // Parent publication name 32 + documents.forEach((doc) => { 33 + console.log(doc.title); // Document title 34 + console.log(doc.url); // Full canonical URL 35 + console.log(doc.publishedAt); // ISO timestamp 36 + console.log(doc.coverImage); // Cover image blob URL 37 + console.log(doc.tags); // Array of tags 38 + console.log(doc.publicationName); // Parent publication name 39 39 }); 40 40 ``` 41 41 ··· 45 45 46 46 ```typescript 47 47 export const slugMappings: SlugMapping[] = [ 48 - { 49 - slug: 'my-blog', 50 - publicationRkey: '3labc123xyz', 51 - platform: 'standard.site' // ← Required for Standard.site 52 - } 48 + { 49 + slug: 'my-blog', 50 + publicationRkey: '3labc123xyz', 51 + platform: 'standard.site' // ← Required for Standard.site 52 + } 53 53 ]; 54 54 ``` 55 55 ··· 68 68 69 69 - **Publication**: `https://yoursite.com/my-blog` 70 70 - Redirects to publication URL 71 - 72 71 - **Document**: `https://yoursite.com/my-blog/3labc123xyz` 73 72 - Redirects to `{publication.url}{document.path}` 74 73 ··· 91 90 const { posts } = await fetchBlogPosts(); 92 91 93 92 // Filter by platform 94 - const standardPosts = posts.filter(p => p.platform === 'standard.site'); 95 - const leafletPosts = posts.filter(p => p.platform === 'leaflet'); 96 - const whiteWindPosts = posts.filter(p => p.platform === 'WhiteWind'); 93 + const standardPosts = posts.filter((p) => p.platform === 'standard.site'); 94 + const leafletPosts = posts.filter((p) => p.platform === 'leaflet'); 95 + const whiteWindPosts = posts.filter((p) => p.platform === 'WhiteWind'); 97 96 ``` 98 97 99 98 ## Document URL Patterns 100 99 101 100 ### Publication-Based Document 101 + 102 102 ```typescript 103 103 { 104 104 site: 'at://did:plc:abc/site.standard.publication/xyz', ··· 108 108 ``` 109 109 110 110 ### Loose Document 111 + 111 112 ```typescript 112 113 { 113 114 site: 'https://myblog.com', ··· 118 119 119 120 ## Collections 120 121 121 - | Collection | Description | 122 - |------------|-------------| 122 + | Collection | Description | 123 + | --------------------------- | ------------------------- | 123 124 | `site.standard.publication` | Publication/blog metadata | 124 - | `site.standard.document` | Individual posts/articles | 125 + | `site.standard.document` | Individual posts/articles | 125 126 126 127 ## Cache Keys 127 128 128 - | Data | Cache Key | 129 - |------|-----------| 129 + | Data | Cache Key | 130 + | ------------ | ---------------------------------- | 130 131 | Publications | `standard-site:publications:{DID}` | 131 - | Documents | `standard-site:documents:{DID}` | 132 + | Documents | `standard-site:documents:{DID}` | 132 133 133 134 ## Common Patterns 134 135 135 136 ### Display Publication Icon 137 + 136 138 ```svelte 137 139 {#if publication.icon} 138 - <img src={publication.icon} alt={publication.name} /> 140 + <img src={publication.icon} alt={publication.name} /> 139 141 {/if} 140 142 ``` 141 143 142 144 ### Display Document Cover 145 + 143 146 ```svelte 144 147 {#if document.coverImage} 145 - <img src={document.coverImage} alt={document.title} /> 148 + <img src={document.coverImage} alt={document.title} /> 146 149 {/if} 147 150 ``` 148 151 149 152 ### Display Tags 153 + 150 154 ```svelte 151 155 {#if document.tags} 152 - <div class="tags"> 153 - {#each document.tags as tag} 154 - <span class="tag">{tag}</span> 155 - {/each} 156 - </div> 156 + <div class="tags"> 157 + {#each document.tags as tag} 158 + <span class="tag">{tag}</span> 159 + {/each} 160 + </div> 157 161 {/if} 158 162 ``` 159 163 160 164 ### Format Date 165 + 161 166 ```svelte 162 167 <time datetime={document.publishedAt}> 163 - {new Date(document.publishedAt).toLocaleDateString()} 168 + {new Date(document.publishedAt).toLocaleDateString()} 164 169 </time> 165 170 ``` 166 171 ··· 171 176 import type { PublicationPlatform } from '$lib/data/slug-mappings'; 172 177 173 178 if (platform === 'standard.site') { 174 - // Handle Standard.site 179 + // Handle Standard.site 175 180 } else if (platform === 'leaflet') { 176 - // Handle Leaflet 181 + // Handle Leaflet 177 182 } 178 183 ``` 179 184 180 185 ## Troubleshooting 181 186 182 - | Issue | Solution | 183 - |-------|----------| 184 - | Documents not showing | Check `publishedAt` is set | 185 - | Redirects not working | Verify publication `url` starts with `http://` or `https://` | 186 - | Images not loading | Check blob CIDs and PDS resolution | 187 - | Slug not found | Add mapping to `slug-mappings.ts` with `platform: 'standard.site'` | 187 + | Issue | Solution | 188 + | --------------------- | ------------------------------------------------------------------ | 189 + | Documents not showing | Check `publishedAt` is set | 190 + | Redirects not working | Verify publication `url` starts with `http://` or `https://` | 191 + | Images not loading | Check blob CIDs and PDS resolution | 192 + | Slug not found | Add mapping to `slug-mappings.ts` with `platform: 'standard.site'` | 188 193 189 194 ## Resources 190 195
+49 -49
docs/configuration.md
··· 95 95 96 96 ### Environment Variable Reference 97 97 98 - | Variable | Required | Default | Purpose | 99 - |----------|----------|---------|---------| 100 - | `PUBLIC_ATPROTO_DID` | ✅ Yes | - | Your AT Protocol identifier | 101 - | `PUBLIC_SITE_TITLE` | ✅ Yes | - | Website title for SEO | 102 - | `PUBLIC_SITE_DESCRIPTION` | ✅ Yes | - | Website description for SEO | 103 - | `PUBLIC_SITE_KEYWORDS` | ✅ Yes | - | SEO keywords | 104 - | `PUBLIC_SITE_URL` | ✅ Yes | - | Your website's URL | 105 - | `PUBLIC_ENABLE_WHITEWIND` | ❌ No | `false` | Enable WhiteWind blog support | 106 - | `PUBLIC_BLOG_FALLBACK_URL` | ❌ No | `""` | Fallback URL for missing posts | 107 - | `PUBLIC_LOCAL_SLINGSHOT_URL` | ❌ No | `""` | Local Slingshot instance URL | 108 - | `PUBLIC_SLINGSHOT_URL` | ❌ No | Public URL | Public Slingshot instance | 109 - | `PUBLIC_CORS_ALLOWED_ORIGINS` | ❌ No | `"*"` | CORS allowed origins | 98 + | Variable | Required | Default | Purpose | 99 + | ----------------------------- | -------- | ---------- | ------------------------------ | 100 + | `PUBLIC_ATPROTO_DID` | ✅ Yes | - | Your AT Protocol identifier | 101 + | `PUBLIC_SITE_TITLE` | ✅ Yes | - | Website title for SEO | 102 + | `PUBLIC_SITE_DESCRIPTION` | ✅ Yes | - | Website description for SEO | 103 + | `PUBLIC_SITE_KEYWORDS` | ✅ Yes | - | SEO keywords | 104 + | `PUBLIC_SITE_URL` | ✅ Yes | - | Your website's URL | 105 + | `PUBLIC_ENABLE_WHITEWIND` | ❌ No | `false` | Enable WhiteWind blog support | 106 + | `PUBLIC_BLOG_FALLBACK_URL` | ❌ No | `""` | Fallback URL for missing posts | 107 + | `PUBLIC_LOCAL_SLINGSHOT_URL` | ❌ No | `""` | Local Slingshot instance URL | 108 + | `PUBLIC_SLINGSHOT_URL` | ❌ No | Public URL | Public Slingshot instance | 109 + | `PUBLIC_CORS_ALLOWED_ORIGINS` | ❌ No | `"*"` | CORS allowed origins | 110 110 111 111 --- 112 112 ··· 139 139 140 140 /** 141 141 * Maps friendly URL slugs to Leaflet publication rkeys 142 - * 142 + * 143 143 * Example usage: 144 144 * - { slug: 'blog', publicationRkey: '3m3x4bgbsh22k' } 145 145 * Accessible at: /blog ··· 147 147 * Accessible at: /essays 148 148 */ 149 149 export const slugMappings: SlugMapping[] = [ 150 - { 151 - slug: 'blog', 152 - publicationRkey: '3m3x4bgbsh22k' // Replace with your actual rkey 153 - } 154 - // Add more mappings as needed: 155 - // { 156 - // slug: 'essays', 157 - // publicationRkey: 'your-essays-rkey' 158 - // }, 159 - // { 160 - // slug: 'notes', 161 - // publicationRkey: 'your-notes-rkey' 162 - // } 150 + { 151 + slug: 'blog', 152 + publicationRkey: '3m3x4bgbsh22k' // Replace with your actual rkey 153 + } 154 + // Add more mappings as needed: 155 + // { 156 + // slug: 'essays', 157 + // publicationRkey: 'your-essays-rkey' 158 + // }, 159 + // { 160 + // slug: 'notes', 161 + // publicationRkey: 'your-notes-rkey' 162 + // } 163 163 ]; 164 164 ``` 165 165 ··· 182 182 183 183 ```typescript 184 184 export const slugMappings: SlugMapping[] = [ 185 - { 186 - slug: 'blog', // Main blog 187 - publicationRkey: '3m3x4bgbsh22k' 188 - }, 189 - { 190 - slug: 'tech', // Tech articles 191 - publicationRkey: 'xyz789tech' 192 - }, 193 - { 194 - slug: 'personal', // Personal writing 195 - publicationRkey: 'abc456personal' 196 - } 185 + { 186 + slug: 'blog', // Main blog 187 + publicationRkey: '3m3x4bgbsh22k' 188 + }, 189 + { 190 + slug: 'tech', // Tech articles 191 + publicationRkey: 'xyz789tech' 192 + }, 193 + { 194 + slug: 'personal', // Personal writing 195 + publicationRkey: 'abc456personal' 196 + } 197 197 ]; 198 198 ``` 199 199 ··· 205 205 206 206 ### Files to Update 207 207 208 - | File | Purpose | Action Required | 209 - |------|---------|-----------------| 210 - | `static/robots.txt` | SEO crawling rules | Update sitemap URL | 211 - | `static/sitemap.xml` | Site structure for SEO | Update with your pages | 212 - | `static/.well-known/*` | Domain verification | Replace or remove | 213 - | `static/favicon/*` | Site icons | Replace with your branding | 208 + | File | Purpose | Action Required | 209 + | ---------------------- | ---------------------- | -------------------------- | 210 + | `static/robots.txt` | SEO crawling rules | Update sitemap URL | 211 + | `static/sitemap.xml` | Site structure for SEO | Update with your pages | 212 + | `static/.well-known/*` | Domain verification | Replace or remove | 213 + | `static/favicon/*` | Site icons | Replace with your branding | 214 214 215 215 ### Step 1: Update robots.txt 216 216 ··· 237 237 <changefreq>daily</changefreq> 238 238 <priority>1.0</priority> 239 239 </url> 240 - 240 + 241 241 <!-- Add your publication slugs --> 242 242 <url> 243 243 <loc>https://yourdomain.com/blog</loc> 244 244 <changefreq>weekly</changefreq> 245 245 <priority>0.8</priority> 246 246 </url> 247 - 247 + 248 248 <!-- Add other important pages --> 249 249 <url> 250 250 <loc>https://yourdomain.com/site/meta</loc> ··· 433 433 434 434 ```css 435 435 @theme { 436 - --color-canvas: /* Background color */; 437 - --color-ink: /* Text color */; 438 - --color-primary: /* Accent color */; 436 + --color-canvas: /* Background color */; 437 + --color-ink: /* Text color */; 438 + --color-primary: /* Accent color */; 439 439 } 440 440 ``` 441 441
+103 -87
docs/standard-site-integration.md
··· 16 16 The integration supports the following Standard.site lexicon collections: 17 17 18 18 ### `site.standard.publication` 19 + 19 20 Publications represent websites, blogs, or content platforms. Each publication defines: 21 + 20 22 - `name`: Publication name 21 23 - `url`: Base URL for the publication 22 24 - `description`: Brief description ··· 25 27 - `preferences`: Platform preferences (e.g., `showInDiscover`) 26 28 27 29 ### `site.standard.document` 30 + 28 31 Documents represent individual articles, posts, or content. Each document includes: 32 + 29 33 - `title`: Document title 30 34 - `site`: Publication reference (AT URI) or standalone URL 31 35 - `path`: Document path (combined with site for canonical URL) ··· 41 45 ## File Structure 42 46 43 47 ### New Files 48 + 44 49 - `/src/lib/services/atproto/standard.ts` - Standard.site-specific fetch functions 45 50 - `/docs/standard-site-integration.md` - This documentation 46 51 47 52 ### Modified Files 53 + 48 54 - `/src/lib/services/atproto/types.ts` - Added Standard.site types 49 55 - `/src/lib/services/atproto/index.ts` - Exported Standard.site functions 50 56 - `/src/lib/services/atproto/posts.ts` - Integrated Standard.site into blog feed ··· 61 67 62 68 ```typescript 63 69 export const slugMappings: SlugMapping[] = [ 64 - { 65 - slug: 'blog', 66 - publicationRkey: 'abc123xyz', 67 - platform: 'standard.site' 68 - }, 69 - { 70 - slug: 'notes', 71 - publicationRkey: 'def456uvw', 72 - platform: 'leaflet' 73 - } 70 + { 71 + slug: 'blog', 72 + publicationRkey: 'abc123xyz', 73 + platform: 'standard.site' 74 + }, 75 + { 76 + slug: 'notes', 77 + publicationRkey: 'def456uvw', 78 + platform: 'leaflet' 79 + } 74 80 ]; 75 81 ``` 76 82 ··· 79 85 Use the provided fetch functions in your components or routes: 80 86 81 87 ```typescript 82 - import { 83 - fetchStandardSitePublications, 84 - fetchStandardSiteDocuments 85 - } from '$lib/services/atproto'; 88 + import { fetchStandardSitePublications, fetchStandardSiteDocuments } from '$lib/services/atproto'; 86 89 87 90 // Fetch all publications 88 91 const { publications } = await fetchStandardSitePublications(); ··· 101 104 - `/{slug}/{rkey}` - Redirects to the specific document URL 102 105 103 106 Example: 107 + 104 108 - `/blog` → Redirects to the Standard.site publication URL 105 109 - `/blog/3labc123xyz` → Redirects to the document at publication URL + document path 106 110 107 111 ## Type Definitions 108 112 109 113 ### StandardSitePublication 114 + 110 115 ```typescript 111 116 interface StandardSitePublication { 112 - name: string; 113 - rkey: string; 114 - uri: string; 115 - url: string; 116 - description?: string; 117 - icon?: string; 118 - basicTheme?: StandardSiteBasicTheme; 119 - preferences?: { 120 - showInDiscover?: boolean; 121 - }; 117 + name: string; 118 + rkey: string; 119 + uri: string; 120 + url: string; 121 + description?: string; 122 + icon?: string; 123 + basicTheme?: StandardSiteBasicTheme; 124 + preferences?: { 125 + showInDiscover?: boolean; 126 + }; 122 127 } 123 128 ``` 124 129 125 130 ### StandardSiteDocument 131 + 126 132 ```typescript 127 133 interface StandardSiteDocument { 128 - title: string; 129 - rkey: string; 130 - uri: string; 131 - url: string; 132 - site: string; 133 - path?: string; 134 - description?: string; 135 - coverImage?: string; 136 - content?: any; 137 - textContent?: string; 138 - bskyPostRef?: { 139 - uri: string; 140 - cid: string; 141 - }; 142 - tags?: string[]; 143 - publishedAt: string; 144 - updatedAt?: string; 145 - publicationName?: string; 146 - publicationRkey?: string; 134 + title: string; 135 + rkey: string; 136 + uri: string; 137 + url: string; 138 + site: string; 139 + path?: string; 140 + description?: string; 141 + coverImage?: string; 142 + content?: any; 143 + textContent?: string; 144 + bskyPostRef?: { 145 + uri: string; 146 + cid: string; 147 + }; 148 + tags?: string[]; 149 + publishedAt: string; 150 + updatedAt?: string; 151 + publicationName?: string; 152 + publicationRkey?: string; 147 153 } 148 154 ``` 149 155 150 156 ### StandardSiteBasicTheme 157 + 151 158 ```typescript 152 159 interface StandardSiteBasicTheme { 153 - background: StandardSiteThemeColor; 154 - foreground: StandardSiteThemeColor; 155 - accent: StandardSiteThemeColor; 156 - accentForeground: StandardSiteThemeColor; 160 + background: StandardSiteThemeColor; 161 + foreground: StandardSiteThemeColor; 162 + accent: StandardSiteThemeColor; 163 + accentForeground: StandardSiteThemeColor; 157 164 } 158 165 159 166 interface StandardSiteThemeColor { 160 - r: number; // 0-255 161 - g: number; // 0-255 162 - b: number; // 0-255 163 - a?: number; // 0-100 (only for rgba) 167 + r: number; // 0-255 168 + g: number; // 0-255 169 + b: number; // 0-255 170 + a?: number; // 0-100 (only for rgba) 164 171 } 165 172 ``` 166 173 ··· 172 179 const { posts } = await fetchBlogPosts(); 173 180 174 181 // Posts include platform property: 'WhiteWind' | 'leaflet' | 'standard.site' 175 - posts.forEach(post => { 176 - console.log(`${post.title} from ${post.platform}`); 182 + posts.forEach((post) => { 183 + console.log(`${post.title} from ${post.platform}`); 177 184 }); 178 185 ``` 179 186 ··· 182 189 Standard.site documents support two URL patterns: 183 190 184 191 ### 1. Publication-Based 192 + 185 193 When `site` points to a publication record (`at://...`): 194 + 186 195 ``` 187 196 {publication.url}{document.path} 188 197 ``` 189 198 190 199 Example: 200 + 191 201 - Publication URL: `https://myblog.com` 192 202 - Document path: `/my-first-post` 193 203 - Result: `https://myblog.com/my-first-post` 194 204 195 205 ### 2. Loose Documents 206 + 196 207 When `site` is a direct URL: 208 + 197 209 ``` 198 210 {site}{document.path} 199 211 ``` 200 212 201 213 Example: 214 + 202 215 - Site: `https://myblog.com` 203 216 - Document path: `/standalone-post` 204 217 - Result: `https://myblog.com/standalone-post` ··· 216 229 217 230 ```svelte 218 231 <script lang="ts"> 219 - import type { StandardSiteDocument } from '$lib/services/atproto'; 220 - 221 - export let document: StandardSiteDocument; 232 + import type { StandardSiteDocument } from '$lib/services/atproto'; 233 + 234 + export let document: StandardSiteDocument; 222 235 </script> 223 236 224 237 <article> 225 - <h1>{document.title}</h1> 226 - 227 - {#if document.description} 228 - <p class="description">{document.description}</p> 229 - {/if} 230 - 231 - {#if document.coverImage} 232 - <img src={document.coverImage} alt={document.title} /> 233 - {/if} 234 - 235 - <div class="meta"> 236 - <time datetime={document.publishedAt}> 237 - {new Date(document.publishedAt).toLocaleDateString()} 238 - </time> 239 - 240 - {#if document.tags && document.tags.length > 0} 241 - <div class="tags"> 242 - {#each document.tags as tag} 243 - <span class="tag">{tag}</span> 244 - {/each} 245 - </div> 246 - {/if} 247 - </div> 248 - 249 - {#if document.textContent} 250 - <div class="content"> 251 - {document.textContent} 252 - </div> 253 - {/if} 254 - 255 - <a href={document.url}>Read full article →</a> 238 + <h1>{document.title}</h1> 239 + 240 + {#if document.description} 241 + <p class="description">{document.description}</p> 242 + {/if} 243 + 244 + {#if document.coverImage} 245 + <img src={document.coverImage} alt={document.title} /> 246 + {/if} 247 + 248 + <div class="meta"> 249 + <time datetime={document.publishedAt}> 250 + {new Date(document.publishedAt).toLocaleDateString()} 251 + </time> 252 + 253 + {#if document.tags && document.tags.length > 0} 254 + <div class="tags"> 255 + {#each document.tags as tag} 256 + <span class="tag">{tag}</span> 257 + {/each} 258 + </div> 259 + {/if} 260 + </div> 261 + 262 + {#if document.textContent} 263 + <div class="content"> 264 + {document.textContent} 265 + </div> 266 + {/if} 267 + 268 + <a href={document.url}>Read full article →</a> 256 269 </article> 257 270 ``` 258 271 ··· 275 288 ## Troubleshooting 276 289 277 290 ### Documents not appearing in feed 291 + 278 292 - Verify the document has a valid `publishedAt` timestamp 279 293 - Check that the document's `site` field correctly references your publication 280 294 - Ensure the publication is correctly configured in slug mappings 281 295 282 296 ### Redirect not working 297 + 283 298 - Confirm the publication URL is properly formatted (should start with `http://` or `https://`) 284 299 - Check that the document's `path` field is set (defaults to `/{rkey}` if omitted) 285 300 - Verify the slug mapping has the correct `publicationRkey` and `platform: 'standard.site'` 286 301 287 302 ### Images not loading 303 + 288 304 - Ensure blob URLs are correctly resolved through the PDS 289 305 - Check that the `icon` and `coverImage` blobs are properly uploaded 290 306 - Verify blob CIDs are correctly referenced in the lexicon records
+27 -20
docs/theme-system.md
··· 41 41 Hue: 240° (blue) 42 42 ============================================================================ */ 43 43 [data-color-theme='midnight'] { 44 - /* Define your CSS custom properties here */ 45 - --color-primary-500: oklch(20% 0.05 240); 46 - /* ... other color definitions ... */ 44 + /* Define your CSS custom properties here */ 45 + --color-primary-500: oklch(20% 0.05 240); 46 + /* ... other color definitions ... */ 47 47 } 48 48 ``` 49 49 ··· 58 58 ## That's It! 59 59 60 60 The theme will automatically: 61 + 61 62 - ✅ Appear in the color theme dropdown 62 63 - ✅ Be type-safe in TypeScript 63 64 - ✅ Work with the theme switcher ··· 66 67 ## Configuration API 67 68 68 69 ### `THEMES` 70 + 69 71 Array of all available themes. Each theme has: 72 + 70 73 - `value`: Unique identifier (string) 71 74 - `label`: Display name (string) 72 75 - `description`: Short description (string) ··· 74 77 - `category`: Theme category (string) 75 78 76 79 ### `ColorTheme` 80 + 77 81 TypeScript type automatically generated from theme values. 78 82 79 83 ### `DEFAULT_THEME` 84 + 80 85 The default theme used when no preference is stored. 81 86 82 87 ### `getThemesByCategory()` 88 + 83 89 Returns themes organized by category for UI rendering. 84 90 85 91 ### `getTheme(value)` 92 + 86 93 Get a specific theme definition by its value. 87 94 88 95 ## Example: Adding Multiple Themes ··· 90 97 ```typescript 91 98 // In themes.config.ts 92 99 export const THEMES: readonly ThemeDefinition[] = [ 93 - // ... existing themes ... 94 - 95 - // New themes 96 - { 97 - value: 'midnight', 98 - label: 'Midnight', 99 - description: 'Deep night', 100 - color: 'oklch(20% 0.05 240)', 101 - category: 'cool' 102 - }, 103 - { 104 - value: 'sunrise', 105 - label: 'Sunrise', 106 - description: 'Morning glow', 107 - color: 'oklch(75% 0.15 50)', 108 - category: 'warm' 109 - } 100 + // ... existing themes ... 101 + 102 + // New themes 103 + { 104 + value: 'midnight', 105 + label: 'Midnight', 106 + description: 'Deep night', 107 + color: 'oklch(20% 0.05 240)', 108 + category: 'cool' 109 + }, 110 + { 111 + value: 'sunrise', 112 + label: 'Sunrise', 113 + description: 'Morning glow', 114 + color: 'oklch(75% 0.15 50)', 115 + category: 'warm' 116 + } 110 117 ] as const; 111 118 ``` 112 119
+8 -8
src/app.css
··· 36 36 37 37 /* Slate - Primary colors (230°) */ 38 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.030 230), oklch(94.8% 0.022 230)); 39 + --color-primary-100: light-dark(oklch(26.5% 0.03 230), oklch(94.8% 0.022 230)); 40 40 --color-primary-200: light-dark(oklch(40.5% 0.048 230), oklch(89.5% 0.042 230)); 41 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.080 230), oklch(69.5% 0.078 230)); 42 + --color-primary-400: light-dark(oklch(66.5% 0.08 230), oklch(69.5% 0.078 230)); 43 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.080 230)); 44 + --color-primary-600: light-dark(oklch(82.2% 0.078 230), oklch(49.5% 0.08 230)); 45 45 --color-primary-700: light-dark(oklch(86.5% 0.062 230), oklch(39.5% 0.065 230)); 46 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.030 230)); 47 + --color-primary-900: light-dark(oklch(95.8% 0.022 230), oklch(21.5% 0.03 230)); 48 48 --color-primary-950: light-dark(oklch(98% 0.012 230), oklch(15.2% 0.018 230)); 49 49 50 50 /* Steel Grey - Secondary colors (215°) */ 51 - --color-secondary-50: light-dark(oklch(18.5% 0.020 215), oklch(97.9% 0.013 215)); 51 + --color-secondary-50: light-dark(oklch(18.5% 0.02 215), oklch(97.9% 0.013 215)); 52 52 --color-secondary-100: light-dark(oklch(26.8% 0.033 215), oklch(95% 0.024 215)); 53 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.070 215), oklch(80.2% 0.065 215)); 54 + --color-secondary-300: light-dark(oklch(54.5% 0.07 215), oklch(80.2% 0.065 215)); 55 55 --color-secondary-400: light-dark(oklch(67% 0.087 215), oklch(70.2% 0.082 215)); 56 56 --color-secondary-500: light-dark(oklch(79% 0.103 215), oklch(60.2% 0.103 215)); 57 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.070 215)); 58 + --color-secondary-700: light-dark(oklch(87% 0.065 215), oklch(40.2% 0.07 215)); 59 59 --color-secondary-800: light-dark(oklch(91.5% 0.045 215), oklch(30.5% 0.052 215)); 60 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.020 215)); 61 + --color-secondary-950: light-dark(oklch(98.2% 0.013 215), oklch(15.8% 0.02 215)); 62 62 63 63 /* Charcoal - Accent colors (240°) */ 64 64 --color-accent-50: light-dark(oklch(18.5% 0.022 240), oklch(98% 0.014 240));
+4 -2
src/hooks.server.ts
··· 12 12 // Handle OPTIONS preflight requests for CORS 13 13 if (event.request.method === 'OPTIONS' && event.url.pathname.startsWith('/api/')) { 14 14 const origin = event.request.headers.get('origin'); 15 - const allowedOrigins = PUBLIC_CORS_ALLOWED_ORIGINS?.split(',').map((o: string) => o.trim()) || []; 15 + const allowedOrigins = 16 + PUBLIC_CORS_ALLOWED_ORIGINS?.split(',').map((o: string) => o.trim()) || []; 16 17 17 18 const headers: Record<string, string> = { 18 19 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', ··· 60 61 // Add CORS headers for API routes 61 62 if (event.url.pathname.startsWith('/api/')) { 62 63 const origin = event.request.headers.get('origin'); 63 - const allowedOrigins = PUBLIC_CORS_ALLOWED_ORIGINS?.split(',').map((o: string) => o.trim()) || []; 64 + const allowedOrigins = 65 + PUBLIC_CORS_ALLOWED_ORIGINS?.split(',').map((o: string) => o.trim()) || []; 64 66 65 67 // If * is specified, allow any origin 66 68 if (allowedOrigins.includes('*')) {
+106 -29
src/lib/components/HappyMacEasterEgg.svelte
··· 83 83 </script> 84 84 85 85 {#if isVisible} 86 - <div 87 - class="happy-mac" 88 - style="left: {position}px" 89 - > 86 + <div class="happy-mac" style="left: {position}px"> 90 87 <!-- 91 88 Happy Mac SVG 92 89 Original by NiloGlock at Italian Wikipedia ··· 102 99 > 103 100 <g transform="translate(-5.3090212,-4.3002038)"> 104 101 <g transform="matrix(0.06455006,0,0,0.06455006,7.6050574,7.0900779)"> 105 - <path d="m -30.937651,99.78759 h 122 v 26.80449 h -122 z" style="fill:#000000;fill-opacity:1;stroke-width:2.38412714"/> 102 + <path 103 + d="m -30.937651,99.78759 h 122 v 26.80449 h -122 z" 104 + style="fill:#000000;fill-opacity:1;stroke-width:2.38412714" 105 + /> 106 106 <g transform="translate(-56.456402,-31.41017)"> 107 - <path style="fill:#555555;fill-opacity:1;stroke:none;stroke-width:0.17674622" d="m 33.668747,136.75006 v 4.69998 h 31.950504 v -4.69998 z m 41.740088,4.69998 V 146.15 h 11.145573 v -4.69996 z M 91.152059,146.15 v 6.29987 H 102.47075 V 146.15 Z"/> 108 - <path style="fill:#444444;fill-opacity:1;stroke:none;stroke-width:0.15800072" d="m 65.619251,136.75006 v 4.69998 H 86.554408 V 146.15 h 15.916342 v 6.29987 h 20.86023 V 146.15 h -15.87449 v -4.69996 H 91.152059 v -4.69998 z"/> 109 - <path style="fill:#222222;fill-opacity:1;stroke:none;stroke-width:0.21712606" d="m 91.152059,136.75006 v 4.69998 H 107.45649 V 146.15 h 15.87449 v 6.29987 h 16.03777 v -6.29987 -4.69996 -4.69998 z"/> 110 - <path style="fill:#777777;fill-opacity:1;stroke:none;stroke-width:0.20201708" d="M 33.668747,141.45004 V 146.15 h 41.740088 v -4.69996 z M 75.408835,146.15 v 6.29987 H 91.152059 V 146.15 Z"/> 111 - <path d="m 33.668823,146.14999 h 41.74001 v 6.3 h -41.74001 z" style="fill:#888888;fill-opacity:1;stroke:none;stroke-width:0.23388879"/> 107 + <path 108 + style="fill:#555555;fill-opacity:1;stroke:none;stroke-width:0.17674622" 109 + d="m 33.668747,136.75006 v 4.69998 h 31.950504 v -4.69998 z m 41.740088,4.69998 V 146.15 h 11.145573 v -4.69996 z M 91.152059,146.15 v 6.29987 H 102.47075 V 146.15 Z" 110 + /> 111 + <path 112 + style="fill:#444444;fill-opacity:1;stroke:none;stroke-width:0.15800072" 113 + d="m 65.619251,136.75006 v 4.69998 H 86.554408 V 146.15 h 15.916342 v 6.29987 h 20.86023 V 146.15 h -15.87449 v -4.69996 H 91.152059 v -4.69998 z" 114 + /> 115 + <path 116 + style="fill:#222222;fill-opacity:1;stroke:none;stroke-width:0.21712606" 117 + d="m 91.152059,136.75006 v 4.69998 H 107.45649 V 146.15 h 15.87449 v 6.29987 h 16.03777 v -6.29987 -4.69996 -4.69998 z" 118 + /> 119 + <path 120 + style="fill:#777777;fill-opacity:1;stroke:none;stroke-width:0.20201708" 121 + d="M 33.668747,141.45004 V 146.15 h 41.740088 v -4.69996 z M 75.408835,146.15 v 6.29987 H 91.152059 V 146.15 Z" 122 + /> 123 + <path 124 + d="m 33.668823,146.14999 h 41.74001 v 6.3 h -41.74001 z" 125 + style="fill:#888888;fill-opacity:1;stroke:none;stroke-width:0.23388879" 126 + /> 112 127 </g> 113 - <path d="M -30.969854,-37.120319 H 91.062349 V 99.787579 H -30.969854 Z" style="fill:#cccccc;fill-opacity:1;stroke-width:0.26458332"/> 114 - <path d="M -15.075892,-21.040775 H 74.98512 v 67.75 h -90.061012 z" style="fill:#ccccff;fill-opacity:1;stroke-width:0.26458332"/> 115 - <path transform="scale(0.26458333)" d="M 102.17383,-23.402344 V 59.882812 H 83.148438 V 78.779297 H 102.17383 120 120.0508 V -23.402344 Z" style="fill:#000000;fill-opacity:1;stroke-width:0.93718952"/> 116 - <path d="M -30.969856,-43.220318 H 91.062347 v 6.1 H -30.969856 Z" style="fill:#000000;fill-opacity:1;stroke-width:1.13749063"/> 117 - <path d="M -15.075892,-27.140776 H 74.98512 v 6.1 h -90.061012 z" style="fill:#444444;fill-opacity:1;stroke-width:0.97719014"/> 118 - <path d="m -21.040775,15.075892 h 67.75 v 6.1 h -67.75 z" style="fill:#444444;fill-opacity:1;stroke-width:0.84755003" transform="rotate(90)"/> 119 - <path d="m -21.040775,-81.085121 h 67.75 v 6.1 h -67.75 z" style="fill:#ffffff;fill-opacity:1;stroke-width:0.84755009" transform="rotate(90)"/> 120 - <path d="m -15.07589,46.709225 h 90.061013 v 6.1 H -15.07589 Z" style="fill:#ffffff;fill-opacity:1;stroke-width:0.9771902"/> 121 - <path d="m 31.655506,73.81324 h 43.400002 v 5 H 31.655506 Z" style="fill:#000000;fill-opacity:1;stroke-width:0.26445001"/> 122 - <path d="m 31.655506,78.81324 h 43.400005 v 6 H 31.655506 Z" style="fill:#ffffff;fill-opacity:1;stroke-width:0.28969046"/> 123 - <path d="m -21.133041,73.785721 h 11.060395 v 5 h -11.060395 z" style="fill:#00bb00;fill-opacity:1;stroke-width:0.13350084"/> 124 - <path d="m -21.133041,78.785721 h 11.060396 v 6 h -11.060396 z" style="fill:#dd0000;fill-opacity:1;stroke-width:0.14624284"/> 125 - <path d="M 5.8799295,-6.1919641 H 10.87993 V 5.0080357 H 5.8799295 Z" style="fill:#000000;fill-opacity:1;stroke-width:0.26576424"/> 126 - <path d="m 47.880306,-6.1919641 h 6.1 V 5.0080357 h -6.1 z" style="fill:#000000;fill-opacity:1;stroke-width:0.29354623"/> 127 - <path d="m 10.8871,25.947487 h 5 v 6 h -5 z" style="fill:#000000;fill-opacity:1;stroke-width:0.19451953"/> 128 - <path d="m 38.149635,25.944651 h 4.75 v 6.002836 h -4.75 z" style="fill:#000000;fill-opacity:1;stroke-width:0.18963902"/> 129 - <path d="m 15.8871,31.947487 h 22.262533 v 5.011021 H 15.8871 Z" style="fill:#000000;fill-opacity:1;stroke-width:11.12128639"/> 130 - <path d="M -37.120319,30.969854 H 99.787579 v 4.6 H -37.120319 Z" style="fill:#000000;fill-opacity:1;stroke-width:1.04625833" transform="rotate(90)"/> 131 - <path d="M -37.120331,-95.662346 H 99.787582 v 4.6 H -37.120331 Z" style="fill:#000000;fill-opacity:1;stroke-width:1.04625833" transform="rotate(90)"/> 128 + <path 129 + d="M -30.969854,-37.120319 H 91.062349 V 99.787579 H -30.969854 Z" 130 + style="fill:#cccccc;fill-opacity:1;stroke-width:0.26458332" 131 + /> 132 + <path 133 + d="M -15.075892,-21.040775 H 74.98512 v 67.75 h -90.061012 z" 134 + style="fill:#ccccff;fill-opacity:1;stroke-width:0.26458332" 135 + /> 136 + <path 137 + transform="scale(0.26458333)" 138 + d="M 102.17383,-23.402344 V 59.882812 H 83.148438 V 78.779297 H 102.17383 120 120.0508 V -23.402344 Z" 139 + style="fill:#000000;fill-opacity:1;stroke-width:0.93718952" 140 + /> 141 + <path 142 + d="M -30.969856,-43.220318 H 91.062347 v 6.1 H -30.969856 Z" 143 + style="fill:#000000;fill-opacity:1;stroke-width:1.13749063" 144 + /> 145 + <path 146 + d="M -15.075892,-27.140776 H 74.98512 v 6.1 h -90.061012 z" 147 + style="fill:#444444;fill-opacity:1;stroke-width:0.97719014" 148 + /> 149 + <path 150 + d="m -21.040775,15.075892 h 67.75 v 6.1 h -67.75 z" 151 + style="fill:#444444;fill-opacity:1;stroke-width:0.84755003" 152 + transform="rotate(90)" 153 + /> 154 + <path 155 + d="m -21.040775,-81.085121 h 67.75 v 6.1 h -67.75 z" 156 + style="fill:#ffffff;fill-opacity:1;stroke-width:0.84755009" 157 + transform="rotate(90)" 158 + /> 159 + <path 160 + d="m -15.07589,46.709225 h 90.061013 v 6.1 H -15.07589 Z" 161 + style="fill:#ffffff;fill-opacity:1;stroke-width:0.9771902" 162 + /> 163 + <path 164 + d="m 31.655506,73.81324 h 43.400002 v 5 H 31.655506 Z" 165 + style="fill:#000000;fill-opacity:1;stroke-width:0.26445001" 166 + /> 167 + <path 168 + d="m 31.655506,78.81324 h 43.400005 v 6 H 31.655506 Z" 169 + style="fill:#ffffff;fill-opacity:1;stroke-width:0.28969046" 170 + /> 171 + <path 172 + d="m -21.133041,73.785721 h 11.060395 v 5 h -11.060395 z" 173 + style="fill:#00bb00;fill-opacity:1;stroke-width:0.13350084" 174 + /> 175 + <path 176 + d="m -21.133041,78.785721 h 11.060396 v 6 h -11.060396 z" 177 + style="fill:#dd0000;fill-opacity:1;stroke-width:0.14624284" 178 + /> 179 + <path 180 + d="M 5.8799295,-6.1919641 H 10.87993 V 5.0080357 H 5.8799295 Z" 181 + style="fill:#000000;fill-opacity:1;stroke-width:0.26576424" 182 + /> 183 + <path 184 + d="m 47.880306,-6.1919641 h 6.1 V 5.0080357 h -6.1 z" 185 + style="fill:#000000;fill-opacity:1;stroke-width:0.29354623" 186 + /> 187 + <path 188 + d="m 10.8871,25.947487 h 5 v 6 h -5 z" 189 + style="fill:#000000;fill-opacity:1;stroke-width:0.19451953" 190 + /> 191 + <path 192 + d="m 38.149635,25.944651 h 4.75 v 6.002836 h -4.75 z" 193 + style="fill:#000000;fill-opacity:1;stroke-width:0.18963902" 194 + /> 195 + <path 196 + d="m 15.8871,31.947487 h 22.262533 v 5.011021 H 15.8871 Z" 197 + style="fill:#000000;fill-opacity:1;stroke-width:11.12128639" 198 + /> 199 + <path 200 + d="M -37.120319,30.969854 H 99.787579 v 4.6 H -37.120319 Z" 201 + style="fill:#000000;fill-opacity:1;stroke-width:1.04625833" 202 + transform="rotate(90)" 203 + /> 204 + <path 205 + d="M -37.120331,-95.662346 H 99.787582 v 4.6 H -37.120331 Z" 206 + style="fill:#000000;fill-opacity:1;stroke-width:1.04625833" 207 + transform="rotate(90)" 208 + /> 132 209 </g> 133 210 </g> 134 211 </svg>
+8 -6
src/lib/components/layout/ColorThemeToggle.svelte
··· 87 87 <!-- Desktop ONLY: Dropdown menu --> 88 88 <div 89 89 id="color-theme-menu" 90 - class="absolute right-0 top-full z-50 mt-2 hidden w-72 rounded-lg border border-canvas-200 bg-canvas-50 shadow-xl md:block dark:border-canvas-800 dark:bg-canvas-950" 90 + class="absolute top-full right-0 z-50 mt-2 hidden w-72 rounded-lg border border-canvas-200 bg-canvas-50 shadow-xl md:block dark:border-canvas-800 dark:bg-canvas-950" 91 91 role="menu" 92 92 aria-label="Colour theme menu" 93 93 > 94 94 <div class="max-h-128 overflow-y-auto p-2"> 95 - <div class="mb-2 px-3 py-2 text-xs font-semibold uppercase text-ink-600 dark:text-ink-400"> 95 + <div class="mb-2 px-3 py-2 text-xs font-semibold text-ink-600 uppercase dark:text-ink-400"> 96 96 Colour Themes 97 97 </div> 98 98 ··· 107 107 onclick={() => selectTheme(theme.value as ColorTheme)} 108 108 class="flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 109 109 {currentTheme === theme.value 110 - ? 'bg-primary-50 text-primary-700 dark:bg-primary-950 dark:text-primary-300' 111 - : 'text-ink-700 hover:bg-canvas-100 focus-visible:bg-canvas-100 dark:text-ink-200 dark:hover:bg-canvas-900 dark:focus-visible:bg-canvas-900'}" 110 + ? 'bg-primary-50 text-primary-700 dark:bg-primary-950 dark:text-primary-300' 111 + : 'text-ink-700 hover:bg-canvas-100 focus-visible:bg-canvas-100 dark:text-ink-200 dark:hover:bg-canvas-900 dark:focus-visible:bg-canvas-900'}" 112 112 role="menuitem" 113 113 aria-current={currentTheme === theme.value ? 'true' : undefined} 114 114 > ··· 117 117 style="background-color: {theme.color}" 118 118 aria-hidden="true" 119 119 ></div> 120 - <div class="flex-1 min-w-0"> 120 + <div class="min-w-0 flex-1"> 121 121 <div 122 - class="font-medium {currentTheme === theme.value ? '' : 'text-ink-900 dark:text-ink-50'}" 122 + class="font-medium {currentTheme === theme.value 123 + ? '' 124 + : 'text-ink-900 dark:text-ink-50'}" 123 125 > 124 126 {theme.label} 125 127 </div>
+9 -16
src/lib/components/layout/DecimalClockInfoBox.svelte
··· 54 54 55 55 {#if show && mounted} 56 56 <div 57 - class="fixed left-0 top-0 z-9999 flex h-screen w-screen items-center justify-center bg-black/70 p-4" 57 + class="fixed top-0 left-0 z-9999 flex h-screen w-screen items-center justify-center bg-black/70 p-4" 58 58 style="position: fixed; margin: 0;" 59 59 onclick={onClose} 60 60 onkeydown={(e) => e.key === 'Escape' && onClose()} ··· 85 85 86 86 <!-- Content --> 87 87 <div class="space-y-4"> 88 - <h2 89 - id="decimal-time-title" 90 - class="text-2xl font-bold text-ink-900 dark:text-ink-50" 91 - > 88 + <h2 id="decimal-time-title" class="text-2xl font-bold text-ink-900 dark:text-ink-50"> 92 89 French Revolutionary Decimal Time 93 90 </h2> 94 91 95 92 <div class="space-y-3 text-ink-700 dark:text-ink-200"> 96 93 <p> 97 - Decimal time was introduced during the French Revolution as part of the 98 - metric system. Instead of dividing the day into 24 hours, it uses a base-10 99 - system: 94 + Decimal time was introduced during the French Revolution as part of the metric 95 + system. Instead of dividing the day into 24 hours, it uses a base-10 system: 100 96 </p> 101 97 102 98 <ul class="list-disc space-y-2 pl-6"> ··· 106 102 </ul> 107 103 108 104 <p> 109 - This means a decimal day has 10 hours, 1,000 minutes, and 100,000 seconds 110 - total. 105 + This means a decimal day has 10 hours, 1,000 minutes, and 100,000 seconds total. 111 106 </p> 112 107 113 108 <Card variant="flat" padding="md" class="bg-canvas-200 dark:bg-canvas-800"> 114 109 {#snippet children()} 115 - <h3 class="mb-2 font-semibold text-ink-900 dark:text-ink-50"> 116 - Conversions: 117 - </h3> 110 + <h3 class="mb-2 font-semibold text-ink-900 dark:text-ink-50">Conversions:</h3> 118 111 <ul class="space-y-1 text-sm"> 119 112 <li>1 decimal hour ≈ 2.4 traditional hours (2h 24m)</li> 120 113 <li>1 decimal minute ≈ 1.44 traditional minutes (86.4 seconds)</li> ··· 140 133 </Card> 141 134 142 135 <p class="text-sm text-ink-600 dark:text-ink-400"> 143 - While decimal time was officially adopted in France from 1793-1795, it never 144 - gained widespread acceptance and was eventually abandoned in favor of the 145 - traditional 24-hour system. 136 + While decimal time was officially adopted in France from 1793-1795, it never gained 137 + widespread acceptance and was eventually abandoned in favor of the traditional 138 + 24-hour system. 146 139 </p> 147 140 148 141 <p class="text-sm text-ink-600 dark:text-ink-400">
+7 -3
src/lib/components/layout/Footer.svelte
··· 96 96 <button 97 97 type="button" 98 98 onclick={() => happyMacStore.incrementClick()} 99 - class="cursor-default select-none transition-colors hover:text-ink-600 dark:hover:text-ink-300" 100 - aria-label="Version 10.6.0{showHint ? ` - ${$happyMacStore.clickCount} of 24 clicks` : ''}" 99 + class="cursor-default transition-colors select-none hover:text-ink-600 dark:hover:text-ink-300" 100 + aria-label="Version 10.6.0{showHint 101 + ? ` - ${$happyMacStore.clickCount} of 24 clicks` 102 + : ''}" 101 103 title={showHint ? `${$happyMacStore.clickCount}/24` : ''} 102 104 > 103 - v10.6.0{#if showHint}<span class="ml-1 text-xs opacity-60">({$happyMacStore.clickCount}/24)</span>{/if} 105 + v10.6.0{#if showHint}<span class="ml-1 text-xs opacity-60" 106 + >({$happyMacStore.clickCount}/24)</span 107 + >{/if} 104 108 </button> 105 109 </div> 106 110 </div>
+18 -27
src/lib/components/layout/Header.svelte
··· 7 7 import WolfToggle from './WolfToggle.svelte'; 8 8 import ColorThemeToggle from './ColorThemeToggle.svelte'; 9 9 import { navItems } from '$lib/data/navItems'; 10 - import { fetchProfile, type ProfileData } from '$lib/services/atproto'; 10 + import type { ProfileData } from '$lib/services/atproto'; 11 11 import { defaultSiteMeta, createSiteMeta, type SiteMetadata } from '$lib/helper/siteMeta'; 12 12 import { colorThemeDropdownOpen } from '$lib/stores/dropdownState'; 13 13 import { colorTheme, type ColorTheme } from '$lib/stores/colorTheme'; 14 - import { 15 - getThemesByCategory, 16 - CATEGORY_LABELS 17 - } from '$lib/config/themes.config'; 14 + import { getThemesByCategory, CATEGORY_LABELS } from '$lib/config/themes.config'; 15 + 16 + interface Props { 17 + profile?: ProfileData | null; 18 + } 19 + 20 + let { profile = null }: Props = $props(); 18 21 19 22 const siteMeta: SiteMetadata = createSiteMeta(defaultSiteMeta); 20 23 const { page } = getStores(); 21 24 22 - let profile = $state<ProfileData | null>(null); 23 - let loading = $state(true); 24 - let error = $state<string | null>(null); 25 25 let imageLoaded = $state(false); 26 26 let mobileMenuOpen = $state(false); 27 27 let colorThemeOpen = $state(false); ··· 80 80 } 81 81 }); 82 82 83 - // Fetch profile 84 - fetchProfile() 85 - .then((data) => { 86 - profile = data; 87 - }) 88 - .catch((err) => { 89 - error = err instanceof Error ? err.message : 'Failed to load profile'; 90 - }) 91 - .finally(() => { 92 - loading = false; 93 - }); 94 - 95 83 // Close mobile menus on Escape key 96 84 const handleEscape = (e: KeyboardEvent) => { 97 85 if (e.key === 'Escape') { ··· 104 92 } 105 93 }; 106 94 document.addEventListener('keydown', handleEscape); 107 - 95 + 108 96 return () => { 109 97 unsubTheme(); 110 98 unsubDropdown(); ··· 150 138 aria-label="Loading profile" 151 139 ></div> 152 140 {/if} 153 - 154 141 </div> 155 142 <!-- Site title revealed on hover --> 156 143 <span ··· 190 177 </li> 191 178 {/each} 192 179 </ul> 193 - 180 + 194 181 <!-- Desktop Toggles --> 195 182 <div class="flex items-center gap-2"> 196 183 <ColorThemeToggle /> ··· 270 257 <div class="container mx-auto flex flex-col px-3 py-2"> 271 258 {#each Object.entries(themesByCategory) as [category, categoryThemes]} 272 259 <div class="mb-4 last:mb-0"> 273 - <div class="mb-2 px-3 text-xs font-semibold uppercase tracking-wide text-ink-600 dark:text-ink-400"> 260 + <div 261 + class="mb-2 px-3 text-xs font-semibold tracking-wide text-ink-600 uppercase dark:text-ink-400" 262 + > 274 263 {CATEGORY_LABELS[category as Category]} 275 264 </div> 276 265 <div class="space-y-1"> ··· 279 268 onclick={() => selectTheme(theme.value as ColorTheme)} 280 269 class="flex w-full items-center gap-3 rounded-lg px-3 py-3 text-left transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 281 270 {currentTheme === theme.value 282 - ? 'bg-primary-50 text-primary-700 dark:bg-primary-950 dark:text-primary-300' 283 - : 'text-ink-700 hover:bg-canvas-100 focus-visible:bg-canvas-100 dark:text-ink-200 dark:hover:bg-canvas-900 dark:focus-visible:bg-canvas-900'}" 271 + ? 'bg-primary-50 text-primary-700 dark:bg-primary-950 dark:text-primary-300' 272 + : 'text-ink-700 hover:bg-canvas-100 focus-visible:bg-canvas-100 dark:text-ink-200 dark:hover:bg-canvas-900 dark:focus-visible:bg-canvas-900'}" 284 273 role="menuitem" 285 274 aria-current={currentTheme === theme.value ? 'true' : undefined} 286 275 > ··· 291 280 ></div> 292 281 <div class="min-w-0 flex-1"> 293 282 <div 294 - class="font-medium {currentTheme === theme.value ? '' : 'text-ink-900 dark:text-ink-50'}" 283 + class="font-medium {currentTheme === theme.value 284 + ? '' 285 + : 'text-ink-900 dark:text-ink-50'}" 295 286 > 296 287 {theme.label} 297 288 </div>
+18 -4
src/lib/components/layout/main/card/BlueskyPostCard.svelte
··· 6 6 import { Heart, Repeat2, MessageCircle, ExternalLink, X } from '@lucide/svelte'; 7 7 import Hls from 'hls.js'; 8 8 9 + interface Props { 10 + post?: BlueskyPost | null; 11 + } 12 + 13 + let { post: initialPost = null }: Props = $props(); 14 + 9 15 let post = $state<BlueskyPost | null>(null); 10 - let loading = $state(true); 16 + let loading = $state(false); 11 17 let error = $state<string | null>(null); 12 18 let lightboxImage = $state<{ url: string; alt: string } | null>(null); 13 19 let videoElements = new Map<string, { element: HTMLVideoElement; hls: Hls | null }>(); ··· 34 40 } 35 41 } 36 42 37 - // Initial load and polling setup using $effect 43 + // Initialize post and set up polling 38 44 $effect(() => { 39 - // Initial load 40 - loadPost(); 45 + // Set initial post if provided 46 + if (initialPost && !post) { 47 + post = initialPost; 48 + } 49 + 50 + // Only do initial load if we don't have a post 51 + if (!post) { 52 + loading = true; 53 + loadPost(); 54 + } 41 55 42 56 // Set up polling for new posts 43 57 const pollInterval = setInterval(async () => {
+7 -22
src/lib/components/layout/main/card/KibunStatusCard.svelte
··· 1 1 <script lang="ts"> 2 - import { onMount } from 'svelte'; 3 2 import { Card } from '$lib/components/ui'; 4 - import { fetchKibunStatus, type KibunStatusData } from '$lib/services/atproto'; 3 + import type { KibunStatusData } from '$lib/services/atproto'; 5 4 import { formatRelativeTime } from '$lib/utils/formatDate'; 6 5 7 6 // Icons 8 7 import { Heart } from '@lucide/svelte'; 9 8 10 - let kibunStatus: KibunStatusData | null = null; 11 - let loading = true; 12 - let error: string | null = null; 9 + interface Props { 10 + kibunStatus?: KibunStatusData | null; 11 + } 13 12 14 - onMount(async () => { 15 - try { 16 - kibunStatus = await fetchKibunStatus(); 17 - if (kibunStatus) { 18 - console.log('[KibunStatusCard] Kibun status loaded:', kibunStatus); 19 - } 20 - } catch (err) { 21 - console.error('[KibunStatusCard] Error loading kibun status:', err); 22 - error = err instanceof Error ? err.message : 'Failed to load kibun status'; 23 - } finally { 24 - loading = false; 25 - } 26 - }); 13 + let { kibunStatus = null }: Props = $props(); 27 14 </script> 28 15 29 16 <div class="mx-auto w-full max-w-2xl"> 30 - {#if loading} 17 + {#if !kibunStatus} 31 18 <Card loading={true} variant="elevated" padding="md"> 32 19 {#snippet skeleton()} 33 20 <div class="mb-3"> ··· 43 30 </div> 44 31 {/snippet} 45 32 </Card> 46 - {:else if error} 47 - <Card error={true} errorMessage={error} /> 48 - {:else if kibunStatus} 33 + {:else} 49 34 {@const safeKibunStatus = kibunStatus} 50 35 <Card variant="elevated" padding="md"> 51 36 {#snippet children()}
+9 -25
src/lib/components/layout/main/card/MusicStatusCard.svelte
··· 1 1 <script lang="ts"> 2 - import { onMount } from 'svelte'; 3 2 import { Card } from '$lib/components/ui'; 4 - import { fetchMusicStatus, type MusicStatusData } from '$lib/services/atproto'; 3 + import type { MusicStatusData } from '$lib/services/atproto'; 5 4 import { formatRelativeTime } from '$lib/utils/formatDate'; 6 5 7 6 // Icons 8 7 import { Music, Disc3, Users, Album, Clock, Radio } from '@lucide/svelte'; 9 8 10 - let musicStatus: MusicStatusData | null = null; 11 - let loading = true; 12 - let error: string | null = null; 13 - let artworkError = false; 9 + interface Props { 10 + musicStatus?: MusicStatusData | null; 11 + } 14 12 15 - onMount(async () => { 16 - try { 17 - musicStatus = await fetchMusicStatus(); 18 - if (musicStatus) { 19 - console.log('[MusicStatusCard] Music status loaded:', musicStatus); 20 - console.log('[MusicStatusCard] Artwork URL:', musicStatus.artworkUrl); 21 - console.log('[MusicStatusCard] Release MBID:', musicStatus.releaseMbId); 22 - } 23 - } catch (err) { 24 - console.error('[MusicStatusCard] Error loading music status:', err); 25 - error = err instanceof Error ? err.message : 'Failed to load music status'; 26 - } finally { 27 - loading = false; 28 - } 29 - }); 13 + let { musicStatus = null }: Props = $props(); 14 + 15 + let artworkError = $state(false); 30 16 31 17 function formatArtists(artists: { artistName: string }[]): string { 32 18 if (!artists || artists.length === 0) return 'Unknown Artist'; ··· 52 38 </script> 53 39 54 40 <div class="mx-auto w-full max-w-2xl"> 55 - {#if loading} 41 + {#if !musicStatus} 56 42 <Card loading={true} variant="elevated" padding="md"> 57 43 {#snippet skeleton()} 58 44 <div class="mb-3 flex items-start gap-4"> ··· 69 55 </div> 70 56 {/snippet} 71 57 </Card> 72 - {:else if error} 73 - <Card error={true} errorMessage={error} /> 74 - {:else if musicStatus} 58 + {:else} 75 59 {@const safeMusicStatus = musicStatus} 76 60 <Card variant="elevated" padding="md"> 77 61 {#snippet children()}
+8 -19
src/lib/components/layout/main/card/PostCard.svelte
··· 1 1 <script lang="ts"> 2 - import { onMount } from 'svelte'; 3 2 import { Card } from '$lib/components/ui'; 4 3 import { BlogPostCard } from '$lib/components/ui'; 5 - import { fetchBlogPosts, type BlogPostsData } from '$lib/services/atproto'; 4 + import type { BlogPostsData } from '$lib/services/atproto'; 6 5 7 - let blogPosts: BlogPostsData | null = null; 8 - let loading = true; 9 - let error: string | null = null; 6 + interface Props { 7 + blogPosts?: BlogPostsData | null; 8 + } 10 9 11 - onMount(async () => { 12 - try { 13 - blogPosts = await fetchBlogPosts(); 14 - } catch (err) { 15 - error = err instanceof Error ? err.message : 'Failed to load blog posts'; 16 - } finally { 17 - loading = false; 18 - } 19 - }); 10 + let { blogPosts = null }: Props = $props(); 20 11 </script> 21 12 22 13 <div class="mx-auto w-full max-w-2xl"> 23 - {#if loading} 14 + {#if !blogPosts} 24 15 <Card loading={true} variant="elevated" padding="md"> 25 16 {#snippet skeleton()} 26 17 <div class="mb-4 h-6 w-32 rounded bg-canvas-300 dark:bg-canvas-700"></div> ··· 39 30 </div> 40 31 {/snippet} 41 32 </Card> 42 - {:else if error} 43 - <Card error={true} errorMessage={error} /> 44 - {:else if blogPosts && blogPosts.posts && blogPosts.posts.length > 0} 33 + {:else if blogPosts.posts && blogPosts.posts.length > 0} 45 34 <Card variant="elevated" padding="md"> 46 35 {#snippet children()} 47 36 <h2 class="mb-4 text-2xl font-bold text-ink-900 dark:text-ink-50">Recent Posts</h2> 48 37 <div class="space-y-3"> 49 - {#each blogPosts?.posts ?? [] as post} 38 + {#each blogPosts.posts as post} 50 39 <BlogPostCard {post} /> 51 40 {/each} 52 41 </div>
+12 -22
src/lib/components/layout/main/card/ProfileCard.svelte
··· 1 1 <script lang="ts"> 2 - import { onMount } from 'svelte'; 3 2 import { Card } from '$lib/components/ui'; 4 - import { fetchProfile, type ProfileData } from '$lib/services/atproto'; 3 + import type { ProfileData } from '$lib/services/atproto'; 5 4 import LinkCard from './LinkCard.svelte'; 6 5 import { formatCompactNumber } from '$lib/utils/formatNumber'; 7 6 8 - let profile: ProfileData | null = null; 9 - let loading = true; 10 - let error: string | null = null; 11 - let imageLoaded = false; 12 - let bannerLoaded = false; 7 + interface Props { 8 + profile?: ProfileData | null; 9 + } 10 + 11 + let { profile = null }: Props = $props(); 12 + 13 + let imageLoaded = $state(false); 14 + let bannerLoaded = $state(false); 13 15 14 16 // Detect system locale, fallback to en-GB 15 17 const locale = typeof navigator !== 'undefined' ? navigator.language || 'en-GB' : 'en-GB'; 16 - 17 - onMount(async () => { 18 - try { 19 - profile = await fetchProfile(); 20 - } catch (err) { 21 - error = err instanceof Error ? err.message : 'Failed to load profile'; 22 - } finally { 23 - loading = false; 24 - } 25 - }); 26 18 </script> 27 19 28 20 <div class="mx-auto w-full max-w-2xl"> 29 - {#if loading} 21 + {#if !profile} 30 22 <Card loading={true} variant="elevated" padding="none" class="overflow-hidden"> 31 23 {#snippet skeleton()} 32 24 <!-- Banner skeleton --> ··· 54 46 </div> 55 47 {/snippet} 56 48 </Card> 57 - {:else if error} 58 - <Card error={true} errorMessage={error} /> 59 - {:else if profile} 49 + {:else} 60 50 {@const safeProfile = profile} 61 51 <Card variant="elevated" padding="none" ariaLabel="Profile information"> 62 52 {#snippet children()} ··· 113 103 </h2> 114 104 <p class="font-medium text-ink-700 dark:text-ink-200">@{safeProfile.handle}</p> 115 105 {#if safeProfile.pronouns} 116 - <p class="text-sm italic text-ink-600 dark:text-ink-300">{safeProfile.pronouns}</p> 106 + <p class="text-sm text-ink-600 italic dark:text-ink-300">{safeProfile.pronouns}</p> 117 107 {/if} 118 108 119 109 {#if safeProfile.description}
+11 -21
src/lib/components/layout/main/card/TangledRepoCard.svelte
··· 1 1 <script lang="ts"> 2 - import { onMount } from 'svelte'; 3 2 import { ExternalLink, GitBranch, Server, User } from '@lucide/svelte'; 4 3 import { Card, InternalCard } from '$lib/components/ui'; 5 - import { fetchTangledRepos, type TangledReposData, fetchProfile } from '$lib/services/atproto'; 4 + import type { TangledReposData, ProfileData } from '$lib/services/atproto'; 6 5 import { PUBLIC_ATPROTO_DID } from '$env/static/public'; 7 6 8 - let repos: TangledReposData | null = $state(null); 9 - let handle: string | null = $state(null); 10 - let loading = $state(true); 11 - let error: string | null = $state(null); 7 + interface Props { 8 + repos?: TangledReposData | null; 9 + profile?: ProfileData | null; 10 + } 12 11 13 - onMount(async () => { 14 - try { 15 - const [reposData, profile] = await Promise.all([fetchTangledRepos(), fetchProfile()]); 16 - repos = reposData; 17 - handle = profile.handle; 18 - } catch (err) { 19 - error = err instanceof Error ? err.message : 'Failed to load Tangled repositories'; 20 - } finally { 21 - loading = false; 22 - } 23 - }); 12 + let { repos = null, profile = null }: Props = $props(); 13 + 14 + // Derive handle from profile 15 + let handle = $derived(profile?.handle || null); 24 16 25 17 // Build the tangled.org URL: tangled.org/[handle or did]/[repo] 26 18 // Prefer handle if available, otherwise use DID ··· 44 36 </script> 45 37 46 38 <div class="mx-auto w-full max-w-2xl"> 47 - {#if loading} 39 + {#if !repos} 48 40 <Card loading={true} variant="elevated" padding="md"> 49 41 {#snippet skeleton()} 50 42 <div class="mb-4 h-6 w-32 rounded bg-canvas-300 dark:bg-canvas-700"></div> ··· 55 47 </div> 56 48 {/snippet} 57 49 </Card> 58 - {:else if error} 59 - <Card error={true} errorMessage={error} /> 60 - {:else if repos && repos.repos.length > 0} 50 + {:else if repos.repos.length > 0} 61 51 {@const safeRepos = repos} 62 52 <Card variant="elevated" padding="md"> 63 53 {#snippet children()}
+1 -1
src/lib/components/ui/BlogPostCard.svelte
··· 54 54 </div> 55 55 56 56 <!-- Right column: External Link Icon and Tags --> 57 - <div class="flex shrink-0 flex-col items-end justify-between self-stretch gap-2"> 57 + <div class="flex shrink-0 flex-col items-end justify-between gap-2 self-stretch"> 58 58 <!-- External Link Icon --> 59 59 <ExternalLink 60 60 class="h-4 w-4 text-ink-700 transition-colors dark:text-ink-200"
+3 -1
src/lib/components/ui/Card.svelte
··· 88 88 let interactiveClasses = $derived(interactive || isLink ? 'cursor-pointer' : ''); 89 89 90 90 // Combine all classes 91 - let cardClasses = $derived(`${baseClasses} ${variantClasses[variant]} ${paddingClasses[padding]} ${interactiveClasses} ${customClass}`); 91 + let cardClasses = $derived( 92 + `${baseClasses} ${variantClasses[variant]} ${paddingClasses[padding]} ${interactiveClasses} ${customClass}` 93 + ); 92 94 93 95 /** 94 96 * Get badge styling classes based on color and variant
+6 -1
src/lib/components/ui/Pagination.svelte
··· 92 92 </div> 93 93 94 94 <!-- Page Info --> 95 - <p class="mt-4 text-center text-sm text-ink-600 dark:text-ink-300" role="status" aria-live="polite" aria-atomic="true"> 95 + <p 96 + class="mt-4 text-center text-sm text-ink-600 dark:text-ink-300" 97 + role="status" 98 + aria-live="polite" 99 + aria-atomic="true" 100 + > 96 101 Page {currentPage} of {totalPages} &middot; Showing {startItem}–{endItem} of {totalItems} 97 102 {totalItems === 1 ? 'item' : 'items'} 98 103 </p>
+34
src/lib/config/cache.config.server.ts
··· 1 + import { env } from '$env/dynamic/private'; 2 + import { CACHE_TTL as DEFAULT_CACHE_TTL } from './cache.config'; 3 + 4 + /** 5 + * Server-only cache configuration that can override defaults with environment variables 6 + * This file should only be imported in server-side code (e.g., +page.server.ts, +layout.server.ts) 7 + */ 8 + 9 + // Parse environment variable or use default (in milliseconds) 10 + const getEnvTTL = (key: string, defaultValue: number): number => { 11 + const value = env[key]; 12 + if (value) { 13 + const minutes = parseInt(value, 10); 14 + return isNaN(minutes) ? defaultValue : minutes * 60 * 1000; 15 + } 16 + return defaultValue; 17 + }; 18 + 19 + /** 20 + * Cache TTL configuration with environment variable overrides 21 + * Values are loaded from environment variables with fallbacks to defaults from cache.config.ts 22 + */ 23 + export const CACHE_TTL = { 24 + PROFILE: getEnvTTL('CACHE_TTL_PROFILE', DEFAULT_CACHE_TTL.PROFILE), 25 + SITE_INFO: getEnvTTL('CACHE_TTL_SITE_INFO', DEFAULT_CACHE_TTL.SITE_INFO), 26 + LINKS: getEnvTTL('CACHE_TTL_LINKS', DEFAULT_CACHE_TTL.LINKS), 27 + MUSIC_STATUS: getEnvTTL('CACHE_TTL_MUSIC_STATUS', DEFAULT_CACHE_TTL.MUSIC_STATUS), 28 + KIBUN_STATUS: getEnvTTL('CACHE_TTL_KIBUN_STATUS', DEFAULT_CACHE_TTL.KIBUN_STATUS), 29 + TANGLED_REPOS: getEnvTTL('CACHE_TTL_TANGLED_REPOS', DEFAULT_CACHE_TTL.TANGLED_REPOS), 30 + BLOG_POSTS: getEnvTTL('CACHE_TTL_BLOG_POSTS', DEFAULT_CACHE_TTL.BLOG_POSTS), 31 + PUBLICATIONS: getEnvTTL('CACHE_TTL_PUBLICATIONS', DEFAULT_CACHE_TTL.PUBLICATIONS), 32 + INDIVIDUAL_POST: getEnvTTL('CACHE_TTL_INDIVIDUAL_POST', DEFAULT_CACHE_TTL.INDIVIDUAL_POST), 33 + IDENTITY: getEnvTTL('CACHE_TTL_IDENTITY', DEFAULT_CACHE_TTL.IDENTITY) 34 + } as const;
+12 -23
src/lib/config/cache.config.ts
··· 1 1 import { dev } from '$app/environment'; 2 - import { env } from '$env/dynamic/private'; 3 2 4 3 /** 5 4 * Cache configuration with environment-aware TTL values ··· 7 6 * Development: Shorter TTLs for faster iteration 8 7 * Production: Longer TTLs to reduce API calls and prevent timeouts 9 8 */ 10 - 11 - // Parse environment variable or use default (in milliseconds) 12 - const getEnvTTL = (key: string, defaultMinutes: number): number => { 13 - const value = env[key]; 14 - if (value) { 15 - const minutes = parseInt(value, 10); 16 - return isNaN(minutes) ? defaultMinutes * 60 * 1000 : minutes * 60 * 1000; 17 - } 18 - return defaultMinutes * 60 * 1000; 19 - }; 20 9 21 10 /** 22 11 * Default TTL values (in minutes) for different data types ··· 57 46 }; 58 47 59 48 /** 60 - * Cache TTL configuration 61 - * Values are loaded from environment variables with fallbacks to defaults 49 + * Cache TTL configuration in milliseconds 50 + * These are the default values - can be overridden via environment variables in server code 62 51 */ 63 52 export const CACHE_TTL = { 64 - PROFILE: getEnvTTL('CACHE_TTL_PROFILE', DEFAULT_TTL.PROFILE), 65 - SITE_INFO: getEnvTTL('CACHE_TTL_SITE_INFO', DEFAULT_TTL.SITE_INFO), 66 - LINKS: getEnvTTL('CACHE_TTL_LINKS', DEFAULT_TTL.LINKS), 67 - MUSIC_STATUS: getEnvTTL('CACHE_TTL_MUSIC_STATUS', DEFAULT_TTL.MUSIC_STATUS), 68 - KIBUN_STATUS: getEnvTTL('CACHE_TTL_KIBUN_STATUS', DEFAULT_TTL.KIBUN_STATUS), 69 - TANGLED_REPOS: getEnvTTL('CACHE_TTL_TANGLED_REPOS', DEFAULT_TTL.TANGLED_REPOS), 70 - BLOG_POSTS: getEnvTTL('CACHE_TTL_BLOG_POSTS', DEFAULT_TTL.BLOG_POSTS), 71 - PUBLICATIONS: getEnvTTL('CACHE_TTL_PUBLICATIONS', DEFAULT_TTL.PUBLICATIONS), 72 - INDIVIDUAL_POST: getEnvTTL('CACHE_TTL_INDIVIDUAL_POST', DEFAULT_TTL.INDIVIDUAL_POST), 73 - IDENTITY: getEnvTTL('CACHE_TTL_IDENTITY', DEFAULT_TTL.IDENTITY) 53 + PROFILE: DEFAULT_TTL.PROFILE * 60 * 1000, 54 + SITE_INFO: DEFAULT_TTL.SITE_INFO * 60 * 1000, 55 + LINKS: DEFAULT_TTL.LINKS * 60 * 1000, 56 + MUSIC_STATUS: DEFAULT_TTL.MUSIC_STATUS * 60 * 1000, 57 + KIBUN_STATUS: DEFAULT_TTL.KIBUN_STATUS * 60 * 1000, 58 + TANGLED_REPOS: DEFAULT_TTL.TANGLED_REPOS * 60 * 1000, 59 + BLOG_POSTS: DEFAULT_TTL.BLOG_POSTS * 60 * 1000, 60 + PUBLICATIONS: DEFAULT_TTL.PUBLICATIONS * 60 * 1000, 61 + INDIVIDUAL_POST: DEFAULT_TTL.INDIVIDUAL_POST * 60 * 1000, 62 + IDENTITY: DEFAULT_TTL.IDENTITY * 60 * 1000 74 63 } as const; 75 64 76 65 /**
+3 -1
src/lib/config/slugs.ts
··· 35 35 * @param slug - The slug to look up (will be normalized) 36 36 * @returns Object with rkey and platform, or null if not found 37 37 */ 38 - export function getPublicationFromSlug(slug: string): { rkey: string; platform: PublicationPlatform } | null { 38 + export function getPublicationFromSlug( 39 + slug: string 40 + ): { rkey: string; platform: PublicationPlatform } | null { 39 41 const normalizedSlug = normalizeSlug(slug); 40 42 const mapping = slugMappings.find((m) => normalizeSlug(m.slug) === normalizedSlug); 41 43 if (!mapping) return null;
+6 -2
src/lib/services/atproto/cache.ts
··· 41 41 const age = Date.now() - entry.timestamp; 42 42 43 43 if (age > ttl) { 44 - console.info(`[Cache] Entry expired for key: ${key} (age: ${Math.round(age / 1000)}s, ttl: ${Math.round(ttl / 1000)}s)`); 44 + console.info( 45 + `[Cache] Entry expired for key: ${key} (age: ${Math.round(age / 1000)}s, ttl: ${Math.round(ttl / 1000)}s)` 46 + ); 45 47 this.cache.delete(key); 46 48 return null; 47 49 } 48 50 49 - console.info(`[Cache] Cache hit for key: ${key} (age: ${Math.round(age / 1000)}s, ttl: ${Math.round(ttl / 1000)}s)`); 51 + console.info( 52 + `[Cache] Cache hit for key: ${key} (age: ${Math.round(age / 1000)}s, ttl: ${Math.round(ttl / 1000)}s)` 53 + ); 50 54 return entry.data; 51 55 } 52 56
+1 -4
src/lib/services/atproto/index.ts
··· 51 51 fetchTangledRepos 52 52 } from './fetch'; 53 53 54 - export { 55 - fetchStandardSitePublications, 56 - fetchStandardSiteDocuments 57 - } from './standard'; 54 + export { fetchStandardSitePublications, fetchStandardSiteDocuments } from './standard'; 58 55 59 56 export { 60 57 fetchBlogPosts,
+4 -6
src/lib/services/atproto/standard.ts
··· 136 136 // It's a publication URI 137 137 publication = publicationsMap.get(siteValue); 138 138 publicationRkey = siteValue.split('/').pop(); 139 - 139 + 140 140 // Build URL from publication base URL + document path 141 141 if (publication) { 142 - const basePath = publication.url.endsWith('/') 143 - ? publication.url.slice(0, -1) 142 + const basePath = publication.url.endsWith('/') 143 + ? publication.url.slice(0, -1) 144 144 : publication.url; 145 145 const docPath = docValue.path || `/${rkey}`; 146 146 url = `${basePath}${docPath.startsWith('/') ? docPath : '/' + docPath}`; ··· 178 178 } 179 179 180 180 // Sort by publishedAt (newest first) 181 - documents.sort( 182 - (a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime() 183 - ); 181 + documents.sort((a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime()); 184 182 185 183 const data: StandardSiteDocumentsData = { documents }; 186 184 cache.set(cacheKey, data);
+1 -1
src/lib/stores/colorTheme.ts
··· 24 24 const theme = stored || DEFAULT_THEME; 25 25 26 26 update((state) => ({ ...state, current: theme, mounted: true })); 27 - 27 + 28 28 // Only apply theme if not already applied (to prevent flash) 29 29 const currentTheme = document.documentElement.getAttribute('data-color-theme'); 30 30 if (currentTheme !== theme) {
+11 -11
src/lib/styles/themes/amber.css
··· 23 23 --color-ink-50: light-dark(oklch(18% 0.023 85), oklch(97.8% 0.015 85)); 24 24 --color-ink-100: light-dark(oklch(26% 0.042 85), oklch(93.5% 0.032 85)); 25 25 --color-ink-200: light-dark(oklch(39.5% 0.072 85), oklch(85.5% 0.062 85)); 26 - --color-ink-300: light-dark(oklch(51.5% 0.100 85), oklch(75.5% 0.092 85)); 27 - --color-ink-400: light-dark(oklch(63% 0.125 85), oklch(65.5% 0.120 85)); 28 - --color-ink-500: light-dark(oklch(74% 0.150 85), oklch(55.5% 0.150 85)); 29 - --color-ink-600: light-dark(oklch(78.8% 0.120 85), oklch(45.5% 0.125 85)); 30 - --color-ink-700: light-dark(oklch(84% 0.092 85), oklch(35.5% 0.100 85)); 26 + --color-ink-300: light-dark(oklch(51.5% 0.1 85), oklch(75.5% 0.092 85)); 27 + --color-ink-400: light-dark(oklch(63% 0.125 85), oklch(65.5% 0.12 85)); 28 + --color-ink-500: light-dark(oklch(74% 0.15 85), oklch(55.5% 0.15 85)); 29 + --color-ink-600: light-dark(oklch(78.8% 0.12 85), oklch(45.5% 0.125 85)); 30 + --color-ink-700: light-dark(oklch(84% 0.092 85), oklch(35.5% 0.1 85)); 31 31 --color-ink-800: light-dark(oklch(89.5% 0.062 85), oklch(25.5% 0.072 85)); 32 32 --color-ink-900: light-dark(oklch(94.8% 0.032 85), oklch(18.5% 0.042 85)); 33 33 --color-ink-950: light-dark(oklch(97.8% 0.015 85), oklch(12.5% 0.023 85)); 34 34 35 35 /* Canvas - Yellow-tinted backgrounds (85°) */ 36 36 --color-canvas-50: light-dark(oklch(18.2% 0.026 85), oklch(98.6% 0.009 85)); 37 - --color-canvas-100: light-dark(oklch(26.2% 0.047 85), oklch(96.8% 0.020 85)); 37 + --color-canvas-100: light-dark(oklch(26.2% 0.047 85), oklch(96.8% 0.02 85)); 38 38 --color-canvas-200: light-dark(oklch(40% 0.082 85), oklch(92.5% 0.045 85)); 39 - --color-canvas-300: light-dark(oklch(52.8% 0.110 85), oklch(86.5% 0.072 85)); 39 + --color-canvas-300: light-dark(oklch(52.8% 0.11 85), oklch(86.5% 0.072 85)); 40 40 --color-canvas-400: light-dark(oklch(65% 0.138 85), oklch(80.5% 0.102 85)); 41 41 --color-canvas-500: light-dark(oklch(76.5% 0.165 85), oklch(76.5% 0.128 85)); 42 42 --color-canvas-600: light-dark(oklch(80.5% 0.102 85), oklch(65% 0.138 85)); 43 - --color-canvas-700: light-dark(oklch(86.5% 0.072 85), oklch(52.8% 0.110 85)); 43 + --color-canvas-700: light-dark(oklch(86.5% 0.072 85), oklch(52.8% 0.11 85)); 44 44 --color-canvas-800: light-dark(oklch(92.5% 0.045 85), oklch(40% 0.082 85)); 45 - --color-canvas-900: light-dark(oklch(96.8% 0.020 85), oklch(26.2% 0.047 85)); 45 + --color-canvas-900: light-dark(oklch(96.8% 0.02 85), oklch(26.2% 0.047 85)); 46 46 --color-canvas-950: light-dark(oklch(98.6% 0.009 85), oklch(18.2% 0.026 85)); 47 47 48 48 /* Secondary - Lime Green (115°) */ ··· 60 60 61 61 /* Accent - Gold (60°) */ 62 62 --color-accent-50: light-dark(oklch(19.3% 0.037 60), oklch(98% 0.024 60)); 63 - --color-accent-100: light-dark(oklch(28% 0.060 60), oklch(95.2% 0.046 60)); 63 + --color-accent-100: light-dark(oklch(28% 0.06 60), oklch(95.2% 0.046 60)); 64 64 --color-accent-200: light-dark(oklch(43% 0.102 60), oklch(90.2% 0.092 60)); 65 65 --color-accent-300: light-dark(oklch(57.2% 0.138 60), oklch(81.8% 0.132 60)); 66 66 --color-accent-400: light-dark(oklch(70% 0.172 60), oklch(72.5% 0.168 60)); ··· 68 68 --color-accent-600: light-dark(oklch(85% 0.168 60), oklch(53% 0.172 60)); 69 69 --color-accent-700: light-dark(oklch(88.5% 0.132 60), oklch(43% 0.138 60)); 70 70 --color-accent-800: light-dark(oklch(92.5% 0.092 60), oklch(33% 0.102 60)); 71 - --color-accent-900: light-dark(oklch(96.2% 0.046 60), oklch(24.2% 0.060 60)); 71 + --color-accent-900: light-dark(oklch(96.2% 0.046 60), oklch(24.2% 0.06 60)); 72 72 --color-accent-950: light-dark(oklch(98.5% 0.024 60), oklch(17% 0.037 60)); 73 73 }
+12 -12
src/lib/styles/themes/coral.css
··· 7 7 ============================================================================ */ 8 8 [data-color-theme='coral'] { 9 9 /* Primary - Coral (20°) */ 10 - --color-primary-50: light-dark(oklch(19.2% 0.040 20), oklch(97.9% 0.027 20)); 11 - --color-primary-100: light-dark(oklch(28% 0.065 20), oklch(94.8% 0.050 20)); 10 + --color-primary-50: light-dark(oklch(19.2% 0.04 20), oklch(97.9% 0.027 20)); 11 + --color-primary-100: light-dark(oklch(28% 0.065 20), oklch(94.8% 0.05 20)); 12 12 --color-primary-200: light-dark(oklch(43% 0.108 20), oklch(89.5% 0.098 20)); 13 13 --color-primary-300: light-dark(oklch(57% 0.145 20), oklch(80.8% 0.142 20)); 14 - --color-primary-400: light-dark(oklch(69.8% 0.180 20), oklch(71.5% 0.178 20)); 14 + --color-primary-400: light-dark(oklch(69.8% 0.18 20), oklch(71.5% 0.178 20)); 15 15 --color-primary-500: light-dark(oklch(81.8% 0.212 20), oklch(62% 0.212 20)); 16 - --color-primary-600: light-dark(oklch(84.8% 0.178 20), oklch(51.5% 0.180 20)); 16 + --color-primary-600: light-dark(oklch(84.8% 0.178 20), oklch(51.5% 0.18 20)); 17 17 --color-primary-700: light-dark(oklch(88.2% 0.142 20), oklch(41.5% 0.145 20)); 18 18 --color-primary-800: light-dark(oklch(92% 0.098 20), oklch(31.5% 0.108 20)); 19 - --color-primary-900: light-dark(oklch(96% 0.050 20), oklch(23% 0.065 20)); 20 - --color-primary-950: light-dark(oklch(98.2% 0.027 20), oklch(16.2% 0.040 20)); 19 + --color-primary-900: light-dark(oklch(96% 0.05 20), oklch(23% 0.065 20)); 20 + --color-primary-950: light-dark(oklch(98.2% 0.027 20), oklch(16.2% 0.04 20)); 21 21 22 22 /* Ink - Coral-tinted text (20°) */ 23 23 --color-ink-50: light-dark(oklch(17.8% 0.027 20), oklch(97.6% 0.018 20)); 24 24 --color-ink-100: light-dark(oklch(25.5% 0.048 20), oklch(93.2% 0.037 20)); 25 - --color-ink-200: light-dark(oklch(39% 0.082 20), oklch(85.2% 0.070 20)); 25 + --color-ink-200: light-dark(oklch(39% 0.082 20), oklch(85.2% 0.07 20)); 26 26 --color-ink-300: light-dark(oklch(51% 0.115 20), oklch(75.2% 0.102 20)); 27 27 --color-ink-400: light-dark(oklch(62.5% 0.145 20), oklch(65.2% 0.132 20)); 28 28 --color-ink-500: light-dark(oklch(73.5% 0.175 20), oklch(55.2% 0.175 20)); 29 29 --color-ink-600: light-dark(oklch(78.5% 0.132 20), oklch(45.2% 0.145 20)); 30 30 --color-ink-700: light-dark(oklch(83.8% 0.102 20), oklch(35.2% 0.115 20)); 31 - --color-ink-800: light-dark(oklch(89.2% 0.070 20), oklch(25.2% 0.082 20)); 31 + --color-ink-800: light-dark(oklch(89.2% 0.07 20), oklch(25.2% 0.082 20)); 32 32 --color-ink-900: light-dark(oklch(94.5% 0.037 20), oklch(18.2% 0.048 20)); 33 33 --color-ink-950: light-dark(oklch(97.6% 0.018 20), oklch(12.5% 0.027 20)); 34 34 35 35 /* Canvas - Coral-tinted backgrounds (20°) */ 36 - --color-canvas-50: light-dark(oklch(18% 0.030 20), oklch(98.5% 0.011 20)); 36 + --color-canvas-50: light-dark(oklch(18% 0.03 20), oklch(98.5% 0.011 20)); 37 37 --color-canvas-100: light-dark(oklch(26% 0.053 20), oklch(96.5% 0.024 20)); 38 - --color-canvas-200: light-dark(oklch(39.8% 0.092 20), oklch(92% 0.050 20)); 38 + --color-canvas-200: light-dark(oklch(39.8% 0.092 20), oklch(92% 0.05 20)); 39 39 --color-canvas-300: light-dark(oklch(52.5% 0.125 20), oklch(86% 0.082 20)); 40 40 --color-canvas-400: light-dark(oklch(64.5% 0.155 20), oklch(80% 0.115 20)); 41 41 --color-canvas-500: light-dark(oklch(76% 0.185 20), oklch(76% 0.145 20)); 42 42 --color-canvas-600: light-dark(oklch(80% 0.115 20), oklch(64.5% 0.155 20)); 43 43 --color-canvas-700: light-dark(oklch(86% 0.082 20), oklch(52.5% 0.125 20)); 44 - --color-canvas-800: light-dark(oklch(92% 0.050 20), oklch(39.8% 0.092 20)); 44 + --color-canvas-800: light-dark(oklch(92% 0.05 20), oklch(39.8% 0.092 20)); 45 45 --color-canvas-900: light-dark(oklch(96.5% 0.024 20), oklch(26% 0.053 20)); 46 - --color-canvas-950: light-dark(oklch(98.5% 0.011 20), oklch(18% 0.030 20)); 46 + --color-canvas-950: light-dark(oklch(98.5% 0.011 20), oklch(18% 0.03 20)); 47 47 48 48 /* Secondary - Peach (35°) */ 49 49 --color-secondary-50: light-dark(oklch(19.3% 0.038 35), oklch(98% 0.025 35));
+17 -17
src/lib/styles/themes/forest.css
··· 8 8 [data-color-theme='forest'] { 9 9 /* Primary - Green (145°) */ 10 10 --color-primary-50: light-dark(oklch(18.8% 0.036 145), oklch(97.6% 0.024 145)); 11 - --color-primary-100: light-dark(oklch(27.2% 0.060 145), oklch(94.3% 0.046 145)); 12 - --color-primary-200: light-dark(oklch(41.8% 0.098 145), oklch(88.8% 0.090 145)); 13 - --color-primary-300: light-dark(oklch(55.5% 0.132 145), oklch(79.2% 0.130 145)); 11 + --color-primary-100: light-dark(oklch(27.2% 0.06 145), oklch(94.3% 0.046 145)); 12 + --color-primary-200: light-dark(oklch(41.8% 0.098 145), oklch(88.8% 0.09 145)); 13 + --color-primary-300: light-dark(oklch(55.5% 0.132 145), oklch(79.2% 0.13 145)); 14 14 --color-primary-400: light-dark(oklch(67.8% 0.165 145), oklch(69.5% 0.168 145)); 15 15 --color-primary-500: light-dark(oklch(79.5% 0.195 145), oklch(59.8% 0.195 145)); 16 16 --color-primary-600: light-dark(oklch(82.8% 0.168 145), oklch(49.2% 0.165 145)); 17 - --color-primary-700: light-dark(oklch(86.8% 0.130 145), oklch(39.5% 0.132 145)); 18 - --color-primary-800: light-dark(oklch(91% 0.090 145), oklch(29.8% 0.098 145)); 19 - --color-primary-900: light-dark(oklch(95.5% 0.046 145), oklch(21.5% 0.060 145)); 17 + --color-primary-700: light-dark(oklch(86.8% 0.13 145), oklch(39.5% 0.132 145)); 18 + --color-primary-800: light-dark(oklch(91% 0.09 145), oklch(29.8% 0.098 145)); 19 + --color-primary-900: light-dark(oklch(95.5% 0.046 145), oklch(21.5% 0.06 145)); 20 20 --color-primary-950: light-dark(oklch(97.9% 0.024 145), oklch(15.2% 0.036 145)); 21 21 22 22 /* Ink - Green-tinted text (145°) */ ··· 33 33 --color-ink-950: light-dark(oklch(97.4% 0.016 145), oklch(12% 0.024 145)); 34 34 35 35 /* Canvas - Green-tinted backgrounds (145°) */ 36 - --color-canvas-50: light-dark(oklch(17.9% 0.028 145), oklch(98.4% 0.010 145)); 37 - --color-canvas-100: light-dark(oklch(25.9% 0.050 145), oklch(96.4% 0.022 145)); 36 + --color-canvas-50: light-dark(oklch(17.9% 0.028 145), oklch(98.4% 0.01 145)); 37 + --color-canvas-100: light-dark(oklch(25.9% 0.05 145), oklch(96.4% 0.022 145)); 38 38 --color-canvas-200: light-dark(oklch(39.8% 0.088 145), oklch(92% 0.048 145)); 39 39 --color-canvas-300: light-dark(oklch(52.5% 0.118 145), oklch(86% 0.078 145)); 40 40 --color-canvas-400: light-dark(oklch(64.5% 0.148 145), oklch(80% 0.108 145)); ··· 42 42 --color-canvas-600: light-dark(oklch(80% 0.108 145), oklch(64.5% 0.148 145)); 43 43 --color-canvas-700: light-dark(oklch(86% 0.078 145), oklch(52.5% 0.118 145)); 44 44 --color-canvas-800: light-dark(oklch(92% 0.048 145), oklch(39.8% 0.088 145)); 45 - --color-canvas-900: light-dark(oklch(96.4% 0.022 145), oklch(25.9% 0.050 145)); 46 - --color-canvas-950: light-dark(oklch(98.4% 0.010 145), oklch(17.9% 0.028 145)); 45 + --color-canvas-900: light-dark(oklch(96.4% 0.022 145), oklch(25.9% 0.05 145)); 46 + --color-canvas-950: light-dark(oklch(98.4% 0.01 145), oklch(17.9% 0.028 145)); 47 47 48 48 /* Secondary - Yellow-Green (125°) */ 49 49 --color-secondary-50: light-dark(oklch(19.2% 0.038 125), oklch(97.8% 0.025 125)); ··· 59 59 --color-secondary-950: light-dark(oklch(98.2% 0.025 125), oklch(15.8% 0.038 125)); 60 60 61 61 /* Accent - Deep Emerald (160°) */ 62 - --color-accent-50: light-dark(oklch(19% 0.040 160), oklch(97.8% 0.027 160)); 63 - --color-accent-100: light-dark(oklch(27.5% 0.065 160), oklch(94.5% 0.050 160)); 64 - --color-accent-200: light-dark(oklch(42.5% 0.110 160), oklch(89.5% 0.098 160)); 62 + --color-accent-50: light-dark(oklch(19% 0.04 160), oklch(97.8% 0.027 160)); 63 + --color-accent-100: light-dark(oklch(27.5% 0.065 160), oklch(94.5% 0.05 160)); 64 + --color-accent-200: light-dark(oklch(42.5% 0.11 160), oklch(89.5% 0.098 160)); 65 65 --color-accent-300: light-dark(oklch(56.5% 0.148 160), oklch(80.5% 0.142 160)); 66 66 --color-accent-400: light-dark(oklch(69.5% 0.185 160), oklch(70.5% 0.178 160)); 67 - --color-accent-500: light-dark(oklch(81.5% 0.220 160), oklch(61% 0.220 160)); 67 + --color-accent-500: light-dark(oklch(81.5% 0.22 160), oklch(61% 0.22 160)); 68 68 --color-accent-600: light-dark(oklch(84.5% 0.178 160), oklch(50.5% 0.185 160)); 69 69 --color-accent-700: light-dark(oklch(88% 0.142 160), oklch(40.5% 0.148 160)); 70 - --color-accent-800: light-dark(oklch(91.8% 0.098 160), oklch(30.5% 0.110 160)); 71 - --color-accent-900: light-dark(oklch(95.8% 0.050 160), oklch(22.5% 0.065 160)); 72 - --color-accent-950: light-dark(oklch(98% 0.027 160), oklch(16% 0.040 160)); 70 + --color-accent-800: light-dark(oklch(91.8% 0.098 160), oklch(30.5% 0.11 160)); 71 + --color-accent-900: light-dark(oklch(95.8% 0.05 160), oklch(22.5% 0.065 160)); 72 + --color-accent-950: light-dark(oklch(98% 0.027 160), oklch(16% 0.04 160)); 73 73 }
+6 -6
src/lib/styles/themes/lavender.css
··· 21 21 22 22 /* Ink - Purple-tinted text (295°) */ 23 23 --color-ink-50: light-dark(oklch(18% 0.028 295), oklch(97.6% 0.018 295)); 24 - --color-ink-100: light-dark(oklch(26% 0.050 295), oklch(93.2% 0.038 295)); 24 + --color-ink-100: light-dark(oklch(26% 0.05 295), oklch(93.2% 0.038 295)); 25 25 --color-ink-200: light-dark(oklch(39.5% 0.085 295), oklch(85.2% 0.072 295)); 26 26 --color-ink-300: light-dark(oklch(51.5% 0.118 295), oklch(75.2% 0.105 295)); 27 27 --color-ink-400: light-dark(oklch(63% 0.148 295), oklch(65.2% 0.135 295)); ··· 29 29 --color-ink-600: light-dark(oklch(78.8% 0.135 295), oklch(45.2% 0.148 295)); 30 30 --color-ink-700: light-dark(oklch(84% 0.105 295), oklch(35.2% 0.118 295)); 31 31 --color-ink-800: light-dark(oklch(89.5% 0.072 295), oklch(25.2% 0.085 295)); 32 - --color-ink-900: light-dark(oklch(94.8% 0.038 295), oklch(18.2% 0.050 295)); 32 + --color-ink-900: light-dark(oklch(94.8% 0.038 295), oklch(18.2% 0.05 295)); 33 33 --color-ink-950: light-dark(oklch(97.6% 0.018 295), oklch(12.5% 0.028 295)); 34 34 35 35 /* Canvas - Purple-tinted backgrounds (295°) */ ··· 48 48 /* Secondary - Violet (280°) */ 49 49 --color-secondary-50: light-dark(oklch(19.2% 0.041 280), oklch(97.9% 0.027 280)); 50 50 --color-secondary-100: light-dark(oklch(27.8% 0.066 280), oklch(94.8% 0.051 280)); 51 - --color-secondary-200: light-dark(oklch(42.8% 0.112 280), oklch(89.8% 0.100 280)); 51 + --color-secondary-200: light-dark(oklch(42.8% 0.112 280), oklch(89.8% 0.1 280)); 52 52 --color-secondary-300: light-dark(oklch(56.8% 0.151 280), oklch(81% 0.145 280)); 53 53 --color-secondary-400: light-dark(oklch(69.8% 0.188 280), oklch(71.5% 0.182 280)); 54 54 --color-secondary-500: light-dark(oklch(81.8% 0.224 280), oklch(62% 0.224 280)); 55 55 --color-secondary-600: light-dark(oklch(84.8% 0.182 280), oklch(51.5% 0.188 280)); 56 56 --color-secondary-700: light-dark(oklch(88.2% 0.145 280), oklch(41.5% 0.151 280)); 57 - --color-secondary-800: light-dark(oklch(92% 0.100 280), oklch(31.5% 0.112 280)); 57 + --color-secondary-800: light-dark(oklch(92% 0.1 280), oklch(31.5% 0.112 280)); 58 58 --color-secondary-900: light-dark(oklch(96% 0.051 280), oklch(23% 0.066 280)); 59 59 --color-secondary-950: light-dark(oklch(98.2% 0.027 280), oklch(16.2% 0.041 280)); 60 60 61 61 /* Accent - Deep Plum (310°) */ 62 62 --color-accent-50: light-dark(oklch(19.5% 0.044 310), oklch(98.1% 0.029 310)); 63 63 --color-accent-100: light-dark(oklch(28.2% 0.071 310), oklch(95.2% 0.054 310)); 64 - --color-accent-200: light-dark(oklch(43.5% 0.120 310), oklch(90.2% 0.105 310)); 64 + --color-accent-200: light-dark(oklch(43.5% 0.12 310), oklch(90.2% 0.105 310)); 65 65 --color-accent-300: light-dark(oklch(57.8% 0.162 310), oklch(82% 0.152 310)); 66 66 --color-accent-400: light-dark(oklch(71% 0.202 310), oklch(72.5% 0.192 310)); 67 67 --color-accent-500: light-dark(oklch(83.5% 0.238 310), oklch(63.2% 0.238 310)); 68 68 --color-accent-600: light-dark(oklch(86.5% 0.192 310), oklch(52.5% 0.202 310)); 69 69 --color-accent-700: light-dark(oklch(89.5% 0.152 310), oklch(42.5% 0.162 310)); 70 - --color-accent-800: light-dark(oklch(92.8% 0.105 310), oklch(32.5% 0.120 310)); 70 + --color-accent-800: light-dark(oklch(92.8% 0.105 310), oklch(32.5% 0.12 310)); 71 71 --color-accent-900: light-dark(oklch(96.5% 0.054 310), oklch(24% 0.071 310)); 72 72 --color-accent-950: light-dark(oklch(98.5% 0.029 310), oklch(17% 0.044 310)); 73 73 }
+18 -18
src/lib/styles/themes/ocean.css
··· 23 23 --color-ink-50: light-dark(oklch(17.6% 0.023 240), oklch(97.4% 0.015 240)); 24 24 --color-ink-100: light-dark(oklch(25.2% 0.043 240), oklch(93% 0.033 240)); 25 25 --color-ink-200: light-dark(oklch(38.5% 0.073 240), oklch(85% 0.063 240)); 26 - --color-ink-300: light-dark(oklch(50.8% 0.100 240), oklch(75% 0.093 240)); 27 - --color-ink-400: light-dark(oklch(62.5% 0.125 240), oklch(65% 0.120 240)); 28 - --color-ink-500: light-dark(oklch(73.5% 0.150 240), oklch(55% 0.150 240)); 29 - --color-ink-600: light-dark(oklch(78.5% 0.120 240), oklch(45% 0.125 240)); 30 - --color-ink-700: light-dark(oklch(83.8% 0.093 240), oklch(35% 0.100 240)); 26 + --color-ink-300: light-dark(oklch(50.8% 0.1 240), oklch(75% 0.093 240)); 27 + --color-ink-400: light-dark(oklch(62.5% 0.125 240), oklch(65% 0.12 240)); 28 + --color-ink-500: light-dark(oklch(73.5% 0.15 240), oklch(55% 0.15 240)); 29 + --color-ink-600: light-dark(oklch(78.5% 0.12 240), oklch(45% 0.125 240)); 30 + --color-ink-700: light-dark(oklch(83.8% 0.093 240), oklch(35% 0.1 240)); 31 31 --color-ink-800: light-dark(oklch(89.2% 0.063 240), oklch(25% 0.073 240)); 32 32 --color-ink-900: light-dark(oklch(94.5% 0.033 240), oklch(18% 0.043 240)); 33 33 --color-ink-950: light-dark(oklch(97.4% 0.015 240), oklch(12% 0.023 240)); 34 34 35 35 /* Canvas - Blue-tinted backgrounds (240°) */ 36 36 --color-canvas-50: light-dark(oklch(17.9% 0.026 240), oklch(98.4% 0.009 240)); 37 - --color-canvas-100: light-dark(oklch(25.9% 0.047 240), oklch(96.4% 0.020 240)); 37 + --color-canvas-100: light-dark(oklch(25.9% 0.047 240), oklch(96.4% 0.02 240)); 38 38 --color-canvas-200: light-dark(oklch(39.8% 0.082 240), oklch(92% 0.045 240)); 39 - --color-canvas-300: light-dark(oklch(52.5% 0.110 240), oklch(86% 0.072 240)); 39 + --color-canvas-300: light-dark(oklch(52.5% 0.11 240), oklch(86% 0.072 240)); 40 40 --color-canvas-400: light-dark(oklch(64.5% 0.138 240), oklch(80% 0.102 240)); 41 41 --color-canvas-500: light-dark(oklch(76% 0.165 240), oklch(76% 0.128 240)); 42 42 --color-canvas-600: light-dark(oklch(80% 0.102 240), oklch(64.5% 0.138 240)); 43 - --color-canvas-700: light-dark(oklch(86% 0.072 240), oklch(52.5% 0.110 240)); 43 + --color-canvas-700: light-dark(oklch(86% 0.072 240), oklch(52.5% 0.11 240)); 44 44 --color-canvas-800: light-dark(oklch(92% 0.045 240), oklch(39.8% 0.082 240)); 45 - --color-canvas-900: light-dark(oklch(96.4% 0.020 240), oklch(25.9% 0.047 240)); 45 + --color-canvas-900: light-dark(oklch(96.4% 0.02 240), oklch(25.9% 0.047 240)); 46 46 --color-canvas-950: light-dark(oklch(98.4% 0.009 240), oklch(17.9% 0.026 240)); 47 47 48 48 /* Secondary - Sky Blue (220°) */ 49 49 --color-secondary-50: light-dark(oklch(19% 0.037 220), oklch(97.8% 0.024 220)); 50 - --color-secondary-100: light-dark(oklch(27.5% 0.060 220), oklch(94.5% 0.046 220)); 50 + --color-secondary-100: light-dark(oklch(27.5% 0.06 220), oklch(94.5% 0.046 220)); 51 51 --color-secondary-200: light-dark(oklch(42.5% 0.102 220), oklch(89.5% 0.092 220)); 52 52 --color-secondary-300: light-dark(oklch(56.5% 0.138 220), oklch(80.5% 0.132 220)); 53 53 --color-secondary-400: light-dark(oklch(69.5% 0.172 220), oklch(70.5% 0.168 220)); ··· 55 55 --color-secondary-600: light-dark(oklch(84.5% 0.168 220), oklch(50.5% 0.172 220)); 56 56 --color-secondary-700: light-dark(oklch(88% 0.132 220), oklch(40.5% 0.138 220)); 57 57 --color-secondary-800: light-dark(oklch(91.8% 0.092 220), oklch(30.5% 0.102 220)); 58 - --color-secondary-900: light-dark(oklch(95.8% 0.046 220), oklch(22.5% 0.060 220)); 58 + --color-secondary-900: light-dark(oklch(95.8% 0.046 220), oklch(22.5% 0.06 220)); 59 59 --color-secondary-950: light-dark(oklch(98% 0.024 220), oklch(16% 0.037 220)); 60 60 61 61 /* Accent - Navy (255°) */ 62 - --color-accent-50: light-dark(oklch(19% 0.040 255), oklch(97.9% 0.027 255)); 63 - --color-accent-100: light-dark(oklch(27.5% 0.065 255), oklch(94.8% 0.050 255)); 64 - --color-accent-200: light-dark(oklch(42.5% 0.110 255), oklch(89.8% 0.098 255)); 62 + --color-accent-50: light-dark(oklch(19% 0.04 255), oklch(97.9% 0.027 255)); 63 + --color-accent-100: light-dark(oklch(27.5% 0.065 255), oklch(94.8% 0.05 255)); 64 + --color-accent-200: light-dark(oklch(42.5% 0.11 255), oklch(89.8% 0.098 255)); 65 65 --color-accent-300: light-dark(oklch(56.5% 0.148 255), oklch(81% 0.142 255)); 66 66 --color-accent-400: light-dark(oklch(69.5% 0.185 255), oklch(71.5% 0.178 255)); 67 - --color-accent-500: light-dark(oklch(81.5% 0.220 255), oklch(62% 0.220 255)); 67 + --color-accent-500: light-dark(oklch(81.5% 0.22 255), oklch(62% 0.22 255)); 68 68 --color-accent-600: light-dark(oklch(84.8% 0.178 255), oklch(51.5% 0.185 255)); 69 69 --color-accent-700: light-dark(oklch(88.2% 0.142 255), oklch(41.5% 0.148 255)); 70 - --color-accent-800: light-dark(oklch(92% 0.098 255), oklch(31.5% 0.110 255)); 71 - --color-accent-900: light-dark(oklch(96% 0.050 255), oklch(23% 0.065 255)); 72 - --color-accent-950: light-dark(oklch(98.2% 0.027 255), oklch(16.2% 0.040 255)); 70 + --color-accent-800: light-dark(oklch(92% 0.098 255), oklch(31.5% 0.11 255)); 71 + --color-accent-900: light-dark(oklch(96% 0.05 255), oklch(23% 0.065 255)); 72 + --color-accent-950: light-dark(oklch(98.2% 0.027 255), oklch(16.2% 0.04 255)); 73 73 }
+16 -16
src/lib/styles/themes/rose.css
··· 7 7 ============================================================================ */ 8 8 [data-color-theme='rose'] { 9 9 /* Primary - Rose (350°) */ 10 - --color-primary-50: light-dark(oklch(19.8% 0.045 350), oklch(98.2% 0.030 350)); 10 + --color-primary-50: light-dark(oklch(19.8% 0.045 350), oklch(98.2% 0.03 350)); 11 11 --color-primary-100: light-dark(oklch(28.8% 0.072 350), oklch(95.5% 0.055 350)); 12 12 --color-primary-200: light-dark(oklch(44.2% 0.118 350), oklch(90.5% 0.105 350)); 13 13 --color-primary-300: light-dark(oklch(58.5% 0.158 350), oklch(82.2% 0.152 350)); 14 14 --color-primary-400: light-dark(oklch(71.5% 0.195 350), oklch(73% 0.188 350)); 15 - --color-primary-500: light-dark(oklch(83.5% 0.230 350), oklch(63.5% 0.230 350)); 15 + --color-primary-500: light-dark(oklch(83.5% 0.23 350), oklch(63.5% 0.23 350)); 16 16 --color-primary-600: light-dark(oklch(86.2% 0.188 350), oklch(53% 0.195 350)); 17 17 --color-primary-700: light-dark(oklch(89.5% 0.152 350), oklch(43% 0.158 350)); 18 18 --color-primary-800: light-dark(oklch(92.8% 0.105 350), oklch(33% 0.118 350)); 19 19 --color-primary-900: light-dark(oklch(96.5% 0.055 350), oklch(24.5% 0.072 350)); 20 - --color-primary-950: light-dark(oklch(98.5% 0.030 350), oklch(17.2% 0.045 350)); 20 + --color-primary-950: light-dark(oklch(98.5% 0.03 350), oklch(17.2% 0.045 350)); 21 21 22 22 /* Ink - Pink-tinted text (350°) */ 23 - --color-ink-50: light-dark(oklch(18.2% 0.030 350), oklch(97.7% 0.020 350)); 24 - --color-ink-100: light-dark(oklch(26.2% 0.053 350), oklch(93.5% 0.040 350)); 25 - --color-ink-200: light-dark(oklch(39.8% 0.090 350), oklch(85.5% 0.075 350)); 26 - --color-ink-300: light-dark(oklch(51.8% 0.125 350), oklch(75.5% 0.110 350)); 23 + --color-ink-50: light-dark(oklch(18.2% 0.03 350), oklch(97.7% 0.02 350)); 24 + --color-ink-100: light-dark(oklch(26.2% 0.053 350), oklch(93.5% 0.04 350)); 25 + --color-ink-200: light-dark(oklch(39.8% 0.09 350), oklch(85.5% 0.075 350)); 26 + --color-ink-300: light-dark(oklch(51.8% 0.125 350), oklch(75.5% 0.11 350)); 27 27 --color-ink-400: light-dark(oklch(63.5% 0.158 350), oklch(65.5% 0.142 350)); 28 - --color-ink-500: light-dark(oklch(74.5% 0.190 350), oklch(55.5% 0.190 350)); 28 + --color-ink-500: light-dark(oklch(74.5% 0.19 350), oklch(55.5% 0.19 350)); 29 29 --color-ink-600: light-dark(oklch(79.2% 0.142 350), oklch(45.5% 0.158 350)); 30 - --color-ink-700: light-dark(oklch(84.2% 0.110 350), oklch(35.5% 0.125 350)); 31 - --color-ink-800: light-dark(oklch(89.6% 0.075 350), oklch(25.5% 0.090 350)); 32 - --color-ink-900: light-dark(oklch(94.9% 0.040 350), oklch(18.5% 0.053 350)); 33 - --color-ink-950: light-dark(oklch(97.7% 0.020 350), oklch(12.8% 0.030 350)); 30 + --color-ink-700: light-dark(oklch(84.2% 0.11 350), oklch(35.5% 0.125 350)); 31 + --color-ink-800: light-dark(oklch(89.6% 0.075 350), oklch(25.5% 0.09 350)); 32 + --color-ink-900: light-dark(oklch(94.9% 0.04 350), oklch(18.5% 0.053 350)); 33 + --color-ink-950: light-dark(oklch(97.7% 0.02 350), oklch(12.8% 0.03 350)); 34 34 35 35 /* Canvas - Pink-tinted backgrounds (350°) */ 36 36 --color-canvas-50: light-dark(oklch(18.4% 0.033 350), oklch(98.7% 0.012 350)); 37 37 --color-canvas-100: light-dark(oklch(26.4% 0.058 350), oklch(96.7% 0.026 350)); 38 - --color-canvas-200: light-dark(oklch(40.2% 0.100 350), oklch(92.8% 0.055 350)); 38 + --color-canvas-200: light-dark(oklch(40.2% 0.1 350), oklch(92.8% 0.055 350)); 39 39 --color-canvas-300: light-dark(oklch(53% 0.135 350), oklch(86.8% 0.088 350)); 40 40 --color-canvas-400: light-dark(oklch(65.2% 0.168 350), oklch(80.8% 0.122 350)); 41 41 --color-canvas-500: light-dark(oklch(76.8% 0.202 350), oklch(76.8% 0.155 350)); 42 42 --color-canvas-600: light-dark(oklch(80.8% 0.122 350), oklch(65.2% 0.168 350)); 43 43 --color-canvas-700: light-dark(oklch(86.8% 0.088 350), oklch(53% 0.135 350)); 44 - --color-canvas-800: light-dark(oklch(92.8% 0.055 350), oklch(40.2% 0.100 350)); 44 + --color-canvas-800: light-dark(oklch(92.8% 0.055 350), oklch(40.2% 0.1 350)); 45 45 --color-canvas-900: light-dark(oklch(96.7% 0.026 350), oklch(26.4% 0.058 350)); 46 46 --color-canvas-950: light-dark(oklch(98.7% 0.012 350), oklch(18.4% 0.033 350)); 47 47 ··· 62 62 --color-accent-50: light-dark(oklch(19.2% 0.043 5), oklch(97.9% 0.029 5)); 63 63 --color-accent-100: light-dark(oklch(27.8% 0.069 5), oklch(94.8% 0.053 5)); 64 64 --color-accent-200: light-dark(oklch(42.8% 0.118 5), oklch(89.8% 0.105 5)); 65 - --color-accent-300: light-dark(oklch(56.8% 0.158 5), oklch(81% 0.150 5)); 65 + --color-accent-300: light-dark(oklch(56.8% 0.158 5), oklch(81% 0.15 5)); 66 66 --color-accent-400: light-dark(oklch(69.8% 0.198 5), oklch(71.5% 0.188 5)); 67 67 --color-accent-500: light-dark(oklch(81.8% 0.235 5), oklch(62% 0.235 5)); 68 68 --color-accent-600: light-dark(oklch(84.8% 0.188 5), oklch(51.5% 0.198 5)); 69 - --color-accent-700: light-dark(oklch(88.2% 0.150 5), oklch(41.5% 0.158 5)); 69 + --color-accent-700: light-dark(oklch(88.2% 0.15 5), oklch(41.5% 0.158 5)); 70 70 --color-accent-800: light-dark(oklch(92% 0.105 5), oklch(31.5% 0.118 5)); 71 71 --color-accent-900: light-dark(oklch(96% 0.053 5), oklch(23% 0.069 5)); 72 72 --color-accent-950: light-dark(oklch(98.2% 0.029 5), oklch(16.2% 0.043 5));
+11 -11
src/lib/styles/themes/ruby.css
··· 46 46 --color-canvas-950: light-dark(oklch(98.5% 0.012 10), oklch(17.8% 0.032 10)); 47 47 48 48 /* Secondary - Orange-Red (30°) */ 49 - --color-secondary-50: light-dark(oklch(19.2% 0.040 30), oklch(97.9% 0.027 30)); 50 - --color-secondary-100: light-dark(oklch(27.8% 0.065 30), oklch(94.8% 0.050 30)); 51 - --color-secondary-200: light-dark(oklch(42.8% 0.110 30), oklch(89.8% 0.098 30)); 52 - --color-secondary-300: light-dark(oklch(56.8% 0.148 30), oklch(81% 0.140 30)); 49 + --color-secondary-50: light-dark(oklch(19.2% 0.04 30), oklch(97.9% 0.027 30)); 50 + --color-secondary-100: light-dark(oklch(27.8% 0.065 30), oklch(94.8% 0.05 30)); 51 + --color-secondary-200: light-dark(oklch(42.8% 0.11 30), oklch(89.8% 0.098 30)); 52 + --color-secondary-300: light-dark(oklch(56.8% 0.148 30), oklch(81% 0.14 30)); 53 53 --color-secondary-400: light-dark(oklch(69.8% 0.185 30), oklch(71.5% 0.178 30)); 54 - --color-secondary-500: light-dark(oklch(81.8% 0.220 30), oklch(62% 0.220 30)); 54 + --color-secondary-500: light-dark(oklch(81.8% 0.22 30), oklch(62% 0.22 30)); 55 55 --color-secondary-600: light-dark(oklch(84.8% 0.178 30), oklch(51.5% 0.185 30)); 56 - --color-secondary-700: light-dark(oklch(88.2% 0.140 30), oklch(41.5% 0.148 30)); 57 - --color-secondary-800: light-dark(oklch(92% 0.098 30), oklch(31.5% 0.110 30)); 58 - --color-secondary-900: light-dark(oklch(96% 0.050 30), oklch(23% 0.065 30)); 59 - --color-secondary-950: light-dark(oklch(98.2% 0.027 30), oklch(16.2% 0.040 30)); 56 + --color-secondary-700: light-dark(oklch(88.2% 0.14 30), oklch(41.5% 0.148 30)); 57 + --color-secondary-800: light-dark(oklch(92% 0.098 30), oklch(31.5% 0.11 30)); 58 + --color-secondary-900: light-dark(oklch(96% 0.05 30), oklch(23% 0.065 30)); 59 + --color-secondary-950: light-dark(oklch(98.2% 0.027 30), oklch(16.2% 0.04 30)); 60 60 61 61 /* Accent - Deep Crimson (355°) */ 62 - --color-accent-50: light-dark(oklch(19.5% 0.045 355), oklch(98% 0.030 355)); 62 + --color-accent-50: light-dark(oklch(19.5% 0.045 355), oklch(98% 0.03 355)); 63 63 --color-accent-100: light-dark(oklch(28.2% 0.072 355), oklch(95.2% 0.055 355)); 64 64 --color-accent-200: light-dark(oklch(43.5% 0.122 355), oklch(90.2% 0.108 355)); 65 65 --color-accent-300: light-dark(oklch(57.8% 0.165 355), oklch(82% 0.155 355)); ··· 69 69 --color-accent-700: light-dark(oklch(89.5% 0.155 355), oklch(42.5% 0.165 355)); 70 70 --color-accent-800: light-dark(oklch(92.8% 0.108 355), oklch(32.5% 0.122 355)); 71 71 --color-accent-900: light-dark(oklch(96.5% 0.055 355), oklch(24% 0.072 355)); 72 - --color-accent-950: light-dark(oklch(98.5% 0.030 355), oklch(17% 0.045 355)); 72 + --color-accent-950: light-dark(oklch(98.5% 0.03 355), oklch(17% 0.045 355)); 73 73 }
+8 -8
src/lib/styles/themes/slate.css
··· 8 8 [data-color-theme='slate'] { 9 9 /* Primary - Slate (230°) */ 10 10 --color-primary-50: light-dark(oklch(18.2% 0.018 230), oklch(97.8% 0.012 230)); 11 - --color-primary-100: light-dark(oklch(26.5% 0.030 230), oklch(94.8% 0.022 230)); 11 + --color-primary-100: light-dark(oklch(26.5% 0.03 230), oklch(94.8% 0.022 230)); 12 12 --color-primary-200: light-dark(oklch(40.5% 0.048 230), oklch(89.5% 0.042 230)); 13 13 --color-primary-300: light-dark(oklch(54% 0.065 230), oklch(79.5% 0.062 230)); 14 - --color-primary-400: light-dark(oklch(66.5% 0.080 230), oklch(69.5% 0.078 230)); 14 + --color-primary-400: light-dark(oklch(66.5% 0.08 230), oklch(69.5% 0.078 230)); 15 15 --color-primary-500: light-dark(oklch(78.5% 0.095 230), oklch(59.5% 0.095 230)); 16 - --color-primary-600: light-dark(oklch(82.2% 0.078 230), oklch(49.5% 0.080 230)); 16 + --color-primary-600: light-dark(oklch(82.2% 0.078 230), oklch(49.5% 0.08 230)); 17 17 --color-primary-700: light-dark(oklch(86.5% 0.062 230), oklch(39.5% 0.065 230)); 18 18 --color-primary-800: light-dark(oklch(91% 0.042 230), oklch(29.5% 0.048 230)); 19 - --color-primary-900: light-dark(oklch(95.8% 0.022 230), oklch(21.5% 0.030 230)); 19 + --color-primary-900: light-dark(oklch(95.8% 0.022 230), oklch(21.5% 0.03 230)); 20 20 --color-primary-950: light-dark(oklch(98% 0.012 230), oklch(15.2% 0.018 230)); 21 21 22 22 /* Ink - Slate-tinted text (230°) */ ··· 46 46 --color-canvas-950: light-dark(oklch(98.6% 0.005 230), oklch(17.8% 0.014 230)); 47 47 48 48 /* Secondary - Steel Grey (215°) */ 49 - --color-secondary-50: light-dark(oklch(18.5% 0.020 215), oklch(97.9% 0.013 215)); 49 + --color-secondary-50: light-dark(oklch(18.5% 0.02 215), oklch(97.9% 0.013 215)); 50 50 --color-secondary-100: light-dark(oklch(26.8% 0.033 215), oklch(95% 0.024 215)); 51 51 --color-secondary-200: light-dark(oklch(41% 0.052 215), oklch(89.8% 0.045 215)); 52 - --color-secondary-300: light-dark(oklch(54.5% 0.070 215), oklch(80.2% 0.065 215)); 52 + --color-secondary-300: light-dark(oklch(54.5% 0.07 215), oklch(80.2% 0.065 215)); 53 53 --color-secondary-400: light-dark(oklch(67% 0.087 215), oklch(70.2% 0.082 215)); 54 54 --color-secondary-500: light-dark(oklch(79% 0.103 215), oklch(60.2% 0.103 215)); 55 55 --color-secondary-600: light-dark(oklch(82.8% 0.082 215), oklch(50.2% 0.087 215)); 56 - --color-secondary-700: light-dark(oklch(87% 0.065 215), oklch(40.2% 0.070 215)); 56 + --color-secondary-700: light-dark(oklch(87% 0.065 215), oklch(40.2% 0.07 215)); 57 57 --color-secondary-800: light-dark(oklch(91.5% 0.045 215), oklch(30.5% 0.052 215)); 58 58 --color-secondary-900: light-dark(oklch(96% 0.024 215), oklch(22.2% 0.033 215)); 59 - --color-secondary-950: light-dark(oklch(98.2% 0.013 215), oklch(15.8% 0.020 215)); 59 + --color-secondary-950: light-dark(oklch(98.2% 0.013 215), oklch(15.8% 0.02 215)); 60 60 61 61 /* Accent - Charcoal (240°) */ 62 62 --color-accent-50: light-dark(oklch(18.5% 0.022 240), oklch(98% 0.014 240));
+4 -4
src/lib/styles/themes/sunset.css
··· 33 33 --color-ink-950: light-dark(oklch(97.5% 0.016 45), oklch(12% 0.025 45)); 34 34 35 35 /* Canvas - Orange-tinted backgrounds (45°) */ 36 - --color-canvas-50: light-dark(oklch(18% 0.028 45), oklch(98.5% 0.010 45)); 37 - --color-canvas-100: light-dark(oklch(26% 0.050 45), oklch(96.5% 0.022 45)); 36 + --color-canvas-50: light-dark(oklch(18% 0.028 45), oklch(98.5% 0.01 45)); 37 + --color-canvas-100: light-dark(oklch(26% 0.05 45), oklch(96.5% 0.022 45)); 38 38 --color-canvas-200: light-dark(oklch(39.8% 0.088 45), oklch(92% 0.048 45)); 39 39 --color-canvas-300: light-dark(oklch(52.5% 0.118 45), oklch(86% 0.078 45)); 40 40 --color-canvas-400: light-dark(oklch(64.5% 0.148 45), oklch(80% 0.108 45)); ··· 42 42 --color-canvas-600: light-dark(oklch(80% 0.108 45), oklch(64.5% 0.148 45)); 43 43 --color-canvas-700: light-dark(oklch(86% 0.078 45), oklch(52.5% 0.118 45)); 44 44 --color-canvas-800: light-dark(oklch(92% 0.048 45), oklch(39.8% 0.088 45)); 45 - --color-canvas-900: light-dark(oklch(96.5% 0.022 45), oklch(26% 0.050 45)); 46 - --color-canvas-950: light-dark(oklch(98.5% 0.010 45), oklch(18% 0.028 45)); 45 + --color-canvas-900: light-dark(oklch(96.5% 0.022 45), oklch(26% 0.05 45)); 46 + --color-canvas-950: light-dark(oklch(98.5% 0.01 45), oklch(18% 0.028 45)); 47 47 48 48 /* Secondary - Golden Yellow (75°) */ 49 49 --color-secondary-50: light-dark(oklch(19.5% 0.035 75), oklch(98% 0.023 75));
+11 -11
src/lib/styles/themes/teal.css
··· 33 33 --color-ink-950: light-dark(oklch(97.5% 0.016 195), oklch(12% 0.025 195)); 34 34 35 35 /* Canvas - Teal-tinted backgrounds (195°) */ 36 - --color-canvas-50: light-dark(oklch(18% 0.028 195), oklch(98.5% 0.010 195)); 37 - --color-canvas-100: light-dark(oklch(26% 0.050 195), oklch(96.5% 0.022 195)); 36 + --color-canvas-50: light-dark(oklch(18% 0.028 195), oklch(98.5% 0.01 195)); 37 + --color-canvas-100: light-dark(oklch(26% 0.05 195), oklch(96.5% 0.022 195)); 38 38 --color-canvas-200: light-dark(oklch(39.8% 0.088 195), oklch(92% 0.048 195)); 39 39 --color-canvas-300: light-dark(oklch(52.5% 0.118 195), oklch(86% 0.078 195)); 40 40 --color-canvas-400: light-dark(oklch(64.5% 0.148 195), oklch(80% 0.108 195)); ··· 42 42 --color-canvas-600: light-dark(oklch(80% 0.108 195), oklch(64.5% 0.148 195)); 43 43 --color-canvas-700: light-dark(oklch(86% 0.078 195), oklch(52.5% 0.118 195)); 44 44 --color-canvas-800: light-dark(oklch(92% 0.048 195), oklch(39.8% 0.088 195)); 45 - --color-canvas-900: light-dark(oklch(96.5% 0.022 195), oklch(26% 0.050 195)); 46 - --color-canvas-950: light-dark(oklch(98.5% 0.010 195), oklch(18% 0.028 195)); 45 + --color-canvas-900: light-dark(oklch(96.5% 0.022 195), oklch(26% 0.05 195)); 46 + --color-canvas-950: light-dark(oklch(98.5% 0.01 195), oklch(18% 0.028 195)); 47 47 48 48 /* Secondary - Aqua (180°) */ 49 49 --color-secondary-50: light-dark(oklch(19% 0.039 180), oklch(97.8% 0.026 180)); ··· 59 59 --color-secondary-950: light-dark(oklch(98% 0.026 180), oklch(16% 0.039 180)); 60 60 61 61 /* Accent - Deep Turquoise (210°) */ 62 - --color-accent-50: light-dark(oklch(19% 0.040 210), oklch(97.9% 0.027 210)); 63 - --color-accent-100: light-dark(oklch(27.5% 0.065 210), oklch(94.8% 0.050 210)); 64 - --color-accent-200: light-dark(oklch(42.5% 0.110 210), oklch(89.8% 0.098 210)); 62 + --color-accent-50: light-dark(oklch(19% 0.04 210), oklch(97.9% 0.027 210)); 63 + --color-accent-100: light-dark(oklch(27.5% 0.065 210), oklch(94.8% 0.05 210)); 64 + --color-accent-200: light-dark(oklch(42.5% 0.11 210), oklch(89.8% 0.098 210)); 65 65 --color-accent-300: light-dark(oklch(56.5% 0.148 210), oklch(81% 0.142 210)); 66 66 --color-accent-400: light-dark(oklch(69.5% 0.185 210), oklch(71.5% 0.178 210)); 67 - --color-accent-500: light-dark(oklch(81.5% 0.220 210), oklch(62% 0.220 210)); 67 + --color-accent-500: light-dark(oklch(81.5% 0.22 210), oklch(62% 0.22 210)); 68 68 --color-accent-600: light-dark(oklch(84.8% 0.178 210), oklch(51.5% 0.185 210)); 69 69 --color-accent-700: light-dark(oklch(88.2% 0.142 210), oklch(41.5% 0.148 210)); 70 - --color-accent-800: light-dark(oklch(92% 0.098 210), oklch(31.5% 0.110 210)); 71 - --color-accent-900: light-dark(oklch(96% 0.050 210), oklch(23% 0.065 210)); 72 - --color-accent-950: light-dark(oklch(98.2% 0.027 210), oklch(16.2% 0.040 210)); 70 + --color-accent-800: light-dark(oklch(92% 0.098 210), oklch(31.5% 0.11 210)); 71 + --color-accent-900: light-dark(oklch(96% 0.05 210), oklch(23% 0.065 210)); 72 + --color-accent-950: light-dark(oklch(98.2% 0.027 210), oklch(16.2% 0.04 210)); 73 73 }
+2 -2
src/routes/+layout.svelte
··· 86 86 <div 87 87 class="flex min-h-screen flex-col overflow-x-hidden bg-canvas-50 text-ink-900 dark:bg-canvas-950 dark:text-ink-50" 88 88 > 89 - <Header /> 89 + <Header profile={data.profile} /> 90 90 91 91 <main id="main-content" class="container mx-auto grow px-4 py-8" tabindex="-1"> 92 92 <ScrollToTop /> ··· 94 94 </main> 95 95 96 96 <Footer /> 97 - 97 + 98 98 <!-- Easter egg: Happy Mac walks across the screen (click version number 24 times!) --> 99 99 <HappyMacEasterEgg /> 100 100 </div>
+12 -8
src/routes/+layout.ts
··· 1 1 import type { LayoutLoad } from './$types'; 2 2 import { createSiteMeta, type SiteMetadata, defaultSiteMeta } from '$lib/helper/siteMeta'; 3 + import { fetchProfile } from '$lib/services/atproto'; 3 4 4 5 /** 5 - * Non-blocking layout load 6 - * Returns immediately with default site metadata 7 - * All data fetching happens client-side in components for faster initial page load 6 + * Layout load function - fetches profile data and provides base site metadata 8 7 */ 9 - export const load: LayoutLoad = async ({ url }) => { 8 + export const load: LayoutLoad = async ({ url, fetch }) => { 10 9 // Provide the default site metadata 11 10 const siteMeta: SiteMetadata = createSiteMeta({ 12 11 title: defaultSiteMeta.title, ··· 14 13 url: url.href // Include current URL for proper OG tags 15 14 }); 16 15 17 - // Return immediately - no blocking data fetches 18 - // Components will fetch their own data client-side with skeletons 16 + // Fetch profile data (needed by Header and page components) 17 + let profile = null; 18 + try { 19 + profile = await fetchProfile(fetch); 20 + } catch (error) { 21 + console.error('[Layout] Failed to load profile:', error); 22 + } 23 + 19 24 return { 20 25 siteMeta, 21 - profile: null, 22 - siteInfo: null 26 + profile 23 27 }; 24 28 };
+15 -16
src/routes/+page.svelte
··· 8 8 KibunStatusCard, 9 9 TangledRepoCard 10 10 } from '$lib/components/layout/main/card'; 11 - import { createSiteMeta, type SiteMetadata } from '$lib/helper/siteMeta'; 11 + import { createSiteMeta } from '$lib/helper/siteMeta'; 12 + import type { PageData } from './$types'; 12 13 13 - // The `data` object includes merged layout/page load data. 14 - // Give it a proper type so TS knows data.meta may exist. 15 - export let data: { siteMeta?: Partial<SiteMetadata>; meta?: Partial<SiteMetadata> }; 14 + let { data }: { data: PageData } = $props(); 16 15 17 - // Merge site defaults (if provided by layout) with page overrides. 18 - // This produces a complete SiteMetadata object we can safely read from. 19 - const meta: SiteMetadata = createSiteMeta({ 20 - ...(data.siteMeta ?? {}), 21 - ...(data.meta ?? {}) 22 - }); 16 + // Use $derived for reactive metadata 17 + const meta = $derived( 18 + createSiteMeta({ 19 + ...data.siteMeta 20 + }) 21 + ); 23 22 </script> 24 23 25 24 <div class="mx-auto max-w-6xl"> ··· 39 38 <!-- Masonry-style grid using Tailwind's column utilities --> 40 39 <div class="columns-1 gap-6 lg:columns-2"> 41 40 <div class="mb-6 break-inside-avoid"> 42 - <ProfileCard /> 41 + <ProfileCard profile={data.profile} /> 43 42 </div> 44 43 <div class="mb-6 break-inside-avoid"> 45 - <KibunStatusCard /> 44 + <KibunStatusCard kibunStatus={data.kibunStatus} /> 46 45 </div> 47 46 <div class="mb-6 break-inside-avoid"> 48 - <MusicStatusCard /> 47 + <MusicStatusCard musicStatus={data.musicStatus} /> 49 48 </div> 50 49 <div class="mb-6 break-inside-avoid"> 51 - <BlueskyPostCard /> 50 + <BlueskyPostCard post={data.latestPost} /> 52 51 </div> 53 52 <div class="mb-6 break-inside-avoid"> 54 53 <DynamicLinks /> 55 54 </div> 56 55 <div class="mb-6 break-inside-avoid"> 57 - <PostCard /> 56 + <PostCard blogPosts={data.blogPosts} /> 58 57 </div> 59 58 <div class="mb-6 break-inside-avoid"> 60 - <TangledRepoCard /> 59 + <TangledRepoCard repos={data.tangledRepos} profile={data.profile} /> 61 60 </div> 62 61 </div> 63 62 </div>
+33
src/routes/+page.ts
··· 1 + import type { PageLoad } from './$types'; 2 + import { 3 + fetchMusicStatus, 4 + fetchKibunStatus, 5 + fetchLatestBlueskyPost, 6 + fetchTangledRepos, 7 + fetchBlogPosts 8 + } from '$lib/services/atproto'; 9 + 10 + export const load: PageLoad = async ({ fetch, parent }) => { 11 + // Get parent data (includes profile from layout) 12 + const { profile } = await parent(); 13 + 14 + // Fetch page-specific data in parallel for better performance 15 + const [musicStatus, kibunStatus, latestPost, tangledRepos, blogPosts] = await Promise.allSettled([ 16 + fetchMusicStatus(fetch), 17 + fetchKibunStatus(fetch), 18 + fetchLatestBlueskyPost(fetch), 19 + fetchTangledRepos(fetch), 20 + fetchBlogPosts(fetch) 21 + ]); 22 + 23 + return { 24 + // Pass through profile from parent 25 + profile, 26 + // Page-specific data 27 + musicStatus: musicStatus.status === 'fulfilled' ? musicStatus.value : null, 28 + kibunStatus: kibunStatus.status === 'fulfilled' ? kibunStatus.value : null, 29 + latestPost: latestPost.status === 'fulfilled' ? latestPost.value : null, 30 + tangledRepos: tangledRepos.status === 'fulfilled' ? tangledRepos.value : null, 31 + blogPosts: blogPosts.status === 'fulfilled' ? blogPosts.value : null 32 + }; 33 + };
+5 -6
src/routes/[slug=slug]/[rkey]/+server.ts
··· 58 58 // Fetch publications to get the publication info 59 59 const { publications } = await fetchStandardSitePublications(); 60 60 let publication = null; 61 - 61 + 62 62 // Check if site points to a publication URI 63 63 if (documentSite?.startsWith('at://')) { 64 64 publication = publications.find((p) => p.uri === documentSite); 65 - 65 + 66 66 // Verify this document belongs to the requested publication 67 67 if (publication && publication.rkey !== publicationRkey) { 68 68 return { platform: 'unknown' }; ··· 79 79 url = `${basePath}${docPath.startsWith('/') ? docPath : '/' + docPath}`; 80 80 } else { 81 81 // Use the site value directly (it's a URL) 82 - const basePath = documentSite.endsWith('/') 83 - ? documentSite.slice(0, -1) 84 - : documentSite; 82 + const basePath = documentSite.endsWith('/') ? documentSite.slice(0, -1) : documentSite; 85 83 const docPath = value.path || `/${rkey}`; 86 84 url = `${basePath}${docPath.startsWith('/') ? docPath : '/' + docPath}`; 87 85 } ··· 250 248 const publicationNote = `\n\nNote: Only checking ${platformName} publication with rkey: ${publicationRkey}`; 251 249 const whiteWindNote = 252 250 PUBLIC_ENABLE_WHITEWIND === 'true' ? '\n- WhiteWind: https://whtwnd.com' : ''; 253 - const standardSiteNote = platform === 'standard.site' ? '\n- Standard.site: https://standard.site' : ''; 251 + const standardSiteNote = 252 + platform === 'standard.site' ? '\n- Standard.site: https://standard.site' : ''; 254 253 255 254 return new Response( 256 255 `Document not found: ${rkey}
+6 -6
svelte.config.js
··· 4 4 /** @type {import('@sveltejs/kit').Config} */ 5 5 const config = { 6 6 preprocess: vitePreprocess(), 7 - 7 + 8 8 kit: { 9 9 adapter: adapter({ 10 10 // Vercel adapter configuration 11 11 runtime: 'nodejs20.x', 12 12 regions: ['iad1'], // Default to US East (adjust based on your target audience) 13 13 split: false, // Set to true to deploy routes as individual functions 14 - 14 + 15 15 // Edge runtime configuration (uncomment to use Edge Functions) 16 16 // runtime: 'edge', 17 17 // regions: 'all', // Deploy to all edge regions 18 - 18 + 19 19 // Memory and execution limits 20 20 memory: 1024, // MB (256, 512, 1024, 3008) 21 21 maxDuration: 10 // seconds (max execution time) 22 22 }), 23 - 23 + 24 24 // Alias configuration for cleaner imports 25 25 alias: { 26 26 $components: 'src/lib/components', ··· 29 29 $services: 'src/lib/services', 30 30 $helper: 'src/lib/helper' 31 31 }, 32 - 32 + 33 33 // Prerender configuration 34 34 prerender: { 35 35 handleHttpError: 'warn', 36 36 handleMissingId: 'warn', 37 37 entries: ['*'] // Prerender all discoverable pages 38 38 }, 39 - 39 + 40 40 // CSP configuration for security 41 41 csp: { 42 42 mode: 'auto',
+4 -4
vite.config.ts
··· 4 4 5 5 export default defineConfig({ 6 6 plugins: [tailwindcss(), sveltekit()], 7 - 7 + 8 8 build: { 9 9 // Optimize chunk splitting for better caching 10 10 rollupOptions: { ··· 37 37 // Chunk size warnings 38 38 chunkSizeWarningLimit: 1000 39 39 }, 40 - 40 + 41 41 optimizeDeps: { 42 42 include: ['@lucide/svelte', 'hls.js', '@atproto/api'] 43 43 }, 44 - 44 + 45 45 server: { 46 46 // Development server configuration 47 47 fs: { 48 48 strict: true 49 49 } 50 50 }, 51 - 51 + 52 52 ssr: { 53 53 // Don't externalize these in SSR 54 54 noExternal: []