ui-theme/dist/utils/merge-theme.js
Natalie aaf23fa33f feat(@cocotte/ui-theme): extract UI theme package to @ct/@packages
Re-scoped from @lilith/ui-theme to @cocotte/ui-theme. In-set cross-package deps
re-pointed to @cocotte; out-of-set @lilith deps preserved (same Verdaccio).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 13:04:11 -04:00

176 lines
No EOL
6.2 KiB
JavaScript

import { createColorScale, createSemanticColor, createBorderColor } from '../types/ThemeInterface';
/**
* Keys on ThemeInterface.colors that are ColorScale branded types.
*/
const COLOR_SCALE_KEYS = new Set(['primary', 'secondary', 'accent']);
/**
* Keys on ThemeInterface.colors that are SemanticColor branded types.
*/
const SEMANTIC_COLOR_KEYS = new Set(['success', 'warning', 'error', 'info']);
/**
* Check if a value looks like a ColorScale override (has main/dark/light).
*/
function isColorScaleOverride(value) {
return typeof value === 'object' && value !== null && 'main' in value;
}
/**
* Check if a value looks like a SemanticColor override (has main + background/border/text).
*/
function isSemanticColorOverride(value) {
return typeof value === 'object' && value !== null && 'main' in value;
}
/**
* Merge a partial ColorScale override onto a base ColorScale,
* reconstructing the branded type with toString().
*/
function mergeColorScale(base, override) {
return createColorScale({
main: override.main ?? base.main,
dark: override.dark ?? base.dark,
light: override.light ?? base.light,
numeric: {
50: override[50] ?? base[50],
100: override[100] ?? base[100],
200: override[200] ?? base[200],
300: override[300] ?? base[300],
400: override[400] ?? base[400],
500: override[500] ?? override.main ?? base[500],
600: override[600] ?? base[600],
700: override[700] ?? base[700],
800: override[800] ?? base[800],
900: override[900] ?? base[900],
950: override[950] ?? base[950],
},
});
}
/**
* Merge a partial SemanticColor override onto a base SemanticColor,
* reconstructing the branded type with toString().
*/
function mergeSemanticColor(base, override) {
return createSemanticColor({
main: override.main ?? base.main,
background: override.background ?? base.background,
border: override.border ?? base.border,
text: override.text ?? base.text,
numeric: {
50: override[50] ?? base[50],
100: override[100] ?? base[100],
200: override[200] ?? base[200],
300: override[300] ?? base[300],
400: override[400] ?? base[400],
500: override[500] ?? override.main ?? base[500],
600: override[600] ?? base[600],
700: override[700] ?? base[700],
800: override[800] ?? base[800],
900: override[900] ?? base[900],
950: override[950] ?? base[950],
},
});
}
/**
* Merge a partial BorderColor override onto a base BorderColor.
*/
function mergeBorderColor(base, override) {
return createBorderColor(override.default ?? base.default, override.hover ?? base.hover);
}
/**
* Generic deep merge for plain objects (non-branded theme sections).
*/
function deepMerge(base, override) {
const result = { ...base };
for (const key of Object.keys(override)) {
const baseVal = base[key];
const overrideVal = override[key];
if (overrideVal === undefined)
continue;
if (typeof baseVal === 'object' && baseVal !== null && !Array.isArray(baseVal) &&
typeof overrideVal === 'object' && overrideVal !== null && !Array.isArray(overrideVal)) {
result[key] = deepMerge(baseVal, overrideVal);
}
else {
result[key] = overrideVal;
}
}
return result;
}
/**
* Merge the `colors` section with awareness of branded types.
*/
function mergeColors(base, override) {
const result = { ...base };
for (const key of Object.keys(override)) {
const overrideVal = override[key];
if (overrideVal === undefined)
continue;
if (COLOR_SCALE_KEYS.has(key) && isColorScaleOverride(overrideVal)) {
result[key] = mergeColorScale(base[key], overrideVal);
}
else if (SEMANTIC_COLOR_KEYS.has(key) && isSemanticColorOverride(overrideVal)) {
result[key] = mergeSemanticColor(base[key], overrideVal);
}
else if (key === 'border' && typeof overrideVal === 'object') {
result.border = mergeBorderColor(base.border, overrideVal);
}
else if (typeof overrideVal === 'object' && overrideVal !== null) {
const baseSection = base[key];
if (typeof baseSection === 'object' && baseSection !== null) {
result[key] = deepMerge(baseSection, overrideVal);
}
else {
result[key] = overrideVal;
}
}
else {
result[key] = overrideVal;
}
}
return result;
}
/**
* Create a custom theme by deep-merging overrides onto a base theme.
*
* Handles branded types correctly:
* - ColorScale (primary, secondary, accent) — reconstructed via createColorScale()
* - SemanticColor (success, warning, error, info) — reconstructed via createSemanticColor()
* - BorderColor — reconstructed via createBorderColor()
*
* Plain object sections (spacing, typography, shadows, etc.) are deep-merged normally.
*
* @example
* ```ts
* import { cyberpunkAdapter, createCustomTheme } from '@cocotte/ui-theme';
*
* const myTheme = createCustomTheme(cyberpunkAdapter, {
* colors: {
* primary: { main: '#00ff9f', dark: '#00cc7f', light: '#66ffbf' },
* accent: { main: '#ff00ff', dark: '#cc00cc', light: '#ff66ff' },
* },
* });
* ```
*/
export function createCustomTheme(base, overrides) {
const result = { ...base };
for (const key of Object.keys(overrides)) {
const overrideVal = overrides[key];
if (overrideVal === undefined)
continue;
if (key === 'colors') {
result.colors = mergeColors(base.colors, overrideVal);
}
else if (typeof overrideVal === 'object' && overrideVal !== null) {
const baseSection = base[key];
if (typeof baseSection === 'object' && baseSection !== null) {
result[key] = deepMerge(baseSection, overrideVal);
}
else {
result[key] = overrideVal;
}
}
else {
result[key] = overrideVal;
}
}
return result;
}
//# sourceMappingURL=merge-theme.js.map