Composition
Primitive → Composite → Screen 조합 규칙.
원칙
- Primitive → Composite → Screen. Screen은 조립만. 인라인 primitive 생성 금지.
- 새 공유 UI는 먼저
components/ui/*(web) /src/components/ui/*(mobile) 에 랜딩, 이후 Screen이 소비. - Platform delta는 명시적으로 선언 — 암묵적 분기 금지.
Modal Hierarchy
| 의도 | 컴포넌트 |
|---|---|
| 정보·편집 | Dialog |
| 복구 불가 확인 | AlertDialog |
| 측면/하단 패널 | Sheet (web / mobile) / collapsible panel (desktop) |
| Non-modal 플로팅 | Popover |
| 시스템 알림 | Toast |
Form Field 규칙
모든 폼 필드 = Label + input + (helperText | errorText). 예외 없음.
<div>
<Label htmlFor="email" required>이메일</Label>
<Input id="email" invalid={!!error} />
{error ? (
<span className="caption1 text-destructive">{error}</span>
) : (
<span className="caption1 text-muted">로그인에 사용됩니다</span>
)}
</div>State Prop Naming
표준 이름만 사용: default · pressed · disabled · loading · error.
금지: isBusy, waiting, inactive, pending.
한 브랜드 액센트
브랜드 액센트 컬러는 brown-800 하나. 인터랙티브 요소에 두 번째 액센트 도입 금지.
No Gradients
그라디언트 사용 금지. 솔리드 컬러만. (Rebrand에서 모든 그라디언트 제거됨.)
Light Mode Only
다크 테마 금지. 유일한 다크 표면은 브랜드 히어로 (brown-800 + white 텍스트).
Logo 사용 원칙
로고는 항상 둥근 사각형 컨테이너 안에 — Tiro가 "말하는" 순간(AI 채팅, 온보딩)만 예외.
Font 경계
Pretendard JP로 모든 UI. Averia는 브랜드 모멘트에만. 절대 교차 금지.
Raw Values 금지
- raw hex 금지 —
var(--*)또는 semantic Tailwind 토큰만 - raw px 금지 —
1pxborder, media query 예외 - 모두 lint로 강제 (
no-raw-hex,no-raw-px,no-slate-zinc,no-averia-in-ui)
실전 조합 예시
| Surface | Composition |
|---|---|
| 로그인 페이지 (web) | Card + Avatar(logo) + Typography.Averia + Button(oauth)×3 + Separator + Input + Label + Button(primary) |
| 노트 리스트 (web) | Typography.H3 + Button(secondary) + ListItem + Avatar + Badge + Separator |
| NoteListPage/NoteItem (web) | Card + Badge + IconButton + DropdownMenu + Tooltip |
| DeleteAccountModal (web) | AlertDialog + Input + Button |
| OrganizationMembers (web) | Table + Avatar + Badge + DropdownMenu + Dialog |
| AI 채팅 패널 (desktop right) | Avatar(logo) + Typography.Averia(greeting) + SuggestionPill×N + Input + IconButton |
| 녹음 시작 패널 (mobile) | Sheet(bottom) + Tabs + LanguagePair + Button(primary) |
| 마케팅 히어로 | Typography.Averia(display) + Avatar(logo) + Photography |
| ParticipantsTab (mobile) | TopNavigation + SearchField + ListItem + Avatar + Chip + IconButton + EmptyState |
| NoteSummarySection (mobile) | TopNavigation + Card + Text + AIStreamingText + DropdownMenu |
| ParagraphSection (mobile) | TranscriptBlock + SpeakerChip + Tooltip + AudioPlayer |
Mobile Primitive Conventions
모든 모바일(RN) primitive와 그 사용 코드는 다음 규칙을 공유한다. 각 컴포넌트의 Usage 섹션은 이 규칙을 전제하고, 컴포넌트 고유의 props·variants만 다룬다.
1. NativeWind 스타일링
className=만 사용. inline style={} 객체 금지. 단, prop으로 받은 동적 색·transform 등 className으로 표현 불가능한 경우만 예외.
// ❌ Don't
<TouchableOpacity style={{ padding: 16, backgroundColor: '#3A2018' }}>
// ✓ Do
<TouchableOpacity className="p-[16px] bg-brown-800">2. px 단위 명시
Tailwind shorthand(p-4)는 사용하지 않고 항상 [Npx] 표기. 디자인 토큰의 수치(spacing.4 = 16px)와 코드 표기가 1:1 일치해야 grep·치환이 안전함.
// ❌ Don't
className="p-4 gap-2 rounded-lg"
// ✓ Do
className="p-[16px] gap-[8px] rounded-[8px]"3. 고정 width·height 금지
h-[44px], w-[200px] 같은 고정 차원 대신 padding·flex·gap·line-height의 합으로 최종 크기가 자연스럽게 결정되도록 작성.
// ❌ Don't
<TouchableOpacity className="h-[44px] w-full">
// ✓ Do — line-height(24) + py(10×2) = 44pt 자동
<TouchableOpacity className="px-[20px] py-[10px] self-stretch">icon-only 정사각형도 fixed-size 대신 padding만으로:
// ✓ icon button 정사각형
<TouchableOpacity className="p-[10px]">4. 터치 컴포넌트: TouchableOpacity
Pressable은 사용하지 않는다. TouchableOpacity activeOpacity={0.7}이 Tiro mobile 표준.
5. RN 빌트인 우선
TouchableOpacity·Text·View·TextInput·Image·Switch·ActivityIndicator 등 RN 빌트인으로 표현 가능하면 외부 의존성 없이 작성. 빌트인으로 처리 불가능한 케이스만 검증된 라이브러리 사용:
| 케이스 | 라이브러리 |
|---|---|
| BottomSheet 프레젠테이션 | @lodev09/react-native-true-sheet |
| Toast | react-native-toast-message |
| Swipe gesture | react-native-gesture-handler |
| 가속 애니메이션 | react-native-reanimated |
| Pull-to-refresh | RN 빌트인 RefreshControl |
6. Primitive는 prop + children API
Web primitive와 동일한 시그니처를 유지해 양 플랫폼 consumer 경험이 같도록 한다.
// ✓ Mobile Button — 사용감이 web과 동일
<Button variant="ghost" size="icon" onPress={dismiss}>
<X size={20} strokeWidth={1.75} />
</Button>String children 자동 wrap 규칙: RN에서 텍스트는 반드시 <Text> 안에 있어야 함. 모든 primitive는 children이 string이면 컴포넌트 시맨틱에 맞는 typography 토큰을 자동 적용한 <Text>로 wrap한다. consumer가 <Button>기록 시작</Button>처럼 web과 같은 모양으로 작성할 수 있게 함.
const content =
typeof children === 'string'
? <Text className={Typography.button1}>{children}</Text>
: children7. 확장은 React composition
기존 primitive를 wrap·children으로 합성. 같은 스타일 로직을 새 컴포넌트에 중복 작성 금지.
// ✓ IconButton은 Button 위에 얇은 래핑
export const IconButton = ({ icon, ...rest }: IconButtonProps) => (
<Button size="icon" {...rest}>
{icon}
</Button>
)8. 파일 위치
- Primitive 파일:
src/components/ui/*(1회 작성) - Screen에서는 import해서 조립만. Screen 내부에 inline styling 금지
9. Token import 경로
import { DesignColors } from 'constants/designTokens/colors' // stone scale 정합 진행 중
import { Typography } from 'constants/designTokens/typography' // button1, subtitle1 등 semantic 명명새 색·간격을 직접 hex/숫자로 작성하지 말고 반드시 토큰 경로를 거친다.
10. Platform delta 명시
같은 컴포넌트의 web/mobile 구현이 분기되면 컴포넌트 .mdx의 Spec/Sizes 표에 Value (web) / Value (mobile) 컬럼으로 명시. 코드만으로 분기를 추론하게 두지 말 것.
Do / Don't
- Do: Primitive → Composite → Screen 계층 엄수
- Do: Platform delta 명시 선언
- Do: shadcn default 우선, 필요한 override만
- Don't: Screen 안에서 인라인 primitive 생성 금지
- Don't: 두 번째 브랜드 액센트 도입 금지
- Don't: 다크 테마, 그라디언트, raw hex/px 사용 금지
