Flat v2 component library — every shared component, with its behaviours (2026-10-09)
Status: Archived · Superseded on 2026-10-11 by the consolidated v2.1 edition at docs/design/flat-v2-component-library-2026-10-09.md; kept as the record of how each value was reached.
0. Rulings on the fifteen conflicts the inventory found
| # | Conflict | Ruling |
|---|---|---|
| C1 | Tag geometry (grammar 24/r6/13 vs list spec 22–26/r8/12 vs code 18–28) | Tag sm 24 tall, r6, 13/18·600, pad 0 8 (default); md 28, r8, 15/22·600, pad 0 10 (hero and identity only); level = 24 circle, 13/18·700 numeral; count = 20 tall, min-width 20, r full, 11/14·700. |
| C2 | Choice geometry (36 vs 32/40 vs code 40/32) | Choice 36 tall, r full, 15/22·500, pad 0 16 on list and settings surfaces; 44 (pad 0 20) inside wizard forms. Hit slop to 44 everywhere. |
| C3 | Brand block (grammar vs list spec §3) | Grammar: 350 wide, pad 20, radius 16; home 144, ELO 160, identity 144; eyebrow 13/18·600. List spec §3 is superseded. |
| C4 | Row pitch (80/64 vs 72/96) | Grammar: media 80 (thumb 56), person 64 (avatar 40), cell 48. The list spec's 72/96 rows are superseded. |
| C5 | Underline tabs vs the group-spread law | Tabs are content-width labels, left-aligned from the rail, gap 24 (Toss, 카카오뱅크, 배민). The group-spread law is amended: it governs equal-column strips (Strip, action-icon rows, CTA pairs), not scope tabs. |
| C6 | Section divider | Divider law amended per grammar §4b: whitespace + title; the band for a change of kind; cell hairline inside single-line cell groups; edge hairline only under scrolled content. |
| C7 | Middle dots | The no-middle-dot-join rule stands: in-app meta uses MetaSegments with the 1px MetaBreak; boards draw the break, never ·. |
| C8 | FAB | Canon "no FABs" is retired: one extended FAB per list screen is the screen's single coloured action (홈 + 일정, 경기 + 번개, 클럽 none). |
| C9 | Tab-bar tint | Ink. The tab bar never carries brand colour; the only colour on it is the 7px danger dot / count tag. |
| C10 | Rail | 20 (SEAM.rail → 20); content 350 at 390. |
| C11 | Shadows | None anywhere. Toast = ink surface on white (no shadow needed); header menu = white panel with a 1px #D4D4D4 edge; coachmark = ink surface. |
| C12 | $primarySubtle as a surface | Leaves. Selected row = trailing check glyph + 600 label (no fill); selected Choice = ink fill; selection in pickers = ink check; notices = weak tone fills from the Tag table; empty-state disc = gray100; progress tracks = #E5E5E5. |
| C13 | Avatar scale | 40 (rows, default) · 48 (roster) · 56 (identity); 32 only inside Tag-sized contexts (DM thread avatars). |
| C14 | "Cell row" naming | Code CellRow (equal columns) is renamed Strip; the new 48 label/value row is Cell. |
| C15 | Brand ink on controls | Ball #D6F04A with ink text only on the primary Button, the FAB and the CTA; every control state is ink or grey; the focus ring is 2px ink (#0D100E), offset 2. The brand green #006D37 survives as the mark and the court-grass tile. |
1. Foundations the components read (hand-off to Phase A)
Colour (S2): canvas #FFFFFF · ink #0D100E · secondary #4F5851 · tertiary #626C64 (≥ 16px only) · gray100 #F2F2F2 (neutral fill, band, tonal buttons, empty-state disc) · cell hairline #E5E5E5 · edge hairline #D4D4D4 · ball #D6F04A (primary action, level, block numeral) · block #15181A · brand mark #006D37 · brand-weak #EFF9B7 (owner to confirm) · success #00796A / text #014B40 / weak #CBEBE5 · warning fill #F5B301 / text #6B4A00 · danger #B8332E · info #4B0082 / weak #E8DCFB · court hard #089AC3 clay #8D3D00 grass #83971F · tiers and series per the ledger. Dark: canvas #0F1211, surface #1A201B, block #232824 (ball numeral), ink #DFE4DC, secondary #A9B3AB, cell hairline white α0.09, edge hairline white α0.14, band #000000, ball unchanged with ink text, success #75D1C6, warning #E1B75C/#E2B13C, danger #FA9B94.
Type: display 40/48·700 · hero 24/32·700 · title 20/28·700 · headline 17/24·600 · body 15/22·400/500 · footnote 13/18·500/600 · caption 12/16·500 (tab-bar labels 11/14·500/600). Spacing 4/8/12/16/20/24/32/40 with the §3 roles. Radii 6 tag · 8 thumb · 12 button, logo, notice, menu, toast · 16 block, sheet · full pill, avatar, choice. Icons 24 actions · 20 hero facts and inline · 16 chevrons; lucide stroke 2 in the label's colour. Touch target 44 (hit slop = (44 − visual) / 2).
2. Behaviours (carried from code unless marked new)
| Behaviour | Value | Source |
|---|---|---|
| Press settle + dim | scale/opacity per variant: button .97/.92 · pill .96/.85 · row .98/.70 → row .98/.80 (v2: the 0.70 dim on an 80pt row reads as a flash; peers dim rows 0.8–0.9) · icon .92/.78 · fab .96/.90; spring in {20, 400, .7}, out {15, 300, .7}; dim 80 ms in / 150 ms out; UI thread | ui/pressable-visual.ts:43-50, ui/motion.ts:29-44 |
| Hover (web, pointer) | opacity .85 + cursor: pointer | ui/pressable.tsx:311-324 |
| Focus (web) | 2px ink ring, offset 2 (was $primary) | ui/pressable-visual.ts:84-89 |
| Disabled / loading | opacity .5, not-allowed, inert; aria-disabled / aria-busy | ui/pressable.tsx:99,164 |
| Reduced motion | drop the settle, keep the dim; shimmer pins .75 | ui/pressable.tsx:286-299 |
| Selection semantics | radio/checkbox/switch → aria-checked; tab/option/row → aria-selected; button → aria-pressed; arrow keys move radio groups on web | ui/selection-accessibility.ts |
| Hit slop | (44 − visual) / 2 | ui/box-sizes.ts:74 |
| Toast | one at a time; 2800 ms; above tab bar and keyboard; swipe-down > 40px or 500px/s dismisses; timer pauses on touch | pres/toast-host.tsx |
| ModalPanel | RN Modal, fade 150 ms; backdrop ink α0.4; tap-outside dismisses (confirm dialogs do not); bottom sheets slide 200 ms quick, no grabber, no drag | ui/modal-panel.tsx |
| Skeleton | opacity .6 ↔ .9, 800 ms half-cycle; shapes at the v2 pitches (§5) | ui/shimmer.ts |
| Pull-to-refresh | native control, spinner only for a user pull, tint ink | usePullToRefresh |
| Haptics | mutation outcomes only, never primitives | unchanged |
| Edge hairline on scroll (new) | header, tab row, tab bar and CTA band draw #D4D4D4 when content passes under them: useScrollEdge(offsetY > 8) with 8px hysteresis, 100 ms fade | grammar §4b |
| FAB on scroll (new) | extended pill at rest; after 120px of downward scroll collapses to a 48 circle (icon only) in 150 ms; expands on upward scroll or at top; hides behind the keyboard | Material extended FAB |
| Keyboard | inputs scroll into view above the keyboard (react-native-keyboard-controller, per-screen, never global pan→resize) | memory: keyboard-controller adoption |
3. Chrome
| Component | Geometry | States | Replaces |
|---|---|---|---|
| Header | 56; title 20/28·700 at the rail (left); back = 24 arrow in a 44 target at the rail; pushed screens: back at the rail + title 17/24·600 left-aligned at x 56; hero pages: no title at rest, the hero title fades in (150 ms) when it scrolls under the header (§13 L1); ≤ 2 action icons 24 in 44 targets, gap 8, right; no divider at rest; edge hairline when scrolled; optional subtitle 13/18 secondary below the title (height 64) | icon pressed .92/.78; title truncates with ellipsis at 1 line | AppHeader (centered 17 title + permanent hairline), MainTabShell title block (24/32) |
| Tabs (scope) | row 44; labels 15/22, active 600 ink + 2pt underline the label width, inactive 500 secondary; left-aligned from the rail, gap 24; horizontally scrollable past 4; role="tablist"; 24 below to a title or day header, 16 to a row | pressed dim .85; focus ring; underline slides with navSlide spring | PillNav (both variants on list surfaces), SegmentedTabs |
| Tab bar | 56 + home inset 34; 5 items; icon 24 stroke 2, label 11/14; active = filled glyph + ink + 600, inactive = outline + secondary + 500; no top line at rest, edge hairline while content lies below; badge = 7px danger dot (ring 1.5 canvas) or count tag 16 tall | pressed .92/.78 on the item; aria-selected; reduced motion no settle | inline tab bar in (tabs)/_layout.tsx (brand tint, permanent border) |
| FAB | extended pill 48; pad 0 20; icon 20 + 8 + label 15/22·600; ball fill, ink; 20 from the right rail, 16 above the tab bar; collapses to a 48 circle on scroll (§2) | pressed .96/.90; focus ring ink; disabled .5; one per screen | none (new); variant="fab" physics exists |
| CTA band | button 52 inside 16 top/bottom + 20 sides + home inset 34; one primary; a secondary tonal appears only in wizards and confirms (pair: tonal flex 1 + primary flex 2, gap 12); edge hairline while content lies below | — | BottomCtaBand (16 sides, permanent hairline) |
| Day header | 32; date 15/22·600 ink left, relative day 13/18·500 secondary right; 16 above; today dot = 6px ball with a 1px ink edge hung in the gutter (x 8–14) so the date stays on the rail; "오늘" in ink 600 (§13 L2) | — | SessionDayHeader |
| Section head | 24; title 17/24·600; action 13/18·500 secondary + chevron 16 right; 32 above, 8 below (edge rule trims the first row's top padding) | action pressed .85; action target 44 | SectionHeader (navTitle 17/22·700 + divider) |
| Band | 12 tall, full-bleed #F2F2F2, 24 above and below; at most one per screen | — | Divider seam="section" (retired), tinted bands |
4. Surfaces
| Component | Geometry | States | Replaces |
|---|---|---|---|
| Block | 350 × 144 (home, identity) / 160 (ELO); radius 16; pad 20; ink #15181A; eyebrow 13/18·600 white α0.8 + chevron 16 α0.8 at the row end; numeral 40/48·700 ball with unit 15/22·600 white on the baseline; meta 15/22·500 white α0.85; ELO adds progress 8 (ball on white α0.2) + caption; identity: avatar 56, name 24/32·700 white, level tag, sub 15/22 α0.85, edit icon 24 ghost-inverse top-right; whole block pressable | pressed .98/.90 (the code's card variant; no new physics entry); focus ring: the standard 2px ink ring, offset 2, outside the block (ink on white 19:1; one focus recipe app-wide); loading = skeleton block; empty = eyebrow + "일정 없음" 24/32 | HeroCard, SummaryCard, StatGrid hero, profile-hero-card |
Strip (was CellRow) | 80; equal columns; numeral 24/32·700 ink; label 13/18·500 secondary; 1 × 24 #E5E5E5 between columns; pad 12 inside | pressed per column (.98/.80) when linked | StatStrip, CellRow, ActionIconRow (icons 24 + 12/16 labels) |
| Hero (detail) | top 24; eyebrow 13/18·500 secondary; 4; title 24/32·700; 16; three facts 17/24·500 with a 20 glyph (calendar, map-pin, card) + 8, gap 8; 32 below | — | TypeHero, SessionFactsSection lead, VenueHero (retire) |
| Chart | 160; inset 16; one ink line 2; no fill; scrub: 1px #D4D4D4 guide + 8px ink marker + readout count tag (ink) above; empty = 48 glyph + 15/22 secondary centred | pan-scrub; reduced motion = no marker animation | SparkLine (120 × 48) |
| Thumb / map tile | 56 × 56 r8; venue photo → static map → court tile cascade; map and tile carry a 1px inset #E5E5E5; club logo 56 r12 | loading = skeleton square | VenueTile (aspect-bound r12), ClubAvatar |
| Notice | inline; r12; pad 12 16; weak tone fill (success #CBEBE5, warning #F5B301, danger #B8332E with white, info #E8DCFB, neutral gray100); icon 20 + 12; title 15/22·600 + body 15/22·400; optional text action 15/22·600 right; optional close 24 in 44; never a surface of its own on a hub | action pressed .85; dismiss animates height 150 ms | Banner, Callout, Well, NetworkBanner (all $primarySubtle) |
| Avatar | circle; 40 / 48 / 56; initials 15/22·600 (40), 17/24·600 (48), 20/28·700 (56) ink on gray100; photo when present; club logo r12 | — | AvatarBubble (default 32, mint), ClubLogoBubble |
| Avatar row | 40 × n, gap 8, from the rail; "+N" as a 40 gray100 circle 13/18·600 | row pressable .98/.80 | AvatarStack (overlap −6) |
5. Rows
| Component | Geometry | States | Replaces |
|---|---|---|---|
| MediaRow | 80; pad 12; thumb 56 r8 + 12; L1 17/24·600 ink (time, tabular); L2 15/22·400 secondary (venue, MetaSegments); trailing: count 15/22·600 ink or one Tag, centred on L1; no divider | pressed .98/.80; selected = trailing check 16 + L1 600 (no fill); loading skeleton: 56 square + 17/24 line at 60% + 15/22 line at 40% | SessionCardV2, SessionCompactRow, ClubCard, DirectoryVenueCard on list surfaces |
| PersonRow | 68 (§15); avatar 40 + 12; L1 15/22·600; L2 13/18·500 secondary; trailing: amount/score 15/22·700 tabular, ELO + level tag cluster, or one Tag; rank variant: 13/18·600 secondary rank in a 24 column before the avatar | pressed .98/.80; selected check; disabled .5 | PersonRow, MemberRow, DuesRow, LeaderboardRow, RankRow |
| Cell | 48; label 15/22·400 secondary left; value 15/22·500 ink right; variants: nav (chevron 16), toggle (Switch), value + chevron, destructive (label danger), input (value editable); cell hairline #E5E5E5 rail-to-rail between cells of one group, none after the last | pressed .98/.80 (nav/value); toggle presses the switch | FactRow, ListRow (34 files), SettingsRow, AdminNavRow, PickerRow |
| Roster cell | 100 pitch, 4-up; avatar 48; name 13/18·500 (20 row); level tag 24; empty slot = 48 dashed #D4D4D4 circle | cell pressable (opens profile) | ParticipationSection rows |
| ResultRow | PersonRow with opponents in L1, date L2, score 17/24·700 tabular + 승/패 Tag (success weak / neutral weak) | — | completed-row, match rows |
6. Labels
| Component | Geometry | Tones / tiers | Replaces |
|---|---|---|---|
| Tag | sm 24 r6 13/18·600 pad 0 8; md 28 r8 15/22·600 pad 0 10; passive (no press, no aria-pressed); ≤ 1 per row, ≤ 2 per object; label 1–2 words | neutral (gray100 / secondary · solid #4F5851 / white), brand-weak (#EFF9B7 / ink), success (#CBEBE5 / #014B40 · #00796A / white), warning (#F5B301 / ink), danger (#B8332E / white), info (#E8DCFB / #4B0082 · #4B0082 / white) | Badge (73 files), hand-drawn pills (9), ContextChip, MarkerPill, ReliabilityPill, TierRangeBadge in rows |
| Level tag | 24 circle; ball fill; 13/18·700 ink numeral; 1px #D4D4D4 edge on white | — | TierRangeBadge, EloTierBadge chrome |
| Count tag | 20 tall, min 20, r full, 11/14·700; danger solid (unread) or neutral; 99+ cap | — | CornerCountBadge |
| Live tag | danger solid Tag with a 6px white pulse dot (1600 ms) before "LIVE" | reduced motion: static dot | SessionLiveBand on rows, PulseDot |
7. Controls
| Component | Geometry | States | Replaces |
|---|---|---|---|
| Button | tones primary (ball fill, ink label), tonal (gray100, ink), ghost (text only, pad 0 12), danger (#B8332E, white); sizes for every tone: sm 36 r10 15/22·600 · md 44 r12 15/22·600 · lg 52 r12 17/24·600 — lg for the CTA band and full-width submits, md for inline actions (empty states, notices, sheet content), sm inside rows and notices (§13 K3); full-width on the rail or content-width (pad 0 20) | pressed .97/.92; hover .85; focus ring ink (ball button: ink ring); disabled .5; loading = spinner replaces the label at full opacity, width held (ink; white on danger — ink on #B8332E is 3.24:1); icon 20 + 8 leading optional | ActionButton/Button (green, 56), ActionPill, hand-drawn pressable pills |
| IconButton | 44 target, 24 glyph, ghost; tones ink / secondary / danger / inverse (on block) | pressed .92/.78; disabled .5; aria-label required | HeaderIconButton, ActionIconButton, RemoveIconButton, SaveButton |
| Choice | 36 (44 in forms), r full, 15/22·500, pad 0 16/20; resting 1px #D4D4D4 on white; selected ink fill + white 700 label + leading check 16; modes: menu (chevron 16), input (× 16, 44 target), range | pressed .96/.85; disabled .5; aria-checked / arrow keys in radio groups | SelectionChip (56 files), DropdownChip, RangeChip, RemovableChip |
| Segmented (form) | 40; track gray100 r full pad 4; selected = white pill + 1px #D4D4D4 + 600 ink; others 500 secondary; equal columns (group-spread); settings and wizards only | indicator slides navSlide; role="radiogroup" | PillNav pill variant in forms |
| Switch | 48 × 28 track r full; off #D4D4D4, on ink; thumb 24 white with 1px #D4D4D4 edge; 150 ms | role="switch" + aria-checked (fixes the a11y hole); disabled .5; pressed none (toggle is the feedback) | TogglePill, Switch |
| Input | 48; r12; 1px #D4D4D4; pad 0 16; text 15/22 ink; placeholder secondary; label 13/18·600 above (4); helper 13/18 secondary below (4); error: border + helper #B8332E; leading/trailing 20 icons | focus = 2px ink border (no ring); disabled .5; filled state unchanged | StyledInput, LabeledField |
| TextArea | min 120, auto-grows to 200 then scrolls; counter 13/18 secondary bottom-right inside the pad | as Input | StyledInput multiline, composer |
| Search | 44; r full; gray100; icon 20 + 8; 15/22 placeholder secondary; clear × 20 (44 target) when filled; cancel ghost button right when focused; header takeover on list screens (icon in header → field replaces the title) | focus: field stays gray100, caret ink | SearchBar |
| Stepper | 36 pill group: − / value 15/22·600 / +, each 44 target | disabled at bounds .5 | Stepper |
| Slider | track 4 #E5E5E5, fill ink, thumb 24 white 1px edge; live value tag above the thumb; band sliders keep the three grab surfaces | reduced motion: no thumb spring | TimeBandSlider, RangeBandSlider, ValueSlider (retire) |
| Checkbox / radio rows | Cell 48 with a trailing check (single) or a 24 checkbox square r6 ink when on (multi) | aria-checked | SelectionCard lists |
| Date / time picker | bottom ModalPanel with the native wheel; title 17/24·600; CTA 완료 primary | — | raw react-native-modal-datetime-picker (6 files) |
8. Overlays and feedback
| Component | Geometry | Behaviour | Replaces |
|---|---|---|---|
| ModalPanel (sheet) | bottom; top radius 16; pad 20; title 17/24·600 + close 24 right; content; CTA band inside | slide 200 ms; backdrop ink α0.4; tap-outside dismisses; no drag, no grabber | ModalPanel bottom |
| Dialog | centred; width 350; r16; pad 20; title 17/24·600; body 15/22 secondary; CTA pair (tonal + primary, danger for destructive) | fade 150 ms; backdrop does not dismiss | ModalPanel confirm |
| Menu | anchored under the header action; width 240; r12; 1px #D4D4D4 edge; rows 48 (icon 20 + 15/22); destructive last in #B8332E after one cell hairline; ≤ 5 rows | fade 100 ms; no shadow | HeaderActionsMenu (shadowed) |
| Toast | ink surface; r12; min 48; pad 12 16; text 15/22 white; optional action 15/22·600 ball text; 16 above the tab bar / FAB clears it | 2800 ms; swipe-down dismiss; no shadow | ToastHost (shadowed, r24) |
| Coachmark | ink surface r12 with an 8px arrow; body 15/22 white; 다음 ghost ball text | spotlight unchanged | CoachmarkTooltip |
9. States
| Component | Geometry | Replaces |
|---|---|---|
| EmptyState | centred; top 64; disc 80 gray100 with a 32 glyph secondary; title 17/24·600; body 15/22 secondary (≤ 2 lines); one md (44) tonal button (md primary when the empty state is the screen's action); variants empty / error (AlertCircle + 다시 시도) / offline (WifiOff) / not-found | EmptyState (mint disc), QueryErrorState, OfflineEmptyState, NotFoundState |
| Skeleton | shapes at the v2 pitches: media row (56 sq r8 + 17/24 line 60% + 15/22 line 40%), person row (40 circle + two lines), cell (label 40% + value 20%), block 144 r16, strip 80, chart 160, hero (24/32 line 70% + three 17/24 lines) — all gray100, pulse .6 ↔ .9 | Skeleton, SkeletonCard, SkeletonHero |
| Spinner | ActivityIndicator ink, small; inside buttons only | raw indicators (8 files) |
| Offline strip | Notice neutral, 350 × 48 (min; grows with text per B6), pinned under the header, "오프라인이에요" + 다시 시도 (the size every board draws, corrected 2026-10-10) | NetworkBanner |
10. Forms (wizard field grammar in v2)
WizardShell: header 56 with back and skip as real buttons per the owner's wizard-navigation LAW (a 40 gray100 back square and an sm tonal 건너뛰기, §21.2; corrected 2026-10-10 from "ghost text"), a 2pt progress line on the header's last 2pt — ink for done + current, #E5E5E5 for remaining (§13 K6; ball on white is 1.28:1), title 24/32·700 at 24, fields at 32 seams, footer CTA band with one primary (pair only on the confirm step). Field = label 13/18·600 (a required field adds the one $error asterisk, size-matched, per the owner's required-marker LAW, and starts unset) + control + helper; Choice sets wrap left-aligned gap 8; AddRow = 56 dashed #D4D4D4 r12 with + 20 and 15/22·600 (replaces AddCard); AttachedPanel → a Notice (neutral) under its field; PickerRow → Cell value + chevron opening a sheet; RemoveIconButton → IconButton danger; RevealStack keeps its 250 ms reveal; live-draft and sequential reveal unchanged. Validation: inline helper in danger under the field, never a toast.
11. Retire (no v2 counterpart by design)
Card on list surfaces (figure-only use remains for the detail hero image), SessionCardV2/ClubCard boxes, Badge, SelectionChip family, PillNav, SegmentedTabs, Banner/Callout/Well, CornerCountBadge, AvatarStack, SessionLiveBand, ProgressBar (becomes the block's progress and the Slider track), GlyphFactRow chip density, the 17 dead exports listed in the inventory. $primarySubtle, $surfaceTertiary as surfaces.
12. Open for the owner
Brand-weak tint #EFF9B7 for 모집 중 · the figure + level-tag cluster in rank/result rows · the FAB collapse-on-scroll behaviour · the toast action colour (ball text on ink) · whether 클럽's row logo keeps the identity hues at 56 r12.
13. Resolved 2026-10-09 (owner: "go with best practices and your recommendation")
| # | Question the boards raised | Resolution | Why |
|---|---|---|---|
| L1 | Header with back and a title | Tab roots: title 20/28·700 at the rail. Pushed screens: back glyph at the rail, title 17/24·600 left-aligned at x 56, one line, ellipsis. Detail pages with a hero: no title at rest; the hero title fades into the header (150 ms) once it scrolls under it, together with the edge hairline | left-aligned keeps the one text axis of the grammar (Material small top app bar); the scroll-linked title is Apple's large-title pattern and useCollapsingTopBar already provides the hysteresis |
| L2 | Today dot inline or hung | Hung in the gutter (6px at x 8–14), the date stays on the rail; "오늘" in ink 600 | alignment of the text edge beats the dot (hanging punctuation); every day header starts at x 20 |
| L3 | Block focus ring | The standard ink ring, offset 2, outside | one focus recipe; WCAG 2.4.13's change-of-contrast holds (white → ink) |
| L4 | Notice / offline strip min height | 48 | on the grid; equals the Cell |
| L5 | Dark weak tints | Tone at 16% over the dark canvas; dark info text #C9B2F2 — accepted, verified by the dark gate at S1 | derived values ship only behind the contrast gate |
| K1 | Toast offset | 16 above the tab bar, and 16 above the FAB when one is present (code's 8 changes) | matches the FAB's own 16 offset; one seam |
| K2 | Unchecked checkbox | 2px #4F5851 edge (7.4:1) | WCAG 1.4.11 needs 3:1 for a control boundary; the 1px hairline is 1.48:1; M3 uses a 2dp outline |
| K3 | Button sizes | sm 36 · md 44 · lg 52 for every tone; lg only for the CTA band and full-width form submits; md for inline actions (empty states, notices, sheet content); sm inside rows and notices | one size scale instead of size-by-tone |
| K4 | Derived dark values (tonal fill, switch track/thumb, backdrop, toast surface, danger label) | accepted, gated at S1 | as L5 |
| K5 | Toast status glyph | kept (20, before the text) in the dark-theme tone values on the ink surface: success #75D1C6, warning #E1B75C, danger #FA9B94, info #C9B2F2 | a status glyph is the state, not decoration (glyph tiers); light tones fail on ink |
| K6 | Ghost padding; wizard progress line | ghost pad 0 12 (text edge lands on the rail); progress line on the header's last 2pt in ink for done + current, #E5E5E5 for remaining (not ball) | the ball segment is 1.2:1 on white and the line carries information (non-text 3:1) |
| — | Block press physics | the code's card variant .98 / .90 | no new physics entry |
14. Components the pages need (added 2026-10-09 from the IA audit §4.0)
The page audit found eleven things the screens use that the library did not specify. Same rules as §3–§10: one recipe per pattern, ink controls, ball only on the screen's one action, no shadows, 4pt grid, states per §2.
| Component | Geometry | States / behaviour | Used by / replaces |
|---|---|---|---|
| HeaderTextAction | ghost text 15/22·600 ink, pad 0 12 in a 44 target, text edge on the right rail; at most one per side (§16 P3), with at most one icon beside it; label ≤ 4 syllables (관리, 초대 코드, 편집, 완료); never destructive; never in a wizard header, where the owner's wizard-navigation LAW makes back and 건너뛰기 real buttons (§21.2 WizardShell header) | pressed .97/.92; disabled .5; role="button" | club 관리 (IA-4), 초대 코드, edit mode; re-specs HeaderTextAction ($primary → ink) |
| ActionSheet | bottom sheet (ModalPanel): title 17/24·600 = the object's name + close; body = groups of Cells 48 (leading icon 20 optional, label 15/22·400 ink; a chevron only on cells that open another screen); group title 13/18·600 secondary, 8 below; cell hairline inside a group; 24 between groups; destructive group last in danger, each with its own Dialog; no CTA band; max 80% height, scrolls | slide 200 ms; tap-outside dismisses; focus returns to the trigger | host tools (up to 9), club 관리 (IA-4); any set of more than five actions (the Menu keeps ≤ 5) |
| CTA band helper line | inside the band, above the button: 13/18·500 secondary, ≤ 2 lines, left-aligned at the rail, MetaSegments; 8 above the button; band 118 → 144 (one line) / 162 (two lines) | static; never a second action | session detail fee + cancellation terms (IA-3), eligibility |
| MediaBand | photo: full-bleed 240 including the status bar; cover-fit; a top scrim ink α0.35 → 0 over the first 100 so the overlaid header reads; the header over it is transparent with white 24 glyphs on 36 ink α0.4 circles; COVER-ALWAYS (the club's default cover when it has none). map: 160 under the white header, static map with a 24 ink pin, a neutral solid Tag "지도" bottom-right at 12 inset | photo: past (240 − 100) of scroll the header turns white (150 ms), glyphs ink, the title fades in, edge hairline on; status bar light over the photo, dark after; map: tap opens the map app; loading gray100; error court tile | club hub, club preview, venues (photo); session detail when a venue map exists (map; IA-10, owner rulings COVER-ALWAYS and MAP-BAR-INK) |
| PostRow | author line (avatar 40 + 12 · name 15/22·600 · time 13/18·500 secondary · ⋯ IconButton 44) → 12 → body 15/22·400, 4-line clamp + "더보기" 15/22·600 secondary → 12 → photos (1: 350 × 262 r12, 4:3; 2: 171² r8, gap 8; 3+: two squares, "+N" 20/28·700 white on ink α0.5 over the second) → 12 → action row 44: heart 20 + count 13/18·600, comment 20 + count, gap 16; posts separated by 32 of air, no line (the author line opens each post) | body pressed .98/.80 opens the post; author opens the profile; skeleton (avatar + 2 lines + 350 × 262); pinned = neutral Tag "공지" trailing the author line; deleted = "삭제된 게시물" 15/22 secondary | club 홈 feed; replaces post-row.tsx + FeedPhotoStrip |
| MessageBubble + Composer | bubble max 264, pad 8 12, r18 with the sender-side corner 6 on the last bubble of a run, 15/22; mine: ink #15181A, white text, right; theirs: gray100, ink, with avatar 32 + name 13/18·600 secondary on the first of a run; runs within a minute stack at 4, new runs at 12; time 11/14·500 secondary beside the last bubble; date and system lines centred 13/18·500 secondary, 16 around. Composer: min 56 + home inset; attach IconButton 24/44; field gray100 r20, min 40, pad 8 16, 15/22, grows to 5 lines then scrolls; send = 36 ball circle with an ink arrow 20 in a 44 target | send disabled .5 when empty; the bar rides the keyboard (KeyboardStickyView), the thread stays bottom-anchored; new bubble fades in with an 8 rise over 150 ms (reduced motion: none); edge hairline over the bar while content is behind | DM thread; replaces the feature-local compose bar. Mine is ink, not ball: ball stays the one action (send) |
| Calendar | month: header 44 (month 17/24·600 at the rail + prev/next IconButtons); weekday row 32 (13/18·500 secondary; 일 in danger, 토 in $series1); day cells 50 × 44 (7 × 50 = 350); numeral 15/22·500 in a 36 circle; today = 1.5px ink ring; selected = ink fill, white 600; range = ink ends + gray100 band; up to three 4px activity dots under the numeral; out-of-month days not drawn. Week strip: 7 × 50, day label 12/16·500 secondary over a 36 numeral circle 15/22·600; height 64; swipes by week | disabled dates .5; role="grid", cells aria-selected | 기간 기록 activity calendar, 홈/date pickers (the wheel stays for times) |
| Podium | three columns over 350 (2위 · 1위 · 3위), centred; 1위 avatar 56 raised 16; 2위/3위 avatar 48; a 2px medal ring (gold #D9A00F, silver #8E8E93, bronze #B2580B); under each: rank 13/18·600 secondary, name 15/22·600 (2 lines, never cut), ELO 17/24·700 tabular, level Tag; height 168 | column pressed .98/.80 opens the profile; the list below starts at rank 4 | leaderboards |
| ScoreEntry | team rows (names 15/22·600, 2 lines) × set columns of number Inputs 48 × 48 r12 with 24/32·700 centred numerals, numeric keypad, auto-advance; a quick-pick Choice row (6-0 … 6-4, 7-5, 7-6) fills the active set; a tiebreak field 36 × 36 appears at 6-6 / 7-6; validation helper in danger under the table; one CTA 저장 | focus = 2px ink border; error per field; never a toast | one entry for host and participants (ruling 14); retires the ±1 stepper sheet |
| LiveScore | ink Block 350 × 144 r16 pad 20: two team rows (names 15/22·600 white α0.85), set scores 24/32·700 white tabular in 32 columns, the current set and the game points (0 / 15 / 30 / 40 / AD) in ball; serving = 6 ball dot before the server; host controls below: two lg tonal Buttons (팀 A 포인트 / 팀 B 포인트) + an undo IconButton | point = haptic outcome + 150 ms numeral cross-fade (reduced motion: none); read-only for participants | live match board |
| Select mode | entered from a HeaderTextAction "선택" (long-press is an accelerator, never the only path); header: 취소 (text, left) · "n명 선택됨" title · 전체 선택 (text, right); each row gains a leading 24 checkbox (2px #4F5851; checked ink fill + white check) at the rail and its content shifts 36; a row tap toggles; the trailing element stays; the CTA band shows the bulk action as lg primary ("3명 입금 확인"), disabled at zero | a money action asks a Dialog before committing (canon: confirm consequential mutations); defaults per ruling 12 (only 확인 대기 ticked); role="checkbox" + aria-checked per row; the count is announced politely | dues reconcile, session-fee confirm (flows 3, 8) |
| CountdownNotice | Notice, warning tone (#F5B301, ink): title "입금 대기 중", the countdown 15/22·600 tabular right ("09:41 남음"), body = the account line; at zero it becomes danger ("시간이 지나 자리가 풀렸어요") with the action "다시 신청" | ticks every second, digits do not animate; screen readers hear 5:00 and 1:00 only | the payment hold (flow 3) |
15. Amendments from the behaviour addendum (2026-10-09)
flat-v2-behaviour-addendum-2026-10-09.md (B6 text scaling, B7 motion, B8 accessibility semantics, B9 platform) changes these rows; its questions Q1–Q10 are ruled by its recommendations (owner: "go with best practices").
| Where | Was | Now |
|---|---|---|
| every fixed height that holds text (§3–§9) | height | the same number as minHeight (a floor); content may grow (B6.4) |
| text scaling | unspecified | follow the OS 85–200% for reading text (cap 2.0, Q1); hero capped 1.7; numerals and labels 1.5; chrome (header, scope tabs, tab bar, FAB) 1.3; iOS per-style curves via dynamicTypeRamp; from 1.5× layouts stack — trailing element under line 2, Cells label over value, roster 2-up, dialog buttons stacked (Q2); header stays 56 and tab bar 56 + inset at every scale |
| PersonRow (§5) | pitch 64 | pitch 68 — the two-line text block (22 + 4 + 18 = 44) is taller than the 40 avatar; 68 = 12 + 44 + 12 (Q9) |
| §2 Toast | 2800 ms, swipe-down | 4000 / 6000 / 8000 ms by kind (plain / with action / error), the Android accessibility timeout honoured, × on error and action toasts, tap to dismiss (Q6); undo label 되돌리기 (Q10) |
| §2 Reduced motion | drop the settle, keep the dim | one table: translate, scale and layout removed; cross-fades become instant; loops stop after 5 s |
| motion tokens (§2) | springs + mixed durations | effects on timing: 100 ms (hairline, hover, exits) · 150 ms (fades, skeleton → content) · 200 ms (toggles, FAB width, sheet exit) · 250 ms (reveals) · 300 ms (sheet rise); curves standard (0.2, 0, 0, 1), enter (0, 0, 0, 1), exit (0.3, 0, 1, 1); press springs unchanged; Tamagui quick/medium/slow no longer drive opacity |
| lists (§5) | unspecified | no layout or entering/exiting animation inside virtualised lists: a row that moves (예비 → 참석) fades out 100 / in 150 and siblings snap; short stacks (≤ 24 children) may use a 200 ms layout transition |
| §2 FAB on scroll | collapses in 150 ms | width 200 ms, label out 100 / in 150, with a direction dead-band |
| §8 Sheet | slide 200 ms, no drag | rise 300 ms / exit 200 ms; iOS: swipe-down on the title row dismisses (HIG; Q3), Android unchanged |
| §6 Live tag pulse | 1600 ms loop, indefinite | loops 5 s per mount, then rests on the static frame (WCAG 2.2.2) |
| §3 Tab bar | label 11/14 · pressed .92/.78 | iOS keeps the non-scaling label + Large Content Viewer; Android caps the label at 1.3; the press physics come from a custom tabBarButton on Pressable variant="icon" (the JS tab bar sets pressOpacity: 1 today) |
| accessibility names (B8) | per component roles | one focus stop per row with a comma-joined spoken name built from the row's own view model (월/일/요일, 오전/오후 N시부터 M시까지, N명 중 M명 참석, NTRP 3.0부터 4.0); state in aria-*, never in the name; exactly one visible h1 per screen; toasts announced on iOS explicitly |
| platform (B9) | — | predictiveBackGestureEnabled stays false (RN 0.86 + screens 4.26 have no back-progress API); fullScreenGestureEnabled: false on slider and wizard screens (iOS 26 full-screen swipe-back; Q7, OTA); ModalPanel adds navigationBarTranslucent; native tabs not adopted (they cannot report bar height, which the FAB/toast/CTA stack needs); Bold Text weight mapping later (Q8); the 3-button-navigation seam fix waits for the emulator check and the native batch (Q5) |
16. Resolved from the patterns board (2026-10-09)
| # | Question | Resolution |
|---|---|---|
| P1 | ScoreEntry quick pick and tiebreak | a quick pick credits the top team first (tap again to swap); the tiebreak is a 44 row under the table with a 36 field at the right; set columns are labelled 1세트 … |
| P2 | ActionSheet height | cap 88% (the code's ModalPanel value); the body scrolls and the destructive group stays last, even below the first screen |
| P3 | HeaderTextAction count | at most one per side (select mode needs 취소 left and 전체 선택 right) |
| P4 | LiveScore numerals | instant (B7 governs: scores and counts never animate) |
| P5 | Text action over a MediaBand photo | white 15/22·600 on an ink α0.4 pill, 36 tall, matching the 36 icon circles |
| — | MediaBand map press / focus | dim .85 without a settle; focus ring inset 2 |
| — | PostRow clamp | 4 lines with 더보기 inline on the fourth (code clamps at 5 today) |
| — | MessageBubble sending / failed | sending: a 12 clock glyph before the time; failed: "전송 실패" in danger + a retry text action under the bubble |
| — | Week strip | no activity dots (the 64 cell has no room) |
| — | Podium height | 168 is a floor; a wrapped name grows its column |
| — | CTA band | the spec stands (16 + 52 + 16 + inset; hairline only while content scrolls under it); the code's max(16, inset) and permanent border change at build |
| — | MetaBreak at large text | a break must never end a line: MetaSegments keeps the break with the following segment (a build fix, found at 130%) |
17. Rulings from the page boards (2026-10-10)
| # | Question | Ruling |
|---|---|---|
| H1 | Header trailing order | a HeaderTextAction sits left of any icon; the overflow ⋯ is always the rightmost item (Material: overflow last) — e.g. [관리] [⋯], [북마크] [⋯] |
| H2 | 홈 Block target | tapping the day Block opens 경기 › 내 경기 (a tab switch, scrolled to that day) |
| H3 | 오늘 복기 rows and the recap card on 홈 | leave 홈 with the scope row; results live in 기록 and arrive by push — 홈 is the schedule |
| H4 | Club pane label | 순위 (shipped; 기록 is reserved for the tab) |
| H5 | 회비 month control | a Choice ("10월 ▾") opening a bottom Sheet of every month since dues began, newest first, current month checked — never a Menu (the Menu caps at five, older months must stay reachable) |
18. Page-board rulings and the components they added (2026-10-10)
18.1 Rulings (list-a D1–D8)
| # | Question | Ruling |
|---|---|---|
| D1 | Dismissing notifications | Select mode is the path (accessible, discoverable); SwipeAction is an accelerator only (§18.2); unread = a 6px danger dot hung in the gutter (x 8–14) + "읽지 않음" prefixed to the spoken label |
| D2 | Request rows (메시지 요청, applicants) | an inline answer pair under line 2: two sm (36) tonal Buttons (수락 · 거절) — neutral, so the screen's one coloured action is unaffected; request lists only |
| D3 | Header subtitle | yes: the 64 Header with a 13/18 secondary subtitle names the context (club, session) on pages reached from it |
| D4 | Row ⋯ | always an ActionSheet (bottom, thumb-reachable, destructive last with its Dialog); the anchored Menu is for the header ⋯ only |
| D5 | 앨범 grid | 3-up full-bleed with 2pt gaps (the photo-grid standard); tile size is derived from the width and exempt from the 4pt grid, like any computed media size |
| D6 | 참가비 status words | the terminology guide governs (입금 필요 · 입금 확인 중 · 입금 완료); flow 3's draft words are superseded |
| D7 | 신고 관리 entry | a persistent Cell in the 관리 ActionSheet for officials, with a count Tag when there are reports |
| D8 | Unresolved 출석 확인 rows | an inline sm tonal pair 참석 · 불참 (an explicit choice; a bare row tap never records attendance); closeout enables when none is unresolved |
18.2 Components added (gaps from the hub and list-a boards)
| Component | Spec |
|---|---|
| Unread marker | 6px danger dot hung at x 8–14, centred on line 1; spoken prefix 읽지 않음 |
| SwipeAction | row swipe left reveals one 72-wide action (danger for delete, ink for archive); full swipe commits with an undo toast; accelerator only — every swipe action also exists in Select mode or the row's ActionSheet; disabled in Select mode; reduced motion: no elastic |
| AnswerPair | two sm tonal Buttons (gap 8) under line 2 of a request row, left-aligned with the text; the row grows by 44; each button 44 target |
| PhotoGrid | 3-up full-bleed, 2pt gaps, square tiles; long-press enters select; the +N overflow tile pattern from PostRow |
| SectionHead variants | count suffix (17/24·600 title + 15/22·500 secondary count, gap 4) · command action (13/18·600 ink, no chevron, e.g. 모두 읽음) · ExpandToggle slot (the one chevron collapse) |
| Tabs item with count | a count Tag (20, neutral, or danger when it needs action) 4 after the label |
| FilterSheet | bottom Sheet: groups of Choices (group title 13/18·600 secondary, 8 below, 24 between groups); footer pair 초기화 (tonal) + "결과 n개 보기" (primary, the count live); trigger: a Choice with a chevron and the active count, followed by removable Choices for each active filter |
| Edit Sheet footer | an edit Sheet has no ×; its footer is the pair 취소 (tonal) + 저장 (primary); unsaved changes on dismiss ask a Dialog |
| CTA band, danger tone | the band's one button in danger for a destructive bulk action (알림 n개 지우기), always preceded by a Dialog |
| PersonRow trailing meta | trailing 13/18·500 secondary text (time, rate) in place of a figure or Tag |
| Caption row | 13/18·500 secondary text at the rail, 32 tall, for a legend or a subtitle under a section head; no action |
| Activity heatmap | 7 rows × n weeks of 12 cells, gap 4, r3; five steps from gray100 to ink; month labels 12/16 secondary above; a scrub readout like the Chart's |
| MediaBand album overlay | photo band count Tag (neutral solid "1 / 12") bottom-right and pager dots (6px white α0.6, current white) bottom-centre |
| PostRow system variant | for 경기 결과 and 사진 업로드: the author line is the club (logo 40) with a 13/18 secondary line naming the event; body is the result (ResultRow) or the PhotoGrid preview; no ⋯ |
| Club hero, place variant | the club hub's identity under the MediaBand: logo 56 r12 overlapping the band bottom by 16, name 24/32·700, region and members as a MetaSegments line 15/22 secondary — no glyph facts |
| Header icon badge | the tab-bar dot or count Tag on a header icon (top-right of the 24 glyph, ring 1.5 canvas) |
| Task Cell | Cell with an ink 15/22·500 label, a count Tag before the chevron; used by the officials' 할 일 section |
| Two-line Cell | Cell with a 13/18 secondary second line under the label (dates, captions); min 64 |
| Reaction picker | a Sheet with a wrapped row of reaction Choices (emoji + count; the emoji are content here, not fact glyphs); the viewer's choice ink-filled |
19. Rulings and components from the detail board (2026-10-10)
19.1 Rulings (detail D1–D6)
| # | Question | Ruling |
|---|---|---|
| D1 | Session header for a host or official | [관리] [⋯]. 일정 수정 is the first row of the 관리 sheet's 운영 group; it enters the page's edit mode as the header pencil did. This keeps the bar at two trailing items, which was the owner's own reason for the pencil (EDIT-PENCIL: "back to two glyphs with ⋯, where it had three"). The in-row pencils of edit mode are unchanged. Everyone else keeps [북마크] [⋯]. |
| D2 | 참석 취소 for a joined member | the CTA band's lg tonal Button. A ghost reads as floating text and hides the one action a joined member has. The danger tone belongs to the confirming Dialog, not to the band. The screen has no ball action in this state, because the RSVP is done. |
| D3 | Map band on 일정 상세 | the map band shows whenever the venue has a map (one page structure for every session; owner 2026-09-20 hero ruling). The fold test is that the roster's head and first row sit above the CTA band, not the whole roster: the head's count gives the total, and a second row cut by the band is the scroll cue (NN/g, "illusion of completeness"). At the corrected band geometry (§19.2) an eight-slot roster starts at 512 and its first row ends at 644, under the 700 top of the tallest band (144). The board's alternative, a map only for one-row rosters, would give two page structures that depend on the roster size. A venue without a map keeps the text hero. |
| D4 | Score entry on the 경기 보드 | ScoreEntry opens in a Sheet over the board, with the keypad up, one 저장 and an action Toast with 되돌리기 (6 s, §15) that restores the previous score. Errors stay inline (§14). |
| D5 | 공개 프로필 header | two icons, [메시지] [⋯]; 공유 moves into the ⋯ Menu with 신고 and 차단 (three rows, under the Menu's five). |
| D6 | Order of work | the components in §19.3 are added to the library before any page is built from the detail board (contract §1). |
19.2 Corrections to earlier sections
| Where | Was | Now (why) |
|---|---|---|
§14 MediaBand map: geometry | 160 under the white header | the photo variant's geometry: full-bleed 240 including the status bar; the top scrim (ink α0.35 → 0 over the first 100); a transparent header with white 24 glyphs on 36 ink α0.4 circles that turns white past 140 of scroll. Owner ruling MAP-BAR-INK (2026-09-28) gave the map the club photo's treatment. One recipe now covers every media band. |
§14 MediaBand map: label | a neutral solid Tag "지도" bottom-right | no Tag or chip on the map. Owner ruling MAP-NO-CHIP: the map is its own cue. |
§14 MediaBand map: press | opens the map app | opens 장소 상세 (the venue page). Owner ruling MAP-NO-CHIP: the map "leads back to the location page". Directions live on the venue page. |
§14 MediaBand map: pin | — | the 24 ink pin is centred in the visible area under the header (y 170 of 240), not in the band. |
| §18.2 Two-line Cell | min 64 | min 68: 12 + 22 + 4 + 18 + 12, the same text block and pitch as PersonRow (§15) |
| §18.2 Reaction picker | a Sheet with a wrapped row of reaction Choices | replaced by Reactions in §19.3: one quick row, one chip summary |
19.3 Components added (the detail board's library gaps, part K)
| Component | Spec |
|---|---|
| ExpandToggle, lone row | 48 full-width row; the label is centred, 15/22·600 secondary, and states its count ("4개 더 보기" / "접기"); a 16 chevron after the label rotates 180° (200 ms; instant under reduced motion); aria-expanded. The label is never a bare chevron, because an icon-only toggle hides what it does. |
| ExpandToggle, disclosure Cell | a Cell whose trailing chevron points down (closed) or up (open); a right chevron always means navigation. An optional value (for example a date) sits before the chevron. Opening reveals the body under the label: 15/22·400 ink, 12 below the label, 16 at the bottom. Cells open independently, and opening one never collapses another, because auto-collapse makes the content jump under the finger (NN/g, accordions on mobile). Used by FAQ, 공지사항 and long fact lists. |
| Cell, reason line | the two-line Cell (68) with a 13/18·500 secondary reason under the label, for a locked field ("시작 후에는 바꿀 수 없어요"). The label stays ink and readable. There is no chevron and no pencil, and the row is not pressable. |
| Cell, Tag value | the value slot may hold one Tag sm (24) in place of text; the Cell stays 48; a navigating Cell keeps its chevron after the Tag. |
| Roster door | when people outnumber the visible cells (eight, two rows of four), the last cell is a door: a 48 gray100 circle with "+N" 15/22·600 ink and the name line "외 N명". The door opens 참석자, as does the section head's 전체 보기. |
| Hero facts: count and state | the facts block holds one to three facts, each 24 tall with 8 between. The hero is as tall as its facts, and no placeholder fact is drawn; with no facts the block and the 16 above it go. One viewer-state Tag (sm) may trail the fact it qualifies, with 8 between (참가비 15,000원 → 입금 완료). At most one per hero, and it counts toward the object's two Tags. |
| PersonRow trailing alignment | text, figures and Tags (≤ 24 tall) centre on line 1; controls taller than 24 (a 36 Choice, sm Buttons) centre on the row. One trailing element still. |
| Composer reply target | a 44 row inside the composer, above the field: "OOO님에게 답글" 13/18·600 secondary, then a one-line excerpt 13/18·400 secondary that ellipsizes. The excerpt is quoted text, not a name, so it may crop. A × IconButton (20 in 44) clears the target. The row appears with the field's focus, and Escape or × removes it. |
| Avatar, group | two member avatars at 0.6× inside the avatar box, offset diagonally: the back one top-left, the front one bottom-right with a 2px canvas ring. They use photos when present, and initials otherwise. Spoken as the member names. Identities stay photos, never a generic glyph. |
| Avatar, anonymous | a gray100 circle with a user glyph (20 in 40, 16 in 32) in secondary, spoken "참석자". It is used where privacy hides names: a non-member's preview shows counts, never names (club avatar-privacy canon). |
| Comment | avatar 32 at the rail; text column at x 64. Line 1 is the name (15/22·600) + MetaBreak + time (13/18·500 secondary), with a ⋯ IconButton (20 in 44) at the right. The body is 15/22·400 ink in the text column: full on the post page, 4 lines + 더보기 in previews. Under the body is a 답글 달기 13/18·600 secondary text action (44 target via hit slop). Replies: one level, avatar 24 at x 64, text column at x 96; deeper replies flatten with an @name; more than two replies collapse behind a lone ExpandToggle aligned to the text column ("답글 3개 더 보기"). Spacing: 24 between comments and 12 between a comment and its replies. A deleted comment reads "삭제된 댓글입니다" (secondary) and keeps its replies. The body indents to the name because short, threaded text belongs to its author. PostRow keeps its full-width body for long content and photos. |
| Live row | min 24, directly under the header: the Live tag (§6), 8, then the date and venue as MetaSegments 13/18·500 secondary at the rail; 8 above and below. Replaces the LIVE band on 스코어카드 and 경기 보드. |
| Strip with deltas | an optional delta line 12/16·600 secondary under each label ("+3", "−2"; a plain sign, never red/green or arrows — deltas are numbers, not status). The Strip grows 80 → 100 for all its columns. The comparison ("지난달 대비") is named once, in the caption row under the section head. |
| Form strip | head-to-head and player records only, never in rows: rows keep one trailing figure, and colour-only dots fail WCAG 1.4.1. The strip is five 24 × 24 r6 cells, gap 4, in chronological order left to right. 승 = ink fill with white 12/16·700; 패 = gray100 with secondary. Spoken "최근 5경기: 승, 승, 패, 승, 패". In a row, recent form is line-2 text ("최근 5경기 3승 2패"). |
| ScoreEntry, Sheet form | (amended §23) the Sheet rises (300 ms) without the keyboard, showing the title row (the match name, §19.5), the set table and the quick-pick row (6-0 … 6-4, 7-5, 7-6): a common result is one tap. Tapping a set field raises the keypad. The quick row folds away while typing (200 ms; instant under reduced motion) and the sheet docks to the keyboard top (KeyboardStickyView), with 저장 right above the keyboard and no home inset. The tiebreak row appears only at 6-6 / 7-6, and inline validation sits above 저장. Measured worst case while typing (tiebreak + validation): 372 + the 291 keypad = 663, 79% of the screen, under the 88% cap (§16 P2); the open state is 454 over the home inset. Dismissing with unsaved digits asks the Edit-Sheet Dialog (§18.2). |
| ActionSheet (scope) | the ActionSheet (§14) is the generic sheet for any object's actions, not a 관리-only sheet: row ⋯ (§18.1 D4), a message's long-press, a comment's ⋯. A leading 20 icon is optional per instance, consistent within one sheet. |
| Reactions | one component in two parts. Quick row: at the top of the object's ActionSheet (a message's long-press, a post's reaction trigger), the content set in 44 gray100 circles with the emoji at 24. The viewer's current reaction has a 2px ink ring, and a tap toggles it and closes the sheet. Summary: under the bubble or post, chips 28 tall, r full, gray100, pad 0 8, emoji 16 + count 13/18·600 tabular. The viewer's own reaction has a 1.5px ink ring. Tapping a chip toggles that reaction; long-press lists who reacted, in a Sheet of PersonRows. There is no centred panel variant. Emoji are content here, not fact glyphs. |
19.4 Build notes from the detail board
- Three reaction vocabularies exist today: match reactions (
thumbs_up, fire, muscle, clap, heart,reaction.entity.ts), post reactions (like, clap, fire, heart, laugh, a local map inpost-row.tsx) and DM quick reactions (👍 ❤️ 😂 😮 😢 🎾,messaging/.../helpers.ts). The Reactions component reads one domain registry. Ruled 2026-10-10: one set of six for every surface, in this order: 👍 ❤️ 👏 🔥 😂 🎾 (approval, affection, applause, intensity, humour, and the sport). The build unifies the match and post DB enums and the DM constant into that registry; the migration is cheap while the database is not live. The boards draw the six. - The host's header pencil (EDIT-PENCIL) becomes the 일정 수정 row of the 관리 sheet (D1). The edit mode it opens, and its row pencils, are unchanged.
- The map band's code (
SessionHeroSection) already uses the photo scrim and white glyphs (MAP-BAR-INK shipped 2026-09-28), so §19.2 aligns the library with the shipped page rather than changing it.
19.5 Settled while drawing (detail board patch, 2026-10-10)
| Item | Ruling |
|---|---|
| ScoreEntry title row | the match name ("2라운드 1번 코트"), not the teams: the teams are the set table's row labels, and two doubles pairs do not fit one 17/24 line without repeating the table |
| Group avatar sizes | the inner avatars are 0.6× rounded to the grid: 20 in a 32 box, 24 in a 40 box, the front one inside a 2px canvas ring |
| Disclosure Cell body | the bottom pad stays 16 (spacing stays on the 4pt grid). A box whose height comes from its text lines (n × 22) is a listed exemption from the 4pt height check, never a reason to bend a pad to 17 (contract §5) |
| Offline strip over a MediaBand | the strip attaches to the header's bottom edge in the header's own layer; over a photo or map it is solid (gray100 fill, ink text), never translucent, and it pushes no content |
| ActionSheet group titles | a group title shows only when the sheet has two or more non-destructive groups; a single group and the destructive group are untitled (the danger colour marks the latter); 24 still separates groups |
| PostRow counts vs Reactions | feed: the action row's heart carries the total reaction count, with no chips in the feed. Post page: the per-emoji chips sit under the body, and the heart has no number. Each count is shown once. |
| Content under a MediaBand | the first element starts 24 below the band. A Notice in that slot is followed by the hero at the hero's own 24 top |
| Strip delta: zero or no data | zero shows "0"; with no comparison data the delta line stays empty in that column (its height is kept so the columns align) and never shows a dash. If no column has data, the Strip returns to 80 and the caption is omitted |
| Form strip data | the last five completed head-to-head matches, in chronological order, from the existing head-to-head query (no new backend). With fewer than five matches, only those cells are drawn; with none, the strip is omitted |
20. Rulings and components from the list-B board (2026-10-10)
20.1 Rulings (list-b L1–L6, G)
| # | Question | Ruling |
|---|---|---|
| L1 | 친구 찾기 result row | the row opens 공개 프로필; 친구 추가 keeps its own target and focus stop. This is the PERSON-ROW canon ("the default tap opens the profile"); today's static row is out of canon. |
| L2 | New copy on the board | approved as drawn. It goes through the terminology guide and the house-style gates at build. A confirming Dialog names the person and the consequence ("정우진님을 친구에서 삭제할까요?"), per the confirm-consequential-mutation canon. |
| L3 | 테니스장 kind chips | into the FilterSheet's first group (single choice: 전체 · 테니스장 · 연습장 · 샵, with counts). The active kind shows as a removable Choice after the 필터 trigger (§18.2). 가까운 순 stays on the page as the sort Choice. 저장한 코트 leads the browse as its own section (a cap and 전체 보기), not as a filter. The pane already sits under the 경기 tabs, and a second chip row would be navigation by chips, which the flat grammar retired. |
| L4 | Level Tag on 리더보드 rows | keep BADGE-EXCEPTION (owner ruling 2026-09-23): the level Tag shows only where a level run starts, and the Podium always shows it. Library §5's "every rank row" is amended to this. The Tag sits in a reserved slot so the figures align (§20.2). |
| L5 | 경기 기록 facets | split 정렬 from the filters. Sort changes order and filters narrow; a sort buried in a seven-facet sheet hides the current order. 정렬 is a Choice ("최신순 ▾") that opens a Sheet of radio Cells (every Choice ▾ opens a bottom Sheet, H5). The seven filters stay in one FilterSheet with 초기화 and the count CTA pinned. |
| L6 | Unreachable 업적 states | keep them drawn and marked "not reachable today". The shipped screen renders progress and hidden states, so the build keeps their spec. Build note: until a non-챌린지 achievement can be earned, the unreachable states are dead UI, so the unlock engine is either scheduled or the states are deleted (no dead code). |
| G | Ten gaps | library first; mapped below. Six were already specified (§18.2, §19.3); four are new (§20.2). |
| Gap (list-b) | Library |
|---|---|
| Request answer pair | §18.2 AnswerPair (both buttons tonal, §18.1 D2) |
| Text row without an avatar | §20.2 Text row |
| Achievement tile | §20.2 Thumb, glyph tile |
| Filter Sheet | §18.2 FilterSheet |
| Section count suffix | §18.2 SectionHead variants |
| Edit Sheet | §18.2 Edit Sheet footer |
| Row trailing control | §20.2 Row trailing control (and §19.3 alignment) |
| Rank figure column | §20.2 Figure column |
| Four-digit rank column | §20.2 Figure column (rank width) |
| Permission-wall EmptyState | §20.2 EmptyState, permission |
20.2 Components added
| Component | Spec |
|---|---|
| Text row | the PersonRow text block without leading media: L1 15/22·600 ink, L2 13/18·500 secondary, min 68, one trailing element (Tag, figure or chevron). Used for lists of documents, receipts and history entries (동의 이력, 제안 내역). A Cell is a label/value pair inside a settings group; a Text row is an object in a list. |
| Thumb, glyph tile | 56 × 56 r12 gray100, with the object's 24 glyph centred. This is the object's identity image, the way a photo or logo is, so the no-glyphs-in-rows rule (which covers inline fact glyphs) does not apply. Achievements: when earned, the glyph takes the achievement's tier colour (ledger tiers); when locked, the glyph is secondary and the row's trailing figure is the progress ("7/10" 15/22·600 tabular). A hidden achievement shows "?" 20/28·700 secondary with L1 "숨겨진 업적". No weak tint fills (S2 neutrals are pure grey). |
| Row trailing control | a row's one trailing element may be a control: an IconButton (20 glyph in a 44 target) or a sm tonal Button. It centres on the row (§19.3) and is its own focus stop. The row's press area excludes the control's target, and pressing the control never presses the row. Spoken as two stops: the row's composed label, then the control ("정우진, 친구 추가"). |
| Figure column | ranked and scored lists right-align their trailing figure (15/22·700 tabular) in a column as wide as the widest figure on the page, so the digits line up. The level Tag (run starts only, L4) sits in a reserved 24 slot, 8 before the column, so a row without a Tag keeps the same figure position. The rank column fits the widest rank on the page: 24 (1–99), 32 (100–999), 40 (1,000+), 13/18·600 tabular, right-aligned, 12 before the avatar. |
| EmptyState, permission | the content area's centred block: 48 lock glyph secondary → 16 → title 17/24·600 ink ("로그인이 필요해요") → 8 → body 15/22 secondary (one line naming what sign-in unlocks) → 24 → one md Button (primary when it is the screen's only action, else tonal), drawn only when an action resolves the wall (로그인). A role wall, which the viewer cannot resolve, has no button; the header's back is the way out. It joins first-run and filtered-empty as the third kind of empty (contract §3) and never appears on a hub. |
20.3 Medal colours and the glyph tile (settled 2026-10-10 from the list-B patch)
The glyph tile's "glyph in the tier colour on gray100" fails WCAG 1.4.11 for two tiers: silver #8E8E93 reaches 2.91:1 and gold #D9A00F 2.09:1, because the ledger made the tier colours as fills, not as glyph colours. The code also has two medal sets: shared-semantics.ts (#D9A621, #A8B0B8, #B8793A, #D9E1E8) and the ledger and Podium set. Resolved:
| Tier | Fill | Glyph on the fill | Contrast |
|---|---|---|---|
| bronze | #B2580B | white | 4.89:1 |
| silver | #8E8E93 | ink #15181A | 5.47:1 |
| gold | #D9A00F | ink | 7.63:1 |
| platinum | #4A6378 (new; L* 41, ΔE76 ≥ 22 from every other tier, 57 from the locked tile) | white | 6.27:1 |
- Earned glyph tile = the solid tier fill with the glyph in its on-colour. Locked = gray100 with the glyph in secondary. Earned and locked never look alike (ΔE76 ≥ 36 between any fill and gray100), and the colour marks the achievement the viewer has.
- One medal set: these four values feed the glyph tile, the Podium rings and every medal use. The
shared-semantics.tsmedal set retires at build. Dark mode keeps the same fills, since the glyph carries the contrast.
21. Rulings and components from the forms board (2026-10-10)
21.1 Rulings (forms A1–F1, plus the departures the board reported)
| # | Question | Ruling |
|---|---|---|
| A1 | Is the step-by-step 일정 만들기 a visible entry? | No separate entry. A host's first creation, and any creation with nothing to copy (no 정기 일정 and no past session), opens the step-by-step path automatically. Every other 새로 만들기 opens the blank review page, whose required Cells carry the asterisk and whose CTA helper names what is missing. A third door in the entry Sheet would add a choice without adding a task. 장소 opens the full-screen Search takeover, per the VenueFinder canon (IA §4.7). |
| B1 | 클럽 만들기 quick path | drop the 지역 screen when the profile has a region (6 taps). The region shows on the final review as an editable Cell, so it is still answerable. IA ruling 10 already pre-answers it. |
| C1 | Leave guard and copy | adopted: create forms ask 나갈까요? (처음부터 다시 작성해야 해요), edit forms ask 수정을 그만둘까요? (바꾼 내용은 저장되지 않아요), and every form with unsaved input gains useUnsavedChangesGuard (never usePreventRemove) |
| D1 | 프라이버시 rows | each row's trailing Choice ▾ opens a bottom Sheet of radio Cells, never a Menu (one pattern for every Choice ▾, §17 H5), and the level's meaning is the second line (Two-line Cell). The title stays 공개 범위 설정: it says what the page does, which matters to the low-digital-literacy audience, and the settings entry keeps the same words. |
| E1 | Routed 관리 page versus the 관리 ActionSheet | retire the routed page as a menu. The 관리 ActionSheet is the one list of management doors, built from one registry (today's admin-tab rows). The attention counts (club-admin-attention.ts) move onto its Cells as count Tags (Task Cell, §18.2), and the header 관리 action carries a Header icon badge when anything is pending. /clubs/[clubId]/admin stays as an alias that opens the club hub with the sheet presented, for old links. Two lists of the same doors would drift. |
| F1 | Twelve gaps | library first; specified in §21.2 |
| — | 건너뛰기 drawn as ghost text | against the owner's wizard-navigation LAW (2026-08-15: "top controls must look like buttons, not bare glyphs"). The wizard header is the §21.2 WizardShell header. The library's ghost HeaderTextAction is for non-wizard pages only. |
| — | Required fields drawn without the asterisk | against the owner's required-marker LAW (2026-08-15, canon wizards.md): every required control's label carries the one LabeledField required asterisk ($error, size-matched), and the field starts genuinely unset. This applies to the step path and to unset required Cells on the review page. |
| — | 경기 유형 shown only when it differs from 정기 | always shown: every editable field has its Cell on the review page, and a default shows its value. A field hidden at its default cannot be changed. |
| — | Save confirmation as a Toast | adopted (a success needs no dialog; dialogs confirm before a consequential action) |
21.2 Components added
| Component | Spec |
|---|---|
| Cell, edit | trailing pencil (EDIT-PENCIL: the shared EditCue, 16 glyph tertiary, the same size and tone as the chevron), never beside a chevron, spoken "장소, 수정"; opens the field's bottom edit Sheet (EDIT-SHEET) |
| Cell, locked | the §19.3 reason line plus a trailing 16 lock glyph in tertiary, in the chevron's slot. Not pressable. |
| Cell trailing slot | one of: value text, a Tag (§19.3), a Choice ▾ (sm 36, opens a Sheet), an sm tonal Button or IconButton (§20.2), a Switch, or a count Tag before the chevron (danger when it needs action, Task Cell). Only one per Cell. |
| Cell, label Tag | one Tag sm 8 after the label (e.g. 신규). The value slot then holds text only. It counts toward the object's two Tags. |
| WizardShell header (owner LAW) | left: a 40 × 40 r12 gray100 square with a 20 ink chevron (× on step 1, with the exit Dialog) in a 44 target. Right: 건너뛰기 as an sm tonal Button, never ghost or plain text. Centre: the progress, in ink. The footer holds the single CTA band and its helper line only. Hardware back mirrors the header back (useWizardBackHandler). |
| Dialog, one action | for an acknowledgement that blocks (a version gate, a rule the user must read): title + body + one full-width primary 확인. The backdrop does not dismiss it. Never used where a Toast would do. |
| Search takeover | full screen, rising 300 ms with the keyboard up. The bar is a Search field (40 r12 gray100, 15/22) filling the header, with 취소 as the text action on the right. Below: recent searches as Text rows with a × trailing control, then results as MediaRows. Empty = the filtered-empty EmptyState plus the contribution row ("찾는 곳이 없나요?"). It is the one venue-pick surface (VenueFinder canon). |
| Map face in a form | after a venue is picked: a 350 × 140 r12 static map with the 24 ink pin, a 1px inset #E5E5E5, 8 under the 장소 Cell; a tap reopens the Search takeover; loading gray100; error = the court tile |
| Photo tiles (composer) | 4-up wrapping rows, tile = (350 − 3 × 8) / 4 (computed, a listed exemption), r8. Remove = a 24 ink α0.6 circle with a white 16 × at top-right inset 4, in a 44 target. The add tile is gray100 with a 24 + and the count against the content's real limit (a club post holds four: "2/4"). Long-press drag reorders, with an accessible move action. |
| Field skeleton | a 13/18 label bar at 30% width, then 8, then a 48 gray100 r12 box; one shimmer per screen per §9 |
| Input counter | "12/20" 12/16·500 secondary, right-aligned on the helper line under the field. At the limit it turns danger and further typing is refused. Spoken only at 80% and at the limit. |
| Caption row, two lines | the §18.2 Caption row may wrap to two lines (min 32, grows); past two lines the copy is too long and is rewritten, never clamped |
| Time range wheel | a Sheet titled with the field name. Tabs 시작 · 종료 show their current values, and one native time wheel (오전/오후 · 시 · 분) edits the selected tab. 완료 commits both. For a range that cannot cross midnight, an end before the start shows the danger helper "종료가 시작보다 빨라요" and disables 완료. A range that may wrap (방해 금지 시간) accepts it, and the end value reads "다음 날 오전 7:00" (settled on the forms board). |
22. Rulings and components from the auth board (2026-10-10)
22.1 Rulings
| Item | Ruling |
|---|---|
| Owner question: sign-up route | open (EXT-11, the owner's). The board draws Kakao OAuth as the target, plus the interim email sign-up as a labelled variant. |
| Consent before birth year | reversed: the birth year comes first. The shipped wizard collects it at step 1 so the under-14 gate runs before any consent is collected (onboarding-wizard-screen.tsx, PIPA §15/§22/§22-2), and the legal order is not traded for a tap. Collecting the name before consent would collect personal data unconsented, so the name follows consent. New-user path: A1 시작 → 출생 연도 (the under-14 Notice inline) → 약관 동의 → 이름 (prefilled from the provider when given) → 첫 시작. That is 6 taps (from 12 today), one more than IA Flow 1's 5. The flows board counts 6. |
| Value props, welcome and done steps | retired. A1's single brand line carries the promise, and the first-action chooser ends the flow. Carousel tutorials are mostly skipped and teach little; the chooser does real work. |
| Provider cancelled | a 13/18 danger helper line above the provider button, not a danger Notice. A user's own cancel is not an alarm. |
| 나중에 할게요 | a ghost Button under the chooser's Cells (six syllables exceed the header text action's four). It is a content action, not navigation. |
| Consent prefix | [필수] / [선택], the shipped strings (onboarding.ts). The board's parentheses are corrected; one wording per concept. |
| Draft resume and sign-out | sign-out from onboarding clears the persisted draft, so the close Dialog's "저장되지 않고" stays true (build note) |
| Wrong credentials | plain-language error, no invented link. Gap recorded: there is no password reset for email accounts. The build adds "비밀번호를 잊으셨나요?" with Supabase's reset mail, which needs the deep-link return (backlog, owner to schedule). |
| 만 N세 readout | dropped: year-only arithmetic overstates the age until the birthday |
| Kakao label in English | "Login with Kakao", Kakao's mandated English label, which overrides our "Sign in" wording on that one button |
22.2 Components added
| Component | Spec |
|---|---|
| ProviderButton (Kakao) | the lg Button geometry (52, full width) with Kakao's mandated values: container #FEE500, the speech-bubble symbol #000000 (left-aligned or centred with the label), label #000000 α0.85 in the system font, radius 6, label "카카오 로그인" / "Login with Kakao". At large text the label stops at the 1.5 cap and the container grows so the label stays within a third of its height. It is the same in dark mode. It is the screen's one coloured action (a brand-mandated exception to S2). Pressed dims .85; there is no settle, because Kakao's proportions are fixed. Source: developers.kakao.com Kakao Login design guide. |
| CTA band, entry | the sign-in foot: the ProviderButton, 12, a ghost md Button (이메일로 로그인), 12, then a 13/18 secondary links row (이용약관 · 개인정보 처리방침 as ghost links that open the document Sheet), plus the home inset. No hairline. Used only on A1 and the web /login. |
| CTA band helper, danger | the §14 helper line in danger, for a failure that belongs to the action (provider failed, cancelled). One line, never a second action. |
| Brand lockup | the MARK slot from the brand touchpoints doc, stacked (mark over the wordmark, 180 wide) for A1 and the version gate, and horizontal (28 high) for the web pages. No mark is drawn until the owner's new mark exists. |
| Cell, consent | a leading 24 checkbox (the Select-mode box), the label with its [필수]/[선택] prefix 15/22, and a trailing sm ghost 보기 (its own focus stop) that opens the document Sheet. Master row 전체 동의: 600 weight, ticks or clears every item, and shows the mixed state when partly ticked. A 16 gap and a cell hairline separate it from the items. Marketing sits last as its own group, with the (광고) wording. Nothing is pre-ticked. |
| Cell, busy | a trailing 20 spinner in place of the chevron while the Cell's action runs; sibling Cells are inert (.5) until it settles; spoken "처리 중" |
| Sheet, document | title = the document name + a 13/18 secondary version line ("시행일 2026. 9. 1."). Body: 17/24·600 article headings, 15/22 paragraphs, tables as label/value Cell pairs. It scrolls within the 88% cap, the close is sticky, and there is no CTA. |
| EmptyState, full screen | the §9 EmptyState centred in the whole screen with no header, for a blocking gate (the version gate): mark slot, title, body, one primary Button when an action exists (업데이트) |
| Offline strip, no header | on a screen with no header, the strip (350 × 48, §9) pins directly under the status bar, solid, and pushes the content down by its own height |
| Web install row | a 13/18 caption plus a tonal 앱 설치하기 button. Blocked on EXT-8 (store listings); drawn as a marked gap until the apps are listed. |
23. Rulings from the additions board (2026-10-10)
/wireframes/flat-v2-additions draws §18.2–§22.2 (55 specimens, 566 frames, 0 failures) and lists 31 spec conflicts, each with a fix. These three needed a ruling:
| # | Conflict | Ruling |
|---|---|---|
| 1 | ScoreEntry: with the keypad up, the quick row plus tiebreak plus validation reaches 785, over the 88% cap (742) | The sheet opens without the keyboard, showing the quick picks; tapping a field raises the keypad and folds the quick row (§19.3 amended). All seven quick results stay one tap, and the typing worst case is 372 + keypad = 663. The board's alternative (five Choices, no 7-5 / 7-6) would push two common results to typing. |
| 2 | Dark mode has no tertiary text token | Add one at the light theme's tertiary contrast ratio against the dark card; it lands in the colour gate with the other dark values at D/S1 |
| 3 | Search field: 40 r12 in the spec, a 44 pill shipped | The 40 r12 field is drawn inside a 44 target (the hit area stays 44, the visual follows the grammar's radii) |
The board's other 28 conflict rows are recorded fixes that follow the existing rules (text-driven heights exempt, hit slop for sub-44 controls, and so on). They need no ruling.