Compare commits
3 Commits
ed2ec6a6a8
...
a13b93bf6d
| Author | SHA1 | Date | |
|---|---|---|---|
| a13b93bf6d | |||
| 1d4584e5d9 | |||
| 6228945065 |
8551
package-lock.json
generated
Normal file
8551
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
93
package.json
Normal file
93
package.json
Normal file
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"name": "vite_react_shadcn_ts",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"build:dev": "vite build --mode development",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.10.0",
|
||||
"@radix-ui/react-accordion": "^1.2.11",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.7",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-collapsible": "^1.1.11",
|
||||
"@radix-ui/react-context-menu": "^2.2.15",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-hover-card": "^1.1.14",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-menubar": "^1.1.15",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.13",
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-radio-group": "^1.3.7",
|
||||
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slider": "^1.3.5",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@radix-ui/react-tabs": "^1.1.12",
|
||||
"@radix-ui/react-toast": "^1.2.14",
|
||||
"@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",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^3.6.0",
|
||||
"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",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.61.1",
|
||||
"react-resizable-panels": "^2.1.9",
|
||||
"react-router-dom": "^6.30.1",
|
||||
"recharts": "^2.15.4",
|
||||
"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",
|
||||
"@types/node": "^22.16.5",
|
||||
"@types/react": "^18.3.23",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"@vitejs/plugin-react-swc": "^3.11.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^9.32.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"globals": "^15.15.0",
|
||||
"jsdom": "^20.0.3",
|
||||
"lovable-tagger": "^1.1.13",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.38.0",
|
||||
"vite": "^5.4.19",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
}
|
||||
97
src/components/Footer.tsx
Normal file
97
src/components/Footer.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import React from 'react';
|
||||
|
||||
const DevStudio: React.FC = () => {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
99
src/components/Header.tsx
Normal file
99
src/components/Header.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
"use client";
|
||||
|
||||
import { Link } from "react-router-dom";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Navbar,
|
||||
NavBody,
|
||||
NavItems,
|
||||
MobileNav,
|
||||
NavbarLogo,
|
||||
NavbarButton,
|
||||
MobileNavHeader,
|
||||
MobileNavToggle,
|
||||
MobileNavMenu,
|
||||
} from "@/components/ui/resizable-navbar";
|
||||
|
||||
const Header = () => {
|
||||
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 (
|
||||
<div className="relative w-full">
|
||||
<Navbar>
|
||||
{/* Desktop Navigation */}
|
||||
<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">
|
||||
<NavbarButton
|
||||
as="span"
|
||||
variant="primary"
|
||||
className="!text-black"
|
||||
>
|
||||
Kontakt
|
||||
</NavbarButton>
|
||||
</Link>
|
||||
</div>
|
||||
</NavBody>
|
||||
|
||||
{/* 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)}
|
||||
/>
|
||||
</MobileNavHeader>
|
||||
|
||||
<MobileNavMenu
|
||||
isOpen={isMobileMenuOpen}
|
||||
onClose={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
{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)}>
|
||||
<NavbarButton
|
||||
as="span"
|
||||
variant="primary"
|
||||
className="block w-full text-center !text-black"
|
||||
>
|
||||
Kontakt
|
||||
</NavbarButton>
|
||||
</Link>
|
||||
</div>
|
||||
</MobileNavMenu>
|
||||
</MobileNav>
|
||||
</Navbar>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
121
src/components/Hero.tsx
Normal file
121
src/components/Hero.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
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
|
||||
|
||||
const Hero = () => {
|
||||
const [companyAge, setCompanyAge] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const calculateAge = () => {
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - FOUNDING_DATE.getTime();
|
||||
|
||||
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 years = Math.floor(days / 365);
|
||||
const remainingDays = days % 365;
|
||||
|
||||
if (years > 0) {
|
||||
setCompanyAge(`${years}J ${remainingDays}T ${hours}h`);
|
||||
} else {
|
||||
setCompanyAge(`${days}T ${hours}h`);
|
||||
}
|
||||
};
|
||||
|
||||
calculateAge();
|
||||
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 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">
|
||||
{/* Label */}
|
||||
<div className="label-tag mb-8 animate-fade-in" style={{ animationDelay: '0.1s' }}>
|
||||
Webentwicklung & Design
|
||||
</div>
|
||||
|
||||
{/* Main Heading - Large Uppercase */}
|
||||
<h1 className="text-5xl sm:text-6xl md:text-7xl lg:text-8xl xl:text-9xl font-display font-medium text-foreground mb-6 leading-[0.95] tracking-tighter uppercase animate-slide-up" style={{ animationDelay: '0.2s' }}>
|
||||
Wir digitalisieren<br />
|
||||
<span className="text-muted-foreground">Ihr Unternehmen</span>
|
||||
</h1>
|
||||
|
||||
{/* Subheadline */}
|
||||
<p className="text-xl md:text-2xl text-foreground/90 max-w-3xl mb-6 font-medium animate-fade-in" style={{ animationDelay: '0.3s' }}>
|
||||
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>
|
||||
|
||||
{/* CTA Buttons */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 mb-6 animate-fade-in" style={{ animationDelay: '0.5s' }}>
|
||||
<Link to="/kontakt">
|
||||
<Button
|
||||
size="lg"
|
||||
className="btn-minimal rounded-full px-8 py-6 text-base font-medium group"
|
||||
>
|
||||
Kostenlose Potenzialanalyse sichern
|
||||
<ArrowRight className="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" />
|
||||
</Button>
|
||||
</Link>
|
||||
<Button
|
||||
size="lg"
|
||||
variant="outline"
|
||||
className="btn-outline rounded-full px-8 py-6 text-base font-medium"
|
||||
>
|
||||
System-Demo anfordern
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Trust Line */}
|
||||
<p className="text-sm text-muted-foreground/70 uppercase tracking-wider animate-fade-in" style={{ animationDelay: '0.55s' }}>
|
||||
Für Unternehmen, die nicht stehen bleiben wollen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Stats Row */}
|
||||
<div className="container mx-auto px-6 mt-auto pb-12 pt-20 relative z-10">
|
||||
<div className="divider mb-12" />
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 md:gap-12">
|
||||
<div className="animate-fade-in" style={{ animationDelay: '0.6s' }}>
|
||||
<div className="stat-number text-4xl md:text-5xl text-foreground mb-2">10+</div>
|
||||
<div className="label-tag">Projekte</div>
|
||||
</div>
|
||||
<div className="animate-fade-in" style={{ animationDelay: '0.7s' }}>
|
||||
<div className="stat-number text-4xl md:text-5xl text-foreground mb-2">{companyAge}</div>
|
||||
<div className="label-tag">Am Markt</div>
|
||||
</div>
|
||||
<div className="animate-fade-in" style={{ animationDelay: '0.8s' }}>
|
||||
<div className="stat-number text-4xl md:text-5xl text-foreground mb-2">99,9%</div>
|
||||
<div className="label-tag">Systemverfügbarkeit</div>
|
||||
</div>
|
||||
<div className="animate-fade-in" style={{ animationDelay: '0.9s' }}>
|
||||
<div className="stat-number text-4xl md:text-5xl text-foreground mb-2">🇩🇪</div>
|
||||
<div className="label-tag">Serverstandort in Deutschland</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Hero;
|
||||
158
src/components/Silk.tsx
Normal file
158
src/components/Silk.tsx
Normal 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;
|
||||
317
src/components/ui/resizable-navbar.tsx
Normal file
317
src/components/ui/resizable-navbar.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
336
src/index.css
Normal file
336
src/index.css
Normal file
@@ -0,0 +1,336 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* webklar Design System - Muradov Inspired Minimal Dark Theme */
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
@import url("https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap");
|
||||
@import url("https://fonts.googleapis.com/css2?family=Crimson+Pro:wght@400;500;600;700&display=swap");
|
||||
@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap");
|
||||
@import url("https://fonts.googleapis.com/css2?family=Lora:wght@400;500;600;700&display=swap");
|
||||
@import url("https://fonts.googleapis.com/css2?family=Space+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap");
|
||||
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap");
|
||||
@import url("https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap");
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
/* Ultra Minimal Deep Black Theme - Muradov Inspired */
|
||||
--background: 0 0% 0%;
|
||||
--foreground: 0 0% 92%;
|
||||
|
||||
--card: 0 0% 6%;
|
||||
--card-foreground: 0 0% 92%;
|
||||
|
||||
--popover: 0 0% 8%;
|
||||
--popover-foreground: 0 0% 92%;
|
||||
|
||||
/* Primary - Subtle cyan accent */
|
||||
--primary: 180 40% 50%;
|
||||
--primary-foreground: 0 0% 3%;
|
||||
|
||||
/* Secondary - Dark Gray */
|
||||
--secondary: 0 0% 12%;
|
||||
--secondary-foreground: 0 0% 92%;
|
||||
|
||||
/* Muted - Medium Gray */
|
||||
--muted: 0 0% 45%;
|
||||
--muted-foreground: 0 0% 65%;
|
||||
|
||||
/* Accent */
|
||||
--accent: 0 0% 10%;
|
||||
--accent-foreground: 0 0% 92%;
|
||||
|
||||
--destructive: 0 62% 50%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--border: 0 0% 15%;
|
||||
--input: 0 0% 15%;
|
||||
--ring: 180 40% 50%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
|
||||
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 2px 4px -1px hsl(0 0% 0% / 0.1);
|
||||
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 4px 6px -1px hsl(0 0% 0% / 0.1);
|
||||
|
||||
--sidebar-background: 0 0% 3%;
|
||||
--sidebar-foreground: 222 47% 11%;
|
||||
--sidebar-primary: 200 98% 39%;
|
||||
--sidebar-primary-foreground: 204 100% 97%;
|
||||
--sidebar-accent: 215 24% 26%;
|
||||
--sidebar-accent-foreground: 210 40% 98%;
|
||||
--sidebar-border: 212 26% 83%;
|
||||
--sidebar-ring: 200 98% 39%;
|
||||
--chart-1: 198 93% 59%;
|
||||
--chart-2: 213 93% 67%;
|
||||
--chart-3: 215 20% 65%;
|
||||
--chart-4: 215 16% 46%;
|
||||
--chart-5: 215 19% 34%;
|
||||
--sidebar: 210 40% 98%;
|
||||
--font-sans: 'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
|
||||
--font-display: 'Space Grotesk', system-ui, sans-serif;
|
||||
--spacing: 0.25rem;
|
||||
--font-serif: 'Lora', ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
|
||||
--font-mono: 'Space Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
||||
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
|
||||
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 8px 10px -1px hsl(0 0% 0% / 0.1);
|
||||
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
|
||||
--tracking-normal: 0em;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222 47% 11%;
|
||||
--foreground: 210 40% 98%;
|
||||
--card: 217 32% 17%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--popover: 215 24% 26%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 198 93% 59%;
|
||||
--primary-foreground: 204 80% 15%;
|
||||
--secondary: 212 26% 83%;
|
||||
--secondary-foreground: 228 84% 4%;
|
||||
--muted: 215 16% 46%;
|
||||
--muted-foreground: 210 40% 98%;
|
||||
--accent: 228 84% 4%;
|
||||
--accent-foreground: 215 20% 65%;
|
||||
--destructive: 0 84% 60%;
|
||||
--destructive-foreground: 0 85% 97%;
|
||||
--border: 215 19% 34%;
|
||||
--input: 215 19% 34%;
|
||||
--ring: 198 93% 59%;
|
||||
--chart-1: 199 95% 73%;
|
||||
--chart-2: 211 96% 78%;
|
||||
--chart-3: 215 20% 65%;
|
||||
--chart-4: 215 16% 46%;
|
||||
--chart-5: 215 19% 34%;
|
||||
--sidebar: 217 32% 17%;
|
||||
--sidebar-foreground: 210 40% 98%;
|
||||
--sidebar-primary: 198 93% 59%;
|
||||
--sidebar-primary-foreground: 204 80% 15%;
|
||||
--sidebar-accent: 215 20% 65%;
|
||||
--sidebar-accent-foreground: 228 84% 4%;
|
||||
--sidebar-border: 215 19% 34%;
|
||||
--sidebar-ring: 198 93% 59%;
|
||||
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
|
||||
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
|
||||
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 2px 4px -1px hsl(0 0% 0% / 0.1);
|
||||
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 4px 6px -1px hsl(0 0% 0% / 0.1);
|
||||
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 8px 10px -1px hsl(0 0% 0% / 0.1);
|
||||
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
|
||||
--radius: 0rem;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground antialiased;
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: 'Space Grotesk', 'Inter', system-ui, sans-serif;
|
||||
letter-spacing: -0.04em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.05em;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
/* Minimal glass nav */
|
||||
.glass-nav {
|
||||
@apply backdrop-blur-xl border-b;
|
||||
background: hsl(0 0% 3% / 0.9);
|
||||
border-color: hsl(0 0% 15% / 0.5);
|
||||
}
|
||||
|
||||
/* Card minimal */
|
||||
.card-minimal {
|
||||
@apply transition-all duration-500;
|
||||
background: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
}
|
||||
.card-minimal:hover {
|
||||
border-color: hsl(0 0% 25%);
|
||||
}
|
||||
|
||||
/* Text gradient - subtle */
|
||||
.text-gradient {
|
||||
background: linear-gradient(135deg, hsl(0 0% 100%) 0%, hsl(0 0% 70%) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* Animated underline for links */
|
||||
.link-underline {
|
||||
@apply relative;
|
||||
}
|
||||
.link-underline::after {
|
||||
content: '';
|
||||
@apply absolute bottom-0 left-0 w-full h-px scale-x-0 origin-right transition-transform duration-300;
|
||||
background: hsl(0 0% 98%);
|
||||
}
|
||||
.link-underline:hover::after {
|
||||
@apply scale-x-100 origin-left;
|
||||
}
|
||||
|
||||
/* Fade up on scroll */
|
||||
.fade-up {
|
||||
@apply opacity-0 translate-y-8 transition-all duration-700 ease-out;
|
||||
}
|
||||
.fade-up.visible {
|
||||
@apply opacity-100 translate-y-0;
|
||||
}
|
||||
|
||||
/* Partner logo marquee */
|
||||
.marquee {
|
||||
@apply flex gap-16;
|
||||
animation: marquee 30s linear infinite;
|
||||
}
|
||||
|
||||
/* Minimal project card */
|
||||
.project-card {
|
||||
@apply transition-all duration-500 overflow-hidden;
|
||||
background: hsl(0 0% 6%);
|
||||
border: 1px solid hsl(0 0% 12%);
|
||||
}
|
||||
.project-card:hover {
|
||||
border-color: hsl(0 0% 25%);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
/* Minimal button */
|
||||
.btn-minimal {
|
||||
@apply relative transition-all duration-300;
|
||||
background: hsl(0 0% 98%);
|
||||
color: hsl(0 0% 3%);
|
||||
}
|
||||
.btn-minimal:hover {
|
||||
background: hsl(0 0% 85%);
|
||||
}
|
||||
|
||||
/* Outline button */
|
||||
.btn-outline {
|
||||
@apply relative transition-all duration-300 border;
|
||||
background: transparent;
|
||||
color: hsl(0 0% 98%);
|
||||
border-color: hsl(0 0% 25%);
|
||||
}
|
||||
.btn-outline:hover {
|
||||
border-color: hsl(0 0% 50%);
|
||||
background: hsl(0 0% 10%);
|
||||
}
|
||||
|
||||
/* Stats number */
|
||||
.stat-number {
|
||||
font-family: 'Space Grotesk', system-ui, sans-serif;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.05em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Grid line decoration */
|
||||
.grid-lines {
|
||||
background-image:
|
||||
linear-gradient(hsl(0 0% 12%) 1px, transparent 1px),
|
||||
linear-gradient(90deg, hsl(0 0% 12%) 1px, transparent 1px);
|
||||
background-size: 80px 80px;
|
||||
}
|
||||
|
||||
/* Horizontal divider */
|
||||
.divider {
|
||||
@apply w-full h-px;
|
||||
background: linear-gradient(90deg, transparent 0%, hsl(0 0% 20%) 50%, transparent 100%);
|
||||
}
|
||||
|
||||
/* Label/Tag */
|
||||
.label-tag {
|
||||
@apply text-xs uppercase tracking-widest font-medium;
|
||||
color: hsl(0 0% 50%);
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.shadow-soft {
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
.shadow-elevated {
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
.shadow-floating {
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Keyframes */
|
||||
@keyframes marquee {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in-up {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-up {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(60px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fade-in 0.8s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-slide-up {
|
||||
animation: slide-up 0.8s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-fade-in-up {
|
||||
animation: fade-in-up 0.6s ease-out forwards;
|
||||
}
|
||||
Reference in New Issue
Block a user