@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
JavaScript
/**
* 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 };