조합
Todae가 UI를 만들기 위해 제공하는 것은 네 종류로 나뉩니다. 새로 만드는 것은 아래 질문으로 자리를 정하세요. ThemeProvider, useDensity, composeRefs 같은 프로바이더와 그 훅, 헬퍼 함수, 상수는 이 구분 밖에 있습니다.
- 네이티브 + CSS. HTML이 이미 그 일을 합니다. Todae는 그 요소의 토큰을 읽을 수 있는 기본 스타일의 CSS만 제공하고 export는 없습니다. Link와 Disclosure처럼 요소 자체에 입히는 스타일이거나, VisuallyHidden처럼 골라 쓰는 유틸리티 클래스입니다. 찾기 쉽도록 이 페이지들은 사이드바에서 컴포넌트와 함께 있습니다.
- 컴포넌트. 제품에 대해 아무것도 몰라도 Todae가 마크업, props, 동작을 정할 수 있는 UI 개념 하나입니다. Menu와 Tabs처럼 동작을 맡을 수도 있고, Stack과 Text처럼 토큰에서 온 스타일만 맡을 수도 있습니다. 대부분의 컴포넌트는 컴파운드로 제공됩니다. 컴파운드는
Menu.Root,Menu.Item처럼Root와 필요한 파트를 가진 네임스페이스입니다.Stack,Text,Label,Checkbox처럼 파트가 없는 컴포넌트는 export 하나입니다. - 훅. 마크업 없는 동작입니다. 레시피가 필요로 하거나 제품이 자체 마크업으로 써야 할 때 Todae가 export합니다. useRovingFocus와 useCombobox가 훅입니다. 사이드바에서는 유틸리티 아래에 있습니다.
- 레시피. 회사 자체 디자인 시스템의 출발점입니다. 결과가 제품에 따라 달라지는 곳에서 Todae export를 어떻게 묶는지 보여 줍니다. 제품의 데이터, 흐름(무엇을 검색할지, 결과를 어떻게 불러올지, 선택하면 무슨 일이 일어날지), 화면 배치가 그 예입니다. 레시피는 얇은 연결 코드여야 합니다. 어려운 동작(키보드 처리, 포커스 관리, ARIA 연결, 측정과 위치 지정)은 컴포넌트나 훅에 있어야 하며, 그래야 회사가 접근성을 깨뜨리지 않고 레시피를 복사해 스타일을 바꿀 수 있습니다. 레시피는 패키지 export가 아니므로
Combobox.*는 없습니다.
새로 만드는 것의 자리 정하기
순서대로 물어보세요.
- 모든
<a>처럼 그 요소 전부가 이렇게 보여야 하거나.todae-visually-hidden처럼 골라 쓰는 유틸리티 클래스여서, 기본 CSS만으로 일반 HTML이 그 일을 하나요? 그렇다면 네이티브입니다. 그 밖의 경우는 모두 2번 질문으로 넘어갑니다. props, 타입으로 강제하는 계약, 다른 컴포넌트의 상태가 필요한 요소도 마찬가지입니다. 그래서Stack(방향),Text(크기), Label(필수htmlFor),Field.Select(Field의 레이블과 오류 연결)는 네이티브가 아닙니다. - 제품의 데이터, 데이터 요청, 라우팅, 비즈니스 규칙, 화면 배치를 몰라도 Todae가 마크업, 모든 prop, 동작을 정할 수 있나요? 그렇다면 컴포넌트입니다.
Stack도 여기에 해당합니다. 화면에서 무엇이 어디에 놓일지가 아니라 토큰에서 온 간격만 정합니다. 레시피나 제품의 자체 마크업에 그 동작이 필요하다면 useRovingFocus처럼 그 동작은 훅도 됩니다. - 그 밖에는 제품의 몫입니다. Todae export를 묶는 것이라면 Todae는 레시피로 문서화합니다. 레시피가 얇은 연결 코드로 남도록, 필요한 어려운 동작은 컴포넌트나 훅으로 옮깁니다. 예를 들어 Combobox는 useCombobox를 씁니다. 복사한 코드에 자체적인 어려운 동작이 남는다면 그 동작은 Todae가 맡아야 합니다.
컴포넌트는 Menu.Trigger가 Button인 것처럼 다른 컴포넌트를 담을 수 있고, Fieldset이 disabled와 invalid를 공유하는 것처럼 담은 자식과 상태를 나눌 수 있습니다. Todae가 그 조각들이 어떻게 맞물리는지 정할 수 있으므로 여전히 컴포넌트입니다. 동작도, 스타일도, 토큰도 더하지 않고 다른 export를 배치하기만 하는 래퍼는 절대 Todae 컴포넌트가 아닙니다. 배치를 보여 줄 가치가 있다면 레시피입니다.
컴포넌트
공개된 형제 컴포넌트의 예는 Field, Label, Description, ErrorMessage, Checkbox, Radio, Switch, Button, Popover, Tooltip, Chip, OptionList, Dialog, BottomSheet입니다. 나란히 놓아 조합하세요. Field.Button.Root처럼 네임스페이스를 중첩하지 마세요. 컴포넌트 루트 하나에는 인터랙티브 역할 하나만 둡니다.
Field.Icon, Field.Affix, Button.Icon은 컴파운드 파트입니다. 따로 떠다니는 원자가 아닙니다. 공유되는 Icon, Affix, Shell은 내부에 있습니다. 공개 export가 아닙니다.
Field.Root는 외형만 담당합니다. Label은 형제 컴포넌트입니다. 장식 요소는 DOM 순서를 따릅니다. side prop은 없습니다.
Field와 Popover는 나란히 놓입니다. 어느 쪽도 다른 쪽을 감싸지 않습니다.
Field 셸은 버튼이 아닙니다. 이런 트리는 절대 허용되지 않습니다.
<Field.Root role="button" onClick={onOpen}>
<Field.TextInput value={q} onChange={(event) => setQ(event.target.value)} />
</Field.Root>
레시피
컴포넌트와 훅을 조합한 작동 예시입니다. Combobox는 패키지 네임스페이스가 아니라 레시피입니다. 아래 블록은 정적입니다. 실행되지 않습니다. 동작은 훅이 맡습니다. 외형은 컴포넌트가 맡습니다. 제공되는 Trigger 편의 컴포넌트는 없습니다.
데스크톱(presentation="popover"). 닫힌 컨트롤은 실제 Field입니다. getAnchorProps를 Field.Root에 펼치세요. 목록은 Popover.Root 안에 있습니다. 작동하는 레시피는 Combobox에 있습니다.
const cb = useCombobox({ presentation: 'popover' })
<div className="labelled-field">
<Label htmlFor={cb.ids.input}>Tags</Label>
<Field.Root {...cb.getAnchorProps()}>
<Field.Icon><Search /></Field.Icon>
<Field.ChipInput {...cb.getChipInputProps()} />
<Field.Icon><ChevronDown /></Field.Icon>
</Field.Root>
</div>
<Popover.Root {...cb.getPopoverProps()}>
<OptionList.Root {...cb.getListboxProps()}>...</OptionList.Root>
</Popover.Root>
트리거가 getTriggerProps가 표시하는 요소가 아닐 때는 anchorRef가 CSS 앵커입니다. Combobox는 Field.Root에 getAnchorProps를 씁니다. usePopover의 getTriggerProps를 함께 펼치지 마세요.
터치(presentation="sheet"). 닫힌 컨트롤은 Button.Root입니다. 검색 Field는 시트 안에 있습니다.
const cb = useCombobox({ presentation: 'sheet' })
<Button.Root {...cb.getTriggerProps()}>Pick…</Button.Root>
<BottomSheet.Root {...cb.getSheetProps()}>
<div className="labelled-field">
<Label htmlFor={cb.ids.input}>Search</Label>
<Field.Root id={cb.ids.input}>
<Field.Icon><Search /></Field.Icon>
<Field.TextInput {...cb.getInputProps()} />
</Field.Root>
</div>
<OptionList.Root {...cb.getListboxProps()}>...</OptionList.Root>
</BottomSheet.Root>
터치 트리거는 Button.Root입니다.
훅
useCombobox는 헤드리스입니다. 열림 상태, 값, aria, 키보드, listbox id를 맡습니다.
useCombobox({ presentation: 'popover' | 'sheet' | 'dialog' | 'auto' })getAnchorProps()는Field.Root에 (데스크톱)getTriggerProps()는Button.Root에 (시트)getInputProps()/getChipInputProps()는 필드 입력에getPopoverProps()는Popover.Root에getSheetProps()는BottomSheet.Root에getDialogProps()는Dialog.Root에getListboxProps()는OptionList.Root에
Trigger 편의 컴포넌트는 없습니다. Combobox.* 외형도 없습니다. Combobox를 보세요.
금지
Field.Root
└─ Field.Button.Root nested compound namespaces
Field.Root as a button that also hosts TextInput
two roles on one root
Combobox.Root, Combobox.Trigger, or Combobox.Icon
recipe chrome in the package
Dual-wiring aria or open outside useCombobox while also using the hook
OptionList and Radio or Checkbox bound to the same value
overlay listbox and permanent choice on one selection
Combobox behavior baked into Field
Touch closed control = Field pretending to be a button
Product trees importing raw Icon or Affix instead of Field.Icon or Field.Affix
Docs that treat Field.Icon or Button.Icon as free-floating atoms
실제로 동작하는 레시피는 Combobox를 보세요.