sellmate-design-system-react 6.1.0 → 7.0.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` | 본문 기본 패딩을 없앱니다. 표를 가장자리까지 채우는 등 본문이 직접 여백을 다룰 때만 사용합니다. |
package/dist/index.cjs CHANGED
@@ -6070,7 +6070,7 @@ function SFooter({
6070
6070
  var SPopup = /* @__PURE__ */ react.forwardRef(function SPopup2({
6071
6071
  popupTitle = "",
6072
6072
  type = "default",
6073
- useFooter = true,
6073
+ useFooter = false,
6074
6074
  submitButton,
6075
6075
  onSubmit,
6076
6076
  footerLeft,
@@ -7873,9 +7873,7 @@ var SPage = /* @__PURE__ */ react.forwardRef(function SPage2({ background = "fra
7873
7873
  ref,
7874
7874
  className: cn(
7875
7875
  "box-border min-h-0 min-w-0 flex-1 overflow-auto p-[var(--cmp-pageBody-padding-default)]",
7876
- // 스크롤 끝 여백은 아직 전용 토큰이 없어 스케일 값을 직접 쓴다.
7877
- // component.pageBody.padding.scrollEnd 가 들어오면 var() 참조로 바꾼다.
7878
- scrollEndSpacing && "pb-sd-80",
7876
+ scrollEndSpacing && "pb-[var(--cmp-pageBody-padding-scrollEnd)]",
7879
7877
  SCROLLBAR_CLASS,
7880
7878
  BACKGROUND_CLASS[background],
7881
7879
  className
@@ -9493,7 +9491,11 @@ var STable = /* @__PURE__ */ react.forwardRef(function STable2({
9493
9491
  "div",
9494
9492
  {
9495
9493
  className: cn(
9496
- "bg-white relative flex flex-col overflow-hidden border border-solid border-[color:var(--cmp-table-border-color)]",
9494
+ // isolate: no-data/loading 오버레이(z-30·31)를 테이블 안에서 끝낸다. relative + z-auto
9495
+ // 만으로는 스태킹 컨텍스트가 생기지 않아, 모달 안에 들어가면 30·31이 모달 Content
9496
+ // 레벨까지 올라가 같은 컨텍스트의 floating portal 밴드(z-2)를 덮는다.
9497
+ // 자손은 이미 overflow-hidden으로 클리핑되므로 테이블 내부 순서는 달라지지 않는다.
9498
+ "bg-white relative isolate flex flex-col overflow-hidden border border-solid border-[color:var(--cmp-table-border-color)]",
9497
9499
  // height가 없으면 루트가 받은 높이를 끝까지 채운다. 호스트가 className으로
9498
9500
  // `flex-1 min-h-0` 등을 줘 루트 높이가 확정된 경우, 행이 적어도 테두리 박스가
9499
9501
  // 남은 영역을 그대로 차지한다. basis는 auto로 둬 높이가 확정되지 않은