abhishek-portfolio-template 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/README.md +59 -0
- package/bin/cli.js +54 -0
- package/package.json +27 -0
- package/template/components.json +22 -0
- package/template/next.config.ts +79 -0
- package/template/package.json +43 -0
- package/template/postcss.config.js +6 -0
- package/template/public/BoliviaSignature-ZpWnz.ttf +0 -0
- package/template/public/Gemini_Generated_Image_xc97toxc97toxc97.png +0 -0
- package/template/public/Hendrigo.otf +0 -0
- package/template/public/audiomass-output.mp3 +0 -0
- package/template/public/file.svg +1 -0
- package/template/public/globe.svg +1 -0
- package/template/public/googlec77e59474f5a09cb.html +1 -0
- package/template/public/icon-192x192.png +0 -0
- package/template/public/icon-512x512.png +0 -0
- package/template/public/next.svg +1 -0
- package/template/public/paper sound .mpeg +0 -0
- package/template/public/removebg.png +0 -0
- package/template/public/resume.pdf +0 -0
- package/template/public/sw.js +1 -0
- package/template/public/swe-worker-5c72df51bb1f6ee0.js +1 -0
- package/template/public/vercel.svg +1 -0
- package/template/public/window.svg +1 -0
- package/template/public/workbox-f1770938.js +1 -0
- package/template/src/app/about/page.tsx +91 -0
- package/template/src/app/actions/optimize-text.ts +54 -0
- package/template/src/app/gaming/page.tsx +308 -0
- package/template/src/app/github/[username]/page.tsx +97 -0
- package/template/src/app/globals.css +321 -0
- package/template/src/app/layout.tsx +39 -0
- package/template/src/app/manifest.ts +25 -0
- package/template/src/app/not-found.tsx +16 -0
- package/template/src/app/page.tsx +28 -0
- package/template/src/app/robots.ts +12 -0
- package/template/src/app/sitemap.ts +38 -0
- package/template/src/app/template.tsx +5 -0
- package/template/src/app/works/[slug]/page.tsx +50 -0
- package/template/src/app/works/client.tsx +44 -0
- package/template/src/app/works/page.tsx +24 -0
- package/template/src/components/about/about-client.tsx +259 -0
- package/template/src/components/home/bento-gallery.tsx +52 -0
- package/template/src/components/home/contact-section.tsx +34 -0
- package/template/src/components/home/craft-card.tsx +18 -0
- package/template/src/components/home/featured-projects.tsx +186 -0
- package/template/src/components/home/focus-card.tsx +171 -0
- package/template/src/components/home/identity-card.tsx +45 -0
- package/template/src/components/home/philosophy-card.tsx +104 -0
- package/template/src/components/home/skills-in-motion.tsx +109 -0
- package/template/src/components/home/tech-stack-marquee.tsx +56 -0
- package/template/src/components/ui/3d-folder.tsx +569 -0
- package/template/src/components/ui/avatar.tsx +50 -0
- package/template/src/components/ui/badge.tsx +36 -0
- package/template/src/components/ui/basic-avatar.tsx +12 -0
- package/template/src/components/ui/button.tsx +117 -0
- package/template/src/components/ui/clipboard-secret.tsx +39 -0
- package/template/src/components/ui/command-menu.tsx +519 -0
- package/template/src/components/ui/command-palette.tsx +152 -0
- package/template/src/components/ui/consciousness-mode.tsx +200 -0
- package/template/src/components/ui/copy-code-button.tsx +135 -0
- package/template/src/components/ui/display-cards.tsx +70 -0
- package/template/src/components/ui/dotted-map.tsx +128 -0
- package/template/src/components/ui/dropdown-menu.tsx +200 -0
- package/template/src/components/ui/emoji-rating.tsx +123 -0
- package/template/src/components/ui/exit-message.tsx +50 -0
- package/template/src/components/ui/image-zoom-overlay.tsx +178 -0
- package/template/src/components/ui/input-otp.tsx +71 -0
- package/template/src/components/ui/kbd.tsx +87 -0
- package/template/src/components/ui/location-tag.tsx +232 -0
- package/template/src/components/ui/minimal-testimonial.tsx +97 -0
- package/template/src/components/ui/mobile-menu.tsx +191 -0
- package/template/src/components/ui/navbar.tsx +148 -0
- package/template/src/components/ui/page-transition.tsx +24 -0
- package/template/src/components/ui/pixeleted-404-not-found.tsx +110 -0
- package/template/src/components/ui/preloader-wrapper.tsx +102 -0
- package/template/src/components/ui/preloader.tsx +104 -0
- package/template/src/components/ui/project-contributors.tsx +57 -0
- package/template/src/components/ui/scroll-area.tsx +117 -0
- package/template/src/components/ui/signature.tsx +173 -0
- package/template/src/components/ui/smooth-scroll.tsx +31 -0
- package/template/src/components/ui/social-icons.tsx +103 -0
- package/template/src/components/ui/social-stories.tsx +394 -0
- package/template/src/components/ui/sound-constants.ts +1 -0
- package/template/src/components/ui/text-explode.tsx +188 -0
- package/template/src/components/ui/toast.tsx +80 -0
- package/template/src/components/ui/tooltip.tsx +30 -0
- package/template/src/components/ui/user-location.tsx +151 -0
- package/template/src/components/ui/vertical-image-stack.tsx +345 -0
- package/template/src/components/works/changelog-overlay.tsx +212 -0
- package/template/src/components/works/currently-working-card.tsx +130 -0
- package/template/src/components/works/project-details-view.tsx +464 -0
- package/template/src/components/works/project-grid.tsx +81 -0
- package/template/src/fonts/BoliviaSignature-ZpWnz.ttf +0 -0
- package/template/src/fonts/Hendrigo.otf +0 -0
- package/template/src/lib/data.ts +61 -0
- package/template/src/lib/fonts.ts +14 -0
- package/template/src/lib/github.ts +15 -0
- package/template/src/lib/supabase.ts +11 -0
- package/template/src/lib/utils.ts +6 -0
- package/template/tailwind.config.ts +31 -0
- package/template/tsconfig.json +34 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
5
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
|
|
8
|
+
const buttonVariants = cva(
|
|
9
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
10
|
+
{
|
|
11
|
+
variants: {
|
|
12
|
+
variant: {
|
|
13
|
+
default:
|
|
14
|
+
"bg-primary text-primary-foreground hover:bg-primary/90 focus-visible:ring-ring shadow-sm",
|
|
15
|
+
destructive:
|
|
16
|
+
"bg-destructive text-destructive-foreground hover:bg-destructive/90 focus-visible:ring-destructive shadow-sm",
|
|
17
|
+
outline:
|
|
18
|
+
"border border-border text-foreground hover:bg-accent hover:text-accent-foreground focus-visible:ring-ring shadow-sm",
|
|
19
|
+
secondary:
|
|
20
|
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80 focus-visible:ring-ring",
|
|
21
|
+
ghost:
|
|
22
|
+
"text-foreground hover:bg-accent hover:text-accent-foreground focus-visible:ring-ring",
|
|
23
|
+
link: "text-secondary-foreground underline-offset-4 hover:underline focus-visible:ring-ring",
|
|
24
|
+
},
|
|
25
|
+
size: {
|
|
26
|
+
default: "h-9 px-4 py-2",
|
|
27
|
+
sm: "h-8 px-3 text-xs",
|
|
28
|
+
lg: "h-10 px-8",
|
|
29
|
+
xl: "h-12 px-10 text-base",
|
|
30
|
+
icon: "h-9 w-9",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
defaultVariants: {
|
|
34
|
+
variant: "default",
|
|
35
|
+
size: "default",
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
export type CustomButtonProps = Omit<
|
|
41
|
+
ButtonProps,
|
|
42
|
+
keyof React.ComponentProps<"button">
|
|
43
|
+
>;
|
|
44
|
+
|
|
45
|
+
export interface ButtonProps
|
|
46
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
47
|
+
VariantProps<typeof buttonVariants> {
|
|
48
|
+
asChild?: boolean;
|
|
49
|
+
loading?: boolean;
|
|
50
|
+
leftIcon?: React.ReactNode;
|
|
51
|
+
rightIcon?: React.ReactNode;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
55
|
+
(
|
|
56
|
+
{
|
|
57
|
+
className,
|
|
58
|
+
variant,
|
|
59
|
+
size,
|
|
60
|
+
asChild = false,
|
|
61
|
+
loading = false,
|
|
62
|
+
leftIcon,
|
|
63
|
+
rightIcon,
|
|
64
|
+
children,
|
|
65
|
+
disabled,
|
|
66
|
+
...props
|
|
67
|
+
},
|
|
68
|
+
ref
|
|
69
|
+
) => {
|
|
70
|
+
const Comp = asChild ? Slot : "button";
|
|
71
|
+
|
|
72
|
+
const content = (
|
|
73
|
+
<>
|
|
74
|
+
{loading && (
|
|
75
|
+
<svg
|
|
76
|
+
className="animate-spin h-4 w-4"
|
|
77
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
78
|
+
fill="none"
|
|
79
|
+
viewBox="0 0 24 24"
|
|
80
|
+
>
|
|
81
|
+
<circle
|
|
82
|
+
className="opacity-25"
|
|
83
|
+
cx="12"
|
|
84
|
+
cy="12"
|
|
85
|
+
r="10"
|
|
86
|
+
stroke="currentColor"
|
|
87
|
+
strokeWidth="4"
|
|
88
|
+
/>
|
|
89
|
+
<path
|
|
90
|
+
className="opacity-75"
|
|
91
|
+
fill="currentColor"
|
|
92
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
93
|
+
/>
|
|
94
|
+
</svg>
|
|
95
|
+
)}
|
|
96
|
+
{leftIcon && !loading && leftIcon}
|
|
97
|
+
{children}
|
|
98
|
+
{rightIcon && !loading && rightIcon}
|
|
99
|
+
</>
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<Comp
|
|
104
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
105
|
+
ref={ref}
|
|
106
|
+
disabled={disabled || loading}
|
|
107
|
+
{...props}
|
|
108
|
+
>
|
|
109
|
+
{asChild ? children : content}
|
|
110
|
+
</Comp>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
Button.displayName = "Button";
|
|
116
|
+
|
|
117
|
+
export { Button, buttonVariants };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
|
|
5
|
+
export const ClipboardSecret = () => {
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const handleCopy = (e: ClipboardEvent) => {
|
|
8
|
+
const selection = window.getSelection();
|
|
9
|
+
if (!selection) return;
|
|
10
|
+
|
|
11
|
+
// Get text and normalize (remove line breaks, double spaces)
|
|
12
|
+
const selectedText = selection.toString().replace(/\s+/g, ' ').trim().toLowerCase();
|
|
13
|
+
|
|
14
|
+
console.log("📋 Clipboard Detect:", selectedText);
|
|
15
|
+
|
|
16
|
+
// TRIGGERS
|
|
17
|
+
// Note: identity-card.tsx has "Product Engineer."
|
|
18
|
+
const triggers = ["product engineer", "abhishek"];
|
|
19
|
+
|
|
20
|
+
const isMatch = triggers.some(trigger => selectedText.includes(trigger));
|
|
21
|
+
|
|
22
|
+
if (isMatch) {
|
|
23
|
+
e.preventDefault();
|
|
24
|
+
if (e.clipboardData) {
|
|
25
|
+
const secret = "If you found this, we should talk.";
|
|
26
|
+
e.clipboardData.setData("text/plain", secret);
|
|
27
|
+
// Also try the navigator API as backup (though preventDefault usually handles it)
|
|
28
|
+
// navigator.clipboard.writeText(secret).catch(() => {});
|
|
29
|
+
console.log("🔒 Secret Injected");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
document.addEventListener("copy", handleCopy);
|
|
35
|
+
return () => document.removeEventListener("copy", handleCopy);
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
return null; // Component is logic only, no visuals
|
|
39
|
+
};
|
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
5
|
+
import * as VisuallyHidden from "@radix-ui/react-visually-hidden";
|
|
6
|
+
import { motion } from "motion/react";
|
|
7
|
+
import { Search, X } from "lucide-react";
|
|
8
|
+
import { cn } from "@/lib/utils";
|
|
9
|
+
import { Kbd } from "@/components/ui/kbd";
|
|
10
|
+
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
11
|
+
|
|
12
|
+
// Utility function to detect OS and return appropriate modifier key
|
|
13
|
+
const getModifierKey = () => {
|
|
14
|
+
if (typeof navigator === "undefined") return { key: "Ctrl", symbol: "Ctrl" };
|
|
15
|
+
|
|
16
|
+
const isMac =
|
|
17
|
+
navigator.platform.toUpperCase().indexOf("MAC") >= 0 ||
|
|
18
|
+
navigator.userAgent.toUpperCase().indexOf("MAC") >= 0;
|
|
19
|
+
|
|
20
|
+
return isMac ? { key: "cmd", symbol: "⌘" } : { key: "ctrl", symbol: "Ctrl" };
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Context for sharing state between components
|
|
24
|
+
interface CommandMenuContextType {
|
|
25
|
+
value: string;
|
|
26
|
+
setValue: (value: string) => void;
|
|
27
|
+
selectedIndex: number;
|
|
28
|
+
setSelectedIndex: (index: number) => void;
|
|
29
|
+
scrollType?: "auto" | "always" | "scroll" | "hover";
|
|
30
|
+
scrollHideDelay?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const CommandMenuContext = React.createContext<
|
|
34
|
+
CommandMenuContextType | undefined
|
|
35
|
+
>(undefined);
|
|
36
|
+
|
|
37
|
+
const CommandMenuProvider: React.FC<{
|
|
38
|
+
children: React.ReactNode;
|
|
39
|
+
value: string;
|
|
40
|
+
setValue: (value: string) => void;
|
|
41
|
+
selectedIndex: number;
|
|
42
|
+
setSelectedIndex: (index: number) => void;
|
|
43
|
+
scrollType?: "auto" | "always" | "scroll" | "hover";
|
|
44
|
+
scrollHideDelay?: number;
|
|
45
|
+
}> = ({
|
|
46
|
+
children,
|
|
47
|
+
value,
|
|
48
|
+
setValue,
|
|
49
|
+
selectedIndex,
|
|
50
|
+
setSelectedIndex,
|
|
51
|
+
scrollType,
|
|
52
|
+
scrollHideDelay,
|
|
53
|
+
}) => (
|
|
54
|
+
<CommandMenuContext.Provider
|
|
55
|
+
value={{
|
|
56
|
+
value,
|
|
57
|
+
setValue,
|
|
58
|
+
selectedIndex,
|
|
59
|
+
setSelectedIndex,
|
|
60
|
+
scrollType,
|
|
61
|
+
scrollHideDelay,
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
{children}
|
|
65
|
+
</CommandMenuContext.Provider>
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const useCommandMenu = () => {
|
|
69
|
+
const context = React.useContext(CommandMenuContext);
|
|
70
|
+
if (!context) {
|
|
71
|
+
throw new Error("useCommandMenu must be used within CommandMenuProvider");
|
|
72
|
+
}
|
|
73
|
+
return context;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Core CommandMenu component using Dialog
|
|
77
|
+
const CommandMenu = DialogPrimitive.Root;
|
|
78
|
+
const CommandMenuTrigger = DialogPrimitive.Trigger;
|
|
79
|
+
const CommandMenuPortal = DialogPrimitive.Portal;
|
|
80
|
+
const CommandMenuClose = DialogPrimitive.Close;
|
|
81
|
+
|
|
82
|
+
// Title components for accessibility
|
|
83
|
+
const CommandMenuTitle = React.forwardRef<
|
|
84
|
+
React.ElementRef<typeof DialogPrimitive.Title>,
|
|
85
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
|
86
|
+
>(({ className, ...props }, ref) => (
|
|
87
|
+
<DialogPrimitive.Title
|
|
88
|
+
ref={ref}
|
|
89
|
+
className={cn(
|
|
90
|
+
"text-lg font-semibold leading-none tracking-tight text-foreground",
|
|
91
|
+
className
|
|
92
|
+
)}
|
|
93
|
+
{...props}
|
|
94
|
+
/>
|
|
95
|
+
));
|
|
96
|
+
CommandMenuTitle.displayName = "CommandMenuTitle";
|
|
97
|
+
|
|
98
|
+
const CommandMenuDescription = React.forwardRef<
|
|
99
|
+
React.ElementRef<typeof DialogPrimitive.Description>,
|
|
100
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
|
101
|
+
>(({ className, ...props }, ref) => (
|
|
102
|
+
<DialogPrimitive.Description
|
|
103
|
+
ref={ref}
|
|
104
|
+
className={cn("text-sm text-muted-foreground", className)}
|
|
105
|
+
{...props}
|
|
106
|
+
/>
|
|
107
|
+
));
|
|
108
|
+
CommandMenuDescription.displayName = "CommandMenuDescription";
|
|
109
|
+
|
|
110
|
+
// Overlay with backdrop blur
|
|
111
|
+
const CommandMenuOverlay = React.forwardRef<
|
|
112
|
+
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
|
113
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
|
114
|
+
>(({ className, ...props }, ref) => (
|
|
115
|
+
<DialogPrimitive.Overlay
|
|
116
|
+
ref={ref}
|
|
117
|
+
className={cn(
|
|
118
|
+
"fixed inset-0 z-50 bg-black/50 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
119
|
+
className
|
|
120
|
+
)}
|
|
121
|
+
{...props}
|
|
122
|
+
/>
|
|
123
|
+
));
|
|
124
|
+
CommandMenuOverlay.displayName = "CommandMenuOverlay";
|
|
125
|
+
|
|
126
|
+
// Main content container with keyboard navigation
|
|
127
|
+
const CommandMenuContent = React.forwardRef<
|
|
128
|
+
React.ElementRef<typeof DialogPrimitive.Content>,
|
|
129
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
|
|
130
|
+
showShortcut?: boolean;
|
|
131
|
+
scrollType?: "auto" | "always" | "scroll" | "hover";
|
|
132
|
+
scrollHideDelay?: number;
|
|
133
|
+
}
|
|
134
|
+
>(
|
|
135
|
+
(
|
|
136
|
+
{
|
|
137
|
+
className,
|
|
138
|
+
children,
|
|
139
|
+
showShortcut = true,
|
|
140
|
+
scrollType = "hover",
|
|
141
|
+
scrollHideDelay = 600,
|
|
142
|
+
...props
|
|
143
|
+
},
|
|
144
|
+
ref
|
|
145
|
+
) => {
|
|
146
|
+
const [value, setValue] = React.useState("");
|
|
147
|
+
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
148
|
+
|
|
149
|
+
// Keyboard navigation
|
|
150
|
+
React.useEffect(() => {
|
|
151
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
152
|
+
if (e.key === "ArrowDown") {
|
|
153
|
+
e.preventDefault();
|
|
154
|
+
// Logic will be handled by CommandMenuList
|
|
155
|
+
} else if (e.key === "ArrowUp") {
|
|
156
|
+
e.preventDefault();
|
|
157
|
+
// Logic will be handled by CommandMenuList
|
|
158
|
+
} else if (e.key === "Enter") {
|
|
159
|
+
e.preventDefault();
|
|
160
|
+
// Logic will be handled by CommandMenuItem
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
165
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
166
|
+
}, []);
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<CommandMenuPortal>
|
|
170
|
+
<CommandMenuOverlay />
|
|
171
|
+
<DialogPrimitive.Content asChild ref={ref} {...props}>
|
|
172
|
+
<motion.div
|
|
173
|
+
initial={{ opacity: 0, scale: 0.95, y: -20 }}
|
|
174
|
+
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
175
|
+
exit={{ opacity: 0, scale: 0.95, y: -20 }}
|
|
176
|
+
transition={{ duration: 0.2, ease: "easeOut" }}
|
|
177
|
+
className={cn(
|
|
178
|
+
"fixed left-[50%] top-[50%] z-50 w-[95%] max-w-2xl translate-x-[-50%] translate-y-[-50%]",
|
|
179
|
+
// GLASS EFFECT UPDATE: Heavy blur, translucent black, subtle border
|
|
180
|
+
"bg-[#09090b]/80 backdrop-blur-3xl border border-white/5 rounded-2xl shadow-2xl ring-1 ring-white/5",
|
|
181
|
+
"overflow-hidden max-h-[85vh] flex flex-col",
|
|
182
|
+
className
|
|
183
|
+
)}
|
|
184
|
+
>
|
|
185
|
+
{" "}
|
|
186
|
+
<CommandMenuProvider
|
|
187
|
+
value={value}
|
|
188
|
+
setValue={setValue}
|
|
189
|
+
selectedIndex={selectedIndex}
|
|
190
|
+
setSelectedIndex={setSelectedIndex}
|
|
191
|
+
scrollType={scrollType}
|
|
192
|
+
scrollHideDelay={scrollHideDelay}
|
|
193
|
+
>
|
|
194
|
+
<VisuallyHidden.Root>
|
|
195
|
+
<CommandMenuTitle>Command Menu</CommandMenuTitle>
|
|
196
|
+
</VisuallyHidden.Root>
|
|
197
|
+
|
|
198
|
+
{children}
|
|
199
|
+
|
|
200
|
+
<CommandMenuClose className="absolute right-3 top-3 rounded-lg p-1.5 text-zinc-400 hover:text-white hover:bg-white/10 focus-visible:outline-none transition-colors">
|
|
201
|
+
<X size={14} />
|
|
202
|
+
<span className="sr-only">Close</span>
|
|
203
|
+
</CommandMenuClose>
|
|
204
|
+
|
|
205
|
+
{showShortcut && (
|
|
206
|
+
<div className="absolute right-12 top-3 flex items-center justify-center gap-1 h-6.5">
|
|
207
|
+
<Kbd size="xs" className="bg-white/5 border-white/10 text-zinc-400">{getModifierKey().symbol}</Kbd>
|
|
208
|
+
<Kbd size="xs" className="bg-white/5 border-white/10 text-zinc-400">K</Kbd>
|
|
209
|
+
</div>
|
|
210
|
+
)}
|
|
211
|
+
</CommandMenuProvider>
|
|
212
|
+
</motion.div>
|
|
213
|
+
</DialogPrimitive.Content>
|
|
214
|
+
</CommandMenuPortal>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
);
|
|
218
|
+
CommandMenuContent.displayName = "CommandMenuContent";
|
|
219
|
+
|
|
220
|
+
// Input component for search
|
|
221
|
+
const CommandMenuInput = React.forwardRef<
|
|
222
|
+
HTMLInputElement,
|
|
223
|
+
React.InputHTMLAttributes<HTMLInputElement> & {
|
|
224
|
+
placeholder?: string;
|
|
225
|
+
}
|
|
226
|
+
>(
|
|
227
|
+
(
|
|
228
|
+
{ className, placeholder = "Type a command or search...", ...props },
|
|
229
|
+
ref
|
|
230
|
+
) => {
|
|
231
|
+
const { value, setValue } = useCommandMenu();
|
|
232
|
+
|
|
233
|
+
return (
|
|
234
|
+
<div className="flex items-center border-b border-white/5 px-4 py-2 shrink-0">
|
|
235
|
+
<Search className="mr-3 h-4 w-4 shrink-0 text-zinc-500" />
|
|
236
|
+
<input
|
|
237
|
+
ref={ref}
|
|
238
|
+
value={value}
|
|
239
|
+
onChange={(e) => setValue(e.target.value)}
|
|
240
|
+
className={cn(
|
|
241
|
+
"flex h-10 w-full rounded-none border-0 bg-transparent py-3 text-sm outline-none placeholder:text-zinc-600 text-zinc-200 disabled:cursor-not-allowed disabled:opacity-50 font-medium",
|
|
242
|
+
className
|
|
243
|
+
)}
|
|
244
|
+
placeholder={placeholder}
|
|
245
|
+
{...props}
|
|
246
|
+
/>
|
|
247
|
+
</div>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
);
|
|
251
|
+
CommandMenuInput.displayName = "CommandMenuInput";
|
|
252
|
+
|
|
253
|
+
// List container for command items with scroll area
|
|
254
|
+
const CommandMenuList = React.forwardRef<
|
|
255
|
+
HTMLDivElement,
|
|
256
|
+
React.HTMLAttributes<HTMLDivElement> & {
|
|
257
|
+
maxHeight?: string;
|
|
258
|
+
}
|
|
259
|
+
>(({ className, children, maxHeight, ...props }, ref) => {
|
|
260
|
+
const {
|
|
261
|
+
selectedIndex,
|
|
262
|
+
setSelectedIndex,
|
|
263
|
+
scrollType = "hover",
|
|
264
|
+
scrollHideDelay = 600,
|
|
265
|
+
} = useCommandMenu();
|
|
266
|
+
|
|
267
|
+
// Handle keyboard navigation
|
|
268
|
+
React.useEffect(() => {
|
|
269
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
270
|
+
const items = document.querySelectorAll("[data-command-item]");
|
|
271
|
+
const maxIndex = items.length - 1;
|
|
272
|
+
|
|
273
|
+
if (e.key === "ArrowDown") {
|
|
274
|
+
e.preventDefault();
|
|
275
|
+
const newIndex = Math.min(selectedIndex + 1, maxIndex);
|
|
276
|
+
setSelectedIndex(newIndex);
|
|
277
|
+
|
|
278
|
+
// Scroll selected item into view
|
|
279
|
+
const selectedItem = items[newIndex] as HTMLElement;
|
|
280
|
+
if (selectedItem) {
|
|
281
|
+
selectedItem.scrollIntoView({
|
|
282
|
+
block: "nearest",
|
|
283
|
+
behavior: "smooth",
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
} else if (e.key === "ArrowUp") {
|
|
287
|
+
e.preventDefault();
|
|
288
|
+
const newIndex = Math.max(selectedIndex - 1, 0);
|
|
289
|
+
setSelectedIndex(newIndex);
|
|
290
|
+
|
|
291
|
+
// Scroll selected item into view
|
|
292
|
+
const selectedItem = items[newIndex] as HTMLElement;
|
|
293
|
+
if (selectedItem) {
|
|
294
|
+
selectedItem.scrollIntoView({
|
|
295
|
+
block: "nearest",
|
|
296
|
+
behavior: "smooth",
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
303
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
304
|
+
}, [selectedIndex, setSelectedIndex]);
|
|
305
|
+
|
|
306
|
+
return (
|
|
307
|
+
<div ref={ref} className={cn("p-2 overflow-hidden", className)} {...props}>
|
|
308
|
+
<ScrollArea
|
|
309
|
+
className="w-full [&_[data-radix-scroll-area-viewport]]:overscroll-contain [&_[data-radix-scroll-area-scrollbar]]:opacity-0 [&_[data-radix-scroll-area-scrollbar]]:w-0 [&_[data-radix-scroll-area-scrollbar]]:bg-transparent"
|
|
310
|
+
style={{ height: maxHeight || "auto" }}
|
|
311
|
+
type="always"
|
|
312
|
+
>
|
|
313
|
+
<div className="space-y-1 p-1">{children}</div>
|
|
314
|
+
</ScrollArea>
|
|
315
|
+
</div>
|
|
316
|
+
);
|
|
317
|
+
});
|
|
318
|
+
CommandMenuList.displayName = "CommandMenuList";
|
|
319
|
+
|
|
320
|
+
// Command group with optional heading
|
|
321
|
+
const CommandMenuGroup = React.forwardRef<
|
|
322
|
+
HTMLDivElement,
|
|
323
|
+
React.HTMLAttributes<HTMLDivElement> & {
|
|
324
|
+
heading?: string;
|
|
325
|
+
}
|
|
326
|
+
>(({ className, children, heading, ...props }, ref) => (
|
|
327
|
+
<div ref={ref} className={cn("", className)} {...props}>
|
|
328
|
+
{heading && (
|
|
329
|
+
<div className="px-3 py-2 text-[10px] font-semibold text-zinc-500 uppercase tracking-widest leading-none">
|
|
330
|
+
{heading}
|
|
331
|
+
</div>
|
|
332
|
+
)}
|
|
333
|
+
{children}
|
|
334
|
+
</div>
|
|
335
|
+
));
|
|
336
|
+
CommandMenuGroup.displayName = "CommandMenuGroup";
|
|
337
|
+
|
|
338
|
+
// Individual command item
|
|
339
|
+
const CommandMenuItem = React.forwardRef<
|
|
340
|
+
HTMLDivElement,
|
|
341
|
+
React.HTMLAttributes<HTMLDivElement> & {
|
|
342
|
+
onSelect?: () => void;
|
|
343
|
+
disabled?: boolean;
|
|
344
|
+
shortcut?: string;
|
|
345
|
+
icon?: React.ReactNode;
|
|
346
|
+
index?: number;
|
|
347
|
+
keywords?: string[];
|
|
348
|
+
label?: string;
|
|
349
|
+
}
|
|
350
|
+
>(
|
|
351
|
+
(
|
|
352
|
+
{
|
|
353
|
+
className,
|
|
354
|
+
children,
|
|
355
|
+
onSelect,
|
|
356
|
+
disabled = false,
|
|
357
|
+
shortcut,
|
|
358
|
+
icon,
|
|
359
|
+
index = 0,
|
|
360
|
+
keywords,
|
|
361
|
+
label,
|
|
362
|
+
...props
|
|
363
|
+
},
|
|
364
|
+
ref
|
|
365
|
+
) => {
|
|
366
|
+
const { selectedIndex, setSelectedIndex, value: searchValue } = useCommandMenu();
|
|
367
|
+
const isSelected = selectedIndex === index;
|
|
368
|
+
|
|
369
|
+
// Filter logic
|
|
370
|
+
const matches = !searchValue || (label || (typeof children === 'string' ? children : '')).toLowerCase().includes(searchValue.toLowerCase()) || keywords?.some(k => k.toLowerCase().includes(searchValue.toLowerCase()));
|
|
371
|
+
|
|
372
|
+
// Handle click and enter key
|
|
373
|
+
const handleSelect = React.useCallback(() => {
|
|
374
|
+
if (!disabled && onSelect) {
|
|
375
|
+
onSelect();
|
|
376
|
+
}
|
|
377
|
+
}, [disabled, onSelect]);
|
|
378
|
+
|
|
379
|
+
React.useEffect(() => {
|
|
380
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
381
|
+
if (e.key === "Enter" && isSelected) {
|
|
382
|
+
e.preventDefault();
|
|
383
|
+
handleSelect();
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
388
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
389
|
+
}, [isSelected, handleSelect]);
|
|
390
|
+
|
|
391
|
+
if (!matches) return null;
|
|
392
|
+
|
|
393
|
+
return (
|
|
394
|
+
<motion.div
|
|
395
|
+
layout
|
|
396
|
+
initial={{ opacity: 0, scale: 0.98 }}
|
|
397
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
398
|
+
exit={{ opacity: 0, scale: 0.98 }}
|
|
399
|
+
transition={{ duration: 0.2 }}
|
|
400
|
+
ref={ref}
|
|
401
|
+
data-command-item
|
|
402
|
+
className={cn(
|
|
403
|
+
"relative flex cursor-pointer select-none items-center rounded-lg px-3 py-2.5 text-sm outline-none transition-all duration-200 gap-3",
|
|
404
|
+
// Default state
|
|
405
|
+
"text-zinc-400",
|
|
406
|
+
// Selected / Hover state -- SUBTLE & SLEEK
|
|
407
|
+
isSelected
|
|
408
|
+
? "bg-white/[0.08] text-zinc-100 shadow-[0_0_0_1px_rgba(255,255,255,0.05)]"
|
|
409
|
+
: "hover:bg-white/[0.04] hover:text-zinc-300",
|
|
410
|
+
|
|
411
|
+
disabled && "pointer-events-none opacity-50",
|
|
412
|
+
className
|
|
413
|
+
)}
|
|
414
|
+
onClick={handleSelect}
|
|
415
|
+
onMouseEnter={() => setSelectedIndex(index)}
|
|
416
|
+
{...(props as any)}
|
|
417
|
+
>
|
|
418
|
+
{icon && (
|
|
419
|
+
<div className={cn("h-4 w-4 flex items-center justify-center transition-colors", isSelected ? "text-zinc-100" : "text-zinc-500")}>
|
|
420
|
+
{/* Clone icon to enforce size if needed, but styling parent is usually enough */}
|
|
421
|
+
{icon}
|
|
422
|
+
</div>
|
|
423
|
+
)}
|
|
424
|
+
|
|
425
|
+
<div className="flex-1 truncate font-medium">{children}</div>
|
|
426
|
+
|
|
427
|
+
{shortcut && (
|
|
428
|
+
<div className="ml-auto flex items-center gap-1">
|
|
429
|
+
{shortcut.split("+").map((key, i) => (
|
|
430
|
+
<React.Fragment key={key}>
|
|
431
|
+
{i > 0 && (
|
|
432
|
+
<span className="text-zinc-600 text-xs">
|
|
433
|
+
+
|
|
434
|
+
</span>
|
|
435
|
+
)}
|
|
436
|
+
<Kbd size="xs" className="bg-transparent border-white/10 text-zinc-500 group-hover:text-zinc-400">
|
|
437
|
+
{key === "cmd" || key === "⌘"
|
|
438
|
+
? getModifierKey().symbol
|
|
439
|
+
: key === "shift"
|
|
440
|
+
? "⇧"
|
|
441
|
+
: key === "alt"
|
|
442
|
+
? "⌥"
|
|
443
|
+
: key === "ctrl"
|
|
444
|
+
? getModifierKey().key === "cmd"
|
|
445
|
+
? "⌃"
|
|
446
|
+
: "Ctrl"
|
|
447
|
+
: key}
|
|
448
|
+
</Kbd>
|
|
449
|
+
</React.Fragment>
|
|
450
|
+
))}
|
|
451
|
+
</div>
|
|
452
|
+
)}
|
|
453
|
+
</motion.div>
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
);
|
|
457
|
+
CommandMenuItem.displayName = "CommandMenuItem";
|
|
458
|
+
|
|
459
|
+
// Separator between groups
|
|
460
|
+
const CommandMenuSeparator = React.forwardRef<
|
|
461
|
+
HTMLDivElement,
|
|
462
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
463
|
+
>(({ className, ...props }, ref) => (
|
|
464
|
+
<div
|
|
465
|
+
ref={ref}
|
|
466
|
+
className={cn("-mx-1 my-2 h-px bg-white/5", className)}
|
|
467
|
+
{...props}
|
|
468
|
+
/>
|
|
469
|
+
));
|
|
470
|
+
CommandMenuSeparator.displayName = "CommandMenuSeparator";
|
|
471
|
+
|
|
472
|
+
// Empty state
|
|
473
|
+
const CommandMenuEmpty = React.forwardRef<
|
|
474
|
+
HTMLDivElement,
|
|
475
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
476
|
+
>(({ className, children = "No results found.", ...props }, ref) => (
|
|
477
|
+
<div
|
|
478
|
+
ref={ref}
|
|
479
|
+
className={cn(
|
|
480
|
+
"py-6 text-center text-sm text-muted-foreground",
|
|
481
|
+
className
|
|
482
|
+
)}
|
|
483
|
+
{...props}
|
|
484
|
+
>
|
|
485
|
+
{children}
|
|
486
|
+
</div>
|
|
487
|
+
));
|
|
488
|
+
CommandMenuEmpty.displayName = "CommandMenuEmpty";
|
|
489
|
+
|
|
490
|
+
// Hook for global keyboard shortcut
|
|
491
|
+
export const useCommandMenuShortcut = (callback: () => void) => {
|
|
492
|
+
React.useEffect(() => {
|
|
493
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
494
|
+
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
|
495
|
+
e.preventDefault();
|
|
496
|
+
callback();
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
501
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
502
|
+
}, [callback]);
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
export {
|
|
506
|
+
CommandMenu,
|
|
507
|
+
CommandMenuTrigger,
|
|
508
|
+
CommandMenuContent,
|
|
509
|
+
CommandMenuTitle,
|
|
510
|
+
CommandMenuDescription,
|
|
511
|
+
CommandMenuInput,
|
|
512
|
+
CommandMenuList,
|
|
513
|
+
CommandMenuEmpty,
|
|
514
|
+
CommandMenuGroup,
|
|
515
|
+
CommandMenuItem,
|
|
516
|
+
CommandMenuSeparator,
|
|
517
|
+
CommandMenuClose,
|
|
518
|
+
useCommandMenu,
|
|
519
|
+
};
|