400 lines
18 KiB
JavaScript
400 lines
18 KiB
JavaScript
// ============================================================
|
|
// vibn-ai-templates/shells.jsx
|
|
// ------------------------------------------------------------
|
|
// Layout shells — both in-product navs (Sidebar / Rail /
|
|
// Topbar) and auth scaffolds (CenteredCard / SplitHero / Glass).
|
|
//
|
|
// These are containers. Wrap your page in any shell and the
|
|
// shell handles brand, search, nav, footer. Compose with the
|
|
// components from components.jsx.
|
|
// ============================================================
|
|
|
|
// ── SidebarShell ─────────────────────────────────────────────
|
|
// Props:
|
|
// brand: { name, mark? }
|
|
// sections: [{ title?, items: [{ id, label, icon, count, active }] }]
|
|
// user: { name, email, color? }
|
|
// children: main pane
|
|
const SidebarShell = ({ brand = { name: "Vibn" }, sections = [], user, search = "Search…", children, width = 248 }) => {
|
|
return (
|
|
<div className="vibn-app" style={{
|
|
width: "100%", height: "100%",
|
|
display: "grid", gridTemplateColumns: `${width}px 1fr`,
|
|
overflow: "hidden",
|
|
}}>
|
|
<aside style={{
|
|
background: "var(--surface-alt)",
|
|
borderRight: "1px solid var(--border)",
|
|
display: "flex", flexDirection: "column",
|
|
}}>
|
|
{/* Brand row */}
|
|
<div style={{
|
|
padding: "12px 14px", display: "flex", alignItems: "center", gap: 10,
|
|
borderBottom: "1px solid var(--border)",
|
|
}}>
|
|
{brand.mark || <VibnMark size={22}/>}
|
|
<div style={{ flex: 1, fontSize: "var(--text-md)", fontWeight: "var(--weight-semibold)" }}>
|
|
{brand.name}
|
|
</div>
|
|
<Icon name="chevDown" size={14} style={{ color: "var(--text-3)" }}/>
|
|
</div>
|
|
{/* Search */}
|
|
<div style={{ padding: 12 }}>
|
|
<Input
|
|
placeholder={search}
|
|
leadingIcon={<Icon name="search" size={14}/>}
|
|
trailingIcon={<KBD>⌘K</KBD>}
|
|
style={{ padding: "6px 10px" }}
|
|
/>
|
|
</div>
|
|
{/* Nav */}
|
|
<nav style={{ padding: "4px 8px", flex: 1, overflowY: "auto" }}>
|
|
{sections.map((s, i) => (
|
|
<div key={s.title || `s${i}`}>
|
|
{s.title && <div style={{
|
|
fontSize: "var(--text-xs)", color: "var(--text-3)",
|
|
padding: "14px 10px 6px", textTransform: "uppercase",
|
|
letterSpacing: "0.04em", fontWeight: 500,
|
|
}}>{s.title}</div>}
|
|
{s.items.map(it => (
|
|
<div key={it.id || it.label} style={{
|
|
display: "flex", alignItems: "center", gap: 10,
|
|
padding: "6px 10px", borderRadius: "var(--radius-sm)",
|
|
fontSize: "var(--text-md)", cursor: "pointer", marginBottom: 1,
|
|
color: it.active ? "var(--text)" : "var(--text-2)",
|
|
fontWeight: it.active ? 500 : 400,
|
|
background: it.active ? "var(--surface)" : "transparent",
|
|
boxShadow: it.active ? "var(--shadow-sm)" : "none",
|
|
}}>
|
|
<span style={{ color: it.active ? "var(--accent)" : "var(--text-3)", display: "flex" }}>
|
|
{typeof it.icon === "string"
|
|
? <Icon name={it.icon} size={15}/>
|
|
: it.icon}
|
|
</span>
|
|
<span style={{ flex: 1, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{it.label}</span>
|
|
{it.count != null && <span style={{
|
|
fontSize: "var(--text-xs)", color: "var(--text-3)",
|
|
fontVariantNumeric: "tabular-nums",
|
|
}}>{it.count}</span>}
|
|
</div>
|
|
))}
|
|
</div>
|
|
))}
|
|
</nav>
|
|
{/* User */}
|
|
{user && (
|
|
<div style={{
|
|
padding: 12, borderTop: "1px solid var(--border)",
|
|
display: "flex", alignItems: "center", gap: 10,
|
|
}}>
|
|
<Avatar name={user.name} color={user.color} size={26}/>
|
|
<div style={{ flex: 1, minWidth: 0, lineHeight: 1.2 }}>
|
|
<div style={{ fontSize: "var(--text-sm)", fontWeight: 500 }}>{user.name}</div>
|
|
{user.email && <div style={{ fontSize: "var(--text-xs)", color: "var(--text-3)" }}>{user.email}</div>}
|
|
</div>
|
|
<Icon name="chevDown" size={14} style={{ color: "var(--text-3)" }}/>
|
|
</div>
|
|
)}
|
|
</aside>
|
|
|
|
<main style={{ display: "flex", flexDirection: "column", overflow: "hidden", background: "var(--bg)" }}>
|
|
{children}
|
|
</main>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// ── TopbarShell ──────────────────────────────────────────────
|
|
// Dark top with breadcrumb + ⌘K + avatar; tabs strip below.
|
|
const TopbarShell = ({ brand = { name: "Vibn" }, breadcrumb, tabs = [], activeTab,
|
|
onTabChange = () => {}, user, children, search = "Find or jump to anything…" }) => {
|
|
return (
|
|
<div className="vibn-app" style={{
|
|
width: "100%", height: "100%", display: "flex", flexDirection: "column",
|
|
overflow: "hidden",
|
|
}}>
|
|
<header style={{
|
|
background: "var(--surface-alt)", color: "var(--text)",
|
|
borderBottom: "1px solid var(--border)",
|
|
}}>
|
|
<div style={{
|
|
display: "flex", alignItems: "center", gap: 14, padding: "12px 24px",
|
|
}}>
|
|
<div style={{ display: "flex", alignItems: "center", gap: 8, fontWeight: "var(--weight-semibold)", fontSize: "var(--text-lg)" }}>
|
|
{brand.mark || <VibnMark size={20}/>}
|
|
{brand.name}
|
|
</div>
|
|
{breadcrumb && (
|
|
<>
|
|
<span style={{ color: "var(--text-3)" }}>/</span>
|
|
{breadcrumb.map((b, i) => (
|
|
<React.Fragment key={i}>
|
|
{i > 0 && <span style={{ color: "var(--text-3)" }}>/</span>}
|
|
<span style={{ fontSize: "var(--text-md)", display: "flex", alignItems: "center", gap: 8 }}>
|
|
{b.avatar && <Avatar name={b.avatar} size={18}/>}
|
|
<span>{b.label}</span>
|
|
{b.badge && <Badge tone="neutral">{b.badge}</Badge>}
|
|
</span>
|
|
</React.Fragment>
|
|
))}
|
|
</>
|
|
)}
|
|
<div style={{ flex: 1 }}/>
|
|
<div style={{ minWidth: 280 }}>
|
|
<Input placeholder={search}
|
|
leadingIcon={<Icon name="search" size={13}/>}
|
|
trailingIcon={<KBD>⌘K</KBD>}
|
|
style={{ padding: "6px 12px" }} />
|
|
</div>
|
|
<Button variant="secondary" size="sm">Feedback</Button>
|
|
<IconButton name="bell" size="md"/>
|
|
{user && <Avatar name={user.name} color={user.color} size={28}/>}
|
|
</div>
|
|
{tabs.length > 0 && (
|
|
<div style={{ padding: "0 16px" }}>
|
|
<Tabs items={tabs} active={activeTab} onChange={onTabChange}/>
|
|
</div>
|
|
)}
|
|
</header>
|
|
<main style={{ flex: 1, overflow: "hidden", background: "var(--bg)" }}>{children}</main>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// ── RailShell ────────────────────────────────────────────────
|
|
// Icon rail + secondary panel + content.
|
|
const RailShell = ({ brand, items = [], activeRail, onRailChange = () => {},
|
|
secondary, secondaryTitle, user, children }) => {
|
|
return (
|
|
<div className="vibn-app" style={{
|
|
width: "100%", height: "100%",
|
|
display: "grid", gridTemplateColumns: "64px 240px 1fr", overflow: "hidden",
|
|
}}>
|
|
{/* Rail */}
|
|
<div style={{
|
|
background: "var(--surface-alt)", borderRight: "1px solid var(--border)",
|
|
padding: "10px 0", display: "flex", flexDirection: "column",
|
|
alignItems: "center", gap: 4,
|
|
}}>
|
|
<div style={{ padding: "0 10px 6px" }}>
|
|
{brand?.mark || <VibnMark size={22}/>}
|
|
</div>
|
|
<Divider />
|
|
{items.map(it => {
|
|
const sel = (it.id || it.label) === activeRail;
|
|
return (
|
|
<button key={it.id || it.label}
|
|
onClick={() => onRailChange(it.id || it.label)}
|
|
aria-label={it.label}
|
|
style={{
|
|
width: 40, height: 40, borderRadius: "var(--radius)",
|
|
background: sel ? "var(--accent)" : "transparent",
|
|
color: sel ? "var(--text-on-accent)" : "var(--text-2)",
|
|
border: "none", cursor: "pointer", position: "relative",
|
|
}}>
|
|
{typeof it.icon === "string" ? <Icon name={it.icon} size={18} stroke={2}/> : it.icon}
|
|
{it.badge && <span style={{
|
|
position: "absolute", top: 2, right: 2, minWidth: 16, height: 16,
|
|
padding: "0 4px", borderRadius: 8,
|
|
background: "var(--danger)", color: "#fff",
|
|
fontSize: 10, fontWeight: 600,
|
|
display: "flex", alignItems: "center", justifyContent: "center",
|
|
border: "2px solid var(--surface-alt)",
|
|
}}>{it.badge}</span>}
|
|
</button>
|
|
);
|
|
})}
|
|
<div style={{ flex: 1 }}/>
|
|
{user && <Avatar name={user.name} color={user.color} size={30} ring={1}/>}
|
|
</div>
|
|
|
|
{/* Secondary */}
|
|
<div style={{
|
|
background: "var(--surface-2)", borderRight: "1px solid var(--border)",
|
|
display: "flex", flexDirection: "column", overflow: "hidden",
|
|
}}>
|
|
{secondaryTitle && (
|
|
<div style={{
|
|
padding: "16px 14px 10px", borderBottom: "1px solid var(--border)",
|
|
}}>
|
|
<div style={{
|
|
fontSize: "var(--text-lg)", fontWeight: "var(--weight-semibold)",
|
|
marginBottom: 10,
|
|
}}>{secondaryTitle}</div>
|
|
<Input
|
|
placeholder="Jump to…"
|
|
leadingIcon={<Icon name="search" size={13}/>}
|
|
trailingIcon={<KBD>⌘K</KBD>}
|
|
style={{ padding: "6px 10px" }}
|
|
/>
|
|
</div>
|
|
)}
|
|
<div style={{ padding: 10, flex: 1, overflowY: "auto" }}>{secondary}</div>
|
|
</div>
|
|
|
|
<main style={{ overflow: "hidden", background: "var(--bg)" }}>{children}</main>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// ── AuthCenteredShell ────────────────────────────────────────
|
|
// A single centered Card on a soft background, with brand top
|
|
// and small footer links. Good for sign-in / sign-up.
|
|
const AuthCenteredShell = ({ brand = { name: "Vibn" }, footerLinks = ["Privacy", "Terms", "Security"], cardWidth = 420, children }) => (
|
|
<div className="vibn-app" style={{
|
|
width: "100%", height: "100%", display: "grid",
|
|
gridTemplateRows: "auto 1fr auto", overflow: "hidden",
|
|
}}>
|
|
<header style={{
|
|
display: "flex", justifyContent: "space-between", alignItems: "center",
|
|
padding: "20px 28px",
|
|
}}>
|
|
<div style={{ display: "flex", alignItems: "center", gap: 8, fontWeight: 600 }}>
|
|
{brand.mark || <VibnMark size={20}/>}
|
|
{brand.name}
|
|
</div>
|
|
<div style={{ fontSize: "var(--text-sm)", color: "var(--text-2)", display: "flex", gap: 18 }}>
|
|
<span>Status</span><span>Docs</span><span style={{ color: "var(--text)", fontWeight: 500 }}>Sign in ↗</span>
|
|
</div>
|
|
</header>
|
|
<main style={{ display: "flex", alignItems: "center", justifyContent: "center", padding: 24 }}>
|
|
<Card variant="raised" padding={32} style={{ width: cardWidth }}>{children}</Card>
|
|
</main>
|
|
<footer style={{
|
|
display: "flex", justifyContent: "space-between", alignItems: "center",
|
|
padding: "16px 28px", fontSize: "var(--text-xs)", color: "var(--text-3)",
|
|
}}>
|
|
<span>© 2026 {brand.name}</span>
|
|
<div style={{ display: "flex", gap: 16 }}>{footerLinks.map(l => <span key={l}>{l}</span>)}</div>
|
|
</footer>
|
|
</div>
|
|
);
|
|
|
|
// ── AuthSplitShell ───────────────────────────────────────────
|
|
// Left storytelling panel, right form. Big SaaS / Vercel feel.
|
|
const AuthSplitShell = ({ brand = { name: "Vibn" }, hero = {}, children }) => (
|
|
<div className="vibn-app" style={{
|
|
width: "100%", height: "100%", display: "grid",
|
|
gridTemplateColumns: "1fr 1fr", overflow: "hidden",
|
|
}}>
|
|
<div style={{
|
|
padding: "32px 44px", borderRight: "1px solid var(--border)",
|
|
display: "flex", flexDirection: "column",
|
|
background: "var(--surface-alt)", position: "relative", overflow: "hidden",
|
|
}}>
|
|
{/* Decorative wash, picks up theme accent */}
|
|
<div style={{
|
|
position: "absolute", top: -140, left: -120, width: 540, height: 540,
|
|
borderRadius: "50%",
|
|
background: "radial-gradient(circle, color-mix(in srgb, var(--accent-2) 40%, transparent), transparent 60%)",
|
|
filter: "blur(60px)", pointerEvents: "none",
|
|
}}/>
|
|
<div style={{
|
|
position: "absolute", bottom: -180, right: -120, width: 480, height: 480,
|
|
borderRadius: "50%",
|
|
background: "radial-gradient(circle, color-mix(in srgb, var(--accent) 30%, transparent), transparent 60%)",
|
|
filter: "blur(60px)", pointerEvents: "none",
|
|
}}/>
|
|
|
|
<div style={{ position: "relative", display: "flex", alignItems: "center", gap: 10, fontWeight: 600 }}>
|
|
{brand.mark || <VibnMark size={22}/>}
|
|
{brand.name}
|
|
</div>
|
|
|
|
<div style={{ position: "relative", marginTop: "auto" }}>
|
|
{hero.badge && (
|
|
<Badge tone="accent" style={{ marginBottom: 22 }}>{hero.badge}</Badge>
|
|
)}
|
|
{hero.headline && <h2 style={{
|
|
fontFamily: "var(--font-display)", fontSize: "var(--text-3xl)",
|
|
lineHeight: 1.05, margin: 0, letterSpacing: "-0.02em",
|
|
fontWeight: 500, textWrap: "balance", maxWidth: 360,
|
|
}}>{hero.headline}</h2>}
|
|
{hero.sub && <p style={{
|
|
fontSize: "var(--text-md)", color: "var(--text-2)",
|
|
marginTop: 14, lineHeight: 1.5, maxWidth: 340,
|
|
}}>{hero.sub}</p>}
|
|
|
|
{hero.quote && (
|
|
<div style={{
|
|
position: "relative", marginTop: 28, padding: 18,
|
|
borderRadius: "var(--card-radius)", background: "var(--surface)",
|
|
border: "1px solid var(--border)",
|
|
}}>
|
|
<p style={{ fontSize: "var(--text-md)", margin: 0, lineHeight: 1.5, color: "var(--text)" }}>
|
|
"{hero.quote.body}"
|
|
</p>
|
|
<div style={{ marginTop: 12, display: "flex", alignItems: "center", gap: 10 }}>
|
|
<Avatar name={hero.quote.author} size={26}/>
|
|
<div style={{ fontSize: "var(--text-xs)" }}>
|
|
<div style={{ fontWeight: 500 }}>{hero.quote.author}</div>
|
|
<div style={{ color: "var(--text-3)" }}>{hero.quote.role}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ display: "flex", flexDirection: "column", padding: "32px 56px" }}>
|
|
<div style={{ display: "flex", justifyContent: "flex-end", fontSize: "var(--text-sm)", color: "var(--text-2)" }}>
|
|
Need help? <span style={{ color: "var(--text)", fontWeight: 500, marginLeft: 4 }}>support</span>
|
|
</div>
|
|
<div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center" }}>
|
|
<div style={{ width: 380 }}>{children}</div>
|
|
</div>
|
|
<div style={{ display: "flex", gap: 18, fontSize: "var(--text-xs)", color: "var(--text-3)", justifyContent: "flex-end" }}>
|
|
<span>Privacy</span><span>Terms</span><span>Security</span><span>v4.2.1</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
// ── AuthGlassShell ───────────────────────────────────────────
|
|
// Aurora background + frosted card. Marketing-leaning.
|
|
const AuthGlassShell = ({ brand = { name: "Vibn" }, eyebrow, cardWidth = 460, children }) => (
|
|
<div className="vibn-app" style={{
|
|
width: "100%", height: "100%", position: "relative", overflow: "hidden",
|
|
}}>
|
|
{/* Top bar (a thin frosted pill — works in any theme thanks to surface vars) */}
|
|
<header style={{
|
|
position: "absolute", top: 22, left: "50%", transform: "translateX(-50%)",
|
|
zIndex: 10, width: "max-content",
|
|
display: "flex", alignItems: "center", gap: 4,
|
|
padding: "8px 8px 8px 18px", borderRadius: "var(--radius-pill)",
|
|
background: "var(--surface)",
|
|
border: "1px solid var(--border)",
|
|
backdropFilter: "blur(var(--surface-blur))",
|
|
WebkitBackdropFilter: "blur(var(--surface-blur))",
|
|
boxShadow: "var(--shadow-lg)",
|
|
}}>
|
|
<div style={{ display: "flex", alignItems: "center", gap: 8, marginRight: 16, fontWeight: 600 }}>
|
|
{brand.mark || <VibnMark size={18}/>}
|
|
{brand.name}
|
|
</div>
|
|
{["Product", "Pricing", "Docs"].map(l => (
|
|
<Button key={l} variant="ghost" size="sm">{l}</Button>
|
|
))}
|
|
<Divider vertical style={{ margin: "0 6px" }}/>
|
|
<Button variant="ghost" size="sm">Sign in</Button>
|
|
<Button size="sm">Get started →</Button>
|
|
</header>
|
|
|
|
<main style={{
|
|
position: "relative", height: "100%",
|
|
display: "flex", alignItems: "center", justifyContent: "center", padding: 24,
|
|
}}>
|
|
<Card variant="floating" padding={36} style={{ width: cardWidth, borderRadius: "var(--radius-xl)" }}>
|
|
{eyebrow && <Badge tone="accent" style={{ marginBottom: 16 }}>{eyebrow}</Badge>}
|
|
{children}
|
|
</Card>
|
|
</main>
|
|
</div>
|
|
);
|
|
|
|
// ─── Exports ─────────────────────────────────────────────────
|
|
Object.assign(window, {
|
|
SidebarShell, TopbarShell, RailShell,
|
|
AuthCenteredShell, AuthSplitShell, AuthGlassShell,
|
|
});
|