feat(theme): expand color settings to 8-token CI palette + alt button

This commit is contained in:
Luca
2026-05-05 16:04:43 +02:00
parent ee7de6f6a1
commit 114aab5777
11 changed files with 612 additions and 150 deletions
@@ -158,7 +158,13 @@ export const ThemeCustomizerEnhanced: React.FC<ThemeCustomizerEnhancedProps> = (
}, [presetName]);
const handleChange = (key: keyof ThemeConfig, newValue: any) => {
const updated = { ...localTheme, [key]: newValue };
const updated: ThemeConfig = { ...localTheme, [key]: newValue };
// Legacy alias: keep primaryColor in lockstep with accentDarkColor so
// any consumer that still reads --color-primary or themeConfig.primaryColor
// doesn't drift after the 8-token migration.
if (key === 'accentDarkColor') {
updated.primaryColor = newValue;
}
setLocalTheme(updated);
// When any change is made, mark it as custom
@@ -265,18 +271,24 @@ export const ThemeCustomizerEnhanced: React.FC<ThemeCustomizerEnhancedProps> = (
</div>
<div className="flex items-center gap-2 mt-3">
<div className="flex gap-1">
{/* Preview swatches: background, surface, accent-dark, accent
— gives a quick read of the preset's full palette. */}
<div
className="w-5 h-5 rounded-full border border-neutral-200 dark:border-neutral-600"
style={{ backgroundColor: theme.config.primaryColor }}
style={{ backgroundColor: theme.config.backgroundColor }}
/>
<div
className="w-5 h-5 rounded-full border border-neutral-200 dark:border-neutral-600"
style={{ backgroundColor: theme.config.surfaceColor || theme.config.backgroundColor }}
/>
<div
className="w-5 h-5 rounded-full border border-neutral-200 dark:border-neutral-600"
style={{ backgroundColor: theme.config.accentDarkColor || theme.config.primaryColor }}
/>
<div
className="w-5 h-5 rounded-full border border-neutral-200 dark:border-neutral-600"
style={{ backgroundColor: theme.config.accentColor }}
/>
<div
className="w-5 h-5 rounded-full border border-neutral-200 dark:border-neutral-600"
style={{ backgroundColor: theme.config.backgroundColor }}
/>
</div>
{theme.config.galleryLayout && layoutIcons[theme.config.galleryLayout] && (
<div className="ml-auto text-neutral-400">
@@ -834,25 +846,27 @@ export const ThemeCustomizerEnhanced: React.FC<ThemeCustomizerEnhancedProps> = (
handleChange('colorMode', mode);
// When switching to dark, auto-populate dark defaults if colors are still light
if (mode === 'dark' && (!localTheme.backgroundColor || localTheme.backgroundColor === '#fafafa' || localTheme.backgroundColor === '#ffffff')) {
const updated = {
const updated: ThemeConfig = {
...localTheme,
colorMode: mode,
backgroundColor: '#0f0f0f',
textColor: '#e5e5e5',
surfaceColor: '#1a1a1a',
elevatedColor: '#242424',
surfaceBorderColor: '#2e2e2e',
textColor: '#e5e5e5',
mutedTextColor: '#a3a3a3',
};
setLocalTheme(updated);
onChange({ ...updated, customCss });
} else if (mode === 'light' && localTheme.colorMode === 'dark') {
const updated = {
const updated: ThemeConfig = {
...localTheme,
colorMode: mode,
backgroundColor: '#fafafa',
textColor: '#171717',
surfaceColor: '#ffffff',
elevatedColor: '#f5f5f5',
surfaceBorderColor: '#e5e5e5',
textColor: '#171717',
mutedTextColor: '#737373',
};
setLocalTheme(updated);
@@ -876,85 +890,119 @@ export const ThemeCustomizerEnhanced: React.FC<ThemeCustomizerEnhancedProps> = (
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/*
* 8-token CI palette pickers, grouped by role.
* Each token writes directly to the same field name on ThemeConfig
* (kebab → camel mapping happens via handleChange's first arg).
* Translation keys fall back to inline strings — German/English
* coverage only (per user language profile); other locales will
* show the fallback until reviewed by a native speaker.
*/}
<div className="space-y-6">
{/* Surfaces */}
<div>
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">
{t('branding.primaryColor')}
</label>
<div className="flex gap-2">
<input
type="color"
value={localTheme.primaryColor || '#5C8762'}
onChange={(e) => handleChange('primaryColor', e.target.value)}
className="h-10 w-20 rounded border border-neutral-300 dark:border-neutral-600 cursor-pointer"
/>
<Input
value={localTheme.primaryColor || '#5C8762'}
onChange={(e) => handleChange('primaryColor', e.target.value)}
placeholder="#5C8762"
className="flex-1"
/>
<h4 className="text-sm font-semibold text-neutral-700 dark:text-neutral-300 uppercase tracking-wide mb-3">
{t('branding.colorGroupSurfaces', 'Surfaces')}
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{[
{ 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' },
].map(({ key, label, help, fallback }) => (
<div key={key}>
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
{label}
</label>
<p className="text-xs text-neutral-500 dark:text-neutral-400 mb-2">{help}</p>
<div className="flex gap-2">
<input
type="color"
value={(localTheme as Record<string, string | undefined>)[key] || fallback}
onChange={(e) => handleChange(key as keyof ThemeConfig, e.target.value)}
className="h-10 w-20 rounded border border-neutral-300 dark:border-neutral-600 cursor-pointer"
/>
<Input
value={(localTheme as Record<string, string | undefined>)[key] || fallback}
onChange={(e) => handleChange(key as keyof ThemeConfig, e.target.value)}
placeholder={fallback}
className="flex-1"
/>
</div>
</div>
))}
</div>
</div>
{/* Text */}
<div>
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">
{t('branding.accentColor')}
</label>
<div className="flex gap-2">
<input
type="color"
value={localTheme.accentColor || '#22c55e'}
onChange={(e) => handleChange('accentColor', e.target.value)}
className="h-10 w-20 rounded border border-neutral-300 dark:border-neutral-600 cursor-pointer"
/>
<Input
value={localTheme.accentColor || '#22c55e'}
onChange={(e) => handleChange('accentColor', e.target.value)}
placeholder="#22c55e"
className="flex-1"
/>
<h4 className="text-sm font-semibold text-neutral-700 dark:text-neutral-300 uppercase tracking-wide mb-3">
{t('branding.colorGroupText', 'Text')}
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{[
{ key: 'textColor', label: t('branding.textColor', 'Primary text'), help: t('branding.textColorHelp', 'Headlines, body'), fallback: '#171717' },
{ key: 'mutedTextColor', label: t('branding.mutedTextColor', 'Secondary text'), help: t('branding.mutedTextColorHelp', 'Captions, labels, meta'), fallback: '#737373' },
].map(({ key, label, help, fallback }) => (
<div key={key}>
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
{label}
</label>
<p className="text-xs text-neutral-500 dark:text-neutral-400 mb-2">{help}</p>
<div className="flex gap-2">
<input
type="color"
value={(localTheme as Record<string, string | undefined>)[key] || fallback}
onChange={(e) => handleChange(key as keyof ThemeConfig, e.target.value)}
className="h-10 w-20 rounded border border-neutral-300 dark:border-neutral-600 cursor-pointer"
/>
<Input
value={(localTheme as Record<string, string | undefined>)[key] || fallback}
onChange={(e) => handleChange(key as keyof ThemeConfig, e.target.value)}
placeholder={fallback}
className="flex-1"
/>
</div>
</div>
))}
</div>
</div>
{/* Accent */}
<div>
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">
{t('branding.backgroundColor')}
</label>
<div className="flex gap-2">
<input
type="color"
value={localTheme.backgroundColor || '#fafafa'}
onChange={(e) => handleChange('backgroundColor', e.target.value)}
className="h-10 w-20 rounded border border-neutral-300 dark:border-neutral-600 cursor-pointer"
/>
<Input
value={localTheme.backgroundColor || '#fafafa'}
onChange={(e) => handleChange('backgroundColor', e.target.value)}
placeholder="#fafafa"
className="flex-1"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">
{t('branding.textColor')}
</label>
<div className="flex gap-2">
<input
type="color"
value={localTheme.textColor || '#171717'}
onChange={(e) => handleChange('textColor', e.target.value)}
className="h-10 w-20 rounded border border-neutral-300 dark:border-neutral-600 cursor-pointer"
/>
<Input
value={localTheme.textColor || '#171717'}
onChange={(e) => handleChange('textColor', e.target.value)}
placeholder="#171717"
className="flex-1"
/>
<h4 className="text-sm font-semibold text-neutral-700 dark:text-neutral-300 uppercase tracking-wide mb-3">
{t('branding.colorGroupAccent', 'Accent')}
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{[
{ key: 'accentColor', label: t('branding.accentColor', 'Accent'), help: t('branding.accentColorHelp', 'Links, focus rings, hover'), fallback: '#22c55e' },
{ key: 'accentDarkColor', label: t('branding.accentDarkColor', 'Accent (filled)'), help: t('branding.accentDarkColorHelp', 'Primary CTA fill'), fallback: '#5C8762' },
].map(({ key, label, help, fallback }) => (
<div key={key}>
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
{label}
</label>
<p className="text-xs text-neutral-500 dark:text-neutral-400 mb-2">{help}</p>
<div className="flex gap-2">
<input
type="color"
value={(localTheme as Record<string, string | undefined>)[key] || fallback}
onChange={(e) => handleChange(key as keyof ThemeConfig, e.target.value)}
className="h-10 w-20 rounded border border-neutral-300 dark:border-neutral-600 cursor-pointer"
/>
<Input
value={(localTheme as Record<string, string | undefined>)[key] || fallback}
onChange={(e) => handleChange(key as keyof ThemeConfig, e.target.value)}
placeholder={fallback}
className="flex-1"
/>
</div>
</div>
))}
</div>
{/* primaryColor is kept in sync with accentDarkColor inside
handleChange() — no dedicated picker. */}
</div>
</div>
</Card>
@@ -1179,10 +1227,14 @@ export const ThemeCustomizerEnhanced: React.FC<ThemeCustomizerEnhancedProps> = (
{t('branding.cssInstructions.variablesDesc', 'Use these CSS variables to match your theme presets:')}
</p>
<code className="block bg-neutral-800 text-green-400 p-3 rounded text-xs overflow-x-auto">
{`--primary-color: ${localTheme.primaryColor || '#5C8762'};
--accent-color: ${localTheme.accentColor || '#22c55e'};
--background-color: ${localTheme.backgroundColor || '#fafafa'};
--text-color: ${localTheme.textColor || '#171717'};
{`--color-background: ${localTheme.backgroundColor || '#fafafa'};
--color-surface: ${localTheme.surfaceColor || '#ffffff'};
--color-elevated: ${localTheme.elevatedColor || '#f5f5f5'};
--color-surface-border: ${localTheme.surfaceBorderColor || '#e5e5e5'};
--color-text: ${localTheme.textColor || '#171717'};
--color-muted-text: ${localTheme.mutedTextColor || '#737373'};
--color-accent: ${localTheme.accentColor || '#22c55e'};
--color-accent-dark: ${localTheme.accentDarkColor || localTheme.primaryColor || '#5C8762'};
--font-family: ${localTheme.fontFamily || 'Inter, sans-serif'};
--heading-font: ${localTheme.headingFontFamily || localTheme.fontFamily || 'Inter, sans-serif'};`}
</code>
+17 -19
View File
@@ -93,32 +93,30 @@ export const ThemeDisplay: React.FC<ThemeDisplayProps> = ({
{showDetails && (
<>
{/* Color Palette */}
{/* Color Palette — show all 8 tokens of the active theme.
Each swatch only renders if its token is set so legacy themes
(pre-8-token migration) still render their original 4 swatches. */}
<div className="flex items-center gap-2">
<Palette className="w-4 h-4 text-neutral-500 dark:text-neutral-300" />
<span className="text-sm text-neutral-600 dark:text-neutral-200">{t('branding.colors')}:</span>
<div className="flex gap-1">
{themeConfig.primaryColor && (
{[
{ value: themeConfig.backgroundColor, title: t('branding.backgroundColor', 'Background') },
{ value: themeConfig.surfaceColor, title: t('branding.surfaceColor', 'Surface') },
{ value: themeConfig.elevatedColor, title: t('branding.elevatedColor', 'Elevated') },
{ value: themeConfig.surfaceBorderColor, title: t('branding.borderColor', 'Border') },
{ value: themeConfig.textColor, title: t('branding.textColor', 'Text') },
{ value: themeConfig.mutedTextColor, title: t('branding.mutedTextColor', 'Muted text') },
{ value: themeConfig.accentColor, title: t('branding.accentColor', 'Accent') },
{ value: themeConfig.accentDarkColor || themeConfig.primaryColor, title: t('branding.accentDarkColor', 'Accent (filled)') },
].filter((s) => !!s.value).map((s, i) => (
<div
key={i}
className="w-6 h-6 rounded border border-neutral-300 dark:border-neutral-600"
style={{ backgroundColor: themeConfig.primaryColor }}
title={t('branding.primaryColor')}
style={{ backgroundColor: s.value }}
title={s.title}
/>
)}
{themeConfig.accentColor && (
<div
className="w-6 h-6 rounded border border-neutral-300 dark:border-neutral-600"
style={{ backgroundColor: themeConfig.accentColor }}
title={t('branding.accentColor')}
/>
)}
{themeConfig.backgroundColor && (
<div
className="w-6 h-6 rounded border border-neutral-300 dark:border-neutral-600"
style={{ backgroundColor: themeConfig.backgroundColor }}
title={t('branding.backgroundColor')}
/>
)}
))}
</div>
</div>
@@ -116,20 +116,20 @@ export const ThemeEditorModal: React.FC<ThemeEditorModalProps> = ({
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col">
<div className="bg-white dark:bg-neutral-900 rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col">
{/* Header */}
<div className="px-6 py-4 border-b border-neutral-200 flex items-center justify-between">
<div className="px-6 py-4 border-b border-neutral-200 dark:border-neutral-700 flex items-center justify-between">
<div>
<h2 className="text-xl font-semibold text-neutral-900">
<h2 className="text-xl font-semibold text-neutral-900 dark:text-neutral-100">
{t('events.galleryTheme')}
</h2>
<p className="text-sm text-neutral-600 mt-1">
<p className="text-sm text-neutral-600 dark:text-neutral-300 mt-1">
{t('events.customizingThemeFor', { event: eventName })}
</p>
</div>
<button
onClick={onClose}
className="text-neutral-400 hover:text-neutral-600 transition-colors"
className="text-neutral-400 dark:text-neutral-500 hover:text-neutral-600 dark:hover:text-neutral-300 transition-colors"
>
<X className="w-6 h-6" />
</button>
@@ -139,7 +139,7 @@ export const ThemeEditorModal: React.FC<ThemeEditorModalProps> = ({
<div className="flex-1 overflow-y-auto">
<div className="grid grid-cols-1 lg:grid-cols-2 h-full">
{/* Left side - Theme Customizer */}
<div className="p-6 overflow-y-auto border-r border-neutral-200">
<div className="p-6 overflow-y-auto border-r border-neutral-200 dark:border-neutral-700">
<ThemeCustomizerEnhanced
value={theme}
onChange={handleThemeChange}
@@ -155,11 +155,11 @@ export const ThemeEditorModal: React.FC<ThemeEditorModalProps> = ({
</div>
{/* Right side - Gallery Preview */}
<div className="p-6 bg-neutral-50 overflow-y-auto">
<div className="p-6 bg-neutral-50 dark:bg-neutral-800 overflow-y-auto">
<div className="space-y-4">
{/* Grid Style Selector */}
<div>
<h3 className="text-sm font-medium text-neutral-700 mb-3">
<h3 className="text-sm font-medium text-neutral-700 dark:text-neutral-200 mb-3">
{t('branding.previewLayout')}
</h3>
<div className="grid grid-cols-3 gap-2">
@@ -169,12 +169,12 @@ export const ThemeEditorModal: React.FC<ThemeEditorModalProps> = ({
onClick={() => setPreviewLayout(layout)}
className={`relative p-3 rounded-lg border-2 transition-all ${
(previewLayout || theme.galleryLayout || 'grid') === layout
? 'border-primary-600 bg-primary-50'
: 'border-neutral-200 hover:border-neutral-300 bg-white'
? 'border-primary-600 bg-primary-50 dark:bg-primary-900/40'
: 'border-neutral-200 dark:border-neutral-700 hover:border-neutral-300 dark:hover:border-neutral-600 bg-white dark:bg-neutral-900'
}`}
>
<div className="flex flex-col items-center gap-1">
<div className="text-neutral-700">
<div className="text-neutral-700 dark:text-neutral-200">
{layoutIcons[layout]}
</div>
<span className="text-xs capitalize">