@utilitywarehouse/hearth-react-native 0.8.2 → 0.10.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 (157) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-lint.log +1 -1
  3. package/CHANGELOG.md +16 -0
  4. package/build/components/Banner/Banner.js +25 -6
  5. package/build/components/Banner/Banner.props.d.ts +2 -2
  6. package/build/components/BottomSheet/BottomSheetHandle.js +8 -0
  7. package/build/components/Menu/Menu.context.d.ts +5 -0
  8. package/build/components/Menu/Menu.context.js +9 -0
  9. package/build/components/Menu/Menu.d.ts +4 -0
  10. package/build/components/Menu/Menu.js +25 -0
  11. package/build/components/Menu/Menu.props.d.ts +21 -0
  12. package/build/components/Menu/Menu.props.js +1 -0
  13. package/build/components/Menu/MenuItem.d.ts +18 -0
  14. package/build/components/Menu/MenuItem.js +115 -0
  15. package/build/components/Menu/MenuItem.props.d.ts +27 -0
  16. package/build/components/Menu/MenuItem.props.js +1 -0
  17. package/build/components/Menu/MenuTrigger.d.ts +9 -0
  18. package/build/components/Menu/MenuTrigger.js +11 -0
  19. package/build/components/Menu/MenuTrigger.props.d.ts +12 -0
  20. package/build/components/Menu/MenuTrigger.props.js +1 -0
  21. package/build/components/Menu/index.d.ts +7 -0
  22. package/build/components/Menu/index.js +4 -0
  23. package/build/components/Modal/Modal.d.ts +1 -1
  24. package/build/components/Modal/Modal.js +32 -30
  25. package/build/components/Modal/Modal.props.d.ts +1 -0
  26. package/build/components/Modal/Modal.web.d.ts +1 -1
  27. package/build/components/Modal/Modal.web.js +25 -25
  28. package/build/components/PillGroup/Pill.d.ts +16 -0
  29. package/build/components/PillGroup/Pill.js +94 -0
  30. package/build/components/PillGroup/Pill.props.d.ts +10 -0
  31. package/build/components/PillGroup/Pill.props.js +1 -0
  32. package/build/components/PillGroup/PillGroup.context.d.ts +6 -0
  33. package/build/components/PillGroup/PillGroup.context.js +5 -0
  34. package/build/components/PillGroup/PillGroup.d.ts +5 -0
  35. package/build/components/PillGroup/PillGroup.js +34 -0
  36. package/build/components/PillGroup/PillGroup.props.d.ts +15 -0
  37. package/build/components/PillGroup/PillGroup.props.js +1 -0
  38. package/build/components/PillGroup/index.d.ts +4 -0
  39. package/build/components/PillGroup/index.js +2 -0
  40. package/build/components/Select/Select.js +2 -1
  41. package/build/components/Toast/Toast.context.d.ts +9 -0
  42. package/build/components/Toast/Toast.context.js +90 -0
  43. package/build/components/Toast/Toast.props.d.ts +29 -0
  44. package/build/components/Toast/Toast.props.js +1 -0
  45. package/build/components/Toast/ToastItem.d.ts +10 -0
  46. package/build/components/Toast/ToastItem.js +129 -0
  47. package/build/components/Toast/index.d.ts +3 -0
  48. package/build/components/Toast/index.js +2 -0
  49. package/build/components/index.d.ts +3 -0
  50. package/build/components/index.js +3 -0
  51. package/build/tokens/components/dark/checkbox.d.ts +3 -0
  52. package/build/tokens/components/dark/checkbox.js +3 -0
  53. package/build/tokens/components/dark/index.d.ts +3 -1
  54. package/build/tokens/components/dark/index.js +3 -1
  55. package/build/tokens/components/dark/input.d.ts +9 -0
  56. package/build/tokens/components/dark/input.js +9 -0
  57. package/build/tokens/components/dark/modal.d.ts +7 -4
  58. package/build/tokens/components/dark/modal.js +7 -4
  59. package/build/tokens/components/dark/radio.d.ts +3 -0
  60. package/build/tokens/components/dark/radio.js +3 -0
  61. package/build/tokens/components/dark/rating.d.ts +8 -0
  62. package/build/tokens/components/dark/rating.js +7 -0
  63. package/build/tokens/components/dark/table.d.ts +2 -3
  64. package/build/tokens/components/dark/table.js +2 -3
  65. package/build/tokens/components/dark/time-picker.d.ts +29 -0
  66. package/build/tokens/components/dark/time-picker.js +28 -0
  67. package/build/tokens/components/dark/timeline.d.ts +27 -0
  68. package/build/tokens/components/dark/timeline.js +26 -0
  69. package/build/tokens/components/dark/toast.d.ts +6 -2
  70. package/build/tokens/components/dark/toast.js +6 -2
  71. package/build/tokens/components/light/checkbox.d.ts +3 -0
  72. package/build/tokens/components/light/checkbox.js +3 -0
  73. package/build/tokens/components/light/index.d.ts +3 -1
  74. package/build/tokens/components/light/index.js +3 -1
  75. package/build/tokens/components/light/input.d.ts +9 -0
  76. package/build/tokens/components/light/input.js +9 -0
  77. package/build/tokens/components/light/modal.d.ts +7 -4
  78. package/build/tokens/components/light/modal.js +7 -4
  79. package/build/tokens/components/light/radio.d.ts +3 -0
  80. package/build/tokens/components/light/radio.js +3 -0
  81. package/build/tokens/components/light/rating.d.ts +8 -0
  82. package/build/tokens/components/light/rating.js +7 -0
  83. package/build/tokens/components/light/table.d.ts +2 -3
  84. package/build/tokens/components/light/table.js +2 -3
  85. package/build/tokens/components/light/time-picker.d.ts +29 -0
  86. package/build/tokens/components/light/time-picker.js +28 -0
  87. package/build/tokens/components/light/timeline.d.ts +27 -0
  88. package/build/tokens/components/light/timeline.js +26 -0
  89. package/build/tokens/components/light/toast.d.ts +6 -2
  90. package/build/tokens/components/light/toast.js +6 -2
  91. package/docs/assets/toast-ios.MP4 +0 -0
  92. package/docs/components/AllComponents.web.tsx +59 -0
  93. package/docs/components/BackToTopButton.tsx +1 -1
  94. package/package.json +4 -4
  95. package/src/components/Banner/Banner.docs.mdx +19 -10
  96. package/src/components/Banner/Banner.props.ts +2 -2
  97. package/src/components/Banner/Banner.stories.tsx +1 -4
  98. package/src/components/Banner/Banner.tsx +47 -7
  99. package/src/components/BottomSheet/BottomSheetHandle.tsx +12 -0
  100. package/src/components/DatePickerInput/DatePickerInput.docs.mdx +1 -1
  101. package/src/components/Menu/Menu.context.ts +15 -0
  102. package/src/components/Menu/Menu.docs.mdx +158 -0
  103. package/src/components/Menu/Menu.props.ts +24 -0
  104. package/src/components/Menu/Menu.stories.tsx +292 -0
  105. package/src/components/Menu/Menu.tsx +54 -0
  106. package/src/components/Menu/MenuItem.props.ts +29 -0
  107. package/src/components/Menu/MenuItem.tsx +145 -0
  108. package/src/components/Menu/MenuTrigger.props.ts +14 -0
  109. package/src/components/Menu/MenuTrigger.tsx +20 -0
  110. package/src/components/Menu/index.ts +7 -0
  111. package/src/components/Modal/Modal.docs.mdx +34 -5
  112. package/src/components/Modal/Modal.props.ts +1 -0
  113. package/src/components/Modal/Modal.stories.tsx +46 -0
  114. package/src/components/Modal/Modal.tsx +37 -33
  115. package/src/components/Modal/Modal.web.tsx +27 -27
  116. package/src/components/PillGroup/Pill.props.ts +13 -0
  117. package/src/components/PillGroup/Pill.tsx +120 -0
  118. package/src/components/PillGroup/PillGroup.context.tsx +12 -0
  119. package/src/components/PillGroup/PillGroup.docs.mdx +96 -0
  120. package/src/components/PillGroup/PillGroup.props.ts +22 -0
  121. package/src/components/PillGroup/PillGroup.stories.tsx +159 -0
  122. package/src/components/PillGroup/PillGroup.tsx +66 -0
  123. package/src/components/PillGroup/index.ts +4 -0
  124. package/src/components/Select/Select.tsx +2 -0
  125. package/src/components/Toast/Toast.context.tsx +118 -0
  126. package/src/components/Toast/Toast.docs.mdx +164 -0
  127. package/src/components/Toast/Toast.props.ts +33 -0
  128. package/src/components/Toast/Toast.stories.tsx +356 -0
  129. package/src/components/Toast/ToastItem.tsx +200 -0
  130. package/src/components/Toast/index.ts +3 -0
  131. package/src/components/index.ts +3 -0
  132. package/src/tokens/components/dark/checkbox.ts +3 -0
  133. package/src/tokens/components/dark/index.ts +3 -1
  134. package/src/tokens/components/dark/input.ts +9 -0
  135. package/src/tokens/components/dark/modal.ts +7 -4
  136. package/src/tokens/components/dark/radio.ts +3 -0
  137. package/src/tokens/components/dark/rating.ts +8 -0
  138. package/src/tokens/components/dark/table.ts +2 -3
  139. package/src/tokens/components/dark/time-picker.ts +29 -0
  140. package/src/tokens/components/dark/timeline.ts +27 -0
  141. package/src/tokens/components/dark/toast.ts +6 -2
  142. package/src/tokens/components/light/checkbox.ts +3 -0
  143. package/src/tokens/components/light/index.ts +3 -1
  144. package/src/tokens/components/light/input.ts +9 -0
  145. package/src/tokens/components/light/modal.ts +7 -4
  146. package/src/tokens/components/light/radio.ts +3 -0
  147. package/src/tokens/components/light/rating.ts +8 -0
  148. package/src/tokens/components/light/table.ts +2 -3
  149. package/src/tokens/components/light/time-picker.ts +29 -0
  150. package/src/tokens/components/light/timeline.ts +27 -0
  151. package/src/tokens/components/light/toast.ts +6 -2
  152. package/build/tokens/components/dark/dialog.d.ts +0 -25
  153. package/build/tokens/components/dark/dialog.js +0 -24
  154. package/build/tokens/components/light/dialog.d.ts +0 -25
  155. package/build/tokens/components/light/dialog.js +0 -24
  156. package/src/tokens/components/dark/dialog.ts +0 -25
  157. package/src/tokens/components/light/dialog.ts +0 -25
