feat(seasonal): redesign all 11 seasonal themes as modular per-theme overlays
CI / Build & Quality Checks (push) Successful in 11m7s
CI / Trigger Desktop Build (push) Successful in 12s

Split the 808-line SeasonalEffect monolith into one self-contained module per
theme under seasonal/themes/ (<Theme>.tsx + <Theme>.css.ts), and gave every
theme a premium, research-backed redesign (one Opus agent per theme against a
shared brief). SeasonalEffect now just imports the 11 overlays and dispatches;
the orphaned shared Seasonal.css.ts is removed (each theme owns its keyframes).

Each overlay: layered oklch palettes, GPU-only animation (transform/opacity),
`contain: layout paint style` to kill repaint flicker, ≤~40-element perf budget,
particles seeded once via useMemo (no per-frame state), a gorgeous STATIC
prefers-reduced-motion form (the settings preview thumbnail), WCAG-AA-preserving
low opacities, and no new deps / no external assets (inline SVG data-URIs,
Tauri/CSP-safe).

Themes: Halloween, Christmas, New Year, Autumn, April Fools, Lunar New Year,
Valentines, St. Patrick's, Earth Day, Deep Space, Arcade.

Gates: tsc clean, ESLint clean, Prettier clean, build OK, 551 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 19:41:58 -04:00
co-authored by Claude Opus 4.8
parent f816049fdf
commit 26f998d243
24 changed files with 4705 additions and 806 deletions
@@ -0,0 +1,364 @@
import React, { useMemo } from 'react';
import { SeasonalOverlayProps } from '../types';
import {
animCosmosDrift,
animNebulaA,
animNebulaB,
animGalaxySpin,
animTwinkle,
animStarPulse,
animParallaxNear,
animParallaxFar,
animComet,
} from './DeepSpace.css';
/**
* Deep Space overlay — a cosmic, awe-inspiring ambient mode. Layered oklch
* radial gradients build a deep violet void seeded with drifting magenta/cyan
* nebula clouds and a faint distant galaxy spiral. A parallax starfield sits at
* two depths (a dense field of tiny twinkling stars plus a handful of brighter
* hero stars), and slow comet streaks cross the sky occasionally.
*
* Palette (oklch): deep cosmic violet oklch(0.2 0.12 300), nebula magenta
* oklch(0.55 0.2 330), cyan oklch(0.75 0.13 200), starlight white
* oklch(0.98 0.02 280).
*
* RENDERING CONTRACT: the parent supplies a fixed inset:0 overflow:hidden
* pointer-events:none container at the right z-index. We only return
* absolutely-positioned aria-hidden children at low opacity — no z-index,
* position:fixed, or pointer-events here — kept well below opaque so chat text
* stays WCAG-AA legible.
*
* REDUCED MOTION: when `reduced`, render a static but gorgeous scene (a still
* nebula, a static starfield, a frozen galaxy and one frozen comet streak) with
* no `animation` at all. The settings preview always passes reduced=true.
*/
const STAR_TINTS = [
'oklch(0.98 0.02 280)', // starlight white
'oklch(0.9 0.07 230)', // cool cyan-white
'oklch(0.88 0.08 330)', // faint magenta-white
] as const;
const HERO_TINTS = [
'oklch(0.92 0.06 200)', // cyan starlight
'oklch(0.9 0.09 330)', // magenta starlight
'oklch(0.98 0.02 280)', // pure starlight
] as const;
type Star = {
top: number;
left: number;
size: number;
color: string;
duration: number;
delay: number;
staticOpacity: number;
};
type HeroStar = Star;
type Comet = {
top: number;
left: number;
length: number;
angle: number;
color: string;
duration: number;
delay: number;
};
// Deterministic pseudo-random so the memoized scene is stable across renders.
const rand = (seed: number) => {
const x = Math.sin(seed * 12.9898 + 78.233) * 43758.5453;
return x - Math.floor(x);
};
// A four-point gleam (sparkle) as an inline SVG data-URI — CSP-safe, no assets.
const gleamUri = (color: string) =>
`url("data:image/svg+xml,${encodeURIComponent(
`<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M12 0 C12.6 7.4 16.6 11.4 24 12 C16.6 12.6 12.6 16.6 12 24 C11.4 16.6 7.4 12.6 0 12 C7.4 11.4 11.4 7.4 12 0 Z' fill='${color}'/></svg>`,
)}")`;
function makeStars(count: number, seedBase: number): Star[] {
return Array.from({ length: count }, (_, i) => {
const s = seedBase + i;
return {
top: rand(s + 1) * 100,
left: rand(s + 101) * 100,
size: 1 + Math.floor(rand(s + 201) * 2), // 1–2px tiny stars
color: STAR_TINTS[i % STAR_TINTS.length],
duration: 2.6 + rand(s + 301) * 3.4,
delay: rand(s + 401) * 5,
staticOpacity: 0.4 + rand(s + 501) * 0.55,
};
});
}
export function DeepSpaceOverlay({ reduced }: SeasonalOverlayProps) {
// Two parallax depths. Far = dense + faint, Near = sparser + slightly larger.
const farStars = useMemo<Star[]>(() => makeStars(16, 1000), []);
const nearStars = useMemo<Star[]>(() => makeStars(12, 2000), []);
const heroStars = useMemo<HeroStar[]>(
() =>
Array.from({ length: 6 }, (_, i) => {
const s = 3000 + i;
return {
top: 6 + rand(s + 1) * 78,
left: 6 + rand(s + 101) * 88,
size: 9 + Math.floor(rand(s + 201) * 9), // 9–17px gleams
color: HERO_TINTS[i % HERO_TINTS.length],
duration: 4 + rand(s + 301) * 4,
delay: rand(s + 401) * 5,
staticOpacity: 0.85,
};
}),
[],
);
const comets = useMemo<Comet[]>(
() =>
Array.from({ length: 3 }, (_, i) => {
const s = 4000 + i;
return {
top: 8 + rand(s + 1) * 44,
left: -10 + rand(s + 101) * 30,
length: 120 + Math.floor(rand(s + 201) * 120),
angle: 18 + rand(s + 301) * 16, // gentle downward diagonal
color: i % 2 === 0 ? 'oklch(0.92 0.06 200)' : 'oklch(0.9 0.09 330)',
duration: 7 + rand(s + 401) * 5,
delay: 2 + i * 6 + rand(s + 501) * 4,
};
}),
[],
);
return (
<>
{/* Deep cosmic void — layered oklch radial gradients for depth. A barely
perceptible drift gives the whole field life without distraction. */}
<div
aria-hidden="true"
style={{
position: 'absolute',
inset: '-6%',
contain: 'layout paint style',
backgroundColor: 'oklch(0.2 0.12 300 / 0.16)',
backgroundImage: [
'radial-gradient(120% 90% at 50% -8%, oklch(0.28 0.13 295 / 0.2) 0%, transparent 60%)',
'radial-gradient(100% 80% at 12% 18%, oklch(0.55 0.2 330 / 0.1) 0%, transparent 55%)',
'radial-gradient(100% 80% at 88% 28%, oklch(0.75 0.13 200 / 0.08) 0%, transparent 55%)',
'radial-gradient(150% 130% at 50% 118%, oklch(0.18 0.1 300 / 0.22) 0%, transparent 70%)',
].join(','),
animation: reduced ? 'none' : `${animCosmosDrift} 26s ease-in-out infinite`,
}}
/>
{/* Drifting nebula clouds — blurred radial gradients in magenta + cyan. */}
<div
aria-hidden="true"
style={{
position: 'absolute',
top: '-12%',
left: '-14%',
width: '70%',
height: '70%',
contain: 'layout paint style',
filter: 'blur(42px)',
background:
'radial-gradient(closest-side, oklch(0.55 0.2 330 / 0.16) 0%, oklch(0.45 0.18 320 / 0.06) 45%, transparent 72%)',
willChange: reduced ? undefined : 'transform',
animation: reduced ? 'none' : `${animNebulaA} 34s ease-in-out infinite`,
}}
/>
<div
aria-hidden="true"
style={{
position: 'absolute',
bottom: '-16%',
right: '-12%',
width: '74%',
height: '74%',
contain: 'layout paint style',
filter: 'blur(46px)',
background:
'radial-gradient(closest-side, oklch(0.75 0.13 200 / 0.13) 0%, oklch(0.6 0.14 240 / 0.05) 48%, transparent 74%)',
willChange: reduced ? undefined : 'transform',
animation: reduced ? 'none' : `${animNebulaB} 40s ease-in-out infinite`,
}}
/>
{/* A third, central violet wash to bind the two color clouds together. */}
<div
aria-hidden="true"
style={{
position: 'absolute',
top: '20%',
left: '28%',
width: '50%',
height: '50%',
contain: 'layout paint style',
filter: 'blur(50px)',
background:
'radial-gradient(closest-side, oklch(0.35 0.16 305 / 0.12) 0%, transparent 70%)',
willChange: reduced ? undefined : 'transform',
animation: reduced ? 'none' : `${animNebulaA} 46s ease-in-out infinite reverse`,
}}
/>
{/* Faint distant galaxy spiral — an inline conic-ish swirl from layered
radial gradients, blurred and very slowly rotating. */}
<div
aria-hidden="true"
style={{
position: 'absolute',
top: '12%',
right: '14%',
width: '180px',
height: '180px',
contain: 'layout paint style',
borderRadius: '50%',
filter: 'blur(6px)',
opacity: reduced ? 0.5 : 0.6,
background: [
'radial-gradient(closest-side, oklch(0.95 0.04 280 / 0.35) 0%, oklch(0.7 0.16 320 / 0.12) 22%, transparent 40%)',
'conic-gradient(from 0deg, transparent 0deg, oklch(0.7 0.16 320 / 0.16) 60deg, transparent 130deg, oklch(0.75 0.13 200 / 0.12) 230deg, transparent 300deg)',
].join(','),
maskImage: 'radial-gradient(closest-side, #000 0%, #000 55%, transparent 80%)',
WebkitMaskImage: 'radial-gradient(closest-side, #000 0%, #000 55%, transparent 80%)',
willChange: reduced ? undefined : 'transform',
transform: reduced ? 'rotate(28deg)' : undefined,
animation: reduced ? 'none' : `${animGalaxySpin} 120s linear infinite`,
}}
/>
{/* Far parallax starfield — dense, faint, tiny twinkling stars. */}
<div
aria-hidden="true"
style={{
position: 'absolute',
inset: 0,
contain: 'layout paint style',
willChange: reduced ? undefined : 'transform',
animation: reduced ? 'none' : `${animParallaxFar} 60s ease-in-out infinite alternate`,
}}
>
{farStars.map((s, i) => (
<div
key={`f${i}`}
aria-hidden="true"
style={{
position: 'absolute',
top: `${s.top}%`,
left: `${s.left}%`,
width: `${s.size}px`,
height: `${s.size}px`,
borderRadius: '50%',
backgroundColor: s.color,
boxShadow: `0 0 ${s.size * 2}px ${s.color}`,
opacity: reduced ? s.staticOpacity : undefined,
transform: reduced ? 'scale(0.95)' : undefined,
animation: reduced
? 'none'
: `${animTwinkle} ${s.duration}s ease-in-out ${s.delay}s infinite`,
}}
/>
))}
</div>
{/* Near parallax starfield — sparser, brighter, drifts a touch faster. */}
<div
aria-hidden="true"
style={{
position: 'absolute',
inset: 0,
contain: 'layout paint style',
willChange: reduced ? undefined : 'transform',
animation: reduced ? 'none' : `${animParallaxNear} 48s ease-in-out infinite alternate`,
}}
>
{nearStars.map((s, i) => (
<div
key={`n${i}`}
aria-hidden="true"
style={{
position: 'absolute',
top: `${s.top}%`,
left: `${s.left}%`,
width: `${s.size + 1}px`,
height: `${s.size + 1}px`,
borderRadius: '50%',
backgroundColor: s.color,
boxShadow: `0 0 ${(s.size + 1) * 2.5}px ${s.color}`,
opacity: reduced ? Math.min(1, s.staticOpacity + 0.15) : undefined,
transform: reduced ? 'scale(1)' : undefined,
animation: reduced
? 'none'
: `${animTwinkle} ${s.duration * 0.85}s ease-in-out ${s.delay}s infinite`,
}}
/>
))}
</div>
{/* Hero stars — a few bright four-point gleams that pulse slowly. */}
{heroStars.map((s, i) => (
<div
key={`h${i}`}
aria-hidden="true"
style={{
position: 'absolute',
top: `${s.top}%`,
left: `${s.left}%`,
width: `${s.size}px`,
height: `${s.size}px`,
backgroundImage: gleamUri(s.color),
backgroundRepeat: 'no-repeat',
backgroundSize: 'contain',
filter: `drop-shadow(0 0 4px ${s.color})`,
opacity: reduced ? s.staticOpacity : undefined,
transform: reduced ? 'scale(1) rotate(20deg)' : undefined,
animation: reduced
? 'none'
: `${animStarPulse} ${s.duration}s ease-in-out ${s.delay}s infinite`,
}}
/>
))}
{/* Comet / warp streaks. In reduced mode, freeze a single streak mid-flight
so the static thumbnail reads as a living cosmos. */}
{(reduced ? comets.slice(0, 1) : comets).map((c, i) => (
<div
key={`c${i}`}
aria-hidden="true"
style={{
position: 'absolute',
top: `${c.top}%`,
left: `${c.left}%`,
width: `${c.length}px`,
height: '2px',
transformOrigin: '0 50%',
// Outer wrapper holds the rotation; inner element does the travel so
// the streak always moves along its own length axis.
transform: `rotate(${c.angle}deg)`,
}}
>
<div
style={{
position: 'absolute',
inset: 0,
borderRadius: '2px',
background: `linear-gradient(90deg, transparent 0%, ${c.color} 80%, oklch(0.98 0.02 280 / 0.95) 100%)`,
boxShadow: `0 0 6px ${c.color}`,
willChange: reduced ? undefined : 'transform, opacity',
opacity: reduced ? 0.85 : undefined,
transform: reduced ? 'translate3d(46%, 0, 0)' : undefined,
animation: reduced
? 'none'
: `${animComet} ${c.duration}s ease-in ${c.delay}s infinite`,
}}
/>
</div>
))}
</>
);
}