fix(branding): selected-state accent colors, force-mode actually flips galleries, compact color picker layout
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { migrateThemeConfig } from '../themeMigration';
|
||||
import { migrateThemeConfig, applyForceColorMode } from '../themeMigration';
|
||||
import type { ThemeConfig } from '../../types/theme.types';
|
||||
|
||||
describe('migrateThemeConfig — 8-token palette fill', () => {
|
||||
@@ -89,3 +89,71 @@ describe('migrateThemeConfig — 8-token palette fill', () => {
|
||||
expect(migrated.accentDarkColor).toBe('#5C8762');
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyForceColorMode', () => {
|
||||
const lightTheme: ThemeConfig = {
|
||||
primaryColor: '#5C8762',
|
||||
accentColor: '#22c55e',
|
||||
accentDarkColor: '#5C8762',
|
||||
backgroundColor: '#fafafa',
|
||||
surfaceColor: '#ffffff',
|
||||
elevatedColor: '#f5f5f5',
|
||||
surfaceBorderColor: '#e5e5e5',
|
||||
textColor: '#171717',
|
||||
mutedTextColor: '#737373',
|
||||
colorMode: 'light',
|
||||
};
|
||||
|
||||
const lbmDark: ThemeConfig = {
|
||||
primaryColor: '#014E4E',
|
||||
accentColor: '#017C7C',
|
||||
accentDarkColor: '#014E4E',
|
||||
backgroundColor: '#0D0D0D',
|
||||
surfaceColor: '#111414',
|
||||
elevatedColor: '#182222',
|
||||
surfaceBorderColor: '#1E2E2E',
|
||||
textColor: '#EBEBEB',
|
||||
mutedTextColor: '#4A6060',
|
||||
colorMode: 'dark',
|
||||
};
|
||||
|
||||
it('returns the theme unchanged when no force mode is set', () => {
|
||||
expect(applyForceColorMode(lightTheme, null)).toEqual(lightTheme);
|
||||
expect(applyForceColorMode(lightTheme, undefined)).toEqual(lightTheme);
|
||||
});
|
||||
|
||||
it('only pins colorMode when the theme already matches the forced mode', () => {
|
||||
const result = applyForceColorMode(lbmDark, 'dark');
|
||||
expect(result.colorMode).toBe('dark');
|
||||
// LBM surfaces preserved.
|
||||
expect(result.backgroundColor).toBe('#0D0D0D');
|
||||
expect(result.surfaceColor).toBe('#111414');
|
||||
expect(result.accentColor).toBe('#017C7C');
|
||||
});
|
||||
|
||||
it('swaps surface tokens when forcing a light theme to dark', () => {
|
||||
const result = applyForceColorMode(lightTheme, 'dark');
|
||||
expect(result.colorMode).toBe('dark');
|
||||
// Surfaces flipped to dark defaults.
|
||||
expect(result.backgroundColor).toBe('#0f0f0f');
|
||||
expect(result.surfaceColor).toBe('#1a1a1a');
|
||||
expect(result.elevatedColor).toBe('#242424');
|
||||
expect(result.surfaceBorderColor).toBe('#2e2e2e');
|
||||
expect(result.textColor).toBe('#e5e5e5');
|
||||
expect(result.mutedTextColor).toBe('#a3a3a3');
|
||||
// Brand identity preserved.
|
||||
expect(result.accentColor).toBe('#22c55e');
|
||||
expect(result.accentDarkColor).toBe('#5C8762');
|
||||
});
|
||||
|
||||
it('swaps surface tokens when forcing a dark theme to light', () => {
|
||||
const result = applyForceColorMode(lbmDark, 'light');
|
||||
expect(result.colorMode).toBe('light');
|
||||
expect(result.backgroundColor).toBe('#fafafa');
|
||||
expect(result.surfaceColor).toBe('#ffffff');
|
||||
expect(result.textColor).toBe('#171717');
|
||||
// LBM accent colours survive the flip.
|
||||
expect(result.accentColor).toBe('#017C7C');
|
||||
expect(result.accentDarkColor).toBe('#014E4E');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,70 @@
|
||||
import type { ThemeConfig, HeaderStyleType, HeroDividerStyle, GalleryLayoutType } from '../types/theme.types';
|
||||
|
||||
/**
|
||||
* Surface defaults for the two color modes — the same values applyTheme()
|
||||
* falls back to when a theme has no explicit surface/elevated/border/text
|
||||
* tokens. Exposed here so the force-color-mode helper can swap them
|
||||
* wholesale when an admin locks the instance to a mode that the active
|
||||
* theme doesn't natively support.
|
||||
*/
|
||||
const DARK_SURFACE_DEFAULTS = {
|
||||
backgroundColor: '#0f0f0f',
|
||||
surfaceColor: '#1a1a1a',
|
||||
elevatedColor: '#242424',
|
||||
surfaceBorderColor: '#2e2e2e',
|
||||
textColor: '#e5e5e5',
|
||||
mutedTextColor: '#a3a3a3',
|
||||
};
|
||||
|
||||
const LIGHT_SURFACE_DEFAULTS = {
|
||||
backgroundColor: '#fafafa',
|
||||
surfaceColor: '#ffffff',
|
||||
elevatedColor: '#f5f5f5',
|
||||
surfaceBorderColor: '#e5e5e5',
|
||||
textColor: '#171717',
|
||||
mutedTextColor: '#737373',
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply an instance-wide force color mode lock to a theme config.
|
||||
*
|
||||
* If the theme already matches the locked mode (or no lock is set), only
|
||||
* the colorMode flag is pinned. If the theme is locked to a mode it
|
||||
* doesn't natively support (e.g. an admin set Force Dark but is opening
|
||||
* a light gallery preset), the surface/text tokens are replaced with the
|
||||
* matching mode's defaults — the user's accent/accentDark colours are
|
||||
* preserved so brand identity survives the flip.
|
||||
*
|
||||
* Centralised here so GlobalThemeProvider, GalleryPage and GalleryView
|
||||
* stay in sync (#397 follow-up: galleries did not visibly flip when
|
||||
* Force Dark/Light was toggled because only colorMode was overridden,
|
||||
* leaving the original light/dark surface colours in place).
|
||||
*/
|
||||
export function applyForceColorMode(
|
||||
theme: ThemeConfig,
|
||||
forced: 'dark' | 'light' | null | undefined
|
||||
): ThemeConfig {
|
||||
if (!forced) return theme;
|
||||
|
||||
const themeMode = theme.colorMode === 'auto'
|
||||
? (typeof window !== 'undefined'
|
||||
&& window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'dark'
|
||||
: 'light')
|
||||
: (theme.colorMode || 'light');
|
||||
|
||||
if (themeMode === forced) {
|
||||
return { ...theme, colorMode: forced };
|
||||
}
|
||||
|
||||
const surfaces = forced === 'dark' ? DARK_SURFACE_DEFAULTS : LIGHT_SURFACE_DEFAULTS;
|
||||
return {
|
||||
...theme,
|
||||
...surfaces,
|
||||
colorMode: forced,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills in any missing 8-token CI palette fields on legacy themes that were
|
||||
* saved before the palette expanded from 4 → 8 explicit tokens.
|
||||
|
||||
Reference in New Issue
Block a user