@praxiis/ui 0.0.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 (187) hide show
  1. package/dist/index.d.mts +52556 -0
  2. package/dist/index.d.ts +52556 -0
  3. package/dist/index.js +8753 -0
  4. package/dist/index.mjs +8777 -0
  5. package/package.json +70 -0
  6. package/src/__test-utils__/index.tsx +39 -0
  7. package/src/components/CalendarStrip/CalendarStrip.helpers.ts +106 -0
  8. package/src/components/CalendarStrip/CalendarStrip.tsx +83 -0
  9. package/src/components/CalendarStrip/CalendarStrip.types.ts +133 -0
  10. package/src/components/CalendarStrip/DayCard/DayCard.helpers.ts +44 -0
  11. package/src/components/CalendarStrip/DayCard/DayCard.tsx +71 -0
  12. package/src/components/CalendarStrip/DayCard/DayCard.types.ts +134 -0
  13. package/src/components/CalendarStrip/DayCard/index.ts +2 -0
  14. package/src/components/CalendarStrip/DayCard/useDayCardLogic.ts +45 -0
  15. package/src/components/CalendarStrip/index.ts +9 -0
  16. package/src/components/CalendarStrip/useCalendarStripLogic.ts +53 -0
  17. package/src/components/EmptyState/EmptyState.helpers.ts +104 -0
  18. package/src/components/EmptyState/EmptyState.tsx +205 -0
  19. package/src/components/EmptyState/EmptyState.types.ts +213 -0
  20. package/src/components/EmptyState/index.ts +44 -0
  21. package/src/components/EmptyState/useEmptyStateLogic.ts +131 -0
  22. package/src/components/Header/Header.helpers.ts +93 -0
  23. package/src/components/Header/Header.tsx +185 -0
  24. package/src/components/Header/Header.types.ts +153 -0
  25. package/src/components/Header/index.ts +44 -0
  26. package/src/components/Header/useHeaderLogic.ts +146 -0
  27. package/src/components/ScheduleItem/ScheduleItem/ScheduleItem.helpers.ts +50 -0
  28. package/src/components/ScheduleItem/ScheduleItem/ScheduleItem.tsx +78 -0
  29. package/src/components/ScheduleItem/ScheduleItem/ScheduleItem.types.ts +99 -0
  30. package/src/components/ScheduleItem/ScheduleItem/index.ts +16 -0
  31. package/src/components/ScheduleItem/ScheduleItem/useScheduleItemLogic.ts +31 -0
  32. package/src/components/ScheduleItem/index.ts +15 -0
  33. package/src/components/index.ts +40 -0
  34. package/src/core/index.ts +34 -0
  35. package/src/core/restyle/RestyleThemeProviderWrapper.tsx +31 -0
  36. package/src/core/restyle/index.ts +38 -0
  37. package/src/core/restyle/restylePresetRegistry.ts +195 -0
  38. package/src/core/restyle/restyleTheme.ts +1352 -0
  39. package/src/core/restyle/restyleTypes.ts +8 -0
  40. package/src/core/restyle/useRestyleTheme.ts +10 -0
  41. package/src/hooks/animations/index.ts +3 -0
  42. package/src/hooks/animations/useAnimatedValue.ts +10 -0
  43. package/src/hooks/animations/useEntranceAnimation.ts +106 -0
  44. package/src/hooks/animations/usePulseAnimation.ts +63 -0
  45. package/src/hooks/index.ts +30 -0
  46. package/src/hooks/useReducedMotion.ts +60 -0
  47. package/src/i18n/index.ts +2 -0
  48. package/src/i18n/labels/en.ts +120 -0
  49. package/src/i18n/labels/es.ts +120 -0
  50. package/src/i18n/labels/index.ts +6 -0
  51. package/src/i18n/labels/types.ts +165 -0
  52. package/src/index.tsx +215 -0
  53. package/src/primitives/actions/Button/Button.helpers.ts +243 -0
  54. package/src/primitives/actions/Button/Button.tsx +198 -0
  55. package/src/primitives/actions/Button/Button.types.ts +207 -0
  56. package/src/primitives/actions/Button/index.ts +41 -0
  57. package/src/primitives/actions/Button/useButtonLogic.ts +160 -0
  58. package/src/primitives/actions/IconButton/IconButton.helpers.ts +235 -0
  59. package/src/primitives/actions/IconButton/IconButton.tsx +177 -0
  60. package/src/primitives/actions/IconButton/IconButton.types.ts +273 -0
  61. package/src/primitives/actions/IconButton/index.ts +30 -0
  62. package/src/primitives/actions/IconButton/useIconButtonLogic.ts +172 -0
  63. package/src/primitives/actions/index.ts +20 -0
  64. package/src/primitives/content/Avatar/Avatar.helpers.ts +177 -0
  65. package/src/primitives/content/Avatar/Avatar.tsx +199 -0
  66. package/src/primitives/content/Avatar/Avatar.types.ts +222 -0
  67. package/src/primitives/content/Avatar/index.ts +46 -0
  68. package/src/primitives/content/Avatar/useAvatarLogic.ts +149 -0
  69. package/src/primitives/content/Badge/Badge.helpers.ts +175 -0
  70. package/src/primitives/content/Badge/Badge.tsx +174 -0
  71. package/src/primitives/content/Badge/Badge.types.ts +223 -0
  72. package/src/primitives/content/Badge/index.ts +40 -0
  73. package/src/primitives/content/Badge/useBadgeLogic.ts +128 -0
  74. package/src/primitives/content/Card/Card.helpers.ts +27 -0
  75. package/src/primitives/content/Card/Card.tsx +123 -0
  76. package/src/primitives/content/Card/Card.types.ts +95 -0
  77. package/src/primitives/content/Card/index.ts +20 -0
  78. package/src/primitives/content/Card/useCardLogic.ts +48 -0
  79. package/src/primitives/content/Chip/Chip.helpers.ts +304 -0
  80. package/src/primitives/content/Chip/Chip.tsx +205 -0
  81. package/src/primitives/content/Chip/Chip.types.ts +234 -0
  82. package/src/primitives/content/Chip/index.ts +47 -0
  83. package/src/primitives/content/Chip/useChipLogic.ts +167 -0
  84. package/src/primitives/content/Icon/Icon.helpers.ts +54 -0
  85. package/src/primitives/content/Icon/Icon.tsx +110 -0
  86. package/src/primitives/content/Icon/Icon.types.ts +95 -0
  87. package/src/primitives/content/Icon/index.ts +20 -0
  88. package/src/primitives/content/Icon/useIconLogic.ts +73 -0
  89. package/src/primitives/content/index.ts +45 -0
  90. package/src/primitives/feedback/ProgressBar/ProgressBar.helpers.ts +122 -0
  91. package/src/primitives/feedback/ProgressBar/ProgressBar.tsx +154 -0
  92. package/src/primitives/feedback/ProgressBar/ProgressBar.types.ts +178 -0
  93. package/src/primitives/feedback/ProgressBar/index.ts +17 -0
  94. package/src/primitives/feedback/ProgressBar/useProgressBarLogic.ts +120 -0
  95. package/src/primitives/feedback/Skeleton/Skeleton.helpers.ts +145 -0
  96. package/src/primitives/feedback/Skeleton/Skeleton.tsx +155 -0
  97. package/src/primitives/feedback/Skeleton/Skeleton.types.ts +223 -0
  98. package/src/primitives/feedback/Skeleton/index.ts +44 -0
  99. package/src/primitives/feedback/Skeleton/useSkeletonLogic.ts +125 -0
  100. package/src/primitives/feedback/Spinner/Spinner.helpers.ts +40 -0
  101. package/src/primitives/feedback/Spinner/Spinner.tsx +105 -0
  102. package/src/primitives/feedback/Spinner/Spinner.types.ts +114 -0
  103. package/src/primitives/feedback/Spinner/index.ts +18 -0
  104. package/src/primitives/feedback/Spinner/useSpinnerLogic.ts +84 -0
  105. package/src/primitives/feedback/Toast/Toast.helpers.ts +163 -0
  106. package/src/primitives/feedback/Toast/Toast.tsx +190 -0
  107. package/src/primitives/feedback/Toast/Toast.types.ts +270 -0
  108. package/src/primitives/feedback/Toast/ToastContext.tsx +96 -0
  109. package/src/primitives/feedback/Toast/ToastProvider.tsx +241 -0
  110. package/src/primitives/feedback/Toast/index.ts +59 -0
  111. package/src/primitives/feedback/Toast/useToastLogic.ts +112 -0
  112. package/src/primitives/feedback/index.ts +45 -0
  113. package/src/primitives/index.ts +158 -0
  114. package/src/primitives/inputs/Checkbox/Checkbox.helpers.ts +132 -0
  115. package/src/primitives/inputs/Checkbox/Checkbox.tsx +150 -0
  116. package/src/primitives/inputs/Checkbox/Checkbox.types.ts +106 -0
  117. package/src/primitives/inputs/Checkbox/index.ts +30 -0
  118. package/src/primitives/inputs/Checkbox/useCheckboxLogic.ts +121 -0
  119. package/src/primitives/inputs/RadioButton/RadioButton.helpers.ts +123 -0
  120. package/src/primitives/inputs/RadioButton/RadioButton.tsx +159 -0
  121. package/src/primitives/inputs/RadioButton/RadioButton.types.ts +106 -0
  122. package/src/primitives/inputs/RadioButton/index.ts +25 -0
  123. package/src/primitives/inputs/RadioButton/useRadioButtonLogic.ts +117 -0
  124. package/src/primitives/inputs/SegmentedControl/SegmentedControl.helpers.ts +174 -0
  125. package/src/primitives/inputs/SegmentedControl/SegmentedControl.tsx +224 -0
  126. package/src/primitives/inputs/SegmentedControl/SegmentedControl.types.ts +187 -0
  127. package/src/primitives/inputs/SegmentedControl/index.ts +39 -0
  128. package/src/primitives/inputs/SegmentedControl/useSegmentedControlLogic.ts +151 -0
  129. package/src/primitives/inputs/SelectSheet/SelectSheet.helpers.ts +147 -0
  130. package/src/primitives/inputs/SelectSheet/SelectSheet.tsx +247 -0
  131. package/src/primitives/inputs/SelectSheet/SelectSheet.types.ts +196 -0
  132. package/src/primitives/inputs/SelectSheet/SelectSheetOption.tsx +177 -0
  133. package/src/primitives/inputs/SelectSheet/index.ts +48 -0
  134. package/src/primitives/inputs/SelectSheet/useSelectSheetLogic.ts +309 -0
  135. package/src/primitives/inputs/Switch/Switch.helpers.ts +109 -0
  136. package/src/primitives/inputs/Switch/Switch.tsx +191 -0
  137. package/src/primitives/inputs/Switch/Switch.types.ts +154 -0
  138. package/src/primitives/inputs/Switch/index.ts +40 -0
  139. package/src/primitives/inputs/Switch/useSwitchLogic.ts +192 -0
  140. package/src/primitives/inputs/TextInput/TextInput.helpers.ts +206 -0
  141. package/src/primitives/inputs/TextInput/TextInput.tsx +392 -0
  142. package/src/primitives/inputs/TextInput/TextInput.types.ts +216 -0
  143. package/src/primitives/inputs/TextInput/index.ts +37 -0
  144. package/src/primitives/inputs/TextInput/useTextInputLogic.ts +195 -0
  145. package/src/primitives/inputs/index.ts +52 -0
  146. package/src/primitives/layout/AnimatedBox.tsx +44 -0
  147. package/src/primitives/layout/Box.tsx +71 -0
  148. package/src/primitives/layout/Divider/Divider.helpers.ts +115 -0
  149. package/src/primitives/layout/Divider/Divider.tsx +139 -0
  150. package/src/primitives/layout/Divider/Divider.types.ts +178 -0
  151. package/src/primitives/layout/Divider/index.ts +24 -0
  152. package/src/primitives/layout/Divider/useDividerLogic.ts +109 -0
  153. package/src/primitives/layout/FlatList.tsx +66 -0
  154. package/src/primitives/layout/Pressable.tsx +74 -0
  155. package/src/primitives/layout/ScrollView.tsx +63 -0
  156. package/src/primitives/layout/Stack.tsx +69 -0
  157. package/src/primitives/layout/index.ts +40 -0
  158. package/src/primitives/navigation/index.ts +6 -0
  159. package/src/primitives/overlays/Modal/Modal.helpers.ts +31 -0
  160. package/src/primitives/overlays/Modal/Modal.tsx +264 -0
  161. package/src/primitives/overlays/Modal/Modal.types.ts +193 -0
  162. package/src/primitives/overlays/Modal/index.ts +43 -0
  163. package/src/primitives/overlays/Modal/useModalLogic.ts +103 -0
  164. package/src/primitives/overlays/index.ts +12 -0
  165. package/src/primitives/typography/Text.tsx +51 -0
  166. package/src/primitives/typography/index.ts +1 -0
  167. package/src/provider/DesignSystemContext.ts +22 -0
  168. package/src/provider/DesignSystemProvider.tsx +121 -0
  169. package/src/provider/index.ts +7 -0
  170. package/src/providers/ThemeProvider/createTheme.ts +304 -0
  171. package/src/providers/ThemeProvider/defaultTheme.ts +70 -0
  172. package/src/providers/ThemeProvider/index.ts +34 -0
  173. package/src/providers/ThemeProvider/types.ts +249 -0
  174. package/src/providers/index.ts +29 -0
  175. package/src/tokens/colors.ts +371 -0
  176. package/src/tokens/index.ts +145 -0
  177. package/src/tokens/motion.ts +176 -0
  178. package/src/tokens/radii.ts +82 -0
  179. package/src/tokens/scales.ts +588 -0
  180. package/src/tokens/shadows.ts +190 -0
  181. package/src/tokens/spacing.ts +140 -0
  182. package/src/tokens/tokens.json +207 -0
  183. package/src/tokens/typography.ts +251 -0
  184. package/src/types.ts +50 -0
  185. package/src/utils/accessibility.ts +169 -0
  186. package/src/utils/index.ts +25 -0
  187. package/src/utils/platform.ts +72 -0
