WIP! A BB-style forum, on the ATmosphere! We're still working... we'll be back soon when we have something to show off!
node typescript hono htmx atproto

feat: add resolveUserThemePreference to theme resolution waterfall

+32 -7
+32 -7
apps/web/src/lib/theme-resolution.ts
··· 67 67 return parts[4] ?? null; 68 68 } 69 69 70 + /** 71 + * Reads the user's theme preference cookie for the active color scheme and 72 + * validates it against the policy's availableThemes list. 73 + * Returns the validated AT URI, or null if absent, stale, or choice not allowed. 74 + */ 75 + export function resolveUserThemePreference( 76 + cookieHeader: string | undefined, 77 + colorScheme: "light" | "dark", 78 + availableThemes: { uri: string }[], 79 + allowUserChoice: boolean 80 + ): string | null { 81 + if (!allowUserChoice) return null; 82 + const cookieName = colorScheme === "light" ? "atbb-light-theme" : "atbb-dark-theme"; 83 + const match = cookieHeader?.match(new RegExp(`(?:^|;\\s*)${cookieName}=([^;]+)`)); 84 + if (!match) return null; 85 + const uri = match[1].trim(); 86 + return availableThemes.some((t) => t.uri === uri) ? uri : null; 87 + } 70 88 71 89 /** 72 90 * Resolves which theme to render for a request using the waterfall: 73 - * 1. User preference — not yet implemented (TODO: Theme Phase 4) 74 - * 2. Color scheme default — atbb-color-scheme cookie or Sec-CH hint 75 - * 3. Forum default — fetched from GET /api/theme-policy (cached in memory) 76 - * 4. Hardcoded fallback — neobrutal-light or neobrutal-dark per color scheme 91 + * 1. Detect color scheme — atbb-color-scheme cookie or Sec-CH hint 92 + * 2. Fetch theme policy — GET /api/theme-policy (cached in memory) 93 + * 3. User preference — atbb-light-theme / atbb-dark-theme cookie (validated against policy) 94 + * 4. Forum default — defaultLightThemeUri / defaultDarkThemeUri from policy 95 + * 5. Hardcoded fallback — neobrutal-light or neobrutal-dark per color scheme 77 96 * 78 97 * Pass a ThemeCache instance to enable in-memory TTL caching of policy and 79 98 * theme data. The cache is checked before each network request and populated ··· 88 107 cache?: ThemeCache 89 108 ): Promise<ResolvedTheme> { 90 109 const colorScheme = detectColorScheme(cookieHeader, colorSchemeHint); 91 - // TODO: user preference (Theme Phase 4) 92 110 93 111 // ── Step 1: Get theme policy (from cache or AppView) ─────────────────────── 94 112 let policy: CachedPolicy | null = cache?.getPolicy() ?? null; ··· 127 145 } 128 146 } 129 147 130 - // ── Step 2: Extract default theme URI and rkey ───────────────────────────── 148 + // ── Step 2: User preference or forum default URI ─────────────────────────── 149 + const userPreferenceUri = resolveUserThemePreference( 150 + cookieHeader, 151 + colorScheme, 152 + policy.availableThemes, 153 + policy.allowUserChoice 154 + ); 131 155 const defaultUri = 132 - colorScheme === "dark" ? policy.defaultDarkThemeUri : policy.defaultLightThemeUri; 156 + userPreferenceUri ?? 157 + (colorScheme === "dark" ? policy.defaultDarkThemeUri : policy.defaultLightThemeUri); 133 158 if (!defaultUri) return fallbackForScheme(colorScheme); 134 159 135 160 const rkey = parseRkeyFromUri(defaultUri);