feat: futuristic theme ladder + stacked landing banner

Add three opt-in dark themes (BooCode+, BooCode Classic, BooCode
Override) plus an in-place Ember polish, on a class-scoped effects
engine: matrix rain, a neon grid field, and frosted glass, all gated
by a localStorage "Animated background" toggle and prefers-reduced-
motion. Extend the server theme_id whitelist so the new ids persist,
and replace the Home landing wordmark with the stacked mascot +
wordmark banner.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-03 14:16:59 +00:00
parent d10d79399b
commit fc4fbb0b7e
21 changed files with 1822 additions and 30 deletions

View File

@@ -1,11 +1,13 @@
import { useEffect, useState } from 'react';
import { api } from '@/api/client';
// themes-v1: source of truth for the 18 presets. id and name are surfaced in
// the picker; family groups visually; supportsDark/supportsLight reflect
// whether the corresponding selector exists in styles/themes/<id>.css; anchors
// are the 5 dark swatches (or the light palette for the two light-only themes)
// used in the picker preview strip.
// themes-v1: source of truth for the 19 presets + 3 futuristic additions.
// id and name are surfaced in the picker; family groups visually;
// supportsDark/supportsLight reflect whether the corresponding selector exists
// in styles/themes/<id>.css; anchors are the 5 dark swatches (or the light
// palette for the two light-only themes) used in the picker preview strip.
// Dark-only themes (supportsLight:false) always render with the dark class —
// see applyTheme force-dark logic below.
export type ThemeId =
| 'obsidian'
| 'gunmetal'
@@ -25,7 +27,10 @@ export type ThemeId =
| 'chalk'
| 'cobalt'
| 'midnight-sapphire'
| 'ember';
| 'ember'
| 'boocode-plus'
| 'boocode-classic'
| 'boocode-override';
export type ThemeMode = 'dark' | 'light' | 'system';
@@ -75,8 +80,16 @@ export const THEMES: readonly ThemeMeta[] = [
anchors: ['#020817', '#061434', '#0c2244', '#3060a0', '#0047ab'] },
{ id: 'midnight-sapphire', name: 'Midnight Sapphire', family: 'Blue', supportsDark: true, supportsLight: true,
anchors: ['#02050e', '#060c1f', '#0e1a36', '#4a6088', '#1e3a8a'] },
{ id: 'ember', name: 'BooCode Ember', family: 'Amber', supportsDark: true, supportsLight: true,
{ id: 'ember', name: 'BooCode', family: 'Amber', supportsDark: true, supportsLight: true,
anchors: ['#0c0c0e', '#15151a', '#1f1f23', '#6b6b75', '#ff7a18'] },
// Futuristic ladder — Phase 1 registrations (final anchors + flags).
// Token stylesheets and effects live in styles/themes/boocode-*.css.
{ id: 'boocode-plus', name: 'BooCode+', family: 'Futuristic', supportsDark: true, supportsLight: true,
anchors: ['#0f1117', '#1a1d2e', '#242838', '#7a7f99', '#5e6ad2'] },
{ id: 'boocode-classic', name: 'BooCode Classic', family: 'Futuristic', supportsDark: true, supportsLight: false,
anchors: ['#0a0604', '#120a06', '#1a0e08', '#9a7a5a', '#f97316'] },
{ id: 'boocode-override', name: 'BooCode Override', family: 'Futuristic', supportsDark: true, supportsLight: false,
anchors: ['#080b14', '#0d1120', '#0f1525', '#7a9dc2', '#ff2d78'] },
] as const;
// BooCode 2.0: orange-on-black "BooCode Ember" is the out-of-the-box signature
@@ -112,8 +125,12 @@ export function applyTheme(id: ThemeId, mode: ThemeMode): void {
if (typeof document === 'undefined') return;
const resolved = resolvedMode(mode);
const effective = effectiveThemeId(id, resolved);
// Dark-only themes (supportsLight:false) always get the dark class so
// dark: utilities and .dark selectors render correctly under any mode pref.
const meta = THEMES.find((t) => t.id === effective);
const isDark = resolved === 'dark' || (meta !== undefined && !meta.supportsLight);
document.documentElement.className =
`theme-${effective}${resolved === 'dark' ? ' dark' : ''}`;
`theme-${effective}${isDark ? ' dark' : ''}`;
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify({ id, mode }));
} catch {