@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.
- package/COMPONENTS.md +419 -38
- package/README.md +14 -5
- package/dist/Accordion.js +1 -1
- package/dist/Accordion.mjs +3 -3
- package/dist/AlertBanner.js +1 -1
- package/dist/AlertBanner.mjs +3 -3
- package/dist/AppHeader.js +1 -1
- package/dist/AppHeader.mjs +4 -4
- package/dist/Avatar.mjs +2 -2
- package/dist/Badge.js +1 -1
- package/dist/Badge.mjs +3 -3
- package/dist/Button.js +1 -1
- package/dist/Button.mjs +3 -3
- package/dist/Card.mjs +2 -2
- package/dist/CategoryStrip.js +1 -1
- package/dist/CategoryStrip.mjs +3 -3
- package/dist/Checkbox.mjs +2 -2
- package/dist/Chip.js +1 -1
- package/dist/Chip.mjs +3 -3
- package/dist/ConfirmDialog.d.mts +1 -6
- package/dist/ConfirmDialog.d.ts +1 -6
- package/dist/ConfirmDialog.js +30 -24
- package/dist/ConfirmDialog.mjs +4 -4
- package/dist/CurrencyDisplay.mjs +2 -2
- package/dist/CurrencyInput.d.mts +3 -8
- package/dist/CurrencyInput.d.ts +3 -8
- package/dist/CurrencyInput.js +4 -2
- package/dist/CurrencyInput.mjs +4 -4
- package/dist/DetailRow.d.mts +1 -1
- package/dist/DetailRow.d.ts +1 -1
- package/dist/DetailRow.js +1 -1
- package/dist/DetailRow.mjs +3 -3
- package/dist/EmptyState.js +1 -1
- package/dist/EmptyState.mjs +4 -4
- package/dist/ErrorBoundary.js +1 -1
- package/dist/ErrorBoundary.mjs +3 -3
- package/dist/Form.mjs +2 -2
- package/dist/IconButton.js +1 -1
- package/dist/IconButton.mjs +3 -3
- package/dist/IconPicker.d.mts +17 -0
- package/dist/IconPicker.d.ts +17 -0
- package/dist/IconPicker.js +1424 -0
- package/dist/IconPicker.mjs +8 -0
- package/dist/ImageUpload.d.mts +3 -1
- package/dist/ImageUpload.d.ts +3 -1
- package/dist/ImageUpload.js +28 -10
- package/dist/ImageUpload.mjs +3 -3
- package/dist/ImageViewer.js +1 -1
- package/dist/ImageViewer.mjs +5 -5
- package/dist/Input.js +1 -1
- package/dist/Input.mjs +3 -3
- package/dist/LabelValue.js +1 -1
- package/dist/LabelValue.mjs +3 -3
- package/dist/ListGroup.mjs +2 -2
- package/dist/ListItem.d.mts +7 -7
- package/dist/ListItem.d.ts +7 -7
- package/dist/ListItem.js +13 -8
- package/dist/ListItem.mjs +3 -3
- package/dist/MediaCard.js +1 -1
- package/dist/MediaCard.mjs +3 -3
- package/dist/MenuGroup.mjs +2 -2
- package/dist/MenuItem.js +1 -1
- package/dist/MenuItem.mjs +3 -3
- package/dist/MonthPicker.mjs +2 -2
- package/dist/NumberStepper.d.mts +19 -0
- package/dist/NumberStepper.d.ts +19 -0
- package/dist/NumberStepper.js +410 -0
- package/dist/NumberStepper.mjs +9 -0
- package/dist/PagerDots.js +1 -1
- package/dist/PagerDots.mjs +3 -3
- package/dist/Pressable.d.mts +15 -7
- package/dist/Pressable.d.ts +15 -7
- package/dist/Pressable.js +7 -3
- package/dist/Pressable.mjs +1 -1
- package/dist/PricingCard.js +1 -1
- package/dist/PricingCard.mjs +5 -5
- package/dist/Progress.mjs +2 -2
- package/dist/RadioGroup.mjs +2 -2
- package/dist/RetrayProvider.mjs +3 -3
- package/dist/Select.mjs +2 -2
- package/dist/SelectableGrid.js +1 -1
- package/dist/SelectableGrid.mjs +3 -3
- package/dist/Separator.mjs +2 -2
- package/dist/Sheet.d.mts +4 -46
- package/dist/Sheet.d.ts +4 -46
- package/dist/Sheet.js +46 -114
- package/dist/Sheet.mjs +2 -3
- package/dist/SheetSelect.js +1 -1
- package/dist/SheetSelect.mjs +3 -3
- package/dist/Skeleton.mjs +2 -2
- package/dist/Slider.mjs +2 -2
- package/dist/Spinner.mjs +2 -2
- package/dist/Stats.d.mts +30 -0
- package/dist/Stats.d.ts +30 -0
- package/dist/Stats.js +429 -0
- package/dist/Stats.mjs +9 -0
- package/dist/Switch.mjs +2 -2
- package/dist/TabBar.js +1 -1
- package/dist/TabBar.mjs +3 -3
- package/dist/Tabs.mjs +2 -2
- package/dist/Text.d.mts +3 -1
- package/dist/Text.d.ts +3 -1
- package/dist/Text.js +3 -3
- package/dist/Text.mjs +2 -2
- package/dist/Textarea.js +1 -1
- package/dist/Textarea.mjs +3 -3
- package/dist/Toast.mjs +2 -2
- package/dist/Toggle.js +1 -1
- package/dist/Toggle.mjs +3 -3
- package/dist/{chunk-DJ7RN37L.mjs → chunk-265G6A46.mjs} +2 -2
- package/dist/{chunk-WOEYDUJZ.mjs → chunk-2A2LEFZG.mjs} +2 -2
- package/dist/{chunk-ID72TK46.mjs → chunk-2CBQKU7H.mjs} +1 -1
- package/dist/{chunk-OB4JUQ3O.mjs → chunk-2I2AYECM.mjs} +1 -1
- package/dist/{chunk-WJLKJMKR.mjs → chunk-357YO24D.mjs} +4 -4
- package/dist/{chunk-GQYFLP3D.mjs → chunk-3GEYJ7I5.mjs} +1 -1
- package/dist/{chunk-AV4EMIRH.mjs → chunk-3N2M3WZL.mjs} +1 -1
- package/dist/{chunk-TERDKCLE.mjs → chunk-3UYAZ7I4.mjs} +2 -2
- package/dist/{chunk-JMOZEC77.mjs → chunk-4WFMPFZB.mjs} +1 -1
- package/dist/chunk-5OLNXP3S.mjs +144 -0
- package/dist/{chunk-6OAZJ577.mjs → chunk-7HSILTC4.mjs} +3 -3
- package/dist/{chunk-IRRY3CRZ.mjs → chunk-AKM4EPOT.mjs} +1 -1
- package/dist/{chunk-VGTDN7SW.mjs → chunk-AQEVCEXV.mjs} +2 -2
- package/dist/{chunk-WBOOUHSS.mjs → chunk-BCWEHE34.mjs} +1 -1
- package/dist/{chunk-AJ7ZDNBT.mjs → chunk-BOVUP27T.mjs} +1 -1
- package/dist/{chunk-BRKYVJVV.mjs → chunk-BQZE3HAW.mjs} +1 -1
- package/dist/{chunk-MLF3EZFW.mjs → chunk-D3Y2T42P.mjs} +2 -2
- package/dist/{chunk-3U4SSNWP.mjs → chunk-DF6DU42P.mjs} +2 -2
- package/dist/{chunk-ZJKGQMYH.mjs → chunk-DI7CBDL6.mjs} +2 -2
- package/dist/{chunk-2TFTAWVJ.mjs → chunk-DOGIPOF5.mjs} +2 -2
- package/dist/{chunk-MBMXYJJV.mjs → chunk-E7NEHHXV.mjs} +7 -3
- package/dist/{chunk-MX6HRKMI.mjs → chunk-EFLFRAHD.mjs} +1 -1
- package/dist/{chunk-SOYNZDVY.mjs → chunk-EMUWGDWC.mjs} +6 -1
- package/dist/{chunk-4I7D47FH.mjs → chunk-F4V6XLP4.mjs} +4 -4
- package/dist/{chunk-UREA2GYY.mjs → chunk-FA2KMTH5.mjs} +2 -2
- package/dist/{chunk-Y2NS74WS.mjs → chunk-FFTYLPSB.mjs} +46 -98
- package/dist/{chunk-OHBNABL5.mjs → chunk-FUVYSVGR.mjs} +14 -9
- package/dist/{chunk-KIHCWCWL.mjs → chunk-FVTVCJAH.mjs} +2 -2
- package/dist/{chunk-Y4GL2MHX.mjs → chunk-GK4VRMNE.mjs} +30 -12
- package/dist/{chunk-6Q64UFIA.mjs → chunk-HJ46DTJE.mjs} +1 -1
- package/dist/{chunk-WF2XDFRK.mjs → chunk-HLMPMUK2.mjs} +1 -1
- package/dist/{chunk-GD6KXMG5.mjs → chunk-I4V5XZPS.mjs} +1 -1
- package/dist/{chunk-AZJF2BLK.mjs → chunk-ISY26JQJ.mjs} +2 -2
- package/dist/{chunk-X4G6APW6.mjs → chunk-J6Q2YJEV.mjs} +1 -1
- package/dist/{chunk-KZL5VTYK.mjs → chunk-JCZQOY4O.mjs} +31 -24
- package/dist/{chunk-CZCQZHG6.mjs → chunk-JNVAIDLK.mjs} +2 -2
- package/dist/{chunk-SOA2Z4RB.mjs → chunk-JULSIZDM.mjs} +1 -1
- package/dist/{chunk-T7XZ7H7Y.mjs → chunk-KA7LTET3.mjs} +17 -3
- package/dist/chunk-KHYX4IOM.mjs +1114 -0
- package/dist/{chunk-LXJIIOYQ.mjs → chunk-LRM4AVYY.mjs} +2 -2
- package/dist/{chunk-VQ57HWPL.mjs → chunk-MYZ2EDYU.mjs} +2 -2
- package/dist/chunk-N4ZPVCJH.mjs +126 -0
- package/dist/{chunk-NA7PARID.mjs → chunk-NXI4YDZ2.mjs} +2 -2
- package/dist/{chunk-4K625MVM.mjs → chunk-OULVKTWL.mjs} +2 -2
- package/dist/{chunk-A4MDAP7G.mjs → chunk-P64WHW4A.mjs} +2 -2
- package/dist/{chunk-URI2WBIV.mjs → chunk-P73V2EKS.mjs} +2 -2
- package/dist/{chunk-ZUR7AU5R.mjs → chunk-PGERH3P7.mjs} +2 -2
- package/dist/{chunk-2UYENBLV.mjs → chunk-QSFV2P7O.mjs} +1 -1
- package/dist/{chunk-JT7HKXRB.mjs → chunk-S3KJCPEJ.mjs} +1 -1
- package/dist/{chunk-6MKGPAR2.mjs → chunk-V6NFJXKO.mjs} +2 -2
- package/dist/{chunk-A3A6KNQN.mjs → chunk-WOEWGSTU.mjs} +1 -1
- package/dist/{chunk-JUXSWN54.mjs → chunk-X26S5EVZ.mjs} +4 -2
- package/dist/{chunk-YFZ3ELX5.mjs → chunk-XBAGGKLW.mjs} +2 -2
- package/dist/{chunk-JB67UOB5.mjs → chunk-ZHMSAYLT.mjs} +2 -2
- package/dist/fonts.d.mts +1 -7
- package/dist/fonts.d.ts +1 -7
- package/dist/fonts.js +0 -2
- package/dist/fonts.mjs +1 -2
- package/dist/index.d.mts +7 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +1831 -475
- package/dist/index.mjs +54 -51
- package/package.json +3 -3
- package/src/components/ConfirmDialog/ConfirmDialog.tsx +39 -30
- package/src/components/CurrencyInput/CurrencyInput.tsx +4 -7
- package/src/components/DetailRow/DetailRow.tsx +1 -1
- package/src/components/IconPicker/IconPicker.tsx +395 -0
- package/src/components/IconPicker/index.ts +1 -0
- package/src/components/ImageUpload/ImageUpload.tsx +34 -12
- package/src/components/ListItem/ListItem.tsx +43 -28
- package/src/components/NumberStepper/NumberStepper.tsx +147 -0
- package/src/components/NumberStepper/index.ts +1 -0
- package/src/components/Pressable/Pressable.tsx +20 -8
- package/src/components/Sheet/Sheet.tsx +64 -172
- package/src/components/Stats/Stats.tsx +226 -0
- package/src/components/Stats/index.ts +2 -0
- package/src/components/Text/Text.tsx +4 -2
- package/src/fonts.ts +0 -7
- package/src/index.ts +7 -1
- package/src/theme/colorUtils.ts +9 -0
- package/src/utils/curatedIcons.ts +849 -0
- package/src/utils/fontGuard.ts +2 -1
- package/src/utils/icons.ts +20 -2
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import React, { useRef, useState, useCallback, 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 { Spinner } from '../Spinner'
|
|
17
|
+
import type { BottomSheetBackdropProps, BottomSheetModal as BottomSheetModalType } from '@gorhom/bottom-sheet'
|
|
18
|
+
|
|
19
|
+
const NUM_COLUMNS = 6
|
|
20
|
+
const GAP = 6
|
|
21
|
+
const TRIGGER_SIZE = s(56)
|
|
22
|
+
const SCREEN_HEIGHT = Dimensions.get('window').height
|
|
23
|
+
|
|
24
|
+
export interface IconPickerProps {
|
|
25
|
+
value: string | null
|
|
26
|
+
onChange: (iconName: string) => void
|
|
27
|
+
label?: string
|
|
28
|
+
error?: string
|
|
29
|
+
hint?: string
|
|
30
|
+
disabled?: boolean
|
|
31
|
+
numColumns?: number
|
|
32
|
+
gap?: number
|
|
33
|
+
style?: ViewStyle
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function IconCell({ name, selected, size, onPress }: { name: string; selected: boolean; size: number; onPress: () => void }) {
|
|
37
|
+
const { colors } = useTheme()
|
|
38
|
+
|
|
39
|
+
const handlePress = () => {
|
|
40
|
+
hapticSelection()
|
|
41
|
+
onPress()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const iconColor = selected ? colors.primaryForeground : colors.foreground
|
|
45
|
+
const bg = selected ? colors.primary : 'transparent'
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<TouchableOpacity
|
|
49
|
+
onPress={handlePress}
|
|
50
|
+
activeOpacity={0.6}
|
|
51
|
+
touchSoundDisabled={true}
|
|
52
|
+
accessibilityRole="button"
|
|
53
|
+
accessibilityState={{ selected }}
|
|
54
|
+
accessibilityLabel={name}
|
|
55
|
+
style={[styles.cell, { width: size, height: size, backgroundColor: bg }]}
|
|
56
|
+
>
|
|
57
|
+
{renderIcon(name, ms(20), iconColor)}
|
|
58
|
+
</TouchableOpacity>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const IconCellMemo = React.memo(IconCell)
|
|
63
|
+
|
|
64
|
+
export function IconPicker({
|
|
65
|
+
value,
|
|
66
|
+
onChange,
|
|
67
|
+
label,
|
|
68
|
+
error,
|
|
69
|
+
hint,
|
|
70
|
+
disabled = false,
|
|
71
|
+
numColumns = NUM_COLUMNS,
|
|
72
|
+
gap = GAP,
|
|
73
|
+
style,
|
|
74
|
+
}: IconPickerProps) {
|
|
75
|
+
const { colors } = useTheme()
|
|
76
|
+
const insets = useSafeAreaInsets()
|
|
77
|
+
const sheetRef = useRef<BottomSheetModalType>(null)
|
|
78
|
+
const catScrollRef = useRef<ScrollView>(null)
|
|
79
|
+
const [activeCategory, setActiveCategory] = useState<string | null>(null)
|
|
80
|
+
const [containerWidth, setContainerWidth] = useState(() => Dimensions.get('window').width - s(16) * 2)
|
|
81
|
+
const [ready, setReady] = useState(false)
|
|
82
|
+
|
|
83
|
+
const sheetName = useId()
|
|
84
|
+
|
|
85
|
+
const activeIcons = useMemo(() => {
|
|
86
|
+
if (activeCategory) {
|
|
87
|
+
return CURATED_ICONS.find((c) => c.name === activeCategory)?.icons ?? ALL_CURATED_ICONS
|
|
88
|
+
}
|
|
89
|
+
return ALL_CURATED_ICONS
|
|
90
|
+
}, [activeCategory])
|
|
91
|
+
|
|
92
|
+
const gapPx = s(gap)
|
|
93
|
+
const cellSize = containerWidth > 0
|
|
94
|
+
? Math.floor((containerWidth - gapPx * (numColumns - 1)) / numColumns)
|
|
95
|
+
: 0
|
|
96
|
+
|
|
97
|
+
const rows = useMemo(() => {
|
|
98
|
+
const result: string[][] = []
|
|
99
|
+
for (let i = 0; i < activeIcons.length; i += numColumns) {
|
|
100
|
+
result.push(activeIcons.slice(i, i + numColumns))
|
|
101
|
+
}
|
|
102
|
+
return result
|
|
103
|
+
}, [activeIcons, numColumns])
|
|
104
|
+
|
|
105
|
+
const handleDismiss = useCallback(() => {
|
|
106
|
+
setActiveCategory(null)
|
|
107
|
+
setReady(false)
|
|
108
|
+
}, [])
|
|
109
|
+
|
|
110
|
+
const handleSelect = useCallback(
|
|
111
|
+
(iconName: string) => {
|
|
112
|
+
onChange(iconName)
|
|
113
|
+
},
|
|
114
|
+
[onChange],
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
const handleOpen = useCallback(() => {
|
|
118
|
+
if (disabled) return
|
|
119
|
+
impactMedium()
|
|
120
|
+
setActiveCategory(null)
|
|
121
|
+
setReady(false)
|
|
122
|
+
sheetRef.current?.present()
|
|
123
|
+
}, [disabled])
|
|
124
|
+
|
|
125
|
+
const renderBackdrop = useCallback(
|
|
126
|
+
(props: BottomSheetBackdropProps) => (
|
|
127
|
+
<BottomSheetBackdrop {...props} disappearsOnIndex={-1} appearsOnIndex={0} pressBehavior="close" />
|
|
128
|
+
),
|
|
129
|
+
[],
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
const selectedIcon = value ? renderIcon(value, ms(28), colors.foreground) : null
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<View style={[styles.triggerContainer, style]}>
|
|
136
|
+
{label ? (
|
|
137
|
+
<Text style={[styles.triggerLabel, { color: colors.foreground }]} allowFontScaling={true}>
|
|
138
|
+
{label}
|
|
139
|
+
</Text>
|
|
140
|
+
) : null}
|
|
141
|
+
<TouchableOpacity
|
|
142
|
+
onPress={handleOpen}
|
|
143
|
+
disabled={disabled}
|
|
144
|
+
activeOpacity={0.7}
|
|
145
|
+
touchSoundDisabled={true}
|
|
146
|
+
accessibilityRole="button"
|
|
147
|
+
accessibilityLabel={label ?? 'Seleccionar icono'}
|
|
148
|
+
accessibilityState={{ disabled }}
|
|
149
|
+
style={[
|
|
150
|
+
styles.trigger,
|
|
151
|
+
{
|
|
152
|
+
backgroundColor: disabled ? colors.surface : colors.background,
|
|
153
|
+
width: TRIGGER_SIZE,
|
|
154
|
+
height: TRIGGER_SIZE,
|
|
155
|
+
borderColor: error ? colors.destructive : value ? colors.primary : colors.border,
|
|
156
|
+
},
|
|
157
|
+
disabled && styles.triggerDisabled,
|
|
158
|
+
]}
|
|
159
|
+
>
|
|
160
|
+
{selectedIcon ?? renderIcon('plus', ms(24), colors.foregroundMuted)}
|
|
161
|
+
</TouchableOpacity>
|
|
162
|
+
{error ? (
|
|
163
|
+
<Text
|
|
164
|
+
style={[styles.helperText, { color: colors.destructive }]}
|
|
165
|
+
allowFontScaling={true}
|
|
166
|
+
accessibilityLiveRegion="polite"
|
|
167
|
+
>
|
|
168
|
+
{error}
|
|
169
|
+
</Text>
|
|
170
|
+
) : null}
|
|
171
|
+
{!error && hint ? (
|
|
172
|
+
<Text style={[styles.helperText, { color: colors.foregroundMuted }]} allowFontScaling={true}>
|
|
173
|
+
{hint}
|
|
174
|
+
</Text>
|
|
175
|
+
) : null}
|
|
176
|
+
|
|
177
|
+
<BottomSheetModal
|
|
178
|
+
ref={sheetRef}
|
|
179
|
+
name={sheetName}
|
|
180
|
+
onDismiss={handleDismiss}
|
|
181
|
+
enableDynamicSizing={true}
|
|
182
|
+
maxDynamicContentSize={SCREEN_HEIGHT * 0.7}
|
|
183
|
+
backdropComponent={renderBackdrop}
|
|
184
|
+
backgroundStyle={{ ...styles.sheetBackground, backgroundColor: colors.card }}
|
|
185
|
+
handleIndicatorStyle={{ ...styles.handle, backgroundColor: colors.border }}
|
|
186
|
+
enablePanDownToClose
|
|
187
|
+
topInset={insets.top}
|
|
188
|
+
android_keyboardInputMode="adjustPan"
|
|
189
|
+
>
|
|
190
|
+
<BottomSheetScrollView
|
|
191
|
+
contentContainerStyle={styles.sheetContent}
|
|
192
|
+
showsVerticalScrollIndicator={true}
|
|
193
|
+
>
|
|
194
|
+
{/* Measuring container — always rendered so onLayout fires */}
|
|
195
|
+
<View
|
|
196
|
+
style={styles.gridContainer}
|
|
197
|
+
onLayout={(e) => {
|
|
198
|
+
setContainerWidth(e.nativeEvent.layout.width)
|
|
199
|
+
setReady(true)
|
|
200
|
+
}}
|
|
201
|
+
>
|
|
202
|
+
{!ready ? (
|
|
203
|
+
<View style={styles.loader}>
|
|
204
|
+
<Spinner size="md" color={colors.primary} label="Cargando iconos..." />
|
|
205
|
+
</View>
|
|
206
|
+
) : (
|
|
207
|
+
<>
|
|
208
|
+
{/* Category section label */}
|
|
209
|
+
<Text style={[styles.sectionLabel, { color: colors.foregroundSubtle }]} allowFontScaling={true}>
|
|
210
|
+
Categorías
|
|
211
|
+
</Text>
|
|
212
|
+
|
|
213
|
+
{/* Horizontal scrollable category chips */}
|
|
214
|
+
<ScrollView
|
|
215
|
+
ref={catScrollRef}
|
|
216
|
+
horizontal
|
|
217
|
+
showsHorizontalScrollIndicator={false}
|
|
218
|
+
contentContainerStyle={styles.categoryStrip}
|
|
219
|
+
style={styles.categoryScroll}
|
|
220
|
+
>
|
|
221
|
+
<TouchableOpacity
|
|
222
|
+
onPress={() => setActiveCategory(null)}
|
|
223
|
+
activeOpacity={0.7}
|
|
224
|
+
touchSoundDisabled={true}
|
|
225
|
+
accessibilityRole="button"
|
|
226
|
+
accessibilityLabel="Todos"
|
|
227
|
+
accessibilityState={{ selected: activeCategory === null }}
|
|
228
|
+
style={[
|
|
229
|
+
styles.categoryChip,
|
|
230
|
+
{
|
|
231
|
+
backgroundColor: activeCategory === null ? colors.primary : colors.surface,
|
|
232
|
+
borderColor: activeCategory === null ? colors.primary : colors.border,
|
|
233
|
+
},
|
|
234
|
+
]}
|
|
235
|
+
>
|
|
236
|
+
<View style={styles.categoryChipInner}>
|
|
237
|
+
{renderIcon('grid', ms(14), activeCategory === null ? colors.primaryForeground : colors.foregroundSubtle)}
|
|
238
|
+
<Text
|
|
239
|
+
style={[
|
|
240
|
+
styles.categoryChipText,
|
|
241
|
+
{ color: activeCategory === null ? colors.primaryForeground : colors.foreground },
|
|
242
|
+
]}
|
|
243
|
+
allowFontScaling={true}
|
|
244
|
+
numberOfLines={1}
|
|
245
|
+
>
|
|
246
|
+
Todos
|
|
247
|
+
</Text>
|
|
248
|
+
</View>
|
|
249
|
+
</TouchableOpacity>
|
|
250
|
+
{CURATED_ICONS.map((cat) => (
|
|
251
|
+
<TouchableOpacity
|
|
252
|
+
key={cat.name}
|
|
253
|
+
onPress={() => setActiveCategory(cat.name)}
|
|
254
|
+
activeOpacity={0.7}
|
|
255
|
+
touchSoundDisabled={true}
|
|
256
|
+
accessibilityRole="button"
|
|
257
|
+
accessibilityLabel={cat.labelEs}
|
|
258
|
+
accessibilityState={{ selected: activeCategory === cat.name }}
|
|
259
|
+
style={[
|
|
260
|
+
styles.categoryChip,
|
|
261
|
+
{
|
|
262
|
+
backgroundColor: activeCategory === cat.name ? colors.primary : colors.surface,
|
|
263
|
+
borderColor: activeCategory === cat.name ? colors.primary : colors.border,
|
|
264
|
+
},
|
|
265
|
+
]}
|
|
266
|
+
>
|
|
267
|
+
<View style={styles.categoryChipInner}>
|
|
268
|
+
{renderIcon(cat.categoryIcon, ms(14), activeCategory === cat.name ? colors.primaryForeground : colors.foregroundSubtle)}
|
|
269
|
+
<Text
|
|
270
|
+
style={[
|
|
271
|
+
styles.categoryChipText,
|
|
272
|
+
{ color: activeCategory === cat.name ? colors.primaryForeground : colors.foreground },
|
|
273
|
+
]}
|
|
274
|
+
allowFontScaling={true}
|
|
275
|
+
numberOfLines={1}
|
|
276
|
+
>
|
|
277
|
+
{cat.labelEs}
|
|
278
|
+
</Text>
|
|
279
|
+
</View>
|
|
280
|
+
</TouchableOpacity>
|
|
281
|
+
))}
|
|
282
|
+
</ScrollView>
|
|
283
|
+
|
|
284
|
+
{/* Separator */}
|
|
285
|
+
<View style={[styles.separator, { backgroundColor: colors.border }]} />
|
|
286
|
+
|
|
287
|
+
{/* Icon grid */}
|
|
288
|
+
{cellSize > 0 ? rows.map((row, i) => (
|
|
289
|
+
<View key={row[0] ?? `row-${i}`} style={[styles.row, { marginBottom: gapPx }]}>
|
|
290
|
+
{row.map((name) => (
|
|
291
|
+
<IconCellMemo
|
|
292
|
+
key={name}
|
|
293
|
+
name={name}
|
|
294
|
+
selected={value === name}
|
|
295
|
+
size={cellSize}
|
|
296
|
+
onPress={() => { handleSelect(name); sheetRef.current?.dismiss() }}
|
|
297
|
+
/>
|
|
298
|
+
))}
|
|
299
|
+
{Array.from({ length: numColumns - row.length }).map((_, j) => (
|
|
300
|
+
<View key={`spacer-${j}`} style={{ width: cellSize, height: cellSize }} />
|
|
301
|
+
))}
|
|
302
|
+
</View>
|
|
303
|
+
)) : null}
|
|
304
|
+
</>
|
|
305
|
+
)}
|
|
306
|
+
</View>
|
|
307
|
+
</BottomSheetScrollView>
|
|
308
|
+
</BottomSheetModal>
|
|
309
|
+
</View>
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const styles = StyleSheet.create({
|
|
314
|
+
triggerContainer: {
|
|
315
|
+
gap: vs(8),
|
|
316
|
+
},
|
|
317
|
+
triggerLabel: {
|
|
318
|
+
fontFamily: 'Sohne-Medium',
|
|
319
|
+
fontSize: ms(14),
|
|
320
|
+
},
|
|
321
|
+
trigger: {
|
|
322
|
+
borderRadius: RADIUS.md,
|
|
323
|
+
borderWidth: 1,
|
|
324
|
+
alignItems: 'center',
|
|
325
|
+
justifyContent: 'center',
|
|
326
|
+
},
|
|
327
|
+
triggerDisabled: {
|
|
328
|
+
opacity: 0.6,
|
|
329
|
+
},
|
|
330
|
+
helperText: {
|
|
331
|
+
fontFamily: 'Sohne-Regular',
|
|
332
|
+
fontSize: ms(13),
|
|
333
|
+
},
|
|
334
|
+
sheetBackground: {
|
|
335
|
+
borderTopLeftRadius: ms(16),
|
|
336
|
+
borderTopRightRadius: ms(16),
|
|
337
|
+
},
|
|
338
|
+
handle: {
|
|
339
|
+
width: s(36),
|
|
340
|
+
height: vs(4),
|
|
341
|
+
borderRadius: ms(2),
|
|
342
|
+
},
|
|
343
|
+
sheetContent: {
|
|
344
|
+
paddingHorizontal: s(16),
|
|
345
|
+
paddingBottom: vs(24),
|
|
346
|
+
},
|
|
347
|
+
sectionLabel: {
|
|
348
|
+
fontFamily: 'Sohne-Medium',
|
|
349
|
+
fontSize: ms(12),
|
|
350
|
+
marginBottom: vs(8),
|
|
351
|
+
textTransform: 'uppercase',
|
|
352
|
+
letterSpacing: 0.5,
|
|
353
|
+
},
|
|
354
|
+
categoryScroll: {
|
|
355
|
+
flexGrow: 0,
|
|
356
|
+
flexShrink: 0,
|
|
357
|
+
},
|
|
358
|
+
categoryStrip: {
|
|
359
|
+
gap: s(8),
|
|
360
|
+
},
|
|
361
|
+
categoryChip: {
|
|
362
|
+
borderRadius: RADIUS.full,
|
|
363
|
+
borderWidth: 1,
|
|
364
|
+
paddingVertical: vs(6),
|
|
365
|
+
paddingHorizontal: s(12),
|
|
366
|
+
},
|
|
367
|
+
categoryChipInner: {
|
|
368
|
+
flexDirection: 'row',
|
|
369
|
+
alignItems: 'center',
|
|
370
|
+
gap: s(6),
|
|
371
|
+
},
|
|
372
|
+
categoryChipText: {
|
|
373
|
+
fontFamily: 'Sohne-Medium',
|
|
374
|
+
fontSize: ms(12),
|
|
375
|
+
},
|
|
376
|
+
separator: {
|
|
377
|
+
height: StyleSheet.hairlineWidth,
|
|
378
|
+
marginVertical: vs(12),
|
|
379
|
+
},
|
|
380
|
+
gridContainer: {},
|
|
381
|
+
row: {
|
|
382
|
+
flexDirection: 'row',
|
|
383
|
+
gap: GAP,
|
|
384
|
+
},
|
|
385
|
+
cell: {
|
|
386
|
+
borderRadius: RADIUS.md,
|
|
387
|
+
alignItems: 'center',
|
|
388
|
+
justifyContent: 'center',
|
|
389
|
+
},
|
|
390
|
+
loader: {
|
|
391
|
+
minHeight: vs(200),
|
|
392
|
+
alignItems: 'center',
|
|
393
|
+
justifyContent: 'center',
|
|
394
|
+
},
|
|
395
|
+
})
|
|
@@ -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
|
-
//
|
|
54
|
-
|
|
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
|
-
|
|
61
|
+
const mod = await import('expo-image-picker/build/ExponentImagePicker')
|
|
62
|
+
picker = (mod as { default: unknown }).default
|
|
57
63
|
} catch {
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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,
|
|
@@ -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
|
-
/**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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` / `
|
|
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
|
-
|
|
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
|
|
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
|
|
126
|
+
: leftRender
|
|
128
127
|
|
|
129
|
-
const
|
|
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
|
-
{
|
|
185
|
-
|
|
186
|
-
{
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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),
|