@retray-dev/ui-kit 10.0.0 → 10.2.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 (128) hide show
  1. package/COMPONENTS.md +150 -17
  2. package/CONSUMER.md +1 -1
  3. package/README.md +4 -4
  4. package/dist/Accordion.d.mts +1 -1
  5. package/dist/Accordion.d.ts +1 -1
  6. package/dist/Accordion.js +3 -3
  7. package/dist/Accordion.mjs +2 -2
  8. package/dist/AlertBanner.js +1 -1
  9. package/dist/AlertBanner.mjs +2 -2
  10. package/dist/AppHeader.js +1 -1
  11. package/dist/AppHeader.mjs +3 -3
  12. package/dist/Badge.js +1 -1
  13. package/dist/Badge.mjs +2 -2
  14. package/dist/Button.js +1 -1
  15. package/dist/Button.mjs +2 -2
  16. package/dist/CategoryStrip.js +1 -1
  17. package/dist/CategoryStrip.mjs +2 -2
  18. package/dist/Chip.js +1 -1
  19. package/dist/Chip.mjs +2 -2
  20. package/dist/ConfirmDialog.d.mts +6 -1
  21. package/dist/ConfirmDialog.d.ts +6 -1
  22. package/dist/ConfirmDialog.js +45 -15
  23. package/dist/ConfirmDialog.mjs +3 -3
  24. package/dist/CurrencyInput.js +1 -1
  25. package/dist/CurrencyInput.mjs +3 -3
  26. package/dist/DetailRow.d.mts +1 -1
  27. package/dist/DetailRow.d.ts +1 -1
  28. package/dist/DetailRow.js +1 -1
  29. package/dist/DetailRow.mjs +2 -2
  30. package/dist/EmptyState.js +1 -1
  31. package/dist/EmptyState.mjs +3 -3
  32. package/dist/ErrorBoundary.js +1 -1
  33. package/dist/ErrorBoundary.mjs +2 -2
  34. package/dist/IconButton.js +1 -1
  35. package/dist/IconButton.mjs +2 -2
  36. package/dist/IconPicker.d.mts +17 -0
  37. package/dist/IconPicker.d.ts +17 -0
  38. package/dist/IconPicker.js +997 -0
  39. package/dist/IconPicker.mjs +7 -0
  40. package/dist/ImageUpload.d.mts +3 -1
  41. package/dist/ImageUpload.d.ts +3 -1
  42. package/dist/ImageUpload.js +28 -10
  43. package/dist/ImageUpload.mjs +1 -1
  44. package/dist/ImageViewer.js +282 -141
  45. package/dist/ImageViewer.mjs +5 -3
  46. package/dist/Input.js +1 -1
  47. package/dist/Input.mjs +2 -2
  48. package/dist/LabelValue.js +1 -1
  49. package/dist/LabelValue.mjs +2 -2
  50. package/dist/ListItem.js +1 -1
  51. package/dist/ListItem.mjs +2 -2
  52. package/dist/MediaCard.js +1 -1
  53. package/dist/MediaCard.mjs +2 -2
  54. package/dist/MenuItem.js +1 -1
  55. package/dist/MenuItem.mjs +2 -2
  56. package/dist/NumberStepper.d.mts +19 -0
  57. package/dist/NumberStepper.d.ts +19 -0
  58. package/dist/NumberStepper.js +410 -0
  59. package/dist/NumberStepper.mjs +9 -0
  60. package/dist/PagerDots.js +1 -1
  61. package/dist/PagerDots.mjs +2 -2
  62. package/dist/PricingCard.js +1 -1
  63. package/dist/PricingCard.mjs +4 -4
  64. package/dist/SelectableGrid.js +1 -1
  65. package/dist/SelectableGrid.mjs +2 -2
  66. package/dist/Sheet.js +16 -13
  67. package/dist/Sheet.mjs +1 -1
  68. package/dist/SheetSelect.js +1 -1
  69. package/dist/SheetSelect.mjs +2 -2
  70. package/dist/Switch.js +40 -17
  71. package/dist/Switch.mjs +1 -1
  72. package/dist/TabBar.js +1 -1
  73. package/dist/TabBar.mjs +2 -2
  74. package/dist/Textarea.js +1 -1
  75. package/dist/Textarea.mjs +2 -2
  76. package/dist/Toggle.js +1 -1
  77. package/dist/Toggle.mjs +2 -2
  78. package/dist/chunk-53Z3NYGE.mjs +742 -0
  79. package/dist/{chunk-VQ57HWPL.mjs → chunk-6L4G6PBT.mjs} +1 -1
  80. package/dist/{chunk-6OAZJ577.mjs → chunk-6SECQ2ZF.mjs} +2 -2
  81. package/dist/{chunk-KIHCWCWL.mjs → chunk-7LWRKMF5.mjs} +1 -1
  82. package/dist/{chunk-4I7D47FH.mjs → chunk-AJRVDP2H.mjs} +3 -3
  83. package/dist/{chunk-6MKGPAR2.mjs → chunk-BEMIQXXU.mjs} +1 -1
  84. package/dist/chunk-BUMAMSTZ.mjs +126 -0
  85. package/dist/{chunk-UREA2GYY.mjs → chunk-DYT7BG5I.mjs} +1 -1
  86. package/dist/{chunk-Z4BVUWW6.mjs → chunk-ELXBDILQ.mjs} +20 -32
  87. package/dist/{chunk-A4MDAP7G.mjs → chunk-FCSSQK3L.mjs} +1 -1
  88. package/dist/{chunk-2TFTAWVJ.mjs → chunk-HTHGSXFG.mjs} +1 -1
  89. package/dist/{chunk-VGTDN7SW.mjs → chunk-IX3NYLYQ.mjs} +1 -1
  90. package/dist/{chunk-T7XZ7H7Y.mjs → chunk-KA7LTET3.mjs} +17 -3
  91. package/dist/{chunk-URI2WBIV.mjs → chunk-KOO4WITD.mjs} +1 -1
  92. package/dist/{chunk-JUXSWN54.mjs → chunk-NMU5FMQJ.mjs} +1 -1
  93. package/dist/{chunk-LXJIIOYQ.mjs → chunk-RYZC432S.mjs} +1 -1
  94. package/dist/{chunk-JB67UOB5.mjs → chunk-S2R7UVOE.mjs} +1 -1
  95. package/dist/{chunk-ZUR7AU5R.mjs → chunk-SXLKNTA4.mjs} +1 -1
  96. package/dist/{chunk-3U4SSNWP.mjs → chunk-T2KCAHOS.mjs} +1 -1
  97. package/dist/{chunk-ZJKGQMYH.mjs → chunk-TB6SD2FT.mjs} +1 -1
  98. package/dist/{chunk-AZJF2BLK.mjs → chunk-TBNZHU6C.mjs} +1 -1
  99. package/dist/{chunk-Y4GL2MHX.mjs → chunk-TZDGAP5N.mjs} +28 -10
  100. package/dist/{chunk-CZCQZHG6.mjs → chunk-U2XJFYED.mjs} +1 -1
  101. package/dist/{chunk-TERDKCLE.mjs → chunk-VF2ATYN3.mjs} +1 -1
  102. package/dist/{chunk-OHBNABL5.mjs → chunk-VKID2D2I.mjs} +1 -1
  103. package/dist/{chunk-QKH5ZOD5.mjs → chunk-WF2XDFRK.mjs} +40 -17
  104. package/dist/{chunk-FZZLPJ6B.mjs → chunk-WYEUNUTP.mjs} +44 -15
  105. package/dist/{chunk-PFZTM6D5.mjs → chunk-Y2NS74WS.mjs} +9 -7
  106. package/dist/{chunk-O3HA6TYM.mjs → chunk-YJ7I257J.mjs} +3 -3
  107. package/dist/{chunk-NA7PARID.mjs → chunk-Z4VHZ7B5.mjs} +1 -1
  108. package/dist/{chunk-MLF3EZFW.mjs → chunk-Z6SFHN6T.mjs} +1 -1
  109. package/dist/{chunk-4K625MVM.mjs → chunk-ZZ2R6KZ3.mjs} +1 -1
  110. package/dist/index.d.mts +4 -1
  111. package/dist/index.d.ts +4 -1
  112. package/dist/index.js +1011 -88
  113. package/dist/index.mjs +34 -32
  114. package/package.json +1 -1
  115. package/src/components/Accordion/Accordion.tsx +7 -3
  116. package/src/components/ConfirmDialog/ConfirmDialog.tsx +61 -23
  117. package/src/components/DetailRow/DetailRow.tsx +1 -1
  118. package/src/components/IconPicker/IconPicker.tsx +383 -0
  119. package/src/components/IconPicker/index.ts +1 -0
  120. package/src/components/ImageUpload/ImageUpload.tsx +34 -12
  121. package/src/components/ImageViewer/ImageViewer.tsx +25 -30
  122. package/src/components/NumberStepper/NumberStepper.tsx +147 -0
  123. package/src/components/NumberStepper/index.ts +1 -0
  124. package/src/components/Sheet/Sheet.tsx +10 -9
  125. package/src/components/Switch/Switch.tsx +30 -17
  126. package/src/index.ts +3 -1
  127. package/src/utils/curatedIcons.ts +286 -0
  128. package/src/utils/icons.ts +20 -2
