"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 (
);
}
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 (