Owner, 2026-10-09: “design all of the components first, including all their interactive behaviours, so that we have shared components used throughout the app.” This is the last of the three component boards. It draws the twelve rows of library section 14 (the components the page audit found missing) and the four things the behaviour addendum changed for every component: how text scales (B6), how things move (B7), what a screen reader hears (B8) and how the chrome sits on the device (B9). Scheme S2 Ball & Ink. The sibling boards are /wireframes/flat-v2-components (chrome, surfaces, rows, labels) and /wireframes/flat-v2-controls (controls, overlays, states, forms).
| Class | Cap | iOS dynamicTypeRamp | Roles | 100% → 130% → 200% (size/line) |
|---|---|---|---|---|
| reading | 2.0 | headline, subheadline, footnote, caption1 | row L1 / L2, cell, input text, button labels lg / md, notice, toast, dialog, day header, section title, block eyebrow and meta | 15/22 → 19.5/28.6 → 30/44 |
| hero | 1.7 | title1 | detail title, identity name | 24/32 → 31.2/41.6 → 40.8/54.4 |
| numeral | 1.5 | largeTitle | block numeral (display 40/48), stat-strip numeral (24/32) | 40/48 → 52/62.4 → 60/72 |
| label | 1.5 | footnote, subheadline | Tag, level numeral, Choice, small button, section action | 13/18 → 16.9/23.4 → 19.5/27 |
| chrome | 1.3 | title3, headline, subheadline, caption2 | tab-root header title (20/28), pushed header title (17/24), scope tab label, FAB label, tab-bar label (11/14), count tag, chart labels | 20/28 → 26/36.4 → 26/36.4 |
| Duration token | ms | Used for |
|---|---|---|
| pressIn | 80 | press dim in |
| pressOut | 150 | press dim out |
| exitFast | 100 | edge hairline, hover, menu out, toast out, Switch track, Choice, FAB label out |
| fast | 150 | skeleton → content, header title, FAB label in, menu in, Notice dismiss, image fade, Switch thumb |
| toggle · modalExit | 200 | chevron, FAB width, Notice height · sheet and dialog exit |
| entrance · reveal | 250 | RevealStack, ExpandReveal, wizard fill, toast in |
| modal · slow | 300 | sheet rise · ProgressBar fill |
| shimmer · pulseDot | 800 · 1600 | loops, 5 s per mount (WCAG 2.2.2) |
| # | Element | Role | Spoken name | Note |
|---|---|---|---|---|
| 1 | Header back | button | 뒤로 | — |
| · | Header title | heading, hidden | aria-hidden until the hero title scrolls under the header (then it shows) | the opacity-zero layer is not enough on iOS: the flag is explicit |
| 2 | Eyebrow | text | 강남 라켓클럽 | — |
| 3 | Hero title | heading, level 1 | 토요일 올림픽공원 복식 | the one visible h1 of the screen |
| 4 | Fact: when | text | 9월 19일 토요일 오전 9시부터 11시까지 | spoken form of 9월 19일 (토) 09:00–11:00 |
| 5 | Fact: where | text | 올림픽공원 테니스장 | opens the map as a link when it is one |
| 6 | Fact: fee | text | 참가비 10,000원 | — |
| 7 | Section 참석 | heading, level 2 | 참석, 8명 중 6명, 2자리 남음, 예비 2명 | the group head carries the count (spotsLeft); empty slots are hidden |
| 8 – 11 | Roster cells | button | 김민준, NTRP 3.5 · 이서연, NTRP 4.0 · 박지훈, NTRP 3.5 · 최유나, NTRP 3.0 | one stop per cell; the level circle and the avatar are hidden |
| 12 | Section 정보 | heading, level 2 | 정보 | — |
| 13 | Cell: 종목 | text group | 종목, 복식 | label then value |
| 14 | Cell: 대상 | text group | 대상, NTRP 3.0부터 4.0 | the range is spoken, not read as symbols |
| 15 | Cell: 호스트 | text group | 호스트, 김민준 | — |
| 16 | CTA helper | text | 참가비 10,000원, 시작 24시간 전까지 무료 취소 | a text stop before the button; the MetaBreak is never spoken |
| 17 | CTA button | button | 참석하기 | the visible label (WCAG 2.5.3); aria-busy while loading, the name does not change |
| Element | Position (B9.6) | Notes |
|---|---|---|
| Tab bar | height 56 + insets.bottom; the canvas fill continues under the system bar | publishes 56 + inset to the chrome store; today's extra +8 goes ((tabs)/_layout.tsx:47,75-76) |
| CTA band | 16 + 52 + 16 + insets.bottom; the hairline only while content lies below | screens without a tab bar; the code today pads max(16, inset) below, v2 adds the inset |
| FAB | right 20; bottom = tab-bar top + 16 on tab screens, insets.bottom + 16 elsewhere | one per screen |
| Toast | centred; bottom = max(top of the bottom chrome, top of the keyboard) + 16; above the FAB when present: FAB top + 16 | TOAST_CHROME_GAP 8 becomes 16 (library K1) |
| Sheet | padding bottom 20 + insets.bottom | existing (ModalPanel); add navigationBarTranslucent |
| Header | the status inset on top of the 56 | topInset |
| component | mark | spec (pt) | measured | instances | library row · result |
|---|---|---|---|---|---|
| HeaderTextAction target | hta | 44 | 44 | 19 | §14 · pass |
| Header (tab root, pushed, select mode) | header | 56 | 56 | 46 | §3 · B6: 56 at every scale · pass |
| ActionSheet at its 80% cap | as-sheet | 675.2 | 675.2 | 1 | §14 (0.8 × 844) · pass |
| ActionSheet Cell | as-cell | 48 | 48 | 41 | §14 · pass |
| ActionSheet group title line | as-gtitle | 18 | 18 | 14 | §14 (13/18) · pass |
| CTA band, no helper | cta-h0 | 118 | 118 | 1 | §3 (16 + 52 + 16 + 34) · pass |
| CTA band, one-line helper | cta-h1 | 144 | 144 | 9 | §14 · pass |
| CTA band, two-line helper | cta-h2 | 162 | 162 | 1 | §14 · pass |
| MediaBand photo (and its skeleton, default cover) | mb-photo | 240 | 240 | 13 | §14 · pass |
| MediaBand top scrim | mb-scrim | 100 | 100 | 18 | §14 · pass |
| MediaBand overlaid header + status | mb-header | 100 | 100 | 17 | §14 (44 + 56) · pass |
| MediaBand glyph circle | photo-circle | 36 | 36 | 44 | §14 · pass |
| MediaBand map | mb-map | 240 | 240 | 7 | §19.2 · pass |
| PostRow author line | post-author | 44 | 44 | 13 | §14 · pass |
| PostRow one photo (350 wide) | post-photo-1 | 262 | 262 | 3 | §14 (4:3) · pass |
| PostRow two photos (171 wide) | post-photo-2 | 171 | 171 | 6 | §14 · pass |
| PostRow action row | post-actions | 44 | 44 | 11 | §14 · pass |
| PostRow deleted | post-deleted | 48 | 48 | 1 | §14 (a 48 row) · pass |
| DM avatar | avatar-32 | 32 | 32 | 11 | §13 C13 · pass |
| date / system line | thread-line | 50 | 50 | 6 | §14 (16 + 18 + 16) · pass |
| send target | send | 44 | 44 | 8 | §14 · pass |
| send circle | send-circle | 36 | 36 | 8 | §14 · pass |
| Composer (min 56 + inset) | composer | min 56 + inset | 56 / 90 / 124 / 168 | 8 | §14: 56 / 90 / 124 / 168 · pass |
| Calendar month header | cal-head | 44 | 44 | 5 | §14 · pass |
| Calendar weekday row | cal-wd | 32 | 32 | 14 | §14 · pass |
| Calendar day cell | cal-cell | 44 | 44 | 205 | §14 (50 × 44) · pass |
| Calendar numeral circle | cal-num | 36 | 36 | 226 | §14 · pass |
| Calendar activity dot | cal-dot | 4 | 4 | 54 | §14 · pass |
| Calendar week strip | cal-week | 64 | 64 | 3 | §14 · pass |
| Podium (a floor) | podium | 168 | 168 | 9 | §14 · pass |
| Podium, a name wrapping in a lower column | podium-2l | 190 | 190 | 1 | §14 + 22 · pass |
| Podium avatar, 1위 | pod-av1 | 56 | 56 | 9 | §14 · pass |
| Podium avatar, 2위 / 3위 | pod-av2 | 48 | 48 | 18 | §14 · pass |
| ScoreEntry input | score-in | 48 | 48 | 52 | §14 · pass |
| ScoreEntry tiebreak field | score-tb | 36 | 36 | 6 | §14 · pass |
| LiveScore block (a floor) | live-block | 144 | 144 / 156 | 12 | §14 · pass |
| LiveScore team row (a floor) | live-row | 32 | 32 / 44 | 22 | §14 · pass |
| LiveScore serving dot | live-dot | 6 | 6 | 11 | §14 · pass |
| Select-mode row | sel-row | 68 | 68 | 40 | §14 + §15 · pass |
| Select-mode checkbox | checkbox | 24 | 24 | 34 | §14 · pass |
| CountdownNotice, running | cd-notice-2 | 68 | 68 | 6 | §14 · pass |
| CountdownNotice, expired | cd-notice-1 | 48 | 48 | 8 | §14 · pass |
| PersonRow (shared) | row-person | 68 | 68 | 5 | §5 + §15 · pass |
| MediaRow (shared) | row-media | 80 | 80 | 57 | §5 · pass |
| Cell (shared) | cell | 48 | 48 | 24 | §5 · pass |
| Tab bar items | tabbar | 56 | 56 | 11 | §3 · pass |
| FAB | fab | 48 | 48 | 11 | §3 · pass |
| Level tag | level | 24 | 24 | 35 | §6 · pass |
| Tag sm | tag | 24 | 24 | 31 | §6 · pass |
| Choice | choice | 36 | 36 | 38 | §7 · pass |
| component | 100% | 130% | 150% | 200% | note |
|---|---|---|---|---|---|
| MediaRow | 80 → 80 | 87.8 → 87.8 | 138 → 138 | 161 → 161 | |
| PersonRow | 68 → 68 | 80 → 80 | 125 → 125 | 156 → 156 | |
| Cell | 48 → 48 | 54.6 → 54.6 | 96 → 96 | 118 → 118 | |
| Tag sm | 24 → 24 | 29.4 → 29.4 | 33 → 33 | 33 → 33 | |
| Tag md | 28 → 28 | 34.6 → 34.6 | 39 → 39 | 39 → 39 | |
| Level tag | 24 → 24 | 32 → 32 | 36 → 36 | 36 → 36 | |
| Button lg | 52 → 52 | 59.2 → 59.2 | 64 → 64 | 76 → 76 | |
| Button md | 44 → 44 | 50.6 → 50.6 | 55 → 55 | 66 → 66 | |
| Button sm | 36 → 36 | 42.6 → 42.6 | 47 → 47 | 47 → 47 | |
| Block home | 144 → 144 | 170.4 → 170.4 | 188 → 188 | 208 → 208 | |
| Header | 56 → 56 | 56 → 56 | 56 → 56 | 56 → 56 | |
| Tab bar | 90 → 90 | 90 → 90 | 90 → 90 | 90 → 90 | |
| Dialog | derived → 166 | derived → 215.6 | derived → 306 | derived → 364 | not in the table: derived from the formulas |
| Strip | 80 → 80 | 93 → 93 | 103 → 103 | 112 → 112 | |
| Roster cell | 98 → 98 | 111.4 → 111.4 | 119 → 119 | 128 → 128 | the cell; 2-up from 150% |
| Notice, one line | 48 → 48 | 52.6 → 52.6 | 57 → 57 | 68 → 68 | |
| MediaRow, two-line venue | – | – | – | 205 → 205 | L2 wraps to two lines |
| Block home, two-line meta | – | – | – | 252 → 252 | meta wraps to two lines |
| PersonRow, long names | – | – | – | 10 syllables → 156 (one line, 156) · 12 syllables → 200 (two lines) | wraps, no ellipsis |
| frame | spec | bottom chrome | FAB above tab bar | FAB above band | toast above FAB | toast above band | FAB right | status | header |
|---|---|---|---|---|---|---|---|---|---|
| iPhone · tab screen | tab bar 56 + 34 | 90 | 16 | – | 16 | – | 20 | 44 | 56 |
| iPhone · CTA screen | band 16 + 52 + 16 + 34 | 118 | – | – | – | 16 | – | 44 | 56 |
| Android gesture · tab screen | tab bar 56 + 24 | 80 | 16 | – | 16 | – | 20 | 24 | 56 |
| Android gesture · CTA screen | band 16 + 52 + 16 + 24 | 108 | – | – | – | 16 | – | 24 | 56 |
| Android 3-button · tab screen | tab bar 56 + 48 | 104 | 16 | – | 16 | – | 20 | 24 | 56 |
| Android 3-button · CTA screen | band 16 + 52 + 16 + 48 | 132 | – | – | – | 16 | – | 24 | 56 |
| iPhone · all layers | tab bar 56 + 34 under a 84 band | 90 + 84 | – | 16 | 16 | – | 20 | 44 | 56 |
| Android gesture · all layers | tab bar 56 + 24 under a 84 band | 80 + 84 | – | 16 | 16 | – | 20 | 24 | 56 |
| Android 3-button · all layers | tab bar 56 + 48 under a 84 band | 104 + 84 | – | 16 | 16 | – | 20 | 24 | 56 |
| Where | What the spec says | What the board draws, and why |
|---|---|---|
| §14 count | The section says "eleven things" and lists twelve rows (ScoreEntry and LiveScore are one gap of the IA audit, two rows of the table). | All twelve rows are drawn, each as a block. |
| Fv* and FV_PAL_S2 | The brief names Fv* primitives and FV_PAL_S2 in the two sibling boards. | Those names live in flat-v2.jsx; the sibling boards carry copies (Fq* in components, Fk* in controls). This board copies from the siblings (Fp*) and reads the palette of FQ_LIGHT / FK_LIGHT, which equal FV_PAL_S2. |
| HeaderTextAction count | "At most one per header" (§14) against the Select-mode header, which has 취소 on the left and 전체 선택 on the right. | Drawn as the Select-mode row says; the rule should read "at most one per side". |
| Text beside an icon | IA-4 listed [관리] [⋯] (text first) while §14 put the icon first and the text last, on the rail. | Resolved by §17 H1 and redrawn: a text action sits left of any icon and the overflow ⋯ is rightmost ([관리] [⋯], [초대 코드] [+]); the rightmost icon takes the rail. |
| Text over a photo | §14 specifies glyph circles over the MediaBand and nothing for a text action. | A proposal: white 600 on an ink α0.4 pill 36 tall (the circles' sibling), marked as proposed. |
| ActionSheet cap | ActionSheet: max 80% of the screen; ModalPanel in the code caps at 88%. Nine actions in three groups plus a destructive group are 866 tall, the 80% sheet 675.2. | Drawn at 80%; the body scrolls and 위험 구역 sits below the first screen (shown scrolled to the end). The sheet's own padding (20 + inset = 54) stays under the scroll body. |
| CTA band bottom | The code pads max(16, inset) below and keeps a permanent top border; §3 and B9.6 say 16 + 52 + 16 + inset with the hairline on scroll only. | Drawn as §3 / B9.6: 118, 144 and 162 at inset 34; the change from the code is stated in the helper block. |
| MediaBand map | §14 drew a 160 band under a white header with a 지도 Tag that opened the map app; §19.2 (MAP-BAR-INK, MAP-NO-CHIP) gives it the photo geometry, no Tag, a tap to 장소 상세 and the pin at y 170. | Redrawn as §19.2: 240 full-bleed with the scrim and glyph circles, no Tag. Press = dim .85 without a settle and the focus ring inset 2 stay proposals (§16). |
| PostRow clamp and photos | §14: 4-line clamp + 더보기, three or more photos = two squares with +N over the second. The code clamps at 5 and shows three squares. | 더보기 is drawn inline at the end of the 4th line (the code's trailing slot, a passive hint); the two-square layout as §14. |
| MessageBubble extras | §14 gives rest only: the time "beside the last bubble", no sending or failed face, no keyboard height. | The time sits bottom-aligned on the inner side; sending (.6, 보내는 중) and failed (danger glyph, 다시 보내기) are drawn as proposals; the keyboard is a 336 stand-in block. |
| Calendar colours | §14 colours only the weekday row (일 danger, 토 $series1). | Numerals stay ink; the activity dots are ink; the week strip carries no dots (the 64 has no room); the range band is flat at the week break. |
| Podium height | 168 holds only if the lower columns have one-line names: 16 + 48 + 4 + 18 + 4 + 22 + 4 + 24 + 4 + 24. | Drawn as a floor: 168 with one-line names, 190 with a wrapped name in a lower column (182 in the raised 1위 column). |
| ScoreEntry unknowns | The text does not say which team a quick pick credits, where the tiebreak field sits, nor the label of the set columns. | The top team gets the first number (an open call); the tiebreak is a 44 row under the table with the 36 × 36 field at the right; the columns are labelled 1세트 … 3세트. |
| LiveScore composition | §14 gives the 350 × 144 block but not its rows; 144 = 20 + 104 + 20. | A LIVE row (24) + two 32 rows + 8s fill the 104, so the block reads as live; AD is wider than its 32 column and overflows centred; a two-line name makes the block a floor (156). |
| LiveScore vs B7.6 | §14: a 150 ms numeral cross-fade on a point; B7.6: counters and scores update instantly (the AnimatedCount precedent). | An open conflict. Row M23 draws both; the LiveScore block says what §14 says. |
| Select mode title | Header: 취소 · "n명 선택됨" · 전체 선택; nothing for when every row is ticked; the viewer's own row and the total come from the IA audit. | The title is centred between the two actions; 전체 선택 keeps its label; the own row (disabled, caption) and the 합계 helper are drawn from the IA text. |
| CountdownNotice last minute | Only the screen-reader milestones (5:00, 1:00) are specified. | No visual change in the last minute; the clock keeps ticking in place. |
| B6 · ELO Block | The addendum's table gives ELO 158 / 181.2 / 198 / 216 (the old 6 pt progress); the library ruled 160 with an 8 pt progress. | ELO is not in the strip (home Block only); by the library's geometry it would be 160 / 186.4 / 204 / 224. |
| B6 · long names | MediaRow and PersonRow "with a 10-syllable name (wraps)". | Pretendard's Hangul advance is about 0.87 em, so ten syllables (260) still fit the 298 text column at 200%; the strip shows that frame and a 12-syllable name that wraps. React Native on a device may wrap the ten-syllable one: unverified. |
| B6 · MetaBreak | MetaSegments is a wrapping flex row with a break before each fact. | At 130% and beyond the break can dangle at the end of the first line (the real component does the same); a fix needs a layout pass: noted, not drawn. |
| B7 · spring numbers | B7.2 prints settle times of 247 / 377 / 311 ms and overshoots of 9.5 / 14.8 / 3.2% for pressIn, pressOut and navSlide. | Those are printed as written; the middle frames use the closed-form damped oscillator, which settles 248 / 379 / 313 ms with overshoots of 9.6 / 15.0 / 3.3% (a 1 to 2 ms difference of integration, no visible effect). |
| B7 · board checks | B7 asks for looping demos, a reduce-motion switch and the token tables printed from motion.ts by importing it. | Not drawn: this is a static render. Every motion is a frozen frame sequence with computed middle values; the tokens are typed from the addendum. |
| B8 · speech | Korean pronunciation of the spoken forms and how a Korean voice reads 오전 9시 or 8명 중 7명 참석 are unverified until a device run; axe-core is not run on a static board. | The Korean names are the addendum's patterns; stops, order and headings are drawn, not tested. |
| B9 · insets | Insets are read at runtime; the 3-button bar is "typically taller" (unverified). | Nominal values drawn: 34 (iPhone), 24 (Android gesture), 48 (3-button); status 44 (47 on a 390 pt iPhone). The tab bar and the CTA band never share a screen, so each platform has a tab frame and a CTA frame. |
Proposal (2026-10-09), Phase B of the flat grammar v2 migration, the third component board: the twelve page components of docs/design/flat-v2-component-library-2026-10-09.md section 14 (HeaderTextAction, ActionSheet, CTA band helper line, MediaBand, PostRow, MessageBubble + Composer, Calendar, Podium, ScoreEntry, LiveScore, Select mode, CountdownNotice) each drawn as a specimen at 1:1 with anatomy rulers, states, variants, behaviour and used by / replaces, then the four behaviours of docs/design/flat-v2-behaviour-addendum-2026-10-09.md: text scaling at 100 / 130 / 150 / 200% (B6), a frame sequence per motion with its curve and the reduced-motion column (B7), the focus order, spoken names and headings (B8) and the chrome stack on iPhone, Android gesture and 3-button navigation in light and dark (B9), then a DOM verification and every judgement call. Built from local primitives only (Fp*): no WFPhone, WFCard, WFChip, WFBadge, WFSelectChip, WFInput or WFSwitch.
packages/ui/src/modal-panel.tsx · header-actions-menu.tsx · bottom-cta-band.tsx · text.tsx · motion.ts · apps/mobile/app/(tabs)/_layout.tsx · packages/features/clubs/src/shared/post-row.tsx · packages/features/messaging/src/dm-thread/compose-bar.tsx · packages/app/src/presentation/components/leaderboard-row.tsx · leaderboard-podium.tsx · activity-metric-calendar.tsx · packages/features/sessions/src/scorecard/score-edit-sheet.tsx · match-card.tsx
grounded in 13 source file(s)
packages/ui/src/modal-panel.tsxpackages/ui/src/header-menu.tsxpackages/ui/src/bottom-cta-band.tsxpackages/ui/src/text.tsxpackages/ui/src/motion.tsapps/mobile/app/(tabs)/_layout.tsxpackages/features/clubs/src/shared/post-row.tsxpackages/features/messaging/src/dm-thread/compose-bar.tsxpackages/app/src/presentation/components/leaderboard-row.tsxpackages/app/src/presentation/components/leaderboard-podium.tsxpackages/features/records/src/record-detail/period-calendar.tsxpackages/features/sessions/src/scorecard-page/overlays/score-entry-sheet.tsxpackages/features/sessions/src/scorecard-page/schedule-match-row.tsx