@srcroot/ui 0.0.55 → 0.0.58
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 +151 -151
- package/dist/index.d.ts +0 -0
- package/dist/index.js +120 -93
- package/package.json +7 -2
- package/src/registry/analytics/google-analytics.tsx +36 -39
- package/src/registry/analytics/google-tag-manager.tsx +62 -65
- package/src/registry/analytics/meta-pixel.tsx +44 -47
- package/src/registry/analytics/microsoft-clarity.tsx +31 -34
- package/src/registry/analytics/tiktok-pixel.tsx +34 -37
- package/src/registry/lib/utils.ts +0 -0
- package/src/registry/themes/v3/blue.css +157 -157
- package/src/registry/themes/v3/glass.css +153 -153
- package/src/registry/themes/v3/gray.css +157 -157
- package/src/registry/themes/v3/green.css +157 -157
- package/src/registry/themes/v3/neutral.css +157 -157
- package/src/registry/themes/v3/orange.css +157 -157
- package/src/registry/themes/v3/rose.css +157 -157
- package/src/registry/themes/v3/slate.css +157 -157
- package/src/registry/themes/v3/stone.css +157 -157
- package/src/registry/themes/v3/violet.css +186 -186
- package/src/registry/themes/v3/zinc.css +157 -157
- package/src/registry/themes/v4/blue.css +184 -184
- package/src/registry/themes/v4/glass.css +180 -180
- package/src/registry/themes/v4/gray.css +184 -184
- package/src/registry/themes/v4/green.css +184 -184
- package/src/registry/themes/v4/neutral.css +184 -184
- package/src/registry/themes/v4/orange.css +184 -184
- package/src/registry/themes/v4/rose.css +184 -184
- package/src/registry/themes/v4/slate.css +184 -184
- package/src/registry/themes/v4/stone.css +184 -184
- package/src/registry/themes/v4/violet.css +184 -184
- package/src/registry/themes/v4/zinc.css +184 -184
- package/src/registry/ui/accordion.tsx +164 -165
- package/src/registry/ui/alert-dialog.tsx +213 -214
- package/src/registry/ui/alert.tsx +73 -76
- package/src/registry/ui/aspect-ratio.tsx +44 -47
- package/src/registry/ui/avatar.tsx +96 -97
- package/src/registry/ui/badge.tsx +52 -55
- package/src/registry/ui/breadcrumb.tsx +147 -150
- package/src/registry/ui/button-group.tsx +64 -67
- package/src/registry/ui/button.tsx +71 -72
- package/src/registry/ui/calendar.tsx +514 -515
- package/src/registry/ui/card.tsx +88 -91
- package/src/registry/ui/carousel.tsx +214 -214
- package/src/registry/ui/chart.tsx +373 -373
- package/src/registry/ui/chatbot.tsx +86 -13
- package/src/registry/ui/checkbox.tsx +93 -94
- package/src/registry/ui/collapsible.tsx +107 -108
- package/src/registry/ui/combobox.tsx +171 -171
- package/src/registry/ui/command.tsx +300 -300
- package/src/registry/ui/container.tsx +44 -47
- package/src/registry/ui/context-menu.tsx +221 -221
- package/src/registry/ui/date-picker.tsx +228 -228
- package/src/registry/ui/dialog.tsx +269 -270
- package/src/registry/ui/drawer.tsx +10 -4
- package/src/registry/ui/dropdown-menu.tsx +529 -530
- package/src/registry/ui/empty-state.tsx +0 -2
- package/src/registry/ui/file-upload.tsx +0 -0
- package/src/registry/ui/floating-dock.tsx +0 -0
- package/src/registry/ui/form-field.tsx +91 -94
- package/src/registry/ui/google-analytics.tsx +38 -0
- package/src/registry/ui/google-tag-manager.tsx +64 -0
- package/src/registry/ui/hover-card.tsx +223 -223
- package/src/registry/ui/image.tsx +144 -147
- package/src/registry/ui/input-group.tsx +82 -85
- package/src/registry/ui/input.tsx +125 -125
- package/src/registry/ui/kbd.tsx +60 -63
- package/src/registry/ui/label.tsx +36 -37
- package/src/registry/ui/loading-spinner.tsx +108 -111
- package/src/registry/ui/map.tsx +0 -0
- package/src/registry/ui/marquee.tsx +2 -0
- package/src/registry/ui/menubar.tsx +246 -246
- package/src/registry/ui/meta-pixel.tsx +46 -0
- package/src/registry/ui/microsoft-clarity.tsx +33 -0
- package/src/registry/ui/native-select.tsx +49 -52
- package/src/registry/ui/otp-input.tsx +163 -155
- package/src/registry/ui/pagination.tsx +149 -152
- package/src/registry/ui/patterns.tsx +28 -0
- package/src/registry/ui/popover.tsx +226 -227
- package/src/registry/ui/progress.tsx +51 -52
- package/src/registry/ui/radio.tsx +99 -102
- package/src/registry/ui/resizable.tsx +314 -314
- package/src/registry/ui/scroll-animation.tsx +45 -0
- package/src/registry/ui/scroll-area.tsx +121 -122
- package/src/registry/ui/scroll-to-top.tsx +0 -0
- package/src/registry/ui/search.tsx +162 -150
- package/src/registry/ui/select.tsx +292 -293
- package/src/registry/ui/separator.tsx +46 -47
- package/src/registry/ui/sheet.tsx +6 -3
- package/src/registry/ui/sidebar.tsx +628 -628
- package/src/registry/ui/skeleton.tsx +26 -29
- package/src/registry/ui/slider.tsx +196 -197
- package/src/registry/ui/slot.tsx +69 -72
- package/src/registry/ui/star-rating.tsx +146 -134
- package/src/registry/ui/switch.tsx +72 -73
- package/src/registry/ui/table-of-contents.tsx +96 -96
- package/src/registry/ui/table.tsx +138 -139
- package/src/registry/ui/tabs.tsx +124 -125
- package/src/registry/ui/text.tsx +61 -64
- package/src/registry/ui/textarea.tsx +41 -42
- package/src/registry/ui/theme-switcher.tsx +66 -66
- package/src/registry/ui/tiktok-pixel.tsx +36 -0
- package/src/registry/ui/toast.tsx +97 -98
- package/src/registry/ui/toggle-group.tsx +129 -129
- package/src/registry/ui/toggle.tsx +72 -72
- package/src/registry/ui/tooltip.tsx +143 -144
- package/src/registry/ui/whatsapp.tsx +0 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { ReactNode, useRef } from "react"
|
|
4
|
+
import gsap from "gsap"
|
|
5
|
+
import { useGSAP } from "@gsap/react"
|
|
6
|
+
import { ScrollTrigger } from "gsap/ScrollTrigger"
|
|
7
|
+
|
|
8
|
+
gsap.registerPlugin(ScrollTrigger)
|
|
9
|
+
|
|
10
|
+
interface ScrollAnimationProps {
|
|
11
|
+
children: ReactNode
|
|
12
|
+
className?: string
|
|
13
|
+
delay?: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function ScrollAnimation({ children, className = "", delay = 0 }: ScrollAnimationProps) {
|
|
17
|
+
const el = useRef<HTMLDivElement>(null)
|
|
18
|
+
|
|
19
|
+
useGSAP(() => {
|
|
20
|
+
gsap.fromTo(el.current,
|
|
21
|
+
{
|
|
22
|
+
opacity: 0,
|
|
23
|
+
y: 50
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
opacity: 1,
|
|
27
|
+
y: 0,
|
|
28
|
+
duration: 0.8,
|
|
29
|
+
delay: delay,
|
|
30
|
+
ease: "circ.out",
|
|
31
|
+
scrollTrigger: {
|
|
32
|
+
trigger: el.current,
|
|
33
|
+
start: "top 85%", // Trigger when top of element hits 85% of viewport height
|
|
34
|
+
toggleActions: "play none none reverse"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
}, { scope: el })
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div ref={el} className={className}>
|
|
42
|
+
{children}
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
@@ -1,122 +1,121 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import * as React from "react"
|
|
4
|
-
import { cn } from "@/lib/utils"
|
|
5
|
-
|
|
6
|
-
interface ScrollAreaProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
7
|
-
/** Orientation of scrollbar */
|
|
8
|
-
orientation?: "vertical" | "horizontal"
|
|
9
|
-
/** Scrollbar size: "thin" (4px), "default" (8px), "thick" (12px) */
|
|
10
|
-
scrollbarSize?: "thin" | "default" | "thick"
|
|
11
|
-
/** Hide scrollbar until hover */
|
|
12
|
-
hideScrollbar?: boolean
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// CSS for custom scrollbar styling
|
|
16
|
-
const scrollbarStyles = {
|
|
17
|
-
thin: {
|
|
18
|
-
width: "4px",
|
|
19
|
-
height: "4px",
|
|
20
|
-
},
|
|
21
|
-
default: {
|
|
22
|
-
width: "8px",
|
|
23
|
-
height: "8px",
|
|
24
|
-
},
|
|
25
|
-
thick: {
|
|
26
|
-
width: "12px",
|
|
27
|
-
height: "12px",
|
|
28
|
-
},
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* ScrollArea - Custom scrollbar container
|
|
33
|
-
*
|
|
34
|
-
* Provides a styled scrollbar that is thin and consistent across browsers.
|
|
35
|
-
* Supports customizable scrollbar size and orientation.
|
|
36
|
-
*/
|
|
37
|
-
const ScrollArea = React.forwardRef<HTMLDivElement, ScrollAreaProps>(
|
|
38
|
-
({ className, children, orientation = "vertical", scrollbarSize = "thin", hideScrollbar = false, style, ...props }, ref) => {
|
|
39
|
-
const sizes = scrollbarStyles[scrollbarSize]
|
|
40
|
-
|
|
41
|
-
const scrollbarCSS: React.CSSProperties = {
|
|
42
|
-
...style,
|
|
43
|
-
// Webkit browsers (Chrome, Safari, Edge)
|
|
44
|
-
// @ts-ignore - CSS custom properties for scrollbar
|
|
45
|
-
"--scrollbar-width": sizes.width,
|
|
46
|
-
"--scrollbar-height": sizes.height,
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return (
|
|
50
|
-
<div
|
|
51
|
-
ref={ref}
|
|
52
|
-
className={cn(
|
|
53
|
-
"relative",
|
|
54
|
-
// Container overflow based on orientation
|
|
55
|
-
orientation === "vertical" && "overflow-y-auto overflow-x-hidden",
|
|
56
|
-
orientation === "horizontal" && "overflow-x-auto overflow-y-hidden",
|
|
57
|
-
// Custom scrollbar classes
|
|
58
|
-
"scrollbar-custom",
|
|
59
|
-
hideScrollbar && "scrollbar-hide hover:scrollbar-show",
|
|
60
|
-
className
|
|
61
|
-
)}
|
|
62
|
-
style={scrollbarCSS}
|
|
63
|
-
{...props}
|
|
64
|
-
>
|
|
65
|
-
<style>{`
|
|
66
|
-
.scrollbar-custom::-webkit-scrollbar {
|
|
67
|
-
width: var(--scrollbar-width, 4px);
|
|
68
|
-
height: var(--scrollbar-height, 4px);
|
|
69
|
-
}
|
|
70
|
-
.scrollbar-custom::-webkit-scrollbar-track {
|
|
71
|
-
background: transparent;
|
|
72
|
-
border-radius: 9999px;
|
|
73
|
-
}
|
|
74
|
-
.scrollbar-custom::-webkit-scrollbar-thumb {
|
|
75
|
-
background: hsl(var(--muted-foreground) / 0.3);
|
|
76
|
-
border-radius: 9999px;
|
|
77
|
-
}
|
|
78
|
-
.scrollbar-custom::-webkit-scrollbar-thumb:hover {
|
|
79
|
-
background: hsl(var(--muted-foreground) / 0.5);
|
|
80
|
-
}
|
|
81
|
-
.scrollbar-custom {
|
|
82
|
-
scrollbar-width: thin;
|
|
83
|
-
scrollbar-color: hsl(var(--muted-foreground) / 0.3) transparent;
|
|
84
|
-
}
|
|
85
|
-
.scrollbar-hide::-webkit-scrollbar {
|
|
86
|
-
opacity: 0;
|
|
87
|
-
}
|
|
88
|
-
.scrollbar-hide:hover::-webkit-scrollbar,
|
|
89
|
-
.scrollbar-show::-webkit-scrollbar {
|
|
90
|
-
opacity: 1;
|
|
91
|
-
}
|
|
92
|
-
`}</style>
|
|
93
|
-
{children}
|
|
94
|
-
</div>
|
|
95
|
-
)
|
|
96
|
-
}
|
|
97
|
-
)
|
|
98
|
-
ScrollArea.displayName = "ScrollArea"
|
|
99
|
-
|
|
100
|
-
// ScrollBar component for explicit scrollbar styling reference (optional usage)
|
|
101
|
-
interface ScrollBarProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
102
|
-
orientation?: "vertical" | "horizontal"
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const ScrollBar = React.forwardRef<HTMLDivElement, ScrollBarProps>(
|
|
106
|
-
({ className, orientation = "vertical", ...props }, ref) => (
|
|
107
|
-
<div
|
|
108
|
-
ref={ref}
|
|
109
|
-
className={cn(
|
|
110
|
-
"flex touch-none select-none transition-colors",
|
|
111
|
-
orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent p-[1px]",
|
|
112
|
-
orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
|
113
|
-
className
|
|
114
|
-
)}
|
|
115
|
-
{...props}
|
|
116
|
-
/>
|
|
117
|
-
)
|
|
118
|
-
)
|
|
119
|
-
ScrollBar.displayName = "ScrollBar"
|
|
120
|
-
|
|
121
|
-
export { ScrollArea, ScrollBar }
|
|
122
|
-
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
interface ScrollAreaProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
/** Orientation of scrollbar */
|
|
8
|
+
orientation?: "vertical" | "horizontal"
|
|
9
|
+
/** Scrollbar size: "thin" (4px), "default" (8px), "thick" (12px) */
|
|
10
|
+
scrollbarSize?: "thin" | "default" | "thick"
|
|
11
|
+
/** Hide scrollbar until hover */
|
|
12
|
+
hideScrollbar?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// CSS for custom scrollbar styling
|
|
16
|
+
const scrollbarStyles = {
|
|
17
|
+
thin: {
|
|
18
|
+
width: "4px",
|
|
19
|
+
height: "4px",
|
|
20
|
+
},
|
|
21
|
+
default: {
|
|
22
|
+
width: "8px",
|
|
23
|
+
height: "8px",
|
|
24
|
+
},
|
|
25
|
+
thick: {
|
|
26
|
+
width: "12px",
|
|
27
|
+
height: "12px",
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* ScrollArea - Custom scrollbar container
|
|
33
|
+
*
|
|
34
|
+
* Provides a styled scrollbar that is thin and consistent across browsers.
|
|
35
|
+
* Supports customizable scrollbar size and orientation.
|
|
36
|
+
*/
|
|
37
|
+
const ScrollArea = React.forwardRef<HTMLDivElement, ScrollAreaProps>(
|
|
38
|
+
({ className, children, orientation = "vertical", scrollbarSize = "thin", hideScrollbar = false, style, ...props }, ref) => {
|
|
39
|
+
const sizes = scrollbarStyles[scrollbarSize]
|
|
40
|
+
|
|
41
|
+
const scrollbarCSS: React.CSSProperties = {
|
|
42
|
+
...style,
|
|
43
|
+
// Webkit browsers (Chrome, Safari, Edge)
|
|
44
|
+
// @ts-ignore - CSS custom properties for scrollbar
|
|
45
|
+
"--scrollbar-width": sizes.width,
|
|
46
|
+
"--scrollbar-height": sizes.height,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div
|
|
51
|
+
ref={ref}
|
|
52
|
+
className={cn(
|
|
53
|
+
"relative",
|
|
54
|
+
// Container overflow based on orientation
|
|
55
|
+
orientation === "vertical" && "overflow-y-auto overflow-x-hidden",
|
|
56
|
+
orientation === "horizontal" && "overflow-x-auto overflow-y-hidden",
|
|
57
|
+
// Custom scrollbar classes
|
|
58
|
+
"scrollbar-custom",
|
|
59
|
+
hideScrollbar && "scrollbar-hide hover:scrollbar-show",
|
|
60
|
+
className
|
|
61
|
+
)}
|
|
62
|
+
style={scrollbarCSS}
|
|
63
|
+
{...props}
|
|
64
|
+
>
|
|
65
|
+
<style>{`
|
|
66
|
+
.scrollbar-custom::-webkit-scrollbar {
|
|
67
|
+
width: var(--scrollbar-width, 4px);
|
|
68
|
+
height: var(--scrollbar-height, 4px);
|
|
69
|
+
}
|
|
70
|
+
.scrollbar-custom::-webkit-scrollbar-track {
|
|
71
|
+
background: transparent;
|
|
72
|
+
border-radius: 9999px;
|
|
73
|
+
}
|
|
74
|
+
.scrollbar-custom::-webkit-scrollbar-thumb {
|
|
75
|
+
background: hsl(var(--muted-foreground) / 0.3);
|
|
76
|
+
border-radius: 9999px;
|
|
77
|
+
}
|
|
78
|
+
.scrollbar-custom::-webkit-scrollbar-thumb:hover {
|
|
79
|
+
background: hsl(var(--muted-foreground) / 0.5);
|
|
80
|
+
}
|
|
81
|
+
.scrollbar-custom {
|
|
82
|
+
scrollbar-width: thin;
|
|
83
|
+
scrollbar-color: hsl(var(--muted-foreground) / 0.3) transparent;
|
|
84
|
+
}
|
|
85
|
+
.scrollbar-hide::-webkit-scrollbar {
|
|
86
|
+
opacity: 0;
|
|
87
|
+
}
|
|
88
|
+
.scrollbar-hide:hover::-webkit-scrollbar,
|
|
89
|
+
.scrollbar-show::-webkit-scrollbar {
|
|
90
|
+
opacity: 1;
|
|
91
|
+
}
|
|
92
|
+
`}</style>
|
|
93
|
+
{children}
|
|
94
|
+
</div>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
ScrollArea.displayName = "ScrollArea"
|
|
99
|
+
|
|
100
|
+
// ScrollBar component for explicit scrollbar styling reference (optional usage)
|
|
101
|
+
interface ScrollBarProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
102
|
+
orientation?: "vertical" | "horizontal"
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const ScrollBar = React.forwardRef<HTMLDivElement, ScrollBarProps>(
|
|
106
|
+
({ className, orientation = "vertical", ...props }, ref) => (
|
|
107
|
+
<div
|
|
108
|
+
ref={ref}
|
|
109
|
+
className={cn(
|
|
110
|
+
"flex touch-none select-none transition-colors",
|
|
111
|
+
orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent p-[1px]",
|
|
112
|
+
orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
|
113
|
+
className
|
|
114
|
+
)}
|
|
115
|
+
{...props}
|
|
116
|
+
/>
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
ScrollBar.displayName = "ScrollBar"
|
|
120
|
+
|
|
121
|
+
export { ScrollArea, ScrollBar }
|
|
File without changes
|
|
@@ -1,150 +1,162 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import * as React from "react"
|
|
4
|
-
import { cn } from "@/lib/utils"
|
|
5
|
-
|
|
6
|
-
interface SearchProps extends Omit<
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
|
|
6
|
+
interface SearchProps extends Omit<
|
|
7
|
+
React.InputHTMLAttributes<HTMLInputElement>,
|
|
8
|
+
"onChange"
|
|
9
|
+
> {
|
|
10
|
+
/** Callback when search value changes */
|
|
11
|
+
onSearch?: (value: string) => void;
|
|
12
|
+
/** Debounce delay in ms */
|
|
13
|
+
debounceMs?: number;
|
|
14
|
+
/** Show clear button */
|
|
15
|
+
showClear?: boolean;
|
|
16
|
+
/** Loading state */
|
|
17
|
+
loading?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Search input with optional debounce and clear button
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* <Search
|
|
25
|
+
* placeholder="Search..."
|
|
26
|
+
* onSearch={(value) => fetchResults(value)}
|
|
27
|
+
* debounceMs={300}
|
|
28
|
+
* />
|
|
29
|
+
*/
|
|
30
|
+
const Search = React.forwardRef<HTMLInputElement, SearchProps>(
|
|
31
|
+
(
|
|
32
|
+
{
|
|
33
|
+
className,
|
|
34
|
+
onSearch,
|
|
35
|
+
debounceMs = 0,
|
|
36
|
+
showClear = true,
|
|
37
|
+
loading,
|
|
38
|
+
defaultValue = "",
|
|
39
|
+
...props
|
|
40
|
+
},
|
|
41
|
+
ref,
|
|
42
|
+
) => {
|
|
43
|
+
const [value, setValue] = React.useState(String(defaultValue));
|
|
44
|
+
const debounceRef = React.useRef<NodeJS.Timeout | null>(null);
|
|
45
|
+
|
|
46
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
47
|
+
const newValue = e.target.value;
|
|
48
|
+
setValue(newValue);
|
|
49
|
+
|
|
50
|
+
if (debounceMs > 0) {
|
|
51
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
52
|
+
debounceRef.current = setTimeout(() => {
|
|
53
|
+
onSearch?.(newValue);
|
|
54
|
+
}, debounceMs);
|
|
55
|
+
} else {
|
|
56
|
+
onSearch?.(newValue);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const handleClear = () => {
|
|
61
|
+
setValue("");
|
|
62
|
+
onSearch?.("");
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
66
|
+
if (e.key === "Escape") {
|
|
67
|
+
handleClear();
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
React.useEffect(() => {
|
|
72
|
+
return () => {
|
|
73
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
74
|
+
};
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div className={cn("relative", className)}>
|
|
79
|
+
{/* Search Icon */}
|
|
80
|
+
<svg
|
|
81
|
+
className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground"
|
|
82
|
+
fill="none"
|
|
83
|
+
viewBox="0 0 24 24"
|
|
84
|
+
stroke="currentColor"
|
|
85
|
+
strokeWidth={2}
|
|
86
|
+
>
|
|
87
|
+
<path
|
|
88
|
+
strokeLinecap="round"
|
|
89
|
+
strokeLinejoin="round"
|
|
90
|
+
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
91
|
+
/>
|
|
92
|
+
</svg>
|
|
93
|
+
|
|
94
|
+
<input
|
|
95
|
+
ref={ref}
|
|
96
|
+
type="search"
|
|
97
|
+
role="searchbox"
|
|
98
|
+
value={value}
|
|
99
|
+
onChange={handleChange}
|
|
100
|
+
onKeyDown={handleKeyDown}
|
|
101
|
+
className={cn(
|
|
102
|
+
"flex h-10 w-full rounded-md border border-input bg-transparent pl-10 pr-10 py-2 text-sm shadow-sm transition-colors",
|
|
103
|
+
"placeholder:text-muted-foreground",
|
|
104
|
+
"focus:outline-none focus:ring-1 focus:ring-ring",
|
|
105
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
106
|
+
"[&::-webkit-search-cancel-button]:appearance-none",
|
|
107
|
+
)}
|
|
108
|
+
{...props}
|
|
109
|
+
/>
|
|
110
|
+
|
|
111
|
+
{/* Loading or Clear */}
|
|
112
|
+
<div className="absolute right-3 top-1/2 -translate-y-1/2">
|
|
113
|
+
{loading ? (
|
|
114
|
+
<svg
|
|
115
|
+
className="h-4 w-4 animate-spin text-muted-foreground"
|
|
116
|
+
fill="none"
|
|
117
|
+
viewBox="0 0 24 24"
|
|
118
|
+
>
|
|
119
|
+
<circle
|
|
120
|
+
className="opacity-25"
|
|
121
|
+
cx="12"
|
|
122
|
+
cy="12"
|
|
123
|
+
r="10"
|
|
124
|
+
stroke="currentColor"
|
|
125
|
+
strokeWidth="4"
|
|
126
|
+
/>
|
|
127
|
+
<path
|
|
128
|
+
className="opacity-75"
|
|
129
|
+
fill="currentColor"
|
|
130
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
131
|
+
/>
|
|
132
|
+
</svg>
|
|
133
|
+
) : showClear && value ? (
|
|
134
|
+
<button
|
|
135
|
+
type="button"
|
|
136
|
+
onClick={handleClear}
|
|
137
|
+
className="text-muted-foreground hover:text-foreground"
|
|
138
|
+
aria-label="Clear search"
|
|
139
|
+
>
|
|
140
|
+
<svg
|
|
141
|
+
className="h-4 w-4"
|
|
142
|
+
fill="none"
|
|
143
|
+
viewBox="0 0 24 24"
|
|
144
|
+
stroke="currentColor"
|
|
145
|
+
strokeWidth={2}
|
|
146
|
+
>
|
|
147
|
+
<path
|
|
148
|
+
strokeLinecap="round"
|
|
149
|
+
strokeLinejoin="round"
|
|
150
|
+
d="M6 18L18 6M6 6l12 12"
|
|
151
|
+
/>
|
|
152
|
+
</svg>
|
|
153
|
+
</button>
|
|
154
|
+
) : null}
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
Search.displayName = "Search";
|
|
161
|
+
|
|
162
|
+
export { Search };
|