feat(theme): expand color settings to 8-token CI palette + alt button
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user