Overview
Compare
Inspect
Flat-canvas DetailShell ("상대 전적") over a FlatColumn with a lead StatStrip + an untitled callout RowList + a "주요 상대" RowList, gated error → loading-skeleton → unified empty → full data, with win-rate everywhere excluding draws from the denominator.
route · apps/mobile/app/records/analysis/rivals.tsx → records/src/head-to-head-screen.tsx
grounded in 2 source file(s)
packages/features/records/src/head-to-head-screen.tsxpackages/app/src/presentation/components/status/status-badges.tsx
Spec
1
Foundations · 디자인 시스템
Spacing · 4px grid
$1 – $4
4 · 8 · 12 · 16 px
$5 – $10
20 · 24 · 32 · 40 · 48 · 64 px
Radius
$2 8 · $3 12 · $4 16 · $7 9999 (pill)
Color · Classic Grass (light)
Surface
$background #F8FBF9 · $card #FFFFFF · $surfaceTertiary #E5FAEA
Brand
$primary #006D37 · $primarySubtle #E5FAEA
Text
$text #0D100E · $textSecondary #4F5851 · $textTertiary #626C64
Border
$border #CAD4CC · $borderSubtle #DFE7E1
Status
$success #14804A · $warning #946200 · $error #B8332E
Badge intents
accent #E0F8F3/#007B6B · warning #FFF4D6/#946200 · error #FDECEC/#B8332E
Court
$courtClay #B84A2E · $courtHard #247BA0 · $courtGreen #006633
Typography · Pretendard
cardTitle · navTitle
16 / 24 · 600 · 17 / 22 · 600
cardBody · cardMeta
14 / 22 · 400 · 13 / 18 · 500
badge · micro
12 / 18 · 600 · 10 / 14 · 600
display Sm/Md/Lg/Xl
20/28 · 28/36 · 36/44 · 48/56 · 700
Primitives · @twomore/ui
Layout
YStack · XStack · Card (default/flat) · Well · Divider
Controls
Pressable · Badge · Icon · PillNav · MainTabShell · BottomCtaBand
Data viz
DateTile · CapacityRing · ProgressBar · AvatarStack · SegmentedDonutChart
Domain cards
SessionCard · ClubCard · VenueHero · SessionCompactRow · member row
Box sizes
AVATAR XS 28 · SM 32 · MD 40 · LG 44 · 2XL 88 · TOUCH_TARGET 44
Rationale
Every part is built from these — one 4px grid, the Pretendard type scale, the Classic-Grass palette, and the shared @twomore/ui primitives + domain cards.
2
요약 · 전체 승률
Structure
StatStrip
FlatColumn lead (head-to-head-screen.tsx:127-149) — a bare StatStrip, no box: 승률 (tone primary) · 전적 (W-L-D) · 상대 수 (data.totalOpponents)
Math
win-rate = wins / (wins + losses) — draws NOT in the denominator (comment L101-102, math L103-111)
3
콜아웃 · 라이벌/천적
Structure
RowList
buildCalloutItems (L59-81) builds 0-4 rows from data.{rival, nemesis, mostPlayedOpponent, bestWinRate} — each nullable, rendered as a ListRow inside an untitled RowList (L151-165), no card, no SectionBlock
Name
title={c.record.opponentName} carries titleLines={NAME_LINES} (2026-09-17) — a long opponent name wraps to 2 lines instead of truncating with an ellipsis
Guard
renders nothing when calloutItems.length === 0 (L151) — the block simply disappears
4
주요 상대 · ListRow
Structure
List
SectionBlock variant="flat" title="주요 상대" → RowList of ListRows (L167-191) — avatar leading, record-only sub, dot-strip trailing, onPress → opponent profile; title carries titleLines={NAME_LINES} (2026-09-17), so a long name wraps to 2 lines
Dots
WinResultBadge mode="dot" dotSize={18} (status-badges.tsx:129-160) — a circle that CARRIES a 승/패/무 glyph, not a plain color fill
5
비어있음 · 통합 상태
Structure
Empty
!data || data.records.length === 0 → single EmptyState icon={Users} variant="full" 아직 상대 전적이 없어요, no subtitle (L124-125)
Note
no separate "no matches" substate — both collapse to this one check
6
로딩 · 플랫 스켈레톤
Structure
Skeleton
isLoading → SkeletonFlatPage kind="type" (L122-123) — 2 hero text lines + a 3-cell strip placeholder + 3 hairline avatar rows, never boxed SkeletonCards
7
오류 · 단독 상태
Structure
Error
isError → JUST a QueryErrorState (L113-118) — the strip/callouts/list never render