@oxyhq/bloom 0.1.13 → 0.1.15
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/lib/commonjs/accordion/Accordion.js +230 -0
- package/lib/commonjs/accordion/Accordion.js.map +1 -0
- package/lib/commonjs/accordion/index.js +31 -0
- package/lib/commonjs/accordion/index.js.map +1 -0
- package/lib/commonjs/accordion/types.js +6 -0
- package/lib/commonjs/accordion/types.js.map +1 -0
- package/lib/commonjs/badge/Badge.js +173 -0
- package/lib/commonjs/badge/Badge.js.map +1 -0
- package/lib/commonjs/badge/index.js +13 -0
- package/lib/commonjs/badge/index.js.map +1 -0
- package/lib/commonjs/badge/types.js +6 -0
- package/lib/commonjs/badge/types.js.map +1 -0
- package/lib/commonjs/bottom-sheet/index.js +40 -21
- package/lib/commonjs/bottom-sheet/index.js.map +1 -1
- package/lib/commonjs/card/Card.js +165 -0
- package/lib/commonjs/card/Card.js.map +1 -0
- package/lib/commonjs/card/index.js +43 -0
- package/lib/commonjs/card/index.js.map +1 -0
- package/lib/commonjs/card/types.js +6 -0
- package/lib/commonjs/card/types.js.map +1 -0
- package/lib/commonjs/checkbox/Checkbox.js +177 -0
- package/lib/commonjs/checkbox/Checkbox.js.map +1 -0
- package/lib/commonjs/checkbox/index.js +13 -0
- package/lib/commonjs/checkbox/index.js.map +1 -0
- package/lib/commonjs/checkbox/types.js +6 -0
- package/lib/commonjs/checkbox/types.js.map +1 -0
- package/lib/commonjs/chip/Chip.js +180 -0
- package/lib/commonjs/chip/Chip.js.map +1 -0
- package/lib/commonjs/chip/index.js +13 -0
- package/lib/commonjs/chip/index.js.map +1 -0
- package/lib/commonjs/chip/types.js +6 -0
- package/lib/commonjs/chip/types.js.map +1 -0
- package/lib/commonjs/index.js +56 -2
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/tabs/Tabs.js +202 -0
- package/lib/commonjs/tabs/Tabs.js.map +1 -0
- package/lib/commonjs/tabs/index.js +25 -0
- package/lib/commonjs/tabs/index.js.map +1 -0
- package/lib/commonjs/tabs/types.js +6 -0
- package/lib/commonjs/tabs/types.js.map +1 -0
- package/lib/module/accordion/Accordion.js +225 -0
- package/lib/module/accordion/Accordion.js.map +1 -0
- package/lib/module/accordion/index.js +4 -0
- package/lib/module/accordion/index.js.map +1 -0
- package/lib/module/accordion/types.js +4 -0
- package/lib/module/accordion/types.js.map +1 -0
- package/lib/module/badge/Badge.js +168 -0
- package/lib/module/badge/Badge.js.map +1 -0
- package/lib/module/badge/index.js +4 -0
- package/lib/module/badge/index.js.map +1 -0
- package/lib/module/badge/types.js +4 -0
- package/lib/module/badge/types.js.map +1 -0
- package/lib/module/bottom-sheet/index.js +40 -21
- package/lib/module/bottom-sheet/index.js.map +1 -1
- package/lib/module/card/Card.js +160 -0
- package/lib/module/card/Card.js.map +1 -0
- package/lib/module/card/index.js +4 -0
- package/lib/module/card/index.js.map +1 -0
- package/lib/module/card/types.js +4 -0
- package/lib/module/card/types.js.map +1 -0
- package/lib/module/checkbox/Checkbox.js +172 -0
- package/lib/module/checkbox/Checkbox.js.map +1 -0
- package/lib/module/checkbox/index.js +4 -0
- package/lib/module/checkbox/index.js.map +1 -0
- package/lib/module/checkbox/types.js +4 -0
- package/lib/module/checkbox/types.js.map +1 -0
- package/lib/module/chip/Chip.js +175 -0
- package/lib/module/chip/Chip.js.map +1 -0
- package/lib/module/chip/index.js +4 -0
- package/lib/module/chip/index.js.map +1 -0
- package/lib/module/chip/types.js +4 -0
- package/lib/module/chip/types.js.map +1 -0
- package/lib/module/index.js +8 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/tabs/Tabs.js +197 -0
- package/lib/module/tabs/Tabs.js.map +1 -0
- package/lib/module/tabs/index.js +4 -0
- package/lib/module/tabs/index.js.map +1 -0
- package/lib/module/tabs/types.js +4 -0
- package/lib/module/tabs/types.js.map +1 -0
- package/lib/typescript/commonjs/__tests__/BloomThemeProvider.test.d.ts +2 -0
- package/lib/typescript/commonjs/__tests__/BloomThemeProvider.test.d.ts.map +1 -0
- package/lib/typescript/commonjs/__tests__/BottomSheet.test.d.ts +2 -0
- package/lib/typescript/commonjs/__tests__/BottomSheet.test.d.ts.map +1 -0
- package/lib/typescript/commonjs/__tests__/Button.test.d.ts +2 -0
- package/lib/typescript/commonjs/__tests__/Button.test.d.ts.map +1 -0
- package/lib/typescript/commonjs/__tests__/theme.test.d.ts +2 -0
- package/lib/typescript/commonjs/__tests__/theme.test.d.ts.map +1 -0
- package/lib/typescript/commonjs/accordion/Accordion.d.ts +7 -0
- package/lib/typescript/commonjs/accordion/Accordion.d.ts.map +1 -0
- package/lib/typescript/commonjs/accordion/index.d.ts +3 -0
- package/lib/typescript/commonjs/accordion/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/accordion/types.d.ts +38 -0
- package/lib/typescript/commonjs/accordion/types.d.ts.map +1 -0
- package/lib/typescript/commonjs/badge/Badge.d.ts +4 -0
- package/lib/typescript/commonjs/badge/Badge.d.ts.map +1 -0
- package/lib/typescript/commonjs/badge/index.d.ts +3 -0
- package/lib/typescript/commonjs/badge/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/badge/types.d.ts +29 -0
- package/lib/typescript/commonjs/badge/types.d.ts.map +1 -0
- package/lib/typescript/commonjs/bottom-sheet/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/card/Card.d.ts +9 -0
- package/lib/typescript/commonjs/card/Card.d.ts.map +1 -0
- package/lib/typescript/commonjs/card/index.d.ts +3 -0
- package/lib/typescript/commonjs/card/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/card/types.d.ts +34 -0
- package/lib/typescript/commonjs/card/types.d.ts.map +1 -0
- package/lib/typescript/commonjs/checkbox/Checkbox.d.ts +4 -0
- package/lib/typescript/commonjs/checkbox/Checkbox.d.ts.map +1 -0
- package/lib/typescript/commonjs/checkbox/index.d.ts +3 -0
- package/lib/typescript/commonjs/checkbox/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/checkbox/types.d.ts +25 -0
- package/lib/typescript/commonjs/checkbox/types.d.ts.map +1 -0
- package/lib/typescript/commonjs/chip/Chip.d.ts +4 -0
- package/lib/typescript/commonjs/chip/Chip.d.ts.map +1 -0
- package/lib/typescript/commonjs/chip/index.d.ts +3 -0
- package/lib/typescript/commonjs/chip/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/chip/types.d.ts +31 -0
- package/lib/typescript/commonjs/chip/types.d.ts.map +1 -0
- package/lib/typescript/commonjs/index.d.ts +6 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/tabs/Tabs.d.ts +6 -0
- package/lib/typescript/commonjs/tabs/Tabs.d.ts.map +1 -0
- package/lib/typescript/commonjs/tabs/index.d.ts +3 -0
- package/lib/typescript/commonjs/tabs/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/tabs/types.d.ts +34 -0
- package/lib/typescript/commonjs/tabs/types.d.ts.map +1 -0
- package/lib/typescript/module/__tests__/BloomThemeProvider.test.d.ts +2 -0
- package/lib/typescript/module/__tests__/BloomThemeProvider.test.d.ts.map +1 -0
- package/lib/typescript/module/__tests__/BottomSheet.test.d.ts +2 -0
- package/lib/typescript/module/__tests__/BottomSheet.test.d.ts.map +1 -0
- package/lib/typescript/module/__tests__/Button.test.d.ts +2 -0
- package/lib/typescript/module/__tests__/Button.test.d.ts.map +1 -0
- package/lib/typescript/module/__tests__/theme.test.d.ts +2 -0
- package/lib/typescript/module/__tests__/theme.test.d.ts.map +1 -0
- package/lib/typescript/module/accordion/Accordion.d.ts +7 -0
- package/lib/typescript/module/accordion/Accordion.d.ts.map +1 -0
- package/lib/typescript/module/accordion/index.d.ts +3 -0
- package/lib/typescript/module/accordion/index.d.ts.map +1 -0
- package/lib/typescript/module/accordion/types.d.ts +38 -0
- package/lib/typescript/module/accordion/types.d.ts.map +1 -0
- package/lib/typescript/module/badge/Badge.d.ts +4 -0
- package/lib/typescript/module/badge/Badge.d.ts.map +1 -0
- package/lib/typescript/module/badge/index.d.ts +3 -0
- package/lib/typescript/module/badge/index.d.ts.map +1 -0
- package/lib/typescript/module/badge/types.d.ts +29 -0
- package/lib/typescript/module/badge/types.d.ts.map +1 -0
- package/lib/typescript/module/bottom-sheet/index.d.ts.map +1 -1
- package/lib/typescript/module/card/Card.d.ts +9 -0
- package/lib/typescript/module/card/Card.d.ts.map +1 -0
- package/lib/typescript/module/card/index.d.ts +3 -0
- package/lib/typescript/module/card/index.d.ts.map +1 -0
- package/lib/typescript/module/card/types.d.ts +34 -0
- package/lib/typescript/module/card/types.d.ts.map +1 -0
- package/lib/typescript/module/checkbox/Checkbox.d.ts +4 -0
- package/lib/typescript/module/checkbox/Checkbox.d.ts.map +1 -0
- package/lib/typescript/module/checkbox/index.d.ts +3 -0
- package/lib/typescript/module/checkbox/index.d.ts.map +1 -0
- package/lib/typescript/module/checkbox/types.d.ts +25 -0
- package/lib/typescript/module/checkbox/types.d.ts.map +1 -0
- package/lib/typescript/module/chip/Chip.d.ts +4 -0
- package/lib/typescript/module/chip/Chip.d.ts.map +1 -0
- package/lib/typescript/module/chip/index.d.ts +3 -0
- package/lib/typescript/module/chip/index.d.ts.map +1 -0
- package/lib/typescript/module/chip/types.d.ts +31 -0
- package/lib/typescript/module/chip/types.d.ts.map +1 -0
- package/lib/typescript/module/index.d.ts +6 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/tabs/Tabs.d.ts +6 -0
- package/lib/typescript/module/tabs/Tabs.d.ts.map +1 -0
- package/lib/typescript/module/tabs/index.d.ts +3 -0
- package/lib/typescript/module/tabs/index.d.ts.map +1 -0
- package/lib/typescript/module/tabs/types.d.ts +34 -0
- package/lib/typescript/module/tabs/types.d.ts.map +1 -0
- package/package.json +79 -1
- package/src/__tests__/BloomThemeProvider.test.tsx +160 -0
- package/src/__tests__/BottomSheet.test.tsx +109 -0
- package/src/__tests__/Button.test.tsx +98 -0
- package/src/__tests__/theme.test.ts +148 -0
- package/src/accordion/Accordion.tsx +261 -0
- package/src/accordion/index.ts +8 -0
- package/src/accordion/types.ts +42 -0
- package/src/badge/Badge.tsx +151 -0
- package/src/badge/index.ts +8 -0
- package/src/badge/types.ts +30 -0
- package/src/bottom-sheet/index.tsx +38 -18
- package/src/card/Card.tsx +197 -0
- package/src/card/index.ts +10 -0
- package/src/card/types.ts +40 -0
- package/src/checkbox/Checkbox.tsx +166 -0
- package/src/checkbox/index.ts +2 -0
- package/src/checkbox/types.ts +26 -0
- package/src/chip/Chip.tsx +156 -0
- package/src/chip/index.ts +2 -0
- package/src/chip/types.ts +32 -0
- package/src/index.ts +8 -0
- package/src/tabs/Tabs.tsx +218 -0
- package/src/tabs/index.ts +2 -0
- package/src/tabs/types.ts +37 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import React, { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef } from 'react';
|
|
2
|
+
import { View, Text, Pressable, Animated, type ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { useTheme } from '../theme/use-theme';
|
|
5
|
+
import { animation, borderRadius, space } from '../styles/tokens';
|
|
6
|
+
import type {
|
|
7
|
+
AccordionProps,
|
|
8
|
+
AccordionItemProps,
|
|
9
|
+
AccordionTriggerProps,
|
|
10
|
+
AccordionContentProps,
|
|
11
|
+
AccordionType,
|
|
12
|
+
} from './types';
|
|
13
|
+
|
|
14
|
+
// ---- Context ----
|
|
15
|
+
|
|
16
|
+
interface AccordionContextValue {
|
|
17
|
+
expandedValues: Set<string>;
|
|
18
|
+
toggle: (value: string) => void;
|
|
19
|
+
type: AccordionType;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const AccordionContext = createContext<AccordionContextValue>({
|
|
23
|
+
expandedValues: new Set(),
|
|
24
|
+
toggle: () => {},
|
|
25
|
+
type: 'single',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
interface AccordionItemContextValue {
|
|
29
|
+
value: string;
|
|
30
|
+
isExpanded: boolean;
|
|
31
|
+
disabled: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const AccordionItemContext = createContext<AccordionItemContextValue>({
|
|
35
|
+
value: '',
|
|
36
|
+
isExpanded: false,
|
|
37
|
+
disabled: false,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// ---- Accordion Root ----
|
|
41
|
+
|
|
42
|
+
const AccordionComponent: React.FC<AccordionProps> = ({
|
|
43
|
+
value,
|
|
44
|
+
onValueChange,
|
|
45
|
+
type = 'single',
|
|
46
|
+
children,
|
|
47
|
+
style,
|
|
48
|
+
testID,
|
|
49
|
+
}) => {
|
|
50
|
+
const expandedValues = useMemo(() => {
|
|
51
|
+
if (value == null) return new Set<string>();
|
|
52
|
+
if (Array.isArray(value)) return new Set(value);
|
|
53
|
+
return new Set([value]);
|
|
54
|
+
}, [value]);
|
|
55
|
+
|
|
56
|
+
const toggle = useCallback(
|
|
57
|
+
(itemValue: string) => {
|
|
58
|
+
if (type === 'single') {
|
|
59
|
+
const next = expandedValues.has(itemValue) ? undefined : itemValue;
|
|
60
|
+
onValueChange(next);
|
|
61
|
+
} else {
|
|
62
|
+
const next = new Set(expandedValues);
|
|
63
|
+
if (next.has(itemValue)) {
|
|
64
|
+
next.delete(itemValue);
|
|
65
|
+
} else {
|
|
66
|
+
next.add(itemValue);
|
|
67
|
+
}
|
|
68
|
+
onValueChange(Array.from(next));
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
[type, expandedValues, onValueChange],
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const contextValue = useMemo(
|
|
75
|
+
() => ({ expandedValues, toggle, type }),
|
|
76
|
+
[expandedValues, toggle, type],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<AccordionContext.Provider value={contextValue}>
|
|
81
|
+
<View style={style} testID={testID}>
|
|
82
|
+
{children}
|
|
83
|
+
</View>
|
|
84
|
+
</AccordionContext.Provider>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// ---- Accordion Item ----
|
|
89
|
+
|
|
90
|
+
const AccordionItemComponent: React.FC<AccordionItemProps> = ({
|
|
91
|
+
value,
|
|
92
|
+
children,
|
|
93
|
+
disabled = false,
|
|
94
|
+
style,
|
|
95
|
+
}) => {
|
|
96
|
+
const { expandedValues } = useContext(AccordionContext);
|
|
97
|
+
const isExpanded = expandedValues.has(value);
|
|
98
|
+
const theme = useTheme();
|
|
99
|
+
|
|
100
|
+
const itemContext = useMemo(
|
|
101
|
+
() => ({ value, isExpanded, disabled }),
|
|
102
|
+
[value, isExpanded, disabled],
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<AccordionItemContext.Provider value={itemContext}>
|
|
107
|
+
<View
|
|
108
|
+
style={[
|
|
109
|
+
{
|
|
110
|
+
borderBottomWidth: 1,
|
|
111
|
+
borderBottomColor: theme.colors.borderLight,
|
|
112
|
+
},
|
|
113
|
+
style,
|
|
114
|
+
]}
|
|
115
|
+
>
|
|
116
|
+
{children}
|
|
117
|
+
</View>
|
|
118
|
+
</AccordionItemContext.Provider>
|
|
119
|
+
);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// ---- Accordion Trigger ----
|
|
123
|
+
|
|
124
|
+
const AccordionTriggerComponent: React.FC<AccordionTriggerProps> = ({
|
|
125
|
+
children,
|
|
126
|
+
icon,
|
|
127
|
+
style,
|
|
128
|
+
textStyle,
|
|
129
|
+
}) => {
|
|
130
|
+
const theme = useTheme();
|
|
131
|
+
const { toggle } = useContext(AccordionContext);
|
|
132
|
+
const { value, isExpanded, disabled } = useContext(AccordionItemContext);
|
|
133
|
+
const rotateAnim = useRef(new Animated.Value(isExpanded ? 1 : 0)).current;
|
|
134
|
+
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
Animated.spring(rotateAnim, {
|
|
137
|
+
toValue: isExpanded ? 1 : 0,
|
|
138
|
+
useNativeDriver: true,
|
|
139
|
+
...animation.spring.snappy,
|
|
140
|
+
}).start();
|
|
141
|
+
}, [isExpanded, rotateAnim]);
|
|
142
|
+
|
|
143
|
+
const handlePress = useCallback(() => {
|
|
144
|
+
if (!disabled) {
|
|
145
|
+
toggle(value);
|
|
146
|
+
}
|
|
147
|
+
}, [value, disabled, toggle]);
|
|
148
|
+
|
|
149
|
+
const rotation = rotateAnim.interpolate({
|
|
150
|
+
inputRange: [0, 1],
|
|
151
|
+
outputRange: ['0deg', '180deg'],
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<Pressable
|
|
156
|
+
style={({ pressed }) => [
|
|
157
|
+
{
|
|
158
|
+
flexDirection: 'row',
|
|
159
|
+
alignItems: 'center',
|
|
160
|
+
paddingVertical: space.md,
|
|
161
|
+
paddingHorizontal: space.xs,
|
|
162
|
+
gap: space.sm,
|
|
163
|
+
opacity: disabled ? 0.4 : pressed ? 0.7 : 1,
|
|
164
|
+
},
|
|
165
|
+
style,
|
|
166
|
+
]}
|
|
167
|
+
onPress={handlePress}
|
|
168
|
+
disabled={disabled}
|
|
169
|
+
accessibilityRole="button"
|
|
170
|
+
accessibilityState={{ expanded: isExpanded, disabled }}
|
|
171
|
+
>
|
|
172
|
+
{icon}
|
|
173
|
+
<View style={{ flex: 1 }}>
|
|
174
|
+
{typeof children === 'string' ? (
|
|
175
|
+
<Text
|
|
176
|
+
style={[
|
|
177
|
+
{
|
|
178
|
+
fontSize: 15,
|
|
179
|
+
fontWeight: '600',
|
|
180
|
+
color: theme.colors.text,
|
|
181
|
+
},
|
|
182
|
+
textStyle,
|
|
183
|
+
]}
|
|
184
|
+
>
|
|
185
|
+
{children}
|
|
186
|
+
</Text>
|
|
187
|
+
) : (
|
|
188
|
+
children
|
|
189
|
+
)}
|
|
190
|
+
</View>
|
|
191
|
+
<Animated.View style={{ transform: [{ rotate: rotation }] }}>
|
|
192
|
+
<Text
|
|
193
|
+
style={{
|
|
194
|
+
fontSize: 16,
|
|
195
|
+
color: theme.colors.textSecondary,
|
|
196
|
+
lineHeight: 18,
|
|
197
|
+
}}
|
|
198
|
+
>
|
|
199
|
+
{'\u25BE'}
|
|
200
|
+
</Text>
|
|
201
|
+
</Animated.View>
|
|
202
|
+
</Pressable>
|
|
203
|
+
);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// ---- Accordion Content ----
|
|
207
|
+
|
|
208
|
+
const AccordionContentComponent: React.FC<AccordionContentProps> = ({
|
|
209
|
+
children,
|
|
210
|
+
style,
|
|
211
|
+
}) => {
|
|
212
|
+
const { isExpanded } = useContext(AccordionItemContext);
|
|
213
|
+
const heightAnim = useRef(new Animated.Value(isExpanded ? 1 : 0)).current;
|
|
214
|
+
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
Animated.spring(heightAnim, {
|
|
217
|
+
toValue: isExpanded ? 1 : 0,
|
|
218
|
+
useNativeDriver: false,
|
|
219
|
+
...animation.spring.gentle,
|
|
220
|
+
}).start();
|
|
221
|
+
}, [isExpanded, heightAnim]);
|
|
222
|
+
|
|
223
|
+
const opacity = heightAnim.interpolate({
|
|
224
|
+
inputRange: [0, 1],
|
|
225
|
+
outputRange: [0, 1],
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const maxHeight = heightAnim.interpolate({
|
|
229
|
+
inputRange: [0, 1],
|
|
230
|
+
outputRange: [0, 500], // reasonable max; content will use its natural height
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
return (
|
|
234
|
+
<Animated.View
|
|
235
|
+
style={[
|
|
236
|
+
{
|
|
237
|
+
overflow: 'hidden',
|
|
238
|
+
opacity,
|
|
239
|
+
maxHeight,
|
|
240
|
+
},
|
|
241
|
+
style,
|
|
242
|
+
]}
|
|
243
|
+
>
|
|
244
|
+
<View style={{ paddingBottom: space.md, paddingHorizontal: space.xs }}>
|
|
245
|
+
{children}
|
|
246
|
+
</View>
|
|
247
|
+
</Animated.View>
|
|
248
|
+
);
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
export const Accordion = memo(AccordionComponent);
|
|
252
|
+
Accordion.displayName = 'Accordion';
|
|
253
|
+
|
|
254
|
+
export const AccordionItem = memo(AccordionItemComponent);
|
|
255
|
+
AccordionItem.displayName = 'AccordionItem';
|
|
256
|
+
|
|
257
|
+
export const AccordionTrigger = memo(AccordionTriggerComponent);
|
|
258
|
+
AccordionTrigger.displayName = 'AccordionTrigger';
|
|
259
|
+
|
|
260
|
+
export const AccordionContent = memo(AccordionContentComponent);
|
|
261
|
+
AccordionContent.displayName = 'AccordionContent';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { StyleProp, ViewStyle, TextStyle } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export type AccordionType = 'single' | 'multiple';
|
|
4
|
+
|
|
5
|
+
export interface AccordionProps {
|
|
6
|
+
/** Controls which items are expanded. For 'single' type, pass a string or undefined.
|
|
7
|
+
* For 'multiple' type, pass an array of strings. */
|
|
8
|
+
value: string | string[] | undefined;
|
|
9
|
+
/** Called when expanded items change. */
|
|
10
|
+
onValueChange: (value: string | string[] | undefined) => void;
|
|
11
|
+
/** Whether only one item can be expanded at a time. */
|
|
12
|
+
type?: AccordionType;
|
|
13
|
+
/** Accordion items. */
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
style?: StyleProp<ViewStyle>;
|
|
16
|
+
testID?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface AccordionItemProps {
|
|
20
|
+
/** Unique value identifying this item. */
|
|
21
|
+
value: string;
|
|
22
|
+
/** Item content: should be AccordionTrigger and AccordionContent. */
|
|
23
|
+
children: React.ReactNode;
|
|
24
|
+
/** Whether this item is disabled. */
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
style?: StyleProp<ViewStyle>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface AccordionTriggerProps {
|
|
30
|
+
/** Trigger content (label text). */
|
|
31
|
+
children: React.ReactNode;
|
|
32
|
+
/** Icon to show on the left side. */
|
|
33
|
+
icon?: React.ReactNode;
|
|
34
|
+
style?: StyleProp<ViewStyle>;
|
|
35
|
+
textStyle?: StyleProp<TextStyle>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface AccordionContentProps {
|
|
39
|
+
/** Content to show when expanded. */
|
|
40
|
+
children: React.ReactNode;
|
|
41
|
+
style?: StyleProp<ViewStyle>;
|
|
42
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import React, { memo, useMemo } from 'react';
|
|
2
|
+
import { View, Text, type ViewStyle, type TextStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { useTheme } from '../theme/use-theme';
|
|
5
|
+
import type { BadgeProps, BadgeColor, BadgeVariant } from './types';
|
|
6
|
+
|
|
7
|
+
const SIZE_CONFIG = {
|
|
8
|
+
small: { minWidth: 16, height: 16, fontSize: 10, paddingHorizontal: 4, dotSize: 6 },
|
|
9
|
+
medium: { minWidth: 20, height: 20, fontSize: 12, paddingHorizontal: 6, dotSize: 8 },
|
|
10
|
+
large: { minWidth: 24, height: 24, fontSize: 14, paddingHorizontal: 8, dotSize: 10 },
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
const PLACEMENT_CONFIG = {
|
|
14
|
+
'top-right': { top: -4, right: -4 },
|
|
15
|
+
'top-left': { top: -4, left: -4 },
|
|
16
|
+
'bottom-right': { bottom: -4, right: -4 },
|
|
17
|
+
'bottom-left': { bottom: -4, left: -4 },
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
function useColorPair(
|
|
21
|
+
color: BadgeColor,
|
|
22
|
+
variant: BadgeVariant,
|
|
23
|
+
theme: ReturnType<typeof useTheme>,
|
|
24
|
+
): { bg: string; fg: string } {
|
|
25
|
+
const colorMap: Record<BadgeColor, string> = {
|
|
26
|
+
default: theme.colors.textSecondary,
|
|
27
|
+
primary: theme.colors.primary,
|
|
28
|
+
success: theme.colors.success,
|
|
29
|
+
warning: theme.colors.warning,
|
|
30
|
+
error: theme.colors.error,
|
|
31
|
+
info: theme.colors.info,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const base = colorMap[color];
|
|
35
|
+
|
|
36
|
+
switch (variant) {
|
|
37
|
+
case 'solid':
|
|
38
|
+
return { bg: base, fg: '#fff' };
|
|
39
|
+
case 'subtle':
|
|
40
|
+
return { bg: base + '20', fg: base };
|
|
41
|
+
case 'outlined':
|
|
42
|
+
return { bg: 'transparent', fg: base };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const BadgeComponent: React.FC<BadgeProps> = ({
|
|
47
|
+
content,
|
|
48
|
+
variant = 'solid',
|
|
49
|
+
color = 'error',
|
|
50
|
+
size = 'medium',
|
|
51
|
+
dot = false,
|
|
52
|
+
max,
|
|
53
|
+
invisible = false,
|
|
54
|
+
placement = 'top-right',
|
|
55
|
+
children,
|
|
56
|
+
style,
|
|
57
|
+
textStyle,
|
|
58
|
+
testID,
|
|
59
|
+
}) => {
|
|
60
|
+
const theme = useTheme();
|
|
61
|
+
const colors = useColorPair(color, variant, theme);
|
|
62
|
+
const sizeConfig = SIZE_CONFIG[size];
|
|
63
|
+
|
|
64
|
+
const displayContent = useMemo(() => {
|
|
65
|
+
if (dot) return null;
|
|
66
|
+
if (content == null) return null;
|
|
67
|
+
if (typeof content === 'number' && max != null && content > max) {
|
|
68
|
+
return `${max}+`;
|
|
69
|
+
}
|
|
70
|
+
return String(content);
|
|
71
|
+
}, [content, dot, max]);
|
|
72
|
+
|
|
73
|
+
const badgeStyle = useMemo((): ViewStyle => {
|
|
74
|
+
if (dot) {
|
|
75
|
+
return {
|
|
76
|
+
width: sizeConfig.dotSize,
|
|
77
|
+
height: sizeConfig.dotSize,
|
|
78
|
+
borderRadius: sizeConfig.dotSize / 2,
|
|
79
|
+
backgroundColor: colors.bg,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const base: ViewStyle = {
|
|
84
|
+
minWidth: sizeConfig.height,
|
|
85
|
+
height: sizeConfig.height,
|
|
86
|
+
borderRadius: sizeConfig.height / 2,
|
|
87
|
+
paddingHorizontal: sizeConfig.paddingHorizontal,
|
|
88
|
+
backgroundColor: colors.bg,
|
|
89
|
+
alignItems: 'center',
|
|
90
|
+
justifyContent: 'center',
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
if (variant === 'outlined') {
|
|
94
|
+
base.borderWidth = 1;
|
|
95
|
+
base.borderColor = colors.fg;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return base;
|
|
99
|
+
}, [dot, sizeConfig, colors, variant]);
|
|
100
|
+
|
|
101
|
+
const badgeTextStyle = useMemo(
|
|
102
|
+
(): TextStyle => ({
|
|
103
|
+
fontSize: sizeConfig.fontSize,
|
|
104
|
+
fontWeight: '600',
|
|
105
|
+
color: colors.fg,
|
|
106
|
+
textAlign: 'center',
|
|
107
|
+
lineHeight: sizeConfig.height,
|
|
108
|
+
}),
|
|
109
|
+
[sizeConfig, colors],
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Standalone badge (no children)
|
|
113
|
+
if (!children) {
|
|
114
|
+
if (invisible) return null;
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<View style={[badgeStyle, style]} testID={testID}>
|
|
118
|
+
{displayContent != null && (
|
|
119
|
+
<Text style={[badgeTextStyle, textStyle]}>{displayContent}</Text>
|
|
120
|
+
)}
|
|
121
|
+
</View>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Positioned badge wrapping children
|
|
126
|
+
return (
|
|
127
|
+
<View style={{ position: 'relative', alignSelf: 'flex-start' }} testID={testID}>
|
|
128
|
+
{children}
|
|
129
|
+
{!invisible && (
|
|
130
|
+
<View
|
|
131
|
+
style={[
|
|
132
|
+
{
|
|
133
|
+
position: 'absolute',
|
|
134
|
+
zIndex: 1,
|
|
135
|
+
...PLACEMENT_CONFIG[placement],
|
|
136
|
+
},
|
|
137
|
+
badgeStyle,
|
|
138
|
+
style,
|
|
139
|
+
]}
|
|
140
|
+
>
|
|
141
|
+
{displayContent != null && (
|
|
142
|
+
<Text style={[badgeTextStyle, textStyle]}>{displayContent}</Text>
|
|
143
|
+
)}
|
|
144
|
+
</View>
|
|
145
|
+
)}
|
|
146
|
+
</View>
|
|
147
|
+
);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export const Badge = memo(BadgeComponent);
|
|
151
|
+
Badge.displayName = 'Badge';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { StyleProp, ViewStyle, TextStyle } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export type BadgeVariant = 'solid' | 'subtle' | 'outlined';
|
|
4
|
+
export type BadgeColor = 'default' | 'primary' | 'success' | 'warning' | 'error' | 'info';
|
|
5
|
+
export type BadgeSize = 'small' | 'medium' | 'large';
|
|
6
|
+
export type BadgePlacement = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left';
|
|
7
|
+
|
|
8
|
+
export interface BadgeProps {
|
|
9
|
+
/** Text or number to display in the badge. */
|
|
10
|
+
content?: string | number;
|
|
11
|
+
/** Visual variant. */
|
|
12
|
+
variant?: BadgeVariant;
|
|
13
|
+
/** Semantic color. */
|
|
14
|
+
color?: BadgeColor;
|
|
15
|
+
/** Size preset. */
|
|
16
|
+
size?: BadgeSize;
|
|
17
|
+
/** If true, renders as a small dot without content. */
|
|
18
|
+
dot?: boolean;
|
|
19
|
+
/** Maximum number to display. Values above this show "{max}+". */
|
|
20
|
+
max?: number;
|
|
21
|
+
/** If true, the badge is hidden. */
|
|
22
|
+
invisible?: boolean;
|
|
23
|
+
/** Where to position the badge relative to its child. */
|
|
24
|
+
placement?: BadgePlacement;
|
|
25
|
+
/** The element the badge is attached to. */
|
|
26
|
+
children?: React.ReactNode;
|
|
27
|
+
style?: StyleProp<ViewStyle>;
|
|
28
|
+
textStyle?: StyleProp<TextStyle>;
|
|
29
|
+
testID?: string;
|
|
30
|
+
}
|
|
@@ -23,18 +23,31 @@ import Animated, {
|
|
|
23
23
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
24
24
|
import { useTheme } from '../theme/use-theme';
|
|
25
25
|
|
|
26
|
-
//
|
|
27
|
-
// The hook is always called (Rules of Hooks) but does nothing without the library
|
|
26
|
+
// Keyboard handler — only on native platforms. On web, keyboard events are handled by the browser.
|
|
28
27
|
const noopKeyboardHandler = (_handlers: Record<string, (e: { height: number }) => void>, _deps: unknown[]) => {};
|
|
29
28
|
let useKeyboardHandler: (handlers: Record<string, (e: { height: number }) => void>, deps: unknown[]) => void = noopKeyboardHandler;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
if (Platform.OS !== 'web') {
|
|
30
|
+
try {
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
32
|
+
useKeyboardHandler = require('react-native-keyboard-controller').useKeyboardHandler;
|
|
33
|
+
} catch {
|
|
34
|
+
// react-native-keyboard-controller not available
|
|
35
|
+
}
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
/** Hook that returns current screen dimensions and updates on rotation/resize. */
|
|
39
|
+
function useScreenDimensions() {
|
|
40
|
+
const [dimensions, setDimensions] = useState(() => Dimensions.get('window'));
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const subscription = Dimensions.addEventListener('change', ({ window }) => {
|
|
44
|
+
setDimensions(window);
|
|
45
|
+
});
|
|
46
|
+
return () => subscription.remove();
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
return dimensions;
|
|
50
|
+
}
|
|
38
51
|
|
|
39
52
|
const SPRING_CONFIG = {
|
|
40
53
|
damping: 25,
|
|
@@ -77,14 +90,22 @@ const BottomSheet = forwardRef((props: BottomSheetProps, ref: React.ForwardedRef
|
|
|
77
90
|
} = props;
|
|
78
91
|
|
|
79
92
|
const insets = useSafeAreaInsets();
|
|
80
|
-
const
|
|
93
|
+
const theme = useTheme();
|
|
94
|
+
const { colors } = theme;
|
|
95
|
+
const { height: screenHeight } = useScreenDimensions();
|
|
81
96
|
const [visible, setVisible] = useState(false);
|
|
82
97
|
const [rendered, setRendered] = useState(false); // keep mounted for exit animation
|
|
83
98
|
const closeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
84
99
|
const hasClosedRef = useRef(false);
|
|
85
100
|
const scrollViewRef = useRef<Animated.ScrollView>(null);
|
|
86
101
|
|
|
87
|
-
const
|
|
102
|
+
const screenHeightSV = useSharedValue(screenHeight);
|
|
103
|
+
// Keep shared value in sync when screen dimensions change (rotation/resize)
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
screenHeightSV.value = screenHeight;
|
|
106
|
+
}, [screenHeight, screenHeightSV]);
|
|
107
|
+
|
|
108
|
+
const translateY = useSharedValue(screenHeight);
|
|
88
109
|
const opacity = useSharedValue(0);
|
|
89
110
|
const scrollOffsetY = useSharedValue(0);
|
|
90
111
|
const isScrollAtTop = useSharedValue(true);
|
|
@@ -134,7 +155,7 @@ const BottomSheet = forwardRef((props: BottomSheetProps, ref: React.ForwardedRef
|
|
|
134
155
|
runOnJS(finishClose)();
|
|
135
156
|
}
|
|
136
157
|
});
|
|
137
|
-
translateY.value = withSpring(
|
|
158
|
+
translateY.value = withSpring(screenHeight, { ...SPRING_CONFIG, stiffness: 250 });
|
|
138
159
|
|
|
139
160
|
// Fallback timer to ensure close completes (especially on web)
|
|
140
161
|
if (closeTimeoutRef.current) {
|
|
@@ -226,14 +247,14 @@ const BottomSheet = forwardRef((props: BottomSheetProps, ref: React.ForwardedRef
|
|
|
226
247
|
const velocity = event.velocityY;
|
|
227
248
|
const distance = translateY.value;
|
|
228
249
|
// Require a deeper pull to close (more like native bottom sheets)
|
|
229
|
-
const closeThreshold = Math.max(140,
|
|
250
|
+
const closeThreshold = Math.max(140, screenHeightSV.value * 0.25);
|
|
230
251
|
const fastSwipeThreshold = 900;
|
|
231
252
|
const shouldClose =
|
|
232
253
|
velocity > fastSwipeThreshold ||
|
|
233
254
|
(distance > closeThreshold && velocity > -300);
|
|
234
255
|
|
|
235
256
|
if (shouldClose) {
|
|
236
|
-
translateY.value = withSpring(
|
|
257
|
+
translateY.value = withSpring(screenHeightSV.value, {
|
|
237
258
|
...SPRING_CONFIG,
|
|
238
259
|
velocity: velocity,
|
|
239
260
|
});
|
|
@@ -261,7 +282,7 @@ const BottomSheet = forwardRef((props: BottomSheetProps, ref: React.ForwardedRef
|
|
|
261
282
|
}));
|
|
262
283
|
|
|
263
284
|
const sheetStyle = useAnimatedStyle(() => {
|
|
264
|
-
const scale = interpolate(translateY.value, [0,
|
|
285
|
+
const scale = interpolate(translateY.value, [0, screenHeightSV.value], [1, 0.95]);
|
|
265
286
|
return {
|
|
266
287
|
transform: [
|
|
267
288
|
{ translateY: translateY.value - keyboardHeight.value },
|
|
@@ -271,7 +292,7 @@ const BottomSheet = forwardRef((props: BottomSheetProps, ref: React.ForwardedRef
|
|
|
271
292
|
});
|
|
272
293
|
|
|
273
294
|
const sheetHeightStyle = useAnimatedStyle(() => ({
|
|
274
|
-
maxHeight:
|
|
295
|
+
maxHeight: screenHeightSV.value - keyboardHeight.value - insets.top - (detached ? insets.bottom + 16 : 0),
|
|
275
296
|
}), [insets.top, insets.bottom, detached]);
|
|
276
297
|
|
|
277
298
|
const sheetMarginStyle = useAnimatedStyle(() => {
|
|
@@ -302,11 +323,10 @@ const BottomSheet = forwardRef((props: BottomSheetProps, ref: React.ForwardedRef
|
|
|
302
323
|
});
|
|
303
324
|
|
|
304
325
|
const dynamicStyles = useMemo(() => {
|
|
305
|
-
const isDark = colors.background === '#000000';
|
|
306
326
|
return StyleSheet.create({
|
|
307
327
|
handle: {
|
|
308
328
|
...styles.handle,
|
|
309
|
-
backgroundColor: isDark ? '#444' : '#C7C7CC',
|
|
329
|
+
backgroundColor: theme.isDark ? '#444' : '#C7C7CC',
|
|
310
330
|
},
|
|
311
331
|
sheet: {
|
|
312
332
|
...styles.sheet,
|
|
@@ -319,7 +339,7 @@ const BottomSheet = forwardRef((props: BottomSheetProps, ref: React.ForwardedRef
|
|
|
319
339
|
// The sheet extends behind safe area, and screens add padding as needed
|
|
320
340
|
},
|
|
321
341
|
});
|
|
322
|
-
}, [colors.background, detached]);
|
|
342
|
+
}, [colors.background, theme.isDark, detached]);
|
|
323
343
|
|
|
324
344
|
if (!rendered) return null;
|
|
325
345
|
|