diff --git a/packages/uniwind/src/common/utils.ts b/packages/uniwind/src/common/utils.ts new file mode 100644 index 00000000..b5a22b32 --- /dev/null +++ b/packages/uniwind/src/common/utils.ts @@ -0,0 +1 @@ +export const isDefined = (value: T): value is NonNullable => value !== undefined && value !== null diff --git a/packages/uniwind/src/components/native/ActivityIndicator.tsx b/packages/uniwind/src/components/native/ActivityIndicator.tsx index a185448a..0c1820b2 100644 --- a/packages/uniwind/src/components/native/ActivityIndicator.tsx +++ b/packages/uniwind/src/components/native/ActivityIndicator.tsx @@ -1,10 +1,11 @@ import { ActivityIndicator as RNActivityIndicator, ActivityIndicatorProps } from 'react-native' import { copyComponentProperties } from '../utils' +import { useAccentColor } from './useAccentColor' import { useStyle } from './useStyle' export const ActivityIndicator = copyComponentProperties(RNActivityIndicator, (props: ActivityIndicatorProps) => { const style = useStyle(props.className, props) - const color = useStyle(props.colorClassName, props).accentColor + const color = useAccentColor(props.colorClassName, props) return ( { - const color = useStyle(props.colorClassName, props).accentColor + const color = useAccentColor(props.colorClassName, props) return ( ) => { @@ -8,7 +9,7 @@ export const FlatList = copyComponentProperties(RNFlatList, (props: FlatListProp const styleContentContainer = useStyle(props.contentContainerClassName, props) const styleListFooterComponent = useStyle(props.ListFooterComponentClassName, props) const styleListHeaderComponent = useStyle(props.ListHeaderComponentClassName, props) - const endFillColor = useStyle(props.endFillColorClassName, props).accentColor + const endFillColor = useAccentColor(props.endFillColorClassName, props) const hasSingleColumn = !('numColumns' in props) || props.numColumns === 1 return ( diff --git a/packages/uniwind/src/components/native/Image.tsx b/packages/uniwind/src/components/native/Image.tsx index 678cdc4a..8d34df16 100644 --- a/packages/uniwind/src/components/native/Image.tsx +++ b/packages/uniwind/src/components/native/Image.tsx @@ -1,10 +1,11 @@ import { Image as RNImage, ImageProps } from 'react-native' import { copyComponentProperties } from '../utils' +import { useAccentColor } from './useAccentColor' import { useStyle } from './useStyle' export const Image = copyComponentProperties(RNImage, (props: ImageProps) => { const style = useStyle(props.className, props) - const tintColor = useStyle(props.tintColorClassName, props).accentColor + const tintColor = useAccentColor(props.tintColorClassName, props) return ( { const style = useStyle(props.className, props) const imageStyle = useStyle(props.imageClassName, props) - const tintColor = useStyle(props.tintColorClassName, props).accentColor + const tintColor = useAccentColor(props.tintColorClassName, props) return ( }) => { const style = useStyle(props.className, props) - const backgroundColor = useStyle(props.backgroundColorClassName, props).accentColor + const backgroundColor = useAccentColor(props.backgroundColorClassName, props) return ( { const style = useStyle(props.className, props) - const backdropColor = useStyle(props.backdropColorClassName, props).accentColor + const backdropColor = useAccentColor(props.backdropColorClassName, props) return ( { const style = useStyle(props.className, props) - const color = useStyle(props.colorsClassName, props).accentColor - const tintColor = useStyle(props.tintColorClassName, props).accentColor - const titleColor = useStyle(props.titleColorClassName, props).accentColor - const progressBackgroundColor = useStyle(props.progressBackgroundColorClassName, props).accentColor + const color = useAccentColor(props.colorsClassName, props) + const tintColor = useAccentColor(props.tintColorClassName, props) + const titleColor = useAccentColor(props.titleColorClassName, props) + const progressBackgroundColor = useAccentColor(props.progressBackgroundColorClassName, props) return ( { const style = useStyle(props.className, props) const contentContainerStyle = useStyle(props.contentContainerClassName, props) - const endFillColor = useStyle(props.endFillColorClassName, props).accentColor + const endFillColor = useAccentColor(props.endFillColorClassName, props) return ( ) => { @@ -7,7 +8,7 @@ export const SectionList = copyComponentProperties(RNSectionList, (props: Sectio const contentContainerStyle = useStyle(props.contentContainerClassName, props) const listFooterComponentStyle = useStyle(props.ListFooterComponentClassName, props) const listHeaderComponentStyle = useStyle(props.ListHeaderComponentClassName, props) - const endFillColor = useStyle(props.endFillColorClassName, props).accentColor + const endFillColor = useAccentColor(props.endFillColorClassName, props) return ( { @@ -8,10 +9,10 @@ export const Switch = copyComponentProperties(RNSwitch, (props: SwitchProps) => isDisabled: Boolean(props.disabled), } satisfies ComponentState const style = useStyle(props.className, props, state) - const trackColorOn = useStyle(props.trackColorOnClassName, props, state).accentColor - const trackColorOff = useStyle(props.trackColorOffClassName, props, state).accentColor - const thumbColor = useStyle(props.thumbColorClassName, props, state).accentColor - const ios_backgroundColor = useStyle(props.ios_backgroundColorClassName, props, state).accentColor + const trackColorOn = useAccentColor(props.trackColorOnClassName, props, state) + const trackColorOff = useAccentColor(props.trackColorOffClassName, props, state) + const thumbColor = useAccentColor(props.thumbColorClassName, props, state) + const ios_backgroundColor = useAccentColor(props.ios_backgroundColorClassName, props, state) return ( { isDisabled: Boolean(props.disabled), } satisfies ComponentState const style = useStyle(props.className, props, state) - const selectionColor = useStyle(props.selectionColorClassName, props, state).accentColor + const selectionColor = useAccentColor(props.selectionColorClassName, props, state) return ( { @@ -13,11 +14,11 @@ export const TextInput = copyComponentProperties(RNTextInput, (props: TextInputP isPressed, } satisfies ComponentState const style = useStyle(props.className, props, state) - const cursorColor = useStyle(props.cursorColorClassName, props, state).accentColor - const selectionColor = useStyle(props.selectionColorClassName, props, state).accentColor - const placeholderTextColor = useStyle(props.placeholderTextColorClassName, props, state).accentColor - const selectionHandleColor = useStyle(props.selectionHandleColorClassName, props, state).accentColor - const underlineColorAndroid = useStyle(props.underlineColorAndroidClassName, props, state).accentColor + const cursorColor = useAccentColor(props.cursorColorClassName, props, state) + const selectionColor = useAccentColor(props.selectionColorClassName, props, state) + const placeholderTextColor = useAccentColor(props.placeholderTextColorClassName, props, state) + const selectionHandleColor = useAccentColor(props.selectionHandleColorClassName, props, state) + const underlineColorAndroid = useAccentColor(props.underlineColorAndroidClassName, props, state) return ( { @@ -13,7 +14,7 @@ export const TouchableHighlight = copyComponentProperties(RNTouchableHighlight, isFocused, } satisfies ComponentState const style = useStyle(props.className, props, state) - const underlayColor = useStyle(props.underlayColorClassName, props, state).accentColor + const underlayColor = useAccentColor(props.underlayColorClassName, props, state) return ( ) => { @@ -7,7 +8,7 @@ export const VirtualizedList = copyComponentProperties(RNVirtualizedList, (props const contentContainerStyle = useStyle(props.contentContainerClassName, props) const listFooterComponentStyle = useStyle(props.ListFooterComponentClassName, props) const listHeaderComponentStyle = useStyle(props.ListHeaderComponentClassName, props) - const endFillColor = useStyle(props.endFillColorClassName, props).accentColor + const endFillColor = useAccentColor(props.endFillColorClassName, props) return ( , state?: ComponentState) => { + const styles = useStyle(className, componentProps, state) + + if (__DEV__ && !warnedOnce && isDefined(className) && className.trim() !== '' && styles.accentColor === undefined) { + warnedOnce = true + Logger.warn( + `className '${className}' was provided to extract accentColor but no color was found. Make sure the className includes a color utility (e.g., 'accent-red-500', 'accent-blue-600').`, + ) + } + + return styles.accentColor +} diff --git a/packages/uniwind/src/components/web/ActivityIndicator.tsx b/packages/uniwind/src/components/web/ActivityIndicator.tsx index 9aadbef7..d78dbe1a 100644 --- a/packages/uniwind/src/components/web/ActivityIndicator.tsx +++ b/packages/uniwind/src/components/web/ActivityIndicator.tsx @@ -1,8 +1,8 @@ import { ActivityIndicator as RNActivityIndicator, ActivityIndicatorProps } from 'react-native' -import { useUniwindAccent } from '../../hooks' import { copyComponentProperties } from '../utils' import { generateDataSet } from './generateDataSet' import { toRNWClassName } from './rnw' +import { useUniwindAccent } from './useUniwindAccent' export const ActivityIndicator = copyComponentProperties(RNActivityIndicator, (props: ActivityIndicatorProps) => { const color = useUniwindAccent(props.colorClassName) diff --git a/packages/uniwind/src/components/web/Button.tsx b/packages/uniwind/src/components/web/Button.tsx index c4dfbceb..4dff979a 100644 --- a/packages/uniwind/src/components/web/Button.tsx +++ b/packages/uniwind/src/components/web/Button.tsx @@ -1,7 +1,7 @@ import { Button as RNButton, ButtonProps } from 'react-native' -import { useUniwindAccent } from '../../hooks' import { copyComponentProperties } from '../utils' import { generateDataSet } from './generateDataSet' +import { useUniwindAccent } from './useUniwindAccent' export const Button = copyComponentProperties(RNButton, (props: ButtonProps) => { const color = useUniwindAccent(props.colorClassName) diff --git a/packages/uniwind/src/components/web/Image.tsx b/packages/uniwind/src/components/web/Image.tsx index 13684855..c77d8e51 100644 --- a/packages/uniwind/src/components/web/Image.tsx +++ b/packages/uniwind/src/components/web/Image.tsx @@ -1,8 +1,9 @@ import { Image as RNImage, ImageProps } from 'react-native' -import { useResolveClassNames, useUniwindAccent } from '../../hooks' +import { useResolveClassNames } from '../../hooks' import { copyComponentProperties } from '../utils' import { generateDataSet } from './generateDataSet' import { toRNWClassName } from './rnw' +import { useUniwindAccent } from './useUniwindAccent' export const Image = copyComponentProperties(RNImage, (props: ImageProps) => { const tintColor = useUniwindAccent(props.tintColorClassName) diff --git a/packages/uniwind/src/components/web/ImageBackground.tsx b/packages/uniwind/src/components/web/ImageBackground.tsx index 87cefe94..b082cd93 100644 --- a/packages/uniwind/src/components/web/ImageBackground.tsx +++ b/packages/uniwind/src/components/web/ImageBackground.tsx @@ -1,8 +1,8 @@ import { ImageBackground as RNImageBackground, ImageBackgroundProps } from 'react-native' -import { useUniwindAccent } from '../../hooks' import { copyComponentProperties } from '../utils' import { generateDataSet } from './generateDataSet' import { toRNWClassName } from './rnw' +import { useUniwindAccent } from './useUniwindAccent' export const ImageBackground = copyComponentProperties(RNImageBackground, (props: ImageBackgroundProps) => { const tintColor = useUniwindAccent(props.tintColorClassName) diff --git a/packages/uniwind/src/components/web/Switch.tsx b/packages/uniwind/src/components/web/Switch.tsx index ba7a5881..760f751f 100644 --- a/packages/uniwind/src/components/web/Switch.tsx +++ b/packages/uniwind/src/components/web/Switch.tsx @@ -1,8 +1,8 @@ import { Switch as RNSwitch, SwitchProps } from 'react-native' -import { useUniwindAccent } from '../../hooks' import { copyComponentProperties } from '../utils' import { generateDataSet } from './generateDataSet' import { toRNWClassName } from './rnw' +import { useUniwindAccent } from './useUniwindAccent' export const Switch = copyComponentProperties(RNSwitch, (props: SwitchProps) => { const trackColorOn = useUniwindAccent(props.trackColorOnClassName) diff --git a/packages/uniwind/src/components/web/TextInput.tsx b/packages/uniwind/src/components/web/TextInput.tsx index 00f29399..a4f4dca2 100644 --- a/packages/uniwind/src/components/web/TextInput.tsx +++ b/packages/uniwind/src/components/web/TextInput.tsx @@ -1,8 +1,8 @@ import { TextInput as RNTextInput, TextInputProps } from 'react-native' -import { useUniwindAccent } from '../../hooks' import { copyComponentProperties } from '../utils' import { generateDataSet } from './generateDataSet' import { toRNWClassName } from './rnw' +import { useUniwindAccent } from './useUniwindAccent' export const TextInput = copyComponentProperties(RNTextInput, (props: TextInputProps) => { const placeholderTextColor = useUniwindAccent(props.placeholderTextColorClassName) diff --git a/packages/uniwind/src/components/web/TouchableHighlight.tsx b/packages/uniwind/src/components/web/TouchableHighlight.tsx index c4d7d94c..c7fa451f 100644 --- a/packages/uniwind/src/components/web/TouchableHighlight.tsx +++ b/packages/uniwind/src/components/web/TouchableHighlight.tsx @@ -1,8 +1,8 @@ import { TouchableHighlight as RNTouchableHighlight, TouchableHighlightProps } from 'react-native' -import { useUniwindAccent } from '../../hooks' import { copyComponentProperties } from '../utils' import { generateDataSet } from './generateDataSet' import { toRNWClassName } from './rnw' +import { useUniwindAccent } from './useUniwindAccent' export const TouchableHighlight = copyComponentProperties(RNTouchableHighlight, (props: TouchableHighlightProps) => { const underlayColor = useUniwindAccent(props.underlayColorClassName) diff --git a/packages/uniwind/src/components/web/useUniwindAccent.ts b/packages/uniwind/src/components/web/useUniwindAccent.ts new file mode 100644 index 00000000..49cf29ce --- /dev/null +++ b/packages/uniwind/src/components/web/useUniwindAccent.ts @@ -0,0 +1,21 @@ +import { isDefined } from '../../common/utils' +import { Logger } from '../../core/logger' +import { formatColor } from '../../core/web/formatColor' +import { useResolveClassNames } from '../../hooks/useResolveClassNames' + +let warnedOnce = false + +export const useUniwindAccent = (className: string | undefined) => { + const styles = useResolveClassNames(className ?? '') + + if (__DEV__ && !warnedOnce && isDefined(className) && className.trim() !== '' && styles.accentColor === undefined) { + warnedOnce = true + Logger.warn( + `className '${className}' was provided to extract accentColor but no color was found. Make sure the className includes a color utility (e.g., 'accent-red-500', 'accent-blue-600').`, + ) + } + + return styles.accentColor !== undefined + ? formatColor(styles.accentColor) + : undefined +} diff --git a/packages/uniwind/src/hoc/withUniwind.native.tsx b/packages/uniwind/src/hoc/withUniwind.native.tsx index c6a11e4e..54d4daf1 100644 --- a/packages/uniwind/src/hoc/withUniwind.native.tsx +++ b/packages/uniwind/src/hoc/withUniwind.native.tsx @@ -1,11 +1,15 @@ import { ComponentProps, useLayoutEffect, useReducer } from 'react' +import { isDefined } from '../common/utils' import { useUniwindContext } from '../core/context' import { UniwindListener } from '../core/listener' +import { Logger } from '../core/logger' import { UniwindStore } from '../core/native' import { StyleDependency } from '../types' import { AnyObject, Component, OptionMapping, WithUniwind } from './types' import { classToColor, classToStyle, isClassProperty, isColorClassProperty, isStyleProperty } from './withUniwindUtils' +let warnedOnce = false + export const withUniwind: WithUniwind = < TComponent extends Component, TOptions extends Record, OptionMapping>, @@ -28,6 +32,13 @@ const withAutoUniwind = (Component: Component) => (props: AnyObject) const { styles, dependencies } = UniwindStore.getStyles(propValue, props, undefined, uniwindContext) + if (__DEV__ && !warnedOnce && isDefined(propValue) && propValue.trim() !== '' && styles.accentColor === undefined) { + warnedOnce = true + Logger.warn( + `className '${propValue}' was provided to extract accentColor but no color was found. Make sure the className includes a color utility (e.g., 'accent-red-500', 'accent-blue-600').`, + ) + } + acc.dependencies.push(...dependencies) acc.generatedProps[colorProp] = styles.accentColor diff --git a/packages/uniwind/src/hoc/withUniwind.tsx b/packages/uniwind/src/hoc/withUniwind.tsx index 7a9a20c0..4fd05035 100644 --- a/packages/uniwind/src/hoc/withUniwind.tsx +++ b/packages/uniwind/src/hoc/withUniwind.tsx @@ -1,10 +1,14 @@ import { ComponentProps, useLayoutEffect, useReducer } from 'react' +import { isDefined } from '../common/utils' import { generateDataSet } from '../components/web/generateDataSet' import { useUniwindContext } from '../core/context' +import { Logger } from '../core/logger' import { CSSListener, formatColor, getWebStyles } from '../core/web' import { AnyObject, Component, OptionMapping, WithUniwind } from './types' import { classToColor, classToStyle, isClassProperty, isColorClassProperty, isStyleProperty } from './withUniwindUtils' +let warnedOnce = false + export const withUniwind: WithUniwind = < TComponent extends Component, TOptions extends Record, OptionMapping>, @@ -29,6 +33,13 @@ const withAutoUniwind = (Component: Component) => (props: AnyObject) const className = propValue const color = getWebStyles(className, props, uniwindContext).accentColor + if (__DEV__ && !warnedOnce && isDefined(className) && className.trim() !== '' && color === undefined) { + warnedOnce = true + Logger.warn( + `className '${className}' was provided to extract accentColor but no color was found. Make sure the className includes a color utility (e.g., 'accent-red-500', 'accent-blue-600').`, + ) + } + acc.generatedProps[colorProp] = color !== undefined ? formatColor(color) : undefined diff --git a/packages/uniwind/src/hooks/index.ts b/packages/uniwind/src/hooks/index.ts index d3f75b5d..e1c1da45 100644 --- a/packages/uniwind/src/hooks/index.ts +++ b/packages/uniwind/src/hooks/index.ts @@ -1,4 +1,3 @@ export * from './useCSSVariable' export * from './useResolveClassNames' export * from './useUniwind' -export * from './useUniwindAccent' diff --git a/packages/uniwind/src/hooks/useUniwindAccent.ts b/packages/uniwind/src/hooks/useUniwindAccent.ts deleted file mode 100644 index 532b6c3c..00000000 --- a/packages/uniwind/src/hooks/useUniwindAccent.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { formatColor } from '../core/web/formatColor' -import { useResolveClassNames } from './useResolveClassNames' - -export const useUniwindAccent = (className: string | undefined) => { - const styles = useResolveClassNames(className ?? '') - - return styles.accentColor !== undefined - ? formatColor(styles.accentColor) - : undefined -} diff --git a/packages/uniwind/src/metro/addMetaToStylesTemplate.ts b/packages/uniwind/src/metro/addMetaToStylesTemplate.ts index a3ef3096..f4c2a63d 100644 --- a/packages/uniwind/src/metro/addMetaToStylesTemplate.ts +++ b/packages/uniwind/src/metro/addMetaToStylesTemplate.ts @@ -1,8 +1,9 @@ import { Platform } from '../common/consts' +import { isDefined } from '../common/utils' import { StyleDependency } from '../types' import { ProcessorBuilder } from './processor' import { StyleSheetTemplate } from './types' -import { isDefined, serialize, toCamelCase } from './utils' +import { serialize, toCamelCase } from './utils' const extractVarsFromString = (value: string) => { const thisIndexes = [...value.matchAll(/this\[/g)].map(m => m.index) diff --git a/packages/uniwind/src/metro/processor/css.ts b/packages/uniwind/src/metro/processor/css.ts index b3f7f79e..85f5cd05 100644 --- a/packages/uniwind/src/metro/processor/css.ts +++ b/packages/uniwind/src/metro/processor/css.ts @@ -1,7 +1,8 @@ import { OverflowKeyword } from 'lightningcss' +import { isDefined } from '../../common/utils' import { Logger } from '../logger' import { DeclarationValues } from '../types' -import { deepEqual, isDefined, pipe, roundToPrecision, shouldBeSerialized } from '../utils' +import { deepEqual, pipe, roundToPrecision, shouldBeSerialized } from '../utils' import type { ProcessorBuilder } from './processor' export class CSS { diff --git a/packages/uniwind/src/metro/processor/rn.ts b/packages/uniwind/src/metro/processor/rn.ts index 948a936f..d347a346 100644 --- a/packages/uniwind/src/metro/processor/rn.ts +++ b/packages/uniwind/src/metro/processor/rn.ts @@ -1,4 +1,5 @@ -import { addMissingSpaces, isDefined, pipe, removeKeys, toCamelCase } from '../utils' +import { isDefined } from '../../common/utils' +import { addMissingSpaces, pipe, removeKeys, toCamelCase } from '../utils' import type { ProcessorBuilder } from './processor' const cssToRNMap: Record Record> = { diff --git a/packages/uniwind/src/metro/utils/common.ts b/packages/uniwind/src/metro/utils/common.ts index 9299501c..be2b16d3 100644 --- a/packages/uniwind/src/metro/utils/common.ts +++ b/packages/uniwind/src/metro/utils/common.ts @@ -1,5 +1,3 @@ -export const isDefined = (value: T): value is NonNullable => value !== null && value !== undefined - export const toCamelCase = (str: string) => str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()) type P = (data: I) => O diff --git a/packages/uniwind/tests/setup.web.ts b/packages/uniwind/tests/setup.web.ts index 18c9bdec..557f43fc 100644 --- a/packages/uniwind/tests/setup.web.ts +++ b/packages/uniwind/tests/setup.web.ts @@ -1,5 +1,10 @@ import '@testing-library/jest-dom' +{ + // Define __DEV__ global for tests + ;(globalThis as any).__DEV__ = true +} + // Mock CSS-related globals that JSDOM doesn't define if (typeof CSSRuleList === 'undefined') { global.CSSRuleList = class CSSRuleList {} as any