Rip out Theia, bump submodules, retire platform/ scaffold, snapshot docs + design assets

Theia rip-out (parent):
- Remove theia submodule entry (the local fork, Gitea repo, Coolify app,
  Cloud Run services, and Artifact Registry image are all gone)
- Drop README.md + INFRASTRUCTURE.md (obsolete "Project OS" snapshots
  that also leaked API tokens) and setup.sh (Theia clone bootstrap)
- Delete UI-DESIGN-GUIDE.md, BACKEND_AGENTS_PLAN.md, VIBN_BUILD_PLAN.md,
  VISUAL_EDITOR_PLAN.md, core-packages.md, ai-packages.md, tools-list.md
  (all 100% Theia-specific or superseded)
- Surgical scrubs of remaining Theia mentions in
  AGENT_EXECUTION_ARCHITECTURE.md and TURBOREPO_MIGRATION_PLAN.md

Submodule bumps:
- vibn-agent-runner: Theia rip-out + MCP refactor (api/wrapper/server
  pattern across shell/file/git/memory/prd/search/agent/gitea/coolify)
- vibn-frontend: Theia rip-out + P5.1 attach E2E + Justine UI WIP

Retire platform/ scaffold:
- Remove platform/backend/ (control-plane, executors, mcp-adapter),
  platform/client-ide/ (gcp-productos extension), platform/contracts/,
  platform/infra/terraform/, platform/scripts/templates/turborepo/
  (replaced by vibn-agent-runner + vibn-frontend + Coolify direct)
- Drop architecture.md, technical_spec.md, vision-ext.md,
  "1.Generate Control Plane API scaffold.md" (same era)

Docs / planning snapshots (new):
- AI_CAPABILITIES.md, AI_CAPABILITIES_ROADMAP.md
- AGENT_TELEMETRY_STREAMING_PROJECT.md
- VIBN_PRD.md, product-idea-a.md

Design assets (new):
- branding/{coolify,gitea,ux-testing}/ static brand collateral
- justine/ HTML mockups for the new onboarding/build flows
- preview-assist-ui/ Vite scratch app
- master-ai.code-workspace

Infra helpers (new):
- setup-coolify-montreal.sh provisioner
- gitea-docker-compose.yml
- vibn-coolify-schema.sql for the Coolify Postgres extensions
- prd-agent-prompt.pdf, prompt, root.txt, remixed-9edec9e9.tsx scratch
- flatten.sh helper

.gitignore: ignore **/node_modules, **/.next, **/.turbo, **/coverage

Made-with: Cursor
This commit is contained in:
2026-04-22 18:06:37 -07:00
parent 54da4c96da
commit 99deb546c8
153 changed files with 22844 additions and 9496 deletions

View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>VIBN Assist Preview</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

1677
preview-assist-ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,19 @@
{
"name": "preview-assist-ui",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.2.0",
"vite": "^5.0.0"
}
}

View File

