@utilitywarehouse/hearth-react-native 0.28.6 → 0.29.1

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 (47) hide show
  1. package/.storybook/preview.tsx +7 -4
  2. package/.turbo/turbo-build.log +1 -1
  3. package/.turbo/turbo-lint.log +15 -18
  4. package/CHANGELOG.md +59 -0
  5. package/build/components/Combobox/Combobox.js +1 -1
  6. package/build/components/Modal/Modal.d.ts +1 -1
  7. package/build/components/Modal/Modal.js +6 -95
  8. package/build/components/Modal/Modal.props.d.ts +2 -31
  9. package/build/components/Modal/Modal.shared.types.d.ts +22 -0
  10. package/build/components/Modal/Modal.web.d.ts +1 -1
  11. package/build/components/Modal/Modal.web.js +6 -71
  12. package/build/components/NavModal/NavModal.d.ts +3 -0
  13. package/build/components/NavModal/NavModal.js +190 -0
  14. package/build/components/NavModal/NavModal.props.d.ts +15 -0
  15. package/build/components/NavModal/NavModal.props.js +1 -0
  16. package/build/components/NavModal/index.d.ts +2 -0
  17. package/build/components/NavModal/index.js +1 -0
  18. package/build/components/Select/Select.js +1 -1
  19. package/build/components/index.d.ts +2 -1
  20. package/build/components/index.js +2 -1
  21. package/docs/changelog.mdx +34 -0
  22. package/docs/components/AllComponents.web.tsx +709 -689
  23. package/package.json +5 -3
  24. package/src/components/Combobox/Combobox.tsx +1 -1
  25. package/src/components/Modal/Modal.docs.mdx +5 -122
  26. package/src/components/Modal/Modal.props.ts +2 -34
  27. package/src/components/Modal/Modal.shared.types.ts +23 -0
  28. package/src/components/Modal/Modal.stories.tsx +0 -1
  29. package/src/components/Modal/Modal.tsx +11 -174
  30. package/src/components/Modal/Modal.web.tsx +29 -127
  31. package/src/components/NavModal/NavModal.docs.mdx +178 -0
  32. package/src/components/NavModal/NavModal.figma.tsx +13 -0
  33. package/src/components/NavModal/NavModal.props.ts +23 -0
  34. package/src/components/NavModal/NavModal.stories.tsx +131 -0
  35. package/src/components/NavModal/NavModal.tsx +375 -0
  36. package/src/components/NavModal/index.ts +2 -0
  37. package/src/components/Select/Select.tsx +1 -1
  38. package/src/components/index.ts +3 -1
  39. package/build/components/SafeAreaView/SafeAreaView.d.ts +0 -5
  40. package/build/components/SafeAreaView/SafeAreaView.js +0 -117
  41. package/build/components/SafeAreaView/SafeAreaView.props.d.ts +0 -17
  42. package/build/components/SafeAreaView/index.d.ts +0 -2
  43. package/build/components/SafeAreaView/index.js +0 -1
  44. package/src/components/SafeAreaView/SafeAreaView.props.ts +0 -20
  45. package/src/components/SafeAreaView/SafeAreaView.tsx +0 -173
  46. package/src/components/SafeAreaView/index.ts +0 -2
  47. /package/build/components/{SafeAreaView/SafeAreaView.props.js → Modal/Modal.shared.types.js} +0 -0
@@ -1,18 +1,9 @@
1
1
  import { BottomSheetScrollViewMethods, SNAP_POINT_TYPE } from '@gorhom/bottom-sheet';
2
2
  import { BottomSheetModalMethods } from '@gorhom/bottom-sheet/lib/typescript/types';
