sellmate-design-system-react 9.0.0-beta.89 → 9.0.0-beta.90

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -40,7 +40,7 @@
40
40
  | 분류 | 컴포넌트 |
41
41
  | --- | --- |
42
42
  | **버튼·링크** | `SButton` `SGhostButton` `SMeatballButton`(⋯ 로 '더 보기' 메뉴를 여는 아이콘 버튼) `SDropdownButton` `STextLink` `SSwitch` `SToggle` |
43
- | **입력 (폼)** | `SForm` `SField` `SInput` `SChatInput`(대화 입력창) `SSearchInput` `SNumberInput` `STextarea` `SEditor` `SSelect` `SCheckbox` `SRadio` `SRadioGroup` `SRadioButton` `SRadioCard` `SRadioCardGroup` `SChip` `SChipInput` `SBarcodeInput` `SFilePicker` |
43
+ | **입력 (폼)** | `SForm` `SField` `SInput` `SChatInput`(대화 입력창) `SSearchInput` `SNumberInput` `SSlider` `STextarea` `SEditor` `SSelect` `SCheckbox` `SRadio` `SRadioGroup` `SRadioButton` `SRadioCard` `SRadioCardGroup` `SChip` `SChipInput` `SBarcodeInput` `SFilePicker` |
44
44
  | **날짜·시간** | `SCalendar` `SCalendarBoard`(한 달치 일정 판) `SDatePicker` `SDatePickerYearListbox` `SDatePickerMonthListbox` `SDateRangePicker` `STimePicker` `STimeRangePicker` |
45
45
  | **표·목록** | `SChatMessage`(대화의 메시지 한 건) `SChatAttachedFile`(입력창 위, 아직 보내지 않은 첨부 한 칸) `SChatFile`(대화 흐름에 선, 보낸 파일 한 칸) `SChatSystemMessage`(대화 흐름 가운데 서는 시스템 안내) `STable` `STableBar` `STreeTable`(행이 부모-자식 계층을 이루는 표 — 펼치고 접는다) `SChipFilter` `SKeyValueTable` `SList` `SListItem` `SExpansionList` `SDraggableList` `SDraggableItem` `STree` `STimeline`(날짜로 묶인 시간순 기록 — 작업 로그·메모 이력) |
46
46
  | **레이아웃** | `SLayout` `SGnb` `SGnbSystem` `SSystemActionButton`(GNB system 패널에 한 줄씩 쌓는 액션 행 — 버튼 고르기는 §3-5) `SAccountListBox`(계정 행을 눌러 뜨는 계정 패널) `SLauncherListBox`(런처 버튼을 눌러 뜨는 서비스 목록) `SPage` `SPageHeader`(페이지 제목 영역 — `SLayout` 안에서 `SPage` 앞에 둔다) `SSectionHeaderCard` `SCard` `SLoginCard`(통합 계정 로그인 화면의 카드) `SDivider` `SSplitter` `SScrollArea` `SExpansionItem` |
@@ -76,6 +76,7 @@ AI 에이전트는 코드를 생성하기 전에 이 목록을 반드시 지킨
76
76
  | `<input type="text/password/...">` | `SInput` |
77
77
  | `<input type="search">` | `SSearchInput` |
78
78
  | `<input type="number">` | `SNumberInput` |
79
+ | `<input type="range">` | `SSlider` |
79
80
  | `<input type="checkbox">` | `SCheckbox`, `SToggle`, `SSwitch` |
80
81
  | `<input type="radio">` | `SRadio`, `SRadioButton` |
81
82
  | `<input type="file">` | `SFilePicker` |
@@ -168,7 +169,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
168
169
  | --- | --- | --- |
169
170
  | **셸** | 앱 전체 뼈대. 페이지가 바뀌어도 남는다 | `SLayout` `SGnb` `SGnbSystem`(GNB 맨 아래 판 · 전폭 상단바 오른쪽 끝) `SPage` `SPageHeader`(`SLayout` 안에서 `SPage` 앞에 둔다) |
170
171
  | **블록** | `SPage` 의 직계 자식. 페이지를 세로로 쌓는 단위 | `SSectionHeaderCard` `SCard` `SChatMessage` `SChatSystemMessage` `SChatInput` `SLoginCard`(유일하게 `SPage` 밖에 선다 — 로그인 화면 자체가 자기 자리다, §4-6) `SForm` `SSplitter` `SScrollArea` `SCalendarBoard` `STable` `STableBar` `STreeTable` `SChipFilter` `SKeyValueTable` `SList` `SExpansionList` `SDraggableList` `STree` `STimeline` `SCallout` `SContextAlert` `SBarChart` `SLineChart` `SPieChart` `SDonutChart` `STabs` `SStepper` `SPagination` `SDivider` |
171
- | **요소** | 블록 **안에** 놓이는 컨트롤. 혼자 페이지에 서지 않는다 | `SButton` `SGhostButton` `SMeatballButton` `SDropdownButton` `SSystemActionButton` `SField` `SInput` `SSearchInput` `SNumberInput` `STextarea` `SEditor` `SSelect` `SCheckbox` `SRadio` `SRadioGroup` `SRadioButton` `SRadioCard` `SRadioCardGroup` `SSwitch` `SToggle` `SChipInput` `SBarcodeInput` `SFilePicker` `SDatePicker` `SDatePickerYearListbox` `SDatePickerMonthListbox` `SDateRangePicker` `STimePicker` `STimeRangePicker` `SCalendar` `SListItem` `SExpansionItem` `SDraggableItem` `SChatAttachedFile` `SChatFile` `SImage` `SLinearProgress` `SCircleProgress` `SSkeleton` |
172
+ | **요소** | 블록 **안에** 놓이는 컨트롤. 혼자 페이지에 서지 않는다 | `SButton` `SGhostButton` `SMeatballButton` `SDropdownButton` `SSystemActionButton` `SField` `SInput` `SSearchInput` `SNumberInput` `SSlider` `STextarea` `SEditor` `SSelect` `SCheckbox` `SRadio` `SRadioGroup` `SRadioButton` `SRadioCard` `SRadioCardGroup` `SSwitch` `SToggle` `SChipInput` `SBarcodeInput` `SFilePicker` `SDatePicker` `SDatePickerYearListbox` `SDatePickerMonthListbox` `SDateRangePicker` `STimePicker` `STimeRangePicker` `SCalendar` `SListItem` `SExpansionItem` `SDraggableItem` `SChatAttachedFile` `SChatFile` `SImage` `SLinearProgress` `SCircleProgress` `SSkeleton` |
172
173
  | **인라인** | 텍스트 흐름·셀·라벨 안에 섞인다. 혼자 블록이 되지 않는다 | `STag` `SBadge` `SIcon` `SLogo` `STextLink` `SChip` |
173
174
  | **레이어** | 문서 흐름 **밖**에 떠서 그려진다. 어느 층에서 띄우든 레이아웃에 영향이 없다 | `SModal` `SActionModal` `SConfirmModal` `SPopup` `SDrawer` `SPopover` `STooltip` `SPortal` `SAccountListBox`(계정 행에 붙어 뜬다 — 직접 띄우지 않는다) `SLauncherListBox`(런처 버튼에 붙어 뜬다 — 직접 띄우지 않는다) `SToast` `SLoadingModal` `SLoadingContainer` `SGuide` |
174
175
 
@@ -529,6 +530,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
529
530
  | 제목·굵게·목록·색 같은 **서식이 남아야 하는** 글을 받는다 | `SEditor` | §3-7-1 |
