UNPKG

@hakuna-matata-ui/styled-system

Version:

Style function for css-in-js building component libraries

151 lines (123 loc) 4.34 kB
import { Dict, isCssVar, isObject, isString, mergeWith as merge, runIfFn, } from "@hakuna-matata-ui/utils" import * as CSS from "csstype" import { pseudoSelectors } from "./pseudos" import { systemProps as systemPropConfigs } from "./system" import { StyleObjectOrFn } from "./system.types" import { expandResponsive } from "./utils/expand-responsive" import { Config, PropConfig } from "./utils/prop-config" import { CssTheme } from "./utils/types" const isCSSVariableTokenValue = (key: string, value: any): value is string => key.startsWith("--") && isString(value) && !isCssVar(value) const resolveTokenValue = (theme: Dict, value: string) => { if (value == null) return value const getVar = (val: string) => theme.__cssMap?.[val]?.varRef const getValue = (val: string) => getVar(val) ?? val const valueSplit = value.split(",").map((v) => v.trim()) const [tokenValue, fallbackValue] = valueSplit value = getVar(tokenValue) ?? getValue(fallbackValue) ?? getValue(value) return value } interface GetCSSOptions { theme: CssTheme configs?: Config pseudos?: Record<string, CSS.Pseudos | (string & {})> } export function getCss(options: GetCSSOptions) { const { configs = {}, pseudos = {}, theme } = options const css = (stylesOrFn: Dict, nested = false) => { const _styles = runIfFn(stylesOrFn, theme) const styles = expandResponsive(_styles)(theme) let computedStyles: Dict = {} for (let key in styles) { const valueOrFn = styles[key] /** * allows the user to pass functional values * boxShadow: theme => `0 2px 2px ${theme.colors.red}` */ let value = runIfFn(valueOrFn, theme) /** * converts pseudo shorthands to valid selector * "_hover" => "&:hover" */ if (key in pseudos) { key = pseudos[key] } /** * allows the user to use theme tokens in css vars * { --banner-height: "sizes.md" } => { --banner-height: "var(--chakra-sizes-md)" } * * You can also provide fallback values * { --banner-height: "sizes.no-exist, 40px" } => { --banner-height: "40px" } */ if (isCSSVariableTokenValue(key, value)) { value = resolveTokenValue(theme, value) } let config = configs[key] if (config === true) { config = { property: key } as PropConfig } if (isObject(value)) { computedStyles[key] = computedStyles[key] ?? {} computedStyles[key] = merge({}, computedStyles[key], css(value, true)) continue } let rawValue = config?.transform?.(value, theme, _styles) ?? value /** * Used for `layerStyle`, `textStyle` and `apply`. After getting the * styles in the theme, we need to process them since they might * contain theme tokens. * * `processResult` is the config property we pass to `layerStyle`, `textStyle` and `apply` */ rawValue = config?.processResult ? css(rawValue, true) : rawValue /** * allows us define css properties for RTL and LTR. * * const marginStart = { * property: theme => theme.direction === "rtl" ? "marginRight": "marginLeft", * } */ const configProperty = runIfFn(config?.property, theme) if (!nested && config?.static) { const staticStyles = runIfFn(config.static, theme) computedStyles = merge({}, computedStyles, staticStyles) } if (configProperty && Array.isArray(configProperty)) { for (const property of configProperty) { computedStyles[property] = rawValue } continue } if (configProperty) { if (configProperty === "&" && isObject(rawValue)) { computedStyles = merge({}, computedStyles, rawValue) } else { computedStyles[configProperty as string] = rawValue } continue } if (isObject(rawValue)) { computedStyles = merge({}, computedStyles, rawValue) continue } computedStyles[key] = rawValue } return computedStyles } return css } export const css = (styles: StyleObjectOrFn) => (theme: any) => { const cssFn = getCss({ theme, pseudos: pseudoSelectors, configs: systemPropConfigs, }) return cssFn(styles) }