From 67d7d8d3fa25ceab0eda02b291f2e220b222f84a Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Tue, 5 May 2026 16:29:00 +0200 Subject: [PATCH] feat(branding): inline force color mode with auto-save + clearer palette help text --- .../src/components/admin/AdminSidebar.tsx | 7 +- .../admin/ThemeCustomizerEnhanced.tsx | 143 ++++++++++++++++-- frontend/src/pages/admin/BrandingPage.tsx | 58 +++---- 3 files changed, 152 insertions(+), 56 deletions(-) diff --git a/frontend/src/components/admin/AdminSidebar.tsx b/frontend/src/components/admin/AdminSidebar.tsx index adaa9b84..42d56d41 100644 --- a/frontend/src/components/admin/AdminSidebar.tsx +++ b/frontend/src/components/admin/AdminSidebar.tsx @@ -90,12 +90,15 @@ export const AdminSidebar: React.FC = ({ isOpen, onClose }) = onClick={() => onClose()} className={`flex items-center px-3 py-2 text-sm font-medium rounded-lg transition-colors ${ isActive - ? 'bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400' + ? 'bg-primary-50 dark:bg-primary-900/30 text-accent-dark' : 'text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-800 hover:text-neutral-900 dark:hover:text-neutral-100' }`} > + {/* Active icon/text use the theme's accent-dark token so the + sidebar follows the user's CI palette (LBM teal, custom + branding, etc.) instead of the legacy primary green. */} {t(item.nameKey)} diff --git a/frontend/src/components/admin/ThemeCustomizerEnhanced.tsx b/frontend/src/components/admin/ThemeCustomizerEnhanced.tsx index 60e3ad3e..5f35cdf7 100644 --- a/frontend/src/components/admin/ThemeCustomizerEnhanced.tsx +++ b/frontend/src/components/admin/ThemeCustomizerEnhanced.tsx @@ -54,6 +54,12 @@ interface ThemeCustomizerEnhancedProps { cssTemplates?: EnabledTemplate[]; cssTemplateId?: number | null; onCssTemplateChange?: (templateId: number | null) => void; + // Force color mode is an instance-level branding setting (not part of the + // per-theme config), but it lives next to the per-theme Color Mode picker + // so the Branding admin can find both controls in one place. When these + // props are omitted (e.g. event-level theme editor), the section is hidden. + forceColorMode?: 'dark' | 'light' | null; + onForceColorModeChange?: (mode: 'dark' | 'light' | null) => void; } const layoutIcons: Record = { @@ -116,7 +122,9 @@ export const ThemeCustomizerEnhanced: React.FC = ( isApplying = false, cssTemplates, cssTemplateId, - onCssTemplateChange + onCssTemplateChange, + forceColorMode, + onForceColorModeChange }) => { const { t } = useTranslation(); const [localTheme, setLocalTheme] = useState(value); @@ -875,7 +883,7 @@ export const ThemeCustomizerEnhanced: React.FC = ( }} className={`px-4 py-2 text-sm font-medium rounded-lg border transition-colors ${ (localTheme.colorMode || 'light') === mode - ? 'border-primary-600 bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300' + ? 'border-accent-dark bg-primary-50 dark:bg-primary-900/30 text-accent-dark' : 'border-neutral-300 dark:border-neutral-600 text-neutral-600 dark:text-neutral-400 hover:bg-neutral-50 dark:hover:bg-neutral-800' }`} > @@ -888,6 +896,51 @@ export const ThemeCustomizerEnhanced: React.FC = (

{t('branding.colorModeHelp', 'Auto follows the visitor\'s system preference.')}

+ + {/* + * Force color mode (instance-wide). Lives next to the per-theme + * Color Mode picker so the admin can find both controls in one + * place. The data flows through props from BrandingPage which + * persists it to branding settings; only renders when the + * onForceColorModeChange handler is provided (i.e. only on the + * Branding admin page, not in event-level theme editors). + */} + {onForceColorModeChange && ( +
+

+ {t('branding.forceColorMode', 'Force color mode')} +

+

+ {t( + 'branding.forceColorModeHelp', + 'Lock the entire admin and public site to dark or light. The user-facing dark/light toggle is hidden whenever a lock is active. Per-event themes that try to override the colour mode are also forced to follow.' + )} +

+
+ {([ + { value: null, label: t('branding.forceColorModeNone', 'No force (user choice)') }, + { value: 'dark', label: t('branding.forceColorModeDark', 'Force dark') }, + { value: 'light', label: t('branding.forceColorModeLight', 'Force light') }, + ] as const).map(({ value, label }) => { + const active = (forceColorMode ?? null) === value; + return ( + + ); + })} +
+
+ )} {/* @@ -901,15 +954,41 @@ export const ThemeCustomizerEnhanced: React.FC = (
{/* Surfaces */}
-

+

{t('branding.colorGroupSurfaces', 'Surfaces')}

+

+ {t( + 'branding.colorGroupSurfacesHelp', + 'The neutral layers behind your content. Background sits furthest back; Surface and Elevated stack on top.' + )} +

{[ - { key: 'backgroundColor', label: t('branding.backgroundColor', 'Background'), help: t('branding.backgroundColorHelp', 'Page base'), fallback: '#fafafa' }, - { key: 'surfaceColor', label: t('branding.surfaceColor', 'Surface'), help: t('branding.surfaceColorHelp', 'Cards, navigation'), fallback: '#ffffff' }, - { key: 'elevatedColor', label: t('branding.elevatedColor', 'Elevated'), help: t('branding.elevatedColorHelp', 'Raised panels, placeholders'), fallback: '#f5f5f5' }, - { key: 'surfaceBorderColor', label: t('branding.borderColor', 'Border'), help: t('branding.borderColorHelp', 'Dividers, grid lines'), fallback: '#e5e5e5' }, + { + key: 'backgroundColor', + label: t('branding.backgroundColor', 'Background'), + help: t('branding.backgroundColorHelp', 'The page itself — body background of every gallery, admin page and CMS page.'), + fallback: '#fafafa', + }, + { + key: 'surfaceColor', + label: t('branding.surfaceColor', 'Surface'), + help: t('branding.surfaceColorHelp', 'Cards, sidebar, header bar and navigation. The first layer above Background.'), + fallback: '#ffffff', + }, + { + key: 'elevatedColor', + label: t('branding.elevatedColor', 'Elevated'), + help: t('branding.elevatedColorHelp', 'Panels that float above cards: image placeholders, hover/active rows, modal headers, code blocks.'), + fallback: '#f5f5f5', + }, + { + key: 'surfaceBorderColor', + label: t('branding.borderColor', 'Border'), + help: t('branding.borderColorHelp', 'Dividers, table grid lines, card outlines, input borders.'), + fallback: '#e5e5e5', + }, ].map(({ key, label, help, fallback }) => (