@oxyhq/bloom 0.1.14 → 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.
Files changed (199) hide show
  1. package/lib/commonjs/accordion/Accordion.js +230 -0
  2. package/lib/commonjs/accordion/Accordion.js.map +1 -0
  3. package/lib/commonjs/accordion/index.js +31 -0
  4. package/lib/commonjs/accordion/index.js.map +1 -0
  5. package/lib/commonjs/accordion/types.js +6 -0
  6. package/lib/commonjs/accordion/types.js.map +1 -0
  7. package/lib/commonjs/badge/Badge.js +173 -0
  8. package/lib/commonjs/badge/Badge.js.map +1 -0
  9. package/lib/commonjs/badge/index.js +13 -0
  10. package/lib/commonjs/badge/index.js.map +1 -0
  11. package/lib/commonjs/badge/types.js +6 -0
  12. package/lib/commonjs/badge/types.js.map +1 -0
  13. package/lib/commonjs/bottom-sheet/index.js +32 -14
  14. package/lib/commonjs/bottom-sheet/index.js.map +1 -1
  15. package/lib/commonjs/card/Card.js +165 -0
  16. package/lib/commonjs/card/Card.js.map +1 -0
  17. package/lib/commonjs/card/index.js +43 -0
  18. package/lib/commonjs/card/index.js.map +1 -0
  19. package/lib/commonjs/card/types.js +6 -0
  20. package/lib/commonjs/card/types.js.map +1 -0
  21. package/lib/commonjs/checkbox/Checkbox.js +177 -0
  22. package/lib/commonjs/checkbox/Checkbox.js.map +1 -0
  23. package/lib/commonjs/checkbox/index.js +13 -0
  24. package/lib/commonjs/checkbox/index.js.map +1 -0
  25. package/lib/commonjs/checkbox/types.js +6 -0
  26. package/lib/commonjs/checkbox/types.js.map +1 -0
  27. package/lib/commonjs/chip/Chip.js +180 -0
  28. package/lib/commonjs/chip/Chip.js.map +1 -0
  29. package/lib/commonjs/chip/index.js +13 -0
  30. package/lib/commonjs/chip/index.js.map +1 -0
  31. package/lib/commonjs/chip/types.js +6 -0
  32. package/lib/commonjs/chip/types.js.map +1 -0
  33. package/lib/commonjs/index.js +56 -2
  34. package/lib/commonjs/index.js.map +1 -1
  35. package/lib/commonjs/tabs/Tabs.js +202 -0
  36. package/lib/commonjs/tabs/Tabs.js.map +1 -0
  37. package/lib/commonjs/tabs/index.js +25 -0
  38. package/lib/commonjs/tabs/index.js.map +1 -0
  39. package/lib/commonjs/tabs/types.js +6 -0
  40. package/lib/commonjs/tabs/types.js.map +1 -0
  41. package/lib/module/accordion/Accordion.js +225 -0
  42. package/lib/module/accordion/Accordion.js.map +1 -0
  43. package/lib/module/accordion/index.js +4 -0
  44. package/lib/module/accordion/index.js.map +1 -0
  45. package/lib/module/accordion/types.js +4 -0
  46. package/lib/module/accordion/types.js.map +1 -0
  47. package/lib/module/badge/Badge.js +168 -0
  48. package/lib/module/badge/Badge.js.map +1 -0
  49. package/lib/module/badge/index.js +4 -0
  50. package/lib/module/badge/index.js.map +1 -0
  51. package/lib/module/badge/types.js +4 -0
  52. package/lib/module/badge/types.js.map +1 -0
  53. package/lib/module/bottom-sheet/index.js +32 -14
  54. package/lib/module/bottom-sheet/index.js.map +1 -1
  55. package/lib/module/card/Card.js +160 -0
  56. package/lib/module/card/Card.js.map +1 -0
  57. package/lib/module/card/index.js +4 -0
  58. package/lib/module/card/index.js.map +1 -0
  59. package/lib/module/card/types.js +4 -0
  60. package/lib/module/card/types.js.map +1 -0
  61. package/lib/module/checkbox/Checkbox.js +172 -0
  62. package/lib/module/checkbox/Checkbox.js.map +1 -0
  63. package/lib/module/checkbox/index.js +4 -0
  64. package/lib/module/checkbox/index.js.map +1 -0
  65. package/lib/module/checkbox/types.js +4 -0
  66. package/lib/module/checkbox/types.js.map +1 -0
  67. package/lib/module/chip/Chip.js +175 -0
  68. package/lib/module/chip/Chip.js.map +1 -0
  69. package/lib/module/chip/index.js +4 -0
  70. package/lib/module/chip/index.js.map +1 -0
  71. package/lib/module/chip/types.js +4 -0
  72. package/lib/module/chip/types.js.map +1 -0
  73. package/lib/module/index.js +8 -0
  74. package/lib/module/index.js.map +1 -1
  75. package/lib/module/tabs/Tabs.js +197 -0
  76. package/lib/module/tabs/Tabs.js.map +1 -0
  77. package/lib/module/tabs/index.js +4 -0
  78. package/lib/module/tabs/index.js.map +1 -0
  79. package/lib/module/tabs/types.js +4 -0
  80. package/lib/module/tabs/types.js.map +1 -0
  81. package/lib/typescript/commonjs/__tests__/BloomThemeProvider.test.d.ts +2 -0
  82. package/lib/typescript/commonjs/__tests__/BloomThemeProvider.test.d.ts.map +1 -0
  83. package/lib/typescript/commonjs/__tests__/BottomSheet.test.d.ts +2 -0
  84. package/lib/typescript/commonjs/__tests__/BottomSheet.test.d.ts.map +1 -0
  85. package/lib/typescript/commonjs/__tests__/Button.test.d.ts +2 -0
  86. package/lib/typescript/commonjs/__tests__/Button.test.d.ts.map +1 -0
  87. package/lib/typescript/commonjs/__tests__/theme.test.d.ts +2 -0
  88. package/lib/typescript/commonjs/__tests__/theme.test.d.ts.map +1 -0
  89. package/lib/typescript/commonjs/accordion/Accordion.d.ts +7 -0
  90. package/lib/typescript/commonjs/accordion/Accordion.d.ts.map +1 -0
  91. package/lib/typescript/commonjs/accordion/index.d.ts +3 -0
  92. package/lib/typescript/commonjs/accordion/index.d.ts.map +1 -0
  93. package/lib/typescript/commonjs/accordion/types.d.ts +38 -0
  94. package/lib/typescript/commonjs/accordion/types.d.ts.map +1 -0
  95. package/lib/typescript/commonjs/badge/Badge.d.ts +4 -0
  96. package/lib/typescript/commonjs/badge/Badge.d.ts.map +1 -0
  97. package/lib/typescript/commonjs/badge/index.d.ts +3 -0
  98. package/lib/typescript/commonjs/badge/index.d.ts.map +1 -0
  99. package/lib/typescript/commonjs/badge/types.d.ts +29 -0
  100. package/lib/typescript/commonjs/badge/types.d.ts.map +1 -0
  101. package/lib/typescript/commonjs/bottom-sheet/index.d.ts.map +1 -1
  102. package/lib/typescript/commonjs/card/Card.d.ts +9 -0
  103. package/lib/typescript/commonjs/card/Card.d.ts.map +1 -0
  104. package/lib/typescript/commonjs/card/index.d.ts +3 -0
  105. package/lib/typescript/commonjs/card/index.d.ts.map +1 -0
  106. package/lib/typescript/commonjs/card/types.d.ts +34 -0
  107. package/lib/typescript/commonjs/card/types.d.ts.map +1 -0
  108. package/lib/typescript/commonjs/checkbox/Checkbox.d.ts +4 -0
  109. package/lib/typescript/commonjs/checkbox/Checkbox.d.ts.map +1 -0
  110. package/lib/typescript/commonjs/checkbox/index.d.ts +3 -0
  111. package/lib/typescript/commonjs/checkbox/index.d.ts.map +1 -0
  112. package/lib/typescript/commonjs/checkbox/types.d.ts +25 -0
  113. package/lib/typescript/commonjs/checkbox/types.d.ts.map +1 -0
  114. package/lib/typescript/commonjs/chip/Chip.d.ts +4 -0
  115. package/lib/typescript/commonjs/chip/Chip.d.ts.map +1 -0
  116. package/lib/typescript/commonjs/chip/index.d.ts +3 -0
  117. package/lib/typescript/commonjs/chip/index.d.ts.map +1 -0
  118. package/lib/typescript/commonjs/chip/types.d.ts +31 -0
  119. package/lib/typescript/commonjs/chip/types.d.ts.map +1 -0
  120. package/lib/typescript/commonjs/index.d.ts +6 -0
  121. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  122. package/lib/typescript/commonjs/tabs/Tabs.d.ts +6 -0
  123. package/lib/typescript/commonjs/tabs/Tabs.d.ts.map +1 -0
  124. package/lib/typescript/commonjs/tabs/index.d.ts +3 -0
  125. package/lib/typescript/commonjs/tabs/index.d.ts.map +1 -0
  126. package/lib/typescript/commonjs/tabs/types.d.ts +34 -0
  127. package/lib/typescript/commonjs/tabs/types.d.ts.map +1 -0
  128. package/lib/typescript/module/__tests__/BloomThemeProvider.test.d.ts +2 -0
  129. package/lib/typescript/module/__tests__/BloomThemeProvider.test.d.ts.map +1 -0
  130. package/lib/typescript/module/__tests__/BottomSheet.test.d.ts +2 -0
  131. package/lib/typescript/module/__tests__/BottomSheet.test.d.ts.map +1 -0
  132. package/lib/typescript/module/__tests__/Button.test.d.ts +2 -0
  133. package/lib/typescript/module/__tests__/Button.test.d.ts.map +1 -0
  134. package/lib/typescript/module/__tests__/theme.test.d.ts +2 -0
  135. package/lib/typescript/module/__tests__/theme.test.d.ts.map +1 -0
  136. package/lib/typescript/module/accordion/Accordion.d.ts +7 -0
  137. package/lib/typescript/module/accordion/Accordion.d.ts.map +1 -0
  138. package/lib/typescript/module/accordion/index.d.ts +3 -0
  139. package/lib/typescript/module/accordion/index.d.ts.map +1 -0
  140. package/lib/typescript/module/accordion/types.d.ts +38 -0
  141. package/lib/typescript/module/accordion/types.d.ts.map +1 -0
  142. package/lib/typescript/module/badge/Badge.d.ts +4 -0
  143. package/lib/typescript/module/badge/Badge.d.ts.map +1 -0
  144. package/lib/typescript/module/badge/index.d.ts +3 -0
  145. package/lib/typescript/module/badge/index.d.ts.map +1 -0
  146. package/lib/typescript/module/badge/types.d.ts +29 -0
  147. package/lib/typescript/module/badge/types.d.ts.map +1 -0
  148. package/lib/typescript/module/bottom-sheet/index.d.ts.map +1 -1
  149. package/lib/typescript/module/card/Card.d.ts +9 -0
  150. package/lib/typescript/module/card/Card.d.ts.map +1 -0
  151. package/lib/typescript/module/card/index.d.ts +3 -0
  152. package/lib/typescript/module/card/index.d.ts.map +1 -0
  153. package/lib/typescript/module/card/types.d.ts +34 -0
  154. package/lib/typescript/module/card/types.d.ts.map +1 -0
  155. package/lib/typescript/module/checkbox/Checkbox.d.ts +4 -0
  156. package/lib/typescript/module/checkbox/Checkbox.d.ts.map +1 -0
  157. package/lib/typescript/module/checkbox/index.d.ts +3 -0
  158. package/lib/typescript/module/checkbox/index.d.ts.map +1 -0
  159. package/lib/typescript/module/checkbox/types.d.ts +25 -0
  160. package/lib/typescript/module/checkbox/types.d.ts.map +1 -0
  161. package/lib/typescript/module/chip/Chip.d.ts +4 -0
  162. package/lib/typescript/module/chip/Chip.d.ts.map +1 -0
  163. package/lib/typescript/module/chip/index.d.ts +3 -0
  164. package/lib/typescript/module/chip/index.d.ts.map +1 -0
  165. package/lib/typescript/module/chip/types.d.ts +31 -0
  166. package/lib/typescript/module/chip/types.d.ts.map +1 -0
  167. package/lib/typescript/module/index.d.ts +6 -0
  168. package/lib/typescript/module/index.d.ts.map +1 -1
  169. package/lib/typescript/module/tabs/Tabs.d.ts +6 -0
  170. package/lib/typescript/module/tabs/Tabs.d.ts.map +1 -0
  171. package/lib/typescript/module/tabs/index.d.ts +3 -0
  172. package/lib/typescript/module/tabs/index.d.ts.map +1 -0
  173. package/lib/typescript/module/tabs/types.d.ts +34 -0
  174. package/lib/typescript/module/tabs/types.d.ts.map +1 -0
  175. package/package.json +79 -1
  176. package/src/__tests__/BloomThemeProvider.test.tsx +160 -0
  177. package/src/__tests__/BottomSheet.test.tsx +109 -0
  178. package/src/__tests__/Button.test.tsx +98 -0
  179. package/src/__tests__/theme.test.ts +148 -0
  180. package/src/accordion/Accordion.tsx +261 -0
  181. package/src/accordion/index.ts +8 -0
  182. package/src/accordion/types.ts +42 -0
  183. package/src/badge/Badge.tsx +151 -0
  184. package/src/badge/index.ts +8 -0
  185. package/src/badge/types.ts +30 -0
  186. package/src/bottom-sheet/index.tsx +30 -11
  187. package/src/card/Card.tsx +197 -0
  188. package/src/card/index.ts +10 -0
  189. package/src/card/types.ts +40 -0
  190. package/src/checkbox/Checkbox.tsx +166 -0
  191. package/src/checkbox/index.ts +2 -0
  192. package/src/checkbox/types.ts +26 -0
  193. package/src/chip/Chip.tsx +156 -0
  194. package/src/chip/index.ts +2 -0
  195. package/src/chip/types.ts +32 -0
  196. package/src/index.ts +8 -0
  197. package/src/tabs/Tabs.tsx +218 -0
  198. package/src/tabs/index.ts +2 -0
  199. 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,8 @@
