UNPKG

@theme-ui/css

Version:

Theme UI CSS lets you write style objects with responsive, theme-aware ergonomic shortcuts. This package powers the `sx` prop in Theme UI.

356 lines (319 loc) 9.96 kB
/** * Allows for nested scales with shorthand values * @example * { * colors: { * primary: { __default: '#00f', light: '#33f' } * } * } * css({ color: 'primary' }); // { color: '#00f' } * css({ color: 'primary.light' }) // { color: '#33f' } */ const THEME_UI_DEFAULT_KEY = '__default'; const hasDefault = x => { return typeof x === 'object' && x !== null && THEME_UI_DEFAULT_KEY in x; }; /** * Extracts value under path from a deeply nested object. * Used for Themes, variants and Theme UI style objects. * Given a path to object with `__default` key, returns the value under that key. * * @param obj a theme, variant or style object * @param path path separated with dots (`.`) * @param fallback default value returned if get(obj, path) is not found */ function get(obj, path, fallback, p, undef) { const pathArray = path && typeof path === 'string' ? path.split('.') : [path]; for (p = 0; p < pathArray.length; p++) { obj = obj ? obj[pathArray[p]] : undef; } if (obj === undef) return fallback; return hasDefault(obj) ? obj[THEME_UI_DEFAULT_KEY] : obj; } const getObjectWithVariants = (obj, theme) => { if (obj && obj['variant']) { let result = {}; for (const key in obj) { const x = obj[key]; if (key === 'variant') { const val = typeof x === 'function' ? x(theme) : x; const variant = getObjectWithVariants(get(theme, val), theme); result = { ...result, ...variant }; } else { result[key] = x; } } return result; } return obj; }; const defaultBreakpoints = [40, 52, 64].map(n => n + 'em'); const defaultTheme = { space: [0, 4, 8, 16, 32, 64, 128, 256, 512], fontSizes: [12, 14, 16, 20, 24, 32, 48, 64, 72] }; const aliases = { bg: 'backgroundColor', m: 'margin', mt: 'marginTop', mr: 'marginRight', mb: 'marginBottom', ml: 'marginLeft', mx: 'marginX', my: 'marginY', p: 'padding', pt: 'paddingTop', pr: 'paddingRight', pb: 'paddingBottom', pl: 'paddingLeft', px: 'paddingX', py: 'paddingY' }; const multiples = { marginX: ['marginLeft', 'marginRight'], marginY: ['marginTop', 'marginBottom'], paddingX: ['paddingLeft', 'paddingRight'], paddingY: ['paddingTop', 'paddingBottom'], scrollMarginX: ['scrollMarginLeft', 'scrollMarginRight'], scrollMarginY: ['scrollMarginTop', 'scrollMarginBottom'], scrollPaddingX: ['scrollPaddingLeft', 'scrollPaddingRight'], scrollPaddingY: ['scrollPaddingTop', 'scrollPaddingBottom'], size: ['width', 'height'] }; const scales = { color: 'colors', backgroundColor: 'colors', borderColor: 'colors', caretColor: 'colors', columnRuleColor: 'colors', textDecorationColor: 'colors', opacity: 'opacities', transition: 'transitions', margin: 'space', marginTop: 'space', marginRight: 'space', marginBottom: 'space', marginLeft: 'space', marginX: 'space', marginY: 'space', marginBlock: 'space', marginBlockEnd: 'space', marginBlockStart: 'space', marginInline: 'space', marginInlineEnd: 'space', marginInlineStart: 'space', padding: 'space', paddingTop: 'space', paddingRight: 'space', paddingBottom: 'space', paddingLeft: 'space', paddingX: 'space', paddingY: 'space', paddingBlock: 'space', paddingBlockEnd: 'space', paddingBlockStart: 'space', paddingInline: 'space', paddingInlineEnd: 'space', paddingInlineStart: 'space', scrollMargin: 'space', scrollMarginTop: 'space', scrollMarginRight: 'space', scrollMarginBottom: 'space', scrollMarginLeft: 'space', scrollMarginX: 'space', scrollMarginY: 'space', scrollPadding: 'space', scrollPaddingTop: 'space', scrollPaddingRight: 'space', scrollPaddingBottom: 'space', scrollPaddingLeft: 'space', scrollPaddingX: 'space', scrollPaddingY: 'space', inset: 'space', insetBlock: 'space', insetBlockEnd: 'space', insetBlockStart: 'space', insetInline: 'space', insetInlineEnd: 'space', insetInlineStart: 'space', top: 'space', right: 'space', bottom: 'space', left: 'space', gridGap: 'space', gridColumnGap: 'space', gridRowGap: 'space', gap: 'space', columnGap: 'space', rowGap: 'space', fontFamily: 'fonts', fontSize: 'fontSizes', fontWeight: 'fontWeights', lineHeight: 'lineHeights', letterSpacing: 'letterSpacings', border: 'borders', borderTop: 'borders', borderRight: 'borders', borderBottom: 'borders', borderLeft: 'borders', borderWidth: 'borderWidths', borderStyle: 'borderStyles', borderRadius: 'radii', borderTopRightRadius: 'radii', borderTopLeftRadius: 'radii', borderBottomRightRadius: 'radii', borderBottomLeftRadius: 'radii', borderTopWidth: 'borderWidths', borderTopColor: 'colors', borderTopStyle: 'borderStyles', borderBottomWidth: 'borderWidths', borderBottomColor: 'colors', borderBottomStyle: 'borderStyles', borderLeftWidth: 'borderWidths', borderLeftColor: 'colors', borderLeftStyle: 'borderStyles', borderRightWidth: 'borderWidths', borderRightColor: 'colors', borderRightStyle: 'borderStyles', borderBlock: 'borders', borderBlockColor: 'colors', borderBlockEnd: 'borders', borderBlockEndColor: 'colors', borderBlockEndStyle: 'borderStyles', borderBlockEndWidth: 'borderWidths', borderBlockStart: 'borders', borderBlockStartColor: 'colors', borderBlockStartStyle: 'borderStyles', borderBlockStartWidth: 'borderWidths', borderBlockStyle: 'borderStyles', borderBlockWidth: 'borderWidths', borderEndEndRadius: 'radii', borderEndStartRadius: 'radii', borderInline: 'borders', borderInlineColor: 'colors', borderInlineEnd: 'borders', borderInlineEndColor: 'colors', borderInlineEndStyle: 'borderStyles', borderInlineEndWidth: 'borderWidths', borderInlineStart: 'borders', borderInlineStartColor: 'colors', borderInlineStartStyle: 'borderStyles', borderInlineStartWidth: 'borderWidths', borderInlineStyle: 'borderStyles', borderInlineWidth: 'borderWidths', borderStartEndRadius: 'radii', borderStartStartRadius: 'radii', columnRuleWidth: 'borderWidths', outlineColor: 'colors', boxShadow: 'shadows', textShadow: 'shadows', zIndex: 'zIndices', width: 'sizes', minWidth: 'sizes', maxWidth: 'sizes', height: 'sizes', minHeight: 'sizes', maxHeight: 'sizes', flexBasis: 'sizes', size: 'sizes', blockSize: 'sizes', inlineSize: 'sizes', maxBlockSize: 'sizes', maxInlineSize: 'sizes', minBlockSize: 'sizes', minInlineSize: 'sizes', columnWidth: 'sizes', // svg fill: 'colors', stroke: 'colors' }; const positiveOrNegative = (scale, value) => { if (typeof value !== 'number' || value >= 0) { if (typeof value === 'string' && value.startsWith('-')) { const valueWithoutMinus = value.substring(1); const n = get(scale, valueWithoutMinus, valueWithoutMinus); if (typeof n === 'number') { return n * -1; } return `-${n}`; } return get(scale, value, value); } const absolute = Math.abs(value); const n = get(scale, absolute, absolute); if (typeof n === 'string') return '-' + n; return Number(n) * -1; }; const transforms = ['margin', 'marginTop', 'marginRight', 'marginBottom', 'marginLeft', 'marginX', 'marginY', 'marginBlock', 'marginBlockEnd', 'marginBlockStart', 'marginInline', 'marginInlineEnd', 'marginInlineStart', 'top', 'bottom', 'left', 'right'].reduce((acc, curr) => ({ ...acc, [curr]: positiveOrNegative }), {}); const responsive = styles => theme => { const next = {}; const breakpoints = theme && theme.breakpoints || defaultBreakpoints; const mediaQueries = [null, ...breakpoints.map(n => n.includes('@media') ? n : `@media screen and (min-width: ${n})`)]; for (const k in styles) { const key = k; let value = styles[key]; if (typeof value === 'function') { value = value(theme || {}); } if (value === false || value == null) { continue; } if (!Array.isArray(value)) { next[key] = value; continue; } for (let i = 0; i < value.slice(0, mediaQueries.length).length; i++) { const media = mediaQueries[i]; if (!media) { next[key] = value[i]; continue; } next[media] = next[media] || {}; if (value[i] == null) continue; next[media][key] = value[i]; } } return next; }; const css = (args = {}) => (props = {}) => { const theme = { ...defaultTheme, ...('theme' in props ? props.theme : props) }; // insert variant props before responsive styles, so they can be merged // we need to maintain order of the style props, so if a variant is place in the middle // of other props, it will extends its props at that same location order. const obj = getObjectWithVariants(typeof args === 'function' ? args(theme) : args, theme); const styles = responsive(obj)(theme); let result = {}; for (const key in styles) { const x = styles[key]; const val = typeof x === 'function' ? x(theme) : x; if (val && typeof val === 'object') { if (hasDefault(val)) { result[key] = val[THEME_UI_DEFAULT_KEY]; continue; } // On type level, val can also be an array here, // but we transform all arrays in `responsive` function. result[key] = css(val)(theme); continue; } const prop = key in aliases ? aliases[key] : key; const scaleName = prop in scales ? scales[prop] : undefined; const scale = scaleName ? theme == null ? void 0 : theme[scaleName] : get(theme, prop, {}); const transform = get(transforms, prop, get); const value = transform(scale, val, val); if (prop in multiples) { const dirs = multiples[prop]; for (let i = 0; i < dirs.length; i++) { result[dirs[i]] = value; } } else { result[prop] = value; } } return result; }; export { THEME_UI_DEFAULT_KEY, css, defaultBreakpoints, get, getObjectWithVariants, multiples, scales };