02b2ce8109
Same treatment as the seasonal themes: split the 502-line chatBackground.ts Record into one premium module per background under lotus/backgrounds/ (each exposes a tuned dark + light ChatBgVariants), one Opus agent per background against a shared brief. chatBackground.ts now assembles DARK/LIGHT from the modules; getChatBg is unchanged. Carbon + Aurora are kept inline as-is (user favorites); none stays the empty layer. Every redesign: layered oklch palettes, seamless tiling with worked-out tile math (integer-multiple periods; edge-wrapping inline-SVG data-URIs for circuit/hexgrid/waves/herringbone/chevron/tactical), independently-tuned dark+light (not a recolor), and low "felt-not-read" opacity so chat text stays WCAG-AA legible. The 5 animated backgrounds (rain, star drift, grid pulse, aurora flow, fireflies) each colocate a vanilla-extract keyframe .css.ts, animate only background-position for a jump-free loop, and — since getChatBg strips animation for reduced-motion — render a finished static frame too. Redesigned: blueprint, stars, topographic, herringbone, crosshatch, chevron, polka, triangles, plaid, tactical, circuit, hexgrid, waves, neon, anim-rain, anim-stars, anim-pulse, anim-aurora, anim-fireflies. Gates: tsc clean, ESLint clean, Prettier clean, build OK, 551 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
156 lines
5.9 KiB
TypeScript
156 lines
5.9 KiB
TypeScript
import { CSSProperties } from 'react';
|
|
import { ChatBackground } from '../../state/settings';
|
|
import { blueprint } from './backgrounds/blueprint';
|
|
import { stars } from './backgrounds/stars';
|
|
import { topographic } from './backgrounds/topographic';
|
|
import { herringbone } from './backgrounds/herringbone';
|
|
import { crosshatch } from './backgrounds/crosshatch';
|
|
import { chevron } from './backgrounds/chevron';
|
|
import { polka } from './backgrounds/polka';
|
|
import { triangles } from './backgrounds/triangles';
|
|
import { plaid } from './backgrounds/plaid';
|
|
import { tactical } from './backgrounds/tactical';
|
|
import { circuit } from './backgrounds/circuit';
|
|
import { hexgrid } from './backgrounds/hexgrid';
|
|
import { waves } from './backgrounds/waves';
|
|
import { neon } from './backgrounds/neon';
|
|
import { animRain } from './backgrounds/animRain';
|
|
import { animStars } from './backgrounds/animStars';
|
|
import { animPulse } from './backgrounds/animPulse';
|
|
import { animAurora } from './backgrounds/animAurora';
|
|
import { animFireflies } from './backgrounds/animFireflies';
|
|
|
|
export const BG_OPTIONS: { value: ChatBackground; label: string }[] = [
|
|
{ value: 'none', label: 'None' },
|
|
{ value: 'blueprint', label: 'Blueprint' },
|
|
{ value: 'carbon', label: 'Carbon' },
|
|
{ value: 'stars', label: 'Stars' },
|
|
{ value: 'topographic', label: 'Topographic' },
|
|
{ value: 'herringbone', label: 'Herringbone' },
|
|
{ value: 'crosshatch', label: 'Crosshatch' },
|
|
{ value: 'chevron', label: 'Chevron' },
|
|
{ value: 'polka', label: 'Polka' },
|
|
{ value: 'triangles', label: 'Triangles' },
|
|
{ value: 'plaid', label: 'Plaid' },
|
|
{ value: 'tactical', label: 'Tactical' },
|
|
{ value: 'circuit', label: 'Circuit' },
|
|
{ value: 'hexgrid', label: 'Hex Grid' },
|
|
{ value: 'waves', label: 'Waves' },
|
|
{ value: 'neon', label: 'Neon Grid' },
|
|
{ value: 'aurora', label: 'Aurora' },
|
|
{ value: 'anim-rain', label: 'Digital Rain' },
|
|
{ value: 'anim-stars', label: 'Star Drift' },
|
|
{ value: 'anim-pulse', label: 'Grid Pulse' },
|
|
{ value: 'anim-aurora', label: 'Aurora Flow' },
|
|
{ value: 'anim-fireflies', label: 'Fireflies' },
|
|
];
|
|
|
|
// `none`, `carbon` and `aurora` stay inline: carbon + aurora are the kept user
|
|
// favorites, none is the empty layer. Every other background is a premium
|
|
// per-pattern module under ./backgrounds/ (each exposes a `dark` + `light`
|
|
// variant). Keeping the whole record here lets getChatBg stay the single entry
|
|
// point and preserves the Record<ChatBackground, ...> exhaustiveness check.
|
|
const DARK: Record<ChatBackground, CSSProperties> = {
|
|
none: {},
|
|
|
|
carbon: {
|
|
backgroundColor: '#0e0e0e',
|
|
backgroundImage: [
|
|
'repeating-linear-gradient(45deg, rgba(255,255,255,0.035) 0, rgba(255,255,255,0.035) 2px, transparent 0, transparent 50%)',
|
|
'repeating-linear-gradient(135deg, rgba(255,255,255,0.035) 0, rgba(255,255,255,0.035) 2px, transparent 0, transparent 50%)',
|
|
].join(','),
|
|
backgroundSize: '8px 8px',
|
|
},
|
|
aurora: {
|
|
backgroundColor: '#030810',
|
|
backgroundImage: [
|
|
'radial-gradient(ellipse at 20% 30%, rgba(0,255,136,0.08) 0%, transparent 55%)',
|
|
'radial-gradient(ellipse at 80% 70%, rgba(0,100,255,0.08) 0%, transparent 55%)',
|
|
'radial-gradient(ellipse at 50% 10%, rgba(120,0,255,0.06) 0%, transparent 50%)',
|
|
'radial-gradient(ellipse at 60% 90%, rgba(0,212,255,0.06) 0%, transparent 50%)',
|
|
].join(','),
|
|
},
|
|
|
|
blueprint: blueprint.dark,
|
|
stars: stars.dark,
|
|
topographic: topographic.dark,
|
|
herringbone: herringbone.dark,
|
|
crosshatch: crosshatch.dark,
|
|
chevron: chevron.dark,
|
|
polka: polka.dark,
|
|
triangles: triangles.dark,
|
|
plaid: plaid.dark,
|
|
tactical: tactical.dark,
|
|
circuit: circuit.dark,
|
|
hexgrid: hexgrid.dark,
|
|
waves: waves.dark,
|
|
neon: neon.dark,
|
|
'anim-rain': animRain.dark,
|
|
'anim-stars': animStars.dark,
|
|
'anim-pulse': animPulse.dark,
|
|
'anim-aurora': animAurora.dark,
|
|
'anim-fireflies': animFireflies.dark,
|
|
};
|
|
|
|
const LIGHT: Record<ChatBackground, CSSProperties> = {
|
|
none: {},
|
|
|
|
carbon: {
|
|
backgroundColor: '#efefef',
|
|
backgroundImage: [
|
|
'repeating-linear-gradient(45deg, rgba(0,0,0,0.04) 0, rgba(0,0,0,0.04) 2px, transparent 0, transparent 50%)',
|
|
'repeating-linear-gradient(135deg, rgba(0,0,0,0.04) 0, rgba(0,0,0,0.04) 2px, transparent 0, transparent 50%)',
|
|
].join(','),
|
|
backgroundSize: '8px 8px',
|
|
},
|
|
aurora: {
|
|
backgroundColor: '#f4faf8',
|
|
backgroundImage: [
|
|
'radial-gradient(ellipse at 20% 30%, rgba(0,160,80,0.09) 0%, transparent 55%)',
|
|
'radial-gradient(ellipse at 80% 70%, rgba(0,80,200,0.09) 0%, transparent 55%)',
|
|
'radial-gradient(ellipse at 50% 10%, rgba(100,0,200,0.07) 0%, transparent 50%)',
|
|
'radial-gradient(ellipse at 60% 90%, rgba(0,160,200,0.07) 0%, transparent 50%)',
|
|
].join(','),
|
|
},
|
|
|
|
blueprint: blueprint.light,
|
|
stars: stars.light,
|
|
topographic: topographic.light,
|
|
herringbone: herringbone.light,
|
|
crosshatch: crosshatch.light,
|
|
chevron: chevron.light,
|
|
polka: polka.light,
|
|
triangles: triangles.light,
|
|
plaid: plaid.light,
|
|
tactical: tactical.light,
|
|
circuit: circuit.light,
|
|
hexgrid: hexgrid.light,
|
|
waves: waves.light,
|
|
neon: neon.light,
|
|
'anim-rain': animRain.light,
|
|
'anim-stars': animStars.light,
|
|
'anim-pulse': animPulse.light,
|
|
'anim-aurora': animAurora.light,
|
|
'anim-fireflies': animFireflies.light,
|
|
};
|
|
|
|
export const getChatBg = (
|
|
bg: ChatBackground,
|
|
isDark: boolean,
|
|
pauseAnimations?: boolean,
|
|
): CSSProperties => {
|
|
const style = isDark ? DARK[bg] : LIGHT[bg];
|
|
const reducedMotion =
|
|
typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
if ((pauseAnimations || reducedMotion) && style.animation) {
|
|
const { animation: _anim, ...rest } = style;
|
|
return rest;
|
|
}
|
|
// For animated backgrounds, promote the element to its own compositor layer so
|
|
// background-position keyframes don't trigger repaints on descendant elements.
|
|
if (style.animation) {
|
|
return { ...style, willChange: 'background-position', contain: 'paint' };
|
|
}
|
|
return style;
|
|
};
|