Skip to content

Latest commit

 

History

History
341 lines (259 loc) · 18.9 KB

File metadata and controls

341 lines (259 loc) · 18.9 KB

Selvedge — UI Guidelines

Single source of truth for frontend design decisions. Status: v1.0 · Documentation only — no components have been modified to produce this file.


0. How this document was produced

Before writing a single guideline, the existing codebase was audited directly (not assumed). Findings referenced throughout this doc are grounded in that audit:

  • 21 distinct font-size values in use across components/pages, with no shared type scale (e.g. 12px, 12.5px, 13px, 13.5px, 14px, 14.5px all present as separate ad-hoc values).
  • No spacing scale exists. Padding is hardcoded per-component (14px, 20px, 24px, 28px, 30px, 32px, 40px, 44px, 48px...) with no shared increments.
  • 9 different responsive breakpoints (380, 420, 460, 480, 520, 560, 720, 860, 900, 940px) instead of a consistent 3-tier system.
  • 18 separate CSS files independently redefine the same "card" pattern (border: 1px solid var(--line) + border-radius + background: var(--surface)) instead of extending one shared primitive.
  • Input styling is duplicated 5 times — Wardrobe's edit form, Upload's item form, Profile's edit form, and both Login/Signup forms each redefine their own input/select rules rather than sharing one.
  • Icons are Unicode glyphs (⌂ ⇧ ⌗ ↻ ☀ ☾ ⌘ ⚑ ✂ ♥ ✓ × ⋯ ☰ ▦ ⌕), not an icon library. These render inconsistently across OS/browser font stacks and can't be sized, stroked, or aligned reliably.
  • Button.css defines a primary variant that is never used anywhere in the app — every call site uses accent, outline, ghost, or danger. Dead code.
  • AuthForm.css exists twice, byte-identical, in pages/Login/ and pages/Signup/.
  • An unused, unreferenced hero.png (16KB) sits in src/assets/ alongside the actual SVG-based hero graphic.
  • A stray malformed directory (src/{assets,components,pages,styles,context}) was left behind by an earlier shell command — harmless but noted for cleanliness.

None of this is a criticism of the visual direction — the burgundy/brass "atelier" identity (Fraunces + Public Sans, garment-tag motif) is a deliberate, distinctive choice and should be preserved, not replaced. This document formalizes it into enforceable tokens so it stays consistent as the app grows.


1. Brand identity (unchanged, now formalized)

Selvedge reads as a boutique atelier, not a SaaS dashboard: warm paper tones, a serif display face, and a garment-tag/hanger visual motif. Every guideline below exists to protect that identity at scale — not to genericize it.


2. Color palette

2.1 Current tokens (keep as-is — already well-structured)

Token Light Dark Usage
--paper #EFEBE2 #131215 App background
--surface #FFFFFF #1C1B1F Cards, modals, inputs
--surface-raised #FAF8F3 #232128 Nested/hover surfaces, skeletons
--ink #19171B #EFEAE3 Primary text, primary buttons
--ink-soft #58534E #A79E93 Secondary text, captions
--line #DCD5C7 #322F35 Borders, dividers
--accent #6B2737 #C77E8C Brand accent, links, active states
--accent-soft #F4E4E6 #2E1E22 Accent tints (icon chips, badges)
--brass #B8935F #D4AF7A Secondary accent (tags, chart fills)
--brass-soft #F1E6D3 #2B2519 Brass tints
--success #3E6B4A same Positive states
--danger #A23B2E same Destructive states, errors

2.2 Gap to close

No --warning token exists. Add one for future use (form warnings, low-stock-style states):

--warning: #9C6B1F;
--warning-soft: #F6EAD2;

2.3 Rules

  • Never hardcode a hex value in component CSS. If a color isn't in the token list, that's a signal to extend the token list, not to inline a one-off.
  • --accent is used for exactly three things: interactive affordances (links, active nav, focused inputs), the brand accent chip, and primary CTA hover. It should never be used as a large background fill (reserve that for --ink).

3. Typography

3.1 Type scale (new — formalizes the 21 ad-hoc sizes into 10)

Add to variables.css:

