"use client"; import React, { ReactNode, CSSProperties, useRef, useState } from "react"; import { JM } from "./modal-theme"; import { STARTER_KITS } from "@/lib/design-kits/registry"; import type { StarterKitDefinition } from "@/lib/design-kits/types"; import { UI_FOUNDATION_LABELS } from "@/lib/design-kits/types"; import { PRODUCT_CATEGORIES, kitsOrderedForCategory, type ProductCategoryId, } from "@/lib/design-kits/product-categories"; import { DesignKitPreviewPane } from "./design-kit-preview-pane"; export interface SetupProps { workspace: string; onClose: () => void; onBack: () => void; } export function FieldLabel({ children }: { children: ReactNode }) { return ( ); } /** Audience picker — drives default infra (auth, payments, email, domain). * - "team" → internal users (your team / employees). SSO-style auth, * no payments by default, simple roles. * - "customers" → external users (the public). Public sign-up, payments * on by default, transactional email, custom domain. * Either choice can be changed later from the Infrastructure tab. */ export type Audience = "team" | "customers"; export function ProductCategorySelector({ value, onChange, }: { value: ProductCategoryId; onChange: (v: ProductCategoryId) => void; }) { return (
What are you building?

Choose a product shape — we'll suggest matching design starters on the next step.

{PRODUCT_CATEGORIES.map((c) => { const sel = c.id === value; return ( ); })}
); } /** Split layout: starter kits (filtered by product category) + live preview pane. */ export function DesignKitSplitSelector({ productCategory, value, onChange, }: { productCategory: ProductCategoryId; value: string; onChange: (kitId: string) => void; }) { const order = kitsOrderedForCategory(productCategory); const kits = order .map((id) => STARTER_KITS.find((k) => k.id === id)) .filter(Boolean) as StarterKitDefinition[]; const selected = kits.find((k) => k.id === value) ?? kits[0]; return (
Design system

Pick a starter look — preview updates as you click. You can customize tokens anytime in the Design tab.

{kits.map((k) => { const sel = value === k.id; const dot = k.defaults.accentHex ?? "#6366f1"; return ( ); })}
{selected ? : null}
); } /** Starter design system picker — persisted as `fs_projects.data.designKit` on create. */ export function DesignKitSelector({ value, onChange, }: { value: string; onChange: (kitId: string) => void; }) { return (
Design system

Pick a starter look for your UI. Vibn uses this when building and when you open the Design tab — you can customize tokens anytime.

{STARTER_KITS.map((k) => { const sel = value === k.id; const dot = k.defaults.accentHex ?? "#6366f1"; return ( ); })}
); } export function AudienceSelector({ value, onChange, }: { value: Audience; onChange: (v: Audience) => void; }) { const cardBase: CSSProperties = { flex: "0 0 auto", width: 148, border: `1px solid ${JM.border}`, borderRadius: 8, padding: "8px 10px", cursor: "pointer", textAlign: "center" as const, background: JM.inputBg, transition: "all 0.15s", fontFamily: JM.fontSans, minHeight: 0, }; const row = (key: Audience, emoji: string, title: string, sub: string) => { const sel = value === key; return ( ); }; return (
Who will use this?
{row("team", "🏢", "My team", "Internal tool")} {row("customers", "🌍", "Customers", "Public product")}

We'll set up the right defaults (sign-up, payments, email). You can change this later.

); } export type SeedDocumentPayload = { fileName: string; kind: "markdown" | "pdf"; text?: string; base64?: string; }; const MAX_MARKDOWN_CHARS = 200_000; const MAX_PDF_BYTES = 4 * 1024 * 1024; function readFileAsText(file: File): Promise { return new Promise((resolve, reject) => { const r = new FileReader(); r.onload = () => resolve(typeof r.result === "string" ? r.result : ""); r.onerror = () => reject(r.error ?? new Error("read failed")); r.readAsText(file); }); } function readFileAsDataUrl(file: File): Promise { return new Promise((resolve, reject) => { const r = new FileReader(); r.onload = () => resolve(typeof r.result === "string" ? r.result : ""); r.onerror = () => reject(r.error ?? new Error("read failed")); r.readAsDataURL(file); }); } /** Optional Markdown / PDF seed for the build wizard last step. */ export function SeedDocumentUpload({ value, onChange, }: { value: SeedDocumentPayload | null; onChange: (v: SeedDocumentPayload | null) => void; }) { const inputRef = useRef(null); const [pickErr, setPickErr] = useState(null); const handlePick = () => { setPickErr(null); inputRef.current?.click(); }; const handleChange = async (e: React.ChangeEvent) => { const list = e.target.files; e.target.value = ""; const file = list?.[0]; if (!file) return; const lower = file.name.toLowerCase(); setPickErr(null); try { if (lower.endsWith(".md") || lower.endsWith(".markdown")) { let text = await readFileAsText(file); if (text.length > MAX_MARKDOWN_CHARS) text = text.slice(0, MAX_MARKDOWN_CHARS); onChange({ fileName: file.name, kind: "markdown", text }); return; } if (lower.endsWith(".pdf")) { if (file.size > MAX_PDF_BYTES) { setPickErr("PDF must be 4MB or smaller."); return; } const dataUrl = await readFileAsDataUrl(file); const comma = dataUrl.indexOf(","); const base64 = comma >= 0 ? dataUrl.slice(comma + 1) : ""; if (!base64) { setPickErr("Could not read PDF."); return; } onChange({ fileName: file.name, kind: "pdf", base64 }); return; } setPickErr("Please choose a .md, .markdown, or .pdf file."); } catch { setPickErr("Could not read that file."); } }; return (
Supporting document
{value ? ( {value.fileName} ({value.kind === "pdf" ? "PDF" : "Markdown"}) ) : ( Optional — specs, notes, or briefs (.md / .pdf, PDF max 4MB) )}
{pickErr ? (

{pickErr}

) : null}
); } export function SetupHeader({ icon, label, tagline, accent, onBack, onClose, }: { icon: string; label: string; tagline: string; accent: string; onBack: () => void; onClose: () => void; }) { return (

{icon} {label}

{tagline}

); } export function TextInput({ value, onChange, placeholder, onKeyDown, autoFocus, inputRef, }: { value: string; onChange: (v: string) => void; placeholder?: string; onKeyDown?: (e: React.KeyboardEvent) => void; autoFocus?: boolean; inputRef?: React.RefObject | React.RefObject; }) { const base: CSSProperties = { width: "100%", padding: "10px 13px", marginBottom: 16, borderRadius: 8, border: `1px solid ${JM.border}`, background: JM.inputBg, fontSize: 14, fontFamily: JM.fontSans, color: JM.ink, outline: "none", boxSizing: "border-box", }; return ( onChange(e.target.value)} onKeyDown={onKeyDown} placeholder={placeholder} autoFocus={autoFocus} style={base} onFocus={e => (e.currentTarget.style.borderColor = JM.indigo)} onBlur={e => (e.currentTarget.style.borderColor = JM.border)} /> ); } export function TextArea({ value, onChange, placeholder, rows = 5, autoFocus, }: { value: string; onChange: (v: string) => void; placeholder?: string; rows?: number; autoFocus?: boolean; }) { return (