feat: flatten routes and merge marketing and onboarding directories
This commit is contained in:
120
design-templates/VIBN (2)/vibn-ai-templates/README.md
Normal file
120
design-templates/VIBN (2)/vibn-ai-templates/README.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# Vibn AI Templates
|
||||
|
||||
A small, themable starter kit for building modern SaaS UIs. Pure React + CSS variables — no build step, no dependencies. Designed to be copy-pasted into any project.
|
||||
|
||||
## What's in it
|
||||
|
||||
- **`tokens.css`** — Every color, radius, shadow, and type token, exposed as CSS custom properties. Four themes ship out of the box:
|
||||
- `.theme-minimal` — soft warm light (Linear / Notion school)
|
||||
- `.theme-dark` — black-and-white surface (Vercel / Stripe school)
|
||||
- `.theme-glass` — aurora gradient + frosted glass
|
||||
- `.theme-editorial` — paper, serif display, hairline rules
|
||||
- **`icons.jsx`** — A small Tabler-style stroke icon set (`<Icon name="search"/>`) plus a `<VibnMark/>` brand glyph.
|
||||
- **`components.jsx`** — Atoms + composites. Every visual property reads from a CSS variable:
|
||||
- **Forms** · `Button`, `IconButton`, `Field`, `Input`, `Textarea`, `Select`, `Checkbox`, `Radio`, `Switch`, `FieldGroup`
|
||||
- **Containers** · `Card`, `CardHeader`, `Divider`, `Modal`, `Banner`
|
||||
- **Display** · `Badge` (tones: neutral / accent / success / warn / danger / info), `Avatar`, `AvatarStack`, `Tabs`, `Table`, `Spinner`, `KBD`
|
||||
- **`shells.jsx`** — Page-level layouts:
|
||||
- **In-product** · `SidebarShell`, `TopbarShell`, `RailShell`
|
||||
- **Auth** · `AuthCenteredShell`, `AuthSplitShell`, `AuthGlassShell`
|
||||
|
||||
## How theming works
|
||||
|
||||
Tokens are CSS custom properties on `:root` (the default minimal theme). Each `.theme-*` class overrides a subset. Apply a theme by adding the class anywhere — usually on `<html>` or a top-level wrapper.
|
||||
|
||||
```html
|
||||
<html class="theme-glass">
|
||||
<!-- the whole page uses the glass theme -->
|
||||
</html>
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- or scope a theme to one region -->
|
||||
<div class="theme-editorial">
|
||||
<Card>… this card is editorial …</Card>
|
||||
</div>
|
||||
```
|
||||
|
||||
Themes can nest. Setting `theme-*` on a child element overrides only the tokens that theme defines; the rest inherit from the parent.
|
||||
|
||||
### Adding a fifth theme
|
||||
|
||||
Add a new class to `tokens.css` that overrides whichever tokens differ from `:root`:
|
||||
|
||||
```css
|
||||
.theme-sunset {
|
||||
--bg: #2b0d0e;
|
||||
--surface: #3a1316;
|
||||
--accent: #ff8a3a;
|
||||
--accent-2: #f43f5e;
|
||||
--text: #fef7ee;
|
||||
--text-2: #f0c8b0;
|
||||
--border: #4a1f23;
|
||||
--button-bg: #ff8a3a;
|
||||
--button-fg: #2b0d0e;
|
||||
}
|
||||
```
|
||||
|
||||
You don't need to redefine the whole token set — just the differences. Components don't change.
|
||||
|
||||
## Usage in plain HTML (no bundler)
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="vibn-ai-templates/tokens.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=DM+Serif+Display:ital@0;1&display=swap" rel="stylesheet">
|
||||
|
||||
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js"></script>
|
||||
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js"></script>
|
||||
|
||||
<script type="text/babel" src="vibn-ai-templates/icons.jsx"></script>
|
||||
<script type="text/babel" src="vibn-ai-templates/components.jsx"></script>
|
||||
<script type="text/babel" src="vibn-ai-templates/shells.jsx"></script>
|
||||
|
||||
<div id="root" class="theme-dark"></div>
|
||||
|
||||
<script type="text/babel">
|
||||
function App() {
|
||||
return (
|
||||
<AuthCenteredShell brand={{ name: "Acme" }}>
|
||||
<h1 style={{ margin: 0, fontSize: 22, fontWeight: 600 }}>Welcome back</h1>
|
||||
<p style={{ color: "var(--text-2)", marginTop: 6 }}>Sign in to continue.</p>
|
||||
<Field label="Email"><Input value="mira@acme.io"/></Field>
|
||||
<Field label="Password"><Input type="password" value="••••••••••"/></Field>
|
||||
<Button full>Sign in</Button>
|
||||
</AuthCenteredShell>
|
||||
);
|
||||
}
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
|
||||
</script>
|
||||
```
|
||||
|
||||
## Usage in a React codebase
|
||||
|
||||
Convert the three `.jsx` files from `Object.assign(window, …)` to named `export` statements. The components have no runtime dependencies beyond React.
|
||||
|
||||
```jsx
|
||||
// In your app
|
||||
import "vibn-ai-templates/tokens.css";
|
||||
import { Button, Card, Input, Field, Tabs } from "vibn-ai-templates/components";
|
||||
import { SidebarShell } from "vibn-ai-templates/shells";
|
||||
|
||||
// Pick a theme on your root
|
||||
<html className="theme-dark">…
|
||||
```
|
||||
|
||||
## Conventions
|
||||
|
||||
- **Inline styles read from CSS vars** — `style={{ background: "var(--surface)" }}`. This is intentional: it lets the entire library reskin with one class swap, and avoids a CSS-in-JS dependency.
|
||||
- **Components are presentational.** State (open/closed modals, active tabs, form values) lives in your app. Pass `active` + `onChange` to controlled components.
|
||||
- **No external icon dependency.** `icons.jsx` ships a curated set. Add to it freely.
|
||||
- **Avatars hash a color from the name** unless you pass `color="#…"`.
|
||||
- **Tables and tabs are uncontrolled-friendly** — pass `rows`/`items`, omit selection props if you don't need them.
|
||||
|
||||
## Showcase
|
||||
|
||||
`Vibn UI Showcase.html` at the project root renders every component across every theme. Use it as the visual reference and as a starting point for new screens.
|
||||
|
||||
## Versioning
|
||||
|
||||
This is a starter — fork it. There's no semver, no changelog. Edit `tokens.css` to match your brand, prune what you don't use, extend what you do.
|
||||
737
design-templates/VIBN (2)/vibn-ai-templates/components.jsx
Normal file
737
design-templates/VIBN (2)/vibn-ai-templates/components.jsx
Normal file
@@ -0,0 +1,737 @@
|
||||
// ============================================================
|
||||
// vibn-ai-templates/components.jsx
|
||||
// ------------------------------------------------------------
|
||||
// The core component set. Every visual property is wired to a
|
||||
// CSS variable from tokens.css — flipping `class="theme-glass"`
|
||||
// (or any other theme class) reskins the whole library.
|
||||
//
|
||||
// Components export to `window` for use in script-tag HTML
|
||||
// projects. In a real codebase, swap the bottom-of-file
|
||||
// assignment for `export { … }`.
|
||||
//
|
||||
// Components included:
|
||||
// Button, IconButton, Field, Input, Textarea, Select,
|
||||
// Checkbox, Radio, Switch, Card, Badge, Tag, Avatar,
|
||||
// AvatarStack, Tabs, Table, Modal, Banner, Divider,
|
||||
// FieldGroup, KBD, Spinner.
|
||||
// ============================================================
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────
|
||||
const cx = (...names) => names.filter(Boolean).join(" ");
|
||||
const noop = () => {};
|
||||
|
||||
// ─── Button ──────────────────────────────────────────────────
|
||||
// variant: primary (default), secondary, ghost, destructive
|
||||
// size: sm | md (default) | lg
|
||||
// leadingIcon / trailingIcon: <Icon name="…"/>
|
||||
// loading: disables and shows a spinner
|
||||
const Button = ({
|
||||
children, variant = "primary", size = "md", full = false,
|
||||
leadingIcon, trailingIcon, loading, disabled, onClick = noop, style, type = "button",
|
||||
...rest
|
||||
}) => {
|
||||
const sizing = {
|
||||
sm: { padY: 6, padX: 12, font: "var(--text-sm)", iconSize: 13 },
|
||||
md: { padY: 9, padX: 16, font: "var(--text-md)", iconSize: 15 },
|
||||
lg: { padY: 12, padX: 22, font: "var(--text-lg)", iconSize: 16 },
|
||||
}[size];
|
||||
|
||||
const variants = {
|
||||
primary: {
|
||||
background: "var(--button-bg)",
|
||||
color: "var(--button-fg)",
|
||||
border: "1px solid var(--button-border)",
|
||||
},
|
||||
secondary: {
|
||||
background: "var(--button-secondary-bg)",
|
||||
color: "var(--button-secondary-fg)",
|
||||
border: "1px solid var(--button-secondary-border)",
|
||||
},
|
||||
ghost: {
|
||||
background: "transparent",
|
||||
color: "var(--button-ghost-fg)",
|
||||
border: "1px solid transparent",
|
||||
},
|
||||
destructive: {
|
||||
background: "var(--danger)",
|
||||
color: "#ffffff",
|
||||
border: "1px solid var(--danger)",
|
||||
},
|
||||
}[variant];
|
||||
|
||||
return (
|
||||
<button
|
||||
type={type}
|
||||
disabled={disabled || loading}
|
||||
onClick={onClick}
|
||||
style={{
|
||||
...variants,
|
||||
padding: `${sizing.padY}px ${sizing.padX}px`,
|
||||
borderRadius: "var(--button-radius)",
|
||||
fontFamily: "var(--font-sans)",
|
||||
fontSize: sizing.font,
|
||||
fontWeight: "var(--weight-medium)",
|
||||
lineHeight: 1.2,
|
||||
cursor: disabled || loading ? "not-allowed" : "pointer",
|
||||
opacity: disabled ? 0.5 : 1,
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 8,
|
||||
width: full ? "100%" : "auto",
|
||||
whiteSpace: "nowrap",
|
||||
transition: "background var(--duration) var(--ease), transform var(--duration-fast) var(--ease)",
|
||||
...style,
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
{loading ? <Spinner size={sizing.iconSize}/> : leadingIcon}
|
||||
<span>{children}</span>
|
||||
{!loading && trailingIcon}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
// ─── IconButton ──────────────────────────────────────────────
|
||||
const IconButton = ({ icon, name, size = "md", variant = "ghost", onClick = noop, label, style }) => {
|
||||
const dims = { sm: 28, md: 32, lg: 38 }[size];
|
||||
const iconSize = { sm: 14, md: 16, lg: 18 }[size];
|
||||
const variants = {
|
||||
ghost: { background: "transparent", color: "var(--text-2)", border: "1px solid transparent" },
|
||||
secondary: { background: "var(--button-secondary-bg)", color: "var(--button-secondary-fg)",
|
||||
border: "1px solid var(--button-secondary-border)" },
|
||||
}[variant];
|
||||
return (
|
||||
<button
|
||||
aria-label={label}
|
||||
onClick={onClick}
|
||||
style={{
|
||||
...variants,
|
||||
width: dims, height: dims, borderRadius: "var(--radius-sm)",
|
||||
display: "inline-flex", alignItems: "center", justifyContent: "center",
|
||||
cursor: "pointer", padding: 0,
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{icon ?? <Icon name={name} size={iconSize} />}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
// ─── Spinner ─────────────────────────────────────────────────
|
||||
const Spinner = ({ size = 14, stroke = 2 }) => (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="9" stroke="currentColor" strokeOpacity="0.2" strokeWidth={stroke} />
|
||||
<path d="M12 3a9 9 0 0 1 9 9" stroke="currentColor" strokeWidth={stroke} strokeLinecap="round">
|
||||
<animateTransform attributeName="transform" type="rotate"
|
||||
from="0 12 12" to="360 12 12" dur="0.9s" repeatCount="indefinite"/>
|
||||
</path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
// ─── Field (wraps a labelled input with hint / error) ────────
|
||||
const Field = ({ label, hint, error, optional, htmlFor, children, style }) => (
|
||||
<div style={{ marginBottom: "var(--space-4)", ...style }}>
|
||||
{label && (
|
||||
<label htmlFor={htmlFor} style={{
|
||||
display: "flex", justifyContent: "space-between", alignItems: "baseline",
|
||||
fontSize: "var(--text-sm)", fontWeight: "var(--weight-medium)",
|
||||
color: "var(--text)", marginBottom: 6,
|
||||
}}>
|
||||
<span>{label}</span>
|
||||
{optional && <span style={{ color: "var(--text-3)", fontWeight: 400 }}>optional</span>}
|
||||
</label>
|
||||
)}
|
||||
{children}
|
||||
{(hint || error) && (
|
||||
<div style={{
|
||||
fontSize: "var(--text-xs)", marginTop: 5,
|
||||
color: error ? "var(--danger)" : "var(--text-3)",
|
||||
}}>{error || hint}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
// ─── Input ───────────────────────────────────────────────────
|
||||
// Bare input (use inside <Field>). leadingIcon / trailingIcon
|
||||
// add an inner ornament. invalid red-rings the border.
|
||||
const Input = ({
|
||||
value, placeholder, type = "text", leadingIcon, trailingIcon,
|
||||
invalid, disabled, autofocus, onChange = noop, id, style, ...rest
|
||||
}) => (
|
||||
<div style={{
|
||||
display: "flex", alignItems: "center", gap: 8,
|
||||
padding: "10px 12px",
|
||||
borderRadius: "var(--field-radius)",
|
||||
background: "var(--field-bg)",
|
||||
border: `1px solid ${invalid ? "var(--danger)" : "var(--field-border)"}`,
|
||||
boxShadow: autofocus ? "var(--shadow-focus)" : "var(--shadow-sm)",
|
||||
fontSize: "var(--text-md)",
|
||||
color: "var(--text)",
|
||||
backdropFilter: "blur(var(--surface-blur))",
|
||||
WebkitBackdropFilter: "blur(var(--surface-blur))",
|
||||
opacity: disabled ? 0.5 : 1,
|
||||
transition: "border-color var(--duration), box-shadow var(--duration)",
|
||||
...style,
|
||||
}}>
|
||||
{leadingIcon && <span style={{ color: "var(--text-3)", display: "flex" }}>{leadingIcon}</span>}
|
||||
<input
|
||||
id={id}
|
||||
type={type}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
onChange={(e) => onChange(e.target.value, e)}
|
||||
style={{
|
||||
flex: 1, minWidth: 0, border: "none", outline: "none", background: "transparent",
|
||||
fontFamily: "inherit", fontSize: "inherit", color: "inherit",
|
||||
padding: 0,
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
{trailingIcon && <span style={{ color: "var(--text-3)", display: "flex" }}>{trailingIcon}</span>}
|
||||
</div>
|
||||
);
|
||||
|
||||
// ─── Textarea ────────────────────────────────────────────────
|
||||
const Textarea = ({ value, placeholder, rows = 4, onChange = noop, invalid, id, style, ...rest }) => (
|
||||
<textarea
|
||||
id={id}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
rows={rows}
|
||||
onChange={(e) => onChange(e.target.value, e)}
|
||||
style={{
|
||||
width: "100%", display: "block", padding: "10px 12px",
|
||||
borderRadius: "var(--field-radius)",
|
||||
background: "var(--field-bg)",
|
||||
border: `1px solid ${invalid ? "var(--danger)" : "var(--field-border)"}`,
|
||||
fontSize: "var(--text-md)", color: "var(--text)",
|
||||
fontFamily: "var(--font-sans)", resize: "vertical",
|
||||
outline: "none", boxShadow: "var(--shadow-sm)",
|
||||
backdropFilter: "blur(var(--surface-blur))",
|
||||
WebkitBackdropFilter: "blur(var(--surface-blur))",
|
||||
...style,
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
||||
// ─── Select (presentation only — clicks the menu open visually) ─
|
||||
const Select = ({ value, placeholder, options = [], leadingIcon, style, ...rest }) => (
|
||||
<div style={{
|
||||
display: "flex", alignItems: "center", gap: 8,
|
||||
padding: "10px 12px", borderRadius: "var(--field-radius)",
|
||||
background: "var(--field-bg)", border: "1px solid var(--field-border)",
|
||||
fontSize: "var(--text-md)", color: value ? "var(--text)" : "var(--text-3)",
|
||||
cursor: "pointer", boxShadow: "var(--shadow-sm)",
|
||||
backdropFilter: "blur(var(--surface-blur))",
|
||||
WebkitBackdropFilter: "blur(var(--surface-blur))",
|
||||
...style,
|
||||
}} {...rest}>
|
||||
{leadingIcon && <span style={{ color: "var(--text-3)", display: "flex" }}>{leadingIcon}</span>}
|
||||
<span style={{ flex: 1 }}>{value || placeholder}</span>
|
||||
<Icon name="chevDown" size={14} style={{ color: "var(--text-3)" }} />
|
||||
</div>
|
||||
);
|
||||
|
||||
// ─── Checkbox ────────────────────────────────────────────────
|
||||
const Checkbox = ({ checked, indeterminate, disabled, label, hint, onChange = noop, style }) => (
|
||||
<label style={{
|
||||
display: "flex", alignItems: "flex-start", gap: 10, cursor: disabled ? "not-allowed" : "pointer",
|
||||
opacity: disabled ? 0.5 : 1, ...style,
|
||||
}}>
|
||||
<span
|
||||
role="checkbox"
|
||||
aria-checked={indeterminate ? "mixed" : !!checked}
|
||||
onClick={() => !disabled && onChange(!checked)}
|
||||
style={{
|
||||
width: 16, height: 16, borderRadius: 4, marginTop: 1, flexShrink: 0,
|
||||
border: `1px solid ${checked || indeterminate ? "var(--accent)" : "var(--border-strong)"}`,
|
||||
background: checked || indeterminate ? "var(--accent)" : "var(--surface)",
|
||||
color: "var(--text-on-accent)",
|
||||
display: "inline-flex", alignItems: "center", justifyContent: "center",
|
||||
transition: "background var(--duration), border-color var(--duration)",
|
||||
}}
|
||||
>
|
||||
{checked && !indeterminate && <Icon name="checkOnly" size={11} stroke={2.6}/>}
|
||||
{indeterminate && <div style={{ width: 8, height: 2, background: "currentColor", borderRadius: 1 }}/>}
|
||||
</span>
|
||||
{(label || hint) && (
|
||||
<span style={{ minWidth: 0 }}>
|
||||
{label && <span style={{ fontSize: "var(--text-md)", color: "var(--text)" }}>{label}</span>}
|
||||
{hint && <div style={{ fontSize: "var(--text-xs)", color: "var(--text-3)", marginTop: 2 }}>{hint}</div>}
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
|
||||
// ─── Radio ───────────────────────────────────────────────────
|
||||
const Radio = ({ checked, disabled, label, hint, onChange = noop, style }) => (
|
||||
<label style={{
|
||||
display: "flex", alignItems: "flex-start", gap: 10, cursor: disabled ? "not-allowed" : "pointer",
|
||||
opacity: disabled ? 0.5 : 1, ...style,
|
||||
}}>
|
||||
<span
|
||||
role="radio"
|
||||
aria-checked={!!checked}
|
||||
onClick={() => !disabled && onChange(true)}
|
||||
style={{
|
||||
width: 16, height: 16, borderRadius: "50%", marginTop: 1, flexShrink: 0,
|
||||
border: `1px solid ${checked ? "var(--accent)" : "var(--border-strong)"}`,
|
||||
background: "var(--surface)",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{checked && <span style={{
|
||||
position: "absolute", top: 3, left: 3, right: 3, bottom: 3,
|
||||
background: "var(--accent)", borderRadius: "50%",
|
||||
}}/>}
|
||||
</span>
|
||||
{(label || hint) && (
|
||||
<span style={{ minWidth: 0 }}>
|
||||
{label && <span style={{ fontSize: "var(--text-md)", color: "var(--text)" }}>{label}</span>}
|
||||
{hint && <div style={{ fontSize: "var(--text-xs)", color: "var(--text-3)", marginTop: 2 }}>{hint}</div>}
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
|
||||
// ─── Switch ──────────────────────────────────────────────────
|
||||
const Switch = ({ checked, disabled, onChange = noop, label, hint, style }) => (
|
||||
<label style={{
|
||||
display: "flex", alignItems: "center", gap: 12,
|
||||
cursor: disabled ? "not-allowed" : "pointer", opacity: disabled ? 0.5 : 1, ...style,
|
||||
}}>
|
||||
<span
|
||||
role="switch"
|
||||
aria-checked={!!checked}
|
||||
onClick={() => !disabled && onChange(!checked)}
|
||||
style={{
|
||||
width: 34, height: 20, borderRadius: 999,
|
||||
background: checked ? "var(--accent)" : "var(--surface-alt)",
|
||||
border: `1px solid ${checked ? "var(--accent)" : "var(--border-strong)"}`,
|
||||
position: "relative", flexShrink: 0,
|
||||
transition: "background var(--duration), border-color var(--duration)",
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
position: "absolute", top: 1, left: checked ? 15 : 1,
|
||||
width: 16, height: 16, borderRadius: "50%",
|
||||
background: checked ? "var(--text-on-accent)" : "var(--surface)",
|
||||
boxShadow: "0 1px 3px rgba(0,0,0,0.2)",
|
||||
transition: "left var(--duration) var(--ease)",
|
||||
}}/>
|
||||
</span>
|
||||
{(label || hint) && (
|
||||
<span style={{ flex: 1, minWidth: 0 }}>
|
||||
{label && <div style={{ fontSize: "var(--text-md)", color: "var(--text)" }}>{label}</div>}
|
||||
{hint && <div style={{ fontSize: "var(--text-xs)", color: "var(--text-3)", marginTop: 2 }}>{hint}</div>}
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
|
||||
// ─── Card / Surface ──────────────────────────────────────────
|
||||
// Card paints a `surface` background with border + shadow.
|
||||
// Use `variant="raised"` for shadow-lg, "flat" for no shadow.
|
||||
const Card = ({ children, variant = "default", padding = 20, style, ...rest }) => {
|
||||
const shadows = {
|
||||
default: "var(--shadow-sm)",
|
||||
raised: "var(--shadow)",
|
||||
floating:"var(--shadow-lg)",
|
||||
flat: "none",
|
||||
};
|
||||
return (
|
||||
<div style={{
|
||||
background: "var(--surface)",
|
||||
border: "1px solid var(--border)",
|
||||
borderRadius: "var(--card-radius)",
|
||||
padding,
|
||||
boxShadow: shadows[variant] || shadows.default,
|
||||
backdropFilter: "blur(var(--surface-blur))",
|
||||
WebkitBackdropFilter: "blur(var(--surface-blur))",
|
||||
color: "var(--text)",
|
||||
...style,
|
||||
}} {...rest}>{children}</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CardHeader = ({ title, subtitle, action, style }) => (
|
||||
<div style={{
|
||||
display: "flex", justifyContent: "space-between", alignItems: "flex-start",
|
||||
marginBottom: "var(--space-4)", gap: 16, ...style,
|
||||
}}>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
{title && <div style={{
|
||||
fontSize: "var(--text-lg)", fontWeight: "var(--weight-semibold)",
|
||||
color: "var(--text)", letterSpacing: "-0.01em",
|
||||
}}>{title}</div>}
|
||||
{subtitle && <div style={{
|
||||
fontSize: "var(--text-sm)", color: "var(--text-2)", marginTop: 2,
|
||||
}}>{subtitle}</div>}
|
||||
</div>
|
||||
{action}
|
||||
</div>
|
||||
);
|
||||
|
||||
// ─── Badge / Tag ─────────────────────────────────────────────
|
||||
// tone: neutral | accent | success | warn | danger | info
|
||||
const Badge = ({ children, tone = "neutral", dot, leadingIcon, style }) => {
|
||||
const palette = {
|
||||
neutral: { bg: "var(--surface-alt)", fg: "var(--text-2)", dotColor: "var(--text-3)" },
|
||||
accent: { bg: "var(--accent-soft)", fg: "var(--accent)", dotColor: "var(--accent)" },
|
||||
success: { bg: "var(--success-soft)", fg: "var(--success)", dotColor: "var(--success)" },
|
||||
warn: { bg: "var(--warn-soft)", fg: "var(--warn)", dotColor: "var(--warn)" },
|
||||
danger: { bg: "var(--danger-soft)", fg: "var(--danger)", dotColor: "var(--danger)" },
|
||||
info: { bg: "var(--accent-soft)", fg: "var(--accent)", dotColor: "var(--accent)" },
|
||||
}[tone] || {};
|
||||
return (
|
||||
<span style={{
|
||||
display: "inline-flex", alignItems: "center", gap: 6,
|
||||
padding: "2px 8px", borderRadius: "var(--radius-pill)",
|
||||
background: palette.bg, color: palette.fg,
|
||||
fontSize: "var(--text-xs)", fontWeight: "var(--weight-medium)",
|
||||
whiteSpace: "nowrap", lineHeight: 1.4,
|
||||
...style,
|
||||
}}>
|
||||
{dot && <span style={{ width: 6, height: 6, borderRadius: "50%", background: palette.dotColor }}/>}
|
||||
{leadingIcon}
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
const Tag = Badge; // alias
|
||||
|
||||
// ─── Avatar ──────────────────────────────────────────────────
|
||||
const avatarPalette = ["#d4b8a8", "#e8a87c", "#c8e8a8", "#a8c8e8", "#c8a8e8", "#e8c8a8", "#a8e8c8", "#e8a8c8"];
|
||||
const hashName = (s = "") => {
|
||||
let h = 0; for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0;
|
||||
return avatarPalette[Math.abs(h) % avatarPalette.length];
|
||||
};
|
||||
const Avatar = ({ name = "?", src, size = 32, status, color, ring, style }) => {
|
||||
const initials = name.split(/\s+/).filter(Boolean).map(w => w[0]).slice(0, 2).join("").toUpperCase();
|
||||
return (
|
||||
<span style={{
|
||||
position: "relative", display: "inline-flex", flexShrink: 0,
|
||||
width: size, height: size, borderRadius: "50%",
|
||||
background: color || hashName(name), color: "#3a2820",
|
||||
alignItems: "center", justifyContent: "center",
|
||||
fontSize: Math.round(size * 0.4), fontWeight: 600,
|
||||
boxShadow: ring ? `0 0 0 2px var(--surface), 0 0 0 ${2 + ring}px var(--accent)` : "none",
|
||||
overflow: "hidden", ...style,
|
||||
}}>
|
||||
{src
|
||||
? <img src={src} alt={name} style={{ width: "100%", height: "100%", objectFit: "cover" }}/>
|
||||
: initials}
|
||||
{status && <span style={{
|
||||
position: "absolute", bottom: 0, right: 0,
|
||||
width: Math.max(8, size * 0.28), height: Math.max(8, size * 0.28),
|
||||
borderRadius: "50%", border: "2px solid var(--surface)",
|
||||
background: status === "online" ? "var(--success)" :
|
||||
status === "busy" ? "var(--danger)" : "var(--text-3)",
|
||||
}}/>}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const AvatarStack = ({ items = [], size = 28, max = 4 }) => {
|
||||
const shown = items.slice(0, max);
|
||||
const remaining = items.length - shown.length;
|
||||
return (
|
||||
<div style={{ display: "inline-flex" }}>
|
||||
{shown.map((p, i) => (
|
||||
<Avatar key={i} name={p.name} src={p.src} color={p.color} size={size}
|
||||
style={{ marginLeft: i ? -size * 0.32 : 0, boxShadow: "0 0 0 2px var(--surface)" }}/>
|
||||
))}
|
||||
{remaining > 0 && (
|
||||
<span style={{
|
||||
width: size, height: size, borderRadius: "50%",
|
||||
background: "var(--surface-alt)", color: "var(--text-2)",
|
||||
display: "inline-flex", alignItems: "center", justifyContent: "center",
|
||||
fontSize: Math.round(size * 0.4), fontWeight: 600,
|
||||
marginLeft: -size * 0.32, boxShadow: "0 0 0 2px var(--surface)",
|
||||
}}>+{remaining}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ─── Tabs ────────────────────────────────────────────────────
|
||||
// Controlled: pass `active` (label of active tab) + `onChange`.
|
||||
const Tabs = ({ items = [], active, onChange = noop, style, variant = "underline" }) => {
|
||||
return (
|
||||
<div style={{
|
||||
display: "flex", gap: 4, borderBottom: variant === "underline" ? "1px solid var(--border)" : "none",
|
||||
...style,
|
||||
}}>
|
||||
{items.map(t => {
|
||||
const isActive = t.label === active || t.id === active;
|
||||
if (variant === "pill") {
|
||||
return (
|
||||
<button key={t.id || t.label}
|
||||
onClick={() => onChange(t.id || t.label)}
|
||||
style={{
|
||||
padding: "6px 12px", borderRadius: "var(--radius-pill)",
|
||||
fontFamily: "var(--font-sans)", fontSize: "var(--text-sm)",
|
||||
background: isActive ? "var(--accent)" : "transparent",
|
||||
color: isActive ? "var(--text-on-accent)" : "var(--text-2)",
|
||||
border: "none", cursor: "pointer", fontWeight: 500,
|
||||
display: "inline-flex", alignItems: "center", gap: 6,
|
||||
}}>
|
||||
{t.icon}{t.label}
|
||||
{t.count != null && (
|
||||
<span style={{
|
||||
fontSize: 10, padding: "1px 6px", borderRadius: 999,
|
||||
background: isActive ? "rgba(255,255,255,0.18)" : "var(--surface-alt)",
|
||||
color: "inherit",
|
||||
}}>{t.count}</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<button key={t.id || t.label}
|
||||
onClick={() => onChange(t.id || t.label)}
|
||||
style={{
|
||||
padding: "10px 4px", margin: "0 12px 0 0",
|
||||
fontFamily: "var(--font-sans)", fontSize: "var(--text-md)",
|
||||
fontWeight: "var(--weight-medium)",
|
||||
background: "transparent", border: "none", cursor: "pointer",
|
||||
color: isActive ? "var(--text)" : "var(--text-2)",
|
||||
borderBottom: isActive ? "2px solid var(--accent)" : "2px solid transparent",
|
||||
position: "relative", top: 1,
|
||||
display: "inline-flex", alignItems: "center", gap: 6, whiteSpace: "nowrap",
|
||||
}}>
|
||||
{t.icon}{t.label}
|
||||
{t.count != null && (
|
||||
<span style={{
|
||||
fontSize: 10, padding: "1px 6px", borderRadius: 999,
|
||||
background: isActive ? "var(--accent-soft)" : "var(--surface-alt)",
|
||||
color: isActive ? "var(--accent)" : "var(--text-3)",
|
||||
}}>{t.count}</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ─── Table ───────────────────────────────────────────────────
|
||||
// columns: [{ key, label, width?, align?, render? }]
|
||||
// rows: [{ id, [key]: value, … }]
|
||||
const Table = ({ columns = [], rows = [], selectable, selected = [], onSelectionChange = noop, density = "comfortable" }) => {
|
||||
const padY = density === "compact" ? 8 : 12;
|
||||
const allChecked = rows.length > 0 && selected.length === rows.length;
|
||||
const someChecked = selected.length > 0 && !allChecked;
|
||||
const toggleAll = () => onSelectionChange(allChecked ? [] : rows.map(r => r.id));
|
||||
const toggleOne = (id) => onSelectionChange(
|
||||
selected.includes(id) ? selected.filter(x => x !== id) : [...selected, id]
|
||||
);
|
||||
|
||||
const headerCell = {
|
||||
padding: `10px 12px`, fontSize: "var(--text-xs)",
|
||||
color: "var(--text-3)", fontWeight: "var(--weight-medium)",
|
||||
textTransform: "uppercase", letterSpacing: "0.04em", textAlign: "left",
|
||||
borderBottom: "1px solid var(--border)", background: "var(--surface)",
|
||||
};
|
||||
const bodyCell = {
|
||||
padding: `${padY}px 12px`, fontSize: "var(--text-md)",
|
||||
color: "var(--text)", borderBottom: "1px solid var(--divider)", verticalAlign: "middle",
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
background: "var(--surface)", border: "1px solid var(--border)",
|
||||
borderRadius: "var(--card-radius)", overflow: "hidden",
|
||||
backdropFilter: "blur(var(--surface-blur))",
|
||||
WebkitBackdropFilter: "blur(var(--surface-blur))",
|
||||
}}>
|
||||
<table style={{ width: "100%", borderCollapse: "collapse", fontFamily: "var(--font-sans)" }}>
|
||||
<thead>
|
||||
<tr>
|
||||
{selectable && (
|
||||
<th style={{ ...headerCell, width: 36, paddingRight: 0 }}>
|
||||
<Checkbox checked={allChecked} indeterminate={someChecked} onChange={toggleAll}/>
|
||||
</th>
|
||||
)}
|
||||
{columns.map(c => (
|
||||
<th key={c.key} style={{ ...headerCell, width: c.width, textAlign: c.align || "left" }}>
|
||||
{c.label}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((r, i) => (
|
||||
<tr key={r.id ?? i}>
|
||||
{selectable && (
|
||||
<td style={{ ...bodyCell, paddingRight: 0 }}>
|
||||
<Checkbox checked={selected.includes(r.id)} onChange={() => toggleOne(r.id)}/>
|
||||
</td>
|
||||
)}
|
||||
{columns.map(c => (
|
||||
<td key={c.key} style={{ ...bodyCell, textAlign: c.align || "left" }}>
|
||||
{c.render ? c.render(r) : r[c.key]}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ─── Modal (presentational — wrap your own state) ────────────
|
||||
const Modal = ({ open, onClose = noop, title, description, footer, children, width = 480 }) => {
|
||||
if (!open) return null;
|
||||
return (
|
||||
<div
|
||||
onClick={onClose}
|
||||
style={{
|
||||
position: "fixed", inset: 0, zIndex: 100,
|
||||
background: "rgba(0,0,0,0.5)",
|
||||
display: "flex", alignItems: "center", justifyContent: "center", padding: 24,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
width, maxWidth: "100%", maxHeight: "85vh", overflow: "auto",
|
||||
background: "var(--surface)", color: "var(--text)",
|
||||
border: "1px solid var(--border)", borderRadius: "var(--modal-radius)",
|
||||
boxShadow: "var(--shadow-modal)",
|
||||
backdropFilter: "blur(var(--surface-blur))",
|
||||
WebkitBackdropFilter: "blur(var(--surface-blur))",
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
padding: "var(--space-6)", display: "flex",
|
||||
justifyContent: "space-between", alignItems: "flex-start", gap: 16,
|
||||
}}>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
{title && <h2 style={{
|
||||
margin: 0, fontSize: "var(--text-xl)", fontWeight: "var(--weight-semibold)",
|
||||
letterSpacing: "-0.01em", fontFamily: "var(--font-display)",
|
||||
}}>{title}</h2>}
|
||||
{description && <p style={{
|
||||
margin: "6px 0 0", fontSize: "var(--text-md)", color: "var(--text-2)",
|
||||
}}>{description}</p>}
|
||||
</div>
|
||||
<IconButton name="x" size="sm" onClick={onClose} label="Close"/>
|
||||
</div>
|
||||
{children && <div style={{ padding: "0 var(--space-6) var(--space-6)" }}>{children}</div>}
|
||||
{footer && (
|
||||
<div style={{
|
||||
padding: "var(--space-4) var(--space-6)",
|
||||
borderTop: "1px solid var(--divider)",
|
||||
display: "flex", justifyContent: "flex-end", gap: 8,
|
||||
background: "var(--surface-2)",
|
||||
}}>{footer}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ─── Banner / Alert ──────────────────────────────────────────
|
||||
const Banner = ({ tone = "info", title, children, action, onDismiss, icon, style }) => {
|
||||
const palette = {
|
||||
info: { bg: "var(--accent-soft)", fg: "var(--accent)", iconName: "info" },
|
||||
success: { bg: "var(--success-soft)", fg: "var(--success)", iconName: "checkOnly" },
|
||||
warn: { bg: "var(--warn-soft)", fg: "var(--warn)", iconName: "alert" },
|
||||
danger: { bg: "var(--danger-soft)", fg: "var(--danger)", iconName: "alert" },
|
||||
}[tone];
|
||||
return (
|
||||
<div style={{
|
||||
display: "flex", gap: 12, padding: "12px 16px",
|
||||
borderRadius: "var(--radius)",
|
||||
background: palette.bg, color: "var(--text)",
|
||||
border: `1px solid ${palette.fg}33`,
|
||||
alignItems: "flex-start",
|
||||
...style,
|
||||
}}>
|
||||
<span style={{ color: palette.fg, display: "flex", marginTop: 1 }}>
|
||||
{icon ?? <Icon name={palette.iconName} size={16} stroke={2}/>}
|
||||
</span>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
{title && <div style={{
|
||||
fontSize: "var(--text-md)", fontWeight: "var(--weight-semibold)",
|
||||
color: "var(--text)",
|
||||
}}>{title}</div>}
|
||||
{children && <div style={{
|
||||
fontSize: "var(--text-sm)", color: "var(--text-2)", marginTop: title ? 2 : 0,
|
||||
}}>{children}</div>}
|
||||
</div>
|
||||
{action}
|
||||
{onDismiss && <IconButton name="x" size="sm" onClick={onDismiss} label="Dismiss"/>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ─── Divider ─────────────────────────────────────────────────
|
||||
const Divider = ({ label, vertical, style }) => {
|
||||
if (vertical) return <span style={{ width: 1, alignSelf: "stretch", background: "var(--border)", ...style }}/>;
|
||||
if (!label) return <hr style={{ border: "none", borderTop: "1px solid var(--border)", margin: "var(--space-4) 0", ...style }}/>;
|
||||
return (
|
||||
<div style={{
|
||||
display: "flex", alignItems: "center", gap: 12,
|
||||
fontSize: "var(--text-xs)", color: "var(--text-3)",
|
||||
letterSpacing: "0.08em", textTransform: "uppercase",
|
||||
margin: "var(--space-4) 0", ...style,
|
||||
}}>
|
||||
<div style={{ flex: 1, height: 1, background: "var(--border)" }}/>
|
||||
<span>{label}</span>
|
||||
<div style={{ flex: 1, height: 1, background: "var(--border)" }}/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ─── FieldGroup — horizontal segmented control ───────────────
|
||||
const FieldGroup = ({ options = [], value, onChange = noop, style }) => (
|
||||
<div style={{
|
||||
display: "inline-flex", padding: 3, gap: 2,
|
||||
background: "var(--surface-alt)", border: "1px solid var(--border)",
|
||||
borderRadius: "var(--radius)", ...style,
|
||||
}}>
|
||||
{options.map(o => {
|
||||
const v = typeof o === "string" ? o : o.value;
|
||||
const label = typeof o === "string" ? o : o.label;
|
||||
const sel = v === value;
|
||||
return (
|
||||
<button key={v} onClick={() => onChange(v)} style={{
|
||||
padding: "5px 12px", borderRadius: "calc(var(--radius) - 3px)",
|
||||
fontFamily: "var(--font-sans)", fontSize: "var(--text-sm)", whiteSpace: "nowrap",
|
||||
background: sel ? "var(--surface)" : "transparent",
|
||||
color: sel ? "var(--text)" : "var(--text-2)",
|
||||
border: "none", cursor: "pointer",
|
||||
boxShadow: sel ? "var(--shadow-sm)" : "none",
|
||||
fontWeight: sel ? 500 : 400,
|
||||
}}>{label}</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
// ─── KBD ─────────────────────────────────────────────────────
|
||||
const KBD = ({ children, style }) => (
|
||||
<kbd style={{
|
||||
fontFamily: "var(--font-mono)", fontSize: "var(--text-xs)",
|
||||
padding: "1px 6px", borderRadius: 4,
|
||||
background: "var(--surface-alt)", color: "var(--text-2)",
|
||||
border: "1px solid var(--border)",
|
||||
...style,
|
||||
}}>{children}</kbd>
|
||||
);
|
||||
|
||||
// ─── Exports ─────────────────────────────────────────────────
|
||||
Object.assign(window, {
|
||||
Button, IconButton, Spinner,
|
||||
Field, Input, Textarea, Select, FieldGroup,
|
||||
Checkbox, Radio, Switch,
|
||||
Card, CardHeader, Divider,
|
||||
Badge, Tag, Avatar, AvatarStack,
|
||||
Tabs, Table, Modal, Banner, KBD,
|
||||
});
|
||||
89
design-templates/VIBN (2)/vibn-ai-templates/icons.jsx
Normal file
89
design-templates/VIBN (2)/vibn-ai-templates/icons.jsx
Normal file
@@ -0,0 +1,89 @@
|
||||
// ============================================================
|
||||
// vibn-ai-templates/icons.jsx
|
||||
// ------------------------------------------------------------
|
||||
// A tiny Tabler-style stroke-icon helper + a curated set of
|
||||
// paths used by the components. All inherit `currentColor` so
|
||||
// they re-tint to whatever the parent's CSS color is.
|
||||
//
|
||||
// Usage:
|
||||
// <Icon name="search" />
|
||||
// <Icon path={icons.bell} size={20} stroke={2} />
|
||||
// ============================================================
|
||||
|
||||
const icons = {
|
||||
// Navigation / surface
|
||||
home: <><path d="m3 12 9-9 9 9"/><path d="M5 10v10h14V10"/></>,
|
||||
inbox: <><path d="M22 12h-6l-2 3h-4l-2-3H2"/><path d="M5 5h14l3 7v7H2v-7z"/></>,
|
||||
search: <><circle cx="11" cy="11" r="7"/><path d="m20 20-3-3"/></>,
|
||||
bell: <><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10 21a2 2 0 0 0 4 0"/></>,
|
||||
settings:<><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1 1.7 1.7 0 0 0-.3-1.8L4.2 7a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3 1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8 1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/></>,
|
||||
|
||||
// Objects
|
||||
people: <><circle cx="9" cy="8" r="4"/><path d="M3 21a6 6 0 0 1 12 0"/><circle cx="17" cy="6" r="3"/><path d="M21 17a4 4 0 0 0-6-3.5"/></>,
|
||||
building: <><path d="M3 21h18M5 21V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16"/><path d="M9 7h2M13 7h2M9 11h2M13 11h2M9 15h2M13 15h2"/></>,
|
||||
target: <><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="1"/></>,
|
||||
doc: <><path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><path d="M14 3v6h6"/></>,
|
||||
check: <><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></>,
|
||||
bar: <><path d="M3 3v18h18"/><rect x="7" y="11" width="3" height="7"/><rect x="13" y="7" width="3" height="11"/></>,
|
||||
workflow: <><rect x="3" y="3" width="6" height="6" rx="1"/><rect x="15" y="15" width="6" height="6" rx="1"/><path d="M9 6h6a3 3 0 0 1 3 3v6"/></>,
|
||||
|
||||
// Actions
|
||||
plus: <path d="M12 5v14M5 12h14"/>,
|
||||
x: <path d="M6 6l12 12M18 6L6 18"/>,
|
||||
more: <><circle cx="5" cy="12" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/></>,
|
||||
chevDown:<path d="m6 9 6 6 6-6"/>,
|
||||
chevUp: <path d="m6 15 6-6 6 6"/>,
|
||||
chevLeft:<path d="m15 6-6 6 6 6"/>,
|
||||
chevRight:<path d="m9 6 6 6-6 6"/>,
|
||||
arrow: <path d="M5 12h14M13 5l7 7-7 7"/>,
|
||||
arrowUp: <path d="M12 19V5M5 12l7-7 7 7"/>,
|
||||
arrowDown:<path d="M12 5v14M5 12l7 7 7-7"/>,
|
||||
|
||||
// Status
|
||||
checkOnly: <path d="M5 12l5 5L20 7"/>,
|
||||
alert: <><path d="M12 9v4M12 17v.01"/><path d="M10.3 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.7 3.86a2 2 0 0 0-3.4 0z"/></>,
|
||||
info: <><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></>,
|
||||
eye: <><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/></>,
|
||||
eyeOff: <><path d="M9.88 9.88a3 3 0 1 0 4.24 4.24"/><path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 11 7 11 7a13.16 13.16 0 0 1-1.67 2.68"/><path d="M6.61 6.61A13.526 13.526 0 0 0 1 12s4 7 11 7a9.74 9.74 0 0 0 5.39-1.61"/><path d="M2 2l20 20"/></>,
|
||||
star: <path d="m12 3 2.6 6.2 6.7.5-5.1 4.4 1.6 6.6L12 17.3 6.2 20.7l1.6-6.6L2.7 9.7l6.7-.5z"/>,
|
||||
spark: <path d="M12 3v4M12 17v4M3 12h4M17 12h4M6 6l3 3M15 15l3 3M6 18l3-3M15 9l3-3"/>,
|
||||
bolt: <path d="m13 2-9 13h7l-1 7 9-13h-7z"/>,
|
||||
shield: <path d="M12 2 4 5v7c0 5 3.5 9 8 10 4.5-1 8-5 8-10V5z"/>,
|
||||
|
||||
// Misc
|
||||
briefcase: <><rect x="3" y="7" width="18" height="13" rx="2"/><path d="M8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M3 13h18"/></>,
|
||||
link: <><path d="M10 13a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1"/><path d="M14 11a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1-1"/></>,
|
||||
copy: <><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></>,
|
||||
};
|
||||
|
||||
const Icon = ({ name, path, size = 16, stroke = 1.6, style, className }) => (
|
||||
<svg
|
||||
width={size} height={size} viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" strokeWidth={stroke}
|
||||
strokeLinecap="round" strokeLinejoin="round"
|
||||
style={{ display: "inline-block", verticalAlign: "middle", flexShrink: 0, ...style }}
|
||||
className={className}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{path ?? icons[name]}
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Tiny brand mark — a gradient triangle, the same as we've been
|
||||
// using everywhere. Exported here so consumers don't redraw it.
|
||||
const VibnMark = ({ size = 22 }) => {
|
||||
const id = `vmk_${size}`;
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<defs>
|
||||
<linearGradient id={id} x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stopColor="#6e6cff"/>
|
||||
<stop offset="100%" stopColor="#b15bff"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M3 20 L12 4 L21 20 Z" fill={`url(#${id})`}/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
Object.assign(window, { Icon, icons, VibnMark });
|
||||
399
design-templates/VIBN (2)/vibn-ai-templates/shells.jsx
Normal file
399
design-templates/VIBN (2)/vibn-ai-templates/shells.jsx
Normal file
@@ -0,0 +1,399 @@
|
||||
// ============================================================
|
||||
// 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,
|
||||
});
|
||||
325
design-templates/VIBN (2)/vibn-ai-templates/tokens.css
Normal file
325
design-templates/VIBN (2)/vibn-ai-templates/tokens.css
Normal file
@@ -0,0 +1,325 @@
|
||||
/* ============================================================
|
||||
vibn-ai-templates · tokens.css
|
||||
------------------------------------------------------------
|
||||
The whole library is themed through CSS custom properties.
|
||||
The :root block holds the DEFAULT theme (minimal). Each
|
||||
.theme-* class below overrides a subset to flip aesthetics.
|
||||
------------------------------------------------------------
|
||||
To use:
|
||||
<html class="theme-glass"> → glass theme app-wide
|
||||
<div class="theme-editorial">…</div> → scope to one block
|
||||
============================================================ */
|
||||
|
||||
:root {
|
||||
/* ── Typography ─────────────────────────────────────────── */
|
||||
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||
--font-display: var(--font-sans); /* themes may override */
|
||||
--font-mono: ui-monospace, 'JetBrains Mono', 'SF Mono', Menlo, monospace;
|
||||
|
||||
--text-xs: 11px;
|
||||
--text-sm: 12px;
|
||||
--text-md: 13px;
|
||||
--text-lg: 16px;
|
||||
--text-xl: 20px;
|
||||
--text-2xl: 28px;
|
||||
--text-3xl: 38px;
|
||||
|
||||
--weight-regular: 400;
|
||||
--weight-medium: 500;
|
||||
--weight-semibold: 600;
|
||||
--weight-bold: 700;
|
||||
|
||||
/* ── Spacing (4 px base) ────────────────────────────────── */
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 20px;
|
||||
--space-6: 24px;
|
||||
--space-8: 32px;
|
||||
--space-10: 40px;
|
||||
--space-12: 48px;
|
||||
|
||||
/* ── Radii ──────────────────────────────────────────────── */
|
||||
--radius-xs: 3px;
|
||||
--radius-sm: 5px;
|
||||
--radius: 8px;
|
||||
--radius-lg: 14px;
|
||||
--radius-xl: 22px;
|
||||
--radius-pill: 999px;
|
||||
--button-radius: var(--radius);
|
||||
--field-radius: 7px;
|
||||
--card-radius: 12px;
|
||||
--modal-radius: 16px;
|
||||
|
||||
/* ── Colors · MINIMAL (default light theme) ────────────── */
|
||||
--bg: #f5f5f2;
|
||||
--surface: #ffffff;
|
||||
--surface-2: #fafaf8;
|
||||
--surface-alt: #f1f0eb; /* sidebar / muted regions */
|
||||
--border: #e8e8e3;
|
||||
--border-strong:#d8d8d2;
|
||||
--divider: #ededea;
|
||||
|
||||
--text: #111111;
|
||||
--text-2: #5a5a5e;
|
||||
--text-3: #8a8a90;
|
||||
--text-on-accent: #ffffff;
|
||||
|
||||
--accent: #5e5cff;
|
||||
--accent-2: #b15bff;
|
||||
--accent-soft: #eeedff;
|
||||
--accent-ring: rgba(94,92,255,0.22);
|
||||
|
||||
--success: #16a34a;
|
||||
--success-soft: #dcfce7;
|
||||
--warn: #d97706;
|
||||
--warn-soft: #fef3c7;
|
||||
--danger: #dc2626;
|
||||
--danger-soft: #fee2e2;
|
||||
|
||||
/* ── Surfaces & effects ────────────────────────────────── */
|
||||
--surface-blur: 0px; /* glass theme overrides */
|
||||
--backdrop: transparent; /* glass theme overrides */
|
||||
--grain: none; /* maximalist themes can use */
|
||||
|
||||
--shadow-sm: 0 1px 2px rgba(0,0,0,0.04);
|
||||
--shadow: 0 4px 12px -4px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.04);
|
||||
--shadow-lg: 0 24px 48px -20px rgba(0,0,0,0.16), 0 2px 8px rgba(0,0,0,0.05);
|
||||
--shadow-modal: 0 40px 80px -20px rgba(0,0,0,0.28), 0 2px 8px rgba(0,0,0,0.08);
|
||||
--shadow-focus: 0 0 0 3px var(--accent-ring);
|
||||
|
||||
/* ── Buttons ───────────────────────────────────────────── */
|
||||
--button-bg: #111111;
|
||||
--button-fg: #ffffff;
|
||||
--button-border: #111111;
|
||||
--button-hover: #2a2a2a;
|
||||
--button-press: #000000;
|
||||
|
||||
--button-secondary-bg: #ffffff;
|
||||
--button-secondary-fg: #111111;
|
||||
--button-secondary-border: var(--border);
|
||||
--button-secondary-hover: #f6f5f0;
|
||||
|
||||
--button-ghost-fg: var(--text);
|
||||
--button-ghost-hover: #00000008;
|
||||
|
||||
/* ── Inputs ────────────────────────────────────────────── */
|
||||
--field-bg: #ffffff;
|
||||
--field-border: var(--border);
|
||||
--field-text: var(--text);
|
||||
--field-placeholder: var(--text-3);
|
||||
--field-focus-ring: var(--shadow-focus);
|
||||
|
||||
/* ── Animation ─────────────────────────────────────────── */
|
||||
--duration-fast: 120ms;
|
||||
--duration: 180ms;
|
||||
--duration-slow: 260ms;
|
||||
--ease: cubic-bezier(0.2, 0.7, 0.3, 1);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
THEME: minimal (default, same as :root)
|
||||
The class exists so consumers can name-toggle.
|
||||
============================================================ */
|
||||
.theme-minimal {}
|
||||
|
||||
|
||||
/* ============================================================
|
||||
THEME: dark — Vercel / Stripe / Linear web school
|
||||
============================================================ */
|
||||
.theme-dark {
|
||||
--bg: #0a0a0a;
|
||||
--surface: #101015;
|
||||
--surface-2: #16161d;
|
||||
--surface-alt: #0a0a0d;
|
||||
--border: #1f1f25;
|
||||
--border-strong:#2a2a32;
|
||||
--divider: #1a1a20;
|
||||
|
||||
--text: #fafafa;
|
||||
--text-2: #a8a8b0;
|
||||
--text-3: #6a6a72;
|
||||
--text-on-accent: #0a0a0a;
|
||||
|
||||
--accent: #ffffff;
|
||||
--accent-2: #b15bff;
|
||||
--accent-soft: rgba(255,255,255,0.08);
|
||||
--accent-ring: rgba(255,255,255,0.24);
|
||||
|
||||
--success: #4ade80;
|
||||
--success-soft: rgba(74,222,128,0.14);
|
||||
--warn: #f59e0b;
|
||||
--warn-soft: rgba(245,158,11,0.14);
|
||||
--danger: #ff4d5e;
|
||||
--danger-soft: rgba(255,77,94,0.16);
|
||||
|
||||
--shadow-sm: 0 1px 2px rgba(0,0,0,0.5);
|
||||
--shadow: 0 4px 12px rgba(0,0,0,0.4);
|
||||
--shadow-lg: 0 24px 60px -20px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.04);
|
||||
--shadow-modal: 0 40px 100px -20px rgba(0,0,0,0.8);
|
||||
|
||||
--button-bg: #ffffff;
|
||||
--button-fg: #0a0a0a;
|
||||
--button-border: #ffffff;
|
||||
--button-hover: #e8e8e8;
|
||||
--button-press: #d4d4d4;
|
||||
|
||||
--button-secondary-bg: #16161d;
|
||||
--button-secondary-fg: #fafafa;
|
||||
--button-secondary-border: var(--border);
|
||||
--button-secondary-hover: #1f1f28;
|
||||
|
||||
--button-ghost-fg: var(--text);
|
||||
--button-ghost-hover: rgba(255,255,255,0.05);
|
||||
|
||||
--field-bg: #16161d;
|
||||
--field-border: var(--border);
|
||||
--field-placeholder: var(--text-3);
|
||||
}
|
||||
|
||||
|
||||
/* ============================================================
|
||||
THEME: glass — vibrant aurora bg + frosted surfaces
|
||||
============================================================ */
|
||||
.theme-glass {
|
||||
--bg: #08081a;
|
||||
--surface: rgba(255,255,255,0.06);
|
||||
--surface-2: rgba(255,255,255,0.10);
|
||||
--surface-alt: rgba(255,255,255,0.04);
|
||||
--border: rgba(255,255,255,0.14);
|
||||
--border-strong:rgba(255,255,255,0.22);
|
||||
--divider: rgba(255,255,255,0.08);
|
||||
|
||||
--text: #ffffff;
|
||||
--text-2: rgba(255,255,255,0.70);
|
||||
--text-3: rgba(255,255,255,0.50);
|
||||
--text-on-accent: #08081a;
|
||||
|
||||
--accent: #ffffff;
|
||||
--accent-2: #b15bff;
|
||||
--accent-soft: rgba(255,255,255,0.12);
|
||||
--accent-ring: rgba(122,120,255,0.40);
|
||||
|
||||
--success: #7aff66;
|
||||
--success-soft: rgba(122,255,102,0.14);
|
||||
--warn: #ffce5b;
|
||||
--warn-soft: rgba(255,206,91,0.14);
|
||||
--danger: #ff5b6b;
|
||||
--danger-soft: rgba(255,91,107,0.14);
|
||||
|
||||
--button-radius: var(--radius-pill);
|
||||
--field-radius: 10px;
|
||||
--card-radius: 22px;
|
||||
--modal-radius: 22px;
|
||||
|
||||
--surface-blur: 20px;
|
||||
--backdrop: radial-gradient(60% 50% at 20% 20%, rgba(122,120,255,0.55), transparent 60%),
|
||||
radial-gradient(50% 50% at 80% 30%, rgba(177,91,255,0.50), transparent 60%),
|
||||
radial-gradient(70% 60% at 50% 100%, rgba(0,229,179,0.35), transparent 60%),
|
||||
#08081a;
|
||||
|
||||
--shadow-sm: 0 1px 2px rgba(0,0,0,0.4);
|
||||
--shadow: 0 10px 40px -10px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.10);
|
||||
--shadow-lg: 0 30px 80px -30px rgba(0,0,0,0.6), inset 0 1px 0 rgba(255,255,255,0.12);
|
||||
--shadow-modal: 0 40px 100px -30px rgba(0,0,0,0.7), inset 0 1px 0 rgba(255,255,255,0.14);
|
||||
|
||||
--button-bg: #ffffff;
|
||||
--button-fg: #08081a;
|
||||
--button-border: transparent;
|
||||
--button-hover: rgba(255,255,255,0.92);
|
||||
--button-press: rgba(255,255,255,0.84);
|
||||
|
||||
--button-secondary-bg: rgba(255,255,255,0.08);
|
||||
--button-secondary-fg: #ffffff;
|
||||
--button-secondary-border: var(--border);
|
||||
--button-secondary-hover: rgba(255,255,255,0.14);
|
||||
|
||||
--button-ghost-fg: #ffffff;
|
||||
--button-ghost-hover: rgba(255,255,255,0.06);
|
||||
|
||||
--field-bg: rgba(255,255,255,0.06);
|
||||
--field-border: var(--border);
|
||||
--field-placeholder: var(--text-3);
|
||||
}
|
||||
|
||||
|
||||
/* ============================================================
|
||||
THEME: editorial — warm paper, serif display, hairline rules
|
||||
============================================================ */
|
||||
.theme-editorial {
|
||||
--font-display: 'DM Serif Display', 'Times New Roman', Times, serif;
|
||||
|
||||
--bg: #f3eee2;
|
||||
--surface: #fbf8f0;
|
||||
--surface-2: #f7f2e6;
|
||||
--surface-alt: #ece6d6;
|
||||
--border: #d8d0bc;
|
||||
--border-strong:#b8a988;
|
||||
--divider: #e2d9c4;
|
||||
|
||||
--text: #1c1a14;
|
||||
--text-2: #5a5044;
|
||||
--text-3: #8a7d6a;
|
||||
--text-on-accent: #fbf8f0;
|
||||
|
||||
--accent: #1c1a14;
|
||||
--accent-2: #b85c28; /* terracotta */
|
||||
--accent-soft: #e8e1cd;
|
||||
--accent-ring: rgba(28,26,20,0.18);
|
||||
|
||||
--success: #3f7a3a;
|
||||
--success-soft: #dde9d4;
|
||||
--warn: #a86b14;
|
||||
--warn-soft: #f3e7c4;
|
||||
--danger: #a32a1e;
|
||||
--danger-soft: #f1d6cf;
|
||||
|
||||
--button-radius: 3px;
|
||||
--field-radius: 3px;
|
||||
--card-radius: 4px;
|
||||
--modal-radius: 4px;
|
||||
|
||||
--shadow-sm: 0 1px 0 rgba(28,26,20,0.06);
|
||||
--shadow: 0 1px 0 rgba(28,26,20,0.06), 0 6px 24px -12px rgba(28,26,20,0.12);
|
||||
--shadow-lg: 0 14px 36px -16px rgba(28,26,20,0.18), 0 1px 0 rgba(28,26,20,0.06);
|
||||
--shadow-modal: 0 30px 60px -20px rgba(28,26,20,0.28);
|
||||
|
||||
--button-bg: #1c1a14;
|
||||
--button-fg: #fbf8f0;
|
||||
--button-border: #1c1a14;
|
||||
--button-hover: #2f2a20;
|
||||
--button-press: #000000;
|
||||
|
||||
--button-secondary-bg: transparent;
|
||||
--button-secondary-fg: #1c1a14;
|
||||
--button-secondary-border: #1c1a14; /* thick rule */
|
||||
--button-secondary-hover: rgba(28,26,20,0.06);
|
||||
|
||||
--button-ghost-fg: var(--text);
|
||||
--button-ghost-hover: rgba(28,26,20,0.06);
|
||||
|
||||
--field-bg: #fbf8f0;
|
||||
--field-border: #1c1a14; /* hairline rule */
|
||||
--field-placeholder: var(--text-3);
|
||||
}
|
||||
|
||||
|
||||
/* ============================================================
|
||||
Body backdrop helper — paint --backdrop on the page root.
|
||||
Glass theme uses this to show the aurora wash.
|
||||
============================================================ */
|
||||
.vibn-app {
|
||||
font-family: var(--font-sans);
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
min-height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.vibn-app::before {
|
||||
content: "";
|
||||
position: absolute; inset: 0;
|
||||
background: var(--backdrop);
|
||||
z-index: 0; pointer-events: none;
|
||||
}
|
||||
.vibn-app > * { position: relative; z-index: 1; }
|
||||
Reference in New Issue
Block a user