@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
package/README.md CHANGED
@@ -59,18 +59,20 @@ robin-library-cli
59
59
  ```
60
60
 
61
61
  The CLI will:
62
- 1. Display a welcome message
62
+ 1. Display a welcome message with a link to view examples online
63
63
  2. Check for React dependency in your `package.json` (warns if not found)
64
64
  3. Show an interactive menu of available components
65
- 4. Copy the selected component to `src/components/{component-name}/`
65
+ 4. For components with variants, show a submenu to select specific configurations
66
+ 5. Copy the selected component to `src/components/{component-name}/`
66
67
 
67
68
  ### Component Structure & Flow
68
69
 
69
70
  Flow:
70
- 1. Main menu (for now: Components or Exit)
71
- 2. Components: multiselect prompt — Please select one or more components
72
- 3. Copy selected components (one run can copy many)
73
- 4. Post-action prompt: “Do something else?” (loop back to menu) or Exit
71
+ 1. Main menu (Components or Exit)
72
+ 2. Components: multiselect prompt — "Please select one or more components"
73
+ 3. **Submenu Selection** (for components with variants): Some components offer submenus to select specific variants or configurations
74
+ 4. Copy selected components (one run can copy many)
75
+ 5. Post-action prompt: "Do something else?" (loop back to menu) or Exit
74
76
 
75
77
  Components are copied to the following structure:
76
78
 
@@ -97,33 +99,77 @@ Additional safety:
97
99
 
98
100
  ## Available Components
99
101
 
100
- ### Example Component
101
-
102
- - **Name**: `example`
103
- - **Description**: A simple example component with Hello World
104
- - **Files**: `example.tsx`
105
- - **Location**: `src/components/example/example.tsx`
106
-
107
- ### Button Component
108
-
109
- - **Name**: `button`
110
- - **Description**: Minimal button component
111
- - **Files**: `button.tsx`
112
- - **Location**: `src/components/button/button.tsx`
113
-
114
- ### Card Component
115
-
116
- - **Name**: `card`
117
- - **Description**: Minimal card wrapper
118
- - **Files**: `card.tsx`
119
- - **Location**: `src/components/card/card.tsx`
120
-
121
- ### Hero Component
122
-
123
- - **Name**: `hero`
124
- - **Description**: Minimal hero section
125
- - **Files**: `hero.tsx`
126
- - **Location**: `src/components/hero/hero.tsx`
102
+ ### Carousel Component
103
+
104
+ - **Name**: `carousel`
105
+ - **Description**: Carousel component with navigation and pagination
106
+ - **Dependencies**: `embla-carousel-react`, `embla-carousel`, `class-variance-authority`
107
+ - **Files**:
108
+ - `carousel.barrel.tsx` (main component)
109
+ - `components/` (navigation buttons, pagination, provider)
110
+ - `hooks/` (use-slide-active hook)
111
+ - `styles/` (embla.css styles)
112
+ - `examples/` (example implementations)
113
+ - **Location**: `src/components/carousel/`
114
+ - **Features**:
115
+ - Navigation buttons (prev/next)
116
+ - Multiple pagination types (dots, numbers, progress bar)
117
+ - Responsive breakpoints
118
+ - Loop support
119
+ - Slide active state tracking
120
+
121
+ ### In-View Component
122
+
123
+ - **Name**: `in-view`
124
+ - **Description**: In view animations for scroll-triggered effects
125
+ - **Dependencies**: `motion` (Framer Motion)
126
+ - **Location**: `src/components/in-view/`
127
+ - **Variants** (selectable via submenu):
128
+ - **Grid**: Grid-based animations
129
+ - Files: `in-view-grid.tsx`, `in-view-animation.tsx`, `data.in-view.ts`
130
+ - Example: `in-view-grid-showcase.tsx`
131
+ - **Individual Elements**: Individual element animations
132
+ - Files: `in-view-hidden-text.tsx`, `in-view-stroke-line.tsx`, `in-view-animation.tsx`, `data.in-view.ts`
133
+ - Example: `in-view-examples.home.tsx`
134
+ - **Features**:
135
+ - Scroll-triggered animations
136
+ - Grid-based and individual element animations
137
+ - Reusable animation utilities
138
+ - Type-safe data attributes
139
+
140
+ ### Ticker Component
141
+
142
+ - **Name**: `ticker`
143
+ - **Description**: Ticker component with hover and non-hover variants
144
+ - **Dependencies**: `motion` (Framer Motion)
145
+ - **Location**: `src/components/ticker/`
146
+ - **Variants** (selectable via submenu):
147
+ - **Stop on hover ticker (motion)**: MotionTicker with hover stop functionality
148
+ - Files: `motion-ticker.tsx`, `hooks/` (use-ticker-clones, use-ticker-incremental)
149
+ - Example: `ticker-hover-showcase.home.tsx`
150
+ - **Non-stop on hover ticker (css)**: TickerStatic with CSS animations
151
+ - Files: `css-ticker/css-ticker.tsx`, `css-ticker/ticker.keyframes.css`
152
+ - Example: `ticker-static-showcase.home.tsx`
153
+ - **Features**:
154
+ - Infinite scrolling ticker
155
+ - Hover to pause (motion variant)
156
+ - CSS-based animation option
157
+ - Customizable speed and direction
158
+
159
+ ### Scroll Tracker Component
160
+
161
+ - **Name**: `scroll-tracker`
162
+ - **Description**: Scroll tracker provider for scroll-driven animations
163
+ - **Dependencies**: `motion` (Framer Motion)
164
+ - **Files**:
165
+ - `scroll-tracker-provider.tsx` (main provider component)
166
+ - `examples/scroll-tracker-showcase.tsx` (example implementation)
167
+ - **Location**: `src/components/scroll-tracker/`
168
+ - **Features**:
169
+ - Scroll progress tracking
170
+ - Context-based scroll state management
171
+ - Enables scroll-driven animations throughout your app
172
+ - Works seamlessly with Framer Motion
127
173
 
128
174
  More components will be added in future updates.
129
175
 
@@ -132,6 +178,7 @@ More components will be added in future updates.
132
178
  - ✨ Beautiful, modern UI with Astro-like aesthetics
133
179
  - 🎯 Interactive component selection
134
180
  - 🔍 Automatic React dependency detection
181
+ - 📦 Automatic dependency installation (detects and installs missing npm packages)
135
182
  - 🛡️ Safe overwrite handling (checks for empty directories)
136
183
  - 📁 Automatic directory creation
137
184
  - ⚠️ Helpful warnings and error messages
@@ -141,13 +188,15 @@ More components will be added in future updates.
141
188
 
142
189
  1. **Dependency Check**: The CLI checks your `package.json` for React. If not found, it displays a warning but still allows you to proceed (you might add React later).
143
190
 
144
- 2. **Component Selection**: An interactive menu displays all available components with descriptions.
191
+ 2. **Component Selection**: An interactive menu displays all available components with descriptions. You can select multiple components at once. Some components offer submenus to select specific variants or configurations (e.g., Grid vs Individual Elements for in-view animations).
192
+
193
+ 3. **Dependency Installation**: The CLI automatically detects missing dependencies required by selected components and offers to install them using your project's package manager (npm, pnpm, or yarn).
145
194
 
146
- 3. **Directory Check**: Before copying, the CLI checks if the destination directory exists and whether it's empty.
195
+ 4. **Directory Check**: Before copying, the CLI checks if the destination directory exists and whether it's empty.
147
196
 
148
- 4. **Copy Operation**: All files from the component template are copied to `src/components/{component-name}/`, creating directories as needed.
197
+ 5. **Copy Operation**: All files from the component template are copied to `src/components/{component-name}/`, creating directories as needed.
149
198
 
150
- 5. **Completion**: A success message confirms the component has been added to your project.
199
+ 6. **Completion**: A success message confirms the component has been added to your project.
151
200
 
152
201
  ## Common Use Cases
153
202
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@androbinco/library-cli",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Interactive CLI tool to add Robin library components to your project",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -11,13 +11,14 @@
11
11
  "src/",
12
12
  "README.md"
13
13
  ],
14
- "keywords": [
15
- "cli",
16
- "robin",
17
- "library",
18
- "components",
19
- "react"
20
- ],
14
+ "scripts": {
15
+ "start": "node ./src/index.js",
16
+ "dev": "node ./src/index.js",
17
+ "test": "echo \"Error: no test specified for robin-library-cli\" && exit 1",
18
+ "build": "echo \"robin-library-cli: No build step required.\"",
19
+ "lint": "echo \"robin-library-cli: Lint not configured.\""
20
+ },
21
+ "keywords": ["cli", "robin", "library", "components", "react"],
21
22
  "author": "androbinco",
22
23
  "license": "MIT",
23
24
  "dependencies": {
@@ -29,12 +30,6 @@
29
30
  },
30
31
  "publishConfig": {
31
32
  "access": "public"
32
- },
33
- "scripts": {
34
- "start": "node ./src/index.js",
35
- "dev": "node ./src/index.js",
36
- "test": "echo \"Error: no test specified for robin-library-cli\" && exit 1",
37
- "build": "echo \"robin-library-cli: No build step required.\"",
38
- "lint": "echo \"robin-library-cli: Lint not configured.\""
39
33
  }
40
- }
34
+ }
35
+
@@ -0,0 +1,107 @@
1
+ import path from 'path';
2
+ import { fileURLToPath } from 'url';
3
+ import {
4
+ COMPONENTS,
5
+ getComponentByName,
6
+ getVariantByName,
7
+ getFilesForVariant
8
+ } from '../utils/components.js';
9
+ import {
10
+ detectPackageManager,
11
+ checkMissingDependencies,
12
+ installDependencies
13
+ } from '../utils/dependencies.js';
14
+ import { copyComponent } from '../utils/files.js';
15
+
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = path.dirname(__filename);
18
+ const TEMPLATES_DIR = path.join(__dirname, '..', 'templates');
19
+
20
+ export async function handleAdd(args) {
21
+ const [componentName, variantName] = args;
22
+
23
+ if (!componentName) {
24
+ console.error('Error: Component name is required');
25
+ console.log('Usage: add <component> [variant]');
26
+ console.log('Run "list" to see available components');
27
+ process.exit(1);
28
+ }
29
+
30
+ const component = getComponentByName(componentName);
31
+
32
+ if (!component) {
33
+ console.error(`Error: Component "${componentName}" not found`);
34
+ console.log('Available components:');
35
+ COMPONENTS.forEach(c => console.log(` - ${c.name}`));
36
+ process.exit(1);
37
+ }
38
+
39
+ // Check if component requires variant
40
+ if (component.hasSubmenu && !variantName) {
41
+ console.error(`Error: Component "${componentName}" requires a variant`);
42
+ console.log('Available variants:');
43
+ component.submenuOptions.forEach(v => console.log(` - ${v.value}: ${v.description}`));
44
+ process.exit(1);
45
+ }
46
+
47
+ // Get variant if specified
48
+ let variant = null;
49
+ let filesToCopy = null;
50
+ let examplesToCopy = [];
51
+
52
+ if (component.hasSubmenu && variantName) {
53
+ variant = getVariantByName(component, variantName);
54
+
55
+ if (!variant) {
56
+ console.error(`Error: Variant "${variantName}" not found for component "${componentName}"`);
57
+ console.log('Available variants:');
58
+ component.submenuOptions.forEach(v => console.log(` - ${v.value}: ${v.description}`));
59
+ process.exit(1);
60
+ }
61
+
62
+ filesToCopy = getFilesForVariant(component, variant);
63
+ examplesToCopy = variant.examples || [];
64
+ } else {
65
+ // Simple component without variant
66
+ examplesToCopy = component.examples || [];
67
+ }
68
+
69
+ // Determine dependencies
70
+ let dependencies = component.dependencies || [];
71
+
72
+ // For ticker non-hover, no motion needed
73
+ if (component.name === 'ticker' && variantName === 'non-hover') {
74
+ dependencies = [];
75
+ }
76
+
77
+ // Check and install dependencies
78
+ if (dependencies.length > 0) {
79
+ const missingDependencies = await checkMissingDependencies(dependencies);
80
+
81
+ if (missingDependencies.length > 0) {
82
+ console.log(`Installing dependencies: ${missingDependencies.join(', ')}`);
83
+ const packageManager = await detectPackageManager();
84
+ const success = await installDependencies(missingDependencies, packageManager);
85
+
86
+ if (!success) {
87
+ console.warn('Warning: Failed to install some dependencies. You may need to install them manually.');
88
+ }
89
+ }
90
+ }
91
+
92
+ // Copy component
93
+ console.log(`Copying ${componentName}${variantName ? ` (${variantName})` : ''}...`);
94
+
95
+ const success = await copyComponent(component, TEMPLATES_DIR, examplesToCopy, filesToCopy);
96
+
97
+ if (success) {
98
+ console.log(`✅ Component ${componentName} copied successfully!`);
99
+ console.log(`Files are available at: src/components/${componentName}/`);
100
+ if (examplesToCopy.length > 0) {
101
+ console.log(`Examples are available at: src/components/${componentName}/examples/`);
102
+ }
103
+ } else {
104
+ console.error(`❌ Failed to copy component ${componentName}`);
105
+ process.exit(1);
106
+ }
107
+ }
@@ -0,0 +1,51 @@
1
+ import { COMPONENTS, getComponentByName } from '../utils/components.js';
2
+
3
+ export function handleList(args) {
4
+ const [componentName] = args;
5
+
6
+ if (!componentName) {
7
+ // List all components
8
+ console.log('Available components:\n');
9
+
10
+ for (const component of COMPONENTS) {
11
+ console.log(` ${component.name}`);
12
+ console.log(` ${component.description}`);
13
+
14
+ if (component.hasSubmenu) {
15
+ console.log(` Variants: ${component.submenuOptions.map(v => v.value).join(', ')}`);
16
+ }
17
+
18
+ console.log('');
19
+ }
20
+
21
+ console.log('Usage:');
22
+ console.log(' add <component> Copy a component');
23
+ console.log(' add <component> <variant> Copy a specific variant');
24
+ console.log(' list <component> List variants for a component');
25
+ } else {
26
+ // List variants for specific component
27
+ const component = getComponentByName(componentName);
28
+
29
+ if (!component) {
30
+ console.error(`Error: Component "${componentName}" not found`);
31
+ console.log('Run "list" to see all available components');
32
+ process.exit(1);
33
+ }
34
+
35
+ console.log(`Component: ${component.name}`);
36
+ console.log(`Description: ${component.description}`);
37
+ console.log(`Dependencies: ${component.dependencies?.join(', ') || 'none'}`);
38
+
39
+ if (component.hasSubmenu) {
40
+ console.log('\nVariants:');
41
+ for (const variant of component.submenuOptions) {
42
+ console.log(` ${variant.value}`);
43
+ console.log(` ${variant.description}`);
44
+ }
45
+
46
+ console.log(`\nUsage: add ${component.name} <variant>`);
47
+ } else {
48
+ console.log(`\nUsage: add ${component.name}`);
49
+ }
50
+ }
51
+ }
package/src/index.js CHANGED
@@ -4,25 +4,44 @@ import * as p from '@clack/prompts';
4
4
  import path from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { checkReactDependency } from './utils/dependencies.js';
7
- import { handleComponentsFlow } from './utils/components.js';
7
+ import { handleComponentsFlow, COMPONENTS } from './utils/components.js';
8
+ import { handleAdd } from './commands/add.js';
9
+ import { handleList } from './commands/list.js';
8
10
 
9
11
  const __filename = fileURLToPath(import.meta.url);
10
12
  const __dirname = path.dirname(__filename);
11
13
  const TEMPLATES_DIR = path.join(__dirname, 'templates');
12
14
 
13
- // Component registry
14
- const COMPONENTS = [
15
- { name: 'example', description: 'Hello World example', sourceDir: 'example', dependencies: [] },
16
- { name: 'button', description: 'Minimal button component', sourceDir: 'button', dependencies: [] },
17
- { name: 'card', description: 'Minimal card wrapper', sourceDir: 'card', dependencies: [] },
18
- { name: 'hero', description: 'Minimal hero section', sourceDir: 'hero', dependencies: [] },
19
- {
20
- name: 'carousel',
21
- description: 'Carousel component with navigation and pagination',
22
- sourceDir: 'carousel',
23
- dependencies: ['embla-carousel-react', 'embla-carousel', 'class-variance-authority']
24
- }
25
- ];
15
+ // Parse command line arguments
16
+ const args = process.argv.slice(2);
17
+ const command = args[0];
18
+
19
+ // Handle direct commands (non-interactive mode)
20
+ if (command === 'add') {
21
+ await handleAdd(args.slice(1));
22
+ process.exit(0);
23
+ }
24
+
25
+ if (command === 'list') {
26
+ handleList(args.slice(1));
27
+ process.exit(0);
28
+ }
29
+
30
+ if (command === 'help' || command === '--help' || command === '-h') {
31
+ console.log('Robin Library CLI\n');
32
+ console.log('Usage:');
33
+ console.log(' robin-library-cli Interactive mode');
34
+ console.log(' robin-library-cli add <comp> Add a component');
35
+ console.log(' robin-library-cli list List all components');
36
+ console.log(' robin-library-cli list <comp> List variants for a component');
37
+ console.log(' robin-library-cli help Show this help\n');
38
+ console.log('Examples:');
39
+ console.log(' robin-library-cli add carousel');
40
+ console.log(' robin-library-cli add ticker hover');
41
+ console.log(' robin-library-cli add scroll-components parallax-image');
42
+ console.log(' robin-library-cli list ticker');
43
+ process.exit(0);
44
+ }
26
45
 
27
46
  async function mainMenu() {
28
47
  const action = await p.select({
@@ -40,6 +59,14 @@ async function main() {
40
59
  // Beautiful intro
41
60
  p.intro('✨ Welcome to Robin Library CLI');
42
61
 
62
+ // Add clickable link using ANSI escape codes for hyperlinks
63
+ const websiteUrl = 'https://robin-animation-library-620346143712.us-east1.run.app/';
64
+ const linkText = 'https://robin-animation-library-620346143712.us-east1.run.app/';
65
+ // ANSI escape code format: \u001b]8;;URL\u0007TEXT\u001b]8;;\u0007
66
+ const clickableLink = `\u001b]8;;${websiteUrl}\u0007${linkText}\u001b]8;;\u0007`;
67
+ p.log.info(`You can check examples in ${clickableLink}`);
68
+
69
+
43
70
  // Check React dependency
44
71
  await checkReactDependency();
45
72
 
@@ -52,7 +79,11 @@ async function main() {
52
79
  }
53
80
 
54
81
  if (action === 'components') {
55
- await handleComponentsFlow(COMPONENTS, TEMPLATES_DIR);
82
+ const result = await handleComponentsFlow(COMPONENTS, TEMPLATES_DIR);
83
+ // If user selected go back, skip the "do more" prompt and show main menu again
84
+ if (result && result.goBack) {
85
+ continue;
86
+ }
56
87
  }
57
88
 
58
89
  const doMore = await p.select({
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import React, { ComponentPropsWithRef } from "react";
2
3
 
3
4
  import { cva, VariantProps } from "class-variance-authority";
@@ -0,0 +1,69 @@
1
+ import { cva, VariantProps } from 'class-variance-authority';
2
+
3
+ import { cn } from '@/common/utils/classname-builder';
4
+
5
+ import { useCarouselContext } from '../provider/carousel.provider';
6
+
7
+ type DotButtonVariants = VariantProps<typeof dotButtonVariants>['variant'];
8
+
9
+ export type DotPaginationProps = {
10
+ scrollSnaps: number[];
11
+ variant?: 'primary';
12
+ className?: string;
13
+ };
14
+ const dotButtonVariants = cva(
15
+ 'size-3 cursor-pointer rounded-full transition-all duration-300 ease-out',
16
+ {
17
+ variants: {
18
+ variant: {
19
+ primary: 'focus:outline-2',
20
+ },
21
+ selected: {
22
+ true: 'w-9',
23
+ false: '',
24
+ },
25
+ },
26
+ compoundVariants: [
27
+ {
28
+ variant: 'primary',
29
+ selected: true,
30
+ className: 'bg-indigo-900',
31
+ },
32
+ {
33
+ variant: 'primary',
34
+ selected: false,
35
+ className: 'bg-indigo-500',
36
+ },
37
+ ],
38
+ },
39
+ );
40
+
41
+ export const DotButton: React.FC<{ index: number; variant: DotButtonVariants }> = ({
42
+ index,
43
+ variant = 'primary',
44
+ ...restProps
45
+ }) => {
46
+ const { selectedIndex, onDotButtonClick } = useCarouselContext();
47
+
48
+ return (
49
+ <button
50
+ className={dotButtonVariants({ variant, selected: selectedIndex === index })}
51
+ disabled={selectedIndex === index}
52
+ onClick={() => onDotButtonClick(index)}
53
+ {...restProps}
54
+ />
55
+ );
56
+ };
57
+
58
+ export const BulletPagination = ({
59
+ scrollSnaps,
60
+ className,
61
+ variant = 'primary',
62
+ ...restProps
63
+ }: DotPaginationProps) => (
64
+ <div className={cn('mx-auto flex w-max gap-2', className)} {...restProps}>
65
+ {scrollSnaps.map((_, index) => (
66
+ <DotButton key={index} index={index} variant={variant} />
67
+ ))}
68
+ </div>
69
+ );
@@ -0,0 +1,30 @@
1
+ import { VariantProps } from 'class-variance-authority';
2
+
3
+ import { cn } from '@/common/utils/classname-builder';
4
+ import { Text } from '@/common/ui/text/text';
5
+ import { textVariants } from '@/common/ui/text/text.styles';
6
+
7
+ export type NumberPaginationProps = {
8
+ className?: string;
9
+ variant?: VariantProps<typeof textVariants>['variant'];
10
+ textClassName?: string;
11
+ selectedIndex: number;
12
+ scrollSnaps: number[];
13
+ };
14
+
15
+ export const NumberPagination = ({
16
+ className,
17
+ variant,
18
+ textClassName,
19
+ selectedIndex,
20
+ scrollSnaps,
21
+ ...props
22
+ }: NumberPaginationProps) => {
23
+ return (
24
+ <div className={cn('mx-auto flex w-max gap-2', className)} {...props}>
25
+ <Text className={textClassName} variant={variant}>
26
+ {selectedIndex + 1} / {scrollSnaps.length}
27
+ </Text>
28
+ </div>
29
+ );
30
+ };
@@ -0,0 +1,99 @@
1
+ import { cva, VariantProps } from 'class-variance-authority';
2
+
3
+ import { cn } from '@/common/utils/classname-builder';
4
+
5
+ import { useSlideProgress } from './use-slide-progress';
6
+
7
+ const effects = {
8
+ scale: (selectedIndex: number, totalItems: number) => ({
9
+ transformOrigin: 'left',
10
+ transform: `scaleX(${(selectedIndex + 1) / totalItems})`,
11
+ }),
12
+ slider: (selectedIndex: number, totalItems: number) => {
13
+ if (totalItems === 0) return {};
14
+ const width = Math.ceil(100 / totalItems);
15
+ const left = Math.ceil((selectedIndex / totalItems) * 100);
16
+
17
+ return {
18
+ width: `${width}%`,
19
+ cursor: 'grab',
20
+ transformOrigin: 'left',
21
+ left: `${left}%`,
22
+ };
23
+ },
24
+ };
25
+
26
+ export type ProgressPaginationProps = {
27
+ className?: string;
28
+ selectedIndex: number;
29
+ scrollSnaps: number[];
30
+ effect?: keyof typeof effects;
31
+ onDotButtonClick: (index: number) => void;
32
+ variant?: VariantProps<typeof backgroundVariants>['variant'];
33
+ };
34
+
35
+ const backgroundVariants = cva('relative mx-auto flex w-200 max-w-full overflow-hidden', {
36
+ variants: {
37
+ variant: {
38
+ primary: 'h-2 rounded-full bg-grey-50-30',
39
+ },
40
+ effect: {
41
+ slider: 'bg-grey-50-6',
42
+ scale: 'bg-grey-50-30',
43
+ },
44
+ },
45
+ });
46
+ const progressVariants = cva('linear absolute bottom-0 left-0 transition-all duration-300', {
47
+ variants: {
48
+ variant: {
49
+ primary: 'h-full rounded-full bg-indigo-500',
50
+ },
51
+ effect: {
52
+ slider: '',
53
+ scale: 'pointer-events-none !w-full',
54
+ },
55
+ },
56
+ });
57
+
58
+ export const ProgressPagination = ({
59
+ className,
60
+ selectedIndex,
61
+ scrollSnaps,
62
+ onDotButtonClick,
63
+ variant = 'primary',
64
+ effect = 'scale',
65
+ ...props
66
+ }: ProgressPaginationProps) => {
67
+ const isSlider = effect === 'slider';
68
+ const { isDragging, barRef, setIsDragging } = useSlideProgress({ isActive: isSlider });
69
+
70
+ return (
71
+ <div className={cn(backgroundVariants({ variant, effect }), className)} {...props}>
72
+ <button
73
+ ref={barRef}
74
+ className={cn(progressVariants({ variant, effect }))}
75
+ style={{ ...effects[effect](selectedIndex, scrollSnaps.length) }}
76
+ onMouseDown={() => {
77
+ if (!isSlider) return;
78
+ setIsDragging(true);
79
+ }}
80
+ onMouseUp={() => setIsDragging(false)}
81
+ />
82
+
83
+ {scrollSnaps.map((_, index) => (
84
+ <div
85
+ key={index}
86
+ className={cn('h-full w-full', !isDragging && 'cursor-pointer')}
87
+ onMouseDown={() => {
88
+ onDotButtonClick(index);
89
+ if (!isSlider) return;
90
+ setIsDragging(true);
91
+ }}
92
+ onMouseEnter={() => {
93
+ if (isDragging) onDotButtonClick(index);
94
+ }}
95
+ />
96
+ ))}
97
+ </div>
98
+ );
99
+ };