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
@@ -0,0 +1,91 @@
import { describe, expect, it } from 'vitest';
import { migrateThemeConfig } from '../themeMigration';
import type { ThemeConfig } from '../../types/theme.types';
describe('migrateThemeConfig — 8-token palette fill', () => {
it('derives light surface defaults for a legacy 4-color light theme', () => {
const legacy: ThemeConfig = {
primaryColor: '#5C8762',
accentColor: '#22c55e',
backgroundColor: '#fafafa',
textColor: '#171717',
colorMode: 'light',
galleryLayout: 'grid',
};
const migrated = migrateThemeConfig(legacy);
expect(migrated.surfaceColor).toBe('#ffffff');
expect(migrated.elevatedColor).toBe('#f5f5f5');
expect(migrated.surfaceBorderColor).toBe('#e5e5e5');
expect(migrated.mutedTextColor).toBe('#737373');
// Legacy primaryColor was used as the CTA fill — preserved as accentDark.
expect(migrated.accentDarkColor).toBe('#5C8762');
// Existing fields untouched.
expect(migrated.primaryColor).toBe('#5C8762');
expect(migrated.backgroundColor).toBe('#fafafa');
expect(migrated.textColor).toBe('#171717');
});
it('derives dark surface defaults for a legacy 4-color dark theme', () => {
const legacy: ThemeConfig = {
primaryColor: '#3b82f6',
accentColor: '#1e40af',
backgroundColor: '#0a0a0a',
textColor: '#f5f5f5',
colorMode: 'dark',
galleryLayout: 'grid',
};
const migrated = migrateThemeConfig(legacy);
expect(migrated.surfaceColor).toBe('#1a1a1a');
expect(migrated.elevatedColor).toBe('#242424');
expect(migrated.surfaceBorderColor).toBe('#2e2e2e');
expect(migrated.mutedTextColor).toBe('#a3a3a3');
expect(migrated.accentDarkColor).toBe('#3b82f6');
});
it('does not overwrite explicit 8-token values', () => {
const fullPalette: ThemeConfig = {
primaryColor: '#014E4E',
accentColor: '#017C7C',
accentDarkColor: '#014E4E',
backgroundColor: '#0D0D0D',
surfaceColor: '#111414',
elevatedColor: '#182222',
surfaceBorderColor: '#1E2E2E',
textColor: '#EBEBEB',
mutedTextColor: '#4A6060',
colorMode: 'dark',
galleryLayout: 'grid',
};
const migrated = migrateThemeConfig(fullPalette);
expect(migrated.surfaceColor).toBe('#111414');
expect(migrated.elevatedColor).toBe('#182222');
expect(migrated.surfaceBorderColor).toBe('#1E2E2E');
expect(migrated.mutedTextColor).toBe('#4A6060');
expect(migrated.accentDarkColor).toBe('#014E4E');
});
it('still migrates the legacy "hero" galleryLayout while filling palette', () => {
const legacy = {
primaryColor: '#5C8762',
accentColor: '#22c55e',
backgroundColor: '#fafafa',
textColor: '#171717',
galleryLayout: 'hero',
} as unknown as ThemeConfig;
const migrated = migrateThemeConfig(legacy);
expect(migrated.galleryLayout).toBe('grid');
expect(migrated.headerStyle).toBe('hero');
expect(migrated.heroDividerStyle).toBe('wave');
// Palette still filled.
expect(migrated.surfaceColor).toBe('#ffffff');
expect(migrated.accentDarkColor).toBe('#5C8762');
});
});
+55 -12
View File
@@ -1,34 +1,77 @@
import type { ThemeConfig, HeaderStyleType, HeroDividerStyle, GalleryLayoutType } from '../types/theme.types';
/**
* Migrates legacy theme configurations that used 'hero' as a galleryLayout
* to the new decoupled headerStyle + galleryLayout system.
* Fills in any missing 8-token CI palette fields on legacy themes that were
* saved before the palette expanded from 4 → 8 explicit tokens.
*
* This ensures backward compatibility with existing events that have
* 'hero' set as their galleryLayout.
* The visible look of an existing instance must not change just because the
* type system grew (per project memory: migrations preserve visual state).
* For each missing token we fall back to the value the renderer was already
* deriving implicitly:
* - accentDarkColor ← primaryColor (legacy primary was used as CTA fill)
* - elevatedColor ← surfaceColor (or a slight shift for light themes)
* - surfaceColor ← '#ffffff' / '#1a1a1a' depending on colorMode
* - surfaceBorderColor← '#e5e5e5' / '#2e2e2e'
* - mutedTextColor ← '#737373' / '#a3a3a3'
*/
function fillMissingPaletteTokens(theme: ThemeConfig): ThemeConfig {
const isDark = theme.colorMode === 'dark';
const filled: ThemeConfig = { ...theme };
if (!filled.surfaceColor) {
filled.surfaceColor = isDark ? '#1a1a1a' : '#ffffff';
}
if (!filled.elevatedColor) {
// For dark themes raise slightly above surface; for light, drop slightly below.
filled.elevatedColor = isDark ? '#242424' : '#f5f5f5';
}
if (!filled.surfaceBorderColor) {
filled.surfaceBorderColor = isDark ? '#2e2e2e' : '#e5e5e5';
}
if (!filled.mutedTextColor) {
filled.mutedTextColor = isDark ? '#a3a3a3' : '#737373';
}
if (!filled.accentDarkColor) {
// Legacy themes used primaryColor as the CTA fill — preserve that.
filled.accentDarkColor = filled.primaryColor;
}
return filled;
}
/**
* Migrates legacy theme configurations:
* - 'hero' galleryLayout → decoupled headerStyle + galleryLayout
* - missing 8-token CI palette fields → derived from legacy 4-color set
*
* This ensures backward compatibility with existing events.
*/
export function migrateThemeConfig(theme: ThemeConfig): ThemeConfig {
if (!theme) return theme;
let migrated = theme;
// Check if this theme uses the legacy 'hero' layout
if ((theme.galleryLayout as string) === 'hero') {
return {
...theme,
if ((migrated.galleryLayout as string) === 'hero') {
migrated = {
...migrated,
headerStyle: 'hero' as HeaderStyleType,
galleryLayout: 'grid' as GalleryLayoutType,
heroDividerStyle: (theme.heroDividerStyle || 'wave') as HeroDividerStyle,
heroDividerStyle: (migrated.heroDividerStyle || 'wave') as HeroDividerStyle,
};
}
// If headerStyle is not set but galleryLayout is valid, default to 'standard'
if (!theme.headerStyle && theme.galleryLayout) {
return {
...theme,
if (!migrated.headerStyle && migrated.galleryLayout) {
migrated = {
...migrated,
headerStyle: 'standard' as HeaderStyleType,
};
}
return theme;
// Fill any missing 8-token palette fields so the renderer never has to
// fall back to hard-coded defaults that diverge from the original look.
return fillMissingPaletteTokens(migrated);
}
/**