diff --git a/.gitignore b/.gitignore index 8cbb8ea..b8deb7e 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,5 @@ dist/ # local tooling artifacts .playwright-mcp/ .playwright-cli/ +.vercel +.env* diff --git a/AGENTS.md b/AGENTS.md index 2485a6c..35e087d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,6 +9,8 @@ Optional auth (off by default), no YAML. See `ROADMAP.md` (canonical) and - `apps/frontend` — React 18 + Vite + TS strict + Tailwind v4 + shadcn/ui - `apps/backend` — Go + Gin + zap, CGO-free, SQLite via `modernc.org/sqlite` - `packages/api-client` — generated TS client; `openapi.yaml` at repo root is the contract +- `landing/` — Next.js 15 marketing site (own package.json, not a workspace); + Vercel project `dash`, root dir `landing`, git-connected to this repo - `infra/` — Dockerfile (multi-stage, one image) - `tools/oxlint/anti-slop` — vendored lint plugin (do not extend casually) - `data/` — runtime SQLite + uploaded icons (gitignored) diff --git a/landing/.gitignore b/landing/.gitignore new file mode 100644 index 0000000..a9ffab7 --- /dev/null +++ b/landing/.gitignore @@ -0,0 +1,4 @@ +.vercel +.env* +node_modules/ +.next/ diff --git a/landing/app/fonts/geist-mono-var.woff2 b/landing/app/fonts/geist-mono-var.woff2 new file mode 100644 index 0000000..0750682 Binary files /dev/null and b/landing/app/fonts/geist-mono-var.woff2 differ diff --git a/landing/app/fonts/geist-var.woff2 b/landing/app/fonts/geist-var.woff2 new file mode 100644 index 0000000..991445d Binary files /dev/null and b/landing/app/fonts/geist-var.woff2 differ diff --git a/landing/app/globals.css b/landing/app/globals.css new file mode 100644 index 0000000..87de80a --- /dev/null +++ b/landing/app/globals.css @@ -0,0 +1,67 @@ +@import "tailwindcss"; + +/* Product tokens — same palette as the app (apps/frontend/src/index.css). */ +@theme { + --color-bg: #0a0a0a; + --color-surface: #111111; + --color-surface-hover: #161616; + --color-line: rgba(255, 255, 255, 0.08); + --color-line-strong: rgba(255, 255, 255, 0.14); + --color-text: #fafafa; + --color-dim: #a1a1aa; + --color-faint: #52525b; + --color-accent: #3b82f6; + --color-up: #22c55e; + --color-down: #f59e0b; + --color-destructive: #ef4444; + --color-iconbg: #1c1c1e; + + --ease-snap: cubic-bezier(0.2, 0, 0, 1); + + --font-sans: var(--font-geist), ui-sans-serif, system-ui, sans-serif; + --font-mono: var(--font-geist-mono), ui-monospace, monospace; + + --animate-pulse-dot: pulse-dot 2.4s ease-in-out infinite; + --animate-float: float 7s ease-in-out infinite; + --animate-scan: scan 5s linear infinite; + --animate-draw: draw 2.2s var(--ease-snap) forwards; + + @keyframes pulse-dot { + 0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.35); } + 50% { opacity: 0.75; box-shadow: 0 0 0 4px rgba(34, 197, 94, 0); } + } + @keyframes float { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-5px); } + } + @keyframes scan { + 0% { transform: translateX(-100%); } + 100% { transform: translateX(400%); } + } + @keyframes draw { + to { stroke-dashoffset: 0; } + } +} + +html { + -webkit-font-smoothing: antialiased; + scroll-behavior: smooth; +} + +body { + background: var(--color-bg); + color: var(--color-text); + min-height: 100dvh; +} + +::selection { + background: rgba(59, 130, 246, 0.3); +} + +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } + *, *::before, *::after { + animation: none !important; + transition-duration: 0.01ms !important; + } +} diff --git a/landing/app/icon.svg b/landing/app/icon.svg new file mode 100644 index 0000000..804cb76 --- /dev/null +++ b/landing/app/icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/landing/app/layout.tsx b/landing/app/layout.tsx new file mode 100644 index 0000000..38ecb66 --- /dev/null +++ b/landing/app/layout.tsx @@ -0,0 +1,45 @@ +import type { Metadata, Viewport } from "next"; +import localFont from "next/font/local"; +import "./globals.css"; + +const geist = localFont({ + src: "./fonts/geist-var.woff2", + variable: "--font-geist", + weight: "100 900", + display: "swap", +}); + +const geistMono = localFont({ + src: "./fonts/geist-mono-var.woff2", + variable: "--font-geist-mono", + weight: "100 900", + display: "swap", +}); + +export const metadata: Metadata = { + metadataBase: new URL("https://dash-homelab.vercel.app"), + title: "Dash - Self-hosted homelab dashboard", + description: + "Service boards, uptime monitors, domain watch, and system metrics in one container. A single static Go binary with an embedded React UI. No YAML, no external database.", + openGraph: { + title: "Dash - Self-hosted homelab dashboard", + description: + "Service boards, uptime monitors, domain watch, and system metrics in one container. One static Go binary, no YAML.", + type: "website", + images: ["/dash.svg"], + }, +}; + +export const viewport: Viewport = { + themeColor: "#0a0a0a", +}; + +export default function RootLayout({ + children, +}: Readonly<{ children: React.ReactNode }>) { + return ( + + {children} + + ); +} diff --git a/landing/app/page.tsx b/landing/app/page.tsx new file mode 100644 index 0000000..bd4daaf --- /dev/null +++ b/landing/app/page.tsx @@ -0,0 +1,270 @@ +import { Nav } from "../components/Nav"; +import { Board } from "../components/Board"; +import { RendererDemo } from "../components/RendererDemo"; +import { MonitorMock, DomainMock, SystemMock, StatusBand } from "../components/Mocks"; +import { Install } from "../components/Install"; +import { CopyButton } from "../components/CopyButton"; +import { Reveal } from "../components/Reveal"; + +function SectionHead({ label, tag }: { label: string; tag: string }) { + return ( +
+

+ {label} +

+ + {tag} +
+ ); +} + +function Feature({ + flip = false, + title, + children, + mock, +}: { + flip?: boolean; + title: string; + children: React.ReactNode; + mock: React.ReactNode; +}) { + return ( + +
+
+

{title}

+

{children}

+
+
{mock}
+
+
+ ); +} + +export default function Page() { + return ( + <> +