@smicolon/fasla-ui 0.2.2
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 +141 -0
- package/bin/fasla-ui.mjs +54 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +241 -0
- package/dist/lib/utils.d.ts +9 -0
- package/dist/lib/utils.js +9 -0
- package/dist/motion/index.d.ts +33 -0
- package/dist/motion/index.js +59 -0
- package/dist/registry/index.d.ts +17 -0
- package/dist/registry/index.js +145 -0
- package/dist/tokens/index.d.ts +55 -0
- package/dist/tokens/index.js +34 -0
- package/package.json +97 -0
- package/registry/blocks/app-shell/AppShell.tsx +66 -0
- package/registry/blocks/app-shell/index.ts +1 -0
- package/registry/blocks/data-table/DataTable.tsx +292 -0
- package/registry/blocks/data-table/index.ts +7 -0
- package/registry/blocks/empty-state/EmptyState.test.tsx +120 -0
- package/registry/blocks/empty-state/EmptyState.tsx +164 -0
- package/registry/blocks/empty-state/index.ts +6 -0
- package/registry/blocks/form-section/FormSection.tsx +140 -0
- package/registry/blocks/form-section/index.ts +8 -0
- package/registry/blocks/navbar/Navbar.tsx +190 -0
- package/registry/blocks/navbar/index.ts +16 -0
- package/registry/blocks/page-header/PageHeader.tsx +64 -0
- package/registry/blocks/page-header/index.ts +1 -0
- package/registry/blocks/sidebar/Sidebar.tsx +202 -0
- package/registry/blocks/sidebar/index.ts +16 -0
- package/registry/blocks/stats-card/StatsCard.tsx +134 -0
- package/registry/blocks/stats-card/index.ts +6 -0
- package/registry/effects/animated-gradient/animated-gradient.tsx +136 -0
- package/registry/effects/animated-gradient/index.ts +6 -0
- package/registry/effects/border-beam/border-beam.tsx +132 -0
- package/registry/effects/border-beam/index.ts +6 -0
- package/registry/effects/glow-card/glow-card.tsx +145 -0
- package/registry/effects/glow-card/index.ts +6 -0
- package/registry/effects/shimmer-button/index.ts +1 -0
- package/registry/effects/shimmer-button/shimmer-button.tsx +91 -0
- package/registry/effects/spotlight/index.ts +6 -0
- package/registry/effects/spotlight/spotlight.tsx +162 -0
- package/registry/effects/text-reveal/index.ts +6 -0
- package/registry/effects/text-reveal/text-reveal.tsx +139 -0
- package/registry/effects/typewriter-text/index.ts +6 -0
- package/registry/effects/typewriter-text/typewriter-text.tsx +220 -0
- package/registry/ui/avatar/avatar.tsx +135 -0
- package/registry/ui/avatar/index.ts +1 -0
- package/registry/ui/badge/badge.test.tsx +51 -0
- package/registry/ui/badge/badge.tsx +39 -0
- package/registry/ui/badge/index.ts +1 -0
- package/registry/ui/button/button.test.tsx +87 -0
- package/registry/ui/button/button.tsx +88 -0
- package/registry/ui/button/index.ts +1 -0
- package/registry/ui/card/card.test.tsx +119 -0
- package/registry/ui/card/card.tsx +75 -0
- package/registry/ui/card/index.ts +8 -0
- package/registry/ui/checkbox/checkbox.tsx +113 -0
- package/registry/ui/checkbox/index.ts +1 -0
- package/registry/ui/combobox/combobox.tsx +410 -0
- package/registry/ui/combobox/index.ts +1 -0
- package/registry/ui/input/index.ts +1 -0
- package/registry/ui/input/input.test.tsx +78 -0
- package/registry/ui/input/input.tsx +76 -0
- package/registry/ui/select/index.ts +1 -0
- package/registry/ui/select/select.tsx +94 -0
- package/registry/ui/skeleton/index.ts +7 -0
- package/registry/ui/skeleton/skeleton.test.tsx +104 -0
- package/registry/ui/skeleton/skeleton.tsx +88 -0
- package/registry/ui/switch/index.ts +1 -0
- package/registry/ui/switch/switch.tsx +89 -0
- package/registry/ui/tabs/index.ts +10 -0
- package/registry/ui/tabs/tabs.tsx +150 -0
- package/registry/ui/textarea/index.ts +1 -0
- package/registry/ui/textarea/textarea.tsx +73 -0
- package/registry.json +247 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "../../../src/lib/utils"
|
|
3
|
+
|
|
4
|
+
export interface PageHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
/** Page title */
|
|
6
|
+
title: string
|
|
7
|
+
/** Optional description below the title */
|
|
8
|
+
description?: string
|
|
9
|
+
/** Action buttons/controls on the right side */
|
|
10
|
+
actions?: React.ReactNode
|
|
11
|
+
/** Breadcrumb navigation above the title */
|
|
12
|
+
breadcrumb?: React.ReactNode
|
|
13
|
+
/** Whether to show the bottom border */
|
|
14
|
+
bordered?: boolean
|
|
15
|
+
/** Semantic heading level for the title */
|
|
16
|
+
headingLevel?: 1 | 2 | 3
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Page header with title, breadcrumb, description, and actions.
|
|
21
|
+
* Use at the top of page content to provide context and primary actions.
|
|
22
|
+
*/
|
|
23
|
+
export function PageHeader({
|
|
24
|
+
title,
|
|
25
|
+
description,
|
|
26
|
+
actions,
|
|
27
|
+
breadcrumb,
|
|
28
|
+
bordered = true,
|
|
29
|
+
headingLevel = 1,
|
|
30
|
+
className,
|
|
31
|
+
...props
|
|
32
|
+
}: PageHeaderProps) {
|
|
33
|
+
const Heading = `h${headingLevel}` as const
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div
|
|
37
|
+
className={cn(
|
|
38
|
+
"flex flex-col gap-3 bg-background px-4 py-4 md:px-6",
|
|
39
|
+
bordered && "border-b",
|
|
40
|
+
className
|
|
41
|
+
)}
|
|
42
|
+
{...props}
|
|
43
|
+
>
|
|
44
|
+
<div className="flex items-start justify-between gap-4">
|
|
45
|
+
<div className="min-w-0 flex-1">
|
|
46
|
+
{breadcrumb && (
|
|
47
|
+
<nav className="mb-2 text-sm text-muted-foreground" aria-label="Breadcrumb">
|
|
48
|
+
{breadcrumb}
|
|
49
|
+
</nav>
|
|
50
|
+
)}
|
|
51
|
+
<Heading className="truncate text-xl font-semibold tracking-tight md:text-2xl">
|
|
52
|
+
{title}
|
|
53
|
+
</Heading>
|
|
54
|
+
{description && (
|
|
55
|
+
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
{actions && (
|
|
59
|
+
<div className="flex shrink-0 items-center gap-2">{actions}</div>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { PageHeader, type PageHeaderProps } from "./PageHeader"
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "../../../src/lib/utils"
|
|
5
|
+
|
|
6
|
+
export interface SidebarProps extends React.HTMLAttributes<HTMLElement> {
|
|
7
|
+
/** Whether the sidebar is collapsed */
|
|
8
|
+
collapsed?: boolean
|
|
9
|
+
/** Called when collapse state changes */
|
|
10
|
+
onCollapsedChange?: (collapsed: boolean) => void
|
|
11
|
+
/** Sidebar width when expanded */
|
|
12
|
+
width?: "sm" | "default" | "lg"
|
|
13
|
+
children: React.ReactNode
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const widthClasses = {
|
|
17
|
+
sm: "w-56",
|
|
18
|
+
default: "w-64",
|
|
19
|
+
lg: "w-72",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function Sidebar({
|
|
23
|
+
collapsed = false,
|
|
24
|
+
onCollapsedChange,
|
|
25
|
+
width = "default",
|
|
26
|
+
className,
|
|
27
|
+
children,
|
|
28
|
+
...props
|
|
29
|
+
}: SidebarProps) {
|
|
30
|
+
return (
|
|
31
|
+
<aside
|
|
32
|
+
className={cn(
|
|
33
|
+
"flex h-full flex-col border-r bg-background transition-all duration-200",
|
|
34
|
+
collapsed ? "w-16" : widthClasses[width],
|
|
35
|
+
className
|
|
36
|
+
)}
|
|
37
|
+
{...props}
|
|
38
|
+
>
|
|
39
|
+
{children}
|
|
40
|
+
</aside>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface SidebarHeaderProps
|
|
45
|
+
extends React.HTMLAttributes<HTMLDivElement> {
|
|
46
|
+
children: React.ReactNode
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function SidebarHeader({
|
|
50
|
+
className,
|
|
51
|
+
children,
|
|
52
|
+
...props
|
|
53
|
+
}: SidebarHeaderProps) {
|
|
54
|
+
return (
|
|
55
|
+
<div
|
|
56
|
+
className={cn("flex h-14 items-center border-b px-4", className)}
|
|
57
|
+
{...props}
|
|
58
|
+
>
|
|
59
|
+
{children}
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface SidebarContentProps
|
|
65
|
+
extends React.HTMLAttributes<HTMLDivElement> {
|
|
66
|
+
children: React.ReactNode
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function SidebarContent({
|
|
70
|
+
className,
|
|
71
|
+
children,
|
|
72
|
+
...props
|
|
73
|
+
}: SidebarContentProps) {
|
|
74
|
+
return (
|
|
75
|
+
<div
|
|
76
|
+
className={cn("flex-1 overflow-auto p-2", className)}
|
|
77
|
+
{...props}
|
|
78
|
+
>
|
|
79
|
+
{children}
|
|
80
|
+
</div>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface SidebarFooterProps
|
|
85
|
+
extends React.HTMLAttributes<HTMLDivElement> {
|
|
86
|
+
children: React.ReactNode
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function SidebarFooter({
|
|
90
|
+
className,
|
|
91
|
+
children,
|
|
92
|
+
...props
|
|
93
|
+
}: SidebarFooterProps) {
|
|
94
|
+
return (
|
|
95
|
+
<div
|
|
96
|
+
className={cn("border-t p-2", className)}
|
|
97
|
+
{...props}
|
|
98
|
+
>
|
|
99
|
+
{children}
|
|
100
|
+
</div>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface SidebarGroupProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
105
|
+
/** Group label */
|
|
106
|
+
label?: string
|
|
107
|
+
children: React.ReactNode
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function SidebarGroup({
|
|
111
|
+
label,
|
|
112
|
+
className,
|
|
113
|
+
children,
|
|
114
|
+
...props
|
|
115
|
+
}: SidebarGroupProps) {
|
|
116
|
+
return (
|
|
117
|
+
<div className={cn("py-2", className)} {...props}>
|
|
118
|
+
{label && (
|
|
119
|
+
<div className="mb-2 px-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
|
120
|
+
{label}
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
<div className="space-y-1">{children}</div>
|
|
124
|
+
</div>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface SidebarItemProps
|
|
129
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
130
|
+
/** Icon element */
|
|
131
|
+
icon?: React.ReactNode
|
|
132
|
+
/** Whether item is active */
|
|
133
|
+
active?: boolean
|
|
134
|
+
/** Whether sidebar is collapsed (hides label) */
|
|
135
|
+
collapsed?: boolean
|
|
136
|
+
children: React.ReactNode
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function SidebarItem({
|
|
140
|
+
icon,
|
|
141
|
+
active,
|
|
142
|
+
collapsed,
|
|
143
|
+
className,
|
|
144
|
+
children,
|
|
145
|
+
...props
|
|
146
|
+
}: SidebarItemProps) {
|
|
147
|
+
return (
|
|
148
|
+
<button
|
|
149
|
+
className={cn(
|
|
150
|
+
"flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors",
|
|
151
|
+
"hover:bg-accent hover:text-accent-foreground",
|
|
152
|
+
active && "bg-accent text-accent-foreground",
|
|
153
|
+
collapsed && "justify-center px-2",
|
|
154
|
+
className
|
|
155
|
+
)}
|
|
156
|
+
{...props}
|
|
157
|
+
>
|
|
158
|
+
{icon && <span className="shrink-0">{icon}</span>}
|
|
159
|
+
{!collapsed && <span className="truncate">{children}</span>}
|
|
160
|
+
</button>
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface SidebarCollapseButtonProps
|
|
165
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
166
|
+
collapsed: boolean
|
|
167
|
+
onCollapsedChange: (collapsed: boolean) => void
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function SidebarCollapseButton({
|
|
171
|
+
collapsed,
|
|
172
|
+
onCollapsedChange,
|
|
173
|
+
className,
|
|
174
|
+
...props
|
|
175
|
+
}: SidebarCollapseButtonProps) {
|
|
176
|
+
return (
|
|
177
|
+
<button
|
|
178
|
+
onClick={() => onCollapsedChange(!collapsed)}
|
|
179
|
+
className={cn(
|
|
180
|
+
"flex h-8 w-8 items-center justify-center rounded-md hover:bg-accent",
|
|
181
|
+
className
|
|
182
|
+
)}
|
|
183
|
+
aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
|
|
184
|
+
{...props}
|
|
185
|
+
>
|
|
186
|
+
<svg
|
|
187
|
+
width="16"
|
|
188
|
+
height="16"
|
|
189
|
+
viewBox="0 0 24 24"
|
|
190
|
+
fill="none"
|
|
191
|
+
stroke="currentColor"
|
|
192
|
+
strokeWidth="2"
|
|
193
|
+
strokeLinecap="round"
|
|
194
|
+
strokeLinejoin="round"
|
|
195
|
+
className={cn("transition-transform", collapsed && "rotate-180")}
|
|
196
|
+
>
|
|
197
|
+
<polyline points="11 17 6 12 11 7" />
|
|
198
|
+
<polyline points="18 17 13 12 18 7" />
|
|
199
|
+
</svg>
|
|
200
|
+
</button>
|
|
201
|
+
)
|
|
202
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export {
|
|
2
|
+
Sidebar,
|
|
3
|
+
SidebarHeader,
|
|
4
|
+
SidebarContent,
|
|
5
|
+
SidebarFooter,
|
|
6
|
+
SidebarGroup,
|
|
7
|
+
SidebarItem,
|
|
8
|
+
SidebarCollapseButton,
|
|
9
|
+
type SidebarProps,
|
|
10
|
+
type SidebarHeaderProps,
|
|
11
|
+
type SidebarContentProps,
|
|
12
|
+
type SidebarFooterProps,
|
|
13
|
+
type SidebarGroupProps,
|
|
14
|
+
type SidebarItemProps,
|
|
15
|
+
type SidebarCollapseButtonProps,
|
|
16
|
+
} from "./Sidebar"
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "../../../src/lib/utils"
|
|
3
|
+
|
|
4
|
+
export interface StatsCardProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
/** The main title/label */
|
|
6
|
+
title: string
|
|
7
|
+
/** The primary value to display */
|
|
8
|
+
value: string | number
|
|
9
|
+
/** Optional description or subtitle */
|
|
10
|
+
description?: string
|
|
11
|
+
/** Optional icon */
|
|
12
|
+
icon?: React.ReactNode
|
|
13
|
+
/** Trend direction and value */
|
|
14
|
+
trend?: {
|
|
15
|
+
value: number
|
|
16
|
+
direction: "up" | "down" | "neutral"
|
|
17
|
+
}
|
|
18
|
+
/** Whether the card is loading */
|
|
19
|
+
loading?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function StatsCard({
|
|
23
|
+
title,
|
|
24
|
+
value,
|
|
25
|
+
description,
|
|
26
|
+
icon,
|
|
27
|
+
trend,
|
|
28
|
+
loading = false,
|
|
29
|
+
className,
|
|
30
|
+
...props
|
|
31
|
+
}: StatsCardProps) {
|
|
32
|
+
return (
|
|
33
|
+
<div
|
|
34
|
+
className={cn(
|
|
35
|
+
"rounded-xl border bg-card p-6 shadow-sm",
|
|
36
|
+
className
|
|
37
|
+
)}
|
|
38
|
+
{...props}
|
|
39
|
+
>
|
|
40
|
+
<div className="flex items-start justify-between">
|
|
41
|
+
<div className="space-y-1">
|
|
42
|
+
<p className="text-sm font-medium text-muted-foreground">{title}</p>
|
|
43
|
+
{loading ? (
|
|
44
|
+
<div className="h-8 w-24 animate-pulse rounded bg-muted" />
|
|
45
|
+
) : (
|
|
46
|
+
<p className="text-2xl font-bold tracking-tight">{value}</p>
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
{icon && (
|
|
50
|
+
<div className="rounded-md bg-primary/10 p-2 text-primary">
|
|
51
|
+
{icon}
|
|
52
|
+
</div>
|
|
53
|
+
)}
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
{(description || trend) && (
|
|
57
|
+
<div className="mt-4 flex items-center gap-2 text-sm">
|
|
58
|
+
{trend && (
|
|
59
|
+
<span
|
|
60
|
+
className={cn(
|
|
61
|
+
"inline-flex items-center gap-1 font-medium",
|
|
62
|
+
trend.direction === "up" && "text-green-600 dark:text-green-400",
|
|
63
|
+
trend.direction === "down" && "text-red-600 dark:text-red-400",
|
|
64
|
+
trend.direction === "neutral" && "text-muted-foreground"
|
|
65
|
+
)}
|
|
66
|
+
>
|
|
67
|
+
{trend.direction === "up" && (
|
|
68
|
+
<svg
|
|
69
|
+
width="16"
|
|
70
|
+
height="16"
|
|
71
|
+
viewBox="0 0 24 24"
|
|
72
|
+
fill="none"
|
|
73
|
+
stroke="currentColor"
|
|
74
|
+
strokeWidth="2"
|
|
75
|
+
strokeLinecap="round"
|
|
76
|
+
strokeLinejoin="round"
|
|
77
|
+
>
|
|
78
|
+
<polyline points="18 15 12 9 6 15" />
|
|
79
|
+
</svg>
|
|
80
|
+
)}
|
|
81
|
+
{trend.direction === "down" && (
|
|
82
|
+
<svg
|
|
83
|
+
width="16"
|
|
84
|
+
height="16"
|
|
85
|
+
viewBox="0 0 24 24"
|
|
86
|
+
fill="none"
|
|
87
|
+
stroke="currentColor"
|
|
88
|
+
strokeWidth="2"
|
|
89
|
+
strokeLinecap="round"
|
|
90
|
+
strokeLinejoin="round"
|
|
91
|
+
>
|
|
92
|
+
<polyline points="6 9 12 15 18 9" />
|
|
93
|
+
</svg>
|
|
94
|
+
)}
|
|
95
|
+
{trend.value > 0 ? "+" : ""}
|
|
96
|
+
{trend.value}%
|
|
97
|
+
</span>
|
|
98
|
+
)}
|
|
99
|
+
{description && (
|
|
100
|
+
<span className="text-muted-foreground">{description}</span>
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface StatsGridProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
109
|
+
/** Number of columns */
|
|
110
|
+
columns?: 2 | 3 | 4
|
|
111
|
+
children: React.ReactNode
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function StatsGrid({
|
|
115
|
+
columns = 4,
|
|
116
|
+
className,
|
|
117
|
+
children,
|
|
118
|
+
...props
|
|
119
|
+
}: StatsGridProps) {
|
|
120
|
+
return (
|
|
121
|
+
<div
|
|
122
|
+
className={cn(
|
|
123
|
+
"grid gap-4",
|
|
124
|
+
columns === 2 && "grid-cols-1 sm:grid-cols-2",
|
|
125
|
+
columns === 3 && "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3",
|
|
126
|
+
columns === 4 && "grid-cols-1 sm:grid-cols-2 lg:grid-cols-4",
|
|
127
|
+
className
|
|
128
|
+
)}
|
|
129
|
+
{...props}
|
|
130
|
+
>
|
|
131
|
+
{children}
|
|
132
|
+
</div>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { motion, useReducedMotion } from "framer-motion"
|
|
5
|
+
import { cn } from "../../../src/lib/utils"
|
|
6
|
+
|
|
7
|
+
export interface AnimatedGradientProps
|
|
8
|
+
extends React.HTMLAttributes<HTMLDivElement> {
|
|
9
|
+
/** Gradient colors */
|
|
10
|
+
colors?: string[]
|
|
11
|
+
/** Animation speed in seconds */
|
|
12
|
+
speed?: number
|
|
13
|
+
/** Blur amount */
|
|
14
|
+
blur?: "sm" | "md" | "lg" | "xl" | "2xl" | "3xl"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const blurValues = {
|
|
18
|
+
sm: "blur-sm",
|
|
19
|
+
md: "blur-md",
|
|
20
|
+
lg: "blur-lg",
|
|
21
|
+
xl: "blur-xl",
|
|
22
|
+
"2xl": "blur-2xl",
|
|
23
|
+
"3xl": "blur-3xl",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Animated gradient background with moving color blobs.
|
|
28
|
+
* Respects prefers-reduced-motion by stopping animation.
|
|
29
|
+
*/
|
|
30
|
+
export function AnimatedGradient({
|
|
31
|
+
colors = [
|
|
32
|
+
"hsl(var(--primary))",
|
|
33
|
+
"hsl(var(--secondary))",
|
|
34
|
+
"hsl(280 100% 70%)",
|
|
35
|
+
"hsl(200 100% 70%)",
|
|
36
|
+
],
|
|
37
|
+
speed = 10,
|
|
38
|
+
blur = "3xl",
|
|
39
|
+
className,
|
|
40
|
+
children,
|
|
41
|
+
...props
|
|
42
|
+
}: AnimatedGradientProps) {
|
|
43
|
+
const prefersReducedMotion = useReducedMotion()
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div
|
|
47
|
+
className={cn("relative overflow-hidden", className)}
|
|
48
|
+
{...props}
|
|
49
|
+
>
|
|
50
|
+
{/* Gradient blobs */}
|
|
51
|
+
<div className="absolute inset-0 -z-10">
|
|
52
|
+
{colors.map((color, i) => (
|
|
53
|
+
<motion.div
|
|
54
|
+
key={i}
|
|
55
|
+
className={cn(
|
|
56
|
+
"absolute h-[50%] w-[50%] rounded-full opacity-60",
|
|
57
|
+
blurValues[blur]
|
|
58
|
+
)}
|
|
59
|
+
style={{
|
|
60
|
+
background: color,
|
|
61
|
+
left: `${(i % 2) * 50}%`,
|
|
62
|
+
top: `${Math.floor(i / 2) * 50}%`,
|
|
63
|
+
}}
|
|
64
|
+
animate={
|
|
65
|
+
prefersReducedMotion
|
|
66
|
+
? {}
|
|
67
|
+
: {
|
|
68
|
+
x: [0, 30, -20, 0],
|
|
69
|
+
y: [0, -30, 20, 0],
|
|
70
|
+
scale: [1, 1.1, 0.9, 1],
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
transition={{
|
|
74
|
+
duration: speed + i * 2,
|
|
75
|
+
repeat: Infinity,
|
|
76
|
+
ease: "easeInOut",
|
|
77
|
+
}}
|
|
78
|
+
/>
|
|
79
|
+
))}
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
{/* Content */}
|
|
83
|
+
{children}
|
|
84
|
+
</div>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Animated gradient text effect.
|
|
90
|
+
*/
|
|
91
|
+
export interface AnimatedGradientTextProps {
|
|
92
|
+
/** Gradient colors for text */
|
|
93
|
+
colors?: string[]
|
|
94
|
+
/** Animation speed in seconds */
|
|
95
|
+
speed?: number
|
|
96
|
+
/** Additional class names */
|
|
97
|
+
className?: string
|
|
98
|
+
/** Text content */
|
|
99
|
+
children: React.ReactNode
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function AnimatedGradientText({
|
|
103
|
+
colors = ["hsl(var(--primary))", "hsl(280 100% 70%)", "hsl(var(--primary))"],
|
|
104
|
+
speed = 3,
|
|
105
|
+
className,
|
|
106
|
+
children,
|
|
107
|
+
}: AnimatedGradientTextProps) {
|
|
108
|
+
const prefersReducedMotion = useReducedMotion()
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<motion.span
|
|
112
|
+
className={cn(
|
|
113
|
+
"inline-block bg-clip-text text-transparent",
|
|
114
|
+
className
|
|
115
|
+
)}
|
|
116
|
+
style={{
|
|
117
|
+
backgroundImage: `linear-gradient(90deg, ${colors.join(", ")})`,
|
|
118
|
+
backgroundSize: "200% auto",
|
|
119
|
+
}}
|
|
120
|
+
animate={
|
|
121
|
+
prefersReducedMotion
|
|
122
|
+
? {}
|
|
123
|
+
: {
|
|
124
|
+
backgroundPosition: ["0% center", "200% center"],
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
transition={{
|
|
128
|
+
duration: speed,
|
|
129
|
+
repeat: Infinity,
|
|
130
|
+
ease: "linear",
|
|
131
|
+
}}
|
|
132
|
+
>
|
|
133
|
+
{children}
|
|
134
|
+
</motion.span>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { motion, useReducedMotion } from "framer-motion"
|
|
5
|
+
import { cn } from "../../../src/lib/utils"
|
|
6
|
+
|
|
7
|
+
export interface BorderBeamProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
8
|
+
/** Duration of the animation in seconds */
|
|
9
|
+
duration?: number
|
|
10
|
+
/** Border width */
|
|
11
|
+
borderWidth?: number
|
|
12
|
+
/** Color of the beam */
|
|
13
|
+
colorFrom?: string
|
|
14
|
+
/** End color of the beam */
|
|
15
|
+
colorTo?: string
|
|
16
|
+
/** Delay before animation starts */
|
|
17
|
+
delay?: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Animated border beam effect that travels around the element.
|
|
22
|
+
* Respects prefers-reduced-motion.
|
|
23
|
+
*/
|
|
24
|
+
export function BorderBeam({
|
|
25
|
+
duration = 4,
|
|
26
|
+
borderWidth = 2,
|
|
27
|
+
colorFrom = "hsl(var(--primary))",
|
|
28
|
+
colorTo = "transparent",
|
|
29
|
+
delay = 0,
|
|
30
|
+
className,
|
|
31
|
+
children,
|
|
32
|
+
...props
|
|
33
|
+
}: BorderBeamProps) {
|
|
34
|
+
const prefersReducedMotion = useReducedMotion()
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
className={cn("relative overflow-hidden rounded-lg", className)}
|
|
39
|
+
{...props}
|
|
40
|
+
>
|
|
41
|
+
{/* Border beam */}
|
|
42
|
+
{!prefersReducedMotion && (
|
|
43
|
+
<motion.div
|
|
44
|
+
className="pointer-events-none absolute inset-0 rounded-[inherit]"
|
|
45
|
+
style={{
|
|
46
|
+
padding: borderWidth,
|
|
47
|
+
background: `linear-gradient(90deg, ${colorFrom}, ${colorTo})`,
|
|
48
|
+
mask: "linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)",
|
|
49
|
+
maskComposite: "exclude",
|
|
50
|
+
WebkitMaskComposite: "xor",
|
|
51
|
+
}}
|
|
52
|
+
animate={{
|
|
53
|
+
rotate: [0, 360],
|
|
54
|
+
}}
|
|
55
|
+
transition={{
|
|
56
|
+
duration,
|
|
57
|
+
delay,
|
|
58
|
+
repeat: Infinity,
|
|
59
|
+
ease: "linear",
|
|
60
|
+
}}
|
|
61
|
+
/>
|
|
62
|
+
)}
|
|
63
|
+
|
|
64
|
+
{/* Static border fallback for reduced motion */}
|
|
65
|
+
{prefersReducedMotion && (
|
|
66
|
+
<div
|
|
67
|
+
className="pointer-events-none absolute inset-0 rounded-[inherit]"
|
|
68
|
+
style={{
|
|
69
|
+
padding: borderWidth,
|
|
70
|
+
background: colorFrom,
|
|
71
|
+
mask: "linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)",
|
|
72
|
+
maskComposite: "exclude",
|
|
73
|
+
WebkitMaskComposite: "xor",
|
|
74
|
+
opacity: 0.5,
|
|
75
|
+
}}
|
|
76
|
+
/>
|
|
77
|
+
)}
|
|
78
|
+
|
|
79
|
+
{/* Content */}
|
|
80
|
+
<div className="relative z-10 rounded-[inherit] bg-background">
|
|
81
|
+
{children}
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface GlowingBorderProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
88
|
+
/** Glow color */
|
|
89
|
+
glowColor?: string
|
|
90
|
+
/** Border radius */
|
|
91
|
+
borderRadius?: string
|
|
92
|
+
/** Glow intensity */
|
|
93
|
+
intensity?: "sm" | "md" | "lg"
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const intensityValues = {
|
|
97
|
+
sm: "0 0 10px 2px",
|
|
98
|
+
md: "0 0 20px 4px",
|
|
99
|
+
lg: "0 0 30px 6px",
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Static glowing border effect.
|
|
104
|
+
*/
|
|
105
|
+
export function GlowingBorder({
|
|
106
|
+
glowColor = "hsl(var(--primary) / 0.5)",
|
|
107
|
+
borderRadius = "0.5rem",
|
|
108
|
+
intensity = "md",
|
|
109
|
+
className,
|
|
110
|
+
children,
|
|
111
|
+
...props
|
|
112
|
+
}: GlowingBorderProps) {
|
|
113
|
+
return (
|
|
114
|
+
<div
|
|
115
|
+
className={cn("relative", className)}
|
|
116
|
+
style={{
|
|
117
|
+
borderRadius,
|
|
118
|
+
}}
|
|
119
|
+
{...props}
|
|
120
|
+
>
|
|
121
|
+
{/* Glow */}
|
|
122
|
+
<div
|
|
123
|
+
className="absolute inset-0 -z-10"
|
|
124
|
+
style={{
|
|
125
|
+
borderRadius,
|
|
126
|
+
boxShadow: `${intensityValues[intensity]} ${glowColor}`,
|
|
127
|
+
}}
|
|
128
|
+
/>
|
|
129
|
+
{children}
|
|
130
|
+
</div>
|
|
131
|
+
)
|
|
132
|
+
}
|