This commit is contained in:
Tomas Dvorak
2026-02-22 15:41:27 +01:00
parent 0b88627e54
commit 409acd2e08
84 changed files with 65382 additions and 27475 deletions
+88
View File
@@ -10,6 +10,7 @@
"dependencies": {
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
@@ -1335,6 +1336,35 @@
}
}
},
"node_modules/@radix-ui/react-dropdown-menu": {
"version": "2.1.16",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz",
"integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-menu": "2.1.16",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-focus-guards": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
@@ -1393,6 +1423,64 @@
}
}
},
"node_modules/@radix-ui/react-menu": {
"version": "2.1.16",
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz",
"integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-collection": "1.1.7",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.11",
"@radix-ui/react-focus-guards": "1.1.3",
"@radix-ui/react-focus-scope": "1.1.7",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-popper": "1.2.8",
"@radix-ui/react-portal": "1.1.9",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-roving-focus": "1.1.11",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"aria-hidden": "^1.2.4",
"react-remove-scroll": "^2.6.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popper": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
+1
View File
@@ -12,6 +12,7 @@
"dependencies": {
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
+20 -17
View File
@@ -1,3 +1,4 @@
import { ThemeProvider } from "@/contexts/ThemeContext"
import { ParticleBackground } from "@/components/ui/magicui"
import { Hero } from "@/components/sections/Hero"
import { Features } from "@/components/sections/Features"
@@ -9,23 +10,25 @@ import { Footer } from "@/components/sections/Footer"
function App() {
return (
<div className="relative min-h-screen bg-background text-foreground overflow-x-hidden">
{/* Particle Background */}
<ParticleBackground />
{/* Main Content */}
<main className="relative z-10">
<Hero />
<Features />
<CodeShowcase />
<Architecture />
<Languages />
<QuickStart />
</main>
{/* Footer */}
<Footer />
</div>
<ThemeProvider defaultTheme="dark" storageKey="devour-ui-theme">
<div className="relative min-h-screen bg-background text-foreground overflow-x-hidden">
{/* Particle Background */}
<ParticleBackground />
{/* Main Content */}
<main className="relative z-10">
<Hero />
<Features />
<CodeShowcase />
<Architecture />
<Languages />
<QuickStart />
</main>
{/* Footer */}
<Footer />
</div>
</ThemeProvider>
)
}
@@ -36,7 +36,7 @@ export function Architecture() {
{/* Central Flow */}
<div className="relative rounded-2xl border border-white/10 bg-white/5 backdrop-blur-sm p-8 md:p-12">
{/* Data Sources */}
<div className="flex justify-center gap-4 mb-8">
<div className="flex flex-wrap justify-center gap-2 sm:gap-4 mb-6 sm:mb-8">
{sources.map((source, i) => (
<motion.div
key={i}
@@ -45,8 +45,8 @@ export function Architecture() {
transition={{ delay: 0.3 + i * 0.1 }}
className="flex flex-col items-center gap-2"
>
<div className={`p-3 rounded-lg bg-white/5 border border-white/10`}>
<source.icon className={`w-5 h-5 ${source.color}`} />
<div className={`p-2 sm:p-3 rounded-lg bg-white/5 border border-white/10`}>
<source.icon className={`w-4 h-4 sm:w-5 sm:h-5 ${source.color}`} />
</div>
<span className="text-xs text-muted-foreground">{source.label}</span>
</motion.div>
@@ -64,7 +64,7 @@ export function Architecture() {
</div>
{/* Main Components Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-3 sm:gap-4 mb-6 sm:mb-8">
{/* Scraper */}
<motion.div
whileHover={{ scale: 1.02 }}
@@ -123,7 +123,7 @@ export function Architecture() {
</div>
{/* Server & Query */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4">
{/* Server */}
<motion.div
whileHover={{ scale: 1.02 }}
+1 -1
View File
@@ -125,7 +125,7 @@ export function Features() {
</div>
{/* Features Grid */}
<StaggerContainer className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 md:gap-6" staggerDelay={0.05}>
<StaggerContainer className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 md:gap-6" staggerDelay={0.05}>
{features.map((feature, index) => (
<StaggerItem key={index}>
<motion.div
+2 -2
View File
@@ -28,8 +28,8 @@ export function Footer() {
{/* Background */}
<div className="absolute inset-0 grid-background opacity-20" />
<div className="container relative z-10 px-4 md:px-6 py-12 md:py-16">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-8 lg:gap-12">
<div className="container relative z-10 px-4 md:px-6 py-8 sm:py-12 md:py-16">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-6 lg:gap-8 lg:gap-12">
{/* Brand */}
<div className="lg:col-span-2">
<FadeIn>
+20 -14
View File
@@ -2,6 +2,7 @@ import { motion } from "framer-motion"
import { Github, Terminal, Sparkles, ArrowRight } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { ThemeToggle } from "@/components/ui/theme-toggle"
import { GradientText, FadeIn } from "@/components/ui/magicui"
export function Hero() {
@@ -12,6 +13,11 @@ export function Hero() {
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-teal-500/20 rounded-full blur-3xl animate-pulse delay-1000" />
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] bg-cyan-500/10 rounded-full blur-3xl" />
<div className="absolute top-0 left-0 right-0 z-20 flex justify-between items-center p-6">
<div></div>
<ThemeToggle />
</div>
<div className="container relative z-10 px-4 md:px-6">
<div className="flex flex-col items-center text-center space-y-8">
{/* Badge */}
@@ -34,21 +40,21 @@ export function Hero() {
<img
src="/devour_logo.svg"
alt="Devour Logo"
className="relative w-32 h-32 md:w-40 md:h-40 drop-shadow-2xl"
className="relative w-24 h-24 sm:w-32 sm:h-32 md:w-40 md:h-40 drop-shadow-2xl"
/>
</motion.div>
</FadeIn>
{/* Title */}
<FadeIn delay={0.3}>
<h1 className="text-5xl md:text-7xl font-bold tracking-tight">
<h1 className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold tracking-tight">
<GradientText>Devour</GradientText>
</h1>
</FadeIn>
{/* Subtitle */}
<FadeIn delay={0.4}>
<p className="text-xl md:text-2xl text-muted-foreground max-w-2xl">
<p className="text-lg sm:text-xl md:text-2xl text-muted-foreground max-w-2xl">
Context Ingestion & Management for{" "}
<span className="text-cyan-400">AI</span>
</p>
@@ -56,7 +62,7 @@ export function Hero() {
{/* Description */}
<FadeIn delay={0.5}>
<p className="text-base md:text-lg text-muted-foreground/80 max-w-3xl leading-relaxed">
<p className="text-sm sm:text-base md:text-lg text-muted-foreground/80 max-w-3xl leading-relaxed px-4 sm:px-0">
Scrape, index, and serve documentation from multiple sources.
Feed structured, relevant context to AI models for generating
accurate, fully working code.
@@ -104,30 +110,30 @@ export function Hero() {
{/* Terminal Preview */}
<FadeIn delay={0.8}>
<div className="w-full max-w-3xl mt-12">
<div className="w-full max-w-3xl mt-8 sm:mt-12">
<div className="relative rounded-xl border border-white/10 bg-black/40 backdrop-blur-sm overflow-hidden">
{/* Terminal Header */}
<div className="flex items-center gap-2 px-4 py-3 border-b border-white/10 bg-white/5">
<div className="flex items-center gap-2 px-3 sm:px-4 py-2 sm:py-3 border-b border-white/10 bg-white/5">
<div className="flex gap-1.5">
<div className="w-3 h-3 rounded-full bg-red-500/80" />
<div className="w-3 h-3 rounded-full bg-yellow-500/80" />
<div className="w-3 h-3 rounded-full bg-green-500/80" />
<div className="w-2.5 h-2.5 sm:w-3 sm:h-3 rounded-full bg-red-500/80" />
<div className="w-2.5 h-2.5 sm:w-3 sm:h-3 rounded-full bg-yellow-500/80" />
<div className="w-2.5 h-2.5 sm:w-3 sm:h-3 rounded-full bg-green-500/80" />
</div>
<span className="text-xs text-muted-foreground ml-2">terminal</span>
</div>
{/* Terminal Content */}
<div className="p-4 font-mono text-sm">
<div className="p-3 sm:p-4 font-mono text-xs sm:text-sm">
<div className="text-muted-foreground">$ devour get go http</div>
<div className="mt-2 text-cyan-400">
<div className="mt-1 sm:mt-2 text-cyan-400">
<span className="text-green-400"></span> Fetching Go net/http documentation...
</div>
<div className="mt-1 text-muted-foreground/80">
<div className="mt-0.5 sm:mt-1 text-muted-foreground/80">
<span className="text-cyan-500"></span> Indexing 47 documents
</div>
<div className="mt-1 text-muted-foreground/80">
<div className="mt-0.5 sm:mt-1 text-muted-foreground/80">
<span className="text-cyan-500"></span> Creating vector embeddings
</div>
<div className="mt-2 text-green-400">
<div className="mt-1 sm:mt-2 text-green-400">
Ready! Query with: devour query "How to create a server?"
</div>
</div>
+53 -26
View File
@@ -1,89 +1,114 @@
import { motion } from "framer-motion"
import {
Code,
Terminal,
Cpu,
BookOpen,
Atom,
Heart,
Rocket,
Cloud,
Coffee,
Leaf,
Container
} from "lucide-react"
import { GradientText, FadeIn, StaggerContainer, StaggerItem } from "@/components/ui/magicui"
const languages = [
{
name: "Go",
alias: "golang",
icon: "🐹",
color: "from-cyan-400 to-cyan-600",
icon: Cpu,
color: "text-cyan-500",
bgColor: "bg-cyan-500/10",
docs: "pkg.go.dev",
},
{
name: "Python",
alias: "py",
icon: "🐍",
color: "from-yellow-400 to-green-500",
icon: Terminal,
color: "text-green-500",
bgColor: "bg-green-500/10",
docs: "docs.python.org",
},
{
name: "Rust",
alias: "rust",
icon: "🦀",
color: "from-orange-400 to-red-500",
icon: Code,
color: "text-orange-500",
bgColor: "bg-orange-500/10",
docs: "docs.rs",
},
{
name: "TypeScript",
alias: "ts",
icon: "📘",
color: "from-blue-400 to-blue-600",
icon: BookOpen,
color: "text-blue-500",
bgColor: "bg-blue-500/10",
docs: "typescriptlang.org",
},
{
name: "React",
alias: "react",
icon: "⚛️",
color: "from-cyan-400 to-purple-500",
icon: Atom,
color: "text-cyan-500",
bgColor: "bg-cyan-500/10",
docs: "react.dev",
},
{
name: "Vue",
alias: "vue",
icon: "💚",
color: "from-green-400 to-emerald-500",
icon: Heart,
color: "text-green-500",
bgColor: "bg-green-500/10",
docs: "vuejs.org",
},
{
name: "Nuxt",
alias: "nuxt",
icon: "💚",
color: "from-green-400 to-teal-500",
icon: Leaf,
color: "text-teal-500",
bgColor: "bg-teal-500/10",
docs: "nuxt.com",
},
{
name: "Docker",
alias: "docker",
icon: "🐳",
color: "from-blue-400 to-cyan-500",
icon: Container,
color: "text-blue-500",
bgColor: "bg-blue-500/10",
docs: "docs.docker.com",
},
{
name: "Java",
alias: "java",
icon: "☕",
color: "from-red-400 to-orange-500",
icon: Coffee,
color: "text-red-500",
bgColor: "bg-red-500/10",
docs: "docs.oracle.com",
},
{
name: "Spring",
alias: "spring",
icon: "🍃",
color: "from-green-500 to-green-600",
icon: Leaf,
color: "text-green-600",
bgColor: "bg-green-600/10",
docs: "docs.spring.io",
},
{
name: "Astro",
alias: "astro",
icon: "🚀",
color: "from-purple-400 to-pink-500",
icon: Rocket,
color: "text-purple-500",
bgColor: "bg-purple-500/10",
docs: "docs.astro.build",
},
{
name: "Cloudflare",
alias: "cf",
icon: "☁️",
color: "from-orange-400 to-yellow-500",
icon: Cloud,
color: "text-orange-500",
bgColor: "bg-orange-500/10",
docs: "developers.cloudflare.com",
},
]
@@ -111,7 +136,7 @@ export function Languages() {
{/* Languages Grid */}
<StaggerContainer
className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-4"
className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-3 sm:gap-4"
staggerDelay={0.05}
>
{languages.map((lang, index) => (
@@ -122,7 +147,9 @@ export function Languages() {
className="group relative p-4 rounded-xl border border-white/10 bg-white/5 backdrop-blur-sm hover:border-cyan-500/30 hover:bg-white/[0.07] transition-all duration-300 cursor-pointer"
>
{/* Icon */}
<div className="text-3xl mb-3">{lang.icon}</div>
<div className={`flex items-center justify-center mb-3 p-3 rounded-lg ${lang.bgColor}`}>
<lang.icon className={`w-6 h-6 ${lang.color}`} />
</div>
{/* Name */}
<h3 className="font-semibold mb-1 group-hover:text-cyan-400 transition-colors">
+10 -10
View File
@@ -63,16 +63,16 @@ export function QuickStart() {
{/* Steps */}
<div className="max-w-4xl mx-auto">
<div className="grid gap-6">
<div className="grid gap-4 sm:gap-6">
{steps.map((step, index) => (
<FadeIn key={index} delay={0.1 + index * 0.1}>
<motion.div
whileHover={{ scale: 1.01 }}
className="group relative flex flex-col md:flex-row md:items-center gap-4 p-6 rounded-xl border border-white/10 bg-white/5 backdrop-blur-sm hover:border-cyan-500/30 hover:bg-white/[0.07] transition-all duration-300"
className="group relative flex flex-col lg:flex-row lg:items-center gap-4 p-4 sm:p-6 rounded-xl border border-white/10 bg-white/5 backdrop-blur-sm hover:border-cyan-500/30 hover:bg-white/[0.07] transition-all duration-300"
>
{/* Step Number */}
<div className="flex-shrink-0 w-16 h-16 flex items-center justify-center rounded-xl bg-gradient-to-br from-cyan-500/20 to-teal-500/20 border border-cyan-500/30">
<span className="text-2xl font-bold text-cyan-400">{step.number}</span>
<div className="flex-shrink-0 w-12 h-12 sm:w-16 sm:h-16 flex items-center justify-center rounded-xl bg-gradient-to-br from-cyan-500/20 to-teal-500/20 border border-cyan-500/30">
<span className="text-lg sm:text-2xl font-bold text-cyan-400">{step.number}</span>
</div>
{/* Content */}
@@ -85,8 +85,8 @@ export function QuickStart() {
</p>
{/* Command */}
<div className="relative flex items-center gap-2 p-3 rounded-lg bg-black/40 border border-white/10 font-mono text-sm">
<Terminal className="w-4 h-4 text-cyan-400 flex-shrink-0" />
<div className="relative flex items-center gap-2 p-2 sm:p-3 rounded-lg bg-black/40 border border-white/10 font-mono text-xs sm:text-sm">
<Terminal className="w-3 h-3 sm:w-4 sm:h-4 text-cyan-400 flex-shrink-0" />
<code className="flex-1 text-muted-foreground overflow-x-auto">
<span className="text-green-400">$</span> {step.command}
</code>
@@ -95,9 +95,9 @@ export function QuickStart() {
className="flex-shrink-0 p-1.5 rounded hover:bg-white/10 transition-colors"
>
{copiedStep === index ? (
<Check className="w-4 h-4 text-green-400" />
<Check className="w-3 h-3 sm:w-4 sm:h-4 text-green-400" />
) : (
<Copy className="w-4 h-4 text-muted-foreground hover:text-foreground" />
<Copy className="w-3 h-3 sm:w-4 sm:h-4 text-muted-foreground hover:text-foreground" />
)}
</button>
</div>
@@ -105,8 +105,8 @@ export function QuickStart() {
{/* Arrow (except last) */}
{index < steps.length - 1 && (
<div className="hidden md:flex absolute -bottom-6 left-1/2 -translate-x-1/2 z-10">
<ArrowRight className="w-4 h-4 text-cyan-500/50 rotate-90" />
<div className="hidden lg:flex absolute -bottom-4 sm:-bottom-6 left-1/2 -translate-x-1/2 z-10">
<ArrowRight className="w-3 h-3 sm:w-4 sm:h-4 text-cyan-500/50 rotate-90" />
</div>
)}
</motion.div>
+6 -6
View File
@@ -4,18 +4,18 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80 shadow-sm",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-sm",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
glow: "border-transparent bg-primary/20 text-primary border border-primary/30",
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80 shadow-sm",
outline: "text-foreground border border-input hover:bg-accent hover:text-accent-foreground",
glow: "border-transparent bg-gradient-to-r from-primary/20 to-primary/10 text-primary border border-primary/30 shadow-lg shadow-primary/20 hover:shadow-xl hover:shadow-primary/30",
},
},
defaultVariants: {
+8 -8
View File
@@ -5,26 +5,26 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90 hover:shadow-lg hover:shadow-primary/25",
default: "bg-primary text-primary-foreground hover:bg-primary/90 hover:shadow-lg hover:shadow-primary/25 hover:-translate-y-0.5 active:translate-y-0",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
"bg-destructive text-destructive-foreground hover:bg-destructive/90 hover:shadow-lg hover:shadow-destructive/25",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
"border border-input bg-background hover:bg-accent hover:text-accent-foreground hover:shadow-md hover:-translate-y-0.5 active:translate-y-0",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
"bg-secondary text-secondary-foreground hover:bg-secondary/80 hover:shadow-md hover:-translate-y-0.5 active:translate-y-0",
ghost: "hover:bg-accent hover:text-accent-foreground hover:shadow-sm",
link: "text-primary underline-offset-4 hover:underline",
glow: "bg-primary text-primary-foreground hover:bg-primary/90 shadow-lg shadow-primary/25 hover:shadow-xl hover:shadow-primary/30",
glow: "bg-gradient-to-r from-primary to-primary/90 text-primary-foreground shadow-lg shadow-primary/25 hover:shadow-xl hover:shadow-primary/30 hover:-translate-y-0.5 active:translate-y-0 border border-primary/20",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
xl: "h-12 rounded-lg px-10 text-base",
xl: "h-12 rounded-lg px-10 text-base font-semibold",
icon: "h-10 w-10",
},
},
+200
View File
@@ -0,0 +1,200 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}
@@ -0,0 +1,39 @@
import { Moon, Sun, Monitor } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { useTheme } from "@/contexts/ThemeContext"
export function ThemeToggle() {
const { setTheme } = useTheme()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
<Sun className="mr-2 h-4 w-4" />
<span>Light</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
<Moon className="mr-2 h-4 w-4" />
<span>Dark</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
<Monitor className="mr-2 h-4 w-4" />
<span>System</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
+73
View File
@@ -0,0 +1,73 @@
import { createContext, useContext, useEffect, useState } from 'react'
type Theme = 'dark' | 'light' | 'system'
type ThemeProviderProps = {
children: React.ReactNode
defaultTheme?: Theme
storageKey?: string
}
type ThemeProviderState = {
theme: Theme
setTheme: (theme: Theme) => void
}
const initialState: ThemeProviderState = {
theme: 'system',
setTheme: () => null,
}
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
export function ThemeProvider({
children,
defaultTheme = 'system',
storageKey = 'devour-ui-theme',
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
)
useEffect(() => {
const root = window.document.documentElement
root.classList.remove('light', 'dark')
if (theme === 'system') {
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)')
.matches
? 'dark'
: 'light'
root.classList.add(systemTheme)
return
}
root.classList.add(theme)
}, [theme])
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme)
setTheme(theme)
},
}
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
)
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext)
if (context === undefined)
throw new Error('useTheme must be used within a ThemeProvider')
return context
}
+159 -22
View File
@@ -1,6 +1,7 @@
@import "tailwindcss";
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap');
/* Custom CSS Variables */
/* CSS Variables for both light and dark themes */
:root {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
@@ -24,6 +25,50 @@
--radius: 0.5rem;
}
.light {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 187 92% 43%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 187 92% 43%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 187 92% 43%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 187 92% 43%;
}
/* Base styles */
* {
border-color: hsl(var(--border));
@@ -33,6 +78,8 @@ body {
background-color: hsl(var(--background));
color: hsl(var(--foreground));
font-feature-settings: "rlig" 1, "calt" 1;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
}
/* Custom scrollbar */
@@ -42,16 +89,29 @@ body {
}
::-webkit-scrollbar-track {
background: hsl(222.2 84% 4.9%);
background: hsl(var(--background));
}
::-webkit-scrollbar-thumb {
background: hsl(217.2 32.6% 17.5%);
background: hsl(var(--muted-foreground) / 0.3);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: hsl(215 20.2% 35.1%);
background: hsl(var(--muted-foreground) / 0.5);
}
/* Light theme scrollbar */
.light ::-webkit-scrollbar-track {
background: hsl(var(--muted));
}
.light ::-webkit-scrollbar-thumb {
background: hsl(var(--border));
}
.light ::-webkit-scrollbar-thumb:hover {
background: hsl(var(--muted-foreground) / 0.7);
}
/* Gradient text */
@@ -59,53 +119,64 @@ body {
background-clip: text;
-webkit-background-clip: text;
color: transparent;
background-image: linear-gradient(to right, #22d3ee, #67e8f9, #2dd4bf);
background-image: linear-gradient(135deg, #22d3ee, #67e8f9, #2dd4bf);
font-weight: 700;
}
/* Glow effects */
.glow {
box-shadow: 0 0 20px rgba(6, 182, 212, 0.3),
0 0 40px rgba(6, 182, 212, 0.2),
0 0 60px rgba(6, 182, 212, 0.1);
box-shadow: 0 0 20px hsl(var(--primary) / 0.3),
0 0 40px hsl(var(--primary) / 0.2),
0 0 60px hsl(var(--primary) / 0.1);
}
.glow-text {
text-shadow: 0 0 20px rgba(6, 182, 212, 0.5),
0 0 40px rgba(6, 182, 212, 0.3);
text-shadow: 0 0 20px hsl(var(--primary) / 0.5),
0 0 40px hsl(var(--primary) / 0.3);
}
/* Glass effect */
.glass {
background-color: rgba(255, 255, 255, 0.05);
background-color: hsl(var(--background) / 0.05);
backdrop-filter: blur(24px);
border: 1px solid rgba(255, 255, 255, 0.1);
border: 1px solid hsl(var(--border));
}
.light .glass {
background-color: hsl(var(--background) / 0.8);
border: 1px solid hsl(var(--border));
}
/* Grid background */
.grid-background {
background-image:
linear-gradient(rgba(6, 182, 212, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(6, 182, 212, 0.03) 1px, transparent 1px);
linear-gradient(hsl(var(--border) / 0.03) 1px, transparent 1px),
linear-gradient(90deg, hsl(var(--border) / 0.03) 1px, transparent 1px);
background-size: 50px 50px;
}
/* Animated gradient border */
.gradient-border {
position: relative;
background: linear-gradient(hsl(222.2 84% 4.9%), hsl(222.2 84% 4.9%)) padding-box,
linear-gradient(135deg, hsl(187 92% 43%), hsl(160 84% 39%), hsl(187 92% 43%)) border-box;
background: linear-gradient(hsl(var(--background)), hsl(var(--background))) padding-box,
linear-gradient(135deg, hsl(var(--primary)), hsl(var(--accent)), hsl(var(--primary))) border-box;
border: 1px solid transparent;
}
/* Code block styling */
.code-block {
background-color: rgba(0, 0, 0, 0.4);
background-color: hsl(var(--muted) / 0.5);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.1);
border: 1px solid hsl(var(--border));
border-radius: 0.5rem;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
}
.light .code-block {
background-color: hsl(var(--muted));
border: 1px solid hsl(var(--border));
}
/* Smooth scrolling */
html {
scroll-behavior: smooth;
@@ -113,8 +184,8 @@ html {
/* Selection */
::selection {
background: rgba(6, 182, 212, 0.3);
color: white;
background: hsl(var(--primary) / 0.3);
color: hsl(var(--primary-foreground));
}
/* Custom animations */
@@ -129,8 +200,8 @@ html {
}
@keyframes glow-anim {
0%, 100% { box-shadow: 0 0 20px rgba(6, 182, 212, 0.3); }
50% { box-shadow: 0 0 40px rgba(6, 182, 212, 0.6); }
0%, 100% { box-shadow: 0 0 20px hsl(var(--primary) / 0.3); }
50% { box-shadow: 0 0 40px hsl(var(--primary) / 0.6); }
}
@keyframes shimmer {
@@ -138,6 +209,55 @@ html {
to { background-position: -200% 0; }
}
@keyframes slide-up {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slide-in-left {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes bounce-in {
0% {
opacity: 0;
transform: scale(0.3);
}
50% {
opacity: 1;
transform: scale(1.05);
}
70% {
transform: scale(0.9);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes gradient-x {
0%, 100% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
}
.animate-float {
animation: float 3s ease-in-out infinite;
}
@@ -152,4 +272,21 @@ html {
.animate-shimmer {
animation: shimmer 2s linear infinite;
}
.animate-slide-up {
animation: slide-up 0.6s ease-out;
}
.animate-slide-in-left {
animation: slide-in-left 0.6s ease-out;
}
.animate-bounce-in {
animation: bounce-in 0.6s ease-out;
}
.animate-gradient-x {
animation: gradient-x 3s ease infinite;
background-size: 200% 200%;
}