3
3
  import { CloseMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
4
- import { useCallback, useEffect, useImperativeHandle, useRef } from 'react';
4
+ import { useImperativeHandle, useRef } from 'react';
5
5
  import { AccessibilityInfo, Platform, View, findNodeHandle } from 'react-native';
6
- import Animated, {
7
- Easing,
8
- useAnimatedStyle,
9
- useSharedValue,
10
- withDelay,
11
- withTiming,
12
- } from 'react-native-reanimated';
13
6
  import { StyleSheet } from 'react-native-unistyles';
14
- import { useTheme } from '../../hooks';
15
- import { hexWithOpacity } from '../../utils';
16
7
  import { BodyText } from '../BodyText';
17
8
  import { BottomSheetModal, BottomSheetScrollView } from '../BottomSheet';
18
9
  import { Button } from '../Button';
@@ -38,74 +29,19 @@ const Modal = ({
38
29
  closeOnSecondaryButtonPress = true,
39
30
  loading,
40
31
  loadingHeading = 'Loading...',
32
+ fullscreen = false,
41
33
  image,
42
34
  primaryButtonProps,
43
35
  secondaryButtonProps,
44
36
  closeButtonProps,
45
- inNavModal = false,
46
37
  ...props
47
38
  }: ModalProps) => {
48
39
  const bottomSheetModalRef = useRef<BottomSheetModal>(null);
49
40
  const viewRef = useRef<View>(null);
50
41
  const scrollViewRef = useRef<BottomSheetScrollViewMethods>(null);
51
- const theme = useTheme();
52
- const backgroundOpacity = useSharedValue(0);
53
- const pretendContentTranslateY = useSharedValue(20);
54
-
55
- const triggerCloseAnimation = useCallback(() => {
56
- if (Platform.OS === 'android' && inNavModal) {
57
- pretendContentTranslateY.value = withTiming(20, {
58
- duration: 50,
59
- easing: Easing.in(Easing.quad),
60
- });
61
- backgroundOpacity.value = withTiming(0, {
62
- duration: 100,
63
- easing: Easing.in(Easing.quad),
64
- });
65
- }
66
- }, [Platform.OS, inNavModal, pretendContentTranslateY, backgroundOpacity]);
67
42
 
68
43
  useImperativeHandle(ref, () => ({
69
44
  ...(bottomSheetModalRef.current as BottomSheetModal),
70
- triggerCloseAnimation,
71
- }));
72
-
73
- // Trigger animations on render for inNavModal Android modal
74
- useEffect(() => {
75
- if (Platform.OS === 'android' && inNavModal) {
76
- backgroundOpacity.value = withDelay(
77
- 300,
78
- withTiming(1, {
79
- duration: 200,
80
- easing: Easing.out(Easing.quad),
81
- })
82
- );
83
- pretendContentTranslateY.value = withDelay(
84
- 500,
85
- withTiming(0, {
86
- duration: 300,
87
- easing: Easing.out(Easing.quad),
88
- })
89
- );
90
- }
91
- }, [inNavModal, backgroundOpacity, pretendContentTranslateY]);
92
-
93
- const animatedBackgroundStyle = useAnimatedStyle(() => ({
94
- backgroundColor: hexWithOpacity(
95
- theme.components.overlay.backgroundColor,
96
- backgroundOpacity.value * (theme.components.overlay.opacity / 100)
97
- ),
98
- }));
99
-
100
- const animatedInNavModalStyle = useAnimatedStyle(() => ({
101
- backgroundColor: hexWithOpacity(
102
- theme.components.overlay.backgroundColor,
103
- backgroundOpacity.value * (theme.components.overlay.opacity / 100)
104
- ),
105
- }));
106
-
107
- const animatedPretendContentStyle = useAnimatedStyle(() => ({
108
- transform: [{ translateY: pretendContentTranslateY.value }],
109
45
  }));
110
46
 
111
47
  const handleChange = (index: number, position: number, type: SNAP_POINT_TYPE) => {
@@ -155,6 +91,8 @@ const Modal = ({
155
91
  }
156
92
  };
157
93
 
94
+ const noButtons = !onPressPrimaryButton && !onPressSecondaryButton;
95
+
158
96
  const content = (
159
97
  <>
160
98
  {loading ? (
@@ -214,49 +152,38 @@ const Modal = ({
214
152
  </View>
215
153
  ) : null}
216
154
  {children}
217
- <View style={styles.footer}>
218
- {onPressPrimaryButton && primaryButtonText ? (
219
- <Button
220
- onPress={handlePrimaryButtonPress}
221
- text={primaryButtonText}
222
- {...primaryButtonProps}
223
- variant={(primaryButtonProps?.variant as 'solid') ?? 'solid'}
224
- colorScheme={(primaryButtonProps?.colorScheme as 'highlight') ?? 'highlight'}
225
- />
226
- ) : null}
227
- {onPressSecondaryButton && secondaryButtonText ? (
228
- <Button
229
- onPress={handleSecondaryButtonPress}
230
- text={secondaryButtonText}
231
- {...secondaryButtonProps}
232
- variant={(secondaryButtonProps?.variant as 'outline') ?? 'outline'}
233
- colorScheme={(secondaryButtonProps?.colorScheme as 'functional') ?? 'functional'}
234
- />
235
- ) : null}
236
- </View>
155
+ {!noButtons ? (
156
+ <View style={styles.footer}>
157
+ {onPressPrimaryButton && primaryButtonText ? (
158
+ <Button
159
+ onPress={handlePrimaryButtonPress}
160
+ text={primaryButtonText}
161
+ {...primaryButtonProps}
162
+ variant={(primaryButtonProps?.variant as 'solid') ?? 'solid'}
163
+ colorScheme={(primaryButtonProps?.colorScheme as 'highlight') ?? 'highlight'}
164
+ />
165
+ ) : null}
166
+ {onPressSecondaryButton && secondaryButtonText ? (
167
+ <Button
168
+ onPress={handleSecondaryButtonPress}
169
+ text={secondaryButtonText}
170
+ {...secondaryButtonProps}
171
+ variant={(secondaryButtonProps?.variant as 'outline') ?? 'outline'}
172
+ colorScheme={(secondaryButtonProps?.colorScheme as 'functional') ?? 'functional'}
173
+ />
174
+ ) : null}
175
+ </View>
176
+ ) : null}
237
177
  </View>
238
178
  )}
239
179
  </>
240
180
  );
241
181
 
242
- return inNavModal ? (
243
- <View style={{ flex: 1, backgroundColor: theme.color.background.primary }}>
244
- {Platform.OS === 'android' ? (
245
- <Animated.View style={[styles.androidContainer, animatedBackgroundStyle]}>
246
- <Animated.View style={[styles.pretendContent, animatedPretendContentStyle]} />
247
- </Animated.View>
248
- ) : null}
249
- <Animated.View
250
- style={[styles.inNavModalContainer, Platform.OS === 'android' && animatedInNavModalStyle]}
251
- >
252
- <View style={styles.inNavModalContent}>{content}</View>
253
- </Animated.View>
254
- </View>
255
- ) : (
182
+ return (
256
183
  <BottomSheetModal
257
184
  ref={bottomSheetModalRef}
258
185
  enableDynamicSizing={true}
259
- snapPoints={image ? ['90%'] : props.snapPoints}
186
+ snapPoints={image || fullscreen ? ['90%'] : props.snapPoints}
260
187
  showHandle={typeof loading !== 'undefined' && loading ? false : props.showHandle}
261
188
  accessible={false}
262
189
  {...props}
@@ -269,7 +196,7 @@ const Modal = ({
269
196
  );
270
197
  };
271
198
 
272
- const styles = StyleSheet.create((theme, rt) => ({
199
+ const styles = StyleSheet.create(theme => ({
273
200
  container: {
274
201
  flex: 1,
275
202
  gap: theme.components.modal.gap,
@@ -300,31 +227,6 @@ const styles = StyleSheet.create((theme, rt) => ({
300
227
  footer: {
301
228
  gap: theme.components.modal.action.gap,
302
229
  },
303
- inNavModalContainer: {
304
- flex: 1,
305
- ...(Platform.OS === 'ios' ? { backgroundColor: theme.components.overlay.backgroundColor } : {}),
306
- },
307
- inNavModalContent: {
308
- flex: 1,
309
- borderTopLeftRadius: theme.components.modal.borderRadius,
310
- borderTopRightRadius: theme.components.modal.borderRadius,
311
- backgroundColor: theme.color.surface.neutral.strong,
312
- gap: theme.components.modal.gap,
313
- padding: theme.components.bottomSheet.padding,
314
- paddingBottom: theme.components.modal.padding + rt.insets.bottom,
315
- },
316
- androidContainer: {
317
- height: rt.insets.top + 18,
318
- paddingLeft: theme.components.bottomSheet.padding,
319
- paddingRight: theme.components.bottomSheet.padding,
320
- justifyContent: 'flex-end',
321
- },
322
- pretendContent: {
323
- borderTopLeftRadius: theme.components.modal.borderRadius,
324
- borderTopRightRadius: theme.components.modal.borderRadius,
325
- height: 12,
326
- backgroundColor: theme.components.parts.modalStack.backgroundColorCardTop,
327
- },
328
230
  }));
329
231
 
330
232
  export default Modal;
@@ -0,0 +1,178 @@
1
+ import { Canvas, Controls, Meta } from '@storybook/addon-docs/blocks';
2
+ import StorybookLink from '../../../../../shared/storybook/StorybookLink';
3
+ import modalAndroidVideo from '../../../docs/assets/modal-android.mp4';
4
+ import modaliOSVideo from '../../../docs/assets/modal-ios.mp4';
5
+ import { BackToTopButton, ViewFigmaButton } from '../../../docs/components';
6
+ import * as Stories from './NavModal.stories';
7
+
8
+ <Meta title="Components / NavModal" />
9
+
10
+ <ViewFigmaButton url="https://www.figma.com/design/dLI9bmyMr42LV7dtFeW27J/Hearth-Patterns---Guides?node-id=6314-9103&t=oq3NaPLaAu3di6Db-4" />
11
+
12
+ <BackToTopButton />
13
+
14
+ # NavModal
15
+
16
+ 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.
17
+
18
+ If you need a bottom-sheet modal that you present with a ref, use <StorybookLink to="components-modal">`Modal`</StorybookLink> instead.
19
+
20
+ - [Playground](#playground)
21
+ - [Usage](#usage)
22
+ - [Props](#props)
23
+ - [Features](#features)
24
+ - [Accessibility](#accessibility)
25
+ - [Examples](#examples)
26
+
27
+ ## Playground
28
+
29
+ <Canvas of={Stories.Playground} />
30
+
31
+ <Controls of={Stories.Playground} />
32
+
33
+ ## Usage
34
+
35
+ ```tsx
36
+ import { useNavigation } from '@react-navigation/native';
37
+ import type { NavigationAction } from '@react-navigation/native';
38
+ import { useCallback, useEffect, useRef } from 'react';
39
+ import { Platform, StyleSheet, View } from 'react-native';
40
+
41
+ import {
42
+ BodyText,
43
+ Heading,
44
+ InlineLink,
45
+ NavModal,
46
+ type NavModalRef,
47
+ } from '@utilitywarehouse/hearth-react-native';
48
+
49
+ export default function ModalScreen({ onClose }: { onClose?: () => void }) {
50
+ const modalRef = useRef<NavModalRef>(null);
51
+ const navigation = useNavigation();
52
+ const isClosingRef = useRef(false);
53
+
54
+ const handleClose = useCallback(
55
+ (action?: NavigationAction) => {
56
+ if (Platform.OS === 'ios') {
57
+ if (onClose) {
58
+ onClose();
59
+ } else {
60
+ navigation.goBack();
61
+ }
62
+
63
+ return;
64
+ }
65
+
66
+ if (isClosingRef.current) {
67
+ return;
68
+ }
69
+
70
+ isClosingRef.current = true;
71
+ modalRef.current?.triggerCloseAnimation?.();
72
+
73
+ setTimeout(() => {
74
+ if (onClose) {
75
+ onClose();
76
+ } else if (action) {
77
+ navigation.dispatch(action);
78
+ } else {
79
+ navigation.goBack();
80
+ }
81
+ }, 100);
82
+ },
83
+ [navigation, onClose]
84
+ );
85
+
86
+ useEffect(() => {
87
+ if (Platform.OS === 'android') {
88
+ const unsubscribe = navigation.addListener('beforeRemove', e => {
89
+ if (!isClosingRef.current) {
90
+ e.preventDefault();
91
+ handleClose(e.data.action);
92
+ }
93
+ });
94
+
95
+ return unsubscribe;
96
+ }
97
+ }, [handleClose, navigation]);
98
+
99
+ return (
100
+ <NavModal
101
+ ref={modalRef}
102
+ onPressCloseButton={handleClose}
103
+ primaryButtonText="Action"
104
+ onPressPrimaryButton={handleClose}
105
+ secondaryButtonText="Cancel"
106
+ onPressSecondaryButton={handleClose}
107
+ >
108
+ <View style={styles.container}>
109
+ <Heading size="xl">This is a modal</Heading>
110
+ <BodyText>
111
+ <InlineLink onPress={handleClose} style={styles.link}>
112
+ Go to home screen
113
+ </InlineLink>
114
+ </BodyText>
115
+ </View>
116
+ </NavModal>
117
+ );
118
+ }
119
+
120
+ const styles = StyleSheet.create({
121
+ container: {
122
+ flex: 1,
123
+ alignItems: 'center',
124
+ justifyContent: 'center',
125
+ padding: 20,
126
+ },
127
+ link: {
128
+ marginTop: 15,
129
+ paddingVertical: 15,
130
+ },
131
+ });
132
+ ```
133
+
134
+ ## Props
135
+
136
+ `NavModal` supports Hearth's modal content props plus the navigation-specific `background`, `scrollable`, and `presentation` options. Set `presentation` to the same value you pass to React Navigation so `NavModal` can match the route's sheet-style or full-screen layout. It does not take bottom-sheet props such as `snapPoints`, and it does not manage its own dismissal. Your screen or navigator owns closing behavior.
137
+
138
+ | Property | Type | Description | Default |
139
+ | ------------------------ | ---------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | -------------- |
140
+ | `heading` | `string` | Heading text shown at the top of the modal when no `image` is provided. | - |
141
+ | `description` | `string` | Supporting text shown below the heading when no `image` is provided. | - |
142
+ | `showCloseButton` | `boolean` | Whether to render the close button in the top-right corner. | `true` |
143
+ | `primaryButtonText` | `string` | Label for the primary action button. | - |
144
+ | `secondaryButtonText` | `string` | Label for the secondary action button. | - |
145
+ | `onPressPrimaryButton` | `() => void` | Called when the primary action button is pressed. | - |
146
+ | `onPressSecondaryButton` | `() => void` | Called when the secondary action button is pressed. | - |
147
+ | `onPressCloseButton` | `() => void` | Called when the close button is pressed. | - |
148
+ | `primaryButtonProps` | `Omit<ButtonWithoutChildrenProps, 'children'>` | Extra props forwarded to the primary button. | - |
149
+ | `secondaryButtonProps` | `Omit<ButtonWithoutChildrenProps, 'children'>` | Extra props forwarded to the secondary button. | - |
150
+ | `closeButtonProps` | `Omit<UnstyledIconButtonProps, 'children'>` | Extra props forwarded to the close button. | - |
151
+ | `loading` | `boolean` | Replaces the content with a loading state and spinner. | `false` |
152
+ | `loadingHeading` | `string` | Heading text shown while `loading` is true. | `'Loading...'` |
153
+ | `image` | `ReactNode` | Optional image or illustration shown above the text content. | - |
154
+ | `children` | `ReactNode` | Content rendered inside the modal body. | - |
155
+ | `stickyFooter` | `boolean` | Keeps action buttons pinned to the bottom instead of flowing with the content. | `true` |
156
+ | `background` | `'default' /\| 'brand'` | Switches between the default surface background and the brand background treatment. | `'default'` |
157
+ | `scrollable` | `boolean` | Wraps the content area in a `ScrollView`. Set this to `false` for custom layouts that should not scroll. | `true` |
158
+ | `presentation` | `'modal' \| 'fullScreenModal' \| 'transparentModal' `<br />` \| 'containedModal' \| 'containedTransparentModal'` | Matches the React Navigation screen presentation. `fullScreenModal` uses the full-screen layout; the other values use the sheet-style layout. | `'modal'` |
159
+ | `safeAreaViewProps` | `SafeAreaViewProps` | Extra props forwarded to the `SafeAreaView` wrapping the modal content. | - |
160
+ | `scrollViewProps` | `ScrollViewProps` | Extra props forwarded to the `ScrollView` wrapping the modal content when `scrollable` is true. | - |
161
+
162
+ ## Accessibility
163
+
164
+ `NavModal` keeps the same heading, description, button labeling, and loading-state support as `Modal`, but it does not force accessibility focus on mount. Because this component is used on a navigation-presented screen, React Navigation and the platform own the initial screen focus behavior.
165
+
166
+ ## Examples
167
+
168
+ | ios | android |
169
+ | ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- |
170
+ | <video src={modaliOSVideo} width={300} height="auto" controls loop autoPlay /> | <video src={modalAndroidVideo} width={300} height="auto" controls loop autoPlay /> |
171
+
172
+ ### Brand Background
173
+
174
+ <Canvas of={Stories.BrandBackground} />
175
+
176
+ ### Full-Screen Presentation
177
+
178
+ <Canvas of={Stories.FullScreenPresentation} />
@@ -0,0 +1,13 @@
1
+ import figma from '@figma/code-connect';
2
+ import { NavModal } from '..';
3
+
4
+ figma.connect(
5
+ NavModal,
6
+ 'https://www.figma.com/design/dLI9bmyMr42LV7dtFeW27J/Hearth-Patterns---Guides?node-id=6314-9103&t=oq3NaPLaAu3di6Db-4',
7
+ {
8
+ props: {},
9
+ example: props => {
10
+ return <NavModal>{/* Your content here */}</NavModal>;
11
+ },
12
+ }
13
+ );
@@ -0,0 +1,23 @@
1
+ import { Ref } from 'react';
2
+ import { SafeAreaViewProps } from 'react-native-safe-area-context';
3
+ import { ModalCommonProps } from '../Modal/Modal.shared.types';
4
+
5
+ export interface NavModalRef {
6
+ triggerCloseAnimation?: () => void;
7
+ }
8
+
9
+ interface NavModalProps extends ModalCommonProps {
10
+ ref?: Ref<NavModalRef>;
11
+ background?: 'default' | 'brand';
12
+ scrollable?: boolean;
13
+ presentation?:
14
+ | 'fullScreenModal'
15
+ | 'modal'
16
+ | 'transparentModal'
17
+ | 'containedModal'
18
+ | 'containedTransparentModal';
19
+ safeAreaViewProps?: Omit<SafeAreaViewProps, 'children'>;
20
+ scrollViewProps?: Omit<SafeAreaViewProps, 'children'>;
21
+ }
22
+
23
+ export default NavModalProps;
@@ -0,0 +1,131 @@
1
+ import { Meta, StoryObj } from '@storybook/react-native';
2
+ import { Platform, View } from 'react-native';
3
+ import { BodyText } from '../BodyText';
4
+ import { Box } from '../Box';
5
+ import NavModal from './NavModal';
6
+
7
+ const meta = {
8
+ title: 'Stories / NavModal',
9
+ component: NavModal,
10
+ parameters: {
11
+ layout: 'fullscreen',
12
+ noScroll: true,
13
+ },
14
+ argTypes: {
15
+ heading: {
16
+ control: 'text',
17
+ description: 'The heading text to be displayed.',
18
+ },
19
+ description: {
20
+ control: 'text',
21
+ description: 'The description text to be displayed.',
22
+ },
23
+ showCloseButton: {
24
+ control: 'boolean',
25
+ description: 'Whether to show the close button.',
26
+ },
27
+ primaryButtonText: {
28
+ control: 'text',
29
+ description: 'Text for the primary button.',
30
+ },
31
+ secondaryButtonText: {
32
+ control: 'text',
33
+ description: 'Text for the secondary button.',
34
+ },
35
+ loading: {
36
+ control: 'boolean',
37
+ description: 'Whether the modal is in a loading state.',
38
+ },
39
+ loadingHeading: {
40
+ control: 'text',
41
+ description: 'The heading text to be displayed when loading is true.',
42
+ },
43
+ background: {
44
+ control: 'radio',
45
+ options: ['default', 'brand'],
46
+ description: 'Sets the modal background.',
47
+ },
48
+ presentation: {
49
+ control: 'radio',
50
+ options: [
51
+ 'modal',
52
+ 'fullScreenModal',
53
+ 'transparentModal',
54
+ 'containedModal',
55
+ 'containedTransparentModal',
56
+ ],
57
+ description: 'Matches the React Navigation presentation style for the screen.',
58
+ },
59
+ scrollable: {
60
+ control: 'boolean',
61
+ description: 'Whether the modal content should be wrapped in a ScrollView.',
62
+ },
63
+ },
64
+ actions: {
65
+ onPressPrimaryButton: { action: () => null },
66
+ onPressSecondaryButton: { action: () => null },
67
+ onPressCloseButton: { action: () => null },
68
+ },
69
+ args: {
70
+ heading: 'NavModal Heading',
71
+ description: 'This is a navigation modal description',
72
+ showCloseButton: true,
73
+ primaryButtonText: 'Primary',
74
+ secondaryButtonText: 'Cancel',
75
+ loading: false,
76
+ background: 'default',
77
+ scrollable: true,
78
+ presentation: 'modal',
79
+ onPressCloseButton: () => null,
80
+ onPressPrimaryButton: () => null,
81
+ onPressSecondaryButton: () => null,
82
+ },
83
+ } satisfies Meta<typeof NavModal>;
84
+
85
+ export default meta;
86
+ type Story = StoryObj<typeof meta>;
87
+
88
+ export const Playground: Story = {
89
+ render: (args: StoryObj<typeof meta.args>) => (
90
+ <View style={Platform.OS === 'web' ? { width: 400, height: 720 } : { flex: 1 }}>
91
+ <NavModal {...args}>
92
+ <Box gap="200">
93
+ <BodyText>This is a navigation modal with content.</BodyText>
94
+ <BodyText>Use it for React Navigation modal screens instead of bottom sheets.</BodyText>
95
+ </Box>
96
+ </NavModal>
97
+ </View>
98
+ ),
99
+ };
100
+
101
+ export const BrandBackground: Story = {
102
+ args: {
103
+ background: 'brand',
104
+ },
105
+ render: (args: StoryObj<typeof meta.args>) => (
106
+ <View style={Platform.OS === 'web' ? { width: 400, height: 720 } : { flex: 1 }}>
107
+ <NavModal {...args}>
108
+ <Box gap="200">
109
+ <BodyText inverted>Brand background content stays readable with inverted text.</BodyText>
110
+ <BodyText inverted>Buttons and the close icon also invert automatically.</BodyText>
111
+ </Box>
112
+ </NavModal>
113
+ </View>
114
+ ),
115
+ };
116
+
117
+ export const FullScreenPresentation: Story = {
118
+ args: {
119
+ presentation: 'fullScreenModal',
120
+ },
121
+ render: (args: StoryObj<typeof meta.args>) => (
122
+ <View style={Platform.OS === 'web' ? { width: 400, height: 720 } : { flex: 1 }}>
123
+ <NavModal {...args}>
124
+ <Box gap="200">
125
+ <BodyText>This uses the full-screen navigation modal presentation.</BodyText>
126
+ <BodyText>The content switches away from the sheet-style card treatment.</BodyText>
127
+ </Box>
128
+ </NavModal>
129
+ </View>
130
+ ),
131
+ };