diff --git a/features/app-core/components/Image.tsx b/features/app-core/components/Image.tsx index 162ab80..49dc3cc 100644 --- a/features/app-core/components/Image.tsx +++ b/features/app-core/components/Image.tsx @@ -1,5 +1,6 @@ import { Image as ExpoImage } from 'expo-image' import { UniversalImageProps, UniversalImageMethods } from './Image.types' +import { parseNativeWindStyles } from '../utils/parseNativeWindStyles' /* --- -------------------------------------------------------------------------------- */ @@ -38,10 +39,13 @@ const Image = (props: UniversalImageProps): JSX.Element => { responsivePolicy, } = props + // -- Nativewind -- + + const { nativeWindStyles, restStyle } = parseNativeWindStyles(style) + const finalStyle = { width, height, ...nativeWindStyles, ...restStyle } + // -- Overrides -- - // @ts-ignore - const finalStyle = { width, height, ...style } if (fill) finalStyle.height = '100%' if (fill) finalStyle.width = '100%' diff --git a/features/app-core/components/Image.types.tsx b/features/app-core/components/Image.types.tsx index e4a3ce6..8768eed 100644 --- a/features/app-core/components/Image.types.tsx +++ b/features/app-core/components/Image.types.tsx @@ -38,6 +38,10 @@ export type UniversalImageProps = { * - Remember that the required width and height props can interact with your styling. If you use styling to modify an image's width, you should also style its height to auto to preserve its intrinsic aspect ratio, or your image will be distorted. */ style?: ExpoImageProps['style'] + /** Universal, will affect both Expo & Next.js + * - Remember that the required width and height props can interact with your styling. If you use styling to modify an image's width, you should also style its height to auto to preserve its intrinsic aspect ratio, or your image will be distorted. */ + className?: string + /** Universal, will affect both Expo & Next.js - Called on an image fetching error. */ onError?: ExpoImageProps['onError'] diff --git a/features/app-core/components/Image.web.tsx b/features/app-core/components/Image.web.tsx index f37b907..6bcc6ee 100644 --- a/features/app-core/components/Image.web.tsx +++ b/features/app-core/components/Image.web.tsx @@ -1,5 +1,6 @@ import NextImage from 'next/image' import { UniversalImageProps, UniversalImageMethods } from './Image.types' +import { parseNativeWindStyles } from '../utils/parseNativeWindStyles' /* --- -------------------------------------------------------------------------------- */ @@ -11,6 +12,7 @@ const Image = (props: UniversalImageProps): JSX.Element => { alt, width, height, + className, style = {}, priority = 'normal', onError, @@ -31,10 +33,13 @@ const Image = (props: UniversalImageProps): JSX.Element => { contentFit, } = props + // -- Nativewind -- + + const { nativeWindStyles, nativeWindClassName, restStyle } = parseNativeWindStyles(style) + const finalStyle = { width, height, ...nativeWindStyles, ...restStyle } as React.CSSProperties + // -- Overrides -- - // @ts-ignore - const finalStyle = { width, height, ...style } if (fill) finalStyle.height = '100%' if (fill) finalStyle.width = '100%' if (fill) finalStyle.objectFit = contentFit || 'cover' @@ -47,7 +52,8 @@ const Image = (props: UniversalImageProps): JSX.Element => { src={src as any} alt={alt || accessibilityLabel} width={width} - height={height} // @ts-ignore + height={height} + className={[className, nativeWindClassName].filter(Boolean).join(' ')} style={finalStyle} priority={priority === 'high'} onError={onError as any} diff --git a/features/app-core/components/styled.tsx b/features/app-core/components/styled.tsx index eb9b30e..4a503f1 100644 --- a/features/app-core/components/styled.tsx +++ b/features/app-core/components/styled.tsx @@ -1,11 +1,13 @@ import { styled } from 'nativewind' import { Text as RNText, View as RNView } from 'react-native' import { Link as UniversalLink } from '../navigation/Link' +import { Image as UniversalImage } from './Image' /* --- Primitives ------------------------------------------------------------------------------ */ export const View = styled(RNView, '') export const Text = styled(RNText, '') +export const Image = styled(UniversalImage, '') /* --- Typography ------------------------------------------------------------------------------ */ @@ -17,6 +19,7 @@ export const P = styled(RNText, 'text-base') /* --- Fix for Next Link ----------------------------------------------------------------------- */ +export const Link = styled(UniversalLink, 'text-blue-500 underline') export const LinkText = styled(RNText, 'text-blue-500 underline') export const TextLink = (props: Omit, 'className'> & { className?: string }) => { const { className, style, children, ...universalLinkProps } = props diff --git a/features/app-core/navigation/Link.tsx b/features/app-core/navigation/Link.tsx index 6f36026..6efc365 100644 --- a/features/app-core/navigation/Link.tsx +++ b/features/app-core/navigation/Link.tsx @@ -1,5 +1,6 @@ import { Link as ExpoLink } from 'expo-router' import type { UniversalLinkProps } from './Link.types' +import { parseNativeWindStyles } from '../utils/parseNativeWindStyles' /* --- --------------------------------------------------------------------------------- */ @@ -21,12 +22,17 @@ export const Link = (props: UniversalLinkProps) => { maxFontSizeMultiplier } = props + // -- Nativewind -- + + const { nativeWindStyles, restStyle } = parseNativeWindStyles(style) + const finalStyle = { ...nativeWindStyles, ...restStyle } + // -- Render -- return ( --------------------------------------------------------------------------------- */ @@ -9,6 +10,7 @@ export const Link = (props: UniversalLinkProps) => { const { children, href, + className, style, replace, onPress, @@ -21,12 +23,18 @@ export const Link = (props: UniversalLinkProps) => { as, } = props + // -- Nativewind -- + + const { nativeWindStyles, nativeWindClassName, restStyle } = parseNativeWindStyles(style) + const finalStyle = { ...nativeWindStyles, ...restStyle } as React.CSSProperties + // -- Render -- return ( ['style']} + className={[className, nativeWindClassName].filter(Boolean).join(' ')} + style={finalStyle as unknown as ComponentProps['style']} onClick={onPress} target={target} replace={replace} diff --git a/features/app-core/screens/HomeScreen.tsx b/features/app-core/screens/HomeScreen.tsx index 52028ec..755f6b2 100644 --- a/features/app-core/screens/HomeScreen.tsx +++ b/features/app-core/screens/HomeScreen.tsx @@ -1,18 +1,20 @@ import React from 'react' -import { Image } from '../components/Image' -import { View, H3, P, TextLink } from '../components/styled' +import { View, Image, H3, P, Link } from '../components/styled' /* --- --------------------------------------------------------------------------- */ const HomeScreen = () => { return ( - -

