@utilitywarehouse/hearth-react-native 0.31.1 → 0.32.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 (65) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-lint.log +13 -13
  3. package/CHANGELOG.md +55 -0
  4. package/build/components/Rating/Rating.d.ts +6 -0
  5. package/build/components/Rating/Rating.js +76 -0
  6. package/build/components/Rating/Rating.props.d.ts +18 -0
  7. package/build/components/Rating/Rating.props.js +1 -0
  8. package/build/components/Rating/RatingStarEmpty.d.ts +6 -0
  9. package/build/components/Rating/RatingStarEmpty.js +9 -0
  10. package/build/components/Rating/RatingStarFilled.d.ts +6 -0
  11. package/build/components/Rating/RatingStarFilled.js +9 -0
  12. package/build/components/Rating/index.d.ts +2 -0
  13. package/build/components/Rating/index.js +1 -0
  14. package/build/components/Roundel/Roundel.d.ts +6 -0
  15. package/build/components/Roundel/Roundel.js +40 -0
  16. package/build/components/Roundel/Roundel.props.d.ts +6 -0
  17. package/build/components/Roundel/Roundel.props.js +1 -0
  18. package/build/components/Roundel/index.d.ts +2 -0
  19. package/build/components/Roundel/index.js +1 -0
  20. package/build/components/StepperInput/StepperButton.d.ts +22 -0
  21. package/build/components/StepperInput/StepperButton.js +55 -0
  22. package/build/components/StepperInput/StepperInput.d.ts +6 -0
  23. package/build/components/StepperInput/StepperInput.js +196 -0
  24. package/build/components/StepperInput/StepperInput.props.d.ts +31 -0
  25. package/build/components/StepperInput/StepperInput.props.js +1 -0
  26. package/build/components/StepperInput/index.d.ts +2 -0
  27. package/build/components/StepperInput/index.js +1 -0
  28. package/build/components/Textarea/Textarea.d.ts +1 -1
  29. package/build/components/Textarea/Textarea.js +10 -3
  30. package/build/components/Textarea/Textarea.props.d.ts +11 -0
  31. package/build/components/index.d.ts +3 -0
  32. package/build/components/index.js +3 -0
  33. package/docs/adding-shadows.mdx +2 -2
  34. package/docs/changelog.mdx +16 -0
  35. package/docs/components/AllComponents.web.tsx +30 -1
  36. package/docs/dark-mode-best-practice.mdx +328 -0
  37. package/package.json +3 -3
  38. package/src/components/Modal/Modal.docs.mdx +58 -4
  39. package/src/components/NavModal/NavModal.docs.mdx +2 -2
  40. package/src/components/Rating/Rating.docs.mdx +178 -0
  41. package/src/components/Rating/Rating.figma.tsx +20 -0
  42. package/src/components/Rating/Rating.props.ts +22 -0
  43. package/src/components/Rating/Rating.stories.tsx +95 -0
  44. package/src/components/Rating/Rating.tsx +140 -0
  45. package/src/components/Rating/RatingStarEmpty.tsx +22 -0
  46. package/src/components/Rating/RatingStarFilled.tsx +27 -0
  47. package/src/components/Rating/index.ts +2 -0
  48. package/src/components/Roundel/Roundel.docs.mdx +48 -0
  49. package/src/components/Roundel/Roundel.figma.tsx +17 -0
  50. package/src/components/Roundel/Roundel.props.ts +8 -0
  51. package/src/components/Roundel/Roundel.stories.tsx +49 -0
  52. package/src/components/Roundel/Roundel.tsx +51 -0
  53. package/src/components/Roundel/index.ts +2 -0
  54. package/src/components/StepperInput/StepperButton.tsx +83 -0
  55. package/src/components/StepperInput/StepperInput.docs.mdx +121 -0
  56. package/src/components/StepperInput/StepperInput.figma.tsx +45 -0
  57. package/src/components/StepperInput/StepperInput.props.ts +39 -0
  58. package/src/components/StepperInput/StepperInput.stories.tsx +270 -0
  59. package/src/components/StepperInput/StepperInput.tsx +349 -0
  60. package/src/components/StepperInput/index.ts +2 -0
  61. package/src/components/Textarea/Textarea.docs.mdx +2 -0
  62. package/src/components/Textarea/Textarea.props.ts +11 -0
  63. package/src/components/Textarea/Textarea.stories.tsx +14 -0
  64. package/src/components/Textarea/Textarea.tsx +11 -2
  65. package/src/components/index.ts +3 -0
@@ -0,0 +1 @@
1
+ export { default as StepperInput } from './StepperInput';
@@ -7,5 +7,5 @@ export declare const TextareaComponent: import("@gluestack-ui/textarea/lib/types
7
7
  };
