From 26f998d24396b3d826b25b40c27e2aee706f4244 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Tue, 30 Jun 2026 19:41:58 -0400 Subject: [PATCH] feat(seasonal): redesign all 11 seasonal themes as modular per-theme overlays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split the 808-line SeasonalEffect monolith into one self-contained module per theme under seasonal/themes/ (.tsx + .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 --- src/app/components/seasonal/Seasonal.css.ts | 136 ---- .../components/seasonal/SeasonalEffect.tsx | 681 +----------------- .../seasonal/themes/AprilFools.css.ts | 71 ++ .../components/seasonal/themes/AprilFools.tsx | 409 +++++++++++ .../components/seasonal/themes/Arcade.css.ts | 121 ++++ src/app/components/seasonal/themes/Arcade.tsx | 382 ++++++++++ .../components/seasonal/themes/Autumn.css.ts | 91 +++ src/app/components/seasonal/themes/Autumn.tsx | 310 ++++++++ .../seasonal/themes/Christmas.css.ts | 56 ++ .../components/seasonal/themes/Christmas.tsx | 256 +++++++ .../seasonal/themes/DeepSpace.css.ts | 73 ++ .../components/seasonal/themes/DeepSpace.tsx | 364 ++++++++++ .../seasonal/themes/EarthDay.css.ts | 70 ++ .../components/seasonal/themes/EarthDay.tsx | 319 ++++++++ .../seasonal/themes/Halloween.css.ts | 56 ++ .../components/seasonal/themes/Halloween.tsx | 267 +++++++ .../seasonal/themes/LunarNewYear.css.ts | 109 +++ .../seasonal/themes/LunarNewYear.tsx | 483 +++++++++++++ .../components/seasonal/themes/NewYear.css.ts | 87 +++ .../components/seasonal/themes/NewYear.tsx | 303 ++++++++ .../seasonal/themes/StPatricks.css.ts | 67 ++ .../components/seasonal/themes/StPatricks.tsx | 325 +++++++++ .../seasonal/themes/Valentines.css.ts | 70 ++ .../components/seasonal/themes/Valentines.tsx | 405 +++++++++++ 24 files changed, 4705 insertions(+), 806 deletions(-) delete mode 100644 src/app/components/seasonal/Seasonal.css.ts create mode 100644 src/app/components/seasonal/themes/AprilFools.css.ts create mode 100644 src/app/components/seasonal/themes/AprilFools.tsx create mode 100644 src/app/components/seasonal/themes/Arcade.css.ts create mode 100644 src/app/components/seasonal/themes/Arcade.tsx create mode 100644 src/app/components/seasonal/themes/Autumn.css.ts create mode 100644 src/app/components/seasonal/themes/Autumn.tsx create mode 100644 src/app/components/seasonal/themes/Christmas.css.ts create mode 100644 src/app/components/seasonal/themes/Christmas.tsx create mode 100644 src/app/components/seasonal/themes/DeepSpace.css.ts create mode 100644 src/app/components/seasonal/themes/DeepSpace.tsx create mode 100644 src/app/components/seasonal/themes/EarthDay.css.ts create mode 100644 src/app/components/seasonal/themes/EarthDay.tsx create mode 100644 src/app/components/seasonal/themes/Halloween.css.ts create mode 100644 src/app/components/seasonal/themes/Halloween.tsx create mode 100644 src/app/components/seasonal/themes/LunarNewYear.css.ts create mode 100644 src/app/components/seasonal/themes/LunarNewYear.tsx create mode 100644 src/app/components/seasonal/themes/NewYear.css.ts create mode 100644 src/app/components/seasonal/themes/NewYear.tsx create mode 100644 src/app/components/seasonal/themes/StPatricks.css.ts create mode 100644 src/app/components/seasonal/themes/StPatricks.tsx create mode 100644 src/app/components/seasonal/themes/Valentines.css.ts create mode 100644 src/app/components/seasonal/themes/Valentines.tsx diff --git a/src/app/components/seasonal/Seasonal.css.ts b/src/app/components/seasonal/Seasonal.css.ts deleted file mode 100644 index bf897133e..000000000 --- a/src/app/components/seasonal/Seasonal.css.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { keyframes } from '@vanilla-extract/css'; - -/** Generic fall: particles drop from top to bottom with a slight rotate. */ -export const animSeasonFall = keyframes({ - '0%': { transform: 'translateY(-20px) translateX(0) rotate(0deg)', opacity: '0' }, - '5%': { opacity: '1' }, - '90%': { opacity: '0.8' }, - '100%': { transform: 'translateY(110vh) translateX(25px) rotate(360deg)', opacity: '0' }, -}); - -/** Leaf fall: exaggerated horizontal sway as the leaf tumbles down. */ -export const animLeafFall = keyframes({ - '0%': { transform: 'translateY(-20px) translateX(0) rotate(-20deg)', opacity: '0' }, - '8%': { opacity: '0.85' }, - '25%': { transform: 'translateY(25vh) translateX(35px) rotate(40deg)' }, - '50%': { transform: 'translateY(50vh) translateX(-25px) rotate(130deg)' }, - '75%': { transform: 'translateY(75vh) translateX(45px) rotate(260deg)' }, - '92%': { opacity: '0.6' }, - '100%': { transform: 'translateY(110vh) translateX(5px) rotate(380deg)', opacity: '0' }, -}); - -/** Float up: hearts / embers rise from the bottom. */ -export const animFloatUp = keyframes({ - '0%': { transform: 'translateY(0) scale(0.6) translateX(0)', opacity: '0' }, - '8%': { opacity: '0.9' }, - '50%': { transform: 'translateY(-50vh) scale(1) translateX(15px)' }, - '85%': { opacity: '0.4' }, - '100%': { transform: 'translateY(-105vh) scale(1.3) translateX(-10px)', opacity: '0' }, -}); - -/** Bob: lanterns gently rise and fall with a slight tilt. */ -export const animBob = keyframes({ - '0%': { transform: 'translateY(0px) rotate(-4deg)' }, - '50%': { transform: 'translateY(-18px) rotate(4deg)' }, - '100%': { transform: 'translateY(0px) rotate(-4deg)' }, -}); - -/** Lantern tassel sway (used on the tassel element only). */ -export const animTasselSway = keyframes({ - '0%': { transform: 'rotate(-8deg)' }, - '50%': { transform: 'rotate(8deg)' }, - '100%': { transform: 'rotate(-8deg)' }, -}); - -/** Glitch jitter: rapid position jumps that feel like a signal error. */ -export const animGlitch = keyframes({ - '0%': { transform: 'translate(0, 0)' }, - '2%': { transform: 'translate(-4px, 2px)' }, - '4%': { transform: 'translate(4px, -2px)' }, - '6%': { transform: 'translate(0, 0)' }, - '48%': { transform: 'translate(0, 0)' }, - '50%': { transform: 'translate(3px, -3px)' }, - '52%': { transform: 'translate(-3px, 3px)' }, - '54%': { transform: 'translate(0, 0)' }, - '78%': { transform: 'translate(0, 0)' }, - '80%': { transform: 'translate(-5px, 1px)' }, - '82%': { transform: 'translate(0, 0)' }, - '100%': { transform: 'translate(0, 0)' }, -}); - -/** Glitch color: hue + saturation spikes that look like a corrupted signal. */ -export const animGlitchColor = keyframes({ - '0%': { filter: 'hue-rotate(0deg) saturate(1)' }, - '8%': { filter: 'hue-rotate(180deg) saturate(3)' }, - '9%': { filter: 'hue-rotate(0deg) saturate(1)' }, - '55%': { filter: 'hue-rotate(0deg) saturate(1)' }, - '57%': { filter: 'hue-rotate(90deg) saturate(2)' }, - '58%': { filter: 'hue-rotate(0deg) saturate(1)' }, - '80%': { filter: 'hue-rotate(0deg) saturate(1)' }, - '82%': { filter: 'hue-rotate(270deg) saturate(2.5)' }, - '83%': { filter: 'hue-rotate(0deg) saturate(1)' }, - '100%': { filter: 'hue-rotate(0deg) saturate(1)' }, -}); - -/** Glitch scanline: a horizontal band sweeps across, flickering. */ -export const animGlitchScan = keyframes({ - '0%': { transform: 'translateY(-100%)' }, - '100%': { transform: 'translateY(100vh)' }, -}); - -/** Burst: circle expands outward from a point and fades — firework petal. */ -export const animBurst = keyframes({ - '0%': { transform: 'scale(0) rotate(0deg)', opacity: '1' }, - '50%': { opacity: '0.7' }, - '100%': { transform: 'scale(1) rotate(45deg)', opacity: '0' }, -}); - -/** Firework trail: a small dot rockets upward before bursting. */ -export const animRocket = keyframes({ - '0%': { transform: 'translateY(0)', opacity: '1' }, - '100%': { transform: 'translateY(-40vh)', opacity: '0' }, -}); - -/** Deep space warp: stars streak from center outward. */ -export const animWarp = keyframes({ - '0%': { transform: 'scale(0.05) translate(0, 0)', opacity: '0' }, - '10%': { opacity: '1' }, - '100%': { transform: 'scale(4) translate(0, 0)', opacity: '0' }, -}); - -/** Arcade scanline flicker. */ -export const animScanline = keyframes({ - '0%': { opacity: '0.12' }, - '50%': { opacity: '0.04' }, - '100%': { opacity: '0.12' }, -}); - -/** Arcade pixel blink: decorative corner glyphs blink. */ -export const animPixelBlink = keyframes({ - '0%, 49%': { opacity: '1' }, - '50%, 100%': { opacity: '0' }, -}); - -/** Gold shimmer: a shine sweeps across a metallic surface. */ -export const animGoldShimmer = keyframes({ - '0%': { backgroundPosition: '-300% 0' }, - '100%': { backgroundPosition: '300% 0' }, -}); - -/** Clover drift: gentle fall with a slow spin. */ -export const animCloverDrift = keyframes({ - '0%': { transform: 'translateY(-20px) rotate(0deg)', opacity: '0' }, - '5%': { opacity: '0.7' }, - '90%': { opacity: '0.5' }, - '100%': { transform: 'translateY(110vh) rotate(720deg)', opacity: '0' }, -}); - -/** Earth Day leaf sway: gentle horizontal oscillation for ambient leaf particles. */ -export const animEarthLeafDrift = keyframes({ - '0%': { transform: 'translateY(-10px) translateX(0) rotate(0deg)', opacity: '0' }, - '8%': { opacity: '0.6' }, - '30%': { transform: 'translateY(30vh) translateX(20px) rotate(90deg)' }, - '60%': { transform: 'translateY(60vh) translateX(-15px) rotate(200deg)' }, - '90%': { opacity: '0.4' }, - '100%': { transform: 'translateY(110vh) translateX(10px) rotate(340deg)', opacity: '0' }, -}); diff --git a/src/app/components/seasonal/SeasonalEffect.tsx b/src/app/components/seasonal/SeasonalEffect.tsx index 7a968c870..a49c76278 100644 --- a/src/app/components/seasonal/SeasonalEffect.tsx +++ b/src/app/components/seasonal/SeasonalEffect.tsx @@ -4,682 +4,23 @@ import { settingsAtom } from '../../state/settings'; import { zIndices } from '../../styles/zIndex'; import { SeasonTheme } from './types'; import { getActiveSeason } from './seasonSchedule'; -import { - animSeasonFall, - animLeafFall, - animFloatUp, - animBob, - animTasselSway, - animGoldShimmer, - animCloverDrift, - animEarthLeafDrift, - animWarp, - animScanline, - animPixelBlink, -} from './Seasonal.css'; +import { HalloweenOverlay } from './themes/Halloween'; +import { ChristmasOverlay } from './themes/Christmas'; +import { NewYearOverlay } from './themes/NewYear'; +import { AutumnOverlay } from './themes/Autumn'; +import { AprilFoolsOverlay } from './themes/AprilFools'; +import { LunarNewYearOverlay } from './themes/LunarNewYear'; +import { ValentinesOverlay } from './themes/Valentines'; +import { StPatricksOverlay } from './themes/StPatricks'; +import { EarthDayOverlay } from './themes/EarthDay'; +import { DeepSpaceOverlay } from './themes/DeepSpace'; +import { ArcadeOverlay } from './themes/Arcade'; // SeasonTheme + the date-window logic now live in leaf modules (single source // of truth, shared with the settings UI). Re-exported here for existing // importers that still reach for it from this file. export type { SeasonTheme }; -// ─── Individual theme overlays ──────────────────────────────────────────────── - -function HalloweenOverlay({ reduced }: { reduced: boolean }) { - const particles = Array.from({ length: 22 }); - return ( - <> - {/* Dark purple ambient tint */} -