@retray-dev/ui-kit 10.2.0 → 12.1.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 (153) hide show
  1. package/COMPONENTS.md +287 -37
  2. package/README.md +11 -2
  3. package/dist/Accordion.mjs +2 -2
  4. package/dist/AlertBanner.mjs +2 -2
  5. package/dist/AppHeader.mjs +3 -3
  6. package/dist/Avatar.mjs +2 -2
  7. package/dist/Badge.mjs +2 -2
  8. package/dist/Button.mjs +2 -2
  9. package/dist/Card.mjs +2 -2
  10. package/dist/CategoryStrip.mjs +2 -2
  11. package/dist/Checkbox.mjs +2 -2
  12. package/dist/Chip.mjs +2 -2
  13. package/dist/ConfirmDialog.d.mts +1 -6
  14. package/dist/ConfirmDialog.d.ts +1 -6
  15. package/dist/ConfirmDialog.js +29 -23
  16. package/dist/ConfirmDialog.mjs +3 -3
  17. package/dist/CurrencyDisplay.mjs +2 -2
  18. package/dist/CurrencyInput.d.mts +3 -8
  19. package/dist/CurrencyInput.d.ts +3 -8
  20. package/dist/CurrencyInput.js +3 -1
  21. package/dist/CurrencyInput.mjs +3 -3
  22. package/dist/DetailRow.mjs +2 -2
  23. package/dist/EmptyState.mjs +3 -3
  24. package/dist/ErrorBoundary.mjs +2 -2
  25. package/dist/Form.mjs +2 -2
  26. package/dist/IconButton.mjs +2 -2
  27. package/dist/IconPicker.js +675 -248
  28. package/dist/IconPicker.mjs +3 -2
  29. package/dist/ImageUpload.mjs +3 -3
  30. package/dist/ImageViewer.mjs +4 -4
  31. package/dist/Input.mjs +2 -2
  32. package/dist/LabelValue.mjs +2 -2
  33. package/dist/ListGroup.mjs +2 -2
  34. package/dist/ListItem.d.mts +7 -7
  35. package/dist/ListItem.d.ts +7 -7
  36. package/dist/ListItem.js +12 -7
  37. package/dist/ListItem.mjs +2 -2
  38. package/dist/MediaCard.mjs +2 -2
  39. package/dist/MenuGroup.mjs +2 -2
  40. package/dist/MenuItem.mjs +2 -2
  41. package/dist/MonthPicker.mjs +2 -2
  42. package/dist/NumberStepper.mjs +2 -2
  43. package/dist/PagerDots.mjs +2 -2
  44. package/dist/Pressable.d.mts +15 -7
  45. package/dist/Pressable.d.ts +15 -7
  46. package/dist/Pressable.js +7 -3
  47. package/dist/Pressable.mjs +1 -1
  48. package/dist/PricingCard.mjs +4 -4
  49. package/dist/Progress.mjs +2 -2
  50. package/dist/RadioGroup.mjs +2 -2
  51. package/dist/RetrayProvider.mjs +3 -3
  52. package/dist/Select.mjs +2 -2
  53. package/dist/SelectableGrid.mjs +2 -2
  54. package/dist/Separator.mjs +2 -2
  55. package/dist/Sheet.d.mts +4 -46
  56. package/dist/Sheet.d.ts +4 -46
  57. package/dist/Sheet.js +46 -114
  58. package/dist/Sheet.mjs +2 -3
  59. package/dist/SheetSelect.mjs +2 -2
  60. package/dist/Skeleton.mjs +2 -2
  61. package/dist/Slider.mjs +2 -2
  62. package/dist/Spinner.mjs +2 -2
  63. package/dist/Stats.d.mts +30 -0
  64. package/dist/Stats.d.ts +30 -0
  65. package/dist/Stats.js +429 -0
  66. package/dist/Stats.mjs +9 -0
  67. package/dist/Switch.mjs +2 -2
  68. package/dist/TabBar.mjs +2 -2
  69. package/dist/Tabs.mjs +2 -2
  70. package/dist/Text.d.mts +3 -1
  71. package/dist/Text.d.ts +3 -1
  72. package/dist/Text.js +3 -3
  73. package/dist/Text.mjs +2 -2
  74. package/dist/Textarea.mjs +2 -2
  75. package/dist/Toast.mjs +2 -2
  76. package/dist/Toggle.mjs +2 -2
  77. package/dist/{chunk-YJ7I257J.mjs → chunk-265G6A46.mjs} +1 -1
  78. package/dist/{chunk-ELXBDILQ.mjs → chunk-2A2LEFZG.mjs} +2 -2
  79. package/dist/{chunk-ID72TK46.mjs → chunk-2CBQKU7H.mjs} +1 -1
  80. package/dist/{chunk-OB4JUQ3O.mjs → chunk-2I2AYECM.mjs} +1 -1
  81. package/dist/{chunk-WJLKJMKR.mjs → chunk-357YO24D.mjs} +4 -4
  82. package/dist/{chunk-GQYFLP3D.mjs → chunk-3GEYJ7I5.mjs} +1 -1
  83. package/dist/{chunk-AV4EMIRH.mjs → chunk-3N2M3WZL.mjs} +1 -1
  84. package/dist/{chunk-VF2ATYN3.mjs → chunk-3UYAZ7I4.mjs} +1 -1
  85. package/dist/{chunk-JMOZEC77.mjs → chunk-4WFMPFZB.mjs} +1 -1
  86. package/dist/chunk-5OLNXP3S.mjs +144 -0
  87. package/dist/{chunk-6SECQ2ZF.mjs → chunk-7HSILTC4.mjs} +2 -2
  88. package/dist/{chunk-IRRY3CRZ.mjs → chunk-AKM4EPOT.mjs} +1 -1
  89. package/dist/{chunk-IX3NYLYQ.mjs → chunk-AQEVCEXV.mjs} +1 -1
  90. package/dist/{chunk-WBOOUHSS.mjs → chunk-BCWEHE34.mjs} +1 -1
  91. package/dist/{chunk-AJ7ZDNBT.mjs → chunk-BOVUP27T.mjs} +1 -1
  92. package/dist/{chunk-BRKYVJVV.mjs → chunk-BQZE3HAW.mjs} +1 -1
  93. package/dist/{chunk-Z6SFHN6T.mjs → chunk-D3Y2T42P.mjs} +1 -1
  94. package/dist/{chunk-T2KCAHOS.mjs → chunk-DF6DU42P.mjs} +1 -1
  95. package/dist/{chunk-TB6SD2FT.mjs → chunk-DI7CBDL6.mjs} +1 -1
  96. package/dist/{chunk-HTHGSXFG.mjs → chunk-DOGIPOF5.mjs} +1 -1
  97. package/dist/{chunk-MBMXYJJV.mjs → chunk-E7NEHHXV.mjs} +7 -3
  98. package/dist/{chunk-MX6HRKMI.mjs → chunk-EFLFRAHD.mjs} +1 -1
  99. package/dist/{chunk-SOYNZDVY.mjs → chunk-EMUWGDWC.mjs} +6 -1
  100. package/dist/{chunk-AJRVDP2H.mjs → chunk-F4V6XLP4.mjs} +3 -3
  101. package/dist/{chunk-DYT7BG5I.mjs → chunk-FA2KMTH5.mjs} +1 -1
  102. package/dist/{chunk-Y2NS74WS.mjs → chunk-FFTYLPSB.mjs} +46 -98
  103. package/dist/{chunk-VKID2D2I.mjs → chunk-FUVYSVGR.mjs} +13 -8
  104. package/dist/{chunk-7LWRKMF5.mjs → chunk-FVTVCJAH.mjs} +1 -1
  105. package/dist/{chunk-TZDGAP5N.mjs → chunk-GK4VRMNE.mjs} +2 -2
  106. package/dist/{chunk-6Q64UFIA.mjs → chunk-HJ46DTJE.mjs} +1 -1
  107. package/dist/{chunk-WF2XDFRK.mjs → chunk-HLMPMUK2.mjs} +1 -1
  108. package/dist/{chunk-GD6KXMG5.mjs → chunk-I4V5XZPS.mjs} +1 -1
  109. package/dist/{chunk-TBNZHU6C.mjs → chunk-ISY26JQJ.mjs} +2 -2
  110. package/dist/{chunk-X4G6APW6.mjs → chunk-J6Q2YJEV.mjs} +1 -1
  111. package/dist/{chunk-WYEUNUTP.mjs → chunk-JCZQOY4O.mjs} +31 -24
  112. package/dist/{chunk-U2XJFYED.mjs → chunk-JNVAIDLK.mjs} +1 -1
  113. package/dist/{chunk-SOA2Z4RB.mjs → chunk-JULSIZDM.mjs} +1 -1
  114. package/dist/chunk-KHYX4IOM.mjs +1114 -0
  115. package/dist/{chunk-RYZC432S.mjs → chunk-LRM4AVYY.mjs} +1 -1
  116. package/dist/{chunk-6L4G6PBT.mjs → chunk-MYZ2EDYU.mjs} +1 -1
  117. package/dist/{chunk-BUMAMSTZ.mjs → chunk-N4ZPVCJH.mjs} +1 -1
  118. package/dist/{chunk-Z4VHZ7B5.mjs → chunk-NXI4YDZ2.mjs} +1 -1
  119. package/dist/{chunk-ZZ2R6KZ3.mjs → chunk-OULVKTWL.mjs} +1 -1
  120. package/dist/{chunk-FCSSQK3L.mjs → chunk-P64WHW4A.mjs} +1 -1
  121. package/dist/{chunk-KOO4WITD.mjs → chunk-P73V2EKS.mjs} +1 -1
  122. package/dist/{chunk-SXLKNTA4.mjs → chunk-PGERH3P7.mjs} +1 -1
  123. package/dist/{chunk-2UYENBLV.mjs → chunk-QSFV2P7O.mjs} +1 -1
  124. package/dist/{chunk-JT7HKXRB.mjs → chunk-S3KJCPEJ.mjs} +1 -1
  125. package/dist/{chunk-BEMIQXXU.mjs → chunk-V6NFJXKO.mjs} +1 -1
  126. package/dist/{chunk-A3A6KNQN.mjs → chunk-WOEWGSTU.mjs} +1 -1
  127. package/dist/{chunk-NMU5FMQJ.mjs → chunk-X26S5EVZ.mjs} +4 -2
  128. package/dist/{chunk-YFZ3ELX5.mjs → chunk-XBAGGKLW.mjs} +2 -2
  129. package/dist/{chunk-S2R7UVOE.mjs → chunk-ZHMSAYLT.mjs} +1 -1
  130. package/dist/fonts.d.mts +1 -7
  131. package/dist/fonts.d.ts +1 -7
  132. package/dist/fonts.js +0 -2
  133. package/dist/fonts.mjs +1 -2
  134. package/dist/index.d.mts +4 -1
  135. package/dist/index.d.ts +4 -1
  136. package/dist/index.js +1184 -708
  137. package/dist/index.mjs +53 -52
  138. package/package.json +3 -3
  139. package/src/components/ConfirmDialog/ConfirmDialog.tsx +39 -30
  140. package/src/components/CurrencyInput/CurrencyInput.tsx +4 -7
  141. package/src/components/IconPicker/IconPicker.tsx +124 -112
  142. package/src/components/ListItem/ListItem.tsx +43 -28
  143. package/src/components/Pressable/Pressable.tsx +20 -8
  144. package/src/components/Sheet/Sheet.tsx +64 -172
  145. package/src/components/Stats/Stats.tsx +226 -0
  146. package/src/components/Stats/index.ts +2 -0
  147. package/src/components/Text/Text.tsx +4 -2
  148. package/src/fonts.ts +0 -7
  149. package/src/index.ts +4 -0
  150. package/src/theme/colorUtils.ts +9 -0
  151. package/src/utils/curatedIcons.ts +698 -135
  152. package/src/utils/fontGuard.ts +2 -1
  153. package/dist/chunk-53Z3NYGE.mjs +0 -742
