afe36fd347
Claude Design export for SD-0001 (login + create-storefront surfaces): wf-*/hf-* screens, design-system bundle, offline preview. First artifact in the ui/designs/ collection (ecomm#8). Also ignore .DS_Store. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
304 lines
13 KiB
React
304 lines
13 KiB
React
// wf-kit.jsx — Wiggleverse ecomm wireframe primitives.
|
|
// Mid-fi: real Wiggleverse tokens + composed DS components (Button, Soul, Eyebrow, Tag, Notice).
|
|
// Everything exported to window at the bottom so sibling babel scripts can use it.
|
|
|
|
const DS = window.WiggleverseDesignSystem_94cd80;
|
|
const { Button, Soul, Eyebrow, Tag, Notice } = DS;
|
|
|
|
// ---- token shorthands (resolved from the linked token stylesheets) -------
|
|
const T = {
|
|
sky: 'var(--wv-midnight)',
|
|
raised: 'var(--wv-indigo)',
|
|
raisedHi: 'var(--wv-indigo-2)',
|
|
night: 'var(--wv-night)',
|
|
star: 'var(--wv-starlight)',
|
|
soft: 'var(--wv-starlight-78)',
|
|
mute: 'var(--wv-starlight-60)',
|
|
lilac: 'var(--wv-lilac)',
|
|
gold: 'var(--wv-gold)',
|
|
goldInk: 'var(--wv-gold-ink)',
|
|
hair: 'var(--border-soft)',
|
|
hairCard: 'var(--border-card)',
|
|
disp: 'var(--wv-font-display)',
|
|
body: 'var(--wv-font-body)',
|
|
};
|
|
|
|
// faint scattered starfield — the brand's one decorative texture (off-center, low alpha)
|
|
const STARFIELD =
|
|
'radial-gradient(1.4px 1.4px at 18% 24%, rgba(237,234,255,.5), transparent),' +
|
|
'radial-gradient(1.4px 1.4px at 73% 16%, rgba(237,234,255,.35), transparent),' +
|
|
'radial-gradient(1.2px 1.2px at 88% 52%, rgba(155,140,255,.45), transparent),' +
|
|
'radial-gradient(1.6px 1.6px at 32% 70%, rgba(237,234,255,.4), transparent),' +
|
|
'radial-gradient(1.2px 1.2px at 58% 84%, rgba(237,234,255,.3), transparent),' +
|
|
'radial-gradient(1.3px 1.3px at 8% 58%, rgba(155,140,255,.4), transparent),' +
|
|
'radial-gradient(1.1px 1.1px at 46% 40%, rgba(237,234,255,.28), transparent)';
|
|
|
|
// ── Screen — fills the artboard, the midnight ground, column flow ──────────
|
|
function Screen({ children, starfield = false, bg = T.sky, style = {} }) {
|
|
return (
|
|
<div style={{
|
|
width: '100%', height: '100%', boxSizing: 'border-box',
|
|
background: starfield ? `${STARFIELD}, ${bg}` : bg,
|
|
color: T.star, fontFamily: T.body,
|
|
display: 'flex', flexDirection: 'column', position: 'relative', overflow: 'hidden',
|
|
...style,
|
|
}}>{children}</div>
|
|
);
|
|
}
|
|
|
|
// ── Wordmark — the mark tile + "ecomm" (Space Grotesk) ─────────────────────
|
|
function Wordmark({ size = 26, muted = false }) {
|
|
return (
|
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: size * 0.42 }}>
|
|
<img src="assets/mark-tile.svg" width={size} height={size}
|
|
style={{ borderRadius: size * 0.22, display: 'block', opacity: muted ? 0.85 : 1 }} alt="" />
|
|
<span style={{
|
|
fontFamily: T.disp, fontWeight: 700, fontSize: size * 0.86,
|
|
letterSpacing: '-0.015em', color: T.star,
|
|
}}>ecomm</span>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
// ── TopBar — the chrome shared by every authenticated surface ──────────────
|
|
function TopBar({ left, right, pad = '0 32px', height = 64 }) {
|
|
return (
|
|
<header style={{
|
|
flex: '0 0 auto', height, padding: pad,
|
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
borderBottom: `1px solid ${T.hair}`,
|
|
background: 'rgba(14,18,48,.6)', backdropFilter: 'blur(10px)',
|
|
}}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>{left}</div>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 18 }}>{right}</div>
|
|
</header>
|
|
);
|
|
}
|
|
|
|
// account chip used at top-right of authenticated surfaces
|
|
function AccountChip({ email = 'mara@studiofern.com', menu = false }) {
|
|
return (
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 14, position: 'relative' }}>
|
|
<span style={{ fontFamily: T.body, fontSize: 14, color: T.soft }}>{email}</span>
|
|
<span style={{ width: 1, height: 18, background: T.hair }} />
|
|
<button style={{
|
|
fontFamily: T.disp, fontWeight: 500, fontSize: 14, color: T.star,
|
|
background: 'transparent', border: 'none', cursor: 'pointer', padding: 0,
|
|
}}>Sign out</button>
|
|
{menu && (
|
|
<div style={{
|
|
position: 'absolute', top: 30, right: 0, minWidth: 200, zIndex: 9,
|
|
background: T.raised, border: `1px solid ${T.hairCard}`, borderRadius: 12,
|
|
padding: 6, boxShadow: '0 8px 24px rgba(9,12,34,.4)',
|
|
}}>
|
|
<MenuRow>{email}</MenuRow>
|
|
<div style={{ borderTop: `1px solid ${T.hair}`, margin: '6px 4px' }} />
|
|
<MenuRow strong>Sign out</MenuRow>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
function MenuRow({ children, strong }) {
|
|
return <div style={{
|
|
padding: '8px 12px', fontFamily: T.body, fontSize: 13.5,
|
|
color: strong ? T.star : T.mute, fontWeight: strong ? 600 : 400, borderRadius: 7,
|
|
}}>{children}</div>;
|
|
}
|
|
|
|
// ── Field — label + input + helper/error. Real token styling. ──────────────
|
|
function Field({ label, placeholder, value, helper, error, focus = false, optional = false }) {
|
|
return (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 7 }}>
|
|
<label style={{
|
|
fontFamily: T.body, fontWeight: 500, fontSize: 13.5, color: T.soft,
|
|
display: 'flex', justifyContent: 'space-between', alignItems: 'baseline',
|
|
}}>
|
|
<span>{label}</span>
|
|
{optional && <span style={{ fontSize: 12.5, color: T.mute, fontWeight: 400 }}>optional</span>}
|
|
</label>
|
|
<div style={{
|
|
height: 50, borderRadius: 10, padding: '0 15px', display: 'flex', alignItems: 'center',
|
|
background: 'rgba(237,234,255,.04)',
|
|
border: `1.5px solid ${error ? T.gold : focus ? T.gold : T.hair}`,
|
|
boxShadow: focus ? '0 0 0 3px var(--wv-gold-28)' : 'none',
|
|
fontFamily: T.body, fontSize: 15.5,
|
|
color: value ? T.star : T.mute,
|
|
}}>
|
|
{value || placeholder}
|
|
{focus && !value && <span style={{
|
|
width: 1.5, height: 20, marginLeft: 1, background: T.gold,
|
|
}} />}
|
|
</div>
|
|
{error && <FieldNote tone="attn">{error}</FieldNote>}
|
|
{helper && !error && <FieldNote>{helper}</FieldNote>}
|
|
</div>
|
|
);
|
|
}
|
|
function FieldNote({ children, tone }) {
|
|
return <span style={{
|
|
fontFamily: T.body, fontSize: 13, lineHeight: 1.4,
|
|
color: tone === 'attn' ? T.gold : T.mute,
|
|
display: 'flex', gap: 6, alignItems: 'baseline',
|
|
}}>
|
|
{tone === 'attn' && <span aria-hidden="true" style={{ color: T.gold }}>◆</span>}
|
|
<span>{children}</span>
|
|
</span>;
|
|
}
|
|
|
|
// ── CodeField — one-time-code entry, 6 cells ───────────────────────────────
|
|
function CodeField({ filled = 0, error = false }) {
|
|
const cells = [0, 1, 2, 3, 4, 5];
|
|
const vals = ['4', '1', '9', '', '', ''];
|
|
return (
|
|
<div style={{ display: 'flex', gap: 10 }}>
|
|
{cells.map((i) => {
|
|
const active = i === filled;
|
|
const has = i < filled;
|
|
return (
|
|
<div key={i} style={{
|
|
width: 50, height: 60, borderRadius: 10,
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
background: 'rgba(237,234,255,.04)',
|
|
border: `1.5px solid ${error ? T.gold : active ? T.gold : T.hair}`,
|
|
boxShadow: active ? '0 0 0 3px var(--wv-gold-28)' : 'none',
|
|
fontFamily: T.disp, fontWeight: 500, fontSize: 24, color: T.star,
|
|
}}>
|
|
{has ? vals[i] : (active ? <span style={{ width: 1.5, height: 26, background: T.gold }} /> : '')}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ── Banner — inline status. attn = gold (no red in palette); info = lilac ──
|
|
function Banner({ children, tone = 'attn', title }) {
|
|
const attn = tone === 'attn';
|
|
return (
|
|
<div style={{
|
|
display: 'flex', gap: 12, padding: '13px 16px', borderRadius: 12,
|
|
background: attn ? 'rgba(244,199,107,.10)' : 'var(--wv-lilac-08)',
|
|
border: `1px solid ${attn ? 'var(--wv-gold-40)' : 'var(--wv-lilac-18)'}`,
|
|
fontFamily: T.body, alignItems: 'flex-start',
|
|
}}>
|
|
<span aria-hidden="true" style={{ color: attn ? T.gold : T.lilac, fontSize: 14, lineHeight: '21px' }}>
|
|
{attn ? '◆' : '◇'}
|
|
</span>
|
|
<div style={{ fontSize: 14, lineHeight: 1.5, color: T.soft }}>
|
|
{title && <div style={{ color: T.star, fontWeight: 600, marginBottom: 2 }}>{title}</div>}
|
|
{children}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ── AuthCard — the centered raised surface for Direction A ─────────────────
|
|
function AuthCard({ children, width = 420 }) {
|
|
return (
|
|
<div style={{
|
|
width, maxWidth: '100%', boxSizing: 'border-box',
|
|
background: T.raised, border: `1px solid ${T.hairCard}`, borderRadius: 14,
|
|
padding: '38px 38px 34px',
|
|
display: 'flex', flexDirection: 'column', gap: 22,
|
|
}}>{children}</div>
|
|
);
|
|
}
|
|
|
|
// ── BrandRail — the persistent identity panel for Direction B ──────────────
|
|
function BrandRail({ compact = false, soul = 'Your storefront is yours.' }) {
|
|
return (
|
|
<aside style={{
|
|
flex: compact ? '0 0 auto' : '0 0 42%',
|
|
background: `${STARFIELD}, ${T.night}`,
|
|
borderRight: compact ? 'none' : `1px solid ${T.hair}`,
|
|
borderBottom: compact ? `1px solid ${T.hair}` : 'none',
|
|
padding: compact ? '26px 28px' : '44px 46px',
|
|
display: 'flex', flexDirection: 'column', justifyContent: compact ? 'flex-start' : 'space-between',
|
|
position: 'relative', overflow: 'hidden', gap: compact ? 18 : 0,
|
|
}}>
|
|
<Wordmark size={compact ? 24 : 28} />
|
|
{!compact && (
|
|
<div style={{ maxWidth: 360 }}>
|
|
<Soul size="lg" tone="gold" as="p">Treat humans as humans —<br />everything else follows.</Soul>
|
|
<p style={{ fontFamily: T.body, fontSize: 14.5, lineHeight: 1.6, color: T.mute, marginTop: 20 }}>
|
|
Honest commerce, built on one shared set of ethics. No trial clock, no plan wall — just your storefront.
|
|
</p>
|
|
</div>
|
|
)}
|
|
{compact && <Soul size="sm" tone="gold" as="p">{soul}</Soul>}
|
|
{!compact && (
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10, fontFamily: T.body, fontSize: 12, color: T.mute }}>
|
|
<span style={{ width: 22, height: 1, background: T.hair }} />
|
|
<span>A Wiggleverse line · Open Core</span>
|
|
</div>
|
|
)}
|
|
</aside>
|
|
);
|
|
}
|
|
|
|
// ── Footer — minimal project identity + the standing dedication ────────────
|
|
function Footer({ onDark = true }) {
|
|
return (
|
|
<footer style={{
|
|
flex: '0 0 auto', padding: '20px 32px', borderTop: `1px solid ${T.hair}`,
|
|
display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 16,
|
|
fontFamily: T.body, fontSize: 12.5, color: T.mute,
|
|
}}>
|
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 9 }}>
|
|
<img src="assets/mark-mono-gold.svg" width={16} height={16} alt="" style={{ opacity: 0.8 }} />
|
|
ecomm · a Wiggleverse line
|
|
</span>
|
|
<span>Privacy · Open Core</span>
|
|
</footer>
|
|
);
|
|
}
|
|
|
|
// ── Section wrapper used in the canvas to title state stacks ───────────────
|
|
function StateBlock({ label, note, children, h }) {
|
|
return (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 9 }}>
|
|
<Tag variant="soon">{label}</Tag>
|
|
{note && <span style={{ fontFamily: T.body, fontSize: 12.5, color: T.mute }}>{note}</span>}
|
|
</div>
|
|
<div style={{
|
|
background: T.raised, border: `1px solid ${T.hairCard}`, borderRadius: 12,
|
|
padding: 22, height: h, boxSizing: 'border-box',
|
|
}}>{children}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ── Honest-empty admin body — the heart of PUC-8 ───────────────────────────
|
|
function EmptyAdmin({ storefront = 'Studio Fern', compact = false }) {
|
|
return (
|
|
<div style={{
|
|
flex: 1, display: 'flex', flexDirection: 'column',
|
|
alignItems: 'flex-start', justifyContent: 'center',
|
|
padding: compact ? '32px 26px' : '0 64px', maxWidth: 720, margin: compact ? 0 : '0 auto',
|
|
width: '100%', boxSizing: 'border-box',
|
|
}}>
|
|
<Eyebrow style={{ margin: '0 0 14px' }}>Your storefront</Eyebrow>
|
|
<h1 style={{
|
|
fontFamily: T.disp, fontWeight: 700, letterSpacing: '-0.015em', lineHeight: 1.05,
|
|
fontSize: compact ? 30 : 'var(--text-h1)', color: T.star, margin: '0 0 16px',
|
|
}}>{storefront}</h1>
|
|
<p style={{
|
|
fontFamily: T.body, fontSize: compact ? 15 : 17, lineHeight: 1.6, color: T.soft,
|
|
maxWidth: 480, margin: 0,
|
|
}}>
|
|
There's nothing to manage yet. Catalog, orders, and settings will appear
|
|
here as ecomm grows.
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
Object.assign(window, {
|
|
WF_T: T, WF_STARFIELD: STARFIELD,
|
|
Screen, Wordmark, TopBar, AccountChip, MenuRow, Field, FieldNote, CodeField,
|
|
Banner, AuthCard, BrandRail, Footer, StateBlock, EmptyAdmin,
|
|
WF_DS: DS,
|
|
});
|