@androbinco/library-cli 0.1.0 → 0.2.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.
@@ -0,0 +1,93 @@
1
+ 'use client';
2
+ import { Fragment, useRef, useState } from 'react';
3
+
4
+ import { animate, motion, useInView } from 'motion/react';
5
+
6
+ import { cn } from '@/common/utils/classname-builder';
7
+
8
+ import { useTickerClones } from './hooks/use-ticker-clones';
9
+ import { useTickerIncremental } from './hooks/use-ticker-incremental';
10
+ type MotionTickerProps = {
11
+ direction?: 'left' | 'right';
12
+ speed?: number;
13
+ delta?: number;
14
+ className?: string;
15
+ wrapperClassName?: string;
16
+ children: React.ReactNode;
17
+ hoverStop?: boolean;
18
+ };
19
+
20
+ export const MotionTicker = ({
21
+ direction = 'right',
22
+ speed = 1,
23
+ delta = 0.05,
24
+ className,
25
+ children,
26
+ wrapperClassName,
27
+ hoverStop = true,
28
+ }: MotionTickerProps) => {
29
+ const containerRef = useRef<HTMLDivElement>(null);
30
+ const tickerClone = useRef<HTMLDivElement>(null);
31
+ const cardWrapper = useRef<HTMLDivElement>(null);
32
+ const [isHovering, setIsHovering] = useState(false);
33
+ const isInView = useInView(containerRef, {
34
+ once: false,
35
+ margin: '50% 0%',
36
+ });
37
+ const { clonesNeeded } = useTickerClones({ tickerCard: cardWrapper, tickerClone, direction });
38
+ const calculateProportionalDuration = (distance: number, fps?: number) => {
39
+ const distancePerFrame = speed * delta;
40
+ const framesNeeded = distance / distancePerFrame;
41
+
42
+ return framesNeeded / (fps ?? 60); // calc is based on 60fps
43
+ };
44
+ const { x1, x2, fps } = useTickerIncremental({
45
+ speed,
46
+ direction,
47
+ delta,
48
+ stopIncrement: isHovering || !isInView,
49
+ onStop: async (motionValue) => {
50
+ if (!isInView) return;
51
+ const distance = 3;
52
+ const EASE_OUT_COMPENSATION = 1.15;
53
+ const duration =
54
+ calculateProportionalDuration(distance, fps?.current) * EASE_OUT_COMPENSATION;
55
+
56
+ return await animate(motionValue, motionValue.get() + distance, {
57
+ duration,
58
+ ease: 'easeOut',
59
+ });
60
+ },
61
+ });
62
+
63
+ return (
64
+ <div
65
+ ref={containerRef}
66
+ className={cn('group relative w-full overflow-hidden', wrapperClassName)}
67
+ role="group"
68
+ onMouseEnter={() => {
69
+ if (!hoverStop) return;
70
+ setIsHovering(true);
71
+ }}
72
+ onMouseLeave={() => {
73
+ if (!hoverStop) return;
74
+ setIsHovering(false);
75
+ }}
76
+ >
77
+ <motion.div
78
+ ref={cardWrapper}
79
+ className={cn('flex w-max flex-row', className)}
80
+ style={{ x: x1 }}
81
+ >
82
+ {Array.from({ length: clonesNeeded }).map((_, index) => {
83
+ return <Fragment key={index}>{children}</Fragment>;
84
+ })}
85
+ </motion.div>
86
+ <motion.div
87
+ ref={tickerClone}
88
+ className={cn('absolute top-0 h-full w-max', className)}
89
+ style={{ x: x2 }}
90
+ />
91
+ </div>
92
+ );
93
+ };
@@ -5,82 +5,433 @@ 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
+ export async function handleSubmenuFlow(component, templatesDir) {
13
+ if (!component.hasSubmenu || !component.submenuOptions || component.submenuOptions.length === 0) {
14
+ p.log.error(`Component "${component.name}" is not configured for submenu flow.`);
15
+ return { goBack: false };
24
16
  }
25
17
 
26
- if (!selectedComponents.length) {
27
- p.log.info('No components selected.');
28
- return;
29
- }
18
+ while (true) {
19
+ // Show submenu with options and go back
20
+ const submenuOptions = component.submenuOptions.map(option => ({
21
+ value: option.value,
22
+ label: option.label,
23
+ hint: option.description || ''
24
+ }));
30
25
 
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
26
+ const selectedOptions = await p.multiselect({
27
+ message: `Select options for ${component.name} (press spacebar to select and return to confirm selection)`,
28
+ options: [
29
+ ...submenuOptions,
30
+ { value: '__go_back__', label: '← Go back', hint: 'Return to component selection' }
31
+ ],
32
+ required: true
43
33
  });
44
34
 
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);
35
+ if (p.isCancel(selectedOptions)) {
36
+ p.cancel('Operation cancelled.');
37
+ return { goBack: false };
38
+ }
39
+
40
+ // Check if user selected go back
41
+ if (selectedOptions.includes('__go_back__')) {
42
+ return { goBack: true };
43
+ }
44
+
45
+ // Remove go back from selected options
46
+ const filteredOptions = selectedOptions.filter(opt => opt !== '__go_back__');
47
+
48
+ if (!filteredOptions.length) {
49
+ p.log.info('No options selected.');
50
+ continue;
51
+ }
52
+
53
+ // Process each selected option
54
+ const allComponentsToCopy = new Set();
55
+ const allExamplesToCopy = [];
56
+ const requiredComponents = new Set();
57
+ let shouldRestartSubmenu = false;
58
+
59
+ for (const selectedValue of filteredOptions) {
60
+ const option = component.submenuOptions.find(opt => opt.value === selectedValue);
61
+ if (!option) continue;
62
+
63
+ // Collect examples
64
+ if (option.examples) {
65
+ allExamplesToCopy.push(...option.examples);
66
+ }
67
+
68
+ // Handle nested submenu
69
+ if (option.hasNestedSubmenu && option.nestedSubmenuOptions) {
70
+ // Filter components based on existence check
71
+ const availableComponents = [];
72
+ const optionRequiredComponents = new Set();
73
+
74
+ for (const nestedOption of option.nestedSubmenuOptions) {
75
+ // Check if component should be filtered out
76
+ if (nestedOption.checkExists) {
77
+ const exists = await checkComponentExists(component.name, nestedOption.file);
78
+ if (exists) {
79
+ continue; // Skip if already exists
80
+ }
81
+ }
82
+
83
+ // Track required components for this option
84
+ if (nestedOption.required) {
85
+ optionRequiredComponents.add(nestedOption.file);
86
+ requiredComponents.add(nestedOption.file);
87
+ }
88
+
89
+ availableComponents.push({
90
+ value: nestedOption.value,
91
+ label: nestedOption.file.replace('.tsx', '').replace('.ts', ''),
92
+ hint: nestedOption.required ? 'Required' : 'Optional'
93
+ });
94
+ }
95
+
96
+ if (availableComponents.length === 0) {
97
+ p.log.info(`All components for "${option.label}" already exist. Skipping.`);
98
+ continue;
99
+ }
100
+
101
+ // Show nested submenu with go back
102
+ const nestedSelected = await p.multiselect({
103
+ message: `Select components for ${option.label} (press spacebar to select and return to confirm selection)`,
104
+ options: [
105
+ ...availableComponents,
106
+ { value: '__go_back__', label: '← Go back', hint: 'Return to previous menu' }
107
+ ],
108
+ required: true
109
+ });
110
+
111
+ if (p.isCancel(nestedSelected)) {
112
+ p.cancel('Operation cancelled.');
113
+ return { goBack: false };
114
+ }
115
+
116
+ // Check if user selected go back in nested menu
117
+ if (nestedSelected.includes('__go_back__')) {
118
+ shouldRestartSubmenu = true;
119
+ break; // Break out of for loop to restart submenu
120
+ }
121
+
122
+ // Remove go back and collect selected components
123
+ const filteredNested = nestedSelected.filter(opt => opt !== '__go_back__');
124
+
125
+ for (const nestedValue of filteredNested) {
126
+ const nestedOption = option.nestedSubmenuOptions.find(opt => opt.value === nestedValue);
127
+ if (nestedOption) {
128
+ allComponentsToCopy.add(nestedOption.file);
129
+ }
130
+ }
131
+
132
+ // Add required components for this option
133
+ for (const requiredFile of optionRequiredComponents) {
134
+ allComponentsToCopy.add(requiredFile);
135
+ }
136
+ } else {
137
+ // Handle ticker component specifically
138
+ if (component.name === 'ticker') {
139
+ if (selectedValue === 'hover') {
140
+ // Hover option: copy motion-ticker.tsx
141
+ allComponentsToCopy.add('motion-ticker.tsx');
142
+ } else if (selectedValue === 'non-hover') {
143
+ // Non-hover option: copy css-ticker directory
144
+ allComponentsToCopy.add('css-ticker');
145
+ }
146
+ } else {
147
+ // No nested submenu - copy all component files (legacy behavior)
148
+ // This maintains backward compatibility
149
+ }
150
+ }
151
+ }
152
+
153
+ // For ticker component, automatically add hooks folder if it doesn't exist
154
+ if (component.name === 'ticker' && allComponentsToCopy.size > 0) {
155
+ const hooksDestPath = path.resolve(process.cwd(), 'src', 'components', component.name, 'hooks');
156
+ if (!(await fs.pathExists(hooksDestPath))) {
157
+ allComponentsToCopy.add('hooks');
158
+ }
159
+ }
160
+
161
+ // If user selected go back from nested menu, restart submenu
162
+ if (shouldRestartSubmenu) {
163
+ continue;
164
+ }
165
+
166
+ // If we have components to copy (from nested submenu), proceed with selective copying
167
+ if (allComponentsToCopy.size > 0) {
168
+ // Check and install dependencies
169
+ // For ticker hover variant, motion is required
170
+ let dependencies = component.dependencies || [];
171
+ if (component.name === 'ticker') {
172
+ // Check if hover variant is selected
173
+ const hoverSelected = filteredOptions.includes('hover');
174
+ if (hoverSelected) {
175
+ // Motion is required for hover variant
176
+ dependencies = ['motion'];
177
+ } else {
178
+ // No dependencies needed for non-hover variant only
179
+ dependencies = [];
180
+ }
181
+ }
182
+ const missingDependencies = await checkMissingDependencies(dependencies);
49
183
 
50
- if (!installSuccess) {
51
- const continueAnyway = await p.confirm({
52
- message: 'Installation failed. Continue with copying components anyway?',
184
+ // For ticker hover variant, motion is required (not optional)
185
+ if (component.name === 'ticker' && filteredOptions.includes('hover') && missingDependencies.includes('motion')) {
186
+ const packageManager = await detectPackageManager();
187
+
188
+ p.log.info('Motion dependency is required for the hover ticker variant.');
189
+
190
+ const shouldInstall = await p.confirm({
191
+ message: `Install motion dependency using ${packageManager.manager}?`,
53
192
  initialValue: true
54
193
  });
194
+
195
+ if (p.isCancel(shouldInstall) || !shouldInstall) {
196
+ p.log.error('Motion dependency is required for hover ticker variant. Operation cancelled.');
197
+ return { goBack: false };
198
+ }
199
+
200
+ const installSuccess = await installDependencies(['motion'], packageManager);
55
201
 
56
- if (p.isCancel(continueAnyway) || !continueAnyway) {
57
- p.log.info('Operation cancelled.');
58
- return;
202
+ if (!installSuccess) {
203
+ const continueAnyway = await p.confirm({
204
+ message: 'Installation failed. Continue with copying component anyway?',
205
+ initialValue: false
206
+ });
207
+
208
+ if (p.isCancel(continueAnyway) || !continueAnyway) {
209
+ p.log.info('Operation cancelled.');
210
+ return { goBack: false };
211
+ }
212
+ }
213
+ } else if (missingDependencies.length > 0) {
214
+ const packageManager = await detectPackageManager();
215
+
216
+ p.log.info(`The following dependencies are missing: ${missingDependencies.join(', ')}`);
217
+
218
+ const shouldInstall = await p.confirm({
219
+ message: `Install missing dependencies using ${packageManager.manager}?`,
220
+ initialValue: true
221
+ });
222
+
223
+ if (p.isCancel(shouldInstall)) {
224
+ p.log.info('Skipping dependency installation. You can install them manually later.');
225
+ } else if (shouldInstall) {
226
+ const installSuccess = await installDependencies(missingDependencies, packageManager);
227
+
228
+ if (!installSuccess) {
229
+ const continueAnyway = await p.confirm({
230
+ message: 'Installation failed. Continue with copying component anyway?',
231
+ initialValue: true
232
+ });
233
+
234
+ if (p.isCancel(continueAnyway) || !continueAnyway) {
235
+ p.log.info('Operation cancelled.');
236
+ return { goBack: false };
237
+ }
238
+ }
239
+ } else {
240
+ p.log.info('Skipping dependency installation. You can install them manually later.');
59
241
  }
60
242
  }
243
+
244
+ // Copy selected components
245
+ const spinner = p.spinner();
246
+ spinner.start(`Copying ${component.name} components...`);
247
+
248
+ const componentsArray = Array.from(allComponentsToCopy);
249
+ const success = await copyComponent(component, templatesDir, allExamplesToCopy, componentsArray);
250
+
251
+ if (success) {
252
+ spinner.stop(`✅ Component ${component.name} copied successfully!`);
253
+ p.log.success(`Component files are now available at: src/components/${component.name}/`);
254
+ if (allExamplesToCopy.length > 0) {
255
+ p.log.success(`Examples are available at: src/components/${component.name}/examples/`);
256
+ }
257
+ } else {
258
+ spinner.stop(`⚠️ Component ${component.name} was not copied.`);
259
+ }
260
+
261
+ return { goBack: false };
61
262
  } else {
62
- p.log.info('Skipping dependency installation. You can install them manually later.');
263
+ // Legacy behavior: copy all files if no nested submenu was used
264
+ // Check and install dependencies
265
+ const dependencies = component.dependencies || [];
266
+ const missingDependencies = await checkMissingDependencies(dependencies);
267
+
268
+ if (missingDependencies.length > 0) {
269
+ const packageManager = await detectPackageManager();
270
+
271
+ p.log.info(`The following dependencies are missing: ${missingDependencies.join(', ')}`);
272
+
273
+ const shouldInstall = await p.confirm({
274
+ message: `Install missing dependencies using ${packageManager.manager}?`,
275
+ initialValue: true
276
+ });
277
+
278
+ if (p.isCancel(shouldInstall)) {
279
+ p.log.info('Skipping dependency installation. You can install them manually later.');
280
+ } else if (shouldInstall) {
281
+ const installSuccess = await installDependencies(missingDependencies, packageManager);
282
+
283
+ if (!installSuccess) {
284
+ const continueAnyway = await p.confirm({
285
+ message: 'Installation failed. Continue with copying component anyway?',
286
+ initialValue: true
287
+ });
288
+
289
+ if (p.isCancel(continueAnyway) || !continueAnyway) {
290
+ p.log.info('Operation cancelled.');
291
+ return { goBack: false };
292
+ }
293
+ }
294
+ } else {
295
+ p.log.info('Skipping dependency installation. You can install them manually later.');
296
+ }
297
+ }
298
+
299
+ // Copy component with selected examples
300
+ const spinner = p.spinner();
301
+ spinner.start(`Copying ${component.name} component...`);
302
+
303
+ const success = await copyComponent(component, templatesDir, allExamplesToCopy);
304
+
305
+ if (success) {
306
+ spinner.stop(`✅ Component ${component.name} copied successfully!`);
307
+ p.log.success(`Component files are now available at: src/components/${component.name}/`);
308
+ if (allExamplesToCopy.length > 0) {
309
+ p.log.success(`Examples are available at: src/components/${component.name}/examples/`);
310
+ }
311
+ } else {
312
+ spinner.stop(`⚠️ Component ${component.name} was not copied.`);
313
+ }
314
+
315
+ return { goBack: false };
63
316
  }
64
317
  }
318
+ }
319
+
320
+ export async function handleComponentsFlow(components, templatesDir) {
321
+ let regularComponents = [];
322
+ let componentsWithSubmenu = [];
323
+
324
+ while (true) {
325
+ const selectedComponents = await p.multiselect({
326
+ message: 'Please select one or more components to add (press spacebar to select and return to confirm selection)',
327
+ options: components.map(comp => ({
328
+ value: comp.name,
329
+ label: comp.name,
330
+ hint: comp.description
331
+ })),
332
+ required: true
333
+ });
65
334
 
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.`);
335
+ if (p.isCancel(selectedComponents)) {
336
+ p.cancel('Operation cancelled.');
337
+ return;
338
+ }
339
+
340
+ if (!selectedComponents.length) {
341
+ p.log.info('No components selected.');
342
+ return;
343
+ }
344
+
345
+ // Separate components with submenu from regular components
346
+ componentsWithSubmenu = [];
347
+ regularComponents = [];
348
+
349
+ for (const componentName of selectedComponents) {
350
+ const component = components.find(c => c.name === componentName);
351
+ if (!component) {
352
+ p.log.warn(`Component "${componentName}" not found. Skipping.`);
353
+ continue;
354
+ }
355
+
356
+ if (component.hasSubmenu) {
357
+ componentsWithSubmenu.push(component);
358
+ } else {
359
+ regularComponents.push(component);
360
+ }
361
+ }
362
+
363
+ // Handle components with submenu
364
+ let shouldGoBack = false;
365
+ for (const component of componentsWithSubmenu) {
366
+ const result = await handleSubmenuFlow(component, templatesDir);
367
+ // If user selected go back, break and show component selection again
368
+ if (result && result.goBack) {
369
+ shouldGoBack = true;
370
+ break;
371
+ }
372
+ }
373
+
374
+ // If user selected go back, continue loop to show component selection again
375
+ if (shouldGoBack) {
71
376
  continue;
72
377
  }
73
378
 
74
- const spinner = p.spinner();
75
- spinner.start(`Copying ${component.name} component...`);
379
+ // If we processed all components without go back, exit the loop
380
+ break;
381
+ }
76
382
 
77
- const success = await copyComponent(component, templatesDir);
383
+ // Handle regular components
384
+ if (regularComponents.length > 0) {
385
+ // Check and install dependencies for regular components
386
+ const regularComponentNames = regularComponents.map(c => c.name);
387
+ const allDependencies = collectDependencies(regularComponentNames, components);
388
+ const missingDependencies = await checkMissingDependencies(allDependencies);
78
389
 
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.`);
390
+ if (missingDependencies.length > 0) {
391
+ const packageManager = await detectPackageManager();
392
+
393
+ p.log.info(`The following dependencies are missing: ${missingDependencies.join(', ')}`);
394
+
395
+ const shouldInstall = await p.confirm({
396
+ message: `Install missing dependencies using ${packageManager.manager}?`,
397
+ initialValue: true
398
+ });
399
+
400
+ if (p.isCancel(shouldInstall)) {
401
+ p.log.info('Skipping dependency installation. You can install them manually later.');
402
+ } else if (shouldInstall) {
403
+ const installSuccess = await installDependencies(missingDependencies, packageManager);
404
+
405
+ if (!installSuccess) {
406
+ const continueAnyway = await p.confirm({
407
+ message: 'Installation failed. Continue with copying components anyway?',
408
+ initialValue: true
409
+ });
410
+
411
+ if (p.isCancel(continueAnyway) || !continueAnyway) {
412
+ p.log.info('Operation cancelled.');
413
+ return;
414
+ }
415
+ }
416
+ } else {
417
+ p.log.info('Skipping dependency installation. You can install them manually later.');
418
+ }
419
+ }
420
+
421
+ // Copy regular components
422
+ for (const component of regularComponents) {
423
+ const spinner = p.spinner();
424
+ spinner.start(`Copying ${component.name} component...`);
425
+
426
+ const examplesToCopy = component.examples || [];
427
+ const success = await copyComponent(component, templatesDir, examplesToCopy);
428
+
429
+ if (success) {
430
+ spinner.stop(`✅ Component ${component.name} copied successfully!`);
431
+ p.log.success(`Component files are now available at: src/components/${component.name}/`);
432
+ } else {
433
+ spinner.stop(`⚠️ Component ${component.name} was not copied.`);
434
+ }
84
435
  }
85
436
  }
86
437
  }