feat(slice-3): design-system foundation — Wiggleverse tokens, fonts, brand marks, UI kit (per ui/designs export)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 22:39:30 -07:00
parent cc9d9dda5f
commit ef1498ba08
17 changed files with 820 additions and 0 deletions
+3
View File
@@ -3,6 +3,9 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#0E1230" />
<style>html { background: #0E1230; }</style>
<link rel="icon" type="image/svg+xml" href="/brand/mark-tile.svg" />
<title>ecomm</title>
</head>
<body>
+8
View File
@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 120 120" fill="none" role="img" aria-label="Wiggleverse">
<title>Wiggleverse — mono gold</title>
<path d="M98 60 Q105 86 79 93 Q60 82 41 93 Q15 86 22 60 Q41 49 41 27 Q60 8 79 27 Q79 49 98 60 Z" stroke="#F4C76B" stroke-width="2.4" fill="none" stroke-linejoin="round"></path>
<g fill="#F4C76B">
<circle cx="98" cy="60" r="4.5"></circle><circle cx="79" cy="93" r="4.5"></circle><circle cx="41" cy="93" r="4.5"></circle>
<circle cx="22" cy="60" r="4.5"></circle><circle cx="41" cy="27" r="4.5"></circle><circle cx="79" cy="27" r="4.5"></circle>
</g>
</svg>

After

Width:  |  Height:  |  Size: 648 B

+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 120 120" fill="none" role="img" aria-label="Wiggleverse">
<title>Wiggleverse</title>
<rect width="120" height="120" rx="26" fill="#0E1230"></rect>
<path d="M98 60 Q105 86 79 93 Q60 82 41 93 Q15 86 22 60 Q41 49 41 27 Q60 8 79 27 Q79 49 98 60 Z" stroke="#F4C76B" stroke-width="5" fill="none" stroke-linejoin="round"></path>
<g fill="#EDEAFF">
<circle cx="98" cy="60" r="7"></circle><circle cx="79" cy="93" r="7"></circle><circle cx="41" cy="93" r="7"></circle>
<circle cx="22" cy="60" r="7"></circle><circle cx="41" cy="27" r="7"></circle><circle cx="79" cy="27" r="7"></circle>
</g>
</svg>

After

Width:  |  Height:  |  Size: 684 B

Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
View File
@@ -1,3 +1,4 @@
import "./styles/index.css";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
+394
View File
@@ -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; }
}
+37
View File
@@ -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");
}
+7
View File
@@ -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";
+60
View File
@@ -0,0 +1,60 @@
/* Wiggleverse Color tokens
Source of truth: wiggleverse-www/assets/tokens.css (brand BRAND.md §89).
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);
}
+55
View File
@@ -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 */
}
+53
View File
@@ -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;
}
+46
View File
@@ -0,0 +1,46 @@
// Six-cell one-time-code input: one real <input> (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 (
<div className={`code${focused ? " code--focus" : ""}${error ? " code--error" : ""}`}>
<input
className="code__hidden"
value={value}
inputMode="numeric"
autoComplete="one-time-code"
aria-label="One-time code"
autoFocus
maxLength={LENGTH}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
onChange={(ev) => onChange(ev.target.value.replace(/\D/g, "").slice(0, LENGTH))}
/>
{Array.from({ length: LENGTH }, (_, i) => (
<span
key={i}
aria-hidden="true"
className={`code__cell${i === active && focused ? " code__cell--active" : ""}`}
>
{digits[i] ?? ""}
</span>
))}
</div>
);
}
+147
View File
@@ -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 <div className={cls}>{children}</div>;
}
export function TopBar({ left, right }: { left: ReactNode; right?: ReactNode }) {
return (
<header className="topbar">
<div className="topbar__side">{left}</div>
<div className="topbar__side">{right}</div>
</header>
);
}
export function Wordmark({ size = 26 }: { size?: number }) {
return (
<span className="wordmark">
<img src="/brand/mark-tile.svg" width={size} height={size} alt="" />
<span className="wordmark__name" style={{ fontSize: size * 0.86 }}>
ecomm
</span>
</span>
);
}
export function Footer() {
return (
<footer className="footer">
<span className="footer__id">
<img src="/brand/mark-mono-gold.svg" width={16} height={16} alt="" />
ecomm · a Wiggleverse line
</span>
</footer>
);
}
export function AuthCard({ children }: { children: ReactNode }) {
return <div className="card">{children}</div>;
}
export function Eyebrow({ children }: { children: ReactNode }) {
return <p className="eyebrow">{children}</p>;
}
export function Banner({
tone = "attn",
title,
children,
}: {
tone?: "attn" | "info";
title?: string;
children: ReactNode;
}) {
return (
<div className={`banner banner--${tone}`} role={tone === "attn" ? "alert" : "status"}>
<span className="banner__icon" aria-hidden="true">
{tone === "attn" ? "◆" : "◇"}
</span>
<div>
{title && <div className="banner__title">{title}</div>}
{children}
</div>
</div>
);
}
export function PrimaryButton({
children,
busy = false,
disabled = false,
type = "submit",
onClick,
}: {
children: ReactNode;
busy?: boolean;
disabled?: boolean;
type?: "submit" | "button";
onClick?: () => void;
}) {
return (
<button className="btn-primary" type={type} disabled={disabled || busy} onClick={onClick}>
{children}
</button>
);
}
export function Field({
label,
optional = false,
error,
helper,
inputProps,
}: {
label: string;
optional?: boolean;
error?: string | null;
helper?: string;
inputProps: React.InputHTMLAttributes<HTMLInputElement>;
}) {
return (
<div className="field">
<label className="field__label">
<span>{label}</span>
{optional && <span className="field__optional">optional</span>}
</label>
<input
className={`field__input${error ? " field__input--error" : ""}`}
aria-invalid={!!error}
{...inputProps}
/>
{error && (
<p className="note note--attn" role="alert">
{error}
</p>
)}
{helper && !error && <p className="note">{helper}</p>}
</div>
);
}
export function AccountChip({ email, onSignOut }: { email: string; onSignOut: () => void }) {
return (
<div className="chip">
<span className="chip__avatar" aria-hidden="true">
{email[0]?.toUpperCase()}
</span>
<span className="chip__email">{email}</span>
<span className="chip__divider" aria-hidden="true" />
<button type="button" className="signout" onClick={onSignOut}>
Sign out
</button>
</div>
);
}