Overview
Compare
Inspect
DetailShell title="" (the hero IS the identity — no header title, ever) → SessionLiveAnchor (self-gates on in_progress) → SessionHeroSection (page-native MediaHero/TypeHero: map band → club eyebrow → venue title, no titleBadge/facts/meter; the collapsing header overlays the band itself, white glyphs over a top scrim — MAP-BAR-INK, 2026-09-28) → ONE FlatColumn whose `lead` slot (HERO-ZONE, 2026-09-28 — owner: "they together form the hero") renders SessionAlertsSection (terminal banner · pending-applicants · refund · payment, ALL obligations in one zone right under the hero) → NoticeSection → LiveParticipantBar → SessionFactsSection (title-less, unconditional, FACTS-LEAD-DETAIL 2026-09-28: LEAD 내 상태 · 일시(+응답 기한) · 장소(+address) · 참가비 · any warning always shows; DETAIL 종목 · 대상 · calm 날씨 waits behind the canonical `ExpandToggle` chevron, EXPAND-TOGGLE 2026-09-28 — never a text link, closed on every visit) as ONE stack with NO divider between any of them → ParticipationSection (참석 {n}/{max}, unconditional, a badge ONLY for the 호스트/친구/예비 exceptions, the page's FIRST divider sitting right above it) → MatchesSection (경기, gated — null before a draw exists), with ONE pinned BottomCtaBand in the footer (ONE-BOTTOM-CTA, 2026-09-28). info-section.tsx is DELETED outright, not a separate unconditional section any more: its facts (경기 유형/경기 방식[+ELO 반영 여부] · 로테이션 · 취소 규정 · calm 날씨) fold into the fact list above — 경기 유형/경기 방식/로테이션 join the DETAIL layer's 종목 row (ELO+로테이션 as its secondaryValue), 취소 규정 and a rainy 날씨 surface in LEAD but ONLY as a warning when one actually applies (never unconditional), a calm 날씨 sits in DETAIL instead. The header ⋯ menu carries 공유/그룹 채팅/문의하기, THEN — folded in from the deleted HostToolsSection/DangerZoneSection — the host's own tools (참석자 관리 · 경기 규칙 · 호스트 관리 · 출석 확인, then red 경기 종료 · 일정 취소 · 일정 삭제), never a second copy. Two header-right glyphs beside ⋯, never three: a host gets the canonical pencil (edits in place), anyone else the bookmark. Non-members opening a public pickup see PublicPickupPreviewScreen instead: the SAME page-native hero + fact block, now ALSO in the `lead` slot (HERO-ZONE reached this screen too, 2026-09-28) plus one 참석 방식 row, no 정보 section, no host menu items, a static default header with no save/⋯, and PickupHostCard (deleted everywhere else) as its one identity card.
route · apps/mobile/app/sessions/[sessionId]/index.tsx → sessions/src/session-detail-screen.tsx
grounded in 20 source file(s)
packages/features/sessions/src/session-detail-screen.tsxpackages/features/sessions/src/public-pickup-preview-screen.tsxpackages/features/sessions/src/pickup-host-card.tsxpackages/features/sessions/src/session-detail/band/use-session-band-handlers.tspackages/features/sessions/src/session-detail/page/session-hero.tsxpackages/features/sessions/src/session-detail/page/session-info-group.tsxpackages/app/src/domain/session-info-model.tspackages/app/src/presentation/components/sessions/session-strip-ui.tsxpackages/app/src/presentation/components/status/status-badges.tsxpackages/app/src/domain/session-strip.tspackages/app/src/presentation/hooks/composites/use-live-session-data.tspackages/ui/src/media-hero.tsxpackages/ui/src/glyph-fact-row.tsxpackages/ui/src/detail-shell.tsxpackages/ui/src/collapsing-top-bar.tsxpackages/ui/src/section-head.tsxpackages/ui/src/banner.tsxpackages/ui/src/callout.tsxpackages/app/src/config/i18n/ko/sessions.tspackages/app/src/config/i18n/ko/home.ts
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 · 12 / 16 · 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
헤더 + 히어로 — collapsing bar overlays the map band
Structure
title
"" — a literal empty string (session-detail-screen.tsx:267, the sanctioned form since title stays required); the hero states the identity, so a header title would just repeat the same venue name. Stays blank even once scrolled solid — never sessionTitle() fading back in, unlike the pre-2026-09-20 hero-card era this replaces.
Component
A bare MediaHero (venue static map — NO mediaHint pill any more, MAP-NO-CHIP 2026-09-28) → club eyebrow → venue title (model.where.venue?.name, never sessionTitle()). NO titleBadge, NO facts, NO meter — every one of those retired keys (statConfirmedLabel/statMyFeeLabel/statMyStandingLabel/statMyStandingNone) is gone from i18n entirely. Falls back to a bare TypeHero (title only) when the session has no venue and no free-text location — not drawn as its own phone, same as before.
Header ink — Now (2026-09-28, MAP-BAR-INK)
the collapsing bar overlays the map band directly (collapsingInk="inverse", the default — collapsingInk="text"'s dark-glyph override retired): bare white ‹ / [pencil OR bookmark] / ⋯ over PhotoScrim's top scrim, same as a club's photo band. Solid $card chrome once scrolled past.
Header-right — Now (2026-09-28)
TWO glyphs beside ⋯, never three (session-detail-header-right.tsx): the HOST sees a pencil (opens the edit state — see create-session.jsx's EditStateFrame) in place of a bookmark ("saving your own session earns nothing"); anyone else sees the bookmark (SessionSaveButton). ⋯ (HeaderActionsMenu) carries 공유 (club loaded) · 그룹 채팅 (1+ confirmed) · 문의하기 (non-host signed-in viewer) FIRST, then — folded in from the deleted HostToolsSection/DangerZoneSection — the host's own tools; see the "⋯ 메뉴" row below. Not drawn open as its own phone (no menu-state convention on this board).
3
⋯ 메뉴 — 호스트 도구 + 위험 구역, folded in (ONE-BOTTOM-CTA)
Structure
Now (2026-09-28)
owner: "get rid of extra control at the bottom of detail pages … one main CTA at the bottom, the rest inside the menu or top right." HostToolsSection and DangerZoneSection (both DELETED files) no longer render as page sections on ANY state — the host phone below has no hostTools/danger zone any more.
Menu order
buildSessionHostMenuActions appends, after 공유/그룹 채팅/문의하기: 참석자 관리 · 경기 규칙 (disabled mid-match) · 호스트 관리 (title carries the pending-application count) · 출석 확인 (in_progress|completed, club-gated), then red-toned 경기 종료 (in_progress only) · 일정 취소 · 일정 삭제 (open|locked only for delete) — each still confirming first, same gates host-tools-gates.ts/buildHostCancelAction/canDeleteSession always used. 일정 수정 is NOT in this list — it is the header's own pencil toggle instead.
4
공지 — Well, no title
Structure
Component
NoticeSection — a Well tone="primary" recess with a 📣 lead, NO section title (a quiet notice pocket). Self-gates to null with no description. Sits AFTER alerts, BEFORE the live-participant utility and the fact block (session-detail-screen.tsx's real order) — not drawn on every phone (only where the sample session actually has a notice).
5
진행 중 밴드 — SessionLiveAnchor
Structure
Component
SessionLiveAnchor (decided 2026-09-17, docs/twomore_design_system/wireframes/live-anchor.jsx — full decision record there) — the FIRST flow child whenever in_progress, above even the hero. Coexists with the header's own titleLeading PulseDot (once scrolled past it) and stays the primary live signal — the hero itself carries no live badge any more (dropped along with every other hero badge/fact).
6
기권하기 — LiveParticipantBar
Structure
Component
Banner tone="error" with a single primary action — 기권하기 (participant) / 경기에서 빠지기 (a playing host steps out without ending the session). Its own zone, between the notice/alerts group and the fact block — inlined from the retired header kebab (Phase 4 flatten); GPS 체크인 does not exist any more (attendance is match-derived since migration 00384).
7
알림 — SessionAlertsSection, ONE zone
Structure
Component
The terminal-state Banner, the pending-applicants Banner, RefundBanners, and PaymentGateBanner ALL render from this ONE section, SEAM.block apart, right under the hero and above the fact block — never scattered mid-page. This phone shows PaymentGateBanner's reserved state (title "입금 필요" → "참가비 {amount}원" → hold countdown + hint → bank account → ONE primary action "입금 완료"; the 호스트 phone below shows the pending-applicants Banner instead). A submitted gate state swaps the Banner for a neutral Callout ("입금 확인 중") — not its own frame here.
8
상단 사실 — SessionFactsSection, two layers (FACTS-LEAD-DETAIL, 2026-09-28)
Structure
Why
Owner: the fact block "has gotten so long, a user has to scroll down to see the participants … is there a way to collapse or compact things down to the bare minimum". session-fact-rows.tsx's buildFactRows now returns {lead, detail} instead of one flat array (session-facts-section.tsx renders both onto the SAME FactList — one rhythm — under a shared YStack gap={SEAM.item}).
LEAD (always shows)
👤 내 상태 (host/confirmed/waitlisted only, a browsing viewer has none) → 📅 일시 (secondary line: 응답 기한, only while one is set and still ahead — RSVP-DEADLINE decision, 2026-09-24: never a past "…마감됨", the row keeps no line at all once it's unset or passed) → 📍 장소 (court count │ 실내/야외 │ surface as one FactSegments value, address as its secondary line — a fact new to this board; it was missing outright before this pass) → 💰 참가비, UNTIL SETTLED (FEE-SETTLED, 2026-09-28: feeSettledForViewer drops the row for the host, always, and a confirmed player who has paid/waived/free OR still owes it — the alert Banner above states that amount instead; kept for a browser, a waitlisted player, or a confirmed spot with no payment record) → [번개 미리보기's extraItems: 📝 참석 방식] → any WARNING, since a consequence never waits behind a tap: 📋 취소 규정 (a late-cancel penalty only) then ☔ 날씨 (only when conditions.rainRisk).
DETAIL (behind the chevron)
🎾 종목 (format │ event type │ 친선/경쟁, ELO + rotation on its own secondary line) → 🎯 대상 (실력 · 구성 · 최대 인원, one FactSegments value — label is "대상", never "실력") → a CALM ☀️ 날씨 (no rain risk). Joins the SAME list under the lead rows when opened — one visual rhythm either way, never a separate boxed panel.
The toggle — EXPAND-TOGGLE (2026-09-28)
SdExpandChevron (ExpandToggle/ExpandChevron, expand-toggle.tsx — the canonical collapse/expand control, owner: "stay away from text-based control buttons at all"). CHEVRON-ROW-RHYTHM (2026-09-28, owner: "if we are rendering the chevron as a separate row, we should give it proper spacing up and below"): the chevron is the fact list's own LAST ROW (FactList disclosure), not a free floating icon under it — 24px from the row above's text (its own rhythm) and 24px down to the divider, replacing an uneven 15/38. CHEVRON-NAMED (2026-09-28, owner: "look for best practices … always going for something clean and uncluttered"): the row reads "종목 │ 대상 │ 날씨" in tertiary MetaSegments ink, left, with the chevron trailing (ExpandToggle summary) — a chevron that hides a DIFFERENT kind of content from what already shows now names it, quietly, instead of staying a bare glyph. Points down at rest, turns up (180°) once opened; once open the named row disappears and only the chevron sits there, in the same trailing place — the DETAIL rows it named are now on screen. Closed on every visit — not persisted (useState(false) on mount) — so every phone on this board, a static snapshot, shows the CLOSED, NAMED chevron with the DETAIL rows absent; only present when detail.some(Boolean) (always true today — 종목 always renders). The retired SdDisclosureLink/DisclosureLink tinted-text link no longer exists anywhere in the real component tree.
Measured (390×844)
참석 rose from y=721 to y=639 (FACTS-LEAD-DETAIL) then to y=588 once FEE-SETTLED also drops 참가비 for a confirmed/host viewer (참가 확정·회비 대기, 호스트, 진행 중, 완료 below) — the first roster rows clear the fold without scrolling on every state, not only the ones that still show a price.
9
참석 {n}/{max} — BADGE-EXCEPTION
Structure
Title
One i18n template folds the count INTO the title — "참석 {confirmed}/{max}" (or "참석 {n}명" with no cap), sessions.ts:570 — so "전체 보기" sits alone on the right rail with no meta line crowding it. Capped at 6 combined confirmed+waitlisted rows + an "외 N명" overflow row.
Badge
The section title already says 참석 — a confirmed row carries NO status badge (never a "참석 확정" repeated on every row, screen-audit class C1/A9). Only 호스트 · 친구 (friends-going decision, 2026-09-24, precedence 나 > 호스트 > 친구 > 예비) · 예비 (RsvpStatusBadge) show one, in the trailing slot beside the chevron. Every row still navigates to the profile regardless — the chevron always shows.
10
경기 — gated on a real draw
Structure
Gate
matches-section.tsx self-gates to null on cancelled AND before a draw exists (zero matches) — screen-audit-2026-09-23.md class B3. The 참가 전/호스트 phones above have NO 경기 zone at all for this reason; the 진행 중/완료 phones here DO, because a round has actually been generated. Never an empty "아직 배정되지 않았어요" well before the event starts.
Component
SectionBlock + SessionMatchupsCard's per-match rows — each a composite MatchRowHeader+MatchScoreboard pair, too entangled to flatten into a ListRow this wave (the build spec's own "wrap + annotate" allowance).
11
정보 (RETIRED) — folded into the ONE fact list's DETAIL layer
Structure
Rows
Now (2026-09-27): merged into session-facts-section.tsx — no separate 정보 section on any phone below any more (the 5 member/host phones that used to draw one now just note it in a comment where it stood). Before: info-section.tsx — 경기 유형 (FIRST, session-event-type-plan.md D3/D5) → 경기 방식 (친선/경쟁) with its ELO consequence (반영/미반영) as the SAME row's secondaryValue (round-4 fix, 2026-09-24) → [날씨, its OWN live condition glyph] → [로테이션] → [취소 규정, warning tone]. Today (FACTS-LEAD-DETAIL, 2026-09-28): 경기 유형/경기 방식/로테이션 join the fact list's DETAIL layer as the 🎾 종목 row (behind the closed-by-default chevron); 취소 규정 and a rainy 날씨 surface in LEAD, but only as a warning when one actually applies; a calm 날씨 sits in DETAIL. Never a second, separate section.
Edit state
Same day: a host's header 수정 → 완료 toggle turns this whole merged fact list into the session RECORD's edit-state rows (session-record.tsx, useSessionFactEditing) — not drawn on this board (read state only); see create-session.jsx's EditStateFrame for the edit-state phone.
12
호스트 카드 — DELETED from the member screen
Structure
Now (2026-09-23)
host-callout-section.tsx (the old member-screen "호스트" card) is deleted — commit 3a4259e7, "consolidate host tools and overhaul player profile". PickupHostCard keeps exactly one mount site left, this preview phone. A signed-in member reaches the host through the header's 문의하기 action instead of a card; none of the 5 member-facing phones on this board show one any more.
13
하단 CTA · 상태 기계
Structure
open / reserved / host / live / completed
참석 확정하기 (common.confirmRsvp) · 참석 취소 (danger) · 초대 (common.invite — NOT "초대하기") · 실시간 스코어카드 보기 (sessionDetail.viewLiveScorecard — NOT "라이브 스코어카드 보기") · 매너 태그 남기기 — the same one-CTA-per-state precedence chain (refund-ack overrides everything else; host-on-live gets 경기 보드 instead of the scorecard link; host-on-completed-with-fees gets 정산).
14
번개 미리보기 — page-native here too
Structure
Component
public-pickup-preview-screen.tsx reuses SessionHeroSection/SessionFactsSection VERBATIM (not forked) — the SAME page-native hero and fact block the member screen uses, plus ONE extraItems row (참석 방식) spliced before 응답 기한. NO separate 정보 FactList (시작 시간/최대 인원 both retired — the hero states the time, 참석 counts render via the silhouette strip below) → 호스트 (PickupHostCard, the one surviving mount) → SilhouetteStrip (nameless gray circles, two hairline-separated facts) → the full location tile (VenueCard, map hidden — the hero band is already the map) → a plain notice line.
Header
DetailShell's DEFAULT (non-collapsing) header, title="" — blank, not sessionTitle()+date as the old static header drew. NO save toggle, NO ⋯ menu (non-member preview, neither action applies).