Skip to content

Shared UI component inventory for the flat v2 library (2026-10-09) ​

Status: Reference · Date: 2026-10-09 · Evidence appendix for flat-v2-component-library-2026-10-09.md: every @twomore/ui export and shared presentation component with its states in code, geometry, consumer count and v2 disposition. Generator scripts live in the session scratch directory (not committed).

Status: Draft for design · Scope: every export of packages/ui/src/index.ts, every shared component under packages/app/src/presentation/components/**, measured against docs/design/flat-mobile-grammar-v2-2026-10-09.md (§2–§4b) and docs/design/research/list-surfaces-spec-2026-10-09.md §4, with docs/canon/components.md as the current law. Purpose: the component library is designed first (owner: "design all of the components first, including all their interactive behaviours … then design the layouts for all pages based on the new components library"), so this file lists what exists, what each piece does today, and where the v2 target list has no answer yet. Source snapshot: commit 0175c593e, 2026-10-09 (includes the migration plan, §0.2). Nothing under the repo was modified.

Headline (details in §5) ​

  • 255 exported component names: 142 in packages/ui (94 rows below), 59 shared presentation components (30 rows), 54 single-consumer-group presentation components (appendix). Disposition by name: keep 123 · re-spec 74 · merge 34 · retire 24.
  • The v2 grammar and the Tag/Choice map name 21 components; the code has 255 names. merge collapses 34 names into 7 v2 targets (Tag, Choice, IconButton, Notice, StatStrip, SwitchTrack, Button sm-tonal); retire removes 24 (17 of them are exported but never rendered today, §4.3).
  • Top consumers decide the order (§5.2): Text 312 files, ActionButton 128, Pressable 108, Icon 94, EmptyState 85, SectionBlock 82, Badge 73, DetailShell 67, SelectionChip 56, Card 55.
  • No code exists yet for: FAB (the fab press variant has zero uses), BrandBlock, Roster (4-up grid with dashed empty slots), the level Tag circle, the spaced friend-avatar row. The tab bar exists only inline in apps/mobile/app/(tabs)/_layout.tsx:62-78, not as a shared component. Detail in §4.

0. How to read this file ​

Path legend. ui: = packages/ui/src/ · pres: = packages/app/src/presentation/components/ · app: = packages/app/src/presentation/ · mobile: = apps/mobile/. file:123 is a line in that file; :123 alone means the file named in the same cell.

Disposition codes. keep = same role, same look, only token re-points; re-spec = same role, new geometry/ink (the v2 component replaces it in place); merge → X = the component dissolves into v2 component X; retire = no v2 counterpart (v2 §6, or dead code).

Consumer counts. files (groups): number of non-test .tsx files under packages/features, apps, packages/app that contain <Name (same regex as the brief, run over all 747 files; script: bench/component-inventory-src/count.mjs (rerun: node count.mjs && node build-tables.mjs && node assemble.mjs from that folder)), and the number of distinct consumer groups among: the 8 feature packages (activity, clubs, home, messaging, onboarding, profile, records, sessions), packages/app (screens/hooks), apps/mobile, apps/web. Uses inside packages/ui are not counted (a primitive used only by other primitives shows 0 (0)). Family rows show one count per member, joined by ·. Caveats: counts are files not call sites; the regex cannot see non-JSX uses (Component({…}), styled(Component)), so a few 0 rows are referenced elsewhere (stated in the note); Text could include non-@twomore/ui Text (312 of 312 files that render <Text also import it from @twomore/ui, checked by import grep).

Counting rules. A row is one design-system component (a file or a file's tightly coupled exports, e.g. ExpandToggle + ExpandChevron + ExpandReveal). A name is one exported identifier that renders UI. export * from 'tamagui' (index.ts:389) is excluded; the three layout stacks are listed but kept out of rankings (YStack 353 files, XStack 260, ZStack 0).

State letters in the tables. P pressed, H hover (web), F focus ring (web), D disabled, L loading, S selected, E error. "passive" = no press behaviour in the component itself.

0.1 The v2 target list used in the "disposition" column ​

Targets come from grammar §4 (rows), list-surfaces spec §4 (the Tag/Choice migration map) and §2–§3. ✔ = specified by one of those two documents; ◇ = needed by the six screens or by merging, not specified yet; ▢ = a form family the grammar has not touched.

IDv2 componentSourceSpecified?
T-Typetype scale 12/13/15/17/20/24/40grammar §2✔
T-Iconicons 24 actions · 20 hero facts · 16 chevronsgrammar §4 "Icons"✔
T-Seamhairline tiers (edge #D4D4D4, cell #E5E5E5) + 12pt bandgrammar §4b✔
T-Headerheader 56, title 20/28·700 at the rail, actions 24 in 44, no divider§4 "Header"✔
T-UnderlineTabsrow 44, 15/22 600/500, underline 2 under the label§4 "Underline tabs"✔
T-BrandBlock350×144 (ELO 158), eyebrow + numeral + meta§4 "Brand block"✔
T-SectionHeader17/24·600, action 13/18·500 + chevron 16, 32 above / 8 below§4✔
T-MediaRowpadding 12, thumb 56, pitch 80, L1 17/24 L2 15/22, one trailing§4✔
T-PersonRowpadding 12, avatar 40, pitch 64, L1 15/22 L2 13/18§4✔
T-DayHeader32 tall, 15/22·600 date + 13/18 relative day§4✔
T-CellRow48, label 15/22·400 / value 15/22·500, cell hairline§4 "Cell row (facts)"✔
T-Tag24 tall, pad 0 8, r6, 13/18·600 (list spec says 22/26, r8, 12/18)§4 · list spec §4✔ (two specs disagree, §6)
T-Choice36 tall, pad 0 14, full radius, 15/22·500, ink fill + check; modes menu/range/input§4 · list spec §4✔
T-Buttonprimary 52, r12, 17/24·600; sm tonal replaces ActionPill§4 · list spec §4✔
T-BottomCtaBandbutton 52 in pad 16 + inset 34, hairline only while scrolling§4✔
T-FABextended pill 48, pad 0 20, 20 from rail, 16 above tab bar§4✔
T-TabBar56 + 34, 24 icons stroke 2, 11/14·500, active = filled glyph + ink§4✔
T-StatStrip80, three columns, 24/32·700 numeral§4✔
T-Heroeyebrow 13/18, title 24/32·700, three 17/24·500 glyph facts§4✔
T-Rosteravatar 48 in a 4-up grid, name 13/18, level Tag§4✔
T-Chart160 tall, inset 16, one ink line§4✔
T-Avatar40 rows · 48 roster · 56 identity§4✔
T-CountBadgeCornerCountBadge keptlist spec §4✔
T-SwitchTrackTogglePill replacementlist spec §4✔ (name only)
T-MetaMetaSegments text linelist spec §2✔
T-IconButtonglyph in a 44 target (derived from §4 "Header")—◇
T-Noticeinline banner/callout/well—◇
T-Toast, T-EmptyState, T-Skeleton, T-Progress, T-Menu, T-Overlay, T-List, T-Page, T-AvatarGroupderived—◇
F-* (Field, Search, Slider, Wizard, Stepper, AddRow, PickerRow, AttachedPanel, ChoiceTile, Check, PeriodStepper)forms—▢

0.2 Phase B families (docs/architecture/flat-grammar-v2-migration-plan.md, commit 0175c593e) ​

The migration plan landed while this inventory was being written (same day, 15:22). Phase B designs the library in these families and asks for "what each replaces (one-for-one map from the inventory)". The map from today's code to those families:

Phase B familyPhase B membersToday's code that feeds it
chromeheader, tabs, tab bar, FAB, CTA band, sticky edgesAppHeader, MainTabShell, DetailShell, PillNav/SegmentedTabs, tab bar inline in mobile:app/(tabs)/_layout.tsx, BottomCtaBand, HeaderIconButton/HeaderTextAction, CollapsingTopBar; FAB and sticky edges have no code
surfacesblock, band, stat strip, hero, chartStatStrip/StatTile, TypeHero/MediaHero, SparkLine, Card/Well/Divider (block and band have no code)
rowsmedia, person, cell, day header, section head, rosterListRow, PersonRow, FactRow, SessionDayHeader, SectionHeader/SectionBlock, VenueRow, LeaderboardRow, MatchHistoryRow, SessionCardV2/ClubCard (roster has no code)
labelsTag, countBadge + 16 status wrappers, MarkerPill, tier pills, CornerCountBadge
controlsButton, Choice, Switch, Input, TextArea, Picker, Slider, Search, StepperButton/ActionButton, SelectionChip family, TogglePill, StyledInput (multiline = TextArea, no separate component), PickerRow + native date picker, four sliders, SearchBar, Stepper
overlaysModalPanel, menu, toastModalPanel (+ ConfirmSheet), HeaderActionsMenu, ToastHost
statesempty, skeleton, error, offlineEmptyState + QueryErrorState/OfflineEmptyState/NotFoundState, Skeleton*, NetworkBanner, QueryBoundary
formswizard field grammarWizardShell, RevealStack, LabeledField, AttachedPanel, AddCard, PickerRow, RemoveIconButton, EditFieldRow

The plan also records decisions that bear on dispositions below: no shadows; pure white canvas; rows 80/64 without lines; one passive Tag + one Choice control; scheme S2 "Ball & Ink" (ball actions with ink text, green only as mark and court tile). Where those overrule current code the conflict table (§6) says so.

1. Primitive table ​

1a. packages/ui/src — every exported component ​

Order follows the file's role (type, actions, tags and controls, surfaces and rows, chrome and shells, forms, charts, infra). Text-heavy cells are deliberate: they carry the file:line evidence.

ComponentFilePurposeVariants / props that change the lookStates implemented (read from source)Sizes / geometry todayConsumers files (groups)v2 disposition
Textui:text.tsxThe one text node; 11 semantic rolestextRole ×11: pageTitle 24/32·700 (:70), cardTitle 16/24·600 (:78), navTitle 17/22·600 (:98), cardBody 14/22·400 (:105), cardMeta 14/20·500 secondary (:112), badge 12/18·600 (:120), micro 10/14·600 (:133), displaySm 20/28 (:140), displayMd 28/36 (:155), displayLg 36/44 (:172), displayXl 48/56 (:189); tabularNums (:203)passive; iOS hangul-word line breaks (:66)Pretendard 400/500/600/700; 800 maps to Bold (fonts.ts:30); tracking −0.1…−0.7 (fonts.ts:71-97)312 (10)re-spec → T-Type. Body is 14 today, 15 in v2; v2 adds 13/18, 12/16, 40/48 and drops micro 10, displayLg/Xl; the role set shrinks 11 → 7.
Iconui:icon.tsxLucide wrappersize xs12 sm14 md16 lg20 xl24 2xl28 (icon-sizes.ts:12-19); color, fill, strokeWidthpassivedefault md 16; header/tab icons stroke 1.8 (header-icon-button.tsx:54)94 (10)re-spec → T-Icon. v2 uses three sizes only: 24 actions, 20 hero facts, 16 chevrons, stroke 2.
YStack / XStack / ZStackui:stack.tsTamagui layout stacks re-exportedTamagui propspassive—353·260·0keep. Layout, not design-system components; counted separately (YStack 353 files, XStack 260, ZStack 0).
LinkifiedTextui:linkified-text.tsxUser text with tappable URLs (wrapped as AutoLinkText)$primary 600 underlined link segments (canon)link press via Pressableinherits parent role1 (1)keep
MetaSegments / MetaBreakui:meta-segments.tsxSeveral short facts on one line, separated by a 1px hairlinetier secondary/primary/eyebrow/caption (text-grammar.ts:40-45)passive; accessible with joined aria-label (:86-87)gap $2 (:84), MetaBreak 1px stretch (:59), on-tint opacity 0.35 (text-grammar.ts:55)29·5keep → T-Meta. v2 boards print ·; code law bans middle-dot joins (canon components.md:139), so MetaBreak stays the separator.
Dot / PulseDot / GroupTierDotui:dot.tsx · pulse-dot.tsx · group-tier-dot.tsxStatus dots; PulseDot = live indicatorsize (Dot 8 :25), colorPulseDot loops scale 1→2, opacity .55→0 over 1600ms (pulse-dot.tsx:67,81; motion.ts:69); reduced motion = static .55DOT_SIZE XS 7 / SM 8 (box-sizes.ts:43-46)10·6·3keep. Live pulse dot rides inside the danger Tag (list spec §2).
Pressableui:pressable.tsx · pressable-visual.tsThe only touchable; owns press physics, hover, focus, disabledvariant card/button/pill/row/icon/fab (pressable-visual.ts:43-50); affordance chevron/button/link/surface/none (lint only)P (UI-thread scale spring + opacity timing, pressable.tsx:286-301) · H web 0.85 (:324) · F web ring (:325) · D/L inert path 0.5, not-allowed (:216,214) · pressed aria via aria-pressed (:319)icon variant default hitSlop 15 (:112); web hitSlop realised as an invisible child (:77-90)108 (9)keep. Carries over unchanged; v2 needs the same six physics rows.
Buttonui:button.tsxLabelled action (wraps Tamagui Button)variant primary/secondary/outline/ghost/danger/destructive (:77-128); size xs/sm/md/lg (:188-224); shape default r12 / pill / circle (:130-134)P pressStyle .97/.92 (:62) · H .85 (:67) · F ring (:68) · D .5 + not-allowed (:74) · L spinner + loadingLabel (:289, :297) · reduced motion drops scale (:292)height xs 28 / sm 36 / md 44 / lg 56 (:201,207,213,222); label sizes via Tamagui $2…$5 (:181); radius 12; lg = width 100% unless flex (button-visual.ts:25)22 (5)re-spec → T-Button. v2 primary is 52 (not 56), 17/24·600; secondary is a 1px $borderStrong neutral fill (:97-101); ActionButton intent map stays on top.
ActionButtonui:action-button.tsxIntent wrapper → Button variant (primary/secondary/neutral/cancel/tertiary/destructive→danger)action ×6 (:ACTION_VARIANT)as Buttonas Button128 (7)keep. #2 by consumer count; keep API, re-skin through Button.
ActionPillui:action-pill.tsxSmall utility trigger pill—P pill .96/.85; stopPropagation (:49)min-height 28 (:27), hitSlop 8, padding $3, fill $surfaceSecondary, label badge 12/18·6002 (2)merge → T-Button sm tonal. Spec: ActionPill → Button size="sm" tone="tonal" (list spec §4).
ActionIconButtonui:action-icon-button.tsxRound icon button (base of RemoveIconButton)iconColor, flushP icon .92/.78 · D · L spinner (:111)36 circle (:42), hitSlop 4 → 44, icon md 166 (2)merge → T-IconButton. Merge with HeaderIconButton + InfoHintButton into one icon-button family (24 glyph in 44 target).
HeaderIconButtonui:header-icon-button.tsxEvery header glyph (back, bell, ⋯)color default $iconMuted, strokeWidth 1.8 (:54)P icon .92/.78 · H · Fglyph 24 (:50), hitSlop 10 → 4415 (7)merge → T-Header action / T-IconButton. Behaviour carries: glyph 24 in a 44 target, spacing by container gap.
HeaderTextActionui:header-text-action.tsxOne labelled text action in a header slot (초대 코드, 취소, 완료)prominent (600/400), color default $primary (:58)P button .97/.92 · Fmin-height 44 (:66), padding $2, label cardBody 14/225 (2)keep → T-Header action
InfoHintButtonui:info-hint-button.tsxⓘ explainer trigger—P iconicon sm 14 $textTertiary, default icon hitSlop 152 (2)merge → T-IconButton
RemoveIconButtonui:remove-icon-button.tsxRow/item remove with optional ConfirmSheet (wizard field grammar)icon (Trash2 :74 / UserMinus), confirm, flushas ActionIconButton · L36 circle, 16 glyph13 (4)keep → T-IconButton. Keep the confirm-before-destroy behaviour; re-base on the merged icon button.
CameraBadgeui:camera-badge.tsxEdit-photo badge on an avatar—P icon · static when no onPress28 circle (:25), 1.5px $border, hitSlop 81 (1)keep
AddCardui:add-card.tsxDashed "+ add" slot in a wizard field listdisabledP row .98/.70 · D .5 + $textTertiarymin-height 52 (:41), 1.5px dashed $primary (:45-46), radius $47 (2)re-spec → F-AddRow. Form family; no v2 spec yet (see section 3).
HeaderActionsMenuui:header-actions-menu.tsx · header-actions-menu-layout.ts⋯ overflow dropdown anchored under the header triggeractions[] with tone default/destructive, triggerIconP rows · D rows · web keyboard (Escape :287, arrows, focus restore) · one Divider before the destructive group (:410)RN Modal fade (:342), width 200–260 (layout.ts:14-15), rows ≥44 (:422), radius $3, shadow .12/r10/elev 8 (:71-76)7 (4)keep → T-Menu. The menu panel is a shadowed floating surface; the one place the app uses a shadow on purpose.
Badgeui:badge.tsx · badge-visual.tsPassive label pill (12 variants)variant neutral/warning/live/accent/success/info/premium/muted/primarySolid/surface/primarySubtle/outline (:58-87); size xs/sm/mdpassive (no press); numberOfLines=1 (:203); optional shrinkxs 10/14 + pad 8×2 r8 = 18 tall (:89-93, visual:15); sm 12/18 + 8×4 r8 = 26 (:94); md 14/20 + 12×4 r12 = 28 (:95)73 (7)merge → T-Tag. Spec: Badge → Tag; call sites use neutral 39, warning 11, live 3, surface 5, xs 33, sm 30.
CornerCountBadgeui:corner-count-badge.tsxRed/amber count bubble pinned to a cornertone unread/attentionpassive, aria-hidden18 circle (:31), top −2 right −2, micro 10/14·7002 (2)keep → T-CountBadge. Spec keeps it as CountBadge.
NaverBadgeui:naver-badge.tsxNaver provenance square—passive22 square $naver (box-sizes.ts:54)1 (1)keep
MarkerPillui:marker-pill.tsxHint pill on a media bandtone primary/neutralpassivebadge 12/18, pad 8×4, full radius, fill $primarySubtle/$card3 (2)merge → T-Tag. Spec lists MarkerPill → Tag.
ChipLabelui:chip-label.tsxThe chip label text (internal to the chip family)selectedS: $primary 700 vs $textSecondary 500 (chip-label-visual.ts:19-24)badge 12/183 (2)merge → T-Choice
SelectionChipui:selection-chip.tsxSelectable DATA chip (filters, scopes)size md 40 / sm 32 (:25); leading/trailing; role radio/checkbox/tabP pill .96/.85 · H · F · D .5 + aria-disabled (:98) · S border $primary + fill $primarySubtle + 700 label (:122-123); aria-pressed/checked/selected by role; web radio arrow keys (:17)min-height 40/32, hitSlop 2/6, pad 12×8 / 12×4, radius full, 1px border56 (5)merge → T-Choice. v2 Choice: 36 tall, pad 0 14, 15/22·500, 1px $border rest, ink fill + check selected.
RemovableChipui:removable-chip.tsxChip with an inner ✕ (applied filters)—P pill; inner remove icon .92/.78, stopPropagationmin-height 40, glyph 12 (:34), always the selected look2 (2)merge → T-Choice mode=input
DropdownChipui:dropdown-chip.tsxFilter chip that opens a ModalPanel of radio/checkbox optionsmulti, options, sheetTitle, controlled openchip as SelectionChip · rows aria-selected/aria-checked · 적용 buttonrows pad $3×$2 with Check 16 (:209); panel via ChipPickerPanel11 (5)merge → T-Choice mode=menu
RangeChipui:range-chip.tsxMin/max picker chipoptions ascendingas DropdownChip; pending min>max auto-raises maxtwo columns of option rows in a ModalPanel2 (2)merge → T-Choice mode=range
ChipPickerPanelui:chip-picker-panel.tsxModalPanel shell for chip pickerstitleModalPanel statescenter dialog, $background, title cardTitle centered0 (0)merge → T-Choice panel. ui-internal only.
SelectionCardui:selection-card.tsxLarger pick-one tile (format, preset, play style)selected; free childrenP card .98/.90 · S fill $primarySubtle + border $primary 2px (:44) · aria by rolepadding $4, radius $3, border 29 (3)re-spec → F-ChoiceTile. $primarySubtle surface leaves in v2; selected cue must change.
SelectionCheckui:selection-check.tsxCheckbox/radio glyphshape square/circleS fill $primary + Check24 (:34), square r6 / 1.5px (:40-41), circle 1px2 (2)keep → F-Check
TogglePillui:toggle-pill.tsxDisplay-only switch visual (host row owns the press)valueS on $primary / off $border (:40); NO thumb animation, NO role="switch"/aria-checkedtrack 44×24 r12, thumb 20, inset 2 (:17-20)9 (3)merge → T-SwitchTrack. Spec: TogglePill → SwitchTrack. On/off is invisible to assistive tech today (hosts are ListRow onPress; ListRowProps has no checked prop, list-row.tsx:54-112).
Stepperui:stepper.tsx−/+ bounded integer controlformat, fullWidth, editable, valueMinWidth 48 (:113)P icon · D at min/max (:174) · aria-value* (:195)buttons 32 circle 1px $border (:31), hitSlop 6; fullWidth min-height 48 (:167)17 (3)keep → F-Stepper
PillNavui:pill-nav.tsx · pill-nav-underline.tsx · pill-nav-shared.tsTab strip: sliding pill (≤4 items) or scrolling pills (>4) or underline (UnderlineNav is the underline variant)variant pill/underline; badge dotP pill .96/.85 · F · active indicator springs (navSlide 22/280/.8, motion.ts:33) · tab ARIA (tab-accessibility.ts) · reduced motion = snappill: container pad 4, radius $5, fill $surfaceTertiary (pill-nav.tsx:51,76-81), 36 tall (:59), label 14/600 (:126-127); underline: 48 + 1px hairline (pill-nav-underline.tsx:24), tabs flex 1 (:57), 2px $text underline (:64), 14px 800/600 label (:81-82), 6px badge dot (shared.ts:83)7 (4)re-spec → T-UnderlineTabs. v2 underline row is 44, 15/22 600/500, underline only under the label width. The pill variant is also the form segmented control in 6 files (match-rules-editor ×5 instances, create-session-step4, create-tournament-modal ×2, practice-log-screen ×2, match-list-controls-sheet, records-screen) and stays there (gap G12).
SegmentedTabsui:segmented-tabs.tsxPillNav + keep-alive paneschrome pill (default, :81,92) / underline (:245) / none; preload visited/all; fill; screenFocusedtab switch under useDeferredValue; hidden panes Freezed; no swipe between panespane-reveal perf threshold 120ms (:15)6 (5)keep → T-UnderlineTabs host
Cardui:card.tsxBordered surfacetone default/flat(dashed); size sm 12 / md 16 / lg 20 / none (:56-59)passive; never onPress (wrap in Pressable)radius $4 = 16 (:29), 1px $borderSubtle, no shadow (canon components.md:10)55 (7)retire. v2: "cards on list surfaces leave" (grammar §6). 55 files; migrate to rows/blocks, keep nothing but dashed empty slots.
Wellui:well.tsxTinted inset panel, optional tap (chevron)tone neutral/primary/warningP card .98/.90 when onPressradius $4, padding $3 (:41)21 (3)merge → T-Notice. Banner + Callout + Well are three shapes of one inline notice.
Calloutui:callout.tsxActionless tinted notetone neutral/info/warning; emoji iconpassiveradius $3, padding $3 (:53), cardBody5 (3)merge → T-Notice
Bannerui:banner.tsxInline notice with ≤2 buttonstone primary/warning/error/neutral/muted; actions[]actions are Button sm (P/H/F/D)padding $3 (:106), radius $3, buttons right-aligned gap $3 (:136); only primary has a 1px border (:83)14 (3)merge → T-Notice
SummaryCardui:summary-card.tsx$primarySubtle summary box—passivepadding $3, radius $4, fill $primarySubtle (:27)0 (0)retire. Dead: no JSX consumer (mentions are comments).
SpecListui:spec-list.tsxOld label/value list with emoji columnvalueTonepassive20px emoji column, gap $30 (0)retire. Dead: no consumer anywhere; superseded by FactRow.
Dividerui:divider.tsx · divider-visual.ts · hairline.tsThe one hairlineinset full/card, seam="section" (24 above/below, divider-visual.ts:39), bleed (−16, :21)passive1px $borderSubtle (hairline.ts:18-20)54 (7)re-spec → T-Seam. v2 has two tiers (edge #D4D4D4, cell #E5E5E5 inset to the rail) + one 12pt band; the section line is retired (D9/D10).
FlatColumnui:flat-column.tsx · flat-column-defaults.tsDetail-page column: rail + sectionstop, gap, divider, leadpassiverail 16 (:104), top 16 (defaults.ts:17), section gap 24 (:23), bleed Divider between sections (:87)42 (5)re-spec → T-Page. v2: rail 20, section gap 32, no divider between sections.
RowListui:row-list.tsxStacks rows; injects first/last edge contextdividers (default off)passiveedge trimming of first/last padding44 (4)keep. Edge-trim behaviour carries (box height = pitch).
ListRowui:list-row.tsx · list-row-visual.tsGeneric leading + title + sub + trailing row (base of PersonRow, settings rows)dense, tone, chevron, edge, hairline, titleLines, subLinesP row .98/.70 · D .5 (visual.ts:241) · S fill $primarySubtle + aria-current (:201,216) · auto chevron when pressable and no trailingtitle cardBody 14/22·600 (dense cardMeta) (:154); sub cardMeta 14/20·500 $textTertiary (:165); gap 12 (:190); pad-y 8 / dense 4; min-height 40 / dense 36; hitSlop to 44 (visual.ts:34-39)34 (5)re-spec → T-MediaRow / T-PersonRow / T-CellRow (+ gap G10 nav row). v2 splits into media 80, person 64, cell 48; the chevron/toggle settings row has no v2 spec yet.
FactRow / FactList / FactSegmentsui:fact-row.tsx · fact-row-visual.tsLabel-left / value-right fact rowsrhythm compact/roomy, tone, locked, editCue, secondaryValue, noteP row when onPress · chevron or edit cuemin-height 36 (visual.ts:30); pad-y 8 / roomy 12 (:51); label cardMeta tertiary, value cardBody 60019·4·2re-spec → T-CellRow. v2 "Cell row (facts)" = 48 tall, 15/22 400 label / 500 value, cell hairline. Not the current CellRow.
CellRow / CellRowGridui:cell-row.tsx · cell-row-visual.tsEqual-column strip primitive (group-spread law), base of StatStrip/ActionIconRowdividers, columnspassivecell pad-x 8, gap 8 / 4·1px·4 with dividers (cell-row-visual.ts:21,25,38)0·1keep. Name collision: v2 "Cell row" is the fact row, not this.
StatStripui:stat-strip.tsxRow of value + label cells with hairline dividerscolumns wraps to a grid; per-cell tone, onPresscell P row when pressablevalue displaySm 20/28·700, label cardMeta 14/20 tertiary (stat-tile.tsx:132)13 (4)re-spec → T-StatStrip. v2: 80 tall, numeral 24/32·700, label 13/18·500, 1×24 hairline.
StatTile / StatGridui:stat-tile.tsx · stat-grid.tsxOne stat (value+label+delta+progress) / card-wrapped gridwrap card/flat, order, delta (micro 10 ▲▼, :115)passive—1·1merge → T-StatStrip. StatGrid (1 file) is a boxed variant of the same.
MiniBarMeterui:mini-bar-meter.tsxSignal-bar meter (n of m)count, filledrole="img" when labelledbar 4 wide (:47), max 14 tall (:46), gap 4 (:48)3 (3)keep. No v2 counterpart; not on the six screens.
ProgressBarui:progress-bar.tsxLinear progressvariant primary/muted; fillColor; heightanimated scaleX 300ms ease-out-cubic (:83); reduced motion snaps; role=progressbar + valuenow (:103)height 6 (:70), radius $2, track $primarySubtle / $borderSubtle (:112)13 (5)re-spec → T-Progress. Height already 6. v2 track is the block ink at α0.2 on the brand block, so track colour becomes a prop.
AvatarBubbleui:avatar-bubble.tsx · avatar-bubble-visual.tsInitials/photo/silhouette avatarsize token or px; tone primary/neutral/muted; outlinedimage → glyph fallback layers; error/loaded tracked per URLXS 28 · SM 32 (default, :159) · MD 40 · LG 44 · HERO 56 · XL 80 · 2XL 88 (box-sizes.ts:24-40); initials 11/14/18/32 (:94-96); no 4820 (6)re-spec → T-Avatar. v2 needs 40 / 48 / 56 (48 missing); default should be 40; tone primary uses $primarySubtle (:151).
AvatarStackui:avatar-stack.tsxOverlapping faces +N, or two-face footprintfit overlap/footprint, max 8 (:73)passive, hidden from a11y treeoverlap −6 (:117); footprint faces 0.7×9 (5)keep → T-AvatarGroup
FlagMedallionui:flag-medallion.tsxGenerated club flag/monogrampreset, shape, hueoptional enter animation gated by reduced motionsize px4 (1)keep → T-Avatar (club logo r12)
DateTileui:date-tile.tsxCalendar-leaf tile (date strip + weekday)strip colourspassive48 wide (:52), strip $primarySubtle1 (1)retire. 1 file; v2 rows carry no date tile.
DayDotsRowui:day-dots-row.tsxWeek/day cell row with active dotsappearance cell/…, variant full/compactactive fill $primarySubtlecell 36 default1 (1)keep. 1 consumer; calendar widget.
EmptyStateui:empty-state.tsxEmpty / error / offline panelvariant full/compact; optional icon, 1–2 actionsbuttons P/H/F/Dfull: icon disc 64 (:75) $primarySubtle, glyph 28 $iconMuted, title 16/24·600, pad 40×24; compact: disc 44, title 14/22; buttons md85 (10)re-spec → T-EmptyState. Wrapped by QueryErrorState / OfflineEmptyState / NotFoundState (45/45/19 files).
Skeleton familyui:skeleton.tsx · shimmer.tsPlaceholder blockswidth/height/borderRadius/circleopacity loop .6↔.9, 800ms half-cycle (shimmer.ts:21,32; motion.ts:67); reduced motion fixed .75 (shimmer.ts:14)default 16 tall r8 (skeleton.tsx:41-42); fill $surfaceTertiary (:64); SkeletonRow = 40 avatar + 2 bars; SkeletonCard/Hero wrap Card5·21·35·14re-spec → T-Skeleton. Shapes must be redrawn to 80/64 row pitch; SkeletonCard/SkeletonHero (35/14 files) die with Card.
SkeletonFlatPageui:skeleton-flat-page.tsxDetail-page skeleton (hero + stat strip + rows)kindshimmermedia hero 140 (layout.ts:14)15 (4)re-spec → T-Skeleton
RemoteImageui:remote-image.tsxexpo-image wrapper with status machinefallback, showSkeletonidle/loading/loaded/error; skeleton while loadingfills parent15 (4)keep
PhotoScrimui:photo-scrim.tsxLegibility gradient over photosedgepassivetop .50→.45→0 (+40 fade), bottom .45→0 over 56 (photo-scrim-visual.ts:33,36)2 (1)keep
MediaHeroui:media-hero.tsx · media-hero-visual.tsDetail hero with a map/photo bandmedia, titleBadge, facts, mediaHintmedia P card when onMediaPressband 2:1, max 280 (visual.ts:38-39), default 1324 (2)re-spec → T-Hero. v2 hero is text-only (eyebrow, 24/32 title, 3 glyph facts); no band.
TypeHeroui:type-hero.tsxText hero: eyebrow, title, factstitleBadge, facts, leading 56passivepageTitle 24/32·700 (:85), eyebrow cardMeta 700 tertiary, rail 16, top 169 (4)re-spec → T-Hero. Title size already matches v2 (24/32·700); eyebrow, facts and spacing change.
GlyphFactRow / GlyphSlotui:glyph-fact-row.tsxGlyph + value factsdensity chip(→Badge surface)/inline; emoji or iconpassiveslot 16 / emoji slot 20 (:7-17)2·4re-spec → T-Hero facts (20 glyph + 8). chip density is deleted in v2; vocabulary 21 → 4 concrete glyphs.
ActionIconRowui:action-icon-row.tsx3–5 icon-circle actionsactiveP card · D .4 (visual.ts:52)circle 40 (:12) $primarySubtle/$primary2 (1)retire. 2 files, 1 consumer group; v2 puts actions in the header and CTA band.
ExpandToggleui:expand-toggle.tsxCollapse/expand row; chevron rotates; content revealswith/without children, summaryP row · aria-expanded · chevron rotates 180° on quick (:57) · reveal 8px slide+fade on reveal (:156)44 floor (:114)14·7·10keep
EditFieldRowui:edit-field-row.tsxLabel/value row with pencil cue (+ exported EditCue pencil)showDividerP row · Dpad-y 8, pad-x 16, pencil md1 (1)re-spec → T-CellRow edit mode
SectionHeader / SectionBlockui:section-header.tsx · section-header-metrics.ts · section-block.tsxSection title + meta/link/right slotvariant block (displaySm 20/28) / list (cardTitle 16/24) / flat (navTitle 17/22·700) (metrics.ts:56-57); link = cardMeta 700 $primary + chevron sm (:162-165)link P row · role=heading aria-level=2 (:133)list min-height 24 (metrics:114), title→content 8 (:93)82·16re-spec → T-SectionHeader. v2: 17/24·600 title, 13/18·500 secondary action + chevron 16, 32 above / 8 below. Link ink changes from $primary to secondary.
SectionEyebrowui:section-eyebrow.tsxSmall overline label—passivecardMeta 600 tertiary0 (0)retire. Dead: no consumer.
AppHeaderui:app-header.tsx · app-header-visual.tsStatic top barchrome default/bare, subtitle, left, right, showBackactions via HeaderIconButton/HeaderTextActionheight 56 / 64 with subtitle (visual.ts:18-20); title CENTERED navTitle 17/22·600 (:160-164); rail 16; slot min 44; permanent bottom hairline (:118)4 (2)re-spec → T-Header. v2: title 20/28·700 at the rail (left-aligned), no divider, hairline only while content scrolls under.
MainTabShellui:main-tab-shell.tsxTab-root page scaffold: title row + SegmentedTabs + body + CTAtabs, headerRight, bottomCta—title pageTitle 24/32·700 left, pad 16, no hairline (:100-103)5 (5)re-spec → T-Header + T-Page. Header height 56 + title 20/28 in v2.
DetailShellui:detail-shell.tsxDetail/form page scaffold: safe area, header, scroll, pinned CTAheader default/collapsing, aside, width reading/wideiOS automaticallyAdjustKeyboardInsets (:281); scroll-perf hooksContentColumn max width per window class67 (8)re-spec → T-Page. #8 by consumers (67 files). Header/CTA re-spec flows through.
PlaceShell / CollapsingTopBar / CollapsingHeaderLayerui:place-shell.tsx · collapsing-top-bar.tsx · collapsing-header-layer.tsxPhoto-first place page with a bar that turns solid on scrollcollapsedbar chrome opacity animates on quick; hysteresis flip (use-collapsing-top-bar.ts)bar row 48 (collapsing-top-bar-visual.ts:13)1·1·1re-spec → T-Header (collapsing). Club home only (1 consumer each).
BottomCtaBandui:bottom-cta-band.tsx · bottom-cta-band-visual.tsPinned bottom action bandchildren = 1 Button or a pair (gap 12, visual.ts:28)inset added only if no ancestor reserved it (:98)pad 16 all sides + safe-area (visual.ts:25-27); PERMANENT top hairline (:117); $card fill; max width reading column37 (7)re-spec → T-BottomCtaBand. v2: button 52, band top 40 after content, hairline only while content scrolls beneath it.
ModalPanelui:modal-panel.tsx · modal-panel-visual.tsThe only overlay: RN Modal with Overlay + Frame + ScrollViewplacement center/bottom; dismissOnOverlayPress; Frame showCloseButtonbackdrop tap dismisses (:99) · Android back (:111) · bottom frame slides y:48 on quick unless reduced motion (:209) · KeyboardAvoidingView padding (:117)RN fade (:108), overFullScreen (:109); frame ≤640 wide, ≤88% high (:189-190), $background, 1px $borderSubtle, r16 / bottom r24 top only (:207), pad 20×16, close X 40 circle at top 12 right 12; no drag, no grabber45 (7)keep → T-Overlay. Not in the §4 table, but every filter/edit/info surface depends on it (45 files).
ConfirmProvider / useConfirmui:confirm-sheet.tsxImperative confirm dialog on ModalPanelbuttons[] cancel/destructive/primary; ≥3 buttons stack vertically (:201)backdrop does NOT dismiss (:180); hardware back closes (:128); single-flight; options cleared 250ms after close (:142)title cardTitle 700 centered, message cardBody tertiary, Button md2 (2)keep → T-Overlay confirm
WizardShellui:wizard-shell.tsxMulti-step flow frameprimaryHint, onSkip, back/close slotsegments tappable for completed steps; KAV padding (:391); footer reports heightheader buttons 40 + hitSlop 2 (:65), 4px segments (:316), single full-width lg CTA3 (3)keep → F-Wizard. Header owns back/skip (wizard-nav law); footer = one CTA.
RevealStackui:reveal-stack.tsxSequential reveal of wizard sectionsitems[] confirmed/optional/selfConfirmingnew section enters on reveal 250ms (opacity 0,y 12; :165); VoiceOver announcementgap $5, confirm row: secondary flex 1 + primary flex 2 (:178)13 (2)keep → F-Wizard
AttachedPanelui:attached-panel.tsxLive-commit panel attached under a field (with notch)notchRightpassive1.5px $primary border + $primarySubtle fill (:74), pad 12, margin-top 8, notch 12 (:39-51), radius $410 (3)re-spec → F-AttachedPanel. $primarySubtle fill must go.
PickerRowui:picker-row.tsxPick-row that opens a pickeractiveP row · active border 1.5 $primary (:78) · Dmin-height 44 (:66), 1px $border, radius $3, pad 122 (1)re-spec → F-PickerRow
LabeledField / StyledInputui:labeled-field.tsx · labeled-field-logic.tsForm field: label + control + caption + error; themed TextInputsize sm/lg, required, labelRight, collapsed; input multiline, bare, invalidrest · F border $primary + web ring (:432) · E border $error, aria suffix, aria-live text (:227, :372) · D editable=false .5 (:464)input radius 12, 1px, pad 12×12, font 15 (:449-455), multiline min 120 (:459); label cardMeta 14/20 secondary (sm) or cardTitle 16/24 (lg), caption/error cardMeta44·48re-spec → F-Field. Font 15 already equals v2 body. The only text-input component (no Tamagui Input in use).
SearchBarui:search-bar.tsxSearch field with clear and optional right accessory (filter toggle)rightAccessory, editable, inputRefF border $primary on quick (:114-117) · D .5 (:118) · clear ✕ shows when non-emptymin-height 44 (:110), pad 12×8, radius $3, 1px $borderSubtle, search icon sm 14, input 15 (:149), max width reading3 (3)re-spec → F-Search
TimeBandSlider / TimeBandField / RangeBandSlider / HueSliderui:time-band-slider.tsx · time-band-field.tsx · range-band-slider.tsx · hue-slider.tsxCustom gesture sliders (time band, tier band, value, hue) — cores: BandSliderCore, SingleBandSliderCorehandle style trim/knob; veil colour; domaindrag (Pan) + a11y adjustable actions; value label; commit on interaction endbar 44 hit, track 28, trim handle 11, knob 18, thumb hit ±22 (box-sizes.ts:115-143);3·1·1keep → F-Slider. Consumers: TimeBandField 3, RangeBandSlider 1 (+ TierRangeSelector), HueSlider 1; TimeBandSlider is used only through TimeBandField.
FeedList / GroupedFeedList / StickySectionListui:feed-list.tsx · feed-list/*.ts(x) · sticky-section-list.tsxVirtualised lists (FlashList-backed; sticky variant uses SectionList) — parts: DefaultSeparator, SectionSeam, ListSlot, GridRowViewrecycling, columns, separators, section footersnative RefreshControl via refreshing/onRefresh; end reached 0.6, draw distance 800 (tuning.ts:38,44)default item separator 12 (tuning.ts:16), section seam 16 (:27)36·8·1re-spec → T-List. v2 rows own their pitch (80/64) so the card→card separator goes to 0; day headers own their own seam.
PeriodScrubberShellui:period-scrubber-shell.tsxCard + ‹ label › stepper header for period pickers—P icon chevrons$surface card4 (2)re-spec → F-PeriodStepper
DaumPostcodeModal / VenueLocationPickerui:daum-postcode-modal.tsx · venue-location-picker.tsxWebView address search / draggable-pin mapplatform variants—full-screen modal1·2keep. Native WebView: EAS-build gated.
Logoui:logo.tsxWordmark / markvariant default/reverse/markpassivewordmark 120, mark 321 (1)keep
ErrorBoundaryui:error-boundary.tsxRender-error catcherfallback——11 (3)keep. Infra.
Infra: PerfProfiler, Freeze, ContentColumn, SupportingPane, WebNavCovered, TabPaneActive, BottomInsetReservationui:perf-profiler.tsx · freeze.tsx · content-width.tsx · supporting-pane.tsx · web-nav-covered.tsx · tab-pane-active.tsx · bottom-inset-reservation.tsxNon-visual or layout infrastructure — members: PerfProfiler, Freeze, ContentColumn, ContentWidthProvider, SupportingPane, WebNavCovered, TabPaneActiveProvider, BottomInsetReservationProvider———13·3keep. No visual spec needed.
Charts: SparkLineui:charts/SparkLine.tsx · SparkLine.shared.tsSVG trend line with scrubcolor (auto trend green/red), selectedIndex, onScrubPan scrub (activeOffsetX biased) sets a marker; reduced-motion n/adefault 120×48 (shared.ts:54-55), stroke 2 (:98)1 (1)re-spec → T-Chart. v2 chart is 160 tall, inset 16, one ink line, no fill.
Charts: CapacityRing / SegmentedDonutChart / TriRingProgressui:charts/*.tsxRing/donut progresssegments, sizepassiveCapacityRing 20, stroke 3 (CapacityRing.tsx:52-53)2·4·1keep. Not on the six v2 screens; capacity leaves rows.
Charts: CourtLinesTile / CelebrationOverlayui:charts/CourtLinesTile.tsx · CelebrationOverlay.tsxCourt-surface graphic / confettisurface kindconfetti auto-plays—6·1keep
ValueSliderui:value-slider.tsxSingle-value flat-fill slider—drag + a11y adjustbar 28, track 6, knob 24 (value-slider.tsx:22-24)0 (0)retire. Dead: no JSX consumer (create-club-step6.tsx:384 mentions it as retired).
Dead exports (no consumers)ui:empty-slot-row.tsx · banner-capacity-cluster.tsx · participation-cluster.tsx · charts/DiagonalGradientFill.tsx · charts/FormatGlyph.tsxExported, never rendered———0·0·0·0·0retire. Zero JSX consumers repo-wide (ParticipationCluster is referenced only by the dead BannerCapacityCluster).
DataSection (retired 2026-11-13: deleted, no importer; the state helper replaced it)ui:data-section.tsxLoading / empty / content switch with optional SectionBlock titlehideWhenAbsent, skeleton, emptycross-fade medium spring on state change, off under reduced motiondefault skeleton = Card + SkeletonRow1 (1)re-spec → T-Skeleton host. 1 consumer; default skeleton still draws a Card.

1b. index.ts exports that are not components ​

These also need a v2 decision because they carry the numbers the components read.

Export (index.ts line)What it holdsTodayv2
tokens, bodyFont, headingFont (:15-16)colour, space, size, radius, zIndex tokens; Pretendard sizes 11–32 and line-heightsspace 0/4/8/12/16/20/24/32/40/48/64 (ui:space-scale.ts:6-18); radius 0/4/8/12/16/20/24/9999 (ui:tokens.ts:157-166); font sizes 11,12,13,14,15,16,18,20,24,28,32 (ui:fonts.ts:35-48)space ladder already covers 4…40; font size set lacks 17 and 40 (17 and 36/48 are literals in text.tsx:98-100,172-189)
SEAM, HAIRLINE, CONTENT_INSET, SCROLL_CONTENT (:17-20)spacing roles, the one hairline, scroll paddingrail $4=16 (ui:seams.ts:44), section $6=24 (:47), block $3=12, item $2=8, element $1=4, tail $9=48; hairline 1px $borderSubtle (ui:hairline.ts:18-20)rail 20, section 32, one more hairline tier, band
WINDOW_*, LAYOUT_WIDTH, windowSizeClass, ContentColumn (:21-31)desktop-web size classesreading column 680, shell 1200 (ui:window-size.ts:45,56)web follows later (grammar §6)
META_TEXT, META_BREAK_ON_TINT_OPACITY (:33-34)meta-text tiersprimary cardBody/secondary, secondary cardMeta/tertiary, eyebrow 700, caption (ui:text-grammar.ts:40-45)re-point to 15/22, 13/18, 12/16
EMOJI_GLYPH, emojiLineHeight, NAME_LINES, EXPLANATION_LINES (:37,41)emoji line box fontSize+8; names wrap to 2 lines; explanations to 4ui:text.tsx:38-56, ui:text-lines.ts:27,37emoji only in the weather exception
MOTION_SPRING, MOTION_DURATION, MOTION_STAGGER, staggerDelay, useReducedMotion, USE_NATIVE_DRIVER (:50-57)motion tokensvalues in §2.3; staggerDelay/MOTION_STAGGER have no consumerkeep; prune unused
AVATAR_SIZE, DOT_SIZE, BADGE_SIZE, TOUCH_TARGET, TERMINAL_MUTE_OPACITY, ROW_HEIGHT_*, RADIUS_PILL, SLIDER_GEOMETRY (:60-71)named box sizesavatar 28/32/40/44/56/80/88 (ui:box-sizes.ts:24-40); touch 44 (:74); row heights 36/40 (:88,93); mute .55 (:104)avatar 40/48/56; row pitches 80/64/48 replace row heights
DISABLED_OPACITY, HOVER_OPACITY, FOCUS_RING (:47-48)interaction constants.5 (ui:pressable.tsx:99), .85 (ui:pressable-visual.ts:71), 2px $primary ring offset 2 (:84-89)keep
accessibility*Props, adjustableActionProps (:112-118)aria mappersselectionStateProps maps role → aria-checked/selected/pressed (ui:selection-accessibility.ts:2-18)keep
Hooks: useConfirm (:105), useScrollRevealTarget (:106), useSheetBottomPadding (:108), useWizardBackHandler (:180), useLiveBandCommit (:279), useCollapsingTopBar (:317-323), useLayoutPathOrigin (:58), useTabPaneActive (:244), useContentWidth (:30), useBottomInsetReserved (:237), useScrollPerf (:354)behaviour hooks—keep (behaviour carries over)
setUiStrings, uiStrings (:352)injectable close/back/search/apply stringsui:ui-strings.tskeep
themeHex, rawColor (:385)token → hex for raw RN stylesneeded for TextInput, Animated.Viewkeep
setPerfSink, reportUiPerf (:355-361)perf telemetrytab-settle 250ms threshold (ui:pill-nav-shared.ts:12)keep
time-band maths (:256-277)DEFAULT_DOMAIN, SNAP_MINUTES…—form family

1c. packages/app/src/presentation/components — shared (≥ 2 consumer groups) ​

Rows below are the shared components; the 16 live XxxStatusBadge wrappers sit in one row because they are one variant table (the 4 club badges, the club-alarm badge and the dead ApprovalModeBadge are in the appendix). Components with a single consumer group are in the appendix (§1d).

ComponentFilePurposeVariants / props that change the lookStates implemented (read from source)Sizes / geometry todayConsumers files (groups)v2 disposition
QueryErrorStatepres:query-error-state.tsxError panel with retry; falls back to the offline panel when offlinevariant full/compactretry button (Button md)= EmptyState + AlertCircle icon (:48)45 (8)keep → T-EmptyState. Wrapper; follows EmptyState re-spec.
OfflineEmptyStatepres:offline-empty-state.tsxNo-connection panelvariant full/compactoptional retry= EmptyState + WifiOff45 (8)keep → T-EmptyState
NotFoundStatepres:not-found-state.tsxMissing entity panel with 돌아가기—Button → router.back= EmptyState full19 (4)keep → T-EmptyState
QueryBoundarypres:query-boundary.tsx5-state network cascade (offline-cold, loading, error, empty, content)loading skeleton proprenders skeleton → error+retry → content—25 (5)keep. No visual of its own.
PersonRowpres:person-row.tsxListRow + AvatarBubble; default tap → public profile with prefetch on press-inavatarSize, badge, trailing, dense, edgeas ListRow (P row .98/.70, D .5, chevron when badge+press)avatar default SM 32 (avatar-bubble.tsx:159) unless the caller passes MD; title NAME_LINES 2 (text-lines.ts:27); pitch = ListRow: pad-y 8+8 around max(avatar, title 22 [+4 + sub 20]) = 48–62 depending on sub (list-row-visual.ts:34-36, list-row.tsx:152-165)23 (5)re-spec → T-PersonRow. v2 person row: avatar 40, L1 15/22·600, L2 13/18·500, trailing 15/22·700 or one Tag, pitch 64.
AutoLinkTextpres:auto-link-text.tsxLinkifiedText bound to openExternalUrl—link pressinherits7 (3)keep
EloTierBadge / TrustTierBadge / TierBadgeChromepres:elo-tier-badge.tsx · trust-tier-badge.tsx · tier-badge-utils.tsxEmoji + label tier pill (two tier systems)size xs/sm/mdpassivehand-drawn pill: pad 8×4, radius $2, emoji line box = size+8 (tier-badge-utils.tsx:47-59)7·6·2merge → T-Tag (neutral + tier dot on hero/profile; plain text in rows). Spec: TierBadgeChrome → neutral Tag. Counts: 7 / 6 / 2 files.
XxxStatusBadge family (16 live wrappers)pres:status/status-badges.tsx (+ status/badges/club.tsx, club-alarm.tsx, tier-range-badge.tsx)Canonical domain-status → Badge variant maps (canon: status renders only via these) — members: SessionStatusBadge, RsvpStatusBadge, WinResultBadge, MatchStatusBadge, EloDeltaBadge, ScoreStatusBadge, VenueTrustBadge, SessionPaymentStatusBadge, ConsentStateBadge, DuesStatusBadge, AttendanceStatusBadge, RecruitmentStatusBadge, ConfidenceTierBadge, AchievementStateBadge, ConsentRequirementBadge, SessionStripChipBadgesize xs/sm; variant tables (e.g. SESSION_STATUS_VARIANT)passive= Badge xs/sm3·5·4·4·3·3·3·2·2·1·1·1·1·1·1·1keep → T-Tag (re-point the variant tables to Tag tones). Keep the wrapper layer; swap Badge for Tag inside. Counts shown per wrapper in order: Session 3, Rsvp 5, WinResult 4, Match 4, EloDelta 3, Score 3, VenueTrust 3, Payment 2, Consent 2, Dues 1, Attendance 1, Recruitment 1, Confidence 1, Achievement 1, ConsentReq 1, StripChip 1. (ApprovalModeBadge is dead and listed in the appendix.)
TierRangeBadgepres:status/tier-range-badge.tsxNTRP/ELO band label (dots + numbers)inline vs badge densitypassive—2 (2)merge → T-Tag tone=level. v2 level Tag is a 22px circle with a numeral (list spec §4).
PlayerIdentityChip / PlayerIdentityCardpres:player-identity-chip.tsx · player-identity-card.tsxAvatar + name + tier, compact (XS avatar) or card—P row when pressablechip: avatar XS 286·2re-spec → T-PersonRow compact
EditControlModal / EditFieldModalpres:edit-field-modal.tsxPer-field bottom edit sheet: title + control + 취소/저장commit apply/saveModalPanel placement=bottom; scrim/취소 closes; no dragfull width, flush bottom, pair of Buttons6·2keep → T-Overlay edit sheet. This is the v2 "filter/edit sheet → ModalPanel" pattern.
VenueRowpres:venue-row.tsxVenue row: photo → static map → glyph tile + name + meta + chevronvariant card (bordered Card) / plain; denseP card or row; chevron cuetile 44 (32 dense) r12 (venue-row.tsx:137,156)6 (3)re-spec → T-MediaRow
TierInfoSheet / TrustTierInfoSheetpres:tier-info-sheet.tsx · trust-tier-info-sheet.tsxModalPanel explainer of tiers (current tier highlighted) — members: TierInfoSheet, ProfileTierInfoSheet, ProfileTrustTierInfoSheet (TrustTierInfoSheet is in the appendix)—ModalPanelrows with $primarySubtle highlight5·2·2keep → T-Overlay
SessionCardFromEntry (→ SessionCardV2)pres:sessions/session-card-from-entry.tsx · session-card-v2.tsxThe session list card: venue tile + L1 + one chip + bookmark + facts + recruitment metervariant card / embedded; live bandP card .98/.90 · live band · bookmark is a sibling Pressable at a measured slotCard r16, padding 12 (session-card-v2.tsx:549); ≈163 pt per card incl. gap (list spec §2)5 (4)retire → T-MediaRow. Replaced by the 80pt media row. SessionCardV2 has 1 direct consumer (this wrapper); SessionCompactRow (1) and ClubSessionCard (0) die with it.
TierRangeSelectorpres:tier-range-selector.tsxContinuous tier band slider + explanation—drag, snap, a11y adjustband bar 44, knobs 44 hit4 (2)keep → F-Slider
VenueSearchTakeoverpres:venue-search-takeover.tsxFull-screen venue search overlay for wizard picks—RN Modal, imperative autofocus on Android, SearchBar + VenueFinderfull-screen4 (2)keep → T-Overlay takeover
RegionDistrictFilterpres:region-district-filter.tsx시/도 → 시/군/구 DropdownChip pair—cascade resettwo Choice chips3 (3)keep → T-Choice mode=menu
SessionDayHeaderpres:sessions/session-day-header.tsxDay group header: date + relative qualifierfirstpassiveSectionHeader variant=list: title cardTitle 16/24, min-height 24, meta cardMeta tertiary right; top pad 4 unless first3 (3)re-spec → T-DayHeader. v2 day header: 32 tall, 15/22·600 ink date + 13/18·500 secondary relative day at right, 16 above.
ExpandableFilterBarpres:expandable-filter-bar.tsxSearchBar + filter toggle (icon + count) + expanding panel—aria-expanded toggle; panel expandspanel: $card, pad 12, radius $3, gap 163 (3)re-spec → F-Search + filter sheet
LeaderboardRow / LeaderboardPodiumpres:leaderboard-row.tsx · leaderboard-podium.tsxRanked row (28px right-aligned rank) / top-3 podiumleading slotP row when pressablerank cardTitle 16/24 secondary, width 28 (leaderboard-row.tsx:115)3·2re-spec → T-PersonRow (rank column 24). Podium has no v2 counterpart on the six screens.
WonAmountField / SuggestedCountFieldpres:won-amount-field.tsx · suggested-count-field.tsxMoney input grammar / suggestion-following Steppercommit modesedit scope, blur or save commitStyledInput / Stepper geometry3·2keep → F-Field
ToastHostpres:toast-host.tsx · stores/toast.store.tsSingle bottom toast with swipe-down dismiss and actionvariant success/error/warning/info; optional actionenter/exit y:12 on reveal 250ms (:333-334); drag-dismiss ≥40px or 500px/s (:164-165); timer pauses on touch (:198); role alert aria-live polite (:336)radius $6, pad 16×12 (:343-345), max 560 (:183), shadow .16/r10 (:350), 8 above bottom chrome/keyboard (:182), text cardBody 2 lines (:355), 2800ms (toast.store.ts:78)2 (2)re-spec → T-Toast. Dark inverse surface; icon + text + ghost action.
CopyableValuepres:copyable-value.tsxCopy-to-clipboard value (web real copy, native hint)—P—2 (2)keep
AnimatedCountpres:motion/animated-count.tsxIn-place numeric update———2 (2)keep
ElapsedClock / TourRefInjectorpres:elapsed-clock.tsx · tour-ref-injector.tsxTicking clock leaf / coachmark tour ref———3·3keep. Infra.
SessionSaveButton / SaveButton / VenueSaveButtonpres:sessions/session-save-button.tsx · save-button.tsx · venue-save-button.tsxBookmark toggle (optimistic)variant card/headeraria-pressed; P iconcard size SAVE_BUTTON_CARD_SIZE2·2·2merge → T-IconButton (toggle). Bookmark leaves rows; stays in the detail header (D6).
RecruitmentMeterpres:sessions/session-compact-row.tsxBar + N/M count—passiveProgressBar height 5 muted (:131)2 (2)retire. Capacity bar leaves rows; N/M becomes the trailing text or a stat.
VenueDirectoryContent / PickedVenueCard / CourtUsagePanelpres:sessions/venue-directory-content.tsx · picked-venue-card.tsx · court-usage-panel.tsxVenue directory body / picked venue + court config——compose VenueRow, AttachedPanel, SelectionChip grid2·2·2keep. Re-skin through the primitives they compose.
MatchHistoryRow / MatchHistoryScopeSelectorpres:match-history-row.tsx · match-history-scope-selector.tsxMatch result row with avatars, badges, score / scope facet fields—P rowavatar stack, chips, delta badge, chevron2·2re-spec → T-PersonRow (L1 opponents, trailing score + W/L Tag)
ReportModalpres:report-modal.tsxReport sheet: category chips + detail field + submit—submit disabled until a category is chosenModalPanel + SelectionChip + StyledInput2 (2)keep → T-Overlay

1d. Appendix — presentation components with one consumer group or none (54 names) ​

Dispositions here are coarse (the component is feature-shaped); the point is to see which of them the six v2 screens replace.

ComponentFileConsumers files (groups)v2 disposition
MatchScoreboardpres:sessions/match-scoreboard.tsx7 (1)keep → scoreboard, feature-specific
MatchRowHeaderpres:sessions/match-row-header.tsx4 (1)keep
StyleOptionRowpres:sessions/style-option-row.tsx3 (1)re-spec → T-CellRow option
SessionLiveAnchorpres:sessions/session-live-anchor.tsx3 (1)re-spec → live band stays on detail
VenueFinderpres:sessions/venue-finder.tsx3 (2)keep → canonical venue search; chrome re-skin
PeriodNavPanelpres:period-nav-panel.tsx3 (1)re-spec → F-PeriodStepper
SessionLiveBandpres:sessions/session-strip-ui.tsx2 (1)re-spec → live → danger Tag (D7)
SourceChipspres:sessions/venue-confidence.tsx2 (1)merge → T-Tag
EloFactRowpres:elo-fact-row.tsx2 (1)re-spec → T-CellRow
UpdateRequiredScreenpres:update-required-screen.tsx1 (1)keep
NetworkBannerpres:network-banner.tsx1 (1)re-spec → T-Notice system strip
ClubAlarmBadgepres:status/badges/club-alarm.tsx1 (1)merge → T-Tag
ClubJoinabilityBadgepres:status/badges/club.tsx1 (1)merge → T-Tag
ClubRecruitBadgepres:status/badges/club.tsx1 (1)merge → T-Tag
ChallengeStatusBadgepres:status/badges/club.tsx1 (1)merge → T-Tag
MatchStatusDotpres:status/match-status-dot.tsx1 (1)keep → dot
ActivityMetricSelectorpres:activity-metric-calendar.tsx1 (1)keep
ActivityMetricCalendarpres:activity-metric-calendar.tsx1 (1)keep → calendar grid
DmThreadRowpres:dm/dm-thread-row.tsx1 (1)re-spec → T-PersonRow
VenueSearchResultsListpres:venue-search-results-list.tsx1 (1)keep
ReliabilityPillpres:reliability-pill.tsx1 (1)merge → T-Tag
SessionLeadBadgepres:sessions/session-strip-ui.tsx1 (1)merge → T-Tag
VenueFilterBarpres:sessions/venue-filter-bar.tsx1 (1)re-spec → F-Search
DirectoryVenueCardpres:sessions/directory-venue-card.tsx1 (1)re-spec → T-MediaRow (venue)
ConfidenceBarspres:sessions/venue-confidence.tsx1 (1)keep
VenueFinderHeaderpres:sessions/venue-finder/venue-finder-header.tsx1 (1)re-spec → F-Search + Choice
VenueFinderSearchResultspres:sessions/venue-finder/venue-finder-search-results.tsx1 (1)keep
VenueFinderBrowseListpres:sessions/venue-finder/venue-finder-browse-list.tsx1 (1)keep
VenueFinderContributionRowpres:sessions/venue-finder/venue-finder-contribution-row.tsx1 (1)keep
VenueTilepres:sessions/venue-tile.tsx1 (1)keep → map-thumb cascade; 48/56 thumb
SessionCompactRowpres:sessions/session-compact-row.tsx1 (1)retire → dies with the card (T-MediaRow)
SessionCardV2pres:sessions/session-card-v2.tsx1 (1)retire → T-MediaRow
VenueCardpres:venue-card.tsx1 (1)keep → detail tile; re-skin
VenueSearchResultRowpres:venue-search-result-row.tsx1 (1)re-spec → T-MediaRow (venue)
VenueLinkpres:venue-link.tsx1 (1)merge → T-Tag / meta text
AppVersionGatepres:app-version-gate.tsx1 (1)keep
MatchHistoryScopeFieldspres:match-history-scope-selector.tsx1 (1)keep
MatchHistoryScopeSheetpres:match-history-scope-sheet.tsx1 (1)keep → T-Overlay
DevPanelpres:dev-panel/index.tsx1 (1)keep → dev only (the one legal Tamagui Sheet)
DiagnosticsPanepres:dev-panel/diagnostics-pane.tsx1 (1)keep → dev
LogsPanepres:dev-panel/logs-pane.tsx1 (1)keep → dev
TelemetryPanepres:dev-panel/telemetry-pane.tsx1 (1)keep → dev
ScenariosPanepres:dev-panel/scenarios-pane.tsx1 (1)keep → dev
VenueMonogrampres:venue-monogram.tsx1 (1)keep
CoachmarkTooltippres:coachmark-tooltip.tsx0 (0)keep → coachmark
ApprovalModeBadgepres:status/status-badges.tsx0 (0)retire → dead (referenced only in comments)
ClubRoleBadgepres:status/badges/club.tsx0 (0)retire → dead (barrel export only)
RecapOutcomePanelpres:recap-outcome-panel.tsx0 (0)retire → dead (barrel export only)
MatchFormatScopeSelectorpres:match-format-scope-selector.tsx0 (0)retire → dead (0 consumers)
ClubSessionCardpres:sessions/club-session-card.tsx0 (0)retire → dead (0 consumers)
VenueHeropres:sessions/venue-hero.tsx0 (0)retire → dead (barrel export only)
VenueTrustRingpres:sessions/venue-confidence.tsx0 (0)retire → dead (0 consumers)
HourlyWeatherStrippres:sessions/hourly-weather-strip.tsx0 (0)retire → dead (0 consumers)
TrustTierInfoSheetpres:trust-tier-info-sheet.tsx0 (0)keep

Feature-local surfaces outside the brief's scope but replaced by v2 rows (consumer files in parentheses; each has one consumer group): ClubCard (1) packages/features/clubs/src/shared/club-card.tsx; ClubAvatar (5) …/club-avatar.tsx; ClubLogoBubble (1); ArchivedClubRow (1); PostFeedRow (1) …/post-row.tsx; ParticipantRow (3) packages/features/sessions/src/components/participant-row.tsx; NotificationRowShell (2) packages/features/home/src/shared/notification-row-shell.tsx; FriendPickerRow (1); EntryIconBubble (2); MemberRow (1); DuesRow (1); MatchCard (1); home period pickers DayCalendarPicker, MonthYearPicker, WeekMonthPicker (1 each).

1e. Foundation deltas — today's numbers versus grammar v2 ​

These are the values every re-spec above is changing; recorded once so component designs do not re-derive them.

ItemToday (file:line)Grammar v2
Rail16: SEAM.rail (ui:seams.ts:44), CTA band sides (ui:bottom-cta-band-visual.ts:26)20 (§1)
Body textcardBody 14/22·400 (ui:text.tsx:105)15/22·400
Meta textcardMeta 14/20·500 (ui:text.tsx:112)footnote 13/18·500-600, caption 12/16
Row line 1ListRow title 14/22·600 (ui:list-row.tsx:154)media 17/24·600, person 15/22·600
Section titleflat navTitle 17/22 with weight 700 (ui:section-header-metrics.ts:57,77), list cardTitle 16/24 (:58)17/24·600
Screen titleAppHeader centered navTitle 17/22·600 (ui:app-header.tsx:160-164); tab roots left pageTitle 24/32·700 (ui:main-tab-shell.tsx:102)20/28·700 at the rail in a 56 header
Hero titlepageTitle 24/32·700 (ui:type-hero.tsx:85)24/32·700 (unchanged)
Header56 (64 with subtitle) + permanent bottom hairline (ui:app-header-visual.ts:18-20, ui:app-header.tsx:118)56, no divider
Tab bar56 + inset + 8, labels 11/600, active tint $primary, 1px $borderSubtle top border, paddingTop 6 (mobile:app/(tabs)/_layout.tsx:62-78)56 + 34, 11/14·500 (600 active), ink tint, hairline only while content lies below
Primary buttonlg 56, r12, label size from Tamagui $5 = 15 in bodyFont (ui:button.tsx:222,131,181; ui:fonts.ts:40)52, 17/24·600, r12
Section seam24 + full-bleed hairline (ui:flat-column-defaults.ts:23, ui:flat-column.tsx:87)32 of air, no line (§4b)
Item seamDefaultSeparator 12 between cards (ui:feed-list/tuning.ts:16)none — row pitch 80/64 owns it
RadiiCard 16 (ui:card.tsx:29), Button 12 (ui:button.tsx:131), Badge sm 8 (ui:badge.tsx:94), venue tile 12 (pres:sessions/venue-tile.tsx:92), chips fulltag 6, thumb 8, club logo 12, button 12, block 16
Icon sizes12/14/16/20/24/28 (ui:icon-sizes.ts:12-19)24 / 20 / 16
Avatars28/32/40/44/56/80/88, default 32 (ui:box-sizes.ts:24-40, ui:avatar-bubble.tsx:159)40 / 48 / 56
Hairlineone tier $borderSubtle, always drawn (ui:hairline.ts:18-20)edge #D4D4D4 + cell #E5E5E5, edge only under scrolling content
$primarySubtle as a surface23 files in packages/ui/src, 11 in presentation, 22 in features (92 occurrences repo-wide)leaves (list spec §3 / grammar §6)
Brand-tinted ink on controlsselected chip $primary, section link $primary, tab bar $primary, HeaderTextAction $primaryneutral ink; one coloured action per list screen

Also record: ui:segmented-tabs.tsx:150 embeds a literal NUL byte (join('^@')), so file/GNU grep/ugrep treat the file as binary and skip it silently; any audit tooling that greps components will miss this file.

2. Interaction behaviours today ​

Every number below is read from source. The v2 library must reproduce the behaviours marked carry; the others are listed so the redesign knows what it is replacing.

2.1 Press physics — Pressable is the only touchable (carry) ​

  • Six variants, one table. VARIANTS (ui:pressable-visual.ts:43-50): card scale 0.98 / opacity 0.90 · button 0.97 / 0.92 · pill 0.96 / 0.85 · row 0.98 / 0.70 · icon 0.92 / 0.78 · fab 0.96 / 0.90. Button gets the same numbers through Tamagui pressStyle on the Reanimated animation driver (mobile:tamagui.config.ts:9,24-33). Pressable, Button and PillNav all read it (ui:button.tsx:62, ui:pill-nav.tsx:115,186,209-210).
  • Settle = spring, dim = timing, both on the UI thread. Press-in spring {damping 20, stiffness 400, mass 0.7}, press-out {15, 300, 0.7} (ui:motion.ts:29,31); opacity fades 80 ms in, 150 ms out (ui:motion.ts:42,44). Wiring: withSpring(config.pressScale, SPRING_IN) and withTiming(config.pressOpacity, { duration: PRESS_IN_MS }) (ui:pressable.tsx:286,288), reversed on press-out (:299,301).
  • No tint band, ever. The stateLayer wash was retired 2026-09-28 (ui:pressable-visual.ts:7-22; canon docs/canon/components.md:100-105). The one persistent selected fill on a list row is ListRow selected, which paints $primarySubtle and sets aria-current (ui:list-row.tsx:201,216).
  • Reduced motion keeps the dim, drops the settle (ui:pressable.tsx:286,299; ui:button.tsx:292-294). A lint rule (@twomore/require-reduced-motion-gate, described ui:motion.ts:129-146) requires every withSpring/withTiming/animation=/enterStyle in packages/ui/src to be gated or carry // motion-exempt:.
  • Hover (web, real pointers only): opacity 0.85 on every Pressable and Button (ui:pressable-visual.ts:71; ui:pressable.tsx:324; ui:button.tsx:67) plus cursor: pointer (ui:pressable.tsx:311).
  • Focus ring (web only): FOCUS_RING = 2px solid $primary, offset 2 (ui:pressable-visual.ts:84-89), shared by Pressable (:325), Button (ui:button.tsx:68), PillNav (ui:pill-nav.tsx:119,187) and StyledInput (ui:labeled-field.tsx:432). Native has no focus indicator.
  • Disabled / loading = the inert path: a plain View, opacity 0.5, cursor: not-allowed, pointerEvents: none, aria-disabled, aria-busy while loading, no press feedback (ui:pressable.tsx:99,164,187-233). A Pressable with no onPress is also inert (:164).
  • Selection a11y by role (carry): selectionStateProps maps radio/checkbox/switch → aria-checked, tab/option/row → aria-selected, button/none → aria-pressed (ui:selection-accessibility.ts:2-18). Web radios get arrow-key movement (ui:keyboard-navigation.ts:20-29,47, wired in ui:selection-chip.tsx:17,96).
  • Nested targets must call e.stopPropagation() first (canon components.md:17); a control on a tappable card is its sibling placed with useLayoutPathOrigin (canon components.md:119; ui:index.ts:58).

2.2 Hit slop and tap targets (carry) ​

One formula everywhere: hitSlop = (TOUCH_TARGET.MIN − visual) / 2 with TOUCH_TARGET.MIN = 44 (ui:box-sizes.ts:74). Instances: icon variant default 15 for a 14px glyph (ui:pressable.tsx:112); header glyph 24 → 10 (ui:header-icon-button.tsx:50-51); chip md 40 → 2 and sm 32 → 6 (ui:selection-chip.tsx:25,85); list row 40 → 2, dense 36 → 4 (ui:list-row-visual.ts:34-39); stepper 32 → 6 (ui:stepper.tsx:31,96); action icon 36 → 4 (ui:action-icon-button.tsx:42-43); camera badge 28 → 8 (ui:camera-badge.tsx:25); action pill 28 → 8 (ui:action-pill.tsx:27); removable-chip ✕ 12 → 16 (ui:removable-chip.tsx:34). On web the slop is realised by an invisible out-of-flow child inside the Pressable, because the DOM has no hitSlop (ui:pressable.tsx:77-90; ui:pressable-visual.ts:91-114). First/last rows in a RowList add their trimmed padding to the slop (ui:list-row-visual.ts:70-83).

2.3 Motion tokens ​

  • Springs (ui:motion.ts:27-34): pressIn, pressOut, navSlide {22, 280, 0.8} (PillNav indicator, ui:pill-nav.tsx:327). Tamagui presets (mobile:tamagui.config.ts:25-32): quick spring {20, 250}, medium {18, 170}, slow {15, 100}, reveal = 250 ms timing (no spring tail; owner QA 2026-08-08). Used by: quick → ModalPanel bottom entry (ui:modal-panel.tsx:209), SearchBar border (ui:search-bar.tsx:117), ExpandChevron (ui:expand-toggle.tsx:56-57); reveal → RevealStack (ui:reveal-stack.tsx:165), ExpandReveal (:156), ToastHost (pres:toast-host.tsx:333); medium → DataSection (ui:data-section.tsx:120).
  • Durations in MOTION_DURATION (ui:motion.ts:40-74). Used: pressIn 80, pressOut 150, slow 300 (ProgressBar, ui:progress-bar.tsx:83), shimmer 800 (ui:shimmer.ts:33,38), pulseDot 1600 (ui:pulse-dot.tsx:81), exitFast 100 (toast drag-out, pres:toast-host.tsx:279-282). Unused: fast 150, standard 220, entrance 250, exitStandard 180, modal 300, modalExit 200, liveDot 500, badgePulse 800, and the whole stagger set (MOTION_STAGGER 50 ms/item, max 300, first 6 — ui:motion.ts:81-93) have no consumer.
  • Screen transitions are native-stack defaults: no animation, presentation or gestureEnabled option is set in any apps/mobile/app/**/_layout.tsx.
  • Reduced motion comes from useReducedMotion (ui:motion.ts:147-170: AccessibilityInfo on native, prefers-reduced-motion on web).

