@rajdeep0510/scaffold-cli 1.2.0 → 1.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.
- package/.vscode/settings.json +10 -0
- package/bin/index.js +4 -4
- package/package.json +2 -2
- package/templates/Card/Card.jsx +187 -0
- package/templates/Card/index.js +2 -0
- package/templates/Card/meta.json +100 -0
- package/templates/Navbar/Navbar.jsx +207 -0
- package/templates/Navbar/index.js +2 -0
- package/templates/Navbar/meta.json +43 -0
- package/templates/RadioGroup/RadioGroup.jsx +148 -0
- package/templates/RadioGroup/index.js +4 -0
- package/templates/RadioGroup/meta.json +10 -0
package/bin/index.js
CHANGED
|
@@ -27,15 +27,15 @@ program.addHelpText('before', () => {
|
|
|
27
27
|
);
|
|
28
28
|
|
|
29
29
|
const importInfo = chalk.blue("How to import components:") + "\n" +
|
|
30
|
-
|
|
30
|
+
chalk.green(" import { Button } from '@/components/ui/button'");
|
|
31
31
|
|
|
32
32
|
return banner + "\n\n" + description + "\n\n" + importInfo + "\n\n";
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
program
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
.command('add <component-name>')
|
|
37
|
+
.description('Add a new component')
|
|
38
|
+
.action(addComponent)
|
|
39
39
|
|
|
40
40
|
program
|
|
41
41
|
.command('list')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rajdeep0510/scaffold-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "This is a CLI app for a UI library which is used for creating versatile and modern UI components.",
|
|
5
5
|
"main": "bin/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -39,4 +39,4 @@
|
|
|
39
39
|
"ora": "^8.2.0",
|
|
40
40
|
"prop-types": "^15.8.1"
|
|
41
41
|
}
|
|
42
|
-
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
const VARIANTS = {
|
|
4
|
+
elevated: "shadow-xl border-transparent",
|
|
5
|
+
outlined: "border border-opacity-20 shadow-sm",
|
|
6
|
+
flat: "shadow-none border-transparent",
|
|
7
|
+
glass: "backdrop-blur-xl bg-opacity-70 border border-white/10 shadow-lg",
|
|
8
|
+
gradient: "border-none shadow-2xl bg-gradient-to-br",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const THEMES = {
|
|
12
|
+
light: {
|
|
13
|
+
base: "bg-white text-slate-800",
|
|
14
|
+
glass: "bg-white/70 text-slate-900 border-slate-200/50",
|
|
15
|
+
border: "border-slate-200",
|
|
16
|
+
subtext: "text-slate-500",
|
|
17
|
+
divider: "border-slate-100",
|
|
18
|
+
},
|
|
19
|
+
dark: {
|
|
20
|
+
base: "bg-slate-900 text-slate-100",
|
|
21
|
+
glass: "bg-slate-900/60 text-white border-white/10",
|
|
22
|
+
border: "border-slate-800",
|
|
23
|
+
subtext: "text-slate-400",
|
|
24
|
+
divider: "border-slate-800",
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const PADDINGS = {
|
|
29
|
+
none: "p-0",
|
|
30
|
+
sm: "p-3",
|
|
31
|
+
md: "p-6",
|
|
32
|
+
lg: "p-8",
|
|
33
|
+
xl: "p-10",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const ROUNDED = {
|
|
37
|
+
none: "rounded-none",
|
|
38
|
+
sm: "rounded-sm",
|
|
39
|
+
md: "rounded-md",
|
|
40
|
+
lg: "rounded-lg",
|
|
41
|
+
xl: "rounded-xl",
|
|
42
|
+
"2xl": "rounded-2xl",
|
|
43
|
+
"3xl": "rounded-3xl",
|
|
44
|
+
full: "rounded-full",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const HOVER_EFFECTS = {
|
|
48
|
+
none: "",
|
|
49
|
+
lift: "hover:-translate-y-1 hover:shadow-2xl",
|
|
50
|
+
scale: "hover:scale-[1.02]",
|
|
51
|
+
glow: "hover:shadow-indigo-500/20 hover:border-indigo-500/30",
|
|
52
|
+
shimmer: "hover:bg-opacity-90",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {Object} props
|
|
57
|
+
* @param {React.ReactNode} [props.children]
|
|
58
|
+
* @param {string} [props.title]
|
|
59
|
+
* @param {string} [props.subtitle]
|
|
60
|
+
* @param {string} [props.image]
|
|
61
|
+
* @param {boolean} [props.imageFull]
|
|
62
|
+
* @param {React.ReactNode} [props.footer]
|
|
63
|
+
* @param {React.ReactNode} [props.action]
|
|
64
|
+
* @param {string} [props.badge]
|
|
65
|
+
* @param {string} [props.theme]
|
|
66
|
+
* @param {string} [props.variant]
|
|
67
|
+
* @param {string} [props.padding]
|
|
68
|
+
* @param {string} [props.rounded]
|
|
69
|
+
* @param {string} [props.hoverEffect]
|
|
70
|
+
* @param {string} [props.className]
|
|
71
|
+
* @param {string} [props.width]
|
|
72
|
+
* @param {function} [props.onClick]
|
|
73
|
+
*/
|
|
74
|
+
export default function Card({
|
|
75
|
+
children = undefined,
|
|
76
|
+
title = undefined,
|
|
77
|
+
subtitle = undefined,
|
|
78
|
+
image = undefined,
|
|
79
|
+
imageFull = false, // If true, image covers the background or top area completely without padding
|
|
80
|
+
footer = undefined,
|
|
81
|
+
action = undefined,
|
|
82
|
+
badge = undefined,
|
|
83
|
+
theme = "dark",
|
|
84
|
+
variant = "elevated",
|
|
85
|
+
padding = "md",
|
|
86
|
+
rounded = "2xl",
|
|
87
|
+
hoverEffect = "lift",
|
|
88
|
+
className = "",
|
|
89
|
+
width = "w-full max-w-sm", // Default width constraint
|
|
90
|
+
onClick = undefined,
|
|
91
|
+
...props
|
|
92
|
+
}) {
|
|
93
|
+
const isGlass = variant === "glass";
|
|
94
|
+
const currentTheme = THEMES[theme] || THEMES.dark;
|
|
95
|
+
const baseStyle = isGlass ? currentTheme.glass : currentTheme.base;
|
|
96
|
+
const borderColor = currentTheme.border;
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div
|
|
100
|
+
onClick={onClick}
|
|
101
|
+
className={`
|
|
102
|
+
relative flex flex-col overflow-hidden transition-all duration-300 ease-out group
|
|
103
|
+
${width}
|
|
104
|
+
${ROUNDED[rounded] || ROUNDED["2xl"]}
|
|
105
|
+
${VARIANTS[variant] || VARIANTS.elevated}
|
|
106
|
+
${baseStyle}
|
|
107
|
+
${!isGlass && variant === "outlined" ? borderColor : ""}
|
|
108
|
+
${HOVER_EFFECTS[hoverEffect] || ""}
|
|
109
|
+
${onClick ? "cursor-pointer" : ""}
|
|
110
|
+
${className}
|
|
111
|
+
`}
|
|
112
|
+
{...props}
|
|
113
|
+
>
|
|
114
|
+
{/* Background Image (Optional absolute overlay) */}
|
|
115
|
+
{image && imageFull && (
|
|
116
|
+
<div className="absolute inset-0 z-0">
|
|
117
|
+
<img
|
|
118
|
+
src={image}
|
|
119
|
+
alt={title || "Card background"}
|
|
120
|
+
className="h-full w-full object-cover opacity-20 group-hover:scale-105 transition-transform duration-700"
|
|
121
|
+
/>
|
|
122
|
+
<div
|
|
123
|
+
className={`absolute inset-0 bg-gradient-to-t ${
|
|
124
|
+
theme === "dark" ? "from-slate-900" : "from-white"
|
|
125
|
+
} via-transparent to-transparent`}
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
|
|
130
|
+
{/* Header Image (Standard top image) */}
|
|
131
|
+
{image && !imageFull && (
|
|
132
|
+
<div className="relative h-48 w-full overflow-hidden z-10">
|
|
133
|
+
<img
|
|
134
|
+
src={image}
|
|
135
|
+
alt={title}
|
|
136
|
+
className="h-full w-full object-cover transition-transform duration-500 group-hover:scale-110"
|
|
137
|
+
/>
|
|
138
|
+
{badge && (
|
|
139
|
+
<div className="absolute top-4 left-4">
|
|
140
|
+
<span className="px-3 py-1 text-xs font-bold uppercase tracking-wider text-white bg-black/50 backdrop-blur-md rounded-full">
|
|
141
|
+
{badge}
|
|
142
|
+
</span>
|
|
143
|
+
</div>
|
|
144
|
+
)}
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
|
|
148
|
+
{/* Content Container */}
|
|
149
|
+
<div className={`relative z-10 flex flex-col h-full ${PADDINGS[padding]}`}>
|
|
150
|
+
{/* Header Section */}
|
|
151
|
+
{(title || subtitle || action) && (
|
|
152
|
+
<div className="flex justify-between items-start mb-4">
|
|
153
|
+
<div className="flex-1">
|
|
154
|
+
{title && (
|
|
155
|
+
<h3 className="text-xl font-bold tracking-tight mb-1">
|
|
156
|
+
{title}
|
|
157
|
+
</h3>
|
|
158
|
+
)}
|
|
159
|
+
{subtitle && (
|
|
160
|
+
<p className={`text-sm font-medium ${currentTheme.subtext}`}>
|
|
161
|
+
{subtitle}
|
|
162
|
+
</p>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
{action && <div className="ml-4">{action}</div>}
|
|
166
|
+
</div>
|
|
167
|
+
)}
|
|
168
|
+
|
|
169
|
+
{/* Main Body */}
|
|
170
|
+
<div className={`flex-1 ${currentTheme.subtext} leading-relaxed`}>
|
|
171
|
+
{children}
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
{/* Footer Section */}
|
|
175
|
+
{footer && (
|
|
176
|
+
<div
|
|
177
|
+
className={`mt-6 pt-4 border-t ${currentTheme.divider} flex items-center justify-between`}
|
|
178
|
+
>
|
|
179
|
+
{footer}
|
|
180
|
+
</div>
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
Card.displayName = "Card";
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Card",
|
|
3
|
+
"description": "A versatile card component with support for multiple themes, variants, and interactive states.",
|
|
4
|
+
"props": {
|
|
5
|
+
"title": {
|
|
6
|
+
"type": "string",
|
|
7
|
+
"description": "The main heading of the card."
|
|
8
|
+
},
|
|
9
|
+
"subtitle": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "Secondary text usually placed below the title."
|
|
12
|
+
},
|
|
13
|
+
"image": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"description": "URL of the image to display."
|
|
16
|
+
},
|
|
17
|
+
"imageFull": {
|
|
18
|
+
"type": "boolean",
|
|
19
|
+
"default": false,
|
|
20
|
+
"description": "If true, the image is used as a background with an overlay."
|
|
21
|
+
},
|
|
22
|
+
"badge": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"description": "Top-left badge text overlaying the image."
|
|
25
|
+
},
|
|
26
|
+
"action": {
|
|
27
|
+
"type": "node",
|
|
28
|
+
"description": "Element to display in the top-right corner (e.g., button, icon)."
|
|
29
|
+
},
|
|
30
|
+
"footer": {
|
|
31
|
+
"type": "node",
|
|
32
|
+
"description": "Content to display at the bottom of the card with a divider."
|
|
33
|
+
},
|
|
34
|
+
"theme": {
|
|
35
|
+
"type": "enum",
|
|
36
|
+
"options": [
|
|
37
|
+
"light",
|
|
38
|
+
"dark"
|
|
39
|
+
],
|
|
40
|
+
"default": "dark",
|
|
41
|
+
"description": "Color scheme of the card."
|
|
42
|
+
},
|
|
43
|
+
"variant": {
|
|
44
|
+
"type": "enum",
|
|
45
|
+
"options": [
|
|
46
|
+
"elevated",
|
|
47
|
+
"outlined",
|
|
48
|
+
"flat",
|
|
49
|
+
"glass",
|
|
50
|
+
"gradient"
|
|
51
|
+
],
|
|
52
|
+
"default": "elevated",
|
|
53
|
+
"description": "Visual style of the card container."
|
|
54
|
+
},
|
|
55
|
+
"padding": {
|
|
56
|
+
"type": "enum",
|
|
57
|
+
"options": [
|
|
58
|
+
"none",
|
|
59
|
+
"sm",
|
|
60
|
+
"md",
|
|
61
|
+
"lg",
|
|
62
|
+
"xl"
|
|
63
|
+
],
|
|
64
|
+
"default": "md",
|
|
65
|
+
"description": "Internal padding of the content area."
|
|
66
|
+
},
|
|
67
|
+
"rounded": {
|
|
68
|
+
"type": "enum",
|
|
69
|
+
"options": [
|
|
70
|
+
"none",
|
|
71
|
+
"sm",
|
|
72
|
+
"md",
|
|
73
|
+
"lg",
|
|
74
|
+
"xl",
|
|
75
|
+
"2xl",
|
|
76
|
+
"3xl",
|
|
77
|
+
"full"
|
|
78
|
+
],
|
|
79
|
+
"default": "2xl",
|
|
80
|
+
"description": "Border radius of the card."
|
|
81
|
+
},
|
|
82
|
+
"hoverEffect": {
|
|
83
|
+
"type": "enum",
|
|
84
|
+
"options": [
|
|
85
|
+
"none",
|
|
86
|
+
"lift",
|
|
87
|
+
"scale",
|
|
88
|
+
"glow",
|
|
89
|
+
"shimmer"
|
|
90
|
+
],
|
|
91
|
+
"default": "lift",
|
|
92
|
+
"description": "Animation effect on hover."
|
|
93
|
+
},
|
|
94
|
+
"width": {
|
|
95
|
+
"type": "string",
|
|
96
|
+
"default": "w-full max-w-sm",
|
|
97
|
+
"description": "Width classes to control the card's dimensions."
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} LinkItem
|
|
5
|
+
* @property {string} label - The text to display.
|
|
6
|
+
* @property {string} href - The URL link.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {Object} props
|
|
11
|
+
* @param {React.ReactNode | string} [props.logo] - Brand logo or text.
|
|
12
|
+
* @param {LinkItem[]} [props.links] - Array of navigation links.
|
|
13
|
+
* @param {actionItem[]} [props.actions] - Right-side actions (e.g. Login buttons).
|
|
14
|
+
* @param {string} [props.theme] - "light" or "dark".
|
|
15
|
+
* @param {boolean} [props.sticky] - Whether the navbar is sticky.
|
|
16
|
+
* @param {string} [props.className] - Additional classes.
|
|
17
|
+
* @param {function} [props.onLinkClick] - Callback when a link is clicked.
|
|
18
|
+
*/
|
|
19
|
+
export default function Navbar({
|
|
20
|
+
logo = "Brand",
|
|
21
|
+
links = [
|
|
22
|
+
{ label: "Home", href: "#" },
|
|
23
|
+
{ label: "Features", href: "#" },
|
|
24
|
+
{ label: "Pricing", href: "#" },
|
|
25
|
+
{ label: "About", href: "#" },
|
|
26
|
+
],
|
|
27
|
+
actions = [
|
|
28
|
+
{ label: "Login", href: "#" },
|
|
29
|
+
{ label: "Sign Up", href: "#" },
|
|
30
|
+
],
|
|
31
|
+
theme = "light",
|
|
32
|
+
sticky = true,
|
|
33
|
+
className = "",
|
|
34
|
+
onLinkClick = undefined,
|
|
35
|
+
...props
|
|
36
|
+
}) {
|
|
37
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
38
|
+
const [scrolled, setScrolled] = useState(false);
|
|
39
|
+
|
|
40
|
+
// Handle scroll for glass effect or shadow
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const handleScroll = () => {
|
|
43
|
+
setScrolled(window.scrollY > 20);
|
|
44
|
+
};
|
|
45
|
+
window.addEventListener("scroll", handleScroll);
|
|
46
|
+
return () => window.removeEventListener("scroll", handleScroll);
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
const isDark = theme === "dark";
|
|
50
|
+
|
|
51
|
+
// Theme Styles
|
|
52
|
+
const baseStyles = isDark ? "bg-slate-900 text-white" : "bg-white text-slate-900";
|
|
53
|
+
const scrolledStyles = isDark
|
|
54
|
+
? "bg-slate-900/80 backdrop-blur-md border-b border-white/5"
|
|
55
|
+
: "bg-white/80 backdrop-blur-md border-b border-slate-200/50";
|
|
56
|
+
|
|
57
|
+
const navClasses = `
|
|
58
|
+
${sticky ? "fixed top-0 left-0 right-0 z-50" : "relative"}
|
|
59
|
+
${scrolled ? scrolledStyles : baseStyles}
|
|
60
|
+
${scrolled ? "shadow-sm" : ""}
|
|
61
|
+
transition-all duration-300 ease-in-out
|
|
62
|
+
${className}
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
// Link Styles
|
|
66
|
+
const linkBase = "text-sm font-medium transition-colors duration-200 cursor-pointer";
|
|
67
|
+
const linkColor = isDark
|
|
68
|
+
? "text-slate-300 hover:text-white"
|
|
69
|
+
: "text-slate-600 hover:text-slate-900";
|
|
70
|
+
|
|
71
|
+
// Mobile Menu Styles
|
|
72
|
+
const mobileMenuClasses = `
|
|
73
|
+
absolute top-full left-0 right-0 p-4 border-b
|
|
74
|
+
${isDark ? "bg-slate-900 border-white/5" : "bg-white border-slate-100"}
|
|
75
|
+
${isOpen ? "opacity-100 translate-y-0" : "opacity-0 -translate-y-4 pointer-events-none"}
|
|
76
|
+
transition-all duration-300 ease-out origin-top shadow-xl
|
|
77
|
+
`;
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<nav className={navClasses} {...props}>
|
|
81
|
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
82
|
+
<div className="flex justify-between items-center h-16">
|
|
83
|
+
|
|
84
|
+
{/* Logo Section */}
|
|
85
|
+
<div className="flex-shrink-0 flex items-center gap-2">
|
|
86
|
+
<span className={`text-xl font-bold tracking-tight ${isDark ? "text-white" : "text-slate-900"}`}>
|
|
87
|
+
{logo}
|
|
88
|
+
</span>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{/* Desktop Links */}
|
|
92
|
+
<div className="hidden md:flex items-center space-x-8">
|
|
93
|
+
{links && links.map((link, index) => (
|
|
94
|
+
<a
|
|
95
|
+
key={index}
|
|
96
|
+
href={link.href}
|
|
97
|
+
onClick={(e) => {
|
|
98
|
+
if (onLinkClick) onLinkClick(e, link);
|
|
99
|
+
}}
|
|
100
|
+
className={`${linkBase} ${linkColor}`}
|
|
101
|
+
>
|
|
102
|
+
{link.label}
|
|
103
|
+
</a>
|
|
104
|
+
))}
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
{/* Actions (Desktop) */}
|
|
108
|
+
<div className="hidden md:flex items-center gap-4">
|
|
109
|
+
{Array.isArray(actions) ? (
|
|
110
|
+
actions.map((action, index) => (
|
|
111
|
+
<a
|
|
112
|
+
key={index}
|
|
113
|
+
href={action.href}
|
|
114
|
+
className={`px-4 py-2 text-sm font-medium rounded-lg transition-all ${
|
|
115
|
+
index === actions.length - 1
|
|
116
|
+
? isDark
|
|
117
|
+
? "bg-indigo-600 text-white hover:bg-indigo-500 shadow-lg shadow-indigo-500/20"
|
|
118
|
+
: "bg-slate-900 text-white hover:bg-slate-800 shadow-lg"
|
|
119
|
+
: isDark
|
|
120
|
+
? "text-slate-300 hover:text-white"
|
|
121
|
+
: "text-slate-600 hover:text-slate-900"
|
|
122
|
+
}`}
|
|
123
|
+
>
|
|
124
|
+
{action.label}
|
|
125
|
+
</a>
|
|
126
|
+
))
|
|
127
|
+
) : (
|
|
128
|
+
actions
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
{/* Mobile Toggle Button */}
|
|
133
|
+
<div className="flex md:hidden">
|
|
134
|
+
<button
|
|
135
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
136
|
+
className={`p-2 rounded-md ${
|
|
137
|
+
isDark ? "text-slate-300 hover:bg-slate-800" : "text-slate-600 hover:bg-slate-100"
|
|
138
|
+
} focus:outline-none`}
|
|
139
|
+
>
|
|
140
|
+
<span className="sr-only">Open main menu</span>
|
|
141
|
+
{/* Hamburger / Close Icon */}
|
|
142
|
+
<svg
|
|
143
|
+
className="h-6 w-6"
|
|
144
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
145
|
+
fill="none"
|
|
146
|
+
viewBox="0 0 24 24"
|
|
147
|
+
stroke="currentColor"
|
|
148
|
+
>
|
|
149
|
+
{isOpen ? (
|
|
150
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
151
|
+
) : (
|
|
152
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
|
153
|
+
)}
|
|
154
|
+
</svg>
|
|
155
|
+
</button>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
{/* Mobile Menu */}
|
|
161
|
+
<div className={mobileMenuClasses}>
|
|
162
|
+
<div className="flex flex-col space-y-4">
|
|
163
|
+
{links && links.map((link, index) => (
|
|
164
|
+
<a
|
|
165
|
+
key={index}
|
|
166
|
+
href={link.href}
|
|
167
|
+
onClick={(e) => {
|
|
168
|
+
setIsOpen(false);
|
|
169
|
+
if (onLinkClick) onLinkClick(e, link);
|
|
170
|
+
}}
|
|
171
|
+
className={`block px-3 py-2 rounded-md text-base font-medium ${
|
|
172
|
+
isDark
|
|
173
|
+
? "text-slate-300 hover:text-white hover:bg-slate-800"
|
|
174
|
+
: "text-slate-600 hover:text-slate-900 hover:bg-slate-50"
|
|
175
|
+
}`}
|
|
176
|
+
>
|
|
177
|
+
{link.label}
|
|
178
|
+
</a>
|
|
179
|
+
))}
|
|
180
|
+
<div className="pt-4 border-t border-gray-200/10 flex flex-col gap-3">
|
|
181
|
+
{Array.isArray(actions) ? (
|
|
182
|
+
actions.map((action, index) => (
|
|
183
|
+
<a
|
|
184
|
+
key={index}
|
|
185
|
+
href={action.href}
|
|
186
|
+
className={`w-full px-4 py-2 text-sm font-medium rounded-lg text-center transition-all ${
|
|
187
|
+
index === actions.length - 1
|
|
188
|
+
? isDark
|
|
189
|
+
? "bg-indigo-600 text-white hover:bg-indigo-500 shadow-lg"
|
|
190
|
+
: "bg-slate-900 text-white hover:bg-slate-800 shadow-lg"
|
|
191
|
+
: isDark
|
|
192
|
+
? "text-slate-300 hover:text-white border border-slate-700 hover:bg-slate-800"
|
|
193
|
+
: "text-slate-600 hover:text-slate-900 border border-slate-200 hover:bg-slate-50"
|
|
194
|
+
}`}
|
|
195
|
+
>
|
|
196
|
+
{action.label}
|
|
197
|
+
</a>
|
|
198
|
+
))
|
|
199
|
+
) : (
|
|
200
|
+
actions
|
|
201
|
+
)}
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
</nav>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Navbar",
|
|
3
|
+
"description": "A responsive navigation bar with logo, links, and action buttons. Supports desktop and mobile layouts with a hamburger menu.",
|
|
4
|
+
"props": {
|
|
5
|
+
"logo": {
|
|
6
|
+
"type": "node",
|
|
7
|
+
"default": "Brand",
|
|
8
|
+
"description": "The logo or brand name displayed on the left."
|
|
9
|
+
},
|
|
10
|
+
"links": {
|
|
11
|
+
"type": "array",
|
|
12
|
+
"default": "Home, Features, Pricing, About",
|
|
13
|
+
"description": "List of navigation items: { label, href }."
|
|
14
|
+
},
|
|
15
|
+
"actions": {
|
|
16
|
+
"type": "node",
|
|
17
|
+
"description": "Custom action buttons (e.g., Login/Signup) for the right side."
|
|
18
|
+
},
|
|
19
|
+
"theme": {
|
|
20
|
+
"type": "enum",
|
|
21
|
+
"options": [
|
|
22
|
+
"light",
|
|
23
|
+
"dark"
|
|
24
|
+
],
|
|
25
|
+
"default": "light",
|
|
26
|
+
"description": "Visual theme of the navbar."
|
|
27
|
+
},
|
|
28
|
+
"sticky": {
|
|
29
|
+
"type": "boolean",
|
|
30
|
+
"default": true,
|
|
31
|
+
"description": "If true, the navbar remains fixed at the top of the viewport."
|
|
32
|
+
},
|
|
33
|
+
"className": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"default": "",
|
|
36
|
+
"description": "Additional custom classes."
|
|
37
|
+
},
|
|
38
|
+
"onLinkClick": {
|
|
39
|
+
"type": "function",
|
|
40
|
+
"description": "Callback function triggered when a navigation link is clicked."
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import React, { forwardRef } from "react";
|
|
2
|
+
|
|
3
|
+
const SIZES = {
|
|
4
|
+
sm: { circle: "h-4 w-4", dot: "h-1.5 w-1.5", text: "text-xs" },
|
|
5
|
+
md: { circle: "h-5 w-5", dot: "h-2 w-2", text: "text-sm" },
|
|
6
|
+
lg: { circle: "h-6 w-6", dot: "h-2.5 w-2.5", text: "text-base" },
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const THEMES = {
|
|
10
|
+
light: {
|
|
11
|
+
labelRaw: "text-slate-500 group-hover:text-slate-800",
|
|
12
|
+
labelChecked: "text-slate-900",
|
|
13
|
+
borderRaw: "border-slate-300",
|
|
14
|
+
borderHover: "group-hover:border-slate-400",
|
|
15
|
+
dot: "bg-current",
|
|
16
|
+
},
|
|
17
|
+
dark: {
|
|
18
|
+
labelRaw: "text-slate-400 group-hover:text-slate-200",
|
|
19
|
+
labelChecked: "text-white",
|
|
20
|
+
borderRaw: "border-slate-700",
|
|
21
|
+
borderHover: "group-hover:border-slate-500",
|
|
22
|
+
dot: "bg-white",
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const RadioButton = forwardRef(
|
|
27
|
+
(
|
|
28
|
+
{
|
|
29
|
+
label,
|
|
30
|
+
value,
|
|
31
|
+
selectedValue,
|
|
32
|
+
onChange,
|
|
33
|
+
name,
|
|
34
|
+
disabled,
|
|
35
|
+
size = "md",
|
|
36
|
+
theme = "dark",
|
|
37
|
+
activeColor = "text-blue-500",
|
|
38
|
+
...props
|
|
39
|
+
},
|
|
40
|
+
ref,
|
|
41
|
+
) => {
|
|
42
|
+
const isSelected = value === selectedValue;
|
|
43
|
+
const currentSize = SIZES[size] || SIZES.md;
|
|
44
|
+
const currentTheme = THEMES[theme] || THEMES.dark;
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<label
|
|
48
|
+
className={`group flex items-center gap-3 select-none transition-all ${
|
|
49
|
+
disabled ? "opacity-30 cursor-not-allowed" : "cursor-pointer"
|
|
50
|
+
}`}
|
|
51
|
+
>
|
|
52
|
+
<div className="relative flex items-center justify-center">
|
|
53
|
+
<input
|
|
54
|
+
{...props}
|
|
55
|
+
ref={ref}
|
|
56
|
+
type="radio"
|
|
57
|
+
name={name}
|
|
58
|
+
checked={isSelected}
|
|
59
|
+
disabled={disabled}
|
|
60
|
+
onChange={() => onChange(value)}
|
|
61
|
+
className="peer sr-only"
|
|
62
|
+
/>
|
|
63
|
+
|
|
64
|
+
{/* Outer Circle */}
|
|
65
|
+
<div
|
|
66
|
+
className={`
|
|
67
|
+
rounded-full border-2 transition-all duration-200 bg-transparent
|
|
68
|
+
${currentSize.circle}
|
|
69
|
+
${currentTheme.borderRaw}
|
|
70
|
+
${!disabled && currentTheme.borderHover}
|
|
71
|
+
peer-checked:border-current ${activeColor}
|
|
72
|
+
`}
|
|
73
|
+
/>
|
|
74
|
+
|
|
75
|
+
{/* Inner Dot */}
|
|
76
|
+
<div
|
|
77
|
+
className={`
|
|
78
|
+
absolute rounded-full transition-transform duration-200
|
|
79
|
+
${currentSize.dot}
|
|
80
|
+
${currentTheme.dot}
|
|
81
|
+
${activeColor}
|
|
82
|
+
${isSelected ? "scale-100 opacity-100" : "scale-0 opacity-0"}
|
|
83
|
+
`}
|
|
84
|
+
/>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
{label && (
|
|
88
|
+
<span
|
|
89
|
+
className={`
|
|
90
|
+
font-medium transition-colors
|
|
91
|
+
${currentSize.text}
|
|
92
|
+
${isSelected ? currentTheme.labelChecked : currentTheme.labelRaw}
|
|
93
|
+
`}
|
|
94
|
+
>
|
|
95
|
+
{label}
|
|
96
|
+
</span>
|
|
97
|
+
)}
|
|
98
|
+
</label>
|
|
99
|
+
);
|
|
100
|
+
},
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
RadioButton.displayName = "RadioButton";
|
|
104
|
+
|
|
105
|
+
export default function RadioGroup({
|
|
106
|
+
options,
|
|
107
|
+
value,
|
|
108
|
+
onChange,
|
|
109
|
+
name,
|
|
110
|
+
label,
|
|
111
|
+
orientation = "vertical",
|
|
112
|
+
size = "md",
|
|
113
|
+
theme = "dark",
|
|
114
|
+
activeColor = "text-blue-500",
|
|
115
|
+
className = "",
|
|
116
|
+
}) {
|
|
117
|
+
return (
|
|
118
|
+
<fieldset className={`flex flex-col gap-4 ${className}`}>
|
|
119
|
+
{label && (
|
|
120
|
+
<legend className="text-[10px] font-bold tracking-[0.2em] text-slate-500 uppercase">
|
|
121
|
+
{label}
|
|
122
|
+
</legend>
|
|
123
|
+
)}
|
|
124
|
+
<div
|
|
125
|
+
className={`flex ${
|
|
126
|
+
orientation === "vertical" ? "flex-col gap-3" : "flex-row gap-6"
|
|
127
|
+
}`}
|
|
128
|
+
>
|
|
129
|
+
{options.map((opt) => (
|
|
130
|
+
<RadioButton
|
|
131
|
+
key={opt.value}
|
|
132
|
+
name={name}
|
|
133
|
+
label={opt.label}
|
|
134
|
+
value={opt.value}
|
|
135
|
+
selectedValue={value}
|
|
136
|
+
onChange={onChange}
|
|
137
|
+
disabled={opt.disabled}
|
|
138
|
+
size={size}
|
|
139
|
+
theme={theme}
|
|
140
|
+
activeColor={activeColor}
|
|
141
|
+
/>
|
|
142
|
+
))}
|
|
143
|
+
</div>
|
|
144
|
+
</fieldset>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
RadioGroup.displayName = "RadioGroup";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "RadioGroup",
|
|
3
|
+
"description": "Minimalist dark-mode radio group with full prop-based customizability.",
|
|
4
|
+
"props": {
|
|
5
|
+
"activeColor": "Tailwind text color class (default: text-blue-500)",
|
|
6
|
+
"size": ["sm", "md", "lg"],
|
|
7
|
+
"orientation": ["vertical", "horizontal"],
|
|
8
|
+
"options": "Array<{label, value, disabled}>"
|
|
9
|
+
}
|
|
10
|
+
}
|