530
531
  | 목록·결과를 검색어로 좁힌다 | `SSearchInput` | §3-7-1 |
531
532
  | 숫자(수량·금액)를 받는다 | `SNumberInput` | §3-7-13 |
533
+ | 정해진 범위 안에서 수치 하나·구간을 끌어서 고르게 한다 | `SSlider` | §3-7-16 |
532
534
  | 바코드를 스캔해 받는다 | `SBarcodeInput` | |
533
535
  | 목록에서 하나 고르게 한다 | `SSelect` | §3-7-2 |
534
536
  | 선택지를 항상 펼쳐 두고 하나 고르게 한다 | `SRadioGroup` | §3-7-2 |
@@ -1901,6 +1903,16 @@ const [from, setFrom] = useState<string | null>(null);
1901
1903
 
1902
1904
  직접 쓰는 경우는 하나뿐이다 — **디자인 시스템에 없는 컨트롤**에 다른 필드와 똑같은 라벨·필수·에러 모양을 붙일 때.
1903
1905
 
1906
+ - **라벨은 `controlId` 로 컨트롤에 잇는다.** 컨트롤에 `id` 를 주고 같은 값을 `controlId` 로 넘기면 라벨이 그 컨트롤의 이름이 되고, 라벨을 누르면 컨트롤에 포커스가 간다. 컨트롤이 `input`·`textarea`·`select`·`button` 이 아니라 `role` 로 만든 요소면 `aria-labelledby={fieldLabelId(id)}` 를 직접 건다.
1907
+ - **라벨이 보이는 입력에 `aria-label` 을 다시 적지 않는다.** DS 입력 컴포넌트는 `label` 을 알아서 컨트롤에 잇는다. 화면의 라벨과 읽히는 이름을 두 번 적으면 어긋난다. `aria-label`(`SCheckbox`·`SChipInput` 은 `ariaLabel`)은 **라벨 글자 없이 서는 컨트롤**에만 준다 — 표의 행 선택 체크박스처럼.
1908
+
1909
+ ```tsx
1910
+ ✅ <SField label="색상" controlId={id}><input id={id} type="color" /></SField>
1911
+ ✅ <STextarea label="그룹명" value={name} onValueChange={setName} /> {/* 이름은 label 이 된다 */}
1912
+ ❌ <STextarea label="그룹명" aria-label="그룹명" … /> {/* 이름을 두 번 적는다 */}
1913
+ ❌ <SField label="색상"><input type="color" aria-label="색상" /></SField> {/* 라벨을 눌러도 포커스가 안 간다 */}
1914
+ ```
1915
+
1904
1916
  #### 3-7-6. 여러 건을 나열하는 넷 — STable vs SList vs SDraggableList vs STimeline
1905
1917
 
1906
1918
  | 판별 | 사용 |
@@ -1917,6 +1929,7 @@ const [from, setFrom] = useState<string | null>(null);
1917
1929
  - **`SList` 의 자식은 `SListItem` 을 권장한다.** 다른 자식도 그대로 렌더되지만, 펼치는 항목은 `SExpansionList` + `SExpansionItem` 이, 끌어서 순서를 바꾸는 항목은 `SDraggableList` + `SDraggableItem` 이 여닫힘·정렬 동작까지 함께 관리하므로 그쪽을 쓴다 (§3-7-7).
1918
1930
  - **항목 사이 구분선은 리스트가 알아서 그린다.** `SListItem`·`SExpansionItem`·`SDraggableItem` 셋 다 스스로 구분선을 그리지 않는다. `SList`·`SExpansionList`·`SDraggableList` 가 자식 **사이에** 구분선을 넣으므로 아이템에 `border-b` 를 직접 붙이지 않고, 켜는 prop 도 따로 없다. 마지막 항목 아래에는 선이 남지 않는다. 테두리형(`bordered`)은 테두리가 구분 역할을 하므로 리스트가 구분선을 빼고, `gap` 으로 띄운다 — `gap` 을 준 목록에도 구분선은 들어가지 않는다.
1919
1931
  - **`SListItem` 은 기본이 표시 전용이다.** 눌러서 이동·선택하게 하려면 `clickable` 을 준다 — hover·`selected`·`interaction="chevron"` 표현이 전부 여기에 딸려 있어서, `clickable` 없이 `selected` 만 주면 선택 표시가 나오지 않는다. `SExpansionList` 는 자식 아이템을 알아서 클릭 가능하게 만들어 이 함정을 막아 준다 — 선택 상태 자체는 앱이 든다 (§3-7-7).
1932
+ - **`clickable` 에 `onClick` 까지 주면 키보드로도 눌린다.** 항목이 버튼으로 Tab 순서에 들고 Enter·Space 가 `onClick` 을 부른다. `role`·`tabIndex`·키 처리를 직접 붙이거나, 그것을 붙인 `div` 로 항목을 감싸지 않는다.
1920
1933
 
1921
1934
  ```tsx
1922
1935
  ✅ <SList><SListItem title="일반 문의" />…</SList> {/* 줄로 구분되는 목록 — 구분선은 자동 */}
@@ -1925,6 +1938,7 @@ const [from, setFrom] = useState<string | null>(null);
1925
1938
  ❌ <SList><SListItem title="일반 문의" className="border-b" />…</SList> {/* 구분선을 직접 붙이지 않는다 */}
1926
1939
  ❌ <SList><><SListItem title="일반 문의" /><SListItem title="결제 문의" /></></SList> {/* Fragment 로 묶으면 그 안쪽은 구분되지 않는다 */}
1927
1940
  ❌ <SListItem title="일반 문의" selected /> {/* clickable 없으면 선택 표시가 안 나온다 */}
1941
+ ❌ <div role="button" tabIndex={0} onKeyDown={…}><SListItem title="일반 문의" clickable /></div> {/* 키보드 조작은 항목이 한다 */}
1928
1942
  ✅ <STimeline groups={[{ heading: '2026-10-07', entries: [{ key: id, time: '14:32', content: <Memo … /> }] }]} />
1929
1943
  ❌ <div className="grid grid-cols-[52px_8px_1fr]"><span className="rounded-full …" />…</div> {/* 타임라인 레일을 직접 그리지 않는다 */}
1930
1944
  ```
@@ -2214,6 +2228,30 @@ const [selectedId, setSelectedId] = useState<string>();
2214
2228
  ❌ <SSkeleton className="bg-grey-20" /> {/* 색은 토큰이 넣는다 — 어두운 면이면 mode="dark" */}
