@retray-dev/ui-kit 10.1.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 (192) hide show
  1. package/COMPONENTS.md +419 -38
  2. package/README.md +14 -5
  3. package/dist/Accordion.js +1 -1
  4. package/dist/Accordion.mjs +3 -3
  5. package/dist/AlertBanner.js +1 -1
  6. package/dist/AlertBanner.mjs +3 -3
  7. package/dist/AppHeader.js +1 -1
  8. package/dist/AppHeader.mjs +4 -4
  9. package/dist/Avatar.mjs +2 -2
  10. package/dist/Badge.js +1 -1
  11. package/dist/Badge.mjs +3 -3
  12. package/dist/Button.js +1 -1
  13. package/dist/Button.mjs +3 -3
  14. package/dist/Card.mjs +2 -2
  15. package/dist/CategoryStrip.js +1 -1
  16. package/dist/CategoryStrip.mjs +3 -3
  17. package/dist/Checkbox.mjs +2 -2
  18. package/dist/Chip.js +1 -1
  19. package/dist/Chip.mjs +3 -3
  20. package/dist/ConfirmDialog.d.mts +1 -6
  21. package/dist/ConfirmDialog.d.ts +1 -6
  22. package/dist/ConfirmDialog.js +30 -24
  23. package/dist/ConfirmDialog.mjs +4 -4
  24. package/dist/CurrencyDisplay.mjs +2 -2
  25. package/dist/CurrencyInput.d.mts +3 -8
  26. package/dist/CurrencyInput.d.ts +3 -8
  27. package/dist/CurrencyInput.js +4 -2
  28. package/dist/CurrencyInput.mjs +4 -4
  29. package/dist/DetailRow.d.mts +1 -1
  30. package/dist/DetailRow.d.ts +1 -1
  31. package/dist/DetailRow.js +1 -1
  32. package/dist/DetailRow.mjs +3 -3
  33. package/dist/EmptyState.js +1 -1
  34. package/dist/EmptyState.mjs +4 -4
  35. package/dist/ErrorBoundary.js +1 -1
  36. package/dist/ErrorBoundary.mjs +3 -3
  37. package/dist/Form.mjs +2 -2
  38. package/dist/IconButton.js +1 -1
  39. package/dist/IconButton.mjs +3 -3
  40. package/dist/IconPicker.d.mts +17 -0
  41. package/dist/IconPicker.d.ts +17 -0
  42. package/dist/IconPicker.js +1424 -0
  43. package/dist/IconPicker.mjs +8 -0
  44. package/dist/ImageUpload.d.mts +3 -1
  45. package/dist/ImageUpload.d.ts +3 -1
  46. package/dist/ImageUpload.js +28 -10
  47. package/dist/ImageUpload.mjs +3 -3
  48. package/dist/ImageViewer.js +1 -1
  49. package/dist/ImageViewer.mjs +5 -5
  50. package/dist/Input.js +1 -1
  51. package/dist/Input.mjs +3 -3
  52. package/dist/LabelValue.js +1 -1
  53. package/dist/LabelValue.mjs +3 -3
  54. package/dist/ListGroup.mjs +2 -2
  55. package/dist/ListItem.d.mts +7 -7
  56. package/dist/ListItem.d.ts +7 -7
  57. package/dist/ListItem.js +13 -8
  58. package/dist/ListItem.mjs +3 -3
  59. package/dist/MediaCard.js +1 -1
  60. package/dist/MediaCard.mjs +3 -3
  61. package/dist/MenuGroup.mjs +2 -2
  62. package/dist/MenuItem.js +1 -1
  63. package/dist/MenuItem.mjs +3 -3
  64. package/dist/MonthPicker.mjs +2 -2
  65. package/dist/NumberStepper.d.mts +19 -0
  66. package/dist/NumberStepper.d.ts +19 -0
  67. package/dist/NumberStepper.js +410 -0
  68. package/dist/NumberStepper.mjs +9 -0
  69. package/dist/PagerDots.js +1 -1
  70. package/dist/PagerDots.mjs +3 -3
  71. package/dist/Pressable.d.mts +15 -7
  72. package/dist/Pressable.d.ts +15 -7
  73. package/dist/Pressable.js +7 -3
  74. package/dist/Pressable.mjs +1 -1
  75. package/dist/PricingCard.js +1 -1
  76. package/dist/PricingCard.mjs +5 -5
  77. package/dist/Progress.mjs +2 -2
  78. package/dist/RadioGroup.mjs +2 -2
  79. package/dist/RetrayProvider.mjs +3 -3
  80. package/dist/Select.mjs +2 -2
  81. package/dist/SelectableGrid.js +1 -1
  82. package/dist/SelectableGrid.mjs +3 -3
  83. package/dist/Separator.mjs +2 -2
  84. package/dist/Sheet.d.mts +4 -46
  85. package/dist/Sheet.d.ts +4 -46
  86. package/dist/Sheet.js +46 -114
  87. package/dist/Sheet.mjs +2 -3
  88. package/dist/SheetSelect.js +1 -1
  89. package/dist/SheetSelect.mjs +3 -3
  90. package/dist/Skeleton.mjs +2 -2
  91. package/dist/Slider.mjs +2 -2
  92. package/dist/Spinner.mjs +2 -2
  93. package/dist/Stats.d.mts +30 -0
  94. package/dist/Stats.d.ts +30 -0
  95. package/dist/Stats.js +429 -0
  96. package/dist/Stats.mjs +9 -0
  97. package/dist/Switch.mjs +2 -2
  98. package/dist/TabBar.js +1 -1
  99. package/dist/TabBar.mjs +3 -3
  100. package/dist/Tabs.mjs +2 -2
  101. package/dist/Text.d.mts +3 -1
  102. package/dist/Text.d.ts +3 -1
  103. package/dist/Text.js +3 -3
  104. package/dist/Text.mjs +2 -2
  105. package/dist/Textarea.js +1 -1
  106. package/dist/Textarea.mjs +3 -3
  107. package/dist/Toast.mjs +2 -2
  108. package/dist/Toggle.js +1 -1
  109. package/dist/Toggle.mjs +3 -3
  110. package/dist/{chunk-DJ7RN37L.mjs → chunk-265G6A46.mjs} +2 -2
  111. package/dist/{chunk-WOEYDUJZ.mjs → chunk-2A2LEFZG.mjs} +2 -2
  112. package/dist/{chunk-ID72TK46.mjs → chunk-2CBQKU7H.mjs} +1 -1
  113. package/dist/{chunk-OB4JUQ3O.mjs → chunk-2I2AYECM.mjs} +1 -1
  114. package/dist/{chunk-WJLKJMKR.mjs → chunk-357YO24D.mjs} +4 -4
  115. package/dist/{chunk-GQYFLP3D.mjs → chunk-3GEYJ7I5.mjs} +1 -1
  116. package/dist/{chunk-AV4EMIRH.mjs → chunk-3N2M3WZL.mjs} +1 -1
  117. package/dist/{chunk-TERDKCLE.mjs → chunk-3UYAZ7I4.mjs} +2 -2
  118. package/dist/{chunk-JMOZEC77.mjs → chunk-4WFMPFZB.mjs} +1 -1
  119. package/dist/chunk-5OLNXP3S.mjs +144 -0
  120. package/dist/{chunk-6OAZJ577.mjs → chunk-7HSILTC4.mjs} +3 -3
  121. package/dist/{chunk-IRRY3CRZ.mjs → chunk-AKM4EPOT.mjs} +1 -1
  122. package/dist/{chunk-VGTDN7SW.mjs → chunk-AQEVCEXV.mjs} +2 -2
  123. package/dist/{chunk-WBOOUHSS.mjs → chunk-BCWEHE34.mjs} +1 -1
  124. package/dist/{chunk-AJ7ZDNBT.mjs → chunk-BOVUP27T.mjs} +1 -1
  125. package/dist/{chunk-BRKYVJVV.mjs → chunk-BQZE3HAW.mjs} +1 -1
  126. package/dist/{chunk-MLF3EZFW.mjs → chunk-D3Y2T42P.mjs} +2 -2
  127. package/dist/{chunk-3U4SSNWP.mjs → chunk-DF6DU42P.mjs} +2 -2
  128. package/dist/{chunk-ZJKGQMYH.mjs → chunk-DI7CBDL6.mjs} +2 -2
  129. package/dist/{chunk-2TFTAWVJ.mjs → chunk-DOGIPOF5.mjs} +2 -2
  130. package/dist/{chunk-MBMXYJJV.mjs → chunk-E7NEHHXV.mjs} +7 -3
  131. package/dist/{chunk-MX6HRKMI.mjs → chunk-EFLFRAHD.mjs} +1 -1
  132. package/dist/{chunk-SOYNZDVY.mjs → chunk-EMUWGDWC.mjs} +6 -1
  133. package/dist/{chunk-4I7D47FH.mjs → chunk-F4V6XLP4.mjs} +4 -4
  134. package/dist/{chunk-UREA2GYY.mjs → chunk-FA2KMTH5.mjs} +2 -2
  135. package/dist/{chunk-Y2NS74WS.mjs → chunk-FFTYLPSB.mjs} +46 -98
  136. package/dist/{chunk-OHBNABL5.mjs → chunk-FUVYSVGR.mjs} +14 -9
  137. package/dist/{chunk-KIHCWCWL.mjs → chunk-FVTVCJAH.mjs} +2 -2
  138. package/dist/{chunk-Y4GL2MHX.mjs → chunk-GK4VRMNE.mjs} +30 -12
  139. package/dist/{chunk-6Q64UFIA.mjs → chunk-HJ46DTJE.mjs} +1 -1
  140. package/dist/{chunk-WF2XDFRK.mjs → chunk-HLMPMUK2.mjs} +1 -1
  141. package/dist/{chunk-GD6KXMG5.mjs → chunk-I4V5XZPS.mjs} +1 -1
  142. package/dist/{chunk-AZJF2BLK.mjs → chunk-ISY26JQJ.mjs} +2 -2
  143. package/dist/{chunk-X4G6APW6.mjs → chunk-J6Q2YJEV.mjs} +1 -1
  144. package/dist/{chunk-KZL5VTYK.mjs → chunk-JCZQOY4O.mjs} +31 -24
  145. package/dist/{chunk-CZCQZHG6.mjs → chunk-JNVAIDLK.mjs} +2 -2
  146. package/dist/{chunk-SOA2Z4RB.mjs → chunk-JULSIZDM.mjs} +1 -1
  147. package/dist/{chunk-T7XZ7H7Y.mjs → chunk-KA7LTET3.mjs} +17 -3
  148. package/dist/chunk-KHYX4IOM.mjs +1114 -0
  149. package/dist/{chunk-LXJIIOYQ.mjs → chunk-LRM4AVYY.mjs} +2 -2
  150. package/dist/{chunk-VQ57HWPL.mjs → chunk-MYZ2EDYU.mjs} +2 -2
  151. package/dist/chunk-N4ZPVCJH.mjs +126 -0
  152. package/dist/{chunk-NA7PARID.mjs → chunk-NXI4YDZ2.mjs} +2 -2
  153. package/dist/{chunk-4K625MVM.mjs → chunk-OULVKTWL.mjs} +2 -2
  154. package/dist/{chunk-A4MDAP7G.mjs → chunk-P64WHW4A.mjs} +2 -2
  155. package/dist/{chunk-URI2WBIV.mjs → chunk-P73V2EKS.mjs} +2 -2
  156. package/dist/{chunk-ZUR7AU5R.mjs → chunk-PGERH3P7.mjs} +2 -2
  157. package/dist/{chunk-2UYENBLV.mjs → chunk-QSFV2P7O.mjs} +1 -1
  158. package/dist/{chunk-JT7HKXRB.mjs → chunk-S3KJCPEJ.mjs} +1 -1
  159. package/dist/{chunk-6MKGPAR2.mjs → chunk-V6NFJXKO.mjs} +2 -2
  160. package/dist/{chunk-A3A6KNQN.mjs → chunk-WOEWGSTU.mjs} +1 -1
  161. package/dist/{chunk-JUXSWN54.mjs → chunk-X26S5EVZ.mjs} +4 -2
  162. package/dist/{chunk-YFZ3ELX5.mjs → chunk-XBAGGKLW.mjs} +2 -2
  163. package/dist/{chunk-JB67UOB5.mjs → chunk-ZHMSAYLT.mjs} +2 -2
  164. package/dist/fonts.d.mts +1 -7
  165. package/dist/fonts.d.ts +1 -7
  166. package/dist/fonts.js +0 -2
  167. package/dist/fonts.mjs +1 -2
  168. package/dist/index.d.mts +7 -1
  169. package/dist/index.d.ts +7 -1
  170. package/dist/index.js +1831 -475
  171. package/dist/index.mjs +54 -51
  172. package/package.json +3 -3
  173. package/src/components/ConfirmDialog/ConfirmDialog.tsx +39 -30
  174. package/src/components/CurrencyInput/CurrencyInput.tsx +4 -7
  175. package/src/components/DetailRow/DetailRow.tsx +1 -1
  176. package/src/components/IconPicker/IconPicker.tsx +395 -0
  177. package/src/components/IconPicker/index.ts +1 -0
  178. package/src/components/ImageUpload/ImageUpload.tsx +34 -12
  179. package/src/components/ListItem/ListItem.tsx +43 -28
  180. package/src/components/NumberStepper/NumberStepper.tsx +147 -0
  181. package/src/components/NumberStepper/index.ts +1 -0
  182. package/src/components/Pressable/Pressable.tsx +20 -8
  183. package/src/components/Sheet/Sheet.tsx +64 -172
  184. package/src/components/Stats/Stats.tsx +226 -0
  185. package/src/components/Stats/index.ts +2 -0
  186. package/src/components/Text/Text.tsx +4 -2
  187. package/src/fonts.ts +0 -7
  188. package/src/index.ts +7 -1
  189. package/src/theme/colorUtils.ts +9 -0
  190. package/src/utils/curatedIcons.ts +849 -0
  191. package/src/utils/fontGuard.ts +2 -1
  192. package/src/utils/icons.ts +20 -2
