1114 lines
28 KiB
TypeScript
1114 lines
28 KiB
TypeScript
import React, {
|
|
useState,
|
|
useEffect,
|
|
useRef,
|
|
useMemo,
|
|
useCallback,
|
|
} from "react";
|
|
import {
|
|
WizardTop,
|
|
WizardBody,
|
|
WizardQ,
|
|
WizardFooter,
|
|
LANE_LABELS,
|
|
ChipGroup,
|
|
PresetGroup,
|
|
Field,
|
|
} from "./onboarding-primitives";
|
|
// Entrepreneur path — 4 steps. Each step is a focused question.
|
|
|
|
const ENTREP_TOTAL = 4;
|
|
const ENTREP_STEP_NAMES = ["Type", "Style", "Idea", "Look"];
|
|
|
|
const IDEA_PROMPTS = [
|
|
"A community for indie game devs to swap playtesters, with weekly demo nights",
|
|
"An AI tool that turns my handwritten recipe notes into a clean cookbook for my family",
|
|
"A waitlist + scheduler for my pottery studio — small classes, six people max",
|
|
"A subscription box service for cold-brew enthusiasts, with monthly tasting cards",
|
|
"A simple tool that turns my Strava data into framed art prints I can sell",
|
|
];
|
|
|
|
export function EntrepIdea({ value, onChange }) {
|
|
const [phIdx, setPhIdx] = React.useState(0);
|
|
const [phChars, setPhChars] = React.useState(0);
|
|
const [deleting, setDeleting] = React.useState(false);
|
|
|
|
React.useEffect(() => {
|
|
if (value.length > 0) return undefined;
|
|
const full = IDEA_PROMPTS[phIdx];
|
|
const speed = deleting ? 18 : 38;
|
|
const t = setTimeout(() => {
|
|
if (!deleting) {
|
|
if (phChars < full.length) setPhChars(phChars + 1);
|
|
else setTimeout(() => setDeleting(true), 1500);
|
|
} else {
|
|
if (phChars > 0) setPhChars(phChars - 1);
|
|
else {
|
|
setDeleting(false);
|
|
setPhIdx((phIdx + 1) % IDEA_PROMPTS.length);
|
|
}
|
|
}
|
|
}, speed);
|
|
return () => clearTimeout(t);
|
|
}, [value, phIdx, phChars, deleting]);
|
|
|
|
return (
|
|
<>
|
|
<WizardQ
|
|
title="What are you building?"
|
|
sub="Don't worry if it's not crisp yet — just dump your thoughts. Talk like you would to a friend."
|
|
/>
|
|
<div style={{ position: "relative" }}>
|
|
<textarea
|
|
className="wiz-input"
|
|
style={{ minHeight: 200, fontSize: 15 }}
|
|
value={value}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
autoFocus
|
|
aria-label="Describe your idea"
|
|
/>
|
|
{value.length === 0 && (
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
top: 12,
|
|
left: 14,
|
|
right: 14,
|
|
pointerEvents: "none",
|
|
color: "var(--fg-faint)",
|
|
font: "14.5px/1.5 var(--font-sans)",
|
|
}}
|
|
>
|
|
{IDEA_PROMPTS[phIdx].slice(0, phChars)}
|
|
<span
|
|
style={{
|
|
display: "inline-block",
|
|
width: 7,
|
|
height: 14,
|
|
verticalAlign: "-2px",
|
|
background: "var(--accent)",
|
|
marginLeft: 1,
|
|
animation: "blink 1s steps(2) infinite",
|
|
boxShadow: "0 0 10px var(--accent-glow)",
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div
|
|
className="mono"
|
|
style={{
|
|
fontSize: 11,
|
|
color: "var(--fg-faint)",
|
|
letterSpacing: "0.06em",
|
|
marginTop: -16,
|
|
}}
|
|
>
|
|
{value.length} chars · be specific where it matters
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
const ARCHETYPES = [
|
|
{
|
|
id: "saas",
|
|
label: "Web App / SaaS",
|
|
desc: "Dashboards, tools, interactive portals",
|
|
},
|
|
{
|
|
id: "marketplace",
|
|
label: "Marketplace",
|
|
desc: "Directories, bookings, listings",
|
|
},
|
|
{
|
|
id: "marketing",
|
|
label: "Marketing Site",
|
|
desc: "Portfolios, lead capture, landing pages",
|
|
},
|
|
{
|
|
id: "ecommerce",
|
|
label: "Online Store",
|
|
desc: "Carts, checkouts, selling physical/digital goods",
|
|
},
|
|
{
|
|
id: "mobile",
|
|
label: "Mobile App",
|
|
desc: "iOS and Android mobile applications",
|
|
},
|
|
{
|
|
id: "blog",
|
|
label: "Blog / Publication",
|
|
desc: "Newsletters, articles, content hubs",
|
|
},
|
|
{
|
|
id: "not_sure",
|
|
label: "I'm not sure",
|
|
desc: "Let Vibn help you decide based on your description",
|
|
fullWidth: true,
|
|
},
|
|
];
|
|
|
|
function EntrepType({ value, onChange }) {
|
|
return (
|
|
<>
|
|
<WizardQ
|
|
title="What kind of product is it?"
|
|
sub="Helps Vibn set up the right database, integrations, and starting code."
|
|
/>
|
|
<PresetGroup
|
|
options={ARCHETYPES.map((a) => ({
|
|
id: a.id,
|
|
label: a.label,
|
|
desc: a.desc,
|
|
icon: undefined,
|
|
fullWidth: a.fullWidth,
|
|
}))}
|
|
value={value}
|
|
onChange={onChange}
|
|
columns={2}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function LayoutPreview({ styleId }: { styleId: string }) {
|
|
// Styles are 100% width, 104px tall absolute layouts resembling the wireframes.
|
|
const isDark = styleId !== "minimal" && styleId !== "swiss";
|
|
const accentColor = "var(--accent)";
|
|
|
|
const wireLine = (w: string | number, opacity = 0.25) => (
|
|
<div
|
|
style={{
|
|
height: 4,
|
|
width: w,
|
|
borderRadius: 2,
|
|
background: isDark ? "rgba(255,255,255,1)" : "rgba(0,0,0,1)",
|
|
opacity,
|
|
}}
|
|
/>
|
|
);
|
|
|
|
let content;
|
|
if (styleId === "sidebar") {
|
|
// Left-side menu column + main dashboard area
|
|
content = (
|
|
<div style={{ display: "flex", width: "100%", height: "100%" }}>
|
|
{/* Sidebar */}
|
|
<div
|
|
style={{
|
|
width: 28,
|
|
borderRight: "1px solid var(--hairline)",
|
|
padding: 6,
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: 5,
|
|
background: "oklch(0.19 0.009 60 / 0.3)",
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
width: 12,
|
|
height: 12,
|
|
borderRadius: "50%",
|
|
background: accentColor,
|
|
opacity: 0.7,
|
|
}}
|
|
/>
|
|
{wireLine(16, 0.4)}
|
|
{wireLine(12, 0.25)}
|
|
{wireLine(14, 0.25)}
|
|
</div>
|
|
{/* Main Dashboard */}
|
|
<div
|
|
style={{
|
|
flex: 1,
|
|
padding: 8,
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: 6,
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
justifyContent: "space-between",
|
|
alignItems: "center",
|
|
}}
|
|
>
|
|
{wireLine(24, 0.5)}
|
|
<div
|
|
style={{
|
|
width: 10,
|
|
height: 10,
|
|
borderRadius: "50%",
|
|
background: "rgba(255,255,255,0.15)",
|
|
}}
|
|
/>
|
|
</div>
|
|
<div style={{ display: "flex", gap: 6, flex: 1 }}>
|
|
<div
|
|
style={{
|
|
flex: 1,
|
|
border: "1px solid var(--hairline)",
|
|
borderRadius: 6,
|
|
padding: 6,
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: 4,
|
|
}}
|
|
>
|
|
{wireLine("100%", 0.3)}
|
|
{wireLine("60%", 0.15)}
|
|
</div>
|
|
<div
|
|
style={{
|
|
width: 32,
|
|
border: "1px solid var(--hairline)",
|
|
borderRadius: 6,
|
|
background: "rgba(255,255,255,0.03)",
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
} else if (styleId === "topbar") {
|
|
// Top horizontal header bar + multi-column page
|
|
content = (
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
width: "100%",
|
|
height: "100%",
|
|
}}
|
|
>
|
|
{/* Topbar */}
|
|
<div
|
|
style={{
|
|
height: 20,
|
|
borderBottom: "1px solid var(--hairline)",
|
|
padding: "0 8px",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
background: "oklch(0.19 0.009 60 / 0.3)",
|
|
}}
|
|
>
|
|
<div style={{ display: "flex", gap: 6 }}>
|
|
<div
|
|
style={{
|
|
width: 8,
|
|
height: 8,
|
|
borderRadius: "50%",
|
|
background: accentColor,
|
|
opacity: 0.7,
|
|
}}
|
|
/>
|
|
{wireLine(12, 0.3)}
|
|
{wireLine(12, 0.3)}
|
|
</div>
|
|
{/* Miniature ⌘K Bar */}
|
|
<div
|
|
style={{
|
|
width: 44,
|
|
height: 10,
|
|
borderRadius: 5,
|
|
border: "1px solid var(--hairline)",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
paddingLeft: 4,
|
|
}}
|
|
>
|
|
{wireLine(8, 0.15)}
|
|
</div>
|
|
<div
|
|
style={{
|
|
width: 8,
|
|
height: 8,
|
|
borderRadius: "50%",
|
|
background: "rgba(255,255,255,0.15)",
|
|
}}
|
|
/>
|
|
</div>
|
|
{/* Main Grid */}
|
|
<div
|
|
style={{
|
|
flex: 1,
|
|
padding: 8,
|
|
display: "grid",
|
|
gridTemplateColumns: "repeat(3, 1fr)",
|
|
gap: 6,
|
|
}}
|
|
>
|
|
{[1, 2, 3].map((i) => (
|
|
<div
|
|
key={i}
|
|
style={{
|
|
border: "1px solid var(--hairline)",
|
|
borderRadius: 6,
|
|
padding: 6,
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: 4,
|
|
}}
|
|
>
|
|
{wireLine("50%", 0.3)}
|
|
{wireLine("100%", 0.15)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
} else if (styleId === "rail") {
|
|
// Narrow vertical rail + content
|
|
content = (
|
|
<div style={{ display: "flex", width: "100%", height: "100%" }}>
|
|
{/* Rail */}
|
|
<div
|
|
style={{
|
|
width: 18,
|
|
borderRight: "1px solid var(--hairline)",
|
|
padding: "6px 0",
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
alignItems: "center",
|
|
gap: 6,
|
|
background: "oklch(0.19 0.009 60 / 0.3)",
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
width: 10,
|
|
height: 10,
|
|
borderRadius: 3,
|
|
background: accentColor,
|
|
opacity: 0.7,
|
|
}}
|
|
/>
|
|
{[1, 2, 3].map((i) => (
|
|
<div
|
|
key={i}
|
|
style={{
|
|
width: 8,
|
|
height: 8,
|
|
borderRadius: 2,
|
|
background: "rgba(255,255,255,0.15)",
|
|
}}
|
|
/>
|
|
))}
|
|
</div>
|
|
{/* Content */}
|
|
<div
|
|
style={{
|
|
flex: 1,
|
|
padding: 8,
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: 6,
|
|
}}
|
|
>
|
|
{wireLine(36, 0.4)}
|
|
<div
|
|
style={{
|
|
flex: 1,
|
|
border: "1px solid var(--hairline)",
|
|
borderRadius: 6,
|
|
padding: 8,
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: 5,
|
|
}}
|
|
>
|
|
<div style={{ display: "flex", gap: 6 }}>
|
|
<div
|
|
style={{
|
|
width: 12,
|
|
height: 12,
|
|
borderRadius: 3,
|
|
background: "rgba(255,255,255,0.1)",
|
|
}}
|
|
/>
|
|
<div
|
|
style={{
|
|
flex: 1,
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: 4,
|
|
paddingTop: 2,
|
|
}}
|
|
>
|
|
{wireLine("40%", 0.3)}
|
|
{wireLine("20%", 0.15)}
|
|
</div>
|
|
</div>
|
|
{wireLine("100%", 0.15)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
} else if (styleId === "flux") {
|
|
// Glass aurora: purple background with a floating card
|
|
content = (
|
|
<div
|
|
style={{
|
|
width: "100%",
|
|
height: "100%",
|
|
position: "relative",
|
|
display: "grid",
|
|
placeItems: "center",
|
|
background: "#0c0a1a",
|
|
}}
|
|
>
|
|
{/* Aurora Glow */}
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
width: 80,
|
|
height: 80,
|
|
borderRadius: "50%",
|
|
background:
|
|
"radial-gradient(circle, oklch(0.6 0.18 300) 0%, transparent 70%)",
|
|
filter: "blur(14px)",
|
|
opacity: 0.7,
|
|
top: "10%",
|
|
left: "30%",
|
|
}}
|
|
/>
|
|
{/* Frosted Card */}
|
|
<div
|
|
style={{
|
|
width: "80%",
|
|
height: "70%",
|
|
borderRadius: 8,
|
|
border: "1px solid rgba(255,255,255,0.14)",
|
|
background: "rgba(255,255,255,0.04)",
|
|
backdropFilter: "blur(12px)",
|
|
padding: 8,
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: 5,
|
|
boxShadow: "0 8px 32px rgba(0,0,0,0.3)",
|
|
}}
|
|
>
|
|
<div style={{ display: "flex", gap: 4 }}>
|
|
<div
|
|
style={{
|
|
width: 8,
|
|
height: 8,
|
|
borderRadius: "50%",
|
|
background: accentColor,
|
|
opacity: 0.8,
|
|
}}
|
|
/>
|
|
{wireLine(18, 0.5)}
|
|
</div>
|
|
{wireLine("80%", 0.3)}
|
|
{wireLine("40%", 0.15)}
|
|
</div>
|
|
</div>
|
|
);
|
|
} else if (styleId === "minimal") {
|
|
// Classic Minimal: parchment background, gridded rule lines
|
|
content = (
|
|
<div
|
|
style={{
|
|
width: "100%",
|
|
height: "100%",
|
|
background: "#f5f4ef",
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
height: 18,
|
|
borderBottom: "1px solid #d4d0c8",
|
|
padding: "0 8px",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
}}
|
|
>
|
|
{wireLine(24, 0.6)}
|
|
{wireLine(12, 0.4)}
|
|
</div>
|
|
<div
|
|
style={{
|
|
flex: 1,
|
|
padding: 8,
|
|
display: "grid",
|
|
gridTemplateColumns: "1fr 1fr",
|
|
gap: 8,
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
borderRight: "1px dashed #d4d0c8",
|
|
paddingRight: 4,
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: 4,
|
|
}}
|
|
>
|
|
{wireLine("90%", 0.5)}
|
|
{wireLine("50%", 0.25)}
|
|
</div>
|
|
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
|
{wireLine("100%", 0.5)}
|
|
{wireLine("70%", 0.25)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
} else if (styleId === "bento") {
|
|
// Dark bento grid
|
|
content = (
|
|
<div
|
|
style={{
|
|
width: "100%",
|
|
height: "100%",
|
|
padding: 8,
|
|
display: "grid",
|
|
gridTemplateColumns: "1.2fr 1fr",
|
|
gridTemplateRows: "1fr 1fr",
|
|
gap: 5,
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
gridRow: "span 2",
|
|
border: "1px solid var(--hairline)",
|
|
borderRadius: 6,
|
|
padding: 6,
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: 4,
|
|
}}
|
|
>
|
|
{wireLine("50%", 0.4)}
|
|
<div
|
|
style={{
|
|
flex: 1,
|
|
background: "rgba(255,255,255,0.03)",
|
|
borderRadius: 4,
|
|
display: "grid",
|
|
placeItems: "center",
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
width: 14,
|
|
height: 14,
|
|
borderRadius: "50%",
|
|
border: "1px solid var(--accent)",
|
|
opacity: 0.7,
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div
|
|
style={{
|
|
border: "1px solid var(--hairline)",
|
|
borderRadius: 6,
|
|
padding: 6,
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: 3,
|
|
}}
|
|
>
|
|
{wireLine("80%", 0.3)}
|
|
{wireLine("40%", 0.15)}
|
|
</div>
|
|
<div
|
|
style={{
|
|
border: "1px solid var(--hairline)",
|
|
borderRadius: 6,
|
|
padding: 6,
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: 3,
|
|
}}
|
|
>
|
|
{wireLine("100%", 0.3)}
|
|
</div>
|
|
</div>
|
|
);
|
|
} else if (styleId === "swiss") {
|
|
// Editorial swiss: White background, bold geometric layouts
|
|
content = (
|
|
<div
|
|
style={{
|
|
width: "100%",
|
|
height: "100%",
|
|
background: "#ffffff",
|
|
padding: 10,
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: 6,
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
justifyContent: "space-between",
|
|
alignItems: "baseline",
|
|
}}
|
|
>
|
|
<span
|
|
style={{
|
|
fontSize: 13,
|
|
fontWeight: 900,
|
|
color: "#000",
|
|
fontFamily: "sans-serif",
|
|
letterSpacing: "-0.04em",
|
|
lineHeight: 1,
|
|
}}
|
|
>
|
|
VIBN.
|
|
</span>
|
|
{wireLine(16, 0.6)}
|
|
</div>
|
|
<div style={{ height: 1, background: "#000000", opacity: 0.15 }} />
|
|
<div style={{ display: "flex", gap: 10, flex: 1 }}>
|
|
<div
|
|
style={{
|
|
flex: 1.2,
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: 4,
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
height: 8,
|
|
width: "100%",
|
|
background: "#000",
|
|
opacity: 0.85,
|
|
}}
|
|
/>
|
|
<div
|
|
style={{
|
|
height: 8,
|
|
width: "70%",
|
|
background: "#000",
|
|
opacity: 0.85,
|
|
}}
|
|
/>
|
|
</div>
|
|
<div
|
|
style={{
|
|
flex: 1,
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: 4,
|
|
paddingTop: 2,
|
|
}}
|
|
>
|
|
{wireLine("100%", 0.4)}
|
|
{wireLine("100%", 0.4)}
|
|
{wireLine("60%", 0.2)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
} else if (styleId === "brutalist") {
|
|
// Neo-brutalist yellow card, thick borders, heavy offsets
|
|
content = (
|
|
<div
|
|
style={{
|
|
width: "100%",
|
|
height: "100%",
|
|
background: "#f0eee9",
|
|
display: "grid",
|
|
placeItems: "center",
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
width: "75%",
|
|
height: "70%",
|
|
background: "#fcf05a",
|
|
border: "2px solid #000",
|
|
borderRadius: 4,
|
|
boxShadow: "3px 3px 0 #000",
|
|
padding: 6,
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: 5,
|
|
}}
|
|
>
|
|
<div style={{ height: 8, width: "100%", background: "#000" }} />
|
|
<div style={{ display: "flex", gap: 4, alignItems: "center" }}>
|
|
<div
|
|
style={{
|
|
width: 12,
|
|
height: 12,
|
|
borderRadius: "50%",
|
|
background: "#ff5bf5",
|
|
border: "1.5px solid #000",
|
|
}}
|
|
/>
|
|
{wireLine(24, 0.85)}
|
|
</div>
|
|
{wireLine("60%", 0.6)}
|
|
</div>
|
|
</div>
|
|
);
|
|
} else {
|
|
// I'm not sure / Undecided
|
|
content = (
|
|
<div
|
|
style={{
|
|
width: "100%",
|
|
height: "100%",
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
gap: 6,
|
|
background:
|
|
"radial-gradient(circle at center, rgba(255,255,255,0.03) 0%, transparent 60%)",
|
|
}}
|
|
>
|
|
<svg
|
|
width="20"
|
|
height="20"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="var(--accent)"
|
|
strokeWidth="1.8"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
style={{
|
|
opacity: 0.75,
|
|
filter: "drop-shadow(0 0 8px var(--accent-glow))",
|
|
}}
|
|
>
|
|
<circle cx="12" cy="12" r="10" />
|
|
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3M12 17h.01" />
|
|
</svg>
|
|
<span
|
|
style={{
|
|
fontSize: 10,
|
|
color: "var(--fg-faint)",
|
|
textTransform: "uppercase",
|
|
letterSpacing: "0.12em",
|
|
fontFamily: "var(--font-mono)",
|
|
}}
|
|
>
|
|
Automatic
|
|
</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
width: "100%",
|
|
height: "100%",
|
|
position: "relative",
|
|
background: isDark ? "oklch(0.155 0.008 60)" : "transparent",
|
|
}}
|
|
>
|
|
{content}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const SAAS_STYLES = [
|
|
{
|
|
id: "sidebar",
|
|
label: "Vertical Sidebar",
|
|
desc: "Left-side collapsible menu, data-dense. Ideal for CRM/dashboards.",
|
|
illustration: <LayoutPreview styleId="sidebar" />,
|
|
},
|
|
{
|
|
id: "topbar",
|
|
label: "Top Horizontal + ⌘K",
|
|
desc: "Spacious top navigation with global command search bar.",
|
|
illustration: <LayoutPreview styleId="topbar" />,
|
|
},
|
|
{
|
|
id: "rail",
|
|
label: "Slim Icon Rail",
|
|
desc: "Minimalist vertical narrow icon bar, maximizes workspace area.",
|
|
illustration: <LayoutPreview styleId="rail" />,
|
|
},
|
|
];
|
|
|
|
const MARKETPLACE_STYLES = [
|
|
{
|
|
id: "flux",
|
|
label: "Dark Glass / Flux",
|
|
desc: "Modern dark-glass panels with glowing fuchsia aurora backdrops.",
|
|
illustration: <LayoutPreview styleId="flux" />,
|
|
},
|
|
{
|
|
id: "minimal",
|
|
label: "Classic Minimal",
|
|
desc: "Warm parchment neutrals, high-contrast typography and clean grids.",
|
|
illustration: <LayoutPreview styleId="minimal" />,
|
|
},
|
|
];
|
|
|
|
const GENERAL_STYLES = [
|
|
{
|
|
id: "bento",
|
|
label: "Dark Bento",
|
|
desc: "Modern dark UI, bento-box card clusters.",
|
|
illustration: <LayoutPreview styleId="bento" />,
|
|
},
|
|
{
|
|
id: "swiss",
|
|
label: "Editorial Swiss",
|
|
desc: "Type-led, gridded, lots of white space — clean and academic.",
|
|
illustration: <LayoutPreview styleId="swiss" />,
|
|
},
|
|
{
|
|
id: "brutalist",
|
|
label: "Neo-Brutalist",
|
|
desc: "Bold offsets, thick hand-drawn borders, highly tactile and organic.",
|
|
illustration: <LayoutPreview styleId="brutalist" />,
|
|
},
|
|
];
|
|
|
|
function EntrepStyle({ productType, value, onChange }) {
|
|
// Dynamically tailor the styles array based on what they picked on Page 2
|
|
const isSaas = productType === "saas";
|
|
const isMarketplace = productType === "marketplace";
|
|
|
|
const styles = isSaas
|
|
? SAAS_STYLES
|
|
: isMarketplace
|
|
? MARKETPLACE_STYLES
|
|
: GENERAL_STYLES;
|
|
|
|
return (
|
|
<>
|
|
<WizardQ
|
|
title="Choose a starting design style"
|
|
sub={
|
|
isSaas
|
|
? "Select the navigation layout that fits your app's density."
|
|
: isMarketplace
|
|
? "Select the design aesthetic for your marketplace templates."
|
|
: "Select the design layout for your custom pages."
|
|
}
|
|
/>
|
|
<PresetGroup
|
|
options={styles.map((s) => ({
|
|
id: s.id,
|
|
label: s.label,
|
|
desc: s.desc,
|
|
icon: undefined,
|
|
illustration: s.illustration,
|
|
}))}
|
|
value={value}
|
|
onChange={onChange}
|
|
columns={styles.length === 3 ? 1 : 2}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
const VIBES = [
|
|
{
|
|
id: "warm",
|
|
name: "Warm coral",
|
|
swatch: "linear-gradient(135deg, #E27855, #B33B2A)",
|
|
desc: "Confident, hand-built, warm.",
|
|
},
|
|
{
|
|
id: "ink",
|
|
name: "Ink & paper",
|
|
swatch: "linear-gradient(135deg, #1d1d1d, #4a4a4a)",
|
|
desc: "Editorial, serif, quiet.",
|
|
},
|
|
{
|
|
id: "sage",
|
|
name: "Sage matte",
|
|
swatch: "linear-gradient(135deg, #7BA890, #3F6B57)",
|
|
desc: "Calm, modern, slightly herbal.",
|
|
},
|
|
{
|
|
id: "neon",
|
|
name: "Neon arcade",
|
|
swatch: "linear-gradient(135deg, #5B6CFF, #FF3DDB)",
|
|
desc: "Loud, fun, late-night.",
|
|
},
|
|
{
|
|
id: "cream",
|
|
name: "Cream linen",
|
|
swatch: "linear-gradient(135deg, #F2E7D5, #C9A977)",
|
|
desc: "Cozy and beige.",
|
|
},
|
|
{
|
|
id: "later",
|
|
name: "Decide later",
|
|
swatch:
|
|
"repeating-linear-gradient(45deg, oklch(0.30 0.010 60), oklch(0.30 0.010 60) 6px, oklch(0.22 0.010 60) 6px, oklch(0.22 0.010 60) 12px)",
|
|
desc: "Vibn picks one that fits.",
|
|
},
|
|
];
|
|
|
|
function EntrepVibe({ value, onChange }) {
|
|
return (
|
|
<>
|
|
<WizardQ
|
|
title="Pick a starting vibe."
|
|
sub="Every color and font is a tweak away once the site is live."
|
|
/>
|
|
<div
|
|
style={{
|
|
display: "grid",
|
|
gridTemplateColumns: "repeat(3, 1fr)",
|
|
gap: 10,
|
|
}}
|
|
>
|
|
{VIBES.map((v) => {
|
|
const active = value === v.id;
|
|
return (
|
|
<button
|
|
key={v.id}
|
|
type="button"
|
|
onClick={() => onChange(v.id)}
|
|
style={{
|
|
padding: "10px 10px 10px",
|
|
borderRadius: 11,
|
|
border: `1px solid ${active ? "var(--accent)" : "var(--hairline)"}`,
|
|
background: active
|
|
? "oklch(0.20 0.04 35 / 0.4)"
|
|
: "oklch(0.18 0.009 60 / 0.6)",
|
|
boxShadow: active
|
|
? "0 0 0 3px oklch(0.74 0.175 35 / 0.1)"
|
|
: "none",
|
|
textAlign: "left",
|
|
color: "var(--fg)",
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: 8,
|
|
transition: "border-color .15s, background .15s",
|
|
}}
|
|
>
|
|
<span
|
|
style={{
|
|
height: 52,
|
|
borderRadius: 7,
|
|
background: v.swatch,
|
|
border: "1px solid oklch(1 0 0 / 0.08)",
|
|
boxShadow: "inset 0 1px 0 oklch(1 0 0 / 0.18)",
|
|
}}
|
|
/>
|
|
<span
|
|
style={{
|
|
fontSize: 13,
|
|
fontWeight: 500,
|
|
letterSpacing: "-0.005em",
|
|
}}
|
|
>
|
|
{v.name}
|
|
</span>
|
|
<span
|
|
style={{
|
|
fontSize: 11.5,
|
|
color: "var(--fg-mute)",
|
|
lineHeight: 1.4,
|
|
}}
|
|
>
|
|
{v.desc}
|
|
</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
// ── Path wrapper ───────────────────────────────────────────────────────────
|
|
export function EntrepreneurPath({
|
|
data,
|
|
onUpdate,
|
|
onBack,
|
|
onClose,
|
|
onComplete,
|
|
onJumpToStep,
|
|
step,
|
|
}) {
|
|
const next = () => {
|
|
if (step < ENTREP_TOTAL - 1) onJumpToStep(step + 1);
|
|
else onComplete();
|
|
};
|
|
const back = () => {
|
|
if (step === 0) onBack();
|
|
else onJumpToStep(step - 1);
|
|
};
|
|
|
|
let body,
|
|
canNext,
|
|
onSkip = null;
|
|
if (step === 0) {
|
|
body = (
|
|
<EntrepType
|
|
value={data.productType || ""}
|
|
onChange={(v) => onUpdate({ productType: v })}
|
|
/>
|
|
);
|
|
canNext = !!data.productType;
|
|
} else if (step === 1) {
|
|
body = (
|
|
<EntrepStyle
|
|
productType={data.productType}
|
|
value={data.designStyle || ""}
|
|
onChange={(v) => onUpdate({ designStyle: v })}
|
|
/>
|
|
);
|
|
canNext = !!data.designStyle;
|
|
} else if (step === 2) {
|
|
body = (
|
|
<EntrepIdea
|
|
value={data.idea || ""}
|
|
onChange={(v) => onUpdate({ idea: v })}
|
|
/>
|
|
);
|
|
canNext = (data.idea || "").trim().length >= 8;
|
|
} else {
|
|
body = (
|
|
<EntrepVibe value={data.vibe} onChange={(v) => onUpdate({ vibe: v })} />
|
|
);
|
|
canNext = !!data.vibe;
|
|
onSkip = () => {
|
|
onUpdate({ vibe: "later" });
|
|
next();
|
|
};
|
|
}
|
|
|
|
// 5 total: fork(1) + 4 path steps
|
|
return (
|
|
<>
|
|
<WizardTop
|
|
onBack={back}
|
|
onClose={onClose}
|
|
lane={LANE_LABELS.entrepreneur}
|
|
stepText={ENTREP_STEP_NAMES[step]}
|
|
current={step + 2}
|
|
total={5}
|
|
/>
|
|
<WizardBody width={step === 2 || step === 3 ? "wide" : null}>
|
|
{body}
|
|
<WizardFooter
|
|
onNext={next}
|
|
canNext={canNext}
|
|
nextLabel={step === ENTREP_TOTAL - 1 ? "Build →" : "Continue"}
|
|
hint={canNext ? "⌘↵" : null}
|
|
onSkip={onSkip}
|
|
skipLabel={step === 0 ? "I'm not sure" : "Pick for me"}
|
|
/>
|
|
</WizardBody>
|
|
</>
|
|
);
|
|
}
|