Skip to content

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.

axiscount
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 rendering31 sites
Hand-rolled divider index math bypassing RowList dividers6 + ~10 private hairlines
Primitives bypassing META_TEXT for a label slot3 (SpecList, EditFieldRow, WizardReviewRow)
Primitives re-deriving a seam with a raw token1 (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 literals0
role + raw lineHeight override (hard ban)1 (venue-confidence.tsx:239)

2. The ten breaks, ranked by what a user feels

  1. Scroll-tail fork. Every list/detail screen ends with a different bottom gap. ~50 sites paste their own contentContainerStyle; 20 of them re-state DetailShell's own default byte-for-byte. Fix: SCROLL_CONTENT presets in @twomore/ui (packages/ui/src/content-insets.ts), every literal replaced, lint rule @twomore/no-raw-content-inset.
  2. 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).
  3. 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.
  4. Three primitives bypass META_TEXT so settings rows, wizard review rows and spec sheets render their label one tier darker than every other label. Fix: META_TEXT.secondary in all three; SpecList retires into FactList.
  5. 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.
  6. 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).
  7. Six hand-rolled divider loops (idx > 0 && <Divider/>) with insets already diverged (full vs card). Fix: RowList dividers.
  8. Two independently invented mini bar meters (venue-confidence.tsx 3.5/1 vs feasibility-tiles.tsx 4/2). Fix: one MiniBarMeter primitive.
  9. SkeletonFlatPage re-derives FlatColumn's ladder with bare $N tokens and its own divider loop, so the loading silhouette only coincidentally matches the loaded page. Fix: SEAM.*, AVATAR_SIZE, RowList.
  10. 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.ts presets + every literal replaced + lint rule.
  • META_TEXT.secondary in the three label primitives.
  • RowList dividers at the six loops; SkeletonFlatPage on SEAM/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; MiniBarMeter primitive; HAIRLINE constant 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-screen onto WizardShell rhythm (retire create-club-styles.ts).
  • EditFieldRowFactRow convergence; GlyphFactRow retirement.

A. UI primitives

file:linewhatrule brokenfix
packages/ui/src/skeleton-flat-page.tsx:39-76Raw $4/$2/$3/$6 seams, height={40}, circle height={32}, own borderBottomWidth loopSeam owned by a bare token; private divider idiomSEAM.*, ROW_HEIGHT_LIST, AVATAR_SIZE.SM, RowList dividers
packages/ui/src/spec-list.tsx:73label cardMeta $textSecondaryMETA_TEXT bypassMETA_TEXT.secondary
packages/ui/src/edit-field-row.tsx:55samesame, plus a private borderTopWidth divider propsame
packages/ui/src/wizard-review-row.tsx:88samesamesame
glyph-fact-row.tsx vs spec-list.tsx vs fact-row.tsxthree icon+label+value rowsduplicate layoutsFactRow/FactList is canonical
pill-nav.tsx:326, app-header.tsx:63, bottom-cta-band.tsx:60, wizard-shell.tsx:416, collapsing-top-bar.tsx:88five hand-rolled border…Width={1} $borderSubtle chrome hairlinesduplicated recipeone HAIRLINE constant
hue-slider.tsx:201-212, time-band-slider.tsx:277-413, band-slider-core.tsx:82-83,442slider thumb/tick geometry literalsdocumented exception in spirit, unnamedSLIDER_GEOMETRY constants
skeleton.tsx (10 sites)shape literals for avatars/rowsnot on box-sizesAVATAR_SIZE.* where the shape stands in for an avatar
naver-badge.tsx:15-1622×22unnamedBADGE_SIZE.NAVER
action-icon-button.tsx:69, stepper.tsx:86borderRadius={999}magic pillRADIUS.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:linewhatrulefix
session-attendance-screen.tsx:245, session-payments-screen.tsx:512{paddingBottom:96, paddingHorizontal:16}doubled tail, no rungSCROLL_CONTENT.banded/shell default
participants-screen.tsx:221, match-rules-screen.tsx:125shell default restatedzero-effect forkdelete prop
public-pickup-preview-screen.tsx:282restated + gap:12raw block gapdelete; gap="$3" inside
spectator-scorecard-screen.tsx:436paddingBottom:32invented rungSCROLL_CONTENT.page
edit-session-screen.tsx:98-102restated + gap:16doubled seamdelete 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,92the same pair ×9nine ownersSCROLL_CONTENT.railedList
venue-search-results-list.tsx:592, venue-finder-browse-list.tsx:54{paddingBottom:48}duplicatedSCROLL_CONTENT.list
match-profiles-sheet.tsx:118 vs score-edit-sheet.tsx:223sheet tail 32 vs 24inconsistentSCROLL_CONTENT.sheet
create-session/create-session-screen.tsx:403-406styles.scrollContent 16/24StyleSheet literalsSCROLL_CONTENT
push-permission-soft-prompt-sheet.tsx:59paddingHorizontal="$5" inner wrapperrail ≠ 16ModalPanel.Frame paddingHorizontal="$4"
tier-range-selector.tsx:98gap={2}rawgap="$1"
session-strip-ui.tsx:318-323cell divider width 1, opacity 0.35re-derives CellRow dividerCELL_ROW_DIVIDER_*
venue-confidence.tsx:187-197bar chart 2/14/3.5/1unnamedMiniBarMeter
venue-confidence.tsx:239role="badge" lineHeight={16}hard bandrop override
add-round/feasibility-tiles.tsx:107-109bar chart 4/2second forkMiniBarMeter
session-card-v2.tsx:227-249map pin −7/−18/14/999/−1.5/6undocumentedreason: block or MapPinMarker
pickup-host-card.tsx:211height={36}is ROW_HEIGHT_HAIRLINEconstant
venue-card.tsx:310opacity={0.92}unnamedname or drop
match-scoreboard.tsx:452gap={0}stylegap="$0"
activity-metric-calendar.tsx:153,237minHeight={28}, borderRadius={4}rawtoken/constant
coachmark-tooltip.tsx:46maxWidth={280}uncommentedreason:
date-rail.tsx:380 (file deleted 2026-09-08, duplication-audit-2026-09-08.md), venue-search-takeover.tsx:283, correction-row.tsx:249hand hairlinesdivider lawDivider/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.

Markdown remains the source of truth. Run yarn docs:check before handoff.