react-native-pro-accordion 1.0.1

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 (99) hide show
  1. package/README.md +476 -0
  2. package/index.js +3 -0
  3. package/lib/commonjs/components/Accordion.js +130 -0
  4. package/lib/commonjs/components/Accordion.js.map +1 -0
  5. package/lib/commonjs/components/AccordionContent.js +125 -0
  6. package/lib/commonjs/components/AccordionContent.js.map +1 -0
  7. package/lib/commonjs/components/AccordionItem.js +146 -0
  8. package/lib/commonjs/components/AccordionItem.js.map +1 -0
  9. package/lib/commonjs/components/AccordionSeparator.js +159 -0
  10. package/lib/commonjs/components/AccordionSeparator.js.map +1 -0
  11. package/lib/commonjs/core/AccordionContext.js +82 -0
  12. package/lib/commonjs/core/AccordionContext.js.map +1 -0
  13. package/lib/commonjs/core/AccordionRef.js +64 -0
  14. package/lib/commonjs/core/AccordionRef.js.map +1 -0
  15. package/lib/commonjs/core/types.js +6 -0
  16. package/lib/commonjs/core/types.js.map +1 -0
  17. package/lib/commonjs/hooks/useAccordion.js +18 -0
  18. package/lib/commonjs/hooks/useAccordion.js.map +1 -0
  19. package/lib/commonjs/hooks/useAccordionAccessibility.js +39 -0
  20. package/lib/commonjs/hooks/useAccordionAccessibility.js.map +1 -0
  21. package/lib/commonjs/hooks/useAccordionAnimation.js +135 -0
  22. package/lib/commonjs/hooks/useAccordionAnimation.js.map +1 -0
  23. package/lib/commonjs/hooks/useAccordionKeyboard.js +43 -0
  24. package/lib/commonjs/hooks/useAccordionKeyboard.js.map +1 -0
  25. package/lib/commonjs/hooks/useAccordionState.js +96 -0
  26. package/lib/commonjs/hooks/useAccordionState.js.map +1 -0
  27. package/lib/commonjs/index.js +176 -0
  28. package/lib/commonjs/index.js.map +1 -0
  29. package/lib/commonjs/package.json +1 -0
  30. package/lib/commonjs/theme/ThemeContext.js +48 -0
  31. package/lib/commonjs/theme/ThemeContext.js.map +1 -0
  32. package/lib/commonjs/theme/defaultTheme.js +79 -0
  33. package/lib/commonjs/theme/defaultTheme.js.map +1 -0
  34. package/lib/commonjs/theme/types.js +6 -0
  35. package/lib/commonjs/theme/types.js.map +1 -0
  36. package/lib/commonjs/utils/animations.js +41 -0
  37. package/lib/commonjs/utils/animations.js.map +1 -0
  38. package/lib/commonjs/utils/layout.js +155 -0
  39. package/lib/commonjs/utils/layout.js.map +1 -0
  40. package/lib/commonjs/utils/validators.js +26 -0
  41. package/lib/commonjs/utils/validators.js.map +1 -0
  42. package/lib/module/components/Accordion.js +125 -0
  43. package/lib/module/components/Accordion.js.map +1 -0
  44. package/lib/module/components/AccordionContent.js +120 -0
  45. package/lib/module/components/AccordionContent.js.map +1 -0
  46. package/lib/module/components/AccordionItem.js +141 -0
  47. package/lib/module/components/AccordionItem.js.map +1 -0
  48. package/lib/module/components/AccordionSeparator.js +155 -0
  49. package/lib/module/components/AccordionSeparator.js.map +1 -0
  50. package/lib/module/core/AccordionContext.js +75 -0
  51. package/lib/module/core/AccordionContext.js.map +1 -0
  52. package/lib/module/core/AccordionRef.js +59 -0
  53. package/lib/module/core/AccordionRef.js.map +1 -0
  54. package/lib/module/core/types.js +136 -0
  55. package/lib/module/core/types.js.map +1 -0
  56. package/lib/module/hooks/useAccordion.js +14 -0
  57. package/lib/module/hooks/useAccordion.js.map +1 -0
  58. package/lib/module/hooks/useAccordionAccessibility.js +34 -0
  59. package/lib/module/hooks/useAccordionAccessibility.js.map +1 -0
  60. package/lib/module/hooks/useAccordionAnimation.js +131 -0
  61. package/lib/module/hooks/useAccordionAnimation.js.map +1 -0
  62. package/lib/module/hooks/useAccordionKeyboard.js +38 -0
  63. package/lib/module/hooks/useAccordionKeyboard.js.map +1 -0
  64. package/lib/module/hooks/useAccordionState.js +92 -0
  65. package/lib/module/hooks/useAccordionState.js.map +1 -0
  66. package/lib/module/index.js +43 -0
  67. package/lib/module/index.js.map +1 -0
  68. package/lib/module/theme/ThemeContext.js +41 -0
  69. package/lib/module/theme/ThemeContext.js.map +1 -0
  70. package/lib/module/theme/defaultTheme.js +75 -0
  71. package/lib/module/theme/defaultTheme.js.map +1 -0
  72. package/lib/module/theme/types.js +66 -0
  73. package/lib/module/theme/types.js.map +1 -0
  74. package/lib/module/utils/animations.js +35 -0
  75. package/lib/module/utils/animations.js.map +1 -0
  76. package/lib/module/utils/layout.js +147 -0
  77. package/lib/module/utils/layout.js.map +1 -0
  78. package/lib/module/utils/validators.js +20 -0
  79. package/lib/module/utils/validators.js.map +1 -0
  80. package/package.json +25 -0
  81. package/src/components/Accordion.js +164 -0
  82. package/src/components/AccordionContent.js +149 -0
  83. package/src/components/AccordionItem.js +146 -0
  84. package/src/components/AccordionSeparator.js +168 -0
  85. package/src/core/AccordionContext.js +86 -0
  86. package/src/core/AccordionRef.js +69 -0
  87. package/src/core/types.js +133 -0
  88. package/src/hooks/useAccordion.js +14 -0
  89. package/src/hooks/useAccordionAccessibility.js +30 -0
  90. package/src/hooks/useAccordionAnimation.js +165 -0
  91. package/src/hooks/useAccordionKeyboard.js +38 -0
  92. package/src/hooks/useAccordionState.js +119 -0
  93. package/src/index.js +56 -0
  94. package/src/theme/ThemeContext.js +40 -0
  95. package/src/theme/defaultTheme.js +73 -0
  96. package/src/theme/types.js +63 -0
  97. package/src/utils/animations.js +38 -0
  98. package/src/utils/layout.js +138 -0
  99. package/src/utils/validators.js +20 -0
