@ghatak/slash-ui 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 +36 -0
- package/__registry__/index.ts +493 -0
- package/app/(auth)/layout.tsx +18 -0
- package/app/(auth)/login/page.tsx +152 -0
- package/app/(protected)/component/[id]/page.tsx +48 -0
- package/app/(protected)/component/page.tsx +151 -0
- package/app/(protected)/docs/page.tsx +222 -0
- package/app/account/page.tsx +109 -0
- package/app/api/me/route.ts +24 -0
- package/app/globals.css +68 -0
- package/app/icon.png +0 -0
- package/app/layout.tsx +43 -0
- package/app/page.tsx +22 -0
- package/app/pricing/page.tsx +12 -0
- package/bin/intex.ts +19 -0
- package/components/smooth-scroll.tsx +26 -0
- package/components/toast.tsx +101 -0
- package/components/ui/IndustryProof.tsx +159 -0
- package/components/ui/ShowcaseContainer.tsx +497 -0
- package/components/ui/dot-cursor.tsx +108 -0
- package/components/ui/featuredComponents.tsx +126 -0
- package/components/ui/footer.tsx +59 -0
- package/components/ui/hero.tsx +85 -0
- package/components/ui/navbar.tsx +337 -0
- package/components/ui/pricing.tsx +163 -0
- package/eslint.config.mjs +18 -0
- package/hooks/use-component-search.tsx +52 -0
- package/lib/actions/auth.action.ts +88 -0
- package/lib/auth.ts +18 -0
- package/lib/email.ts +46 -0
- package/lib/prisma.ts +14 -0
- package/lib/registry.ts +17 -0
- package/lib/utils.ts +6 -0
- package/middleware/middleware.ts +21 -0
- package/next.config.ts +7 -0
- package/package.json +61 -0
- package/postcss.config.mjs +7 -0
- package/prisma/migrations/20260303172729_init/migration.sql +21 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +22 -0
- package/prisma.config.ts +14 -0
- package/public/compVideos/neubrutal-button.mp4 +0 -0
- package/public/fonts/BeVietnamPro-ExtraBold.otf +0 -0
- package/public/fonts/CartographCF-Regular.ttf +0 -0
- package/public/fonts/Hoshiko-Satsuki.ttf +0 -0
- package/public/fonts/Switzer-Regular.otf +0 -0
- package/public/images/PricingSlash.svg +58 -0
- package/public/images/slash_1.svg +59 -0
- package/public/images/slash_2.svg +18 -0
- package/public/video/hero_video.mp4 +0 -0
- package/registry/details/buttons/neubrutal-button-details.tsx +146 -0
- package/registry/details/cursor/dot-cursor-details.tsx +11 -0
- package/registry/details/navbar/floating-navbar-details.tsx +11 -0
- package/registry/details/scrollbars/minimal-scrollbar-details.tsx +0 -0
- package/registry/index.ts +35 -0
- package/registry/ui/buttons/neubrutal-button.tsx +33 -0
- package/registry/ui/cursors/dot-cursor.tsx +108 -0
- package/registry/ui/navbars/floating-navbar.tsx +99 -0
- package/registry/ui/scrollbars/minimal-scrollbar.tsx +203 -0
- package/scripts/build-registry.ts +60 -0
- package/src/commands/add.ts +40 -0
- package/src/commands/init.ts +75 -0
- package/src/commands/list.ts +44 -0
- package/src/index.ts +35 -0
- package/src/utils/get-pkg-manager.ts +7 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { atomDark as theme } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
|
4
|
+
import ReactMarkdown from 'react-markdown';
|
|
5
|
+
import remarkGfm from 'remark-gfm';
|
|
6
|
+
import React, { useState, Suspense, useEffect } from 'react';
|
|
7
|
+
import Link from 'next/link';
|
|
8
|
+
import { useParams } from 'next/navigation';
|
|
9
|
+
import {
|
|
10
|
+
Code2,
|
|
11
|
+
Command,
|
|
12
|
+
PanelLeft,
|
|
13
|
+
Info,
|
|
14
|
+
Check,
|
|
15
|
+
X,
|
|
16
|
+
Search,
|
|
17
|
+
Box,
|
|
18
|
+
Copy,
|
|
19
|
+
Loader2,
|
|
20
|
+
Maximize,
|
|
21
|
+
Home,
|
|
22
|
+
LoaderCircle,
|
|
23
|
+
MousePointer2,
|
|
24
|
+
Rocket,
|
|
25
|
+
} from 'lucide-react';
|
|
26
|
+
import { AnimatePresence, motion } from 'framer-motion';
|
|
27
|
+
import { Index } from '@/__registry__';
|
|
28
|
+
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
29
|
+
import { getComponentSource } from '@/lib/registry';
|
|
30
|
+
|
|
31
|
+
export default function ShowcaseContainer({
|
|
32
|
+
children,
|
|
33
|
+
title,
|
|
34
|
+
code: propsCode,
|
|
35
|
+
description: propsDescription,
|
|
36
|
+
install: propsInstall,
|
|
37
|
+
}: {
|
|
38
|
+
children: React.ReactNode;
|
|
39
|
+
title: string;
|
|
40
|
+
code?: string;
|
|
41
|
+
description?: string;
|
|
42
|
+
install?: string;
|
|
43
|
+
}) {
|
|
44
|
+
const [isSidebarOpen, setSidebarOpen] = useState(false);
|
|
45
|
+
const [activePanel, setActivePanel] = useState<'code' | 'info' | null>(null);
|
|
46
|
+
const [copied, setCopied] = useState(false);
|
|
47
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
48
|
+
const { id } = useParams();
|
|
49
|
+
const [sourceCode, setSourceCode] = useState<string | null>(null);
|
|
50
|
+
const [isLoadingCode, setIsLoadingCode] = useState(false);
|
|
51
|
+
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
52
|
+
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
|
53
|
+
|
|
54
|
+
const componentsList = Object.values(Index['default']);
|
|
55
|
+
const activeItem = Index['default'][id as string] as any;
|
|
56
|
+
|
|
57
|
+
// Sync state if user exits via ESC or uses Cmd+K
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
60
|
+
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
setIsSearchOpen((prev) => !prev);
|
|
63
|
+
}
|
|
64
|
+
if (e.key === 'Escape') {
|
|
65
|
+
setIsSearchOpen(false);
|
|
66
|
+
setActivePanel(null);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const handleFullscreenChange = () => {
|
|
71
|
+
setIsFullscreen(!!document.fullscreenElement);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
75
|
+
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
|
76
|
+
return () => {
|
|
77
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
78
|
+
document.removeEventListener('fullscreenchange', handleFullscreenChange);
|
|
79
|
+
};
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
async function fetchSource() {
|
|
84
|
+
if (activePanel === 'code' && activeItem?.files) {
|
|
85
|
+
setIsLoadingCode(true);
|
|
86
|
+
const code = await getComponentSource(activeItem.files);
|
|
87
|
+
setSourceCode(code ?? '// Error: Source code not found.');
|
|
88
|
+
setIsLoadingCode(false);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
fetchSource();
|
|
92
|
+
}, [activePanel, activeItem, id]);
|
|
93
|
+
|
|
94
|
+
const getContainerStyle = () => {
|
|
95
|
+
switch (id) {
|
|
96
|
+
case 'neubrutal-button':
|
|
97
|
+
return 'bg-[#538F37]';
|
|
98
|
+
case 'dot-cursor':
|
|
99
|
+
case 'flaoting-navbar':
|
|
100
|
+
return '';
|
|
101
|
+
default:
|
|
102
|
+
return 'bg-[#0a0908]';
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const toggleFullscreen = () => {
|
|
107
|
+
if (!document.fullscreenElement) {
|
|
108
|
+
document.documentElement
|
|
109
|
+
.requestFullscreen()
|
|
110
|
+
.then(() => setIsFullscreen(true));
|
|
111
|
+
} else {
|
|
112
|
+
document.exitFullscreen().then(() => setIsFullscreen(false));
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const copyToClipboard = async (text: string) => {
|
|
117
|
+
await navigator.clipboard.writeText(text);
|
|
118
|
+
setCopied(true);
|
|
119
|
+
setTimeout(() => setCopied(false), 2000);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const dynamicCode =
|
|
123
|
+
propsCode ||
|
|
124
|
+
sourceCode ||
|
|
125
|
+
activeItem?.content ||
|
|
126
|
+
'// No source code found.';
|
|
127
|
+
const dynamicDescription =
|
|
128
|
+
propsDescription ||
|
|
129
|
+
activeItem?.description ||
|
|
130
|
+
`Premium ${title} component.`;
|
|
131
|
+
const dynamicInstall =
|
|
132
|
+
propsInstall ||
|
|
133
|
+
activeItem?.install ||
|
|
134
|
+
'npm install framer-motion lucide-react';
|
|
135
|
+
|
|
136
|
+
// Filter logic for the search modal
|
|
137
|
+
const filteredComponents = componentsList.filter((comp: any) =>
|
|
138
|
+
comp.name.toLowerCase().includes(searchQuery.toLowerCase()),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const staticSearchItems = [
|
|
142
|
+
{ icon: <Home size={16} />, label: 'Home', category: 'Pages', path: '/' },
|
|
143
|
+
{
|
|
144
|
+
icon: <LoaderCircle size={16} />,
|
|
145
|
+
label: 'Loader',
|
|
146
|
+
category: 'Pages',
|
|
147
|
+
path: '/loader',
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
icon: <MousePointer2 size={16} />,
|
|
151
|
+
label: 'Cursor',
|
|
152
|
+
category: 'Pages',
|
|
153
|
+
path: '/cursor',
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
icon: <Box size={16} />,
|
|
157
|
+
label: 'All Components',
|
|
158
|
+
category: 'Pages',
|
|
159
|
+
path: '/component',
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
icon: <Rocket size={16} />,
|
|
163
|
+
label: 'Quick Start',
|
|
164
|
+
category: 'Get Started',
|
|
165
|
+
path: '/docs',
|
|
166
|
+
},
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div className='h-screen w-screen bg-black p-2 overflow-hidden text-white font-sans'>
|
|
171
|
+
<div className='h-full w-full rounded-[30px] border border-white/5 overflow-hidden flex relative bg-[#0A0A0A]'>
|
|
172
|
+
{/* LEFT NAV SIDEBAR */}
|
|
173
|
+
<aside
|
|
174
|
+
className={`absolute top-0 left-0 z-[150] h-full w-[320px] transition-all duration-500 ease-[cubic-bezier(0.23,1,0.32,1)] bg-[#0A0A0A]/80 backdrop-blur-2xl border-r border-white/5 ${isSidebarOpen ? 'translate-x-0 opacity-100' : '-translate-x-full opacity-0 pointer-events-none'}`}
|
|
175
|
+
>
|
|
176
|
+
<div className='flex flex-col h-full p-10'>
|
|
177
|
+
<div className='flex items-center justify-between mb-12'>
|
|
178
|
+
<div className='text-sm text-zinc-500'>Navigation</div>
|
|
179
|
+
<button
|
|
180
|
+
onClick={() => setSidebarOpen(false)}
|
|
181
|
+
className='p-2 text-zinc-500 hover:text-white transition-colors cursor-pointer'
|
|
182
|
+
>
|
|
183
|
+
<X size={16} />
|
|
184
|
+
</button>
|
|
185
|
+
</div>
|
|
186
|
+
<nav className='flex-1 overflow-y-auto custom-scrollbar space-y-2'>
|
|
187
|
+
{componentsList.map((comp: any, index: number) => (
|
|
188
|
+
<Link
|
|
189
|
+
key={comp.name}
|
|
190
|
+
href={`/component/${comp.name}`}
|
|
191
|
+
onClick={() => setSidebarOpen(false)}
|
|
192
|
+
className='group flex items-center gap-4 py-2'
|
|
193
|
+
>
|
|
194
|
+
<div
|
|
195
|
+
className={`h-px transition-all duration-300 ${id === comp.name ? 'w-6 bg-white' : 'w-3 bg-zinc-800 group-hover:bg-zinc-500'}`}
|
|
196
|
+
/>
|
|
197
|
+
<span
|
|
198
|
+
className={`text-[13px] transition-colors ${id === comp.name ? 'text-white font-medium' : 'text-zinc-500 group-hover:text-zinc-300'}`}
|
|
199
|
+
>
|
|
200
|
+
<span className='font-mono mr-2 opacity-30'>
|
|
201
|
+
{String(index + 1).padStart(2, '0')}
|
|
202
|
+
</span>
|
|
203
|
+
{comp.name}
|
|
204
|
+
</span>
|
|
205
|
+
</Link>
|
|
206
|
+
))}
|
|
207
|
+
</nav>
|
|
208
|
+
</div>
|
|
209
|
+
</aside>
|
|
210
|
+
|
|
211
|
+
<AnimatePresence>
|
|
212
|
+
{isSidebarOpen && (
|
|
213
|
+
<motion.div
|
|
214
|
+
initial={{ opacity: 0 }}
|
|
215
|
+
animate={{ opacity: 1 }}
|
|
216
|
+
exit={{ opacity: 0 }}
|
|
217
|
+
onClick={() => setSidebarOpen(false)}
|
|
218
|
+
className='absolute inset-0 bg-black/40 z-[140] backdrop-blur-sm cursor-pointer'
|
|
219
|
+
/>
|
|
220
|
+
)}
|
|
221
|
+
</AnimatePresence>
|
|
222
|
+
|
|
223
|
+
<div className='flex w-full h-full relative'>
|
|
224
|
+
<main
|
|
225
|
+
className={`flex-1 relative flex flex-col items-center justify-center transition-all duration-700 ${getContainerStyle()} ease-in-out ${activePanel || isSearchOpen ? 'scale-[0.9] opacity-50' : 'scale-100 opacity-100'}`}
|
|
226
|
+
>
|
|
227
|
+
{!isFullscreen && (
|
|
228
|
+
<>
|
|
229
|
+
<button
|
|
230
|
+
onClick={(e) => {
|
|
231
|
+
e.stopPropagation();
|
|
232
|
+
setSidebarOpen(true);
|
|
233
|
+
}}
|
|
234
|
+
className='absolute top-8 left-8 z-[110] p-3 bg-[#161616]/80 backdrop-blur-md border border-white/10 rounded-2xl text-zinc-400 hover:text-white transition-all cursor-pointer shadow-xl'
|
|
235
|
+
>
|
|
236
|
+
<PanelLeft size={20} />
|
|
237
|
+
</button>
|
|
238
|
+
<div className='absolute top-10 right-10 text-right pointer-events-none'>
|
|
239
|
+
<h2 className='text-2xl text-white font-black uppercase tracking-tighter'>
|
|
240
|
+
{title}
|
|
241
|
+
</h2>
|
|
242
|
+
</div>
|
|
243
|
+
</>
|
|
244
|
+
)}
|
|
245
|
+
|
|
246
|
+
<div className='transition-transform duration-700'>{children}</div>
|
|
247
|
+
|
|
248
|
+
{/* FLOATING TOOLBAR */}
|
|
249
|
+
<div className='absolute bottom-10 left-1/2 -translate-x-1/2 flex items-center gap-1 bg-[#161616]/90 border border-white/10 p-1.5 rounded-2xl shadow-2xl backdrop-blur-xl z-[120]'>
|
|
250
|
+
<button
|
|
251
|
+
onClick={toggleFullscreen}
|
|
252
|
+
className='p-2.5 rounded-xl cursor-pointer text-zinc-500 hover:text-white hover:bg-white/5 transition-all'
|
|
253
|
+
>
|
|
254
|
+
<Maximize size={18} />
|
|
255
|
+
</button>
|
|
256
|
+
<button
|
|
257
|
+
onClick={() =>
|
|
258
|
+
setActivePanel(activePanel === 'info' ? null : 'info')
|
|
259
|
+
}
|
|
260
|
+
className={`p-2.5 rounded-xl cursor-pointer transition-all ${activePanel === 'info' ? 'bg-white text-black' : 'text-zinc-500 hover:text-white hover:bg-white/5'}`}
|
|
261
|
+
>
|
|
262
|
+
<Info size={18} />
|
|
263
|
+
</button>
|
|
264
|
+
<div className='w-px h-4 bg-white/10 mx-1' />
|
|
265
|
+
<button
|
|
266
|
+
onClick={() =>
|
|
267
|
+
setActivePanel(activePanel === 'code' ? null : 'code')
|
|
268
|
+
}
|
|
269
|
+
className={`p-2.5 rounded-xl transition-all cursor-pointer ${activePanel === 'code' ? 'bg-white text-black' : 'text-zinc-500 hover:text-white hover:bg-white/5'}`}
|
|
270
|
+
>
|
|
271
|
+
<Code2 size={18} />
|
|
272
|
+
</button>
|
|
273
|
+
<button
|
|
274
|
+
onClick={() => setIsSearchOpen(true)}
|
|
275
|
+
className={`p-2.5 rounded-xl transition-all cursor-pointer text-zinc-500 hover:text-white hover:bg-white/5`}
|
|
276
|
+
>
|
|
277
|
+
<Command size={18} />
|
|
278
|
+
</button>
|
|
279
|
+
</div>
|
|
280
|
+
</main>
|
|
281
|
+
|
|
282
|
+
{/* RIGHT SIDE PANEL (Code/Info) */}
|
|
283
|
+
<aside
|
|
284
|
+
className={`h-full bg-[#080808] border-l border-white/5 transition-all duration-500 ease-[cubic-bezier(0.23,1,0.32,1)] overflow-hidden relative z-[130] ${activePanel ? 'w-[45%] opacity-100' : 'w-0 opacity-0'}`}
|
|
285
|
+
>
|
|
286
|
+
<div className='p-12 h-full flex flex-col min-w-[450px]'>
|
|
287
|
+
<div className='flex items-center justify-between mb-12'>
|
|
288
|
+
<h3 className='text-sm uppercase font-beVietnamPro text-zinc-500'>
|
|
289
|
+
{activePanel === 'code' ? 'Source Code' : 'Documentation'}
|
|
290
|
+
</h3>
|
|
291
|
+
<button
|
|
292
|
+
onClick={() => setActivePanel(null)}
|
|
293
|
+
className='p-2 hover:bg-white/5 rounded-lg text-zinc-500 hover:text-white cursor-pointer transition-colors'
|
|
294
|
+
>
|
|
295
|
+
<X size={18} />
|
|
296
|
+
</button>
|
|
297
|
+
</div>
|
|
298
|
+
|
|
299
|
+
<div className='flex-1 overflow-y-auto custom-scrollbar pr-4'>
|
|
300
|
+
<AnimatePresence mode='wait'>
|
|
301
|
+
{activePanel === 'code' ? (
|
|
302
|
+
<motion.div
|
|
303
|
+
key='code'
|
|
304
|
+
initial={{ opacity: 0, x: 20 }}
|
|
305
|
+
animate={{ opacity: 1, x: 0 }}
|
|
306
|
+
exit={{ opacity: 0, x: 20 }}
|
|
307
|
+
className='relative rounded-2xl bg-black border border-white/10 overflow-hidden'
|
|
308
|
+
>
|
|
309
|
+
{isLoadingCode ? (
|
|
310
|
+
<div className='flex flex-col items-center justify-center py-32 text-zinc-600 gap-4'>
|
|
311
|
+
<Loader2 className='animate-spin' size={24} />
|
|
312
|
+
<span className='text-[10px] uppercase tracking-widest font-bold'>
|
|
313
|
+
Fetching Code
|
|
314
|
+
</span>
|
|
315
|
+
</div>
|
|
316
|
+
) : (
|
|
317
|
+
<>
|
|
318
|
+
<button
|
|
319
|
+
onClick={() => copyToClipboard(dynamicCode)}
|
|
320
|
+
className='absolute top-4 right-4 p-2 bg-white/5 rounded-md text-zinc-400 cursor-pointer z-10 hover:bg-white/10'
|
|
321
|
+
>
|
|
322
|
+
{copied ? (
|
|
323
|
+
<Check size={14} className='text-white' />
|
|
324
|
+
) : (
|
|
325
|
+
<Copy size={14} />
|
|
326
|
+
)}
|
|
327
|
+
</button>
|
|
328
|
+
<SyntaxHighlighter
|
|
329
|
+
language='tsx'
|
|
330
|
+
style={theme}
|
|
331
|
+
customStyle={{
|
|
332
|
+
margin: 0,
|
|
333
|
+
padding: '32px',
|
|
334
|
+
fontSize: '13px',
|
|
335
|
+
background: 'transparent',
|
|
336
|
+
lineHeight: '1.7',
|
|
337
|
+
}}
|
|
338
|
+
>
|
|
339
|
+
{dynamicCode}
|
|
340
|
+
</SyntaxHighlighter>
|
|
341
|
+
</>
|
|
342
|
+
)}
|
|
343
|
+
</motion.div>
|
|
344
|
+
) : activePanel === 'info' ? (
|
|
345
|
+
<motion.div
|
|
346
|
+
key='info'
|
|
347
|
+
initial={{ opacity: 0, x: 20 }}
|
|
348
|
+
animate={{ opacity: 1, x: 0 }}
|
|
349
|
+
exit={{ opacity: 0, x: 20 }}
|
|
350
|
+
>
|
|
351
|
+
<section className='space-y-12'>
|
|
352
|
+
<div>
|
|
353
|
+
<h4 className='text-[10px] font-black uppercase tracking-[0.4em] text-zinc-600 mb-6'>
|
|
354
|
+
Description
|
|
355
|
+
</h4>
|
|
356
|
+
<div className='prose prose-invert prose-sm text-zinc-400'>
|
|
357
|
+
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
|
358
|
+
{dynamicDescription}
|
|
359
|
+
</ReactMarkdown>
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
<div>
|
|
363
|
+
<h4 className='text-[10px] font-black uppercase tracking-[0.4em] text-zinc-600 mb-6'>
|
|
364
|
+
Installation
|
|
365
|
+
</h4>
|
|
366
|
+
<div className='bg-black p-6 rounded-2xl border border-white/10 font-mono text-xs flex items-center justify-between'>
|
|
367
|
+
<span className='text-zinc-300'>
|
|
368
|
+
{dynamicInstall}
|
|
369
|
+
</span>
|
|
370
|
+
<button
|
|
371
|
+
onClick={() => copyToClipboard(dynamicInstall)}
|
|
372
|
+
className='p-2 hover:bg-white/5 rounded-md text-zinc-500'
|
|
373
|
+
>
|
|
374
|
+
{copied ? (
|
|
375
|
+
<Check size={14} />
|
|
376
|
+
) : (
|
|
377
|
+
<Copy size={14} />
|
|
378
|
+
)}
|
|
379
|
+
</button>
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
</section>
|
|
383
|
+
</motion.div>
|
|
384
|
+
) : null}
|
|
385
|
+
</AnimatePresence>
|
|
386
|
+
</div>
|
|
387
|
+
</div>
|
|
388
|
+
</aside>
|
|
389
|
+
</div>
|
|
390
|
+
|
|
391
|
+
{/* SEARCH OVERLAY */}
|
|
392
|
+
<AnimatePresence>
|
|
393
|
+
{isSearchOpen && (
|
|
394
|
+
<div className='fixed inset-0 z-[200] flex items-start justify-center pt-[18vh] px-4'>
|
|
395
|
+
<motion.div
|
|
396
|
+
initial={{ opacity: 0 }}
|
|
397
|
+
animate={{ opacity: 1 }}
|
|
398
|
+
exit={{ opacity: 0 }}
|
|
399
|
+
onClick={() => setIsSearchOpen(false)}
|
|
400
|
+
className='absolute inset-0 bg-black/60 backdrop-blur-md'
|
|
401
|
+
/>
|
|
402
|
+
|
|
403
|
+
<motion.div
|
|
404
|
+
initial={{ opacity: 0, scale: 0.95, y: -20 }}
|
|
405
|
+
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
406
|
+
exit={{ opacity: 0, scale: 0.95, y: -20 }}
|
|
407
|
+
className='relative w-full max-w-[600px] bg-zinc-950 border border-zinc-800 rounded-xl shadow-2xl overflow-hidden'
|
|
408
|
+
>
|
|
409
|
+
<div className='flex items-center px-4 border-b border-zinc-800'>
|
|
410
|
+
<Search className='text-zinc-500' size={18} />
|
|
411
|
+
<input
|
|
412
|
+
autoFocus
|
|
413
|
+
value={searchQuery}
|
|
414
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
415
|
+
placeholder='Search components or pages...'
|
|
416
|
+
className='w-full h-14 bg-transparent border-none outline-none px-4 text-white text-sm placeholder:text-zinc-600'
|
|
417
|
+
/>
|
|
418
|
+
</div>
|
|
419
|
+
|
|
420
|
+
<div className='max-h-[400px] overflow-y-auto p-2 custom-scrollbar'>
|
|
421
|
+
{searchQuery.length > 0 ? (
|
|
422
|
+
<div className='p-2'>
|
|
423
|
+
<p className='px-3 py-2 text-[10px] font-semibold text-zinc-500 uppercase tracking-wider'>
|
|
424
|
+
Component Results
|
|
425
|
+
</p>
|
|
426
|
+
{filteredComponents.length > 0 ? (
|
|
427
|
+
filteredComponents.map((comp: any) => (
|
|
428
|
+
<Link
|
|
429
|
+
key={comp.name}
|
|
430
|
+
href={`/component/${comp.name}`}
|
|
431
|
+
onClick={() => {
|
|
432
|
+
setIsSearchOpen(false);
|
|
433
|
+
setSearchQuery('');
|
|
434
|
+
}}
|
|
435
|
+
className='flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-white/5 transition-all group'
|
|
436
|
+
>
|
|
437
|
+
<Box
|
|
438
|
+
size={16}
|
|
439
|
+
className='text-zinc-500 group-hover:text-white'
|
|
440
|
+
/>
|
|
441
|
+
<span className='text-sm text-zinc-300 group-hover:text-white'>
|
|
442
|
+
{comp.name}
|
|
443
|
+
</span>
|
|
444
|
+
</Link>
|
|
445
|
+
))
|
|
446
|
+
) : (
|
|
447
|
+
<p className='px-3 py-4 text-sm text-zinc-600'>
|
|
448
|
+
No components found...
|
|
449
|
+
</p>
|
|
450
|
+
)}
|
|
451
|
+
</div>
|
|
452
|
+
) : (
|
|
453
|
+
['Pages', 'Get Started'].map((category) => (
|
|
454
|
+
<div key={category} className='mb-2'>
|
|
455
|
+
<p className='px-3 py-2 text-[10px] font-semibold text-zinc-500 uppercase tracking-wider'>
|
|
456
|
+
{category}
|
|
457
|
+
</p>
|
|
458
|
+
{staticSearchItems
|
|
459
|
+
.filter((item) => item.category === category)
|
|
460
|
+
.map((item) => (
|
|
461
|
+
<Link
|
|
462
|
+
key={item.label}
|
|
463
|
+
href={item.path}
|
|
464
|
+
onClick={() => setIsSearchOpen(false)}
|
|
465
|
+
className='flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-white/5 transition-all group'
|
|
466
|
+
>
|
|
467
|
+
<div className='text-zinc-500 group-hover:text-white'>
|
|
468
|
+
{item.icon}
|
|
469
|
+
</div>
|
|
470
|
+
<span className='text-sm text-zinc-300 group-hover:text-white'>
|
|
471
|
+
{item.label}
|
|
472
|
+
</span>
|
|
473
|
+
</Link>
|
|
474
|
+
))}
|
|
475
|
+
</div>
|
|
476
|
+
))
|
|
477
|
+
)}
|
|
478
|
+
</div>
|
|
479
|
+
<div className='px-4 py-3 border-t border-zinc-800 bg-zinc-900/30 flex justify-between items-center text-[10px] text-zinc-500 font-medium'>
|
|
480
|
+
<div className='flex gap-3'>
|
|
481
|
+
<span className='flex items-center gap-1'>
|
|
482
|
+
<Command size={10} /> to select
|
|
483
|
+
</span>
|
|
484
|
+
<span className='flex items-center gap-1'>
|
|
485
|
+
Enter to open
|
|
486
|
+
</span>
|
|
487
|
+
</div>
|
|
488
|
+
<span>ESC to close</span>
|
|
489
|
+
</div>
|
|
490
|
+
</motion.div>
|
|
491
|
+
</div>
|
|
492
|
+
)}
|
|
493
|
+
</AnimatePresence>
|
|
494
|
+
</div>
|
|
495
|
+
</div>
|
|
496
|
+
);
|
|
497
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect } from 'react';
|
|
4
|
+
|
|
5
|
+
const CustomCursor = () => {
|
|
6
|
+
const [position, setPosition] = useState({ x: 0, y: 0 });
|
|
7
|
+
const [isHovering, setIsHovering] = useState(false);
|
|
8
|
+
const [isDarkBackground, setIsDarkBackground] = useState(false);
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
|
|
12
|
+
// document.body.style.cursor = 'none';
|
|
13
|
+
|
|
14
|
+
const updatePosition = (e: MouseEvent) => {
|
|
15
|
+
setPosition({ x: e.clientX, y: e.clientY });
|
|
16
|
+
|
|
17
|
+
const element = document.elementFromPoint(e.clientX, e.clientY);
|
|
18
|
+
if (element) {
|
|
19
|
+
const bgColor = window.getComputedStyle(element).backgroundColor;
|
|
20
|
+
const brightness = getBrightness(bgColor);
|
|
21
|
+
setIsDarkBackground(brightness < 128);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const handleMouseOver = (e: MouseEvent) => {
|
|
26
|
+
const target = e.target as HTMLElement;
|
|
27
|
+
if (
|
|
28
|
+
target.tagName === 'A' ||
|
|
29
|
+
target.tagName === 'BUTTON' ||
|
|
30
|
+
target.onclick !== null ||
|
|
31
|
+
window.getComputedStyle(target).cursor === 'pointer'
|
|
32
|
+
) {
|
|
33
|
+
setIsHovering(true);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const handleMouseOut = () => {
|
|
38
|
+
setIsHovering(false);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
window.addEventListener('mousemove', updatePosition);
|
|
42
|
+
document.addEventListener('mouseover', handleMouseOver);
|
|
43
|
+
document.addEventListener('mouseout', handleMouseOut);
|
|
44
|
+
|
|
45
|
+
return () => {
|
|
46
|
+
window.removeEventListener('mousemove', updatePosition);
|
|
47
|
+
document.removeEventListener('mouseover', handleMouseOver);
|
|
48
|
+
document.removeEventListener('mouseout', handleMouseOut);
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
};
|
|
52
|
+
}, []);
|
|
53
|
+
|
|
54
|
+
const getBrightness = (color: string) => {
|
|
55
|
+
const rgb = color.match(/d+/g);
|
|
56
|
+
if (!rgb) return 255;
|
|
57
|
+
const r = parseInt(rgb[0]);
|
|
58
|
+
const g = parseInt(rgb[1]);
|
|
59
|
+
const b = parseInt(rgb[2]);
|
|
60
|
+
return (r * 299 + g * 587 + b * 114) / 1000;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div
|
|
65
|
+
style={{
|
|
66
|
+
cursor: 'none',
|
|
67
|
+
width: '100vw',
|
|
68
|
+
height: '100vh',
|
|
69
|
+
position: 'fixed',
|
|
70
|
+
top: 0,
|
|
71
|
+
left: 0,
|
|
72
|
+
pointerEvents: 'none'
|
|
73
|
+
}}>
|
|
74
|
+
|
|
75
|
+
<div
|
|
76
|
+
style={{
|
|
77
|
+
position: 'fixed',
|
|
78
|
+
left: `${position.x}px`,
|
|
79
|
+
top: `${position.y}px`,
|
|
80
|
+
pointerEvents: 'none',
|
|
81
|
+
transform: `translate(-50%, -50%) scale(${isHovering ? 1.5 : 1})`,
|
|
82
|
+
transition: 'transform 0.2s ease',
|
|
83
|
+
zIndex: 9999,
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
<svg
|
|
87
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
88
|
+
width="24"
|
|
89
|
+
height="24"
|
|
90
|
+
viewBox="0 0 24 24"
|
|
91
|
+
style={{
|
|
92
|
+
display: 'block',
|
|
93
|
+
transition: 'all 0.2s ease',
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
<path
|
|
97
|
+
fill={isDarkBackground ? '#ffffff' : '#000000'}
|
|
98
|
+
stroke={isDarkBackground ? '#000000' : '#ffffff'}
|
|
99
|
+
strokeWidth="2"
|
|
100
|
+
d="M5.5 3.21V20.8c0 .45.54.67.85.35l4.86-4.86a.5.5 0 0 1 .35-.15h6.87a.5.5 0 0 0 .35-.85L6.35 2.85a.5.5 0 0 0-.85.35Z"
|
|
101
|
+
/>
|
|
102
|
+
</svg>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export default CustomCursor;
|