Component Library Day: 17 Components From Scratch
Bare Next.js skeleton to a near-complete component library in a single day. 17 components across three phases, all hand-typed to internalize the patterns. Here’s how it came together.
Morning: API smoke tests + design system overhaul
Started the day by writing 17 integration tests against the FastAPI backend. All 14 endpoints covered. Two bugs surfaced immediately: httpx 0.28 broke Starlette’s TestClient (downgraded to 0.27), and CLIP choked on a 1x1 PNG test image (swapped to a 4x4 PIL-generated one). Quick fixes—17/17 green.
Then gutted the frontend’s color system. Swapped Playfair Display for Cormorant Garamond, replaced the entire vintage.* Tailwind palette with flat tokens matching the Figma handoff:
- Core palette: terracotta, gold, sage, cream, charcoal
- Platform-specific: The Met, Smithsonian, and Fashionpedia each get their own accent color
Updated all four UI primitives—Button, Card, Badge, Input—to use the new tokens. Clean build.
Afternoon: search + bridge components
Search components
Three components, each with a distinct interaction pattern:
- SearchBar — debounced input (400ms) with instant-fire on Enter, large/compact variants
- ImageUpload — drag-and-drop with
FileReaderbase64 conversion, camera icon on mobile - ProductCard — the first real composition challenge. Uses a union type (
SearchResult | ProductSummary) with a type guard so the same card works in search results and bridge displays
Bridge primitives
Then knocked out 7 of 10 bridge primitives:
- PlatformBadge / EraBadge — frosted-glass overlays for images
- ScoreCircle — circular match percentage (terracotta > gold > muted based on score)
- BridgeConnector — gold circle with exchange icon
- AttributePill — shows shared DNA (“SILHOUETTE • Fitted bodice”)
- NarrativeBlock — italic serif blockquote for AI-generated bridge stories
- ScoreBreakdown — three horizontal bars for semantic/visual/structural scores
Each one is a small, focused server component. No "use client" except where hooks are needed (SearchBar, ImageUpload). The payoff comes next: BridgeCardFull composes all 7 primitives into a single card.
Patterns that clicked
Runtime colors need inline styles
Tailwind generates classes at build time, so bg-${color} doesn’t work when the color comes from a JS object. Platform badge colors and score circle borders use style={{ color: platformColor }} instead. Straightforward once you remember Tailwind’s static extraction model.
Union types with type guards
ProductCard accepts both search results and product summaries. A simple “score” in p discriminator lets TypeScript narrow the type at runtime without any casting:
type ProductCardProps = SearchResult | ProductSummary;
function isSearchResult(p: ProductCardProps): p is SearchResult {
return "score" in p;
}
One component, two shapes, zero as assertions.
The .map() with early return pattern
ScoreBreakdown maps over three bars but returns null for image similarity when it’s null (not every bridge has image data). Took a minute to grok the nested returns—the outer function returns JSX, the .map() callback returns JSX per item, and either level can bail early. Once that layering was clear, the pattern felt natural.
What’s next
- BridgeCardFull — the hero component, composing all 7 primitives with responsive image pairs (stacked mobile, side-by-side desktop)
- BridgeCardCompact — carousel variant (280px fixed width)
- Barrel export + utility components (Skeleton, ImageWithFallback)
- Then the four pages: Home, Search, Product Detail, About
The component library is almost done. Pages are where it all comes together.
Vintage Vestige — a fashion intelligence platform connecting garments across 500 years of design history. Built with Next.js, TypeScript, Tailwind CSS, and React.