@androbinco/library-cli 0.1.0 → 0.3.0

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 (53) hide show
  1. package/README.md +86 -37
  2. package/package.json +11 -16
  3. package/src/commands/add.js +107 -0
  4. package/src/commands/list.js +51 -0
  5. package/src/index.js +46 -15
  6. package/src/templates/carousel/components/navigation-buttons.tsx +1 -0
  7. package/src/templates/carousel/components/pagination/bullet.pagination.carousel.tsx +69 -0
  8. package/src/templates/carousel/components/pagination/number.pagination.carousel.tsx +30 -0
  9. package/src/templates/carousel/components/pagination/progress/progress.pagination.carousel.tsx +99 -0
  10. package/src/templates/carousel/components/pagination/progress/use-slide-progress.tsx +31 -0
  11. package/src/templates/carousel/components/pagination.tsx +47 -82
  12. package/src/templates/faqs-accordion/examples/faqs-showcase.tsx +42 -0
  13. package/src/templates/faqs-accordion/faqs-accordion.tsx +70 -0
  14. package/src/templates/faqs-accordion/mock-data.ts +38 -0
  15. package/src/templates/faqs-accordion/types.ts +18 -0
  16. package/src/templates/in-view/data.in-view.ts +89 -0
  17. package/src/templates/in-view/examples/in-view-examples.home.tsx +101 -0
  18. package/src/templates/in-view/examples/in-view-grid-showcase.tsx +41 -0
  19. package/src/templates/in-view/in-view-animation.tsx +72 -0
  20. package/src/templates/in-view/in-view-grid.tsx +81 -0
  21. package/src/templates/in-view/in-view-hidden-text.tsx +45 -0
  22. package/src/templates/in-view/in-view-stroke-line.tsx +30 -0
  23. package/src/templates/lenis/examples/providers.tsx +23 -0
  24. package/src/templates/lenis/lenis-provider.tsx +46 -0
  25. package/src/templates/scroll-components/hooks/use-client-dimensions.ts +21 -0
  26. package/src/templates/scroll-components/parallax/examples/parallax-showcase.tsx +87 -0
  27. package/src/templates/scroll-components/parallax/parallax.css +36 -0
  28. package/src/templates/scroll-components/parallax/parallax.tsx +67 -0
  29. package/src/templates/scroll-components/scale-gallery/components/expanding-element.tsx +40 -0
  30. package/src/templates/scroll-components/scale-gallery/examples/scale-gallery-showcase.tsx +68 -0
  31. package/src/templates/scroll-components/scale-gallery/scale-gallery.tsx +57 -0
  32. package/src/templates/scroll-components/scroll-tracker-provider.tsx +78 -0
  33. package/src/templates/scroll-components/scroll-tracker-showcase.tsx +44 -0
  34. package/src/templates/strapi-dynamic-zone/README.md +157 -0
  35. package/src/templates/strapi-dynamic-zone/dynamic-zone.tsx +113 -0
  36. package/src/templates/strapi-dynamic-zone/examples/page.tsx +53 -0
  37. package/src/templates/strapi-dynamic-zone/examples/renderers.tsx +74 -0
  38. package/src/templates/strapi-dynamic-zone/examples/types.ts +41 -0
  39. package/src/templates/strapi-dynamic-zone/index.ts +11 -0
  40. package/src/templates/strapi-dynamic-zone/types.ts +73 -0
  41. package/src/templates/ticker/css-ticker/css-ticker.tsx +61 -0
  42. package/src/templates/ticker/css-ticker/ticker.keyframes.css +86 -0
  43. package/src/templates/ticker/examples/ticker-hover-showcase.home.tsx +57 -0
  44. package/src/templates/ticker/examples/ticker-static-showcase.home.tsx +56 -0
  45. package/src/templates/ticker/hooks/use-ticker-clones.tsx +70 -0
  46. package/src/templates/ticker/hooks/use-ticker-incremental.tsx +72 -0
  47. package/src/templates/ticker/motion-ticker.tsx +93 -0
  48. package/src/utils/components.js +587 -54
  49. package/src/utils/files.js +89 -5
  50. package/src/templates/button/button.tsx +0 -5
  51. package/src/templates/card/card.tsx +0 -5
  52. package/src/templates/example/example.tsx +0 -5
  53. package/src/templates/hero/hero.tsx +0 -5
