sellmate-design-system-react 6.1.0 → 7.1.0

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
@@ -261,7 +261,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
261
261
 
262
262
  | 스크롤 종류 | 어떻게 |
263
263
  | --- | --- |
264
- | **패널 자체 스크롤** (좌측 목록, 중앙 대화 등) | 그 패널 안쪽 하단에 `pb-sd-80` |
264
+ | **패널 자체 스크롤** (좌측 목록, 중앙 대화 등) | 그 패널 안쪽 하단에 `pb-[var(--cmp-pageBody-padding-scrollEnd)]` — `SPage` 와 같은 토큰이라 값이 바뀌어도 함께 따라간다 |
265
265
  | **페이지 단위 스크롤** | **`SPage` 가 넣는다. 직접 주지 않는다** |
266
266
 
267
267
  `SPage` 는 기본으로 넣으므로 **아무것도 하지 않으면 맞다.** 끄는 경우는 하나뿐이다 — **페이지네이션이 붙은 테이블.** 페이지네이션이 이미 "여기서 끝"을 알려주므로 `scrollEndSpacing={false}` 로 끈다 (§4-2 목록 페이지).
@@ -405,7 +405,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
405
405
 
406
406
  - 작은 창이 아니라 **화면 하나가 통째로** 들어간다 — 검색 필터·테이블·페이지네이션이 그대로 있는 목록(`엑셀 파일 관리`), 헤더 카드·탭이 있는 상세(`이동 오더 상세`).
407
407
  - 상세를 팝업으로 여는 이유는 **목록을 떠나지 않고 여러 건을 번갈아 보기** 위해서다. 단 상세가 **항상** 팝업인 것은 아니고, 데이터 양이 많을 때 팝업을 쓴다.
408
- - 구조는 헤더(제목 중앙) + 본문 + 푸터다. **확정할 작업이 있으면 푸터에 `저장`, 조회만이면 `useFooter={false}`** 로 푸터를 없앤다.
408
+ - 구조는 헤더(제목 중앙) + 본문이고 **푸터는 기본으로 없다**. 조회만 하는 팝업은 그대로 두고, **확정할 작업이 있을 때만 `useFooter` 로 푸터를 켜서 `submitButton` 에 `저장`** 을 둔다.
409
409
  - **본문 패딩은 `SPopup` 이 토큰으로 넣는다. 직접 주지 않는다** (`p-sd-*` 로 덮어쓰면 토큰이 바뀌어도 안 따라간다). 표를 가장자리까지 채우는 등 콘텐츠가 여백을 직접 다뤄야 할 때만 `noPadding` 으로 끈다.
410
410
  - 그 밖에는 팝업 안도 일반 페이지와 같은 규칙(§4)을 따른다: 블록 간격 `gap-sd-12`, 표는 `SKeyValueTable` / `STable`.
411
411
 
