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>
268 lines
15 KiB
React
268 lines
15 KiB
React
// hf-kit.jsx — Wiggleverse ecomm HI-FI primitives (Direction A · Quiet centered).
|
||
// Depth = layered surfaces + hairlines (the brand avoids shadows). Hover = 1–3px lift.
|
||
// Merchant persona: Ben's Bets · ben@bensbets.com
|
||
|
||
const HFDS = window.WiggleverseDesignSystem_94cd80;
|
||
const { Button: HFButton, Soul: HFSoul, Eyebrow: HFEyebrow, Tag: HFTag } = HFDS;
|
||
|
||
// Resolve a brand asset: blob URL when bundled offline (window.__resources), else relative path.
|
||
const HF_ASSET = (id, path) => (window.__resources && window.__resources[id]) || path;
|
||
window.HF_ASSET = HF_ASSET;
|
||
|
||
// ---- token shorthands ----------------------------------------------------
|
||
const H = {
|
||
sky: 'var(--wv-midnight)', night: 'var(--wv-night)',
|
||
raised: 'var(--wv-indigo)', raisedHi: 'var(--wv-indigo-2)',
|
||
star: 'var(--wv-starlight)', soft: 'var(--wv-starlight-78)',
|
||
mute: 'var(--wv-starlight-60)', faint: 'var(--wv-starlight-55)',
|
||
lilac: 'var(--wv-lilac)', violet: 'var(--wv-violet)',
|
||
gold: 'var(--wv-gold)', goldHi: 'var(--wv-gold-hi)', goldInk: 'var(--wv-gold-ink)',
|
||
hair: 'var(--border-soft)', hairCard: 'var(--border-card)', hairStrong: 'var(--border-strong)',
|
||
disp: 'var(--wv-font-display)', body: 'var(--wv-font-body)', soul: 'var(--wv-font-human)',
|
||
shadow: 'var(--shadow-soft)',
|
||
};
|
||
const MENU_EMAIL = 'ben@bensbets.com';
|
||
const STORE = "Ben's Bets";
|
||
|
||
// barely-there starfield (Direction A: restrained) — a few asymmetric nodes
|
||
const HF_STARS =
|
||
'radial-gradient(1.3px 1.3px at 16% 22%, rgba(237,234,255,.34), transparent),' +
|
||
'radial-gradient(1.2px 1.2px at 78% 14%, rgba(237,234,255,.22), transparent),' +
|
||
'radial-gradient(1.1px 1.1px at 88% 46%, rgba(155,140,255,.30), transparent),' +
|
||
'radial-gradient(1.3px 1.3px at 30% 72%, rgba(237,234,255,.24), transparent),' +
|
||
'radial-gradient(1.1px 1.1px at 62% 86%, rgba(237,234,255,.18), transparent),' +
|
||
'radial-gradient(1.2px 1.2px at 7% 56%, rgba(155,140,255,.26), transparent)';
|
||
// gold horizon — the brand's warmth, a low glow at the bottom of the sky
|
||
const HF_HORIZON = 'radial-gradient(120% 70% at 50% 142%, rgba(244,199,107,.16) 0%, rgba(244,199,107,.05) 38%, transparent 64%)';
|
||
|
||
// One-time injected stylesheet: hover + focus affordances (subtle motion only).
|
||
function HFStyles() {
|
||
return (
|
||
<style dangerouslySetInnerHTML={{ __html: `
|
||
.hf { --r: var(--radius-card); }
|
||
.hf a.lk { color: var(--wv-lilac); text-decoration: none; transition: color .15s ease; }
|
||
.hf a.lk:hover { color: var(--wv-gold); }
|
||
.hf .row-lift { transition: transform .15s ease, border-color .15s ease, background .15s ease; }
|
||
.hf .row-lift:hover { transform: translateY(-1px); border-color: var(--border-strong); background: var(--wv-indigo-2); }
|
||
.hf .field { transition: border-color .15s ease, box-shadow .15s ease, background .15s ease; }
|
||
.hf .field:hover { border-color: var(--border-strong); }
|
||
.hf .field.is-focus { border-color: var(--wv-gold); box-shadow: 0 0 0 3px var(--wv-gold-28); background: rgba(237,234,255,.06); }
|
||
.hf .field.is-attn { border-color: var(--wv-gold); }
|
||
.hf .caret { display:inline-block; width:1.5px; height:1.1em; background: var(--wv-gold); margin-left:1px; vertical-align:-2px; animation: hfblink 1.1s steps(1) infinite; }
|
||
@keyframes hfblink { 50% { opacity: 0; } }
|
||
.hf .signout { font-family: var(--wv-font-display); font-weight:500; font-size:14px; color: var(--wv-starlight); background:transparent; border:none; cursor:pointer; padding:0; transition: color .15s ease; }
|
||
.hf .signout:hover { color: var(--wv-gold); }
|
||
.hf .navitem { transition: background .15s ease, color .15s ease; }
|
||
` }} />
|
||
);
|
||
}
|
||
|
||
// ── Screen ground ──────────────────────────────────────────────────────────
|
||
function HFScreen({ children, horizon = false, stars = true, bg = H.sky, style = {} }) {
|
||
const layers = [];
|
||
if (stars) layers.push(HF_STARS);
|
||
if (horizon) layers.unshift(HF_HORIZON);
|
||
const background = layers.length ? `${layers.join(',')}, ${bg}` : bg;
|
||
return (
|
||
<div className="hf" style={{
|
||
width: '100%', height: '100%', boxSizing: 'border-box', background,
|
||
color: H.star, fontFamily: H.body, display: 'flex', flexDirection: 'column',
|
||
position: 'relative', overflow: 'hidden', ...style,
|
||
}}>{children}</div>
|
||
);
|
||
}
|
||
|
||
// ── Wordmark — mark tile + "ecomm" in Space Grotesk ───────────────────────
|
||
function HFWordmark({ size = 26, sub = false }) {
|
||
return (
|
||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: size * 0.42 }}>
|
||
<img src={HF_ASSET('markTile', 'assets/mark-tile.svg')} width={size} height={size}
|
||
style={{ borderRadius: size * 0.22, display: 'block' }} alt="" />
|
||
<span style={{ display: 'inline-flex', alignItems: 'baseline', gap: size * 0.3 }}>
|
||
<span style={{ fontFamily: H.disp, fontWeight: 700, fontSize: size * 0.86, letterSpacing: '-0.015em', color: H.star }}>ecomm</span>
|
||
{sub && <span style={{ fontFamily: H.body, fontSize: size * 0.42, color: H.mute, letterSpacing: '0.02em' }}>by Wiggleverse</span>}
|
||
</span>
|
||
</span>
|
||
);
|
||
}
|
||
|
||
// ── TopBar — glass chrome ──────────────────────────────────────────────────
|
||
function HFTopBar({ left, right, pad = '0 36px', height = 68 }) {
|
||
return (
|
||
<header style={{
|
||
flex: '0 0 auto', height, padding: pad, display: 'flex', alignItems: 'center',
|
||
justifyContent: 'space-between', borderBottom: `1px solid ${H.hair}`,
|
||
background: 'var(--glass-sky)', backdropFilter: 'blur(var(--glass-blur))',
|
||
position: 'relative', zIndex: 5,
|
||
}}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>{left}</div>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 18 }}>{right}</div>
|
||
</header>
|
||
);
|
||
}
|
||
|
||
function HFAccountChip({ email = MENU_EMAIL, menu = false }) {
|
||
return (
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 14, position: 'relative' }}>
|
||
<span style={{ width: 30, height: 30, borderRadius: 15, background: 'var(--wv-lilac-16)', border: `1px solid ${H.hairCard}`, display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: H.disp, fontWeight: 700, fontSize: 13, color: H.star }}>B</span>
|
||
<span style={{ fontFamily: H.body, fontSize: 14, color: H.soft }}>{email}</span>
|
||
<span style={{ width: 1, height: 18, background: H.hair }} />
|
||
<button className="signout">Sign out</button>
|
||
{menu && (
|
||
<div style={{ position: 'absolute', top: 40, right: 0, minWidth: 216, zIndex: 9, background: H.raised, border: `1px solid ${H.hairCard}`, borderRadius: 12, padding: 7, boxShadow: H.shadow }}>
|
||
<div style={{ padding: '9px 12px' }}>
|
||
<div style={{ fontFamily: H.disp, fontWeight: 500, fontSize: 13.5, color: H.star }}>Ben</div>
|
||
<div style={{ fontFamily: H.body, fontSize: 12.5, color: H.mute }}>{email}</div>
|
||
</div>
|
||
<div style={{ borderTop: `1px solid ${H.hair}`, margin: '6px 4px' }} />
|
||
<div className="navitem" style={{ padding: '9px 12px', fontFamily: H.body, fontSize: 13.5, color: H.soft, borderRadius: 8, cursor: 'pointer' }}>Account settings</div>
|
||
<div className="navitem" style={{ padding: '9px 12px', fontFamily: H.body, fontSize: 13.5, color: H.gold, fontWeight: 600, borderRadius: 8, cursor: 'pointer' }}>Sign out</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── Field ──────────────────────────────────────────────────────────────────
|
||
function HFField({ label, placeholder, value, helper, error, focus = false, optional = false, hint }) {
|
||
const cls = `field${focus ? ' is-focus' : ''}${error ? ' is-attn' : ''}`;
|
||
return (
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||
<label style={{ fontFamily: H.body, fontWeight: 500, fontSize: 13.5, color: H.soft, display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
|
||
<span>{label}</span>
|
||
{optional && <span style={{ fontSize: 12.5, color: H.mute, fontWeight: 400 }}>optional</span>}
|
||
</label>
|
||
<div className={cls} style={{
|
||
height: 52, borderRadius: 10, padding: '0 16px', display: 'flex', alignItems: 'center',
|
||
background: 'rgba(237,234,255,.035)', border: `1.5px solid ${H.hair}`,
|
||
fontFamily: H.body, fontSize: 16, color: value ? H.star : H.mute,
|
||
}}>
|
||
{value || placeholder}
|
||
{focus && !value && <span className="caret" />}
|
||
{hint && <span style={{ marginLeft: 'auto', fontFamily: H.body, fontSize: 12.5, color: H.mute }}>{hint}</span>}
|
||
</div>
|
||
{error && <HFNote tone="attn">{error}</HFNote>}
|
||
{helper && !error && <HFNote>{helper}</HFNote>}
|
||
</div>
|
||
);
|
||
}
|
||
function HFNote({ children, tone }) {
|
||
return <span style={{ fontFamily: H.body, fontSize: 13, lineHeight: 1.45, color: tone === 'attn' ? H.gold : H.mute, display: 'flex', gap: 7, alignItems: 'baseline' }}>
|
||
{tone === 'attn' && <span aria-hidden="true" style={{ color: H.gold, fontSize: 11 }}>◆</span>}
|
||
<span>{children}</span>
|
||
</span>;
|
||
}
|
||
|
||
// ── CodeField — 6 cells ─────────────────────────────────────────────────────
|
||
function HFCodeField({ filled = 0, error = false, vals = ['4', '1', '9', '2', '0', '4'] }) {
|
||
return (
|
||
<div style={{ display: 'flex', gap: 11 }}>
|
||
{[0, 1, 2, 3, 4, 5].map((i) => {
|
||
const active = i === filled && !error;
|
||
const has = i < filled;
|
||
return (
|
||
<div key={i} className={`field${active ? ' is-focus' : ''}${error ? ' is-attn' : ''}`} style={{
|
||
width: 52, height: 64, borderRadius: 11, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||
background: 'rgba(237,234,255,.035)', border: `1.5px solid ${H.hair}`,
|
||
fontFamily: H.disp, fontWeight: 500, fontSize: 26, color: H.star,
|
||
}}>
|
||
{has ? vals[i] : (active ? <span className="caret" style={{ height: 28 }} /> : '')}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── Banner — gold attention / lilac info (no red in palette) ───────────────
|
||
function HFBanner({ children, tone = 'attn', title }) {
|
||
const attn = tone === 'attn';
|
||
return (
|
||
<div style={{
|
||
display: 'flex', gap: 12, padding: '14px 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)'}`,
|
||
alignItems: 'flex-start',
|
||
}}>
|
||
<span aria-hidden="true" style={{ color: attn ? H.gold : H.lilac, fontSize: 13, lineHeight: '22px' }}>{attn ? '◆' : '◇'}</span>
|
||
<div style={{ fontFamily: H.body, fontSize: 14, lineHeight: 1.5, color: H.soft }}>
|
||
{title && <div style={{ color: H.star, fontWeight: 600, marginBottom: 2 }}>{title}</div>}
|
||
{children}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── AuthCard — the one surface that earns a real shadow ────────────────────
|
||
function HFAuthCard({ children, width = 440 }) {
|
||
return (
|
||
<div style={{
|
||
width, maxWidth: '100%', boxSizing: 'border-box', background: H.raised,
|
||
border: `1px solid ${H.hairCard}`, borderRadius: 'var(--radius-card)',
|
||
padding: '40px 40px 34px', display: 'flex', flexDirection: 'column', gap: 22,
|
||
boxShadow: H.shadow, position: 'relative', zIndex: 2,
|
||
}}>{children}</div>
|
||
);
|
||
}
|
||
|
||
// ── Primary action wrapper (DS Button, full-width option) ──────────────────
|
||
function HFPrimary({ children, full = true, disabled = false }) {
|
||
return (
|
||
<HFButton variant="primary" href={disabled ? undefined : '#'} disabled={disabled}
|
||
style={{ justifyContent: 'center', width: full ? '100%' : 'auto', padding: '.85rem 1.4rem', fontSize: 15.5, boxSizing: 'border-box' }}>
|
||
{children}
|
||
</HFButton>
|
||
);
|
||
}
|
||
|
||
// ── Footer ──────────────────────────────────────────────────────────────────
|
||
function HFFooter({ pad = '20px 36px' }) {
|
||
return (
|
||
<footer style={{ flex: '0 0 auto', padding: pad, borderTop: `1px solid ${H.hair}`, display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 16, fontFamily: H.body, fontSize: 12.5, color: H.mute, background: 'rgba(9,12,34,.4)' }}>
|
||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 9 }}>
|
||
<img src={HF_ASSET('markGold', 'assets/mark-mono-gold.svg')} width={16} height={16} alt="" style={{ opacity: 0.85 }} />
|
||
ecomm · a Wiggleverse line
|
||
</span>
|
||
<span style={{ display: 'inline-flex', gap: 18 }}>
|
||
<a className="lk" href="#" style={{ color: H.mute }}>Privacy</a>
|
||
<a className="lk" href="#" style={{ color: H.mute }}>Open Core</a>
|
||
<a className="lk" href="#" style={{ color: H.mute }}>Terms</a>
|
||
</span>
|
||
</footer>
|
||
);
|
||
}
|
||
|
||
// ── Heading helper ───────────────────────────────────────────────────────────
|
||
function HFHeading({ children, sub, size = 27 }) {
|
||
return (
|
||
<div>
|
||
<h1 style={{ fontFamily: H.disp, fontWeight: 700, letterSpacing: '-0.015em', fontSize: size, color: H.star, margin: '0 0 9px', lineHeight: 1.08 }}>{children}</h1>
|
||
{sub && <p style={{ fontFamily: H.body, fontSize: 14.5, lineHeight: 1.55, color: H.soft, margin: 0 }}>{sub}</p>}
|
||
</div>
|
||
);
|
||
}
|
||
const hfHonest = { fontFamily: H.body, fontSize: 13, color: H.mute, lineHeight: 1.5, margin: 0 };
|
||
|
||
// ── StateCard — frames a single state in the states stacks ─────────────────
|
||
function HFStateCard({ label, note, children }) {
|
||
return (
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 11 }}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 9, flexWrap: 'wrap' }}>
|
||
<HFTag variant="soon">{label}</HFTag>
|
||
{note && <span style={{ fontFamily: H.body, fontSize: 12.5, color: H.mute }}>{note}</span>}
|
||
</div>
|
||
<div style={{ background: H.raised, border: `1px solid ${H.hairCard}`, borderRadius: 12, padding: 22 }}>{children}</div>
|
||
</div>
|
||
);
|
||
}
|
||
function HFStates({ children }) {
|
||
return <div className="hf" style={{ height: '100%', background: `${HF_STARS}, ${H.sky}`, padding: 26, boxSizing: 'border-box', overflow: 'hidden', fontFamily: H.body, display: 'flex', flexDirection: 'column', gap: 20 }}>{children}</div>;
|
||
}
|
||
|
||
Object.assign(window, {
|
||
HF_H: H, HF_STARS, HF_HORIZON, HF_STORE: STORE, HF_EMAIL: MENU_EMAIL, HF_DSC: HFDS,
|
||
HFStyles, HFScreen, HFWordmark, HFTopBar, HFAccountChip, HFField, HFNote, HFCodeField,
|
||
HFBanner, HFAuthCard, HFPrimary, HFFooter, HFHeading, HFStateCard, HFStates,
|
||
hfHonest,
|
||
});
|