@@ -5,82 +5,615 @@ import {
5
5
  checkMissingDependencies,
6
6
  installDependencies
7
7
  } from './dependencies.js';
8
- import { copyComponent } from './files.js';
8
+ import { copyComponent, checkComponentExists } from './files.js';
9
+ import fs from 'fs-extra';
10
+ import path from 'path';
9
11
 
10
- export async function handleComponentsFlow(components, templatesDir) {
11
- const selectedComponents = await p.multiselect({
12
- message: 'Please select one or more components to add (press spacebar to select and return to confirm selection)',
13
- options: components.map(comp => ({
14
- value: comp.name,
15
- label: comp.name,
16
- hint: comp.description
17
- })),
18
- required: true
19
- });
20
-
21
- if (p.isCancel(selectedComponents)) {
22
- p.cancel('Operation cancelled.');
23
- return;
12
+ // Component registry
13
+ export const COMPONENTS = [
14
+ {
15
+ name: 'carousel',
16
+ description: 'Carousel component with navigation and pagination',
17
+ sourceDir: 'carousel',
18
+ dependencies: ['embla-carousel-react', 'embla-carousel', 'class-variance-authority']
19
+ },
20
+ {
21
+ name: 'in-view',
22
+ description: 'In view animations',
23
+ sourceDir: 'in-view',
24
+ hasSubmenu: true,
25
+ submenuOptions: [
26
+ {
27
+ value: 'grid',
28
+ label: 'Grid',
29
+ description: 'Grid-based animations',
30
+ hasNestedSubmenu: true,
31
+ nestedSubmenuOptions: [
32
+ { value: 'in-view-grid', file: 'in-view-grid.tsx', required: true },
33
+ { value: 'in-view-animation', file: 'in-view-animation.tsx', required: false, checkExists: true },
34
+ { value: 'data.in-view', file: 'data.in-view.ts', required: true }
35
+ ],
36
+ examples: ['in-view-grid-showcase.tsx']
37
+ },
38
+ {
39
+ value: 'individual',
40
+ label: 'Individual Elements',
41
+ description: 'Individual element animations',
42
+ hasNestedSubmenu: true,
43
+ nestedSubmenuOptions: [
44
+ { value: 'in-view-hidden-text', file: 'in-view-hidden-text.tsx', required: true },
45
+ { value: 'in-view-stroke-line', file: 'in-view-stroke-line.tsx', required: true },
46
+ { value: 'in-view-animation', file: 'in-view-animation.tsx', required: false, checkExists: true },
47
+ { value: 'data.in-view', file: 'data.in-view.ts', required: true }
48
+ ],
49
+ examples: ['in-view-examples.home.tsx']
50
+ }
51
+ ],
52
+ dependencies: ['motion']
53
+ },
54
+ {
55
+ name: 'ticker',
56
+ description: 'Ticker component with hover and non-hover variants',
57
+ sourceDir: 'ticker',
58
+ hasSubmenu: true,
59
+ submenuOptions: [
60
+ {
61
+ value: 'hover',
62
+ label: 'Stop on hover ticker (motion)',
63
+ description: 'MotionTicker with hover stop functionality',
64
+ examples: ['ticker-hover-showcase.home.tsx']
65
+ },
66
+ {
67
+ value: 'non-hover',
68
+ label: 'Non-stop on hover ticker (css)',
69
+ description: 'TickerStatic with CSS animations',
70
+ examples: ['ticker-static-showcase.home.tsx']
71
+ }
72
+ ],
73
+ dependencies: ['motion']
74
+ },
75
+ {
76
+ name: 'scroll-components',
77
+ description: 'Scroll-driven animation components',
78
+ sourceDir: 'scroll-components',
79
+ hasSubmenu: true,
80
+ submenuOptions: [
81
+ {
82
+ value: 'scroll-tracker',
83
+ label: 'Scroll Tracker Provider',
84
+ description: 'Scroll progress tracking provider for animations',
85
+ examples: ['scroll-tracker-showcase.tsx']
86
+ },
87
+ {
88
+ value: 'parallax-image',
89
+ label: 'Parallax Image',
90
+ description: 'CSS-based parallax effect using scroll-driven animations',
91
+ examples: ['parallax/examples/parallax-showcase.tsx']
92
+ },
93
+ {
94
+ value: 'scale-gallery',
95
+ label: 'Scale Gallery',
96
+ description: 'Expanding gallery with scroll-driven scaling',
97
+ examples: ['scale-gallery/examples/scale-gallery-showcase.tsx']
98
+ }
99
+ ],
100
+ dependencies: ['motion']
101
+ },
102
+ {
103
+ name: 'strapi-dynamic-zone',
104
+ description: 'Type-safe renderer for Strapi Dynamic Zones with React',
105
+ sourceDir: 'strapi-dynamic-zone',
106
+ dependencies: [],
107
+ examples: ['page.tsx', 'renderers.tsx', 'types.ts']
108
+ },
109
+ {
110
+ name: 'faqs-accordion',
111
+ description: 'Accessible FAQ accordion component using Radix UI',
112
+ sourceDir: 'faqs-accordion',
113
+ dependencies: ['@radix-ui/react-accordion'],
114
+ examples: ['faqs-showcase.tsx']
115
+ },
116
+ {
117
+ name: 'lenis',
118
+ description: 'Smooth scroll provider with Lenis',
119
+ sourceDir: 'lenis',
120
+ dependencies: ['lenis'],
121
+ examples: ['providers.tsx']
122
+ }
123
+ ];
124
+
125
+ // Helper functions for CLI commands
126
+ export function getComponentByName(name) {
127
+ return COMPONENTS.find(c => c.name === name);
128
+ }
129
+
130
+ export function getVariantByName(component, variantName) {
131
+ if (!component.hasSubmenu || !component.submenuOptions) return null;
132
+ return component.submenuOptions.find(v => v.value === variantName);
133
+ }
134
+
135
+ export function getFilesForVariant(component, variant) {
136
+ const files = [];
137
+
138
+ if (component.name === 'ticker') {
139
+ if (variant.value === 'hover') {
140
+ files.push('motion-ticker.tsx');
141
+ files.push('hooks');
142
+ } else if (variant.value === 'non-hover') {
143
+ files.push('css-ticker');
144
+ files.push('hooks');
145
+ }
146
+ } else if (component.name === 'scroll-components') {
147
+ if (variant.value === 'scroll-tracker') {
148
+ files.push('scroll-tracker-provider.tsx');
149
+ } else if (variant.value === 'parallax-image') {
150
+ files.push('parallax/parallax.tsx');
151
+ files.push('parallax/parallax.css');
152
+ } else if (variant.value === 'scale-gallery') {
153
+ files.push('scale-gallery/scale-gallery.tsx');
154
+ files.push('scale-gallery/components/expanding-element.tsx');
155
+ files.push('hooks/use-client-dimensions.ts');
156
+ files.push('scroll-tracker-provider.tsx');
157
+ }
158
+ } else if (component.name === 'in-view' && variant.hasNestedSubmenu) {
159
+ // Copy all files for the variant
160
+ for (const nested of variant.nestedSubmenuOptions) {
161
+ files.push(nested.file);
162
+ }
24
163
  }
25
164
 
26
- if (!selectedComponents.length) {
27
- p.log.info('No components selected.');
28
- return;
165
+ return files;
166
+ }
167
+
168
+ export async function handleSubmenuFlow(component, templatesDir) {
169
+ if (!component.hasSubmenu || !component.submenuOptions || component.submenuOptions.length === 0) {
170
+ p.log.error(`Component "${component.name}" is not configured for submenu flow.`);
171
+ return { goBack: false };
29
172
  }
30
173
 
31
- // Check and install dependencies
32
- const allDependencies = collectDependencies(selectedComponents, components);
33
- const missingDependencies = await checkMissingDependencies(allDependencies);
34
-
35
- if (missingDependencies.length > 0) {
36
- const packageManager = await detectPackageManager();
37
-
38
- p.log.info(`The following dependencies are missing: ${missingDependencies.join(', ')}`);
39
-
40
- const shouldInstall = await p.confirm({
41
- message: `Install missing dependencies using ${packageManager.manager}?`,
42
- initialValue: true
174
+ while (true) {
175
+ // Show submenu with options and go back
176
+ const submenuOptions = component.submenuOptions.map(option => ({
177
+ value: option.value,
178
+ label: option.label,
179
+ hint: option.description || ''
180
+ }));
181
+
182
+ const selectedOptions = await p.multiselect({
183
+ message: `Select options for ${component.name} (press spacebar to select and return to confirm selection)`,
184
+ options: [
185
+ ...submenuOptions,
186
+ { value: '__go_back__', label: '← Go back', hint: 'Return to component selection' }
187
+ ],
188
+ required: true
43
189
  });
44
190
 
45
- if (p.isCancel(shouldInstall)) {
46
- p.log.info('Skipping dependency installation. You can install them manually later.');
47
- } else if (shouldInstall) {
48
- const installSuccess = await installDependencies(missingDependencies, packageManager);
191
+ if (p.isCancel(selectedOptions)) {
192
+ p.cancel('Operation cancelled.');
193
+ return { goBack: false };
194
+ }
195
+
196
+ // Check if user selected go back
197
+ if (selectedOptions.includes('__go_back__')) {
198
+ return { goBack: true };
199
+ }
200
+
201
+ // Remove go back from selected options
202
+ const filteredOptions = selectedOptions.filter(opt => opt !== '__go_back__');
203
+
204
+ if (!filteredOptions.length) {
205
+ p.log.info('No options selected.');
206
+ continue;
207
+ }
208
+
209
+ // Process each selected option
210
+ const allComponentsToCopy = new Set();
211
+ const allExamplesToCopy = [];
212
+ const requiredComponents = new Set();
213
+ let shouldRestartSubmenu = false;
214
+
215
+ for (const selectedValue of filteredOptions) {
216
+ const option = component.submenuOptions.find(opt => opt.value === selectedValue);
217
+ if (!option) continue;
218
+
219
+ // Collect examples
220
+ if (option.examples) {
221
+ allExamplesToCopy.push(...option.examples);
222
+ }
223
+
224
+ // Handle nested submenu
225
+ if (option.hasNestedSubmenu && option.nestedSubmenuOptions) {
226
+ // Filter components based on existence check
227
+ const availableComponents = [];
228
+ const optionRequiredComponents = new Set();
229
+
230
+ for (const nestedOption of option.nestedSubmenuOptions) {
231
+ // Check if component should be filtered out
232
+ if (nestedOption.checkExists) {
233
+ const exists = await checkComponentExists(component.name, nestedOption.file);
234
+ if (exists) {
235
+ continue; // Skip if already exists
236
+ }
237
+ }
238
+
239
+ // Track required components for this option
240
+ if (nestedOption.required) {
241
+ optionRequiredComponents.add(nestedOption.file);
242
+ requiredComponents.add(nestedOption.file);
243
+ }
244
+
245
+ availableComponents.push({
246
+ value: nestedOption.value,
247
+ label: nestedOption.file.replace('.tsx', '').replace('.ts', ''),
248
+ hint: nestedOption.required ? 'Required' : 'Optional'
249
+ });
250
+ }
251
+
252
+ if (availableComponents.length === 0) {
253
+ p.log.info(`All components for "${option.label}" already exist. Skipping.`);
254
+ continue;
255
+ }
256
+
257
+ // Show nested submenu with go back
258
+ const nestedSelected = await p.multiselect({
259
+ message: `Select components for ${option.label} (press spacebar to select and return to confirm selection)`,
260
+ options: [
261
+ ...availableComponents,
262
+ { value: '__go_back__', label: '← Go back', hint: 'Return to previous menu' }
263
+ ],
264
+ required: true
265
+ });
266
+
267
+ if (p.isCancel(nestedSelected)) {
268
+ p.cancel('Operation cancelled.');
269
+ return { goBack: false };
270
+ }
271
+
272
+ // Check if user selected go back in nested menu
273
+ if (nestedSelected.includes('__go_back__')) {
274
+ shouldRestartSubmenu = true;
275
+ break; // Break out of for loop to restart submenu
276
+ }
277
+
278
+ // Remove go back and collect selected components
279
+ const filteredNested = nestedSelected.filter(opt => opt !== '__go_back__');
280
+
281
+ for (const nestedValue of filteredNested) {
282
+ const nestedOption = option.nestedSubmenuOptions.find(opt => opt.value === nestedValue);
283
+ if (nestedOption) {
284
+ allComponentsToCopy.add(nestedOption.file);
285
+ }
286
+ }
287
+
288
+ // Add required components for this option
289
+ for (const requiredFile of optionRequiredComponents) {
290
+ allComponentsToCopy.add(requiredFile);
291
+ }
292
+ } else {
293
+ // Handle ticker component specifically
294
+ if (component.name === 'ticker') {
295
+ if (selectedValue === 'hover') {
296
+ // Hover option: copy motion-ticker.tsx
297
+ allComponentsToCopy.add('motion-ticker.tsx');
298
+ } else if (selectedValue === 'non-hover') {
299
+ // Non-hover option: copy css-ticker directory
300
+ allComponentsToCopy.add('css-ticker');
301
+ }
302
+ } else if (component.name === 'scroll-components') {
303
+ // Handle scroll-components submenu options
304
+ if (selectedValue === 'scroll-tracker') {
305
+ allComponentsToCopy.add('scroll-tracker-provider.tsx');
306
+ } else if (selectedValue === 'parallax-image') {
307
+ allComponentsToCopy.add('parallax/parallax.tsx');
308
+ allComponentsToCopy.add('parallax/parallax.css');
309
+ } else if (selectedValue === 'scale-gallery') {
310
+ allComponentsToCopy.add('scale-gallery/scale-gallery.tsx');
311
+ allComponentsToCopy.add('scale-gallery/components/expanding-element.tsx');
312
+ allComponentsToCopy.add('hooks/use-client-dimensions.ts');
313
+
314
+ // Check if scroll-tracker-provider exists, if not add it
315
+ const trackerExists = await checkComponentExists(component.name, 'scroll-tracker-provider.tsx');
316
+ if (!trackerExists) {
317
+ allComponentsToCopy.add('scroll-tracker-provider.tsx');
318
+ }
319
+ }
320
+ } else {
321
+ // No nested submenu - copy all component files (legacy behavior)
322
+ // This maintains backward compatibility
323
+ }
324
+ }
325
+ }
326
+
327
+ // For ticker component, automatically add hooks folder if it doesn't exist
328
+ if (component.name === 'ticker' && allComponentsToCopy.size > 0) {
329
+ const hooksDestPath = path.resolve(process.cwd(), 'src', 'components', component.name, 'hooks');
330
+ if (!(await fs.pathExists(hooksDestPath))) {
331
+ allComponentsToCopy.add('hooks');
332
+ }
333
+ }
334
+
335
+ // If user selected go back from nested menu, restart submenu
336
+ if (shouldRestartSubmenu) {
337
+ continue;
338
+ }
339
+
340
+ // If we have components to copy (from nested submenu), proceed with selective copying
341
+ if (allComponentsToCopy.size > 0) {
342
+ // Check and install dependencies
343
+ // For ticker hover variant, motion is required
344
+ let dependencies = component.dependencies || [];
345
+ if (component.name === 'ticker') {
346
+ // Check if hover variant is selected
347
+ const hoverSelected = filteredOptions.includes('hover');
348
+ if (hoverSelected) {
349
+ // Motion is required for hover variant
350
+ dependencies = ['motion'];
351
+ } else {
352
+ // No dependencies needed for non-hover variant only
353
+ dependencies = [];
354
+ }
355
+ }
356
+ const missingDependencies = await checkMissingDependencies(dependencies);
49
357
 
50
- if (!installSuccess) {
51
- const continueAnyway = await p.confirm({
52
- message: 'Installation failed. Continue with copying components anyway?',
358
+ // For ticker hover variant, motion is required (not optional)
359
+ if (component.name === 'ticker' && filteredOptions.includes('hover') && missingDependencies.includes('motion')) {
360
+ const packageManager = await detectPackageManager();
361
+
362
+ p.log.info('Motion dependency is required for the hover ticker variant.');
363
+
364
+ const shouldInstall = await p.confirm({
365
+ message: `Install motion dependency using ${packageManager.manager}?`,
53
366
  initialValue: true
54
367
  });
368
+
369
+ if (p.isCancel(shouldInstall) || !shouldInstall) {
370
+ p.log.error('Motion dependency is required for hover ticker variant. Operation cancelled.');
371
+ return { goBack: false };
372
+ }
373
+
374
+ const installSuccess = await installDependencies(['motion'], packageManager);
375
+
376
+ if (!installSuccess) {
377
+ const continueAnyway = await p.confirm({
378
+ message: 'Installation failed. Continue with copying component anyway?',
379
+ initialValue: false
380
+ });
381
+
382
+ if (p.isCancel(continueAnyway) || !continueAnyway) {
383
+ p.log.info('Operation cancelled.');
384
+ return { goBack: false };
385
+ }
386
+ }
387
+ } else if (missingDependencies.length > 0) {
388
+ const packageManager = await detectPackageManager();
55
389
 
56
- if (p.isCancel(continueAnyway) || !continueAnyway) {
57
- p.log.info('Operation cancelled.');
58
- return;
390
+ p.log.info(`The following dependencies are missing: ${missingDependencies.join(', ')}`);
391
+
392
+ const shouldInstall = await p.confirm({
393
+ message: `Install missing dependencies using ${packageManager.manager}?`,
394
+ initialValue: true
395
+ });
396
+
397
+ if (p.isCancel(shouldInstall)) {
398
+ p.log.info('Skipping dependency installation. You can install them manually later.');
399
+ } else if (shouldInstall) {
400
+ const installSuccess = await installDependencies(missingDependencies, packageManager);
401
+
402
+ if (!installSuccess) {
403
+ const continueAnyway = await p.confirm({
404
+ message: 'Installation failed. Continue with copying component anyway?',
405
+ initialValue: true
406
+ });
407
+
408
+ if (p.isCancel(continueAnyway) || !continueAnyway) {
409
+ p.log.info('Operation cancelled.');
410
+ return { goBack: false };
411
+ }
412
+ }
413
+ } else {
414
+ p.log.info('Skipping dependency installation. You can install them manually later.');
59
415
  }
60
416
  }
417
+
418
+ // Copy selected components
419
+ const spinner = p.spinner();
420
+ spinner.start(`Copying ${component.name} components...`);
421
+
422
+ const componentsArray = Array.from(allComponentsToCopy);
423
+ const success = await copyComponent(component, templatesDir, allExamplesToCopy, componentsArray);
424
+
425
+ if (success) {
426
+ spinner.stop(`✅ Component ${component.name} copied successfully!`);
427
+ p.log.success(`Component files are now available at: src/components/${component.name}/`);
428
+ if (allExamplesToCopy.length > 0) {
429
+ p.log.success(`Examples are available at: src/components/${component.name}/examples/`);
430
+ }
431
+ } else {
432
+ spinner.stop(`⚠️ Component ${component.name} was not copied.`);
433
+ }
434
+
435
+ return { goBack: false };
61
436
  } else {
62
- p.log.info('Skipping dependency installation. You can install them manually later.');
437
+ // Legacy behavior: copy all files if no nested submenu was used
438
+ // Check and install dependencies
439
+ const dependencies = component.dependencies || [];
440
+ const missingDependencies = await checkMissingDependencies(dependencies);
441
+
442
+ if (missingDependencies.length > 0) {
443
+ const packageManager = await detectPackageManager();
444
+
445
+ p.log.info(`The following dependencies are missing: ${missingDependencies.join(', ')}`);
446
+
447
+ const shouldInstall = await p.confirm({
448
+ message: `Install missing dependencies using ${packageManager.manager}?`,
449
+ initialValue: true
450
+ });
451
+
452
+ if (p.isCancel(shouldInstall)) {
453
+ p.log.info('Skipping dependency installation. You can install them manually later.');
454
+ } else if (shouldInstall) {
455
+ const installSuccess = await installDependencies(missingDependencies, packageManager);
456
+
457
+ if (!installSuccess) {
458
+ const continueAnyway = await p.confirm({
459
+ message: 'Installation failed. Continue with copying component anyway?',
460
+ initialValue: true
461
+ });
462
+
463
+ if (p.isCancel(continueAnyway) || !continueAnyway) {
464
+ p.log.info('Operation cancelled.');
465
+ return { goBack: false };
466
+ }
467
+ }
468
+ } else {
469
+ p.log.info('Skipping dependency installation. You can install them manually later.');
470
+ }
471
+ }
472
+
473
+ // Copy component with selected examples
474
+ const spinner = p.spinner();
475
+ spinner.start(`Copying ${component.name} component...`);
476
+
477
+ const success = await copyComponent(component, templatesDir, allExamplesToCopy);
478
+
479
+ if (success) {
480
+ spinner.stop(`✅ Component ${component.name} copied successfully!`);
481
+ p.log.success(`Component files are now available at: src/components/${component.name}/`);
482
+ if (allExamplesToCopy.length > 0) {
483
+ p.log.success(`Examples are available at: src/components/${component.name}/examples/`);
484
+ }
485
+ } else {
486
+ spinner.stop(`⚠️ Component ${component.name} was not copied.`);
487
+ }
488
+
489
+ return { goBack: false };
63
490
  }
64
491
  }
492
+ }
493
+
494
+ export async function handleComponentsFlow(components, templatesDir) {
495
+ let regularComponents = [];
496
+ let componentsWithSubmenu = [];
65
497
 
66
- // Copy components
67
- for (const componentName of selectedComponents) {
68
- const component = components.find(c => c.name === componentName);
69
- if (!component) {
70
- p.log.warn(`Component "${componentName}" not found. Skipping.`);
498
+ while (true) {
499
+ const selectedComponents = await p.multiselect({
500
+ message: 'Please select one or more components to add (press spacebar to select and return to confirm selection)',
501
+ options: [
502
+ ...components.map(comp => ({
503
+ value: comp.name,
504
+ label: comp.name,
505
+ hint: comp.description
506
+ })),
507
+ { value: '__go_back__', label: '← Go back', hint: 'Return to main menu' }
508
+ ],
509
+ required: true
510
+ });
511
+
512
+ if (p.isCancel(selectedComponents)) {
513
+ p.cancel('Operation cancelled.');
514
+ return;
515
+ }
516
+
517
+ // Check if user selected go back
518
+ if (selectedComponents.includes('__go_back__')) {
519
+ return { goBack: true };
520
+ }
521
+
522
+ if (!selectedComponents.length) {
523
+ p.log.info('No components selected.');
524
+ return;
525
+ }
526
+
527
+ // Separate components with submenu from regular components
528
+ componentsWithSubmenu = [];
529
+ regularComponents = [];
530
+
531
+ for (const componentName of selectedComponents) {
532
+ const component = components.find(c => c.name === componentName);
533
+ if (!component) {
534
+ p.log.warn(`Component "${componentName}" not found. Skipping.`);
535
+ continue;
536
+ }
537
+
538
+ if (component.hasSubmenu) {
539
+ componentsWithSubmenu.push(component);
540
+ } else {
541
+ regularComponents.push(component);
542
+ }
543
+ }
544
+
545
+ // Handle components with submenu
546
+ let shouldGoBack = false;
547
+ for (const component of componentsWithSubmenu) {
548
+ const result = await handleSubmenuFlow(component, templatesDir);
549
+ // If user selected go back, break and show component selection again
550
+ if (result && result.goBack) {
551
+ shouldGoBack = true;
552
+ break;
553
+ }
554
+ }
555
+
556
+ // If user selected go back, continue loop to show component selection again
557
+ if (shouldGoBack) {
71
558
  continue;
72
559
  }
73
560
 
74
- const spinner = p.spinner();
75
- spinner.start(`Copying ${component.name} component...`);
561
+ // If we processed all components without go back, exit the loop
562
+ break;
563
+ }
76
564
 
77
- const success = await copyComponent(component, templatesDir);
565
+ // Handle regular components
566
+ if (regularComponents.length > 0) {
567
+ // Check and install dependencies for regular components
568
+ const regularComponentNames = regularComponents.map(c => c.name);
569
+ const allDependencies = collectDependencies(regularComponentNames, components);
570
+ const missingDependencies = await checkMissingDependencies(allDependencies);
78
571
 
79
- if (success) {
80
- spinner.stop(`✅ Component ${component.name} copied successfully!`);
81
- p.log.success(`Component files are now available at: src/components/${component.name}/`);
82
- } else {
83
- spinner.stop(`⚠️ Component ${component.name} was not copied.`);
572
+ if (missingDependencies.length > 0) {
573
+ const packageManager = await detectPackageManager();
574
+
575
+ p.log.info(`The following dependencies are missing: ${missingDependencies.join(', ')}`);
576
+
577
+ const shouldInstall = await p.confirm({
578
+ message: `Install missing dependencies using ${packageManager.manager}?`,
579
+ initialValue: true
580
+ });
581
+
582
+ if (p.isCancel(shouldInstall)) {
583
+ p.log.info('Skipping dependency installation. You can install them manually later.');
584
+ } else if (shouldInstall) {
585
+ const installSuccess = await installDependencies(missingDependencies, packageManager);
586
+
587
+ if (!installSuccess) {
588
+ const continueAnyway = await p.confirm({
589
+ message: 'Installation failed. Continue with copying components anyway?',
590
+ initialValue: true
591
+ });
592
+
593
+ if (p.isCancel(continueAnyway) || !continueAnyway) {
594
+ p.log.info('Operation cancelled.');
595
+ return;
596
+ }
597
+ }
598
+ } else {
599
+ p.log.info('Skipping dependency installation. You can install them manually later.');
600
+ }
601
+ }
602
+
603
+ // Copy regular components
604
+ for (const component of regularComponents) {
605
+ const spinner = p.spinner();
606
+ spinner.start(`Copying ${component.name} component...`);
607
+
608
+ const examplesToCopy = component.examples || [];
609
+ const success = await copyComponent(component, templatesDir, examplesToCopy);
610
+
611
+ if (success) {
612
+ spinner.stop(`✅ Component ${component.name} copied successfully!`);
613
+ p.log.success(`Component files are now available at: src/components/${component.name}/`);
614
+ } else {
615
+ spinner.stop(`⚠️ Component ${component.name} was not copied.`);
616
+ }
84
617
  }
85
618
  }
86
619
  }