2215
2229
  ```
2216
2230
 
2231
+ #### 3-7-16. 숫자 둘 — SNumberInput vs SSlider
2232
+
2233
+ | 판별 | 사용 |
2234
+ | --- | --- |
2235
+ | **정확한 값**이 중요하다 (수량·금액·우선순위) | `SNumberInput` |
2236
+ | 범위가 정해져 있고 **대략의 위치**를 고르면 된다 (할인율·가중치·가격대 필터) | `SSlider` |
2237
+ | 범위의 **양 끝**을 함께 고른다 | `SSlider` 에 `value={[시작, 끝]}` |
2238
+
2239
+ - **단일·범위는 `value` 의 모양으로 정한다.** `number` 면 손잡이 하나, `[시작, 끝]` 이면 둘이다. 따로 켜는 prop 이 없다.
2240
+ - **서버 요청·목록 다시 불러오기는 `onValueCommit` 에서 한다.** `onValueChange` 는 끄는 동안 step 마다 불린다.
2241
+ - **값을 옆에 따로 적으면 `showValue="none"`, 아니면 기본(`drag`)이나 `always`.** 두 손잡이가 가까이 모일 수 있는 범위에는 `always` 를 쓰지 않는다 — 말풍선이 겹친다.
2242
+ - **`marks` 는 고를 수 있는 값이 몇 개 안 될 때만 `true` 로 준다.** step 이 촘촘하면 대표 위치만 배열로 준다.
2243
+ - **라벨을 준다.** `label` 이 손잡이의 접근성 이름이 된다.
2244
+ - 트랙·손잡이 색과 크기는 컴포넌트가 넣는다. 직접 주지 않는다.
2245
+
2246
+ ```tsx
2247
+ ✅ <SSlider label="할인율" value={rate} onValueChange={setRate} formatValue={v => `${v}%`} />
2248
+ ✅ <SSlider label="가격대" min={0} max={100000} step={1000} value={range}
2249
+ onValueChange={setRange} onValueCommit={refetch} />
2250
+
2251
+ ❌ <SSlider label="주문 수량" min={1} max={9999} … /> {/* 정확한 값을 받는 자리 — SNumberInput */}
2252
+ ❌ <SSlider marks min={0} max={100} … /> {/* step 1 마다 점 100개 — 대표 위치만 배열로 */}
2253
+ ```
2254
+
2217
2255
  ---
2218
2256
 
2219
2257
  ## 4. 페이지 레시피 — 표준 골격
@@ -4467,10 +4505,12 @@ export default function StoreSetupPage() {
4467
4505
  - **여백은 영역마다 다르다** (§2-2 상담 콘솔 예). 목록·템플릿은 같은 항목의 반복이라 목록 컴포넌트가 자기 여백을 갖고, 머리 줄도 목록 항목의 좌우 여백(`--cmp-listItem-paddingX`)에 맞춘다. 대화는 말풍선·파일·시스템 안내가 섞여 24 다 — 머리 줄·스크롤 안쪽 상자가 24 를 쓴다. 입력 영역은 `SDivider` 로 대화와 나누고, 좌우는 메시지와 끝을 맞춰 24, 위아래는 16 이다.
4468
4506
  - **대화는 최신이 바닥이다.** 쌓기 상자에 `min-h-full justify-end` 로 메시지가 적어도 바닥에 붙이고, 처음 열 때와 메시지가 늘 때 스크롤 영역(`SScrollArea` 의 ref)을 맨 아래로 내린다. 입력창은 스크롤 밖 바닥에 고정한다.
4469
4507
  - **메시지 한 건 = 한 사람이 이어 말한 묶음이다.** 같은 사람이 같은 시각에 이어 보낸 글·파일은 `SChatMessage` 하나로 묶어 `messages`·`attachments` 로 넘긴다 — 말풍선마다 컴포넌트를 따로 세우지 않는다. 파일은 `attachments` 로만 넘긴다 (`messages` 로 넘기면 칸이 두 겹이 된다).
4508
+ - **아바타 칸은 말하는 상대가 둘 이상일 때만 둔다.** 상담 콘솔처럼 고객·상담사가 오가면 켜 둔다(기본). 챗봇 답변처럼 받는 쪽이 하나뿐인 대화는 받은 메시지 전부에 `showAvatar={false}` 를 준다 — 대화 안에서 섞지 않는다. 칸을 CSS 로 숨기지 않는다.
4470
4509
  - **줄 사이 간격은 토큰이 정한다** — 메시지 묶음 사이는 `--cmp-chatMessage-group-message-gap`, 시스템 안내(`SChatSystemMessage`) 앞뒤는 그보다 넓은 `--cmp-chatMessage-group-system-gap`. 숫자로 적지 않는다.
4471
4510
  - **날짜가 바뀌는 자리에는 날짜 구분선을 끼운다** — 가로 `SDivider` 두 개 사이에 날짜(`typo-body-sm-default` + `text-fg-tertiary`). 일어난 사실이 아니라 흐름의 경계라 시스템 안내 알약(`SChatSystemMessage`)을 쓰지 않는다. **날짜는 `2026년 10월 6일 화요일`** 로 쓴다 — 말풍선 시각과 같은 대화 예외다(§1-4). `toLocaleDateString()` 은 로케일에 따라 결과가 바뀌므로 쓰지 않고 연·월·일·요일을 직접 이어 붙인다. 가로 구분선은 기본이 전체 폭이라 한 줄에 둘을 놓을 때는 `className="w-auto flex-1"` 로 남은 폭을 나눠 갖게 한다. 앞뒤 간격은 시스템 안내와 같은 토큰이다. 날짜가 다르면 같은 시각이어도 한 묶음으로 합치지 않는다.
4472
4511
  - 대화의 시각은 `오후 5:52` 로 쓴다 (§1-4 대화 예외). 읽음 표시는 보낸 메시지에만, 템플릿으로 보낸 글은 `variant="template"` 로 면을 나눈다.
4473
4512
  - **입력 중인 글은 페이지가 상담마다 든다.** 상담을 옮겨도 쓰던 글이 남고, 템플릿 패널이 그 글에 넣을 수 있다. 템플릿은 누르면 **입력창에 넣는다** — 바로 보내지 않고 상담사가 고쳐 보낸다. 보내기 전 첨부는 입력창 위에 `SChatAttachedFile` 로 쌓는다.
4513
+ - **첨부를 받는 대화면 `onAttach` 와 `onPasteFiles` 를 함께 넘긴다** — 붙여넣은 파일·캡처 이미지도 첨부 버튼으로 고른 것과 같은 자리에 쌓는다. 붙여넣기에서 파일을 골라내고 글로 들어가지 않게 막는 것은 입력창이 한다 — `onPaste` 로 직접 처리하지 않는다. 첨부가 쌓여 있으면 `hasAttachments` 를 켠다 — 글 없이 파일만 보낼 수 있고, 이때 `onSend` 에는 빈 문자열이 온다.
4474
4514
  - **상담 종료는 `SChatInput` 의 `footerAction` 이다** — 전송과 층이 다른 동작이라 입력창 아래 보조 슬롯(`STextLink`)에 두고, 실행 전에 `SModal.confirm` 으로 묻는다. 종료된 상담은 입력창을 `disabled` 로 잠그고 종료 링크를 뺀다.
4475
4515
  - 목록의 상태는 `STag` 다 — 색 점(`SBadge`)만으로 찍지 않는다 (§3-1). 템플릿 분류는 `SExpansionList` 루트 하나에 분류(`SExpansionItem`) 안 템플릿(`SListItem`)을 바로 넣는다 (§3-7-7).
4476
4516
 
@@ -4836,6 +4876,10 @@ function Conversation({
4836
4876
  onValueChange={onDraftChange}
4837
4877
  onSend={send}
4838
4878
  onAttach={() => setAttached(current => [...current, `첨부_${current.length + 1}.png`])}
4879
+ // 붙여넣은 파일·캡처 이미지도 첨부 버튼으로 고른 것과 같은 자리에 쌓는다
4880
+ onPasteFiles={files => setAttached(current => [...current, ...files.map(f => f.name)])}
4881
+ // 첨부가 쌓여 있으면 글 없이도 보낼 수 있다
4882
+ hasAttachments={attached.length > 0}
4839
4883
  // 상담 종료는 전송과 층이 다른 동작 — 입력창 아래 보조 액션 슬롯이다
4840
4884
  footerAction={!closed && <STextLink label="상담 종료" onClick={confirmClose} />}
4841
4885
  />
@@ -11,6 +11,7 @@
11
11
  | `value?` | `string` | `''` | 입력값 (제어) |
12
12
  | `placeholder?` | `string` | `DEFAULT_PLACEHOLDER` | 안내 문구. 세 제품이 같은 말로 서도록 기본값을 쥐고 있다 |
13
13
  | `disabled?` | `boolean` | `false` | 비활성 |
14
+ | `hasAttachments?` | `boolean` | `false` | 보낼 첨부가 쌓여 있는지. 켜면 글이 비어도 전송 버튼이 열린다 — 파일·이미지만 보낼 수 있다. 첨부 목록은 앱이 쥐므로 입력창은 그 유무만 받는다. 첨부를 받는 대화면 쌓인 첨부 수로 켠다. |
14
15
  | `busy?` | `boolean` | `false` | 응답을 기다리는 중인지. 켜면 전송 버튼 자리에 정지 버튼이 서고 Enter 전송이 잠긴다 — 답이 오는 동안 또 보내지 못하게 막는 자리다. |
15
16
  | `footerAction?` | `ReactNode` | — | 입력창 아래 보조 액션 슬롯. 없으면 그 줄이 통째로 빠진다. 상담 종료처럼 **대화 자체를 끝내는, 전송과 층이 다른 동작**이 오는 자리다. 무엇을 놓을지는 앱이 정하고, 입력창은 가운데에 세우는 것까지만 한다 — `STextLink` 를 넣는 것이 보통이다. |
16
17
  | `maxRows?` | `number` | `DEFAULT_MAX_ROWS` | 입력창이 자랄 수 있는 최대 줄 수. 여기 닿으면 더 자라지 않고 그 안에서 스크롤한다. |
@@ -23,8 +24,9 @@
23
24
  | Event | Type | Description |
24
25
  |-------|------|-------------|
25
26
  | `onValueChange` | `(value: string) => void` | 값 변경 |
26
- | `onSend` | `(value: string) => void` | 전송. 전송 버튼을 누르거나 Enter 를 쳤을 때 현재 값을 넘긴다. **값이 비면 전송 버튼이 잠긴다** — 빈 메시지는 보낼 수 없다. |
27
+ | `onSend` | `(value: string) => void` | 전송. 전송 버튼을 누르거나 Enter 를 쳤을 때 현재 값을 넘긴다. **값이 비면 전송 버튼이 잠긴다** — 빈 메시지는 보낼 수 없다. 단 `hasAttachments` 가 켜져 있으면 글 없이도 보낼 수 있고, 이때 빈 문자열을 넘긴다. |
27
28
  | `onAttach` | `() => void` | 파일 첨부. **주면 입력창 왼쪽에 첨부 버튼이 선다** |
29
+ | `onPasteFiles` | `(files: File[]) => void` | 붙여넣은 파일. 클립보드에 파일(캡처한 이미지 포함)이 있으면 불린다. **파일이 있으면 글자 붙여넣기를 막는다** — 그대로 두면 파일 이름이 글로 함께 들어간다. 파일이 없는 붙여넣기는 평소대로 글로 들어간다. 받은 파일을 어디에 쌓을지는 앱이 정한다 — 첨부 버튼(`onAttach`)으로 고른 파일과 같은 자리에 쌓는 것이 보통이다. |
28
30
  | `onStop` | `() => void` | 정지. `busy` 일 때 정지 버튼을 누르면 불린다 |
29
31
 
30
32
  ## Dependencies
@@ -15,11 +15,26 @@ export interface SChatInputProps extends Omit<TextareaHTMLAttributes<HTMLTextAre
15
15
  /**
16
16
  * 전송. 전송 버튼을 누르거나 Enter 를 쳤을 때 현재 값을 넘긴다.
17
17
  *
18
- * **값이 비면 전송 버튼이 잠긴다** — 빈 메시지는 보낼 수 없다.
18
+ * **값이 비면 전송 버튼이 잠긴다** — 빈 메시지는 보낼 수 없다. 단 `hasAttachments` 가 켜져
19
+ * 있으면 글 없이도 보낼 수 있고, 이때 빈 문자열을 넘긴다.
19
20
  */
20
21
  onSend?: (value: string) => void;
22
+ /**
23
+ * 보낼 첨부가 쌓여 있는지. 켜면 글이 비어도 전송 버튼이 열린다 — 파일·이미지만 보낼 수 있다.
24
+ *
25
+ * 첨부 목록은 앱이 쥐므로 입력창은 그 유무만 받는다. 첨부를 받는 대화면 쌓인 첨부 수로 켠다.
26
+ */
27
+ hasAttachments?: boolean;
21
28
  /** 파일 첨부. **주면 입력창 왼쪽에 첨부 버튼이 선다** */
22
29
  onAttach?: () => void;
30
+ /**
31
+ * 붙여넣은 파일. 클립보드에 파일(캡처한 이미지 포함)이 있으면 불린다.
32
+ *
33
+ * **파일이 있으면 글자 붙여넣기를 막는다** — 그대로 두면 파일 이름이 글로 함께 들어간다.
34
+ * 파일이 없는 붙여넣기는 평소대로 글로 들어간다. 받은 파일을 어디에 쌓을지는 앱이 정한다 —
35
+ * 첨부 버튼(`onAttach`)으로 고른 파일과 같은 자리에 쌓는 것이 보통이다.
36
+ */
37
+ onPasteFiles?: (files: File[]) => void;
23
38
  /**
24
39
  * 응답을 기다리는 중인지. 켜면 전송 버튼 자리에 정지 버튼이 서고 Enter 전송이 잠긴다 —
25
40
  * 답이 오는 동안 또 보내지 못하게 막는 자리다.
@@ -50,6 +65,8 @@ export interface SChatInputProps extends Omit<TextareaHTMLAttributes<HTMLTextAre
50
65
  *
51
66
  * 첨부·입력·전송을 한 줄로 세우고, 필요하면 그 아래 보조 액션(상담 종료 등)을 붙인다.
52
67
  * Enter 로 보내고 Shift+Enter 로 줄을 바꾼다 — 한글 조합 중의 Enter 는 보내지 않는다.
68
+ * 파일·이미지를 붙여넣으면 `onPasteFiles` 로 넘기고, 첨부가 쌓여 있으면(`hasAttachments`)
69
+ * 글 없이도 보낼 수 있다.
53
70
  *
54
71
  * **글이 길어지면 입력창이 자란다** — `maxRows` 줄까지 늘어나고, 거기 닿으면 그 안에서
55
72
  * 스크롤한다. 상한이 없으면 대화가 화면 밖으로 밀려난다.
@@ -14,6 +14,7 @@
14
14
  | `readStatus?` | `SChatMessageReadStatus` | — | 상대가 읽었는지. 시각 왼쪽에 `읽음`·`안 읽음` 으로 붙고, 주지 않으면 표시하지 않는다. **내가 보낸 메시지에만 붙인다** — 받은 메시지에 읽음 표시를 다는 것은 뜻이 없다. |
15
15
  | `avatarSrc?` | `string` | — | 아바타 이미지 경로. 없으면 사람 아이콘 자리표시가 선다. **받은 메시지에만 쓰인다** — 보낸 메시지에는 아바타가 서지 않는다(`direction` 참고). |
16
16
  | `avatarAlt?` | `string` | — | 아바타 대체 텍스트 |
17
+ | `showAvatar?` | `boolean` | `true` | 받은 메시지에 아바타 칸을 세울지. 끄면 **칸 자체가 빠져** 말풍선이 행 왼쪽 끝에서 시작한다. 챗봇 답변처럼 **말하는 상대가 하나뿐인 대화**에서 끈다 — 얼굴로 가를 사람이 없으니 칸이 폭만 먹는다. 여러 사람이 말하는 대화에서는 켜 둔다. **한 대화 안에서는 모든 받은 메시지에 같은 값을 준다** — 섞으면 말풍선 시작선이 묶음마다 44px 씩 어긋난다. 끈 대화에서 `continued` 는 이름만 숨긴다(비워 둘 아바타 자리가 없다). 보낸 메시지에는 원래 아바타가 없으므로 영향이 없다. |
17
18
  | `continued?` | `boolean` | `false` | 같은 사람이 이어서 말한 묶음인지. 켜면 **아바타와 `sender` 를 그리지 않는다** — 말풍선이 위 묶음과 같은 선에서 시작해 한 사람의 말로 이어 읽힌다. 받은 메시지에서는 아바타 자리를 비워 둔 채 남긴다 — 안 남기면 말풍선이 44px 왼쪽으로 밀려 위 묶음과 어긋난다. 보낸 메시지는 아바타가 없으므로 이름만 숨긴다. `time`·`readStatus` 는 그대로 나온다. 시간이 벌어져 묶음을 나눴어도 얼굴은 한 번만 보이면 되고 시각은 묶음마다 필요하기 때문이다. **얼마나 벌어져야 묶음을 나눌지는 앱이 정한다** — 30분이든 하루든 대화의 성격에 달렸으므로 컴포넌트는 기준을 갖지 않고 스위치만 연다. |
18
19
  | `variant?` | `SChatMessageVariant` | `'default'` | 말풍선 종류. `template` 은 미리 등록된 서식으로, `admin` 은 관리자·시스템이 보낸 메시지임을 각각 다른 면으로 드러낸다. 면 색은 토큰이 정하므로 여기서 색을 정하지 않는다. **보낸 메시지(`direction="outgoing"`)에만 쓰인다** — 받은 메시지에는 넘겨도 그리지 않는다. 셋 다 사업자 쪽이 보내는 것이라 받은 쪽에는 뜻이 없다. 앱이 메시지를 한 번만 매핑해 방향과 무관하게 넘길 수 있도록 막지 않고 무시한다. |
19
20
  | `actions?` | `ReactNode` | — | 시각 아래에 붙는 동작 슬롯. 없으면 그 줄이 통째로 빠진다. 「상담원 연결」처럼 **이 메시지에 이어서 할 수 있는 동작**이 오는 자리다. 무엇을 놓을지는 앱이 정하고, 메시지는 줄로 세우고 방향에 맞춰 정렬하는 것까지만 한다. 시각과 같은 선에서 시작한다 — 말풍선 끝에서 한 칸 안이다. |
@@ -50,6 +50,18 @@ export interface SChatMessageProps extends Omit<HTMLAttributes<HTMLDivElement>,
50
50
  avatarSrc?: string;
51
51
  /** 아바타 대체 텍스트 */
52
52
  avatarAlt?: string;
53
+ /**
54
+ * 받은 메시지에 아바타 칸을 세울지. 끄면 **칸 자체가 빠져** 말풍선이 행 왼쪽 끝에서 시작한다.
55
+ *
56
+ * 챗봇 답변처럼 **말하는 상대가 하나뿐인 대화**에서 끈다 — 얼굴로 가를 사람이 없으니 칸이
57
+ * 폭만 먹는다. 여러 사람이 말하는 대화에서는 켜 둔다.
58
+ *
59
+ * **한 대화 안에서는 모든 받은 메시지에 같은 값을 준다** — 섞으면 말풍선 시작선이 묶음마다
60
+ * 44px 씩 어긋난다. 끈 대화에서 `continued` 는 이름만 숨긴다(비워 둘 아바타 자리가 없다).
61
+ *
62
+ * 보낸 메시지에는 원래 아바타가 없으므로 영향이 없다.
63
+ */
64
+ showAvatar?: boolean;
53
65
  /**
54
66
  * 같은 사람이 이어서 말한 묶음인지. 켜면 **아바타와 `sender` 를 그리지 않는다** —
55
67
  * 말풍선이 위 묶음과 같은 선에서 시작해 한 사람의 말로 이어 읽힌다.
@@ -11,6 +11,7 @@
11
11
  | `value?` | `SCheckboxValue` | `false` | 선택 상태. boolean(단일) / 배열(그룹) / null(indeterminate) |
12
12
  | `val?` | `unknown` | — | 배열(그룹) 모드에서 이 체크박스의 값 |
13
13
  | `label?` | `string` | `''` | 라벨 |
14
+ | `ariaLabel?` | `string` | — | 스크린리더용 접근성 레이블. **`label` 이 없을 때만 준다** — 표의 행 선택처럼 라벨 글자 없이 서는 체크박스는 이것이 없으면 "체크박스" 로만 읽힌다. `label` 이 있으면 그 글자가 이미 이름이 된다. 둘 다 주면 이 값이 화면의 라벨을 덮어써 보이는 글자와 읽히는 이름이 갈린다. |
14
15
  | `disabled?` | `boolean` | `false` | 비활성 |
15
16
  | `inverse?` | `boolean` | `false` | 어두운 배경용 흰색 스타일 |
16
17
  | `className?` | `string` | — | |
@@ -9,6 +9,14 @@ export interface SCheckboxProps {
9
9
  onValueChange?: (value: boolean | unknown[]) => void;
10
10
  /** 라벨 */
11
11
  label?: string;
12
+ /**
13
+ * 스크린리더용 접근성 레이블. **`label` 이 없을 때만 준다** — 표의 행 선택처럼 라벨 글자 없이 서는
14
+ * 체크박스는 이것이 없으면 "체크박스" 로만 읽힌다.
15
+ *
16
+ * `label` 이 있으면 그 글자가 이미 이름이 된다. 둘 다 주면 이 값이 화면의 라벨을 덮어써 보이는
17
+ * 글자와 읽히는 이름이 갈린다.
18
+ */
19
+ ariaLabel?: string;
12
20
  /** 비활성 */
13
21
  disabled?: boolean;
14
22
  /** 어두운 배경용 흰색 스타일 */
@@ -20,4 +28,4 @@ export interface SCheckboxProps {
20
28
  * SCheckbox — sd-checkbox 포팅 (Radix SCheckbox).
21
29
  * boolean(단일)·배열(그룹, val 필요)·null(indeterminate) 지원, inverse 스타일.
22
30
  */
23
- export declare function SCheckbox({ value, val, onValueChange, label, disabled, inverse, className, style, }: SCheckboxProps): import("react").JSX.Element;
31
+ export declare function SCheckbox({ value, val, onValueChange, label, ariaLabel, disabled, inverse, className, style, }: SCheckboxProps): import("react").JSX.Element;
@@ -13,7 +13,7 @@
13
13
  | `disabledChips?` | `boolean[] \| ((value: string) => boolean)` | `[]` | 칩별 비활성 (배열 또는 판별 함수) |
14
14
  | `size?` | `SChipInputSize` | `'sm'` | |
15
15
  | `disabled?` | `boolean` | `false` | |
16
- | `placeholder?` | `string` | `'태그 입력 (Enter로 등록 / 콤마로 구분 / 띄어쓰기 불가)'` | |
16
+ | `placeholder?` | `string` | — | 칩이 없을 때 보이는 안내. 주지 않으면 확정 방법을 안내하는 문구가 선다 — `separators` 에 콤마가 있으면 `태그 입력 (Enter·콤마로 등록)`, 없으면 `태그 입력 (Enter로 등록)`. 직접 줄 때도 **실제 확정 방법과 맞춘다** — 콤마를 안내하면서 `separators` 를 주지 않으면 콤마가 글자로 들어가 칩 하나에 값 여러 개가 붙는다. |
17
17
  | `name?` | `string` | — | |
18
18
  | `rules?` | `Rule[]` | — | |
19
19
  | `error?` | `boolean` | — | |
@@ -26,6 +26,9 @@
26
26
  | `recommendedItems?` | `string[]` | `[]` | 추천 항목 (입력값 없을 때) |
27
27
  | `loadingRecommendedItems?` | `boolean` | `false` | |
28
28
  | `dropdownMinWidth?` | `number \| string` | `200` | 드롭다운 최소 너비 (숫자=px) |
29
+ | `commitOnBlur?` | `boolean` | `false` | 입력 칸을 벗어날 때(blur) 쳐 둔 글자를 칩으로 확정할지. 끄면(기본) 친 글자는 칸에 남은 채로 확정되지 않아, 저장하면 빠진다. 이메일 받는 사람처럼 **친 글자가 곧 넣으려는 값인 칸**에서 켠다. 자동완성 후보에서 고르게 하는 칸이라면 끈다 — 덜 친 검색어가 칩으로 굳는다. |
30
+ | `separators?` | `string[]` | — | 칩을 나누는 구분자 (예: `[',', '\n']`). 입력에 구분자가 들어오면 그 앞까지를 칩으로 확정한다. **붙여 넣기에도 적용된다** — 여러 줄·콤마로 이어진 목록을 붙여 넣으면 조각마다 칩이 되고, 마지막 조각까지 모두 확정한다. 앞뒤 공백은 지우고 빈 조각은 버린다. `maxCount` 를 넘는 조각은 버린다. 주지 않으면 Enter 로만 확정한다. |
31
+ | `ariaLabel?` | `string` | — | 스크린리더용 접근성 레이블. **`label` 이 없을 때만 준다** — 라벨 글자 없이 서는 칸은 이것이 없으면 이름 없이 읽힌다. `label` 이 있으면 그 글자가 이미 이름이 된다. |
29
32
  | `label?` | `string` | — | |
30
33
  | `labelPosition?` | `SFieldLabelPosition` | — | 레이블 위치 — `top` 이면 라벨이 컨트롤 위에 선다 (SField 로 그대로 넘어간다). |
31
34
  | `subLabel?` | `string` | — | 보조 레이블 — 레이블 오른쪽 같은 줄에 붙는다 (SField 로 그대로 넘어간다). |
@@ -27,6 +27,13 @@ export interface SChipInputProps {
27
27
  disabledChips?: boolean[] | ((value: string) => boolean);
28
28
  size?: SChipInputSize;
29
29
  disabled?: boolean;
30
+ /**
31
+ * 칩이 없을 때 보이는 안내. 주지 않으면 확정 방법을 안내하는 문구가 선다 —
32
+ * `separators` 에 콤마가 있으면 `태그 입력 (Enter·콤마로 등록)`, 없으면 `태그 입력 (Enter로 등록)`.
33
+ *
34
+ * 직접 줄 때도 **실제 확정 방법과 맞춘다** — 콤마를 안내하면서 `separators` 를 주지 않으면 콤마가
35
+ * 글자로 들어가 칩 하나에 값 여러 개가 붙는다.
36
+ */
30
37
  placeholder?: string;
31
38
  name?: string;
32
39
  rules?: Rule[];
@@ -47,6 +54,29 @@ export interface SChipInputProps {
47
54
  loadingRecommendedItems?: boolean;
48
55
  /** 드롭다운 최소 너비 (숫자=px) */
49
56
  dropdownMinWidth?: number | string;
57
+ /**
58
+ * 입력 칸을 벗어날 때(blur) 쳐 둔 글자를 칩으로 확정할지.
59
+ *
60
+ * 끄면(기본) 친 글자는 칸에 남은 채로 확정되지 않아, 저장하면 빠진다. 이메일 받는 사람처럼
61
+ * **친 글자가 곧 넣으려는 값인 칸**에서 켠다. 자동완성 후보에서 고르게 하는 칸이라면 끈다 —
62
+ * 덜 친 검색어가 칩으로 굳는다.
63
+ */
64
+ commitOnBlur?: boolean;
65
+ /**
66
+ * 칩을 나누는 구분자 (예: `[',', '\n']`). 입력에 구분자가 들어오면 그 앞까지를 칩으로 확정한다.
67
+ *
68
+ * **붙여 넣기에도 적용된다** — 여러 줄·콤마로 이어진 목록을 붙여 넣으면 조각마다 칩이 되고,
69
+ * 마지막 조각까지 모두 확정한다. 앞뒤 공백은 지우고 빈 조각은 버린다. `maxCount` 를 넘는
70
+ * 조각은 버린다.
71
+ *
72
+ * 주지 않으면 Enter 로만 확정한다.
73
+ */
74
+ separators?: string[];
75
+ /**
76
+ * 스크린리더용 접근성 레이블. **`label` 이 없을 때만 준다** — 라벨 글자 없이 서는 칸은 이것이
77
+ * 없으면 이름 없이 읽힌다. `label` 이 있으면 그 글자가 이미 이름이 된다.
78
+ */
79
+ ariaLabel?: string;
50
80
  label?: string;
51
81
  /**
52
82
  * 레이블 위치 — `top` 이면 라벨이 컨트롤 위에 선다 (SField 로 그대로 넘어간다).
@@ -25,6 +25,18 @@ export interface EditorBodyProps {
25
25
  onImageUpload?: (file: File) => Promise<string>;
26
26
  editorClassName?: string;
27
27
  editorStyle?: CSSProperties;
28
+ /**
29
+ * 편집 영역(contenteditable)의 id — SField 레이블이 이 id 로 본문을 가리킨다
30
+ *
31
+ * @nodemo 내부 전용(배럴로 내보내지 않음) — SEditor 가 `label` 에서 정해 넘기는 배선이다.
32
+ */
33
+ controlId: string;
34
+ /**
35
+ * SField 레이블이 그려졌는지. 그려졌을 때만 본문을 레이블 id 로 잇는다
36
+ *
37
+ * @nodemo 내부 전용(배럴로 내보내지 않음) — SEditor 가 `label` 에서 정해 넘기는 배선이다.
38
+ */
39
+ labelled: boolean;
28
40
  }
29
41
  /**
30
42
  * SEditor 의 편집 영역 (내부 전용 — 배럴로 내보내지 않는다).
@@ -21,6 +21,8 @@
21
21
  | `bubbleMenu` | `SEditorToolbarItem[]` | — | 선택 영역 위에 뜨는 서식 판 — 편집할 수 있으면 언제나 뜬다 (SEditor 주석 참고) |
22
22
  | `editorClassName?` | `string` | — | |
23
23
  | `editorStyle?` | `CSSProperties` | — | |
24
+ | `controlId` | `string` | — | 편집 영역(contenteditable)의 id — SField 레이블이 이 id 로 본문을 가리킨다 |
25
+ | `labelled` | `boolean` | — | SField 레이블이 그려졌는지. 그려졌을 때만 본문을 레이블 id 로 잇는다 |
24
26
 
25
27
  #### Events
26
28
 
@@ -10,6 +10,7 @@
10
10
  |------|------|---------|-------------|
11
11
  | `name?` | `string` | — | 폼 연동용 name 속성 |
12
12
  | `label?` | `string` | `''` | 레이블 텍스트 |
13
+ | `controlId?` | `string` | — | 레이블이 가리킬 컨트롤의 `id`. 주면 레이블 글자가 `<label for>` 로 그 컨트롤에 이어져, **스크린리더가 레이블을 컨트롤 이름으로 읽고 레이블을 누르면 컨트롤에 포커스가 간다.** DS 입력 컴포넌트는 알아서 넘긴다 — `SField` 로 직접 컨트롤을 감쌀 때만 준다. 컨트롤이 `input`·`textarea`·`select`·`button` 이 아니면(`role` 로 만든 요소) `<label for>` 가 닿지 않으므로, 컨트롤에 `aria-labelledby={fieldLabelId(controlId)}` 를 직접 건다. |
13
14
  | `subLabel?` | `string` | `''` | 보조 레이블. 레이블 오른쪽 같은 줄에 붙는다 (예: `(선택)`, `최대 50자`). 레이블보다 한 단계 약하게 보이도록 기본 타이포(body/sm/medium)·`fg.secondary` 색을 쓴다. 레이블 폭이 모자라면 레이블이 먼저 말줄임되고 보조 레이블은 그대로 남는다. `label` 이 없으면(`addonLabel` 만 있을 때 포함) 그려지지 않는다. |
14
15
  | `icon?` | `SIconName` | — | 레이블 영역 아이콘명 (크기는 size 토큰 16px 고정) |
15
16
  | `iconColor?` | `SColor` | — | 레이블 영역 아이콘 색상. 미지정 시 currentColor 상속 |
@@ -92,6 +93,7 @@ export type SFieldStatus = 'default' | 'pass' | 'error';
92
93
  - [SNumberInput](../SNumberInput)
93
94
  - [SSearchInput](../SSearchInput)
94
95
  - [SSelect](../SSelect)
96
+ - [SSlider](../SSlider)
95
97
  - [STextarea](../STextarea)
96
98
  - [STimePicker](../STimePicker)
97
99
  - [STimeRangePicker](../STimeRangePicker)
@@ -117,6 +119,7 @@ graph TD;
117
119
  SNumberInput --> SField
118
120
  SSearchInput --> SField
119
121
  SSelect --> SField
122
+ SSlider --> SField
120
123
  STextarea --> SField
121
124
  STimePicker --> SField
122
125
  STimeRangePicker --> SField
@@ -18,6 +18,15 @@ export interface SFieldProps {
18
18
  name?: string;
19
19
  /** 레이블 텍스트 */
20
20
  label?: string;
21
+ /**
22
+ * 레이블이 가리킬 컨트롤의 `id`. 주면 레이블 글자가 `<label for>` 로 그 컨트롤에 이어져,
23
+ * **스크린리더가 레이블을 컨트롤 이름으로 읽고 레이블을 누르면 컨트롤에 포커스가 간다.**
24
+ *
25
+ * DS 입력 컴포넌트는 알아서 넘긴다 — `SField` 로 직접 컨트롤을 감쌀 때만 준다. 컨트롤이
26
+ * `input`·`textarea`·`select`·`button` 이 아니면(`role` 로 만든 요소) `<label for>` 가 닿지 않으므로,
27
+ * 컨트롤에 `aria-labelledby={fieldLabelId(controlId)}` 를 직접 건다.
28
+ */
29
+ controlId?: string;
21
30
  /**
22
31
  * 보조 레이블. 레이블 오른쪽 같은 줄에 붙는다 (예: `(선택)`, `최대 50자`).
23
32
  * 레이블보다 한 단계 약하게 보이도록 기본 타이포(body/sm/medium)·`fg.secondary` 색을 쓴다.
@@ -121,6 +130,16 @@ export interface SFieldProps {
121
130
  onMouseEnter?: () => void;
122
131
  onMouseLeave?: () => void;
123
132
  }
133
+ /**
134
+ * `controlId` 를 받은 SField 가 레이블 글자에 붙이는 id. `<label for>` 가 닿지 않는 컨트롤
135
+ * (`role` 로 만든 요소)은 `aria-labelledby` 로 이 id 를 가리킨다.
136
+ */
137
+ export declare const fieldLabelId: (controlId: string) => string;
138
+ /**
139
+ * DS 입력 컴포넌트가 SField 에 넘길 `controlId` 를 정한다. 앱이 컨트롤에 `id` 를 줬으면 그것을
140
+ * 쓴다 — 앱의 `<label for>`·테스트 선택자가 그 id 를 이미 가리키고 있을 수 있다.
141
+ */
142
+ export declare function useFieldControlId(id?: string): string;
124
143
  /**
125
144
  * SField — sd-field 포팅. 폼 컨트롤 래퍼.
126
145
  * ★ SField 가 컨트롤 테두리 박스(border/radius/bg + hover/focus/error/disabled 상태)를 소유하고,
@@ -1 +1 @@
1
- export { SField, type SFieldHandle, type SFieldProps, type SFieldSize, type SFieldAddonAlign, type SFieldStatus, type SFieldLabelPosition, } from './SField';
1
+ export { SField, fieldLabelId, type SFieldHandle, type SFieldProps, type SFieldSize, type SFieldAddonAlign, type SFieldStatus, type SFieldLabelPosition, } from './SField';
@@ -16,7 +16,7 @@
16
16
  | `trailing?` | `SListItemSlot` | — | 타이틀 뒤에 표시할 태그/콘텐츠 |
17
17
  | `bordered?` | `boolean` | `false` | 외곽 테두리 사용 여부 |
18
18
  | `selected?` | `boolean` | `false` | 선택 상태 여부. `clickable` 이 false 면 무시된다 |
19
- | `clickable?` | `boolean` | `false` | 클릭 가능 상태 여부. hover·selected·chevron 등 인터랙션 표현이 여기에 딸린다. `SExpansionList` 안에서는 기본으로 켜진다 — 표시 전용이면 `false` 를 준다. |
19
+ | `clickable?` | `boolean` | `false` | 클릭 가능 상태 여부. hover·selected·chevron 등 인터랙션 표현이 여기에 딸린다. `SExpansionList` 안에서는 기본으로 켜진다 — 표시 전용이면 `false` 를 준다. **`onClick` 까지 주면 키보드로도 누를 수 있다** — 항목이 `role="button"` 으로 Tab 순서에 들고, Enter·Space 가 `onClick` 을 부른다. 앱이 `role`·`tabIndex` 를 직접 주면 그 값이 우선한다. `onClick` 이 없으면 눌러도 일어날 일이 없으므로 Tab 순서에 넣지 않는다. |
20
20
  | `interaction?` | `SListItemInteraction` | — | hover/selected 상태에서 적용할 인터랙션 preset |
21
21
  | `accentStripe?` | `boolean` | `false` | 아이템 왼쪽 accent stripe 표시 여부 |
22
22
  | `dense?` | `boolean` | `false` | 조밀한 높이 사용 여부 |
@@ -31,6 +31,10 @@ export interface SListItemProps extends Omit<HTMLAttributes<HTMLDivElement>, 'ch
31
31
  /**
32
32
  * 클릭 가능 상태 여부. hover·selected·chevron 등 인터랙션 표현이 여기에 딸린다.
33
33
  * `SExpansionList` 안에서는 기본으로 켜진다 — 표시 전용이면 `false` 를 준다.
34
+ *
35
+ * **`onClick` 까지 주면 키보드로도 누를 수 있다** — 항목이 `role="button"` 으로 Tab 순서에 들고,
36
+ * Enter·Space 가 `onClick` 을 부른다. 앱이 `role`·`tabIndex` 를 직접 주면 그 값이 우선한다.
37
+ * `onClick` 이 없으면 눌러도 일어날 일이 없으므로 Tab 순서에 넣지 않는다.
34
38
  */
35
39
  clickable?: boolean;
36
40
  /** hover/selected 상태에서 적용할 인터랙션 preset */
@@ -0,0 +1,86 @@
1
+ # SSlider
2
+
3
+ > 자동 생성 문서 — `npm run docs:gen`. 소스: 각 컴포넌트의 Props/Handle 인터페이스 + import 의존성.
4
+
5
+ ### SSlider
6
+
7
+ #### Props
8
+
9
+ | Prop | Type | Default | Description |
10
+ |------|------|---------|-------------|
11
+ | `value?` | `V` | — | 값 (제어). **`number` 면 단일, `[시작, 끝]` 이면 범위 슬라이더**가 된다 — 손잡이 개수와 `onValueChange` 가 돌려주는 모양이 이 값을 따른다. 범위는 두 손잡이가 서로를 넘어가지 않는다. |
12
+ | `min?` | `number` | `0` | 최솟값 |
13
+ | `max?` | `number` | `100` | 최댓값 |
14
+ | `step?` | `number` | `1` | 한 칸 크기. 값은 `min` 에서 이 간격으로만 움직인다 |
15
+ | `marks?` | `boolean \| SSliderMark[]` | — | 눈금. `true` 면 `step` 마다 점을 찍고, 배열이면 그 위치에만 찍는다(라벨을 함께 줄 수 있다). 고를 수 있는 값이 몇 개 안 될 때 어디서 멈추는지 보여 주는 용도다 — `step` 이 촘촘하면 `true` 대신 배열로 대표 위치만 준다. |
16
+ | `showValue?` | `SSliderShowValue` | `'drag'` | 값 말풍선 표시 시점. - `drag` — 드래그·키보드 조작 중, 손잡이에 마우스를 올렸을 때만 보인다 - `always` — 늘 보인다. 값을 숫자로 따로 보여 주지 않는 자리에서 쓴다 - `none` — 보이지 않는다. 옆에 값을 따로 적어 둘 때 쓴다 `none` 이 아니면 말풍선 자리만큼 위쪽 공간을 미리 잡아 두어, 나타날 때 레이아웃이 밀리지 않는다. |
17
+ | `formatValue?` | `(value: number) => string` | `String` | 말풍선·스크린리더에 읽히는 값 표기 (예: `v => \`${v}%\``) |
18
+ | `size?` | `SFieldSize` | `'sm'` | 크기 — 다른 필드와 나란히 놓일 때 라벨과 높이를 맞추는 등급이다. 슬라이더 모양 자체는 같다. |
19
+ | `rules?` | `Rule[]` | — | 유효성 규칙 — 조작이 끝날 때(`onValueCommit` 시점)와 SForm 제출 시 검증 |
20
+ | `name?` | `string` | — | 폼 연동용 name |
21
+ | `status?` | `SFieldStatus` | — | 필드 상태 ('default' | 'pass' | 'error') |
22
+ | `label?` | `string` | — | 레이블 텍스트. 손잡이의 접근성 이름으로도 쓰인다 |
23
+ | `labelPosition?` | `SFieldLabelPosition` | — | 레이블 위치 — `top` 이면 라벨이 컨트롤 위에 선다 (SField 로 그대로 넘어간다). |
24
+ | `subLabel?` | `string` | — | 보조 레이블 — 레이블 오른쪽 같은 줄에 붙는다 (SField 로 그대로 넘어간다). |
25
+ | `labelWidth?` | `number \| string` | — | 레이블 너비 (숫자=px) |
26
+ | `icon?` | `SIconName` | — | 레이블 영역 아이콘 |
27
+ | `iconColor?` | `SColor` | — | 레이블 영역 아이콘 색상 |
28
+ | `labelTooltip?` | `string` | — | 레이블 툴팁 텍스트 |
29
+ | `labelTooltipProps?` | `Partial<STooltipProps>` | — | 레이블 툴팁 상세 옵션 |
30
+ | `hint?` | `string` | — | 하단 힌트 |
31
+ | `error?` | `boolean` | — | 에러 상태 |
32
+ | `errorMessage?` | `string` | — | 에러 메시지 |
33
+ | `width?` | `SFieldWidth` | — | 컨트롤 너비 — 폭 등급 · 숫자=px · CSS 길이. 기본은 부모 폭을 채운다. |
34
+ | `disabled?` | `boolean` | `false` | 비활성 — 조작·포커스가 모두 막힌다 |
35
+ | `readOnly?` | `boolean` | `false` | 읽기 전용 — 값은 그대로 보이고 키보드 포커스는 받지만 값이 바뀌지 않는다 |
36
+ | `className?` | `string` | — | |
37
+ | `style?` | `CSSProperties` | — | |
38
+
39
+ #### Events
40
+
41
+ | Event | Type | Description |
42
+ |-------|------|-------------|
43
+ | `onValueChange` | `(value: V) => void` | 값 변경 (sdUpdate) — 드래그 중 매 step 마다 호출된다 |
44
+ | `onValueCommit` | `(value: V) => void` | 조작이 끝났을 때 한 번 호출된다 (포인터를 놓을 때, 키보드로 한 칸 움직일 때). 값을 서버에 보내거나 목록을 다시 불러오는 등 **매 step 마다 하면 안 되는 일**은 여기서 한다. |
45
+
46
+ ## Types
47
+
48
+ ### SSliderValue
49
+
50
+ ```ts
51
+ /** 단일 값은 `number`, 범위는 `[시작, 끝]` */
52
+ export type SSliderValue = number | [number, number];
53
+ ```
54
+
55
+ ### SSliderMark
56
+
57
+ ```ts
58
+ /** 눈금 하나 */
59
+ export interface SSliderMark {
60
+ /** 눈금 위치 값 (`min`~`max`) */
61
+ value: number;
62
+ /** 눈금 아래 라벨. 없으면 점만 찍힌다 */
63
+ label?: string;
64
+ }
65
+ ```
66
+
67
+ ### SSliderShowValue
68
+
69
+ ```ts
70
+ /** 값 말풍선을 언제 보일지 */
71
+ export type SSliderShowValue = 'none' | 'drag' | 'always';
72
+ ```
73
+
74
+ ## Dependencies
75
+
76
+ ### Depends on
77
+
78
+ - [SField](../SField)
79
+
80
+ ### Graph
81
+
82
+ ```mermaid
83
+ graph TD;
84
+ SSlider --> SField
85
+ style SSlider fill:#f9f,stroke:#333,stroke-width:4px
86
+ ```