2.4 Loading patterns ​

  • Skeleton: Animated opacity loop 0.6 ↔ 0.9, 800 ms per half-cycle, native driver off on web (ui:shimmer.ts:21,32-38; ui:motion.ts:67,178); reduced motion pins 0.75 (ui:shimmer.ts:14). Fill $surfaceTertiary (ui:skeleton.tsx:64), default 16 × 100%, radius 8 (:41-42). RemoteImage reuses the same hook for its loading pulse (ui:remote-image.tsx, skeleton while status = loading).
  • Spinners: RN ActivityIndicator size="small" only — inside Button loading (replaces the icon slot, button auto-disabled, optional loadingLabel; ui:button.tsx:288-289,297) and ActionIconButton loading (ui:action-icon-button.tsx:111); 8 other files use a raw ActivityIndicator. No branded spinner, no determinate loader except ProgressBar.
  • Screen cascade: QueryBoundary renders offline-cold → skeleton → error+retry → content (pres:query-boundary.tsx); errors are EmptyState + AlertCircle, offline is EmptyState + WifiOff (pres:query-error-state.tsx:48, pres:offline-empty-state.tsx). NetworkBanner is the one root-mounted strip: $badgeWarningBg, pad 16×8, WifiOff glyph, role="alert" (pres:network-banner.tsx:64-69).
  • Content entrance: only RevealStack/ExpandReveal/DataSection (retired 2026-11-13) animate in; list rows do not stagger (the stagger tokens are unused).

