From 6352aef1438517e0f869ef04621d1d7c280f07b6 Mon Sep 17 00:00:00 2001 From: Rohit Agrawal Date: Fri, 8 Dec 2023 02:51:27 +0530 Subject: [PATCH] feat: Added typesafety for component configs, added performance optimizations and memoization, fixes issues with the pressable component --- App.tsx | 230 +++++++- demo/assets/avatar.png | Bin 0 -> 5398 bytes demo/assets/badge.png | Bin 0 -> 3778 bytes demo/assets/box.png | Bin 0 -> 2236 bytes demo/assets/button.png | Bin 0 -> 3398 bytes demo/assets/center.png | Bin 0 -> 3089 bytes demo/assets/checkbox.png | Bin 0 -> 3506 bytes demo/assets/divider.png | Bin 0 -> 2358 bytes demo/assets/icon-button.png | Bin 0 -> 5025 bytes demo/assets/icon.png | Bin 0 -> 3694 bytes demo/assets/image.png | Bin 0 -> 4338 bytes demo/assets/input.png | Bin 0 -> 2766 bytes demo/assets/pressable.png | Bin 0 -> 4089 bytes demo/assets/progress.png | Bin 0 -> 2738 bytes demo/assets/radio.png | Bin 0 -> 4134 bytes demo/assets/screen.png | Bin 0 -> 3181 bytes demo/assets/skeleton.png | Bin 0 -> 2926 bytes demo/assets/spacer.png | Bin 0 -> 2413 bytes demo/assets/spinner.png | Bin 0 -> 3455 bytes demo/assets/stack.png | Bin 0 -> 3203 bytes demo/assets/switch.png | Bin 0 -> 4436 bytes demo/assets/text-link.png | Bin 0 -> 3008 bytes demo/assets/text.png | Bin 0 -> 2863 bytes demo/assets/textarea.png | Bin 0 -> 3075 bytes demo/assets/transitions.png | Bin 0 -> 5802 bytes demo/assets/video.png | Bin 0 -> 3433 bytes demo/components/component-card.tsx | 50 ++ {documentation => docs}/.gitignore | 0 {documentation => docs}/babel.config.js | 0 .../docs/components/_category_.json | 0 .../docs/components/feedback/Badge.mdx | 0 .../docs/components/feedback/Progress.mdx | 0 .../docs/components/feedback/Skeleton.mdx | 1 + .../docs/components/feedback/Spinner.mdx | 3 + .../docs/components/feedback/_category_.json | 0 .../docs/components/forms/Button.mdx | 0 .../docs/components/forms/CheckBox.mdx | 0 .../docs/components/forms/Icon Button.mdx | 0 .../docs/components/forms/Input.mdx | 0 .../docs/components/forms/Pressable.mdx | 2 +- .../docs/components/forms/Radio.mdx | 0 .../docs/components/forms/Switch.mdx | 0 .../docs/components/forms/Text Link.mdx | 0 .../docs/components/forms/Textarea.mdx | 0 .../docs/components/forms/_category_.json | 0 .../docs/components/layout/Box.mdx | 1 + .../docs/components/layout/Center.mdx | 0 .../docs/components/layout/Divider.mdx | 1 + .../docs/components/layout/Screen.mdx | 0 .../docs/components/layout/Spacer.mdx | 0 .../docs/components/layout/Stack.mdx | 0 .../docs/components/layout/_category_.json | 0 .../docs/components/media/Avatar.mdx | 0 .../docs/components/media/Icon.mdx | 1 + .../docs/components/media/Image.mdx | 2 +- .../docs/components/media/Video.mdx | 0 .../docs/components/media/_category_.json | 0 .../docs/components/transitions/Collapse.mdx | 0 .../docs/components/transitions/Fade.mdx | 0 .../components/transitions/Scale Fade.mdx | 0 .../components/transitions/Slide Fade.mdx | 0 .../docs/components/transitions/Slide.mdx | 0 .../components/transitions/_category_.json | 0 .../docs/components/typography/Text.mdx | 1 + .../components/typography/_category_.json | 0 .../docs/core-features/_category_.json | 0 .../docs/core-features/animation-support.mdx | 0 .../docs/core-features/dark-mode.mdx | 0 .../docs/core-features/extensibility.mdx | 19 +- .../docs/core-features/responsivity.md | 0 .../docs/core-features/style-props.md | 25 + .../docs/getting-started/_category_.json | 0 .../docs/getting-started/design-principles.md | 0 .../docs/getting-started/installation.md | 0 .../docs/getting-started/introduction.mdx | 0 .../docs/hooks/_category_.json | 0 {documentation => docs}/docs/hooks/pearl.md | 0 .../docs/hooks/useAccessibleColor.md | 0 .../docs/hooks/useAnimationState.md | 0 .../docs/hooks/useAtomicComponentConfig.mdx | 0 .../docs/hooks/useCheckedState.mdx | 0 .../docs/hooks/useColorModeValue.md | 0 .../docs/hooks/useColorScheme.md | 0 .../docs/hooks/useDimensions.mdx | 0 .../docs/hooks/useDisabledState.mdx | 0 .../docs/hooks/useDynamicStateStyle.mdx | 0 .../docs/hooks/useFocusedState.mdx | 0 .../docs/hooks/useInvalidState.mdx | 0 .../hooks/useMolecularComponentConfig.mdx | 0 .../docs/hooks/useMotiWithStyleProps.mdx | 0 .../docs/hooks/usePressedState.mdx | 0 .../docs/hooks/useResponsiveProp.mdx | 0 .../docs/hooks/useStyleProps.mdx | 0 .../docs/hooks/useTheme.md | 0 .../docs/others/_category_.json | 0 .../docs/others/generatePalette.mdx | 0 .../docs/others/style-functions.md | 10 + .../docs/theming/_category_.json | 0 .../docs/theming/customize-theme.md | 0 .../docs/theming/default-theme.mdx | 16 + .../docs/theming/typescript-support.md | 0 {documentation => docs}/docusaurus.config.js | 4 +- {documentation => docs}/package.json | 2 +- {documentation => docs}/sidebars.js | 1 - .../BorderRadiiBox/BorderRadiiBox.module.css | 0 .../BorderRadiiBox/BorderRadiiBox.tsx | 0 .../ElevationBox/ElevationBox.module.css | 0 .../components/ElevationBox/ElevationBox.tsx | 0 .../src/components/ExpoSnack.tsx | 0 .../PaletteColor/PaletteColor.module.css | 0 .../components/PaletteColor/PaletteColor.tsx | 0 .../src/components/Props/Props.module.css | 0 .../src/components/Props/Props.tsx | 0 .../SourceButton/SourceButton.module.css | 0 .../components/SourceButton/SourceButton.tsx | 0 .../src/components/SpacingBox/SpacingBox.tsx | 0 .../TypographyVariant.module.css | 0 .../TypographyVariant/TypographyVariant.tsx | 0 {documentation => docs}/src/css/custom.css | 2 + .../src/pages/index.module.css | 0 {documentation => docs}/src/pages/index.tsx | 0 {documentation => docs}/static/.nojekyll | 0 .../static/img/android_elevation.png | Bin .../static/img/component_styles_icon_dark.png | Bin .../img/component_styles_icon_light.png | Bin .../static/img/discord_black.svg | 0 .../static/img/discord_white.svg | 0 .../static/img/favicon.ico | Bin .../static/img/feature_elevation.png | Bin .../static/img/feature_palette.png | Bin .../static/img/feature_spacing.png | Bin .../static/img/feature_typography.png | Bin .../static/img/ios_elevation.png | Bin {documentation => docs}/static/img/logo.png | Bin .../static/img/logoDark.png | Bin .../static/img/responsivity_phone_demo.png | Bin .../static/img/responsivity_tablet_demo.png | Bin .../static/img/twitter_black.svg | 0 .../static/img/twitter_white.svg | 0 .../static/img/typescript_example.png | Bin {documentation => docs}/tsconfig.json | 0 {documentation => docs}/yarn.lock | 0 package.json | 5 +- src/components/Atoms/Center/Center.tsx | 16 +- .../Atoms/Divider/Divider.config.ts | 9 +- src/components/Atoms/Divider/Divider.tsx | 49 +- src/components/Atoms/Icon/Icon.config.ts | 7 +- src/components/Atoms/Icon/Icon.tsx | 56 +- src/components/Atoms/Pressable/Pressable.tsx | 186 +++--- src/components/Atoms/Screen/Screen.config.ts | 8 +- src/components/Atoms/Screen/Screen.tsx | 188 +++--- src/components/Atoms/Spacer/Spacer.tsx | 17 +- .../Atoms/Spinner/Spinner.config.ts | 83 ++- src/components/Atoms/Spinner/Spinner.tsx | 125 ++-- .../Atoms/Spinner/indicators/activity.tsx | 154 ++--- .../Atoms/Spinner/indicators/ball.tsx | 176 +++--- .../Atoms/Spinner/indicators/bar.tsx | 226 +++---- .../Atoms/Spinner/indicators/dot.tsx | 121 ++-- .../Atoms/Spinner/indicators/indicator.tsx | 84 +-- .../Atoms/Spinner/indicators/material.tsx | 290 ++++----- .../Atoms/Spinner/indicators/pacman.tsx | 269 ++++----- .../Atoms/Spinner/indicators/pulse.tsx | 128 ++-- .../Atoms/Spinner/indicators/skype.tsx | 183 +++--- .../Atoms/Spinner/indicators/wave.tsx | 157 ++--- src/components/Atoms/Stack/Stack.tsx | 226 +++---- src/components/Atoms/Text/Text.config.ts | 7 +- src/components/Atoms/Text/Text.tsx | 60 +- .../Molecules/Avatar/Avatar.config.ts | 13 +- src/components/Molecules/Avatar/Avatar.tsx | 150 ++--- .../Molecules/Badge/Badge.config.ts | 13 +- src/components/Molecules/Badge/Badge.tsx | 66 ++- src/components/Molecules/Badge/withBadge.tsx | 12 +- .../Molecules/Button/Button.config.ts | 61 +- src/components/Molecules/Button/Button.tsx | 246 ++++---- .../Molecules/CheckBox/CheckBox.config.ts | 76 ++- .../Molecules/CheckBox/CheckBox.tsx | 321 +++++----- .../Molecules/Image/Image.config.ts | 17 +- src/components/Molecules/Image/Image.tsx | 549 ++++++++++-------- .../Molecules/Input/Input.config.ts | 53 +- src/components/Molecules/Input/Input.tsx | 403 +++++++------ .../Molecules/Radio/Radio.config.ts | 21 +- src/components/Molecules/Radio/Radio.tsx | 224 ++++--- src/components/atoms/collapse/collapse.tsx | 166 +++--- src/components/atoms/fade/fade.tsx | 112 ++-- .../atoms/scale-fade/scale-fade.tsx | 121 ++-- .../atoms/skeleton/skeleton-circle.tsx | 26 +- .../atoms/skeleton/skeleton-text.tsx | 129 ++-- .../atoms/skeleton/skeleton.config.ts | 7 +- src/components/atoms/skeleton/skeleton.tsx | 80 ++- .../atoms/slide-fade/slide-fade.tsx | 143 ++--- src/components/atoms/slide/slide.tsx | 168 +++--- .../molecules/avatar/avatar-group.tsx | 127 ++-- .../molecules/button/button-group.tsx | 106 ++-- .../molecules/checkbox/checkbox-group.tsx | 85 +-- .../icon-button/icon-button.config.ts | 44 +- .../molecules/icon-button/icon-button.tsx | 112 ++-- .../molecules/image/cache-manager.ts | 54 +- .../molecules/progress/progress.config.ts | 22 +- .../molecules/progress/progress.tsx | 63 +- .../molecules/radio/radio-group.tsx | 84 +-- .../molecules/switch/switch.config.ts | 16 +- src/components/molecules/switch/switch.tsx | 117 ++-- .../molecules/text-link/text-link.config.ts | 19 +- .../molecules/text-link/text-link.tsx | 55 +- .../molecules/textarea/textarea.tsx | 6 +- src/components/molecules/video/video.tsx | 445 +++++++------- src/hooks/useAnimationState.ts | 18 +- src/hooks/useColorScheme.ts | 34 +- src/hooks/useMolecularComponentConfig.ts | 68 ++- src/hooks/useMotiWithStyleProps.ts | 180 ++---- src/hooks/useResponsiveProp.ts | 24 +- src/hooks/useStateWithStyleProps.ts | 29 +- src/hooks/useStyleProps.ts | 26 +- src/pearl.tsx | 132 ++--- src/theme/index.ts | 52 +- src/theme/src/base/border-radii.ts | 1 + src/theme/src/base/components.ts | 24 +- src/theme/src/base/elevation.ts | 10 + src/theme/src/style-functions.ts | 74 ++- src/theme/src/style-properties.ts | 10 + src/theme/src/types.ts | 40 +- tsconfig.json | 2 +- wdyr.js | 8 + yarn.lock | 9 +- 224 files changed, 4635 insertions(+), 3839 deletions(-) create mode 100644 demo/assets/avatar.png create mode 100644 demo/assets/badge.png create mode 100644 demo/assets/box.png create mode 100644 demo/assets/button.png create mode 100644 demo/assets/center.png create mode 100644 demo/assets/checkbox.png create mode 100644 demo/assets/divider.png create mode 100644 demo/assets/icon-button.png create mode 100644 demo/assets/icon.png create mode 100644 demo/assets/image.png create mode 100644 demo/assets/input.png create mode 100644 demo/assets/pressable.png create mode 100644 demo/assets/progress.png create mode 100644 demo/assets/radio.png create mode 100644 demo/assets/screen.png create mode 100644 demo/assets/skeleton.png create mode 100644 demo/assets/spacer.png create mode 100644 demo/assets/spinner.png create mode 100644 demo/assets/stack.png create mode 100644 demo/assets/switch.png create mode 100644 demo/assets/text-link.png create mode 100644 demo/assets/text.png create mode 100644 demo/assets/textarea.png create mode 100644 demo/assets/transitions.png create mode 100644 demo/assets/video.png create mode 100644 demo/components/component-card.tsx rename {documentation => docs}/.gitignore (100%) rename {documentation => docs}/babel.config.js (100%) rename {documentation => docs}/docs/components/_category_.json (100%) rename {documentation => docs}/docs/components/feedback/Badge.mdx (100%) rename {documentation => docs}/docs/components/feedback/Progress.mdx (100%) rename {documentation => docs}/docs/components/feedback/Skeleton.mdx (99%) rename {documentation => docs}/docs/components/feedback/Spinner.mdx (92%) rename {documentation => docs}/docs/components/feedback/_category_.json (100%) rename {documentation => docs}/docs/components/forms/Button.mdx (100%) rename {documentation => docs}/docs/components/forms/CheckBox.mdx (100%) rename {documentation => docs}/docs/components/forms/Icon Button.mdx (100%) rename {documentation => docs}/docs/components/forms/Input.mdx (100%) rename {documentation => docs}/docs/components/forms/Pressable.mdx (96%) rename {documentation => docs}/docs/components/forms/Radio.mdx (100%) rename {documentation => docs}/docs/components/forms/Switch.mdx (100%) rename {documentation => docs}/docs/components/forms/Text Link.mdx (100%) rename {documentation => docs}/docs/components/forms/Textarea.mdx (100%) rename {documentation => docs}/docs/components/forms/_category_.json (100%) rename {documentation => docs}/docs/components/layout/Box.mdx (99%) rename {documentation => docs}/docs/components/layout/Center.mdx (100%) rename {documentation => docs}/docs/components/layout/Divider.mdx (99%) rename {documentation => docs}/docs/components/layout/Screen.mdx (100%) rename {documentation => docs}/docs/components/layout/Spacer.mdx (100%) rename {documentation => docs}/docs/components/layout/Stack.mdx (100%) rename {documentation => docs}/docs/components/layout/_category_.json (100%) rename {documentation => docs}/docs/components/media/Avatar.mdx (100%) rename {documentation => docs}/docs/components/media/Icon.mdx (99%) rename {documentation => docs}/docs/components/media/Image.mdx (98%) rename {documentation => docs}/docs/components/media/Video.mdx (100%) rename {documentation => docs}/docs/components/media/_category_.json (100%) rename {documentation => docs}/docs/components/transitions/Collapse.mdx (100%) rename {documentation => docs}/docs/components/transitions/Fade.mdx (100%) rename {documentation => docs}/docs/components/transitions/Scale Fade.mdx (100%) rename {documentation => docs}/docs/components/transitions/Slide Fade.mdx (100%) rename {documentation => docs}/docs/components/transitions/Slide.mdx (100%) rename {documentation => docs}/docs/components/transitions/_category_.json (100%) rename {documentation => docs}/docs/components/typography/Text.mdx (99%) rename {documentation => docs}/docs/components/typography/_category_.json (100%) rename {documentation => docs}/docs/core-features/_category_.json (100%) rename {documentation => docs}/docs/core-features/animation-support.mdx (100%) rename {documentation => docs}/docs/core-features/dark-mode.mdx (100%) rename {documentation => docs}/docs/core-features/extensibility.mdx (92%) rename {documentation => docs}/docs/core-features/responsivity.md (100%) rename {documentation => docs}/docs/core-features/style-props.md (95%) rename {documentation => docs}/docs/getting-started/_category_.json (100%) rename {documentation => docs}/docs/getting-started/design-principles.md (100%) rename {documentation => docs}/docs/getting-started/installation.md (100%) rename {documentation => docs}/docs/getting-started/introduction.mdx (100%) rename {documentation => docs}/docs/hooks/_category_.json (100%) rename {documentation => docs}/docs/hooks/pearl.md (100%) rename {documentation => docs}/docs/hooks/useAccessibleColor.md (100%) rename {documentation => docs}/docs/hooks/useAnimationState.md (100%) rename {documentation => docs}/docs/hooks/useAtomicComponentConfig.mdx (100%) rename {documentation => docs}/docs/hooks/useCheckedState.mdx (100%) rename {documentation => docs}/docs/hooks/useColorModeValue.md (100%) rename {documentation => docs}/docs/hooks/useColorScheme.md (100%) rename {documentation => docs}/docs/hooks/useDimensions.mdx (100%) rename {documentation => docs}/docs/hooks/useDisabledState.mdx (100%) rename {documentation => docs}/docs/hooks/useDynamicStateStyle.mdx (100%) rename {documentation => docs}/docs/hooks/useFocusedState.mdx (100%) rename {documentation => docs}/docs/hooks/useInvalidState.mdx (100%) rename {documentation => docs}/docs/hooks/useMolecularComponentConfig.mdx (100%) rename {documentation => docs}/docs/hooks/useMotiWithStyleProps.mdx (100%) rename {documentation => docs}/docs/hooks/usePressedState.mdx (100%) rename {documentation => docs}/docs/hooks/useResponsiveProp.mdx (100%) rename {documentation => docs}/docs/hooks/useStyleProps.mdx (100%) rename {documentation => docs}/docs/hooks/useTheme.md (100%) rename {documentation => docs}/docs/others/_category_.json (100%) rename {documentation => docs}/docs/others/generatePalette.mdx (100%) rename {documentation => docs}/docs/others/style-functions.md (94%) rename {documentation => docs}/docs/theming/_category_.json (100%) rename {documentation => docs}/docs/theming/customize-theme.md (100%) rename {documentation => docs}/docs/theming/default-theme.mdx (98%) rename {documentation => docs}/docs/theming/typescript-support.md (100%) rename {documentation => docs}/docusaurus.config.js (97%) rename {documentation => docs}/package.json (98%) rename {documentation => docs}/sidebars.js (99%) rename {documentation => docs}/src/components/BorderRadiiBox/BorderRadiiBox.module.css (100%) rename {documentation => docs}/src/components/BorderRadiiBox/BorderRadiiBox.tsx (100%) rename {documentation => docs}/src/components/ElevationBox/ElevationBox.module.css (100%) rename {documentation => docs}/src/components/ElevationBox/ElevationBox.tsx (100%) rename {documentation => docs}/src/components/ExpoSnack.tsx (100%) rename {documentation => docs}/src/components/PaletteColor/PaletteColor.module.css (100%) rename {documentation => docs}/src/components/PaletteColor/PaletteColor.tsx (100%) rename {documentation => docs}/src/components/Props/Props.module.css (100%) rename {documentation => docs}/src/components/Props/Props.tsx (100%) rename {documentation => docs}/src/components/SourceButton/SourceButton.module.css (100%) rename {documentation => docs}/src/components/SourceButton/SourceButton.tsx (100%) rename {documentation => docs}/src/components/SpacingBox/SpacingBox.tsx (100%) rename {documentation => docs}/src/components/TypographyVariant/TypographyVariant.module.css (100%) rename {documentation => docs}/src/components/TypographyVariant/TypographyVariant.tsx (100%) rename {documentation => docs}/src/css/custom.css (99%) rename {documentation => docs}/src/pages/index.module.css (100%) rename {documentation => docs}/src/pages/index.tsx (100%) rename {documentation => docs}/static/.nojekyll (100%) rename {documentation => docs}/static/img/android_elevation.png (100%) rename {documentation => docs}/static/img/component_styles_icon_dark.png (100%) rename {documentation => docs}/static/img/component_styles_icon_light.png (100%) rename {documentation => docs}/static/img/discord_black.svg (100%) rename {documentation => docs}/static/img/discord_white.svg (100%) rename {documentation => docs}/static/img/favicon.ico (100%) rename {documentation => docs}/static/img/feature_elevation.png (100%) rename {documentation => docs}/static/img/feature_palette.png (100%) rename {documentation => docs}/static/img/feature_spacing.png (100%) rename {documentation => docs}/static/img/feature_typography.png (100%) rename {documentation => docs}/static/img/ios_elevation.png (100%) rename {documentation => docs}/static/img/logo.png (100%) rename {documentation => docs}/static/img/logoDark.png (100%) rename {documentation => docs}/static/img/responsivity_phone_demo.png (100%) rename {documentation => docs}/static/img/responsivity_tablet_demo.png (100%) rename {documentation => docs}/static/img/twitter_black.svg (100%) rename {documentation => docs}/static/img/twitter_white.svg (100%) rename {documentation => docs}/static/img/typescript_example.png (100%) rename {documentation => docs}/tsconfig.json (100%) rename {documentation => docs}/yarn.lock (100%) create mode 100644 wdyr.js diff --git a/App.tsx b/App.tsx index dba4282a..248e0f9f 100644 --- a/App.tsx +++ b/App.tsx @@ -1,3 +1,4 @@ +import "./wdyr"; import React from "react"; import { useFonts, @@ -22,6 +23,8 @@ import { } from "@expo-google-fonts/poppins"; import { ThemeProvider } from "./src/theme/src/theme-context"; import Screen from "./src/components/atoms/screen/screen"; +import Pressable from "./src/components/atoms/pressable/pressable"; +import ComponentCard from "./demo/components/component-card"; import Text from "./src/components/atoms/text/text"; import Icon from "./src/components/atoms/icon/icon"; import Fade from "./src/components/atoms/fade/fade"; @@ -49,9 +52,16 @@ import ButtonGroup from "./src/components/molecules/button/button-group"; import Box from "./src/components/atoms/box/box"; import SkeletonText from "./src/components/atoms/skeleton/skeleton-text"; import { MotiPressable } from "moti/interactions"; -import { Pressable } from "react-native"; -import { ResizeMode } from "expo-av"; +import { + FlatList, + LogBox, + SafeAreaView, + View, + Pressable as RNPressable, + Text as RNText, +} from "react-native"; import { NativeModules } from "react-native"; +import { useTheme } from "./src"; if (__DEV__) { NativeModules.DevSettings.setIsDebuggingRemotely(true); @@ -86,7 +96,111 @@ const App = () => { ); }; +const componentList = [ + { + label: "Box", + imageSrc: require("./demo/assets/box.png"), + }, + { + label: "Center", + imageSrc: require("./demo/assets/center.png"), + }, + { + label: "Screen", + imageSrc: require("./demo/assets/screen.png"), + }, + { + label: "Spacer", + imageSrc: require("./demo/assets/spacer.png"), + }, + { + label: "Stack", + imageSrc: require("./demo/assets/stack.png"), + }, + { + label: "Divider", + imageSrc: require("./demo/assets/divider.png"), + }, + { + label: "Text", + imageSrc: require("./demo/assets/text.png"), + }, + { + label: "Icon", + imageSrc: require("./demo/assets/icon.png"), + }, + { + label: "Image", + imageSrc: require("./demo/assets/image.png"), + }, + { + label: "Avatar", + imageSrc: require("./demo/assets/avatar.png"), + }, + { + label: "Video", + imageSrc: require("./demo/assets/video.png"), + }, + { + label: "Pressable", + imageSrc: require("./demo/assets/pressable.png"), + }, + { + label: "Button", + imageSrc: require("./demo/assets/button.png"), + }, + { + label: "Icon Button", + imageSrc: require("./demo/assets/icon-button.png"), + }, + { + label: "Text Link", + imageSrc: require("./demo/assets/text-link.png"), + }, + { + label: "Input", + imageSrc: require("./demo/assets/input.png"), + }, + { + label: "Switch", + imageSrc: require("./demo/assets/switch.png"), + }, + { + label: "Textarea", + imageSrc: require("./demo/assets/textarea.png"), + }, + { + label: "Checkbox", + imageSrc: require("./demo/assets/checkbox.png"), + }, + { + label: "Radio", + imageSrc: require("./demo/assets/radio.png"), + }, + { + label: "Spinner", + imageSrc: require("./demo/assets/spinner.png"), + }, + { + label: "Skeleton", + imageSrc: require("./demo/assets/skeleton.png"), + }, + { + label: "Badge", + imageSrc: require("./demo/assets/badge.png"), + }, + { + label: "Progress", + imageSrc: require("./demo/assets/progress.png"), + }, + { + label: "Transitions", + imageSrc: require("./demo/assets/transitions.png"), + }, +]; + const Index = () => { + const { colorMode, toggleColorMode } = useTheme(); const [radioGroupValue, setRadioGroupValue] = React.useState("B"); const [checkboxGroupValue, setCheckboxGroupValue] = React.useState(["B"]); const [checked, setChecked] = React.useState(false); @@ -110,31 +224,75 @@ const Index = () => { }, [progress]); return ( - - - setChecked(!checked)} - /> + + + + Pearl UI - Showcase + + } + /> + - setChecked(!checked)} + ( + + )} + keyExtractor={(item) => item.label} + style={{ padding: 10 }} /> + + + ); - setChecked(!checked)} - /> + return ( + + {/* + + + + + - setChecked(!checked)} - /> + setChecked(!checked)} /> { Save - + - alskdnalskndalknsdad + + alskdnalskndalknsdad + - - + + + + Controlled Checkbox group + setCheckboxGroupValue(newVal)} + > + A + B + C + + + */} - + {/* + */} ); }; diff --git a/demo/assets/avatar.png b/demo/assets/avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..16987ef8de8d6a470c2e1ae3568c5b46fc76ddca GIT binary patch literal 5398 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 literal 0 HcmV?d00001 diff --git a/demo/assets/button.png b/demo/assets/button.png new file mode 100644 index 0000000000000000000000000000000000000000..6022aeacceb3d8c7a64241012aa9e6f4cb2d2a47 GIT binary patch literal 3398 zcmeHKSyU4076!v4NlC5F$qE}znahFIH8nMbOeIS#r(-#$M&_KNImOAbG6(D&)2rb? z4yY;4niFJLrYJa*hEsGDQ6!v5K)amtc3$qf5BIT$z4rfqd#(RpYp-vA>)R>UZBYvH zs`3B;K*8#=xjg^?%=|0ByQN4RMZaH~_T0OC69WK1RQ?JeAP@hu6a-@IQDy)V{m7#9 z0`k3Ra}fZj!$BlovH-x|Pgdp^9ixD}spvF!Ct#02w{g<{oGxhB=izst+M(vG*(;jW z$||9LLx{bh73rr{yBfD4p(&@7B1eAD{5QC}{%3E;GsD+dt2 zqFntd_$;r}j?HAxuJtM5)-Q0r%}oC~&HzK<<1zf)`21@;!yAC2t3CSxU@#b<1Oy&C zaSUJ%0>wRv1ME}U2RI=kgEU71p8P2OXu?03@GtV9QLIogtDSFH7C2=}xe10uVDg&QnBm z(ltM_?_cm*zZen|__%P&L;aWuef>bE|3^kJL*@U$_cZvuB-=cfKCy4a%8XrK=%wW1 zk5pVbOP}T3_g*Z2LXq zJ@@QD`GCVf)Uil7>{c&91mG&9D2aF0VrUc7jd-oswuZpyu zi)!93brY=l(DGY69qol79ratV!~BktL5f?7BKA(v5iRkiVvcc$>-Ymnf#Hntmo2RL z5p7(~`8hWBeoh6kA0Bc9ANFBQ57sjMt#n=xpuYE@C`Sprp4(zw;A*#2teFfW+w zj96|(wNz^`-He(fThP?LN!RSGSH`&Sky)Wk9xYF-S`bP9W>0u{C?2V1REn8>$V(#J zX%w)O-d7vw_*<%D7UvdHt=5BPxM688SsZ{h>r4yRA%OE_V?yD>%V+Sn{Id87;p`1V<$iUx&K^zx% zgI0ES=|DI$DdiH{!`B8K{xo&x__0A0ck&(m=+cXxklb@EB|976M5}|lUcEOh5H7#G zux)`X!bvvkZw?rt^u)!QeQ821os>4k)rp#_0Lx=Nom-B@IMY+oSIY1mMavJov6wdx zmr8V@;w9L}+WJ=gFA9kcef>h6U^>faCCLG@ZG((q)g{W&NK?CH6?>L(I`9-!bIqaH z4uP6xUjm~h*b?@A!dTJ)QGha2dJqnrAemvd?RgJm)W(=@LpI#gBlz{)qxs3q7Y`$C zb+Jxkas3ZwekT#B_}MWrsHDdx|3MLpGBNS7qb*zOru{*!8-px=r2hr326>}7VrhKd zo67B-s~*@kw$1%spSnI`Yjm9xlySjir7=?NjEedZt;}*?^wEr;+F&Iffv?fD?9)0< z($Sf&Y;kgC#P$xTJ~ncwX()sItr-TP-Wr8F-$rEH?4Tb}MZ(j8tHVQh+E-uLnK_{j zQuFAeIc8CCJ^h*jJKQ9 zF8q#3b>zmRfZqREHa&RCG-j}d`!>pi=0DvLJusibXu3D|c3$7e-Dxc9f^+&i&U`Ao zj5LB3vBk#4=#w~orOfOKhg}=Z!6UE3`13w}Y~u_Ks`uLlu0~4)zj9-VnWGmlo@P<9 z`=w=(vxw(#2wwjw; zwvM~Yc(tp+rnR*9!!f7EG+sdqRu*cjtKyM`TU;i^?zBw-E6q%&wR`5Ay85v}-`#R@ zw08)R-&)i}J(}SvjH60v`Ix@btxh|HF&*uuo=M7zLl(Bpz6~)DZcL`P9qrZh3wLxt z88_xg@ye5#7=$o+{ubr#(96{;stUuO)NOPcnQ!RC&GRjaqS2M9c3M?rX9EVo!oQC# zA@Y9JgEwS8 zV;E7M6Xc*zAN5aF3=N^0RtkkVR#GkufI%X+qseZfR8M`}QiABYv&T(kJ=n&TO>P2& zauSBU#gd&7(d%W_n_|4YroPT+G_@GCa}@XP4R3rFa8rV{d-yYod_LSPgzY6OEzNwSt-v zj}an6OAW6}#EV-Qha=XHOYPxjEv+A>@P{-0U)O^#l(WP6DLwz{@ z#XRhgQYn)>HnU2ijf6Il?9BJK?_cokhx5bzx?k7(y6@}0Ue|TK?)N*x59^_+4pRq# zK$>2jZUG<=`1;BMRFw#gZ~IcYsh#o+iUWZ()~_rusPOh?B?yiS@W6noy9_=n4V9xV zzAhk84P9d?a<%eQg_oNP;S6|6B*+{d19!|EwY8|eH~A@|2iT*--e}^VQTb(9{DH#H4UyFPFzKKh1j{D<&k znXzELU_j$(hIiXJ^WEZkN9PuYG6Da9;U-W$cK2!!1Oh?3p+Q#w0Hh5DBTNt=Hx(7y z6&eVt4F#F}>-;|zAy`913esaFVqfK6ra#Pmg7X*~Uhu!5ADbeqpB&z4IX}`&GORyY z*Ufo79=@HfAzF}%ALa&z3rR%FE2?&aVN)|4>a@Qnaw`J9D4KqB%emOPz8t>C1cyEG z?Fr2D=*StoBUxZ%#_3#aO=|%>Jw~V}I~Vmgk_gU&y;b`cjS=u9S4Z+Ni$jf}#;u`3 zuVZ^(r-u@`C&n-q)3zN7?ZF^@qbYrYro zz0)F}6JqRcC;VdbbvWjNMS^q(^Q9oGGqllsUidWXM~KCv>85WA$$9>81Jge!%tHEz zzR6sXlEO_n|LL*+h3Ksa9|kNd{fkjv^Zd2W>f%v~bh7Se3oLg*v6L+MvM8`x6?Y_( zkX>TH7MGyHBaQ)psL>I2_B6fIj=fxn^h(8_RnQgGgq|_OL36v{)FMyMRoi99k zH+$W{X%`+_tgEB!R(xqTa$Sm|r?PDC9#iM#*Ccy+e`Qlje8u+g->lC-+TO*Ai7#*^ z;ju@0XZXEUb!|SB+98B8sH&figo^lY4wS~T|@6dgzy|pQ%EuvRMl)>m>eS30}uUT(^v>|$>juN8C*fKwym3&;wSCgP_Y^4=p5e za`MxvSjvY|mbCFfTwgSqD9YiU7k|6sCap7eH|F{%k)OJ)(FWAxm5sI6FysPS_g~S5 z)Z^CbrBM*fO~bc#qB6Q>!iHK!iZRnyi5x@Me{4CcZsUa}V?!Oq4}#IQz2j{*x;u2B zgHPjX~UM02SWmjRv(6!Gf(iVgbbDZJD7ihBo=O6a9ZzK$+j;Hklz}oiN7nSIJ%G>8^Crld(#uyM^ZD=7yelBu-$#z}x zRa*Ckxuv4@fh$4Bj5NY-6zy#Gh-nIr_Wqe0TdiOfFZiL!naWIHS|wbLy!GOEsvH1b zB~8b_S2xG115?UqaUaU_#uz@~uT7>2h4_0lUHP>O93(X=5w=o_!!YT-3J#e;>n=xT zMcEOXEsqegHGnLoi!l1fWUP7DsJVvCJ~X-CNPOpuzbBkJ=!foXLPqy<=@`kx;}7;b zRvPGGEW7!gTh+0&=zkLT_R~i1H!c%e3y++6dM-`pL=+Cb5np`Dv}}lVD)2!v7cZ2aQ;xa)ymAly{r^Gao*&%^qCFA8>yo?4T3d3Ev!|NZ zxYpJEjqb~NHpDEL9WJe-NRR8~@*Pw2G5LgBr;?j+#qi%ZnWnHQc@|5&q7XQ z(7G8&=Khm7qXXNtrncFkOcU6ngTfRW6jcYxBP7}u{~q*WnQCWl+SY0Vh4S_f=pFM6 zb0(Q->){UWvP_(6sWD!Nh$Jji7JCM|)z>tyhgM)vr2|5FLx6mE=GBw(Y#XPr?RQ%V zI?xJlIHPyQ!BQhgPtI9> zBhgSat?6BK=XU$!i83olPl3_$yALZ&IXkhuzl^>v%Smx?s68oNNuc|ZMqjc2OVmZo z>Q2;;mrtHg3_dIKfb=xLZpsM=Q(G7xaXdGKj+>bKKJ!`zdtwYS0_7Z zNfk*D2qb;M-o^z40_X1ys5r2LCYtmE)4oW1?`RN6N`7yELB*xt1DoJz7dvZEEm>^| zScnFnbvz3K;qs&e{$jvQb{A~Uy2XQ6=mQZ5UKxqY>^@<#4+YcdFoF&oAJaS$>a2)z zQ?x~K4yn?}d5v9SY^x5c1lF8B1_RK2STbyv78dQhjRhkR>> zQHPM1#7R|5Y%XmkUhp)vJrX=qx_O$I87)5Q>0zn@N_NaM20@`vkQ^ASsig_B5fw%M zfdXkTW)CK!5xb{69}ZZ#wnim7UP=jp#YnqZ+z$V{3hL{qO@PRAU63*tP{; z?D)L>fo02XAGmyr-!lk{@T^^NJQ*7`yJnlTA^?a)dsDk*J-TN|5zA>7_&nbBnio&n zpzjUTzB@1>L+8s1K^^UFvH`B%!pxnRlpdiwsjK>V#K`5t@}%pTSFJRgCPI@*_@7mZ z5JqXg)3DFpjN??>`=he`Vl}(2HQkFPGPUWP=eItq@ ze|9^m_cp=r)w!tlLdu;!-O>4pI%dW7;45#LZAsgy?AvIE^i4f?i+NRpmQZqo-a2d8 zw$tA#3~ zhwtT;bYXL|YlPELG}D<$5(F7a{!^P0j2Mm6Tp-VLkC5^j`M07`S5o2!9JL%ud|}Tg zc;ibEStLBANqp#8t;qT(sTz9ocz{A)#g(vctg6jB?IYf1TaS2;*r)T031fp>*{y3e z3!}8l9IxAApJuCFQ+&QE`&C2+J29C)o-z)Z+nWP@kY)o3} z=7qzq1%x3bM{21Q^ILo>Et;Rvp7GK=@68xHNC`M_)cKtEpe3QL1y|4i-HRc0_wlO9 zF_mDax&wCmEgTB=xf84E-X_<99Uz4`kPYxCGd>-S%|oE8)P4!%wH!8%{i6}6A#`5E zkgOo5f3s0VWbqdzureu74DWdr)c;aXdw7tT81Z!Q-ply6u{+@+ML`JBr%sD-welf5 zxq@eRFLM`zbdC+xb@1*}YRNZwPYV0G1~WY+lJHo9MHpi);SuOEOd5A+?@ND3QLRGOzvMd-=Ee>sV+>(oWPc-NZ5Ub&@f znmN_X-vZ;uS zF=V#sjt$1&E0(Gb9KnTFy~0S>%Yz?A8vFY?shKy{amPI5s(<)%P($^zvnaBY5{ivl z`#ghk!>FZB>tm$XjzA2j@P#Z)lr>@1+A1v!ZMUb!CI<}_55O!!OdojCYVOyZhZxj^ zc29S9CY#?7{2F(WDV9-yeZPg(ND25O=i#rqitL8rU$8qdHv#||L1niGwIUoJKrYMGI?i)8@ zDJyF`PqufZ>l?t>&vsuq`K%-Lttm zK4n8`yB+JnI(&JOOw%n14Q9i@&k7h7Ei9pQCD3By%kV0Lvp?AqV)&z#MuE_61Gc-i zY4=X#D^?WwVR9Q8v*B98;fLzP)!<)7j^y9>*Zqt=2t2X&F!*nC+r1LmnyZ^PfqT}< zOiYxWrAcK=siKxfY`^h0*d-e1x`4=(pqR=N+;E0UqiHHXo)CqKc!IloDARP}`T{3B zRb;tGmik>)yx?oDFptE?`D(zhZQ$5Lny!zgaTcBO+kCoAd-xu}$)xq~SH)T3Fa8bs zj71iZEmZ*dzl5L91~2z32XwAbT0$nv zGiK{m=6TMH6rW^Ly+T9Xstu!cgakVVUV2wK&4usMY(q5y}Hv|>4(ziQ!_+4dk`~XifWpU2`Da3GYXRC$kYu3P9cu{!i+b}y#xj?^XM##kI zz}lB@TsSpXs}C1Mi_OA!YMN&3@Ts-+$yP88pKB|`)Q>&$RdG2(Swcl_50%XH<(f|Me literal 0 HcmV?d00001 diff --git a/demo/assets/divider.png b/demo/assets/divider.png new file mode 100644 index 0000000000000000000000000000000000000000..5061c78a15b8d0eda1e4311fc1888e69acb55793 GIT binary patch literal 2358 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 literal 0 HcmV?d00001 diff --git a/demo/assets/icon.png b/demo/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..dc749de32a272be4c7544d484b29988c4b4a3a2d GIT binary patch literal 3694 zcmeHK`7;}c7Z0^9MQP2}QIW2;YrgB;I-(6q>nx@1+By@guC#(|1qmT>Y*(#Hbgi4D zglZ6XP;sU~L=FR(LT=jHOJ*;yW z005}ExjJ440OYd2n&QEIB)P|`f8Qzn;p&G10F*VpnjE00L~9?ELtS^d2&fy-UEB{8 z;0{+D0DwlK@?OYy0Kkz0ZjKJ#7`bI$7ZNH0verT&1Yiza{)c4!tMf;4JG}YVGcuxp z+$_)K@g52SlIvNi!F#BjTIOox3j8UST5a#Bq!u5ox3^Rw#xk$y=DgTqiAmU`y#%)# z$&QV82cE1&KOu(kp ze{Nij6^E2Y(?A-e+UIwqz0oM?9B&2Kd;ud3G{@e2R0ip{|MBffUCU@LQAaw$i9p6h z3ztxAD$HZ-L(o)Gv`ydCWN{=;Ts-pE3Ii8PMNdq|^MhS%I$Z33e3h`WKAc;RT!!d+ z?P4{NGz(4G`9(+EUxrg2VDRxO(#|dz+v5{t^1E$Q@l4#Ov4~6Jc2~O}!Z+g<*Xd=f z(3%~FX%dbm>%uhU=9bDlXql@|`}oc%2_7706E#1tP?1@i;*ng#5pGp|i9CnD`FK!ptdY5SlXTfZJrUzi>iEh%8MGnr?^wwC9$872;~&8=%5Tz zLRzFd(Bg-Gui1%hQ>Z95Dkr}FNXWfYLoOqoI)@D_JLkcV9!UqOt*qi@X{V&39I~`! z_vi z=pq!FQym7f+;%fIR(91?a?K16cyK3Bxz1^w=L_X6Y#LR_59JZ)cu=3(zx!Z2Vl%0eP z?kHwBhl!0B-eg}2y3{!F;MvxkI@zy!W|d2*YX&;Y5B<}sr(5f^L1U0c`$OwF^@7|d zW@bx_Dn6l3I&rN3vwC)3Esgl)QhdWLyQ^q#p3-rlAW2|m_IJP_BKZAFdMnd;zq@>G zS6ayME=AnrF5Gh3(D_u!_4F9BGK~2J7)l3r-qVecKbr!>EqdcsDPbCPQe%$Pq=;#) zqCc%#cql))hF$pTOftno8t;1ztq_^G`qfu$aqRdg0og z=!$NBXQ4o~n;+Zm0*C9tV6?)WFjFl$yYI3L(IuNYvSC=naeQ|NRs^_|S&N#k$NK34s@Kil(iV$|&gw@ktBTA6bJBOQI4 zEzDHb$bIq2v4l%Xl=+-;CuV^^5*i2^EtlM=@*Ds+@Eo>->m0^YKcH%AQ(7LId^gO& zwZ(y!&R8rN^dg?EzgQD~R{>j!K{ZO<%|x~W?BF11&tO+Drq$aBxonUixStk#aXjSU zP*aHUYSwRz2PinBZQB?05!r)k$wikTedMSsyj-vb%o^WR=>2^=Y7m6HwZ8%pX3B*Y z^zAXafR%;`l!PUMK8m^+#~$ot+hN1baxR9m4mK0FzMc`+sDFgvRPyX|_o^b3!ap%8 zD*9&$Z*DQIFiz{Ism}%-{64Sf$Z4mYmkTO*xEOau%CJh;U*wZ~NfA&YqS<;_}NPPIh^ultl0%40`cHlpBFgGeLLyzvzAK0M- zLWg9%3kKEHgeJU%zYG5(?>NUG%S3aq_%Tj`a8b_CqEI-OpU2&swvdrEdkScso%L()Y0 z)5kvCuw11UZa$tg@jXE9xI0!G-&AnGGrOpWQ^!zi4eNol9EIMcgaLE5_g+_=OXA~D zkHn%Dr|q4jwe9YmWc=Dh#-xmKH@*IZR^B+QDieJ;E=Bv5LXF@JSTmm@t<#H`#5h{Z z=bLzm$=tChoDjD$T0gpz-+*a-2C>b{<4{uGE-l62%WwjOa)FI}G+?-Rl7lo%1!N`u6vGSfX6uyQVxUn&@p)f_c*MuSke(X2ZJ*yscs9@jdYyS+Q%1!;nb+ z7|p%;#ED`ekWu;DwR(1`0N5O>LbeX^s!6-J|M+T^%YXZWzlnkWqv(-bc++QeP;oCN RZC@S&+?+fe>n;YT{uiALw^aZD literal 0 HcmV?d00001 diff --git a/demo/assets/image.png b/demo/assets/image.png new file mode 100644 index 0000000000000000000000000000000000000000..f6f304bb46e69263d4498fc654c3740ea2937f91 GIT binary patch literal 4338 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 literal 0 HcmV?d00001 diff --git a/demo/assets/input.png b/demo/assets/input.png new file mode 100644 index 0000000000000000000000000000000000000000..92d630950d576fa6d86447c24ea9923658b679d8 GIT binary patch literal 2766 zcmeHJ{WsGK82@hOC6c5|SD_Tny``b!rM$F4%MFR-t$Ev|OV(IJUWZ$VM$WldNTnuY zUh)=eu!`->l*^PK1NoadM4JfF|=$@XxEtE=j& z0sx@ya?;5Y06_U)Lq%yF!FO2ouBT0ylYUqLP}BMvAmDcCj&%@(^@JY>YG3c-uNRQ; zV{XR)pdn9fEkqFjRFN)D$IixsW4A21p^$7@q1eYH-GUp;!^f*N=s`Pc zTT59_RA&^TRxS>Y@6qpsH)Ed>8e{^|iADq}1HatFuRw&cM28OqfGU}<)uV#0ETLEe zDAW}4Abg}+YH)oPsko5ZoD*?-P;85{lIa}D%v)xhIpxzyY}#*8l96c|VhMgrq`7#& z@P~-BQhD*toJ0r&oaZ1aN$+z1#?CQjyrUPha(eWZ7H+MqCxM!1Ca>|O*EnD&j4U5d zkAWxMVPKPy5^_Ad&&+$K7r=vdTOT>`b9SJCe;^H3qcW1zzZRt8VU5p zbJFqf)5~Mg6F(HO^62uL{{7C7*CvU((_){sNxQ3O+MBB*x|Y;#NUCPT5r^6zqotgw zbgCN7nU@o=I`W2NthG4G3|E~I=zV_Yjb(>IOeE^{?(c|?}5_> z68C8I1qF-GvA6ZLPKJLN=5rSx`lxFE78PJEMjKw5;}h5|T<=n98FhuBK2xa%;xY2h#=dH>-QwKjBHa z0?`&^tgGTh8E)Y@ANd(w4l_RmbEyBg;)NU3^RhJe6Hf7rHSgBB>CcufpySX_>T)vq zJV&ekqoJ!(FWRptabt$Cy3G4@+)*1ggFKKcGUy!ZD1XA3t`B=Po zF1eXIYZ&v16hL(u@A7X*dQ?kAM^XHk^068{_MEAWPhovRKs}vgNnWpbmM()c!=+$;s0}45@>=+$rFnFzR|k< O1YDfmoobHm6P_JqNtVa7{~gvwIcmr59Gie$}} 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 BnJEAO literal 0 HcmV?d00001 diff --git a/demo/assets/progress.png b/demo/assets/progress.png new file mode 100644 index 0000000000000000000000000000000000000000..3125f272b6946c0a19d3f8b1b6992262b5082d76 GIT binary patch literal 2738 zcmeH}Sx}Q#6oCIoSR~rOqynRqAkLVnKm;rxnna*cpvs~MNLVchYAkyeAdpmRA%skz zOhMVKSVR&bAQB-M!XhA22}l6}Sq#Xcf-DJpLKE6)%S_*VtPgkox#ypI9?o~aa}V9k z#bKxBZcP9Hb~-s?&j0`@O}RAG6cL)x>Nmw_dxWERGyp(g$^`nieQ(lC0`+y=Yeo=J2XNFncW91kljX z0JK3MLnA`~3kK7YX@Hit7GSh(n=RHBNdENm|4;bql)gmF0aLgNATCXRR`k z%Dp(;z)CiOa=qXZF)WkZK?)xh^}e%)9A8_kHRx%dv6I>@fTl1$bagK~-SoV{zJdfl z>}M<&4&k|a@aWIv2*Tqvvj9nBmZ0&n-vX#_b?iYdCn%IuI;oCGGY{-f;fzUgTGwjz znEkf`F3Y)*VLaWhgCmfKy6zOf`JF>OmhiLz<82Mydi)2uPeuCB9zRsBZS#|gqEI7yE@AE*H4r9%^2sKqi$O4fwd>88 zMC2}iPRX=Wm-koP8nXZ#s4=$#gtjT?~Yh1BSr5&5zGOAMcBW2YP<4D_HO< zm(3c05hxa?C0!E9y|A6EcSD0RoI(EQsWq*w^xf;0?$+TuE}PvbuD!8LiI525MphWb z{V(5pKy8vLyJa|`(5oZmI$Yb^z@LkmY(kjKRq=;vZ*hCWbu@A1OVTnT5z2;ULmij; zzCo4pkVZ#qDpX4*jiSggxDq_Za1Zy%H8u%84#CY|_)v!HrP%T?)h0!Wxwhk)I5F5l z?UYZajV#0{kC%F4T6EBIgB!MqV*OUgT?bXusT1)&iZfwc4GoXB#?h;pG4XqqXm(nDQE*s+wi;&OT2{v*?q;_`2nT^+1;p*r0_6IlYL$+QAQCYR=D}*3%E&@74dEf9TZ!>$Wsi`En+-vpScnl%TLxf$n$)c4Sm{d zJV#?JDu_f@*S`N^Gt`5~dK1w}Bu=EXz1g&(zUineq+#O$^G_YTEUX6@doA>g6`e^O zMYtnSmiv(x7>w+npRKC$9rSlpX3lU3RrS3eR-D#G*ovg?xOYW*;?zGzfRVPkEUCIx z=RtQ}R$)`(m4c=5FEiRjg$?-1O7x{8koMw#^`@+`C7*L>o7nXi92zh?p|_~^>4pC^ f<|xLw^bLTiTD%<(dRC?wF#)IJF4$80z^i`%jYyj4 literal 0 HcmV?d00001 diff --git a/demo/assets/radio.png b/demo/assets/radio.png new file mode 100644 index 0000000000000000000000000000000000000000..2f73fb4f764583ffb3ca40fb75f951be586ce847 GIT binary patch literal 4134 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 literal 0 HcmV?d00001 diff --git a/demo/assets/screen.png b/demo/assets/screen.png new file mode 100644 index 0000000000000000000000000000000000000000..68d91a73868e16219db9145bb52bc5439937a9fe GIT binary patch literal 3181 zcmd^Cdo#lqMIP0vl&sqEI{q1k<^PNNo zds}%Kbr}Ev>7;e`}yMnny&Ylrc_9X7}z_`LRXCCan9JXA%kup#^v`Z-%=>YYT6<80f4N-Y6uvJ&mjD zkK`mt$Is|9Kh`u=6WpK$M=VT?&#c?xt(=*3XQ#5do4C5?ky9PGNFh^;-=|d;p`GlAC>iLX|ALs=rbc4|9P{b|5cBPffZV`xi zAALA;HVKm+@(CI#{d$Sg{PgE#OpT57J2;2P8uIvQc}<6KLZtR-JFm_~7>qbSCmB6Z9^8r&Es*Az%5-7oMx8J556QSr z9-ma9zCObLN)$~7X;}V+%UiJ2Ll)G5&h8T!7Q+a`7eu}4+rmR4=|lW9;AyY=pEZmx zR@QF}jbD|-Do1m=%De;>R0C$QpWP1^)XQT_Ud>$Y?JExD-c*-neOdJ*ztr!tLN;{A zMeDMXF-cyYzplso5b?+P#tpHBnpur59O|`VNhVwi-R9T3;uQvi85v_ks?FAUL24*! z-i69gY4QAE2<@GfMQaRMVSjt(=H3Zj;*!A+(wXOzEdK ziT6-1Wk*Db>Z!|ppOh6TaiW3TQo8W@d=Yx&J?OtY2Yn+G=YX{wVpw?&N?X7)rhnfi}%`L31VRlDGf zz_n*6^FX<#@1eCF%&Sf*l@r4twAKI4R0{lj)8VcEFt;m^0^?lt2XRi)#k)E?sZwV? z5m-Ti6URgI-E_&J<@aNrNC}fNdnkvf^*%yw z2eww~KlAQQZ)i$+88Sa88p)Md|IyOXzCZciCJQwkunQ5))}4!+SA;|fnh1NUc#S(? zze0WQt)+Y}RY#9itT_Nvpi`=(~qf=EeaZ;6@hI0@bqCMOua*U$8!IoxVI%D7w zUHiteK|X2rbyC4u6Lb(>VxfPfIr$ZwI7%~$>K!yi5DTy=dz zGWT;PGvLZjNQZy3WILO5=&p|5LW2o1O5a=gciS`2mWcB(8i#laT(eQgDh&=$EkxLP zDru3q3IKUx7$spn;NOg_?05SkAu(m>?t`87QzZ+RPADoa^o&_!1*}XD8Clcz`Uf1u z*fG#!K+;=g=Hf0mJ(<+K!0y8;e?8}k6?mDRNA>zQBQA!bV>bTfEnUqTmASqKp$XZ{ zusHWDGVr&^pLZ#4ZsIHZR=Aea_(?dtT}vq3E6CiwZti~b{ri%V&cMJjfKgpL!!EOw zQ$6+PdJJ(Dq!epAaN{S=}>h(q8*p7|Az_Q?W{E z87p07y8Kw@ZM*a>Nb_@#@iwi($=iv`Dyy^a&xLbD+$B-x(YTnBH>~jpzd~Wi!i&er z&;nLzTFz=zUH@R}y=7JNz#360e#x9nqhKOwl-g5IuhC}Y4*2Z`Gi3Zz;{Q#HextdU Z&?)W%k42<14EQ?$jvcW_Rakpm{|#{$(wP7N literal 0 HcmV?d00001 diff --git a/demo/assets/skeleton.png b/demo/assets/skeleton.png new file mode 100644 index 0000000000000000000000000000000000000000..7215cb5f344f909824904127138bb637c3618c30 GIT binary patch literal 2926 zcmeHJ`&Uy}7QQzD5)=e%5sDF_C{qCubU+*+xyZZnszoVa2-GQJcu9}|8A$FpA}B7Q zpoqw`rHVm$h@d5qAU;3^gFtvBLPnlLVj2?OfrPpCKj63i&>!|X>+G}FK4+cp+k1ak z0{y*ox0q}J06^Er+x-LpfO(${tA%)Au0uDnXrK2ENdkbaUwk$YC@wKZoM6%kuVX;% zJKQX?(Kzes=L!G~xm(vGHIXQ^kGpG7DkvQdOW4`@Rp-Z_+%G@2KK|2#y+>R*8#9*l zf}kwNN8Y=vf5o9J1t<>|G5xE7t=krbICN_7?u2MZDaeSLjk4+;gj zLqIkb3mAYPeh(gS*U*5oVL;#Dufsq0Cv=;H<>m&&H8G->m&==(1ecAGva&Ma{4~#K ze%193Z<~N>7n9R7GdIVUiomL*6fwIU z@U%S*5!M^jQCp`sawR<}5~mJ;wbK%>D)A!XpGuUp@y>ve*C{Z`E(oGK_XX#t`>Cwo z)N#{pXbw2|LUc#vM2)5L+zL-+X$aSyD}sYkFD!~|Y$O?HVIwz3rC5~?EiEsb0A%tijKd9ZuN3q2Od$J3^dlk!MV0MEE#BZ#cX4h%5f|nt ziD+AwDfFZ7yF-)zxJ>6fMnS6ieXd^31AGHk;RVhAy~*hn%7~5G+XfE55KCXPf2*IUfB>Y(F zr(&1lxaW|d)g4?jo=7^m_G@9z$Pyd87BM{3u2#&?`g>G${=)nl{H@N;JWdhE${_7Z zry8g$!=yH(q&1o{@XHBCN2FpppOeX%{@7q>`?F80S1Y;QNYsN+HSS;O=@haX7cCA; z=5dx;+ar7no}$yiu0tY+|oPPOSLxnkM=<4Y?KUGZdDtxWJ04ypqXjT;J zd5eM%?1A>FitUd<4fBzPB{B?w}aeKpdNR+*!K2L(-|Sq zTAUIIZgxT(df}WG-$ai#3VtGVT{Q1D(R$TC1DD_qW*J-Xbq(@z-U|Jk2@Wu+Ig}Vn z%+2Mw`JOnifvpMH{iR+-KBGsx`+htOf3Sxs%7Oy%8_Yj1T}Aiz55?42ndK3xO)H8G z9M+I}quM0^d>rRKQ)+3JXFxjy^dIgxHKqV(*H|7J)m7w1Pi&X|4D^n7U64+zAMIm@ z4v~vcuaHEyaz-pny@cd+uX==fwAh_+@Vt%nrbp!EB23@IQ+m>IG0mx(aVg|^d#Vzb zS1>W9|GcUN=_HA1>?@s}TJ>*FNwTCfa(P;|4{ORSa&iBqaWJT_F2rRoUspemtrh6U zq@ExlSOq3V(3SMKkmN^QmcP!x2;9kQ`WI3iye252J`RlUlYK+v=E zX<9dbK*$VqhQCqT*JM?-fGFw^z?u#4XCW9GSZ?zTAlE%aD6FiY!xYcqPu~&nZ(p{8 zK}joj)lD@BwTE8=zC3JL+FMMOtY(M7XIhx-#=DiEj}o0I^CQn_$7K`}GjfYDl89sz zbte@U$CU4SHa68GXov&Cd)W1VNuA9C{pS*cYVFXQ5$ywZ3U5Rn13n)9?zP7vF8&{; C7BNZy literal 0 HcmV?d00001 diff --git a/demo/assets/spacer.png b/demo/assets/spacer.png new file mode 100644 index 0000000000000000000000000000000000000000..d38b3080e69352bfba040e7b16cbaf1fc30b519f GIT binary patch literal 2413 zcmeAS@N?(olHy`uVBq!ia0y~yV4MKL9Be?5hW%z|fD~teM`SSr1Gg{;GcwGYBLNg- zEDmyaVpw-h<|UBBlJ4m1$iT3%pZiZDD^LTIr;B4q1>@U0w*9hCB^w@=@84r)c4LO9 zyrxi~z)2wy1t}G&fK?YhI=>8@u{`KqZ$3Kf_KmNm=e$4a6%}B}l^;@m>{VdCk*}2Oo zFK<1M*!gUIdl_*(8=2|%Dh%$`y?*liIM4qV*JnJeTobdu_Wi?%gz|Uq`s!nE+U;Qw z|8;-Honyyt-Pl#}Ekgg${{P9jHJ@Jox^YvqH~nPmXa3{u@88dl@Hq3~ymhDKyz5Tv zPj>&mBWIrX*WgELwD|enwbRe_t^E7){OiZ->#NnoHvDOdbg{GeW9^srwwc<>bK=|g?{cys z^Uj<-tT;DU*ymxl{&lfOZw*;{XhJ-u?~!$lv3+LuGbTye?OK^u;l(XJ;pXJio9Y%C_TS__bu(P=^Nh82HK|)`%J}4K?Eh!& z3hj{0TQ=k2O5GTTcXuj(O)Kn9`qufxW}jW#S{|{(=J)-|R~sC@yT{mOYb*DOd;czP zZHZRd{QmXx!tCk|Oa9fC?wYz>^4+t!U<1m^zO$X(bDJ%E-{-@ur(a!biJqReqx#mi ze|H2&^E0AI6Vp?WmdHU zWTbiD|Exbp@^i)Wv+R$*EDJrcX=ZNx-oMvM7!41f6@f(Nn?t|Pu71w$zPa?{n;F+% zUQVw0bMaon&YBb9qYwJ%tB{}cXp}J$gKVAFW&!_eOc8pcSe$Bb^>h>)GV z-0WC!x}WU66*jdMjPuSg6^l(;|9_{|x|mqm?MG4+&&EyJ|LfGjioe_0AI~-C=eu7% z_fPJ_k38?++e=vQKE3nM->qpos?wO44xIcHtj3_i#N?o)FiMRE0VoyxXH`|pH|%9t S*$r$RF?hQAxvXtdSSW|QEQhyVr?w7?oWE$Z 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 Du)CYT literal 0 HcmV?d00001 diff --git a/demo/assets/switch.png b/demo/assets/switch.png new file mode 100644 index 0000000000000000000000000000000000000000..c27087e8727c9c0e0be299de8b7190e3222f20bc GIT binary patch literal 4436 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 OfXfeV=xWE{l>Y*%gC_d` literal 0 HcmV?d00001 diff --git a/demo/assets/text.png b/demo/assets/text.png new file mode 100644 index 0000000000000000000000000000000000000000..69907ffae7af652812af2d666f6816be454ff3f9 GIT binary patch literal 2863 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 am*JR2xoYCUExg9F1ABb@y>EMlX8#*Dzhuk+ literal 0 HcmV?d00001 diff --git a/demo/assets/transitions.png b/demo/assets/transitions.png new file mode 100644 index 0000000000000000000000000000000000000000..b4331adcedb8990d1bb3cd87cddc2d4147762065 GIT binary patch literal 5802 zcmeHr=UbCauy+DMieiE^MTIE62vQUTfglJGq$d<{DhK9@f{7amUb(MNFrjJrE-cyZYoZ+vaSV!_3sU>+VX(VC&a<*MXxg&i?!VfAhq;7hiNy7Y2V@T_L{~SpM>* znfmn&QzV2B$z+mSnRA~ou-RvtsO9fZ>vk?(S0qP4rH}|;N(9&9pf4{UFTZK1vT|B) z6$6*cS+EIqE63(ZIQHhWg(TuJE7au_z%ta45%Q=XKB<3$rE|jNxrTM7F_t}APxzxR zyC9BO@fXrFs~ol*MGdp`UL}NOVuJxCKk9$g?YuqNs`%ZOaG@+Um1Hda99-VvfL7wv zlb&RIFg{Y{x9Qd3vy<)yanr^2qsJy*RM(7UodHhUYtmZ2`%L#q9I zpc;I<<+t5#aj2VoDeCmE_bGN`QxbX}CqWCGt`07b7>I^UWK9nKi=;iHhHx)i;a0)d zQVr!t0r4#%Ww%OC0(!rco}8@OFq0=6p;ak0ST|0%YL(A{mt*8(;=YRd1Tj5kqqU}x zEZ8ORggFKl@lVcWxP`9HtzI;HUZibyyQ%JImRbGwNSU^XjFKV)S_lzLnb@Ia_pfO$ zRbhH!RP?M6J1ltq2Cq$BBGJQ|>%#C4`HYdOi>t;jinMbdK#qc%bkM{@C8tF$B8RF?wH070+S39aB;lbB{+R4C{y|K3noZkCh zo%3%n{tWLDnJ5>O61~wn&tNB;KSXh!Jg=ZS-$f>0K^L@Cj0`037owpJf~AGNB)1RT z$mlR+q53(LppkSiTpp(XGnKWMmwQK^3elEwZmu}GoFadx)1oy!FQ^peoj1?hvp7T_ z9sd-DE)kHMVtaS6YWE$iH5s0Ff`ZE{wB#~k{6Bknc}Pr-rSFprO$Sx?zeJXq3E2JaGz(wc7%-JMM_~k#&}Cz$DX8=aJb@By z+p8ufd>wK(j=SBmT8uG0+V89+a!{^VS+c^HejIn8FMU2g7A&9clAZu-j9q1d}V z^5qL+qH@pZ!Ka7ES)+^;erDFw3jQI~gFfjGxXv!>MD;S2ycB$`{LGoD#9zG^Tny2d z9w?M^UtJP+Sn&FtIXM|!z-QU)W698<$z+1B!w51grp38Ww8YyY$`8Q=l{t>W(S;kh zb?E~3T?h_K2qYrU#!Lgly`(WQ{#a zS{gsv;4;-z(y`AZV0_n-FBM$Hl^CN0y#fHeSg24ap60Tr^&r8>bNv_&4$xsT`9z6& z9dGzSK=n{7Ke*fe71cC?Yvs*S?4Y$usDxztvg}d+((8>?6nsP2siJMW2+;t~w(nUl zD>Hc5E`6uPErr<+mcUC#AEs!(04 zx3h~1>qsXLmv^eCQ$$Q7{O)~B6Hp1NnU2mH$Qh(#m7gsx`29+(P*A=(_bwzyt6EmB z=@Z&G?6)LZr%A-QH=d~NatUl=|J@3_y<5hZ?V~5sDny58eTiQBFRM`PDWIFjgHHoWh&^@c6gek8TQ@+G zud21+@4SCZE>t#FwN)^=GM{g}a@{Dh0r1lJc2a!2;neV;OQw8a$XX}{lREJR>8|yF-y5@h5`)OVwLK{C4 zMJ3`rmGfrOI?nnFAsVDcm=ki8R7z(-xP6K4-%y8o;i1UI!mj(OJgJG#OLfU_Ez(5_-nuP zcv_Rq-n8vvide+qAtww@NnuDV{s>N zQKlPYDyQr=hgTgnOXTxWt$PDE+8(bO$sXb5j3Ty;tR1~~Xv0I2;k7|F^aDNh`z0q% zt_y8~?S1eto=tgNjz0HYnQLd?6CK1E!DP+N#r;30n{PqS3E+)`Hg@KnO*>#XU7=vt ztT}YB;C3R2S`p~;_d|wNEZMSVzI<6~6^&LHE8yd}x*ZG11}bo6v$56Q)5P{|)Ge2V zx4lI!3m|*SZWQaYG(i3)??q|#{@R+21?gjh;c&Cr5}OcEEMBE{cwCZe1}+z=nGJPP zXoPL8pGq!`V32Xz11$)xIk8I2vW3AzVLg2w(u&rk`L@Rm_U@iG?QNYguRO0gub#AV z3__{OU@~(euQa9_>NCGU%k-wyN@OwDU-Z6J&~=`A6A)H#`#Jf#~x2d&*>}cshuh7|6Hs>mog% zQ4MxA{OzEslzneJI=Q!QXs=_b-)r~ap6%3DWyP!E<}{k?U@~fA8T3b;=0NI6)&|i0B{E{&JJ~uLIJL_1>Gbn%&GK_-dYZL zDCR@;lTScdfH-nijzI-flYVBf;MFejKS(`$GH%Mh#5w$9tF|vj3tyzYF9%o{K!zXJ z7jnKEk2GtMzWMC@gANn}0b>8z+AFoBfp$FM4>-OzMl0dF!h$~+y#@DN8_Rk5YTXbu z=*8=4E}a2*V%5(97ls*@58O{!)-U>?!|&1S(XhYAHwYb73tI-QQ1-R*(;!GzEdj#O zOY6#3@t@`=uX4+@f9C}#cB?^x-zYZ|K;pQ4Sd8NPDC^-ZhqiTvSgX*-JRy)6 zm@5z1qvfM5RT6HpiLotaUTiHp;(%K16p z@KkXp*=`%uWMJoTp~liFE2Az@-k8mf#JXdsqi(R?9Ns=?-P!J5<+#OZbNQ4R^XB?Z zjGMnxrKvgt!GW}i@=Fe-DyU@S#GZN@utQty1z$S8= z&v`s0JjH0z!C_fTA5%NlUe3}67{PSu!}iB7c|$ubb0=hM$$&TvuB(->?=i|O$~ z-w(65T>Zc%^(e_`TVrOTLS2Ki)b_wAn&Br3xvBZ04zQ)@T4wvPK_ z6!8v)5(wG7tlRIGwLzI$bLR;ODnjT=z1;eU*^->%7)C-me{B5BayO<3fEiFG_cvZ^ z*2-dgG|j>-S|ut1CzJyz77Qd>ed)ys?&u9b$;78(#$EmPGxWB$vfzsajV`I(86Y@3 zp7=t2pD532dSh+yi^9HmkzgGtq$sKAPX1L~NVURvqaKrq&j9myZ#90G178{sEjA>RDCTgtSe1(k?07 zkiJzBGDwxLB##|fvJ%|_2YRafI(w>iE0$ds_R2*YG_feeyIB>h<@&x^>CRZpMt?JP z*sjkYMfx;5&AEtMWU|JVQ^@8g#DJ;Y;O+VA!*R>gC%?m;3$xbuoe`mNwM43UxN86L z@swF}`_*EEME=FSKo2YCHaZ? zOHgsN67&x(d>wf=$TYCus3Ma)Lp${r*o4E>`D!H=E><`DG`HgPxRk^KWm9ESFqt%-$!a3>%7N1&XPA_kH zQ@bYt^dV7uJ%$AfoUFOCPdbWIQ|zAI1iy=5BSfO$hzl(E7@XRVi<7hd%_{u?33-`7$}O|$h*`i(2)p2qG+1=Ks^+e+X6u{-4$@y`OlFKN7zv# zP(uLV5Way43n>xKp@M725!s26m`Dxo4UD zW1|(ty(z@kby2~vX-(oyFbsY}R!$-H*BMm7dv5o6i3F$EQT1RuR0gQ{&1hI-$+^+S z6F;lS-EXW!eeBQ=05tGjmfQ=%2hxVFi?+8gqLJ0Cc)o*y%4OY__W@hqAOY?50z^GP z^=D+zd%IS$k~2|BLD!!ZpB}WjmLHGClVAP_E6_{5gZ<}V9$HdBx3KT^ z2f5(&hmk7t!~miml<+d_6Lk}1Cd+H@ce~8?o`UsXmFuE}2^9PW`d3EK$&ooLxx7{s z=#HDg!oD8vwjT;nCktSJ|Jd(0yIcMAX3!P5px(GnXlB zU=!aGbG0E40wb3`l<;d?1M6WNq3{LBWJ(n11t**x#w$sS0^J2hpB^0%6`UW!!Cb(3 z0{<5#%44K2fqywxxsF$3F%kolVX_w&_i>f1xd0yHvu|rayLx%qGKj^1T%+I2eT5ha z=~=M<8Cjo|Tursg4Td12&UB|`#pet$gJT}CmS|*;SBZeR?qU^wWP)WBjV=M&TNBHT zoCu(Bv`KkOTo#K4xu&wu+9iQv;0QhuI=n5`Lj>rsd1jH_nIPBPxj9MJ^d~N2#vm{m zk)TdG#os@>yr8Kf_}&RBO@C=438vj!~eG5xDYj6E4CWJwtt{eG><8a{U; zE-=~1eRK+`{%qnR&MByjo1d64&w@X7NHr|0o;B)+s6V4O&rtH9 wY~OclMHU$7GbI9qzl;4p`ri_%!X&H3yu7OtBorjVC52Y4xR{#J2 literal 0 HcmV?d00001 diff --git a/demo/assets/video.png b/demo/assets/video.png new file mode 100644 index 0000000000000000000000000000000000000000..c42dbc47f5088b99e6cd93545e925f77c6dfeb96 GIT binary patch literal 3433 zcmds)do+~m9>?FArVN7_m&`UXCKMGz8DW|jMY83RF6359QDR&gjO!?EYz^9Nms74` z7nQas2{WXKT&8hLq2w}*+a8x>Fmv8nb=JB3vDfOXbN<`xj4*=3aYYX$EVUXzo7Da^{+cXr~TvX)6>x_QfQnlp}t7Uw=t zHTwc}96u=kTy4;?PFfmqzfd`PEPE-NCb21W>&ki{hvNvL)9C<84h5(~p=1j(kN}4R zau5hk9S2yzVDyB))=On2mz~(zUl(0n&#nym_q~6w=a5a}yO~5?AndhM9LGS;*9#hZ z2-r}>>I1%)cgWPtOj^0ws(CD)55Je+_FjSik-VrDT#QLWor1S%qa<0xPEezYiC%$Zrv)zKIB}}%uJ5Eh3hA| z znc?R4skZ|I<-Xolw771EfvXOrk1O4i1wX~je9P;{W}bGo*yuk|U(f5u4QSwGbaiE< z{_s*tzjT96Ah@`1tGkg+8X``pL(aeA$s1=D)eLCjWV#>Es6#(I^iYFxZ2vQ_I&nlJ zV;5Ds;j-P;&9`rD0Gse!B@lK2>k0Wkcc7QEv3zf-UYfnBJ3iM|v@$xD$Cg2NOlKgt zOD#L}>{D!zQVV@x=)G9%o0#MsPVwa;j$cRapx*52>B^dgPad(R@~KMQ0dC9-4!uSc z7L8S7xHpf|zqQhWB)BK6KFO!pBP9sSw3FZu=@+)EHh6SZ*#$ocU`+X5uk0Ge-~H7ye*yvo{#U1=FU6#jx@_FV;V z8ezyBUhZ5OT?9W{^kr^vN;LV?32H7P!X5aaiHm&wGI(WlZ7HP1r$RK|alyeOJdlD( zlNillxsxmlO6hW_S(>RF=G%gyNfQZEB1Rt=&7uLw&~@VTyK*M4-gnYhI|!P3 zDxbV#XXH`XVvTDJCHch#*JFfzYr9-F%8zS+bGUUZA)d~@Nt7zy@$}k(JT+L{@x3aHuGAkG1Mr4x99xKu`{0jM{C!cgYI)H1sO;YQ&OJ=i}Nl?J|J8iP_>E9Uk8gjaAuF zPyg;WuULn)-o`F#;#waIY#)RJ^H$`{2z0w7yj26@In%w-rEXOYH8#k&Wp^ags`Sv< zJEvP$=K2=RA!8|Y{a*Y@)%58ID0JS(`IM|51=%9OQcgEpqg4U(*5u4RYEdr8+? zFS~nV#HkdYfiw$|dVV`-ajv!daFfWMOJw#Fcy!CHIFi8uRkm)aS5Bh1%vsCXMOExA z(C6155zxN)Uk)EO|aH=14pD#~y`C?aK_xbfvAAh{FF@0>G2o7gB z-$bm9mT<3cGLjet3o2f?EGS+?M*IDuBKVOUHc~L5{VC06kAyI*sEWJ=M{>O4aPrFv z{~nULdxq3X%m(jD8YU*2E+J~wsvQRxUJleJ}H~bMngH5jXbJP;&Xzz zecpyD6j7V$9GePyiBh!Lfq}Wp)g2xs{4YyOAyNx%;zOEJn;!YtL_6c5>}#!DJX$c6 zW`k_mW5zP8O$<2oJp8K44 zk%E*;dXT)!!}zfoqmd~mp$M|ah%(|&7(^QJpqO$iX03;oZ-vj9KSx~C#OYQAQJqa! z8-n_n+#6e>jtm<4NT8+E{oP!+!jMMyQx}%=gXz2o@^}N^hr5<;1eT)i{aH5XTC+*our2&g$&iG zN=<-qeZ{e5PB410isG2x#SbXTm4st!f~?8o$l2J6LOgeEc;UE{2{e7 zJ!r|vHrPTU2v+?6;yV9+H1l%yc^Ou7q=({{HXTuYmO^3GsS|0Rs)7VvKH*u~0jPgM zkIu7ygC4DaKu=yU&%}K{)LtDY<2N{nXIoOeWW+|5O~E~0B$kv&Csb4rwAq$QSH#PS zp(ig1gdYESFER9Nzm!%~qXLpq$>f-2Txl`4f*ybZ!}D?KZ;v1Ca?8=3P_;iqdX&kF z9Q2_YGn(E_HNs_giJ|AnhW`olSO = ({ + label, + imageSrc, + ...props +}) => { + return ( + + + + + + {label} + + + + + ); +}; + +export default ComponentCard; diff --git a/documentation/.gitignore b/docs/.gitignore similarity index 100% rename from documentation/.gitignore rename to docs/.gitignore diff --git a/documentation/babel.config.js b/docs/babel.config.js similarity index 100% rename from documentation/babel.config.js rename to docs/babel.config.js diff --git a/documentation/docs/components/_category_.json b/docs/docs/components/_category_.json similarity index 100% rename from documentation/docs/components/_category_.json rename to docs/docs/components/_category_.json diff --git a/documentation/docs/components/feedback/Badge.mdx b/docs/docs/components/feedback/Badge.mdx similarity index 100% rename from documentation/docs/components/feedback/Badge.mdx rename to docs/docs/components/feedback/Badge.mdx diff --git a/documentation/docs/components/feedback/Progress.mdx b/docs/docs/components/feedback/Progress.mdx similarity index 100% rename from documentation/docs/components/feedback/Progress.mdx rename to docs/docs/components/feedback/Progress.mdx diff --git a/documentation/docs/components/feedback/Skeleton.mdx b/docs/docs/components/feedback/Skeleton.mdx similarity index 99% rename from documentation/docs/components/feedback/Skeleton.mdx rename to docs/docs/components/feedback/Skeleton.mdx index 45d9a003..78dfaf46 100644 --- a/documentation/docs/components/feedback/Skeleton.mdx +++ b/docs/docs/components/feedback/Skeleton.mdx @@ -98,6 +98,7 @@ The `Skeleton` component supports the following style props: - [backgroundColor](../../core-features/style-props#color-and-background-color) (Only `backgroundColor` props are supported, not the `color` prop) - [layout](../../core-features/style-props#layout) +- [transform](../../core-features/style-props#transform) - [position](../../core-features/style-props#position) - [opacity and visiblity](../../core-features/style-props#opacity-and-visibility) - [margin and padding](../../core-features/style-props#margin-and-padding) diff --git a/documentation/docs/components/feedback/Spinner.mdx b/docs/docs/components/feedback/Spinner.mdx similarity index 92% rename from documentation/docs/components/feedback/Spinner.mdx rename to docs/docs/components/feedback/Spinner.mdx index 2323e098..fcfeab52 100644 --- a/documentation/docs/components/feedback/Spinner.mdx +++ b/docs/docs/components/feedback/Spinner.mdx @@ -122,6 +122,7 @@ The `Spinner` component supports these style properties: - [color](../../core-features/style-props#color-and-background-color) (Only the `color` prop is supported, not the `backgroundColor` props) - [margin and padding](../../core-features/style-props#margin-and-padding) - [layout](../../core-features/style-props#layout) +- [transform](../../core-features/style-props#transform) - [position](../../core-features/style-props#position) ### Additional Properties @@ -136,3 +137,5 @@ The `Spinner` component also supports these additional properties: | `isExpanded` | No | boolean | `false` | Determines if the Spinner spans the parent container and centers the spinner within. | | `colorScheme` | No | PearlTheme["palette"] | `"primary""` | Sets the active color palette of the spinner. | | `animationDuration` | No | number | `1200` | Sets the animation duration in milliseconds. | +| `rawSize` | No | number | `20` | Adjusts the size of the spinner. | +| `sizeMultiplier` | No | number | `1` | Adjusts the size of the spinner relative to its container. | diff --git a/documentation/docs/components/feedback/_category_.json b/docs/docs/components/feedback/_category_.json similarity index 100% rename from documentation/docs/components/feedback/_category_.json rename to docs/docs/components/feedback/_category_.json diff --git a/documentation/docs/components/forms/Button.mdx b/docs/docs/components/forms/Button.mdx similarity index 100% rename from documentation/docs/components/forms/Button.mdx rename to docs/docs/components/forms/Button.mdx diff --git a/documentation/docs/components/forms/CheckBox.mdx b/docs/docs/components/forms/CheckBox.mdx similarity index 100% rename from documentation/docs/components/forms/CheckBox.mdx rename to docs/docs/components/forms/CheckBox.mdx diff --git a/documentation/docs/components/forms/Icon Button.mdx b/docs/docs/components/forms/Icon Button.mdx similarity index 100% rename from documentation/docs/components/forms/Icon Button.mdx rename to docs/docs/components/forms/Icon Button.mdx diff --git a/documentation/docs/components/forms/Input.mdx b/docs/docs/components/forms/Input.mdx similarity index 100% rename from documentation/docs/components/forms/Input.mdx rename to docs/docs/components/forms/Input.mdx diff --git a/documentation/docs/components/forms/Pressable.mdx b/docs/docs/components/forms/Pressable.mdx similarity index 96% rename from documentation/docs/components/forms/Pressable.mdx rename to docs/docs/components/forms/Pressable.mdx index 3e2dc564..7723b066 100644 --- a/documentation/docs/components/forms/Pressable.mdx +++ b/docs/docs/components/forms/Pressable.mdx @@ -12,7 +12,7 @@ import SourceButton from "../../../src/components/SourceButton/SourceButton"; /> -The `Pressable` component is a wrapper around the [React Native Pressable](https://reactnative.dev/docs/pressable) component. It is designed to incorporate Pearl style props, making it a versatile tool for creating interactive components such as buttons, links, and more. +The `Pressable` is a functional component use to make interactive elements such as buttons, links, and more. ## Importing the Component diff --git a/documentation/docs/components/forms/Radio.mdx b/docs/docs/components/forms/Radio.mdx similarity index 100% rename from documentation/docs/components/forms/Radio.mdx rename to docs/docs/components/forms/Radio.mdx diff --git a/documentation/docs/components/forms/Switch.mdx b/docs/docs/components/forms/Switch.mdx similarity index 100% rename from documentation/docs/components/forms/Switch.mdx rename to docs/docs/components/forms/Switch.mdx diff --git a/documentation/docs/components/forms/Text Link.mdx b/docs/docs/components/forms/Text Link.mdx similarity index 100% rename from documentation/docs/components/forms/Text Link.mdx rename to docs/docs/components/forms/Text Link.mdx diff --git a/documentation/docs/components/forms/Textarea.mdx b/docs/docs/components/forms/Textarea.mdx similarity index 100% rename from documentation/docs/components/forms/Textarea.mdx rename to docs/docs/components/forms/Textarea.mdx diff --git a/documentation/docs/components/forms/_category_.json b/docs/docs/components/forms/_category_.json similarity index 100% rename from documentation/docs/components/forms/_category_.json rename to docs/docs/components/forms/_category_.json diff --git a/documentation/docs/components/layout/Box.mdx b/docs/docs/components/layout/Box.mdx similarity index 99% rename from documentation/docs/components/layout/Box.mdx rename to docs/docs/components/layout/Box.mdx index d2b3a4cf..cf90d8c5 100644 --- a/documentation/docs/components/layout/Box.mdx +++ b/docs/docs/components/layout/Box.mdx @@ -48,6 +48,7 @@ The `Box` component supports the following style props: - [backgroundColor](../../core-features/style-props#color-and-background-color) (Note: Only `backgroundColor` props are supported, not the `color` prop) - [layout](../../core-features/style-props#layout) +- [transform](../../core-features/style-props#transform) - [position](../../core-features/style-props#position) - [opacity and visibility](../../core-features/style-props#opacity-and-visibility) - [margin and padding](../../core-features/style-props#margin-and-padding) diff --git a/documentation/docs/components/layout/Center.mdx b/docs/docs/components/layout/Center.mdx similarity index 100% rename from documentation/docs/components/layout/Center.mdx rename to docs/docs/components/layout/Center.mdx diff --git a/documentation/docs/components/layout/Divider.mdx b/docs/docs/components/layout/Divider.mdx similarity index 99% rename from documentation/docs/components/layout/Divider.mdx rename to docs/docs/components/layout/Divider.mdx index 13ac29c8..0e99557b 100644 --- a/documentation/docs/components/layout/Divider.mdx +++ b/docs/docs/components/layout/Divider.mdx @@ -77,6 +77,7 @@ The `Divider` component supports the following style props: - [backgroundColor](../../core-features/style-props#color-and-background-color) (Only `backgroundColor` props are supported, not the `color` prop) - [layout](../../core-features/style-props#layout) +- [transform](../../core-features/style-props#transform) - [position](../../core-features/style-props#position) - [opacity and visiblity](../../core-features/style-props#opacity-and-visibility) - [margin and padding](../../core-features/style-props#margin-and-padding) diff --git a/documentation/docs/components/layout/Screen.mdx b/docs/docs/components/layout/Screen.mdx similarity index 100% rename from documentation/docs/components/layout/Screen.mdx rename to docs/docs/components/layout/Screen.mdx diff --git a/documentation/docs/components/layout/Spacer.mdx b/docs/docs/components/layout/Spacer.mdx similarity index 100% rename from documentation/docs/components/layout/Spacer.mdx rename to docs/docs/components/layout/Spacer.mdx diff --git a/documentation/docs/components/layout/Stack.mdx b/docs/docs/components/layout/Stack.mdx similarity index 100% rename from documentation/docs/components/layout/Stack.mdx rename to docs/docs/components/layout/Stack.mdx diff --git a/documentation/docs/components/layout/_category_.json b/docs/docs/components/layout/_category_.json similarity index 100% rename from documentation/docs/components/layout/_category_.json rename to docs/docs/components/layout/_category_.json diff --git a/documentation/docs/components/media/Avatar.mdx b/docs/docs/components/media/Avatar.mdx similarity index 100% rename from documentation/docs/components/media/Avatar.mdx rename to docs/docs/components/media/Avatar.mdx diff --git a/documentation/docs/components/media/Icon.mdx b/docs/docs/components/media/Icon.mdx similarity index 99% rename from documentation/docs/components/media/Icon.mdx rename to docs/docs/components/media/Icon.mdx index 6bf4165d..c90c8e97 100644 --- a/documentation/docs/components/media/Icon.mdx +++ b/docs/docs/components/media/Icon.mdx @@ -86,6 +86,7 @@ The `Icon` component supports the following style props: - [color and backgroundColor](../../core-features/style-props#color-and-background-color) - [margin and padding](../../core-features/style-props#margin-and-padding) - [layout](../../core-features/style-props#layout) +- [transform](../../core-features/style-props#transform) - [opacity and visibility](../../core-features/style-props#opacity-and-visibility) ### Animation Properties diff --git a/documentation/docs/components/media/Image.mdx b/docs/docs/components/media/Image.mdx similarity index 98% rename from documentation/docs/components/media/Image.mdx rename to docs/docs/components/media/Image.mdx index a55d2f5a..8620aae2 100644 --- a/documentation/docs/components/media/Image.mdx +++ b/docs/docs/components/media/Image.mdx @@ -251,7 +251,7 @@ Apart from the properties listed below, the `Image` component also inherits all | `isCached` | No | boolean | `true` | Determines if a remote image should be cached. | | `loaderType` | No | "progressive" \| "spinner" | `"spinner"` | Specifies the type of loader to display until the image is fully loaded. | | `previewSource` | No | [ImageSource](https://reactnative.dev/docs/image#imagesource) | | Specifies the source of the placeholder image while the remote image is loading. | -| `previewColor` | No | string | | Defines the color of the image container while the remote image is loading. | +| `previewColor` | No | PearlTheme["palette"] | | Defines the color of the image container while the remote image is loading. | | `overlayTransitionDuration` | No | number | `300` | Specifies the duration (in ms) for the progressive loading overlay to fade after the image loads. | | `imageDownloadOptions` | No | [DownloadOptions](https://docs.expo.dev/versions/latest/sdk/filesystem/#arguments-8) | {} | Configures the download options when fetching the remote image. | | `tint` | No | "dark" \| "light" \| "default" | `"dark"` | Specifies the tint of the progressive loading overlay. | diff --git a/documentation/docs/components/media/Video.mdx b/docs/docs/components/media/Video.mdx similarity index 100% rename from documentation/docs/components/media/Video.mdx rename to docs/docs/components/media/Video.mdx diff --git a/documentation/docs/components/media/_category_.json b/docs/docs/components/media/_category_.json similarity index 100% rename from documentation/docs/components/media/_category_.json rename to docs/docs/components/media/_category_.json diff --git a/documentation/docs/components/transitions/Collapse.mdx b/docs/docs/components/transitions/Collapse.mdx similarity index 100% rename from documentation/docs/components/transitions/Collapse.mdx rename to docs/docs/components/transitions/Collapse.mdx diff --git a/documentation/docs/components/transitions/Fade.mdx b/docs/docs/components/transitions/Fade.mdx similarity index 100% rename from documentation/docs/components/transitions/Fade.mdx rename to docs/docs/components/transitions/Fade.mdx diff --git a/documentation/docs/components/transitions/Scale Fade.mdx b/docs/docs/components/transitions/Scale Fade.mdx similarity index 100% rename from documentation/docs/components/transitions/Scale Fade.mdx rename to docs/docs/components/transitions/Scale Fade.mdx diff --git a/documentation/docs/components/transitions/Slide Fade.mdx b/docs/docs/components/transitions/Slide Fade.mdx similarity index 100% rename from documentation/docs/components/transitions/Slide Fade.mdx rename to docs/docs/components/transitions/Slide Fade.mdx diff --git a/documentation/docs/components/transitions/Slide.mdx b/docs/docs/components/transitions/Slide.mdx similarity index 100% rename from documentation/docs/components/transitions/Slide.mdx rename to docs/docs/components/transitions/Slide.mdx diff --git a/documentation/docs/components/transitions/_category_.json b/docs/docs/components/transitions/_category_.json similarity index 100% rename from documentation/docs/components/transitions/_category_.json rename to docs/docs/components/transitions/_category_.json diff --git a/documentation/docs/components/typography/Text.mdx b/docs/docs/components/typography/Text.mdx similarity index 99% rename from documentation/docs/components/typography/Text.mdx rename to docs/docs/components/typography/Text.mdx index cd12365f..5245f008 100644 --- a/documentation/docs/components/typography/Text.mdx +++ b/docs/docs/components/typography/Text.mdx @@ -74,6 +74,7 @@ The `Text` component supports the following style props: - [color and backgroundColor](../../core-features/style-props#color-and-background-color) - [typography](../../core-features/style-props#typography) - [layout](../../core-features/style-props#layout) +- [transform](../../core-features/style-props#transform) - [opacity and visiblity](../../core-features/style-props#opacity-and-visibility) - [margin and padding](../../core-features/style-props#margin-and-padding) - [textShadow](../../core-features/style-props#text-shadow) diff --git a/documentation/docs/components/typography/_category_.json b/docs/docs/components/typography/_category_.json similarity index 100% rename from documentation/docs/components/typography/_category_.json rename to docs/docs/components/typography/_category_.json diff --git a/documentation/docs/core-features/_category_.json b/docs/docs/core-features/_category_.json similarity index 100% rename from documentation/docs/core-features/_category_.json rename to docs/docs/core-features/_category_.json diff --git a/documentation/docs/core-features/animation-support.mdx b/docs/docs/core-features/animation-support.mdx similarity index 100% rename from documentation/docs/core-features/animation-support.mdx rename to docs/docs/core-features/animation-support.mdx diff --git a/documentation/docs/core-features/dark-mode.mdx b/docs/docs/core-features/dark-mode.mdx similarity index 100% rename from documentation/docs/core-features/dark-mode.mdx rename to docs/docs/core-features/dark-mode.mdx diff --git a/documentation/docs/core-features/extensibility.mdx b/docs/docs/core-features/extensibility.mdx similarity index 92% rename from documentation/docs/core-features/extensibility.mdx rename to docs/docs/core-features/extensibility.mdx index 98f30fc1..cb551b9a 100644 --- a/documentation/docs/core-features/extensibility.mdx +++ b/docs/docs/core-features/extensibility.mdx @@ -43,11 +43,24 @@ import { MolecularComponentConfig, } from "pearl-ui"; -const CustomActionButton = (props) => { - return ; +type ActionButtonAtoms = { + button: ButtonProps; }; -const molecularConfig: MolecularComponentConfig = { +const CustomActionButton = ({ + atoms, +}: Omit< + MoleculeComponentProps<"MyActionButton", ButtonProps, ActionButtonAtoms>, + "atoms" +> & { + atoms?: ActionButtonAtoms; +}) => { + return ( + + ); +}; + +const molecularConfig: MolecularComponentConfig = { parts: ["button"], baseStyle: { button: { diff --git a/documentation/docs/core-features/responsivity.md b/docs/docs/core-features/responsivity.md similarity index 100% rename from documentation/docs/core-features/responsivity.md rename to docs/docs/core-features/responsivity.md diff --git a/documentation/docs/core-features/style-props.md b/docs/docs/core-features/style-props.md similarity index 95% rename from documentation/docs/core-features/style-props.md rename to docs/docs/core-features/style-props.md index 8bb3356f..ffbbd9af 100644 --- a/documentation/docs/core-features/style-props.md +++ b/docs/docs/core-features/style-props.md @@ -109,6 +109,31 @@ The following sections provide a comprehensive list of supported style propertie | `flexShrink` | `flexShrink` | number | | `flexWrap` | `flexWrap` | 'wrap' \| 'nowrap' \| 'wrap-reverse' | +### Transform + +```jsx + + Transformed Box + +``` + +| Prop | Stylesheet property | Type | +| ----------------- | ------------------- | ------------- | +| `transform` | `transform` | array | +| `transformMatrix` | `transformMatrix` | array | +| `rotation` | `rotation` | number | +| `scaleX` | `scaleX` | number | +| `scaleY` | `scaleY` | number | +| `translateX` | `translateX` | number | +| `translateY` | `translateY` | number | + ### Position ```jsx diff --git a/documentation/docs/getting-started/_category_.json b/docs/docs/getting-started/_category_.json similarity index 100% rename from documentation/docs/getting-started/_category_.json rename to docs/docs/getting-started/_category_.json diff --git a/documentation/docs/getting-started/design-principles.md b/docs/docs/getting-started/design-principles.md similarity index 100% rename from documentation/docs/getting-started/design-principles.md rename to docs/docs/getting-started/design-principles.md diff --git a/documentation/docs/getting-started/installation.md b/docs/docs/getting-started/installation.md similarity index 100% rename from documentation/docs/getting-started/installation.md rename to docs/docs/getting-started/installation.md diff --git a/documentation/docs/getting-started/introduction.mdx b/docs/docs/getting-started/introduction.mdx similarity index 100% rename from documentation/docs/getting-started/introduction.mdx rename to docs/docs/getting-started/introduction.mdx diff --git a/documentation/docs/hooks/_category_.json b/docs/docs/hooks/_category_.json similarity index 100% rename from documentation/docs/hooks/_category_.json rename to docs/docs/hooks/_category_.json diff --git a/documentation/docs/hooks/pearl.md b/docs/docs/hooks/pearl.md similarity index 100% rename from documentation/docs/hooks/pearl.md rename to docs/docs/hooks/pearl.md diff --git a/documentation/docs/hooks/useAccessibleColor.md b/docs/docs/hooks/useAccessibleColor.md similarity index 100% rename from documentation/docs/hooks/useAccessibleColor.md rename to docs/docs/hooks/useAccessibleColor.md diff --git a/documentation/docs/hooks/useAnimationState.md b/docs/docs/hooks/useAnimationState.md similarity index 100% rename from documentation/docs/hooks/useAnimationState.md rename to docs/docs/hooks/useAnimationState.md diff --git a/documentation/docs/hooks/useAtomicComponentConfig.mdx b/docs/docs/hooks/useAtomicComponentConfig.mdx similarity index 100% rename from documentation/docs/hooks/useAtomicComponentConfig.mdx rename to docs/docs/hooks/useAtomicComponentConfig.mdx diff --git a/documentation/docs/hooks/useCheckedState.mdx b/docs/docs/hooks/useCheckedState.mdx similarity index 100% rename from documentation/docs/hooks/useCheckedState.mdx rename to docs/docs/hooks/useCheckedState.mdx diff --git a/documentation/docs/hooks/useColorModeValue.md b/docs/docs/hooks/useColorModeValue.md similarity index 100% rename from documentation/docs/hooks/useColorModeValue.md rename to docs/docs/hooks/useColorModeValue.md diff --git a/documentation/docs/hooks/useColorScheme.md b/docs/docs/hooks/useColorScheme.md similarity index 100% rename from documentation/docs/hooks/useColorScheme.md rename to docs/docs/hooks/useColorScheme.md diff --git a/documentation/docs/hooks/useDimensions.mdx b/docs/docs/hooks/useDimensions.mdx similarity index 100% rename from documentation/docs/hooks/useDimensions.mdx rename to docs/docs/hooks/useDimensions.mdx diff --git a/documentation/docs/hooks/useDisabledState.mdx b/docs/docs/hooks/useDisabledState.mdx similarity index 100% rename from documentation/docs/hooks/useDisabledState.mdx rename to docs/docs/hooks/useDisabledState.mdx diff --git a/documentation/docs/hooks/useDynamicStateStyle.mdx b/docs/docs/hooks/useDynamicStateStyle.mdx similarity index 100% rename from documentation/docs/hooks/useDynamicStateStyle.mdx rename to docs/docs/hooks/useDynamicStateStyle.mdx diff --git a/documentation/docs/hooks/useFocusedState.mdx b/docs/docs/hooks/useFocusedState.mdx similarity index 100% rename from documentation/docs/hooks/useFocusedState.mdx rename to docs/docs/hooks/useFocusedState.mdx diff --git a/documentation/docs/hooks/useInvalidState.mdx b/docs/docs/hooks/useInvalidState.mdx similarity index 100% rename from documentation/docs/hooks/useInvalidState.mdx rename to docs/docs/hooks/useInvalidState.mdx diff --git a/documentation/docs/hooks/useMolecularComponentConfig.mdx b/docs/docs/hooks/useMolecularComponentConfig.mdx similarity index 100% rename from documentation/docs/hooks/useMolecularComponentConfig.mdx rename to docs/docs/hooks/useMolecularComponentConfig.mdx diff --git a/documentation/docs/hooks/useMotiWithStyleProps.mdx b/docs/docs/hooks/useMotiWithStyleProps.mdx similarity index 100% rename from documentation/docs/hooks/useMotiWithStyleProps.mdx rename to docs/docs/hooks/useMotiWithStyleProps.mdx diff --git a/documentation/docs/hooks/usePressedState.mdx b/docs/docs/hooks/usePressedState.mdx similarity index 100% rename from documentation/docs/hooks/usePressedState.mdx rename to docs/docs/hooks/usePressedState.mdx diff --git a/documentation/docs/hooks/useResponsiveProp.mdx b/docs/docs/hooks/useResponsiveProp.mdx similarity index 100% rename from documentation/docs/hooks/useResponsiveProp.mdx rename to docs/docs/hooks/useResponsiveProp.mdx diff --git a/documentation/docs/hooks/useStyleProps.mdx b/docs/docs/hooks/useStyleProps.mdx similarity index 100% rename from documentation/docs/hooks/useStyleProps.mdx rename to docs/docs/hooks/useStyleProps.mdx diff --git a/documentation/docs/hooks/useTheme.md b/docs/docs/hooks/useTheme.md similarity index 100% rename from documentation/docs/hooks/useTheme.md rename to docs/docs/hooks/useTheme.md diff --git a/documentation/docs/others/_category_.json b/docs/docs/others/_category_.json similarity index 100% rename from documentation/docs/others/_category_.json rename to docs/docs/others/_category_.json diff --git a/documentation/docs/others/generatePalette.mdx b/docs/docs/others/generatePalette.mdx similarity index 100% rename from documentation/docs/others/generatePalette.mdx rename to docs/docs/others/generatePalette.mdx diff --git a/documentation/docs/others/style-functions.md b/docs/docs/others/style-functions.md similarity index 94% rename from documentation/docs/others/style-functions.md rename to docs/docs/others/style-functions.md index bb20c685..f2708efe 100644 --- a/documentation/docs/others/style-functions.md +++ b/docs/docs/others/style-functions.md @@ -19,6 +19,7 @@ These are the style functions used to construct the [Box](../components/layout/B - [Opacity](#opacity) - [Visibility](#visibility) - [Layout](#layout) +- [Transform](#transform) - [Spacing](#spacing) - [Border](#border) - [Shadow](#shadow) @@ -37,6 +38,7 @@ These are the style functions used to construct the [Text](../components/typogra - [Opacity](#opacity) - [Visibility](#visibility) - [Layout](#layout) +- [Transform](#transform) - [Typography](#typography) - [Spacing](#spacing) - [Position](#position) @@ -114,6 +116,14 @@ The **layoutStyleFunction** converts all the [layout style props](../core-featur import { layoutStyleFunction } from "pearl-ui"; ``` +### Transform + +The **transformStyleFunction** converts all the [transform style props](../core-features/style-props#transform) to React Native styles. + +```js +import { transformStyleFunction } from "pearl-ui"; +``` + ### Position The **positionStyleFunction** converts all the [position style props](../core-features/style-props#position) to React Native styles. diff --git a/documentation/docs/theming/_category_.json b/docs/docs/theming/_category_.json similarity index 100% rename from documentation/docs/theming/_category_.json rename to docs/docs/theming/_category_.json diff --git a/documentation/docs/theming/customize-theme.md b/docs/docs/theming/customize-theme.md similarity index 100% rename from documentation/docs/theming/customize-theme.md rename to docs/docs/theming/customize-theme.md diff --git a/documentation/docs/theming/default-theme.mdx b/docs/docs/theming/default-theme.mdx similarity index 98% rename from documentation/docs/theming/default-theme.mdx rename to docs/docs/theming/default-theme.mdx index 36cdcce8..4903d9dc 100644 --- a/documentation/docs/theming/default-theme.mdx +++ b/docs/docs/theming/default-theme.mdx @@ -650,6 +650,7 @@ We use a t-shirt size naming convention for our sizing system, but it's customiz // theme.js export default { borderRadii: { + none: 0, xs: 2, s: 4, m: 8, @@ -664,6 +665,7 @@ export default { import BorderRadiiBox from "../../src/components/BorderRadiiBox/BorderRadiiBox";
+ @@ -682,6 +684,16 @@ Our sizing system follows a t-shirt size naming convention, but it's flexible an // theme.js export default { elevation: { + none: { + shadowColor: "#1A2138", + shadowOffset: { + width: 0, + height: 0, + }, + shadowOpacity: 0, + shadowRadius: 0, + elevation: 0, + }, xs: { shadowColor: "#1A2138", shadowOffset: { @@ -776,6 +788,10 @@ import ElevationBox from "../../src/components/ElevationBox/ElevationBox"; marginTop: 40, }} > + diff --git a/documentation/docs/theming/typescript-support.md b/docs/docs/theming/typescript-support.md similarity index 100% rename from documentation/docs/theming/typescript-support.md rename to docs/docs/theming/typescript-support.md diff --git a/documentation/docusaurus.config.js b/docs/docusaurus.config.js similarity index 97% rename from documentation/docusaurus.config.js rename to docs/docusaurus.config.js index baf11393..d286cac4 100644 --- a/documentation/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -24,7 +24,7 @@ sidebarPath: require.resolve("./sidebars.js"), sidebarCollapsible: false, editUrl: - "https://github.com/agrawal-rohit/pearl-ui/tree/main/documentation/", + "https://github.com/agrawal-rohit/pearl-ui/tree/main/docs/", }, theme: { customCss: require.resolve("./src/css/custom.css"), @@ -82,7 +82,7 @@ }, items: [ { - to: "/docs/getting-started/introduction", // ./docs/Intro.md + to: "/docs/getting-started/introduction", label: "Docs", position: "left", activeBasePath: "docs", diff --git a/documentation/package.json b/docs/package.json similarity index 98% rename from documentation/package.json rename to docs/package.json index 7738b240..feb66368 100644 --- a/documentation/package.json +++ b/docs/package.json @@ -1,5 +1,5 @@ { - "name": "documentation", + "name": "docs", "version": "0.0.0", "private": true, "scripts": { diff --git a/documentation/sidebars.js b/docs/sidebars.js similarity index 99% rename from documentation/sidebars.js rename to docs/sidebars.js index 9df9b834..d7282d3b 100644 --- a/documentation/sidebars.js +++ b/docs/sidebars.js @@ -12,7 +12,6 @@ module.exports = { // By default, Docusaurus generates a sidebar from the docs folder structure tutorialSidebar: [{ type: "autogenerated", dirName: "." }], - // But you can create a sidebar manually /* tutorialSidebar: [ diff --git a/documentation/src/components/BorderRadiiBox/BorderRadiiBox.module.css b/docs/src/components/BorderRadiiBox/BorderRadiiBox.module.css similarity index 100% rename from documentation/src/components/BorderRadiiBox/BorderRadiiBox.module.css rename to docs/src/components/BorderRadiiBox/BorderRadiiBox.module.css diff --git a/documentation/src/components/BorderRadiiBox/BorderRadiiBox.tsx b/docs/src/components/BorderRadiiBox/BorderRadiiBox.tsx similarity index 100% rename from documentation/src/components/BorderRadiiBox/BorderRadiiBox.tsx rename to docs/src/components/BorderRadiiBox/BorderRadiiBox.tsx diff --git a/documentation/src/components/ElevationBox/ElevationBox.module.css b/docs/src/components/ElevationBox/ElevationBox.module.css similarity index 100% rename from documentation/src/components/ElevationBox/ElevationBox.module.css rename to docs/src/components/ElevationBox/ElevationBox.module.css diff --git a/documentation/src/components/ElevationBox/ElevationBox.tsx b/docs/src/components/ElevationBox/ElevationBox.tsx similarity index 100% rename from documentation/src/components/ElevationBox/ElevationBox.tsx rename to docs/src/components/ElevationBox/ElevationBox.tsx diff --git a/documentation/src/components/ExpoSnack.tsx b/docs/src/components/ExpoSnack.tsx similarity index 100% rename from documentation/src/components/ExpoSnack.tsx rename to docs/src/components/ExpoSnack.tsx diff --git a/documentation/src/components/PaletteColor/PaletteColor.module.css b/docs/src/components/PaletteColor/PaletteColor.module.css similarity index 100% rename from documentation/src/components/PaletteColor/PaletteColor.module.css rename to docs/src/components/PaletteColor/PaletteColor.module.css diff --git a/documentation/src/components/PaletteColor/PaletteColor.tsx b/docs/src/components/PaletteColor/PaletteColor.tsx similarity index 100% rename from documentation/src/components/PaletteColor/PaletteColor.tsx rename to docs/src/components/PaletteColor/PaletteColor.tsx diff --git a/documentation/src/components/Props/Props.module.css b/docs/src/components/Props/Props.module.css similarity index 100% rename from documentation/src/components/Props/Props.module.css rename to docs/src/components/Props/Props.module.css diff --git a/documentation/src/components/Props/Props.tsx b/docs/src/components/Props/Props.tsx similarity index 100% rename from documentation/src/components/Props/Props.tsx rename to docs/src/components/Props/Props.tsx diff --git a/documentation/src/components/SourceButton/SourceButton.module.css b/docs/src/components/SourceButton/SourceButton.module.css similarity index 100% rename from documentation/src/components/SourceButton/SourceButton.module.css rename to docs/src/components/SourceButton/SourceButton.module.css diff --git a/documentation/src/components/SourceButton/SourceButton.tsx b/docs/src/components/SourceButton/SourceButton.tsx similarity index 100% rename from documentation/src/components/SourceButton/SourceButton.tsx rename to docs/src/components/SourceButton/SourceButton.tsx diff --git a/documentation/src/components/SpacingBox/SpacingBox.tsx b/docs/src/components/SpacingBox/SpacingBox.tsx similarity index 100% rename from documentation/src/components/SpacingBox/SpacingBox.tsx rename to docs/src/components/SpacingBox/SpacingBox.tsx diff --git a/documentation/src/components/TypographyVariant/TypographyVariant.module.css b/docs/src/components/TypographyVariant/TypographyVariant.module.css similarity index 100% rename from documentation/src/components/TypographyVariant/TypographyVariant.module.css rename to docs/src/components/TypographyVariant/TypographyVariant.module.css diff --git a/documentation/src/components/TypographyVariant/TypographyVariant.tsx b/docs/src/components/TypographyVariant/TypographyVariant.tsx similarity index 100% rename from documentation/src/components/TypographyVariant/TypographyVariant.tsx rename to docs/src/components/TypographyVariant/TypographyVariant.tsx diff --git a/documentation/src/css/custom.css b/docs/src/css/custom.css similarity index 99% rename from documentation/src/css/custom.css rename to docs/src/css/custom.css index 54b74a25..a8b8cd85 100644 --- a/documentation/src/css/custom.css +++ b/docs/src/css/custom.css @@ -224,6 +224,8 @@ html[data-theme="dark"] .header-twitter-link:before { */ .menu__link[href*="pearl"]:before, .menu__link[href*="/Button"]:before, +.menu__link[href*="/style-props"]:before, +.menu__link[href*="/style-functions"]:before, .menu__link[href*="extensibility"]:before { content: "Updated"; display: inline-block; diff --git a/documentation/src/pages/index.module.css b/docs/src/pages/index.module.css similarity index 100% rename from documentation/src/pages/index.module.css rename to docs/src/pages/index.module.css diff --git a/documentation/src/pages/index.tsx b/docs/src/pages/index.tsx similarity index 100% rename from documentation/src/pages/index.tsx rename to docs/src/pages/index.tsx diff --git a/documentation/static/.nojekyll b/docs/static/.nojekyll similarity index 100% rename from documentation/static/.nojekyll rename to docs/static/.nojekyll diff --git a/documentation/static/img/android_elevation.png b/docs/static/img/android_elevation.png similarity index 100% rename from documentation/static/img/android_elevation.png rename to docs/static/img/android_elevation.png diff --git a/documentation/static/img/component_styles_icon_dark.png b/docs/static/img/component_styles_icon_dark.png similarity index 100% rename from documentation/static/img/component_styles_icon_dark.png rename to docs/static/img/component_styles_icon_dark.png diff --git a/documentation/static/img/component_styles_icon_light.png b/docs/static/img/component_styles_icon_light.png similarity index 100% rename from documentation/static/img/component_styles_icon_light.png rename to docs/static/img/component_styles_icon_light.png diff --git a/documentation/static/img/discord_black.svg b/docs/static/img/discord_black.svg similarity index 100% rename from documentation/static/img/discord_black.svg rename to docs/static/img/discord_black.svg diff --git a/documentation/static/img/discord_white.svg b/docs/static/img/discord_white.svg similarity index 100% rename from documentation/static/img/discord_white.svg rename to docs/static/img/discord_white.svg diff --git a/documentation/static/img/favicon.ico b/docs/static/img/favicon.ico similarity index 100% rename from documentation/static/img/favicon.ico rename to docs/static/img/favicon.ico diff --git a/documentation/static/img/feature_elevation.png b/docs/static/img/feature_elevation.png similarity index 100% rename from documentation/static/img/feature_elevation.png rename to docs/static/img/feature_elevation.png diff --git a/documentation/static/img/feature_palette.png b/docs/static/img/feature_palette.png similarity index 100% rename from documentation/static/img/feature_palette.png rename to docs/static/img/feature_palette.png diff --git a/documentation/static/img/feature_spacing.png b/docs/static/img/feature_spacing.png similarity index 100% rename from documentation/static/img/feature_spacing.png rename to docs/static/img/feature_spacing.png diff --git a/documentation/static/img/feature_typography.png b/docs/static/img/feature_typography.png similarity index 100% rename from documentation/static/img/feature_typography.png rename to docs/static/img/feature_typography.png diff --git a/documentation/static/img/ios_elevation.png b/docs/static/img/ios_elevation.png similarity index 100% rename from documentation/static/img/ios_elevation.png rename to docs/static/img/ios_elevation.png diff --git a/documentation/static/img/logo.png b/docs/static/img/logo.png similarity index 100% rename from documentation/static/img/logo.png rename to docs/static/img/logo.png diff --git a/documentation/static/img/logoDark.png b/docs/static/img/logoDark.png similarity index 100% rename from documentation/static/img/logoDark.png rename to docs/static/img/logoDark.png diff --git a/documentation/static/img/responsivity_phone_demo.png b/docs/static/img/responsivity_phone_demo.png similarity index 100% rename from documentation/static/img/responsivity_phone_demo.png rename to docs/static/img/responsivity_phone_demo.png diff --git a/documentation/static/img/responsivity_tablet_demo.png b/docs/static/img/responsivity_tablet_demo.png similarity index 100% rename from documentation/static/img/responsivity_tablet_demo.png rename to docs/static/img/responsivity_tablet_demo.png diff --git a/documentation/static/img/twitter_black.svg b/docs/static/img/twitter_black.svg similarity index 100% rename from documentation/static/img/twitter_black.svg rename to docs/static/img/twitter_black.svg diff --git a/documentation/static/img/twitter_white.svg b/docs/static/img/twitter_white.svg similarity index 100% rename from documentation/static/img/twitter_white.svg rename to docs/static/img/twitter_white.svg diff --git a/documentation/static/img/typescript_example.png b/docs/static/img/typescript_example.png similarity index 100% rename from documentation/static/img/typescript_example.png rename to docs/static/img/typescript_example.png diff --git a/documentation/tsconfig.json b/docs/tsconfig.json similarity index 100% rename from documentation/tsconfig.json rename to docs/tsconfig.json diff --git a/documentation/yarn.lock b/docs/yarn.lock similarity index 100% rename from documentation/yarn.lock rename to docs/yarn.lock diff --git a/package.json b/package.json index 16a33dc7..5e833d4e 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "@types/react": "18.2.28", "@types/react-native": "0.72.3", "@types/react-test-renderer": "18.0.3", + "@welldone-software/why-did-you-render": "^7.0.1", "babel-loader": "^9.1.3", "commitizen": "^4.3.0", "cz-conventional-changelog": "^3.3.0", @@ -100,7 +101,7 @@ ], "modulePathIgnorePatterns": [ "/lib/", - "/documentation/" + "/docs/" ] }, "files": [ @@ -155,4 +156,4 @@ } ] } -} +} \ No newline at end of file diff --git a/src/components/Atoms/Center/Center.tsx b/src/components/Atoms/Center/Center.tsx index aae58084..8c5af3ed 100644 --- a/src/components/Atoms/Center/Center.tsx +++ b/src/components/Atoms/Center/Center.tsx @@ -4,13 +4,15 @@ import Box, { BoxProps } from "../box/box"; /** * A layout component that centers its child within itself across both axes */ -const Center = React.forwardRef(({ children, ...rest }: BoxProps, ref: any) => { - return ( - - {children} - - ); -}); +const Center = React.memo( + React.forwardRef(({ children, ...rest }: BoxProps, ref: any) => { + return ( + + {children} + + ); + }) +); Center.displayName = "Center"; diff --git a/src/components/Atoms/Divider/Divider.config.ts b/src/components/Atoms/Divider/Divider.config.ts index 5b460e91..d8a57f5c 100644 --- a/src/components/Atoms/Divider/Divider.config.ts +++ b/src/components/Atoms/Divider/Divider.config.ts @@ -1,11 +1,16 @@ -export default { +import { AtomicComponentConfig } from "../../../theme/src/types"; +import { DividerProps } from "./divider"; + +const DividerConfig: AtomicComponentConfig = { baseStyle: { orientation: "horizontal", bgColor: { light: "neutral.300", - dark: "neutral.900", + dark: "neutral.600", }, thickness: 1, length: "100%", }, }; + +export default DividerConfig; diff --git a/src/components/Atoms/Divider/Divider.tsx b/src/components/Atoms/Divider/Divider.tsx index 754a78fc..bc1caf50 100644 --- a/src/components/Atoms/Divider/Divider.tsx +++ b/src/components/Atoms/Divider/Divider.tsx @@ -33,32 +33,35 @@ type BaseDividerProps = BoxProps & { * CustomDivider is a functional component that returns a Box component with specific styles based on the props. * It uses the forwardRef function from React to pass the ref to the Box component. */ -const CustomDivider = React.forwardRef( - ( - { - children, - length = "100%", - thickness = 1, - orientation = "horizontal", - ...props - }: AtomComponentProps<"Divider", BaseDividerProps>, - ref: any - ) => { - // Styles are applied based on the orientation of the divider - return ( - , + ref: any + ) => { + // Styles are applied based on the orientation of the divider + const style = React.useMemo( + () => ({ ...(props.style as any), height: orientation === "horizontal" ? thickness : length, width: orientation === "vertical" ? thickness : length, - }} - > - {children} - - ); - } + }), + [props.style, orientation, thickness, length] + ); + + return ( + + {children} + + ); + } + ) ); /** diff --git a/src/components/Atoms/Icon/Icon.config.ts b/src/components/Atoms/Icon/Icon.config.ts index 029cf01a..d1156241 100644 --- a/src/components/Atoms/Icon/Icon.config.ts +++ b/src/components/Atoms/Icon/Icon.config.ts @@ -1,4 +1,7 @@ -export default { +import { AtomicComponentConfig } from "../../../theme/src/types"; +import { IconProps } from "./icon"; + +const IconConfig: AtomicComponentConfig = { baseStyle: { color: { light: "neutral.900", @@ -23,3 +26,5 @@ export default { size: "m", }, }; + +export default IconConfig; diff --git a/src/components/Atoms/Icon/Icon.tsx b/src/components/Atoms/Icon/Icon.tsx index 9588e279..38cd7f15 100644 --- a/src/components/Atoms/Icon/Icon.tsx +++ b/src/components/Atoms/Icon/Icon.tsx @@ -54,7 +54,7 @@ type IconStyleProps = ColorProps & VisibleProps; // BaseIconProps defines the basic properties for the Icon component -type BaseIconProps = React.ComponentProps & { +export type BaseIconProps = React.ComponentProps & { /** Icon family that contains the icon you want to use */ iconFamily: | "AntDesign" @@ -98,33 +98,35 @@ const iconFamilyMapping = { }; // CustomIcon is a wrapper around the icon components that allows for additional customization -const CustomIcon = React.forwardRef( - ( - { - iconFamily, - iconName, - accessibilityLabel = undefined, - rawSize = undefined, - ...props - }: AtomComponentProps<"Icon", BaseIconProps, IconStyleProps>, - ref: any - ) => { - // Select the appropriate icon component based on the iconFamily prop - const IconToUse = iconFamilyMapping[iconFamily]; +const CustomIcon = React.memo( + React.forwardRef( + ( + { + iconFamily, + iconName, + accessibilityLabel = undefined, + rawSize = undefined, + ...props + }: AtomComponentProps<"Icon", BaseIconProps, IconStyleProps>, + ref: any + ) => { + // Select the appropriate icon component based on the iconFamily prop + const IconToUse = iconFamilyMapping[iconFamily]; - return ( - - ); - } + return ( + + ); + } + ) ); /** The `Icon` component can used to add Expo Icons to your app and customize them using style props. */ diff --git a/src/components/Atoms/Pressable/Pressable.tsx b/src/components/Atoms/Pressable/Pressable.tsx index c2eb42e7..387e84fa 100644 --- a/src/components/Atoms/Pressable/Pressable.tsx +++ b/src/components/Atoms/Pressable/Pressable.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from "react"; +import React from "react"; import { BoxProps } from "../box/box"; import { boxStyleFunctions } from "../../../theme/src/style-functions"; import { BasicComponentProps, StateProps } from "../../../theme/src/types"; @@ -15,7 +15,34 @@ import { useMotiWithStyleProps } from "../../../hooks/useMotiWithStyleProps"; // Define the properties for the BasePressable component export type PressableProps = Omit & - Omit & + Omit< + MotiPressableProps, + | "unstable_pressDelay" + | "disabled" + | "animate" + | "from" + | "transition" + | "delay" + | "state" + | "stylePriority" + | "onDidAnimate" + | "exit" + | "exitTransition" + | "animateInitialState" + > & + Pick< + BoxProps, + | "animate" + | "from" + | "transition" + | "delay" + | "state" + | "stylePriority" + | "onDidAnimate" + | "exit" + | "exitTransition" + | "animateInitialState" + > & StateProps<"_pressed" | "_disabled"> & { /** * Duration (in milliseconds) to wait after press down before calling onPressIn. @@ -32,94 +59,89 @@ export type PressableProps = Omit & }; /** - * CustomPressable is a functional component that returns a MotiPressable component with specific styles based on the props. - * It uses the usePressedState and useDisabledState hooks to update the component's styles based on its state. - * @param children The children to render inside the MotiPressable - * @param onPressInDelay Duration (in milliseconds) to wait after press down before calling onPressIn. - * @param isDisabled Whether the press behavior is disabled. - * @param accessibilityLabel A label for the pressable component that is used by screen readers. - * @param accessibilityState Additional accessibility state information for the pressable component. - * @param onPress A function to be called when the pressable component is pressed. - * @param onPressIn A function to be called when the pressable component is pressed and held down. - * @param onPressOut A function to be called when the pressable component is released. - * @param onLongPress A function to be called when the pressable component is pressed and held down for a long time. + * Pressable is a functional component use to make interactive elements such as buttons, links, and more. * @returns A MotiPressable component with updated props and styles. */ -const Pressable = React.forwardRef( - ( - { - children, - onPressInDelay = 100, - isDisabled = false, - accessibilityLabel = "Press me!", - accessibilityState = undefined, - onPress = undefined, - onPressIn = undefined, - onPressOut = undefined, - onLongPress = undefined, - ...props - }: BasicComponentProps, - ref: any - ) => { - props = useStyleProps(props, boxStyleFunctions); - props = useMotiWithStyleProps(props, boxStyleFunctions); +const Pressable = React.memo( + React.forwardRef( + ( + { + children, + onPressInDelay = 100, + isDisabled = false, + accessibilityLabel = "Press me!", + accessibilityState = undefined, + onPress = undefined, + onPressIn = undefined, + onPressOut = undefined, + onLongPress = undefined, + ...props + }: BasicComponentProps, + ref: any + ) => { + props = useStyleProps(props, boxStyleFunctions); + props = useMotiWithStyleProps(props, boxStyleFunctions); - // Use State for dynamic styles - const { setPressed, propsWithPressedStyles } = usePressedState( - props, - boxStyleFunctions - ); - // Update props with pressed styles - props = propsWithPressedStyles; + // Use State for dynamic styles + const { setPressed, propsWithPressedStyles } = usePressedState( + props, + boxStyleFunctions + ); + // Update props with pressed styles + props = propsWithPressedStyles; - // Use State for disabled styles - const { propsWithDisabledStyles } = useDisabledState( - props, - boxStyleFunctions, - "basic", - true, - isDisabled - ); - // Update props with disabled styles - props = propsWithDisabledStyles; + // Use State for disabled styles + const { propsWithDisabledStyles } = useDisabledState( + props, + boxStyleFunctions, + "basic", + true, + isDisabled + ); + // Update props with disabled styles + props = propsWithDisabledStyles; - // Methods to handle local pressable state - const onPressInHandler = () => { - setPressed(true); - if (onPressIn) onPressIn(); - }; + // Methods to handle local pressable state + const onPressInHandler = () => { + setPressed(true); + if (onPressIn) onPressIn(); + }; - const onPressOutHandler = () => { - setPressed(false); - if (onPressOut) onPressOut(); - }; + const onPressOutHandler = () => { + setPressed(false); + if (onPressOut) onPressOut(); + }; - return ( - { - "worklet"; + return ( + { + "worklet"; - // Merge the interaction and animate props - return mergeAnimateProp(interaction, props.animate); - }} - containerStyle={{ alignSelf: "flex-start" }} - > - {children} - - ); - } + // Merge the interaction and animate props + return mergeAnimateProp(interaction, props.animate as any); + }} + containerStyle={{ + alignSelf: "flex-start", + ..._.pick(props.style, ["flex", "alignSelf"]), + }} + > + {children} + + ); + } + ) ); Pressable.displayName = "Pressable"; diff --git a/src/components/Atoms/Screen/Screen.config.ts b/src/components/Atoms/Screen/Screen.config.ts index 1a7bfbfd..28875ba9 100644 --- a/src/components/Atoms/Screen/Screen.config.ts +++ b/src/components/Atoms/Screen/Screen.config.ts @@ -1,4 +1,7 @@ -export default { +import { AtomicComponentConfig } from "../../../theme/src/types"; +import { ScreenProps } from "./screen"; + +const ScreenConfig: AtomicComponentConfig = { baseStyle: { scrollable: true, showScrollBar: false, @@ -6,6 +9,9 @@ export default { light: "neutral.50", dark: "neutral.800", }, + transition: { duration: 100 }, padding: "4", }, }; + +export default ScreenConfig; diff --git a/src/components/Atoms/Screen/Screen.tsx b/src/components/Atoms/Screen/Screen.tsx index d9f9d39f..f536b640 100644 --- a/src/components/Atoms/Screen/Screen.tsx +++ b/src/components/Atoms/Screen/Screen.tsx @@ -5,12 +5,12 @@ import { KeyboardAwareScrollView, KeyboardAwareScrollViewProps, } from "react-native-keyboard-aware-scroll-view"; -import { useTheme } from "../../../hooks/useTheme"; import { pearl } from "../../../pearl"; import { AtomComponentProps } from "../../../theme/src/types"; import { SafeAreaView, View as MotiView } from "moti"; import { MOTI_PROPS } from "../../../hooks/utils/utils"; import _ from "lodash"; +import { useTheme } from "../../../hooks/useTheme"; export type BaseScreenProps = Omit< BoxProps, @@ -35,7 +35,7 @@ export type BaseScreenProps = Omit< * @default false */ showScrollBar?: boolean; - /** Method to execute when a pull-to-refresh action is performed */ + /** Method to execute when a pull-to-refresh action is performed. Note: `scrollable` should be set as `true` to support pull-to-refresh. */ onPullToRefresh?: Function; /** The colors (at least one) that will be used to draw the refresh indicator (Android only) */ refreshIndicatorColors?: string[]; @@ -72,100 +72,106 @@ export type BaseScreenProps = Omit< * @param refreshTitleColor The color of the refresh indicator title (iOS only) * @returns A SafeAreaView component with a KeyboardAwareScrollView component inside */ -const CustomScreen = React.forwardRef( - ( - { - children, - size, - variant, - scrollable, - showScrollBar, - onPullToRefresh, - refreshIndicatorColors, - refreshProgressBackgroundColor, - refreshProgressViewOffset, - refreshIndicatorSize, - refreshTintColor, - refreshTitle, - refreshTitleColor, - ...props - }: AtomComponentProps<"Screen", BaseScreenProps>, - ref: any - ) => { - const { colorMode } = useTheme(); - const [refreshing, setRefreshing] = useState(false); - const animationProps = _.pick(props, MOTI_PROPS); - const nativeProps = _.omit(props, [...MOTI_PROPS, "style"]); +const CustomScreen = React.memo( + React.forwardRef( + ( + { + children, + size, + variant, + scrollable, + showScrollBar, + onPullToRefresh, + refreshIndicatorColors, + refreshProgressBackgroundColor, + refreshProgressViewOffset, + refreshIndicatorSize, + refreshTintColor, + refreshTitle, + refreshTitleColor, + ...props + }: AtomComponentProps<"Screen", BaseScreenProps>, + ref: any + ) => { + const { colorMode } = useTheme(); + const [refreshing, setRefreshing] = useState(false); + const animationProps = _.pick(props, MOTI_PROPS); + const nativeProps = _.omit(props, [...MOTI_PROPS, "style"]); - /** - * Function to execute when a pull-to-refresh action is performed - */ - const onRefresh = React.useCallback(async () => { - if (onPullToRefresh) { - setRefreshing(true); - const functionValue = await onPullToRefresh(); - let isPromise = functionValue instanceof Promise; - if (isPromise) - Promise.resolve(functionValue) - .then(() => { - setRefreshing(false); - }) - .catch((error) => { - console.error(error); - setRefreshing(false); - }); - else { - setRefreshing(false); + /** + * Function to execute when a pull-to-refresh action is performed + */ + const onRefresh = React.useCallback(async () => { + if (onPullToRefresh) { + setRefreshing(true); + try { + await onPullToRefresh(); + } catch (error) { + console.error(error); + } finally { + setRefreshing(false); + } } - } - }, [onPullToRefresh]); + }, [onPullToRefresh]); - return ( - <> - - + ) : undefined + } + > + + {children} + + + ) : ( + - - ) : undefined - } + {children} + + ); + + return ( + <> + + - - {children} - - - - - ); - } + {mainView} + + + ); + } + ) ); /** A layout component that you can use to wrap all the views in your app. */ diff --git a/src/components/Atoms/Spacer/Spacer.tsx b/src/components/Atoms/Spacer/Spacer.tsx index 39e29929..18f33804 100644 --- a/src/components/Atoms/Spacer/Spacer.tsx +++ b/src/components/Atoms/Spacer/Spacer.tsx @@ -4,13 +4,16 @@ import Box, { BoxProps } from "../box/box"; /** * A layout component that creates an adjustable, empty space that can be used to tune the spacing between sibling elements */ -const Spacer: React.FC = ({ children, ...rest }) => { - return ( - - {children} - - ); -}; +const Spacer = React.memo( + React.forwardRef((props, ref) => { + const { children, ...rest } = props; + return ( + + {children} + + ); + }) +); Spacer.displayName = "Spacer"; diff --git a/src/components/Atoms/Spinner/Spinner.config.ts b/src/components/Atoms/Spinner/Spinner.config.ts index c3b8d7fb..9a4d6658 100644 --- a/src/components/Atoms/Spinner/Spinner.config.ts +++ b/src/components/Atoms/Spinner/Spinner.config.ts @@ -1,51 +1,74 @@ -export default { +import { AtomicComponentConfig } from "../../../theme/src/types"; +import { SpinnerProps } from "./spinner"; +import { BallIndicatorProps } from "./indicators/ball"; +import { BarIndicatorProps } from "./indicators/bar"; +import { DotIndicatorProps } from "./indicators/dot"; +import { PacmanIndicatorProps } from "./indicators/pacman"; +import { PulseIndicatorProps } from "./indicators/pulse"; +import { MaterialIndicatorProps } from "./indicators/material"; +import { SkypeIndicatorProps } from "./indicators/skype"; +import { ActivityIndicatorProps } from "./indicators/activity"; +import { WaveIndicatorProps } from "./indicators/wave"; + +const BallConfig: SpinnerProps & BallIndicatorProps = { count: 8 }; +const BarConfig: SpinnerProps & BarIndicatorProps = { count: 3 }; +const DotConfig: SpinnerProps & DotIndicatorProps = { + sizeMultiplier: 0.2, + count: 4, +}; +const MaterialConfig: SpinnerProps & MaterialIndicatorProps = { + animationDuration: 3600, +}; +const PacmanConfig: SpinnerProps & PacmanIndicatorProps = {}; +const PulseConfig: SpinnerProps & PulseIndicatorProps = {}; +const SkypeConfig: SpinnerProps & SkypeIndicatorProps = { + animationDuration: 1600, + count: 5, + minScale: 0.2, + maxScale: 1.0, +}; +const ActivityConfig: SpinnerProps & ActivityIndicatorProps = { count: 12 }; +const WaveConfig: SpinnerProps & WaveIndicatorProps = { + animationDuration: 1600, + count: 4, + waveFactor: 0.54, + waveMode: "fill", +}; + +const SpinnerConfig: AtomicComponentConfig = { baseStyle: { color: "primary.500", animationDuration: 1200, - animating: true, }, sizes: { xs: { - spinnerSize: 10, + rawSize: 10, }, s: { - spinnerSize: 15, + rawSize: 15, }, m: { - spinnerSize: 20, + rawSize: 20, }, l: { - spinnerSize: 30, + rawSize: 30, }, }, variants: { - ball: { count: 8 }, - bar: { count: 3 }, - dot: { - sizeMultiplier: 0.2, - count: 4, - }, - spinner: { - animationDuration: 3600, - }, - pacman: {}, - pulse: {}, - skype: { - animationDuration: 1600, - count: 5, - minScale: 0.2, - maxScale: 1.0, - }, - activity: { count: 12 }, - wave: { - animationDuration: 1600, - count: 4, - waveFactor: 0.54, - waveMode: "fill", - }, + ball: BallConfig, + bar: BarConfig, + dot: DotConfig, + spinner: MaterialConfig, + pacman: PacmanConfig, + pulse: PulseConfig, + skype: SkypeConfig, + activity: ActivityConfig, + wave: WaveConfig, }, defaults: { size: "m", variant: "spinner", }, }; + +export default SpinnerConfig; diff --git a/src/components/Atoms/Spinner/Spinner.tsx b/src/components/Atoms/Spinner/Spinner.tsx index fe7423b2..7cfffaca 100644 --- a/src/components/Atoms/Spinner/Spinner.tsx +++ b/src/components/Atoms/Spinner/Spinner.tsx @@ -54,6 +54,18 @@ type BaseSpinnerProps = React.ComponentProps & { * @default 1200 */ animationDuration?: number; + /** + * The raw size of the spinner. + * + * @default 20 + */ + rawSize?: number; + /** + * The size multiplier of the spinner. + * + * @default 1 + */ + sizeMultiplier?: number; }; type SpinnerStyleProps = ColorProps & @@ -76,65 +88,74 @@ const IndicatorTypeToComponentMap = { /** * A component used to provide a visual cue that an action is either processing, awaiting a course of change or a result. */ -const Spinner: React.FC< - AtomComponentProps<"Spinner", BaseSpinnerProps, SpinnerStyleProps> -> = ({ - isLoading = true, - isExpanded = false, - animationDuration = 1200, - colorScheme = "primary", - ...rest -}) => { - // If isLoading is false, return null - if (!isLoading) return null; +const Spinner = React.memo( + React.forwardRef< + AtomComponentProps<"Spinner", BaseSpinnerProps, SpinnerStyleProps>, + any + >( + ( + { + isLoading = true, + isExpanded = false, + animationDuration = 1200, + colorScheme = "primary", + ...rest + }, + ref + ) => { + // Set default variant to "spinner" + rest.variant = rest.variant ?? "spinner"; - // Set default variant to "spinner" - rest.variant = rest.variant ?? "spinner"; + // Get props for the Spinner + let props = useAtomicComponentConfig( + "Spinner", + rest, + { + size: rest.size, + variant: rest.variant, + }, + colorScheme, + indicatorStyleFunctions + ); - // Get props for the Spinner - let props = useAtomicComponentConfig( - "Spinner", - rest, - { - size: rest.size, - variant: rest.variant, - }, - colorScheme, - indicatorStyleFunctions - ); + // Add style props to props using useMotiWithStyleProps + props = useMotiWithStyleProps(props, indicatorStyleFunctions); - // Add style props to props using useMotiWithStyleProps - props = useMotiWithStyleProps(props, indicatorStyleFunctions); + // Get variant for current screen size + const variantForCurrentScreenSize = useResponsiveProp(rest.variant); - // Get variant for current screen size - const variantForCurrentScreenSize = useResponsiveProp(rest.variant); + // If isLoading is false, return null + if (!isLoading) return null; - // Create and return the Spinner component - return React.createElement( - IndicatorTypeToComponentMap[ - variantForCurrentScreenSize as keyof typeof IndicatorTypeToComponentMap - ], - { - ...props, - color: props.style.color ? props.style.color : props.style.color, - size: props.sizeMultiplier - ? props.sizeMultiplier * props.spinnerSize - : props.spinnerSize, - accessible: true, - accessibilityLabel: rest.accessibilityLabel - ? rest.accessibilityLabel - : "Loading indicator", - accessibilityRole: "progressbar" as AccessibilityRoles, - style: [ - isExpanded ? StyleSheet.absoluteFill : { alignSelf: "flex-start" }, + // Create and return the Spinner component + return React.createElement( + IndicatorTypeToComponentMap[ + variantForCurrentScreenSize as keyof typeof IndicatorTypeToComponentMap + ], { - flex: 0, - ...props.style, - }, - ], + ...props, + color: props.style.color, + size: props.sizeMultiplier + ? props.sizeMultiplier * props.rawSize + : props.rawSize, + accessible: true, + accessibilityLabel: rest.accessibilityLabel + ? rest.accessibilityLabel + : "Loading indicator", + accessibilityRole: "progressbar" as AccessibilityRoles, + style: [ + isExpanded ? StyleSheet.absoluteFill : { alignSelf: "flex-start" }, + { + flex: 0, + ...props.style, + }, + ], + ref: ref, + } + ); } - ); -}; + ) +); export type SpinnerProps = React.ComponentProps; diff --git a/src/components/Atoms/Spinner/indicators/activity.tsx b/src/components/Atoms/Spinner/indicators/activity.tsx index 6c2a6118..23a43a68 100644 --- a/src/components/Atoms/Spinner/indicators/activity.tsx +++ b/src/components/Atoms/Spinner/indicators/activity.tsx @@ -8,88 +8,94 @@ export type ActivityIndicatorProps = IndicatorProps & { count?: number; }; -const ActivityIndicator: React.FC = ({ - size = 30, - count = 12, - color = "rgb(0, 0, 0)", - ...rest -}): JSX.Element => { - const _renderComponent = ({ - index, - count, - progress, - }: { - index: number; - count: number; - progress: Animated.Value; - }) => { - let angle = (index * 360) / count; +const ActivityIndicator = React.memo( + React.forwardRef( + ( + { size = 30, count = 12, color = "rgb(0, 0, 0)", ...rest }, + ref: any + ): JSX.Element => { + const _renderComponent = React.useCallback( + ({ + index, + count, + progress, + }: { + index: number; + count: number; + progress: Animated.Value; + }) => { + let angle = (index * 360) / count; - let layerStyle = { - transform: [ - { - rotate: angle + "deg", - }, - ], - }; + let layerStyle = { + transform: [ + { + rotate: angle + "deg", + }, + ], + }; - let inputRange = Array.from( - new Array(count + 1), - (item, index) => index / count - ); + let inputRange = Array.from( + new Array(count + 1), + (item, index) => index / count + ); - let outputRange = Array.from(new Array(count), (item, index) => - Math.max(1.0 - index * (1 / (count - 1)), 0) - ); + let outputRange = Array.from(new Array(count), (item, index) => + Math.max(1.0 - index * (1 / (count - 1)), 0) + ); - for (let j = 0; j < index; j++) { - outputRange.unshift(outputRange.pop() as number); - } + for (let j = 0; j < index; j++) { + outputRange.unshift(outputRange.pop() as number); + } - outputRange.unshift(...outputRange.slice(-1)); + outputRange.unshift(...outputRange.slice(-1)); - let barStyle = { - width: size / 10, - height: size / 4, - borderRadius: size / 20, - backgroundColor: color, - opacity: progress.interpolate({ inputRange, outputRange }), - }; + let barStyle = { + width: size / 10, + height: size / 4, + borderRadius: size / 20, + backgroundColor: color, + opacity: progress.interpolate({ inputRange, outputRange }), + }; - return ( - - - - ); - }; + return ( + + + + ); + }, + [size, color] + ); - const { style, ...props } = rest; + const { style, ...props } = rest; - return ( - - - - ); -}; + return ( + + + + ); + } + ) +); export default ActivityIndicator; diff --git a/src/components/Atoms/Spinner/indicators/ball.tsx b/src/components/Atoms/Spinner/indicators/ball.tsx index 3aa2df74..373fece9 100644 --- a/src/components/Atoms/Spinner/indicators/ball.tsx +++ b/src/components/Atoms/Spinner/indicators/ball.tsx @@ -9,99 +9,105 @@ export type BallIndicatorProps = IndicatorProps & { }; /** A component used to provide a visual cue that an action is either processing, awaiting a course of change or a result. */ -const BallIndicator: React.FC = ({ - count = 8, - size = 30, - color = "rgb(0, 0, 0)", - ...rest -}): JSX.Element => { - const _renderComponent = ({ - index, - count, - progress, - }: { - index: number; - count: number; - progress: Animated.Value; - }) => { - let angle = (index * 360) / count; +const BallIndicator = React.memo( + React.forwardRef( + ( + { count = 8, size = 30, color = "rgb(0, 0, 0)", ...rest }, + ref: any + ): JSX.Element => { + const _renderComponent = React.useCallback( + ({ + index, + count, + progress, + }: { + index: number; + count: number; + progress: Animated.Value; + }) => { + let angle = (index * 360) / count; - let layerStyle = { - transform: [ - { - rotate: angle + "deg", - }, - ], - }; + let layerStyle = { + transform: [ + { + rotate: angle + "deg", + }, + ], + }; - let inputRange = Array.from( - new Array(count + 1), - (item, index) => index / count - ); + let inputRange = Array.from( + new Array(count + 1), + (item, index) => index / count + ); - let outputRange = Array.from( - new Array(count), - (item, index) => 1.2 - (0.5 * index) / (count - 1) - ); + let outputRange = Array.from( + new Array(count), + (item, index) => 1.2 - (0.5 * index) / (count - 1) + ); - for (let j = 0; j < index; j++) { - outputRange.unshift(outputRange.pop() as number); - } + for (let j = 0; j < index; j++) { + outputRange.unshift(outputRange.pop() as number); + } - outputRange.unshift(...outputRange.slice(-1)); + outputRange.unshift(...outputRange.slice(-1)); - let ballStyle = { - margin: size / 20, - backgroundColor: color, - width: size / 5, - height: size / 5, - borderRadius: size / 10, - transform: [ - { - scale: progress.interpolate({ inputRange, outputRange }), - }, - ], - }; + let ballStyle = { + margin: size / 20, + backgroundColor: color, + width: size / 5, + height: size / 5, + borderRadius: size / 10, + transform: [ + { + scale: progress.interpolate({ inputRange, outputRange }), + }, + ], + }; - return ( - - - - ); - }; + return ( + + + + ); + }, + [count, size, color] + ); - const { style, ...props } = rest; + const { style, ...props } = rest; - return ( - - - - ); -}; + return ( + + + + ); + } + ) +); export default BallIndicator; diff --git a/src/components/Atoms/Spinner/indicators/bar.tsx b/src/components/Atoms/Spinner/indicators/bar.tsx index 6b4844ef..51500c87 100644 --- a/src/components/Atoms/Spinner/indicators/bar.tsx +++ b/src/components/Atoms/Spinner/indicators/bar.tsx @@ -10,128 +10,132 @@ export type BarIndicatorProps = IndicatorProps & { }; /** A component used to provide a visual cue that an action is either processing, awaiting a course of change or a result. */ -const BarIndicator: React.FC = ({ - count = 3, - size = 30, - color = "rgb(0, 0, 0)", - ...rest -}): JSX.Element => { - const outputRange = ( - base: number, - index: number, - count: number, - samples: number - ) => { - let range = Array.from( - new Array(samples), - (item, index) => - base * Math.abs(Math.cos((Math.PI * index) / (samples - 1))) - ); +const BarIndicator = React.memo( + React.forwardRef( + ( + { count = 3, size = 30, color = "rgb(0, 0, 0)", ...rest }, + ref: any + ): JSX.Element => { + const outputRange = React.useCallback( + (base: number, index: number, count: number, samples: number) => { + let range = Array.from( + new Array(samples), + (item, index) => + base * Math.abs(Math.cos((Math.PI * index) / (samples - 1))) + ); - for (let j = 0; j < index * (samples / count); j++) { - range.unshift(range.pop() as number); - } - - range.unshift(...range.slice(-1)); + for (let j = 0; j < index * (samples / count); j++) { + range.unshift(range.pop() as number); + } - return range; - }; + range.unshift(...range.slice(-1)); - const _renderComponent = ({ - index, - count, - progress, - }: { - index: number; - count: number; - progress: Animated.Value; - }) => { - let frames = (60 * (rest.animationDuration as number)) / 1000; - let samples = 0; + return range; + }, + [] + ); - do samples += count; - while (samples < frames); + const _renderComponent = React.useCallback( + ({ + index, + count, + progress, + }: { + index: number; + count: number; + progress: Animated.Value; + }) => { + let frames = (60 * (rest.animationDuration as number)) / 1000; + let samples = 0; - let inputRange = Array.from( - new Array(samples + 1), - (item, index) => index / samples - ); + do samples += count; + while (samples < frames); - let width = Math.floor(size / 5), - height = Math.floor(size / 2), - radius = Math.ceil(width / 2); + let inputRange = Array.from( + new Array(samples + 1), + (item, index) => index / samples + ); - let containerStyle = { - height: size, - width: width, - marginHorizontal: radius, - }; + let width = Math.floor(size / 5), + height = Math.floor(size / 2), + radius = Math.ceil(width / 2); - let topStyle = { - width, - height, - backgroundColor: color, - borderTopLeftRadius: radius, - borderTopRightRadius: radius, - transform: [ - { - translateY: progress.interpolate({ - inputRange, - outputRange: outputRange( - +(height - radius) / 2, - index, - count, - samples - ), - }), - }, - ], - }; + let containerStyle = { + height: size, + width: width, + marginHorizontal: radius, + }; - let bottomStyle = { - width, - height, - backgroundColor: color, - borderBottomLeftRadius: radius, - borderBottomRightRadius: radius, - transform: [ - { - translateY: progress.interpolate({ - inputRange, - outputRange: outputRange( - -(height - radius) / 2, - index, - count, - samples - ), - }), - }, - ], - }; + let topStyle = { + width, + height, + backgroundColor: color, + borderTopLeftRadius: radius, + borderTopRightRadius: radius, + transform: [ + { + translateY: progress.interpolate({ + inputRange, + outputRange: outputRange( + +(height - radius) / 2, + index, + count, + samples + ), + }), + }, + ], + }; - return ( - - - - - ); - }; + let bottomStyle = { + width, + height, + backgroundColor: color, + borderBottomLeftRadius: radius, + borderBottomRightRadius: radius, + transform: [ + { + translateY: progress.interpolate({ + inputRange, + outputRange: outputRange( + -(height - radius) / 2, + index, + count, + samples + ), + }), + }, + ], + }; - return ( - + + + + ); }, - rest.style, - ]} - renderComponent={_renderComponent} - count={count} - /> - ); -}; + [outputRange] + ); + + return ( + + ); + } + ) +); export default BarIndicator; diff --git a/src/components/Atoms/Spinner/indicators/dot.tsx b/src/components/Atoms/Spinner/indicators/dot.tsx index d83791aa..d5a2ef81 100644 --- a/src/components/Atoms/Spinner/indicators/dot.tsx +++ b/src/components/Atoms/Spinner/indicators/dot.tsx @@ -8,63 +8,74 @@ export type DotIndicatorProps = IndicatorProps & { color?: string; }; -const DotIndicator: React.FC = ({ - count = 4, - size = 16, - color = "rgb(0, 0, 0)", - animationEasing = Easing.inOut(Easing.ease), - ...rest -}): JSX.Element => { - const _renderComponent = ({ - index, - count, - progress, - }: { - index: number; - count: number; - progress: Animated.Value; - }) => { - let style = { - width: size, - height: size, - margin: size / 2, - borderRadius: size / 2, - backgroundColor: color, - transform: [ - { - scale: progress.interpolate({ - inputRange: [ - 0.0, - (index + 0.5) / (count + 1), - (index + 1.0) / (count + 1), - (index + 1.5) / (count + 1), - 1.0, +const DotIndicator = React.memo( + React.forwardRef( + ( + { + count = 4, + size = 16, + color = "rgb(0, 0, 0)", + animationEasing = Easing.inOut(Easing.ease), + ...rest + }, + ref: any + ) => { + const _renderComponent = React.useCallback( + ({ + index, + count, + progress, + }: { + index: number; + count: number; + progress: Animated.Value; + }) => { + let style = { + width: size, + height: size, + margin: size / 2, + borderRadius: size / 2, + backgroundColor: color, + transform: [ + { + scale: progress.interpolate({ + inputRange: [ + 0.0, + (index + 0.5) / (count + 1), + (index + 1.0) / (count + 1), + (index + 1.5) / (count + 1), + 1.0, + ], + outputRange: [1.0, 1.36, 1.56, 1.06, 1.0], + }), + }, ], - outputRange: [1.0, 1.36, 1.56, 1.06, 1.0], - }), - }, - ], - }; - - return ; - }; + }; - return ( - ; }, - rest.style, - ]} - animationEasing={animationEasing} - renderComponent={_renderComponent} - count={count} - /> - ); -}; + [size, color] + ); + + return ( + + ); + } + ) +); export default DotIndicator; diff --git a/src/components/Atoms/Spinner/indicators/indicator.tsx b/src/components/Atoms/Spinner/indicators/indicator.tsx index 87a7ea78..5f859be5 100644 --- a/src/components/Atoms/Spinner/indicators/indicator.tsx +++ b/src/components/Atoms/Spinner/indicators/indicator.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef } from "react"; +import React, { useCallback, useEffect, useRef } from "react"; import { Animated, Easing, Platform, View } from "react-native"; type ViewProps = React.ComponentProps; @@ -11,44 +11,54 @@ export type IndicatorProps = ViewProps & { }; /** A component used to provide a visual cue that an action is either processing, awaiting a course of change or a result. */ -const Indicator: React.FC = ({ - animationEasing = Easing.linear, - animationDuration = 1200, - renderComponent = (props: any) => {}, - count = 1, - ...rest -}) => { - const progress = useRef(new Animated.Value(0)).current; - - const startAnimation = () => { - let animation = Animated.timing(progress, { - duration: animationDuration, - easing: animationEasing, - useNativeDriver: Platform.OS !== "web", // Web doesn't support loops if using Native Driver, - isInteraction: true, - toValue: 1, - }); - - Animated.loop(animation).start(); - }; - - const _renderComponent = (item: number, index: number) => { - if ("function" === typeof renderComponent) { - return renderComponent({ index, count, progress }); - } +const Indicator = React.memo( + React.forwardRef( + ( + { + animationEasing = Easing.linear, + animationDuration = 1200, + renderComponent = () => {}, + count = 1, + ...rest + }, + ref + ) => { + const progress = useRef(new Animated.Value(0)).current; - return null; - }; + const startAnimation = useCallback(() => { + let animation = Animated.timing(progress, { + duration: animationDuration, + easing: animationEasing, + useNativeDriver: Platform.OS !== "web", // Web doesn't support loops if using Native Driver, + isInteraction: true, + toValue: 1, + }); - useEffect(() => { - startAnimation(); - }, []); + Animated.loop(animation).start(); + }, [animationDuration, animationEasing, progress]); - return ( - - <>{Array.from(new Array(count), _renderComponent, this)} - - ); -}; + const _renderComponent = useCallback( + (item: number, index: number) => { + if ("function" === typeof renderComponent) { + return renderComponent({ index, count, progress }); + } + + return null; + }, + [renderComponent, count, progress] + ); + + useEffect(() => { + startAnimation(); + }, []); + + return ( + + <>{Array.from(new Array(count), _renderComponent, this)} + + ); + } + ) +); export default Indicator; diff --git a/src/components/Atoms/Spinner/indicators/material.tsx b/src/components/Atoms/Spinner/indicators/material.tsx index 6810bfed..3fa442c4 100644 --- a/src/components/Atoms/Spinner/indicators/material.tsx +++ b/src/components/Atoms/Spinner/indicators/material.tsx @@ -9,145 +9,161 @@ export type MaterialIndicatorProps = IndicatorProps & { }; /** A component used to provide a visual cue that an action is either processing, awaiting a course of change or a result. */ -const MaterialIndicator: React.FC = ({ - count = 2, - size = 30, - color = "rgb(0, 0, 0)", - animationDuration = 3600, - ...rest -}): JSX.Element => { - const _renderComponent = ({ - index, - count, - progress, - }: { - index: number; - count: number; - progress: Animated.Value; - }) => { - const trackWidth = size / 10; - - let frames = (60 * animationDuration) / 1000; - let easing = Easing.bezier(0.4, 0.0, 0.7, 1.0); - - let sa = 7.5; - let ea = 30; - - let sequences = 3; - let rotations = 5; - - let inputRange = Array.from( - new Array(frames), - (item, frameIndex) => frameIndex / (frames - 1) - ); - - let outputRange = Array.from(new Array(frames), (item, frameIndex) => { - let progress = (2 * sequences * frameIndex) / (frames - 1); - let rotation = index ? +(360 - sa) : -(180 - sa); - - let sequence = Math.ceil(progress); - - if (sequence % 2) { - progress = progress - sequence + 1; - } else { - progress = sequence - progress; - } - - let direction = index ? -1 : +1; +const MaterialIndicator = React.memo( + React.forwardRef( + ( + { + count = 2, + size = 30, + color = "rgb(0, 0, 0)", + animationDuration = 3600, + ...rest + }, + ref: any + ): JSX.Element => { + const _renderComponent = React.useCallback( + ({ + index, + count, + progress, + }: { + index: number; + count: number; + progress: Animated.Value; + }) => { + const trackWidth = size / 10; + + let frames = (60 * animationDuration) / 1000; + let easing = Easing.bezier(0.4, 0.0, 0.7, 1.0); + + let sa = 7.5; + let ea = 30; + + let sequences = 3; + let rotations = 5; + + let inputRange = Array.from( + new Array(frames), + (item, frameIndex) => frameIndex / (frames - 1) + ); + + let outputRange = Array.from( + new Array(frames), + (item, frameIndex) => { + let progress = (2 * sequences * frameIndex) / (frames - 1); + let rotation = index ? +(360 - sa) : -(180 - sa); + + let sequence = Math.ceil(progress); + + if (sequence % 2) { + progress = progress - sequence + 1; + } else { + progress = sequence - progress; + } + + let direction = index ? -1 : +1; + + return ( + direction * (180 - (sa + ea)) * easing(progress) + + rotation + + "deg" + ); + } + ); + + let layerStyle = { + width: size, + height: size, + transform: [ + { + rotate: 90 - sa + "deg", + }, + { + rotate: progress.interpolate({ + inputRange: [0, 1], + outputRange: ["0deg", 360 * rotations + "deg"], + }), + }, + ], + }; + + let viewportStyle = { + width: size, + height: size, + transform: [ + { + translateY: index ? -size / 2 : 0, + }, + { + rotate: progress.interpolate({ inputRange, outputRange }), + }, + ], + }; + + let containerStyle = { + width: size, + height: size / 2, + overflow: "hidden" as "hidden", + }; + + let offsetStyle = index ? { top: size / 2 } : null; + + let lineStyle = { + width: size, + height: size, + borderColor: color, + borderRadius: size / 2, + borderWidth: trackWidth, + }; + + return ( + + + + + + + + + + + + ); + }, + [size, color, animationDuration] + ); + + const { style, ...props } = rest; return ( - direction * (180 - (sa + ea)) * easing(progress) + rotation + "deg" + + + ); - }); - - let layerStyle = { - width: size, - height: size, - transform: [ - { - rotate: 90 - sa + "deg", - }, - { - rotate: progress.interpolate({ - inputRange: [0, 1], - outputRange: ["0deg", 360 * rotations + "deg"], - }), - }, - ], - }; - - let viewportStyle = { - width: size, - height: size, - transform: [ - { - translateY: index ? -size / 2 : 0, - }, - { - rotate: progress.interpolate({ inputRange, outputRange }), - }, - ], - }; - - let containerStyle = { - width: size, - height: size / 2, - overflow: "hidden" as "hidden", - }; - - let offsetStyle = index ? { top: size / 2 } : null; - - let lineStyle = { - width: size, - height: size, - borderColor: color, - borderRadius: size / 2, - borderWidth: trackWidth, - }; - - return ( - - - - - - - - - - - - ); - }; - - const { style, ...props } = rest; - - return ( - - - - ); -}; + } + ) +); export default MaterialIndicator; diff --git a/src/components/Atoms/Spinner/indicators/pacman.tsx b/src/components/Atoms/Spinner/indicators/pacman.tsx index e395c51c..fa063746 100644 --- a/src/components/Atoms/Spinner/indicators/pacman.tsx +++ b/src/components/Atoms/Spinner/indicators/pacman.tsx @@ -8,139 +8,146 @@ export type PacmanIndicatorProps = IndicatorProps & { }; /** A component used to provide a visual cue that an action is either processing, awaiting a course of change or a result. */ -const PacmanIndicator: React.FC = ({ - size = 30, - color = "rgb(0, 0, 0)", - ...rest -}): JSX.Element => { - const _renderBlock = ({ - index, - count, - progress, - }: { - index: number; - count: number; - progress: Animated.Value; - }) => { - let transform: object[] = [ - { - translateX: progress.interpolate({ - inputRange: [0.5, 1], - outputRange: [0, size / (I18nManager.isRTL ? 4 : -4)], - extrapolate: "clamp", - }), - }, - ]; - - let style: any = { - position: "absolute", - top: size / 2 - size / 16, - left: size / 2 + size / 16 + ((index - 2) * size) / 4, - width: size / 8, - height: size / 8, - borderRadius: size / 16, - backgroundColor: color, - transform, - }; - - if (index === count - 1) { - transform.push({ - scale: progress.interpolate({ - inputRange: [0, 0.5], - outputRange: [0, 1], - extrapolate: "clamp", - }), - }); - - style.opacity = progress.interpolate({ - inputRange: [0, 0.25], - outputRange: [0, 1], - extrapolate: "clamp", - }); - } - - return ; - }; - - const _renderComponent = ({ - index, - count, - progress, - }: { - index: number; - count: number; - progress: Animated.Value; - }) => { - if (index > 1) { - return _renderBlock({ index, count, progress }); - } +const PacmanIndicator = React.memo( + React.forwardRef( + ({ size = 30, color = "rgb(0, 0, 0)", ...rest }, ref: any): JSX.Element => { + const _renderBlock = React.useCallback( + ({ + index, + count, + progress, + }: { + index: number; + count: number; + progress: Animated.Value; + }) => { + let transform: object[] = [ + { + translateX: progress.interpolate({ + inputRange: [0.5, 1], + outputRange: [0, size / (I18nManager.isRTL ? 4 : -4)], + extrapolate: "clamp", + }), + }, + ]; + + let style: any = { + position: "absolute", + top: size / 2 - size / 16, + left: size / 2 + size / 16 + ((index - 2) * size) / 4, + width: size / 8, + height: size / 8, + borderRadius: size / 16, + backgroundColor: color, + transform, + }; + + if (index === count - 1) { + transform.push({ + scale: progress.interpolate({ + inputRange: [0, 0.5], + outputRange: [0, 1], + extrapolate: "clamp", + }), + }); + + style.opacity = progress.interpolate({ + inputRange: [0, 0.25], + outputRange: [0, 1], + extrapolate: "clamp", + }); + } - let hf = size / 2; - let qr = size / 4; - - let pacmanStyle = { - position: "absolute" as "absolute", - top: qr, - left: 0, - - width: hf, - height: hf, - - transform: [ - { - rotate: progress.interpolate({ - inputRange: [0, 0.67, 1], - outputRange: - index ^ (I18nManager.isRTL as any) - ? ["0deg", "45deg", "0deg"] - : ["0deg", "-45deg", "0deg"], - extrapolate: "clamp", - }), + return ; }, - ], - }; - - let containerStyle = { - overflow: "hidden" as "hidden", - width: hf, - height: qr, - ...(index - ? { - top: qr, - borderBottomLeftRadius: qr, - borderBottomRightRadius: qr, + [size, color] + ); + + const _renderComponent = React.useCallback( + ({ + index, + count, + progress, + }: { + index: number; + count: number; + progress: Animated.Value; + }) => { + if (index > 1) { + return _renderBlock({ index, count, progress }); } - : { - borderTopLeftRadius: qr, - borderTopRightRadius: qr, - }), - backgroundColor: color, - }; - - return ( - - - - ); - }; - - const { style, ...props } = rest; - - return ( - - - - ); -}; + + let hf = size / 2; + let qr = size / 4; + + let pacmanStyle = { + position: "absolute" as "absolute", + top: qr, + left: 0, + + width: hf, + height: hf, + + transform: [ + { + rotate: progress.interpolate({ + inputRange: [0, 0.67, 1], + outputRange: + index ^ (I18nManager.isRTL as any) + ? ["0deg", "45deg", "0deg"] + : ["0deg", "-45deg", "0deg"], + extrapolate: "clamp", + }), + }, + ], + }; + + let containerStyle = { + overflow: "hidden" as "hidden", + width: hf, + height: qr, + ...(index + ? { + top: qr, + borderBottomLeftRadius: qr, + borderBottomRightRadius: qr, + } + : { + borderTopLeftRadius: qr, + borderTopRightRadius: qr, + }), + backgroundColor: color, + }; + + return ( + + + + ); + }, + [_renderBlock, size, color] + ); + + const { style, ...props } = rest; + + return ( + + + + ); + } + ) +); export default PacmanIndicator; diff --git a/src/components/Atoms/Spinner/indicators/pulse.tsx b/src/components/Atoms/Spinner/indicators/pulse.tsx index 94e2db6d..152adfc1 100644 --- a/src/components/Atoms/Spinner/indicators/pulse.tsx +++ b/src/components/Atoms/Spinner/indicators/pulse.tsx @@ -8,70 +8,74 @@ export type PulseIndicatorProps = IndicatorProps & { }; /** A component used to provide a visual cue that an action is either processing, awaiting a course of change or a result. */ -const PulseIndicator: React.FC = ({ - size = 30, - color = "rgb(0, 0, 0)", - ...rest -}): JSX.Element => { - const _renderComponent = ({ - index, - count, - progress, - }: { - index: number; - count: number; - progress: Animated.Value; - }) => { - let pulseStyle = { - height: size, - width: size, - borderRadius: size / 2, - backgroundColor: color, - transform: [ - { - scale: progress.interpolate({ - inputRange: [0, 0.67, 1], - outputRange: index ? [0.4, 0.6, 0.4] : [0.4, 0.6, 1.0], - }), - }, - ], - opacity: progress.interpolate({ - inputRange: [0, 0.67, 1], - outputRange: index ? [1.0, 1.0, 1.0] : [0.5, 0.5, 0.0], - }), - }; +const PulseIndicator = React.memo( + React.forwardRef( + ({ size = 30, color = "rgb(0, 0, 0)", ...rest }, ref: any): JSX.Element => { + const _renderComponent = React.useCallback( + ({ + index, + count, + progress, + }: { + index: number; + count: number; + progress: Animated.Value; + }) => { + let pulseStyle = { + height: size, + width: size, + borderRadius: size / 2, + backgroundColor: color, + transform: [ + { + scale: progress.interpolate({ + inputRange: [0, 0.67, 1], + outputRange: index ? [0.4, 0.6, 0.4] : [0.4, 0.6, 1.0], + }), + }, + ], + opacity: progress.interpolate({ + inputRange: [0, 0.67, 1], + outputRange: index ? [1.0, 1.0, 1.0] : [0.5, 0.5, 0.0], + }), + }; - return ( - - - - ); - }; + return ( + + + + ); + }, + [size, color] + ); - const { style, ...props } = rest; + const { style, ...props } = rest; - return ( - - - - ); -}; + return ( + + + + ); + } + ) +); export default PulseIndicator; diff --git a/src/components/Atoms/Spinner/indicators/skype.tsx b/src/components/Atoms/Spinner/indicators/skype.tsx index 4f8a6145..be613a84 100644 --- a/src/components/Atoms/Spinner/indicators/skype.tsx +++ b/src/components/Atoms/Spinner/indicators/skype.tsx @@ -10,99 +10,110 @@ export type SkypeIndicatorProps = IndicatorProps & { maxScale?: number; }; -const SkypeIndicator: React.FC = ({ - size = 30, - count = 5, - color = "rgb(0, 0, 0)", - animationDuration = 1600, - minScale = 0.2, - maxScale = 1.0, - ...rest -}): JSX.Element => { - const _renderComponent = ({ - index, - count, - progress, - }: { - index: number; - count: number; - progress: Animated.Value; - }) => { - let frames = (60 * animationDuration) / 1000; - let offset = index / (count - 1); - let easing = Easing.bezier(0.5, offset, 0.5, 1.0); +const SkypeIndicator = React.memo( + React.forwardRef( + ( + { + size = 30, + count = 5, + color = "rgb(0, 0, 0)", + animationDuration = 1600, + minScale = 0.2, + maxScale = 1.0, + ...rest + }, + ref: any + ): JSX.Element => { + const _renderComponent = React.useCallback( + ({ + index, + count, + progress, + }: { + index: number; + count: number; + progress: Animated.Value; + }) => { + let frames = (60 * animationDuration) / 1000; + let offset = index / (count - 1); + let easing = Easing.bezier(0.5, offset, 0.5, 1.0); - let inputRange = Array.from( - new Array(frames), - (item, index) => index / (frames - 1) - ); + let inputRange = Array.from( + new Array(frames), + (item, index) => index / (frames - 1) + ); - let outputRange = Array.from( - new Array(frames), - (item, index) => easing(index / (frames - 1)) * 360 + "deg" - ); + let outputRange = Array.from( + new Array(frames), + (item, index) => easing(index / (frames - 1)) * 360 + "deg" + ); - let layerStyle = { - transform: [ - { - rotate: progress.interpolate({ inputRange, outputRange }), - }, - ], - }; + let layerStyle = { + transform: [ + { + rotate: progress.interpolate({ inputRange, outputRange }), + }, + ], + }; - let ballStyle = { - width: size / 5, - height: size / 5, - borderRadius: size / 10, - backgroundColor: color, - transform: [ - { - scale: progress.interpolate({ - inputRange: [0, 1], - outputRange: [ - maxScale - (maxScale - minScale) * offset, - minScale + (maxScale - minScale) * offset, + let ballStyle = { + width: size / 5, + height: size / 5, + borderRadius: size / 10, + backgroundColor: color, + transform: [ + { + scale: progress.interpolate({ + inputRange: [0, 1], + outputRange: [ + maxScale - (maxScale - minScale) * offset, + minScale + (maxScale - minScale) * offset, + ], + }), + }, ], - }), - }, - ], - }; + }; - return ( - - - - ); - }; + return ( + + + + ); + }, + [animationDuration, color, maxScale, minScale, size] + ); - const { style, ...props } = rest; + const { style, ...props } = rest; - return ( - - - - ); -}; + return ( + + + + ); + } + ) +); export default SkypeIndicator; diff --git a/src/components/Atoms/Spinner/indicators/wave.tsx b/src/components/Atoms/Spinner/indicators/wave.tsx index fcfef978..aec0beba 100644 --- a/src/components/Atoms/Spinner/indicators/wave.tsx +++ b/src/components/Atoms/Spinner/indicators/wave.tsx @@ -12,85 +12,96 @@ export type WaveIndicatorProps = IndicatorProps & { const floatEpsilon = Math.pow(2, -23); -const WaveIndicator: React.FC = ({ - size = 30, - count = 4, - color = "rgb(0, 0, 0)", - animationDuration = 1600, - animationEasing = Easing.out(Easing.ease), - waveFactor = 0.54, - waveMode = "fill", - ...rest -}): JSX.Element => { - const _renderComponent = ({ - index, - count, - progress, - }: { - index: number; - count: number; - progress: Animated.Value; - }) => { - let fill = waveMode === "fill"; +const WaveIndicator = React.memo( + React.forwardRef( + ( + { + size = 30, + count = 4, + color = "rgb(0, 0, 0)", + animationDuration = 1600, + animationEasing = Easing.out(Easing.ease), + waveFactor = 0.54, + waveMode = "fill", + ...rest + }, + ref + ): JSX.Element => { + const _renderComponent = React.useCallback( + ({ + index, + count, + progress, + }: { + index: number; + count: number; + progress: Animated.Value; + }) => { + let fill = waveMode === "fill"; - let factor = Math.max(1 - Math.pow(waveFactor, index), floatEpsilon); + let factor = Math.max(1 - Math.pow(waveFactor, index), floatEpsilon); - let waveStyle = { - height: size, - width: size, - borderRadius: size / 2, - borderWidth: fill ? 0 : Math.floor(size / 20), - [fill ? "backgroundColor" : "borderColor"]: color, + let waveStyle = { + height: size, + width: size, + borderRadius: size / 2, + borderWidth: fill ? 0 : Math.floor(size / 20), + [fill ? "backgroundColor" : "borderColor"]: color, - transform: [ - { - scale: progress.interpolate({ - inputRange: [factor, 1], - outputRange: [0, 1], - extrapolate: "clamp", - }), - }, - ], + transform: [ + { + scale: progress.interpolate({ + inputRange: [factor, 1], + outputRange: [0, 1], + extrapolate: "clamp", + }), + }, + ], - opacity: progress.interpolate({ - inputRange: [0, factor, 1], - outputRange: [0, 1, 0], - }), - }; + opacity: progress.interpolate({ + inputRange: [0, factor, 1], + outputRange: [0, 1, 0], + }), + }; - return ( - - - - ); - }; + return ( + + + + ); + }, + [waveMode, waveFactor, size, color] + ); - const { style, ...props } = rest; + const { style, ...props } = rest; - return ( - - - - ); -}; + return ( + + + + ); + } + ) +); export default WaveIndicator; diff --git a/src/components/Atoms/Stack/Stack.tsx b/src/components/Atoms/Stack/Stack.tsx index 5fec1d84..0b1a2704 100644 --- a/src/components/Atoms/Stack/Stack.tsx +++ b/src/components/Atoms/Stack/Stack.tsx @@ -34,139 +34,145 @@ export type ZStackProps = BoxProps & { /** * Stack is a layout component that makes it easy to stack elements together and apply a space between them. */ -const Stack: React.FC = ({ - children, - direction = "vertical", - spacing = "2", - ...rest -}) => { - const arrayChildren = React.Children.toArray(children); +const Stack = React.memo( + React.forwardRef( + ( + { children, direction = "vertical", spacing = "2", ...rest }: StackProps, + ref: any + ) => { + const arrayChildren = React.Children.toArray(children); - /** - * Renders the children of the stack. - * - * @returns The rendered children. - */ - const renderChildren = () => { - return React.Children.map(arrayChildren, (child, index) => { - const isLast = index === arrayChildren.length - 1; + /** + * Renders the children of the stack. + * + * @returns The rendered children. + */ + const renderChildren = React.useMemo(() => { + return arrayChildren.map((child, index) => { + const isLast = index === arrayChildren.length - 1; + + return ( + + {React.cloneElement(child as ReactElement)} + {rest.divider && + !isLast && + React.cloneElement(rest.divider, { + orientation: + direction === "horizontal" ? "vertical" : "horizontal", + ml: direction === "horizontal" ? spacing : undefined, + mt: direction === "vertical" ? spacing : undefined, + })} + + ); + }); + }, [arrayChildren, direction, spacing, rest.divider]); return ( - {React.cloneElement(child as ReactElement)} - {rest.divider && - !isLast && - React.cloneElement(rest.divider, { - orientation: - direction === "horizontal" ? "vertical" : "horizontal", - ml: direction === "horizontal" ? spacing : undefined, - mt: direction === "vertical" ? spacing : undefined, - })} + {renderChildren} ); - }); - }; - - return ( - - {renderChildren()} - - ); -}; + } + ) +); /** * HStack is a layout component that stacks elements horizontally and apply a space between them. */ -export const HStack: React.FC> = ({ - children, - ...rest -}) => { - return ( - - {children} - - ); -}; +export const HStack = React.memo( + React.forwardRef, any>( + ({ children, ...rest }, ref) => { + return ( + + {children} + + ); + } + ) +); /** * VStack is a layout component that stacks elements vertically and apply a space between them. */ -export const VStack: React.FC> = ({ - children, - ...rest -}) => { - return ( - - {children} - - ); -}; +export const VStack = React.memo( + React.forwardRef, any>( + ({ children, ...rest }, ref) => { + return ( + + {children} + + ); + } + ) +); /** * ZStack is a layout component that stacks elements on top of each other. */ -export const ZStack: React.FC = ({ - children, - reversed = false, - ...rest -}) => { - const arrayChildren = React.Children.toArray(children); +export const ZStack = React.memo( + React.forwardRef, any>( + ({ children, reversed = false, ...rest }, ref) => { + const arrayChildren = React.Children.toArray(children); - /** - * Renders the children of the stack. - * - * @returns The rendered children. - */ - const renderChildren = () => { - return React.Children.map(arrayChildren, (child, index) => { - const isOverridenZIndexProvided = - (child as ReactElement).props && - getKeys((child as ReactElement).props).includes("zIndex"); - const computedZIndex = useStyleProps((child as ReactElement).props, [ - ...positionStyleFunction, - ]); + /** + * Renders the children of the stack. + * + * @returns The rendered children. + */ + const renderChildren = React.useMemo(() => { + return arrayChildren.map((child, index) => { + const isOverridenZIndexProvided = + (child as ReactElement).props && + getKeys((child as ReactElement).props).includes("zIndex"); + const computedZIndex = useStyleProps((child as ReactElement).props, [ + ...positionStyleFunction, + ]); - return React.cloneElement(child as ReactElement, { - ...(child as ReactElement).props, - style: { - position: index === 0 ? "relative" : "absolute", - zIndex: isOverridenZIndexProvided - ? computedZIndex.style.zIndex - : reversed - ? arrayChildren.length - index - : index, - ...(child as ReactElement).props.style, - }, - }); - }); - }; + return React.cloneElement(child as ReactElement, { + ...(child as ReactElement).props, + style: { + position: index === 0 ? "relative" : "absolute", + zIndex: isOverridenZIndexProvided + ? computedZIndex.style.zIndex + : reversed + ? arrayChildren.length - index + : index, + ...(child as ReactElement).props.style, + }, + }); + }); + }, [arrayChildren, reversed]); - return ( - - {renderChildren()} - - ); -}; + return ( + + {renderChildren} + + ); + } + ) +); Stack.displayName = "Stack"; HStack.displayName = "HStack"; diff --git a/src/components/Atoms/Text/Text.config.ts b/src/components/Atoms/Text/Text.config.ts index 07fb54c2..7443e342 100644 --- a/src/components/Atoms/Text/Text.config.ts +++ b/src/components/Atoms/Text/Text.config.ts @@ -1,4 +1,7 @@ -export default { +import { AtomicComponentConfig } from "../../../theme/src/types"; +import { TextProps } from "./text"; + +const TextConfig: AtomicComponentConfig = { baseStyle: { color: { light: "neutral.900", @@ -73,3 +76,5 @@ export default { variant: "p2", }, }; + +export default TextConfig; diff --git a/src/components/Atoms/Text/Text.tsx b/src/components/Atoms/Text/Text.tsx index 81a589d5..55db7c23 100644 --- a/src/components/Atoms/Text/Text.tsx +++ b/src/components/Atoms/Text/Text.tsx @@ -1,32 +1,15 @@ import React from "react"; -import { Text as RNText } from "react-native"; +import { Text as RNText, TextStyle } from "react-native"; import responsiveSize from "../../../utils/responsive-size"; import { textStyleFunctions, TextStyleProps, } from "../../../theme/src/style-functions"; -import { - AtomComponentProps, - ComponentSizes, - ComponentVariants, - ResponsiveValue, -} from "../../../theme/src/types"; +import { AtomComponentProps, FinalPearlTheme } from "../../../theme/src/types"; import { useTheme } from "../../../hooks/useTheme"; import { pearl } from "../../../pearl"; type BaseTextProps = React.ComponentProps & { - /** - * The size of the text - * - * @default undefined - */ - size?: ResponsiveValue>; - /** - * The variant of the text - * - * @default "p2" - */ - variant?: ResponsiveValue>; /** * Whether to slightly scale the font size based on the screen dimensions * @@ -43,10 +26,14 @@ type BaseTextProps = React.ComponentProps & { * @returns A font configuration object. */ export const buildFontConfig = ( - textStyle: any, - allowFontScaling: boolean + textStyle: TextStyle, + allowFontScaling: boolean, + theme: FinalPearlTheme ): object => { - const fontWeight = textStyle.fontWeight; + let fontWeight: string | number = textStyle.fontWeight ?? "400"; + if (!!theme.fontWeights[fontWeight]) + fontWeight = theme.fontWeights[fontWeight]; + const fontStyle = textStyle.fontStyle ?? "normal"; let fontSize = textStyle.fontSize; @@ -58,7 +45,12 @@ export const buildFontConfig = ( } const initialFontFamily = textStyle.fontFamily; - const { theme } = useTheme(); + + if (!initialFontFamily || !theme.fontConfig[initialFontFamily]) { + throw new Error( + `Font family "${initialFontFamily}" does not exist in the theme.fontConfig` + ); + } const finalFontFamily = theme.fontConfig[initialFontFamily][fontWeight][fontStyle]; @@ -87,27 +79,23 @@ const CustomText = React.forwardRef( }: AtomComponentProps<"Text", BaseTextProps, TextStyleProps>, ref: any ) => { - /** - * Memoized function that builds the font configuration object. - */ - const memoizedBuildFontConfig = React.useCallback( - () => buildFontConfig(props.style, scaleFontSize), - [props.style, scaleFontSize] - ); + const { theme } = useTheme(); // Apply the font configuration to the provided text style. - props.style = { - includeFontPadding: false, - ...(props.style as any), - ...memoizedBuildFontConfig(), - }; + props.style = React.useMemo(() => { + return { + includeFontPadding: false, + ...(props.style as any), + ...buildFontConfig(props.style as TextStyle, scaleFontSize, theme), + }; + }, [props.style, scaleFontSize, theme]); return ( {props.children} diff --git a/src/components/Molecules/Avatar/Avatar.config.ts b/src/components/Molecules/Avatar/Avatar.config.ts index 454ce5f7..5dfd973b 100644 --- a/src/components/Molecules/Avatar/Avatar.config.ts +++ b/src/components/Molecules/Avatar/Avatar.config.ts @@ -1,4 +1,13 @@ -export default { +import { MolecularComponentConfig } from "../../../theme"; +import { TextProps } from "../../atoms/text/text"; +import { AvatarProps } from "./avatar"; + +export type AvatarAtoms = { + box: AvatarProps; + text: TextProps; +}; + +const AvatarConfig: MolecularComponentConfig = { parts: ["box", "text"], baseStyle: { box: { @@ -58,3 +67,5 @@ export default { size: "m", }, }; + +export default AvatarConfig; diff --git a/src/components/Molecules/Avatar/Avatar.tsx b/src/components/Molecules/Avatar/Avatar.tsx index c5c92ecd..a5636956 100644 --- a/src/components/Molecules/Avatar/Avatar.tsx +++ b/src/components/Molecules/Avatar/Avatar.tsx @@ -1,15 +1,19 @@ import React, { useRef } from "react"; import { ImageProps } from "../image/image"; import Image from "../image/image"; -import { ImageSourcePropType } from "react-native"; +import { ImageSourcePropType, ViewStyle } from "react-native"; import Box from "../../atoms/box/box"; import Text from "../../atoms/text/text"; import namedColors from "../../../theme/utils/named-colors.json"; import { getKeys } from "../../../theme/utils/type-helpers"; import { useAccessibleColor } from "../../../hooks/useAccessibleColor"; -import { MoleculeComponentProps } from "../../../theme/src/types"; +import { + MoleculeComponentProps, + PaletteColors, +} from "../../../theme/src/types"; import { pearl } from "../../../pearl"; import { useAvatarGroup } from "./useAvatarGroup"; +import { AvatarAtoms } from "./avatar.config"; /** * A function that generates initials from a name @@ -43,78 +47,90 @@ export type BaseAvatarProps = Omit< * If neither image source nor name is provided, it will render the fallbackComponent. * If no fallbackComponent is provided, it will render nothing. */ -const CustomAvatar = React.forwardRef( - ({ atoms }: MoleculeComponentProps<"Avatar", BaseAvatarProps>, ref: any) => { - const { name, src, getInitials, fallbackComponent, ...otherBoxProps } = - atoms.box; - - // Function to pick a random color from the namedColors object - const pickRandomColor = () => { - const namedColorKeys = getKeys(namedColors).filter( - (color) => color !== "transparent" +const CustomAvatar = React.memo( + React.forwardRef( + ( + { atoms }: MoleculeComponentProps<"Avatar", BaseAvatarProps, AvatarAtoms>, + ref: any + ) => { + const { name, src, getInitials, fallbackComponent, ...otherBoxProps } = + atoms.box; + + // Function to pick a random color from the namedColors object + const pickRandomColor = () => { + const namedColorKeys = getKeys(namedColors).filter( + (color) => color !== "transparent" + ); + const randomColorKey = + namedColorKeys[Math.floor(Math.random() * namedColorKeys.length)]; + + return randomColorKey; + }; + + // Store the random color in a ref to prevent it from changing on re-renders + const randomColor = useRef(pickRandomColor()).current; + // Determine the text color based on the background color to ensure accessibility + const accessibleTextColor = useAccessibleColor( + typeof otherBoxProps.backgroundColor === "string" + ? (otherBoxProps.backgroundColor as PaletteColors) + : typeof otherBoxProps.bgColor === "string" + ? (otherBoxProps.bgColor as PaletteColors) + : typeof (otherBoxProps.style as ViewStyle)?.backgroundColor === + "string" + ? ((otherBoxProps.style as ViewStyle) + ?.backgroundColor as PaletteColors) + : randomColor, + { + light: "neutral.50", + dark: "neutral.900", + } ); - const randomColorKey = - namedColorKeys[Math.floor(Math.random() * namedColorKeys.length)]; - - return randomColorKey; - }; - - // Store the random color in a ref to prevent it from changing on re-renders - const randomColor = useRef(pickRandomColor()).current; - // Determine the text color based on the background color to ensure accessibility - const accessibleTextColor = useAccessibleColor( - otherBoxProps.backgroundColor ?? - otherBoxProps.bgColor ?? - otherBoxProps.style.backgroundColor ?? - randomColor, - { - light: "neutral.50", - dark: "neutral.900", - } - ); - // Function to render the fallback component (initials or fallbackComponent prop) - const renderFallBack = () => { - if (name) { - const initialComputeFunction = getInitials ?? defaultGetInitials; - const nameInitials = initialComputeFunction(name); - return ( - - {nameInitials} - - ); - } + // Function to render the fallback component (initials or fallbackComponent prop) + const renderFallBack = () => { + if (name) { + const initialComputeFunction = getInitials ?? defaultGetInitials; + const nameInitials = initialComputeFunction(name); + return ( + + {nameInitials} + + ); + } - if (fallbackComponent) return React.cloneElement(fallbackComponent); + if (fallbackComponent) return React.cloneElement(fallbackComponent); - return null; - }; + return null; + }; - // Determine the final source of the image - const finalSource = - src && typeof src === "string" - ? ({ uri: src } as ImageSourcePropType) - : (src as ImageSourcePropType); + // Determine the final source of the image + const finalSource = + src && typeof src === "string" + ? ({ uri: src } as ImageSourcePropType) + : (src as ImageSourcePropType); - // If a source is provided, render the image - if (finalSource) { - return ; - } + // If a source is provided, render the image + if (finalSource) { + return ; + } - // If no source is provided, render the fallback - return ( - - {renderFallBack()} - - ); - } + // If no source is provided, render the fallback + return ( + + {renderFallBack()} + + ); + } + ) ); const StyledAvatar = pearl( diff --git a/src/components/Molecules/Badge/Badge.config.ts b/src/components/Molecules/Badge/Badge.config.ts index 4a333326..d3014cba 100644 --- a/src/components/Molecules/Badge/Badge.config.ts +++ b/src/components/Molecules/Badge/Badge.config.ts @@ -1,4 +1,13 @@ -export default { +import { MolecularComponentConfig } from "../../../theme/src/types"; +import { TextProps } from "../../atoms/text/text"; +import { BadgeProps } from "./badge"; + +export type BadgeAtoms = { + box: BadgeProps; + text: TextProps; +}; + +const BadgeConfig: MolecularComponentConfig = { parts: ["box", "text"], baseStyle: { box: { @@ -70,3 +79,5 @@ export default { variant: "rounded", }, }; + +export default BadgeConfig; diff --git a/src/components/Molecules/Badge/Badge.tsx b/src/components/Molecules/Badge/Badge.tsx index 26b1e39d..010deed7 100644 --- a/src/components/Molecules/Badge/Badge.tsx +++ b/src/components/Molecules/Badge/Badge.tsx @@ -1,8 +1,9 @@ -import React from "react"; +import React, { useMemo } from "react"; import Text from "../../atoms/text/text"; import { MoleculeComponentProps } from "../../../theme/src/types"; import Pressable, { PressableProps } from "../../atoms/pressable/pressable"; import { pearl } from "../../../pearl"; +import { BadgeAtoms } from "./badge.config"; export type BaseBadgeProps = PressableProps; @@ -14,38 +15,43 @@ export type BaseBadgeProps = PressableProps; * If the value is an array, it will be joined into a string and rendered inside a Text component. * If the value is undefined, nothing will be rendered. */ -const CustomBadge = React.forwardRef( - ( - { children, atoms }: MoleculeComponentProps<"Badge", BaseBadgeProps>, - ref: any - ) => { - // Function to render the value of the badge - const renderValue = () => { - if (children === undefined) return null; +const CustomBadge = React.memo( + React.forwardRef( + ( + { + children, + atoms, + }: MoleculeComponentProps<"Badge", BaseBadgeProps, BadgeAtoms>, + ref: any + ) => { + // Function to render the value of the badge + const renderValue = useMemo(() => { + if (children === undefined) return null; - // If children is an array, join it into a string - if (Array.isArray(children)) children = children.join(""); + // If children is an array, join it into a string + if (Array.isArray(children)) children = children.join(""); - // If children is a number or a string, render it inside a Text component - if (typeof children === "number" || typeof children === "string") - return {children}; - // If children is a React element, clone it and render it as is - else return React.cloneElement(children as React.ReactElement); - }; + // If children is a number or a string, render it inside a Text component + if (typeof children === "number" || typeof children === "string") + return {children}; + // If children is a React element, clone it and render it as is + else return React.cloneElement(children as React.ReactElement); + }, [children, atoms]); - // Return a Pressable component with the value rendered inside it - return ( - - {renderValue()} - - ); - } + // Return a Pressable component with the value rendered inside it + return ( + + {renderValue} + + ); + } + ) ); /** A Badge is a small component typically used to communicate a numerical value or indicate the status of an item to the user. */ diff --git a/src/components/Molecules/Badge/withBadge.tsx b/src/components/Molecules/Badge/withBadge.tsx index ebae6671..c1281065 100644 --- a/src/components/Molecules/Badge/withBadge.tsx +++ b/src/components/Molecules/Badge/withBadge.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useCallback, useMemo, useState } from "react"; import { LayoutChangeEvent, Platform } from "react-native"; import Box from "../../atoms/box/box"; import Badge, { BadgeProps } from "./badge"; @@ -50,7 +50,7 @@ const withBadge = const [badgeWidth, setBadgeWidth] = useState(0); // Function to compute the position of the badge based on the placement option - const computePositionForBadge = () => { + const computePositionForBadge = useCallback(() => { const positionValue = -1 * offset; if (placement === "topLeft") @@ -64,7 +64,7 @@ const withBadge = // Return bottom right position by default return { bottom: positionValue, right: positionValue }; - }; + }, [placement, offset]); // Function to update the width of the badge when its layout changes const onBadgeLayoutChange = (event: LayoutChangeEvent) => { @@ -79,7 +79,7 @@ const withBadge = }; // Function to compute the position of the badge with margins for web platform - const computePositionWithWebMarginsForBadge = () => { + const computedPositionWithWebMarginsForBadge = useMemo(() => { const position = computePositionForBadge(); if (Platform.OS === "web") { @@ -95,7 +95,7 @@ const withBadge = } return position; - }; + }, [computePositionForBadge, baseComponentWidth, badgeWidth]); // Render the wrapped component with the badge return ( @@ -110,7 +110,7 @@ const withBadge = onLayout={onBadgeLayoutChange} position="absolute" zIndex="overlay" - style={computePositionWithWebMarginsForBadge()} + style={computedPositionWithWebMarginsForBadge} > {badgeValue} diff --git a/src/components/Molecules/Button/Button.config.ts b/src/components/Molecules/Button/Button.config.ts index 3a8d1247..5459fd13 100644 --- a/src/components/Molecules/Button/Button.config.ts +++ b/src/components/Molecules/Button/Button.config.ts @@ -1,7 +1,20 @@ -export default { - parts: ["box", "text", "spinner", "icon"], +import { MolecularComponentConfig } from "../../../theme"; +import { TextProps } from "../../atoms/text/text"; +import { SpinnerProps } from "../../atoms/spinner/spinner"; +import { IconProps } from "../../atoms/icon/icon"; +import { ButtonProps } from "./button"; + +export type ButtonAtoms = { + pressable: ButtonProps; + text: TextProps; + spinner: SpinnerProps; + icon: IconProps; +}; + +const ButtonConfig: MolecularComponentConfig = { + parts: ["pressable", "text", "spinner", "icon"], baseStyle: { - box: { + pressable: { my: "1", justifyContent: "center", alignItems: "center", @@ -16,12 +29,12 @@ export default { alignSelf: "center", }, text: { - fontWeight: 500, + fontWeight: "500", }, }, sizes: { xs: { - box: { + pressable: { py: "0.5", px: "2", borderRadius: "s", @@ -38,7 +51,7 @@ export default { }, }, s: { - box: { + pressable: { py: "1.5", px: "2.5", borderRadius: "m", @@ -54,7 +67,7 @@ export default { }, }, m: { - box: { + pressable: { py: "2.5", px: "3", borderRadius: "m", @@ -70,7 +83,7 @@ export default { }, }, l: { - box: { + pressable: { py: "3", px: "4", borderRadius: "m", @@ -88,10 +101,8 @@ export default { }, variants: { filled: { - box: { - animate: { - backgroundColor: "primary.500", - }, + pressable: { + backgroundColor: "primary.500", _pressed: { bgColor: "primary.400", }, @@ -105,15 +116,13 @@ export default { }, }, outline: { - box: { - animate: { - backgroundColor: { - light: "neutral.50", - dark: "neutral.800", - }, - borderWidth: 1, - borderColor: "primary.500", + pressable: { + backgroundColor: { + light: "neutral.50", + dark: "neutral.800", }, + borderWidth: 1, + borderColor: "primary.500", _pressed: { bgColor: "primary.50", }, @@ -127,12 +136,10 @@ export default { }, }, ghost: { - box: { - animate: { - backgroundColor: { - light: "neutral.50", - dark: "neutral.800", - }, + pressable: { + backgroundColor: { + light: "neutral.50", + dark: "neutral.800", }, _pressed: { bgColor: "primary.50", @@ -152,3 +159,5 @@ export default { variant: "filled", }, }; + +export default ButtonConfig; diff --git a/src/components/Molecules/Button/Button.tsx b/src/components/Molecules/Button/Button.tsx index 070ccab4..acfcf49a 100644 --- a/src/components/Molecules/Button/Button.tsx +++ b/src/components/Molecules/Button/Button.tsx @@ -7,6 +7,7 @@ import { MoleculeComponentProps } from "../../../theme/src/types"; import { useButtonGroup } from "./button-group"; import { useMolecularComponentConfig } from "../../../hooks/useMolecularComponentConfig"; import { boxStyleFunctions } from "../../../theme/src/style-functions"; +import { ButtonAtoms } from "./button.config"; export type ButtonProps = PressableProps & { /** @@ -37,133 +38,140 @@ export type ButtonProps = PressableProps & { }; /** Button is used to trigger an action or event, such as submitting a form, opening a dialog, canceling an action, or performing a delete operation */ -const Button = React.forwardRef( - ( - { - children, - loadingText = undefined, - spinnerPlacement = "start", - isLoading = false, - isFullWidth = false, - leftIcon = undefined, - rightIcon = undefined, - ...rest - }: Omit, "atoms"> & { - atoms?: Record; - }, - ref: any - ) => { - const { size, variant, isDisabled, colorScheme } = useButtonGroup(); - - // Overwrite props from checkbox group - rest.size = rest.size ?? size; - rest.variant = rest.variant ?? variant; - rest.isDisabled = rest.isDisabled ?? isDisabled; - rest.colorScheme = rest.colorScheme ?? colorScheme; - - const molecularProps = useMolecularComponentConfig( - "Button", - rest, +const Button = React.memo( + React.forwardRef( + ( { - size: rest.size, - variant: rest.variant, + children, + loadingText = undefined, + spinnerPlacement = "start", + isLoading = false, + isFullWidth = false, + leftIcon = undefined, + rightIcon = undefined, + ...rest + }: Omit< + MoleculeComponentProps<"Button", ButtonProps, ButtonAtoms>, + "atoms" + > & { + atoms?: ButtonAtoms; }, - rest.colorScheme, - boxStyleFunctions, - "box", - "box", - "box" - ); - const { atoms } = molecularProps; + ref: any + ) => { + const { size, variant, isDisabled, colorScheme } = useButtonGroup(); - // Determine if the button is disabled - const isButtonDisabled = rest.isDisabled ? true : isLoading; + // Overwrite props from checkbox group + rest.size = rest.size ?? size; + rest.variant = rest.variant ?? variant; + rest.isDisabled = rest.isDisabled ?? isDisabled; + rest.colorScheme = rest.colorScheme ?? colorScheme; - // Function to render the loading status of the button - const renderLoadingStatus = () => { - // If there is loading text, render the spinner and the loading text - if (loadingText) { - return ( - - {spinnerPlacement === "start" ? ( - <> - - - {loadingText} - - - ) : ( - <> - - {loadingText} - + const molecularProps = useMolecularComponentConfig( + "Button", + rest, + { + size: rest.size, + variant: rest.variant, + }, + rest.colorScheme, + boxStyleFunctions, + "pressable", + "pressable", + "pressable" + ); + const { atoms } = molecularProps; - - - )} - - ); - } else { - // If there is no loading text, render the spinner and the children with transparent color - return ( - <> - - - {children} - - - ); - } - }; + // Determine if the button is disabled + const isButtonDisabled = rest.isDisabled ? true : isLoading; - // Function to render the main content of the button - const renderMainContent = () => { - // If there are left or right icons, render them along with the children - if (leftIcon || rightIcon) { - return ( - - {leftIcon - ? React.cloneElement(leftIcon, { - ...atoms.icon, - marginRight: atoms.box.py ?? atoms.box.paddingVertical, - ...leftIcon.props, - }) - : null} - {children} - {rightIcon - ? React.cloneElement(rightIcon, { - ...atoms.icon, - marginLeft: atoms.box.py ?? atoms.box.paddingVertical, - ...rightIcon.props, - }) - : null} - - ); - } else { - // If there are no icons, render only the children - return {children}; - } - }; + // Function to render the loading status of the button + const renderLoadingStatus = () => { + // If there is loading text, render the spinner and the loading text + if (loadingText) { + return ( + + {spinnerPlacement === "start" ? ( + <> + + + {loadingText} + + + ) : ( + <> + + {loadingText} + - return ( - + + )} + + ); + } else { + // If there is no loading text, render the spinner and the children with transparent color + return ( + <> + + + {children} + + + ); } - accessibilityState={{ disabled: isButtonDisabled, busy: isLoading }} - > - {isLoading ? renderLoadingStatus() : renderMainContent()} - - ); - } + }; + + // Function to render the main content of the button + const renderMainContent = () => { + // If there are left or right icons, render them along with the children + if (leftIcon || rightIcon) { + return ( + + {leftIcon + ? React.cloneElement(leftIcon, { + ...atoms.icon, + marginRight: + atoms.pressable.py ?? atoms.pressable.paddingVertical, + ...leftIcon.props, + }) + : null} + {children} + {rightIcon + ? React.cloneElement(rightIcon, { + ...atoms.icon, + marginLeft: + atoms.pressable.py ?? atoms.pressable.paddingVertical, + ...rightIcon.props, + }) + : null} + + ); + } else { + // If there are no icons, render only the children + return {children}; + } + }; + + return ( + + {isLoading ? renderLoadingStatus() : renderMainContent()} + + ); + } + ) ); Button.displayName = "Button"; diff --git a/src/components/Molecules/CheckBox/CheckBox.config.ts b/src/components/Molecules/CheckBox/CheckBox.config.ts index c5788074..bc9f30cf 100644 --- a/src/components/Molecules/CheckBox/CheckBox.config.ts +++ b/src/components/Molecules/CheckBox/CheckBox.config.ts @@ -1,4 +1,19 @@ -export default { +import { MolecularComponentConfig } from "../../../theme/src/types"; +import { BoxProps } from "../../atoms/box/box"; +import { IconProps } from "../../atoms/icon/icon"; +import { PressableProps } from "../../atoms/pressable/pressable"; +import { StackProps } from "../../atoms/stack/stack"; +import { TextProps } from "../../atoms/text/text"; +import { CheckBoxProps } from "./checkbox"; + +export type CheckboxAtoms = { + container: PressableProps & StackProps; + box: Omit & CheckBoxProps; + text: TextProps; + icon: IconProps; +}; + +const CheckboxConfig: MolecularComponentConfig = { parts: ["container", "box", "icon", "text"], baseStyle: { container: { @@ -14,18 +29,19 @@ export default { borderWidth: 2, borderColor: "neutral.300", transition: { + type: "timing", duration: 50, }, - _invalid: { - borderColor: "danger.500", - }, - }, - icon: { checkedIconFamily: "Ionicons", checkedIconName: "checkmark-sharp", indeterminateIconFamily: "Ionicons", indeterminateIconName: "remove-outline", - color: "neutral.50", + _invalid: { + borderColor: "danger.500", + }, + }, + text: { + alignSelf: "center", }, }, sizes: { @@ -77,40 +93,48 @@ export default { variants: { filled: { box: { - animate: { - backgroundColor: { - light: "neutral.200", - dark: "neutral.900", - }, - borderColor: { - light: "neutral.300", - dark: "neutral.600", - }, + backgroundColor: { + light: "neutral.200", + dark: "neutral.900", + }, + borderColor: { + light: "neutral.300", + dark: "neutral.600", }, _checked: { bgColor: "primary.500", borderColor: "primary.500", }, }, + icon: { + color: { + light: "neutral.200", + dark: "neutral.900", + }, + }, }, outline: { box: { borderWidth: 2, - animate: { - backgroundColor: { - light: "neutral.50", - dark: "neutral.800", - }, - borderColor: { - light: "neutral.400", - dark: "neutral.500", - }, + backgroundColor: { + light: "neutral.50", + dark: "neutral.800", + }, + borderColor: { + light: "neutral.400", + dark: "neutral.500", }, _checked: { bgColor: "primary.500", borderColor: "primary.500", }, }, + icon: { + color: { + light: "neutral.50", + dark: "neutral.800", + }, + }, }, }, defaults: { @@ -118,3 +142,5 @@ export default { variant: "filled", }, }; + +export default CheckboxConfig; diff --git a/src/components/Molecules/CheckBox/CheckBox.tsx b/src/components/Molecules/CheckBox/CheckBox.tsx index f9404921..84339251 100644 --- a/src/components/Molecules/CheckBox/CheckBox.tsx +++ b/src/components/Molecules/CheckBox/CheckBox.tsx @@ -3,13 +3,10 @@ import Text from "../../atoms/text/text"; import { FinalPearlTheme, ResponsiveValue, - ColorScheme, - ComponentSizes, - ComponentVariants, MoleculeComponentProps, StateProps, } from "../../../theme/src/types"; -import Icon from "../../atoms/icon/icon"; +import Icon, { IconProps } from "../../atoms/icon/icon"; import Pressable, { PressableProps } from "../../atoms/pressable/pressable"; import { useMolecularComponentConfig } from "../../../hooks/useMolecularComponentConfig"; import Stack from "../../atoms/stack/stack"; @@ -19,22 +16,11 @@ import { useInvalidState } from "../../../hooks/state/useInvalidState"; import { useCheckedState } from "../../../hooks/state/useCheckedState"; import { useDisabledState } from "../../../hooks"; import Center from "../../atoms/center/center"; +import { CheckboxAtoms } from "./checkbox.config"; import _ from "lodash"; export type CheckBoxProps = PressableProps & StateProps<"_checked" | "_invalid" | "_disabled"> & { - /** - * Size of the checkbox. - * - * @default "m" - */ - size?: ResponsiveValue>; - /** - * Variant of the checkbox. - * - * @default "filled" - */ - variant?: ResponsiveValue>; /** Value of the checkbox if it is part of a group. */ value?: string | number | undefined; /** @@ -78,200 +64,177 @@ export type CheckBoxProps = PressableProps & * * @default "Ionicons" */ - checkedIconFamily?: - | "AntDesign" - | "Entypo" - | "EvilIcons" - | "Feather" - | "FontAwesome" - | "FontAwesome5" - | "Fontisto" - | "Foundation" - | "Ionicons" - | "MaterialCommunityIcons" - | "MaterialIcons" - | "Octicons" - | "SimpleLineIcons" - | "Zocial"; + checkedIconFamily?: IconProps["iconFamily"]; /** * Name of the icon when the checkbox is in checked state. * * @default "checkmark-sharp" */ - checkedIconName?: string; + checkedIconName?: IconProps["iconName"]; /** * Family of the icon when the checkbox is in indeterminate state. * * @default "Ionicons" */ - indeterminateIconFamily?: - | "AntDesign" - | "Entypo" - | "EvilIcons" - | "Feather" - | "FontAwesome" - | "FontAwesome5" - | "Fontisto" - | "Foundation" - | "Ionicons" - | "MaterialCommunityIcons" - | "MaterialIcons" - | "Octicons" - | "SimpleLineIcons" - | "Zocial"; + indeterminateIconFamily?: IconProps["iconFamily"]; /** * Name of the icon when the checkbox is in indeterminate state. * * @default "remove-outline" */ - indeterminateIconName?: string; + indeterminateIconName?: IconProps["iconName"]; children?: string; }; /** The Checkbox component is used in forms when a user needs to select multiple values from several options. **/ -const CheckBox = React.forwardRef( - ( - { - children, - onPress = () => {}, - ...rest - }: Omit, "atoms"> & { - atoms?: Record; - }, - checkboxRef: any - ) => { - const { - size, - variant, - isDisabled, - colorScheme, - shape, - checkboxGroupValue, - addCheckBoxGroupValue, - deleteCheckBoxGroupValue, - } = useCheckBoxGroup(); +const CheckBox = React.memo( + React.forwardRef( + ( + { + children, + onPress = () => {}, + ...rest + }: Omit< + MoleculeComponentProps<"CheckBox", CheckBoxProps, CheckboxAtoms>, + "atoms" + > & { + atoms?: CheckboxAtoms; + }, + checkboxRef: any + ) => { + const { + size, + variant, + isDisabled, + colorScheme, + shape, + checkboxGroupValue, + addCheckBoxGroupValue, + deleteCheckBoxGroupValue, + } = useCheckBoxGroup(); - // Overwrite props from checkbox group - rest.size = rest.size ?? size; - rest.variant = rest.variant ?? variant; - rest.isDisabled = rest.isDisabled ?? isDisabled; - rest.colorScheme = rest.colorScheme ?? colorScheme; - rest.shape = rest.shape ?? shape; + // Overwrite props from checkbox group + rest.size = rest.size ?? size; + rest.variant = rest.variant ?? variant; + rest.isDisabled = rest.isDisabled ?? isDisabled; + rest.colorScheme = rest.colorScheme ?? colorScheme; + rest.shape = rest.shape ?? shape; - const isCheckBoxInGroup = addCheckBoxGroupValue !== undefined; - const isCheckBoxChecked = isCheckBoxInGroup - ? checkboxGroupValue?.includes(rest.value as string | number) && - rest.value !== undefined - : rest.isChecked; + const isCheckBoxInGroup = addCheckBoxGroupValue !== undefined; + const isCheckBoxChecked = isCheckBoxInGroup + ? checkboxGroupValue?.includes(rest.value as string | number) && + rest.value !== undefined + : rest.isChecked; - const molecularProps = useMolecularComponentConfig( - "CheckBox", - rest, - { - size: rest.size, - variant: rest.variant, - }, - rest.colorScheme, - boxStyleFunctions, - "container", - "box", - "container" - ); - const { atoms } = molecularProps; + const molecularProps = useMolecularComponentConfig( + "CheckBox", + rest, + { + size: rest.size, + variant: rest.variant, + }, + rest.colorScheme, + boxStyleFunctions, + "container", + "box", + "container" + ); + const { atoms } = molecularProps; - // Use state for dynamic style - const { propsWithCheckedStyles } = useCheckedState( - atoms.box, - boxStyleFunctions, - "molecule", - true, - isCheckBoxChecked - ); - atoms.box = propsWithCheckedStyles; - const { propsWithInvalidStyles } = useInvalidState( - atoms.box, - boxStyleFunctions, - "molecule", - true, - rest.isInvalid - ); - atoms.box = propsWithInvalidStyles; - const { propsWithDisabledStyles } = useDisabledState( - atoms.container, - boxStyleFunctions, - "molecule", - true, - rest.isDisabled - ); - atoms.container = propsWithDisabledStyles; + // Use state for dynamic style + const { propsWithCheckedStyles } = useCheckedState( + atoms.box, + boxStyleFunctions, + "molecule", + true, + isCheckBoxChecked + ); + atoms.box = propsWithCheckedStyles; + const { propsWithInvalidStyles } = useInvalidState( + atoms.box, + boxStyleFunctions, + "molecule", + true, + rest.isInvalid + ); + atoms.box = propsWithInvalidStyles; + const { propsWithDisabledStyles } = useDisabledState( + atoms.container, + boxStyleFunctions, + "molecule", + true, + rest.isDisabled + ); + atoms.container = propsWithDisabledStyles; - // OTHER METHODS - const checkboxPressHandler = () => { - if (isCheckBoxInGroup) { - // Add the value to the group if the checkbox is currently unchecked - if (!isCheckBoxChecked) addCheckBoxGroupValue(rest.value); - else deleteCheckBoxGroupValue(rest.value); + // OTHER METHODS + const checkboxPressHandler = () => { + if (isCheckBoxInGroup) { + // Add the value to the group if the checkbox is currently unchecked + if (!isCheckBoxChecked) addCheckBoxGroupValue(rest.value); + else deleteCheckBoxGroupValue(rest.value); + if (onPress) onPress(); + } if (onPress) onPress(); - } - if (onPress) onPress(); - }; + }; - // RENDER METHODS - return ( - - -
- -
+ {...atoms.box} + > + + - {!!children && ( - - {children} - - )} -
-
- ); - } + {!!children && {children}} + + + ); + } + ) ); CheckBox.displayName = "CheckBox"; diff --git a/src/components/Molecules/Image/Image.config.ts b/src/components/Molecules/Image/Image.config.ts index 45d747c4..3e326764 100644 --- a/src/components/Molecules/Image/Image.config.ts +++ b/src/components/Molecules/Image/Image.config.ts @@ -1,4 +1,17 @@ -export default { +import { MolecularComponentConfig } from "../../../theme/src/types"; +import { BoxProps } from "../../atoms/box/box"; +import { SpinnerProps } from "../../atoms/spinner/spinner"; +import { ImageProps } from "./image"; + +export type ImageAtoms = { + container: BoxProps; + image: ImageProps; + previewImage: ImageProps; + fallbackImage: ImageProps; + spinner: SpinnerProps; +}; + +const ImageConfig: MolecularComponentConfig = { parts: ["container", "image", "previewImage", "fallbackImage", "spinner"], baseStyle: { container: { @@ -16,3 +29,5 @@ export default { }, }, }; + +export default ImageConfig; diff --git a/src/components/Molecules/Image/Image.tsx b/src/components/Molecules/Image/Image.tsx index 9f7bebfe..15572b27 100644 --- a/src/components/Molecules/Image/Image.tsx +++ b/src/components/Molecules/Image/Image.tsx @@ -1,4 +1,10 @@ -import React, { useEffect, useRef, useState } from "react"; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { Animated, ImageErrorEventData, @@ -9,7 +15,6 @@ import { Platform, StyleSheet, ImageURISource, - ColorValue, View, } from "react-native"; import { DownloadOptions } from "expo-file-system"; @@ -17,9 +22,19 @@ import CacheManager from "./cache-manager"; import Box, { BoxProps } from "../../atoms/box/box"; import { BlurView } from "expo-blur"; import Spinner from "../../atoms/spinner/spinner"; -import { MoleculeComponentProps } from "../../../theme/src/types"; +import { + MoleculeComponentProps, + PaletteColors, + ResponsiveValue, +} from "../../../theme/src/types"; import { pearl } from "../../../pearl"; import Center from "../../atoms/center/center"; +import { ImageAtoms } from "./image.config"; +import { + createStyleFunction, + transformColorValue, +} from "../../../theme/src/style-functions"; +import { useStyleProps } from "../../../hooks"; function usePrevious(value: any) { const ref = useRef(); @@ -78,7 +93,7 @@ export type BaseImageProps = BoxProps & /** Source of the image to show while the remote image is being fetched */ previewSource?: ImageSourcePropType; /** Color of the image container while the remote image is being fetched */ - previewColor?: ColorValue; + previewColor?: ResponsiveValue; /** Download configuration when fetching the remote image */ imageDownloadOptions?: DownloadOptions; /** @@ -105,201 +120,258 @@ export type BaseImageProps = BoxProps & fallbackSource?: ImageSourcePropType; }; -const CustomImage = React.forwardRef( - ({ atoms }: MoleculeComponentProps<"Image", BaseImageProps>, ref: any) => { - const { - source, - onError, - testID, - fallbackSource, - fallbackComponent, - previewSource, - previewColor, - imageDownloadOptions, - isCached = true, - loaderType = "spinner", - tint = "dark", - overlayTransitionDuration = 300, - ...restImageProps - } = atoms.image; - - const isMounted = useRef(true); - const isRemoteImage = typeof source === "object"; - const shouldCache = isRemoteImage && isCached; - const [uri, setUri] = useState(undefined); - const [error, setError] = useState(false); - const previousUri = usePrevious(uri); - - // The image should be ready by default if it's a local image - const isImageReady = isRemoteImage ? !!uri : true; - - const finalSource: ImageSourcePropType = isRemoteImage - ? { ...(source as object), uri: uri } - : source; - - const intensity = useRef(new Animated.Value(100)).current; - const previewSourceOverlayOpacity = intensity.interpolate({ - inputRange: [0, 100], - outputRange: [0, 1], - }); - const blurIntensity = intensity.interpolate({ - inputRange: [0, 100], - outputRange: [0, 50], - }); - const previewColorOverlayOpacity = intensity.interpolate({ - inputRange: [0, 100], - outputRange: [0, 1], - }); - - // Separate out border radius properties - const { - borderRadius, - borderBottomLeftRadius, - borderBottomRightRadius, - borderTopLeftRadius, - borderTopRightRadius, - ...finalContainerProps - } = atoms.container; - const borderRadiiStyles = { - borderRadius, - borderBottomLeftRadius, - borderBottomRightRadius, - borderTopLeftRadius, - borderTopRightRadius, - }; - - // A handler function for catching errors while loading the image - const onErrorHandler = ( - error: NativeSyntheticEvent +const previewColorStyleFunction = createStyleFunction({ + property: "previewColor", + styleProperty: "previewColor", + themeKey: "palette", + transform: transformColorValue, +}); + +const CustomImage = React.memo( + React.forwardRef( + ( + { atoms }: MoleculeComponentProps<"Image", BaseImageProps, ImageAtoms>, + ref: any ) => { - setError(true); - - if (onError) onError(error); - }; - - // Fetches and caches the remote image - const loadRemoteImage = async ( - uri: string, - options = {} - ): Promise => { - // Use CacheManager if the image is supposed to be cached - if (shouldCache && Platform.OS !== "web") { - try { - const path = await CacheManager.get(uri, options).getPath(); - if (isMounted.current) { - if (path) { - setUri(path); - } else { - onErrorHandler({ - nativeEvent: { error: new Error("Could not load image") }, - } as NativeSyntheticEvent); + const { + source, + onError, + testID, + fallbackSource, + fallbackComponent, + previewSource, + imageDownloadOptions, + isCached = true, + loaderType = "spinner", + tint = "dark", + overlayTransitionDuration = 300, + ...restImageProps + } = atoms.image; + + const isMounted = useRef(true); + const isRemoteImage = useMemo(() => typeof source === "object", [source]); + const shouldCache = useMemo( + () => isRemoteImage && isCached, + [isRemoteImage, isCached] + ); + const [uri, setUri] = useState(undefined); + const [error, setError] = useState(false); + const previousUri = usePrevious(uri); + + const imageProps = useStyleProps(atoms.image, [ + previewColorStyleFunction, + ]); + const { previewColor } = imageProps.style; + + const isImageReady = useMemo( + () => (isRemoteImage ? !!uri : true), + [isRemoteImage, uri] + ); + + const finalSource: ImageSourcePropType = useMemo( + () => (isRemoteImage ? { ...(source as object), uri: uri } : source), + [isRemoteImage, source, uri] + ); + + const intensity = useRef(new Animated.Value(100)).current; + const previewSourceOverlayOpacity = intensity.interpolate({ + inputRange: [0, 100], + outputRange: [0, 1], + }); + const blurIntensity = intensity.interpolate({ + inputRange: [0, 100], + outputRange: [0, 50], + }); + const previewColorOverlayOpacity = intensity.interpolate({ + inputRange: [0, 100], + outputRange: [0, 1], + }); + + const { + borderRadius, + borderBottomLeftRadius, + borderBottomRightRadius, + borderTopLeftRadius, + borderTopRightRadius, + ...finalContainerProps + } = atoms.container; + const borderRadiiStyles = useMemo( + () => ({ + borderRadius, + borderBottomLeftRadius, + borderBottomRightRadius, + borderTopLeftRadius, + borderTopRightRadius, + }), + [ + borderRadius, + borderBottomLeftRadius, + borderBottomRightRadius, + borderTopLeftRadius, + borderTopRightRadius, + ] + ); + + const onErrorHandler = ( + error: NativeSyntheticEvent + ) => { + setError(true); + + if (onError) onError(error); + }; + + const loadRemoteImage = async ( + uri: string, + options = {} + ): Promise => { + if (shouldCache && Platform.OS !== "web") { + try { + const path = await CacheManager.get(uri, options).getPath(); + if (isMounted.current) { + if (path) { + setUri(path); + } else { + onErrorHandler({ + nativeEvent: { error: new Error("Could not load image") }, + } as NativeSyntheticEvent); + } } + } catch (error) { + onErrorHandler({ + nativeEvent: { error }, + } as NativeSyntheticEvent); } - } catch (error) { - onErrorHandler({ - nativeEvent: { error }, - } as NativeSyntheticEvent); + } else { + setUri(uri); } - } - // Else load the remote image directly - else { - setUri(uri); - } - }; - - const renderFallback = () => { - if (error) { - if (!!fallbackComponent) { - return ( - - {React.cloneElement(fallbackComponent)} - - ); + }; + + const renderFallback = useCallback(() => { + if (error) { + if (!!fallbackComponent) { + return ( + + {React.cloneElement(fallbackComponent)} + + ); + } + + if (!!fallbackSource) { + return ( + + ); + } + + return null; } - if (!!fallbackSource) { + return null; + }, [error, fallbackComponent, fallbackSource, borderRadiiStyles]); + + const renderFinalImage = useCallback(() => { + if (isImageReady) { return ( ); } return null; - } - - return null; - }; - - const renderFinalImage = () => { - if (isImageReady) { - return ( - - ); - } - - return null; - }; - - const renderPreview = () => { - if (!!previewSource) { - return ( - { + if (!!previewSource) { + return ( + + ); + } + }, [previewSource, borderRadiiStyles]); + + const renderImageLoader = useCallback(() => { + if (typeof finalSource === "number") return null; + + if (loaderType === "progressive") { + if (!!previewSource) { + if (Platform.OS === "ios") { + return ( + + {renderFallback()} + + ); } - /> - ); - } - }; - const renderImageLoader = () => { - if (loaderType === "progressive") { - if (!!previewSource) { - // Render a blur overlay over the preview image - if (Platform.OS === "ios") { - return ( - - {renderFallback()} - - ); + if (Platform.OS === "android" || Platform.OS === "web") { + return ( + + {renderFallback()} + + ); + } } - // Render a static overlay over the preview image - if (Platform.OS === "android" || Platform.OS === "web") { + if (!!previewColor) { return ( {renderFallback()} @@ -320,72 +392,69 @@ const CustomImage = React.forwardRef( } } - if (!!previewColor) { + if (loaderType === "spinner" && !error) { return ( - - {renderFallback()} - + + + ); } - } - - if (loaderType === "spinner" && !error) { - return ( - - - - ); - } - }; - - useEffect(() => { - // Reload the network image when the URI updates - if (isRemoteImage) { - loadRemoteImage( - (source as ImageURISource).uri as string, - imageDownloadOptions - ); - - // Start animating the overlay opacity as soon as the URI becomes available - if (uri && !previousUri) { - Animated.timing(intensity, { - toValue: 0, - duration: overlayTransitionDuration, - useNativeDriver: Platform.OS === "android", - }).start(); + }, [ + loaderType, + previewSource, + tint, + finalSource, + borderRadiiStyles, + blurIntensity, + renderFallback, + previewColor, + previewColorOverlayOpacity, + error, + atoms.spinner, + ]); + + useEffect(() => { + if (isRemoteImage) { + loadRemoteImage( + (source as ImageURISource).uri as string, + imageDownloadOptions + ); + + if (uri && !previousUri) { + Animated.timing(intensity, { + toValue: 0, + duration: overlayTransitionDuration, + useNativeDriver: Platform.OS === "android", + }).start(); + } } - } - return () => { - isMounted.current = false; - }; - }, [uri]); - - return ( -
- {renderFinalImage()} - {renderPreview()} - {renderImageLoader()} -
- ); - } + return () => { + isMounted.current = false; + }; + }, [ + uri, + isRemoteImage, + loadRemoteImage, + source, + imageDownloadOptions, + overlayTransitionDuration, + ]); + + return ( +
+ {renderFinalImage()} + {renderPreview()} + {renderImageLoader()} +
+ ); + } + ) ); /** The Image component is used to display images. */ diff --git a/src/components/Molecules/Input/Input.config.ts b/src/components/Molecules/Input/Input.config.ts index 2f524ed8..e1ec0926 100644 --- a/src/components/Molecules/Input/Input.config.ts +++ b/src/components/Molecules/Input/Input.config.ts @@ -1,4 +1,17 @@ -export default { +import { MolecularComponentConfig, StateProps } from "../../../theme/src/types"; +import { BoxProps } from "../../atoms/box/box"; +import { IconProps } from "../../atoms/icon/icon"; +import { TextProps } from "../../atoms/text/text"; +import { InputProps } from "./input"; + +export type InputAtoms = { + container: BoxProps & StateProps<"_focused" | "_disabled" | "_invalid">; + input: InputProps; + text: TextProps; + icon: IconProps; +}; + +const InputConfig: MolecularComponentConfig = { parts: ["container", "input", "text", "icon"], baseStyle: { container: { @@ -108,16 +121,14 @@ export default { variants: { filled: { container: { - animate: { - borderWidth: 1, - backgroundColor: { - light: "neutral.200", - dark: "neutral.900", - }, - borderColor: { - light: "neutral.200", - dark: "neutral.900", - }, + borderWidth: 1, + backgroundColor: { + light: "neutral.200", + dark: "neutral.900", + }, + borderColor: { + light: "neutral.200", + dark: "neutral.900", }, _focused: { borderColor: "primary.500", @@ -126,16 +137,14 @@ export default { }, outline: { container: { - animate: { - borderWidth: 1, - backgroundColor: { - light: "neutral.50", - dark: "neutral.800", - }, - borderColor: { - light: "neutral.400", - dark: "neutral.500", - }, + borderWidth: 1, + backgroundColor: { + light: "neutral.50", + dark: "neutral.800", + }, + borderColor: { + light: "neutral.400", + dark: "neutral.500", }, _focused: { borderColor: "primary.500", @@ -160,3 +169,5 @@ export default { variant: "filled", }, }; + +export default InputConfig; diff --git a/src/components/Molecules/Input/Input.tsx b/src/components/Molecules/Input/Input.tsx index 759b8040..5f4a8b93 100644 --- a/src/components/Molecules/Input/Input.tsx +++ b/src/components/Molecules/Input/Input.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState } from "react"; +import React, { useCallback, useMemo, useRef, useState } from "react"; import Box, { BoxProps } from "../../atoms/box/box"; import { buildFontConfig } from "../../atoms/text/text"; import { useMolecularComponentConfig } from "../../../hooks/useMolecularComponentConfig"; @@ -16,8 +16,6 @@ import { StyleFunctionContainer, ResponsiveValue, PaletteColors, - ComponentSizes, - ComponentVariants, MoleculeComponentProps, StateProps, } from "../../../theme/src/types"; @@ -34,6 +32,8 @@ import { useFocusedState } from "../../../hooks/state/useFocusedState"; import { useDisabledState } from "../../../hooks/state/useDisabledState"; import { useInvalidState } from "../../../hooks/state/useInvalidState"; import { HStack } from "../../atoms/stack/stack"; +import { useTheme } from "../../../hooks"; +import { InputAtoms } from "./input.config"; import _ from "lodash"; export type InputProps = Omit< @@ -42,18 +42,6 @@ export type InputProps = Omit< > & BoxProps & StateProps<"_focused" | "_disabled" | "_invalid"> & { - /** - * Size of the input field. - * - * @default "m" - */ - size?: ResponsiveValue>; - /** - * Variant of the input field. - * - * @default "filled" - */ - variant?: ResponsiveValue>; /** * Whether the input field is disabled. * @@ -123,219 +111,220 @@ const inputTextStyleFunction = [ ] as StyleFunctionContainer[]; /** The Input component is a component that is used to get user input in a text field. **/ -const Input = React.forwardRef( - ( - { - children, - onFocus = undefined, - onBlur = undefined, - size = "m", - isDisabled = false, - isFullWidth = false, - hasClearButton = false, - isInvalid = false, - leftIcon = undefined, - rightIcon = undefined, - colorScheme = "primary", - onChangeText = () => {}, - onChange = () => {}, - ...rest - }: Omit, "atoms"> & { - atoms?: Record; - }, - textInputRef: any - ) => { - if (!textInputRef) { - textInputRef = useRef(); - } - - const [isCleared, setIsCleared] = useState( - rest.value && rest.value.length > 0 ? false : true - ); - - let molecularProps = useMolecularComponentConfig( - "Input", - rest, +const Input = React.memo( + React.forwardRef( + ( { - size: size, - variant: rest["variant"], + children, + onFocus = undefined, + onBlur = undefined, + size = "m", + isDisabled = false, + isFullWidth = false, + hasClearButton = false, + isInvalid = false, + leftIcon = undefined, + rightIcon = undefined, + colorScheme = "primary", + onChangeText = () => {}, + onChange = () => {}, + ...rest + }: Omit< + MoleculeComponentProps<"Input", InputProps, InputAtoms>, + "atoms" + > & { + atoms?: InputAtoms; }, - colorScheme, - boxStyleFunctions, - "container", - "input", - "container" - ); + textInputRef: any + ) => { + const { theme } = useTheme(); + const [isCleared, setIsCleared] = useState( + rest.value && rest.value.length > 0 ? false : true + ); - let { atoms } = molecularProps; + let molecularProps = useMolecularComponentConfig( + "Input", + rest, + { + size: size, + variant: rest["variant"], + }, + colorScheme, + boxStyleFunctions, + "container", + "input", + "container" + ); - const inputProps = useStyleProps(atoms.input, inputRootStyleFunction); - const { placeholderTextColor, selectionColor, ...finalInputStyle } = - inputProps.style; + const { atoms } = molecularProps; - const textProps = useAtomicComponentConfig( - "Text", - atoms.text, - { - size: size, - variant: atoms.text.variant, - }, - "primary", - inputTextStyleFunction - ); + const inputProps = useStyleProps(atoms.input, inputRootStyleFunction); + const { placeholderTextColor, selectionColor, ...finalInputStyle } = + inputProps.style; - const memoizedBuildFontConfig = React.useCallback( - () => buildFontConfig(textProps.style, atoms.input.allowFontScaling), - [textProps.style, atoms.input.allowFontScaling] - ); + const textProps = useAtomicComponentConfig( + "Text", + atoms.text, + { + size: size, + variant: atoms.text.variant, + }, + "primary", + inputTextStyleFunction + ); - const finalTextStyles = { - ...textProps.style, - ...memoizedBuildFontConfig(), - }; + const memoizedBuildFontConfig = useCallback( + () => + buildFontConfig( + textProps.style, + atoms.input.allowFontScaling || true, + theme + ), + [textProps.style, atoms.input.allowFontScaling] + ); - // Use State for dynamic styles - const { focused, setFocused, propsWithFocusedStyles } = useFocusedState( - atoms.container, - boxStyleFunctions, - "molecule" - ); - atoms.container = propsWithFocusedStyles; - const { propsWithDisabledStyles } = useDisabledState( - atoms.container, - boxStyleFunctions, - "molecule", - true, - isDisabled - ); - atoms.container = propsWithDisabledStyles; - const { propsWithInvalidStyles } = useInvalidState( - atoms.container, - boxStyleFunctions, - "molecule", - true, - isInvalid - ); - atoms.container = propsWithInvalidStyles; + const finalTextStyles = useMemo( + () => ({ + ...textProps.style, + ...memoizedBuildFontConfig(), + }), + [textProps.style, memoizedBuildFontConfig] + ); - // METHODS - const clearInputHandler = () => { - (textInputRef.current as any).clear(); - setIsCleared(true); - onChangeText(""); - }; + // Use State for dynamic styles + const { focused, setFocused, propsWithFocusedStyles } = useFocusedState( + atoms.container, + boxStyleFunctions, + "molecule" + ); + atoms.container = propsWithFocusedStyles; + const { propsWithDisabledStyles } = useDisabledState( + atoms.container, + boxStyleFunctions, + "molecule", + true, + isDisabled + ); + atoms.container = propsWithDisabledStyles; + const { propsWithInvalidStyles } = useInvalidState( + atoms.container, + boxStyleFunctions, + "molecule", + true, + isInvalid + ); + atoms.container = propsWithInvalidStyles; - const onChangeHandler = ( - event: NativeSyntheticEvent - ) => { - const { text } = event.nativeEvent; - if (text.length > 0) { - setIsCleared(false); - } + // METHODS + const clearInputHandler = () => { + (textInputRef.current as any).clear(); + setIsCleared(true); + onChangeText(""); + }; - onChange(event); - }; + const onChangeHandler = ( + event: NativeSyntheticEvent + ) => { + const { text } = event.nativeEvent; + if (text.length > 0) { + setIsCleared(false); + } - const onChangeTextHandler = (value: string) => { - if (value.length > 0) { - setIsCleared(false); - } + onChange(event); + }; - onChangeText(value); - }; + const onChangeTextHandler = (value: string) => { + if (value.length > 0) { + setIsCleared(false); + } - const focusInputHandler = ( - event: NativeSyntheticEvent - ) => { - setFocused(true); - if (onFocus) { - onFocus(event); - } - }; + onChangeText(value); + }; - const blurInputHandler = ( - event: NativeSyntheticEvent - ) => { - setFocused(false); - if (onBlur) { - onBlur(event); - } - }; + const focusInputHandler = ( + event: NativeSyntheticEvent + ) => { + setFocused(true); + if (onFocus) { + onFocus(event); + } + }; - // Render Functions - const renderLeftIcon = () => { - if (leftIcon) { - return React.cloneElement(leftIcon, { - ...atoms.icon, - ...leftIcon.props, - }); - } - }; + const blurInputHandler = ( + event: NativeSyntheticEvent + ) => { + setFocused(false); + if (onBlur) { + onBlur(event); + } + }; - const renderRightIcon = () => { - if (rightIcon) { - return React.cloneElement(rightIcon, { - ...atoms.icon, - ...rightIcon.props, - }); - } - }; + // Render Functions + const renderLeftIcon = useCallback(() => { + if (leftIcon) { + return React.cloneElement(leftIcon, { + ...atoms.icon, + ...leftIcon.props, + }); + } + }, [leftIcon, atoms.icon]); - const renderClearIcon = () => { - if (hasClearButton && !isCleared) { - return ( - - - - ); - } - }; + const renderRightIcon = useCallback(() => { + if (rightIcon) { + return React.cloneElement(rightIcon, { + ...atoms.icon, + ...rightIcon.props, + }); + } + }, [rightIcon, atoms.icon]); - return ( - - {renderLeftIcon()} + const renderClearIcon = useCallback(() => { + if (hasClearButton && !isCleared) { + return ( + + + + ); + } + }, [hasClearButton, isCleared, clearInputHandler, atoms.icon]); - + return ( + + {renderLeftIcon()} + - - {renderRightIcon()} - {renderClearIcon()} - - - ); - } + + {renderRightIcon()} + {renderClearIcon()} + + + ); + } + ) ); Input.displayName = "Input"; diff --git a/src/components/Molecules/Radio/Radio.config.ts b/src/components/Molecules/Radio/Radio.config.ts index 261922ee..6a3afc51 100644 --- a/src/components/Molecules/Radio/Radio.config.ts +++ b/src/components/Molecules/Radio/Radio.config.ts @@ -1,4 +1,17 @@ -export default { +import { MolecularComponentConfig, StateProps } from "../../../theme/src/types"; +import { BoxProps } from "../../atoms/box/box"; +import { PressableProps } from "../../atoms/pressable/pressable"; +import { TextProps } from "../../atoms/text/text"; +import { RadioProps } from "./radio"; + +export type RadioAtoms = { + container: Omit & RadioProps; + outerBox: BoxProps & StateProps<"_checked" | "_invalid">; + innerBox: BoxProps & StateProps<"_checked">; + text: TextProps; +}; + +const RadioConfig: MolecularComponentConfig = { parts: ["container", "outerBox", "innerBox", "text"], baseStyle: { container: { @@ -10,7 +23,6 @@ export default { }, outerBox: { p: "0.5", - shape: "square", borderWidth: 2, borderRadius: "full", borderColor: "neutral.300", @@ -29,6 +41,9 @@ export default { duration: 50, }, }, + text: { + alignSelf: "center", + }, }, sizes: { xs: { @@ -139,3 +154,5 @@ export default { variant: "filled", }, }; + +export default RadioConfig; diff --git a/src/components/Molecules/Radio/Radio.tsx b/src/components/Molecules/Radio/Radio.tsx index 218b6c4e..ecdcc6e9 100644 --- a/src/components/Molecules/Radio/Radio.tsx +++ b/src/components/Molecules/Radio/Radio.tsx @@ -3,8 +3,6 @@ import Text from "../../atoms/text/text"; import { FinalPearlTheme, ResponsiveValue, - ComponentSizes, - ComponentVariants, MoleculeComponentProps, StateProps, } from "../../../theme/src/types"; @@ -16,23 +14,12 @@ import { useRadioGroup } from "./radio-group"; import { useCheckedState } from "../../../hooks/state/useCheckedState"; import { useInvalidState } from "../../../hooks/state/useInvalidState"; import Center from "../../atoms/center/center"; -import _ from "lodash"; import Box from "../../atoms/box/box"; +import _ from "lodash"; +import { RadioAtoms } from "./radio.config"; export type RadioProps = PressableProps & StateProps<"_checked" | "_invalid" | "_disabled"> & { - /** - * Size of the radio. - * - * @default "m" - */ - size?: ResponsiveValue>; - /** - * Variant of the radio. - * - * @default "filled" - */ - variant?: ResponsiveValue>; /** Value of the radio if it is part of a group. */ value?: string | number | undefined; /** @@ -59,123 +46,128 @@ export type RadioProps = PressableProps & * @default 2 */ spacing?: ResponsiveValue; + children?: string; }; /** The Radio component is used when only one choice may be selected in a series of options. **/ -const Radio = React.forwardRef( - ( - { - children, - onPress = () => {}, - ...rest - }: Omit, "atoms"> & { - atoms?: Record; - }, - radioRef: any - ) => { - const { - size, - variant, - isDisabled, - colorScheme, - radioGroupValue, - setRadioGroupValue, - } = useRadioGroup(); - - // Overwrite props from radio group - rest.size = rest.size ?? size; - rest.variant = rest.variant ?? variant; - rest.isDisabled = rest.isDisabled ?? isDisabled; - rest.colorScheme = rest.colorScheme ?? colorScheme; - - const isRadioInGroup = setRadioGroupValue !== undefined; - const isRadioChecked = isRadioInGroup - ? radioGroupValue === rest.value && rest.value !== undefined - : rest.isChecked; - - const molecularProps = useMolecularComponentConfig( - "Radio", - rest, +const Radio = React.memo( + React.forwardRef( + ( { - size: rest.size, - variant: rest.variant, + children, + onPress = () => {}, + ...rest + }: Omit< + MoleculeComponentProps<"Radio", RadioProps, RadioAtoms>, + "atoms" + > & { + atoms?: RadioAtoms; }, - rest.colorScheme, - boxStyleFunctions, - "container", - "container", - "container" - ); - const { atoms } = molecularProps; + radioRef: any + ) => { + const { + size, + variant, + isDisabled, + colorScheme, + radioGroupValue, + setRadioGroupValue, + } = useRadioGroup(); - // Use state for dynamic style - const { propsWithCheckedStyles: propsWithCheckedStylesForOuterBox } = - useCheckedState( - atoms.outerBox, + // Overwrite props from radio group + rest.size = rest.size ?? size; + rest.variant = rest.variant ?? variant; + rest.isDisabled = rest.isDisabled ?? isDisabled; + rest.colorScheme = rest.colorScheme ?? colorScheme; + + const isRadioInGroup = setRadioGroupValue !== undefined; + const isRadioChecked = isRadioInGroup + ? radioGroupValue === rest.value && rest.value !== undefined + : rest.isChecked; + + const molecularProps = useMolecularComponentConfig( + "Radio", + rest, + { + size: rest.size, + variant: rest.variant, + }, + rest.colorScheme, boxStyleFunctions, - "molecule", - true, - isRadioChecked + "container", + "container", + "container" ); - atoms.outerBox = propsWithCheckedStylesForOuterBox; - const { propsWithInvalidStyles } = useInvalidState( - atoms.outerBox, - boxStyleFunctions, - "molecule", - true, - rest.isInvalid - ); - atoms.outerBox = propsWithInvalidStyles; - const { propsWithCheckedStyles: propsWithCheckedStylesForInnerBox } = - useCheckedState( - atoms.innerBox, + const { atoms } = molecularProps; + + // Use state for dynamic style + const { propsWithCheckedStyles: propsWithCheckedStylesForOuterBox } = + useCheckedState( + atoms.outerBox, + boxStyleFunctions, + "molecule", + true, + isRadioChecked + ); + atoms.outerBox = propsWithCheckedStylesForOuterBox; + const { propsWithInvalidStyles } = useInvalidState( + atoms.outerBox, boxStyleFunctions, "molecule", true, - isRadioChecked + rest.isInvalid ); - atoms.innerBox = propsWithCheckedStylesForInnerBox; + atoms.outerBox = propsWithInvalidStyles; + const { propsWithCheckedStyles: propsWithCheckedStylesForInnerBox } = + useCheckedState( + atoms.innerBox, + boxStyleFunctions, + "molecule", + true, + isRadioChecked + ); + atoms.innerBox = propsWithCheckedStylesForInnerBox; - // OTHER METHODS - const radioPressHandler = () => { - if (isRadioInGroup) { - setRadioGroupValue(rest.value); + // OTHER METHODS + const radioPressHandler = () => { + if (isRadioInGroup) { + setRadioGroupValue(rest.value); + if (onPress) onPress(); + } if (onPress) onPress(); - } - if (onPress) onPress(); - }; + }; - // RENDER METHODS - return ( - - -
- -
- - {!!children && {children}} -
-
- ); - } + +
+ +
+ {!!children && {children}} +
+ + ); + } + ) ); Radio.displayName = "Radio"; diff --git a/src/components/atoms/collapse/collapse.tsx b/src/components/atoms/collapse/collapse.tsx index b1ab0dfc..2b6e06e5 100644 --- a/src/components/atoms/collapse/collapse.tsx +++ b/src/components/atoms/collapse/collapse.tsx @@ -29,88 +29,92 @@ type CollapseProps = BoxProps & { /** * Collapse is a component that provides an expandable view. */ -const Collapse = React.forwardRef( - ( - { - children, - show, - animateOpacity = true, - startingHeight = 0, - transition = {}, - exitTransition = {}, - endingHeight = "auto", - ...rest - }: CollapseProps, - ref: any - ) => { - const endingHeightRef = useRef(endingHeight); +const Collapse = React.memo( + React.forwardRef( + ( + { + children, + show, + animateOpacity = true, + startingHeight = 0, + transition = {}, + exitTransition = {}, + endingHeight = "auto", + ...rest + }: CollapseProps, + ref: any + ) => { + const endingHeightRef = useRef(endingHeight); - return ( - - {show && ( - ["transition"] - } - exit={{ - ...(animateOpacity && { opacity: 0 }), - height: 0, - }} - exitTransition={ - { - dampingRatio: 1, - duration: 100, - easing: Easing.inOut, - ...exitTransition, - type: "spring", - } as MotiWithPearlStyleProps< - ViewStyle, - BoxStyleProps - >["exitTransition"] - } - > - { - if ( - typeof endingHeightRef.current === "string" || - nativeEvent.layout.height > endingHeightRef.current - ) - endingHeightRef.current = Math.ceil( - nativeEvent.layout.height - ); - }} - > - {children} - - - )} - - ); - } + return ( + + {show && ( + + ["transition"] + } + exit={{ + ...(animateOpacity && { opacity: 0 }), + height: 0, + }} + exitTransition={ + { + dampingRatio: 1, + duration: 100, + easing: Easing.inOut, + ...exitTransition, + type: "spring", + } as MotiWithPearlStyleProps< + ViewStyle, + BoxStyleProps + >["exitTransition"] + } + > + { + if ( + typeof endingHeightRef.current === "string" || + nativeEvent.layout.height > endingHeightRef.current + ) + endingHeightRef.current = Math.ceil( + nativeEvent.layout.height + ); + }} + > + {children} + + + + )} + + ); + } + ) ); Collapse.displayName = "Collapse"; diff --git a/src/components/atoms/fade/fade.tsx b/src/components/atoms/fade/fade.tsx index 52c71b6c..6188e7cd 100644 --- a/src/components/atoms/fade/fade.tsx +++ b/src/components/atoms/fade/fade.tsx @@ -14,63 +14,61 @@ type FadeProps = BoxProps & { /** * Fade is a component that provides an view with a fade transition. */ -const Fade = React.forwardRef( - ( - { - children, - show, - transition = {}, - exitTransition = {}, - ...rest - }: FadeProps, - ref: any - ) => { - return ( - - {show && ( - ["transition"] - } - exitTransition={ - { - dampingRatio: 1, - duration: 100, - easing: Easing.inOut, - ...exitTransition, - type: "spring", - } as MotiWithPearlStyleProps< - ViewStyle, - BoxStyleProps - >["exitTransition"] - } - > - {children} - - )} - - ); - } +const Fade = React.memo( + React.forwardRef( + ( + { + children, + show, + transition = {}, + exitTransition = {}, + ...rest + }: FadeProps, + ref: any + ) => { + const fromStyle = React.useMemo(() => ({ opacity: 0 }), []); + const animateStyle = React.useMemo(() => ({ opacity: 1 }), []); + const exitStyle = React.useMemo(() => ({ opacity: 0 }), []); + const transitionStyle = React.useMemo( + () => ({ + dampingRatio: 1, + duration: 100, + easing: Easing.inOut, + type: "spring", + ...(transition as any), + }), + [transition] + ); + const exitTransitionStyle = React.useMemo( + () => ({ + dampingRatio: 1, + duration: 100, + easing: Easing.inOut, + type: "spring", + ...(exitTransition as any), + }), + [exitTransition] + ); + + return ( + + {show && ( + + {children} + + )} + + ); + } + ) ); Fade.displayName = "Fade"; diff --git a/src/components/atoms/scale-fade/scale-fade.tsx b/src/components/atoms/scale-fade/scale-fade.tsx index dbd8cd9f..a38f32bc 100644 --- a/src/components/atoms/scale-fade/scale-fade.tsx +++ b/src/components/atoms/scale-fade/scale-fade.tsx @@ -20,67 +20,66 @@ type ScaleFadeProps = BoxProps & { /** * ScaleFade is a component that provides an view with a scaling fade transition. */ -const ScaleFade = React.forwardRef( - ( - { - children, - show, - initialScale = 0.9, - transition = {}, - exitTransition = {}, - ...rest - }: ScaleFadeProps, - ref: any - ) => { - return ( - - {show && ( - ["transition"] - } - exitTransition={ - { - dampingRatio: 1, - duration: 100, - easing: Easing.inOut, - ...exitTransition, - type: "spring", - } as MotiWithPearlStyleProps< - ViewStyle, - BoxStyleProps - >["exitTransition"] - } - > - {children} - - )} - - ); - } +const ScaleFade = React.memo( + React.forwardRef( + ( + { + children, + show, + initialScale = 0.9, + transition = {}, + exitTransition = {}, + ...rest + }: ScaleFadeProps, + ref: any + ) => { + const fromStyle = React.useMemo( + () => ({ scale: initialScale, opacity: 0 }), + [initialScale] + ); + const animateStyle = React.useMemo(() => ({ scale: 1, opacity: 1 }), []); + const exitStyle = React.useMemo( + () => ({ scale: initialScale, opacity: 0 }), + [initialScale] + ); + const transitionProps = React.useMemo( + () => ({ + dampingRatio: 1, + duration: 100, + type: "spring", + ...(transition as any), + }), + [transition] + ); + const exitTransitionProps = React.useMemo( + () => ({ + dampingRatio: 1, + duration: 100, + type: "spring", + ...(exitTransition as any), + }), + [exitTransition] + ); + + return ( + + {show && ( + + {children} + + )} + + ); + } + ) ); ScaleFade.displayName = "ScaleFade"; diff --git a/src/components/atoms/skeleton/skeleton-circle.tsx b/src/components/atoms/skeleton/skeleton-circle.tsx index d40f8873..5519510e 100644 --- a/src/components/atoms/skeleton/skeleton-circle.tsx +++ b/src/components/atoms/skeleton/skeleton-circle.tsx @@ -13,18 +13,20 @@ type SkeletonCircleProps = SkeletonProps & { /** * `SkeletonCircle` is a layout component that displays a circular skeleton placeholder. */ -const SkeletonCircle = React.forwardRef( - ({ boxSize = 20, ...rest }: SkeletonCircleProps, ref: any) => { - return ( - - ); - } +const SkeletonCircle = React.memo( + React.forwardRef( + ({ boxSize = 20, ...rest }: SkeletonCircleProps, ref: any) => { + return ( + + ); + } + ) ); SkeletonCircle.displayName = "SkeletonCircle"; diff --git a/src/components/atoms/skeleton/skeleton-text.tsx b/src/components/atoms/skeleton/skeleton-text.tsx index bc6881cf..0812e58b 100644 --- a/src/components/atoms/skeleton/skeleton-text.tsx +++ b/src/components/atoms/skeleton/skeleton-text.tsx @@ -1,9 +1,5 @@ import React from "react"; -import { - FinalPearlTheme, - PaletteColors, - ResponsiveValue, -} from "../../../theme/src/types"; +import { FinalPearlTheme, ResponsiveValue } from "../../../theme/src/types"; import Skeleton, { SkeletonProps } from "./skeleton"; import Stack from "../stack/stack"; @@ -37,68 +33,73 @@ function range(count: number) { /** * `SkeletonText` is used to display the loading state in the form of text. */ -const SkeletonText = React.forwardRef( - ( - { - children, - size, - variant, - colorScheme, - startColor, - endColor, - fadeDuration, - speed, - spacing = "3", - noOfLines = 3, - isLoaded = false, - skeletonHeight = 20, - ...rest - }: SkeletonTextProps, - ref: any - ) => { - const numbers = range(noOfLines); +const SkeletonText = React.memo( + React.forwardRef( + ( + { + children, + size, + variant, + colorScheme, + startColor, + endColor, + fadeDuration, + speed, + spacing = "3", + noOfLines = 3, + isLoaded = false, + skeletonHeight = 20, + ...rest + }: SkeletonTextProps, + ref: any + ) => { + const numbers = React.useMemo(() => range(noOfLines), [noOfLines]); - const getWidth = (index: number): any => { - if (noOfLines > 1) { - return index === numbers.length ? "80%" : "100%"; - } - return "100%"; - }; - - return ( - - {numbers.map((number, index) => { - if (isLoaded && index > 0) { - return null; + const getWidth = React.useCallback( + (index: number): any => { + if (noOfLines > 1) { + return index === numbers.length ? "80%" : "100%"; } + return "100%"; + }, + [noOfLines, numbers.length] + ); + + return ( + + {numbers.map((number, index) => { + if (isLoaded && index > 0) { + return null; + } - return ( - - {index === 0 ? children : undefined} - - ); - })} - - ); - } + return ( + + {index === 0 ? children : undefined} + + ); + })} + + ); + } + ) ); SkeletonText.displayName = "SkeletonText"; diff --git a/src/components/atoms/skeleton/skeleton.config.ts b/src/components/atoms/skeleton/skeleton.config.ts index e44f6174..a0b3ca9d 100644 --- a/src/components/atoms/skeleton/skeleton.config.ts +++ b/src/components/atoms/skeleton/skeleton.config.ts @@ -1,4 +1,7 @@ -export default { +import { AtomicComponentConfig } from "../../../theme/src/types"; +import { SkeletonProps } from "./skeleton"; + +const SkeletonConfig: AtomicComponentConfig = { baseStyle: { borderRadius: "m", startColor: { @@ -11,3 +14,5 @@ export default { }, }, }; + +export default SkeletonConfig; diff --git a/src/components/atoms/skeleton/skeleton.tsx b/src/components/atoms/skeleton/skeleton.tsx index a895a3d2..f642782f 100644 --- a/src/components/atoms/skeleton/skeleton.tsx +++ b/src/components/atoms/skeleton/skeleton.tsx @@ -34,57 +34,55 @@ type BaseSkeletonProps = BoxProps & { fadeDuration?: number; }; -const BaseSkeleton = React.forwardRef( - ( - { - startColor, - endColor, - isLoaded = false, - speed = 800, - fadeDuration = 200, - ...rest - }: BaseSkeletonProps, - ref: any - ) => { - const [key, setKey] = React.useState(Math.random()); +const BaseSkeleton = React.memo( + React.forwardRef( + ( + { + startColor, + endColor, + isLoaded = false, + speed = 800, + fadeDuration = 200, + ...rest + }: BaseSkeletonProps, + ref: any + ) => { + const key = React.useMemo(() => Math.random(), [isLoaded]); - React.useEffect(() => { - setKey(Math.random()); - }, [isLoaded]); + if (isLoaded) { + return ( + + {rest.children} + + ); + } + + rest.children = {rest.children}; - if (isLoaded) { return ( - {rest.children} - + /> ); } - - rest.children = {rest.children}; - - return ( - - ); - } + ) ); /** diff --git a/src/components/atoms/slide-fade/slide-fade.tsx b/src/components/atoms/slide-fade/slide-fade.tsx index 487728bf..17059dd2 100644 --- a/src/components/atoms/slide-fade/slide-fade.tsx +++ b/src/components/atoms/slide-fade/slide-fade.tsx @@ -1,10 +1,6 @@ import React from "react"; -import { ViewStyle } from "react-native"; import Box, { BoxProps } from "../box/box"; import { AnimatePresence } from "moti"; -import { MotiWithPearlStyleProps } from "../../../theme/src/types"; -import { BoxStyleProps } from "../../../theme/src/style-functions"; -import { Easing } from "react-native-reanimated"; type SlideFadeProps = BoxProps & { /** Whether to show the component */ @@ -26,71 +22,80 @@ type SlideFadeProps = BoxProps & { /** * SlideFade is a component that provides an view with a sliding fade transition. */ -const SlideFade = React.forwardRef( - ( - { - children, - show, - offsetX = 0, - offsetY = 10, - transition = {}, - exitTransition = {}, - ...rest - }: SlideFadeProps, - ref: any - ) => { - return ( - - {show && ( - ["transition"] - } - exitTransition={ - { - dampingRatio: 1, - duration: 100, - easing: Easing.inOut, - ...exitTransition, - type: "spring", - } as MotiWithPearlStyleProps< - ViewStyle, - BoxStyleProps - >["exitTransition"] - } - > - {children} - - )} - - ); - } +const SlideFade = React.memo( + React.forwardRef( + ( + { + children, + show, + offsetX = 0, + offsetY = 10, + transition = {}, + exitTransition = {}, + ...rest + }: SlideFadeProps, + ref: any + ) => { + const from = React.useMemo( + () => ({ + opacity: 0, + ...(offsetX !== 0 && { translateX: offsetX }), + ...(offsetY !== 0 && { translateY: offsetY }), + }), + [offsetX, offsetY] + ); + + const animate = React.useMemo( + () => ({ + opacity: 1, + translateX: 0, + translateY: 0, + }), + [] + ); + + const exit = React.useMemo( + () => ({ + opacity: 0, + ...(offsetX !== 0 && { translateX: offsetX }), + ...(offsetY !== 0 && { translateY: offsetY }), + }), + [offsetX, offsetY] + ); + + return ( + + {show && ( + + {children} + + )} + + ); + } + ) ); SlideFade.displayName = "SlideFade"; diff --git a/src/components/atoms/slide/slide.tsx b/src/components/atoms/slide/slide.tsx index 54e851a1..1210ad7a 100644 --- a/src/components/atoms/slide/slide.tsx +++ b/src/components/atoms/slide/slide.tsx @@ -1,10 +1,6 @@ import React from "react"; -import { ViewStyle } from "react-native"; import Box, { BoxProps } from "../box/box"; import { AnimatePresence } from "moti"; -import { MotiWithPearlStyleProps } from "../../../theme/src/types"; -import { BoxStyleProps } from "../../../theme/src/style-functions"; -import { Easing } from "react-native-reanimated"; type SlideProps = BoxProps & { /** Whether to show the component */ @@ -20,86 +16,94 @@ type SlideProps = BoxProps & { /** * Slide is a component that provides an view with a sliding transition. */ -const Slide = React.forwardRef( - ( - { - children, - show, - direction = "right", - transition = {}, - exitTransition = {}, - ...rest - }: SlideProps, - ref: any - ) => { - const getAnimationProps = () => { - let from = {}; - let animate = {}; - let exit = {}; - switch (direction) { - case "bottom": - from = { translateY: -10000 }; - animate = { translateY: 0 }; - exit = { translateY: -10000 }; - break; - case "top": - from = { translateY: 10000 }; - animate = { translateY: 0 }; - exit = { translateY: 10000 }; - break; - case "right": - from = { translateX: -10000 }; - animate = { translateX: 0 }; - exit = { translateX: -10000 }; - break; - case "left": - from = { translateX: 10000 }; - animate = { translateX: 0 }; - exit = { translateX: 10000 }; - break; - } +const Slide = React.memo( + React.forwardRef( + ( + { + children, + show, + direction = "right", + transition = {}, + exitTransition = {}, + ...rest + }: SlideProps, + ref: any + ) => { + const from = React.useMemo(() => { + switch (direction) { + case "bottom": + return { translateY: -10000 }; + case "top": + return { translateY: 10000 }; + case "right": + return { translateX: -10000 }; + case "left": + return { translateX: 10000 }; + default: + return {}; + } + }, [direction]); - return { from, animate, exit }; - }; + const animate = React.useMemo(() => { + switch (direction) { + case "bottom": + case "top": + case "right": + case "left": + return { translateX: 0, translateY: 0 }; + default: + return {}; + } + }, [direction]); - return ( - - {show && ( - ["transition"] - } - exitTransition={ - { - dampingRatio: 1, - duration: 200, - easing: Easing.inOut, - ...exitTransition, - type: "spring", - } as MotiWithPearlStyleProps< - ViewStyle, - BoxStyleProps - >["exitTransition"] - } - > - {children} - - )} - - ); - } + const exit = React.useMemo(() => { + switch (direction) { + case "bottom": + return { translateY: -10000 }; + case "top": + return { translateY: 10000 }; + case "right": + return { translateX: -10000 }; + case "left": + return { translateX: 10000 }; + default: + return {}; + } + }, [direction]); + + return ( + + {show && ( + + {children} + + )} + + ); + } + ) ); Slide.displayName = "Slide"; diff --git a/src/components/molecules/avatar/avatar-group.tsx b/src/components/molecules/avatar/avatar-group.tsx index 754aa598..dd107ddd 100644 --- a/src/components/molecules/avatar/avatar-group.tsx +++ b/src/components/molecules/avatar/avatar-group.tsx @@ -43,77 +43,76 @@ export type AvatarGroupProps = BoxProps & { /** * AvatarGroup is a component that groups multiple Avatar components together. It can truncate the avatars and show a "+X" label (where X is the remaining avatars). */ -const AvatarGroup: React.FC = ({ - children, - spacing = "2", - max = 3, - ...rest -}) => { - // Convert the spacing to a style object - const convertedElementSpacing = useStyleProps({ marginLeft: spacing }, [ - ...spacingStyleFunction, - ]); +const AvatarGroup = React.memo( + React.forwardRef( + ({ children, spacing = "2", max = 3, ...rest }, ref) => { + // Convert the spacing to a style object + const convertedElementSpacing = useStyleProps({ marginLeft: spacing }, [ + ...spacingStyleFunction, + ]); - // Convert the children to an array - const avatarChildren = React.Children.toArray(children); + // Convert the children to an array + const avatarChildren = React.Children.toArray(children); - /** - * Render the avatars. - * @returns An array of Avatar components. - */ - const renderAvatars = () => { - return React.Children.map(avatarChildren, (child, index) => { - const shouldBreakLoop = index > max; - const shouldRenderAvatar = index + 1 <= max; + /** + * Render the avatars. + * @returns An array of Avatar components. + */ + const renderAvatars = () => { + return React.Children.map(avatarChildren, (child, index) => { + const shouldBreakLoop = index > max; + const shouldRenderAvatar = index + 1 <= max; + + if (shouldBreakLoop) return; - if (shouldBreakLoop) return; + if (shouldRenderAvatar) + return React.cloneElement(child as ReactElement, { + ...(child as ReactElement).props, + style: { + ...(child as ReactElement).props.style, + marginLeft: index * convertedElementSpacing.style.marginLeft, + }, + }); + else { + if (rest.customTruncatedComponent) + return React.cloneElement(rest.customTruncatedComponent, { + ...rest.customTruncatedComponent.props, + remainingAvatars: avatarChildren.length - max, + style: { + ...rest.customTruncatedComponent.props.style, + marginLeft: index * convertedElementSpacing.style.marginLeft, + }, + }); - if (shouldRenderAvatar) - return React.cloneElement(child as ReactElement, { - ...(child as ReactElement).props, - style: { - ...(child as ReactElement).props.style, - marginLeft: index * convertedElementSpacing.style.marginLeft, - }, + return ( + name} + backgroundColor={rest.truncatedBackgroundColor} + style={{ + marginLeft: index * convertedElementSpacing.style.marginLeft, + }} + /> + ); + } }); - else { - if (rest.customTruncatedComponent) - return React.cloneElement(rest.customTruncatedComponent, { - ...rest.customTruncatedComponent.props, - remainingAvatars: avatarChildren.length - max, - style: { - ...rest.customTruncatedComponent.props.style, - marginLeft: index * convertedElementSpacing.style.marginLeft, - }, - }); + }; - return ( - name} - backgroundColor={rest.truncatedBackgroundColor} - style={{ - marginLeft: index * convertedElementSpacing.style.marginLeft, + return ( + + - ); - } - }); - }; - - return ( - - - {renderAvatars()} - - - ); -}; + > + {renderAvatars()} + + + ); + } + ) +); AvatarGroup.displayName = "AvatarGroup"; diff --git a/src/components/molecules/button/button-group.tsx b/src/components/molecules/button/button-group.tsx index 54007a25..1cb1f5fd 100644 --- a/src/components/molecules/button/button-group.tsx +++ b/src/components/molecules/button/button-group.tsx @@ -72,58 +72,66 @@ export type ButtonGroupProps = BoxProps & { /** * ButtonGroup is a layout component that makes it easy to stack buttons together and apply a space between them. */ -const ButtonGroup: React.FC = ({ - children, - isDisabled = false, - isAttached = false, - spacing = "2", - size = "m", - variant = "filled", - colorScheme = "primary", - ...rest -}) => { - const arrayChildren = React.Children.toArray(children); +const ButtonGroup = React.memo( + React.forwardRef( + ( + { + children, + isDisabled = false, + isAttached = false, + spacing = "2", + size = "m", + variant = "filled", + colorScheme = "primary", + ...rest + }, + ref + ) => { + const arrayChildren = React.Children.toArray(children); - /** - * Renders the children of the ButtonGroup. - * - * @returns The rendered children. - */ - const renderChildren = () => { - return React.Children.map(arrayChildren, (child, index) => { - const isFirst = index === 0; - const isLast = index === arrayChildren.length - 1; + /** + * Renders the children of the ButtonGroup. + * + * @returns The rendered children. + */ + const renderChildren = React.useCallback(() => { + return arrayChildren.map((child, index) => { + const isFirst = index === 0; + const isLast = index === arrayChildren.length - 1; - return React.cloneElement(child as ReactElement, { - borderRadius: isAttached && !isFirst && !isLast ? 0 : undefined, - borderTopRightRadius: isAttached && isFirst ? 0 : undefined, - borderBottomRightRadius: isAttached && isFirst ? 0 : undefined, - borderTopLeftRadius: isAttached && isLast ? 0 : undefined, - borderBottomLeftRadius: isAttached && isLast ? 0 : undefined, - borderRightWidth: isAttached && !isLast ? 0 : undefined, - }); - }); - }; + return React.cloneElement(child as ReactElement, { + borderRadius: isAttached && !isFirst && !isLast ? 0 : undefined, + borderTopRightRadius: isAttached && isFirst ? 0 : undefined, + borderBottomRightRadius: isAttached && isFirst ? 0 : undefined, + borderTopLeftRadius: isAttached && isLast ? 0 : undefined, + borderBottomLeftRadius: isAttached && isLast ? 0 : undefined, + borderRightWidth: isAttached && !isLast ? 0 : undefined, + }); + }); + }, [arrayChildren, isAttached]); - return ( - - - {renderChildren()} - - - ); -}; + return ( + + + {renderChildren()} + + + ); + } + ) +); ButtonGroup.displayName = "ButtonGroup"; diff --git a/src/components/molecules/checkbox/checkbox-group.tsx b/src/components/molecules/checkbox/checkbox-group.tsx index 9136d9f5..03bb32e8 100644 --- a/src/components/molecules/checkbox/checkbox-group.tsx +++ b/src/components/molecules/checkbox/checkbox-group.tsx @@ -78,48 +78,55 @@ export type CheckBoxGroupProps = BoxProps & { /** * CheckBoxGroup is a component that groups together multiple CheckBox components. */ -const CheckBoxGroup: React.FC = ({ - children, - isDisabled = false, - shape = "square", - value = undefined, - defaultValue = [], - spacing = "2", - size = "m", - variant = "filled", - colorScheme = "primary", - onChange = () => {}, - ...rest -}) => { - const currentValue = value ?? defaultValue ?? []; +const CheckBoxGroup = React.memo( + React.forwardRef( + ( + { + children, + isDisabled = false, + shape = "square", + value = undefined, + defaultValue = [], + spacing = "2", + size = "m", + variant = "filled", + colorScheme = "primary", + onChange = () => {}, + ...rest + }, + ref + ) => { + const currentValue = value ?? defaultValue ?? []; - const addCheckBoxGroupValue = (valueToAdd: string | number) => { - onChange([...currentValue, valueToAdd]); - }; + const addCheckBoxGroupValue = (valueToAdd: string | number) => { + onChange([...currentValue, valueToAdd]); + }; - const deleteCheckBoxGroupValue = (valueToDelete: string | number) => { - onChange(currentValue.filter((value) => value !== valueToDelete)); - }; + const deleteCheckBoxGroupValue = (valueToDelete: string | number) => { + onChange(currentValue.filter((value) => value !== valueToDelete)); + }; - return ( - - - {children} - - - ); -}; + return ( + + + {children} + + + ); + } + ) +); CheckBoxGroup.displayName = "CheckBoxGroup"; diff --git a/src/components/molecules/icon-button/icon-button.config.ts b/src/components/molecules/icon-button/icon-button.config.ts index ea761f57..6ef978ad 100644 --- a/src/components/molecules/icon-button/icon-button.config.ts +++ b/src/components/molecules/icon-button/icon-button.config.ts @@ -1,4 +1,15 @@ -export default { +import { MolecularComponentConfig } from "../../../theme/src/types"; +import { IconProps } from "../../atoms/icon/icon"; +import { SpinnerProps } from "../../atoms/spinner/spinner"; +import { IconButtonProps } from "./icon-button"; + +export type IconButtonAtoms = { + box: IconButtonProps; + spinner: SpinnerProps; + icon: IconProps; +}; + +const IconButtonConfig: MolecularComponentConfig = { parts: ["box", "spinner", "icon"], baseStyle: { box: { @@ -70,9 +81,7 @@ export default { variants: { filled: { box: { - animate: { - backgroundColor: "primary.500", - }, + backgroundColor: "primary.500", _pressed: { bgColor: "primary.400", }, @@ -86,14 +95,12 @@ export default { }, outline: { box: { - animate: { - backgroundColor: { - light: "neutral.50", - dark: "neutral.800", - }, - borderWidth: 1, - borderColor: "primary.500", + backgroundColor: { + light: "neutral.50", + dark: "neutral.800", }, + borderWidth: 1, + borderColor: "primary.500", _pressed: { bgColor: "primary.50", }, @@ -107,14 +114,15 @@ export default { }, ghost: { box: { - animate: { - backgroundColor: { - light: "neutral.50", - dark: "neutral.800", - }, + backgroundColor: { + light: "neutral.50", + dark: "neutral.800", }, _pressed: { - bgColor: "primary.50", + bgColor: { + light: "primary.50", + dark: "neutral.900", + }, }, }, spinner: { @@ -130,3 +138,5 @@ export default { variant: "filled", }, }; + +export default IconButtonConfig; diff --git a/src/components/molecules/icon-button/icon-button.tsx b/src/components/molecules/icon-button/icon-button.tsx index 0e4cf93b..aa6065ef 100644 --- a/src/components/molecules/icon-button/icon-button.tsx +++ b/src/components/molecules/icon-button/icon-button.tsx @@ -5,6 +5,7 @@ import { MoleculeComponentProps } from "../../../theme/src/types"; import { useMolecularComponentConfig } from "../../../hooks/useMolecularComponentConfig"; import { boxStyleFunctions } from "../../../theme/src/style-functions"; import { useButtonGroup } from "../button/button-group"; +import { IconButtonAtoms } from "./icon-button.config"; export type IconButtonProps = PressableProps & { /** @@ -24,63 +25,68 @@ export type IconButtonProps = PressableProps & { }; /** IconButton is a specialized Button which renders an icon within. It can be used to trigger an action or event, such as submitting a form, opening a dialog, canceling an action, or performing a delete operation */ -const IconButton = React.forwardRef( - ( - { - icon, - isLoading = false, - isFullWidth = false, - ...rest - }: Omit, "atoms"> & { - atoms?: Record; - }, - ref: any - ) => { - const { size, variant, isDisabled, colorScheme } = useButtonGroup(); - - // Overwrite props from checkbox group - rest.size = rest.size ?? size; - rest.variant = rest.variant ?? variant; - rest.isDisabled = rest.isDisabled ?? isDisabled; - rest.colorScheme = rest.colorScheme ?? colorScheme; - - const molecularProps = useMolecularComponentConfig( - "IconButton", - rest, +const IconButton = React.memo( + React.forwardRef( + ( { - size: rest.size, - variant: rest.variant, + icon, + isLoading = false, + isFullWidth = false, + ...rest + }: Omit< + MoleculeComponentProps<"IconButton", IconButtonProps, IconButtonAtoms>, + "atoms" + > & { + atoms?: IconButtonAtoms; }, - rest.colorScheme, - boxStyleFunctions, - "box", - "box", - "box" - ); - const { atoms } = molecularProps; + ref: any + ) => { + const { size, variant, isDisabled, colorScheme } = useButtonGroup(); + + // Overwrite props from checkbox group + rest.size = rest.size ?? size; + rest.variant = rest.variant ?? variant; + rest.isDisabled = rest.isDisabled ?? isDisabled; + rest.colorScheme = rest.colorScheme ?? colorScheme; + + const molecularProps = useMolecularComponentConfig( + "IconButton", + rest, + { + size: rest.size, + variant: rest.variant, + }, + rest.colorScheme, + boxStyleFunctions, + "box", + "box", + "box" + ); + const { atoms } = molecularProps; - // Determine if the button is disabled - const isButtonDisabled = rest.isDisabled ? true : isLoading; + // Determine if the button is disabled + const isButtonDisabled = rest.isDisabled ? true : isLoading; - return ( - - {isLoading ? ( - - ) : ( - React.cloneElement(icon, atoms.icon) - )} - - ); - } + return ( + + {isLoading ? ( + + ) : ( + React.cloneElement(icon, atoms.icon) + )} + + ); + } + ) ); IconButton.displayName = "IconButton"; diff --git a/src/components/molecules/image/cache-manager.ts b/src/components/molecules/image/cache-manager.ts index 582f767e..c05c3a70 100644 --- a/src/components/molecules/image/cache-manager.ts +++ b/src/components/molecules/image/cache-manager.ts @@ -11,12 +11,15 @@ const BASE_DIR = `${FileSystem.cacheDirectory}`; export class CacheEntry { uri: string; - options: DownloadOptions; + path: string | undefined; + isPathResolved: boolean; constructor(uri: string, options: DownloadOptions) { this.uri = uri; this.options = options; + this.path = undefined; + this.isPathResolved = false; } /** @@ -25,26 +28,28 @@ export class CacheEntry { * @returns {Promise} The path of the cached image, or undefined if the download failed. */ async getPath(): Promise { - // Get the URI and options of the cache entry - const { uri, options } = this; - // Get the paths of the cached image and its temporary download location - const { path, exists, tmpPath } = await getCacheEntry(uri); - // If the image is already cached, return its path - if (exists) { - return path; - } - // Download the image and cache it - const result = await FileSystem.createDownloadResumable( - uri, - tmpPath, - options - ).downloadAsync(); - // If the image download failed, we don't cache anything - if (result && result.status !== 200) { - return undefined; + if (!this.isPathResolved) { + const { uri, options } = this; + const { path, exists, tmpPath } = await getCacheEntry(uri); + + if (exists) { + this.path = path; + } else { + const result = await FileSystem.createDownloadResumable( + uri, + tmpPath, + options + ).downloadAsync(); + + if (result && result.status === 200) { + await FileSystem.moveAsync({ from: tmpPath, to: path }); + this.path = path; + } + } + + this.isPathResolved = true; } - await FileSystem.moveAsync({ from: tmpPath, to: path }); - return path; + return this.path; } } @@ -52,7 +57,7 @@ export class CacheEntry { * CacheManager is a class that manages the caching of images. */ export default class CacheManager { - static entries: { [uri: string]: CacheEntry } = {}; + static entries: Map = new Map(); /** * Returns a CacheEntry object for the given uri and options. @@ -62,10 +67,10 @@ export default class CacheManager { * @returns {CacheEntry} The CacheEntry object for the given uri and options. */ static get(uri: string, options: DownloadOptions): CacheEntry { - if (!CacheManager.entries[uri]) { - CacheManager.entries[uri] = new CacheEntry(uri, options); + if (!CacheManager.entries.has(uri)) { + CacheManager.entries.set(uri, new CacheEntry(uri, options)); } - return CacheManager.entries[uri]; + return CacheManager.entries.get(uri) as CacheEntry; } /** @@ -75,6 +80,7 @@ export default class CacheManager { static async clearCache(): Promise { await FileSystem.deleteAsync(BASE_DIR, { idempotent: true }); await FileSystem.makeDirectoryAsync(BASE_DIR); + CacheManager.entries.clear(); } /** diff --git a/src/components/molecules/progress/progress.config.ts b/src/components/molecules/progress/progress.config.ts index ac26e3ea..276b8935 100644 --- a/src/components/molecules/progress/progress.config.ts +++ b/src/components/molecules/progress/progress.config.ts @@ -1,6 +1,13 @@ -import { Easing } from "react-native-reanimated"; +import { MolecularComponentConfig } from "../../../theme/src/types"; +import { BoxProps } from "../../atoms/box/box"; +import { BaseProgressProps } from "./progress"; -export default { +export type ProgressAtoms = { + container: BaseProgressProps; + bar: BoxProps; +}; + +const ProgressConfig: MolecularComponentConfig = { parts: ["container", "bar"], baseStyle: { container: { @@ -14,13 +21,12 @@ export default { type: "spring", dampingRatio: 1, duration: 200, - easing: Easing.inOut, }, }, }, sizes: { xs: { - container: (variant: string) => { + container: (variant) => { const baseStyle = { height: 10, borderRadius: "s", @@ -40,7 +46,7 @@ export default { }, }, s: { - container: (variant: string) => { + container: (variant) => { const baseStyle = { height: 15, borderRadius: "m", @@ -60,7 +66,7 @@ export default { }, }, m: { - container: (variant: string) => { + container: (variant) => { const baseStyle = { height: 20, borderRadius: "m", @@ -80,7 +86,7 @@ export default { }, }, l: { - container: (variant: string) => { + container: (variant) => { const baseStyle = { height: 25, borderRadius: "m", @@ -125,3 +131,5 @@ export default { variant: "filled", }, }; + +export default ProgressConfig; diff --git a/src/components/molecules/progress/progress.tsx b/src/components/molecules/progress/progress.tsx index 43cf4a5d..32e1c184 100644 --- a/src/components/molecules/progress/progress.tsx +++ b/src/components/molecules/progress/progress.tsx @@ -2,6 +2,8 @@ import React, { useMemo } from "react"; import Box, { BoxProps } from "../../atoms/box/box"; import { MoleculeComponentProps } from "../../../theme/src/types"; import { pearl } from "../../../pearl"; +import { ProgressAtoms } from "./progress.config"; +import { DimensionValue } from "react-native"; export type BaseProgressProps = BoxProps & { /** @@ -12,38 +14,39 @@ export type BaseProgressProps = BoxProps & { value?: number; }; -const BaseProgress = React.forwardRef( - ( - { - children, - atoms, - ...rest - }: MoleculeComponentProps<"Progress", BaseProgressProps>, - ref: any - ) => { - const { value, ...otherContainerProps } = atoms.container; - const calculatedWidth = useMemo(() => `${value}%`, [value]); +const BaseProgress = React.memo( + React.forwardRef( + ( + { + atoms, + }: MoleculeComponentProps<"Progress", BaseProgressProps, ProgressAtoms>, + ref: any + ) => { + const { value, ...otherContainerProps } = atoms.container; + const calculatedWidth = useMemo( + () => `${value || 0}%`, + [value] + ); - return ( - + return ( - - ); - } + {...otherContainerProps} + ref={ref} + accessible={true} + accessibilityRole="progressbar" + accessibilityLabel={otherContainerProps.accessibilityLabel} + > + + + ); + } + ) ); /** The Progress component is a visual indicator of completion percentage. */ diff --git a/src/components/molecules/radio/radio-group.tsx b/src/components/molecules/radio/radio-group.tsx index c551118b..576fc4e7 100644 --- a/src/components/molecules/radio/radio-group.tsx +++ b/src/components/molecules/radio/radio-group.tsx @@ -68,45 +68,53 @@ export type RadioGroupProps = BoxProps & { /** * RadioGroup is a component that groups together multiple Radio components. */ -const RadioGroup: React.FC = ({ - children, - isDisabled = false, - value = undefined, - defaultValue = undefined, - spacing = "2", - onChange = () => {}, - size = "m", - variant = "filled", - colorScheme = "primary", - ...rest -}) => { - const currentValue = value ?? defaultValue; - const setRadioGroupValue = (value: string | number) => { - onChange(value); - }; +const RadioGroup = React.memo( + React.forwardRef( + ( + { + children, + isDisabled = false, + value = undefined, + defaultValue = undefined, + spacing = "2", + onChange = () => {}, + size = "m", + variant = "filled", + colorScheme = "primary", + ...rest + }, + ref + ) => { + const currentValue = value ?? defaultValue; + const setRadioGroupValue = (value: string | number) => { + onChange(value); + }; - return ( - - - {children} - - - ); -}; + return ( + + + {children} + + + ); + } + ) +); RadioGroup.displayName = "RadioGroup"; diff --git a/src/components/molecules/switch/switch.config.ts b/src/components/molecules/switch/switch.config.ts index 6c496456..df54514d 100644 --- a/src/components/molecules/switch/switch.config.ts +++ b/src/components/molecules/switch/switch.config.ts @@ -1,6 +1,13 @@ -import { Easing } from "react-native-reanimated"; +import { BaseSwitchProps } from "./switch"; +import { BoxProps } from "../../atoms/box/box"; +import { MolecularComponentConfig, StateProps } from "../../../theme/src/types"; -export default { +export type SwitchAtoms = { + track: BaseSwitchProps; + knob: BoxProps & StateProps<"_checked">; +}; + +const SwitchConfig: MolecularComponentConfig = { parts: ["track", "knob"], baseStyle: { track: { @@ -8,12 +15,10 @@ export default { p: "0.5", borderRadius: "full", bgColor: "neutral.300", - animate: {}, transition: { type: "spring", dampingRatio: 1, duration: 50, - easing: Easing.inOut, }, _checked: { bgColor: "primary.500", @@ -34,7 +39,6 @@ export default { type: "spring", dampingRatio: 1, duration: 50, - easing: Easing.inOut, }, }, }, @@ -88,3 +92,5 @@ export default { size: "m", }, }; + +export default SwitchConfig; diff --git a/src/components/molecules/switch/switch.tsx b/src/components/molecules/switch/switch.tsx index dc0995e7..4d7484ef 100644 --- a/src/components/molecules/switch/switch.tsx +++ b/src/components/molecules/switch/switch.tsx @@ -1,31 +1,14 @@ import React from "react"; import Box from "../../atoms/box/box"; -import { - ComponentSizes, - ComponentVariants, - MoleculeComponentProps, - ResponsiveValue, - StateProps, -} from "../../../theme/src/types"; +import { MoleculeComponentProps, StateProps } from "../../../theme/src/types"; import { pearl } from "../../../pearl"; import Pressable, { PressableProps } from "../../atoms/pressable/pressable"; import { useCheckedState } from "../../../hooks"; import { boxStyleFunctions } from "../../../theme/src/style-functions"; +import { SwitchAtoms } from "./switch.config"; export type BaseSwitchProps = PressableProps & StateProps<"_checked" | "_disabled"> & { - /** - * Size of the switch. - * - * @default "m" - */ - size?: ResponsiveValue>; - /** - * Variant of the switch. - * - * @default "filled" - */ - variant?: ResponsiveValue>; /** * Whether the switch is in a checked state. * @@ -40,55 +23,55 @@ export type BaseSwitchProps = PressableProps & isDisabled?: boolean; }; -const BaseSwitch = React.forwardRef( - ( - { - children, - atoms, - ...rest - }: MoleculeComponentProps<"Switch", BaseSwitchProps>, - ref: any - ) => { - let { isChecked, isDisabled, ...otherTrackProps } = atoms.track; - const { propsWithCheckedStyles: propsWithCheckedStylesForTrack } = - useCheckedState( - otherTrackProps, - boxStyleFunctions, - "molecule", - true, - isChecked - ); - otherTrackProps = propsWithCheckedStylesForTrack; - const { propsWithCheckedStyles: propsWithCheckedStylesForKnob } = - useCheckedState( - atoms.knob, - boxStyleFunctions, - "molecule", - true, - isChecked - ); - atoms.knob = propsWithCheckedStylesForKnob; +const BaseSwitch = React.memo( + React.forwardRef( + ( + { + children, + atoms, + ...rest + }: MoleculeComponentProps<"Switch", BaseSwitchProps, SwitchAtoms>, + ref: any + ) => { + let { isChecked, isDisabled, ...otherTrackProps } = atoms.track; + const { propsWithCheckedStyles: propsWithCheckedStylesForTrack } = + useCheckedState( + otherTrackProps, + boxStyleFunctions, + "molecule", + true, + isChecked + ); + otherTrackProps = propsWithCheckedStylesForTrack; + const { propsWithCheckedStyles: propsWithCheckedStylesForKnob } = + useCheckedState( + atoms.knob, + boxStyleFunctions, + "molecule", + true, + isChecked + ); + atoms.knob = propsWithCheckedStylesForKnob; - return ( - - - - ); - } + return ( + + + + ); + } + ) ); /** The Progress component is a visual indicator of completion percentage. */ diff --git a/src/components/molecules/text-link/text-link.config.ts b/src/components/molecules/text-link/text-link.config.ts index 9b4dfd0b..094f0429 100644 --- a/src/components/molecules/text-link/text-link.config.ts +++ b/src/components/molecules/text-link/text-link.config.ts @@ -1,11 +1,22 @@ -export default { +import { MolecularComponentConfig } from "../../../theme/src/types"; +import { PressableProps } from "../../atoms/pressable/pressable"; +import { TextProps } from "../../atoms/text/text"; + +export type TextLinkAtoms = { + container: PressableProps; + text: TextProps; +}; + +const TextLinkConfig: MolecularComponentConfig = { parts: ["container", "text"], baseStyle: { container: { - activeOpacity: 0.8, + _pressed: { + bgColor: "primary.400", + }, }, text: { - fontWeight: 500, + fontWeight: "500", color: "primary.500", }, }, @@ -35,3 +46,5 @@ export default { size: "m", }, }; + +export default TextLinkConfig; diff --git a/src/components/molecules/text-link/text-link.tsx b/src/components/molecules/text-link/text-link.tsx index e9e17b31..b538182f 100644 --- a/src/components/molecules/text-link/text-link.tsx +++ b/src/components/molecules/text-link/text-link.tsx @@ -3,38 +3,41 @@ import Text from "../../atoms/text/text"; import Pressable, { PressableProps } from "../../atoms/pressable/pressable"; import { MoleculeComponentProps } from "../../../theme/src/types"; import { pearl } from "../../../pearl"; +import { TextLinkAtoms } from "./text-link.config"; export type BaseTextLinkProps = PressableProps & { children?: string; }; -const CustomTextLink = React.forwardRef( - ( - { - children, - atoms, - ...rest - }: MoleculeComponentProps<"TextLink", BaseTextLinkProps>, - ref: any - ) => { - const isDisabled = atoms.container.isDisabled ?? false; +const CustomTextLink = React.memo( + React.forwardRef( + ( + { + children, + atoms, + ...rest + }: MoleculeComponentProps<"TextLink", BaseTextLinkProps, TextLinkAtoms>, + ref: any + ) => { + const isDisabled = atoms.container.isDisabled ?? false; - return ( - - {children} - - ); - } + return ( + + {children} + + ); + } + ) ); /** TextLink wraps a Text component with a Pressable component that can be used to trigger an action or event, such as submitting a form, opening a dialog, canceling an action, or performing a delete operation */ diff --git a/src/components/molecules/textarea/textarea.tsx b/src/components/molecules/textarea/textarea.tsx index 93b4c26e..f9146803 100644 --- a/src/components/molecules/textarea/textarea.tsx +++ b/src/components/molecules/textarea/textarea.tsx @@ -16,8 +16,8 @@ export type TextareaProps = Omit< }; /** The Textarea component is a component that is used to add multiline support for text inputs. **/ -const Textarea = React.forwardRef( - ({ rows = 4, ...rest }: TextareaProps, textareaRef: any) => { +const Textarea = React.memo( + React.forwardRef(({ rows = 4, ...rest }: TextareaProps, textareaRef: any) => { return ( ); - } + }) ); Textarea.displayName = "Textarea"; diff --git a/src/components/molecules/video/video.tsx b/src/components/molecules/video/video.tsx index f3ebd179..888fafae 100644 --- a/src/components/molecules/video/video.tsx +++ b/src/components/molecules/video/video.tsx @@ -1,4 +1,10 @@ -import React, { useEffect, useRef, useState } from "react"; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { Video as ExpoVideo, VideoProps as ExpoVideoProps } from "expo-av"; import { pearl } from "../../../pearl"; import { @@ -79,180 +85,234 @@ export type BaseVideoProps = BoxProps & fallbackSource?: ImageSourcePropType; }; -const BaseVideo = React.forwardRef( - ({ atoms }: MoleculeComponentProps<"Video", BaseVideoProps>, ref: any) => { - const { - source, - onError, - testID, - previewSource, - fallbackSource, - fallbackComponent, - previewColor, - tint = "dark", - loaderType = "spinner", - overlayTransitionDuration = 300, - onLoad, - onLoadStart, - ...restVideoProps - } = atoms.video; +const BaseVideo = React.memo( + React.forwardRef( + ({ atoms }: MoleculeComponentProps<"Video", BaseVideoProps>, ref: any) => { + const { + source, + onError, + testID, + previewSource, + fallbackSource, + fallbackComponent, + previewColor, + tint = "dark", + loaderType = "spinner", + overlayTransitionDuration = 300, + onLoad, + onLoadStart, + ...restVideoProps + } = atoms.video; - const isRemoteVideo = typeof source === "object"; - const [error, setError] = useState(false); - const [hasVideoLoaded, setHasVideoLoaded] = useState(false); - const intensity = useRef(new Animated.Value(100)).current; + const isRemoteVideo = typeof source === "object"; + const [error, setError] = useState(false); + const [hasVideoLoaded, setHasVideoLoaded] = useState(false); + const intensity = useRef(new Animated.Value(100)).current; - const previewSourceOverlayOpacity = intensity.interpolate({ - inputRange: [0, 100], - outputRange: [0, 1], - }); - const blurIntensity = intensity.interpolate({ - inputRange: [0, 100], - outputRange: [0, 50], - }); - const previewColorOverlayOpacity = intensity.interpolate({ - inputRange: [0, 100], - outputRange: [0, 1], - }); + const previewSourceOverlayOpacity = intensity.interpolate({ + inputRange: [0, 100], + outputRange: [0, 1], + }); + const blurIntensity = intensity.interpolate({ + inputRange: [0, 100], + outputRange: [0, 50], + }); + const previewColorOverlayOpacity = intensity.interpolate({ + inputRange: [0, 100], + outputRange: [0, 1], + }); - // Separate out border radius properties - const { - borderRadius, - borderBottomLeftRadius, - borderBottomRightRadius, - borderTopLeftRadius, - borderTopRightRadius, - ...finalContainerProps - } = atoms.container; - const borderRadiiStyles = { - borderRadius, - borderBottomLeftRadius, - borderBottomRightRadius, - borderTopLeftRadius, - borderTopRightRadius, - }; + // Separate out border radius properties + const { + borderRadius, + borderBottomLeftRadius, + borderBottomRightRadius, + borderTopLeftRadius, + borderTopRightRadius, + ...finalContainerProps + } = atoms.container; + const borderRadiiStyles = useMemo( + () => ({ + borderRadius, + borderBottomLeftRadius, + borderBottomRightRadius, + borderTopLeftRadius, + borderTopRightRadius, + }), + [ + borderRadius, + borderBottomLeftRadius, + borderBottomRightRadius, + borderTopLeftRadius, + borderTopRightRadius, + ] + ); - // A handler function for catching errors while loading the video - const onErrorHandler = (error: string) => { - setError(true); - if (onError) onError(error); - }; + // A handler function for catching errors while loading the video + const onErrorHandler = (error: string) => { + setError(true); + if (onError) onError(error); + }; - const renderFallback = () => { - if (error) { - if (!!fallbackComponent) { - return ( - - {React.cloneElement(fallbackComponent)} - - ); + const renderFallback = useCallback(() => { + if (error) { + if (!!fallbackComponent) { + return ( + + {React.cloneElement(fallbackComponent)} + + ); + } + + if (!!fallbackSource) { + return ( + + ); + } } - if (!!fallbackSource) { + return null; + }, [ + error, + fallbackComponent, + fallbackSource, + borderRadiiStyles, + atoms.fallbackImage, + ]); + + const renderVideo = useCallback(() => { + return ( + { + if (onLoad) onLoad(status); + + if (isRemoteVideo && status.isLoaded) { + // Start animating the overlay opacity as soon as the URI becomes available + Animated.timing(intensity, { + toValue: 0, + duration: overlayTransitionDuration, + useNativeDriver: Platform.OS === "android", + }).start(); + } + setHasVideoLoaded(true); + }} + onLoadStart={() => { + if (Platform.OS === "ios") { + setTimeout(() => { + if (!hasVideoLoaded) { + setError(true); + } + }, 1000); + } + if (onLoadStart) onLoadStart(); + }} + onError={onErrorHandler} + testID={testID} + source={source} + zIndex={3} + width="100%" + height="100%" + style={StyleSheet.absoluteFill} + /> + ); + }, [ + restVideoProps, + borderRadiiStyles, + ref, + hasVideoLoaded, + onLoad, + isRemoteVideo, + intensity, + overlayTransitionDuration, + onLoadStart, + onErrorHandler, + testID, + source, + ]); + + const renderPreview = useCallback(() => { + if ((intensity as any)._value === 0) return null; + + if (!!previewSource) { return ( ); } - } - - return null; - }; + }, [intensity, previewSource, borderRadiiStyles, atoms.previewImage]); - const renderVideo = () => { - return ( - { - if (onLoad) onLoad(status); + const renderImageLoader = useCallback(() => { + if ((intensity as any)._value === 0) return null; - if (isRemoteVideo && status.isLoaded) { - // Start animating the overlay opacity as soon as the URI becomes available - Animated.timing(intensity, { - toValue: 0, - duration: overlayTransitionDuration, - useNativeDriver: Platform.OS === "android", - }).start(); - } - setHasVideoLoaded(true); - }} - onLoadStart={() => { + if (loaderType === "progressive") { + if (!!previewSource) { + // Render a blur overlay over the preview image if (Platform.OS === "ios") { - setTimeout(() => { - if (!hasVideoLoaded) { - setError(true); - } - }, 1000); + return ( + + {renderFallback()} + + ); } - if (onLoadStart) onLoadStart(); - }} - onError={onErrorHandler} - testID={testID} - source={source} - zIndex={3} - width="100%" - height="100%" - style={StyleSheet.absoluteFill} - /> - ); - }; - const renderPreview = () => { - if ((intensity as any)._value === 0) return null; - - if (!!previewSource) { - return ( - + {renderFallback()} +
+ ); } - /> - ); - } - }; - - const renderImageLoader = () => { - if ((intensity as any)._value === 0) return null; - - if (loaderType === "progressive") { - if (!!previewSource) { - // Render a blur overlay over the preview image - if (Platform.OS === "ios") { - return ( - - {renderFallback()} - - ); } - // Render a static overlay over the preview image - if (Platform.OS === "android" || Platform.OS === "web") { + if (!!previewColor) { return ( {renderFallback()} @@ -273,54 +333,47 @@ const BaseVideo = React.forwardRef( } } - if (!!previewColor) { + if (loaderType === "spinner" && !error) { return ( - - {renderFallback()} - + + + ); } - } - - if (loaderType === "spinner" && !error) { - return ( - - - - ); - } - }; + }, [ + intensity, + loaderType, + previewSource, + borderRadiiStyles, + tint, + blurIntensity, + previewSourceOverlayOpacity, + renderFallback, + previewColor, + previewColorOverlayOpacity, + error, + atoms.spinner, + ]); - useEffect(() => { - setHasVideoLoaded(false); - intensity.setValue(100); - }, [JSON.stringify(source)]); + useEffect(() => { + setHasVideoLoaded(false); + intensity.setValue(100); + }, [JSON.stringify(source)]); - return ( -
- {renderVideo()} - {renderPreview()} - {renderImageLoader()} -
- ); - } + return ( +
+ {renderVideo()} + {renderPreview()} + {renderImageLoader()} +
+ ); + } + ) ); /** diff --git a/src/hooks/useAnimationState.ts b/src/hooks/useAnimationState.ts index fc929444..7ee2685d 100644 --- a/src/hooks/useAnimationState.ts +++ b/src/hooks/useAnimationState.ts @@ -4,6 +4,7 @@ import { useMotiWithStyleProps } from "../hooks"; import { MotiWithPearlStyleProps } from "../theme/src/types"; import { ViewStyle } from "react-native"; import { BoxStyleProps } from "../theme/src/style-functions"; +import { useMemo } from "react"; /** * This function is a custom hook that creates an animation state using the provided props and style functions. @@ -19,9 +20,18 @@ export const useAnimationState = ( }, styleFunctions: StyleFunctionContainer[] = boxStyleFunctions ) => { - // Convert the provided props using the specified style functions. This step is necessary because the props provided may not be in a format that can be used to create an animation state directly. - const convertedProps = useMotiWithStyleProps(props, styleFunctions); + // Use the useMemo hook to memoize the converted props. This will prevent unnecessary computations if the props and style functions have not changed. + const convertedProps = useMemo( + () => useMotiWithStyleProps(props, styleFunctions), + [props, styleFunctions] + ); - // Use the converted props to create an animation state. The useAnimationState function from the "moti" library is used to create the animation state. - return useMotiAnimationState(convertedProps); + // Use the useMemo hook to memoize the animation state. This will prevent unnecessary computations if the converted props have not changed. + const animationState = useMemo( + () => useMotiAnimationState(convertedProps), + [convertedProps] + ); + + // Return the memoized animation state. + return animationState; }; diff --git a/src/hooks/useColorScheme.ts b/src/hooks/useColorScheme.ts index c7412f1a..609222b4 100644 --- a/src/hooks/useColorScheme.ts +++ b/src/hooks/useColorScheme.ts @@ -1,3 +1,4 @@ +import { useMemo } from "react"; import { ColorScheme } from "../theme/src/types"; import { getKeys } from "../theme/utils/type-helpers"; import { useTheme } from "./useTheme"; @@ -13,18 +14,19 @@ const replaceColorValuesInObject = ( newValue: string, obj: Record ): Record => { - const updatedObj: Record = {}; - - for (const [key, value] of Object.entries(obj)) { - if (typeof value === "object" && !getKeys(obj).includes("$$typeof")) { - updatedObj[key] = replaceColorValuesInObject(newValue, value); - } else if (typeof value === "string" && value.includes("primary")) { - updatedObj[key] = value.replace("primary", newValue); - } else { - updatedObj[key] = value; - } - } - return updatedObj; + return Object.entries(obj).reduce( + (updatedObj: Record, [key, value]) => { + if (typeof value === "object" && !getKeys(obj).includes("$$typeof")) { + updatedObj[key] = replaceColorValuesInObject(newValue, value); + } else if (typeof value === "string" && value.includes("primary")) { + updatedObj[key] = value.replace("primary", newValue); + } else { + updatedObj[key] = value; + } + return updatedObj; + }, + {} + ); }; /** @@ -47,5 +49,11 @@ export const useColorScheme = ( checkKeyAvailability(targetColorScheme, theme.palette, "theme.palette"); // Replace the color values in the props object with the new target color scheme. - return replaceColorValuesInObject(targetColorScheme, props); + // Use a memoized version of replaceColorValuesInObject to avoid unnecessary computations. + const memoizedReplaceColorValuesInObject = useMemo( + () => replaceColorValuesInObject(targetColorScheme, props), + [targetColorScheme, props] + ); + + return memoizedReplaceColorValuesInObject; }; diff --git a/src/hooks/useMolecularComponentConfig.ts b/src/hooks/useMolecularComponentConfig.ts index 68e125da..b53ef9e1 100644 --- a/src/hooks/useMolecularComponentConfig.ts +++ b/src/hooks/useMolecularComponentConfig.ts @@ -16,8 +16,10 @@ import { useColorScheme } from "./useColorScheme"; import { useResponsiveProp } from "./useResponsiveProp"; import { useStyleProps } from "./useStyleProps"; import { boxStyleFunctions } from "../theme/src/style-functions"; -import _ from "lodash"; import { removeUndefined } from "../theme/utils/utils"; +import { StyleSheetProperties } from "react-native"; +import { useMemo } from "react"; +import _ from "lodash"; /** * useMolecularComponentConfig is a custom hook used to convert a molecular component style config to the appropriate React Native styles. @@ -33,7 +35,9 @@ import { removeUndefined } from "../theme/utils/utils"; * @param partForOverridenAnimationProps The part where animation props passed to the component instance should be reflected. If undefined, the animation props are passed to the first part as specified in the config * @returns */ -export const useMolecularComponentConfig = ( +export const useMolecularComponentConfig = < + ComponentAtoms extends Record = Record, +>( componentName: keyof FinalPearlTheme["components"], receivedProps: Record, sizeAndVariantProps: { @@ -48,7 +52,10 @@ export const useMolecularComponentConfig = ( partForOverridenStyleProps: string | undefined = undefined, partForOverridenNativeProps: string | undefined = undefined, partForOverridenAnimationProps: string | undefined = undefined -) => { +): { + atoms: ComponentAtoms; + style?: StyleSheetProperties; +} => { const { theme } = useTheme(); checkKeyAvailability( @@ -58,17 +65,27 @@ export const useMolecularComponentConfig = ( ); // User overriden props - const buildStyleProperties = composeStyleProps(styleFunctions); - const overridenStyleProps = _.pick( - receivedProps, - buildStyleProperties.properties + const buildStyleProperties = useMemo( + () => composeStyleProps(styleFunctions), + [styleFunctions] + ); + const overridenStyleProps = useMemo( + () => _.pick(receivedProps, buildStyleProperties.properties), + [receivedProps, buildStyleProperties] + ); + const overridenAnimationProps = useMemo( + () => _.pick(receivedProps, MOTI_PROPS), + [receivedProps] + ); + const overridenNativeProps = useMemo( + () => + _.omit(receivedProps, [ + ...MOTI_PROPS, + "style", + ...buildStyleProperties.properties, + ]), + [receivedProps, buildStyleProperties] ); - const overridenAnimationProps = _.pick(receivedProps, MOTI_PROPS); - const overridenNativeProps = _.omit(receivedProps, [ - ...MOTI_PROPS, - "style", - ...buildStyleProperties.properties, - ]); const overridenProps = useStyleProps(receivedProps, styleFunctions); // Responsive Size and Variant @@ -79,14 +96,19 @@ export const useMolecularComponentConfig = ( sizeAndVariantProps.variant ) as string; - const componentStyleConfig = theme.components[ - componentName - ] as MolecularComponentConfig; + const componentStyleConfig = useMemo( + () => theme.components[componentName] as MolecularComponentConfig, + [theme, componentName] + ); const activeSizeAndVariantConfig: MolecularComponentConfig["defaults"] = {}; - const defaultComponentConfig = componentStyleConfig[ - "defaults" - ] as NonNullable; + const defaultComponentConfig = useMemo( + () => + componentStyleConfig["defaults"] as NonNullable< + MolecularComponentConfig["defaults"] + >, + [componentStyleConfig] + ); if (defaultComponentConfig) { if (defaultComponentConfig.hasOwnProperty("size")) { @@ -135,7 +157,7 @@ export const useMolecularComponentConfig = ( }; } - componentStyleConfig.parts.forEach((part) => { + (componentStyleConfig.parts as string[]).forEach((part) => { const componentTypeStyles: Record = getKeys( activeSizeAndVariantConfig ).reduce( @@ -209,6 +231,7 @@ export const useMolecularComponentConfig = ( if (partForOverridenStyleProps && part === partForOverridenStyleProps) { currentComponentPartProps = { ...currentComponentPartProps, + ...removeUndefined(overridenStyleProps), style: { ...currentComponentPartProps.style, ...removeUndefined(overridenProps.style), @@ -250,5 +273,8 @@ export const useMolecularComponentConfig = ( }); finalComponentProps = useColorScheme(colorScheme, finalComponentProps); - return finalComponentProps; + return finalComponentProps as { + atoms: ComponentAtoms; + style?: StyleSheetProperties; + }; }; diff --git a/src/hooks/useMotiWithStyleProps.ts b/src/hooks/useMotiWithStyleProps.ts index 4f15c064..0e19fedf 100644 --- a/src/hooks/useMotiWithStyleProps.ts +++ b/src/hooks/useMotiWithStyleProps.ts @@ -41,131 +41,63 @@ export const useMotiWithStyleProps = ( "textShadowOffset", ]); - // Convert 'from' prop - if (props.from) { - props.from = composeCleanStyleProps(props.from, buildStyleProperties, { - theme, - colorMode, - dimensions, - }); - } - - // Convert 'animate' prop - if (props.animate) { - props.animate = composeCleanStyleProps( - props.animate, - buildStyleProperties, - { - theme, - colorMode, - dimensions, - } - ); - - // Merge componentStyles and animate - props.animate = { - ...componentStyles, - ...removeUndefined(props.animate), - }; - } - - // Convert 'transition' prop - if (props.transition) { - // Filter object values from 'transition' - const keysWithObjectValues = getKeys(props.transition).filter( - (key) => typeof props.transition[key] === "object" - ); - const propsWithObjectValues = _.pick( - props.transition, - keysWithObjectValues - ); - const nullObject = keysWithObjectValues.reduce((obj, key) => { - return { - ...obj, - [key]: null, - }; - }, {}); - props.transition = { ...props.transition, ...removeUndefined(nullObject) }; - - // Convert style props - props.transition = composeCleanStyleProps( - props.transition, - buildStyleProperties, - { - theme, - colorMode, - dimensions, - } - ); - - // Add the filtered values back into 'transition' - props.transition = keysWithObjectValues.reduce((obj, key: any) => { - let finalKey = key; - if (getKeys(shorthandPropMapper).includes(key)) { - finalKey = (shorthandPropMapper as any)[key]; - } - - return { - ...obj, - [finalKey]: propsWithObjectValues[key], - }; - }, props.transition); - } - - // Convert 'exit' prop - if (props.exit) { - props.exit = composeCleanStyleProps(props.exit, buildStyleProperties, { - theme, - colorMode, - dimensions, - }); - } - - // Convert 'exitTransition' prop - if (props.exitTransition) { - // Filter object values from 'exitTransition' - const keysWithObjectValues = getKeys(props.exitTransition).filter( - (key) => typeof props.exitTransition[key] === "object" - ); - const propsWithObjectValues = _.pick( - props.exitTransition, - keysWithObjectValues - ); - const nullObject = keysWithObjectValues.reduce((obj, key) => { - return { - ...obj, - [key]: null, - }; - }, {}); - props.exitTransition = { - ...props.exitTransition, - ...removeUndefined(nullObject), - }; - - // Convert style props - props.exitTransition = composeCleanStyleProps( - props.exitTransition, - buildStyleProperties, - { - theme, - colorMode, - dimensions, + // Convert 'from', 'animate', 'transition', 'exit', 'exitTransition' props + ["from", "animate", "transition", "exit", "exitTransition"].forEach( + (prop) => { + if (prop === "animate" && props[prop] === undefined) props[prop] = {}; + + if (props[prop]) { + props[prop] = composeCleanStyleProps( + props[prop], + buildStyleProperties, + { + theme, + colorMode, + dimensions, + } + ); + + if (prop === "animate") { + // Merge componentStyles and animate + props.animate = { + ...componentStyles, + ...removeUndefined(props.animate), + }; + } + + if (["transition", "exitTransition"].includes(prop)) { + // Filter object values from 'transition' or 'exitTransition' + const keysWithObjectValues = getKeys(props[prop]).filter( + (key) => typeof props[prop][key] === "object" + ); + const propsWithObjectValues = _.pick( + props[prop], + keysWithObjectValues + ); + const nullObject = keysWithObjectValues.reduce((obj, key) => { + return { + ...obj, + [key]: null, + }; + }, {}); + props[prop] = { ...props[prop], ...removeUndefined(nullObject) }; + + // Add the filtered values back into 'transition' or 'exitTransition' + props[prop] = keysWithObjectValues.reduce((obj, key: any) => { + let finalKey = key; + if (getKeys(shorthandPropMapper).includes(key)) { + finalKey = (shorthandPropMapper as any)[key]; + } + + return { + ...obj, + [finalKey]: propsWithObjectValues[key], + }; + }, props[prop]); + } } - ); - - // Add the filtered values back into 'exitTransition' - props.exitTransition = keysWithObjectValues.reduce((obj, key: any) => { - let finalKey = key; - if (getKeys(shorthandPropMapper).includes(key)) { - finalKey = (shorthandPropMapper as any)[key]; - } - - return { - ...obj, - [finalKey]: propsWithObjectValues[key], - }; - }, props.exitTransition); - } + } + ); return props; }; diff --git a/src/hooks/useResponsiveProp.ts b/src/hooks/useResponsiveProp.ts index 78ff8b9d..788dd401 100644 --- a/src/hooks/useResponsiveProp.ts +++ b/src/hooks/useResponsiveProp.ts @@ -5,6 +5,7 @@ import { getValueForScreenSize, isResponsiveObjectValue, } from "../theme/src/responsive-helpers"; +import { useMemo } from "react"; /** * Hook to get the appropriate value from a responsive style object based on the current screen size. @@ -16,15 +17,18 @@ export const useResponsiveProp = (propValue: ResponsiveValue) => { const dimensions = useDimensions(); // If the propValue is a responsive object value, get the appropriate value for the current screen size - if (isResponsiveObjectValue(propValue, theme)) { - const valueForScreenSize = getValueForScreenSize({ - responsiveValue: propValue, - breakpoints: theme.breakpoints, - dimensions, - }); - return valueForScreenSize; - } + // Use useMemo to avoid unnecessary calculations on every render + return useMemo(() => { + if (isResponsiveObjectValue(propValue, theme)) { + const valueForScreenSize = getValueForScreenSize({ + responsiveValue: propValue, + breakpoints: theme.breakpoints, + dimensions, + }); + return valueForScreenSize; + } - // If the propValue is not a responsive object value, return the propValue as is - return propValue; + // If the propValue is not a responsive object value, return the propValue as is + return propValue; + }, [propValue, theme, dimensions]); }; diff --git a/src/hooks/useStateWithStyleProps.ts b/src/hooks/useStateWithStyleProps.ts index 0308fef8..fbba886d 100644 --- a/src/hooks/useStateWithStyleProps.ts +++ b/src/hooks/useStateWithStyleProps.ts @@ -1,8 +1,9 @@ -import { useMemo } from "react"; +import _ from "lodash"; import { StyleFunctionContainer } from "../theme/src/types"; import { useDimensions } from "./useDimensions"; import { useTheme } from "./useTheme"; import { composeCleanStyleProps, composeStyleProps } from "./utils/utils"; +import { useMemo } from "react"; /** * Returns a state object with style properties. @@ -17,20 +18,26 @@ export const useStateWithStyleProps = ( const { theme, colorMode } = useTheme(); const dimensions = useDimensions(); + // If props is falsy, return an empty object. + if (_.isEmpty(props)) { + return {}; + } + + // Compose the style functions const buildStyleProperties = useMemo( () => composeStyleProps(styleFunctions), [styleFunctions] ); - // If props is falsy, return an empty object. - if (!props) { - return {}; - } - // Compose clean style properties from props, buildStyleProperties, theme, and dimensions. - return composeCleanStyleProps(props, buildStyleProperties, { - theme, - colorMode, - dimensions, - }); + // Use memoization to avoid unnecessary computations. + return useMemo( + () => + composeCleanStyleProps(props, buildStyleProperties, { + theme, + colorMode, + dimensions, + }), + [props, buildStyleProperties, theme, colorMode, dimensions] + ); }; diff --git a/src/hooks/useStyleProps.ts b/src/hooks/useStyleProps.ts index c74fc6c4..bec8783e 100644 --- a/src/hooks/useStyleProps.ts +++ b/src/hooks/useStyleProps.ts @@ -2,6 +2,7 @@ import { useTheme } from "./useTheme"; import { StyleFunctionContainer } from "../theme/src/types"; import { useDimensions } from "./useDimensions"; import { buildFinalStyleProps, composeStyleProps } from "./utils/utils"; +import { useMemo } from "react"; import _ from "lodash"; /** @@ -17,18 +18,25 @@ export const useStyleProps = ( const { theme, colorMode } = useTheme(); const dimensions = useDimensions(); - // Compose the style functions - const buildStyleProperties = composeStyleProps(styleFunctions); - // If no props are passed, return an empty style object - if (!props) { + if (_.isEmpty(props)) { return { style: {} }; } + // Compose the style functions + const buildStyleProperties = useMemo( + () => composeStyleProps(styleFunctions), + [styleFunctions] + ); + // Build the final style properties - return buildFinalStyleProps(props, buildStyleProperties, { - theme, - colorMode, - dimensions, - }); + return useMemo( + () => + buildFinalStyleProps(props, buildStyleProperties, { + theme, + colorMode, + dimensions, + }), + [props, buildStyleProperties, theme, colorMode, dimensions] + ); }; diff --git a/src/pearl.tsx b/src/pearl.tsx index d423959b..1885f460 100644 --- a/src/pearl.tsx +++ b/src/pearl.tsx @@ -59,99 +59,93 @@ export function pearl< partForOverridenAnimationProps?: string | undefined; } = {} ) { - /** - * The final component that will be returned - */ let FinalComponent: any | undefined; - if (config.animatable && config.type !== "molecule") { - /** - * Class component that wraps the base component and adds animation functionality - */ - class ConvertedClassComponent extends React.Component { + const isAnimatable = config.animatable && config.type !== "molecule"; + + if (isAnimatable) { + const ConvertedClassComponent = class extends React.Component { static displayName = `Pearl${ Component.name ?? Component.displayName ?? `NoName` }`; render() { const { children, ...props } = this.props; - return ( {children} ); } - } + }; FinalComponent = motify(ConvertedClassComponent)(); } else { FinalComponent = Component; } - /** - * The final component that will be returned - */ - return React.forwardRef( - ( - { - children, - ...rest - }: PearlComponent & - AtomComponent<(typeof config)["componentName"], ComponentType> & - MoleculeComponent<(typeof config)["componentName"], ComponentType> & { - children?: string | JSX.Element | JSX.Element[] | React.ReactNode; - }, - ref: any - ) => { - /** - * The converted props that will be passed to the final component - */ - let convertedProps; - if (config.type === "atom") { - convertedProps = useAtomicComponentConfig( - config["componentName"], - rest, - { - size: (rest as any).size, - variant: (rest as any).variant, + return React.memo( + React.forwardRef( + ( + { + children, + ...rest + }: PearlComponent & + AtomComponent<(typeof config)["componentName"], ComponentType> & + MoleculeComponent<(typeof config)["componentName"], ComponentType> & { + children?: string | JSX.Element | JSX.Element[] | React.ReactNode; }, - rest.colorScheme, - styleFunctions - ); - if (config.animatable) - convertedProps = useMotiWithStyleProps( - convertedProps, + ref: any + ) => { + /** + * The converted props that will be passed to the final component + */ + let convertedProps; + if (config.type === "atom") { + convertedProps = useAtomicComponentConfig( + config["componentName"], + rest, + { + size: (rest as any).size, + variant: (rest as any).variant, + }, + rest.colorScheme, styleFunctions ); - } else if (config.type === "molecule") { - convertedProps = useMolecularComponentConfig( - config["componentName"], - rest, - { - size: (rest as any).size, - variant: (rest as any).variant, - }, - rest.colorScheme, - styleFunctions, - moleculeConfigOptions.partForOverridenStyleProps, - moleculeConfigOptions.partForOverridenNativeProps, - moleculeConfigOptions.partForOverridenAnimationProps - ); - } else { - convertedProps = useStyleProps(rest, styleFunctions); - if (config.animatable) { - convertedProps = useMotiWithStyleProps( - convertedProps, - styleFunctions + if (config.animatable) + convertedProps = useMotiWithStyleProps( + convertedProps, + styleFunctions + ); + } else if (config.type === "molecule") { + convertedProps = useMolecularComponentConfig( + config["componentName"], + rest, + { + size: (rest as any).size, + variant: (rest as any).variant, + }, + rest.colorScheme, + styleFunctions, + moleculeConfigOptions.partForOverridenStyleProps, + moleculeConfigOptions.partForOverridenNativeProps, + moleculeConfigOptions.partForOverridenAnimationProps ); + } else { + convertedProps = useStyleProps(rest, styleFunctions); + if (config.animatable) { + convertedProps = useMotiWithStyleProps( + convertedProps, + styleFunctions + ); + } } - } - return ( - - {children} - - ); - } + return ( + + {children} + + ); + } + ) ); } diff --git a/src/theme/index.ts b/src/theme/index.ts index 87a72618..ee3b0b0b 100644 --- a/src/theme/index.ts +++ b/src/theme/index.ts @@ -1,57 +1,13 @@ -// Theme functions -export { extendTheme } from "./src/base/index"; - // Style functions -export { - colorStyleFunction, - opacityStyleFunction, - visibleStyleFunction, - backgroundColorStyleFunction, - spacingStyleFunction, - typographyStyleFunction, - layoutStyleFunction, - positionStyleFunction, - borderStyleFunction, - shadowStyleFunction, - textShadowStyleFunction, - boxStyleFunctions, - textStyleFunctions, - allStyleFunctions, -} from "./src/style-functions"; +export * from "./src/style-functions"; // Theme Context export { ThemeProvider } from "./src/theme-context"; -export { baseTheme } from "./src/base/index"; +export { baseTheme, extendTheme } from "./src/base/index"; // Utils export { generatePalette } from "./utils/utils"; // Types -export type { - ResponsiveValue, - AtomicComponentConfig, - MolecularComponentConfig, - CustomPearlTheme, - FinalPearlTheme, - PaletteColors, - ComponentSizes, - ComponentVariants, - ColorScheme, - StyleFunctionContainer, - BasicComponentProps, - AtomComponentProps, - MoleculeComponentProps, -} from "./src/types"; -export type { - AllProps, - BackgroundColorProps, - ColorProps, - OpacityProps, - SpacingProps, - TypographyProps, - LayoutProps, - PositionProps, - BorderProps, - ShadowProps, - TextShadowProps, -} from "./src/style-functions"; +export type * from "./src/types"; +export type * from "./src/style-functions"; diff --git a/src/theme/src/base/border-radii.ts b/src/theme/src/base/border-radii.ts index 65f21632..915dada9 100644 --- a/src/theme/src/base/border-radii.ts +++ b/src/theme/src/base/border-radii.ts @@ -1,4 +1,5 @@ export const borderRadii = { + none: 0, xs: 2, s: 4, m: 8, diff --git a/src/theme/src/base/components.ts b/src/theme/src/base/components.ts index 66d53144..d766eee6 100644 --- a/src/theme/src/base/components.ts +++ b/src/theme/src/base/components.ts @@ -17,6 +17,28 @@ import SwitchConfig from "../../../components/molecules/switch/switch.config"; import TextLinkConfig from "../../../components/molecules/text-link/text-link.config"; import VideoConfig from "../../../components/molecules/video/video.config"; +interface Config { + None: { baseStyle: {} }; + Icon: typeof IconConfig; + Text: typeof TextConfig; + Skeleton: typeof SkeletonConfig; + Screen: typeof ScreenConfig; + Spinner: typeof SpinnerConfig; + Button: typeof ButtonConfig; + IconButton: typeof IconButtonConfig; + TextLink: typeof TextLinkConfig; + Input: typeof InputConfig; + CheckBox: typeof CheckBoxConfig; + Radio: typeof RadioConfig; + Badge: typeof BadgeConfig; + Divider: typeof DividerConfig; + Switch: typeof SwitchConfig; + Image: typeof ImageConfig; + Video: typeof VideoConfig; + Avatar: typeof AvatarConfig; + Progress: typeof ProgressConfig; +} + export default { None: { baseStyle: {} }, Icon: IconConfig, @@ -37,4 +59,4 @@ export default { Video: VideoConfig, Avatar: AvatarConfig, Progress: ProgressConfig, -}; +} as Config; diff --git a/src/theme/src/base/elevation.ts b/src/theme/src/base/elevation.ts index fbffffdb..2804fdd0 100644 --- a/src/theme/src/base/elevation.ts +++ b/src/theme/src/base/elevation.ts @@ -1,4 +1,14 @@ export const elevation = { + none: { + shadowColor: "#1A2138", + shadowOffset: { + width: 0, + height: 0, + }, + shadowOpacity: 0, + shadowRadius: 0, + elevation: 0, + }, xs: { shadowColor: "#1A2138", shadowOffset: { diff --git a/src/theme/src/style-functions.ts b/src/theme/src/style-functions.ts index 8a0ca99b..317c4684 100644 --- a/src/theme/src/style-functions.ts +++ b/src/theme/src/style-functions.ts @@ -1,4 +1,10 @@ -import { ColorValue, FlexStyle, TextStyle, ViewStyle } from "react-native"; +import { + ColorValue, + FlexStyle, + TextStyle, + TransformsStyle, + ViewStyle, +} from "react-native"; import { getKeys, getNestedObject, isThemeKey } from "../utils/type-helpers"; import { borderColorProperties, @@ -11,6 +17,7 @@ import { spacingProperties, spacingPropertiesShorthand, textShadowProperties, + transformProperties, typographyProperties, } from "./style-properties"; import { @@ -72,9 +79,7 @@ export const createStyleFunction = ({ : props[property]; // Transform the value if transformation function exists - if (transform) { - value = transform(value, colorMode); - } + if (transform) value = transform(value, colorMode); // Check if this value refers to a key in the theme config if (isThemeKey(theme, themeKey)) { @@ -82,12 +87,14 @@ export const createStyleFunction = ({ let themeTokenValue = theme[themeKey][value]; // For color palettes with multiple shades - if (typeof value === "string" && value.match(/^[a-z]+\.\d+$/)) { - themeTokenValue = getNestedObject(theme, [ - themeKey, - value.split(".")[0], - value.split(".")[1], - ]); + if (themeKey === "palette") { + if (typeof value === "string" && value.match(/^[a-z]+\.\d+$/)) { + themeTokenValue = getNestedObject(theme, [ + themeKey, + value.split(".")[0], + value.split(".")[1], + ]); + } } // Apply the value @@ -206,6 +213,14 @@ export const layoutStyleFunction = [ }), ]; +export const transformStyleFunction = getKeys(transformProperties).map( + (property) => { + return createStyleFunction({ + property, + }); + } +); + export const spacingStyleFunction = [ ...getKeys(spacingProperties).map((property) => { return createStyleFunction({ @@ -295,6 +310,7 @@ export const allStyleFunctions = [ ...spacingStyleFunction, ...typographyStyleFunction, ...layoutStyleFunction, + ...transformStyleFunction, ...positionStyleFunction, ...borderStyleFunction, ...shadowStyleFunction, @@ -307,6 +323,7 @@ export const boxStyleFunctions = [ visibleStyleFunction, ...backgroundColorStyleFunction, ...layoutStyleFunction, + ...transformStyleFunction, ...spacingStyleFunction, ...borderStyleFunction, ...shadowStyleFunction, @@ -314,15 +331,16 @@ export const boxStyleFunctions = [ ] as StyleFunctionContainer[]; export const textStyleFunctions = [ - colorStyleFunction, - backgroundColorStyleFunction, opacityStyleFunction, visibleStyleFunction, - layoutStyleFunction, - typographyStyleFunction, - spacingStyleFunction, - textShadowStyleFunction, - positionStyleFunction, + colorStyleFunction, + ...backgroundColorStyleFunction, + ...layoutStyleFunction, + ...transformStyleFunction, + ...typographyStyleFunction, + ...spacingStyleFunction, + ...textShadowStyleFunction, + ...positionStyleFunction, ] as StyleFunctionContainer[]; // PropTypes @@ -356,13 +374,18 @@ type SpacingShorthandProps = { export type SpacingProps = SpacingPropsBase & SpacingShorthandProps; -export type TypographyProps = { - [Key in keyof typeof typographyProperties]?: TextStyle[Key]; -} & { +export type TypographyProps = Omit< + { + [Key in keyof typeof typographyProperties]?: TextStyle[Key]; + }, + "fontWeight" | "letterSpacing" | "fontSize" | "lineHeight" | "fontFamily" +> & { letterSpacing?: ResponsiveValue; fontSize?: ResponsiveValue; lineHeight?: ResponsiveValue; - fontWeight?: ResponsiveValue; + fontWeight?: ResponsiveValue< + keyof FinalPearlTheme["fontWeights"] | string | number + >; fontFamily?: ResponsiveValue; }; @@ -378,6 +401,12 @@ type LayoutShorthandProps = { export type LayoutProps = LayoutPropsBase & LayoutShorthandProps; +export type TransformProps = { + [Key in keyof typeof transformProperties]?: ResponsiveValue< + TransformsStyle[Key] + >; +}; + export type PositionProps = { [Key in keyof typeof positionProperties]?: ResponsiveValue; } & { @@ -413,6 +442,7 @@ export type AllProps = BackgroundColorProps & SpacingProps & TypographyProps & LayoutProps & + TransformProps & PositionProps & BorderProps & ShadowProps & @@ -423,6 +453,7 @@ export type BoxStyleProps = BackgroundColorProps & OpacityProps & VisibleProps & LayoutProps & + TransformProps & SpacingProps & BorderProps & ShadowProps & @@ -433,6 +464,7 @@ export type TextStyleProps = ColorProps & OpacityProps & VisibleProps & LayoutProps & + TransformProps & TypographyProps & SpacingProps & TextShadowProps; diff --git a/src/theme/src/style-properties.ts b/src/theme/src/style-properties.ts index 83986a8b..932262f2 100644 --- a/src/theme/src/style-properties.ts +++ b/src/theme/src/style-properties.ts @@ -71,6 +71,16 @@ export const layoutProperties = { flexWrap: true, }; +export const transformProperties = { + transform: true, + transformMatrix: true, + rotation: true, + scaleX: true, + scaleY: true, + translateX: true, + translateY: true, +}; + export const layoutPropertiesShorthand = { w: "width", h: "height", diff --git a/src/theme/src/types.ts b/src/theme/src/types.ts index 15a5ecf5..95f03b59 100644 --- a/src/theme/src/types.ts +++ b/src/theme/src/types.ts @@ -54,15 +54,15 @@ export interface ColorPalette { [key: number]: string; } -export interface AtomicComponentConfig { - baseStyle: { - [key: string]: any; - }; +export interface AtomicComponentConfig< + T extends Record = Record, +> { + baseStyle: Partial; sizes?: { - [key: string]: any; + [key: string]: Partial; }; variants?: { - [key: string]: any; + [key: string]: Partial; }; defaults?: { size?: string; @@ -70,16 +70,26 @@ export interface AtomicComponentConfig { }; } -export interface MolecularComponentConfig { - parts: string[]; +export interface MolecularComponentConfig< + T extends Record = Record, +> { + parts: (keyof T[])[] | string[]; baseStyle: { - [key: string]: any; + [P in keyof Partial]: Partial; }; sizes?: { - [key: string]: any; + [key: string]: { + [P in keyof Partial]: + | Partial + | ((variant?: string, colorScheme?: string) => Partial); + }; }; variants?: { - [key: string]: any; + [key: string]: { + [P in keyof Partial]: + | Partial + | ((size?: string, colorScheme?: string) => Partial); + }; }; defaults?: { size?: string; @@ -236,11 +246,12 @@ export type AtomComponent< export type MoleculeComponent< ComponentName extends keyof FinalPearlTheme["components"], ComponentType extends ComponentTypes = "molecule", + ComponentAtoms extends Record = Record, > = ComponentType extends "molecule" ? { size?: ResponsiveValue>; variant?: ResponsiveValue>; - atoms?: Record; + atoms?: ComponentAtoms; } : {}; @@ -282,14 +293,13 @@ export type AtomComponentProps< export type MoleculeComponentProps< ComponentName extends keyof FinalPearlTheme["components"], ComponentProps, + ComponentAtoms extends Record = Record, StyleProps = BoxStyleProps, Animateable = true, > = PearlComponent & { - /** Size of the component. */ size?: ResponsiveValue>; - /** Variant of the component. */ variant?: ResponsiveValue>; - atoms: Record; + atoms: ComponentAtoms; }; type AnimateableProps< diff --git a/tsconfig.json b/tsconfig.json index 98b0e552..394d3773 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,6 +15,6 @@ "**/*.test.[tj]sx", "App.test.tsx", "App.tsx", - "documentation/*" + "docs/*" ] } diff --git a/wdyr.js b/wdyr.js new file mode 100644 index 00000000..f34dcda2 --- /dev/null +++ b/wdyr.js @@ -0,0 +1,8 @@ +import React from "react"; + +if (process.env.NODE_ENV === "development") { + const whyDidYouRender = require("@welldone-software/why-did-you-render"); + whyDidYouRender(React, { + trackAllPureComponents: true, + }); +} diff --git a/yarn.lock b/yarn.lock index f2bf59d7..51973042 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3184,6 +3184,13 @@ "@urql/core" ">=2.3.1" wonka "^4.0.14" +"@welldone-software/why-did-you-render@^7.0.1": + version "7.0.1" + resolved "https://registry.yarnpkg.com/@welldone-software/why-did-you-render/-/why-did-you-render-7.0.1.tgz#09f487d84844bd8e66435843c2e0305702e61efb" + integrity sha512-Qe/8Xxa2G+LMdI6VoazescPzjjkHYduCDa8aHOJR50e9Bgs8ihkfMBY+ev7B4oc3N59Zm547Sgjf8h5y0FOyoA== + dependencies: + lodash "^4" + "@xmldom/xmldom@^0.8.8": version "0.8.10" resolved "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz" @@ -7753,7 +7760,7 @@ lodash.upperfirst@^4.3.1: resolved "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz" integrity sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg== -lodash@4.17.21, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.17.4: +lodash@4.17.21, lodash@^4, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.17.4: version "4.17.21" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==