Skip to content

Instantly share code, notes, and snippets.

@smashercosmo
Created July 30, 2020 14:17
Show Gist options
  • Save smashercosmo/327dcb8ac3f8a4182d8cf0b7aabef175 to your computer and use it in GitHub Desktop.
Save smashercosmo/327dcb8ac3f8a4182d8cf0b7aabef175 to your computer and use it in GitHub Desktop.
Base
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