@retray-dev/ui-kit 10.2.0 → 12.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.
- package/COMPONENTS.md +384 -40
- package/README.md +14 -5
- package/dist/Accordion.d.mts +6 -0
- package/dist/Accordion.d.ts +6 -0
- package/dist/Accordion.js +16 -0
- package/dist/Accordion.mjs +2 -2
- package/dist/AlertBanner.js +2 -0
- package/dist/AlertBanner.mjs +2 -2
- package/dist/AppHeader.js +2 -0
- package/dist/AppHeader.mjs +3 -3
- package/dist/Avatar.js +2 -0
- package/dist/Avatar.mjs +2 -2
- package/dist/Badge.js +2 -0
- package/dist/Badge.mjs +2 -2
- package/dist/Button.js +17 -17
- package/dist/Button.mjs +2 -2
- package/dist/Card.js +2 -0
- package/dist/Card.mjs +2 -2
- package/dist/CategoryStrip.js +2 -0
- package/dist/CategoryStrip.mjs +2 -2
- package/dist/Checkbox.js +2 -0
- package/dist/Checkbox.mjs +2 -2
- package/dist/Chip.js +2 -0
- package/dist/Chip.mjs +2 -2
- package/dist/ConfirmDialog.d.mts +1 -6
- package/dist/ConfirmDialog.d.ts +1 -6
- package/dist/ConfirmDialog.js +53 -41
- package/dist/ConfirmDialog.mjs +3 -3
- package/dist/CurrencyDisplay.js +2 -0
- package/dist/CurrencyDisplay.mjs +2 -2
- package/dist/CurrencyInput.d.mts +3 -8
- package/dist/CurrencyInput.d.ts +3 -8
- package/dist/CurrencyInput.js +5 -1
- package/dist/CurrencyInput.mjs +3 -3
- package/dist/DetailRow.js +2 -0
- package/dist/DetailRow.mjs +2 -2
- package/dist/EmptyState.js +17 -17
- package/dist/EmptyState.mjs +3 -3
- package/dist/ErrorBoundary.js +2 -0
- package/dist/ErrorBoundary.mjs +2 -2
- package/dist/Form.js +2 -0
- package/dist/Form.mjs +2 -2
- package/dist/IconButton.js +2 -0
- package/dist/IconButton.mjs +2 -2
- package/dist/IconPicker.js +677 -248
- package/dist/IconPicker.mjs +3 -2
- package/dist/ImageUpload.d.mts +3 -1
- package/dist/ImageUpload.d.ts +3 -1
- package/dist/ImageUpload.js +10 -3
- package/dist/ImageUpload.mjs +3 -3
- package/dist/ImageViewer.js +2 -0
- package/dist/ImageViewer.mjs +4 -4
- package/dist/Input.js +2 -0
- package/dist/Input.mjs +2 -2
- package/dist/LabelValue.js +2 -0
- package/dist/LabelValue.mjs +2 -2
- package/dist/ListGroup.js +2 -0
- package/dist/ListGroup.mjs +2 -2
- package/dist/ListItem.d.mts +7 -7
- package/dist/ListItem.d.ts +7 -7
- package/dist/ListItem.js +14 -7
- package/dist/ListItem.mjs +2 -2
- package/dist/MediaCard.js +2 -0
- package/dist/MediaCard.mjs +2 -2
- package/dist/MenuGroup.js +2 -0
- package/dist/MenuGroup.mjs +2 -2
- package/dist/MenuItem.js +2 -0
- package/dist/MenuItem.mjs +2 -2
- package/dist/MonthPicker.js +2 -0
- package/dist/MonthPicker.mjs +2 -2
- package/dist/NumberStepper.js +2 -0
- package/dist/NumberStepper.mjs +2 -2
- package/dist/PagerDots.js +2 -0
- package/dist/PagerDots.mjs +2 -2
- 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 +17 -17
- package/dist/PricingCard.mjs +4 -4
- package/dist/Progress.js +2 -0
- package/dist/Progress.mjs +2 -2
- package/dist/RadioGroup.js +2 -0
- package/dist/RadioGroup.mjs +2 -2
- package/dist/RetrayProvider.d.mts +1 -1
- package/dist/RetrayProvider.d.ts +1 -1
- package/dist/RetrayProvider.js +2 -0
- package/dist/RetrayProvider.mjs +3 -3
- package/dist/Select.js +2 -0
- package/dist/Select.mjs +2 -2
- package/dist/SelectableCard.d.mts +27 -0
- package/dist/SelectableCard.d.ts +27 -0
- package/dist/SelectableCard.js +511 -0
- package/dist/SelectableCard.mjs +8 -0
- package/dist/SelectableGrid.js +2 -0
- package/dist/SelectableGrid.mjs +2 -2
- package/dist/Separator.js +2 -0
- package/dist/Separator.mjs +2 -2
- package/dist/Sheet.d.mts +4 -46
- package/dist/Sheet.d.ts +4 -46
- package/dist/Sheet.js +55 -115
- package/dist/Sheet.mjs +2 -3
- package/dist/SheetSelect.js +2 -0
- package/dist/SheetSelect.mjs +2 -2
- package/dist/Skeleton.d.mts +3 -1
- package/dist/Skeleton.d.ts +3 -1
- package/dist/Skeleton.js +5 -2
- package/dist/Skeleton.mjs +2 -2
- package/dist/Slider.js +2 -0
- package/dist/Slider.mjs +2 -2
- package/dist/Spinner.js +2 -0
- package/dist/Spinner.mjs +2 -2
- package/dist/Stats.d.mts +33 -0
- package/dist/Stats.d.ts +33 -0
- package/dist/Stats.js +453 -0
- package/dist/Stats.mjs +9 -0
- package/dist/Switch.js +2 -0
- package/dist/Switch.mjs +2 -2
- package/dist/TabBar.js +2 -0
- package/dist/TabBar.mjs +2 -2
- package/dist/Tabs.js +2 -0
- package/dist/Tabs.mjs +2 -2
- package/dist/Text.d.mts +3 -1
- package/dist/Text.d.ts +3 -1
- package/dist/Text.js +5 -3
- package/dist/Text.mjs +2 -2
- package/dist/Textarea.js +2 -0
- package/dist/Textarea.mjs +2 -2
- package/dist/Toast.js +2 -0
- package/dist/Toast.mjs +2 -2
- package/dist/Toggle.js +2 -0
- package/dist/Toggle.mjs +2 -2
- package/dist/{chunk-U2XJFYED.mjs → chunk-2BA3JMKK.mjs} +1 -1
- package/dist/{chunk-NMU5FMQJ.mjs → chunk-2HFD4IHU.mjs} +4 -2
- package/dist/{chunk-S2R7UVOE.mjs → chunk-2LG326TT.mjs} +1 -1
- package/dist/chunk-2P2CB235.mjs +236 -0
- package/dist/{chunk-6L4G6PBT.mjs → chunk-3XCFYSX4.mjs} +1 -1
- package/dist/{chunk-HTHGSXFG.mjs → chunk-4J2PXL36.mjs} +16 -18
- package/dist/{chunk-BEMIQXXU.mjs → chunk-4OORJ2DY.mjs} +1 -1
- package/dist/chunk-4XOB5TTD.mjs +166 -0
- package/dist/{chunk-FCSSQK3L.mjs → chunk-57V2LXCK.mjs} +1 -1
- package/dist/{chunk-6Q64UFIA.mjs → chunk-7AFZWSCI.mjs} +1 -1
- package/dist/{chunk-IX3NYLYQ.mjs → chunk-7ELGZ66G.mjs} +1 -1
- package/dist/{chunk-GD6KXMG5.mjs → chunk-AENAVIKT.mjs} +1 -1
- package/dist/{chunk-ID72TK46.mjs → chunk-BXF4AMHY.mjs} +1 -1
- package/dist/{chunk-SOA2Z4RB.mjs → chunk-C43HRKXH.mjs} +1 -1
- package/dist/{chunk-TZDGAP5N.mjs → chunk-CF27NBXO.mjs} +11 -6
- package/dist/{chunk-SXLKNTA4.mjs → chunk-DF7JA72E.mjs} +1 -1
- package/dist/{chunk-AJRVDP2H.mjs → chunk-E5UKLSJZ.mjs} +3 -3
- package/dist/{chunk-MBMXYJJV.mjs → chunk-E7NEHHXV.mjs} +7 -3
- package/dist/{chunk-VKID2D2I.mjs → chunk-EDLCGYIO.mjs} +13 -8
- package/dist/{chunk-BUMAMSTZ.mjs → chunk-ELGEOM7I.mjs} +1 -1
- package/dist/{chunk-DYT7BG5I.mjs → chunk-F3YTWO3T.mjs} +1 -1
- package/dist/{chunk-VF2ATYN3.mjs → chunk-GH67YXG6.mjs} +1 -1
- package/dist/{chunk-WJLKJMKR.mjs → chunk-GUTDFUNF.mjs} +4 -4
- package/dist/{chunk-6SECQ2ZF.mjs → chunk-HC4VVCWY.mjs} +2 -2
- package/dist/{chunk-A3A6KNQN.mjs → chunk-HEDQPK4I.mjs} +1 -1
- package/dist/{chunk-GQYFLP3D.mjs → chunk-IVSRW4HS.mjs} +1 -1
- package/dist/{chunk-KOO4WITD.mjs → chunk-KSUWPU2F.mjs} +1 -1
- package/dist/{chunk-WBOOUHSS.mjs → chunk-LIS6I5UP.mjs} +1 -1
- package/dist/{chunk-X4G6APW6.mjs → chunk-LNPKGWBG.mjs} +1 -1
- package/dist/{chunk-T2KCAHOS.mjs → chunk-LOBLCFMN.mjs} +1 -1
- package/dist/{chunk-ELXBDILQ.mjs → chunk-LPV4NJJK.mjs} +2 -2
- package/dist/{chunk-Y2NS74WS.mjs → chunk-M3C7XM2M.mjs} +53 -99
- package/dist/{chunk-BRKYVJVV.mjs → chunk-MEPSKGBO.mjs} +1 -1
- package/dist/{chunk-TBNZHU6C.mjs → chunk-MVMGPZN6.mjs} +2 -2
- package/dist/{chunk-YJ7I257J.mjs → chunk-NHDI3VQB.mjs} +15 -1
- package/dist/{chunk-Z6SFHN6T.mjs → chunk-NJG7DHVF.mjs} +1 -1
- package/dist/{chunk-RYZC432S.mjs → chunk-NLZY4TXU.mjs} +1 -1
- package/dist/{chunk-ZZ2R6KZ3.mjs → chunk-OLVJFKXS.mjs} +1 -1
- package/dist/{chunk-AJ7ZDNBT.mjs → chunk-QDAZGZUF.mjs} +4 -3
- package/dist/{chunk-JT7HKXRB.mjs → chunk-QOLWA2PW.mjs} +1 -1
- package/dist/{chunk-WYEUNUTP.mjs → chunk-QXDGGOLC.mjs} +38 -25
- package/dist/{chunk-JMOZEC77.mjs → chunk-RJNLAH76.mjs} +1 -1
- package/dist/{chunk-WF2XDFRK.mjs → chunk-RMRS44MQ.mjs} +1 -1
- package/dist/chunk-SAWUXP3A.mjs +1114 -0
- package/dist/{chunk-OB4JUQ3O.mjs → chunk-TS7DGUIR.mjs} +1 -1
- package/dist/{chunk-AV4EMIRH.mjs → chunk-UBUXUMER.mjs} +1 -1
- package/dist/{chunk-IRRY3CRZ.mjs → chunk-ULGNQPNE.mjs} +1 -1
- package/dist/{chunk-7LWRKMF5.mjs → chunk-UNNRUJTM.mjs} +1 -1
- package/dist/{chunk-TB6SD2FT.mjs → chunk-UQ4742ET.mjs} +1 -1
- package/dist/{chunk-MX6HRKMI.mjs → chunk-VJBUCITV.mjs} +1 -1
- package/dist/{chunk-2UYENBLV.mjs → chunk-YMYIEVZP.mjs} +1 -1
- package/dist/{chunk-SOYNZDVY.mjs → chunk-YTXRIXNZ.mjs} +8 -1
- package/dist/{chunk-YFZ3ELX5.mjs → chunk-ZIMY2QUM.mjs} +2 -2
- package/dist/{chunk-Z4VHZ7B5.mjs → chunk-ZR6HSEAB.mjs} +1 -1
- 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-wt-orHUi.d.ts → index-CY34hxPN.d.mts} +1 -0
- package/dist/{index-wt-orHUi.d.mts → index-CY34hxPN.d.ts} +1 -0
- package/dist/index.d.mts +7 -3
- package/dist/index.d.ts +7 -3
- package/dist/index.js +1517 -761
- package/dist/index.mjs +54 -52
- package/package.json +3 -3
- package/src/components/Accordion/Accordion.tsx +20 -0
- package/src/components/Button/Button.tsx +29 -26
- package/src/components/ConfirmDialog/ConfirmDialog.tsx +47 -31
- package/src/components/CurrencyInput/CurrencyInput.tsx +4 -7
- package/src/components/IconPicker/IconPicker.tsx +124 -112
- package/src/components/ImageUpload/ImageUpload.tsx +10 -3
- package/src/components/ListItem/ListItem.tsx +43 -28
- package/src/components/Pressable/Pressable.tsx +20 -8
- package/src/components/SelectableCard/SelectableCard.tsx +304 -0
- package/src/components/SelectableCard/index.ts +1 -0
- package/src/components/Sheet/Sheet.tsx +72 -173
- package/src/components/Skeleton/Skeleton.tsx +5 -2
- package/src/components/Stats/Stats.tsx +254 -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 +5 -0
- package/src/theme/colorUtils.ts +9 -0
- package/src/theme/colors.ts +7 -0
- package/src/theme/types.ts +4 -1
- package/src/utils/curatedIcons.ts +698 -135
- package/src/utils/fontGuard.ts +2 -1
- package/dist/chunk-53Z3NYGE.mjs +0 -742
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import React, { createContext, useContext } from 'react'
|
|
2
|
+
import { View, Text, StyleSheet, ViewStyle, Pressable } from 'react-native'
|
|
3
|
+
import { EaseView } from 'react-native-ease'
|
|
4
|
+
import { impactLight } from '../../utils/haptics'
|
|
5
|
+
import { useTheme } from '../../theme'
|
|
6
|
+
import { s, vs, ms, mvs } from '../../utils/scaling'
|
|
7
|
+
import { RADIUS } from '../../tokens'
|
|
8
|
+
import { renderIcon } from '../../utils/icons'
|
|
9
|
+
import { COLOR_TRANSITION, OPACITY_TRANSITION, SPRING_ELASTIC } from '../../utils/animations'
|
|
10
|
+
|
|
11
|
+
type SelectType = 'radio' | 'checkbox'
|
|
12
|
+
type CardVariant = 'elevated' | 'outlined' | 'filled'
|
|
13
|
+
|
|
14
|
+
interface SelectableCardContextValue {
|
|
15
|
+
type: SelectType
|
|
16
|
+
value: string | string[]
|
|
17
|
+
onValueChange: (value: string | string[]) => void
|
|
18
|
+
variant: CardVariant
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const SelectableCardContext = createContext<SelectableCardContextValue | null>(null)
|
|
22
|
+
|
|
23
|
+
// ─── Group ────────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
export interface SelectableCardGroupProps {
|
|
26
|
+
type: SelectType
|
|
27
|
+
value: string | string[]
|
|
28
|
+
onValueChange: (value: string | string[]) => void
|
|
29
|
+
variant?: CardVariant
|
|
30
|
+
gap?: number
|
|
31
|
+
style?: ViewStyle
|
|
32
|
+
children: React.ReactNode
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function SelectableCardGroup({
|
|
36
|
+
type,
|
|
37
|
+
value,
|
|
38
|
+
onValueChange,
|
|
39
|
+
variant = 'elevated',
|
|
40
|
+
gap = s(8),
|
|
41
|
+
style,
|
|
42
|
+
children,
|
|
43
|
+
}: SelectableCardGroupProps) {
|
|
44
|
+
return (
|
|
45
|
+
<SelectableCardContext.Provider value={{ type, value, onValueChange, variant }}>
|
|
46
|
+
<View style={[styles.group, { gap }, style]}>
|
|
47
|
+
{children}
|
|
48
|
+
</View>
|
|
49
|
+
</SelectableCardContext.Provider>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ─── Card ─────────────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
export interface SelectableCardProps {
|
|
56
|
+
value: string
|
|
57
|
+
title: string
|
|
58
|
+
description?: string
|
|
59
|
+
iconName?: string
|
|
60
|
+
icon?: React.ReactNode
|
|
61
|
+
disabled?: boolean
|
|
62
|
+
style?: ViewStyle
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function SelectableCard({
|
|
66
|
+
value,
|
|
67
|
+
title,
|
|
68
|
+
description,
|
|
69
|
+
iconName,
|
|
70
|
+
icon,
|
|
71
|
+
disabled = false,
|
|
72
|
+
style,
|
|
73
|
+
}: SelectableCardProps) {
|
|
74
|
+
const ctx = useContext(SelectableCardContext)
|
|
75
|
+
if (!ctx) {
|
|
76
|
+
throw new Error('SelectableCard must be used inside <SelectableCard.Group>')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const { colors } = useTheme()
|
|
80
|
+
const { type, value: selectedValue, onValueChange, variant } = ctx
|
|
81
|
+
|
|
82
|
+
const isSelected =
|
|
83
|
+
type === 'radio'
|
|
84
|
+
? selectedValue === value
|
|
85
|
+
: Array.isArray(selectedValue) && selectedValue.includes(value)
|
|
86
|
+
|
|
87
|
+
const handlePress = () => {
|
|
88
|
+
if (disabled) return
|
|
89
|
+
impactLight()
|
|
90
|
+
if (type === 'radio') {
|
|
91
|
+
onValueChange(value)
|
|
92
|
+
} else {
|
|
93
|
+
const arr = Array.isArray(selectedValue) ? selectedValue : []
|
|
94
|
+
if (arr.includes(value)) {
|
|
95
|
+
onValueChange(arr.filter((v) => v !== value))
|
|
96
|
+
} else {
|
|
97
|
+
onValueChange([...arr, value])
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const variantStyle: ViewStyle = (() => {
|
|
103
|
+
const borderWidth = 2 // always 2 — no layout shift on select
|
|
104
|
+
|
|
105
|
+
const base = {
|
|
106
|
+
elevated: {
|
|
107
|
+
backgroundColor: colors.card,
|
|
108
|
+
borderWidth,
|
|
109
|
+
borderColor: 'transparent', // reserve space for selected border
|
|
110
|
+
},
|
|
111
|
+
outlined: {
|
|
112
|
+
backgroundColor: colors.card,
|
|
113
|
+
borderWidth,
|
|
114
|
+
borderColor: colors.border,
|
|
115
|
+
},
|
|
116
|
+
filled: {
|
|
117
|
+
backgroundColor: colors.surfaceStrong,
|
|
118
|
+
borderWidth,
|
|
119
|
+
borderColor: colors.border,
|
|
120
|
+
},
|
|
121
|
+
}[variant]
|
|
122
|
+
|
|
123
|
+
if (isSelected && !disabled) {
|
|
124
|
+
return {
|
|
125
|
+
...base,
|
|
126
|
+
borderColor: colors.primary,
|
|
127
|
+
shadowColor: 'transparent',
|
|
128
|
+
shadowOpacity: 0,
|
|
129
|
+
shadowRadius: 0,
|
|
130
|
+
elevation: 0,
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (disabled) {
|
|
135
|
+
return {
|
|
136
|
+
...base,
|
|
137
|
+
shadowColor: 'transparent',
|
|
138
|
+
shadowOpacity: 0,
|
|
139
|
+
shadowRadius: 0,
|
|
140
|
+
elevation: 0,
|
|
141
|
+
borderColor: colors.border,
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return base
|
|
146
|
+
})()
|
|
147
|
+
|
|
148
|
+
const resolvedIcon = iconName
|
|
149
|
+
? renderIcon(iconName, ms(22), disabled ? colors.foregroundMuted : colors.foregroundMuted)
|
|
150
|
+
: icon
|
|
151
|
+
|
|
152
|
+
const resolvedIconElement = resolvedIcon ? (
|
|
153
|
+
<View style={[styles.iconWrapper, disabled && { opacity: 0.45 }]}>{resolvedIcon}</View>
|
|
154
|
+
) : null
|
|
155
|
+
|
|
156
|
+
const selectorAccessibilityRole = type === 'radio' ? 'radio' : 'checkbox'
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<Pressable
|
|
160
|
+
onPress={handlePress}
|
|
161
|
+
disabled={disabled}
|
|
162
|
+
accessibilityRole="button"
|
|
163
|
+
accessibilityLabel={`${title}${description ? `, ${description}` : ''}`}
|
|
164
|
+
accessibilityState={{ selected: isSelected, disabled }}
|
|
165
|
+
style={[
|
|
166
|
+
styles.card,
|
|
167
|
+
variantStyle,
|
|
168
|
+
isSelected && !disabled && styles.cardSelected,
|
|
169
|
+
style,
|
|
170
|
+
]}
|
|
171
|
+
>
|
|
172
|
+
<View style={styles.row}>
|
|
173
|
+
{/* Selection indicator */}
|
|
174
|
+
<View style={styles.selectorContainer} accessibilityRole={selectorAccessibilityRole} accessibilityState={{ selected: isSelected, disabled }}>
|
|
175
|
+
{type === 'radio' ? (
|
|
176
|
+
<EaseView
|
|
177
|
+
style={styles.radioCircle}
|
|
178
|
+
animate={{ borderColor: !disabled && isSelected ? colors.primary : colors.border }}
|
|
179
|
+
transition={COLOR_TRANSITION}
|
|
180
|
+
>
|
|
181
|
+
<EaseView
|
|
182
|
+
animate={{
|
|
183
|
+
scale: !disabled && isSelected ? 1 : 0,
|
|
184
|
+
opacity: !disabled && isSelected ? 1 : 0,
|
|
185
|
+
}}
|
|
186
|
+
transition={SPRING_ELASTIC}
|
|
187
|
+
>
|
|
188
|
+
<View style={[styles.radioDot, { backgroundColor: colors.primary }]} />
|
|
189
|
+
</EaseView>
|
|
190
|
+
</EaseView>
|
|
191
|
+
) : (
|
|
192
|
+
<EaseView
|
|
193
|
+
style={styles.checkboxBox}
|
|
194
|
+
animate={{
|
|
195
|
+
borderColor: !disabled && isSelected ? colors.primary : colors.border,
|
|
196
|
+
backgroundColor: !disabled && isSelected ? colors.primary : 'transparent',
|
|
197
|
+
}}
|
|
198
|
+
transition={COLOR_TRANSITION}
|
|
199
|
+
>
|
|
200
|
+
<EaseView
|
|
201
|
+
animate={{ opacity: !disabled && isSelected ? 1 : 0 }}
|
|
202
|
+
transition={OPACITY_TRANSITION}
|
|
203
|
+
>
|
|
204
|
+
<View style={[styles.checkmark, { borderColor: colors.primaryForeground }]} />
|
|
205
|
+
</EaseView>
|
|
206
|
+
</EaseView>
|
|
207
|
+
)}
|
|
208
|
+
</View>
|
|
209
|
+
|
|
210
|
+
{/* Optional icon */}
|
|
211
|
+
{resolvedIconElement}
|
|
212
|
+
|
|
213
|
+
{/* Title + description */}
|
|
214
|
+
<View style={styles.textArea}>
|
|
215
|
+
<Text
|
|
216
|
+
style={[styles.title, { color: disabled ? colors.foregroundMuted : colors.foreground }]}
|
|
217
|
+
allowFontScaling={true}
|
|
218
|
+
numberOfLines={2}
|
|
219
|
+
>
|
|
220
|
+
{title}
|
|
221
|
+
</Text>
|
|
222
|
+
{description ? (
|
|
223
|
+
<Text
|
|
224
|
+
style={[styles.description, { color: disabled ? colors.foregroundMuted : colors.foregroundSubtle }]}
|
|
225
|
+
allowFontScaling={true}
|
|
226
|
+
numberOfLines={4}
|
|
227
|
+
>
|
|
228
|
+
{description}
|
|
229
|
+
</Text>
|
|
230
|
+
) : null}
|
|
231
|
+
</View>
|
|
232
|
+
</View>
|
|
233
|
+
</Pressable>
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ─── Styles ───────────────────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
const styles = StyleSheet.create({
|
|
240
|
+
group: {
|
|
241
|
+
width: '100%',
|
|
242
|
+
},
|
|
243
|
+
card: {
|
|
244
|
+
borderRadius: RADIUS.md,
|
|
245
|
+
borderWidth: 1,
|
|
246
|
+
},
|
|
247
|
+
cardSelected: {
|
|
248
|
+
backgroundColor: undefined, // overridden by variantStyle
|
|
249
|
+
},
|
|
250
|
+
row: {
|
|
251
|
+
flexDirection: 'row',
|
|
252
|
+
alignItems: 'flex-start',
|
|
253
|
+
padding: s(16),
|
|
254
|
+
gap: s(12),
|
|
255
|
+
},
|
|
256
|
+
selectorContainer: {
|
|
257
|
+
paddingTop: vs(1),
|
|
258
|
+
},
|
|
259
|
+
radioCircle: {
|
|
260
|
+
width: s(24),
|
|
261
|
+
height: s(24),
|
|
262
|
+
borderRadius: s(12),
|
|
263
|
+
borderWidth: 2,
|
|
264
|
+
alignItems: 'center',
|
|
265
|
+
justifyContent: 'center',
|
|
266
|
+
},
|
|
267
|
+
radioDot: {
|
|
268
|
+
width: s(10),
|
|
269
|
+
height: s(10),
|
|
270
|
+
borderRadius: s(5),
|
|
271
|
+
},
|
|
272
|
+
checkboxBox: {
|
|
273
|
+
width: s(24),
|
|
274
|
+
height: s(24),
|
|
275
|
+
borderRadius: ms(4),
|
|
276
|
+
borderWidth: 2,
|
|
277
|
+
alignItems: 'center',
|
|
278
|
+
justifyContent: 'center',
|
|
279
|
+
},
|
|
280
|
+
checkmark: {
|
|
281
|
+
width: s(12),
|
|
282
|
+
height: vs(7),
|
|
283
|
+
borderLeftWidth: 2,
|
|
284
|
+
borderBottomWidth: 2,
|
|
285
|
+
transform: [{ rotate: '-45deg' }, { translateY: -1 }],
|
|
286
|
+
},
|
|
287
|
+
iconWrapper: {
|
|
288
|
+
paddingTop: vs(1),
|
|
289
|
+
},
|
|
290
|
+
textArea: {
|
|
291
|
+
flex: 1,
|
|
292
|
+
gap: vs(4),
|
|
293
|
+
},
|
|
294
|
+
title: {
|
|
295
|
+
fontFamily: 'Sohne-SemiBold',
|
|
296
|
+
fontSize: ms(16),
|
|
297
|
+
lineHeight: mvs(22),
|
|
298
|
+
},
|
|
299
|
+
description: {
|
|
300
|
+
fontFamily: 'Sohne-Regular',
|
|
301
|
+
fontSize: ms(13),
|
|
302
|
+
lineHeight: mvs(18),
|
|
303
|
+
},
|
|
304
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './SelectableCard'
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useRef } from 'react'
|
|
2
|
-
import { View, Text, TouchableOpacity, StyleSheet, ViewStyle
|
|
3
|
-
import
|
|
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).
|
|
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
|
|
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,80 @@ 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
|
|
151
|
-
const
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
100
|
+
const ref = useRef<BottomSheetModal>(null)
|
|
101
|
+
const wasOpened = useRef(false)
|
|
102
|
+
const isPresentedRef = useRef(false)
|
|
103
|
+
const name = useId()
|
|
104
|
+
|
|
105
|
+
const handleDismiss = useCallback(() => {
|
|
106
|
+
isPresentedRef.current = false
|
|
107
|
+
onClose?.()
|
|
108
|
+
}, [onClose])
|
|
157
109
|
|
|
158
110
|
useEffect(() => {
|
|
159
|
-
if (open) {
|
|
111
|
+
if (open && !isPresentedRef.current) {
|
|
160
112
|
impactMedium()
|
|
161
|
-
ref.current?.
|
|
162
|
-
|
|
163
|
-
|
|
113
|
+
ref.current?.present()
|
|
114
|
+
wasOpened.current = true
|
|
115
|
+
isPresentedRef.current = true
|
|
116
|
+
} else if (!open && wasOpened.current && isPresentedRef.current) {
|
|
117
|
+
ref.current?.dismiss()
|
|
164
118
|
}
|
|
165
119
|
}, [open])
|
|
166
120
|
|
|
167
|
-
const renderBackdrop = useCallback(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
121
|
+
const renderBackdrop = useCallback(
|
|
122
|
+
(props: BottomSheetBackdropProps) => (
|
|
123
|
+
<BottomSheetBackdrop
|
|
124
|
+
{...props}
|
|
125
|
+
disappearsOnIndex={-1}
|
|
126
|
+
appearsOnIndex={0}
|
|
127
|
+
pressBehavior="close"
|
|
128
|
+
/>
|
|
129
|
+
),
|
|
130
|
+
[]
|
|
131
|
+
)
|
|
175
132
|
|
|
176
|
-
// Detect compound components in children
|
|
177
133
|
const childArray = React.Children.toArray(children)
|
|
178
134
|
const customHeader = childArray.find((child) => React.isValidElement(child) && child.type === SheetHeader)
|
|
179
135
|
const customContent = childArray.find((child) => React.isValidElement(child) && child.type === SheetContent)
|
|
180
136
|
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
137
|
|
|
191
|
-
const
|
|
192
|
-
|
|
138
|
+
const filteredChildren =
|
|
139
|
+
customHeader || customContent || customFooter
|
|
140
|
+
? childArray.filter(
|
|
141
|
+
(child) =>
|
|
142
|
+
!React.isValidElement(child) ||
|
|
143
|
+
(child.type !== SheetHeader && child.type !== SheetContent && child.type !== SheetFooter)
|
|
144
|
+
)
|
|
145
|
+
: children
|
|
193
146
|
|
|
194
|
-
const
|
|
147
|
+
const showHeader = !!(title || subtitle || showCloseButton) && !customHeader
|
|
148
|
+
|
|
149
|
+
const headerNode = customHeader ? customHeader : showHeader ? (
|
|
195
150
|
<View style={[styles.header, { backgroundColor: colors.card }]} accessibilityRole="header">
|
|
196
151
|
<View style={styles.headerRow}>
|
|
197
152
|
{title ? (
|
|
198
153
|
<Text style={[styles.title, { color: colors.foreground }]} allowFontScaling={true}>
|
|
199
154
|
{title}
|
|
200
155
|
</Text>
|
|
201
|
-
) :
|
|
156
|
+
) : (
|
|
157
|
+
<View style={{ flex: 1 }} />
|
|
158
|
+
)}
|
|
202
159
|
{showCloseButton ? (
|
|
203
160
|
<TouchableOpacity
|
|
204
161
|
onPress={onClose}
|
|
@@ -213,90 +170,52 @@ export function Sheet({
|
|
|
213
170
|
</TouchableOpacity>
|
|
214
171
|
) : null}
|
|
215
172
|
</View>
|
|
216
|
-
{
|
|
173
|
+
{subtitle ? (
|
|
217
174
|
<Text style={[styles.subtitle, { color: colors.foregroundMuted }]} allowFontScaling={true}>
|
|
218
|
-
{
|
|
175
|
+
{subtitle}
|
|
219
176
|
</Text>
|
|
220
177
|
) : null}
|
|
221
178
|
</View>
|
|
222
|
-
) : null
|
|
179
|
+
) : null
|
|
223
180
|
|
|
224
181
|
const contentNode = customContent ? customContent : filteredChildren
|
|
225
182
|
const effectiveFooter = customFooter ? customFooter : footer
|
|
226
183
|
|
|
227
|
-
const renderFooter = useCallback(
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
<BottomSheetFooter {...props}>
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
}
|
|
184
|
+
const renderFooter = useCallback(
|
|
185
|
+
(props: BottomSheetFooterProps) => {
|
|
186
|
+
if (!effectiveFooter) return null
|
|
187
|
+
return <BottomSheetFooter {...props}>{effectiveFooter}</BottomSheetFooter>
|
|
188
|
+
},
|
|
189
|
+
[effectiveFooter]
|
|
190
|
+
)
|
|
264
191
|
|
|
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
192
|
const useDynamicSizing = !snapPoints
|
|
270
|
-
|
|
193
|
+
|
|
271
194
|
return (
|
|
272
|
-
|
|
195
|
+
<BottomSheetModal
|
|
273
196
|
ref={ref}
|
|
274
|
-
|
|
275
|
-
|
|
197
|
+
name={name}
|
|
198
|
+
onDismiss={handleDismiss}
|
|
276
199
|
enableDynamicSizing={useDynamicSizing}
|
|
277
200
|
snapPoints={snapPoints}
|
|
278
|
-
maxDynamicContentSize={useDynamicSizing ?
|
|
201
|
+
maxDynamicContentSize={useDynamicSizing && maxHeight ? maxHeight : undefined}
|
|
279
202
|
backdropComponent={renderBackdrop}
|
|
280
203
|
footerComponent={effectiveFooter ? renderFooter : undefined}
|
|
281
|
-
backgroundStyle={
|
|
282
|
-
handleIndicatorStyle={
|
|
204
|
+
backgroundStyle={{ ...styles.background, backgroundColor: colors.card }}
|
|
205
|
+
handleIndicatorStyle={{ ...styles.handle, backgroundColor: colors.border }}
|
|
283
206
|
enablePanDownToClose
|
|
284
207
|
topInset={insets.top}
|
|
285
|
-
keyboardBehavior={
|
|
208
|
+
keyboardBehavior={keyboardBehavior}
|
|
286
209
|
keyboardBlurBehavior={keyboardBlurBehavior}
|
|
287
210
|
android_keyboardInputMode={android_keyboardInputMode}
|
|
288
211
|
enableBlurKeyboardOnGesture={enableBlurKeyboardOnGesture}
|
|
289
212
|
>
|
|
290
|
-
{
|
|
213
|
+
{scrollable ? (
|
|
291
214
|
<BottomSheetScrollView
|
|
292
|
-
contentContainerStyle={[
|
|
293
|
-
styles.scrollContent,
|
|
294
|
-
style,
|
|
295
|
-
]}
|
|
215
|
+
contentContainerStyle={[styles.scrollContent, style]}
|
|
296
216
|
style={contentStyle}
|
|
297
|
-
showsVerticalScrollIndicator
|
|
298
|
-
|
|
299
|
-
persistentScrollbar={isAndroid}
|
|
217
|
+
showsVerticalScrollIndicator
|
|
218
|
+
bounces={false}
|
|
300
219
|
stickyHeaderIndices={headerNode ? [0] : undefined}
|
|
301
220
|
>
|
|
302
221
|
{headerNode}
|
|
@@ -308,7 +227,7 @@ export function Sheet({
|
|
|
308
227
|
{contentNode}
|
|
309
228
|
</BottomSheetView>
|
|
310
229
|
)}
|
|
311
|
-
</
|
|
230
|
+
</BottomSheetModal>
|
|
312
231
|
)
|
|
313
232
|
}
|
|
314
233
|
|
|
@@ -358,7 +277,6 @@ const styles = StyleSheet.create({
|
|
|
358
277
|
scrollContent: {
|
|
359
278
|
paddingHorizontal: s(16),
|
|
360
279
|
paddingBottom: vs(32),
|
|
361
|
-
paddingRight: s(16),
|
|
362
280
|
},
|
|
363
281
|
sheetContent: {
|
|
364
282
|
gap: vs(16),
|
|
@@ -370,23 +288,4 @@ const styles = StyleSheet.create({
|
|
|
370
288
|
flexDirection: 'row',
|
|
371
289
|
gap: s(12),
|
|
372
290
|
},
|
|
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
291
|
})
|
|
392
|
-
|