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.
This commit is contained in:
2026-06-03 14:16:59 +00:00
parent 41f93f5d8e
commit f42c673881
21 changed files with 1822 additions and 30 deletions

View File

@@ -5,6 +5,7 @@ import { Card } from '@/components/ui/card';
import { Label } from '@/components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { THEMES, setTheme, useTheme, type ThemeId, type ThemeMode } from '@/lib/theme';
import { useAnimBg, setAnimBg } from '@/lib/anim';
import { cn } from '@/lib/utils';
// v1.9: lifted out of pages/Settings.tsx so the SettingsPane Theme tab and
@@ -19,6 +20,7 @@ const MODES: { value: ThemeMode; label: string; hint: string }[] = [
export function ThemePicker() {
const { id: currentId, mode: currentMode } = useTheme();
const animOn = useAnimBg();
// Track the most recent in-flight pick so the picker can show a subtle
// "applying…" state on the targeted card while the PATCH is in flight.
const [pending, setPending] = useState<
@@ -70,6 +72,33 @@ export function ThemePicker() {
</RadioGroup>
</section>
<section className="space-y-3">
<div className="flex items-center justify-between gap-4">
<h2 className="text-sm font-medium">Animated background</h2>
<button
type="button"
role="switch"
aria-checked={animOn}
onClick={() => setAnimBg(!animOn)}
className={cn(
'relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent',
'transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
animOn ? 'bg-primary' : 'bg-input',
)}
>
<span
className={cn(
'pointer-events-none block h-4 w-4 rounded-full bg-white shadow-lg ring-0 transition-transform duration-200',
animOn ? 'translate-x-4' : 'translate-x-0',
)}
/>
</button>
</div>
<p className="text-xs text-muted-foreground">
Disables canvas animations and CSS effects for all themes. Persisted locally.
</p>
</section>
<section className="space-y-3">
<h2 className="text-sm font-medium">Theme</h2>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
@@ -77,6 +106,7 @@ export function ThemePicker() {
const isActive = t.id === currentId;
const isPending = pending?.kind === 'theme' && pending.id === t.id;
const isLightOnly = !t.supportsDark;
const isDarkOnly = !t.supportsLight;
return (
<Card
key={t.id}
@@ -112,6 +142,9 @@ export function ThemePicker() {
{isLightOnly && (
<div className="mt-2 text-xs text-muted-foreground italic">Light only</div>
)}
{isDarkOnly && (
<div className="mt-2 text-xs text-muted-foreground italic">Dark only</div>
)}
</Card>
);
})}