--text-xs: 12px;    /* meta, timestamps, mono captions */
--text-sm: 13px;    /* secondary body, form hints */
--text-base: 14px;  /* default body copy, buttons */
--text-md: 15px;    /* comfortable body (hero lede) */
--text-lg: 16px;    /* card titles, emphasized body */
--text-xl: 18px;    /* section sub-headings, modal titles */
--text-2xl: 22px;   /* card display numbers, small H2 */
--text-3xl: 26px;   /* page H1 (compact pages) */
--text-4xl: 32px;   /* page H1 (standard) */
--text-5xl: 40px;   /* section H2 */
--text-6xl: 46px;   /* hero H1 */

Migration is additive: existing near-matches (12.5px→12, 13.5px→13, 14.5px→14/15, 24px→22, 30px→32) round to the nearest token with no visible regression.

3.2 Font families (unchanged)

  • DisplayFraunces — page titles, hero headline, card display numbers. Use weight 600–700; italic for the one-line editorial accent (as in the hero's "wear on cue").
  • BodyPublic Sans — everything else. Weights 400 (body) / 600 (labels, buttons, nav links).
  • MonoIBM Plex Mono — eyebrows, tag pills, chart labels, timestamps. Always uppercase + letter-spacing 0.06–0.14em when used as a label.

3.3 Rules

  • Headings never take font-weight above 700 or below 500.
  • Body text line-height: 1.5 default, 1.6–1.65 for lede/paragraph copy, 1.1–1.25 for display headings.
  • Letter-spacing on headings: -0.01em (already applied globally — keep).

4. Spacing scale

4.1 New token set (4px base unit)

--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--space-10: 40px;
--space-12: 48px;
--space-16: 64px;
--space-20: 80px;
--space-24: 96px;

4.2 Rules

  • Component internal padding: --space-4 (compact: chips, tags) to --space-6 (cards, modals).
  • Section vertical rhythm: --space-16 to --space-24 between major page sections (matches existing .section { padding: 72px 0 } closely — round to --space-16 + a bit, or introduce --space-18: 72px if exact preservation matters).
  • Grid gaps: --space-5 (20px) is the default for card grids — this is already the de facto standard across .wardrobe-grid, .outfit-grid, .favorites-grid. Formalize it, don't change it.
  • Never use an arbitrary px value for padding/gap/margin once this scale exists. If nothing fits, that's a sign the layout needs reconsideration, not a new one-off value.

5. Border radius

Keep the existing 3-tier scale, but name the two currently-unnamed values that recur everywhere:

--radius-sm: 6px;   /* inputs, tags, small chips */
--radius-md: 12px;  /* cards, buttons-as-blocks, dropdowns */
--radius-lg: 20px;  /* modals, hero panels, empty states */
--radius-pill: 999px; /* buttons, search bar, filter chips, toggles */
--radius-full: 50%;   /* avatars, icon circles, dots */

--radius-pill and --radius-full are currently written as raw 999px / 50% in 12+ places — tokenizing them costs nothing and makes intent explicit in a diff.


6. Shadows

Keep both existing shadows, they're well-tuned (soft, warm-tinted, dark-mode-aware):

--shadow-tag: 0 1px 2px rgba(25,23,27,0.06), 0 8px 20px rgba(25,23,27,0.06);   /* floating/rotated elements (garment tags) */
--shadow-raised: 0 4px 14px rgba(25,23,27,0.10), 0 1px 3px rgba(25,23,27,0.08); /* hover elevation, modals, toasts */

Add one for pressed/inset states (currently missing, would help form focus states feel more tactile):

--shadow-inset: inset 0 1px 2px rgba(25,23,27,0.06);

Rule: exactly two elevation levels exist by design (resting = border only, raised = --shadow-raised). Do not introduce a third — flatness is part of the brand's restraint.


7. Buttons

7.1 Variants (keep 4, remove 1)

Variant Use for Status
accent Primary CTA (one per view) ✅ keep
outline Secondary action ✅ keep
ghost Tertiary / low-emphasis (cancel, menu triggers) ✅ keep
danger Destructive confirm ✅ keep
primary remove — defined in Button.css but called zero times across the app. Delete to avoid a confusing 5th option next to accent.

7.2 Sizes

sm (14px pill, most-used — chips, table-row actions) / md (default) / lg (hero, form submit). This 3-tier system is already correctly used everywhere — no changes needed.

7.3 Rules

  • One accent button per screen/section maximum. Everything else is outline or ghost.
  • Icon-only buttons (avatar, theme toggle, modal close) are a separate pattern from Button.jsx — that's correct as-is, don't force them into the Button component.

8. Inputs

8.1 The gap

Five different files currently define their own input styling with near-identical but not-quite-matching rules (border color, padding, radius, focus treatment). This is the single highest-value consolidation target in the whole codebase.

8.2 New shared primitive

Introduce one canonical rule set — either as a new Input component or, at minimum, a single .field utility class in global.css that every form imports instead of redefining:

.field {
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: var(--space-3) var(--space-4); /* 12px 16px, was 10–12px inconsistently */
  font-size: var(--text-base);
  color: var(--ink);
  background: var(--surface);
  transition: border-color var(--duration) var(--ease), box-shadow var(--duration) var(--ease);
}
.field:focus { border-color: var(--accent); outline: none; box-shadow: 0 0 0 3px var(--accent-soft); }

Note the one real UX gap found: current inputs only change border-color on focus, with no visible focus ring beyond that — weaker than the rest of the app's :focus-visible accent-outline standard defined globally. Adding the box-shadow ring here brings inputs in line with buttons/links.

8.3 Labels

Standardize the label wrapper (currently repeated 5×: display:flex; flex-direction:column; gap:6px; font-size:12.5px; color:var(--ink-soft)) into .field-label alongside .field.


9. Cards

9.1 The gap

18 files independently write background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-*). None conflict destructively, but every one is a copy-paste of the same 3 lines.

