@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,126 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useRef } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { ChevronRight } from 'lucide-react';
|
|
6
|
+
import { Index } from "@/__registry__";
|
|
7
|
+
|
|
8
|
+
interface ComponentCardProps {
|
|
9
|
+
title: string;
|
|
10
|
+
videoSrc?: string;
|
|
11
|
+
children?: React.ReactNode;
|
|
12
|
+
span?: string;
|
|
13
|
+
bg?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ComponentCard = ({
|
|
17
|
+
title,
|
|
18
|
+
videoSrc,
|
|
19
|
+
children,
|
|
20
|
+
span = '',
|
|
21
|
+
bg = 'bg-[#161616]',
|
|
22
|
+
}: ComponentCardProps) => {
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
className={`group relative flex flex-col justify-between rounded-3xl ${bg} border border-white/5 p-1 hover:border-white/20 transition-all duration-500 h-full cursor-pointer ${span}`}
|
|
26
|
+
>
|
|
27
|
+
<div className='relative flex-grow overflow-hidden rounded-[22px] bg-[#0A0A0A] flex items-center justify-center min-h-[140px] border border-white/[0.03]'>
|
|
28
|
+
{videoSrc ? (
|
|
29
|
+
<video
|
|
30
|
+
src={videoSrc}
|
|
31
|
+
autoPlay
|
|
32
|
+
loop
|
|
33
|
+
muted
|
|
34
|
+
playsInline
|
|
35
|
+
className="absolute inset-0 w-full h-full object-cover opacity-100 scale-100"
|
|
36
|
+
/>
|
|
37
|
+
) : (
|
|
38
|
+
<div className="relative z-10 w-full h-full flex items-center justify-center">
|
|
39
|
+
{children}
|
|
40
|
+
</div>
|
|
41
|
+
)}
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div className='px-4 py-3 flex justify-between items-center bg-transparent relative z-10'>
|
|
45
|
+
<h3 className='text-[13px] font-medium text-zinc-400 group-hover:text-white transition-colors'>
|
|
46
|
+
{title}
|
|
47
|
+
</h3>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const FeaturedComponents = () => {
|
|
54
|
+
const getComp = (name: string) => (Index as any)["default"]?.[name];
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className='min-h-screen w-full p-8 text-white mt-10'>
|
|
58
|
+
<div className='max-w-7xl mx-auto'>
|
|
59
|
+
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 auto-rows-[220px]'>
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
<Link href={`/component/`}>
|
|
63
|
+
<ComponentCard title="" videoSrc={getComp("reveal")?.video} />
|
|
64
|
+
</Link>
|
|
65
|
+
|
|
66
|
+
{/* 2. Navbar */}
|
|
67
|
+
<Link href={`/component/floating-navbar`} className="lg:col-span-2">
|
|
68
|
+
<ComponentCard title="Floating Navbar" videoSrc={getComp("navbar")?.video} />
|
|
69
|
+
</Link>
|
|
70
|
+
|
|
71
|
+
{/* 3. Custom ScrollBar */}
|
|
72
|
+
<Link href={`/component/minimal-scrollbar`} className="lg:row-span-3">
|
|
73
|
+
<ComponentCard title="Minimal Scrollbar" videoSrc={getComp("minimal-scrollbar")?.video} />
|
|
74
|
+
</Link>
|
|
75
|
+
|
|
76
|
+
{/* 4. Custom Cursor */}
|
|
77
|
+
<Link href={`/component/dot-cursor`} className="lg:col-span-2 lg:row-span-2">
|
|
78
|
+
<ComponentCard title="Custom Cursor">
|
|
79
|
+
|
|
80
|
+
</ComponentCard>
|
|
81
|
+
</Link>
|
|
82
|
+
|
|
83
|
+
{/* 5. Devouring Details */}
|
|
84
|
+
<Link href={`/component/`}>
|
|
85
|
+
<ComponentCard title="" videoSrc={getComp("details")?.video} />
|
|
86
|
+
</Link>
|
|
87
|
+
|
|
88
|
+
{/* 6. Neubrutal Button */}
|
|
89
|
+
<Link href={`/component/neubrutal-button`}>
|
|
90
|
+
<ComponentCard
|
|
91
|
+
title="Neubrutal Button"
|
|
92
|
+
videoSrc={getComp("neubrutal-button")?.video || "/compVideos/neubrutal-button.mp4"}
|
|
93
|
+
/>
|
|
94
|
+
</Link>
|
|
95
|
+
|
|
96
|
+
{/* 7. Creative Layouts */}
|
|
97
|
+
<Link href={`/component/`} className="lg:col-span-3">
|
|
98
|
+
<ComponentCard title="" videoSrc={getComp("creative-grid")?.video} />
|
|
99
|
+
</Link>
|
|
100
|
+
|
|
101
|
+
{/* 8. User Feedback */}
|
|
102
|
+
<Link href={`/component/`}>
|
|
103
|
+
<ComponentCard title="" videoSrc={getComp("feedback")?.video}>
|
|
104
|
+
<div className="text-2xl font-bold opacity-50 transition-opacity">?</div>
|
|
105
|
+
</ComponentCard>
|
|
106
|
+
</Link>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
{/* Explore Button */}
|
|
110
|
+
<div className='mt-8 flex justify-center'>
|
|
111
|
+
<Link
|
|
112
|
+
href='/component'
|
|
113
|
+
className='relative z-30 h-12 px-6 rounded-lg text-sm text-white transition-all flex items-center gap-2 group'
|
|
114
|
+
>
|
|
115
|
+
Explore All Components
|
|
116
|
+
<div className='flex items-center justify-center transition-transform group-hover:translate-x-1'>
|
|
117
|
+
<ChevronRight size={18} />
|
|
118
|
+
</div>
|
|
119
|
+
</Link>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export default FeaturedComponents;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
const Footer = () => {
|
|
4
|
+
return (
|
|
5
|
+
<footer className=' text-white border-neutral-800 overflow-hidden relative'>
|
|
6
|
+
{/* Top Full-Width Logo */}
|
|
7
|
+
<div className='w-full'>
|
|
8
|
+
<img
|
|
9
|
+
src='/images/slash_1.svg'
|
|
10
|
+
alt='Slash Logo'
|
|
11
|
+
className='w-full h-auto object-cover block '
|
|
12
|
+
/>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
{/* Main CTA Section with Background Glow */}
|
|
16
|
+
<div className='relative max-w-7xl mx-auto pt-20 pb-40 px-4 sm:px-6 lg:px-8 overflow-hidden'>
|
|
17
|
+
{/* Subtle Background Radial Glow */}
|
|
18
|
+
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[500px] h-[300px] bg-zinc-800/20 blur-[120px] rounded-full -z-10 pointer-events-none" />
|
|
19
|
+
|
|
20
|
+
<div className="relative z-10 overflow-hidden text-center">
|
|
21
|
+
<h1 className='text-center text-4xl md:text-6xl font-switzer capitalize font-bold tracking-tighter leading-tight text-neutral-100 overflow-hidden'>
|
|
22
|
+
Start slashing your UI <br />
|
|
23
|
+
development with <span className='italic font-hoshiko text-red-500 tracking-wider px-5'>Slash/Ui</span> today!
|
|
24
|
+
</h1>
|
|
25
|
+
|
|
26
|
+
<div className="flex justify-center mt-10 overflow-x-hidden">
|
|
27
|
+
<button className='group relative px-15 py-4 tracking-widest overflow-hidden border text-white hover:text-black font-hoshiko rounded-full font-semibold transition-all duration-300 hover:bg-red-500 hover:scale-105 active:scale-95 cursor-pointer flex items-center gap-2'>
|
|
28
|
+
Slash Now
|
|
29
|
+
<span className="transition-transform duration-300 group-hover:translate-x-1"></span>
|
|
30
|
+
</button>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
{/* Bottom Credit Section */}
|
|
36
|
+
<div className='py-8 border-t border-neutral-900'>
|
|
37
|
+
<div className="max-w-7xl mx-auto px-6 flex flex-col md:flex-row justify-between items-center gap-4">
|
|
38
|
+
<p className='text-sm font-cartographCF text-neutral-500 capitalize'>
|
|
39
|
+
© 2026 Slash/Ui. All Rights Reserved.
|
|
40
|
+
</p>
|
|
41
|
+
|
|
42
|
+
<h1 className='text-sm font-cartographCF text-neutral-500'>
|
|
43
|
+
Designed and Developed by{' '}
|
|
44
|
+
<a
|
|
45
|
+
className='italic text-neutral-300 hover:text-white transition-colors duration-300 border-b border-neutral-700 hover:border-white'
|
|
46
|
+
href='https://x.com/rahulll_parihar'
|
|
47
|
+
target='_blank'
|
|
48
|
+
rel='noopener noreferrer'
|
|
49
|
+
>
|
|
50
|
+
@Rahul
|
|
51
|
+
</a>
|
|
52
|
+
</h1>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</footer>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export default Footer;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { JSX, useState } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { ChevronRight, Terminal, Copy, Check } from 'lucide-react';
|
|
6
|
+
|
|
7
|
+
const Hero = (): JSX.Element => {
|
|
8
|
+
const [copied, setCopied] = useState<boolean>(false);
|
|
9
|
+
const installCommand: string = 'npx slash-ui@latest init';
|
|
10
|
+
|
|
11
|
+
const handleCopy = async (): Promise<void> => {
|
|
12
|
+
try {
|
|
13
|
+
await navigator.clipboard.writeText(installCommand);
|
|
14
|
+
setCopied(true);
|
|
15
|
+
setTimeout(() => setCopied(false), 2000);
|
|
16
|
+
} catch (err) {
|
|
17
|
+
console.error('Failed to copy text: ', err);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<section className='relative min-h-screen w-full flex flex-col items-center justify-center overflow-hidden '>
|
|
23
|
+
{/* --- VIDEO BACKGROUND --- */}
|
|
24
|
+
<video
|
|
25
|
+
autoPlay
|
|
26
|
+
muted
|
|
27
|
+
loop
|
|
28
|
+
playsInline
|
|
29
|
+
className='absolute inset-0 z-0 h-full w-full object-cover mix-blend-screen grayscale-100'
|
|
30
|
+
>
|
|
31
|
+
<source src='/video/hero_video.mp4' type='video/mp4' />
|
|
32
|
+
Your browser does not support the video tag.
|
|
33
|
+
</video>
|
|
34
|
+
|
|
35
|
+
<div className='relative z-10 container mx-auto px-6 flex mt-40 flex-col items-center'>
|
|
36
|
+
<h1 className='max-w-4xl text-center text-3xl font-beVietnamPro tracking-tight md:text-5xl lg:text-8xl text-white'>
|
|
37
|
+
SLASH/UI <br />
|
|
38
|
+
<span className='font-sans italic text-6xl md:text-8xl text-zinc-200'>
|
|
39
|
+
components
|
|
40
|
+
</span>
|
|
41
|
+
</h1>
|
|
42
|
+
|
|
43
|
+
<p className='mt-8 max-w-2xl text-center font-cartographCF text-zinc-400 leading-relaxed md:text-sm'>
|
|
44
|
+
A collection of accessible, high-performance components built with
|
|
45
|
+
React and Tailwind. Stop styling from scratch and start building.
|
|
46
|
+
</p>
|
|
47
|
+
|
|
48
|
+
<div className='mt-10 flex flex-col items-center gap-4 sm:flex-row font-cartographCF'>
|
|
49
|
+
<Link
|
|
50
|
+
href='/component'
|
|
51
|
+
className='h-14 px-8 rounded-xl text-sm bg-white text-black font-bold hover:bg-zinc-200 transition-all flex items-center gap-4 group'
|
|
52
|
+
>
|
|
53
|
+
Explore Components
|
|
54
|
+
<ChevronRight
|
|
55
|
+
size={18}
|
|
56
|
+
className='transition-transform group-hover:translate-x-1'
|
|
57
|
+
/>
|
|
58
|
+
</Link>
|
|
59
|
+
|
|
60
|
+
<div
|
|
61
|
+
onClick={handleCopy}
|
|
62
|
+
className='group flex h-14 items-center gap-3 rounded-xl border border-white/10 bg-black/50 backdrop-blur-md px-5 hover:bg-white/10 transition-all cursor-pointer'
|
|
63
|
+
>
|
|
64
|
+
<Terminal size={16} className='text-zinc-500' />
|
|
65
|
+
<code className='text-sm text-zinc-300 font-mono'>
|
|
66
|
+
{installCommand}
|
|
67
|
+
</code>
|
|
68
|
+
<div className='ml-2 border-l border-white/10 pl-4'>
|
|
69
|
+
{copied ? (
|
|
70
|
+
<Check size={16} className='text-green-400' />
|
|
71
|
+
) : (
|
|
72
|
+
<Copy
|
|
73
|
+
size={16}
|
|
74
|
+
className='text-zinc-600 group-hover:text-white'
|
|
75
|
+
/>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</section>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export default Hero;
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { Command } from 'lucide-react';
|
|
6
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
7
|
+
import { useSearch } from '@/hooks/use-component-search';
|
|
8
|
+
import { Slant as Hamburger } from 'hamburger-react';
|
|
9
|
+
import { logout } from '@/lib/actions/auth.action';
|
|
10
|
+
|
|
11
|
+
const Navbar: React.FC = () => {
|
|
12
|
+
const { searchQuery, setSearchQuery, filteredItems, staticPages } = useSearch();
|
|
13
|
+
|
|
14
|
+
const [mounted, setMounted] = useState(false);
|
|
15
|
+
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
|
16
|
+
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
|
17
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
18
|
+
const [userEmail, setUserEmail] = useState<string | null>(null);
|
|
19
|
+
const [authLoaded, setAuthLoaded] = useState(false);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
async function getUser() {
|
|
23
|
+
const res = await fetch('/api/me', {
|
|
24
|
+
credentials: 'include',
|
|
25
|
+
cache: 'no-store',
|
|
26
|
+
});
|
|
27
|
+
const data = await res.json();
|
|
28
|
+
setUserEmail(data.user?.email || null);
|
|
29
|
+
setAuthLoaded(true);
|
|
30
|
+
}
|
|
31
|
+
getUser();
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
setMounted(true);
|
|
36
|
+
|
|
37
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
38
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
39
|
+
e.preventDefault();
|
|
40
|
+
setIsSearchOpen((prev) => !prev);
|
|
41
|
+
setIsMenuOpen(false);
|
|
42
|
+
}
|
|
43
|
+
if (!isSearchOpen) return;
|
|
44
|
+
if (e.key === 'Escape') {
|
|
45
|
+
setIsSearchOpen(false);
|
|
46
|
+
setSearchQuery('');
|
|
47
|
+
}
|
|
48
|
+
if (e.key === 'ArrowDown') {
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
setSelectedIndex((prev) => (prev + 1) % filteredItems.length);
|
|
51
|
+
} else if (e.key === 'ArrowUp') {
|
|
52
|
+
e.preventDefault();
|
|
53
|
+
setSelectedIndex((prev) => (prev - 1 + filteredItems.length) % filteredItems.length);
|
|
54
|
+
} else if (e.key === 'Enter') {
|
|
55
|
+
const selected = filteredItems[selectedIndex];
|
|
56
|
+
if (selected) {
|
|
57
|
+
window.location.href = selected.path;
|
|
58
|
+
setIsSearchOpen(false);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
64
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
65
|
+
}, [isSearchOpen, filteredItems, selectedIndex, setSearchQuery]);
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
setSelectedIndex(0);
|
|
69
|
+
}, [searchQuery]);
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
document.body.style.overflow = isMenuOpen ? 'hidden' : '';
|
|
73
|
+
return () => {
|
|
74
|
+
document.body.style.overflow = '';
|
|
75
|
+
};
|
|
76
|
+
}, [isMenuOpen]);
|
|
77
|
+
|
|
78
|
+
const menuLinks = React.useMemo(() => {
|
|
79
|
+
return [
|
|
80
|
+
{ label: 'Quick Start', path: '/docs', tag: 'Guide' },
|
|
81
|
+
{ label: 'Pricing', path: '/pricing', tag: 'Free' },
|
|
82
|
+
{ label: 'Components', path: '/component', tag: '105+' },
|
|
83
|
+
{ label: 'Get Support', path: '/support', tag: null },
|
|
84
|
+
...(authLoaded
|
|
85
|
+
? userEmail
|
|
86
|
+
? [
|
|
87
|
+
{ label: 'Account', path: '/account', tag: userEmail },
|
|
88
|
+
{ label: 'Logout', path: '#logout', tag: null },
|
|
89
|
+
]
|
|
90
|
+
: [{ label: 'Login', path: '/login', tag: null }]
|
|
91
|
+
: []
|
|
92
|
+
),
|
|
93
|
+
];
|
|
94
|
+
}, [userEmail, authLoaded]);
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<>
|
|
98
|
+
{/* ── NAVBAR ── */}
|
|
99
|
+
<nav className='fixed top-0 left-0 w-full z-[100] flex justify-center pt-4 px-6 pointer-events-none'>
|
|
100
|
+
<div className='flex items-center justify-between px-6 h-14 w-full max-w-[860px] bg-zinc-900/80 backdrop-blur-3xl border border-zinc-800 rounded-2xl pointer-events-auto'>
|
|
101
|
+
{/* Left */}
|
|
102
|
+
<div className='flex items-center gap-8'>
|
|
103
|
+
<Link href='/' className='font-bold text-white text-lg'>
|
|
104
|
+
Slash/Ui
|
|
105
|
+
</Link>
|
|
106
|
+
<div className='hidden md:flex items-center gap-6 text-sm font-medium text-zinc-400'>
|
|
107
|
+
<Link href='/docs' className='hover:text-white transition-colors'>
|
|
108
|
+
Docs
|
|
109
|
+
</Link>
|
|
110
|
+
<Link href='/component' className='hover:text-white transition-colors'>
|
|
111
|
+
Components
|
|
112
|
+
</Link>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
{/* Right */}
|
|
117
|
+
<div className='flex items-center gap-2'>
|
|
118
|
+
<button
|
|
119
|
+
onClick={() => {
|
|
120
|
+
setIsSearchOpen(true);
|
|
121
|
+
setIsMenuOpen(false);
|
|
122
|
+
}}
|
|
123
|
+
className='flex items-center justify-center w-9 h-9 rounded-lg bg-zinc-800 hover:bg-zinc-700 text-zinc-400 hover:text-white transition-all cursor-pointer'
|
|
124
|
+
aria-label='Open search'
|
|
125
|
+
>
|
|
126
|
+
<Command size={16} />
|
|
127
|
+
</button>
|
|
128
|
+
|
|
129
|
+
<button
|
|
130
|
+
onClick={() => {
|
|
131
|
+
setIsMenuOpen((v) => !v);
|
|
132
|
+
setIsSearchOpen(false);
|
|
133
|
+
}}
|
|
134
|
+
className='flex items-center justify-center w-9 h-9 rounded-lg bg-zinc-800 hover:bg-zinc-700 text-zinc-400 hover:text-white transition-all cursor-pointer'
|
|
135
|
+
aria-label='Open menu'
|
|
136
|
+
>
|
|
137
|
+
<AnimatePresence mode='wait' initial={false}>
|
|
138
|
+
{isMenuOpen ? (
|
|
139
|
+
<motion.span key='close'>
|
|
140
|
+
<Hamburger size={16} toggled={isMenuOpen} />
|
|
141
|
+
</motion.span>
|
|
142
|
+
) : (
|
|
143
|
+
<motion.span key='open'>
|
|
144
|
+
<Hamburger size={16} toggled={isMenuOpen} toggle={() => {}} />
|
|
145
|
+
</motion.span>
|
|
146
|
+
)}
|
|
147
|
+
</AnimatePresence>
|
|
148
|
+
</button>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
</nav>
|
|
152
|
+
|
|
153
|
+
{/* ── SLIDE-IN MENU PANEL ── */}
|
|
154
|
+
<AnimatePresence>
|
|
155
|
+
{isMenuOpen && (
|
|
156
|
+
<>
|
|
157
|
+
<motion.div
|
|
158
|
+
initial={{ opacity: 0 }}
|
|
159
|
+
animate={{ opacity: 1 }}
|
|
160
|
+
exit={{ opacity: 0 }}
|
|
161
|
+
transition={{ duration: 0.3 }}
|
|
162
|
+
onClick={() => setIsMenuOpen(false)}
|
|
163
|
+
className='fixed inset-0 z-[150] backdrop-blur-md'
|
|
164
|
+
/>
|
|
165
|
+
|
|
166
|
+
<motion.div
|
|
167
|
+
initial={{ opacity: 0, scale: 1.1 }}
|
|
168
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
169
|
+
exit={{ opacity: 0, scale: 1.1 }}
|
|
170
|
+
transition={{ type: 'spring', damping: 25, stiffness: 200 }}
|
|
171
|
+
className='fixed inset-0 z-[160] flex flex-col justify-center items-center bg-transparent pointer-events-none'
|
|
172
|
+
>
|
|
173
|
+
<nav className='flex flex-col items-center justify-center w-full max-w-4xl pointer-events-auto'>
|
|
174
|
+
{menuLinks.map((link, i) => (
|
|
175
|
+
<motion.div
|
|
176
|
+
key={link.label}
|
|
177
|
+
initial={{ opacity: 0, y: 20 }}
|
|
178
|
+
animate={{ opacity: 1, y: 0 }}
|
|
179
|
+
transition={{ delay: i * 0.05, duration: 0.4 }}
|
|
180
|
+
className='w-full'
|
|
181
|
+
>
|
|
182
|
+
{link.label === 'Logout' ? (
|
|
183
|
+
<form action={logout} className='w-full'>
|
|
184
|
+
<button
|
|
185
|
+
type='submit'
|
|
186
|
+
onClick={() => setIsMenuOpen(false)}
|
|
187
|
+
className='group flex flex-col items-center py-2 w-full cursor-pointer'
|
|
188
|
+
>
|
|
189
|
+
<span className='text-6xl md:text-7xl font-black uppercase tracking-tighter leading-[0.85] text-white transition-transform duration-300 group-hover:scale-105'>
|
|
190
|
+
Logout
|
|
191
|
+
</span>
|
|
192
|
+
{link.tag && (
|
|
193
|
+
<span className='text-[10px] mt-2 opacity-50 tracking-widest'>
|
|
194
|
+
{link.tag}
|
|
195
|
+
</span>
|
|
196
|
+
)}
|
|
197
|
+
</button>
|
|
198
|
+
</form>
|
|
199
|
+
) : (
|
|
200
|
+
<Link
|
|
201
|
+
href={link.path}
|
|
202
|
+
onClick={() => setIsMenuOpen(false)}
|
|
203
|
+
className='group flex flex-col items-center py-2'
|
|
204
|
+
>
|
|
205
|
+
<span className='text-6xl md:text-7xl font-black uppercase tracking-tighter leading-[0.85] text-white transition-transform duration-300 group-hover:scale-105'>
|
|
206
|
+
{link.label}
|
|
207
|
+
</span>
|
|
208
|
+
{link.tag && (
|
|
209
|
+
<span className='text-[10px] mt-2 opacity-50 tracking-widest'>
|
|
210
|
+
{link.tag}
|
|
211
|
+
</span>
|
|
212
|
+
)}
|
|
213
|
+
</Link>
|
|
214
|
+
)}
|
|
215
|
+
</motion.div>
|
|
216
|
+
))}
|
|
217
|
+
</nav>
|
|
218
|
+
|
|
219
|
+
<div className='absolute bottom-10 w-full flex justify-between px-10 text-md font-bold text-zinc-500 uppercase pointer-events-auto'>
|
|
220
|
+
<Link href='/privacy-policy' className='hover:text-white transition-all duration-500'>
|
|
221
|
+
Privacy Policy
|
|
222
|
+
</Link>
|
|
223
|
+
<Link href='/terms-of-service' className='hover:text-white transition-all duration-500'>
|
|
224
|
+
Terms of Service
|
|
225
|
+
</Link>
|
|
226
|
+
</div>
|
|
227
|
+
</motion.div>
|
|
228
|
+
</>
|
|
229
|
+
)}
|
|
230
|
+
</AnimatePresence>
|
|
231
|
+
|
|
232
|
+
{/* ── SEARCH MODAL ── */}
|
|
233
|
+
<AnimatePresence>
|
|
234
|
+
{isSearchOpen && (
|
|
235
|
+
<div className='fixed inset-0 z-[200] flex items-start justify-center pt-[18vh] px-4'>
|
|
236
|
+
<motion.div
|
|
237
|
+
initial={{ opacity: 0 }}
|
|
238
|
+
animate={{ opacity: 1 }}
|
|
239
|
+
exit={{ opacity: 0 }}
|
|
240
|
+
onClick={() => setIsSearchOpen(false)}
|
|
241
|
+
className='absolute inset-0 bg-black/60 backdrop-blur-sm'
|
|
242
|
+
/>
|
|
243
|
+
<motion.div
|
|
244
|
+
initial={{ opacity: 0, scale: 0.95, y: -20 }}
|
|
245
|
+
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
246
|
+
exit={{ opacity: 0, scale: 0.95, y: -20 }}
|
|
247
|
+
className='relative w-full max-w-[600px] bg-zinc-950 border border-zinc-800 rounded-xl shadow-2xl overflow-hidden'
|
|
248
|
+
>
|
|
249
|
+
<div className='flex items-center px-4 border-b border-zinc-800'>
|
|
250
|
+
<Command className='text-zinc-500' size={18} />
|
|
251
|
+
<input
|
|
252
|
+
autoFocus
|
|
253
|
+
value={searchQuery}
|
|
254
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
255
|
+
placeholder='Search components or pages...'
|
|
256
|
+
className='w-full h-14 bg-transparent border-none outline-none px-4 text-white text-sm placeholder:text-zinc-600'
|
|
257
|
+
/>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<div className='max-h-[400px] overflow-y-auto p-2 custom-scrollbar'>
|
|
261
|
+
{searchQuery.length > 0 ? (
|
|
262
|
+
<div className='p-2'>
|
|
263
|
+
<p className='px-3 py-2 text-[10px] font-semibold text-zinc-500 uppercase tracking-wider'>
|
|
264
|
+
Results
|
|
265
|
+
</p>
|
|
266
|
+
{filteredItems.length > 0 ? (
|
|
267
|
+
filteredItems.map((item) => (
|
|
268
|
+
<Link
|
|
269
|
+
key={item.path}
|
|
270
|
+
href={item.path}
|
|
271
|
+
onClick={() => {
|
|
272
|
+
setIsSearchOpen(false);
|
|
273
|
+
setSearchQuery('');
|
|
274
|
+
}}
|
|
275
|
+
className='flex items-center justify-between px-3 py-2.5 rounded-lg hover:bg-white/5 transition-all group'
|
|
276
|
+
>
|
|
277
|
+
<div className='flex items-center gap-3'>
|
|
278
|
+
<item.icon size={16} className='text-zinc-500 group-hover:text-white' />
|
|
279
|
+
<span className='text-sm text-zinc-300 group-hover:text-white'>
|
|
280
|
+
{item.label}
|
|
281
|
+
</span>
|
|
282
|
+
</div>
|
|
283
|
+
<span className='text-[10px] opacity-50 uppercase tracking-widest'>
|
|
284
|
+
{item.category}
|
|
285
|
+
</span>
|
|
286
|
+
</Link>
|
|
287
|
+
))
|
|
288
|
+
) : (
|
|
289
|
+
<p className='px-3 py-4 text-sm text-zinc-600'>No results found...</p>
|
|
290
|
+
)}
|
|
291
|
+
</div>
|
|
292
|
+
) : (
|
|
293
|
+
['Pages', 'Get Started'].map((cat) => (
|
|
294
|
+
<div key={cat} className='mb-2'>
|
|
295
|
+
<p className='px-3 py-2 text-[10px] font-semibold text-zinc-500 uppercase tracking-wider'>
|
|
296
|
+
{cat}
|
|
297
|
+
</p>
|
|
298
|
+
{staticPages
|
|
299
|
+
.filter((p) => p.category === cat)
|
|
300
|
+
.map((p) => (
|
|
301
|
+
<Link
|
|
302
|
+
key={p.label}
|
|
303
|
+
href={p.path}
|
|
304
|
+
onClick={() => setIsSearchOpen(false)}
|
|
305
|
+
className='flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-white/5 transition-all group'
|
|
306
|
+
>
|
|
307
|
+
<div className='text-zinc-500 group-hover:text-white'>
|
|
308
|
+
<p.icon size={16} />
|
|
309
|
+
</div>
|
|
310
|
+
<span className='text-sm text-zinc-300 group-hover:text-white'>
|
|
311
|
+
{p.label}
|
|
312
|
+
</span>
|
|
313
|
+
</Link>
|
|
314
|
+
))}
|
|
315
|
+
</div>
|
|
316
|
+
))
|
|
317
|
+
)}
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
<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'>
|
|
321
|
+
<div className='flex gap-3'>
|
|
322
|
+
<span className='flex items-center gap-1'>
|
|
323
|
+
<Command size={10} /> to select
|
|
324
|
+
</span>
|
|
325
|
+
<span className='flex items-center gap-1'>Enter to open</span>
|
|
326
|
+
</div>
|
|
327
|
+
<span>ESC to close</span>
|
|
328
|
+
</div>
|
|
329
|
+
</motion.div>
|
|
330
|
+
</div>
|
|
331
|
+
)}
|
|
332
|
+
</AnimatePresence>
|
|
333
|
+
</>
|
|
334
|
+
);
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
export default Navbar;
|