sellmate-design-system-react 9.0.0-beta.45 → 9.0.0-beta.46

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
@@ -27,10 +27,10 @@
27
27
  | 분류 | 컴포넌트 |
28
28
  | --- | --- |
29
29
  | **버튼·링크** | `SButton` `SGhostButton` `SDropdownButton` `STextLink` `SSwitch` `SToggle` |
30
- | **입력 (폼)** | `SForm` `SField` `SInput` `SSearchInput` `SNumberInput` `STextarea` `SEditor` `SSelect` `SCheckbox` `SRadio` `SRadioGroup` `SRadioButton` `SRadioCard` `SRadioCardGroup` `SChip` `SChipInput` `SBarcodeInput` `SFilePicker` |
30
+ | **입력 (폼)** | `SForm` `SField` `SInput` `SChatInput`(대화 입력창) `SSearchInput` `SNumberInput` `STextarea` `SEditor` `SSelect` `SCheckbox` `SRadio` `SRadioGroup` `SRadioButton` `SRadioCard` `SRadioCardGroup` `SChip` `SChipInput` `SBarcodeInput` `SFilePicker` |
31
31
  | **날짜·시간** | `SCalendar` `SCalendarBoard`(한 달치 일정 판) `SDatePicker` `SDatePickerYearListbox` `SDatePickerMonthListbox` `SDateRangePicker` `STimePicker` `STimeRangePicker` |
32
- | **표·목록** | `STable` `STableBar` `SChipFilter` `SKeyValueTable` `SList` `SListItem` `SExpansionList` `SDraggableList` `SDraggableItem` `STree` |
33
- | **레이아웃** | `SLayout` `SGnb` `SGnbSystem` `SSystemActionButton`(GNB system 패널에 한 줄씩 쌓는 액션 행 — 버튼 고르기는 §3-5) `SAccountListBox`(계정 행을 눌러 뜨는 계정 패널) `SPage` `SPageHeader`(페이지 제목 영역 — `SLayout` 안에서 `SPage` 앞에 둔다) `SSectionHeaderCard` `SCard` `SDivider` `SSplitter` `SScrollArea` `SExpansionItem` |
32
+ | **표·목록** | `SChatMessage`(대화의 메시지 한 건) `STable` `STableBar` `SChipFilter` `SKeyValueTable` `SList` `SListItem` `SExpansionList` `SDraggableList` `SDraggableItem` `STree` |
33
+ | **레이아웃** | `SLayout` `SGnb` `SGnbSystem` `SSystemActionButton`(GNB system 패널에 한 줄씩 쌓는 액션 행 — 버튼 고르기는 §3-5) `SAccountListBox`(계정 행을 눌러 뜨는 계정 패널) `SPage` `SPageHeader`(페이지 제목 영역 — `SLayout` 안에서 `SPage` 앞에 둔다) `SSectionHeaderCard` `SCard` `SLoginCard`(통합 계정 로그인 화면의 카드) `SDivider` `SSplitter` `SScrollArea` `SExpansionItem` |
34
34
  | **내비게이션** | `STabs` `SPagination` `SStepper` |
35
35
  | **표시·상태** | `STag` `SBadge` `SIcon` `SImage` `SCallout` `SGuide` |
36
36
  | **진행·로딩** | `SLinearProgress` `SCircleProgress` `SLoadingContainer` `SLoadingModal` |
@@ -138,6 +138,8 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
138
138
 
139
139
  **`toLocaleDateString()` 은 쓰지 않는다** — 로케일에 따라 결과가 바뀌어 표기를 지킬 수 없다.
140
140
 
141
+ **대화 화면의 시각은 예외다.** `SChatMessage` 의 `time` 처럼 말풍선에 붙는 시각은 `오후 5:52` 로 쓴다. 여기서 시각은 정렬하거나 크기를 비교하는 값이 아니라 대화의 흐름을 짚는 표시라, 자릿수를 맞춘 `17:52` 보다 말하듯 읽히는 표기가 맞다. **이 예외는 대화 말풍선에만 열린다** — 같은 화면이라도 표·상세·목록에 들어가는 일시는 그대로 `YYYY-MM-DD HH:mm` 이다.
142
+
141
143
  ---
142
144
 
143
145
  ## 2. 조합 규칙 — 화면을 어떻게 쌓는가
@@ -151,7 +153,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
151
153
  | 층 | 무엇인가 | 컴포넌트 |
152
154
  | --- | --- | --- |
153
155
  | **셸** | 앱 전체 뼈대. 페이지가 바뀌어도 남는다 | `SLayout` `SGnb` `SGnbSystem`(GNB 맨 아래 판 · 전폭 상단바 오른쪽 끝) `SPage` `SPageHeader`(`SLayout` 안에서 `SPage` 앞에 둔다) |
154
- | **블록** | `SPage` 의 직계 자식. 페이지를 세로로 쌓는 단위 | `SSectionHeaderCard` `SCard` `SForm` `SSplitter` `SScrollArea` `SCalendarBoard` `STable` `STableBar` `SChipFilter` `SKeyValueTable` `SList` `SExpansionList` `SDraggableList` `STree` `SCallout` `STabs` `SStepper` `SPagination` `SDivider` |
156
+ | **블록** | `SPage` 의 직계 자식. 페이지를 세로로 쌓는 단위 | `SSectionHeaderCard` `SCard` `SChatMessage` `SChatInput` `SLoginCard`(유일하게 `SPage` 밖에 선다 — 로그인 화면 자체가 자기 자리다, §4-6) `SForm` `SSplitter` `SScrollArea` `SCalendarBoard` `STable` `STableBar` `SChipFilter` `SKeyValueTable` `SList` `SExpansionList` `SDraggableList` `STree` `SCallout` `STabs` `SStepper` `SPagination` `SDivider` |
155
157
  | **요소** | 블록 **안에** 놓이는 컨트롤. 혼자 페이지에 서지 않는다 | `SButton` `SGhostButton` `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` `SImage` `SLinearProgress` `SCircleProgress` |
