expo-app-ui 1.0.2 → 1.0.4
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/README.md +13 -0
- package/package.json +1 -1
- package/src/commands/add.js +59 -16
- package/src/utils/pathUtils.js +93 -10
- package/templates/components/ui/button.tsx +4 -5
- package/templates/components/ui/custom-modal.tsx +161 -28
- package/templates/components/ui/profile-pic.tsx +20 -9
- package/templates/components/ui/progress-bar.tsx +33 -13
package/README.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
A UI component library for Expo React Native. Copy components directly into your project and customize them to your needs.
|
|
4
4
|
|
|
5
|
+
## Component Showcase
|
|
6
|
+
|
|
7
|
+
<div align="center">
|
|
8
|
+
|
|
9
|
+
<img src="https://expo-apps-ui.vercel.app/examples/buttons-example.png" alt="Button Component" width="150" />
|
|
10
|
+
<img src="https://expo-apps-ui.vercel.app/examples/custom-modal-example.gif" alt="Custom Modal" width="150" />
|
|
11
|
+
<img src="https://expo-apps-ui.vercel.app/examples/otp-input-example.gif" alt="OTP Input" width="150" />
|
|
12
|
+
<img src="https://expo-apps-ui.vercel.app/examples/top-loading-bar-example.gif" alt="Top Loading Bar" width="150" />
|
|
13
|
+
|
|
14
|
+
*Button • Custom Modal • OTP Input • Loading Bar*
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
5
18
|
## 📚 Documentation
|
|
6
19
|
|
|
7
20
|
**👉 [View Full Documentation →](https://expo-apps-ui.vercel.app)**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-app-ui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "A UI component library for Expo React Native. Copy components directly into your project and customize them to your needs. Documentation: https://expo-apps-ui.vercel.app",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
package/src/commands/add.js
CHANGED
|
@@ -153,7 +153,20 @@ async function addComponent(componentName, options = {}) {
|
|
|
153
153
|
const packageDir = getPackageDir();
|
|
154
154
|
const templatesDir = path.join(packageDir, 'templates');
|
|
155
155
|
|
|
156
|
+
// Track processing to prevent circular dependencies
|
|
157
|
+
const processingSet = options.processingSet || new Set();
|
|
156
158
|
const kebabName = toKebabCase(componentName);
|
|
159
|
+
|
|
160
|
+
// Check if already processing this component to prevent infinite loops
|
|
161
|
+
const itemKey = `component:${kebabName}`;
|
|
162
|
+
if (processingSet.has(itemKey)) {
|
|
163
|
+
logger.debug(`Skipping ${componentName} - already being processed`);
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Mark as processing
|
|
168
|
+
processingSet.add(itemKey);
|
|
169
|
+
|
|
157
170
|
const templatePath = path.join(templatesDir, 'components', 'ui', `${kebabName}.tsx`);
|
|
158
171
|
const targetPath = path.join(config.getComponentsDir(), `${kebabName}.tsx`);
|
|
159
172
|
|
|
@@ -182,12 +195,17 @@ async function addComponent(componentName, options = {}) {
|
|
|
182
195
|
}
|
|
183
196
|
}
|
|
184
197
|
|
|
185
|
-
// Check for related context
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
198
|
+
// Check for related context (but skip if already processing to prevent loops)
|
|
199
|
+
// Only check if we're not being called from addContext (to prevent circular detection)
|
|
200
|
+
let relatedContext = null;
|
|
201
|
+
if (!options.skipRelatedCheck) {
|
|
202
|
+
relatedContext = detectRelatedContext(kebabName, templatesDir);
|
|
203
|
+
if (relatedContext) {
|
|
204
|
+
const contextPath = path.join(projectRoot, 'context', `${relatedContext}.tsx`);
|
|
205
|
+
const contextKey = `context:${relatedContext}`;
|
|
206
|
+
if (!fs.existsSync(contextPath) && !processingSet.has(contextKey)) {
|
|
207
|
+
dependenciesToAdd.push(`${relatedContext} context`);
|
|
208
|
+
}
|
|
191
209
|
}
|
|
192
210
|
}
|
|
193
211
|
|
|
@@ -222,15 +240,19 @@ async function addComponent(componentName, options = {}) {
|
|
|
222
240
|
}
|
|
223
241
|
}
|
|
224
242
|
|
|
225
|
-
// Add related context if needed
|
|
226
|
-
if (
|
|
243
|
+
// Add related context if needed (pass processingSet to prevent loops)
|
|
244
|
+
// Only add if we're not being called from addContext (to prevent circular detection)
|
|
245
|
+
if (!options.skipRelatedCheck && relatedContext) {
|
|
227
246
|
const contextPath = path.join(projectRoot, 'context', `${relatedContext}.tsx`);
|
|
228
|
-
|
|
247
|
+
const contextKey = `context:${relatedContext}`;
|
|
248
|
+
if (!fs.existsSync(contextPath) && !processingSet.has(contextKey)) {
|
|
229
249
|
await addContext(relatedContext, {
|
|
230
250
|
logger,
|
|
231
251
|
config,
|
|
232
252
|
silent: false,
|
|
233
253
|
overwrite: false,
|
|
254
|
+
processingSet,
|
|
255
|
+
skipRelatedCheck: true, // Prevent context from checking for related component again
|
|
234
256
|
});
|
|
235
257
|
}
|
|
236
258
|
}
|
|
@@ -282,7 +304,20 @@ async function addContext(contextName, options = {}) {
|
|
|
282
304
|
const packageDir = getPackageDir();
|
|
283
305
|
const templatesDir = path.join(packageDir, 'templates');
|
|
284
306
|
|
|
307
|
+
// Track processing to prevent circular dependencies
|
|
308
|
+
const processingSet = options.processingSet || new Set();
|
|
285
309
|
const kebabName = toKebabCase(contextName);
|
|
310
|
+
|
|
311
|
+
// Check if already processing this context to prevent infinite loops
|
|
312
|
+
const itemKey = `context:${kebabName}`;
|
|
313
|
+
if (processingSet.has(itemKey)) {
|
|
314
|
+
logger.debug(`Skipping ${contextName} - already being processed`);
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Mark as processing
|
|
319
|
+
processingSet.add(itemKey);
|
|
320
|
+
|
|
286
321
|
const templatePath = path.join(templatesDir, 'context', `${kebabName}.tsx`);
|
|
287
322
|
const contextDir = path.join(projectRoot, 'context');
|
|
288
323
|
const targetPath = path.join(contextDir, `${kebabName}.tsx`);
|
|
@@ -295,13 +330,14 @@ async function addContext(contextName, options = {}) {
|
|
|
295
330
|
// Detect dependencies including related component
|
|
296
331
|
const dependencies = detectDependencies(content);
|
|
297
332
|
|
|
298
|
-
// Check for related component
|
|
333
|
+
// Check for related component (but skip if already processing to prevent loops)
|
|
299
334
|
const relatedComponent = detectRelatedComponent(kebabName, templatesDir);
|
|
300
335
|
const dependenciesToAdd = [];
|
|
301
336
|
|
|
302
337
|
if (relatedComponent) {
|
|
303
338
|
const componentPath = path.join(config.getComponentsDir(), `${relatedComponent}.tsx`);
|
|
304
|
-
|
|
339
|
+
const componentKey = `component:${relatedComponent}`;
|
|
340
|
+
if (!fs.existsSync(componentPath) && !processingSet.has(componentKey)) {
|
|
305
341
|
dependenciesToAdd.push(`${relatedComponent} component`);
|
|
306
342
|
}
|
|
307
343
|
}
|
|
@@ -312,15 +348,19 @@ async function addContext(contextName, options = {}) {
|
|
|
312
348
|
logger.debug('Adding required dependencies...\n');
|
|
313
349
|
}
|
|
314
350
|
|
|
315
|
-
// Add related component if needed
|
|
351
|
+
// Add related component if needed (pass processingSet to prevent loops)
|
|
352
|
+
// Skip related check to prevent circular detection
|
|
316
353
|
if (relatedComponent) {
|
|
317
354
|
const componentPath = path.join(config.getComponentsDir(), `${relatedComponent}.tsx`);
|
|
318
|
-
|
|
355
|
+
const componentKey = `component:${relatedComponent}`;
|
|
356
|
+
if (!fs.existsSync(componentPath) && !processingSet.has(componentKey)) {
|
|
319
357
|
await addComponent(relatedComponent, {
|
|
320
358
|
logger,
|
|
321
359
|
config,
|
|
322
360
|
silent: false,
|
|
323
361
|
overwrite: false,
|
|
362
|
+
processingSet,
|
|
363
|
+
skipRelatedCheck: true, // Prevent component from checking for related context again
|
|
324
364
|
});
|
|
325
365
|
}
|
|
326
366
|
}
|
|
@@ -456,10 +496,13 @@ async function handleAdd(name, options = {}) {
|
|
|
456
496
|
return await addTopLoadingBar({ logger, config, overwrite });
|
|
457
497
|
}
|
|
458
498
|
|
|
499
|
+
// Initialize processing set to track items and prevent circular dependencies
|
|
500
|
+
const processingSet = new Set();
|
|
501
|
+
|
|
459
502
|
// Check if it's a component (look in components/ui/)
|
|
460
503
|
const componentPath = path.join(templatesDir, 'components', 'ui', `${kebabName}.tsx`);
|
|
461
504
|
if (fs.existsSync(componentPath)) {
|
|
462
|
-
return await addComponent(name, { logger, config, overwrite });
|
|
505
|
+
return await addComponent(name, { logger, config, overwrite, processingSet });
|
|
463
506
|
}
|
|
464
507
|
|
|
465
508
|
// Check if it's a helper
|
|
@@ -478,11 +521,11 @@ async function handleAdd(name, options = {}) {
|
|
|
478
521
|
const contextPathKebab = path.join(templatesDir, 'context', `${kebabName}.tsx`);
|
|
479
522
|
const contextPathPascal = path.join(templatesDir, 'context', `${toPascalCase(kebabName)}.tsx`);
|
|
480
523
|
if (fs.existsSync(contextPathKebab)) {
|
|
481
|
-
return await addContext(name, { logger, config, overwrite });
|
|
524
|
+
return await addContext(name, { logger, config, overwrite, processingSet });
|
|
482
525
|
} else if (fs.existsSync(contextPathPascal)) {
|
|
483
526
|
// Use the actual file name
|
|
484
527
|
const actualName = path.basename(contextPathPascal, '.tsx');
|
|
485
|
-
return await addContext(actualName, { logger, config, overwrite });
|
|
528
|
+
return await addContext(actualName, { logger, config, overwrite, processingSet });
|
|
486
529
|
}
|
|
487
530
|
|
|
488
531
|
// Not found
|
package/src/utils/pathUtils.js
CHANGED
|
@@ -55,38 +55,121 @@ function validatePath(filePath, baseDir) {
|
|
|
55
55
|
return resolved;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
// Cache the package directory to avoid repeated lookups
|
|
59
|
+
let cachedPackageDir = null;
|
|
60
|
+
|
|
58
61
|
/**
|
|
59
62
|
* Get package directory - works from any context
|
|
60
63
|
* @returns {string} Package directory path
|
|
61
64
|
*/
|
|
62
65
|
function getPackageDir() {
|
|
63
|
-
//
|
|
66
|
+
// Return cached value if available
|
|
67
|
+
if (cachedPackageDir) {
|
|
68
|
+
return cachedPackageDir;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Strategy 1: Use require.main.filename (works when running as CLI via npx or node)
|
|
72
|
+
// When running via npx, require.main points to the bin script
|
|
73
|
+
try {
|
|
74
|
+
if (require.main && require.main.filename) {
|
|
75
|
+
let currentDir = path.dirname(require.main.filename);
|
|
76
|
+
|
|
77
|
+
// Search up from bin script location
|
|
78
|
+
let depth = 0;
|
|
79
|
+
while (currentDir !== path.dirname(currentDir) && depth < 10) {
|
|
80
|
+
const packageJsonPath = path.join(currentDir, 'package.json');
|
|
81
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
82
|
+
try {
|
|
83
|
+
const pkg = fs.readJsonSync(packageJsonPath);
|
|
84
|
+
if (pkg.name === 'expo-app-ui') {
|
|
85
|
+
const templatesPath = path.join(currentDir, 'templates');
|
|
86
|
+
if (fs.existsSync(templatesPath)) {
|
|
87
|
+
cachedPackageDir = currentDir;
|
|
88
|
+
return cachedPackageDir;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} catch (err) {
|
|
92
|
+
// Continue searching
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
currentDir = path.dirname(currentDir);
|
|
96
|
+
depth++;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} catch (error) {
|
|
100
|
+
// Continue to next strategy
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Strategy 2: Try require.resolve for package.json (works when installed via npm/npx)
|
|
104
|
+
try {
|
|
105
|
+
const packageJsonPath = require.resolve('expo-app-ui/package.json', { paths: [process.cwd(), __dirname] });
|
|
106
|
+
cachedPackageDir = path.dirname(packageJsonPath);
|
|
107
|
+
// Verify it has templates directory
|
|
108
|
+
const templatesPath = path.join(cachedPackageDir, 'templates');
|
|
109
|
+
if (fs.existsSync(templatesPath)) {
|
|
110
|
+
return cachedPackageDir;
|
|
111
|
+
}
|
|
112
|
+
} catch (error) {
|
|
113
|
+
// Continue to next strategy
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Strategy 3: Use relative path from current file (works for local development)
|
|
117
|
+
try {
|
|
118
|
+
// Go up from src/utils/pathUtils.js to package root
|
|
119
|
+
let currentDir = path.dirname(path.dirname(__dirname));
|
|
120
|
+
|
|
121
|
+
const packageJsonPath = path.join(currentDir, 'package.json');
|
|
122
|
+
const templatesPath = path.join(currentDir, 'templates');
|
|
123
|
+
if (fs.existsSync(packageJsonPath) && fs.existsSync(templatesPath)) {
|
|
124
|
+
try {
|
|
125
|
+
const pkg = fs.readJsonSync(packageJsonPath);
|
|
126
|
+
if (pkg.name === 'expo-app-ui') {
|
|
127
|
+
cachedPackageDir = currentDir;
|
|
128
|
+
return cachedPackageDir;
|
|
129
|
+
}
|
|
130
|
+
} catch (err) {
|
|
131
|
+
// Continue to next strategy
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} catch (error) {
|
|
135
|
+
// Continue to next strategy
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Strategy 3: Search from current file's directory
|
|
64
139
|
let currentDir = __dirname;
|
|
65
140
|
|
|
66
|
-
// If we're in src/utils, go up
|
|
67
|
-
|
|
141
|
+
// If we're in src/utils or src/commands, go up two levels
|
|
142
|
+
if (currentDir.endsWith(path.join('src', 'utils')) || currentDir.endsWith(path.join('src', 'commands'))) {
|
|
143
|
+
currentDir = path.dirname(path.dirname(currentDir));
|
|
144
|
+
} else if (currentDir.includes(path.sep + 'src' + path.sep)) {
|
|
145
|
+
currentDir = path.dirname(currentDir);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Verify and search up if needed
|
|
68
149
|
while (currentDir !== path.dirname(currentDir)) {
|
|
69
150
|
const packageJsonPath = path.join(currentDir, 'package.json');
|
|
70
151
|
if (fs.existsSync(packageJsonPath)) {
|
|
71
152
|
try {
|
|
72
153
|
const pkg = fs.readJsonSync(packageJsonPath);
|
|
73
|
-
if (pkg.
|
|
74
|
-
|
|
154
|
+
if (pkg.name === 'expo-app-ui') {
|
|
155
|
+
cachedPackageDir = currentDir;
|
|
156
|
+
return cachedPackageDir;
|
|
75
157
|
}
|
|
76
|
-
} catch (
|
|
158
|
+
} catch (err) {
|
|
77
159
|
// Continue searching
|
|
78
160
|
}
|
|
79
161
|
}
|
|
80
162
|
currentDir = path.dirname(currentDir);
|
|
81
163
|
}
|
|
82
164
|
|
|
83
|
-
//
|
|
165
|
+
// Final fallback (shouldn't normally reach here)
|
|
84
166
|
if (__dirname.includes('src')) {
|
|
85
|
-
|
|
167
|
+
cachedPackageDir = path.dirname(path.dirname(__dirname));
|
|
168
|
+
} else {
|
|
169
|
+
cachedPackageDir = path.dirname(__dirname);
|
|
86
170
|
}
|
|
87
171
|
|
|
88
|
-
|
|
89
|
-
return path.dirname(__dirname);
|
|
172
|
+
return cachedPackageDir;
|
|
90
173
|
}
|
|
91
174
|
|
|
92
175
|
/**
|
|
@@ -7,9 +7,8 @@ import {
|
|
|
7
7
|
TextStyle,
|
|
8
8
|
TouchableOpacityProps,
|
|
9
9
|
} from "react-native";
|
|
10
|
-
|
|
10
|
+
|
|
11
11
|
import { ActivityIndicator } from "react-native";
|
|
12
|
-
import { normalizeSize } from "@/helper/normalizeSize";
|
|
13
12
|
|
|
14
13
|
type ButtonVariant = {
|
|
15
14
|
backgroundColor?: string;
|
|
@@ -62,11 +61,11 @@ const Button = ({
|
|
|
62
61
|
IconCenter, // New IconCenter prop
|
|
63
62
|
style, // Custom style for the button container
|
|
64
63
|
textStyle, // Custom style for the text
|
|
65
|
-
fontSize =
|
|
64
|
+
fontSize = 18, // Default font size
|
|
66
65
|
fontWeight = "normal", // Default font weight
|
|
67
66
|
textColor = "#fff", // Default text color
|
|
68
67
|
borderColor = "#d3d3d3",
|
|
69
|
-
bgColor =
|
|
68
|
+
bgColor = "black", // Default background color
|
|
70
69
|
hideTextWithCenterIcon = false, // Default to false (show text with center icon)
|
|
71
70
|
...props
|
|
72
71
|
}: ButtonPropsExtended) => {
|
|
@@ -171,7 +170,7 @@ const Button = ({
|
|
|
171
170
|
styles.buttonText,
|
|
172
171
|
{
|
|
173
172
|
color: variantStyles.textColor,
|
|
174
|
-
fontFamily: fonts.inter,
|
|
173
|
+
// fontFamily: fonts.inter, // Commented out to avoid using fonts.inter
|
|
175
174
|
// Hide text visually (but keep it for screen readers) if IconCenter is present and hideTextWithCenterIcon is true
|
|
176
175
|
opacity: IconCenter && hideTextWithCenterIcon ? 0 : 1,
|
|
177
176
|
},
|
|
@@ -6,8 +6,11 @@ import {
|
|
|
6
6
|
StyleSheet,
|
|
7
7
|
TouchableWithoutFeedback,
|
|
8
8
|
useWindowDimensions,
|
|
9
|
-
StyleProp,
|
|
10
|
-
ViewStyle,
|
|
9
|
+
StyleProp,
|
|
10
|
+
ViewStyle,
|
|
11
|
+
Keyboard,
|
|
12
|
+
Platform,
|
|
13
|
+
BackHandler,
|
|
11
14
|
} from "react-native";
|
|
12
15
|
import Animated, {
|
|
13
16
|
useSharedValue,
|
|
@@ -17,14 +20,25 @@ import Animated, {
|
|
|
17
20
|
runOnJS,
|
|
18
21
|
} from "react-native-reanimated";
|
|
19
22
|
|
|
23
|
+
// Default colors - using black and white as defaults
|
|
24
|
+
const defaultColors = {
|
|
25
|
+
white: "#FFFFFF",
|
|
26
|
+
black: "#000000",
|
|
27
|
+
backdrop: "rgba(0, 0, 0, 0.5)",
|
|
28
|
+
};
|
|
29
|
+
|
|
20
30
|
// Define the props interface
|
|
21
31
|
interface CustomModalProps {
|
|
22
32
|
visible: boolean;
|
|
23
33
|
onClose: () => void;
|
|
24
34
|
preventBackgroundTouchEvent?: boolean;
|
|
25
35
|
children: React.ReactNode;
|
|
26
|
-
style?: StyleProp<ViewStyle>; //
|
|
27
|
-
modalStyle?: StyleProp<ViewStyle>; //
|
|
36
|
+
style?: StyleProp<ViewStyle>; // Prop for root container
|
|
37
|
+
modalStyle?: StyleProp<ViewStyle>; // Prop for the content box
|
|
38
|
+
noBackdrop?: boolean; // Hide backdrop
|
|
39
|
+
backgroundColor?: string; // Modal background color
|
|
40
|
+
backdropColor?: string; // Backdrop color
|
|
41
|
+
borderRadius?: number; // Border radius for modal
|
|
28
42
|
}
|
|
29
43
|
|
|
30
44
|
const MODAL_ANIMATION_DURATION = 300;
|
|
@@ -36,27 +50,106 @@ const CustomModal: React.FC<CustomModalProps> = ({
|
|
|
36
50
|
onClose,
|
|
37
51
|
preventBackgroundTouchEvent,
|
|
38
52
|
children,
|
|
39
|
-
style,
|
|
40
|
-
modalStyle,
|
|
53
|
+
style,
|
|
54
|
+
modalStyle,
|
|
55
|
+
noBackdrop = false,
|
|
56
|
+
backgroundColor = defaultColors.white,
|
|
57
|
+
backdropColor = defaultColors.backdrop,
|
|
58
|
+
borderRadius = 15,
|
|
41
59
|
}) => {
|
|
42
60
|
const { height } = useWindowDimensions();
|
|
43
61
|
|
|
44
62
|
const [isModalRendered, setIsModalRendered] = useState(visible);
|
|
45
63
|
const backdropOpacity = useSharedValue(0);
|
|
46
64
|
const modalTranslateY = useSharedValue(height);
|
|
65
|
+
const keyboardOffset = useSharedValue(0);
|
|
66
|
+
|
|
67
|
+
// Check if modal is positioned at bottom (bottom sheet style)
|
|
68
|
+
const flattenedStyle = style ? StyleSheet.flatten(style) : {};
|
|
69
|
+
const isBottomSheet = (flattenedStyle as any)?.justifyContent === "flex-end";
|
|
47
70
|
|
|
48
71
|
const backdropAnimatedStyle = useAnimatedStyle(() => ({
|
|
49
|
-
opacity: backdropOpacity.value,
|
|
72
|
+
opacity: noBackdrop ? 0 : backdropOpacity.value,
|
|
50
73
|
}));
|
|
51
74
|
|
|
52
|
-
const modalAnimatedStyle = useAnimatedStyle(() =>
|
|
53
|
-
|
|
54
|
-
|
|
75
|
+
const modalAnimatedStyle = useAnimatedStyle(() => {
|
|
76
|
+
if (isBottomSheet) {
|
|
77
|
+
// For bottom sheets, position at bottom and translate from below
|
|
78
|
+
// Subtract keyboardOffset to move modal up when keyboard appears
|
|
79
|
+
return {
|
|
80
|
+
transform: [
|
|
81
|
+
{
|
|
82
|
+
translateY: modalTranslateY.value - keyboardOffset.value,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
// For centered modals, use standard transform
|
|
88
|
+
return {
|
|
89
|
+
transform: [{ translateY: modalTranslateY.value - keyboardOffset.value }],
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Handle Android back button
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (!visible) return;
|
|
96
|
+
|
|
97
|
+
const backHandler = BackHandler.addEventListener(
|
|
98
|
+
"hardwareBackPress",
|
|
99
|
+
() => {
|
|
100
|
+
if (!preventBackgroundTouchEvent) {
|
|
101
|
+
onClose();
|
|
102
|
+
return true; // Prevent default back behavior
|
|
103
|
+
}
|
|
104
|
+
return false; // Allow default back behavior if preventBackgroundTouchEvent is true
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return () => backHandler.remove();
|
|
109
|
+
}, [visible, preventBackgroundTouchEvent, onClose]);
|
|
110
|
+
|
|
111
|
+
// Handle keyboard events for bottom sheet modals
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (!visible || !isBottomSheet) {
|
|
114
|
+
// Reset keyboard offset when modal is not visible or not a bottom sheet
|
|
115
|
+
keyboardOffset.value = 0;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const showEvent =
|
|
120
|
+
Platform.OS === "ios" ? "keyboardWillShow" : "keyboardDidShow";
|
|
121
|
+
const hideEvent =
|
|
122
|
+
Platform.OS === "ios" ? "keyboardWillHide" : "keyboardDidHide";
|
|
123
|
+
|
|
124
|
+
const keyboardWillShowListener = Keyboard.addListener(showEvent, (e) => {
|
|
125
|
+
const keyboardHeight = e.endCoordinates.height;
|
|
126
|
+
keyboardOffset.value = withTiming(keyboardHeight, {
|
|
127
|
+
duration: Platform.OS === "ios" ? e.duration || 250 : 250,
|
|
128
|
+
easing: Easing.out(Easing.ease),
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const keyboardWillHideListener = Keyboard.addListener(hideEvent, () => {
|
|
133
|
+
keyboardOffset.value = withTiming(0, {
|
|
134
|
+
duration: 250,
|
|
135
|
+
easing: Easing.out(Easing.ease),
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return () => {
|
|
140
|
+
keyboardWillShowListener.remove();
|
|
141
|
+
keyboardWillHideListener.remove();
|
|
142
|
+
};
|
|
143
|
+
}, [visible, isBottomSheet, keyboardOffset]);
|
|
55
144
|
|
|
56
145
|
useEffect(() => {
|
|
57
146
|
if (visible) {
|
|
58
147
|
setIsModalRendered(true);
|
|
59
|
-
|
|
148
|
+
// Ensure modalTranslateY starts from height for bottom sheets
|
|
149
|
+
if (isBottomSheet) {
|
|
150
|
+
modalTranslateY.value = height;
|
|
151
|
+
}
|
|
152
|
+
backdropOpacity.value = withTiming(noBackdrop ? 0 : 0.5, {
|
|
60
153
|
duration: MODAL_ANIMATION_DURATION,
|
|
61
154
|
easing: backdropEasing,
|
|
62
155
|
});
|
|
@@ -69,6 +162,10 @@ const CustomModal: React.FC<CustomModalProps> = ({
|
|
|
69
162
|
duration: MODAL_ANIMATION_DURATION,
|
|
70
163
|
easing: backdropEasing,
|
|
71
164
|
});
|
|
165
|
+
keyboardOffset.value = withTiming(0, {
|
|
166
|
+
duration: MODAL_ANIMATION_DURATION,
|
|
167
|
+
easing: modalEasing,
|
|
168
|
+
});
|
|
72
169
|
modalTranslateY.value = withTiming(
|
|
73
170
|
height,
|
|
74
171
|
{
|
|
@@ -82,23 +179,46 @@ const CustomModal: React.FC<CustomModalProps> = ({
|
|
|
82
179
|
}
|
|
83
180
|
);
|
|
84
181
|
}
|
|
85
|
-
}, [visible, height, backdropOpacity, modalTranslateY]);
|
|
182
|
+
}, [visible, height, isBottomSheet, backdropOpacity, modalTranslateY, keyboardOffset, noBackdrop]);
|
|
86
183
|
|
|
87
184
|
if (!isModalRendered) {
|
|
88
185
|
return null;
|
|
89
186
|
}
|
|
90
187
|
|
|
91
188
|
return (
|
|
92
|
-
// Apply the custom root 'style' prop here
|
|
93
189
|
<Animated.View style={[styles.container, style]}>
|
|
94
|
-
{!
|
|
95
|
-
<TouchableWithoutFeedback
|
|
96
|
-
|
|
190
|
+
{!noBackdrop && (
|
|
191
|
+
<TouchableWithoutFeedback
|
|
192
|
+
onPress={() => {
|
|
193
|
+
// By default, backdrop touch closes the modal
|
|
194
|
+
// Only prevent if explicitly set to true
|
|
195
|
+
if (!preventBackgroundTouchEvent) {
|
|
196
|
+
onClose();
|
|
197
|
+
}
|
|
198
|
+
}}
|
|
199
|
+
>
|
|
200
|
+
<Animated.View
|
|
201
|
+
style={[
|
|
202
|
+
styles.backdrop,
|
|
203
|
+
{ backgroundColor: backdropColor },
|
|
204
|
+
backdropAnimatedStyle,
|
|
205
|
+
]}
|
|
206
|
+
/>
|
|
97
207
|
</TouchableWithoutFeedback>
|
|
98
208
|
)}
|
|
99
209
|
|
|
100
|
-
|
|
101
|
-
|
|
210
|
+
<Animated.View
|
|
211
|
+
style={[
|
|
212
|
+
styles.modalView,
|
|
213
|
+
{
|
|
214
|
+
backgroundColor,
|
|
215
|
+
borderRadius: isBottomSheet ? borderRadius : borderRadius,
|
|
216
|
+
...(isBottomSheet && styles.modalViewBottomSheet),
|
|
217
|
+
},
|
|
218
|
+
modalAnimatedStyle,
|
|
219
|
+
modalStyle,
|
|
220
|
+
]}
|
|
221
|
+
>
|
|
102
222
|
{children}
|
|
103
223
|
</Animated.View>
|
|
104
224
|
</Animated.View>
|
|
@@ -112,25 +232,38 @@ const styles = StyleSheet.create({
|
|
|
112
232
|
left: 0,
|
|
113
233
|
right: 0,
|
|
114
234
|
bottom: 0,
|
|
115
|
-
// Changed to 'flex-end' to act like a bottom-sheet
|
|
116
235
|
justifyContent: "center",
|
|
117
236
|
alignItems: "center",
|
|
118
237
|
zIndex: 1000,
|
|
119
238
|
},
|
|
120
239
|
backdrop: {
|
|
121
240
|
...StyleSheet.absoluteFillObject,
|
|
122
|
-
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
|
123
241
|
},
|
|
124
242
|
modalView: {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
243
|
+
padding: 20,
|
|
244
|
+
width: "90%",
|
|
245
|
+
maxWidth: 500,
|
|
246
|
+
maxHeight: "80%",
|
|
247
|
+
shadowColor: "#000",
|
|
248
|
+
shadowOffset: {
|
|
249
|
+
width: 0,
|
|
250
|
+
height: 2,
|
|
251
|
+
},
|
|
252
|
+
shadowOpacity: 0.25,
|
|
253
|
+
shadowRadius: 3.84,
|
|
254
|
+
elevation: 5,
|
|
255
|
+
},
|
|
256
|
+
modalViewBottomSheet: {
|
|
257
|
+
position: "absolute",
|
|
258
|
+
bottom: 0,
|
|
259
|
+
left: 0,
|
|
260
|
+
right: 0,
|
|
130
261
|
width: "100%",
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
262
|
+
maxWidth: "100%",
|
|
263
|
+
borderTopLeftRadius: 20,
|
|
264
|
+
borderTopRightRadius: 20,
|
|
265
|
+
borderBottomLeftRadius: 0,
|
|
266
|
+
borderBottomRightRadius: 0,
|
|
134
267
|
},
|
|
135
268
|
});
|
|
136
269
|
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { ActivityIndicator, View, ViewStyle, Text } from "react-native";
|
|
2
3
|
import { Image } from "expo-image";
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
// Default colors - using black and white as defaults
|
|
6
|
+
const defaultColors = {
|
|
7
|
+
white: "#FFFFFF",
|
|
8
|
+
black: "#000000",
|
|
9
|
+
primary: "#000000", // Default to black
|
|
10
|
+
};
|
|
5
11
|
|
|
6
12
|
interface ProfilePicProps {
|
|
7
13
|
source?: string;
|
|
@@ -24,10 +30,10 @@ const ProfilePic = ({
|
|
|
24
30
|
username,
|
|
25
31
|
width = 50,
|
|
26
32
|
height = 50,
|
|
27
|
-
borderColor =
|
|
33
|
+
borderColor = defaultColors.white,
|
|
28
34
|
borderWidth = 1,
|
|
29
35
|
borderRadius = 50,
|
|
30
|
-
backgroundColor =
|
|
36
|
+
backgroundColor = defaultColors.primary,
|
|
31
37
|
isLoading = false,
|
|
32
38
|
style,
|
|
33
39
|
}: ProfilePicProps) => {
|
|
@@ -47,8 +53,7 @@ const ProfilePic = ({
|
|
|
47
53
|
return (
|
|
48
54
|
<View style={[combinedStyle, style]}>
|
|
49
55
|
<ActivityIndicator
|
|
50
|
-
color={
|
|
51
|
-
// style={{ borderColor: colors.white }}
|
|
56
|
+
color={defaultColors.white}
|
|
52
57
|
/>
|
|
53
58
|
</View>
|
|
54
59
|
);
|
|
@@ -65,9 +70,15 @@ const ProfilePic = ({
|
|
|
65
70
|
transition={1000}
|
|
66
71
|
/>
|
|
67
72
|
) : (
|
|
68
|
-
<
|
|
73
|
+
<Text
|
|
74
|
+
style={{
|
|
75
|
+
color: defaultColors.white,
|
|
76
|
+
fontSize: 20,
|
|
77
|
+
}}
|
|
78
|
+
allowFontScaling={false}
|
|
79
|
+
>
|
|
69
80
|
{username ? username.charAt(0).toUpperCase() : "Z"}
|
|
70
|
-
</
|
|
81
|
+
</Text>
|
|
71
82
|
)}
|
|
72
83
|
</View>
|
|
73
84
|
);
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { useEffect, useRef } from "react";
|
|
2
2
|
import { Animated, StyleSheet, Text, View } from "react-native";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
|
|
4
|
+
// Default colors - using black and white as defaults
|
|
5
|
+
const defaultColors = {
|
|
6
|
+
white: "#FFFFFF",
|
|
7
|
+
black: "#000000",
|
|
8
|
+
darkGray: "#1C1C1E",
|
|
9
|
+
forest_green: "#228B22", // Keep forest_green for progress bar
|
|
10
|
+
};
|
|
6
11
|
|
|
7
12
|
interface CustomProgressBarProps {
|
|
8
13
|
progress?: number;
|
|
@@ -20,7 +25,7 @@ const CustomProgressBar: React.FC<CustomProgressBarProps> = ({
|
|
|
20
25
|
progress,
|
|
21
26
|
width = 300,
|
|
22
27
|
height = 20,
|
|
23
|
-
color =
|
|
28
|
+
color = defaultColors.forest_green,
|
|
24
29
|
backgroundColor = "#e0e0e0",
|
|
25
30
|
label = "",
|
|
26
31
|
variant = "normal",
|
|
@@ -69,18 +74,33 @@ const CustomProgressBar: React.FC<CustomProgressBarProps> = ({
|
|
|
69
74
|
]}
|
|
70
75
|
/>
|
|
71
76
|
{variant === "count" && (
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
<View
|
|
78
|
+
style={{
|
|
79
|
+
position: "absolute",
|
|
80
|
+
right: 10,
|
|
81
|
+
left: 10,
|
|
82
|
+
top: 5,
|
|
83
|
+
flexDirection: "row",
|
|
84
|
+
justifyContent: "space-between",
|
|
85
|
+
}}
|
|
76
86
|
>
|
|
77
|
-
<
|
|
87
|
+
<Text
|
|
88
|
+
style={{
|
|
89
|
+
color: defaultColors.white,
|
|
90
|
+
fontWeight: "700",
|
|
91
|
+
}}
|
|
92
|
+
>
|
|
78
93
|
{currentCount}
|
|
79
|
-
</
|
|
80
|
-
<
|
|
94
|
+
</Text>
|
|
95
|
+
<Text
|
|
96
|
+
style={{
|
|
97
|
+
color: defaultColors.darkGray,
|
|
98
|
+
fontWeight: "700",
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
81
101
|
{count}
|
|
82
|
-
</
|
|
83
|
-
</
|
|
102
|
+
</Text>
|
|
103
|
+
</View>
|
|
84
104
|
)}
|
|
85
105
|
</View>
|
|
86
106
|
</View>
|