diff --git a/.gitignore b/.gitignore index 577395af..ffcb0f99 100644 --- a/.gitignore +++ b/.gitignore @@ -68,3 +68,4 @@ apps/docs/.next # TypeScript packages/**/tsconfig.tsbuildinfo tsconfig.tsbuildinfo +dist diff --git a/apps/examples/app/_layout.tsx b/apps/examples/app/_layout.tsx index fb72e5ae..db36ffe0 100644 --- a/apps/examples/app/_layout.tsx +++ b/apps/examples/app/_layout.tsx @@ -1,12 +1,16 @@ -import { ThemeProvider } from 'react-native-ficus-ui'; + import { Stack } from 'expo-router'; +import { Box, ThemeProvider as ThemeProviderV2 } from '@ficus-ui/native' +import { ThemeProvider } from 'react-native-ficus-ui'; export default function RootLayout() { return ( - - - - - + + + + + + + ); } diff --git a/apps/examples/app/components-v2/Box.tsx b/apps/examples/app/components-v2/Box.tsx new file mode 100644 index 00000000..693a6a19 --- /dev/null +++ b/apps/examples/app/components-v2/Box.tsx @@ -0,0 +1,251 @@ +import { Pressable, SafeAreaView, ScrollView, Text, View } from "react-native"; +import { Box } from "@ficus-ui/native"; +import ExampleSection from "@/src/ExampleSection"; +import { Link, useRouter } from "expo-router"; + +const BoxComponent = () => { + const router = useRouter(); + return ( + + + Box component + + + + + + + + + + + + + + + + + none + + + xs + + + sm + + + md + + + lg + + + xl + + + 2xl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default BoxComponent; diff --git a/apps/examples/app/components-v2/index.tsx b/apps/examples/app/components-v2/index.tsx new file mode 100644 index 00000000..26f5c70e --- /dev/null +++ b/apps/examples/app/components-v2/index.tsx @@ -0,0 +1,5 @@ +import StackV2 from '@/src/v2'; + +export default function Components() { + return ; +} diff --git a/apps/examples/app/components/index.tsx b/apps/examples/app/components/index.tsx new file mode 100644 index 00000000..4fe0569b --- /dev/null +++ b/apps/examples/app/components/index.tsx @@ -0,0 +1,5 @@ +import StackV1 from '@/src/v1'; + +export default function Components() { + return ; +} diff --git a/apps/examples/app/items-v2.ts b/apps/examples/app/items-v2.ts new file mode 100644 index 00000000..99730320 --- /dev/null +++ b/apps/examples/app/items-v2.ts @@ -0,0 +1,14 @@ +import { ComponentType } from 'react'; +import BoxComponent from './components-v2/Box'; + +type ExampleComponentType = { + onScreenName: string; + navigationPath: string; + component: ComponentType; +}; + +export const components: ExampleComponentType[] = [ + + { navigationPath: 'Box', onScreenName: 'Box', component: BoxComponent }, + +]; diff --git a/apps/examples/package.json b/apps/examples/package.json index f65f1214..ac561216 100644 --- a/apps/examples/package.json +++ b/apps/examples/package.json @@ -18,6 +18,7 @@ }, "dependencies": { "@expo/vector-icons": "14.0.2", + "@ficus-ui/native": "workspace:*", "@react-native-community/slider": "4.5.4", "@react-navigation/native": "7.0.0-rc.21", "@shopify/flash-list": "1.5.0", diff --git a/apps/examples/src/HomeScreen.tsx b/apps/examples/src/HomeScreen.tsx index 598acd55..9b73e4ad 100644 --- a/apps/examples/src/HomeScreen.tsx +++ b/apps/examples/src/HomeScreen.tsx @@ -6,7 +6,6 @@ import { SafeAreaBox, } from 'react-native-ficus-ui'; -import { components } from '@/app/items'; import { useRouter } from 'expo-router'; import { ScrollView } from 'react-native'; @@ -21,21 +20,26 @@ const HomeScreen = () => { Components - - {components.map((item, index) => ( - - router.push(`/components/${item.navigationPath}`) + router.push(`/components`) } mt={10} w="100%" py={10} > - {item.onScreenName} - - ))} - + V1 + + + router.push(`/components-v2`) + } + mt={10} + w="100%" + py={10} + > + V2 + diff --git a/apps/examples/src/v1/index.tsx b/apps/examples/src/v1/index.tsx new file mode 100644 index 00000000..594a1fac --- /dev/null +++ b/apps/examples/src/v1/index.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { + Box, + Text, + TouchableOpacity, + SafeAreaBox, +} from 'react-native-ficus-ui'; + +import { components } from '@/app/items'; +import { useRouter } from 'expo-router'; +import { ScrollView } from 'react-native'; + +const StackV1 = () => { + const router = useRouter(); + return ( + <> + + + + + router.back() + } + mt={10} + w="100%" + py={10} + > + Back + + + Components + + + {components.map((item, index) => ( + + router.push(`/components/${item.navigationPath}`) + } + mt={10} + w="100%" + py={10} + > + {item.onScreenName} + + ))} + + + + + + ); +}; + +export default StackV1; diff --git a/apps/examples/src/v2/index.tsx b/apps/examples/src/v2/index.tsx new file mode 100644 index 00000000..b681a696 --- /dev/null +++ b/apps/examples/src/v2/index.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { + Box, + Text, + TouchableOpacity, + SafeAreaBox, +} from 'react-native-ficus-ui'; + +import { components } from '@/app/items-v2'; +import { useRouter } from 'expo-router'; +import { ScrollView } from 'react-native'; + +const StackV2 = () => { + const router = useRouter(); + return ( + <> + + + + + router.back() + } + mt={10} + w="100%" + py={10} + > + Back + + + Components + + + {components.map((item, index) => ( + + router.push(`/components-v2/${item.navigationPath}`) + } + mt={10} + w="100%" + py={10} + > + {item.onScreenName} + + ))} + + + + + + ); +}; + +export default StackV2; diff --git a/apps/examples/tsconfig.json b/apps/examples/tsconfig.json index 7001c0d3..3e7a6770 100644 --- a/apps/examples/tsconfig.json +++ b/apps/examples/tsconfig.json @@ -7,7 +7,8 @@ "@/*": ["./*"], "react-native-ficus-ui": [ "../../packages/react-native-ficus-ui/src/index" - ] + ], + "@ficus-ui/native": ["../../packages/components/src/index"] } }, "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"] diff --git a/package.json b/package.json index 424832ff..56db0adc 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "typecheck": "pnpm -C ./packages/react-native-ficus-ui run typecheck", "test": "jest --config ./jest.config.cjs", "ci": "pnpm typecheck && pnpm lint && pnpm test", - "clean": "del ./**/node_modules ./**/lib ./**/.next ./**/.expo", + "clean": "del ./**/node_modules ./**/lib ./**/dist ./**/.next ./**/.expo", "release": "release-it", "format:check": "prettier --check packages --cache", "format:fix": "prettier --write packages --cache" diff --git a/packages/components/.eslintrc b/packages/components/.eslintrc new file mode 100644 index 00000000..2b6d558d --- /dev/null +++ b/packages/components/.eslintrc @@ -0,0 +1,6 @@ +{ + "extends": "../../.eslintrc", + "rules": { + "react/react-in-jsx-scope": "off", + }, +} diff --git a/packages/components/package.json b/packages/components/package.json new file mode 100644 index 00000000..ce332d1b --- /dev/null +++ b/packages/components/package.json @@ -0,0 +1,43 @@ +{ + "name": "@ficus-ui/native", + "version": "2.0.0-alpha", + "scripts": { + "dev": "tsc --watch", + "test": "echo \"Error: no test specified\" && exit 1", + "build": "tsc" + }, + "dependencies": { + "@chakra-ui/utils": "2.2.2", + "@ficus-ui/style-system": "workspace:*", + "@ficus-ui/theme": "workspace:*" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "main": "dist/cjs/index.cjs", + "module": "dist/esm/index.mjs", + "types": "dist/types/index.d.ts", + "exports": { + ".": { + "import": { + "types": "./dist/types/index.d.ts", + "default": "./dist/esm/index.mjs" + }, + "require": { + "types": "./dist/types/index.d.ts", + "default": "./dist/cjs/index.cjs" + } + }, + "./config": { + "import": { + "types": "./dist/types/config/index.d.ts", + "default": "./dist/esm/config/index.mjs" + }, + "require": { + "types": "./dist/types/config/index.d.ts", + "default": "./dist/cjs/config/index.cjs" + } + } + } +} diff --git a/packages/components/src/box/index.tsx b/packages/components/src/box/index.tsx new file mode 100644 index 00000000..43531361 --- /dev/null +++ b/packages/components/src/box/index.tsx @@ -0,0 +1,12 @@ +import { type NativeFicusProps, ficus } from '../system'; + +// TODO: Improve Box as Image API from V1 +export interface BoxProps extends NativeFicusProps<'View'> {} + +export const Box = ficus('View'); + +export const Circle = ficus('View', { + baseStyle: { + borderRadius: 'full', + }, +}); diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts new file mode 100644 index 00000000..7604f697 --- /dev/null +++ b/packages/components/src/index.ts @@ -0,0 +1,2 @@ +export * from './box'; +export { ThemeProvider } from '@ficus-ui/theme'; diff --git a/packages/components/src/system/base-elements.ts b/packages/components/src/system/base-elements.ts new file mode 100644 index 00000000..a0cc63fa --- /dev/null +++ b/packages/components/src/system/base-elements.ts @@ -0,0 +1,24 @@ +import { + Image as RNImage, + Pressable as RNPressable, + ScrollView as RNScrollView, + Text as RNText, + View as RNView, +} from 'react-native'; + +/** + * These are the basic react native elements that are supported with the `ficus.View` notation. + */ +export const baseRNElements = { + View: RNView, + Image: RNImage, + Text: RNText, + ScrollView: RNScrollView, + Pressable: RNPressable, +} as const; + +export type BaseRNElements = keyof typeof baseRNElements; + +export type BaseRNElementProps = React.ComponentProps< + (typeof baseRNElements)[T] +>; diff --git a/packages/components/src/system/factory.ts b/packages/components/src/system/factory.ts new file mode 100644 index 00000000..710573f1 --- /dev/null +++ b/packages/components/src/system/factory.ts @@ -0,0 +1,30 @@ +import type { BaseRNElements } from './base-elements'; +import { FicusStyledOptions, NativeFicusComponents, styled } from './system'; +import { FicusComponent } from './system.types'; +import { RNElementType } from './system.utils'; + +interface FicusFactory { + ( + component: T, + options?: FicusStyledOptions + ): FicusComponent; +} + +function factory() { + const cache = new Map>(); + + return new Proxy(styled, { + apply(_target, _thisArg, argArray: [RNElementType, FicusStyledOptions]) { + return styled(...argArray); + }, + + get(_, element: BaseRNElements) { + if (!cache.has(element)) { + cache.set(element, styled(element)); + } + return cache.get(element); + }, + }) as FicusFactory & NativeFicusComponents; +} + +export const ficus = factory(); diff --git a/packages/components/src/system/forward-ref.ts b/packages/components/src/system/forward-ref.ts new file mode 100644 index 00000000..78b1558f --- /dev/null +++ b/packages/components/src/system/forward-ref.ts @@ -0,0 +1,29 @@ +/** + * Retrieved from Chakra UI. + * + * All credit goes to Chance (Reach UI), Haz (Reakit) and (fluentui) + * for creating the base type definitions upon which we improved on + * + */ +import { forwardRef as forwardReactRef } from 'react'; + +import { ComponentWithAs, PropsOf, RightJoinProps } from './system.types'; +import { RNElementType } from './system.utils'; + +export function forwardRef< + Props extends object, + Component extends RNElementType, +>( + component: React.ForwardRefRenderFunction< + any, + RightJoinProps, Props> & { + as?: RNElementType; + } + > +) { + // @ts-ignore + return forwardReactRef(component) as unknown as ComponentWithAs< + Component, + Props + >; +} diff --git a/packages/components/src/system/index.ts b/packages/components/src/system/index.ts new file mode 100644 index 00000000..253ec725 --- /dev/null +++ b/packages/components/src/system/index.ts @@ -0,0 +1,3 @@ +export * from './system'; +export * from './forward-ref'; +export * from './factory'; diff --git a/packages/components/src/system/system.ts b/packages/components/src/system/system.ts new file mode 100644 index 00000000..caf6b8f8 --- /dev/null +++ b/packages/components/src/system/system.ts @@ -0,0 +1,103 @@ +import { createElement, forwardRef } from 'react'; + +import { assignAfter, compact, runIfFn, splitProps } from '@chakra-ui/utils'; +import { + StyleProps, + SystemStyleObject, + isStyleProp, + styleSheet, +} from '@ficus-ui/style-system'; +import { useTheme } from '@ficus-ui/theme'; +import type { StyleProp } from 'react-native'; + +import { BaseRNElements } from './base-elements'; +import type { + AsProps, + FicusComponent, + FicusProps, + PropsOf, +} from './system.types'; +import { RNElementType, getComponent } from './system.utils'; + +type Dict = Record; + +type GetStyleObject = (options: { baseStyle?: SystemStyleObject }) => ( + propsWithTheme: { + theme: Dict; + style: StyleProp; + } & Dict +) => StyleProp; + +export const toStyleSheetObject: GetStyleObject = + ({ baseStyle }) => + (props) => { + const { theme, style, __styles, ...styleProps } = props; + + const finalBaseStyle = runIfFn(baseStyle, props); + const finalStyles = assignAfter( + {}, + __styles, + finalBaseStyle, + compact(styleProps), + style + ); + + return styleSheet(finalStyles)(theme); + }; + +export interface FicusStyledOptions extends Dict { + label?: string; + baseStyle?: SystemStyleObject; // Base styles applied to the component. +} + +export function styled< + T extends React.ComponentType | RNElementType, + P extends object = {}, +>(component: T, options?: FicusStyledOptions) { + const { baseStyle } = options ?? {}; + const styleObject = toStyleSheetObject({ baseStyle }); + + const Component = getComponent(component); + + const ficusComponent = forwardRef( + function FicusComponent(props, ref) { + const { children, style, as, ...rest } = props; + const { theme } = useTheme(); + + const restProps = splitProps(rest, isStyleProp)[1]; + + const AsComponent = as ? getComponent(as) : Component; + + const propsWithTheme = { + style, + theme, + ...rest, + }; + + const computedStyle = styleObject(propsWithTheme); + + return createElement( + AsComponent, + { + ref, + style: computedStyle, + ...restProps, + }, + children + ); + } + ); + + return ficusComponent as FicusComponent; +} + +export type NativeFicusComponents = { + [Tag in BaseRNElements]: FicusComponent; +}; + +export type NativeFicusProps = Omit< + PropsOf, + 'ref' | keyof StyleProps +> & + FicusProps & + AsProps; diff --git a/packages/components/src/system/system.types.ts b/packages/components/src/system/system.types.ts new file mode 100644 index 00000000..3a9753e3 --- /dev/null +++ b/packages/components/src/system/system.types.ts @@ -0,0 +1,75 @@ +import { + SystemProps as BaseSystemProps, + SystemStyleObject, + TextStyleProps, +} from '@ficus-ui/style-system'; + +import { NativeElementProps, RNElementType } from './system.utils'; + +export type FicusProps = SystemProps & { + /** + * Used for internal style management (like theme styles) + * @private + */ + __styles?: SystemStyleObject; +}; + +export type SystemProps = T extends 'Text' + ? BaseSystemProps // Ensure BaseSystemProps always gets a type argument + : BaseSystemProps>; // Provide a default type argument + +export interface AsProps { + as?: T; +} + +// Props for a component with `as` prop to dynamically change the component type +export type PropsOf = Omit< + NativeElementProps, + 'ref' +> & + AsProps; + +// Omit common props like `as` or any additional props you define +export type OmitCommonProps< + Target, + OmitAdditionalProps extends keyof any = never, +> = Omit; + +// Utility type to merge props of the base component and the `as` component +export type RightJoinProps< + SourceProps extends object = {}, + OverrideProps extends object = {}, +> = OmitCommonProps & OverrideProps; + +// Utility type to merge props of two components, with `as` dynamic resolution +export type MergeWithAs< + ComponentProps extends object, + AsCompProps extends object, + AdditionalProps extends object = {}, + AsComponent extends RNElementType = RNElementType, +> = ( + | RightJoinProps + | RightJoinProps +) & { as?: AsComponent }; + +// Component type with `as` prop for dynamic component rendering +export type ComponentWithAs< + Component extends RNElementType, + Props extends object = {}, +> = { + ( + props: MergeWithAs< + NativeElementProps, + NativeElementProps, + Props, + AsComponent + > + ): JSX.Element; + + displayName?: string; + defaultProps?: Partial; + id?: string; +}; + +export interface FicusComponent + extends ComponentWithAs & P> {} diff --git a/packages/components/src/system/system.utils.ts b/packages/components/src/system/system.utils.ts new file mode 100644 index 00000000..4fb4b7b5 --- /dev/null +++ b/packages/components/src/system/system.utils.ts @@ -0,0 +1,26 @@ +import { + type BaseRNElementProps, + type BaseRNElements, + baseRNElements, +} from './base-elements'; + +/** + * To allow user to extend custom component with ficus properties + */ +type CustomNativeElementProps> = + React.ComponentProps; + +export type RNElementType = BaseRNElements | React.ComponentType; + +export type NativeElementProps = + T extends BaseRNElements + ? BaseRNElementProps + : T extends React.ComponentType + ? CustomNativeElementProps + : never; + +export function getComponent(component: T) { + return typeof component === 'string' + ? baseRNElements[component as BaseRNElements] + : component; +} diff --git a/packages/components/tsconfig.json b/packages/components/tsconfig.json new file mode 100644 index 00000000..90f87c4e --- /dev/null +++ b/packages/components/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src"], + "compilerOptions": { + "jsx": "react-native", + "outDir": "./dist/types" + } +} diff --git a/packages/style-system/package.json b/packages/style-system/package.json new file mode 100644 index 00000000..14df2c0f --- /dev/null +++ b/packages/style-system/package.json @@ -0,0 +1,45 @@ +{ + "name": "@ficus-ui/style-system", + "version": "1.0.0", + "scripts": { + "dev": "tsc --watch", + "test": "echo \"Error: no test specified\" && exit 1", + "typecheck": "tsc --noEmit", + "build": "tsc" + }, + "keywords": [], + "author": { + "email": "noe@bearstudio.fr", + "name": "NoƩ Tatoud" + }, + "license": "ISC", + "description": "", + "dependencies": { + "@chakra-ui/utils": "2.2.2" + }, + "types": "dist/types/src/index.d.ts", + "main": "dist/types/src/index.js", + "module": "dist/types/src/index.js", + "exports": { + ".": { + "import": { + "types": "./dist/types/index.d.ts", + "default": "./dist/esm/index.mjs" + }, + "require": { + "types": "./dist/types/index.d.ts", + "default": "./dist/cjs/index.cjs" + } + }, + "./config": { + "import": { + "types": "./dist/types/config/index.d.ts", + "default": "./dist/esm/config/index.mjs" + }, + "require": { + "types": "./dist/types/config/index.d.ts", + "default": "./dist/cjs/config/index.cjs" + } + } + } +} diff --git a/packages/style-system/src/config/background.ts b/packages/style-system/src/config/background.ts new file mode 100644 index 00000000..4e0777f5 --- /dev/null +++ b/packages/style-system/src/config/background.ts @@ -0,0 +1,10 @@ +import { t } from '../utils'; +import { Config } from '../utils/prop-config'; + +export const background: Config = { + bg: t.colors('backgroundColor'), +}; + +export interface BackgroundProps { + bg?: string; +} diff --git a/packages/style-system/src/config/border.ts b/packages/style-system/src/config/border.ts new file mode 100644 index 00000000..d77e19cb --- /dev/null +++ b/packages/style-system/src/config/border.ts @@ -0,0 +1,37 @@ +import { ViewStyle } from 'react-native'; + +import { t } from '../utils'; +import { Config } from '../utils/prop-config'; +import { ResponsiveValue } from '../utils/types'; + +export const border: Config = { + borderWidth: t.borderWidths('borderWidth'), + borderTopWidth: t.borderWidths('borderTopWidth'), + borderRightWidth: t.borderWidths('borderRightWidth'), + borderBottomWidth: t.borderWidths('borderBottomWidth'), + borderLeftWidth: t.borderWidths('borderLeftWidth'), + borderStartWidth: t.borderWidths('borderStartWidth'), + borderEndWidth: t.borderWidths('borderEndWidth'), + borderColor: t.colors('borderColor'), + borderTopColor: t.colors('borderTopColor'), + borderRightColor: t.colors('borderRightColor'), + borderBottomColor: t.colors('borderBottomColor'), + borderLeftColor: t.colors('borderLeftColor'), + borderStyle: t.borderStyles('borderStyle'), +}; + +export interface BorderProps { + borderWidth?: ResponsiveValue; + borderTopWidth?: ResponsiveValue; + borderRightWidth?: ResponsiveValue; + borderBottomWidth?: ResponsiveValue; + borderLeftWidth?: ResponsiveValue; + borderStartWidth?: ResponsiveValue; + borderEndWidth?: ResponsiveValue; + borderColor?: ResponsiveValue; + borderTopColor?: ResponsiveValue; + borderRightColor?: ResponsiveValue; + borderBottomColor?: ResponsiveValue; + borderLeftColor?: ResponsiveValue; + borderStyle?: ResponsiveValue; +} diff --git a/packages/style-system/src/config/colors.ts b/packages/style-system/src/config/colors.ts new file mode 100644 index 00000000..0d8a2683 --- /dev/null +++ b/packages/style-system/src/config/colors.ts @@ -0,0 +1,16 @@ +import { t } from '../utils'; +import { Config } from '../utils/prop-config'; + +export const color: Config = { + color: t.colors('color'), + textColor: t.colors('color'), + overlayColor: t.colors('overlayColor'), + shadowColor: t.colors('shadowColor'), +}; + +export interface ColorProps { + color?: string; + textColor?: string; + overlayColor?: string; + shadowColor?: string; +} diff --git a/packages/style-system/src/config/effect.ts b/packages/style-system/src/config/effect.ts new file mode 100644 index 00000000..cbf3f91f --- /dev/null +++ b/packages/style-system/src/config/effect.ts @@ -0,0 +1,25 @@ +import { ViewStyle } from 'react-native'; + +import { t } from '../utils'; +import { Config } from '../utils/prop-config'; +import { ResponsiveValue } from '../utils/types'; + +export const effect: Config = { + boxShadow: true, + shadow: t.shadows(), + shadowOpacity: true, + shadowOffset: t.shadows('shadowOffset'), + shadowRadius: t.radius('shadowRadius'), + opacity: true, + filter: true, +}; + +export interface EffectProps { + boxShadow?: ViewStyle['boxShadow']; + shadow?: string; + shadowOpacity?: ResponsiveValue; + shadowOffset?: ResponsiveValue; + shadowRadius?: ResponsiveValue; + opacity?: ResponsiveValue; + filter?: ViewStyle['shadowOffset']; +} diff --git a/packages/style-system/src/config/flexbox.ts b/packages/style-system/src/config/flexbox.ts new file mode 100644 index 00000000..08f71a8a --- /dev/null +++ b/packages/style-system/src/config/flexbox.ts @@ -0,0 +1,48 @@ +import { ViewStyle } from 'react-native'; + +import { t } from '../utils'; +import { Config } from '../utils/prop-config'; +import { ResponsiveValue } from '../utils/types'; + +export const flexbox: Config = { + flex: true, + flexDirection: true, + direction: t.flexbox('flexDirection'), + flexWrap: true, + wrap: t.flexbox('flexWrap'), + flexBasis: true, + basis: t.flexbox('flexBasis'), + flexGrow: true, + grow: t.flexbox('flexGrow'), + flexShrink: true, + shrink: t.flexbox('flexShrink'), + justifyContent: true, + justify: t.flexbox('justifyContent'), + alignSelf: t.flexbox('alignItems'), + alignItems: true, + align: t.flexbox('alignItems'), +}; + +type FlexDirection = ViewStyle['flexDirection']; +type FlexWrap = ViewStyle['flexWrap']; +type FlexJustify = ViewStyle['justifyContent']; +type FlexAlignSelf = ViewStyle['alignSelf']; +type FlexAlignItems = ViewStyle['alignItems']; +export interface FlexboxProps { + flex?: ResponsiveValue; + flexDirection?: ResponsiveValue; + direction?: ResponsiveValue; + flexWrap?: ResponsiveValue; + wrap?: ResponsiveValue; + flexBasis?: ResponsiveValue; + basis?: ResponsiveValue; + flexGrow?: ResponsiveValue; + grow?: ResponsiveValue; + flexShrink?: ResponsiveValue; + shrink?: ResponsiveValue; + justifyContent?: ResponsiveValue; + justify?: ResponsiveValue; + alignSelf?: ResponsiveValue; + alignItems?: ResponsiveValue; + align?: ResponsiveValue; +} diff --git a/packages/style-system/src/config/index.ts b/packages/style-system/src/config/index.ts new file mode 100644 index 00000000..705c9289 --- /dev/null +++ b/packages/style-system/src/config/index.ts @@ -0,0 +1,12 @@ +export * from './background'; +export * from './border'; +export * from './colors'; +export * from './effect'; +export * from './flexbox'; +export * from './interactivity'; +export * from './interactivity'; +export * from './layout'; +export * from './position'; +export * from './radius'; +export * from './space'; +export * from './text'; diff --git a/packages/style-system/src/config/interactivity.ts b/packages/style-system/src/config/interactivity.ts new file mode 100644 index 00000000..d9e3644e --- /dev/null +++ b/packages/style-system/src/config/interactivity.ts @@ -0,0 +1,13 @@ +import { TextStyle, ViewStyle } from 'react-native'; + +import { Config } from '../utils/prop-config'; + +export const interactivty: Config = { + pointerEvents: true, + userSelect: true, +}; + +export interface InteractivityProps { + pointerEvents?: ViewStyle['pointerEvents']; + userSelect?: TextStyle['userSelect']; +} diff --git a/packages/style-system/src/config/layout.ts b/packages/style-system/src/config/layout.ts new file mode 100644 index 00000000..fa9e4d32 --- /dev/null +++ b/packages/style-system/src/config/layout.ts @@ -0,0 +1,31 @@ +import { ViewStyle } from 'react-native'; + +import { t } from '../utils'; +import { Config } from '../utils/prop-config'; +import { ResponsiveValue } from '../utils/types'; + +export const layout: Config = { + minH: t.sizes('minHeight'), + minW: t.sizes('minWidth'), + maxH: t.sizes('maxHeight'), + maxW: t.sizes('maxWidth'), + h: t.sizes('height'), + w: t.sizes('width'), + overflow: true, + display: true, + aspectRatio: true, +}; + +type Overflow = ViewStyle['overflow']; +type Display = ViewStyle['display']; +export interface LayoutProps { + minH?: ResponsiveValue; + minW?: ResponsiveValue; + maxH?: ResponsiveValue; + maxW?: ResponsiveValue; + h?: ResponsiveValue; + w?: ResponsiveValue; + overflow?: ResponsiveValue; + display?: ResponsiveValue; + aspectRatio?: ResponsiveValue; +} diff --git a/packages/style-system/src/config/position.ts b/packages/style-system/src/config/position.ts new file mode 100644 index 00000000..ef8e4d07 --- /dev/null +++ b/packages/style-system/src/config/position.ts @@ -0,0 +1,23 @@ +import { t } from '../utils'; +import { Config } from '../utils/prop-config'; +import { ResponsiveValue } from '../utils/types'; + +export const position: Config = { + position: true, + top: t.space('top'), + right: t.space('top'), + bottom: t.space('top'), + left: t.space('top'), + zIndex: t.space('top'), + elevation: t.space('top'), +}; + +export interface PositionProps { + position?: ResponsiveValue<'absolute' | 'relative'>; + top?: ResponsiveValue; + right?: ResponsiveValue; + bottom?: ResponsiveValue; + left?: ResponsiveValue; + zIndex?: ResponsiveValue; + elevation?: ResponsiveValue; +} diff --git a/packages/style-system/src/config/radius.ts b/packages/style-system/src/config/radius.ts new file mode 100644 index 00000000..e1626263 --- /dev/null +++ b/packages/style-system/src/config/radius.ts @@ -0,0 +1,33 @@ +import { t } from '../utils'; +import { Config } from '../utils/prop-config'; +import { ResponsiveValue } from '../utils/types'; + +export const radius: Config = { + borderRadius: t.radius('borderRadius'), + borderTopLeftRadius: t.radius('borderTopLeftRadius'), + borderTopRightRadius: t.radius('borderTopRightRadius'), + borderBottomLeftRadius: t.radius('borderBottomLeftRadius'), + borderBottomRightRadius: t.radius('borderBottomRightRadius'), + borderTopRadius: t.radius(['borderTopLeftRadius', 'borderTopRightRadius']), + borderLeftRadius: t.radius(['borderTopLeftRadius', 'borderBottomLeftRadius']), + borderRightRadius: t.radius([ + 'borderTopRightRadius', + 'borderBottomRightRadius', + ]), + borderBottomRadius: t.radius([ + 'borderBottomLeftRadius', + 'borderBottomRightRadius', + ]), +}; + +export interface RadiusProps { + borderRadius?: ResponsiveValue; + borderTopLeftRadius?: ResponsiveValue; + borderTopRightRadius?: ResponsiveValue; + borderBottomLeftRadius?: ResponsiveValue; + borderBottomRightRadius?: ResponsiveValue; + borderTopRadius?: ResponsiveValue; + borderLeftRadius?: ResponsiveValue; + borderRightRadius?: ResponsiveValue; + borderBottomRadius?: ResponsiveValue; +} diff --git a/packages/style-system/src/config/space.ts b/packages/style-system/src/config/space.ts new file mode 100644 index 00000000..cc8c7a63 --- /dev/null +++ b/packages/style-system/src/config/space.ts @@ -0,0 +1,47 @@ +import { t } from '../utils'; +import { Config } from '../utils/prop-config'; +import { ResponsiveValue } from '../utils/types'; + +export const space: Config = { + m: t.space('margin'), + mt: t.space('marginTop'), + mr: t.space('marginRight'), + mb: t.space('marginBottom'), + ml: t.space('marginLeft'), + mx: t.space('marginHorizontal'), + my: t.space('marginVertical'), + ms: t.space('marginStart'), + p: t.space('padding'), + pt: t.space('paddingTop'), + pr: t.space('paddingRight'), + pb: t.space('paddingBottom'), + pl: t.space('paddingLeft'), + px: t.space('paddingHorizontal'), + py: t.space('paddingVertical'), + ps: t.space('paddingStart'), + gap: t.space('gap'), + gapX: t.space('columnGap'), + gapY: t.space('rowGap'), +}; + +export interface SpaceProps { + m?: ResponsiveValue; + mt?: ResponsiveValue; + mr?: ResponsiveValue; + mb?: ResponsiveValue; + ml?: ResponsiveValue; + mx?: ResponsiveValue; + my?: ResponsiveValue; + ms?: ResponsiveValue; + p?: ResponsiveValue; + pt?: ResponsiveValue; + pr?: ResponsiveValue; + pb?: ResponsiveValue; + pl?: ResponsiveValue; + px?: ResponsiveValue; + py?: ResponsiveValue; + ps?: ResponsiveValue; + gap?: ResponsiveValue; + gapX?: ResponsiveValue; + gapY?: ResponsiveValue; +} diff --git a/packages/style-system/src/config/text.ts b/packages/style-system/src/config/text.ts new file mode 100644 index 00000000..d538f67a --- /dev/null +++ b/packages/style-system/src/config/text.ts @@ -0,0 +1,55 @@ +import { TextStyle } from 'react-native'; + +import { t } from '../utils'; +import { Config } from '../utils/prop-config'; +import { transforms } from '../utils/transform-functions'; +import { ResponsiveValue } from '../utils/types'; + +export const text: Config = { + fontSize: t.prop('fontSize', 'fontSizes', transforms.getThemeProp), + textDecorLine: t.prop('textDecorationLine'), + textDecorStyle: t.prop('textDecorationStyle'), + fontStyle: true, + textDecorColor: t.colors('textDecorationColor'), + fontWeight: t.prop('fontWeight', 'fonts', transforms.getFontWeight, [ + 'fontFamily', + ]), + fontFamily: t.prop('fontFamily', 'fonts', transforms.getThemeFontFamily, [ + 'fontWeight', + ]), + lineHeight: true, + textAlign: true, + textTransform: true, + letterSpacing: true, + textAlignVertical: true, + textDecorationLine: true, + textDecorationStyle: true, + textDecorationColor: t.colors('textDecorationColor'), + textShadowColor: t.colors('shadowColor'), + textShadowOffset: t.shadows('textShadowOffset'), + textShadowRadius: t.radius('textShadowRadius'), +}; + +/** + * Only for React native Text Component + */ +export interface TextStyleProps { + fontSize?: ResponsiveValue; + textDecorLine?: ResponsiveValue; + textDecorStyle?: ResponsiveValue; + fontStyle?: ResponsiveValue; + textDecorColor?: ResponsiveValue; + fontWeight?: ResponsiveValue; + fontFamily?: ResponsiveValue<'heading' | 'mono' | (string & {})>; + lineHeight?: ResponsiveValue; + textAlign?: ResponsiveValue; + textTransform?: ResponsiveValue; + letterSpacing?: ResponsiveValue; + textAlignVertical?: ResponsiveValue; + textDecorationLine?: ResponsiveValue; + textDecorationStyle?: ResponsiveValue; + textDecorationColor?: ResponsiveValue; + textShadowColor?: ResponsiveValue; + textShadowOffset?: ResponsiveValue; + textShadowRadius?: ResponsiveValue; +} diff --git a/packages/style-system/src/define-style.ts b/packages/style-system/src/define-style.ts new file mode 100644 index 00000000..7afbc0fb --- /dev/null +++ b/packages/style-system/src/define-style.ts @@ -0,0 +1,56 @@ +import { SystemStyleObject } from './system.types'; +import { Dict } from './utils/types'; + +export type StyleFunctionProps = { + colorScheme: string; + colorMode: 'light' | 'dark'; + orientation?: 'horizontal' | 'vertical'; + theme: Dict; + [key: string]: any; +}; + +export type SystemStyleFunction = ( + props: StyleFunctionProps +) => SystemStyleObject; + +export type SystemStyleInterpolation = SystemStyleObject | SystemStyleFunction; + +// ------------------------------------------------------------------ // + +export function defineStyle(styles: T) { + return styles; +} + +// ------------------------------------------------------------------ // + +type DefaultProps = { + size?: string; + variant?: string; + colorScheme: string; +}; + +export type StyleConfig = { + baseStyle?: SystemStyleInterpolation; + sizes?: { [size: string]: SystemStyleInterpolation }; + variants?: { [variant: string]: SystemStyleInterpolation }; + defaultProps: DefaultProps; +}; + +export function defineStyleConfig< + BaseStyle extends SystemStyleInterpolation, + Sizes extends Dict, + Variants extends Dict, +>(config: { + baseStyle?: BaseStyle; + sizes: Sizes; + variants: Variants; + defaultProps?: { + size?: keyof Sizes; + variant?: keyof Variants; + colorScheme?: string; + }; +}) { + return config; +} + +// ------------------------------------------------------------------ // diff --git a/packages/style-system/src/index.ts b/packages/style-system/src/index.ts new file mode 100644 index 00000000..fa11ee69 --- /dev/null +++ b/packages/style-system/src/index.ts @@ -0,0 +1,8 @@ +export * from './system.types'; +export * from './system'; +export * from './theme.types'; +export * from './theming-props'; +export * from './define-style'; +export * from './style-sheet'; +export * from './utils'; +export * from './config'; diff --git a/packages/style-system/src/style-sheet.ts b/packages/style-system/src/style-sheet.ts new file mode 100644 index 00000000..368b77ea --- /dev/null +++ b/packages/style-system/src/style-sheet.ts @@ -0,0 +1,108 @@ +import { Dict, isObject, mergeWith, runIfFn } from '@chakra-ui/utils'; + +import { systemProps } from './system'; +import { SystemStyleObject } from './system.types'; +import { expandResponsive } from './utils/expand-responsive'; +import { Config, PropConfig } from './utils/prop-config'; + +interface GetStyleSheetOptions { + theme: { [key: string]: any }; + configs?: Config; +} + +/** + * This is the function that will be used to convert all the props to react native StyleSheet + */ +export function getStyleSheet({ configs = {}, theme }: GetStyleSheetOptions) { + const styleSheet = (stylesOrFn: Record) => { + const _styles = runIfFn(stylesOrFn, theme); + + const styles = expandResponsive(_styles)(theme); + + let computedStyles: Dict = {}; + + for (let key in styles) { + let value = styles[key]; + + let config = configs[key]; + + if (config === true) { + config = { property: key } as PropConfig; + } + + // if (isObject(value)) { + // computedStyles[key] = computedStyles[key] ?? {}; + // computedStyles[key] = mergeWith( + // {}, + // computedStyles[key], + // styleSheet(value, true) + // ); + // continue; + // } + + /** + * Add the peer properties to help for computing the value + * e.g. fontWeight needs fontFamily + */ + if (config?.peerProperties) { + const peerProperties = config.peerProperties; + + for (let i in peerProperties) { + const peerProperty = peerProperties[i]!; + value = { + [key]: value, // Preserve original value + [peerProperty]: styles[peerProperty] ?? undefined, + }; + } + } + + /** + * The value of the property after transformation + * */ + let rawValue = + config?.transform?.( + value, + config.scope ? theme[config.scope] : theme + ) ?? value; + + /** + * The StyleSheet property or property array this prop style key maps to + */ + const configProperty = config?.property; + + if (configProperty && Array.isArray(configProperty)) { + for (const property of configProperty) { + computedStyles[property] = rawValue; + } + continue; + } + + // We know configProperty is not an array + if (configProperty) { + computedStyles[configProperty as string] = rawValue; + + continue; + } + + if (isObject(rawValue)) { + computedStyles = mergeWith({}, computedStyles, rawValue); + continue; + } + + computedStyles[key] = rawValue; + } + + return computedStyles; + }; + + return styleSheet; +} + +export const styleSheet = (styles: SystemStyleObject) => (theme: any) => { + const styleSheetFn = getStyleSheet({ + theme, + configs: systemProps, + }); + + return styleSheetFn(styles); +}; diff --git a/packages/style-system/src/system.ts b/packages/style-system/src/system.ts new file mode 100644 index 00000000..a0faadf8 --- /dev/null +++ b/packages/style-system/src/system.ts @@ -0,0 +1,32 @@ +import { mergeWith } from '@chakra-ui/utils'; + +import { + background, + border, + color, + effect, + flexbox, + interactivty, + layout, + position, + radius, + space, + text, +} from './config'; + +export const systemProps = mergeWith( + {}, + background, + border, + color, + effect, + flexbox, + interactivty, + layout, + position, + radius, + space, + text +); + +export const isStyleProp = (prop: string) => prop in systemProps; diff --git a/packages/style-system/src/system.types.ts b/packages/style-system/src/system.types.ts new file mode 100644 index 00000000..36aee5ab --- /dev/null +++ b/packages/style-system/src/system.types.ts @@ -0,0 +1,70 @@ +import { + BackgroundProps, + BorderProps, + ColorProps, + EffectProps, + FlexboxProps, + InteractivityProps, + LayoutProps, + PositionProps, + RadiusProps, + SpaceProps, +} from './config'; +import { RNStyleSheet, RNStyleSheetProperties } from './utils/prop-config'; +import { Dict, ResponsiveValue } from './utils/types'; + +// Define the base interface for style props with generic ExtraProps that extends Config + +export interface StyleProps + extends BackgroundProps, + BorderProps, + ColorProps, + EffectProps, + FlexboxProps, + InteractivityProps, + LayoutProps, + PositionProps, + RadiusProps, + SpaceProps {} + +export interface SystemStyleSheetProperties + extends RNStyleSheet, + Omit {} + +export type ThemeThunk = T | ((theme: Record) => T); + +type PropertyValue = ThemeThunk< + ResponsiveValue +>; + +export type StyleSheetWithMultiValues = { + [K in keyof SystemStyleSheetProperties]?: K extends keyof StyleProps + ? StyleProps[K] | PropertyValue + : PropertyValue; +}; + +type StyleSheetDefinition = + | D + | string + | RecursiveStyleSheetSelector; + +export interface RecursiveStyleSheetSelector { + [selector: string]: StyleSheetDefinition & D; +} + +export type RecursiveStyleSheetObject = D & + (D | RecursiveStyleSheetSelector); + +export type SystemStyleObject = + RecursiveStyleSheetObject; + +/** + * We might need to extend SystemProps. + * For example for Text + */ +export type SystemProps = StyleProps & ExtraProps; + +/** + * Extensible style props + */ +export type { TextStyleProps } from './config'; diff --git a/packages/style-system/src/theme.types.ts b/packages/style-system/src/theme.types.ts new file mode 100644 index 00000000..fd24799a --- /dev/null +++ b/packages/style-system/src/theme.types.ts @@ -0,0 +1,37 @@ +const tokens = [ + /** These are part of the theme */ + 'breakpoints', + 'fonts', + 'fontSizes', + 'fontWeights', + 'colors', + 'radius', + 'shadows', + 'sizes', + 'space', + /** These are not part of the theme */ + 'borderStyles', + 'borderWidths', + 'flexbox', +] as const; + +export type ThemeScope = (typeof tokens)[number]; + +export interface ThemeTypings { + breakpoints: string; + fonts: string; + fontSizes: string; + fontWeights: string; + colors: string; + colorSchemes: string; + radius: string; + sizes: string; + space: string; + shadows: string; + components: { + [componentName: string]: { + sizes: string; + variants: string; + }; + }; +} diff --git a/packages/style-system/src/theming-props.ts b/packages/style-system/src/theming-props.ts new file mode 100644 index 00000000..d47c1f4e --- /dev/null +++ b/packages/style-system/src/theming-props.ts @@ -0,0 +1,25 @@ +import { omit } from '@chakra-ui/utils'; + +import { ThemeTypings } from './theme.types'; +import { Dict, ResponsiveValue } from './utils/types'; + +export interface ThemingProps { + variant?: ResponsiveValue< + ResponsiveValue< + ThemeComponent extends keyof ThemeTypings['components'] + ? ThemeTypings['components'][ThemeComponent]['variants'] + : string + > + >; + size?: ResponsiveValue< + ThemeComponent extends keyof ThemeTypings['components'] + ? ThemeTypings['components'][ThemeComponent]['sizes'] + : string + >; + colorScheme?: ThemeTypings['colorSchemes']; + styleConfig?: Dict; +} + +export function omitThemingProps(props: T) { + return omit(props, ['size', 'variant', 'colorScheme', 'styleConfig']); +} diff --git a/packages/style-system/src/utils/create-transform.ts b/packages/style-system/src/utils/create-transform.ts new file mode 100644 index 00000000..a9380757 --- /dev/null +++ b/packages/style-system/src/utils/create-transform.ts @@ -0,0 +1,13 @@ +import { Transform } from './types'; + +interface CreateTransformOptions { + transform?: Transform; +} + +export function createTransform({ + transform, +}: CreateTransformOptions): Transform { + return (value, theme) => { + return transform?.(value, theme) ?? value; + }; +} diff --git a/packages/style-system/src/utils/expand-responsive.ts b/packages/style-system/src/utils/expand-responsive.ts new file mode 100644 index 00000000..1fd2cb09 --- /dev/null +++ b/packages/style-system/src/utils/expand-responsive.ts @@ -0,0 +1,70 @@ +import { isObject } from '@chakra-ui/utils'; + +import { Dict } from './types'; + +/** + * Resolve all the responsive property values as a single value depending on current device's windowWidth + */ +export const expandResponsive = (styles: Dict) => (theme: Dict) => { + const windowWidth = theme?.__windowWidth; + const computedStyles: Dict = {}; + + for (const key in styles) { + let value = styles[key]; + + computedStyles[key] = value; + + if (value === null) { + continue; + } + + /** + * For responsive values with `{base: 0, xs: 2}` format + */ + if (isObject(value)) { + for (const breakpoint in value) { + if ( + theme?.breakpoints[breakpoint] === undefined || + theme?.breakpoints[breakpoint] === null + ) { + continue; + } + + const currentBreakpointValue = theme?.breakpoints[breakpoint]; + + if (windowWidth >= currentBreakpointValue) { + computedStyles[key] = value[breakpoint]; + } + } + continue; + } + + /** + * Breakpoints ordered by ascending size + */ + const breakpoints = theme?.__breakpoints; + + /** + * For responsive values with `[0, 2]` format + */ + if (Array.isArray(value)) { + // Defaults to the first value of the array + let matchedValue = value[0]; + + for (let i = 0; i < breakpoints.length; i++) { + const [, minWidth] = breakpoints[i]!; + + if (windowWidth >= minWidth) { + matchedValue = + value[i] !== undefined && value[i] !== null + ? value[i] + : matchedValue; + } + } + + computedStyles[key] = matchedValue; + } + } + + return computedStyles; +}; diff --git a/packages/style-system/src/utils/index.ts b/packages/style-system/src/utils/index.ts new file mode 100644 index 00000000..3e469513 --- /dev/null +++ b/packages/style-system/src/utils/index.ts @@ -0,0 +1,31 @@ +import { ThemeScope } from '../theme.types'; +import { createTransform } from './create-transform'; +import { PropConfig, toConfig } from './prop-config'; +import { transforms } from './transform-functions'; + +export const t = { + colors: toConfig('colors', transforms.getThemeColor), + borderWidths: toConfig('borderWidths'), + borderStyles: toConfig('borderStyles'), + radius: toConfig('radius', transforms.getThemeProp), + space: toConfig('space', transforms.getThemeProp), + shadows: toConfig('shadows', transforms.getThemeProp), + sizes: toConfig('sizes', transforms.getThemeProp), + flexbox: toConfig('flexbox'), + prop( + property: PropConfig['property'], + scope?: ThemeScope, + transform?: PropConfig['transform'], + peerProperties?: PropConfig['peerProperties'] + ) { + return { + property, + scope, + transform: createTransform({ transform }), + peerProperties, + }; + }, +}; + +export * from './prop-config'; +export * from './types'; diff --git a/packages/style-system/src/utils/prop-config.ts b/packages/style-system/src/utils/prop-config.ts new file mode 100644 index 00000000..d7e3e3da --- /dev/null +++ b/packages/style-system/src/utils/prop-config.ts @@ -0,0 +1,49 @@ +import { FlexStyle, ImageStyle, TextStyle, ViewStyle } from 'react-native'; + +import { ThemeScope } from '../theme.types'; +import { createTransform } from './create-transform'; +import { Transform } from './types'; + +export type RNStyleSheet = TextStyle & ViewStyle & FlexStyle & ImageStyle; + +export type RNStyleSheetProperties = keyof RNStyleSheet; + +type MaybeArray = T | T[]; + +export interface PropConfig { + /** + * The theme scope this maps to + */ + scope?: ThemeScope; + /** + * StyleSheet or React Native prop + * This can be undefined in case the property maps to a theme aware style that includes several properties. + * E.g. `shadow` prop which maps to the theme scope `shadows` + */ + property?: MaybeArray; + /** + * Function to transform the value passed + */ + transform?: Transform; + /** + * Array of properties that may be needed to transform the current prop + * e.g. fontWeight needs fontFamily + */ + peerProperties?: string[]; +} + +export type Config = Record< + T extends Record ? keyof T : string, + PropConfig | true // True if ficus prop and style prop have same name and do not need transformation +>; + +export function toConfig(scope: ThemeScope, transform?: Transform) { + return (property?: T | T[]) => { + const result: PropConfig = { property, scope }; + result.transform = createTransform({ + transform, + }); + + return result; + }; +} diff --git a/packages/style-system/src/utils/transform-functions.ts b/packages/style-system/src/utils/transform-functions.ts new file mode 100644 index 00000000..a2d30339 --- /dev/null +++ b/packages/style-system/src/utils/transform-functions.ts @@ -0,0 +1,98 @@ +import { Dict } from './types'; + +// NOTE: This function are taken from Magnus and could probably be improved +export const transforms = { + getThemeProp(value: any, themeScope: Dict | undefined) { + if (themeScope?.[value]) { + return themeScope[value]; + } + + return value; + }, + getFontWeight( + value: { + fontWeight: string | number; + fontFamily?: string; + }, + themeScope: Dict + ) { + const fontFamilyKey = value.fontFamily ?? ''; + const fontFamily = themeScope.fontFamily?.[fontFamilyKey]; + + if (fontFamily) { + return 'normal'; + } + + const resolvedFontWeight = resolveFontWeight(value.fontWeight, themeScope); + + return resolvedFontWeight; + }, + getThemeFontFamily( + value: { + fontFamily: string; + fontWeight?: string | number; + }, + themeScope: Dict + ): string | undefined { + const rawFontWeight = value.fontWeight; + const rawFontFamily = value.fontFamily; + + const fontFamily = themeScope.fontFamily?.[rawFontFamily] ?? {}; + + const resolvedFontWeight = resolveFontWeight(rawFontWeight, themeScope); + + return fontFamily[resolvedFontWeight] ?? rawFontFamily; + }, + getThemeColor(value: any, theme: Dict | undefined) { + let colorValueResult: string | String = value as string; + + if (theme && value) { + // Check if color value is a valid theme color + if ( + theme.hasOwnProperty(value) && + (typeof theme[value] === 'string' || theme[value] instanceof String) + ) { + const colorValue: string | String = theme[value] as string; + return colorValue as string; + } + + // If color value contains dots, check into theme sub objects if it's a valid theme color + if (value.includes('.')) { + const keyParts = value.split('.'); + let subPropertyValue: any = theme; + for (const part of keyParts) { + if (subPropertyValue && part) { + subPropertyValue = subPropertyValue[part]; + } + } + if ( + typeof subPropertyValue === 'string' || + subPropertyValue instanceof String + ) { + colorValueResult = subPropertyValue; + } + } + } + + return isValidColor(colorValueResult as string) + ? (colorValueResult as string) + : 'transparent'; + }, +}; + +export const isValidColor = (color: string): boolean => { + // TODO + return typeof color === 'string'; +}; + +const resolveFontWeight = ( + fontWeight: string | number = 'normal', + themeScope: Dict +) => { + return ( + // If font weight is a keyword (`light `, `normal`, ...) map it to a number (300, 400) + themeScope.fontWeights[fontWeight] ?? + // If font weight is already number (300, 400) + fontWeight + ); +}; diff --git a/packages/style-system/src/utils/types.ts b/packages/style-system/src/utils/types.ts new file mode 100644 index 00000000..b29ad0a6 --- /dev/null +++ b/packages/style-system/src/utils/types.ts @@ -0,0 +1,5 @@ +export type ResponsiveValue = T | Record | T[]; + +export type Dict = { [key: string]: T }; + +export type Transform = (value: any, themeScope: { [key: string]: any }) => any; diff --git a/packages/style-system/tsconfig.json b/packages/style-system/tsconfig.json new file mode 100644 index 00000000..4445f0d4 --- /dev/null +++ b/packages/style-system/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src"], + "compilerOptions": { + "outDir": "./dist/types" + } +} diff --git a/packages/theme/package.json b/packages/theme/package.json new file mode 100644 index 00000000..af2caa02 --- /dev/null +++ b/packages/theme/package.json @@ -0,0 +1,42 @@ +{ + "name": "@ficus-ui/theme", + "version": "1.0.0", + "scripts": { + "dev": "tsc --watch", + "test": "echo \"Error: no test specified\" && exit 1", + "build": "tsc" + }, + "dependencies": { + "@ficus-ui/style-system": "workspace:*", + "deepmerge": "4.2.2" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "types": "dist/types/src/index.d.ts", + "main": "dist/types/src/index.js", + "module": "dist/types/src/index.js", + "exports": { + ".": { + "import": { + "types": "./dist/types/index.d.ts", + "default": "./dist/esm/index.mjs" + }, + "require": { + "types": "./dist/types/index.d.ts", + "default": "./dist/cjs/index.cjs" + } + }, + "./config": { + "import": { + "types": "./dist/types/config/index.d.ts", + "default": "./dist/esm/config/index.mjs" + }, + "require": { + "types": "./dist/types/config/index.d.ts", + "default": "./dist/cjs/config/index.cjs" + } + } + } +} diff --git a/packages/theme/src/components/badge.ts b/packages/theme/src/components/badge.ts new file mode 100644 index 00000000..a1dd6591 --- /dev/null +++ b/packages/theme/src/components/badge.ts @@ -0,0 +1 @@ +export const badgeTheme = {}; diff --git a/packages/theme/src/components/index.ts b/packages/theme/src/components/index.ts new file mode 100644 index 00000000..67615dd8 --- /dev/null +++ b/packages/theme/src/components/index.ts @@ -0,0 +1,7 @@ +import { badgeTheme } from './badge'; + +export { badgeTheme as Badge } from './badge'; + +export const components = { + Badge: badgeTheme, +}; diff --git a/packages/theme/src/context.ts b/packages/theme/src/context.ts new file mode 100644 index 00000000..66b4f867 --- /dev/null +++ b/packages/theme/src/context.ts @@ -0,0 +1,25 @@ +import { createContext } from 'react'; + +import { theme } from './theme.default'; +import { FicusTheme } from './theme.types'; + +export interface FicusThemeWithMetadata extends FicusTheme { + /** + * Current device window width. Useful for responsive layouts + */ + __windowWidth?: number; + /** + * Breakpoints from the theme ordered by ascending size + */ + __breakpoints?: [string, number][]; +} + +export interface ThemeContext { + theme: FicusThemeWithMetadata; + setTheme: (theme: FicusThemeWithMetadata) => void; +} + +export const ThemeContext = createContext({ + theme: Object.assign(theme, { __windowWidth: 0 }), + setTheme: (_theme: FicusThemeWithMetadata) => {}, +}); diff --git a/packages/theme/src/foundations/breakpoints.ts b/packages/theme/src/foundations/breakpoints.ts new file mode 100644 index 00000000..8ccea1a8 --- /dev/null +++ b/packages/theme/src/foundations/breakpoints.ts @@ -0,0 +1,9 @@ +const breakpoints = { + base: 0, + sm: 480, + md: 768, + lg: 992, + xl: 1280, +}; + +export default breakpoints; diff --git a/packages/theme/src/foundations/colors.ts b/packages/theme/src/foundations/colors.ts new file mode 100644 index 00000000..c831c259 --- /dev/null +++ b/packages/theme/src/foundations/colors.ts @@ -0,0 +1,164 @@ +const colors = { + transparent: 'transparent', + current: 'currentColor', + black: '#000000', + white: '#FFFFFF', + + whiteAlpha: { + 50: 'rgba(255, 255, 255, 0.04)', + 100: 'rgba(255, 255, 255, 0.06)', + 200: 'rgba(255, 255, 255, 0.08)', + 300: 'rgba(255, 255, 255, 0.16)', + 400: 'rgba(255, 255, 255, 0.24)', + 500: 'rgba(255, 255, 255, 0.36)', + 600: 'rgba(255, 255, 255, 0.48)', + 700: 'rgba(255, 255, 255, 0.64)', + 800: 'rgba(255, 255, 255, 0.80)', + 900: 'rgba(255, 255, 255, 0.92)', + }, + + blackAlpha: { + 50: 'rgba(0, 0, 0, 0.04)', + 100: 'rgba(0, 0, 0, 0.06)', + 200: 'rgba(0, 0, 0, 0.08)', + 300: 'rgba(0, 0, 0, 0.16)', + 400: 'rgba(0, 0, 0, 0.24)', + 500: 'rgba(0, 0, 0, 0.36)', + 600: 'rgba(0, 0, 0, 0.48)', + 700: 'rgba(0, 0, 0, 0.64)', + 800: 'rgba(0, 0, 0, 0.80)', + 900: 'rgba(0, 0, 0, 0.92)', + }, + + gray: { + 50: '#F7FAFC', + 100: '#EDF2F7', + 200: '#E2E8F0', + 300: '#CBD5E0', + 400: '#A0AEC0', + 500: '#718096', + 600: '#4A5568', + 700: '#2D3748', + 800: '#1A202C', + 900: '#171923', + }, + + red: { + 50: '#FFF5F5', + 100: '#FED7D7', + 200: '#FEB2B2', + 300: '#FC8181', + 400: '#F56565', + 500: '#E53E3E', + 600: '#C53030', + 700: '#9B2C2C', + 800: '#822727', + 900: '#63171B', + }, + + orange: { + 50: '#FFFAF0', + 100: '#FEEBC8', + 200: '#FBD38D', + 300: '#F6AD55', + 400: '#ED8936', + 500: '#DD6B20', + 600: '#C05621', + 700: '#9C4221', + 800: '#7B341E', + 900: '#652B19', + }, + + yellow: { + 50: '#FFFFF0', + 100: '#FEFCBF', + 200: '#FAF089', + 300: '#F6E05E', + 400: '#ECC94B', + 500: '#D69E2E', + 600: '#B7791F', + 700: '#975A16', + 800: '#744210', + 900: '#5F370E', + }, + + green: { + 50: '#F0FFF4', + 100: '#C6F6D5', + 200: '#9AE6B4', + 300: '#68D391', + 400: '#48BB78', + 500: '#38A169', + 600: '#2F855A', + 700: '#276749', + 800: '#22543D', + 900: '#1C4532', + }, + + teal: { + 50: '#E6FFFA', + 100: '#B2F5EA', + 200: '#81E6D9', + 300: '#4FD1C5', + 400: '#38B2AC', + 500: '#319795', + 600: '#2C7A7B', + 700: '#285E61', + 800: '#234E52', + 900: '#1D4044', + }, + + blue: { + 50: '#ebf8ff', + 100: '#bee3f8', + 200: '#90cdf4', + 300: '#63b3ed', + 400: '#4299e1', + 500: '#3182ce', + 600: '#2b6cb0', + 700: '#2c5282', + 800: '#2a4365', + 900: '#1A365D', + }, + + cyan: { + 50: '#EDFDFD', + 100: '#C4F1F9', + 200: '#9DECF9', + 300: '#76E4F7', + 400: '#0BC5EA', + 500: '#00B5D8', + 600: '#00A3C4', + 700: '#0987A0', + 800: '#086F83', + 900: '#065666', + }, + + purple: { + 50: '#FAF5FF', + 100: '#E9D8FD', + 200: '#D6BCFA', + 300: '#B794F4', + 400: '#9F7AEA', + 500: '#805AD5', + 600: '#6B46C1', + 700: '#553C9A', + 800: '#44337A', + 900: '#322659', + }, + + pink: { + 50: '#FFF5F7', + 100: '#FED7E2', + 200: '#FBB6CE', + 300: '#F687B3', + 400: '#ED64A6', + 500: '#D53F8C', + 600: '#B83280', + 700: '#97266D', + 800: '#702459', + 900: '#521B41', + }, +}; + +export default colors; diff --git a/packages/theme/src/foundations/index.ts b/packages/theme/src/foundations/index.ts new file mode 100644 index 00000000..1c0baf6f --- /dev/null +++ b/packages/theme/src/foundations/index.ts @@ -0,0 +1,15 @@ +import breakpoints from './breakpoints'; +import colors from './colors'; +import radius from './radius'; +import shadows from './shadows'; +import space from './space'; +import typography from './typography'; + +export const foundations = { + breakpoints, + colors, + radius, + shadows, + space, + ...typography, +}; diff --git a/packages/theme/src/foundations/radius.ts b/packages/theme/src/foundations/radius.ts new file mode 100644 index 00000000..db05ba9b --- /dev/null +++ b/packages/theme/src/foundations/radius.ts @@ -0,0 +1,12 @@ +const radius = { + none: 0, + xs: 2, + sm: 4, + md: 6, + lg: 8, + xl: 12, + '2xl': 16, + full: 99999, +}; + +export default radius; diff --git a/packages/theme/src/foundations/shadows.ts b/packages/theme/src/foundations/shadows.ts new file mode 100644 index 00000000..00f9a5f3 --- /dev/null +++ b/packages/theme/src/foundations/shadows.ts @@ -0,0 +1,71 @@ +const shadows = { + none: {}, + xs: { + shadowColor: '#000000', + shadowOffset: { + width: 0, + height: 1, + }, + shadowOpacity: 0.18, + shadowRadius: 1.0, + + elevation: 1, + }, + sm: { + shadowColor: '#000000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.23, + shadowRadius: 2.62, + + elevation: 4, + }, + md: { + shadowColor: '#000000', + shadowOffset: { + width: 0, + height: 4, + }, + shadowOpacity: 0.3, + shadowRadius: 4.65, + + elevation: 8, + }, + lg: { + shadowColor: '#000000', + shadowOffset: { + width: 0, + height: 6, + }, + shadowOpacity: 0.37, + shadowRadius: 7.49, + + elevation: 12, + }, + xl: { + shadowColor: '#000000', + shadowOffset: { + width: 0, + height: 8, + }, + shadowOpacity: 0.44, + shadowRadius: 10.32, + + elevation: 16, + }, + '2xl': { + shadowColor: '#000000', + shadowOffset: { + width: 0, + height: 10, + }, + shadowOpacity: 0.51, + shadowRadius: 13.16, + + elevation: 20, + }, +}; + +export default shadows; diff --git a/packages/theme/src/foundations/space.ts b/packages/theme/src/foundations/space.ts new file mode 100644 index 00000000..13e7cead --- /dev/null +++ b/packages/theme/src/foundations/space.ts @@ -0,0 +1,19 @@ +const space = { + none: 0, + xs: 4, + sm: 6, + md: 8, + lg: 12, + xl: 24, + '2xl': 32, + '3xl': 64, + '-xs': -4, + '-sm': -6, + '-md': -8, + '-lg': -12, + '-xl': -24, + '-2xl': -32, + '-3xl': -64, +}; + +export default space; diff --git a/packages/theme/src/foundations/typography.ts b/packages/theme/src/foundations/typography.ts new file mode 100644 index 00000000..bc651d40 --- /dev/null +++ b/packages/theme/src/foundations/typography.ts @@ -0,0 +1,31 @@ +const typography = { + fontSizes: { + xs: 11, + sm: 12, + md: 13, + lg: 15, + xl: 17, + '2xl': 19, + '3xl': 21, + '4xl': 24, + '5xl': 27, + '6xl': 32, + }, + + fonts: { + fontWeights: { + hairline: 100, + thin: 200, + light: 300, + normal: 400, + medium: 500, + semibold: 600, + bold: 700, + extrabold: 800, + black: 900, + }, + fontFamily: {}, + }, +}; + +export default typography; diff --git a/packages/theme/src/hook.ts b/packages/theme/src/hook.ts new file mode 100644 index 00000000..fc322ee2 --- /dev/null +++ b/packages/theme/src/hook.ts @@ -0,0 +1,13 @@ +import { useContext } from 'react'; + +import { ThemeContext } from './context'; + +export const useTheme = () => { + const themeContext = useContext(ThemeContext); + + if (!themeContext) { + throw new Error('useTheme must be used within ficus ThemeProvider'); + } + + return themeContext; +}; diff --git a/packages/theme/src/index.ts b/packages/theme/src/index.ts new file mode 100644 index 00000000..351bbf91 --- /dev/null +++ b/packages/theme/src/index.ts @@ -0,0 +1,3 @@ +export { theme, type Theme } from './theme.default'; +export { ThemeProvider } from './provider'; +export * from './hook'; diff --git a/packages/theme/src/provider.tsx b/packages/theme/src/provider.tsx new file mode 100644 index 00000000..cdd9fac4 --- /dev/null +++ b/packages/theme/src/provider.tsx @@ -0,0 +1,48 @@ +import { FC, ReactNode, useMemo, useState } from 'react'; + +import deepmerge from 'deepmerge'; +import { useWindowDimensions } from 'react-native'; + +import { FicusThemeWithMetadata, ThemeContext } from './context'; +import { theme } from './theme.default'; + +export interface ThemeProviderProps { + theme?: FicusThemeWithMetadata; + children: ReactNode; +} + +export const ThemeProvider: FC = (props) => { + const { theme: customTheme = {}, children } = props; + + // Get Metadata + const { width: windowWidth } = useWindowDimensions(); + const breakpoints = theme?.breakpoints + ? Object.entries(theme.breakpoints).sort((a, b) => a[1] - b[1]) + : []; + + // Add metadata to the theme + const themeWithMetadata: FicusThemeWithMetadata = Object.assign(theme, { + __windowWidth: windowWidth, + __breakpoints: breakpoints, + }); + + const [themeState, setThemeState] = useState( + deepmerge(themeWithMetadata, customTheme) + ); + + const setTheme = (newTheme: FicusThemeWithMetadata) => { + const mergedTheme = deepmerge(themeWithMetadata, newTheme); + setThemeState(mergedTheme); + }; + + const contextValue = useMemo( + () => ({ theme: themeState, setTheme }), + [themeWithMetadata] + ); + + return ( + + {children} + + ); +}; diff --git a/packages/theme/src/theme.default.ts b/packages/theme/src/theme.default.ts new file mode 100644 index 00000000..a2e3935d --- /dev/null +++ b/packages/theme/src/theme.default.ts @@ -0,0 +1,9 @@ +import { components } from './components'; +import { foundations } from './foundations'; + +export const theme = { + components, + ...foundations, +}; + +export type Theme = typeof theme; diff --git a/packages/theme/src/theme.types.ts b/packages/theme/src/theme.types.ts new file mode 100644 index 00000000..5d12f3ef --- /dev/null +++ b/packages/theme/src/theme.types.ts @@ -0,0 +1,62 @@ +import type { + SystemStyleInterpolation, + ThemingProps, +} from '@ficus-ui/style-system'; + +type Dict = Record; + +export type RecursiveProperty = RecursiveObject | T; + +export interface RecursiveObject { + [property: string]: RecursiveProperty; +} + +export interface ColorHues { + 50: string; + 100: string; + 200: string; + 300: string; + 400: string; + 500: string; + 600: string; + 700: string; + 800: string; + 900: string; + 950: string; +} + +export type Colors = RecursiveObject> | string>; + +export interface ComponentDefaultProps extends ThemingProps, Dict {} + +export interface ComponentStyleConfig { + baseStyle?: SystemStyleInterpolation; + sizes?: Dict; + variants?: Dict; + defaultProps?: any; +} + +export interface ThemeComponents { + [componentName: string]: ComponentStyleConfig; +} + +interface Typography { + fontSizes?: RecursiveObject; + // FIXME + fonts?: { + fontWeights?: RecursiveObject; + } & RecursiveObject; +} + +interface Foundations extends Typography { + borders?: RecursiveObject; + breakpoints?: Dict; + colors?: Colors; + radius?: RecursiveObject; + shadows?: RecursiveObject; + space?: RecursiveObject; +} + +export interface FicusTheme extends Foundations { + components?: ThemeComponents; +} diff --git a/packages/theme/tsconfig.json b/packages/theme/tsconfig.json new file mode 100644 index 00000000..4445f0d4 --- /dev/null +++ b/packages/theme/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src"], + "compilerOptions": { + "outDir": "./dist/types" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e062480f..7bd0a258 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -181,6 +181,9 @@ importers: '@expo/vector-icons': specifier: 14.0.2 version: 14.0.2 + '@ficus-ui/native': + specifier: workspace:* + version: link:../../packages/components '@react-native-community/slider': specifier: 4.5.4 version: 4.5.4 @@ -291,6 +294,18 @@ importers: specifier: 5.3.3 version: 5.3.3 + packages/components: + dependencies: + '@chakra-ui/utils': + specifier: 2.2.2 + version: 2.2.2(react@18.3.1) + '@ficus-ui/style-system': + specifier: workspace:* + version: link:../style-system + '@ficus-ui/theme': + specifier: workspace:* + version: link:../theme + packages/react-native-ficus-ui: dependencies: color: @@ -400,6 +415,21 @@ importers: specifier: 5.1.6 version: 5.1.6 + packages/style-system: + dependencies: + '@chakra-ui/utils': + specifier: 2.2.2 + version: 2.2.2(react@18.3.1) + + packages/theme: + dependencies: + '@ficus-ui/style-system': + specifier: workspace:* + version: link:../style-system + deepmerge: + specifier: 4.2.2 + version: 4.2.2 + packages: '@alloc/quick-lru@5.2.0': @@ -1188,6 +1218,11 @@ packages: '@braintree/sanitize-url@6.0.4': resolution: {integrity: sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==} + '@chakra-ui/utils@2.2.2': + resolution: {integrity: sha512-jUPLT0JzRMWxpdzH6c+t0YMJYrvc5CLericgITV3zDSXblkfx3DsYXqU11DJTSGZI9dUKzM1Wd0Wswn4eJwvFQ==} + peerDependencies: + react: 18.3.1 + '@commitlint/cli@17.8.1': resolution: {integrity: sha512-ay+WbzQesE0Rv4EQKfNbSMiJJ12KdKTDzIt0tcK4k11FdsWmtwP0Kp1NWMOUswfIWo6Eb7p7Ln721Nx9FLNBjg==} engines: {node: '>=v14'} @@ -2245,6 +2280,12 @@ packages: '@types/keyv@3.1.4': resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + '@types/lodash.mergewith@4.6.9': + resolution: {integrity: sha512-fgkoCAOF47K7sxrQ7Mlud2TH023itugZs2bUg8h/KzT+BnZNrR2jAOmaokbLunHNnobXVWOezAeNn/lZqwxkcw==} + + '@types/lodash@4.17.15': + resolution: {integrity: sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==} + '@types/mdast@3.0.15': resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==} @@ -9969,6 +10010,12 @@ snapshots: '@braintree/sanitize-url@6.0.4': {} + '@chakra-ui/utils@2.2.2(react@18.3.1)': + dependencies: + '@types/lodash.mergewith': 4.6.9 + lodash.mergewith: 4.6.2 + react: 18.3.1 + '@commitlint/cli@17.8.1': dependencies: '@commitlint/format': 17.8.1 @@ -10031,7 +10078,7 @@ snapshots: '@types/node': 20.5.1 chalk: 4.1.2 cosmiconfig: 8.3.6(typescript@5.3.3) - cosmiconfig-typescript-loader: 4.4.0(@types/node@20.5.1)(cosmiconfig@8.3.6(typescript@5.3.3))(ts-node@10.9.2(@types/node@22.13.1)(typescript@5.1.6))(typescript@5.3.3) + cosmiconfig-typescript-loader: 4.4.0(@types/node@20.5.1)(cosmiconfig@8.3.6(typescript@5.1.6))(ts-node@10.9.2(@types/node@22.13.1)(typescript@5.1.6))(typescript@5.3.3) lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 lodash.uniq: 4.5.0 @@ -11806,6 +11853,12 @@ snapshots: dependencies: '@types/node': 22.13.1 + '@types/lodash.mergewith@4.6.9': + dependencies: + '@types/lodash': 4.17.15 + + '@types/lodash@4.17.15': {} + '@types/mdast@3.0.15': dependencies: '@types/unist': 2.0.11 @@ -13210,7 +13263,7 @@ snapshots: dependencies: layout-base: 1.0.2 - cosmiconfig-typescript-loader@4.4.0(@types/node@20.5.1)(cosmiconfig@8.3.6(typescript@5.3.3))(ts-node@10.9.2(@types/node@22.13.1)(typescript@5.1.6))(typescript@5.3.3): + cosmiconfig-typescript-loader@4.4.0(@types/node@20.5.1)(cosmiconfig@8.3.6(typescript@5.1.6))(ts-node@10.9.2(@types/node@22.13.1)(typescript@5.1.6))(typescript@5.3.3): dependencies: '@types/node': 20.5.1 cosmiconfig: 8.3.6(typescript@5.3.3) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 49987984..9ac09c98 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,3 @@ packages: - - "packages/*" - - "apps/*" \ No newline at end of file + - "packages/**/**" + - "apps/**" diff --git a/tsconfig.json b/tsconfig.json index 5fea11b5..d08d05d4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,5 +28,5 @@ "types": ["node", "jest"] }, "include": ["packages", "__tests__"], - "exclude": ["node_modules", "apps"] + "exclude": ["node_modules", "apps", "dist"] }