Expo + Next.js app routing 👋

+ +

Expo + Next.js app routing 🚀

Open HomeScreen.tsx in features/app-core/screens to start working on your app

- + Test navigation - + + + Test images +
) } diff --git a/features/app-core/screens/ImagesScreen.tsx b/features/app-core/screens/ImagesScreen.tsx index 81bd68e..06a1e6e 100644 --- a/features/app-core/screens/ImagesScreen.tsx +++ b/features/app-core/screens/ImagesScreen.tsx @@ -1,67 +1,37 @@ import React from 'react' -import { StyleSheet, Text, View } from 'react-native' -import { Link } from '../navigation/Link' -import { Image } from '../components/Image' +import { View, Text, Image, Link } from '../components/styled' /* --- --------------------------------------------------------------------------- */ const ImagesScreen = () => { return ( - + {`< Back`} {/* - 1 - */} - src=static-require | width: 60 | height: 60 + src=static-require | width: 60 | height: 60 {/* - 2 - */} - src=external-url | width: 60 | height: 60 + src=external-url | width: 60 | height: 60 {/* - 3 - */} - + - wrapper=50x80, relative | fill=true + wrapper=50x80, relative | fill=true {/* - 4 - */} - + - wrapper=80x60, relative | fill | contentFit=contain + wrapper=80x60, relative | fill | contentFit=contain ) } -/* --- Styles ---------------------------------------------------------------------------------- */ - -const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - backButton: { - position: 'absolute', - top: 16, - left: 16, - }, - subtitle: { - marginTop: 8, - marginBottom: 16, - fontSize: 16, - textAlign: 'center', - }, - link: { - marginTop: 16, - fontSize: 16, - color: 'blue', - textAlign: 'center', - textDecorationLine: 'underline', - }, -}) - /* --- Exports --------------------------------------------------------------------------------- */ export default ImagesScreen diff --git a/features/app-core/screens/SlugScreen.tsx b/features/app-core/screens/SlugScreen.tsx index e72b264..2924afb 100644 --- a/features/app-core/screens/SlugScreen.tsx +++ b/features/app-core/screens/SlugScreen.tsx @@ -1,6 +1,6 @@ import React from 'react' import { useRouteParams } from '@app/core/navigation/useRouteParams' -import { View, Text, H3, TextLink } from '../components/styled' +import { View, Text, H3, Link } from '../components/styled' import { useRouter } from '../navigation/useRouter' /* --- --------------------------------------------------------------------------- */ @@ -32,13 +32,13 @@ const SlugScreen = (props) => { Need a more robust, Fully-Stacked, Full-Product, Universal App Setup? - Check out the GREEN Stack Starter - + push('/subpages/push')}> {`router.push()`} diff --git a/features/app-core/utils/parseNativeWindStyles.ts b/features/app-core/utils/parseNativeWindStyles.ts new file mode 100644 index 0000000..afa1855 --- /dev/null +++ b/features/app-core/utils/parseNativeWindStyles.ts @@ -0,0 +1,31 @@ + +/** --- parseNativeWindStyles() ---------------------------------------------------------------- */ +/** -i- Util to extract Nativewind's style and/or className from a styled() components style prop */ +export const parseNativeWindStyles = (style: any) => { + return Object.entries(style || {}).reduce( + (acc, [key, value]) => { + // If the key is unsupported, ignore it + if (['mask', 'childClassNames'].includes(key)) return acc + // If the key is a number, it's a Nativewind style + if (Number.isInteger(Number(key))) { + const isCSS = !!(value as Record)['$$css'] + if (isCSS) { + // If it's a CSS object, add as a Nativewind className + const { '$$css': _, ...classNameObjects } = value as Record + const className = [acc.nativeWindClassName, ...Object.values(classNameObjects)].filter(Boolean).join(' ') // prettier-ignore + return { ...acc, nativeWindClassName: className } + } else if (Array.isArray(value)) { + // If it's an array, we should merge the arrays + const flattenedStyles = value.reduce((acc, val) => ({ ...acc, ...val }), {}) + return { ...acc, nativeWindStyles: { ...acc.nativeWindStyles, ...flattenedStyles } } // prettier-ignore + } else { + // If it's a React-Native style object, check if we should merge arrays or objects + return { ...acc, nativeWindStyles: { ...acc.nativeWindStyles, ...(value as Record) } } // prettier-ignore + } + } + // If the key is a string, it's a regular style + return { ...acc, restStyle: { ...acc.restStyle, [key]: value } } + }, + { nativeWindStyles: {}, nativeWindClassName: '', restStyle: {} } + ) +}