Rhythm, flow, spacing and alignment audit — every screen, every primitive
Status: Active · Date: 2026-09-06 · Owner ask: "from the very top component to the bottom component, the visual rhythm and doneness can be broken if spacing and alignment are not properly worked out. Inspect every single page for rhythm, flow, spacing and alignment. Don't leave a primitive, component or the composition of them unturned. All things should be configuration-driven, not hard coded."
Scope: 560 .tsx files read in full — packages/ui/src (114), packages/app/src/presentation/components (69), packages/features/{sessions,clubs,profile,records,home,messaging,onboarding} (377). Measured against the seam ladder (packages/ui/src/seams.ts), the divider law, the row-group rule (CellRow), the META_TEXT grammar, graphics-lead, badge placement, the affordance law and the canon shells (canon/components.md).
1. Verdict
The primitives are sound. The rhythm breaks between them: screens re-state seams the shell already owns, and the values they re-state have forked. Nothing here needs a redesign; it needs one owner per seam and the numbers moved into configuration.
| axis | count |
|---|---|
Screens on-system (shell → hero → one FlatColumn → band) | 13 of ~68 |
| Screens partially on-system (right shell, hand-rolled scroll rhythm) | ~54 |
| Screens off-system (no shell / bespoke geometry) | 2 + 4 dead legacy files |
Hand-written contentContainerStyle padding literals | ~50 sites, tail forked across 16/24/32/48/56/96/100/104 |
| Middle-dot joins still rendering | 31 sites |
Hand-rolled divider index math bypassing RowList dividers | 6 + ~10 private hairlines |
Primitives bypassing META_TEXT for a label slot | 3 (SpecList, EditFieldRow, WizardReviewRow) |
| Primitives re-deriving a seam with a raw token | 1 (SkeletonFlatPage) + 5 chrome hairlines |
Raw box sizes that should be box-sizes.ts constants | ~16 |
Raw radii / opacities without a constant or reason: | ~14 / ~13 |
| Undocumented live hex literals | 0 |
role + raw lineHeight override (hard ban) | 1 (venue-confidence.tsx:239) |
2. The ten breaks, ranked by what a user feels
- Scroll-tail fork. Every list/detail screen ends with a different bottom gap. ~50 sites paste their own
contentContainerStyle; 20 of them re-stateDetailShell's own default byte-for-byte. Fix:SCROLL_CONTENTpresets in@twomore/ui(packages/ui/src/content-insets.ts), every literal replaced, lint rule@twomore/no-raw-content-inset. - Legacy card-on-tinted-ground grammar on 10+ live session screens (payments, host dashboard, tournament board, attendance, participants, scorecard tabs) while the four flagship detail pages are flat. The ground and chrome change under the user mid-flow. Fix: wave 5 of the flat migration (design-level, queued for owner).
- Onboarding value props — the first screens a new user sees carry ~15 bespoke radii/opacities/sizes. Fix: token sweep now; composition review with the owner.
- Three primitives bypass
META_TEXTso settings rows, wizard review rows and spec sheets render their label one tier darker than every other label. Fix:META_TEXT.secondaryin all three;SpecListretires intoFactList. - 31 middle-dot joins on the rows users scan most (rosters, leaderboards, match rows, venue cards, DM lists). Fix: stacked meta lines / fact rows; drop each file from the ratchet waiver.
- Six copies of the same tail literal inside club-detail's tab panes, nine across the scorecard tabs. Fix: one preset per family (item 1).
- Six hand-rolled divider loops (
idx > 0 && <Divider/>) with insets already diverged (fullvscard). Fix:RowList dividers. - Two independently invented mini bar meters (
venue-confidence.tsx3.5/1 vsfeasibility-tiles.tsx4/2). Fix: oneMiniBarMeterprimitive. SkeletonFlatPagere-derivesFlatColumn's ladder with bare$Ntokens and its own divider loop, so the loading silhouette only coincidentally matches the loaded page. Fix:SEAM.*,AVATAR_SIZE,RowList.- Muted states at four different strengths (0.35 / 0.5 / 0.7 / 0.7) beside the one named
TERMINAL_MUTE_OPACITY. Fix: the constant, or a named sibling.
Also: push-permission-soft-prompt-sheet is the one sheet on a 20px rail (every sibling is 16); sheet scroll tails differ (24 vs 32); session-card-v2's map pin carries six undocumented literals; four dead legacy session cards (session-identity-card, session-status-participation-card, session-venue-card, session-weather-card) survive only through a stale test and must go; naver-badge 22px and the records rank column widths (18/28/36/48/50) are unnamed.
3. Full findings
The two per-package reports are reproduced verbatim below so every file:line stays citable.
3.1 packages/ui and features/{clubs,profile,records,home,messaging,onboarding}
See §A–B of the sweep — tables A (primitives), B1 (scroll tails), B2 (middle dots), B3 (box sizes), B4 (radii/opacity), B5 (private dividers), B6 (hex).
3.2 features/sessions and app/presentation/components
See §C of the sweep — the value table, middle dots, divider loops, dead files, and the per-screen flow classes.
4. Implementation plan
Now (mechanical, zero design judgment) — shipped in this pass:
content-insets.tspresets + every literal replaced + lint rule.META_TEXT.secondaryin the three label primitives.RowList dividersat the six loops;SkeletonFlatPageonSEAM/AVATAR_SIZE/RowList.- Middle-dot joins split into meta lines at all 31 sites; ratchet waiver pruned.
- Box sizes, radii, opacities → constants or a
reason:comment;MiniBarMeterprimitive;HAIRLINEconstant for chrome; sheet rail/tail unified; dead legacy cards deleted.
Next (design-level, owner review on the board first):
- Wave 5: migrate the ten legacy card-grammar session screens to
DetailShell ground="canvas"+FlatColumn, then flip the ground default. - Onboarding value props composition on the seam ladder.
create-club-screenontoWizardShellrhythm (retirecreate-club-styles.ts).EditFieldRow→FactRowconvergence;GlyphFactRowretirement.
A. UI primitives
| file:line | what | rule broken | fix |
|---|---|---|---|
packages/ui/src/skeleton-flat-page.tsx:39-76 | Raw $4/$2/$3/$6 seams, height={40}, circle height={32}, own borderBottomWidth loop | Seam owned by a bare token; private divider idiom | SEAM.*, ROW_HEIGHT_LIST, AVATAR_SIZE.SM, RowList dividers |
packages/ui/src/spec-list.tsx:73 | label cardMeta $textSecondary | META_TEXT bypass | META_TEXT.secondary |
packages/ui/src/edit-field-row.tsx:55 | same | same, plus a private borderTopWidth divider prop | same |
packages/ui/src/wizard-review-row.tsx:88 | same | same | same |
glyph-fact-row.tsx vs spec-list.tsx vs fact-row.tsx | three icon+label+value rows | duplicate layouts | FactRow/FactList is canonical |
pill-nav.tsx:326, app-header.tsx:63, bottom-cta-band.tsx:60, wizard-shell.tsx:416, collapsing-top-bar.tsx:88 | five hand-rolled border…Width={1} $borderSubtle chrome hairlines | duplicated recipe | one HAIRLINE constant |
hue-slider.tsx:201-212, time-band-slider.tsx:277-413, band-slider-core.tsx:82-83,442 | slider thumb/tick geometry literals | documented exception in spirit, unnamed | SLIDER_GEOMETRY constants |
skeleton.tsx (10 sites) | shape literals for avatars/rows | not on box-sizes | AVATAR_SIZE.* where the shape stands in for an avatar |
naver-badge.tsx:15-16 | 22×22 | unnamed | BADGE_SIZE.NAVER |
action-icon-button.tsx:69, stepper.tsx:86 | borderRadius={999} | magic pill | RADIUS.PILL |
B. Clubs, profile, records, home, messaging, onboarding
B1. Scroll tails (each a private literal)
clubs/club-sessions-screen.tsx:106,191 48 · club-venues-screen.tsx:324 104 · clubs-discover-screen.tsx:164 48 · club-members-screen.tsx:352 16/24 · post-create-screen.tsx:232 0/24 · club-attendance-screen.tsx:325 48 · club-list-screen.tsx:161,329 48 and 100 · club-album-screen.tsx:48 48 · club-detail/{chat-pane:142, interim-pane:45, home-tab:198, sessions-pane:207, ranking-tab:432,481} 48 ×6 · club-dues/sections/{member-roster:74, admin-roster:92} 16 · profile/delete-account-screen.tsx:78 32 · consent-history-screen.tsx:125 16/16/48 · settings-{signal-preferences:249, blocked-users:66, consent-management:224} identical object ×3 · edit-profile-screen.tsx:394 16/32 · settings-privacy-screen.tsx:401 48 · change-password-screen.tsx:112 32 · add-friend-screen.tsx:164, friends-screen.tsx:335 48 · settings-screen.tsx:257 48 + raw gap:16 · messaging/dm-add-members-screen.tsx:137, dm-create-group-screen.tsx:190 96 · dm-thread-screen.tsx:273 8 · records/records-history-screen.tsx:472 48 · records-screen.tsx:199 56 · home/home-feed-screen.tsx:169 48 · notification-center-screen.tsx:480 48 · profile/components/content-page.tsx:19 48. On-system contrast: post-detail-screen.tsx:338 uses a named constant.
B2. Middle-dot joins
clubs/partner-suggester-screen.tsx:144 · club-detail/club-mission-card.tsx:33 · club-detail/home-tab.tsx:125 · club-detail/feed-auto-rows.tsx:50 · club-detail/home/auto-match-row.tsx:28 · shared/post-row.tsx:397 · settings/club-guests/priority-row.tsx:55 · public-club-profile/activity-card-section.tsx:127 · records/record-detail/match-timeline-row.tsx:37 · records/cards/top-partners-card.tsx:74 · records/cards/format-stats-card.tsx:49 · profile/add-friend-screen.tsx:82 · profile/friends-screen.tsx:195 · profile/public-profile-screen.tsx:337 · home/cards/recap-summary-card.tsx:164 · ui/range-chip.tsx:193 · clubs/club-attendance-screen.tsx:357 · clubs/members/member-modals.tsx:172.
B3. Raw box sizes
club-detail/feed-auto-rows.tsx:32-33 32 → AVATAR_SIZE.SM · shared/club-photo-seam.tsx:235 28 → AVATAR_SIZE.XS · profile/practice-log-screen.tsx:153-154 36 (no rung) · records/head-to-head-screen.tsx:174, personal-records-section.tsx:88,122 width 28 · records/leaderboard-rank-row.tsx:66 width 50 · club-detail/member-stats-modal.tsx:175,185,189 widths 18/36/48.
B4. Raw radii / opacity
profile/practice-log-screen.tsx:155 r18 · onboarding/onboarding-value-props.tsx:68,72,79,87,127,161 r15/23/13/26/2 and :64,154 opacity 0.3/0.9 · create-club/step1/decor-section.tsx:204,213 r13/11 · settings/club-groups-section.tsx:256 r5 · settings/image-section.tsx:151 r40 · shared/club-card.tsx:399,403 r2 · dues/dues-row.tsx:60 0.5 · dues/dues-bank-card.tsx:59 0.7 · records/personal-records-section.tsx:86 0.35 · settings/regroup-preview.tsx:198 0.7.
B5. Private divider idiom
club-detail-screen.tsx:455 · public-club-profile/guest-sessions-section.tsx:62 · club-members/member-row.tsx:90 · club-detail/sessions-pane.tsx:146, feed-auto-rows.tsx:80 · post-create-screen.tsx:284 · shared/post-row.tsx:374 · club-detail/ranking-tab.tsx:270,334 · dues/dues-row.tsx:80. (club-schedule-rows.tsx:78 is the canon-cited bottom-hairline precedent.)
B6. Hex
None live. Two comment-only citations.
C. Sessions and shared presentation
| file:line | what | rule | fix |
|---|---|---|---|
session-attendance-screen.tsx:245, session-payments-screen.tsx:512 | {paddingBottom:96, paddingHorizontal:16} | doubled tail, no rung | SCROLL_CONTENT.banded/shell default |
participants-screen.tsx:221, match-rules-screen.tsx:125 | shell default restated | zero-effect fork | delete prop |
public-pickup-preview-screen.tsx:282 | restated + gap:12 | raw block gap | delete; gap="$3" inside |
spectator-scorecard-screen.tsx:436 | paddingBottom:32 | invented rung | SCROLL_CONTENT.page |
edit-session-screen.tsx:98-102 | restated + gap:16 | doubled seam | delete keys |
king-of-court-ladder.tsx:252, standings-tab.tsx:215, rounds-tab.tsx:253,273,291, draws-tab.tsx:242,259, live-tab.tsx:69,92 | the same pair ×9 | nine owners | SCROLL_CONTENT.railedList |
venue-search-results-list.tsx:592, venue-finder-browse-list.tsx:54 | {paddingBottom:48} | duplicated | SCROLL_CONTENT.list |
match-profiles-sheet.tsx:118 vs score-edit-sheet.tsx:223 | sheet tail 32 vs 24 | inconsistent | SCROLL_CONTENT.sheet |
create-session/create-session-screen.tsx:403-406 | styles.scrollContent 16/24 | StyleSheet literals | SCROLL_CONTENT |
push-permission-soft-prompt-sheet.tsx:59 | paddingHorizontal="$5" inner wrapper | rail ≠ 16 | ModalPanel.Frame paddingHorizontal="$4" |
tier-range-selector.tsx:98 | gap={2} | raw | gap="$1" |
session-strip-ui.tsx:318-323 | cell divider width 1, opacity 0.35 | re-derives CellRow divider | CELL_ROW_DIVIDER_* |
venue-confidence.tsx:187-197 | bar chart 2/14/3.5/1 | unnamed | MiniBarMeter |
venue-confidence.tsx:239 | role="badge" lineHeight={16} | hard ban | drop override |
add-round/feasibility-tiles.tsx:107-109 | bar chart 4/2 | second fork | MiniBarMeter |
session-card-v2.tsx:227-249 | map pin −7/−18/14/999/−1.5/6 | undocumented | reason: block or MapPinMarker |
pickup-host-card.tsx:211 | height={36} | is ROW_HEIGHT_HAIRLINE | constant |
venue-card.tsx:310 | opacity={0.92} | unnamed | name or drop |
match-scoreboard.tsx:452 | gap={0} | style | gap="$0" |
activity-metric-calendar.tsx:153,237 | minHeight={28}, borderRadius={4} | raw | token/constant |
coachmark-tooltip.tsx:46 | maxWidth={280} | uncommented | reason: |
date-rail.tsx:380 (file deleted 2026-09-08, duplication-audit-2026-09-08.md), venue-search-takeover.tsx:283, correction-row.tsx:249 | hand hairlines | divider law | Divider/RowList |
Middle-dot joins: format-chooser-sheet.tsx:168 · tournament-board-screen.tsx:73,83,137 · spectator-scorecard-screen.tsx:217 · scorecard/match-profiles-sheet.tsx:89 · create-session-step-players.tsx:462 · venue-correction/fields/telephone-field.tsx:61, court-count-field.tsx:110 · match-row-header.tsx:65 · session-compact-row.tsx:158 · venue-card.tsx:136 · match-card.tsx:139,144 · scorecard/king-of-court/court-card.tsx:108.
Divider loops: manner-tags-sheet.tsx:166 · guest-rating-block.tsx:189 · session-matchups-card.tsx:161 · scorecard/round-section-list.tsx:181 · scorecard/standings-tab.tsx:292 · pickup-host-dashboard-screen.tsx:347.
Dead legacy grammar (delete): session-identity-card.tsx, session-status-participation-card.tsx, session-venue-card.tsx, session-weather-card.tsx and the stale __tests__/session-detail-screen.test.tsx that alone references them; date-rail.tsx was parked here — deleted 2026-09-08 (0 live renderers, see duplication-audit-2026-09-08.md).
Flow classes: on-system session-detail, venue-detail, match-board, directory-venue-detail; partially ~17 (edit-session, attendance, participants, match-rules, spectator-scorecard, public-pickup-preview, payments, host-dashboard, tournament-board, create-session, four scorecard tabs, king-of-court ladder); sheets on-system except the two noted.