@@ -17,6 +17,8 @@ export interface ImageUploadProps {
17
17
  loading?: boolean
18
18
  /** Text shown when no image is selected. */
19
19
  placeholder?: string
20
+ /** Whether to show the placeholder text. Use false for compact/avatar variants. */
21
+ showPlaceholderText?: boolean
20
22
  /** Width of the upload area. Defaults to full width (undefined). */
21
23
  width?: number
22
24
  /** Height of the upload area. Defaults to 200. */
@@ -35,6 +37,7 @@ export function ImageUpload({
35
37
  onChange,
36
38
  loading = false,
37
39
  placeholder = 'Tap to add image',
40
+ showPlaceholderText = true,
38
41
  width,
39
42
  height = 200,
40
43
  borderRadius = RADIUS.lg,
@@ -50,27 +53,39 @@ export function ImageUpload({
50
53
  impactLight()
51
54
 
52
55
  // Dynamic import so expo-image-picker is optional at module load time.
53
- // Consumers who don't use ImageUpload never pull this dep.
54
- let ImagePicker: typeof import('expo-image-picker')
56
+ // Import ExponentImagePicker (the raw native module proxy) directly to
57
+ // avoid expo-image-picker's top-level createPermissionHook dependency.
58
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
59
+ let picker: any
55
60
  try {
56
- ImagePicker = await import('expo-image-picker')
61
+ const mod = await import('expo-image-picker/build/ExponentImagePicker')
62
+ picker = (mod as { default: unknown }).default
57
63
  } catch {
58
- if (__DEV__) console.warn('[ImageUpload] expo-image-picker not installed. Add it as a dependency.')
59
- return
64
+ // Fallback: try the main module
65
+ try {
66
+ picker = await import('expo-image-picker')
67
+ } catch {
68
+ if (__DEV__) console.warn('[ImageUpload] expo-image-picker not installed.')
69
+ return
70
+ }
60
71
  }
61
72
 
62
73
  if (Platform.OS !== 'web') {
63
- const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync()
64
- if (status !== 'granted') return
74
+ try {
75
+ const { status } = await picker.requestMediaLibraryPermissionsAsync()
76
+ if (status !== 'granted') return
77
+ } catch {
78
+ // Permission check failed — try picker anyway
79
+ }
65
80
  }
66
81
 
67
- const result = await ImagePicker.launchImageLibraryAsync({
82
+ const result = await picker.launchImageLibraryAsync({
68
83
  mediaTypes: ['images'],
69
84
  allowsEditing: true,
70
85
  quality: 0.8,
71
86
  })
72
87
 
73
- if (!result.canceled && result.assets[0]) {
88
+ if (!result.canceled && result.assets?.[0]) {
74
89
  onChange?.(result.assets[0].uri)
75
90
  }
76
91
  }
@@ -106,9 +121,15 @@ export function ImageUpload({
106
121
  ) : (
107
122
  <View style={styles.placeholder}>
108
123
  <Feather name="image" size={ms(28)} color={colors.foregroundMuted} />
109
- <Text style={[styles.placeholderText, { color: colors.foregroundMuted }]} allowFontScaling={true}>
110
- {placeholder}
111
- </Text>
124
+ {showPlaceholderText ? (
125
+ <Text
126
+ style={[styles.placeholderText, { color: colors.foregroundMuted }]}
127
+ numberOfLines={1}
128
+ allowFontScaling={true}
129
+ >
130
+ {placeholder}
131
+ </Text>
132
+ ) : null}
112
133
  </View>
113
134
  )}
114
135
  {loading ? (
@@ -137,6 +158,7 @@ const styles = StyleSheet.create({
137
158
  placeholderText: {
138
159
  fontFamily: 'Sohne-Regular',
139
160
  fontSize: ms(13),
161
+ textAlign: 'center',
140
162
  },
141
163
  loadingOverlay: {
142
164
  ...StyleSheet.absoluteFillObject,
@@ -2,7 +2,8 @@ import React, { useState, useCallback } from 'react'
2
2
  import {
3
3
  Modal,
4
4
  View,
5
- TouchableOpacity,
5
+ Image,
6
+ Dimensions,
6
7
  StyleSheet,
7
8
  useWindowDimensions,
8
9
  ImageSourcePropType,
@@ -18,7 +19,7 @@ import Animated, {
18
19
  runOnJS,
19
20
  } from 'react-native-reanimated'
20
21
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
21
- import { renderIcon } from '../../utils/icons'
22
+ import { IconButton } from '../IconButton'
22
23
  import { PagerDots } from '../PagerDots'
23
24
  import { s, vs } from '../../utils/scaling'
24
25
 
@@ -102,13 +103,11 @@ function ZoomableImage({ source, width, height, onZoomChange }: ZoomableImagePro
102
103
 
103
104
  return (
104
105
  <GestureDetector gesture={composed}>
105
- <Animated.View style={[{ width, height }, styles.imageWrap]}>
106
- <Animated.Image
107
- source={source}
108
- style={[{ width, height }, animatedStyle]}
109
- resizeMode="contain"
110
- />
111
- </Animated.View>
106
+ <View style={[{ width, height }, styles.imageWrap]} collapsable={false}>
107
+ <Animated.View style={[{ width, height }, animatedStyle]}>
108
+ <Image source={source} style={{ width, height }} resizeMode="contain" />
109
+ </Animated.View>
110
+ </View>
112
111
  </GestureDetector>
113
112
  )
114
113
  }
@@ -132,7 +131,9 @@ export interface ImageViewerProps {
132
131
  * <ImageViewer images={pages} visible={open} initialIndex={page} onClose={() => setOpen(false)} />
133
132
  */
134
133
  export function ImageViewer({ images, visible, onClose, initialIndex = 0 }: ImageViewerProps) {
135
- const { width, height } = useWindowDimensions()
134
+ const window = useWindowDimensions()
135
+ const width = window.width > 0 ? window.width : Dimensions.get('window').width
136
+ const height = window.height > 0 ? window.height : Dimensions.get('window').height
136
137
  const insets = useSafeAreaInsets()
137
138
  const [index, setIndex] = useState(initialIndex)
138
139
  const [pagingEnabled, setPagingEnabled] = useState(true)
@@ -203,7 +204,7 @@ export function ImageViewer({ images, visible, onClose, initialIndex = 0 }: Imag
203
204
  <Animated.View style={[styles.backdrop, backdropStyle]} pointerEvents="none" />
204
205
  <Animated.View style={[styles.container, dismissStyle]}>
205
206
  <GestureDetector gesture={swipeDown}>
206
- <Animated.View style={styles.root}>
207
+ <View style={styles.root} collapsable={false}>
207
208
  <ScrollView
208
209
  ref={scrollRef}
209
210
  horizontal
@@ -223,20 +224,20 @@ export function ImageViewer({ images, visible, onClose, initialIndex = 0 }: Imag
223
224
  />
224
225
  ))}
225
226
  </ScrollView>
226
- </Animated.View>
227
+ </View>
227
228
  </GestureDetector>
228
229
 
229
- <TouchableOpacity
230
- style={[styles.closeButton, { top: insets.top + vs(8) }]}
231
- onPress={onClose}
232
- activeOpacity={0.7}
233
- touchSoundDisabled={true}
234
- accessibilityRole="button"
235
- accessibilityLabel="Close"
236
- hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }}
237
- >
238
- {renderIcon('x', 26, '#fff')}
239
- </TouchableOpacity>
230
+ <View style={[styles.closeButtonWrapper, { top: insets.top + vs(8) }]}>
231
+ <IconButton
232
+ iconName="x"
233
+ size="md"
234
+ variant="text"
235
+ style={{ backgroundColor: 'rgba(255,255,255,0.18)' }}
236
+ iconColor="#fff"
237
+ onPress={onClose}
238
+ accessibilityLabel="Close"
239
+ />
240
+ </View>
240
241
 
241
242
  {images.length > 1 ? (
242
243
  <View style={[styles.dots, { bottom: insets.bottom + vs(16) }]} pointerEvents="box-none">
@@ -271,15 +272,9 @@ const styles = StyleSheet.create({
271
272
  justifyContent: 'center',
272
273
  overflow: 'hidden',
273
274
  },
274
- closeButton: {
275
+ closeButtonWrapper: {
275
276
  position: 'absolute',
276
277
  right: s(12),
277
- width: s(40),
278
- height: s(40),
279
- borderRadius: s(20),
280
- backgroundColor: 'rgba(0,0,0,0.4)',
281
- alignItems: 'center',
282
- justifyContent: 'center',
283
278
  },
284
279
  dots: {
285
280
  position: 'absolute',
@@ -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,7 +1,6 @@
1
1
  import React, { useCallback, useEffect, useRef } from 'react'
2
2
  import { View, Text, TouchableOpacity, StyleSheet, ViewStyle, Dimensions, Platform, Modal, ScrollView, useWindowDimensions, Pressable } from 'react-native'
3
- import {
4
- BottomSheetModal,
3
+ import BottomSheet, {
5
4
  BottomSheetView,
6
5
  BottomSheetScrollView,
7
6
  BottomSheetBackdrop,
@@ -149,7 +148,7 @@ export function Sheet({
149
148
  const { colors } = useTheme()
150
149
  const insets = useSafeAreaInsets()
151
150
  const { width: windowWidth } = useWindowDimensions()
152
- const ref = useRef<BottomSheetModal>(null)
151
+ const ref = useRef<BottomSheet>(null)
153
152
  const asDialog = responsive && windowWidth >= BREAKPOINTS.wide
154
153
 
155
154
  // 'interactive' + 'adjustPan' works properly with enableDynamicSizing on both platforms
@@ -159,9 +158,9 @@ export function Sheet({
159
158
  useEffect(() => {
160
159
  if (open) {
161
160
  impactMedium()
162
- ref.current?.present()
161
+ ref.current?.snapToIndex(0)
163
162
  } else {
164
- ref.current?.dismiss()
163
+ ref.current?.close()
165
164
  }
166
165
  }, [open])
167
166
 
@@ -193,7 +192,7 @@ export function Sheet({
193
192
  const showHeader = !!(title || effectiveSubtitle || showCloseButton) && !customHeader
194
193
 
195
194
  const headerNode = customHeader ? customHeader : (showHeader ? (
196
- <View style={styles.header} accessibilityRole="header">
195
+ <View style={[styles.header, { backgroundColor: colors.card }]} accessibilityRole="header">
197
196
  <View style={styles.headerRow}>
198
197
  {title ? (
199
198
  <Text style={[styles.title, { color: colors.foreground }]} allowFontScaling={true}>
@@ -270,12 +269,13 @@ export function Sheet({
270
269
  const useDynamicSizing = !snapPoints
271
270
 
272
271
  return (
273
- <BottomSheetModal
272
+ <BottomSheet
274
273
  ref={ref}
274
+ index={-1}
275
+ onClose={onClose}
275
276
  enableDynamicSizing={useDynamicSizing}
276
277
  snapPoints={snapPoints}
277
278
  maxDynamicContentSize={useDynamicSizing ? effectiveMaxHeight : undefined}
278
- onDismiss={onClose}
279
279
  backdropComponent={renderBackdrop}
280
280
  footerComponent={effectiveFooter ? renderFooter : undefined}
281
281
  backgroundStyle={[styles.background, { backgroundColor: colors.card }]}
@@ -297,6 +297,7 @@ export function Sheet({
297
297
  showsVerticalScrollIndicator={true}
298
298
  indicatorStyle="black"
299
299
  persistentScrollbar={isAndroid}
300
+ stickyHeaderIndices={headerNode ? [0] : undefined}
300
301
  >
301
302
  {headerNode}
302
303
  {contentNode}
@@ -307,7 +308,7 @@ export function Sheet({
307
308
  {contentNode}
308
309
  </BottomSheetView>
309
310
  )}
310
- </BottomSheetModal>
311
+ </BottomSheet>
311
312
  )
312
313
  }
313
314
 
@@ -14,6 +14,8 @@ const THUMB_OFFSET = s(3)
14
14
  const THUMB_TRAVEL = TRACK_WIDTH - THUMB_SIZE - THUMB_OFFSET * 2
15
15
  const ICON_SIZE = s(13)
16
16
 
17
+ const DISABLED_OPACITY = 0.45
18
+
17
19
  export interface SwitchProps {
18
20
  checked?: boolean
19
21
  onCheckedChange?: (checked: boolean) => void
@@ -24,9 +26,10 @@ export interface SwitchProps {
24
26
 
25
27
  export function Switch({ checked = false, onCheckedChange, disabled, style, accessibilityLabel }: SwitchProps) {
26
28
  const { colors } = useTheme()
29
+ const isDisabled = !!disabled
27
30
 
28
31
  return (
29
- <View style={[{ opacity: disabled ? 0.45 : 1, alignSelf: 'flex-start' }, style]}>
32
+ <View style={[{ alignSelf: 'flex-start' }, style]}>
30
33
  <TouchableOpacity
31
34
  onPress={() => {
32
35
  hapticSelection()
@@ -37,20 +40,15 @@ export function Switch({ checked = false, onCheckedChange, disabled, style, acce
37
40
  touchSoundDisabled={true}
38
41
  accessibilityRole="switch"
39
42
  accessibilityLabel={accessibilityLabel}
40
- accessibilityState={{ checked, disabled: !!disabled }}
43
+ accessibilityState={{ checked, disabled: isDisabled }}
41
44
  style={styles.touchable}
42
45
  >
43
- <EaseView
44
- style={styles.track}
45
- animate={{ backgroundColor: checked ? colors.primary : colors.surfaceStrong }}
46
- transition={COLOR_TRANSITION}
47
- >
48
- {/*
49
- AUDIT FIX: the off-state track used surfaceStrong (~#ebebeb in light mode)
50
- with no border — nearly invisible on white page/card surfaces. A 1.5px border
51
- that fades out as the track fills gives the off state clear visual definition
52
- without adding visual weight to the on state.
53
- */}
46
+ <View style={styles.trackContainer}>
47
+ <EaseView
48
+ style={[styles.track, isDisabled && styles.disabledTrack]}
49
+ animate={{ backgroundColor: checked ? colors.primary : colors.surfaceStrong }}
50
+ transition={COLOR_TRANSITION}
51
+ />
54
52
  <EaseView
55
53
  style={[styles.trackBorder, { borderWidth: 1.5 }]}
56
54
  pointerEvents="none"
@@ -62,14 +60,22 @@ export function Switch({ checked = false, onCheckedChange, disabled, style, acce
62
60
  animate={{ translateX: checked ? THUMB_TRAVEL : 0 }}
63
61
  transition={SPRING_ELASTIC}
64
62
  >
65
- <EaseView style={styles.iconWrapper} animate={{ opacity: checked ? 1 : 0 }} transition={OPACITY_TRANSITION}>
63
+ <EaseView
64
+ style={styles.iconWrapper}
65
+ animate={{ opacity: checked ? (isDisabled ? DISABLED_OPACITY : 1) : 0 }}
66
+ transition={OPACITY_TRANSITION}
67
+ >
66
68
  <Feather name="check" size={ICON_SIZE} color={colors.primary} />
67
69
  </EaseView>
68
- <EaseView style={styles.iconWrapper} animate={{ opacity: checked ? 0 : 1 }} transition={OPACITY_TRANSITION}>
70
+ <EaseView
71
+ style={styles.iconWrapper}
72
+ animate={{ opacity: checked ? 0 : (isDisabled ? DISABLED_OPACITY : 1) }}
73
+ transition={OPACITY_TRANSITION}
74
+ >
69
75
  <Feather name="x" size={ICON_SIZE} color={colors.foregroundMuted} />
70
76
  </EaseView>
71
77
  </EaseView>
72
- </EaseView>
78
+ </View>
73
79
  </TouchableOpacity>
74
80
  </View>
75
81
  )
@@ -79,11 +85,18 @@ const styles = StyleSheet.create({
79
85
  touchable: {
80
86
  alignSelf: 'flex-start',
81
87
  },
82
- track: {
88
+ trackContainer: {
89
+ position: 'relative',
83
90
  width: TRACK_WIDTH,
84
91
  height: TRACK_HEIGHT,
92
+ },
93
+ track: {
94
+ ...StyleSheet.absoluteFillObject,
85
95
  borderRadius: TRACK_HEIGHT / 2,
86
96
  },
97
+ disabledTrack: {
98
+ opacity: DISABLED_OPACITY,
99
+ },
87
100
  trackBorder: {
88
101
  ...StyleSheet.absoluteFillObject,
89
102
  borderRadius: TRACK_HEIGHT / 2,
package/src/index.ts CHANGED
@@ -55,12 +55,14 @@ export * from './components/TabBar'
55
55
  export * from './components/ImageViewer'
56
56
  export * from './components/SheetSelect'
57
57
  export * from './components/ImageUpload'
58
+ export * from './components/IconPicker'
59
+ export * from './components/NumberStepper'
58
60
  // HolographicCard is intentionally NOT re-exported here — it depends on the
59
61
  // optional peer @shopify/react-native-skia, so it must stay out of the main
60
62
  // barrel's module graph. Deep-import it: '@retray-dev/ui-kit/HolographicCard'.
61
63
 
62
64
  // Icon utility
63
- export { Icon, renderIcon, configureIconFamilies } from './utils/icons'
65
+ export { Icon, renderIcon, configureIconFamilies, getValidIconNames } from './utils/icons'
64
66
 
65
67
  // Typography utilities
66
68
  export { getResponsiveFontSize } from './utils/typography'