gradient-forge 1.0.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/.eslintrc.json +3 -0
- package/.github/FUNDING.yml +2 -0
- package/README.md +140 -0
- package/app/docs/page.tsx +417 -0
- package/app/gallery/page.tsx +398 -0
- package/app/globals.css +1155 -0
- package/app/layout.tsx +36 -0
- package/app/page.tsx +600 -0
- package/app/showcase/page.tsx +730 -0
- package/app/studio/page.tsx +1310 -0
- package/cli/index.mjs +1141 -0
- package/cli/templates/theme-context.tsx +120 -0
- package/cli/templates/theme-engine.ts +237 -0
- package/cli/templates/themes.css +512 -0
- package/components/site/component-showcase.tsx +623 -0
- package/components/site/site-data.ts +103 -0
- package/components/site/site-header.tsx +270 -0
- package/components/templates/blog.tsx +198 -0
- package/components/templates/components-showcase.tsx +298 -0
- package/components/templates/dashboard.tsx +246 -0
- package/components/templates/ecommerce.tsx +199 -0
- package/components/templates/mail.tsx +275 -0
- package/components/templates/saas-landing.tsx +169 -0
- package/components/theme/studio-code-panel.tsx +485 -0
- package/components/theme/theme-context.tsx +120 -0
- package/components/theme/theme-engine.ts +237 -0
- package/components/theme/theme-exporter.tsx +369 -0
- package/components/theme/theme-panel.tsx +268 -0
- package/components/theme/token-export-utils.ts +1211 -0
- package/components/ui/animated.tsx +55 -0
- package/components/ui/avatar.tsx +38 -0
- package/components/ui/badge.tsx +32 -0
- package/components/ui/button.tsx +65 -0
- package/components/ui/card.tsx +56 -0
- package/components/ui/checkbox.tsx +19 -0
- package/components/ui/command-palette.tsx +245 -0
- package/components/ui/gsap-animated.tsx +436 -0
- package/components/ui/input.tsx +17 -0
- package/components/ui/select.tsx +176 -0
- package/components/ui/skeleton.tsx +102 -0
- package/components/ui/switch.tsx +43 -0
- package/components/ui/tabs.tsx +115 -0
- package/components/ui/toast.tsx +119 -0
- package/gradient-forge/theme-context.tsx +119 -0
- package/gradient-forge/theme-engine.ts +236 -0
- package/gradient-forge/themes.css +556 -0
- package/lib/animations.ts +50 -0
- package/lib/gsap.ts +426 -0
- package/lib/utils.ts +6 -0
- package/next-env.d.ts +6 -0
- package/next.config.mjs +6 -0
- package/package.json +53 -0
- package/postcss.config.mjs +5 -0
- package/tailwind.config.ts +15 -0
- package/tsconfig.json +43 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { motion } from "framer-motion";
|
|
5
|
+
import { fadeInUp, staggerContainer, staggerItem } from "@/lib/animations";
|
|
6
|
+
|
|
7
|
+
interface AnimatedSectionProps {
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function AnimatedSection({ children, className = "" }: AnimatedSectionProps) {
|
|
13
|
+
return (
|
|
14
|
+
<motion.section
|
|
15
|
+
initial="hidden"
|
|
16
|
+
whileInView="visible"
|
|
17
|
+
viewport={{ once: true, margin: "-100px" }}
|
|
18
|
+
variants={fadeInUp}
|
|
19
|
+
className={className}
|
|
20
|
+
>
|
|
21
|
+
{children}
|
|
22
|
+
</motion.section>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface StaggerContainerProps {
|
|
27
|
+
children: React.ReactNode;
|
|
28
|
+
className?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function StaggerContainer({ children, className = "" }: StaggerContainerProps) {
|
|
32
|
+
return (
|
|
33
|
+
<motion.div
|
|
34
|
+
initial="hidden"
|
|
35
|
+
animate="visible"
|
|
36
|
+
variants={staggerContainer}
|
|
37
|
+
className={className}
|
|
38
|
+
>
|
|
39
|
+
{children}
|
|
40
|
+
</motion.div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface StaggerItemProps {
|
|
45
|
+
children: React.ReactNode;
|
|
46
|
+
className?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function StaggerItem({ children, className = "" }: StaggerItemProps) {
|
|
50
|
+
return (
|
|
51
|
+
<motion.div variants={staggerItem} className={className}>
|
|
52
|
+
{children}
|
|
53
|
+
</motion.div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
|
|
4
|
+
interface AvatarProps {
|
|
5
|
+
className?: string;
|
|
6
|
+
children?: React.ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function Avatar({ className, children }: AvatarProps) {
|
|
10
|
+
return (
|
|
11
|
+
<div
|
|
12
|
+
className={cn(
|
|
13
|
+
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
|
14
|
+
className
|
|
15
|
+
)}
|
|
16
|
+
>
|
|
17
|
+
{children}
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface AvatarFallbackProps {
|
|
23
|
+
className?: string;
|
|
24
|
+
children?: React.ReactNode;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function AvatarFallback({ className, children }: AvatarFallbackProps) {
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
className={cn(
|
|
31
|
+
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
|
32
|
+
className
|
|
33
|
+
)}
|
|
34
|
+
>
|
|
35
|
+
{children}
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
|
|
4
|
+
type BadgeVariant = "default" | "outline" | "glass";
|
|
5
|
+
|
|
6
|
+
type BadgeProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
7
|
+
variant?: BadgeVariant;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const variantClasses: Record<BadgeVariant, string> = {
|
|
11
|
+
default: "bg-primary/15 text-primary border border-primary/30",
|
|
12
|
+
outline: "border border-border/60 text-foreground",
|
|
13
|
+
glass:
|
|
14
|
+
"bg-background/30 border border-border/30 backdrop-blur text-foreground",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function Badge({
|
|
18
|
+
className,
|
|
19
|
+
variant = "default",
|
|
20
|
+
...props
|
|
21
|
+
}: BadgeProps) {
|
|
22
|
+
return (
|
|
23
|
+
<div
|
|
24
|
+
className={cn(
|
|
25
|
+
"inline-flex items-center gap-2 rounded-full px-3 py-1 text-xs font-semibold uppercase tracking-[0.2em]",
|
|
26
|
+
variantClasses[variant],
|
|
27
|
+
className,
|
|
28
|
+
)}
|
|
29
|
+
{...props}
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
|
|
4
|
+
type ButtonVariant =
|
|
5
|
+
| "default"
|
|
6
|
+
| "secondary"
|
|
7
|
+
| "ghost"
|
|
8
|
+
| "outline"
|
|
9
|
+
| "glow";
|
|
10
|
+
|
|
11
|
+
type ButtonSize = "sm" | "md" | "lg";
|
|
12
|
+
|
|
13
|
+
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
14
|
+
variant?: ButtonVariant;
|
|
15
|
+
size?: ButtonSize;
|
|
16
|
+
asChild?: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const variantClasses: Record<ButtonVariant, string> = {
|
|
20
|
+
default:
|
|
21
|
+
"bg-primary text-primary-foreground shadow-sm hover:shadow-md hover:translate-y-[-1px]",
|
|
22
|
+
secondary:
|
|
23
|
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
24
|
+
ghost: "bg-transparent hover:bg-muted/70",
|
|
25
|
+
outline:
|
|
26
|
+
"border border-border/70 bg-transparent hover:bg-muted/70 text-foreground",
|
|
27
|
+
glow:
|
|
28
|
+
"bg-primary text-primary-foreground shadow-[0_0_25px_hsl(var(--primary)_/_0.35)] hover:shadow-[0_0_35px_hsl(var(--primary)_/_0.55)]",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const sizeClasses: Record<ButtonSize, string> = {
|
|
32
|
+
sm: "h-8 px-3 text-xs",
|
|
33
|
+
md: "h-10 px-4 text-sm",
|
|
34
|
+
lg: "h-12 px-5 text-base",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function Button({
|
|
38
|
+
className,
|
|
39
|
+
variant = "default",
|
|
40
|
+
size = "md",
|
|
41
|
+
asChild = false,
|
|
42
|
+
children,
|
|
43
|
+
...props
|
|
44
|
+
}: ButtonProps) {
|
|
45
|
+
const classes = cn(
|
|
46
|
+
"inline-flex items-center justify-center gap-2 rounded-full font-semibold transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50",
|
|
47
|
+
variantClasses[variant],
|
|
48
|
+
sizeClasses[size],
|
|
49
|
+
className,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
if (asChild && React.isValidElement(children)) {
|
|
53
|
+
const child = children as React.ReactElement<{ className?: string }>;
|
|
54
|
+
return React.cloneElement(child, {
|
|
55
|
+
className: cn(classes, child.props.className),
|
|
56
|
+
...(props as Record<string, unknown>),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<button className={classes} {...props}>
|
|
62
|
+
{children}
|
|
63
|
+
</button>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
|
|
4
|
+
export function Card({
|
|
5
|
+
className,
|
|
6
|
+
...props
|
|
7
|
+
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
8
|
+
return (
|
|
9
|
+
<div
|
|
10
|
+
className={cn(
|
|
11
|
+
"rounded-3xl border border-border/40 bg-card/70 text-card-foreground shadow-[0_20px_80px_-60px_rgba(0,0,0,0.5)]",
|
|
12
|
+
className,
|
|
13
|
+
)}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function CardHeader({
|
|
20
|
+
className,
|
|
21
|
+
...props
|
|
22
|
+
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
23
|
+
return (
|
|
24
|
+
<div className={cn("p-6 pb-3", className)} {...props} />
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function CardTitle({
|
|
29
|
+
className,
|
|
30
|
+
...props
|
|
31
|
+
}: React.HTMLAttributes<HTMLHeadingElement>) {
|
|
32
|
+
return (
|
|
33
|
+
<h3
|
|
34
|
+
className={cn("text-lg font-semibold tracking-tight", className)}
|
|
35
|
+
{...props}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function CardDescription({
|
|
41
|
+
className,
|
|
42
|
+
...props
|
|
43
|
+
}: React.HTMLAttributes<HTMLParagraphElement>) {
|
|
44
|
+
return (
|
|
45
|
+
<p className={cn("text-sm text-muted-foreground", className)} {...props} />
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function CardContent({
|
|
50
|
+
className,
|
|
51
|
+
...props
|
|
52
|
+
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
53
|
+
return (
|
|
54
|
+
<div className={cn("p-6 pt-0", className)} {...props} />
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
|
|
4
|
+
interface CheckboxProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Checkbox({ className, ...props }: CheckboxProps) {
|
|
9
|
+
return (
|
|
10
|
+
<input
|
|
11
|
+
type="checkbox"
|
|
12
|
+
className={cn(
|
|
13
|
+
"h-4 w-4 rounded border border-border/50 bg-background text-primary focus:outline-none focus:ring-2 focus:ring-primary/20",
|
|
14
|
+
className
|
|
15
|
+
)}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback, useMemo } from "react";
|
|
4
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
5
|
+
import { Search, Command, Sun, Moon, Palette, Code, LayoutTemplate, Image, FileText, Home, X, Sparkles } from "lucide-react";
|
|
6
|
+
import { useRouter } from "next/navigation";
|
|
7
|
+
import { useThemeContext } from "@/components/theme/theme-context";
|
|
8
|
+
import { NITRO_ALL_THEMES, type ThemeId } from "@/components/theme/theme-engine";
|
|
9
|
+
|
|
10
|
+
interface CommandItem {
|
|
11
|
+
id: string;
|
|
12
|
+
label: string;
|
|
13
|
+
shortcut?: string;
|
|
14
|
+
icon: React.ReactNode;
|
|
15
|
+
action: () => void;
|
|
16
|
+
category: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function CommandPalette() {
|
|
20
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
21
|
+
const [search, setSearch] = useState("");
|
|
22
|
+
const router = useRouter();
|
|
23
|
+
const { themeId, setThemeId, colorMode, setColorMode } = useThemeContext();
|
|
24
|
+
|
|
25
|
+
const toggle = useCallback(() => setIsOpen((prev) => !prev), []);
|
|
26
|
+
const close = useCallback(() => {
|
|
27
|
+
setIsOpen(false);
|
|
28
|
+
setSearch("");
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
// Keyboard shortcut listener
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
34
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
|
35
|
+
e.preventDefault();
|
|
36
|
+
toggle();
|
|
37
|
+
}
|
|
38
|
+
if (e.key === "Escape") {
|
|
39
|
+
close();
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
44
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
45
|
+
}, [toggle, close]);
|
|
46
|
+
|
|
47
|
+
const commands: CommandItem[] = useMemo(() => [
|
|
48
|
+
// Navigation
|
|
49
|
+
{
|
|
50
|
+
id: "nav-home",
|
|
51
|
+
label: "Go to Home",
|
|
52
|
+
shortcut: "H",
|
|
53
|
+
icon: <Home className="h-4 w-4" />,
|
|
54
|
+
action: () => { router.push("/"); close(); },
|
|
55
|
+
category: "Navigation",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "nav-studio",
|
|
59
|
+
label: "Open Studio",
|
|
60
|
+
shortcut: "T",
|
|
61
|
+
icon: <Palette className="h-4 w-4" />,
|
|
62
|
+
action: () => { router.push("/studio"); close(); },
|
|
63
|
+
category: "Navigation",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "nav-gallery",
|
|
67
|
+
label: "View Gallery",
|
|
68
|
+
shortcut: "G",
|
|
69
|
+
icon: <Image className="h-4 w-4" />,
|
|
70
|
+
action: () => { router.push("/gallery"); close(); },
|
|
71
|
+
category: "Navigation",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "nav-showcase",
|
|
75
|
+
label: "View Showcase",
|
|
76
|
+
shortcut: "S",
|
|
77
|
+
icon: <LayoutTemplate className="h-4 w-4" />,
|
|
78
|
+
action: () => { router.push("/showcase"); close(); },
|
|
79
|
+
category: "Navigation",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "nav-docs",
|
|
83
|
+
label: "Read Documentation",
|
|
84
|
+
shortcut: "D",
|
|
85
|
+
icon: <FileText className="h-4 w-4" />,
|
|
86
|
+
action: () => { router.push("/docs"); close(); },
|
|
87
|
+
category: "Navigation",
|
|
88
|
+
},
|
|
89
|
+
// Theme
|
|
90
|
+
{
|
|
91
|
+
id: "theme-toggle",
|
|
92
|
+
label: `Switch to ${colorMode === "dark" ? "Light" : "Dark"} Mode`,
|
|
93
|
+
shortcut: "T",
|
|
94
|
+
icon: colorMode === "dark" ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />,
|
|
95
|
+
action: () => { setColorMode(colorMode === "dark" ? "light" : "dark"); close(); },
|
|
96
|
+
category: "Theme",
|
|
97
|
+
},
|
|
98
|
+
// Export
|
|
99
|
+
{
|
|
100
|
+
id: "export-css",
|
|
101
|
+
label: "Export Theme as CSS",
|
|
102
|
+
icon: <Code className="h-4 w-4" />,
|
|
103
|
+
action: () => { router.push("/studio"); close(); },
|
|
104
|
+
category: "Export",
|
|
105
|
+
},
|
|
106
|
+
], [router, close, colorMode, setColorMode]);
|
|
107
|
+
|
|
108
|
+
// Theme commands
|
|
109
|
+
const themeCommands: CommandItem[] = useMemo(() =>
|
|
110
|
+
NITRO_ALL_THEMES.map((theme) => ({
|
|
111
|
+
id: `theme-${theme.id}`,
|
|
112
|
+
label: `Apply ${theme.label} Theme`,
|
|
113
|
+
icon: <Sparkles className="h-4 w-4" />,
|
|
114
|
+
action: () => { setThemeId(theme.id as ThemeId); close(); },
|
|
115
|
+
category: "Themes",
|
|
116
|
+
}))
|
|
117
|
+
, [setThemeId, close]);
|
|
118
|
+
|
|
119
|
+
const allCommands = useMemo(() => [...commands, ...themeCommands], [commands, themeCommands]);
|
|
120
|
+
|
|
121
|
+
const filteredCommands = useMemo(() => {
|
|
122
|
+
if (!search.trim()) return allCommands;
|
|
123
|
+
const query = search.toLowerCase();
|
|
124
|
+
return allCommands.filter((cmd) =>
|
|
125
|
+
cmd.label.toLowerCase().includes(query) ||
|
|
126
|
+
cmd.category.toLowerCase().includes(query)
|
|
127
|
+
);
|
|
128
|
+
}, [allCommands, search]);
|
|
129
|
+
|
|
130
|
+
const groupedCommands = useMemo(() => {
|
|
131
|
+
const groups: Record<string, CommandItem[]> = {};
|
|
132
|
+
filteredCommands.forEach((cmd) => {
|
|
133
|
+
if (!groups[cmd.category]) groups[cmd.category] = [];
|
|
134
|
+
groups[cmd.category].push(cmd);
|
|
135
|
+
});
|
|
136
|
+
return groups;
|
|
137
|
+
}, [filteredCommands]);
|
|
138
|
+
|
|
139
|
+
if (!isOpen) return null;
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<AnimatePresence>
|
|
143
|
+
<motion.div
|
|
144
|
+
initial={{ opacity: 0 }}
|
|
145
|
+
animate={{ opacity: 1 }}
|
|
146
|
+
exit={{ opacity: 0 }}
|
|
147
|
+
className="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
|
|
148
|
+
onClick={close}
|
|
149
|
+
>
|
|
150
|
+
<motion.div
|
|
151
|
+
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
152
|
+
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
153
|
+
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
154
|
+
transition={{ type: "spring", stiffness: 400, damping: 30 }}
|
|
155
|
+
className="fixed left-1/2 top-[20%] -translate-x-1/2 w-full max-w-2xl px-4"
|
|
156
|
+
onClick={(e) => e.stopPropagation()}
|
|
157
|
+
>
|
|
158
|
+
<div className="bg-background/95 backdrop-blur-xl rounded-3xl border border-border/50 shadow-2xl overflow-hidden">
|
|
159
|
+
{/* Search header */}
|
|
160
|
+
<div className="flex items-center gap-3 px-6 py-4 border-b border-border/50">
|
|
161
|
+
<Search className="h-5 w-5 text-muted-foreground" />
|
|
162
|
+
<input
|
|
163
|
+
type="text"
|
|
164
|
+
placeholder="Search commands, themes, or pages..."
|
|
165
|
+
value={search}
|
|
166
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
167
|
+
className="flex-1 bg-transparent text-lg outline-none placeholder:text-muted-foreground"
|
|
168
|
+
autoFocus
|
|
169
|
+
/>
|
|
170
|
+
<div className="flex items-center gap-2">
|
|
171
|
+
<kbd className="hidden sm:inline-flex items-center gap-1 px-2 py-1 text-xs font-mono bg-muted rounded-lg border">
|
|
172
|
+
<Command className="h-3 w-3" /> K
|
|
173
|
+
</kbd>
|
|
174
|
+
<button
|
|
175
|
+
onClick={close}
|
|
176
|
+
className="p-2 rounded-full hover:bg-muted/50 transition-colors"
|
|
177
|
+
>
|
|
178
|
+
<X className="h-4 w-4" />
|
|
179
|
+
</button>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
{/* Results */}
|
|
184
|
+
<div className="max-h-[400px] overflow-y-auto p-2">
|
|
185
|
+
{Object.entries(groupedCommands).map(([category, items]) => (
|
|
186
|
+
items.length > 0 && (
|
|
187
|
+
<div key={category} className="mb-2">
|
|
188
|
+
<div className="px-4 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
|
189
|
+
{category}
|
|
190
|
+
</div>
|
|
191
|
+
{items.map((cmd) => (
|
|
192
|
+
<CommandItem key={cmd.id} item={cmd} onSelect={cmd.action} />
|
|
193
|
+
))}
|
|
194
|
+
</div>
|
|
195
|
+
)
|
|
196
|
+
))}
|
|
197
|
+
|
|
198
|
+
{filteredCommands.length === 0 && (
|
|
199
|
+
<div className="px-4 py-8 text-center text-muted-foreground">
|
|
200
|
+
<p>No commands found</p>
|
|
201
|
+
<p className="text-sm mt-1">Try a different search term</p>
|
|
202
|
+
</div>
|
|
203
|
+
)}
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
{/* Footer */}
|
|
207
|
+
<div className="px-6 py-3 border-t border-border/50 text-xs text-muted-foreground flex items-center justify-between">
|
|
208
|
+
<div className="flex items-center gap-4">
|
|
209
|
+
<span className="flex items-center gap-1">
|
|
210
|
+
<kbd className="px-1.5 py-0.5 bg-muted rounded border">↑</kbd>
|
|
211
|
+
<kbd className="px-1.5 py-0.5 bg-muted rounded border">↓</kbd>
|
|
212
|
+
<span className="ml-1">Navigate</span>
|
|
213
|
+
</span>
|
|
214
|
+
<span className="flex items-center gap-1">
|
|
215
|
+
<kbd className="px-1.5 py-0.5 bg-muted rounded border">Enter</kbd>
|
|
216
|
+
<span className="ml-1">Select</span>
|
|
217
|
+
</span>
|
|
218
|
+
</div>
|
|
219
|
+
<span>{filteredCommands.length} results</span>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
</motion.div>
|
|
223
|
+
</motion.div>
|
|
224
|
+
</AnimatePresence>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function CommandItem({ item, onSelect }: { item: CommandItem; onSelect: () => void }) {
|
|
229
|
+
return (
|
|
230
|
+
<button
|
|
231
|
+
onClick={onSelect}
|
|
232
|
+
className="w-full flex items-center gap-3 px-4 py-2.5 rounded-xl text-left hover:bg-accent hover:text-accent-foreground transition-colors group"
|
|
233
|
+
>
|
|
234
|
+
<span className="text-muted-foreground group-hover:text-accent-foreground">
|
|
235
|
+
{item.icon}
|
|
236
|
+
</span>
|
|
237
|
+
<span className="flex-1 font-medium">{item.label}</span>
|
|
238
|
+
{item.shortcut && (
|
|
239
|
+
<kbd className="px-2 py-0.5 text-xs font-mono bg-muted rounded border group-hover:bg-accent-foreground/10">
|
|
240
|
+
{item.shortcut}
|
|
241
|
+
</kbd>
|
|
242
|
+
)}
|
|
243
|
+
</button>
|
|
244
|
+
);
|
|
245
|
+
}
|