feat(seasonal): tone down overlays and add visual preview grid in Settings
- New Year: replace flashing animBurst rays with gentle falling confetti - Lunar New Year: reduce 9 lanterns to 4, halve sizes, dim silk/shimmer - April Fools: remove all glitch/scanline/watermark effects; replace with a subtle rainbow stripe and falling punctuation symbols - Add SeasonalPreview export (position:absolute, reduced-motion) for use inside contained card elements - Replace SettingsSelect dropdown for Seasonal Theme with SeasonalBgGrid, a visual card grid (matches ChatBgGrid pattern) showing ambient previews Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,16 +7,12 @@ import {
|
||||
animFloatUp,
|
||||
animBob,
|
||||
animTasselSway,
|
||||
animGlitch,
|
||||
animGlitchColor,
|
||||
animGlitchScan,
|
||||
animBurst,
|
||||
animWarp,
|
||||
animScanline,
|
||||
animPixelBlink,
|
||||
animGoldShimmer,
|
||||
animCloverDrift,
|
||||
animEarthLeafDrift,
|
||||
animWarp,
|
||||
animScanline,
|
||||
animPixelBlink,
|
||||
} from './Seasonal.css';
|
||||
|
||||
export type SeasonTheme =
|
||||
@@ -98,7 +94,7 @@ function HalloweenOverlay({ reduced }: { reduced: boolean }) {
|
||||
particles.map((_, i) => {
|
||||
const isOrange = i % 3 === 0;
|
||||
const size = 4 + (i % 3) * 2;
|
||||
const left = ((i * 4597 + 137) % 100);
|
||||
const left = (i * 4597 + 137) % 100;
|
||||
const duration = 8 + (i % 7) * 1.5;
|
||||
const delay = (i * 0.45) % 7;
|
||||
return (
|
||||
@@ -141,7 +137,7 @@ function ChristmasOverlay({ reduced }: { reduced: boolean }) {
|
||||
{!reduced &&
|
||||
flakes.map((_, i) => {
|
||||
const size = 3 + (i % 4) * 2;
|
||||
const left = ((i * 3571 + 251) % 100);
|
||||
const left = (i * 3571 + 251) % 100;
|
||||
const duration = 10 + (i % 8) * 2;
|
||||
const delay = (i * 0.55) % 10;
|
||||
const drift = ((i % 5) - 2) * 12;
|
||||
@@ -168,17 +164,10 @@ function ChristmasOverlay({ reduced }: { reduced: boolean }) {
|
||||
);
|
||||
}
|
||||
|
||||
// Replaced flashing burst rays with gentle falling confetti
|
||||
function NewYearOverlay({ reduced }: { reduced: boolean }) {
|
||||
const bursts = [
|
||||
{ x: 20, y: 25, color: '#ffd700', delay: 0 },
|
||||
{ x: 75, y: 15, color: '#ff4466', delay: 1.2 },
|
||||
{ x: 50, y: 35, color: '#00d4ff', delay: 2.4 },
|
||||
{ x: 15, y: 60, color: '#ffd700', delay: 3.6 },
|
||||
{ x: 85, y: 45, color: '#aa44ff', delay: 0.8 },
|
||||
{ x: 40, y: 20, color: '#ff8800', delay: 2.0 },
|
||||
];
|
||||
|
||||
const petals = Array.from({ length: 8 });
|
||||
const confetti = Array.from({ length: 24 });
|
||||
const colors = ['#ffd700', '#ff4466', '#00d4ff', '#aa44ff', '#ff8800', '#ffffff'];
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -187,50 +176,48 @@ function NewYearOverlay({ reduced }: { reduced: boolean }) {
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
backgroundColor: 'rgba(10,5,0,0.15)',
|
||||
backgroundColor: 'rgba(10,5,0,0.10)',
|
||||
backgroundImage:
|
||||
'radial-gradient(ellipse at 50% 50%, rgba(255,200,0,0.04) 0%, transparent 70%)',
|
||||
}}
|
||||
/>
|
||||
{/* Gentle falling confetti */}
|
||||
{!reduced &&
|
||||
bursts.map((b, bi) =>
|
||||
petals.map((_, pi) => {
|
||||
const angle = (pi / petals.length) * 360;
|
||||
const dist = 80 + (pi % 3) * 30;
|
||||
const duration = 1.6 + (bi % 3) * 0.4;
|
||||
const period = 3.5 + bi * 0.8;
|
||||
return (
|
||||
<div
|
||||
key={`${bi}-${pi}`}
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: `${b.x}%`,
|
||||
top: `${b.y}%`,
|
||||
width: `${dist}px`,
|
||||
height: '2px',
|
||||
backgroundColor: b.color,
|
||||
boxShadow: `0 0 6px ${b.color}`,
|
||||
transformOrigin: '0 50%',
|
||||
transform: `rotate(${angle}deg)`,
|
||||
animation: `${animBurst} ${duration}s ease-out ${b.delay + pi * 0.05}s ${period}s infinite`,
|
||||
borderRadius: '1px',
|
||||
opacity: 0,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})
|
||||
)}
|
||||
{/* Gold shimmer overlay */}
|
||||
confetti.map((_, i) => {
|
||||
const c = colors[i % colors.length];
|
||||
const left = (i * 4597 + 137) % 100;
|
||||
const size = 3 + (i % 3) * 2;
|
||||
const duration = 8 + (i % 7) * 1.5;
|
||||
const delay = (i * 0.4) % 8;
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '-8px',
|
||||
left: `${left}%`,
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
borderRadius: i % 2 === 0 ? '50%' : '1px',
|
||||
backgroundColor: c,
|
||||
boxShadow: `0 0 ${size + 2}px ${c}`,
|
||||
animation: `${animSeasonFall} ${duration}s ease-in ${delay}s infinite`,
|
||||
opacity: 0.7 + (i % 3) * 0.1,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{/* Slow gold shimmer */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
backgroundImage:
|
||||
'linear-gradient(105deg, transparent 30%, rgba(255,215,0,0.06) 50%, transparent 70%)',
|
||||
'linear-gradient(105deg, transparent 30%, rgba(255,215,0,0.05) 50%, transparent 70%)',
|
||||
backgroundSize: '200% 100%',
|
||||
animation: reduced ? 'none' : `${animGoldShimmer} 4s linear infinite`,
|
||||
animation: reduced ? 'none' : `${animGoldShimmer} 5s linear infinite`,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
@@ -259,11 +246,11 @@ function AutumnOverlay({ reduced }: { reduced: boolean }) {
|
||||
/>
|
||||
{!reduced &&
|
||||
leaves.map((_, i) => {
|
||||
const left = ((i * 5381 + 179) % 100);
|
||||
const left = (i * 5381 + 179) % 100;
|
||||
const duration = 12 + (i % 6) * 2;
|
||||
const delay = (i * 0.65) % 12;
|
||||
const size = 10 + (i % 4) * 4;
|
||||
const color = colors[i % colors.length];
|
||||
const col = colors[i % colors.length];
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
@@ -275,8 +262,8 @@ function AutumnOverlay({ reduced }: { reduced: boolean }) {
|
||||
width: `${size}px`,
|
||||
height: `${size * 0.7}px`,
|
||||
borderRadius: '50% 0 50% 0',
|
||||
backgroundColor: color,
|
||||
boxShadow: `0 0 4px ${color}`,
|
||||
backgroundColor: col,
|
||||
boxShadow: `0 0 4px ${col}`,
|
||||
animation: `${animLeafFall} ${duration}s ease-in ${delay}s infinite`,
|
||||
}}
|
||||
/>
|
||||
@@ -286,115 +273,104 @@ function AutumnOverlay({ reduced }: { reduced: boolean }) {
|
||||
);
|
||||
}
|
||||
|
||||
// Replaced aggressive glitch with playful confetti rain
|
||||
function AprilFoolsOverlay({ reduced }: { reduced: boolean }) {
|
||||
const particles = Array.from({ length: 20 });
|
||||
const symbols = ['?', '!', '¿', '‽', '?', '!'];
|
||||
const colors = [
|
||||
'rgba(255,80,80,0.55)',
|
||||
'rgba(255,200,0,0.55)',
|
||||
'rgba(80,200,80,0.55)',
|
||||
'rgba(80,80,255,0.55)',
|
||||
'rgba(200,80,200,0.55)',
|
||||
'rgba(80,200,200,0.55)',
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* RGB channel separation layers */}
|
||||
{/* Subtle rainbow stripe along top edge */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
backgroundColor: 'rgba(255,0,0,0.04)',
|
||||
transform: 'translateX(2px)',
|
||||
mixBlendMode: 'multiply',
|
||||
animation: reduced ? 'none' : `${animGlitch} 5s step-end infinite`,
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '3px',
|
||||
backgroundImage:
|
||||
'linear-gradient(90deg, rgba(255,0,0,0.4), rgba(255,165,0,0.4), rgba(255,255,0,0.4), rgba(0,200,0,0.4), rgba(0,0,255,0.4), rgba(128,0,128,0.4))',
|
||||
opacity: 0.7,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
backgroundColor: 'rgba(0,255,255,0.04)',
|
||||
transform: 'translateX(-2px)',
|
||||
mixBlendMode: 'multiply',
|
||||
animation: reduced ? 'none' : `${animGlitch} 5s step-end 0.3s infinite`,
|
||||
}}
|
||||
/>
|
||||
{/* Color corruption */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
animation: reduced ? 'none' : `${animGlitchColor} 7s step-end infinite`,
|
||||
}}
|
||||
/>
|
||||
{/* Sweeping scanline */}
|
||||
{!reduced && (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '3px',
|
||||
backgroundColor: 'rgba(0,255,136,0.35)',
|
||||
boxShadow: '0 0 8px rgba(0,255,136,0.5)',
|
||||
animation: `${animGlitchScan} 2.8s linear infinite`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{/* "ERROR" watermark */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%) rotate(-15deg)',
|
||||
fontSize: '80px',
|
||||
fontWeight: 900,
|
||||
fontFamily: 'monospace',
|
||||
color: 'rgba(255,0,0,0.07)',
|
||||
letterSpacing: '0.1em',
|
||||
pointerEvents: 'none',
|
||||
userSelect: 'none',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
SIGNAL LOST
|
||||
</div>
|
||||
{/* Gentle falling punctuation symbols */}
|
||||
{!reduced &&
|
||||
particles.map((_, i) => {
|
||||
const left = (i * 5381 + 179) % 100;
|
||||
const duration = 11 + (i % 5) * 2.5;
|
||||
const delay = (i * 0.55) % 10;
|
||||
const col = colors[i % colors.length];
|
||||
const sym = symbols[i % symbols.length];
|
||||
const size = 12 + (i % 3) * 5;
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '-20px',
|
||||
left: `${left}%`,
|
||||
fontSize: `${size}px`,
|
||||
color: col,
|
||||
fontWeight: 700,
|
||||
fontFamily: 'monospace',
|
||||
animation: `${animSeasonFall} ${duration}s ease-in ${delay}s infinite`,
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
{sym}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Reduced to 4 lanterns, subtler tint and shimmer
|
||||
function LunarNewYearOverlay({ reduced }: { reduced: boolean }) {
|
||||
const lanterns = Array.from({ length: 9 });
|
||||
const lanterns = Array.from({ length: 4 }); // was 9
|
||||
return (
|
||||
<>
|
||||
{/* Silk-like texture overlay */}
|
||||
{/* Very subtle red silk tint */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
backgroundColor: 'rgba(140,0,0,0.08)',
|
||||
backgroundColor: 'rgba(140,0,0,0.05)',
|
||||
backgroundImage: [
|
||||
'repeating-linear-gradient(45deg, rgba(200,20,0,0.03) 0px, rgba(200,20,0,0.03) 1px, transparent 1px, transparent 8px)',
|
||||
'repeating-linear-gradient(135deg, rgba(200,20,0,0.03) 0px, rgba(200,20,0,0.03) 1px, transparent 1px, transparent 8px)',
|
||||
'repeating-linear-gradient(45deg, rgba(200,20,0,0.015) 0px, rgba(200,20,0,0.015) 1px, transparent 1px, transparent 8px)',
|
||||
'repeating-linear-gradient(135deg, rgba(200,20,0,0.015) 0px, rgba(200,20,0,0.015) 1px, transparent 1px, transparent 8px)',
|
||||
].join(','),
|
||||
}}
|
||||
/>
|
||||
{/* Gold shimmer sweep */}
|
||||
{/* Slow gold shimmer */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
backgroundImage:
|
||||
'linear-gradient(100deg, transparent 25%, rgba(255,200,0,0.07) 45%, rgba(255,220,50,0.1) 50%, rgba(255,200,0,0.07) 55%, transparent 75%)',
|
||||
'linear-gradient(100deg, transparent 25%, rgba(255,200,0,0.05) 45%, rgba(255,220,50,0.07) 50%, rgba(255,200,0,0.05) 55%, transparent 75%)',
|
||||
backgroundSize: '300% 100%',
|
||||
animation: reduced ? 'none' : `${animGoldShimmer} 5s linear infinite`,
|
||||
animation: reduced ? 'none' : `${animGoldShimmer} 8s linear infinite`,
|
||||
}}
|
||||
/>
|
||||
{/* Floating paper lanterns */}
|
||||
{/* 4 floating lanterns */}
|
||||
{lanterns.map((_, i) => {
|
||||
const left = 5 + ((i * 4603 + 311) % 90);
|
||||
const top = 8 + ((i * 2311 + 97) % 55);
|
||||
const left = 10 + ((i * 4603 + 311) % 75);
|
||||
const top = 10 + ((i * 2311 + 97) % 50);
|
||||
const duration = 3.5 + (i % 4) * 0.7;
|
||||
const delay = i * 0.5;
|
||||
const delay = i * 0.9;
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
@@ -403,10 +379,10 @@ function LunarNewYearOverlay({ reduced }: { reduced: boolean }) {
|
||||
position: 'absolute',
|
||||
left: `${left}%`,
|
||||
top: `${top}%`,
|
||||
animation: reduced ? 'none' : `${animBob} ${duration}s ease-in-out ${delay}s infinite`,
|
||||
animation:
|
||||
reduced ? 'none' : `${animBob} ${duration}s ease-in-out ${delay}s infinite`,
|
||||
}}
|
||||
>
|
||||
{/* Lantern top cap */}
|
||||
<div
|
||||
style={{
|
||||
width: '18px',
|
||||
@@ -417,7 +393,6 @@ function LunarNewYearOverlay({ reduced }: { reduced: boolean }) {
|
||||
boxShadow: '0 0 4px rgba(255,215,0,0.6)',
|
||||
}}
|
||||
/>
|
||||
{/* Lantern body */}
|
||||
<div
|
||||
style={{
|
||||
width: '24px',
|
||||
@@ -429,7 +404,6 @@ function LunarNewYearOverlay({ reduced }: { reduced: boolean }) {
|
||||
margin: '1px auto',
|
||||
}}
|
||||
/>
|
||||
{/* Lantern bottom cap */}
|
||||
<div
|
||||
style={{
|
||||
width: '18px',
|
||||
@@ -439,14 +413,16 @@ function LunarNewYearOverlay({ reduced }: { reduced: boolean }) {
|
||||
margin: '0 auto',
|
||||
}}
|
||||
/>
|
||||
{/* Tassel */}
|
||||
<div
|
||||
style={{
|
||||
width: '2px',
|
||||
height: '14px',
|
||||
backgroundColor: '#ffd700',
|
||||
margin: '0 auto',
|
||||
animation: reduced ? 'none' : `${animTasselSway} ${duration * 0.6}s ease-in-out ${delay}s infinite`,
|
||||
animation:
|
||||
reduced
|
||||
? 'none'
|
||||
: `${animTasselSway} ${duration * 0.6}s ease-in-out ${delay}s infinite`,
|
||||
transformOrigin: 'top center',
|
||||
}}
|
||||
/>
|
||||
@@ -482,7 +458,7 @@ function ValentinesOverlay({ reduced }: { reduced: boolean }) {
|
||||
const duration = 9 + (i % 6) * 1.8;
|
||||
const delay = (i * 0.6) % 9;
|
||||
const size = 14 + (i % 4) * 5;
|
||||
const color = colors[i % colors.length];
|
||||
const col = colors[i % colors.length];
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
@@ -492,7 +468,7 @@ function ValentinesOverlay({ reduced }: { reduced: boolean }) {
|
||||
bottom: '-20px',
|
||||
left: `${left}%`,
|
||||
fontSize: `${size}px`,
|
||||
color,
|
||||
color: col,
|
||||
filter: 'drop-shadow(0 0 4px rgba(255,100,140,0.4))',
|
||||
animation: `${animFloatUp} ${duration}s ease-in ${delay}s infinite`,
|
||||
userSelect: 'none',
|
||||
@@ -521,7 +497,6 @@ function StPatricksOverlay({ reduced }: { reduced: boolean }) {
|
||||
].join(','),
|
||||
}}
|
||||
/>
|
||||
{/* Moving metallic gold shimmer on the accent border at top */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
@@ -538,7 +513,7 @@ function StPatricksOverlay({ reduced }: { reduced: boolean }) {
|
||||
/>
|
||||
{!reduced &&
|
||||
clovers.map((_, i) => {
|
||||
const left = ((i * 4129 + 223) % 100);
|
||||
const left = (i * 4129 + 223) % 100;
|
||||
const duration = 14 + (i % 6) * 2;
|
||||
const delay = (i * 0.7) % 12;
|
||||
const size = 14 + (i % 3) * 6;
|
||||
@@ -581,7 +556,6 @@ function EarthDayOverlay({ reduced }: { reduced: boolean }) {
|
||||
].join(','),
|
||||
}}
|
||||
/>
|
||||
{/* Vine line along the left edge */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
@@ -626,7 +600,6 @@ function DeepSpaceOverlay({ reduced }: { reduced: boolean }) {
|
||||
const stars = Array.from({ length: 24 });
|
||||
return (
|
||||
<>
|
||||
{/* Deep space ambient */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
@@ -640,7 +613,6 @@ function DeepSpaceOverlay({ reduced }: { reduced: boolean }) {
|
||||
].join(','),
|
||||
}}
|
||||
/>
|
||||
{/* Warp streak particles emanating from center */}
|
||||
{!reduced &&
|
||||
stars.map((_, i) => {
|
||||
const angle = (i / stars.length) * 360;
|
||||
@@ -648,7 +620,11 @@ function DeepSpaceOverlay({ reduced }: { reduced: boolean }) {
|
||||
const delay = (i * 0.18) % 2.5;
|
||||
const period = 3 + (i % 4) * 0.5;
|
||||
const size = 1 + (i % 3);
|
||||
const colors = ['rgba(200,180,255,0.9)', 'rgba(150,200,255,0.8)', 'rgba(255,255,255,0.7)'];
|
||||
const starColors = [
|
||||
'rgba(200,180,255,0.9)',
|
||||
'rgba(150,200,255,0.8)',
|
||||
'rgba(255,255,255,0.7)',
|
||||
];
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
@@ -659,10 +635,10 @@ function DeepSpaceOverlay({ reduced }: { reduced: boolean }) {
|
||||
top: '50%',
|
||||
width: `${80 + i * 6}px`,
|
||||
height: `${size}px`,
|
||||
backgroundColor: colors[i % colors.length],
|
||||
backgroundColor: starColors[i % starColors.length],
|
||||
transformOrigin: '0 50%',
|
||||
transform: `rotate(${angle}deg)`,
|
||||
boxShadow: `0 0 ${size * 2}px ${colors[i % colors.length]}`,
|
||||
boxShadow: `0 0 ${size * 2}px ${starColors[i % starColors.length]}`,
|
||||
animation: `${animWarp} ${duration}s ease-out ${delay}s ${period}s infinite`,
|
||||
opacity: 0,
|
||||
}}
|
||||
@@ -676,7 +652,6 @@ function DeepSpaceOverlay({ reduced }: { reduced: boolean }) {
|
||||
function ArcadeOverlay({ reduced }: { reduced: boolean }) {
|
||||
return (
|
||||
<>
|
||||
{/* CRT scanlines */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
@@ -687,7 +662,6 @@ function ArcadeOverlay({ reduced }: { reduced: boolean }) {
|
||||
animation: reduced ? 'none' : `${animScanline} 3s ease-in-out infinite`,
|
||||
}}
|
||||
/>
|
||||
{/* Pixel corner decorations */}
|
||||
{(['0,0', '0,auto', 'auto,0', 'auto,auto'] as const).map((corner, i) => {
|
||||
const [t, b] = corner.split(',');
|
||||
return (
|
||||
@@ -712,7 +686,6 @@ function ArcadeOverlay({ reduced }: { reduced: boolean }) {
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{/* "INSERT COIN" prompt */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
@@ -731,7 +704,6 @@ function ArcadeOverlay({ reduced }: { reduced: boolean }) {
|
||||
>
|
||||
— INSERT COIN —
|
||||
</div>
|
||||
{/* Vignette */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
@@ -745,29 +717,40 @@ function ArcadeOverlay({ reduced }: { reduced: boolean }) {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Wrapper ──────────────────────────────────────────────────────────────────
|
||||
// ─── Overlay content map (shared between SeasonalOverlay and SeasonalPreview) ──
|
||||
|
||||
function SeasonalOverlay({
|
||||
theme,
|
||||
reduced,
|
||||
}: {
|
||||
theme: SeasonTheme;
|
||||
reduced: boolean;
|
||||
}) {
|
||||
const overlayMap: Record<SeasonTheme, React.ReactNode> = {
|
||||
halloween: <HalloweenOverlay reduced={reduced} />,
|
||||
christmas: <ChristmasOverlay reduced={reduced} />,
|
||||
newyear: <NewYearOverlay reduced={reduced} />,
|
||||
autumn: <AutumnOverlay reduced={reduced} />,
|
||||
aprilfools: <AprilFoolsOverlay reduced={reduced} />,
|
||||
lunar: <LunarNewYearOverlay reduced={reduced} />,
|
||||
valentines: <ValentinesOverlay reduced={reduced} />,
|
||||
stpatricks: <StPatricksOverlay reduced={reduced} />,
|
||||
earthday: <EarthDayOverlay reduced={reduced} />,
|
||||
deepspace: <DeepSpaceOverlay reduced={reduced} />,
|
||||
arcade: <ArcadeOverlay reduced={reduced} />,
|
||||
};
|
||||
function buildOverlayContent(theme: SeasonTheme, reduced: boolean): React.ReactNode {
|
||||
switch (theme) {
|
||||
case 'halloween':
|
||||
return <HalloweenOverlay reduced={reduced} />;
|
||||
case 'christmas':
|
||||
return <ChristmasOverlay reduced={reduced} />;
|
||||
case 'newyear':
|
||||
return <NewYearOverlay reduced={reduced} />;
|
||||
case 'autumn':
|
||||
return <AutumnOverlay reduced={reduced} />;
|
||||
case 'aprilfools':
|
||||
return <AprilFoolsOverlay reduced={reduced} />;
|
||||
case 'lunar':
|
||||
return <LunarNewYearOverlay reduced={reduced} />;
|
||||
case 'valentines':
|
||||
return <ValentinesOverlay reduced={reduced} />;
|
||||
case 'stpatricks':
|
||||
return <StPatricksOverlay reduced={reduced} />;
|
||||
case 'earthday':
|
||||
return <EarthDayOverlay reduced={reduced} />;
|
||||
case 'deepspace':
|
||||
return <DeepSpaceOverlay reduced={reduced} />;
|
||||
case 'arcade':
|
||||
return <ArcadeOverlay reduced={reduced} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Full-screen overlay (fixed position, used in App) ────────────────────────
|
||||
|
||||
function SeasonalOverlay({ theme, reduced }: { theme: SeasonTheme; reduced: boolean }) {
|
||||
return (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
@@ -779,11 +762,30 @@ function SeasonalOverlay({
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{overlayMap[theme]}
|
||||
{buildOverlayContent(theme, reduced)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Preview overlay (absolute position, contained in a card) ─────────────────
|
||||
|
||||
/**
|
||||
* Renders the ambient (reduced-motion) version of a seasonal overlay inside
|
||||
* a parent container. The parent must have `position: relative; overflow: hidden`.
|
||||
*/
|
||||
export function SeasonalPreview({ theme }: { theme: SeasonTheme }) {
|
||||
return (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{ position: 'absolute', inset: 0, overflow: 'hidden', pointerEvents: 'none' }}
|
||||
>
|
||||
{buildOverlayContent(theme, true)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Main exported component ──────────────────────────────────────────────────
|
||||
|
||||
export function SeasonalEffect() {
|
||||
const settings = useAtomValue(settingsAtom);
|
||||
const reduced =
|
||||
|
||||
@@ -42,8 +42,13 @@ import {
|
||||
DateFormat,
|
||||
MessageLayout,
|
||||
MessageSpacing,
|
||||
Settings,
|
||||
settingsAtom,
|
||||
} from '../../../state/settings';
|
||||
import {
|
||||
SeasonalPreview,
|
||||
SeasonTheme,
|
||||
} from '../../../components/seasonal/SeasonalEffect';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { KeySymbol } from '../../../utils/key-symbol';
|
||||
import { isMacOS } from '../../../utils/user-agent';
|
||||
@@ -488,32 +493,22 @@ function Appearance() {
|
||||
/>
|
||||
</SequenceCard>
|
||||
|
||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="SurfaceVariant"
|
||||
direction="Column"
|
||||
gap="200"
|
||||
>
|
||||
<SettingTile
|
||||
title="Seasonal Theme"
|
||||
description="Decorative overlays that activate automatically on holidays and events, or choose one manually."
|
||||
after={
|
||||
<SettingsSelect
|
||||
value={seasonalThemeOverride ?? 'auto'}
|
||||
onChange={(v) => setSeasonalThemeOverride(v as typeof seasonalThemeOverride)}
|
||||
options={[
|
||||
{ value: 'auto', label: '🗓 Auto (date-based)' },
|
||||
{ value: 'off', label: 'Off' },
|
||||
{ value: 'newyear', label: '🎆 New Year' },
|
||||
{ value: 'lunar', label: '🏮 Lunar New Year' },
|
||||
{ value: 'valentines', label: '💖 Valentine\'s Day' },
|
||||
{ value: 'stpatricks', label: '🍀 St. Patrick\'s Day' },
|
||||
{ value: 'aprilfools', label: '🃏 April Fool\'s Day' },
|
||||
{ value: 'earthday', label: '🌱 Earth Day' },
|
||||
{ value: 'autumn', label: '🍂 Autumn' },
|
||||
{ value: 'halloween', label: '🎃 Halloween' },
|
||||
{ value: 'christmas', label: '❄️ Christmas' },
|
||||
{ value: 'arcade', label: '👾 Retro Arcade Day' },
|
||||
{ value: 'deepspace', label: '🚀 Deep Space Week' },
|
||||
]}
|
||||
/>
|
||||
}
|
||||
description="Decorative overlays for holidays and events. Preview below — click to select."
|
||||
/>
|
||||
<Box style={{ padding: `0 ${config.space.S400} ${config.space.S300}` }}>
|
||||
<SeasonalBgGrid
|
||||
value={seasonalThemeOverride ?? 'auto'}
|
||||
onChange={(v) => setSeasonalThemeOverride(v)}
|
||||
/>
|
||||
</Box>
|
||||
</SequenceCard>
|
||||
|
||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
@@ -1351,6 +1346,89 @@ function Calls() {
|
||||
);
|
||||
}
|
||||
|
||||
const SEASONAL_OPTIONS: { value: Settings['seasonalThemeOverride']; label: string; emoji: string }[] =
|
||||
[
|
||||
{ value: 'auto', label: 'Auto', emoji: '🗓' },
|
||||
{ value: 'off', label: 'Off', emoji: '×' },
|
||||
{ value: 'newyear', label: 'New Year', emoji: '🎆' },
|
||||
{ value: 'lunar', label: 'Lunar New Year', emoji: '🏮' },
|
||||
{ value: 'valentines', label: "Valentine's", emoji: '💖' },
|
||||
{ value: 'stpatricks', label: "St. Patrick's", emoji: '🍀' },
|
||||
{ value: 'aprilfools', label: 'April Fools', emoji: '?' },
|
||||
{ value: 'earthday', label: 'Earth Day', emoji: '🌱' },
|
||||
{ value: 'autumn', label: 'Autumn', emoji: '🍂' },
|
||||
{ value: 'halloween', label: 'Halloween', emoji: '🎃' },
|
||||
{ value: 'christmas', label: 'Christmas', emoji: '❄️' },
|
||||
{ value: 'arcade', label: 'Arcade Day', emoji: '👾' },
|
||||
{ value: 'deepspace', label: 'Deep Space', emoji: '🚀' },
|
||||
];
|
||||
|
||||
function SeasonalBgGrid({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: Settings['seasonalThemeOverride'];
|
||||
onChange: (v: Settings['seasonalThemeOverride']) => void;
|
||||
}) {
|
||||
return (
|
||||
<Box wrap="Wrap" gap="200">
|
||||
{SEASONAL_OPTIONS.map((opt) => {
|
||||
const selected = value === opt.value;
|
||||
const isSpecial = opt.value === 'auto' || opt.value === 'off';
|
||||
return (
|
||||
<Box key={opt.value} direction="Column" gap="100" style={{ alignItems: 'center' }}>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={opt.label}
|
||||
aria-pressed={selected}
|
||||
onClick={() => onChange(opt.value)}
|
||||
style={{
|
||||
position: 'relative',
|
||||
display: 'block',
|
||||
width: toRem(76),
|
||||
height: toRem(56),
|
||||
borderRadius: toRem(8),
|
||||
cursor: 'pointer',
|
||||
border: selected
|
||||
? `2px solid ${color.Critical.Main}`
|
||||
: '2px solid rgba(128,128,128,0.25)',
|
||||
padding: 0,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: '#030508',
|
||||
}}
|
||||
>
|
||||
{!isSpecial && <SeasonalPreview theme={opt.value as SeasonTheme} />}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundImage:
|
||||
opt.value === 'auto'
|
||||
? 'linear-gradient(135deg, rgba(255,100,0,0.2), rgba(255,200,0,0.2), rgba(0,200,100,0.2), rgba(0,100,255,0.2))'
|
||||
: undefined,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
{isSpecial && (
|
||||
<span style={{ fontSize: '22px', opacity: opt.value === 'off' ? 0.4 : 1 }}>
|
||||
{opt.emoji}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
<Text size="T200" style={selected ? { color: color.Critical.Main } : undefined}>
|
||||
{opt.label}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ChatBgGrid() {
|
||||
const [chatBackground, setChatBackground] = useSetting(settingsAtom, 'chatBackground');
|
||||
const [pauseAnimations] = useSetting(settingsAtom, 'pauseAnimations');
|
||||
|
||||
Reference in New Issue
Block a user