Created
July 30, 2020 14:17
-
-
Save smashercosmo/327dcb8ac3f8a4182d8cf0b7aabef175 to your computer and use it in GitHub Desktop.
Base
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useContext } from 'react' | |
import cx from 'classnames' | |
import { ThemeContext } from '../ThemeContext/ThemeContext' | |
import type { Theme } from '../ThemeContext/ThemeContext' | |
import './Base.css' | |
type ResponsiveProperty<T> = T | { min?: T; max: T } | |
type MediaQueryProperty<T> = T | { xs?: T; sm?: T; md?: T; lg?: T; xl?: T } | |
type PositiveSpace = 0 | 8 | 16 | 24 | 32 | 48 | 64 | 96 | 128 | 192 | |
type NegativeSpace = -192 | -128 | -96 | -64 | -48 | -32 | -24 | -16 | -8 | 0 | |
type FontSpace = 12 | 14 | 20 | PositiveSpace | |
type Dimensions<T> = T extends { __dangerousNonStrictMode: true } | |
? number | '100%' | '-100%' | '100vw' | '100vh' | |
: 0 | '100%' | '-100%' | '100vw' | '100vh' | |
type Padding<T> = T extends { __dangerousNonStrictMode: true } | |
? ResponsiveProperty<number> | |
: ResponsiveProperty<PositiveSpace> | |
type Margin<T> = | |
| (T extends { __dangerousNonStrictMode: true } | |
? ResponsiveProperty<number> | |
: ResponsiveProperty<NegativeSpace>) | |
| 'auto' | |
type Gap = ResponsiveProperty<PositiveSpace> | |
type FontSize = ResponsiveProperty<FontSpace> | |
type Position = 'relative' | 'absolute' | 'fixed' | 'sticky' | |
type Coordinates = 0 | '100%' | '-100%' | |
type Display = 'flex' | 'grid' | 'inline' | 'block' | 'inline-block' | 'none' | |
type Overflow = 'hidden' | 'visible' | 'scroll' | |
type FontWeight = 'normal' | 'bold' | |
type FontStyle = 'italic' | |
type FontFamily = 'rift' | 'sans' | |
type LineHeight = 0 | 1 | |
type TextAlign = 'left' | 'center' | 'right' | |
type AlignItems = 'baseline' | 'center' | 'flex-start' | 'flex-end' | |
type JustifyContent = 'flex-start' | 'flex-end' | 'center' | 'space-between' | |
type FlexWrap = 'wrap' | |
type WhiteSpace = 'nowrap' | |
type WordBreak = 'break-word' | |
type Stroke = 0 | 1 | 2 | 3 | 4 | |
type Colors = | |
| 'grey' | |
| 'grey-dark' | |
| 'grey-light' | |
| 'grey-light-2' | |
| 'blue' | |
| 'blue-light' | |
| 'blue-dark' | |
| 'red' | |
| 'white' | |
| 'inherit' | |
type BaseProps< | |
T = { | |
__dangerousNonStrictMode: false | |
} | |
> = { | |
__dangerousClassName?: string | |
__dangerousStyle?: React.CSSProperties | |
p?: Padding<T> | |
px?: Padding<T> | |
py?: Padding<T> | |
pl?: Padding<T> | |
pr?: Padding<T> | |
pt?: Padding<T> | |
pb?: Padding<T> | |
m?: Margin<T> | |
mx?: Margin<T> | |
my?: Margin<T> | |
ml?: Margin<T> | |
mr?: Margin<T> | |
mt?: Margin<T> | |
mb?: Margin<T> | |
gap?: Gap | |
rowGap?: Gap | |
columnGap?: Gap | |
width?: Dimensions<T> | |
height?: Dimensions<T> | |
maxWidth?: Dimensions<T> | |
maxHeight?: Dimensions<T> | |
minWidth?: Dimensions<T> | |
minHeight?: Dimensions<T> | |
fontSize?: FontSize | |
fontWeight?: FontWeight | |
fontStyle?: FontStyle | |
fontFamily?: FontFamily | |
lineHeight?: LineHeight | |
position?: Position | |
top?: Coordinates | |
bottom?: Coordinates | |
left?: Coordinates | |
right?: Coordinates | |
display?: MediaQueryProperty<Display> | |
overflow?: Overflow | |
textAlign?: MediaQueryProperty<TextAlign> | |
flexWrap?: MediaQueryProperty<FlexWrap> | |
alignItems?: MediaQueryProperty<AlignItems> | |
justifyContent?: MediaQueryProperty<JustifyContent> | |
whiteSpace?: WhiteSpace | |
wordBreak?: WordBreak | |
color?: Colors | |
bg?: Colors | |
borderColor?: Colors | |
borderWidth?: Stroke | |
borderLeftWidth?: Stroke | |
borderRightWidth?: Stroke | |
borderTopWidth?: Stroke | |
borderBottomWidth?: Stroke | |
} | |
function capitalize(string: string) { | |
return string.charAt(0).toUpperCase() + string.slice(1) | |
} | |
function snakeCaseToPascalCase(string: string) { | |
return string.split('-').map(capitalize).join('') | |
} | |
function getMediaQueryProperty<T extends string>({ | |
name, | |
value, | |
}: { | |
name: string | |
value?: MediaQueryProperty<T> | |
}) { | |
if (typeof value === 'object') { | |
const { xs } = value | |
const sm = value.sm ? value.sm : xs | |
const md = value.md ? value.md : sm | |
const lg = value.lg ? value.lg : md | |
const xl = value.xl ? value.xl : lg | |
const mediaAll = xs ? `var(--mqxs, ${xs})` : undefined | |
const mediaSm = sm ? `var(--mqsm, ${sm})` : undefined | |
const mediaMd = md ? `var(--mqmd, ${md})` : undefined | |
const mediaLg = lg ? `var(--mqlg, ${lg})` : undefined | |
const mediaXl = xl ? `var(--mqxl, ${xl})` : undefined | |
return { | |
[`--${name}`]: [mediaAll, mediaSm, mediaMd, mediaLg, mediaXl] | |
.filter(Boolean) | |
.join(' '), | |
} | |
} | |
return { | |
...(value === undefined ? {} : { [`--${name}`]: value }), | |
} | |
} | |
function getResponsiveProperty({ | |
name, | |
value: valueFromArgs, | |
boundaries, | |
}: { | |
name: string | |
value?: ResponsiveProperty<number> | string | |
boundaries?: Partial<Record<number, { min: number; max: number }>> | |
}) { | |
const value = | |
boundaries && typeof valueFromArgs === 'number' && boundaries[valueFromArgs] | |
? boundaries[valueFromArgs] | |
: valueFromArgs | |
if (value === null || value === undefined) return undefined | |
if ( | |
typeof value === 'object' && | |
typeof value.max === 'number' && | |
typeof value.min === 'number' && | |
value.min === value.max | |
) { | |
return { | |
[`--${name}`]: String(value.max), | |
} | |
} | |
if ( | |
typeof value === 'object' && | |
typeof value.max === 'number' && | |
(typeof value.min === 'number' || value.min === undefined) && | |
value.min !== value.max | |
) { | |
return { | |
[`--${name}-min`]: String(value.min || value.max / 2), | |
[`--${name}-max`]: String(value.max), | |
} | |
} | |
if (typeof value === 'number') { | |
return { | |
[`--${name}-min`]: value > 0 ? String(value / 2) : value, | |
[`--${name}-max`]: value <= 0 ? String(value / 2) : value, | |
} | |
} | |
return { | |
[`--${name}`]: String(value), | |
} | |
} | |
function getPadding<T>( | |
props: { | |
p?: Padding<T> | |
px?: Padding<T> | |
py?: Padding<T> | |
pl?: Padding<T> | |
pr?: Padding<T> | |
pt?: Padding<T> | |
pb?: Padding<T> | |
}, | |
theme?: Theme, | |
) { | |
const { p, px, py, pl: _pl, pr: _pr, pt: _pt, pb: _pb } = props | |
const pl = _pl ?? px ?? p | |
const pr = _pr ?? px ?? p | |
const pt = _pt ?? py ?? p | |
const pb = _pb ?? py ?? p | |
const responsivePl = getResponsiveProperty({ | |
name: theme?.aliases.paddingLeft || 'padding-left', | |
value: pl, | |
}) | |
const responsivePr = getResponsiveProperty({ | |
name: theme?.aliases.paddingRight || 'padding-right', | |
value: pr, | |
}) | |
const responsivePt = getResponsiveProperty({ | |
name: theme?.aliases.paddingTop || 'padding-top', | |
value: pt, | |
}) | |
const responsivePb = getResponsiveProperty({ | |
name: theme?.aliases.paddingBottom || 'padding-bottom', | |
value: pb, | |
}) | |
return { | |
...responsivePl, | |
...responsivePr, | |
...responsivePt, | |
...responsivePb, | |
} | |
} | |
function getMargin<T>( | |
props: { | |
m?: Margin<T> | |
mx?: Margin<T> | |
my?: Margin<T> | |
ml?: Margin<T> | |
mr?: Margin<T> | |
mt?: Margin<T> | |
mb?: Margin<T> | |
}, | |
theme?: Theme, | |
) { | |
const { m, mx, my, ml: _ml, mr: _mr, mt: _mt, mb: _mb } = props | |
const ml = _ml ?? mx ?? m | |
const mr = _mr ?? mx ?? m | |
const mt = _mt ?? my ?? m | |
const mb = _mb ?? my ?? m | |
const responsiveMl = | |
ml === 'auto' | |
? { '--ml-auto': 'auto' } | |
: getResponsiveProperty({ | |
name: theme?.aliases.marginLeft || 'margin-left', | |
value: ml, | |
}) | |
const responsiveMr = | |
mr === 'auto' | |
? { '--mr-auto': 'auto' } | |
: getResponsiveProperty({ | |
name: theme?.aliases.marginRight || 'margin-right', | |
value: mr, | |
}) | |
const responsiveMt = | |
mt === 'auto' | |
? { '--mt-auto': 'auto' } | |
: getResponsiveProperty({ | |
name: theme?.aliases.marginTop || 'margin-top', | |
value: mt, | |
}) | |
const responsiveMb = | |
mb === 'auto' | |
? { '--mb-auto': 'auto' } | |
: getResponsiveProperty({ | |
name: theme?.aliases.marginBottom || 'margin-bottom', | |
value: mb, | |
}) | |
return { | |
...responsiveMl, | |
...responsiveMr, | |
...responsiveMt, | |
...responsiveMb, | |
} | |
} | |
function getBorderWidth(props: { | |
borderWidth?: Stroke | |
borderLeftWidth?: Stroke | |
borderRightWidth?: Stroke | |
borderTopWidth?: Stroke | |
borderBottomWidth?: Stroke | |
}) { | |
const { | |
borderWidth, | |
borderLeftWidth: _borderLeftWidth, | |
borderRightWidth: _borderRightWidth, | |
borderTopWidth: _borderTopWidth, | |
borderBottomWidth: _borderBottomWidth, | |
} = props | |
const borderLeftWidth = _borderLeftWidth ?? borderWidth | |
const borderRightWidth = _borderRightWidth ?? borderWidth | |
const borderTopWidth = _borderTopWidth ?? borderWidth | |
const borderBottomWidth = _borderBottomWidth ?? borderWidth | |
return { | |
...(borderRightWidth === undefined | |
? {} | |
: { [`--bdrw`]: String(borderRightWidth) }), | |
...(borderLeftWidth === undefined | |
? {} | |
: { [`--bdlw`]: String(borderLeftWidth) }), | |
...(borderTopWidth === undefined | |
? {} | |
: { [`--bdtw`]: String(borderTopWidth) }), | |
...(borderBottomWidth === undefined | |
? {} | |
: { [`--bdbw`]: String(borderBottomWidth) }), | |
} | |
} | |
function getGap<T>( | |
props: { gap?: Gap; rowGap?: Gap; columnGap?: Gap }, | |
theme: Theme, | |
) { | |
const { gap, rowGap: _rowGap, columnGap: _columnGap } = props | |
const rowGap = _rowGap ?? gap | |
const columnGap = _columnGap ?? gap | |
const responsiveRowGap = getResponsiveProperty({ | |
name: theme?.aliases.rowGap || 'row-gap', | |
value: rowGap, | |
}) | |
const responsiveColumnGap = getResponsiveProperty({ | |
name: theme?.aliases.columnGap || 'column-gap', | |
value: columnGap, | |
}) | |
return { | |
...responsiveRowGap, | |
...responsiveColumnGap, | |
} | |
} | |
function getClassNameFromProps<T>({ | |
props, | |
componentClassName, | |
}: { | |
props: BaseProps<T> | |
componentClassName?: string | |
}) { | |
const { | |
fontFamily, | |
fontWeight, | |
fontStyle, | |
whiteSpace, | |
wordBreak, | |
overflow, | |
__dangerousClassName, | |
} = props | |
return cx( | |
'root', | |
componentClassName, | |
fontFamily && `ff${snakeCaseToPascalCase(fontFamily)}`, | |
fontWeight && `fw${snakeCaseToPascalCase(fontWeight)}`, | |
fontStyle && `fs${snakeCaseToPascalCase(fontStyle)}`, | |
whiteSpace && `ws${snakeCaseToPascalCase(whiteSpace)}`, | |
wordBreak && `wb${snakeCaseToPascalCase(wordBreak)}`, | |
overflow && `o${snakeCaseToPascalCase(overflow)}`, | |
__dangerousClassName, | |
) | |
} | |
function useProps<T>({ | |
props, | |
componentClassName, | |
}: { | |
props: BaseProps<T> | |
componentClassName?: string | |
}) { | |
const theme = useContext(ThemeContext) | |
const { | |
p, | |
px, | |
py, | |
pl, | |
pr, | |
pt, | |
pb, | |
m, | |
mx, | |
my, | |
ml, | |
mr, | |
mt, | |
mb, | |
gap, | |
rowGap, | |
columnGap, | |
width, | |
height, | |
maxWidth, | |
maxHeight, | |
minWidth, | |
minHeight, | |
position, | |
top, | |
bottom, | |
left, | |
right, | |
color, | |
bg, | |
borderColor, | |
borderWidth, | |
borderRightWidth, | |
borderLeftWidth, | |
borderTopWidth, | |
borderBottomWidth, | |
fontSize, | |
lineHeight, | |
textAlign, | |
flexWrap, | |
alignItems, | |
justifyContent, | |
fontFamily, | |
fontWeight, | |
fontStyle, | |
whiteSpace, | |
wordBreak, | |
display, | |
overflow, | |
__dangerousStyle, | |
__dangerousClassName, | |
...rest | |
} = props | |
const className = getClassNameFromProps({ | |
props: { | |
fontFamily, | |
fontWeight, | |
fontStyle, | |
whiteSpace, | |
wordBreak, | |
display, | |
overflow, | |
__dangerousClassName, | |
}, | |
componentClassName, | |
}) | |
const style = { | |
...getPadding({ p, px, py, pl, pr, pt, pb }, theme), | |
...getMargin({ m, mx, my, ml, mr, mt, mb }, theme), | |
...getGap({ gap, rowGap, columnGap }, theme), | |
...(width === undefined ? {} : { [`--w`]: width }), | |
...(height === undefined ? {} : { [`--h`]: height }), | |
...(maxWidth === undefined ? {} : { [`--max-w`]: maxWidth }), | |
...(maxHeight === undefined ? {} : { [`--max-h`]: maxHeight }), | |
...(minWidth === undefined ? {} : { [`--min-w`]: minWidth }), | |
...(minHeight === undefined ? {} : { [`--min-h`]: minHeight }), | |
...(position ? { [`--pos`]: position } : {}), | |
...(top !== undefined ? { [`--t`]: top } : {}), | |
...(bottom !== undefined ? { [`--b`]: bottom } : {}), | |
...(left !== undefined ? { [`--l`]: left } : {}), | |
...(right !== undefined ? { [`--r`]: right } : {}), | |
...(color ? { [`--c`]: `var(--${color})` } : {}), | |
...(bg ? { [`--bgc`]: `var(--${bg})` } : {}), | |
...(borderColor ? { [`--bdc`]: `var(--${borderColor})` } : {}), | |
...getBorderWidth({ | |
borderWidth, | |
borderRightWidth, | |
borderLeftWidth, | |
borderTopWidth, | |
borderBottomWidth, | |
}), | |
...getResponsiveProperty({ | |
name: theme.aliases.fontSize || 'fontSize', | |
value: fontSize, | |
boundaries: theme.boundaries.fontSize, | |
}), | |
...(lineHeight !== undefined ? { [`--lh`]: lineHeight } : {}), | |
...getMediaQueryProperty({ name: 'ta', value: textAlign }), | |
...getMediaQueryProperty({ name: 'd', value: display }), | |
...getMediaQueryProperty({ name: 'fxw', value: flexWrap }), | |
...getMediaQueryProperty({ name: 'fxai', value: alignItems }), | |
...getMediaQueryProperty({ name: 'fxjc', value: justifyContent }), | |
...(__dangerousStyle || {}), | |
} | |
return { | |
style, | |
className, | |
props: rest, | |
} | |
} | |
type ComponentWithoutClassName<T extends keyof JSX.IntrinsicElements> = Omit< | |
JSX.IntrinsicElements[T], | |
'className' | |
> | |
function Box<T>(props: BaseProps<T> & ComponentWithoutClassName<'div'>) { | |
const { style, className, props: rest } = useProps({ | |
props, | |
componentClassName: 'box', | |
}) | |
return <div style={style} className={className} {...rest} /> | |
} | |
function Text<T>(props: BaseProps<T> & ComponentWithoutClassName<'span'>) { | |
const { style, className, props: rest } = useProps({ | |
props, | |
componentClassName: 'text', | |
}) | |
return <div style={style} className={className} {...rest} /> | |
} | |
function Paragraph<T>(props: BaseProps<T> & ComponentWithoutClassName<'p'>) { | |
const { style, className, props: rest } = useProps({ | |
props, | |
componentClassName: 'paragraph', | |
}) | |
return <p style={style} className={className} {...rest} /> | |
} | |
function Li<T>(props: BaseProps<T> & ComponentWithoutClassName<'li'>) { | |
const { style, className, props: rest } = useProps({ | |
props, | |
componentClassName: 'listitem', | |
}) | |
return <li style={style} className={className} {...rest} /> | |
} | |
function Grid<T>(props: BaseProps<T> & ComponentWithoutClassName<'div'>) { | |
const { style, className, props: rest } = useProps({ | |
props, | |
componentClassName: 'grid', | |
}) | |
return <div style={style} className={className} {...rest} /> | |
} | |
function Flex<T>(props: BaseProps<T> & ComponentWithoutClassName<'div'>) { | |
const { style, className, props: rest } = useProps({ | |
props, | |
componentClassName: 'flex', | |
}) | |
return <div style={style} className={className} {...rest} /> | |
} | |
const [ | |
Footer, | |
Article, | |
Nav, | |
Blockquote, | |
Legend, | |
Figure, | |
Figcaption, | |
Ul, | |
Dl, | |
Dt, | |
Dd, | |
] = ([ | |
'footer', | |
'article', | |
'nav', | |
'blockquote', | |
'legend', | |
'figure', | |
'figcaption', | |
'ul', | |
'dl', | |
'dt', | |
'dd', | |
] as const).map((Tag) => { | |
function Component<T>( | |
props: BaseProps<T> & ComponentWithoutClassName<typeof Tag>, | |
) { | |
const { style, className, props: rest } = useProps({ | |
props, | |
componentClassName: 'box', | |
}) | |
return <Tag style={style} className={className} {...rest} /> | |
} | |
Component.displayName = capitalize(Tag) | |
return Component | |
}) | |
const [H1, H2, H3, H4, H5, H6] = ([ | |
'h1', | |
'h2', | |
'h3', | |
'h4', | |
'h5', | |
'h6', | |
] as const).map((Heading) => { | |
return function Component<T>( | |
props: BaseProps<T> & ComponentWithoutClassName<typeof Heading>, | |
) { | |
const { style, className, props: rest } = useProps<T>({ | |
props: { ...props, fontFamily: 'rift' }, | |
componentClassName: 'heading', | |
}) | |
return <Heading style={style} className={className} {...rest} /> | |
} | |
}) | |
export type { BaseProps } | |
export type { Colors } | |
export type { ResponsiveProperty } | |
export type { MediaQueryProperty } | |
export type { AlignItems } | |
export type { JustifyContent } | |
export type { PositiveSpace } | |
export type { NegativeSpace } | |
export type { Padding } | |
export type { Margin } | |
export type { FontSize } | |
export type { Gap } | |
export { useProps } | |
export { Box } | |
export { Text } | |
export { Grid } | |
export { Flex } | |
export { Paragraph } | |
export { Article } | |
export { Nav } | |
export { Ul } | |
export { Li } | |
export { Dl } | |
export { Dt } | |
export { Dd } | |
export { Footer } | |
export { Blockquote } | |
export { Legend } | |
export { Figure } | |
export { Figcaption } | |
export { H1 } | |
export { H2 } | |
export { H3 } | |
export { H4 } | |
export { H5 } | |
export { H6 } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment