@spark-web/theme
Version:
--- title: Theme isExperimentalPackage: true ---
749 lines (683 loc) • 21.3 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var polished = require('polished');
var react = require('react');
var _objectSpread = require('@babel/runtime/helpers/objectSpread2');
var _objectWithoutProperties = require('@babel/runtime/helpers/objectWithoutProperties');
var core = require('@capsizecss/core');
var mapValues = require('lodash/mapValues');
var _defineProperty = require('@babel/runtime/helpers/defineProperty');
var _typeof = require('@babel/runtime/helpers/typeof');
var facepaint = require('facepaint');
var omit = require('lodash/omit');
function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
var mapValues__default = /*#__PURE__*/_interopDefault(mapValues);
var facepaint__default = /*#__PURE__*/_interopDefault(facepaint);
var omit__default = /*#__PURE__*/_interopDefault(omit);
var smoothSaturation = function smoothSaturation(saturation, luminance) {
var isBright = luminance > 0.6;
if (isBright) {
return saturation * 0.8;
}
return saturation * 0.45;
};
var smoothLightness = function smoothLightness(lightness, luminance) {
var isBright = luminance > 0.6;
if (isBright) {
return 0.95 - lightness * 0.03;
}
return 0.95 - lightness * 0.06;
};
function getLightVariant(color) {
var _parseToHsl3 = polished.parseToHsl(color),
hue = _parseToHsl3.hue,
saturation = _parseToHsl3.saturation,
lightness = _parseToHsl3.lightness;
var luminance = polished.getLuminance(color);
return polished.toColorString({
hue: hue,
saturation: smoothSaturation(saturation, luminance),
lightness: smoothLightness(lightness, luminance)
});
}
var isLight = function isLight(inputColor) {
var _parseToRgb = polished.parseToRgb(inputColor),
red = _parseToRgb.red,
green = _parseToRgb.green,
blue = _parseToRgb.blue; // Convert RGB to YIQ to better take into account the
// luminance of the separate color channels:
//
// Further reading:
// - YIQ:
// https://en.wikipedia.org/wiki/YIQ
// - Calculating contrast:
// https://24ways.org/2010/calculating-color-contrast/
var yiq = (red * 299 + green * 587 + blue * 114) / 1000; // Colour is considered `light` if greater than the midpoint:
// eg. 256 / 2 = 128.
return yiq >= 128;
};
var breakpointNames = ['mobile', 'tablet', 'desktop', 'wide'];
var breakpoints = {
mobile: 0,
tablet: 740,
desktop: 992,
wide: 1200
};
/**
* Utilities related to responsive props. Emotion's
* [facepaint](https://github.com/emotion-js/facepaint) ultimately generates
* media queries for the resolved styles.
*/
var makeThemeUtils = function makeThemeUtils() {
// NOTE: the `mobile` key is used to represent "below tablet" in certain
// cases, but it SHOULD NOT create a media query: facepaint will apply the
// first property in the array without a query.
var validBreakpoints = Object.values(breakpointQuery);
return {
mapResponsiveProp: mapResponsiveProp,
mapResponsiveScale: mapResponsiveScale,
optimizeResponsiveArray: optimizeResponsiveArray,
responsiveRange: responsiveRange,
responsiveStyles: responsiveStyles,
resolveResponsiveProps: facepaint__default["default"](validBreakpoints)
};
}; // Responsive props
// ------------------------------
/**
* Helper for mapping keys/breakpoint map to a theme scale e.g.
*
* @example
* mapResponsiveProp('small', { small: 8, large: 16 }) // 8
* mapResponsiveProp(
* { mobile:'small', tablet:'large' },
* { small: 8, large: 16 }
* ) // [8,16]
*/
var mapResponsiveScale = function mapResponsiveScale(value, scaleDefinition) {
if (value === undefined) {
return value;
}
if (_typeof(value) === 'object') {
return resolveResponsiveScale(value, scaleDefinition);
}
return scaleDefinition[value];
};
function resolveResponsiveScale(value, scaleDefinition) {
var valueArray = [];
for (var i = 0; i < breakpointNames.length; i++) {
var breakpoint = breakpointNames[i];
var keyForBreakpoint = value[breakpoint]; // NOTE: media queries are applied by index (facepaint). nullish value
// ensures array length always matches breakpoints
valueArray.push(keyForBreakpoint ? scaleDefinition[keyForBreakpoint] : null);
}
return valueArray;
}
var mapResponsiveProp = function mapResponsiveProp(value) {
if (_typeof(value) === 'object') {
return resolveResponsiveProp(value);
}
return value;
};
function resolveResponsiveProp(value) {
var valueArray = [];
for (var i = 0; i < breakpointNames.length; i++) {
var _value$breakpoint;
var breakpoint = breakpointNames[i];
valueArray.push((_value$breakpoint = value[breakpoint]) !== null && _value$breakpoint !== void 0 ? _value$breakpoint : null);
}
return valueArray;
}
function createResponsiveMapFn(lookupMap) {
return function mapResponsiveValue(prop) {
if (typeof prop == 'undefined') {
return prop;
}
if (_typeof(prop) === 'object') {
var mobile = prop.mobile,
tablet = prop.tablet,
desktop = prop.desktop,
wide = prop.wide;
return {
mobile: mobile ? lookupMap[mobile] : undefined,
tablet: tablet ? lookupMap[tablet] : undefined,
desktop: desktop ? lookupMap[desktop] : undefined,
wide: wide ? lookupMap[wide] : undefined
};
}
return lookupMap[prop];
};
} // Responsive range
// ------------------------------
var responsiveRange = function responsiveRange(props) {
var above = props.above,
below = props.below;
if (!above && !below) {
return [false, false, false, false];
}
var startIndex = above ? breakpointNames.indexOf(above) + 1 : 0;
var endIndex = below ? breakpointNames.indexOf(below) - 1 : breakpointNames.length - 1;
var range = breakpointNames.slice(startIndex, endIndex + 1);
var includeMobile = range.indexOf('mobile') >= 0;
var includeTablet = range.indexOf('tablet') >= 0;
var includeDesktop = range.indexOf('desktop') >= 0;
var includeWide = range.indexOf('wide') >= 0;
return [includeMobile, includeTablet, includeDesktop, includeWide];
};
var optimizeResponsiveArray = function optimizeResponsiveArray(value) {
var _values$, _values$2, _values$3, _values$4;
var lastValue;
var values = value.map(function (v) {
if (v !== lastValue && v !== null) {
lastValue = v;
return v;
}
return null;
});
return [(_values$ = values[0]) !== null && _values$ !== void 0 ? _values$ : null, (_values$2 = values[1]) !== null && _values$2 !== void 0 ? _values$2 : null, (_values$3 = values[2]) !== null && _values$3 !== void 0 ? _values$3 : null, (_values$4 = values[3]) !== null && _values$4 !== void 0 ? _values$4 : null];
}; // ==============================
// Experiment
// ==============================
var breakpointQuery = mapValues__default["default"](omit__default["default"](breakpoints, 'mobile'), function (bp) {
return "@media screen and (min-width: ".concat(bp, "px)");
});
var makeMediaQuery = function makeMediaQuery(breakpoint) {
return function (styles) {
return !styles || Object.keys(styles).length === 0 ? {} : _defineProperty({}, breakpointQuery[breakpoint], styles);
};
};
var mediaQuery = {
tablet: makeMediaQuery('tablet'),
desktop: makeMediaQuery('desktop'),
wide: makeMediaQuery('wide')
};
var responsiveStyles = function responsiveStyles(_ref2) {
var mobile = _ref2.mobile,
tablet = _ref2.tablet,
desktop = _ref2.desktop,
wide = _ref2.wide;
return _objectSpread(_objectSpread({}, mobile), tablet || desktop || wide ? _objectSpread(_objectSpread(_objectSpread({}, mediaQuery.tablet(tablet !== null && tablet !== void 0 ? tablet : {})), mediaQuery.desktop(desktop !== null && desktop !== void 0 ? desktop : {})), mediaQuery.wide(wide !== null && wide !== void 0 ? wide : {})) : {});
};
var _excluded = ["fontSize", "lineHeight"],
_excluded2 = ["border", "color", "sizing", "spacing", "typography"];
/**
* Afford consumers the simplicity of pixel values for token declarations while
* supporting our users' browser preferences.
*/
function pxToRem(value) {
var px = typeof value === 'string' ? parseFloat(value) : value; // NOTE: assume default browser settings of 16px root
var modifier = 1 / 16;
return "".concat(px * modifier, "rem");
}
/**
* Calculate leading and trim styles using
* [Capsize](https://seek-oss.github.io/capsize/) to ensure vertical spacing
* around text elements behaves as expected.
*/
function fontSizeToCapHeight(definition, fontMetrics) {
var rowHeight = 4; // TODO: move to theme?
var capHeight = core.getCapHeight({
fontSize: definition.fontSize,
fontMetrics: fontMetrics
});
var _precomputeValues = core.precomputeValues({
fontSize: definition.fontSize,
leading: definition.rows * rowHeight,
fontMetrics: fontMetrics
}),
fontSize = _precomputeValues.fontSize,
lineHeight = _precomputeValues.lineHeight,
trims = _objectWithoutProperties(_precomputeValues, _excluded);
return {
fontSize: pxToRem(fontSize),
lineHeight: pxToRem(lineHeight),
capHeight: pxToRem(capHeight),
trims: trims
};
}
function responsiveTypography(definition, fontMetrics) {
var mobile = definition.mobile,
tablet = definition.tablet;
return {
mobile: fontSizeToCapHeight(mobile, fontMetrics),
tablet: fontSizeToCapHeight(tablet, fontMetrics)
};
}
/**
* Apply "capsized" style rules to the typographic tokens, making them available
* in the theme.
*/
function decorateTypography(typography) {
var heading = typography.heading,
text = typography.text,
fontFamily = typography.fontFamily;
return _objectSpread(_objectSpread({}, typography), {}, {
heading: _objectSpread(_objectSpread({}, heading), {}, {
level: _objectSpread({}, mapValues__default["default"](heading.level, function (definition) {
return responsiveTypography(definition, fontFamily.display.fontMetrics);
}))
}),
text: _objectSpread(_objectSpread({}, text), mapValues__default["default"](text, function (definition) {
return responsiveTypography(definition, fontFamily.sans.fontMetrics);
}))
});
}
function decorateTokens(tokens) {
var border = tokens.border,
color = tokens.color,
sizing = tokens.sizing,
spacing = tokens.spacing,
typography = tokens.typography,
restTokens = _objectWithoutProperties(tokens, _excluded2);
var decoratedTokens = _objectSpread({
breakpoint: breakpoints,
border: _objectSpread(_objectSpread({}, border), {}, {
radius: _objectSpread(_objectSpread({}, border.radius), {}, {
full: 9999 // prefer over percentage to avoid ovals for irregular rectangles
})
}),
color: _objectSpread(_objectSpread({}, color), {}, {
background: _objectSpread(_objectSpread({}, color.background), {}, {
infoLight: getLightVariant(color.background.info),
criticalLight: getLightVariant(color.background.critical),
positiveLight: getLightVariant(color.background.positive),
cautionLight: getLightVariant(color.background.caution)
})
}),
spacing: _objectSpread(_objectSpread({}, spacing), {}, {
none: 0
}),
sizing: _objectSpread(_objectSpread({}, sizing), {}, {
none: 0,
full: '100%'
}),
typography: decorateTypography(typography)
}, restTokens);
return decoratedTokens;
} // Export
// ------------------------------
function makeBrighteTheme(tokens) {
var decoratedTokens = decorateTokens(tokens);
return _objectSpread(_objectSpread({}, decoratedTokens), {}, {
backgroundLightness: mapValues__default["default"](decoratedTokens.color.background, function (background) {
return isLight(background) ? 'light' : 'dark';
}),
utils: makeThemeUtils()
});
}
var white = '#ffffff';
var colors = {
neutral: {
'0': white,
'50': '#fafcfe',
'100': '#f1f4fb',
'200': '#dce1ec',
'300': '#c7cedb',
'500': '#98a2b8',
'600': '#646f84',
'700': '#1a2a3a'
},
primary: {
'0': white,
'50': '#f5fdf9',
'100': '#edfaf5',
'200': '#c8eada',
'300': '#9acbb8',
'500': '#00c28d',
'600': '#00a87b',
'700': '#108663'
},
secondary: {
'0': white,
'50': '#fef5eb',
'100': '#fff0e0',
'500': '#ffbb66',
'600': '#ffaa44',
'700': '#e58f27'
},
green: {
'0': white,
'50': '#f6fbf8',
'100': '#f0f9f1',
'200': '#cde9d2',
'300': '#b1dab9',
'500': '#1e9c65',
'600': '#2c855d',
'700': '#327e59'
},
blue: {
'0': white,
'50': '#f6fafd',
'100': '#f3f8fc',
'200': '#d0e4ff',
'300': '#b7d2f4',
'500': '#2b8aed',
'600': '#0677d6',
'700': '#106fb8'
},
red: {
'0': white,
'50': '#fef8f8',
'100': '#fff4f4',
'200': '#ffdad7',
'300': '#fec1b5',
'500': '#f53841',
'600': '#e61e32',
'700': '#c81b0e'
},
yellow: {
'0': white,
'50': '#fefaf6',
'100': '#fff5eb',
'200': '#fdddc4',
'300': '#face9b',
'500': '#ffaa44',
'600': '#be5c1c',
'700': '#ad541a'
}
};
// the design team, but the shape shouldn't change too much.
var aesteticoFontMetrics = {
capHeight: 666,
ascent: 980,
descent: -340,
lineGap: 0,
unitsPerEm: 1000
}; // Typography
// ------------------------------
// Tokens
// ------------------------------
var defaultTokens = {
name: 'Brighte web: light',
// tweak for breakpoints
typography: {
fontFamily: {
sans: {
fontMetrics: aesteticoFontMetrics,
name: '"Aestetico", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'
},
display: {
fontMetrics: aesteticoFontMetrics,
name: '"Aestetico", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'
}
},
fontWeight: {
regular: 400,
semibold: 600
},
heading: {
level: {
'1': {
mobile: {
fontSize: 35,
rows: 9
},
tablet: {
fontSize: 35,
rows: 11
}
},
'2': {
mobile: {
fontSize: 23,
rows: 8
},
tablet: {
fontSize: 23,
rows: 9
}
},
'3': {
mobile: {
fontSize: 19,
rows: 6
},
tablet: {
fontSize: 19,
rows: 7
}
},
'4': {
mobile: {
fontSize: 17,
rows: 5
},
tablet: {
fontSize: 17,
rows: 7
}
}
}
},
text: {
xsmall: {
mobile: {
fontSize: 13,
rows: 5
},
tablet: {
fontSize: 13,
rows: 5
}
},
small: {
mobile: {
fontSize: 15,
rows: 5
},
tablet: {
fontSize: 15,
rows: 5
}
},
standard: {
mobile: {
fontSize: 17,
rows: 6
},
tablet: {
fontSize: 17,
rows: 6
}
},
large: {
mobile: {
fontSize: 19,
rows: 7
},
tablet: {
fontSize: 19,
rows: 7
}
}
}
},
border: {
radius: {
small: 4,
medium: 8,
large: 16
},
width: {
standard: 1,
large: 2
},
color: {
neutral: colors.neutral[700],
standard: colors.neutral[200],
standardInverted: colors.neutral[0],
field: colors.neutral[200],
fieldHover: colors.neutral[300],
fieldAccent: colors.primary[500],
fieldDisabled: colors.neutral[300],
// tones
primary: colors.primary[500],
primaryHover: colors.primary[600],
primaryActive: colors.primary[700],
secondary: colors.secondary[500],
secondaryHover: colors.secondary[600],
secondaryActive: colors.secondary[700],
accent: '#8b5cf6',
accentMuted: '#997dd8',
caution: colors.yellow[600],
cautionMuted: colors.yellow[500],
critical: colors.red[600],
criticalMuted: colors.red[500],
info: colors.blue[600],
infoMuted: colors.blue[500],
positive: colors.green[600],
positiveMuted: colors.green[500]
}
},
color: {
foreground: {
neutral: colors.neutral[700],
neutralInverted: colors.neutral[0],
muted: colors.neutral[600],
mutedInverted: 'hsla(0, 0%, 100%, 0.75)',
link: colors.primary[600],
disabled: colors.neutral[500],
fieldAccent: colors.primary[500],
placeholder: colors.neutral[600],
// tones
accent: '#8b5cf6',
primary: colors.primary[500],
primaryHover: colors.primary[600],
primaryActive: colors.primary[700],
secondary: colors.secondary[500],
secondaryHover: colors.secondary[600],
secondaryActive: colors.secondary[700],
caution: colors.yellow[700],
critical: colors.red[700],
info: colors.blue[700],
positive: colors.green[700]
},
background: {
muted: colors.neutral[600],
disabled: colors.neutral[500],
backdrop: 'hsla(0, 0%, 0%, 0.4)',
body: colors.neutral[50],
surface: colors.primary[0],
surfaceMuted: colors.neutral[50],
surfacePressed: colors.neutral[300],
fieldAccent: colors.neutral[700],
input: colors.primary[0],
inputPressed: colors.neutral[100],
inputDisabled: colors.neutral[50],
// tones
accent: '#8b5cf6',
accentMuted: '#f7f5ff',
neutral: colors.neutral[0],
neutralLow: colors.neutral[50],
primary: colors.primary[600],
primaryLow: colors.primary[100],
primaryMuted: colors.primary[50],
secondary: colors.secondary[600],
secondaryLow: colors.secondary[100],
secondaryMuted: colors.secondary[50],
caution: colors.yellow[600],
cautionLow: colors.yellow[100],
cautionMuted: colors.yellow[50],
critical: colors.red[600],
criticalLow: colors.red[100],
criticalMuted: colors.red[50],
info: colors.blue[600],
infoLow: colors.blue[100],
infoMuted: colors.blue[50],
positive: colors.green[600],
positiveLow: colors.green[100],
positiveMuted: colors.green[50]
},
status: {
// tones for statuses can be either foreground or background
accent: '#8b5cf6',
caution: colors.yellow[500],
critical: colors.red[500],
info: colors.blue[500],
neutral: colors.neutral[700],
positive: colors.green[500]
}
},
backgroundInteractions: {
none: colors.neutral[0],
primaryActive: colors.primary[700],
primaryHover: colors.primary[500],
primaryLowHover: colors.primary[100],
primaryLowActive: colors.primary[200],
secondaryActive: colors.secondary[700],
secondaryHover: colors.secondary[500],
secondaryLowHover: colors.yellow[100],
secondaryLowActive: colors.yellow[200],
neutralHover: colors.neutral[100],
neutralActive: colors.neutral[200],
neutralLowHover: colors.neutral[100],
neutralLowActive: colors.neutral[200],
cautionLowHover: colors.yellow[200],
cautionLowActive: colors.yellow[300],
criticalActive: colors.red[700],
criticalHover: colors.red[500],
criticalLowHover: colors.red[200],
criticalLowActive: colors.red[300],
infoLowHover: colors.blue[200],
infoLowActive: colors.blue[300],
positiveHover: colors.green[500],
positiveActive: colors.green[700],
positiveLowHover: colors.green[200],
positiveLowActive: colors.green[300]
},
// misc
contentWidth: {
xsmall: 400,
small: 660,
medium: 940,
large: 1280,
xlarge: 1400
},
elevation: {
dropdownBlanket: 90,
dropdown: 100,
sticky: 200,
modalBlanket: 290,
modal: 300,
notification: 400
},
spacing: {
xxsmall: 2,
xsmall: 4,
small: 8,
medium: 12,
large: 16,
xlarge: 24,
xxlarge: 32
},
sizing: {
xxsmall: 16,
xsmall: 24,
small: 32,
medium: 44,
large: 56
},
shadow: {
small: '0 1px 2px rgba(0, 0, 0, 0.05)',
medium: '0 2px 8px rgba(0, 0, 0, 0.04)',
large: '0 6px 12px rgba(0, 0, 0, 0.1)'
},
animation: {
standard: {
duration: 300,
easing: 'cubic-bezier(0.2, 0, 0, 1)'
}
}
};
var defaultTheme = makeBrighteTheme(defaultTokens);
var ThemeContext = /*#__PURE__*/react.createContext(defaultTheme);
var ThemeProvider = ThemeContext.Provider;
var useTheme = function useTheme() {
return react.useContext(ThemeContext);
};
exports.ThemeProvider = ThemeProvider;
exports.createResponsiveMapFn = createResponsiveMapFn;
exports.defaultTheme = defaultTheme;
exports.defaultTokens = defaultTokens;
exports.isLight = isLight;
exports.makeBrighteTheme = makeBrighteTheme;
exports.useTheme = useTheme;