Approved 2026-10-11 — the build spec until it ships
Library — overlays and feedback
Which surface each situation gets, then the dialog, menu sheet, sheets, toast, empty states, skeletons, offline strip and countdown.
The overlay rule
Pick the surface by what the person is doing. One rule, seven cases; a failure is never a Toast.
S-1
confirming a consequential action
경기
9:41
10월 14일 복식 일정을 취소할까요?참석자 6명에게 알림이 가고, 취소한 일정은 되돌릴 수 없어요
닫기일정 취소
Dialog
S-2
choosing a command for an object
경기
9:41
9월 19일 복식 일정
공유
링크 복사
신고
MenuSheet
S-3
choosing one value
경기
9:41
정렬
최신순
인기순
마감 임박순
Sheet of radio Cells
S-4
entering or editing data
경기
9:41
클럽 이름
강남 라켓클럽
저장
Sheet with × in the title
S-5
reading at length or running a flow
서비스 이용약관
2026년 9월 1일 시행
제1조 (목적)이 약관은 투마가 제공하는 클럽 운영과 일정 관리 서비스의 이용 조건과 절차를 정합니다.
제2조 (보관 기간)회원 정보는 탈퇴 뒤 아래 기간 동안만 보관해요.
회원 정보
탈퇴 후 30일
결제 기록
5년
9:41
Page or takeover
S-6
picking between the consequences of an action
경기
9:41
정기 일정을 어디까지 취소할까요?
이 일정만
이후 일정 모두
MenuSheet with a question title
S-7
being told what happened
경기
9:41
홈클럽경기기록프로필
참석을 취소했어요
되돌리기
Toast
Trigger to surface
관리 gear on the club hub or the session pageMenuSheet (운영 group, then the destructive group) · S-2⋯ on a club, session, venue or profile (three or more rows)MenuSheet · S-2⋯ on a post, a comment, a person row (three or more rows)MenuSheet · S-2a long-press on a messageMenuSheet with the Reactions quick row first · S-2+ on the 클럽 tabMenuSheet: 클럽 만들기 · 초대 코드로 가입 · 클럽 찾기 · S-2일정 삭제, 일정 취소, 클럽 나가기, 로그아웃 (a row of a MenuSheet)Dialog naming the object · S-1참석 취소 (CTA band)Dialog · S-1정기 일정 취소MenuSheet with a question title · S-610월 ▾, 정렬 ▾Sheet of radio Cells · S-3필터FilterSheet · S-4a field's pencilEdit Sheet; dismissing with changes asks a Dialog · S-4점수 입력ScoreEntry Sheet · S-4점수 검토 요청 ›ScoreEntry Sheet in appeal mode, the stored score in the cells · S-4the 검토 Task Cell, a 검토 중 row, the review pushthe Host review Sheet (그대로 유지 · 이 점수로 바꾸기); with several reviews a Sheet of the Match rows opens first · S-4이용약관 보기Document page · S-5입금 확인 saved, 참석 확정Toast · S-7Rules for every overlayone at a time (a Dialog may sit over a Sheet as a guard; a Sheet never sits over a Sheet); backdrop ink α0.4; system back closes the topmost layer, and with the keyboard open the first back hides it; focus returns to the trigger on close; never ask "정말 …?" for anything a 되돌리기 Toast can reverse; never a MenuSheet or Menu for one or two commands: they sit on the row or the page as buttons, and a row's ⋯ follows the same three-or-more rule as every ⋯.
Dialog
A decision that cannot be postponed: confirm a consequential action, or acknowledge a fact that blocks.
10월 14일 복식 일정을 취소할까요?참석자 6명에게 알림이 가고, 취소한 일정은 되돌릴 수 없어요
닫기일정 취소
Ideal destructive: the commit is danger, the larger button
Variants
9월 19일 복식 일정에 참석할까요?참가비 10,000원은 입금 확인 뒤에 확정돼요
닫기참석 확정
Confirm the commit is primary (ball)
신고된 적이 있는 계좌예요입금하기 전에 계좌 주인을 한 번 더 확인해 주세요
입금하기입금 중단
System warning the app warns: the safe action is primary
나갈까요?지금 나가면 입력한 내용이 사라져요
계속하기나가기
Guard leaving unsaved input: the commit is danger (the kit's one FvkLeaveGuard)
수정을 그만둘까요?바꾼 내용은 저장되지 않아요
계속 수정그만두기
Guard, edit an edit form asks the same, in its own words (the kit's one FvkEditGuard)
새 버전으로 업데이트해 주세요이 버전은 더 이상 쓸 수 없어요확인
One action a blocking acknowledgement; never a Toast
위치 정보 이용 동의를 철회할까요?철회해도 동의 관리에서 언제든 다시 동의할 수 있어요
닫기철회
Consent two equal choices: one size, one shape, one colour (equal)
States
10월 14일 복식 일정을 취소할까요?참석자 6명에게 알림이 가고, 취소한 일정은 되돌릴 수 없어요
닫기취소 중…일정 취소
Committing the commit shows its loading state; both are inert
Dark
10월 14일 복식 일정을 취소할까요?참석자 6명에게 알림이 가고, 취소한 일정은 되돌릴 수 없어요
닫기일정 취소
Dark the card surface; danger takes ink text
From 600 pt
10월 14일 복식 일정을 취소할까요?참석자 6명에게 알림이 가고, 취소한 일정은 되돌릴 수 없어요
닫기일정 취소
Wide 560 at most, centred; below that the content width
200% text
10월 14일 복식 일정을 취소할까요?참석자 6명에게 알림이 가고, 취소한 일정은 되돌릴 수 없어요
닫기일정 취소
200% the pair stacks, the commit on top
English
Leave?What you entered will be lost if you leave
Keep goingLeave
English buttons named by action
Anatomycentred · width min(content width, 560): 350 on a phone, 560 from 600 pt up · r16 · a 1px $hairlineEdge edge in both themes (1.5:1 or more against the dimmed page in dark; the fill alone is 1.2 or less) · pad 20 · title 17/24·600 ink · 8 · body 16/24 $textSecondary (two lines at 100%) · 20 · the button pair: two lg Buttons, the tonal one third and the primary or danger two thirds of the line, gap 12, so the committing button is the larger and sits on the right (thumb side). Min height 192. Backdrop ink α0.4 (dark: black α0.6); the backdrop does not dismiss.VariantsConfirm (primary commit) · Destructive (a confirm the person started: danger commit) · System warning (the safe action is primary, the risky one tonal) · Guard ("나갈까요?": 계속 작성 and 나가기; an edit form's "수정을 그만둘까요?": 계속 수정 and 그만두기; the kit has one of each, FvkLeaveGuard and FvkEditGuard) · Consent (revoking a purpose, or granting one from its text: two tonal choices of equal size, shape and colour, neither primary nor danger) · One action (title, body, one full-width primary 확인).Contentthe title names the object and the consequence; the body is one consequence sentence. Buttons are named by action; a pure dismissal is 닫기; never a bare 취소 or 확인 that could mean the person's task. A consent dialog gives its two choices equal size, shape and colour.Accessibilityalertdialog on web, aria-modal on iOS; the title is the first element; initial focus on the tonal (least destructive) button so Enter does not delete; Escape and system back act as the tonal button; focus returns to the trigger.Motionthe platform fade, no scale; reduced motion: the platform decides.Replaces in codeconfirm-sheet.tsx (ConfirmProvider, useConfirm: the API stays), native Alert.alert, ModalPanel placement="center" used as a confirm.
MenuSheet
The commands for one object, from a header gear or ⋯, a row ⋯, a long-press, or +. The generic sheet for any object's actions.
9월 19일 복식 일정
공유
링크 복사
캘린더에 추가
신고
Ideal object menu: one group, then the destructive group, no titles
Variants
정기 일정을 어디까지 취소할까요?
이 일정만
이후 일정 모두
Question title the consequence rows of an action just chosen
김민준내일 비 오면 어떻게 할까요?
👍❤️👏🔥😂🎾
답장
복사
신고
With Reactions quick row first; the title shows the message
Tool menu (관리), one registry
강남 라켓클럽
운영
멤버 관리
2
일정 관리
회비 관리
4
출석 현황
신고 관리
1
설정
테니스장 관리
클럽 대항전
성장 분석
클럽 알림
클럽 설정
클럽 나가기
Officials every door in its two groups, which get titles; counts ride the Cells; leaving is last
강남 라켓클럽
운영
멤버 관리
일정 관리
회비 관리
출석 현황
신고 관리
설정
테니스장 관리
클럽 대항전
성장 분석
클럽 알림
클럽 설정
클럽 나가기
Nothing waits no Tag on a Cell, and none on the gear
강남 라켓클럽
운영
일정 관리
설정
클럽 알림
클럽 나가기
Scheduler only one group, so no title; leaving sits last
강남 라켓클럽
Loading the title shows at once; one skeleton at the Cell pitch
강남 라켓클럽오프라인이에요. 건수는 연결되면 보여요
운영
멤버 관리
일정 관리
회비 관리
출석 현황
신고 관리
설정
테니스장 관리
클럽 대항전
성장 분석
클럽 알림
클럽 설정
클럽 나가기
Offline the doors stay; the counts wait for the connection
9월 19일 (토) 09:00
일정 수정
참석자 관리
경기 규칙
대진 짜기
참석 확인
3
참가비 관리
3
일정 끝내기
일정 취소
일정 삭제
Session, host the session page's gear, a second registry, titled with the day and the start time (the page names the place): 일정 수정 first with no chevron, 참석 확인's count neutral, the ends last
9월 19일 (토) 09:00
일정 수정
참석자 관리
경기 규칙
대진 짜기
참석 확인
3
호스트 관리
2
참가비 관리
3
일정 끝내기
일정 취소
일정 삭제
Session, applicants the host of a session that takes applicants also sees 호스트 관리, the layout-dashboard glyph, with the applicants waiting
States
9월 19일 복식 일정
공유
링크 복사
캘린더에 추가
신고
Pressed row .98 and .80; a destructive row opens its Dialog
From 600 pt
강남 라켓클럽
공유
링크 복사
캘린더에 추가
신고
Anchored Menu right-aligned to the rail, 4 below the trigger, 240 wide
Dark
9월 19일 복식 일정
공유
링크 복사
캘린더에 추가
신고
Dark destructive labels use the light danger
English
Sep 19 Doubles
Share
Copy link
Add to calendar
Report
English row labels are short verbs
Anatomya bottom Sheet (width min(frame, 640), as every Sheet): top radius 16 · pad 20 · title row 48: the object's name 17/24·600 (wraps, never cut) + × (IconButton) · body of groups of Cells 48: each row is one Cell (command, or destructive in the last group): leading 20 icon optional and consistent within one sheet, label 15/22·400 ink, a 16 chevron only on a Cell that opens another screen, a count Tag before the chevron on a task Cell · cell hairline #E5E5E5 inside a group · 24 between groups · a group title (13/18·600 $textSecondary, 8 below) only when the sheet has two or more non-destructive groups; a single group and the destructive group are untitled · the destructive group is last, labels in $error, each row opening its own Dialog · no CTA band · opens at content height, cap 88%, scrolls only beyond it · bottom padding 20 + the home inset.Anchored Menuat 600 pt and up the same rows render as the anchored Menu: right-aligned to the rail 4 below the trigger, 240 wide, r12, 1px #D4D4D4 edge, no shadow, rows 48 (the same Cell inset to 16, its hairline full width; glyph 20 + 15/22), the destructive rows after one cell hairline, scrolling past the available height. On phones there is no anchored Menu.Contentrow labels are 2–5 syllable verbs or nouns (공유, 링크 복사, 수정, 신고, 삭제). Three or more rows: with one or two commands they sit on the row as buttons or its own pencil, never in a MenuSheet (a court row with two commands, 홈 코트 and remove, draws a radio ring and a remove control on the row, not a ⋯). Every destructive row asks a Dialog that names the object. Question title (S-6): at most three consequence rows, no group titles.Tool menu (관리)task Cells with count Tags, the 운영 group first and the destructive group (클럽 나가기) last. The doors, their order and their groups are registries of the kit, the club's (FVK_ADMIN_DOORS and FVK_ADMIN_GROUPS: the hub's gear, the reports entry and the forms board's 관리 page) and the session page's (FVK_SESSION_DOORS and FVK_SESSION_GROUPS: its ends last, each asking its Dialog), both drawn by FvkAdminSheet; a role sees only the doors its flags reach, and a group left empty is not drawn. The session’s sheet is titled with its day and start time (“9월 19일 (토) 09:00”: the page names the place, and a session named on another page is its place and its day, never a name), every row carries its leading 20 icon, the code’s where it has one (host-menu-actions.ts: users, scroll-text, layout-dashboard, clipboard-check, flag, calendar-x, trash-2).Accessibilitynative Modal, aria-modal on iOS; the title heading is the first element; close is "닫기"; the scrim is hidden. From 600 pt: trigger with aria-haspopup="menu" and aria-expanded, menu and menuitem, arrow keys, Escape closes and returns focus. The trigger is named by its object ("김민준 메뉴", "더보기"). A long-press has an accessibility action that opens the same sheet.Motionrise 300 ms (sheetIn), exit 200 ms; iOS: a swipe down on the title row dismisses (40 pt or 500 pt/s); the Menu fades in 150 ms and out 100 ms. Reduced motion: a 100–150 ms fade replaces the rise.Replaces in codeheader-actions-menu.tsx (the shadowed anchored dropdown becomes the presentation switch: a sheet below 600 pt, the anchored Menu from 600 pt), the local row menus (post-row.tsx, member-kebab-modals.tsx), host-menu-actions.ts, the name ActionSheet. A two-row kebab such as edit-delete-action-modal.tsx becomes buttons or the row's own pencil.
Sheet
The one bottom overlay for a scoped task on the page behind it: pick a value, edit a field, filter, enter a score.
강남 테니스 클럽에 초대
초대 코드
3f9a1c7e
초대 링크
twomore.app/join-club?code=3f9a1c7e
공유
Ideal title row with ×, body, one lg primary footer
Title row
점수 검토 요청2라운드 1번 코트
1세트
김
이
김민준 (나)이서연
6
박
최
박지훈최유나
4
검토 요청
Sub a 48 row with a sub, the object’s excerpt (the match of a score sheet); no description, the title asks
Club invite
클럽 설정
9:41
강남 테니스 클럽에 초대
초대 코드
3f9a1c7e
초대 링크
twomore.app/join-club?code=3f9a1c7e
공유
초대 코드를 복사했어요
Copied the code’s copy button confirms with a Toast over the Sheet, as the account number does
Long content
회비
9:41
월
전체
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
12월
완료
Capped at 88% the body shrinks and scrolls; the footer stays
From 600 pt
강남 테니스 클럽에 초대
초대 코드
3f9a1c7e
초대 링크
twomore.app/join-club?code=3f9a1c7e
공유
Wide 640 at most, centred on the frame
Anatomytop radius 16 · pad 20 · title row 48: the title 17/24·600 (the field or object name, wraps) with the × IconButton at the right (its glyph's right edge on the rail); a `sub` (13/18 secondary: the match, the club) keeps the row at 48 (3 + 24 + 18 + 3) · no description line, the title asks (the owner, 2026-10-20; 1.16) · content in a scroll body · footer = the CTA band geometry when the Sheet has a primary: 16 above, the lg Button (and, only where 1.16 keeps one, its helper, 8 above it), then max(16, inset) below: 34 on a phone with a home indicator, 16 with the keyboard up or no inset; a Sheet with no footer keeps 20 plus the inset under its last row. Width min(frame, 640): the frame’s width on a phone, 640 centred from 700 pt up. No grabber and no drag handle. Auto-height, capped at 88%; the body shrinks and scrolls past the cap; the footer is a normal sibling, never absolute. The keyboard lifts the Sheet (and with an input the footer stays above it). Backdrop ink α0.4.VariantsValue picker · Edit Sheet · FilterSheet · ScoreEntry · the Host review sheet · Date and time picker · Time range wheel · the Club invite Sheet (FvkClubInviteSheet: the club in the title, the 초대 코드 as a copy Cell, the link a plain fact, 공유하기 the one primary).Statesrising · open · dismissing. The scrim, ×, system back and (iOS) a swipe down on the title row all dismiss; with unsaved input they ask the guard Dialog instead.Contentthe title names the field or object, never "수정" alone, and it asks: a Sheet has no description line (1.16). `sub` is the object’s excerpt (the match, the club) and nothing else.Accessibilitynative Modal, aria-modal; the title heading is the first element and the first stop; close is "닫기"; the scrim is hidden; focus returns to the trigger.Motionrise 300 ms (sheetIn: a 48 pt rise over the platform's fade), exit 200 ms; reduced motion: a 100–150 ms fade.Replaces in codemodal-panel.tsx (ModalPanel placement="bottom"; the Frame's close × is always present), chip-picker-panel.tsx.
Sheet, value picker
Choose one value from a list: a Choice ▾, 정렬, a month, a level.
정렬
최신순
인기순
마감 임박순
Ideal radio Cells: the chosen row is 600 with a 20 check
공개 범위
모두에게
링크를 아는 사람도 볼 수 있어요
클럽 멤버에게만
멤버만 볼 수 있어요
Two-line options each option's meaning in a 13/18 line, min 68
월
전체
10월
9월
8월
7월
6월
A month 전체 first when empty is allowed; newest month next
States
정렬
최신순
인기순
마감 임박순
Pressed a tap selects and closes; no footer
강남 라켓클럽
저장하지 못했어요. 다시 시도해 주세요
전체
클럽 소식을 모두 푸시로 받아요
공지만
클럽 공지만 푸시로 받아요
끔
클럽 소식은 푸시로 받지 않아요
A choice that did not save the Sheet stays open; one danger line is the body’s first line
정렬
최신순
인기순
마감 임박순
Dark the card surface; the check stays ink
Anatomya Sheet (width min(frame, 640)) titled with the field name. Radio Cells 48: the chosen row's label turns 600 with a 20 ink check at the end (no fill, no tint). A Two-line Cell (min 68, 13/18·500 secondary second line) gives each option its meaning (공개 범위). No footer: a tap selects and closes. Order: "전체" first when empty is allowed; months newest first with the current month checked, every month since dues began reachable.A choice that did not saveThe Sheet stays open (the caller does not close it) and the body’s first line is one danger Helper line on the rail: the alert glyph and the message, a foot line, 8 above the rows, spoken as an alert when it appears. It is the Sheet’s one support line; the title’s sub line keeps saying what the choice is for. Opening the Sheet again clears it.Contentoption labels are values, not sentences.Accessibilityradio group; the title names the field; the trigger Choice ▾ reads its current value.Replaces in codeDropdownChip panels and RangeChip panels (dropdown-chip.tsx, range-chip.tsx).
People picker Sheet
Pick several people from a known list inside a Sheet: the draw’s 참석 picks who plays from the attendees.
참석
고른 사람8명
김민준
이서연
박지훈
최유나
이름으로 검색
참석10명
김민준
이서연
박지훈
최유나
정
정우진
한소희
강도윤
윤
윤채원
임하준
오예린
8명 선택
Ideal eight of ten picked: the faces on top, the search, the check rows; the footer counts the picked
참석
이름으로 검색
참석10명
김민준
이서연
박지훈
최유나
정
정우진
한소희
강도윤
윤
윤채원
임하준
오예린
선택
None picked no strip; the footer waits, soft, and reads 선택
참석
고른 사람3명
김민준
이서연
박지훈
윤
취소
참석2명
강도윤
윤
윤채원
3명 선택
Searching the search narrows the rows; the picked faces and the count stay as they were
Dark · 200% text
참석
고른 사람8명
김민준
이서연
박지훈
최유나
이름으로 검색
참석10명
김민준
이서연
박지훈
최유나
정
정우진
한소희
강도윤
윤
윤채원
임하준
오예린
8명 선택
Dark the card surface; the check stays ink
참석
고른 사람8명
김민준
이서연
박지훈
최유나
이름으로 검색
참석10명
김민준
이서연
박지훈
최유나
정
정우진
한소희
강도윤
윤
윤채원
임하준
오예린
8명 선택
200% text the rows keep their pitch and the names wrap; the list scrolls under the pinned footer
Anatomya Sheet (the second step of 참석, × to close) titled with the thing it picks for, body without side padding: 고른 사람 head 40 with the count and the Shelf of the picked faces (the cell preset: 76, the face and the name, the first on the rail; four are drawn in a frame, the strip scrolls for more and a name is not drawn half past the rail) · 24 · the search field on the rail (placeholder 이름으로 찾기) · 20 · the list head 40 (the list’s name and the number shown) · picker PersonRows 64 with the check square 24 trailing, no line 2 · the footer: one lg primary, N명 선택 (the count is the picked, live), 선택 and soft at zero. The rows scroll under the pinned footer.BehaviourA tap anywhere on a row ticks it and the face joins the strip; a tap on a face in the strip unticks it. The search narrows the rows and never the picked set. The Sheet writes nothing until the footer is pressed, and an online write only (offline the footer is disabled).ContentNames are never cut: they wrap. The strip shows the picked faces in the order of the list; the rows are in the list’s order. A row that cannot be picked is not drawn.Accessibilitythe list is a group of checkboxes: "이서연, 선택됨"; the strip is a list named "고른 사람, 8명"; the footer reads its count.One piecethe people picker of 단톡방 만들기 and 멤버 초대 (a page, library § 1.22) in a Sheet; FvkPeoplePickerSheet is the Sheet, the same rows, strip and field. A page that picks people (the form grammar) and a Sheet that picks people (this) differ only in the shell and the footer.
Edit Sheet
Edit one answer in place: opened by a Cell's pencil.
클럽 이름
강남 라켓클럽
저장
Ideal × in the title and one 저장, soft until the value differs
States
클럽 이름
강남 라켓클럽 일요반
저장
Dirty the value differs: 저장 turns ball
클럽 설정
9:41
클럽 이름
강남 라켓클럽 일요반
저장
ㅂㅈㄷㄱㅅㅛㅕㅑㅐㅔ
ㅁㄴㅇㄹㅎㅗㅓㅏㅣ
⇧ㅋㅌㅊㅍㅠㅜㅡ⌫
123스페이스완료
Keyboard up the Sheet lifts; 저장 stays above the keys
클럽 이름
강
클럽 이름은 2자 이상 입력해 주세요
저장
Validation the line sits above 저장, in danger
클럽 이름
강남 라켓클럽 일요반
저장 중…저장
Saving 저장 shows 저장 중… and the sheet stays
Dismissing and saved
클럽 설정
9:41
클럽 이름
강남 라켓클럽 일요반
저장
수정을 그만둘까요?바꾼 내용은 저장되지 않아요
계속 수정그만두기
Guard with changes, ×, the scrim or back ask a Dialog
저장했어요
Saved the Sheet closes and a result Toast says so
Variants
참가비
10,000
원
저장
Number a Number field with its unit
종목
단식복식혼합 복식
저장
Choice a Choice set without its own label
지역
지역
서울 강남구
저장
Picker a Picker Cell that opens its own sheet
Anatomya Sheet titled with the field name and × in the title row; the control (an Input, Number field, Choice set or picker, without its own label); a footer with one 저장 (primary lg). 저장 is soft-disabled until the value differs from the saved one. Inline validation sits above 저장 in danger. With the keyboard up the Sheet lifts and 저장 stays visible above it.Statesclean (× closes silently) · dirty (×, the scrim, back or a swipe asks the guard Dialog "수정을 그만둘까요?") · saving (저장 loading) · saved (the Sheet closes and a result Toast says "저장했어요").Contentone answer per sheet. An answer may have parts that belong together (a date and its times, a range, an account): they share the Sheet and its one 저장, because the write is one. Two unrelated answers are two Sheets. Example: 일시 in 일정 만들기 is the date with its start and end times in one Sheet.Accessibilityas Sheet; the pencil Cell is spoken "{field}, 수정".Replaces in codeedit-field-modal.tsx (EditControlModal, EditFieldModal: the 취소 · 저장 pair and the missing ×).
FilterSheet
Narrow a list by facets in one Sheet with the result count live on the button.
필터
종류
전체테니스장연습장샵
코트 바닥
하드클레이잔디
시설
야간 조명주차샤워실
초기화결과 26개 보기
Ideal filters active: 초기화 and the live count
필터
종류
전체테니스장연습장샵
코트 바닥
하드클레이잔디
시설
야간 조명주차샤워실
초기화결과 128개 보기
No filter 초기화 is soft-disabled when nothing is active
필터
종류
전체테니스장연습장샵
코트 바닥
하드클레이잔디
시설
야간 조명주차샤워실
초기화결과 0개 보기
Zero results the button stays pressable; it shows the empty state
Kinds
필터
날짜
이번 주
지역
서울 강남구 외 1
코트 바닥
하드클레이잔디
수준
NTRP 3.0–4.0
3.0
참가비
무료~20,000원
무료
초기화결과 42개 보기
All four kinds a Picker Cell, a Choice set, a level band and a fee band: the forms' own pieces
Date picker
날짜
오늘내일이번 주이번 달
2026년 9월
일월화수목금토
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
적용
A week opened from the 날짜 Cell: the presets over the Calendar, 이번 주 drawn as a range
날짜
오늘내일이번 주이번 달
2026년 9월
일월화수목금토
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
적용
A day a tapped day is selected and no preset is
Trigger
필터 2하드
야간 조명정렬
On the list a Choice with the count, then a removable one per filter
Dark
필터
종류
전체테니스장연습장샵
코트 바닥
하드클레이잔디
시설
야간 조명주차샤워실
초기화결과 26개 보기
Dark Choices keep the ink fill
English
Filter
Type
AllCourts
PracticeShops
Surface
HardClayGrass
ResetShow 26 results
English the button reads the live count
Anatomya Sheet titled "필터", built only from the shared field pieces so a filter reads as a form does (the owner’s page review, 2026-10-28). Groups in the caller’s order, which follows one grammar: 보기 first when the page has one (a Choice set of views: 가까운 순 · 저장됨, 전체 · 저장됨); then where and when, Picker Cells stacked full-bleed with no gap (날짜, with the value as it reads, 이번 주 or 10월 30일 (목), 전체 unset; 지역, 서울 서초구, 서울 강남구 외 1 for several districts, 전국 unset; each opens its picker); then what, a Choice set per facet (legend 13/18·600, Choices 44 tall, plain labels: no count inside a Choice, the live count is on the button); then how much, a Field with a band, the forms’ own Slider with its readout and Stepper (수준 the level band over the five NTRP tiers, 참가비 the fee band over 무료 to 100,000원). Fields stand 32 apart. The footer is a pair: 초기화 (tonal, one third of the line; soft-disabled when nothing is active) + "결과 n개 보기" (primary, two thirds, the count live). Trigger: a Choice ▾ with the active count ("필터 2", ink-filled when active) followed by a removable Choice for each active filter. Sort is its own Choice and its own value-picker Sheet.Statesno filter · filters active · zero results ("결과 0개 보기" stays pressable and shows the filtered-empty EmptyState). Group titles are nouns (종류, 코트 바닥, 시설).Accessibilityeach Choice set is a labelled group of toggle buttons or radios; a Picker Cell is spoken as its field and value ("지역, 서울 서초구"); a band is a slider with its Stepper; the live count is the button's label.Date pickerFvkDateFilterSheet, opened from a filter’s 날짜 Cell: a Sheet titled 날짜 (× in the title row), the four presets (오늘, 내일, 이번 주, 이번 달) as a single Choice set, the Calendar under them with the selection (a day selected, a week or a month as a range), and one 적용 (primary lg). It is the app’s DateSelection: a tapped day sets a day, the presets set a day, the week (Monday to Sunday) or the month.Replaces in codeexpandable-filter-bar.tsx (the inline expandable panel) with its DropdownChip / RangeChip contents (discover-filters.tsx), and match-history-scope-sheet.tsx.
Document page
A legal or long document read at length: 이용약관, 개인정보 처리방침.
서비스 이용약관
2026년 9월 1일 시행
제1조 (목적)이 약관은 투마가 제공하는 클럽 운영과 일정 관리 서비스의 이용 조건과 절차를 정합니다.
제2조 (보관 기간)회원 정보는 탈퇴 뒤 아래 기간 동안만 보관해요.
회원 정보
탈퇴 후 30일
결제 기록
5년
9:41
Ideal a pushed page: version line, article headings, paragraphs
서비스 이용약관
9:41
Loading a page skeleton while the text arrives
서비스 이용약관
2026년 9월 1일 시행
제1조 (목적)이 약관은 투마가 제공하는 클럽 운영과 일정 관리 서비스의 이용 조건과 절차를 정합니다.
제2조 (보관 기간)회원 정보는 탈퇴 뒤 아래 기간 동안만 보관해요.
회원 정보
탈퇴 후 30일
결제 기록
5년
9:41
오프라인이에요
다시 시도
Offline the strip over the cached text
Anatomya pushed page: the pushed Header (back at the rail, the document name 17/24·600 at x 56, one line) · a caption row with the version line ("시행일 2026. 9. 1.", 13/18·500 secondary) · article headings 17/24·600 ink · paragraphs 16/24·400 ink · tables as label / value Cell pairs. It scrolls; there is no CTA band. Reading never ticks a consent; back returns to the screen that linked it.Accessibilitythe document name is the h1, articles are h2; reading order is document order.Motionthe platform push; none inside.Replaces in codethe document Sheet; policy-document-page.tsx is the renderer (terms-screen.tsx, privacy-screen.tsx).
Toast
Report the result of what the person just did, or offer to undo it. Results and undo only: never an error, never the only copy of something.
홈클럽경기기록프로필
일정을 만들었어요
Ideal a result: ink surface, no glyph, 16 above the tab bar
홈클럽경기기록프로필
참석을 취소했어요
되돌리기
With 되돌리기 an action: a real verb in ball, and a ×
Where it sits
참석하기
입금을 확인했어요
Above a CTA band 16 above the band
홈클럽경기기록프로필
일정
일정을 만들었어요
Above a FAB 16 above the FAB when one is on screen
게시글
9:41
댓글을 남겼어요
ㅂㅈㄷㄱㅅㅛㅕㅑㅐㅔ
ㅁㄴㅇㄹㅎㅗㅓㅏㅣ
⇧ㅋㅌㅊㅍㅠㅜㅡ⌫
123스페이스완료
Above the keyboard 16 above the taller of the bar and the keys
Dark
홈클럽경기기록프로필
참석을 취소했어요
되돌리기
Dark the surface flips to #DFE4DC, text and action in ink
200% text
참석하기
참석을 취소했어요
되돌리기
200% the action drops under the text
English
HomeClubsGamesRecordsProfile
Attendance cancelled
Undo
English state first, then one verb
A failure is never a Toast
입금이 아직 확인되지 않았어요
입금 다시 확인
At the button helper states the consequence, one verb
입금 확인에 실패했어요계좌 정보를 확인한 뒤 다시 시도해 주세요
다시 시도
Notice stays until resolved; the action is one verb
결제가 거절됐어요카드를 확인하고 다시 시도해 주세요
닫기다시 결제
Dialog when the failure blocks the person
Anatomyinverse surface (ink in light; in dark it flips to a light surface, #DFE4DC, as Material’s does) · r12 · min 48 · pad 12 16 · text 16/24 in the inverse text colour (white; ink in dark), two lines at 100% and up to four at large text, never truncated · no glyph · no shadow. An optional action at the right: a real verb, 15/22·600 in ball on ink (ink on the light surface in dark), under the text from 1.5× text, as the one text action that is a command (the snackbar convention; kind=command is legal only here) with its 44 target. A toast with an action also carries a × (24 glyph in a 48 target, its edge on the 16 pad). Position: 16 above the tallest layer of the chrome stack under it: the tab bar (64 + inset), the CTA band (16 + 52 + max(16, inset) = 102, taller with a helper or at large text), the keyboard, or a FAB (the FAB sits 16 above the tab bar, the Toast 16 above the FAB); 16 above the home inset when there is none; centred on the content width.Timinga result stays 4 s; a toast with an action stays 8 s. On Android the larger of that and AccessibilityInfo.getRecommendedTimeoutMillis. The timer pauses while the toast is touched and while a screen reader's focus is on it. One at a time: a new toast replaces the old. A tap on a toast without an action dismisses it; a swipe down (past 40 pt or 500 pt/s) dismisses any toast.Contentstate or consequence first, then one verb: "참석을 취소했어요" + 되돌리기. One sentence, about 20 syllables a line (15 beside an action); a toast that needs more is a Notice. 닫기 is a dismissal, not an action.Failuresa failure goes next to the button (the CTA helper in danger), in a Notice that stays, or in a Dialog when it blocks. The same copy rule: the consequence first, then one verb.Accessibilitya live region without focus: on iOS an explicit announcement (queued), on Android aria-live="polite", on web role="status"; the message is read, then the action label; the action is its own stop and takes no focus on insertion.Motionenter 250 ms (reveal: a 12 rise and a fade), leave a 100 ms fade; a drag follows the finger and releases on a spring. Reduced motion: a 150 ms fade in and a 100 ms fade out, no rise.Replaces in codetoast-host.tsx and toast.store.ts (a shadowed pill with a status glyph). createMutationHook (create-mutation-hook.ts, onError) gets an errorSurface option (helper, notice, dialog) in place of its single error toast.
EmptyState
Nothing here, a failure, no network, a wall, or a missing record: one calm, centred explanation and, only when one exists, one way forward.
아직 일정이 없어요일정을 만들면 클럽 멤버에게 알림이 가요
일정 만들기
Ideal first-run: the state in the title, the next step in the body
Kinds
조건에 맞는 일정이 없어요필터를 바꿔서 다시 찾아 보세요
필터 초기화
Filtered the filters hide everything; the Button clears them, named for where it goes: 전체 보기 for one value, 필터 초기화 for several
로그인이 필요해요로그인하면 내 일정을 볼 수 있어요
로그인
Permission a bare 48 lock; a Button only if it resolves it
일정을 불러오지 못했어요연결을 확인하고 다시 시도해 주세요
다시 시도
Error the kit writes it: what failed in 해요체 from `what`, the next step in the body, one 다시 시도
오프라인이에요연결되면 자동으로 다시 불러와요
다시 시도
Offline only when nothing is cached, otherwise the strip; the kit writes all three lines
찾을 수 없는 일정이에요삭제됐거나 볼 수 없는 일정이에요
목록으로
Not found the title says so; the body names the way out
총무만 볼 수 있어요회비 내역은 클럽 총무와 운영진에게만 보여요
Role wall no Button: the header back is the way out
Placement
게시글
아직 게시글이 없어요첫 글을 남겨 보세요
글쓰기
Compact inside a section: 24 from the top, a 64 disc
DABUSI
새 버전으로 업데이트해 주세요더 안정적인 앱을 쓰려면 업데이트가 필요해요
업데이트
9:41
Full screen a blocking gate with one primary Button
Dark
일정을 불러오지 못했어요연결을 확인하고 다시 시도해 주세요
다시 시도
Dark the disc is gray100 on the dark canvas
200% text
아직 일정이 없어요일정을 만들면 클럽 멤버에게 알림이 가요
일정 만들기
200% the block grows and the Button wraps
English
No sessions yetMembers get a notice when you add one
Add a session
English never an apology first
Couldn’t load sessionsCheck your connection and try again
Retry
English, error Couldn’t load {what}, then the next step and Retry: the kit’s lines
You’re offlineWe’ll load it once you’re back online
Retry
English, offline You’re offline, and it loads on its own once the connection is back
Anatomycentred in the content area · 64 from the top · a disc 80 gray100 with a 32 glyph in $textSecondary · 16 · title 17/24·600 ink · 8 · body 16/24 $textSecondary (two lines at most) · 24 · one md Button (tonal; primary when the empty state is the screen's one action). Compact placement inside a section: 24 from the top, disc 64. No card, no illustration, no mint.Permissionthe page is real but this viewer may not see it. A bare 48 lock glyph in secondary → 16 → title → 8 → body (one line naming what signing in unlocks) → 24 → the Button, drawn only when an action resolves the wall (로그인). A role wall the viewer cannot resolve has no Button; the header back is the way out. Never on a hub.Error and offlineError: an alert glyph; the title says what failed in 해요체 ("{what}을 불러오지 못했어요", the particle chosen by the last syllable’s 받침 in one kit helper: 일정을, 전적을, 순위를), the body gives the next step, one 다시 시도; never a title like "오류". Offline: a wifi-off glyph, shown only when nothing is cached; with cached rows the Offline strip appears instead. Offline trumps error. The kit writes these lines: kind offline draws 오프라인이에요, 연결되면 자동으로 다시 불러와요 and 다시 시도; kind error draws 연결을 확인하고 다시 시도해 주세요 and 다시 시도, and takes `what` (the object that failed: 일정, 전적, 참가비 내역) for its title; English “Couldn’t load {what}”, “You’re offline”, “We’ll load it once you’re back online”, “Retry”. A page passes the kind and never types the lines; it may still pass its own title, body or action, and action={null} draws no Button (a page whose CTA band carries the retry).Full screena blocking gate (the version gate) owns the whole screen with no header: the brand lockup slot, title, body and one primary Button when an action exists.Accessibilityone unit for a screen reader: the glyph hidden, the title (h2; h1 on a full-screen gate), the body, then the Button.Motiona 150 ms cross-fade from the skeleton; reduced motion: instant.Replaces in codeempty-state.tsx (mint disc, variant full and compact), query-error-state.tsx, offline-empty-state.tsx, not-found-state.tsx, update-required-screen.tsx.
Skeleton
The loading shape of each surface at its exact pitch, so nothing jumps when data arrives.
Media row a 56 square and two bars: pitch 80
Person row a 40 circle and two bars: pitch 68
Cell a label bar and a value bar: 48
Field a label bar and a 48 box: 70 per field
Block 144, r16: bars at 40%, 24% and 60%
Strip three label and figure bars: 80
Chart a 160 gray100 block
Hero a 24/32 bar at 70% and three fact bars
Dark
Dark gray100 pulsing .6 to .9; static at .75
Anatomyshapes, all gray100: media row (56 square r8 + a 17/24 bar at 60% + a 15/22 bar at 40%) · person row (40 circle + two bars) · Cell (label 40% + value 20%) · Block 144 r16 (bars 40% / 24% / 60% for eyebrow, figure, meta) · Strip 80 · Chart 160 · hero (a 24/32 bar at 70% + three 17/24 bars) · Field (70). Bars take the height of the type's cap line. The pulse is one shared value per screen: opacity .6 to .9 over an 800 ms half-cycle.Timinga skeleton appears only after 300 ms of loading; once shown it stays at least 500 ms; cached data is shown instead of a skeleton; past 10 s it gives way to a determinate indicator with an estimate where the work reports progress (uploads), and otherwise to the slow-load Notice with 다시 시도. The pulse stops after 5 s at the static frame.Accessibilitythe container carries aria-busy and one label ("불러오는 중"); the shapes are hidden; nothing is announced.Motionskeleton to content is a 150 ms cross-fade; reduced motion: static .75 and an instant swap.Replaces in codeskeleton.tsx (Skeleton, SkeletonCard, SkeletonHero), skeleton-flat-page.tsx, shimmer.ts.
Offline strip
Connectivity is lost while content is on screen: a quiet notice, not an error. It is the app's only offline message, drawn once at the root; a cached page stays under it and the app disables its writes.
경기
9:41
오프라인이에요
다시 시도
Ideal the frame's one place: under the status bar, above the header; it pushes the screen down
9:41
오프라인이에요
다시 시도
Over a MediaBand the same place: the band starts under the strip, which is solid
다붓이에 오신 걸 환영해요
9:41
오프라인이에요
다시 시도
No header the same place, directly under the status bar, solid
Queued writes
오프라인이에요3건 대기 중
다시 시도
Queued writes waiting in the durable queue are counted in the body: N건 대기 중 (score entry, RSVP, attendance, dues, messages); nothing is drawn for zero
Dark
오프라인이에요
다시 시도
Dark gray100 on the dark canvas
200% text
오프라인이에요
다시 시도
200% the action drops under the text
Anatomya neutral one-line Notice, 350 × 48 (min; it grows with text), r12: a 20 wifi-off glyph in secondary + 12 + the title "오프라인이에요" 15/22·600 ink + 다시 시도 as a ghost sm Button at the right (a command is a Button, owner review 6 D1). Plain page: pinned under the header, pushing the content down by its height. Over a MediaBand: attached to the header's bottom edge in the header's own layer, solid (gray100 fill, ink text), never translucent, pushes no content. No header: pinned directly under the status bar, solid, pushing the content down by its own height.Statesabsent · present · present with writes waiting (the body names how many). It appears the moment the device reports no route and goes the moment one returns; 다시 시도 forces a retry. Lists keep their cached rows and the app disables their writes; the offline EmptyState appears only with nothing cached, and for staff data (a hidden-class page) even with a warm cache.Accessibilityrole="status", announced once ("오프라인이에요"); the action is its own stop.Motionit leaves as a Notice dismisses (opacity, then a 150 ms height collapse); reduced motion: instant.Replaces in codenetwork-banner.tsx (NetworkBanner).
CountdownNotice
The clock on a held seat: how long the transfer has, and what happens at zero.
미납09:41 남음카카오뱅크 3333-01-1234567 박총무
Ideal running: a warning Notice with the clock and the account
시간이 지나 자리가 풀렸어요
다시 신청하기
Expired danger at zero: the seat is released; one action
Dark
미납09:41 남음카카오뱅크 3333-01-1234567 박총무
Dark the tone at 24% over the canvas, tone-coloured title
200% text
미납09:41 남음카카오뱅크 3333-01-1234567 박총무
200% the glyph scales to 1.5×; the line wraps
Anatomya warning Notice (fill #F5B301, ink text, r12, pad 12 16, on its own 20 rail), the one Notice layout with a `trailing` node: a 20 clock glyph + 12 · title 미납 15/22·600 with the countdown 09:41 남음 15/22·600 tabular at the end of the title’s line · body 16/24 = the account line (70, text-driven). At zero it becomes a danger Notice ($error fill, white text, the alert glyph): "시간이 지나 자리가 풀렸어요", with the action 다시 신청 trailing the title while it fits and under the text when it does not (a 44 target). Dark: the tone at 16% over the dark canvas, the title in the tone's own colour. From 1.5× text the countdown drops under the title.Statesticks every second (the digits do not animate); expired. The account line is the next step; no second action while running.Accessibilitythe spoken name carries the time at the moment of focus ("미납, 9분 41초 남음"); live announcements only at 5:00 and 1:00 (polite). The payment hold is an essential time limit, a server deadline.Motionnone; the expiry swaps instantly.Replaces in codethe payment-hold banner built on useLiveCountdown (use-live-countdown.ts) in the session detail fee flow.
Spinner
Work in progress inside a control: the only place a spinner appears.
저장 중…저장저장 중…저장삭제 중…삭제
In a Button beside the progressive label; ink, white on danger
입금 확인
3명
참가비 정산
영수증
In a busy Cell it replaces the chevron; sibling Cells are inert
Pull to refresh the native control, ink tint, for a person's own pull
Anatomythe platform ActivityIndicator, 20, ink (white on a danger Button); inside a Button beside its progressive label, or in place of a Cell's chevron while its action runs (sibling Cells inert at .5, spoken "처리 중"). It never stands alone on a list or a screen (that is a Skeleton). Pull-to-refresh uses the native control with an ink tint, shown only for a person's own pull.Accessibilitythe control carries aria-busy; the spinner itself is hidden.Motionthe platform's rotation; it continues under reduced motion.Replaces in coderaw ActivityIndicator uses (eight files).
DOM check (scripts/verify-flat-v2-boards.mjs): 110 frames, rail 0, type 0, targets 0, shadow 0, dot 0, ball 0.
Spec (grounding, anatomy)
Overlays and feedback, composed from the shared flat v2 kit.
route · flat-v2-kit-controls.jsxgrounded in 32 source file(s)