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

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
@@ -1435,6 +1435,7 @@ tableRef.current.scrollToRow(row); // 복원
1435
1435
  - **`ariaLabel` 은 필수다.** 라벨이 없어 스크린리더가 읽을 것이 아이콘밖에 없다. 뜻이 덜 명확한 아이콘은 **`tooltipText`** 로 이름을 띄운다.
1436
1436
  - 여러 개를 나란히 둘 때 간격은 **`gap-sd-4` 고정** — 가로·세로가 같다 (§2-2).
1437
1437
  - **라벨이 필요하거나 화면의 주 액션이면 `SGhostButton` 이 아니라 `SButton` 이다.** 위계가 낮다는 이유만으로 `SGhostButton` 을 고르지 않는다 — 라벨이 붙는 저강조 액션은 `SButton` 의 `outline` 조합(§3-5-1)이다.
1438
+ - **다른 화면·문서로 가는 아이콘 버튼은 `href` 를 준다.** 링크(`<a>`)로 그려져 가운데 클릭·⌘클릭·주소 복사가 듣는다. `onClick` 에서 `location`·`window.open` 으로 옮기거나 `<a>` 로 감싸지 않는다. 새 탭이면 `target="_blank"` 만 준다 — `rel` 은 컴포넌트가 붙인다.
1438
1439
 
1439
1440
  ```tsx
1440
1441
  // 테이블 행 끝 인라인 액션 — 수정은 action, 행 삭제는 default, 간격은 gap-sd-4
@@ -1452,6 +1453,8 @@ tableRef.current.scrollToRow(row); // 복원
1452
1453
  ✅ <SGhostButton icon="close" ariaLabel="닫기" onClick={onClose} /> // 모달 닫기 = default
1453
1454
  ✅ <SGhostButton icon="copy" ariaLabel="상품코드 복사" tooltipText="복사" /> // 뜻이 덜 명확 → 툴팁
1454
1455
  ❌ <SGhostButton icon="download" ariaLabel="엑셀 다운로드" /> // 목록의 주 액션 → SButton
1456
+ ✅ <SGhostButton icon="helpOutline" ariaLabel="도움말" href={HELP_URL} target="_blank" />
1457
+ ❌ <SGhostButton icon="helpOutline" ariaLabel="도움말" onClick={() => window.open(HELP_URL)} /> // 가는 버튼 → href
1455
1458
  ```
1456
1459
 
1457
1460
  ##### `SMeatballButton` — ⋯ 뒤에 메뉴를 접어 둘 때
