Skip to content

TwoMore Design System

TwoMore is a universal Korean tennis club app that removes every hurdle between amateur players and their next match. Built for 총무 (club managers) who book courts, organize sessions, and keep the community playing more tennis.

This design system packages the classic grass-court green, Toss-minimal visual language of TwoMore v2 — its tokens and primitives (colors, type, badges, cards) — for mocks, prototypes, and marketing.

⚠ Not the source of truth. The hand-built *Screen.jsx mockups in ui_kits/mobile/ are illustrative and may lag the app. What actually ships is defined by the real components under packages/…; the authoritative design surface is the code-grounded wireframes system (deployed at /wireframes), whose build hard-fails if it drifts from the components it mirrors. To know what a screen looks like, read the real component — never these mocks. (This distinction exists because trusting a stale mock/doc over the code caused a real drift incident.)

Live wireframes: the code-grounded, config-driven wireframes render under /wireframes on the deployed Cockpit — static (no CDN), mobile-responsive, regenerated from the section configs on every build.


Sources

  • Codebase: github.com/ivorybridge-2025/twomore-v2 (private, main branch)
    • Design tokens: packages/ui/src/tokens.ts
    • Themes: packages/ui/src/themes.ts
    • Fonts: packages/ui/src/fonts.ts
    • Primitives: packages/ui/src/{button,card,text,badge,pill-nav,empty-state,wizard-shell,icon}.tsx
    • Product rules / brand principles: CLAUDE.md
    • Feature surfaces: packages/features/{home,clubs,sessions,records,profile,activity}
  • No Figma file provided. All visual decisions are lifted directly from code.

Product context

TwoMore v2 is a universal app — iOS, Android, and Web from a single codebase (Expo + Next.js + Tamagui). It is 총무-first SaaS for Korean tennis clubs:

  • Mobile app (primary surface) — 5 bottom tabs: 홈 · 클럽 · 경기 · 기록 · 프로필. Toss-style card feed on Home. Korean-first copy throughout.
  • Web app — Same Tamagui primitives rendered via Next.js (scaffold stage; thin today).

Core loops:

  1. Discover — find a 번개 (pickup match) or club session nearby
  2. Commit — RSVP, pay dues, show up
  3. Play — live matchboard with rounds, courts, partners, opponents
  4. Reflect — post-game ELO change, MVP, streaks, momentum

Benchmarks: Toss (토스) for minimal chrome and motion-confirmation; classic grass-court environments for restraint.


Index — what's in this folder

PathPurpose
README.mdYou are here.
SKILL.mdSkill manifest so this folder works as a Claude Code Agent Skill.
colors_and_type.cssAll design tokens as CSS variables + semantic type roles. Import into any HTML.
assets/Logos, icon fallbacks, illustrative placeholders, fonts.
preview/HTML cards that populate the Design System tab (color palettes, type specimens, component specimens).
ui_kits/mobile/Mobile UI kit — React components + index.html click-through of the TwoMore app.
ui_kits/mobile/club-surfaces.htmlConfig-driven club-surfaces wireframe — card + detail per audience (비회원/회원/운영/경기이사) from one CS_CLUB + CS_AUDIENCES config in ClubSurfaces.jsx. Audience is a projection (sections show/hide).

Content Fundamentals

TwoMore speaks Korean first. Copy is quiet, humble, and functional — Toss-school pragmatism with a light warmth. No exclamation stacks, no emoji walls, no marketing-speak.

Voice & tone

  • Humble + helpful. The app narrates what's happening, not what the user should feel.
  • Second person, polite form. Uses -요 / -세요 (informal-polite). Never -ㅂ니다 (too formal) and never -해 (too casual).
  • Terse in cards, warmer in greetings. Card titles are nouns or short declaratives. Empty states and greetings can add one sentence of encouragement.
  • No hype. No "🔥 SUPER WIN!" — just 🔥 3연승 중.

Casing & punctuation

  • No trailing periods in card titles.
  • Numbers beat words: 3연승, 회비 2건 미납, 근처 번개 5개.
  • Use · (middle dot) as a separator in meta rows: 서울 강남 · 19:00–21:00.
  • Use (em dash) to hold a pause; avoid parentheses for emphasis.

Pronouns

  • Prefer 너 / 나 avoidance: use subject-less Korean constructions.
  • When direct address is needed: 당신 is corporate; TwoMore uses implied subject + -요 ending.

Emoji

  • Used sparingly and only as celebration accents in momentum copy: 🔥 (win streak), 🏆 (milestone). Never as UI affordances.
  • Unicode bullet is used for a "LIVE" indicator in-line.

