sellmate-design-system-react 9.0.0-beta.3 → 9.0.0-beta.5

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
@@ -28,7 +28,7 @@
28
28
  | --- | --- |
29
29
  | **버튼·링크** | `SButton` `SGhostButton` `SDropdownButton` `STextLink` `SSwitch` `SToggle` |
30
30
  | **입력 (폼)** | `SForm` `SField` `SInput` `SNumberInput` `STextarea` `SSelect` `SCheckbox` `SRadio` `SRadioGroup` `SRadioButton` `SChip` `SChipInput` `SBarcodeInput` `SFilePicker` |
31
- | **날짜·시간** | `SCalendar` `SDatePicker` `SDateRangePicker` `STimePicker` `STimeRangePicker` |
31
+ | **날짜·시간** | `SCalendar` `SDatePicker` `SDatePickerYearListbox` `SDatePickerMonthListbox` `SDateRangePicker` `STimePicker` `STimeRangePicker` |
32
32
  | **표·목록** | `STable` `STableBar` `SChipFilter` `SKeyValueTable` `SList` `SListItem` `SExpansionList` `SDraggableList` `SDraggableItem` `STree` |
33
33
  | **레이아웃** | `SLayout` `SGnb` `SPage`(제목 영역은 `header` prop) `SSectionHeaderCard` `SCard` `SDivider` `SSplitter` `SScrollArea` `SExpansionItem` |
34
34
  | **내비게이션** | `STabs` `SPagination` `SStepper` |
@@ -72,7 +72,7 @@ AI 에이전트는 코드를 생성하기 전에 이 목록을 반드시 지킨
72
72
  | `alert()`, `confirm()` | `SToast`, `SModal.confirm(...)` |
73
73
  | 직접 만든 탭/페이지네이션/스텝퍼 | `STabs`, `SPagination`, `SStepper` |
74
74
  | `<ul>`/`<li>` 로 만든 목록 UI | `SList` + `SListItem` (드래그 정렬은 `SDraggableItem`) |
75
- | 직접 만든 섹션 카드(제목 바 + 본문 박스) | `SSectionHeaderCard` + `.Header` / `.Body` |
75
+ | 직접 만든 섹션 카드(제목 바 + 본문 박스) | `SSectionHeaderCard` `title` / `padding` props |
76
76
  | `<svg>` 직접 삽입, 이모지 아이콘 | `SIcon` |
77
77
  | `<hr>` | `SDivider` |
78
78
  | `<details>` / `<summary>` | `SExpansionItem` |
@@ -138,7 +138,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
138
138
  | --- | --- | --- |
139
139
  | **셸** | 앱 전체 뼈대. 페이지가 바뀌어도 남는다 | `SLayout` `SGnb` `SPage`(제목 영역은 `header` prop) |
140
140
  | **블록** | `SPage` 의 직계 자식. 페이지를 세로로 쌓는 단위 | `SSectionHeaderCard` `SCard` `SForm` `SSplitter` `SScrollArea` `STable` `STableBar` `SChipFilter` `SKeyValueTable` `SList` `SExpansionList` `SDraggableList` `STree` `SCallout` `STabs` `SStepper` `SPagination` `SDivider` |
141
- | **요소** | 블록 **안에** 놓이는 컨트롤. 혼자 페이지에 서지 않는다 | `SButton` `SGhostButton` `SDropdownButton` `SField` `SInput` `SNumberInput` `STextarea` `SSelect` `SCheckbox` `SRadio` `SRadioGroup` `SRadioButton` `SSwitch` `SToggle` `SChipInput` `SBarcodeInput` `SFilePicker` `SDatePicker` `SDateRangePicker` `STimePicker` `STimeRangePicker` `SCalendar` `SListItem` `SExpansionItem` `SDraggableItem` `SImage` `SLinearProgress` `SCircleProgress` |
141
+ | **요소** | 블록 **안에** 놓이는 컨트롤. 혼자 페이지에 서지 않는다 | `SButton` `SGhostButton` `SDropdownButton` `SField` `SInput` `SNumberInput` `STextarea` `SSelect` `SCheckbox` `SRadio` `SRadioGroup` `SRadioButton` `SSwitch` `SToggle` `SChipInput` `SBarcodeInput` `SFilePicker` `SDatePicker` `SDatePickerYearListbox` `SDatePickerMonthListbox` `SDateRangePicker` `STimePicker` `STimeRangePicker` `SCalendar` `SListItem` `SExpansionItem` `SDraggableItem` `SImage` `SLinearProgress` `SCircleProgress` |
142
142
  | **인라인** | 텍스트 흐름·셀·라벨 안에 섞인다. 혼자 블록이 되지 않는다 | `STag` `SBadge` `SIcon` `STextLink` `SChip` |
143
143
  | **레이어** | 문서 흐름 **밖**에 떠서 그려진다. 어느 층에서 띄우든 레이아웃에 영향이 없다 | `SModal` `SActionModal` `SConfirmModal` `SPopup` `SDrawer` `SPopover` `STooltip` `SPortal` `SToast` `SLoadingModal` `SLoadingContainer` `SGuide` |
144
144
 
@@ -158,7 +158,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
158
158
  | 담는 것 | 올 수 있는 것 | 오면 안 되는 것 |
159
159
  | --- | --- | --- |
160
160
  | `SPage` | **블록만** | **요소를 직접** — 버튼 하나도 블록에 담아 놓는다 |
161
- | `SSectionHeaderCard.Body` | 그리는 블록 · 요소 | `SSectionHeaderCard` · `SCard` (카드 겹침, §3-7-8) |
161
+ | `SSectionHeaderCard` 의 `children` | 그리는 블록 · 요소 | `SSectionHeaderCard` · `SCard` (카드 겹침, §3-7-8) |
162
162
  | `SCard` | 그리는 블록 · 요소 | `SCard` · `SSectionHeaderCard` |
163
163
  | `SForm` | 블록 (보통 `SKeyValueTable` + 하단 액션) | — |
164
164
  | `SSplitter.Before` / `.After` | 블록 | — |
@@ -222,7 +222,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
222
222
 
223
223
  페이지 제목만 18px 로 크게 두고 그 아래는 14 / 12 로 촘촘하게 간다. 중간 크기(16px)는 기본 골격에서 쓰지 않는다.
224
224
 
225
- - **페이지·섹션 제목의 타이포를 직접 주지 않는다.** `SPage` 의 `header.title`, `SSectionHeaderCard.Header` 의 `title` 이 이미 넣는다 — 그 위에 `typo-heading-lg`/`typo-heading-sm` 을 또 씌우지 않는다.
225
+ - **페이지·섹션 제목의 타이포를 직접 주지 않는다.** `SPage` 의 `header.title`, `SSectionHeaderCard` 의 `header.title` 이 이미 넣는다 — 그 위에 `typo-heading-lg`/`typo-heading-sm` 을 또 씌우지 않는다.
226
226
  - **하위 제목이 필요하면 먼저 섹션을 나눌 수 없는지 본다.** 한 섹션 안에서 제목이 두 단으로 갈린다는 것은 대개 섹션이 둘이라는 뜻이다 (§3-7-8).
227
227
  - 본문 안에서 한 단어를 강조할 때는 `typo-body-sm-medium` 을 쓴다. `typo-body-sm-bold` 는 제목 성격의 짧은 라벨에만 쓴다. <!-- TODO(디자인): 강조 굵기 기준 확정 -->
228
228
 
@@ -292,7 +292,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
292
292
 
293
293
  **페이지 프레임은 예외 없이 `SPage` 가 넣는다.** 아래 규칙은 그 안의 **섹션·패널 레벨에만** 적용된다.
294
294
 