@@ -31,13 +31,13 @@ export interface ListItemProps {
31
31
  leftRender?: React.ReactNode
32
32
  /**
33
33
  * Arbitrary content rendered on the right (badge, price, chevron, switch, etc.).
34
- * Replaces the old `trailing` prop (still accepted as an alias).
35
34
  */
36
35
  rightRender?: React.ReactNode | string
37
- /** @deprecated Use `rightRender` instead. */
38
- trailing?: React.ReactNode | string
39
- /** @deprecated Use `leftRender` instead. */
40
- icon?: React.ReactNode
36
+ /**
37
+ * Multiple action buttons rendered on the right with automatic gap.
38
+ * Takes precedence over `rightRender` and `showChevron`.
39
+ */
40
+ rightActions?: React.ReactNode[]
41
41
  /**
42
42
  * Icon name from `@expo/vector-icons` rendered in the left slot.
43
43
  * See https://icons.expo.fyi. Takes precedence over `leftRender`.
@@ -65,7 +65,7 @@ export interface ListItemProps {
65
65
  */
66
66
  variant?: ListItemVariant
67
67
 
68
- /** Show a right-pointing chevron on the far right. Ignored when `rightRender` / `trailing` is set. */
68
+ /** Show a right-pointing chevron on the far right. Ignored when `rightRender` / `rightActions` / `rightIcon` is set. */
69
69
  showChevron?: boolean
70
70
 
71
71
  /** Visual separator line at the bottom of the item. Useful when rendering multiple plain items in a list. */
@@ -91,8 +91,7 @@ function ListItemBase({
91
91
  imageSource,
92
92
  leftRender,
93
93
  rightRender,
94
- trailing,
95
- icon,
94
+ rightActions,
96
95
  leftIcon,
97
96
  rightIcon,
98
97
  leftIconColor,
@@ -119,16 +118,14 @@ function ListItemBase({
119
118
  onPress?.()
120
119
  }
121
120
 
122
- // imageSource takes precedence, then leftIcon, then leftRender/icon
121
+ // imageSource takes precedence, then leftIcon, then leftRender
123
122
  const effectiveLeft: React.ReactNode = imageSource
124
123
  ? <Image source={imageSource} style={styles.image} />
125
124
  : leftIcon
126
125
  ? renderIcon(leftIcon, 24, leftIconColor ?? colors.foreground)
127
- : leftRender ?? icon
126
+ : leftRender
128
127
 
129
- const effectiveRight: React.ReactNode | string | undefined = rightIcon
130
- ? renderIcon(rightIcon, 24, rightIconColor ?? colors.foregroundMuted)
131
- : rightRender ?? trailing
128
+ const hasRightContent = !!(rightIcon || (rightActions && rightActions.length > 0) || rightRender !== undefined || showChevron)
132
129
 
133
130
  const cardStyle: ViewStyle =
134
131
  variant === 'card'
@@ -181,21 +178,33 @@ function ListItemBase({
181
178
  ) : null}
182
179
  </View>
183
180
 
184
- {effectiveRight !== undefined ? (
185
- <View style={styles.rightContainer}>
186
- {typeof effectiveRight === 'string' ? (
187
- <Text
188
- style={[styles.rightText, { color: colors.foregroundMuted }]}
189
- allowFontScaling={true}
190
- >
191
- {effectiveRight}
192
- </Text>
193
- ) : (
194
- effectiveRight
195
- )}
196
- </View>
197
- ) : showChevron ? (
198
- <Entypo name="chevron-with-circle-right" size={20} color={colors.foregroundMuted} />
181
+ {hasRightContent ? (
182
+ rightIcon ? (
183
+ <View style={styles.rightContainer}>
184
+ {renderIcon(rightIcon, 24, rightIconColor ?? colors.foregroundMuted)}
185
+ </View>
186
+ ) : rightActions && rightActions.length > 0 ? (
187
+ <View style={styles.rightActionsContainer}>
188
+ {rightActions.map((action, i) => (
189
+ <React.Fragment key={i}>{action}</React.Fragment>
190
+ ))}
191
+ </View>
192
+ ) : rightRender !== undefined ? (
193
+ <View style={styles.rightContainer}>
194
+ {typeof rightRender === 'string' ? (
195
+ <Text
196
+ style={[styles.rightText, { color: colors.foregroundMuted }]}
197
+ allowFontScaling={true}
198
+ >
199
+ {rightRender}
200
+ </Text>
201
+ ) : (
202
+ rightRender
203
+ )}
204
+ </View>
205
+ ) : showChevron ? (
206
+ <Entypo name="chevron-with-circle-right" size={20} color={colors.foregroundMuted} />
207
+ ) : null
199
208
  ) : null}
200
209
  </>
201
210
  )
@@ -283,6 +292,12 @@ const styles = StyleSheet.create({
283
292
  flexShrink: 0,
284
293
  maxWidth: s(160),
285
294
  },
295
+ rightActionsContainer: {
296
+ flexDirection: 'row',
297
+ alignItems: 'center',
298
+ gap: s(8),
299
+ flexShrink: 0,
300
+ },
286
301
  rightText: {
287
302
  fontFamily: 'Sohne-Regular',
288
303
  fontSize: ms(14),
@@ -1,5 +1,5 @@
1
1
  import React from 'react'
2
- import { ViewStyle } from 'react-native'
2
+ import { ViewStyle, type AccessibilityRole } from 'react-native'
3
3
  import { impactLight } from '../../utils/haptics'
4
4
  import { PressableCard } from '../../utils/pressable'
5
5
  import { PRESS_SCALE } from '../../utils/animations'
@@ -11,11 +11,6 @@ export interface PressableProps {
11
11
  onPress?: () => void
12
12
  /** Scale value on press. Defaults to `0.98` (MediaCard-style). */
13
13
  pressScale?: number
14
- /**
15
- * @deprecated Use Reanimated spring config via `pressOutSpring` instead. Ignored.
16
- * Kept for backwards compatibility with v6.x consumers.
17
- */
18
- bounciness?: number
19
14
  /** Enable haptic feedback on press. Defaults to `true`. */
20
15
  haptics?: boolean
21
16
  /** Additional style for the wrapper. */
@@ -24,6 +19,19 @@ export interface PressableProps {
24
19
  disabled?: boolean
25
20
  /** Hover scale (web only). Defaults to `1.02`. Set to `1` to disable. */
26
21
  hoverScale?: number
22
+ /**
23
+ * Accessibility role for the pressable element.
24
+ * Defaults to `"button"`.
25
+ */
26
+ accessibilityRole?: AccessibilityRole
27
+ /**
28
+ * Accessibility state for screen readers.
29
+ * Used to communicate states like `selected`, `expanded`, `checked`, etc.
30
+ * Defaults to `{ disabled: !!disabled }`.
31
+ */
32
+ accessibilityState?: Record<string, unknown>
33
+ /** Accessibility label for screen readers. */
34
+ accessibilityLabel?: string
27
35
  }
28
36
 
29
37
  /**
@@ -41,6 +49,9 @@ export function Pressable({
41
49
  style,
42
50
  disabled,
43
51
  hoverScale: _hoverScale = 1.02,
52
+ accessibilityRole,
53
+ accessibilityState,
54
+ accessibilityLabel,
44
55
  }: PressableProps) {
45
56
  const handlePress = () => {
46
57
  if (disabled || !onPress) return
@@ -56,8 +67,9 @@ export function Pressable({
56
67
  rippleColor="transparent"
57
68
  touchSoundDisabled
58
69
  activateOnHover
59
- accessibilityRole="button"
60
- accessibilityState={{ disabled: !!disabled }}
70
+ accessibilityRole={accessibilityRole ?? 'button'}
71
+ accessibilityState={accessibilityState ?? { disabled: !!disabled }}
72
+ accessibilityLabel={accessibilityLabel}
61
73
  >
62
74
  {children}
63
75
  </PressableCard>
@@ -1,6 +1,7 @@
1
- import React, { useCallback, useEffect, useRef } from 'react'
2
- import { View, Text, TouchableOpacity, StyleSheet, ViewStyle, Dimensions, Platform, Modal, ScrollView, useWindowDimensions, Pressable } from 'react-native'
3
- import BottomSheet, {
1
+ import React, { useCallback, useEffect, useRef, useId } from 'react'
2
+ import { View, Text, TouchableOpacity, StyleSheet, ViewStyle } from 'react-native'
3
+ import {
4
+ BottomSheetModal,
4
5
  BottomSheetView,
5
6
  BottomSheetScrollView,
6
7
  BottomSheetBackdrop,
@@ -15,14 +16,8 @@ import { AntDesign } from '@expo/vector-icons'
15
16
  import { impactMedium } from '../../utils/haptics'
16
17
  import { useTheme } from '../../theme'
17
18
  import { s, vs, ms, mvs } from '../../utils/scaling'
18
- import { BREAKPOINTS, RADIUS, SHADOWS } from '../../tokens'
19
-
20
- const SCREEN_HEIGHT = Dimensions.get('window').height
21
- const DEFAULT_MAX_HEIGHT = SCREEN_HEIGHT * 0.85
22
- const isAndroid = Platform.OS === 'android'
23
19
 
24
20
  export { BottomSheetModalProvider }
25
- // Re-export BottomSheetTextInput as SheetTextInput for consumer convenience
26
21
  export { BottomSheetTextInput as SheetTextInput }
27
22
 
28
23
  export interface SheetHeaderProps {
@@ -44,67 +39,25 @@ export interface SheetProps {
44
39
  open: boolean
45
40
  onClose: () => void
46
41
  title?: string
47
- /** Secondary text below title. */
48
42
  subtitle?: string
49
- /** @deprecated Use `subtitle` instead. */
50
- description?: string
51
- /** Show an X close button in the header. */
52
43
  showCloseButton?: boolean
53
44
  children?: React.ReactNode
54
- /** Style for the inner content container. */
55
45
  style?: ViewStyle
56
- /** Style for the content wrapper (outside the scroll container). */
57
46
  contentStyle?: ViewStyle
58
- /** Render children inside BottomSheetScrollView. */
47
+ /** Render children inside BottomSheetScrollView instead of BottomSheetView. */
59
48
  scrollable?: boolean
60
- /** Cap sheet height (dp). Children scroll when content exceeds this value. */
49
+ /** Cap sheet height (dp). Content scrolls when exceeding this value. Requires `scrollable`. */
61
50
  maxHeight?: number
62
- /**
63
- * Keyboard behavior — how the sheet responds to keyboard appearance.
64
- * - 'interactive': offset sheet by keyboard size (default, works on both platforms)
65
- * - 'fillParent': extend sheet to fill parent view (can cause restore issues with dynamic sizing)
66
- * - 'extend': extend sheet to maximum snap point
67
- *
68
- * Default: 'interactive' on both platforms.
69
- */
70
51
  keyboardBehavior?: 'extend' | 'fillParent' | 'interactive'
71
- /**
72
- * Keyboard blur behavior — what happens when keyboard dismisses.
73
- * - 'none': do nothing
74
- * - 'restore': restore sheet to previous position (default)
75
- */
76
52
  keyboardBlurBehavior?: 'none' | 'restore'
77
- /**
78
- * Blur keyboard when user starts dragging the sheet down.
79
- * Default: true (recommended for better UX)
80
- */
81
53
  enableBlurKeyboardOnGesture?: boolean
82
- /**
83
- * Android-only: defines keyboard input mode.
84
- * - 'adjustPan': pan the sheet content (default, fixes restore issues with dynamic sizing)
85
- * - 'adjustResize': resize the sheet container (can cause transparent gap on dismiss)
86
- */
87
54
  android_keyboardInputMode?: 'adjustPan' | 'adjustResize'
88
- /** Sticky footer rendered below the scroll area. */
89
55
  footer?: React.ReactNode
90
56
  /**
91
- * Array of snap points for the sheet (e.g., ['50%', '85%'] or [200, 500]).
57
+ * Array of snap points (e.g., ['50%', '85%'] or [200, 500]).
92
58
  * When provided, disables enableDynamicSizing.
93
- * When omitted, sheet uses dynamic sizing (auto-fits content).
94
59
  */
95
60
  snapPoints?: (string | number)[]
96
- /**
97
- * When true, render as a centered modal dialog on wide screens (width ≥
98
- * `BREAKPOINTS.wide`) instead of a bottom sheet. On narrow screens it stays a
99
- * bottom sheet. Use for store/category/picker dialogs that should feel native
100
- * on tablets and web.
101
- *
102
- * Note: the centered-dialog path uses a plain RN `Modal`, so `SheetTextInput`
103
- * is not required there — use a regular `TextInput`.
104
- */
105
- responsive?: boolean
106
- /** Max width of the centered dialog (dp). Only applies when `responsive`. Defaults to 480. */
107
- dialogMaxWidth?: number
108
61
  }
109
62
 
110
63
  export function SheetHeader({ children, style }: SheetHeaderProps) {
@@ -129,76 +82,73 @@ export function Sheet({
129
82
  onClose,
130
83
  title,
131
84
  subtitle,
132
- description,
133
85
  showCloseButton = false,
134
86
  children,
135
87
  style,
136
88
  contentStyle,
137
- scrollable,
89
+ scrollable = false,
138
90
  maxHeight,
139
- keyboardBehavior,
91
+ keyboardBehavior = 'interactive',
140
92
  keyboardBlurBehavior = 'restore',
141
93
  enableBlurKeyboardOnGesture = true,
142
94
  android_keyboardInputMode = 'adjustPan',
143
95
  footer,
144
96
  snapPoints,
145
- responsive = false,
146
- dialogMaxWidth = 480,
147
97
  }: SheetProps) {
148
98
  const { colors } = useTheme()
149
99
  const insets = useSafeAreaInsets()
150
- const { width: windowWidth } = useWindowDimensions()
151
- const ref = useRef<BottomSheet>(null)
152
- const asDialog = responsive && windowWidth >= BREAKPOINTS.wide
153
-
154
- // 'interactive' + 'adjustPan' works properly with enableDynamicSizing on both platforms
155
- // 'fillParent' + 'adjustResize' causes restore issues (transparent gap when keyboard dismisses)
156
- const effectiveKeyboardBehavior = keyboardBehavior ?? 'interactive'
100
+ const ref = useRef<BottomSheetModal>(null)
101
+ const wasOpened = useRef(false)
102
+ const name = useId()
157
103
 
158
104
  useEffect(() => {
159
105
  if (open) {
160
106
  impactMedium()
161
- ref.current?.snapToIndex(0)
162
- } else {
163
- ref.current?.close()
107
+ ref.current?.present()
108
+ wasOpened.current = true
109
+ } else if (wasOpened.current) {
110
+ ref.current?.dismiss()
164
111
  }
165
112
  }, [open])
166
113
 
167
- const renderBackdrop = useCallback((props: BottomSheetBackdropProps) => (
168
- <BottomSheetBackdrop
169
- {...props}
170
- disappearsOnIndex={-1}
171
- appearsOnIndex={0}
172
- pressBehavior="close"
173
- />
174
- ), [])
114
+ const renderBackdrop = useCallback(
115
+ (props: BottomSheetBackdropProps) => (
116
+ <BottomSheetBackdrop
117
+ {...props}
118
+ disappearsOnIndex={-1}
119
+ appearsOnIndex={0}
120
+ pressBehavior="close"
121
+ />
122
+ ),
123
+ []
124
+ )
175
125
 
176
- // Detect compound components in children
177
126
  const childArray = React.Children.toArray(children)
178
127
  const customHeader = childArray.find((child) => React.isValidElement(child) && child.type === SheetHeader)
179
128
  const customContent = childArray.find((child) => React.isValidElement(child) && child.type === SheetContent)
180
129
  const customFooter = childArray.find((child) => React.isValidElement(child) && child.type === SheetFooter)
181
-
182
- // If using compound components, filter them out from main children
183
- const filteredChildren = customHeader || customContent || customFooter
184
- ? childArray.filter(
185
- (child) =>
186
- !React.isValidElement(child) ||
187
- (child.type !== SheetHeader && child.type !== SheetContent && child.type !== SheetFooter)
188
- )
189
- : children
190
130
 
191
- const effectiveSubtitle = subtitle ?? description
192
- const showHeader = !!(title || effectiveSubtitle || showCloseButton) && !customHeader
131
+ const filteredChildren =
132
+ customHeader || customContent || customFooter
133
+ ? childArray.filter(
134
+ (child) =>
135
+ !React.isValidElement(child) ||
136
+ (child.type !== SheetHeader && child.type !== SheetContent && child.type !== SheetFooter)
137
+ )
138
+ : children
193
139
 
194
- const headerNode = customHeader ? customHeader : (showHeader ? (
140
+ const showHeader = !!(title || subtitle || showCloseButton) && !customHeader
141
+
142
+ const headerNode = customHeader ? customHeader : showHeader ? (
195
143
  <View style={[styles.header, { backgroundColor: colors.card }]} accessibilityRole="header">
196
144
  <View style={styles.headerRow}>
197
145
  {title ? (
198
146
  <Text style={[styles.title, { color: colors.foreground }]} allowFontScaling={true}>
199
147
  {title}
200
148
  </Text>
201
- ) : <View style={{ flex: 1 }} />}
149
+ ) : (
150
+ <View style={{ flex: 1 }} />
151
+ )}
202
152
  {showCloseButton ? (
203
153
  <TouchableOpacity
204
154
  onPress={onClose}
@@ -213,90 +163,52 @@ export function Sheet({
213
163
  </TouchableOpacity>
214
164
  ) : null}
215
165
  </View>
216
- {effectiveSubtitle ? (
166
+ {subtitle ? (
217
167
  <Text style={[styles.subtitle, { color: colors.foregroundMuted }]} allowFontScaling={true}>
218
- {effectiveSubtitle}
168
+ {subtitle}
219
169
  </Text>
220
170
  ) : null}
221
171
  </View>
222
- ) : null)
172
+ ) : null
223
173
 
224
174
  const contentNode = customContent ? customContent : filteredChildren
225
175
  const effectiveFooter = customFooter ? customFooter : footer
226
176
 
227
- const renderFooter = useCallback((props: BottomSheetFooterProps) => {
228
- if (!effectiveFooter) return null
229
- return (
230
- <BottomSheetFooter {...props}>
231
- {effectiveFooter}
232
- </BottomSheetFooter>
233
- )
234
- }, [effectiveFooter])
235
-
236
- // Centered dialog path for wide screens — plain RN Modal, same header/content/footer.
237
- if (asDialog) {
238
- return (
239
- <Modal visible={open} transparent animationType="fade" onRequestClose={onClose}>
240
- <Pressable style={styles.dialogBackdrop} onPress={onClose} accessibilityRole="button" accessibilityLabel="Close">
241
- {/* Inner Pressable swallows presses so taps inside the card don't close it. */}
242
- <Pressable
243
- style={[
244
- styles.dialogCard,
245
- { backgroundColor: colors.card, maxWidth: dialogMaxWidth, maxHeight: SCREEN_HEIGHT * 0.85 },
246
- ]}
247
- onPress={() => {}}
248
- >
249
- {headerNode}
250
- <ScrollView
251
- contentContainerStyle={[styles.dialogContent, style]}
252
- style={contentStyle}
253
- showsVerticalScrollIndicator={true}
254
- bounces={false}
255
- >
256
- {contentNode}
257
- </ScrollView>
258
- {effectiveFooter}
259
- </Pressable>
260
- </Pressable>
261
- </Modal>
262
- )
263
- }
177
+ const renderFooter = useCallback(
178
+ (props: BottomSheetFooterProps) => {
179
+ if (!effectiveFooter) return null
180
+ return <BottomSheetFooter {...props}>{effectiveFooter}</BottomSheetFooter>
181
+ },
182
+ [effectiveFooter]
183
+ )
264
184
 
265
- const useScroll = scrollable || !!maxHeight
266
- const effectiveMaxHeight = maxHeight ?? DEFAULT_MAX_HEIGHT
267
-
268
- // If snapPoints provided, disable dynamic sizing. Otherwise use dynamic sizing.
269
185
  const useDynamicSizing = !snapPoints
270
-
186
+
271
187
  return (
272
- <BottomSheet
188
+ <BottomSheetModal
273
189
  ref={ref}
274
- index={-1}
275
- onClose={onClose}
190
+ name={name}
191
+ onDismiss={onClose}
276
192
  enableDynamicSizing={useDynamicSizing}
277
193
  snapPoints={snapPoints}
278
- maxDynamicContentSize={useDynamicSizing ? effectiveMaxHeight : undefined}
194
+ maxDynamicContentSize={useDynamicSizing && maxHeight ? maxHeight : undefined}
279
195
  backdropComponent={renderBackdrop}
280
196
  footerComponent={effectiveFooter ? renderFooter : undefined}
281
- backgroundStyle={[styles.background, { backgroundColor: colors.card }]}
282
- handleIndicatorStyle={[styles.handle, { backgroundColor: colors.border }]}
197
+ backgroundStyle={{ ...styles.background, backgroundColor: colors.card }}
198
+ handleIndicatorStyle={{ ...styles.handle, backgroundColor: colors.border }}
283
199
  enablePanDownToClose
284
200
  topInset={insets.top}
285
- keyboardBehavior={effectiveKeyboardBehavior}
201
+ keyboardBehavior={keyboardBehavior}
286
202
  keyboardBlurBehavior={keyboardBlurBehavior}
287
203
  android_keyboardInputMode={android_keyboardInputMode}
288
204
  enableBlurKeyboardOnGesture={enableBlurKeyboardOnGesture}
289
205
  >
290
- {useScroll ? (
206
+ {scrollable ? (
291
207
  <BottomSheetScrollView
292
- contentContainerStyle={[
293
- styles.scrollContent,
294
- style,
295
- ]}
208
+ contentContainerStyle={[styles.scrollContent, style]}
296
209
  style={contentStyle}
297
- showsVerticalScrollIndicator={true}
298
- indicatorStyle="black"
299
- persistentScrollbar={isAndroid}
210
+ showsVerticalScrollIndicator
211
+ bounces={false}
300
212
  stickyHeaderIndices={headerNode ? [0] : undefined}
301
213
  >
302
214
  {headerNode}
@@ -308,7 +220,7 @@ export function Sheet({
308
220
  {contentNode}
309
221
  </BottomSheetView>
310
222
  )}
311
- </BottomSheet>
223
+ </BottomSheetModal>
312
224
  )
313
225
  }
314
226
 
@@ -358,7 +270,6 @@ const styles = StyleSheet.create({
358
270
  scrollContent: {
359
271
  paddingHorizontal: s(16),
360
272
  paddingBottom: vs(32),
361
- paddingRight: s(16),
362
273
  },
363
274
  sheetContent: {
364
275
  gap: vs(16),
@@ -370,23 +281,4 @@ const styles = StyleSheet.create({
370
281
  flexDirection: 'row',
371
282
  gap: s(12),
372
283
  },
373
- dialogBackdrop: {
374
- flex: 1,
375
- backgroundColor: 'rgba(0,0,0,0.5)',
376
- alignItems: 'center',
377
- justifyContent: 'center',
378
- padding: s(24),
379
- },
380
- dialogCard: {
381
- width: '100%',
382
- borderRadius: RADIUS.lg,
383
- paddingTop: vs(16),
384
- overflow: 'hidden',
385
- ...SHADOWS.xl,
386
- },
387
- dialogContent: {
388
- paddingHorizontal: s(16),
389
- paddingBottom: vs(16),
390
- },
391
284
  })
392
-