2.5 Disabled and muted recipes ​

0.5 opacity is the single disabled value: Pressable (ui:pressable.tsx:99), Button (ui:button.tsx:74), ListRow (ui:list-row-visual.ts:241), StyledInput editable={false} (ui:labeled-field.tsx:464), SearchBar (ui:search-bar.tsx:118), AddCard (ui:add-card.tsx, border drops to $borderSubtle, ink to $textTertiary). Outliers: ActionIconRow disabled 0.4 (ui:action-icon-row-visual.ts:52); terminal-state muting 0.55 via TERMINAL_MUTE_OPACITY (ui:box-sizes.ts:104); reduced-motion shimmer 0.75.

2.6 Toast / snackbar (ToastHost) ​

One toast at a time, from useToastStore (app:stores/toast.store.ts). Duration 2800 ms default (toast.store.ts:78); variants success/error/warning/info (:3); optional text action rendered as a ghost Button sm with weight 800 (pres:toast-host.tsx:361-363). Position: absolute, centered, bottom = max(tab-bar chrome height or safe inset + 8, keyboard height + 8) (:182,220-223; chrome height published from mobile:app/(tabs)/_layout.tsx:47,57). Enter/exit: opacity 0 and y 12 on reveal 250 ms (:333-334). Dismiss: swipe down — drag past 40px or 500px/s flings it out in 100 ms, upward drag resisted ×0.15 (:164-171,259-294); the auto-dismiss timer pauses while touched (:198, pauseToastTimer). Surface: inverse $inverseSurface, radius $6 (24), pad 16×12, max width 560, shadow opacity .16 radius 10 offset 4 (:343-352), role="alert" aria-live="polite" (:336); text cardBody 14/22, 2 lines (:355). It is the only floating surface besides the header menu that carries a shadow.

