@oxyhq/bloom 0.1.14 → 0.1.16

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 (211) 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/avatar/Avatar.js +19 -7
  8. package/lib/commonjs/avatar/Avatar.js.map +1 -1
  9. package/lib/commonjs/badge/Badge.js +173 -0
  10. package/lib/commonjs/badge/Badge.js.map +1 -0
  11. package/lib/commonjs/badge/index.js +13 -0
  12. package/lib/commonjs/badge/index.js.map +1 -0
  13. package/lib/commonjs/badge/types.js +6 -0
  14. package/lib/commonjs/badge/types.js.map +1 -0
  15. package/lib/commonjs/bottom-sheet/index.js +32 -14
  16. package/lib/commonjs/bottom-sheet/index.js.map +1 -1
  17. package/lib/commonjs/card/Card.js +165 -0
  18. package/lib/commonjs/card/Card.js.map +1 -0
  19. package/lib/commonjs/card/index.js +43 -0
  20. package/lib/commonjs/card/index.js.map +1 -0
  21. package/lib/commonjs/card/types.js +6 -0
  22. package/lib/commonjs/card/types.js.map +1 -0
  23. package/lib/commonjs/checkbox/Checkbox.js +177 -0
  24. package/lib/commonjs/checkbox/Checkbox.js.map +1 -0
  25. package/lib/commonjs/checkbox/index.js +13 -0
  26. package/lib/commonjs/checkbox/index.js.map +1 -0
  27. package/lib/commonjs/checkbox/types.js +6 -0
  28. package/lib/commonjs/checkbox/types.js.map +1 -0
  29. package/lib/commonjs/chip/Chip.js +180 -0
  30. package/lib/commonjs/chip/Chip.js.map +1 -0
  31. package/lib/commonjs/chip/index.js +13 -0
  32. package/lib/commonjs/chip/index.js.map +1 -0
  33. package/lib/commonjs/chip/types.js +6 -0
  34. package/lib/commonjs/chip/types.js.map +1 -0
  35. package/lib/commonjs/index.js +56 -2
  36. package/lib/commonjs/index.js.map +1 -1
  37. package/lib/commonjs/tabs/Tabs.js +202 -0
  38. package/lib/commonjs/tabs/Tabs.js.map +1 -0
  39. package/lib/commonjs/tabs/index.js +25 -0
  40. package/lib/commonjs/tabs/index.js.map +1 -0
  41. package/lib/commonjs/tabs/types.js +6 -0
  42. package/lib/commonjs/tabs/types.js.map +1 -0
  43. package/lib/module/accordion/Accordion.js +225 -0
  44. package/lib/module/accordion/Accordion.js.map +1 -0
  45. package/lib/module/accordion/index.js +4 -0
  46. package/lib/module/accordion/index.js.map +1 -0
  47. package/lib/module/accordion/types.js +4 -0
  48. package/lib/module/accordion/types.js.map +1 -0
  49. package/lib/module/avatar/Avatar.js +19 -7
  50. package/lib/module/avatar/Avatar.js.map +1 -1
  51. package/lib/module/badge/Badge.js +168 -0
  52. package/lib/module/badge/Badge.js.map +1 -0
  53. package/lib/module/badge/index.js +4 -0
  54. package/lib/module/badge/index.js.map +1 -0
  55. package/lib/module/badge/types.js +4 -0
  56. package/lib/module/badge/types.js.map +1 -0
  57. package/lib/module/bottom-sheet/index.js +32 -14
  58. package/lib/module/bottom-sheet/index.js.map +1 -1
  59. package/lib/module/card/Card.js +160 -0
  60. package/lib/module/card/Card.js.map +1 -0
  61. package/lib/module/card/index.js +4 -0
  62. package/lib/module/card/index.js.map +1 -0
  63. package/lib/module/card/types.js +4 -0
  64. package/lib/module/card/types.js.map +1 -0
  65. package/lib/module/checkbox/Checkbox.js +172 -0
  66. package/lib/module/checkbox/Checkbox.js.map +1 -0
  67. package/lib/module/checkbox/index.js +4 -0
  68. package/lib/module/checkbox/index.js.map +1 -0
  69. package/lib/module/checkbox/types.js +4 -0
  70. package/lib/module/checkbox/types.js.map +1 -0
  71. package/lib/module/chip/Chip.js +175 -0
  72. package/lib/module/chip/Chip.js.map +1 -0
  73. package/lib/module/chip/index.js +4 -0
  74. package/lib/module/chip/index.js.map +1 -0
  75. package/lib/module/chip/types.js +4 -0
  76. package/lib/module/chip/types.js.map +1 -0
  77. package/lib/module/index.js +8 -0
  78. package/lib/module/index.js.map +1 -1
  79. package/lib/module/tabs/Tabs.js +197 -0
  80. package/lib/module/tabs/Tabs.js.map +1 -0
  81. package/lib/module/tabs/index.js +4 -0
  82. package/lib/module/tabs/index.js.map +1 -0
  83. package/lib/module/tabs/types.js +4 -0
  84. package/lib/module/tabs/types.js.map +1 -0
  85. package/lib/typescript/commonjs/__tests__/BloomThemeProvider.test.d.ts +2 -0
  86. package/lib/typescript/commonjs/__tests__/BloomThemeProvider.test.d.ts.map +1 -0
  87. package/lib/typescript/commonjs/__tests__/BottomSheet.test.d.ts +2 -0
  88. package/lib/typescript/commonjs/__tests__/BottomSheet.test.d.ts.map +1 -0
  89. package/lib/typescript/commonjs/__tests__/Button.test.d.ts +2 -0
  90. package/lib/typescript/commonjs/__tests__/Button.test.d.ts.map +1 -0
  91. package/lib/typescript/commonjs/__tests__/theme.test.d.ts +2 -0
  92. package/lib/typescript/commonjs/__tests__/theme.test.d.ts.map +1 -0
  93. package/lib/typescript/commonjs/accordion/Accordion.d.ts +7 -0
  94. package/lib/typescript/commonjs/accordion/Accordion.d.ts.map +1 -0
  95. package/lib/typescript/commonjs/accordion/index.d.ts +3 -0
  96. package/lib/typescript/commonjs/accordion/index.d.ts.map +1 -0
  97. package/lib/typescript/commonjs/accordion/types.d.ts +38 -0
  98. package/lib/typescript/commonjs/accordion/types.d.ts.map +1 -0
  99. package/lib/typescript/commonjs/avatar/Avatar.d.ts.map +1 -1
  100. package/lib/typescript/commonjs/avatar/types.d.ts +4 -0
  101. package/lib/typescript/commonjs/avatar/types.d.ts.map +1 -1
  102. package/lib/typescript/commonjs/badge/Badge.d.ts +4 -0
  103. package/lib/typescript/commonjs/badge/Badge.d.ts.map +1 -0
  104. package/lib/typescript/commonjs/badge/index.d.ts +3 -0
  105. package/lib/typescript/commonjs/badge/index.d.ts.map +1 -0
  106. package/lib/typescript/commonjs/badge/types.d.ts +29 -0
  107. package/lib/typescript/commonjs/badge/types.d.ts.map +1 -0
  108. package/lib/typescript/commonjs/bottom-sheet/index.d.ts.map +1 -1
  109. package/lib/typescript/commonjs/card/Card.d.ts +9 -0
  110. package/lib/typescript/commonjs/card/Card.d.ts.map +1 -0
  111. package/lib/typescript/commonjs/card/index.d.ts +3 -0
  112. package/lib/typescript/commonjs/card/index.d.ts.map +1 -0
  113. package/lib/typescript/commonjs/card/types.d.ts +34 -0
  114. package/lib/typescript/commonjs/card/types.d.ts.map +1 -0
  115. package/lib/typescript/commonjs/checkbox/Checkbox.d.ts +4 -0
  116. package/lib/typescript/commonjs/checkbox/Checkbox.d.ts.map +1 -0
  117. package/lib/typescript/commonjs/checkbox/index.d.ts +3 -0
  118. package/lib/typescript/commonjs/checkbox/index.d.ts.map +1 -0
  119. package/lib/typescript/commonjs/checkbox/types.d.ts +25 -0
  120. package/lib/typescript/commonjs/checkbox/types.d.ts.map +1 -0
  121. package/lib/typescript/commonjs/chip/Chip.d.ts +4 -0
  122. package/lib/typescript/commonjs/chip/Chip.d.ts.map +1 -0
  123. package/lib/typescript/commonjs/chip/index.d.ts +3 -0
  124. package/lib/typescript/commonjs/chip/index.d.ts.map +1 -0
  125. package/lib/typescript/commonjs/chip/types.d.ts +31 -0
  126. package/lib/typescript/commonjs/chip/types.d.ts.map +1 -0
  127. package/lib/typescript/commonjs/index.d.ts +6 -0
  128. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  129. package/lib/typescript/commonjs/tabs/Tabs.d.ts +6 -0
  130. package/lib/typescript/commonjs/tabs/Tabs.d.ts.map +1 -0
  131. package/lib/typescript/commonjs/tabs/index.d.ts +3 -0
  132. package/lib/typescript/commonjs/tabs/index.d.ts.map +1 -0
  133. package/lib/typescript/commonjs/tabs/types.d.ts +34 -0
  134. package/lib/typescript/commonjs/tabs/types.d.ts.map +1 -0
  135. package/lib/typescript/module/__tests__/BloomThemeProvider.test.d.ts +2 -0
  136. package/lib/typescript/module/__tests__/BloomThemeProvider.test.d.ts.map +1 -0
  137. package/lib/typescript/module/__tests__/BottomSheet.test.d.ts +2 -0
  138. package/lib/typescript/module/__tests__/BottomSheet.test.d.ts.map +1 -0
  139. package/lib/typescript/module/__tests__/Button.test.d.ts +2 -0
  140. package/lib/typescript/module/__tests__/Button.test.d.ts.map +1 -0
  141. package/lib/typescript/module/__tests__/theme.test.d.ts +2 -0
  142. package/lib/typescript/module/__tests__/theme.test.d.ts.map +1 -0
  143. package/lib/typescript/module/accordion/Accordion.d.ts +7 -0
  144. package/lib/typescript/module/accordion/Accordion.d.ts.map +1 -0
  145. package/lib/typescript/module/accordion/index.d.ts +3 -0
  146. package/lib/typescript/module/accordion/index.d.ts.map +1 -0
  147. package/lib/typescript/module/accordion/types.d.ts +38 -0
  148. package/lib/typescript/module/accordion/types.d.ts.map +1 -0
  149. package/lib/typescript/module/avatar/Avatar.d.ts.map +1 -1
  150. package/lib/typescript/module/avatar/types.d.ts +4 -0
  151. package/lib/typescript/module/avatar/types.d.ts.map +1 -1
  152. package/lib/typescript/module/badge/Badge.d.ts +4 -0
  153. package/lib/typescript/module/badge/Badge.d.ts.map +1 -0
  154. package/lib/typescript/module/badge/index.d.ts +3 -0
  155. package/lib/typescript/module/badge/index.d.ts.map +1 -0
  156. package/lib/typescript/module/badge/types.d.ts +29 -0
  157. package/lib/typescript/module/badge/types.d.ts.map +1 -0
  158. package/lib/typescript/module/bottom-sheet/index.d.ts.map +1 -1
  159. package/lib/typescript/module/card/Card.d.ts +9 -0
  160. package/lib/typescript/module/card/Card.d.ts.map +1 -0
  161. package/lib/typescript/module/card/index.d.ts +3 -0
  162. package/lib/typescript/module/card/index.d.ts.map +1 -0
  163. package/lib/typescript/module/card/types.d.ts +34 -0
  164. package/lib/typescript/module/card/types.d.ts.map +1 -0
  165. package/lib/typescript/module/checkbox/Checkbox.d.ts +4 -0
  166. package/lib/typescript/module/checkbox/Checkbox.d.ts.map +1 -0
  167. package/lib/typescript/module/checkbox/index.d.ts +3 -0
  168. package/lib/typescript/module/checkbox/index.d.ts.map +1 -0
  169. package/lib/typescript/module/checkbox/types.d.ts +25 -0
  170. package/lib/typescript/module/checkbox/types.d.ts.map +1 -0
  171. package/lib/typescript/module/chip/Chip.d.ts +4 -0
  172. package/lib/typescript/module/chip/Chip.d.ts.map +1 -0
  173. package/lib/typescript/module/chip/index.d.ts +3 -0
  174. package/lib/typescript/module/chip/index.d.ts.map +1 -0
  175. package/lib/typescript/module/chip/types.d.ts +31 -0
  176. package/lib/typescript/module/chip/types.d.ts.map +1 -0
  177. package/lib/typescript/module/index.d.ts +6 -0
  178. package/lib/typescript/module/index.d.ts.map +1 -1
  179. package/lib/typescript/module/tabs/Tabs.d.ts +6 -0
  180. package/lib/typescript/module/tabs/Tabs.d.ts.map +1 -0
  181. package/lib/typescript/module/tabs/index.d.ts +3 -0
  182. package/lib/typescript/module/tabs/index.d.ts.map +1 -0
  183. package/lib/typescript/module/tabs/types.d.ts +34 -0
  184. package/lib/typescript/module/tabs/types.d.ts.map +1 -0
  185. package/package.json +79 -1
  186. package/src/__tests__/BloomThemeProvider.test.tsx +160 -0
  187. package/src/__tests__/BottomSheet.test.tsx +109 -0
  188. package/src/__tests__/Button.test.tsx +98 -0
  189. package/src/__tests__/theme.test.ts +148 -0
  190. package/src/accordion/Accordion.tsx +261 -0
  191. package/src/accordion/index.ts +8 -0
  192. package/src/accordion/types.ts +42 -0
  193. package/src/avatar/Avatar.tsx +16 -6
  194. package/src/avatar/types.ts +4 -0
  195. package/src/badge/Badge.tsx +151 -0
  196. package/src/badge/index.ts +8 -0
  197. package/src/badge/types.ts +30 -0
  198. package/src/bottom-sheet/index.tsx +30 -11
  199. package/src/card/Card.tsx +197 -0
  200. package/src/card/index.ts +10 -0
  201. package/src/card/types.ts +40 -0
  202. package/src/checkbox/Checkbox.tsx +166 -0
  203. package/src/checkbox/index.ts +2 -0
  204. package/src/checkbox/types.ts +26 -0
  205. package/src/chip/Chip.tsx +156 -0
  206. package/src/chip/index.ts +2 -0
  207. package/src/chip/types.ts +32 -0
  208. package/src/index.ts +8 -0
  209. package/src/tabs/Tabs.tsx +218 -0
  210. package/src/tabs/index.ts +2 -0
  211. package/src/tabs/types.ts +37 -0