@@ -0,0 +1,932 @@
import { useState, useRef, useEffect } from "react";
import Website from "./Website.jsx";
import Dashboard from "./Dashboard.jsx";
// ─── DESIGN TOKENS ────────────────────────────────────────────────────────────
const C = {
ink: "#0f172a", ink2: "#1e293b", ink3: "#475569", muted: "#94a3b8",
border: "#f1f5f9", border2: "#e2e8f0", surface: "#f8fafc", white: "#fff",
green: "#22c55e", greenBg: "#f0fdf4",
purple: "#6366f1", purpleBg: "#eef2ff",
amber: "#f59e0b", amberBg: "#fffbeb", amberBorder: "#fde68a", amberText: "#92400e",
blue: "#0ea5e9", blueBg: "#f0f9ff",
rose: "#f43f5e", roseBg: "#fff1f2",
teal: "#14b8a6", tealBg: "#f0fdfa",
violet: "#8b5cf6", violetBg: "#f5f3ff",
};
const PHASES = [
{ id: "welcome", label: "Welcome", icon: "◎", desc: "How this works" },
{ id: "discover", label: "Discover", icon: "◇", desc: "Your idea" },
{ id: "architect", label: "Architect", icon: "⬡", desc: "What gets built" },
{ id: "design", label: "Design", icon: "◈", desc: "How it looks" },
{ id: "market", label: "Market", icon: "✦", desc: "How you'll grow" },
{ id: "build", label: "Build MVP", icon: "▲", desc: "Review & launch" },
];
// ─── DISCOVER DATA ────────────────────────────────────────────────────────────
const QUESTIONS = [
{ key: "idea", q: "Tell me about your idea. What does it do and who is it for?" },
{ key: "problem", q: "What's the painful thing your users are doing today instead?" },
{ key: "users", q: "Describe your ideal first customer in one sentence." },
{ key: "value", q: "What's the one thing they'll love most about your product?" },
{ key: "revenue", q: "How will you charge for it? Subscription, one-time, or free to start?" },
{ key: "features", q: "Name the 3 things users must be able to do in version one." },
];
// ─── ARCHITECT DATA ───────────────────────────────────────────────────────────
const ARCH_BLOCKS = [
{ id: "frontend", icon: "◇", label: "Frontend", color: C.purple, bg: C.purpleBg, chosen: "Web app", what: "The screens your users see — dashboard, sign up, settings, and everything in between. Works on desktop and mobile.", alts: [{ id: "webapp", label: "Web app", desc: "Runs in any browser, desktop & mobile" }, { id: "mobile", label: "Mobile-first", desc: "Optimised for phones, still works on desktop" }] },
{ id: "backend", icon: "⬡", label: "Backend & Database", color: C.blue, bg: C.blueBg, chosen: "API + database", what: "The engine behind the scenes. Stores all your data securely and runs your product's logic.", alts: [{ id: "api_pg", label: "API + database", desc: "Standard setup, works for almost everything" }, { id: "realtime", label: "Real-time", desc: "If your product needs live updates between users" }] },
{ id: "auth", icon: "◎", label: "Sign up & Login", color: C.rose, bg: C.roseBg, chosen: "Email + social login", what: "How your users create accounts and log in. Getting this right reduces drop-off at the first step.", alts: [{ id: "email_social", label: "Email + social login", desc: "Email/password plus Google & GitHub — recommended" }, { id: "email_only", label: "Email & password only", desc: "Simpler, no third-party login" }, { id: "magic_link", label: "Magic link", desc: "No password — users get a login link by email" }, { id: "sso", label: "SSO for teams", desc: "If you're selling to companies, not individuals" }] },
{ id: "payments", icon: "◈", label: "Payments", color: C.amber, bg: C.amberBg, chosen: "Subscription billing", what: "How you collect money from customers. Set up now so you're ready to charge from day one.", alts: [{ id: "subscription", label: "Subscription billing", desc: "Monthly or annual plans — recommended for SaaS" }, { id: "one_time", label: "One-time purchase", desc: "Pay once, own forever" }, { id: "usage", label: "Usage-based", desc: "Charge based on what users consume" }, { id: "free", label: "Free for now", desc: "No payments yet — add billing later" }] },
{ id: "email", icon: "✦", label: "Email", color: C.teal, bg: C.tealBg, chosen: "Transactional + marketing", what: "Emails sent to your users — welcome messages, password resets, and newsletters when you're ready.", alts: [{ id: "both", label: "Transactional + marketing", desc: "Welcome emails, resets, plus campaign capability" }, { id: "trans", label: "Transactional only", desc: "Just the essential emails, nothing more" }, { id: "none", label: "None for now", desc: "Skip email entirely — add it later" }] },
{ id: "hosting", icon: "▲", label: "Hosting", color: C.violet, bg: C.violetBg, chosen: "Your own servers", what: "Where your product lives. Deployed to your own infrastructure — you own everything, no lock-in.", alts: [{ id: "own", label: "Your own servers", desc: "Coolify + Gitea — already configured" }], locked: true },
];
const PAGES_GENERATED = [
{ group: "Public", color: C.purple, pages: ["Landing page", "Pricing", "About", "Blog"] },
{ group: "Auth", color: C.rose, pages: ["Sign up", "Log in", "Forgot password"] },
{ group: "App", color: C.blue, pages: ["Dashboard", "Onboarding", "Settings", "Invite team"] },
{ group: "Payments", color: C.amber, pages: ["Checkout", "Success", "Manage subscription"] },
];
// ─── DESIGN DATA ──────────────────────────────────────────────────────────────
const DESIGN_FEELS = [
{ id: "clean", label: "Clean & focused", ref: "Like Notion or Linear", bg: "#fff", surface: "#f8fafc", text: "#0f172a", accent: "#6366f1", radius: 8 },
{ id: "bold", label: "Bold & confident", ref: "Like Stripe or Vercel", bg: "#0f172a", surface: "#1e293b", text: "#f8fafc", accent: "#f43f5e", radius: 8 },
{ id: "warm", label: "Warm & friendly", ref: "Like Mailchimp or Basecamp", bg: "#fffbeb", surface: "#fef3c7", text: "#78350f", accent: "#f59e0b", radius: 14 },
{ id: "fresh", label: "Fresh & modern", ref: "Like Loom or Superhuman", bg: "#f0fdf4", surface: "#dcfce7", text: "#14532d", accent: "#22c55e", radius: 10 },
{ id: "electric", label: "Electric & vivid", ref: "Like Figma or Framer", bg: "#faf5ff", surface: "#ede9fe", text: "#4c1d95", accent: "#8b5cf6", radius: 8 },
{ id: "luxury", label: "Premium & refined", ref: "Like Linear or Craft", bg: "#0c0a09", surface: "#1c1917", text: "#f5f5f4", accent: "#d4a853", radius: 6 },
];
// ─── MARKET DATA ─────────────────────────────────────────────────────────────
const VOICE_OPTIONS = [
{ key: "tone", label: "Tone", a: "Friendly & approachable", b: "Professional & authoritative" },
{ key: "style", label: "Style", a: "Conversational & casual", b: "Precise & concise" },
{ key: "personality", label: "Personality", a: "Warm & encouraging", b: "Direct & confident" },
];
const SUGGESTED_TOPICS = () => [
{ id: "t1", title: "The problem we're solving", angle: "Show users you deeply understand their pain before pitching your solution.", channels: ["Blog", "Tweet thread", "LinkedIn", "Email"] },
{ id: "t2", title: "Who this is built for", angle: "Paint a picture of your ideal user — they should read it and think 'that's me'.", channels: ["Blog", "LinkedIn", "Website section"] },
{ id: "t3", title: "Why now is the right time", angle: "What's changed in the market that makes this product possible or necessary?", channels: ["Blog", "Tweet thread", "Email"] },
];
const WEBSITE_FEELS = [
{ id: "editorial", label: "Editorial", ref: "Bold headlines, strong opinions", bg: "#fff", accent: "#ef4444" },
{ id: "startup", label: "Startup energy", ref: "Clear, conversion-focused", bg: "#f8fafc", accent: "#0ea5e9" },
{ id: "minimal", label: "Ultra minimal", ref: "Let the product speak", bg: "#fff", accent: "#111" },
{ id: "warm_w", label: "Warm & human", ref: "Feels personal and trustworthy", bg: "#fff7ed", accent: "#ea580c" },
];
// ─── SHARED UI ────────────────────────────────────────────────────────────────
function PhaseHeader({ title, desc, action }) {
return (
<div style={{ padding: "22px 32px 18px", background: C.white, borderBottom: `1px solid ${C.border}`, display: "flex", alignItems: "center", justifyContent: "space-between", flexShrink: 0 }}>
<div>
<div style={{ fontSize: 18, fontWeight: 800, color: C.ink, letterSpacing: "-0.03em" }}>{title}</div>
{desc && <div style={{ fontSize: 13, color: C.muted, marginTop: 2 }}>{desc}</div>}
</div>
{action}
</div>
);
}
function ContinueBtn({ label, onClick, disabled }) {
return (
<button onClick={onClick} disabled={disabled}
style={{ background: disabled ? C.border2 : C.ink, color: disabled ? C.muted : C.white, border: "none", borderRadius: 10, padding: "11px 22px", fontSize: 13.5, fontWeight: 700, cursor: disabled ? "default" : "pointer", letterSpacing: "-0.01em", transition: "all 0.2s" }}>
{label}
</button>
);
}
// ─── LIVE APP MOCKUP ──────────────────────────────────────────────────────────
function AppMockup({ feel }) {
const t = DESIGN_FEELS.find((f) => f.id === feel) || DESIGN_FEELS[0];
const isDark = t.bg === "#0f172a" || t.bg === "#0c0a09";
const border = isDark ? "rgba(255,255,255,0.08)" : "rgba(0,0,0,0.06)";
return (
<div style={{ width: "100%", height: "100%", background: t.bg, borderRadius: 10, overflow: "hidden", display: "flex", flexDirection: "column", transition: "all 0.4s", fontFamily: "DM Sans, sans-serif" }}>
<div style={{ padding: "10px 16px", display: "flex", alignItems: "center", justifyContent: "space-between", borderBottom: `1px solid ${border}`, flexShrink: 0 }}>
<div style={{ fontSize: 13, fontWeight: 800, color: t.text, letterSpacing: "-0.03em" }}>YourApp</div>
<div style={{ display: "flex", gap: 12 }}>{["Dashboard", "Settings", "Billing"].map((l) => <span key={l} style={{ fontSize: 10, color: t.text, opacity: 0.45 }}>{l}</span>)}</div>
<div style={{ background: t.accent, color: "#fff", fontSize: 10, fontWeight: 700, padding: "4px 10px", borderRadius: t.radius / 1.5 }}>Upgrade</div>
</div>
<div style={{ flex: 1, padding: "14px", overflow: "hidden", display: "flex", flexDirection: "column", gap: 10 }}>
<div style={{ fontSize: 12, fontWeight: 700, color: t.text, opacity: 0.8 }}>Dashboard</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 7 }}>
{[["Users", "182"], ["Revenue", "$2.4k"], ["Active", "94%"]].map(([l, v], i) => (
<div key={l} style={{ background: t.surface, borderRadius: t.radius / 1.5, padding: "9px 11px", border: `1px solid ${border}` }}>
<div style={{ fontSize: 9, color: t.text, opacity: 0.45, marginBottom: 4 }}>{l}</div>
<div style={{ fontSize: 15, fontWeight: 800, color: i === 1 ? t.accent : t.text }}>{v}</div>
</div>
))}
</div>
<div style={{ background: t.surface, borderRadius: t.radius / 1.5, padding: "10px 12px", border: `1px solid ${border}`, flex: 1 }}>
<div style={{ fontSize: 9, color: t.text, opacity: 0.4, marginBottom: 8 }}>Recent activity</div>
{["Alex signed up · 2m ago", "Priya upgraded to Pro · 1h ago", "Dan invited 3 teammates · 3h ago"].map((item, i) => (
<div key={i} style={{ display: "flex", alignItems: "center", gap: 7, padding: "5px 0", borderBottom: i < 2 ? `1px solid ${border}` : "none" }}>
<div style={{ width: 5, height: 5, borderRadius: "50%", background: t.accent, flexShrink: 0 }} />
<span style={{ fontSize: 10, color: t.text, opacity: 0.65 }}>{item}</span>
</div>
))}
</div>
</div>
</div>
);
}
// ─── PHASE: WELCOME ───────────────────────────────────────────────────────────
function WelcomePhase({ onStart }) {
const steps = [
{ icon: "◇", label: "You describe your idea", desc: "A short conversation — no technical knowledge needed." },
{ icon: "⬡", label: "We plan what gets built", desc: "AI maps out your full product architecture in plain language." },
{ icon: "◈", label: "You choose how it looks", desc: "Pick a visual style for your product and marketing." },
{ icon: "✦", label: "You set your market angle", desc: "Define your voice and the topics that'll drive your content." },
{ icon: "▲", label: "We build it", desc: "AI codes everything and deploys to your live URL." },
];
return (
<div style={{ height: "100%", display: "flex", alignItems: "center", justifyContent: "center", padding: "40px" }}>
<div style={{ maxWidth: 540, width: "100%" }}>
<div style={{ marginBottom: 36 }}>
<div style={{ display: "inline-flex", alignItems: "center", gap: 8, background: C.purpleBg, border: "1px solid #c7d2fe", borderRadius: 20, padding: "5px 14px", marginBottom: 20 }}>
<div style={{ width: 7, height: 7, borderRadius: "50%", background: C.green, animation: "vpulse 2s infinite" }} />
<span style={{ fontSize: 12, fontWeight: 600, color: C.purple }}>vibn · MVP Builder</span>
</div>
<div style={{ fontSize: 32, fontWeight: 800, color: C.ink, letterSpacing: "-0.04em", lineHeight: 1.15, marginBottom: 14 }}>
From idea to live product.<br />No code needed.
</div>
<div style={{ fontSize: 15, color: C.ink3, lineHeight: 1.7, maxWidth: 440 }}>
Answer a few questions about your idea. AI plans, designs, and builds your full SaaS product then deploys it to your own servers. Takes about 10 minutes to set up.
</div>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 10, marginBottom: 32 }}>
{steps.map((s, i) => (
<div key={i} style={{ display: "flex", alignItems: "flex-start", gap: 14, padding: "14px 16px", background: C.white, borderRadius: 12, border: `1px solid ${C.border2}` }}>
<div style={{ width: 32, height: 32, borderRadius: 9, background: C.surface, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 14, color: C.ink3, flexShrink: 0, border: `1px solid ${C.border2}` }}>{s.icon}</div>
<div>
<div style={{ fontSize: 13.5, fontWeight: 700, color: C.ink, marginBottom: 2 }}>{s.label}</div>
<div style={{ fontSize: 12.5, color: C.muted, lineHeight: 1.5 }}>{s.desc}</div>
</div>
</div>
))}
</div>
<div style={{ display: "flex", alignItems: "center", gap: 14 }}>
<button onClick={onStart} style={{ background: C.ink, color: C.white, border: "none", borderRadius: 12, padding: "14px 32px", fontSize: 15, fontWeight: 800, cursor: "pointer", letterSpacing: "-0.02em" }}>
Let's build it →
</button>
<span style={{ fontSize: 12.5, color: C.muted }}>~10 minutes · You own everything at the end</span>
</div>
</div>
</div>
);
}
// ─── PHASE: DISCOVER ─────────────────────────────────────────────────────────
function DiscoverPhase({ onComplete }) {
const [step, setStep] = useState(0);
const [answers, setAnswers] = useState({});
const [input, setInput] = useState("");
const [msgs, setMsgs] = useState([{ role: "assistant", text: "Let's start with the big picture.\n\n" + QUESTIONS[0].q }]);
const [done, setDone] = useState(false);
const bottomRef = useRef(null);
useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: "smooth" }); }, [msgs]);
const acks = { idea: "Good I can work with that.", problem: "That's a real pain worth solving.", users: "Clear. That focus will shape everything.", value: "Strong differentiator.", revenue: "Makes sense for this type of product." };
const send = () => {
if (!input.trim()) return;
const val = input.trim(); setInput("");
const q = QUESTIONS[step];
const newA = { ...answers, [q.key]: val };
setAnswers(newA);
setMsgs((m) => [...m, { role: "user", text: val }]);
setTimeout(() => {
const next = step + 1;
if (next < QUESTIONS.length) {
setStep(next);
setMsgs((m) => [...m, { role: "assistant", text: `${acks[q.key] || "Got it."}\n\n${QUESTIONS[next].q}` }]);
} else {
setMsgs((m) => [...m, { role: "assistant", text: "That's all I need.\n\nI've drafted your product plan on the right. Have a read — if it looks right, let's move on to planning what gets built." }]);
setDone(true);
}
}, 400);
};
return (
<div style={{ display: "flex", height: "100%", overflow: "hidden" }}>
<div style={{ flex: 1, display: "flex", flexDirection: "column", borderRight: `1px solid ${C.border}` }}>
<PhaseHeader title="Discover" desc="Tell me about your idea I'll turn it into a product plan" />
<div style={{ padding: "0 32px 12px", background: C.white, borderBottom: `1px solid ${C.border}` }}>
<div style={{ display: "flex", gap: 4 }}>
{QUESTIONS.map((_, i) => <div key={i} style={{ flex: 1, height: 3, borderRadius: 3, background: i < step ? C.ink : i === step ? C.purple : C.border2, transition: "background 0.3s" }} />)}
</div>
</div>
<div style={{ flex: 1, overflowY: "auto", padding: "24px 32px", display: "flex", flexDirection: "column", gap: 14 }}>
{msgs.map((msg, i) => (
<div key={i} style={{ display: "flex", flexDirection: "column", alignItems: msg.role === "user" ? "flex-end" : "flex-start" }}>
<div style={{ maxWidth: "80%", background: msg.role === "user" ? C.ink : C.surface, color: msg.role === "user" ? C.white : C.ink2, borderRadius: msg.role === "user" ? "14px 14px 4px 14px" : "14px 14px 14px 4px", padding: "12px 16px", fontSize: 13.5, lineHeight: 1.65, border: msg.role === "assistant" ? `1px solid ${C.border2}` : "none", whiteSpace: "pre-line" }}>{msg.text}</div>
</div>
))}
<div ref={bottomRef} />
</div>
<div style={{ padding: "14px 32px 24px", borderTop: `1px solid ${C.border}`, background: C.white }}>
{!done
? <div style={{ display: "flex", gap: 8, background: C.surface, border: `1px solid ${C.border2}`, borderRadius: 12, padding: "10px 14px" }}>
<input value={input} onChange={(e) => setInput(e.target.value)} onKeyDown={(e) => e.key === "Enter" && send()} placeholder="Type your answer…" style={{ flex: 1, border: "none", background: "transparent", fontSize: 14, color: C.ink, outline: "none" }} autoFocus />
<button onClick={send} style={{ background: C.ink, color: C.white, border: "none", borderRadius: 8, padding: "8px 16px", fontSize: 13, fontWeight: 700, cursor: "pointer" }}>→</button>
</div>
: <button onClick={() => onComplete(answers)} style={{ width: "100%", background: C.ink, color: C.white, border: "none", borderRadius: 10, padding: "13px", fontSize: 14, fontWeight: 700, cursor: "pointer" }}>Plan looks good — next: Architect →</button>
}
</div>
</div>
<div style={{ width: 280, overflowY: "auto", background: C.surface, padding: "22px 18px", display: "flex", flexDirection: "column", gap: 8 }}>
<div style={{ fontSize: 10.5, fontWeight: 700, letterSpacing: "0.07em", textTransform: "uppercase", color: C.muted, marginBottom: 4 }}>Your product plan</div>
{QUESTIONS.map((q, i) => (
<div key={i} style={{ padding: "11px 13px", background: C.white, borderRadius: 10, border: `1px solid ${i < step ? C.border2 : C.border}`, opacity: i > step ? 0.35 : 1, transition: "all 0.3s" }}>
<div style={{ fontSize: 10, fontWeight: 700, color: i <= step ? C.purple : C.muted, letterSpacing: "0.06em", textTransform: "uppercase", marginBottom: 3 }}>{q.key}</div>
<div style={{ fontSize: 12.5, color: i < step ? C.ink2 : C.muted, lineHeight: 1.5 }}>{answers[q.key] || "Waiting…"}</div>
</div>
))}
</div>
</div>
);
}
// ─── PHASE: ARCHITECT ─────────────────────────────────────────────────────────
function ArchCard({ block, label, onEdit }) {
return (
<div style={{ background: C.white, borderRadius: 14, border: `1px solid ${C.border2}`, overflow: "hidden", display: "flex", flexDirection: "column" }}
onMouseEnter={(e) => (e.currentTarget.style.boxShadow = "0 4px 20px rgba(0,0,0,0.07)")}
onMouseLeave={(e) => (e.currentTarget.style.boxShadow = "none")}>
<div style={{ height: 3, background: block.color, opacity: 0.6 }} />
<div style={{ padding: "16px 18px", flex: 1 }}>
<div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 10 }}>
<div style={{ width: 34, height: 34, borderRadius: 9, background: block.bg, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 16, color: block.color, flexShrink: 0 }}>{block.icon}</div>
<div style={{ fontSize: 13.5, fontWeight: 700, color: C.ink }}>{block.label}</div>
</div>
<div style={{ fontSize: 12.5, color: C.ink3, lineHeight: 1.65, marginBottom: 14 }}>{block.what}</div>
<div style={{ display: "inline-flex", alignItems: "center", gap: 6, background: block.bg, borderRadius: 20, padding: "5px 11px" }}>
<div style={{ width: 6, height: 6, borderRadius: "50%", background: block.color }} />
<span style={{ fontSize: 12, fontWeight: 600, color: block.color }}>{label}</span>
</div>
</div>
<div style={{ padding: "10px 18px", borderTop: `1px solid ${C.border}`, display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<span style={{ fontSize: 11.5, color: C.muted }}>{block.locked ? "Required for your setup" : "AI recommended this"}</span>
<button onClick={onEdit} style={{ fontSize: 12, fontWeight: 600, color: block.locked ? C.muted : C.ink3, background: "transparent", border: `1px solid ${C.border2}`, borderRadius: 7, padding: "5px 11px", cursor: "pointer" }}>{block.locked ? "Why? →" : "Change →"}</button>
</div>
</div>
);
}
function ArchitectPhase({ onComplete }) {
const [choices, setChoices] = useState(() => Object.fromEntries(ARCH_BLOCKS.map((b) => [b.id, b.alts[0].id])));
const [editing, setEditing] = useState(null);
const editBlock = ARCH_BLOCKS.find((b) => b.id === editing);
const save = (id, val) => { setChoices((c) => ({ ...c, [id]: val })); setEditing(null); };
const choiceLabel = (block) => block.alts.find((a) => a.id === choices[block.id])?.label || block.alts[0].label;
return (
<div style={{ display: "flex", height: "100%", overflow: "hidden" }}>
<div style={{ flex: 1, display: "flex", flexDirection: "column" }}>
<PhaseHeader title="Architect" desc="Here's what we're going to build. AI has made smart choices — change anything that doesn't feel right."
action={<ContinueBtn label="Confirm next: Design " onClick={() => onComplete(choices)} />} />
<div style={{ flex: 1, overflowY: "auto", padding: "24px 32px" }}>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 14, marginBottom: 14 }}>
{ARCH_BLOCKS.slice(0, 3).map((b) => <ArchCard key={b.id} block={b} label={choiceLabel(b)} onEdit={() => setEditing(b.id)} />)}
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 14 }}>
{ARCH_BLOCKS.slice(3).map((b) => <ArchCard key={b.id} block={b} label={choiceLabel(b)} onEdit={() => setEditing(b.id)} />)}
</div>
</div>
</div>
<div style={{ width: 250, borderLeft: `1px solid ${C.border}`, background: C.white, overflowY: "auto", padding: "22px 18px", flexShrink: 0 }}>
<div style={{ fontSize: 10.5, fontWeight: 700, letterSpacing: "0.07em", textTransform: "uppercase", color: C.muted, marginBottom: 16 }}>Pages to build</div>
{PAGES_GENERATED.map((g) => (
<div key={g.group}>
<div style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 7 }}>
<div style={{ width: 6, height: 6, borderRadius: "50%", background: g.color }} />
<div style={{ fontSize: 10, fontWeight: 700, letterSpacing: "0.08em", textTransform: "uppercase", color: C.muted }}>{g.group}</div>
</div>
{g.pages.map((p) => (
<div key={p} style={{ display: "flex", alignItems: "center", gap: 8, padding: "7px 10px", background: C.surface, borderRadius: 7, border: `1px solid ${C.border}`, marginBottom: 4, fontSize: 12.5, color: C.ink3 }}>
<div style={{ width: 5, height: 5, borderRadius: "50%", background: g.color, opacity: 0.5, flexShrink: 0 }} />{p}
</div>
))}
</div>
))}
<div style={{ marginTop: 18, padding: "13px", background: C.violetBg, border: "1px solid #ddd6fe", borderRadius: 11 }}>
<div style={{ fontSize: 12, fontWeight: 700, color: C.violet, marginBottom: 4 }}>◉ Your infrastructure</div>
<div style={{ fontSize: 12, color: "#5b21b6", lineHeight: 1.6 }}>Code pushed to Gitea. Coolify auto-deploys. You own everything.</div>
</div>
</div>
{editing && editBlock && (
<div style={{ position: "fixed", inset: 0, background: "rgba(15,23,42,0.5)", zIndex: 600, display: "flex", alignItems: "center", justifyContent: "center", backdropFilter: "blur(4px)", animation: "fadeIn 0.15s" }}>
<div style={{ background: C.white, borderRadius: 18, width: 420, boxShadow: "0 24px 60px rgba(0,0,0,0.2)", overflow: "hidden", animation: "slideup 0.2s" }}>
<div style={{ padding: "20px 24px", borderBottom: `1px solid ${C.border}`, display: "flex", alignItems: "center", gap: 12 }}>
<div style={{ width: 36, height: 36, borderRadius: 10, background: editBlock.bg, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 16, color: editBlock.color, flexShrink: 0 }}>{editBlock.icon}</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 15, fontWeight: 700, color: C.ink }}>{editBlock.label}</div>
<div style={{ fontSize: 12, color: C.muted, lineHeight: 1.5 }}>{editBlock.what}</div>
</div>
<button onClick={() => setEditing(null)} style={{ background: "transparent", border: "none", color: C.muted, cursor: "pointer", fontSize: 20 }}>×</button>
</div>
<div style={{ padding: "16px 24px", display: "flex", flexDirection: "column", gap: 8 }}>
{editBlock.alts.map((alt) => {
const isSel = choices[editBlock.id] === alt.id;
return (
<button key={alt.id} onClick={() => !editBlock.locked && setChoices((c) => ({ ...c, [editBlock.id]: alt.id }))}
style={{ display: "flex", alignItems: "center", gap: 12, padding: "12px 16px", borderRadius: 11, border: isSel ? `2px solid ${editBlock.color}` : `1px solid ${C.border2}`, background: isSel ? editBlock.bg : C.white, cursor: editBlock.locked ? "default" : "pointer", textAlign: "left", transition: "all 0.15s" }}>
<div style={{ width: 18, height: 18, borderRadius: "50%", border: `2px solid ${isSel ? editBlock.color : C.border2}`, background: isSel ? editBlock.color : "transparent", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
{isSel && <div style={{ width: 6, height: 6, borderRadius: "50%", background: C.white }} />}
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 13.5, fontWeight: isSel ? 700 : 500, color: C.ink }}>{alt.label}</div>
<div style={{ fontSize: 12, color: C.muted }}>{alt.desc}</div>
</div>
</button>
);
})}
</div>
<div style={{ padding: "10px 24px 20px", display: "flex", gap: 8 }}>
<button onClick={() => setEditing(null)} style={{ flex: 1, background: C.surface, color: C.ink3, border: `1px solid ${C.border2}`, borderRadius: 10, padding: "11px", fontSize: 13, cursor: "pointer" }}>Cancel</button>
<button onClick={() => save(editing, choices[editing])} style={{ flex: 2, background: C.ink, color: C.white, border: "none", borderRadius: 10, padding: "11px", fontSize: 13, fontWeight: 700, cursor: "pointer" }}>Save</button>
</div>
</div>
</div>
)}
</div>
);
}
// ─── PHASE: DESIGN ────────────────────────────────────────────────────────────
function DesignPhase({ onComplete }) {
const [feel, setFeel] = useState("clean");
return (
<div style={{ display: "flex", height: "100%", overflow: "hidden" }}>
<div style={{ width: 280, borderRight: `1px solid ${C.border}`, display: "flex", flexDirection: "column", overflow: "hidden", background: C.white }}>
<PhaseHeader title="Design" desc="How should your product feel?" />
<div style={{ flex: 1, overflowY: "auto", padding: "20px 20px", display: "flex", flexDirection: "column", gap: 8 }}>
<div style={{ fontSize: 11.5, color: C.muted, lineHeight: 1.6, marginBottom: 8, padding: "0 2px" }}>
Pick the feeling you want users to get. You can refine every detail after launch — this sets the foundation.
</div>
{DESIGN_FEELS.map((f) => {
const isDark = f.bg === "#0f172a" || f.bg === "#0c0a09";
const isSel = feel === f.id;
return (
<button key={f.id} onClick={() => setFeel(f.id)}
style={{ borderRadius: 12, border: isSel ? `2px solid ${C.ink}` : `1px solid ${C.border2}`, overflow: "hidden", cursor: "pointer", textAlign: "left", transition: "all 0.15s", background: f.bg, padding: 0, boxShadow: isSel ? "0 2px 16px rgba(0,0,0,0.1)" : "none" }}>
<div style={{ padding: "12px 14px" }}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 6 }}>
<div style={{ fontSize: 13.5, fontWeight: 700, color: f.text }}>{f.label}</div>
{isSel && <span style={{ color: C.green, fontSize: 13, fontWeight: 900 }}>✓</span>}
</div>
<div style={{ fontSize: 11.5, color: f.text, opacity: 0.55 }}>{f.ref}</div>
<div style={{ display: "flex", gap: 5, marginTop: 10 }}>
{[f.bg, f.accent, f.surface].map((col, i) => <div key={i} style={{ width: 14, height: 14, borderRadius: "50%", background: col, border: `1.5px solid ${isDark ? "rgba(255,255,255,0.15)" : "rgba(0,0,0,0.1)"}` }} />)}
</div>
</div>
{isSel && <div style={{ height: 3, background: f.accent }} />}
</button>
);
})}
</div>
<div style={{ padding: "14px 20px 20px", borderTop: `1px solid ${C.border}` }}>
<button onClick={() => onComplete({ feel })} style={{ width: "100%", background: C.ink, color: C.white, border: "none", borderRadius: 10, padding: "12px", fontSize: 13.5, fontWeight: 700, cursor: "pointer" }}>
Looks good — next: Market →
</button>
</div>
</div>
<div style={{ flex: 1, background: "#dde3ea", display: "flex", flexDirection: "column" }}>
<div style={{ padding: "11px 18px", background: C.white, borderBottom: `1px solid ${C.border}`, display: "flex", alignItems: "center", gap: 8 }}>
<div style={{ display: "flex", gap: 5 }}>{["#f43f5e", "#f59e0b", "#22c55e"].map((c) => <div key={c} style={{ width: 10, height: 10, borderRadius: "50%", background: c }} />)}</div>
<span style={{ fontFamily: "monospace", fontSize: 11.5, color: C.muted }}>app.yourproduct.com</span>
<span style={{ marginLeft: "auto", fontSize: 11.5, fontWeight: 600, color: C.ink3 }}>{DESIGN_FEELS.find((f) => f.id === feel)?.label}</span>
</div>
<div style={{ flex: 1, padding: 28, overflow: "hidden" }}>
<AppMockup feel={feel} />
</div>
</div>
</div>
);
}
// ─── PHASE: MARKET ────────────────────────────────────────────────────────────
function WebsitePreview({ feel }) {
const t = WEBSITE_FEELS.find((f) => f.id === feel) || WEBSITE_FEELS[0];
const isDark = t.bg === "#0c0a09" || t.bg === "#0f172a";
const border = isDark ? "rgba(255,255,255,0.07)" : "rgba(0,0,0,0.06)";
const textCol = isDark ? "#fff" : (t.text || C.ink);
return (
<div style={{ width: "100%", height: "100%", background: t.bg, borderRadius: 10, overflow: "hidden", fontFamily: "DM Sans, sans-serif", display: "flex", flexDirection: "column", transition: "all 0.4s" }}>
<div style={{ padding: "11px 18px", display: "flex", alignItems: "center", justifyContent: "space-between", borderBottom: `1px solid ${border}` }}>
<div style={{ fontSize: 13, fontWeight: 800, color: textCol }}>YourApp</div>
<div style={{ display: "flex", gap: 14 }}>{["Product", "Blog", "Pricing"].map((l) => <span key={l} style={{ fontSize: 10, color: isDark ? "rgba(255,255,255,0.4)" : C.muted }}>{l}</span>)}</div>
<div style={{ background: t.accent, color: isDark && t.accent === "#d4a853" ? "#0c0a09" : "#fff", fontSize: 10, fontWeight: 700, padding: "5px 11px", borderRadius: 6 }}>Get started</div>
</div>
<div style={{ flex: 1, padding: "22px 20px", display: "flex", flexDirection: "column", alignItems: "center", gap: 14 }}>
<div style={{ textAlign: "center", maxWidth: 320 }}>
<div style={{ fontSize: 22, fontWeight: 800, color: textCol, letterSpacing: "-0.04em", lineHeight: 1.2, marginBottom: 8 }}>Launch your SaaS in days, not months</div>
<div style={{ fontSize: 12, color: isDark ? "rgba(255,255,255,0.45)" : C.muted, lineHeight: 1.65, marginBottom: 16 }}>Describe your idea. AI builds, deploys, and markets your product automatically.</div>
<div style={{ display: "flex", gap: 8, justifyContent: "center" }}>
<div style={{ background: t.accent, color: "#fff", fontSize: 12, fontWeight: 700, padding: "9px 18px", borderRadius: 7 }}>Start free</div>
<div style={{ background: "transparent", color: isDark ? "rgba(255,255,255,0.6)" : C.ink3, fontSize: 12, padding: "9px 14px", borderRadius: 7, border: `1px solid ${border}` }}>See demo</div>
</div>
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 8, width: "100%" }}>
{[["", "Build", "AI writes every line"], ["", "Grow", "Content on autopilot"], ["", "Launch", "Live in minutes"]].map(([ic, lb, desc]) => (
<div key={lb} style={{ background: isDark ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.03)", borderRadius: 8, padding: "12px 10px", border: `1px solid ${border}`, textAlign: "center" }}>
<div style={{ fontSize: 16, marginBottom: 5 }}>{ic}</div>
<div style={{ fontSize: 11, fontWeight: 700, color: textCol, marginBottom: 3 }}>{lb}</div>
<div style={{ fontSize: 10, color: isDark ? "rgba(255,255,255,0.35)" : C.muted }}>{desc}</div>
</div>
))}
</div>
</div>
</div>
);
}
function MarketPhase({ prd, onComplete }) {
const [tab, setTab] = useState("voice");
const [voice, setVoice] = useState({ tone: 0.3, style: 0.4, personality: 0.3 });
const [topics, setTopics] = useState(SUGGESTED_TOPICS());
const [editingTopic, setEditingTopic] = useState(null);
const [websiteFeel, setWebsiteFeel] = useState("startup");
const tabs = ["voice", "topics", "style"];
const tabLabels = { voice: "Voice", topics: "Topics", style: "Website style" };
const voiceDesc = (key, val) => { const opt = VOICE_OPTIONS.find((v) => v.key === key); return val < 0.4 ? opt.a : val > 0.6 ? opt.b : "Balanced"; };
const addTopic = () => { const id = `t${Date.now()}`; setTopics((t) => [...t, { id, title: "", angle: "", channels: ["Blog", "Email"], editing: true }]); setEditingTopic(id); };
const updateTopic = (id, field, val) => setTopics((t) => t.map((tp) => (tp.id === id ? { ...tp, [field]: val } : tp)));
const removeTopic = (id) => setTopics((t) => t.filter((tp) => tp.id !== id));
return (
<div style={{ display: "flex", height: "100%", overflow: "hidden", flexDirection: "column" }}>
<PhaseHeader title="Market" desc="Set your brand voice, campaign topics, and website style AI uses these to generate all your content."
action={<ContinueBtn label="Done review & build " onClick={() => onComplete({ voice, topics, websiteFeel })} />} />
<div style={{ display: "flex", borderBottom: `1px solid ${C.border}`, background: C.white, flexShrink: 0 }}>
{tabs.map((t) => (
<button key={t} onClick={() => setTab(t)} style={{ padding: "12px 24px", border: "none", background: "transparent", borderBottom: tab === t ? `2px solid ${C.ink}` : "2px solid transparent", cursor: "pointer", fontSize: 13.5, fontWeight: tab === t ? 700 : 400, color: tab === t ? C.ink : C.muted, transition: "all 0.15s", display: "flex", alignItems: "center", gap: 7 }}>
{tabLabels[t]}
{t === "topics" && <span style={{ fontSize: 11, background: C.surface, border: `1px solid ${C.border2}`, borderRadius: 10, padding: "1px 7px", color: C.ink3 }}>{topics.length}</span>}
</button>
))}
</div>
<div style={{ flex: 1, overflow: "hidden", display: "flex" }}>
{tab === "voice" && (
<div style={{ flex: 1, overflowY: "auto", padding: "32px", display: "flex", justifyContent: "center" }}>
<div style={{ width: "100%", maxWidth: 520 }}>
<div style={{ fontSize: 14, color: C.ink3, lineHeight: 1.7, marginBottom: 28 }}>This defines how your brand sounds — in emails, on your website, in social posts. AI uses this to write content that sounds like you.</div>
<div style={{ display: "flex", flexDirection: "column", gap: 24 }}>
{VOICE_OPTIONS.map((opt) => (
<div key={opt.key} style={{ background: C.white, borderRadius: 14, border: `1px solid ${C.border2}`, padding: "20px 24px" }}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 16 }}>
<div style={{ fontSize: 13, fontWeight: 700, color: C.ink }}>{opt.label}</div>
<div style={{ fontSize: 12.5, fontWeight: 600, color: C.purple, background: C.purpleBg, borderRadius: 20, padding: "3px 11px" }}>{voiceDesc(opt.key, voice[opt.key])}</div>
</div>
<div style={{ display: "flex", alignItems: "center", gap: 12, marginBottom: 10 }}>
<span style={{ fontSize: 12, color: C.ink3, width: 140, textAlign: "right", lineHeight: 1.4 }}>{opt.a}</span>
<input type="range" min="0" max="1" step="0.05" value={voice[opt.key]} onChange={(e) => setVoice((v) => ({ ...v, [opt.key]: parseFloat(e.target.value) }))} style={{ flex: 1, accentColor: C.ink, cursor: "pointer" }} />
<span style={{ fontSize: 12, color: C.ink3, width: 140, lineHeight: 1.4 }}>{opt.b}</span>
</div>
</div>
))}
</div>
<div style={{ marginTop: 24, padding: "16px 20px", background: C.surface, borderRadius: 12, border: `1px solid ${C.border2}` }}>
<div style={{ fontSize: 11, fontWeight: 700, color: C.muted, textTransform: "uppercase", letterSpacing: "0.07em", marginBottom: 6 }}>How your brand will sound</div>
<div style={{ fontSize: 13.5, color: C.ink2, lineHeight: 1.7 }}>
{voice.tone < 0.4 ? "Warm and approachable" : voice.tone > 0.6 ? "Professional and authoritative" : "Balanced"}
{" · "}{voice.style < 0.4 ? "conversational" : voice.style > 0.6 ? "precise" : "clear"}
{" · "}{voice.personality < 0.4 ? "encouraging" : voice.personality > 0.6 ? "direct" : "steady"}
</div>
</div>
</div>
</div>
)}
{tab === "topics" && (
<div style={{ flex: 1, overflowY: "auto", padding: "28px 32px" }}>
<div style={{ maxWidth: 640 }}>
<div style={{ fontSize: 14, color: C.ink3, lineHeight: 1.7, marginBottom: 20 }}>Topics are the campaign angles your content will be built around. Each topic generates a blog post, tweet thread, LinkedIn post, and email. AI suggested these from your PRD — edit or add your own.</div>
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
{topics.map((topic) => (
<div key={topic.id} style={{ background: C.white, borderRadius: 14, border: `1px solid ${C.border2}`, overflow: "hidden" }}>
{editingTopic === topic.id ? (
<div style={{ padding: "18px 20px", display: "flex", flexDirection: "column", gap: 10 }}>
<input value={topic.title} onChange={(e) => updateTopic(topic.id, "title", e.target.value)} placeholder="Topic title" style={{ fontSize: 14, fontWeight: 600, color: C.ink, border: `1px solid ${C.border2}`, borderRadius: 8, padding: "9px 12px", outline: "none", background: C.surface }} autoFocus />
<textarea value={topic.angle} onChange={(e) => updateTopic(topic.id, "angle", e.target.value)} placeholder="What angle does this take?" style={{ fontSize: 13, color: C.ink2, border: `1px solid ${C.border2}`, borderRadius: 8, padding: "9px 12px", outline: "none", resize: "none", height: 72, background: C.surface, lineHeight: 1.6 }} />
<div style={{ display: "flex", gap: 8 }}>
<button onClick={() => setEditingTopic(null)} style={{ flex: 1, background: C.surface, color: C.ink3, border: `1px solid ${C.border2}`, borderRadius: 8, padding: "9px", fontSize: 13, cursor: "pointer" }}>Done</button>
<button onClick={() => { removeTopic(topic.id); setEditingTopic(null); }} style={{ background: "transparent", color: C.rose, border: "1px solid #fecdd3", borderRadius: 8, padding: "9px 14px", fontSize: 12, cursor: "pointer" }}>Remove</button>
</div>
</div>
) : (
<div style={{ padding: "16px 20px" }}>
<div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: 12, marginBottom: 8 }}>
<div style={{ fontSize: 14, fontWeight: 700, color: C.ink, lineHeight: 1.4 }}>{topic.title || "Untitled topic"}</div>
<button onClick={() => setEditingTopic(topic.id)} style={{ fontSize: 12, color: C.ink3, background: "transparent", border: `1px solid ${C.border2}`, borderRadius: 7, padding: "4px 10px", cursor: "pointer", flexShrink: 0 }}>Edit</button>
</div>
<div style={{ fontSize: 13, color: C.ink3, lineHeight: 1.6, marginBottom: 12 }}>{topic.angle}</div>
<div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
<span style={{ fontSize: 10.5, fontWeight: 600, color: C.muted }}>Generates:</span>
{topic.channels.map((ch) => <span key={ch} style={{ fontSize: 11, fontWeight: 600, color: C.ink3, background: C.surface, border: `1px solid ${C.border2}`, borderRadius: 5, padding: "2px 8px" }}>{ch}</span>)}
</div>
</div>
)}
</div>
))}
{topics.length < 5 && (
<button onClick={addTopic} style={{ padding: "14px", background: "transparent", border: `1.5px dashed ${C.border2}`, borderRadius: 14, fontSize: 13.5, color: C.muted, cursor: "pointer", fontWeight: 500 }}>+ Add a topic</button>
)}
</div>
<div style={{ marginTop: 16, padding: "13px 16px", background: C.amberBg, border: `1px solid ${C.amberBorder}`, borderRadius: 11 }}>
<div style={{ fontSize: 12.5, color: C.amberText, lineHeight: 1.6 }}>💡 Start with 3 topics — you can always add more after launch. AI will generate all the content once your product is live.</div>
</div>
</div>
</div>
)}
{tab === "style" && (
<div style={{ flex: 1, overflow: "hidden", display: "flex" }}>
<div style={{ width: 260, borderRight: `1px solid ${C.border}`, overflowY: "auto", background: C.white, padding: "20px 18px", display: "flex", flexDirection: "column", gap: 8 }}>
<div style={{ fontSize: 12, color: C.muted, lineHeight: 1.6, marginBottom: 6 }}>How should your marketing website feel to visitors?</div>
{WEBSITE_FEELS.map((f) => {
const isSel = websiteFeel === f.id;
const isDark = f.bg === "#0c0a09" || f.bg === "#0f172a";
return (
<button key={f.id} onClick={() => setWebsiteFeel(f.id)}
style={{ borderRadius: 12, border: isSel ? `2px solid ${C.ink}` : `1px solid ${C.border2}`, background: f.bg, cursor: "pointer", textAlign: "left", padding: "12px 14px", transition: "all 0.15s", boxShadow: isSel ? "0 2px 12px rgba(0,0,0,0.08)" : "none" }}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 4 }}>
<div style={{ fontSize: 13.5, fontWeight: 700, color: isDark ? "#fff" : C.ink }}>{f.label}</div>
{isSel && <span style={{ color: C.green, fontSize: 12, fontWeight: 900 }}>✓</span>}
</div>
<div style={{ fontSize: 11.5, color: isDark ? "rgba(255,255,255,0.5)" : C.muted }}>{f.ref}</div>
<div style={{ display: "flex", gap: 5, marginTop: 9 }}>
{[f.bg, f.accent, "#f1f5f9"].map((col, i) => <div key={i} style={{ width: 12, height: 12, borderRadius: "50%", background: col, border: `1.5px solid ${isDark ? "rgba(255,255,255,0.15)" : "rgba(0,0,0,0.1)"}` }} />)}
</div>
</button>
);
})}
</div>
<div style={{ flex: 1, background: "#dde3ea", display: "flex", flexDirection: "column" }}>
<div style={{ padding: "11px 18px", background: C.white, borderBottom: `1px solid ${C.border}`, display: "flex", alignItems: "center", gap: 8 }}>
<div style={{ display: "flex", gap: 5 }}>{["#f43f5e", "#f59e0b", "#22c55e"].map((c) => <div key={c} style={{ width: 10, height: 10, borderRadius: "50%", background: c }} />)}</div>
<span style={{ fontFamily: "monospace", fontSize: 11.5, color: C.muted }}>yourproduct.com</span>
<span style={{ marginLeft: "auto", fontSize: 11.5, fontWeight: 600, color: C.ink3 }}>{WEBSITE_FEELS.find((f) => f.id === websiteFeel)?.label}</span>
</div>
<div style={{ flex: 1, padding: 24, overflow: "hidden" }}>
<WebsitePreview feel={websiteFeel} />
</div>
</div>
</div>
)}
</div>
</div>
);
}
// ─── PHASE: BUILD ─────────────────────────────────────────────────────────────
function BuildPhase({ prd, arch, design, market }) {
const [building, setBuilding] = useState(false);
const [step, setStep] = useState(0);
const [done, setDone] = useState(false);
const BUILD_STEPS = [
{ label: "Creating Gitea repository", detail: "Setting up version control for your project" },
{ label: "Scaffolding the app", detail: "Next.js project structure, TypeScript, Tailwind" },
{ label: "Setting up your database", detail: "PostgreSQL + schema based on your product plan" },
{ label: "Building sign up & login", detail: arch?.auth === "email_social" ? "Email + Google + GitHub" : "Email & password" },
{ label: "Wiring payments", detail: arch?.payments === "free" ? "Skipped no payments yet" : "Stripe checkout, webhooks, billing portal" },
{ label: "Generating all app pages", detail: "Dashboard, settings, onboarding, invite flow" },
{ label: "Applying your design", detail: `${DESIGN_FEELS.find((f) => f.id === design?.feel)?.label || "Clean & focused"} theme` },
{ label: "Building your marketing website", detail: `${WEBSITE_FEELS.find((f) => f.id === market?.websiteFeel)?.label || "Startup energy"} style` },
{ label: "Setting up email", detail: "Welcome, password reset, and marketing templates" },
{ label: "Pushing to Gitea", detail: "Full codebase committed and pushed" },
{ label: "Deploying via Coolify", detail: "Building Docker image, deploying to your servers" },
{ label: "Running health checks", detail: "Verifying all pages, auth, and payments are live" },
];
const start = () => {
setBuilding(true);
let s = 0;
const iv = setInterval(() => {
s++;
setStep(s);
if (s >= BUILD_STEPS.length) { clearInterval(iv); setTimeout(() => setDone(true), 600); }
}, 800);
};
const feel = DESIGN_FEELS.find((f) => f.id === design?.feel) || DESIGN_FEELS[0];
if (building) {
return (
<div style={{ height: "100%", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", padding: 40, background: C.surface }}>
<div style={{ width: "100%", maxWidth: 520 }}>
<div style={{ textAlign: "center", marginBottom: 28 }}>
{done ? (
<>
<div style={{ fontSize: 36, marginBottom: 12 }}>🚀</div>
<div style={{ fontSize: 26, fontWeight: 800, color: C.ink, letterSpacing: "-0.04em", marginBottom: 8 }}>Your MVP is live</div>
<div style={{ fontSize: 14, color: C.muted }}>Deployed to Coolify · Pushed to Gitea · Ready to share</div>
</>
) : (
<>
<div style={{ fontSize: 22, fontWeight: 800, color: C.ink, letterSpacing: "-0.04em", marginBottom: 6 }}>Building your product…</div>
<div style={{ fontSize: 13.5, color: C.muted }}>Step {Math.min(step, BUILD_STEPS.length)} of {BUILD_STEPS.length}</div>
</>
)}
</div>
<div style={{ background: C.white, borderRadius: 14, border: `1px solid ${C.border2}`, overflow: "hidden", marginBottom: 20 }}>
{BUILD_STEPS.map((s, i) => {
const isActive = i === step - 1 && !done;
const isDoneStep = i < step;
return (
<div key={i} style={{ display: "flex", alignItems: "center", gap: 12, padding: "11px 16px", borderBottom: i < BUILD_STEPS.length - 1 ? `1px solid ${C.border}` : "none", background: isActive ? C.purpleBg : "transparent", transition: "background 0.3s" }}>
<div style={{ width: 22, height: 22, borderRadius: "50%", background: isDoneStep ? C.green : isActive ? C.purple : C.border2, display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
{isDoneStep ? <span style={{ color: C.white, fontSize: 9, fontWeight: 900 }}>✓</span> : isActive ? <span style={{ color: C.white, fontSize: 9, display: "inline-block", animation: "vspin 1s linear infinite" }}>◎</span> : null}
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 13, fontWeight: isActive ? 600 : 400, color: isDoneStep ? C.ink3 : isActive ? C.purple : C.muted }}>{s.label}</div>
{(isActive || isDoneStep) && <div style={{ fontSize: 11.5, color: C.muted, marginTop: 1 }}>{s.detail}</div>}
</div>
</div>
);
})}
</div>
{done && (
<>
<div style={{ background: C.white, borderRadius: 14, border: `1px solid ${C.border2}`, padding: "18px 20px", marginBottom: 16 }}>
<div style={{ fontSize: 12, fontWeight: 700, color: C.muted, textTransform: "uppercase", letterSpacing: "0.07em", marginBottom: 12 }}>Your next 3 actions</div>
{[{ n: "1", label: "Open your live app", desc: "Share the URL with 5 real people today." }, { n: "2", label: "Sign up as a user", desc: "Go through your own onboarding." }, { n: "3", label: "Post your first topic", desc: "AI has drafted your first content batch." }].map((a) => (
<div key={a.n} style={{ display: "flex", gap: 12, padding: "10px 0", borderBottom: a.n !== "3" ? `1px solid ${C.border}` : "none" }}>
<div style={{ width: 24, height: 24, borderRadius: "50%", background: C.ink, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 11, fontWeight: 700, color: C.white, flexShrink: 0 }}>{a.n}</div>
<div>
<div style={{ fontSize: 13.5, fontWeight: 600, color: C.ink, marginBottom: 2 }}>{a.label}</div>
<div style={{ fontSize: 12.5, color: C.muted, lineHeight: 1.5 }}>{a.desc}</div>
</div>
</div>
))}
</div>
<div style={{ display: "flex", gap: 10 }}>
<button style={{ flex: 2, background: C.ink, color: C.white, border: "none", borderRadius: 11, padding: "14px", fontSize: 14, fontWeight: 700, cursor: "pointer" }}>Open my app ↗</button>
<button style={{ flex: 1, background: C.white, color: C.ink3, border: `1px solid ${C.border2}`, borderRadius: 11, padding: "14px", fontSize: 13, cursor: "pointer" }}>View in Gitea ↗</button>
</div>
</>
)}
</div>
</div>
);
}
const summaryItems = [
{ label: "Sign up & login", value: ARCH_BLOCKS.find((b) => b.id === "auth")?.alts.find((a) => a.id === arch?.auth)?.label || "Email + social login", color: C.rose, icon: "" },
{ label: "Payments", value: ARCH_BLOCKS.find((b) => b.id === "payments")?.alts.find((a) => a.id === arch?.payments)?.label || "Subscription billing", color: C.amber, icon: "" },
{ label: "Email", value: ARCH_BLOCKS.find((b) => b.id === "email")?.alts.find((a) => a.id === arch?.email)?.label || "Transactional + marketing", color: C.teal, icon: "" },
{ label: "Product style", value: feel.label, color: C.purple, icon: "" },
{ label: "Website style", value: WEBSITE_FEELS.find((f) => f.id === market?.websiteFeel)?.label || "Startup energy", color: C.blue, icon: "" },
{ label: "Campaign topics", value: `${market?.topics?.length || 3} topics ready`, color: C.violet, icon: "" },
];
return (
<div style={{ height: "100%", overflowY: "auto", padding: "32px", display: "flex", justifyContent: "center", background: C.surface }}>
<div style={{ width: "100%", maxWidth: 620 }}>
<div style={{ marginBottom: 24 }}>
<div style={{ fontSize: 24, fontWeight: 800, color: C.ink, letterSpacing: "-0.04em", marginBottom: 6 }}>Ready to build</div>
<div style={{ fontSize: 14, color: C.muted, lineHeight: 1.6 }}>Review everything below. Once you hit Build, AI will code your full product and deploy it to your live URL.</div>
</div>
<div style={{ background: C.white, borderRadius: 14, border: `1px solid ${C.border2}`, overflow: "hidden", marginBottom: 16 }}>
<div style={{ padding: "14px 20px", borderBottom: `1px solid ${C.border}` }}>
<span style={{ fontSize: 11, fontWeight: 700, color: C.muted, textTransform: "uppercase", letterSpacing: "0.07em" }}>What's being built</span>
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 0 }}>
{summaryItems.map((item, i) => (
<div key={i} style={{ padding: "14px 20px", borderRight: i % 2 === 0 ? `1px solid ${C.border}` : "none", borderBottom: i < summaryItems.length - 2 ? `1px solid ${C.border}` : "none", display: "flex", alignItems: "center", gap: 10 }}>
<div style={{ width: 28, height: 28, borderRadius: 8, background: `${item.color}15`, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 12, color: item.color, flexShrink: 0 }}>{item.icon}</div>
<div>
<div style={{ fontSize: 11, color: C.muted, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.05em", marginBottom: 2 }}>{item.label}</div>
<div style={{ fontSize: 13.5, fontWeight: 600, color: C.ink2 }}>{item.value}</div>
</div>
</div>
))}
</div>
</div>
<div style={{ background: C.white, borderRadius: 14, border: `1px solid ${C.border2}`, overflow: "hidden", marginBottom: 16 }}>
<div style={{ padding: "14px 20px", borderBottom: `1px solid ${C.border}`, display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<span style={{ fontSize: 11, fontWeight: 700, color: C.muted, textTransform: "uppercase", letterSpacing: "0.07em" }}>Pages</span>
<span style={{ fontSize: 12, color: C.muted }}>{PAGES_GENERATED.reduce((a, g) => a + g.pages.length, 0)} pages total</span>
</div>
<div style={{ padding: "16px 20px", display: "flex", gap: 20, flexWrap: "wrap" }}>
{PAGES_GENERATED.map((g) => (
<div key={g.group}>
<div style={{ fontSize: 10, fontWeight: 700, color: g.color, textTransform: "uppercase", letterSpacing: "0.08em", marginBottom: 6, opacity: 0.8 }}>{g.group}</div>
{g.pages.map((p) => <div key={p} style={{ fontSize: 13, color: C.ink3, marginBottom: 4, display: "flex", gap: 6, alignItems: "center" }}><span style={{ fontSize: 8, color: g.color }}>●</span>{p}</div>)}
</div>
))}
</div>
</div>
<div style={{ background: C.violetBg, border: "1px solid #ddd6fe", borderRadius: 12, padding: "14px 18px", marginBottom: 24, display: "flex", gap: 12 }}>
<span style={{ fontSize: 16, color: C.violet }}>◉</span>
<div>
<div style={{ fontSize: 13, fontWeight: 700, color: C.violet, marginBottom: 3 }}>Deploying to your infrastructure</div>
<div style={{ fontSize: 12.5, color: "#5b21b6", lineHeight: 1.6 }}>Code is pushed to your Gitea repo. Coolify auto-deploys on push. Your domain, SSL, and database are all pre-configured.</div>
</div>
</div>
<button onClick={start} style={{ width: "100%", background: C.ink, color: C.white, border: "none", borderRadius: 13, padding: "18px", fontSize: 17, fontWeight: 800, cursor: "pointer", letterSpacing: "-0.02em", marginBottom: 10 }}>
▲ Build my MVP
</button>
<div style={{ fontSize: 12.5, color: C.muted, textAlign: "center" }}>~15 minutes · All pages built in parallel · You can refine everything after it's live</div>
</div>
</div>
);
}
// ─── FLOATING CHAT ────────────────────────────────────────────────────────────
const STARTERS = {
welcome: ["What exactly gets built?", "How long does the whole thing take?", "What do I need to prepare?"],
discover: ["Help me think through my idea", "What makes a good SaaS product?", "Who should I target first?"],
architect: ["Why did you choose these components?", "What's the difference between these options?", "Can I change things after it's built?"],
design: ["Which style works best for B2B?", "Can I change it after launch?", "What do the reference products look like?"],
market: ["How should I describe my voice?", "What makes a good campaign topic?", "How much content will be generated?"],
build: ["What gets built first?", "What do I do when it's live?", "Can I make changes during the build?"],
};
function FloatingChat({ msgs, onSend, phase, onClose }) {
const [input, setInput] = useState("");
const bottomRef = useRef(null);
useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: "smooth" }); }, [msgs]);
const send = (val) => {
const text = val || input; if (!text.trim()) return; setInput("");
onSend({ role: "user", text });
setTimeout(() => onSend({ role: "assistant", text: "Good question. Based on everything you've told me so far, I'd say: focus on getting to a live URL first. Every decision you're making now can be refined after launch." }), 600);
};
return (
<div style={{ position: "fixed", bottom: 80, right: 24, width: 304, background: C.white, borderRadius: 18, boxShadow: "0 8px 40px rgba(0,0,0,0.18)", border: `1px solid ${C.border2}`, zIndex: 500, display: "flex", flexDirection: "column", overflow: "hidden", animation: "slideup 0.2s ease" }}>
<div style={{ padding: "12px 14px", background: C.ink, display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<div style={{ width: 7, height: 7, borderRadius: "50%", background: C.green, animation: "vpulse 2s infinite" }} />
<span style={{ fontSize: 13, fontWeight: 700, color: C.white }}>Assist</span>
<span style={{ fontSize: 11, color: "rgba(255,255,255,0.35)", textTransform: "capitalize" }}>· {phase}</span>
</div>
<button onClick={onClose} style={{ background: "transparent", border: "none", color: "rgba(255,255,255,0.45)", cursor: "pointer", fontSize: 20 }}>×</button>
</div>
<div style={{ maxHeight: 220, overflowY: "auto", padding: "12px 14px", display: "flex", flexDirection: "column", gap: 8 }}>
{msgs.map((msg, i) => (
<div key={i} style={{ display: "flex", flexDirection: "column", alignItems: msg.role === "user" ? "flex-end" : "flex-start" }}>
<div style={{ maxWidth: "90%", background: msg.role === "user" ? C.ink : C.surface, color: msg.role === "user" ? C.white : C.ink2, borderRadius: msg.role === "user" ? "12px 12px 3px 12px" : "12px 12px 12px 3px", padding: "8px 11px", fontSize: 12.5, lineHeight: 1.55, border: msg.role === "assistant" ? `1px solid ${C.border}` : "none" }}>{msg.text}</div>
</div>
))}
<div ref={bottomRef} />
</div>
{msgs.length < 2 && (
<div style={{ padding: "0 10px 8px", display: "flex", flexDirection: "column", gap: 4 }}>
{(STARTERS[phase] || []).map((s, i) => (
<button key={i} onClick={() => send(s)} style={{ fontSize: 12, color: C.ink3, background: C.surface, border: `1px solid ${C.border2}`, borderRadius: 7, padding: "6px 10px", cursor: "pointer", textAlign: "left" }}>{s}</button>
))}
</div>
)}
<div style={{ padding: "8px 10px 12px", borderTop: `1px solid ${C.border}`, display: "flex", gap: 6 }}>
<input value={input} onChange={(e) => setInput(e.target.value)} onKeyDown={(e) => e.key === "Enter" && send()} placeholder="Ask anything" style={{ flex: 1, border: `1px solid ${C.border2}`, borderRadius: 8, padding: "7px 10px", fontSize: 12.5, color: C.ink, outline: "none", background: C.surface }} />
<button onClick={() => send()} style={{ background: C.ink, color: C.white, border: "none", borderRadius: 8, padding: "7px 12px", fontSize: 12, fontWeight: 600, cursor: "pointer" }}>→</button>
</div>
</div>
);
}
// ─── ROOT ─────────────────────────────────────────────────────────────────────
const SIDEBAR_PHASES = PHASES.filter((p) => p.id !== "welcome");
const CHECKLIST = [
{ key: "prd", label: "Product plan", done: (p) => p !== "welcome" },
{ key: "arch", label: "Architecture", done: (p) => ["architect", "design", "market", "build"].includes(p) },
{ key: "design", label: "Product design", done: (p) => ["design", "market", "build"].includes(p) },
{ key: "market", label: "Marketing", done: (p) => ["market", "build"].includes(p) },
];
export default function App() {
const [phase, setPhase] = useState("website");
const [prd, setPrd] = useState({});
const [arch, setArch] = useState({});
const [design, setDesign] = useState({});
const [market, setMarket] = useState({ topics: SUGGESTED_TOPICS() });
const [chatOpen, setChatOpen] = useState(false);
const [chatMsgs, setChatMsgs] = useState([{ role: "assistant", text: "I'm here throughout the whole setup. Ask me anything." }]);
const goTo = (id) => setPhase(id);
return (
<div style={{ minHeight: "100vh", background: C.surface, display: "flex", flexDirection: "row" }}>
{["discover","architect","design","market","build"].includes(phase) && (
<aside style={{ width: 220, minWidth: 220, background: C.white, borderRight: `1px solid ${C.border2}`, flexShrink: 0, minHeight: "100vh", display: "flex", flexDirection: "column" }}>
{/* Logo */}
<div style={{ padding: "18px 18px 16px", display: "flex", alignItems: "center", gap: 9, borderBottom: `1px solid ${C.border}` }}>
<div style={{ width: 28, height: 28, borderRadius: 7, background: C.ink, display: "flex", alignItems: "center", justifyContent: "center", color: C.white, fontSize: 13, fontWeight: 700, letterSpacing: "-0.04em", flexShrink: 0 }}>v</div>
<span style={{ fontSize: 15, fontWeight: 700, color: C.ink, letterSpacing: "-0.04em" }}>vibn</span>
</div>
{/* Progress */}
<div style={{ padding: "16px 16px 12px", borderBottom: `1px solid ${C.border}` }}>
<div style={{ fontSize: 10, fontWeight: 700, color: C.muted, textTransform: "uppercase", letterSpacing: "0.08em", marginBottom: 10 }}>Progress</div>
{CHECKLIST.map((c) => (
<div key={c.key} style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 7, fontSize: 12.5, color: c.done(phase) ? C.ink3 : C.muted }}>
<span style={{ color: c.done(phase) ? C.green : C.border2, fontSize: 10, flexShrink: 0 }}>{c.done(phase) ? "" : ""}</span>
{c.label}
</div>
))}
</div>
{/* Phase nav */}
<div style={{ padding: "12px 10px", flex: 1 }}>
<div style={{ fontSize: 10, fontWeight: 700, color: C.muted, textTransform: "uppercase", letterSpacing: "0.08em", padding: "0 8px", marginBottom: 6 }}>Phases</div>
{SIDEBAR_PHASES.map((p) => (
<button
key={p.id}
onClick={() => goTo(p.id)}
style={{
width: "100%",
display: "flex",
alignItems: "center",
gap: 9,
padding: "8px 10px",
marginBottom: 2,
borderRadius: 6,
border: "none",
background: phase === p.id ? C.purpleBg : "transparent",
color: phase === p.id ? C.purple : C.ink3,
cursor: "pointer",
textAlign: "left",
fontSize: 13,
fontWeight: phase === p.id ? 600 : 500,
transition: "background 0.12s",
}}
onMouseEnter={(e) => { if (phase !== p.id) e.currentTarget.style.background = C.surface; }}
onMouseLeave={(e) => { if (phase !== p.id) e.currentTarget.style.background = "transparent"; }}
>
<span style={{ opacity: 0.6, fontSize: 12, width: 16, textAlign: "center", flexShrink: 0 }}>{p.icon}</span>
{p.label}
</button>
))}
</div>
{/* User */}
<div style={{ padding: "14px 18px", borderTop: `1px solid ${C.border}`, display: "flex", alignItems: "center", gap: 9 }}>
<div style={{ width: 28, height: 28, borderRadius: "50%", background: C.surface, border: `1px solid ${C.border2}`, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 11, fontWeight: 700, color: C.ink3, flexShrink: 0 }}>M</div>
<div style={{ overflow: "hidden" }}>
<div style={{ fontSize: 12.5, fontWeight: 600, color: C.ink, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>Mark</div>
<div style={{ fontSize: 11, color: C.muted }}>Pro plan</div>
</div>
</div>
</aside>
)}
<main style={{ flex: 1, display: "flex", flexDirection: "column", minHeight: 0 }}>
{phase === "welcome" && <WelcomePhase onStart={() => setPhase("discover")} />}
{phase === "website" && <Website onGetStarted={() => setPhase("welcome")} onLogin={() => setPhase("dashboard")} />}
{phase === "dashboard" && (
<Dashboard
onNewProject={() => setPhase("welcome")}
onContinueBuilding={() => setPhase("build")}
/>
)}
{phase === "discover" && <DiscoverPhase onComplete={(data) => { setPrd(data); setPhase("architect"); }} />}
{phase === "architect" && <ArchitectPhase onComplete={(data) => { setArch(data); setPhase("design"); }} />}
{phase === "design" && <DesignPhase onComplete={(data) => { setDesign(data); setPhase("market"); }} />}
{phase === "market" && <MarketPhase prd={prd} onComplete={(data) => { setMarket(data); setPhase("build"); }} />}
{phase === "build" && <BuildPhase prd={prd} arch={arch} design={design} market={market} />}
</main>
{["discover","architect","design","market","build"].includes(phase) && !chatOpen && (
<button
onClick={() => setChatOpen(true)}
style={{
position: "fixed",
bottom: 24,
right: 24,
width: 56,
height: 56,
borderRadius: "50%",
background: C.ink,
color: C.white,
border: "none",
boxShadow: "0 4px 20px rgba(0,0,0,0.2)",
cursor: "pointer",
fontSize: 20,
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: 400,
}}
>
💬
</button>
)}
{["discover","architect","design","market","build"].includes(phase) && chatOpen && (
<FloatingChat
msgs={chatMsgs}
onSend={(msg) => setChatMsgs((m) => [...m, msg])}
phase={phase}
onClose={() => setChatOpen(false)}
/>
)}
</div>
);
}

View File

@@ -0,0 +1,497 @@
// vibn — Projects Dashboard
// Restyled from original (DM Sans + purple/colour accents) → Ink & parchment
// Design: Lora serif + Inter sans, #1a1510 ink, #f7f4ee paper, no colour accent
// Usage: default export, no required props
import { useState } from "react";
const T = {
ink: "#1a1510",
ink2: "#2c2c2a",
ink3: "#444441",
mid: "#5f5e5a",
muted: "#888780",
stone: "#b4b2a9",
parch: "#d3d1c7",
cream: "#f1efe8",
paper: "#f7f4ee",
white: "#fdfcfa",
border: "#e8e2d9",
border2:"#d3d1c7",
};
const F = { serif: "'Lora', Georgia, serif", sans: "'Inter', sans-serif" };
// ─── Shared primitives ─────────────────────────────────────────────────────────
function StatusPill({ label, variant = "default" }) {
const styles = {
live: { bg: T.cream, text: T.ink3, border: T.border },
building: { bg: T.cream, text: T.ink3, border: T.border },
default: { bg: T.paper, text: T.muted, border: T.border },
invoiced: { bg: T.ink, text: T.paper, border: T.ink },
unbilled: { bg: T.cream, text: T.ink3, border: T.border },
scheduled: { bg: T.parch, text: T.ink2, border: T.border2 },
};
const s = styles[variant] || styles.default;
return (
<span style={{
fontFamily: F.sans, fontSize: 10.5, fontWeight: 600,
color: s.text, background: s.bg,
border: `1px solid ${s.border}`,
borderRadius: 5, padding: "2px 8px", whiteSpace: "nowrap",
}}>{label}</span>
);
}
function InkBtn({ children, onClick, small, outline }) {
return (
<button onClick={onClick} style={{
fontFamily: F.sans, fontWeight: 600,
fontSize: small ? 12 : 13.5,
padding: small ? "6px 14px" : "10px 22px",
background: outline ? "transparent" : T.ink,
color: outline ? T.ink3 : T.paper,
border: outline ? `1px solid ${T.border2}` : "none",
borderRadius: 8, cursor: "pointer",
}}>{children}</button>
);
}
// ─── Nav ───────────────────────────────────────────────────────────────────────
function Nav({ screen, setScreen, onNewProject }) {
return (
<nav style={{
background: T.white, borderBottom: `1px solid ${T.border}`,
padding: "0 32px", height: 60, display: "flex",
alignItems: "center", justifyContent: "space-between",
}}>
<div onClick={() => setScreen("projects")} style={{ display: "flex", alignItems: "center", gap: 10, cursor: "pointer" }}>
<div style={{ width: 28, height: 28, background: T.ink, borderRadius: 7, display: "flex", alignItems: "center", justifyContent: "center" }}>
<span style={{ fontFamily: F.serif, fontSize: 14, fontWeight: 700, color: T.paper }}>V</span>
</div>
<span style={{ fontFamily: F.serif, fontSize: 18, fontWeight: 700, color: T.ink, letterSpacing: "-0.02em" }}>vibn</span>
</div>
<div style={{ display: "flex", alignItems: "center", gap: 22 }}>
{screen === "billing" && (
<span onClick={() => setScreen("projects")} style={{ fontFamily: F.sans, fontSize: 13.5, color: T.muted, cursor: "pointer" }}> All projects</span>
)}
<span style={{ fontFamily: F.sans, fontSize: 13.5, color: T.muted, cursor: "pointer" }}>Settings</span>
<div style={{ width: 30, height: 30, borderRadius: "50%", background: T.ink3, display: "flex", alignItems: "center", justifyContent: "center", fontFamily: F.sans, fontSize: 11, color: T.paper, fontWeight: 700 }}>MH</div>
</div>
</nav>
);
}
// ─── Data ──────────────────────────────────────────────────────────────────────
const PROJECTS = [
{
id: "launchpad", label: "Launchpad", initial: "L",
type: "own", status: "live", url: "launchpad.vibn.app",
stats: { visitors: "2.4k", signups: 183, mrr: "$840" },
},
{
id: "flowmatic", label: "Flowmatic", initial: "F",
type: "client", status: "live", url: "flowmatic.app",
client: "Acme Corp",
stats: { visitors: "890", signups: 54, mrr: "$210" },
costs: { total: 48.20, llm: 29.20, compute: 11.60, other: 7.40, billed: false },
},
{
id: "taskly", label: "Taskly", initial: "T",
type: "client", status: "building", url: null,
client: "Beta Labs", buildProgress: 60,
costs: { total: 12.40, llm: 9.20, compute: 3.20, other: 0, billed: false },
},
];
const ACTIVITY = [
{ text: "Launchpad — Blog post published:", detail: '"How to launch faster with AI"', time: "2h ago" },
{ text: "Flowmatic — New signup:", detail: "marcus@email.com", time: "4h ago" },
{ text: "Taskly — Checkout page built and deployed", detail: "", time: "6h ago" },
{ text: "Launchpad — Newsletter #12", detail: "scheduled", time: "Yesterday" },
];
const BILLING_ROWS = [
{ label: "Flowmatic", initial: "F", client: "Acme Corp", llm: 29.20, compute: 11.60, other: 7.40, total: 48.20, billed: false },
{ label: "Taskly", initial: "T", client: "Beta Labs", llm: 9.20, compute: 3.20, other: 0, total: 12.40, billed: false },
{ label: "Flowmatic", initial: "F", client: "Acme · Feb", llm: 22.10, compute: 8.40, other: 4.20, total: 34.70, billed: true },
];
const COST_LOG = [
{ time: "2h ago", desc: "LLM: Homepage copy generation", project: "Flowmatic", cost: 0.82 },
{ time: "3h ago", desc: "LLM: Checkout page code", project: "Taskly", cost: 1.24 },
{ time: "5h ago", desc: "LLM: Weekly newsletter draft", project: "Flowmatic", cost: 0.43 },
{ time: "6h ago", desc: "Compute: Build pipeline run", project: "Taskly", cost: 0.18 },
{ time: "8h ago", desc: "LLM: Discover phase Q&A", project: "Flowmatic", cost: 0.31 },
{ time: "Yesterday", desc: "Email delivery · 240 recipients", project: "Flowmatic", cost: 0.96 },
];
// ─── Project card ──────────────────────────────────────────────────────────────
function ProjectCard({ project, onContinue }) {
const isClient = project.type === "client";
const isBuilding = project.status === "building";
return (
<div style={{ background: T.white, border: `1px solid ${T.border}`, borderRadius: 14, overflow: "hidden" }}>
{/* Header preview */}
{isBuilding ? (
<div style={{ height: 100, background: T.cream, borderBottom: `1px solid ${T.border}`, display: "flex", alignItems: "center", justifyContent: "center", position: "relative" }}>
<div style={{ textAlign: "center" }}>
<div style={{ fontFamily: F.sans, fontSize: 11.5, color: T.muted, fontWeight: 500, marginBottom: 10 }}>
Build phase · {project.buildProgress}% complete
</div>
<div style={{ width: 160, height: 4, background: T.parch, borderRadius: 3, overflow: "hidden" }}>
<div style={{ width: `${project.buildProgress}%`, height: "100%", background: T.ink3, borderRadius: 3 }} />
</div>
</div>
{isClient && (
<div style={{ position: "absolute", top: 10, right: 12, fontFamily: F.sans, fontSize: 10, fontWeight: 600, color: T.mid, background: T.white, border: `1px solid ${T.border}`, borderRadius: 5, padding: "2px 8px" }}>
Client
</div>
)}
</div>
) : (
<div style={{ height: 100, background: T.ink, display: "flex", alignItems: "center", justifyContent: "center", position: "relative" }}>
<div style={{ background: "rgba(247,244,238,0.1)", borderRadius: 8, width: "55%", padding: "10px 14px" }}>
<div style={{ height: 8, background: "rgba(247,244,238,0.6)", borderRadius: 3, width: "65%", marginBottom: 6 }} />
<div style={{ height: 5, background: "rgba(247,244,238,0.25)", borderRadius: 3, width: "85%" }} />
</div>
<div style={{ position: "absolute", top: 10, right: 12, fontFamily: F.sans, fontSize: 10, fontWeight: 600, color: "rgba(247,244,238,0.6)", background: "rgba(247,244,238,0.1)", borderRadius: 5, padding: "2px 8px" }}>
{isClient ? "Client" : "My product"}
</div>
</div>
)}
<div style={{ padding: "18px 20px" }}>
{/* Identity row */}
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 12 }}>
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<div style={{ width: 28, height: 28, background: T.ink, borderRadius: 8, display: "flex", alignItems: "center", justifyContent: "center", fontFamily: F.serif, fontSize: 12, color: T.paper, fontWeight: 700 }}>
{project.initial}
</div>
<div>
<div style={{ fontFamily: F.serif, fontSize: 15, fontWeight: 700, color: T.ink }}>{project.label}</div>
<div style={{ fontFamily: F.sans, fontSize: 11, color: T.muted }}>
{isClient ? `${project.client} · ` : ""}
{project.url || "Setting up pages…"}
</div>
</div>
</div>
<StatusPill label={isBuilding ? "Building" : "Live"} variant={isBuilding ? "building" : "live"} />
</div>
{/* Cost strip — client + building */}
{isClient && project.costs && isBuilding && (
<div style={{ background: T.cream, border: `1px solid ${T.border}`, borderRadius: 9, padding: "10px 14px", marginBottom: 12, display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<div>
<div style={{ fontFamily: F.sans, fontSize: 10, fontWeight: 600, color: T.muted, textTransform: "uppercase", letterSpacing: "0.05em", marginBottom: 3 }}>Costs so far</div>
<div style={{ fontFamily: F.serif, fontSize: 17, fontWeight: 700, color: T.ink }}>${project.costs.total.toFixed(2)}</div>
</div>
<StatusPill label="Unbilled" variant="unbilled" />
</div>
)}
{/* Cost strip — client + live */}
{isClient && project.costs && !isBuilding && (
<div style={{ background: T.cream, border: `1px solid ${T.border}`, borderRadius: 9, padding: "10px 14px", marginBottom: 12, display: "flex", alignItems: "center", gap: 10 }}>
<div style={{ flex: 1 }}>
<div style={{ fontFamily: F.sans, fontSize: 10, fontWeight: 600, color: T.muted, textTransform: "uppercase", letterSpacing: "0.05em", marginBottom: 3 }}>Costs this month</div>
<div style={{ fontFamily: F.serif, fontSize: 17, fontWeight: 700, color: T.ink }}>${project.costs.total.toFixed(2)}</div>
</div>
<div style={{ fontFamily: F.sans, fontSize: 11, color: T.mid, lineHeight: 1.7 }}>
LLM ${project.costs.llm.toFixed(2)}<br />
Compute ${project.costs.compute.toFixed(2)}
</div>
{!project.costs.billed && (
<button style={{ background: T.ink, border: "none", color: T.paper, borderRadius: 7, padding: "7px 13px", fontFamily: F.sans, fontSize: 11.5, fontWeight: 600, cursor: "pointer" }}>
Bill
</button>
)}
</div>
)}
{/* Stats */}
{!isBuilding && project.stats && (
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 8, marginBottom: 14 }}>
{[["visitors", project.stats.visitors], ["signups", project.stats.signups], ["MRR", project.stats.mrr]].map(([k, v]) => (
<div key={k} style={{ textAlign: "center" }}>
<div style={{ fontFamily: F.serif, fontSize: 16, fontWeight: 700, color: T.ink }}>{v}</div>
<div style={{ fontFamily: F.sans, fontSize: 10, color: T.muted }}>{k}</div>
</div>
))}
</div>
)}
{/* Actions */}
{isBuilding ? (
<button onClick={onContinue} style={{ width: "100%", background: T.ink, border: "none", color: T.paper, borderRadius: 8, padding: 10, fontFamily: F.sans, fontSize: 13, fontWeight: 600, cursor: "pointer" }}>
Continue building
</button>
) : (
<div style={{ display: "flex", gap: 6 }}>
{[["⬡", "Build"], ["◈", "Grow"]].map(([icon, label]) => (
<div key={label} style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", gap: 5, padding: "7px 10px", background: T.cream, border: `1px solid ${T.border}`, borderRadius: 7, cursor: "pointer" }}>
<span style={{ fontSize: 11 }}>{icon}</span>
<span style={{ fontFamily: F.sans, fontSize: 11.5, color: T.ink3 }}>{label}</span>
</div>
))}
<div style={{ padding: "7px 12px", background: T.cream, border: `1px solid ${T.border}`, borderRadius: 7, fontFamily: F.sans, fontSize: 11.5, color: T.ink3, cursor: "pointer" }}>
</div>
</div>
)}
</div>
</div>
);
}
// ─── Projects screen ───────────────────────────────────────────────────────────
function ProjectsScreen({ setScreen, onNewProject, onContinueBuilding }) {
const totalUnbilled = PROJECTS
.filter(p => p.type === "client" && p.costs?.billed === false)
.reduce((s, p) => s + p.costs.total, 0);
return (
<div style={{ maxWidth: 1000, margin: "0 auto", padding: "36px 32px" }}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 32 }}>
<div>
<h1 style={{ fontFamily: F.serif, fontSize: 26, fontWeight: 700, color: T.ink, letterSpacing: "-0.02em", marginBottom: 4 }}>Your projects</h1>
<p style={{ fontFamily: F.sans, fontSize: 13.5, color: T.muted }}>3 active · 1 building</p>
</div>
<div style={{ display: "flex", gap: 10, alignItems: "center" }}>
{totalUnbilled > 0 && (
<button onClick={() => setScreen("billing")} style={{ fontFamily: F.sans, fontSize: 13, color: T.ink3, background: T.cream, border: `1px solid ${T.border}`, borderRadius: 8, padding: "9px 16px", cursor: "pointer" }}>
${totalUnbilled.toFixed(2)} unbilled
</button>
)}
<InkBtn onClick={onNewProject}>+ New project</InkBtn>
</div>
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 14, marginBottom: 28 }}>
{PROJECTS.map(p => (
<ProjectCard
key={p.id}
project={p}
onContinue={p.status === "building" ? onContinueBuilding : undefined}
/>
))}
{/* New project CTA card */}
<div
onClick={onNewProject}
style={{ background: "transparent", border: `1px dashed ${T.parch}`, borderRadius: 14, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 12, padding: 40, cursor: "pointer", minHeight: 220 }}
onMouseEnter={e => e.currentTarget.style.background = T.cream}
onMouseLeave={e => e.currentTarget.style.background = "transparent"}
>
<div style={{ width: 42, height: 42, borderRadius: 10, border: `1px solid ${T.parch}`, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 22, color: T.stone }}>+</div>
<div style={{ textAlign: "center" }}>
<div style={{ fontFamily: F.serif, fontSize: 14, fontWeight: 600, color: T.ink, marginBottom: 4 }}>New project</div>
<div style={{ fontFamily: F.sans, fontSize: 12.5, color: T.muted }}>For yourself or a client</div>
</div>
</div>
</div>
{/* Activity feed */}
<div style={{ background: T.white, border: `1px solid ${T.border}`, borderRadius: 14, padding: "20px 24px" }}>
<div style={{ fontFamily: F.serif, fontSize: 14, fontWeight: 600, color: T.ink, marginBottom: 16 }}>Recent activity</div>
{ACTIVITY.map((a, i) => (
<div key={i} style={{ display: "flex", alignItems: "center", gap: 12, padding: "11px 0", borderBottom: i < ACTIVITY.length - 1 ? `1px solid ${T.border}` : "none" }}>
<div style={{ width: 7, height: 7, borderRadius: "50%", background: T.ink3, flexShrink: 0 }} />
<div style={{ flex: 1, fontFamily: F.sans, fontSize: 13.5, color: T.ink }}>
{a.text}{" "}{a.detail && <span style={{ color: T.muted }}>{a.detail}</span>}
</div>
<span style={{ fontFamily: F.sans, fontSize: 11.5, color: T.stone, whiteSpace: "nowrap" }}>{a.time}</span>
</div>
))}
</div>
</div>
);
}
// ─── Billing screen ────────────────────────────────────────────────────────────
function BillingScreen() {
const [tab, setTab] = useState("billing");
const unbilled = BILLING_ROWS.filter(r => !r.billed).reduce((s, r) => s + r.total, 0);
return (
<div style={{ maxWidth: 1000, margin: "0 auto", padding: "28px 32px" }}>
{/* Sub-tabs */}
<div style={{ display: "flex", borderBottom: `1px solid ${T.border}`, marginBottom: 28 }}>
{[["billing", "Client billing"], ["costs", "Cost tracker"]].map(([id, label]) => (
<button key={id} onClick={() => setTab(id)} style={{
padding: "10px 18px", border: "none", background: "transparent",
borderBottom: tab === id ? `2px solid ${T.ink}` : "2px solid transparent",
fontFamily: F.sans, fontSize: 13.5, cursor: "pointer",
color: tab === id ? T.ink : T.muted, fontWeight: tab === id ? 600 : 400,
}}>{label}</button>
))}
</div>
{tab === "billing" && <>
<div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", marginBottom: 22 }}>
<div>
<h2 style={{ fontFamily: F.serif, fontSize: 22, fontWeight: 700, color: T.ink, marginBottom: 4 }}>Client billing</h2>
<p style={{ fontFamily: F.sans, fontSize: 13, color: T.muted }}>All costs tracked and ready to invoice</p>
</div>
<InkBtn>Generate invoice</InkBtn>
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr 1fr", gap: 10, marginBottom: 24 }}>
{[
{ label: "Total unbilled", value: `$${unbilled.toFixed(2)}` },
{ label: "LLM costs", value: "$38.40" },
{ label: "Compute", value: "$14.80" },
{ label: "Other", value: "$7.40" },
].map(c => (
<div key={c.label} style={{ background: T.cream, borderRadius: 10, padding: "14px 16px" }}>
<div style={{ fontFamily: F.sans, fontSize: 11, color: T.muted, marginBottom: 5 }}>{c.label}</div>
<div style={{ fontFamily: F.serif, fontSize: 22, fontWeight: 700, color: T.ink }}>{c.value}</div>
</div>
))}
</div>
<div style={{ background: T.white, border: `1px solid ${T.border}`, borderRadius: 12, overflow: "hidden" }}>
<div style={{ padding: "13px 20px", borderBottom: `1px solid ${T.border}`, display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<span style={{ fontFamily: F.serif, fontSize: 14, fontWeight: 600, color: T.ink }}>Breakdown by client</span>
<select style={{ border: `1px solid ${T.border}`, borderRadius: 6, padding: "4px 10px", fontFamily: F.sans, fontSize: 12, color: T.muted, background: T.paper, outline: "none" }}>
<option>March 2026</option>
</select>
</div>
<div style={{ display: "grid", gridTemplateColumns: "2fr 1fr 1fr 1fr 1fr 120px", padding: "9px 20px", background: T.cream, borderBottom: `1px solid ${T.border}` }}>
{["Project / Client", "LLM", "Compute", "Other", "Total", "Status"].map(h => (
<div key={h} style={{ fontFamily: F.sans, fontSize: 10.5, fontWeight: 600, color: T.muted, textTransform: "uppercase", letterSpacing: "0.05em" }}>{h}</div>
))}
</div>
{BILLING_ROWS.map((r, i) => (
<div key={i} style={{ display: "grid", gridTemplateColumns: "2fr 1fr 1fr 1fr 1fr 120px", padding: "13px 20px", borderBottom: i < BILLING_ROWS.length - 1 ? `1px solid ${T.border}` : "none", alignItems: "center", opacity: r.billed ? 0.5 : 1 }}>
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<div style={{ width: 24, height: 24, background: T.ink, borderRadius: 6, display: "flex", alignItems: "center", justifyContent: "center", fontFamily: F.serif, fontSize: 10, color: T.paper, fontWeight: 700 }}>{r.initial}</div>
<div>
<div style={{ fontFamily: F.sans, fontSize: 13, fontWeight: 600, color: T.ink }}>{r.label}</div>
<div style={{ fontFamily: F.sans, fontSize: 11, color: T.muted }}>{r.client}</div>
</div>
</div>
<div style={{ fontFamily: F.sans, fontSize: 13, color: T.ink }}>${r.llm.toFixed(2)}</div>
<div style={{ fontFamily: F.sans, fontSize: 13, color: T.ink }}>${r.compute.toFixed(2)}</div>
<div style={{ fontFamily: F.sans, fontSize: 13, color: T.ink }}>${r.other.toFixed(2)}</div>
<div style={{ fontFamily: F.sans, fontSize: 13, fontWeight: 600, color: T.ink }}>${r.total.toFixed(2)}</div>
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
<StatusPill label={r.billed ? "Invoiced" : "Unbilled"} variant={r.billed ? "invoiced" : "unbilled"} />
{!r.billed && (
<button style={{ background: "transparent", border: `1px solid ${T.border2}`, borderRadius: 5, padding: "3px 9px", fontFamily: F.sans, fontSize: 11, color: T.muted, cursor: "pointer" }}>Invoice</button>
)}
</div>
</div>
))}
</div>
</>}
{tab === "costs" && <>
<div style={{ marginBottom: 22 }}>
<h2 style={{ fontFamily: F.serif, fontSize: 22, fontWeight: 700, color: T.ink, marginBottom: 4 }}>Cost tracker</h2>
<p style={{ fontFamily: F.sans, fontSize: 13, color: T.muted }}>Every dollar spent, broken down by type and project</p>
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 14, marginBottom: 20 }}>
<div style={{ background: T.white, border: `1px solid ${T.border}`, borderRadius: 12, padding: "18px 20px" }}>
<div style={{ fontFamily: F.serif, fontSize: 14, fontWeight: 600, color: T.ink, marginBottom: 16 }}>LLM usage</div>
{[
{ label: "Code generation", amount: 21.40, pct: 56 },
{ label: "Content & marketing", amount: 10.20, pct: 27 },
{ label: "Chat assist", amount: 6.80, pct: 18 },
].map(r => (
<div key={r.label} style={{ marginBottom: 14 }}>
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: 6 }}>
<span style={{ fontFamily: F.sans, fontSize: 12.5, color: T.mid }}>{r.label}</span>
<span style={{ fontFamily: F.sans, fontSize: 12.5, fontWeight: 600, color: T.ink }}>${r.amount.toFixed(2)}</span>
</div>
<div style={{ height: 4, background: T.cream, borderRadius: 2, overflow: "hidden" }}>
<div style={{ width: `${r.pct}%`, height: "100%", background: T.ink, borderRadius: 2 }} />
</div>
</div>
))}
<div style={{ marginTop: 14, paddingTop: 12, borderTop: `1px solid ${T.border}`, display: "flex", justifyContent: "space-between" }}>
<span style={{ fontFamily: F.sans, fontSize: 12, color: T.muted }}>Total LLM</span>
<span style={{ fontFamily: F.serif, fontSize: 15, fontWeight: 700, color: T.ink }}>$38.40</span>
</div>
</div>
<div style={{ background: T.white, border: `1px solid ${T.border}`, borderRadius: 12, padding: "18px 20px" }}>
<div style={{ fontFamily: F.serif, fontSize: 14, fontWeight: 600, color: T.ink, marginBottom: 16 }}>Infrastructure</div>
{[
{ label: "Hosting & compute", amount: 11.60 },
{ label: "Database", amount: 3.20 },
{ label: "Email delivery", amount: 4.20 },
{ label: "Domain & SSL", amount: 3.20 },
].map(r => (
<div key={r.label} style={{ display: "flex", justifyContent: "space-between", padding: "8px 11px", background: T.cream, borderRadius: 7, marginBottom: 7 }}>
<span style={{ fontFamily: F.sans, fontSize: 13, color: T.mid }}>{r.label}</span>
<span style={{ fontFamily: F.sans, fontSize: 13, fontWeight: 600, color: T.ink }}>${r.amount.toFixed(2)}</span>
</div>
))}
<div style={{ marginTop: 14, paddingTop: 12, borderTop: `1px solid ${T.border}`, display: "flex", justifyContent: "space-between" }}>
<span style={{ fontFamily: F.sans, fontSize: 12, color: T.muted }}>Total infra</span>
<span style={{ fontFamily: F.serif, fontSize: 15, fontWeight: 700, color: T.ink }}>$22.20</span>
</div>
</div>
</div>
<div style={{ background: T.white, border: `1px solid ${T.border}`, borderRadius: 12, overflow: "hidden" }}>
<div style={{ padding: "13px 20px", borderBottom: `1px solid ${T.border}` }}>
<span style={{ fontFamily: F.serif, fontSize: 14, fontWeight: 600, color: T.ink }}>Recent charges</span>
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 2fr 1fr 80px", padding: "9px 20px", background: T.cream, borderBottom: `1px solid ${T.border}` }}>
{["Time", "Description", "Project", "Cost"].map(h => (
<div key={h} style={{ fontFamily: F.sans, fontSize: 10.5, fontWeight: 600, color: T.muted, textTransform: "uppercase", letterSpacing: "0.05em" }}>{h}</div>
))}
</div>
{COST_LOG.map((row, i) => (
<div key={i} style={{ display: "grid", gridTemplateColumns: "1fr 2fr 1fr 80px", padding: "11px 20px", borderBottom: i < COST_LOG.length - 1 ? `1px solid ${T.border}` : "none", alignItems: "center" }}>
<div style={{ fontFamily: F.sans, fontSize: 12, color: T.muted }}>{row.time}</div>
<div style={{ fontFamily: F.sans, fontSize: 13, color: T.ink }}>{row.desc}</div>
<div style={{ fontFamily: F.sans, fontSize: 12, color: T.mid }}>{row.project}</div>
<div style={{ fontFamily: F.sans, fontSize: 13, fontWeight: 600, color: T.ink }}>${row.cost.toFixed(2)}</div>
</div>
))}
</div>
</>}
</div>
);
}
// ─── Root export ───────────────────────────────────────────────────────────────
export default function Dashboard({ onNewProject, onContinueBuilding }) {
const [screen, setScreen] = useState("projects");
return (
<div style={{ background: T.paper, minHeight: "100vh" }}>
<style>{`
@import url('https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,600;0,700;1,400;1,600&family=Inter:wght@400;500;600&display=swap');
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
button { font-family: inherit; cursor: pointer; }
input, select { font-family: inherit; }
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-thumb { background: ${T.parch}; border-radius: 4px; }
`}</style>
<Nav screen={screen} setScreen={setScreen} />
{screen === "projects" && (
<ProjectsScreen
setScreen={setScreen}
onNewProject={onNewProject}
onContinueBuilding={onContinueBuilding}
/>
)}
{screen === "billing" && <BillingScreen />}
</div>
);
}