295
- **컴포넌트가 자체 여백을 가지면 컴포넌트 기준이 우선한다.** `SKeyValueTable`·`STable` 처럼 자기 여백을 토큰으로 갖고 있는 컴포넌트에는 이 판정을 적용하지 않는다 — 손댈 것이 없다. 아래 판정이 필요한 자리는 **직접 만든 컨테이너**와 **`SSectionHeaderCard.Body`** 두 곳뿐이다.
295
+ **컴포넌트가 자체 여백을 가지면 컴포넌트 기준이 우선한다.** `SKeyValueTable`·`STable` 처럼 자기 여백을 토큰으로 갖고 있는 컴포넌트에는 이 판정을 적용하지 않는다 — 손댈 것이 없다. 아래 판정이 필요한 자리는 **직접 만든 컨테이너**와 **`SSectionHeaderCard` 의 `padding`** 두 곳뿐이다.
296
296
 
297
297
  판정은 **그 영역이 담고 있는 콘텐츠 덩어리의 종류 수**로 한다.
298
298
 
@@ -327,14 +327,47 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
327
327
 
328
328
  **중첩되면 안쪽 여백을 주지 않는다.** 24 영역 안에 또 여백을 주면 가장자리가 40 으로 벌어져 한 면적처럼 읽힌다. 안쪽 카드·목록이 **배경색이 다르거나 테두리가 있어** 경계가 스스로 보이는 경우에만 자기 여백을 유지한다.
329
329
 
330
- `SSectionHeaderCard.Body` 는 이 규칙을 **prop 으로 받는다** — 직접 `p-sd-*` 를 주지 않는다.
330
+ `SSectionHeaderCard` 는 이 규칙을 **`padding` prop 으로 받는다** — 직접 `p-sd-*` 를 주지 않는다.
331
331
 
332
332
  ```tsx
333
- <SSectionHeaderCard.Body>…</SSectionHeaderCard.Body> {/* 기본 = 16 */}
334
- <SSectionHeaderCard.Body padding="wide">…</SSectionHeaderCard.Body> {/* 3종류 이상 */}
335
- <SSectionHeaderCard.Body padding="none">…</SSectionHeaderCard.Body> {/* 표를 가장자리까지 */}
333
+ <SSectionHeaderCard title="기본 정보">…</SSectionHeaderCard> {/* 기본 = 16 */}
334
+ <SSectionHeaderCard title="기본 정보" padding="wide">…</SSectionHeaderCard> {/* 3종류 이상 */}
335
+ <SSectionHeaderCard title="기본 정보" padding="none">…</SSectionHeaderCard> {/* 표를 가장자리까지 */}
336
336
  ```
337
337
 
338
+ ##### 카드 가장자리까지 채우는 표는 자기 테두리를 끈다
339
+
340
+ 여기서 "표"는 **`STable` 과 `SKeyValueTable` 둘 다**다. 두 컴포넌트 모두 자기 바깥 테두리를 그리는데, 카드도 바깥 테두리를 그린다. `padding="none"` 으로 붙이면 **1px 두 개가 나란히 놓여 그 변만 2px** 로 보인다(카드의 다른 변은 1px 그대로라 굵기가 어긋난다).
341
+
342
+ ```tsx
343
+ <SSectionHeaderCard title="발주 내역" padding="none">
344
+ <SKeyValueTable fields={…} bordered={false} radius="useTop" />
345
+ <STable columns={…} rows={…} bordered={false} radius="useTop" />
346
+ </SSectionHeaderCard>
347
+ ```
348
+
349
+ - **`bordered={false}`** 로 표의 테두리를 끈다. 카드가 이미 그린다.
350
+ - **`radius="useTop"`** 으로 위쪽 모서리를 죽인다. 아래쪽 라운드는 카드가 처리한다.
351
+ - `STable` 은 **페이지네이션 바의 테두리와 `-mt-px` 겹침도 함께 꺼진다** — 본문 테두리가 없으면 겹칠 대상이 없어, 그대로 두면 페이지네이션만 테두리를 갖고 1px 어긋난다.
352
+
353
+ #### 본문 바탕 눌러앉히기 (선택)
354
+
355
+ `SSectionHeaderCard` 는 **`background="neutral"`** 로 본문 바탕을 한 단계 눌러앉힐 수 있다. 흰 면 덩어리(표·리스트)가 여럿일 때 그 덩어리들이 **"면 위에 놓인 객체"로 읽혀 묶음이 더 강하게 보인다.**
356
+
357
+ ```tsx
358
+ <SSectionHeaderCard title="발주 상세" background="neutral">…흰 면 표 여럿…</SSectionHeaderCard>
359
+ ```
360
+
361
+ **기본값(`frame`, 흰 면)이 틀린 것이 아니다.** 이 저장소의 표·리스트는 테두리·라운드·헤더 줄과 `gap-sd-12` 를 이미 갖고 있어 흰 바탕에서도 경계가 읽힌다. 위계를 한 단계 더 주고 싶을 때 고르는 수단이지, 덩어리가 둘 이상이면 반드시 깔아야 하는 규칙이 아니다.
362
+
363
+ 깔아도 **효과가 없는** 자리는 있다.
364
+
365
+ - **덩어리가 가장자리까지 차는 경우** — `padding="none"` 으로 표를 채우면 깐 바탕이 표에 완전히 가려 보이지 않는다.
366
+ - **덩어리에 회색 면이 섞인 경우** — 그 덩어리가 바탕과 같은 색이 되어 묻힌다.
367
+ - **맨 텍스트·폼 컨트롤만 있는 본문** — 떠오를 흰 면이 없다.
368
+
369
+ 바탕을 깐 경우, 표 사이 구분선(`SDivider`)은 대개 불필요해진다 — 색이 이미 경계를 만든다.
370
+
338
371
  #### 스크롤 영역의 하단 여백
339
372
 
340
373
  스크롤을 끝까지 내렸을 때 마지막 항목이 화면 경계에 붙으면 **목록이 끝난 것인지 더 있는 것인지** 읽히지 않는다. 그래서 스크롤 영역은 **하단만** 넓게 둔다. 나머지 세 방향은 위 16 / 24 규칙 그대로다.
@@ -344,7 +377,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
344
377
  | **패널 자체 스크롤** (좌측 목록, 중앙 대화 등) | 그 패널 안쪽 하단에 `pb-[var(--cmp-pageBody-padding-scrollEnd)]` — `SPage` 와 같은 토큰이라 값이 바뀌어도 함께 따라간다 |
345
378
  | **페이지 단위 스크롤** | **`SPage` 가 넣는다. 직접 주지 않는다** |
346
379
 
347
- `SPage` 는 기본으로 넣으므로 **아무것도 하지 않으면 맞다.** 끄는 경우는 하나뿐이다 — **페이지네이션이 붙은 테이블.** 페이지네이션이 이미 "여기서"을 알려주므로 `scrollEndSpacing={false}` 끈다 (§4-2 목록 페이지).
380
+ `SPage` 는 기본으로 넣으므로 **아무것도 하지 않으면 맞다.** 직접 일은 없다 — **페이지네이션이 붙은 테이블**은 `contentHeight="fill"` 쓰고, 이 모드에서는 페이지가 스크롤하지 않아 스크롤 여백이 **자동으로 무시**된다 (§4-2 목록 페이지).
348
381
 
349
382
  ### 2-3. 색상
350
383
 
@@ -419,6 +452,8 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
419
452
  | 입력된 값 하나를 지우거나 고치게 한다 | `SChip` | §3-1 |
420
453
  | 파일을 받는다 | `SFilePicker` | |
421
454
  | 날짜 하나를 받는다 | `SDatePicker` | §3-7-4 |