@@ -12,7 +12,7 @@ import { Button } from '../Button';
12
12
  import { Heading } from '../Heading';
13
13
  import { Spinner } from '../Spinner';
14
14
  import { UnstyledIconButton } from '../UnstyledIconButton';
15
- const Modal = ({ ref, children, heading, description, showCloseButton = true, primaryButtonText, secondaryButtonText, onPressPrimaryButton, onPressCloseButton, onPressSecondaryButton, closeOnPrimaryButtonPress = true, closeOnSecondaryButtonPress = true, loading, image, primaryButtonProps, secondaryButtonProps, closeButtonProps, fullscreen = false, ...props }) => {
15
+ const Modal = ({ ref, children, heading, description, showCloseButton = true, primaryButtonText, secondaryButtonText, onPressPrimaryButton, onPressCloseButton, onPressSecondaryButton, closeOnPrimaryButtonPress = true, closeOnSecondaryButtonPress = true, loading, image, primaryButtonProps, secondaryButtonProps, closeButtonProps, inNavModal = false, ...props }) => {
16
16
  const bottomSheetModalRef = useRef(null);
17
17
  const viewRef = useRef(null);
18
18
  const scrollViewRef = useRef(null);
@@ -20,7 +20,7 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
20
20
  const backgroundOpacity = useSharedValue(0);
21
21
  const pretendContentTranslateY = useSharedValue(20);
22
22
  const triggerCloseAnimation = useCallback(() => {
23
- if (Platform.OS === 'android' && fullscreen) {
23
+ if (Platform.OS === 'android' && inNavModal) {
24
24
  pretendContentTranslateY.value = withTiming(20, {
25
25
  duration: 50,
26
26
  easing: Easing.in(Easing.quad),
@@ -30,14 +30,14 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
30
30
  easing: Easing.in(Easing.quad),
31
31
  });
32
32
  }
33
- }, [Platform.OS, fullscreen, pretendContentTranslateY, backgroundOpacity]);
33
+ }, [Platform.OS, inNavModal, pretendContentTranslateY, backgroundOpacity]);
34
34
  useImperativeHandle(ref, () => ({
35
35
  ...bottomSheetModalRef.current,
36
36
  triggerCloseAnimation,
37
37
  }));
38
- // Trigger animations on render for fullscreen Android modal
38
+ // Trigger animations on render for inNavModal Android modal
39
39
  useEffect(() => {
40
- if (Platform.OS === 'android' && fullscreen) {
40
+ if (Platform.OS === 'android' && inNavModal) {
41
41
  backgroundOpacity.value = withDelay(300, withTiming(1, {
42
42
  duration: 200,
43
43
  easing: Easing.out(Easing.quad),
@@ -47,11 +47,11 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
47
47
  easing: Easing.out(Easing.quad),
48
48
  }));
49
49
  }
50
- }, [fullscreen, backgroundOpacity, pretendContentTranslateY]);
50
+ }, [inNavModal, backgroundOpacity, pretendContentTranslateY]);
51
51
  const animatedBackgroundStyle = useAnimatedStyle(() => ({
52
52
  backgroundColor: hexWithOpacity(theme.components.overlay.backgroundColor, backgroundOpacity.value * (theme.components.overlay.opacity / 100)),
53
53
  }));