@@ -0,0 +1,148 @@
1
+ import {
2
+ APP_COLOR_PRESETS,
3
+ APP_COLOR_NAMES,
4
+ HEX_TO_APP_COLOR,
5
+ hexToAppColorName,
6
+ } from '../theme/color-presets';
7
+ import type { Theme, ThemeColors, ThemeMode } from '../theme/types';
8
+
9
+ describe('Theme system', () => {
10
+ describe('color presets', () => {
11
+ it('has all named presets defined', () => {
12
+ for (const name of APP_COLOR_NAMES) {
13
+ expect(APP_COLOR_PRESETS[name]).toBeDefined();
14
+ expect(APP_COLOR_PRESETS[name]!.name).toBe(name);
15
+ expect(APP_COLOR_PRESETS[name]!.hex).toMatch(/^#[0-9a-f]{6}$/i);
16
+ }
17
+ });
18
+
19
+ it('each preset has both light and dark color variables', () => {
20
+ const requiredVars = [
21
+ '--background',
22
+ '--foreground',
23
+ '--surface',
24
+ '--primary',
25
+ '--border',
26
+ '--muted',
27
+ '--muted-foreground',
28
+ '--destructive',
29
+ '--input',
30
+ ];
31
+
32
+ for (const name of APP_COLOR_NAMES) {
33
+ const preset = APP_COLOR_PRESETS[name]!;
34
+ for (const v of requiredVars) {
35
+ expect(preset.light[v]).toBeDefined();
36
+ expect(preset.dark[v]).toBeDefined();
37
+ }
38
+ }
39
+ });
40
+
41
+ it('light and dark variants have different values', () => {
42
+ for (const name of APP_COLOR_NAMES) {
43
+ const preset = APP_COLOR_PRESETS[name]!;
44
+ // Background should differ between light and dark
45
+ expect(preset.light['--background']).not.toBe(preset.dark['--background']);
46
+ }
47
+ });
48
+ });
49
+
50
+ describe('hexToAppColorName', () => {
51
+ it('maps known hex values to correct color names', () => {
52
+ expect(hexToAppColorName('#005c67')).toBe('teal');
53
+ expect(hexToAppColorName('#1d9bf0')).toBe('blue');
54
+ expect(hexToAppColorName('#10b981')).toBe('green');
55
+ expect(hexToAppColorName('#ef4444')).toBe('red');
56
+ expect(hexToAppColorName('#c46ede')).toBe('oxy');
57
+ });
58
+
59
+ it('is case-insensitive', () => {
60
+ expect(hexToAppColorName('#005C67')).toBe('teal');
61
+ expect(hexToAppColorName('#1D9BF0')).toBe('blue');
62
+ });
63
+
64
+ it('defaults to teal for unknown hex values', () => {
65
+ expect(hexToAppColorName('#000000')).toBe('teal');
66
+ expect(hexToAppColorName('#ffffff')).toBe('teal');
67
+ expect(hexToAppColorName('#123456')).toBe('teal');
68
+ });
69
+ });
70
+
71
+ describe('HEX_TO_APP_COLOR mapping', () => {
72
+ it('has a hex entry for every named preset', () => {
73
+ const mappedNames = new Set(Object.values(HEX_TO_APP_COLOR));
74
+ for (const name of APP_COLOR_NAMES) {
75
+ expect(mappedNames.has(name)).toBe(true);
76
+ }
77
+ });
78
+
79
+ it('hex keys match the preset hex values', () => {
80
+ for (const [hex, name] of Object.entries(HEX_TO_APP_COLOR)) {
81
+ const preset = APP_COLOR_PRESETS[name];
82
+ expect(preset).toBeDefined();
83
+ expect(preset!.hex.toLowerCase()).toBe(hex.toLowerCase());
84
+ }
85
+ });
86
+ });
87
+
88
+ describe('Theme type shape', () => {
89
+ it('supports light and dark modes with isDark/isLight flags', () => {
90
+ const lightTheme: Theme = {
91
+ mode: 'light',
92
+ isDark: false,
93
+ isLight: true,
94
+ colors: createMockColors(),
95
+ };
96
+ expect(lightTheme.isDark).toBe(false);
97
+ expect(lightTheme.isLight).toBe(true);
98
+
99
+ const darkTheme: Theme = {
100
+ mode: 'dark',
101
+ isDark: true,
102
+ isLight: false,
103
+ colors: createMockColors(),
104
+ };
105
+ expect(darkTheme.isDark).toBe(true);
106
+ expect(darkTheme.isLight).toBe(false);
107
+ });
108
+
109
+ it('ThemeMode includes all expected variants', () => {
110
+ const modes: ThemeMode[] = ['light', 'dark', 'system', 'adaptive'];
111
+ expect(modes).toHaveLength(4);
112
+ });
113
+ });
114
+ });
115
+
116
+ function createMockColors(): ThemeColors {
117
+ return {
118
+ background: '#fff',
119
+ backgroundSecondary: '#f5f5f5',
120
+ backgroundTertiary: '#eee',
121
+ text: '#000',
122
+ textSecondary: '#666',
123
+ textTertiary: '#999',
124
+ border: '#ccc',
125
+ borderLight: '#ddd',
126
+ primary: '#005c67',
127
+ primaryLight: '#e0f7fa',
128
+ primaryDark: '#003d44',
129
+ secondary: '#005c67',
130
+ tint: '#005c67',
131
+ icon: '#666',
132
+ iconActive: '#005c67',
133
+ success: '#10B981',
134
+ error: '#EF4444',
135
+ warning: '#F59E0B',
136
+ info: '#3B82F6',
137
+ primarySubtle: '#e0f7fa',
138
+ primarySubtleForeground: '#003d44',
139
+ negative: '#B91C1C',
140
+ negativeForeground: '#FFFFFF',
141
+ negativeSubtle: '#fef2f2',
142
+ negativeSubtleForeground: '#B91C1C',
143
+ contrast50: '#f5f5f5',
144
+ card: '#f5f5f5',
145
+ shadow: 'rgba(0,0,0,0.1)',
146
+ overlay: 'rgba(0,0,0,0.5)',
147
+ };
148
+ }
@@ -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
+ }
@@ -37,18 +37,20 @@ function SquircleImage({
37
37
  fallbackSource,
38
38
  size,
39
39
  fallbackColor,
40
+ placeholderIcon,
40
41
  onError,
41
42
  }: {
42
43
  uri?: string;
43
44
  fallbackSource?: AvatarProps['fallbackSource'];
44
45
  size: number;
45
46
  fallbackColor: string;
47
+ placeholderIcon?: React.ReactNode;
46
48
  onError: () => void;
47
49
  }) {
48
50
  const svg = getSvgModule();
49
51
  if (!svg) {
50
52
  // Fallback to circle if react-native-svg is not installed
51
- return <CircleFallback size={size} fallbackColor={fallbackColor} />;
53
+ return <CircleFallback size={size} fallbackColor={fallbackColor} icon={placeholderIcon} />;
52
54
  }
53
55
 
54
56
  const { default: Svg, Defs, ClipPath, Path, Image: SvgImage } = svg;
@@ -56,7 +58,7 @@ function SquircleImage({
56
58
 
57
59
  const href = uri ? { uri } : fallbackSource;
58
60
  if (!href) {
59
- return <CircleFallback size={size} fallbackColor={fallbackColor} />;
61
+ return <CircleFallback size={size} fallbackColor={fallbackColor} icon={placeholderIcon} />;
60
62
  }
61
63
 
62
64
  return (
@@ -87,7 +89,7 @@ function SquircleImage({
87
89
  );
88
90
  }
89
91
 
90
- function CircleFallback({ size, fallbackColor }: { size: number; fallbackColor: string }) {
92
+ function CircleFallback({ size, fallbackColor, icon }: { size: number; fallbackColor: string; icon?: React.ReactNode }) {
91
93
  const radius = size / 2;
92
94
  return (
93
95
  <View
@@ -96,8 +98,13 @@ function CircleFallback({ size, fallbackColor }: { size: number; fallbackColor:
96
98
  height: size,
97
99
  borderRadius: radius,
98
100
  backgroundColor: fallbackColor,
101
+ justifyContent: 'center',
102
+ alignItems: 'center',
103
+ overflow: 'hidden',
99
104
  }}
100
- />
105
+ >
106
+ {icon ?? null}
107
+ </View>
101
108
  );
102
109
  }
103
110
 
@@ -111,13 +118,15 @@ const AvatarComponent: React.FC<AvatarProps> = ({
111
118
  shape = 'circle',
112
119
  style,
113
120
  imageStyle,
121
+ placeholderColor,
122
+ placeholderIcon,
114
123
  onPress,
115
124
  testID,
116
125
  }) => {
117
126
  const [errored, setErrored] = useState(false);
118
127
  const theme = useTheme();
119
128
  const radius = size / 2;
120
- const fallbackColor = theme.colors.backgroundTertiary;
129
+ const fallbackColor = placeholderColor || theme.colors.backgroundTertiary;
121
130
 
122
131
  // Reset error state when source changes (e.g., list item recycling
123
132
  // or async URL resolution replacing an initial file ID).
@@ -168,6 +177,7 @@ const AvatarComponent: React.FC<AvatarProps> = ({
168
177
  fallbackSource={fallbackSource}
169
178
  size={size}
170
179
  fallbackColor={fallbackColor}
180
+ placeholderIcon={placeholderIcon}
171
181
  onError={() => setErrored(true)}
172
182
  />
173
183
  ) : (
@@ -180,7 +190,7 @@ const AvatarComponent: React.FC<AvatarProps> = ({
180
190
  style={[StyleSheet.absoluteFillObject, { borderRadius: radius }, imageStyle]}
181
191
  />
182
192
  ) : (
183
- <CircleFallback size={size} fallbackColor={fallbackColor} />
193
+ <CircleFallback size={size} fallbackColor={fallbackColor} icon={placeholderIcon} />
184
194
  )}
185
195
  </View>
186
196
  )}
@@ -26,6 +26,10 @@ export interface AvatarProps {
26
26
  style?: StyleProp<ViewStyle>;
27
27
  /** Image style (circle shape only) */
28
28
  imageStyle?: StyleProp<ImageStyle>;
29
+ /** Custom background color for the placeholder circle (overrides theme default) */
30
+ placeholderColor?: string;
31
+ /** Custom icon rendered inside the placeholder circle when no image is available */
32
+ placeholderIcon?: ReactNode;
29
33
  /** Press handler — wraps avatar in TouchableOpacity when provided */
30
34
  onPress?: () => void;
31
35
  testID?: string;
@@ -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';