diff --git a/packages/components/package.json b/packages/components/package.json index ce332d1b..df9425af 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -9,7 +9,8 @@ "dependencies": { "@chakra-ui/utils": "2.2.2", "@ficus-ui/style-system": "workspace:*", - "@ficus-ui/theme": "workspace:*" + "@ficus-ui/theme": "workspace:*", + "react-fast-compare": "3.2.2" }, "keywords": [], "author": "", diff --git a/packages/components/src/system/base-elements.ts b/packages/components/src/system/base-elements.ts index a0cc63fa..a48e7320 100644 --- a/packages/components/src/system/base-elements.ts +++ b/packages/components/src/system/base-elements.ts @@ -1,6 +1,5 @@ import { Image as RNImage, - Pressable as RNPressable, ScrollView as RNScrollView, Text as RNText, View as RNView, @@ -14,11 +13,6 @@ export const baseRNElements = { 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/forward-ref.ts b/packages/components/src/system/forward-ref.ts index 78b1558f..5bf37931 100644 --- a/packages/components/src/system/forward-ref.ts +++ b/packages/components/src/system/forward-ref.ts @@ -24,6 +24,6 @@ export function forwardRef< // @ts-ignore return forwardReactRef(component) as unknown as ComponentWithAs< Component, - Props + Omit >; } diff --git a/packages/components/src/system/index.ts b/packages/components/src/system/index.ts index 253ec725..8a324bd1 100644 --- a/packages/components/src/system/index.ts +++ b/packages/components/src/system/index.ts @@ -1,3 +1,4 @@ export * from './system'; export * from './forward-ref'; export * from './factory'; +export * from './use-style-config'; diff --git a/packages/components/src/system/system.types.ts b/packages/components/src/system/system.types.ts index 3a9753e3..9024439c 100644 --- a/packages/components/src/system/system.types.ts +++ b/packages/components/src/system/system.types.ts @@ -15,27 +15,24 @@ export type FicusProps = SystemProps & { }; export type SystemProps = T extends 'Text' - ? BaseSystemProps // Ensure BaseSystemProps always gets a type argument - : BaseSystemProps>; // Provide a default type argument + ? BaseSystemProps + : BaseSystemProps; 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 = {}, @@ -52,7 +49,6 @@ export type MergeWithAs< | RightJoinProps ) & { as?: AsComponent }; -// Component type with `as` prop for dynamic component rendering export type ComponentWithAs< Component extends RNElementType, Props extends object = {}, diff --git a/packages/components/src/system/system.utils.ts b/packages/components/src/system/system.utils.ts index 4fb4b7b5..20573025 100644 --- a/packages/components/src/system/system.utils.ts +++ b/packages/components/src/system/system.utils.ts @@ -1,23 +1,13 @@ -import { - type BaseRNElementProps, - type BaseRNElements, - baseRNElements, -} from './base-elements'; - -/** - * To allow user to extend custom component with ficus properties - */ -type CustomNativeElementProps> = - React.ComponentProps; +import { type BaseRNElements, baseRNElements } from './base-elements'; export type RNElementType = BaseRNElements | React.ComponentType; export type NativeElementProps = - T extends BaseRNElements - ? BaseRNElementProps - : T extends React.ComponentType - ? CustomNativeElementProps - : never; + React.ComponentPropsWithRef< + T extends BaseRNElements + ? (typeof baseRNElements)[T] + : React.ComponentType + >; export function getComponent(component: T) { return typeof component === 'string' diff --git a/packages/components/src/system/use-style-config.ts b/packages/components/src/system/use-style-config.ts new file mode 100644 index 00000000..9146aa57 --- /dev/null +++ b/packages/components/src/system/use-style-config.ts @@ -0,0 +1,73 @@ +import { useRef } from 'react'; + +import { compact, get, mergeWith, omit } from '@chakra-ui/utils'; +import { + Dict, + type SystemStyleObject, + type ThemingProps, + resolveStyleConfig, +} from '@ficus-ui/style-system'; +import { useTheme } from '@ficus-ui/theme'; +import isEqual from 'react-fast-compare'; + +type StylesRef = SystemStyleObject | Record; + +function useStyleConfigFn( + themeKey: string | null, + props: ThemingProps & Record = {} +) { + const { styleConfig: styleConfigProp, ...rest } = props; + + const { theme } = useTheme(); + + const themeStyleConfig = themeKey + ? get(theme, `components.${themeKey}`) + : undefined; + + const styleConfig = styleConfigProp || themeStyleConfig; + + const mergedProps = mergeWith( + { theme }, + styleConfig?.defaultProps ?? {}, + compact(omit(rest, ['children'])), + (obj, src) => (!obj ? src : undefined) + ); + + /** + * Store the computed styles in a `ref` to avoid unneeded re-computation + */ + const stylesRef = useRef({}); + + if (styleConfig) { + const getStyles = resolveStyleConfig(styleConfig); + const styles = getStyles(mergedProps); + + const isStyleEqual = isEqual(stylesRef.current, styles); + + if (!isStyleEqual) { + stylesRef.current = styles; + } + } + + return stylesRef.current; +} + +/** + * Allow to resolve style config from the theme and merge it with props passed by the user. + */ +export function useStyleConfig( + themeKey: string, + props: ThemingProps & Record = {} +) { + return useStyleConfigFn(themeKey, props) as SystemStyleObject; +} + +/** + * Allow to resolve style config for multi parts component + */ +export function useMultiStyleConfig( + themeKey: string, + props: ThemingProps & Dict = {} +) { + return useStyleConfigFn(themeKey, props) as Record; +} diff --git a/packages/style-system/src/config/colors.ts b/packages/style-system/src/config/colors.ts index 0d8a2683..0f69cd86 100644 --- a/packages/style-system/src/config/colors.ts +++ b/packages/style-system/src/config/colors.ts @@ -3,14 +3,12 @@ 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/flexbox.ts b/packages/style-system/src/config/flexbox.ts index 08f71a8a..6fd2a497 100644 --- a/packages/style-system/src/config/flexbox.ts +++ b/packages/style-system/src/config/flexbox.ts @@ -18,7 +18,7 @@ export const flexbox: Config = { shrink: t.flexbox('flexShrink'), justifyContent: true, justify: t.flexbox('justifyContent'), - alignSelf: t.flexbox('alignItems'), + alignSelf: t.flexbox('alignSelf'), alignItems: true, align: t.flexbox('alignItems'), }; diff --git a/packages/style-system/src/config/text.ts b/packages/style-system/src/config/text.ts index d538f67a..1e3d9a2e 100644 --- a/packages/style-system/src/config/text.ts +++ b/packages/style-system/src/config/text.ts @@ -6,6 +6,7 @@ import { transforms } from '../utils/transform-functions'; import { ResponsiveValue } from '../utils/types'; export const text: Config = { + textColor: t.colors('color'), fontSize: t.prop('fontSize', 'fontSizes', transforms.getThemeProp), textDecorLine: t.prop('textDecorationLine'), textDecorStyle: t.prop('textDecorationStyle'), @@ -34,6 +35,7 @@ export const text: Config = { * Only for React native Text Component */ export interface TextStyleProps { + textColor?: string; fontSize?: ResponsiveValue; textDecorLine?: ResponsiveValue; textDecorStyle?: ResponsiveValue; diff --git a/packages/style-system/src/define-style.ts b/packages/style-system/src/define-style.ts index 7afbc0fb..48624f4b 100644 --- a/packages/style-system/src/define-style.ts +++ b/packages/style-system/src/define-style.ts @@ -26,14 +26,14 @@ export function defineStyle(styles: T) { type DefaultProps = { size?: string; variant?: string; - colorScheme: string; + colorScheme?: string; }; export type StyleConfig = { baseStyle?: SystemStyleInterpolation; sizes?: { [size: string]: SystemStyleInterpolation }; variants?: { [variant: string]: SystemStyleInterpolation }; - defaultProps: DefaultProps; + defaultProps?: DefaultProps; }; export function defineStyleConfig< @@ -42,8 +42,8 @@ export function defineStyleConfig< Variants extends Dict, >(config: { baseStyle?: BaseStyle; - sizes: Sizes; - variants: Variants; + sizes?: Sizes; + variants?: Variants; defaultProps?: { size?: keyof Sizes; variant?: keyof Variants; @@ -54,3 +54,59 @@ export function defineStyleConfig< } // ------------------------------------------------------------------ // + +type Anatomy = string[]; + +export type PartsStyleObject = Partial< + Record +>; + +export type PartsStyleFunction = ( + props: StyleFunctionProps +) => PartsStyleObject; + +export type PartsStyleInterpolation = + | PartsStyleObject + | PartsStyleFunction; + +export interface MultiStyleConfig { + parts: Parts['keys']; + baseStyle?: PartsStyleInterpolation; + sizes?: { [size: string]: PartsStyleInterpolation }; + variants?: { [variant: string]: PartsStyleInterpolation }; + defaultProps?: DefaultProps; +} + +// ------------------------------------------------------------------ // + +/** + * Returns an object of helpers that can be used to define + * the style configuration for a multi-part component. + */ +export function createMultiStyleConfigHelpers( + parts: Part[] | Readonly +) { + return { + definePartsStyle>( + config: PartStyles + ) { + return config; + }, + defineMultiStyleConfig< + BaseStyle extends PartsStyleInterpolation, + Sizes extends Dict>, + Variants extends Dict>, + >(config: { + baseStyle?: BaseStyle; + sizes?: Sizes; + variants?: Variants; + defaultProps?: { + size?: keyof Sizes; + variant?: keyof Variants; + colorScheme?: string; + }; + }) { + return { parts: parts as Part[], ...config }; + }, + }; +} diff --git a/packages/style-system/src/index.ts b/packages/style-system/src/index.ts index fa11ee69..646a4618 100644 --- a/packages/style-system/src/index.ts +++ b/packages/style-system/src/index.ts @@ -6,3 +6,4 @@ export * from './define-style'; export * from './style-sheet'; export * from './utils'; export * from './config'; +export * from './style-config'; diff --git a/packages/style-system/src/style-config.ts b/packages/style-system/src/style-config.ts new file mode 100644 index 00000000..77fcb38f --- /dev/null +++ b/packages/style-system/src/style-config.ts @@ -0,0 +1,95 @@ +import { isObject, mergeWith, runIfFn } from '@chakra-ui/utils'; + +import { Dict, ResponsiveValue } from './utils'; +import { expandResponsive } from './utils'; + +type Theme = Dict; + +type ValueType = ResponsiveValue; + +type Config = { + parts?: string[]; + baseStyle?: Record; + variants?: Record; + sizes?: Record; +}; + +function normalize(value: any) { + if (Array.isArray(value)) { + return value; + } + + if (isObject(value)) { + return Object.values(value); + } + + if (value !== null) { + return [value]; + } +} + +// TODO: Handle multiparts +/** + * `size` and `variant` are special props that are not resolved in the style-system. + * We need to take care of the responsive values here. + */ +function createResolver(theme: Theme) { + return function resolver( + config: Config, + prop: 'variants' | 'sizes', + value: ValueType | undefined, + props: Record + ) { + const result: Record = {}; + + const normalized = normalize(value); + + if (!normalized) { + return; + } + + const isMultipart = !!config.parts; + + const propValue = { + [prop]: normalized.map((norm) => config[prop]?.[norm]), + }; + + const _styles = expandResponsive(propValue)(theme); + const styles = runIfFn(_styles[prop], props); + + if (!styles) { + return; + } + + if (isMultipart) { + config.parts?.forEach((part) => { + mergeWith(result, { + [part]: styles[part], + }); + }); + } + + return styles; + }; +} + +type Values = { + theme: Theme; + variant?: ValueType; + size?: ValueType; +}; + +export function resolveStyleConfig(config: Config) { + return (props: Values) => { + const { variant, size, theme, ...rest } = props; + const recipe = createResolver(theme); + + return mergeWith( + {}, + runIfFn(config.baseStyle ?? {}, props), + recipe(config, 'sizes', size, props), + recipe(config, 'variants', variant, props), + rest + ); + }; +} diff --git a/packages/style-system/src/style-sheet.ts b/packages/style-system/src/style-sheet.ts index 368b77ea..a337074b 100644 --- a/packages/style-system/src/style-sheet.ts +++ b/packages/style-system/src/style-sheet.ts @@ -2,8 +2,7 @@ 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'; +import { type Config, type PropConfig, expandResponsive } from './utils'; interface GetStyleSheetOptions { theme: { [key: string]: any }; @@ -16,7 +15,6 @@ interface GetStyleSheetOptions { export function getStyleSheet({ configs = {}, theme }: GetStyleSheetOptions) { const styleSheet = (stylesOrFn: Record) => { const _styles = runIfFn(stylesOrFn, theme); - const styles = expandResponsive(_styles)(theme); let computedStyles: Dict = {}; @@ -30,15 +28,11 @@ export function getStyleSheet({ configs = {}, theme }: GetStyleSheetOptions) { config = { property: key } as PropConfig; } - // if (isObject(value)) { - // computedStyles[key] = computedStyles[key] ?? {}; - // computedStyles[key] = mergeWith( - // {}, - // computedStyles[key], - // styleSheet(value, true) - // ); - // continue; - // } + if (isObject(value)) { + computedStyles[key] = computedStyles[key] ?? {}; + computedStyles = mergeWith({}, computedStyles[key], styleSheet(value)); + continue; + } /** * Add the peer properties to help for computing the value diff --git a/packages/style-system/src/system.ts b/packages/style-system/src/system.ts index a0faadf8..01bc1e1d 100644 --- a/packages/style-system/src/system.ts +++ b/packages/style-system/src/system.ts @@ -1,4 +1,4 @@ -import { mergeWith } from '@chakra-ui/utils'; +import { mergeWith, splitProps } from '@chakra-ui/utils'; import { background, @@ -13,6 +13,7 @@ import { space, text, } from './config'; +import { Dict } from './utils'; export const systemProps = mergeWith( {}, @@ -30,3 +31,13 @@ export const systemProps = mergeWith( ); export const isStyleProp = (prop: string) => prop in systemProps; + +export const isTextProp = (prop: string) => prop in text; + +/** + * In React Native, text styles can only be applied to Text component. + * We sometimes need to split the props to apply them to the proper element. + */ +export function splitTextProps(props: Dict) { + return splitProps(props, isTextProp); +} diff --git a/packages/style-system/src/system.types.ts b/packages/style-system/src/system.types.ts index 36aee5ab..c8e7acb5 100644 --- a/packages/style-system/src/system.types.ts +++ b/packages/style-system/src/system.types.ts @@ -13,8 +13,6 @@ import { 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, @@ -58,11 +56,16 @@ export type RecursiveStyleSheetObject = D & export type SystemStyleObject = RecursiveStyleSheetObject; +type Assign = Omit & U; + /** * We might need to extend SystemProps. * For example for Text */ -export type SystemProps = StyleProps & ExtraProps; +export type SystemProps = Assign< + StyleProps, + ExtraProps +>; /** * Extensible style props diff --git a/packages/style-system/src/utils/index.ts b/packages/style-system/src/utils/index.ts index 3e469513..5bf2f83e 100644 --- a/packages/style-system/src/utils/index.ts +++ b/packages/style-system/src/utils/index.ts @@ -29,3 +29,4 @@ export const t = { export * from './prop-config'; export * from './types'; +export * from './expand-responsive'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7bd0a258..c8e92e43 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -305,6 +305,9 @@ importers: '@ficus-ui/theme': specifier: workspace:* version: link:../theme + react-fast-compare: + specifier: 3.2.2 + version: 3.2.2 packages/react-native-ficus-ui: dependencies: @@ -10078,7 +10081,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.1.6))(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.3.3))(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 @@ -13263,7 +13266,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.1.6))(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.3.3))(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)