@androbinco/library-cli 0.2.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 (40) hide show
  1. package/package.json +1 -1
  2. package/src/commands/add.js +107 -0
  3. package/src/commands/list.js +51 -0
  4. package/src/index.js +38 -73
  5. package/src/templates/carousel/components/navigation-buttons.tsx +1 -0
  6. package/src/templates/carousel/components/pagination/bullet.pagination.carousel.tsx +69 -0
  7. package/src/templates/carousel/components/pagination/number.pagination.carousel.tsx +30 -0
  8. package/src/templates/carousel/components/pagination/progress/progress.pagination.carousel.tsx +99 -0
  9. package/src/templates/carousel/components/pagination/progress/use-slide-progress.tsx +31 -0
  10. package/src/templates/carousel/components/pagination.tsx +47 -82
  11. package/src/templates/faqs-accordion/examples/faqs-showcase.tsx +42 -0
  12. package/src/templates/faqs-accordion/faqs-accordion.tsx +70 -0
  13. package/src/templates/faqs-accordion/mock-data.ts +38 -0
  14. package/src/templates/faqs-accordion/types.ts +18 -0
  15. package/src/templates/in-view/data.in-view.ts +1 -1
  16. package/src/templates/in-view/in-view-animation.tsx +7 -7
  17. package/src/templates/in-view/in-view-grid.tsx +22 -20
  18. package/src/templates/in-view/in-view-hidden-text.tsx +7 -7
  19. package/src/templates/in-view/in-view-stroke-line.tsx +6 -5
  20. package/src/templates/lenis/examples/providers.tsx +23 -0
  21. package/src/templates/lenis/lenis-provider.tsx +46 -0
  22. package/src/templates/scroll-components/hooks/use-client-dimensions.ts +21 -0
  23. package/src/templates/scroll-components/parallax/examples/parallax-showcase.tsx +87 -0
  24. package/src/templates/scroll-components/parallax/parallax.css +36 -0
  25. package/src/templates/scroll-components/parallax/parallax.tsx +67 -0
  26. package/src/templates/scroll-components/scale-gallery/components/expanding-element.tsx +40 -0
  27. package/src/templates/scroll-components/scale-gallery/examples/scale-gallery-showcase.tsx +68 -0
  28. package/src/templates/scroll-components/scale-gallery/scale-gallery.tsx +57 -0
  29. package/src/templates/scroll-components/scroll-tracker-showcase.tsx +44 -0
  30. package/src/templates/strapi-dynamic-zone/README.md +157 -0
  31. package/src/templates/strapi-dynamic-zone/dynamic-zone.tsx +113 -0
  32. package/src/templates/strapi-dynamic-zone/examples/page.tsx +53 -0
  33. package/src/templates/strapi-dynamic-zone/examples/renderers.tsx +74 -0
  34. package/src/templates/strapi-dynamic-zone/examples/types.ts +41 -0
  35. package/src/templates/strapi-dynamic-zone/index.ts +11 -0
  36. package/src/templates/strapi-dynamic-zone/types.ts +73 -0
  37. package/src/utils/components.js +187 -5
  38. package/src/utils/files.js +13 -12
  39. package/src/templates/scroll-tracker/examples/scroll-tracker-showcase.tsx +0 -90
  40. /package/src/templates/{scroll-tracker → scroll-components}/scroll-tracker-provider.tsx +0 -0
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Strapi Dynamic Zone Type Utilities
3
+ *
4
+ * These types provide type-safe rendering for Strapi Dynamic Zones.
5
+ * Define your component union type with `__component` discriminator,
6
+ * then use these utilities for type-safe prop extraction.
7
+ */
8
+
9
+ /**
10
+ * Base constraint for Strapi dynamic zone components.
11
+ * All components must have a `__component` string identifier.
12
+ */
13
+ export type StrapiComponent = {
14
+ __component: string;
15
+ id?: number | string;
16
+ };
17
+
18
+ /**
19
+ * Extracts the props for a specific component type from a union,
20
+ * omitting the `__component` discriminator.
21
+ *
22
+ * @example
23
+ * type HeroProps = ExtractComponentProps<PageComponent, 'hero.hero'>;
24
+ * // Results in: { title: string; subtitle: string; image: ImageType }
25
+ */
26
+ export type ExtractComponentProps<
27
+ TUnion extends StrapiComponent,
28
+ TComponentKey extends TUnion['__component']
29
+ > = Omit<Extract<TUnion, { __component: TComponentKey }>, '__component'>;
30
+
31
+ /**
32
+ * Creates a type-safe renderer map from a component union.
33
+ * Each key is a `__component` value, each value is a render function.
34
+ *
35
+ * @example
36
+ * const renderers: ComponentRendererMap<PageComponent> = {
37
+ * 'hero.hero': (props) => <Hero {...props} />,
38
+ * 'cta.cta': (props) => <CTA {...props} />,
39
+ * };
40
+ */
41
+ export type ComponentRendererMap<
42
+ TUnion extends StrapiComponent,
43
+ TExtra = object
44
+ > = {
45
+ [K in TUnion['__component']]: (
46
+ props: ExtractComponentProps<TUnion, K> & TExtra
47
+ ) => React.ReactElement | null;
48
+ };
49
+
50
+ /**
51
+ * Utility type to get all component keys from a union.
52
+ */
53
+ export type ComponentKeys<TUnion extends StrapiComponent> =
54
+ TUnion['__component'];
55
+
56
+ /**
57
+ * Props for the DynamicZone component (renders multiple components).
58
+ */
59
+ export type DynamicZoneProps<
60
+ TUnion extends StrapiComponent,
61
+ TExtra = object
62
+ > = {
63
+ /** Array of components from Strapi Dynamic Zone */
64
+ components: TUnion[];
65
+ /** Map of component renderers */
66
+ renderers: ComponentRendererMap<TUnion, TExtra>;
67
+ /** Function to generate extra props per component (receives component and index) */
68
+ getExtraProps?: (component: TUnion, index: number) => TExtra;
69
+ /** Optional fallback for unknown components */
70
+ fallback?: (component: TUnion) => React.ReactElement | null;
71
+ /** Optional key generator (defaults to component.id or index) */
72
+ getKey?: (component: TUnion, index: number) => string | number;
73
+ };
@@ -9,6 +9,162 @@ import { copyComponent, checkComponentExists } from './files.js';
9
9
  import fs from 'fs-extra';
