expo-app-ui 1.0.1 → 1.0.2

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.2",
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) {
@@ -161,6 +182,15 @@ async function addComponent(componentName, options = {}) {
161
182
  }
162
183
  }
163
184
 
185
+ // Check for related context
186
+ const relatedContext = detectRelatedContext(kebabName, templatesDir);
187
+ if (relatedContext) {
188
+ const contextPath = path.join(projectRoot, 'context', `${relatedContext}.tsx`);
189
+ if (!fs.existsSync(contextPath)) {
190
+ dependenciesToAdd.push(`${relatedContext} context`);
191
+ }
192
+ }
193
+
164
194
  // Show summary of what will be added
165
195
  if (dependenciesToAdd.length > 0) {
166
196
  logger.info(`\nDetected dependencies: ${dependenciesToAdd.join(', ')}`);
@@ -192,11 +222,26 @@ async function addComponent(componentName, options = {}) {
192
222
  }
193
223
  }
194
224
 
225
+ // Add related context if needed
226
+ if (relatedContext) {
227
+ const contextPath = path.join(projectRoot, 'context', `${relatedContext}.tsx`);
228
+ if (!fs.existsSync(contextPath)) {
229
+ await addContext(relatedContext, {
230
+ logger,
231
+ config,
232
+ silent: false,
233
+ overwrite: false,
234
+ });
235
+ }
236
+ }
237
+
238
+ const handleFileExists = createFileExistsHandler(options);
195
239
  const validatedPath = await writeFile(
196
240
  targetPath,
197
241
  content,
198
242
  projectRoot,
199
- options.overwrite || false
243
+ options.overwrite || false,
244
+ handleFileExists
200
245
  );
201
246
 
202
247
  logger.success(`Added ${componentName} component to ${path.relative(projectRoot, validatedPath)}`);
@@ -240,17 +285,53 @@ async function addContext(contextName, options = {}) {
240
285
  const kebabName = toKebabCase(contextName);
241
286
  const templatePath = path.join(templatesDir, 'context', `${kebabName}.tsx`);
242
287
  const contextDir = path.join(projectRoot, 'context');
243
- const targetPath = path.join(contextDir, `${toPascalCase(kebabName)}.tsx`);
288
+ const targetPath = path.join(contextDir, `${kebabName}.tsx`);
244
289
 
245
290
  try {
246
291
  logger.debug(`Looking for template: ${templatePath}`);
247
292
 
248
293
  const content = await readTemplate(templatePath);
294
+
295
+ // Detect dependencies including related component
296
+ const dependencies = detectDependencies(content);
297
+
298
+ // Check for related component
299
+ const relatedComponent = detectRelatedComponent(kebabName, templatesDir);
300
+ const dependenciesToAdd = [];
301
+
302
+ if (relatedComponent) {
303
+ const componentPath = path.join(config.getComponentsDir(), `${relatedComponent}.tsx`);
304
+ if (!fs.existsSync(componentPath)) {
305
+ dependenciesToAdd.push(`${relatedComponent} component`);
306
+ }
307
+ }
308
+
309
+ // Show summary of what will be added
310
+ if (dependenciesToAdd.length > 0) {
311
+ logger.info(`\nDetected dependencies: ${dependenciesToAdd.join(', ')}`);
312
+ logger.debug('Adding required dependencies...\n');
313
+ }
314
+
315
+ // Add related component if needed
316
+ if (relatedComponent) {
317
+ const componentPath = path.join(config.getComponentsDir(), `${relatedComponent}.tsx`);
318
+ if (!fs.existsSync(componentPath)) {
319
+ await addComponent(relatedComponent, {
320
+ logger,
321
+ config,
322
+ silent: false,
323
+ overwrite: false,
324
+ });
325
+ }
326
+ }
327
+
328
+ const handleFileExists = createFileExistsHandler(options);
249
329
  const validatedPath = await writeFile(
250
330
  targetPath,
251
331
  content,
252
332
  projectRoot,
253
- options.overwrite || false
333
+ options.overwrite || false,
334
+ handleFileExists
254
335
  );
255
336
 
256
337
  if (!options.silent) {
@@ -277,6 +358,7 @@ async function addTopLoadingBar(options = {}) {
277
358
  const packageDir = getPackageDir();
278
359
  const templatesDir = path.join(packageDir, 'templates');
279
360
  const overwrite = options.overwrite || false;
361
+ const handleFileExists = createFileExistsHandler(options);
280
362
 
281
363
  logger.info('Adding Top Loading Bar (context + component)...\n');
282
364
 
@@ -291,7 +373,8 @@ async function addTopLoadingBar(options = {}) {
291
373
  targetComponentPath,
292
374
  content,
293
375
  projectRoot,
294
- overwrite
376
+ overwrite,
377
+ handleFileExists
295
378
  );
296
379
  logger.success(`Added loading-bar component to ${path.relative(projectRoot, validatedPath)}`);
297
380
  } else {
@@ -309,7 +392,8 @@ async function addTopLoadingBar(options = {}) {
309
392
  targetContextPath,
310
393
  content,
311
394
  projectRoot,
312
- overwrite
395
+ overwrite,
396
+ handleFileExists
313
397
  );
314
398
  logger.success(`Added top-loading-bar-context to ${path.relative(projectRoot, validatedPath)}`);
315
399
  } else {
@@ -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
@@ -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
+