Examples (from the codebase)

  • Greeting: 안녕하세요, {name} — first line; a single meta line below.
  • Pull-to-refresh hint: 당겨서 새로고침
  • Action required: 회비 2건 미납 / 밀린 회비를 납부해주세요
  • Momentum: 🔥 3연승 중 / 기세가 좋아요
  • Empty state: 대진표 준비 중
  • CTA verbs (button labels): 확인하기, 참가 확정하기, 후기 남기기, 번개 둘러보기, 납부하기, 기록 보기

English vs Korean

  • All user-visible strings are Korean (sourced via t() from packages/app/src/config/i18n/).
  • Code, comments, docs, commit messages are English.
  • Mix: product names stay English (TwoMore, ELO, MVP), while verbs and labels stay Korean.

Visual Foundations

Color discipline — neutral-first, icons + text carry meaning

The system uses one base surface (--card on --background) and reserves color for a very narrow job. Do not tint a card, badge, avatar, or icon bubble to differentiate categories. Differentiate with icon + text + neutral instead.

Only these colors carry meaning:

  • --primary (forest green) — primary CTA fill, progress fill, active tab, link. Nothing else.
  • --warning (amber) — dues/action-needed state on a badge, inline warning text, warning dot. Never as decoration.
  • --error (red) — LIVE dot and the LIVE badge only. Destructive confirm button.
  • Everything else — neutral. Category, format, tier, origin, filters, empty states, avatars, icon bubbles: all neutral.

Badge variants — only four: neutral (default), warning (dues/action-needed), live (LIVE dot), surface (on-tinted-surface pill — plain card bg + secondary text, for badges rendered on warning strips / unread-tinted rows where a same-hue badge bg would disappear). Legacy accent / success / info / premium now alias to neutral for back-compat.

Card variants — only two: default (neutral card surface) and flat (dashed-border empty state). No tonal, no live, no warning card tones. Live state is expressed with a live dot inside the card, not with a colored frame.

Icon color — always neutral (--text-muted or --text-subtle) unless it sits inside a primary/warning/error surface. Never tint an icon green just to mark "my thing."