View File

@@ -0,0 +1,318 @@
import { useState } from "react";
// ── DESIGN TOKENS ─────────────────────────────────────────────────────────────
const T = {
ink: "#1a1510",
ink2: "#2c2c2a",
ink3: "#444441",
mid: "#5f5e5a",
muted: "#888780",
stone: "#b4b2a9",
parch: "#d3d1c7",
cream: "#f1efe8",
paper: "#f7f4ee",
white: "#fdfcfa",
border: "#e8e2d9",
border2: "#d3d1c7",
};
const serif = "'Lora', Georgia, serif";
const sans = "'Inter', sans-serif";
// ── SHARED COMPONENTS ─────────────────────────────────────────────────────────
function Nav({ onGetStarted, onLogin }) {
return (
<nav style={{
background: T.paper, borderBottom: `1px solid ${T.border}`,
padding: "0 48px", display: "flex", alignItems: "center",
justifyContent: "space-between", height: 60,
position: "sticky", top: 0, zIndex: 50,
}}>
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<div style={{
width: 30, height: 30, background: T.ink, borderRadius: 7,
display: "flex", alignItems: "center", justifyContent: "center",
}}>
<span style={{ fontFamily: serif, fontSize: 15, fontWeight: 700, color: T.paper, lineHeight: 1 }}>V</span>
</div>
<span style={{ fontFamily: serif, fontSize: 19, fontWeight: 700, color: T.ink, letterSpacing: "-0.02em" }}>vibn</span>
</div>
<div style={{ display: "flex", alignItems: "center", gap: 30 }}>
{["Product", "Pricing", "Stories", "Blog"].map(l => (
<span key={l} style={{ fontFamily: sans, fontSize: 14, color: T.muted, cursor: "pointer" }}>{l}</span>
))}
</div>
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<button onClick={onLogin} style={{ fontFamily: sans, fontSize: 14, color: T.muted, cursor: "pointer", background: "none", border: "none", padding: 0 }}>Log in</button>
<button onClick={onGetStarted} style={{
background: T.ink, color: T.paper, fontFamily: sans,
fontSize: 13.5, fontWeight: 600, padding: "9px 22px",
border: "none", borderRadius: 8, cursor: "pointer",
}}>Get started free</button>
</div>
</nav>
);
}
// ── HERO ──────────────────────────────────────────────────────────────────────
function Hero({ onCta }) {
return (
<section style={{ maxWidth: 960, margin: "0 auto", padding: "84px 48px 68px" }}>
<div style={{ fontFamily: sans, fontSize: 11, fontWeight: 600, letterSpacing: "0.13em", textTransform: "uppercase", color: T.muted, marginBottom: 22 }}>
For non-technical founders
</div>
<h1 style={{
fontFamily: serif, fontSize: 62, fontWeight: 700, color: T.ink,
letterSpacing: "-0.03em", lineHeight: 1.07, marginBottom: 26, maxWidth: 700,
}}>
You have the idea.<br />
We handle<br />
<em style={{ fontStyle: "italic", color: T.ink3 }}>everything else.</em>
</h1>
<p style={{ fontFamily: sans, fontSize: 17.5, color: T.mid, lineHeight: 1.75, maxWidth: 500, marginBottom: 38 }}>
No backend. No DevOps. No marketing agency. Describe your idea and vibn builds,
deploys, and promotes it automatically.
</p>
<div style={{ display: "flex", alignItems: "center", gap: 16, flexWrap: "wrap", marginBottom: 16 }}>
<button onClick={onCta} style={{
background: T.ink, color: T.paper, fontFamily: sans,
fontSize: 15, fontWeight: 600, padding: "14px 32px",
border: "none", borderRadius: 10, cursor: "pointer",
}}>
Start free no code needed
</button>
<span style={{ fontFamily: sans, fontSize: 13.5, color: T.stone }}>
&nbsp; 280 founders launched
</span>
</div>
<p style={{ fontFamily: sans, fontSize: 12, color: T.stone }}>
No credit card required · Free forever plan
</p>
</section>
);
}
// ── PULL QUOTE BAND ───────────────────────────────────────────────────────────
const QUOTES = [
{ text: "I had the idea for 2 years. The backend terrified me. vibn shipped it in 4 days and handles all my marketing.", name: "Alex K.", company: "founder of Taskly" },
{ text: "I have zero coding experience. Three weeks in, I have 300 paying users. That's entirely because of vibn.", name: "Marcus L.", company: "founder of Flowmatic" },
{ text: "The marketing autopilot alone saved me ten hours a week. My blog runs itself. I just focus on product.", name: "Sara R.", company: "founder of Nudge" },
];
function QuoteBand() {
return (
<section style={{ background: T.ink, padding: "52px 48px" }}>
<div style={{ maxWidth: 960, margin: "0 auto", display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 40 }}>
{QUOTES.map((q, i) => (
<div key={i} style={{ display: "flex", gap: 18 }}>
<div style={{ width: 3, background: T.mid, borderRadius: 2, flexShrink: 0 }} />
<div>
<p style={{ fontFamily: serif, fontSize: 15, color: T.parch, lineHeight: 1.7, fontStyle: "italic", marginBottom: 10 }}>
"{q.text}"
</p>
<span style={{ fontFamily: sans, fontSize: 11.5, color: T.muted, fontWeight: 600 }}>
{q.name}, {q.company}
</span>
</div>
</div>
))}
</div>
</section>
);
}
// ── HOW IT WORKS ──────────────────────────────────────────────────────────────
const HOW_PHASES = [
{ n: "01", phase: "Discover", title: "Define your idea", body: "Six guided questions turn a rough idea into a full product plan — pages, architecture, revenue model. No jargon, no assumptions." },
{ n: "02", phase: "Design", title: "Choose your style", body: "Pick a visual style and see your exact site and emails live before a single line of code is written." },
{ n: "03", phase: "Build", title: "Your app, live", body: "AI writes every line. Auth, database, payments, all pages — deployed and live. Describe changes in plain English." },
{ n: "04", phase: "Grow", title: "Market & automate", body: "AI generates your blog, emails, and social schedule — publishing on autopilot so you can focus entirely on your users." },
];
function HowItWorks() {
return (
<section style={{ maxWidth: 960, margin: "0 auto", padding: "80px 48px" }}>
<div style={{ fontFamily: sans, fontSize: 11, fontWeight: 600, letterSpacing: "0.13em", textTransform: "uppercase", color: T.muted, marginBottom: 16 }}>
How it works
</div>
<h2 style={{ fontFamily: serif, fontSize: 38, fontWeight: 700, color: T.ink, letterSpacing: "-0.02em", marginBottom: 52, maxWidth: 480, lineHeight: 1.15 }}>
Four phases. One complete product.
</h2>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 0, border: `1px solid ${T.border}`, borderRadius: 14, overflow: "hidden" }}>
{HOW_PHASES.map((p, i) => (
<div key={i} style={{
padding: "36px 40px",
background: T.white,
borderRight: i % 2 === 0 ? `1px solid ${T.border}` : "none",
borderBottom: i < 2 ? `1px solid ${T.border}` : "none",
}}>
<div style={{ fontFamily: sans, fontSize: 11, fontWeight: 600, letterSpacing: "0.1em", textTransform: "uppercase", color: T.muted, marginBottom: 16 }}>
{p.n} {p.phase}
</div>
<div style={{ fontFamily: serif, fontSize: 21, fontWeight: 700, color: T.ink, marginBottom: 10 }}>{p.title}</div>
<p style={{ fontFamily: sans, fontSize: 13.5, color: T.mid, lineHeight: 1.7 }}>{p.body}</p>
</div>
))}
</div>
</section>
);
}
// ── EMPATHY SECTION ───────────────────────────────────────────────────────────
const PAINS = [
{ title: "No more \"I need to hire a developer first\"", body: "vibn is your developer. Start building the moment you have an idea." },
{ title: "No more staring at a blank marketing calendar", body: "AI generates and publishes your content every single week." },
{ title: "No more \"I'll launch when it's ready\"", body: "Most founders ship their first version in under 72 hours." },
];
function EmpathySection() {
return (
<section style={{ background: T.cream, borderTop: `1px solid ${T.border}`, borderBottom: `1px solid ${T.border}`, padding: "72px 48px" }}>
<div style={{ maxWidth: 960, margin: "0 auto", display: "grid", gridTemplateColumns: "1fr 1fr", gap: 64, alignItems: "center" }}>
<div>
<div style={{ fontFamily: sans, fontSize: 11, fontWeight: 600, letterSpacing: "0.13em", textTransform: "uppercase", color: T.muted, marginBottom: 18 }}>
Sound familiar?
</div>
<h2 style={{ fontFamily: serif, fontSize: 34, fontWeight: 700, color: T.ink, lineHeight: 1.2, marginBottom: 24, letterSpacing: "-0.02em" }}>
The idea is the hard part. Everything else shouldn't be.
</h2>
<p style={{ fontFamily: sans, fontSize: 15, color: T.mid, lineHeight: 1.8, marginBottom: 20 }}>
You know exactly what you want to build and who it's for. But the moment you think about
servers, databases, deployment pipelines, SEO the whole thing stalls.
</p>
<p style={{ fontFamily: sans, fontSize: 15, color: T.mid, lineHeight: 1.8 }}>
vibn exists to remove all of that. Not abstract it {" "}
<em style={{ fontFamily: serif, fontStyle: "italic" }}>remove it entirely.</em>
</p>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
{PAINS.map((p, i) => (
<div key={i} style={{
background: T.white, border: `1px solid ${T.border}`,
borderRadius: 10, padding: "18px 20px",
display: "flex", alignItems: "flex-start", gap: 14,
}}>
<div style={{
width: 20, height: 20, borderRadius: "50%",
border: `1.5px solid ${T.stone}`,
display: "flex", alignItems: "center", justifyContent: "center",
flexShrink: 0, marginTop: 2,
}}>
<div style={{ width: 7, height: 7, borderRadius: "50%", background: T.ink }} />
</div>
<div>
<div style={{ fontFamily: serif, fontSize: 14, fontWeight: 600, color: T.ink, marginBottom: 4 }}>{p.title}</div>
<div style={{ fontFamily: sans, fontSize: 13, color: T.muted, lineHeight: 1.6 }}>{p.body}</div>
</div>
</div>
))}
</div>
</div>
</section>
);
}
// ── STATS BAR ─────────────────────────────────────────────────────────────────
function StatsBar() {
const stats = [
{ n: "280+", label: "founders launched" },
{ n: "72h", label: "average time to first version" },
{ n: "4.9", label: "average rating" },
{ n: "3×", label: "faster than hiring a developer" },
];
return (
<section style={{ borderTop: `1px solid ${T.border}`, borderBottom: `1px solid ${T.border}`, background: T.white }}>
<div style={{ maxWidth: 960, margin: "0 auto", padding: "0 48px", display: "grid", gridTemplateColumns: "1fr 1fr 1fr 1fr" }}>
{stats.map((s, i) => (
<div key={i} style={{
padding: "36px 0",
borderRight: i < 3 ? `1px solid ${T.border}` : "none",
paddingLeft: i > 0 ? 36 : 0,
}}>
<div style={{ fontFamily: serif, fontSize: 36, fontWeight: 700, color: T.ink, letterSpacing: "-0.03em", marginBottom: 6 }}>{s.n}</div>
<div style={{ fontFamily: sans, fontSize: 13, color: T.muted, lineHeight: 1.5 }}>{s.label}</div>
</div>
))}
</div>
</section>
);
}
// ── FINAL CTA ─────────────────────────────────────────────────────────────────
function FinalCta({ onCta }) {
return (
<section style={{ maxWidth: 680, margin: "0 auto", padding: "88px 48px", textAlign: "center" }}>
<h2 style={{ fontFamily: serif, fontSize: 44, fontWeight: 700, color: T.ink, letterSpacing: "-0.03em", lineHeight: 1.1, marginBottom: 20 }}>
Your idea deserves to exist.
</h2>
<p style={{ fontFamily: sans, fontSize: 16, color: T.mid, lineHeight: 1.75, marginBottom: 36 }}>
Don't let the backend be the reason it doesn't. Start today free, no code, no credit card.
</p>
<button onClick={onCta} style={{
display: "inline-block", background: T.ink, color: T.paper,
fontFamily: sans, fontSize: 15, fontWeight: 600,
padding: "15px 40px", border: "none", borderRadius: 10,
cursor: "pointer", marginBottom: 16,
}}>
Build my product free
</button>
<div style={{ fontFamily: sans, fontSize: 12.5, color: T.stone }}>
Joins 280+ non-technical founders already live
</div>
</section>
);
}
// ── FOOTER ────────────────────────────────────────────────────────────────────
function Footer() {
const links = ["Product", "Pricing", "Stories", "Blog", "Privacy", "Terms"];
return (
<footer style={{ borderTop: `1px solid ${T.border}`, padding: "32px 48px", display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<span style={{ fontFamily: serif, fontSize: 16, fontWeight: 700, color: T.ink }}>vibn</span>
<div style={{ display: "flex", gap: 28 }}>
{links.map(l => (
<span key={l} style={{ fontFamily: sans, fontSize: 13, color: T.stone, cursor: "pointer" }}>{l}</span>
))}
</div>
<span style={{ fontFamily: sans, fontSize: 12.5, color: T.stone }}>© 2026 vibn</span>
</footer>
);
}
// ── ROOT EXPORT ───────────────────────────────────────────────────────────────
export default function Website({ onGetStarted, onLogin }) {
return (
<div style={{ fontFamily: sans, background: T.paper, color: T.ink, minHeight: "100vh" }}>
<style>{`
@import url('https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,600;0,700;1,400;1,600&family=Inter:wght@400;500;600&display=swap');
* { box-sizing: border-box; margin: 0; padding: 0; }
button { font-family: inherit; }
`}</style>
<Nav onGetStarted={onGetStarted} onLogin={onLogin} />
<Hero onCta={onGetStarted} />
<QuoteBand />
<HowItWorks />
<StatsBar />
<EmpathySection />
<FinalCta onCta={onGetStarted} />
<Footer />
</div>
);
}

View File

@@ -0,0 +1,14 @@
* { box-sizing: border-box; }
body { margin: 0; font-family: system-ui, -apple-system, sans-serif; }
@keyframes vpulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
@keyframes vspin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes slideup {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}

View File

@@ -0,0 +1,10 @@
import React from 'react'
import './index.css'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

View File

@@ -0,0 +1,6 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
})