diff --git a/.replit b/.replit
index 82bac8f..d9cc22f 100644
--- a/.replit
+++ b/.replit
@@ -18,3 +18,15 @@ expertMode = true
[postMerge]
path = "scripts/post-merge.sh"
timeoutMs = 20000
+
+[[ports]]
+localPort = 8080
+externalPort = 8080
+
+[[ports]]
+localPort = 8081
+externalPort = 80
+
+[[ports]]
+localPort = 23924
+externalPort = 3000
diff --git a/artifacts/joachim-portfolio/.replit-artifact/artifact.toml b/artifacts/joachim-portfolio/.replit-artifact/artifact.toml
new file mode 100644
index 0000000..4803696
--- /dev/null
+++ b/artifacts/joachim-portfolio/.replit-artifact/artifact.toml
@@ -0,0 +1,31 @@
+kind = "web"
+previewPath = "/"
+title = "Joachim Hummel – Portfolio"
+version = "1.0.0"
+id = "artifacts/joachim-portfolio"
+router = "path"
+
+[[integratedSkills]]
+name = "react-vite"
+version = "1.0.0"
+
+[[services]]
+name = "web"
+paths = [ "/" ]
+localPort = 23924
+
+[services.development]
+run = "pnpm --filter @workspace/joachim-portfolio run dev"
+
+[services.production]
+build = [ "pnpm", "--filter", "@workspace/joachim-portfolio", "run", "build" ]
+publicDir = "artifacts/joachim-portfolio/dist/public"
+serve = "static"
+
+[[services.production.rewrites]]
+from = "/*"
+to = "/index.html"
+
+[services.env]
+PORT = "23924"
+BASE_PATH = "/"
diff --git a/artifacts/joachim-portfolio/components.json b/artifacts/joachim-portfolio/components.json
new file mode 100644
index 0000000..3ff62cf
--- /dev/null
+++ b/artifacts/joachim-portfolio/components.json
@@ -0,0 +1,20 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": false,
+ "tsx": true,
+ "tailwind": {
+ "config": "",
+ "css": "src/index.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ }
+}
\ No newline at end of file
diff --git a/artifacts/joachim-portfolio/index.html b/artifacts/joachim-portfolio/index.html
new file mode 100644
index 0000000..3968508
--- /dev/null
+++ b/artifacts/joachim-portfolio/index.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+ Joachim Hummel – Portfolio
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/artifacts/joachim-portfolio/package.json b/artifacts/joachim-portfolio/package.json
new file mode 100644
index 0000000..5792331
--- /dev/null
+++ b/artifacts/joachim-portfolio/package.json
@@ -0,0 +1,77 @@
+{
+ "name": "@workspace/joachim-portfolio",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --config vite.config.ts --host 0.0.0.0",
+ "build": "vite build --config vite.config.ts",
+ "serve": "vite preview --config vite.config.ts --host 0.0.0.0",
+ "typecheck": "tsc -p tsconfig.json --noEmit"
+ },
+ "devDependencies": {
+ "@hookform/resolvers": "^3.10.0",
+ "@radix-ui/react-accordion": "^1.2.4",
+ "@radix-ui/react-alert-dialog": "^1.1.7",
+ "@radix-ui/react-aspect-ratio": "^1.1.3",
+ "@radix-ui/react-avatar": "^1.1.4",
+ "@radix-ui/react-checkbox": "^1.1.5",
+ "@radix-ui/react-collapsible": "^1.1.4",
+ "@radix-ui/react-context-menu": "^2.2.7",
+ "@radix-ui/react-dialog": "^1.1.7",
+ "@radix-ui/react-dropdown-menu": "^2.1.7",
+ "@radix-ui/react-hover-card": "^1.1.7",
+ "@radix-ui/react-label": "^2.1.3",
+ "@radix-ui/react-menubar": "^1.1.7",
+ "@radix-ui/react-navigation-menu": "^1.2.6",
+ "@radix-ui/react-popover": "^1.1.7",
+ "@radix-ui/react-progress": "^1.1.3",
+ "@radix-ui/react-radio-group": "^1.2.4",
+ "@radix-ui/react-scroll-area": "^1.2.4",
+ "@radix-ui/react-select": "^2.1.7",
+ "@radix-ui/react-separator": "^1.1.3",
+ "@radix-ui/react-slider": "^1.2.4",
+ "@radix-ui/react-slot": "^1.2.0",
+ "@radix-ui/react-switch": "^1.1.4",
+ "@radix-ui/react-tabs": "^1.1.4",
+ "@radix-ui/react-toast": "^1.2.7",
+ "@radix-ui/react-toggle": "^1.1.3",
+ "@radix-ui/react-toggle-group": "^1.1.3",
+ "@radix-ui/react-tooltip": "^1.2.0",
+ "@replit/vite-plugin-cartographer": "catalog:",
+ "@replit/vite-plugin-dev-banner": "catalog:",
+ "@replit/vite-plugin-runtime-error-modal": "catalog:",
+ "@tailwindcss/typography": "^0.5.15",
+ "@tailwindcss/vite": "catalog:",
+ "@tanstack/react-query": "catalog:",
+ "@types/node": "catalog:",
+ "@types/react": "catalog:",
+ "@types/react-dom": "catalog:",
+ "@vitejs/plugin-react": "catalog:",
+ "@workspace/api-client-react": "workspace:*",
+ "class-variance-authority": "catalog:",
+ "clsx": "catalog:",
+ "cmdk": "^1.1.1",
+ "date-fns": "^3.6.0",
+ "embla-carousel-react": "^8.6.0",
+ "framer-motion": "catalog:",
+ "input-otp": "^1.4.2",
+ "lucide-react": "catalog:",
+ "next-themes": "^0.4.6",
+ "react": "catalog:",
+ "react-day-picker": "^9.11.1",
+ "react-dom": "catalog:",
+ "react-hook-form": "^7.55.0",
+ "react-icons": "^5.4.0",
+ "react-resizable-panels": "^2.1.7",
+ "recharts": "^2.15.2",
+ "sonner": "^2.0.7",
+ "tailwind-merge": "catalog:",
+ "tailwindcss": "catalog:",
+ "tw-animate-css": "^1.4.0",
+ "vaul": "^1.1.2",
+ "vite": "catalog:",
+ "wouter": "^3.3.5",
+ "zod": "catalog:"
+ }
+}
diff --git a/artifacts/joachim-portfolio/public/favicon.svg b/artifacts/joachim-portfolio/public/favicon.svg
new file mode 100644
index 0000000..4373d3c
--- /dev/null
+++ b/artifacts/joachim-portfolio/public/favicon.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/artifacts/joachim-portfolio/public/opengraph.jpg b/artifacts/joachim-portfolio/public/opengraph.jpg
new file mode 100644
index 0000000..8b36111
Binary files /dev/null and b/artifacts/joachim-portfolio/public/opengraph.jpg differ
diff --git a/artifacts/joachim-portfolio/public/robots.txt b/artifacts/joachim-portfolio/public/robots.txt
new file mode 100644
index 0000000..c2a49f4
--- /dev/null
+++ b/artifacts/joachim-portfolio/public/robots.txt
@@ -0,0 +1,2 @@
+User-agent: *
+Allow: /
diff --git a/artifacts/joachim-portfolio/src/App.tsx b/artifacts/joachim-portfolio/src/App.tsx
new file mode 100644
index 0000000..681037e
--- /dev/null
+++ b/artifacts/joachim-portfolio/src/App.tsx
@@ -0,0 +1,32 @@
+import { Switch, Route, Router as WouterRouter } from "wouter";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { Toaster } from "@/components/ui/toaster";
+import { TooltipProvider } from "@/components/ui/tooltip";
+import Home from "@/pages/home";
+import NotFound from "@/pages/not-found";
+
+const queryClient = new QueryClient();
+
+function Router() {
+ return (
+
+
+
+
+ );
+}
+
+function App() {
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/artifacts/joachim-portfolio/src/components/bio.tsx b/artifacts/joachim-portfolio/src/components/bio.tsx
new file mode 100644
index 0000000..dcf817f
--- /dev/null
+++ b/artifacts/joachim-portfolio/src/components/bio.tsx
@@ -0,0 +1,31 @@
+import { motion } from "framer-motion";
+
+export function Bio() {
+ return (
+
+
+
+ 04. Über Mich
+
+
+
+
+ Joachim Hummel ist Senior IT-Consultant, IT-Systems Engineer und technischer Umsetzer mit rund 30 Jahren Erfahrung im IT-Betrieb.
+
+
+ Seine Schwerpunkte liegen auf Linux, Docker, On-Premise-Infrastrukturen, DSGVO-konformem Selfhosting, Mailservern, Monitoring, Automatisierung und KI-gestützter Softwareentwicklung.
+
+
+ Mit Vibe-Coding entwickelt er aus Ideen lauffähige Anwendungen, Automatisierungen und SaaS-Projekte.
+
+
+
+
+ );
+}
diff --git a/artifacts/joachim-portfolio/src/components/competencies.tsx b/artifacts/joachim-portfolio/src/components/competencies.tsx
new file mode 100644
index 0000000..c236ba9
--- /dev/null
+++ b/artifacts/joachim-portfolio/src/components/competencies.tsx
@@ -0,0 +1,81 @@
+import { motion } from "framer-motion";
+import { Server, Box, ShieldCheck, Activity, Cpu } from "lucide-react";
+import { SiDocker, SiLinux, SiGrafana, SiNginx, SiUbuntu } from "react-icons/si";
+
+const skills = [
+ {
+ icon: Server,
+ title: "Linux, Serverbetrieb & Infrastruktur",
+ desc: "Ubuntu, Debian, Proxmox, Systemd, Shell-Scripting, TLS, Reverse Proxy"
+ },
+ {
+ icon: Box,
+ title: "Docker & Container-Betrieb",
+ desc: "Docker Compose, Netzwerke, Volumes, Persistenz, Hetzner/Contabo"
+ },
+ {
+ icon: ShieldCheck,
+ title: "On-Premise & DSGVO Selfhosting",
+ desc: "iRedMail/Mailcow, Nextcloud, SOGo, Authentik/OIDC, Zoraxy/Nginx"
+ },
+ {
+ icon: Activity,
+ title: "Monitoring",
+ desc: "Grafana, Prometheus, Loki, Alloy"
+ },
+ {
+ icon: Cpu,
+ title: "KI, Automatisierung & Vibe-Coding",
+ desc: "Claude Code, ChatGPT, n8n, RAG-Systeme, Pinecone"
+ }
+];
+
+export function Competencies() {
+ return (
+
+
+
+ 01. Kernkompetenzen
+
+
+ Über 30 Jahre Erfahrung im IT-Betrieb, destilliert in hochspezialisierte Fähigkeiten.
+
+
+
+ {skills.map((skill, index) => (
+
+
+
+
+
+ {skill.title}
+
+ {skill.desc}
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/artifacts/joachim-portfolio/src/components/contact.tsx b/artifacts/joachim-portfolio/src/components/contact.tsx
new file mode 100644
index 0000000..1c8fe18
--- /dev/null
+++ b/artifacts/joachim-portfolio/src/components/contact.tsx
@@ -0,0 +1,42 @@
+import { motion } from "framer-motion";
+import { Mail, Github, Linkedin, Terminal } from "lucide-react";
+
+export function Contact() {
+ return (
+
+
+
+
+
+
+ Bereit für das nächste Projekt?
+
+ Lassen Sie uns darüber sprechen, wie wir Ihre Infrastruktur modernisieren oder Ihre nächste Anwendung bauen können.
+
+
+
+
+ Kontakt aufnehmen
+
+
+
+
+
+ );
+}
diff --git a/artifacts/joachim-portfolio/src/components/hero.tsx b/artifacts/joachim-portfolio/src/components/hero.tsx
new file mode 100644
index 0000000..7ce49c2
--- /dev/null
+++ b/artifacts/joachim-portfolio/src/components/hero.tsx
@@ -0,0 +1,61 @@
+import { motion } from "framer-motion";
+import { Terminal, ChevronDown } from "lucide-react";
+
+export function Hero() {
+ return (
+
+
+
+
+ System.init()
+
+
+
+ Joachim
+ Hummel
+
+
+
+ Senior IT-Consultant · IT-Systems Engineer · Vibe-Coder
+
+
+
+ Zwei Welten, eine Lösung: Tiefe, praxisbewährte Infrastruktur-Expertise
+ trifft auf moderne KI-gestützte Softwareentwicklung. Ich baue Systeme, die
+ in Produktion laufen – präzise, sicher und zukunftsfähig.
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/artifacts/joachim-portfolio/src/components/navbar.tsx b/artifacts/joachim-portfolio/src/components/navbar.tsx
new file mode 100644
index 0000000..0ffcd65
--- /dev/null
+++ b/artifacts/joachim-portfolio/src/components/navbar.tsx
@@ -0,0 +1,56 @@
+import { useState, useEffect } from "react";
+import { motion, useScroll, useMotionValueEvent } from "framer-motion";
+
+export function Navbar() {
+ const [isScrolled, setIsScrolled] = useState(false);
+ const { scrollY } = useScroll();
+
+ useMotionValueEvent(scrollY, "change", (latest) => {
+ setIsScrolled(latest > 50);
+ });
+
+ const navLinks = [
+ { name: "Kompetenzen", href: "#competencies" },
+ { name: "3 Welten", href: "#strengths" },
+ { name: "Projekte", href: "#projects" },
+ { name: "Kontakt", href: "#contact" },
+ ];
+
+ return (
+
+
+
+ );
+}
diff --git a/artifacts/joachim-portfolio/src/components/projects.tsx b/artifacts/joachim-portfolio/src/components/projects.tsx
new file mode 100644
index 0000000..58bc3a0
--- /dev/null
+++ b/artifacts/joachim-portfolio/src/components/projects.tsx
@@ -0,0 +1,82 @@
+import { motion } from "framer-motion";
+import { FolderGit2, ArrowRight } from "lucide-react";
+
+const projects = [
+ {
+ title: "SafeDocs Portal",
+ desc: "Sichere Upload-Plattform mit AES-256-GCM-Verschlüsselung, JWT-Auth, Passwort-Reset, Rate Limiting, Docker-Deployment",
+ tags: ["Docker", "Security", "AES-256"]
+ },
+ {
+ title: "zensend.email",
+ desc: "Newsletter & E-Mail-Marketing SaaS: Double-Opt-In, DSGVO, SPF/DKIM/DMARC, Zahlungsmodelle, Onboarding",
+ tags: ["SaaS", "Mail", "DSGVO"]
+ },
+ {
+ title: "KI-Automation mit n8n",
+ desc: "Automatisierte Bildgenerierung, Lead-Prozesse, KI-gestützte Bildprüfung, API/Webhook-Workflows",
+ tags: ["n8n", "AI", "API"]
+ },
+ {
+ title: "Eigene KI- & RAG-Systeme",
+ desc: "PDF-Verarbeitung, Vektorisierung, Pinecone, eigene Wissensdatenbanken",
+ tags: ["RAG", "Pinecone", "LLMs"]
+ },
+ {
+ title: "On-Premise Monitoring",
+ desc: "Grafana, Prometheus, Loki, Alloy Stacks auf Docker-Basis",
+ tags: ["Grafana", "Prometheus", "Docker"]
+ },
+ {
+ title: "Mailserver & Groupware",
+ desc: "iRedMail, Mailcow, SOGo, DNS, SPF, DKIM, DMARC, Zustellbarkeit",
+ tags: ["Mailcow", "DNS", "Security"]
+ }
+];
+
+export function Projects() {
+ return (
+
+
+
+ 03. Ausgewählte Projekte
+
+
+ Von der sicheren Infrastruktur bis zur modernen SaaS-Lösung – in der Praxis bewährt.
+
+
+
+ {projects.map((project, index) => (
+
+
+ {project.title}
+ {project.desc}
+
+ {project.tags.map((tag, tIndex) => (
+
+ {tag}
+
+ ))}
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/artifacts/joachim-portfolio/src/components/strengths.tsx b/artifacts/joachim-portfolio/src/components/strengths.tsx
new file mode 100644
index 0000000..5774f2a
--- /dev/null
+++ b/artifacts/joachim-portfolio/src/components/strengths.tsx
@@ -0,0 +1,64 @@
+import { motion } from "framer-motion";
+import { TerminalSquare, BrainCircuit, Wrench } from "lucide-react";
+
+const strengths = [
+ {
+ icon: TerminalSquare,
+ title: "Klassischer IT-Betrieb",
+ desc: "Server, Netzwerke, Logs, Zertifikate, Mailserver aus 30 Jahren Praxis. Fundiertes Wissen, das die Basis für jedes zuverlässige System bildet."
+ },
+ {
+ icon: BrainCircuit,
+ title: "KI-gestützte Entwicklung",
+ desc: "Vibe-Coding mit Claude Code & ChatGPT. Der direkte Weg von der Idee zur lauffähigen Anwendung, unterstützt durch modernste AI-Tools."
+ },
+ {
+ icon: Wrench,
+ title: "Praxisnahe Umsetzung",
+ desc: "Baut, testet, betreibt, dokumentiert und verbessert reale Systeme. Kein Elfenbeinturm-Engineering, sondern Lösungen, die in Produktion funktionieren."
+ }
+];
+
+export function Strengths() {
+ return (
+
+
+
+ 02. 3 Welten
+
+
+ Die besondere Stärke liegt in der Kombination aus klassischem Betrieb, moderner KI-Entwicklung und handfester Umsetzung.
+
+
+
+ {strengths.map((item, index) => (
+
+
+
+
+ {item.title}
+
+ {item.desc}
+
+
+ {/* Decorative line */}
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/artifacts/joachim-portfolio/src/components/ui/accordion.tsx b/artifacts/joachim-portfolio/src/components/ui/accordion.tsx
new file mode 100644
index 0000000..e1797c9
--- /dev/null
+++ b/artifacts/joachim-portfolio/src/components/ui/accordion.tsx
@@ -0,0 +1,55 @@
+import * as React from "react"
+import * as AccordionPrimitive from "@radix-ui/react-accordion"
+import { ChevronDown } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Accordion = AccordionPrimitive.Root
+
+const AccordionItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AccordionItem.displayName = "AccordionItem"
+
+const AccordionTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ svg]:rotate-180",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+))
+AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
+
+const AccordionContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+))
+AccordionContent.displayName = AccordionPrimitive.Content.displayName
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
diff --git a/artifacts/joachim-portfolio/src/components/ui/alert-dialog.tsx b/artifacts/joachim-portfolio/src/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000..fa2b442
--- /dev/null
+++ b/artifacts/joachim-portfolio/src/components/ui/alert-dialog.tsx
@@ -0,0 +1,139 @@
+import * as React from "react"
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+const AlertDialog = AlertDialogPrimitive.Root
+
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger
+
+const AlertDialogPortal = AlertDialogPrimitive.Portal
+
+const AlertDialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
+
+const AlertDialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+))
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
+
+const AlertDialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogHeader.displayName = "AlertDialogHeader"
+
+const AlertDialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogFooter.displayName = "AlertDialogFooter"
+
+const AlertDialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
+
+const AlertDialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogDescription.displayName =
+ AlertDialogPrimitive.Description.displayName
+
+const AlertDialogAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
+
+const AlertDialogCancel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+}
diff --git a/artifacts/joachim-portfolio/src/components/ui/alert.tsx b/artifacts/joachim-portfolio/src/components/ui/alert.tsx
new file mode 100644
index 0000000..5afd41d
--- /dev/null
+++ b/artifacts/joachim-portfolio/src/components/ui/alert.tsx
@@ -0,0 +1,59 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive:
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+))
+Alert.displayName = "Alert"
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertTitle.displayName = "AlertTitle"
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertDescription.displayName = "AlertDescription"
+
+export { Alert, AlertTitle, AlertDescription }
diff --git a/artifacts/joachim-portfolio/src/components/ui/aspect-ratio.tsx b/artifacts/joachim-portfolio/src/components/ui/aspect-ratio.tsx
new file mode 100644
index 0000000..c4abbf3
--- /dev/null
+++ b/artifacts/joachim-portfolio/src/components/ui/aspect-ratio.tsx
@@ -0,0 +1,5 @@
+import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
+
+const AspectRatio = AspectRatioPrimitive.Root
+
+export { AspectRatio }
diff --git a/artifacts/joachim-portfolio/src/components/ui/avatar.tsx b/artifacts/joachim-portfolio/src/components/ui/avatar.tsx
new file mode 100644
index 0000000..51e507b
--- /dev/null
+++ b/artifacts/joachim-portfolio/src/components/ui/avatar.tsx
@@ -0,0 +1,50 @@
+"use client"
+
+import * as React from "react"
+import * as AvatarPrimitive from "@radix-ui/react-avatar"
+
+import { cn } from "@/lib/utils"
+
+const Avatar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Avatar.displayName = AvatarPrimitive.Root.displayName
+
+const AvatarImage = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarImage.displayName = AvatarPrimitive.Image.displayName
+
+const AvatarFallback = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
+
+export { Avatar, AvatarImage, AvatarFallback }
diff --git a/artifacts/joachim-portfolio/src/components/ui/badge.tsx b/artifacts/joachim-portfolio/src/components/ui/badge.tsx
new file mode 100644
index 0000000..3f03665
--- /dev/null
+++ b/artifacts/joachim-portfolio/src/components/ui/badge.tsx
@@ -0,0 +1,43 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const badgeVariants = cva(
+ // @replit
+ // Whitespace-nowrap: Badges should never wrap.
+ "whitespace-nowrap inline-flex items-center rounded-md 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" +
+ " hover-elevate ",
+ {
+ variants: {
+ variant: {
+ default:
+ // @replit shadow-xs instead of shadow, no hover because we use hover-elevate
+ "border-transparent bg-primary text-primary-foreground shadow-xs",
+ secondary:
+ // @replit no hover because we use hover-elevate
+ "border-transparent bg-secondary text-secondary-foreground",
+ destructive:
+ // @replit shadow-xs instead of shadow, no hover because we use hover-elevate
+ "border-transparent bg-destructive text-destructive-foreground shadow-xs",
+ // @replit shadow-xs" - use badge outline variable
+ outline: "text-foreground border [border-color:var(--badge-outline)]",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/artifacts/joachim-portfolio/src/components/ui/breadcrumb.tsx b/artifacts/joachim-portfolio/src/components/ui/breadcrumb.tsx
new file mode 100644
index 0000000..60e6c96
--- /dev/null
+++ b/artifacts/joachim-portfolio/src/components/ui/breadcrumb.tsx
@@ -0,0 +1,115 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { ChevronRight, MoreHorizontal } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Breadcrumb = React.forwardRef<
+ HTMLElement,
+ React.ComponentPropsWithoutRef<"nav"> & {
+ separator?: React.ReactNode
+ }
+>(({ ...props }, ref) => )
+Breadcrumb.displayName = "Breadcrumb"
+
+const BreadcrumbList = React.forwardRef<
+ HTMLOListElement,
+ React.ComponentPropsWithoutRef<"ol">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbList.displayName = "BreadcrumbList"
+
+const BreadcrumbItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentPropsWithoutRef<"li">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbItem.displayName = "BreadcrumbItem"
+
+const BreadcrumbLink = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentPropsWithoutRef<"a"> & {
+ asChild?: boolean
+ }
+>(({ asChild, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : "a"
+
+ return (
+
+ )
+})
+BreadcrumbLink.displayName = "BreadcrumbLink"
+
+const BreadcrumbPage = React.forwardRef<
+ HTMLSpanElement,
+ React.ComponentPropsWithoutRef<"span">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbPage.displayName = "BreadcrumbPage"
+
+const BreadcrumbSeparator = ({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<"li">) => (
+ svg]:w-3.5 [&>svg]:h-3.5", className)}
+ {...props}
+ >
+ {children ?? }
+
+)
+BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
+
+const BreadcrumbEllipsis = ({
+ className,
+ ...props
+}: React.ComponentProps<"span">) => (
+
+
+ More
+
+)
+BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+}
diff --git a/artifacts/joachim-portfolio/src/components/ui/button-group.tsx b/artifacts/joachim-portfolio/src/components/ui/button-group.tsx
new file mode 100644
index 0000000..d6e7801
--- /dev/null
+++ b/artifacts/joachim-portfolio/src/components/ui/button-group.tsx
@@ -0,0 +1,83 @@
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+import { Separator } from "@/components/ui/separator"
+
+const buttonGroupVariants = cva(
+ "flex w-fit items-stretch has-[>[data-slot=button-group]]:gap-2 [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
+ {
+ variants: {
+ orientation: {
+ horizontal:
+ "[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
+ vertical:
+ "flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
+ },
+ },
+ defaultVariants: {
+ orientation: "horizontal",
+ },
+ }
+)
+
+function ButtonGroup({
+ className,
+ orientation,
+ ...props
+}: React.ComponentProps<"div"> & VariantProps) {
+ return (
+
+ )
+}
+
+function ButtonGroupText({
+ className,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"div"> & {
+ asChild?: boolean
+}) {
+ const Comp = asChild ? Slot : "div"
+
+ return (
+
+ )
+}
+
+function ButtonGroupSeparator({
+ className,
+ orientation = "vertical",
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export {
+ ButtonGroup,
+ ButtonGroupSeparator,
+ ButtonGroupText,
+ buttonGroupVariants,
+}
diff --git a/artifacts/joachim-portfolio/src/components/ui/button.tsx b/artifacts/joachim-portfolio/src/components/ui/button.tsx
new file mode 100644
index 0000000..16eb95d
--- /dev/null
+++ b/artifacts/joachim-portfolio/src/components/ui/button.tsx
@@ -0,0 +1,65 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0" +
+" hover-elevate active-elevate-2",
+ {
+ variants: {
+ variant: {
+ default:
+ // @replit: no hover, and add primary border
+ "bg-primary text-primary-foreground border border-primary-border",
+ destructive:
+ "bg-destructive text-destructive-foreground shadow-sm border-destructive-border",
+ outline:
+ // @replit Shows the background color of whatever card / sidebar / accent background it is inside of.
+ // Inherits the current text color. Uses shadow-xs. no shadow on active
+ // No hover state
+ " border [border-color:var(--button-outline)] shadow-xs active:shadow-none ",
+ secondary:
+ // @replit border, no hover, no shadow, secondary border.
+ "border bg-secondary text-secondary-foreground border border-secondary-border ",
+ // @replit no hover, transparent border
+ ghost: "border border-transparent",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ // @replit changed sizes
+ default: "min-h-9 px-4 py-2",
+ sm: "min-h-8 rounded-md px-3 text-xs",
+ lg: "min-h-10 rounded-md px-8",
+ icon: "h-9 w-9",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+
+ )
+ }
+)
+Button.displayName = "Button"
+
+export { Button, buttonVariants }
diff --git a/artifacts/joachim-portfolio/src/components/ui/calendar.tsx b/artifacts/joachim-portfolio/src/components/ui/calendar.tsx
new file mode 100644
index 0000000..a623682
--- /dev/null
+++ b/artifacts/joachim-portfolio/src/components/ui/calendar.tsx
@@ -0,0 +1,213 @@
+"use client"
+
+import * as React from "react"
+import {
+ ChevronDownIcon,
+ ChevronLeftIcon,
+ ChevronRightIcon,
+} from "lucide-react"
+import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
+
+import { cn } from "@/lib/utils"
+import { Button, buttonVariants } from "@/components/ui/button"
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ captionLayout = "label",
+ buttonVariant = "ghost",
+ formatters,
+ components,
+ ...props
+}: React.ComponentProps & {
+ buttonVariant?: React.ComponentProps["variant"]
+}) {
+ const defaultClassNames = getDefaultClassNames()
+
+ return (
+ svg]:rotate-180`,
+ String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
+ className
+ )}
+ captionLayout={captionLayout}
+ formatters={{
+ formatMonthDropdown: (date) =>
+ date.toLocaleString("default", { month: "short" }),
+ ...formatters,
+ }}
+ classNames={{
+ root: cn("w-fit", defaultClassNames.root),
+ months: cn(
+ "relative flex flex-col gap-4 md:flex-row",
+ defaultClassNames.months
+ ),
+ month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
+ nav: cn(
+ "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
+ defaultClassNames.nav
+ ),
+ button_previous: cn(
+ buttonVariants({ variant: buttonVariant }),
+ "h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50",
+ defaultClassNames.button_previous
+ ),
+ button_next: cn(
+ buttonVariants({ variant: buttonVariant }),
+ "h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50",
+ defaultClassNames.button_next
+ ),
+ month_caption: cn(
+ "flex h-[--cell-size] w-full items-center justify-center px-[--cell-size]",
+ defaultClassNames.month_caption
+ ),
+ dropdowns: cn(
+ "flex h-[--cell-size] w-full items-center justify-center gap-1.5 text-sm font-medium",
+ defaultClassNames.dropdowns
+ ),
+ dropdown_root: cn(
+ "has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border",
+ defaultClassNames.dropdown_root
+ ),
+ dropdown: cn(
+ "bg-popover absolute inset-0 opacity-0",
+ defaultClassNames.dropdown
+ ),
+ caption_label: cn(
+ "select-none font-medium",
+ captionLayout === "label"
+ ? "text-sm"
+ : "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5",
+ defaultClassNames.caption_label
+ ),
+ table: "w-full border-collapse",
+ weekdays: cn("flex", defaultClassNames.weekdays),
+ weekday: cn(
+ "text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal",
+ defaultClassNames.weekday
+ ),
+ week: cn("mt-2 flex w-full", defaultClassNames.week),
+ week_number_header: cn(
+ "w-[--cell-size] select-none",
+ defaultClassNames.week_number_header
+ ),
+ week_number: cn(
+ "text-muted-foreground select-none text-[0.8rem]",
+ defaultClassNames.week_number
+ ),
+ day: cn(
+ "group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md",
+ defaultClassNames.day
+ ),
+ range_start: cn(
+ "bg-accent rounded-l-md",
+ defaultClassNames.range_start
+ ),
+ range_middle: cn("rounded-none", defaultClassNames.range_middle),
+ range_end: cn("bg-accent rounded-r-md", defaultClassNames.range_end),
+ today: cn(
+ "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
+ defaultClassNames.today
+ ),
+ outside: cn(
+ "text-muted-foreground aria-selected:text-muted-foreground",
+ defaultClassNames.outside
+ ),
+ disabled: cn(
+ "text-muted-foreground opacity-50",
+ defaultClassNames.disabled
+ ),
+ hidden: cn("invisible", defaultClassNames.hidden),
+ ...classNames,
+ }}
+ components={{
+ Root: ({ className, rootRef, ...props }) => {
+ return (
+
+ )
+ },
+ Chevron: ({ className, orientation, ...props }) => {
+ if (orientation === "left") {
+ return (
+
+ )
+ }
+
+ if (orientation === "right") {
+ return (
+
+ )
+ }
+
+ return (
+
+ )
+ },
+ DayButton: CalendarDayButton,
+ WeekNumber: ({ children, ...props }) => {
+ return (
+
+
+ {children}
+
+
+ )
+ },
+ ...components,
+ }}
+ {...props}
+ />
+ )
+}
+
+function CalendarDayButton({
+ className,
+ day,
+ modifiers,
+ ...props
+}: React.ComponentProps) {
+ const defaultClassNames = getDefaultClassNames()
+
+ const ref = React.useRef(null)
+ React.useEffect(() => {
+ if (modifiers.focused) ref.current?.focus()
+ }, [modifiers.focused])
+
+ return (
+ span]:text-xs [&>span]:opacity-70",
+ defaultClassNames.day,
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+export { Calendar, CalendarDayButton }
diff --git a/artifacts/joachim-portfolio/src/components/ui/card.tsx b/artifacts/joachim-portfolio/src/components/ui/card.tsx
new file mode 100644
index 0000000..cabfbfc
--- /dev/null
+++ b/artifacts/joachim-portfolio/src/components/ui/card.tsx
@@ -0,0 +1,76 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Card.displayName = "Card"
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardHeader.displayName = "CardHeader"
+
+const CardTitle = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardTitle.displayName = "CardTitle"
+
+const CardDescription = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardDescription.displayName = "CardDescription"
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardContent.displayName = "CardContent"
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardFooter.displayName = "CardFooter"
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/artifacts/joachim-portfolio/src/components/ui/carousel.tsx b/artifacts/joachim-portfolio/src/components/ui/carousel.tsx
new file mode 100644
index 0000000..9c2b9bf
--- /dev/null
+++ b/artifacts/joachim-portfolio/src/components/ui/carousel.tsx
@@ -0,0 +1,260 @@
+import * as React from "react"
+import useEmblaCarousel, {
+ type UseEmblaCarouselType,
+} from "embla-carousel-react"
+import { ArrowLeft, ArrowRight } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+
+type CarouselApi = UseEmblaCarouselType[1]
+type UseCarouselParameters = Parameters
+type CarouselOptions = UseCarouselParameters[0]
+type CarouselPlugin = UseCarouselParameters[1]
+
+type CarouselProps = {
+ opts?: CarouselOptions
+ plugins?: CarouselPlugin
+ orientation?: "horizontal" | "vertical"
+ setApi?: (api: CarouselApi) => void
+}
+
+type CarouselContextProps = {
+ carouselRef: ReturnType[0]
+ api: ReturnType[1]
+ scrollPrev: () => void
+ scrollNext: () => void
+ canScrollPrev: boolean
+ canScrollNext: boolean
+} & CarouselProps
+
+const CarouselContext = React.createContext(null)
+
+function useCarousel() {
+ const context = React.useContext(CarouselContext)
+
+ if (!context) {
+ throw new Error("useCarousel must be used within a ")
+ }
+
+ return context
+}
+
+const Carousel = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & CarouselProps
+>(
+ (
+ {
+ orientation = "horizontal",
+ opts,
+ setApi,
+ plugins,
+ className,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ const [carouselRef, api] = useEmblaCarousel(
+ {
+ ...opts,
+ axis: orientation === "horizontal" ? "x" : "y",
+ },
+ plugins
+ )
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false)
+ const [canScrollNext, setCanScrollNext] = React.useState(false)
+
+ const onSelect = React.useCallback((api: CarouselApi) => {
+ if (!api) {
+ return
+ }
+
+ setCanScrollPrev(api.canScrollPrev())
+ setCanScrollNext(api.canScrollNext())
+ }, [])
+
+ const scrollPrev = React.useCallback(() => {
+ api?.scrollPrev()
+ }, [api])
+
+ const scrollNext = React.useCallback(() => {
+ api?.scrollNext()
+ }, [api])
+
+ const handleKeyDown = React.useCallback(
+ (event: React.KeyboardEvent) => {
+ if (event.key === "ArrowLeft") {
+ event.preventDefault()
+ scrollPrev()
+ } else if (event.key === "ArrowRight") {
+ event.preventDefault()
+ scrollNext()
+ }
+ },
+ [scrollPrev, scrollNext]
+ )
+
+ React.useEffect(() => {
+ if (!api || !setApi) {
+ return
+ }
+
+ setApi(api)
+ }, [api, setApi])
+
+ React.useEffect(() => {
+ if (!api) {
+ return
+ }
+
+ onSelect(api)
+ api.on("reInit", onSelect)
+ api.on("select", onSelect)
+
+ return () => {
+ api?.off("select", onSelect)
+ }
+ }, [api, onSelect])
+
+ return (
+
+
+ {children}
+
+
+ )
+ }
+)
+Carousel.displayName = "Carousel"
+
+const CarouselContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { carouselRef, orientation } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselContent.displayName = "CarouselContent"
+
+const CarouselItem = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { orientation } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselItem.displayName = "CarouselItem"
+
+const CarouselPrevious = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel()
+
+ return (
+
+
+ Previous slide
+
+ )
+})
+CarouselPrevious.displayName = "CarouselPrevious"
+
+const CarouselNext = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollNext, canScrollNext } = useCarousel()
+
+ return (
+
+
+ Next slide
+
+ )
+})
+CarouselNext.displayName = "CarouselNext"
+
+export {
+ type CarouselApi,
+ Carousel,
+ CarouselContent,
+ CarouselItem,
+ CarouselPrevious,
+ CarouselNext,
+}
diff --git a/artifacts/joachim-portfolio/src/components/ui/chart.tsx b/artifacts/joachim-portfolio/src/components/ui/chart.tsx
new file mode 100644
index 0000000..23dc1c1
--- /dev/null
+++ b/artifacts/joachim-portfolio/src/components/ui/chart.tsx
@@ -0,0 +1,367 @@
+import * as React from "react"
+import * as RechartsPrimitive from "recharts"
+
+import { cn } from "@/lib/utils"
+
+// Format: { THEME_NAME: CSS_SELECTOR }
+const THEMES = { light: "", dark: ".dark" } as const
+
+export type ChartConfig = {
+ [k in string]: {
+ label?: React.ReactNode
+ icon?: React.ComponentType
+ } & (
+ | { color?: string; theme?: never }
+ | { color?: never; theme: Record }
+ )
+}
+
+type ChartContextProps = {
+ config: ChartConfig
+}
+
+const ChartContext = React.createContext(null)
+
+function useChart() {
+ const context = React.useContext(ChartContext)
+
+ if (!context) {
+ throw new Error("useChart must be used within a ")
+ }
+
+ return context
+}
+
+const ChartContainer = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ config: ChartConfig
+ children: React.ComponentProps<
+ typeof RechartsPrimitive.ResponsiveContainer
+ >["children"]
+ }
+>(({ id, className, children, config, ...props }, ref) => {
+ const uniqueId = React.useId()
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
+
+ return (
+
+
+
+
+ {children}
+
+
+
+ )
+})
+ChartContainer.displayName = "Chart"
+
+const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
+ const colorConfig = Object.entries(config).filter(
+ ([, config]) => config.theme || config.color
+ )
+
+ if (!colorConfig.length) {
+ return null
+ }
+
+ return (
+