@@ -0,0 +1,63 @@
1
+ import React, { forwardRef } from "react";
2
+ import {
3
+ ScrollView as RNScrollView,
4
+ ScrollViewProps as RNScrollViewProps,
5
+ } from "react-native";
6
+ import {
7
+ backgroundColor,
8
+ BackgroundColorProps,
9
+ composeRestyleFunctions,
10
+ layout,
11
+ LayoutProps,
12
+ spacing,
13
+ SpacingProps,
14
+ useRestyle,
15
+ } from "@shopify/restyle";
16
+ import { RestyleTheme } from "../../core/restyle";
17
+
18
+ /**
19
+ * Restyle props that can be applied to ScrollView
20
+ */
21
+ export type ScrollViewRestyleProps = SpacingProps<RestyleTheme> &
22
+ LayoutProps<RestyleTheme> &
23
+ BackgroundColorProps<RestyleTheme>;
24
+
25
+ export type ScrollViewProps = ScrollViewRestyleProps & RNScrollViewProps;
26
+
27
+ /** Ref type for the ScrollView component — the underlying React Native ScrollView instance. */
28
+ export type ScrollViewRef = RNScrollView;
29
+
30
+ const restyleFunctions = composeRestyleFunctions<
31
+ RestyleTheme,
32
+ ScrollViewRestyleProps
33
+ >([spacing, layout, backgroundColor]);
34
+
35
+ /**
36
+ * ScrollView — Restyled ScrollView component
37
+ *
38
+ * A ScrollView wrapper that accepts Restyle props (padding, margin, flex, backgroundColor)
39
+ * while preserving all native ScrollView functionality and supporting ref forwarding.
40
+ *
41
+ * @example
42
+ * ```tsx
43
+ * <ScrollView
44
+ * ref={scrollRef}
45
+ * horizontal
46
+ * paddingHorizontal="md"
47
+ * showsHorizontalScrollIndicator={false}
48
+ * contentContainerStyle={{ gap: 8 }}
49
+ * >
50
+ * {items.map((item) => (
51
+ * <Chip key={item.id} label={item.name} />
52
+ * ))}
53
+ * </ScrollView>
54
+ * ```
55
+ */
56
+ export const ScrollView = forwardRef<RNScrollView, ScrollViewProps>(
57
+ function ScrollView(props, ref) {
58
+ const { style, ...rest } = useRestyle(restyleFunctions, props);
59
+ return <RNScrollView ref={ref} style={style} {...rest} />;
60
+ },
61
+ );
62
+
63
+ ScrollView.displayName = "ScrollView";
@@ -0,0 +1,69 @@
1
+ /**
2
+ * HStack / VStack Components
3
+ *
4
+ * @description Convenience layout primitives for flex-based stacking - Atom
5
+ *
6
+ * Thin wrappers around Box that set sensible flexDirection defaults,
7
+ * eliminating the most common Box boilerplate in consumer code.
8
+ *
9
+ * ## Components
10
+ * | Component | Direction | Use Case |
11
+ * |-----------|-----------|----------|
12
+ * | HStack | row | Toolbars, inline groups |
13
+ * | VStack | column | Vertical lists, form fields |
14
+ *
15
+ * All Box/Restyle props are forwarded — spacing, colors, borders, etc.
16
+ * For edge cases like `row-reverse`, use `Box` directly.
17
+ *
18
+ * @example
19
+ * // Vertical stack with gap
20
+ * <VStack gap="md" padding="lg">
21
+ * <Text>First</Text>
22
+ * <Text>Second</Text>
23
+ * </VStack>
24
+ *
25
+ * @example
26
+ * // Horizontal stack centered
27
+ * <HStack gap="sm" alignItems="center">
28
+ * <Icon name="check" />
29
+ * <Text>Confirmed</Text>
30
+ * </HStack>
31
+ */
32
+
33
+ import React, { memo } from "react";
34
+ import { Box, BoxRestyleProps } from "./Box";
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Types
38
+ // ---------------------------------------------------------------------------
39
+
40
+ export type HStackProps = BoxRestyleProps;
41
+ export type VStackProps = BoxRestyleProps;
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Components
45
+ // ---------------------------------------------------------------------------
46
+
47
+ /**
48
+ * Horizontal stack (row direction). All Box props forwarded.
49
+ */
50
+ function HStackComponent({ style, ...rest }: HStackProps) {
51
+ return <Box flexDirection="row" style={style} {...rest} />;
52
+ }
53
+
54
+ /**
55
+ * Vertical stack (column direction). All Box props forwarded.
56
+ */
57
+ function VStackComponent({ style, ...rest }: VStackProps) {
58
+ return <Box flexDirection="column" style={style} {...rest} />;
59
+ }
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Exports
63
+ // ---------------------------------------------------------------------------
64
+
65
+ export const HStack = memo(HStackComponent);
66
+ HStack.displayName = "HStack";
67
+
68
+ export const VStack = memo(VStackComponent);
69
+ VStack.displayName = "VStack";
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Layout Components Module
3
+ *
4
+ * Components that structure and organize space.
5
+ *
6
+ * Note: Box and AnimatedBox are core primitives — import from "@/design-system/core".
7
+ */
8
+
9
+ export * from "./AnimatedBox";
10
+ export * from "./Box";
11
+ export * from "./Stack";
12
+
13
+ export {
14
+ Divider,
15
+ type DividerColor,
16
+ type DividerOrientation,
17
+ type DividerProps,
18
+ type DividerSpacing,
19
+ type DividerThickness,
20
+ } from "./Divider";
21
+
22
+ export {
23
+ FlatList,
24
+ type FlatListProps,
25
+ type FlatListRef,
26
+ type FlatListRestyleProps,
27
+ } from "./FlatList";
28
+
29
+ export {
30
+ Pressable,
31
+ type PressableProps,
32
+ type PressableRestyleProps,
33
+ } from "./Pressable";
34
+
35
+ export {
36
+ ScrollView,
37
+ type ScrollViewProps,
38
+ type ScrollViewRef,
39
+ type ScrollViewRestyleProps,
40
+ } from "./ScrollView";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Navigation Components Module
3
+ *
4
+ * Components that orient the user within the app.
5
+ */
6
+
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Modal Helper Functions
3
+ *
4
+ * Pure utility functions for Modal component.
5
+ * All values derived from tokens/scales.ts for consistency.
6
+ *
7
+ * @see tokens/scales.ts - MODAL_LAYOUT
8
+ */
9
+
10
+ import type { ModalPosition } from "./Modal.types";
11
+
12
+ // =============================================================================
13
+ // ANIMATION HELPERS
14
+ // =============================================================================
15
+
16
+ /**
17
+ * Get the animation type based on modal position.
18
+ *
19
+ * - center: fade (standard dialog)
20
+ * - top: slide (slides down from top)
21
+ * - bottom: slide (slides up from bottom)
22
+ *
23
+ * @param position - Modal position
24
+ * @returns RN Modal animationType
25
+ */
26
+ export function getAnimationType(
27
+ position: ModalPosition
28
+ ): "fade" | "slide" | "none" {
29
+ if (position === "center") return "fade";
30
+ return "slide";
31
+ }
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Modal Component
3
+ *
4
+ * @description Dialog overlay for focused user interactions - Molecule
5
+ *
6
+ * Modal presents content in a focused overlay that requires user attention.
7
+ * Supports multiple positions (center, top, bottom) with appropriate
8
+ * animations and safe area handling.
9
+ *
10
+ * ## Width Behavior
11
+ * | Viewport | Width |
12
+ * |----------|------------------------------------------|
13
+ * | Phone | Full width minus lg (16px) horizontal |
14
+ * | Tablet | Capped at 480px max-width |
15
+ *
16
+ * ## Positions
17
+ * - `center`: Standard dialog centered on screen (default)
18
+ * - `top`: Anchored to top with slide-down animation
19
+ * - `bottom`: Anchored to bottom with slide-up animation
20
+ *
21
+ * ## Features
22
+ * - Three position variants with matching animations
23
+ * - Optional title and close button header
24
+ * - Primary and secondary action buttons in footer
25
+ * - Backdrop press to dismiss
26
+ * - Safe area aware (status bar + home indicator)
27
+ * - Full accessibility support (dialog role)
28
+ * - Memoized for performance
29
+ *
30
+ * @see Modal.types.ts - Type definitions
31
+ * @see Modal.helpers.ts - Animation type resolution
32
+ * @see Modal.a11y.ts - Accessibility prop generation
33
+ * @see tokens/scales.ts - MODAL_LAYOUT
34
+ *
35
+ * @example
36
+ * // Basic usage
37
+ * <Modal visible={isOpen} onClose={handleClose} title="Confirm">
38
+ * <Text>Are you sure?</Text>
39
+ * </Modal>
40
+ *
41
+ * @example
42
+ * // With actions
43
+ * <Modal
44
+ * visible={isOpen}
45
+ * onClose={handleClose}
46
+ * title="Delete Item"
47
+ * position="center"
48
+ * primaryAction={{ label: "Delete", onPress: handleDelete }}
49
+ * secondaryAction={{ label: "Cancel", onPress: handleClose }}
50
+ * accessibilityLabel="Delete confirmation dialog"
51
+ * />
52
+ *
53
+ * @example
54
+ * // Bottom sheet style
55
+ * <Modal
56
+ * visible={isOpen}
57
+ * onClose={handleClose}
58
+ * title="Options"
59
+ * position="bottom"
60
+ * >
61
+ * <Button title="Share" onPress={handleShare} />
62
+ * <Button title="Edit" onPress={handleEdit} />
63
+ * </Modal>
64
+ */
65
+
66
+ import React, { memo } from "react";
67
+ import { Pressable, Modal as RNModal, StyleSheet } from "react-native";
68
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
69
+ import { MODAL_LAYOUT } from "../../../tokens/scales";
70
+ import { Button } from "../../actions/Button";
71
+ import { IconButton } from "../../actions/IconButton";
72
+ import { Box } from "../../layout";
73
+ import { Text } from "../../typography";
74
+ import type { ModalProps } from "./Modal.types";
75
+ import { useModalLogic } from "./useModalLogic";
76
+
77
+ // =============================================================================
78
+ // COMPONENT IMPLEMENTATION
79
+ // =============================================================================
80
+
81
+ function ModalComponent({
82
+ visible,
83
+ onClose,
84
+ children,
85
+ title,
86
+ position = "center",
87
+ closeOnBackdropPress = true,
88
+ showCloseButton = true,
89
+ primaryAction,
90
+ secondaryAction,
91
+ testID,
92
+ accessibilityLabel,
93
+ }: ModalProps) {
94
+ const insets = useSafeAreaInsets();
95
+
96
+ const { handleBackdropPress, animationType, a11yProps, closeButtonA11yProps } = useModalLogic({
97
+ visible,
98
+ onClose,
99
+ title,
100
+ position,
101
+ closeOnBackdropPress,
102
+ showCloseButton,
103
+ accessibilityLabel,
104
+ });
105
+
106
+ // Safe area aware container padding — modal floats inside safe areas
107
+ const containerPadding = {
108
+ paddingTop:
109
+ position === "top"
110
+ ? insets.top + MODAL_LAYOUT.horizontalMargin
111
+ : position === "center"
112
+ ? MODAL_LAYOUT.horizontalMargin
113
+ : MODAL_LAYOUT.horizontalMargin,
114
+ paddingBottom:
115
+ position === "bottom"
116
+ ? insets.bottom + MODAL_LAYOUT.horizontalMargin
117
+ : position === "center"
118
+ ? MODAL_LAYOUT.horizontalMargin
119
+ : MODAL_LAYOUT.horizontalMargin,
120
+ paddingHorizontal: MODAL_LAYOUT.horizontalMargin,
121
+ };
122
+
123
+ const containerStyle =
124
+ position === "bottom"
125
+ ? styles.bottomContainer
126
+ : position === "top"
127
+ ? styles.topContainer
128
+ : styles.centerContainer;
129
+
130
+ const hasFooter = !!primaryAction || !!secondaryAction;
131
+ const hasHeader = !!title || showCloseButton;
132
+
133
+ return (
134
+ <RNModal
135
+ visible={visible}
136
+ transparent
137
+ animationType={animationType}
138
+ onRequestClose={onClose}
139
+ statusBarTranslucent
140
+ >
141
+ <Pressable
142
+ style={[styles.overlay, containerStyle, containerPadding]}
143
+ onPress={handleBackdropPress}
144
+ >
145
+ <Pressable style={styles.sheet} onPress={(e) => e.stopPropagation()}>
146
+ <Box
147
+ testID={testID}
148
+ backgroundColor="surfaceElevated"
149
+ borderRadius="xl"
150
+ overflow="hidden"
151
+ {...a11yProps}
152
+ >
153
+ {/* Header */}
154
+ {hasHeader && (
155
+ <Box
156
+ flexDirection="row"
157
+ alignItems="center"
158
+ justifyContent="space-between"
159
+ paddingHorizontal="lg"
160
+ paddingVertical="md"
161
+ borderBottomWidth={1}
162
+ borderBottomColor="borderSubtle"
163
+ >
164
+ {title ? (
165
+ <Box flex={1} marginRight={showCloseButton ? "sm" : "none"}>
166
+ <Text variant="headingSmall">{title}</Text>
167
+ </Box>
168
+ ) : (
169
+ <Box flex={1} />
170
+ )}
171
+ {showCloseButton && (
172
+ <IconButton
173
+ iconName="close"
174
+ variant="ghost"
175
+ size="sm"
176
+ onPress={onClose}
177
+ accessibilityLabel={closeButtonA11yProps.accessibilityLabel}
178
+ accessibilityHint={closeButtonA11yProps.accessibilityHint}
179
+ />
180
+ )}
181
+ </Box>
182
+ )}
183
+
184
+ {/* Body (children) */}
185
+ <Box paddingHorizontal="lg" paddingVertical="md">
186
+ {children}
187
+ </Box>
188
+
189
+ {/* Footer */}
190
+ {hasFooter && (
191
+ <Box
192
+ paddingHorizontal="lg"
193
+ paddingVertical="md"
194
+ borderTopWidth={1}
195
+ borderTopColor="borderSubtle"
196
+ flexDirection="row"
197
+ justifyContent="flex-end"
198
+ gap="sm"
199
+ >
200
+ {secondaryAction && (
201
+ <Button
202
+ title={secondaryAction.label}
203
+ variant="outline"
204
+ size="md"
205
+ onPress={secondaryAction.onPress}
206
+ isLoading={secondaryAction.loading}
207
+ disabled={secondaryAction.disabled}
208
+ testID={testID ? `${testID}-secondary-action` : undefined}
209
+ />
210
+ )}
211
+ {primaryAction && (
212
+ <Button
213
+ title={primaryAction.label}
214
+ variant="solid"
215
+ size="md"
216
+ onPress={primaryAction.onPress}
217
+ isLoading={primaryAction.loading}
218
+ disabled={primaryAction.disabled}
219
+ testID={testID ? `${testID}-primary-action` : undefined}
220
+ />
221
+ )}
222
+ </Box>
223
+ )}
224
+ </Box>
225
+ </Pressable>
226
+ </Pressable>
227
+ </RNModal>
228
+ );
229
+ }
230
+
231
+ // =============================================================================
232
+ // STYLES (POSITIONING ONLY - COLORS FROM THEME)
233
+ // =============================================================================
234
+
235
+ const styles = StyleSheet.create({
236
+ overlay: {
237
+ flex: 1,
238
+ backgroundColor: `rgba(0, 0, 0, ${MODAL_LAYOUT.overlayOpacity})`,
239
+ },
240
+ centerContainer: {
241
+ justifyContent: "center",
242
+ alignItems: "center",
243
+ },
244
+ topContainer: {
245
+ justifyContent: "flex-start",
246
+ alignItems: "center",
247
+ },
248
+ bottomContainer: {
249
+ justifyContent: "flex-end",
250
+ alignItems: "center",
251
+ },
252
+ sheet: {
253
+ width: "100%",
254
+ maxWidth: MODAL_LAYOUT.tabletMaxWidth,
255
+ alignItems: "stretch",
256
+ },
257
+ });
258
+
259
+ // =============================================================================
260
+ // EXPORTS
261
+ // =============================================================================
262
+
263
+ export const Modal = memo(ModalComponent);
264
+ Modal.displayName = "Modal";
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Modal Component Types
3
+ *
4
+ * Type definitions for the Modal dialog overlay component.
5
+ *
6
+ * Width behavior:
7
+ * - Phone: Full width minus lg (16px) horizontal padding on each side
8
+ * - Tablet (≥768px): Capped at 480px max-width (phone-like experience)
9
+ *
10
+ * @see tokens/scales.ts - MODAL_LAYOUT
11
+ */
12
+
13
+ import type { ReactNode } from "react";
14
+ import type { AccessibilityProps } from "react-native";
15
+
16
+ // =============================================================================
17
+ // VARIANT TYPES (Explicitly exported for external use)
18
+ // =============================================================================
19
+
20
+ /**
21
+ * Modal position controlling vertical placement.
22
+ *
23
+ * - `center`: Centered vertically (default, standard dialog)
24
+ * - `top`: Anchored to top of screen (notifications, alerts)
25
+ * - `bottom`: Anchored to bottom of screen (action sheets)
26
+ */
27
+ export type ModalPosition = "center" | "top" | "bottom";
28
+
29
+ // =============================================================================
30
+ // ACTION TYPES
31
+ // =============================================================================
32
+
33
+ /**
34
+ * Action button configuration for modal footer.
35
+ */
36
+ export interface ModalAction {
37
+ /** Button label text */
38
+ label: string;
39
+ /** Press handler */
40
+ onPress: () => void;
41
+ /** Whether the action is in a loading state */
42
+ loading?: boolean;
43
+ /** Whether the action is disabled */
44
+ disabled?: boolean;
45
+ }
46
+
47
+ // =============================================================================
48
+ // COMPONENT PROPS
49
+ // =============================================================================
50
+
51
+ /**
52
+ * Modal component props.
53
+ *
54
+ * @example
55
+ * ```tsx
56
+ * <Modal
57
+ * visible={isVisible}
58
+ * onClose={handleClose}
59
+ * title="Confirm Action"
60
+ * position="center"
61
+ * >
62
+ * <Text>Are you sure you want to proceed?</Text>
63
+ * </Modal>
64
+ * ```
65
+ */
66
+ export interface ModalProps {
67
+ // -------------------------------------------------------------------------
68
+ // Required Props
69
+ // -------------------------------------------------------------------------
70
+
71
+ /** Whether the modal is visible */
72
+ visible: boolean;
73
+ /** Callback when modal should close */
74
+ onClose: () => void;
75
+ /** Modal content */
76
+ children: ReactNode;
77
+
78
+ // -------------------------------------------------------------------------
79
+ // Content Props
80
+ // -------------------------------------------------------------------------
81
+
82
+ /** Modal title displayed in the header */
83
+ title?: string;
84
+
85
+ // -------------------------------------------------------------------------
86
+ // Position Props
87
+ // -------------------------------------------------------------------------
88
+
89
+ /**
90
+ * Vertical position of the modal
91
+ * @default "center"
92
+ */
93
+ position?: ModalPosition;
94
+
95
+ // -------------------------------------------------------------------------
96
+ // Behavior Props
97
+ // -------------------------------------------------------------------------
98
+
99
+ /**
100
+ * Whether tapping the backdrop closes the modal
101
+ * @default true
102
+ */
103
+ closeOnBackdropPress?: boolean;
104
+
105
+ /**
106
+ * Whether to show the close button in the header
107
+ * @default true
108
+ */
109
+ showCloseButton?: boolean;
110
+
111
+ // -------------------------------------------------------------------------
112
+ // Action Props
113
+ // -------------------------------------------------------------------------
114
+
115
+ /** Primary action button (e.g., "Confirm", "Save") */
116
+ primaryAction?: ModalAction;
117
+ /** Secondary action button (e.g., "Cancel") */
118
+ secondaryAction?: ModalAction;
119
+
120
+ // -------------------------------------------------------------------------
121
+ // Accessibility Props
122
+ // -------------------------------------------------------------------------
123
+
124
+ /** Test ID for testing */
125
+ testID?: string;
126
+ /** Accessibility label override */
127
+ accessibilityLabel?: string;
128
+ }
129
+
130
+ // =============================================================================
131
+ // INTERNAL TYPES
132
+ // =============================================================================
133
+
134
+ /**
135
+ * Parameters for useModalLogic hook.
136
+ */
137
+ export interface UseModalLogicParams {
138
+ visible: boolean;
139
+ onClose: () => void;
140
+ title?: string;
141
+ position: ModalPosition;
142
+ closeOnBackdropPress: boolean;
143
+ showCloseButton: boolean;
144
+ accessibilityLabel?: string;
145
+ }
146
+
147
+ /**
148
+ * Return value from useModalLogic hook.
149
+ */
150
+ export interface UseModalLogicReturn {
151
+ /** Handler for backdrop press */
152
+ handleBackdropPress: () => void;
153
+ /** Animation type for the RN Modal */
154
+ animationType: "fade" | "slide" | "none";
155
+ /** Accessibility props for the modal container */
156
+ a11yProps: AccessibilityProps;
157
+ /** Accessibility props for the close button */
158
+ closeButtonA11yProps: {
159
+ accessible: true;
160
+ accessibilityRole: "button";
161
+ accessibilityLabel: string;
162
+ accessibilityHint: string;
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Parameters for getModalA11y function.
168
+ */
169
+ export interface ModalA11yParams {
170
+ title?: string;
171
+ position: ModalPosition;
172
+ accessibilityLabel?: string;
173
+ fallbackTitle: string;
174
+ positionLabels: { bottom: string; top: string; center: string };
175
+ }
176
+
177
+ /**
178
+ * Parameters for getModalCloseButtonA11yProps function.
179
+ */
180
+ export interface ModalCloseButtonA11yParams {
181
+ closeLabel: string;
182
+ closeHint: string;
183
+ }
184
+
185
+ /**
186
+ * Accessibility props returned by getModalA11y.
187
+ */
188
+ export interface ModalA11yProps {
189
+ accessible: true;
190
+ accessibilityRole: "none";
191
+ accessibilityLabel: string;
192
+ accessibilityViewIsModal: true;
193
+ }