1
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from './Accordion';
2
+ export type {
3
+ AccordionProps,
4
+ AccordionItemProps,
5
+ AccordionTriggerProps,
6
+ AccordionContentProps,
7
+ AccordionType,
8
+ } from './types';
@@ -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,8 @@
1
+ export { Badge } from './Badge';
2
+ export type {
3
+ BadgeProps,
4
+ BadgeVariant,
5
+ BadgeColor,
6
+ BadgeSize,
7
+ BadgePlacement,
8
+ } from './types';
@@ -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
+ }
@@ -35,7 +35,19 @@ if (Platform.OS !== 'web') {
35
35
  }
36
36
  }
37
37
 
38
- const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
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
+ }
39
51
 
40
52
  const SPRING_CONFIG = {
41
53
  damping: 25,
@@ -78,14 +90,22 @@ const BottomSheet = forwardRef((props: BottomSheetProps, ref: React.ForwardedRef
78
90
  } = props;
79
91
 
80
92
  const insets = useSafeAreaInsets();
81
- const { colors } = useTheme();
93
+ const theme = useTheme();
94
+ const { colors } = theme;
95
+ const { height: screenHeight } = useScreenDimensions();
82
96
  const [visible, setVisible] = useState(false);
83
97
  const [rendered, setRendered] = useState(false); // keep mounted for exit animation
84
98
  const closeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
85
99
  const hasClosedRef = useRef(false);
86
100
  const scrollViewRef = useRef<Animated.ScrollView>(null);
87
101
 
88
- const translateY = useSharedValue(SCREEN_HEIGHT);
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);
89
109
  const opacity = useSharedValue(0);
90
110
  const scrollOffsetY = useSharedValue(0);
91
111
  const isScrollAtTop = useSharedValue(true);
@@ -135,7 +155,7 @@ const BottomSheet = forwardRef((props: BottomSheetProps, ref: React.ForwardedRef
135
155
  runOnJS(finishClose)();
136
156
  }
137
157
  });
