@retray-dev/ui-kit 10.1.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 (112) hide show
  1. package/COMPONENTS.md +136 -5
  2. package/README.md +4 -4
  3. package/dist/Accordion.js +1 -1
  4. package/dist/Accordion.mjs +2 -2
  5. package/dist/AlertBanner.js +1 -1
  6. package/dist/AlertBanner.mjs +2 -2
  7. package/dist/AppHeader.js +1 -1
  8. package/dist/AppHeader.mjs +3 -3
  9. package/dist/Badge.js +1 -1
  10. package/dist/Badge.mjs +2 -2
  11. package/dist/Button.js +1 -1
  12. package/dist/Button.mjs +2 -2
  13. package/dist/CategoryStrip.js +1 -1
  14. package/dist/CategoryStrip.mjs +2 -2
  15. package/dist/Chip.js +1 -1
  16. package/dist/Chip.mjs +2 -2
  17. package/dist/ConfirmDialog.js +1 -1
  18. package/dist/ConfirmDialog.mjs +3 -3
  19. package/dist/CurrencyInput.js +1 -1
  20. package/dist/CurrencyInput.mjs +3 -3
  21. package/dist/DetailRow.d.mts +1 -1
  22. package/dist/DetailRow.d.ts +1 -1
  23. package/dist/DetailRow.js +1 -1
  24. package/dist/DetailRow.mjs +2 -2
  25. package/dist/EmptyState.js +1 -1
  26. package/dist/EmptyState.mjs +3 -3
  27. package/dist/ErrorBoundary.js +1 -1
  28. package/dist/ErrorBoundary.mjs +2 -2
  29. package/dist/IconButton.js +1 -1
  30. package/dist/IconButton.mjs +2 -2
  31. package/dist/IconPicker.d.mts +17 -0
  32. package/dist/IconPicker.d.ts +17 -0
  33. package/dist/IconPicker.js +997 -0
  34. package/dist/IconPicker.mjs +7 -0
  35. package/dist/ImageUpload.d.mts +3 -1
  36. package/dist/ImageUpload.d.ts +3 -1
  37. package/dist/ImageUpload.js +28 -10
  38. package/dist/ImageUpload.mjs +1 -1
  39. package/dist/ImageViewer.js +1 -1
  40. package/dist/ImageViewer.mjs +4 -4
  41. package/dist/Input.js +1 -1
  42. package/dist/Input.mjs +2 -2
  43. package/dist/LabelValue.js +1 -1
  44. package/dist/LabelValue.mjs +2 -2
  45. package/dist/ListItem.js +1 -1
  46. package/dist/ListItem.mjs +2 -2
  47. package/dist/MediaCard.js +1 -1
  48. package/dist/MediaCard.mjs +2 -2
  49. package/dist/MenuItem.js +1 -1
  50. package/dist/MenuItem.mjs +2 -2
  51. package/dist/NumberStepper.d.mts +19 -0
  52. package/dist/NumberStepper.d.ts +19 -0
  53. package/dist/NumberStepper.js +410 -0
  54. package/dist/NumberStepper.mjs +9 -0
  55. package/dist/PagerDots.js +1 -1
  56. package/dist/PagerDots.mjs +2 -2
  57. package/dist/PricingCard.js +1 -1
  58. package/dist/PricingCard.mjs +4 -4
  59. package/dist/SelectableGrid.js +1 -1
  60. package/dist/SelectableGrid.mjs +2 -2
  61. package/dist/SheetSelect.js +1 -1
  62. package/dist/SheetSelect.mjs +2 -2
  63. package/dist/TabBar.js +1 -1
  64. package/dist/TabBar.mjs +2 -2
  65. package/dist/Textarea.js +1 -1
  66. package/dist/Textarea.mjs +2 -2
  67. package/dist/Toggle.js +1 -1
  68. package/dist/Toggle.mjs +2 -2
  69. package/dist/chunk-53Z3NYGE.mjs +742 -0
  70. package/dist/{chunk-VQ57HWPL.mjs → chunk-6L4G6PBT.mjs} +1 -1
  71. package/dist/{chunk-6OAZJ577.mjs → chunk-6SECQ2ZF.mjs} +2 -2
  72. package/dist/{chunk-KIHCWCWL.mjs → chunk-7LWRKMF5.mjs} +1 -1
  73. package/dist/{chunk-4I7D47FH.mjs → chunk-AJRVDP2H.mjs} +3 -3
  74. package/dist/{chunk-6MKGPAR2.mjs → chunk-BEMIQXXU.mjs} +1 -1
  75. package/dist/chunk-BUMAMSTZ.mjs +126 -0
  76. package/dist/{chunk-UREA2GYY.mjs → chunk-DYT7BG5I.mjs} +1 -1
  77. package/dist/{chunk-WOEYDUJZ.mjs → chunk-ELXBDILQ.mjs} +2 -2
  78. package/dist/{chunk-A4MDAP7G.mjs → chunk-FCSSQK3L.mjs} +1 -1
  79. package/dist/{chunk-2TFTAWVJ.mjs → chunk-HTHGSXFG.mjs} +1 -1
  80. package/dist/{chunk-VGTDN7SW.mjs → chunk-IX3NYLYQ.mjs} +1 -1
  81. package/dist/{chunk-T7XZ7H7Y.mjs → chunk-KA7LTET3.mjs} +17 -3
  82. package/dist/{chunk-URI2WBIV.mjs → chunk-KOO4WITD.mjs} +1 -1
  83. package/dist/{chunk-JUXSWN54.mjs → chunk-NMU5FMQJ.mjs} +1 -1
  84. package/dist/{chunk-LXJIIOYQ.mjs → chunk-RYZC432S.mjs} +1 -1
  85. package/dist/{chunk-JB67UOB5.mjs → chunk-S2R7UVOE.mjs} +1 -1
  86. package/dist/{chunk-ZUR7AU5R.mjs → chunk-SXLKNTA4.mjs} +1 -1
  87. package/dist/{chunk-3U4SSNWP.mjs → chunk-T2KCAHOS.mjs} +1 -1
  88. package/dist/{chunk-ZJKGQMYH.mjs → chunk-TB6SD2FT.mjs} +1 -1
  89. package/dist/{chunk-AZJF2BLK.mjs → chunk-TBNZHU6C.mjs} +1 -1
  90. package/dist/{chunk-Y4GL2MHX.mjs → chunk-TZDGAP5N.mjs} +28 -10
  91. package/dist/{chunk-CZCQZHG6.mjs → chunk-U2XJFYED.mjs} +1 -1
  92. package/dist/{chunk-TERDKCLE.mjs → chunk-VF2ATYN3.mjs} +1 -1
  93. package/dist/{chunk-OHBNABL5.mjs → chunk-VKID2D2I.mjs} +1 -1
  94. package/dist/{chunk-KZL5VTYK.mjs → chunk-WYEUNUTP.mjs} +1 -1
  95. package/dist/{chunk-DJ7RN37L.mjs → chunk-YJ7I257J.mjs} +1 -1
  96. package/dist/{chunk-NA7PARID.mjs → chunk-Z4VHZ7B5.mjs} +1 -1
  97. package/dist/{chunk-MLF3EZFW.mjs → chunk-Z6SFHN6T.mjs} +1 -1
  98. package/dist/{chunk-4K625MVM.mjs → chunk-ZZ2R6KZ3.mjs} +1 -1
  99. package/dist/index.d.mts +4 -1
  100. package/dist/index.d.ts +4 -1
  101. package/dist/index.js +892 -12
  102. package/dist/index.mjs +33 -31
  103. package/package.json +1 -1
  104. package/src/components/DetailRow/DetailRow.tsx +1 -1
  105. package/src/components/IconPicker/IconPicker.tsx +383 -0
  106. package/src/components/IconPicker/index.ts +1 -0
  107. package/src/components/ImageUpload/ImageUpload.tsx +34 -12
  108. package/src/components/NumberStepper/NumberStepper.tsx +147 -0
  109. package/src/components/NumberStepper/index.ts +1 -0
  110. package/src/index.ts +3 -1
  111. package/src/utils/curatedIcons.ts +286 -0
  112. package/src/utils/icons.ts +20 -2