156
158
  | **인라인** | 텍스트 흐름·셀·라벨 안에 섞인다. 혼자 블록이 되지 않는다 | `STag` `SBadge` `SIcon` `STextLink` `SChip` |
157
159
  | **레이어** | 문서 흐름 **밖**에 떠서 그려진다. 어느 층에서 띄우든 레이아웃에 영향이 없다 | `SModal` `SActionModal` `SConfirmModal` `SPopup` `SDrawer` `SPopover` `STooltip` `SPortal` `SAccountListBox`(계정 행에 붙어 뜬다 — 직접 띄우지 않는다) `SToast` `SLoadingModal` `SLoadingContainer` `SGuide` |
@@ -474,6 +476,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
474
476
  | --- | --- | --- |
475
477
  | 한 줄 텍스트를 받는다 | `SInput` | §3-7-1 |
476
478
  | 여러 줄 텍스트를 받는다 | `STextarea` | §3-7-1 |
479
+ | 대화 화면 아래에서 메시지를 입력받는다 | `SChatInput` | |
477
480
  | 제목·굵게·목록·색 같은 **서식이 남아야 하는** 글을 받는다 | `SEditor` | §3-7-1 |
478
481
  | 목록·결과를 검색어로 좁힌다 | `SSearchInput` | §3-7-1 |
479
482
  | 숫자(수량·금액)를 받는다 | `SNumberInput` | §3-7-13 |
@@ -509,6 +512,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
509
512
  | 여러 건을 여러 열로 보여주고 열끼리 비교하게 한다 | `STable` | §3-7-6 |
510
513
  | 항목 하나의 속성들을 `라벨: 값` 으로 보여준다 | `SKeyValueTable` | §4-4 |
511
514
  | 한 줄로 읽히는 항목을 세로로 나열한다 | `SList` + `SListItem` | §3-7-6 |
515
+ | 대화의 메시지 한 건을 보여준다 | `SChatMessage` | |
512
516
  | 나열한 항목을 펼쳐 하위 내용을 보여준다 | `SExpansionList` + `SExpansionItem` | §3-7-7 |
513
517
  | 부모-자식 계층을 들여쓰기로 보여준다 | `STree` | §3-7-7 |
514
518
  | 사용자가 순서를 드래그로 바꾸게 한다 | `SDraggableList` + `SDraggableItem` | §3-7-6 |
@@ -542,6 +546,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
542
546
  | 페이지 제목(+ 서브 텍스트·뒤로가기·우측 슬롯)을 만든다 | `SPageHeader` (`SLayout` 안에서 `SPage` 앞에 둔다) | §4-1 |
543
547
  | 제목 있는 섹션으로 묶는다 | `SSectionHeaderCard` | §3-7-8 |
544
548
  | 제목 없이 흰 면으로만 묶는다 | `SCard` | §3-7-8 |
549
+ | 통합 계정 로그인 화면을 만든다 | `SLoginCard` | §4-6 |
545
550
  | 가로선으로 끊는다 | `SDivider` | §3-6 |
546
551
  | 사용자가 영역 크기를 조절하게 한다 | `SSplitter` | §3-6 |
547
552
  | 특정 영역 안에서만 스크롤시킨다 | `SScrollArea` | |
