Compare commits

..

2 Commits

Author SHA1 Message Date
1d4584e5d9 fixes 2026-02-01 22:34:47 +01:00
6228945065 Added Silk component for animated background in Hero section 2026-02-01 16:59:54 +01:00
8 changed files with 2537 additions and 210 deletions

1879
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -41,6 +41,8 @@
"@radix-ui/react-toggle": "^1.1.9",
"@radix-ui/react-toggle-group": "^1.1.10",
"@radix-ui/react-tooltip": "^1.2.7",
"@react-three/fiber": "^8.18.0",
"@tabler/icons-react": "^3.36.1",
"@tanstack/react-query": "^5.83.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -49,6 +51,7 @@
"embla-carousel-react": "^8.6.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.462.0",
"motion": "^12.29.2",
"next-themes": "^0.3.0",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
@@ -60,14 +63,15 @@
"sonner": "^1.7.4",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"three": "^0.182.0",
"vaul": "^0.9.9",
"zod": "^3.25.76"
},
"devDependencies": {
"@eslint/js": "^9.32.0",
"@tailwindcss/typography": "^0.5.16",
"@testing-library/jest-dom": "^6.6.0",
"@testing-library/react": "^16.0.0",
"@tailwindcss/typography": "^0.5.16",
"@types/node": "^22.16.5",
"@types/react": "^18.3.23",
"@types/react-dom": "^18.3.7",

View File

@@ -1,105 +1,97 @@
const Footer = () => {
const currentYear = new Date().getFullYear();
const links = {
services: [
{ label: "Strategieberatung", href: "#services" },
{ label: "UX/UI Design", href: "#services" },
{ label: "Entwicklung", href: "#services" },
{ label: "SEO & Support", href: "#services" },
],
company: [
{ label: "Über uns", href: "#about" },
{ label: "Projekte", href: "#projects" },
{ label: "Ablauf", href: "#process" },
{ label: "Kontakt", href: "#contact" },
],
legal: [
{ label: "Impressum", href: "/impressum" },
{ label: "Datenschutz", href: "/datenschutz" },
{ label: "AGB", href: "/agb" },
],
};
import React from 'react';
const DevStudio: React.FC = () => {
return (
<footer className="bg-secondary/20 border-t border-border relative">
<div className="container mx-auto px-6 py-16">
<div className="grid md:grid-cols-4 gap-12 mb-16">
{/* Logo & Description */}
<div className="md:col-span-1">
<span className="text-xl font-display font-medium text-foreground tracking-tight mb-6 block">
webklar
</span>
<p className="text-muted-foreground text-sm leading-relaxed">
Maßgeschneiderte Weblösungen für Ihr Unternehmen. Sicher, zuverlässig und modern.
<p className="inset-x-0 mt-20 bg-gradient-to-b from-black via-neutral-950 to-neutral-900 bg-clip-text text-center text-5xl font-bold text-transparent md:text-9xl lg:text-[12rem] xl:text-[13rem]">
WEBklar
</p>
</div>
{/* Services */}
<div>
<h4 className="label-tag mb-6">Leistungen</h4>
<ul className="space-y-3">
{links.services.map((link) => (
<li key={link.label}>
<a
href={link.href}
className="text-muted-foreground hover:text-foreground transition-colors text-sm"
>
{link.label}
</a>
</li>
))}
</ul>
</div>
{/* Company */}
<div>
<h4 className="label-tag mb-6">Unternehmen</h4>
<ul className="space-y-3">
{links.company.map((link) => (
<li key={link.label}>
<a
href={link.href}
className="text-muted-foreground hover:text-foreground transition-colors text-sm"
>
{link.label}
</a>
</li>
))}
</ul>
</div>
{/* Legal */}
<div>
<h4 className="label-tag mb-6">Rechtliches</h4>
<ul className="space-y-3">
{links.legal.map((link) => (
<li key={link.label}>
<a
href={link.href}
className="text-muted-foreground hover:text-foreground transition-colors text-sm"
>
{link.label}
</a>
</li>
))}
</ul>
</div>
</div>
{/* Bottom Bar */}
<div className="divider mb-8" />
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
<p className="text-muted-foreground text-sm">
© {currentYear} webklar. Alle Rechte vorbehalten.
</p>
<div className="text-muted-foreground text-sm">
Made in Germany
</div>
</div>
</div>
</footer>
);
};
const Footer: React.FC = () => {
return (
<div className="relative w-full overflow-hidden border-t border-white/[0.1] bg-black px-8 py-20 dark:border-white/[0.1] dark:bg-black">
<div className="mx-auto flex max-w-7xl flex-col items-start justify-between text-sm text-neutral-500 sm:flex-row md:px-8">
<div>
<div className="mr-0 mb-4 md:mr-4 md:flex">
<a className="relative z-20 mr-4 flex items-center space-x-2 px-2 py-1 text-sm font-normal text-white" href="/">
<img alt="logo" width="30" height="30" src="https://assets.aceternity.com/logo-dark.png" />
<span className="font-medium text-white">WEBklar</span>
</a>
</div>
<div className="mt-2 ml-2">© copyright WEBklar 2024. All rights reserved.</div>
</div>
<div className="mt-10 grid grid-cols-2 items-start gap-10 sm:mt-0 md:mt-0 lg:grid-cols-4">
<div className="flex w-full flex-col justify-center space-y-4">
<p className="hover:text-neutral-300 font-bold text-neutral-600 transition-colors dark:text-neutral-300">Pages</p>
<ul className="hover:text-neutral-300 list-none space-y-4 text-neutral-600 transition-colors dark:text-neutral-300">
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/products">All Products</a>
</li>
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/products">Studio</a>
</li>
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/products">Clients</a>
</li>
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/products">Pricing</a>
</li>
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/products">Blog</a>
</li>
</ul>
</div>
<div className="flex flex-col justify-center space-y-4">
<p className="hover:text-neutral-300 font-bold text-neutral-600 transition-colors dark:text-neutral-300">Socials</p>
<ul className="hover:text-neutral-300 list-none space-y-4 text-neutral-600 transition-colors dark:text-neutral-300">
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/products">Facebook</a>
</li>
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/products">Instagram</a>
</li>
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/products">Twitter</a>
</li>
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/products">LinkedIn</a>
</li>
</ul>
</div>
<div className="flex flex-col justify-center space-y-4">
<p className="hover:text-neutral-300 font-bold text-neutral-600 transition-colors dark:text-neutral-300">Legal</p>
<ul className="hover:text-neutral-300 list-none space-y-4 text-neutral-600 transition-colors dark:text-neutral-300">
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/agb">AGBs</a>
</li>
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/impressum">Impressum</a>
</li>
</ul>
</div>
<div className="flex flex-col justify-center space-y-4">
<p className="hover:text-neutral-300 font-bold text-neutral-600 transition-colors dark:text-neutral-300">Register</p>
<ul className="hover:text-neutral-300 list-none space-y-4 text-neutral-600 transition-colors dark:text-neutral-300">
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/products">Sign Up</a>
</li>
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/products">Login</a>
</li>
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/products">Forgot Password</a>
</li>
</ul>
</div>
</div>
</div>
<p className="inset-x-0 mt-20 bg-gradient-to-b from-black via-neutral-950 to-neutral-900 bg-clip-text text-center text-5xl font-bold text-transparent md:text-9xl lg:text-[12rem] xl:text-[13rem]">
WEBklar
</p>
</div>
);
};
export { DevStudio, Footer };
export default Footer;

View File

@@ -1,104 +1,98 @@
import { useState, useEffect } from "react";
"use client";
import { Link } from "react-router-dom";
import { Menu, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useState } from "react";
import {
Navbar,
NavBody,
NavItems,
MobileNav,
NavbarLogo,
NavbarButton,
MobileNavHeader,
MobileNavToggle,
MobileNavMenu,
} from "@/components/ui/resizable-navbar";
const Header = () => {
const [isScrolled, setIsScrolled] = useState(false);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 20);
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
const navLinks = [
{ href: "#about", label: "Über uns" },
{ href: "#services", label: "Leistungen" },
{ href: "#projects", label: "Projekte" },
{ href: "#process", label: "Ablauf" },
const navItems = [
{ name: "Über uns", link: "#about" },
{ name: "Leistungen", link: "#services" },
{ name: "Projekte", link: "#projects" },
{ name: "Ablauf", link: "#process" },
];
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
return (
<header
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-500 ${
isScrolled
? "glass-nav py-4"
: "bg-transparent py-6"
}`}
>
<div className="container mx-auto px-6">
<div className="flex items-center justify-between">
{/* Logo */}
<a href="#" className="flex items-center gap-2 group">
<span className="text-xl font-display font-medium text-foreground tracking-tight">Webklar</span>
</a>
<div className="relative w-full">
<Navbar>
{/* Desktop Navigation */}
<nav className="hidden md:flex items-center gap-10">
{navLinks.map((link) => (
<a
key={link.href}
href={link.href}
className="text-muted-foreground hover:text-foreground transition-colors duration-300 text-sm font-medium uppercase tracking-wider"
>
{link.label}
</a>
))}
</nav>
{/* CTA Button */}
<div className="hidden md:block">
<NavBody>
<NavbarLogo href="#">
<span className="font-display text-lg font-medium tracking-tight">
Webklar
</span>
</NavbarLogo>
<NavItems items={navItems} />
<div className="navbar-actions flex items-center gap-4">
<Link to="/kontakt">
<Button
className="btn-minimal rounded-full px-6 py-5 text-sm font-medium"
<NavbarButton
as="span"
variant="primary"
className="!text-black"
>
Kontakt
</Button>
</NavbarButton>
</Link>
</div>
</NavBody>
{/* Mobile Menu Button */}
<button
className="md:hidden p-2 text-foreground hover:text-muted-foreground transition-colors"
{/* Mobile Navigation */}
<MobileNav>
<MobileNavHeader>
<NavbarLogo href="#">
<span className="font-display text-lg font-medium tracking-tight">
Webklar
</span>
</NavbarLogo>
<MobileNavToggle
isOpen={isMobileMenuOpen}
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
aria-label="Toggle menu"
>
{isMobileMenuOpen ? (
<X className="w-6 h-6" />
) : (
<Menu className="w-6 h-6" />
)}
</button>
</div>
/>
</MobileNavHeader>
{/* Mobile Menu */}
{isMobileMenuOpen && (
<div className="md:hidden absolute top-full left-0 right-0 bg-background/98 backdrop-blur-xl border-b border-border p-6 animate-fade-in">
<nav className="flex flex-col gap-6">
{navLinks.map((link) => (
<a
key={link.href}
href={link.href}
className="text-foreground hover:text-muted-foreground transition-colors text-lg font-medium uppercase tracking-wider"
onClick={() => setIsMobileMenuOpen(false)}
<MobileNavMenu
isOpen={isMobileMenuOpen}
onClose={() => setIsMobileMenuOpen(false)}
>
{link.label}
{navItems.map((item, idx) => (
<a
key={`mobile-link-${idx}`}
href={item.link}
onClick={() => setIsMobileMenuOpen(false)}
className="relative text-neutral-600 dark:text-neutral-300"
>
<span className="block font-medium uppercase tracking-wider">
{item.name}
</span>
</a>
))}
<div className="flex w-full flex-col gap-4">
<Link to="/kontakt" onClick={() => setIsMobileMenuOpen(false)}>
<Button className="btn-minimal rounded-full mt-4 py-5 text-sm font-medium w-full">
<NavbarButton
as="span"
variant="primary"
className="block w-full text-center !text-black"
>
Kontakt
</Button>
</NavbarButton>
</Link>
</nav>
</div>
)}
</MobileNavMenu>
</MobileNav>
</Navbar>
</div>
</header>
);
};

View File

@@ -2,6 +2,7 @@ import { Link } from "react-router-dom";
import { Button } from "@/components/ui/button";
import { ArrowRight } from "lucide-react";
import { useState, useEffect } from "react";
import Silk from "@/components/Silk";
const FOUNDING_DATE = new Date("2026-01-25"); // Samstag, 25. Januar 2026
@@ -16,28 +17,35 @@ const Hero = () => {
const totalSeconds = Math.floor(diff / 1000);
const days = Math.floor(totalSeconds / (60 * 60 * 24));
const hours = Math.floor((totalSeconds % (60 * 60 * 24)) / (60 * 60));
const minutes = Math.floor((totalSeconds % (60 * 60)) / 60);
const seconds = totalSeconds % 60;
const years = Math.floor(days / 365);
const remainingDays = days % 365;
if (years > 0) {
setCompanyAge(`${years}J ${remainingDays}T ${hours}h ${minutes}m ${seconds}s`);
setCompanyAge(`${years}J ${remainingDays}T ${hours}h`);
} else {
setCompanyAge(`${days}T ${hours}h ${minutes}m ${seconds}s`);
setCompanyAge(`${days}T ${hours}h`);
}
};
calculateAge();
const interval = setInterval(calculateAge, 1000); // Update every second
const interval = setInterval(calculateAge, 60 * 60 * 1000); // Update every hour (only days/hours shown)
return () => clearInterval(interval);
}, []);
return (
<section className="relative min-h-screen flex flex-col justify-center bg-background overflow-hidden pt-20">
{/* Subtle grid lines */}
<div className="absolute inset-0 grid-lines opacity-30" />
<section className="relative min-h-screen flex flex-col justify-center overflow-hidden pt-20">
{/* Silk animated background */}
<div className="absolute inset-0 z-0 w-full h-full">
<Silk
speed={3}
scale={0.5}
color="#373737"
noiseIntensity={4
}
rotation={0}
/>
</div>
<div className="container mx-auto px-6 relative z-10">
<div className="max-w-6xl">
@@ -57,11 +65,6 @@ const Hero = () => {
Wir digitalisieren, automatisieren und vernetzen Ihre gesamte Firma in einem einzigen System damit Ihr Unternehmen wachsen kann, ohne dass Sie mehr arbeiten müssen.
</p>
{/* Kurztext */}
<p className="text-lg md:text-xl text-muted-foreground max-w-2xl mb-8 animate-fade-in" style={{ animationDelay: '0.4s' }}>
Die meisten Unternehmen arbeiten mit zu vielen Tools, manuellen Prozessen und ineffizienten Abläufen. Wir ersetzen Chaos durch Struktur und bauen Ihnen eine digitale Infrastruktur, die Zeit spart, Fehler reduziert und Wachstum planbar macht.
</p>
{/* CTA Buttons */}
<div className="flex flex-col sm:flex-row gap-4 mb-6 animate-fade-in" style={{ animationDelay: '0.5s' }}>
<Link to="/kontakt">

158
src/components/Silk.tsx Normal file
View File

@@ -0,0 +1,158 @@
/* eslint-disable react/no-unknown-property */
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import { forwardRef, useRef, useMemo, useLayoutEffect } from "react";
import { Color, type Mesh, type ShaderMaterial } from "three";
const hexToNormalizedRGB = (hex: string): [number, number, number] => {
hex = hex.replace("#", "");
return [
parseInt(hex.slice(0, 2), 16) / 255,
parseInt(hex.slice(2, 4), 16) / 255,
parseInt(hex.slice(4, 6), 16) / 255,
];
};
const vertexShader = `
varying vec2 vUv;
varying vec3 vPosition;
void main() {
vPosition = position;
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const fragmentShader = `
varying vec2 vUv;
varying vec3 vPosition;
uniform float uTime;
uniform vec3 uColor;
uniform float uSpeed;
uniform float uScale;
uniform float uRotation;
uniform float uNoiseIntensity;
const float e = 2.71828182845904523536;
float noise(vec2 texCoord) {
float G = e;
vec2 r = (G * sin(G * texCoord));
return fract(r.x * r.y * (1.0 + texCoord.x));
}
vec2 rotateUvs(vec2 uv, float angle) {
float c = cos(angle);
float s = sin(angle);
mat2 rot = mat2(c, -s, s, c);
return rot * uv;
}
void main() {
float rnd = noise(gl_FragCoord.xy);
vec2 uv = rotateUvs(vUv * uScale, uRotation);
vec2 tex = uv * uScale;
float tOffset = uSpeed * uTime;
tex.y += 0.03 * sin(8.0 * tex.x - tOffset);
float pattern = 0.6 +
0.4 * sin(5.0 * (tex.x + tex.y +
cos(3.0 * tex.x + 5.0 * tex.y) +
0.02 * tOffset) +
sin(20.0 * (tex.x + tex.y - 0.1 * tOffset)));
vec4 col = vec4(uColor, 1.0) * vec4(pattern) - rnd / 15.0 * uNoiseIntensity;
col.a = 1.0;
gl_FragColor = col;
}
`;
type SilkPlaneProps = {
uniforms: {
uSpeed: { value: number };
uScale: { value: number };
uNoiseIntensity: { value: number };
uColor: { value: Color };
uRotation: { value: number };
uTime: { value: number };
};
};
const SilkPlane = forwardRef<Mesh, SilkPlaneProps>(function SilkPlane(
{ uniforms },
ref
) {
const { viewport } = useThree();
useLayoutEffect(() => {
if (ref && typeof ref !== "function" && ref.current) {
ref.current.scale.set(viewport.width, viewport.height, 1);
}
}, [ref, viewport]);
useFrame((_, delta) => {
if (ref && typeof ref !== "function" && ref.current) {
const mat = ref.current.material as ShaderMaterial & {
uniforms: { uTime: { value: number } };
};
if (mat.uniforms?.uTime) mat.uniforms.uTime.value += 0.1 * delta;
}
});
return (
<mesh ref={ref}>
<planeGeometry args={[1, 1, 1, 1]} />
<shaderMaterial
uniforms={uniforms}
vertexShader={vertexShader}
fragmentShader={fragmentShader}
/>
</mesh>
);
});
SilkPlane.displayName = "SilkPlane";
type SilkProps = {
speed?: number;
scale?: number;
color?: string;
noiseIntensity?: number;
rotation?: number;
};
const Silk = ({
speed = 5,
scale = 1,
color = "#7B7481",
noiseIntensity = 1.5,
rotation = 0,
}: SilkProps) => {
const meshRef = useRef<Mesh>(null);
const uniforms = useMemo(
() => ({
uSpeed: { value: speed },
uScale: { value: scale },
uNoiseIntensity: { value: noiseIntensity },
uColor: { value: new Color(...hexToNormalizedRGB(color)) },
uRotation: { value: rotation },
uTime: { value: 0 },
}),
[speed, scale, noiseIntensity, color, rotation]
);
return (
<Canvas
dpr={[1, 2]}
frameloop="always"
style={{ width: "100%", height: "100%" }}
camera={{ position: [0, 0, 5], fov: 75 }}
>
<SilkPlane ref={meshRef} uniforms={uniforms} />
</Canvas>
);
};
export default Silk;

View File

@@ -0,0 +1,317 @@
"use client";
import { cn } from "@/lib/utils";
import { IconMenu2, IconX } from "@tabler/icons-react";
import {
motion,
AnimatePresence,
useScroll,
useMotionValueEvent,
} from "motion/react";
import React, { useRef, useState } from "react";
interface NavbarProps {
children: React.ReactNode;
className?: string;
}
export const Navbar = ({ children, className }: NavbarProps) => {
const ref = useRef<HTMLDivElement>(null);
const { scrollY } = useScroll({
target: ref,
offset: ["start start", "end start"],
});
const [visible, setVisible] = useState(false);
useMotionValueEvent(scrollY, "change", (latest) => {
if (latest > 100) {
setVisible(true);
} else {
setVisible(false);
}
});
return (
<motion.div
ref={ref}
className={cn("fixed inset-x-0 top-0 z-40 w-full", className)}
>
{React.Children.map(children, (child) =>
React.isValidElement(child)
? React.cloneElement(child, { visible } as { visible: boolean })
: child
)}
</motion.div>
);
};
interface NavBodyProps {
children: React.ReactNode;
className?: string;
visible?: boolean;
}
export const NavBody = ({ children, className, visible }: NavBodyProps) => {
return (
<motion.div
animate={{
backdropFilter: visible ? "blur(10px)" : "none",
boxShadow: visible
? "0 0 24px rgba(34, 42, 53, 0.06), 0 1px 1px rgba(0, 0, 0, 0.05), 0 0 0 1px rgba(34, 42, 53, 0.04), 0 0 4px rgba(34, 42, 53, 0.08), 0 16px 68px rgba(47, 48, 55, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1) inset"
: "none",
width: visible ? "40%" : "100%",
y: visible ? 20 : 0,
}}
transition={{
type: "spring",
stiffness: 200,
damping: 50,
}}
style={{
minWidth: "800px",
}}
className={cn(
"relative z-[60] mx-auto hidden w-full max-w-7xl flex-row items-center justify-between self-start rounded-full bg-transparent px-4 py-2 lg:flex",
"text-white [&_a]:text-white [&_a:hover]:text-white/90 [&_.navbar-actions_a]:!text-black",
visible && "bg-black/90",
className
)}
>
{children}
</motion.div>
);
};
interface NavItem {
name: string;
link: string;
}
interface NavItemsProps {
items: NavItem[];
className?: string;
onItemClick?: () => void;
}
export const NavItems = ({
items,
className,
onItemClick,
}: NavItemsProps) => {
const [hovered, setHovered] = useState<number | null>(null);
return (
<motion.div
onMouseLeave={() => setHovered(null)}
className={cn(
"absolute inset-0 hidden flex-1 flex-row items-center justify-center space-x-2 text-sm font-medium text-zinc-600 transition duration-200 hover:text-zinc-800 lg:flex lg:space-x-2",
className
)}
>
{items.map((item, idx) => (
<a
onMouseEnter={() => setHovered(idx)}
onClick={onItemClick}
className="relative px-4 py-2 text-neutral-600 dark:text-neutral-300"
key={`link-${idx}`}
href={item.link}
>
{hovered === idx && (
<motion.div
layoutId="hovered"
className="absolute inset-0 h-full w-full rounded-full bg-white/10"
/>
)}
<span className="relative z-20">{item.name}</span>
</a>
))}
</motion.div>
);
};
interface MobileNavProps {
children: React.ReactNode;
className?: string;
visible?: boolean;
}
export const MobileNav = ({
children,
className,
visible,
}: MobileNavProps) => {
return (
<motion.div
animate={{
backdropFilter: visible ? "blur(10px)" : "none",
boxShadow: visible
? "0 0 24px rgba(34, 42, 53, 0.06), 0 1px 1px rgba(0, 0, 0, 0.05), 0 0 0 1px rgba(34, 42, 53, 0.04), 0 0 4px rgba(34, 42, 53, 0.08), 0 16px 68px rgba(47, 48, 55, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1) inset"
: "none",
width: visible ? "90%" : "100%",
paddingRight: visible ? "12px" : "0px",
paddingLeft: visible ? "12px" : "0px",
borderRadius: visible ? "4px" : "2rem",
y: visible ? 20 : 0,
}}
transition={{
type: "spring",
stiffness: 200,
damping: 50,
}}
className={cn(
"relative z-50 mx-auto flex w-full max-w-[calc(100vw-2rem)] flex-col items-center justify-between bg-transparent px-0 py-2 lg:hidden",
"[&>div:first-child]:text-white [&>div:first-child_a]:text-white [&>div:first-child_svg]:text-white",
visible && "bg-black/90",
className
)}
>
{children}
</motion.div>
);
};
interface MobileNavHeaderProps {
children: React.ReactNode;
className?: string;
}
export const MobileNavHeader = ({
children,
className,
}: MobileNavHeaderProps) => {
return (
<div
className={cn(
"flex w-full flex-row items-center justify-between",
className
)}
>
{children}
</div>
);
};
interface MobileNavMenuProps {
children: React.ReactNode;
className?: string;
isOpen: boolean;
onClose: () => void;
}
export const MobileNavMenu = ({
children,
className,
isOpen,
}: MobileNavMenuProps) => {
return (
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className={cn(
"absolute inset-x-0 top-16 z-50 flex w-full flex-col items-start justify-start gap-4 rounded-lg bg-white px-4 py-8 shadow-[0_0_24px_rgba(34,_42,_53,_0.06),_0_1px_1px_rgba(0,_0,_0,_0.05),_0_0_0_1px_rgba(34,_42,_53,_0.04),_0_0_4px_rgba(34,_42,_53,_0.08),_0_16px_68px_rgba(47,_48,_55,_0.05),_0_1px_0_rgba(255,_255,_255,_0.1)_inset] dark:bg-neutral-950",
className
)}
>
{children}
</motion.div>
)}
</AnimatePresence>
);
};
interface MobileNavToggleProps {
isOpen: boolean;
onClick: () => void;
}
export const MobileNavToggle = ({ isOpen, onClick }: MobileNavToggleProps) => {
return isOpen ? (
<IconX
className="h-6 w-6 cursor-pointer text-black dark:text-white"
onClick={onClick}
/>
) : (
<IconMenu2
className="h-6 w-6 cursor-pointer text-black dark:text-white"
onClick={onClick}
/>
);
};
interface NavbarLogoProps {
href?: string;
logoSrc?: string;
logoAlt?: string;
children?: React.ReactNode;
className?: string;
}
export const NavbarLogo = ({
href = "#",
logoSrc,
logoAlt = "Logo",
children,
className,
}: NavbarLogoProps) => {
return (
<a
href={href}
className={cn(
"relative z-20 mr-4 flex items-center space-x-2 px-2 py-1 text-sm font-normal text-black dark:text-white",
className
)}
>
{logoSrc ? (
<img src={logoSrc} alt={logoAlt} width={30} height={30} />
) : null}
{children}
</a>
);
};
interface NavbarButtonProps
extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
href?: string;
as?: "a" | "button";
children: React.ReactNode;
className?: string;
variant?: "primary" | "secondary" | "dark" | "gradient";
}
export const NavbarButton = ({
href,
as: Tag = "a",
children,
className,
variant = "primary",
...props
}: NavbarButtonProps) => {
const baseStyles =
"px-4 py-2 rounded-md bg-white text-black text-sm font-bold relative cursor-pointer hover:-translate-y-0.5 transition duration-200 inline-block text-center";
const variantStyles = {
primary:
"shadow-[0_0_24px_rgba(34,_42,_53,_0.06),_0_1px_1px_rgba(0,_0,_0,_0.05),_0_0_0_1px_rgba(34,_42,_53,_0.04),_0_0_4px_rgba(34,_42,_53,_0.08),_0_16px_68px_rgba(47,_48,_55,_0.05),_0_1px_0_rgba(255,_255,_255,_0.1)_inset]",
secondary: "bg-transparent shadow-none dark:text-white",
dark: "bg-black text-white shadow-[0_0_24px_rgba(34,_42,_53,_0.06),_0_1px_1px_rgba(0,_0,_0,_0.05),_0_0_0_1px_rgba(34,_42,_53,_0.04),_0_0_4px_rgba(34,_42,_53,_0.08),_0_16px_68px_rgba(47,_48,_55,_0.05),_0_1px_0_rgba(255,_255,_255,_0.1)_inset]",
gradient:
"bg-gradient-to-b from-blue-500 to-blue-700 text-white shadow-[0px_2px_0px_0px_rgba(255,255,255,0.3)_inset]",
};
const componentProps =
Tag === "a"
? { href: href ?? undefined, ...props }
: { ...props };
return (
<Tag
className={cn(baseStyles, variantStyles[variant], className)}
{...(componentProps as React.ComponentProps<typeof Tag>)}
>
{children}
</Tag>
);
};

View File

@@ -17,7 +17,7 @@
@layer base {
:root {
/* Ultra Minimal Deep Black Theme - Muradov Inspired */
--background: 0 0% 3%;
--background: 0 0% 0%;
--foreground: 0 0% 92%;
--card: 0 0% 6%;