package/dist/index.mjs CHANGED
@@ -1,60 +1,62 @@
1
+ export { Switch } from './chunk-WF2XDFRK.mjs';
2
+ export { TabBar } from './chunk-Z6SFHN6T.mjs';
1
3
  export { Tabs, TabsContent } from './chunk-GQYFLP3D.mjs';
2
4
  export { Text } from './chunk-WJLKJMKR.mjs';
3
- export { Textarea } from './chunk-CZCQZHG6.mjs';
4
- export { Toggle } from './chunk-KIHCWCWL.mjs';
5
+ export { Textarea } from './chunk-U2XJFYED.mjs';
6
+ export { Toggle } from './chunk-7LWRKMF5.mjs';
5
7
  export { VirtualList } from './chunk-NC5ZTR2Y.mjs';
8
+ export { Select } from './chunk-A3A6KNQN.mjs';
9
+ export { SelectableGrid } from './chunk-Z4VHZ7B5.mjs';
6
10
  export { Separator } from './chunk-MX6HRKMI.mjs';
7
11
  export { BottomSheetModalProvider, Sheet, BottomSheetTextInput as SheetTextInput } from './chunk-Y2NS74WS.mjs';
8
- export { SheetSelect } from './chunk-URI2WBIV.mjs';
12
+ export { SheetSelect } from './chunk-KOO4WITD.mjs';
9
13
  export { Skeleton } from './chunk-AJ7ZDNBT.mjs';