@@ -2226,6 +2231,45 @@ export default function ProductDetailPage() {
2226
2231
 
2227
2232
  본문 래퍼는 `className` 을 받지 않으므로(여백은 `padding` prop 으로만 받는다) 루트에서 내려 준다. **하단 액션 영역이 있으면 본문이 더 이상 마지막 자식이 아니다** — 그때는 `[&>div:nth-last-child(2)]` 로 겨눈다. 흔한 구성은 아니다 — 대부분은 `STable` 이 자기 안에서 스크롤하므로 여기까지 갈 일이 없다.
2228
2233
 
2234
+ ### 4-6. 로그인 화면 — SLoginCard
2235
+
2236
+ 앱 셸이 아직 없는 유일한 화면이다. `SLayout`·`SGnb`·`SPage` 가 없고, 회색 바탕 위에 카드 하나만 선다.
2237
+
2238
+ ```tsx
2239
+ <div className="flex h-screen items-stretch justify-center bg-(--sys-color-bg-neutralLight) p-sd-24">
2240
+ <SLoginCard
2241
+ product="AI assistant"
2242
+ title="셀메이트 AI 어시스턴트와 함께 자료 찾는 시간은 줄이고, 업무는 빠르게!"
2243
+ description="(사내 답변은 사내 문서를 기반으로 답변합니다.)"
2244
+ onLogin={signIn}
2245
+ footerNote="계정이 없다면 담당자에게 문의를 요청해 주세요."
2246
+ />
2247
+ </div>
2248
+ ```
2249
+
2250
+ **바깥 한 겹은 앱이 만든다.** 카드는 화면을 잡지 않는다 — 덕분에 로그인 화면 밖(세션 만료 재인증 등)에서도 같은 카드를 그대로 쓴다. 그 한 겹은 위 세 가지만 하면 되고, 그 이상은 하지 않는다.
2251
+
2252
+ - 바탕은 `bg-(--sys-color-bg-neutralLight)`, 여백은 `p-sd-24`. **여백은 카드가 최소 폭(368px)을 지킬 수 있는 값으로 둔다** — 좌우 합이 창 폭에서 368px 을 남기지 못하면 카드가 넘친다
2253
+ - `items-stretch` — 카드가 남은 높이를 채운다. `items-center` 로 두면 카드가 내용 높이로 줄어든다
2254
+ - `justify-center` — 카드를 가로 가운데에
2255
+
2256
+ | Prop | 용도 |
2257
+ | --- | --- |
2258
+ | `product` | 로고 아래 제품명 (`AI assistant` · `account` · `chat`) |
2259
+ | `title` | 제품을 한 줄로 소개하는 문장 |
2260
+ | `description` | 소개 바로 아래 붙는 보조 설명. 소개와 한 문단으로 읽힌다 — 없으면 생략한다 |
2261
+ | `onLogin` | 로그인 버튼을 눌렀을 때. **버튼은 카드가 그리므로 동작만 넘긴다** — 문구·색·크기는 세 제품에서 같아야 한다 |
2262
+ | `footerNote` | 버튼 아래 안내 문구 |
2263
+
2264
+ **문구 말고는 주지 않는다.** 라운드·간격·타이포·버튼 색은 카드가 쥐고 있어 제품이 달라도 같은 화면이 선다.
2265
+
2266
+ **크기는 부모가 정한다 — `className` 으로 폭을 박지 않는다.** 카드는 폭을 368~640px 사이에서 부모에 맞추고 높이도 부모가 준 만큼 채운다. 밖에서 고정하면 상하한이 무의미해져, 좁은 화면에서 함께 좁아져야 할 카드가 그대로 남아 넘친다. 좁게 세워야 하면 카드가 아니라 바깥 한 겹을 좁힌다.
2267
+
2268
+ **Sellmate 워드마크와 로그인 버튼은 카드가 직접 그린다 — prop 이 없다.** 세 제품이 같은 로고·같은 버튼 문구(`통합 계정으로 로그인`)로 서므로 넘길 것도, 갈아 끼울 것도 없다. 앱이 주는 것은 문구와 `onLogin` 동작뿐이다.
2269
+
2270
+ **로그인 카드 옆에 다른 블록을 두지 않는다.** 이 화면에서 할 일은 로그인 하나이고, 옆에 공지·배너가 붙는 순간 그 뜻이 깨진다. 안내가 필요하면 `description` 이나 `footerNote` 로 넣는다.
2271
+
2272
+
2229
2273
  ---
2230
2274
 
2231
2275
  ## 5. 자가 점검 체크리스트
@@ -60,6 +60,7 @@ export const BUTTON_SIZES = ['xs', 'sm', 'md', 'lg'] as const;
60
60
  - [SFooter](../SFooter)
61
61
  - [SKeyValueTable](../SKeyValueTable)
62
62
  - [SLoadingModal](../SLoadingModal)
63
+ - [SLoginCard](../SLoginCard)
63
64
  - [SPopover](../SPopover)
64
65
  - [SToast](../SToast)
65
66
 
@@ -79,6 +80,7 @@ graph TD;
79
80
  SFooter --> SButton
80
81
  SKeyValueTable --> SButton
81
82
  SLoadingModal --> SButton
83
+ SLoginCard --> SButton
82
84
  SPopover --> SButton
83
85
  SToast --> SButton
84
86
  style SButton fill:#f9f,stroke:#333,stroke-width:4px
@@ -0,0 +1,44 @@
1
+ # SChatInput
2
+
3
+ > 자동 생성 문서 — `npm run docs:gen`. 소스: 각 컴포넌트의 Props/Handle 인터페이스 + import 의존성.
4
+
5
+ ### SChatInput
6
+
7
+ #### Props
8
+
9
+ | Prop | Type | Default | Description |
10
+ |------|------|---------|-------------|
11
+ | `value?` | `string` | `''` | 입력값 (제어) |
12
+ | `placeholder?` | `string` | `DEFAULT_PLACEHOLDER` | 안내 문구. 세 제품이 같은 말로 서도록 기본값을 쥐고 있다 |
13
+ | `disabled?` | `boolean` | `false` | 비활성 |
14
+ | `busy?` | `boolean` | `false` | 응답을 기다리는 중인지. 켜면 전송 버튼 자리에 정지 버튼이 서고 Enter 전송이 잠긴다 — 답이 오는 동안 또 보내지 못하게 막는 자리다. |
15
+ | `footerAction?` | `ReactNode` | — | 입력창 아래 보조 액션 슬롯. 없으면 그 줄이 통째로 빠진다. 상담 종료처럼 **대화 자체를 끝내는, 전송과 층이 다른 동작**이 오는 자리다. 무엇을 놓을지는 앱이 정하고, 입력창은 가운데에 세우는 것까지만 한다 — `STextLink` 를 넣는 것이 보통이다. |
16
+ | `maxRows?` | `number` | `DEFAULT_MAX_ROWS` | 입력창이 자랄 수 있는 최대 줄 수. 여기 닿으면 더 자라지 않고 그 안에서 스크롤한다. |
17
+ | `className?` | `string` | — | 바깥 컨테이너 className (안쪽 textarea 가 아니다) |
18
+ | `style?` | `CSSProperties` | — | 바깥 컨테이너 style |
19
+ | `textareaClassName?` | `string` | — | 안쪽 textarea className |
20
+
21
+ #### Events
22
+
23
+ | Event | Type | Description |
24
+ |-------|------|-------------|
25
+ | `onValueChange` | `(value: string) => void` | 값 변경 |
26
+ | `onSend` | `(value: string) => void` | 전송. 전송 버튼을 누르거나 Enter 를 쳤을 때 현재 값을 넘긴다. **값이 비면 전송 버튼이 잠긴다** — 빈 메시지는 보낼 수 없다. |
27
+ | `onAttach` | `() => void` | 파일 첨부. **주면 입력창 왼쪽에 첨부 버튼이 선다** |
28
+ | `onStop` | `() => void` | 정지. `busy` 일 때 정지 버튼을 누르면 불린다 |
29
+
30
+ ## Dependencies
31
+
32
+ ### Depends on
33
+
34
+ - [SGhostButton](../SGhostButton)
35
+ - [STextarea](../STextarea)
36
+
37
+ ### Graph
38
+
39
+ ```mermaid
40
+ graph TD;
41
+ SChatInput --> SGhostButton
42
+ SChatInput --> STextarea
43
+ style SChatInput fill:#f9f,stroke:#333,stroke-width:4px
44
+ ```
@@ -0,0 +1,60 @@
1
+ import { type CSSProperties, type ReactNode, type TextareaHTMLAttributes } from 'react';
2
+ /**
3
+ * 안쪽 textarea 로 그대로 흘러가는 네이티브 속성 — `SInput`·`STextarea` 와 같은 방식이다.
4
+ * 컴포넌트가 직접 다루는 것만 뺀다.
5
+ */
6
+ export interface SChatInputProps extends Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'value' | 'onChange' | 'placeholder' | 'disabled' | 'rows' | 'className' | 'style'> {
7
+ /** 입력값 (제어) */
8
+ value?: string;
9
+ /** 값 변경 */
10
+ onValueChange?: (value: string) => void;
11
+ /** 안내 문구. 세 제품이 같은 말로 서도록 기본값을 쥐고 있다 */
12
+ placeholder?: string;
13
+ /** 비활성 */
14
+ disabled?: boolean;
15
+ /**
16
+ * 전송. 전송 버튼을 누르거나 Enter 를 쳤을 때 현재 값을 넘긴다.
17
+ *
18
+ * **값이 비면 전송 버튼이 잠긴다** — 빈 메시지는 보낼 수 없다.
19
+ */
20
+ onSend?: (value: string) => void;
21
+ /** 파일 첨부. **주면 입력창 왼쪽에 첨부 버튼이 선다** */
22
+ onAttach?: () => void;
23
+ /**
24
+ * 응답을 기다리는 중인지. 켜면 전송 버튼 자리에 정지 버튼이 서고 Enter 전송이 잠긴다 —
25
+ * 답이 오는 동안 또 보내지 못하게 막는 자리다.
26
+ */
27
+ busy?: boolean;
28
+ /** 정지. `busy` 일 때 정지 버튼을 누르면 불린다 */
29
+ onStop?: () => void;
30
+ /**
31
+ * 입력창 아래 보조 액션 슬롯. 없으면 그 줄이 통째로 빠진다.
32
+ *
33
+ * 상담 종료처럼 **대화 자체를 끝내는, 전송과 층이 다른 동작**이 오는 자리다. 무엇을 놓을지는
34
+ * 앱이 정하고, 입력창은 가운데에 세우는 것까지만 한다 — `STextLink` 를 넣는 것이 보통이다.
35
+ */
36
+ footerAction?: ReactNode;
37
+ /**
38
+ * 입력창이 자랄 수 있는 최대 줄 수. 여기 닿으면 더 자라지 않고 그 안에서 스크롤한다.
39
+ */
40
+ maxRows?: number;
41
+ /** 바깥 컨테이너 className (안쪽 textarea 가 아니다) */
42
+ className?: string;
43
+ /** 바깥 컨테이너 style */
44
+ style?: CSSProperties;
45
+ /** 안쪽 textarea className */
46
+ textareaClassName?: string;
47
+ }
48
+ /**
49
+ * SChatInput — 대화 화면 맨 아래에 서는 입력창.
50
+ *
51
+ * 첨부·입력·전송을 한 줄로 세우고, 필요하면 그 아래 보조 액션(상담 종료 등)을 붙인다.
52
+ * Enter 로 보내고 Shift+Enter 로 줄을 바꾼다 — 한글 조합 중의 Enter 는 보내지 않는다.
53
+ *
54
+ * **글이 길어지면 입력창이 자란다** — `maxRows` 줄까지 늘어나고, 거기 닿으면 그 안에서
55
+ * 스크롤한다. 상한이 없으면 대화가 화면 밖으로 밀려난다.
56
+ *
57
+ * **응답을 기다리는 동안에는 `busy` 를 켠다** — 전송 버튼이 정지 버튼으로 바뀌고 Enter 전송이
58
+ * 잠겨, 답이 오는 중에 또 보내는 일이 막힌다.
59
+ */
60
+ export declare const SChatInput: import("react").ForwardRefExoticComponent<SChatInputProps & import("react").RefAttributes<HTMLTextAreaElement>>;
@@ -0,0 +1 @@
1
+ export { SChatInput, type SChatInputProps } from './SChatInput';
@@ -0,0 +1,81 @@
1
+ # SChatMessage
2
+
3
+ > 자동 생성 문서 — `npm run docs:gen`. 소스: 각 컴포넌트의 Props/Handle 인터페이스 + import 의존성.
4
+
5
+ ### SChatMessage
6
+
7
+ #### Props
8
+
9
+ | Prop | Type | Default | Description |
10
+ |------|------|---------|-------------|
11
+ | `direction` | `SChatMessageDirection` | `'incoming'` | 받은 메시지(왼쪽)인지 보낸 메시지(오른쪽)인지. **아바타와 이름 유무도 여기서 갈린다** — 받은 메시지에만 아바타와 `sender` 가 선다. 보낸 메시지는 내가 보냈다는 것이 자리로 이미 드러나 얼굴도 이름도 필요 없다. |
12
+ | `sender?` | `ReactNode` | — | 말풍선 위에 붙는 보낸 사람 이름. 없으면 자리째 빠진다. **받은 메시지에만 쓰인다** — 보낸 메시지에는 넘겨도 그리지 않는다(`direction` 참고). 앱이 데이터를 한 번만 매핑해 방향과 무관하게 넘길 수 있도록 막지 않고 무시한다. |
13
+ | `time?` | `ReactNode` | — | 말풍선 아래에 붙는 시각. 표기는 앱이 정해 문자열로 넘긴다 — 대화 화면의 시각은 `오후 5:52` 처럼 읽는 표기를 쓰므로 카드가 서식을 강제하지 않는다 (AGENTS.md §1-4 예외). |
14
+ | `readStatus?` | `SChatMessageReadStatus` | — | 상대가 읽었는지. 시각 왼쪽에 `읽음`·`안 읽음` 으로 붙고, 주지 않으면 표시하지 않는다. **내가 보낸 메시지에만 붙인다** — 받은 메시지에 읽음 표시를 다는 것은 뜻이 없다. |
15
+ | `avatarSrc?` | `string` | — | 아바타 이미지 경로. 없으면 사람 아이콘 자리표시가 선다. **받은 메시지에만 쓰인다** — 보낸 메시지에는 아바타가 서지 않는다(`direction` 참고). |
16
+ | `avatarAlt?` | `string` | — | 아바타 대체 텍스트 |
17
+ | `continued?` | `boolean` | `false` | 같은 사람이 이어서 말한 묶음인지. 켜면 **아바타와 `sender` 를 그리지 않는다** — 말풍선이 위 묶음과 같은 선에서 시작해 한 사람의 말로 이어 읽힌다. 받은 메시지에서는 아바타 자리를 비워 둔 채 남긴다 — 안 남기면 말풍선이 44px 왼쪽으로 밀려 위 묶음과 어긋난다. 보낸 메시지는 아바타가 없으므로 이름만 숨긴다. `time`·`readStatus` 는 그대로 나온다. 시간이 벌어져 묶음을 나눴어도 얼굴은 한 번만 보이면 되고 시각은 묶음마다 필요하기 때문이다. **얼마나 벌어져야 묶음을 나눌지는 앱이 정한다** — 30분이든 하루든 대화의 성격에 달렸으므로 컴포넌트는 기준을 갖지 않고 스위치만 연다. |
18
+ | `variant?` | `SChatMessageVariant` | `'default'` | 말풍선 종류. `template` 이면 말풍선이 옅은 파랑 면으로 서서 미리 등록된 서식으로 보내진 메시지임을 드러낸다. **`direction` 과 별개 축이라** 받은 메시지에도 보낸 메시지에도 붙는다. |
19
+ | `actions?` | `ReactNode` | — | 시각 아래에 붙는 동작 슬롯. 없으면 그 줄이 통째로 빠진다. 「상담원 연결」처럼 **이 메시지에 이어서 할 수 있는 동작**이 오는 자리다. 무엇을 놓을지는 앱이 정하고, 메시지는 줄로 세우고 방향에 맞춰 정렬하는 것까지만 한다. 시각과 같은 선에서 시작한다 — 말풍선 끝에서 한 칸 안이다. |
20
+ | `messages?` | `ReactNode[]` | — | 말풍선 목록. **한 항목이 말풍선 하나**가 되어 위에서 아래로 쌓인다. 한 사람이 연달아 말하면 아바타·이름·시각은 한 번만 서고 말풍선만 늘어난다 — 그 묶음이 이 컴포넌트 한 개다. 말풍선 안에는 글뿐 아니라 버튼 같은 요소도 올 수 있다. |
21
+
22
+ ## Types
23
+
24
+ ### SChatMessageDirection
25
+
26
+ ```ts
27
+ export type SChatMessageDirection = (typeof CHAT_MESSAGE_DIRECTIONS)[number];
28
+ ```
29
+
30
+ ### SChatMessageReadStatus
31
+
32
+ ```ts
33
+ export type SChatMessageReadStatus = (typeof CHAT_MESSAGE_READ_STATUSES)[number];
34
+ ```
35
+
36
+ ### SChatMessageVariant
37
+
38
+ ```ts
39
+ export type SChatMessageVariant = (typeof CHAT_MESSAGE_VARIANTS)[number];
40
+ ```
41
+
42
+ ### CHAT_MESSAGE_DIRECTIONS
43
+
44
+ ```ts
45
+ /** 메시지가 오는 쪽 — 받은 메시지(incoming)는 왼쪽, 보낸 메시지(outgoing)는 오른쪽에 선다. */
46
+ export const CHAT_MESSAGE_DIRECTIONS = ['incoming', 'outgoing'] as const;
47
+ ```
48
+
49
+ ### CHAT_MESSAGE_READ_STATUSES
50
+
51
+ ```ts
52
+ /** 상대가 내 메시지를 읽었는지. 시각 왼쪽에 붙는다. */
53
+ export const CHAT_MESSAGE_READ_STATUSES = ['read', 'unread'] as const;
54
+ ```
55
+
56
+ ### CHAT_MESSAGE_VARIANTS
57
+
58
+ ```ts
59
+ /**
60
+ * 말풍선의 종류. `template` 은 미리 등록된 서식으로 보내진 메시지다.
61
+ *
62
+ * **`direction` 과 별개 축이다** — 받은 메시지도 보낸 메시지도 템플릿일 수 있다.
63
+ */
64
+ export const CHAT_MESSAGE_VARIANTS = ['default', 'template'] as const;
65
+ ```
66
+
67
+ ## Dependencies
68
+
69
+ ### Depends on
70
+
71
+ - [SIcon](../SIcon)
72
+ - [SImage](../SImage)
73
+
74
+ ### Graph
75
+
76
+ ```mermaid
77
+ graph TD;
78
+ SChatMessage --> SIcon
79
+ SChatMessage --> SImage
80
+ style SChatMessage fill:#f9f,stroke:#333,stroke-width:4px
81
+ ```
@@ -0,0 +1,102 @@
1
+ import { type HTMLAttributes, type ReactNode } from 'react';
2
+ /** 메시지가 오는 쪽 — 받은 메시지(incoming)는 왼쪽, 보낸 메시지(outgoing)는 오른쪽에 선다. */
3
+ export declare const CHAT_MESSAGE_DIRECTIONS: readonly ["incoming", "outgoing"];
4
+ export type SChatMessageDirection = (typeof CHAT_MESSAGE_DIRECTIONS)[number];
5
+ /**
6
+ * 말풍선의 종류. `template` 은 미리 등록된 서식으로 보내진 메시지다.
7
+ *
8
+ * **`direction` 과 별개 축이다** — 받은 메시지도 보낸 메시지도 템플릿일 수 있다.
9
+ */
10
+ export declare const CHAT_MESSAGE_VARIANTS: readonly ["default", "template"];
11
+ export type SChatMessageVariant = (typeof CHAT_MESSAGE_VARIANTS)[number];
12
+ /** 상대가 내 메시지를 읽었는지. 시각 왼쪽에 붙는다. */
13
+ export declare const CHAT_MESSAGE_READ_STATUSES: readonly ["read", "unread"];
14
+ export type SChatMessageReadStatus = (typeof CHAT_MESSAGE_READ_STATUSES)[number];
15
+ export interface SChatMessageProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
16
+ /**
17
+ * 받은 메시지(왼쪽)인지 보낸 메시지(오른쪽)인지.
18
+ *
19
+ * **아바타와 이름 유무도 여기서 갈린다** — 받은 메시지에만 아바타와 `sender` 가 선다.
20
+ * 보낸 메시지는 내가 보냈다는 것이 자리로 이미 드러나 얼굴도 이름도 필요 없다.
21
+ */
22
+ direction: SChatMessageDirection;
23
+ /**
24
+ * 말풍선 위에 붙는 보낸 사람 이름. 없으면 자리째 빠진다.
25
+ *
26
+ * **받은 메시지에만 쓰인다** — 보낸 메시지에는 넘겨도 그리지 않는다(`direction` 참고).
27
+ * 앱이 데이터를 한 번만 매핑해 방향과 무관하게 넘길 수 있도록 막지 않고 무시한다.
28
+ */
29
+ sender?: ReactNode;
30
+ /**
31
+ * 말풍선 아래에 붙는 시각. 표기는 앱이 정해 문자열로 넘긴다 — 대화 화면의 시각은
32
+ * `오후 5:52` 처럼 읽는 표기를 쓰므로 카드가 서식을 강제하지 않는다 (AGENTS.md §1-4 예외).
33
+ */
34
+ time?: ReactNode;
35
+ /**
36
+ * 상대가 읽었는지. 시각 왼쪽에 `읽음`·`안 읽음` 으로 붙고, 주지 않으면 표시하지 않는다.
37
+ *
38
+ * **내가 보낸 메시지에만 붙인다** — 받은 메시지에 읽음 표시를 다는 것은 뜻이 없다.
39
+ */
40
+ readStatus?: SChatMessageReadStatus;
41
+ /**
42
+ * 아바타 이미지 경로. 없으면 사람 아이콘 자리표시가 선다.
43
+ *
44
+ * **받은 메시지에만 쓰인다** — 보낸 메시지에는 아바타가 서지 않는다(`direction` 참고).
45
+ */
46
+ avatarSrc?: string;
47
+ /** 아바타 대체 텍스트 */
48
+ avatarAlt?: string;
49
+ /**
50
+ * 같은 사람이 이어서 말한 묶음인지. 켜면 **아바타와 `sender` 를 그리지 않는다** —
51
+ * 말풍선이 위 묶음과 같은 선에서 시작해 한 사람의 말로 이어 읽힌다.
52
+ *
53
+ * 받은 메시지에서는 아바타 자리를 비워 둔 채 남긴다 — 안 남기면 말풍선이 44px 왼쪽으로 밀려
54
+ * 위 묶음과 어긋난다. 보낸 메시지는 아바타가 없으므로 이름만 숨긴다.
55
+ *
56
+ * `time`·`readStatus` 는 그대로 나온다. 시간이 벌어져 묶음을 나눴어도 얼굴은 한 번만 보이면
57
+ * 되고 시각은 묶음마다 필요하기 때문이다.
58
+ *
59
+ * **얼마나 벌어져야 묶음을 나눌지는 앱이 정한다** — 30분이든 하루든 대화의 성격에 달렸으므로
60
+ * 컴포넌트는 기준을 갖지 않고 스위치만 연다.
61
+ */
62
+ continued?: boolean;
63
+ /**
64
+ * 말풍선 종류. `template` 이면 말풍선이 옅은 파랑 면으로 서서 미리 등록된 서식으로 보내진
65
+ * 메시지임을 드러낸다. **`direction` 과 별개 축이라** 받은 메시지에도 보낸 메시지에도 붙는다.
66
+ */
67
+ variant?: SChatMessageVariant;
68
+ /**
69
+ * 시각 아래에 붙는 동작 슬롯. 없으면 그 줄이 통째로 빠진다.
70
+ *
71
+ * 「상담원 연결」처럼 **이 메시지에 이어서 할 수 있는 동작**이 오는 자리다. 무엇을 놓을지는
72
+ * 앱이 정하고, 메시지는 줄로 세우고 방향에 맞춰 정렬하는 것까지만 한다.
73
+ *
74
+ * 시각과 같은 선에서 시작한다 — 말풍선 끝에서 한 칸 안이다.
75
+ */
76
+ actions?: ReactNode;
77
+ /**
78
+ * 말풍선 목록. **한 항목이 말풍선 하나**가 되어 위에서 아래로 쌓인다.
79
+ *
80
+ * 한 사람이 연달아 말하면 아바타·이름·시각은 한 번만 서고 말풍선만 늘어난다 —
81
+ * 그 묶음이 이 컴포넌트 한 개다. 말풍선 안에는 글뿐 아니라 버튼 같은 요소도 올 수 있다.
82
+ */
83
+ messages?: ReactNode[];
84
+ }
85
+ /**
86
+ * SChatMessage — 대화 화면에서 한 사람이 한 번에 말한 묶음.
87
+ *
88
+ * 아바타·보낸 사람(받은 메시지만)·말풍선·시각을 한 줄로 세운다. **말풍선은 `messages` 항목 수만큼 쌓이고**
89
+ * 아바타·이름·시각은 그 묶음에 한 번만 붙는다 — 연달아 말한 여러 줄이 한 덩어리로 읽힌다.
90
+ *
91
+ * 시각 아래에 동작을 붙여야 하면 `actions` 슬롯에 넣는다 — 시각과 같은 선에서 시작해 방향에
92
+ * 맞춰 정렬된다.
93
+ *
94
+ * 시간이 벌어져 묶음을 나눠야 하는데 말한 사람은 같을 때는 `continued` 를 켠다 — 아바타 자리는
95
+ * 유지한 채 얼굴과 이름만 빠져, 두 묶음이 한 사람의 말로 이어 읽힌다.
96
+ `direction` 하나로 받은 메시지(왼쪽)와
97
+ * 보낸 메시지(오른쪽)가 갈리고, 그에 따라 정렬과 말풍선 색이 함께 뒤집힌다.
98
+ *
99
+ * **말풍선 폭은 내용이 정한다** — 짧은 메시지는 글자만큼만 차지하고, 길면 부모 폭에서 접힌다.
100
+ * 대화 폭을 제한하려면 이 컴포넌트가 아니라 담는 쪽을 좁힌다.
101
+ */
102
+ export declare const SChatMessage: import("react").ForwardRefExoticComponent<SChatMessageProps & import("react").RefAttributes<HTMLDivElement>>;
@@ -0,0 +1 @@
1
+ export { SChatMessage, CHAT_MESSAGE_DIRECTIONS, CHAT_MESSAGE_READ_STATUSES, CHAT_MESSAGE_VARIANTS, type SChatMessageDirection, type SChatMessageReadStatus, type SChatMessageVariant, type SChatMessageProps, } from './SChatMessage';
@@ -48,6 +48,7 @@ export type SGhostButtonIntent = 'default' | 'danger' | 'action' | 'subAction' |
48
48
  - [SBarcodeInput](../SBarcodeInput)
