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 +40 -13
- package/dist/components/SChatInput/README.md +8 -2
- package/dist/components/SChatInput/SChatInput.d.ts +54 -4
- package/dist/components/SDatePicker/README.md +1 -0
- package/dist/components/SDatePicker/SDatePicker.d.ts +9 -1
- package/dist/components/SDateRangePicker/README.md +1 -0
- package/dist/components/SDateRangePicker/SDateRangePicker.d.ts +9 -1
- package/dist/components/SExpansionItem/README.md +1 -0
- package/dist/components/SExpansionItem/SExpansionItem.d.ts +10 -0
- package/dist/components/SGhostButton/README.md +3 -1
- package/dist/components/SGhostButton/SGhostButton.d.ts +18 -3
- package/dist/components/SGnbSystem/README.md +6 -2
- package/dist/components/SGnbSystem/SGnbSystem.d.ts +3 -3
- package/dist/components/SGnbSystem/gnbSystem.config.d.ts +5 -3
- package/dist/components/STimePicker/README.md +1 -1
- package/dist/components/STimePicker/STimePicker.d.ts +3 -7
- package/dist/components/STimePicker/TimeListbox.d.ts +21 -0
- package/dist/components/STimeRangePicker/README.md +2 -2
- package/dist/components/STimeRangePicker/STimeRangePicker.d.ts +5 -1
- package/dist/index.cjs +644 -626
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +644 -626
- package/dist/index.js.map +1 -1
- package/dist/lib/date.d.ts +9 -0
- package/dist/lib/mode-listbox.d.ts +10 -0
- package/dist/llms-full.txt +62 -21
- package/dist/llms.txt +40 -13
- package/dist/styles.css +6 -35
- package/dist/theme.css +0 -6
- package/package.json +1 -1
package/dist/lib/date.d.ts
CHANGED
|
@@ -14,3 +14,12 @@ export interface DayCell {
|
|
|
14
14
|
/** year/month(1-12)에 대한 6주 그리드 셀 목록 (앞뒤 달 채움 포함) */
|
|
15
15
|
export declare function monthCells(year: number, month: number): DayCell[];
|
|
16
16
|
export declare const WEEK_LABELS: readonly ["일", "월", "화", "수", "목", "금", "토"];
|
|
17
|
+
/** 요일 꼬리(` 월요일`)가 글자 크기 `fontSize`(px)에서 차지하는 폭(px) — 피커가 폭을 이만큼 넓힌다 */
|
|
18
|
+
export declare const weekdaySuffixWidth: (fontSize: number) => number;
|
|
19
|
+
/**
|
|
20
|
+
* "YYYY-MM-DD" 또는 "YYYY-MM-DD HH:mm" 의 날짜 뒤에 요일을 붙인 표시 문자열.
|
|
21
|
+
* "2026-10-19" → "2026-10-19 월요일"
|
|
22
|
+
* "2026-10-19 09:00" → "2026-10-19 월요일 09:00"
|
|
23
|
+
* 날짜로 읽히지 않으면 그대로 돌려준다.
|
|
24
|
+
*/
|
|
25
|
+
export declare function withWeekday(value: string): string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 연·월·시간을 세로 리스트에서 고르는 피커 조각(SDatePicker 의 연·월 리스트, STimePicker 의 시간
|
|
3
|
+
* 리스트)이 함께 쓰는 치수와 항목 클래스. 스펙(`--cmp-datepicker-mode-*`)이 하나라 한곳에 둔다.
|
|
4
|
+
*/
|
|
5
|
+
/** 리스트 높이(px) — 항목 높이와 함께 "선택 항목을 가운데로" 계산에 쓴다 */
|
|
6
|
+
export declare const MODE_LIST_HEIGHT = 240;
|
|
7
|
+
/** 항목 한 줄 높이(px) — paddingY(4) × 2 + line-height(20) */
|
|
8
|
+
export declare const MODE_ITEM_HEIGHT = 28;
|
|
9
|
+
/** 리스트 항목(button) 클래스 — 기본 / 선택 / 비활성 */
|
|
10
|
+
export declare const modeListItemClass: (selected: boolean, disabled: boolean) => string;
|
package/dist/llms-full.txt
CHANGED
|
@@ -1443,6 +1443,7 @@ tableRef.current.scrollToRow(row); // 복원
|
|
|
1443
1443
|
- **`ariaLabel` 은 필수다.** 라벨이 없어 스크린리더가 읽을 것이 아이콘밖에 없다. 뜻이 덜 명확한 아이콘은 **`tooltipText`** 로 이름을 띄운다.
|
|
1444
1444
|
- 여러 개를 나란히 둘 때 간격은 **`gap-sd-4` 고정** — 가로·세로가 같다 (§2-2).
|
|
1445
1445
|
- **라벨이 필요하거나 화면의 주 액션이면 `SGhostButton` 이 아니라 `SButton` 이다.** 위계가 낮다는 이유만으로 `SGhostButton` 을 고르지 않는다 — 라벨이 붙는 저강조 액션은 `SButton` 의 `outline` 조합(§3-5-1)이다.
|
|
1446
|
+
- **다른 화면·문서로 가는 아이콘 버튼은 `href` 를 준다.** 링크(`<a>`)로 그려져 가운데 클릭·⌘클릭·주소 복사가 듣는다. `onClick` 에서 `location`·`window.open` 으로 옮기거나 `<a>` 로 감싸지 않는다. 새 탭이면 `target="_blank"` 만 준다 — `rel` 은 컴포넌트가 붙인다.
|
|
1446
1447
|
|
|
1447
1448
|
```tsx
|
|
1448
1449
|
// 테이블 행 끝 인라인 액션 — 수정은 action, 행 삭제는 default, 간격은 gap-sd-4
|
|
@@ -1460,6 +1461,8 @@ tableRef.current.scrollToRow(row); // 복원
|
|
|
1460
1461
|
✅ <SGhostButton icon="close" ariaLabel="닫기" onClick={onClose} /> // 모달 닫기 = default
|
|
1461
1462
|
✅ <SGhostButton icon="copy" ariaLabel="상품코드 복사" tooltipText="복사" /> // 뜻이 덜 명확 → 툴팁
|
|
1462
1463
|
❌ <SGhostButton icon="download" ariaLabel="엑셀 다운로드" /> // 목록의 주 액션 → SButton
|
|
1464
|
+
✅ <SGhostButton icon="helpOutline" ariaLabel="도움말" href={HELP_URL} target="_blank" />
|
|
1465
|
+
❌ <SGhostButton icon="helpOutline" ariaLabel="도움말" onClick={() => window.open(HELP_URL)} /> // 가는 버튼 → href
|
|
1463
1466
|
```
|
|
1464
1467
|
|
|
1465
1468
|
##### `SMeatballButton` — ⋯ 뒤에 메뉴를 접어 둘 때
|
|
@@ -1885,12 +1888,21 @@ useEffect(() => {
|
|
|
1885
1888
|
| `STimePicker` | md | lg |
|
|
1886
1889
|
| `STimeRangePicker` | md | lg (오전/오후 표시는 두 사이즈 모두 lg) |
|
|
1887
1890
|
|
|
1891
|
+
`SDatePicker`·`SDateRangePicker` 에 `showWeekday` 를 켜면 날짜 뒤에 요일이 붙어(`2026-10-19 월요일`) 붙는 글자(띄어쓰기 + 요일 3자) 폭만큼 위 표의 하한·상한이 넓어진다 — 등급이 바뀌는 것이 아니다. 늘어난 폭은 컴포넌트가 스스로 잡으므로 요일 때문에 `width` 를 따로 주지 않는다. 표시만 바뀌고 값 형식은 그대로라, **요일을 붙이려고 값을 가공하거나 `hint` 에 요일을 따로 적지 않는다.**
|
|
1892
|
+
|
|
1888
1893
|
`SDateRangePicker` 가 한 등급씩 위인 것은 값이 `YYYY-MM-DD ~ YYYY-MM-DD` 로 두 배가 넘기 때문이다. 같은 이유로 `STimeRangePicker` 의 오전/오후 모드도 sm 에서 한 등급 위를 쓴다 — 그 모드의 최소 폭이 md 등급을 이미 넘어, 그대로 두면 하한이 상한을 넘어 상한이 무력해진다.
|
|
1889
1894
|
|
|
1890
1895
|
네 컴포넌트 모두 이 상한을 `maxWidth` 로 덮을 수 있다 — 등급을 주면 그 등급이 상한이 되고, `width="100%" maxWidth="100%"` 면 부모 폭을 그대로 채운다. **폭이 이미 좁게 정해진 자리(팝오버·좁은 카드)에서 쓴다.**
|
|
1891
1896
|
|
|
1892
1897
|
**`SKeyValueTable` 안에서는 필드마다 `maxWidth` 를 주지 않는다.** 표 안에서 이 상한이 거슬리는 이유는 대개 "같은 행의 다른 필드와 오른쪽 끝이 어긋난다" 이고, 그건 표 전체의 문제다. 표의 `fieldWidth="fill"` 로 한 번에 정한다 (§4-3). 그 표에서 한 필드만 예외로 둘 때만 `options.maxWidth` 를 쓴다.
|
|
1893
1898
|
|
|
1899
|
+
##### 시간 피커 — 분 간격은 `minuteStep`, 범위 순서는 `rangeOrder`
|
|
1900
|
+
|
|
1901
|
+
`STimePicker`·`STimeRangePicker` 는 `[오전/오후] | 시 | 분` 리스트에서 고른다 (직접 타이핑하지 않는다).
|
|
1902
|
+
|
|
1903
|
+
- **분 간격은 `minuteStep` 으로 정한다.** 분 리스트에 그 간격의 분만 나열되므로, 고른 값을 앱에서 다시 반올림하거나 `rules` 로 간격을 검사하지 않는다.
|
|
1904
|
+
- **`STimeRangePicker` 의 "종료가 시작보다 이르면 안 된다" 는 앱이 검증하지 않는다.** 기본 `rangeOrder="strict"` 가 종료 리스트에서 그 시각을 비활성으로 막는다. 자정을 넘는 범위(야간 근무 22:00~02:00 등)를 받아야 할 때만 `rangeOrder="allow-cross-day"` 를 준다.
|
|
1905
|
+
|
|
1894
1906
|
##### 값을 지울 수 있게 하려면 `clearable`
|
|
1895
1907
|
|
|
1896
1908
|
`SSelect` · `SDatePicker` · `SDateRangePicker` · `STimePicker` · `STimeRangePicker` 가 같은 규칙으로 갖는다. 값이 있을 때만 지우기 버튼이 나타나고, 누르면 **`onValueChange` 로 `null` 이 온다** (빈 문자열이 아니다). 받는 쪽 상태도 `null` 을 담을 수 있어야 한다.
|
|
@@ -1983,7 +1995,7 @@ const [from, setFrom] = useState<string | null>(null);
|
|
|
1983
1995
|
- **펼침 버튼·들여쓰기는 컴포넌트가 그린다.** 기본은 보이는 첫 컬럼이고 `treeColumn` 으로 옮긴다. 그 컬럼은 왼쪽 정렬로 두고, 셀 내용은 `render`·`format` 으로 바꾼다 — `renderCell` 은 펼침 버튼을 넣을 자리가 없어 에러가 난다.
|
|
1984
1996
|
- **셀 병합은 가로(`colSpan`)만 한다.** 트리 컬럼이 아닌 컬럼은 `renderCell` 로 `<td>` 를 바꿀 수 있고, 한 행 안에서 끝나는 `colSpan` 은 접고 펴도 그대로다. **`rowSpan` 으로 세로 병합하지 않는다** — 행이 접히면 사라진 행 몫까지 병합이 남아 아래 행의 칸을 덮고, 덮인 행의 칸은 옆 컬럼으로 밀려난다. 부모와 하위 행에 같은 값이 반복되면 부모 행에만 쓰고 하위 행은 비워 둔다.
|
|
1985
1997
|
- **펼침 영역은 트리 컬럼 셀 전체다.** 하위 행이 있는 행은 펼침 버튼뿐 아니라 그 셀 어디를 눌러도 접고 편다. 다른 셀을 누르면 행 클릭(`onRowClick`)이다 — 트리 컬럼 셀을 누른 것은 행 클릭으로 가지 않는다.
|
|
1986
|
-
-
|
|
1998
|
+
- **펼침·접힘 모션은 컴포넌트가 넣는다.** 하위 행이 부모 아래에서 서랍처럼 펼쳐지고 접히며, 아래 행들도 함께 내려가고 올라온다. 모션을 줄인 환경에서는 꺼진다. 행(`rowClassName`)에 애니메이션·`transform`·`clip-path` 를 직접 주지 않는다 — 컴포넌트가 행의 위치를 움직이는 것과 겹친다. `onExpandedChange` 는 모션을 기다리지 않고 누른 즉시 온다.
|
|
1987
1999
|
- **트리 컬럼에 링크·버튼을 넣어도 된다.** 셀 안의 링크·버튼·입력을 누르면 그 동작만 일어나고 펼침은 일어나지 않는다 — `stopPropagation` 을 직접 넣지 않는다. 상품명을 상세 링크로 거는 것처럼 쓴다.
|
|
1988
2000
|
- **선택은 계층을 따른다.** 부모를 고르면 접힌 하위 행까지 함께 골라지고, 일부만 골라진 부모는 "일부 선택"으로 그려진다. 하위 행이 모두 골라지면 부모도 `selected` 에 들어간다 — 처리 대상이 잎 행뿐이면 받은 목록에서 하위 행이 없는 행만 걸러 쓴다.
|
|
1989
2001
|
- **가로로 스크롤되는 표는 트리 컬럼을 왼쪽에 고정한다**(`stickyColumn={{ left: 1 }}`, 트리 컬럼이 첫 컬럼일 때). 스크롤로 들여쓰기가 밀려나면 어느 행이 누구의 하위인지 읽히지 않는다. 체크박스 열은 `selectable` 이면 늘 고정되므로 개수에 넣지 않는다.
|
|
@@ -2032,6 +2044,13 @@ const [selectedId, setSelectedId] = useState<string>();
|
|
|
2032
2044
|
- **리스트 밖 단독 `SExpansionItem` 은 본문을 다루지 않는다.** 선택하는 목록을 담으려면 `SExpansionList` 로 감싼다.
|
|
2033
2045
|
|
|
2034
2046
|
- **하위를 가지지 않는 항목은 `SExpansionItem` 이 아니라 `SListItem`** 으로 둔다. 펼칠 것이 없는데 펼침 항목으로 만들면 화살표만 남는다.
|
|
2047
|
+
- **제목 줄에 누르는 컨트롤(⋯ 메뉴·아이콘 버튼)은 `headerAction` 에 둔다.** 제목 줄은 통째로 여닫는 버튼이라 `title`·`trailing` 에 컨트롤을 넣으면 버튼이 중첩되고 누를 때마다 함께 여닫힌다. `headerAction` 은 그 버튼 밖에 그려져 눌러도 여닫히지 않는다 — `stopPropagation` 이나 제어 모드(`expanded`)로 막지 않는다. 태그·건수처럼 보기만 하는 것은 그대로 `trailing` 이다.
|
|
2048
|
+
|
|
2049
|
+
```tsx
|
|
2050
|
+
✅ <SExpansionItem title="상품 문의" trailing={<STag label="3" color="grey" />}
|
|
2051
|
+
headerAction={<SMeatballButton size="xs" items={folderMenu} onItemClick={v => onFolderAction(id, v)} />}>…</SExpansionItem>
|
|
2052
|
+
❌ <SExpansionItem title="상품 문의" trailing={<SMeatballButton … />}>…</SExpansionItem> {/* 버튼 안 버튼 — 누르면 함께 여닫힌다 */}
|
|
2053
|
+
```
|
|
2035
2054
|
|
|
2036
2055
|
#### 3-7-8. SCard vs SSectionHeaderCard
|
|
2037
2056
|
|
|
@@ -4518,7 +4537,8 @@ export default function StoreSetupPage() {
|
|
|
4518
4537
|
- **날짜가 바뀌는 자리에는 날짜 구분선을 끼운다** — 가로 `SDivider` 두 개 사이에 날짜(`typo-body-sm-default` + `text-fg-tertiary`). 일어난 사실이 아니라 흐름의 경계라 시스템 안내 알약(`SChatSystemMessage`)을 쓰지 않는다. **날짜는 `2026년 10월 6일 화요일`** 로 쓴다 — 말풍선 시각과 같은 대화 예외다(§1-4). `toLocaleDateString()` 은 로케일에 따라 결과가 바뀌므로 쓰지 않고 연·월·일·요일을 직접 이어 붙인다. 가로 구분선은 기본이 전체 폭이라 한 줄에 둘을 놓을 때는 `className="w-auto flex-1"` 로 남은 폭을 나눠 갖게 한다. 앞뒤 간격은 시스템 안내와 같은 토큰이다. 날짜가 다르면 같은 시각이어도 한 묶음으로 합치지 않는다.
|
|
4519
4538
|
- 대화의 시각은 `오후 5:52` 로 쓴다 (§1-4 대화 예외). 읽음 표시는 보낸 메시지에만, 템플릿으로 보낸 글은 `variant="template"` 로 면을 나눈다.
|
|
4520
4539
|
- **입력 중인 글은 페이지가 상담마다 든다.** 상담을 옮겨도 쓰던 글이 남고, 템플릿 패널이 그 글에 넣을 수 있다. 템플릿은 누르면 **입력창에 넣는다** — 바로 보내지 않고 상담사가 고쳐 보낸다. 보내기 전 첨부는 입력창 위에 `SChatAttachedFile` 로 쌓는다.
|
|
4521
|
-
- **첨부를 받는 대화면 `
|
|
4540
|
+
- **첨부를 받는 대화면 `onAttachFiles` 와 `onPasteFiles` 에 같은 함수를 넘긴다** — 첨부 버튼으로 고른 파일과 붙여넣은 파일·캡처 이미지를 같은 자리에 쌓는다. 파일 선택 창은 입력창이 연다 — `<input type="file">` 을 따로 두지 않는다. `onAttach` 는 서버 파일함 모달처럼 앱이 직접 고르는 흐름에만 쓴다. 붙여넣기에서 파일을 골라내고 글로 들어가지 않게 막는 것은 입력창이 한다 — `onPaste` 로 직접 처리하지 않는다. 첨부가 쌓여 있으면 `hasAttachments` 를 켠다 — 글 없이 파일만 보낼 수 있고, 이때 `onSend` 에는 빈 문자열이 온다.
|
|
4541
|
+
- **입력창의 일부만 막을 때는 그 동작만 잠근다.** 첨부가 올라가는 중이면 `sendDisabled` 로 전송만 잠근다 — 글을 적고 파일을 더 붙이는 것은 된다. 첨부 수가 상한에 닿았으면 `attachDisabled` 로 첨부를 잠그고 이유는 `attachTooltip` 으로 알린다 — 파일 붙여넣기도 함께 막힌다. 상한 아래에서 여러 파일이 한 번에 들어와 넘치는 것은 앱이 `onPasteFiles` 에서 잘라 낸다. `disabled` 는 입력까지 잠그므로 종료된 대화에만, `busy` 는 정지 버튼으로 바뀌므로 응답을 기다릴 때만 쓴다.
|
|
4522
4542
|
- **상담 종료는 `SChatInput` 의 `footerAction` 이다** — 전송과 층이 다른 동작이라 입력창 아래 보조 슬롯(`STextLink`)에 두고, 실행 전에 `SModal.confirm` 으로 묻는다. 종료된 상담은 입력창을 `disabled` 로 잠그고 종료 링크를 뺀다.
|
|
4523
4543
|
- 목록의 상태는 `STag` 다 — 색 점(`SBadge`)만으로 찍지 않는다 (§3-1). 템플릿 분류는 `SExpansionList` 루트 하나에 분류(`SExpansionItem`) 안 템플릿(`SListItem`)을 바로 넣는다 (§3-7-7).
|
|
4524
4544
|
|
|
@@ -4742,7 +4762,14 @@ function Conversation({
|
|
|
4742
4762
|
}) {
|
|
4743
4763
|
const initial = useConsultMessages(consult.id);
|
|
4744
4764
|
const [entries, setEntries] = useState<ChatEntry[]>(initial);
|
|
4745
|
-
|
|
4765
|
+
// 보내기 전 첨부. 같은 파일을 두 번 붙일 수 있으므로 이름이 아니라 id 로 가른다
|
|
4766
|
+
const [attached, setAttached] = useState<{ id: number; file: File }[]>([]);
|
|
4767
|
+
const nextAttachId = useRef(0);
|
|
4768
|
+
// 첨부 버튼으로 고른 파일과 붙여넣은 파일을 같은 함수로 받아 같은 자리에 쌓는다
|
|
4769
|
+
const addFiles = (files: File[]) => {
|
|
4770
|
+
const added = files.map(file => ({ id: nextAttachId.current++, file }));
|
|
4771
|
+
setAttached(current => [...current, ...added]);
|
|
4772
|
+
};
|
|
4746
4773
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
4747
4774
|
const closed = consult.status === 'closed';
|
|
4748
4775
|
|
|
@@ -4772,13 +4799,13 @@ function Conversation({
|
|
|
4772
4799
|
},
|
|
4773
4800
|
]
|
|
4774
4801
|
: []),
|
|
4775
|
-
...attached.map((
|
|
4802
|
+
...attached.map(({ id, file }) => ({
|
|
4776
4803
|
kind: 'message' as const,
|
|
4777
|
-
id: `f${current.length}-${
|
|
4804
|
+
id: `f${current.length}-${id}`,
|
|
4778
4805
|
direction: 'outgoing' as const,
|
|
4779
4806
|
date,
|
|
4780
4807
|
time,
|
|
4781
|
-
file: { name, size:
|
|
4808
|
+
file: { name: file.name, size: file.size },
|
|
4782
4809
|
})),
|
|
4783
4810
|
]);
|
|
4784
4811
|
onDraftChange('');
|
|
@@ -4869,11 +4896,11 @@ function Conversation({
|
|
|
4869
4896
|
<div className="flex flex-col gap-sd-8 py-sd-16">
|
|
4870
4897
|
{attached.length > 0 && (
|
|
4871
4898
|
<div className="flex flex-row flex-wrap gap-sd-8 px-sd-24">
|
|
4872
|
-
{attached.map(
|
|
4899
|
+
{attached.map(({ id, file }) => (
|
|
4873
4900
|
<SChatAttachedFile
|
|
4874
|
-
key={
|
|
4875
|
-
name={name}
|
|
4876
|
-
onRemove={() => setAttached(current => current.filter(
|
|
4901
|
+
key={id}
|
|
4902
|
+
name={file.name}
|
|
4903
|
+
onRemove={() => setAttached(current => current.filter(a => a.id !== id))}
|
|
4877
4904
|
/>
|
|
4878
4905
|
))}
|
|
4879
4906
|
</div>
|
|
@@ -4883,9 +4910,9 @@ function Conversation({
|
|
|
4883
4910
|
disabled={closed}
|
|
4884
4911
|
onValueChange={onDraftChange}
|
|
4885
4912
|
onSend={send}
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
onPasteFiles={
|
|
4913
|
+
// 파일 선택 창은 입력창이 연다 — <input type="file"> 을 따로 두지 않는다
|
|
4914
|
+
onAttachFiles={addFiles}
|
|
4915
|
+
onPasteFiles={addFiles}
|
|
4889
4916
|
// 첨부가 쌓여 있으면 글 없이도 보낼 수 있다
|
|
4890
4917
|
hasAttachments={attached.length > 0}
|
|
4891
4918
|
// 상담 종료는 전송과 층이 다른 동작 — 입력창 아래 보조 액션 슬롯이다
|
|
@@ -6427,6 +6454,11 @@ export type SCalloutMessage = string | SCalloutMessage[];
|
|
|
6427
6454
|
| `placeholder?` | `string` | `DEFAULT_PLACEHOLDER` | 안내 문구. 세 제품이 같은 말로 서도록 기본값을 쥐고 있다 |
|
|
6428
6455
|
| `disabled?` | `boolean` | `false` | 비활성 |
|
|
6429
6456
|
| `hasAttachments?` | `boolean` | `false` | 보낼 첨부가 쌓여 있는지. 켜면 글이 비어도 전송 버튼이 열린다 — 파일·이미지만 보낼 수 있다. 첨부 목록은 앱이 쥐므로 입력창은 그 유무만 받는다. 첨부를 받는 대화면 쌓인 첨부 수로 켠다. |
|
|
6457
|
+
| `attachAccept?` | `string` | — | 파일 선택 창에서 고를 수 있는 형식 (`<input accept>` 와 같다). 예: `'image/*,.pdf'` |
|
|
6458
|
+
| `attachMultiple?` | `boolean` | `true` | 파일 선택 창에서 여러 개를 한 번에 고를 수 있는지 |
|
|
6459
|
+
| `attachDisabled?` | `boolean` | `false` | 첨부를 잠근다 — 첨부 버튼(파일 선택 창)이 잠기고, **파일 붙여넣기도 받지 않는다**. 입력과 전송은 그대로 둔다. 첨부 수가 상한에 닿았을 때처럼 **더 붙일 수 없는 자리**에 쓴다. 버튼만 잠그면 캡처 이미지를 붙여넣어 상한을 넘길 수 있으므로 붙여넣기도 함께 막는다 — 파일이 든 붙여넣기는 버려지고 (`onPasteFiles` 가 불리지 않는다) 파일 이름이 글로 들어가지도 않는다. 글만 붙여넣는 것은 된다. 왜 잠겼는지는 `attachTooltip` 으로 알린다. |
|
|
6460
|
+
| `attachTooltip?` | `string` | — | 첨부 버튼에 마우스를 올리면 뜨는 툴팁. 받을 수 있는 파일 종류·크기나, 잠긴 이유를 적는다. 버튼의 접근성 이름("파일 첨부")은 바뀌지 않는다. |
|
|
6461
|
+
| `sendDisabled?` | `boolean` | `false` | 전송을 잠근다 — 전송 버튼과 Enter 전송이 함께 잠기고, 입력과 첨부는 그대로 둔다. 첨부가 올라가는 중처럼 **아직 보낼 수 없지만 적고 붙이는 것은 되는 자리**에 쓴다. 응답을 기다리는 중이면 이것이 아니라 `busy` 를 켠다 — 그쪽은 정지 버튼으로 바뀐다. |
|
|
6430
6462
|
| `busy?` | `boolean` | `false` | 응답을 기다리는 중인지. 켜면 전송 버튼 자리에 정지 버튼이 서고 Enter 전송이 잠긴다 — 답이 오는 동안 또 보내지 못하게 막는 자리다. |
|
|
6431
6463
|
| `footerAction?` | `ReactNode` | — | 입력창 아래 보조 액션 슬롯. 없으면 그 줄이 통째로 빠진다. 상담 종료처럼 **대화 자체를 끝내는, 전송과 층이 다른 동작**이 오는 자리다. 무엇을 놓을지는 앱이 정하고, 입력창은 가운데에 세우는 것까지만 한다 — `STextLink` 를 넣는 것이 보통이다. |
|
|
6432
6464
|
| `maxRows?` | `number` | `DEFAULT_MAX_ROWS` | 입력창이 자랄 수 있는 최대 줄 수. 여기 닿으면 더 자라지 않고 그 안에서 스크롤한다. |
|
|
@@ -6440,8 +6472,9 @@ export type SCalloutMessage = string | SCalloutMessage[];
|
|
|
6440
6472
|
|-------|------|-------------|
|
|
6441
6473
|
| `onValueChange` | `(value: string) => void` | 값 변경 |
|
|
6442
6474
|
| `onSend` | `(value: string) => void` | 전송. 전송 버튼을 누르거나 Enter 를 쳤을 때 현재 값을 넘긴다. **값이 비면 전송 버튼이 잠긴다** — 빈 메시지는 보낼 수 없다. 단 `hasAttachments` 가 켜져 있으면 글 없이도 보낼 수 있고, 이때 빈 문자열을 넘긴다. |
|
|
6443
|
-
| `
|
|
6444
|
-
| `
|
|
6475
|
+
| `onAttachFiles` | `(files: File[]) => void` | 첨부 버튼으로 고른 파일. **주면 입력창 왼쪽에 첨부 버튼이 서고, 누르면 파일 선택 창이 열린다.** 붙여넣기(`onPasteFiles`)와 같은 모양으로 받는다 — 두 길을 같은 함수로 받아 같은 자리에 쌓는다. 파일 선택 창은 입력창이 연다. 앱이 `<input type="file">` 을 따로 두지 않는다. |
|
|
6476
|
+
| `onAttach` | `() => void` | 첨부 버튼을 누름. **주면 입력창 왼쪽에 첨부 버튼이 선다** — 파일 선택 창은 열리지 않는다. 서버 파일함 모달처럼 **앱이 직접 고르는 흐름**에만 쓴다. 기기의 파일을 고르는 것이면 `onAttachFiles` 를 준다. 둘 다 주면 `onAttachFiles` 가 쓰이고 이것은 불리지 않는다. |
|
|
6477
|
+
| `onPasteFiles` | `(files: File[]) => void` | 붙여넣은 파일. 클립보드에 파일(캡처한 이미지 포함)이 있으면 불린다. **파일이 있으면 글자 붙여넣기를 막는다** — 그대로 두면 파일 이름이 글로 함께 들어간다. 파일이 없는 붙여넣기는 평소대로 글로 들어간다. 받은 파일을 어디에 쌓을지는 앱이 정한다 — 첨부 버튼으로 고른 파일(`onAttachFiles`)과 같은 함수로 받아 같은 자리에 쌓는 것이 보통이다. |
|
|
6445
6478
|
| `onStop` | `() => void` | 정지. `busy` 일 때 정지 버튼을 누르면 불린다 |
|
|
6446
6479
|
|
|
6447
6480
|
## Dependencies
|
|
@@ -7371,6 +7404,7 @@ export const CONTEXT_ALERT_INTENTS = [
|
|
|
7371
7404
|
| `placeholder?` | `string` | — | |
|
|
7372
7405
|
| `selectable?` | `[string, string]` | — | 선택 가능 범위 [시작, 종료] |
|
|
7373
7406
|
| `disabled?` | `boolean` | `false` | |
|
|
7407
|
+
| `showWeekday?` | `boolean` | `false` | 트리거에 보이는 날짜 뒤에 요일을 붙인다 — `2026-10-19 월요일`. 표시만 바뀌고 `value`·`onValueChange` 의 형식(`YYYY-MM-DD`)은 그대로다. `mode="date"` 에서만 동작한다 (월·연도에는 요일이 없다). 붙는 글자(띄어쓰기 + 요일 3자)만큼 내장 폭 하한·상한이 넓어진다. |
|
|
7374
7408
|
| `clearable?` | `boolean` | `false` | 선택값 지우기 버튼. 값이 있을 때만 나타나고, 누르면 `onValueChange` 로 `null` 이 온다. **필수 입력 필드에는 켜지 않는다** — 지우면 다시 고르기 전까지 폼이 통과하지 못한다. `STimePicker` · `STimeRangePicker` · `SSelect` 의 `clearable` 과 같은 규칙이다. |
|
|
7375
7409
|
| `width?` | `SFieldWidth` | — | 컨트롤 너비 — 폭 등급(`'xs' | 'sm' | 'md' | 'lg' | 'xl'`) · 숫자=px · CSS 길이. 등급은 `--cmp-field-width-*` 토큰으로 풀리므로 토큰이 바뀌면 화면이 따라간다. 값 길이에 상한이 없으면 `"100%"` 로 두어 행 전체를 쓴다. |
|
|
7376
7410
|
| `maxWidth?` | `SFieldWidth` | — | 컨트롤 최대 너비 — 폭 등급 · 숫자=px · CSS 길이. 지정하지 않으면 값 길이(`YYYY-MM-DD`)에 맞춘 내장 상한(`size='md'` → `lg`, `'sm'` → `md`)이 걸려, `width="100%"` 를 받아도 행 전체로 늘어나지 않는다. 팝오버처럼 폭이 이미 좁게 정해진 자리에서 그 폭을 그대로 채워야 하면 `width="100%"` 와 함께 `maxWidth="100%"` 를 준다. `0` 은 상한을 아예 걸지 않는다 — 부모보다 넓어지는 것까지 허용해야 할 때만 쓴다. |
|
|
@@ -7466,6 +7500,7 @@ export type SDatePickerSize = SFieldSize;
|
|
|
7466
7500
|
| `selectable?` | `[string, string]` | — | |
|
|
7467
7501
|
| `maxRange?` | `number` | — | 최대 선택 일수 |
|
|
7468
7502
|
| `showTimePicker?` | `boolean` | `false` | 시간(시:분) 선택 푸터 사용 여부. true이면 value는 "YYYY-MM-DD HH:mm" 형식이 되고, 캘린더 하단에 시작·종료 시간 입력이 표시된다. |
|
|
7503
|
+
| `showWeekday?` | `boolean` | `false` | 트리거에 보이는 시작·종료 날짜 뒤에 요일을 붙인다 — `2026-10-19 월요일 ~ 2026-10-25 일요일`. `showTimePicker` 와 함께 쓰면 요일이 날짜와 시간 사이에 온다 (`2026-10-19 월요일 09:00`). 표시만 바뀌고 `value`·`onValueChange` 의 형식은 그대로다. 붙는 글자(띄어쓰기 + 요일 3자, 양쪽)만큼 내장 폭 하한·상한이 넓어진다. |
|
|
7469
7504
|
| `disabled?` | `boolean` | `false` | |
|
|
7470
7505
|
| `clearable?` | `boolean` | `false` | 선택값 지우기 버튼. 값이 있을 때만 나타나고, 누르면 `onValueChange` 로 `null` 이 온다. **필수 입력 필드에는 켜지 않는다** — 지우면 다시 고르기 전까지 폼이 통과하지 못한다. `SDatePicker` · `STimePicker` · `SSelect` 의 `clearable` 과 같은 규칙이다. |
|
|
7471
7506
|
| `width?` | `SFieldWidth` | — | 컨트롤 너비 — 폭 등급(`'xs' | 'sm' | 'md' | 'lg' | 'xl'`) · 숫자=px · CSS 길이. 등급은 `--cmp-field-width-*` 토큰으로 풀리므로 토큰이 바뀌면 화면이 따라간다. 값 길이에 상한이 없으면 `"100%"` 로 두어 행 전체를 쓴다. |
|
|
@@ -8180,6 +8215,7 @@ export const SEDITOR_TOOLBAR_ITEMS = [
|
|
|
8180
8215
|
| `depth?` | `1 \| 2 \| 3` | `1` | 중첩 단계. Figma 기준 1~3단계. `SExpansionList` 안에서는 주지 않아도 중첩에서 정해진다 — 직접 주면 그 값이 우선한다. |
|
|
8181
8216
|
| `leading?` | `ReactNode \| SExpansionItemRenderProp` | — | 타이틀 앞에 표시할 아이콘/콘텐츠 |
|
|
8182
8217
|
| `trailing?` | `ReactNode` | — | 타이틀 뒤에 표시할 태그/콘텐츠 |
|
|
8218
|
+
| `headerAction?` | `ReactNode` | — | 제목 줄 오른쪽 끝에 놓는 컨트롤 슬롯 — ⋯ 메뉴·아이콘 버튼처럼 **누르는 것**이 오는 자리다. 제목 줄은 통째로 여닫는 `<button>` 이라 `title`·`trailing` 에 컨트롤을 넣으면 버튼 안에 버튼이 중첩되고, 누를 때마다 항목이 함께 여닫힌다. 이 슬롯은 그 버튼 **밖**에 그려지므로 눌러도 여닫히지 않고, Tab 으로 따로 닿는다. 태그·숫자처럼 보기만 하는 것은 `trailing` 에 둔다. 항목이 `disabled` 여도 슬롯은 잠그지 않는다 — 잠글지는 넣은 컨트롤에 앱이 정한다. |
|
|
8183
8219
|
| `bodyPadding?` | `boolean` | `true` | body 내부 패딩 사용 여부. `SExpansionList` 안에서 본문이 목록(`SListItem`·`SExpansionItem` 만, 또는 `SExpansionList`)이면 기본으로 꺼진다. |
|
|
8184
8220
|
| `bordered?` | `boolean` | `false` | 외곽 테두리 사용 여부 |
|
|
8185
8221
|
| `expanded?` | `boolean` | — | |
|
|
@@ -8587,6 +8623,8 @@ export interface SFormValidationError {
|
|
|
8587
8623
|
| `ariaExpanded?` | `boolean` | — | disclosure 버튼일 때 펼침 상태 (aria-expanded) |
|
|
8588
8624
|
| `tooltipText?` | `string` | — | 호버 시 표시할 툴팁 텍스트 |
|
|
8589
8625
|
| `disabled?` | `boolean` | `false` | 비활성 |
|
|
8626
|
+
| `href?` | `string` | — | 이동할 주소. **주면 `<button>` 대신 `<a>` 로 그린다** — 가운데 클릭·⌘클릭·주소 복사가 링크처럼 듣는다. 눌러서 다른 화면·문서로 가는 버튼이면 `onClick` 에서 `location` 을 바꾸지 말고 이것을 준다. 비활성이면 주소를 떼어 눌러도 가지 않는다. |
|
|
8627
|
+
| `target?` | `string` | — | `href` 를 여는 곳. `'_blank'` 면 `rel="noopener noreferrer"` 를 함께 붙인다 |
|
|
8590
8628
|
| `anchorClassName?` | `string` | — | tooltip anchor wrapper 클래스. tooltipText가 있을 때 위치 조정용으로 사용합니다. |
|
|
8591
8629
|
| `anchorStyle?` | `CSSProperties` | — | tooltip anchor wrapper 스타일. tooltipText가 있을 때 위치 조정용으로 사용합니다. |
|
|
8592
8630
|
| `className?` | `string` | — | |
|
|
@@ -8596,7 +8634,7 @@ export interface SFormValidationError {
|
|
|
8596
8634
|
|
|
8597
8635
|
| Event | Type | Description |
|
|
8598
8636
|
|-------|------|-------------|
|
|
8599
|
-
| `onClick` | `(e: MouseEvent<HTMLButtonElement>) => void` | 클릭 (sdClick) |
|
|
8637
|
+
| `onClick` | `(e: MouseEvent<HTMLButtonElement \| HTMLAnchorElement>) => void` | 클릭 (sdClick). `href` 와 함께 주면 이동 전에 불린다 |
|
|
8600
8638
|
|
|
8601
8639
|
## Types
|
|
8602
8640
|
|
|
@@ -8857,7 +8895,7 @@ export interface SGnbSystemAlert {
|
|
|
8857
8895
|
/** 라벨 텍스트. 기본 `'알림'` */
|
|
8858
8896
|
label?: string;
|
|
8859
8897
|
/**
|
|
8860
|
-
* 읽지 않은 알림 개수. 1 이상이면 벨이 울리는 아이콘·강조색으로 바뀌고
|
|
8898
|
+
* 읽지 않은 알림 개수. 1 이상이면 벨이 울리는 아이콘·강조색으로 바뀌고 개수 배지가 붙는다.
|
|
8861
8899
|
* 개수를 적을 가로가 모자란 자리(접힌 레일 · 기본 폭보다 좁게 줄인 판)에서는 점 배지로 바뀐다.
|
|
8862
8900
|
* 표기는 컴포넌트가 맡는다 — 세 자리마다 콤마를 넣어 그리므로 앱이 포맷한 문자열을 넘기지 않는다.
|
|
8863
8901
|
*/
|
|
@@ -8959,7 +8997,9 @@ export interface SGnbSystemAccount {
|
|
|
8959
8997
|
* 타이포그래피는 CSS 변수가 아니라 `.typo-*` 프리셋으로 나온다.
|
|
8960
8998
|
* domainTag.typography → typo-body-sm-medium (12/500/20)
|
|
8961
8999
|
* item.label.typography → typo-body-sm-semibold (12/600/20)
|
|
8962
|
-
*
|
|
9000
|
+
*
|
|
9001
|
+
* 알림 개수는 글자가 아니라 배지(`STag` xs · deep)로 그린다 — 배경·글자색·치수는 태그 토큰
|
|
9002
|
+
* (`--cmp-tag-*`)이 쥔다.
|
|
8963
9003
|
*/
|
|
8964
9004
|
|
|
8965
9005
|
/** 색상(표면). light = 흰 면, dark = 오션블루 면. GNB 의 color 를 그대로 따라간다 */
|
|
@@ -8975,6 +9015,7 @@ export const GNB_SYSTEM_COLORS = ['light', 'dark'] as const;
|
|
|
8975
9015
|
- [SIcon](../SIcon)
|
|
8976
9016
|
- [SPortal](../SPortal)
|
|
8977
9017
|
- [SSystemActionButton](../SSystemActionButton)
|
|
9018
|
+
- [STag](../STag)
|
|
8978
9019
|
- [STooltip](../STooltip)
|
|
8979
9020
|
|
|
8980
9021
|
### Graph
|
|
@@ -12441,7 +12482,7 @@ export type STextLinkSize = 'sm' | 'md' | 'lg';
|
|
|
12441
12482
|
| `disabled?` | `boolean` | `false` | |
|
|
12442
12483
|
| `clearable?` | `boolean` | `false` | |
|
|
12443
12484
|
| `showMeridiem?` | `boolean` | — | 오전/오후 선택 표시 여부. 지정하지 않으면 type="midday"일 때만 켜집니다. |
|
|
12444
|
-
| `minuteStep?` | `number` | `1` |
|
|
12485
|
+
| `minuteStep?` | `number` | `1` | 분 리스트에 나열할 간격(분). 10 이면 `00 10 20 30 40 50` 만 고를 수 있다 |
|
|
12445
12486
|
| `width?` | `SFieldWidth` | — | 컨트롤 너비 — 폭 등급(`'xs' | 'sm' | 'md' | 'lg' | 'xl'`) · 숫자=px · CSS 길이. 등급은 `--cmp-field-width-*` 토큰으로 풀리므로 토큰이 바뀌면 화면이 따라간다. 값 길이에 상한이 없으면 `"100%"` 로 두어 행 전체를 쓴다. |
|
|
12446
12487
|
| `maxWidth?` | `SFieldWidth` | — | 컨트롤 최대 너비 — 폭 등급 · 숫자=px · CSS 길이. 지정하지 않으면 값 길이(`HH:mm`)에 맞춘 내장 상한(`size='md'` → `lg`, `'sm'` → `md`)이 걸려, `width="100%"` 를 받아도 행 전체로 늘어나지 않는다. 팝오버처럼 폭이 이미 좁게 정해진 자리에서 그 폭을 그대로 채워야 하면 `width="100%"` 와 함께 `maxWidth="100%"` 를 준다. `0` 은 상한을 아예 걸지 않는다 — 부모보다 넓어지는 것까지 허용해야 할 때만 쓴다. |
|
|
12447
12488
|
| `name?` | `string` | — | |
|
|
@@ -12517,8 +12558,8 @@ export type STimePickerSize = 'sm' | 'md';
|
|
|
12517
12558
|
| `disabled?` | `boolean` | `false` | |
|
|
12518
12559
|
| `clearable?` | `boolean` | `false` | |
|
|
12519
12560
|
| `showMeridiem?` | `boolean` | — | 오전/오후 선택 표시 여부. 지정하지 않으면 type="midday"일 때만 켜집니다. |
|
|
12520
|
-
| `minuteStep?` | `number` | `1` |
|
|
12521
|
-
| `rangeOrder?` | `STimeRangePickerRangeOrder` | `'strict'` | 범위 순서 정책. strict는
|
|
12561
|
+
| `minuteStep?` | `number` | `1` | 분 리스트에 나열할 간격(분). 10 이면 `00 10 20 30 40 50` 만 고를 수 있다 |
|
|
12562
|
+
| `rangeOrder?` | `STimeRangePickerRangeOrder` | `'strict'` | 범위 순서 정책. `strict` 는 종료 리스트에서 시작보다 이른 시각을 비활성으로 막고, 시작을 종료 뒤로 옮기면 종료가 따라온다. `allow-cross-day` 는 제약 없이 고른다(자정을 넘는 범위). |
|
|
12522
12563
|
| `width?` | `SFieldWidth` | — | 컨트롤 너비 — 폭 등급(`'xs' | 'sm' | 'md' | 'lg' | 'xl'`) · 숫자=px · CSS 길이. 등급은 `--cmp-field-width-*` 토큰으로 풀리므로 토큰이 바뀌면 화면이 따라간다. 값 길이에 상한이 없으면 `"100%"` 로 두어 행 전체를 쓴다. |
|
|
12523
12564
|
| `maxWidth?` | `SFieldWidth` | — | 컨트롤 최대 너비 — 폭 등급 · 숫자=px · CSS 길이. 지정하지 않으면 값 길이(`HH:mm ~ HH:mm`)에 맞춘 내장 상한이 걸려, `width="100%"` 를 받아도 행 전체로 늘어나지 않는다. 팝오버처럼 폭이 이미 좁게 정해진 자리에서 그 폭을 그대로 채워야 하면 `width="100%"` 와 함께 `maxWidth="100%"` 를 준다. `0` 은 상한을 아예 걸지 않는다 — 부모보다 넓어지는 것까지 허용해야 할 때만 쓴다. |
|
|
12524
12565
|
| `name?` | `string` | — | |
|
package/dist/llms.txt
CHANGED
|
@@ -1444,6 +1444,7 @@ tableRef.current.scrollToRow(row); // 복원
|
|
|
1444
1444
|
- **`ariaLabel` 은 필수다.** 라벨이 없어 스크린리더가 읽을 것이 아이콘밖에 없다. 뜻이 덜 명확한 아이콘은 **`tooltipText`** 로 이름을 띄운다.
|
|
1445
1445
|
- 여러 개를 나란히 둘 때 간격은 **`gap-sd-4` 고정** — 가로·세로가 같다 (§2-2).
|
|
1446
1446
|
- **라벨이 필요하거나 화면의 주 액션이면 `SGhostButton` 이 아니라 `SButton` 이다.** 위계가 낮다는 이유만으로 `SGhostButton` 을 고르지 않는다 — 라벨이 붙는 저강조 액션은 `SButton` 의 `outline` 조합(§3-5-1)이다.
|
|
1447
|
+
- **다른 화면·문서로 가는 아이콘 버튼은 `href` 를 준다.** 링크(`<a>`)로 그려져 가운데 클릭·⌘클릭·주소 복사가 듣는다. `onClick` 에서 `location`·`window.open` 으로 옮기거나 `<a>` 로 감싸지 않는다. 새 탭이면 `target="_blank"` 만 준다 — `rel` 은 컴포넌트가 붙인다.
|
|
1447
1448
|
|
|
1448
1449
|
```tsx
|
|
1449
1450
|
// 테이블 행 끝 인라인 액션 — 수정은 action, 행 삭제는 default, 간격은 gap-sd-4
|
|
@@ -1461,6 +1462,8 @@ tableRef.current.scrollToRow(row); // 복원
|
|
|
1461
1462
|
✅ <SGhostButton icon="close" ariaLabel="닫기" onClick={onClose} /> // 모달 닫기 = default
|
|
1462
1463
|
✅ <SGhostButton icon="copy" ariaLabel="상품코드 복사" tooltipText="복사" /> // 뜻이 덜 명확 → 툴팁
|
|
1463
1464
|
❌ <SGhostButton icon="download" ariaLabel="엑셀 다운로드" /> // 목록의 주 액션 → SButton
|
|
1465
|
+
✅ <SGhostButton icon="helpOutline" ariaLabel="도움말" href={HELP_URL} target="_blank" />
|
|
1466
|
+
❌ <SGhostButton icon="helpOutline" ariaLabel="도움말" onClick={() => window.open(HELP_URL)} /> // 가는 버튼 → href
|
|
1464
1467
|
```
|
|
1465
1468
|
|
|
1466
1469
|
##### `SMeatballButton` — ⋯ 뒤에 메뉴를 접어 둘 때
|
|
@@ -1886,12 +1889,21 @@ useEffect(() => {
|
|
|
1886
1889
|
| `STimePicker` | md | lg |
|
|
1887
1890
|
| `STimeRangePicker` | md | lg (오전/오후 표시는 두 사이즈 모두 lg) |
|
|
1888
1891
|
|
|
1892
|
+
`SDatePicker`·`SDateRangePicker` 에 `showWeekday` 를 켜면 날짜 뒤에 요일이 붙어(`2026-10-19 월요일`) 붙는 글자(띄어쓰기 + 요일 3자) 폭만큼 위 표의 하한·상한이 넓어진다 — 등급이 바뀌는 것이 아니다. 늘어난 폭은 컴포넌트가 스스로 잡으므로 요일 때문에 `width` 를 따로 주지 않는다. 표시만 바뀌고 값 형식은 그대로라, **요일을 붙이려고 값을 가공하거나 `hint` 에 요일을 따로 적지 않는다.**
|
|
1893
|
+
|
|
1889
1894
|
`SDateRangePicker` 가 한 등급씩 위인 것은 값이 `YYYY-MM-DD ~ YYYY-MM-DD` 로 두 배가 넘기 때문이다. 같은 이유로 `STimeRangePicker` 의 오전/오후 모드도 sm 에서 한 등급 위를 쓴다 — 그 모드의 최소 폭이 md 등급을 이미 넘어, 그대로 두면 하한이 상한을 넘어 상한이 무력해진다.
|
|
1890
1895
|
|
|
1891
1896
|
네 컴포넌트 모두 이 상한을 `maxWidth` 로 덮을 수 있다 — 등급을 주면 그 등급이 상한이 되고, `width="100%" maxWidth="100%"` 면 부모 폭을 그대로 채운다. **폭이 이미 좁게 정해진 자리(팝오버·좁은 카드)에서 쓴다.**
|
|
1892
1897
|
|
|
1893
1898
|
**`SKeyValueTable` 안에서는 필드마다 `maxWidth` 를 주지 않는다.** 표 안에서 이 상한이 거슬리는 이유는 대개 "같은 행의 다른 필드와 오른쪽 끝이 어긋난다" 이고, 그건 표 전체의 문제다. 표의 `fieldWidth="fill"` 로 한 번에 정한다 (§4-3). 그 표에서 한 필드만 예외로 둘 때만 `options.maxWidth` 를 쓴다.
|
|
1894
1899
|
|
|
1900
|
+
##### 시간 피커 — 분 간격은 `minuteStep`, 범위 순서는 `rangeOrder`
|
|
1901
|
+
|
|
1902
|
+
`STimePicker`·`STimeRangePicker` 는 `[오전/오후] | 시 | 분` 리스트에서 고른다 (직접 타이핑하지 않는다).
|
|
1903
|
+
|
|
1904
|
+
- **분 간격은 `minuteStep` 으로 정한다.** 분 리스트에 그 간격의 분만 나열되므로, 고른 값을 앱에서 다시 반올림하거나 `rules` 로 간격을 검사하지 않는다.
|
|
1905
|
+
- **`STimeRangePicker` 의 "종료가 시작보다 이르면 안 된다" 는 앱이 검증하지 않는다.** 기본 `rangeOrder="strict"` 가 종료 리스트에서 그 시각을 비활성으로 막는다. 자정을 넘는 범위(야간 근무 22:00~02:00 등)를 받아야 할 때만 `rangeOrder="allow-cross-day"` 를 준다.
|
|
1906
|
+
|
|
1895
1907
|
##### 값을 지울 수 있게 하려면 `clearable`
|
|
1896
1908
|
|
|
1897
1909
|
`SSelect` · `SDatePicker` · `SDateRangePicker` · `STimePicker` · `STimeRangePicker` 가 같은 규칙으로 갖는다. 값이 있을 때만 지우기 버튼이 나타나고, 누르면 **`onValueChange` 로 `null` 이 온다** (빈 문자열이 아니다). 받는 쪽 상태도 `null` 을 담을 수 있어야 한다.
|
|
@@ -1984,7 +1996,7 @@ const [from, setFrom] = useState<string | null>(null);
|
|
|
1984
1996
|
- **펼침 버튼·들여쓰기는 컴포넌트가 그린다.** 기본은 보이는 첫 컬럼이고 `treeColumn` 으로 옮긴다. 그 컬럼은 왼쪽 정렬로 두고, 셀 내용은 `render`·`format` 으로 바꾼다 — `renderCell` 은 펼침 버튼을 넣을 자리가 없어 에러가 난다.
|
|
1985
1997
|
- **셀 병합은 가로(`colSpan`)만 한다.** 트리 컬럼이 아닌 컬럼은 `renderCell` 로 `<td>` 를 바꿀 수 있고, 한 행 안에서 끝나는 `colSpan` 은 접고 펴도 그대로다. **`rowSpan` 으로 세로 병합하지 않는다** — 행이 접히면 사라진 행 몫까지 병합이 남아 아래 행의 칸을 덮고, 덮인 행의 칸은 옆 컬럼으로 밀려난다. 부모와 하위 행에 같은 값이 반복되면 부모 행에만 쓰고 하위 행은 비워 둔다.
|
|
1986
1998
|
- **펼침 영역은 트리 컬럼 셀 전체다.** 하위 행이 있는 행은 펼침 버튼뿐 아니라 그 셀 어디를 눌러도 접고 편다. 다른 셀을 누르면 행 클릭(`onRowClick`)이다 — 트리 컬럼 셀을 누른 것은 행 클릭으로 가지 않는다.
|
|
1987
|
-
-
|
|
1999
|
+
- **펼침·접힘 모션은 컴포넌트가 넣는다.** 하위 행이 부모 아래에서 서랍처럼 펼쳐지고 접히며, 아래 행들도 함께 내려가고 올라온다. 모션을 줄인 환경에서는 꺼진다. 행(`rowClassName`)에 애니메이션·`transform`·`clip-path` 를 직접 주지 않는다 — 컴포넌트가 행의 위치를 움직이는 것과 겹친다. `onExpandedChange` 는 모션을 기다리지 않고 누른 즉시 온다.
|
|
1988
2000
|
- **트리 컬럼에 링크·버튼을 넣어도 된다.** 셀 안의 링크·버튼·입력을 누르면 그 동작만 일어나고 펼침은 일어나지 않는다 — `stopPropagation` 을 직접 넣지 않는다. 상품명을 상세 링크로 거는 것처럼 쓴다.
|
|
1989
2001
|
- **선택은 계층을 따른다.** 부모를 고르면 접힌 하위 행까지 함께 골라지고, 일부만 골라진 부모는 "일부 선택"으로 그려진다. 하위 행이 모두 골라지면 부모도 `selected` 에 들어간다 — 처리 대상이 잎 행뿐이면 받은 목록에서 하위 행이 없는 행만 걸러 쓴다.
|
|
1990
2002
|
- **가로로 스크롤되는 표는 트리 컬럼을 왼쪽에 고정한다**(`stickyColumn={{ left: 1 }}`, 트리 컬럼이 첫 컬럼일 때). 스크롤로 들여쓰기가 밀려나면 어느 행이 누구의 하위인지 읽히지 않는다. 체크박스 열은 `selectable` 이면 늘 고정되므로 개수에 넣지 않는다.
|
|
@@ -2033,6 +2045,13 @@ const [selectedId, setSelectedId] = useState<string>();
|
|
|
2033
2045
|
- **리스트 밖 단독 `SExpansionItem` 은 본문을 다루지 않는다.** 선택하는 목록을 담으려면 `SExpansionList` 로 감싼다.
|
|
2034
2046
|
|
|
2035
2047
|
- **하위를 가지지 않는 항목은 `SExpansionItem` 이 아니라 `SListItem`** 으로 둔다. 펼칠 것이 없는데 펼침 항목으로 만들면 화살표만 남는다.
|
|
2048
|
+
- **제목 줄에 누르는 컨트롤(⋯ 메뉴·아이콘 버튼)은 `headerAction` 에 둔다.** 제목 줄은 통째로 여닫는 버튼이라 `title`·`trailing` 에 컨트롤을 넣으면 버튼이 중첩되고 누를 때마다 함께 여닫힌다. `headerAction` 은 그 버튼 밖에 그려져 눌러도 여닫히지 않는다 — `stopPropagation` 이나 제어 모드(`expanded`)로 막지 않는다. 태그·건수처럼 보기만 하는 것은 그대로 `trailing` 이다.
|
|
2049
|
+
|
|
2050
|
+
```tsx
|
|
2051
|
+
✅ <SExpansionItem title="상품 문의" trailing={<STag label="3" color="grey" />}
|
|
2052
|
+
headerAction={<SMeatballButton size="xs" items={folderMenu} onItemClick={v => onFolderAction(id, v)} />}>…</SExpansionItem>
|
|
2053
|
+
❌ <SExpansionItem title="상품 문의" trailing={<SMeatballButton … />}>…</SExpansionItem> {/* 버튼 안 버튼 — 누르면 함께 여닫힌다 */}
|
|
2054
|
+
```
|
|
2036
2055
|
|
|
2037
2056
|
#### 3-7-8. SCard vs SSectionHeaderCard
|
|
2038
2057
|
|
|
@@ -4519,7 +4538,8 @@ export default function StoreSetupPage() {
|
|
|
4519
4538
|
- **날짜가 바뀌는 자리에는 날짜 구분선을 끼운다** — 가로 `SDivider` 두 개 사이에 날짜(`typo-body-sm-default` + `text-fg-tertiary`). 일어난 사실이 아니라 흐름의 경계라 시스템 안내 알약(`SChatSystemMessage`)을 쓰지 않는다. **날짜는 `2026년 10월 6일 화요일`** 로 쓴다 — 말풍선 시각과 같은 대화 예외다(§1-4). `toLocaleDateString()` 은 로케일에 따라 결과가 바뀌므로 쓰지 않고 연·월·일·요일을 직접 이어 붙인다. 가로 구분선은 기본이 전체 폭이라 한 줄에 둘을 놓을 때는 `className="w-auto flex-1"` 로 남은 폭을 나눠 갖게 한다. 앞뒤 간격은 시스템 안내와 같은 토큰이다. 날짜가 다르면 같은 시각이어도 한 묶음으로 합치지 않는다.
|
|
4520
4539
|
- 대화의 시각은 `오후 5:52` 로 쓴다 (§1-4 대화 예외). 읽음 표시는 보낸 메시지에만, 템플릿으로 보낸 글은 `variant="template"` 로 면을 나눈다.
|
|
4521
4540
|
- **입력 중인 글은 페이지가 상담마다 든다.** 상담을 옮겨도 쓰던 글이 남고, 템플릿 패널이 그 글에 넣을 수 있다. 템플릿은 누르면 **입력창에 넣는다** — 바로 보내지 않고 상담사가 고쳐 보낸다. 보내기 전 첨부는 입력창 위에 `SChatAttachedFile` 로 쌓는다.
|
|
4522
|
-
- **첨부를 받는 대화면 `
|
|
4541
|
+
- **첨부를 받는 대화면 `onAttachFiles` 와 `onPasteFiles` 에 같은 함수를 넘긴다** — 첨부 버튼으로 고른 파일과 붙여넣은 파일·캡처 이미지를 같은 자리에 쌓는다. 파일 선택 창은 입력창이 연다 — `<input type="file">` 을 따로 두지 않는다. `onAttach` 는 서버 파일함 모달처럼 앱이 직접 고르는 흐름에만 쓴다. 붙여넣기에서 파일을 골라내고 글로 들어가지 않게 막는 것은 입력창이 한다 — `onPaste` 로 직접 처리하지 않는다. 첨부가 쌓여 있으면 `hasAttachments` 를 켠다 — 글 없이 파일만 보낼 수 있고, 이때 `onSend` 에는 빈 문자열이 온다.
|
|
4542
|
+
- **입력창의 일부만 막을 때는 그 동작만 잠근다.** 첨부가 올라가는 중이면 `sendDisabled` 로 전송만 잠근다 — 글을 적고 파일을 더 붙이는 것은 된다. 첨부 수가 상한에 닿았으면 `attachDisabled` 로 첨부를 잠그고 이유는 `attachTooltip` 으로 알린다 — 파일 붙여넣기도 함께 막힌다. 상한 아래에서 여러 파일이 한 번에 들어와 넘치는 것은 앱이 `onPasteFiles` 에서 잘라 낸다. `disabled` 는 입력까지 잠그므로 종료된 대화에만, `busy` 는 정지 버튼으로 바뀌므로 응답을 기다릴 때만 쓴다.
|
|
4523
4543
|
- **상담 종료는 `SChatInput` 의 `footerAction` 이다** — 전송과 층이 다른 동작이라 입력창 아래 보조 슬롯(`STextLink`)에 두고, 실행 전에 `SModal.confirm` 으로 묻는다. 종료된 상담은 입력창을 `disabled` 로 잠그고 종료 링크를 뺀다.
|
|
4524
4544
|
- 목록의 상태는 `STag` 다 — 색 점(`SBadge`)만으로 찍지 않는다 (§3-1). 템플릿 분류는 `SExpansionList` 루트 하나에 분류(`SExpansionItem`) 안 템플릿(`SListItem`)을 바로 넣는다 (§3-7-7).
|
|
4525
4545
|
|
|
@@ -4743,7 +4763,14 @@ function Conversation({
|
|
|
4743
4763
|
}) {
|
|
4744
4764
|
const initial = useConsultMessages(consult.id);
|
|
4745
4765
|
const [entries, setEntries] = useState<ChatEntry[]>(initial);
|
|
4746
|
-
|
|
4766
|
+
// 보내기 전 첨부. 같은 파일을 두 번 붙일 수 있으므로 이름이 아니라 id 로 가른다
|
|
4767
|
+
const [attached, setAttached] = useState<{ id: number; file: File }[]>([]);
|
|
4768
|
+
const nextAttachId = useRef(0);
|
|
4769
|
+
// 첨부 버튼으로 고른 파일과 붙여넣은 파일을 같은 함수로 받아 같은 자리에 쌓는다
|
|
4770
|
+
const addFiles = (files: File[]) => {
|
|
4771
|
+
const added = files.map(file => ({ id: nextAttachId.current++, file }));
|
|
4772
|
+
setAttached(current => [...current, ...added]);
|
|
4773
|
+
};
|
|
4747
4774
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
4748
4775
|
const closed = consult.status === 'closed';
|
|
4749
4776
|
|
|
@@ -4773,13 +4800,13 @@ function Conversation({
|
|
|
4773
4800
|
},
|
|
4774
4801
|
]
|
|
4775
4802
|
: []),
|
|
4776
|
-
...attached.map((
|
|
4803
|
+
...attached.map(({ id, file }) => ({
|
|
4777
4804
|
kind: 'message' as const,
|
|
4778
|
-
id: `f${current.length}-${
|
|
4805
|
+
id: `f${current.length}-${id}`,
|
|
4779
4806
|
direction: 'outgoing' as const,
|
|
4780
4807
|
date,
|
|
4781
4808
|
time,
|
|
4782
|
-
file: { name, size:
|
|
4809
|
+
file: { name: file.name, size: file.size },
|
|
4783
4810
|
})),
|
|
4784
4811
|
]);
|
|
4785
4812
|
onDraftChange('');
|
|
@@ -4870,11 +4897,11 @@ function Conversation({
|
|
|
4870
4897
|
<div className="flex flex-col gap-sd-8 py-sd-16">
|
|
4871
4898
|
{attached.length > 0 && (
|
|
4872
4899
|
<div className="flex flex-row flex-wrap gap-sd-8 px-sd-24">
|
|
4873
|
-
{attached.map(
|
|
4900
|
+
{attached.map(({ id, file }) => (
|
|
4874
4901
|
<SChatAttachedFile
|
|
4875
|
-
key={
|
|
4876
|
-
name={name}
|
|
4877
|
-
onRemove={() => setAttached(current => current.filter(
|
|
4902
|
+
key={id}
|
|
4903
|
+
name={file.name}
|
|
4904
|
+
onRemove={() => setAttached(current => current.filter(a => a.id !== id))}
|
|
4878
4905
|
/>
|
|
4879
4906
|
))}
|
|
4880
4907
|
</div>
|
|
@@ -4884,9 +4911,9 @@ function Conversation({
|
|
|
4884
4911
|
disabled={closed}
|
|
4885
4912
|
onValueChange={onDraftChange}
|
|
4886
4913
|
onSend={send}
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
onPasteFiles={
|
|
4914
|
+
// 파일 선택 창은 입력창이 연다 — <input type="file"> 을 따로 두지 않는다
|
|
4915
|
+
onAttachFiles={addFiles}
|
|
4916
|
+
onPasteFiles={addFiles}
|
|
4890
4917
|
// 첨부가 쌓여 있으면 글 없이도 보낼 수 있다
|
|
4891
4918
|
hasAttachments={attached.length > 0}
|
|
4892
4919
|
// 상담 종료는 전송과 층이 다른 동작 — 입력창 아래 보조 액션 슬롯이다
|
package/dist/styles.css
CHANGED
|
@@ -425,7 +425,6 @@
|
|
|
425
425
|
--animate-floating-out-bottom: floating-out-bottom 0.15s ease-in forwards;
|
|
426
426
|
--animate-floating-out-left: floating-out-left 0.15s ease-in forwards;
|
|
427
427
|
--animate-floating-out-right: floating-out-right 0.15s ease-in forwards;
|
|
428
|
-
--animate-table-row-in: floating-in-bottom 0.15s ease-out;
|
|
429
428
|
--animate-skeleton-wave: skeleton-wave 1.5s linear 0.5s infinite;
|
|
430
429
|
--animate-skeleton-pulse: skeleton-pulse 1.5s ease-in-out 0.5s infinite;
|
|
431
430
|
--animate-skeleton-fade: skeleton-fade 1.5s linear 0.5s infinite;
|
|
@@ -918,9 +917,6 @@
|
|
|
918
917
|
.mt-sd-36 {
|
|
919
918
|
margin-top: var(--spacing-sd-36);
|
|
920
919
|
}
|
|
921
|
-
.-mr-px {
|
|
922
|
-
margin-right: -1px;
|
|
923
|
-
}
|
|
924
920
|
.mr-\[6px\] {
|
|
925
921
|
margin-right: 6px;
|
|
926
922
|
}
|
|
@@ -1212,9 +1208,6 @@
|
|
|
1212
1208
|
.h-\[var\(--cmp-table-header-height\)\] {
|
|
1213
1209
|
height: var(--cmp-table-header-height);
|
|
1214
1210
|
}
|
|
1215
|
-
.h-\[var\(--cmp-textinput-sm-height\)\] {
|
|
1216
|
-
height: var(--cmp-textinput-sm-height);
|
|
1217
|
-
}
|
|
1218
1211
|
.h-\[var\(--sys-size-\.\.\.\)\] {
|
|
1219
1212
|
height: var(--sys-size-...);
|
|
1220
1213
|
}
|
|
@@ -1380,9 +1373,6 @@
|
|
|
1380
1373
|
.w-\[48px\] {
|
|
1381
1374
|
width: 48px;
|
|
1382
1375
|
}
|
|
1383
|
-
.w-\[61px\] {
|
|
1384
|
-
width: 61px;
|
|
1385
|
-
}
|
|
1386
1376
|
.w-\[72px\] {
|
|
1387
1377
|
width: 72px;
|
|
1388
1378
|
}
|
|
@@ -2536,10 +2526,6 @@
|
|
|
2536
2526
|
border-top-left-radius: 4px;
|
|
2537
2527
|
border-bottom-left-radius: 4px;
|
|
2538
2528
|
}
|
|
2539
|
-
.rounded-l-\[var\(--cmp-select-sm-radius\)\] {
|
|
2540
|
-
border-top-left-radius: var(--cmp-select-sm-radius);
|
|
2541
|
-
border-bottom-left-radius: var(--cmp-select-sm-radius);
|
|
2542
|
-
}
|
|
2543
2529
|
.rounded-tl {
|
|
2544
2530
|
border-top-left-radius: 0.25rem;
|
|
2545
2531
|
}
|
|
@@ -2551,10 +2537,6 @@
|
|
|
2551
2537
|
border-top-right-radius: 4px;
|
|
2552
2538
|
border-bottom-right-radius: 4px;
|
|
2553
2539
|
}
|
|
2554
|
-
.rounded-r-\[var\(--cmp-select-sm-radius\)\] {
|
|
2555
|
-
border-top-right-radius: var(--cmp-select-sm-radius);
|
|
2556
|
-
border-bottom-right-radius: var(--cmp-select-sm-radius);
|
|
2557
|
-
}
|
|
2558
2540
|
.rounded-tr {
|
|
2559
2541
|
border-top-right-radius: 0.25rem;
|
|
2560
2542
|
}
|
|
@@ -2810,12 +2792,6 @@
|
|
|
2810
2792
|
.border-\[var\(--cmp-table-border-color\)\] {
|
|
2811
2793
|
border-color: var(--cmp-table-border-color);
|
|
2812
2794
|
}
|
|
2813
|
-
.border-\[var\(--cmp-timepicker-border-default\)\] {
|
|
2814
|
-
border-color: var(--cmp-timepicker-border-default);
|
|
2815
|
-
}
|
|
2816
|
-
.border-\[var\(--cmp-timepicker-border-focus\)\] {
|
|
2817
|
-
border-color: var(--cmp-timepicker-border-focus);
|
|
2818
|
-
}
|
|
2819
2795
|
.border-border-default {
|
|
2820
2796
|
border-color: var(--color-border-default);
|
|
2821
2797
|
}
|
|
@@ -3779,6 +3755,9 @@
|
|
|
3779
3755
|
.pr-\(--cmp-chatMessage-row-paddingInset\) {
|
|
3780
3756
|
padding-right: var(--cmp-chatMessage-row-paddingInset);
|
|
3781
3757
|
}
|
|
3758
|
+
.pr-\(--cmp-listExpansionItem-title-gap\) {
|
|
3759
|
+
padding-right: var(--cmp-listExpansionItem-title-gap);
|
|
3760
|
+
}
|
|
3782
3761
|
.pr-\(--cmp-listExpansionItem-title-paddingX\) {
|
|
3783
3762
|
padding-right: var(--cmp-listExpansionItem-title-paddingX);
|
|
3784
3763
|
}
|
|
@@ -4461,12 +4440,6 @@
|
|
|
4461
4440
|
.text-\[color\:var\(--sys-color-field-text-default\)\] {
|
|
4462
4441
|
color: var(--sys-color-field-text-default);
|
|
4463
4442
|
}
|
|
4464
|
-
.text-\[var\(--cmp-field-hint-color\)\] {
|
|
4465
|
-
color: var(--cmp-field-hint-color);
|
|
4466
|
-
}
|
|
4467
|
-
.text-\[var\(--cmp-timepicker-border-focus\)\] {
|
|
4468
|
-
color: var(--cmp-timepicker-border-focus);
|
|
4469
|
-
}
|
|
4470
4443
|
.text-\[var\(--cmp-timepicker-selector-range-heading-color\)\] {
|
|
4471
4444
|
color: var(--cmp-timepicker-selector-range-heading-color);
|
|
4472
4445
|
}
|
|
@@ -4682,6 +4655,9 @@
|
|
|
4682
4655
|
outline-style: var(--tw-outline-style);
|
|
4683
4656
|
outline-width: 2px;
|
|
4684
4657
|
}
|
|
4658
|
+
.outline-offset-\[-2px\] {
|
|
4659
|
+
outline-offset: -2px;
|
|
4660
|
+
}
|
|
4685
4661
|
.outline-\(--sys-color-border-default\) {
|
|
4686
4662
|
outline-color: var(--sys-color-border-default);
|
|
4687
4663
|
}
|
|
@@ -5800,11 +5776,6 @@
|
|
|
5800
5776
|
scrollbar-color: var(--color-grey-45) var(--color-white);
|
|
5801
5777
|
}
|
|
5802
5778
|
}
|
|
5803
|
-
.motion-safe\:animate-table-row-in {
|
|
5804
|
-
@media (prefers-reduced-motion: no-preference) {
|
|
5805
|
-
animation: var(--animate-table-row-in);
|
|
5806
|
-
}
|
|
5807
|
-
}
|
|
5808
5779
|
.motion-reduce\:hidden {
|
|
5809
5780
|
@media (prefers-reduced-motion: reduce) {
|
|
5810
5781
|
display: none;
|
package/dist/theme.css
CHANGED
|
@@ -2470,12 +2470,6 @@
|
|
|
2470
2470
|
--animate-floating-out-left: floating-out-left 0.15s ease-in forwards;
|
|
2471
2471
|
--animate-floating-out-right: floating-out-right 0.15s ease-in forwards;
|
|
2472
2472
|
|
|
2473
|
-
/* 표 행 펼침 (STreeTable) — 여러 행을 연달아 열고 닫는 반복 조작이라 툴팁(0.35s)·STree(0.2s)보다 짧게 둔다.
|
|
2474
|
-
0.1s 아래로 내리면 움직임이 보이지 않아 깜빡임처럼 읽힌다.
|
|
2475
|
-
모양은 아래로 열리는 floating 과 같다 (위에서 4px 내려오며 나타남) */
|
|
2476
|
-
--animate-table-row-in: floating-in-bottom 0.15s ease-out;
|
|
2477
|
-
/* 접을 때는 모션 없이 바로 뺀다. 사라지는 모션을 넣으면 그 동안 아래 행이 기다렸다 올라와 굼떠 보인다 */
|
|
2478
|
-
|
|
2479
2473
|
/* 스켈레톤 (Quasar QSkeleton 포팅). 속도는 SSkeleton 이 animation-duration 으로 덮어쓴다 */
|
|
2480
2474
|
--animate-skeleton-wave: skeleton-wave 1.5s linear 0.5s infinite;
|
|
2481
2475
|
--animate-skeleton-pulse: skeleton-pulse 1.5s ease-in-out 0.5s infinite;
|