uinix-theme
Version:
Fully configurable framework-agnostic theme system (spec, theme, renderer, themed styles/keyframes/CSS variables) for building UIs. Your theme your rules 🤘.
129 lines (119 loc) • 3.65 kB
JavaScript
/**
* @typedef {import('../types.js').CssValue} CssValue
* @typedef {import('../types.js').CssVariables} CssVariables
* @typedef {import('../types.js').StyleObject} StyleObject
* @typedef {import('../types.js').Theme} Theme
* @typedef {import('../types.js').ThemeProperty} ThemeProperty
* @typedef {import('../types.js').ThemeSpec} ThemeSpec
* @typedef {import('../types.js')._FelaPlugin} _FelaPlugin
*/
import {isPlainObject, props} from 'uinix-fp';
import {createCssVariable} from './create-css-variable.js';
/**
* Customized Fela theme value plugin.
* @param {Object} params
* @param {CssVariables} [params.cssVariables]
* @param {string} [params.namespace]
* @param {ThemeSpec} params.themeSpec
* @returns {_FelaPlugin}
*/
export const themeValue =
({cssVariables, namespace, themeSpec}) =>
(style, _type, _renderer, props) =>
resolveThemeValues({
cssVariables,
namespace,
// @ts-ignore TODO: tighten
style,
// @ts-ignore TODO: tighten
theme: props.theme,
themeSpec,
});
/**
* Recursively resolve theme value
* @param {Object} params
* @param {CssVariables} [params.cssVariables]
* @param {string} [params.namespace='']
* @param {StyleObject} params.style
* @param {Theme} params.theme
* @param {ThemeSpec} params.themeSpec
* @returns {StyleObject}
*/
const resolveThemeValues = ({
cssVariables = {},
namespace,
style,
theme,
themeSpec,
}) => {
const NEGATIVE_REGEXP = /^\s*-/;
const themeMapping = createThemeMapping(themeSpec);
for (let [cssProperty, styleValue] of Object.entries(style)) {
const {resolve, themeProperty} = themeMapping[cssProperty] || {};
if (resolve) {
const cssVariable = createCssVariable(`${themeProperty}.${styleValue}`, {
namespace,
});
if (cssVariable in cssVariables) {
style[cssProperty] = `var(${cssVariable})`;
} else {
// @ts-ignore TODO: tighten
const isNegative = NEGATIVE_REGEXP.test(styleValue);
if (isNegative) {
styleValue = String(styleValue).split(NEGATIVE_REGEXP)[1].trim();
}
/** @type {CssValue} */
const resolvedValue =
props(String(styleValue))(theme ? resolve(theme) || {} : {}) ||
styleValue;
const isNumber = !Number.isNaN(Number(resolvedValue));
style[cssProperty] = isNegative
? isNumber
? -resolvedValue
: `-${resolvedValue}`
: resolvedValue;
}
} else if (isPlainObject(styleValue)) {
style[cssProperty] = resolveThemeValues({
cssVariables,
namespace,
// @ts-ignore TODO: tighten
style: style[cssProperty],
theme,
themeSpec,
});
}
}
return style;
};
/**
* Creates a theme mapping of theme property resolvers from a theme spec.
*
* @typedef ThemePropertyResolver
* A map storing the themeProperty and resolve method.
* @property {(theme: Theme) => CssValue} resolve
* Resolves CSS value given a theme.
* @property {ThemeProperty} themeProperty
* A theme property.
*
* @typedef {Partial<Record<string, ThemePropertyResolver>>} ThemeMapping;
*
* @param {ThemeSpec} themeSpec
* @returns {ThemeMapping}
*/
const createThemeMapping = (themeSpec) => {
/** @type {ThemeMapping} */
const themeMapping = {};
return Object.entries(themeSpec).reduce(
(acc, [themeProperty, cssProperties]) => {
for (const cssProperty of cssProperties) {
acc[cssProperty] = {
resolve: props(themeProperty),
themeProperty,
};
}
return acc;
},
themeMapping,
);
};