49
49
  - [SCalendar](../SCalendar)
50
50
  - [SCalendarBoard](../SCalendarBoard)
51
+ - [SChatInput](../SChatInput)
51
52
  - [SChip](../SChip)
52
53
  - [SDatePicker](../SDatePicker)
53
54
  - [SDateRangePicker](../SDateRangePicker)
@@ -81,6 +82,7 @@ graph TD;
81
82
  SBarcodeInput --> SGhostButton
82
83
  SCalendar --> SGhostButton
83
84
  SCalendarBoard --> SGhostButton
85
+ SChatInput --> SGhostButton
84
86
  SChip --> SGhostButton
85
87
  SDatePicker --> SGhostButton
86
88
  SDateRangePicker --> SGhostButton
@@ -23,6 +23,7 @@
23
23
  - [SAccountListBox](../SAccountListBox)
24
24
  - [SButton](../SButton)
25
25
  - [SCallout](../SCallout)
26
+ - [SChatMessage](../SChatMessage)
26
27
  - [SCheckbox](../SCheckbox)
27
28
  - [SChipFilter](../SChipFilter)
28
29
  - [SChipInput](../SChipInput)
@@ -66,6 +67,7 @@ graph TD;
66
67
  SAccountListBox --> SIcon
67
68
  SButton --> SIcon
