├── .nvmrc ├── .gitattributes ├── .gitignore ├── test ├── snapshots │ ├── box.tsx.snap │ └── box.tsx.md ├── _setup.js ├── value-to-string.ts ├── utils │ ├── decamelize.ts │ ├── split-props.ts │ ├── regex.ts │ ├── is-production.ts │ ├── split-box-props.ts │ └── flatten-object.ts ├── get-safe-value.ts ├── styles.ts ├── prefixer.ts ├── cache.ts ├── expand-aliases.ts ├── get-class-name.ts ├── keyframes.ts ├── index.tsx ├── box.tsx ├── get-css.ts └── enhance-props.ts ├── src ├── utils │ ├── is-production.ts │ ├── regex.ts │ ├── decamelize.ts │ ├── flatten-object.ts │ ├── split-box-props.ts │ ├── split-props.ts │ ├── safeHref.ts │ └── style-sheet.ts ├── @types │ └── @emotion │ │ └── hash │ │ └── index.d.ts ├── value-to-string.ts ├── enhancers │ ├── selectors.ts │ ├── resize.ts │ ├── outline.ts │ ├── box-shadow.ts │ ├── opacity.ts │ ├── transform.ts │ ├── overflow.ts │ ├── interaction.ts │ ├── list.ts │ ├── position.ts │ ├── transition.ts │ ├── dimensions.ts │ ├── svg.ts │ ├── border-radius.ts │ ├── layout.ts │ ├── background.ts │ ├── animation.ts │ ├── index.ts │ ├── spacing.ts │ ├── flex.ts │ ├── text.ts │ ├── borders.ts │ └── grid.ts ├── get-safe-value.ts ├── styles.ts ├── cache.ts ├── prefixer.ts ├── expand-aliases.ts ├── index.tsx ├── get-class-name.ts ├── types │ ├── keyframes.ts │ ├── box-types.ts │ └── enhancers.ts ├── box.tsx ├── get-css.ts ├── enhance-props.ts └── keyframes.ts ├── tools ├── benchmarks │ └── box.ts ├── fixtures │ ├── selector-uniquness-story.tsx │ ├── keyframes-story.tsx │ └── selectors-story.tsx ├── all-properties-component.tsx └── box.stories.tsx ├── .storybook ├── config.js └── webpack.config.js ├── .editorconfig ├── tsconfig.json ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── logo.svg ├── package.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 12 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /dist/ 3 | /.out/ 4 | .DS_Store 5 | *.log 6 | /.nyc_output/ 7 | /coverage/ 8 | -------------------------------------------------------------------------------- /test/snapshots/box.tsx.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segmentio/ui-box/HEAD/test/snapshots/box.tsx.snap -------------------------------------------------------------------------------- /src/utils/is-production.ts: -------------------------------------------------------------------------------- 1 | const isProduction = (): boolean => process.env.NODE_ENV === 'production' 2 | 3 | export default isProduction 4 | -------------------------------------------------------------------------------- /test/_setup.js: -------------------------------------------------------------------------------- 1 | import {configure} from 'enzyme' 2 | import Adapter from 'enzyme-adapter-react-16' 3 | 4 | configure({adapter: new Adapter()}) 5 | -------------------------------------------------------------------------------- /src/@types/@emotion/hash/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@emotion/hash' { 2 | function murmurhash(value: string): string 3 | export default murmurhash 4 | } 5 | -------------------------------------------------------------------------------- /src/utils/regex.ts: -------------------------------------------------------------------------------- 1 | export const spacesOutsideParentheses = / (?=([^()]*\([^()]*\))*[^()]*$)/g 2 | export const unsafeClassNameCharacters = /[^_a-zA-Z0-9-]/g 3 | -------------------------------------------------------------------------------- /tools/benchmarks/box.ts: -------------------------------------------------------------------------------- 1 | import allPropertiesComponent from '../all-properties-component' 2 | 3 | export default function benchmark() { 4 | return allPropertiesComponent() 5 | } 6 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@storybook/react' 2 | 3 | function loadStories() { 4 | require('../tools/box.stories') 5 | } 6 | 7 | configure(loadStories, module) 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /src/value-to-string.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts number values to a string with a unit. 3 | */ 4 | export default function valueToString(value: string | number, unit = 'px'): string { 5 | return typeof value === 'number' ? `${value}${unit}` : value 6 | } 7 | -------------------------------------------------------------------------------- /test/value-to-string.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import valueToString from '../src/value-to-string' 3 | 4 | test('converts numbers to string', t => { 5 | t.is(valueToString(50.5), '50.5px') 6 | }) 7 | 8 | test('allows changing the default unit', t => { 9 | t.is(valueToString(0.5, ''), '0.5') 10 | }) 11 | -------------------------------------------------------------------------------- /test/utils/decamelize.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import decamelize from '../../src/utils/decamelize' 3 | 4 | test('decamelizes', t => { 5 | t.is(decamelize('userSelect'), 'user-select') 6 | }) 7 | 8 | test('handles starting capital', t => { 9 | t.is(decamelize('WebkitUserSelect'), 'webkit-user-select') 10 | }) 11 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ config }) => { 2 | config.module.rules.push({ 3 | test: /\.(ts|tsx)$/, 4 | use: [ 5 | { 6 | loader: require.resolve('awesome-typescript-loader') 7 | }, 8 | { 9 | loader: require.resolve('react-docgen-typescript-loader') 10 | } 11 | ] 12 | }) 13 | config.resolve.extensions.push('.ts', '.tsx') 14 | return config 15 | } 16 | -------------------------------------------------------------------------------- /src/enhancers/selectors.ts: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import { PropValidators, PropEnhancers, PropTypesMapping, PropAliases } from '../types/enhancers' 3 | 4 | export const propTypes: PropTypesMapping = { 5 | selectors: PropTypes.object 6 | } 7 | 8 | export const propAliases: PropAliases = {} 9 | 10 | export const propValidators: PropValidators = {} 11 | 12 | export const propEnhancers: PropEnhancers = {} 13 | -------------------------------------------------------------------------------- /src/get-safe-value.ts: -------------------------------------------------------------------------------- 1 | import {unsafeClassNameCharacters} from './utils/regex' 2 | 3 | const dashRegex = /[ .]/g 4 | const percentRegex = /%/g 5 | 6 | /** 7 | * Makes the value safe for use in a class name. 8 | */ 9 | export default function getSafeValue(value: string): string { 10 | return value 11 | .replace(dashRegex, '-') 12 | .replace(percentRegex, 'prcnt') 13 | .replace(unsafeClassNameCharacters, '') 14 | } 15 | -------------------------------------------------------------------------------- /test/utils/split-props.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import splitProps from '../../src/utils/split-props' 3 | 4 | test('splits props', t => { 5 | const props = { 6 | background: 'red', 7 | color: 'blue' 8 | } 9 | const keys = ['background'] 10 | t.deepEqual(splitProps(props, keys), { 11 | matchedProps: { 12 | background: 'red' 13 | }, 14 | remainingProps: { 15 | color: 'blue' 16 | } 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /src/utils/decamelize.ts: -------------------------------------------------------------------------------- 1 | // Fork of https://github.com/sindresorhus/decamelize because it contains ES6 code 2 | // And css properties don't contain unicode 3 | 4 | const separator = '-' 5 | const regex1 = /([a-z\d])([A-Z])/g 6 | const regex2 = /([a-z]+)([A-Z][a-z\d]+)/g 7 | 8 | export default function decamelize(text: string): string { 9 | return text 10 | .replace(regex1, `$1${separator}$2`) 11 | .replace(regex2, `$1${separator}$2`) 12 | .toLowerCase() 13 | } 14 | -------------------------------------------------------------------------------- /test/get-safe-value.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import getSafeValue from '../src/get-safe-value' 3 | 4 | test('replaces dots and spaces with dashes', t => { 5 | t.is(getSafeValue('10.5px 20.5px 30.5px'), '10-5px-20-5px-30-5px') 6 | }) 7 | 8 | test('replaces percentages with prcnt', t => { 9 | t.is(getSafeValue('10% 20%'), '10prcnt-20prcnt') 10 | }) 11 | 12 | test('strips unsafe characters', t => { 13 | t.is(getSafeValue('calc(20px + 100%)'), 'calc20px--100prcnt') 14 | }) 15 | -------------------------------------------------------------------------------- /test/styles.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import {add, getAll, clear} from '../src/styles' 3 | 4 | test.afterEach.always(() => { 5 | clear() 6 | }) 7 | 8 | test.serial('returns a style', t => { 9 | add(`.test { width: 10px; }`) 10 | t.is(getAll(), '.test { width: 10px; }') 11 | }) 12 | 13 | test.serial('returns multiple styles', t => { 14 | add('.test { width: 11px; }') 15 | add('.test2 { height: 20px; }') 16 | t.is(getAll(), '.test { width: 11px; }.test2 { height: 20px; }') 17 | }) 18 | -------------------------------------------------------------------------------- /src/styles.ts: -------------------------------------------------------------------------------- 1 | import StyleSheet from './utils/style-sheet' 2 | 3 | const styleSheet = new StyleSheet({}) 4 | styleSheet.inject() 5 | 6 | export function add(styles: string) { 7 | styleSheet.insert(styles) 8 | } 9 | 10 | export function getAll() { 11 | // Convert rules array to a string 12 | return styleSheet 13 | .rules() 14 | .reduce((combinedRules: string, rule: {cssText: string; }) => combinedRules + rule.cssText, '') 15 | } 16 | 17 | export function clear() { 18 | styleSheet.flush() 19 | styleSheet.inject() 20 | } 21 | -------------------------------------------------------------------------------- /src/enhancers/resize.ts: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import getCss from '../get-css' 3 | import { PropEnhancerValueType, PropValidators, PropEnhancers, PropTypesMapping } from '../types/enhancers' 4 | 5 | export const propTypes: PropTypesMapping = { 6 | resize: PropTypes.string 7 | } 8 | 9 | export const propAliases = {} 10 | 11 | export const propValidators: PropValidators = {} 12 | 13 | const resize = { 14 | className: 'rsz', 15 | cssName: 'resize', 16 | jsName: 'resize' 17 | } 18 | 19 | export const propEnhancers: PropEnhancers = { 20 | resize: (value: PropEnhancerValueType, selector: string) => getCss(resize, value, selector) 21 | } 22 | -------------------------------------------------------------------------------- /src/enhancers/outline.ts: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import getCss from '../get-css' 3 | import { PropEnhancerValueType, PropValidators, PropEnhancers, PropTypesMapping } from '../types/enhancers' 4 | 5 | export const propTypes: PropTypesMapping = { 6 | outline: PropTypes.string 7 | } 8 | 9 | export const propAliases = {} 10 | 11 | export const propValidators: PropValidators = {} 12 | 13 | const outline = { 14 | className: 'otln', 15 | cssName: 'outline', 16 | jsName: 'outline', 17 | complexValue: true 18 | } 19 | 20 | export const propEnhancers: PropEnhancers = { 21 | outline: (value: PropEnhancerValueType, selector: string) => getCss(outline, value, selector) 22 | } 23 | -------------------------------------------------------------------------------- /src/enhancers/box-shadow.ts: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import getCss from '../get-css' 3 | import { PropEnhancerValueType, PropValidators, PropEnhancers, PropTypesMapping, PropAliases } from '../types/enhancers' 4 | 5 | export const propTypes: PropTypesMapping = { 6 | boxShadow: PropTypes.string 7 | } 8 | 9 | export const propAliases: PropAliases = {} 10 | 11 | export const propValidators: PropValidators = {} 12 | 13 | const boxShadow = { 14 | className: 'bs', 15 | cssName: 'box-shadow', 16 | jsName: 'boxShadow', 17 | complexValue: true 18 | } 19 | 20 | export const propEnhancers: PropEnhancers = { 21 | boxShadow: (value: PropEnhancerValueType, selector: string) => getCss(boxShadow, value, selector) 22 | } 23 | -------------------------------------------------------------------------------- /src/enhancers/opacity.ts: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import getCss from '../get-css' 3 | import { PropEnhancerValueType, PropValidators, PropEnhancers, PropTypesMapping, PropAliases } from '../types/enhancers' 4 | 5 | export const propTypes: PropTypesMapping = { 6 | opacity: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) 7 | } 8 | 9 | export const propAliases: PropAliases = {} 10 | 11 | export const propValidators: PropValidators = {} 12 | 13 | const opacity = { 14 | className: 'opct', 15 | cssName: 'opacity', 16 | jsName: 'opacity', 17 | defaultUnit: '' 18 | } 19 | 20 | export const propEnhancers: PropEnhancers = { 21 | opacity: (value: PropEnhancerValueType, selector: string) => getCss(opacity, value, selector) 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/flatten-object.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Flattens an object into a string representation in the form of `key:type:value` 3 | */ 4 | const flattenObject = (object: Record): string => { 5 | const keys = Object.keys(object) 6 | return keys 7 | .map(key => { 8 | const value = object[key] 9 | const type = typeof value 10 | 11 | if (Array.isArray(value)) { 12 | return `${key}:array:[${value.map((value, index) => flattenObject({ [index]: value }))}]` 13 | } 14 | 15 | if (value != null && type === 'object') { 16 | return `${key}:${type}:${flattenObject(value)}` 17 | } 18 | 19 | return `${key}:${type}:${value}` 20 | }) 21 | .join(';') 22 | } 23 | 24 | export default flattenObject 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "target": "es6", 5 | "module": "commonjs", 6 | "lib": [ 7 | "es2015", 8 | "es2016", 9 | "es2017", 10 | "es2018", 11 | "dom" 12 | ], 13 | "declaration": true, 14 | "sourceMap": false, 15 | "removeComments": true, 16 | "rootDirs": [ 17 | "./src", 18 | "./tools" 19 | ], 20 | "jsx": "react", 21 | "moduleResolution": "node", 22 | "strict": true, 23 | "noImplicitReturns": true, 24 | "noUnusedLocals": true, 25 | "noUnusedParameters": true, 26 | "noFallthroughCasesInSwitch": true, 27 | "esModuleInterop": true 28 | }, 29 | "include": [ 30 | "src", 31 | "tools" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /src/utils/split-box-props.ts: -------------------------------------------------------------------------------- 1 | import { propNames } from '../enhancers/index' 2 | import splitProps from './split-props' 3 | import { EnhancerProps } from '../types/enhancers' 4 | 5 | interface SplitBoxProps

{ 6 | matchedProps: Pick 7 | remainingProps: Pick> 8 | } 9 | 10 | /** 11 | * Convenience method to split the Box props. 12 | * 13 | * Useful for when you want to pass all of the Box props to the root Box and 14 | * pass the remaining props to a child element (e.g: disabled, readOnly, required, etc). 15 | */ 16 | export default function splitBoxProps

(props: P): SplitBoxProps

{ 17 | return splitProps(props, propNames as Array) 18 | } 19 | -------------------------------------------------------------------------------- /test/utils/regex.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { 3 | spacesOutsideParentheses, 4 | unsafeClassNameCharacters 5 | } from '../../src/utils/regex' 6 | 7 | test('spacesOutsideParentheses matches spaces', t => { 8 | t.true(spacesOutsideParentheses.test('10px 20px')) 9 | }) 10 | 11 | test('spacesOutsideParentheses doesn՚t match spaces in parentheses', t => { 12 | t.false(spacesOutsideParentheses.test('calc(10px 20px)')) 13 | }) 14 | 15 | test('spacesOutsideParentheses matches spaces outside parentheses', t => { 16 | t.true(spacesOutsideParentheses.test('20px calc(10px 20px)')) 17 | }) 18 | 19 | test('unsafeClassNameCharacters matches unsafe characters', t => { 20 | t.true(unsafeClassNameCharacters.test('rgba(0, 0, 0)')) 21 | }) 22 | 23 | test('unsafeClassNameCharacters doesn՚t match safe characters', t => { 24 | t.false(unsafeClassNameCharacters.test('min-w_20px')) 25 | }) 26 | -------------------------------------------------------------------------------- /test/utils/is-production.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import isProduction from '../../src/utils/is-production' 3 | 4 | test.beforeEach(() => { 5 | process.env.NODE_ENV = 'development' 6 | }) 7 | 8 | test.serial('should return false when NODE_ENV is undefined', t => { 9 | process.env.NODE_ENV = undefined 10 | 11 | const result = isProduction() 12 | 13 | t.is(result, false) 14 | }) 15 | 16 | test.serial('should return false when NODE_ENV is null', t => { 17 | ;(process.env.NODE_ENV as any) = null 18 | 19 | const result = isProduction() 20 | 21 | t.is(result, false) 22 | }) 23 | 24 | test.serial("should return false when NODE_ENV is not 'production'", t => { 25 | process.env.NODE_ENV = 'development' 26 | 27 | const result = isProduction() 28 | 29 | t.is(result, false) 30 | }) 31 | 32 | test.serial("should return true when NODE_ENV is 'production'", t => { 33 | process.env.NODE_ENV = 'production' 34 | 35 | const result = isProduction() 36 | 37 | t.is(result, true) 38 | }) 39 | -------------------------------------------------------------------------------- /src/utils/split-props.ts: -------------------------------------------------------------------------------- 1 | type Omit, K extends keyof T> = Pick> 2 | 3 | interface Dictionary { 4 | [key: string]: T 5 | } 6 | 7 | export interface SplitProps

, K extends keyof P> { 8 | matchedProps: Pick 9 | remainingProps: Omit 10 | } 11 | 12 | /** 13 | * Utility to split props based on an array of keys 14 | */ 15 | export default function splitProps

, K extends keyof P>( 16 | props: P, 17 | keys: K[] 18 | ): SplitProps { 19 | const matchedProps = {} as Pick 20 | const remainingProps = {} as P 21 | const propKeys = Object.keys(props) as K[] 22 | 23 | for (let i = 0; i < propKeys.length; i++) { 24 | const propKey = propKeys[i] 25 | const propValue = props[propKey] 26 | 27 | if (keys.includes(propKey)) { 28 | matchedProps[propKey] = propValue 29 | } else { 30 | remainingProps[propKey] = propValue 31 | } 32 | } 33 | 34 | return { matchedProps, remainingProps } 35 | } 36 | -------------------------------------------------------------------------------- /src/enhancers/transform.ts: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import getCss from '../get-css' 3 | import { PropEnhancerValueType, PropValidators, PropEnhancers, PropTypesMapping, PropAliases } from '../types/enhancers' 4 | 5 | export const propTypes: PropTypesMapping = { 6 | transform: PropTypes.string, 7 | transformOrigin: PropTypes.string 8 | } 9 | 10 | export const propAliases: PropAliases = {} 11 | 12 | export const propValidators: PropValidators = {} 13 | 14 | const transform = { 15 | className: 'tfrm', 16 | cssName: 'transform', 17 | jsName: 'transform', 18 | complexValue: true 19 | } 20 | const transformOrigin = { 21 | className: 'tfrm-orgn', 22 | cssName: 'transform-origin', 23 | jsName: 'transformOrigin', 24 | complexValue: true 25 | } 26 | 27 | export const propEnhancers: PropEnhancers = { 28 | transform: (value: PropEnhancerValueType, selector: string) => getCss(transform, value, selector), 29 | transformOrigin: (value: PropEnhancerValueType, selector: string) => getCss(transformOrigin, value, selector) 30 | } 31 | -------------------------------------------------------------------------------- /test/utils/split-box-props.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import splitBoxProps from '../../src/utils/split-box-props' 3 | 4 | test('splits box props', t => { 5 | const props = { 6 | background: 'red', 7 | disabled: true 8 | } 9 | t.deepEqual(splitBoxProps(props), { 10 | /* @ts-ignore We are only passing and expecting a partial object back */ 11 | matchedProps: { 12 | background: 'red' 13 | }, 14 | remainingProps: { 15 | disabled: true 16 | } 17 | }) 18 | }) 19 | 20 | test('includes selectors in matchedProps', t => { 21 | const props = { 22 | selectors: { 23 | '&:hover': { 24 | backgroundColor: 'red' 25 | } 26 | } 27 | } 28 | 29 | const result = splitBoxProps(props) 30 | 31 | t.deepEqual(result, { 32 | /* @ts-ignore We are only passing and expecting a partial object back */ 33 | matchedProps: { 34 | selectors: { 35 | '&:hover': { 36 | backgroundColor: 'red' 37 | } 38 | } 39 | }, 40 | remainingProps: {} 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /test/prefixer.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import prefixer from '../src/prefixer' 3 | 4 | test('prefixes properties', t => { 5 | t.deepEqual(prefixer('userSelect', 'none'), [ 6 | { 7 | property: '-webkit-user-select', 8 | value: 'none' 9 | }, 10 | { 11 | property: '-moz-user-select', 12 | value: 'none' 13 | }, 14 | { 15 | property: '-ms-user-select', 16 | value: 'none' 17 | }, 18 | { 19 | property: 'user-select', 20 | value: 'none' 21 | } 22 | ]) 23 | }) 24 | 25 | test('prefixes values', t => { 26 | t.deepEqual(prefixer('display', 'flex'), [ 27 | { 28 | property: 'display', 29 | value: '-webkit-box' 30 | }, 31 | { 32 | property: 'display', 33 | value: '-moz-box' 34 | }, 35 | { 36 | property: 'display', 37 | value: '-ms-flexbox' 38 | }, 39 | { 40 | property: 'display', 41 | value: '-webkit-flex' 42 | }, 43 | { 44 | property: 'display', 45 | value: 'flex' 46 | } 47 | ]) 48 | }) 49 | -------------------------------------------------------------------------------- /tools/fixtures/selector-uniquness-story.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import Box from '../../src' 3 | 4 | const SelectorUniqueness: React.FC = () => { 5 | const [isInputDisabled, setIsInputDisabled] = useState(false) 6 | 7 | return ( 8 | 9 | Border style on hover 10 | 16 | Border style only when disabled 17 | 18 | Disable input 19 | setIsInputDisabled((disabled: boolean) => !disabled)} 23 | checked={isInputDisabled} 24 | /> 25 | 26 | 33 | 34 | ) 35 | } 36 | 37 | export default SelectorUniqueness 38 | -------------------------------------------------------------------------------- /test/utils/flatten-object.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import flattenObject from '../../src/utils/flatten-object' 3 | 4 | test.serial('flattens basic object', t => { 5 | const result = flattenObject({ width: 10, height: '20' }) 6 | 7 | t.is(result, 'width:number:10;height:string:20') 8 | }) 9 | 10 | test.serial('handles null values', t => { 11 | const result = flattenObject({ width: null }) 12 | 13 | t.is(result, 'width:object:null') 14 | }) 15 | 16 | test.serial('handles undefined values', t => { 17 | const result = flattenObject({ width: undefined }) 18 | 19 | t.is(result, 'width:undefined:undefined') 20 | }) 21 | 22 | test.serial('handles arrays', t => { 23 | const result = flattenObject({ fizz: [1, '2', { foo: 'bar' }] }) 24 | 25 | t.is(result, 'fizz:array:[0:number:1,1:string:2,2:object:foo:string:bar]') 26 | }) 27 | 28 | test.serial('flattens nested objects', t => { 29 | const result = flattenObject({ 30 | baz: 'buzz', 31 | foo: { 32 | bar: 123 33 | } 34 | }) 35 | 36 | t.is(result, 'baz:string:buzz;foo:object:bar:number:123') 37 | }) 38 | -------------------------------------------------------------------------------- /src/enhancers/overflow.ts: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import getCss from '../get-css' 3 | import { PropEnhancerValueType, PropValidators, PropEnhancers, PropTypesMapping, PropAliases } from '../types/enhancers' 4 | 5 | export const propTypes: PropTypesMapping = { 6 | overflow: PropTypes.string, 7 | overflowX: PropTypes.string, 8 | overflowY: PropTypes.string 9 | } 10 | 11 | export const propAliases: PropAliases = { 12 | overflow: ['overflowX', 'overflowY'] 13 | } 14 | 15 | export const propValidators: PropValidators = {} 16 | 17 | const overflowY = { 18 | className: 'ovflw-y', 19 | cssName: 'overflow-y', 20 | jsName: 'overflowY', 21 | safeValue: true 22 | } 23 | const overflowX = { 24 | className: 'ovflw-x', 25 | cssName: 'overflow-x', 26 | jsName: 'overflowX', 27 | safeValue: true 28 | } 29 | 30 | export const propEnhancers: PropEnhancers = { 31 | overflowX: (value: PropEnhancerValueType, selector: string) => getCss(overflowX, value, selector), 32 | overflowY: (value: PropEnhancerValueType, selector: string) => getCss(overflowY, value, selector) 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | push: 8 | branches: ["*"] 9 | 10 | jobs: 11 | test: 12 | name: Tests 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | node_version: [12] 17 | steps: 18 | - uses: actions/checkout@master 19 | 20 | - uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node_version }} 23 | - id: yarn-cache-dir-path 24 | run: echo "::set-output name=dir::$(yarn cache dir)" 25 | 26 | - uses: actions/cache@v1 27 | id: yarn-cache 28 | with: 29 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 30 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 31 | restore-keys: | 32 | ${{ runner.os }}-yarn- 33 | 34 | - name: Install dependencies 35 | run: yarn install --frozen-lockfile 36 | 37 | - name: Run build 38 | run: yarn run build 39 | 40 | - name: Run test 41 | run: yarn run test 42 | 43 | - name: Run size 44 | run: yarn run size 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2017 Segment.io, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/cache.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import * as cache from '../src/cache' 3 | 4 | test.afterEach.always(() => { 5 | cache.clear() 6 | }) 7 | 8 | test.serial('caches a className', t => { 9 | cache.set('minWidth', '10px', 'min-w-10px') 10 | t.is(cache.get('minWidth', '10px'), 'min-w-10px') 11 | }) 12 | 13 | test.serial('validates the value', t => { 14 | t.throws(() => { 15 | cache.set('width', {herpa: 'derp'}, 'w-10px') 16 | }, /invalid cache value/) 17 | }) 18 | 19 | test.serial('returns the cache entries', t => { 20 | cache.set('minHeight', '10px', 'min-h-10px') 21 | t.log(cache.entries()) 22 | t.deepEqual(cache.entries(), [['minHeight10px', 'min-h-10px']]) 23 | }) 24 | 25 | test.serial('hydrates the cache', t => { 26 | const fixture: [string, string][] = [ 27 | ['height10px', 'h-10px'] 28 | ] 29 | cache.hydrate(fixture) 30 | t.deepEqual(cache.entries(), fixture) 31 | }) 32 | 33 | test.serial('existing keys are maintained when hydrating', t => { 34 | cache.set('minWidth', '10px', 'min-w-10px') 35 | cache.hydrate([['height10px', 'h-10px']]) 36 | t.deepEqual(cache.entries(), [ 37 | ['minWidth10px', 'min-w-10px'], 38 | ['height10px', 'h-10px'] 39 | ]) 40 | }) 41 | -------------------------------------------------------------------------------- /src/cache.ts: -------------------------------------------------------------------------------- 1 | import {BoxPropValue} from './types/enhancers' 2 | 3 | type CacheValue = BoxPropValue 4 | let cache = new Map() 5 | 6 | export function get(property: string, value: CacheValue, selectorHead = '') { 7 | return cache.get(selectorHead + property + value) 8 | } 9 | 10 | export function set(property: string, value: CacheValue | object, className: string, selectorHead = '') { 11 | if (process.env.NODE_ENV !== 'production') { 12 | const valueType = typeof value 13 | if ( 14 | valueType !== 'boolean' && 15 | valueType !== 'number' && 16 | valueType !== 'string' 17 | ) { 18 | const encodedValue = JSON.stringify(value) 19 | throw new TypeError( 20 | `📦 ui-box: invalid cache value “${encodedValue}”. Only booleans, numbers and strings are supported.` 21 | ) 22 | } 23 | } 24 | 25 | cache.set(selectorHead + property + value, className) 26 | } 27 | 28 | export function entries() { 29 | return [...cache] 30 | } 31 | 32 | type CacheEntry = [/** key */ string, /** value */ string] 33 | export function hydrate(newEntries: CacheEntry[]) { 34 | cache = new Map([...cache, ...newEntries]) 35 | } 36 | 37 | export function clear() { 38 | cache.clear() 39 | } 40 | -------------------------------------------------------------------------------- /src/prefixer.ts: -------------------------------------------------------------------------------- 1 | import {prefix} from 'inline-style-prefixer' 2 | import decamelize from './utils/decamelize' 3 | 4 | const prefixRegex = /^(Webkit|ms|Moz|O)/ 5 | 6 | export interface Rule { 7 | property: string 8 | value: string 9 | } 10 | /** 11 | * Adds vendor prefixes to properties and values. 12 | */ 13 | export default function prefixer(property: string, value: string): Rule[] { 14 | const rules = prefix({[property]: value}) 15 | const rulesArray: Rule[] = [] 16 | const propertyNames = Object.keys(rules) 17 | 18 | // Convert rules object to an array 19 | for (let i = 0; i < propertyNames.length; i++) { 20 | const propertyName = propertyNames[i] 21 | // Add a dash in front of the prefixes 22 | const prefixedProp = propertyName.match(prefixRegex) 23 | ? `-${propertyName}` 24 | : propertyName 25 | const prop = decamelize(prefixedProp) 26 | const values = rules[propertyName] 27 | 28 | // Handle prefixed values 29 | if (Array.isArray(values)) { 30 | for (let j = 0; j < values.length; j++) { 31 | rulesArray.push({property: prop, value: values[j]}) 32 | } 33 | } else { 34 | rulesArray.push({property: prop, value: values}) 35 | } 36 | } 37 | 38 | return rulesArray 39 | } 40 | -------------------------------------------------------------------------------- /src/expand-aliases.ts: -------------------------------------------------------------------------------- 1 | import {propAliases, propValidators} from './enhancers/index' 2 | import { BoxPropValue } from './types/enhancers' 3 | 4 | /** 5 | * Expands aliases like `margin` to `marginTop`, `marginBottom`, `marginLeft` and `marginRight`. 6 | * 7 | * This prevents edge cases where longhand properties can't override shorthand 8 | * properties due to the style insertion order. 9 | */ 10 | export default function expandAliases(props: { [key: string]: BoxPropValue }) { 11 | const propNames = Object.keys(props) 12 | // Use a Map because it's faster for setting values and looping over than an Object 13 | const newProps = new Map() 14 | 15 | propNames.forEach(propName => { 16 | const propValue: BoxPropValue = props[propName] 17 | const aliases: string[] = propAliases[propName] || [propName] 18 | 19 | // Check that the alias has a valid value in development 20 | if (process.env.NODE_ENV !== 'production') { 21 | const validator = propValidators[propName] 22 | if (validator) { 23 | const result = validator(propValue) 24 | if (result) { 25 | throw new Error(`📦 ui-box: ${result}`) 26 | } 27 | } 28 | } 29 | 30 | // Expand aliases 31 | aliases.forEach(alias => { 32 | newProps.set(alias, propValue) 33 | }) 34 | }) 35 | 36 | return newProps 37 | } 38 | -------------------------------------------------------------------------------- /test/expand-aliases.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import expandAliases from '../src/expand-aliases' 3 | 4 | test('expands an alias', t => { 5 | t.deepEqual( 6 | expandAliases({ 7 | margin: '10px' 8 | }), 9 | new Map([ 10 | ['marginBottom', '10px'], 11 | ['marginLeft', '10px'], 12 | ['marginRight', '10px'], 13 | ['marginTop', '10px'] 14 | ]) 15 | ) 16 | }) 17 | 18 | test('aliases override earlier props', t => { 19 | t.deepEqual( 20 | expandAliases({ 21 | marginTop: '20px', 22 | margin: '10px' 23 | }), 24 | new Map([ 25 | ['marginTop', '10px'], 26 | ['marginBottom', '10px'], 27 | ['marginLeft', '10px'], 28 | ['marginRight', '10px'] 29 | ]) 30 | ) 31 | }) 32 | 33 | test('props override earlier aliases', t => { 34 | t.deepEqual( 35 | expandAliases({ 36 | margin: '10px', 37 | marginTop: '20px' 38 | }), 39 | new Map([ 40 | ['marginBottom', '10px'], 41 | ['marginLeft', '10px'], 42 | ['marginRight', '10px'], 43 | ['marginTop', '20px'] 44 | ]) 45 | ) 46 | }) 47 | 48 | test('maintains original prop order', t => { 49 | t.deepEqual( 50 | expandAliases({ 51 | width: '10px', 52 | height: '10px', 53 | marginTop: '10px' 54 | }), 55 | new Map([['width', '10px'], ['height', '10px'], ['marginTop', '10px']]) 56 | ) 57 | }) 58 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as cache from './cache' 2 | import * as styles from './styles' 3 | 4 | export { default } from './box' 5 | export { default as keyframes } from './keyframes' 6 | export { default as splitProps } from './utils/split-props' 7 | export { default as splitBoxProps } from './utils/split-box-props' 8 | export { setClassNamePrefix } from './get-class-name' 9 | export { configureSafeHref } from './utils/safeHref' 10 | export { CssProps, BoxCssProps, EnhancerProps, SelectorMap } from './types/enhancers' 11 | export { BoxProps, BoxOwnProps, PropsOf, PolymorphicBoxProps, BoxComponent } from './types/box-types' 12 | export { 13 | KeyframesPercentageKey, 14 | KeyframesPositionalKey, 15 | KeyframesTimeline, 16 | KeyframesTimelineKey 17 | } from './types/keyframes' 18 | 19 | export { 20 | background, 21 | borderRadius, 22 | borders, 23 | boxShadow, 24 | dimensions, 25 | flex, 26 | interaction, 27 | layout, 28 | list, 29 | opacity, 30 | overflow, 31 | position, 32 | spacing, 33 | text, 34 | transform, 35 | propTypes, 36 | propNames, 37 | propAliases, 38 | propEnhancers 39 | } from './enhancers/index' 40 | 41 | export const hydrate = cache.hydrate 42 | 43 | export function extractStyles() { 44 | const output = { 45 | cache: cache.entries(), 46 | styles: styles.getAll() 47 | } 48 | clearStyles() 49 | return output 50 | } 51 | 52 | export function clearStyles() { 53 | cache.clear() 54 | styles.clear() 55 | } 56 | -------------------------------------------------------------------------------- /tools/fixtures/keyframes-story.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Box, { keyframes } from '../../src' 3 | 4 | const KeyframesStory: React.FC = () => { 5 | const translateTo0 = { 6 | transform: 'translate3d(0,0,0)' 7 | } 8 | const translateNeg30 = { 9 | transform: 'translate3d(0, -30px, 0)' 10 | } 11 | 12 | const translateNeg15 = { 13 | transform: 'translate3d(0, -15px, 0)' 14 | } 15 | 16 | const translateNeg4 = { 17 | transform: 'translate3d(0,-4px,0)' 18 | } 19 | 20 | // Based on https://emotion.sh/docs/keyframes 21 | const bounce = keyframes('bounce', { 22 | from: translateTo0, 23 | 20: translateTo0, 24 | 40: translateNeg30, 25 | 43: translateNeg30, 26 | 53: translateTo0, 27 | 70: translateNeg15, 28 | 80: translateTo0, 29 | 90: translateNeg4, 30 | to: translateTo0 31 | }) 32 | 33 | return ( 34 | 35 | Single prop 36 | some bouncing text! 37 | Separate props 38 | 48 | some bouncing text! 49 | 50 | 51 | ) 52 | } 53 | 54 | export default KeyframesStory 55 | -------------------------------------------------------------------------------- /src/enhancers/interaction.ts: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import getCss from '../get-css' 3 | import { PropEnhancerValueType, PropValidators, PropEnhancers, PropTypesMapping, PropAliases } from '../types/enhancers' 4 | 5 | export const propTypes: PropTypesMapping = { 6 | cursor: PropTypes.string, 7 | pointerEvents: PropTypes.string, 8 | userSelect: PropTypes.string, 9 | visibility: PropTypes.string 10 | } 11 | 12 | export const propAliases: PropAliases = {} 13 | 14 | export const propValidators: PropValidators = {} 15 | 16 | const cursor = { 17 | className: 'crsr', 18 | cssName: 'cursor', 19 | jsName: 'cursor' 20 | } 21 | const userSelect = { 22 | className: 'usr-slct', 23 | cssName: 'user-select', 24 | jsName: 'userSelect', 25 | safeValue: true, 26 | isPrefixed: true 27 | } 28 | const visibility = { 29 | className: 'vsblt', 30 | cssName: 'visibility', 31 | jsName: 'visibility', 32 | safeValue: true 33 | } 34 | const pointerEvents = { 35 | className: 'ptr-evts', 36 | cssName: 'pointer-events', 37 | jsName: 'pointerEvents', 38 | safeValue: true 39 | } 40 | 41 | export const propEnhancers: PropEnhancers = { 42 | cursor: (value: PropEnhancerValueType, selector: string) => getCss(cursor, value, selector), 43 | pointerEvents: (value: PropEnhancerValueType, selector: string) => getCss(pointerEvents, value, selector), 44 | userSelect: (value: PropEnhancerValueType, selector: string) => getCss(userSelect, value, selector), 45 | visibility: (value: PropEnhancerValueType, selector: string) => getCss(visibility, value, selector) 46 | } 47 | -------------------------------------------------------------------------------- /src/get-class-name.ts: -------------------------------------------------------------------------------- 1 | import hash from '@emotion/hash' 2 | import getSafeValue from './get-safe-value' 3 | 4 | let PREFIX = 'ub-' 5 | 6 | export function getClassNamePrefix(): string { 7 | return PREFIX 8 | } 9 | 10 | export function setClassNamePrefix(prefix: string): void { 11 | PREFIX = prefix 12 | } 13 | 14 | export interface PropertyInfo { 15 | className?: string 16 | safeValue?: boolean 17 | complexValue?: boolean 18 | jsName?: string 19 | cssName?: string 20 | defaultUnit?: string 21 | isPrefixed?: boolean 22 | } 23 | /** 24 | * Generates the class name. 25 | */ 26 | export default function getClassName(propertyInfo: PropertyInfo, value: string, selector: string = '') { 27 | const { 28 | className, 29 | safeValue = false, // Value never contains unsafe characters. e.g: 10, hidden, border-box 30 | complexValue = false // Complex values that are best hashed. e.g: background-image 31 | } = propertyInfo 32 | let valueKey: string 33 | 34 | // Shortcut the global keywords 35 | if (value === 'inherit' || value === 'initial' || value === 'unset') { 36 | valueKey = value 37 | /* Always hash values that contain a calc() because the operators get 38 | stripped which can result in class name collisions 39 | */ 40 | } else if (complexValue || value.includes('calc(')) { 41 | valueKey = hash(value) 42 | } else if (safeValue) { 43 | valueKey = value 44 | } else { 45 | valueKey = getSafeValue(value) 46 | } 47 | 48 | if (selector) { 49 | valueKey = `${valueKey}_${hash(selector)}` 50 | } 51 | 52 | return `${PREFIX}${className}_${valueKey}` 53 | } 54 | -------------------------------------------------------------------------------- /src/enhancers/list.ts: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import getCss from '../get-css' 3 | import { PropEnhancerValueType, PropValidators, PropEnhancers, PropTypesMapping, PropAliases } from '../types/enhancers' 4 | 5 | export const propTypes: PropTypesMapping = { 6 | listStyle: PropTypes.string, 7 | listStyleType: PropTypes.string, 8 | listStyleImage: PropTypes.string, 9 | listStylePosition: PropTypes.string 10 | } 11 | 12 | export const propAliases: PropAliases = {} 13 | 14 | export const propValidators: PropValidators = {} 15 | 16 | const listStyle = { 17 | className: 'ls', 18 | cssName: 'list-style', 19 | jsName: 'listStyle', 20 | complexValue: true 21 | } 22 | const listStyleType = { 23 | className: 'ls-typ', 24 | cssName: 'list-style-type', 25 | jsName: 'listStyleType' 26 | } 27 | const listStyleImage = { 28 | className: 'ls-img', 29 | cssName: 'list-style-image', 30 | jsName: 'listStyleImage', 31 | complexValue: true 32 | } 33 | const listStylePosition = { 34 | className: 'ls-pos', 35 | cssName: 'list-style-position', 36 | jsName: 'listStylePosition', 37 | safeValue: true 38 | } 39 | 40 | export const propEnhancers: PropEnhancers = { 41 | listStyle: (value: PropEnhancerValueType, selector: string) => getCss(listStyle, value, selector), 42 | listStyleType: (value: PropEnhancerValueType, selector: string) => getCss(listStyleType, value, selector), 43 | listStyleImage: (value: PropEnhancerValueType, selector: string) => getCss(listStyleImage, value, selector), 44 | listStylePosition: (value: PropEnhancerValueType, selector: string) => getCss(listStylePosition, value, selector) 45 | } 46 | -------------------------------------------------------------------------------- /src/types/keyframes.ts: -------------------------------------------------------------------------------- 1 | import { BoxCssProps, CssProps } from './enhancers' 2 | 3 | export type KeyframesPercentageKey = 4 | | 0 5 | | 1 6 | | 2 7 | | 3 8 | | 4 9 | | 5 10 | | 6 11 | | 7 12 | | 8 13 | | 9 14 | | 10 15 | | 11 16 | | 12 17 | | 13 18 | | 14 19 | | 15 20 | | 16 21 | | 17 22 | | 18 23 | | 19 24 | | 20 25 | | 21 26 | | 22 27 | | 23 28 | | 24 29 | | 25 30 | | 26 31 | | 27 32 | | 28 33 | | 29 34 | | 30 35 | | 31 36 | | 32 37 | | 33 38 | | 34 39 | | 35 40 | | 36 41 | | 37 42 | | 38 43 | | 39 44 | | 40 45 | | 41 46 | | 42 47 | | 43 48 | | 44 49 | | 45 50 | | 46 51 | | 47 52 | | 48 53 | | 49 54 | | 50 55 | | 51 56 | | 52 57 | | 53 58 | | 54 59 | | 55 60 | | 56 61 | | 57 62 | | 58 63 | | 59 64 | | 60 65 | | 61 66 | | 62 67 | | 63 68 | | 64 69 | | 65 70 | | 66 71 | | 67 72 | | 68 73 | | 69 74 | | 70 75 | | 71 76 | | 72 77 | | 73 78 | | 74 79 | | 75 80 | | 76 81 | | 77 82 | | 78 83 | | 79 84 | | 80 85 | | 81 86 | | 82 87 | | 83 88 | | 84 89 | | 85 90 | | 86 91 | | 87 92 | | 88 93 | | 89 94 | | 90 95 | | 91 96 | | 92 97 | | 93 98 | | 94 99 | | 95 100 | | 96 101 | | 97 102 | | 98 103 | | 99 104 | | 100 105 | 106 | export type KeyframesPositionalKey = 'from' | 'to' 107 | 108 | export type KeyframesTimelineKey = KeyframesPositionalKey | KeyframesPercentageKey 109 | 110 | export type KeyframesTimeline = Partial>> 111 | -------------------------------------------------------------------------------- /test/get-class-name.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import getClassName, { setClassNamePrefix } from '../src/get-class-name' 3 | 4 | test('supports inherit', t => { 5 | t.is(getClassName({ className: 'w' }, 'inherit'), 'ub-w_inherit') 6 | }) 7 | 8 | test('supports initial', t => { 9 | t.is(getClassName({ className: 'w' }, 'initial'), 'ub-w_initial') 10 | }) 11 | 12 | test('supports unset', t => { 13 | t.is(getClassName({ className: 'w' }, 'unset'), 'ub-w_unset') 14 | }) 15 | 16 | test('safeValue does not transform value', t => { 17 | const result = getClassName({ className: 'w', safeValue: true }, '50.5%') 18 | t.is(result, 'ub-w_50.5%') 19 | }) 20 | 21 | test('hashes complex values', t => { 22 | const result = getClassName( 23 | { className: 'bg', complexValue: true }, 24 | 'url(https://s-media-cache-ak0.pinimg.com/736x/07/c3/45/07c345d0eca11d0bc97c894751ba1b46.jpg)' 25 | ) 26 | t.is(result, 'ub-bg_181xl07') 27 | }) 28 | 29 | test('removes all unsafe values by default', t => { 30 | const result = getClassName({ className: 'w' }, '50.5%') 31 | t.is(result, 'ub-w_50-5prcnt') 32 | }) 33 | 34 | test('always hashes values that contain a calc()', t => { 35 | const result = getClassName({ className: 'w', safeValue: true }, 'calc(50% + 20px)') 36 | t.is(result, 'ub-w_1vuvdht') 37 | }) 38 | 39 | test('always appends hash when a selector is provided', t => { 40 | const result = getClassName({ className: 'bg-clr' }, 'blue', ':hover') 41 | t.is(result, 'ub-bg-clr_blue_1k2el8q') 42 | }) 43 | 44 | test('allows custom classname prefixes', t => { 45 | setClassNamePrefix('📦') 46 | t.is(getClassName({ className: 'w' }, 'inherit'), '📦w_inherit') 47 | }) 48 | -------------------------------------------------------------------------------- /src/enhancers/position.ts: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import getCss from '../get-css' 3 | import { PropEnhancerValueType, PropValidators, PropEnhancers, PropTypesMapping, PropAliases } from '../types/enhancers' 4 | 5 | export const propTypes: PropTypesMapping = { 6 | bottom: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 7 | left: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 8 | position: PropTypes.string, 9 | right: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 10 | top: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) 11 | } 12 | 13 | export const propAliases: PropAliases = {} 14 | export const propValidators: PropValidators = {} 15 | 16 | const position = { 17 | className: 'pst', 18 | cssName: 'position', 19 | jsName: 'position', 20 | safeValue: true, 21 | isPrefixed: true 22 | } 23 | const top = { 24 | className: 'top', 25 | cssName: 'top', 26 | jsName: 'top' 27 | } 28 | const right = { 29 | className: 'rgt', 30 | cssName: 'right', 31 | jsName: 'right' 32 | } 33 | const bottom = { 34 | className: 'btm', 35 | cssName: 'bottom', 36 | jsName: 'bottom' 37 | } 38 | const left = { 39 | className: 'lft', 40 | cssName: 'left', 41 | jsName: 'left' 42 | } 43 | 44 | export const propEnhancers: PropEnhancers = { 45 | bottom: (value: PropEnhancerValueType, selector: string) => getCss(bottom, value, selector), 46 | left: (value: PropEnhancerValueType, selector: string) => getCss(left, value, selector), 47 | position: (value: PropEnhancerValueType, selector: string) => getCss(position, value, selector), 48 | right: (value: PropEnhancerValueType, selector: string) => getCss(right, value, selector), 49 | top: (value: PropEnhancerValueType, selector: string) => getCss(top, value, selector) 50 | } 51 | -------------------------------------------------------------------------------- /src/box.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { BoxProps } from './types/box-types' 4 | import { propTypes } from './enhancers' 5 | import enhanceProps from './enhance-props' 6 | import { extractAnchorProps, getUseSafeHref } from './utils/safeHref' 7 | 8 | const Box = React.forwardRef(({ is, children, allowUnsafeHref, ...props }: BoxProps, ref: React.Ref) => { 9 | // Convert the CSS props to class names (and inject the styles) 10 | const {className, enhancedProps: parsedProps} = enhanceProps(props) 11 | 12 | parsedProps.className = className 13 | 14 | if (ref) { 15 | parsedProps.ref = ref 16 | } 17 | 18 | /** 19 | * If the user has enabled safe hrefs we want to make sure that the url passed 20 | * uses a safe protocol and that the other attributes that make the link safe are 21 | * added to the element 22 | */ 23 | const safeHrefEnabled = (typeof allowUnsafeHref === 'boolean' ? !allowUnsafeHref : getUseSafeHref()) && is === 'a' && parsedProps.href 24 | if (safeHrefEnabled) { 25 | const {safeHref, safeRel} = extractAnchorProps(parsedProps.href, parsedProps.rel) 26 | parsedProps.href = safeHref 27 | parsedProps.rel = safeRel 28 | } 29 | 30 | return React.createElement(is || 'div', parsedProps, children) 31 | }) as (props: BoxProps) => JSX.Element 32 | 33 | // @ts-ignore 34 | Box.displayName = 'Box' 35 | 36 | // @ts-ignore 37 | Box.propTypes = { 38 | ...propTypes, 39 | is: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.elementType]), 40 | allowUnsafeHref: PropTypes.bool 41 | } 42 | 43 | // @ts-ignore 44 | Box.defaultProps = { 45 | is: 'div', 46 | boxSizing: 'border-box' 47 | } 48 | 49 | export default Box 50 | -------------------------------------------------------------------------------- /src/types/box-types.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { EnhancerProps } from './enhancers' 3 | 4 | /** 5 | * @template T Object 6 | * @template K Union of keys (not necessarily present in T) 7 | */ 8 | export type Without = Pick> 9 | 10 | /** 11 | * @see {@link https://github.com/emotion-js/emotion/blob/b4214b8757c7ede1db1688075251946b2082f9d1/packages/styled-base/types/helper.d.ts#L6-L8} 12 | */ 13 | export type PropsOf< 14 | E extends keyof JSX.IntrinsicElements | React.JSXElementConstructor 15 | > = JSX.LibraryManagedAttributes> 16 | 17 | /** 18 | * Generic component props with "is" prop 19 | * @template P Additional props 20 | * @template T React component or string element 21 | */ 22 | export type BoxOwnProps = Without & { 23 | /** 24 | * Replaces the underlying element 25 | */ 26 | is?: E 27 | 28 | /** 29 | * Allows the high level value of safeHref to be overwritten on an individual component basis 30 | */ 31 | allowUnsafeHref?: boolean 32 | } 33 | 34 | export type BoxProps = BoxOwnProps & Without, keyof BoxOwnProps> 35 | 36 | /** 37 | * Convenience type for defining your own component props that extend Box and pass-through props 38 | */ 39 | export type PolymorphicBoxProps< 40 | E extends React.ElementType, 41 | // optional additional props (which we get removed from BoxOwnProps and PropsOf) 42 | // this is useful for defining some pass-through props on a wrapper for Box 43 | P = {} 44 | > = BoxOwnProps & Without, keyof (BoxOwnProps & P)> & P 45 | 46 | /** 47 | * Convenience type for defining your own components that extend Box and pass-through props 48 | */ 49 | export type BoxComponent

= ( 50 | props: PolymorphicBoxProps 51 | ) => JSX.Element 52 | -------------------------------------------------------------------------------- /test/keyframes.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import flattenObject from '../src/utils/flatten-object' 3 | import { KeyframesTimeline } from '../src/types/keyframes' 4 | import keyframes from '../src/keyframes' 5 | import hash from '@emotion/hash' 6 | import * as stylesheet from '../src/styles' 7 | import * as cache from '../src/cache' 8 | 9 | test.beforeEach(() => { 10 | cache.clear() 11 | stylesheet.clear() 12 | }) 13 | 14 | test.serial('returns name with hashed timeline value', t => { 15 | const timeline: KeyframesTimeline = { 16 | from: { 17 | opacity: 0, 18 | transform: 'translateY(-120%)' 19 | }, 20 | to: { 21 | transform: 'translateY(0)' 22 | } 23 | } 24 | const expectedHash = hash(flattenObject(timeline)) 25 | 26 | const name = keyframes('openAnimation', timeline) 27 | 28 | t.is(name, `openAnimation_${expectedHash}`) 29 | }) 30 | 31 | test.serial('should check cache before inserting styles when called multiple times', t => { 32 | const timeline: KeyframesTimeline = { 33 | from: { 34 | opacity: 0, 35 | transform: 'translateY(-120%)' 36 | }, 37 | to: { 38 | transform: 'translateY(0)' 39 | } 40 | } 41 | 42 | keyframes('openAnimation', timeline) 43 | keyframes('openAnimation', timeline) 44 | 45 | const styles = stylesheet.getAll() 46 | const matches = styles.match(/@keyframes/g) || [] 47 | t.is(matches.length, 1) 48 | }) 49 | 50 | test.serial('should insert keyframes styles', t => { 51 | const timeline: KeyframesTimeline = { 52 | from: { 53 | opacity: 0, 54 | transform: 'translateY(-120%)' 55 | }, 56 | to: { 57 | transform: 'translateY(0)' 58 | } 59 | } 60 | 61 | keyframes('openAnimation', timeline) 62 | 63 | const styles = stylesheet.getAll() 64 | t.is( 65 | styles, 66 | `@keyframes openAnimation_65p985 { 67 | from { 68 | opacity: 0; 69 | transform: translateY(-120%); 70 | } 71 | to { 72 | transform: translateY(0); 73 | } 74 | }` 75 | ) 76 | }) 77 | -------------------------------------------------------------------------------- /src/enhancers/transition.ts: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import getCss from '../get-css' 3 | import { PropEnhancerValueType, PropValidators, PropEnhancers, PropTypesMapping, PropAliases } from '../types/enhancers' 4 | 5 | export const propTypes: PropTypesMapping = { 6 | transition: PropTypes.string, 7 | transitionDelay: PropTypes.string, 8 | transitionDuration: PropTypes.string, 9 | transitionProperty: PropTypes.string, 10 | transitionTimingFunction: PropTypes.string 11 | } 12 | 13 | export const propAliases: PropAliases = {} 14 | 15 | export const propValidators: PropValidators = {} 16 | 17 | const transition = { 18 | className: 'tstn', 19 | cssName: 'transition', 20 | jsName: 'transition', 21 | complexValue: true 22 | } 23 | const transitionDelay = { 24 | className: 'tstn-dly', 25 | cssName: 'transition-delay', 26 | jsName: 'transitionDelay', 27 | complexValue: true 28 | } 29 | const transitionDuration = { 30 | className: 'tstn-drn', 31 | cssName: 'transition-duration', 32 | jsName: 'transitionDuration', 33 | complexValue: true 34 | } 35 | const transitionProperty = { 36 | className: 'tstn-pty', 37 | cssName: 'transition-property', 38 | jsName: 'transitionProperty', 39 | complexValue: true 40 | } 41 | const transitionTimingFunction = { 42 | className: 'tstn-tf', 43 | cssName: 'transition-timing-function', 44 | jsName: 'transitionTimingFunction', 45 | complexValue: true 46 | } 47 | 48 | export const propEnhancers: PropEnhancers = { 49 | transition: (value: PropEnhancerValueType, selector: string) => getCss(transition, value, selector), 50 | transitionDelay: (value: PropEnhancerValueType, selector: string) => getCss(transitionDelay, value, selector), 51 | transitionDuration: (value: PropEnhancerValueType, selector: string) => getCss(transitionDuration, value, selector), 52 | transitionProperty: (value: PropEnhancerValueType, selector: string) => getCss(transitionProperty, value, selector), 53 | transitionTimingFunction: (value: PropEnhancerValueType, selector: string) => getCss(transitionTimingFunction, value, selector) 54 | } 55 | -------------------------------------------------------------------------------- /tools/fixtures/selectors-story.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Box from '../../src' 3 | 4 | const SelectorsStory: React.FC = () => { 5 | return ( 6 | 7 | 8 | Border style on hover 9 | 15 | 16 | 17 | No border style on hover - :not(:disabled) selector 18 | 25 | 26 | 27 | Red background on child hover 28 | 29 | 30 | 31 | 32 | 33 | Green background on child hover (comma-separated class name selectors) 34 | 35 | 36 | 37 | Pink background on :focus or :hover 38 | 39 | Nested selector - blue background when data-active=true, red background on hover 40 | 53 | 54 | ) 55 | } 56 | 57 | export default SelectorsStory 58 | -------------------------------------------------------------------------------- /src/enhancers/dimensions.ts: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import getCss from '../get-css' 3 | import { PropEnhancerValueType, PropValidators, PropEnhancers, PropTypesMapping, PropAliases } from '../types/enhancers' 4 | 5 | export const propTypes: PropTypesMapping = { 6 | height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 7 | maxHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 8 | maxWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 9 | minHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 10 | minWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 11 | width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) 12 | } 13 | 14 | export const propAliases: PropAliases = {} 15 | export const propValidators: PropValidators = {} 16 | 17 | const width = { 18 | className: 'w', 19 | cssName: 'width', 20 | jsName: 'width' 21 | } 22 | const height = { 23 | className: 'h', 24 | cssName: 'height', 25 | jsName: 'height' 26 | } 27 | const minWidth = { 28 | className: 'min-w', 29 | cssName: 'min-width', 30 | jsName: 'minWidth' 31 | } 32 | const minHeight = { 33 | className: 'min-h', 34 | cssName: 'min-height', 35 | jsName: 'minHeight' 36 | } 37 | const maxWidth = { 38 | className: 'max-w', 39 | cssName: 'max-width', 40 | jsName: 'maxWidth' 41 | } 42 | const maxHeight = { 43 | className: 'max-h', 44 | cssName: 'max-height', 45 | jsName: 'maxHeight' 46 | } 47 | 48 | export const propEnhancers: PropEnhancers = { 49 | height: (value: PropEnhancerValueType, selector: string) => getCss(height, value, selector), 50 | maxHeight: (value: PropEnhancerValueType, selector: string) => getCss(maxHeight, value, selector), 51 | maxWidth: (value: PropEnhancerValueType, selector: string) => getCss(maxWidth, value, selector), 52 | minHeight: (value: PropEnhancerValueType, selector: string) => getCss(minHeight, value, selector), 53 | minWidth: (value: PropEnhancerValueType, selector: string) => getCss(minWidth, value, selector), 54 | width: (value: PropEnhancerValueType, selector: string) => getCss(width, value, selector) 55 | } 56 | -------------------------------------------------------------------------------- /src/get-css.ts: -------------------------------------------------------------------------------- 1 | import prefixer, { Rule } from './prefixer' 2 | import valueToString from './value-to-string' 3 | import getClassName, { PropertyInfo } from './get-class-name' 4 | import { EnhancedProp } from './types/enhancers' 5 | import isProduction from './utils/is-production' 6 | 7 | /** 8 | * Generates the class name and styles. 9 | */ 10 | export default function getCss(propertyInfo: PropertyInfo, value: string | number, selector = ''): EnhancedProp | null { 11 | let rules: Rule[] 12 | 13 | // Protect against unexpected values 14 | const valueType = typeof value 15 | if (valueType !== 'string' && valueType !== 'number') { 16 | if (process.env.NODE_ENV !== 'production') { 17 | const name = propertyInfo.jsName 18 | const encodedValue = JSON.stringify(value) 19 | console.error( 20 | `📦 ui-box: property “${name}” was passed invalid value “${encodedValue}”. Only numbers and strings are supported.` 21 | ) 22 | } 23 | 24 | return null 25 | } 26 | 27 | const valueString = valueToString(value, propertyInfo.defaultUnit) 28 | const className = getClassName(propertyInfo, valueString, selector) 29 | 30 | // Avoid running the prefixer when possible because it's slow 31 | if (propertyInfo.isPrefixed) { 32 | rules = prefixer(propertyInfo.jsName || '', valueString) 33 | } else { 34 | rules = [{ property: propertyInfo.cssName || '', value: valueString }] 35 | } 36 | 37 | let styles: string 38 | 39 | if (isProduction()) { 40 | const rulesString = rules.map(rule => `${rule.property}:${rule.value}`).join(';') 41 | styles = `${expandSelectors(className, selector)}{${rulesString}}` 42 | } else { 43 | const rulesString = rules.map(rule => ` ${rule.property}: ${rule.value};`).join('\n') 44 | styles = ` 45 | ${expandSelectors(className, selector)} { 46 | ${rulesString} 47 | }` 48 | } 49 | 50 | return { className, styles, rules } 51 | } 52 | 53 | const expandSelectors = (className: string, selector: string): string => { 54 | if (!selector.includes(',')) { 55 | return `.${className}${selector}` 56 | } 57 | 58 | return selector 59 | .split(',') 60 | .map(selectorPart => `.${className}${selectorPart}`) 61 | .join(', ') 62 | } 63 | -------------------------------------------------------------------------------- /src/enhancers/svg.ts: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import getCss from '../get-css' 3 | import { PropEnhancerValueType, PropValidators, PropEnhancers, PropTypesMapping, PropAliases } from '../types/enhancers' 4 | 5 | export const propTypes: PropTypesMapping = { 6 | fill: PropTypes.string, 7 | stroke: PropTypes.string, 8 | strokeDasharray: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 9 | strokeDashoffset: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 10 | strokeLinecap: PropTypes.string, 11 | strokeMiterlimit: PropTypes.number, 12 | strokeWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) 13 | } 14 | 15 | export const propAliases: PropAliases = {} 16 | export const propValidators: PropValidators = {} 17 | 18 | const fill = { 19 | className: 'fill', 20 | cssName: 'fill', 21 | jsName: 'fill' 22 | } 23 | 24 | const stroke = { className: 'strk', cssName: 'stroke', jsName: 'stroke' } 25 | 26 | const strokeDasharray = { 27 | className: 'strk-dshary', 28 | cssName: 'stroke-dasharray', 29 | jsName: 'strokeDasharray', 30 | defaultUnit: '' 31 | } 32 | 33 | const strokeDashoffset = { 34 | className: 'strk-dshofst', 35 | cssName: 'stroke-dashoffset', 36 | jsName: 'strokeDashoffset', 37 | defaultUnit: '' 38 | } 39 | 40 | const strokeLinecap = { className: 'strk-lncp', cssName: 'stroke-linecap', jsName: 'strokeLinecap', safeValue: true } 41 | 42 | const strokeMiterlimit = { 43 | className: 'strk-mtrlmt', 44 | cssName: 'stroke-miterlimit', 45 | jsName: 'strokeMiterlimit', 46 | defaultUnit: '' 47 | } 48 | 49 | const strokeWidth = { className: 'strk-w', cssName: 'stroke-width', jsName: 'strokeWidth', defaultUnit: '' } 50 | 51 | export const propEnhancers: PropEnhancers = { 52 | fill: (value: PropEnhancerValueType, selector: string) => getCss(fill, value, selector), 53 | stroke: (value: PropEnhancerValueType, selector: string) => getCss(stroke, value, selector), 54 | strokeDasharray: (value: PropEnhancerValueType, selector: string) => getCss(strokeDasharray, value, selector), 55 | strokeDashoffset: (value: PropEnhancerValueType, selector: string) => getCss(strokeDashoffset, value, selector), 56 | strokeLinecap: (value: PropEnhancerValueType, selector: string) => getCss(strokeLinecap, value, selector), 57 | strokeMiterlimit: (value: PropEnhancerValueType, selector: string) => getCss(strokeMiterlimit, value, selector), 58 | strokeWidth: (value: PropEnhancerValueType, selector: string) => getCss(strokeWidth, value, selector) 59 | } 60 | -------------------------------------------------------------------------------- /test/index.tsx: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import React from 'react' 3 | import {shallow} from 'enzyme' 4 | import * as cache from '../src/cache' 5 | import Box, {hydrate, extractStyles, clearStyles} from '../src' 6 | 7 | const originalNodeEnv = process.env.NODE_ENV 8 | test.afterEach.always(() => { 9 | process.env.NODE_ENV = originalNodeEnv 10 | clearStyles() 11 | }) 12 | 13 | test.serial('hydrate method hydrates the cache', t => { 14 | const fixture: [string, string][] = [['height10px', 'ub-h_10px']] 15 | hydrate(fixture) 16 | t.deepEqual(cache.entries(), fixture) 17 | }) 18 | 19 | test.serial('extractStyles method returns css and cache', t => { 20 | shallow() 21 | t.deepEqual(extractStyles(), { 22 | styles: ` 23 | .ub-h_11px { 24 | height: 11px; 25 | } 26 | .ub-box-szg_border-box { 27 | box-sizing: border-box; 28 | }`, 29 | cache: [ 30 | ['height11px', 'ub-h_11px'], 31 | ['boxSizingborder-box', 'ub-box-szg_border-box'] 32 | ] 33 | }) 34 | }) 35 | 36 | test.serial('extractStyles clears the cache and styles', t => { 37 | shallow() 38 | t.deepEqual(extractStyles(), { 39 | styles: ` 40 | .ub-h_12px { 41 | height: 12px; 42 | } 43 | .ub-box-szg_border-box { 44 | box-sizing: border-box; 45 | }`, 46 | cache: [ 47 | ['height12px', 'ub-h_12px'], 48 | ['boxSizingborder-box', 'ub-box-szg_border-box'] 49 | ] 50 | }) 51 | shallow() 52 | t.deepEqual(extractStyles(), { 53 | styles: ` 54 | .ub-h_13px { 55 | height: 13px; 56 | } 57 | .ub-box-szg_border-box { 58 | box-sizing: border-box; 59 | }`, 60 | cache: [ 61 | ['height13px', 'ub-h_13px'], 62 | ['boxSizingborder-box', 'ub-box-szg_border-box'] 63 | ] 64 | }) 65 | }) 66 | 67 | test.serial('clearStyles clears the cache and styles', t => { 68 | shallow() 69 | clearStyles() 70 | shallow() 71 | t.deepEqual(extractStyles(), { 72 | styles: ` 73 | .ub-h_15px { 74 | height: 15px; 75 | } 76 | .ub-box-szg_border-box { 77 | box-sizing: border-box; 78 | }`, 79 | cache: [ 80 | ['height15px', 'ub-h_15px'], 81 | ['boxSizingborder-box', 'ub-box-szg_border-box'] 82 | ] 83 | }) 84 | }) 85 | 86 | test.serial('returns minified css in production', t => { 87 | process.env.NODE_ENV = 'production' 88 | shallow() 89 | t.deepEqual( 90 | extractStyles().styles, 91 | `.ub-h_11px{height:11px}.ub-box-szg_border-box{box-sizing:border-box}` 92 | ) 93 | }) 94 | -------------------------------------------------------------------------------- /src/enhancers/border-radius.ts: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import getCss from '../get-css' 3 | import {spacesOutsideParentheses} from '../utils/regex' 4 | import { PropEnhancerValueType, PropValidators, PropEnhancers, PropTypesMapping } from '../types/enhancers' 5 | 6 | export const propTypes: PropTypesMapping = { 7 | borderBottomLeftRadius: PropTypes.oneOfType([ 8 | PropTypes.string, 9 | PropTypes.number 10 | ]), 11 | borderBottomRightRadius: PropTypes.oneOfType([ 12 | PropTypes.string, 13 | PropTypes.number 14 | ]), 15 | borderRadius: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 16 | borderTopLeftRadius: PropTypes.oneOfType([ 17 | PropTypes.string, 18 | PropTypes.number 19 | ]), 20 | borderTopRightRadius: PropTypes.oneOfType([ 21 | PropTypes.string, 22 | PropTypes.number 23 | ]) 24 | } 25 | 26 | export const propAliases = { 27 | borderRadius: [ 28 | 'borderBottomLeftRadius', 29 | 'borderBottomRightRadius', 30 | 'borderTopLeftRadius', 31 | 'borderTopRightRadius' 32 | ] 33 | } 34 | 35 | export const propValidators: PropValidators = {} 36 | 37 | if (process.env.NODE_ENV !== 'production') { 38 | propValidators.borderRadius = value => { 39 | if (spacesOutsideParentheses.test(value)) { 40 | return `multiple values (“${value}”) aren՚t supported with “borderRadius”. Use “borderBottomLeftRadius”, “borderBottomRightRadius” “borderTopLeftRadius” and “borderTopRightRadius” instead.` 41 | } 42 | 43 | return 44 | } 45 | } 46 | 47 | const borderTopLeftRadius = { 48 | className: 'btlr', 49 | cssName: 'border-top-left-radius', 50 | jsName: 'borderTopLeftRadius' 51 | } 52 | const borderTopRightRadius = { 53 | className: 'btrr', 54 | cssName: 'border-top-right-radius', 55 | jsName: 'borderTopRightRadius' 56 | } 57 | const borderBottomLeftRadius = { 58 | className: 'bblr', 59 | cssName: 'border-bottom-left-radius', 60 | jsName: 'borderBottomLeftRadius' 61 | } 62 | const borderBottomRightRadius = { 63 | className: 'bbrr', 64 | cssName: 'border-bottom-right-radius', 65 | jsName: 'borderBottomRightRadius' 66 | } 67 | 68 | export const propEnhancers: PropEnhancers = { 69 | borderBottomLeftRadius: (value: PropEnhancerValueType, selector: string) => getCss(borderBottomLeftRadius, value, selector), 70 | borderBottomRightRadius: (value: PropEnhancerValueType, selector: string) => getCss(borderBottomRightRadius, value, selector), 71 | borderTopLeftRadius: (value: PropEnhancerValueType, selector: string) => getCss(borderTopLeftRadius, value, selector), 72 | borderTopRightRadius: (value: PropEnhancerValueType, selector: string) => getCss(borderTopRightRadius, value, selector) 73 | } 74 | -------------------------------------------------------------------------------- /src/enhancers/layout.ts: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import getCss from '../get-css' 3 | import { getClassNamePrefix } from '../get-class-name' 4 | import { PropEnhancerValueType, PropValidators, PropEnhancers, PropTypesMapping, PropAliases } from '../types/enhancers' 5 | import { Rule } from '../prefixer' 6 | 7 | export const propTypes: PropTypesMapping = { 8 | boxSizing: PropTypes.string, 9 | clear: PropTypes.string, 10 | clearfix: PropTypes.bool, 11 | content: PropTypes.string, 12 | display: PropTypes.string, 13 | float: PropTypes.string, 14 | zIndex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) 15 | } 16 | 17 | export const propAliases: PropAliases = {} 18 | export const propValidators: PropValidators = {} 19 | 20 | const display = { 21 | className: 'dspl', 22 | cssName: 'display', 23 | jsName: 'display', 24 | safeValue: true, 25 | isPrefixed: true 26 | } 27 | 28 | const float = { 29 | className: 'flt', 30 | cssName: 'float', 31 | jsName: 'float', 32 | safeValue: true 33 | } 34 | 35 | const clear = { 36 | className: 'clr', 37 | cssName: 'clear', 38 | jsName: 'clear', 39 | safeValue: true 40 | } 41 | 42 | const zIndex = { 43 | className: 'z-idx', 44 | cssName: 'z-index', 45 | jsName: 'zIndex', 46 | safeValue: true, 47 | defaultUnit: '' 48 | } 49 | 50 | const boxSizing = { 51 | className: 'box-szg', 52 | cssName: 'box-sizing', 53 | jsName: 'boxSizing', 54 | safeValue: true 55 | } 56 | 57 | const clearfix = () => { 58 | const className = `${getClassNamePrefix()}clearfix` 59 | const rules: Rule[] = [ 60 | { property: 'display', value: 'table' }, 61 | { property: 'clear', value: 'both' }, 62 | { property: 'content', value: '""' } 63 | ] 64 | const concatenatedRules = rules.map(rule => ` ${rule.property}: ${rule.value};`).join('\n') 65 | const styles = `\n.${className}:before, .${className}:after {\n${concatenatedRules}\n}` 66 | return { className, rules, styles } 67 | } 68 | 69 | const content = { 70 | className: 'cnt', 71 | cssName: 'content', 72 | jsName: 'content', 73 | complexValue: true 74 | } 75 | 76 | export const propEnhancers: PropEnhancers = { 77 | boxSizing: (value: PropEnhancerValueType, selector: string) => getCss(boxSizing, value, selector), 78 | clear: (value: PropEnhancerValueType, selector: string) => getCss(clear, value, selector), 79 | clearfix, 80 | content: (value: PropEnhancerValueType, selector: string) => getCss(content, value, selector), 81 | display: (value: PropEnhancerValueType, selector: string) => getCss(display, value, selector), 82 | float: (value: PropEnhancerValueType, selector: string) => getCss(float, value, selector), 83 | zIndex: (value: PropEnhancerValueType, selector: string) => getCss(zIndex, value, selector) 84 | } 85 | -------------------------------------------------------------------------------- /src/enhancers/background.ts: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import getCss from '../get-css' 3 | import { PropValidators, PropTypesMapping, PropEnhancerValueType, PropAliases, PropEnhancers } from '../types/enhancers' 4 | 5 | export const propTypes: PropTypesMapping = { 6 | background: PropTypes.string, 7 | backgroundBlendMode: PropTypes.string, 8 | backgroundClip: PropTypes.string, 9 | backgroundColor: PropTypes.string, 10 | backgroundImage: PropTypes.string, 11 | backgroundOrigin: PropTypes.string, 12 | backgroundPosition: PropTypes.string, 13 | backgroundRepeat: PropTypes.string, 14 | backgroundSize: PropTypes.string 15 | } 16 | 17 | export const propAliases: PropAliases = {} 18 | 19 | export const propValidators: PropValidators = {} 20 | 21 | const background = { 22 | className: 'bg', 23 | cssName: 'background', 24 | jsName: 'background', 25 | isPrefixed: true, 26 | complexValue: true 27 | } 28 | const backgroundColor = { 29 | className: 'bg-clr', 30 | cssName: 'background-color', 31 | jsName: 'backgroundColor' 32 | } 33 | const backgroundImage = { 34 | className: 'bg-img', 35 | cssName: 'background-image', 36 | jsName: 'backgroundImage', 37 | isPrefixed: true, 38 | complexValue: true 39 | } 40 | const backgroundPosition = { 41 | className: 'bg-pos', 42 | cssName: 'background-position', 43 | jsName: 'backgroundPosition' 44 | } 45 | const backgroundSize = { 46 | className: 'bg-siz', 47 | cssName: 'background-size', 48 | jsName: 'backgroundSize' 49 | } 50 | const backgroundOrigin = { 51 | className: 'bg-orgn', 52 | cssName: 'background-origin', 53 | jsName: 'backgroundOrigin' 54 | } 55 | const backgroundRepeat = { 56 | className: 'bg-rpt', 57 | cssName: 'background-repeat', 58 | jsName: 'backgroundRepeat' 59 | } 60 | const backgroundClip = { 61 | className: 'bg-clp', 62 | cssName: 'background-clip', 63 | jsName: 'backgroundClip' 64 | } 65 | const backgroundBlendMode = { 66 | className: 'bg-blnd-md', 67 | cssName: 'background-blend-mode', 68 | jsName: 'backgroundBlendMode' 69 | } 70 | 71 | export const propEnhancers: PropEnhancers = { 72 | background: (value: PropEnhancerValueType, selector: string) => getCss(background, value, selector), 73 | backgroundBlendMode: (value: PropEnhancerValueType, selector: string) => getCss(backgroundBlendMode, value, selector), 74 | backgroundClip: (value: PropEnhancerValueType, selector: string) => getCss(backgroundClip, value, selector), 75 | backgroundColor: (value: PropEnhancerValueType, selector: string) => getCss(backgroundColor, value, selector), 76 | backgroundImage: (value: PropEnhancerValueType, selector: string) => getCss(backgroundImage, value, selector), 77 | backgroundOrigin: (value: PropEnhancerValueType, selector: string) => getCss(backgroundOrigin, value, selector), 78 | backgroundPosition: (value: PropEnhancerValueType, selector: string) => getCss(backgroundPosition, value, selector), 79 | backgroundRepeat: (value: PropEnhancerValueType, selector: string) => getCss(backgroundRepeat, value, selector), 80 | backgroundSize: (value: PropEnhancerValueType, selector: string) => getCss(backgroundSize, value, selector) 81 | } 82 | -------------------------------------------------------------------------------- /src/utils/safeHref.ts: -------------------------------------------------------------------------------- 1 | export interface URLInfo { 2 | url: string | undefined 3 | sameOrigin: boolean 4 | } 5 | 6 | export interface SafeHrefConfigObj { 7 | enabled?: boolean 8 | origin?: string 9 | } 10 | 11 | const PROTOCOL_REGEX = /^[a-z]+:/ 12 | const ORIGIN_REGEX = /^(?:[a-z]+:?:)?(?:\/\/)?([^\/\?]+)/ 13 | let useSafeHref = true 14 | let globalOrigin = typeof window !== 'undefined' ? window.location.origin : false 15 | 16 | export function configureSafeHref(configObject: SafeHrefConfigObj) { 17 | if (typeof configObject.enabled === 'boolean') { 18 | useSafeHref = configObject.enabled 19 | } 20 | 21 | if (configObject.origin) { 22 | globalOrigin = configObject.origin 23 | } 24 | } 25 | 26 | export function getUseSafeHref(): boolean { 27 | return useSafeHref 28 | } 29 | 30 | export function getURLInfo(url: string): URLInfo { 31 | /** 32 | * An array of the safely allowed url protocols 33 | */ 34 | const safeProtocols = ['http:', 'https:', 'mailto:', 'tel:', 'data:'] 35 | 36 | /** 37 | * - Find protocol of URL or set to 'relative' 38 | * - Find origin of URL 39 | * - Determine if sameOrigin 40 | * - Determine if protocol of URL is safe 41 | */ 42 | const protocolResult = url.match(PROTOCOL_REGEX) 43 | const originResult = url.match(ORIGIN_REGEX) 44 | const urlProtocol = protocolResult ? protocolResult[0] : 'relative' 45 | let sameOrigin = urlProtocol === 'relative' 46 | if (!sameOrigin && globalOrigin) { 47 | sameOrigin = globalOrigin === (originResult && originResult[0]) 48 | } 49 | 50 | const isSafeProtocol = sameOrigin ? true : safeProtocols.includes(urlProtocol) 51 | if (!isSafeProtocol) { 52 | /** 53 | * If the url is unsafe, put a error in the console, and return the URLInfo object 54 | * with the value of url being `undefined` 55 | */ 56 | console.error( 57 | '📦 `href` passed to anchor tag is unsafe. Because of this, the `href` on the element was not set. Please review the safe href documentation if you have questions.', 58 | 'https://www.github.com/segmentio/ui-box' 59 | ) 60 | return { 61 | url: undefined, 62 | sameOrigin 63 | } 64 | } 65 | 66 | /** 67 | * If the url is safe, return the url and origin 68 | */ 69 | return { 70 | url, 71 | sameOrigin 72 | } 73 | } 74 | 75 | export function extractAnchorProps(href: string, rel: string) { 76 | /** 77 | * Get url info and update href 78 | */ 79 | const urlInfo = getURLInfo(href) 80 | const safeHref = urlInfo.url 81 | 82 | /** 83 | * If the url passed is safe, we want to also update the attributes of the element 84 | * to be safe 85 | */ 86 | let safeRel = rel || '' 87 | if (urlInfo.url) { 88 | if (!safeRel.includes('noopener')) { 89 | safeRel += `${safeRel.length > 0 ? ' ' : ''}noopener` 90 | } 91 | 92 | if (!safeRel.includes('noreferrer') && !urlInfo.sameOrigin) { 93 | safeRel += `${safeRel.length > 0 ? ' ' : ''}noreferrer` 94 | } 95 | } 96 | 97 | return { 98 | safeHref, 99 | safeRel 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/enhance-props.ts: -------------------------------------------------------------------------------- 1 | import { propEnhancers } from './enhancers' 2 | import expandAliases from './expand-aliases' 3 | import * as cache from './cache' 4 | import * as styles from './styles' 5 | import { Without } from './types/box-types' 6 | import { BoxPropValue, EnhancerProps } from './types/enhancers' 7 | 8 | type PreservedProps = Without, keyof EnhancerProps> 9 | 10 | interface EnhancePropsResult { 11 | className: string 12 | enhancedProps: PreservedProps 13 | } 14 | 15 | const SELECTORS_PROP = 'selectors' 16 | 17 | /** 18 | * Converts the CSS props to class names and inserts the styles. 19 | */ 20 | export default function enhanceProps( 21 | props: EnhancerProps & React.ComponentPropsWithoutRef, 22 | selectorHead = '', 23 | parentProperty = '' 24 | ): EnhancePropsResult { 25 | const propsMap = expandAliases(props) 26 | const preservedProps: PreservedProps = {} 27 | let className: string = props.className || '' 28 | 29 | for (const [property, value] of propsMap) { 30 | const isSelectorOrChildProp = property === SELECTORS_PROP || parentProperty.length > 0 31 | // Only attempt to process objects for the `selectors` prop or the individual selectors below it 32 | if (isObject(value) && isSelectorOrChildProp) { 33 | const prop = property === SELECTORS_PROP ? '' : property 34 | 35 | // If the selector head includes a comma, map over selectorHead and attach property for nested selectors so it isn't just added to the last entry 36 | // i.e. [aria-current="page"],[aria-selected="true"] + :before -> [aria-current="page"]:before,[aria-selected="true"]:before instead of [aria-current="page"],[aria-selected="true"]:before 37 | const newSelectorHead = selectorHead.includes(',') 38 | ? selectorHead 39 | .split(',') 40 | .map(selector => `${selector}${prop}`) 41 | .join(',') 42 | : `${selectorHead}${prop}` 43 | const parsed = enhanceProps(value, noAnd(newSelectorHead), property) 44 | className = `${className} ${parsed.className}` 45 | continue 46 | } 47 | 48 | const enhancer = propEnhancers[property] 49 | if (!enhancer) { 50 | // Pass through native props. e.g: disabled, value, type 51 | preservedProps[property] = value 52 | continue 53 | } 54 | 55 | // Skip false boolean enhancers. e.g: `clearfix={false}` 56 | // Also allows omitting props via overriding with `null` (i.e: neutralising props) 57 | if (value === null || value === undefined || value === false) { 58 | continue 59 | } 60 | 61 | const cachedClassName = cache.get(property, value, selectorHead) 62 | if (cachedClassName) { 63 | className = `${className} ${cachedClassName}` 64 | continue 65 | } 66 | 67 | const newCss = enhancer(value, selectorHead) 68 | // Allow enhancers to return null for invalid values 69 | if (newCss) { 70 | styles.add(newCss.styles) 71 | cache.set(property, value, newCss.className, selectorHead) 72 | className = `${className} ${newCss.className}` 73 | } 74 | } 75 | 76 | className = className.trim() 77 | 78 | return { className, enhancedProps: preservedProps } 79 | } 80 | 81 | const isObject = (value: BoxPropValue | object): value is object => value != null && typeof value === 'object' 82 | 83 | const noAnd = (value: string): string => value.replace(/&/g, '') 84 | -------------------------------------------------------------------------------- /test/box.tsx: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import React, { CSSProperties } from 'react' 3 | import * as render from 'react-test-renderer' 4 | import { shallow } from 'enzyme' 5 | import * as sinon from 'sinon' 6 | import Box from '../src/box' 7 | import * as styles from '../src/styles' 8 | import allPropertiesComponent from '../tools/all-properties-component' 9 | import { propNames } from '../src/enhancers' 10 | 11 | test.afterEach.always(() => { 12 | styles.clear() 13 | }) 14 | 15 | test.serial('all properties', t => { 16 | const component = allPropertiesComponent() 17 | const tree = render.create(component).toJSON() 18 | t.snapshot(tree, 'DOM') 19 | t.snapshot(styles.getAll(), 'CSS') 20 | }) 21 | 22 | test.serial('all properties set to inherit', t => { 23 | const properties: any = {} 24 | for (const name of propNames) { 25 | properties[name] = 'inherit' 26 | } 27 | 28 | delete properties.clearfix // Non-css property 29 | const component = 30 | shallow(component) 31 | t.snapshot(styles.getAll(), 'CSS') 32 | }) 33 | 34 | test.serial('all properties set to initial', t => { 35 | const properties: any = {} 36 | for (const name of propNames) { 37 | properties[name] = 'initial' 38 | } 39 | 40 | delete properties.clearfix // Non-css property 41 | const component = 42 | shallow(component) 43 | t.snapshot(styles.getAll(), 'CSS') 44 | }) 45 | 46 | test('is prop allows changing the dom element type', t => { 47 | const component = shallow() 48 | t.true(component.is('h1')) 49 | }) 50 | 51 | test('is prop allows changing the component type', t => { 52 | function TestComponent(props) { 53 | return

54 | } 55 | 56 | const component = shallow() 57 | t.true(component.is(TestComponent)) 58 | }) 59 | 60 | test('ref gets forwarded', t => { 61 | const node = { domNode: true } 62 | const ref = sinon.spy() 63 | render.create(, { 64 | createNodeMock() { 65 | return node 66 | } 67 | }) 68 | t.true(ref.calledOnce) 69 | t.is(ref.args[0][0], node) 70 | }) 71 | 72 | test('renders children', t => { 73 | const component = shallow( 74 | 75 |

Hi

76 |
77 | ) 78 | t.true(component.contains(

Hi

)) 79 | }) 80 | 81 | test('maintains the original className', t => { 82 | const component = shallow() 83 | t.true(component.hasClass('derp')) 84 | }) 85 | 86 | test('renders with style prop', t => { 87 | const expected: CSSProperties = { backgroundColor: 'red' } 88 | 89 | const component = shallow() 90 | 91 | t.deepEqual(component.prop('style'), expected) 92 | }) 93 | 94 | test('renders with arbitrary non-enhancer props', t => { 95 | interface CustomComponentProps { 96 | foo: string 97 | baz: number 98 | fizz: { 99 | buzz: boolean 100 | } 101 | } 102 | 103 | const CustomComponent: React.FC = props => {JSON.stringify(props, undefined, 4)} 104 | 105 | const component = shallow() 106 | 107 | t.is(component.prop('foo'), 'bar') 108 | t.is(component.prop('baz'), 123) 109 | t.deepEqual(component.prop('fizz'), { buzz: true }) 110 | }) 111 | -------------------------------------------------------------------------------- /src/enhancers/animation.ts: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import getCss from '../get-css' 3 | import { PropValidators, PropTypesMapping, PropEnhancerValueType, PropAliases, PropEnhancers } from '../types/enhancers' 4 | 5 | export const propTypes: PropTypesMapping = { 6 | animation: PropTypes.string, 7 | animationDelay: PropTypes.string, 8 | animationDirection: PropTypes.string, 9 | animationDuration: PropTypes.string, 10 | animationFillMode: PropTypes.string, 11 | animationIterationCount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 12 | animationName: PropTypes.string, 13 | animationPlayState: PropTypes.string, 14 | animationTimingFunction: PropTypes.string 15 | } 16 | 17 | export const propAliases: PropAliases = {} 18 | 19 | export const propValidators: PropValidators = {} 20 | 21 | const animation = { 22 | className: 'a', 23 | cssName: 'animation', 24 | jsName: 'animation', 25 | complexValue: true 26 | } 27 | 28 | const animationDelay = { 29 | className: 'a-dly', 30 | cssName: 'animation-delay', 31 | jsName: 'animationDelay', 32 | defaultUnit: 'ms' 33 | } 34 | 35 | const animationDirection = { 36 | className: 'a-dir', 37 | cssName: 'animation-direction', 38 | jsName: 'animationDirection', 39 | safeValue: true 40 | } 41 | 42 | const animationDuration = { 43 | className: 'a-dur', 44 | cssName: 'animation-duration', 45 | jsName: 'animationDuration', 46 | defaultUnit: 'ms' 47 | } 48 | 49 | const animationFillMode = { 50 | className: 'a-fill-md', 51 | cssName: 'animation-fill-mode', 52 | jsName: 'animationFillMode', 53 | safeValue: true 54 | } 55 | 56 | const animationIterationCount = { 57 | className: 'a-itr-ct', 58 | cssName: 'animation-iteration-count', 59 | jsName: 'animationIterationCount', 60 | defaultUnit: '' 61 | } 62 | 63 | const animationName = { 64 | className: 'a-nm', 65 | cssName: 'animation-name', 66 | jsName: 'animationName' 67 | } 68 | 69 | const animationPlayState = { 70 | className: 'a-ply-ste', 71 | cssName: 'animation-play-state', 72 | jsName: 'animationPlayState', 73 | safeValue: true 74 | } 75 | 76 | const animationTimingFunction = { 77 | className: 'a-tmng-fn', 78 | cssName: 'animation-timing-function', 79 | jsName: 'animationTimingFunction', 80 | complexValue: true 81 | } 82 | 83 | export const propEnhancers: PropEnhancers = { 84 | animation: (value: PropEnhancerValueType, selector: string) => getCss(animation, value, selector), 85 | animationDelay: (value: PropEnhancerValueType, selector: string) => getCss(animationDelay, value, selector), 86 | animationDirection: (value: PropEnhancerValueType, selector: string) => getCss(animationDirection, value, selector), 87 | animationDuration: (value: PropEnhancerValueType, selector: string) => getCss(animationDuration, value, selector), 88 | animationFillMode: (value: PropEnhancerValueType, selector: string) => getCss(animationFillMode, value, selector), 89 | animationIterationCount: (value: PropEnhancerValueType, selector: string) => 90 | getCss(animationIterationCount, value, selector), 91 | animationName: (value: PropEnhancerValueType, selector: string) => getCss(animationName, value, selector), 92 | animationPlayState: (value: PropEnhancerValueType, selector: string) => getCss(animationPlayState, value, selector), 93 | animationTimingFunction: (value: PropEnhancerValueType, selector: string) => 94 | getCss(animationTimingFunction, value, selector) 95 | } 96 | -------------------------------------------------------------------------------- /src/keyframes.ts: -------------------------------------------------------------------------------- 1 | import { KeyframesTimeline, KeyframesTimelineKey } from './types/keyframes' 2 | import hash from '@emotion/hash' 3 | import flattenObject from './utils/flatten-object' 4 | import { propEnhancers } from './enhancers' 5 | import { BoxCssProps, CssProps } from './types/enhancers' 6 | import { Rule } from './prefixer' 7 | import isProduction from './utils/is-production' 8 | import * as stylesheet from './styles' 9 | import * as cache from './cache' 10 | 11 | /** 12 | * Generates a unique keyframe name and injects the styles into the stylesheet. 13 | * @returns Generated name of the keyframe to use in animation props, i.e. `openAnimation_65p985` 14 | * 15 | * @example 16 | * const openAnimation = keyframes('openAnimation', { 17 | * from: { 18 | * opacity: 0, 19 | * transform: 'translateY(-120%)' 20 | * }, 21 | * to: { 22 | * transform: 'translateY(0)' 23 | * } 24 | * }) 25 | * 26 | * 27 | */ 28 | const keyframes = (friendlyName: string, timeline: KeyframesTimeline): string => { 29 | const hashedValue = hash(flattenObject(timeline)) 30 | const name = `${friendlyName}_${hashedValue}` 31 | 32 | // Check for styles in cache before continuing 33 | const cachedStyles = cache.get(friendlyName, hashedValue, 'keyframe') 34 | if (cachedStyles != null) { 35 | return name 36 | } 37 | 38 | const keys = Object.keys(timeline) as KeyframesTimelineKey[] 39 | const timelineStyles = keys.map(key => getStylesForTimelineKey(key, timeline[key] || {})) 40 | 41 | const styles = getKeyframesStyles(name, timelineStyles) 42 | 43 | cache.set(friendlyName, hashedValue, styles, 'keyframe') 44 | stylesheet.add(styles) 45 | 46 | return name 47 | } 48 | 49 | const flatten = (values: T[][]): T[] => { 50 | const flattenedValues: T[] = [] 51 | return flattenedValues.concat(...values) 52 | } 53 | 54 | /** 55 | * Returns the style string with the rules for the given timeline key 56 | * @example 57 | * ``` 58 | * from { 59 | * opacity: 0; 60 | * transform: translateY(-120%); 61 | * }``` 62 | */ 63 | const getStylesForTimelineKey = (timelineKey: KeyframesTimelineKey, cssProps: BoxCssProps): string => { 64 | const cssPropKeys = Object.keys(cssProps) as Array> 65 | const rules = flatten(cssPropKeys.map(cssPropKey => getRulesForKey(cssPropKey, cssProps))) 66 | const key = timelineKeyToString(timelineKey) 67 | const rulesString = rules 68 | .map(rule => { 69 | const { property, value } = rule 70 | if (isProduction()) { 71 | return `${property}:${value};` 72 | } 73 | 74 | return ` ${property}: ${value};` 75 | }) 76 | .join(isProduction() ? '' : '\n') 77 | 78 | if (isProduction()) { 79 | return `${key} {${rulesString}}` 80 | } 81 | 82 | return ` ${key} {\n${rulesString}\n }` 83 | } 84 | 85 | const getRulesForKey = (key: keyof BoxCssProps, cssProps: BoxCssProps): Rule[] => { 86 | const value = cssProps[key] 87 | const enhancer = propEnhancers[key] 88 | 89 | if (enhancer == null || value == null || value === false) { 90 | return [] 91 | } 92 | 93 | const enhancedProp = enhancer(value, '') 94 | if (enhancedProp == null) { 95 | return [] 96 | } 97 | 98 | return enhancedProp.rules 99 | } 100 | 101 | /** 102 | * Returns keyframes style string for insertion into the stylesheet 103 | * @example 104 | * ```@keyframes openAnimation_65p985 { 105 | * from { 106 | * opacity: 0; 107 | * transform: translateY(-120%); 108 | * } 109 | * to { 110 | * transform: translateY(0); 111 | * } 112 | * }``` 113 | */ 114 | const getKeyframesStyles = (name: string, rules: string[]): string => { 115 | const separator = isProduction() ? '' : '\n' 116 | const openBrace = `{${separator}` 117 | const closeBrace = `${separator}}` 118 | const concatenatedRules = rules.join(separator) 119 | 120 | return `@keyframes ${name} ${openBrace}${concatenatedRules}${closeBrace}` 121 | } 122 | 123 | const timelineKeyToString = (timelineKey: KeyframesTimelineKey): string => { 124 | const isNumber = !isNaN(Number(timelineKey)) 125 | return isNumber ? `${timelineKey}%` : timelineKey.toString() 126 | } 127 | 128 | export default keyframes 129 | -------------------------------------------------------------------------------- /test/get-css.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import getCss from '../src/get-css' 3 | 4 | const originalNodeEnv = process.env.NODE_ENV 5 | test.afterEach.always(() => { 6 | process.env.NODE_ENV = originalNodeEnv 7 | }) 8 | 9 | test('supports basic prop + value', t => { 10 | const propInfo = { 11 | className: 'min-w', 12 | cssName: 'min-width', 13 | jsName: 'minWidth' 14 | } 15 | const result = getCss(propInfo, '10px') 16 | t.deepEqual(result, { 17 | className: 'ub-min-w_10px', 18 | styles: ` 19 | .ub-min-w_10px { 20 | min-width: 10px; 21 | }`, 22 | rules: [{ property: 'min-width', value: '10px' }] 23 | }) 24 | }) 25 | 26 | test('supports number value', t => { 27 | const propInfo = { 28 | className: 'min-w', 29 | cssName: 'min-width', 30 | jsName: 'minWidth' 31 | } 32 | const result = getCss(propInfo, 10) 33 | t.deepEqual(result, { 34 | className: 'ub-min-w_10px', 35 | styles: ` 36 | .ub-min-w_10px { 37 | min-width: 10px; 38 | }`, 39 | rules: [{ property: 'min-width', value: '10px' }] 40 | }) 41 | }) 42 | 43 | test('adds prefixes', t => { 44 | const propInfo = { 45 | className: 'usr-slct', 46 | cssName: 'user-select', 47 | jsName: 'userSelect', 48 | safeValue: true, 49 | isPrefixed: true 50 | } 51 | const result = getCss(propInfo, 'none') 52 | t.deepEqual( 53 | result.styles, 54 | ` 55 | .ub-usr-slct_none { 56 | -webkit-user-select: none; 57 | -moz-user-select: none; 58 | -ms-user-select: none; 59 | user-select: none; 60 | }` 61 | ) 62 | }) 63 | 64 | test.serial('returns minified css in production', t => { 65 | process.env.NODE_ENV = 'production' 66 | const propInfo = { 67 | className: 'usr-slct', 68 | cssName: 'user-select', 69 | jsName: 'userSelect', 70 | safeValue: true, 71 | isPrefixed: true 72 | } 73 | const result = getCss(propInfo, 'none') 74 | t.deepEqual( 75 | result.styles, 76 | '.ub-usr-slct_none{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}' 77 | ) 78 | }) 79 | 80 | test('appends selector when present', t => { 81 | const propInfo = { 82 | className: 'bg-clr', 83 | cssName: 'background-color', 84 | jsName: 'backgroundColor' 85 | } 86 | 87 | const result = getCss(propInfo, 'blue', ':hover') 88 | 89 | t.deepEqual(result, { 90 | className: 'ub-bg-clr_blue_1k2el8q', 91 | styles: ` 92 | .ub-bg-clr_blue_1k2el8q:hover { 93 | background-color: blue; 94 | }`, 95 | rules: [{ property: 'background-color', value: 'blue' }] 96 | }) 97 | }) 98 | 99 | test('expands comma-separated selectors for class name', t => { 100 | const propInfo = { 101 | className: 'bg-clr', 102 | cssName: 'background-color', 103 | jsName: 'backgroundColor' 104 | } 105 | 106 | const result = getCss(propInfo, 'blue', ':hover,[data-active=true]') 107 | 108 | t.deepEqual(result, { 109 | className: 'ub-bg-clr_blue_vwpa9u', 110 | styles: ` 111 | .ub-bg-clr_blue_vwpa9u:hover, .ub-bg-clr_blue_vwpa9u[data-active=true] { 112 | background-color: blue; 113 | }`, 114 | rules: [{ property: 'background-color', value: 'blue' }] 115 | }) 116 | }) 117 | 118 | test('props with same value but different selector should be unique', t => { 119 | const propInfo = { 120 | className: 'bg-clr', 121 | cssName: 'background-color', 122 | jsName: 'backgroundColor' 123 | } 124 | 125 | const blueHover = getCss(propInfo, 'blue', ':hover') 126 | const blueDisabled = getCss(propInfo, 'blue', ':disabled') 127 | 128 | t.notDeepEqual(blueHover!.className, blueDisabled!.className) 129 | }) 130 | 131 | test('maintains whitespace when expanding comma-separated selectors', t => { 132 | const propInfo = { 133 | className: 'bg-clr', 134 | cssName: 'background-color', 135 | jsName: 'backgroundColor' 136 | } 137 | 138 | const result = getCss(propInfo, 'blue', '.fancy-link:hover, .fancy-input:active') 139 | t.deepEqual(result, { 140 | className: 'ub-bg-clr_blue_ua1hgg', 141 | // Intentionally expecting '.fancy-link' to be up against main class name since it does not start w/ a space 142 | // while ' .fancy-input' should maintain space 143 | styles: ` 144 | .ub-bg-clr_blue_ua1hgg.fancy-link:hover, .ub-bg-clr_blue_ua1hgg .fancy-input:active { 145 | background-color: blue; 146 | }`, 147 | rules: [{ property: 'background-color', value: 'blue' }] 148 | }) 149 | }) 150 | -------------------------------------------------------------------------------- /src/enhancers/index.ts: -------------------------------------------------------------------------------- 1 | import * as animation from './animation' 2 | import * as background from './background' 3 | import * as borderRadius from './border-radius' 4 | import * as borders from './borders' 5 | import * as boxShadow from './box-shadow' 6 | import * as dimensions from './dimensions' 7 | import * as flex from './flex' 8 | import * as grid from './grid' 9 | import * as interaction from './interaction' 10 | import * as layout from './layout' 11 | import * as list from './list' 12 | import * as opacity from './opacity' 13 | import * as outline from './outline' 14 | import * as overflow from './overflow' 15 | import * as position from './position' 16 | import * as resize from './resize' 17 | import * as selectors from './selectors' 18 | import * as spacing from './spacing' 19 | import * as svg from './svg' 20 | import * as text from './text' 21 | import * as transform from './transform' 22 | import * as transition from './transition' 23 | import { PropValidators, PropEnhancers, PropAliases, PropTypesMapping } from '../types/enhancers' 24 | 25 | export { 26 | animation, 27 | background, 28 | borderRadius, 29 | borders, 30 | boxShadow, 31 | dimensions, 32 | flex, 33 | grid, 34 | interaction, 35 | layout, 36 | list, 37 | opacity, 38 | outline, 39 | overflow, 40 | position, 41 | resize, 42 | selectors, 43 | spacing, 44 | svg, 45 | text, 46 | transform, 47 | transition 48 | } 49 | 50 | export const propTypes: PropTypesMapping = { 51 | ...animation.propTypes, 52 | ...background.propTypes, 53 | ...borderRadius.propTypes, 54 | ...borders.propTypes, 55 | ...boxShadow.propTypes, 56 | ...dimensions.propTypes, 57 | ...flex.propTypes, 58 | ...grid.propTypes, 59 | ...interaction.propTypes, 60 | ...layout.propTypes, 61 | ...list.propTypes, 62 | ...opacity.propTypes, 63 | ...outline.propTypes, 64 | ...overflow.propTypes, 65 | ...position.propTypes, 66 | ...resize.propTypes, 67 | ...selectors.propTypes, 68 | ...spacing.propTypes, 69 | ...svg.propTypes, 70 | ...text.propTypes, 71 | ...transform.propTypes, 72 | ...transition.propTypes 73 | } 74 | 75 | export const propNames = Object.keys(propTypes) 76 | 77 | export const propAliases: PropAliases = { 78 | ...animation.propAliases, 79 | ...background.propAliases, 80 | ...borderRadius.propAliases, 81 | ...borders.propAliases, 82 | ...boxShadow.propAliases, 83 | ...dimensions.propAliases, 84 | ...flex.propAliases, 85 | ...grid.propAliases, 86 | ...interaction.propAliases, 87 | ...layout.propAliases, 88 | ...list.propAliases, 89 | ...opacity.propAliases, 90 | ...outline.propAliases, 91 | ...overflow.propAliases, 92 | ...position.propAliases, 93 | ...resize.propAliases, 94 | ...selectors.propAliases, 95 | ...spacing.propAliases, 96 | ...svg.propAliases, 97 | ...text.propAliases, 98 | ...transform.propAliases, 99 | ...transition.propAliases 100 | } 101 | 102 | export const propValidators: PropValidators = { 103 | ...animation.propValidators, 104 | ...background.propValidators, 105 | ...borderRadius.propValidators, 106 | ...borders.propValidators, 107 | ...boxShadow.propValidators, 108 | ...dimensions.propValidators, 109 | ...flex.propValidators, 110 | ...grid.propValidators, 111 | ...interaction.propValidators, 112 | ...layout.propValidators, 113 | ...list.propValidators, 114 | ...opacity.propValidators, 115 | ...outline.propValidators, 116 | ...overflow.propValidators, 117 | ...position.propValidators, 118 | ...resize.propValidators, 119 | ...selectors.propValidators, 120 | ...spacing.propValidators, 121 | ...svg.propValidators, 122 | ...text.propValidators, 123 | ...transform.propValidators, 124 | ...transition.propValidators 125 | } 126 | 127 | export const propEnhancers: PropEnhancers = { 128 | ...animation.propEnhancers, 129 | ...background.propEnhancers, 130 | ...borderRadius.propEnhancers, 131 | ...borders.propEnhancers, 132 | ...boxShadow.propEnhancers, 133 | ...dimensions.propEnhancers, 134 | ...flex.propEnhancers, 135 | ...grid.propEnhancers, 136 | ...interaction.propEnhancers, 137 | ...layout.propEnhancers, 138 | ...list.propEnhancers, 139 | ...opacity.propEnhancers, 140 | ...outline.propEnhancers, 141 | ...overflow.propEnhancers, 142 | ...position.propEnhancers, 143 | ...resize.propEnhancers, 144 | ...selectors.propEnhancers, 145 | ...spacing.propEnhancers, 146 | ...svg.propEnhancers, 147 | ...text.propEnhancers, 148 | ...transform.propEnhancers, 149 | ...transition.propEnhancers 150 | } 151 | -------------------------------------------------------------------------------- /src/utils/style-sheet.ts: -------------------------------------------------------------------------------- 1 | import isProduction from './is-production' 2 | 3 | /** 4 | * This file is based off glamor's StyleSheet 5 | * @see https://github.com/threepointone/glamor/blob/v2.20.40/src/sheet.js 6 | */ 7 | 8 | const isBrowser = typeof window !== 'undefined' 9 | 10 | function last(arr: T[]) { 11 | return arr[arr.length - 1] 12 | } 13 | 14 | function sheetForTag(tag: HTMLStyleElement): CSSStyleSheet | undefined { 15 | if (tag.sheet) { 16 | return tag.sheet as CSSStyleSheet 17 | } 18 | 19 | // This weirdness brought to you by firefox 20 | for (let i = 0; i < document.styleSheets.length; i += 1) { 21 | if (document.styleSheets[i].ownerNode === tag) { 22 | return document.styleSheets[i] as CSSStyleSheet 23 | } 24 | } 25 | 26 | return 27 | } 28 | 29 | function makeStyleTag() { 30 | const tag = document.createElement('style') 31 | tag.type = 'text/css' 32 | tag.setAttribute('data-ui-box', '') 33 | tag.append(document.createTextNode('')) 34 | ;(document.head || document.querySelector('head')).append(tag) 35 | return tag 36 | } 37 | 38 | type Writeable = { -readonly [P in keyof T]: T[P] } 39 | 40 | interface SSCSSRule { 41 | cssText: string 42 | } 43 | 44 | interface ServerSideStyleSheet { 45 | cssRules: SSCSSRule[] 46 | insertRule(rule: string): any 47 | } 48 | 49 | interface Options { 50 | speedy?: boolean 51 | maxLength?: number 52 | } 53 | 54 | export default class CustomStyleSheet { 55 | private isSpeedy: boolean 56 | private sheet?: Writeable | ServerSideStyleSheet | null 57 | private tags: HTMLStyleElement[] = [] 58 | private maxLength: number 59 | private ctr: number = 0 60 | private injected: boolean = false 61 | 62 | constructor(options: Options = {}) { 63 | // The big drawback here is that the css won't be editable in devtools 64 | this.isSpeedy = options.speedy === undefined ? isProduction() : options.speedy 65 | 66 | this.maxLength = options.maxLength || 65000 67 | } 68 | 69 | getSheet() { 70 | return sheetForTag(last(this.tags)) 71 | } 72 | 73 | inject() { 74 | if (this.injected) { 75 | throw new Error('StyleSheet has already been injected.') 76 | } 77 | 78 | if (isBrowser) { 79 | this.tags[0] = makeStyleTag() 80 | } else { 81 | // Server side 'polyfill'. just enough behavior to be useful. 82 | this.sheet = { 83 | cssRules: [] as SSCSSRule[], 84 | insertRule: (rule: string) => { 85 | // Enough 'spec compliance' to be able to extract the rules later 86 | // in other words, just the cssText field 87 | ;(this.sheet!.cssRules as SSCSSRule[]).push({ cssText: rule }) 88 | } 89 | } 90 | } 91 | 92 | this.injected = true 93 | } 94 | 95 | speedy(bool: boolean) { 96 | if (this.ctr !== 0) { 97 | throw new Error( 98 | `StyleSheet cannot change speedy mode after inserting any rule to sheet. Either call speedy(${bool}) earlier in your app, or call flush() before speedy(${bool})` 99 | ) 100 | } 101 | 102 | this.isSpeedy = Boolean(bool) 103 | } 104 | 105 | _insert(sheet: CSSStyleSheet, rule: string) { 106 | // This weirdness for perf 107 | sheet.insertRule(rule, sheet.cssRules.length) 108 | } 109 | 110 | insert(rule: string) { 111 | if (isBrowser) { 112 | const sheet = this.getSheet() 113 | 114 | // This is the ultrafast version, works across browsers 115 | if (this.isSpeedy && sheet != null) { 116 | this._insert(sheet, rule) 117 | } else { 118 | last(this.tags).append(document.createTextNode(rule)) 119 | } 120 | } else if (this.sheet) { 121 | // Server side is pretty simple 122 | this.sheet.insertRule(rule, this.sheet.cssRules.length) 123 | } 124 | 125 | this.ctr += 1 126 | if (isBrowser && this.ctr % this.maxLength === 0) { 127 | this.tags.push(makeStyleTag()) 128 | } 129 | 130 | return this.ctr - 1 131 | } 132 | 133 | flush() { 134 | if (isBrowser) { 135 | this.tags.forEach(tag => tag.parentNode!.removeChild(tag)) 136 | this.tags = [] 137 | this.sheet = null 138 | this.ctr = 0 139 | } else if (this.sheet) { 140 | // Simpler on server 141 | this.sheet.cssRules = [] as SSCSSRule[] 142 | } 143 | 144 | this.injected = false 145 | } 146 | 147 | rules() { 148 | if (!isBrowser) { 149 | return (this.sheet ? this.sheet.cssRules : []) as CSSRule[] 150 | } 151 | 152 | const arr: CSSRule[] = [] 153 | this.tags.forEach(tag => { 154 | const sheet = sheetForTag(tag) 155 | if (sheet) { 156 | const rules = Array.from(sheet.cssRules) 157 | arr.splice(arr.length, 0, ...[...rules]) 158 | } 159 | }) 160 | 161 | return arr 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui-box", 3 | "version": "5.4.1", 4 | "description": "Blazing Fast React UI Primitive", 5 | "contributors": [ 6 | "Jeroen Ransijn (https://twitter.com/jeroen_ransijn)", 7 | "Roland Warmerdam (https://roland.codes)", 8 | "Meichen Zhou (https://github.com/jfmaggie)", 9 | "Netto Farah (https://twitter.com/nettofarah)", 10 | "Matt Shwery (https://github.com/mshwery)" 11 | ], 12 | "keywords": [ 13 | "react" 14 | ], 15 | "repository": "segmentio/ui-box", 16 | "license": "MIT", 17 | "main": "dist/src/index.js", 18 | "typings": "dist/src/index.d.ts", 19 | "files": [ 20 | "dist/src" 21 | ], 22 | "sideEffects": false, 23 | "engines": { 24 | "node": ">=12" 25 | }, 26 | "scripts": { 27 | "test": "xo && nyc ava", 28 | "prepublishOnly": "rm -rf dist && yarn run build", 29 | "dev": "start-storybook -p 9009", 30 | "build": "tsc", 31 | "build-storybook": "build-storybook -s .storybook/static -o .out", 32 | "release": "np", 33 | "benchmark": "react-benchmark tools/benchmarks/box.ts", 34 | "size": "size-limit", 35 | "coverage": "nyc report --reporter=html" 36 | }, 37 | "dependencies": { 38 | "@emotion/hash": "^0.7.1", 39 | "inline-style-prefixer": "^5.0.4", 40 | "prop-types": "^15.7.2" 41 | }, 42 | "peerDependencies": { 43 | "react": "^16.0.0 || ^17.0.0 || ^18.0.0" 44 | }, 45 | "devDependencies": { 46 | "@babel/core": "^7.4.4", 47 | "@size-limit/preset-big-lib": "^4.5.4", 48 | "@storybook/react": "^5.0.1", 49 | "@storybook/storybook-deployer": "^2.8.1", 50 | "@types/enzyme": "^3.9.1", 51 | "@types/inline-style-prefixer": "^5.0.0", 52 | "@types/prop-types": "^15.7.1", 53 | "@types/react": "^16.8.16", 54 | "@types/react-dom": "^16.8.4", 55 | "@types/react-test-renderer": "^16.8.1", 56 | "@types/sinon": "^7.0.11", 57 | "@types/storybook__react": "^4.0.1", 58 | "@typescript-eslint/eslint-plugin": "^1.7.0", 59 | "ava": "^1.3.1", 60 | "awesome-typescript-loader": "^5.2.1", 61 | "babel-loader": "^8.0.5", 62 | "csstype": "^2.6.4", 63 | "enzyme": "^3.9.0", 64 | "enzyme-adapter-react-16": "^1.10.0", 65 | "eslint": "^5.16.0", 66 | "eslint-config-xo-react": "^0.19.0", 67 | "eslint-config-xo-typescript": "^0.10.1", 68 | "eslint-plugin-react": "^7.12.4", 69 | "eslint-plugin-react-hooks": "^1.5.0", 70 | "husky": "^2.2.0", 71 | "lint-staged": "^8.1.5", 72 | "nyc": "^14.1.0", 73 | "react": "^18.2.0", 74 | "react-benchmark": "^3.0.0", 75 | "react-docgen-typescript-loader": "^3.1.0", 76 | "react-dom": "^18.2.0", 77 | "react-test-renderer": "^18.2.0", 78 | "sinon": "^7.2.7", 79 | "size-limit": "^4.5.4", 80 | "ts-node": "^8.1.0", 81 | "typescript": "4.9.4", 82 | "webpack": "^4.30.0", 83 | "xo": "^0.24.0" 84 | }, 85 | "prettier": { 86 | "semi": false, 87 | "singleQuote": true, 88 | "printWidth": 120, 89 | "tabWidth": 2, 90 | "useTabs": false 91 | }, 92 | "xo": { 93 | "parser": "@typescript-eslint/parser", 94 | "extends": [ 95 | "xo-react", 96 | "xo-typescript" 97 | ], 98 | "plugins": [ 99 | "@typescript-eslint" 100 | ], 101 | "extensions": [ 102 | "ts", 103 | "tsx" 104 | ], 105 | "envs": [ 106 | "node", 107 | "browser" 108 | ], 109 | "ignores": [ 110 | "**/*.ts", 111 | "**/*.tsx" 112 | ], 113 | "prettier": true, 114 | "space": true, 115 | "semicolon": false, 116 | "rules": { 117 | "@typescript-eslint/indent": [ 118 | "off" 119 | ], 120 | "@typescript-eslint/member-delimiter-style": [ 121 | "error", 122 | { 123 | "multiline": { 124 | "delimiter": "none", 125 | "requireLast": false 126 | }, 127 | "singleline": { 128 | "delimiter": "none", 129 | "requireLast": false 130 | } 131 | }, 132 | 2 133 | ], 134 | "react/jsx-sort-props": [ 135 | "error", 136 | { 137 | "callbacksLast": false, 138 | "shorthandFirst": false, 139 | "noSortAlphabetically": true, 140 | "reservedFirst": true 141 | } 142 | ] 143 | }, 144 | "settings": { 145 | "react": { 146 | "version": "16.5.0" 147 | } 148 | } 149 | }, 150 | "husky": { 151 | "hooks": { 152 | "pre-commit": "lint-staged" 153 | } 154 | }, 155 | "lint-staged": { 156 | "*.{js,ts,tsx}": [ 157 | "xo --fix", 158 | "git add" 159 | ] 160 | }, 161 | "size-limit": [ 162 | { 163 | "path": "dist/src/index.js", 164 | "limit": "65 KB", 165 | "running": false, 166 | "gzip": false 167 | } 168 | ], 169 | "ava": { 170 | "require": [ 171 | "ts-node/register/transpile-only", 172 | "./test/_setup" 173 | ], 174 | "compileEnhancements": false, 175 | "extensions": [ 176 | "ts", 177 | "tsx" 178 | ] 179 | }, 180 | "nyc": { 181 | "extension": [ 182 | ".ts", 183 | ".tsx" 184 | ], 185 | "exclude": [ 186 | "**/*.d.ts", 187 | "dist", 188 | "test", 189 | "tools" 190 | ] 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/enhancers/spacing.ts: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import getCss from '../get-css' 3 | import {spacesOutsideParentheses} from '../utils/regex' 4 | import { PropEnhancerValueType, PropValidators, PropEnhancers, PropTypesMapping, PropAliases } from '../types/enhancers' 5 | 6 | export const propTypes: PropTypesMapping = { 7 | margin: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 8 | marginBottom: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 9 | marginLeft: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 10 | marginRight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 11 | marginTop: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 12 | marginX: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 13 | marginY: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 14 | padding: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 15 | paddingBottom: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 16 | paddingLeft: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 17 | paddingRight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 18 | paddingTop: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 19 | paddingX: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 20 | paddingY: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) 21 | } 22 | 23 | export const propAliases: PropAliases = { 24 | margin: ['marginBottom', 'marginLeft', 'marginRight', 'marginTop'], 25 | marginX: ['marginLeft', 'marginRight'], 26 | marginY: ['marginBottom', 'marginTop'], 27 | padding: ['paddingBottom', 'paddingLeft', 'paddingRight', 'paddingTop'], 28 | paddingX: ['paddingLeft', 'paddingRight'], 29 | paddingY: ['paddingBottom', 'paddingTop'] 30 | } 31 | 32 | export const propValidators: PropValidators = {} 33 | 34 | if (process.env.NODE_ENV !== 'production') { 35 | propValidators.margin = value => { 36 | if (spacesOutsideParentheses.test(value)) { 37 | return `multiple values (“${value}”) aren՚t supported with “margin”. Use “marginX”, “marginY” “marginBottom”, “marginLeft”, “marginRight” and “marginTop” instead.` 38 | } 39 | return 40 | } 41 | 42 | propValidators.marginX = value => { 43 | if (spacesOutsideParentheses.test(value)) { 44 | return `multiple values (“${value}”) aren՚t supported with “marginX”. Use “marginLeft” and “marginRight” instead.` 45 | } 46 | return 47 | } 48 | 49 | propValidators.marginY = value => { 50 | if (spacesOutsideParentheses.test(value)) { 51 | return `multiple values (“${value}”) aren՚t supported with “marginY”. Use “marginBottom” and “marginTop” instead.` 52 | } 53 | return 54 | } 55 | 56 | propValidators.padding = value => { 57 | if (spacesOutsideParentheses.test(value)) { 58 | return `multiple values (“${value}”) aren՚t supported with “padding”. Use “paddingX”, “paddingY” “paddingBottom”, “paddingLeft”, “paddingRight” and “paddingTop” instead.` 59 | } 60 | return 61 | } 62 | 63 | propValidators.paddingX = value => { 64 | if (spacesOutsideParentheses.test(value)) { 65 | return `multiple values (“${value}”) aren՚t supported with “paddingX”. Use “paddingLeft” and “paddingRight” instead.` 66 | } 67 | return 68 | } 69 | 70 | propValidators.paddingY = value => { 71 | if (spacesOutsideParentheses.test(value)) { 72 | return `multiple values (“${value}”) aren՚t supported with “paddingY”. Use “paddingBottom” and “paddingTop” instead.` 73 | } 74 | return 75 | } 76 | } 77 | 78 | const marginTop = { 79 | className: 'mt', 80 | cssName: 'margin-top', 81 | jsName: 'marginTop' 82 | } 83 | const marginRight = { 84 | className: 'mr', 85 | cssName: 'margin-right', 86 | jsName: 'marginRight' 87 | } 88 | const marginBottom = { 89 | className: 'mb', 90 | cssName: 'margin-bottom', 91 | jsName: 'marginBottom' 92 | } 93 | const marginLeft = { 94 | className: 'ml', 95 | cssName: 'margin-left', 96 | jsName: 'marginLeft' 97 | } 98 | const paddingTop = { 99 | className: 'pt', 100 | cssName: 'padding-top', 101 | jsName: 'paddingTop' 102 | } 103 | const paddingRight = { 104 | className: 'pr', 105 | cssName: 'padding-right', 106 | jsName: 'paddingRight' 107 | } 108 | const paddingBottom = { 109 | className: 'pb', 110 | cssName: 'padding-bottom', 111 | jsName: 'paddingBottom' 112 | } 113 | const paddingLeft = { 114 | className: 'pl', 115 | cssName: 'padding-left', 116 | jsName: 'paddingLeft' 117 | } 118 | 119 | export const propEnhancers: PropEnhancers = { 120 | marginBottom: (value: PropEnhancerValueType, selector: string) => getCss(marginBottom, value, selector), 121 | marginLeft: (value: PropEnhancerValueType, selector: string) => getCss(marginLeft, value, selector), 122 | marginRight: (value: PropEnhancerValueType, selector: string) => getCss(marginRight, value, selector), 123 | marginTop: (value: PropEnhancerValueType, selector: string) => getCss(marginTop, value, selector), 124 | paddingBottom: (value: PropEnhancerValueType, selector: string) => getCss(paddingBottom, value, selector), 125 | paddingLeft: (value: PropEnhancerValueType, selector: string) => getCss(paddingLeft, value, selector), 126 | paddingRight: (value: PropEnhancerValueType, selector: string) => getCss(paddingRight, value, selector), 127 | paddingTop: (value: PropEnhancerValueType, selector: string) => getCss(paddingTop, value, selector) 128 | } 129 | -------------------------------------------------------------------------------- /src/enhancers/flex.ts: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import getCss from '../get-css' 3 | import { PropEnhancerValueType, PropValidators, PropEnhancers, PropTypesMapping, PropAliases } from '../types/enhancers' 4 | 5 | export const propTypes: PropTypesMapping = { 6 | alignContent: PropTypes.string, 7 | alignItems: PropTypes.string, 8 | alignSelf: PropTypes.string, 9 | flex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 10 | flexBasis: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 11 | flexDirection: PropTypes.string, 12 | flexFlow: PropTypes.string, 13 | flexGrow: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 14 | flexShrink: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 15 | flexWrap: PropTypes.string, 16 | justifyContent: PropTypes.string, 17 | justifyItems: PropTypes.string, 18 | justifySelf: PropTypes.string, 19 | order: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 20 | placeContent: PropTypes.string, 21 | placeItems: PropTypes.string, 22 | placeSelf: PropTypes.string 23 | } 24 | 25 | export const propAliases: PropAliases = {} 26 | export const propValidators: PropValidators = {} 27 | 28 | const flex = { 29 | className: 'flx', 30 | cssName: 'flex', 31 | jsName: 'flex', 32 | isPrefixed: true, 33 | defaultUnit: '' 34 | } 35 | const alignItems = { 36 | className: 'algn-itms', 37 | cssName: 'align-items', 38 | jsName: 'alignItems', 39 | isPrefixed: true 40 | } 41 | const alignSelf = { 42 | className: 'algn-slf', 43 | cssName: 'align-self', 44 | jsName: 'alignSelf', 45 | isPrefixed: true 46 | } 47 | const alignContent = { 48 | className: 'algn-cnt', 49 | cssName: 'align-content', 50 | jsName: 'alignContent', 51 | isPrefixed: true 52 | } 53 | const justifyContent = { 54 | className: 'just-cnt', 55 | cssName: 'justify-content', 56 | jsName: 'justifyContent', 57 | isPrefixed: true 58 | } 59 | const justifyItems = { 60 | className: 'just-items', 61 | cssName: 'justify-items', 62 | jsName: 'justifyItems', 63 | isPrefixed: true 64 | } 65 | const justifySelf = { 66 | className: 'just-self', 67 | cssName: 'justify-self', 68 | jsName: 'justifySelf', 69 | isPrefixed: true 70 | } 71 | const flexDirection = { 72 | className: 'flx-drct', 73 | cssName: 'flex-direction', 74 | jsName: 'flexDirection', 75 | isPrefixed: true, 76 | safeValue: true 77 | } 78 | const flexWrap = { 79 | className: 'flx-wrap', 80 | cssName: 'flex-wrap', 81 | jsName: 'flexWrap', 82 | isPrefixed: true, 83 | safeValue: true 84 | } 85 | const flexGrow = { 86 | className: 'flx-grow', 87 | cssName: 'flex-grow', 88 | jsName: 'flexGrow', 89 | isPrefixed: true, 90 | defaultUnit: '' 91 | } 92 | const flexShrink = { 93 | className: 'flx-srnk', 94 | cssName: 'flex-shrink', 95 | jsName: 'flexShrink', 96 | isPrefixed: true, 97 | defaultUnit: '' 98 | } 99 | const flexBasis = { 100 | className: 'flx-basis', 101 | cssName: 'flex-basis', 102 | jsName: 'flexBasis', 103 | isPrefixed: true 104 | } 105 | const order = { 106 | className: 'order', 107 | cssName: 'order', 108 | jsName: 'order', 109 | isPrefixed: true, 110 | defaultUnit: '', 111 | safeValue: true 112 | } 113 | const flexFlow = { 114 | className: 'flx-flow', 115 | cssName: 'flex-flow', 116 | jsName: 'flexFlow', 117 | isPrefixed: true, 118 | defaultUnit: '' 119 | } 120 | const placeContent = { 121 | className: 'plc-cnt', 122 | cssName: 'place-content', 123 | jsName: 'placeContent' 124 | } 125 | const placeItems = { 126 | className: 'plc-items', 127 | cssName: 'place-items', 128 | jsName: 'placeItems' 129 | } 130 | const placeSelf = { 131 | className: 'plc-self', 132 | cssName: 'place-self', 133 | jsName: 'placeSelf' 134 | } 135 | 136 | export const propEnhancers: PropEnhancers = { 137 | alignContent: (value: PropEnhancerValueType, selector: string) => getCss(alignContent, value, selector), 138 | alignItems: (value: PropEnhancerValueType, selector: string) => getCss(alignItems, value, selector), 139 | alignSelf: (value: PropEnhancerValueType, selector: string) => getCss(alignSelf, value, selector), 140 | flex: (value: PropEnhancerValueType, selector: string) => getCss(flex, value, selector), 141 | flexBasis: (value: PropEnhancerValueType, selector: string) => getCss(flexBasis, value, selector), 142 | flexDirection: (value: PropEnhancerValueType, selector: string) => getCss(flexDirection, value, selector), 143 | flexFlow: (value: PropEnhancerValueType, selector: string) => getCss(flexFlow, value, selector), 144 | flexGrow: (value: PropEnhancerValueType, selector: string) => getCss(flexGrow, value, selector), 145 | flexShrink: (value: PropEnhancerValueType, selector: string) => getCss(flexShrink, value, selector), 146 | flexWrap: (value: PropEnhancerValueType, selector: string) => getCss(flexWrap, value, selector), 147 | justifyContent: (value: PropEnhancerValueType, selector: string) => getCss(justifyContent, value, selector), 148 | justifyItems: (value: PropEnhancerValueType, selector: string) => getCss(justifyItems, value, selector), 149 | justifySelf: (value: PropEnhancerValueType, selector: string) => getCss(justifySelf, value, selector), 150 | order: (value: PropEnhancerValueType, selector: string) => getCss(order, value, selector), 151 | placeContent: (value: PropEnhancerValueType, selector: string) => getCss(placeContent, value, selector), 152 | placeItems: (value: PropEnhancerValueType, selector: string) => getCss(placeItems, value, selector), 153 | placeSelf: (value: PropEnhancerValueType, selector: string) => getCss(placeSelf, value, selector) 154 | } 155 | -------------------------------------------------------------------------------- /test/enhance-props.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import * as cache from '../src/cache' 3 | import * as styles from '../src/styles' 4 | import enhanceProps from '../src/enhance-props' 5 | 6 | test.afterEach.always(() => { 7 | cache.clear() 8 | styles.clear() 9 | }) 10 | 11 | test.serial('enhances a prop', t => { 12 | const { className, enhancedProps } = enhanceProps({ width: 10 }) 13 | t.is(className, 'ub-w_10px') 14 | t.deepEqual(enhancedProps, {}) 15 | }) 16 | 17 | test.serial('expands aliases', t => { 18 | const { className, enhancedProps } = enhanceProps({ margin: 11 }) 19 | t.is(className, 'ub-mb_11px ub-ml_11px ub-mr_11px ub-mt_11px') 20 | t.deepEqual(enhancedProps, {}) 21 | }) 22 | 23 | test.serial('injects styles', t => { 24 | enhanceProps({ width: 12 }) 25 | t.is( 26 | styles.getAll(), 27 | ` 28 | .ub-w_12px { 29 | width: 12px; 30 | }` 31 | ) 32 | }) 33 | 34 | test.serial('uses the cache', t => { 35 | enhanceProps({ width: 13 }) 36 | enhanceProps({ width: 13 }) 37 | t.is( 38 | styles.getAll(), 39 | ` 40 | .ub-w_13px { 41 | width: 13px; 42 | }` 43 | ) 44 | t.is(cache.get('width', 13), 'ub-w_13px') 45 | }) 46 | 47 | test.serial('strips falsey enhancer props', t => { 48 | const { className, enhancedProps } = enhanceProps({ width: false }) 49 | t.is(className, '') 50 | t.deepEqual(enhancedProps, {}) 51 | }) 52 | 53 | test.serial('does not strip enhancer props with 0 values', t => { 54 | const { className, enhancedProps } = enhanceProps({ width: 0 }) 55 | t.is(className, 'ub-w_0px') 56 | t.deepEqual(enhancedProps, {}) 57 | }) 58 | 59 | test.serial('passes through non-enhancer props', t => { 60 | const expected = { disabled: true, foo: 'bar', baz: 123, fizz: { buzz: true } } 61 | 62 | const { className, enhancedProps } = enhanceProps(expected) 63 | 64 | t.is(className, '') 65 | t.deepEqual(enhancedProps, expected) 66 | }) 67 | 68 | test.serial('passes through falsey non-enhancer props', t => { 69 | const { className, enhancedProps } = enhanceProps({ disabled: false }) 70 | t.is(className, '') 71 | t.deepEqual(enhancedProps, { disabled: false }) 72 | }) 73 | 74 | test.serial('handles invalid values', t => { 75 | // @ts-ignore 76 | const { className, enhancedProps } = enhanceProps({ minWidth: true }) 77 | t.is(className, '') 78 | t.deepEqual(enhancedProps, {}) 79 | }) 80 | 81 | test.serial('preserves style prop', t => { 82 | const expected = { style: { backgroundColor: 'red' } } 83 | 84 | const { enhancedProps } = enhanceProps(expected) 85 | 86 | t.deepEqual(enhancedProps, expected) 87 | }) 88 | 89 | test.serial('converts styles in selectors to class name', t => { 90 | const { className, enhancedProps } = enhanceProps({ 91 | selectors: { 92 | '&:hover': { 93 | backgroundColor: 'blue' 94 | } 95 | } 96 | }) 97 | 98 | t.deepEqual(className, 'ub-bg-clr_blue_1k2el8q') 99 | t.deepEqual(enhancedProps, {}) 100 | }) 101 | 102 | test.serial('injects selector styles', t => { 103 | enhanceProps({ 104 | selectors: { 105 | '&:hover': { 106 | backgroundColor: 'blue' 107 | } 108 | } 109 | }) 110 | 111 | t.deepEqual( 112 | styles.getAll(), 113 | ` 114 | .ub-bg-clr_blue_1k2el8q:hover { 115 | background-color: blue; 116 | }` 117 | ) 118 | }) 119 | 120 | test.serial('converts styles in nested selectors to class name', t => { 121 | const { className, enhancedProps } = enhanceProps({ 122 | selectors: { 123 | '&[data-active]': { 124 | selectors: { 125 | '&:hover': { 126 | backgroundColor: 'blue' 127 | } 128 | } 129 | } 130 | } 131 | }) 132 | 133 | t.deepEqual(className, 'ub-bg-clr_blue_187q98e') 134 | t.deepEqual(enhancedProps, {}) 135 | }) 136 | 137 | test.serial("selectors can be nested without 'selectors' key", t => { 138 | const { className, enhancedProps } = enhanceProps({ 139 | selectors: { 140 | '&[data-active]': { 141 | '&:hover': { 142 | backgroundColor: 'blue' 143 | } 144 | } 145 | } 146 | }) 147 | 148 | t.deepEqual(className, 'ub-bg-clr_blue_187q98e') 149 | t.deepEqual(enhancedProps, {}) 150 | }) 151 | 152 | test.serial('injects nested selector styles', t => { 153 | enhanceProps({ 154 | selectors: { 155 | '&[data-active]': { 156 | selectors: { 157 | '&:hover': { 158 | backgroundColor: 'blue' 159 | } 160 | } 161 | } 162 | } 163 | }) 164 | 165 | t.deepEqual( 166 | styles.getAll(), 167 | ` 168 | .ub-bg-clr_blue_187q98e[data-active]:hover { 169 | background-color: blue; 170 | }` 171 | ) 172 | }) 173 | 174 | test.serial('comma-separated selectors with nested selectors are expanded with nested selector appended to each', t => { 175 | enhanceProps({ 176 | selectors: { 177 | '&[aria-current="page"],&[aria-selected="true"]': { 178 | color: '#3366FF', 179 | 180 | '&:before': { 181 | transform: 'scaleY(1)' 182 | }, 183 | 184 | '&:focus': { 185 | color: '#2952CC' 186 | } 187 | } 188 | } 189 | }) 190 | 191 | t.deepEqual( 192 | styles.getAll(), 193 | ` 194 | .ub-color_3366FF_75lazh[aria-current="page"], .ub-color_3366FF_75lazh[aria-selected="true"] { 195 | color: #3366FF; 196 | } 197 | .ub-tfrm_qu4iyp_fiauus[aria-current="page"]:before, .ub-tfrm_qu4iyp_fiauus[aria-selected="true"]:before { 198 | transform: scaleY(1); 199 | } 200 | .ub-color_2952CC_drif9m[aria-current="page"]:focus, .ub-color_2952CC_drif9m[aria-selected="true"]:focus { 201 | color: #2952CC; 202 | }` 203 | ) 204 | }) 205 | -------------------------------------------------------------------------------- /src/enhancers/text.ts: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import getCss from '../get-css' 3 | import { PropEnhancerValueType, PropValidators, PropEnhancers, PropTypesMapping, PropAliases } from '../types/enhancers' 4 | 5 | export const propTypes: PropTypesMapping = { 6 | color: PropTypes.string, 7 | font: PropTypes.string, 8 | fontFamily: PropTypes.string, 9 | fontSize: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 10 | fontStyle: PropTypes.string, 11 | fontVariant: PropTypes.string, 12 | fontWeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 13 | letterSpacing: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 14 | lineHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 15 | textAlign: PropTypes.string, 16 | textDecoration: PropTypes.string, 17 | textOverflow: PropTypes.string, 18 | textShadow: PropTypes.string, 19 | textTransform: PropTypes.string, 20 | verticalAlign: PropTypes.string, 21 | whiteSpace: PropTypes.string, 22 | wordBreak: PropTypes.string, 23 | wordWrap: PropTypes.string 24 | } 25 | 26 | export const propAliases: PropAliases = {} 27 | export const propValidators: PropValidators = {} 28 | 29 | const textAlign = { 30 | className: 'txt-algn', 31 | safeValue: true, 32 | cssName: 'text-align', 33 | jsName: 'textAlign' 34 | } 35 | const textDecoration = { 36 | className: 'txt-deco', 37 | cssName: 'text-decoration', 38 | jsName: 'textDecoration' 39 | } 40 | const textTransform = { 41 | className: 'txt-trns', 42 | cssName: 'text-transform', 43 | jsName: 'textTransform', 44 | safeValue: true 45 | } 46 | const textShadow = { 47 | className: 'txt-shdw', 48 | cssName: 'text-shadow', 49 | jsName: 'textShadow', 50 | complexValue: true 51 | } 52 | const textOverflow = { 53 | className: 'txt-ovrf', 54 | cssName: 'text-overflow', 55 | jsName: 'textOverflow', 56 | safeValue: true 57 | } 58 | const color = { 59 | className: 'color', 60 | cssName: 'color', 61 | jsName: 'color' 62 | } 63 | const font = { 64 | className: 'fnt', 65 | cssName: 'font', 66 | jsName: 'font', 67 | complexValue: true 68 | } 69 | const fontFamily = { 70 | className: 'fnt-fam', 71 | cssName: 'font-family', 72 | jsName: 'fontFamily', 73 | complexValue: true 74 | } 75 | const fontSize = { 76 | className: 'fnt-sze', 77 | cssName: 'font-size', 78 | jsName: 'fontSize' 79 | } 80 | const fontStyle = { 81 | className: 'fnt-stl', 82 | cssName: 'font-style', 83 | jsName: 'fontStyle', 84 | safeValue: true 85 | } 86 | const fontVariant = { 87 | className: 'f-vari', 88 | cssName: 'font-variant', 89 | jsName: 'fontVariant' 90 | } 91 | const fontWeight = { 92 | className: 'f-wght', 93 | cssName: 'font-weight', 94 | jsName: 'fontWeight', 95 | safeValue: true, 96 | defaultUnit: '' 97 | } 98 | const lineHeight = { 99 | className: 'ln-ht', 100 | cssName: 'line-height', 101 | jsName: 'lineHeight', 102 | defaultUnit: '' 103 | } 104 | const verticalAlign = { 105 | className: 'ver-algn', 106 | cssName: 'vertical-align', 107 | jsName: 'verticalAlign', 108 | safeValue: true 109 | } 110 | const wordBreak = { 111 | className: 'wrd-brk', 112 | cssName: 'word-break', 113 | jsName: 'wordBreak', 114 | safeValue: true 115 | } 116 | const wordWrap = { 117 | className: 'wrd-wrp', 118 | cssName: 'word-wrap', 119 | jsName: 'wordWrap', 120 | safeValue: true 121 | } 122 | const whiteSpace = { 123 | className: 'wht-spc', 124 | cssName: 'white-space', 125 | jsName: 'whiteSpace', 126 | safeValue: true 127 | } 128 | const letterSpacing = { 129 | className: 'ltr-spc', 130 | cssName: 'letter-spacing', 131 | jsName: 'letterSpacing' 132 | } 133 | 134 | export const propEnhancers: PropEnhancers = { 135 | color: (value: PropEnhancerValueType, selector: string) => getCss(color, value, selector), 136 | font: (value: PropEnhancerValueType, selector: string) => getCss(font, value, selector), 137 | fontFamily: (value: PropEnhancerValueType, selector: string) => getCss(fontFamily, value, selector), 138 | fontSize: (value: PropEnhancerValueType, selector: string) => getCss(fontSize, value, selector), 139 | fontStyle: (value: PropEnhancerValueType, selector: string) => getCss(fontStyle, value, selector), 140 | fontVariant: (value: PropEnhancerValueType, selector: string) => getCss(fontVariant, value, selector), 141 | fontWeight: (value: PropEnhancerValueType, selector: string) => getCss(fontWeight, value, selector), 142 | letterSpacing: (value: PropEnhancerValueType, selector: string) => getCss(letterSpacing, value, selector), 143 | lineHeight: (value: PropEnhancerValueType, selector: string) => getCss(lineHeight, value, selector), 144 | textAlign: (value: PropEnhancerValueType, selector: string) => getCss(textAlign, value, selector), 145 | textDecoration: (value: PropEnhancerValueType, selector: string) => getCss(textDecoration, value, selector), 146 | textOverflow: (value: PropEnhancerValueType, selector: string) => getCss(textOverflow, value, selector), 147 | textShadow: (value: PropEnhancerValueType, selector: string) => getCss(textShadow, value, selector), 148 | textTransform: (value: PropEnhancerValueType, selector: string) => getCss(textTransform, value, selector), 149 | verticalAlign: (value: PropEnhancerValueType, selector: string) => getCss(verticalAlign, value, selector), 150 | whiteSpace: (value: PropEnhancerValueType, selector: string) => getCss(whiteSpace, value, selector), 151 | wordBreak: (value: PropEnhancerValueType, selector: string) => getCss(wordBreak, value, selector), 152 | wordWrap: (value: PropEnhancerValueType, selector: string) => getCss(wordWrap, value, selector) 153 | } 154 | -------------------------------------------------------------------------------- /tools/all-properties-component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Box, { keyframes } from '../src' 3 | 4 | const openAnimation = keyframes('openAnimation', { 5 | from: { 6 | opacity: 0, 7 | transform: 'translateY(-120%)' 8 | }, 9 | to: { 10 | transform: 'translateY(0)' 11 | } 12 | }) 13 | 14 | // Built as a regular function instead of a component to reduce impact on the benchmark 15 | export default () => { 16 | return ( 17 | 183 | ) 184 | } 185 | -------------------------------------------------------------------------------- /src/types/enhancers.ts: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import * as CSS from 'csstype' 3 | import { Rule } from '../prefixer' 4 | 5 | export type CssProps = Pick< 6 | CSS.StandardProperties, 7 | | 'alignContent' 8 | | 'alignItems' 9 | | 'alignSelf' 10 | | 'animation' 11 | | 'animationDelay' 12 | | 'animationDirection' 13 | | 'animationDuration' 14 | | 'animationFillMode' 15 | | 'animationIterationCount' 16 | | 'animationName' 17 | | 'animationPlayState' 18 | | 'animationTimingFunction' 19 | | 'background' 20 | | 'backgroundBlendMode' 21 | | 'backgroundClip' 22 | | 'backgroundColor' 23 | | 'backgroundImage' 24 | | 'backgroundOrigin' 25 | | 'backgroundPosition' 26 | | 'backgroundRepeat' 27 | | 'backgroundSize' 28 | | 'border' 29 | | 'borderBottom' 30 | | 'borderBottomColor' 31 | | 'borderBottomLeftRadius' 32 | | 'borderBottomRightRadius' 33 | | 'borderBottomStyle' 34 | | 'borderBottomWidth' 35 | | 'borderColor' 36 | | 'borderLeft' 37 | | 'borderLeftColor' 38 | | 'borderLeftStyle' 39 | | 'borderLeftWidth' 40 | | 'borderRadius' 41 | | 'borderRight' 42 | | 'borderRightColor' 43 | | 'borderRightStyle' 44 | | 'borderRightWidth' 45 | | 'borderStyle' 46 | | 'borderTop' 47 | | 'borderTopColor' 48 | | 'borderTopLeftRadius' 49 | | 'borderTopRightRadius' 50 | | 'borderTopStyle' 51 | | 'borderTopWidth' 52 | | 'borderWidth' 53 | | 'bottom' 54 | | 'boxShadow' 55 | | 'boxSizing' 56 | | 'clear' 57 | | 'color' 58 | | 'columnGap' 59 | | 'content' 60 | | 'cursor' 61 | | 'display' 62 | | 'flex' 63 | | 'flexBasis' 64 | | 'flexDirection' 65 | | 'flexFlow' 66 | | 'flexGrow' 67 | | 'flexShrink' 68 | | 'flexWrap' 69 | | 'float' 70 | | 'font' 71 | | 'fontFamily' 72 | | 'fontSize' 73 | | 'fontStyle' 74 | | 'fontVariant' 75 | | 'fontWeight' 76 | | 'gap' 77 | | 'grid' 78 | | 'gridArea' 79 | | 'gridAutoColumns' 80 | | 'gridAutoFlow' 81 | | 'gridAutoRows' 82 | | 'gridColumn' 83 | | 'gridColumnEnd' 84 | | 'gridColumnStart' 85 | | 'gridRow' 86 | | 'gridRowEnd' 87 | | 'gridRowStart' 88 | | 'gridTemplate' 89 | | 'gridTemplateAreas' 90 | | 'gridTemplateColumns' 91 | | 'gridTemplateRows' 92 | | 'height' 93 | | 'justifyContent' 94 | | 'justifyItems' 95 | | 'justifySelf' 96 | | 'left' 97 | | 'letterSpacing' 98 | | 'lineHeight' 99 | | 'listStyle' 100 | | 'listStyleImage' 101 | | 'listStylePosition' 102 | | 'listStyleType' 103 | | 'margin' 104 | | 'marginBottom' 105 | | 'marginLeft' 106 | | 'marginRight' 107 | | 'marginTop' 108 | | 'maxHeight' 109 | | 'maxWidth' 110 | | 'minHeight' 111 | | 'minWidth' 112 | | 'opacity' 113 | | 'order' 114 | | 'outline' 115 | | 'overflow' 116 | | 'overflowX' 117 | | 'overflowY' 118 | | 'padding' 119 | | 'paddingBottom' 120 | | 'paddingLeft' 121 | | 'paddingRight' 122 | | 'paddingTop' 123 | | 'placeContent' 124 | | 'placeItems' 125 | | 'placeSelf' 126 | | 'pointerEvents' 127 | | 'position' 128 | | 'resize' 129 | | 'right' 130 | | 'rowGap' 131 | | 'textAlign' 132 | | 'textDecoration' 133 | | 'textOverflow' 134 | | 'textShadow' 135 | | 'textTransform' 136 | | 'top' 137 | | 'transform' 138 | | 'transformOrigin' 139 | | 'transition' 140 | | 'transitionDelay' 141 | | 'transitionDuration' 142 | | 'transitionProperty' 143 | | 'transitionTimingFunction' 144 | | 'userSelect' 145 | | 'verticalAlign' 146 | | 'visibility' 147 | | 'whiteSpace' 148 | | 'width' 149 | | 'wordBreak' 150 | | 'wordWrap' 151 | | 'zIndex' 152 | > & 153 | Pick & 154 | Pick< 155 | CSS.SvgProperties, 156 | 'fill' | 'stroke' | 'strokeDasharray' | 'strokeDashoffset' | 'strokeLinecap' | 'strokeMiterlimit' | 'strokeWidth' 157 | > 158 | 159 | export type BoxCssProps = { 160 | // Enhance the CSS props with the ui-box supported values. 161 | // `string` isn't added because it'll ruin props with string literal types (e.g textAlign) 162 | [P in keyof CP]: CP[P] | number | false | null | undefined 163 | } 164 | 165 | export type BoxPropValue = string | number | false | null | undefined 166 | 167 | export type EnhancerProps = BoxCssProps & { 168 | /** 169 | * Sets `marginLeft` and `marginRight` to the same value 170 | */ 171 | marginX?: BoxPropValue 172 | 173 | /** 174 | * Sets `marginTop` and `marginBottom` to the same value 175 | */ 176 | marginY?: BoxPropValue 177 | 178 | /** 179 | * Sets `paddingLeft` and `paddingRight` to the same value 180 | */ 181 | paddingX?: BoxPropValue 182 | 183 | /** 184 | * Sets `paddingTop` and `paddingBottom` to the same value 185 | */ 186 | paddingY?: BoxPropValue 187 | 188 | /** 189 | * Utility property for easily adding clearfix styles to the element. 190 | */ 191 | clearfix?: boolean 192 | 193 | /** 194 | * Map of selectors and styles to apply when the selector conditions are met 195 | * @example 196 | * 197 | * Hello world 198 | * 199 | */ 200 | selectors?: SelectorMap 201 | } 202 | 203 | export type SelectorMap = { 204 | [selector: string]: BoxCssProps | SelectorMap 205 | } 206 | 207 | export type PropEnhancerValueType = string | number 208 | 209 | export interface PropTypesMapping { 210 | [key: string]: PropTypes.Validator 211 | } 212 | 213 | export interface PropAliases { 214 | [key: string]: string[] 215 | } 216 | 217 | export interface PropEnhancers { 218 | [key: string]: (value: PropEnhancerValueType, selector: string) => EnhancedProp | null 219 | } 220 | 221 | export interface PropValidators { 222 | [key: string]: (value: any) => string | undefined 223 | } 224 | 225 | export interface EnhancedProp { 226 | /** 227 | * Generated class name representing the styles 228 | */ 229 | className: string 230 | 231 | /** 232 | * Collection of css property/value objects 233 | */ 234 | rules: Rule[] 235 | 236 | /** 237 | * Full style string in the format of `.className[:selector] { property: value; }` 238 | */ 239 | styles: string 240 | } 241 | -------------------------------------------------------------------------------- /src/enhancers/borders.ts: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import getCss from '../get-css' 3 | import {spacesOutsideParentheses} from '../utils/regex' 4 | import { PropEnhancerValueType, PropValidators, PropEnhancers, PropTypesMapping } from '../types/enhancers' 5 | 6 | export const propTypes: PropTypesMapping = { 7 | border: PropTypes.string, 8 | borderBottom: PropTypes.string, 9 | borderBottomColor: PropTypes.string, 10 | borderBottomStyle: PropTypes.string, 11 | borderBottomWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 12 | borderColor: PropTypes.string, 13 | borderLeft: PropTypes.string, 14 | borderLeftColor: PropTypes.string, 15 | borderLeftStyle: PropTypes.string, 16 | borderLeftWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 17 | borderRight: PropTypes.string, 18 | borderRightColor: PropTypes.string, 19 | borderRightStyle: PropTypes.string, 20 | borderRightWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 21 | borderStyle: PropTypes.string, 22 | borderTop: PropTypes.string, 23 | borderTopColor: PropTypes.string, 24 | borderTopStyle: PropTypes.string, 25 | borderTopWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 26 | borderWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) 27 | } 28 | 29 | export const propAliases = { 30 | border: ['borderBottom', 'borderLeft', 'borderRight', 'borderTop'], 31 | borderColor: [ 32 | 'borderBottomColor', 33 | 'borderLeftColor', 34 | 'borderRightColor', 35 | 'borderTopColor' 36 | ], 37 | borderStyle: [ 38 | 'borderBottomStyle', 39 | 'borderLeftStyle', 40 | 'borderRightStyle', 41 | 'borderTopStyle' 42 | ], 43 | borderWidth: [ 44 | 'borderBottomWidth', 45 | 'borderLeftWidth', 46 | 'borderRightWidth', 47 | 'borderTopWidth' 48 | ] 49 | } 50 | 51 | export const propValidators: PropValidators = {} 52 | 53 | if (process.env.NODE_ENV !== 'production') { 54 | propValidators.borderColor = value => { 55 | if (spacesOutsideParentheses.test(value)) { 56 | return `multiple values (“${value}”) aren՚t supported with “borderColor”. Use “borderBottomColor”, “borderLeftColor” “borderRightColor” and “borderTopColor” instead.` 57 | } 58 | 59 | return 60 | } 61 | propValidators.borderStyle = value => { 62 | if (spacesOutsideParentheses.test(value)) { 63 | return `multiple values (“${value}”) aren՚t supported with “borderStyle”. Use “borderBottomStyle”, “borderLeftStyle” “borderRightStyle” and “borderTopStyle” instead.` 64 | } 65 | 66 | return 67 | } 68 | propValidators.borderWidth = value => { 69 | if (spacesOutsideParentheses.test(value)) { 70 | return `multiple values (“${value}”) aren՚t supported with “borderWidth”. Use “borderBottomWidth”, “borderLeftWidth” “borderRightWidth” and “borderTopWidth” instead.` 71 | } 72 | 73 | return 74 | } 75 | } 76 | 77 | const borderLeft = { 78 | className: 'b-lft', 79 | cssName: 'border-left', 80 | jsName: 'borderLeft' 81 | } 82 | const borderLeftColor = { 83 | className: 'b-lft-clr', 84 | cssName: 'border-left-color', 85 | jsName: 'borderLeftColor' 86 | } 87 | const borderLeftStyle = { 88 | className: 'b-lft-stl', 89 | cssName: 'border-left-style', 90 | jsName: 'borderLeftStyle', 91 | safeValue: true 92 | } 93 | const borderLeftWidth = { 94 | className: 'b-lft-wdt', 95 | cssName: 'border-left-width', 96 | jsName: 'borderLeftWidth' 97 | } 98 | const borderRight = { 99 | className: 'b-rgt', 100 | cssName: 'border-right', 101 | jsName: 'borderRight' 102 | } 103 | const borderRightColor = { 104 | className: 'b-rgt-clr', 105 | cssName: 'border-right-color', 106 | jsName: 'borderRightColor' 107 | } 108 | const borderRightStyle = { 109 | className: 'b-rgt-stl', 110 | cssName: 'border-right-style', 111 | jsName: 'borderRightStyle', 112 | safeValue: true 113 | } 114 | const borderRightWidth = { 115 | className: 'b-rgt-wdt', 116 | cssName: 'border-right-width', 117 | jsName: 'borderRightWidth' 118 | } 119 | const borderTop = { 120 | className: 'b-top', 121 | cssName: 'border-top', 122 | jsName: 'borderTop' 123 | } 124 | const borderTopColor = { 125 | className: 'b-top-clr', 126 | cssName: 'border-top-color', 127 | jsName: 'borderTopColor' 128 | } 129 | const borderTopStyle = { 130 | className: 'b-top-stl', 131 | cssName: 'border-top-style', 132 | jsName: 'borderTopStyle', 133 | safeValue: true 134 | } 135 | const borderTopWidth = { 136 | className: 'b-top-wdt', 137 | cssName: 'border-top-width', 138 | jsName: 'borderTopWidth' 139 | } 140 | const borderBottom = { 141 | className: 'b-btm', 142 | cssName: 'border-bottom', 143 | jsName: 'borderBottom' 144 | } 145 | const borderBottomColor = { 146 | className: 'b-btm-clr', 147 | cssName: 'border-bottom-color', 148 | jsName: 'borderBottomColor' 149 | } 150 | const borderBottomStyle = { 151 | className: 'b-btm-stl', 152 | cssName: 'border-bottom-style', 153 | jsName: 'borderBottomStyle', 154 | safeValue: true 155 | } 156 | const borderBottomWidth = { 157 | className: 'b-btm-wdt', 158 | cssName: 'border-bottom-width', 159 | jsName: 'borderBottomWidth' 160 | } 161 | 162 | export const propEnhancers: PropEnhancers = { 163 | borderBottom: (value: PropEnhancerValueType, selector: string) => getCss(borderBottom, value, selector), 164 | borderBottomColor: (value: PropEnhancerValueType, selector: string) => getCss(borderBottomColor, value, selector), 165 | borderBottomStyle: (value: PropEnhancerValueType, selector: string) => getCss(borderBottomStyle, value, selector), 166 | borderBottomWidth: (value: PropEnhancerValueType, selector: string) => getCss(borderBottomWidth, value, selector), 167 | borderLeft: (value: PropEnhancerValueType, selector: string) => getCss(borderLeft, value, selector), 168 | borderLeftColor: (value: PropEnhancerValueType, selector: string) => getCss(borderLeftColor, value, selector), 169 | borderLeftStyle: (value: PropEnhancerValueType, selector: string) => getCss(borderLeftStyle, value, selector), 170 | borderLeftWidth: (value: PropEnhancerValueType, selector: string) => getCss(borderLeftWidth, value, selector), 171 | borderRight: (value: PropEnhancerValueType, selector: string) => getCss(borderRight, value, selector), 172 | borderRightColor: (value: PropEnhancerValueType, selector: string) => getCss(borderRightColor, value, selector), 173 | borderRightStyle: (value: PropEnhancerValueType, selector: string) => getCss(borderRightStyle, value, selector), 174 | borderRightWidth: (value: PropEnhancerValueType, selector: string) => getCss(borderRightWidth, value, selector), 175 | borderTop: (value: PropEnhancerValueType, selector: string) => getCss(borderTop, value, selector), 176 | borderTopColor: (value: PropEnhancerValueType, selector: string) => getCss(borderTopColor, value, selector), 177 | borderTopStyle: (value: PropEnhancerValueType, selector: string) => getCss(borderTopStyle, value, selector), 178 | borderTopWidth: (value: PropEnhancerValueType, selector: string) => getCss(borderTopWidth, value, selector) 179 | } 180 | -------------------------------------------------------------------------------- /src/enhancers/grid.ts: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import getCss from '../get-css' 3 | import { PropEnhancerValueType, PropValidators, PropEnhancers, PropTypesMapping, PropAliases } from '../types/enhancers' 4 | 5 | export const propTypes: PropTypesMapping = { 6 | columnGap: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 7 | gap: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 8 | grid: PropTypes.string, 9 | gridArea: PropTypes.string, 10 | gridAutoColumns: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 11 | gridAutoFlow: PropTypes.string, 12 | gridAutoRows: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 13 | gridColumn: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 14 | gridColumnEnd: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 15 | gridColumnGap: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 16 | gridColumnStart: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 17 | gridGap: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 18 | gridRow: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 19 | gridRowEnd: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 20 | gridRowGap: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 21 | gridRowStart: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 22 | gridTemplate: PropTypes.string, 23 | gridTemplateAreas: PropTypes.string, 24 | gridTemplateColumns: PropTypes.string, 25 | gridTemplateRows: PropTypes.string, 26 | rowGap: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) 27 | } 28 | 29 | export const propAliases: PropAliases = {} 30 | 31 | export const propValidators: PropValidators = {} 32 | 33 | const columnGap = { 34 | className: 'col-gap', 35 | cssName: 'column-gap', 36 | jsName: 'columnGap' 37 | } 38 | const gap = { 39 | className: 'gap', 40 | cssName: 'gap', 41 | jsName: 'gap' 42 | } 43 | const grid = { 44 | className: 'grd', 45 | cssName: 'grid', 46 | jsName: 'grid', 47 | complexValue: true 48 | } 49 | const gridArea = { 50 | className: 'grd-ara', 51 | cssName: 'grid-area', 52 | jsName: 'gridArea', 53 | complexValue: true 54 | } 55 | const gridAutoColumns = { 56 | className: 'grd-ato-col', 57 | cssName: 'grid-auto-columns', 58 | jsName: 'gridAutoColumns', 59 | complexValue: true 60 | } 61 | const gridAutoFlow = { 62 | className: 'grd-ato-flw', 63 | cssName: 'grid-auto-flow', 64 | jsName: 'gridAutoFlow' 65 | } 66 | const gridAutoRows = { 67 | className: 'grd-ato-row', 68 | cssName: 'grid-auto-rows', 69 | jsName: 'gridAutoRows', 70 | complexValue: true 71 | } 72 | const gridColumn = { 73 | className: 'grd-col', 74 | cssName: 'grid-column', 75 | jsName: 'gridColumn', 76 | defaultUnit: '', 77 | complexValue: true 78 | } 79 | const gridColumnEnd = { 80 | className: 'grd-col-end', 81 | cssName: 'grid-column-end', 82 | jsName: 'gridColumnEnd', 83 | defaultUnit: '' 84 | } 85 | const gridColumnGap = { 86 | className: 'grd-col-gap', 87 | cssName: 'grid-column-gap', 88 | jsName: 'gridColumnGap' 89 | } 90 | const gridColumnStart = { 91 | className: 'grd-col-str', 92 | cssName: 'grid-column-start', 93 | jsName: 'gridColumnStart', 94 | defaultUnit: '' 95 | } 96 | const gridGap = { 97 | className: 'grd-gap', 98 | cssName: 'grid-gap', 99 | jsName: 'gridGap' 100 | } 101 | const gridRow = { 102 | className: 'grd-row', 103 | cssName: 'grid-row', 104 | jsName: 'gridRow', 105 | defaultUnit: '', 106 | complexValue: true 107 | } 108 | const gridRowEnd = { 109 | className: 'grd-row-end', 110 | cssName: 'grid-row-end', 111 | jsName: 'gridRowEnd', 112 | defaultUnit: '' 113 | } 114 | const gridRowGap = { 115 | className: 'grd-row-gap', 116 | cssName: 'grid-row-gap', 117 | jsName: 'gridRowGap' 118 | } 119 | const gridRowStart = { 120 | className: 'grd-row-str', 121 | cssName: 'grid-row-start', 122 | jsName: 'gridRowStart', 123 | defaultUnit: '' 124 | } 125 | const gridTemplate = { 126 | className: 'grd-tmp', 127 | cssName: 'grid-template', 128 | jsName: 'gridTemplate', 129 | complexValue: true 130 | } 131 | const gridTemplateAreas = { 132 | className: 'grd-tmp-ara', 133 | cssName: 'grid-template-areas', 134 | jsName: 'gridTemplateAreas', 135 | complexValue: true 136 | } 137 | const gridTemplateColumns = { 138 | className: 'grd-tmp-col', 139 | cssName: 'grid-template-columns', 140 | jsName: 'gridTemplateColumns', 141 | complexValue: true 142 | } 143 | const gridTemplateRows = { 144 | className: 'grd-tmp-row', 145 | cssName: 'grid-template-rows', 146 | jsName: 'gridTemplateRows', 147 | complexValue: true 148 | } 149 | const rowGap = { 150 | className: 'row-gap', 151 | cssName: 'row-gap', 152 | jsName: 'rowGap' 153 | } 154 | 155 | export const propEnhancers: PropEnhancers = { 156 | columnGap: (value: PropEnhancerValueType, selector: string) => getCss(columnGap, value, selector), 157 | gap: (value: PropEnhancerValueType, selector: string) => getCss(gap, value, selector), 158 | grid: (value: PropEnhancerValueType, selector: string) => getCss(grid, value, selector), 159 | gridArea: (value: PropEnhancerValueType, selector: string) => getCss(gridArea, value, selector), 160 | gridAutoColumns: (value: PropEnhancerValueType, selector: string) => getCss(gridAutoColumns, value, selector), 161 | gridAutoFlow: (value: PropEnhancerValueType, selector: string) => getCss(gridAutoFlow, value, selector), 162 | gridAutoRows: (value: PropEnhancerValueType, selector: string) => getCss(gridAutoRows, value, selector), 163 | gridColumn: (value: PropEnhancerValueType, selector: string) => getCss(gridColumn, value, selector), 164 | gridColumnEnd: (value: PropEnhancerValueType, selector: string) => getCss(gridColumnEnd, value, selector), 165 | gridColumnGap: (value: PropEnhancerValueType, selector: string) => getCss(gridColumnGap, value, selector), 166 | gridColumnStart: (value: PropEnhancerValueType, selector: string) => getCss(gridColumnStart, value, selector), 167 | gridGap: (value: PropEnhancerValueType, selector: string) => getCss(gridGap, value, selector), 168 | gridRow: (value: PropEnhancerValueType, selector: string) => getCss(gridRow, value, selector), 169 | gridRowEnd: (value: PropEnhancerValueType, selector: string) => getCss(gridRowEnd, value, selector), 170 | gridRowGap: (value: PropEnhancerValueType, selector: string) => getCss(gridRowGap, value, selector), 171 | gridRowStart: (value: PropEnhancerValueType, selector: string) => getCss(gridRowStart, value, selector), 172 | gridTemplate: (value: PropEnhancerValueType, selector: string) => getCss(gridTemplate, value, selector), 173 | gridTemplateAreas: (value: PropEnhancerValueType, selector: string) => getCss(gridTemplateAreas, value, selector), 174 | gridTemplateColumns: (value: PropEnhancerValueType, selector: string) => getCss(gridTemplateColumns, value, selector), 175 | gridTemplateRows: (value: PropEnhancerValueType, selector: string) => getCss(gridTemplateRows, value, selector), 176 | rowGap: (value: PropEnhancerValueType, selector: string) => getCss(rowGap, value, selector) 177 | } 178 | -------------------------------------------------------------------------------- /tools/box.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react' 2 | import { default as Box, configureSafeHref, BoxProps } from '../src' 3 | import { storiesOf } from '@storybook/react' 4 | import allPropertiesComponent from './all-properties-component' 5 | import SelectorUniqueness from './fixtures/selector-uniquness-story' 6 | import KeyframesStory from './fixtures/keyframes-story' 7 | import SelectorsStory from './fixtures/selectors-story' 8 | 9 | const RedBox: React.FunctionComponent> = redBoxProps => ( 10 | 11 | ) 12 | 13 | const logRef = (ref: Element | null) => console.log(ref) 14 | const reactRef = React.createRef() 15 | 16 | interface CustomProps { 17 | children: React.ReactNode 18 | } 19 | 20 | const CustomComp: React.FunctionComponent = props => { 21 | return ( 22 |
23 | custom component 24 | {props.children} 25 |
26 | ) 27 | } 28 | 29 | storiesOf('Box', module) 30 | .add(`is=''`, () => { 31 | return ( 32 | 33 | h1 34 | h2 35 | h3 36 | p 37 | strong 38 | 39 | 40 | ) 41 | }) 42 | .add('safe `href`', () => { 43 | configureSafeHref({ 44 | enabled: true 45 | }) 46 | return ( 47 | 48 | Links 49 | 50 | Internal Link 51 | 52 | 53 | Same Origin Link 54 | 55 | 56 | External Link 57 | 58 | 59 | Javascript protocol Link 60 | 61 | 62 | Overwride Safe Href 63 | 64 | 65 | ) 66 | }) 67 | .add('unsafe `href`', () => { 68 | configureSafeHref({ 69 | enabled: false 70 | }) 71 | return ( 72 | 73 | Links 74 | 75 | Internal Link 76 | 77 | 78 | Same Origin Link 79 | 80 | 81 | External Link 82 | 83 | 84 | Javascript protocol Link 85 | 86 | 87 | Overwride Safe Href 88 | 89 | 90 | ) 91 | }) 92 | .add(`custom comp`, () => ( 93 | 94 | 95 | chiiillld 96 | 97 | 98 | )) 99 | .add('background', () => ( 100 | 101 | 102 | 103 | 110 | 111 | )) 112 | .add('borderRadius', () => ( 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | )) 121 | .add('borders', () => ( 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | )) 133 | .add('boxShadow', () => ( 134 | 135 | 136 | 137 | )) 138 | .add('dimensions', () => ( 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | )) 147 | .add('display', () => ( 148 | 149 | 150 | inline 151 | 152 | )) 153 | .add('flex', () => ( 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | )) 162 | .add('overflow', () => ( 163 | 164 | 165 | 166 | 167 | 168 | )) 169 | .add('position', () => ( 170 | 171 | 172 | 173 | )) 174 | .add('spacing', () => ( 175 | 176 | 177 | 178 | )) 179 | .add('text', () => ( 180 | 181 | Center 182 | Right 183 | Middle 184 | Right 185 | sans-serif 186 | bold 187 | bold 188 | 72px 189 | 190 | )) 191 | .add('list', () => ( 192 | 193 | I՚m 194 | a 195 | list 196 | 197 | )) 198 | .add('utils', () => ( 199 | 200 | Center 201 | boxSizing: border-box 202 | 203 | )) 204 | .add('ref', () => ( 205 | 206 | ref 207 | 208 | )) 209 | .add('ref as React ref', () => ( 210 | 211 | React ref 212 | 213 | )) 214 | .add('props pass through', () => { 215 | interface CustomComponentProps { 216 | foo: string 217 | baz: number 218 | fizz: { 219 | buzz: boolean 220 | } 221 | } 222 | 223 | const CustomComponent: React.FC = props => {JSON.stringify(props, undefined, 4)} 224 | 225 | return ( 226 | 227 | 228 | 229 | 230 | ) 231 | }) 232 | .add('all properties', () => ( 233 | 234 | {allPropertiesComponent()} 235 | {allPropertiesComponent()} 236 | 237 | )) 238 | .add('overrides', () => ( 239 | 240 | 241 | 242 | )) 243 | .add('selectors', () => ) 244 | .add('selector uniqueness', () => ) 245 | .add('style prop', () => { 246 | const style: CSSProperties = { backgroundColor: 'red', width: 200 } 247 | return {JSON.stringify(style, undefined, 4)} 248 | }) 249 | .add('keyframes', () => ) 250 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | UI-BOX: Blazing Fast React UI Primitive 5 |
6 |
7 |
8 | 9 | 10 | 11 |
12 |
13 |
14 |
15 |
16 | 17 | 📦 ui-box is a low level CSS-in-JS solution that focuses on being simple, fast and extensible. All CSS properties are set using simple React props, which allows you to easily create reusable components that can be enhanced with additional CSS properties. This is very useful for adding things like margins to components, which would normally require adding non-reusable wrapper elements/classes. 18 | 19 | ## Install 20 | 21 | ```shell 22 | yarn add ui-box 23 | # or 24 | npm install --save ui-box 25 | ``` 26 | 27 | ## Usage 28 | 29 | ```jsx 30 | import Box from 'ui-box' 31 | 32 | function Button(props) { 33 | return 34 | } 35 | 36 | function Example() { 37 | return ( 38 | 41 | ) 42 | } 43 | ``` 44 | 45 | The above example component renders a red, disabled `