@@ -0,0 +1,164 @@
1
+ import React, {
2
+ useRef,
3
+ useState,
4
+ useCallback,
5
+ useImperativeHandle,
6
+ forwardRef,
7
+ } from "react";
8
+ import { View, StyleSheet } from "react-native";
9
+ import { AccordionProvider } from "../core/AccordionContext";
10
+ import { AccordionRefImpl } from "../core/AccordionRef";
11
+ import { defaultLightTheme, defaultDarkTheme } from "../theme/defaultTheme";
12
+
13
+ export const Accordion = forwardRef((props, ref) => {
14
+ const {
15
+ children,
16
+ value: controlledValue,
17
+ defaultValue = [],
18
+ onChange,
19
+ mode = "single",
20
+ readOnly = false,
21
+ animation = { type: "timing", duration: 300 },
22
+ renderIcon,
23
+ iconPosition = "right",
24
+ showSeparators = false,
25
+ separatorComponent,
26
+ separatorStyle,
27
+ stickyHeaders = false,
28
+ lazyRender = false,
29
+ theme: customTheme,
30
+ darkMode = false,
31
+ accessibilityLabel,
32
+ testID,
33
+ onOpen,
34
+ onClose,
35
+ onToggle,
36
+ onAnimationStart,
37
+ onAnimationEnd,
38
+ onStateChange,
39
+ } = props;
40
+
41
+ // State management
42
+ const isControlled = controlledValue !== undefined;
43
+ const [internalExpandedValues, setInternalExpandedValues] =
44
+ useState(defaultValue);
45
+
46
+ const expandedValues = isControlled
47
+ ? controlledValue
48
+ : internalExpandedValues;
49
+ const expandedValuesRef = useRef(expandedValues);
50
+ expandedValuesRef.current = expandedValues;
51
+
52
+ const setExpandedValues = useCallback(
53
+ (newValues) => {
54
+ if (!isControlled) {
55
+ setInternalExpandedValues(newValues);
56
+ }
57
+ onChange?.(newValues);
58
+ onStateChange?.(newValues);
59
+ },
60
+ [isControlled, onChange, onStateChange],
61
+ );
62
+
63
+ const handleToggleItem = useCallback(
64
+ (value) => {
65
+ const isCurrentlyExpanded = expandedValues.includes(value);
66
+ let newValues;
67
+
68
+ if (isCurrentlyExpanded) {
69
+ newValues = expandedValues.filter((v) => v !== value);
70
+ onClose?.(value);
71
+ onToggle?.(value, false);
72
+ } else {
73
+ if (mode === "single") {
74
+ newValues = [value];
75
+ } else {
76
+ newValues = [...expandedValues, value];
77
+ }
78
+ onOpen?.(value);
79
+ onToggle?.(value, true);
80
+ }
81
+
82
+ setExpandedValues(newValues);
83
+ },
84
+ [expandedValues, mode, setExpandedValues, onOpen, onClose, onToggle],
85
+ );
86
+
87
+ // Expose ref methods
88
+ useImperativeHandle(
89
+ ref,
90
+ () =>
91
+ new AccordionRefImpl(
92
+ expandedValuesRef,
93
+ setExpandedValues,
94
+ mode,
95
+ readOnly,
96
+ ),
97
+ [mode, readOnly, setExpandedValues],
98
+ );
99
+
100
+ // Theme merging
101
+ const baseTheme = darkMode ? defaultDarkTheme : defaultLightTheme;
102
+ const theme = customTheme
103
+ ? {
104
+ ...baseTheme,
105
+ ...customTheme,
106
+ colors: { ...baseTheme.colors, ...customTheme.colors },
107
+ }
108
+ : baseTheme;
109
+
110
+ return (
111
+ <AccordionProvider
112
+ mode={mode}
113
+ readOnly={readOnly}
114
+ animationConfig={animation}
115
+ renderIcon={renderIcon}
116
+ iconPosition={iconPosition}
117
+ showSeparators={showSeparators}
118
+ lazyRender={lazyRender}
119
+ theme={theme}
120
+ darkMode={darkMode}
121
+ expandedValues={expandedValues}
122
+ onToggleItem={handleToggleItem}
123
+ >
124
+ <View
125
+ style={styles.container}
126
+ accessible={!!accessibilityLabel}
127
+ accessibilityLabel={accessibilityLabel}
128
+ testID={testID}
129
+ >
130
+ {React.Children.map(children, (child, index) => {
131
+ const isLast = index === React.Children.count(children) - 1;
132
+ return (
133
+ <React.Fragment key={index}>
134
+ {child}
135
+ {showSeparators &&
136
+ !isLast &&
137
+ (separatorComponent || (
138
+ <View
139
+ style={[
140
+ styles.defaultSeparator,
141
+ separatorStyle,
142
+ { backgroundColor: theme.colors.border },
143
+ ]}
144
+ />
145
+ ))}
146
+ </React.Fragment>
147
+ );
148
+ })}
149
+ </View>
150
+ </AccordionProvider>
151
+ );
152
+ });
153
+
154
+ const styles = StyleSheet.create({
155
+ container: {
156
+ width: "100%",
157
+ },
158
+ defaultSeparator: {
159
+ height: 1,
160
+ width: "100%",
161
+ },
162
+ });
163
+
164
+ Accordion.displayName = "Accordion";
@@ -0,0 +1,149 @@
1
+ import React, { useRef, useEffect, useState, memo } from "react";
2
+ import { View, StyleSheet } from "react-native";
3
+ import Animated, {
4
+ useSharedValue,
5
+ useAnimatedStyle,
6
+ withTiming,
7
+ withSpring,
8
+ Easing,
9
+ runOnJS,
10
+ } from "react-native-reanimated";
11
+
12
+ export const AccordionContent = memo((props) => {
13
+ const {
14
+ expanded,
15
+ children,
16
+ animationConfig,
17
+ lazyRender = false,
18
+ theme,
19
+ contentStyle,
20
+ onAnimationStart,
21
+ onAnimationEnd,
22
+ } = props;
23
+
24
+ const [contentHeight, setContentHeight] = useState(0);
25
+ const [shouldRender, setShouldRender] = useState(expanded || !lazyRender);
26
+ const height = useSharedValue(expanded ? contentHeight : 0);
27
+ const opacity = useSharedValue(expanded ? 1 : 0);
28
+
29
+ useEffect(() => {
30
+ if (expanded && lazyRender && !shouldRender) {
31
+ setShouldRender(true);
32
+ }
33
+ }, [expanded, lazyRender, shouldRender]);
34
+
35
+ useEffect(() => {
36
+ if (onAnimationStart) {
37
+ runOnJS(onAnimationStart)();
38
+ }
39
+
40
+ const animateHeight = () => {
41
+ const toValue = expanded ? contentHeight : 0;
42
+
43
+ if (animationConfig.type === "spring") {
44
+ height.value = withSpring(
45
+ toValue,
46
+ {
47
+ damping: animationConfig.damping ?? 10,
48
+ mass: animationConfig.mass ?? 1,
49
+ stiffness: animationConfig.stiffness ?? 100,
50
+ overshootClamping: animationConfig.overshootClamping ?? false,
51
+ restSpeedThreshold: animationConfig.restSpeedThreshold ?? 0.01,
52
+ restDisplacementThreshold:
53
+ animationConfig.restDisplacementThreshold ?? 0.01,
54
+ },
55
+ (finished) => {
56
+ if (finished && onAnimationEnd) {
57
+ runOnJS(onAnimationEnd)();
58
+ }
59
+ if (!expanded && lazyRender && !finished) {
60
+ runOnJS(setShouldRender)(false);
61
+ }
62
+ },
63
+ );
64
+ } else {
65
+ height.value = withTiming(
66
+ toValue,
67
+ {
68
+ duration: animationConfig.duration ?? 300,
69
+ easing: animationConfig.easing ?? Easing.inOut(Easing.ease),
70
+ },
71
+ (finished) => {
72
+ if (finished && onAnimationEnd) {
73
+ runOnJS(onAnimationEnd)();
74
+ }
75
+ if (!expanded && lazyRender && !finished) {
76
+ runOnJS(setShouldRender)(false);
77
+ }
78
+ },
79
+ );
80
+ }
81
+ };
82
+
83
+ animateHeight();
84
+
85
+ // Animate opacity for fade effect
86
+ if (animationConfig.type === "spring") {
87
+ opacity.value = withSpring(expanded ? 1 : 0, {
88
+ damping: animationConfig.damping ?? 10,
89
+ mass: animationConfig.mass ?? 1,
90
+ stiffness: animationConfig.stiffness ?? 100,
91
+ });
92
+ } else {
93
+ opacity.value = withTiming(expanded ? 1 : 0, {
94
+ duration: (animationConfig.duration ?? 300) * 0.6,
95
+ easing: animationConfig.easing ?? Easing.inOut(Easing.ease),
96
+ });
97
+ }
98
+ }, [expanded, contentHeight, animationConfig]);
99
+
100
+ const animatedStyle = useAnimatedStyle(() => ({
101
+ height: height.value,
102
+ opacity: opacity.value,
103
+ overflow: "hidden",
104
+ }));
105
+
106
+ const onLayout = (event) => {
107
+ const newHeight = event.nativeEvent.layout.height;
108
+ if (newHeight !== contentHeight && newHeight > 0) {
109
+ setContentHeight(newHeight);
110
+ if (expanded && height.value === 0) {
111
+ height.value = newHeight;
112
+ }
113
+ }
114
+ };
115
+
116
+ if (!shouldRender) return null;
117
+
118
+ return (
119
+ <Animated.View style={[styles.container, animatedStyle]}>
120
+ <View
121
+ onLayout={onLayout}
122
+ style={[
123
+ styles.content,
124
+ contentStyle,
125
+ { backgroundColor: theme?.colors?.background },
126
+ ]}
127
+ >
128
+ {children}
129
+ </View>
130
+ </Animated.View>
131
+ );
132
+ });
133
+
134
+ const styles = StyleSheet.create({
135
+ container: {
136
+ width: "100%",
137
+ overflow: "hidden",
138
+ },
139
+ content: {
140
+ position: "absolute",
141
+ top: 0,
142
+ left: 0,
143
+ right: 0,
144
+ paddingHorizontal: 16,
145
+ paddingVertical: 12,
146
+ },
147
+ });
148
+
149
+ AccordionContent.displayName = "AccordionContent";
@@ -0,0 +1,146 @@
1
+ import React, { useEffect, useCallback, memo } from "react";
2
+ import { View, TouchableOpacity, StyleSheet } from "react-native";
3
+ import { useAccordion } from "../core/AccordionContext";
4
+ import { AccordionContent } from "./AccordionContent";
5
+ import { useAccordionAccessibility } from "../hooks/useAccordionAccessibility";
6
+ import { useAccordionKeyboard } from "../hooks/useAccordionKeyboard";
7
+
8
+ export const AccordionItem = memo((props) => {
9
+ const {
10
+ value,
11
+ header,
12
+ children,
13
+ disabled = false,
14
+ style,
15
+ headerStyle,
16
+ contentStyle,
17
+ data,
18
+ } = props;
19
+
20
+ const accordion = useAccordion();
21
+ const {
22
+ mode,
23
+ readOnly,
24
+ renderIcon,
25
+ iconPosition,
26
+ lazyRender,
27
+ theme,
28
+ darkMode,
29
+ registerItem,
30
+ unregisterItem,
31
+ toggleItem,
32
+ isExpanded,
33
+ } = accordion;
34
+
35
+ const expanded = isExpanded(value);
36
+
37
+ // Register/unregister item
38
+ useEffect(() => {
39
+ registerItem(value, disabled, data);
40
+ return () => unregisterItem(value);
41
+ }, [value, disabled, data, registerItem, unregisterItem]);
42
+
43
+ const handlePress = useCallback(() => {
44
+ if (readOnly || disabled) return;
45
+ toggleItem(value);
46
+ }, [readOnly, disabled, toggleItem, value]);
47
+
48
+ // Accessibility
49
+ const { accessibilityProps } = useAccordionAccessibility({
50
+ expanded,
51
+ disabled: disabled || readOnly,
52
+ label: `Toggle accordion item ${value}`,
53
+ });
54
+
55
+ // Keyboard navigation
56
+ const { keyboardProps } = useAccordionKeyboard({
57
+ onPress: handlePress,
58
+ disabled: disabled || readOnly,
59
+ });
60
+
61
+ // Render header content
62
+ const renderHeaderContent = () => {
63
+ if (typeof header === "function") {
64
+ return header({ expanded, disabled });
65
+ }
66
+ return header;
67
+ };
68
+
69
+ // Render icon
70
+ const renderIconComponent = () => {
71
+ if (!renderIcon) return null;
72
+
73
+ const iconElement = renderIcon({ expanded, disabled });
74
+ if (!iconElement) return null;
75
+
76
+ return (
77
+ <View
78
+ style={iconPosition === "left" ? styles.leftIcon : styles.rightIcon}
79
+ >
80
+ {iconElement}
81
+ </View>
82
+ );
83
+ };
84
+
85
+ const headerContent = (
86
+ <TouchableOpacity
87
+ activeOpacity={0.7}
88
+ onPress={handlePress}
89
+ disabled={disabled || readOnly}
90
+ style={[
91
+ styles.header,
92
+ headerStyle,
93
+ disabled && styles.disabledHeader,
94
+ { backgroundColor: theme?.colors?.surface },
95
+ ]}
96
+ {...accessibilityProps}
97
+ {...keyboardProps}
98
+ >
99
+ {iconPosition === "left" && renderIconComponent()}
100
+ <View style={styles.headerTextContainer}>{renderHeaderContent()}</View>
101
+ {iconPosition === "right" && renderIconComponent()}
102
+ </TouchableOpacity>
103
+ );
104
+
105
+ return (
106
+ <View style={[styles.container, style]}>
107
+ {headerContent}
108
+ <AccordionContent
109
+ expanded={expanded}
110
+ animationConfig={accordion.animationConfig}
111
+ lazyRender={lazyRender}
112
+ theme={theme}
113
+ contentStyle={contentStyle}
114
+ >
115
+ {typeof children === "function" ? children({ expanded }) : children}
116
+ </AccordionContent>
117
+ </View>
118
+ );
119
+ });
120
+
121
+ const styles = StyleSheet.create({
122
+ container: {
123
+ width: "100%",
124
+ },
125
+ header: {
126
+ flexDirection: "row",
127
+ alignItems: "center",
128
+ minHeight: 48,
129
+ paddingHorizontal: 16,
130
+ paddingVertical: 12,
131
+ },
132
+ disabledHeader: {
133
+ opacity: 0.5,
134
+ },
135
+ headerTextContainer: {
136
+ flex: 1,
137
+ },
138
+ leftIcon: {
139
+ marginRight: 12,
140
+ },
141
+ rightIcon: {
142
+ marginLeft: 12,
143
+ },
144
+ });
145
+
146
+ AccordionItem.displayName = "AccordionItem";
@@ -0,0 +1,168 @@
1
+ import React, { memo } from "react";
2
+ import { View, StyleSheet } from "react-native";
3
+
4
+ /**
5
+ * Separator component for accordion items
6
+ * @param {Object} props
7
+ * @param {Object} props.style - Custom style for separator
8
+ * @param {string} props.color - Color of separator
9
+ * @param {number} props.thickness - Thickness of separator
10
+ * @param {number} props.marginVertical - Vertical margin
11
+ * @param {boolean} props.hasIcon - Whether adjacent item has icon
12
+ * @param {string} props.iconPosition - Position of icon ('left' or 'right')
13
+ */
14
+ export const AccordionSeparator = memo(
15
+ ({
16
+ style,
17
+ color = "#E0E0E0",
18
+ thickness = 1,
19
+ marginVertical = 8,
20
+ hasIcon = false,
21
+ iconPosition = "right",
22
+ }) => {
23
+ const getSeparatorStyle = () => {
24
+ const baseStyle = {
25
+ height: thickness,
26
+ backgroundColor: color,
27
+ marginVertical,
28
+ };
29
+
30
+ // Adjust indentation if there's an icon
31
+ if (hasIcon) {
32
+ if (iconPosition === "left") {
33
+ return {
34
+ ...baseStyle,
35
+ marginLeft: 40, // Account for icon width + padding
36
+ };
37
+ }
38
+ }
39
+
40
+ return baseStyle;
41
+ };
42
+
43
+ return <View style={[styles.container, getSeparatorStyle(), style]} />;
44
+ },
45
+ );
46
+
47
+ /**
48
+ * Separator with indentation for nested accordions
49
+ */
50
+ export const NestedAccordionSeparator = memo(
51
+ ({
52
+ style,
53
+ color = "#E0E0E0",
54
+ thickness = 1,
55
+ marginVertical = 8,
56
+ nestingLevel = 0,
57
+ }) => {
58
+ const getNestedStyle = () => {
59
+ const baseStyle = {
60
+ height: thickness,
61
+ backgroundColor: color,
62
+ marginVertical,
63
+ marginLeft: nestingLevel * 16, // Indent based on nesting level
64
+ };
65
+ return baseStyle;
66
+ };
67
+
68
+ return <View style={[styles.container, getNestedStyle(), style]} />;
69
+ },
70
+ );
71
+
72
+ /**
73
+ * Custom separator with dashed style
74
+ */
75
+ export const DashedSeparator = memo(
76
+ ({
77
+ style,
78
+ color = "#E0E0E0",
79
+ thickness = 1,
80
+ dashLength = 5,
81
+ dashGap = 3,
82
+ marginVertical = 8,
83
+ }) => {
84
+ return (
85
+ <View style={[styles.dashedContainer, { marginVertical }, style]}>
86
+ <View
87
+ style={{
88
+ height: thickness,
89
+ backgroundColor: color,
90
+ width: dashLength,
91
+ marginRight: dashGap,
92
+ }}
93
+ />
94
+ <View
95
+ style={{
96
+ height: thickness,
97
+ backgroundColor: color,
98
+ width: dashLength,
99
+ marginRight: dashGap,
100
+ }}
101
+ />
102
+ <View
103
+ style={{
104
+ height: thickness,
105
+ backgroundColor: color,
106
+ width: dashLength,
107
+ marginRight: dashGap,
108
+ }}
109
+ />
110
+ <View
111
+ style={{
112
+ height: thickness,
113
+ backgroundColor: color,
114
+ width: dashLength,
115
+ }}
116
+ />
117
+ </View>
118
+ );
119
+ },
120
+ );
121
+
122
+ /**
123
+ * Gradient separator for modern UI
124
+ */
125
+ export const GradientSeparator = memo(
126
+ ({
127
+ style,
128
+ colors = ["#E0E0E0", "#FFFFFF"],
129
+ height = 1,
130
+ marginVertical = 8,
131
+ }) => {
132
+ // Note: For actual gradient, you'd need Expo LinearGradient or similar
133
+ // This is a simplified version
134
+ return (
135
+ <View
136
+ style={[
137
+ styles.gradientContainer,
138
+ {
139
+ height,
140
+ marginVertical,
141
+ backgroundColor: colors[0],
142
+ },
143
+ style,
144
+ ]}
145
+ />
146
+ );
147
+ },
148
+ );
149
+
150
+ const styles = StyleSheet.create({
151
+ container: {
152
+ width: "100%",
153
+ },
154
+ dashedContainer: {
155
+ flexDirection: "row",
156
+ alignItems: "center",
157
+ width: "100%",
158
+ },
159
+ gradientContainer: {
160
+ width: "100%",
161
+ opacity: 0.5,
162
+ },
163
+ });
164
+
165
+ AccordionSeparator.displayName = "AccordionSeparator";
166
+ NestedAccordionSeparator.displayName = "NestedAccordionSeparator";
167
+ DashedSeparator.displayName = "DashedSeparator";
168
+ GradientSeparator.displayName = "GradientSeparator";
@@ -0,0 +1,86 @@
1
+ import React, { createContext, useContext, useRef, useCallback } from "react";
2
+
3
+ const defaultAnimationConfig = {
4
+ type: "timing",
5
+ duration: 300,
6
+ };
7
+
8
+ export const AccordionContext = createContext(null);
9
+
10
+ export const useAccordion = () => {
11
+ const context = useContext(AccordionContext);
12
+ if (!context) {
13
+ throw new Error("useAccordion must be used within an Accordion");
14
+ }
15
+ return context;
16
+ };
17
+
18
+ export const AccordionProvider = ({
19
+ mode,
20
+ readOnly,
21
+ animationConfig = defaultAnimationConfig,
22
+ renderIcon,
23
+ iconPosition = "right",
24
+ showSeparators = false,
25
+ lazyRender = false,
26
+ theme,
27
+ darkMode = false,
28
+ expandedValues,
29
+ onToggleItem,
30
+ children,
31
+ }) => {
32
+ const itemsMap = useRef(new Map());
33
+ const eventsRef = useRef({});
34
+
35
+ const registerItem = useCallback((value, disabled = false, data) => {
36
+ itemsMap.current.set(value, { disabled, data });
37
+ }, []);
38
+
39
+ const unregisterItem = useCallback((value) => {
40
+ itemsMap.current.delete(value);
41
+ }, []);
42
+
43
+ const toggleItem = useCallback(
44
+ (value) => {
45
+ if (readOnly) return;
46
+ const item = itemsMap.current.get(value);
47
+ if (item?.disabled) return;
48
+ onToggleItem(value);
49
+ },
50
+ [readOnly, onToggleItem],
51
+ );
52
+
53
+ const isExpanded = useCallback(
54
+ (value) => {
55
+ return expandedValues.includes(value);
56
+ },
57
+ [expandedValues],
58
+ );
59
+
60
+ const getItemData = useCallback((value) => {
61
+ return itemsMap.current.get(value)?.data;
62
+ }, []);
63
+
64
+ const contextValue = {
65
+ mode,
66
+ readOnly,
67
+ animationConfig,
68
+ renderIcon,
69
+ iconPosition,
70
+ showSeparators,
71
+ lazyRender,
72
+ theme,
73
+ darkMode,
74
+ registerItem,
75
+ unregisterItem,
76
+ toggleItem,
77
+ isExpanded,
78
+ getItemData,
79
+ };
80
+
81
+ return (
82
+ <AccordionContext.Provider value={contextValue}>
83
+ {children}
84
+ </AccordionContext.Provider>
85
+ );
86
+ };