diff --git a/.gitignore b/.gitignore index e0132b7..e43b0f9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ .DS_Store -*.local diff --git a/ui/designs/ecomm-login-and-create-storefront-designs/project/.thumbnail b/ui/designs/ecomm-login-and-create-storefront-designs/project/.thumbnail new file mode 100644 index 0000000..9fdbab4 Binary files /dev/null and b/ui/designs/ecomm-login-and-create-storefront-designs/project/.thumbnail differ diff --git a/ui/designs/ecomm-login-and-create-storefront-designs/project/Ecomm Hi-Fi (offline).html b/ui/designs/ecomm-login-and-create-storefront-designs/project/Ecomm Hi-Fi (offline).html new file mode 100644 index 0000000..4a8d3aa --- /dev/null +++ b/ui/designs/ecomm-login-and-create-storefront-designs/project/Ecomm Hi-Fi (offline).html @@ -0,0 +1,180 @@ + + + + + ecomm — SD-0001 Hi-Fi (Direction A) + + + + +
+ + + + + +
+
Unpacking...
+ + + + + + + + + + \ No newline at end of file diff --git a/ui/designs/ecomm-login-and-create-storefront-designs/project/_ds/wiggleverse-design-system-94cd8055-314c-4d7c-b347-3d24b698eff8/_adherence.oxlintrc.json b/ui/designs/ecomm-login-and-create-storefront-designs/project/_ds/wiggleverse-design-system-94cd8055-314c-4d7c-b347-3d24b698eff8/_adherence.oxlintrc.json new file mode 100644 index 0000000..236d2d9 --- /dev/null +++ b/ui/designs/ecomm-login-and-create-storefront-designs/project/_ds/wiggleverse-design-system-94cd8055-314c-4d7c-b347-3d24b698eff8/_adherence.oxlintrc.json @@ -0,0 +1,378 @@ +{ + "plugins": [ + "react", + "import" + ], + "rules": { + "react/forbid-elements": [ + "warn", + { + "forbid": [] + } + ], + "no-restricted-imports": [ + "warn", + { + "patterns": [ + { + "group": [ + "components/brand/**", + "components/cards/**", + "components/core/**", + "components/navigation/**", + "ui_kits/wiggleverse-www/**" + ], + "message": "Import design-system components from 'index.js', not component internals." + } + ] + } + ], + "no-restricted-syntax": [ + "warn", + { + "selector": "Literal[value=/#[0-9a-fA-F]{3,8}\\b/]", + "message": "Raw hex color — use a design-system color token via var()." + }, + { + "selector": "Literal[value=/\\b\\d+px\\b/]", + "message": "Raw px value — use a design-system spacing token via var()." + }, + { + "selector": "Literal[value=/font-family\\s*:\\s*(?!['\\\"]?(?:Space Grotesk|Inter|Fraunces))/i]", + "message": "Font not provided by the design system. Available: Space Grotesk, Inter, Fraunces." + }, + { + "selector": "JSXOpeningElement[name.name='BrandLockup'] > JSXAttribute > JSXIdentifier[name!=/^(?:variant|size|href|showWordmark|assetBase|key|ref|className|style|children)$/]", + "message": " doesn't accept that prop. Declared props: variant, size, href, showWordmark, assetBase." + }, + { + "selector": "JSXOpeningElement[name.name='BrandLockup'] > JSXAttribute[name.name='variant'] > Literal[value!=/^(?:primary|footer|onLight)$/]", + "message": " variant must be one of 'primary' | 'footer' | 'onLight'." + }, + { + "selector": "JSXOpeningElement[name.name='BuildCard'] > JSXAttribute > JSXIdentifier[name!=/^(?:tag|title|children|key|ref|className|style|children)$/]", + "message": " doesn't accept that prop. Declared props: tag, title, children." + }, + { + "selector": "JSXOpeningElement[name.name='Button'] > JSXAttribute > JSXIdentifier[name!=/^(?:children|variant|href|onLight|disabled|type|onClick|key|ref|className|style|children)$/]", + "message": " + {menuOpen && ( +
e.stopPropagation()}> + + +
+ +
+ )} + + + + +
+ {children ||
{id}
} +
+ + ); +} + +// Inline rename — commits on blur or Enter. +function DCEditable({ value, onChange, style, tag = 'span', onClick }) { + const T = tag; + return ( + e.stopPropagation()} + onBlur={(e) => onChange && onChange(e.currentTarget.textContent)} + onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); e.currentTarget.blur(); } }} + style={style}>{value} + ); +} + +// ───────────────────────────────────────────────────────────── +// Focus mode — overlay one artboard; ←/→ within section, ↑/↓ across +// sections, Esc or backdrop click to exit. +// ───────────────────────────────────────────────────────────── +function DCFocusOverlay({ entry, sectionMeta, sectionOrder }) { + const ctx = React.useContext(DCCtx); + const { sectionId, artboard } = entry; + const sec = ctx.section(sectionId); + const meta = sectionMeta[sectionId]; + const peers = meta.slotIds; + const aid = artboard.props.id ?? artboard.props.label; + const idx = peers.indexOf(aid); + const secIdx = sectionOrder.indexOf(sectionId); + + const go = (d) => { const n = peers[(idx + d + peers.length) % peers.length]; if (n) ctx.setFocus(`${sectionId}/${n}`); }; + const goSection = (d) => { + // Sections whose artboards are all deleted have slotIds:[] — step past + // them to the next non-empty section so ↑/↓ doesn't dead-end. + const n = sectionOrder.length; + for (let i = 1; i < n; i++) { + const ns = sectionOrder[(((secIdx + d * i) % n) + n) % n]; + const first = sectionMeta[ns] && sectionMeta[ns].slotIds[0]; + if (first) { ctx.setFocus(`${ns}/${first}`); return; } + } + }; + + React.useEffect(() => { + const k = (e) => { + if (e.key === 'ArrowLeft') { e.preventDefault(); go(-1); } + if (e.key === 'ArrowRight') { e.preventDefault(); go(1); } + if (e.key === 'ArrowUp') { e.preventDefault(); goSection(-1); } + if (e.key === 'ArrowDown') { e.preventDefault(); goSection(1); } + }; + document.addEventListener('keydown', k); + return () => document.removeEventListener('keydown', k); + }); + + const { width = 260, height = 480, children } = artboard.props; + const [vp, setVp] = React.useState({ w: window.innerWidth, h: window.innerHeight }); + React.useEffect(() => { const r = () => setVp({ w: window.innerWidth, h: window.innerHeight }); window.addEventListener('resize', r); return () => window.removeEventListener('resize', r); }, []); + const scale = Math.max(0.1, Math.min((vp.w - 200) / width, (vp.h - 260) / height, 2)); + + const [ddOpen, setDd] = React.useState(false); + const Arrow = ({ dir, onClick }) => ( + + ); + + // Portal to body so position:fixed is the real viewport regardless of any + // transform on DesignCanvas's ancestors (including the canvas zoom itself). + return ReactDOM.createPortal( +
ctx.setFocus(null)} + onWheel={(e) => e.preventDefault()} + style={{ position: 'fixed', inset: 0, zIndex: 100, background: 'rgba(24,20,16,.6)', backdropFilter: 'blur(14px)', + fontFamily: DC.font, color: '#fff' }}> + + {/* top bar: section dropdown (left) · close (right) */} +
e.stopPropagation()} + style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 72, display: 'flex', alignItems: 'flex-start', padding: '16px 20px 0', gap: 16 }}> +
+ + {ddOpen && ( +
+ {sectionOrder.filter((sid) => sectionMeta[sid].slotIds.length).map((sid) => ( + + ))} +
+ )} +
+
+ +
+ + {/* card centered, label + index below — only the card itself stops + propagation so any backdrop click (including the margins around + the card) exits focus */} +
+
e.stopPropagation()} style={{ width: width * scale, height: height * scale, position: 'relative' }}> +
+ {children ||
{aid}
} +
+
+
e.stopPropagation()} style={{ fontSize: 14, fontWeight: 500, opacity: .85, textAlign: 'center' }}> + {(sec.labels || {})[aid] ?? artboard.props.label} + {idx + 1} / {peers.length} +
+
+ + go(-1)} /> + go(1)} /> + + {/* dots */} +
e.stopPropagation()} + style={{ position: 'absolute', bottom: 20, left: '50%', transform: 'translateX(-50%)', display: 'flex', gap: 8 }}> + {peers.map((p, i) => ( +
+
, + document.body, + ); +} + +// ───────────────────────────────────────────────────────────── +// Post-it — absolute-positioned sticky note +// ───────────────────────────────────────────────────────────── +function DCPostIt({ children, top, left, right, bottom, rotate = -2, width = 180 }) { + return ( +
{children}
+ ); +} + +Object.assign(window, { DesignCanvas, DCSection, DCArtboard, DCPostIt }); + diff --git a/ui/designs/ecomm-login-and-create-storefront-designs/project/hf-admin.jsx b/ui/designs/ecomm-login-and-create-storefront-designs/project/hf-admin.jsx new file mode 100644 index 0000000..5990d6e --- /dev/null +++ b/ui/designs/ecomm-login-and-create-storefront-designs/project/hf-admin.jsx @@ -0,0 +1,80 @@ +// hf-admin.jsx — Admin shell (PUC-8 / PUC-9) hi-fi. Honestly empty + states. +const { HFScreen: AScreen, HFTopBar: ATopBar, HFAccountChip: AChip, HFBanner: ABanner, + HFStateCard: AStateCard, HFStates: AStates, HF_H: A, HF_STORE: ASTORE, HF_DSC: ADS } = window; +const { Eyebrow: AEyebrow, Soul: ASoul } = ADS; + +// storefront identity lockup — the admin's anchor +function StoreId({ size = 17 }) { + return ( + + + + {ASTORE} + ecomm storefront + + + ); +} + +// the honest-empty centerpiece — intentional, not sparse +function EmptyHome({ compact = false }) { + return ( +
+ {/* faint constellation seal — one decorative mark, low alpha */} +
+ +
+ Your storefront +