2.7 Pull-to-refresh ​

Native RefreshControl through FeedList/GroupedFeedList refreshing/onRefresh (ui:feed-list/grouped-feed-list.tsx:61-62,157-158; ui:feed-list.tsx:92-93,148-149); 9 screens also mount a raw <RefreshControl> (list below). The spinner is a gesture acknowledgement only: usePullToRefresh sets refreshing from the pull until that refetch settles and never for background refetches (app:hooks/use-pull-to-refresh.ts:39-70; lint no-background-refetch-spinner). There is no custom pull visual, colour or threshold anywhere. Raw <RefreshControl> files: club-challenges, public-club-profile, session-detail, directory-venue-detail, match-board, profile, records, record-day-detail, record-detail.

2.8 Keyboard handling ​

  • Shells: ModalPanel wraps in KeyboardAvoidingView behavior="padding" (ui:modal-panel.tsx:117); WizardShell wraps content + footer in the same (ui:wizard-shell.tsx:391) so the CTA lifts above the keyboard; DetailShell sets iOS automaticallyAdjustKeyboardInsets (ui:detail-shell.tsx:281) and relies on Android softwareKeyboardLayoutMode: "pan" (mobile:app.json:80; comment ui:detail-shell.tsx:279-280).
  • Taps while typing: ModalPanel.ScrollView keyboardShouldPersistTaps="handled" (ui:modal-panel.tsx:244); DetailShell forwards the prop (ui:detail-shell.tsx:120,142,269).
  • Android autofocus in a Modal is unreliable, so VenueSearchTakeover focuses imperatively through SearchBar inputRef (pres:venue-search-takeover.tsx:83-87,186).
  • ToastHost listens to keyboardDidShow/Hide to float above the keyboard (pres:toast-host.tsx:209).
  • react-native-keyboard-controller 1.22.5 is installed (mobile:package.json:55, plugin :80) but imported nowhere — dormant, per project memory; v2 forms should not assume it.
  • Submit behaviour: StyledInput forwards returnKeyType/onSubmitEditing (ui:labeled-field.tsx:259-260,394-395); RevealStack confirms a section on keyboard done, never on validity (canon components.md:88).