455
+ | 연도 선택 리스트만 커스텀 조합에 넣는다 | `SDatePickerYearListbox` | §3-7-4 |
456
+ | 연도+월 선택 리스트만 커스텀 조합에 넣는다 | `SDatePickerMonthListbox` | §3-7-4 |
422
457
  | 날짜 기간을 받는다 | `SDateRangePicker` | §3-7-4 |
423
458
  | 시각 하나를 받는다 | `STimePicker` | |
424
459
  | 시각 범위를 받는다 | `STimeRangePicker` | |
@@ -550,9 +585,26 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
550
585
  | | 무엇인가 | 크기 |
551
586
  | --- | --- | --- |
552
587
  | **SPopup** | **별도 브라우저 창** (`window.open` 으로 여는 전용 라우트) | 창 크기 = 콘텐츠 크기 |
553
- | **SActionModal** | 같은 창 위 오버레이 카드 | `width` / `height` prop |
588
+ | **SActionModal** | 같은 창 위 오버레이 카드 | `width` prop. **높이는 주지 않는다** (아래) |
554
589
  | **SModal.confirm** (`SConfirmModal`) | 같은 창 위 확인창 | 고정 |
555
590
 
591
+ ##### 모달 높이는 내용이 정하고, 상한은 시스템이 건다
592
+
593
+ **`height` 를 주지 않는다.** 높이는 내용이 정하고, 카드는 **뷰포트의 85%** 에서 멈춘다(시스템이 모든 모달에 건다). 데이터가 적으면 내용만큼 작아지고, 많으면 85% 에서 멈춘다.
594
+
595
+ 가로는 좌우 24px 씩을 뺀 값으로 클램핑하는데 **세로만 비율**인 이유는, 모달이 화면을 거의 다 덮으면 뒤 맥락이 사라져 "떠 있는 것"으로 읽히지 않기 때문이다.
596
+
597
+ **상한에 닿았을 때 스크롤되어야 하는 것은 모달 본문이 아니라 표다.**
598
+
599
+ ```tsx
600
+ <SActionModal modalTitle="발주 검토" button={{ label: '확정', onClick: submit }}>
601
+ {/* 표가 남은 높이를 먹고 자기 안에서 스크롤한다 — 헤더·합계·푸터는 늘 보인다 */}
602
+ <STable className="min-h-0 flex-1" columns={columns} rows={rows} />
603
+ </SActionModal>
604
+ ```
605
+
606
+ 본문(`overflow-auto` 영역)이 통째로 스크롤되면 표 헤더와 합계 줄이 위로 밀려 사라진다. `SActionModal` 의 본문은 이미 `min-h-0 flex-1` 이므로, 표에 `min-h-0 flex-1` 을 주면 세로 축이 이어져 표만 스크롤한다 (§4-2 목록 페이지와 같은 사슬이다).
607
+
556
608
  **성격이 먼저 둘로 갈린다.**
557
609
 
558
610
  | 성격 | 정의 | 컴포넌트 |