54
- const animatedFullscreenStyle = useAnimatedStyle(() => ({
54
+ const animatedInNavModalStyle = useAnimatedStyle(() => ({
55
55
  backgroundColor: hexWithOpacity(theme.components.overlay.backgroundColor, backgroundOpacity.value * (theme.components.overlay.opacity / 100)),
56
56
  }));
57
57
  const animatedPretendContentStyle = useAnimatedStyle(() => ({
@@ -98,20 +98,20 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
98
98
  }
99
99
  };
100
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] })] })) }));
101
- return fullscreen ? (_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.fullscreenContainer, Platform.OS === 'android' && animatedFullscreenStyle], children: _jsx(View, { style: styles.fullscreenContent, 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 }) }));
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) => ({
104
104
  container: {
105
105
  flex: 1,
106
- gap: theme.components.dialog.gap,
106
+ gap: theme.components.modal.gap,
107
107
  },
108
108
  header: {
109
109
  flexDirection: 'row',
110
- gap: theme.components.dialog.gap,
110
+ gap: theme.components.modal.gap,
111
111
  },
112
112
  headerTextContent: {
113
113
  flex: 1,
114
- gap: theme.components.dialog.content.gap,
114
+ gap: theme.components.modal.content.gap,
115
115
  },
116
116
  image: {
117
117
  width: 260,
@@ -123,40 +123,40 @@ const styles = StyleSheet.create((theme, rt) => ({
123
123
  flex: 1,
124
124
  },
125
125
  textContent: {
126
- gap: theme.components.dialog.content.gap,
126
+ gap: theme.components.modal.content.gap,
127
127
  },
128
128
  loadingContainer: {
129
129
  flex: 1,
130
130
  alignItems: 'center',
131
131
  justifyContent: 'center',
132
132
  minHeight: 140,
133
- gap: theme.components.dialog.content.gap,
133
+ gap: theme.components.modal.content.gap,
134
134
  },
135
135
  footer: {
136
- gap: theme.components.dialog.action.gap,
136
+ gap: theme.components.modal.action.gap,
137
137
  },
138
- fullscreenContainer: {
138
+ inNavModalContainer: {
139
139
  flex: 1,
140
140
  ...(Platform.OS === 'ios' ? { backgroundColor: theme.components.overlay.backgroundColor } : {}),
141
141
  },
142
- fullscreenContent: {
142
+ inNavModalContent: {
143
143
  flex: 1,
144
- borderTopLeftRadius: theme.components.dialog.borderRadius,
145
- borderTopRightRadius: theme.components.dialog.borderRadius,
144
+ borderTopLeftRadius: theme.components.modal.borderRadius,
145
+ borderTopRightRadius: theme.components.modal.borderRadius,
146
146
  backgroundColor: theme.color.surface.neutral.strong,
147
- gap: theme.components.dialog.gap,
148
- padding: theme.components.dialog.padding,
149
- paddingBottom: theme.components.dialog.padding + rt.insets.bottom,
147
+ gap: theme.components.modal.gap,
148
+ padding: theme.components.modal.padding,
149
+ paddingBottom: theme.components.modal.padding + rt.insets.bottom,
150
150
  },
151
151
  androidContainer: {
152
152
  height: rt.insets.top + 18,
153
- paddingLeft: theme.components.dialog.padding,
154
- paddingRight: theme.components.dialog.padding,
153
+ paddingLeft: theme.components.modal.padding,
154
+ paddingRight: theme.components.modal.padding,
155
155
  justifyContent: 'flex-end',
156
156
  },
157
157
  pretendContent: {
158
- borderTopLeftRadius: theme.components.dialog.borderRadius,
159
- borderTopRightRadius: theme.components.dialog.borderRadius,
158
+ borderTopLeftRadius: theme.components.modal.borderRadius,
159
+ borderTopRightRadius: theme.components.modal.borderRadius,
160
160
  height: 12,
161
161
  backgroundColor: theme.components.parts.modalStack.backgroundColorCardTop,
162
162
  },
@@ -0,0 +1,16 @@
1
+ import type { PillProps } from './Pill.props';
2
+ export declare const Pill: import("react").ForwardRefExoticComponent<PillProps & {
3
+ states?: {
4
+ active?: boolean;
5
+ };
6
+ } & Omit<import("react-native").PressableProps, "children"> & {
7
+ tabIndex?: 0 | -1 | undefined;
8
+ } & {
9
+ children?: import("react").ReactNode | (({ hovered, pressed, focused, focusVisible, disabled, }: {
10
+ hovered?: boolean | undefined;
11
+ pressed?: boolean | undefined;
12
+ focused?: boolean | undefined;
13
+ focusVisible?: boolean | undefined;
14
+ disabled?: boolean | undefined;
15
+ }) => import("react").ReactNode);
16
+ } & import("react").RefAttributes<unknown>>;
@@ -0,0 +1,94 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { createPressable } from '@gluestack-ui/pressable';
3
+ import { Pressable } from 'react-native';
4
+ import { StyleSheet } from 'react-native-unistyles';
5
+ import { Icon } from '../Icon';
6
+ import { BodyText } from '../BodyText';
7
+ import { usePillGroupContext } from './PillGroup.context';
8
+ const PillRoot = ({ value, label, icon, states = {}, ...props }) => {
9
+ const { active } = states;
10
+ const context = usePillGroupContext();
11
+ const isSelected = context?.value.includes(value) ?? false;
12
+ styles.useVariants({ selected: isSelected, active });
13
+ const handlePress = () => {
14
+ context?.onChange(value);
15
+ };
16
+ return (_jsxs(Pressable, { ...props, style: styles.pill, accessibilityRole: "button", accessibilityState: { selected: isSelected }, onPress: handlePress, children: [icon && _jsx(Icon, { as: icon, size: "sm", style: styles.icon }), _jsx(BodyText, { weight: "semibold", style: styles.text, children: label })] }));
17
+ };
18
+ export const Pill = createPressable({ Root: PillRoot });
19
+ Pill.displayName = 'Pill';
20
+ const styles = StyleSheet.create(theme => ({
21
+ pill: {
22
+ flexDirection: 'row',
23
+ alignItems: 'center',
24
+ justifyContent: 'center',
25
+ height: theme.components.pill.height,
26
+ minWidth: theme.components.pill.minWidth,
27
+ gap: theme.components.pill.gap,
28
+ paddingHorizontal: theme.components.pill.paddingHorizontal,
29
+ paddingVertical: theme.components.pill.paddingVertical,
30
+ borderRadius: theme.components.pill.borderRadius,
31
+ borderWidth: theme.components.pill.borderWidth,
32
+ borderColor: theme.color.interactive.neutral.border.subtle,
33
+ backgroundColor: 'transparent',
34
+ _web: {
35
+ _hover: {
36
+ backgroundColor: theme.color.interactive.neutral.surface.subtle.hover,
37
+ },
38
+ '_focus-visible': theme.helpers.focusVisible,
39
+ },
40
+ variants: {
41
+ active: {
42
+ true: {
43
+ backgroundColor: theme.color.interactive.neutral.surface.subtle.active,
44
+ },
45
+ },
46
+ selected: {
47
+ true: {
48
+ backgroundColor: theme.color.interactive.brand.surface.strong.default,
49
+ borderColor: theme.color.interactive.brand.surface.strong.default,
50
+ _web: {
51
+ _hover: {
52
+ backgroundColor: theme.color.interactive.brand.surface.strong.hover,
53
+ borderColor: theme.color.interactive.brand.surface.strong.hover,
54
+ },
55
+ },
56
+ },
57
+ },
58
+ },
59
+ compoundVariants: [
60
+ {
61
+ selected: true,
62
+ active: true,
63
+ styles: {
64
+ backgroundColor: theme.color.interactive.brand.surface.strong.active,
65
+ borderColor: theme.color.interactive.brand.surface.strong.active,
66
+ },
67
+ },
68
+ ],
69
+ },
70
+ text: {
71
+ variants: {
72
+ selected: {
73
+ true: {
74
+ color: theme.color.text.inverted,
75
+ },
76
+ false: {
77
+ color: theme.color.text.primary,
78
+ },
79
+ },
80
+ },
81
+ },
82
+ icon: {
83
+ variants: {
84
+ selected: {
85
+ true: {
86
+ color: theme.color.icon.inverted,
87
+ },
88
+ false: {
89
+ color: theme.color.icon.primary,
90
+ },
91
+ },
92
+ },
93
+ },
94
+ }));
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import { PressableProps } from 'react-native';
3
+ export interface PillProps extends Omit<PressableProps, 'children'> {
4
+ /** Value returned when selected */
5
+ value: string;
6
+ /** Text label shown inside the pill */
7
+ label: string;
8
+ /** Left icon */
9
+ icon?: React.ComponentType<any>;
10
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ export interface PillGroupContextValue {
2
+ value: string[];
3
+ onChange: (value: string) => void;
4
+ }
5
+ export declare const PillGroupContext: import("react").Context<PillGroupContextValue | null>;
6
+ export declare const usePillGroupContext: () => PillGroupContextValue | null;
@@ -0,0 +1,5 @@
1
+ import { createContext, useContext } from 'react';
2
+ export const PillGroupContext = createContext(null);
3
+ export const usePillGroupContext = () => {
4
+ return useContext(PillGroupContext);
5
+ };
@@ -0,0 +1,5 @@
1
+ import type { PillGroupProps } from './PillGroup.props';
2
+ export declare const PillGroup: {
3
+ ({ children, value, multiple, wrap, onChange, style, ...props }: PillGroupProps): import("react/jsx-runtime").JSX.Element;
4
+ displayName: string;
5
+ };
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useMemo } from 'react';
3
+ import { ScrollView } from 'react-native';
4
+ import { StyleSheet } from 'react-native-unistyles';
5
+ import { Box } from '../Box';
6
+ import { PillGroupContext } from './PillGroup.context';
7
+ export const PillGroup = ({ children, value, multiple = false, wrap = true, onChange, style, ...props }) => {
8
+ const normalizedValue = Array.isArray(value) ? value : [value];
9
+ const contextValue = useMemo(() => ({
10
+ value: normalizedValue,
11
+ onChange: (pillValue) => {
12
+ if (multiple) {
13
+ const newValue = normalizedValue.includes(pillValue)
14
+ ? normalizedValue.filter(v => v !== pillValue)
15
+ : [...normalizedValue, pillValue];
16
+ onChange?.(newValue);
17
+ }
18
+ else {
19
+ onChange?.(pillValue);
20
+ }
21
+ },
22
+ }), [normalizedValue, multiple, onChange]);
23
+ return (_jsx(PillGroupContext.Provider, { value: contextValue, children: wrap ? (_jsx(Box, { style: [styles.group, styles.wrap, style], ...props, children: children })) : (_jsx(ScrollView, { horizontal: true, contentContainerStyle: [styles.group, style], showsHorizontalScrollIndicator: false, ...props, children: children })) }));
24
+ };
25
+ PillGroup.displayName = 'PillGroup';
26
+ const styles = StyleSheet.create(theme => ({
27
+ group: {
28
+ flexDirection: 'row',
29
+ gap: theme.components.pill.group.gap,
30
+ },
31
+ wrap: {
32
+ flexWrap: 'wrap',
33
+ },
34
+ }));
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+ import { ScrollViewProps, ViewStyle } from 'react-native';
3
+ export interface PillGroupProps extends Omit<ScrollViewProps, 'horizontal' | 'contentContainerStyle' | 'showsHorizontalScrollIndicator'> {
4
+ /** Controlled selected value(s) */
5
+ value: string | string[];
6
+ /** Multi-select mode. Default = false */
7
+ multiple?: boolean;
8
+ /** Allow pills to wrap lines. Default = true */
9
+ wrap?: boolean;
10
+ /** Handle selection changes */
11
+ onChange?: (value: string | string[]) => void;
12
+ /** Children must be <Pill> elements */
13
+ children: React.ReactNode;
14
+ style?: ViewStyle | ViewStyle[];
15
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ export { PillGroup } from './PillGroup';
2
+ export type { PillGroupProps } from './PillGroup.props';
3
+ export { Pill } from './Pill';
4
+ export type { PillProps } from './Pill.props';
@@ -0,0 +1,2 @@
1
+ export { PillGroup } from './PillGroup';
2
+ export { Pill } from './Pill';
@@ -77,7 +77,7 @@ const Select = ({ options = [], value, onValueChange, label, placeholder = 'Sele
77
77
  selectedValue: value,
78
78
  onValueChange,
79
79
  close: closeBottomSheet,
80
- }, children: [menuHeading && (_jsx(View, { style: styles.headingContainer, children: _jsx(DetailText, { size: "lg", children: menuHeading }) })), searchable && (_jsx(View, { style: styles.searchContainer, children: _jsx(Input, { placeholder: searchPlaceholder, value: search, onChangeText: setSearch, type: "search" }) })), children ? (_jsx(BottomSheetScrollView, { children: children })) : (_jsx(BottomSheetFlatList, { data: filteredOptions, keyExtractor: (option) => option.value, renderItem: renderSelectOption, ListEmptyComponent: renderEmptyComponent, ...listProps }))] }) })] }));
80
+ }, children: [menuHeading && (_jsx(View, { style: styles.headingContainer, children: _jsx(DetailText, { size: "lg", children: menuHeading }) })), searchable && (_jsx(View, { style: styles.searchContainer, children: _jsx(Input, { placeholder: searchPlaceholder, value: search, inBottomSheet: true, onChangeText: setSearch, type: "search" }) })), children ? (_jsx(BottomSheetScrollView, { children: children })) : (_jsx(BottomSheetFlatList, { data: filteredOptions, keyExtractor: (option) => option.value, renderItem: renderSelectOption, ListEmptyComponent: renderEmptyComponent, ...listProps }))] }) })] }));
81
81
  };