2.9 Overlays (carry the behaviours) ​

OverlayMechanismDismissMotionGeometry
ModalPanel center dialogRN Modal transparent, overFullScreen, statusBarTranslucent (ui:modal-panel.tsx:105-111)backdrop tap (default on, :99), Android back (:111), built-in ✕Modal fade (:108)frame ≤640 × ≤88%, $background, 1px $borderSubtle, r16, pad 20×16 (:189-197); ✕ 40 circle at top 12 right 12 (:214-231)
ModalPanel placement="bottom" (edit sheet)samebackdrop / 취소; no drag, no grabberfade + frame slides y: 48 on quick (:209)flush bottom, top radii 24, pad-bottom 20 + safe inset (:199-207), no ✕ by default (ui:modal-panel-visual.ts:22)
ConfirmSheet (useConfirm)ModalPanel center (ui:confirm-sheet.tsx)backdrop does not dismiss (:180); Android back closes (:128)fadetitle cardTitle 700 centered; ≥3 buttons stack (:201) else row
HeaderActionsMenuRN Modal fade, transparent, anchored by measureInWindow (ui:header-actions-menu.tsx:342, layout ui:header-actions-menu-layout.ts)backdrop, Escape (web, :287), item pressfadewidth 200–260, rows ≥44, r12, shadow .12/r10/elev 8 (:71-76,384,422); one Divider before the destructive group (:410)
ToastHostabsolute view in the rootswipe / timer / actionreveal§2.6
Dev panelTamagui Sheet (the one legal Sheet)drag—dev only (pres:dev-panel/index.tsx:414-430)
Date/timereact-native-modal-datetime-picker used directly in 6 feature files (web shim at apps/web/lib/shims/react-native-modal-datetime-picker.tsx); locale="ko_KR", iOS confirm/cancel strings (packages/features/sessions/src/create-session/step1/create-session-step1.tsx:280-288)nativenativeno shared wrapper
Coachmarkreact-native-spotlight-tour + pres:coachmark-tooltip.tsxtour controls—home FAB spotlight (packages/features/home/src/views/home-today-view.tsx:64,124-137)