9.2 New shared primitive

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  transition: transform var(--duration) var(--ease), box-shadow var(--duration) var(--ease);
}
.card--hoverable:hover { transform: translateY(-3px); box-shadow: var(--shadow-raised); }

Existing card-like components (ClothingCard, OutfitCard, StatsCard, .feature-card, .step-card, .chart-card, .profile-card, .settings-section) keep their own class for content-specific layout, but that class should extend .card (add card to the className string) rather than re-declare the base visual rules. This is a pure subtractive change — remove ~3 duplicate lines from each of the 18 files, add nothing new to markup logic.


10. Icon library

10.1 The gap

Unicode glyphs (⌂ ⇧ ⌗ ↻ ☀ ☾ ⌘ ⚑ ✂ ♥ ✓ × ⋯ ☰ ▦ ⌕) are used throughout for icons. Problems:

  • Rendering varies by OS font (emoji-style on some systems, dotted-box "tofu" on others for less common glyphs).
  • Cannot be sized, stroked, or color-transitioned independently of text — currently coupled to font-size + color, which is fragile.
  • Not accessible by default (need aria-hidden + separate label everywhere, currently inconsistent).

10.2 Recommendation

Adopt lucide-react (already available in this project's toolchain) as the single icon source. It matches the brand's restrained, linear aesthetic far better than default emoji glyphs, and every icon shares consistent stroke width/sizing props.

Suggested 1:1 replacements:

Current glyph Meaning Lucide icon
Favorite Heart
Confirm/selected Check
× Close/remove X
More menu MoreHorizontal
/ List/grid view List / LayoutGrid
Search Search
/ Theme toggle Sun / Moon
Upload Upload
Wear count / refresh RotateCw
Tags Hash or Tags
Flag/unworn Flag

The hand-drawn hero hanger SVG and garment-tag motif are the one deliberate signature illustration — that stays hand-crafted SVG, not swapped for a library icon. Icon library replacement applies only to functional UI icons, never to the brand's signature visual.


11. Animation guidelines

11.1 Current tokens (keep)

--ease: cubic-bezier(0.22, 1, 0.36, 1);
--duration: 220ms;

Already respects prefers-reduced-motion globally — this is correctly implemented and should not be touched.

11.2 Rules going forward

  • 220ms + --ease is the only transition timing for interactive state changes (hover, focus, toggle). Don't introduce a second duration for the same category of interaction.
  • Ambient/looping animation (the hero's floating garment tags) is the one place continuous motion is allowed. It should not spread to cards, buttons, or list items — those get discrete transitions only, triggered by user action.
  • Skeleton shimmer (Loader.css) is the only other continuous animation permitted, since it communicates active loading state.

12. Grid & layout rules

  • Container: max-width: 1180px, padding: 0 32px — already global via .container, correct, keep as the only container width in the app.
  • Card grids: grid-template-columns: repeat(auto-fill, minmax(Npx, 1fr)) where N is 210px (dense wardrobe grid) or 220–240px (outfit/favorites grids). Standardize on 220px as the single minimum card width across all three grids — the 210/220/240px split found in the audit is cosmetic drift, not intentional.
  • Two-column layouts (Upload form, Auth split-screen): 1fr 1fr or 0.85fr 1fr, always collapsing to single column at the md breakpoint (see §13).

13. Responsive breakpoints

13.1 The gap

9 distinct breakpoint values currently in use (380, 420, 460, 480, 520, 560, 720, 860, 900, 940px) with no naming or shared logic — each component picked whatever number made its own layout stop breaking.

13.2 New standard (3 tiers + 1 nav-specific)

--bp-sm: 480px;   /* phone */
--bp-md: 768px;   /* tablet / stacked two-column layouts */
--bp-lg: 1024px;  /* nav collapse, sidebar/grid breakpoints */

Since CSS custom properties can't be used inside @media directly, treat these as documented constants and hardcode the matching px value at each of the 3 tiers only — never a 4th number. The one justified exception is the navbar's own collapse point, which can stay wherever the link count actually wraps (currently 900px) — document it as --bp-nav-collapse: 900px in comments so it reads as intentional, not drift.

13.3 Migration

This is the lowest-risk, highest-clarity fix in this document: every existing breakpoint rounds to its nearest tier (380/420/460/480→480, 520/560→768 or stays a deliberate in-between only for nav, 720/768→768, 860/900/940→1024). Visually near-identical, dramatically easier to maintain.


14. Image & illustration guidelines

  • Product/item photography (clothing images): keep the current 3:4 aspect ratio and object-fit: cover treatment used in ClothingCard — correct for a fashion catalog and should propagate to any new photo-bearing component.
  • Placeholder images: currently sourced from loremflickr.com with keyword+lock params so demo content visually matches item names. This is correct practice for a portfolio demo — keep, but note in code comments that production would replace this with real user-uploaded + background-removed images.
  • Illustration (hero hanger, garment tags): hand-built inline SVG using CSS variables for stroke/fill so it themes correctly in dark mode — this is the right pattern and should be the template for any future custom illustration. Never hardcode a color inside an SVG that should adapt to theme.
  • Cleanup: src/assets/hero.png (16KB) is unused and unreferenced anywhere in the codebase — safe to delete, it's dead weight from initial scaffolding.

15. Component naming & styling conventions

  • File pairing: every component ships as ComponentName.jsx + ComponentName.css in its own folder — already consistent across all 13 components, keep this pattern for anything new.
  • CSS class naming: BEM-lite, already the de facto standard in this codebase (.clothing-card__media, .clothing-card__fav, .clothing-card.is-selected). Formalize it:
    • Block: .component-name
    • Element: .component-name__part
    • State: .is-state (e.g. .is-active, .is-selected, .is-open) — modifies a block or element, never a standalone class.
  • No inline style={{ }} for anything expressible in CSS. Current exceptions (Hero.jsx's per-tag rotation/depth, Statistics.jsx's dynamic bar heights, Footer.jsx's icon color override) are legitimate — they're driven by runtime data (loop index, computed percentage), which CSS classes can't express. Anything else should move to a class.
  • Shared primitives get a shared home. Once .card and .field exist (§8, §9), they belong in global.css or a new src/styles/primitives.css — not duplicated into whichever component needs them first.
  • De-duplicate AuthForm.css. It's currently byte-identical in pages/Login/ and pages/Signup/. Move it to src/styles/auth-form.css (or make it AuthLayout.css's responsibility, since both pages already share AuthLayout.jsx) and delete the second copy.

16. Quick-reference token additions summary

Everything below is additive to variables.css — nothing existing is removed or renamed:

/* type scale */
--text-xs: 12px; --text-sm: 13px; --text-base: 14px; --text-md: 15px;
--text-lg: 16px; --text-xl: 18px; --text-2xl: 22px; --text-3xl: 26px;
--text-4xl: 32px; --text-5xl: 40px; --text-6xl: 46px;

/* spacing */
--space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px;
--space-5: 20px; --space-6: 24px; --space-8: 32px; --space-10: 40px;
--space-12: 48px; --space-16: 64px; --space-20: 80px; --space-24: 96px;

/* radius additions */
--radius-pill: 999px; --radius-full: 50%;

/* shadow addition */
--shadow-inset: inset 0 1px 2px rgba(25,23,27,0.06);

/* color addition */
--warning: #9C6B1F; --warning-soft: #F6EAD2;

This document reflects the state of the codebase as audited. It should be updated whenever a new pattern is deliberately introduced — if a component needs something not covered here, that's a signal to extend this file first, then build.