expo-app-ui 1.0.1 → 1.0.3

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 CHANGED
@@ -1,8 +1,25 @@
1
- # Expo UI
1
+ # Expo App UI
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
- ## Installation
5
+ ## 📚 Documentation
6
+
7
+ **👉 [View Full Documentation →](https://expo-apps-ui.vercel.app)**
8
+
9
+ For complete documentation, usage examples, API references, and detailed instructions, visit our documentation site:
10
+
11
+ **https://expo-apps-ui.vercel.app**
12
+
13
+ The documentation includes:
14
+ - 📖 Getting Started Guide
15
+ - 🎨 Component Documentation
16
+ - 🛠️ CLI Commands Reference
17
+ - 💡 Usage Examples
18
+ - 🔧 Configuration Guides
19
+
20
+ ## Quick Start
21
+
22
+ ### Installation
6
23
 
7
24
  You can use this library directly with npx:
8
25
 
@@ -26,14 +43,14 @@ expo-app-ui add <component-name>
26
43
 
27
44
  ### Adding Components
28
45
 
29
- To add a component to your project:
46
+ Add a component to your project:
30
47
 
31
48
  ```bash
32
49
  npx expo-app-ui add custom-text
33
50
  ```
34
51
 
35
52
  This will:
36
- - Copy the component template to `components/ui/CustomText.tsx` in your project
53
+ - Copy the component template to `components/ui/custom-text.tsx` in your project
37
54
  - Automatically detect and add required dependencies (helpers, constants)
38
55
  - Preserve all imports and dependencies
39
56
  - Allow you to customize the component as needed
@@ -44,44 +61,33 @@ npx expo-app-ui add button
44
61
  # Automatically adds normalizeSize helper and theme constants if needed
45
62
  ```
46
63
 
47
- ### Adding Helpers
48
-
49
- To add a helper utility:
64
+ ### Adding Helpers, Constants, and Contexts
50
65
 
51
66
  ```bash
67
+ # Add a helper
52
68
  npx expo-app-ui add normalizeSize
53
- ```
54
-
55
- This will copy the helper to `helpers/normalizeSize.ts` in your project.
56
69
 
57
- ### Adding Constants
58
-
59
- To add constants (like theme):
60
-
61
- ```bash
70
+ # Add constants
62
71
  npx expo-app-ui add theme
63
- ```
64
-
65
- This will copy the constants to `constants/theme.ts` in your project.
66
72
 
67
- **Note:** The `theme` constant requires the `normalizeSize` helper. If you add `theme` first, you'll be prompted to add `normalizeSize`.
73
+ # Add contexts (e.g., top loading bar)
74
+ npx expo-app-ui add top-loading-bar
75
+ ```
68
76
 
69
77
  ### Listing Available Items
70
78
 
71
- To see all available components, helpers, and constants:
72
-
73
79
  ```bash
74
80
  npx expo-app-ui list
75
81
  ```
76
82
 
77
83
  ### Overwriting Existing Files
78
84
 
79
- To replace an existing component, helper, or constant:
80
-
81
85
  ```bash
82
86
  npx expo-app-ui add custom-text --overwrite
83
87
  ```
84
88
 
89
+ > 📖 **For detailed usage instructions, examples, and API documentation, visit [expo-apps-ui.vercel.app](https://expo-apps-ui.vercel.app)**
90
+
85
91
  ## Project Structure
86
92
 
87
93
  After adding components, your project structure will look like:
@@ -105,9 +111,12 @@ your-project/
105
111
  The CLI automatically detects when a component requires:
106
112
  - `normalizeSize` helper (from `@/helper/normalizeSize`)
107
113
  - `theme` constants (from `@/constants/theme`)
114
+ - Related components or contexts
108
115
 
109
116
  When you add a component that uses these dependencies, they will be automatically added to your project.
110
117
 
118
+ > 📖 **Learn more about auto-dependency detection in the [documentation](https://expo-apps-ui.vercel.app/docs/cli)**
119
+
111
120
  ## Component Templates
112
121
 
113
122
  Components are copied directly into your project, so you have full control:
@@ -119,12 +128,13 @@ Components are copied directly into your project, so you have full control:
119
128
 
120
129
  ## Path Aliases
121
130
 
122
- Components use path aliases like `@/components/ui/CustomText` and `@/constants/theme`. Make sure your Expo project has these configured:
131
+ Components use path aliases like `@/components/ui/custom-text` and `@/constants/theme`. Make sure your Expo project has these configured:
123
132
 
124
133
  **tsconfig.json:**
125
134
  ```json
126
135
  {
127
136
  "compilerOptions": {
137
+ "baseUrl": ".",
128
138
  "paths": {
129
139
  "@/*": ["./*"]
130
140
  }
@@ -149,6 +159,8 @@ module.exports = {
149
159
  };
150
160
  ```
151
161
 
162
+ > 📖 **See the [Getting Started guide](https://expo-apps-ui.vercel.app/docs/getting-started) for detailed setup instructions**
163
+
152
164
  ## Available Components
153
165
 
154
166
  - `custom-text` - A customizable Text component with font, color, and spacing props
@@ -159,6 +171,11 @@ module.exports = {
159
171
  - `progress-bar` - A progress bar component with variants
160
172
  - `marquee` - A scrolling marquee component
161
173
  - `otp-input` - An OTP input component
174
+ - `loading-bar` - An animated top loading bar component
175
+
176
+ ## Available Contexts
177
+
178
+ - `top-loading-bar-context` - React Context for managing top loading bar state
162
179
 
163
180
  ## Available Helpers
164
181
 
@@ -168,15 +185,20 @@ module.exports = {
168
185
 
169
186
  - `theme` - Theme constants including colors, fonts, and sizes
170
187
 
188
+ > 📖 **View complete documentation, props, examples, and usage for all components at [expo-apps-ui.vercel.app/docs/components](https://expo-apps-ui.vercel.app/docs/components)**
189
+
171
190
  ## Contributing
172
191
 
173
- To add new components, helpers, or constants:
192
+ To add new components, helpers, constants, or contexts:
193
+
194
+ 1. **Components**: Create a new `.tsx` file in the `templates/components/ui/` directory
195
+ 2. **Contexts**: Create a new `.tsx` file in the `templates/context/` directory
196
+ 3. **Helpers**: Create a new `.ts` file in the `templates/helpers/` directory
197
+ 4. **Constants**: Create a new `.ts` file in the `templates/constants/` directory
198
+ 5. Use kebab-case for filenames (e.g., `my-component.tsx`)
199
+ 6. The item will be automatically available via the CLI
174
200
 
175
- 1. **Components**: Create a new `.tsx` file in the `templates/` directory
176
- 2. **Helpers**: Create a new `.ts` file in the `templates/helpers/` directory
177
- 3. **Constants**: Create a new `.ts` file in the `templates/constants/` directory
178
- 4. Use kebab-case for filenames (e.g., `my-component.tsx`)
179
- 5. The item will be automatically available via the CLI
201
+ > 📖 **For contribution guidelines and best practices, visit the [documentation](https://expo-apps-ui.vercel.app)**
180
202
 
181
203
  ## License
182
204
 
@@ -45,8 +45,9 @@ program
45
45
  logger.error(`"${name}" not found.`);
46
46
  logger.info('Run "npx expo-app-ui list" to see available items.');
47
47
  } else if (error instanceof FileExistsError) {
48
- logger.error(`File already exists: ${error.filePath}`);
49
- logger.info('Use --overwrite to replace it.');
48
+ // User was prompted and chose not to overwrite, or non-interactive mode
49
+ logger.warning(`File already exists: ${error.filePath}`);
50
+ logger.info('Use --overwrite to replace it, or run the command again and choose "y" when prompted.');
50
51
  } else if (error instanceof InvalidInputError) {
51
52
  logger.error(error.message);
52
53
  } else if (error instanceof CLIError) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "expo-app-ui",
3
- "version": "1.0.1",
4
- "description": "A UI component library for Expo React Native",
3
+ "version": "1.0.3",
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": {
7
7
  "expo-app-ui": "bin/expo-app-ui.js"
@@ -18,12 +18,20 @@
18
18
  "keywords": [
19
19
  "expo",
20
20
  "react-native",
21
+ "android",
22
+ "ios",
21
23
  "ui",
22
24
  "components",
23
25
  "cli",
24
- "component-library"
26
+ "component-library",
27
+ "expo-ui",
28
+ "react-native-components",
29
+ "mobile-ui",
30
+ "expo-components",
31
+ "ui-library",
32
+ "react-native-ui"
25
33
  ],
26
- "author": "",
34
+ "author": "Krish Panchani",
27
35
  "license": "MIT",
28
36
  "repository": {
29
37
  "type": "git",
@@ -32,7 +40,7 @@
32
40
  "bugs": {
33
41
  "url": "https://github.com/Krish-Panchani/expo-app-ui/issues"
34
42
  },
35
- "homepage": "https://github.com/Krish-Panchani/expo-app-ui#readme",
43
+ "homepage": "https://expo-apps-ui.vercel.app",
36
44
  "engines": {
37
45
  "node": ">=14.0.0"
38
46
  },
@@ -3,9 +3,26 @@ const fs = require('fs-extra');
3
3
  const { toKebabCase, toPascalCase, getPackageDir } = require('../utils/pathUtils');
4
4
  const { TemplateNotFoundError } = require('../utils/errors');
5
5
  const { readTemplate, writeFile, listTemplates } = require('../core/templateProcessor');
6
- const { detectDependencies } = require('../core/dependencyDetector');
6
+ const { detectDependencies, detectRelatedContext, detectRelatedComponent } = require('../core/dependencyDetector');
7
7
  const Logger = require('../utils/logger');
8
8
  const Config = require('../utils/config');
9
+ const { promptOverwrite } = require('../utils/prompt');
10
+
11
+ /**
12
+ * Helper to create a file exists handler with prompt
13
+ */
14
+ function createFileExistsHandler(options) {
15
+ return async (filePath) => {
16
+ if (options.overwrite) {
17
+ return true; // Already confirmed via --overwrite flag
18
+ }
19
+ // Check if we're in interactive mode (not silent and TTY)
20
+ if (!options.silent && process.stdin.isTTY) {
21
+ return await promptOverwrite(filePath);
22
+ }
23
+ return false; // Non-interactive, don't overwrite
24
+ };
25
+ }
9
26
 
10
27
  /**
11
28
  * Add a helper file
@@ -25,11 +42,13 @@ async function addHelper(helperName, options = {}) {
25
42
  logger.debug(`Looking for template: ${templatePath}`);
26
43
 
27
44
  const content = await readTemplate(templatePath);
45
+ const handleFileExists = createFileExistsHandler(options);
28
46
  const validatedPath = await writeFile(
29
47
  targetPath,
30
48
  content,
31
49
  projectRoot,
32
- options.overwrite || false
50
+ options.overwrite || false,
51
+ handleFileExists
33
52
  );
34
53
 
35
54
  if (!options.silent) {
@@ -91,11 +110,13 @@ async function addConstant(constantName, options = {}) {
91
110
  }
92
111
  }
93
112
 
113
+ const handleFileExists = createFileExistsHandler(options);
94
114
  const validatedPath = await writeFile(
95
115
  targetPath,
96
116
  content,
97
117
  projectRoot,
98
- options.overwrite || false
118
+ options.overwrite || false,
119
+ handleFileExists
99
120
  );
100
121
 
101
122
  if (!options.silent) {
@@ -132,7 +153,20 @@ async function addComponent(componentName, options = {}) {
132
153
  const packageDir = getPackageDir();
133
154
  const templatesDir = path.join(packageDir, 'templates');
134
155
 
156
+ // Track processing to prevent circular dependencies
157
+ const processingSet = options.processingSet || new Set();
135
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
+
136
170
  const templatePath = path.join(templatesDir, 'components', 'ui', `${kebabName}.tsx`);
137
171
  const targetPath = path.join(config.getComponentsDir(), `${kebabName}.tsx`);
138
172
 
@@ -161,6 +195,20 @@ async function addComponent(componentName, options = {}) {
161
195
  }
162
196
  }
163
197
 
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
+ }
209
+ }
210
+ }
211
+
164
212
  // Show summary of what will be added
165
213
  if (dependenciesToAdd.length > 0) {
166
214
  logger.info(`\nDetected dependencies: ${dependenciesToAdd.join(', ')}`);
@@ -192,11 +240,30 @@ async function addComponent(componentName, options = {}) {
192
240
  }
193
241
  }
194
242
 
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) {
246
+ const contextPath = path.join(projectRoot, 'context', `${relatedContext}.tsx`);
247
+ const contextKey = `context:${relatedContext}`;
248
+ if (!fs.existsSync(contextPath) && !processingSet.has(contextKey)) {
249
+ await addContext(relatedContext, {
250
+ logger,
251
+ config,
252
+ silent: false,
253
+ overwrite: false,
254
+ processingSet,
255
+ skipRelatedCheck: true, // Prevent context from checking for related component again
256
+ });
257
+ }
258
+ }
259
+
260
+ const handleFileExists = createFileExistsHandler(options);
195
261
  const validatedPath = await writeFile(
196
262
  targetPath,
197
263
  content,
198
264
  projectRoot,
199
- options.overwrite || false
265
+ options.overwrite || false,
266
+ handleFileExists
200
267
  );
201
268
 
202
269
  logger.success(`Added ${componentName} component to ${path.relative(projectRoot, validatedPath)}`);
@@ -237,20 +304,74 @@ async function addContext(contextName, options = {}) {
237
304
  const packageDir = getPackageDir();
238
305
  const templatesDir = path.join(packageDir, 'templates');
239
306
 
307
+ // Track processing to prevent circular dependencies
308
+ const processingSet = options.processingSet || new Set();
240
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
+
241
321
  const templatePath = path.join(templatesDir, 'context', `${kebabName}.tsx`);
242
322
  const contextDir = path.join(projectRoot, 'context');
243
- const targetPath = path.join(contextDir, `${toPascalCase(kebabName)}.tsx`);
323
+ const targetPath = path.join(contextDir, `${kebabName}.tsx`);
244
324
 
245
325
  try {
246
326
  logger.debug(`Looking for template: ${templatePath}`);
247
327
 
248
328
  const content = await readTemplate(templatePath);
329
+
330
+ // Detect dependencies including related component
331
+ const dependencies = detectDependencies(content);
332
+
333
+ // Check for related component (but skip if already processing to prevent loops)
334
+ const relatedComponent = detectRelatedComponent(kebabName, templatesDir);
335
+ const dependenciesToAdd = [];
336
+
337
+ if (relatedComponent) {
338
+ const componentPath = path.join(config.getComponentsDir(), `${relatedComponent}.tsx`);
339
+ const componentKey = `component:${relatedComponent}`;
340
+ if (!fs.existsSync(componentPath) && !processingSet.has(componentKey)) {
341
+ dependenciesToAdd.push(`${relatedComponent} component`);
342
+ }
343
+ }
344
+
345
+ // Show summary of what will be added
346
+ if (dependenciesToAdd.length > 0) {
347
+ logger.info(`\nDetected dependencies: ${dependenciesToAdd.join(', ')}`);
348
+ logger.debug('Adding required dependencies...\n');
349
+ }
350
+
351
+ // Add related component if needed (pass processingSet to prevent loops)
352
+ // Skip related check to prevent circular detection
353
+ if (relatedComponent) {
354
+ const componentPath = path.join(config.getComponentsDir(), `${relatedComponent}.tsx`);
355
+ const componentKey = `component:${relatedComponent}`;
356
+ if (!fs.existsSync(componentPath) && !processingSet.has(componentKey)) {
357
+ await addComponent(relatedComponent, {
358
+ logger,
359
+ config,
360
+ silent: false,
361
+ overwrite: false,
362
+ processingSet,
363
+ skipRelatedCheck: true, // Prevent component from checking for related context again
364
+ });
365
+ }
366
+ }
367
+
368
+ const handleFileExists = createFileExistsHandler(options);
249
369
  const validatedPath = await writeFile(
250
370
  targetPath,
251
371
  content,
252
372
  projectRoot,
253
- options.overwrite || false
373
+ options.overwrite || false,
374
+ handleFileExists
254
375
  );
255
376
 
256
377
  if (!options.silent) {
@@ -277,6 +398,7 @@ async function addTopLoadingBar(options = {}) {
277
398
  const packageDir = getPackageDir();
278
399
  const templatesDir = path.join(packageDir, 'templates');
279
400
  const overwrite = options.overwrite || false;
401
+ const handleFileExists = createFileExistsHandler(options);
280
402
 
281
403
  logger.info('Adding Top Loading Bar (context + component)...\n');
282
404
 
@@ -291,7 +413,8 @@ async function addTopLoadingBar(options = {}) {
291
413
  targetComponentPath,
292
414
  content,
293
415
  projectRoot,
294
- overwrite
416
+ overwrite,
417
+ handleFileExists
295
418
  );
296
419
  logger.success(`Added loading-bar component to ${path.relative(projectRoot, validatedPath)}`);
297
420
  } else {
@@ -309,7 +432,8 @@ async function addTopLoadingBar(options = {}) {
309
432
  targetContextPath,
310
433
  content,
311
434
  projectRoot,
312
- overwrite
435
+ overwrite,
436
+ handleFileExists
313
437
  );
314
438
  logger.success(`Added top-loading-bar-context to ${path.relative(projectRoot, validatedPath)}`);
315
439
  } else {
@@ -372,10 +496,13 @@ async function handleAdd(name, options = {}) {
372
496
  return await addTopLoadingBar({ logger, config, overwrite });
373
497
  }
374
498
 
499
+ // Initialize processing set to track items and prevent circular dependencies
500
+ const processingSet = new Set();
501
+
375
502
  // Check if it's a component (look in components/ui/)
376
503
  const componentPath = path.join(templatesDir, 'components', 'ui', `${kebabName}.tsx`);
377
504
  if (fs.existsSync(componentPath)) {
378
- return await addComponent(name, { logger, config, overwrite });
505
+ return await addComponent(name, { logger, config, overwrite, processingSet });
379
506
  }
380
507
 
381
508
  // Check if it's a helper
@@ -394,11 +521,11 @@ async function handleAdd(name, options = {}) {
394
521
  const contextPathKebab = path.join(templatesDir, 'context', `${kebabName}.tsx`);
395
522
  const contextPathPascal = path.join(templatesDir, 'context', `${toPascalCase(kebabName)}.tsx`);
396
523
  if (fs.existsSync(contextPathKebab)) {
397
- return await addContext(name, { logger, config, overwrite });
524
+ return await addContext(name, { logger, config, overwrite, processingSet });
398
525
  } else if (fs.existsSync(contextPathPascal)) {
399
526
  // Use the actual file name
400
527
  const actualName = path.basename(contextPathPascal, '.tsx');
401
- return await addContext(actualName, { logger, config, overwrite });
528
+ return await addContext(actualName, { logger, config, overwrite, processingSet });
402
529
  }
403
530
 
404
531
  // Not found
@@ -8,12 +8,16 @@ function detectDependencies(content) {
8
8
  return {
9
9
  needsNormalizeSize: false,
10
10
  needsTheme: false,
11
+ needsComponent: null,
12
+ needsContext: null,
11
13
  };
12
14
  }
13
15
 
14
16
  const dependencies = {
15
17
  needsNormalizeSize: false,
16
18
  needsTheme: false,
19
+ needsComponent: null,
20
+ needsContext: null,
17
21
  };
18
22
 
19
23
  // More precise pattern matching
@@ -23,10 +27,88 @@ function detectDependencies(content) {
23
27
  dependencies.needsNormalizeSize = normalizeSizePattern.test(content);
24
28
  dependencies.needsTheme = themePattern.test(content);
25
29
 
30
+ // Detect component imports in context files
31
+ const componentImportPattern = /from\s+['"]@\/components\/ui\/([^'"]+)['"]/;
32
+ const componentMatch = content.match(componentImportPattern);
33
+ if (componentMatch) {
34
+ dependencies.needsComponent = componentMatch[1]; // e.g., "loading-bar"
35
+ }
36
+
37
+ // Detect context imports in component files
38
+ const contextImportPattern = /from\s+['"]@\/context\/([^'"]+)['"]/;
39
+ const contextMatch = content.match(contextImportPattern);
40
+ if (contextMatch) {
41
+ dependencies.needsContext = contextMatch[1]; // e.g., "top-loading-bar-context"
42
+ }
43
+
26
44
  return dependencies;
27
45
  }
28
46
 
47
+ /**
48
+ * Detect related context for a component
49
+ * @param {string} componentName - Component name in kebab-case
50
+ * @param {string} templatesDir - Templates directory
51
+ * @returns {string|null} Related context name or null
52
+ */
53
+ function detectRelatedContext(componentName, templatesDir) {
54
+ const fs = require('fs-extra');
55
+ const path = require('path');
56
+
57
+ // Check all context files to see if any imports this component
58
+ const contextDir = path.join(templatesDir, 'context');
59
+ if (!fs.existsSync(contextDir)) {
60
+ return null;
61
+ }
62
+
63
+ const contextFiles = fs.readdirSync(contextDir).filter(file => file.endsWith('.tsx'));
64
+
65
+ for (const contextFile of contextFiles) {
66
+ const contextPath = path.join(contextDir, contextFile);
67
+ const content = fs.readFileSync(contextPath, 'utf-8');
68
+
69
+ // Check if this context imports the component
70
+ // Escape special regex characters in componentName
71
+ const escapedComponentName = componentName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
72
+ const componentImportPattern = new RegExp(`@/components/ui/${escapedComponentName}`);
73
+ if (componentImportPattern.test(content)) {
74
+ return path.basename(contextFile, '.tsx');
75
+ }
76
+ }
77
+
78
+ return null;
79
+ }
80
+
81
+ /**
82
+ * Detect related component for a context
83
+ * @param {string} contextName - Context name in kebab-case
84
+ * @param {string} templatesDir - Templates directory
85
+ * @returns {string|null} Related component name or null
86
+ */
87
+ function detectRelatedComponent(contextName, templatesDir) {
88
+ const fs = require('fs-extra');
89
+ const path = require('path');
90
+
91
+ const contextPath = path.join(templatesDir, 'context', `${contextName}.tsx`);
92
+ if (!fs.existsSync(contextPath)) {
93
+ return null;
94
+ }
95
+
96
+ const content = fs.readFileSync(contextPath, 'utf-8');
97
+
98
+ // Extract component name from import
99
+ const componentImportPattern = /from\s+['"]@\/components\/ui\/([^'"]+)['"]/;
100
+ const match = content.match(componentImportPattern);
101
+
102
+ if (match) {
103
+ return match[1]; // e.g., "loading-bar"
104
+ }
105
+
106
+ return null;
107
+ }
108
+
29
109
  module.exports = {
30
110
  detectDependencies,
111
+ detectRelatedContext,
112
+ detectRelatedComponent,
31
113
  };
32
114
 
@@ -30,16 +30,26 @@ async function readTemplate(templatePath) {
30
30
  * @param {string} content - File content
31
31
  * @param {string} baseDir - Base directory for validation
32
32
  * @param {boolean} overwrite - Whether to overwrite existing file
33
- * @throws {FileExistsError} If file exists and overwrite is false
33
+ * @param {Function} onExists - Optional callback when file exists (returns Promise<boolean>)
34
+ * @throws {FileExistsError} If file exists and overwrite is false and onExists is not provided
34
35
  * @throws {CLIError} If path validation fails
35
36
  */
36
- async function writeFile(targetPath, content, baseDir, overwrite = false) {
37
+ async function writeFile(targetPath, content, baseDir, overwrite = false, onExists = null) {
37
38
  // Validate path
38
39
  const validatedPath = validatePath(targetPath, baseDir);
39
40
 
40
41
  // Check if file exists
41
42
  if (fs.existsSync(validatedPath) && !overwrite) {
42
- throw new FileExistsError(validatedPath);
43
+ // If callback is provided, use it to ask user
44
+ if (onExists && typeof onExists === 'function') {
45
+ const shouldOverwrite = await onExists(validatedPath);
46
+ if (!shouldOverwrite) {
47
+ throw new FileExistsError(validatedPath);
48
+ }
49
+ // User confirmed, proceed with overwrite
50
+ } else {
51
+ throw new FileExistsError(validatedPath);
52
+ }
43
53
  }
44
54
 
45
55
  // Ensure directory exists
@@ -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
- // Start from the current file's directory
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 to project root
67
- // If we're in bin/, go up to project root
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.bin && (pkg.bin['expo-app-ui'] || pkg.name === 'expo-app-ui')) {
74
- return currentDir;
154
+ if (pkg.name === 'expo-app-ui') {
155
+ cachedPackageDir = currentDir;
156
+ return cachedPackageDir;
75
157
  }
76
- } catch (error) {
158
+ } catch (err) {
77
159
  // Continue searching
78
160
  }
79
161
  }
80
162
  currentDir = path.dirname(currentDir);
81
163
  }
82
164
 
83
- // Fallback: if we're in src/utils, go up two levels
165
+ // Final fallback (shouldn't normally reach here)
84
166
  if (__dirname.includes('src')) {
85
- return path.dirname(path.dirname(__dirname));
167
+ cachedPackageDir = path.dirname(path.dirname(__dirname));
168
+ } else {
169
+ cachedPackageDir = path.dirname(__dirname);
86
170
  }
87
171
 
88
- // If we're in bin/, go up one level
89
- return path.dirname(__dirname);
172
+ return cachedPackageDir;
90
173
  }
91
174
 
92
175
  /**
@@ -0,0 +1,42 @@
1
+ const readline = require('readline');
2
+
3
+ /**
4
+ * Prompt user for confirmation
5
+ * @param {string} message - Message to display
6
+ * @returns {Promise<boolean>} User's response (true for yes, false for no)
7
+ */
8
+ function promptUser(message) {
9
+ return new Promise((resolve) => {
10
+ const rl = readline.createInterface({
11
+ input: process.stdin,
12
+ output: process.stdout,
13
+ });
14
+
15
+ rl.question(`${message} (y/N): `, (answer) => {
16
+ rl.close();
17
+ // Default to 'no' if empty or anything other than 'y'/'Y'/'yes'/'YES'
18
+ const normalized = answer.trim().toLowerCase();
19
+ resolve(normalized === 'y' || normalized === 'yes');
20
+ });
21
+ });
22
+ }
23
+
24
+ /**
25
+ * Prompt user to overwrite existing file
26
+ * @param {string} filePath - Path to the file that exists
27
+ * @returns {Promise<boolean>} true if user wants to overwrite, false otherwise
28
+ */
29
+ async function promptOverwrite(filePath) {
30
+ const path = require('path');
31
+ const relativePath = path.relative(process.cwd(), filePath);
32
+
33
+ return await promptUser(
34
+ `File "${relativePath}" already exists. Do you want to overwrite it?`
35
+ );
36
+ }
37
+
38
+ module.exports = {
39
+ promptUser,
40
+ promptOverwrite,
41
+ };
42
+
@@ -7,9 +7,8 @@ import {
7
7
  TextStyle,
8
8
  TouchableOpacityProps,
9
9
  } from "react-native";
10
- import { colors, fonts, size } from "@/constants/theme";
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 = normalizeSize(size.md), // Default font size
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 = colors.primary, // Default background color
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
  },
@@ -1,7 +1,13 @@
1
- import { ActivityIndicator, View, ViewStyle } from "react-native";
1
+ import React from "react";
2
+ import { ActivityIndicator, View, ViewStyle, Text } from "react-native";
2
3
  import { Image } from "expo-image";
3
- import { colors, size } from "@/constants/theme";
4
- import CustomText from "@/components/ui/CustomText";
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 = colors.white,
33
+ borderColor = defaultColors.white,
28
34
  borderWidth = 1,
29
35
  borderRadius = 50,
30
- backgroundColor = colors.primary,
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={colors.white}
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
- <CustomText color={colors.white} fontSize={size.lg}>
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
- </CustomText>
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
- import BoxView from "@/components/ui/BoxView";
4
- import CustomText from "@/components/ui/CustomText";
5
- import { colors } from "@/constants/theme";
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 = colors.forest_green,
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
- <BoxView
73
- flexDirection="row"
74
- justifyContent="space-between"
75
- style={{ position: "absolute", right: 10, left: 10, top: 5 }}
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
- <CustomText color={colors.white} fontWeight="700">
87
+ <Text
88
+ style={{
89
+ color: defaultColors.white,
90
+ fontWeight: "700",
91
+ }}
92
+ >
78
93
  {currentCount}
79
- </CustomText>
80
- <CustomText color={colors.darkGray} fontWeight="700">
94
+ </Text>
95
+ <Text
96
+ style={{
97
+ color: defaultColors.darkGray,
98
+ fontWeight: "700",
99
+ }}
100
+ >
81
101
  {count}
82
- </CustomText>
83
- </BoxView>
102
+ </Text>
103
+ </View>
84
104
  )}
85
105
  </View>
86
106
  </View>