@utilitywarehouse/hearth-react-native 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-lint.log +1 -1
  3. package/CHANGELOG.md +10 -0
  4. package/build/components/Banner/Banner.context.d.ts +7 -0
  5. package/build/components/Banner/Banner.context.js +8 -0
  6. package/build/components/Banner/Banner.js +10 -40
  7. package/build/components/Banner/Banner.props.d.ts +3 -5
  8. package/build/components/Banner/BannerIllustration.d.ts +4 -0
  9. package/build/components/Banner/BannerIllustration.js +53 -0
  10. package/build/components/Banner/BannerImage.d.ts +4 -0
  11. package/build/components/Banner/BannerImage.js +53 -0
  12. package/build/components/Banner/index.d.ts +2 -0
  13. package/build/components/Banner/index.js +2 -0
  14. package/build/components/Card/CardAction/CardAction.props.d.ts +2 -3
  15. package/build/components/Card/CardAction/CardActionRoot.js +1 -2
  16. package/build/components/Checkbox/Checkbox.js +1 -2
  17. package/build/components/Checkbox/Checkbox.props.d.ts +3 -3
  18. package/build/components/Checkbox/CheckboxImage.d.ts +2 -1
  19. package/build/components/Checkbox/CheckboxImage.js +8 -1
  20. package/build/components/ExpandableCard/ExpandableCard.props.d.ts +1 -2
  21. package/build/components/ExpandableCard/ExpandableCardTrigger.props.d.ts +4 -5
  22. package/build/components/ExpandableCard/ExpandableCardTriggerRoot.js +1 -14
  23. package/build/components/HighlightBanner/HighlightBanner.js +2 -6
  24. package/build/components/HighlightBanner/HighlightBanner.props.d.ts +2 -3
  25. package/build/components/HighlightBanner/HighlightBannerImage.d.ts +4 -0
  26. package/build/components/HighlightBanner/HighlightBannerImage.js +18 -0
  27. package/build/components/HighlightBanner/index.d.ts +1 -0
  28. package/build/components/HighlightBanner/index.js +1 -0
  29. package/build/components/Input/Input.d.ts +5 -7
  30. package/build/components/Input/Input.js +11 -4
  31. package/build/components/Input/InputField.d.ts +4 -7
  32. package/build/components/Input/InputField.js +6 -5
  33. package/build/components/List/ListItem/ListItem.props.d.ts +2 -2
  34. package/build/components/List/ListItem/ListItemRoot.js +1 -2
  35. package/build/components/Modal/Modal.js +2 -6
  36. package/build/components/Modal/Modal.props.d.ts +3 -2
  37. package/build/components/Modal/Modal.web.js +2 -6
  38. package/build/components/Modal/ModalImage.d.ts +4 -0
  39. package/build/components/Modal/ModalImage.js +18 -0
  40. package/build/components/Modal/index.d.ts +1 -0
  41. package/build/components/Modal/index.js +1 -0
  42. package/build/components/Radio/Radio.js +1 -2
  43. package/build/components/Radio/Radio.props.d.ts +3 -3
  44. package/build/components/Radio/RadioImage.d.ts +2 -1
  45. package/build/components/Radio/RadioImage.js +8 -1
  46. package/build/utils/index.d.ts +2 -1
  47. package/build/utils/index.js +2 -1
  48. package/build/utils/isThemedImageProps.d.ts +4 -0
  49. package/build/utils/isThemedImageProps.js +4 -0
  50. package/package.json +2 -2
  51. package/src/components/Banner/Banner.context.ts +11 -0
  52. package/src/components/Banner/Banner.docs.mdx +55 -37
  53. package/src/components/Banner/Banner.props.ts +3 -5
  54. package/src/components/Banner/Banner.stories.tsx +86 -57
  55. package/src/components/Banner/Banner.tsx +24 -67
  56. package/src/components/Banner/BannerIllustration.tsx +63 -0
  57. package/src/components/Banner/BannerImage.tsx +63 -0
  58. package/src/components/Banner/index.ts +2 -0
  59. package/src/components/Card/Card.docs.mdx +4 -4
  60. package/src/components/Card/CardAction/CardAction.props.ts +2 -3
  61. package/src/components/Card/CardAction/CardAction.stories.tsx +4 -3
  62. package/src/components/Card/CardAction/CardActionRoot.tsx +4 -5
  63. package/src/components/Checkbox/Checkbox.docs.mdx +23 -4
  64. package/src/components/Checkbox/Checkbox.props.ts +3 -3
  65. package/src/components/Checkbox/Checkbox.stories.tsx +14 -8
  66. package/src/components/Checkbox/Checkbox.tsx +1 -2
  67. package/src/components/Checkbox/CheckboxImage.tsx +8 -3
  68. package/src/components/ExpandableCard/ExpandableCard.docs.mdx +2 -2
  69. package/src/components/ExpandableCard/ExpandableCard.props.ts +1 -2
  70. package/src/components/ExpandableCard/ExpandableCard.stories.tsx +3 -3
  71. package/src/components/ExpandableCard/ExpandableCardTrigger.props.ts +4 -5
  72. package/src/components/ExpandableCard/ExpandableCardTriggerRoot.tsx +2 -17
  73. package/src/components/HighlightBanner/HighlightBanner.docs.mdx +73 -42
  74. package/src/components/HighlightBanner/HighlightBanner.props.ts +2 -3
  75. package/src/components/HighlightBanner/HighlightBanner.stories.tsx +85 -60
  76. package/src/components/HighlightBanner/HighlightBanner.tsx +3 -10
  77. package/src/components/HighlightBanner/HighlightBannerImage.tsx +20 -0
  78. package/src/components/HighlightBanner/index.ts +1 -0
  79. package/src/components/Input/Input.stories.tsx +76 -3
  80. package/src/components/Input/Input.tsx +110 -98
  81. package/src/components/Input/InputField.tsx +27 -26
  82. package/src/components/List/List.docs.mdx +15 -9
  83. package/src/components/List/List.stories.tsx +2 -2
  84. package/src/components/List/ListItem/ListItem.props.ts +2 -2
  85. package/src/components/List/ListItem/ListItemRoot.tsx +2 -3
  86. package/src/components/Modal/Modal.docs.mdx +16 -4
  87. package/src/components/Modal/Modal.props.ts +3 -2
  88. package/src/components/Modal/Modal.stories.tsx +2 -5
  89. package/src/components/Modal/Modal.tsx +2 -6
  90. package/src/components/Modal/Modal.web.tsx +2 -6
  91. package/src/components/Modal/ModalImage.tsx +20 -0
  92. package/src/components/Modal/index.ts +1 -0
  93. package/src/components/PillGroup/PillGroup.stories.tsx +1 -1
  94. package/src/components/Radio/Radio.docs.mdx +21 -8
  95. package/src/components/Radio/Radio.props.ts +3 -3
  96. package/src/components/Radio/Radio.stories.tsx +15 -11
  97. package/src/components/Radio/Radio.tsx +1 -2
  98. package/src/components/Radio/RadioImage.tsx +8 -3
  99. package/src/utils/index.ts +2 -1
  100. package/src/utils/isThemedImageProps.ts +8 -0