10
14
  export { Slider } from './chunk-JMOZEC77.mjs';
11
- export { Switch } from './chunk-WF2XDFRK.mjs';
12
- export { TabBar } from './chunk-MLF3EZFW.mjs';
15
+ export { MonthPicker, dateToMonthPickerValue, monthPickerValueToDate } from './chunk-GD6KXMG5.mjs';
16
+ export { NumberStepper } from './chunk-BUMAMSTZ.mjs';
13
17
  export { Pressable } from './chunk-MBMXYJJV.mjs';
14
- export { PricingCard } from './chunk-4I7D47FH.mjs';
18
+ export { PricingCard } from './chunk-AJRVDP2H.mjs';
15
19
  export { Progress } from './chunk-OB4JUQ3O.mjs';
16
20
  export { RadioGroup } from './chunk-X4G6APW6.mjs';
17
21
  export { RetrayProvider } from './chunk-YFZ3ELX5.mjs';
18
22
  export { ToastProvider, sonnerToast as toast, useToast } from './chunk-2UYENBLV.mjs';
19
- export { Select } from './chunk-A3A6KNQN.mjs';
20
- export { SelectableGrid } from './chunk-NA7PARID.mjs';
21
- export { LabelValue } from './chunk-A4MDAP7G.mjs';
23
+ export { ImageViewer } from './chunk-ELXBDILQ.mjs';
24
+ export { PagerDots } from './chunk-ZZ2R6KZ3.mjs';
25
+ export { LabelValue } from './chunk-FCSSQK3L.mjs';
22
26
  export { ListGroup, ListGroupFooter, ListGroupHeader } from './chunk-SOA2Z4RB.mjs';
23
- export { ListItem } from './chunk-OHBNABL5.mjs';
24
- export { MediaCard } from './chunk-VGTDN7SW.mjs';
27
+ export { ListItem } from './chunk-VKID2D2I.mjs';
28
+ export { MediaCard } from './chunk-IX3NYLYQ.mjs';
25
29
  export { MenuGroup, MenuGroupFooter, MenuGroupHeader } from './chunk-IRRY3CRZ.mjs';
