blocks-dusted 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/LICENSE +21 -0
- package/README.md +68 -0
- package/bin/index.js +8 -0
- package/package.json +40 -0
- package/src/cli.js +79 -0
- package/src/commands/add.js +327 -0
- package/src/commands/doctor.js +21 -0
- package/src/commands/list.js +18 -0
- package/src/installers/checkRequirements.js +101 -0
- package/src/installers/copyTemplateFiles.js +79 -0
- package/src/installers/installDependencies.js +69 -0
- package/src/installers/patchPayloadBlocksArray.js +122 -0
- package/src/installers/patchRenderBlocks.js +110 -0
- package/src/installers/runGenerators.js +3 -0
- package/src/installers/writeInstalledBlockReadme.js +117 -0
- package/src/registry/listTemplates.js +17 -0
- package/src/registry/loadTemplateManifest.js +20 -0
- package/src/registry/validateTemplateManifest.js +69 -0
- package/src/utils/logger.js +8 -0
- package/src/utils/packageManager.js +42 -0
- package/src/utils/paths.js +7 -0
- package/src/utils/project.js +122 -0
- package/src/utils/strings.js +7 -0
- package/templates/blocks/DD-BookCall/Component.tsx +573 -0
- package/templates/blocks/DD-BookCall/config.ts +239 -0
- package/templates/blocks/DD-BookCall/manifest.json +112 -0
- package/templates/blocks/DD-CardTemplate01/Component.tsx +144 -0
- package/templates/blocks/DD-CardTemplate01/config.ts +106 -0
- package/templates/blocks/DD-CardTemplate01/manifest.json +84 -0
- package/templates/blocks/DD-Carousel-A/Component.tsx +266 -0
- package/templates/blocks/DD-Carousel-A/config.ts +163 -0
- package/templates/blocks/DD-Carousel-A/manifest.json +86 -0
- package/templates/blocks/DD-Carousel-B/Component.tsx +432 -0
- package/templates/blocks/DD-Carousel-B/config.ts +176 -0
- package/templates/blocks/DD-Carousel-B/manifest.json +86 -0
- package/templates/blocks/DD-ComparisonTable/Component.tsx +272 -0
- package/templates/blocks/DD-ComparisonTable/config.ts +105 -0
- package/templates/blocks/DD-ComparisonTable/manifest.json +78 -0
- package/templates/blocks/DD-Contact/Component.tsx +439 -0
- package/templates/blocks/DD-Contact/config.ts +217 -0
- package/templates/blocks/DD-Contact/manifest.json +107 -0
- package/templates/blocks/DD-FeatCat/Component.tsx +361 -0
- package/templates/blocks/DD-FeatCat/config.ts +204 -0
- package/templates/blocks/DD-FeatCat/manifest.json +87 -0
- package/templates/blocks/DD-FeatStrip/Component.tsx +615 -0
- package/templates/blocks/DD-FeatStrip/config.ts +134 -0
- package/templates/blocks/DD-FeatStrip/manifest.json +82 -0
- package/templates/blocks/DD-Hero/Component.tsx +317 -0
- package/templates/blocks/DD-Hero/config.ts +267 -0
- package/templates/blocks/DD-Hero/manifest.json +92 -0
- package/templates/blocks/DD-HorizontalScroll/Component.tsx +995 -0
- package/templates/blocks/DD-HorizontalScroll/config.ts +231 -0
- package/templates/blocks/DD-HorizontalScroll/manifest.json +85 -0
- package/templates/blocks/DD-MasonaryMedia/Component.tsx +571 -0
- package/templates/blocks/DD-MasonaryMedia/config.ts +390 -0
- package/templates/blocks/DD-MasonaryMedia/manifest.json +101 -0
- package/templates/blocks/DD-MasonaryMedia/vendor.d.ts +2 -0
- package/templates/blocks/DD-Pricing/Component.tsx +380 -0
- package/templates/blocks/DD-Pricing/config.ts +215 -0
- package/templates/blocks/DD-Pricing/manifest.json +78 -0
- package/templates/blocks/DD-Process/Component.tsx +155 -0
- package/templates/blocks/DD-Process/config.ts +150 -0
- package/templates/blocks/DD-Process/manifest.json +82 -0
- package/templates/blocks/DD-Section/Component.tsx +327 -0
- package/templates/blocks/DD-Section/config.ts +214 -0
- package/templates/blocks/DD-Section/manifest.json +85 -0
- package/templates/blocks/DD-Services/Component.tsx +188 -0
- package/templates/blocks/DD-Services/config.ts +149 -0
- package/templates/blocks/DD-Services/manifest.json +95 -0
- package/templates/blocks/DD-ShowcaseGrid/Component.tsx +317 -0
- package/templates/blocks/DD-ShowcaseGrid/config.ts +197 -0
- package/templates/blocks/DD-ShowcaseGrid/manifest.json +80 -0
- package/templates/blocks/DD-Slider-A/Component.tsx +252 -0
- package/templates/blocks/DD-Slider-A/config.ts +118 -0
- package/templates/blocks/DD-Slider-A/manifest.json +86 -0
- package/templates/blocks/DD-StackCards/Component.tsx +496 -0
- package/templates/blocks/DD-StackCards/config.ts +399 -0
- package/templates/blocks/DD-StackCards/manifest.json +87 -0
- package/templates/blocks/DD-Team/Component.tsx +265 -0
- package/templates/blocks/DD-Team/config.ts +173 -0
- package/templates/blocks/DD-Team/manifest.json +82 -0
- package/templates/blocks/DD-TechStack/Component.tsx +189 -0
- package/templates/blocks/DD-TechStack/config.ts +153 -0
- package/templates/blocks/DD-TechStack/manifest.json +118 -0
- package/templates/blocks/DD-Testimonials/Component.tsx +147 -0
- package/templates/blocks/DD-Testimonials/config.ts +120 -0
- package/templates/blocks/DD-Testimonials/manifest.json +87 -0
- package/templates/blocks/DD-Work/Component.tsx +328 -0
- package/templates/blocks/DD-Work/config.ts +165 -0
- package/templates/blocks/DD-Work/manifest.json +87 -0
- package/templates/blocks/DD-Work-B/Component.tsx +320 -0
- package/templates/blocks/DD-Work-B/config.ts +165 -0
- package/templates/blocks/DD-Work-B/manifest.json +75 -0
- package/templates/shared/components/ui/anims/fade.tsx +83 -0
- package/templates/shared/components/ui/button.tsx +233 -0
- package/templates/shared/components/ui/input.tsx +41 -0
- package/templates/shared/fields/lucide-icon-picker/Cell.tsx +24 -0
- package/templates/shared/fields/lucide-icon-picker/README.md +160 -0
- package/templates/shared/fields/lucide-icon-picker/field.ts +40 -0
- package/templates/shared/fields/lucide-icon-picker/index.tsx +203 -0
- package/templates/shared/utilities/canUseDOM.ts +1 -0
- package/templates/shared/utilities/getURL.ts +31 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import { motion, useInView } from 'framer-motion'
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
|
|
5
|
+
export function TextFade({
|
|
6
|
+
direction,
|
|
7
|
+
children,
|
|
8
|
+
className = '',
|
|
9
|
+
stagger = 0.1,
|
|
10
|
+
delay = 0.3
|
|
11
|
+
}: {
|
|
12
|
+
direction: 'up' | 'down' | 'left' | 'right'
|
|
13
|
+
children: React.ReactNode
|
|
14
|
+
className?: string
|
|
15
|
+
stagger?: number
|
|
16
|
+
delay?: number
|
|
17
|
+
}) {
|
|
18
|
+
const getVariant = () => {
|
|
19
|
+
const baseTransition = {
|
|
20
|
+
type: 'spring' as const,
|
|
21
|
+
duration: 0.9,
|
|
22
|
+
delay
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
switch (direction) {
|
|
26
|
+
case 'down':
|
|
27
|
+
return {
|
|
28
|
+
show: { opacity: 1, y: 0, transition: baseTransition },
|
|
29
|
+
hidden: { opacity: 0, y: -20 }
|
|
30
|
+
}
|
|
31
|
+
case 'up':
|
|
32
|
+
return {
|
|
33
|
+
show: { opacity: 1, y: 0, transition: baseTransition },
|
|
34
|
+
hidden: { opacity: 0, y: 20 }
|
|
35
|
+
}
|
|
36
|
+
case 'left':
|
|
37
|
+
return {
|
|
38
|
+
show: { opacity: 1, x: 0, transition: baseTransition },
|
|
39
|
+
hidden: { opacity: 0, x: 20 }
|
|
40
|
+
}
|
|
41
|
+
case 'right':
|
|
42
|
+
return {
|
|
43
|
+
show: { opacity: 1, x: 0, transition: baseTransition },
|
|
44
|
+
hidden: { opacity: 0, x: -20 }
|
|
45
|
+
}
|
|
46
|
+
default:
|
|
47
|
+
return {
|
|
48
|
+
show: { opacity: 1, y: 0, transition: baseTransition },
|
|
49
|
+
hidden: { opacity: 0, y: 20 }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const FADE_VARIANT = getVariant()
|
|
55
|
+
const ref = React.useRef(null)
|
|
56
|
+
const isInView = useInView(ref, { once: false })
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<motion.div
|
|
60
|
+
ref={ref}
|
|
61
|
+
initial="hidden"
|
|
62
|
+
animate={isInView ? 'show' : ''}
|
|
63
|
+
viewport={{ once: false, amount: 0.5 }}
|
|
64
|
+
variants={{
|
|
65
|
+
hidden: {},
|
|
66
|
+
show: {
|
|
67
|
+
transition: {
|
|
68
|
+
staggerChildren: stagger
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}}
|
|
72
|
+
className={className}
|
|
73
|
+
>
|
|
74
|
+
{React.Children.map(children, (child) =>
|
|
75
|
+
React.isValidElement(child) ? (
|
|
76
|
+
<motion.div variants={FADE_VARIANT}>{child}</motion.div>
|
|
77
|
+
) : (
|
|
78
|
+
child
|
|
79
|
+
)
|
|
80
|
+
)}
|
|
81
|
+
</motion.div>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "@/utilities/ui";
|
|
5
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
6
|
+
import { type VariantProps, cva } from "class-variance-authority";
|
|
7
|
+
|
|
8
|
+
// Ripple effect CSS (injected at runtime)
|
|
9
|
+
const rippleCSS = `
|
|
10
|
+
.ripple-container { position: relative; overflow: hidden; }
|
|
11
|
+
.ripple-effect { position: absolute; border-radius: 50%; transform: scale(0); animation: ripple-animation 600ms linear; pointer-events: none; z-index: 10; }
|
|
12
|
+
@keyframes ripple-animation { to { transform: scale(4); opacity: 0; } }
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
// Tailwind + CVA variants
|
|
16
|
+
const buttonVariants = cva(
|
|
17
|
+
"inline-flex items-center justify-center whitespace-nowrap text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
|
18
|
+
{
|
|
19
|
+
defaultVariants: { size: "default", variant: "default" },
|
|
20
|
+
variants: {
|
|
21
|
+
size: {
|
|
22
|
+
clear: "",
|
|
23
|
+
default: "h-10 px-4 py-2",
|
|
24
|
+
icon: "h-10 w-10",
|
|
25
|
+
lg: "h-11 px-8",
|
|
26
|
+
sm: "h-7 px-2",
|
|
27
|
+
},
|
|
28
|
+
variant: {
|
|
29
|
+
default:
|
|
30
|
+
"ripple-default bg-primary text-stone-200 dark:text-stone-200 --text-primary-foreground transition-all duration-300 hover:shadow-md hover:color-accent will-change-transform",
|
|
31
|
+
destructive:
|
|
32
|
+
"ripple-destructive bg-destructive text-stone-200 dark:text-stone-200 text-destructive-foreground transition-all duration-300 hover:shadow-md hover:bg-destructive/90 will-change-transform",
|
|
33
|
+
ghost:
|
|
34
|
+
"ripple-ghost bg-blur text-stone-200 dark:text-stone-200 transition-all duration-300 hover:shadow-md hover:text-accent-foreground will-change-transform",
|
|
35
|
+
link: "ripple-link",
|
|
36
|
+
outline:
|
|
37
|
+
"ripple-outline border border-border text-stone-200 dark:text-stone-200 bg-transparent hover:bg-white/10 transition-all duration-300 hover:shadow-lg hover:text-alt will-change-transform",
|
|
38
|
+
secondary:
|
|
39
|
+
"ripple-secondary text-stone-200 dark:text-stone-200 bg-secondary text-secondary-foreground transition-all duration-300 hover:shadow-md hover:bg-secondary/80 will-change-transform",
|
|
40
|
+
|
|
41
|
+
// ---- extra visual variants (v1..v8)
|
|
42
|
+
v1: "ripple-v1 border-[0.5px] text-stone-200 dark:text-stone-200 rounded-sm bg-transparent duration-200 hover:bg-primary shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] active:shadow-none border-stone-800 text-zinc-800 dark:border-stone-600 dark:shadow-[3px_3px_0px_0px_rgba(255,255,255,0.7)] active:dark:shadow-none dark:text-zinc-50 dark:bg-zinc-950",
|
|
43
|
+
v2: "ripple-v2 text-stone-200 dark:text-stone-200 group relative justify-center rounded-0 overflow-hidden ease-in-out hover:scale-100 hover:shadow-lg text-zinc-50 bg-linear-to-tr from-zinc-900 to-zinc-700 hover:shadow-zinc-500/30 dark:text-stone-900 dark:bg-linear-to-tr dark:from-zinc-50 dark:to-zinc-100 dark:hover:shadow-zinc-700/30",
|
|
44
|
+
v3: "ripple-v3 text-stone-200 dark:text-stone-200 relative overflow-hidden group hover:ring-2 hover:ring-offset-2 ease-out hover:bg-gradient-to-r bg-zinc-900 hover:from-zinc-800 hover:to-zinc-700 text-zinc-50 hover:ring-zinc-900 dark:bg-zinc-50 dark:hover:from-zinc-50 dark:hover:to-zinc-100 dark:text-zinc-800 dark:hover:ring-white dark:ring-offset-black",
|
|
45
|
+
v4: "ripple-v4 text-stone-200 dark:text-stone-200 group relative overflow-hidden rounded-md bg-linear-to-tr duration-300 ease-in-out active:translate-y-0.5 active:scale-100 active:shadow-none from-zinc-800 to-zinc-700 text-black shadow-[0px_3px_0px_rgba(82,82,91,0.9)] dark:active:shadow-none dark:from-zinc-50 dark:to-zinc-100 dark:text-zinc-800 dark:shadow-[0px_3px_0px_rgba(161,161,170,0.9)]",
|
|
46
|
+
v5: "ripple-v5 text-stone-200 dark:text-stone-200 relative overflow-hidden border shadow group invert-[1] border-stone-300 text-zinc-800 bg-zinc-50 dark:border-stone-700 dark:text-zinc-100 dark:bg-zinc-950",
|
|
47
|
+
v6: "ripple-v6 text-stone-200 dark:text-stone-200 relative rounded-sm border dark:border-stone-200 border-stone-600 group scale-y-[.88] scale-x-[0.97] group-hover:scale-100 duration-300 font-semibold transition dark:bg-zinc-50 bg-zinc-950 text-zinc-100 dark:text-zinc-800",
|
|
48
|
+
v7: "ripple-v7 text-stone-200 dark:text-stone-200 relative hover:no-underline px-[1px] pb-0 ease-in after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-full after:translate-y-[3px] after:rounded-full after:dark:bg-zinc-50 after:bg-zinc-800 after:opacity-0 after:duration-300 after:content-[''] hover:after:-translate-y-1 hover:after:opacity-100",
|
|
49
|
+
v8: "ripple-v8 text-stone-200 dark:text-stone-200 shadow-embossed hover:shadow-none ease-out hover:scale-100 dark:bg-[#121212] dark:text-[#999999] bg-zinc-50 text-stone-900",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
export interface ButtonProps
|
|
56
|
+
extends
|
|
57
|
+
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
58
|
+
VariantProps<typeof buttonVariants> {
|
|
59
|
+
asChild?: boolean;
|
|
60
|
+
icon?: React.ReactNode;
|
|
61
|
+
href?: string; // ← Add href prop for navigation
|
|
62
|
+
newTab?: boolean; // ← Add newTab prop
|
|
63
|
+
onNavigate?: () => void; // ← Add navigation callback
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
|
67
|
+
{
|
|
68
|
+
asChild = false,
|
|
69
|
+
className,
|
|
70
|
+
size,
|
|
71
|
+
variant,
|
|
72
|
+
onClick,
|
|
73
|
+
icon,
|
|
74
|
+
children,
|
|
75
|
+
href,
|
|
76
|
+
newTab = false,
|
|
77
|
+
onNavigate,
|
|
78
|
+
...props
|
|
79
|
+
},
|
|
80
|
+
ref,
|
|
81
|
+
) {
|
|
82
|
+
// inject CSS once
|
|
83
|
+
React.useEffect(() => {
|
|
84
|
+
const style = document.createElement("style");
|
|
85
|
+
style.innerHTML = rippleCSS;
|
|
86
|
+
document.head.appendChild(style);
|
|
87
|
+
return () => style.remove();
|
|
88
|
+
}, []);
|
|
89
|
+
|
|
90
|
+
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
91
|
+
const button = e.currentTarget;
|
|
92
|
+
const ripple = document.createElement("span");
|
|
93
|
+
const rect = button.getBoundingClientRect();
|
|
94
|
+
|
|
95
|
+
ripple.className = "ripple-effect";
|
|
96
|
+
ripple.style.left = `${e.clientX - rect.left}px`;
|
|
97
|
+
ripple.style.top = `${e.clientY - rect.top}px`;
|
|
98
|
+
ripple.style.width =
|
|
99
|
+
ripple.style.height = `${Math.max(rect.width, rect.height)}px`;
|
|
100
|
+
|
|
101
|
+
const variantClass = Array.from(button.classList).find((cls) =>
|
|
102
|
+
cls.startsWith("ripple-"),
|
|
103
|
+
);
|
|
104
|
+
switch (variantClass) {
|
|
105
|
+
case "ripple-destructive":
|
|
106
|
+
ripple.style.backgroundColor = "rgba(255, 0, 0, 0.3)";
|
|
107
|
+
break;
|
|
108
|
+
case "ripple-outline":
|
|
109
|
+
ripple.style.backgroundColor = "rgba(255, 255, 255, 0.15)";
|
|
110
|
+
break;
|
|
111
|
+
case "ripple-secondary":
|
|
112
|
+
ripple.style.backgroundColor = "rgba(255, 255, 255, 0.2)";
|
|
113
|
+
break;
|
|
114
|
+
case "ripple-ghost":
|
|
115
|
+
case "ripple-link":
|
|
116
|
+
ripple.style.backgroundColor = "rgba(139, 92, 246, 0.25)";
|
|
117
|
+
break;
|
|
118
|
+
default:
|
|
119
|
+
ripple.style.backgroundColor = "rgba(139, 92, 246, 0.8)";
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
button.appendChild(ripple);
|
|
124
|
+
ripple.addEventListener("animationend", () => ripple.remove());
|
|
125
|
+
|
|
126
|
+
// Handle navigation if href is provided
|
|
127
|
+
if (href) {
|
|
128
|
+
e.preventDefault();
|
|
129
|
+
|
|
130
|
+
// Call onNavigate callback (e.g., closeDrawer)
|
|
131
|
+
onNavigate?.();
|
|
132
|
+
|
|
133
|
+
// Navigate after a small delay for drawer animation
|
|
134
|
+
setTimeout(() => {
|
|
135
|
+
if (newTab) {
|
|
136
|
+
window.open(href, "_blank");
|
|
137
|
+
} else {
|
|
138
|
+
window.location.href = href;
|
|
139
|
+
}
|
|
140
|
+
}, 150);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Call original onClick handler
|
|
144
|
+
onClick?.(e);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// --- CONTENT (decorations only when not `asChild`)
|
|
148
|
+
const content =
|
|
149
|
+
variant === "v5" ? (
|
|
150
|
+
<>
|
|
151
|
+
<span className="absolute inset-0 rounded-sm flex items-center justify-center size-full duration-700 -translate-x-full group-hover:translate-x-0 bg-zinc-800 dark:bg-zinc-100 text-zinc-100 dark:text-zinc-800">
|
|
152
|
+
{icon ?? null}
|
|
153
|
+
</span>
|
|
154
|
+
<span className="absolute flex items-center justify-center w-full h-full transition-all duration-500 ease-out transform group-hover:translate-x-full ">
|
|
155
|
+
{children}
|
|
156
|
+
</span>
|
|
157
|
+
<span className="relative invisible">{children}</span>
|
|
158
|
+
</>
|
|
159
|
+
) : variant === "v2" ? (
|
|
160
|
+
<>
|
|
161
|
+
<span className="relative">{children}</span>
|
|
162
|
+
<span
|
|
163
|
+
className="absolute inset-0 flex size-full justify-center [transform:skew(-14deg)_translateX(-100%)] group-hover:duration-1000 group-hover:[transform:skew(-14deg)_translateX(100%)]"
|
|
164
|
+
style={{ width: "200% !important" }}
|
|
165
|
+
aria-hidden="true"
|
|
166
|
+
>
|
|
167
|
+
<span className="relative h-full w-32 scale-200 bg-black/20 dark:bg-black/10" />
|
|
168
|
+
</span>
|
|
169
|
+
</>
|
|
170
|
+
) : variant === "v3" ? (
|
|
171
|
+
<>
|
|
172
|
+
<span
|
|
173
|
+
className="absolute right-0 w-8 h-32 -mt-12 transition-all duration-1000 transform translate-x-12 bg-white/80 dark:bg-zinc-900 opacity-10 rotate-12 group-hover:-translate-x-60 ease"
|
|
174
|
+
aria-hidden="true"
|
|
175
|
+
/>
|
|
176
|
+
<span className="relative">{children}</span>
|
|
177
|
+
</>
|
|
178
|
+
) : variant === "v4" ? (
|
|
179
|
+
<>
|
|
180
|
+
<span
|
|
181
|
+
className="absolute size-0 rounded-md bg-white/80 dark:bg-black/80 opacity-10 transition-all duration-300 ease-out group-hover:size-full text-stone-200 dark:text-stone-200"
|
|
182
|
+
aria-hidden="true"
|
|
183
|
+
/>
|
|
184
|
+
<span className="relative">{children}</span>
|
|
185
|
+
</>
|
|
186
|
+
) : (
|
|
187
|
+
<span className="relative inline-flex items-center gap-1">
|
|
188
|
+
{icon && <span className="mx-1">{icon}</span>}
|
|
189
|
+
{children}
|
|
190
|
+
</span>
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// --- RENDER
|
|
194
|
+
if (asChild) {
|
|
195
|
+
const singleChild = React.isValidElement(children) ? (
|
|
196
|
+
children
|
|
197
|
+
) : (
|
|
198
|
+
<span className="inline-flex items-center">{children}</span>
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<Slot
|
|
203
|
+
onClick={handleClick as any}
|
|
204
|
+
className={cn(
|
|
205
|
+
"ripple-container",
|
|
206
|
+
buttonVariants({ size, variant }),
|
|
207
|
+
className,
|
|
208
|
+
)}
|
|
209
|
+
ref={ref as any}
|
|
210
|
+
{...props}
|
|
211
|
+
>
|
|
212
|
+
{singleChild}
|
|
213
|
+
</Slot>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<button
|
|
219
|
+
ref={ref}
|
|
220
|
+
onClick={handleClick}
|
|
221
|
+
className={cn(
|
|
222
|
+
"ripple-container",
|
|
223
|
+
buttonVariants({ size, variant }),
|
|
224
|
+
className,
|
|
225
|
+
)}
|
|
226
|
+
{...props}
|
|
227
|
+
>
|
|
228
|
+
{content}
|
|
229
|
+
</button>
|
|
230
|
+
);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
export { Button, buttonVariants };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { cn } from '@/utilities/ui'
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
|
|
4
|
+
const Input: React.FC<
|
|
5
|
+
{
|
|
6
|
+
ref?: React.Ref<HTMLInputElement>
|
|
7
|
+
} & React.InputHTMLAttributes<HTMLInputElement>
|
|
8
|
+
> = ({ type, className, ref, ...props }) => {
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
className={cn(
|
|
12
|
+
'group relative flex h-10 w-full items-center justify-center top-0 rounded',
|
|
13
|
+
'!rounded !border-border !border-stone-600'
|
|
14
|
+
)}
|
|
15
|
+
>
|
|
16
|
+
<input
|
|
17
|
+
className={cn(
|
|
18
|
+
'text-stone-200 dark:text-stone-200 flex h-10 w-full rounded border border-stone-600 bg-neutral-900/50 px-3 py-2 text-sm',
|
|
19
|
+
'ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium',
|
|
20
|
+
'placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
|
21
|
+
'group-focus-within:brightness-110 group-focus-within:contrast-110',
|
|
22
|
+
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
23
|
+
className
|
|
24
|
+
)}
|
|
25
|
+
ref={ref}
|
|
26
|
+
type={type}
|
|
27
|
+
{...props}
|
|
28
|
+
/>
|
|
29
|
+
|
|
30
|
+
<style>{`.glass-input{position:relative;overflow:hidden;border:1px solid rgba(255,255,255,.075);background:linear-gradient(180deg,rgba(255,255,255,.035),rgba(255,255,255,.008) 32%,rgba(255,255,255,.004)),linear-gradient(145deg,rgba(255,255,255,.03),transparent 26%,rgba(0,0,0,.24) 100%),rgba(9,10,15,.82);backdrop-filter:blur(22px) saturate(1.05);-webkit-backdrop-filter:blur(22px) saturate(1.05);box-shadow:0 22px 34px rgba(0,0,0,.55),inset 0 1px 0 rgba(255,255,255,.105),inset 0 -18px 30px rgba(0,0,0,.46);transition:transform .28s ease,border-color .28s ease,box-shadow .28s ease,background .28s ease,filter .28s ease;corner-shape:squircle}.glass-input::before{content:'';position:absolute;inset:0;pointer-events:none;border-radius:inherit;corner-shape:squircle;background:linear-gradient(130deg,rgba(255,255,255,.075),transparent 26%,transparent 68%,rgba(255,255,255,.025)),radial-gradient(circle at var(--mx,50%) var(--my,22%),rgba(255,255,255,.08),transparent 7rem);mix-blend-mode:soft-light}.glass-input::after{content:'';position:absolute;inset:1px;pointer-events:none;border-radius:inherit;corner-shape:squircle;box-shadow:inset 0 0 0 1px rgba(255,255,255,.04),inset 0 12px 18px rgba(255,255,255,.018),inset 0 -18px 32px rgba(0,0,0,.42)}.glass-input:hover,.glass-input:focus-within{transform:translateY(-2px);filter:brightness(1.03) saturate(1.05);border-color:rgba(129,140,248,.22);background:linear-gradient(180deg,rgba(255,255,255,.04),rgba(255,255,255,.012) 32%,rgba(255,255,255,.006)),linear-gradient(145deg,rgba(255,255,255,.035),transparent 26%,rgba(0,0,0,.28) 100%),rgba(6,7,11,.92);box-shadow:0 30px 42px rgba(0,0,0,.66),0 0 38px rgba(99,102,241,.11),inset 0 1px 0 rgba(255,255,255,.14),inset 0 -18px 30px rgba(0,0,0,.52)}.glass-input input{position:relative;z-index:2;background:transparent;color:#fff;outline:none}.glass-input input::placeholder{color:rgba(224,231,255,.28)}`}</style>
|
|
31
|
+
|
|
32
|
+
{/* LEFT EDGE GLOW (hover + focus only) */}
|
|
33
|
+
<div className="pointer-events-none absolute inset-y-0 left-0 w-[10rem] rounded bg-transparent opacity-0 duration-300 group-hover:duration-150 group-hover:opacity-100 group-focus-within:!opacity-100 group-focus-within:saturate-0 group-focus-within:brightness-50 group-focus-within:w-[15rem] !bg-[radial-gradient(ellipse_at_left,rgba(139,92,246,0.35)_0%,rgba(139,92,246,0.25)_20%,rgba(139,92,246,0.12)_40%,transparent_70%)] mix-blend-screen brightness-75 transition-all" />
|
|
34
|
+
|
|
35
|
+
{/* HARD EDGE LINE (hover + focus only) */}
|
|
36
|
+
<div className="pointer-events-none absolute left-[0] top-[50%] group-hover:top-[48%] group-focus-within:top-[48%] h-[55%] group-focus-within:h-[57%] group-hover:h-[57%] w-[2px] group-focus-within:w-[3px] group-hover:w-[3px] -translate-y-1/2 rounded bg-violet-500/70 opacity-100 blur-[0] group-hover:blur-[0.5px] transition-opacity duration-300 group-hover:duration-150 group-hover:opacity-100 group-focus-within:blur-[0.5px] mix-blend-plus-lighter" />
|
|
37
|
+
</div>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export { Input }
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as LucideIcons from 'lucide-react'
|
|
4
|
+
import { icons } from 'lucide-react'
|
|
5
|
+
|
|
6
|
+
export type IconName = keyof typeof icons
|
|
7
|
+
|
|
8
|
+
const IconPickerCell = ({ cellData }: { cellData: string }) => {
|
|
9
|
+
if (!cellData) return <span className="text-muted-foreground">No icon</span>
|
|
10
|
+
|
|
11
|
+
const iconName = cellData as IconName
|
|
12
|
+
const Icon = LucideIcons[iconName]
|
|
13
|
+
|
|
14
|
+
if (!Icon) return <span className="text-muted-foreground">{cellData}</span>
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="flex items-center gap-2">
|
|
18
|
+
<Icon className="h-4 w-4" style={{ marginRight: '4px' }} />
|
|
19
|
+
<span className="text-sm">{iconName}</span>
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default IconPickerCell
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Lucide Icon Picker Field
|
|
2
|
+
|
|
3
|
+
A customizable icon picker field for PayloadCMS with Lucide icons, fuzzy search, and virtualized rendering.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✨ **Lucide Icons** - Access to the complete Lucide icon library
|
|
8
|
+
- 🔍 **Fuzzy Search** - Powered by Fuse.js for intelligent icon discovery
|
|
9
|
+
- ⚡ **Virtualized Rendering** - Smooth performance with @tanstack/react-virtual
|
|
10
|
+
- 👁️ **Icon Preview** - Visual preview in both edit and list views
|
|
11
|
+
- 🎨 **Custom Cell Component** - Displays icon preview with name in admin list views
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
Dependencies installed:
|
|
16
|
+
|
|
17
|
+
- `fuse.js` - For fuzzy search
|
|
18
|
+
- `@tanstack/react-virtual` - For virtualized rendering
|
|
19
|
+
- `lucide-react` - Icon library (already in project)
|
|
20
|
+
|
|
21
|
+
## Basic Usage
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { iconPicker } from '@/fields/lucide-icon-picker/field'
|
|
25
|
+
|
|
26
|
+
export const MyCollection = {
|
|
27
|
+
slug: 'my-collection',
|
|
28
|
+
fields: [
|
|
29
|
+
iconPicker({
|
|
30
|
+
name: 'icon',
|
|
31
|
+
required: true,
|
|
32
|
+
description: 'Choose an icon for this feature'
|
|
33
|
+
})
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Advanced Usage
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import { iconPicker } from '@/fields/lucide-icon-picker/field'
|
|
42
|
+
|
|
43
|
+
export const Features = {
|
|
44
|
+
slug: 'features',
|
|
45
|
+
fields: [
|
|
46
|
+
{
|
|
47
|
+
name: 'title',
|
|
48
|
+
type: 'text',
|
|
49
|
+
required: true
|
|
50
|
+
},
|
|
51
|
+
iconPicker({
|
|
52
|
+
name: 'featureIcon',
|
|
53
|
+
required: true,
|
|
54
|
+
description: 'Icon displayed for this feature'
|
|
55
|
+
}),
|
|
56
|
+
{
|
|
57
|
+
name: 'description',
|
|
58
|
+
type: 'textarea'
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## With Conditional Logic
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
iconPicker({
|
|
68
|
+
name: 'menuIcon',
|
|
69
|
+
description: 'Icon displayed in the navigation menu',
|
|
70
|
+
overrides: (field) => ({
|
|
71
|
+
...field,
|
|
72
|
+
admin: {
|
|
73
|
+
...field.admin,
|
|
74
|
+
condition: (data) => data.showIcon === true
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## API Reference
|
|
81
|
+
|
|
82
|
+
### iconPicker Options
|
|
83
|
+
|
|
84
|
+
| Prop | Type | Default | Description |
|
|
85
|
+
| ------------- | --------------------------------- | -------- | --------------------------------- |
|
|
86
|
+
| `name` | `string` | `'icon'` | Field name |
|
|
87
|
+
| `required` | `boolean` | `false` | Whether field is required |
|
|
88
|
+
| `description` | `string` | - | Helper text shown below field |
|
|
89
|
+
| `overrides` | `(field: TextField) => TextField` | - | Function to override field config |
|
|
90
|
+
|
|
91
|
+
## Using Icons on Frontend
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
import * as LucideIcons from 'lucide-react'
|
|
95
|
+
|
|
96
|
+
interface FeatureProps {
|
|
97
|
+
icon: string
|
|
98
|
+
title: string
|
|
99
|
+
description: string
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function Feature({ icon, title, description }: FeatureProps) {
|
|
103
|
+
const Icon = LucideIcons[icon as keyof typeof LucideIcons]
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<div className="feature">
|
|
107
|
+
{Icon && <Icon className="h-6 w-6" />}
|
|
108
|
+
<h3>{title}</h3>
|
|
109
|
+
<p>{description}</p>
|
|
110
|
+
</div>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Implementation Details
|
|
116
|
+
|
|
117
|
+
### Files Structure
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
src/fields/lucide-icon-picker/
|
|
121
|
+
├── field.ts # Field definition and types
|
|
122
|
+
├── index.tsx # Main field component with picker UI
|
|
123
|
+
└── Cell.tsx # List view cell component
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Key Features
|
|
127
|
+
|
|
128
|
+
1. **Fuzzy Search**: Uses Fuse.js with a threshold of 0.3 for flexible matching
|
|
129
|
+
2. **Virtualization**: Only renders visible icons for performance (6 icons per row)
|
|
130
|
+
3. **Icon Preview**: Shows selected icon in both the picker and list view
|
|
131
|
+
4. **Responsive**: Grid layout adapts to container size
|
|
132
|
+
|
|
133
|
+
### Performance
|
|
134
|
+
|
|
135
|
+
- Virtualizes ~1000+ icons for smooth scrolling
|
|
136
|
+
- Debounced search for smooth typing experience
|
|
137
|
+
- Only visible icons are rendered in the DOM
|
|
138
|
+
|
|
139
|
+
## Example Collections
|
|
140
|
+
|
|
141
|
+
See these collections for examples:
|
|
142
|
+
|
|
143
|
+
- (Add your collection examples here as you use the field)
|
|
144
|
+
|
|
145
|
+
## Troubleshooting
|
|
146
|
+
|
|
147
|
+
### Icons not displaying
|
|
148
|
+
|
|
149
|
+
- Ensure `lucide-react` is installed: `npm install lucide-react`
|
|
150
|
+
- Verify the icon name matches exactly (case-sensitive)
|
|
151
|
+
|
|
152
|
+
### Search not working
|
|
153
|
+
|
|
154
|
+
- Check that `fuse.js` is installed
|
|
155
|
+
- Clear browser cache and reload
|
|
156
|
+
|
|
157
|
+
### Performance issues
|
|
158
|
+
|
|
159
|
+
- The field uses virtualization, but very large result sets (1000+) may still lag
|
|
160
|
+
- Consider adding more specific search terms
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { TextField } from 'payload'
|
|
2
|
+
|
|
3
|
+
export interface IconPickerFieldProps {
|
|
4
|
+
name?: string
|
|
5
|
+
required?: boolean
|
|
6
|
+
showTextInput?: boolean
|
|
7
|
+
searchable?: boolean
|
|
8
|
+
categorized?: boolean
|
|
9
|
+
searchPlaceholder?: string
|
|
10
|
+
triggerPlaceholder?: string
|
|
11
|
+
description?: string
|
|
12
|
+
overrides?: (field: TextField) => TextField
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const iconPicker = ({
|
|
16
|
+
name = 'icon',
|
|
17
|
+
required = false,
|
|
18
|
+
showTextInput = true,
|
|
19
|
+
searchable = true,
|
|
20
|
+
categorized = true,
|
|
21
|
+
searchPlaceholder = 'Search icons...',
|
|
22
|
+
triggerPlaceholder = 'Select Icon',
|
|
23
|
+
description,
|
|
24
|
+
overrides
|
|
25
|
+
}: IconPickerFieldProps = {}): TextField => {
|
|
26
|
+
const field: TextField = {
|
|
27
|
+
name,
|
|
28
|
+
type: 'text',
|
|
29
|
+
required,
|
|
30
|
+
admin: {
|
|
31
|
+
description,
|
|
32
|
+
components: {
|
|
33
|
+
Field: '/fields/lucide-icon-picker/index.tsx',
|
|
34
|
+
Cell: '/fields/lucide-icon-picker/Cell.tsx'
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return overrides ? overrides(field) : field
|
|
40
|
+
}
|