@@ -1,8 +1,5 @@
1
- import { TextInputProps } from 'react-native';
2
- declare const InputField: {
3
- ({ style, inBottomSheet, ...props }: TextInputProps & {
4
- inBottomSheet?: boolean;
5
- }): import("react/jsx-runtime").JSX.Element;
6
- displayName: string;
7
- };
1
+ import { TextInput as RNTextInput, TextInputProps } from 'react-native';
2
+ declare const InputField: import("react").ForwardRefExoticComponent<TextInputProps & {
3
+ inBottomSheet?: boolean;
4
+ } & import("react").RefAttributes<RNTextInput>>;
8
5
  export default InputField;
@@ -1,20 +1,21 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { BottomSheetTextInput } from '@gorhom/bottom-sheet';
3
+ import { forwardRef } from 'react';
3
4
  import { TextInput as RNTextInput } from 'react-native';
4
5
  import { StyleSheet } from 'react-native-unistyles';
5
6
  import { useTheme } from '../../hooks';
6
7
  import { useInputContext } from './Input.context';
7
- const InputField = ({ style, inBottomSheet = false, ...props }) => {
8
+ const InputField = forwardRef(({ style, inBottomSheet = false, ...props }, ref) => {
8
9
  const { disabled, focused = false, type } = useInputContext();
9
10
  styles.useVariants({ focused, type });
10
11
  const { color } = useTheme();
11
12
  if (inBottomSheet) {
12
13
  return (
13
- // @ts-expect-error - BottomSheetTextInput type issue
14
- _jsx(BottomSheetTextInput, { placeholderTextColor: color.text.secondary, selectionColor: color.purple[700], cursorColor: color.purple[700], verticalAlign: "middle", "aria-disabled": disabled, ...props, style: [styles.input, style] }));
14
+ // @ts-expect-error - BottomSheetTextInput has incompatible event types with TextInput
15
+ _jsx(BottomSheetTextInput, { ref: ref, placeholderTextColor: color.text.secondary, selectionColor: color.surface.brand.default, cursorColor: color.surface.brand.default, verticalAlign: "middle", "aria-disabled": disabled, ...props, style: [styles.input, style] }));
15
16
  }
16
- return (_jsx(RNTextInput, { placeholderTextColor: color.text.secondary, selectionColor: color.purple[700], cursorColor: color.purple[700], verticalAlign: "middle", "aria-disabled": disabled, ...props, style: [styles.input, style] }));
17
- };
17
+ return (_jsx(RNTextInput, { ref: ref, placeholderTextColor: color.text.secondary, selectionColor: color.surface.brand.default, cursorColor: color.surface.brand.default, verticalAlign: "middle", "aria-disabled": disabled, ...props, style: [styles.input, style] }));
18
+ });
18
19
  InputField.displayName = 'InputField';
