Flat layout — implementation plan (track F)
Status: Active · Owner ruling (2026-09-07): one grammar keyed on content shape; every detail page, list/settings shell and club surface on the flat canvas; object cards only inside browsing collections; device verification waits until the flat layouts are properly built. Spec: the approved boards on the cockpit review lane (/wireframes), docs/canon/components.md (Canvas, Divider, Seam-ladder, Group-spread, Hero canon, Content-inset laws), docs/design/decision-log-detail-pages.md.
Slices follow the implementation plan contract. Execution follows the slice-completion loop with one substitution the owner made: stage 7's device check is replaced by the web-render screenshot rung (F0-c) until F8.
Why this order
The session card is rendered on ten list sites and inside the club card and club detail; the shells and the flat primitives are read by every page. Anything built on top of a foundation that then moves has to be redone. So: foundation and the card first (F0, F1), then the club detail as the reference page every other page copies (F2), then the mechanical migrations (F3, F4), then the feature slices whose boards are approved (F5–F7), and only then the default flip and the deferred device pass (F8).
Inventory (2026-09-07, from the code)
- Flat primitives that exist and are contract-tested:
FlatColumn,SectionBlock variant="flat",SectionHeader(+link),ListRow,FactRow/FactList,RowList,StatStrip/CellRow,ActionIconRow,Well,Callout,Banner,MediaHero,TypeHero,DetailShell ground,PlaceShell,BottomCtaBand,Divider,PillNav variant="underline",MiniBarMeter,SEAM,CONTENT_INSET/SCROLL_CONTENT,HAIRLINE(packages/ui/src/__tests__/seam-contracts.test.ts). - Kit mocks with no real counterpart:
WFSessionCardV3/WFSessionStateWord(the no-strip card),WFRankRow(real isLeaderboardRow+ ranking cells). Kit API drift:FlSectionTitle rightis a string; the realSectionHeader linkis{ label, onPress }. - Tinted shells: 50 files render
DetailShellon the default tinted ground onceapps/routes and Codex's venue-contribution/owner-claim screens are counted (scripts/tinted-shells-baseline.jsonis the authoritative list; the feature-screen core is activity 2, clubs 14, home 1, messaging 4, profile 13, records 3, sessions 1). 20 screens are already onground="canvas". <Cardin features: 134 uses / 88 files — ~6 object cards (stay), ~40 modal/form sections (stay), ~30 files of utility chrome on detail/list pages (flatten).StatGrid: one consumer (member-stats modal, allowed). InlineStatTileclusters: club attendance (4), dues summary (3), pickup host card (2) — all becomeStatStrip.- Session strip consumers:
session-strip-ui.tsx,session-compact-row.tsx,session-card-v2.tsx,club-card.tsx,club-status-banner.tsx,session-detail-screen.tsx,closed-banner.tsx,use-session-detail-model.ts,home-today-view.tsx,live-session-stack.tsx, domainsession-strip.ts+ 3 tests.
Slices
F0 — Foundation: contracts, parity, verification rung · Accepted 2026-09-07 (kit parity test, tinted-shell ratchet at 50, yarn screenshot-web rendering 8/8 default routes)
| Field | Value |
|---|---|
| Primary artifact | A foundation that later slices can build on without moving: kit ↔ code parity, a tinted-shell ratchet, and a web-render screenshot rung |
| Allowed surfaces | packages/ui/src, docs/twomore_design_system/wireframes/_kit.jsx, scripts/, package.json scripts, docs/canon/components.md |
| Blocked scope | No screen migrations; no session-card change (F1) |
| Start condition | This plan committed |
| Acceptance proof | (a) _kit.jsx FlSectionTitle takes link={{label}} like the real SectionHeader; WFRankRow documented as LeaderboardRow + cells; every Fl* metric matches seam-contracts.test.ts (a parity test reads both). (b) scripts/check-tinted-shells.mjs + baseline (37) wired into yarn check: a new tinted shell fails, a migrated one lowers the baseline. (c) scripts/screenshot-web.mjs: boots apps/web against the local stack seeded by yarn qa:seed --local, logs in as the dev account, screenshots a route list at 390×844 with the local Chromium, writes PNGs to the scratchpad; documented in docs/workflows/qa-automation.md as rung 2b. |
| Verification | yarn check; one screenshot run over five current flat pages (session detail, public club profile, post detail, record detail, venue detail) that renders without error |
| Stop condition | If the web target cannot render a feature screen (native-only module), record the route as native-only in the script and move on; do not port modules |
| Deferral rule | Findings → this file, "Deferred" |
| Handoff output | The three artifacts above + a rebuild-log entry |
F1 — Session card v3: retire the top strip · Accepted 2026-09-07 (SessionStateInline on the closing row, bookmark on the venue line, SessionStripChipBadge in the detail hero, terminal Banner; rung-verified on home, activity, session and club detail)
| Field | Value |
|---|---|
| Primary artifact | SessionCardV2 without the band, per /wireframes/session-card: venue line = venue · weather · bookmark; closing row = state word + tone (pulse when live) · standing badges (호스트 · 참석/예비) · meter |
| Allowed surfaces | packages/app/src/domain/session-strip.ts (becomes the state/standing model), session-strip-ui.tsx, session-card-v2.tsx, session-compact-row.tsx, club-card.tsx (row dot tone), session-detail-screen.tsx + use-session-detail-model.ts (detail band → hero badge row), home-today-view.tsx, live-session-stack.tsx, the 3 domain/feature tests, i18n keys, the strip-map board (→ decided/superseded) |
| Blocked scope | Terminal banners (club-status-banner, closed-banner) stay banners; no card-list site changes beyond the card itself |
| Start condition | F0 accepted; the owner has seen the card board (2026-09-07) — the bookmark placement is the owner's; confirm nothing else is open before the first commit |
| Acceptance proof | Card renders with no band on all ten list sites; a saved card shows the bookmark right of the weather; live/closing/open/waitlist render the state word with tone; SessionStripView size="detail" is gone and the detail hero's badge row carries the state; strip-map board archived; screenshot rung shows home, activity, club schedule, session detail |
| Verification | yarn check; updated session-strip.test.ts / session-info-model.test.ts / session-detail-screen.test.tsx; screenshot rung |
| Stop condition | If the detail screen needs a new primitive for terminal states beyond Banner, stop and design it on the board first |
F2 — Club detail: six tabs flat + home v2.2 · Tabs accepted 2026-09-07; home v2.2 held as F2b (the shipped home tab is already the flat feed; v2.2 is a reorder that wires rank/group/gallery hooks into the home tab — build on the owner's word)
| Field | Value |
|---|---|
| Primary artifact | /wireframes/club-detail-redesign built: home v2.2 (next-session card v3, vertical 이후 일정 rows, two news rows, one match row, 내 조/내 순위 fact rows, gallery grid), 일정 as compact rows grouped 예정/완료 (no card boxes inside the detail page), 채팅 rows as-is, 랭킹 as-is, 멤버 role sections divided by the column with spacing-only rows, 회비 with a bare month row + StatStrip lead + Well bank info + flat dues rows; 소식→게시판, 기록→랭킹 renames (i18n only) |
| Allowed surfaces | packages/features/clubs/src/club-detail/*, club-dues-screen.tsx (+ dues/*), club-members/member-row.tsx (hairline → spacing-only under RowList), i18n ko/en/clubs.ts, tests |
| Blocked scope | Option-3 IA (게시판 as a tab, 관리 into overflow) stays held; ops screens are F3 |
| Start condition | F1 accepted (the home tab embeds the card) |
| Acceptance proof | Each tab matches its board phone in the screenshot rung; club-members-screen.test, club-dues-screen.test updated; no <Card under club-detail/ except the club card itself |
| Verification | yarn check; screenshot rung on the six tabs with the seeded club |
F3 — Club ops: 관리 + six 운영 screens flat · Accepted 2026-09-07 (baseline 50 → 43; rung-verified on the seven web routes)
| Field | Value |
|---|---|
| Primary artifact | /wireframes/club-ops built, order 회비 (shared with F2) → 출석 → 멤버 → 관리 → 코트 → 대항전 → 일정 |
| Allowed surfaces | club-admin-screen.tsx + admin-nav-row.tsx (→ ListRow), club-members-screen.tsx + members-list-header.tsx (three Cards → ListRow entries with count badge), club-attendance-screen.tsx (StatStrip lead; row = name + MiniBarMeter + rate, no 4-tile cluster; StrikeRecordRow unboxed), club-venues-screen.tsx (+ a flat variant of venue-row.tsx), club-challenges-screen.tsx (StatStrip lead + grouped rows), club-sessions-screen.tsx (card v3 only), all on ground="canvas" |
| Blocked scope | Analytics/settings screens (F4) |
| Start condition | F2 accepted |
| Acceptance proof | Seven screens match their phones; tinted-shell baseline drops by 7 |
F4 — Canvas migration of the remaining shells · Accepted 2026-09-07 (a9c0a84f · deb191e2 · round three; baseline 35 → 0)
Round one (settings cluster 8 + profile forms 5 + records 3 + home 1 + courts 1 + payments + content-page): SettingsRow is a ListRow wrapper and SectionCard is retired; forms sit on FlatColumn with Banner errors and BottomCtaBand CTAs; head-to-head leads with a StatStrip. Round two (messaging 4, activity 2, clubs misc 7, share routes 2, profile 2): every remaining shell on the canvas; Divider seam="section"/bleed replaces the four hand-rolled section seams; lint @twomore/no-hand-hairline. Round three: the eight club-settings section files unboxed (rows as ListRow/RowList, summaries as Well/FactList, add tiles as AddCard). Left for later slices: EditFieldRow's own row divider (a shared primitive whose change ripples), the ClubCard (F5), the web rung's short-page ground artifact (native fills the screen), and a pre-existing flash: the club-settings 정기 일정/조 관리/게스트/모집 계획 sections render their empty Well while their queries are still loading (no loading gate is passed down) — the rung caught it as an "empty" capture of a seeded club.
| Field | Value |
|---|---|
| Primary artifact | Every DetailShell on ground="canvas" with its utility chrome flattened, in risk order per /wireframes/canvas-migration (decided): batches of ≤7 files — (1) profile/settings 13, (2) records 3 + home 1 + courts 1, (3) messaging 4 + activity 2, (4) clubs misc (admin, album, analytics, settings, discover, join, post-create, partner-suggester) |
| Allowed surfaces | The listed screens + their class-(c) utility cards (summary/entry/hero cards → TypeHero/StatStrip/ListRow/Well); tests that mock Card |
| Blocked scope | Modal/form-section cards stay; object cards stay |
| Start condition | F3 accepted |
| Acceptance proof | Tinted-shell baseline reaches 0; each batch screenshot-verified; group-spread law holds (no space-between peer rows introduced — lint @twomore/no-raw-content-inset + a grep gate in the batch review) |
F5 — Club cards and the public profile · Card shipped 2026-09-07 (D23-E); guest hub pending
Owner rulings 2026-09-07: D23-E is the card — recruitment status top-right, capacity chip on the seam, terms row (join policy, dues) below the identity row. Shipped in club-card.tsx / club-photo-seam.tsx / club-card-model.ts with kit parity. The PublicClubProfileScreen guest hub half of this slice is still planned.
| Field | Value |
|---|---|
| Primary artifact | Discover ClubCard: recruitment status (N명 모집 중) top-right, capacity chip stays on the seam, terms chips as the bottom row (D23-E); PublicClubProfileScreen guest hub: proof strip, locked 게시판/랭킹/멤버 rows, gallery before description, CTA sublabel |
| Allowed surfaces | club-card.tsx, club-photo-seam.tsx, club-card-model.ts (proof signals already computed), public-club-profile/*, join-cta-band.tsx (sublabel slot on BottomCtaBand) |
| Start condition | F1 accepted; the owner confirms the terms-row placement (open note on the clubs board) |
F6 — Guest → member review flow · Planned
| Field | Value |
|---|---|
| Primary artifact | /wireframes/guest-roster built: 심사함 club-wide inbox (read model over club_join_request + per-session guest_applications + club_guests.proposed), the guest profile page (identity hero, participation StatStrip, qualification FactRow checklist, visit-history rows), flat roster (club-guests-section), candidate signals for nominate_for_membership and membership_recommendations, guest fee on the session_payments hold row |
| Allowed surfaces | One migration (inbox RPC + two signals_emit calls + fee hold seed), join-review-detail-screen.tsx, club-members/*, settings/club-guests-section.tsx, members/nominate-guest-modal.tsx, hooks/adapters via @/registry, pgTAP |
| Start condition | F3 accepted; the owner confirms the guest-fee settlement authority (session host or canManageDues) |
F7 — Venue finder streamlining · Shipped 2026-09-08 (batches A–C + 클럽 코트 pick-intent)
Owner rulings 2026-09-07: yes to the ship-now set; hours standardized (one parser over the three production shapes) with client-side open-now; recent searches and 최근 본 코트 per device; near-me default only when permission is already granted, never a prompt on open; the trust badge only for owner-verified venues; 클럽 코트 pick-intent explained, decision still open. Shipped: list projection widened (rating, review count, hours, telephone, fee schedule), parseOperatingHours/isVenueOpenNow/isVenueFree in domain/venue, 무료 and 영업 중 as client-side facets, verification field removed from the finder, card v3 (rating + review count, open-now word, shop card 영업시간·전화), near-me default from useUserLocation. Data finding: only 28 of 645 full courts carry hours and 304 carry a phone on production; 234 phone-less courts have a Naver place id — a refresh, not a UI fix.
The research pass (board vs code) found that rating/reviewCount/operatingHours only need VENUE_LIST_COLUMNS widened (the entity and mapper already carry them; no migration), but six things the board treats as settled are not: (1) 최근 검색/최근 본 코트 have no persistence anywhere — a new MMKV-backed store (shape, cap, dedup) is a design decision; (2) "near-me default when granted" has no defined gating (today 가까운 순 is an opt-in toggle that prompts for permission); (3) 지금 영업 중 needs a real open-now predicate — the only existing hours predicate is open_24h's regex and operating_hours JSONB is documented as not standardized — server-side predicate or client parse; (4) pick-intent 클럽 코트 needs club context threaded from the wizard steps, which are outside the allowed surfaces; (5) the shop card's 전화 needs telephone in the list projection, which the board's data note omits; (6) the board's card mock shows 미확인, which canon/venue-data.md forbids — the real card keeps VenueTrustBadge's certification path. Recommended path: ship (a) projection widen + card v3 rating/open-now (client-side open-now from a standardized hours parser with a null fallback that hides the dot) + 무료 facet + verification field removal + telephone in the shop card now; hold (b) recent searches, near-me default and 클럽 코트 until the owner rules on store shape, gating and the club-context prop path.
| Field | Value |
|---|---|
| Primary artifact | /wireframes/venue-finder built: near-me default when location is granted, zero-query state (저장 → 최근 → 가까운; pick: 클럽 코트 → 최근 선택 → search), recent searches under the field, 영업 중 · 무료 facets, verification toggle type removed, card v3 (rating + open-now), shop card 영업시간·전화 |
| Allowed surfaces | venue-finder/*, venue-filter-bar.tsx, directory-venue-card.tsx, venue-search-takeover.tsx, use-venue-directory.ts + the list projection (rating/reviewCount/open-now into the list SELECT), one migration if the RPC changes, i18n venues.ts, tests |
| Start condition | F1 accepted (no dependency on F2–F6); the Codex venue branch is merged (done 2026-09-07) |
F8 — Default flip and the deferred device pass · Code shipped 2026-09-07 — device pass open
DetailShell is canvas-only: the ground prop is gone (swept from 67 screens), resolveDetailShellContentDefaults takes no ground, scripts/check-tinted-shells.mjs and its baseline are deleted, shell-ground.test.ts covers the canvas defaults. MainTabShell keeps the tinted option: three of its four consumers (the main tabs other than 프로필) still ride the legacy ground — the owner's call, not part of this track's ruling. Boards: only venue-finder is approved (F7 not built), so nothing flips to decided yet. Remaining: the owner's on-device verification of the venue release and the flat layouts together on preview.
| Field | Value |
|---|---|
| Primary artifact | DetailShell default ground = canvas, the tinted option and shell-ground.test.ts retired, check-tinted-shells deleted; the approved boards flipped to decided; then the owner's on-device verification of the venue release and the flat layouts together (preview OTA) |
| Start condition | F4 accepted and F1–F3 on preview |
Working rules for every slice
- Read the board and the canon paragraphs before touching a file; the board is the spec, the code is the truth for what exists.
- Compose only from the flat primitives; a screen never types a seam value, a raw inset, a
space-betweenpeer row, or a hairline of its own. - Agents: sonnet, ≤3 parallel, ≤7 files each, never
git stash/checkout/reset; Opus reviews every diff against the board before commit. - Every slice ends with:
yarn check, the screenshot rung, a rebuild-log entry, the board status updated, the tinted-shell baseline lowered.
Deferred
- (empty)