Backdrop colour is $overlayBackdrop = rgba(0,0,0,0.5) (ui:tokens.ts:128).

2.10 Haptics ​

expo-haptics is wrapped once (app:utils/haptics.ts:8-43: light, medium, heavy, success, warning, error, selection). Primitives fire none: Pressable, Button, PillNav, SelectionChip have no haptic call. They fire from mutation outcomes: createMutationHook plays haptic.success() by default and haptic.error() on failure (app:hooks/create-mutation-hook.ts:88-95,182-188,243-244), plus 5 direct calls (RSVP, post reaction, DM send, poll select, booking alert — haptic.light/selection in app:hooks/mutations/use-rsvp.ts:458, …/post/reactions.ts:216, …/post/poll.ts:100, app:hooks/use-booking-alert.ts:27, …/use-dm-mutations.ts:202) and two haptic.warning() in the create-club submit. Toast has no haptic of its own.

2.11 Scroll-linked and list behaviours ​

  • CollapsingTopBar: expanded transparent over the hero, flips to solid past heroBand − 48 with a hysteresis band (ui:use-collapsing-top-bar.ts:37-59; ui:collapsing-top-bar-visual.ts:13); the solid layer's opacity animates, not the colour (ui:collapsing-top-bar.tsx:122-124). Club home only.
  • SegmentedTabs: useDeferredValue keeps the tab flip urgent, hidden panes are frozen with React Activity (ui:freeze.tsx:1-10), no horizontal swipe between panes.
  • FeedList: FlashList, draw distance 800, end-reached threshold 0.6 (ui:feed-list/tuning.ts:38,44); recycling={false} is the fix for the Fabric blank-cell bug (ui:feed-list.tsx:83, memory note). Web variant is separate (ui:feed-list.web.tsx).
  • Prefetch on press-in: rows pass onPressIn to a prefetchX helper (pres:person-row.tsx default; canon components.md:108).

