Rebuild footer, finalize structure, and refresh social metadata

- footer: dark #050e11 band with an Instrument Serif display CTA, an inverted
  Download button, link columns (tracked download hrefs preserved) and copyright
- structure: move icon-mark to brand/, theme provider+toggle to theme/,
  site-constants to lib/; sections/layout/ui/brand/theme now fully separated
- button: add an inverted variant for dark surfaces
- metadata: point OpenGraph/Twitter at a 1200x630 og.jpg and the JSON-LD
  screenshot at dashboard.webp; drop the old /screenshots references
- remove orphans: framer-motion (replaced by CSS + IntersectionObserver),
  ascii-background, and the seven superseded product screenshots
- add @playwright/test for the e2e suite
This commit is contained in:
ZacharyZhang-NY committed 2026-07-07 22:24:33 -04:00
1 parent 39de74320c
commit 72e3abacf9
21 files changed
+168 -301

No files matched your search

+7 -7
View File
@@ -1,7 +1,7 @@
import type { Metadata } from "next";
import { Instrument_Sans, Instrument_Serif, Newsreader, JetBrains_Mono } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "../components/theme-provider";
import { ThemeProvider } from "../components/theme/theme-provider";
const instrumentSans = Instrument_Sans({
subsets: ["latin"],
@@ -74,10 +74,10 @@ export const metadata: Metadata = {
locale: "en_US",
images: [
{
url: "/screenshots/Dashboard.jpeg",
width: 1920,
height: 1080,
alt: "AgentCompany dashboard showing autonomous AI agents managing a company",
url: "/images/og.jpg",
width: 1200,
height: 630,
alt: "AgentCompany desktop dashboard — quarter goal, budget, agent workload, and recent runs",
},
],
type: "website",
@@ -86,7 +86,7 @@ export const metadata: Metadata = {
card: "summary_large_image",
title: "AgentCompany - Launch an Autonomous AI Company from Your Desktop",
description,
images: ["/screenshots/Dashboard.jpeg"],
images: ["/images/og.jpg"],
},
robots: {
index: true,
@@ -110,7 +110,7 @@ const softwareJsonLd = {
operatingSystem: "macOS, Windows, Linux",
url: "https://agent-company.dev",
downloadUrl: "https://agent-company.dev/#download",
screenshot: "https://agent-company.dev/screenshots/Dashboard.jpeg",
screenshot: "https://agent-company.dev/images/dashboard.webp",
softwareVersion: landingStructuredDataNarrative.softwareVersion,
description,
featureList: landingStructuredDataNarrative.featureList,
+1 -1
View File
@@ -1,7 +1,7 @@
import { DownloadSection } from "../components/sections/download-section";
import { FAQ } from "../components/sections/faq";
import { Features } from "../components/sections/features";
import { Footer } from "../components/footer";
import { Footer } from "../components/layout/footer";
import { Hero } from "../components/sections/hero";
import { HowItWorks } from "../components/sections/how-it-works";
import { Navbar } from "../components/layout/navbar";
-158
View File
@@ -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>
);
}
File renamed without changes.
-91
View File
@@ -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>
);
}
+95
View File
@@ -0,0 +1,95 @@
import { Container } from "../ui/container";
import { Reveal } from "../ui/reveal";
import { Button } from "../ui/button";
import { IconMark } from "../brand/icon-mark";
import { getTrackedDownloadHref } from "../../lib/downloads";
import { CONTACT_EMAIL, WEBSITE_URL } from "../../lib/site-constants";
type FooterLink = { label: string; href: string; external?: boolean };
const footerGroups: { title: string; links: FooterLink[] }[] = [
{
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="bg-[var(--footer-bg)] text-[var(--footer-text)]">
<Container>
<div className="border-b border-white/10 py-24 text-center md:py-32">
<Reveal>
<h2 className="font-display text-[clamp(2.75rem,1.8rem+4vw,5.5rem)] leading-[1] tracking-[-0.02em]">
Launch your company now.
</h2>
<div className="mt-8 flex justify-center">
<Button href="#download" variant="inverted">
Download
</Button>
</div>
</Reveal>
</div>
<div className="grid gap-10 py-14 md:grid-cols-[1.6fr_1fr_1fr_1fr]">
<div>
<a href="#" className="inline-flex items-center gap-2.5" aria-label="AgentCompany home">
<IconMark size={28} />
<span className="text-lg font-medium tracking-[-0.02em]">AgentCompany</span>
</a>
<p className="mt-4 max-w-[32ch] text-sm leading-relaxed opacity-60">
Local-first desktop control plane for autonomous AI companies.
</p>
</div>
{footerGroups.map((group) => (
<div key={group.title}>
<h3 className="eyebrow text-xs opacity-50">{group.title}</h3>
<ul className="mt-4 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 opacity-75 transition-opacity hover:opacity-100"
>
{link.label}
</a>
</li>
))}
</ul>
</div>
))}
</div>
<div className="border-t border-white/10 py-6">
<p className="text-xs opacity-45">© {year} AgentCompany</p>
</div>
</Container>
</footer>
);
}
+2 -2
View File
@@ -5,8 +5,8 @@ import { Menu } from "reicon-react/icons/Menu";
import { Xmark } from "reicon-react/icons/Xmark";
import { Container } from "../ui/container";
import { Button } from "../ui/button";
import { IconMark } from "../icon-mark";
import { ThemeToggle } from "../theme-toggle";
import { IconMark } from "../brand/icon-mark";
import { ThemeToggle } from "../theme/theme-toggle";
const navLinks = [
{ label: "Features", href: "#features" },
+2 -1
View File
@@ -1,6 +1,6 @@
import type { ReactNode } from "react";
type ButtonVariant = "primary" | "ghost";
type ButtonVariant = "primary" | "ghost" | "inverted";
type ButtonSize = "md" | "sm";
const base =
@@ -14,6 +14,7 @@ const sizeClasses: Record<ButtonSize, string> = {
const variantClasses: Record<ButtonVariant, string> = {
primary: "bg-[var(--cta)] text-[var(--text-on-cta)] hover:bg-[var(--cta-hover)]",
ghost: "border border-[var(--border-strong)] text-[var(--text-secondary)] hover:border-[var(--muted)]",
inverted: "bg-[#fbfdf6] text-[#0a1d08] hover:bg-[#fbfdf6]/90",
};
/** Link-styled call to action. Every landing CTA is an anchor. */
File renamed without changes.
+60 -40
View File
@@ -8,7 +8,6 @@
"name": "agent-company-landing",
"version": "1.0.0",
"dependencies": {
"framer-motion": "12.38.0",
"next": "16.2.6",
"react": "19.2.4",
"react-dom": "19.2.4",
@@ -16,6 +15,7 @@
"reicon-react": "^1.1.2"
},
"devDependencies": {
"@playwright/test": "^1.58.2",
"@tailwindcss/postcss": "^4.2.2",
"@types/node": "^24.9.1",
"@types/react": "^19.2.2",
@@ -757,6 +757,22 @@
"node": ">= 10"
}
},
"node_modules/@playwright/test": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.58.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -1148,31 +1164,18 @@
"node": ">=10.13.0"
}
},
"node_modules/framer-motion": {
"version": "12.38.0",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz",
"integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==",
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"motion-dom": "^12.38.0",
"motion-utils": "^12.36.0",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/graceful-fs": {
@@ -1475,21 +1478,6 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/motion-dom": {
"version": "12.38.0",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz",
"integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.36.0"
}
},
"node_modules/motion-utils": {
"version": "12.36.0",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz",
"integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==",
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -1567,6 +1555,38 @@
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
},
"node_modules/playwright": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.58.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
"devOptional": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/postcss": {
"version": "8.5.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
+1 -1
View File
@@ -10,7 +10,6 @@
"preview": "wrangler pages dev out"
},
"dependencies": {
"framer-motion": "12.38.0",
"next": "16.2.6",
"react": "19.2.4",
"react-dom": "19.2.4",
@@ -18,6 +17,7 @@
"reicon-react": "^1.1.2"
},
"devDependencies": {
"@playwright/test": "^1.58.2",
"@tailwindcss/postcss": "^4.2.2",
"@types/node": "^24.9.1",
"@types/react": "^19.2.2",
Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 752 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 337 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 465 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 606 KiB