Color

  • Primary hue: classic forest green #006633 (--brand-600). Used for CTAs, active tab tint, primary text on accent surfaces, link color.
  • Primary subtle: #ECFDF3 (--brand-50) — tonal card backgrounds, soft CTAs, empty-state icon bubbles.
  • Neutrals: green-gray ramp (#F8FAF8#0A120C), hue-shifted ~15° from Tailwind. Picks up a whisper of the brand without ever competing with it.
  • Status: success is the brand green itself (#006633), warning #D97706, error #DC2626, info #4B0082 (deep violet — used for informational not alarm).
  • Tennis-specific: court-green #006633, court-clay #DC6B2F, court-hard #3B82F6.
  • Third-party locked-in: Kakao #FEE500 / #191919, Naver #03C75A — used only in their respective login/share buttons.
  • Medals: gold / silver / bronze / platinum for tier badges.

Typography

  • Family: Pretendard (weights 400, 500, 600, 700) — Korean fintech default. Cleaner at small sizes than Noto/Spoqa.
  • Role system (bounded by @twomore/bounded-variant-enums):
    • Semantic: pageTitle, cardTitle, navTitle, cardBody, cardMeta, badge, micro
    • Display: displaySm, displayMd, displayLg, displayXl
  • Tracking: use the Text role defaults. Do not set ad-hoc letter spacing in feature screens.

Spacing

  • 4px grid. Tokens $1$10 map to 4/8/12/16/20/24/32/40/48/64.
  • Default gap inside cards: 12px ($3). Between cards in feed: 12px ($3).
  • Horizontal page padding: 16px ($4).

Backgrounds

  • Flat surfaces only. Page is --background (#F5F8F5); cards are --card (#FBFDFB). No gradients outside brand art. No hero photography in chrome.
  • No repeating patterns, no textures, no grain. The restraint is the aesthetic.
  • Imagery (when used) is warm / daylight / court-side, never stylized. Crop tight to the action; avoid empty skies.

Borders & radii

  • Border: 1px, --border-subtle (#EAF0EA) on cards, --border (#D5E3D6) on inputs.
  • Radius: 12px on cards ($4), 12px on default buttons ($3), pill (9999px) on CTA pills, 8px ($2) on badges. Circle buttons are pill on a 1:1.
  • No hard-edged containers — every surface has at least an 8px radius.

Elevation (Toss-flat first)

  • Default card: border only, no shadow.
  • Overlays use ModalPanel with a backdrop (rgba(0,0,0,0.5)), not shadow. Product bottom sheets are forbidden; Tamagui Sheet is dev-panel only.

Transparency & blur

  • Used only on overlays (rgba(0,0,0,0.5) backdrop) and on opacity: 0.12 washes behind warning/error badges to tint their backgrounds.
  • No frosted glass, no backdrop-filter anywhere in product chrome.

Cards (anatomy + role)

Cards are for display and navigation only. Tapping a card opens its detail screen; a card has no other interaction. Never put a CTA inside a card. CTAs — "참가 확정", "번개 만들기", "회비 납부" — live on the detail screen or anchored to the feed footer.

  • Variants: default (neutral card surface) and flat (dashed-border empty state). No tonal / live / warning tones — state lives in the card's content (LIVE dot, amber eyebrow), not the frame.
  • Affordance: no chevrons, no arrows. Hover darkens the border (--border-subtle--border); press drops scale to 0.985 + opacity 0.95. That's enough to read as clickable.
  • Padding: 16px default, 20px hero, 12px compact.
  • Shape: 12px radius, 1px --border-subtle border, no shadow by default.

Buttons (anatomy + placement)

Buttons are where action lives. They are always outside cards.

  • Variants: primary, secondary (brand-tonal on subtle), outline, ghost, destructive.
  • Sizes: xs (28px), sm (36px), md (44px), lg (52px, full-width).
  • Shapes: default (12r), pill (9999r), circle (1:1 pill).
  • Press state: scale(0.97) + opacity(0.9). No hover chromes on mobile; on web, hover drops opacity to 0.92.
  • Conventions:
    • Page CTA → lg + primary (pinned to bottom bar)
    • Cards never contain CTAs. Use a detail screen action or BottomCtaBand.
    • Stepper → xs + circle icon-only

Motion

  • Spring-based, three named curves (from tamagui.config.ts):
    • quick — damping 20, stiffness 250 (pill-nav, press feedback)
    • medium — 18 / 170 (card entrance)
    • slow — 15 / 100 (ModalPanel open)
  • Transitions are confirmations, not decoration. Toss rule: motion tells you what happened, never that something is pretty.
  • No fades longer than 200ms. No bounces. No parallax.

Hover / press / focus

  • Hover (web): opacity 0.92, no color shift.
  • Press (all): scale(0.97) + opacity 0.9.
  • Focus (web): 0 0 0 3px rgba(0, 102, 51, 0.18) outline — brand green at 18%.
  • Disabled: opacity 0.45, no other changes.

Layout rules

  • Vertical feed of independent cards on Home. Each card is self-sufficient and returns null when it has nothing to say.
  • Horizontal scroll is avoided except for Pill-Nav and horizontal gallery use-cases.
  • Bottom nav is always visible on tab screens (5 tabs, 56pt + safe area).
  • Wizards use a fixed bottom action bar with 1–3 buttons (back, secondary, primary).
  • The page title lives inline at the top of scroll content, not in a sticky nav bar.

Iconography

  • Source of truth: @tamagui/lucide-icons — full Lucide icon set, outline style, 1.5px stroke weight.
  • Size scale: xs 12 · sm 14 · md 16 · lg 20 · xl 24. Default is md inline, lg in-card, xl in empty/momentum hero.
  • Color: always via token — $primary, $text, $textMuted, $textSubtle, $warning, $error. Never raw hex in usage.
  • Usage in this design system: icons are delivered via Lucide's CDN (unpkg @0.454.0) as inline SVG. If you need the actual npm package: npm i lucide-react.
  • Core icons in use (from repo):
    • Nav: Home (홈), Shield (클럽), Swords (경기), BarChart3 (기록), User (프로필)
    • Session origins: Zap (pickup), Users (club), Shield (league), Trophy (tournament)
    • Actions/states: AlertCircle, CreditCard, CheckCheck, CloudRain, MapPin, Flame, X, ChevronRight, Plus, Search

Emoji

  • Used only in momentum copy as mood accents: 🔥 (streak), 🏆 (milestone).
  • Never as UI affordances or navigation.

Unicode

  • for LIVE dot.
  • · middle dot for separating metadata.
  • em dash in empty-state copy.

Custom illustrations

  • None in the codebase today. Empty states use a single Lucide icon in a circular --primary-subtle bubble (64×64 for full, 44×44 for compact). Do not draw bespoke SVG illustrations — flag to user if the design needs them.

Substitutions & known gaps

  • Pretendard font files are not in the repo (downloaded on build). This design system loads Pretendard from jsDelivr CDN (@orioncactus/pretendard@1.3.9). If you need offline/bundled fonts, download the four OTFs per assets/fonts/README.md in the source repo.
  • No brand logo file was provided. assets/{logo, logo-reverse, logomark}.svg are a provisional wordmark + conceptual racquet mark. Please replace with official brand assets if/when available.
  • No photography / court imagery was provided. Where imagery is needed in mocks, placeholders are used. Ask user to supply real assets.
  • Web app (apps/web) is a Next.js scaffold at the time of reading; there's no web-specific UI yet. This system targets the mobile surface, which is the live product.

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