26
- export { MenuItem } from './chunk-ZJKGQMYH.mjs';
27
- export { MonthPicker, dateToMonthPickerValue, monthPickerValueToDate } from './chunk-GD6KXMG5.mjs';
28
- export { DetailRow } from './chunk-JB67UOB5.mjs';
29
- export { EmptyState } from './chunk-6OAZJ577.mjs';
30
- export { ErrorBoundary } from './chunk-LXJIIOYQ.mjs';
30
+ export { MenuItem } from './chunk-TB6SD2FT.mjs';
31
+ export { DetailRow } from './chunk-S2R7UVOE.mjs';
32
+ export { EmptyState } from './chunk-6SECQ2ZF.mjs';
33
+ export { ErrorBoundary } from './chunk-RYZC432S.mjs';
31
34
  export { Form, FormField, FormFooter, FormSection } from './chunk-6Q64UFIA.mjs';
32
- export { ImageUpload } from './chunk-Y4GL2MHX.mjs';
35
+ export { IconPicker } from './chunk-53Z3NYGE.mjs';
36
+ export { ImageUpload } from './chunk-TZDGAP5N.mjs';
33
37
  export { Spinner } from './chunk-WBOOUHSS.mjs';
34
- export { ImageViewer } from './chunk-WOEYDUJZ.mjs';
35
- export { PagerDots } from './chunk-4K625MVM.mjs';
36
38
  export { ButtonGroup } from './chunk-3BBOZ3OQ.mjs';
37
39
  export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './chunk-ID72TK46.mjs';
38
- export { CategoryStrip } from './chunk-VQ57HWPL.mjs';
40
+ export { CategoryStrip } from './chunk-6L4G6PBT.mjs';
39
41
  import './chunk-YNROWHQJ.mjs';
40
42
  export { Checkbox } from './chunk-AV4EMIRH.mjs';
41
- export { Chip, ChipGroup } from './chunk-UREA2GYY.mjs';
42
- export { ConfirmDialog } from './chunk-KZL5VTYK.mjs';
43
+ export { Chip, ChipGroup } from './chunk-DYT7BG5I.mjs';
44
+ export { ConfirmDialog } from './chunk-WYEUNUTP.mjs';
43
45
  export { CurrencyDisplay } from './chunk-BRKYVJVV.mjs';
44
- export { CurrencyInput } from './chunk-JUXSWN54.mjs';
45
- export { Input } from './chunk-ZUR7AU5R.mjs';
46
- export { Accordion } from './chunk-DJ7RN37L.mjs';
47
- export { AlertBanner } from './chunk-6MKGPAR2.mjs';
48
- export { AppHeader } from './chunk-AZJF2BLK.mjs';
49
- export { IconButton } from './chunk-3U4SSNWP.mjs';
46
+ export { CurrencyInput } from './chunk-NMU5FMQJ.mjs';
47
+ export { Input } from './chunk-SXLKNTA4.mjs';
48
+ export { Accordion } from './chunk-YJ7I257J.mjs';
49
+ export { AlertBanner } from './chunk-BEMIQXXU.mjs';
50
+ export { AppHeader } from './chunk-TBNZHU6C.mjs';
51
+ export { IconButton } from './chunk-T2KCAHOS.mjs';
50
52
  export { Avatar } from './chunk-JT7HKXRB.mjs';
51
- export { Badge } from './chunk-TERDKCLE.mjs';
52
- export { Button } from './chunk-2TFTAWVJ.mjs';
53
+ export { Badge } from './chunk-VF2ATYN3.mjs';
54
+ export { Button } from './chunk-HTHGSXFG.mjs';
53
55
  import './chunk-3DKJ2GIC.mjs';
54
56
  export { impactHeavy, impactLight, impactMedium, notificationError, notificationSuccess, notificationWarning, richHaptics, selectionAsync } from './chunk-EJ7ZPXOH.mjs';
55
57
  import './chunk-DVK4G2GT.mjs';
56
58
  export { BREAKPOINTS, ICON_SIZES, RADIUS, SHADOWS, SPACING, TYPOGRAPHY } from './chunk-QY3X2UYR.mjs';