3. Forms — what a v2 form family must cover ​

The grammar has not specified forms. Below is every control the app renders today with its geometry, so the family can be designed once. v2 anchors worth reusing: body 15/22 (the input font is already 15, ui:labeled-field.tsx:455), button radius 12 (inputs are already r12, :449), Choice 36, Primary button 52.

ControlFileGeometry todayStates todayNotes for v2
Field wrapper LabeledFieldui:labeled-field.tsxlabel row: sm = cardMeta 14/20 secondary, lg = cardTitle 16/24 (:173-181); required * in $error; labelRight slot; gap $1/$2 (:198); caption/error cardMeta (:222,227); collapsible label row min 44 with ExpandChevron (:210)error = text + aria-live="polite" (:227), input gets an aria-label suffix (:365); no icon, no success stateone wrapper for label, caption, error; caption goes after any AttachedPanel (canon wizard rule)
Text input StyledInputui:labeled-field.tsx:359-445raw RN TextInput: 1px $border, r12, pad 12×12, 15px, placeholderTextColor $textSecondary; no min-height single-line (≈44–46 from padding + line); multiline min 120, top-aligned (:449-461); bare removes border/padrest · focus border $primary (+web ring :432) · invalid border $error (:372) · editable={false} 0.5 (:464)v2 field height, label placement and error ink undefined; focus border colour is brand $primary
Search SearchBarui:search-bar.tsxmin-height 44, pad 12×8, r12, 1px (:110-114), Search icon 14, input 15 (:149), clear ✕ hitSlop 15 (:169), rightAccessory slotfocus border $primary on quick (:114-117); disabled 0.5 (:118)used inside ExpandableFilterBar with a filter toggle + count badge as the accessory
Money / countpres:won-amount-field.tsx, pres:suggested-count-field.tsx, ui:stepper.tsxStepper: buttons 32 circle 1px $border fill $surfaceSecondary (:31,59-72), value min-width 48 (:113), fullWidth variant min 48 r12 (:167)disabled at bounds; aria-valuemin/max/now (:195)digits-first edit, comma display on commit
Switch TogglePillui:toggle-pill.tsxtrack 44×24 r12, thumb 20, inset 2 (:17-20)on $primary / off $border (:40); no thumb animation, no role="switch", no aria-checked; host row (ListRow onPress) owns the press; 9 filesneeds motion (a 200 ms toggle duration token exists, ui:motion.ts:48, but nothing uses it), switch semantics, disabled and focus states
Check / radio SelectionCheckui:selection-check.tsx24; square r6 border 1.5, circle border 1 (:34-41)checked = fill $primary + Checkused in 2 files; option lists mostly use a trailing Check 16 (ui:dropdown-chip.tsx:209)
Choice chip SelectionChipui:selection-chip.tsx§1a§1a→ T-Choice (36 / 14 / 15/22·500)
Choice tile SelectionCardui:selection-card.tsxpadding 16, r12, border 2 (:44), fill $primarySubtle when selectedP card; S border+fill9 files (format tiles, presets, play style)
Dropdownui:dropdown-chip.tsx, ui:range-chip.tsx, ui:chip-picker-panel.tsxchip → center ModalPanel with title, option rows (pad 12×8, Check 16), primary lg 적용 button (dropdown-chip.tsx:320)single aria-selected, multi aria-checked; "전체" always firstpicker is a centered dialog, not a bottom sheet
Picker row PickerRowui:picker-row.tsxmin 44, 1px $border, r12, pad 12; active border 1.5 $primary (:63-78)active/disabledopens a native date/time picker or modal; 2 files
Edit row EditFieldRowui:edit-field-row.tsxpad 8×16, label 14/20 tertiary over value 14/22 (2 lines), pencil 16P rowsettings edit entry
Add / removeui:add-card.tsx, ui:remove-icon-button.tsx, ui:action-icon-button.tsxAddCard min 52, 1.5px dashed $primary; remove = 36 circle Trash2 16 with built-in confirm (remove-icon-button.tsx:74)D 0.5canon: trash = delete item, ✕ = dismiss
Attached panelui:attached-panel.tsx1.5px $primary border + $primarySubtle fill, pad 12, margin-top 8, 12px notch (:39-74)passive; live-commitfill must change in v2
Reveal stackui:reveal-stack.tsxsection gap 20 ($5, :109); confirm row = skip (flex 1) + confirm (flex 2) (:178)new section enters on revealsequential-reveal contract (canon components.md:88)
Wizard frameui:wizard-shell.tsxheader: 40 back/close square + hit slop 2 (:65), 4px tappable progress segments (:316), count text; footer: one full-width lg Button (+ primaryHint)back → ✕ on step 0; skip in headernav law: header owns movement, footer one CTA
Slidersui:time-band-slider.tsx, ui:time-band-field.tsx, ui:range-band-slider.tsx, pres:tier-range-selector.tsx, ui:hue-slider.tsx, ui:value-slider.tsxbar 44 hit, track 28, trim handle 11, knob 18, handle hit ±22 (ui:box-sizes.ts:115-143); ValueSlider track 6 knob 24 (dead)drag via Pan, snap + live label, a11y adjustable actions"no scroll + drag on one axis" rule; four custom sliders, no shared base spec
Date / timereact-native-modal-datetime-picker in 6 feature files (+ a web shim, apps/web/lib/shims/react-native-modal-datetime-picker.tsx) + home calendarsnative wheel (iOS) / dialog (Android); home: DayCalendarPicker, MonthYearPicker, WeekMonthPicker over PeriodScrubberShell cardnativeno shared wrapper component at all
Composerpackages/features/messaging/src/dm-thread/compose-bar.tsxraw RN input r20, pad 14×10, min 40 / max 100 (:116-123); send = 40 circle $primary with ↑ glyph (:97-103)send disabled when emptyfeature-local; the post comment box is separate
Score entrypackages/features/sessions/src/multi-set-score-entry.tsx, match-card.tsx10 raw RNTextInputs with local styles—numeric inputs with number-pad; only other raw inputs in the repo
Search chromepres:expandable-filter-bar.tsx, pres:sessions/venue-filter-bar.tsx, pres:sessions/venue-finder/venue-finder-header.tsx, pres:venue-search-takeover.tsxSearchBar + toggle (SlidersHorizontal + count) + panel $card pad 12 r12 gap 16 (expandable-filter-bar.tsx:161-163); header: RemovableChips + nav SelectionChips wrap with gap 8 (venue-finder-header.tsx:77-116); takeover = full-screen Modaltoggle aria-expandedvenue finder is the one canonical search surface (canon components.md:89)
Edit sheetpres:edit-field-modal.tsxbottom ModalPanel with title, control, 취소 · 저장 pairscrim/취소 closesthe v2 "filter sheet / edit sheet"

Form-wide laws that must survive: wizard nav law (footer = one CTA, header owns back/skip); sequential reveal on explicit confirm; live-draft vs staged-commit split (AttachedPanel live, ModalPanel staged); useUnsavedChangesGuard for editors (canon components.md Wizards & confirms); error text below the control; captions after attached panels.

4. Gaps ​

4.1 Components the six v2 screens imply but the grammar does not specify ​

"Today" cites what exists; "Must decide" is what the library design owes before page layouts start.

#ComponentWhere the six screens need itTodayMust decide
G01Search field경기 header search; 클럽 탐색SearchBar min 44, r12, pad 12×8, focus border $primary (ui:search-bar.tsx:110-118)header-takeover vs inline; field height/radius at the 20 rail; focus ink (neutral or brand); where the filter toggle + count live
G02Filter / edit sheetlist spec §2 turns scopes into underline tabs, but region/format/fee/tier filters have no tab formDropdownChip/RangeChip open a centered ModalPanel with 적용 (ui:dropdown-chip.tsx:320); EditControlModal is a bottom ModalPanel (pres:edit-field-modal.tsx)one sheet grammar (bottom, scrim closes, no drag, pair of buttons) vs the two in use; Choice menu/range modes inside it
G03Toastevery mutationToastHost (§2.6): inverse surface r24, shadow, bottom of screen, swipe-downcolour/ink, radius, shadow allowed? (grammar §4b says no shadow on tab bar/CTA; toast and header menu are the only shadowed surfaces), position relative to FAB and tab bar
G04Noticedetail hero zone alerts, payment/approval notices, offline stripfour shapes: Banner, Callout, Well, NetworkBanner — all fill with $primarySubtle / badge tints (ui:banner.tsx:83-91, ui:well-tone.ts)one inline notice from the weak Tag tone table; with/without action; whether a tappable notice shows a chevron
G05Empty and error states기록 친구 tab, no sessions, offlineEmptyState icon disc 64 on $primarySubtle (ui:empty-state.tsx:75)icon disc fill (surface leaves), title 17/24 vs 15/22, button size; one component for empty/error/offline/not-found
G06Skeleton at v2 pitchesall listsskeleton rows ≈56 pitch (avatar 40), SkeletonCard/SkeletonHero wrap Card (ui:skeleton.tsx:77-135)shapes for media row 80, person row 64, block 144/158, stat strip 80, chart 160, hero
G07Pull-to-refreshevery listnative RefreshControl, no styling (§2.7)tint colour, whether the brand block participates
G08FAB behaviour홈, 경기none; variant="fab" physics exists, zero uses; canon line says "no FABs" (docs/canon/components.md:13)scroll hide/shrink, keyboard, tab-bar offset 16, safe area, coachmark target, label length
G09Tab-bar badge / dot홈 live dot, 클럽 unreadHomeTabIcon 7px $error dot with 1.5px $card ring (mobile:app/(tabs)/_layout.tsx:168-194); no count formdot vs count; ink; whether tab bar may carry colour
G10Overflow menudetail header ⋯HeaderActionsMenu shadowed dropdown 200–260 wide (§2.9)surface fill, radius, shadow vs hairline, 20 rail alignment
G11Settings / navigation rowsettings, admin hubs, link lists (ListRow is in 34 files)ListRow with chevron or trailing control (§1a)v2 has media 80, person 64, cell 48 only. Needs: nav row with chevron, toggle row, value+chevron row, destructive row — pitch, type, hairline between cells
G12Segmented control (form)settings and wizards (list spec: "remains a form control")PillNav pill variant: track $surfaceTertiary r20 pad 4, sliding $primary indicator, scrolls past 4 items (ui:pill-nav.tsx:51,76-81,68); used as a form control in match-rules-editor (5 instances), create-session-step4, create-tournament-modal, practice-log, match-list-controls-sheet, records-screengeometry and ink without brand fill; keep or fold into Choice
G13Count in a Tag클럽 row "trailing unread count Tag (neutral)"CornerCountBadge 18 circle (ui:corner-count-badge.tsx:31), formatBadgeCount caps at 99+ (ui:badge-visual.ts:27)variable-width count Tag vs the corner badge; 99+ rule
G14Avatar row / group프로필 friends row (40 × 6, gap 8), 세션 상세 rosterAvatarStack overlaps −6, max 8 (ui:avatar-stack.tsx:73,117); participants are a RowList of PersonRow (packages/features/sessions/src/session-detail/sections/participation-section.tsx:146)spaced avatar row; roster 4-up grid with dashed empty slot (new component)
G15Progress in the brand block기록 ELO blockProgressBar height 6, track $primarySubtle/$borderSubtle, fill fillColor (ui:progress-bar.tsx:70,112)optic fill on an α0.2 track needs a track-colour prop
G16Chart behaviour기록 trendSparkLine 120×48, stroke 2, trend colour, pan-scrub marker (ui:charts/SparkLine.shared.ts:54-55, SparkLine.tsx:98)scrub readout, selected marker, empty/one-point state, axis labels (none in spec)
G17Venue / map thumbnailsession and club rowsVenueTile: photo → static map → glyph cascade, aspect-bound to the text column (pres:sessions/venue-tile.tsx:15-92), r1256 r8 fixed tile, map tile inset border, club logo r12 variant
G18Photo strip / viewerfeed, galleryFeedPhotoStrip is local to packages/features/clubs/src/shared/post-row.tsx:200-253 (1 photo 4:3, 2 squares, 3+ squares with +N); no shared full-screen viewer existswhether v2 needs a viewer
G19ComposerDM and commentsfeature-local raw input (…/dm-thread/compose-bar.tsx:66-123)shared composer or leave feature-local
G20Bottom CTA pairwizard confirm, banner actions, destructive confirmsCTA_PAIR_GAP 12, secondary flex 1 + primary flex 2 (ui:bottom-cta-band-visual.ts:28; ui:reveal-stack.tsx:178)v2 says one CTA per detail page; wizards and sheets still need the pair
G21Edge hairline on scrollsticky header, tab row, CTA band, tab barborders are static (ui:app-header.tsx:118, ui:bottom-cta-band.tsx:117, tab bar borderTopWidth: 1)new behaviour: per-shell scroll-offset state; the hysteresis hook useCollapsingTopBar (ui:use-collapsing-top-bar.ts:53-59) is the nearest precedent
G22Period / calendar controls홈 period pickers, dues month selectorPeriodScrubberShell card + feature pickers (ui:period-scrubber-shell.tsx; home day-calendar-picker.tsx etc.)flat restyle of the card
G23Date / time picker wrappercreate-session, venue hours, settingsreact-native-modal-datetime-picker called raw in 6 files, no wrapperone wrapper or leave native
G24Achievements tile / grid프로필 업적AchievementsGalleryScreen + AchievementStateBadge (feature-local)tile size, locked state (TERMINAL_MUTE_OPACITY .55 today)
G25Rank column기록 ranking rowsLeaderboardRow rank 16/24 in a 28 column (pres:leaderboard-row.tsx:115); podiumv2 rank 13/18·600 in a 24 column
G26level Tag기록, 프로필, rosterno 22px numeral circle exists; TierRangeBadge prints dots + numbers (pres:status/tier-range-badge.tsx)circle vs 24 tall box (the two specs and the board disagree, §6)
G27Live indicator경기 rowsSessionLiveBand ribbon on the card + PulseDot (ui:pulse-dot.tsx)danger-solid Tag with pulse dot in the trailing slot (D7); where the pulse sits inside a 24 Tag
G28Coachmark / tooltipfirst-runCoachmarkTooltip (pres:coachmark-tooltip.tsx) with react-native-spotlight-tourskin only
G29TextArea / multilinePhase B lists TextArea as its own controlStyledInput multiline: min-height 120, top-aligned (ui:labeled-field.tsx:459-461); no character counter inside (counter goes in LabeledField labelRight)height, counter placement, auto-grow (the composer grows 40→100: …/compose-bar.tsx:121-122)

