diff --git a/backend/apps/settings/models.py b/backend/apps/settings/models.py index 70ad7e70..9507599c 100644 --- a/backend/apps/settings/models.py +++ b/backend/apps/settings/models.py @@ -65,6 +65,8 @@ class AppSettings(BaseModel): onboarding_v3: Optional[str] = None # User-picked accent hex from the onboarding theme pad; None = stock accent. accent_color: Optional[str] = None + # Multi-stop gradient from the theme pad (2-3 hexes); washes the canvas. + accent_gradient: Optional[list[str]] = None personalized_greeting: Optional[str] = None personalized_starters: list["PersonalizedStarter"] = Field(default_factory=list) # Suppresses preflight suggestion modal entries the user dismissed; keyed by ToolDefinition.name, value ISO timestamp. diff --git a/frontend/src/app/Main.tsx b/frontend/src/app/Main.tsx index e2536068..127f92fd 100644 --- a/frontend/src/app/Main.tsx +++ b/frontend/src/app/Main.tsx @@ -203,9 +203,10 @@ const DeepLinkListener: React.FC<{ children: React.ReactNode }> = ({ children }) const SettingsLoader: React.FC<{ children: React.ReactNode }> = ({ children }) => { const dispatch = useAppDispatch(); const { setMode: setThemeMode } = useThemeMode(); - const { setAccent } = useThemeAccent(); + const { setAccent, setGradient } = useThemeAccent(); const theme = useAppSelector((s) => s.settings.data.theme); const accentColor = useAppSelector((s) => s.settings.data.accent_color); + const accentGradient = useAppSelector((s) => s.settings.data.accent_gradient); const loaded = useAppSelector((s) => s.settings.loaded); const allowExperimentalUpdates = useAppSelector((s) => s.settings.data.allow_experimental_updates); useEffect(() => { @@ -263,6 +264,13 @@ const SettingsLoader: React.FC<{ children: React.ReactNode }> = ({ children }) = if (loaded) setAccent(accentColor ?? null); }, [loaded, accentColor, setAccent]); + // Object identity churns on every settings fetch, so key the effect on the serialized stops. + const gradientKey = JSON.stringify(accentGradient ?? null); + useEffect(() => { + if (loaded) setGradient(accentGradient ?? null); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [loaded, gradientKey, setGradient]); + useEffect(() => { if (!loaded) return; (window as any).openswarm?.setAllowPrerelease?.(allowExperimentalUpdates); diff --git a/frontend/src/app/components/OnboardingV3/beats/BeatCard.tsx b/frontend/src/app/components/OnboardingV3/beats/BeatCard.tsx index b3653cef..219547d5 100644 --- a/frontend/src/app/components/OnboardingV3/beats/BeatCard.tsx +++ b/frontend/src/app/components/OnboardingV3/beats/BeatCard.tsx @@ -1,5 +1,6 @@ import React, { useMemo, useState } from 'react'; import { motion } from 'framer-motion'; +import { useThemeAccent } from '@/shared/styles/ThemeContext'; import { Dices } from 'lucide-react'; import type { ClaudeTokens } from '@/shared/styles/claudeTokens'; import type { ProviderIdentity } from '../onboardingV3Api'; @@ -27,6 +28,7 @@ const BeatCard: React.FC<{ onFinish: (name: string | null) => void; onBack: () => void; }> = ({ c, identity, onFinish, onBack }) => { + const { gradient } = useThemeAccent(); const [name, setName] = useState(() => nameFromIdentity(identity)); const seed = useMemo(() => Math.floor(Math.random() * EPITHETS.length), []); const [roll, setRoll] = useState(0); @@ -54,7 +56,7 @@ const BeatCard: React.FC<{ }} >