68
69
  SCallout --> SIcon
70
+ SChatMessage --> SIcon
69
71
  SCheckbox --> SIcon
70
72
  SChipFilter --> SIcon
71
73
  SChipInput --> SIcon
@@ -88,6 +88,7 @@ export declare const ICONS: {
88
88
  readonly shield: (p: IconRenderProps) => import("react").JSX.Element;
89
89
  readonly sidebar: (p: IconRenderProps) => import("react").JSX.Element;
90
90
  readonly star: (p: IconRenderProps) => import("react").JSX.Element;
91
+ readonly stop: (p: IconRenderProps) => import("react").JSX.Element;
91
92
  readonly store: (p: IconRenderProps) => import("react").JSX.Element;
92
93
  readonly synchronize: (p: IconRenderProps) => import("react").JSX.Element;
93
94
  readonly thumbsDown: (p: IconRenderProps) => import("react").JSX.Element;
@@ -43,6 +43,10 @@ export const IMAGE_FITS = ['cover', 'contain', 'fill', 'none', 'scale-down'] as
43
43
 
44
44
  ## Dependencies
45
45
 
46
+ ### Used by
47
+
48
+ - [SChatMessage](../SChatMessage)
49
+
46
50
  ### Depends on
47
51
 
48
52
  - [SCircleProgress](../SCircleProgress)
@@ -54,5 +58,6 @@ export const IMAGE_FITS = ['cover', 'contain', 'fill', 'none', 'scale-down'] as
54
58
  graph TD;
55
59
  SImage --> SCircleProgress
56
60
  SImage --> SIcon
61
+ SChatMessage --> SImage
57
62
  style SImage fill:#f9f,stroke:#333,stroke-width:4px
58
63
  ```
@@ -0,0 +1,34 @@
1
+ # SLoginCard
2
+
3
+ > 자동 생성 문서 — `npm run docs:gen`. 소스: 각 컴포넌트의 Props/Handle 인터페이스 + import 의존성.
4
+
5
+ ### SLoginCard
6
+
7
+ #### Props
8
+
9
+ | Prop | Type | Default | Description |
10
+ |------|------|---------|-------------|
11
+ | `product` | `ReactNode` | — | 로고 아래 제품명 (예: `AI assistant` · `account` · `chat`) |
12
+ | `title` | `ReactNode` | — | 제품을 한 줄로 소개하는 문장 |
13
+ | `description?` | `ReactNode` | — | 소개 문장 바로 아래 붙는 보조 설명. 소개와 한 덩어리로 읽힌다 |
14
+ | `footerNote?` | `ReactNode` | — | 버튼 아래 안내 문구 (예: 계정 발급 문의 안내) |
15
+
16
+ #### Events
17
+
18
+ | Event | Type | Description |
19
+ |-------|------|-------------|
20
+ | `onLogin` | `() => void` | 로그인 버튼을 눌렀을 때. 버튼 자체는 카드가 그리므로 **동작만 넘긴다** — 문구·색·크기는 세 제품에서 같아야 해서 카드가 쥐고 있다. |
21
+
22
+ ## Dependencies
23
+
24
+ ### Depends on
25
+
26
+ - [SButton](../SButton)
27
+
28
+ ### Graph
29
+
30
+ ```mermaid
31
+ graph TD;
32
+ SLoginCard --> SButton
33
+ style SLoginCard fill:#f9f,stroke:#333,stroke-width:4px
34
+ ```
@@ -0,0 +1,28 @@
1
+ import { type HTMLAttributes, type ReactNode } from 'react';
2
+ export interface SLoginCardProps extends Omit<HTMLAttributes<HTMLDivElement>, 'title'> {
3
+ /** 로고 아래 제품명 (예: `AI assistant` · `account` · `chat`) */
4
+ product: ReactNode;
5
+ /** 제품을 한 줄로 소개하는 문장 */
6
+ title: ReactNode;
7
+ /** 소개 문장 바로 아래 붙는 보조 설명. 소개와 한 덩어리로 읽힌다 */
8
+ description?: ReactNode;
9
+ /**
10
+ * 로그인 버튼을 눌렀을 때. 버튼 자체는 카드가 그리므로 **동작만 넘긴다** —
11
+ * 문구·색·크기는 세 제품에서 같아야 해서 카드가 쥐고 있다.
12
+ */
13
+ onLogin?: () => void;
14
+ /** 버튼 아래 안내 문구 (예: 계정 발급 문의 안내) */
15
+ footerNote?: ReactNode;
16
+ }
17
+ /**
18
+ * SLoginCard — 통합 계정 로그인 화면의 카드.
19
+ *
20
+ * 로고·제품명·소개·로그인 버튼·안내 문구를 세로 가운데로 세운다. 제품마다 다른 것은 문구뿐이라
21
+ * 문구와 로그인 동작만 prop 으로 받고 로고·버튼·간격·타이포·색은 카드가 쥔다.
22
+ *
23
+ * **크기는 부모가 정한다** — 폭은 368~640px 사이에서 부모를 채우고, 높이도 부모가 준 만큼 채운다.
24
+ *
25
+ * **카드는 화면을 잡지 않는다** — 회색 바탕과 가운데 정렬은 앱이 만드는 한 겹이다 (AGENTS.md §4-6).
26
+ * 덕분에 로그인 화면 밖(세션 만료 재인증 등)에서도 같은 카드를 그대로 쓸 수 있다.
27
+ */
28
+ export declare const SLoginCard: import("react").ForwardRefExoticComponent<SLoginCardProps & import("react").RefAttributes<HTMLDivElement>>;
@@ -0,0 +1 @@
1
+ export { SLoginCard, type SLoginCardProps } from './SLoginCard';
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @internal
3
+ * Sellmate 워드마크 — 로그인 카드의 기본 로고.
4
+ *
5
+ * 색(`#00275F`)은 **브랜드 자산의 일부**라 토큰을 거치지 않는다. 대응하는 색 토큰이 없고
6
+ * (가장 가까운 `--color-primary-90` 도 `#002B5E` 로 다른 색이다), 있더라도 로고 색은
7
+ * 테마를 따라 바뀌면 안 되는 값이다. `scripts/check-hardcoded.mjs` 의 ALLOW 에 등록해 둔다.
8
+ *
9
+ * 원본 비율은 276×40 이다. 크기를 바꾸려면 `className` 으로 높이만 주고 폭은 비율에 맡긴다.
10
+ */
11
+ export declare function SellmateWordmark({ className }: {
12
+ className?: string;
13
+ }): import("react").JSX.Element;
@@ -47,6 +47,7 @@
47
47
 
48
48
  ### Used by
49
49
 
50
+ - [SChatInput](../SChatInput)
50
51
  - [SKeyValueTable](../SKeyValueTable)
51
52
 
52
53
  ### Depends on
@@ -58,6 +59,7 @@
58
59
  ```mermaid
59
60
  graph TD;
60
61
  STextarea --> SField
62
+ SChatInput --> STextarea
61
63
  SKeyValueTable --> STextarea
62
64
  style STextarea fill:#f9f,stroke:#333,stroke-width:4px
63
65
  ```