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>
129 lines
5.9 KiB
React
129 lines
5.9 KiB
React
// wf-admin.jsx — Screen 4: Admin shell (PUC-8, PUC-9). A/B + states.
|
|
const { Screen: AScreen, Wordmark: AWordmark, TopBar: ATopBar, AccountChip: AAccountChip,
|
|
Banner: ABanner, EmptyAdmin: AEmptyAdmin, StateBlock: AStateBlock, WF_T: AT, WF_DS: ADS } = window;
|
|
const { Eyebrow: AEyebrow } = ADS;
|
|
|
|
const STORE = 'Studio Fern';
|
|
|
|
// storefront identity lockup — the admin's anchor (left of top bar in A)
|
|
function StoreId({ size = 17 }) {
|
|
return (
|
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 11 }}>
|
|
<img src="assets/mark-tile.svg" width={size + 6} height={size + 6} style={{ borderRadius: 6, display: 'block' }} alt="" />
|
|
<span style={{ display: 'inline-flex', flexDirection: 'column', lineHeight: 1.1 }}>
|
|
<span style={{ fontFamily: AT.disp, fontWeight: 700, fontSize: size, color: AT.star, letterSpacing: '-0.01em' }}>{STORE}</span>
|
|
<span style={{ fontFamily: AT.body, fontSize: 11, color: AT.mute }}>ecomm storefront</span>
|
|
</span>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
// ── Direction A — top bar + centered honest-empty ──────────────────────────
|
|
function AdminA({ device = 'desktop' }) {
|
|
const desktop = device === 'desktop';
|
|
return (
|
|
<AScreen>
|
|
<ATopBar pad={desktop ? '0 32px' : '0 18px'} left={<StoreId size={desktop ? 17 : 15} />}
|
|
right={desktop ? <AAccountChip /> : <button style={signOutBtn}>Sign out</button>} />
|
|
<AEmptyAdmin storefront={STORE} compact={!desktop} />
|
|
</AScreen>
|
|
);
|
|
}
|
|
|
|
// ── Direction B — durable app shell (sidebar + content) ────────────────────
|
|
function AdminB({ device = 'desktop' }) {
|
|
const desktop = device === 'desktop';
|
|
if (!desktop) {
|
|
return (
|
|
<AScreen>
|
|
<ATopBar pad="0 18px" left={<StoreId size={15} />} right={<button style={signOutBtn}>Sign out</button>} />
|
|
<div style={{ flex: '0 0 auto', padding: '12px 18px', borderBottom: `1px solid ${AT.hair}`, display: 'flex', gap: 8 }}>
|
|
<NavItem active>Home</NavItem>
|
|
</div>
|
|
<AEmptyAdmin storefront={STORE} compact />
|
|
</AScreen>
|
|
);
|
|
}
|
|
return (
|
|
<AScreen style={{ flexDirection: 'row' }}>
|
|
<Sidebar />
|
|
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
|
|
<header style={{ flex: '0 0 auto', height: 60, padding: '0 36px', display: 'flex', alignItems: 'center', borderBottom: `1px solid ${AT.hair}` }}>
|
|
<span style={{ fontFamily: AT.disp, fontWeight: 500, fontSize: 15, color: AT.soft }}>Home</span>
|
|
</header>
|
|
<div style={{ flex: 1, display: 'flex' }}>
|
|
<AEmptyAdmin storefront={STORE} />
|
|
</div>
|
|
</div>
|
|
</AScreen>
|
|
);
|
|
}
|
|
|
|
function Sidebar() {
|
|
return (
|
|
<aside style={{ flex: '0 0 258px', background: 'var(--wv-night)', borderRight: `1px solid ${AT.hair}`, display: 'flex', flexDirection: 'column', padding: '22px 16px' }}>
|
|
<div style={{ padding: '0 8px 18px' }}><StoreId /></div>
|
|
<div style={{ borderTop: `1px solid ${AT.hair}`, margin: '0 8px 16px' }} />
|
|
<nav style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
|
<NavItem active full>Home</NavItem>
|
|
</nav>
|
|
<div style={{ flex: 1 }} />
|
|
<p style={{ fontFamily: AT.body, fontSize: 11.5, lineHeight: 1.5, color: 'var(--wv-starlight-55)', padding: '0 10px', margin: '0 0 16px' }}>
|
|
Catalog, orders & settings join this rail as ecomm grows. Nothing is shown here before it exists.
|
|
</p>
|
|
<div style={{ borderTop: `1px solid ${AT.hair}`, padding: '14px 10px 2px', display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
<span style={{ fontFamily: AT.body, fontSize: 13, color: AT.soft }}>mara@studiofern.com</span>
|
|
<button style={{ ...signOutBtn, fontSize: 13, textAlign: 'left' }}>Sign out</button>
|
|
</div>
|
|
</aside>
|
|
);
|
|
}
|
|
|
|
function NavItem({ children, active, full }) {
|
|
return (
|
|
<span style={{
|
|
display: 'inline-flex', alignItems: 'center', gap: 9, padding: full ? '9px 12px' : '7px 13px',
|
|
borderRadius: 9, fontFamily: AT.disp, fontWeight: 500, fontSize: 14,
|
|
color: active ? AT.star : AT.mute,
|
|
background: active ? 'var(--wv-lilac-12)' : 'transparent',
|
|
border: full ? `1px solid ${active ? 'var(--wv-lilac-18)' : 'transparent'}` : 'none',
|
|
}}>
|
|
<span aria-hidden="true" style={{ width: 6, height: 6, borderRadius: 3, background: active ? AT.lilac : 'transparent' }} />
|
|
{children}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
const signOutBtn = { fontFamily: AT.disp, fontWeight: 500, fontSize: 14, color: AT.star, background: 'transparent', border: 'none', cursor: 'pointer', padding: 0 };
|
|
|
|
// ── States stack ───────────────────────────────────────────────────────────
|
|
function AdminStates() {
|
|
return (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 22 }}>
|
|
<AStateBlock label="Loading" note="minimal skeleton while /me resolves">
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
<Skel w={90} h={12} />
|
|
<Skel w={200} h={28} />
|
|
<Skel w={280} h={12} />
|
|
<Skel w={240} h={12} />
|
|
</div>
|
|
</AStateBlock>
|
|
<AStateBlock label="Session expired" note="redirect to sign-in">
|
|
<ABanner tone="info" title="Your session ended">
|
|
For your security you've been signed out. <a href="#" style={{ color: AT.lilac, textDecoration: 'none' }}>Sign in again →</a>
|
|
</ABanner>
|
|
</AStateBlock>
|
|
<AStateBlock label="Sign out" note="account menu open (PUC-9)">
|
|
<div style={{ position: 'relative', height: 96 }}>
|
|
<AAccountChip menu />
|
|
</div>
|
|
</AStateBlock>
|
|
</div>
|
|
);
|
|
}
|
|
function Skel({ w, h }) {
|
|
return <div style={{ width: w, height: h, borderRadius: 6, background: 'rgba(237,234,255,.08)' }} />;
|
|
}
|
|
|
|
Object.assign(window, { AdminA, AdminB, AdminStates });
|