diff --git a/frontend/index.html b/frontend/index.html index 1a13c4f..567778d 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,6 +3,9 @@ + + + ecomm diff --git a/frontend/public/brand/mark-mono-gold.svg b/frontend/public/brand/mark-mono-gold.svg new file mode 100644 index 0000000..cc7fde2 --- /dev/null +++ b/frontend/public/brand/mark-mono-gold.svg @@ -0,0 +1,8 @@ + + Wiggleverse — mono gold + + + + + + \ No newline at end of file diff --git a/frontend/public/brand/mark-tile.svg b/frontend/public/brand/mark-tile.svg new file mode 100644 index 0000000..51d6c69 --- /dev/null +++ b/frontend/public/brand/mark-tile.svg @@ -0,0 +1,9 @@ + + Wiggleverse + + + + + + + \ No newline at end of file diff --git a/frontend/public/fonts/inter-v20-latin-500.woff2 b/frontend/public/fonts/inter-v20-latin-500.woff2 new file mode 100644 index 0000000..54f0a59 Binary files /dev/null and b/frontend/public/fonts/inter-v20-latin-500.woff2 differ diff --git a/frontend/public/fonts/inter-v20-latin-600.woff2 b/frontend/public/fonts/inter-v20-latin-600.woff2 new file mode 100644 index 0000000..d189794 Binary files /dev/null and b/frontend/public/fonts/inter-v20-latin-600.woff2 differ diff --git a/frontend/public/fonts/inter-v20-latin-regular.woff2 b/frontend/public/fonts/inter-v20-latin-regular.woff2 new file mode 100644 index 0000000..f15b025 Binary files /dev/null and b/frontend/public/fonts/inter-v20-latin-regular.woff2 differ diff --git a/frontend/public/fonts/space-grotesk-v22-latin-500.woff2 b/frontend/public/fonts/space-grotesk-v22-latin-500.woff2 new file mode 100644 index 0000000..0db251f Binary files /dev/null and b/frontend/public/fonts/space-grotesk-v22-latin-500.woff2 differ diff --git a/frontend/public/fonts/space-grotesk-v22-latin-700.woff2 b/frontend/public/fonts/space-grotesk-v22-latin-700.woff2 new file mode 100644 index 0000000..44604a0 Binary files /dev/null and b/frontend/public/fonts/space-grotesk-v22-latin-700.woff2 differ diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index f8fc6f5..0b18e9d 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,3 +1,4 @@ +import "./styles/index.css"; import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import App from "./App"; diff --git a/frontend/src/styles/app.css b/frontend/src/styles/app.css new file mode 100644 index 0000000..683dd53 --- /dev/null +++ b/frontend/src/styles/app.css @@ -0,0 +1,394 @@ +/* ecomm app chrome — the hf-kit primitives (Direction A · Quiet centered) as CSS. + Depth = layered surfaces + hairlines; hover = small lift, no new shadows. */ + +* { box-sizing: border-box; } + +html, body, #root { height: 100%; } + +body { + margin: 0; + background: var(--wv-midnight); + color: var(--wv-starlight); + font-family: var(--wv-font-body); + -webkit-font-smoothing: antialiased; +} + +/* ── screen ground: barely-there starfield + optional gold horizon ─────────── */ +.screen { + min-height: 100%; + display: flex; + flex-direction: column; + position: relative; + background: + 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), + var(--wv-midnight); +} +.screen--horizon { + background: + radial-gradient(120% 70% at 50% 142%, rgba(244, 199, 107, .16) 0%, rgba(244, 199, 107, .05) 38%, transparent 64%), + 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), + var(--wv-midnight); +} +.screen--plain { background: var(--wv-midnight); } + +.screen__main { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 32px 24px; +} + +/* ── top bar: glass chrome ──────────────────────────────────────────────────── */ +.topbar { + flex: 0 0 auto; + height: 68px; + padding: 0 36px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + border-bottom: 1px solid var(--border-soft); + background: var(--glass-sky); + backdrop-filter: blur(var(--glass-blur)); + position: relative; + z-index: 5; +} +.topbar__side { display: flex; align-items: center; gap: 16px; min-width: 0; } + +/* ── wordmark ───────────────────────────────────────────────────────────────── */ +.wordmark { display: inline-flex; align-items: center; gap: 10px; } +.wordmark img { display: block; border-radius: 6px; } +.wordmark__name { + font-family: var(--wv-font-display); + font-weight: var(--weight-bold); + font-size: 22px; + letter-spacing: var(--tracking-display); + color: var(--wv-starlight); +} + +/* ── links & text buttons ───────────────────────────────────────────────────── */ +.lk { + color: var(--wv-lilac); + text-decoration: none; + background: none; + border: none; + padding: 0; + font: inherit; + cursor: pointer; + transition: color var(--dur-fast) var(--ease); +} +.lk:hover { color: var(--wv-gold); } +.lk--mute { color: var(--text-on-dark-mute); } +.lk--nav { + font-family: var(--wv-font-display); + font-weight: var(--weight-medium); + font-size: 14px; + color: var(--wv-starlight); +} + +.signout { + font-family: var(--wv-font-display); + font-weight: var(--weight-medium); + font-size: 14px; + color: var(--wv-starlight); + background: transparent; + border: none; + cursor: pointer; + padding: 0; + transition: color var(--dur-fast) var(--ease); +} +.signout:hover { color: var(--wv-gold); } + +/* ── account chip (top-bar right, signed in) ────────────────────────────────── */ +.chip { display: flex; align-items: center; gap: 14px; min-width: 0; } +.chip__avatar { + width: 30px; + height: 30px; + border-radius: 15px; + background: var(--wv-lilac-16); + border: 1px solid var(--border-card); + display: flex; + align-items: center; + justify-content: center; + font-family: var(--wv-font-display); + font-weight: var(--weight-bold); + font-size: 13px; + flex: 0 0 auto; +} +.chip__email { + font-size: 14px; + color: var(--text-on-dark-soft); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.chip__divider { width: 1px; height: 18px; background: var(--border-soft); flex: 0 0 auto; } + +/* ── eyebrow ────────────────────────────────────────────────────────────────── */ +.eyebrow { + font-family: var(--wv-font-display); + font-weight: var(--weight-medium); + font-size: var(--text-eyebrow); + letter-spacing: var(--tracking-eyebrow); + text-transform: uppercase; + color: var(--wv-lilac); + margin: 0; +} + +/* ── auth card — the one surface with a real shadow ─────────────────────────── */ +.card { + width: 440px; + max-width: 100%; + background: var(--surface-raised); + border: 1px solid var(--border-card); + border-radius: var(--radius-card); + padding: 40px 40px 34px; + display: flex; + flex-direction: column; + gap: 22px; + box-shadow: var(--shadow-soft); + position: relative; + z-index: 2; +} +.card h1 { + font-family: var(--wv-font-display); + font-weight: var(--weight-bold); + letter-spacing: var(--tracking-display); + font-size: 27px; + line-height: 1.08; + margin: 0 0 9px; +} +.card__sub { font-size: 14.5px; line-height: 1.55; color: var(--text-on-dark-soft); margin: 0; } + +/* ── fields ─────────────────────────────────────────────────────────────────── */ +.field { display: flex; flex-direction: column; gap: 8px; } +.field__label { + font-weight: var(--weight-medium); + font-size: 13.5px; + color: var(--text-on-dark-soft); + display: flex; + justify-content: space-between; + align-items: baseline; +} +.field__optional { font-size: 12.5px; color: var(--text-on-dark-mute); font-weight: var(--weight-regular); } +.field__input { + height: 52px; + border-radius: 10px; + padding: 0 16px; + background: rgba(237, 234, 255, .035); + border: 1.5px solid var(--border-soft); + font-family: var(--wv-font-body); + font-size: 16px; + color: var(--wv-starlight); + width: 100%; + transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease); +} +.field__input::placeholder { color: var(--text-on-dark-mute); } +.field__input:hover { border-color: var(--border-strong); } +.field__input:focus { + outline: none; + border-color: var(--wv-gold); + box-shadow: 0 0 0 3px var(--wv-gold-28); + background: rgba(237, 234, 255, .06); +} +.field__input--error { border-color: var(--wv-gold); } + +.note { font-size: 13px; line-height: 1.45; color: var(--text-on-dark-mute); margin: 0; } +.note--attn { color: var(--wv-gold); display: flex; gap: 7px; align-items: baseline; } +.note--attn::before { content: "◆"; font-size: 11px; } + +/* ── primary button: gold pill ──────────────────────────────────────────────── */ +.btn-primary { + display: inline-flex; + align-items: center; + justify-content: center; + gap: .4em; + width: 100%; + font-family: var(--wv-font-display); + font-weight: var(--weight-medium); + font-size: 15.5px; + line-height: 1; + padding: .85rem 1.4rem; + border-radius: var(--radius-pill); + border: var(--btn-border-w) solid transparent; + background: var(--cta); + color: var(--cta-text); + cursor: pointer; + transition: transform var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease); +} +.btn-primary:hover:not(:disabled) { background: var(--cta-hover); transform: var(--lift-1); } +.btn-primary:disabled { opacity: .5; cursor: not-allowed; } +.btn-primary--auto { width: auto; } +.btn-primary:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; } + +/* ── banner: gold attention / lilac info (no red in the palette) ────────────── */ +.banner { + display: flex; + gap: 12px; + padding: 14px 16px; + border-radius: 12px; + align-items: flex-start; + font-size: 14px; + line-height: 1.5; + color: var(--text-on-dark-soft); + text-align: left; +} +.banner--attn { background: rgba(244, 199, 107, .10); border: 1px solid var(--wv-gold-40); } +.banner--info { background: var(--wv-lilac-08); border: 1px solid var(--wv-lilac-18); } +.banner__icon { font-size: 13px; line-height: 22px; flex: 0 0 auto; } +.banner--attn .banner__icon { color: var(--wv-gold); } +.banner--info .banner__icon { color: var(--wv-lilac); } +.banner__title { color: var(--wv-starlight); font-weight: var(--weight-semibold); margin-bottom: 2px; } + +/* ── code input: 6 cells over one invisible input ───────────────────────────── */ +.code { position: relative; display: flex; gap: 11px; justify-content: center; } +.code__hidden { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + opacity: 0; + border: none; + font-size: 16px; + cursor: pointer; +} +.code__cell { + width: 52px; + height: 64px; + border-radius: 11px; + display: flex; + align-items: center; + justify-content: center; + background: rgba(237, 234, 255, .035); + border: 1.5px solid var(--border-soft); + font-family: var(--wv-font-display); + font-weight: var(--weight-medium); + font-size: 26px; + transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease); +} +.code--focus .code__cell--active { + border-color: var(--wv-gold); + box-shadow: 0 0 0 3px var(--wv-gold-28); +} +.code--error .code__cell { border-color: var(--wv-gold); } + +/* ── footer ─────────────────────────────────────────────────────────────────── */ +.footer { + flex: 0 0 auto; + padding: 20px 36px; + border-top: 1px solid var(--border-soft); + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; + font-size: 12.5px; + color: var(--text-on-dark-mute); + background: rgba(9, 12, 34, .4); +} +.footer__id { display: inline-flex; align-items: center; gap: 9px; } +.footer__id img { opacity: .85; } + +/* ── landing ────────────────────────────────────────────────────────────────── */ +.hero { max-width: 680px; display: flex; flex-direction: column; align-items: center; text-align: center; } +.hero h1 { + font-family: var(--wv-font-display); + font-weight: var(--weight-bold); + letter-spacing: -0.02em; + line-height: 1.02; + font-size: clamp(36px, 6.5vw, 62px); + margin: 20px 0 22px; +} +.hero h1 em { font-style: normal; color: var(--wv-gold); } +.hero__lead { + font-size: clamp(15.5px, 1.8vw, 18.5px); + line-height: 1.6; + color: var(--text-on-dark-soft); + max-width: 520px; + margin: 0 0 32px; + text-wrap: pretty; +} +.hero__actions { display: flex; gap: 14px; align-items: center; flex-wrap: wrap; justify-content: center; } +.hero__actions .btn-primary { width: auto; } + +.promises { + display: grid; + grid-template-columns: repeat(3, 1fr); + width: 100%; + margin-top: 44px; + border-top: 1px solid var(--border-soft); + padding-top: 24px; + text-align: left; +} +.promises > div { padding: 0 22px; } +.promises > div + div { border-left: 1px solid var(--border-soft); } +.promises__title { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 5px; + font-family: var(--wv-font-display); + font-weight: var(--weight-medium); + font-size: 14.5px; +} +.promises__title::before { content: "◆"; color: var(--wv-gold); font-size: 10px; } +.promises p { font-size: 13px; line-height: 1.5; color: var(--text-on-dark-mute); margin: 0; } + +/* ── admin shell ────────────────────────────────────────────────────────────── */ +.storeid { display: inline-flex; align-items: center; gap: 11px; min-width: 0; } +.storeid img { display: block; border-radius: 7px; flex: 0 0 auto; } +.storeid__col { display: inline-flex; flex-direction: column; line-height: 1.12; min-width: 0; } +.storeid__name { + font-family: var(--wv-font-display); + font-weight: var(--weight-bold); + font-size: 17px; + letter-spacing: -0.01em; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.storeid__sub { font-size: 11px; color: var(--text-on-dark-mute); } + +.empty { display: flex; flex-direction: column; align-items: center; text-align: center; max-width: 560px; } +.empty__seal { + width: 76px; + height: 76px; + border-radius: 50%; + border: 1px solid var(--border-card); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 26px; + background: var(--wv-lilac-08); +} +.empty h1 { + font-family: var(--wv-font-display); + font-weight: var(--weight-bold); + letter-spacing: var(--tracking-display); + line-height: 1.04; + font-size: clamp(30px, 4.5vw, 42px); + margin: 12px 0 16px; +} +.empty__copy { font-size: 17px; line-height: 1.6; color: var(--text-on-dark-soft); max-width: 460px; margin: 0; text-wrap: pretty; } + +/* ── small screens ──────────────────────────────────────────────────────────── */ +@media (max-width: 720px) { + .topbar { padding: 0 20px; } + .footer { padding: 16px 20px; flex-direction: column; } + .card { padding: 28px 22px 24px; } + .promises { grid-template-columns: 1fr; gap: 14px; } + .promises > div { padding: 0; } + .promises > div + div { border-left: none; } + .code__cell { width: 44px; height: 56px; font-size: 22px; } +} diff --git a/frontend/src/styles/fonts.css b/frontend/src/styles/fonts.css new file mode 100644 index 0000000..56abbf6 --- /dev/null +++ b/frontend/src/styles/fonts.css @@ -0,0 +1,37 @@ +/* Wiggleverse webfonts (from the design-system export) — Space Grotesk (display) + + Inter (body/UI). Fraunces (human register) deliberately omitted: no screen uses it yet. */ +@font-face { + font-family: "Space Grotesk"; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url("/fonts/space-grotesk-v22-latin-500.woff2") format("woff2"); +} +@font-face { + font-family: "Space Grotesk"; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url("/fonts/space-grotesk-v22-latin-700.woff2") format("woff2"); +} +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url("/fonts/inter-v20-latin-regular.woff2") format("woff2"); +} +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url("/fonts/inter-v20-latin-500.woff2") format("woff2"); +} +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url("/fonts/inter-v20-latin-600.woff2") format("woff2"); +} diff --git a/frontend/src/styles/index.css b/frontend/src/styles/index.css new file mode 100644 index 0000000..d924144 --- /dev/null +++ b/frontend/src/styles/index.css @@ -0,0 +1,7 @@ +/* Global style entry point — Wiggleverse design-system tokens (copied from the + ui/designs export) + the ecomm app chrome built on them. */ +@import "./fonts.css"; +@import "./tokens-colors.css"; +@import "./tokens-typography.css"; +@import "./tokens-spacing.css"; +@import "./app.css"; diff --git a/frontend/src/styles/tokens-colors.css b/frontend/src/styles/tokens-colors.css new file mode 100644 index 0000000..57edf8e --- /dev/null +++ b/frontend/src/styles/tokens-colors.css @@ -0,0 +1,60 @@ +/* Wiggleverse — Color tokens + Source of truth: wiggleverse-www/assets/tokens.css (brand BRAND.md §8–9). + Dark "sky" is the primary ground; Paper is for long reading. No-center motif. */ + +:root { + /* ---- Brand palette (base values) ---- */ + --wv-midnight: #0E1230; /* sky / ground — primary dark background */ + --wv-indigo: #1C2150; /* raised surfaces on dark (cards, mobile nav) */ + --wv-indigo-2: #232A63; /* hover state for raised surfaces */ + --wv-lilac: #9B8CFF; /* accent — "the bonds between us"; links, nodes */ + --wv-violet: #7C6FE0; /* secondary links / strokes (on light) */ + --wv-gold: #F4C76B; /* warmth / horizon / primary CTAs */ + --wv-gold-hi: #F7D488; /* gold hover */ + --wv-starlight:#EDEAFF; /* nodes / text on dark */ + --wv-paper: #F6F4FB; /* light-mode background */ + --wv-ink: #3B2F7A; /* text on light */ + --wv-night: #090C22; /* footer / deepest ground */ + + /* CTA text-on-gold (very dark gold-brown, not pure black) */ + --wv-gold-ink: #2A2003; + --wv-gold-ink-soft: #6B4E10; /* "soon" tag text on gold tint */ + + /* ---- Alpha derivations (lilac / gold / starlight washes) ---- */ + --wv-lilac-08: rgba(155, 140, 255, .08); + --wv-lilac-12: rgba(155, 140, 255, .12); + --wv-lilac-16: rgba(155, 140, 255, .16); + --wv-lilac-18: rgba(155, 140, 255, .18); + --wv-lilac-32: rgba(155, 140, 255, .32); + --wv-gold-28: rgba(244, 199, 107, .28); + --wv-gold-40: rgba(244, 199, 107, .40); + --wv-starlight-85: rgba(237, 234, 255, .85); + --wv-starlight-78: rgba(237, 234, 255, .78); + --wv-starlight-60: rgba(237, 234, 255, .60); + --wv-starlight-55: rgba(237, 234, 255, .55); + + /* ---- Semantic aliases ---- */ + --surface-sky: var(--wv-midnight); /* page ground (dark) */ + --surface-raised: var(--wv-indigo); /* cards / panels on dark */ + --surface-raised-hi: var(--wv-indigo-2); /* raised hover */ + --surface-paper: var(--wv-paper); /* long-reading light sections */ + --surface-card-light:#FFFFFF; /* build-cards on paper */ + --surface-footer: var(--wv-night); + + --text-on-dark: var(--wv-starlight); + --text-on-dark-soft: var(--wv-starlight-78); + --text-on-dark-mute: var(--wv-starlight-60); + --text-on-light: var(--wv-ink); + --text-on-light-soft:#4B4170; + + --accent: var(--wv-lilac); /* links + nodes on dark */ + --accent-on-light: var(--wv-violet); /* links + strokes on light */ + --cta: var(--wv-gold); /* primary action / horizon */ + --cta-hover: var(--wv-gold-hi); + --cta-text: var(--wv-gold-ink); + + --border-soft: var(--wv-lilac-16); /* hairlines on dark */ + --border-card: var(--wv-lilac-18); + --border-strong: var(--wv-lilac-32); + --focus-ring: var(--wv-gold); +} diff --git a/frontend/src/styles/tokens-spacing.css b/frontend/src/styles/tokens-spacing.css new file mode 100644 index 0000000..04a084a --- /dev/null +++ b/frontend/src/styles/tokens-spacing.css @@ -0,0 +1,55 @@ +/* Wiggleverse — Spacing, radius, shadow, layout & motion tokens + Derived from the marketing-site CSS. The brand has almost no shadow system — + depth is carried by surface color + hairline borders, not drop shadows. */ + +:root { + /* ---- Spacing scale (rem) ---- */ + --space-0: 0; + --space-1: .25rem; + --space-2: .5rem; + --space-3: .7rem; + --space-4: 1rem; + --space-5: 1.2rem; + --space-6: 1.6rem; + --space-8: 2rem; + --space-10: 2.5rem; + --space-12: 3rem; + + /* Section rhythm — fluid vertical padding for page bands */ + --section-pad: clamp(3rem, 7vw, 5.5rem); + --page-hero-pad: clamp(2.8rem, 6vw, 4.5rem); + + /* ---- Layout ---- */ + --wrap-max: 1080px; /* content column */ + --wrap-gutter: 92vw; /* width: min(--wrap-max, --wrap-gutter) */ + --measure-prose: 68ch; /* reading measure for prose blocks */ + + /* ---- Radii ---- */ + --radius-card: 14px; /* path-cards, build-cards */ + --radius-panel: 12px; /* principle tiles */ + --radius-sm: 4px; /* focus ring rounding */ + --radius-pill: 999px; /* buttons, tags, notices */ + --radius-mark: 26px; /* favicon tile rounding */ + + /* ---- Borders ---- */ + --border-hair: 1px; /* default hairline */ + --border-card-w: 1px; + --btn-border-w: 1.5px; /* ghost button / focus weight */ + + /* ---- Elevation ---- * + * The system avoids drop shadows. "Lift" on hover is a -1 to -3px translateY, + * not a shadow. These tokens exist for the rare card that needs real elevation. */ + --shadow-none: none; + --shadow-soft: 0 8px 24px rgba(9, 12, 34, .28); + --lift-1: translateY(-1px); /* @kind other */ /* buttons */ + --lift-3: translateY(-3px); /* @kind other */ /* cards */ + + /* ---- Glass (sticky header) ---- */ + --glass-sky: rgba(14, 18, 48, .82); + --glass-blur: 10px; + + /* ---- Motion ---- */ + --ease: ease; /* @kind other */ + --dur-fast: .15s; /* @kind other */ /* hover / press transitions */ + --dur-mid: .25s; /* @kind other */ /* mobile nav reveal */ +} diff --git a/frontend/src/styles/tokens-typography.css b/frontend/src/styles/tokens-typography.css new file mode 100644 index 0000000..900a7fe --- /dev/null +++ b/frontend/src/styles/tokens-typography.css @@ -0,0 +1,53 @@ +/* Wiggleverse — Typography tokens + Two registers, one meaning (BRAND.md): + • Machine register — Space Grotesk (display) + Inter (body/UI): precise, structural. + • Human register — Fraunces, ITALIC ONLY: warm, literary, used sparingly + for the lines that carry conscience ("the soul"). + @font-face rules live in assets/fonts/fonts.css. */ + +:root { + /* ---- Families ---- */ + --wv-font-display: 'Space Grotesk', system-ui, sans-serif; /* headings, wordmark */ + --wv-font-body: 'Inter', system-ui, sans-serif; /* body / UI */ + --wv-font-human: 'Fraunces', Georgia, serif; /* pull-quotes — italic only */ + + /* semantic aliases */ + --font-display: var(--wv-font-display); + --font-body: var(--wv-font-body); + --font-soul: var(--wv-font-human); + + /* ---- Weights ---- */ + --weight-regular: 400; /* Inter body */ + --weight-medium: 500; /* nav, eyebrows, buttons, Space Grotesk text */ + --weight-semibold:600; /* Inter emphasis, ledger totals */ + --weight-bold: 700; /* Space Grotesk headings, wordmark */ + --weight-soul: 500; /* Fraunces italic pull-quotes */ + + /* ---- Fluid display sizes (clamp: min, vw, max) ---- */ + --text-h1: clamp(2.1rem, 5.2vw, 3.6rem); + --text-h2: clamp(1.6rem, 3.4vw, 2.4rem); + --text-h3: 1.2rem; + --text-lead: clamp(1.05rem, 1.8vw, 1.3rem); /* intro paragraph */ + --text-soul: clamp(1.2rem, 2.4vw, 1.7rem); /* hero pull-quote */ + + /* ---- Body / UI scale ---- */ + --text-body: 1rem; /* 16px base */ + --text-small: .95rem; + --text-fine: .92rem; + --text-eyebrow: .8rem; /* uppercase label */ + --text-tag: .72rem; /* pill tags */ + + /* ---- Line heights ---- */ + --leading-tight: 1.12; /* headings */ + --leading-body: 1.6; /* paragraphs */ + + /* ---- Letter spacing ---- */ + --tracking-display: -0.015em; /* headings + wordmark draw in slightly */ + --tracking-eyebrow: 0.12em; /* uppercase eyebrows open up */ + --tracking-tag: 0.08em; + --tracking-notice: 0.06em; + + /* ---- Measure ---- */ + --measure-lead: 60ch; + --measure-prose:68ch; +} diff --git a/frontend/src/ui/CodeInput.tsx b/frontend/src/ui/CodeInput.tsx new file mode 100644 index 0000000..32934e0 --- /dev/null +++ b/frontend/src/ui/CodeInput.tsx @@ -0,0 +1,46 @@ +// Six-cell one-time-code input: one real (invisible, full-bleed) carries the value +// and the OTC autofill semantics; the cells are presentation. Click anywhere to focus; +// paste works because the input is real. +import { useState } from "react"; + +const LENGTH = 6; + +export default function CodeInput({ + value, + onChange, + error = false, +}: { + value: string; + onChange: (code: string) => void; + error?: boolean; +}) { + const [focused, setFocused] = useState(false); + const digits = value.slice(0, LENGTH).split(""); + const active = Math.min(digits.length, LENGTH - 1); + + return ( +
+ setFocused(true)} + onBlur={() => setFocused(false)} + onChange={(ev) => onChange(ev.target.value.replace(/\D/g, "").slice(0, LENGTH))} + /> + {Array.from({ length: LENGTH }, (_, i) => ( + + ))} +
+ ); +} diff --git a/frontend/src/ui/kit.tsx b/frontend/src/ui/kit.tsx new file mode 100644 index 0000000..d666764 --- /dev/null +++ b/frontend/src/ui/kit.tsx @@ -0,0 +1,147 @@ +// The ecomm UI kit — React faces of the app.css primitives, matching the Claude Design +// export (ui/designs/ecomm-login-and-create-storefront-designs, hf-kit). Presentation +// only: no business rules live here (§6.2 — the SPA renders what the BFF returns). +import type { ReactNode } from "react"; + +export function Screen({ + children, + horizon = false, + plain = false, +}: { + children: ReactNode; + horizon?: boolean; + plain?: boolean; +}) { + const cls = plain ? "screen screen--plain" : horizon ? "screen screen--horizon" : "screen"; + return
{children}
; +} + +export function TopBar({ left, right }: { left: ReactNode; right?: ReactNode }) { + return ( +
+
{left}
+
{right}
+
+ ); +} + +export function Wordmark({ size = 26 }: { size?: number }) { + return ( + + + + ecomm + + + ); +} + +export function Footer() { + return ( + + ); +} + +export function AuthCard({ children }: { children: ReactNode }) { + return
{children}
; +} + +export function Eyebrow({ children }: { children: ReactNode }) { + return

{children}

; +} + +export function Banner({ + tone = "attn", + title, + children, +}: { + tone?: "attn" | "info"; + title?: string; + children: ReactNode; +}) { + return ( +
+ +
+ {title &&
{title}
} + {children} +
+
+ ); +} + +export function PrimaryButton({ + children, + busy = false, + disabled = false, + type = "submit", + onClick, +}: { + children: ReactNode; + busy?: boolean; + disabled?: boolean; + type?: "submit" | "button"; + onClick?: () => void; +}) { + return ( + + ); +} + +export function Field({ + label, + optional = false, + error, + helper, + inputProps, +}: { + label: string; + optional?: boolean; + error?: string | null; + helper?: string; + inputProps: React.InputHTMLAttributes; +}) { + return ( +
+ + + {error && ( +

+ {error} +

+ )} + {helper && !error &&

{helper}

} +
+ ); +} + +export function AccountChip({ email, onSignOut }: { email: string; onSignOut: () => void }) { + return ( +
+ + {email} +
+ ); +}