UNPKG

@reusable-ui/themable

Version:
363 lines (292 loc) 11.8 kB
// cssfn: import { // cssfn general types: Factory, // cssfn css specific types: CssRule, CssStyleCollection, CssSelector, CssClassName, // writes css in javascript: rule, variants, style, vars, startsCapitalized, // strongly typed of css variables: CssVars, cssVars, // writes complex stylesheets in simpler way: memoizeResult, memoizeStyle, memoizeStyleWithVariants, } from '@cssfn/core' // writes css in javascript // reusable-ui configs: import { // configs: colors, themes, cssColorConfig, } from '@reusable-ui/colors' // a color management system // defaults: const _defaultTheme : Required<ThemableProps>['theme'] = 'inherit' // hooks: // variants: //#region themable export type ThemeName = (keyof typeof themes) | (string & {}) export interface ThemableVars { /** * themed background color. */ backg : any /** * themed foreground color. */ foreg : any /** * themed border color. */ border : any /** * themed alternate background color. */ altBackg : any /** * themed alternate foreground color. */ altForeg : any /** * themed foreground color - at outlined variant. */ foregOutlined : any /** * themed alternate background color - at outlined variant. */ altBackgOutlined : any /** * themed alternate foreground color - at outlined variant. */ altForegOutlined : any /** * themed background color - at mild variant. */ backgMild : any /** * themed foreground color - at mild variant. */ foregMild : any /** * themed alternate background color - at mild variant. */ altBackgMild : any /** * themed alternate foreground color - at mild variant. */ altForegMild : any /** * themed ring color. */ ring : any /** * conditional background color. */ backgCond : any /** * conditional foreground color. */ foregCond : any /** * conditional border color. */ borderCond : any /** * conditional alternate background color. */ altBackgCond : any /** * conditional alternate foreground color. */ altForegCond : any /** * conditional foreground color - at outlined variant. */ foregOutlinedCond : any /** * conditional alternate background color - at outlined variant. */ altBackgOutlinedCond : any /** * conditional alternate foreground color - at outlined variant. */ altForegOutlinedCond : any /** * conditional background color - at mild variant. */ backgMildCond : any /** * conditional foreground color - at mild variant. */ foregMildCond : any /** * conditional alternate background color - at mild variant. */ altBackgMildCond : any /** * conditional alternate foreground color - at mild variant. */ altForegMildCond : any /** * conditional ring color. */ ringCond : any } const [themableVars] = cssVars<ThemableVars>({ prefix: 'th', minify: false }); // shared variables: ensures the server-side & client-side have the same generated css variable names //#region caches const themeClassesCache = new Map<ThemeName, CssClassName|null>(); export const createThemeClass = (themeName: ThemeName): CssClassName|null => { const cached = themeClassesCache.get(themeName); if (cached !== undefined) return cached; // null is allowed if (themeName === 'inherit') { themeClassesCache.set(themeName, null); return null; } // if const themeClass = `th${startsCapitalized(themeName)}`; themeClassesCache.set(themeName, themeClass); return themeClass; }; const themeSelectorsCache = new Map<ThemeName, CssSelector|null>(); export const createThemeSelector = (themeName: ThemeName): CssSelector|null => { const cached = themeSelectorsCache.get(themeName); if (cached) return cached; const themeClass = createThemeClass(themeName); if (themeClass === null) return null; const themeRule : CssSelector = `.${themeClass}`; themeSelectorsCache.set(themeName, themeRule); return themeRule; }; let hasThemeSelectorsCache : CssSelector[] | undefined = undefined; let noThemeSelectorsCache : CssSelector | undefined = undefined; cssColorConfig.onChange.subscribe(() => { themeClassesCache.clear(); themeSelectorsCache.clear(); hasThemeSelectorsCache = undefined; noThemeSelectorsCache = undefined; }); //#endregion caches export const ifTheme = (themeName: ThemeName, styles: CssStyleCollection): CssRule => rule(createThemeSelector(themeName), styles); export const ifHasTheme = (styles: CssStyleCollection): CssRule => { return rule( hasThemeSelectorsCache ?? (hasThemeSelectorsCache = ( Object.keys(themes) .map((themeName) => createThemeSelector(themeName)) .filter((selector): selector is CssSelector => (selector !== null)) )) , styles ); }; export const ifNoTheme = (styles: CssStyleCollection): CssRule => { return rule( noThemeSelectorsCache ?? (noThemeSelectorsCache = (`:not(:is(${ Object.keys(themes) .map((themeName) => createThemeSelector(themeName)) .filter((selector): selector is CssSelector => (selector !== null)) .join(', ') }))`)) , styles ); }; export interface ThemableStuff { themableRule: Factory<CssRule>, themableVars: CssVars<ThemableVars> } const createThemableRule = (themeDefinition : ((themeName: ThemeName) => CssStyleCollection) = defineThemeRule, options : ThemeName[] = themeOptions()): CssRule => { return style({ ...variants([ options.map((themeName) => ifTheme(themeName, themeDefinition(themeName) ) ), ]), }); }; const getDefaultThemableRule = memoizeStyle(() => createThemableRule(), cssColorConfig.onChange); /** * Uses theme (color) options. * For example: `primary`, `success`, `danger`. * @param themeDefinition A callback to create a theme rules for each theme color in `options`. * @param options Defines all available theme color options. * @returns A `ThemableStuff` represents the theme rules for each theme color in `options`. */ export const usesThemable = (themeDefinition : ((themeName: ThemeName) => CssStyleCollection) = defineThemeRule, options : ThemeName[] = themeOptions()): ThemableStuff => { return { themableRule: ( ((themeDefinition === defineThemeRule) && (options === themeOptions())) ? getDefaultThemableRule : () => createThemableRule(themeDefinition, options) ), themableVars, }; }; /** * Defines a theme rules for the given `themeName`. * @param themeName The theme name. * @returns A `CssRule` represents a theme rules for the given `themeName`. */ export const defineThemeRule = memoizeStyleWithVariants((themeName: ThemeName): CssRule => { return style({ ...vars({ [themableVars.backg ] : colors[ themeName as keyof typeof colors], // base color [themableVars.foreg ] : colors[`${themeName}Text` as keyof typeof colors], // light on dark base color | dark on light base color [themableVars.border ] : colors[`${themeName}Bold` as keyof typeof colors], // 20% base color + 80% page's foreground [themableVars.altBackg ] : themableVars.backgMild, [themableVars.altForeg ] : themableVars.foregMild, [themableVars.foregOutlined ] : themableVars.backg, [themableVars.altBackgOutlined ] : themableVars.backg, [themableVars.altForegOutlined ] : themableVars.foreg, [themableVars.backgMild ] : colors[`${themeName}Mild` as keyof typeof colors], // 20% base color + 80% page's background [themableVars.foregMild ] : themableVars.border, [themableVars.altBackgMild ] : themableVars.backg, [themableVars.altForegMild ] : themableVars.foreg, [themableVars.ring ] : colors[`${themeName}Thin` as keyof typeof colors], // 50% transparency of base color }), }); }, cssColorConfig.onChange); /** * Gets all available theme color options. * @returns A `ThemeName[]` represents all available theme color options. */ export const themeOptions = memoizeResult((): ThemeName[] => { return (Object.keys(themes) as ThemeName[]); }, cssColorConfig.onChange); /** * Creates an conditional theme color rules for the given `themeName`. * @param themeName The theme name as the conditional theme color -or- `null` for undefining the conditional. * @returns A `CssRule` represents an conditional theme color rules for the given `themeName`. */ export const usesThemeConditional = (themeName: ThemeName|null): CssRule => style({ ...vars({ [themableVars.backgCond ] : !themeName ? null : colors[ themeName as keyof typeof colors], // base color [themableVars.foregCond ] : !themeName ? null : colors[`${themeName}Text` as keyof typeof colors], // light on dark base color | dark on light base color [themableVars.borderCond ] : !themeName ? null : colors[`${themeName}Bold` as keyof typeof colors], // 20% base color + 80% page's foreground [themableVars.altBackgCond ] : themableVars.backgMildCond, [themableVars.altForegCond ] : themableVars.foregMildCond, [themableVars.foregOutlinedCond ] : !themeName ? null : themableVars.backgCond, [themableVars.altBackgOutlinedCond] : themableVars.backgCond, [themableVars.altForegOutlinedCond] : themableVars.foregCond, [themableVars.backgMildCond ] : !themeName ? null : colors[`${themeName}Mild` as keyof typeof colors], // 20% base color + 80% page's background [themableVars.foregMildCond ] : !themeName ? null : themableVars.borderCond, [themableVars.altBackgMildCond ] : themableVars.backgCond, [themableVars.altForegMildCond ] : themableVars.foregCond, [themableVars.ringCond ] : !themeName ? null : colors[`${themeName}Thin` as keyof typeof colors], // 50% transparency of base color }), }); export interface ThemableProps { // variants: theme ?: ThemeName|'inherit' } export const useThemable = ({theme = _defaultTheme}: ThemableProps) => ({ class: (theme === 'inherit') ? null : createThemeClass(theme), }); //#endregion themable