57
- export { Icon, configureIconFamilies, renderIcon } from './chunk-T7XZ7H7Y.mjs';
59
+ export { Icon, configureIconFamilies, getValidIconNames, renderIcon } from './chunk-KA7LTET3.mjs';
58
60
  export { ThemeProvider, defaultDark, defaultLight, deriveColors, useTheme } from './chunk-SOYNZDVY.mjs';
59
61
  import './chunk-2CE3TQVY.mjs';
60
62
  import './chunk-Y6FXYEAI.mjs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retray-dev/ui-kit",
3
- "version": "10.1.0",
3
+ "version": "10.2.0",
4
4
  "description": "Personal UI Kit for React Native / Expo",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -16,7 +16,7 @@ const weightMap: Record<DetailRowLabelWeight, string> = {
16
16
 
17
17
  export interface DetailRowProps {
18
18
  label: React.ReactNode
19
- value: string | React.ReactNode
19
+ value: string | number | React.ReactNode
20
20
  /** Dotted/dashed/solid line between label and value. Defaults to 'dotted'. */
21
21
  separator?: DetailRowSeparator
22
22
  labelWeight?: DetailRowLabelWeight
@@ -0,0 +1,383 @@
1
+ import React, { useRef, useState, useCallback, useEffect, useMemo, useId } from 'react'
2
+ import { View, Text, TouchableOpacity, StyleSheet, ViewStyle, Dimensions } from 'react-native'
3
+ import { ScrollView } from 'react-native-gesture-handler'
4
+ import {
5
+ BottomSheetModal,
6
+ BottomSheetScrollView,
7
+ BottomSheetBackdrop,
8
+ } from '@gorhom/bottom-sheet'
9
+ import { useSafeAreaInsets } from 'react-native-safe-area-context'
10
+ import { useTheme } from '../../theme'
11
+ import { renderIcon } from '../../utils/icons'
12
+ import { CURATED_ICONS, ALL_CURATED_ICONS } from '../../utils/curatedIcons'
13
+ import { selectionAsync as hapticSelection, impactMedium } from '../../utils/haptics'
14
+ import { s, vs, ms } from '../../utils/scaling'
15
+ import { RADIUS } from '../../tokens'
16
+ import type { BottomSheetBackdropProps, BottomSheetModal as BottomSheetModalType } from '@gorhom/bottom-sheet'
17
+
18
+ const NUM_COLUMNS = 6
19
+ const GAP = 6
20
+ const TRIGGER_SIZE = s(56)
21
+ const SCREEN_HEIGHT = Dimensions.get('window').height
22
+
23
+ export interface IconPickerProps {
24
+ value: string | null
25
+ onChange: (iconName: string) => void
26
+ label?: string
27
+ error?: string
28
+ hint?: string
29
+ disabled?: boolean
30
+ numColumns?: number
31
+ gap?: number
32
+ style?: ViewStyle
33
+ }
34
+
35
+ function IconCell({ name, selected, size, onPress }: { name: string; selected: boolean; size: number; onPress: () => void }) {
36
+ const { colors } = useTheme()
37
+
38
+ const handlePress = () => {
39
+ hapticSelection()
40
+ onPress()
41
+ }
42
+
43
+ const iconColor = selected ? colors.primaryForeground : colors.foreground
44
+ const bg = selected ? colors.primary : 'transparent'
45
+
46
+ return (
47
+ <TouchableOpacity
48
+ onPress={handlePress}
49
+ activeOpacity={0.6}
50
+ touchSoundDisabled={true}
51
+ accessibilityRole="button"
52
+ accessibilityState={{ selected }}
53
+ accessibilityLabel={name}
54
+ style={[styles.cell, { width: size, height: size, backgroundColor: bg }]}
55
+ >
56
+ {renderIcon(name, ms(20), iconColor)}
57
+ </TouchableOpacity>
58
+ )
59
+ }
60
+
61
+ const IconCellMemo = React.memo(IconCell)
62
+
63
+ export function IconPicker({
64
+ value,
65
+ onChange,
66
+ label,
67
+ error,
68
+ hint,
69
+ disabled = false,
70
+ numColumns = NUM_COLUMNS,
71
+ gap = GAP,
72
+ style,
73
+ }: IconPickerProps) {
74
+ const { colors } = useTheme()
75
+ const insets = useSafeAreaInsets()
76
+ const sheetRef = useRef<BottomSheetModalType>(null)
77
+ const catScrollRef = useRef<any>(null)
78
+ const [open, setOpen] = useState(false)
79
+ const [activeCategory, setActiveCategory] = useState<string | null>(null)
80
+ const [containerWidth, setContainerWidth] = useState(() => Dimensions.get('window').width - s(16) * 2)
81
+
82
+ const sheetName = useId()
83
+
84
+ const activeIcons = useMemo(() => {
85
+ if (activeCategory) {
86
+ return CURATED_ICONS.find((c) => c.name === activeCategory)?.icons ?? ALL_CURATED_ICONS
87
+ }
88
+ return ALL_CURATED_ICONS
89
+ }, [activeCategory])
90
+
91
+ const gapPx = s(gap)
92
+ const cellSize = containerWidth > 0
93
+ ? Math.floor((containerWidth - gapPx * (numColumns - 1)) / numColumns)
94
+ : 0
95
+
96
+ const rows = useMemo(() => {
97
+ const result: string[][] = []
98
+ for (let i = 0; i < activeIcons.length; i += numColumns) {
99
+ result.push(activeIcons.slice(i, i + numColumns))
100
+ }
101
+ return result
102
+ }, [activeIcons, numColumns])
103
+
104
+ useEffect(() => {
105
+ if (open) {
106
+ impactMedium()
107
+ sheetRef.current?.present()
108
+ } else {
109
+ sheetRef.current?.dismiss()
110
+ }
111
+ }, [open])
112
+
113
+ const handleSelect = useCallback(
114
+ (iconName: string) => {
115
+ onChange(iconName)
116
+ setOpen(false)
117
+ setActiveCategory(null)
118
+ },
119
+ [onChange],
120
+ )
121
+
122
+ const handleOpen = useCallback(() => {
123
+ if (disabled) return
124
+ setActiveCategory(null)
125
+ setOpen(true)
126
+ }, [disabled])
127
+
128
+ const handleClose = useCallback(() => {
129
+ setOpen(false)
130
+ setActiveCategory(null)
131
+ }, [])
132
+
133
+ const renderBackdrop = useCallback(
134
+ (props: BottomSheetBackdropProps) => (
135
+ <BottomSheetBackdrop {...props} disappearsOnIndex={-1} appearsOnIndex={0} pressBehavior="close" />
136
+ ),
137
+ [],
138
+ )
139
+
140
+ const selectedIcon = value ? renderIcon(value, ms(28), colors.foreground) : null
141
+
142
+ return (
143
+ <View style={[styles.triggerContainer, style]}>
144
+ {label ? (
145
+ <Text style={[styles.triggerLabel, { color: colors.foreground }]} allowFontScaling={true}>
146
+ {label}
147
+ </Text>
148
+ ) : null}
149
+ <TouchableOpacity
150
+ onPress={handleOpen}
151
+ disabled={disabled}
152
+ activeOpacity={0.7}
153
+ touchSoundDisabled={true}
154
+ accessibilityRole="button"
155
+ accessibilityLabel={label ?? 'Seleccionar icono'}
156
+ accessibilityState={{ disabled }}
157
+ style={[
158
+ styles.trigger,
159
+ {
160
+ backgroundColor: disabled ? colors.surface : colors.background,
161
+ width: TRIGGER_SIZE,
162
+ height: TRIGGER_SIZE,
163
+ borderColor: error ? colors.destructive : value ? colors.primary : colors.border,
164
+ },
165
+ disabled && styles.triggerDisabled,
166
+ ]}
167
+ >
168
+ {selectedIcon ?? renderIcon('plus', ms(24), colors.foregroundMuted)}
169
+ </TouchableOpacity>
170
+ {error ? (
171
+ <Text
172
+ style={[styles.helperText, { color: colors.destructive }]}
173
+ allowFontScaling={true}
174
+ accessibilityLiveRegion="polite"
175
+ >
176
+ {error}
177
+ </Text>
178
+ ) : null}
179
+ {!error && hint ? (
180
+ <Text style={[styles.helperText, { color: colors.foregroundMuted }]} allowFontScaling={true}>
181
+ {hint}
182
+ </Text>
183
+ ) : null}
184
+
185
+ <BottomSheetModal
186
+ ref={sheetRef}
187
+ name={sheetName}
188
+ onDismiss={handleClose}
189
+ enableDynamicSizing={true}
190
+ maxDynamicContentSize={SCREEN_HEIGHT * 0.7}
191
+ backdropComponent={renderBackdrop}
192
+ backgroundStyle={[styles.sheetBackground, { backgroundColor: colors.card }]}
193
+ handleIndicatorStyle={[styles.handle, { backgroundColor: colors.border }]}
194
+ enablePanDownToClose
195
+ topInset={insets.top}
196
+ android_keyboardInputMode="adjustPan"
197
+ >
198
+ <BottomSheetScrollView
199
+ contentContainerStyle={styles.sheetContent}
200
+ showsVerticalScrollIndicator={true}
201
+ >
202
+ {/* Category section label */}
203
+ <Text style={[styles.sectionLabel, { color: colors.foregroundSubtle }]} allowFontScaling={true}>
204
+ Categorías
205
+ </Text>
206
+
207
+ {/* Horizontal scrollable category chips */}
208
+ <ScrollView
209
+ ref={catScrollRef}
210
+ horizontal
211
+ showsHorizontalScrollIndicator={false}
212
+ contentContainerStyle={styles.categoryStrip}
213
+ style={styles.categoryScroll}
214
+ >
215
+ <TouchableOpacity
216
+ onPress={() => setActiveCategory(null)}
217
+ activeOpacity={0.7}
218
+ touchSoundDisabled={true}
219
+ accessibilityRole="button"
220
+ accessibilityLabel="Todos"
221
+ accessibilityState={{ selected: activeCategory === null }}
222
+ style={[
223
+ styles.categoryChip,
224
+ {
225
+ backgroundColor: activeCategory === null ? colors.primary : colors.surface,
226
+ borderColor: activeCategory === null ? colors.primary : colors.border,
227
+ },
228
+ ]}
229
+ >
230
+ <View style={styles.categoryChipInner}>
231
+ {renderIcon('grid', ms(14), activeCategory === null ? colors.primaryForeground : colors.foregroundSubtle)}
232
+ <Text
233
+ style={[
234
+ styles.categoryChipText,
235
+ { color: activeCategory === null ? colors.primaryForeground : colors.foreground },
236
+ ]}
237
+ allowFontScaling={true}
238
+ numberOfLines={1}
239
+ >
240
+ Todos
241
+ </Text>
242
+ </View>
243
+ </TouchableOpacity>
244
+ {CURATED_ICONS.map((cat) => (
245
+ <TouchableOpacity
246
+ key={cat.name}
247
+ onPress={() => setActiveCategory(cat.name)}
248
+ activeOpacity={0.7}
249
+ touchSoundDisabled={true}
250
+ accessibilityRole="button"
251
+ accessibilityLabel={cat.labelEs}
252
+ accessibilityState={{ selected: activeCategory === cat.name }}
253
+ style={[
254
+ styles.categoryChip,
255
+ {
256
+ backgroundColor: activeCategory === cat.name ? colors.primary : colors.surface,
257
+ borderColor: activeCategory === cat.name ? colors.primary : colors.border,
258
+ },
259
+ ]}
260
+ >
261
+ <View style={styles.categoryChipInner}>
262
+ {renderIcon(cat.categoryIcon, ms(14), activeCategory === cat.name ? colors.primaryForeground : colors.foregroundSubtle)}
263
+ <Text
264
+ style={[
265
+ styles.categoryChipText,
266
+ { color: activeCategory === cat.name ? colors.primaryForeground : colors.foreground },
267
+ ]}
268
+ allowFontScaling={true}
269
+ numberOfLines={1}
270
+ >
271
+ {cat.labelEs}
272
+ </Text>
273
+ </View>
274
+ </TouchableOpacity>
275
+ ))}
276
+ </ScrollView>
277
+
278
+ {/* Separator */}
279
+ <View style={[styles.separator, { backgroundColor: colors.border }]} />
280
+
281
+ {/* Icon grid — rendered directly inside BottomSheetScrollView */}
282
+ <View style={styles.gridContainer} onLayout={(e) => setContainerWidth(e.nativeEvent.layout.width)}>
283
+ {cellSize > 0 ? rows.map((row, i) => (
284
+ <View key={row[0] ?? `row-${i}`} style={[styles.row, { marginBottom: gapPx }]}>
285
+ {row.map((name) => (
286
+ <IconCellMemo
287
+ key={name}
288
+ name={name}
289
+ selected={value === name}
290
+ size={cellSize}
291
+ onPress={() => handleSelect(name)}
292
+ />
293
+ ))}
294
+ {Array.from({ length: numColumns - row.length }).map((_, j) => (
295
+ <View key={`spacer-${j}`} style={{ width: cellSize, height: cellSize }} />
296
+ ))}
297
+ </View>
298
+ )) : null}
299
+ </View>
300
+ </BottomSheetScrollView>
301
+ </BottomSheetModal>
302
+ </View>
303
+ )
304
+ }
305
+
306
+ const styles = StyleSheet.create({
307
+ triggerContainer: {
308
+ gap: vs(8),
309
+ },
310
+ triggerLabel: {
311
+ fontFamily: 'Sohne-Medium',
312
+ fontSize: ms(14),
313
+ },
314
+ trigger: {
315
+ borderRadius: RADIUS.md,
316
+ borderWidth: 1,
317
+ alignItems: 'center',
318
+ justifyContent: 'center',
319
+ },
320
+ triggerDisabled: {
321
+ opacity: 0.6,
322
+ },
323
+ helperText: {
324
+ fontFamily: 'Sohne-Regular',
325
+ fontSize: ms(13),
326
+ },
327
+ sheetBackground: {
328
+ borderTopLeftRadius: ms(16),
329
+ borderTopRightRadius: ms(16),
330
+ },
331
+ handle: {
332
+ width: s(36),
333
+ height: vs(4),
334
+ borderRadius: ms(2),
335
+ },
336
+ sheetContent: {
337
+ paddingHorizontal: s(16),
338
+ paddingBottom: vs(24),
339
+ },
340
+ sectionLabel: {
341
+ fontFamily: 'Sohne-Medium',
342
+ fontSize: ms(12),
343
+ marginBottom: vs(8),
344
+ textTransform: 'uppercase',
345
+ letterSpacing: 0.5,
346
+ },
347
+ categoryScroll: {
348
+ flexGrow: 0,
349
+ flexShrink: 0,
350
+ },
351
+ categoryStrip: {
352
+ gap: s(8),
353
+ },
354
+ categoryChip: {
355
+ borderRadius: RADIUS.full,
356
+ borderWidth: 1,
357
+ paddingVertical: vs(6),
358
+ paddingHorizontal: s(12),
359
+ },
360
+ categoryChipInner: {
361
+ flexDirection: 'row',
362
+ alignItems: 'center',
363
+ gap: s(6),
364
+ },
365
+ categoryChipText: {
366
+ fontFamily: 'Sohne-Medium',
367
+ fontSize: ms(12),
368
+ },
369
+ separator: {
370
+ height: StyleSheet.hairlineWidth,
371
+ marginVertical: vs(12),
372
+ },
373
+ gridContainer: {},
374
+ row: {
375
+ flexDirection: 'row',
376
+ gap: GAP,
377
+ },
378
+ cell: {
379
+ borderRadius: RADIUS.md,
380
+ alignItems: 'center',
381
+ justifyContent: 'center',
382
+ },
383
+ })
@@ -0,0 +1 @@
1
+ export * from './IconPicker'
@@ -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,