UNPKG

@aesthetic/system

Version:

Web based building blocks for the Aesthetic design system.

120 lines (98 loc) 2.88 kB
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any */ import { ColorScheme, ContrastLevel, VariablesMap } from '@aesthetic/types'; import { deepMerge, hyphenate, isObject, objectLoop } from '@aesthetic/utils'; import type { Design } from './Design'; import { MIXIN_MAP } from './mixins'; import { DeepPartial, MixinType, ThemeOptions, ThemeTokens, Tokens, Utilities, VariableName, } from './types'; export class Theme<Block extends object> implements Utilities<Block> { name: string = ''; readonly contrast: ContrastLevel; readonly design: Design<Block>; readonly scheme: ColorScheme; readonly tokens: Tokens; private cssVariables?: VariablesMap; constructor(options: ThemeOptions, tokens: ThemeTokens, design: Design<Block>) { this.contrast = options.contrast; this.scheme = options.scheme; this.design = design; this.tokens = { ...design.tokens, ...tokens }; } /** * Extend and instantiate a new theme instance with customized tokens. */ extend(tokens: DeepPartial<ThemeTokens>, options: Partial<ThemeOptions> = {}): Theme<Block> { return new Theme( { contrast: this.contrast, scheme: this.scheme, ...options, }, deepMerge(this.tokens, tokens), this.design, ); } /** * Return both design and theme tokens as a mapping of CSS variables. */ toVariables(): VariablesMap { if (this.cssVariables) { return this.cssVariables; } const vars: VariablesMap = {}; const collapseTree = (data: Record<string, any>, path: string[]) => { objectLoop(data, (value, key) => { const nextPath = [...path, hyphenate(key)]; if (isObject(value)) { collapseTree(value, nextPath); } else { vars[`--${nextPath.join('-')}`] = value; } }); }; collapseTree(this.tokens, []); this.cssVariables = vars; return this.cssVariables; } /** * Return merged CSS properties from the defined mixin, all template overrides, * and the provided additional CSS properties. */ mixin = (name: MixinType, rule?: Block): Block => { const properties: object = {}; const mixin = MIXIN_MAP[name]; if (mixin) { Object.assign(properties, mixin(this)); } else if (__DEV__) { throw new Error(`Unknown mixin "${name}".`); } if (rule) { return deepMerge(properties, rule); } return properties as Block; }; /** * Return a `rem` unit equivalent for the current spacing type and unit. */ unit = (...multipliers: number[]): string => multipliers .map( (m) => `${((this.design.spacingUnit * m) / this.design.rootTextSize) .toFixed(2) .replace('.00', '')}rem`, ) .join(' '); /** * Return a CSS variable declaration with the defined name and fallbacks. */ var = (name: VariableName, ...fallbacks: (number | string)[]): string => `var(${[`--${name}`, ...fallbacks].join(', ')})`; }