import { CactusTheme } from '@repay/cactus-theme' import { Property } from 'csstype' import { omit, pick } from 'lodash' import PropTypes from 'prop-types' import React from 'react' import styled, { StyledComponent, ThemedStyledFunction } from 'styled-components' import * as SS from 'styled-system' import { isIE } from './constants' // This file exists, in part, because styled-components types are a PAIN. export type Styled

= StyledComponent, CactusTheme> // This works around the `babel-plugin-styled-components` `displayName` setting, // so we can specify the CSS class of the resulting component. export const styledWithClass = ( component: C, className: string ): ThemedStyledFunction => { const tag: any = styled(component) return tag.withConfig({ componentId: className }) } // This gives us a styled component without the polymorphic behavior: the `as` // attr overrides any `as` prop passed in. Unfortunately I can't think of a way // to make Typescript consider it an invalid prop as well. export const styledUnpoly = ( component: C, fixed: F = component as any ): ThemedStyledFunction => { let tag: any = styled(component).attrs({ as: fixed } as any) if ('displayName' in fixed) { tag = tag.withConfig({ displayName: (fixed as any).displayName }) } return tag } const cssVal = PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired export const styledProp = PropTypes.oneOfType([cssVal, PropTypes.arrayOf(cssVal)]) export const classes = (...args: (string | undefined)[]): string => args.filter(Boolean).join(' ') export const pickStyles = (styles: SS.styleFn, ...keys: string[]): SS.styleFn => { if (!styles.config) return styles return SS.createParser(pick(styles.config, keys)) } export const omitStyles = (styles: SS.styleFn, ...keys: string[]): SS.styleFn => { if (!styles.config) return styles return SS.createParser(omit(styles.config, keys)) } // Keeping this separate from `flexContainer` styles because it applies // to the element's children, not the element itself. export const gapWorkaround: SS.styleFn | undefined = (function () { if (typeof CSS !== 'undefined' && CSS.supports?.('gap', '1px')) return const calcGap = (value: any, scale: SS.Scale | undefined) => { try { value = scale?.[value] ?? value } catch {} if (typeof value === 'number') { return value / 2 } else if (typeof value === 'string' && !isNaN(parseFloat(value))) { return `calc(${value.trim()} / 2)` } return value } const makeGapParser = (...props: string[]) => { const parser: SS.ConfigFunction = (value, scale) => { const styles: any = {} const gap = calcGap(value, scale) for (const prop of props) { styles[prop] = gap } return { '&&&& > *': styles } } parser.scale = 'space' return parser } return SS.system({ gap: makeGapParser('margin'), rowGap: makeGapParser('marginTop', 'marginBottom'), colGap: makeGapParser('marginLeft', 'marginRight'), }) })() // IE doesn't support `space-evenly`, but you can fake it using pseudo-elements. const spaceEvenlyFix: SS.Config | undefined = (function () { if (!isIE) return const justifyContent = (value: any) => { if (!value) return const placeholderBlock = { content: "''", display: 'none', flex: '0 0 0' } const styles = { justifyContent: value, '&::before': placeholderBlock, '&::after': placeholderBlock, } if (value === 'space-evenly') { placeholderBlock.display = 'block' styles.justifyContent = 'space-between' } return styles } return { justifyContent } })() const flexKeys = [ 'alignItems', 'alignContent', 'justifyContent', 'flexWrap', 'flexDirection', ] as const const itemKeys = ['flex', 'flexGrow', 'flexShrink', 'flexBasis', 'alignSelf', 'order'] as const export interface FlexProps extends Pick { flexFlow?: SS.ResponsiveValue gap?: SS.ResponsiveValue> rowGap?: SS.ResponsiveValue> colGap?: SS.ResponsiveValue> } export interface FlexOptionProps extends Omit { flexFlow?: SS.ResponsiveValue } export type FlexItemProps = Pick export const flexContainer = SS.system({ ...pick(SS.flexbox.config, flexKeys), ...spaceEvenlyFix, gap: { property: 'gap', scale: 'space' }, rowGap: { property: 'rowGap', scale: 'space' }, colGap: { property: 'columnGap', scale: 'space' }, flexFlow: true, }) export const flexItem = pickStyles(SS.flexbox, ...itemKeys) // Not exhaustive, but all possible values include at least one of these words. const isFlexKey = RegExp.prototype.test.bind(/row|column|reverse|wrap/) // Just like `flexContainer`, except it expects the default display mode to be // something other than flex and has a shortcut to "activate" flex behavior: // `` -> ``. // Be careful that it doesn't override another display mode, e.g. `inline-flex`. export const flexContainerOption = SS.system({ ...flexContainer.config, flexFlow: (value: any) => { if (typeof value === 'boolean') { return value ? { display: 'flex' } : undefined } else if (isFlexKey(value)) { return { display: 'flex', flexFlow: value } } }, }) export type AllWidthProps = SS.WidthProps & SS.MinWidthProps & SS.MaxWidthProps export const allWidth = SS.compose(SS.width, SS.minWidth, SS.maxWidth) export type AllHeightProps = SS.HeightProps & SS.MinHeightProps & SS.MaxHeightProps export const allHeight = SS.compose(SS.height, SS.minHeight, SS.maxHeight) export type SizingProps = AllWidthProps & AllHeightProps export const sizing = SS.compose( SS.width, SS.minWidth, SS.maxWidth, SS.height, SS.minHeight, SS.maxHeight ) /******* Text Styling *******/ interface TextDecorationProps { textDecoration?: SS.ResponsiveValue textDecorationColor?: SS.ResponsiveValue textDecorationLine?: SS.ResponsiveValue textDecorationSkipInk?: SS.ResponsiveValue textDecorationStyle?: SS.ResponsiveValue textDecorationThickness?: SS.ResponsiveValue> textUnderlineOffset?: SS.ResponsiveValue> textUnderlinePosition?: SS.ResponsiveValue } const textDecoration = SS.system({ textDecoration: true, textDecorationColor: { property: 'textDecorationColor', scale: 'colors' }, textDecorationLine: true, textDecorationSkipInk: true, textDecorationStyle: true, textDecorationThickness: { property: 'textDecorationThickness', scale: 'space' }, textUnderlineOffset: { property: 'textUnderlineOffset', scale: 'space' }, textUnderlinePosition: true, }) interface TextDirectionProps { textOrientation?: SS.ResponsiveValue writingMode?: SS.ResponsiveValue } const textDirection = SS.system({ textOrientation: true, writingMode: true, }) interface TextDisplayProps { fontStretch?: SS.ResponsiveValue fontVariant?: SS.ResponsiveValue fontVariantCaps?: SS.ResponsiveValue fontVariantLigatures?: SS.ResponsiveValue fontVariantNumeric?: SS.ResponsiveValue textIndent?: SS.ResponsiveValue> textTransform?: SS.ResponsiveValue } const textDisplay = SS.system({ fontStretch: true, fontVariant: true, fontVariantCaps: true, fontVariantLigatures: true, fontVariantNumeric: true, textIndent: { property: 'textIndent', scale: 'space', // Unlike most other space props, it makes sense to allow negative numbers. transform: (value, scale) => { if (scale) { if (typeof value === 'number' && value < 0) { const scaleVal = scale[-value] if (typeof scaleVal === 'number') { return -scaleVal } else if (scaleVal) { return `-${scaleVal}` } } return scale[value] ?? value } return value }, }, textTransform: true, }) interface TextOverflowProps extends SS.OverflowProps { hyphens?: SS.ResponsiveValue overflowWrap?: SS.ResponsiveValue textOverflow?: SS.ResponsiveValue whiteSpace?: SS.ResponsiveValue wordBreak?: SS.ResponsiveValue } const textOverflow = SS.compose( SS.overflow, SS.overflowX, SS.overflowY, SS.system({ hyphens: true, overflowWrap: { properties: ['overflowWrap', 'wordWrap'] }, textOverflow: true, whiteSpace: true, wordBreak: true, }) ) interface UserSelectProps { userSelect?: SS.ResponsiveValue } // This one doesn't really fit anywhere else... const userSelect = SS.system({ userSelect: true }) // Other text-related props I'm not including: // `textJustify`: lack of browser support // `fontVariantAlternates`: lack of browser support // `fontVariantPosition`: lack of browser support // `fontVariantAlternates`: lack of browser support // `fontVariationSettings`: common use cases covered by other properties // `textShadow`: requires custom transform to utilize themed shadows, wait for use case to add export const allText = SS.compose( SS.color, SS.typography, textDecoration, textDirection, textDisplay, textOverflow, userSelect ) export type AllTextProps = SS.ColorProps & SS.TypographyProps & TextDecorationProps & TextDirectionProps & TextDisplayProps & TextOverflowProps & UserSelectProps