{ASTORE}

+

+ There's nothing to manage yet — and that's a finished state, not a missing one. + Catalog, orders, and settings will appear here as ecomm grows. +

+
+ ); +} + +function HFAdmin({ device = 'desktop' }) { + const desktop = device === 'desktop'; + return ( + + } + right={desktop ? : } /> + + + ); +} + +function HFAdminStates() { + return ( + + +
+ + + + + +
+
+ + + For your security you've been signed out. Sign in again → + + + +
+ +
+
+
+ ); +} +function Skel({ w, h, r = 6 }) { + return
; +} + +Object.assign(window, { HFAdmin, HFAdminStates }); diff --git a/ui/designs/ecomm-login-and-create-storefront-designs/project/hf-bootstrap.jsx b/ui/designs/ecomm-login-and-create-storefront-designs/project/hf-bootstrap.jsx new file mode 100644 index 0000000..6310f70 --- /dev/null +++ b/ui/designs/ecomm-login-and-create-storefront-designs/project/hf-bootstrap.jsx @@ -0,0 +1,205 @@ +// hf-bootstrap.jsx — Bootstrap (PUC-10 / PUC-11) hi-fi. Terminals + runbook. +const { HF_H: B, HF_STARS: B_STARS, HF_DSC: BDS } = window; +const { Eyebrow: BEyebrow, Soul: BSoul, Callout: BCallout } = BDS; +const BMONO = 'ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace'; + +function BBay({ children, pad = 32 }) { + return
{children}
; +} +function BHead({ kicker, title, sub }) { + return ( +
+ {kicker} +

{title}

+ {sub &&

{sub}

} +
+ ); +} + +function BTerminal({ title, lines, style = {} }) { + return ( +
+
+ + {['rgba(155,140,255,.55)', 'rgba(244,199,107,.55)', 'rgba(237,234,255,.32)'].map((c, i) => ( + + ))} + + {title} +
+
+ {lines.map((ln, i) => )} +
+
+ ); +} +function BLine({ ln }) { + const [k, t] = ln; + if (k === 'sp') return
; + if (k === 'cmd') return
$ {t}
; + if (k === 'step') return
{t}
; + if (k === 'ok') return
{t}
; + if (k === 'hl') return
{t}
; + if (k === 'note') return
{t}
; + return
{t}
; +} + +// 1 · principle ---------------------------------------------------------------- +function HFBootPrinciple() { + const steps = [ + ['Operator starts the app', 'scripts/dev.sh locally · flotilla deploy when deployed', 'lilac'], + ['App migrates itself', 'pending .sql migrations apply in order, fail-stop (INV-7)', 'lilac'], + ['/healthz goes green', 'process up · DB reachable · migrations current', 'gold'], + ['First merchant walks the product', 'sign up → create storefront → admin (PUC-2 / 4 / 6)', 'gold'], + ['First rows exist', 'created through the flows alone — nothing placed by hand', 'lilac'], + ]; + return ( + + +
+ {steps.map(([t, d, c], i) => { + const col = c === 'gold' ? B.gold : B.lilac; + return ( +
+
+ {i + 1} + {i < steps.length - 1 && } +
+
+
{t}
+
{d}
+
+
+ ); + })} +
+
+ ); +} + +// 2 · runbook (Paper) ---------------------------------------------------------- +function HFBootRunbook() { + const envs = [ + ['localhost', './scripts/dev.sh', 'Docker is the one prerequisite. Brings up Postgres, migrates, serves on :5173.'], + ['pre-prod (PPE)', 'flotilla deploy ecomm --env ppe', 'Provisions Cloud SQL, resolves secrets, migrates at startup. Rehearse here first.'], + ['production', 'flotilla deploy ecomm --env prod', 'The identical gesture. Day-one prod is simply the bootstrap state.'], + ]; + return ( +
+
+ docs/BOOTSTRAP.md + the documented gesture +
+

