@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,146 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
MousePointer2,
|
|
6
|
+
Zap,
|
|
7
|
+
Copy,
|
|
8
|
+
Check,
|
|
9
|
+
ShieldCheck,
|
|
10
|
+
} from 'lucide-react';
|
|
11
|
+
|
|
12
|
+
const NeubrutalButtonDetails = () => {
|
|
13
|
+
const [copied, setCopied] = useState(false);
|
|
14
|
+
const [activeTab, setActiveTab] = useState<string | null>(null);
|
|
15
|
+
|
|
16
|
+
const command = 'npx slash-ui@latest add neubrutal-button';
|
|
17
|
+
|
|
18
|
+
const handleCopyCommand = () => {
|
|
19
|
+
navigator.clipboard.writeText(command);
|
|
20
|
+
setCopied(true);
|
|
21
|
+
setTimeout(() => setCopied(false), 2000);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const copyDependency = (pkg: string) => {
|
|
25
|
+
navigator.clipboard.writeText(`npm install ${pkg}`);
|
|
26
|
+
setActiveTab(pkg);
|
|
27
|
+
setTimeout(() => setActiveTab(null), 2000);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className='flex flex-col gap-16 py-8 animate-in fade-in slide-in-from-bottom-4 duration-1000'>
|
|
32
|
+
{/* 1. Description */}
|
|
33
|
+
<section>
|
|
34
|
+
<h4 className='text-sm uppercase text-zinc-600 mb-6 font-mono tracking-widest'>Description</h4>
|
|
35
|
+
<p className='text-zinc-400 text-lg leading-relaxed max-w-2xl font-medium'>
|
|
36
|
+
A high-performance 3D button built with Framer Motion. It features
|
|
37
|
+
tactile depth, dynamic shadow scaling, and smooth state transitions.
|
|
38
|
+
</p>
|
|
39
|
+
</section>
|
|
40
|
+
|
|
41
|
+
{/* 2. Dependencies */}
|
|
42
|
+
<section>
|
|
43
|
+
<h4 className='text-sm uppercase text-zinc-600 mb-4 font-mono tracking-widest'>Dependencies</h4>
|
|
44
|
+
<div className='flex items-center gap-3'>
|
|
45
|
+
<button
|
|
46
|
+
onClick={() => copyDependency('framer-motion')}
|
|
47
|
+
className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-zinc-950 border border-white/5 hover:border-white/20 transition-all active:scale-95"
|
|
48
|
+
>
|
|
49
|
+
{activeTab === 'framer-motion' ? (
|
|
50
|
+
<Check size={12} className="text-white" />
|
|
51
|
+
) : (
|
|
52
|
+
<svg width='12' height='12' viewBox='0 0 24 24' fill='currentColor' className="text-white">
|
|
53
|
+
<path d='M0 0l12 12L24 0H0zm0 12l12 12V12H0z' />
|
|
54
|
+
</svg>
|
|
55
|
+
)}
|
|
56
|
+
<span className="text-[11px] font-mono text-white">
|
|
57
|
+
{activeTab === 'framer-motion' ? 'Copied!' : 'framer-motion'}
|
|
58
|
+
</span>
|
|
59
|
+
</button>
|
|
60
|
+
|
|
61
|
+
<button
|
|
62
|
+
onClick={() => copyDependency('lucide-react')}
|
|
63
|
+
className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-zinc-950 border border-white/5 hover:border-white/20 transition-all active:scale-95"
|
|
64
|
+
>
|
|
65
|
+
{activeTab === 'lucide-react' ? (
|
|
66
|
+
<Check size={12} className="text-white" />
|
|
67
|
+
) : (
|
|
68
|
+
<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='currentColor' strokeWidth='2'>
|
|
69
|
+
<path d='M14 12C14 9.79086 12.2091 8 10 8C7.79086 8 6 9.79086 6 12C6 16.4183 9.58172 20 14 20C18.4183 20 22 16.4183 22 12C22 8.446 20.455 5.25285 18 3.05557' stroke='#fff' />
|
|
70
|
+
<path d='M10 12C10 14.2091 11.7909 16 14 16C16.2091 16 18 14.2091 18 12C18 7.58172 14.4183 4 10 4C5.58172 4 2 7.58172 2 12C2 15.5841 3.57127 18.8012 6.06253 21' stroke='#fff' />
|
|
71
|
+
</svg>
|
|
72
|
+
)}
|
|
73
|
+
<span className="text-[11px] font-mono text-white">
|
|
74
|
+
{activeTab === 'lucide-react' ? 'Copied!' : 'lucide-react'}
|
|
75
|
+
</span>
|
|
76
|
+
</button>
|
|
77
|
+
</div>
|
|
78
|
+
</section>
|
|
79
|
+
|
|
80
|
+
{/* 3. Interaction Type */}
|
|
81
|
+
<section>
|
|
82
|
+
<h4 className='text-sm uppercase text-zinc-600 mb-6 font-mono tracking-widest'>Interaction</h4>
|
|
83
|
+
<div className='space-y-4'>
|
|
84
|
+
<div className='flex items-center gap-4 group'>
|
|
85
|
+
<div className='p-2 rounded-lg bg-white/5 text-zinc-500 group-hover:text-white transition-colors'>
|
|
86
|
+
<MousePointer2 size={16} />
|
|
87
|
+
</div>
|
|
88
|
+
<span className='text-sm font-medium text-zinc-400'>Realistic 3D depth on click</span>
|
|
89
|
+
</div>
|
|
90
|
+
<div className='flex items-center gap-4 group'>
|
|
91
|
+
<div className='p-2 rounded-lg bg-white/5 text-zinc-500 group-hover:text-white transition-colors'>
|
|
92
|
+
<Zap size={16} />
|
|
93
|
+
</div>
|
|
94
|
+
<span className='text-sm font-medium text-zinc-400'>Smooth hover effects</span>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</section>
|
|
98
|
+
|
|
99
|
+
{/* 4. Installation CLI */}
|
|
100
|
+
<section>
|
|
101
|
+
<h4 className='text-sm uppercase text-zinc-600 mb-6 font-mono tracking-widest'>Installation</h4>
|
|
102
|
+
<div
|
|
103
|
+
onClick={handleCopyCommand}
|
|
104
|
+
className='group relative flex items-center justify-between p-5 rounded-2xl bg-black border border-white/10 hover:border-white/20 transition-all cursor-pointer active:scale-[0.99]'
|
|
105
|
+
>
|
|
106
|
+
<code className='text-xs font-cartographCF text-zinc-300'>
|
|
107
|
+
<span className='text-zinc-500'>npx</span> slash-ui@latest add <span className='text-white'>neubrutal-button</span>
|
|
108
|
+
</code>
|
|
109
|
+
<div className='p-2 hover:bg-white/5 rounded-lg text-zinc-500 group-hover:text-white transition-colors'>
|
|
110
|
+
{copied ? <Check size={14} className="text-white" /> : <Copy size={14} />}
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
</section>
|
|
114
|
+
|
|
115
|
+
{/* 5. Props */}
|
|
116
|
+
<section>
|
|
117
|
+
<h4 className='text-sm uppercase text-zinc-600 mb-6 font-mono tracking-widest'>Props</h4>
|
|
118
|
+
<div className='w-full border-t border-white/5'>
|
|
119
|
+
{[
|
|
120
|
+
{ prop: 'children', desc: 'Button label or content' },
|
|
121
|
+
{ prop: 'onClick', desc: 'Click handler function' },
|
|
122
|
+
{ prop: 'className', desc: 'Additional Tailwind classes' },
|
|
123
|
+
].map((item, i) => (
|
|
124
|
+
<div key={i} className='flex py-4 border-b border-white/5 text-[13px] group'>
|
|
125
|
+
<span className='w-1/3 font-mono font-bold text-white'>{item.prop}</span>
|
|
126
|
+
<span className='w-2/3 text-zinc-500 font-medium group-hover:text-zinc-400 transition-colors'>{item.desc}</span>
|
|
127
|
+
</div>
|
|
128
|
+
))}
|
|
129
|
+
</div>
|
|
130
|
+
</section>
|
|
131
|
+
|
|
132
|
+
{/* 6. Footer */}
|
|
133
|
+
<footer className='pt-8 border-t border-white/5'>
|
|
134
|
+
<div className='flex items-center gap-2 text-zinc-600 text-[11px] font-bold uppercase tracking-widest font-mono'>
|
|
135
|
+
<ShieldCheck size={14} />
|
|
136
|
+
<span>License</span>
|
|
137
|
+
</div>
|
|
138
|
+
<p className='mt-4 text-xs text-zinc-500 leading-relaxed font-medium'>
|
|
139
|
+
Free for all projects.
|
|
140
|
+
</p>
|
|
141
|
+
</footer>
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export default NeubrutalButtonDetails;
|
|
File without changes
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// registry/index.ts
|
|
2
|
+
|
|
3
|
+
export const Index = [
|
|
4
|
+
{
|
|
5
|
+
name: 'neubrutal-button',
|
|
6
|
+
type: 'ui',
|
|
7
|
+
files: ['ui/buttons/neubrutal-button.tsx'],
|
|
8
|
+
category: 'buttons',
|
|
9
|
+
install: 'npm install framer-motion lucide-react',
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
{
|
|
13
|
+
name: 'dot-cursor',
|
|
14
|
+
type: 'ui',
|
|
15
|
+
files: ['ui/cursors/dot-cursor.tsx'],
|
|
16
|
+
category: 'cursors',
|
|
17
|
+
install: 'npm install framer-motion',
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
{
|
|
21
|
+
name: 'flaoting-navbar',
|
|
22
|
+
type: 'ui',
|
|
23
|
+
files: ['ui/navbars/floating-navbar.tsx'],
|
|
24
|
+
category: 'navbars',
|
|
25
|
+
install: 'npm install lucide-react framer-motion gsap',
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
{
|
|
29
|
+
name: 'minimal-scrollbar',
|
|
30
|
+
type: 'ui',
|
|
31
|
+
files: ['ui/scrollbars/minimal-scrollbar.tsx'],
|
|
32
|
+
category: 'scrollbars',
|
|
33
|
+
install: '',
|
|
34
|
+
}
|
|
35
|
+
];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { cn } from '@/lib/utils';
|
|
3
|
+
|
|
4
|
+
interface NeubrutalButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// 1. CHANGE THIS TO DEFAULT EXPORT
|
|
9
|
+
export default function NeubrutalButton({
|
|
10
|
+
children = 'Click Me!',
|
|
11
|
+
className,
|
|
12
|
+
...props
|
|
13
|
+
}: NeubrutalButtonProps) {
|
|
14
|
+
return (
|
|
15
|
+
<button
|
|
16
|
+
{...props}
|
|
17
|
+
className={cn(
|
|
18
|
+
'group relative w-[140px] h-[50px] bg-none outline-none border-none p-0 cursor-pointer active:translate-y-[2px] transition-transform',
|
|
19
|
+
className,
|
|
20
|
+
)}
|
|
21
|
+
>
|
|
22
|
+
<div className='absolute top-[14px] -left-[1px] w-[calc(100%+2px)] h-full bg-[#8c8c8c] rounded-[7mm] outline outline-2 outline-[#242622] -z-10' />
|
|
23
|
+
<div className='absolute top-[10px] left-0 w-full h-full bg-[#e5e5c7] rounded-[7mm] outline outline-2 outline-[#242622] -z-10'>
|
|
24
|
+
<div className='absolute bottom-0 left-[15%] w-[2px] h-[9px] bg-[#242622]' />
|
|
25
|
+
<div className='absolute bottom-0 left-[85%] w-[2px] h-[9px] bg-[#242622]' />
|
|
26
|
+
</div>
|
|
27
|
+
<div className='relative w-full h-full flex items-center justify-center bg-[#ffffee] rounded-[7mm] outline outline-2 outline-[#242622] text-[#242622] font-semibold text-base overflow-hidden transition-all duration-200 group-active:translate-y-[10px]'>
|
|
28
|
+
<div className='absolute top-0 -left-[20px] w-[15px] h-full bg-black/10 skew-x-[30deg] transition-all duration-300 group-active:left-[calc(100%+20px)]' />
|
|
29
|
+
{children}
|
|
30
|
+
</div>
|
|
31
|
+
</button>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -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;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
6
|
+
import { Sun, Moon } from 'lucide-react';
|
|
7
|
+
|
|
8
|
+
const Navbar = () => {
|
|
9
|
+
const [mounted, setMounted] = useState(false);
|
|
10
|
+
const [isDark, setIsDark] = useState(true);
|
|
11
|
+
const [scrolled, setScrolled] = useState(false);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
setMounted(true);
|
|
15
|
+
const handleScroll = () => setScrolled(window.scrollY > 20);
|
|
16
|
+
window.addEventListener('scroll', handleScroll);
|
|
17
|
+
return () => window.removeEventListener('scroll', handleScroll);
|
|
18
|
+
}, []);
|
|
19
|
+
|
|
20
|
+
const toggleTheme = () => {
|
|
21
|
+
setIsDark(!isDark);
|
|
22
|
+
document.documentElement.classList.toggle('dark');
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const navLinks = [
|
|
26
|
+
{ name: 'Archives', slug: 'work' },
|
|
27
|
+
{ name: 'Capabilities', slug: 'service' },
|
|
28
|
+
{ name: 'Ethos', slug: 'about' },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
if (!mounted) return null;
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<nav className='fixed top-0 left-0 w-full z-[100] flex justify-center pt-6 px-6 pointer-events-none mt-10'>
|
|
35
|
+
<motion.div
|
|
36
|
+
initial={{ y: -100 }}
|
|
37
|
+
animate={{ y: 0 }}
|
|
38
|
+
className={`
|
|
39
|
+
flex items-center justify-between px-6 transition-all duration-500 pointer-events-auto
|
|
40
|
+
${
|
|
41
|
+
scrolled
|
|
42
|
+
? 'w-full md:w-[800px] h-14 bg-white/5 backdrop-blur-md border border-white/10 rounded-full shadow-2xl'
|
|
43
|
+
: 'w-full h-20 bg-transparent border-transparent'
|
|
44
|
+
}
|
|
45
|
+
`}
|
|
46
|
+
>
|
|
47
|
+
<Link href='/' className='group flex items-center'>
|
|
48
|
+
<span className=' text-3xl uppercase tracking-wide font-bold'>
|
|
49
|
+
Renoh
|
|
50
|
+
</span>
|
|
51
|
+
</Link>
|
|
52
|
+
|
|
53
|
+
<div className='hidden md:flex items-center space-x-8'>
|
|
54
|
+
{navLinks.map((link) => (
|
|
55
|
+
<Link
|
|
56
|
+
key={link.slug}
|
|
57
|
+
href={`/${link.slug}`}
|
|
58
|
+
className='text-[10px] font-plex uppercase tracking-[0.2em] text-zinc-400 hover:text-white transition-colors relative group'
|
|
59
|
+
>
|
|
60
|
+
{link.name}
|
|
61
|
+
<span className='absolute -bottom-1 left-0 w-0 h-px bg-white transition-all duration-500 group-hover:w-full' />
|
|
62
|
+
</Link>
|
|
63
|
+
))}
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div className='flex items-center space-x-4'>
|
|
67
|
+
<button
|
|
68
|
+
onClick={toggleTheme}
|
|
69
|
+
className='p-2 hover:bg-white/10 rounded-full transition-colors text-white'
|
|
70
|
+
>
|
|
71
|
+
<AnimatePresence mode='wait'>
|
|
72
|
+
<motion.div
|
|
73
|
+
key={isDark ? 'dark' : 'light'}
|
|
74
|
+
initial={{ opacity: 0, rotate: -90 }}
|
|
75
|
+
animate={{ opacity: 1, rotate: 0 }}
|
|
76
|
+
exit={{ opacity: 0, rotate: 90 }}
|
|
77
|
+
transition={{ duration: 0.2 }}
|
|
78
|
+
>
|
|
79
|
+
{isDark ? <Sun size={16} /> : <Moon size={16} />}
|
|
80
|
+
</motion.div>
|
|
81
|
+
</AnimatePresence>
|
|
82
|
+
</button>
|
|
83
|
+
|
|
84
|
+
<Link href='/contact'>
|
|
85
|
+
<motion.div
|
|
86
|
+
whileHover={{ scale: 1.05 }}
|
|
87
|
+
whileTap={{ scale: 0.95 }}
|
|
88
|
+
className='px-4 py-2 bg-white text-black text-[10px] font-plex uppercase tracking-widest rounded-full'
|
|
89
|
+
>
|
|
90
|
+
Inquire
|
|
91
|
+
</motion.div>
|
|
92
|
+
</Link>
|
|
93
|
+
</div>
|
|
94
|
+
</motion.div>
|
|
95
|
+
</nav>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export default Navbar;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
4
|
+
import { usePathname } from 'next/navigation';
|
|
5
|
+
|
|
6
|
+
const VisualScrollbar = () => {
|
|
7
|
+
const [thumbTop, setThumbTop] = useState(0);
|
|
8
|
+
const channelRef = useRef<HTMLDivElement>(null);
|
|
9
|
+
const thumbRef = useRef<HTMLDivElement>(null);
|
|
10
|
+
const tickingRef = useRef(false);
|
|
11
|
+
const measurementsRef = useRef({
|
|
12
|
+
height: 0,
|
|
13
|
+
thumbH: 0,
|
|
14
|
+
paddingTop: 0,
|
|
15
|
+
paddingBottom: 0,
|
|
16
|
+
available: 0,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Hide scrollbar on contact page
|
|
20
|
+
const pathname = usePathname();
|
|
21
|
+
const shouldHide = pathname === '/contact';
|
|
22
|
+
|
|
23
|
+
const measure = useCallback(() => {
|
|
24
|
+
if (!channelRef.current || !thumbRef.current) return;
|
|
25
|
+
|
|
26
|
+
const chRect = channelRef.current.getBoundingClientRect();
|
|
27
|
+
const thumbRect = thumbRef.current.getBoundingClientRect();
|
|
28
|
+
const style = window.getComputedStyle(channelRef.current);
|
|
29
|
+
|
|
30
|
+
const height = chRect.height;
|
|
31
|
+
const thumbH = thumbRect.height;
|
|
32
|
+
const paddingTop = parseFloat(style.paddingTop) || 0;
|
|
33
|
+
const paddingBottom = parseFloat(style.paddingBottom) || 0;
|
|
34
|
+
const available = Math.max(
|
|
35
|
+
height - thumbH - (paddingTop + paddingBottom),
|
|
36
|
+
0
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
measurementsRef.current = {
|
|
40
|
+
height,
|
|
41
|
+
thumbH,
|
|
42
|
+
paddingTop,
|
|
43
|
+
paddingBottom,
|
|
44
|
+
available,
|
|
45
|
+
};
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
const updateThumb = useCallback(() => {
|
|
49
|
+
const scrollTop = window.scrollY;
|
|
50
|
+
const scrollHeight = document.documentElement.scrollHeight;
|
|
51
|
+
const clientHeight = document.documentElement.clientHeight;
|
|
52
|
+
const maxScroll = Math.max(scrollHeight - clientHeight, 1);
|
|
53
|
+
const scrollPercent = Math.min(Math.max(scrollTop / maxScroll, 0), 1);
|
|
54
|
+
const topPx =
|
|
55
|
+
measurementsRef.current.paddingTop +
|
|
56
|
+
scrollPercent * measurementsRef.current.available;
|
|
57
|
+
|
|
58
|
+
setThumbTop(topPx);
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
const handleScroll = () => {
|
|
63
|
+
if (!tickingRef.current) {
|
|
64
|
+
tickingRef.current = true;
|
|
65
|
+
requestAnimationFrame(() => {
|
|
66
|
+
updateThumb();
|
|
67
|
+
tickingRef.current = false;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
let resizeTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
73
|
+
const handleResize = () => {
|
|
74
|
+
if (resizeTimeout) clearTimeout(resizeTimeout);
|
|
75
|
+
resizeTimeout = setTimeout(() => {
|
|
76
|
+
measure();
|
|
77
|
+
updateThumb();
|
|
78
|
+
}, 150);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Initial setup with small delay to ensure DOM is ready
|
|
82
|
+
const initTimeout = setTimeout(() => {
|
|
83
|
+
measure();
|
|
84
|
+
updateThumb();
|
|
85
|
+
}, 100);
|
|
86
|
+
|
|
87
|
+
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
88
|
+
window.addEventListener('resize', handleResize, { passive: true });
|
|
89
|
+
|
|
90
|
+
return () => {
|
|
91
|
+
clearTimeout(initTimeout);
|
|
92
|
+
if (resizeTimeout) clearTimeout(resizeTimeout);
|
|
93
|
+
window.removeEventListener('scroll', handleScroll);
|
|
94
|
+
window.removeEventListener('resize', handleResize);
|
|
95
|
+
};
|
|
96
|
+
}, [measure, updateThumb]);
|
|
97
|
+
|
|
98
|
+
// Return null if on contact page
|
|
99
|
+
if (shouldHide) return null;
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<>
|
|
103
|
+
<style>{`
|
|
104
|
+
:root {
|
|
105
|
+
--left-offset: 20px;
|
|
106
|
+
--track-h: 50vh;
|
|
107
|
+
--track-w: 3px;
|
|
108
|
+
--thumb-w: 2px;
|
|
109
|
+
--thumb-h: 50px;
|
|
110
|
+
--track-bg: #212121;
|
|
111
|
+
--channel-bg: rgba(255,255,255,0.15);
|
|
112
|
+
--thumb-gradient: linear-gradient(180deg,#ffffff,#d7d7d7);
|
|
113
|
+
--track-radius: 0px;
|
|
114
|
+
--thumb-radius: 0px;
|
|
115
|
+
--transition-speed: 1ms;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
* {
|
|
119
|
+
box-sizing: border-box;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
body {
|
|
123
|
+
background: #0a0a0a;
|
|
124
|
+
color: #e6e6e6;
|
|
125
|
+
margin: 0;
|
|
126
|
+
padding: 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.vs {
|
|
130
|
+
position: fixed;
|
|
131
|
+
left: var(--left-offset);
|
|
132
|
+
top: 50%;
|
|
133
|
+
transform: translateY(-50%);
|
|
134
|
+
width: var(--track-w);
|
|
135
|
+
height: var(--track-h);
|
|
136
|
+
background: var(--track-bg);
|
|
137
|
+
border-radius: var(--track-radius);
|
|
138
|
+
overflow: hidden;
|
|
139
|
+
box-shadow:
|
|
140
|
+
inset 0 1px 0 rgba(255,255,255,0.08),
|
|
141
|
+
0 10px 24px rgba(0,0,0,0.6);
|
|
142
|
+
backdrop-filter: blur(6px);
|
|
143
|
+
pointer-events: none;
|
|
144
|
+
z-index: 9999;
|
|
145
|
+
display: flex;
|
|
146
|
+
justify-content: center;
|
|
147
|
+
will-change: transform;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.vs__channel {
|
|
151
|
+
position: relative;
|
|
152
|
+
width: 100%;
|
|
153
|
+
height: 100%;
|
|
154
|
+
background: var(--channel-bg);
|
|
155
|
+
border-radius: var(--track-radius);
|
|
156
|
+
overflow: hidden;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.vs__thumb {
|
|
160
|
+
position: absolute;
|
|
161
|
+
left: 50%;
|
|
162
|
+
transform: translateX(-50%) translateZ(0);
|
|
163
|
+
width: var(--thumb-w);
|
|
164
|
+
height: var(--thumb-h);
|
|
165
|
+
border-radius: var(--thumb-radius);
|
|
166
|
+
background: var(--thumb-gradient);
|
|
167
|
+
box-shadow:
|
|
168
|
+
0 4px 14px rgba(0,0,0,0.6),
|
|
169
|
+
inset 0 1px 0 rgba(255,255,255,0.4);
|
|
170
|
+
will-change: top;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.vs:hover .vs__thumb {
|
|
174
|
+
transform: translateX(-50%) translateZ(0) scaleY(1.05);
|
|
175
|
+
transition: transform 180ms ease;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
@media (max-width:720px) {
|
|
179
|
+
:root {
|
|
180
|
+
--left-offset: 10px;
|
|
181
|
+
--thumb-h: 30px;
|
|
182
|
+
--track-w: 3px;
|
|
183
|
+
--track-h: 30vh;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
`}</style>
|
|
187
|
+
|
|
188
|
+
{/* Visual Scrollbar */}
|
|
189
|
+
<div className='vs'>
|
|
190
|
+
<div className='vs__channel' ref={channelRef}>
|
|
191
|
+
<div
|
|
192
|
+
className='vs__thumb'
|
|
193
|
+
ref={thumbRef}
|
|
194
|
+
style={{ top: `${thumbTop}px` }}
|
|
195
|
+
aria-hidden='true'
|
|
196
|
+
/>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
</>
|
|
200
|
+
);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
export default VisualScrollbar;
|