@androbinco/library-cli 0.1.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.
- package/README.md +262 -0
- package/package.json +40 -0
- package/src/index.js +79 -0
- package/src/templates/button/button.tsx +5 -0
- package/src/templates/card/card.tsx +5 -0
- package/src/templates/carousel/carousel.barrel.tsx +108 -0
- package/src/templates/carousel/components/navigation-buttons.tsx +100 -0
- package/src/templates/carousel/components/pagination.tsx +89 -0
- package/src/templates/carousel/components/provider/carousel.provider.tsx +72 -0
- package/src/templates/carousel/components/provider/use-navigation.tsx +52 -0
- package/src/templates/carousel/components/provider/use-pagination.tsx +56 -0
- package/src/templates/carousel/examples/base-cards.carousel.tsx +60 -0
- package/src/templates/carousel/examples/gallery-carousel.tsx +53 -0
- package/src/templates/carousel/examples/loop-carousel.tsx +43 -0
- package/src/templates/carousel/hooks/use-slide-active.tsx +9 -0
- package/src/templates/carousel/styles/embla.css +63 -0
- package/src/templates/example/example.tsx +5 -0
- package/src/templates/hero/hero.tsx +5 -0
- package/src/utils/components.js +87 -0
- package/src/utils/dependencies.js +134 -0
- package/src/utils/files.js +68 -0
package/README.md
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# @androbinco/library-cli
|
|
2
|
+
|
|
3
|
+
Interactive CLI tool to add Robin library components to your React project. This tool provides a beautiful, user-friendly interface for selecting and copying components into your project structure.
|
|
4
|
+
|
|
5
|
+
## Description
|
|
6
|
+
|
|
7
|
+
`@androbinco/library-cli` is an interactive command-line tool that helps you quickly add pre-built React components to your project. With a modern, Astro-like UI powered by `@clack/prompts`, you can easily browse and install components with just a few keystrokes.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
### Global Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g @androbinco/library-cli
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Using npx (without installation)
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx @androbinco/library-cli
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Using pnpx (for pnpm users)
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pnpx @androbinco/library-cli
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Prerequisites
|
|
30
|
+
|
|
31
|
+
- Node.js (v14 or higher)
|
|
32
|
+
- A React project (components require React to function)
|
|
33
|
+
- `package.json` in your project root (recommended for dependency detection)
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
1. Navigate to your React project directory:
|
|
38
|
+
```bash
|
|
39
|
+
cd my-react-project
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
2. Run the CLI:
|
|
43
|
+
```bash
|
|
44
|
+
npx @androbinco/library-cli
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
3. Select a component from the interactive menu
|
|
48
|
+
|
|
49
|
+
4. The component will be copied to `src/components/{component-name}/`
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
### Basic Usage
|
|
54
|
+
|
|
55
|
+
Simply run the CLI from your project root:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
robin-library-cli
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The CLI will:
|
|
62
|
+
1. Display a welcome message
|
|
63
|
+
2. Check for React dependency in your `package.json` (warns if not found)
|
|
64
|
+
3. Show an interactive menu of available components
|
|
65
|
+
4. Copy the selected component to `src/components/{component-name}/`
|
|
66
|
+
|
|
67
|
+
### Component Structure & Flow
|
|
68
|
+
|
|
69
|
+
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
|
|
74
|
+
|
|
75
|
+
Components are copied to the following structure:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
your-project/
|
|
79
|
+
├── src/
|
|
80
|
+
│ └── components/
|
|
81
|
+
│ └── {component-name}/
|
|
82
|
+
│ └── {component-files}
|
|
83
|
+
└── package.json
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Overwrite & Safety Behavior
|
|
87
|
+
|
|
88
|
+
If a component directory already exists:
|
|
89
|
+
- **Empty directory**: The component will be copied without prompting
|
|
90
|
+
- **Non-empty directory**: You'll be prompted to confirm overwrite
|
|
91
|
+
- Select `Yes` to overwrite existing files
|
|
92
|
+
- Select `No` to cancel the operation
|
|
93
|
+
|
|
94
|
+
Additional safety:
|
|
95
|
+
- Destination paths are validated to stay inside your current project
|
|
96
|
+
- Templates are resolved via ESM-safe `import.meta.url`
|
|
97
|
+
|
|
98
|
+
## Available Components
|
|
99
|
+
|
|
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`
|
|
127
|
+
|
|
128
|
+
More components will be added in future updates.
|
|
129
|
+
|
|
130
|
+
## Features
|
|
131
|
+
|
|
132
|
+
- ✨ Beautiful, modern UI with Astro-like aesthetics
|
|
133
|
+
- 🎯 Interactive component selection
|
|
134
|
+
- 🔍 Automatic React dependency detection
|
|
135
|
+
- 🛡️ Safe overwrite handling (checks for empty directories)
|
|
136
|
+
- 📁 Automatic directory creation
|
|
137
|
+
- ⚠️ Helpful warnings and error messages
|
|
138
|
+
- 🚀 Zero configuration required
|
|
139
|
+
|
|
140
|
+
## How It Works
|
|
141
|
+
|
|
142
|
+
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
|
+
|
|
144
|
+
2. **Component Selection**: An interactive menu displays all available components with descriptions.
|
|
145
|
+
|
|
146
|
+
3. **Directory Check**: Before copying, the CLI checks if the destination directory exists and whether it's empty.
|
|
147
|
+
|
|
148
|
+
4. **Copy Operation**: All files from the component template are copied to `src/components/{component-name}/`, creating directories as needed.
|
|
149
|
+
|
|
150
|
+
5. **Completion**: A success message confirms the component has been added to your project.
|
|
151
|
+
|
|
152
|
+
## Common Use Cases
|
|
153
|
+
|
|
154
|
+
### Adding a Component to a New Project
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
mkdir my-app && cd my-app
|
|
158
|
+
npm init -y
|
|
159
|
+
npm install react react-dom
|
|
160
|
+
npx @androbinco/library-cli
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Adding Multiple Components
|
|
164
|
+
|
|
165
|
+
Run once and select multiple components in the multiselect:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
npx @androbinco/library-cli # Pick multiple components in one go
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Working with Existing Components
|
|
172
|
+
|
|
173
|
+
If you've modified a component and want to update it:
|
|
174
|
+
|
|
175
|
+
1. Run the CLI
|
|
176
|
+
2. Select the component
|
|
177
|
+
3. When prompted about overwriting, choose `Yes`
|
|
178
|
+
|
|
179
|
+
**Note**: This will overwrite your custom changes. Consider backing up your modifications first.
|
|
180
|
+
|
|
181
|
+
## Troubleshooting
|
|
182
|
+
|
|
183
|
+
### "React dependency not found" Warning
|
|
184
|
+
|
|
185
|
+
This warning appears if React is not listed in your `package.json`. The CLI will still copy the component, but you'll need to install React for it to work:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
npm install react react-dom
|
|
189
|
+
# or
|
|
190
|
+
pnpm add react react-dom
|
|
191
|
+
# or
|
|
192
|
+
yarn add react react-dom
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### "No package.json found" Warning
|
|
196
|
+
|
|
197
|
+
If you're not in a project directory with a `package.json`, the CLI will warn you but still allow component installation. Make sure you're in the correct directory.
|
|
198
|
+
|
|
199
|
+
### Permission Errors
|
|
200
|
+
|
|
201
|
+
If you encounter permission errors, ensure:
|
|
202
|
+
- You have write permissions in the current directory
|
|
203
|
+
- The `src/components/` directory (or its parent) is not read-only
|
|
204
|
+
|
|
205
|
+
### Component Not Appearing
|
|
206
|
+
|
|
207
|
+
If a component doesn't appear in the selection menu:
|
|
208
|
+
- Make sure you're using the latest version of the CLI
|
|
209
|
+
- Check that the component template exists in the CLI's templates directory
|
|
210
|
+
|
|
211
|
+
## Development
|
|
212
|
+
|
|
213
|
+
For contributing to this CLI tool, see the main Robin CLI Tools monorepo.
|
|
214
|
+
|
|
215
|
+
### Local Development
|
|
216
|
+
|
|
217
|
+
1. Clone the monorepo
|
|
218
|
+
2. Install dependencies:
|
|
219
|
+
```bash
|
|
220
|
+
pnpm install
|
|
221
|
+
```
|
|
222
|
+
3. Link the package:
|
|
223
|
+
```bash
|
|
224
|
+
pnpm --filter @androbinco/library-cli dev
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Technical Details
|
|
228
|
+
|
|
229
|
+
### Template Resolution
|
|
230
|
+
|
|
231
|
+
The CLI uses ESM-compatible path resolution (`import.meta.url`) to locate templates, ensuring it works correctly when distributed via NPM regardless of where it's installed.
|
|
232
|
+
|
|
233
|
+
### File Operations
|
|
234
|
+
|
|
235
|
+
- Uses `fs-extra` for reliable file operations
|
|
236
|
+
- Automatically creates nested directories as needed
|
|
237
|
+
- Handles file overwrites safely
|
|
238
|
+
|
|
239
|
+
### Error Handling
|
|
240
|
+
|
|
241
|
+
- Graceful error handling with user-friendly messages
|
|
242
|
+
- Non-blocking warnings (dependency checks don't prevent installation)
|
|
243
|
+
- Clear cancellation paths
|
|
244
|
+
|
|
245
|
+
## Updating
|
|
246
|
+
|
|
247
|
+
To update to the latest version:
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
npm update -g @androbinco/library-cli
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Or use npx/pnpx to always get the latest version without global installation.
|
|
254
|
+
|
|
255
|
+
## License
|
|
256
|
+
|
|
257
|
+
MIT
|
|
258
|
+
|
|
259
|
+
## Support
|
|
260
|
+
|
|
261
|
+
For issues, questions, or contributions, please refer to the main Robin CLI Tools monorepo.
|
|
262
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@androbinco/library-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Interactive CLI tool to add Robin library components to your project",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"robin-library-cli": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src/",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"cli",
|
|
16
|
+
"robin",
|
|
17
|
+
"library",
|
|
18
|
+
"components",
|
|
19
|
+
"react"
|
|
20
|
+
],
|
|
21
|
+
"author": "androbinco",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@clack/prompts": "^0.7.0",
|
|
25
|
+
"fs-extra": "^11.1.1"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"nodemon": "^3.0.0"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"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
|
+
}
|
|
40
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import * as p from '@clack/prompts';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { checkReactDependency } from './utils/dependencies.js';
|
|
7
|
+
import { handleComponentsFlow } from './utils/components.js';
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
const TEMPLATES_DIR = path.join(__dirname, 'templates');
|
|
12
|
+
|
|
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
|
+
];
|
|
26
|
+
|
|
27
|
+
async function mainMenu() {
|
|
28
|
+
const action = await p.select({
|
|
29
|
+
message: 'What would you like to do?',
|
|
30
|
+
options: [
|
|
31
|
+
{ value: 'components', label: 'Components', hint: 'Browse and copy components' },
|
|
32
|
+
{ value: 'exit', label: 'Exit' }
|
|
33
|
+
]
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return action;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function main() {
|
|
40
|
+
// Beautiful intro
|
|
41
|
+
p.intro('✨ Welcome to Robin Library CLI');
|
|
42
|
+
|
|
43
|
+
// Check React dependency
|
|
44
|
+
await checkReactDependency();
|
|
45
|
+
|
|
46
|
+
let continueLoop = true;
|
|
47
|
+
while (continueLoop) {
|
|
48
|
+
const action = await mainMenu();
|
|
49
|
+
|
|
50
|
+
if (p.isCancel(action) || action === 'exit') {
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (action === 'components') {
|
|
55
|
+
await handleComponentsFlow(COMPONENTS, TEMPLATES_DIR);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const doMore = await p.select({
|
|
59
|
+
message: 'Would you like to do something else?',
|
|
60
|
+
options: [
|
|
61
|
+
{ value: 'continue', label: 'Yes, go back to the menu' },
|
|
62
|
+
{ value: 'exit', label: 'No, exit' }
|
|
63
|
+
]
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (p.isCancel(doMore) || doMore === 'exit') {
|
|
67
|
+
continueLoop = false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Beautiful outro
|
|
72
|
+
p.outro('🎉 Done! Happy coding!');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
main().catch((error) => {
|
|
76
|
+
p.log.error(`Unexpected error: ${error.message}`);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
});
|
|
79
|
+
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { DetailedHTMLProps, HTMLAttributes, useEffect } from "react";
|
|
3
|
+
|
|
4
|
+
import "./styles/embla.css";
|
|
5
|
+
import { EmblaOptionsType } from "embla-carousel";
|
|
6
|
+
import useEmblaCarousel from "embla-carousel-react";
|
|
7
|
+
|
|
8
|
+
import { cn } from "@/common/utils/classname-builder";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
NavigationButtons,
|
|
12
|
+
NextButton,
|
|
13
|
+
PrevButton,
|
|
14
|
+
} from "./components/navigation-buttons";
|
|
15
|
+
import { Pagination } from "./components/pagination";
|
|
16
|
+
import {
|
|
17
|
+
CarouselProvider,
|
|
18
|
+
useCarouselContext,
|
|
19
|
+
} from "./components/provider/carousel.provider";
|
|
20
|
+
|
|
21
|
+
/*
|
|
22
|
+
This is a carousel component that is used to display a list of items in a carousel.
|
|
23
|
+
It is a wrapper around the embla-carousel library and uses the useEmblaCarousel hook to create the carousel.
|
|
24
|
+
It is a client component that is used to display a list of items in a carousel.
|
|
25
|
+
|
|
26
|
+
You have some examples in the examples folder. To test them you can just use them like this in any page:
|
|
27
|
+
|
|
28
|
+
const slides = Array.from({ length: 5 }, (_, i) => i);
|
|
29
|
+
<>
|
|
30
|
+
<LoopCenterCarousel slides={slides} />
|
|
31
|
+
<CarouselWithNumberPagination slides={slides} />
|
|
32
|
+
</>
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const BodyCarousel = ({
|
|
36
|
+
children,
|
|
37
|
+
className,
|
|
38
|
+
containerClassName,
|
|
39
|
+
...props
|
|
40
|
+
}: {
|
|
41
|
+
children: React.ReactNode;
|
|
42
|
+
className?: string;
|
|
43
|
+
containerClassName?: string;
|
|
44
|
+
} & DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>) => {
|
|
45
|
+
const { options, setEmblaApi } = useCarouselContext();
|
|
46
|
+
const [emblaRef, emblaApi] = useEmblaCarousel({
|
|
47
|
+
direction: "ltr",
|
|
48
|
+
...options,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
setEmblaApi(emblaApi);
|
|
53
|
+
}, [emblaApi, setEmblaApi]);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div
|
|
57
|
+
ref={emblaRef}
|
|
58
|
+
className={cn("embla w-full overflow-hidden", containerClassName)}
|
|
59
|
+
{...props}
|
|
60
|
+
>
|
|
61
|
+
<div className={cn("flex w-full gap-2 px-2", className)}>{children}</div>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const RootCarousel = ({
|
|
67
|
+
children,
|
|
68
|
+
options,
|
|
69
|
+
className,
|
|
70
|
+
...props
|
|
71
|
+
}: {
|
|
72
|
+
children: React.ReactNode;
|
|
73
|
+
options: EmblaOptionsType;
|
|
74
|
+
className?: string;
|
|
75
|
+
} & React.HTMLAttributes<HTMLDivElement>) => {
|
|
76
|
+
return (
|
|
77
|
+
<CarouselProvider options={options}>
|
|
78
|
+
<div className={cn("relative", className)} {...props}>
|
|
79
|
+
{children}
|
|
80
|
+
</div>
|
|
81
|
+
</CarouselProvider>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const SlideCarousel = ({
|
|
86
|
+
className,
|
|
87
|
+
children,
|
|
88
|
+
...props
|
|
89
|
+
}: {
|
|
90
|
+
className?: string;
|
|
91
|
+
children: React.ReactNode;
|
|
92
|
+
} & React.HTMLAttributes<HTMLDivElement>) => {
|
|
93
|
+
return (
|
|
94
|
+
<div className={cn("embla-slot min-h-max", className)} {...props}>
|
|
95
|
+
{children}
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const Carousel = {
|
|
101
|
+
Root: RootCarousel,
|
|
102
|
+
Slides: BodyCarousel,
|
|
103
|
+
Slide: SlideCarousel,
|
|
104
|
+
Pagination: Pagination,
|
|
105
|
+
Navigation: NavigationButtons,
|
|
106
|
+
NextButton: NextButton,
|
|
107
|
+
PrevButton: PrevButton,
|
|
108
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import React, { ComponentPropsWithRef } from "react";
|
|
2
|
+
|
|
3
|
+
import { cva, VariantProps } from "class-variance-authority";
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/common/utils/classname-builder";
|
|
6
|
+
|
|
7
|
+
import { useCarouselContext } from "./provider/carousel.provider";
|
|
8
|
+
|
|
9
|
+
const buttonVariants = cva(
|
|
10
|
+
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
|
11
|
+
{
|
|
12
|
+
variants: {
|
|
13
|
+
variant: {
|
|
14
|
+
primary: "bg-blue-600 text-white hover:bg-blue-700",
|
|
15
|
+
secondary: "bg-gray-200 text-gray-900 hover:bg-gray-300",
|
|
16
|
+
outline: "border border-gray-300 bg-transparent hover:bg-gray-100",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
defaultVariants: {
|
|
20
|
+
variant: "primary",
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
export const PrevButton: React.FC<
|
|
26
|
+
{
|
|
27
|
+
variant?: VariantProps<typeof buttonVariants>["variant"];
|
|
28
|
+
} & ComponentPropsWithRef<"button">
|
|
29
|
+
> = ({ variant = "primary", children, ...props }) => {
|
|
30
|
+
const { prevBtnDisabled, onPrevButtonClick } = useCarouselContext();
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<button
|
|
34
|
+
className={cn(buttonVariants({ variant }))}
|
|
35
|
+
disabled={prevBtnDisabled}
|
|
36
|
+
onClick={onPrevButtonClick}
|
|
37
|
+
{...props}
|
|
38
|
+
>
|
|
39
|
+
<svg className="size-4" viewBox="0 0 532 532">
|
|
40
|
+
<path
|
|
41
|
+
d="M355.66 11.354c13.793-13.805 36.208-13.805 50.001 0 13.785 13.804 13.785 36.238 0 50.034L201.22 266l204.442 204.61c13.785 13.805 13.785 36.239 0 50.044-13.793 13.796-36.208 13.796-50.002 0a5994246.277 5994246.277 0 0 0-229.332-229.454 35.065 35.065 0 0 1-10.326-25.126c0-9.2 3.393-18.26 10.326-25.2C172.192 194.973 332.731 34.31 355.66 11.354Z"
|
|
42
|
+
fill="currentColor"
|
|
43
|
+
/>
|
|
44
|
+
</svg>
|
|
45
|
+
{children}
|
|
46
|
+
</button>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const NextButton: React.FC<
|
|
51
|
+
{
|
|
52
|
+
variant?: VariantProps<typeof buttonVariants>["variant"];
|
|
53
|
+
} & ComponentPropsWithRef<"button">
|
|
54
|
+
> = ({ variant = "primary", children, ...props }) => {
|
|
55
|
+
const { nextBtnDisabled, onNextButtonClick } = useCarouselContext();
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<button
|
|
59
|
+
className={cn(buttonVariants({ variant }))}
|
|
60
|
+
disabled={nextBtnDisabled}
|
|
61
|
+
onClick={onNextButtonClick}
|
|
62
|
+
{...props}
|
|
63
|
+
>
|
|
64
|
+
<svg className="size-4" viewBox="0 0 532 532">
|
|
65
|
+
<path
|
|
66
|
+
d="M176.34 520.646c-13.793 13.805-36.208 13.805-50.001 0-13.785-13.804-13.785-36.238 0-50.034L330.78 266 126.34 61.391c-13.785-13.805-13.785-36.239 0-50.044 13.793-13.796 36.208-13.796 50.002 0 22.928 22.947 206.395 206.507 229.332 229.454a35.065 35.065 0 0 1 10.326 25.126c0 9.2-3.393 18.26-10.326 25.2-45.865 45.901-206.404 206.564-229.332 229.52Z"
|
|
67
|
+
fill="currentColor"
|
|
68
|
+
/>
|
|
69
|
+
</svg>
|
|
70
|
+
{children}
|
|
71
|
+
</button>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const containerVariants = cva("", {
|
|
76
|
+
variants: {
|
|
77
|
+
variant: {
|
|
78
|
+
"absolute-center":
|
|
79
|
+
"absolute top-1/2 flex w-full -translate-y-1/2 justify-between",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
export const NavigationButtons = ({
|
|
85
|
+
className,
|
|
86
|
+
variant = "absolute-center",
|
|
87
|
+
buttonVariant = "primary",
|
|
88
|
+
...props
|
|
89
|
+
}: {
|
|
90
|
+
className?: string;
|
|
91
|
+
variant?: VariantProps<typeof containerVariants>["variant"];
|
|
92
|
+
buttonVariant?: VariantProps<typeof buttonVariants>["variant"];
|
|
93
|
+
} & React.HTMLAttributes<HTMLDivElement>) => {
|
|
94
|
+
return (
|
|
95
|
+
<div className={cn(containerVariants({ variant }), className)} {...props}>
|
|
96
|
+
<PrevButton variant={buttonVariant} />
|
|
97
|
+
<NextButton variant={buttonVariant} />
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { cva, VariantProps } from 'class-variance-authority';
|
|
4
|
+
|
|
5
|
+
import { cn } from '@/common/utils/classname-builder';
|
|
6
|
+
|
|
7
|
+
import { useCarouselContext } from './provider/carousel.provider';
|
|
8
|
+
|
|
9
|
+
export const Pagination: React.FC<
|
|
10
|
+
React.HTMLAttributes<HTMLDivElement> & { type?: 'dot' | 'number' | 'progress' }
|
|
11
|
+
> = ({ className, type = 'dot', ...props }) => {
|
|
12
|
+
const { scrollSnaps, selectedIndex, onDotButtonClick } = useCarouselContext();
|
|
13
|
+
|
|
14
|
+
if (type === 'number')
|
|
15
|
+
return (
|
|
16
|
+
<div className={cn('mx-auto flex w-max gap-2', className)} {...props}>
|
|
17
|
+
<p className="text-body-2 text-grey-50-15">
|
|
18
|
+
{selectedIndex + 1} / {scrollSnaps.length}
|
|
19
|
+
</p>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
if (type === 'progress')
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
className={cn(
|
|
26
|
+
'relative mx-auto flex h-2 w-200 max-w-full gap-2 overflow-hidden rounded-full bg-bg-primary',
|
|
27
|
+
className,
|
|
28
|
+
)}
|
|
29
|
+
{...props}
|
|
30
|
+
>
|
|
31
|
+
<div
|
|
32
|
+
className="pointer-events-none absolute bottom-0 left-0 h-2 w-full bg-fill-brand-primary transition-all duration-300 ease-out"
|
|
33
|
+
style={{
|
|
34
|
+
transformOrigin: 'left',
|
|
35
|
+
transform: `scaleX(${(selectedIndex + 1) / scrollSnaps.length})`,
|
|
36
|
+
}}
|
|
37
|
+
/>
|
|
38
|
+
{scrollSnaps.map((_, index) => (
|
|
39
|
+
<div
|
|
40
|
+
key={index}
|
|
41
|
+
className="h-2 w-full cursor-pointer"
|
|
42
|
+
onClick={() => onDotButtonClick(index)}
|
|
43
|
+
/>
|
|
44
|
+
))}
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
if (type === 'dot')
|
|
49
|
+
return (
|
|
50
|
+
<div className={cn('mx-auto flex w-max gap-2', className)} {...props}>
|
|
51
|
+
{scrollSnaps.map((_, index) => (
|
|
52
|
+
<DotButton key={index} index={index} variant="primary" />
|
|
53
|
+
))}
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
type DotButtonVariants = VariantProps<typeof dotButtonVariants>['variant'];
|
|
59
|
+
const dotButtonVariants = cva(
|
|
60
|
+
'size-3 cursor-pointer rounded-full transition-all duration-300 ease-out focus:outline-2',
|
|
61
|
+
{
|
|
62
|
+
variants: {
|
|
63
|
+
variant: {
|
|
64
|
+
primary: 'bg-fill-brand-primary',
|
|
65
|
+
},
|
|
66
|
+
selected: {
|
|
67
|
+
true: 'w-9 !bg-amber-950',
|
|
68
|
+
false: '',
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
export const DotButton: React.FC<{ index: number; variant: DotButtonVariants }> = ({
|
|
75
|
+
index,
|
|
76
|
+
variant = 'primary',
|
|
77
|
+
...restProps
|
|
78
|
+
}) => {
|
|
79
|
+
const { selectedIndex, onDotButtonClick } = useCarouselContext();
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<button
|
|
83
|
+
className={dotButtonVariants({ variant, selected: selectedIndex === index })}
|
|
84
|
+
disabled={selectedIndex === index}
|
|
85
|
+
onClick={() => onDotButtonClick(index)}
|
|
86
|
+
{...restProps}
|
|
87
|
+
/>
|
|
88
|
+
);
|
|
89
|
+
};
|