Bringing ecomm up from empty

+

+ One gesture, three environments. No hand-seeded data — the first merchant arrives through the product flows alone. +

+
+ {envs.map(([env, cmd, note]) => ( +
+ {env} +
+
{cmd}
+
{note}
+
+
+ ))} +
+
+ Empty is a working state — there is no seed. +
+ ); +} + +// 3 · localhost terminal ------------------------------------------------------- +function HFBootDev() { + return ( + + + + + ); +} + +// 4 · the OTC channel ---------------------------------------------------------- +function HFBootChannel() { + return ( + + +
+
+ Dev — the code prints to the backend log + +
+
+ Deployed — the code arrives as real email +
+
+ ecomm <no-reply@ecomm.wiggleverse.org>now +
+
Your ecomm code: 419 204
+
+ Enter this code to sign in. It's good for 10 minutes. If you didn't request it, you can ignore this email. +
+
+
+
+
+ ); +} +function ChanLabel({ children, tone }) { + const c = tone === 'gold' ? B.gold : B.lilac; + return
+ {children} +
; +} + +// 5 · deploy terminal ---------------------------------------------------------- +function HFBootDeploy() { + return ( + + + + + ); +} + +Object.assign(window, { HFBootPrinciple, HFBootRunbook, HFBootDev, HFBootChannel, HFBootDeploy }); diff --git a/ui/designs/ecomm-login-and-create-storefront-designs/project/hf-intro.jsx b/ui/designs/ecomm-login-and-create-storefront-designs/project/hf-intro.jsx new file mode 100644 index 0000000..18c24d8 --- /dev/null +++ b/ui/designs/ecomm-login-and-create-storefront-designs/project/hf-intro.jsx @@ -0,0 +1,87 @@ +// hf-intro.jsx — Hi-fi cover: the design language Direction A locks in. +const { HFScreen: IScreen, HFWordmark: IWordmark, HF_H: I, HF_DSC: IDS } = window; +const { Eyebrow: IEyebrow, Soul: ISoul, Tag: ITag } = IDS; + +function HFCover() { + const langs = [ + ['Ground', 'Midnight sky with a barely-there starfield; a gold horizon glow warms the bottom of entry screens.'], + ['Depth', 'Carried by layered surfaces and hairlines — not drop shadows. Only the auth card earns real elevation.'], + ['Type', 'Space Grotesk for structure, Inter for body — calm and precise, never hypey. Sentence case throughout; uppercase only for small eyebrows.'], + ['Action', 'One gold CTA per screen. Lilac for links and nodes. Hover lifts 1px; focus is a gold ring.'], + ['Attention', 'Gold, always — the palette carries no red. Honest failure, never a faked success.'], + ['Emptiness', 'A finished state with its own composition — no zeroed tiles, no locked-feature teasers.'], + ]; + return ( + +
+ + SD-0001 · Hi-fi · Direction A +
+
+ Quiet centered — the full design +