@@ -740,6 +792,7 @@ const columns: STableColumn[] = [
740
792
 
741
793
  컬럼 폭은 `width` 로 **고정**되고, `<td>` 는 그 폭을 넘는 내용을 잘라낸다(`overflow: hidden`). 텍스트라면 말줄임으로 끝나지만, 셀에 `STag` · `SButton` · `SGhostButton` · `SSelect` · `SInput` · `SNumberInput` 처럼 **자기 폭을 가진 요소**를 넣으면 요소 자체가 잘려 **누르거나 읽거나 입력할 수 없게 된다.** `width` 를 생략해도 내용에 맞춰 늘어나지 않고 `STable` 의 기본 폭이 될 뿐이므로, 컨트롤이 들어가는 컬럼은 폭을 직접 판단해서 준다.
742
794
 
795
+ - **컨트롤이 들어가는 컬럼에는 `contentType: 'control'` 을 함께 준다.** 좌우 패딩이 텍스트용(넓게)에서 컨트롤용(좁게)으로 바뀌어, 같은 컬럼 폭에서도 요소가 쓸 폭이 넓어진다. 기본값은 `text` 다.
743
796
  - 기준은 **요소가 온전히 보이는 폭 + 셀 좌우 패딩**이다. 좌우 패딩은 `STable` 이 토큰으로 넣으므로(직접 주지 않는다) 그만큼을 뺀 나머지가 요소 몫이라는 점을 계산에 넣는다.
744
797
  - 요소가 둘 이상이면 요소 폭의 합에 **`gap` 까지** 더한다 (행 내부 인라인 액션 간격은 `gap-sd-4` 고정, §3-5-5).
745
798
  - 값에 따라 폭이 달라지는 요소(`STag` 라벨, 라벨 있는 버튼)는 **가장 긴 값** 기준으로 잡는다. `판매중` 에 맞춰두면 `판매중지 요청` 에서 잘린다.
@@ -747,6 +800,13 @@ const columns: STableColumn[] = [
747
800
  - 폭을 넉넉히 줄 수 없는 자리는 폭을 줄이는 게 아니라 **요소를 바꾼다** — 라벨 버튼 대신 아이콘만 있는 `SGhostButton`, `size="xs"` (§3-5-2, §3-5-5).
748
801
  - **`autoWidth` 는 해법이 아니다.** 내용에 맞춰 늘어나는 게 아니라 고정폭 컬럼들이 가져가고 **남은 폭을 나눠 갖는 것**이라, 테이블이 좁으면 역시 잘린다. 컨트롤 컬럼은 `width` 로 직접 확보한다.
749
802
 
803
+ ```tsx
804
+ { name: 'normal', label: '정상', field: 'normal', width: '96px',
805
+ align: 'center', contentType: 'control', render: row => <SNumberInput … /> },
806
+ ```
807
+
808
+ **셀 좌우 여백은 내용이 정한다.** 텍스트는 넓게, 컨트롤은 좁게다 — `SKeyValueTable` 은 `field.type` 으로 이 판정을 스스로 하지만, `STable` 의 셀은 소비 앱이 넘긴 임의의 `render` 결과라 컴포넌트가 알 수 없다. 그래서 `contentType` 으로 알려준다. 여백 값 자체는 토큰이 정하므로 `tdClass` 로 패딩을 직접 덮어쓰지 않는다.
809
+
750
810
  **`resizable` 테이블이면 `minWidth` 를 함께 준다.** resize 하한 기본값은 어떤 컨트롤도 담지 못할 만큼 작아, 사용자가 끝까지 끌면 그대로 잘린다. `width` 를 정한 근거와 같은 값을 하한으로 둔다 — 텍스트 컬럼과 달리 여기서는 더 줄일 여지가 없다.
751
811
 
752
812
  ```tsx
@@ -1005,11 +1065,25 @@ const columns: STableColumn[] = [
1005
1065
  | 판별 | 사용 |
1006
1066
  | --- | --- |
1007
1067
  | 날짜 **하나**를 값으로 받는다 | `SDatePicker` |
1068
+ | 연도 선택 리스트만 필요하다 (트리거·팝오버는 직접 조합) | `SDatePickerYearListbox` |
1069
+ | 연도+월 선택 리스트만 필요하다 (트리거·팝오버는 직접 조합) | `SDatePickerMonthListbox` |
1008
1070
  | **시작~종료** 를 값으로 받는다 | `SDateRangePicker` |
1009
1071
  | 달력 격자 **자체가 화면 콘텐츠** 다 (일정·이벤트 보기) | `SCalendar` |
1010
1072
 
1011
1073
  - **기간을 `SDatePicker` 두 개로 만들지 않는다.** 시작이 종료보다 뒤인 입력을 막는 검증과 한쪽만 고른 중간 상태 처리가 `SDateRangePicker` 안에 이미 있다. 두 개로 쪼개면 그게 전부 앱 몫이 된다.
1012
1074
  - `SDatePicker`·`SDateRangePicker` 는 내부적으로 `SCalendar` 를 팝오버로 띄운다. 값을 받는 자리에 `SCalendar` 를 직접 쓰지 않는다.
1075
+ - `SDatePickerYearListbox`·`SDatePickerMonthListbox` 는 `SDatePicker` 의 mode listbox 조각만 떼어낸 컴포넌트다. 일반 폼 입력에는 `SDatePicker mode="year" | "month"` 를 우선 쓰고, 다른 트리거·팝오버 안에 리스트만 끼워 넣을 때만 직접 쓴다.
1076
+
1077
+ **날짜·시간 피커는 폭 상한을 스스로 갖는다 — `width` 를 주지 않는다.** 값 길이가 `YYYY-MM-DD` 처럼 정해져 있어 컴포넌트가 사이즈별 상한을 안다. `SKeyValueTable` 이 모든 컨트롤에 `width="100%"` 를 넘기지만, 이 상한 덕분에 행 전체로 늘어나지 않고 제 폭에서 멈춘다.
1078
+
1079
+ | 컴포넌트 | `size="sm"` | `size="md"` |
1080
+ | --- | --- | --- |
1081
+ | `SDatePicker` | md | lg |
1082
+ | `SDateRangePicker` | lg | xl |
1083
+ | `STimePicker` | md | lg |
1084
+ | `STimeRangePicker` | md | lg (오전/오후 표시는 두 사이즈 모두 lg) |
1085
+
1086
+ `SDateRangePicker` 가 한 등급씩 위인 것은 값이 `YYYY-MM-DD ~ YYYY-MM-DD` 로 두 배가 넘기 때문이다. 같은 이유로 `STimeRangePicker` 의 오전/오후 모드도 sm 에서 한 등급 위를 쓴다 — 그 모드의 최소 폭이 md 등급을 이미 넘어, 그대로 두면 하한이 상한을 넘어 상한이 무력해진다.
1013
1087
 
1014
1088
  #### 3-7-5. SField 를 직접 쓰는 경우
1015
1089
 
@@ -1057,7 +1131,7 @@ const columns: STableColumn[] = [
1057
1131
 
1058
1132
  - 페이지 골격에서 콘텐츠를 묶는 섹션은 **사실상 전부 `SSectionHeaderCard`** 다 (§4-4·§4-5). 제목·필수 표시·도움말·헤더 우측 액션이 전부 여기 붙는다.
1059
1133
  - **카드 안에 카드를 겹치지 않는다.** 섹션 안을 더 나눠야 하면 `SDivider` 로 끊거나(§3-6) 섹션을 둘로 분리한다.
1060
- - 안쪽 여백은 `SSectionHeaderCard.Body` 의 `padding` prop 으로 준다 — `p-sd-*` 를 직접 주지 않는다 (§2-2).
1134
+ - 안쪽 여백은 `SSectionHeaderCard` 의 `padding` prop 으로 준다 — `p-sd-*` 를 직접 주지 않는다 (§2-2).
1061
1135
 
1062
1136
  #### 3-7-9. SLinearProgress vs SCircleProgress
1063
1137
 
@@ -1129,15 +1203,27 @@ export default function AppShell({
1129
1203
  children,
1130
1204
  header,
1131
1205
  scrollEndSpacing,
1132
- }: { children: React.ReactNode; header?: SPageHeaderProps; scrollEndSpacing?: boolean }) {
1206
+ contentHeight,
1207
+ }: {
1208
+ children: React.ReactNode;
1209
+ header?: SPageHeaderProps;
1210
+ scrollEndSpacing?: boolean;
1211
+ contentHeight?: SPageContentHeight;
1212
+ }) {
1133
1213
  return (
1134
1214
  <SLayout type="box" header="fix">
1135
1215
  {/* type/header/folded 는 SLayout 에만 준다 — SGnb 는 context 에서 읽는다 */}
1136
1216
  <SGnb items={MENU} value={current} onValueChange={navigate} logo={<Logo />} />
1137
1217
  {/* 콘텐츠 패딩은 SPage 가 토큰으로 넣는다 — p-sd-* 로 덮어쓰지 않는다 */}
1138
1218
  {/* 스크롤 끝 여백도 SPage 가 넣는다. 끄는 건 페이지네이션 있는 목록뿐이라 페이지가 정한다 */}
1219
+ {/* 본문이 남은 높이를 채우는 목록 페이지는 contentHeight="fill" 을 넘긴다 (§4-2) */}
1139
1220
  {/* header 는 페이지마다 달라 AppShell 이 그대로 받아 넘긴다 — 페이지 제목은 여기서 만들지 않는다 */}
1140
- <SPage background="frame" scrollEndSpacing={scrollEndSpacing} header={header}>
1221
+ <SPage
1222
+ background="frame"
1223
+ scrollEndSpacing={scrollEndSpacing}
1224
+ contentHeight={contentHeight}
1225
+ header={header}
1226
+ >
1141
1227
  {children}
1142
1228
  </SPage>
1143
1229
  </SLayout>
@@ -1179,7 +1265,7 @@ import { SModalOutlet } from 'sellmate-design-system-react';
1179
1265
 
1180
1266
  **최소 너비는 `SLayout` 이 보장한다 — 앱이 `min-w-*` 를 직접 주지 않는다.** 창이 최소 너비(`SLAYOUT_MIN_WIDTH`, GNB 포함한 전체 기준)보다 좁아지면 GNB·상단바는 제자리에 남고 **`SPage` 안에만 가로 스크롤이 생긴다.** 문서(브라우저 창)에는 가로 스크롤이 생기지 않는다. 그래서 셸을 감싸는 요소에 `min-width` 나 `overflow-x` 를 걸지 않는다 — 걸면 창 전체가 스크롤되어 GNB 가 화면 밖으로 밀린다.
1181
1267
 
1182
- **셸의 `SPage` 는 모든 페이지가 공유하므로, 스크롤 여백을 끄려면 페이지가 셸에 알려야 한다.** 위처럼 프레임 컴포넌트가 `scrollEndSpacing` 받아 그대로 넘기고, 페이지네이션이 있는 목록 페이지만 `false` 준다 (§4-2). 나머지 페이지는 넘기지 않으면 기본값(켬)이 적용된다.
1268
+ **셸의 `SPage` 는 모든 페이지가 공유하므로, 페이지마다 달라지는 것은 페이지가 셸에 알려야 한다.** 위처럼 프레임 컴포넌트가 `scrollEndSpacing` · `contentHeight` 를 받아 그대로 넘기고, 페이지네이션이 있는 목록 페이지만 `contentHeight="fill"` 준다 (§4-2). 나머지 페이지는 넘기지 않으면 기본값(`auto`, 스크롤 끝 여백 켬)이 적용된다.
1183
1269
 
1184
1270
  **상단바 배치는 `header` 가 정한다.** 요소 순서가 달라지므로 슬롯을 채우기 전에 어느 쪽인지부터 정한다.
1185
1271
 
@@ -1237,7 +1323,9 @@ import { SModalOutlet } from 'sellmate-design-system-react';
1237
1323
  - **페이지 제목 줄에는 이 페이지의 주요 액션을 두지 않는다.** 부가적인 것만 `header.slot` 에 `SButton size="sm"` 으로 온다 (§4-1 "페이지 헤더 사용 규칙").
1238
1324
  - **주요 액션(등록 등)은 `STableBar` 의 `rightActions`** 에 둔다.
1239
1325
  - **선택 상태 액션(선택 삭제 등)은 `STableBar` 의 `actions`** 에 둔다. `actions` 슬롯은 건수 요약이 있으면 앞에 구분선(`SDivider`)을 **자동으로** 넣으므로 직접 구분선을 만들지 않는다.
1240
- - **페이지네이션이 있으면 스크롤 여백을 끈다** — `AppShell` 에 `scrollEndSpacing={false}` 넘긴다 (§2-2). 페이지네이션이 이미 "여기서 끝" 알려준다.
1326
+ - **페이지네이션이 있으면 본문이 남은 높이를 채우게 한다** — `AppShell` 에 `contentHeight="fill"` 넘긴다. 페이지가 통째로 스크롤되면 페이지네이션이 화면 밖으로 밀려 "여기서 끝" 읽히지 않는다. `fill` 이면 **표만 자기 안에서 스크롤하고 페이지네이션은 하단에 고정**된다.
1327
+ - 본문 루트에 `h-full min-h-0` 으로 세로 축을 잇고, 남은 높이를 먹을 `STable` 에 `min-h-0 flex-1` 을 준다. 이 사슬이 하나라도 끊기면 표가 높이를 못 잡는다.
1328
+ - `fill` 에서는 페이지가 스크롤하지 않으므로 **`scrollEndSpacing` 은 무시된다** — 따로 끄지 않는다 (§2-2).
1241
1329
 
1242
1330
  ```tsx
1243
1331
  import {
@@ -1285,9 +1373,10 @@ export default function ProductListPage() {
1285
1373
  // 이 페이지의 주요 액션이 아니라 부가 액션 — slot 은 sm 버튼으로만 채운다
1286
1374
  slot: <SButton size="sm" color="neutral" outline label="이용 가이드" onClick={openGuide} />,
1287
1375
  }}
1288
- scrollEndSpacing={false} // 페이지네이션이 있으므로 끈다
1376
+ contentHeight="fill" // 표가 남은 높이를 채우고 페이지네이션이 하단에 고정된다
1289
1377
  >
1290
- <div className="flex flex-col gap-sd-12">
1378
+ {/* h-full min-h-0 → STable 의 min-h-0 flex-1 로 세로 축이 이어진다 */}
1379
+ <div className="flex h-full min-h-0 flex-col gap-sd-12">
1291
1380
  {/* 필터 — search 를 켜면 우측에 검색 패널이 붙는다 */}
1292
1381
  <SKeyValueTable
1293
1382
  fields={filterFields}
@@ -1312,7 +1401,9 @@ export default function ProductListPage() {
1312
1401
  }
1313
1402
  />
1314
1403
 
1404
+ {/* 남은 높이를 채우고 본문만 스크롤한다 — 페이지네이션 바는 표 안에서 하단 고정 */}
1315
1405
  <STable
1406
+ className="min-h-0 flex-1"
1316
1407
  columns={columns}
1317
1408
  rows={rows}
1318
1409
  rowKey="id"
@@ -1398,7 +1489,7 @@ export default function ProductCreatePage() {
1398
1489
 
1399
1490
  - 조회 값은 `type: 'text'` 행으로 표시한다. **상태·분류 태그도 별도 영역이 아니라 표의 한 행**으로 넣는다 (`render` 에 `STag`).
1400
1491
  - 행이 많아지면 **유형별로 섹션을 나누고, 각 섹션을 `SSectionHeaderCard` 로 감싼다.**
1401
- 합성 컴포넌트라 `SSectionHeaderCard.Header` / `SSectionHeaderCard.Body` 자식으로 쓴다.
1492
+ 섹션 제목은 `title` prop 으로, 바디 여백은 `padding` prop 으로 준다.
1402
1493
  - **수정·삭제 버튼은 하단에 둔다.** 내용이 짧아 우측 상단에 두는 변형도 있으나 기본은 하단이다.
1403
1494
 
1404
1495
  ```tsx
@@ -1431,24 +1522,18 @@ export default function ProductDetailPage() {
1431
1522
  // 목록에서 들어온 상세 페이지 — onBack 으로 뒤로가기를 준다
1432
1523
  <AppShell header={{ fix: true, title: '클래식 셔츠', onBack: goList }}>
1433
1524
  <div className="flex flex-col gap-sd-12">
1434
- <SSectionHeaderCard>
1435
- <SSectionHeaderCard.Header title="기본 정보" marker thickness="accent" />
1436
- <SSectionHeaderCard.Body>
1437
- <SKeyValueTable fields={basicFields} values={product} />
1438
- </SSectionHeaderCard.Body>
1525
+ <SSectionHeaderCard title="기본 정보" marker thickness="accent">
1526
+ <SKeyValueTable fields={basicFields} values={product} />
1439
1527
  </SSectionHeaderCard>
1440
1528
 
1441
- <SSectionHeaderCard>
1442
- {/* 헤더 우측에 액션이 필요하면 slot 을 쓴다 */}
1443
- <SSectionHeaderCard.Header
1444
- title="가격 정보"
1445
- marker
1446
- helpText={['부가세 포함 금액입니다.']}
1447
- slot={<SButton size="sm" color="secondary" label="이력" onClick={openHistory} />}
1448
- />
1449
- <SSectionHeaderCard.Body>
1450
- <SKeyValueTable fields={priceFields} values={product} />
1451
- </SSectionHeaderCard.Body>
1529
+ {/* 헤더 우측에 액션이 필요하면 slot 을 쓴다 */}
1530
+ <SSectionHeaderCard
1531
+ title="가격 정보"
1532
+ marker
1533
+ helpText={['부가세 포함 금액입니다.']}
1534
+ slot={<SButton size="sm" color="secondary" label="이력" onClick={openHistory} />}
1535
+ >
1536
+ <SKeyValueTable fields={priceFields} values={product} />
1452
1537
  </SSectionHeaderCard>
1453
1538
 
1454
1539
  {/* 액션은 하단 — 목록(되돌리기)은 왼쪽 끝, 실행 액션은 오른쪽 끝 */}
@@ -1497,8 +1582,8 @@ export default function ProductDetailPage() {
1497
1582
  - [ ] 텍스트 회색 위계를 순차 적용했는가 (기본 → `text-fg-secondary` → `text-fg-tertiary`, 단계 건너뛰기 ❌)
1498
1583
  - [ ] `SPage`·`SPopup` 의 기본 패딩을 `p-sd-*` 로 덮어쓰지 않았는가, 블록·섹션 **간격**이 `gap-sd-12` 인가 (`gap-sd-16`/`gap-sd-24` ❌ — 24 는 안쪽 여백에만 열린다)
1499
1584
  - [ ] 섹션·패널의 안쪽 **여백**이 §2-2 판정과 맞는가 (덩어리 두 종류 → 16 / 세 종류 이상 → 24, 서면 16)
1500
- - [ ] `SSectionHeaderCard.Body` 의 여백을 `p-sd-*` 가 아니라 `padding` prop 으로 줬는가
1501
- - [ ] 자체 스크롤하는 패널의 하단에 `pb-[var(--cmp-pageBody-padding-scrollEnd)]` 이 있는가, 페이지네이션 있는 목록에서 `scrollEndSpacing={false}` 넘겼는가
1585
+ - [ ] `SSectionHeaderCard` 의 여백을 `p-sd-*` 가 아니라 `padding` prop 으로 줬는가
1586
+ - [ ] 자체 스크롤하는 패널의 하단에 `pb-[var(--cmp-pageBody-padding-scrollEnd)]` 이 있는가, 페이지네이션 있는 목록에서 `contentHeight="fill"` 넘기고 `h-full min-h-0` → `STable` 의 `min-h-0 flex-1` 사슬을 이었는가
1502
1587
  - [ ] 같은 컴포넌트를 나열할 때 §2-2 그룹 간격을 썼는가 (체크박스 가로 `gap-sd-24` 등)
1503
1588
  - [ ] 페이지가 §4의 표준 골격에서 시작했는가
1504
1589
  - [ ] `header.fix` 가 프로젝트 전체와 같은 값인가 (다른 페이지와 다르게 섞어 쓰지 않았는가, §4-1)
@@ -13,7 +13,6 @@
13
13
  | `activeKeys?` | `string[]` | — | 노출 필터 key 목록. 지정하면 제어 컴포넌트로 동작 — "필터 추가"로 고른 필드를 이 배열에 직접 넣어줘야 칩이 나타난다(onActiveKeysChange에서 받은 keys로 갱신). 지정하지 않으면 필드별 defaultActive 값을 기준으로 컴포넌트가 내부 상태로 관리하는 비제어 방식으로 동작한다 |
14
14
  | `label?` | `string` | `'검색 필터'` | 좌측 태그 텍스트 |
15
15
  | `showLabel?` | `boolean` | `false` | 좌측 태그(label)·구분선 표시 여부 |
16
- | `showAddButton?` | `boolean` | `true` | 필터 추가 버튼 표시 여부 |
17
16
  | `showReset?` | `boolean` | `true` | 검색 초기화 링크 표시 여부 |
18
17
  | `disabled?` | `boolean` | `false` | 바 비활성 상태 |
19
18
  | `className?` | `string` | — | |
@@ -48,6 +47,7 @@
48
47
  - [SIcon](../SIcon)
49
48
  - [SRadio](../SRadio)
50
49
  - [SRadioButton](../SRadioButton)
50
+ - [SSelect](../SSelect)
51
51
  - [STag](../STag)
52
52
  - [STextLink](../STextLink)
53
53
  - [STooltip](../STooltip)
@@ -62,6 +62,7 @@ graph TD;
62
62
  SChipFilter --> SIcon
63
63
  SChipFilter --> SRadio
64
64
  SChipFilter --> SRadioButton
65
+ SChipFilter --> SSelect
65
66
  SChipFilter --> STag
66
67
  SChipFilter --> STextLink
67
68
  SChipFilter --> STooltip
@@ -1,14 +1,15 @@
1
1
  import { type CSSProperties, type ReactNode } from 'react';
2
2
  import { type SDateRangeValue } from '../SDateRangePicker';
3
- export type SChipFilterType = 'single' | 'multi' | 'keyword' | 'date' | 'custom';
3
+ export type SChipFilterType = 'single' | 'multi' | 'keyword' | 'date' | 'period' | 'custom';
4
4
  export type SChipFilterOptionValue = string | number;
5
5
  export type SChipFilterMatchMode = 'contains' | 'exact' | 'excludes';
6
+ export type SChipFilterPeriodUnit = 'day' | 'month' | 'quarter' | 'half' | 'year' | 'custom';
6
7
  export interface SChipFilterOption {
7
8
  value: SChipFilterOptionValue;
8
9
  label: string;
9
10
  disabled?: boolean;
10
11
  }
11
- /** date 필드의 프리셋 라디오 항목 (오늘/지난 7일/사용자 지정 등) */
12
+ /** date/period 필드의 프리셋 라디오 항목 (오늘/지난 7일/일별/월별/사용자 지정 등) */
12
13
  export interface SChipFilterDatePreset {
13
14
  /** 프리셋 식별자 */
14
15
  value: string;
@@ -24,9 +25,14 @@ export interface SChipFilterKeywordValue {
24
25
  keywords: string[];
25
26
  mode: SChipFilterMatchMode;
26
27
  }
28
+ /** period 필드 값 — 선택 단위(unit)와 그 단위의 입력값(value)을 함께 보관한다 */
29
+ export interface SChipFilterPeriodValue {
30
+ unit: SChipFilterPeriodUnit;
31
+ value?: string | number | SDateRangeValue | null;
32
+ }
27
33
  /** custom 필드가 자유롭게 담는 값. 형태를 강제하지 않는다 — render에서 직접 정의한 그대로 읽고 쓴다 */
28
34
  export type SChipFilterCustomValue = Record<string, unknown>;
29
- export type SChipFilterValue = SChipFilterOptionValue | SChipFilterOptionValue[] | SDateRangeValue | SChipFilterKeywordValue | SChipFilterCustomValue | null | undefined;
35
+ export type SChipFilterValue = SChipFilterOptionValue | SChipFilterOptionValue[] | SDateRangeValue | SChipFilterKeywordValue | SChipFilterPeriodValue | SChipFilterCustomValue | null | undefined;
30
36
  export type SChipFilterValueMap = Record<string, SChipFilterValue>;
31
37
  export interface SChipFilterField {
32
38
  /** 필터 식별자 */
@@ -40,6 +46,8 @@ export interface SChipFilterField {
40
46
  /** 필수 필터 표시. true면 값이 비어 있을 때 기본값(defaultValue 또는 타입별 내장 기본값)이
41
47
  * 자동으로 채워지고, clearable은 현재 값이 기본값과 같을 땐 숨겨지며 클릭 시 기본값으로 되돌아간다 */
42
48
  required?: boolean;
49
+ /** 고정 필터. true면 activeKeys/defaultActive와 무관하게 항상 노출되고 "필터 추가" 목록에는 나타나지 않는다 */
50
+ fix?: boolean;
43
51
  /** 초기값 및 clearable 클릭 시 되돌아갈 값. required 여부와 무관하게 적용된다 — 값이 비어 있으면
44
52
  * 마운트(또는 "필터 추가"로 활성화) 시 이 값이 자동으로 채워진다. required인데 지정하지 않으면
45
53
  * 타입별 내장 기본값(single: 첫 번째 옵션, date: 오늘 날짜)을 대신 쓴다 */
@@ -62,7 +70,8 @@ export interface SChipFilterField {
62
70
  selectable?: [string, string];
63
71
  /** date 프리셋의 "사용자 지정" 기간 선택 시 최대 선택 일수 */
64
72
  maxRange?: number;
65
- /** date 필드의 프리셋 라디오 목록. 지정하지 않으면 기존처럼 단일 캘린더 트리거로 동작한다 */
73
+ /** date/period 필드의 프리셋 라디오 목록. date에서 지정하지 않으면 기존처럼 단일 캘린더 트리거로 동작하고,
74
+ * period에서 지정하지 않으면 일별·월별·분기별·반기별·연도별·사용자 지정 기본 목록을 쓴다 */
66
75
  presets?: SChipFilterDatePreset[];
67
76
  /** presets 사용 시 UI 형태 — true: 필터 바에 세그먼트 라디오로 인라인 노출(팝오버 없음),
68
77
  * false(기본): 칩 클릭 시 팝오버 안에 세로 라디오 목록(+사용자 지정 선택 시 기간 피커) */
@@ -147,8 +156,6 @@ export interface SChipFilterProps {
147
156
  label?: string;
148
157
  /** 좌측 태그(label)·구분선 표시 여부 */
149
158
  showLabel?: boolean;
150
- /** 필터 추가 버튼 표시 여부 */
151
- showAddButton?: boolean;
152
159
  /** 검색 초기화 링크 표시 여부 */
153
160
  showReset?: boolean;
154
161
  /** 바 비활성 상태 */
@@ -2,6 +2,25 @@
2
2
 
3
3
  > 자동 생성 문서 — `npm run docs:gen`. 소스: 각 컴포넌트의 Props/Handle 인터페이스 + import 의존성.
4
4
 
5
+ ### SDatePickerMonthListbox
6
+
7
+ #### Props
8
+
9
+ | Prop | Type | Default | Description |
10
+ |------|------|---------|-------------|
11
+ | `value?` | `string \| null` | — | 선택 월 (YYYY-MM) |
12
+ | `anchorYear?` | `number` | — | 연도 리스트가 처음 열릴 때 중앙에 둘 연도. 기본값은 올해 |
13
+ | `selectable?` | `[string, string]` | — | 선택 가능 범위 [시작, 종료] |
14
+ | `className?` | `string` | — | |
15
+ | `style?` | `CSSProperties` | — | |
16
+
17
+ #### Events
18
+
19
+ | Event | Type | Description |
20
+ |-------|------|-------------|
21
+ | `onValueChange` | `(value: string) => void` | |
22
+ | `onMonthSelect` | `(value: string) => void` | 월 컬럼에서 월을 선택했을 때 호출 |
23
+
5
24
  ### SDatePicker
6
25
 
7
26
  #### Props
@@ -39,6 +58,24 @@
39
58
  | `onValueChange` | `(value: string) => void` | 선택 변경 (sdUpdate) |
40
59
  | `onViewChange` | `(view: { year: number; month: number }) => void` | 보이는 연·월 변경 (sdViewChange) |
41
60
 
61
+ ### SDatePickerYearListbox
62
+
63
+ #### Props
64
+
65
+ | Prop | Type | Default | Description |
66
+ |------|------|---------|-------------|
67
+ | `value?` | `string \| null` | — | 선택 연도 (YYYY) |
68
+ | `anchorYear?` | `number` | — | 리스트가 처음 열릴 때 중앙에 둘 연도. 기본값은 올해 |
69
+ | `selectable?` | `[string, string]` | — | 선택 가능 범위 [시작, 종료] |
70
+ | `className?` | `string` | — | |
71
+ | `style?` | `CSSProperties` | — | |
72
+
73
+ #### Events
74
+
75
+ | Event | Type | Description |
76
+ |-------|------|-------------|
77
+ | `onValueChange` | `(year: string) => void` | |
78
+
42
79
  ## Dependencies
43
80
 
44
81
  ### Used by
@@ -41,5 +41,35 @@ export interface SDatePickerProps {
41
41
  className?: string;
42
42
  style?: CSSProperties;
43
43
  }
44
+ export interface SDatePickerYearListboxProps {
45
+ /** 선택 연도 (YYYY) */
46
+ value?: string | null;
47
+ /** 리스트가 처음 열릴 때 중앙에 둘 연도. 기본값은 올해 */
48
+ anchorYear?: number;
49
+ /** 선택 가능 범위 [시작, 종료] */
50
+ selectable?: [string, string];
51
+ onValueChange?: (year: string) => void;
52
+ className?: string;
53
+ style?: CSSProperties;
54
+ }
55
+ /** SDatePickerYearListbox — SDatePicker의 연도 선택 listbox 조각.
56
+ * 자체 트리거/팝오버 없이 연도 리스트만 필요한 커스텀 조합에 재사용한다. */
57
+ export declare function SDatePickerYearListbox({ value, anchorYear, selectable, onValueChange, className, style, }: SDatePickerYearListboxProps): import("react").JSX.Element;
58
+ export interface SDatePickerMonthListboxProps {
59
+ /** 선택 월 (YYYY-MM) */
60
+ value?: string | null;
61
+ /** 연도 리스트가 처음 열릴 때 중앙에 둘 연도. 기본값은 올해 */
62
+ anchorYear?: number;
63
+ /** 선택 가능 범위 [시작, 종료] */
64
+ selectable?: [string, string];
65
+ onValueChange?: (value: string) => void;
66
+ /** 월 컬럼에서 월을 선택했을 때 호출 */
67
+ onMonthSelect?: (value: string) => void;
68
+ className?: string;
69
+ style?: CSSProperties;
70
+ }
71
+ /** SDatePickerMonthListbox — SDatePicker의 월 선택 listbox 조각.
72
+ * 자체 트리거/팝오버 없이 연도+월 리스트만 필요한 커스텀 조합에 재사용한다. */
73
+ export declare function SDatePickerMonthListbox({ value, anchorYear, selectable, onValueChange, onMonthSelect, className, style, }: SDatePickerMonthListboxProps): import("react").JSX.Element;
44
74
  /** SDatePicker — sd-date-picker 포팅. SField + SPopover(SCalendar). */
45
75
  export declare function SDatePicker({ value, onValueChange, onViewChange, mode, size, placeholder, selectable, disabled, width, name, rules, status, label, labelWidth, icon, iconColor, labelTooltip, labelTooltipProps, addonLabel, addonAlign, hint, error, errorMessage, className, style, }: SDatePickerProps): import("react").JSX.Element;
@@ -1 +1 @@
1
- export { SDatePicker, type SDatePickerMode, type SDatePickerProps, type SDatePickerSize, } from './SDatePicker';
1
+ export { SDatePicker, SDatePickerMonthListbox, SDatePickerYearListbox, type SDatePickerMode, type SDatePickerMonthListboxProps, type SDatePickerProps, type SDatePickerSize, type SDatePickerYearListboxProps, } from './SDatePicker';
@@ -30,6 +30,7 @@
30
30
  | `readOnly?` | `boolean` | `false` | 읽기 전용 (회색 배경) |
31
31
  | `width?` | `number \| string` | — | 컨트롤 너비 (숫자=px). 지정하면 필드가 부모 폭을 다 먹지 않고 (레이블 + width) 만큼만 차지한다. 다른 요소와 나란히 놓으려면 부모를 flex 로 두면 된다. |
32
32
  | `minWidth?` | `number \| string` | — | 컨트롤 최소 너비 (숫자=px). 하한선만 지정하며 필드는 계속 부모 폭을 채운다 |
33
+ | `maxWidth?` | `number \| string` | — | 컨트롤 최대 너비 (숫자=px). 상한선만 지정하며 필드는 그 아래에서 부모 폭을 채운다. `width` 와 달리 hug 로 전환하지 않는다 — 부모가 좁으면 같이 좁아지고, 넓어도 여기서 멈춘다. 값 길이가 정해진 컨트롤(날짜·시간)이 `width="100%"` 를 받아 행 전체로 늘어나는 것을 막는 데 쓴다. **라벨은 상한에 들어가지 않는다** — 라벨은 컨트롤의 형제라 이 상한 밖이다. `addonLabel` 은 테두리 박스 안이라 상한을 나눠 먹으므로, `labelWidth`(= addon 폭)만큼 상한을 자동으로 늘린다. addon 이 있는데 `labelWidth` 가 없으면 폭을 알 수 없어 상한을 걸지 않는다 — 컨트롤이 잘리는 것보다 넓은 편이 낫다. |
33
34
  | `multiline?` | `boolean` | `false` | 멀티라인(textarea) — 컨트롤 높이를 고정하지 않고 min-height만 적용 |
34
35
  | `borderless?` | `boolean` | `false` | 테두리 박스 제거 (inline 컨트롤용) — border/배경/hover·focus 강조만 사라지고 label·hint·errorMessage 등 나머지 필드 구성은 그대로 동작한다. |
35
36
  | `children?` | `ReactNode` | — | 실제 컨트롤 (input/select 등) — 테두리 없이 렌더, 테두리는 SField가 제공 |
@@ -63,6 +63,18 @@ export interface SFieldProps {
63
63
  width?: number | string;
64
64
  /** 컨트롤 최소 너비 (숫자=px). 하한선만 지정하며 필드는 계속 부모 폭을 채운다 */
65
65
  minWidth?: number | string;
66
+ /**
67
+ * 컨트롤 최대 너비 (숫자=px). 상한선만 지정하며 필드는 그 아래에서 부모 폭을 채운다.
68
+ *
69
+ * `width` 와 달리 hug 로 전환하지 않는다 — 부모가 좁으면 같이 좁아지고, 넓어도 여기서 멈춘다.
70
+ * 값 길이가 정해진 컨트롤(날짜·시간)이 `width="100%"` 를 받아 행 전체로 늘어나는 것을 막는 데 쓴다.
71
+ *
72
+ * **라벨은 상한에 들어가지 않는다** — 라벨은 컨트롤의 형제라 이 상한 밖이다.
73
+ * `addonLabel` 은 테두리 박스 안이라 상한을 나눠 먹으므로, `labelWidth`(= addon 폭)만큼
74
+ * 상한을 자동으로 늘린다. addon 이 있는데 `labelWidth` 가 없으면 폭을 알 수 없어 상한을
75
+ * 걸지 않는다 — 컨트롤이 잘리는 것보다 넓은 편이 낫다.
76
+ */
77
+ maxWidth?: number | string;
66
78
  /** 멀티라인(textarea) — 컨트롤 높이를 고정하지 않고 min-height만 적용 */
67
79
  multiline?: boolean;
68
80
  /**
@@ -12,6 +12,7 @@
12
12
  | `values?` | `Record<string, unknown>` | `{}` | field name을 key로 하는 값 객체 (`{ [name]: value }`). 지정 시 해당 field의 값으로 사용되며, field별 `options.value`보다 우선합니다. `onChange`의 `detail.values`와 함께 controlled 패턴으로 사용합니다. |
13
13
  | `search?` | `boolean` | `false` | 우측 검색 패널 |
14
14
  | `radius?` | `'default' \| 'useTop' \| 'full'` | `'default'` | border-radius 제어 |
15
+ | `bordered?` | `boolean` | `true` | 바깥 테두리. 기본은 `true`. `SSectionHeaderCard` 의 `padding="none"` 안에 넣어 카드 가장자리까지 채울 때 `false` 로 끈다 — 카드가 이미 바깥 테두리를 그리므로, 켜 두면 1px 두 개가 나란히 놓여 그 변만 2px 로 보인다. |
15
16
  | `className?` | `string` | — | |
16
17
  | `style?` | `CSSProperties` | — | |
17
18
 
@@ -104,6 +104,13 @@ export interface SKeyValueTableProps {
104
104
  search?: boolean;
105
105
  /** border-radius 제어 */
106
106
  radius?: 'default' | 'useTop' | 'full';
107
+ /**
108
+ * 바깥 테두리. 기본은 `true`.
109
+ *
110
+ * `SSectionHeaderCard` 의 `padding="none"` 안에 넣어 카드 가장자리까지 채울 때 `false` 로 끈다 —
111
+ * 카드가 이미 바깥 테두리를 그리므로, 켜 두면 1px 두 개가 나란히 놓여 그 변만 2px 로 보인다.
112
+ */
113
+ bordered?: boolean;
107
114
  /** 값 변경 (sdChange) */
108
115
  onChange?: (detail: SKeyValueChangeDetail) => void;
109
116
  /** 검색 클릭 (sdSearch) */
@@ -112,5 +119,5 @@ export interface SKeyValueTableProps {
112
119
  style?: CSSProperties;
113
120
  }
114
121
  /** SKeyValueTable — sd-key-value-table 포팅. th/td 폼 레이아웃 + 필드 렌더러. */
115
- export declare function SKeyValueTable({ fields, values, search, radius, onChange, onSearch, className, style, }: SKeyValueTableProps): import("react").JSX.Element | null;
122
+ export declare function SKeyValueTable({ fields, values, search, radius, bordered, onChange, onSearch, className, style, }: SKeyValueTableProps): import("react").JSX.Element | null;
116
123
  export {};
@@ -28,6 +28,7 @@
28
28
  |------|------|---------|-------------|
29
29
  | `background?` | `SPageBackground` | `'frame'` | 페이지 배경 표면. frame=흰 콘텐츠 면, neutral=옅은 회색 면, screen=앱 바탕. 스크롤바 처리도 여기 묶여 있다 — 셋 다 구분선+트랙 배경이고, 트랙 색만 neutral 에서 흰색이 된다. |
30
30
  | `scrollEndSpacing?` | `boolean` | `true` | 스크롤 끝 여백. 마지막 항목이 창 하단에 붙어 "여기서 끝"이 안 읽히는 것을 막는다. 기본으로 켜져 있고, 페이지네이션처럼 끝을 알려주는 것이 이미 있으면 끈다(목록 페이지). |
31
+ | `contentHeight?` | `SPageContentHeight` | `'auto'` | 본문 높이 모드. 기본은 `auto` — 콘텐츠가 흐르고 넘치면 페이지가 스크롤한다. `fill` 은 본문이 남은 높이를 정확히 채우고 페이지는 스크롤하지 않는다. 표가 자기 안에서 스크롤하고 페이지네이션이 하단에 고정되는 목록 화면용이다. 본문 루트에 `h-full` 이 먹으므로 `<div className="flex h-full min-h-0 flex-col">` + `<STable className="min-h-0 flex-1" />` 구성이 성립한다. `fill` 에서는 페이지가 스크롤하지 않으므로 `scrollEndSpacing` 이 무시된다. |
31
32
  | `header?` | `SPageHeaderProps` | — | 페이지 타이틀 영역(pageHeader 포팅). 주면 스크롤·페이지 패딩 밖으로 빼내 상단에 고정 배치한다. `children` 은 항상 본문이다 — header 유무와 무관하게 같은 자리에 같은 뜻으로 들어간다. |
32
33
 
33
34
  ## Dependencies
@@ -1,5 +1,5 @@
1
1
  import { type HTMLAttributes } from 'react';
2
- import type { SPageBackground } from './page.config';
2
+ import type { SPageBackground, SPageContentHeight } from './page.config';
3
3
  import { type SPageHeaderProps } from './SPageHeader';
4
4
  export interface SPageProps extends HTMLAttributes<HTMLElement> {
5
5
  /**
@@ -12,6 +12,17 @@ export interface SPageProps extends HTMLAttributes<HTMLElement> {
12
12
  * 기본으로 켜져 있고, 페이지네이션처럼 끝을 알려주는 것이 이미 있으면 끈다(목록 페이지).
13
13
  */
14
14
  scrollEndSpacing?: boolean;
15
+ /**
16
+ * 본문 높이 모드. 기본은 `auto` — 콘텐츠가 흐르고 넘치면 페이지가 스크롤한다.
17
+ *
18
+ * `fill` 은 본문이 남은 높이를 정확히 채우고 페이지는 스크롤하지 않는다. 표가 자기 안에서
19
+ * 스크롤하고 페이지네이션이 하단에 고정되는 목록 화면용이다. 본문 루트에 `h-full` 이 먹으므로
20
+ * `<div className="flex h-full min-h-0 flex-col">` + `<STable className="min-h-0 flex-1" />`
21
+ * 구성이 성립한다.
22
+ *
23
+ * `fill` 에서는 페이지가 스크롤하지 않으므로 `scrollEndSpacing` 이 무시된다.
24
+ */
25
+ contentHeight?: SPageContentHeight;
15
26
  /**
16
27
  * 페이지 타이틀 영역(pageHeader 포팅). 주면 스크롤·페이지 패딩 밖으로 빼내 상단에 고정 배치한다.
17
28
  * `children` 은 항상 본문이다 — header 유무와 무관하게 같은 자리에 같은 뜻으로 들어간다.
@@ -1,3 +1,3 @@
1
1
  export { SPage, type SPageProps } from './SPage';
2
- export { PAGE_BACKGROUNDS, type SPageBackground } from './page.config';
2
+ export { PAGE_BACKGROUNDS, type SPageBackground, PAGE_CONTENT_HEIGHTS, type SPageContentHeight, } from './page.config';
3
3
  export { type SPageHeaderProps } from './SPageHeader';