82
82
  const styles = StyleSheet.create(theme => ({
83
83
  container: {
@@ -161,6 +161,7 @@ const styles = StyleSheet.create(theme => ({
161
161
  emptyContainer: {
162
162
  alignItems: 'center',
163
163
  justifyContent: 'center',
164
+ marginTop: theme.space.md,
164
165
  },
165
166
  }));
166
167
  Select.displayName = 'Select';
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import type { ToastContextValue } from './Toast.props';
3
+ declare const ToastContext: React.Context<ToastContextValue | undefined>;
4
+ export declare const useToastContext: () => ToastContextValue;
5
+ export declare const ToastProvider: React.FC<{
6
+ children: React.ReactNode;
7
+ }>;
8
+ export declare const useToast: () => ToastContextValue;
9
+ export default ToastContext;
@@ -0,0 +1,90 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';
3
+ import { View } from 'react-native';
4
+ import { StyleSheet } from 'react-native-unistyles';
5
+ import ToastItem from './ToastItem';
6
+ const ToastContext = createContext(undefined);
7
+ export const useToastContext = () => {
8
+ const ctx = useContext(ToastContext);
9
+ if (!ctx)
10
+ throw new Error('useToastContext must be used within ToastProvider');
11
+ return ctx;
12
+ };
13
+ export const ToastProvider = ({ children }) => {
14
+ const [toasts, setToasts] = useState([]);
15
+ const timers = useRef({});
16
+ const toastRefs = useRef({});
17
+ const removeToast = useCallback((id) => {
18
+ setToasts(s => s.filter(t => t.id !== id));
19
+ const timer = timers.current[id];
20
+ if (timer) {
21
+ clearTimeout(timer);
22
+ delete timers.current[id];
23
+ }
24
+ delete toastRefs.current[id];
25
+ }, []);
26
+ const addToast = useCallback((opts) => {
27
+ const id = opts.id ?? `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
28
+ const toast = {
29
+ id,
30
+ text: opts.text,
31
+ actionText: opts.actionText,
32
+ onPress: opts.onPress,
33
+ onDismiss: opts.onDismiss,
34
+ icon: opts.icon,
35
+ duration: opts.duration ?? 6000,
36
+ showDismissIcon: opts.showDismissIcon,
37
+ dismissOnPress: opts.dismissOnPress ?? true,
38
+ };
39
+ setToasts(s => [toast, ...s]);
40
+ // set auto-dismiss timer
41
+ if (toast.duration && toast.duration > 0) {
42
+ const t = setTimeout(() => {
43
+ // call dismiss animation if ref exists, otherwise remove immediately
44
+ const ref = toastRefs.current[id];
45
+ if (ref) {
46
+ ref.dismiss();
47
+ }
48
+ else {
49
+ removeToast(id);
50
+ }
51
+ }, toast.duration);
52
+ timers.current[id] = t;
53
+ }
54
+ return id;
55
+ }, [removeToast]);
56
+ useEffect(() => {
57
+ return () => {
58
+ // cleanup timers on unmount
59
+ Object.values(timers.current).forEach(t => clearTimeout(t));
60
+ timers.current = {};
61
+ };
62
+ }, []);
63
+ return (_jsxs(ToastContext.Provider, { value: { addToast, removeToast }, children: [children, _jsx(View, { pointerEvents: "box-none", style: styles.container, children: _jsx(View, { style: styles.stack, children: toasts.map(t => (_jsx(ToastItem, { ref: el => {
64
+ toastRefs.current[t.id] = el;
65
+ }, toast: t, onClose: removeToast }, t.id))) }) })] }));
66
+ };
67
+ export const useToast = () => {
68
+ const ctx = useContext(ToastContext);
69
+ if (!ctx)
70
+ throw new Error('useToast must be used within ToastProvider');
71
+ return ctx;
72
+ };
73
+ export default ToastContext;
74
+ const styles = StyleSheet.create(theme => ({
75
+ container: {
76
+ position: 'absolute',
77
+ left: 0,
78
+ right: 0,
79
+ bottom: 0,
80
+ alignItems: 'stretch',
81
+ paddingBottom: theme.space['200'],
82
+ pointerEvents: 'box-none',
83
+ },
84
+ stack: {
85
+ width: '100%',
86
+ alignItems: 'center',
87
+ justifyContent: 'flex-end',
88
+ gap: theme.components.toast.stack.gap,
89
+ },
90
+ }));
@@ -0,0 +1,29 @@
1
+ import type { ReactNode } from 'react';
2
+ export interface ToastOptions {
3
+ id?: string;
4
+ text: string | ReactNode;
5
+ /** Optional action text to display as a link */
6
+ actionText?: string;
7
+ /** Optional callback when action link or toast is pressed */
8
+ onPress?: () => void;
9
+ /** Optional callback when toast is dismissed */
10
+ onDismiss?: () => void;
11
+ /** Optional icon component */
12
+ icon?: React.ComponentType;
13
+ /** Duration in milliseconds; default 6000 */
14
+ duration?: number;
15
+ /** Whether to show the dismiss icon button; default true */
16
+ showDismissIcon?: boolean;
17
+ /** Whether to dismiss the toast when pressed; default true */
18
+ dismissOnPress?: boolean;
19
+ }
20
+ export interface ToastInstance extends ToastOptions {
21
+ id: string;
22
+ /** resolved duration */
23
+ duration: number;
24
+ }
25
+ export interface ToastContextValue {
26
+ addToast: (opts: ToastOptions) => string;
27
+ removeToast: (id: string) => void;
28
+ }
29
+ export default ToastOptions;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ import type { ToastInstance } from './Toast.props';
2
+ export interface ToastItemHandle {
3
+ dismiss: () => void;
4
+ }
5
+ interface Props {
6
+ toast: ToastInstance;
7
+ onClose: (id: string) => void;
8
+ }
9
+ declare const ToastItem: import("react").ForwardRefExoticComponent<Props & import("react").RefAttributes<ToastItemHandle>>;
10
+ export default ToastItem;
@@ -0,0 +1,129 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { CloseSmallIcon } from '@utilitywarehouse/hearth-react-native-icons';
3
+ import { forwardRef, useEffect, useImperativeHandle } from 'react';
4
+ import { AccessibilityInfo, Platform, Pressable, View } from 'react-native';
5
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
6
+ import Animated, { useAnimatedStyle, useSharedValue, withSpring, withTiming, } from 'react-native-reanimated';
7
+ import { StyleSheet, withUnistyles } from 'react-native-unistyles';
8
+ import { scheduleOnRN } from 'react-native-worklets';
9
+ import { BodyText } from '../BodyText';
10
+ import { Icon } from '../Icon';
11
+ import { Link } from '../Link';
12
+ import { UnstyledIconButton } from '../UnstyledIconButton';
13
+ const AnimatedView = Platform.OS === 'web' ? withUnistyles(Animated.View) : Animated.View;
14
+ const ToastItem = forwardRef(({ toast, onClose }, ref) => {
15
+ const translateY = useSharedValue(30);
16
+ const opacity = useSharedValue(0);
17
+ const gestureTranslateY = useSharedValue(0);
18
+ useEffect(() => {
19
+ translateY.value = withTiming(0, { duration: 300 });
20
+ opacity.value = withTiming(1, { duration: 300 });
21
+ // Announce toast content to screen readers
22
+ // Use a slight delay to ensure iOS VoiceOver picks it up
23
+ const timer = setTimeout(() => {
24
+ const message = typeof toast.text === 'string' ? toast.text : 'Toast notification';
25
+ const announcement = toast.actionText ? `${message}, ${toast.actionText}` : message;
26
+ AccessibilityInfo.announceForAccessibility(announcement);
27
+ }, 100);
28
+ return () => clearTimeout(timer);
29
+ }, [toast.text, toast.actionText]);
30
+ const animatedStyle = useAnimatedStyle(() => ({
31
+ transform: [{ translateY: translateY.value + gestureTranslateY.value }],
32
+ opacity: opacity.value,
33
+ }));
34
+ const handleDismiss = (fromGesture = false) => {
35
+ 'worklet';
36
+ // Call onDismiss callback if provided
37
+ if (toast.onDismiss) {
38
+ scheduleOnRN(toast.onDismiss);
39
+ }
40
+ // animate out then call onClose
41
+ if (!fromGesture) {
42
+ gestureTranslateY.value = 0;
43
+ }
44
+ // Continue from current position and animate further down
45
+ translateY.value = withTiming(100, { duration: 250 });
46
+ opacity.value = withTiming(0, { duration: 250 }, finished => {
47
+ if (finished)
48
+ scheduleOnRN(onClose, toast.id);
49
+ });
50
+ };
51
+ useImperativeHandle(ref, () => ({
52
+ dismiss: handleDismiss,
53
+ }));
54
+ const panGesture = Gesture.Pan()
55
+ .onUpdate(event => {
56
+ // only allow downward drag
57
+ if (event.translationY > 0) {
58
+ gestureTranslateY.value = event.translationY;
59
+ }
60
+ })
61
+ .onEnd(event => {
62
+ if (event.translationY > 30 || event.velocityY > 800) {
63
+ handleDismiss(true);
64
+ }
65
+ else {
66
+ // spring back to original position
67
+ gestureTranslateY.value = withSpring(0, {
68
+ damping: 20,
69
+ stiffness: 300,
70
+ });
71
+ }
72
+ });
73
+ const IconComp = toast.icon;
74
+ const showDismissIcon = toast.showDismissIcon !== false; // default true
75
+ const dismissOnPress = toast.dismissOnPress === true; // default false
76
+ const handlePress = () => {
77
+ if (toast.onPress) {
78
+ toast.onPress();
79
+ if (dismissOnPress) {
80
+ handleDismiss(false);
81
+ }
82
+ }
83
+ };
84
+ const toastContent = (_jsxs(View, { style: styles.inner, children: [_jsxs(View, { style: styles.content, children: [IconComp ? (_jsx(View, { style: styles.iconWrap, children: _jsx(Icon, { as: IconComp, style: styles.icon }) })) : null, _jsx(BodyText, { inverted: true, children: toast.text })] }), toast.actionText ? (_jsx(Link, { onPress: handlePress, showIcon: false, inverted: true, children: toast.actionText })) : null, showDismissIcon ? (_jsx(View, { style: styles.actions, children: _jsx(UnstyledIconButton, { icon: CloseSmallIcon, accessibilityLabel: "Dismiss", inverted: true, onPress: () => handleDismiss(false) }) })) : null] }));
85
+ return (_jsx(GestureDetector, { gesture: panGesture, children: _jsx(AnimatedView, { style: [styles.toast, animatedStyle], ...(Platform.OS === 'ios' && {
86
+ accessible: true,
87
+ accessibilityRole: 'alert',
88
+ accessibilityLiveRegion: 'polite',
89
+ }), importantForAccessibility: Platform.OS === 'android' ? 'no-hide-descendants' : undefined, children: toast.onPress ? (_jsx(Pressable, { onPress: handlePress, style: styles.pressable, children: toastContent })) : (toastContent) }) }));
90
+ });
91
+ ToastItem.displayName = 'ToastItem';
92
+ const styles = StyleSheet.create(theme => ({
93
+ toast: {
94
+ backgroundColor: theme.components.toast.backgroundColor,
95
+ borderRadius: theme.components.toast.borderRadius,
96
+ padding: theme.components.toast.padding,
97
+ width: '95%',
98
+ },
99
+ pressable: {
100
+ width: '100%',
101
+ },
102
+ inner: {
103
+ flexDirection: 'row',
104
+ alignItems: 'center',
105
+ width: '100%',
106
+ gap: theme.components.toast.gap,
107
+ },
108
+ iconWrap: {
109
+ width: 24,
110
+ height: 24,
111
+ justifyContent: 'center',
112
+ alignItems: 'center',
113
+ flexShrink: 0,
114
+ },
115
+ icon: {
116
+ color: theme.color.icon.inverted,
117
+ },
118
+ content: {
119
+ flex: 1,
120
+ gap: theme.components.toast.text.gap,
121
+ flexDirection: 'row',
122
+ alignItems: 'center',
123
+ minWidth: 0,
124
+ },
125
+ actions: {
126
+ flexShrink: 0,
127
+ },
128
+ }));
129
+ export default ToastItem;
@@ -0,0 +1,3 @@
1
+ export { ToastProvider, useToast, useToastContext } from './Toast.context';
2
+ export type { ToastInstance, ToastOptions } from './Toast.props';
3
+ export { default as ToastItem } from './ToastItem';
@@ -0,0 +1,2 @@
1
+ export { ToastProvider, useToast, useToastContext } from './Toast.context';
2
+ export { default as ToastItem } from './ToastItem';