+ The language, before
the hundred screens +

+

+ Every surface below is the same six decisions, applied. Get these right and the next ninety-six + screens inherit a calm, honest house style for free. +

+
+
+ {langs.map(([k, v]) => ( +
+ +
+
{k}
+
{v}
+
+
+ ))} +
+
+ ); +} + +// Entry routing — the shared spine, hi-fi +function HFRouting() { + const lanes = [ + { sess: 'No session', store: '—', arrow: 'Landing', puc: 'PUC-1', tone: 'mute' }, + { sess: 'Valid session', store: 'No storefront', arrow: 'Create storefront', puc: 'PUC-5', tone: 'gold' }, + { sess: 'Valid session', store: 'Has storefront', arrow: 'Admin shell', puc: 'PUC-6', tone: 'lilac' }, + ]; + return ( + + The shared spine +

Entry routing

+

+ One server answer — GET /api/auth/me — decides where every arrival lands. +

+
+ {lanes.map((l, i) => { + const c = l.tone === 'gold' ? I.gold : l.tone === 'lilac' ? I.lilac : I.mute; + return ( +
+
+ {l.sess}{l.store} +
+ +
+ {l.arrow} + {l.puc} +
+
+ ); + })} +
+

+ + Exhaustive by design — no state lands nowhere. A merchant without a storefront is guided to create one, never stranded. +

+
+ ); +} +function Cond({ children }) { + return {children}; +} + +Object.assign(window, { HFCover, HFRouting }); diff --git a/ui/designs/ecomm-login-and-create-storefront-designs/project/hf-kit.jsx b/ui/designs/ecomm-login-and-create-storefront-designs/project/hf-kit.jsx new file mode 100644 index 0000000..60bb7a7 --- /dev/null +++ b/ui/designs/ecomm-login-and-create-storefront-designs/project/hf-kit.jsx @@ -0,0 +1,267 @@ +// 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 ( +