diff --git a/apps/showcase/app/button.tsx b/apps/showcase/app/button.tsx index 5cf26bbc..bf8efc28 100644 --- a/apps/showcase/app/button.tsx +++ b/apps/showcase/app/button.tsx @@ -1,6 +1,8 @@ +import { Apple, Bot } from 'lucide-react-native'; import { View } from 'react-native'; import { Button } from '~/components/ui/button'; import { Text } from '~/components/ui/text'; +import { Icon } from '~/lib/icon'; export default function ButtonScreen() { return ( @@ -29,6 +31,14 @@ export default function ButtonScreen() { + + ); } diff --git a/apps/showcase/lib/icon.tsx b/apps/showcase/lib/icon.tsx new file mode 100644 index 00000000..b8c53584 --- /dev/null +++ b/apps/showcase/lib/icon.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { cssInterop } from 'nativewind'; +import { createIcon, PrimitiveIcon, IPrimitiveIcon, Svg } from '~/lib/rnr-icon'; + +export const UIIcon = createIcon({ + Root: PrimitiveIcon, +}) as React.ForwardRefExoticComponent< + React.ComponentPropsWithoutRef & + React.RefAttributes> +>; + +const iconStyle = cva('text-typography-950 fill-none pointer-events-none', { + variants: { + size: { + '2xs': 'h-3 w-3', + xs: 'h-3.5 w-3.5', + sm: 'h-4 w-4', + md: 'h-[18px] w-[18px]', + lg: 'h-5 w-5', + xl: 'h-6 w-6', + }, + }, + defaultVariants: { + size: 'md', + }, +}); + +cssInterop(UIIcon, { + className: { + target: 'style', + nativeStyleToProp: { + height: true, + width: true, + fill: true, + color: 'classNameColor', + stroke: true, + }, + }, +}); + +type IIConProps = IPrimitiveIcon & + (VariantProps | { size: number }) & + React.ComponentPropsWithoutRef; + +export const Icon = React.forwardRef, IIConProps>( + ({ size = 'md', className, ...props }, ref) => { + if (typeof size === 'number') { + return ; + } else if ((props.height !== undefined || props.width !== undefined) && size === undefined) { + return ; + } + return ; + } +); + +type ParameterTypes = Omit[0], 'Root'>; + +const createIconUI = ({ ...props }: ParameterTypes) => { + const UIIconCreateIcon = createIcon({ + Root: Svg, + ...props, + }) as React.ForwardRefExoticComponent< + React.ComponentPropsWithoutRef & + React.RefAttributes> + >; + + return React.forwardRef>( + ( + { + className, + size, + ...inComingProps + }: VariantProps & React.ComponentPropsWithoutRef, + ref + ) => { + return ( + + ); + } + ); +}; +export { createIconUI as createIcon }; diff --git a/apps/showcase/lib/rnr-icon/createIcon/index.tsx b/apps/showcase/lib/rnr-icon/createIcon/index.tsx new file mode 100644 index 00000000..7c797df4 --- /dev/null +++ b/apps/showcase/lib/rnr-icon/createIcon/index.tsx @@ -0,0 +1,125 @@ +import React, { forwardRef } from 'react'; +import type { ColorValue, ViewProps } from 'react-native'; +import { Path, G } from 'react-native-svg'; + +interface CreateIconOptions { + /** + * The icon `svg` viewBox + * @default "0 0 24 24" + */ + viewBox?: string; + /** + * The `svg` path or group element + * @type React.ReactElement | React.ReactElement[] + */ + path?: React.ReactElement | React.ReactElement[]; + /** + * If the `svg` has a single path, simply copy the path's `d` attribute + */ + d?: string; + /** + * The display name useful in the dev tools + */ + displayName?: string; + /** + * Default props automatically passed to the component; overwritable + */ + defaultProps?: any; + type?: any; +} + +export interface IIconProps extends ViewProps {} + +export type IIconComponentType = React.ForwardRefExoticComponent< + IIconProps & IconProps & React.RefAttributes +>; + +const ChildPath = ({ element, fill, stroke: pathStroke }: any) => { + const pathStrokeColor = pathStroke || ''; + const fillColor = fill || ''; + + if (!element) { + return null; + } + + return React.cloneElement(element, { + fill: fillColor ? fillColor : 'currentColor', + stroke: pathStrokeColor, + }); +}; + +export function createIcon({ + Root, + path, + d, + ...initialProps +}: { Root: React.ComponentType } & CreateIconOptions) { + const IconTemp = forwardRef((props: any, ref?: any) => { + let children = path; + if (d && (!path || Object.keys(path).length === 0)) { + children = ; + } + + const finalProps = { + ...initialProps, + ...props, + }; + + const { + stroke = 'currentColor', + color, + role = 'img', + ...resolvedProps + } = finalProps; + let type = resolvedProps.type; + if (type === undefined) { + type = 'svg'; + } + let colorProps = {}; + if (color) { + colorProps = { ...colorProps, color: color }; + } + if (stroke) { + colorProps = { ...colorProps, stroke: stroke }; + } + + let sizeProps = {}; + let sizeStyle = {}; + if (type === 'font') { + if (resolvedProps.sx) { + sizeProps = { ...sizeProps, fontSize: resolvedProps?.sx?.h }; + } + if (resolvedProps.size) { + // sizeProps = { ...sizeProps, fontSize: resolvedProps?.size }; + } + } + + return ( + + {React.Children.count(children) > 0 ? ( + + {React.Children.map(children, (child, i) => ( + + ))} + + ) : null} + + ); + }); + + const Icon = IconTemp as IIconComponentType< + IconProps | { fill?: ColorValue; stroke?: ColorValue } + >; + return Icon; +} diff --git a/apps/showcase/lib/rnr-icon/createIcon/index.web.tsx b/apps/showcase/lib/rnr-icon/createIcon/index.web.tsx new file mode 100644 index 00000000..7b1e0779 --- /dev/null +++ b/apps/showcase/lib/rnr-icon/createIcon/index.web.tsx @@ -0,0 +1,128 @@ +import React, { forwardRef } from 'react'; +import type { ColorValue, ViewProps } from 'react-native'; + +interface CreateIconOptions { + /** + * The icon `svg` viewBox + * @default "0 0 24 24" + */ + viewBox?: string; + /** + * The `svg` path or group element + * @type React.ReactElement | React.ReactElement[] + */ + path?: React.ReactElement | React.ReactElement[]; + /** + * If the `svg` has a single path, simply copy the path's `d` attribute + */ + d?: string; + /** + * The display name useful in the dev tools + */ + displayName?: string; + /** + * Default props automatically passed to the component; overwritable + */ + defaultProps?: any; + type?: any; +} + +export interface IIconProps extends ViewProps {} + +export type IIconComponentType = React.ForwardRefExoticComponent< + IIconProps & IconProps +>; + +const ChildPath = ({ element, fill, stroke: pathStroke }: any) => { + const pathStrokeColor = pathStroke || ''; + const fillColor = fill || ''; + + if (!element) { + return null; + } + + if (element.type === React.Fragment) { + return element; + } + + return React.cloneElement(element, { + fill: fillColor ? fillColor : 'currentColor', + stroke: pathStrokeColor, + }); +}; + +export function createIcon({ + Root, + path, + d, + ...initialProps +}: { Root: React.ComponentType } & CreateIconOptions) { + const IconTemp = forwardRef((props: any, ref?: any) => { + let children = path; + if (d && (!path || Object.keys(path).length === 0)) { + children = ; + } + + const finalProps = { + ...initialProps, + ...props, + }; + + const { + stroke = 'currentColor', + color, + role = 'img', + ...resolvedProps + } = finalProps; + let type = resolvedProps.type; + if (type === undefined) { + type = 'svg'; + } + let colorProps = {}; + if (color) { + colorProps = { ...colorProps, color: color }; + } + if (stroke) { + colorProps = { ...colorProps, stroke: stroke }; + } + + let sizeProps = {}; + let sizeStyle = {}; + if (type === 'font') { + if (resolvedProps.sx) { + sizeProps = { ...sizeProps, fontSize: resolvedProps?.sx?.h }; + } + if (resolvedProps.size) { + // sizeProps = { ...sizeProps, fontSize: resolvedProps?.size }; + } + } + + return ( + + {React.Children.count(children) > 0 ? ( + + {React.Children.map(children, (child, i) => ( + + ))} + + ) : null} + + ); + }); + + const Icon = IconTemp as IIconComponentType< + IconProps | { fill?: ColorValue; stroke?: ColorValue } + >; + return Icon; +} diff --git a/apps/showcase/lib/rnr-icon/index.tsx b/apps/showcase/lib/rnr-icon/index.tsx new file mode 100644 index 00000000..65932e98 --- /dev/null +++ b/apps/showcase/lib/rnr-icon/index.tsx @@ -0,0 +1,3 @@ +export { createIcon } from './createIcon'; +export { PrimitiveIcon, Svg, UIIcon } from './primitiveIcon'; +export type { IPrimitiveIcon } from './primitiveIcon'; diff --git a/apps/showcase/lib/rnr-icon/index.web.tsx b/apps/showcase/lib/rnr-icon/index.web.tsx new file mode 100644 index 00000000..65932e98 --- /dev/null +++ b/apps/showcase/lib/rnr-icon/index.web.tsx @@ -0,0 +1,3 @@ +export { createIcon } from './createIcon'; +export { PrimitiveIcon, Svg, UIIcon } from './primitiveIcon'; +export type { IPrimitiveIcon } from './primitiveIcon'; diff --git a/apps/showcase/lib/rnr-icon/primitiveIcon/index.tsx b/apps/showcase/lib/rnr-icon/primitiveIcon/index.tsx new file mode 100644 index 00000000..aea77b2c --- /dev/null +++ b/apps/showcase/lib/rnr-icon/primitiveIcon/index.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { Svg } from 'react-native-svg'; +import { createIcon } from '../createIcon'; + +export { Svg }; +export type IPrimitiveIcon = { + height?: number | string; + width?: number | string; + fill?: string; + color?: string; + size?: number | string; + stroke?: string; + as?: React.ElementType; + className?: string; + classNameColor?: string; + style?: any; +}; + +export const PrimitiveIcon = React.forwardRef, IPrimitiveIcon>( + ( + { + height, + width, + fill, + color, + classNameColor, + size, + stroke = 'currentColor', + as: AsComp, + style, + ...props + }, + ref + ) => { + color = color ?? classNameColor; + const sizeProps = React.useMemo(() => { + if (size) return { size }; + if (height && width) return { height, width }; + if (height) return { height }; + if (width) return { width }; + return {}; + }, [size, height, width]); + + let colorProps = {}; + if (fill) { + colorProps = { ...colorProps, fill: fill }; + } + if (stroke !== 'currentColor') { + colorProps = { ...colorProps, stroke: stroke }; + } else if (stroke === 'currentColor' && color !== undefined) { + colorProps = { ...colorProps, stroke: color }; + } + + if (AsComp) { + return ; + } + return ; + } +); + +export const UIIcon = createIcon({ + Root: PrimitiveIcon, +}); diff --git a/apps/showcase/lib/rnr-icon/primitiveIcon/index.web.tsx b/apps/showcase/lib/rnr-icon/primitiveIcon/index.web.tsx new file mode 100644 index 00000000..5f1acd28 --- /dev/null +++ b/apps/showcase/lib/rnr-icon/primitiveIcon/index.web.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { createIcon } from '../createIcon'; + +const accessClassName = (style: any) => { + const styleObject = Array.isArray(style) ? style[0] : style; + const keys = Object.keys(styleObject); + return styleObject[keys[1]]; +}; + +const Svg = React.forwardRef, React.ComponentPropsWithoutRef<'svg'>>( + ({ style, className, ...props }, ref) => { + const calculateClassName = React.useMemo(() => { + return className === undefined ? accessClassName(style) : className; + }, [className, style]); + return ; + } +); + +export type IPrimitiveIcon = { + height?: number | string; + width?: number | string; + fill?: string; + color?: string; + size?: number | string; + stroke?: string; + as?: React.ElementType; + className?: string; + classNameColor?: string; + style?: any; +}; + +const PrimitiveIcon = React.forwardRef, IPrimitiveIcon>( + ({ height, width, fill, color, classNameColor, size, stroke, as: AsComp, ...props }, ref) => { + color = color ?? classNameColor; + const sizeProps = React.useMemo(() => { + if (size) return { size }; + if (height && width) return { height, width }; + if (height) return { height }; + if (width) return { width }; + return {}; + }, [size, height, width]); + + let colorProps = {}; + if (fill) { + colorProps = { ...colorProps, fill: fill }; + } + if (stroke !== 'currentColor') { + colorProps = { ...colorProps, stroke: stroke }; + } else if (stroke === 'currentColor' && color !== undefined) { + colorProps = { ...colorProps, stroke: color }; + } + + if (AsComp) { + return ; + } + return ; + } +); + +const UIIcon = createIcon({ + Root: PrimitiveIcon, +}); + +export { PrimitiveIcon, Svg, UIIcon };