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