@@ -422,7 +422,7 @@ function openDetailPopup(orderId: string) {
422
422
  // 2) 그 라우트의 루트에 SPopup 을 둔다 (조회만 → 푸터 없음)
423
423
  export default function TransferOrderPopupPage() {
424
424
  return (
425
- <SPopup popupTitle="이동 오더 상세" useFooter={false}>
425
+ <SPopup popupTitle="이동 오더 상세">
426
426
  {/* 본문 패딩은 SPopup 이 토큰으로 넣는다 — p-sd-* 로 덮어쓰지 않는다 */}
427
427
  <div className="flex flex-col gap-sd-12">
428
428
  <SSectionHeaderCard>…</SSectionHeaderCard>
@@ -485,7 +485,9 @@ SModal.create({ component: OrderModal, componentProps: { orderId } })
485
485
 
486
486
  작성 중인 내용이 있을 때 닫기를 시도하면 이탈 안내를 띄우는 것은 **소비 앱 몫**이다. 디자인 시스템은 dirty 상태를 알지 못하므로 백드롭·ESC 를 일괄 차단할 뿐이다. 안내가 필요하면 앱이 자체 dirty 판정 후 `SModal.confirm` 으로 띄운다.
487
487
 
488
- ### 3-4. 테이블 컬럼 정렬
488
+ ### 3-4. 테이블 컬럼 — 정렬과 너비
489
+
490
+ #### 정렬
489
491
 
490
492
  **값의 크기를 비교하는 숫자 컬럼은 예외 없이 오른쪽 정렬한다** (`align: 'right'`).
491
493
  자릿수가 세로로 맞아야 값의 크기를 눈으로 비교할 수 있기 때문이다.
@@ -529,6 +531,41 @@ const columns: STableColumn[] = [
529
531
 
530
532
  - `SKeyValueTable` 의 값 셀도 같은 기준을 따른다.
531
533
 
534
+ #### 컨트롤이 들어가는 컬럼은 너비를 명시한다
535
+
536
+ 컬럼 폭은 `width` 로 **고정**되고, `<td>` 는 그 폭을 넘는 내용을 잘라낸다(`overflow: hidden`). 텍스트라면 말줄임으로 끝나지만, 셀에 `STag` · `SButton` · `SGhostButton` · `SSelect` · `SInput` · `SNumberInput` 처럼 **자기 폭을 가진 요소**를 넣으면 요소 자체가 잘려 **누르거나 읽거나 입력할 수 없게 된다.** `width` 를 생략해도 내용에 맞춰 늘어나지 않고 `STable` 의 기본 폭이 될 뿐이므로, 컨트롤이 들어가는 컬럼은 폭을 직접 판단해서 준다.
537
+
538
+ - 기준은 **요소가 온전히 보이는 폭 + 셀 좌우 패딩**이다. 좌우 패딩은 `STable` 이 토큰으로 넣으므로(직접 주지 않는다) 그만큼을 뺀 나머지가 요소 몫이라는 점을 계산에 넣는다.
539
+ - 요소가 둘 이상이면 요소 폭의 합에 **`gap` 까지** 더한다 (행 내부 인라인 액션 간격은 `gap-sd-4` 고정, §3-5-5).
540
+ - 값에 따라 폭이 달라지는 요소(`STag` 라벨, 라벨 있는 버튼)는 **가장 긴 값** 기준으로 잡는다. `판매중` 에 맞춰두면 `판매중지 요청` 에서 잘린다.
541
+ - `SSelect` · `SInput` 처럼 셀 폭을 채우는 컨트롤은 **컬럼 폭이 곧 컨트롤 폭**이다. 실제 선택값·입력값이 말줄임 없이 읽히는 폭인지 확인한다.
542
+ - 폭을 넉넉히 줄 수 없는 자리는 폭을 줄이는 게 아니라 **요소를 바꾼다** — 라벨 버튼 대신 아이콘만 있는 `SGhostButton`, `size="xs"` (§3-5-2, §3-5-5).
543
+ - **`autoWidth` 는 해법이 아니다.** 내용에 맞춰 늘어나는 게 아니라 고정폭 컬럼들이 가져가고 **남은 폭을 나눠 갖는 것**이라, 테이블이 좁으면 역시 잘린다. 컨트롤 컬럼은 `width` 로 직접 확보한다.
544
+
545
+ **`resizable` 테이블이면 `minWidth` 를 함께 준다.** resize 하한 기본값은 어떤 컨트롤도 담지 못할 만큼 작아, 사용자가 끝까지 끌면 그대로 잘린다. `width` 를 정한 근거와 같은 값을 하한으로 둔다 — 텍스트 컬럼과 달리 여기서는 더 줄일 여지가 없다.
546
+
547
+ ```tsx
548
+ const columns: STableColumn[] = [
549
+ // 태그 — 가장 긴 라벨 기준
550
+ { name: 'status', label: '상태', field: 'status', width: '120px', minWidth: 120, align: 'center',
551
+ render: (row: SRow) => <STag size="sm" color="green" label={row.statusLabel} /> },
552
+ // 셀 안 입력 — 컬럼 폭이 곧 입력 폭
553
+ { name: 'qty', label: '수량', field: 'qty', width: '100px', minWidth: 100, align: 'right',
554
+ render: (row: SRow) => <SNumberInput value={row.qty} onValueChange={v => setQty(row, v)} /> },
555
+ // 인라인 액션 둘 — 폭 = xs 버튼 2개 + gap-sd-4 + 셀 좌우 패딩
556
+ { name: 'actions', label: '', field: 'id', width: '84px', minWidth: 84, align: 'center',
557
+ render: (row: SRow) => (
558
+ <div className="flex items-center justify-center gap-sd-4">
559
+ <SGhostButton size="xs" intent="action" icon="edit" ariaLabel="수정" onClick={() => editRow(row)} />
560
+ <SGhostButton size="xs" icon="delete" ariaLabel="삭제" onClick={() => confirmRemove(row)} />
561
+ </div>
562
+ ) },
563
+
564
+ // ❌ 컨트롤 컬럼에 width 생략 — 기본 폭에 맡기면 버튼이 잘린다
565
+ { name: 'move', label: '', field: 'id', render: () => <SButton label="재고 이동" size="xs" /> },
566
+ ];
567
+ ```
568
+
532
569
  #### 값이 없는 셀은 회색 하이픈
533
570
 
534
571
  셀을 **빈칸으로 두지 않는다.** 값이 `null` · `undefined` · 빈 문자열이면 `-` 를 `text-fg-tertiary`(`grey_65`)로 표시한다.
@@ -1012,7 +1049,7 @@ export default function ProductDetailPage() {
1012
1049
  - [ ] `SPage`·`SPopup` 의 기본 패딩을 `p-sd-*` 로 덮어쓰지 않았는가, 블록·섹션 **간격**이 `gap-sd-12` 인가 (`gap-sd-16`/`gap-sd-24` ❌ — 24 는 안쪽 여백에만 열린다)
1013
1050
  - [ ] 섹션·패널의 안쪽 **여백**이 §2-2 판정과 맞는가 (덩어리 두 종류 → 16 / 세 종류 이상 → 24, 서면 16)
1014
1051
  - [ ] `SSectionHeaderCard.Body` 의 여백을 `p-sd-*` 가 아니라 `padding` prop 으로 줬는가
1015
- - [ ] 자체 스크롤하는 패널의 하단에 `pb-sd-80` 이 있는가, 페이지네이션 있는 목록에서 `scrollEndSpacing={false}` 를 넘겼는가
1052
+ - [ ] 자체 스크롤하는 패널의 하단에 `pb-[var(--cmp-pageBody-padding-scrollEnd)]` 이 있는가, 페이지네이션 있는 목록에서 `scrollEndSpacing={false}` 를 넘겼는가
1016
1053
  - [ ] 같은 컴포넌트를 나열할 때 §2-2 그룹 간격을 썼는가 (체크박스 가로 `gap-sd-24` 등)
1017
1054
  - [ ] 페이지가 §4의 표준 골격에서 시작했는가
1018
1055
  - [ ] 앱 셸이나 그 바깥에 `min-width`·`overflow-x` 를 직접 걸지 않았는가 (최소 너비는 `SLayout` 이 보장한다, §4-1)
@@ -1022,6 +1059,7 @@ export default function ProductDetailPage() {
1022
1059
  - [ ] 상태 표시에 `STag size="sm"` 을 썼는가
1023
1060
  - [ ] 테이블에서 양을 나타내는 컬럼(금액·수량·개수 등)이 전부 `align: 'right'` 인가
1024
1061
  - [ ] 번호·코드·전화번호·일자 컬럼에 `align: 'center'` 를 **명시**했는가 (생략하면 좌측이 된다)
1062
+ - [ ] 컨트롤(`STag`·`SButton`·`SSelect`·`SInput` …)이 들어가는 컬럼에 `width` 를 명시했는가, `resizable` 이면 `minWidth` 도 줬는가 (§3-4 — 폭이 모자라면 요소가 잘려 못 쓴다)
1025
1063
  - [ ] 금액·수량 등 양을 나타내는 숫자에 빠짐없이 `toLocaleString()` 을 썼는가 (번호·코드는 제외)
1026
1064
  - [ ] 하단 버튼이 양끝 분리(`justify-between`)이고, 되돌리기가 왼쪽 끝 · 실행이 오른쪽 끝인가
1027
1065
  - [ ] 페이지에 `color="primary"` 채움 버튼이 **1개뿐**인가 (`danger` 채움도 1개, `SDropdownButton` 포함)
@@ -10,7 +10,7 @@
10
10
  |------|------|---------|-------------|
11
11
  | `popupTitle?` | `string` | `''` | 헤더 제목 |
12
12
  | `type?` | `SPopupType` | `'default'` | 타입 (헤더 색상) |
13
- | `useFooter?` | `boolean` | `true` | 하단 footer 표시 여부 |
13
+ | `useFooter?` | `boolean` | `false` | 하단 footer 표시 여부 |
14
14
  | `submitButton?` | `SPopupSubmitButton` | — | 확인 버튼 설정 |
15
15
  | `footerLeft?` | `ReactNode` | — | footer 좌측 영역 |
16
16
  | `noPadding?` | `boolean` | `false` | 본문 기본 패딩을 없앱니다. 표를 가장자리까지 채우는 등 본문이 직접 여백을 다룰 때만 사용합니다. |
@@ -11,20 +11,37 @@
11
11
  | `items` | `SStepperItem[]` | — | 단계 목록 |
12
12
  | `value?` | `string` | — | 현재 활성 단계 value |
13
13
  | `size?` | `SStepperSize` | `'sm'` | 크기 |
14
+ | `vertical?` | `boolean` | `false` | 세로형 여부 |
15
+ | `clickable?` | `boolean` | — | 단계 클릭 가능 여부. 미지정 시 가로형=false, 세로형=true |
14
16
  | `ariaLabel?` | `string` | `'진행 단계'` | 접근성 레이블 |
15
17
  | `className?` | `string` | — | |
16
18
  | `style?` | `CSSProperties` | — | |
17
19
 
20
+ #### Events
21
+
22
+ | Event | Type | Description |
23
+ |-------|------|-------------|
24
+ | `onValueChange` | `(value: string, item: SStepperItem, index: number) => void` | 단계 선택 시 호출 |
25
+
26
+ #### Methods (ref)
27
+
28
+ | Method | Type | Description |
29
+ |--------|------|-------------|
30
+ | `showItemTooltip` | `() => void` | error 아이템 중 첫 번째에 툴팁을 연다. error 아이템이 없으면 아무 일도 일어나지 않는다. |
31
+ | `hideItemTooltip` | `() => void` | 열려있는 커스텀 툴팁을 닫는다. 활성 단계(value prop)가 바뀌면 별도 호출 없이도 자동으로 닫힌다. |
32
+
18
33
  ## Dependencies
19
34
 
20
35
  ### Depends on
21
36
 
22
37
  - [SIcon](../SIcon)
38
+ - [STooltip](../STooltip)
23
39
 
24
40
  ### Graph
25
41
 
26
42
  ```mermaid
27
43
  graph TD;
28
44
  SStepper --> SIcon
45
+ SStepper --> STooltip
29
46
  style SStepper fill:#f9f,stroke:#333,stroke-width:4px
30
47
  ```
@@ -7,10 +7,30 @@ export interface SStepperProps {
7
7
  value?: string;
8
8
  /** 크기 */
9
9
  size?: SStepperSize;
10
+ /** 세로형 여부 */
11
+ vertical?: boolean;
12
+ /** 단계 클릭 가능 여부. 미지정 시 가로형=false, 세로형=true */
13
+ clickable?: boolean;
14
+ /** 단계 선택 시 호출 */
15
+ onValueChange?: (value: string, item: SStepperItem, index: number) => void;
10
16
  /** 접근성 레이블 */
11
17
  ariaLabel?: string;
12
18
  className?: string;
13
19
  style?: CSSProperties;
14
20
  }
21
+ /**
22
+ * ref 로 노출되는 커스텀 툴팁 제어 API. 세로형 전용 — items 중 error 가 true 인 첫 번째
23
+ * 아이템의 텍스트+뱃지 영역에 표시된다.
24
+ *
25
+ * 지금은 대상(첫 번째 error 아이템 하나)·문구·스타일이 hover 툴팁(ERROR_TOOLTIP_MESSAGE, danger)과
26
+ * 동일하게 고정되어 있다 — 열고 닫는 타이밍만 소비자가 제어한다. 대상 아이템을 지정하거나
27
+ * content·type 을 커스터마이즈하는 기능은 필요해지면 추가한다(지금은 만들지 않음).
28
+ */
29
+ export interface SStepperHandle {
30
+ /** error 아이템 중 첫 번째에 툴팁을 연다. error 아이템이 없으면 아무 일도 일어나지 않는다. */
31
+ showItemTooltip: () => void;
32
+ /** 열려있는 커스텀 툴팁을 닫는다. 활성 단계(value prop)가 바뀌면 별도 호출 없이도 자동으로 닫힌다. */
33
+ hideItemTooltip: () => void;
34
+ }
15
35
  /** SStepper — sd-stepper 포팅. 단계 진행 상태 표시 전용. */
16
- export declare function SStepper({ items, value, size, ariaLabel, className, style, }: SStepperProps): import("react").JSX.Element;
36
+ export declare const SStepper: import("react").ForwardRefExoticComponent<SStepperProps & import("react").RefAttributes<SStepperHandle>>;
@@ -1,2 +1,2 @@
1
- export { SStepper, type SStepperProps } from './SStepper';
1
+ export { SStepper, type SStepperHandle, type SStepperProps } from './SStepper';
2
2
  export { STEPPER_SIZES, type SStepperItem, type SStepperSize } from './stepper.config';
@@ -3,6 +3,9 @@ export type SStepperSize = (typeof STEPPER_SIZES)[number];
3
3
  export type SStepperItem = {
4
4
  label: string;
5
5
  value?: string;
6
+ group?: string;
7
+ completed?: boolean;
8
+ error?: boolean;
6
9
  };
7
10
  export declare const STEPPER_SIZE_CONFIG: Record<SStepperSize, {
8
11
  sequenceSize: string;
@@ -34,6 +37,7 @@ export declare const STEPPER_COLORS: {
34
37
  sequenceText: {
35
38
  active: string;
36
39
  default: string;
40
+ completed: string;
37
41
  };
38
42
  text: {
39
43
  active: string;
@@ -41,3 +45,39 @@ export declare const STEPPER_COLORS: {
41
45
  completed: string;
42
46
  };
43
47
  };
48
+ export declare const STEPPER_HORIZONTAL_COMPLETED_COLORS: {
49
+ sequenceBorder: string;
50
+ sequenceBg: string;
51
+ sequenceIcon: string;
52
+ text: string;
53
+ };
54
+ export declare const STEPPER_VERTICAL_LAYOUT: {
55
+ width: string;
56
+ itemGap: string;
57
+ itemPaddingX: string;
58
+ itemPaddingY: string;
59
+ titlePaddingTop: string;
60
+ titlePaddingBottom: string;
61
+ titleFontSize: string;
62
+ titleLineHeight: string;
63
+ accentStripeWidth: string;
64
+ accentStripeColor: string;
65
+ };
66
+ export declare const STEPPER_VERTICAL_COLORS: {
67
+ itemBg: {
68
+ default: string;
69
+ hover: string;
70
+ selected: string;
71
+ };
72
+ titleText: string;
73
+ textDefault: string;
74
+ textCompleted: string;
75
+ sequenceBorderActive: string;
76
+ sequenceBgActive: string;
77
+ sequenceTextActive: string;
78
+ sequenceTextDefault: string;
79
+ sequenceBorderCompleted: string;
80
+ sequenceBgCompleted: string;
81
+ sequenceIconCompleted: string;
82
+ errorBadge: string;
83
+ };