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.
Files changed (102) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +68 -0
  3. package/bin/index.js +8 -0
  4. package/package.json +40 -0
  5. package/src/cli.js +79 -0
  6. package/src/commands/add.js +327 -0
  7. package/src/commands/doctor.js +21 -0
  8. package/src/commands/list.js +18 -0
  9. package/src/installers/checkRequirements.js +101 -0
  10. package/src/installers/copyTemplateFiles.js +79 -0
  11. package/src/installers/installDependencies.js +69 -0
  12. package/src/installers/patchPayloadBlocksArray.js +122 -0
  13. package/src/installers/patchRenderBlocks.js +110 -0
  14. package/src/installers/runGenerators.js +3 -0
  15. package/src/installers/writeInstalledBlockReadme.js +117 -0
  16. package/src/registry/listTemplates.js +17 -0
  17. package/src/registry/loadTemplateManifest.js +20 -0
  18. package/src/registry/validateTemplateManifest.js +69 -0
  19. package/src/utils/logger.js +8 -0
  20. package/src/utils/packageManager.js +42 -0
  21. package/src/utils/paths.js +7 -0
  22. package/src/utils/project.js +122 -0
  23. package/src/utils/strings.js +7 -0
  24. package/templates/blocks/DD-BookCall/Component.tsx +573 -0
  25. package/templates/blocks/DD-BookCall/config.ts +239 -0
  26. package/templates/blocks/DD-BookCall/manifest.json +112 -0
  27. package/templates/blocks/DD-CardTemplate01/Component.tsx +144 -0
  28. package/templates/blocks/DD-CardTemplate01/config.ts +106 -0
  29. package/templates/blocks/DD-CardTemplate01/manifest.json +84 -0
  30. package/templates/blocks/DD-Carousel-A/Component.tsx +266 -0
  31. package/templates/blocks/DD-Carousel-A/config.ts +163 -0
  32. package/templates/blocks/DD-Carousel-A/manifest.json +86 -0
  33. package/templates/blocks/DD-Carousel-B/Component.tsx +432 -0
  34. package/templates/blocks/DD-Carousel-B/config.ts +176 -0
  35. package/templates/blocks/DD-Carousel-B/manifest.json +86 -0
  36. package/templates/blocks/DD-ComparisonTable/Component.tsx +272 -0
  37. package/templates/blocks/DD-ComparisonTable/config.ts +105 -0
  38. package/templates/blocks/DD-ComparisonTable/manifest.json +78 -0
  39. package/templates/blocks/DD-Contact/Component.tsx +439 -0
  40. package/templates/blocks/DD-Contact/config.ts +217 -0
  41. package/templates/blocks/DD-Contact/manifest.json +107 -0
  42. package/templates/blocks/DD-FeatCat/Component.tsx +361 -0
  43. package/templates/blocks/DD-FeatCat/config.ts +204 -0
  44. package/templates/blocks/DD-FeatCat/manifest.json +87 -0
  45. package/templates/blocks/DD-FeatStrip/Component.tsx +615 -0
  46. package/templates/blocks/DD-FeatStrip/config.ts +134 -0
  47. package/templates/blocks/DD-FeatStrip/manifest.json +82 -0
  48. package/templates/blocks/DD-Hero/Component.tsx +317 -0
  49. package/templates/blocks/DD-Hero/config.ts +267 -0
  50. package/templates/blocks/DD-Hero/manifest.json +92 -0
  51. package/templates/blocks/DD-HorizontalScroll/Component.tsx +995 -0
  52. package/templates/blocks/DD-HorizontalScroll/config.ts +231 -0
  53. package/templates/blocks/DD-HorizontalScroll/manifest.json +85 -0
  54. package/templates/blocks/DD-MasonaryMedia/Component.tsx +571 -0
  55. package/templates/blocks/DD-MasonaryMedia/config.ts +390 -0
  56. package/templates/blocks/DD-MasonaryMedia/manifest.json +101 -0
  57. package/templates/blocks/DD-MasonaryMedia/vendor.d.ts +2 -0
  58. package/templates/blocks/DD-Pricing/Component.tsx +380 -0
  59. package/templates/blocks/DD-Pricing/config.ts +215 -0
  60. package/templates/blocks/DD-Pricing/manifest.json +78 -0
  61. package/templates/blocks/DD-Process/Component.tsx +155 -0
  62. package/templates/blocks/DD-Process/config.ts +150 -0
  63. package/templates/blocks/DD-Process/manifest.json +82 -0
  64. package/templates/blocks/DD-Section/Component.tsx +327 -0
  65. package/templates/blocks/DD-Section/config.ts +214 -0
  66. package/templates/blocks/DD-Section/manifest.json +85 -0
  67. package/templates/blocks/DD-Services/Component.tsx +188 -0
  68. package/templates/blocks/DD-Services/config.ts +149 -0
  69. package/templates/blocks/DD-Services/manifest.json +95 -0
  70. package/templates/blocks/DD-ShowcaseGrid/Component.tsx +317 -0
  71. package/templates/blocks/DD-ShowcaseGrid/config.ts +197 -0
  72. package/templates/blocks/DD-ShowcaseGrid/manifest.json +80 -0
  73. package/templates/blocks/DD-Slider-A/Component.tsx +252 -0
  74. package/templates/blocks/DD-Slider-A/config.ts +118 -0
  75. package/templates/blocks/DD-Slider-A/manifest.json +86 -0
  76. package/templates/blocks/DD-StackCards/Component.tsx +496 -0
  77. package/templates/blocks/DD-StackCards/config.ts +399 -0
  78. package/templates/blocks/DD-StackCards/manifest.json +87 -0
  79. package/templates/blocks/DD-Team/Component.tsx +265 -0
  80. package/templates/blocks/DD-Team/config.ts +173 -0
  81. package/templates/blocks/DD-Team/manifest.json +82 -0
  82. package/templates/blocks/DD-TechStack/Component.tsx +189 -0
  83. package/templates/blocks/DD-TechStack/config.ts +153 -0
  84. package/templates/blocks/DD-TechStack/manifest.json +118 -0
  85. package/templates/blocks/DD-Testimonials/Component.tsx +147 -0
  86. package/templates/blocks/DD-Testimonials/config.ts +120 -0
  87. package/templates/blocks/DD-Testimonials/manifest.json +87 -0
  88. package/templates/blocks/DD-Work/Component.tsx +328 -0
  89. package/templates/blocks/DD-Work/config.ts +165 -0
  90. package/templates/blocks/DD-Work/manifest.json +87 -0
  91. package/templates/blocks/DD-Work-B/Component.tsx +320 -0
  92. package/templates/blocks/DD-Work-B/config.ts +165 -0
  93. package/templates/blocks/DD-Work-B/manifest.json +75 -0
  94. package/templates/shared/components/ui/anims/fade.tsx +83 -0
  95. package/templates/shared/components/ui/button.tsx +233 -0
  96. package/templates/shared/components/ui/input.tsx +41 -0
  97. package/templates/shared/fields/lucide-icon-picker/Cell.tsx +24 -0
  98. package/templates/shared/fields/lucide-icon-picker/README.md +160 -0
  99. package/templates/shared/fields/lucide-icon-picker/field.ts +40 -0
  100. package/templates/shared/fields/lucide-icon-picker/index.tsx +203 -0
  101. package/templates/shared/utilities/canUseDOM.ts +1 -0
  102. 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
+ }