@@ -0,0 +1,147 @@
1
+ import React from 'react'
2
+ import { View, Text, StyleSheet, ViewStyle } from 'react-native'
3
+ import { impactLight } from '../../utils/haptics'
4
+ import { useTheme } from '../../theme'
5
+ import { s, ms, mvs } from '../../utils/scaling'
6
+ import { renderIcon } from '../../utils/icons'
7
+ import { RADIUS } from '../../tokens'
8
+ import { PressableButton } from '../../utils/pressable'
9
+
10
+ export type NumberStepperSize = 'sm' | 'md' | 'lg'
11
+
12
+ export interface NumberStepperProps {
13
+ value: number
14
+ onValueChange: (value: number) => void
15
+ min?: number
16
+ max?: number
17
+ step?: number
18
+ size?: NumberStepperSize
19
+ disabled?: boolean
20
+ style?: ViewStyle
21
+ accessibilityLabel?: string
22
+ }
23
+
24
+ const sizeConfig: Record<NumberStepperSize, { button: number; icon: number; valueFontSize: number; valueLineHeight: number; valueMinWidth: number }> = {
25
+ sm: { button: s(40), icon: 16, valueFontSize: ms(18), valueLineHeight: mvs(24), valueMinWidth: s(32) },
26
+ md: { button: s(44), icon: 18, valueFontSize: ms(22), valueLineHeight: mvs(28), valueMinWidth: s(36) },
27
+ lg: { button: s(52), icon: 22, valueFontSize: ms(26), valueLineHeight: mvs(32), valueMinWidth: s(40) },
28
+ }
29
+
30
+ function NumberStepperBase({
31
+ value,
32
+ onValueChange,
33
+ min = 1,
34
+ max = 99,
35
+ step = 1,
36
+ size = 'md',
37
+ disabled = false,
38
+ style,
39
+ accessibilityLabel,
40
+ }: NumberStepperProps) {
41
+ const { colors } = useTheme()
42
+
43
+ const canDecrement = value > min && !disabled
44
+ const canIncrement = value < max && !disabled
45
+
46
+ const handleDecrement = () => {
47
+ if (!canDecrement) return
48
+ impactLight()
49
+ onValueChange(Math.max(min, value - step))
50
+ }
51
+
52
+ const handleIncrement = () => {
53
+ if (!canIncrement) return
54
+ impactLight()
55
+ onValueChange(Math.min(max, value + step))
56
+ }
57
+
58
+ const { button: buttonSize, icon: iconSize, valueFontSize, valueLineHeight, valueMinWidth } = sizeConfig[size]
59
+
60
+ const displayValue = String(value)
61
+
62
+ return (
63
+ <View style={[styles.container, style]}>
64
+ <PressableButton
65
+ style={[
66
+ styles.button,
67
+ {
68
+ width: buttonSize,
69
+ height: buttonSize,
70
+ backgroundColor: colors.surface,
71
+ borderColor: colors.border,
72
+ },
73
+ !canDecrement && styles.buttonDisabled,
74
+ ]}
75
+ enabled={canDecrement}
76
+ onPress={handleDecrement}
77
+ rippleColor="transparent"
78
+ touchSoundDisabled
79
+ accessibilityRole="button"
80
+ accessibilityLabel={`Decrease, current value ${displayValue}`}
81
+ accessibilityState={{ disabled: !canDecrement }}
82
+ >
83
+ {renderIcon('minus', iconSize, canDecrement ? colors.foreground : colors.foregroundMuted)}
84
+ </PressableButton>
85
+ <Text
86
+ style={[
87
+ styles.value,
88
+ {
89
+ color: colors.foreground,
90
+ fontSize: valueFontSize,
91
+ lineHeight: valueLineHeight,
92
+ minWidth: valueMinWidth,
93
+ },
94
+ ]}
95
+ allowFontScaling={true}
96
+ accessibilityLabel={accessibilityLabel ?? `Quantity: ${displayValue}`}
97
+ accessibilityRole="text"
98
+ >
99
+ {displayValue}
100
+ </Text>
101
+ <PressableButton
102
+ style={[
103
+ styles.button,
104
+ {
105
+ width: buttonSize,
106
+ height: buttonSize,
107
+ backgroundColor: colors.surface,
108
+ borderColor: colors.border,
109
+ },
110
+ !canIncrement && styles.buttonDisabled,
111
+ ]}
112
+ enabled={canIncrement}
113
+ onPress={handleIncrement}
114
+ rippleColor="transparent"
115
+ touchSoundDisabled
116
+ accessibilityRole="button"
117
+ accessibilityLabel={`Increase, current value ${displayValue}`}
118
+ accessibilityState={{ disabled: !canIncrement }}
119
+ >
120
+ {renderIcon('plus', iconSize, canIncrement ? colors.foreground : colors.foregroundMuted)}
121
+ </PressableButton>
122
+ </View>
123
+ )
124
+ }
125
+
126
+ export const NumberStepper = React.memo(NumberStepperBase)
127
+
128
+ const styles = StyleSheet.create({
129
+ container: {
130
+ flexDirection: 'row',
131
+ alignItems: 'center',
132
+ gap: s(12),
133
+ },
134
+ button: {
135
+ borderRadius: RADIUS.md,
136
+ alignItems: 'center',
137
+ justifyContent: 'center',
138
+ borderWidth: 1.5,
139
+ },
140
+ buttonDisabled: {
141
+ opacity: 0.35,
142
+ },
143
+ value: {
144
+ fontFamily: 'Sohne-Medium',
145
+ textAlign: 'center',
146
+ },
147
+ })
@@ -0,0 +1 @@
1
+ export * from './NumberStepper'
@@ -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
-