8
8
  }, import("react-native").TextInputProps>;
9
9
  export declare const TextareaField: import("react").ForwardRefExoticComponent<import("react-native").TextInputProps & import("react").RefAttributes<import("react-native").TextInputProps> & import("@gluestack-ui/textarea/lib/typescript/types").IInputProps>;
10
- declare const Textarea: ({ validationStatus, children, resizable, disabled, focused, readonly, label, labelVariant, helperText, validText, invalidText, required, helperIcon, onLayout, ...props }: TextareaProps) => import("react/jsx-runtime").JSX.Element;
10
+ declare const Textarea: ({ validationStatus, children, resizable, defaultHeight, disabled, focused, readonly, label, labelVariant, helperText, validText, invalidText, required, helperIcon, onLayout, ...props }: TextareaProps) => import("react/jsx-runtime").JSX.Element;
11
11
  export default Textarea;
@@ -18,7 +18,7 @@ export const TextareaField = TextareaComponent.Input;
18
18
  const DEFAULT_TEXTAREA_HEIGHT = 96;
19
19
  const RESIZE_HANDLE_TOUCH_SIZE = 28;
20
20
  const RESIZE_HANDLE_ICON_SIZE = 9;
21
- const Textarea = ({ validationStatus = 'initial', children, resizable = false, disabled, focused, readonly, label, labelVariant, helperText, validText, invalidText, required, helperIcon, onLayout, ...props }) => {
21
+ const Textarea = ({ validationStatus = 'initial', children, resizable = false, defaultHeight, disabled, focused, readonly, label, labelVariant, helperText, validText, invalidText, required, helperIcon, onLayout, ...props }) => {
22
22
  const formFieldContext = useFormFieldContext();
23
23
  const hasMeasuredHeight = useRef(false);
24
24
  const textareaLabel = label ?? formFieldContext?.label;
@@ -29,14 +29,21 @@ const Textarea = ({ validationStatus = 'initial', children, resizable = false, d
29
29
  const textareaDisabled = disabled ?? formFieldContext?.disabled;
30
30
  const textareaReadonly = readonly ?? formFieldContext?.readonly;
31
31
  const textareaValidationStatus = formFieldContext?.validationStatus ?? validationStatus;
32
- const textareaHeight = useSharedValue(DEFAULT_TEXTAREA_HEIGHT);
33
- const resizeStartHeight = useSharedValue(DEFAULT_TEXTAREA_HEIGHT);
32
+ const textareaDefaultHeight = defaultHeight ?? DEFAULT_TEXTAREA_HEIGHT;
33
+ const textareaHeight = useSharedValue(textareaDefaultHeight);
34
+ const resizeStartHeight = useSharedValue(textareaDefaultHeight);
34
35
  const theme = useTheme();
35
36
  useEffect(() => {
36
37
  if (formFieldContext?.setShouldHandleAccessibility) {
37
38
  formFieldContext.setShouldHandleAccessibility(true);
38
39
  }
39
40
  }, [formFieldContext]);
41
+ useEffect(() => {
42
+ if (!hasMeasuredHeight.current) {
43
+ textareaHeight.value = textareaDefaultHeight;
44
+ resizeStartHeight.value = textareaDefaultHeight;
45
+ }
46
+ }, [resizeStartHeight, textareaDefaultHeight, textareaHeight]);
40
47
  const getAccessibilityLabel = () => {
41
48
  let accessibilityLabel = '';
42
49
  if (textareaLabel) {
@@ -1,5 +1,16 @@
1
1
  import type { TextInputProps, ViewProps } from 'react-native';
2
2
  export interface TextareaBaseProps {
3
+ /**
4
+ * Sets the initial height of a resizable textarea in pixels.
5
+ * Has no effect unless `resizable` is enabled.
6
+ *
7
+ * @type number
8
+ * @example
9
+ * ```tsx
10
+ * <Textarea resizable defaultHeight={140} />
11
+ * ```
12
+ */
13
+ defaultHeight?: number;
3
14
  /**
4
15
  * If true, the textarea can be resized vertically using a drag handle.
5
16
  *
@@ -47,11 +47,14 @@ export * from './ProgressBar';
47
47
  export * from './ProgressStepper';
48
48
  export * from './Radio';
49
49
  export * from './RadioCard';
50
+ export * from './Rating';
51
+ export * from './Roundel';
50
52
  export * from './SectionHeader';
51
53
  export * from './SegmentedControl';
52
54
  export * from './Select';
53
55
  export * from './Skeleton';
54
56
  export * from './Spinner';
57
+ export * from './StepperInput';
55
58
  export * from './Switch';
56
59
  export * from './Table';
57
60
  export * from './Tabs';
@@ -48,11 +48,14 @@ export * from './ProgressBar';
48
48
  export * from './ProgressStepper';
49
49
  export * from './Radio';
50
50
  export * from './RadioCard';
51
+ export * from './Rating';
52
+ export * from './Roundel';
51
53
  export * from './SectionHeader';
52
54
  export * from './SegmentedControl';
53
55
  export * from './Select';
54
56
  export * from './Skeleton';
55
57
  export * from './Spinner';
58
+ export * from './StepperInput';
56
59
  export * from './Switch';
57
60
  export * from './Table';
58
61
  export * from './Tabs';
@@ -62,6 +62,6 @@ const MyComponent = () => <Card shadowColor="brand">{/* Card content */}</Card>;
62
62
  <NextPrevPage
63
63
  prevLink="all-components"
64
64
  prevTitle="All Components"
65
- nextLink="primitives-box"
66
- nextTitle="Box"
65
+ nextLink="guides-dark-mode-best-practice"
66
+ nextTitle="Dark Mode Best Practice"
67
67
  />
@@ -9,6 +9,22 @@ import { BackToTopButton, NextPrevPage } from './components';
9
9
  The changelog for the Hearth React Native library. Here you can find all the changes, improvements, and bug fixes for each version.
10
10
 
11
11
 
12
+ ## 0.31.1
13
+
14
+ ### Patch Changes
15
+
16
+ - [#1119](https://github.com/utilitywarehouse/hearth/pull/1119) [`19415d4`](https://github.com/utilitywarehouse/hearth/commit/19415d4d54458b3fb019df6647b9a5e4c375b672) Thanks [@jordmccord](https://github.com/jordmccord)! - 🐛 [FIX]: Refresh dark mode tokens across components and semantic colors.
17
+
18
+ Dark mode color tokens have been updated across semantic and component tokens to improve contrast and visual consistency. This also fixes `TableHeaderCell` text colors so purple and white header variants resolve the correct foreground token.
19
+
20
+ **Components affected**:
21
+ - dark mode tokens
22
+ - `TableHeaderCell`
23
+
24
+ **Developer changes**:
25
+
26
+ No code changes are required unless you rely on the previous dark mode token values or visual snapshots.
27
+
12
28
  ## 0.31.0
13
29
 
14
30
  ### Minor Changes
@@ -87,12 +87,15 @@ import {
87
87
  RadioCard,
88
88
  RadioCardGroup,
89
89
  RadioGroup,
90
+ Rating,
91
+ Roundel,
90
92
  SectionHeader,
91
93
  SegmentedControl,
92
94
  SegmentedControlOption,
93
95
  Select,
94
96
  Skeleton,
95
97
  Spinner,
98
+ StepperInput,
96
99
  Switch,
97
100
  Tab,
98
101
  Table,
@@ -145,6 +148,8 @@ const ComponentWrapper = ({
145
148
  const AllComponents: React.FC = () => {
146
149
  const [comboboxValue, setComboboxValue] = React.useState<string | null>('uk');
147
150
  const [selectValue, setSelectValue] = React.useState('1');
151
+ const [stepperValue, setStepperValue] = React.useState('10');
152
+ const [ratingValue, setRatingValue] = React.useState<0 | 1 | 2 | 3 | 4 | 5>(3);
148
153
  const [toggleButtonValue, setToggleButtonValue] = React.useState('');
149
154
  const bottomSheetRef = useRef<BottomSheet>(null);
150
155
  const handleOpenPress = useCallback(() => {
@@ -233,6 +238,7 @@ const AllComponents: React.FC = () => {
233
238
  </View>
234
239
  </Center>
235
240
  </ComponentWrapper>
241
+
236
242
  <ComponentWrapper name="Banner" link="components-banner">
237
243
  <Center flex={1} p="200">
238
244
  <Banner
@@ -733,7 +739,20 @@ const AllComponents: React.FC = () => {
733
739
  </RadioCardGroup>
734
740
  </Center>
735
741
  </ComponentWrapper>
736
-
742
+ <ComponentWrapper name="Rating" link="components-rating">
743
+ <Center flex={1} padding="200">
744
+ <Rating value={ratingValue} onChange={setRatingValue} />
745
+ </Center>
746
+ </ComponentWrapper>
747
+ <ComponentWrapper name="Roundel" link="components-roundel">
748
+ <Center flex={1}>
749
+ <Flex direction="row" spacing="md" alignItems="center">
750
+ <Roundel variant="success" />
751
+ <Roundel variant="pending" />
752
+ <Roundel variant="error" />
753
+ </Flex>
754
+ </Center>
755
+ </ComponentWrapper>
737
756
  <ComponentWrapper name="Section Header" link="components-section-header">
738
757
  <Center flex={1} p="300">
739
758
  <SectionHeader
@@ -790,6 +809,16 @@ const AllComponents: React.FC = () => {
790
809
  <Switch value={switchEnabled} onValueChange={toggleSwitch} />
791
810
  </Center>
792
811
  </ComponentWrapper>
812
+ <ComponentWrapper name="Stepper Input" link="forms-stepper-input">
813
+ <Center flex={1} padding="200">
814
+ <StepperInput
815
+ label="Label"
816
+ helperText="Helper text"
817
+ value={stepperValue}
818
+ onChangeText={setStepperValue}
819
+ />
820
+ </Center>
821
+ </ComponentWrapper>
793
822
  <ComponentWrapper name="Table" link="components-table">
794
823
  <Center flex={1} px="300">
795
824
  <Box style={{ width: 360 }}>
@@ -0,0 +1,328 @@
1
+ import { Meta } from '@storybook/addon-docs/blocks';
2
+ import { SearchMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
3
+ import SceneBroadbandDark from '@utilitywarehouse/hearth-svg-assets/lib/scene-broadband-dark.svg';
4
+ import SceneBroadbandLight from '@utilitywarehouse/hearth-svg-assets/lib/scene-broadband-light.svg';
5
+ import SpotPiggyBankDark from '@utilitywarehouse/hearth-svg-assets/lib/spot-piggy-bank-dark.svg';
6
+ import SpotPiggyBankLight from '@utilitywarehouse/hearth-svg-assets/lib/spot-piggy-bank-light.svg';
7
+ import { ScopedTheme } from 'react-native-unistyles';
8
+ import StorybookLink from '../../../shared/storybook/StorybookLink';
9
+ import { Alert, BodyText, Box, Center, Flex, Heading, Icon } from '../src';
10
+ import { BackToTopButton, NextPrevPage, UsageWrap } from './components';
11
+
12
+ <Meta title="Guides / Dark Mode Best Practice" />
13
+ <BackToTopButton />
14
+
15
+ # Dark Mode Best Practice
16
+
17
+ When designing for dark mode in Hearth React Native, it's important to ensure that your UI remains visually appealing and accessible. Here are some best practices to follow:
18
+
19
+ - [Setting Up Dark Mode](#setting-up-dark-mode)
20
+ - [Use Semantic Theme Colours](#use-semantic-theme-colours)
21
+ - [Use Semantic Utility Props](#use-semantic-utility-props)
22
+ - [Use the asset libraries](#use-the-asset-libraries)
23
+ - [Icons](#icons)
24
+ - [Illustrations](#illustrations)
25
+ - [Animations](#animations)
26
+ - [`ThemedImage` component](#themedimage-component)
27
+
28
+ <br />
29
+ <Box my="400">
30
+ <Alert text="By default, all components adapt to the current themed colour mode. This guide will help you understand how to work with and easily support dark mode in your apps using Hearth React Native." />
31
+ </Box>
32
+ <br />
33
+ <br />
34
+
35
+ ## Setting Up Dark Mode
36
+
37
+ By default Hearth React Native theme is set to light mode. To enable dark mode, you can set the `colorMode` property in your theme configuration to `'dark'`.
38
+ This will automatically apply the dark mode color palette and styles across your app.
39
+
40
+ ```tsx
41
+ import { useEffect } from 'react';
42
+ import { Appearance } from 'react-native';
43
+ import { useColorMode } from '@utilitywarehouse/hearth-react-native';
44
+
45
+ const App = () => {
46
+ // To set the colour mode use the useColorMode hook and set the color mode
47
+ // to the current system preference on app load
48
+ const [colorMode, setColorMode] = useColorMode();
49
+
50
+ // You can optionally set the theme from the system preference on app load or
51
+ // load from async storage if you are persisting the user's theme choice
52
+ useEffect(() => {
53
+ setColorMode(Appearance.getColorScheme() || 'light');
54
+ }, []);
55
+
56
+ const toggleColorMode = () => {
57
+ setColorMode(colorMode === 'light' ? 'dark' : 'light');
58
+ };
59
+
60
+ return (/* Your app content */);
61
+ };
62
+ ```
63
+
64
+ You can also use the Unistyles runtime to set the theme outside of React components, [read more here](https://www.unistyl.es/v3/guides/theming/#change-theme):
65
+
66
+ ```tsx
67
+ import { UnistylesRuntime } from 'react-native-unistyles';
68
+
69
+ const toggleTheme = () => {
70
+ const theme = UnistylesRuntime.getTheme();
71
+ UnistylesRuntime.setTheme(theme === 'dark' ? 'light' : 'dark');
72
+ };
73
+ ```
74
+
75
+ ## Use Semantic Theme Colours
76
+
77
+ When styling your components, it's best to use the semantic colour tokens provided by the Hearth theme. This ensures that your colours will
78
+ automatically adapt to both light and dark modes without needing to write custom styles for each mode.
79
+
80
+ To use the semantic colours, you can access them from the theme object in your styles:
81
+
82
+ ```tsx
83
+ import { StyleSheet } from '@utilitywarehouse/hearth-react-native';
84
+
85
+ const styles = StyleSheet.create(theme => ({
86
+ text: {
87
+ color: theme.colors.text.primary, // Use semantic colour token
88
+ },
89
+ }));
90
+ ```
91
+
92
+ You can also use the `useTheme` hook to access the theme colours directly in your components:
93
+
94
+ <UsageWrap>
95
+ <Center>
96
+ <Flex direction="row" spacing="lg">
97
+ <ScopedTheme name="dark">
98
+ <Box backgroundColor="primary" p="400">
99
+ <Center>
100
+ <BodyText>This text adapts to dark mode!</BodyText>
101
+ </Center>
102
+ </Box>
103
+ </ScopedTheme>
104
+ <ScopedTheme name="light">
105
+ <Box backgroundColor="primary" p="400">
106
+ <Center>
107
+ <BodyText>This text adapts to light mode!</BodyText>
108
+ </Center>
109
+ </Box>
110
+ </ScopedTheme>
111
+ </Flex>
112
+ </Center>
113
+ </UsageWrap>
114
+
115
+ ```tsx
116
+ import { useTheme } from '@utilitywarehouse/hearth-react-native';
117
+
118
+ const MyComponent = () => {
119
+ const theme = useTheme();
120
+
121
+ return <Text style={{ color: theme.colors.text.primary }}>This text adapts to dark mode!</Text>;
122
+ };
123
+ ```
124
+
125
+ To learn more about the available colours in the Hearth theme, check out the <StorybookLink to="theme-tokens">theme tokens documentation</StorybookLink>.
126
+
127
+ ## Use Semantic Utility Props
128
+
129
+ In addition to using semantic colour tokens, you should also use the semantic utility props provided by Hearth components.
130
+ These props are designed to work with the theme and will automatically adjust their styles based on the current colour mode.
131
+
132
+ For example, instead of setting a background colour directly, you can use the `backgroundColor` or, for text, the `color` prop with a semantic colour value:
133
+
134
+ <UsageWrap>
135
+ <Center spacing="lg">
136
+ <Box>
137
+ <BodyText weight="semibold" mb="100">
138
+ {'Light Mode'}
139
+ </BodyText>
140
+ <Flex direction="row" spacing="lg">
141
+ <ScopedTheme name="light">
142
+ <Box backgroundColor="brand" p="400">
143
+ <BodyText color="inverted">
144
+ This branded box and text adapt to light or dark mode!
145
+ </BodyText>
146
+ </Box>
147
+ <Box backgroundColor="secondary" p="400">
148
+ <BodyText color="primary">This box and text adapt to light or dark mode!</BodyText>
149
+ <BodyText color="secondary">This secondary text adapt to light or dark mode!</BodyText>
150
+ </Box>
151
+ </ScopedTheme>
152
+ </Flex>
153
+ </Box>
154
+ <Box>
155
+ <BodyText weight="semibold" mb="100">
156
+ {'Dark Mode'}
157
+ </BodyText>
158
+ <Flex direction="row" spacing="lg">
159
+ <ScopedTheme name="dark">
160
+ <Box backgroundColor="brand" p="400">
161
+ <BodyText color="inverted">
162
+ This branded box and text adapt to light or dark mode!
163
+ </BodyText>
164
+ </Box>
165
+ <Box backgroundColor="secondary" p="400">
166
+ <BodyText color="primary">This box and text adapt to light or dark mode!</BodyText>
167
+ <BodyText color="secondary">This secondary text adapt to light or dark mode!</BodyText>
168
+ </Box>
169
+ </ScopedTheme>
170
+ </Flex>
171
+ </Box>
172
+ </Center>
173
+ </UsageWrap>
174
+
175
+ ```tsx
176
+ <Box backgroundColor="brand" p="400">
177
+ <BodyText color="inverted">This branded box and text adapt to light or dark mode!</BodyText>
178
+ </Box>
179
+ <Box backgroundColor="secondary" p="400">
180
+ <BodyText color="primary">This box and text adapt to light or dark mode!</BodyText>
181
+ <BodyText color="secondary">This secondary text adapt to light or dark mode!</BodyText>
182
+ </Box>
183
+ ```
184
+
185
+ By using these semantic utility props, you can ensure that your components will look great and be accessible in both light and dark
186
+ modes without needing to write custom styles for each mode.
187
+
188
+ ## Use the asset libraries
189
+
190
+ Hearth provides asset libraries for icons and illustrations that are designed to work well in both light and dark modes.
191
+ When using these assets, make sure to choose the appropriate version (light or dark) based on the current colour mode.
192
+
193
+ ### Icons
194
+
195
+ When using icons from the `@utilitywarehouse/hearth-react-native-icons` library, you can automatically handle light and dark mode
196
+ by using the `color` prop and the `Icon` component with a semantic colour token (by default the `Icon` will use the `theme.colors.icon.primary`
197
+ colour which adapts to light and dark mode):
198
+
199
+ <UsageWrap>
200
+ <Center>
201
+ <Flex direction="row" spacing="lg">
202
+ <ScopedTheme name="light">
203
+ <Box p="400" backgroundColor="secondary">
204
+ <Icon as={SearchMediumIcon} />
205
+ </Box>
206
+ </ScopedTheme>
207
+ <ScopedTheme name="dark">
208
+ <Box p="400" backgroundColor="secondary">
209
+ <Icon as={SearchMediumIcon} color="warmWhite0" />
210
+ </Box>
211
+ </ScopedTheme>
212
+ </Flex>
213
+ </Center>
214
+ </UsageWrap>
215
+
216
+ ```tsx
217
+ import { Icon , useTheme } from '@utilitywarehouse/hearth-react-native';
218
+ import { SearchMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
219
+
220
+ <Icon as={SearchMediumIcon} />;
221
+
222
+ // Or to specify a colour
223
+ const theme = useTheme();
224
+ ...
225
+ <SearchMediumIcon color={theme.colors.icon.primary} />;
226
+ ```
227
+
228
+ To learn more about the available icons, check out the [icon library documentation](https://hearth.prod.uw.systems/icons/?path=/docs/introduction--docs).
229
+
230
+ ### Illustrations
231
+
232
+ When using illustrations from the `@utilitywarehouse/hearth-svg-assets` library, you'll need to import both the light and dark versions
233
+ of the illustration and conditionally render the appropriate one based on the current colour mode.
234
+
235
+ <UsageWrap>
236
+ <Center>
237
+ <Flex direction="row" spacing="lg">
238
+ <ScopedTheme name="light">
239
+ <Box p="400" backgroundColor="secondary">
240
+ <SceneBroadbandLight width={200} height={200} />
241
+ </Box>
242
+ </ScopedTheme>
243
+ <ScopedTheme name="dark">
244
+ <Box p="400" backgroundColor="secondary">
245
+ <SceneBroadbandDark width={200} height={200} />
246
+ </Box>
247
+ </ScopedTheme>
248
+ </Flex>
249
+ </Center>
250
+ </UsageWrap>
251
+
252
+ ```tsx
253
+ import { useColorMode } from '@utilitywarehouse/hearth-react-native';
254
+ import SceneBroadbandDark from '@utilitywarehouse/hearth-svg-assets/lib/scene-broadband-dark.svg';
255
+ import SceneBroadbandLight from '@utilitywarehouse/hearth-svg-assets/lib/scene-broadband-light.svg';
256
+
257
+ const MyComponent = () => {
258
+ const [colorMode] = useColorMode();
259
+
260
+ return colorMode === 'dark' ? (
261
+ <SceneBroadbandDark width={200} height={200} />
262
+ ) : (
263
+ <SceneBroadbandLight width={200} height={200} />
264
+ );
265
+ };
266
+ ```
267
+
268
+ The above example is to illustrate the concept of handling light and dark mode with illustrations,
269
+ but you should use a reusable `ThemedImage` component to simplify this pattern, which we will cover in the next section.
270
+
271
+ You can view the available illustrations in the [Hearth SVG asset library documentation](https://hearth.prod.uw.systems/assets/?path=/docs/introduction--docs).
272
+
273
+ ### Animations
274
+
275
+ When using animations from the `@utilitywarehouse/hearth-json-assets` library, you can also import both light and dark versions of the animation.
276
+
277
+ We currently only have the light variation of the animations available, but when the dark versions are added you can use a similar approach to
278
+ the illustrations example above to conditionally render the appropriate version based on the current colour mode.
279
+
280
+ You can view the available animations in the [Hearth JSON asset library documentation](https://hearth.prod.uw.systems/assets/?path=/docs/introduction--docs).
281
+
282
+ ## `ThemedImage` component
283
+
284
+ To simplify the process of handling light and dark mode for images, you can create a reusable `ThemedImage` component that takes both light and dark versions of an image as props and automatically renders the correct one based on the current colour mode.
285
+ Here's an example implementation of a `ThemedImage` component:
286
+
287
+ <UsageWrap>
288
+ <Center>
289
+ <Flex direction="row" spacing="lg">
290
+ <ScopedTheme name="light">
291
+ <Box p="400" backgroundColor="secondary">
292
+ <SpotPiggyBankLight width={200} height={200} />
293
+ </Box>
294
+ </ScopedTheme>
295
+ <ScopedTheme name="dark">
296
+ <Box p="400" backgroundColor="secondary">
297
+ <SpotPiggyBankDark width={200} height={200} />
298
+ </Box>
299
+ </ScopedTheme>
300
+ </Flex>
301
+ </Center>
302
+ </UsageWrap>
303
+
304
+ ```tsx
305
+ import React from 'react';
306
+ import { ThemedImage } from '@utilitywarehouse/hearth-react-native';
307
+ import SpotPiggyBankLight from '@utilitywarehouse/hearth-svg-assets/lib/spot-piggy-bank-light.svg';
308
+ import SpotPiggyBankDark from '@utilitywarehouse/hearth-svg-assets/lib/spot-piggy-bank-dark.svg';
309
+
310
+ const MyComponent = () => {
311
+ return (
312
+ <ThemedImage
313
+ light={SpotPiggyBankLight}
314
+ dark={SpotPiggyBankDark}
315
+ style={{ width: 200, height: 200 }}
316
+ />
317
+ );
318
+ };
319
+ ```
320
+
321
+ See the full `ThemedImage` docs and implementation in the <StorybookLink to="utility-components-themed-image">`ThemedImage` documentation</StorybookLink>.
322
+
323
+ <NextPrevPage
324
+ prevLink="guides-adding-shadows"
325
+ prevTitle="Adding Shadows"
326
+ nextLink="primitives-box"
327
+ nextTitle="Box"
328
+ />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utilitywarehouse/hearth-react-native",
3
- "version": "0.31.1",
3
+ "version": "0.32.0",
4
4
  "description": "Utility Warehouse React Native UI library",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -58,10 +58,10 @@
58
58
  "vite-plugin-svgr": "^4.5.0",
59
59
  "vitest": "^3.2.4",
60
60
  "@utilitywarehouse/hearth-fonts": "^0.0.4",
61
+ "@utilitywarehouse/hearth-react-icons": "^0.8.0",
61
62
  "@utilitywarehouse/hearth-react-native-icons": "^0.8.0",
62
63
  "@utilitywarehouse/hearth-svg-assets": "^0.6.0",
63
- "@utilitywarehouse/hearth-tokens": "^0.2.4",
64
- "@utilitywarehouse/hearth-react-icons": "^0.8.0"
64
+ "@utilitywarehouse/hearth-tokens": "^0.2.4"
65
65
  },
66
66
  "peerDependencies": {
67
67
  "@gorhom/bottom-sheet": ">=5.0.0",
@@ -12,11 +12,14 @@ import * as Stories from './Modal.stories';
12
12
 
13
13
  # Modal
14
14
 
15
- The `Modal` component provides a versatile dialog interface that slides up from the bottom of the screen. It's built on top of the `BottomSheetModal` component and includes pre-configured layouts for common modal patterns including headers, content areas, and action buttons.
15
+ The `Modal` component provides a versatile dialog interface that slides up from the bottom of the screen. It's built on top of
16
+ the <StorybookLink to="components-bottom-sheet">`BottomSheetModal`</StorybookLink>
17
+ component and includes pre-configured layouts for common modal patterns including headers, content areas, and action buttons.
16
18
 
17
- The Modal component is ideal for displaying important information, collecting user input, or presenting choices that require user attention without navigating away from the current screen.
19
+ The Modal component is ideal for displaying important information, collecting user input, or presenting choices that require user attention
20
+ without navigating away from the current screen.
18
21
 
19
- If you need a modal layout inside a React Navigation modal screen, use <StorybookLink to="components-navmodal">`NavModal`</StorybookLink> instead.
22
+ If you need a modal layout inside a React Navigation modal screen, use <StorybookLink to="components-nav-modal">`NavModal`</StorybookLink> instead.
20
23
 
21
24
  - [Playground](#playground)
22
25
  - [Usage](#usage)
@@ -32,6 +35,8 @@ If you need a modal layout inside a React Navigation modal screen, use <Storyboo
32
35
  - [Loading State](#loading-state)
33
36
  - [Without Close Button](#without-close-button)
34
37
  - [Single Action Modal](#single-action-modal)
38
+ - [Navigation Modals](#navigation-modals)
39
+ - [Close handlers and state management](#close-handlers-and-state-management)
35
40
  - [Integration Notes](#integration-notes)
36
41
  - [External Resources](#external-resources)
37
42
 
@@ -514,6 +519,53 @@ const AlertModal = () => {
514
519
 
515
520
  For React Navigation modal screens, use <StorybookLink to="components-navmodal">`NavModal`</StorybookLink>. It contains the extracted screen-based modal layout, background variants, scrollable content handling, and the Android `triggerCloseAnimation()` ref used during navigation dismissal.
516
521
 
522
+ ### Close handlers and state management
523
+
524
+ The Modal component provides multiple ways to handle closing the modal and managing state:
525
+
526
+ - Use the `onPressPrimaryButton`, `onPressSecondaryButton`, and `onPressCloseButton` props to run custom logic when buttons are pressed, such as form validation or API calls, before closing the modal.
527
+ - Control whether the modal should automatically close when action buttons are pressed using the `closeOnPrimaryButtonPress` and `closeOnSecondaryButtonPress` props. This allows you to keep the modal
528
+ open while performing async operations and only close it when those operations are complete.
529
+ - Use the `onChange` prop to detect when the modal is opened or closed based on the index parameter (0 for open, -1 for closed) and manage state accordingly.
530
+
531
+ ```tsx
532
+ const MyModal = () => {
533
+ const modalRef = useRef<BottomSheetModal>(null);
534
+ const [isSubmitting, setIsSubmitting] = useState(false);
535
+
536
+ const handleSubmit = async () => {
537
+ setIsSubmitting(true);
538
+ // Simulate API call
539
+ await new Promise(resolve => setTimeout(resolve, 2000));
540
+ setIsSubmitting(false);
541
+ modalRef.current?.dismiss();
542
+ };
543
+
544
+ return (
545
+ <>
546
+ <Button onPress={() => modalRef.current?.present()}>Open Modal</Button>
547
+
548
+ <Modal
549
+ ref={modalRef}
550
+ heading="Submit Data"
551
+ description="Please confirm your submission"
552
+ primaryButtonText="Submit"
553
+ secondaryButtonText="Cancel"
554
+ onPressPrimaryButton={handleSubmit}
555
+ closeOnPrimaryButtonPress={false} // Keep modal open while submitting
556
+ onChange={index => {
557
+ if (index === -1) {
558
+ // Modal closed, reset state if needed
559
+ setIsSubmitting(false);
560
+ }
561
+ }}
562
+ loading={isSubmitting}
563
+ />
564
+ </>
565
+ );
566
+ };
567
+ ```
568
+
517
569
  ## Integration Notes
518
570
 
519
571
  ### BottomSheetModalProvider
@@ -567,4 +619,6 @@ modalRef.current?.snapToIndex(1);
567
619
 
568
620
  ## External Resources
569
621
 
570
- This component is built on top of [@gorhom/bottom-sheet](https://gorhom.github.io/react-native-bottom-sheet/) (v5). For more information about the underlying BottomSheet functionality, please refer to the [BottomSheet documentation](./BottomSheet.docs.mdx) and the [official documentation](https://gorhom.dev/react-native-bottom-sheet/).
622
+ This component is built on top of [@gorhom/bottom-sheet](https://gorhom.github.io/react-native-bottom-sheet/) (v5). For more information about the
623
+ underlying BottomSheet functionality, please refer to the <StorybookLink to="components-bottomsheet">BottomSheet documentation</StorybookLink> and
624
+ the [official documentation](https://gorhom.dev/react-native-bottom-sheet/).
@@ -6,13 +6,13 @@ import modaliOSVideo from '../../../docs/assets/modal-ios.mp4';
6
6
  import { BackToTopButton, ViewFigmaButton } from '../../../docs/components';
7
7
  import * as Stories from './NavModal.stories';
8
8
 
9
- <Meta title="Components / NavModal" />
9
+ <Meta title="Components / Nav Modal" />
10
10
 
11
11
  <ViewFigmaButton url="https://www.figma.com/design/dLI9bmyMr42LV7dtFeW27J/Hearth-Patterns---Guides?node-id=6314-9103&t=oq3NaPLaAu3di6Db-4" />
12
12
 
13
13
  <BackToTopButton />
14
14
 
15
- # NavModal
15
+ # Nav Modal
16
16
 
17
17
  The `NavModal` component is the screen-based modal layout for navigation flows. Use it when a screen is already being presented by React Navigation with `presentation: 'modal'` or `presentation: 'fullScreenModal'` and you want Hearth's modal structure, actions, and Android close animation support.
18
18