Flat spacing audit — 2026-09-08
Status: Active · Trigger: owner, 2026-09-08 — "many of the flat pages we've implemented don't seem to have proper spacing and alignment." · Method: local-stack seed (club_showcase) + web-render screenshot rung (scripts/screenshot-web.mjs) over every apps/web route named in the ask — home/records/profile/courts/activity tabs, session detail, the club home + all 5 member tabs (홈·일정·채팅·순위·회비), all 10 club 운영 screens (admin/members/attendance/dues/sessions/venues/challenges/settings/analytics/partners), discover, settings ×3, profile/friends, records history/leaderboard, directory-venue detail, messages, public profile — plus a full re-read of the flat primitives (FlatColumn, Divider, CellRow, StatStrip, FactRow, ListRow, RowList, SectionHeader) against docs/canon/components.md's Canvas/Divider/Seam-ladder/Group-spread/Hero laws.
Capture caveats (read before the findings)
- This machine runs several concurrent Claude/Codex sessions against the same local Supabase stack. The local DB was reset out-of-band by another session twice during this audit, invalidating two full screenshot passes (stale ids resolving to "클럽을 찾을 수 없어요"). Every screenshot cited below was re-captured after the final reset, against club
d304c404-ed2e-4509-9d91-2d26320169dd("강남 코트 클럽") — verified live in the DB at time of writing. - Headless Chromium in this sandbox does not load the lucide icon font/SVG sprite for a handful of icon call sites, so several screenshots show a "☒" tofu glyph where a real device renders a MapPin/Users/etc. icon (e.g. the region chip on club home, fact-glyphs on
/courts). This is a capture-environment artifact, not a code defect — verified by grep: those call sites use the canonicalIcon/FactGlyphcomponents, not a broken import. Not counted as a finding. /,/records(tab-root) and/sessions/<id>repeatedly screenshotted mid-query (skeleton state) despite the script's hydration wait — likely dev-mode query latency under the shared box's memory pressure, not a rendering bug. Not counted as a finding; re-verify these three on a quieter box before trusting a "clean" read.
Verdict
The flat-layout primitives (FlatColumn, Divider, CellRow/StatStrip, FactRow, ListRow, RowList, SectionHeader) are correctly built and contract-tested (packages/ui/src/__tests__/seam-contracts.test.ts locks every seam to its SEAM.* rung) — reading every primitive source file found no drift from the seam ladder, the divider law, or META_TEXT. Of the ~26 screens read, the great majority (club home, all 5 member tabs, 9 of 10 club-ops screens, courts, activity, dues, partners) render cleanly on-system: full-bleed section dividers at SEAM.section, spacing-only peer rows, chevron affordances present, trailing values right-aligned and vertically centered.
The one confirmed, repeated, code-verifiable defect is a StatStrip/CellRow composition bug: when a screen needs more stat cells than fit one row, two independent call sites hand-stacked two separate CellRow-based rows (3 cells then 2 cells) instead of sharing one grid. CellRow divides its own row's width into equal flex:1 columns — so a 3-cell row and a 2-cell row compute two different column widths for the same page width, and row 2's values drift out from under row 1's. This is exactly the "StatStrip cells uneven" symptom, confirmed pixel-for-pixel in two independent screenshots (own-profile hero, club admin snapshot) before the fix.
Ranked slip table
| # | Page(s) | Slip | Law broken | Evidence | file:line | Fix |
|---|---|---|---|---|---|---|
| 1 | /profile (own profile hero) AND /clubs/<id>/admin (attention snapshot) | Two stacked stat-cell rows (3-cell then 2-cell) each compute their own column width, so row 2's values don't sit under row 1's — a jagged, misaligned grid instead of one table | Group-spread law ("equal flex:1 columns... GAP is the constant") applied per-row instead of across the stacked group; the "StatStrip cells uneven" symptom named in the audit brief | profile.png: row 1 cell centers at ~x=80/230/370 (of 390pt viewport), row 2 at ~x=115/330 — visibly off-grid. clubs_..._admin.png: row 1 centers ~x=146/389/635 (of 780px@2x), row 2 ~x=192/559 — same drift | packages/features/profile/src/profile-screen.tsx:334-364 (before fix); packages/features/clubs/src/club-detail/admin-snapshot-strip.tsx:33-49 (before fix) | FIXED — added CellRowGrid primitive (packages/ui/src/cell-row.tsx) + chunkCellRowItems (packages/ui/src/cell-row-visual.ts) that pads every stacked row to the SAME column count; StatStrip gained an optional columns prop that routes through it. Both call sites now render one shared grid. |
| 2 | /clubs/<id>/analytics ("탐색" section) | The funnel-step list (SignalRow: 탐색 노출/일정 관심/탐색 피드/club_card/…) stacks label-then-value on two lines, while the stat cells one section above it (StatTile) render value-then-label — two different label/value orders on the same page reads inconsistent, though each is individually a legitimate distinct component (list rows vs. stat cells) | Not a named canon law violation — a content-shape difference, not a seam/divider drift | clubs_..._analytics.png, "탐색" section | packages/features/clubs/src/club-analytics-screen.tsx (SignalRow usage) | Not fixed — low confidence this is wrong by design; flagging for owner call before touching SignalRow's shared shape (it's also used by home's notification rows). |
| 3 | /clubs/<id> home hero | Club identity block shows two stacked badge-like rows (region chip with its own chevron, then NTRP badge) rather than one shared badge row under the title | Possible Hero canon drift ("a hero has ONE badge home: the badge row under the title... two badges max in a hero") — but the region row is a navigational multi-region entry (its own chevron), which the Hero canon doesn't explicitly cover | clubs_d304c404..._admin.png (home crop, same hero renders on every tab) | packages/features/clubs/src/club-detail/* hero composition (not traced to an exact file:line — ran out of audit budget) | Not fixed — needs a code trace to confirm before treating as a violation vs. an intentional navigational exception. |
Only three rows are listed — every other page read (club schedule/members/dues/chat/records tabs, club attendance/dues/sessions/venues/challenges/settings/partners ops screens, /courts, /activity, /clubs/discover, /settings, /settings/edit-profile, /settings/privacy, /profile/friends, /records/history, /records/leaderboard, /messages, /directory-venues/<id>) showed no seam-ladder, divider-law, or group-spread violations on this pass. That is a genuine finding in itself: the flat migration's foundational work (F0-F2) is holding up under a real cross-app read, and the remaining risk is narrow, repeatable composition bugs like #1 rather than widespread drift.
What was fixed (batch 1 of 1 — no second batch needed)
Root cause shared by both confirmed instances: a stacked multi-row stat/cell group must share one column grid across rows, never let each row size itself independently.
packages/ui/src/cell-row-visual.ts— addedchunkCellRowItems(items, columns), a pure function that splits a flat cell list intocolumns-wide rows, padding the final row withnullplaceholders so every row has the same column count.packages/ui/src/cell-row.tsx— addedCellRowGrid(cells +columns+dividers), which renderschunkCellRowItems's rows as stackedCellRows atSEAM.itemgap. Exported frompackages/ui/src/index.ts.packages/ui/src/stat-strip.tsx—StatStripgained an optionalcolumnsprop; whencolumns < cells.lengthit routes throughCellRowGridinstead of a singleCellRow. Every existing single-row call site is unaffected (prop omitted → unchanged behavior).packages/features/profile/src/profile-screen.tsx— thehasAttendancebranch's two stacked<StatStrip>calls (3-cell + 2-cell) collapsed into one<StatStrip columns={3} cells={[...5 cells]} />.packages/features/clubs/src/club-detail/admin-snapshot-strip.tsx— replaced the hand-rolledforloop + stacked<CellRow>s with<CellRowGrid columns={CELLS_PER_ROW} cells={cells.map(toCellRowCell)} />.
This is a primitive-level fix: any future screen that needs an N-cell stat/cell group where N doesn't divide evenly gets correct alignment for free via StatStrip columns={n} or CellRowGrid directly, instead of re-deriving the hand-stacking bug.
Re-screenshot verification
Re-captured /profile and /clubs/<id>/admin after the fix (see scripts/screenshot-web.mjs); both now render StatStrip/CellRowGrid cells with row 2 sharing row 1's exact column x-positions (visually confirmed against the pre-fix screenshots archived in the scratchpad web-shots/ directory of this session).
What remains for a next batch
- Row #2 (analytics SignalRow label/value order) and #3 (club-home hero badge-row count) need a code trace + owner call before any fix — left open rather than guessed at.
/,/records,/sessions/<id>need a clean re-capture off this shared, memory-pressured box to actually audit (every attempt here caught them mid-query).- This pass did not reach the wizard flows, session-detail sub-routes (attendance/edit/host/match-rules/participants/payments/scorecard), or the messaging thread view — out of the originally-scoped route list but worth a follow-up sweep if the owner wants full coverage.