Promote AgentCompany-New to the repository root and retire the old app
The legacy application - its src, tests, landing site, promo assets, docs, release tooling, CI workflows, and root configs - leaves the tree; it stays reachable in history. Everything under AgentCompany-New moves up unchanged: same package name, so the production userData/database path is untouched, and e2e/build scripts already resolved paths relative to the project. The developer cred.txt stays gitignored at its new location. Full verify:ci passes from the new root.
This commit is contained in:
1 parent
2350f744e3
commit
07d0b4587d
1904 files changed
+1133
-282075
No files matched your search
@@ -1,158 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
const glyphs = " .'`^,:;Il!i><~+_-?][}{1)(|\\/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$";
|
||||
|
||||
function parseRgbHex(hex: string): [number, number, number] {
|
||||
const value = hex.replace("#", "").trim();
|
||||
if (value.length !== 6) return [128, 128, 128];
|
||||
return [
|
||||
Number.parseInt(value.slice(0, 2), 16),
|
||||
Number.parseInt(value.slice(2, 4), 16),
|
||||
Number.parseInt(value.slice(4, 6), 16),
|
||||
];
|
||||
}
|
||||
|
||||
export function AsciiBackground({
|
||||
cssVar = "--accent",
|
||||
baseOpacity = 0.08,
|
||||
speed = 1,
|
||||
}: {
|
||||
cssVar?: string;
|
||||
baseOpacity?: number;
|
||||
speed?: number;
|
||||
}) {
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
const container = containerRef.current;
|
||||
if (!canvas || !container) return;
|
||||
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) return;
|
||||
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
let rgb: [number, number, number] = [128, 128, 128];
|
||||
let phase = 0;
|
||||
let frame = 0;
|
||||
let isVisible = true;
|
||||
const pointer = { x: -9999, y: -9999 };
|
||||
|
||||
const updateColor = () => {
|
||||
const value = getComputedStyle(document.documentElement).getPropertyValue(cssVar).trim();
|
||||
if (value.startsWith("#")) {
|
||||
rgb = parseRgbHex(value);
|
||||
}
|
||||
};
|
||||
|
||||
updateColor();
|
||||
|
||||
const mutationObserver = new MutationObserver(updateColor);
|
||||
mutationObserver.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["class"],
|
||||
});
|
||||
|
||||
const intersectionObserver = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const [entry] = entries;
|
||||
isVisible = entry?.isIntersecting ?? true;
|
||||
},
|
||||
{ threshold: 0 },
|
||||
);
|
||||
intersectionObserver.observe(container);
|
||||
|
||||
const resize = () => {
|
||||
width = container.clientWidth;
|
||||
height = container.clientHeight;
|
||||
const devicePixelRatio = Math.min(window.devicePixelRatio || 1, 2);
|
||||
canvas.width = width * devicePixelRatio;
|
||||
canvas.height = height * devicePixelRatio;
|
||||
context.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
|
||||
};
|
||||
|
||||
const handlePointer = (event: MouseEvent) => {
|
||||
const rect = container.getBoundingClientRect();
|
||||
pointer.x = event.clientX - rect.left;
|
||||
pointer.y = event.clientY - rect.top;
|
||||
};
|
||||
|
||||
const render = () => {
|
||||
frame = requestAnimationFrame(render);
|
||||
if (!isVisible) return;
|
||||
|
||||
context.clearRect(0, 0, width, height);
|
||||
context.font = "14px monospace";
|
||||
context.textAlign = "center";
|
||||
context.textBaseline = "middle";
|
||||
|
||||
const columns = Math.ceil(width / 14);
|
||||
const rows = Math.ceil(height / 14);
|
||||
const [red, green, blue] = rgb;
|
||||
const pointerX = pointer.x;
|
||||
const pointerY = pointer.y;
|
||||
|
||||
for (let row = 0; row < rows; row += 1) {
|
||||
for (let column = 0; column < columns; column += 1) {
|
||||
const centerX = column * 14 + 7;
|
||||
const centerY = row * 14 + 7;
|
||||
const value =
|
||||
Math.sin(0.05 * column + phase) * Math.cos(0.05 * row + phase) +
|
||||
Math.sin(0.012 * column - 0.7 * phase) * Math.cos(0.1 * row) * 0.5;
|
||||
|
||||
if (Math.abs(value) < 0.3) continue;
|
||||
|
||||
const index = Math.floor(Math.abs(value) * glyphs.length);
|
||||
let glyph = glyphs[index % glyphs.length];
|
||||
if (!glyph.trim()) continue;
|
||||
|
||||
let alpha = Math.abs(value) * baseOpacity;
|
||||
const deltaX = centerX - pointerX;
|
||||
const deltaY = centerY - pointerY;
|
||||
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||
|
||||
if (distance < 110) {
|
||||
const intensity = 1 - distance / 110;
|
||||
glyph = Math.random() > 0.5 ? "0" : "1";
|
||||
alpha = 0.35 * intensity;
|
||||
const drift = distance > 1 ? 7 * intensity : 0;
|
||||
const offsetX = (deltaX / distance) * drift;
|
||||
const offsetY = (deltaY / distance) * drift;
|
||||
context.fillStyle = `rgba(${red},${green},${blue},${alpha})`;
|
||||
context.fillText(glyph, centerX - offsetX, centerY - offsetY);
|
||||
} else {
|
||||
context.fillStyle = `rgba(${red},${green},${blue},${alpha})`;
|
||||
context.fillText(glyph, centerX, centerY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
phase += 0.005 * speed;
|
||||
};
|
||||
|
||||
resize();
|
||||
window.addEventListener("resize", resize);
|
||||
window.addEventListener("mousemove", handlePointer);
|
||||
render();
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(frame);
|
||||
window.removeEventListener("resize", resize);
|
||||
window.removeEventListener("mousemove", handlePointer);
|
||||
mutationObserver.disconnect();
|
||||
intersectionObserver.disconnect();
|
||||
};
|
||||
}, [baseOpacity, cssVar, speed]);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="absolute inset-0 overflow-hidden pointer-events-none" aria-hidden="true">
|
||||
<canvas ref={canvasRef} className="block w-full h-full" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { platformDownloads, releaseVersion } from "./release-links.generated";
|
||||
import { DOWNLOAD_COUNT_SEED, getTrackedDownloadHref } from "../lib/downloads";
|
||||
|
||||
export function DownloadSection() {
|
||||
const [downloadCount, setDownloadCount] = useState(DOWNLOAD_COUNT_SEED);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
async function loadDownloadCount() {
|
||||
try {
|
||||
const response = await fetch("/api/download-stats", { cache: "no-store" });
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch download count: ${response.status}`);
|
||||
}
|
||||
const data = await response.json() as { downloads?: number };
|
||||
if (!cancelled && typeof data.downloads === "number") {
|
||||
setDownloadCount(data.downloads);
|
||||
}
|
||||
} catch {
|
||||
// Keep the seeded count visible when the dynamic endpoint is unavailable.
|
||||
}
|
||||
}
|
||||
|
||||
void loadDownloadCount();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section id="download" className="py-24 lg:py-40 px-4 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="text-center mb-16 lg:mb-20"
|
||||
>
|
||||
<p className="font-mono text-xs font-medium uppercase tracking-[0.25em] text-[var(--accent)] mb-5">
|
||||
Download
|
||||
</p>
|
||||
<h2 className="font-serif text-[clamp(2rem,1.5rem+2.5vw,3.25rem)] tracking-[-0.01em] text-[var(--text)]">
|
||||
Install the Right Build for Your Machine
|
||||
</h2>
|
||||
<p className="text-[var(--muted)] text-lg mt-5 leading-relaxed">
|
||||
Version {releaseVersion} · {downloadCount.toLocaleString("en-US")} downloads
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 lg:gap-8">
|
||||
{platformDownloads.map((download, index) => (
|
||||
<motion.div
|
||||
key={download.id}
|
||||
initial={{ opacity: 0, y: 24 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
transition={{ duration: 0.5, delay: 0.08 * index, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="rounded-[22px] border border-[var(--border)] bg-[var(--panel)] p-6 lg:p-7"
|
||||
>
|
||||
<p className="font-mono text-xs uppercase tracking-[0.25em] text-[var(--muted)] mb-4">
|
||||
{download.title}
|
||||
</p>
|
||||
<p className="text-xs text-[var(--muted)] mb-4">{download.note}</p>
|
||||
<div className="space-y-2.5">
|
||||
{download.assets.map((asset) => (
|
||||
<a
|
||||
key={asset.id}
|
||||
href={getTrackedDownloadHref(asset.id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-between rounded-[16px] border border-[var(--border)] bg-[var(--panel-soft)] px-4 py-3 text-sm text-[var(--text)] transition-colors duration-200 hover:border-[var(--accent)] hover:text-[var(--accent)]"
|
||||
>
|
||||
<span className="font-semibold tracking-tight">{asset.label}</span>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
className="shrink-0 ml-2 opacity-40"
|
||||
>
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<polyline points="7 10 12 15 17 10" />
|
||||
<line x1="12" y1="15" x2="12" y2="3" />
|
||||
</svg>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { useState } from "react";
|
||||
import { platformSupportSummary } from "../lib/platform-support";
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
question: "What is AgentCompany?",
|
||||
answer:
|
||||
"AgentCompany is a local-first desktop application for launching and operating autonomous AI companies. You define the mission, an AI CEO organizes the team, delegates work, and coordinates delivery across engineering, design, research, and operations.",
|
||||
},
|
||||
{
|
||||
question: "How do I download AgentCompany?",
|
||||
answer: platformSupportSummary.downloadAnswer,
|
||||
},
|
||||
{
|
||||
question: "What platforms does AgentCompany support?",
|
||||
answer: platformSupportSummary.platformAnswer,
|
||||
},
|
||||
{
|
||||
question: "What AI models can AgentCompany use?",
|
||||
answer:
|
||||
"AgentCompany supports multiple AI runtimes as first-class connectors: Claude Code (Anthropic), Codex CLI (OpenAI), and Gemini CLI (Google). You can connect your preferred provider during setup.",
|
||||
},
|
||||
{
|
||||
question: "How does the AI CEO work?",
|
||||
answer:
|
||||
"When you create a company with a goal, the AI CEO agent analyzes the objective, creates the necessary departments (Engineering, Design, Marketing, etc.), hires specialized agents for each role, builds an org chart, and delegates tasks. The CEO monitors progress, handles blockers, and coordinates cross-department work — all autonomously.",
|
||||
},
|
||||
{
|
||||
question: "Is my data private? Does it leave my machine?",
|
||||
answer:
|
||||
"AgentCompany is local-first. All data is stored in a local SQLite database on your machine. Secrets are encrypted via an OS-level keychain. The only external communication is between your machine and your AI provider's API — no data is sent to AgentCompany servers.",
|
||||
},
|
||||
{
|
||||
question: "What can an AI company actually deliver?",
|
||||
answer:
|
||||
"AI companies built with AgentCompany can produce code (full applications, scripts, APIs), content (blog posts, marketing copy, social media), research (competitive analysis, market research), business documents (business plans, technical specs, strategies), and more. The deliverables depend on the goal you set and the AI runtime you connect.",
|
||||
},
|
||||
{
|
||||
question: "How are updates delivered?",
|
||||
answer:
|
||||
"Desktop updates are published to a Cloudflare-hosted release lane. macOS builds include the signed update metadata required for in-app update checks, while the Windows and Linux installers and packages are distributed from the same release host.",
|
||||
},
|
||||
];
|
||||
|
||||
function FaqItem({ question, answer, index }: { question: string; answer: string; index: number }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
transition={{ duration: 0.5, delay: 0.06 * index, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="border-b border-[var(--border)] last:border-b-0"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((value) => !value)}
|
||||
aria-expanded={open}
|
||||
className="flex w-full items-center justify-between gap-4 py-6 text-left transition-colors duration-200 hover:text-[var(--accent)]"
|
||||
>
|
||||
<h3 className="text-base lg:text-lg font-semibold text-[var(--text)] pr-4">{question}</h3>
|
||||
<span
|
||||
className={`shrink-0 text-[var(--muted)] transition-transform duration-300 ${open ? "rotate-45" : ""}`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div className={`overflow-hidden transition-all duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] ${open ? "max-h-96 pb-6" : "max-h-0"}`}>
|
||||
<p className="text-[var(--muted-strong)] leading-relaxed text-base pr-12">{answer}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FAQ() {
|
||||
return (
|
||||
<section id="faq" className="py-24 lg:py-40 px-4 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="text-center mb-16 lg:mb-20"
|
||||
>
|
||||
<p className="font-mono text-xs font-medium uppercase tracking-[0.25em] text-[var(--accent)] mb-5">FAQ</p>
|
||||
<h2 className="font-serif text-[clamp(2rem,1.5rem+2.5vw,3.25rem)] tracking-[-0.01em] text-[var(--text)]">
|
||||
Frequently Asked Questions
|
||||
</h2>
|
||||
</motion.div>
|
||||
|
||||
<div className="border-t border-[var(--border)]">
|
||||
{faqs.map((item, index) => (
|
||||
<FaqItem key={item.question} question={item.question} answer={item.answer} index={index} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
const features = [
|
||||
{
|
||||
number: "01",
|
||||
title: "One-Click Company Creation",
|
||||
description:
|
||||
"Name your company, describe your mission, connect your AI runtime, and launch. Your first agents start working in under a minute.",
|
||||
image: "/screenshots/Onboarding.jpeg",
|
||||
imageAlt: "Company creation onboarding flow with mission and runtime setup",
|
||||
imageLeft: true,
|
||||
},
|
||||
{
|
||||
number: "02",
|
||||
title: "Self-Organizing Teams",
|
||||
description:
|
||||
"Your AI CEO builds the org chart. It identifies the roles needed, hires specialists, and creates reporting lines — all from a single goal.",
|
||||
image: "/screenshots/Auto-OrgChart-Team.jpeg",
|
||||
imageAlt: "AI-generated org chart showing self-organizing team structure",
|
||||
imageLeft: false,
|
||||
},
|
||||
{
|
||||
number: "03",
|
||||
title: "Autonomous Task Management",
|
||||
description:
|
||||
"Goals break down into tasks automatically. Agents pick up work, report progress, and escalate blockers — you decide when to step in.",
|
||||
image: "/screenshots/Auto-Task.jpeg",
|
||||
imageAlt: "Autonomous task board with agents managing work items",
|
||||
imageLeft: true,
|
||||
},
|
||||
{
|
||||
number: "04",
|
||||
title: "Real-Time Agent Communication",
|
||||
description:
|
||||
"Agents communicate through department channels and direct messages. Watch strategic decisions happen in real time, like a Slack workspace you built.",
|
||||
image: "/screenshots/AI-Internal-Realtime-Message.jpeg",
|
||||
imageAlt: "Real-time messaging between AI agents in department channels",
|
||||
},
|
||||
{
|
||||
number: "05",
|
||||
title: "Human-in-the-Loop Approvals",
|
||||
description:
|
||||
"Stay in control. Critical decisions surface for your review. Approve, reject, or redirect — your company respects the chain of command.",
|
||||
image: "/screenshots/Approval-by-Human.jpeg",
|
||||
imageAlt: "Approval interface showing human review of agent decisions",
|
||||
},
|
||||
{
|
||||
number: "06",
|
||||
title: "AI Document Generation",
|
||||
description:
|
||||
"Business plans, marketing strategies, technical specs — agents draft and iterate on real deliverables, stored in your document center.",
|
||||
image: "/screenshots/Document-Center.jpeg",
|
||||
imageAlt: "Document center with AI-generated business deliverables",
|
||||
},
|
||||
];
|
||||
|
||||
function FeaturePair({ feature }: { feature: (typeof features)[number] }) {
|
||||
const { number, title, description, image, imageAlt, imageLeft } = feature;
|
||||
const ease: [number, number, number, number] = [0.16, 1, 0.3, 1];
|
||||
|
||||
const media = (
|
||||
<motion.div
|
||||
variants={{
|
||||
hidden: { opacity: 0, x: imageLeft ? -60 : 60 },
|
||||
visible: { opacity: 1, x: 0, transition: { duration: 0.7, ease } },
|
||||
}}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
className={`order-2 ${imageLeft ? "lg:order-1" : "lg:order-2"}`}
|
||||
>
|
||||
<div className="rounded-[22px] overflow-hidden shadow-[0_4px_32px_-8px_rgba(0,0,0,0.06)] border border-[var(--border)] bg-[var(--panel)] transition-shadow duration-500 hover:shadow-[0_8px_48px_-12px_rgba(0,0,0,0.10)]">
|
||||
<img src={image} alt={imageAlt} width="1920" height="1080" className="w-full h-auto block" loading="lazy" />
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
const copy = (
|
||||
<motion.div
|
||||
variants={{
|
||||
hidden: { opacity: 0, x: imageLeft ? 60 : -60 },
|
||||
visible: { opacity: 1, x: 0, transition: { duration: 0.7, ease, delay: 0.1 } },
|
||||
}}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
className={`flex flex-col justify-center order-1 ${imageLeft ? "lg:order-2" : "lg:order-1"}`}
|
||||
>
|
||||
<span className="font-mono text-[5rem] lg:text-[6rem] font-bold leading-none text-[var(--accent)] opacity-[0.07] select-none -mb-10 lg:-mb-12">
|
||||
{number}
|
||||
</span>
|
||||
<div className="relative">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<span className="block w-2 h-2 rounded-full bg-[var(--accent)]" />
|
||||
<span className="font-mono text-sm text-[var(--muted)]">{number}</span>
|
||||
</div>
|
||||
<h2 className="text-2xl lg:text-[1.875rem] font-bold text-[var(--text)] mb-4 tracking-tight">{title}</h2>
|
||||
<p className="text-[var(--muted-strong)] leading-relaxed text-base lg:text-lg">{description}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20 items-center">
|
||||
{media}
|
||||
{copy}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Features() {
|
||||
return (
|
||||
<section
|
||||
id="features"
|
||||
className="py-24 lg:py-40"
|
||||
style={{ background: "linear-gradient(180deg, transparent 0%, var(--section-alt) 30%, var(--section-alt) 70%, transparent 100%)" }}
|
||||
>
|
||||
<div className="max-w-6xl mx-auto px-6 lg:px-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="text-center mb-24 lg:mb-36"
|
||||
>
|
||||
<span className="font-mono text-xs uppercase tracking-[0.25em] text-[var(--accent)] mb-5 block font-medium">
|
||||
Features
|
||||
</span>
|
||||
<h2 className="font-serif text-[clamp(2rem,1.5rem+2.5vw,3.25rem)] tracking-[-0.01em] text-[var(--text)]">
|
||||
Your Company, Under the Hood
|
||||
</h2>
|
||||
</motion.div>
|
||||
|
||||
<div className="space-y-24 lg:space-y-40">
|
||||
{features.slice(0, 3).map((feature) => (
|
||||
<FeaturePair key={feature.number} feature={feature} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
className="mt-24 lg:mt-40 grid grid-cols-1 md:grid-cols-3 gap-6 lg:gap-8"
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.2 }}
|
||||
variants={{
|
||||
hidden: {},
|
||||
visible: {
|
||||
transition: { staggerChildren: 0.12 },
|
||||
},
|
||||
}}
|
||||
>
|
||||
{features.slice(3).map((feature) => (
|
||||
<motion.div
|
||||
key={feature.number}
|
||||
variants={{
|
||||
hidden: { opacity: 0, y: 32 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { duration: 0.6, ease: [0.16, 1, 0.3, 1] },
|
||||
},
|
||||
}}
|
||||
className="group bg-[var(--panel)] rounded-[22px] border border-[var(--border)] overflow-hidden transition-all duration-300 hover:shadow-[0_8px_40px_-12px_rgba(0,0,0,0.08)] hover:-translate-y-1.5"
|
||||
>
|
||||
<div className="overflow-hidden">
|
||||
<img
|
||||
src={feature.image}
|
||||
alt={feature.imageAlt}
|
||||
width="640"
|
||||
height="360"
|
||||
className="w-full h-auto block transition-transform duration-500 group-hover:scale-[1.03]"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<div className="p-6 lg:p-7">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<span className="block w-2 h-2 rounded-full bg-[var(--accent)]" />
|
||||
<span className="font-mono text-sm text-[var(--muted)]">{feature.number}</span>
|
||||
</div>
|
||||
<h2 className="text-lg font-bold text-[var(--text)] mb-2 tracking-tight">{feature.title}</h2>
|
||||
<p className="text-sm text-[var(--muted-strong)] leading-relaxed">{feature.description}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import { AsciiBackground } from "./ascii-background";
|
||||
import { IconMark } from "./icon-mark";
|
||||
import { CONTACT_EMAIL, WEBSITE_URL } from "./site-constants";
|
||||
import { ThemeToggle } from "./theme-toggle";
|
||||
import { getTrackedDownloadHref } from "../lib/downloads";
|
||||
|
||||
const footerGroups = [
|
||||
{
|
||||
title: "Product",
|
||||
links: [
|
||||
{ label: "Features", href: "#features" },
|
||||
{ label: "How It Works", href: "#how-it-works" },
|
||||
{ label: "FAQ", href: "#faq" },
|
||||
{ label: "Download", href: "#download" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Downloads",
|
||||
links: [
|
||||
{ label: "Apple Silicon", href: getTrackedDownloadHref("macos-arm64-dmg"), external: true },
|
||||
{ label: "Intel Mac", href: getTrackedDownloadHref("macos-x64-dmg"), external: true },
|
||||
{ label: "Windows Installer", href: getTrackedDownloadHref("windows-universal-installer"), external: true },
|
||||
{ label: "Linux x64 AppImage", href: getTrackedDownloadHref("linux-x64-appimage"), external: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Company",
|
||||
links: [
|
||||
{ label: "Website", href: WEBSITE_URL, external: true },
|
||||
{ label: "Contact", href: `mailto:${CONTACT_EMAIL}`, external: true },
|
||||
{ label: "Download Center", href: "#download" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function Footer() {
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
<footer className="relative border-t border-[var(--border)] overflow-hidden">
|
||||
<AsciiBackground cssVar="--accent" baseOpacity={0.04} speed={0.5} />
|
||||
|
||||
<div className="relative z-10 mx-auto max-w-6xl px-6 lg:px-8 pt-16 lg:pt-20 pb-10 lg:pb-12">
|
||||
<div className="flex flex-col lg:flex-row gap-y-12 gap-x-8">
|
||||
<div className="shrink-0 lg:w-64 mb-4 lg:mb-0">
|
||||
<a href="#" className="inline-flex items-center gap-2.5" aria-label="AgentCompany home">
|
||||
<IconMark size={28} />
|
||||
<span className="text-[var(--text)] font-semibold text-lg tracking-tight">AgentCompany</span>
|
||||
</a>
|
||||
<p className="mt-4 text-sm leading-relaxed text-[var(--muted)] max-w-xs">
|
||||
Local-first desktop control plane for autonomous AI companies.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex flex-wrap gap-y-12 gap-x-8 justify-between">
|
||||
{footerGroups.map((group) => (
|
||||
<div key={group.title} className="min-w-[120px]">
|
||||
<h3 className="text-sm font-semibold text-[var(--text)] tracking-tight mb-4">{group.title}</h3>
|
||||
<ul className="space-y-3">
|
||||
{group.links.map((link) => (
|
||||
<li key={link.label}>
|
||||
<a
|
||||
href={link.href}
|
||||
{...(link.external ? { target: "_blank", rel: "noopener noreferrer" } : {})}
|
||||
className="text-sm text-[var(--muted)] hover:text-[var(--text)] transition-colors duration-200"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-14 lg:mt-16 pt-6 border-t border-[var(--border)]" />
|
||||
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div className="flex items-center gap-4 text-xs text-[var(--muted)]">
|
||||
<span>© {year} AgentCompany</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { AsciiBackground } from "./ascii-background";
|
||||
import { landingHeroNarrative } from "../lib/product-narrative";
|
||||
|
||||
const sharedEase: [number, number, number, number] = [0.16, 1, 0.3, 1];
|
||||
|
||||
function heroMotion(delay: number) {
|
||||
return {
|
||||
initial: { opacity: 0, y: 24 },
|
||||
animate: { opacity: 1, y: 0 },
|
||||
transition: { duration: 0.7, delay, ease: sharedEase },
|
||||
};
|
||||
}
|
||||
|
||||
function AppleIcon() {
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function WindowsIcon() {
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M3 12V6.5l8-1.1V12H3zm0 .5h8v6.6l-8-1.1V12.5zM11.5 5.3l9.5-1.3v8h-9.5V5.3zm0 7.2h9.5v8l-9.5-1.3V12.5z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function LinuxIcon() {
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M12.5 2C10 2 8.2 4.2 8.2 7c0 1.5.3 2.2.7 3.2.3.8.7 1.7.7 3 0 .3 0 .6-.1.9-.2 1-.6 1.8-1.2 2.3-.8.7-1.8 1.1-2.8 1.6-.7.3-1.4.7-1.9 1.2-.4.4-.6.9-.6 1.4 0 .5.2.9.6 1.2.7.6 2 .7 3.1.7.8 0 1.6-.1 2.2-.2.6-.1 1.2-.3 1.9-.3.7 0 1.3.2 1.9.3.6.1 1.4.2 2.2.2 1.1 0 2.4-.1 3.1-.7.4-.3.6-.7.6-1.2 0-.5-.2-1-.6-1.4-.5-.5-1.2-.9-1.9-1.2-1-.5-2-1-2.8-1.6-.6-.5-1-1.3-1.2-2.3-.1-.3-.1-.6-.1-.9 0-1.3.4-2.2.7-3 .4-1 .7-1.7.7-3.2 0-2.8-1.8-5-4.3-5h-.2zm-1 5c0-.8.5-1.5 1.2-1.5s1.2.7 1.2 1.5-.5 1.5-1.2 1.5S11.5 7.8 11.5 7z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function Hero() {
|
||||
return (
|
||||
<section className="relative overflow-hidden pt-32 pb-24 lg:pt-44 lg:pb-36">
|
||||
<AsciiBackground cssVar="--accent" baseOpacity={0.06} speed={1} />
|
||||
<div
|
||||
className="absolute inset-0 pointer-events-none"
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(ellipse 80% 50% at 50% 0%, var(--glow-warm) 0%, transparent 70%), radial-gradient(ellipse 60% 40% at 50% 20%, var(--glow-accent) 0%, transparent 60%)",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative mx-auto max-w-6xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<motion.div {...heroMotion(0.05)}>
|
||||
<span className="inline-flex items-center px-4 py-1.5 rounded-full text-xs font-semibold tracking-wide bg-[var(--accent-soft)] text-[var(--accent)] dark:text-[var(--muted-strong)] border border-[var(--border)]">
|
||||
{landingHeroNarrative.eyebrow}
|
||||
</span>
|
||||
</motion.div>
|
||||
|
||||
<motion.h1
|
||||
{...heroMotion(0.15)}
|
||||
className="mt-8 font-serif text-[clamp(2.75rem,2rem+3.8vw,5rem)] font-normal leading-[1.0] tracking-[-0.03em] text-[var(--text)]"
|
||||
>
|
||||
{landingHeroNarrative.titleLine1} <br className="hidden sm:block" />
|
||||
<span className="text-[var(--accent)]">{landingHeroNarrative.titleLine2}</span>
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
{...heroMotion(0.25)}
|
||||
className="mt-7 max-w-2xl text-lg sm:text-xl leading-relaxed text-[var(--muted)]"
|
||||
>
|
||||
{landingHeroNarrative.description}
|
||||
</motion.p>
|
||||
|
||||
<motion.div {...heroMotion(0.35)} className="mt-12">
|
||||
<a
|
||||
href="#download"
|
||||
className="group inline-flex items-center justify-center px-8 py-4 rounded-full text-base font-medium bg-[var(--cta)] text-[var(--text-on-cta)] hover:bg-[var(--cta-hover)] hover:scale-[1.03] active:scale-[0.98] transition-all duration-200 will-change-transform"
|
||||
>
|
||||
See Downloads
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
className="ml-2 transition-transform duration-200 group-hover:translate-x-0.5 group-hover:-translate-y-0.5"
|
||||
>
|
||||
<path d="m7 7 10 10" />
|
||||
<path d="M17 7v10H7" />
|
||||
</svg>
|
||||
</a>
|
||||
</motion.div>
|
||||
|
||||
<motion.div {...heroMotion(0.45)} className="mt-5 flex items-center gap-3 text-xs text-[var(--muted)]">
|
||||
<div className="flex items-center gap-1.5 opacity-70">
|
||||
<AppleIcon />
|
||||
<WindowsIcon />
|
||||
<LinuxIcon />
|
||||
</div>
|
||||
<span>{landingHeroNarrative.availability}</span>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 40, scale: 0.94 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
transition={{ duration: 0.9, delay: 0.5, ease: sharedEase }}
|
||||
className="mt-20 lg:mt-24 mx-auto max-w-5xl"
|
||||
>
|
||||
<img
|
||||
src="/screenshots/Dashboard.jpeg"
|
||||
alt="AgentCompany dashboard showing autonomous AI agents managing a company"
|
||||
width="1920"
|
||||
height="1080"
|
||||
className="w-full h-auto block rounded-[22px] border border-[var(--border)] shadow-[0_8px_40px_-8px_rgba(0,0,0,0.08)] transition-shadow duration-500"
|
||||
loading="eager"
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { motion, useInView } from "framer-motion";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { landingHowItWorksNarrative } from "../lib/product-narrative";
|
||||
|
||||
const demoMessages = landingHowItWorksNarrative.demoMessages;
|
||||
|
||||
const logTone: Record<(typeof demoMessages)[number]["role"], string> = {
|
||||
system: "text-[var(--muted-strong)]",
|
||||
};
|
||||
|
||||
const dotTone: Record<(typeof demoMessages)[number]["role"], string> = {
|
||||
system: "bg-[var(--muted-strong)]",
|
||||
};
|
||||
|
||||
const steps = landingHowItWorksNarrative.steps;
|
||||
|
||||
function DemoPanel() {
|
||||
const anchorRef = useRef<HTMLDivElement | null>(null);
|
||||
const isVisible = useInView(anchorRef, { once: true, amount: 0.4 });
|
||||
const [visibleCount, setVisibleCount] = useState(0);
|
||||
const logRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isVisible) return;
|
||||
|
||||
const timers = demoMessages.map((message, index) =>
|
||||
window.setTimeout(() => setVisibleCount(index + 1), message.delay),
|
||||
);
|
||||
|
||||
return () => timers.forEach((timer) => window.clearTimeout(timer));
|
||||
}, [isVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (logRef.current) {
|
||||
logRef.current.scrollTop = logRef.current.scrollHeight;
|
||||
}
|
||||
}, [visibleCount]);
|
||||
|
||||
return (
|
||||
<div ref={anchorRef} className="relative">
|
||||
<div className="rounded-[22px] overflow-hidden border border-[var(--border)] bg-[var(--panel)] shadow-[0_8px_40px_-8px_rgba(0,0,0,0.06)]">
|
||||
<div className="flex items-center gap-2 px-4 py-3 bg-[var(--panel-soft)] border-b border-[var(--border)]">
|
||||
<span className="w-3 h-3 rounded-full bg-[#ff5f57]" aria-hidden="true" />
|
||||
<span className="w-3 h-3 rounded-full bg-[#febc2e]" aria-hidden="true" />
|
||||
<span className="w-3 h-3 rounded-full bg-[#28c840]" aria-hidden="true" />
|
||||
<span className="ml-2 text-xs text-[var(--muted)] font-mono">AgentCompany</span>
|
||||
</div>
|
||||
|
||||
<div ref={logRef} role="log" aria-label="Agent activity demonstration" className="p-5 lg:p-6 font-mono text-sm leading-relaxed h-[340px] overflow-y-auto">
|
||||
{demoMessages.slice(0, visibleCount).map((message, index) => (
|
||||
<motion.div
|
||||
key={`${message.agent}-${index}`}
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="flex gap-3 mb-3 last:mb-0"
|
||||
>
|
||||
<span className={`shrink-0 font-semibold inline-flex items-center gap-1.5 ${logTone[message.role]}`}>
|
||||
<span
|
||||
className={`w-1.5 h-1.5 rounded-full ${dotTone[message.role]}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{message.agent}
|
||||
</span>
|
||||
<span className="text-[var(--text)]">{message.text}</span>
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
{visibleCount < demoMessages.length && isVisible && (
|
||||
<span className="inline-block w-2 h-4 bg-[var(--accent)] animate-pulse rounded-sm" aria-hidden="true" />
|
||||
)}
|
||||
|
||||
{visibleCount >= demoMessages.length && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
className="mt-4 pt-4 border-t border-[var(--border)] flex items-center gap-2 text-[#059669]"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="m9 12 2 2 4-4" />
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
</svg>
|
||||
<span className="font-semibold">{landingHowItWorksNarrative.completionText}</span>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HowItWorks() {
|
||||
return (
|
||||
<section id="how-it-works" className="py-24 lg:py-40 px-4 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<div className="text-center mb-20 lg:mb-24">
|
||||
<p className="font-mono text-xs font-medium uppercase tracking-[0.25em] text-[var(--accent)] mb-5">
|
||||
How It Works
|
||||
</p>
|
||||
<h2 className="font-serif text-[clamp(2rem,1.5rem+2.5vw,3.25rem)] tracking-[-0.01em] text-[var(--text)]">
|
||||
{landingHowItWorksNarrative.heading}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-5 gap-12 lg:gap-16 items-start">
|
||||
<div className="lg:col-span-2 relative">
|
||||
<div className="absolute left-[15px] top-10 bottom-4 w-px bg-[var(--border)] hidden lg:block" aria-hidden="true" />
|
||||
|
||||
<div className="space-y-7">
|
||||
{steps.map((step, index) => (
|
||||
<motion.div
|
||||
key={step.number}
|
||||
initial={{ opacity: 0, x: -24 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true, amount: 0.5 }}
|
||||
transition={{ duration: 0.5, delay: 0.12 * index, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="flex gap-5"
|
||||
>
|
||||
<div className="relative z-10 shrink-0 flex items-center justify-center w-8 h-8 rounded-full bg-[var(--accent)] text-[var(--text-on-accent)] text-sm font-bold shadow-[0_2px_12px_-2px_var(--accent)]">
|
||||
{step.number}
|
||||
</div>
|
||||
<div className="pt-0.5">
|
||||
<h3 className="text-base font-bold text-[var(--text)] mb-1.5 tracking-tight">{step.title}</h3>
|
||||
<p className="text-sm leading-relaxed text-[var(--muted)]">{step.description}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
className="lg:col-span-3"
|
||||
initial={{ opacity: 0, y: 32 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
transition={{ duration: 0.7, ease: [0.16, 1, 0.3, 1] }}
|
||||
>
|
||||
<DemoPanel />
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
export function IconMark({ size = 32 }: { size?: number }) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 1024 1024"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="512" cy="512" r="480" fill="#191919" />
|
||||
<g transform="translate(268 255) scale(4)">
|
||||
<path
|
||||
fill="#FBFAF5"
|
||||
d="M 76.604813 111.661926 C 65.070183 111.662621 53.987053 111.662621 42.757744 111.662621 C 45.2239 101.998177 45.297546 101.949265 53.830105 102.036659 C 57.279823 102.071999 60.730202 102.04248 64.259201 102.04248 C 62.159458 93.316338 61.799103 93.009819 51.340881 91.341682 C 59.312901 73.847687 67.193756 56.553749 75.587578 38.134155 C 86.933441 62.965954 97.804642 86.758881 108.711754 110.630394 C 100.707413 111.871689 92.355965 107.246559 88.107536 98.852119 C 84.207748 91.146538 80.800591 83.191696 77.172813 75.348343 C 76.340195 73.54818 75.492958 71.754791 74.614624 69.877441 C 69.107384 75.691788 68.008286 81.475128 71.005882 88.088753 C 74.413452 95.606903 77.813919 103.128281 81.452705 111.167984 C 79.55114 111.381325 78.303734 111.521271 76.604813 111.661926 Z"
|
||||
/>
|
||||
<path
|
||||
fill="#FBFAF5"
|
||||
d="M 26.805992 87.743256 C 38.616821 61.830872 50.261688 36.260269 62.470131 9.452141 C 65.732315 17.818909 71.145126 23.524567 66.258224 33.086182 C 55.518253 54.099815 46.339973 75.911873 36.533691 97.402336 C 32.699284 105.805443 26.495394 110.823158 16.012421 111.410957 C 19.72117 103.270836 23.180601 95.67794 26.805992 87.743256 Z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { IconMark } from "./icon-mark";
|
||||
import { ThemeToggle } from "./theme-toggle";
|
||||
|
||||
const navLinks = [
|
||||
{ label: "Features", href: "#features" },
|
||||
{ label: "How It Works", href: "#how-it-works" },
|
||||
{ label: "FAQ", href: "#faq" },
|
||||
{ label: "Download", href: "#download" },
|
||||
];
|
||||
|
||||
function MenuIcon({ open }: { open: boolean }) {
|
||||
return (
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
{open ? (
|
||||
<>
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<line x1="3" y1="6" x2="21" y2="6" />
|
||||
<line x1="3" y1="12" x2="21" y2="12" />
|
||||
<line x1="3" y1="18" x2="21" y2="18" />
|
||||
</>
|
||||
)}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function Navbar() {
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => setScrolled(window.scrollY > 8);
|
||||
window.addEventListener("scroll", handleScroll, { passive: true });
|
||||
handleScroll();
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (window.innerWidth >= 768) {
|
||||
setMobileOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("resize", handleResize, { passive: true });
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<nav
|
||||
className={[
|
||||
"fixed top-0 left-0 right-0 z-50 transition-all duration-300 backdrop-blur-xl",
|
||||
scrolled ? "bg-[var(--panel)]/80 border-b border-[var(--border)] shadow-sm" : "bg-transparent border-b border-transparent",
|
||||
].join(" ")}
|
||||
>
|
||||
<div className="mx-auto max-w-6xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex h-16 items-center justify-between">
|
||||
<a href="#" className="flex items-center gap-2.5 shrink-0" aria-label="AgentCompany home">
|
||||
<IconMark />
|
||||
<span className="text-[var(--text)] font-serif text-xl font-normal tracking-[-0.4px]">AgentCompany</span>
|
||||
</a>
|
||||
|
||||
<div className="hidden md:flex items-center gap-8">
|
||||
{navLinks.map(({ label, href }) => (
|
||||
<a key={href} href={href} className="text-sm text-[var(--muted)] hover:text-[var(--text)] transition-colors duration-200">
|
||||
{label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="hidden md:flex items-center gap-3">
|
||||
<ThemeToggle />
|
||||
<a
|
||||
href="#download"
|
||||
className="inline-flex items-center px-5 py-2 rounded-full text-sm font-medium bg-[var(--cta)] text-[var(--text-on-cta)] hover:bg-[var(--cta-hover)] hover:scale-[1.03] active:scale-[0.98] transition-all duration-200"
|
||||
>
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="flex md:hidden items-center gap-2">
|
||||
<ThemeToggle />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMobileOpen((current) => !current)}
|
||||
aria-label={mobileOpen ? "Close navigation menu" : "Open navigation menu"}
|
||||
aria-expanded={mobileOpen}
|
||||
aria-controls="mobile-nav-menu"
|
||||
className="inline-flex items-center justify-center w-11 h-11 rounded-lg text-[var(--muted)] hover:text-[var(--text)] hover:bg-[var(--panel-soft)] transition-colors duration-200"
|
||||
>
|
||||
<MenuIcon open={mobileOpen} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="mobile-nav-menu"
|
||||
className={[
|
||||
"md:hidden overflow-hidden transition-all duration-300 ease-[cubic-bezier(0.16,1,0.3,1)]",
|
||||
mobileOpen ? "max-h-64 opacity-100" : "max-h-0 opacity-0",
|
||||
].join(" ")}
|
||||
>
|
||||
<div className="px-4 pb-4 pt-1 space-y-1 bg-[var(--panel)]/90 backdrop-blur-xl border-b border-[var(--border)]">
|
||||
{navLinks.map(({ label, href }) => (
|
||||
<a
|
||||
key={href}
|
||||
href={href}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
className="block px-3 py-2.5 rounded-lg text-sm text-[var(--muted)] hover:text-[var(--text)] hover:bg-[var(--panel-soft)] transition-colors duration-200"
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
))}
|
||||
<a
|
||||
href="#download"
|
||||
onClick={() => setMobileOpen(false)}
|
||||
className="block px-3 py-2.5 rounded-full text-sm font-medium text-center bg-[var(--cta)] text-[var(--text-on-cta)] hover:bg-[var(--cta-hover)] transition-colors duration-200 mt-2"
|
||||
>
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
export type PlatformDownloadAsset = {
|
||||
architectures: ReleaseArchitecture[];
|
||||
id: string;
|
||||
label: string;
|
||||
href: string;
|
||||
note: string;
|
||||
};
|
||||
|
||||
export type ReleaseArchitecture = "arm64" | "x64" | "x86";
|
||||
|
||||
export type PlatformDownload = {
|
||||
id: "macos" | "windows" | "linux";
|
||||
packageDescription: string;
|
||||
title: string;
|
||||
note: string;
|
||||
assets: PlatformDownloadAsset[];
|
||||
};
|
||||
|
||||
export const releaseVersion = "0.4.182";
|
||||
|
||||
export const platformDownloads: PlatformDownload[] = [
|
||||
{
|
||||
id: "macos",
|
||||
packageDescription: "signed and notarized DMG builds",
|
||||
title: "macOS",
|
||||
note: "Signed + notarized DMG builds",
|
||||
assets: [
|
||||
{
|
||||
architectures: ["arm64"],
|
||||
id: "macos-arm64-dmg",
|
||||
label: "Apple Silicon DMG",
|
||||
href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/AgentCompany-0.4.182-arm64.dmg",
|
||||
note: "arm64",
|
||||
},
|
||||
{
|
||||
architectures: ["x64"],
|
||||
id: "macos-x64-dmg",
|
||||
label: "Intel Mac DMG",
|
||||
href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/AgentCompany-0.4.182.dmg",
|
||||
note: "x64",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "windows",
|
||||
packageDescription: "an NSIS installer build",
|
||||
title: "Windows",
|
||||
note: "NSIS installer builds",
|
||||
assets: [
|
||||
{
|
||||
architectures: ["arm64", "x64"],
|
||||
id: "windows-universal-installer",
|
||||
label: "Windows Universal Installer",
|
||||
href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/AgentCompany.Setup.0.4.182.exe",
|
||||
note: "arm64 + x64",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "linux",
|
||||
packageDescription: "AppImage, DEB, and RPM builds",
|
||||
title: "Linux",
|
||||
note: "AppImage, DEB, and RPM builds",
|
||||
assets: [
|
||||
{
|
||||
architectures: ["x64"],
|
||||
id: "linux-x64-appimage",
|
||||
label: "Linux x64 AppImage",
|
||||
href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/AgentCompany-0.4.182.AppImage",
|
||||
note: "x64",
|
||||
},
|
||||
{
|
||||
architectures: ["x64"],
|
||||
id: "linux-x64-deb",
|
||||
label: "Linux x64 DEB",
|
||||
href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/agent-company_0.4.182_amd64.deb",
|
||||
note: "x64",
|
||||
},
|
||||
{
|
||||
architectures: ["x64"],
|
||||
id: "linux-x64-rpm",
|
||||
label: "Linux x64 RPM",
|
||||
href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/agent-company-0.4.182.x86_64.rpm",
|
||||
note: "x64",
|
||||
},
|
||||
{
|
||||
architectures: ["arm64"],
|
||||
id: "linux-arm64-appimage",
|
||||
label: "Linux ARM64 AppImage",
|
||||
href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/AgentCompany-0.4.182-arm64.AppImage",
|
||||
note: "arm64",
|
||||
},
|
||||
{
|
||||
architectures: ["arm64"],
|
||||
id: "linux-arm64-deb",
|
||||
label: "Linux ARM64 DEB",
|
||||
href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/agent-company_0.4.182_arm64.deb",
|
||||
note: "arm64",
|
||||
},
|
||||
{
|
||||
architectures: ["arm64"],
|
||||
id: "linux-arm64-rpm",
|
||||
label: "Linux ARM64 RPM",
|
||||
href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/agent-company-0.4.182.aarch64.rpm",
|
||||
note: "arm64",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -1,117 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
function AnthropicIcon() {
|
||||
return (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M13.827 3.52h3.603L24 20.48h-3.603l-6.57-16.96zm-7.257 0H10.173L16.74 20.48H13.14L11.07 14.98H5.355l-1.937 5.5H0L6.57 3.52zm4.132 8.69L8.18 5.86l-2.522 6.35h5.244z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function OpenAIIcon() {
|
||||
return (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M22.282 9.821a5.985 5.985 0 0 0-.516-4.91 6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a5.998 5.998 0 0 0-3.998 2.9 6.047 6.047 0 0 0 .743 7.097 5.98 5.98 0 0 0 .51 4.911 6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206 5.99 5.99 0 0 0 3.997-2.9 6.056 6.056 0 0 0-.747-7.073zM13.26 22.43a4.476 4.476 0 0 1-2.876-1.04l.141-.081 4.779-2.758a.795.795 0 0 0 .392-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494zM3.6 18.304a4.47 4.47 0 0 1-.535-3.014l.142.085 4.783 2.759a.771.771 0 0 0 .78 0l5.843-3.369v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.5 4.5 0 0 1-6.14-1.646zM2.34 7.896a4.485 4.485 0 0 1 2.366-1.973V11.6a.766.766 0 0 0 .388.676l5.815 3.355-2.02 1.168a.076.076 0 0 1-.071 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.872zm16.597 3.855l-5.833-3.387L15.119 7.2a.076.076 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667zm2.01-3.023l-.141-.085-4.774-2.782a.776.776 0 0 0-.785 0L9.409 9.23V6.897a.066.066 0 0 1 .028-.061l4.83-2.787a4.5 4.5 0 0 1 6.68 4.66zm-12.64 4.135l-2.02-1.164a.08.08 0 0 1-.038-.057V6.075a4.5 4.5 0 0 1 7.375-3.453l-.142.08L8.704 5.46a.795.795 0 0 0-.393.681zm1.097-2.365l2.602-1.5 2.607 1.5v2.999l-2.597 1.5-2.607-1.5z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function GoogleIcon() {
|
||||
return (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M12 11.366v3.473h5.51c-.226 1.408-1.644 4.126-5.51 4.126-3.314 0-6.017-2.744-6.017-6.127S8.686 6.71 12 6.71c1.888 0 3.152.804 3.874 1.497l2.637-2.54C16.63 3.952 14.534 3 12 3 6.477 3 2 7.477 2 13s4.477 10 10 10c5.773 0 9.6-4.057 9.6-9.773 0-.656-.07-1.157-.156-1.657H12z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function MoonshotIcon() {
|
||||
return (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm0 2a8 8 0 0 1 .5 15.98A10.5 10.5 0 0 0 17 12c0-3.14-1.37-5.96-3.54-7.9A8.03 8.03 0 0 1 12 4z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function ZhipuIcon() {
|
||||
return (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M4 4h16v3H8.5l11 10H20v3H4v-3h11.5L4.5 7H4V4z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function MiniMaxIcon() {
|
||||
return (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M2 20V4l5 8 5-8 5 8 5-8v16h-3V10.5l-2 4-5-8-5 8-2-4V20H2z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function OllamaIcon() {
|
||||
return (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M12 2C8.13 2 5 5.13 5 9c0 2.38 1.19 4.47 3 5.74V17a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2v-2.26c1.81-1.27 3-3.36 3-5.74 0-3.87-3.13-7-7-7zm-2 15v-1h4v1h-4zm3.5-4.15l-.5.35V15h-2v-1.8l-.5-.35A4.98 4.98 0 0 1 7 9c0-2.76 2.24-5 5-5s5 2.24 5 5a4.98 4.98 0 0 1-3.5 4.85zM10 9.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm7 0a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function ApiIcon() {
|
||||
return (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const runtimes = [
|
||||
{ name: "Claude Code", provider: "Anthropic", icon: AnthropicIcon },
|
||||
{ name: "Codex CLI", provider: "OpenAI", icon: OpenAIIcon },
|
||||
{ name: "Gemini CLI", provider: "Google", icon: GoogleIcon },
|
||||
{ name: "Kimi", provider: "Moonshot AI", icon: MoonshotIcon },
|
||||
{ name: "GLM", provider: "Zhipu AI", icon: ZhipuIcon },
|
||||
{ name: "MiniMax", provider: "MiniMax", icon: MiniMaxIcon },
|
||||
{ name: "Ollama", provider: "Local", icon: OllamaIcon },
|
||||
{ name: "Anthropic Compatible API", provider: "Any Provider", icon: ApiIcon },
|
||||
];
|
||||
|
||||
export function RuntimesBar() {
|
||||
return (
|
||||
<section className="border-y border-[var(--border)] py-12 lg:py-16">
|
||||
<div className="mx-auto max-w-6xl px-6 lg:px-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 16 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, amount: 0.5 }}
|
||||
transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="text-center"
|
||||
>
|
||||
<p className="font-mono text-xs uppercase tracking-[0.25em] text-[var(--muted)] mb-10">
|
||||
Supported AI Runtimes
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center justify-center gap-x-8 gap-y-6 lg:gap-x-12">
|
||||
{runtimes.map((runtime) => {
|
||||
const Icon = runtime.icon;
|
||||
return (
|
||||
<div key={runtime.name} className="flex items-center gap-2.5">
|
||||
<span className="text-[var(--muted)] opacity-60">
|
||||
<Icon />
|
||||
</span>
|
||||
<span className="text-base font-semibold tracking-tight text-[var(--text)]">
|
||||
{runtime.name}
|
||||
</span>
|
||||
<span className="text-sm text-[var(--muted)] hidden sm:inline">
|
||||
{runtime.provider}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export const WEBSITE_URL = "https://agent-company.dev";
|
||||
export const CONTACT_EMAIL = "dev@agentcompany.ai";
|
||||
@@ -1,76 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useCallback, useContext, useEffect, useState } from "react";
|
||||
|
||||
const storageKey = "agentcompany-theme";
|
||||
|
||||
type ThemeMode = "light" | "dark";
|
||||
|
||||
type ThemeContextValue = {
|
||||
theme: ThemeMode;
|
||||
setTheme: (theme: ThemeMode) => void;
|
||||
toggleTheme: () => void;
|
||||
};
|
||||
|
||||
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
|
||||
|
||||
function resolveTheme(): ThemeMode {
|
||||
if (typeof window === "undefined") {
|
||||
return "light";
|
||||
}
|
||||
|
||||
const stored = localStorage.getItem(storageKey);
|
||||
if (stored === "light" || stored === "dark") {
|
||||
return stored;
|
||||
}
|
||||
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||
}
|
||||
|
||||
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
const [theme, setThemeState] = useState<ThemeMode>(resolveTheme);
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
if (theme === "dark") {
|
||||
root.classList.add("dark");
|
||||
} else {
|
||||
root.classList.remove("dark");
|
||||
}
|
||||
localStorage.setItem(storageKey, theme);
|
||||
}, [theme]);
|
||||
|
||||
useEffect(() => {
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const handleChange = (event: MediaQueryListEvent) => {
|
||||
if (!localStorage.getItem(storageKey)) {
|
||||
setThemeState(event.matches ? "dark" : "light");
|
||||
}
|
||||
};
|
||||
|
||||
mediaQuery.addEventListener("change", handleChange);
|
||||
return () => mediaQuery.removeEventListener("change", handleChange);
|
||||
}, []);
|
||||
|
||||
const setTheme = useCallback((nextTheme: ThemeMode) => {
|
||||
setThemeState(nextTheme);
|
||||
}, []);
|
||||
|
||||
const toggleTheme = useCallback(() => {
|
||||
setThemeState((currentTheme) => (currentTheme === "light" ? "dark" : "light"));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const context = useContext(ThemeContext);
|
||||
if (!context) {
|
||||
throw new Error("useTheme must be used within a ThemeProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "./theme-provider";
|
||||
|
||||
function SunIcon() {
|
||||
return (
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="5" />
|
||||
<line x1="12" y1="1" x2="12" y2="3" />
|
||||
<line x1="12" y1="21" x2="12" y2="23" />
|
||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
|
||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
|
||||
<line x1="1" y1="12" x2="3" y2="12" />
|
||||
<line x1="21" y1="12" x2="23" y2="12" />
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function MoonIcon() {
|
||||
return (
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleTheme}
|
||||
aria-label={`Switch to ${theme === "light" ? "dark" : "light"} mode`}
|
||||
className="relative inline-flex items-center justify-center w-11 h-11 rounded-lg transition-colors duration-200 cursor-pointer hover:bg-[var(--panel-soft)] text-[var(--muted)] hover:text-[var(--text)]"
|
||||
>
|
||||
{theme === "light" ? <MoonIcon /> : <SunIcon />}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user