feat(events): Sync from Branding button in gallery theme customizer + clarified default inheritance

This commit is contained in:
Luca
2026-05-06 01:59:02 +02:00
parent 47b6b39f3a
commit bdbe7b80a1
3 changed files with 89 additions and 5 deletions
@@ -60,6 +60,12 @@ interface ThemeCustomizerEnhancedProps {
// props are omitted (e.g. event-level theme editor), the section is hidden. // props are omitted (e.g. event-level theme editor), the section is hidden.
forceColorMode?: 'dark' | 'light' | null; forceColorMode?: 'dark' | 'light' | null;
onForceColorModeChange?: (mode: 'dark' | 'light' | null) => void; 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<ThemeCustomizerEnhancedProps> = (
cssTemplateId, cssTemplateId,
onCssTemplateChange, onCssTemplateChange,
forceColorMode, forceColorMode,
onForceColorModeChange onForceColorModeChange,
onSyncFromBranding
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [localTheme, setLocalTheme] = useState<ThemeConfig>(value); const [localTheme, setLocalTheme] = useState<ThemeConfig>(value);
@@ -878,10 +885,26 @@ export const ThemeCustomizerEnhanced: React.FC<ThemeCustomizerEnhancedProps> = (
{/* Color Customization */} {/* Color Customization */}
<Card className="p-6"> <Card className="p-6">
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4 flex items-center gap-2"> <div className="flex items-center justify-between gap-2 mb-4">
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
<Palette className="w-5 h-5" /> <Palette className="w-5 h-5" />
{t('branding.colors')} {t('branding.colors')}
</h3> </h3>
{/* "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 && (
<Button
type="button"
variant="outline"
size="sm"
leftIcon={<RotateCcw className="w-4 h-4" />}
onClick={onSyncFromBranding}
>
{t('branding.syncFromBranding', 'Sync from Branding')}
</Button>
)}
</div>
{/* Color Mode Selector */} {/* Color Mode Selector */}
<div className="mb-6"> <div className="mb-6">
@@ -242,6 +242,9 @@ export const CreateEventPage: React.FC = () => {
// Apply the global Branding default theme on first load so admins who set a // 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). // 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); const brandingThemeApplied = useRef(false);
useEffect(() => { useEffect(() => {
if (brandingThemeApplied.current) return; if (brandingThemeApplied.current) return;
@@ -597,6 +600,36 @@ export const CreateEventPage: React.FC = () => {
onPresetChange={handlePresetChange} onPresetChange={handlePresetChange}
showGalleryLayouts={true} showGalleryLayouts={true}
hideActions={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 */} {/* Gallery Preview */}
@@ -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} isPreviewMode={true}
showGalleryLayouts={true} showGalleryLayouts={true}
hideActions={true} hideActions={true}