10
10
  import path from 'path';
11
11
 
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
+ }
163
+ }
164
+
165
+ return files;
166
+ }
167
+
12
168
  export async function handleSubmenuFlow(component, templatesDir) {
13
169
  if (!component.hasSubmenu || !component.submenuOptions || component.submenuOptions.length === 0) {
14
170
  p.log.error(`Component "${component.name}" is not configured for submenu flow.`);
@@ -143,6 +299,24 @@ export async function handleSubmenuFlow(component, templatesDir) {
143
299
  // Non-hover option: copy css-ticker directory
144
300
  allComponentsToCopy.add('css-ticker');
145
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
+ }
146
320
  } else {
147
321
  // No nested submenu - copy all component files (legacy behavior)
148
322
  // This maintains backward compatibility
@@ -324,11 +498,14 @@ export async function handleComponentsFlow(components, templatesDir) {
324
498
  while (true) {
325
499
  const selectedComponents = await p.multiselect({
326
500
  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
- })),
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
+ ],
332
509
  required: true
333
510
  });
334
511
 
@@ -337,6 +514,11 @@ export async function handleComponentsFlow(components, templatesDir) {
337
514
  return;
338
515
  }
339
516
 
517
+ // Check if user selected go back
518
+ if (selectedComponents.includes('__go_back__')) {
519
+ return { goBack: true };
520
+ }
521
+
340
522
  if (!selectedComponents.length) {
341
523
  p.log.info('No components selected.');
342
524
  return;
@@ -31,24 +31,25 @@ export async function copyExamples(component, templatesDir, examplesToCopy) {
31
31
  return true;
32
32
  }
33
33
 
34
- const examplesSourcePath = path.join(templatesDir, component.sourceDir, 'examples');
35
- const examplesDestPath = path.resolve(process.cwd(), 'src', 'components', component.name, 'examples');
36
-
37
- if (!(await fs.pathExists(examplesSourcePath))) {
38
- p.log.warn(`Examples directory not found at ${examplesSourcePath}. Skipping examples.`);
39
- return true;
40
- }
34
+ const componentSourcePath = path.join(templatesDir, component.sourceDir);
35
+ const componentDestPath = path.resolve(process.cwd(), 'src', 'components', component.name);
41
36
 
42
37
  try {
43
- // Ensure destination directory exists
44
- await fs.ensureDir(examplesDestPath);
45
-
46
38
  // Copy selected example files
47
39
  for (const exampleFile of examplesToCopy) {
48
- const sourceFile = path.join(examplesSourcePath, exampleFile);
49
- const destFile = path.join(examplesDestPath, exampleFile);
40
+ // Support both nested paths (e.g., 'parallax/examples/parallax-showcase.tsx')
41
+ // and flat paths (e.g., 'scroll-tracker-showcase.tsx')
42
+ const sourceFile = path.join(componentSourcePath, exampleFile);
43
+
44
+ // For nested paths like 'parallax/examples/parallax-showcase.tsx',
45
+ // copy to 'examples/parallax-showcase.tsx' in destination
46
+ // For flat paths like 'scroll-tracker-showcase.tsx',
47
+ // copy to 'examples/scroll-tracker-showcase.tsx' in destination
48
+ const filename = path.basename(exampleFile);
49
+ const destFile = path.join(componentDestPath, 'examples', filename);
50
50
 
51
51
  if (await fs.pathExists(sourceFile)) {
52
+ await fs.ensureDir(path.dirname(destFile));
52
53
  await fs.copy(sourceFile, destFile, { overwrite: true });
53
54
  } else {
54
55
  p.log.warn(`Example file "${exampleFile}" not found. Skipping.`);
@@ -1,90 +0,0 @@
1
- "use client";
2
- import { motion, MotionValue, useSpring, useTransform } from "motion/react";
3
-
4
- import {
5
- ScrollTrackerProvider,
6
- useScrollTrackerContext,
7
- } from "../scroll-tracker-provider";
8
- import { Center } from "@/common/ui/containers/center";
9
- import { Text } from "@/common/ui/text/text";
10
-
11
- /*
12
- This is a scroll tracker component that provides scroll-driven animations.
13
- It uses the motion library to track scroll progress and provides a context
14
- for child components to access the scroll progress.
15
-
16
- To test it you can just use it like this in any page:
17
- <ScrollTrackerShowcase />
18
- */
19
-
20
- const AnimatedText = ({
21
- text,
22
- motionValue,
23
- offsetIntro,
24
- opacityOffset = [0, 0.5, 1],
25
- yOffset = [100, -20, 0],
26
- }: {
27
- text: string;
28
- motionValue: MotionValue<number>;
29
- offsetIntro: [number, number] | [number, number, number];
30
- opacityOffset?: [number, number] | [number, number, number];
31
- yOffset?: [number, number] | [number, number, number];
32
- }) => {
33
- const animateEntry = useTransform(motionValue, offsetIntro, opacityOffset);
34
- const y = useSpring(useTransform(motionValue, offsetIntro, yOffset));
35
-
36
- return (
37
- <motion.div style={{ opacity: animateEntry, y }}>
38
- <Text className="text-white">{text}</Text>
39
- </motion.div>
40
- );
41
- };
42
-
43
- const ScrollSection = () => {
44
- const { scrollYProgress } = useScrollTrackerContext();
45
- const text = "SCROLL DRIVEN ANIMATION".split("");
46
- const showImage = useTransform(scrollYProgress, [0.3, 1 * 0.9], [0, 1]);
47
- const rotateImage = useTransform(scrollYProgress, [0.3, 1 * 0.9], [0, 360]);
48
-
49
- return (
50
- <div className="relative flex h-full w-full flex-col items-center justify-center rounded-2xl border-2 border-fill-brand-primary bg-bg-primary-inverse p-2">
51
- <motion.div
52
- className="absolute top-0 h-2 w-full bg-red-500"
53
- style={{ scaleX: scrollYProgress, transformOrigin: "left center" }}
54
- />
55
- <div className="flex flex-col gap-2">
56
- <div className="flex flex-row gap-0.5">
57
- {text.map((char, index) => (
58
- <AnimatedText
59
- key={index}
60
- motionValue={scrollYProgress}
61
- offsetIntro={[
62
- ((index + 1) / text.length) * 0.3,
63
- ((index + 1) / text.length) * 0.5,
64
- ((index + 1) / text.length) * 0.9,
65
- ]}
66
- text={char}
67
- />
68
- ))}
69
- </div>
70
- <motion.img
71
- alt="scroll-tracker-showcase"
72
- className="mx-auto size-40 object-contain"
73
- src="/img/good-emoji.png"
74
- style={{ opacity: showImage, rotate: rotateImage }}
75
- />
76
- </div>
77
- </div>
78
- );
79
- };
80
-
81
- export const ScrollTrackerShowcase = () => {
82
- return (
83
- <div className="w-full">
84
- <Text className="mb-4">Scroll Tracker</Text>
85
- <ScrollTrackerProvider height={200} offset={["0 0", "1 1"]}>
86
- <ScrollSection />
87
- </ScrollTrackerProvider>
88
- </div>
89
- );
90
- };