19
20
  const styles = StyleSheet.create(theme => ({
20
21
  input: {
@@ -1,5 +1,5 @@
1
+ import { ReactNode } from 'react';
1
2
  import type { PressableProps, ViewProps } from 'react-native';
2
- import BadgeProps from '../../Badge/Badge.props';
3
3
  interface ListItemBaseProps extends Omit<PressableProps, 'children'> {
4
4
  loading?: boolean;
5
5
  disabled?: boolean;
@@ -23,7 +23,7 @@ export interface ListItemWithoutChildren extends ListItemBaseProps {
23
23
  leadingContent?: ViewProps['children'];
24
24
  trailingContent?: ViewProps['children'];
25
25
  numericValue?: string | number;
26
- badge?: BadgeProps;
26
+ badge?: ReactNode;
27
27
  badgePosition?: 'top' | 'bottom';
28
28
  }
29
29
  type ListItemProps = ListItemWithChildren | ListItemWithoutChildren;
@@ -3,7 +3,6 @@ import { ChevronRightSmallIcon } from '@utilitywarehouse/hearth-react-native-ico
3
3
  import { useMemo } from 'react';
4
4
  import { Pressable } from 'react-native';
5
5
  import { StyleSheet } from 'react-native-unistyles';
6
- import { Badge } from '../../Badge';
7
6
  import { DetailText } from '../../DetailText';
8
7
  import { Skeleton } from '../../Skeleton';
9
8
  import { useListContext } from '../List.context';
@@ -53,7 +52,7 @@ const ListItemRoot = ({ heading, helperText, leadingContent, trailingContent, di
53
52
  if (loading || listContext?.loading) {
54
53
  return (_jsxs(Pressable, { ...props, testID: loadingTestID, style: [styles.container, props.style], disabled: isDisabled, children: [leadingContent ? _jsx(Skeleton, { width: 24, height: 24 }) : null, _jsxs(ListItemContent, { children: [_jsx(Skeleton, { width: "80%", height: 20 }), _jsx(Skeleton, { width: "100%", height: 16 })] }), onPress || trailingContent ? _jsx(Skeleton, { width: 24, height: 24 }) : null] }));
55
54
  }
56
- return (_jsx(ListItemContext.Provider, { value: value, children: _jsx(Pressable, { ...props, testID: testID, style: [styles.container, props.style], disabled: isDisabled, accessibilityRole: onPress ? 'button' : undefined, children: children ? (children) : (_jsxs(_Fragment, { children: [leadingContent ? (_jsx(ListItemLeadingContent, { children: leadingContent })) : null, _jsxs(ListItemContent, { children: [badgePosition === 'top' && badge ? _jsx(Badge, { ...badge }) : null, _jsx(ListItemText, { children: heading }), helperText ? _jsx(ListItemHelperText, { children: helperText }) : null, badgePosition === 'bottom' && badge ? _jsx(Badge, { ...badge }) : null] }), !!numericValue && _jsx(DetailText, { size: "lg", children: numericValue }), trailingContent ? (_jsx(ListItemTrailingContent, { children: trailingContent })) : onPress ? (_jsx(ListItemTrailingContent, { style: styles.centeredTrailingIcon, children: _jsx(ListItemTrailingIcon, { as: ChevronRightSmallIcon }) })) : null] })) }) }));
55
+ return (_jsx(ListItemContext.Provider, { value: value, children: _jsx(Pressable, { ...props, testID: testID, style: [styles.container, props.style], disabled: isDisabled, accessibilityRole: onPress ? 'button' : undefined, children: children ? (children) : (_jsxs(_Fragment, { children: [leadingContent ? (_jsx(ListItemLeadingContent, { children: leadingContent })) : null, _jsxs(ListItemContent, { children: [badgePosition === 'top' && badge ? badge : null, _jsx(ListItemText, { children: heading }), helperText ? _jsx(ListItemHelperText, { children: helperText }) : null, badgePosition === 'bottom' && badge ? badge : null] }), !!numericValue && _jsx(DetailText, { size: "lg", children: numericValue }), trailingContent ? (_jsx(ListItemTrailingContent, { children: trailingContent })) : onPress ? (_jsx(ListItemTrailingContent, { style: styles.centeredTrailingIcon, children: _jsx(ListItemTrailingIcon, { as: ChevronRightSmallIcon }) })) : null] })) }) }));
57
56
  };
58
57
  ListItemRoot.displayName = 'ListItemRoot';
59
58
  const styles = StyleSheet.create(theme => ({
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { CloseMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
3
3
  import { useCallback, useEffect, useImperativeHandle, useRef } from 'react';
4
- import { AccessibilityInfo, Image, Platform, View, findNodeHandle } from 'react-native';
4
+ import { AccessibilityInfo, Platform, View, findNodeHandle } from 'react-native';
5
5
  import Animated, { Easing, useAnimatedStyle, useSharedValue, withDelay, withTiming, } from 'react-native-reanimated';
6
6
  import { StyleSheet } from 'react-native-unistyles';
7
7
  import { useTheme } from '../../hooks';
@@ -98,7 +98,7 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
98
98
  }
99
99
  };
100
100
  styles.useVariants({ loading });
101
- const content = (_jsx(_Fragment, { children: loading ? (_jsxs(View, { style: styles.loadingContainer, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? 'Loading' : undefined, screenReaderFocusable: true, ref: viewRef, children: [_jsx(Spinner, { size: "lg" }), _jsx(Heading, { size: "lg", textAlign: "center", children: "Loading..." })] })) : (_jsxs(View, { style: styles.container, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? 'Modal content' : undefined, screenReaderFocusable: true, ref: viewRef, children: [_jsxs(View, { style: styles.header, children: [_jsxs(View, { style: styles.headerTextContent, children: [heading && !image ? (_jsx(Heading, { size: "lg", accessible: true, children: heading })) : null, description && !image ? _jsx(BodyText, { accessible: true, children: description }) : null] }), showCloseButton ? (_jsx(UnstyledIconButton, { icon: CloseMediumIcon, onPress: handleCloseButtonPress, accessibilityLabel: "Close modal", ...closeButtonProps })) : null] }), image ? (_jsxs(View, { style: styles.imageContainer, children: [_jsx(Image, { style: styles.image, ...image }), _jsxs(View, { style: styles.textContent, children: [heading ? (_jsx(Heading, { size: "lg", textAlign: "center", accessible: true, children: heading })) : null, description ? (_jsx(BodyText, { textAlign: "center", accessible: true, children: description })) : null] })] })) : null, children, _jsxs(View, { style: styles.footer, children: [onPressPrimaryButton && primaryButtonText ? (_jsx(Button, { onPress: handlePrimaryButtonPress, text: primaryButtonText, ...primaryButtonProps, variant: primaryButtonProps?.variant ?? 'solid', colorScheme: primaryButtonProps?.colorScheme ?? 'highlight' })) : null, onPressSecondaryButton && secondaryButtonText ? (_jsx(Button, { onPress: handleSecondaryButtonPress, text: secondaryButtonText, ...secondaryButtonProps, variant: secondaryButtonProps?.variant ?? 'outline', colorScheme: secondaryButtonProps?.colorScheme ?? 'functional' })) : null] })] })) }));
101
+ const content = (_jsx(_Fragment, { children: loading ? (_jsxs(View, { style: styles.loadingContainer, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? 'Loading' : undefined, screenReaderFocusable: true, ref: viewRef, children: [_jsx(Spinner, { size: "lg" }), _jsx(Heading, { size: "lg", textAlign: "center", children: "Loading..." })] })) : (_jsxs(View, { style: styles.container, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? 'Modal content' : undefined, screenReaderFocusable: true, ref: viewRef, children: [_jsxs(View, { style: styles.header, children: [_jsxs(View, { style: styles.headerTextContent, children: [heading && !image ? (_jsx(Heading, { size: "lg", accessible: true, children: heading })) : null, description && !image ? _jsx(BodyText, { accessible: true, children: description }) : null] }), showCloseButton ? (_jsx(UnstyledIconButton, { icon: CloseMediumIcon, onPress: handleCloseButtonPress, accessibilityLabel: "Close modal", ...closeButtonProps })) : null] }), image ? (_jsxs(View, { style: styles.imageContainer, children: [image, _jsxs(View, { style: styles.textContent, children: [heading ? (_jsx(Heading, { size: "lg", textAlign: "center", accessible: true, children: heading })) : null, description ? (_jsx(BodyText, { textAlign: "center", accessible: true, children: description })) : null] })] })) : null, children, _jsxs(View, { style: styles.footer, children: [onPressPrimaryButton && primaryButtonText ? (_jsx(Button, { onPress: handlePrimaryButtonPress, text: primaryButtonText, ...primaryButtonProps, variant: primaryButtonProps?.variant ?? 'solid', colorScheme: primaryButtonProps?.colorScheme ?? 'highlight' })) : null, onPressSecondaryButton && secondaryButtonText ? (_jsx(Button, { onPress: handleSecondaryButtonPress, text: secondaryButtonText, ...secondaryButtonProps, variant: secondaryButtonProps?.variant ?? 'outline', colorScheme: secondaryButtonProps?.colorScheme ?? 'functional' })) : null] })] })) }));
102
102
  return inNavModal ? (_jsxs(View, { style: { flex: 1, backgroundColor: theme.color.background.primary }, children: [Platform.OS === 'android' ? (_jsx(Animated.View, { style: [styles.androidContainer, animatedBackgroundStyle], children: _jsx(Animated.View, { style: [styles.pretendContent, animatedPretendContentStyle] }) })) : null, _jsx(Animated.View, { style: [styles.inNavModalContainer, Platform.OS === 'android' && animatedInNavModalStyle], children: _jsx(View, { style: styles.inNavModalContent, children: content }) })] })) : (_jsxs(BottomSheetModal, { ref: bottomSheetModalRef, enableDynamicSizing: true, snapPoints: image || fullscreen ? ['90%'] : props.snapPoints, showHandle: typeof loading !== 'undefined' && loading ? false : props.showHandle, accessible: false, style: styles.modal, ...props, onChange: handleChange, children: [loading ? _jsx(View, { style: styles.loadingTop }) : null, _jsx(BottomSheetScrollView, { contentContainerStyle: styles.container, ref: scrollViewRef, children: content })] }));
103
103
  };
104
104
  const styles = StyleSheet.create((theme, rt) => ({
@@ -124,10 +124,6 @@ const styles = StyleSheet.create((theme, rt) => ({
124
124
  flex: 1,
125
125
  gap: theme.components.modal.content.gap,
126
126
  },
127
- image: {
128
- width: 260,
129
- height: 260,
130
- },
131
127
  imageContainer: {
132
128
  alignItems: 'center',
133
129
  flex: 1,
@@ -1,10 +1,11 @@
1
- import { ImageProps, ViewProps } from 'react-native';
1
+ import { ReactNode } from 'react';
2
+ import { ViewProps } from 'react-native';
2
3
  import { BottomSheetProps } from '../BottomSheet';
3
4
  import { ButtonWithoutChildrenProps } from '../Button/Button.props';
4
5
  import { UnstyledIconButtonProps } from '../UnstyledIconButton';
5
6
  interface ModalProps extends Omit<BottomSheetProps, 'children'> {
6
7
  loading?: boolean;
7
- image?: ImageProps;
8
+ image?: ReactNode;
8
9
  showCloseButton?: boolean;
9
10
  heading?: string;
10
11
  description?: string;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { CloseMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
3
3
  import { useCallback, useEffect, useImperativeHandle, useRef } from 'react';
4
- import { AccessibilityInfo, Image, Platform, View, findNodeHandle } from 'react-native';
4
+ import { AccessibilityInfo, Platform, View, findNodeHandle } from 'react-native';
5
5
  import Animated, { Easing, useAnimatedStyle, useSharedValue, withDelay, withTiming, } from 'react-native-reanimated';
6
6
  import { StyleSheet } from 'react-native-unistyles';
7
7
  import { useTheme } from '../../hooks';
@@ -97,7 +97,7 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
97
97
  bottomSheetModalRef.current?.dismiss();
98
98
  }
99
99
  };
100
- const content = (_jsx(_Fragment, { children: loading ? (_jsxs(View, { style: styles.loadingContainer, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? 'Loading' : undefined, screenReaderFocusable: true, ref: viewRef, children: [_jsx(Spinner, { size: "lg" }), _jsx(Heading, { size: "lg", textAlign: "center", children: "Loading..." })] })) : (_jsxs(View, { style: styles.container, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? 'Modal content' : undefined, screenReaderFocusable: true, ref: viewRef, children: [_jsxs(View, { style: styles.header, children: [_jsxs(View, { style: styles.headerTextContent, children: [heading && !image ? (_jsx(Heading, { size: "lg", accessible: true, children: heading })) : null, description && !image ? _jsx(BodyText, { accessible: true, children: description }) : null] }), showCloseButton ? (_jsx(UnstyledIconButton, { icon: CloseMediumIcon, onPress: handleCloseButtonPress, accessibilityLabel: "Close modal", ...closeButtonProps })) : null] }), image ? (_jsxs(View, { style: styles.imageContainer, children: [_jsx(Image, { style: styles.image, ...image }), _jsxs(View, { style: styles.textContent, children: [heading ? (_jsx(Heading, { size: "lg", textAlign: "center", accessible: true, children: heading })) : null, description ? (_jsx(BodyText, { textAlign: "center", accessible: true, children: description })) : null] })] })) : null, children, _jsxs(View, { style: styles.footer, children: [onPressPrimaryButton && primaryButtonText ? (_jsx(Button, { onPress: handlePrimaryButtonPress, text: primaryButtonText, ...primaryButtonProps, variant: primaryButtonProps?.variant ?? 'solid', colorScheme: primaryButtonProps?.colorScheme ?? 'highlight' })) : null, onPressSecondaryButton && secondaryButtonText ? (_jsx(Button, { onPress: handleSecondaryButtonPress, text: secondaryButtonText, ...secondaryButtonProps, variant: secondaryButtonProps?.variant ?? 'outline', colorScheme: secondaryButtonProps?.colorScheme ?? 'functional' })) : null] })] })) }));
100
+ const content = (_jsx(_Fragment, { children: loading ? (_jsxs(View, { style: styles.loadingContainer, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? 'Loading' : undefined, screenReaderFocusable: true, ref: viewRef, children: [_jsx(Spinner, { size: "lg" }), _jsx(Heading, { size: "lg", textAlign: "center", children: "Loading..." })] })) : (_jsxs(View, { style: styles.container, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? 'Modal content' : undefined, screenReaderFocusable: true, ref: viewRef, children: [_jsxs(View, { style: styles.header, children: [_jsxs(View, { style: styles.headerTextContent, children: [heading && !image ? (_jsx(Heading, { size: "lg", accessible: true, children: heading })) : null, description && !image ? _jsx(BodyText, { accessible: true, children: description }) : null] }), showCloseButton ? (_jsx(UnstyledIconButton, { icon: CloseMediumIcon, onPress: handleCloseButtonPress, accessibilityLabel: "Close modal", ...closeButtonProps })) : null] }), image ? (_jsxs(View, { style: styles.imageContainer, children: [image, _jsxs(View, { style: styles.textContent, children: [heading ? (_jsx(Heading, { size: "lg", textAlign: "center", accessible: true, children: heading })) : null, description ? (_jsx(BodyText, { textAlign: "center", accessible: true, children: description })) : null] })] })) : null, children, _jsxs(View, { style: styles.footer, children: [onPressPrimaryButton && primaryButtonText ? (_jsx(Button, { onPress: handlePrimaryButtonPress, text: primaryButtonText, ...primaryButtonProps, variant: primaryButtonProps?.variant ?? 'solid', colorScheme: primaryButtonProps?.colorScheme ?? 'highlight' })) : null, onPressSecondaryButton && secondaryButtonText ? (_jsx(Button, { onPress: handleSecondaryButtonPress, text: secondaryButtonText, ...secondaryButtonProps, variant: secondaryButtonProps?.variant ?? 'outline', colorScheme: secondaryButtonProps?.colorScheme ?? 'functional' })) : null] })] })) }));
101
101
  return inNavModal ? (_jsxs(View, { style: { flex: 1, backgroundColor: theme.color.background.primary }, children: [Platform.OS === 'android' ? (_jsx(Animated.View, { style: [styles.androidContainer, animatedBackgroundStyle], children: _jsx(Animated.View, { style: [styles.pretendContent, animatedPretendContentStyle] }) })) : null, _jsx(Animated.View, { style: [styles.inNavModalContainer, Platform.OS === 'android' && animatedInNavModalStyle], children: _jsx(View, { style: styles.inNavModalContent, children: content }) })] })) : (_jsx(BottomSheetModal, { ref: bottomSheetModalRef, enableDynamicSizing: true, snapPoints: image ? ['90%'] : props.snapPoints, showHandle: typeof loading !== 'undefined' && loading ? false : props.showHandle, accessible: false, ...props, onChange: handleChange, children: _jsx(BottomSheetScrollView, { contentContainerStyle: styles.container, ref: scrollViewRef, children: content }) }));
102
102
  };
103
103
  const styles = StyleSheet.create((theme, rt) => ({
@@ -113,10 +113,6 @@ const styles = StyleSheet.create((theme, rt) => ({
113
113
  flex: 1,
114
114
  gap: theme.components.modal.content.gap,
115
115
  },
116
- image: {
117
- width: 260,
118
- height: 260,
119
- },
120
116
  imageContainer: {
121
117
  alignItems: 'center',
122
118
  justifyContent: 'center',
@@ -0,0 +1,4 @@
1
+ import { ImageProps } from 'react-native';
2
+ import { ThemedImageProps } from '../ThemedImage';
3
+ declare const ModalImage: (props: ImageProps | ThemedImageProps) => import("react/jsx-runtime").JSX.Element;
4
+ export default ModalImage;
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Image } from 'react-native';
3
+ import { StyleSheet } from 'react-native-unistyles';
4
+ import { isThemedImageProps } from '../../utils';
5
+ import { ThemedImage } from '../ThemedImage';
6
+ const ModalImage = (props) => {
7
+ if (isThemedImageProps(props)) {
8
+ return _jsx(ThemedImage, { ...props, style: [styles.image, props.style] });
9
+ }
10
+ return _jsx(Image, { resizeMode: "cover", ...props, style: [styles.image, props.style] });
11
+ };
12
+ const styles = StyleSheet.create({
13
+ image: {
14
+ width: 260,
15
+ height: 260,
16
+ },
17
+ });
18
+ export default ModalImage;
@@ -1,2 +1,3 @@
1
1
  export { default as Modal } from './Modal';
2
2
  export type { default as ModalProps } from './Modal.props';
3
+ export { default as ModalImage } from './ModalImage';
@@ -1 +1,2 @@
1
1
  export { default as Modal } from './Modal';
2
+ export { default as ModalImage } from './ModalImage';
@@ -8,7 +8,6 @@ import StyledRadio from './RadioRoot';
8
8
  import { useFormFieldContext } from '../FormField';
9
9
  import { Helper } from '../Helper';
10
10
  import { useRadioGroupContext } from './RadioGroup.context';
11
- import RadioImage from './RadioImage';
12
11
  import RadioTextContent from './RadioTextContent';
13
12
  import RadioTileRoot from './RadioTileRoot';
14
13
  const RadioComponent = createRadio({
@@ -31,7 +30,7 @@ const Radio = ({ children, label, disabled, helperIcon, helperText, invalidText,
31
30
  const { validationStatus: groupValidationStatus, type: groupType } = useRadioGroupContext();
32
31
  const validationStatus = fieldValidationStatus ?? groupValidationStatus ?? validation ?? 'initial';
33
32
  const radioType = groupType ?? type;
34
- const radioChildren = children ? (children) : (_jsxs(_Fragment, { children: [_jsx(RadioIndicator, { children: _jsx(RadioIcon, {}) }), image ? _jsx(RadioImage, { ...image }) : null, _jsxs(RadioTextContent, { children: [!!label && _jsx(RadioLabel, { children: label }), !!helperText && _jsx(Helper, { disabled: disabled, icon: helperIcon, text: helperText }), validationStatus === 'invalid' && !!invalidText && (_jsx(Helper, { showIcon: showValidationIcon, disabled: disabled, validationStatus: "invalid", text: invalidText })), validationStatus === 'valid' && !!validText && (_jsx(Helper, { disabled: disabled, showIcon: showValidationIcon, validationStatus: "valid", text: validText }))] })] }));
33
+ const radioChildren = children ? (children) : (_jsxs(_Fragment, { children: [_jsx(RadioIndicator, { children: _jsx(RadioIcon, {}) }), image ? image : null, _jsxs(RadioTextContent, { children: [!!label && _jsx(RadioLabel, { children: label }), !!helperText && _jsx(Helper, { disabled: disabled, icon: helperIcon, text: helperText }), validationStatus === 'invalid' && !!invalidText && (_jsx(Helper, { showIcon: showValidationIcon, disabled: disabled, validationStatus: "invalid", text: invalidText })), validationStatus === 'valid' && !!validText && (_jsx(Helper, { disabled: disabled, showIcon: showValidationIcon, validationStatus: "valid", text: validText }))] })] }));
35
34
  return (_jsx(RadioComponent, { ...props, isDisabled: disabled, children: radioType === 'tile' ? _jsx(RadioTileRoot, { children: radioChildren }) : radioChildren }));
36
35
  };
37
36
  const RadioTile = ({ type = 'tile', ...props }) => _jsx(Radio, { ...props, type: type });
@@ -1,5 +1,5 @@
1
- import type { ComponentType } from 'react';
2
- import type { ImageProps, PressableProps, ViewProps } from 'react-native';
1
+ import type { ComponentType, ReactNode } from 'react';
2
+ import type { PressableProps, ViewProps } from 'react-native';
3
3
  interface RadioBaseProps extends Omit<PressableProps, 'children'> {
4
4
  value: string;
5
5
  onChange?: (isSelected: boolean) => void;
@@ -25,7 +25,7 @@ interface RadioWithoutChildrenProps extends RadioBaseProps {
25
25
  invalidText?: string;
26
26
  validText?: string;
27
27
  showValidationIcon?: boolean;
28
- image?: ImageProps;
28
+ image?: ReactNode;
29
29
  }
30
30
  type RadioProps = RadioWithChildrenProps | RadioWithoutChildrenProps;
31
31
  export default RadioProps;
@@ -1,6 +1,7 @@
1
1
  import { ImageProps } from 'react-native';
2
+ import { ThemedImageProps } from '../ThemedImage';
2
3
  declare const RadioImage: {
3
- ({ source, style, ...props }: ImageProps): import("react/jsx-runtime").JSX.Element;
4
+ ({ ...props }: ImageProps | ThemedImageProps): import("react/jsx-runtime").JSX.Element;
4
5
  displayName: string;
5
6
  };
6
7
  export default RadioImage;
@@ -1,5 +1,12 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Image } from 'react-native';
3
- const RadioImage = ({ source, style, ...props }) => (_jsx(Image, { source: source, style: style, ...props }));
3
+ import { isThemedImageProps } from '../../utils';
4
+ import { ThemedImage } from '../ThemedImage';
5
+ const RadioImage = ({ ...props }) => {
6
+ if (isThemedImageProps(props)) {
7
+ return _jsx(ThemedImage, { ...props });
8
+ }
9
+ return _jsx(Image, { ...props });
10
+ };
4
11
  RadioImage.displayName = 'RadioImage';
5
12
  export default RadioImage;
@@ -1,9 +1,10 @@
1
1
  export { default as coloursAsArray, extractLightColorValues } from './coloursAsArray';
2
2
  export { default as formatThousands } from './formatThousands';
3
3
  export { default as getFlattenedColorValue } from './getFlattenedColorValue';
4
+ export { getInitials } from './getInitials';
4
5
  export { default as getStyleValue } from './getStyleValue';
5
6
  export { default as hexWithOpacity } from './hexWithOpacity';
6
- export { getInitials } from './getInitials';
7
7
  export { default as isEqual } from './isEqual';
8
+ export { default as isThemedImageProps } from './isThemedImageProps';
8
9
  export * from './styleUtils';
9
10
  export * from './themeValueHelpers';
@@ -1,9 +1,10 @@
1
1
  export { default as coloursAsArray, extractLightColorValues } from './coloursAsArray';
2
2
  export { default as formatThousands } from './formatThousands';
3
3
  export { default as getFlattenedColorValue } from './getFlattenedColorValue';
4
+ export { getInitials } from './getInitials';
4
5
  export { default as getStyleValue } from './getStyleValue';
5
6
  export { default as hexWithOpacity } from './hexWithOpacity';
6
- export { getInitials } from './getInitials';
7
7
  export { default as isEqual } from './isEqual';
8
+ export { default as isThemedImageProps } from './isThemedImageProps';
8
9
  export * from './styleUtils';
9
10
  export * from './themeValueHelpers';
@@ -0,0 +1,4 @@
1
+ import { ImageProps } from 'react-native';
2
+ import { ThemedImageProps } from 'src/components';
3
+ declare const isThemedImageProps: (props: ThemedImageProps | ImageProps) => props is ThemedImageProps;
4
+ export default isThemedImageProps;
@@ -0,0 +1,4 @@
1
+ const isThemedImageProps = (props) => {
2
+ return 'light' in props && 'dark' in props;
3
+ };
4
+ export default isThemedImageProps;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utilitywarehouse/hearth-react-native",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Utility Warehouse React Native UI library",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -60,7 +60,7 @@
60
60
  "@utilitywarehouse/hearth-react-icons": "^0.7.4",
61
61
  "@utilitywarehouse/hearth-react-native-icons": "^0.7.4",
62
62
  "@utilitywarehouse/hearth-svg-assets": "^0.2.0",
63
- "@utilitywarehouse/hearth-tokens": "^0.2.0"
63
+ "@utilitywarehouse/hearth-tokens": "^0.2.1"
64
64
  },
65
65
  "peerDependencies": {
66
66
  "@gorhom/bottom-sheet": "^5.0.0",
@@ -0,0 +1,11 @@
1
+ import { createContext, useContext } from 'react';
2
+
3
+ const BannerContext = createContext<{ direction: 'horizontal' | 'vertical' }>({
4
+ direction: 'horizontal',
5
+ });
6
+
7
+ export const useBannerContext = () => {
8
+ return useContext(BannerContext);
9
+ };
10
+
11
+ export default BannerContext;
@@ -63,26 +63,39 @@ const MyComponent = () => (
63
63
 
64
64
  ## Props
65
65
 
66
- | Property | Type | Description | Default |
67
- | -------------------- | -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | -------------- |
68
- | icon | `ComponentType` | Icon component to display (mutually exclusive with image/illustration) | `-` |
69
- | iconContainerVariant | `'subtle' \| 'emphasis'` | Icon container visual style | `'subtle'` |
70
- | iconContainerSize | `'sm' \| 'md' \| 'lg'` | Icon container size | `'md'` |
71
- | iconContainerColor | `'pig' \| 'energy' \| 'broadband' \| 'mobile' \|` <br />`'insurance' \| 'cashback' \| 'highlight'` | Icon container color scheme | `'pig'` |
72
- | illustration | `ThemedImageProps \| ImageProps` | Themed illustration object (mutually exclusive with icon/image) | `-` |
73
- | image | `ThemedImageProps \| ImageProps` | Themed image object (mutually exclusive with icon/illustration) | `-` |
74
- | heading | `string` | Heading text | `-` (required) |
75
- | description | `string` | Description text | `-` (required) |
76
- | direction | `'horizontal' \| 'vertical'` | Layout direction for icon/image and content | `'horizontal'` |
77
- | link | `ReactNode` | Link element to display | `-` |
78
- | button | `ReactNode` | Button element to display | `-` |
79
- | onPress | `() => void` | Makes the entire banner pressable (shows chevron) | `-` |
80
- | onClose | `() => void` | Shows close button with handler | `-` |
81
- | variant | `'subtle' \| 'emphasis'` | Card visual style variant | `'subtle'` |
82
- | colorScheme | `'pig' \| 'energy' \| 'broadband' \| 'mobile' \|` <br />`'insurance' \| 'cashback' \| 'highlight'` | Color scheme for buttons | `'pig'` |
66
+ | Property | Type | Description | Default |
67
+ | -------------------- | -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | -------------- |
68
+ | icon | `ComponentType` | Icon component to display (mutually exclusive with image/illustration) | `-` |
69
+ | iconContainerVariant | `'subtle' \| 'emphasis'` | Icon container visual style | `'subtle'` |
70
+ | iconContainerSize | `'sm' \| 'md' \| 'lg'` | Icon container size | `'md'` |
71
+ | iconContainerColor | `'pig' \| 'energy' \| 'broadband' \| 'mobile' \|` <br />`'insurance' \| 'cashback' \| 'highlight'` | Icon container color scheme | `'pig'` |
72
+ | illustration | `ReactNode` | The illustration element to render (mutually exclusive with icon/image) | `-` |
73
+ | image | `ReactNode` | The image element to render (mutually exclusive with icon/illustration) | `-` |
74
+ | heading | `string` | Heading text | `-` (required) |
75
+ | description | `string` | Description text | `-` (required) |
76
+ | direction | `'horizontal' \| 'vertical'` | Layout direction for icon/image and content | `'horizontal'` |
77
+ | link | `ReactNode` | Link element to display | `-` |
78
+ | button | `ReactNode` | Button element to display | `-` |
79
+ | onPress | `() => void` | Makes the entire banner pressable (shows chevron) | `-` |
80
+ | onClose | `() => void` | Shows close button with handler | `-` |
81
+ | variant | `'subtle' \| 'emphasis'` | Card visual style variant | `'subtle'` |
82
+ | colorScheme | `'pig' \| 'energy' \| 'broadband' \| 'mobile' \|` <br />`'insurance' \| 'cashback' \| 'highlight'` | Color scheme for buttons | `'pig'` |
83
83
 
84
84
  The component also accepts all standard Card props except `noPadding`, `space`, `gap`, `rowGap`, `columnGap`, `flexDirection`, `flexWrap`, `alignItems`, and `justifyContent`.
85
85
 
86
+ ### `BannerImage` & `BannerIllustration` Props
87
+
88
+ The `BannerImage` & `BannerIllustration` component can be used to display an image within the Banner. It accepts the following props:
89
+
90
+ | Property | Type | Description |
91
+ | --------- | --------------------- | ------------------------------------------------------------------------ |
92
+ | `source` | `ImageSourcePropType` | The source of the image to display |
93
+ | `light` | `ImageSourcePropType` | The source of the image to display in light mode (use instead of source) |
94
+ | `dark` | `ImageSourcePropType` | The source of the image to display in dark mode (use instead of source) |
95
+ | `...rest` | `ImageProps` | Additional props to pass to the underlying Image component |
96
+
97
+ For more details about the ThemedImage component used internally, refer to the [`ThemedImage` documentation](/docs/utility-components-themed-image--docs).
98
+
86
99
  ## Layout Options
87
100
 
88
101
  ### Horizontal Layout (Default)
@@ -153,22 +166,28 @@ Display a themed image that automatically switches between light and dark modes:
153
166
  <Canvas of={Stories.WithImage} />
154
167
 
155
168
  ```jsx
156
- import { Banner } from '@utilitywarehouse/hearth-react-native';
169
+ import { Banner, BannerImage } from '@utilitywarehouse/hearth-react-native';
157
170
 
158
171
  const MyComponent = () => (
159
172
  <>
160
173
  <Banner
161
- image={{
162
- source: { uri: 'https://example.com/image.jpg' },
163
- }}
174
+ image={
175
+ <BannerImage
176
+ source={{
177
+ uri: 'https://example.com/image.jpg',
178
+ }}
179
+ />
180
+ }
164
181
  heading="Featured Content"
165
182
  description="Discover amazing content curated just for you."
166
183
  />
167
184
  <Banner
168
- image={{
169
- light: { uri: 'https://example.com/light-image.jpg' },
170
- dark: { uri: 'https://example.com/dark-image.jpg' },
171
- }}
185
+ image={
186
+ <BannerImage
187
+ light={{ uri: 'https://example.com/light-image.jpg' }}
188
+ dark={{ uri: 'https://example.com/dark-image.jpg' }}
189
+ />
190
+ }
172
191
  heading="Featured Content"
173
192
  description="Discover amazing content curated just for you."
174
193
  />
@@ -183,18 +202,15 @@ Display a themed illustration that adapts to layout changes:
183
202
  <Canvas of={Stories.WithIllustration} />
184
203
 
185
204
  ```jsx
186
- import { Banner } from '@utilitywarehouse/hearth-react-native';
205
+ import { Banner, BannerIllustration } from '@utilitywarehouse/hearth-react-native';
187
206
  import SpotBillingDark from '@utilitywarehouse/hearth-svg-assets/lib/spot-billing-dark.svg';
188
207
  import SpotBillingLight from '@utilitywarehouse/hearth-svg-assets/lib/spot-billing-light.svg';
189
208
 
190
209
  const MyComponent = () => (
191
210
  <Banner
192
- illustration={{
193
- light: SpotBillingLight,
194
- dark: SpotBillingDark,
195
- width: 80,
196
- height: 80,
197
- }}
211
+ illustration={
212
+ <BannerIllustration light={SpotBillingLight} dark={SpotBillingDark} width={80} height={80} />
213
+ }
198
214
  heading="Featured Content"
199
215
  description="Discover amazing content curated just for you."
200
216
  />
@@ -358,7 +374,7 @@ Combine multiple features for rich interactive banners:
358
374
  <Canvas of={Stories.ComplexExample} />
359
375
 
360
376
  ```jsx
361
- import { Banner } from '@utilitywarehouse/hearth-react-native';
377
+ import { Banner, BannerImage } from '@utilitywarehouse/hearth-react-native';
362
378
  import { InsuranceMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
363
379
 
364
380
  const MyComponent = () => (
@@ -381,10 +397,12 @@ const MyComponent = () => (
381
397
 
382
398
  {/* Dismissible banner with link */}
383
399
  <Banner
384
- image={{
385
- light: { uri: 'https://example.com/light.jpg' },
386
- dark: { uri: 'https://example.com/dark.jpg' },
387
- }}
400
+ image={
401
+ <BannerImage
402
+ light={{ uri: 'https://example.com/light.jpg' }}
403
+ dark={{ uri: 'https://example.com/dark.jpg' }}
404
+ />
405
+ }
388
406
  heading="Exclusive Member Benefit"
389
407
  description="As a valued member, you now have access to premium features at no extra cost."
390
408
  variant="emphasis"
@@ -1,7 +1,5 @@
1
- import type { ComponentType, ReactElement } from 'react';
2
- import { ImageProps } from 'react-native';
1
+ import type { ComponentType, ReactElement, ReactNode } from 'react';
3
2
  import type CardProps from '../Card/Card.props';
4
- import { ThemedImageProps } from '../ThemedImage';
5
3
 
6
4
  export type BannerDirection = 'horizontal' | 'vertical';
7
5
 
@@ -51,12 +49,12 @@ export interface BannerProps
51
49
  * Illustration to display in the banner
52
50
  * Mutually exclusive with icon and image
53
51
  */
54
- illustration?: ThemedImageProps | ImageProps;
52
+ illustration?: ReactNode;
55
53
  /**
56
54
  * Image to display in the banner
57
55
  * Mutually exclusive with icon and illustration
58
56
  */
59
- image?: ThemedImageProps | ImageProps;
57
+ image?: ReactNode;
60
58
  /**
61
59
  * Heading text
62
60
  */