UNPKG

@aesthetic/core

Version:

Aesthetic is a powerful type-safe, framework agnostic, CSS-in-JS library for styling components.

523 lines (405 loc) 14.7 kB
// Bundled with Packemon: https://packemon.dev // Platform: browser, Support: stable, Format: esm import { ThemeRegistry } from '@aesthetic/system'; export * from '@aesthetic/system'; import { objectLoop, deepMerge, arrayLoop, toArray, isObject, isDOM } from '@aesthetic/utils'; export * from '@aesthetic/types'; function createCacheKey(name, params) { let key = name; // Since all other values are scalars, we can just join the values. // This is 3x faster than JSON.stringify(), and 1.5x faster than Object.values()! objectLoop(params, value => { key += String(value); }); return key; } class Sheet { constructor(factory, renderer) { this.factory = void 0; this.renderer = void 0; this.cache = {}; this.factory = this.validateFactory(factory); this.renderer = renderer; } compose(params) { return this.factory; } render(engine, theme, baseParams) { const params = { contrast: theme.contrast, deterministic: false, direction: 'ltr', scheme: theme.scheme, theme: theme.name, unit: 'px', vendor: false, ...baseParams }; const key = createCacheKey(engine.name, params); const cache = this.cache[key]; if (cache) { return cache; } const composer = this.compose(params); const result = this.renderer(engine, composer(theme), params); this.cache[key] = result; return result; } validateFactory(factory) { if (process.env.NODE_ENV !== "production") { const typeOf = typeof factory; if (typeOf !== 'function') { throw new TypeError(`A style sheet factory function is required, found "${typeOf}".`); } } return factory; } } class FeatureSheet extends Sheet { constructor(...args) { super(...args); this.contrastOverrides = {}; this.overrides = []; this.schemeOverrides = {}; this.themeOverrides = {}; } addColorSchemeOverride(scheme, factory) { var _this$schemeOverrides; if (process.env.NODE_ENV !== "production" && scheme !== 'light' && scheme !== 'dark') { throw new Error('Color scheme override must be one of "light" or "dark".'); } ((_this$schemeOverrides = this.schemeOverrides)[scheme] || (_this$schemeOverrides[scheme] = [])).push(this.validateFactory(factory)); return this; } addContrastOverride(contrast, factory) { var _this$contrastOverrid; if (process.env.NODE_ENV !== "production" && contrast !== 'normal' && contrast !== 'high' && contrast !== 'low') { throw new Error('Contrast level override must be one of "high", "low", or "normal".'); } ((_this$contrastOverrid = this.contrastOverrides)[contrast] || (_this$contrastOverrid[contrast] = [])).push(this.validateFactory(factory)); return this; } addOverride(factory) { this.overrides.push(this.validateFactory(factory)); return this; } addThemeOverride(theme, factory) { var _this$themeOverrides; ((_this$themeOverrides = this.themeOverrides)[theme] || (_this$themeOverrides[theme] = [])).push(this.validateFactory(factory)); return this; } compose(params) { const factories = [this.factory, ...this.overrides]; if (params.scheme && this.schemeOverrides[params.scheme]) { factories.push(...this.schemeOverrides[params.scheme]); } if (params.contrast && this.contrastOverrides[params.contrast]) { factories.push(...this.contrastOverrides[params.contrast]); } if (params.theme && this.themeOverrides[params.theme]) { factories.push(...this.themeOverrides[params.theme]); } if (factories.length === 1) { return factories[0]; } const composer = utils => deepMerge(...factories.map(factory => factory(utils))); return composer; } } function renderTheme(engine, theme, options) { const resultSheet = { root: {} }; objectLoop(theme['@font-face'], (fontFaces, fontFamily) => { arrayLoop(toArray(fontFaces), fontFace => engine.renderFontFace({ ...fontFace, fontFamily }, options)); }); arrayLoop(toArray(theme['@import']), importPath => engine.renderImport(importPath, options)); objectLoop(theme['@keyframes'], (keyframes, animationName) => { engine.renderKeyframes(keyframes, animationName, options); }); if (theme['@root']) { resultSheet.root.result = engine.renderRuleGrouped(theme['@root'], { ...options, type: 'global' }).result; } if (theme['@variables']) { engine.setRootVariables(theme['@variables']); } return resultSheet; } function renderComponent(engine, styles, options) { const resultSheet = {}; const rankings = {}; objectLoop(styles, (style, selector) => { // eslint-disable-next-line no-multi-assign const meta = resultSheet[selector] || (resultSheet[selector] = {}); // At-rule if (selector.startsWith('@')) { if (process.env.NODE_ENV !== "production") { throw new SyntaxError(`At-rules may not be defined at the root of a style sheet, found "${selector}".`); } // Rule } else if (isObject(style)) { const result = engine.renderRule(style, { ...options, debugName: selector, rankings }); meta.result = result.result; if (result.variants.length > 0) { const types = new Set(); arrayLoop(result.variants, variant => { arrayLoop(variant.types, type => { types.add(type.split(':')[0]); }); }); meta.variants = result.variants; meta.variantTypes = types; } // Unknown } else if (process.env.NODE_ENV !== "production") { throw new Error(`Invalid rule for "${selector}". Must be an object (style declaration) or string (class name).`); } }); return resultSheet; } /* eslint-disable no-param-reassign, unicorn/prefer-prototype-methods */ class Aesthetic { constructor(_options) { this.activeDirection = void 0; this.activeTheme = void 0; this.globalSheetRegistry = new Map(); this.listeners = new Map(); this.options = {}; this.styleEngine = void 0; this.themeRegistry = new ThemeRegistry(); this.changeDirection = (direction, propagate = true) => { if (direction === this.activeDirection) { return; } const engine = this.getEngine(); // Set the active direction this.activeDirection = direction; // Update engine direction engine.direction = direction; engine.setDirection(direction); // Let consumers know about the change if (propagate) { this.emit('change:direction', [direction]); } }; this.changeTheme = (name, propagate = true) => { if (name === this.activeTheme) { return; } const theme = this.getTheme(name); const engine = this.getEngine(); // Set the active theme this.activeTheme = name; // Set root variables if (this.options.rootVariables) { engine.setRootVariables(theme.toVariables()); } // Render theme styles const results = this.renderThemeSheet(theme); // Update engine theme engine.setTheme(results); // Let consumers know about the change if (propagate) { this.emit('change:theme', [name, results]); } }; this.configure = options => { Object.assign(this.options, options); // Configure the engine with itself to reapply options if (this.styleEngine) { this.configureEngine(this.styleEngine); } }; this.configureEngine = engine => { const { options } = this; if (options.customProperties) { engine.customProperties = options.customProperties; } if (options.directionConverter) { engine.directionConverter = options.directionConverter; } if (options.defaultUnit) { engine.unitSuffixer = options.defaultUnit; } if (options.vendorPrefixer) { engine.vendorPrefixer = options.vendorPrefixer; } this.styleEngine = engine; }; this.createStyleSheet = factory => new FeatureSheet(factory, renderComponent); this.createScopedStyleSheet = (factory, selector) => this.createStyleSheet(utils => ({ [selector ?? 'element']: typeof factory === 'function' ? factory(utils) : factory })); this.createThemeSheet = factory => new Sheet(factory, renderTheme); this.generateResults = (args, variants, renderResult) => { const output = []; arrayLoop(args, arg => { if (!arg) { return; } // Custom results being passes if (Array.isArray(arg)) { output.push(...arg.filter(Boolean)); return; } const item = renderResult[arg]; if (item?.result) { output.push(item?.result); } arrayLoop(item?.variants, ({ types, result }) => { if (types.every(type => variants.has(type))) { output.push(result); } }); }); return output; }; this.getActiveDirection = () => { if (this.activeDirection) { return this.activeDirection; } // Detect theme from engine const direction = this.getEngine().direction || 'ltr'; this.changeDirection(direction); return direction; }; this.getActiveTheme = () => { if (this.activeTheme) { return this.getTheme(this.activeTheme); } // Detect theme from browser preferences const engine = this.getEngine(); const theme = this.themeRegistry.getPreferredTheme({ matchColorScheme: engine.prefersColorScheme, matchContrastLevel: engine.prefersContrastLevel }); this.changeTheme(theme.name); return theme; }; this.getEngine = () => { const current = typeof global !== 'undefined' && global.AESTHETIC_CUSTOM_ENGINE || this.styleEngine; if (current) { return current; } throw new Error('No style engine defined. Have you configured one with `configureEngine()`?'); }; this.getListeners = type => { const set = this.listeners.get(type) ?? new Set(); this.listeners.set(type, set); return set; }; this.getTheme = name => this.themeRegistry.getTheme(name); this.mergeStyleSheets = (...sheets) => { if (sheets.length === 1) { return sheets[0]; } const mergedSheet = new FeatureSheet(sheets[0].factory, sheets[0].renderer); arrayLoop(sheets, (sheet, index) => { if (index > 0) { mergedSheet.addOverride(sheet.factory); } // @ts-expect-error Allow access arrayLoop(sheet.overrides, override => { mergedSheet.addOverride(override); }); // @ts-expect-error Allow access objectLoop(sheet.contrastOverrides, (overrides, contrast) => { arrayLoop(overrides, override => { mergedSheet.addContrastOverride(contrast, override); }); }); // @ts-expect-error Allow access objectLoop(sheet.schemeOverrides, (overrides, scheme) => { arrayLoop(overrides, override => { mergedSheet.addColorSchemeOverride(scheme, override); }); }); // @ts-expect-error Allow access objectLoop(sheet.themeOverrides, (overrides, theme) => { arrayLoop(overrides, override => { mergedSheet.addThemeOverride(theme, override); }); }); }); return mergedSheet; }; this.registerTheme = (name, theme, sheet = null, isDefault = false) => { this.themeRegistry.register(name, theme, isDefault); if (sheet) { if (process.env.NODE_ENV !== "production" && !(sheet instanceof Sheet)) { throw new TypeError('Rendering theme styles require a `Sheet` instance.'); } this.globalSheetRegistry.set(name, sheet); } }; this.registerDefaultTheme = (name, theme, sheet = null) => { this.registerTheme(name, theme, sheet, true); }; this.renderFontFace = (fontFace, fontFamily, params) => this.getEngine().renderFontFace({ fontFamily, ...fontFace }, params); this.renderImport = (path, params) => this.getEngine().renderImport(path, params); this.renderKeyframes = (keyframes, animationName, params) => this.getEngine().renderKeyframes(keyframes, animationName, params); this.renderStyles = rule => this.getEngine().renderRule(rule, this.getRenderOptions()); this.renderStyleSheet = (sheet, params = {}) => { if (process.env.NODE_ENV !== "production" && !(sheet instanceof Sheet)) { throw new TypeError('Rendering component styles require a `Sheet` instance.'); } return this.renderSheet(sheet, params.theme ? this.getTheme(params.theme) : this.getActiveTheme(), params); }; this.renderThemeSheet = (theme, params = {}) => { const sheet = this.globalSheetRegistry.get(theme.name); const results = []; // Render theme CSS variables if (isDOM()) { results.push(this.getEngine().renderRuleGrouped({ '@variables': theme.toVariables() }, { type: 'global' }).result); } // Render theme styles if (sheet) { const result = this.renderSheet(sheet, theme, params).root?.result; if (result) { results.push(result); } } return results; }; if (_options) { this.configure(_options); } // Must be bound manually because of method overloading this.emit = this.emit.bind(this); this.subscribe = this.subscribe.bind(this); this.unsubscribe = this.unsubscribe.bind(this); } /** * Change the active direction. */ emit(type, args) { this.getListeners(type).forEach(listener => { listener(...args); }); } /** * Generate a list of results using the selectors of a style sheet. * If a set is provided, it will be used to check for variants. */ subscribe(type, listener) { this.getListeners(type).add(listener); return () => { this.unsubscribe(type, listener); }; } /** * Unsubscribe from an event by name. */ unsubscribe(type, listener) { this.getListeners(type).delete(listener); } getRenderOptions(options) { return { deterministic: !!this.options.deterministicClasses, direction: this.getActiveDirection(), vendor: !!this.options.vendorPrefixer, ...options }; } renderSheet(sheet, theme, params) { return sheet.render(this.getEngine(), theme, this.getRenderOptions(params)); } } export { Aesthetic, FeatureSheet, Sheet }; //# sourceMappingURL=index.js.map