diff --git a/frontend/src/components/admin/ThemeCustomizerEnhanced.tsx b/frontend/src/components/admin/ThemeCustomizerEnhanced.tsx index ad47c708..241f1e9c 100644 --- a/frontend/src/components/admin/ThemeCustomizerEnhanced.tsx +++ b/frontend/src/components/admin/ThemeCustomizerEnhanced.tsx @@ -60,6 +60,12 @@ interface ThemeCustomizerEnhancedProps { // props are omitted (e.g. event-level theme editor), the section is hidden. forceColorMode?: 'dark' | 'light' | null; onForceColorModeChange?: (mode: 'dark' | 'light' | null) => void; + // Sync palette from Branding. When provided, a small button appears in + // the colour-pickers section header. The caller resolves the active + // Branding theme and fires onChange with the merged 8-token values — + // only the colour tokens swap, layout/header/typography stay put so an + // admin who's already arranged the structure can pull just the palette. + onSyncFromBranding?: () => void; } /** @@ -167,7 +173,8 @@ export const ThemeCustomizerEnhanced: React.FC = ( cssTemplateId, onCssTemplateChange, forceColorMode, - onForceColorModeChange + onForceColorModeChange, + onSyncFromBranding }) => { const { t } = useTranslation(); const [localTheme, setLocalTheme] = useState(value); @@ -878,10 +885,26 @@ export const ThemeCustomizerEnhanced: React.FC = ( {/* Color Customization */} -

- - {t('branding.colors')} -

+
+

+ + {t('branding.colors')} +

+ {/* "Sync from Branding" — caller-supplied so the customizer + doesn't have to know how to resolve the Branding theme. + Used in event create/edit to reset palette to site colours. */} + {onSyncFromBranding && ( + + )} +
{/* Color Mode Selector */}
diff --git a/frontend/src/pages/admin/CreateEventPage.tsx b/frontend/src/pages/admin/CreateEventPage.tsx index 089aff1f..bec6863c 100644 --- a/frontend/src/pages/admin/CreateEventPage.tsx +++ b/frontend/src/pages/admin/CreateEventPage.tsx @@ -242,6 +242,9 @@ export const CreateEventPage: React.FC = () => { // Apply the global Branding default theme on first load so admins who set a // site-wide default in Branding actually see it on new events (#323). + // This is the "always inherit colours from Branding" guarantee — every new + // gallery starts with the site palette unless the admin then picks a preset + // or hits Sync from Branding inside the customizer to re-pull it later. const brandingThemeApplied = useRef(false); useEffect(() => { if (brandingThemeApplied.current) return; @@ -597,6 +600,36 @@ export const CreateEventPage: React.FC = () => { onPresetChange={handlePresetChange} showGalleryLayouts={true} hideActions={true} + onSyncFromBranding={() => { + // Pull the 8 colour tokens (+ legacy primary alias) from + // the global Branding theme into the current event theme. + // Layout / header / typography are kept untouched so an + // admin who has already arranged structure can refresh + // just the palette. + const branding = settings?.theme_config as ThemeConfig | undefined; + if (!branding) { + toast.error(t('toast.brandingThemeMissing', 'No branding theme has been saved yet.')); + return; + } + setFormData(prev => ({ + ...prev, + theme_preset: 'custom', + theme_config: { + ...prev.theme_config, + primaryColor: branding.primaryColor, + accentColor: branding.accentColor, + accentDarkColor: branding.accentDarkColor, + backgroundColor: branding.backgroundColor, + surfaceColor: branding.surfaceColor, + elevatedColor: branding.elevatedColor, + surfaceBorderColor: branding.surfaceBorderColor, + textColor: branding.textColor, + mutedTextColor: branding.mutedTextColor, + colorMode: branding.colorMode ?? prev.theme_config.colorMode, + }, + })); + toast.success(t('toast.brandingPaletteSynced', 'Palette synced from Branding.')); + }} /> {/* Gallery Preview */} diff --git a/frontend/src/pages/admin/EventDetailsPage.tsx b/frontend/src/pages/admin/EventDetailsPage.tsx index 0a02c027..f29ec14e 100644 --- a/frontend/src/pages/admin/EventDetailsPage.tsx +++ b/frontend/src/pages/admin/EventDetailsPage.tsx @@ -2095,6 +2095,34 @@ export const EventDetailsPage: React.FC = () => { } } }} + onSyncFromBranding={() => { + // Reset only the 8 colour tokens to the site Branding — + // layout, header, typography all stay so the admin doesn't + // lose tweaks made for this specific event. + const branding = publicSettings?.theme_config as ThemeConfig | undefined; + if (!branding) { + toast.error(t('toast.brandingThemeMissing', 'No branding theme has been saved yet.')); + return; + } + const base = currentTheme || GALLERY_THEME_PRESETS.default.config; + const merged: ThemeConfig = { + ...base, + primaryColor: branding.primaryColor, + accentColor: branding.accentColor, + accentDarkColor: branding.accentDarkColor, + backgroundColor: branding.backgroundColor, + surfaceColor: branding.surfaceColor, + elevatedColor: branding.elevatedColor, + surfaceBorderColor: branding.surfaceBorderColor, + textColor: branding.textColor, + mutedTextColor: branding.mutedTextColor, + colorMode: branding.colorMode ?? base.colorMode, + }; + setCurrentTheme(merged); + setCurrentPresetName('custom'); + setEditForm(prev => ({ ...prev, color_theme: JSON.stringify(merged) })); + toast.success(t('toast.brandingPaletteSynced', 'Palette synced from Branding.')); + }} isPreviewMode={true} showGalleryLayouts={true} hideActions={true}