// Auto-ported from new-site
"use client";
import React, { useState, useEffect, useRef, Fragment } from "react";
// --- tweaks-panel.jsx ---
// tweaks-panel.jsx
// Reusable Tweaks shell + form-control helpers.
//
// Owns the host protocol (listens for __activate_edit_mode / __deactivate_edit_mode,
// posts __edit_mode_available / __edit_mode_set_keys / __edit_mode_dismissed) so
// individual prototypes don't re-roll it. Ships a consistent set of controls so you
// don't hand-draw , segmented radios, steppers, etc.
//
// Usage (in an HTML file that loads React + Babel):
//
// const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
// "primaryColor": "#D97757",
// "palette": ["#D97757", "#29261b", "#f6f4ef"],
// "fontSize": 16,
// "density": "regular",
// "dark": false
// }/*EDITMODE-END*/;
//
// function App() {
// const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
// return (
//
// Hello
//
//
// setTweak('fontSize', v)} />
// setTweak('density', v)} />
//
// setTweak('primaryColor', v)} />
// setTweak('palette', v)} />
// setTweak('dark', v)} />
//
//
// );
// }
//
// ─────────────────────────────────────────────────────────────────────────────
const __TWEAKS_STYLE = `
.twk-panel{position:fixed;right:16px;bottom:16px;z-index:2147483646;width:280px;
max-height:calc(100vh - 32px);display:flex;flex-direction:column;
transform:scale(var(--dc-inv-zoom,1));transform-origin:bottom right;
background:rgba(250,249,247,.78);color:#29261b;
-webkit-backdrop-filter:blur(24px) saturate(160%);backdrop-filter:blur(24px) saturate(160%);
border:.5px solid rgba(255,255,255,.6);border-radius:14px;
box-shadow:0 1px 0 rgba(255,255,255,.5) inset,0 12px 40px rgba(0,0,0,.18);
font:11.5px/1.4 ui-sans-serif,system-ui,-apple-system,sans-serif;overflow:hidden}
.twk-hd{display:flex;align-items:center;justify-content:space-between;
padding:10px 8px 10px 14px;cursor:move;user-select:none}
.twk-hd b{font-size:12px;font-weight:600;letter-spacing:.01em}
.twk-x{appearance:none;border:0;background:transparent;color:rgba(41,38,27,.55);
width:22px;height:22px;border-radius:6px;cursor:default;font-size:13px;line-height:1}
.twk-x:hover{background:rgba(0,0,0,.06);color:#29261b}
.twk-body{padding:2px 14px 14px;display:flex;flex-direction:column;gap:10px;
overflow-y:auto;overflow-x:hidden;min-height:0;
scrollbar-width:thin;scrollbar-color:rgba(0,0,0,.15) transparent}
.twk-body::-webkit-scrollbar{width:8px}
.twk-body::-webkit-scrollbar-track{background:transparent;margin:2px}
.twk-body::-webkit-scrollbar-thumb{background:rgba(0,0,0,.15);border-radius:4px;
border:2px solid transparent;background-clip:content-box}
.twk-body::-webkit-scrollbar-thumb:hover{background:rgba(0,0,0,.25);
border:2px solid transparent;background-clip:content-box}
.twk-row{display:flex;flex-direction:column;gap:5px}
.twk-row-h{flex-direction:row;align-items:center;justify-content:space-between;gap:10px}
.twk-lbl{display:flex;justify-content:space-between;align-items:baseline;
color:rgba(41,38,27,.72)}
.twk-lbl>span:first-child{font-weight:500}
.twk-val{color:rgba(41,38,27,.5);font-variant-numeric:tabular-nums}
.twk-sect{font-size:10px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;
color:rgba(41,38,27,.45);padding:10px 0 0}
.twk-sect:first-child{padding-top:0}
.twk-field{appearance:none;box-sizing:border-box;width:100%;min-width:0;height:26px;padding:0 8px;
border:.5px solid rgba(0,0,0,.1);border-radius:7px;
background:rgba(255,255,255,.6);color:inherit;font:inherit;outline:none}
.twk-field:focus{border-color:rgba(0,0,0,.25);background:rgba(255,255,255,.85)}
select.twk-field{padding-right:22px;
background-image:url("data:image/svg+xml;utf8, ");
background-repeat:no-repeat;background-position:right 8px center}
.twk-slider{appearance:none;-webkit-appearance:none;width:100%;height:4px;margin:6px 0;
border-radius:999px;background:rgba(0,0,0,.12);outline:none}
.twk-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;
width:14px;height:14px;border-radius:50%;background:#fff;
border:.5px solid rgba(0,0,0,.12);box-shadow:0 1px 3px rgba(0,0,0,.2);cursor:default}
.twk-slider::-moz-range-thumb{width:14px;height:14px;border-radius:50%;
background:#fff;border:.5px solid rgba(0,0,0,.12);box-shadow:0 1px 3px rgba(0,0,0,.2);cursor:default}
.twk-seg{position:relative;display:flex;padding:2px;border-radius:8px;
background:rgba(0,0,0,.06);user-select:none}
.twk-seg-thumb{position:absolute;top:2px;bottom:2px;border-radius:6px;
background:rgba(255,255,255,.9);box-shadow:0 1px 2px rgba(0,0,0,.12);
transition:left .15s cubic-bezier(.3,.7,.4,1),width .15s}
.twk-seg.dragging .twk-seg-thumb{transition:none}
.twk-seg button{appearance:none;position:relative;z-index:1;flex:1;border:0;
background:transparent;color:inherit;font:inherit;font-weight:500;min-height:22px;
border-radius:6px;cursor:default;padding:4px 6px;line-height:1.2;
overflow-wrap:anywhere}
.twk-toggle{position:relative;width:32px;height:18px;border:0;border-radius:999px;
background:rgba(0,0,0,.15);transition:background .15s;cursor:default;padding:0}
.twk-toggle[data-on="1"]{background:#34c759}
.twk-toggle i{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;
background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .15s}
.twk-toggle[data-on="1"] i{transform:translateX(14px)}
.twk-num{display:flex;align-items:center;box-sizing:border-box;min-width:0;height:26px;padding:0 0 0 8px;
border:.5px solid rgba(0,0,0,.1);border-radius:7px;background:rgba(255,255,255,.6)}
.twk-num-lbl{font-weight:500;color:rgba(41,38,27,.6);cursor:ew-resize;
user-select:none;padding-right:8px}
.twk-num input{flex:1;min-width:0;height:100%;border:0;background:transparent;
font:inherit;font-variant-numeric:tabular-nums;text-align:right;padding:0 8px 0 0;
outline:none;color:inherit;-moz-appearance:textfield}
.twk-num input::-webkit-inner-spin-button,.twk-num input::-webkit-outer-spin-button{
-webkit-appearance:none;margin:0}
.twk-num-unit{padding-right:8px;color:rgba(41,38,27,.45)}
.twk-btn{appearance:none;height:26px;padding:0 12px;border:0;border-radius:7px;
background:rgba(0,0,0,.78);color:#fff;font:inherit;font-weight:500;cursor:default}
.twk-btn:hover{background:rgba(0,0,0,.88)}
.twk-btn.secondary{background:rgba(0,0,0,.06);color:inherit}
.twk-btn.secondary:hover{background:rgba(0,0,0,.1)}
.twk-swatch{appearance:none;-webkit-appearance:none;width:56px;height:22px;
border:.5px solid rgba(0,0,0,.1);border-radius:6px;padding:0;cursor:default;
background:transparent;flex-shrink:0}
.twk-swatch::-webkit-color-swatch-wrapper{padding:0}
.twk-swatch::-webkit-color-swatch{border:0;border-radius:5.5px}
.twk-swatch::-moz-color-swatch{border:0;border-radius:5.5px}
.twk-chips{display:flex;gap:6px}
.twk-chip{position:relative;appearance:none;flex:1;min-width:0;height:46px;
padding:0;border:0;border-radius:6px;overflow:hidden;cursor:default;
box-shadow:0 0 0 .5px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.06);
transition:transform .12s cubic-bezier(.3,.7,.4,1),box-shadow .12s}
.twk-chip:hover{transform:translateY(-1px);
box-shadow:0 0 0 .5px rgba(0,0,0,.18),0 4px 10px rgba(0,0,0,.12)}
.twk-chip[data-on="1"]{box-shadow:0 0 0 1.5px rgba(0,0,0,.85),
0 2px 6px rgba(0,0,0,.15)}
.twk-chip>span{position:absolute;top:0;bottom:0;right:0;width:34%;
display:flex;flex-direction:column;box-shadow:-1px 0 0 rgba(0,0,0,.1)}
.twk-chip>span>i{flex:1;box-shadow:0 -1px 0 rgba(0,0,0,.1)}
.twk-chip>span>i:first-child{box-shadow:none}
.twk-chip svg{position:absolute;top:6px;left:6px;width:13px;height:13px;
filter:drop-shadow(0 1px 1px rgba(0,0,0,.3))}
`;
// ── useTweaks ───────────────────────────────────────────────────────────────
// Single source of truth for tweak values. setTweak persists via the host
// (__edit_mode_set_keys → host rewrites the EDITMODE block on disk).
function useTweaks(defaults) {
const [values, setValues] = useState(defaults);
// Accepts either setTweak('key', value) or setTweak({ key: value, ... }) so a
// useState-style call doesn't write a "[object Object]" key into the persisted
// JSON block.
const setTweak = React.useCallback((keyOrEdits, val) => {
const edits =
typeof keyOrEdits === "object" && keyOrEdits !== null
? keyOrEdits
: { [keyOrEdits]: val };
setValues((prev) => ({ ...prev, ...edits }));
window.parent.postMessage({ type: "__edit_mode_set_keys", edits }, "*");
// Same-window signal so in-page listeners (deck-stage rail thumbnails)
// can react — the parent message only reaches the host, not peers.
window.dispatchEvent(new CustomEvent("tweakchange", { detail: edits }));
}, []);
return [values, setTweak];
}
// ── TweaksPanel ─────────────────────────────────────────────────────────────
// Floating shell. Registers the protocol listener BEFORE announcing
// availability — if the announce ran first, the host's activate could land
// before our handler exists and the toolbar toggle would silently no-op.
// The close button posts __edit_mode_dismissed so the host's toolbar toggle
// flips off in lockstep; the host echoes __deactivate_edit_mode back which
// is what actually hides the panel.
function TweaksPanel({ title = "Tweaks", noDeckControls = false, children }) {
const [open, setOpen] = useState(false);
const dragRef = useRef(null);
// Auto-inject a rail toggle when a is on the page. The
// toggle drives the deck's per-viewer _railVisible via window message;
// state is mirrored from the same localStorage key the deck reads so
// the control reflects reality across reloads. The mechanism is the
// message — authors who want custom placement can post it directly
// and pass noDeckControls to suppress this one.
const hasDeckStage = React.useMemo(
() =>
typeof document !== "undefined" && !!document.querySelector("deck-stage"),
[],
);
// deck-stage enables its rail in connectedCallback, but this panel can
// mount before that element has upgraded. The initial read catches the
// common case; the listener covers mounting first. (Older deck-stage.js
// copies still wait for the host's __omelette_rail_enabled postMessage —
// same listener handles those.)
const [railEnabled, setRailEnabled] = useState(
() => hasDeckStage && !!document.querySelector("deck-stage")?._railEnabled,
);
useEffect(() => {
if (!hasDeckStage || railEnabled) return undefined;
const onMsg = (e) => {
if (e.data && e.data.type === "__omelette_rail_enabled")
setRailEnabled(true);
};
window.addEventListener("message", onMsg);
return () => window.removeEventListener("message", onMsg);
}, [hasDeckStage, railEnabled]);
const [railVisible, setRailVisible] = useState(() => {
try {
return localStorage.getItem("deck-stage.railVisible") !== "0";
} catch (e) {
return true;
}
});
const toggleRail = (on) => {
setRailVisible(on);
window.postMessage({ type: "__deck_rail_visible", on }, "*");
};
const offsetRef = useRef({ x: 16, y: 16 });
const PAD = 16;
const clampToViewport = React.useCallback(() => {
const panel = dragRef.current;
if (!panel) return;
const w = panel.offsetWidth,
h = panel.offsetHeight;
const maxRight = Math.max(PAD, window.innerWidth - w - PAD);
const maxBottom = Math.max(PAD, window.innerHeight - h - PAD);
offsetRef.current = {
x: Math.min(maxRight, Math.max(PAD, offsetRef.current.x)),
y: Math.min(maxBottom, Math.max(PAD, offsetRef.current.y)),
};
panel.style.right = offsetRef.current.x + "px";
panel.style.bottom = offsetRef.current.y + "px";
}, []);
useEffect(() => {
if (!open) return;
clampToViewport();
if (typeof ResizeObserver === "undefined") {
window.addEventListener("resize", clampToViewport);
return () => window.removeEventListener("resize", clampToViewport);
}
const ro = new ResizeObserver(clampToViewport);
ro.observe(document.documentElement);
return () => ro.disconnect();
}, [open, clampToViewport]);
useEffect(() => {
const onMsg = (e) => {
const t = e?.data?.type;
if (t === "__activate_edit_mode") setOpen(true);
else if (t === "__deactivate_edit_mode") setOpen(false);
};
window.addEventListener("message", onMsg);
window.parent.postMessage({ type: "__edit_mode_available" }, "*");
return () => window.removeEventListener("message", onMsg);
}, []);
const dismiss = () => {
setOpen(false);
window.parent.postMessage({ type: "__edit_mode_dismissed" }, "*");
};
const onDragStart = (e) => {
const panel = dragRef.current;
if (!panel) return;
const r = panel.getBoundingClientRect();
const sx = e.clientX,
sy = e.clientY;
const startRight = window.innerWidth - r.right;
const startBottom = window.innerHeight - r.bottom;
const move = (ev) => {
offsetRef.current = {
x: startRight - (ev.clientX - sx),
y: startBottom - (ev.clientY - sy),
};
clampToViewport();
};
const up = () => {
window.removeEventListener("mousemove", move);
window.removeEventListener("mouseup", up);
};
window.addEventListener("mousemove", move);
window.addEventListener("mouseup", up);
};
if (!open) return null;
return (
<>
{title}
e.stopPropagation()}
onClick={dismiss}
>
✕
{children}
{hasDeckStage && railEnabled && !noDeckControls && (
)}
>
);
}
// ── Layout helpers ──────────────────────────────────────────────────────────
function TweakSection({ label, children }) {
return (
<>
{label}
{children}
>
);
}
function TweakRow({ label, value, children, inline = false }) {
return (
{label}
{value != null && {value} }
{children}
);
}
// ── Controls ────────────────────────────────────────────────────────────────
function TweakSlider({
label,
value,
min = 0,
max = 100,
step = 1,
unit = "",
onChange,
}) {
return (
onChange(Number(e.target.value))}
/>
);
}
function TweakToggle({ label, value, onChange }) {
return (
{label}
onChange(!value)}
>
);
}
function TweakRadio({ label, value, options, onChange }) {
const trackRef = useRef(null);
const [dragging, setDragging] = useState(false);
// The active value is read by pointer-move handlers attached for the lifetime
// of a drag — ref it so a stale closure doesn't fire onChange for every move.
const valueRef = useRef(value);
valueRef.current = value;
// Segments wrap mid-word once per-segment width runs out. The track is
// ~248px (280 panel − 28 body pad − 4 seg pad), each button loses 12px
// to its own padding, and 11.5px system-ui averages ~6.3px/char — so 2
// options fit ~16 chars each, 3 fit ~10. Past that (or >3 options), fall
// back to a dropdown rather than wrap.
const labelLen = (o) => String(typeof o === "object" ? o.label : o).length;
const maxLen = options.reduce((m, o) => Math.max(m, labelLen(o)), 0);
const fitsAsSegments = maxLen <= ({ 2: 16, 3: 10 }[options.length] ?? 0);
if (!fitsAsSegments) {
// emits strings — map back to the original option value so the
// fallback stays type-preserving (numbers, booleans) like the segment path.
const resolve = (s) => {
const m = options.find(
(o) => String(typeof o === "object" ? o.value : o) === s,
);
return m === undefined ? s : typeof m === "object" ? m.value : m;
};
return (
onChange(resolve(s))}
/>
);
}
const opts = options.map((o) =>
typeof o === "object" ? o : { value: o, label: o },
);
const idx = Math.max(
0,
opts.findIndex((o) => o.value === value),
);
const n = opts.length;
const segAt = (clientX) => {
const r = trackRef.current.getBoundingClientRect();
const inner = r.width - 4;
const i = Math.floor(((clientX - r.left - 2) / inner) * n);
return opts[Math.max(0, Math.min(n - 1, i))].value;
};
const onPointerDown = (e) => {
setDragging(true);
const v0 = segAt(e.clientX);
if (v0 !== valueRef.current) onChange(v0);
const move = (ev) => {
if (!trackRef.current) return;
const v = segAt(ev.clientX);
if (v !== valueRef.current) onChange(v);
};
const up = () => {
setDragging(false);
window.removeEventListener("pointermove", move);
window.removeEventListener("pointerup", up);
};
window.addEventListener("pointermove", move);
window.addEventListener("pointerup", up);
};
return (
{opts.map((o) => (
{o.label}
))}
);
}
function TweakSelect({ label, value, options, onChange }) {
return (
onChange(e.target.value)}
>
{options.map((o) => {
const v = typeof o === "object" ? o.value : o;
const l = typeof o === "object" ? o.label : o;
return (
{l}
);
})}
);
}
function TweakText({ label, value, placeholder, onChange }) {
return (
onChange(e.target.value)}
/>
);
}
function TweakNumber({
label,
value,
min,
max,
step = 1,
unit = "",
onChange,
}) {
const clamp = (n) => {
if (min != null && n < min) return min;
if (max != null && n > max) return max;
return n;
};
const startRef = useRef({ x: 0, val: 0 });
const onScrubStart = (e) => {
e.preventDefault();
startRef.current = { x: e.clientX, val: value };
const decimals = (String(step).split(".")[1] || "").length;
const move = (ev) => {
const dx = ev.clientX - startRef.current.x;
const raw = startRef.current.val + dx * step;
const snapped = Math.round(raw / step) * step;
onChange(clamp(Number(snapped.toFixed(decimals))));
};
const up = () => {
window.removeEventListener("pointermove", move);
window.removeEventListener("pointerup", up);
};
window.addEventListener("pointermove", move);
window.addEventListener("pointerup", up);
};
return (
{label}
onChange(clamp(Number(e.target.value)))}
/>
{unit && {unit} }
);
}
// Relative-luminance contrast pick — checkmarks drawn over a swatch need to
// read on both #111 and #fafafa without per-option configuration. Hex input
// only (#rgb / #rrggbb); named or rgb()/hsl() colors fall through to "light".
function __twkIsLight(hex) {
const h = String(hex).replace("#", "");
const x = h.length === 3 ? h.replace(/./g, (c) => c + c) : h.padEnd(6, "0");
const n = parseInt(x.slice(0, 6), 16);
if (Number.isNaN(n)) return true;
const r = (n >> 16) & 255,
g = (n >> 8) & 255,
b = n & 255;
return r * 299 + g * 587 + b * 114 > 148000;
}
const __TwkCheck = ({ light }) => (
);
// TweakColor — curated color/palette picker. Each option is either a single
// hex string or an array of 1-5 hex strings; the card adapts — a lone color
// renders solid, a palette renders colors[0] as the hero (left ~2/3) with the
// rest stacked in a sharp column on the right. onChange emits the
// option in the shape it was passed (string stays string, array stays array).
// Without options it falls back to the native color input for back-compat.
function TweakColor({ label, value, options, onChange }) {
if (!options || !options.length) {
return (
);
}
// Native emits lowercase hex per the HTML spec, so
// compare case-insensitively. String() guards JSON.stringify(undefined),
// which returns the primitive undefined (no .toLowerCase).
const key = (o) => String(JSON.stringify(o)).toLowerCase();
const cur = key(value);
return (
{options.map((o, i) => {
const colors = Array.isArray(o) ? o : [o];
const [hero, ...rest] = colors;
const sup = rest.slice(0, 4);
const on = key(o) === cur;
return (
onChange(o)}
>
{sup.length > 0 && (
{sup.map((c, j) => (
))}
)}
{on && <__TwkCheck light={__twkIsLight(hero)} />}
);
})}
);
}
function TweakButton({ label, onClick, secondary = false }) {
return (
{label}
);
}
// --- primitives.jsx ---
// Small shared primitives: logo, arrow icon, ambient glow, eyebrow, trust strip.
// The "V_" mark — bold filled V + terminal-cursor underscore. Sized via the
// outer .logo-mark; the SVG fills it. `stroke-linejoin="round"` + a thin
// stroke on the filled paths softens the corners just enough.
export function LogoMark({
size = 26,
blink = true,
}: {
size?: number;
blink?: boolean;
}) {
return (
);
}
export function Logo({ size = 26 }: { size?: number }) {
return (
vibn
);
}
function Arrow({ size = 14 }) {
return (
);
}
function Eyebrow({ children }) {
return {children}
;
}
// Soft radial glow blob for ambient backgrounds. Place absolutely positioned.
function Glow({
color = "var(--accent-glow)",
size = 700,
opacity = 1,
style = {},
}) {
return (
);
}
function TrustStrip({ items }) {
return (
{items.map((item, i) => (
{i > 0 && · }
{item}
))}
);
}
// A subtle gradient hairline used inside cards & frames.
function Hairline({ vertical = false, style = {} }) {
return (
);
}
// --- hero.jsx ---
// Hero: the Reddit quote headline + prompt input.
// Visitors can type into the prompt; cycling placeholders, suggestion chips, submit handler logs to console.
const HERO_PLACEHOLDERS = [
"A booking site for my dog grooming business…",
"An invoice tracker for my freelance clients…",
"A members-only recipe site for my supper club…",
"A custom CRM for our 3-person real estate team…",
"A tip calculator app for our restaurant staff…",
"A waitlist site for my new ceramics studio…",
];
const HERO_CHIPS = [
"📋 Client intake form",
"📅 Booking site",
"🧾 Invoice tracker",
"🛒 Online store",
"📰 Email newsletter",
];
function Hero({ onStart, variant = "quote" }) {
const [text, setText] = useState("");
const [phIdx, setPhIdx] = useState(0);
const [phChars, setPhChars] = useState(0);
const [deleting, setDeleting] = useState(false);
const taRef = useRef(null);
// Type-on placeholder when textarea is empty.
useEffect(() => {
if (text.length > 0) return undefined;
const full = HERO_PLACEHOLDERS[phIdx];
const speed = deleting ? 18 : 38;
const t = setTimeout(() => {
if (!deleting) {
if (phChars < full.length) setPhChars(phChars + 1);
else setTimeout(() => setDeleting(true), 1700);
} else {
if (phChars > 0) setPhChars(phChars - 1);
else {
setDeleting(false);
setPhIdx((phIdx + 1) % HERO_PLACEHOLDERS.length);
}
}
}, speed);
return () => clearTimeout(t);
}, [text, phIdx, phChars, deleting]);
const placeholder = HERO_PLACEHOLDERS[phIdx].slice(0, phChars);
const submit = () => {
const value = text || HERO_PLACEHOLDERS[phIdx];
if (onStart) onStart(value);
};
const useChip = (chip) => {
const clean = chip.replace(/^[^\w]+/, "").trim();
setText(`Build me ${clean.toLowerCase()} for my business.`);
if (taRef.current) taRef.current.focus();
};
return (
{/* ambient glows behind hero */}
Live from minute one
{variant === "promise" ? (
<>
Every small business deserves their{" "}
perfect tool .
idea → live → marketed → customers
Vibn is the platform for people building them.
>
) : (
<>
"
I built my product,
now what
?"
posted 2 hours ago · r/SideProject
Keep vibing. All the way to launch.
Your AI handles the technical stuff, puts your idea online, and
helps you find your first customers.
>
)}
{/* Prompt */}
Screenshot
Voice
Templates
Start building
{/* Suggestion chips */}
{HERO_CHIPS.map((c) => (
useChip(c)}
>
{c}
))}
);
}
function PromptIcon({ name }) {
const props = {
width: 13,
height: 13,
viewBox: "0 0 16 16",
fill: "none",
stroke: "currentColor",
strokeWidth: 1.5,
strokeLinecap: "round",
strokeLinejoin: "round",
};
if (name === "paperclip")
return (
);
if (name === "mic")
return (
);
if (name === "grid")
return (
);
return null;
}
// --- wall.jsx ---
// The Wall — recreates the moment the vibe dies. Faux chat from a "generic" AI
// coding tool that hands back a homework list. Ends on the punchline.
function Wall() {
return (
The wall
Keep vibing. All the way to launch .
Your AI handles the technical stuff, puts your idea online, and
helps you find customers. No extra tools. No headaches.
Does this look familiar?
untitled-project · main
generic ai coder · chat
YOU
You · just now
okay it works!! how do i put this online so my customers can
use it?
AI
Generic AI · just now
Great job 🎉 Your app is running locally. To take it live,
you'll need to set a few things up first:
Sign up for Supabase and create a project for your
database.↗ external
Configure authentication with Supabase Auth or Clerk
— pick one.↗ external
Create a GitHub repo , commit your code, and push it.
↗ external
Deploy to Vercel : connect repo, configure framework
preset.↗ external
Add environment variables for your API keys and DB
url in the Vercel dashboard.
↗ external
Set up DNS for your custom domain and verify
nameservers with your registrar.
↗ external
Configure SSL / TLS certificates for HTTPS (or use
Vercel's automatic provisioning).
↗ external
Set up Stripe if you want to take payments, and
configure webhooks.↗ external
↓ 23 more steps
And just like that — the vibe is gone.
);
}
// --- crossed.jsx ---
// Crossed-out list — technical terms struck through, ending in "Your AI handles
// all of it. You just keep building."
const CROSSED_TERMS = [
"Databases",
"Auth providers",
"GitHub",
"Hosting",
"API keys",
"Environment variables",
"Deployment",
"Backend code",
"Servers",
"DNS records",
"SSL certificates",
"CORS errors",
"Webhooks",
"Build pipelines",
"package.json",
"npm install",
];
function CrossedOut() {
return (
What you don't have to learn
All the stuff that made you give up last time.
Forget every word on this list.
{CROSSED_TERMS.map((term, i) => (
{term}
))}
Your AI handles all of it .
You just keep building.
);
}
// --- journey.jsx ---
// The Journey — 4 steps from idea → first 100 customers. A visual marker shows
// where most tools stop. Each step shows a tiny "demo" snippet of what Vibn does.
const JOURNEY_STEPS = [
{
num: "01",
title: "You describe it.",
sub: "The AI builds it.",
body: "Talk to it like you'd talk to a friend who codes. It builds the screens, the buttons, the logic — whatever your idea needs.",
demo: "describe",
},
{
num: "02",
title: "It goes live.",
sub: "The AI puts it online.",
body: "Logins, saving your stuff, hosting — handled. You get a live link from minute one. Share it. Show your friends. It just works.",
demo: "live",
},
{
num: "03",
title: "It gets seen.",
sub: "The AI markets it.",
body: "Posts, emails, social — written, scheduled, and shipped on autopilot. The tone matches your brand because you trained it talking to your AI.",
demo: "seen",
},
{
num: "04",
title: "It gets customers.",
sub: "Your first 100.",
body: "Through our Google partnership, Vibn helps the right people find your product when they're searching for what you built.",
demo: "customers",
},
];
function Journey() {
return (
The journey
From idea to first 100 customers.
In one chat.
Other tools take you to step two and wave goodbye. Vibn keeps
building with you.
{/* "Where everyone else stops" marker, sits over the gap between cards 2 and 3 */}
↑ Where every other tool stops
{JOURNEY_STEPS.map((step, i) => (
= 2} />
))}
One tool. One chat. From "wouldn't it be cool if…" to{" "}
real customers paying you money.
);
}
function StepCard({ step, stopped }) {
return (
{step.num}
{step.title}
{step.sub}
{step.body}
);
}
function StepDemo({ demo }) {
if (demo === "describe") {
return (
YOU
build a booking site for my dog grooming biz
VIBN
on it — designing screens…
VIBN
✓ booking flow ready
);
}
if (demo === "live") {
return (
VIBN
put it online
pawsandposh.vibn.app
✓ logins · ✓ saving · ✓ live
);
}
if (demo === "seen") {
return (
VIBN
draft a launch post for Instagram + email blast
↳ scheduled for Tue 9:00 AM
↳ scheduled for Thu 6:00 PM
✓ 3 channels on autopilot
);
}
if (demo === "customers") {
return (
+47this week
found you via Google
✓ tracking toward 100
);
}
return null;
}
// --- audience.jsx ---
// Who it's for — three audience cards, each with a Reddit-style customer quote
// and Vibn's answer.
const AUDIENCE = [
{
label: "Small business owners",
icon: "shop",
quote:
"I'm paying $312/month for software that does 60% of what I need and zero of the rest.",
source: "u/coffeeshop_owner · r/smallbusiness",
answer:
"Build the tool that actually fits your shop — exactly your workflow, no monthly fee bleed.",
},
{
label: "Freelancers building for clients",
icon: "spark",
quote:
"My client wants a quote tool. I can mock the frontend in a day. The backend? Two weeks I don't have.",
source: "u/agency_of_one · r/freelance",
answer:
"Deliver the whole thing — login, data, hosting — in the same chat where you built the screens.",
},
{
label: "Anyone with an idea",
icon: "spark2",
quote:
"I built the homepage in an afternoon. Then the AI told me to 'just deploy it' and I cried.",
source: "u/first_time_builder · r/sideproject",
answer:
"No deploys. No GitHub. No fear. The thing you described is online, with logins, ready for users.",
},
];
function Audience() {
return (
Who Vibn is for
People who have an idea — not a stack.
If you've ever felt this, Vibn was built for you.
{AUDIENCE.map((a) => (
{a.label}
Vibn
{a.answer}
))}
);
}
function AudienceIcon({ name }) {
const p = {
width: 20,
height: 20,
viewBox: "0 0 20 20",
fill: "none",
stroke: "currentColor",
strokeWidth: 1.5,
strokeLinecap: "round",
strokeLinejoin: "round",
};
if (name === "shop")
return (
);
if (name === "spark")
return (
);
if (name === "spark2")
return (
);
return null;
}
// --- closing.jsx ---
// Closing CTA + Footer.
function Closing() {
return (
If you can describe it,
you can build it.
And you can keep building it — all the way to customers.
No new tools. No homework. No going back to the wall.
);
}
export function Footer() {
return (
🇨🇦 Built in Canada
·
Your data stays safe
·
No credit card to start
© 2026 Vibn Inc. · Made for makers, not engineers.
);
}
// --- app.jsx ---
// App — composes the page. Includes the sticky nav, the success modal that
// appears when the user submits the hero prompt, and the Tweaks panel.
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/ {
accent: ["#ff6b47", "#ffae9a", "#9c3a1f"],
heroVariant: "promise",
showStopMarker: true,
showLivePill: false,
}; /*EDITMODE-END*/
const ACCENT_PRESETS = {
coral: ["#ff6b47", "#ffae9a", "#9c3a1f"], // warm coral (default)
amber: ["#ffb347", "#ffd9a3", "#9c6e1f"], // soft amber
lime: ["#9ee649", "#d2f3a6", "#3f7a1c"], // electric lime
violet: ["#b07cff", "#dabfff", "#5a2fa3"], // violet
};
function applyAccent(arr) {
// arr[0] is the hero color we map to var(--accent); compute soft + glow + fg.
const hero = arr[0];
const soft = `${hero}24`; // 14% alpha
const glow = `${hero}59`; // 35% alpha
const root = document.documentElement;
root.style.setProperty("--accent", hero);
root.style.setProperty("--accent-soft", soft);
root.style.setProperty("--accent-glow", glow);
// Foreground on accent: derive a dark-on-accent for primary buttons.
root.style.setProperty("--accent-fg", "#1a0f0a");
}
function App() {
const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
const [scrolled, setScrolled] = useState(false);
const [showLaunch, setShowLaunch] = useState(null);
useEffect(() => {
applyAccent(t.accent);
}, [t.accent]);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 8);
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
const handleStart = (prompt) => {
setShowLaunch(prompt || "Build me a tool for my business.");
};
return (
<>
{showLaunch !== null && (
setShowLaunch(null)} />
)}
setTweak("accent", v)}
/>
setTweak("heroVariant", v)}
/>
setTweak("showLivePill", v)}
/>
setTweak("showStopMarker", v)}
/>
{/* Tweak-driven CSS overrides */}
>
);
}
export function Nav({ scrolled = false }: { scrolled?: boolean }) {
return (
);
}
// Modal that fires when the user submits the hero prompt. Reassures them their
// vibe will be honored — playfully sells the rest of the flow.
function LaunchModal({ prompt, onClose }) {
useEffect(() => {
const onKey = (e) => {
if (e.key === "Escape") onClose();
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [onClose]);
// Preserve prompt for onboarding seeding (T12)
useEffect(() => {
if (typeof window !== "undefined" && prompt) {
try {
localStorage.setItem("vibn:firstName", prompt);
} catch (err) {
console.error("Failed to save hero prompt to localStorage:", err);
}
}
}, [prompt]);
const [step, setStep] = useState(0);
useEffect(() => {
if (step >= 4) return undefined;
const t = setTimeout(() => setStep(step + 1), 700);
return () => clearTimeout(t);
}, [step]);
const [redirectCount, setRedirectCount] = useState(3);
useEffect(() => {
if (step < 4) return undefined;
if (redirectCount <= 0) {
window.location.href = "/auth";
return undefined;
}
const t = setTimeout(() => setRedirectCount(redirectCount - 1), 1000);
return () => clearTimeout(t);
}, [step, redirectCount]);
return (
e.stopPropagation()}>
✕
Vibn is on it
Keep vibing — we've got the rest.
"{prompt}"
{[
"Drafting the screens",
"Setting up logins",
"Saving your stuff",
"Putting it online",
].map((s, i) => (
{i < step ? (
) : i === step ? (
) : (
)}
{s}
))}
{step === 4 ? (
) : (
No homework · No setup · No new tools to learn
)}
);
}
export default function NewSite() {
return (
);
}