4.2 Components the app has that have no v2 counterpart yet ​

Kept in the "keep" bucket only because nothing replaces them; each needs either a v2 skin or an explicit "stays as is" ruling: Stepper, TimeBandSlider/TimeBandField, RangeBandSlider (+ TierRangeSelector), HueSlider, WizardShell, RevealStack, AttachedPanel, AddCard, PickerRow, EditFieldRow, SelectionCard, SelectionCheck, ModalPanel (+ ConfirmSheet), HeaderActionsMenu, PeriodScrubberShell, DayDotsRow, FlagMedallion, CapacityRing/SegmentedDonutChart/TriRingProgress, MiniBarMeter, CourtLinesTile, CelebrationOverlay, CopyableValue, MatchScoreboard (live scorecard), LeaderboardPodium, VenueFinder family, TierInfoSheet family, DaumPostcodeModal, VenueLocationPicker.

4.3 Dead and unused ​

  • Exported but never rendered (zero references outside the definition, comments and the barrel): in packages/ui: SummaryCard, SpecList, SectionEyebrow, ValueSlider, EmptySlotRow, BannerCapacityCluster, DiagonalGradientFill, FormatGlyph, and ParticipationCluster (used only by the dead BannerCapacityCluster); in presentation: ApprovalModeBadge, ClubRoleBadge, ClubSessionCard, HourlyWeatherStrip, MatchFormatScopeSelector, RecapOutcomePanel, VenueHero, VenueTrustRing. All 17 are in the retire list; delete them before the library is redesigned so nobody re-spec's code nothing renders.
  • Tokens with no consumer: staggerDelay/MOTION_STAGGER; MOTION_DURATION fast, standard, entrance, exitStandard, modal, modalExit, liveDot, badgePulse (§2.3).
  • Press variant with no consumer: fab (ui:pressable-visual.ts:49).

5. Counts ​

Method: names and rows as defined in §0; dispositions assigned per row in §1; numbers computed by script from the same data that produced the tables, so they match the rows.

5.1 Totals and dispositions ​

PopulationRowsNameskeepre-specmergeretire
packages/ui exports (§1a)9414259521912
Shared presentation, ≥ 2 consumer groups (§1c)3059391172
Presentation, one consumer group or none (§1d)54542511810
Total178255123743424

The same split without the K that are infrastructure or layout (stacks, Freeze, ErrorBoundary, PerfProfiler, ContentColumn, providers, dev panel, WebView modals): 33 of the 123 keep names are layout, infrastructure, dev-only or platform-WebView pieces with no v2 visual, leaving 90 keep names that still need a token re-point/skin pass (colour, radius, type) even though their role and geometry survive.

5.2 Top 15 by consumer files (decides migration order) ​

#ComponentFilesGroupsv2 dispositionFile
1Text31210re-spec → T-Typeui:text.tsx
2ActionButton1287keepui:action-button.tsx
3Pressable1089keepui:pressable.tsx
4Icon9410re-spec → T-Iconui:icon.tsx
5EmptyState8510re-spec → T-EmptyStateui:empty-state.tsx
6SectionBlock825re-spec → T-SectionHeaderui:section-header.tsx
7Badge737merge → T-Tagui:badge.tsx
8DetailShell678re-spec → T-Pageui:detail-shell.tsx
9SelectionChip565merge → T-Choiceui:selection-chip.tsx
10Card557retireui:card.tsx
11Divider547re-spec → T-Seamui:divider.tsx
12StyledInput486re-spec → F-Fieldui:labeled-field.tsx
13ModalPanel457keep → T-Overlayui:modal-panel.tsx
14QueryErrorState458keep → T-EmptyStatepres:query-error-state.tsx
15OfflineEmptyState458keep → T-EmptyStatepres:offline-empty-state.tsx

Ranks 16–25 for context: RowList 44 · LabeledField 44 · FlatColumn 42 · BottomCtaBand 37 · FeedList 36 · SkeletonCard 35 · ListRow 34 · MetaSegments 29 · QueryBoundary 25 · PersonRow 23.

What the order implies: (1) Text, Icon, Pressable, Button/ActionButton are touched by 94–312 files each, so the type scale, icon sizes and button geometry must land as additive roles/sizes first (a one-shot swap of cardBody 14 → 15 changes 312 files at once); (2) Badge → Tag (73 files, plus the 16 live XxxStatusBadge wrappers that keep their API and swap the inner component, plus 14 names merged in) and SelectionChip → Choice (56 files + DropdownChip 11 + RemovableChip 2 + RangeChip 2) are the two big merges; (3) EmptyState (85) and its three wrappers (45/45/19) are one visual change; (4) shells and seams — SectionBlock 82, DetailShell 67, Divider 54, FlatColumn 42, BottomCtaBand 37 — change together because the rail (16 → 20), section seam (24 + line → 32) and header change together; (5) Card (55) and the card-shaped surfaces (SessionCardFromEntry 5, SkeletonCard 35) retire last, as pages move to rows.

5.3 What merges into what ​

Merge targetNames absorbedNames (consumer files)
T-Tag14Badge 73, MarkerPill 3, EloTierBadge 7, TrustTierBadge 6, TierBadgeChrome 2, TierRangeBadge 2, VenueLink 1, SessionLeadBadge 1, SourceChips 2, ReliabilityPill 1, ClubAlarmBadge 1, ClubJoinabilityBadge 1, ClubRecruitBadge 1, ChallengeStatusBadge 1
T-Choice7ChipLabel 3, SelectionChip 56, RemovableChip 2, DropdownChip 11, RangeChip 2, ChipPickerPanel 0, ChipChevronIcon 0
T-IconButton6ActionIconButton 6, HeaderIconButton 15, InfoHintButton 2, SessionSaveButton 2, SaveButton 2, VenueSaveButton 2
T-Notice3Well 21, Callout 5, Banner 14
T-StatStrip2StatTile 1, StatGrid 1
T-Button (sm, tonal)1ActionPill 2
T-SwitchTrack1TogglePill 9

5.4 Retire list ​

ComponentConsumer filesWhy
Card55v2: "cards on list surfaces leave" (grammar §6). 55 files; migrate to rows/blocks, keep nothing but dashed empty slots.
SummaryCard0Dead: no JSX consumer (mentions are comments).
SpecList0Dead: no consumer anywhere; superseded by FactRow.
DateTile11 file; v2 rows carry no date tile.
ActionIconRow22 files, 1 consumer group; v2 puts actions in the header and CTA band.
SectionEyebrow0Dead: no consumer.
ValueSlider0Dead: no JSX consumer (create-club-step6.tsx:384 mentions it as retired).
EmptySlotRow0Zero JSX consumers repo-wide (ParticipationCluster is referenced only by the dead BannerCapacityCluster).
BannerCapacityCluster0Zero JSX consumers repo-wide (ParticipationCluster is referenced only by the dead BannerCapacityCluster).
ParticipationCluster0Zero JSX consumers repo-wide (ParticipationCluster is referenced only by the dead BannerCapacityCluster).
DiagonalGradientFill0Zero JSX consumers repo-wide (ParticipationCluster is referenced only by the dead BannerCapacityCluster).
FormatGlyph0Zero JSX consumers repo-wide (ParticipationCluster is referenced only by the dead BannerCapacityCluster).
SessionCardFromEntry5Replaced by the 80pt media row. SessionCardV2 has 1 direct consumer (this wrapper); SessionCompactRow (1) and ClubSessionCard (0) die with it.
RecruitmentMeter2Capacity bar leaves rows; N/M becomes the trailing text or a stat.
SessionCompactRow1dies with the card (T-MediaRow)
SessionCardV21T-MediaRow
ClubSessionCard0dead (0 consumers)
ApprovalModeBadge0dead (referenced only in comments)
RecapOutcomePanel0dead (barrel export only)
VenueHero0dead (barrel export only)
HourlyWeatherStrip0dead (0 consumers)
MatchFormatScopeSelector0dead (0 consumers)
VenueTrustRing0dead (0 consumers)
ClubRoleBadge0dead (barrel export only)

6. Conflicts between sources to settle before building ​

The brief's three documents and the code disagree in these places. The grammar (round 2, 2026-10-09) is the newest and is assumed to win; each line states what that would overrule.

#TopicSource ASource BConsequence
C1Tag geometrygrammar §4: 24 tall, pad 0 8, r6, 13/18·600list spec §4: sm 22 / md 26, r8, 12/18; code Badge xs 18 / sm 26 / md 28 (ui:badge.tsx:89-95); board FvTag 24/r6/13/18 (wireframes/flat-v2.jsx:309-337)pick one; level is a 22 circle in the list spec and a 24 box with a 1px edge in the board
C2Choice geometrygrammar §4: 36, pad 0 14, 15/22·500list spec §4: pill 32/40 + 44 slop, 14/500, $text fill + leading check; code 40/32, 12/18, $primary tint (ui:selection-chip.tsx:25,122-123)grammar's 36 sits between the two
C3Brand blockgrammar §4: 350 wide, pad 20, 144 / 158, eyebrow 13/18·600list spec §3: margin 16, pad 16, heights 120 / 146 / 124, eyebrow 14/20·700list spec §3 is stale
C4Row pitchgrammar §4: media 80 (thumb 56), person 64list spec §2: compact 72 (tile 48), discovery 96 (photo 72)grammar wins; spec rows 72/96 are superseded
C5Underline tabs vs group-spread lawgrammar §4: label padding 0 12, underline under the label widthcanon components.md:151: tabs span the rail with equal flex columns; code flex={1} (ui:pill-nav-underline.tsx:57)amend the law or draw the board's tabs as spread columns
C6Section dividergrammar §4b: retired (D9), whitespace + title, one 12pt band for zone changecanon Divider law components.md:139: full-bleed hairline with 24 above/below; code ui:flat-column.tsx:87canon needs the amendment the grammar announces
C7Middle dotsgrammar §5 prints · in row meta (e.g. "서초구 · 멤버 42 · NTRP 3.0–4.0")@twomore/no-middle-dot-join error, zero waivers (components.md:139, MetaSegments)boards must show MetaBreak, or the rule needs a v2 exception
C8FABgrammar §4: the one coloured action on list screenscanon components.md:13: "no FABs"amend the canon
C9Tab-bar inkgrammar §4: neutral, never brandcode tabBarActiveTintColor: primary (mobile:app/(tabs)/_layout.tsx:62)re-point
C10Rail and widthsgrammar §1: rail 20, content 350 at 390code rail 16 everywhere (SEAM.rail), CTA band side 16the rail change touches every shell
C11Shadowsgrammar §4b: no shadow on tab bar / CTA band; migration plan (docs/architecture/flat-grammar-v2-migration-plan.md, "already decided"): no shadowstoast (pres:toast-host.tsx:350) and header menu (ui:header-actions-menu.tsx:71-76, elevation: 8) carry shadows; canon forbids elevation on global primitivesdecided: both lose the shadow; they need a hairline or the ink fill to separate from the page
C12$primarySubtlegrammar §6 / list spec §3: not a surface23 ui files, 11 presentation files, 22 feature files (§1e)each re-spec row above must pick a replacement cue
C13Avatar scalegrammar: 40 / 48 / 56AVATAR_SIZE has no 48; default is 32 (ui:box-sizes.ts:24-40, ui:avatar-bubble.tsx:159)add 48, change the default to 40
C14"Cell row" namegrammar §4: label/value 48 rowcode CellRow = equal-column strip primitive (ui:cell-row.tsx)rename one of them to avoid a wrong merge
C15Brand ink on controlsmigration plan: scheme S2 "Ball & Ink" — ball-coloured actions with ink text, green only as a mark and court tilecode fills primary Button, selected chips, FAB-to-be, tab-bar tint and the web focus ring with $primary green (ui:button.tsx:79, ui:pressable-visual.ts:85, ui:selection-chip.tsx:122-123)every re-spec that says "ink" or "primary" waits on the S2 token names; the focus ring colour is undecided

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