sellmate-design-system-react 6.0.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,8 +405,9 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
405
405
 
406
406
  - 작은 창이 아니라 **화면 하나가 통째로** 들어간다 — 검색 필터·테이블·페이지네이션이 그대로 있는 목록(`엑셀 파일 관리`), 헤더 카드·탭이 있는 상세(`이동 오더 상세`).
407
407
  - 상세를 팝업으로 여는 이유는 **목록을 떠나지 않고 여러 건을 번갈아 보기** 위해서다. 단 상세가 **항상** 팝업인 것은 아니고, 데이터 양이 많을 때 팝업을 쓴다.
408
- - 구조는 헤더(제목 중앙) + 본문 + 푸터다. **확정할 작업이 있으면 푸터에 `저장`, 조회만이면 `useFooter={false}`** 로 푸터를 없앤다.
409
- - 본문에는 패딩이 없다 콘텐츠가 여백을 담당한다. 팝업 안도 일반 페이지와 같은 규칙(§4) 따른다: 패딩 `p-sd-16`(`SPage` 아니라 직접 준다), 블록 간격 `gap-sd-12`, 표는 `SKeyValueTable` / `STable`.
408
+ - 구조는 헤더(제목 중앙) + 본문이고 **푸터는 기본으로 없다**. 조회만 하는 팝업은 그대로 두고, **확정할 작업이 있을 때만 `useFooter` 로 푸터를 켜서 `submitButton` 에 `저장`** 을 둔다.
409
+ - **본문 패딩은 `SPopup` 토큰으로 넣는다. 직접 주지 않는다** (`p-sd-*` 덮어쓰면 토큰이 바뀌어도 안 따라간다). 표를 가장자리까지 채우는 콘텐츠가 여백을 직접 다뤄야 때만 `noPadding` 으로 끈다.
410
+ - 그 밖에는 팝업 안도 일반 페이지와 같은 규칙(§4)을 따른다: 블록 간격 `gap-sd-12`, 표는 `SKeyValueTable` / `STable`.
410
411
 
