From 7787f0bb16203732be1681a7b6c31e96133aea5a Mon Sep 17 00:00:00 2001 From: Johannes Merz Date: Wed, 11 Dec 2024 15:55:06 +0100 Subject: [PATCH 1/2] fix: add Link to index (#84) --- src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index ea19f4d..9579faf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,12 +7,13 @@ export * from './ButtonBar'; export * from './ButtonBase'; export * from './Card'; export * from './Checkbox'; +export * from './ClickOutside'; export * from './IconButton'; export * from './Image'; export * from './Input'; -export * from './ClickOutside'; export * from './FlexGrid'; export * from './Hide'; +export * from './Link'; export * from './Menu'; export * from './Modal'; export * from './NativeSelect'; From 40ce4b43ea6227fd19b1c2298b79fb52869d917a Mon Sep 17 00:00:00 2001 From: Simon Jentsch Date: Sat, 21 Dec 2024 15:55:57 +0100 Subject: [PATCH 2/2] feat: Move from styled-system to own solution (#86) --- package.json | 10 +- site/docs/Customization.mdx | 8 +- site/docs/components/Box.mdx | 2 +- site/docs/components/Image.mdx | 2 +- site/src/components/PropsTable.tsx | 10 +- site/src/pages/index.mdx | 8 +- src/Box/Box.tsx | 46 +--- src/Box/Flex.tsx | 30 +-- src/Box/color.ts | 24 -- src/Box/interpolations/color.spec.ts | 49 ++++ src/Box/interpolations/color.ts | 25 ++ src/Box/interpolations/flex.spec.ts | 128 +++++++++ src/Box/interpolations/flex.ts | 80 ++++++ src/Box/interpolations/layout.spec.ts | 115 +++++++++ src/Box/interpolations/layout.ts | 55 ++++ src/Box/interpolations/position.spec.ts | 58 +++++ src/Box/interpolations/position.ts | 46 ++++ src/Box/interpolations/spacing.spec.ts | 104 ++++++++ src/Box/interpolations/spacing.ts | 85 ++++++ src/Box/interpolations/svg.spec.ts | 34 +++ src/Box/interpolations/svg.ts | 20 ++ src/Box/interpolations/typography.ts | 35 +++ src/Box/spacingInterpolation.ts | 98 ------- src/Box/system.spec.ts | 140 ++++++++++ src/Box/system.ts | 242 ++++++++++++++++++ src/Button/Button.stories.tsx | 2 +- .../__snapshots__/Checkbox.spec.tsx.snap | 17 +- src/FlexGrid/FlexGrid.stories.tsx | 16 +- src/FlexGrid/FlexGrid.tsx | 52 ++-- src/Menu/MenuItem.tsx | 7 +- src/Modal/__snapshots__/Modal.spec.tsx.snap | 18 +- src/Radio/__snapshots__/Radio.spec.tsx.snap | 17 +- .../__snapshots__/RadioGroup.spec.tsx.snap | 18 +- src/Slider/Slider.spec.tsx | 7 +- src/Slider/__snapshots__/Slider.spec.tsx.snap | 148 ++++++----- src/Switch/__snapshots__/Switch.spec.tsx.snap | 17 +- src/Tabs/Tabs.tsx | 2 +- src/Tabs/__snapshots__/Tabs.spec.tsx.snap | 36 +-- src/ToastCard/ToastCard.tsx | 11 +- .../__snapshots__/ToastCard.spec.tsx.snap | 80 +++--- src/ToastProvider/ToastStack.tsx | 5 +- src/Typography/Typography.tsx | 5 +- src/animation/animation.stories.tsx | 16 +- src/breakpoints/mediaQueryFns.spec.ts | 32 ++- src/breakpoints/mediaQueryFns.ts | 35 +-- src/breakpoints/useBreakpoint.ts | 8 +- src/shared/BaseInput.tsx | 4 +- src/styleHelpers/breakpoint.spec.tsx | 43 ++-- src/styleHelpers/breakpoint.ts | 9 +- src/styleHelpers/getSpacing.ts | 4 +- src/styleHelpers/styleProp.ts | 9 +- src/theme/PabloThemeProvider.tsx | 8 +- src/theme/breakpoints.ts | 44 ++-- src/theme/themeVars.ts | 5 + src/types.ts | 42 +++ src/utils/enforceArray.ts | 4 +- src/utils/getByPath.ts | 13 +- src/utils/isNumber.ts | 5 + yarn.lock | 121 +-------- 59 files changed, 1650 insertions(+), 664 deletions(-) delete mode 100644 src/Box/color.ts create mode 100644 src/Box/interpolations/color.spec.ts create mode 100644 src/Box/interpolations/color.ts create mode 100644 src/Box/interpolations/flex.spec.ts create mode 100644 src/Box/interpolations/flex.ts create mode 100644 src/Box/interpolations/layout.spec.ts create mode 100644 src/Box/interpolations/layout.ts create mode 100644 src/Box/interpolations/position.spec.ts create mode 100644 src/Box/interpolations/position.ts create mode 100644 src/Box/interpolations/spacing.spec.ts create mode 100644 src/Box/interpolations/spacing.ts create mode 100644 src/Box/interpolations/svg.spec.ts create mode 100644 src/Box/interpolations/svg.ts create mode 100644 src/Box/interpolations/typography.ts delete mode 100644 src/Box/spacingInterpolation.ts create mode 100644 src/Box/system.spec.ts create mode 100644 src/Box/system.ts create mode 100644 src/utils/isNumber.ts diff --git a/package.json b/package.json index 177d3b5..065b66f 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,6 @@ "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@types/requestidlecallback": "^0.3.1", - "@types/styled-system": "^5.1.10", "@typescript-eslint/eslint-plugin": "^8.15.0", "@typescript-eslint/eslint-plugin-tslint": "^7.0.2", "@typescript-eslint/parser": "4.10.0", @@ -92,9 +91,9 @@ "rollup-plugin-terser": "^7.0.2", "size-limit": "^11.1.6", "storybook": "^8.4.4", - "styled-system": "^5.1.5", "ts-jest": "^26.4.4", "ts-node": "^9.1.1", + "ts-toolbelt": "^9.6.0", "tslint": "6.1.3", "tslint-plugin-prettier": "2.3.0", "typescript": "^5.6.3", @@ -105,13 +104,6 @@ }, "dependencies": { "@react-hook/media-query": "^1.1.1", - "@styled-system/border": "^5.1.5", - "@styled-system/core": "^5.1.2", - "@styled-system/flexbox": "^5.1.2", - "@styled-system/layout": "^5.1.2", - "@styled-system/position": "^5.1.2", - "@styled-system/space": "^5.1.2", - "@styled-system/typography": "^5.1.2", "deepmerge": "^4.2.2", "isobject": "^4.0.0", "nanopop": "^2.4.2", diff --git a/site/docs/Customization.mdx b/site/docs/Customization.mdx index d9aadc3..ff9c691 100644 --- a/site/docs/Customization.mdx +++ b/site/docs/Customization.mdx @@ -3,8 +3,8 @@ import { PropsTable } from '@site/src/components/PropsTable'; # Customization Pablo is highly customizable with the principle of making customizations as easy as possible and -reduce boilerplate code. As Pablo is built on top of `emotion` and `styled-system` it -utilizes both libraries to achive this. +reduce boilerplate code. As Pablo is built on top of `emotion`. It uses a custom styled system implementation +to be able to customize styles via props. ## Ways of customization @@ -15,7 +15,7 @@ to inconsistent design and code quality. There are five different ways you can customize components: -* Overwrite single styling properties such as padding or margin with `styled-system` props +* Overwrite single styling properties such as padding or margin with styled system props * Update the theme values to update primitives like color or typographic values globally * Change the `componentStyles` config to update component specific values such as gaps, colors or sizes * Overwrite styling of parts of components globally with styled-component's `css` tag literal @@ -23,7 +23,7 @@ There are five different ways you can customize components: ## Use styling props -The quickest way to overwrite certain styles are the styling props that `styled-system` provides. +The quickest way to overwrite certain styles is to use the styled system props. Most of the components of Pablo support these props which mainly should be used for layouting purposes like adding margins (the `mx` and `my` props are quite useful for this). But you can also use them to change color on more plain components like ``. diff --git a/site/docs/components/Box.mdx b/site/docs/components/Box.mdx index 07fc80e..958d7c6 100644 --- a/site/docs/components/Box.mdx +++ b/site/docs/components/Box.mdx @@ -2,7 +2,7 @@ import { PropsTable } from '@site/src/components/PropsTable'; # Box -The `Box` component is one of the fundamentals of Pablo. It exposes the style props from `styled-system` +The `Box` component is one of the fundamentals of Pablo. It exposes styled system like props and we also partially use it within our components. The main purpose of the `Box` component is to build layouts without the need to create separate diff --git a/site/docs/components/Image.mdx b/site/docs/components/Image.mdx index 7fbc0f8..3d5a987 100644 --- a/site/docs/components/Image.mdx +++ b/site/docs/components/Image.mdx @@ -3,7 +3,7 @@ import { PropsTable } from '@site/src/components/PropsTable'; # Image The `Image` component is a plain proxy of the normal HTML `img` element but it adds support for the -styled-system layouting props (like margins, paddings, positioning, etc.). +styled system layouting props (like margins, paddings, positioning, etc.). ## Import diff --git a/site/src/components/PropsTable.tsx b/site/src/components/PropsTable.tsx index f456187..bb5e2d1 100644 --- a/site/src/components/PropsTable.tsx +++ b/site/src/components/PropsTable.tsx @@ -3,15 +3,9 @@ import { useDynamicImport } from 'docusaurus-plugin-react-docgen-typescript/useD const getFilterFn = (type) => { switch (type) { case 'direct': - return (prop) => - !prop.parent?.fileName.includes('styled-system') && - !prop.parent?.fileName.includes('spacingInterpolation.ts') && - !prop.parent?.fileName.includes('Box/color.ts'); + return (prop) => !prop.parent?.fileName.includes('interpolations'); case 'box': - return (prop) => - prop.parent?.fileName.includes('styled-system') || - prop.parent?.fileName.includes('spacingInterpolation.ts') || - prop.parent?.fileName.includes('Box/color.ts'); + return (prop) => prop.parent?.fileName.includes('interpolations'); case 'all': default: return () => true; diff --git a/site/src/pages/index.mdx b/site/src/pages/index.mdx index 04046c9..a0003f8 100644 --- a/site/src/pages/index.mdx +++ b/site/src/pages/index.mdx @@ -2,7 +2,7 @@ ## What is Pablo? -Pablo is the design system of Bojagi built on top of `emotion` and `styled-system`. +Pablo is the design system of Bojagi built on top of `emotion`. It's build on these three core principles: @@ -21,8 +21,8 @@ const [name, setName] = useState(''); ``` -Every component of Pablo is also extending most of the `Box` component props, which is built on -top of `styled-system`. This way there is no need for wrapping divs only for the sake of layouting. +Every component of Pablo is also extending most of the `Box` component props, which implements our +own styled system implementation. This way there is no need for wrapping divs only for the sake of layouting. ```jsx @@ -39,7 +39,7 @@ extend. There are different ways you can customize: * Update the theme values to update primitives like color or typographic values globally * Change the `componentStyles` config to update component specific values such as gaps, colors or sizes * Overwrite styling of parts of components globally with styled-component's `css` tag literal -* Overwrite single styling properties such as padding or margin with `styled-system` props +* Overwrite single styling properties such as padding or margin with styled system props * Overwrite styling of parts of components in place with the `customStyles` prop Get a full insight into customization in the [Customization Section](/docs/Customization). diff --git a/src/Box/Box.tsx b/src/Box/Box.tsx index 6f938fc..f8ec9c1 100644 --- a/src/Box/Box.tsx +++ b/src/Box/Box.tsx @@ -1,70 +1,46 @@ import styled from '@emotion/styled'; -import { layout } from '@styled-system/layout'; -import { flexbox } from '@styled-system/flexbox'; -import { position } from '@styled-system/position'; -import type { LayoutProps, FlexboxProps, PositionProps } from 'styled-system'; -import { system } from '@styled-system/core'; - -import { color, ColorProps } from './color'; +import { color, ColorProps } from './interpolations/color'; import { CssFunctionReturn } from '../types'; import { baseStyle } from '../shared/baseStyle'; -import { getByPath } from '../utils/getByPath'; -import { themeVars } from '../theme/themeVars'; import { interpolateCssProp } from '../utils/interpolateCssProp'; -import { margin, MarginProps, PaddingProps, padding } from './spacingInterpolation'; -import { ifProp } from '../styleHelpers/styleProp'; +import { margin, MarginProps, PaddingProps, padding } from './interpolations/spacing'; +import { layout, LayoutProps } from './interpolations/layout'; +import { svg, SvgProps } from './interpolations/svg'; +import { position, PositionProps } from './interpolations/position'; +import { flexItem, FlexItemProps } from './interpolations/flex'; export interface BoxCssProps { css?: CssFunctionReturn; } -export interface BoxFillableProps { - fillColor?: string; -} - -export interface BoxFlexProps extends FlexboxProps { - grow?: number | boolean; - shrink?: number | boolean; -} - export type BoxProps = MarginProps & PaddingProps & ColorProps & LayoutProps & - BoxFlexProps & + FlexItemProps & PositionProps & - BoxFillableProps & + SvgProps & BoxCssProps; -const flexGrow = ifProp('grow', (_, value) => `flex-grow: ${value};`); -const flexShrink = ifProp('shrink', (_, value) => `flex-shrink: ${value};`); export const boxInterpolateFn = (props) => - [margin, padding, color, layout, flexbox, position, flexGrow, flexShrink].map((fn) => fn(props)); - -const fill = system({ - fillColor: { - property: 'fill', - transform: (value: string) => getByPath(themeVars.colors, value), - }, -}); + [margin, padding, color, layout, svg, position, flexItem].map((fn) => fn(props)); export const Box = styled.div` ${baseStyle} ${interpolateCssProp} - ${fill} ${(props) => props.css} ${boxInterpolateFn} `; export type LayoutBoxProps = MarginProps & PaddingProps & - BoxFlexProps & + FlexItemProps & LayoutProps & PositionProps & BoxCssProps; export const layoutInterpolationFn = (props) => - [margin, padding, layout, flexbox, position, flexGrow, flexShrink] + [margin, padding, layout, position, flexItem] .map((fn) => fn(props)) .reduce((acc, styles) => ({ ...acc, ...styles }), {}); diff --git a/src/Box/Flex.tsx b/src/Box/Flex.tsx index 0b0adb3..8d6498c 100644 --- a/src/Box/Flex.tsx +++ b/src/Box/Flex.tsx @@ -1,27 +1,25 @@ -import type * as CSS from 'csstype'; import { ifProp } from '../styleHelpers/styleProp'; import { Box, type BoxProps } from './Box'; import styled from '@emotion/styled'; +import { flexContainer, FlexContainerProps } from './interpolations/flex'; -export type FlexProps = BoxProps & { - center?: boolean; - equal?: boolean; - end?: boolean; - start?: boolean; - between?: boolean; - stretch?: boolean; - direction?: CSS.Property.FlexDirection; -}; - -const justifyContent = (where: CSS.Property.JustifyContent) => `justify-content: ${where};`; +export type FlexProps = BoxProps & + FlexContainerProps & { + center?: boolean; + equal?: boolean; + end?: boolean; + start?: boolean; + between?: boolean; + stretch?: boolean; + }; export const Flex = styled(Box)` display: flex; + ${flexContainer} ${ifProp('center', 'justify-content: center; align-items: center;')} ${ifProp('equal', '> * { flex-basis: 100%; flex-grow: 1; flex-shrink: 1; }')} - ${ifProp('between', justifyContent('space-between'))} - ${ifProp('end', justifyContent('flex-end'))} - ${ifProp('start', justifyContent('flex-start'))} ${ifProp('stretch', 'align-items: stretch;')} - ${ifProp('direction', (_, value) => `flex-direction: ${value};`)} + ${ifProp('between', flexContainer.justifyContent('space-between'))} + ${ifProp('end', flexContainer.justifyContent('flex-end'))} + ${ifProp('start', flexContainer.justifyContent('flex-start'))} `; diff --git a/src/Box/color.ts b/src/Box/color.ts deleted file mode 100644 index 3527f45..0000000 --- a/src/Box/color.ts +++ /dev/null @@ -1,24 +0,0 @@ -// Only types - -import type { TextColorProps, BackgroundColorProps } from 'styled-system'; -import { system } from '@styled-system/core'; -import { themeVars } from '../theme/themeVars'; -import { getByPath } from '../utils/getByPath'; - -export interface ColorProps { - bgColor?: BackgroundColorProps['backgroundColor']; - textColor?: TextColorProps['color']; - opacity?: number; -} - -export const color = system({ - textColor: { - property: 'color', - transform: (value: string) => getByPath(themeVars.colors, value) || value, - }, - bgColor: { - property: 'backgroundColor', - transform: (value: string) => getByPath(themeVars.colors, value) || value, - }, - opacity: true, -}); diff --git a/src/Box/interpolations/color.spec.ts b/src/Box/interpolations/color.spec.ts new file mode 100644 index 0000000..6d366d7 --- /dev/null +++ b/src/Box/interpolations/color.spec.ts @@ -0,0 +1,49 @@ +import { defaultTheme } from '../../theme'; +import { PabloThemeableProps } from '../../theme/types'; +import { color } from './color'; + +let props: PabloThemeableProps = { + theme: defaultTheme, +} as any; + +beforeEach(() => { + props = { + theme: defaultTheme, + } as any; +}); + +test('bg color', () => { + expect(color({ ...props, bgColor: 'brand.main' })).toEqual({ + backgroundColor: 'var(--pbl-theme-colors-brand-main)', + }); +}); + +test('text color', () => { + expect(color({ ...props, textColor: 'brand.main' })).toEqual({ + color: 'var(--pbl-theme-colors-brand-main)', + }); +}); + +test('opacity', () => { + expect(color({ ...props, opacity: 0.5 })).toEqual({ + opacity: 0.5, + }); + expect(color({ ...props, opacity: '0.4' })).toEqual({ + opacity: '0.4', + }); +}); + +test('styled interpolation functions', () => { + expect(color.bgColor('brand.main')(props)).toEqual({ + backgroundColor: 'var(--pbl-theme-colors-brand-main)', + }); + expect(color.textColor('brand.main')(props)).toEqual({ + color: 'var(--pbl-theme-colors-brand-main)', + }); + expect(color.opacity(0.5)(props)).toEqual({ + opacity: 0.5, + }); + expect(color.opacity('0.4')(props)).toEqual({ + opacity: '0.4', + }); +}); diff --git a/src/Box/interpolations/color.ts b/src/Box/interpolations/color.ts new file mode 100644 index 0000000..ec5f884 --- /dev/null +++ b/src/Box/interpolations/color.ts @@ -0,0 +1,25 @@ +import { colorTransform, ResponsiveValue, system } from '../system'; +import { CssColor, KeyMap } from '../../types'; +import { Colors } from '../../theme/colors'; + +export interface ColorProps { + bgColor?: ResponsiveValue | CssColor>; + textColor?: ResponsiveValue | CssColor>; + opacity?: ResponsiveValue; +} + +export const color = system([ + { + properties: ['color'], + fromProps: ['textColor'], + transform: colorTransform, + }, + { + properties: ['backgroundColor'], + fromProps: ['bgColor'], + transform: colorTransform, + }, + { + properties: ['opacity'], + }, +]); diff --git a/src/Box/interpolations/flex.spec.ts b/src/Box/interpolations/flex.spec.ts new file mode 100644 index 0000000..399081c --- /dev/null +++ b/src/Box/interpolations/flex.spec.ts @@ -0,0 +1,128 @@ +import { defaultTheme } from '../../theme'; +import { PabloThemeableProps } from '../../theme/types'; +import { flexItem, flexContainer } from './flex'; + +let props: PabloThemeableProps = { + theme: defaultTheme, +} as any; + +beforeEach(() => { + props = { + theme: defaultTheme, + } as any; +}); + +describe('flexItem', () => { + test('flex', () => { + expect(flexItem({ ...props, flex: 1 })).toEqual({ + flex: 1, + }); + }); + + test('flexGrow', () => { + expect(flexItem({ ...props, grow: 1 })).toEqual({ + flexGrow: 1, + }); + }); + + test('flexShrink', () => { + expect(flexItem({ ...props, shrink: 1 })).toEqual({ + flexShrink: 1, + }); + }); + + test('flexBasis', () => { + expect(flexItem({ ...props, flexBasis: 'auto' })).toEqual({ + flexBasis: 'auto', + }); + }); + + test('justifySelf', () => { + expect(flexItem({ ...props, justifySelf: 'center' })).toEqual({ + justifySelf: 'center', + }); + }); + + test('alignSelf', () => { + expect(flexItem({ ...props, alignSelf: 'center' })).toEqual({ + alignSelf: 'center', + }); + }); + + test('order', () => { + expect(flexItem({ ...props, order: 1 })).toEqual({ + order: 1, + }); + }); + + test('styled interpolation functions', () => { + expect(flexItem.grow(1)(props)).toEqual({ + flexGrow: 1, + }); + expect(flexItem.shrink(1)(props)).toEqual({ + flexShrink: 1, + }); + expect(flexItem.basis('auto')(props)).toEqual({ + flexBasis: 'auto', + }); + expect(flexItem.justifySelf('center')(props)).toEqual({ + justifySelf: 'center', + }); + expect(flexItem.alignSelf('center')(props)).toEqual({ + alignSelf: 'center', + }); + expect(flexItem.order(1)(props)).toEqual({ + order: 1, + }); + }); +}); + +describe('flexContainer', () => { + test('flexDirection', () => { + expect(flexContainer({ ...props, direction: 'column' })).toEqual({ + flexDirection: 'column', + }); + }); + + test('flexWrap', () => { + expect(flexContainer({ ...props, wrap: 'wrap' })).toEqual({ + flexWrap: 'wrap', + }); + }); + + test('justifyContent', () => { + expect(flexContainer({ ...props, justifyContent: 'center' })).toEqual({ + justifyContent: 'center', + }); + }); + + test('alignItems', () => { + expect(flexContainer({ ...props, alignItems: 'center' })).toEqual({ + alignItems: 'center', + }); + }); + + test('alignContent', () => { + expect(flexContainer({ ...props, alignContent: 'center' })).toEqual({ + alignContent: 'center', + }); + }); + + test('styled interpolation functions', () => { + expect(flexContainer.direction('column')(props)).toEqual({ + flexDirection: 'column', + }); + expect(flexContainer.wrap('wrap')(props)).toEqual({ + flexWrap: 'wrap', + }); + expect(flexContainer.justifyContent('center')(props)).toEqual({ + justifyContent: 'center', + }); + expect(flexContainer.alignItems('center')(props)).toEqual({ + alignItems: 'center', + }); + expect(flexContainer.alignContent('center')(props)).toEqual({ + alignContent: 'center', + }); + }); +}); diff --git a/src/Box/interpolations/flex.ts b/src/Box/interpolations/flex.ts new file mode 100644 index 0000000..503d771 --- /dev/null +++ b/src/Box/interpolations/flex.ts @@ -0,0 +1,80 @@ +import type * as CSS from 'csstype'; +import { identityTransform, InterpolationTransformFn, ResponsiveValue, system } from '../system'; + +export interface FlexItemProps { + grow?: ResponsiveValue; + shrink?: ResponsiveValue; + flexBasis?: ResponsiveValue; + flex?: ResponsiveValue; + justifySelf?: ResponsiveValue; + alignSelf?: ResponsiveValue; + order?: ResponsiveValue; +} + +export interface FlexContainerProps { + direction?: ResponsiveValue; + wrap?: ResponsiveValue; + justifyContent?: ResponsiveValue; + alignItems?: ResponsiveValue; + alignContent?: ResponsiveValue; +} + +const shrinkGrowTransform: InterpolationTransformFn = ( + value +) => (value === true ? 1 : value === false ? 0 : value); + +export const flexItem = system([ + { + properties: ['flexGrow'], + fromProps: ['grow', 'flexGrow'], + transform: shrinkGrowTransform, + }, + { + properties: ['flexShrink'], + fromProps: ['shrink', 'flexShrink'], + transform: shrinkGrowTransform, + }, + { + properties: ['flexBasis'], + as: 'basis', + }, + { + properties: ['flex'], + }, + { + properties: ['justifySelf'], + transform: identityTransform as InterpolationTransformFn, + }, + { + properties: ['alignSelf'], + transform: identityTransform as InterpolationTransformFn, + }, + { + properties: ['order'], + }, +]); + +export const flexContainer = system([ + { + properties: ['flexWrap'], + fromProps: ['wrap'], + transform: identityTransform as InterpolationTransformFn, + }, + { + properties: ['flexDirection'], + fromProps: ['direction'], + transform: identityTransform as InterpolationTransformFn, + }, + { + properties: ['justifyContent'], + transform: identityTransform as InterpolationTransformFn, + }, + { + properties: ['alignItems'], + transform: identityTransform as InterpolationTransformFn, + }, + { + properties: ['alignContent'], + transform: identityTransform as InterpolationTransformFn, + }, +]); diff --git a/src/Box/interpolations/layout.spec.ts b/src/Box/interpolations/layout.spec.ts new file mode 100644 index 0000000..b8fe7fe --- /dev/null +++ b/src/Box/interpolations/layout.spec.ts @@ -0,0 +1,115 @@ +import { defaultTheme } from '../../theme'; +import { PabloThemeableProps } from '../../theme/types'; +import { layout } from './layout'; + +let props: PabloThemeableProps = { + theme: defaultTheme, +} as any; + +beforeEach(() => { + props = { + theme: defaultTheme, + } as any; +}); + +test('width', () => { + expect(layout({ ...props, width: 100 })).toEqual({ + width: '100px', + }); + expect(layout({ ...props, width: '100%' })).toEqual({ + width: '100%', + }); + expect(layout({ ...props, width: 0.9 })).toEqual({ + width: '90%', + }); +}); + +test('height', () => { + expect(layout({ ...props, height: 100 })).toEqual({ + height: '100px', + }); + expect(layout({ ...props, height: '100%' })).toEqual({ + height: '100%', + }); +}); + +test('minWidth', () => { + expect(layout({ ...props, minWidth: 100 })).toEqual({ + minWidth: '100px', + }); + expect(layout({ ...props, minWidth: '100%' })).toEqual({ + minWidth: '100%', + }); +}); + +test('minHeight', () => { + expect(layout({ ...props, minHeight: 100 })).toEqual({ + minHeight: '100px', + }); + expect(layout({ ...props, minHeight: '100%' })).toEqual({ + minHeight: '100%', + }); +}); + +test('maxWidth', () => { + expect(layout({ ...props, maxWidth: 100 })).toEqual({ + maxWidth: '100px', + }); + expect(layout({ ...props, maxWidth: '100%' })).toEqual({ + maxWidth: '100%', + }); +}); + +test('maxHeight', () => { + expect(layout({ ...props, maxHeight: 100 })).toEqual({ + maxHeight: '100px', + }); + expect(layout({ ...props, maxHeight: '100%' })).toEqual({ + maxHeight: '100%', + }); +}); + +test('squareSize', () => { + expect(layout({ ...props, squareSize: 100 })).toEqual({ + width: '100px', + height: '100px', + }); + expect(layout({ ...props, squareSize: '100%' })).toEqual({ + width: '100%', + height: '100%', + }); +}); + +test('display', () => { + expect(layout({ ...props, display: 'flex' })).toEqual({ + display: 'flex', + }); +}); + +test('styled interpolation functions', () => { + expect(layout.width(100)(props)).toEqual({ + width: '100px', + }); + expect(layout.height(100)(props)).toEqual({ + height: '100px', + }); + expect(layout.minWidth(100)(props)).toEqual({ + minWidth: '100px', + }); + expect(layout.minHeight(100)(props)).toEqual({ + minHeight: '100px', + }); + expect(layout.maxWidth(100)(props)).toEqual({ + maxWidth: '100px', + }); + expect(layout.maxHeight(100)(props)).toEqual({ + maxHeight: '100px', + }); + expect(layout.squareSize(100)(props)).toEqual({ + width: '100px', + height: '100px', + }); + expect(layout.display('flex')(props)).toEqual({ + display: 'flex', + }); +}); diff --git a/src/Box/interpolations/layout.ts b/src/Box/interpolations/layout.ts new file mode 100644 index 0000000..fb57dc3 --- /dev/null +++ b/src/Box/interpolations/layout.ts @@ -0,0 +1,55 @@ +import type * as CSS from 'csstype'; +import { pixelTransform, ResponsiveValue, system } from '../system'; +import { PabloTheme } from '../../theme/types'; +import { isNumber } from '../../utils/isNumber'; + +interface LayoutProps { + width?: ResponsiveValue; + height?: ResponsiveValue; + minWidth?: ResponsiveValue; + minHeight?: ResponsiveValue; + maxWidth?: ResponsiveValue; + maxHeight?: ResponsiveValue; + squareSize?: ResponsiveValue; + display?: ResponsiveValue; +} + +const widthTransform = (value: number | string, theme: PabloTheme) => + isNumber(value) && value <= 1 ? `${value * 100}%` : pixelTransform(value, theme); + +const layout = system([ + { + properties: ['width'], + transform: widthTransform, + }, + { + properties: ['height'], + transform: pixelTransform, + }, + { + properties: ['minWidth'], + transform: pixelTransform, + }, + { + properties: ['minHeight'], + transform: pixelTransform, + }, + { + properties: ['maxWidth'], + transform: pixelTransform, + }, + { + properties: ['maxHeight'], + transform: pixelTransform, + }, + { + properties: ['width', 'height'], + fromProps: ['squareSize'], + transform: pixelTransform, + }, + { + properties: ['display'], + }, +]); + +export { layout, widthTransform, type LayoutProps }; diff --git a/src/Box/interpolations/position.spec.ts b/src/Box/interpolations/position.spec.ts new file mode 100644 index 0000000..8865ea3 --- /dev/null +++ b/src/Box/interpolations/position.spec.ts @@ -0,0 +1,58 @@ +import { defaultTheme } from '../../theme'; +import { PabloThemeableProps } from '../../theme/types'; +import { position } from './position'; + +let props: PabloThemeableProps = { + theme: defaultTheme, +} as any; + +beforeEach(() => { + props = { + theme: defaultTheme, + } as any; +}); + +test('position', () => { + expect(position({ ...props, position: 'absolute' })).toEqual({ + position: 'absolute', + }); + expect(position({ ...props, pos: 'absolute' })).toEqual({ + position: 'absolute', + }); +}); + +test('top', () => { + expect(position({ ...props, top: 2 })).toEqual({ + top: '16px', + }); + expect(position({ ...props, top: '100%' })).toEqual({ + top: '100%', + }); +}); + +test('right', () => { + expect(position({ ...props, right: 2 })).toEqual({ + right: '16px', + }); + expect(position({ ...props, right: '100%' })).toEqual({ + right: '100%', + }); +}); + +test('bottom', () => { + expect(position({ ...props, bottom: 2 })).toEqual({ + bottom: '16px', + }); + expect(position({ ...props, bottom: '100%' })).toEqual({ + bottom: '100%', + }); +}); + +test('left', () => { + expect(position({ ...props, left: 2 })).toEqual({ + left: '16px', + }); + expect(position({ ...props, left: '100%' })).toEqual({ + left: '100%', + }); +}); diff --git a/src/Box/interpolations/position.ts b/src/Box/interpolations/position.ts new file mode 100644 index 0000000..4c63293 --- /dev/null +++ b/src/Box/interpolations/position.ts @@ -0,0 +1,46 @@ +import type * as CSS from 'csstype'; +import { + identityTransform, + InterpolationTransformFn, + ResponsiveValue, + spacingTransform, + system, +} from '../system'; + +export interface PositionProps { + position?: ResponsiveValue; + pos?: ResponsiveValue; + zIndex?: ResponsiveValue; + top?: ResponsiveValue; + right?: ResponsiveValue; + bottom?: ResponsiveValue; + left?: ResponsiveValue; +} + +export const position = system([ + { + properties: ['position'], + fromProps: ['position', 'pos'], + transform: identityTransform as InterpolationTransformFn, + as: 'type', + }, + { + properties: ['zIndex'], + }, + { + properties: ['top'], + transform: spacingTransform, + }, + { + properties: ['right'], + transform: spacingTransform, + }, + { + properties: ['bottom'], + transform: spacingTransform, + }, + { + properties: ['left'], + transform: spacingTransform, + }, +]); diff --git a/src/Box/interpolations/spacing.spec.ts b/src/Box/interpolations/spacing.spec.ts new file mode 100644 index 0000000..4b00961 --- /dev/null +++ b/src/Box/interpolations/spacing.spec.ts @@ -0,0 +1,104 @@ +import { defaultTheme } from '../../theme'; +import { PabloThemeableProps } from '../../theme/types'; +import { margin, padding } from './spacing'; + +let props: PabloThemeableProps = { + theme: defaultTheme, +} as any; + +beforeEach(() => { + props = { + theme: defaultTheme, + } as any; +}); + +test('Margin system', () => { + expect(margin({ m: 10, ...props })).toEqual({ + margin: '80px', + }); +}); + +test('Margin system single props', () => { + expect(margin({ mt: 1, mr: 2, mb: 3, ml: 4, ...props })).toEqual({ + marginTop: '8px', + marginRight: '16px', + marginBottom: '24px', + marginLeft: '32px', + }); +}); + +test('Margin system with x and y props', () => { + expect(margin({ mx: 10, my: 20, ...props })).toEqual({ + marginLeft: '80px', + marginRight: '80px', + marginTop: '160px', + marginBottom: '160px', + }); +}); + +test('Padding system', () => { + expect(padding({ p: 10, ...props })).toEqual({ + padding: '80px', + }); +}); + +test('Padding system single props', () => { + expect(padding({ pt: 1, pr: 2, pb: 3, pl: 4, ...props })).toEqual({ + paddingTop: '8px', + paddingRight: '16px', + paddingBottom: '24px', + paddingLeft: '32px', + }); +}); + +test('Padding system with x and y props', () => { + expect(padding({ px: 10, py: 20, ...props })).toEqual({ + paddingLeft: '80px', + paddingRight: '80px', + paddingTop: '160px', + paddingBottom: '160px', + }); +}); + +test('Padding system with gap', () => { + expect(padding({ gap: 10, ...props })).toEqual({ + gap: '80px 80px', + }); + expect(padding({ gap: [[10, 1]], ...props })).toEqual({ + gap: '80px 8px', + }); +}); + +test('styled interpolation functions', () => { + expect(margin.all(10)(props)).toEqual({ + margin: '80px', + }); + expect(margin.top(1)(props)).toEqual({ + marginTop: '8px', + }); + expect(margin.x(10)(props)).toEqual({ + marginLeft: '80px', + marginRight: '80px', + }); + expect(margin.y(10)(props)).toEqual({ + marginTop: '80px', + marginBottom: '80px', + }); + expect(padding.all(10)(props)).toEqual({ + padding: '80px', + }); + expect(padding.top(1)(props)).toEqual({ + paddingTop: '8px', + }); + expect(padding.x(10)(props)).toEqual({ + paddingLeft: '80px', + paddingRight: '80px', + }); + expect(padding.y(10)(props)).toEqual({ + paddingTop: '80px', + paddingBottom: '80px', + }); + expect(padding.gap(10)(props)).toEqual({ + gap: '80px 80px', + }); +}); diff --git a/src/Box/interpolations/spacing.ts b/src/Box/interpolations/spacing.ts new file mode 100644 index 0000000..3b5dee1 --- /dev/null +++ b/src/Box/interpolations/spacing.ts @@ -0,0 +1,85 @@ +import { PabloTheme } from '../../theme/types'; +import { ResponsiveValue, spacingTransform, system } from '../system'; + +const getGapSpacing = (value: any, theme: PabloTheme) => { + if (Array.isArray(value)) { + return value.map((val) => spacingTransform(val, theme)).join(' '); + } + const spacing = spacingTransform(value, theme); + return [spacing, spacing].join(' '); +}; + +interface MarginProps { + m?: ResponsiveValue; + mt?: ResponsiveValue; + mr?: ResponsiveValue; + mb?: ResponsiveValue; + ml?: ResponsiveValue; + mx?: ResponsiveValue; + my?: ResponsiveValue; +} + +interface PaddingProps { + p?: ResponsiveValue; + pt?: ResponsiveValue; + pr?: ResponsiveValue; + pb?: ResponsiveValue; + pl?: ResponsiveValue; + px?: ResponsiveValue; + py?: ResponsiveValue; + gap?: ResponsiveValue>; +} + +const getConfig =

(property: P, shortHand: S) => + [ + { + properties: [property], + transform: spacingTransform, + fromProps: [shortHand], + as: 'all', + }, + { + properties: [`${property}Top`], + transform: spacingTransform, + fromProps: [`${shortHand}t`], + as: 'top', + }, + { + properties: [`${property}Right`], + transform: spacingTransform, + fromProps: [`${shortHand}r`], + as: 'right', + }, + { + properties: [`${property}Bottom`], + transform: spacingTransform, + fromProps: [`${shortHand}b`], + as: 'bottom', + }, + { + properties: [`${property}Left`], + transform: spacingTransform, + fromProps: [`${shortHand}l`], + as: 'left', + }, + { + properties: [`${property}Left`, `${property}Right`], + transform: spacingTransform, + fromProps: [`${shortHand}x`], + as: 'x', + }, + { + properties: [`${property}Top`, `${property}Bottom`], + transform: spacingTransform, + fromProps: [`${shortHand}y`], + as: 'y', + }, + ] as const; + +const margin = system([...getConfig('margin', 'm')]); +const padding = system([ + ...getConfig('padding', 'p'), + { properties: ['gap'], transform: getGapSpacing }, +]); + +export { margin, padding, MarginProps, PaddingProps }; diff --git a/src/Box/interpolations/svg.spec.ts b/src/Box/interpolations/svg.spec.ts new file mode 100644 index 0000000..7c28205 --- /dev/null +++ b/src/Box/interpolations/svg.spec.ts @@ -0,0 +1,34 @@ +import { defaultTheme } from '../../theme'; +import { PabloThemeableProps } from '../../theme/types'; +import { svg } from './svg'; + +let props: PabloThemeableProps = { + theme: defaultTheme, +} as any; + +beforeEach(() => { + props = { + theme: defaultTheme, + } as any; +}); + +test('fill color', () => { + expect(svg({ ...props, fillColor: 'brand.main' })).toEqual({ + fill: 'var(--pbl-theme-colors-brand-main)', + }); +}); + +test('stroke color', () => { + expect(svg({ ...props, strokeColor: 'brand.main' })).toEqual({ + stroke: 'var(--pbl-theme-colors-brand-main)', + }); +}); + +test('styled interpolation functions', () => { + expect(svg.fillColor('brand.main')(props)).toEqual({ + fill: 'var(--pbl-theme-colors-brand-main)', + }); + expect(svg.strokeColor('brand.main')(props)).toEqual({ + stroke: 'var(--pbl-theme-colors-brand-main)', + }); +}); diff --git a/src/Box/interpolations/svg.ts b/src/Box/interpolations/svg.ts new file mode 100644 index 0000000..584ff7b --- /dev/null +++ b/src/Box/interpolations/svg.ts @@ -0,0 +1,20 @@ +import { Colors } from '../../theme/colors'; +import { CssColor, KeyMap } from '../../types'; +import { colorTransform, ResponsiveValue, system } from '../system'; + +export interface SvgProps { + fillColor?: ResponsiveValue | CssColor>; +} + +export const svg = system([ + { + properties: ['fill'], + fromProps: ['fillColor'], + transform: colorTransform, + }, + { + properties: ['stroke'], + fromProps: ['strokeColor'], + transform: colorTransform, + }, +]); diff --git a/src/Box/interpolations/typography.ts b/src/Box/interpolations/typography.ts new file mode 100644 index 0000000..197b7c6 --- /dev/null +++ b/src/Box/interpolations/typography.ts @@ -0,0 +1,35 @@ +import type * as CSS from 'csstype'; +import { ResponsiveValue, system } from '../system'; + +export interface TypographyInterpolationProps { + fontSize?: ResponsiveValue; + size?: ResponsiveValue; + lineHeight?: ResponsiveValue; + letterSpacing?: ResponsiveValue; + fontWeight?: ResponsiveValue; + align?: ResponsiveValue; + fontStyle?: ResponsiveValue; +} + +export const typography = system([ + { + properties: ['fontSize'], + fromProps: ['size', 'fontSize'], + }, + { + properties: ['lineHeight'], + }, + { + properties: ['letterSpacing'], + }, + { + properties: ['fontWeight'], + }, + { + properties: ['textAlign'], + fromProps: ['align'], + }, + { + properties: ['fontStyle'], + }, +]); diff --git a/src/Box/spacingInterpolation.ts b/src/Box/spacingInterpolation.ts deleted file mode 100644 index c65f6ce..0000000 --- a/src/Box/spacingInterpolation.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { system } from '@styled-system/core'; -import { ResponsiveValue } from 'styled-system'; - -const DEFAULT_SCALE = 8; - -const getSpacing = (value: any, scale: number) => { - if (value === undefined) { - return undefined; - } - - if (typeof value === 'string') { - return value; - } - return `${value * scale}px`; -}; - -const getGapSpacing = (value: any, scale: number) => { - if (Array.isArray(value)) { - return value.map((val) => getSpacing(val, scale)).join(' '); - } - - const spacing = getSpacing(value, scale); - return [spacing, spacing].join(' '); -}; - -interface MarginProps { - m?: ResponsiveValue; - mt?: ResponsiveValue; - mr?: ResponsiveValue; - mb?: ResponsiveValue; - ml?: ResponsiveValue; - mx?: ResponsiveValue; - my?: ResponsiveValue; -} - -interface PaddingProps { - p?: ResponsiveValue; - pt?: ResponsiveValue; - pr?: ResponsiveValue; - pb?: ResponsiveValue; - pl?: ResponsiveValue; - px?: ResponsiveValue; - py?: ResponsiveValue; - gap?: ResponsiveValue>; -} - -const getConfig = (property: string, shortHand: string) => ({ - [shortHand]: { - property, - scale: 'spacing', - transform: getSpacing, - defaultScale: DEFAULT_SCALE, - }, - [`${shortHand}t`]: { - property: `${property}Top`, - scale: 'spacing', - transform: getSpacing, - defaultScale: DEFAULT_SCALE, - }, - [`${shortHand}r`]: { - property: `${property}Right`, - scale: 'spacing', - transform: getSpacing, - defaultScale: DEFAULT_SCALE, - }, - [`${shortHand}b`]: { - property: `${property}Bottom`, - scale: 'spacing', - transform: getSpacing, - defaultScale: DEFAULT_SCALE, - }, - [`${shortHand}l`]: { - property: `${property}Left`, - scale: 'spacing', - transform: getSpacing, - defaultScale: DEFAULT_SCALE, - }, - [`${shortHand}x`]: { - properties: [`${property}Left`, `${property}Right`], - scale: 'spacing', - transform: getSpacing, - defaultScale: DEFAULT_SCALE, - }, - [`${shortHand}y`]: { - properties: [`${property}Top`, `${property}Bottom`], - scale: 'spacing', - transform: getSpacing, - defaultScale: DEFAULT_SCALE, - }, -}); - -const margin = system(getConfig('margin', 'm')); -const padding = system({ - ...getConfig('padding', 'p'), - gap: { property: 'gap', scale: 'spacing', transform: getGapSpacing, defaultScale: DEFAULT_SCALE }, -}); - -export { margin, padding, MarginProps, PaddingProps }; diff --git a/src/Box/system.spec.ts b/src/Box/system.spec.ts new file mode 100644 index 0000000..e4b9da2 --- /dev/null +++ b/src/Box/system.spec.ts @@ -0,0 +1,140 @@ +import { defaultTheme } from '../theme'; +import { pixelTransform, system, systemInterpolation } from './system'; + +const callInterpolation = (interpolation: (props: object) => object, props?: object) => + interpolation({ + ...props, + theme: defaultTheme as any, + }); + +test('Create a system interpolation function', () => { + const interpolationFn = systemInterpolation({ + properties: ['margin'], + transform: pixelTransform, + }); + const interpolation = interpolationFn(10); + expect(callInterpolation(interpolation)).toEqual({ margin: '10px' }); +}); + +test('Create a system interpolation function with multiple properties', () => { + const interpolationFn = systemInterpolation({ + properties: ['margin-left', 'margin-right'], + transform: pixelTransform, + }); + const interpolation = interpolationFn(10); + expect(callInterpolation(interpolation)).toEqual({ + 'margin-left': '10px', + 'margin-right': '10px', + }); +}); + +test('Create a system interpolation from props', () => { + const interpolation = system({ + properties: ['margin'], + fromProps: ['m'], + transform: pixelTransform, + }); + expect( + callInterpolation(interpolation, { + m: 12, + }) + ).toEqual({ margin: '12px' }); +}); + +test('Create a system interpolation from props with multiple configs', () => { + const interpolation = system([ + { + properties: ['margin'], + fromProps: ['m'], + transform: pixelTransform, + }, + { + properties: ['padding'], + fromProps: ['p'], + transform: pixelTransform, + }, + ]); + expect( + callInterpolation(interpolation, { + m: 12, + p: 13, + }) + ).toEqual({ margin: '12px', padding: '13px' }); +}); + +test('Create a system interpolation from props with responsive values as array', () => { + const interpolation = system({ + properties: ['margin'], + fromProps: ['m'], + transform: pixelTransform, + }); + expect( + callInterpolation(interpolation, { + m: [12, 15, 18], + }) + ).toEqual({ + margin: '12px', + '@media only screen and (min-width: 700px)': { + margin: '15px', + }, + '@media only screen and (min-width: 1000px)': { + margin: '18px', + }, + }); +}); + +test('Create a system interpolation from props with responsive values as object', () => { + const interpolation = system([ + { + properties: ['margin'], + fromProps: ['m'], + transform: pixelTransform, + }, + ]); + expect( + callInterpolation(interpolation, { + m: { base: 12, sm: 15, lg: 18 }, + }) + ).toEqual({ + margin: '12px', + '@media only screen and (min-width: 700px)': { + margin: '15px', + }, + '@media only screen and (min-width: 1200px)': { + margin: '18px', + }, + }); +}); + +test('Expose styled components function with "as" name', () => { + const interpolation = system([ + { + properties: ['margin'], + fromProps: ['m'], + transform: pixelTransform, + as: 'all', + }, + ]); + expect(callInterpolation(interpolation.all(12))).toEqual({ margin: '12px' }); +}); + +test('Expose styled components function without "as" name and use "properties', () => { + const interpolation = system([ + { + properties: ['margin'], + fromProps: ['m'], + transform: pixelTransform, + }, + ]); + expect(callInterpolation(interpolation.m(12))).toEqual({ margin: '12px' }); +}); + +test('Expose styled components function without "as" or "fromProps name and use "properties', () => { + const interpolation = system([ + { + properties: ['margin'], + transform: pixelTransform, + }, + ]); + expect(callInterpolation(interpolation.margin(12))).toEqual({ margin: '12px' }); +}); diff --git a/src/Box/system.ts b/src/Box/system.ts new file mode 100644 index 0000000..c5c4ca0 --- /dev/null +++ b/src/Box/system.ts @@ -0,0 +1,242 @@ +import type { CSSObject } from '@emotion/react'; +import { mediaQueryAbove } from '../breakpoints/mediaQueryFns'; +import { themeVars } from '../theme'; +import { Breakpoint } from '../theme/breakpoints'; +import { PabloTheme, PabloThemeableProps } from '../theme/types'; +import { enforceArray } from '../utils/enforceArray'; +import { getByPath } from '../utils/getByPath'; +import { Colors } from '../theme/colors'; +import { KeyMap } from '../types'; +type InterpolationReturn = string | number | null | undefined; +type IdentityTransformFn = + InterpolationTransformFn; +type InterpolationTransformFn = ( + value: T, + theme: PabloTheme +) => R; +type BreakpointObject = Partial>; +type ResponsiveValue = T | (T | null | undefined)[] | BreakpointObject; + +type InterpolationFn = (props: PabloThemeableProps) => CSSObject; +type SystemInterpolationFn = (value: T) => InterpolationFn; + +interface SystemInterpolationPropertyConfig { + properties: readonly string[]; + transform?: InterpolationTransformFn; +} + +interface SystemPropertyConfig extends SystemInterpolationPropertyConfig { + fromProps?: readonly string[]; + as?: PropertyKey; +} + +interface InterpolationFunction

{ + (props: PabloThemeableProps & P): CSSObject; +} + +type ExtractSystemProp> = T extends { + fromProps?: infer B extends readonly string[]; +} + ? B[number] + : T['properties'][number]; + +type IncludedInArray = readonly [K, ...T[]] | readonly [...T[], K]; + +type SingleSystemConfigProps, T = any> = { + [K in ExtractSystemProp]?: ResponsiveValue< + TransformParameterType< + Extract< + C, + | { as?: K } + | { fromProps?: IncludedInArray } + | { properties: IncludedInArray } + > + > + >; +}; + +type SystemConfigProps< + C extends SystemPropertyConfig | readonly SystemPropertyConfig[], + T = any, +> = C extends readonly SystemPropertyConfig[] + ? SingleSystemConfigProps + : C extends SystemPropertyConfig + ? SingleSystemConfigProps + : never; + +type TransformParameterType = + T['transform'] extends InterpolationTransformFn + ? Parameters[0] + : InterpolationReturn; + +type ExtractStyledFunctionKey> = T extends { + as: infer A extends string; +} + ? A + : T extends { fromProps?: infer B extends readonly string[] } + ? B[number] + : T['properties'][number]; + +type StyledInterpolationFunctions, T = any> = { + [K in ExtractStyledFunctionKey]: SystemInterpolationFn>; +}; + +type ArrayStyledInterpolationFunctions = { + [K in ExtractStyledFunctionKey]: SystemInterpolationFn< + TransformParameterType< + Extract< + C[number], + | { as?: K } + | { fromProps?: IncludedInArray } + | { properties: IncludedInArray } + > + > + >; +}; + +type SystemFn< + C extends readonly SystemPropertyConfig[] | SystemPropertyConfig, + T = any, +> = InterpolationFunction> & + (C extends readonly SystemPropertyConfig[] + ? ArrayStyledInterpolationFunctions + : C extends SystemPropertyConfig + ? StyledInterpolationFunctions + : never); + +type InterpolateReturnTuple = readonly [string, InterpolationReturn, Breakpoint | null]; + +const stringableTransform = + (transformFn: InterpolationTransformFn>) => + (value: T, theme: PabloTheme): ReturnType => { + if (typeof value === 'string') { + return value; + } + return transformFn(value as Exclude, theme); + }; +const identityTransform: IdentityTransformFn = (value: T): T => value; +const pixelTransform: InterpolationTransformFn = stringableTransform( + (value) => `${value}px` +); +const spacingTransform: InterpolationTransformFn = stringableTransform( + (value, theme) => `${value * theme.spacing}px` +); + +const colorTransform: InterpolationTransformFn> = (value) => + (getByPath(themeVars.colors as Colors, value) as InterpolationReturn) || value; + +const interpolateSingleValue = ( + properties: readonly string[], + value: any, + props: PabloThemeableProps, + transform: InterpolationTransformFn = identityTransform, + forBreakpoint: Breakpoint | null = null +): InterpolateReturnTuple[] => { + const transformedValue = transform(value, props.theme); + return properties.map((property) => [property, transformedValue, forBreakpoint] as const); +}; + +const interpolate = ( + properties: readonly string[], + value: any | readonly any[], + props: PabloThemeableProps, + transform: InterpolationTransformFn = identityTransform +): InterpolateReturnTuple[] => { + if (value === undefined) { + return []; + } + + if (Array.isArray(value)) { + const breakpointNames = Array.from(props.theme.breakpoints.keys()); + return value.flatMap((v, index) => + interpolateSingleValue(properties, v, props, transform, breakpointNames[index]) + ); + } + if (typeof value === 'object') { + return Object.entries(value).flatMap(([key, value]) => { + return interpolateSingleValue(properties, value, props, transform, key as Breakpoint); + }); + } + return interpolateSingleValue(properties, value, props, transform); +}; + +const makeObject = ( + pairs: (readonly [string, string | number | undefined | null, Breakpoint | null])[], + theme: PabloTheme +) => { + return pairs.reduce((acc, [key, value, breakpointName]) => { + if (breakpointName && breakpointName !== 'base') { + const breakpointKey = `@media ${mediaQueryAbove(breakpointName, theme.breakpoints)}`; + + if (!acc[breakpointKey]) { + acc[breakpointKey] = {}; + } + acc[breakpointKey][key] = value; + return acc; + } + acc[key] = value; + return acc; + }, {}); +}; + +const systemInterpolation = + (config: SystemInterpolationPropertyConfig) => + (value: TransformParameterType) => + (props: any) => + makeObject(interpolate(config.properties, value, props, config.transform), props.theme); + +const createSystemProperty = (config: SystemPropertyConfig) => { + const fromProps = config.fromProps || config.properties; + const interpolateFn = (props) => + enforceArray(fromProps) + .filter((propName) => props[propName]) + .flatMap((propName) => { + return interpolate(config.properties, props[propName], props, config.transform); + }); + return interpolateFn; +}; + +const createSystemProperties = (configs: T): SystemFn => { + const interpolationFn = (props: PabloThemeableProps): CSSObject => + makeObject( + configs.flatMap((config) => createSystemProperty(config)(props)), + props.theme + ); + + configs.forEach((config) => { + const fromProps = config.fromProps || config.properties; + if (config.as) { + (interpolationFn as any)[config.as] = systemInterpolation(config); + } else { + fromProps.forEach((property) => { + (interpolationFn as any)[property] = systemInterpolation(config); + }); + } + }); + + return interpolationFn as SystemFn; +}; + +const system = ( + config: T +): SystemFn => { + const arrayConfig = enforceArray(config); + return createSystemProperties(arrayConfig) as SystemFn; +}; + +export type { + ResponsiveValue, + InterpolationTransformFn, + InterpolationFunction, + IdentityTransformFn, + SystemInterpolationPropertyConfig, + SystemPropertyConfig, +}; +export { + system, + systemInterpolation, + pixelTransform, + spacingTransform, + identityTransform, + colorTransform, +}; diff --git a/src/Button/Button.stories.tsx b/src/Button/Button.stories.tsx index 837320d..6570167 100644 --- a/src/Button/Button.stories.tsx +++ b/src/Button/Button.stories.tsx @@ -21,7 +21,7 @@ const SetOfButtons = ({ inverted, ...args }) => ( mx={-4} mb={4} width={args.fullWidth ? 400 : 0} - flexDirection={args.fullWidth ? 'column' : 'row'} + direction={args.fullWidth ? 'column' : 'row'} >