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:
@@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user