411
412
  ```tsx
412
413
  // 1) 목록에서 별도 창을 연다 — 창 크기 = 콘텐츠 크기
@@ -421,8 +422,9 @@ function openDetailPopup(orderId: string) {
421
422
  // 2) 그 라우트의 루트에 SPopup 을 둔다 (조회만 → 푸터 없음)
422
423
  export default function TransferOrderPopupPage() {
423
424
  return (
424
- <SPopup popupTitle="이동 오더 상세" useFooter={false}>
425
- <div className="flex flex-col gap-sd-12 p-sd-16">
425
+ <SPopup popupTitle="이동 오더 상세">
426
+ {/* 본문 패딩은 SPopup 이 토큰으로 넣는다 — p-sd-* 로 덮어쓰지 않는다 */}
427
+ <div className="flex flex-col gap-sd-12">
426
428
  <SSectionHeaderCard>…</SSectionHeaderCard>
427
429
  <STabs value={tab} tabs={TABS} onValueChange={setTab} />
428
430
  <STable columns={columns} rows={rows} rowKey="id" />
@@ -483,7 +485,9 @@ SModal.create({ component: OrderModal, componentProps: { orderId } })
483
485
 
484
486
  작성 중인 내용이 있을 때 닫기를 시도하면 이탈 안내를 띄우는 것은 **소비 앱 몫**이다. 디자인 시스템은 dirty 상태를 알지 못하므로 백드롭·ESC 를 일괄 차단할 뿐이다. 안내가 필요하면 앱이 자체 dirty 판정 후 `SModal.confirm` 으로 띄운다.
485
487
 
486
- ### 3-4. 테이블 컬럼 정렬
488
+ ### 3-4. 테이블 컬럼 — 정렬과 너비
489
+
490
+ #### 정렬
487
491
 
488
492
  **값의 크기를 비교하는 숫자 컬럼은 예외 없이 오른쪽 정렬한다** (`align: 'right'`).
489
493
  자릿수가 세로로 맞아야 값의 크기를 눈으로 비교할 수 있기 때문이다.
@@ -527,6 +531,41 @@ const columns: STableColumn[] = [
527
531
 
528
532
  - `SKeyValueTable` 의 값 셀도 같은 기준을 따른다.
529
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
+
530
569
  #### 값이 없는 셀은 회색 하이픈
531
570
 
532
571
  셀을 **빈칸으로 두지 않는다.** 값이 `null` · `undefined` · 빈 문자열이면 `-` 를 `text-fg-tertiary`(`grey_65`)로 표시한다.
@@ -1007,10 +1046,10 @@ export default function ProductDetailPage() {
1007
1046
  - [ ] 간격이 전부 `sd-` 접두 스케일 값인가 (`gap-13`·`gap-sd-13` ❌ → `gap-sd-12` ✅)
1008
1047
  - [ ] 본문이 12px(`typo-body-sm-default`)인가 (14px 본문 ❌)
1009
1048
  - [ ] 텍스트 회색 위계를 순차 적용했는가 (기본 → `text-fg-secondary` → `text-fg-tertiary`, 단계 건너뛰기 ❌)
1010
- - [ ] `SPage` 의 기본 패딩을 `p-sd-*` 로 덮어쓰지 않았는가, 블록·섹션 **간격**이 `gap-sd-12` 인가 (`gap-sd-16`/`gap-sd-24` ❌ — 24 는 안쪽 여백에만 열린다)
1049
+ - [ ] `SPage`·`SPopup` 의 기본 패딩을 `p-sd-*` 로 덮어쓰지 않았는가, 블록·섹션 **간격**이 `gap-sd-12` 인가 (`gap-sd-16`/`gap-sd-24` ❌ — 24 는 안쪽 여백에만 열린다)
1011
1050
  - [ ] 섹션·패널의 안쪽 **여백**이 §2-2 판정과 맞는가 (덩어리 두 종류 → 16 / 세 종류 이상 → 24, 서면 16)
1012
1051
  - [ ] `SSectionHeaderCard.Body` 의 여백을 `p-sd-*` 가 아니라 `padding` prop 으로 줬는가
1013
- - [ ] 자체 스크롤하는 패널의 하단에 `pb-sd-80` 이 있는가, 페이지네이션 있는 목록에서 `scrollEndSpacing={false}` 를 넘겼는가
1052
+ - [ ] 자체 스크롤하는 패널의 하단에 `pb-[var(--cmp-pageBody-padding-scrollEnd)]` 이 있는가, 페이지네이션 있는 목록에서 `scrollEndSpacing={false}` 를 넘겼는가
1014
1053
  - [ ] 같은 컴포넌트를 나열할 때 §2-2 그룹 간격을 썼는가 (체크박스 가로 `gap-sd-24` 등)
1015
1054
  - [ ] 페이지가 §4의 표준 골격에서 시작했는가
1016
1055
  - [ ] 앱 셸이나 그 바깥에 `min-width`·`overflow-x` 를 직접 걸지 않았는가 (최소 너비는 `SLayout` 이 보장한다, §4-1)
@@ -1020,6 +1059,7 @@ export default function ProductDetailPage() {
1020
1059
  - [ ] 상태 표시에 `STag size="sm"` 을 썼는가
1021
1060
  - [ ] 테이블에서 양을 나타내는 컬럼(금액·수량·개수 등)이 전부 `align: 'right'` 인가
1022
1061
  - [ ] 번호·코드·전화번호·일자 컬럼에 `align: 'center'` 를 **명시**했는가 (생략하면 좌측이 된다)
1062
+ - [ ] 컨트롤(`STag`·`SButton`·`SSelect`·`SInput` …)이 들어가는 컬럼에 `width` 를 명시했는가, `resizable` 이면 `minWidth` 도 줬는가 (§3-4 — 폭이 모자라면 요소가 잘려 못 쓴다)
1023
1063
  - [ ] 금액·수량 등 양을 나타내는 숫자에 빠짐없이 `toLocaleString()` 을 썼는가 (번호·코드는 제외)
1024
1064
  - [ ] 하단 버튼이 양끝 분리(`justify-between`)이고, 되돌리기가 왼쪽 끝 · 실행이 오른쪽 끝인가
1025
1065
  - [ ] 페이지에 `color="primary"` 채움 버튼이 **1개뿐**인가 (`danger` 채움도 1개, `SDropdownButton` 포함)
@@ -10,9 +10,10 @@
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
+ | `noPadding?` | `boolean` | `false` | 본문 기본 패딩을 없앱니다. 표를 가장자리까지 채우는 등 본문이 직접 여백을 다룰 때만 사용합니다. |
16
17
  | `children?` | `ReactNode` | — | 본문 |
17
18
  | `className?` | `string` | — | |
18
19
  | `style?` | `CSSProperties` | — | |
@@ -20,6 +20,8 @@ export interface SPopupProps {
20
20
  onSubmit?: () => void;
21
21
  /** footer 좌측 영역 */
22
22
  footerLeft?: ReactNode;
23
+ /** 본문 기본 패딩을 없앱니다. 표를 가장자리까지 채우는 등 본문이 직접 여백을 다룰 때만 사용합니다. */
24
+ noPadding?: boolean;
23
25
  /** 본문 */
24
26
  children?: ReactNode;
25
27
  className?: string;
package/dist/index.cjs CHANGED
@@ -6070,10 +6070,11 @@ 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,
6077
+ noPadding = false,
6077
6078
  children,
6078
6079
  className,
6079
6080
  style
@@ -6104,10 +6105,11 @@ var SPopup = /* @__PURE__ */ react.forwardRef(function SPopup2({
6104
6105
  }
6105
6106
  ),
6106
6107
  /* @__PURE__ */ jsxRuntime.jsx(
6107
- "div",
6108
+ "main",
6108
6109
  {
6109
6110
  className: cn(
6110
- "min-h-0 flex-1 overflow-auto",
6111
+ "box-border min-h-0 flex-1 overflow-auto",
6112
+ !noPadding && "p-(--sys-space-panel-padding-default)",
6111
6113
  SCROLLBAR_CLASS,
6112
6114
  SCROLLBAR_TRACK_BG_CLASS,
6113
6115
  SCROLLBAR_TRACK_BORDER_CLASS
@@ -7871,9 +7873,7 @@ var SPage = /* @__PURE__ */ react.forwardRef(function SPage2({ background = "fra
7871
7873
  ref,
7872
7874
  className: cn(
7873
7875
  "box-border min-h-0 min-w-0 flex-1 overflow-auto p-[var(--cmp-pageBody-padding-default)]",
7874
- // 스크롤 끝 여백은 아직 전용 토큰이 없어 스케일 값을 직접 쓴다.
7875
- // component.pageBody.padding.scrollEnd 가 들어오면 var() 참조로 바꾼다.
7876
- scrollEndSpacing && "pb-sd-80",
7876
+ scrollEndSpacing && "pb-[var(--cmp-pageBody-padding-scrollEnd)]",
7877
7877
  SCROLLBAR_CLASS,
7878
7878
  BACKGROUND_CLASS[background],
7879
7879
  className
@@ -9491,7 +9491,11 @@ var STable = /* @__PURE__ */ react.forwardRef(function STable2({
9491
9491
  "div",
9492
9492
  {
9493
9493
  className: cn(
9494
- "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)]",
9495
9499
  // height가 없으면 루트가 받은 높이를 끝까지 채운다. 호스트가 className으로
9496
9500
  // `flex-1 min-h-0` 등을 줘 루트 높이가 확정된 경우, 행이 적어도 테두리 박스가
9497
9501
  // 남은 영역을 그대로 차지한다. basis는 auto로 둬 높이가 확정되지 않은