@@ -1877,12 +1880,21 @@ useEffect(() => {
1877
1880
  | `STimePicker` | md | lg |
1878
1881
  | `STimeRangePicker` | md | lg (오전/오후 표시는 두 사이즈 모두 lg) |
1879
1882
 
1883
+ `SDatePicker`·`SDateRangePicker` 에 `showWeekday` 를 켜면 날짜 뒤에 요일이 붙어(`2026-10-19 월요일`) 붙는 글자(띄어쓰기 + 요일 3자) 폭만큼 위 표의 하한·상한이 넓어진다 — 등급이 바뀌는 것이 아니다. 늘어난 폭은 컴포넌트가 스스로 잡으므로 요일 때문에 `width` 를 따로 주지 않는다. 표시만 바뀌고 값 형식은 그대로라, **요일을 붙이려고 값을 가공하거나 `hint` 에 요일을 따로 적지 않는다.**
1884
+
1880
1885
  `SDateRangePicker` 가 한 등급씩 위인 것은 값이 `YYYY-MM-DD ~ YYYY-MM-DD` 로 두 배가 넘기 때문이다. 같은 이유로 `STimeRangePicker` 의 오전/오후 모드도 sm 에서 한 등급 위를 쓴다 — 그 모드의 최소 폭이 md 등급을 이미 넘어, 그대로 두면 하한이 상한을 넘어 상한이 무력해진다.
1881
1886
 
1882
1887
  네 컴포넌트 모두 이 상한을 `maxWidth` 로 덮을 수 있다 — 등급을 주면 그 등급이 상한이 되고, `width="100%" maxWidth="100%"` 면 부모 폭을 그대로 채운다. **폭이 이미 좁게 정해진 자리(팝오버·좁은 카드)에서 쓴다.**
1883
1888
 
1884
1889
  **`SKeyValueTable` 안에서는 필드마다 `maxWidth` 를 주지 않는다.** 표 안에서 이 상한이 거슬리는 이유는 대개 "같은 행의 다른 필드와 오른쪽 끝이 어긋난다" 이고, 그건 표 전체의 문제다. 표의 `fieldWidth="fill"` 로 한 번에 정한다 (§4-3). 그 표에서 한 필드만 예외로 둘 때만 `options.maxWidth` 를 쓴다.
1885
1890
 
1891
+ ##### 시간 피커 — 분 간격은 `minuteStep`, 범위 순서는 `rangeOrder`
1892
+
1893
+ `STimePicker`·`STimeRangePicker` 는 `[오전/오후] | 시 | 분` 리스트에서 고른다 (직접 타이핑하지 않는다).
1894
+
1895
+ - **분 간격은 `minuteStep` 으로 정한다.** 분 리스트에 그 간격의 분만 나열되므로, 고른 값을 앱에서 다시 반올림하거나 `rules` 로 간격을 검사하지 않는다.
1896
+ - **`STimeRangePicker` 의 "종료가 시작보다 이르면 안 된다" 는 앱이 검증하지 않는다.** 기본 `rangeOrder="strict"` 가 종료 리스트에서 그 시각을 비활성으로 막는다. 자정을 넘는 범위(야간 근무 22:00~02:00 등)를 받아야 할 때만 `rangeOrder="allow-cross-day"` 를 준다.
1897
+
1886
1898
  ##### 값을 지울 수 있게 하려면 `clearable`
1887
1899
 
1888
1900
  `SSelect` · `SDatePicker` · `SDateRangePicker` · `STimePicker` · `STimeRangePicker` 가 같은 규칙으로 갖는다. 값이 있을 때만 지우기 버튼이 나타나고, 누르면 **`onValueChange` 로 `null` 이 온다** (빈 문자열이 아니다). 받는 쪽 상태도 `null` 을 담을 수 있어야 한다.
@@ -1975,7 +1987,7 @@ const [from, setFrom] = useState<string | null>(null);
1975
1987
  - **펼침 버튼·들여쓰기는 컴포넌트가 그린다.** 기본은 보이는 첫 컬럼이고 `treeColumn` 으로 옮긴다. 그 컬럼은 왼쪽 정렬로 두고, 셀 내용은 `render`·`format` 으로 바꾼다 — `renderCell` 은 펼침 버튼을 넣을 자리가 없어 에러가 난다.
1976
1988
  - **셀 병합은 가로(`colSpan`)만 한다.** 트리 컬럼이 아닌 컬럼은 `renderCell` 로 `<td>` 를 바꿀 수 있고, 한 행 안에서 끝나는 `colSpan` 은 접고 펴도 그대로다. **`rowSpan` 으로 세로 병합하지 않는다** — 행이 접히면 사라진 행 몫까지 병합이 남아 아래 행의 칸을 덮고, 덮인 행의 칸은 옆 컬럼으로 밀려난다. 부모와 하위 행에 같은 값이 반복되면 부모 행에만 쓰고 하위 행은 비워 둔다.
1977
1989
  - **펼침 영역은 트리 컬럼 셀 전체다.** 하위 행이 있는 행은 펼침 버튼뿐 아니라 그 셀 어디를 눌러도 접고 편다. 다른 셀을 누르면 행 클릭(`onRowClick`)이다 — 트리 컬럼 셀을 누른 것은 행 클릭으로 가지 않는다.
1978
- - **펼침 모션은 컴포넌트가 넣는다.** 펼쳐서 새로 보이게 된 행이 위에서 내려오며 나타나고(접을 때는 바로 사라진다), 모션을 줄인 환경에서는 꺼진다. 행에 애니메이션 클래스를 직접 붙이지 않는다.
1990
+ - **펼침·접힘 모션은 컴포넌트가 넣는다.** 하위 행이 부모 아래에서 서랍처럼 펼쳐지고 접히며, 아래 행들도 함께 내려가고 올라온다. 모션을 줄인 환경에서는 꺼진다. 행(`rowClassName`)에 애니메이션·`transform`·`clip-path` 를 직접 주지 않는다 — 컴포넌트가 행의 위치를 움직이는 것과 겹친다. `onExpandedChange` 는 모션을 기다리지 않고 누른 즉시 온다.
1979
1991
  - **트리 컬럼에 링크·버튼을 넣어도 된다.** 셀 안의 링크·버튼·입력을 누르면 그 동작만 일어나고 펼침은 일어나지 않는다 — `stopPropagation` 을 직접 넣지 않는다. 상품명을 상세 링크로 거는 것처럼 쓴다.
1980
1992
  - **선택은 계층을 따른다.** 부모를 고르면 접힌 하위 행까지 함께 골라지고, 일부만 골라진 부모는 "일부 선택"으로 그려진다. 하위 행이 모두 골라지면 부모도 `selected` 에 들어간다 — 처리 대상이 잎 행뿐이면 받은 목록에서 하위 행이 없는 행만 걸러 쓴다.
1981
1993
  - **가로로 스크롤되는 표는 트리 컬럼을 왼쪽에 고정한다**(`stickyColumn={{ left: 1 }}`, 트리 컬럼이 첫 컬럼일 때). 스크롤로 들여쓰기가 밀려나면 어느 행이 누구의 하위인지 읽히지 않는다. 체크박스 열은 `selectable` 이면 늘 고정되므로 개수에 넣지 않는다.
@@ -2024,6 +2036,13 @@ const [selectedId, setSelectedId] = useState<string>();
2024
2036
  - **리스트 밖 단독 `SExpansionItem` 은 본문을 다루지 않는다.** 선택하는 목록을 담으려면 `SExpansionList` 로 감싼다.
2025
2037
 
2026
2038
  - **하위를 가지지 않는 항목은 `SExpansionItem` 이 아니라 `SListItem`** 으로 둔다. 펼칠 것이 없는데 펼침 항목으로 만들면 화살표만 남는다.
2039
+ - **제목 줄에 누르는 컨트롤(⋯ 메뉴·아이콘 버튼)은 `headerAction` 에 둔다.** 제목 줄은 통째로 여닫는 버튼이라 `title`·`trailing` 에 컨트롤을 넣으면 버튼이 중첩되고 누를 때마다 함께 여닫힌다. `headerAction` 은 그 버튼 밖에 그려져 눌러도 여닫히지 않는다 — `stopPropagation` 이나 제어 모드(`expanded`)로 막지 않는다. 태그·건수처럼 보기만 하는 것은 그대로 `trailing` 이다.
2040
+
2041
+ ```tsx
2042
+ ✅ <SExpansionItem title="상품 문의" trailing={<STag label="3" color="grey" />}
2043
+ headerAction={<SMeatballButton size="xs" items={folderMenu} onItemClick={v => onFolderAction(id, v)} />}>…</SExpansionItem>
2044
+ ❌ <SExpansionItem title="상품 문의" trailing={<SMeatballButton … />}>…</SExpansionItem> {/* 버튼 안 버튼 — 누르면 함께 여닫힌다 */}
2045
+ ```
2027
2046
 
2028
2047
  #### 3-7-8. SCard vs SSectionHeaderCard
2029
2048
 
@@ -4510,7 +4529,8 @@ export default function StoreSetupPage() {
4510
4529
  - **날짜가 바뀌는 자리에는 날짜 구분선을 끼운다** — 가로 `SDivider` 두 개 사이에 날짜(`typo-body-sm-default` + `text-fg-tertiary`). 일어난 사실이 아니라 흐름의 경계라 시스템 안내 알약(`SChatSystemMessage`)을 쓰지 않는다. **날짜는 `2026년 10월 6일 화요일`** 로 쓴다 — 말풍선 시각과 같은 대화 예외다(§1-4). `toLocaleDateString()` 은 로케일에 따라 결과가 바뀌므로 쓰지 않고 연·월·일·요일을 직접 이어 붙인다. 가로 구분선은 기본이 전체 폭이라 한 줄에 둘을 놓을 때는 `className="w-auto flex-1"` 로 남은 폭을 나눠 갖게 한다. 앞뒤 간격은 시스템 안내와 같은 토큰이다. 날짜가 다르면 같은 시각이어도 한 묶음으로 합치지 않는다.
4511
4530
  - 대화의 시각은 `오후 5:52` 로 쓴다 (§1-4 대화 예외). 읽음 표시는 보낸 메시지에만, 템플릿으로 보낸 글은 `variant="template"` 로 면을 나눈다.
4512
4531
  - **입력 중인 글은 페이지가 상담마다 든다.** 상담을 옮겨도 쓰던 글이 남고, 템플릿 패널이 그 글에 넣을 수 있다. 템플릿은 누르면 **입력창에 넣는다** — 바로 보내지 않고 상담사가 고쳐 보낸다. 보내기 전 첨부는 입력창 위에 `SChatAttachedFile` 로 쌓는다.
4513
- - **첨부를 받는 대화면 `onAttach` 와 `onPasteFiles` 를 함께 넘긴다** — 붙여넣은 파일·캡처 이미지도 첨부 버튼으로 고른 것과 같은 자리에 쌓는다. 붙여넣기에서 파일을 골라내고 글로 들어가지 않게 막는 것은 입력창이 한다 — `onPaste` 로 직접 처리하지 않는다. 첨부가 쌓여 있으면 `hasAttachments` 를 켠다 — 글 없이 파일만 보낼 수 있고, 이때 `onSend` 에는 빈 문자열이 온다.
4532
+ - **첨부를 받는 대화면 `onAttachFiles` 와 `onPasteFiles` 에 같은 함수를 넘긴다** — 첨부 버튼으로 고른 파일과 붙여넣은 파일·캡처 이미지를 같은 자리에 쌓는다. 파일 선택 창은 입력창이 연다 — `<input type="file">` 을 따로 두지 않는다. `onAttach` 는 서버 파일함 모달처럼 앱이 직접 고르는 흐름에만 쓴다. 붙여넣기에서 파일을 골라내고 글로 들어가지 않게 막는 것은 입력창이 한다 — `onPaste` 로 직접 처리하지 않는다. 첨부가 쌓여 있으면 `hasAttachments` 를 켠다 — 글 없이 파일만 보낼 수 있고, 이때 `onSend` 에는 빈 문자열이 온다.
4533
+ - **입력창의 일부만 막을 때는 그 동작만 잠근다.** 첨부가 올라가는 중이면 `sendDisabled` 로 전송만 잠근다 — 글을 적고 파일을 더 붙이는 것은 된다. 첨부 수가 상한에 닿았으면 `attachDisabled` 로 첨부를 잠그고 이유는 `attachTooltip` 으로 알린다 — 파일 붙여넣기도 함께 막힌다. 상한 아래에서 여러 파일이 한 번에 들어와 넘치는 것은 앱이 `onPasteFiles` 에서 잘라 낸다. `disabled` 는 입력까지 잠그므로 종료된 대화에만, `busy` 는 정지 버튼으로 바뀌므로 응답을 기다릴 때만 쓴다.
4514
4534
  - **상담 종료는 `SChatInput` 의 `footerAction` 이다** — 전송과 층이 다른 동작이라 입력창 아래 보조 슬롯(`STextLink`)에 두고, 실행 전에 `SModal.confirm` 으로 묻는다. 종료된 상담은 입력창을 `disabled` 로 잠그고 종료 링크를 뺀다.
4515
4535
  - 목록의 상태는 `STag` 다 — 색 점(`SBadge`)만으로 찍지 않는다 (§3-1). 템플릿 분류는 `SExpansionList` 루트 하나에 분류(`SExpansionItem`) 안 템플릿(`SListItem`)을 바로 넣는다 (§3-7-7).
4516
4536
 
@@ -4734,7 +4754,14 @@ function Conversation({
4734
4754
  }) {
4735
4755
  const initial = useConsultMessages(consult.id);
4736
4756
  const [entries, setEntries] = useState<ChatEntry[]>(initial);
4737
- const [attached, setAttached] = useState<string[]>([]);
4757
+ // 보내기 전 첨부. 같은 파일을 두 번 붙일 수 있으므로 이름이 아니라 id 로 가른다
4758
+ const [attached, setAttached] = useState<{ id: number; file: File }[]>([]);
4759
+ const nextAttachId = useRef(0);
4760
+ // 첨부 버튼으로 고른 파일과 붙여넣은 파일을 같은 함수로 받아 같은 자리에 쌓는다
4761
+ const addFiles = (files: File[]) => {
4762
+ const added = files.map(file => ({ id: nextAttachId.current++, file }));
4763
+ setAttached(current => [...current, ...added]);
4764
+ };
4738
4765
  const scrollRef = useRef<HTMLDivElement>(null);
4739
4766
  const closed = consult.status === 'closed';
4740
4767
 
@@ -4764,13 +4791,13 @@ function Conversation({
4764
4791
  },
4765
4792
  ]
4766
4793
  : []),
4767
- ...attached.map((name, i) => ({
4794
+ ...attached.map(({ id, file }) => ({
4768
4795
  kind: 'message' as const,
4769
- id: `f${current.length}-${i}`,
4796
+ id: `f${current.length}-${id}`,
4770
4797
  direction: 'outgoing' as const,
4771
4798
  date,
4772
4799
  time,
4773
- file: { name, size: 204800 },
4800
+ file: { name: file.name, size: file.size },
4774
4801
  })),
4775
4802
  ]);
4776
4803
  onDraftChange('');
@@ -4861,11 +4888,11 @@ function Conversation({
4861
4888
  <div className="flex flex-col gap-sd-8 py-sd-16">
4862
4889
  {attached.length > 0 && (
4863
4890
  <div className="flex flex-row flex-wrap gap-sd-8 px-sd-24">
4864
- {attached.map(name => (
4891
+ {attached.map(({ id, file }) => (
4865
4892
  <SChatAttachedFile
4866
- key={name}
4867
- name={name}
4868
- onRemove={() => setAttached(current => current.filter(n => n !== name))}
4893
+ key={id}
4894
+ name={file.name}
4895
+ onRemove={() => setAttached(current => current.filter(a => a.id !== id))}
4869
4896
  />
4870
4897
  ))}
4871
4898
  </div>
@@ -4875,9 +4902,9 @@ function Conversation({
4875
4902
  disabled={closed}
4876
4903
  onValueChange={onDraftChange}
4877
4904
  onSend={send}
4878
- onAttach={() => setAttached(current => [...current, `첨부_${current.length + 1}.png`])}
4879
- // 붙여넣은 파일·캡처 이미지도 첨부 버튼으로 고른 것과 같은 자리에 쌓는다
4880
- onPasteFiles={files => setAttached(current => [...current, ...files.map(f => f.name)])}
4905
+ // 파일 선택 창은 입력창이 연다 — <input type="file"> 을 따로 두지 않는다
4906
+ onAttachFiles={addFiles}
4907
+ onPasteFiles={addFiles}
4881
4908
  // 첨부가 쌓여 있으면 글 없이도 보낼 수 있다
4882
4909
  hasAttachments={attached.length > 0}
4883
4910
  // 상담 종료는 전송과 층이 다른 동작 — 입력창 아래 보조 액션 슬롯이다
@@ -12,6 +12,11 @@
12
12
  | `placeholder?` | `string` | `DEFAULT_PLACEHOLDER` | 안내 문구. 세 제품이 같은 말로 서도록 기본값을 쥐고 있다 |
13
13
  | `disabled?` | `boolean` | `false` | 비활성 |
14
14
  | `hasAttachments?` | `boolean` | `false` | 보낼 첨부가 쌓여 있는지. 켜면 글이 비어도 전송 버튼이 열린다 — 파일·이미지만 보낼 수 있다. 첨부 목록은 앱이 쥐므로 입력창은 그 유무만 받는다. 첨부를 받는 대화면 쌓인 첨부 수로 켠다. |
15
+ | `attachAccept?` | `string` | — | 파일 선택 창에서 고를 수 있는 형식 (`<input accept>` 와 같다). 예: `'image/*,.pdf'` |
16
+ | `attachMultiple?` | `boolean` | `true` | 파일 선택 창에서 여러 개를 한 번에 고를 수 있는지 |
17
+ | `attachDisabled?` | `boolean` | `false` | 첨부를 잠근다 — 첨부 버튼(파일 선택 창)이 잠기고, **파일 붙여넣기도 받지 않는다**. 입력과 전송은 그대로 둔다. 첨부 수가 상한에 닿았을 때처럼 **더 붙일 수 없는 자리**에 쓴다. 버튼만 잠그면 캡처 이미지를 붙여넣어 상한을 넘길 수 있으므로 붙여넣기도 함께 막는다 — 파일이 든 붙여넣기는 버려지고 (`onPasteFiles` 가 불리지 않는다) 파일 이름이 글로 들어가지도 않는다. 글만 붙여넣는 것은 된다. 왜 잠겼는지는 `attachTooltip` 으로 알린다. |
18
+ | `attachTooltip?` | `string` | — | 첨부 버튼에 마우스를 올리면 뜨는 툴팁. 받을 수 있는 파일 종류·크기나, 잠긴 이유를 적는다. 버튼의 접근성 이름("파일 첨부")은 바뀌지 않는다. |
19
+ | `sendDisabled?` | `boolean` | `false` | 전송을 잠근다 — 전송 버튼과 Enter 전송이 함께 잠기고, 입력과 첨부는 그대로 둔다. 첨부가 올라가는 중처럼 **아직 보낼 수 없지만 적고 붙이는 것은 되는 자리**에 쓴다. 응답을 기다리는 중이면 이것이 아니라 `busy` 를 켠다 — 그쪽은 정지 버튼으로 바뀐다. |
15
20
  | `busy?` | `boolean` | `false` | 응답을 기다리는 중인지. 켜면 전송 버튼 자리에 정지 버튼이 서고 Enter 전송이 잠긴다 — 답이 오는 동안 또 보내지 못하게 막는 자리다. |
16
21
  | `footerAction?` | `ReactNode` | — | 입력창 아래 보조 액션 슬롯. 없으면 그 줄이 통째로 빠진다. 상담 종료처럼 **대화 자체를 끝내는, 전송과 층이 다른 동작**이 오는 자리다. 무엇을 놓을지는 앱이 정하고, 입력창은 가운데에 세우는 것까지만 한다 — `STextLink` 를 넣는 것이 보통이다. |
17
22
  | `maxRows?` | `number` | `DEFAULT_MAX_ROWS` | 입력창이 자랄 수 있는 최대 줄 수. 여기 닿으면 더 자라지 않고 그 안에서 스크롤한다. |
@@ -25,8 +30,9 @@
25
30
  |-------|------|-------------|
26
31
  | `onValueChange` | `(value: string) => void` | 값 변경 |
27
32
  | `onSend` | `(value: string) => void` | 전송. 전송 버튼을 누르거나 Enter 를 쳤을 때 현재 값을 넘긴다. **값이 비면 전송 버튼이 잠긴다** — 빈 메시지는 보낼 수 없다. 단 `hasAttachments` 가 켜져 있으면 글 없이도 보낼 수 있고, 이때 빈 문자열을 넘긴다. |
28
- | `onAttach` | `() => void` | 파일 첨부. **주면 입력창 왼쪽에 첨부 버튼이 선다** |
29
- | `onPasteFiles` | `(files: File[]) => void` | 붙여넣은 파일. 클립보드에 파일(캡처한 이미지 포함)이 있으면 불린다. **파일이 있으면 글자 붙여넣기를 막는다** — 그대로 두면 파일 이름이 글로 함께 들어간다. 파일이 없는 붙여넣기는 평소대로 글로 들어간다. 받은 파일을 어디에 쌓을지는 앱이 정한다 — 첨부 버튼(`onAttach`)으로 고른 파일과 같은 자리에 쌓는 것이 보통이다. |
33
+ | `onAttachFiles` | `(files: File[]) => void` | 첨부 버튼으로 고른 파일. **주면 입력창 왼쪽에 첨부 버튼이 서고, 누르면 파일 선택 창이 열린다.** 붙여넣기(`onPasteFiles`)와 같은 모양으로 받는다 — 두 길을 같은 함수로 받아 같은 자리에 쌓는다. 파일 선택 창은 입력창이 연다. 앱이 `<input type="file">` 을 따로 두지 않는다. |
34
+ | `onAttach` | `() => void` | 첨부 버튼을 누름. **주면 입력창 왼쪽에 첨부 버튼이 선다** — 파일 선택 창은 열리지 않는다. 서버 파일함 모달처럼 **앱이 직접 고르는 흐름**에만 쓴다. 기기의 파일을 고르는 것이면 `onAttachFiles` 를 준다. 둘 다 주면 `onAttachFiles` 가 쓰이고 이것은 불리지 않는다. |
35
+ | `onPasteFiles` | `(files: File[]) => void` | 붙여넣은 파일. 클립보드에 파일(캡처한 이미지 포함)이 있으면 불린다. **파일이 있으면 글자 붙여넣기를 막는다** — 그대로 두면 파일 이름이 글로 함께 들어간다. 파일이 없는 붙여넣기는 평소대로 글로 들어간다. 받은 파일을 어디에 쌓을지는 앱이 정한다 — 첨부 버튼으로 고른 파일(`onAttachFiles`)과 같은 함수로 받아 같은 자리에 쌓는 것이 보통이다. |
30
36
  | `onStop` | `() => void` | 정지. `busy` 일 때 정지 버튼을 누르면 불린다 |
31
37
 
32
38
  ## Dependencies
@@ -25,14 +25,52 @@ export interface SChatInputProps extends Omit<TextareaHTMLAttributes<HTMLTextAre
25
25
  * 첨부 목록은 앱이 쥐므로 입력창은 그 유무만 받는다. 첨부를 받는 대화면 쌓인 첨부 수로 켠다.
26
26
  */
27
27
  hasAttachments?: boolean;
28
- /** 파일 첨부. **주면 입력창 왼쪽에 첨부 버튼이 선다** */
28
+ /**
29
+ * 첨부 버튼으로 고른 파일. **주면 입력창 왼쪽에 첨부 버튼이 서고, 누르면 파일 선택 창이 열린다.**
30
+ *
31
+ * 붙여넣기(`onPasteFiles`)와 같은 모양으로 받는다 — 두 길을 같은 함수로 받아 같은 자리에 쌓는다.
32
+ * 파일 선택 창은 입력창이 연다. 앱이 `<input type="file">` 을 따로 두지 않는다.
33
+ */
34
+ onAttachFiles?: (files: File[]) => void;
35
+ /** 파일 선택 창에서 고를 수 있는 형식 (`<input accept>` 와 같다). 예: `'image/*,.pdf'` */
36
+ attachAccept?: string;
37
+ /** 파일 선택 창에서 여러 개를 한 번에 고를 수 있는지 */
38
+ attachMultiple?: boolean;
39
+ /**
40
+ * 첨부 버튼을 누름. **주면 입력창 왼쪽에 첨부 버튼이 선다** — 파일 선택 창은 열리지 않는다.
41
+ *
42
+ * 서버 파일함 모달처럼 **앱이 직접 고르는 흐름**에만 쓴다. 기기의 파일을 고르는 것이면
43
+ * `onAttachFiles` 를 준다. 둘 다 주면 `onAttachFiles` 가 쓰이고 이것은 불리지 않는다.
44
+ */
29
45
  onAttach?: () => void;
46
+ /**
47
+ * 첨부를 잠근다 — 첨부 버튼(파일 선택 창)이 잠기고, **파일 붙여넣기도 받지 않는다**. 입력과
48
+ * 전송은 그대로 둔다.
49
+ *
50
+ * 첨부 수가 상한에 닿았을 때처럼 **더 붙일 수 없는 자리**에 쓴다. 버튼만 잠그면 캡처 이미지를
51
+ * 붙여넣어 상한을 넘길 수 있으므로 붙여넣기도 함께 막는다 — 파일이 든 붙여넣기는 버려지고
52
+ * (`onPasteFiles` 가 불리지 않는다) 파일 이름이 글로 들어가지도 않는다. 글만 붙여넣는 것은 된다.
53
+ * 왜 잠겼는지는 `attachTooltip` 으로 알린다.
54
+ */
55
+ attachDisabled?: boolean;
56
+ /**
57
+ * 첨부 버튼에 마우스를 올리면 뜨는 툴팁. 받을 수 있는 파일 종류·크기나, 잠긴 이유를 적는다.
58
+ * 버튼의 접근성 이름("파일 첨부")은 바뀌지 않는다.
59
+ */
60
+ attachTooltip?: string;
61
+ /**
62
+ * 전송을 잠근다 — 전송 버튼과 Enter 전송이 함께 잠기고, 입력과 첨부는 그대로 둔다.
63
+ *
64
+ * 첨부가 올라가는 중처럼 **아직 보낼 수 없지만 적고 붙이는 것은 되는 자리**에 쓴다. 응답을
65
+ * 기다리는 중이면 이것이 아니라 `busy` 를 켠다 — 그쪽은 정지 버튼으로 바뀐다.
66
+ */
67
+ sendDisabled?: boolean;
30
68
  /**
31
69
  * 붙여넣은 파일. 클립보드에 파일(캡처한 이미지 포함)이 있으면 불린다.
32
70
  *
33
71
  * **파일이 있으면 글자 붙여넣기를 막는다** — 그대로 두면 파일 이름이 글로 함께 들어간다.
34
72
  * 파일이 없는 붙여넣기는 평소대로 글로 들어간다. 받은 파일을 어디에 쌓을지는 앱이 정한다 —
35
- * 첨부 버튼(`onAttach`)으로 고른 파일과 같은 자리에 쌓는 것이 보통이다.
73
+ * 첨부 버튼으로 고른 파일(`onAttachFiles`)과 같은 함수로 받아 같은 자리에 쌓는 것이 보통이다.
36
74
  */
37
75
  onPasteFiles?: (files: File[]) => void;
38
76
  /**
@@ -65,13 +103,25 @@ export interface SChatInputProps extends Omit<TextareaHTMLAttributes<HTMLTextAre
65
103
  *
66
104
  * 첨부·입력·전송을 한 줄로 세우고, 필요하면 그 아래 보조 액션(상담 종료 등)을 붙인다.
67
105
  * Enter 로 보내고 Shift+Enter 로 줄을 바꾼다 — 한글 조합 중의 Enter 는 보내지 않는다.
68
- * 파일·이미지를 붙여넣으면 `onPasteFiles` 로 넘기고, 첨부가 쌓여 있으면(`hasAttachments`)
69
- * 글 없이도 보낼 수 있다.
106
+ * 첨부 버튼으로 고른 파일은 `onAttachFiles`, 붙여넣은 파일은 `onPasteFiles` 로 넘기고, 첨부가
107
+ * 쌓여 있으면(`hasAttachments`) 글 없이도 보낼 수 있다.
70
108
  *
71
109
  * **글이 길어지면 입력창이 자란다** — `maxRows` 줄까지 늘어나고, 거기 닿으면 그 안에서
72
110
  * 스크롤한다. 상한이 없으면 대화가 화면 밖으로 밀려난다.
73
111
  *
74
112
  * **응답을 기다리는 동안에는 `busy` 를 켠다** — 전송 버튼이 정지 버튼으로 바뀌고 Enter 전송이
75
113
  * 잠겨, 답이 오는 중에 또 보내는 일이 막힌다.
114
+ *
115
+ * **잠금 규칙** — 무엇을 막을지에 따라 하나만 켠다. 함께 켜면 표의 위쪽이 이긴다.
116
+ *
117
+ * | prop | 입력 | 첨부(파일 선택·붙여넣기) | 전송(버튼·Enter) | 정지 | 쓰는 자리 |
118
+ * | --- | --- | --- | --- | --- | --- |
119
+ * | `disabled` | 잠금 | 잠금 | 잠금 | 잠금 | 대화가 끝났다 |
120
+ * | `busy` | 열림 | 그대로 | 정지 버튼으로 바뀜 | 열림 | 응답을 기다린다 |
121
+ * | `sendDisabled` | 열림 | 그대로 | 잠금 | — | 첨부가 올라가는 중이다 |
122
+ * | `attachDisabled` | 열림 | 잠금 | 그대로 | — | 첨부 수가 상한에 닿았다 |
123
+ *
124
+ * 첨부 잠금은 나머지와 따로 움직인다 — `busy`·`sendDisabled` 와 함께 켜도 첨부만 더
125
+ * 잠길 뿐이다. 전송은 이와 별개로 글도 첨부(`hasAttachments`)도 없으면 잠긴다.
76
126
  */
77
127
  export declare const SChatInput: import("react").ForwardRefExoticComponent<SChatInputProps & import("react").RefAttributes<HTMLTextAreaElement>>;
@@ -33,6 +33,7 @@
33
33
  | `placeholder?` | `string` | — | |
34
34
  | `selectable?` | `[string, string]` | — | 선택 가능 범위 [시작, 종료] |
35
35
  | `disabled?` | `boolean` | `false` | |
36
+ | `showWeekday?` | `boolean` | `false` | 트리거에 보이는 날짜 뒤에 요일을 붙인다 — `2026-10-19 월요일`. 표시만 바뀌고 `value`·`onValueChange` 의 형식(`YYYY-MM-DD`)은 그대로다. `mode="date"` 에서만 동작한다 (월·연도에는 요일이 없다). 붙는 글자(띄어쓰기 + 요일 3자)만큼 내장 폭 하한·상한이 넓어진다. |
36
37
  | `clearable?` | `boolean` | `false` | 선택값 지우기 버튼. 값이 있을 때만 나타나고, 누르면 `onValueChange` 로 `null` 이 온다. **필수 입력 필드에는 켜지 않는다** — 지우면 다시 고르기 전까지 폼이 통과하지 못한다. `STimePicker` · `STimeRangePicker` · `SSelect` 의 `clearable` 과 같은 규칙이다. |
37
38
  | `width?` | `SFieldWidth` | — | 컨트롤 너비 — 폭 등급(`'xs' | 'sm' | 'md' | 'lg' | 'xl'`) · 숫자=px · CSS 길이. 등급은 `--cmp-field-width-*` 토큰으로 풀리므로 토큰이 바뀌면 화면이 따라간다. 값 길이에 상한이 없으면 `"100%"` 로 두어 행 전체를 쓴다. |
38
39
  | `maxWidth?` | `SFieldWidth` | — | 컨트롤 최대 너비 — 폭 등급 · 숫자=px · CSS 길이. 지정하지 않으면 값 길이(`YYYY-MM-DD`)에 맞춘 내장 상한(`size='md'` → `lg`, `'sm'` → `md`)이 걸려, `width="100%"` 를 받아도 행 전체로 늘어나지 않는다. 팝오버처럼 폭이 이미 좁게 정해진 자리에서 그 폭을 그대로 채워야 하면 `width="100%"` 와 함께 `maxWidth="100%"` 를 준다. `0` 은 상한을 아예 걸지 않는다 — 부모보다 넓어지는 것까지 허용해야 할 때만 쓴다. |
@@ -24,6 +24,14 @@ export interface SDatePickerProps {
24
24
  /** 선택 가능 범위 [시작, 종료] */
25
25
  selectable?: [string, string];
26
26
  disabled?: boolean;
27
+ /**
28
+ * 트리거에 보이는 날짜 뒤에 요일을 붙인다 — `2026-10-19 월요일`.
29
+ *
30
+ * 표시만 바뀌고 `value`·`onValueChange` 의 형식(`YYYY-MM-DD`)은 그대로다.
31
+ * `mode="date"` 에서만 동작한다 (월·연도에는 요일이 없다).
32
+ * 붙는 글자(띄어쓰기 + 요일 3자)만큼 내장 폭 하한·상한이 넓어진다.
33
+ */
34
+ showWeekday?: boolean;
27
35
  /**
28
36
  * 선택값 지우기 버튼. 값이 있을 때만 나타나고, 누르면 `onValueChange` 로 `null` 이 온다.
29
37
  *
@@ -109,4 +117,4 @@ export interface SDatePickerMonthListboxProps {
109
117
  * 자체 트리거/팝오버 없이 연도+월 리스트만 필요한 커스텀 조합에 재사용한다. */
110
118
  export declare function SDatePickerMonthListbox({ value, anchorYear, selectable, onValueChange, onMonthSelect, className, style, }: SDatePickerMonthListboxProps): import("react").JSX.Element;
111
119
  /** SDatePicker — sd-date-picker 포팅. SField + SPopover(SCalendar). */
112
- export declare function SDatePicker({ value, onValueChange, onViewChange, mode, size, placeholder, selectable, disabled, clearable, width, maxWidth, name, rules, status, label, labelPosition, subLabel, labelWidth, icon, iconColor, labelTooltip, labelTooltipProps, addonLabel, addonAlign, hint, error, errorMessage, className, style, }: SDatePickerProps): import("react").JSX.Element;
120
+ export declare function SDatePicker({ value, onValueChange, onViewChange, mode, size, placeholder, selectable, disabled, showWeekday, clearable, width, maxWidth, name, rules, status, label, labelPosition, subLabel, labelWidth, icon, iconColor, labelTooltip, labelTooltipProps, addonLabel, addonAlign, hint, error, errorMessage, className, style, }: SDatePickerProps): import("react").JSX.Element;
@@ -14,6 +14,7 @@
14
14
  | `selectable?` | `[string, string]` | — | |
15
15
  | `maxRange?` | `number` | — | 최대 선택 일수 |
16
16
  | `showTimePicker?` | `boolean` | `false` | 시간(시:분) 선택 푸터 사용 여부. true이면 value는 "YYYY-MM-DD HH:mm" 형식이 되고, 캘린더 하단에 시작·종료 시간 입력이 표시된다. |
17
+ | `showWeekday?` | `boolean` | `false` | 트리거에 보이는 시작·종료 날짜 뒤에 요일을 붙인다 — `2026-10-19 월요일 ~ 2026-10-25 일요일`. `showTimePicker` 와 함께 쓰면 요일이 날짜와 시간 사이에 온다 (`2026-10-19 월요일 09:00`). 표시만 바뀌고 `value`·`onValueChange` 의 형식은 그대로다. 붙는 글자(띄어쓰기 + 요일 3자, 양쪽)만큼 내장 폭 하한·상한이 넓어진다. |
17
18
  | `disabled?` | `boolean` | `false` | |
18
19
  | `clearable?` | `boolean` | `false` | 선택값 지우기 버튼. 값이 있을 때만 나타나고, 누르면 `onValueChange` 로 `null` 이 온다. **필수 입력 필드에는 켜지 않는다** — 지우면 다시 고르기 전까지 폼이 통과하지 못한다. `SDatePicker` · `STimePicker` · `SSelect` 의 `clearable` 과 같은 규칙이다. |
19
20
  | `width?` | `SFieldWidth` | — | 컨트롤 너비 — 폭 등급(`'xs' | 'sm' | 'md' | 'lg' | 'xl'`) · 숫자=px · CSS 길이. 등급은 `--cmp-field-width-*` 토큰으로 풀리므로 토큰이 바뀌면 화면이 따라간다. 값 길이에 상한이 없으면 `"100%"` 로 두어 행 전체를 쓴다. |
@@ -26,6 +26,14 @@ export interface SDateRangePickerProps {
26
26
  * true이면 value는 "YYYY-MM-DD HH:mm" 형식이 되고, 캘린더 하단에 시작·종료 시간 입력이 표시된다.
27
27
  */
28
28
  showTimePicker?: boolean;
29
+ /**
30
+ * 트리거에 보이는 시작·종료 날짜 뒤에 요일을 붙인다 — `2026-10-19 월요일 ~ 2026-10-25 일요일`.
31
+ * `showTimePicker` 와 함께 쓰면 요일이 날짜와 시간 사이에 온다 (`2026-10-19 월요일 09:00`).
32
+ *
33
+ * 표시만 바뀌고 `value`·`onValueChange` 의 형식은 그대로다.
34
+ * 붙는 글자(띄어쓰기 + 요일 3자, 양쪽)만큼 내장 폭 하한·상한이 넓어진다.
35
+ */
36
+ showWeekday?: boolean;
29
37
  disabled?: boolean;
30
38
  /**
31
39
  * 선택값 지우기 버튼. 값이 있을 때만 나타나고, 누르면 `onValueChange` 로 `null` 이 온다.
@@ -98,4 +106,4 @@ export interface SRangeCalendarProps {
98
106
  * 캘린더 그리드만 필요한 커스텀 조합(예: 다른 트리거에 팝오버로 얹는 경우)에 재사용한다. */
99
107
  export declare function RangeCalendar({ value, selectable, maxRange, showTimePicker, onSelect, onPendingStartChange, onViewChange, }: SRangeCalendarProps): import("react").JSX.Element;
100
108
  /** SDateRangePicker — sd-date-range-picker 포팅. SField + SPopover(범위 캘린더). */
101
- export declare function SDateRangePicker({ value, onValueChange, onViewChange, size, placeholder, selectable, maxRange, showTimePicker, disabled, clearable, width, maxWidth, name, rules, status, label, labelPosition, subLabel, labelWidth, icon, iconColor, labelTooltip, labelTooltipProps, addonLabel, addonAlign, hint, error, errorMessage, className, style, }: SDateRangePickerProps): import("react").JSX.Element;
109
+ export declare function SDateRangePicker({ value, onValueChange, onViewChange, size, placeholder, selectable, maxRange, showTimePicker, showWeekday, disabled, clearable, width, maxWidth, name, rules, status, label, labelPosition, subLabel, labelWidth, icon, iconColor, labelTooltip, labelTooltipProps, addonLabel, addonAlign, hint, error, errorMessage, className, style, }: SDateRangePickerProps): import("react").JSX.Element;
@@ -14,6 +14,7 @@
14
14
  | `depth?` | `1 \| 2 \| 3` | `1` | 중첩 단계. Figma 기준 1~3단계. `SExpansionList` 안에서는 주지 않아도 중첩에서 정해진다 — 직접 주면 그 값이 우선한다. |
15
15
  | `leading?` | `ReactNode \| SExpansionItemRenderProp` | — | 타이틀 앞에 표시할 아이콘/콘텐츠 |
16
16
  | `trailing?` | `ReactNode` | — | 타이틀 뒤에 표시할 태그/콘텐츠 |
17
+ | `headerAction?` | `ReactNode` | — | 제목 줄 오른쪽 끝에 놓는 컨트롤 슬롯 — ⋯ 메뉴·아이콘 버튼처럼 **누르는 것**이 오는 자리다. 제목 줄은 통째로 여닫는 `<button>` 이라 `title`·`trailing` 에 컨트롤을 넣으면 버튼 안에 버튼이 중첩되고, 누를 때마다 항목이 함께 여닫힌다. 이 슬롯은 그 버튼 **밖**에 그려지므로 눌러도 여닫히지 않고, Tab 으로 따로 닿는다. 태그·숫자처럼 보기만 하는 것은 `trailing` 에 둔다. 항목이 `disabled` 여도 슬롯은 잠그지 않는다 — 잠글지는 넣은 컨트롤에 앱이 정한다. |
17
18
  | `bodyPadding?` | `boolean` | `true` | body 내부 패딩 사용 여부. `SExpansionList` 안에서 본문이 목록(`SListItem`·`SExpansionItem` 만, 또는 `SExpansionList`)이면 기본으로 꺼진다. |
18
19
  | `bordered?` | `boolean` | `false` | 외곽 테두리 사용 여부 |
19
20
  | `expanded?` | `boolean` | — | |
@@ -23,6 +23,16 @@ export interface SExpansionItemProps extends Omit<HTMLAttributes<HTMLDivElement>
23
23
  leading?: ReactNode | SExpansionItemRenderProp;
24
24
  /** 타이틀 뒤에 표시할 태그/콘텐츠 */
25
25
  trailing?: ReactNode;
26
+ /**
27
+ * 제목 줄 오른쪽 끝에 놓는 컨트롤 슬롯 — ⋯ 메뉴·아이콘 버튼처럼 **누르는 것**이 오는 자리다.
28
+ *
29
+ * 제목 줄은 통째로 여닫는 `<button>` 이라 `title`·`trailing` 에 컨트롤을 넣으면 버튼 안에 버튼이
30
+ * 중첩되고, 누를 때마다 항목이 함께 여닫힌다. 이 슬롯은 그 버튼 **밖**에 그려지므로 눌러도
31
+ * 여닫히지 않고, Tab 으로 따로 닿는다. 태그·숫자처럼 보기만 하는 것은 `trailing` 에 둔다.
32
+ *
33
+ * 항목이 `disabled` 여도 슬롯은 잠그지 않는다 — 잠글지는 넣은 컨트롤에 앱이 정한다.
34
+ */
35
+ headerAction?: ReactNode;
26
36
  /**
27
37
  * body 내부 패딩 사용 여부.
28
38
  * `SExpansionList` 안에서 본문이 목록(`SListItem`·`SExpansionItem` 만, 또는 `SExpansionList`)이면 기본으로 꺼진다.
@@ -16,6 +16,8 @@
16
16
  | `ariaExpanded?` | `boolean` | — | disclosure 버튼일 때 펼침 상태 (aria-expanded) |
17
17
  | `tooltipText?` | `string` | — | 호버 시 표시할 툴팁 텍스트 |
18
18
  | `disabled?` | `boolean` | `false` | 비활성 |
19
+ | `href?` | `string` | — | 이동할 주소. **주면 `<button>` 대신 `<a>` 로 그린다** — 가운데 클릭·⌘클릭·주소 복사가 링크처럼 듣는다. 눌러서 다른 화면·문서로 가는 버튼이면 `onClick` 에서 `location` 을 바꾸지 말고 이것을 준다. 비활성이면 주소를 떼어 눌러도 가지 않는다. |
20
+ | `target?` | `string` | — | `href` 를 여는 곳. `'_blank'` 면 `rel="noopener noreferrer"` 를 함께 붙인다 |
19
21
  | `anchorClassName?` | `string` | — | tooltip anchor wrapper 클래스. tooltipText가 있을 때 위치 조정용으로 사용합니다. |
20
22
  | `anchorStyle?` | `CSSProperties` | — | tooltip anchor wrapper 스타일. tooltipText가 있을 때 위치 조정용으로 사용합니다. |
21
23
  | `className?` | `string` | — | |
@@ -25,7 +27,7 @@
25
27
 
26
28
  | Event | Type | Description |
27
29
  |-------|------|-------------|
28
- | `onClick` | `(e: MouseEvent<HTMLButtonElement>) => void` | 클릭 (sdClick) |
30
+ | `onClick` | `(e: MouseEvent<HTMLButtonElement \| HTMLAnchorElement>) => void` | 클릭 (sdClick). `href` 와 함께 주면 이동 전에 불린다 |
29
31
 
30
32
  ## Types
31
33
 
@@ -2,6 +2,8 @@ import { type CSSProperties, type MouseEvent } from 'react';
2
2
  import { type SIconName } from '../SIcon';
3
3
  export type SGhostButtonSize = 'xxs' | 'xs' | 'sm' | 'md' | 'lg';
4
4
  export type SGhostButtonIntent = 'default' | 'danger' | 'action' | 'subAction' | 'complete' | 'inverse';
5
+ /** `href` 가 없으면 `<button>`, 있으면 `<a>` 로 그린다 */
6
+ type GhostButtonElement = HTMLButtonElement | HTMLAnchorElement;
5
7
  export interface SGhostButtonProps {
6
8
  /** 표시할 아이콘명 (필수) */
7
9
  icon: SIconName;
@@ -19,8 +21,17 @@ export interface SGhostButtonProps {
19
21
  tooltipText?: string;
20
22
  /** 비활성 */
21
23
  disabled?: boolean;
22
- /** 클릭 (sdClick) */
23
- onClick?: (e: MouseEvent<HTMLButtonElement>) => void;
24
+ /**
25
+ * 이동할 주소. **주면 `<button>` 대신 `<a>` 로 그린다** — 가운데 클릭·⌘클릭·주소 복사가 링크처럼 듣는다.
26
+ *
27
+ * 눌러서 다른 화면·문서로 가는 버튼이면 `onClick` 에서 `location` 을 바꾸지 말고 이것을 준다.
28
+ * 비활성이면 주소를 떼어 눌러도 가지 않는다.
29
+ */
30
+ href?: string;
31
+ /** `href` 를 여는 곳. `'_blank'` 면 `rel="noopener noreferrer"` 를 함께 붙인다 */
32
+ target?: string;
33
+ /** 클릭 (sdClick). `href` 와 함께 주면 이동 전에 불린다 */
34
+ onClick?: (e: MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => void;
24
35
  /** tooltip anchor wrapper 클래스. tooltipText가 있을 때 위치 조정용으로 사용합니다. */
25
36
  anchorClassName?: string;
26
37
  /** tooltip anchor wrapper 스타일. tooltipText가 있을 때 위치 조정용으로 사용합니다. */
@@ -32,7 +43,11 @@ export interface SGhostButtonProps {
32
43
  * SGhostButton — sd-ghost-button 포팅. 아이콘 전용 투명 버튼.
33
44
  * hover 시 intent 색 오버레이(5%), focus-visible 링, 옵션 툴팁(회색 STag).
34
45
  *
46
+ * `href` 를 주면 같은 모양의 링크(`<a>`)로 그린다.
47
+ *
35
48
  * ref 는 버튼 요소로 그대로 간다 — 이 버튼에 붙여 띄우는 층(SGnb 런처의 서비스 목록 등)이
36
49
  * 버튼을 앵커로 삼아야 한다. 툴팁을 두른 형태에서도 앵커는 래퍼가 아니라 버튼이다.
50
+ * 링크로 그리면 ref 는 `<a>` 로 간다.
37
51
  */
38
- export declare const SGhostButton: import("react").ForwardRefExoticComponent<SGhostButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
52
+ export declare const SGhostButton: import("react").ForwardRefExoticComponent<SGhostButtonProps & import("react").RefAttributes<GhostButtonElement>>;
53
+ export {};
@@ -52,7 +52,7 @@ export interface SGnbSystemAlert {
52
52
  /** 라벨 텍스트. 기본 `'알림'` */
53
53
  label?: string;
54
54
  /**
55
- * 읽지 않은 알림 개수. 1 이상이면 벨이 울리는 아이콘·강조색으로 바뀌고 개수가 붙는다.
55
+ * 읽지 않은 알림 개수. 1 이상이면 벨이 울리는 아이콘·강조색으로 바뀌고 개수 배지가 붙는다.
56
56
  * 개수를 적을 가로가 모자란 자리(접힌 레일 · 기본 폭보다 좁게 줄인 판)에서는 점 배지로 바뀐다.
57
57
  * 표기는 컴포넌트가 맡는다 — 세 자리마다 콤마를 넣어 그리므로 앱이 포맷한 문자열을 넘기지 않는다.
58
58
  */
@@ -154,7 +154,9 @@ export interface SGnbSystemAccount {
154
154
  * 타이포그래피는 CSS 변수가 아니라 `.typo-*` 프리셋으로 나온다.
155
155
  * domainTag.typography → typo-body-sm-medium (12/500/20)
156
156
  * item.label.typography → typo-body-sm-semibold (12/600/20)
157
- * alertButton.count → typo-body-md-bold (13/700/22)
157
+ *
158
+ * 알림 개수는 글자가 아니라 배지(`STag` xs · deep)로 그린다 — 배경·글자색·치수는 태그 토큰
159
+ * (`--cmp-tag-*`)이 쥔다.
158
160
  */
159
161
 
160
162
  /** 색상(표면). light = 흰 면, dark = 오션블루 면. GNB 의 color 를 그대로 따라간다 */
@@ -170,6 +172,7 @@ export const GNB_SYSTEM_COLORS = ['light', 'dark'] as const;
170
172
  - [SIcon](../SIcon)
171
173
  - [SPortal](../SPortal)
172
174
  - [SSystemActionButton](../SSystemActionButton)
175
+ - [STag](../STag)
173
176
  - [STooltip](../STooltip)
174
177
 
175
178
  ### Graph
@@ -181,6 +184,7 @@ graph TD;
181
184
  SGnbSystem --> SIcon
182
185
  SGnbSystem --> SPortal
183
186
  SGnbSystem --> SSystemActionButton
187
+ SGnbSystem --> STag
184
188
  SGnbSystem --> STooltip
185
189
  style SGnbSystem fill:#f9f,stroke:#333,stroke-width:4px
186
190
  ```
@@ -23,7 +23,7 @@ export interface SGnbSystemAlert {
23
23
  /** 라벨 텍스트. 기본 `'알림'` */
24
24
  label?: string;
25
25
  /**
26
- * 읽지 않은 알림 개수. 1 이상이면 벨이 울리는 아이콘·강조색으로 바뀌고 개수가 붙는다.
26
+ * 읽지 않은 알림 개수. 1 이상이면 벨이 울리는 아이콘·강조색으로 바뀌고 개수 배지가 붙는다.
27
27
  * 개수를 적을 가로가 모자란 자리(접힌 레일 · 기본 폭보다 좁게 줄인 판)에서는 점 배지로 바뀐다.
28
28
  * 표기는 컴포넌트가 맡는다 — 세 자리마다 콤마를 넣어 그리므로 앱이 포맷한 문자열을 넘기지 않는다.
29
29
  */
@@ -156,8 +156,8 @@ export interface SGnbSystemProps extends Omit<HTMLAttributes<HTMLDivElement>, 'c
156
156
  * 접힌 패널은 렌더되지 않으므로, 접힌 상태에서도 닿아야 하는 것은 SGnb 의 `foldedTop`·`foldedFooter` 에 둔다.
157
157
  * 상단바는 접어도 남으므로 `top` 에는 접힘 형태가 없다.
158
158
  *
159
- * `alert.count` 가 1 이상이면 벨이 울리는 아이콘(`bellRinging`)과 강조색으로 바뀐다 — light 는 파랑,
160
- * 어두운 면은 노랑(토큰이 정한다). 0 이거나 없으면 조용한 벨(`bell`)에 기본 색이다.
159
+ * `alert.count` 가 1 이상이면 벨이 울리는 아이콘(`bellRinging`)과 강조색으로 바뀌고 개수 배지가 붙는다 —
160
+ * light 는 파랑 배지에 흰 글자, 어두운 면은 노랑 배지에 검은 글자(토큰이 정한다). 0 이거나 없으면 조용한 벨(`bell`)에 기본 색이다.
161
161
  *
162
162
  * 안내 툴팁(`alert.tooltip`)은 문구를 받은 동안 떠 있다가 알림을 누르거나 커서를 빼면 닫힌다.
163
163
  * 다시 띄우는 신호는 문구가 **없음 → 있음** 으로 바뀌는 것이라, 같은 문구도 비웠다 다시 넘기면
@@ -13,7 +13,9 @@ import type { SSystemActionButtonColor } from '../SSystemActionButton';
13
13
  * 타이포그래피는 CSS 변수가 아니라 `.typo-*` 프리셋으로 나온다.
14
14
  * domainTag.typography → typo-body-sm-medium (12/500/20)
15
15
  * item.label.typography → typo-body-sm-semibold (12/600/20)
16
- * alertButton.count → typo-body-md-bold (13/700/22)
16
+ *
17
+ * 알림 개수는 글자가 아니라 배지(`STag` xs · deep)로 그린다 — 배경·글자색·치수는 태그 토큰
18
+ * (`--cmp-tag-*`)이 쥔다.
17
19
  */
18
20
  /** 색상(표면). light = 흰 면, dark = 오션블루 면. GNB 의 color 를 그대로 따라간다 */
19
21
  export declare const GNB_SYSTEM_COLORS: readonly ["light", "dark"];
@@ -48,7 +50,7 @@ export interface GnbSystemColorTokens {
48
50
  domainText: string;
49
51
  /** 알림이 있을 때의 벨 글리프색 */
50
52
  alertIcon: string;
51
- /** 알림 개수(펼침) · 점 배지(접힘) 색 */
53
+ /** 점 배지(접힘·좁은 판) 색 */
52
54
  alertCount: string;
53
55
  /** 알림이 있을 때의 hover 배경. 없으면 행 기본 hover 를 쓴다 */
54
56
  alertHoverBg?: string;
@@ -73,7 +75,7 @@ export interface GnbSystemTopColorTokens {
73
75
  domainText: string;
74
76
  /** 알림이 있을 때의 벨 글리프색 */
75
77
  alertIcon: string;
76
- /** 알림 개수 색 */
78
+ /** 점 배지 색 (상단바는 점 배지를 쓰지 않지만 색표를 패널과 맞춰 둔다) */
77
79
  alertCount: string;
78
80
  /** 알림이 있을 때의 hover 배경. 없으면 행 기본 hover 를 쓴다 */
79
81
  alertHoverBg?: string;
@@ -15,7 +15,7 @@
15
15
  | `disabled?` | `boolean` | `false` | |
16
16
  | `clearable?` | `boolean` | `false` | |
17
17
  | `showMeridiem?` | `boolean` | — | 오전/오후 선택 표시 여부. 지정하지 않으면 type="midday"일 때만 켜집니다. |
18
- | `minuteStep?` | `number` | `1` | |
18
+ | `minuteStep?` | `number` | `1` | 분 리스트에 나열할 간격(분). 10 이면 `00 10 20 30 40 50` 만 고를 수 있다 |
19
19
  | `width?` | `SFieldWidth` | — | 컨트롤 너비 — 폭 등급(`'xs' | 'sm' | 'md' | 'lg' | 'xl'`) · 숫자=px · CSS 길이. 등급은 `--cmp-field-width-*` 토큰으로 풀리므로 토큰이 바뀌면 화면이 따라간다. 값 길이에 상한이 없으면 `"100%"` 로 두어 행 전체를 쓴다. |
20
20
  | `maxWidth?` | `SFieldWidth` | — | 컨트롤 최대 너비 — 폭 등급 · 숫자=px · CSS 길이. 지정하지 않으면 값 길이(`HH:mm`)에 맞춘 내장 상한(`size='md'` → `lg`, `'sm'` → `md`)이 걸려, `width="100%"` 를 받아도 행 전체로 늘어나지 않는다. 팝오버처럼 폭이 이미 좁게 정해진 자리에서 그 폭을 그대로 채워야 하면 `width="100%"` 와 함께 `maxWidth="100%"` 를 준다. `0` 은 상한을 아예 걸지 않는다 — 부모보다 넓어지는 것까지 허용해야 할 때만 쓴다. |
21
21
  | `name?` | `string` | — | |
@@ -7,6 +7,7 @@ import { type STooltipProps } from '../STooltip';
7
7
  import { type STimePickerSize, type STimePickerType } from './timepicker.config';
8
8
  import { type SFieldWidth } from '../../lib/field-width';
9
9
  export type { STimePickerSize, STimePickerType } from './timepicker.config';
10
+ export { TimeListbox } from './TimeListbox';
10
11
  export interface STimePickerProps {
11
12
  /** 선택 시간 (HH:mm) */
12
13
  value?: string | null;
@@ -22,6 +23,7 @@ export interface STimePickerProps {
22
23
  clearable?: boolean;
23
24
  /** 오전/오후 선택 표시 여부. 지정하지 않으면 type="midday"일 때만 켜집니다. */
24
25
  showMeridiem?: boolean;
26
+ /** 분 리스트에 나열할 간격(분). 10 이면 `00 10 20 30 40 50` 만 고를 수 있다 */
25
27
  minuteStep?: number;
26
28
  /**
27
29
  * 컨트롤 너비 — 폭 등급(`'xs' | 'sm' | 'md' | 'lg' | 'xl'`) · 숫자=px · CSS 길이.
@@ -70,11 +72,5 @@ export interface STimePickerProps {
70
72
  className?: string;
71
73
  style?: CSSProperties;
72
74
  }
73
- export declare function TimeSelector({ value, showMeridiem, minuteStep, onChange, }: {
74
- value?: string | null;
75
- showMeridiem: boolean;
76
- minuteStep: number;
77
- onChange: (time: string) => void;
78
- }): import("react").JSX.Element;
79
- /** STimePicker — sd-time-picker 포팅. SField + Popover(TimeSelector). */
75
+ /** STimePicker — sd-time-picker 포팅. SField + Popover(TimeListbox). */
80
76
  export declare function STimePicker({ value, onValueChange, onOpenChange, type, size, placeholder, disabled, clearable, showMeridiem, minuteStep, width, maxWidth, name, rules, status, label, labelPosition, subLabel, labelWidth, icon, iconColor, labelTooltip, labelTooltipProps, addonLabel, addonAlign, hint, error, errorMessage, className, style, }: STimePickerProps): import("react").JSX.Element;
@@ -0,0 +1,21 @@
1
+ /** 리스트에서 고른 열 — STimePicker 는 분을 고르면 팝오버를 닫는다 */
2
+ export type TimeListboxPart = 'meridiem' | 'hour' | 'minute';
3
+ interface TimeListboxProps {
4
+ /** 선택 시간 (HH:mm). 비어 있으면 아무것도 선택되지 않은 채 열린다 */
5
+ value?: string | null;
6
+ showMeridiem: boolean;
7
+ /** 분 열에 나열할 간격 (분) */
8
+ minuteStep: number;
9
+ /** 고를 수 없는 시각 — 자정부터 센 분(0~1439)을 받는다. 시·오전/오후 항목은 그 안의 모든
10
+ * 분을 고를 수 없을 때만 비활성이 된다 */
11
+ isDisabled?: (minutes: number) => boolean;
12
+ onChange: (time: string, part: TimeListboxPart) => void;
13
+ /** 열 접근성 레이블 앞에 붙는 말 — STimeRangePicker 의 "시작"·"종료" */
14
+ labelPrefix?: string;
15
+ /** 그림자·모서리를 두른 떠 있는 상자로 그린다. 다른 상자 안에 넣을 때는 끈다 */
16
+ elevated?: boolean;
17
+ }
18
+ /** TimeListbox — `[오전/오후] | 시 | 분` 을 세로 리스트에서 고르는 시간 선택 조각.
19
+ * SDatePickerMonthListbox(연 | 월)와 같은 스펙이며, STimePicker·STimeRangePicker 팝오버가 쓴다. */
20
+ export declare function TimeListbox({ value, showMeridiem, minuteStep, isDisabled, onChange, labelPrefix, elevated, }: TimeListboxProps): import("react").JSX.Element;
21
+ export {};
@@ -15,8 +15,8 @@
15
15
  | `disabled?` | `boolean` | `false` | |
16
16
  | `clearable?` | `boolean` | `false` | |
17
17
  | `showMeridiem?` | `boolean` | — | 오전/오후 선택 표시 여부. 지정하지 않으면 type="midday"일 때만 켜집니다. |
18
- | `minuteStep?` | `number` | `1` | |
19
- | `rangeOrder?` | `STimeRangePickerRangeOrder` | `'strict'` | 범위 순서 정책. strict는 시작 시간이 종료 시간보다 늦어지지 않도록 보정합니다. |
18
+ | `minuteStep?` | `number` | `1` | 분 리스트에 나열할 간격(분). 10 이면 `00 10 20 30 40 50` 만 고를 수 있다 |
19
+ | `rangeOrder?` | `STimeRangePickerRangeOrder` | `'strict'` | 범위 순서 정책. `strict` 는 종료 리스트에서 시작보다 이른 시각을 비활성으로 막고, 시작을 종료 뒤로 옮기면 종료가 따라온다. `allow-cross-day` 는 제약 없이 고른다(자정을 넘는 범위). |
20
20
  | `width?` | `SFieldWidth` | — | 컨트롤 너비 — 폭 등급(`'xs' | 'sm' | 'md' | 'lg' | 'xl'`) · 숫자=px · CSS 길이. 등급은 `--cmp-field-width-*` 토큰으로 풀리므로 토큰이 바뀌면 화면이 따라간다. 값 길이에 상한이 없으면 `"100%"` 로 두어 행 전체를 쓴다. |
21
21
  | `maxWidth?` | `SFieldWidth` | — | 컨트롤 최대 너비 — 폭 등급 · 숫자=px · CSS 길이. 지정하지 않으면 값 길이(`HH:mm ~ HH:mm`)에 맞춘 내장 상한이 걸려, `width="100%"` 를 받아도 행 전체로 늘어나지 않는다. 팝오버처럼 폭이 이미 좁게 정해진 자리에서 그 폭을 그대로 채워야 하면 `width="100%"` 와 함께 `maxWidth="100%"` 를 준다. `0` 은 상한을 아예 걸지 않는다 — 부모보다 넓어지는 것까지 허용해야 할 때만 쓴다. |
22
22
  | `name?` | `string` | — | |
@@ -22,8 +22,12 @@ export interface STimeRangePickerProps {
22
22
  clearable?: boolean;
23
23
  /** 오전/오후 선택 표시 여부. 지정하지 않으면 type="midday"일 때만 켜집니다. */
24
24
  showMeridiem?: boolean;
25
+ /** 분 리스트에 나열할 간격(분). 10 이면 `00 10 20 30 40 50` 만 고를 수 있다 */
25
26
  minuteStep?: number;
26
- /** 범위 순서 정책. strict는 시작 시간이 종료 시간보다 늦어지지 않도록 보정합니다. */
27
+ /**
28
+ * 범위 순서 정책. `strict` 는 종료 리스트에서 시작보다 이른 시각을 비활성으로 막고,
29
+ * 시작을 종료 뒤로 옮기면 종료가 따라온다. `allow-cross-day` 는 제약 없이 고른다(자정을 넘는 범위).
30
+ */
27
31
  rangeOrder?: STimeRangePickerRangeOrder;
28
32
  /**
29
33
  * 컨트롤 너비 — 폭 등급(`'xs' | 'sm' | 'md' | 'lg' | 'xl'`) · 숫자=px · CSS 길이.