138
- translateY.value = withSpring(SCREEN_HEIGHT, { ...SPRING_CONFIG, stiffness: 250 });
158
+ translateY.value = withSpring(screenHeight, { ...SPRING_CONFIG, stiffness: 250 });
139
159
 
140
160
  // Fallback timer to ensure close completes (especially on web)
141
161
  if (closeTimeoutRef.current) {
@@ -227,14 +247,14 @@ const BottomSheet = forwardRef((props: BottomSheetProps, ref: React.ForwardedRef
227
247
  const velocity = event.velocityY;
228
248
  const distance = translateY.value;
229
249
  // Require a deeper pull to close (more like native bottom sheets)
230
- const closeThreshold = Math.max(140, SCREEN_HEIGHT * 0.25);
250
+ const closeThreshold = Math.max(140, screenHeightSV.value * 0.25);
231
251
  const fastSwipeThreshold = 900;
232
252
  const shouldClose =
233
253
  velocity > fastSwipeThreshold ||
234
254
  (distance > closeThreshold && velocity > -300);
235
255
 
236
256
  if (shouldClose) {
237
- translateY.value = withSpring(SCREEN_HEIGHT, {
257
+ translateY.value = withSpring(screenHeightSV.value, {
238
258
  ...SPRING_CONFIG,
239
259
  velocity: velocity,
240
260
  });
@@ -262,7 +282,7 @@ const BottomSheet = forwardRef((props: BottomSheetProps, ref: React.ForwardedRef
262
282
  }));
263
283
 
264
284
  const sheetStyle = useAnimatedStyle(() => {
265
- const scale = interpolate(translateY.value, [0, SCREEN_HEIGHT], [1, 0.95]);
285
+ const scale = interpolate(translateY.value, [0, screenHeightSV.value], [1, 0.95]);
266
286
  return {
267
287
  transform: [
268
288
  { translateY: translateY.value - keyboardHeight.value },
@@ -272,7 +292,7 @@ const BottomSheet = forwardRef((props: BottomSheetProps, ref: React.ForwardedRef
272
292
  });
273
293
 
274
294
  const sheetHeightStyle = useAnimatedStyle(() => ({
275
- maxHeight: SCREEN_HEIGHT - keyboardHeight.value - insets.top - (detached ? insets.bottom + 16 : 0),
295
+ maxHeight: screenHeightSV.value - keyboardHeight.value - insets.top - (detached ? insets.bottom + 16 : 0),
276
296
  }), [insets.top, insets.bottom, detached]);
277
297
 
278
298
  const sheetMarginStyle = useAnimatedStyle(() => {
@@ -303,11 +323,10 @@ const BottomSheet = forwardRef((props: BottomSheetProps, ref: React.ForwardedRef
303
323
  });
304
324
 
305
325
  const dynamicStyles = useMemo(() => {
306
- const isDark = colors.background === '#000000';
307
326
  return StyleSheet.create({
308
327
  handle: {
309
328
  ...styles.handle,
310
- backgroundColor: isDark ? '#444' : '#C7C7CC',
329
+ backgroundColor: theme.isDark ? '#444' : '#C7C7CC',
311
330
  },
312
331
  sheet: {
313
332
  ...styles.sheet,
@@ -320,7 +339,7 @@ const BottomSheet = forwardRef((props: BottomSheetProps, ref: React.ForwardedRef
320
339
  // The sheet extends behind safe area, and screens add padding as needed
321
340
  },
322
341
  });
323
- }, [colors.background, detached]);
342
+ }, [colors.background, theme.isDark, detached]);
324
343
 
325
344
  if (!rendered) return null;
326
345