sellmate-design-system-react 9.0.0-beta.20 → 9.0.0-beta.22

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
@@ -1153,6 +1153,25 @@ const columns: STableColumn[] = [
1153
1153
 
1154
1154
  `SSplitter` 의 구분선은 평소 자리만 잡고 칠해지지 않다가, 경계에 커서를 올리거나 포커스를 주면 그때 드러난다 — 조절 가능한 자리라는 신호다. **항상 보이는 선이 필요하면 `SDivider` 를 쓴다.** 선 색·두께·주변 여백은 토큰이 정하므로 직접 주지 않는다.
1155
1155
 
1156
+ **세로 구분선의 길이는 `height` prop 으로 준다 — `className` 의 `h-*` 로 주지 않는다.**
1157
+
1158
+ ```tsx
1159
+ // 줄 높이를 그대로 채운다 (기본)
1160
+ <div className="flex items-center gap-sd-8">
1161
+ <span>총 주문 3건</span>
1162
+ <SDivider vertical />
1163
+ <span>총 품목 12건</span>
1164
+ </div>
1165
+
1166
+ // 양옆 글자보다 짧은 선이 필요할 때만 길이를 정한다
1167
+ <SDivider vertical height={20} />
1168
+
1169
+ // 하지 말 것 — 선이 줄 맨 위에 붙는다
1170
+ <SDivider vertical className="h-sd-20" />
1171
+ ```
1172
+
1173
+ 길이를 주지 않으면 `align-self: stretch` 로 부모 줄 높이를 채우는데, `stretch` 는 높이가 `auto` 일 때만 늘린다. `className` 으로 높이를 정하면 `stretch` 가 조용히 무효가 되어 선이 위로 솟는다. `height` 는 길이와 교차축 가운데 정렬을 함께 적용하므로 이 함정이 없다 — 위·아래 정렬이 필요하면 `className="self-start"` 처럼 명시한다. (`sellmate/divider-vertical-height` 규칙이 잡는다.)
1174
+
1156
1175
  ```tsx
1157
1176
  <SSplitter defaultValue={30} limits={[20, 60]}>
1158
1177
  <SSplitter.Before>내비게이션</SSplitter.Before>
@@ -1352,6 +1371,22 @@ const [selectedId, setSelectedId] = useState<string>();
1352
1371
  - **카드 안에 카드를 겹치지 않는다.** 섹션 안을 더 나눠야 하면 `SDivider` 로 끊거나(§3-6) 섹션을 둘로 분리한다.
1353
1372
  - 안쪽 여백은 `SSectionHeaderCard` 의 `padding` prop 으로 준다 — `p-sd-*` 를 직접 주지 않는다 (§2-2).
1354
1373
 
1374
+ **카드가 자기 안에서 확정하는 액션을 가지면 하단 버튼을 `children` 에 직접 두지 않는다.** 두 카드 모두 모달·드로어와 같은 하단 액션 영역을 갖는다 — 주 액션은 `button`, 보조 버튼은 `footerLeft` 로 넘긴다(§3-3-4 와 같은 규칙). 배경·상단 구분선·좌우 여백·양끝 분리가 컴포넌트 규칙대로 잡히고, 좌우 끝이 헤더·본문과 맞는다. `footerLeft` 는 슬롯이라 `SButton` 을 직접 배치하며 `size="md"` 를 명시한다(§3-5-2).
1375
+
1376
+ ```tsx
1377
+ {/* 섹션 안에서 바로 수정·저장하는 인라인 폼 섹션 (§4-4) */}
1378
+ <SSectionHeaderCard
1379
+ title="배송지"
1380
+ marker
1381
+ footerLeft={<SButton color="neutral" outline size="md" label="취소" onClick={reset} />}
1382
+ button={{ label: '저장', onClick: save }}
1383
+ >
1384
+ <SKeyValueTable … />
1385
+ </SSectionHeaderCard>
1386
+ ```
1387
+
1388
+ **페이지 전체를 확정하는 액션은 카드 푸터가 아니라 페이지 하단에 둔다.** 카드 푸터는 **그 카드 안에서 닫히는 액션**의 자리다 — 여러 섹션을 한 번에 저장하는 버튼을 마지막 카드의 푸터에 넣으면 그 카드에만 걸리는 액션으로 읽힌다. 이때는 §4-4 처럼 카드 밖 하단 줄에 둔다.
1389
+
1355
1390
  #### 3-7-9. SLinearProgress vs SCircleProgress
1356
1391
 
1357
1392
  | 판별 | 사용 |
@@ -1645,24 +1680,28 @@ export default function ProductListPage() {
1645
1680
 
1646
1681
  행 높이를 줄이는 것은 `dense` 다. 세로 여백만 줄고 좌우 패딩은 그대로라, 값이 잘리지 않으면서 한 화면에 들어가는 행 수가 늘어난다.
1647
1682
 
1648
- **어느 쪽이 편한지는 화면이 아니라 사용자가 안다.** 그래서 목록 페이지는 밀도를 고정하지 말고 `useDensityToggle` 고를 있게 둔다페이지네이션 바 우측(rows per page 셀렉트 왼쪽)에 `좁게 보기` · `넓게 보기` 링크가 붙는다.
1683
+ **어느 쪽이 편한지는 화면이 아니라 사용자가 안다.** 그래서 `dense` 밀도를 고정하는 스위치가 아니라 **시작 밀도이자 밀도 토글의 스위치**다켜면 하단 바 우측(rows per page 셀렉트 왼쪽)에 `좁게 보기` · `넓게 보기` 링크가 붙는다. 켜고 끄는 별도 prop 은 없다.
1649
1684
 
1650
1685
  ```tsx
1651
- // 사용자가 고른 밀도는 다음 방문에도 남는 것이 자연스럽다 — 저장은 페이지 몫이다
1652
- const [dense, setDense] = useState(() => loadPref('list.dense', true));
1686
+ // 좁게 시작하고, 사용자가 바꾸는 밀도는 표가 알아서 들고 간다
1687
+ <STable dense useRowsPerPageSelect pagination={{ currentPage, lastPage }} />;
1688
+
1689
+ // 사용자가 고른 밀도를 다음 방문에도 남기려는 화면만 받아서 저장한다.
1690
+ // 저장한 값은 시작 밀도로만 돌려준다 — 되돌려 넣지 않는다
1691
+ const [initialDense] = useState(() => loadPref('list.dense', true));
1653
1692
 
1654
1693
  <STable
1655
- dense={dense}
1656
- onDenseChange={next => { setDense(next); savePref('list.dense', next); }}
1657
- useDensityToggle
1694
+ dense={initialDense}
1695
+ onDenseChange={next => savePref('list.dense', next)}
1658
1696
  useRowsPerPageSelect
1659
1697
  pagination={{ currentPage, lastPage }}
1660
1698
  />;
1661
1699
  ```
1662
1700
 
1663
- - **밀도는 `STable` 이 갖지 않는다.** `dense` 곧 현재 상태이고, `onDenseChange` 없이 `useDensityToggle` 켜면 눌러도 아무 일도 일어나지 않는다.
1664
- - **토글은 페이지네이션이 있을 때만 나타난다** 사는 곳이 바이기 때문이다. 페이지네이션 없는 표에서 밀도를 고르게 하려면 `STableBar` 쪽에 직접 둔다.
1665
- - 라벨과 아이콘은 현재 상태가 아니라 **누르면 되는 상태**를 가리킨다. `dense` `넓게 보기` 다.
1701
+ - **누른 뒤의 밀도는 `STable` 이 내부 상태로 들고 간다.** `onDenseChange` 없이도 토글은 동작한다. 저장이 필요한 화면만 받아서 저장하면 된다.
1702
+ - **`dense` `onDenseChange` 값을 되돌려 넣지 않는다.** 토글을 붙일지는 prop 정하므로, 넓게 순간 `dense` `false` 되면 토글이 사라져 다시 좁힐 길이 없다. prop 값을 바꿔 넘기는 것은 외부 버튼 등으로 밀도를 **되돌릴 때**만 쓴다 — 내부 밀도가 그 값으로 맞춰진다.
1703
+ - **`dense` 페이지네이션이 없어도 토글이 나온다** 하단 바를 토글만 담아 그린다. `dense` 아니면 토글도 없다.
1704
+ - 라벨과 아이콘은 현재 상태가 아니라 **누르면 되는 상태**를 가리킨다. 좁게 보고 있으면 `넓게 보기` 다.
1666
1705
 
1667
1706
  ### 4-3. 폼 페이지 (등록/수정)
1668
1707
 
@@ -1750,7 +1789,7 @@ export default function ProductCreatePage() {
1750
1789
  섹션 제목은 `title` prop 으로, 바디 여백은 `padding` prop 으로 준다.
1751
1790
  - **수정·삭제 버튼은 하단에 둔다.** 내용이 짧아 우측 상단에 두는 변형도 있으나 기본은 하단이다.
1752
1791
  - **상세 페이지도 `contentHeight="fill"` 이 기본이다**(§2-2). 섹션이 많아 페이지가 실제로 스크롤되는 화면에서만 `auto` + `scrollEndSpacing` 을 켠다.
1753
- - **섹션마다 독립 인라인 폼이 있는 형태**도 상세 페이지의 변형이다. 섹션 안에서 바로 수정·저장하게 하는 화면인데, 이때 버튼 강조는 **섹션 단위가 아니라 페이지 단위로 판단한다** — §3-5-1 의 "`secondary` 연속 배치 금지"는 섹션이 다르면 적용되지 않는다.
1792
+ - **섹션마다 독립 인라인 폼이 있는 형태**도 상세 페이지의 변형이다. 섹션 안에서 바로 수정·저장하게 하는 화면인데, 이때 버튼 강조는 **섹션 단위가 아니라 페이지 단위로 판단한다** — §3-5-1 의 "`secondary` 연속 배치 금지"는 섹션이 다르면 적용되지 않는다. 그 섹션 안에서 닫히는 저장·취소는 `SSectionHeaderCard` 의 `button`·`footerLeft` 로 넘긴다 (§3-7-8). 아래 예처럼 **페이지 전체를 확정하는 버튼은 카드 밖 하단 줄**에 둔다 — 둘을 섞지 않는다.
1754
1793
 
1755
1794
  ```tsx
1756
1795
  import {
@@ -1827,6 +1866,14 @@ export default function ProductDetailPage() {
1827
1866
  | Prop (Body) | 용도 |
1828
1867
  | --- | --- |
1829
1868
  | `padding` | 안쪽 여백 — `'default'`(기본) / `'wide'` / `'none'`. 판정은 §2-2 "섹션·패널 안쪽 여백". `p-sd-*` 를 직접 주지 않는다 |
1869
+ | `background` | 본문 바탕 — `'frame'`(기본) / `'neutral'`. 판정은 §2-2 "본문 바탕 눌러앉히기" |
1870
+
1871
+ | Prop (Footer) | 용도 |
1872
+ | --- | --- |
1873
+ | `button` | 하단 액션 영역 우측 주 액션 (§3-7-8) |
1874
+ | `footerLeft` | 하단 액션 영역 좌측 슬롯 — 보조 버튼. `SButton` 에 `size="md"` 를 명시한다 |
1875
+
1876
+ 둘 중 하나라도 주면 하단 액션 영역이 렌더된다. 회색 바탕 + 상단 구분선이며 좌우 끝은 헤더에 맞는다 — 배경·여백을 직접 주지 않는다.
1830
1877
 
1831
1878
  **한 페이지에 섹션 카드가 둘 이상이면 `marker` 를 켠다.** 점은 섹션을 서로 구분할 대상이 여럿일 때만 의미가 있어, 카드가 하나뿐인 페이지에서는 켜지 않는다. 한 페이지 안에서는 켜거나 끄거나 전부 같게 간다.
1832
1879
 
@@ -1841,7 +1888,7 @@ export default function ProductDetailPage() {
1841
1888
  </SSectionHeaderCard>
1842
1889
  ```
1843
1890
 
1844
- 본문 래퍼는 `className` 을 받지 않으므로(여백은 `padding` prop 으로만 받는다) 루트에서 내려 준다. 흔한 구성은 아니다 — 대부분은 `STable` 이 자기 안에서 스크롤하므로 여기까지 갈 일이 없다.
1891
+ 본문 래퍼는 `className` 을 받지 않으므로(여백은 `padding` prop 으로만 받는다) 루트에서 내려 준다. **하단 액션 영역이 있으면 본문이 더 이상 마지막 자식이 아니다** — 그때는 `[&>div:nth-last-child(2)]` 로 겨눈다. 흔한 구성은 아니다 — 대부분은 `STable` 이 자기 안에서 스크롤하므로 여기까지 갈 일이 없다.
1845
1892
 
1846
1893
  ---
1847
1894
 
@@ -1870,7 +1917,7 @@ export default function ProductDetailPage() {
1870
1917
  - [ ] 필터·폼·상세 정보를 `SKeyValueTable` 로 만들었는가 (컨트롤을 `div` 로 나열하지 않았는가). 필터를 `SChipFilter` 로 했다면 §3-7-11 의 판정을 거쳤는가
1871
1918
  - [ ] 섹션 구분에 `SSectionHeaderCard` 를 썼는가 (직접 만든 카드가 아니라)
1872
1919
  - [ ] 목록의 주요 액션이 `STableBar` 의 `rightActions` 에 있는가 (페이지 제목 줄이 아니라)
1873
- - [ ] 목록 페이지 표에 `useDensityToggle` 로 밀도를 고를 있게 뒀는가, `onDenseChange` 함께 줬는가 (§4-2 — 핸들러 없이 켜면 눌러도 아무 일도 없다)
1920
+ - [ ] 목록 페이지 표에 `dense` 로 밀도 토글을 띄웠는가, 값을 `onDenseChange` 결과로 되돌려 넣지는 않았는가 (§4-2 — 되돌려 넣으면 넓게 순간 토글이 사라진다)
1874
1921
  - [ ] 상태 표시에 `STag size="sm"` 을 썼는가
1875
1922
  - [ ] 테이블에서 양을 나타내는 컬럼(금액·수량·개수 등)이 전부 `align: 'right'` 인가
1876
1923
  - [ ] 번호·코드·전화번호·일자 컬럼에 `align: 'center'` 를 **명시**했는가 (생략하면 좌측이 된다)
@@ -1892,7 +1939,7 @@ export default function ProductDetailPage() {
1892
1939
  - [ ] `SGhostButton` 의 `intent` 가 조작 성격과 맞는가 (되돌릴 수 없는 삭제만 `danger`, 진입·추가는 `action`, 나머지는 `default`)
1893
1940
  - [ ] 창을 띄울 때 §3-3-1 판별 순서를 따랐는가 (그 자체가 화면 → `SPopup` / 실행 여부만 확정 → `SModal.confirm` / 모달 안에서 작성 → `SActionModal`)
1894
1941
  - [ ] 작업용 모달을 `SActionModal` + `SModal.create` 로 만들었는가 (직접 오버레이 ❌)
1895
- - [ ] 모달·드로어의 하단 버튼을 본문이 아니라 `button` · `footerLeft` prop 으로 넘겼는가 (§3-3-4)
1942
+ - [ ] 모달·드로어의 하단 버튼을 본문이 아니라 `button` · `footerLeft` prop 으로 넘겼는가 (§3-3-4), 카드 안에서 닫히는 액션도 같은 prop 으로 넘겼는가 (§3-7-8)
1896
1943
  - [ ] 앱 부트스트랩의 Provider 안쪽에 `<SModalOutlet />` 이 한 번 렌더되어 있는가 (§4-1 — 없으면 모달 안에서 앱 훅이 죽는다), 그 대신으로 모달 컴포넌트를 Provider 로 다시 감싸지 않았는가
1897
1944
  - [ ] 고른 컴포넌트를 §2-0 의 제 층에 놓았는가 (요소를 `SPage` 에 직접 놓지 않았는가, 블록을 `div` 로 감싸지 않았는가)
1898
1945
  - [ ] §2-0 포함 규칙을 지켰는가 (카드 안 카드 ❌, 표 셀 안 블록 ❌)
package/README.md CHANGED
@@ -178,17 +178,18 @@ export default [
178
178
 
179
179
  **`error` 는 "지키지 않으면 깨지는 것" 에만 씁니다.** 앱 화면을 Tailwind 로 자유롭게 만드는 것은 정상이고, 디자인 시스템이 그 자유까지 막지 않습니다. 나머지는 권고(`warn`)입니다.
180
180
 
181
- | 규칙 | 기본 | 검출 대상 |
182
- | -------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
183
- | `sellmate/no-off-scale-spacing` | **error** | 스케일 밖 디자인 시스템 간격 (`gap-sd-13`) — Tailwind v4 에서 **조용히 무시되어** 스타일이 사라진다 |
184
- | `sellmate/no-raw-html-control` | **error** | 대응 컴포넌트가 있는 생 HTML (`<button>` `<input>` `<select>` `<textarea>` `<table>` `<form>` `<dialog>` `<hr>` `<details>` `<progress>`), `alert()`/`confirm()` — 디자인 시스템을 통째로 우회하는 경우 |
185
- | `sellmate/prefer-typo-preset` | warn | `text-14 font-bold` 같은 낱개 조합 `typo-*` 프리셋 |
186
- | `sellmate/component-group-gap` | warn | 같은 컴포넌트를 나열할 때의 그룹 간격 배열 방향에 따라 값이 다르다(체크박스 가로 24 / 세로 8) |
187
- | `sellmate/table-numeric-align` | warn | 수량 컬럼(금액·수량 등)에 `align: 'right'` 누락**`--fix` 자동 교정** |
188
- | `sellmate/require-locale-number` | warn | 수량 컬럼의 `toLocaleString()` 누락 — 자리 콤마 |
189
- | `sellmate/field-width-grade` | warn | 필드 폭이 `maxLength`(스키마 상한) 어긋남등급 미지정 · 등급 밖 폭 · 상한 대비 과부족 · 등급 px 직접 지정(**`--fix` 로 등급 이름 치환**) |
190
- | `sellmate/table-column-width` | warn | 컬럼 미지정(기본 120px 조용히 들어감) · px 아닌(`%` `clamp()`) · 값을 그리는 열의 `autoWidth` 오용 |
191
- | `sellmate/no-arbitrary-class` | **off** | 토큰이 있는 속성(색·타이포·간격·모서리) 임의 `text-[14px]`, `bg-[#eee]`. 고유 화면에는 정당한 사용이 많아 기본값은 끕니다 |
181
+ | 규칙 | 기본 | 검출 대상 |
182
+ | ---------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
183
+ | `sellmate/no-off-scale-spacing` | **error** | 스케일 밖 디자인 시스템 간격 (`gap-sd-13`) — Tailwind v4 에서 **조용히 무시되어** 스타일이 사라진다 |
184
+ | `sellmate/no-raw-html-control` | **error** | 대응 컴포넌트가 있는 생 HTML (`<button>` `<input>` `<select>` `<textarea>` `<table>` `<form>` `<dialog>` `<hr>` `<details>` `<progress>`), `alert()`/`confirm()` — 디자인 시스템을 통째로 우회하는 경우 |
185
+ | `sellmate/divider-vertical-height` | **error** | 세로 `SDivider` 의 높이를 `className="h-*"` 로 준 경우 — `align-self: stretch` 무효가 되어 선이 줄 맨 위에 붙는다. `height` prop 으로 준다(**`--fix` 로 치환**) |
186
+ | `sellmate/prefer-typo-preset` | warn | `text-14 font-bold` 같은 낱개 조합 `typo-*` 프리셋 |
187
+ | `sellmate/component-group-gap` | warn | 같은 컴포넌트를 나열할 때의 그룹 간격배열 방향에 따라 값이 다르다(체크박스 가로 24 / 세로 8) |
188
+ | `sellmate/table-numeric-align` | warn | 수량 컬럼(금액·수량 )`align: 'right'` 누락 — **`--fix` 자동 교정** |
189
+ | `sellmate/require-locale-number` | warn | 수량 컬럼의 `toLocaleString()` 누락 자리 콤마 |
190
+ | `sellmate/field-width-grade` | warn | 필드 폭이 `maxLength`(스키마 상한)와 어긋남 등급 미지정 · 등급 · 상한 대비 과부족 · 등급 px 직접 지정(**`--fix` 로 등급 이름 치환**) |
191
+ | `sellmate/table-column-width` | warn | 컬럼 미지정(기본 120px 이 조용히 들어감) · px 아닌 폭(`%` `clamp()`) · 값을 그리는 열의 `autoWidth` 오용 |
192
+ | `sellmate/no-arbitrary-class` | **off** | 토큰이 있는 속성(색·타이포·간격·모서리)의 임의 값 — `text-[14px]`, `bg-[#eee]`. 앱 고유 화면에는 정당한 사용이 많아 기본값은 끕니다 |
192
193
 
193
194
  `className` 뿐 아니라 `cn()`/`clsx()` 인자, 템플릿 리터럴, 객체 키 안까지 검사합니다.
194
195
 
@@ -286,9 +287,9 @@ export default [
286
287
  한글은 전각이라 영숫자의 약 두 배를 먹으므로 **두 기준 사이에는 아무것도 보고하지 않습니다** —
287
288
  영숫자로 채워도 넘칠 때만 "좁다", 한글로 채워도 남을 때만 "넓다" 로 판정합니다.
288
289
 
289
- | 등급 | `SInput` (sm) | `SInput` (md) | `SNumberInput` (sm) | `SNumberInput` (sm, `useButton`) |
290
- | ---- | ------------- | ------------- | ------------------- | -------------------------------- |
291
- | `xs` | — | — | 영숫자 8 | 영숫자 2 |
290
+ | 등급 | `SInput` (sm) | `SInput` (md) | `SNumberInput` (sm) | `SNumberInput` (sm, `useButton`) |
291
+ | ---- | ------------------- | ------------------- | ------------------- | -------------------------------- |
292
+ | `xs` | — | — | 영숫자 8 | 영숫자 2 |
292
293
  | `sm` | 영숫자 14 / 한글 7 | 영숫자 11 / 한글 6 | 영숫자 14 | 영숫자 8 |
293
294
  | `md` | 영숫자 20 / 한글 11 | 영숫자 16 / 한글 9 | 영숫자 20 | 영숫자 14 |
294
295
  | `lg` | 영숫자 32 / 한글 17 | 영숫자 26 / 한글 14 | 영숫자 32 | 영숫자 26 |
@@ -9,4 +9,20 @@
9
9
  | Prop | Type | Default | Description |
10
10
  |------|------|---------|-------------|
11
11
  | `bordered?` | `boolean` | `false` | 테두리 표시 여부 |
12
+ | `footerLeft?` | `ReactNode` | — | 하단 액션 영역 좌측 슬롯. 보조 버튼(취소·목록 등)이 오는 자리다. `footerLeft` 또는 `button` 중 하나라도 있으면 하단 액션 영역이 렌더된다. |
13
+ | `button?` | `SFooterButton` | — | 하단 액션 영역 우측 주 액션 버튼 |
14
+ | `footerClassName?` | `string` | — | 하단 액션 영역 클래스 |
12
15
 
16
+ ## Dependencies
17
+
18
+ ### Depends on
19
+
20
+ - [SFooter](../SFooter)
21
+
22
+ ### Graph
23
+
24
+ ```mermaid
25
+ graph TD;
26
+ SCard --> SFooter
27
+ style SCard fill:#f9f,stroke:#333,stroke-width:4px
28
+ ```
@@ -1,7 +1,22 @@
1
- import { type HTMLAttributes } from 'react';
1
+ import { type HTMLAttributes, type ReactNode } from 'react';
2
+ import { type SFooterButton } from '../SFooter';
2
3
  export interface SCardProps extends HTMLAttributes<HTMLDivElement> {
3
4
  /** 테두리 표시 여부 */
4
5
  bordered?: boolean;
6
+ /**
7
+ * 하단 액션 영역 좌측 슬롯. 보조 버튼(취소·목록 등)이 오는 자리다.
8
+ * `footerLeft` 또는 `button` 중 하나라도 있으면 하단 액션 영역이 렌더된다.
9
+ */
10
+ footerLeft?: ReactNode;
11
+ /** 하단 액션 영역 우측 주 액션 버튼 */
12
+ button?: SFooterButton;
13
+ /** 하단 액션 영역 클래스 */
14
+ footerClassName?: string;
5
15
  }
6
- /** SCard — sd-card 포팅. 라운드 8px, 흰 배경, 옵션 테두리. 콘텐츠는 라운드 경계에서 잘린다. */
16
+ /**
17
+ * SCard — sd-card 포팅. 라운드 8px, 흰 배경, 옵션 테두리. 콘텐츠는 라운드 경계에서 잘린다.
18
+ *
19
+ * 하단 액션은 `children` 에 직접 두지 않고 `footerLeft`·`button` 으로 넘긴다 — 배경·구분선·여백과
20
+ * 양끝 분리가 컴포넌트 규칙대로 잡히는 자리다.
21
+ */
7
22
  export declare const SCard: import("react").ForwardRefExoticComponent<SCardProps & import("react").RefAttributes<HTMLDivElement>>;
@@ -9,6 +9,7 @@
9
9
  | Prop | Type | Default | Description |
10
10
  |------|------|---------|-------------|
11
11
  | `vertical?` | `boolean` | `false` | true면 수직 분할선, false면 수평 분할선 |
12
+ | `height?` | `number \| string` | — | 수직 분할선의 길이. 숫자는 px, 문자열은 CSS 값(토큰 var 참조 등)으로 쓴다. 주지 않으면 `align-self: stretch` 로 부모 줄 높이를 그대로 채운다. 값을 주면 그 높이로 고정하고 교차축 가운데(`self-center`)에 맞춘다 — `align-self: stretch` 는 높이가 auto 일 때만 늘리므로, 높이를 정하는 순간 정렬을 함께 정해야 한다. 그래서 높이는 `className="h-*"` 가 아니라 이 prop 으로 준다. 수평 분할선에는 적용되지 않는다 (두께는 1px 로 고정). |
12
13
 
13
14
  ## Dependencies
14
15
 
@@ -2,6 +2,17 @@ import { type HTMLAttributes } from 'react';
2
2
  export interface SDividerProps extends HTMLAttributes<HTMLHRElement> {
3
3
  /** true면 수직 분할선, false면 수평 분할선 */
4
4
  vertical?: boolean;
5
+ /**
6
+ * 수직 분할선의 길이. 숫자는 px, 문자열은 CSS 값(토큰 var 참조 등)으로 쓴다.
7
+ *
8
+ * 주지 않으면 `align-self: stretch` 로 부모 줄 높이를 그대로 채운다. 값을 주면
9
+ * 그 높이로 고정하고 교차축 가운데(`self-center`)에 맞춘다 — `align-self: stretch`
10
+ * 는 높이가 auto 일 때만 늘리므로, 높이를 정하는 순간 정렬을 함께 정해야 한다.
11
+ * 그래서 높이는 `className="h-*"` 가 아니라 이 prop 으로 준다.
12
+ *
13
+ * 수평 분할선에는 적용되지 않는다 (두께는 1px 로 고정).
14
+ */
15
+ height?: number | string;
5
16
  }
6
17
  /** SDivider — sd-divider 포팅. 색상은 `--sys-color-divider-default`. */
7
18
  export declare const SDivider: import("react").ForwardRefExoticComponent<SDividerProps & import("react").RefAttributes<HTMLHRElement>>;
@@ -42,8 +42,10 @@ export interface SFooterButton {
42
42
  ### Used by
43
43
 
44
44
  - [SActionModal](../SActionModal)
45
+ - [SCard](../SCard)
45
46
  - [SDrawer](../SDrawer)
46
47
  - [SPopup](../SPopup)
48
+ - [SSectionHeaderCard](../SSectionHeaderCard)
47
49
 
48
50
  ### Depends on
49
51
 
@@ -55,7 +57,9 @@ export interface SFooterButton {
55
57
  graph TD;
56
58
  SFooter --> SButton
57
59
  SActionModal --> SFooter
60
+ SCard --> SFooter
58
61
  SDrawer --> SFooter
59
62
  SPopup --> SFooter
63
+ SSectionHeaderCard --> SFooter
60
64
  style SFooter fill:#f9f,stroke:#333,stroke-width:4px
61
65
  ```
@@ -11,7 +11,7 @@ export interface SFooterButton {
11
11
  }
12
12
  /**
13
13
  * @internal
14
- * 모달·드로어·팝업 하단 영역 공통 props.
14
+ * 모달·드로어·팝업·카드 하단 영역 공통 props.
15
15
  */
16
16
  export interface SFooterProps {
17
17
  /** 배경색 */
@@ -27,10 +27,11 @@ export interface SFooterProps {
27
27
  style?: CSSProperties;
28
28
  }
29
29
  /**
30
- * SFooter — overlay 계열 내부 전용 footer.
30
+ * SFooter — 오버레이·카드 내부 전용 footer.
31
31
  *
32
32
  * @internal
33
- * SActionModal/SConfirmModal/SDrawer/SPopup 의 하단 액션 영역만 공통화한다.
33
+ * SActionModal/SConfirmModal/SDrawer/SPopup 과 SCard/SSectionHeaderCard 의 하단 액션 영역만
34
+ * 공통화한다. 소비자는 호스트 컴포넌트의 `footerLeft`·`button` prop 으로만 닿는다 —
34
35
  * 패키지 public export 에 추가하지 말 것.
35
36
  */
36
37
  export declare function SFooter({ bg, left, button, children, className, leftClassName, style, }: SFooterProps): import("react").JSX.Element;
@@ -19,6 +19,9 @@
19
19
  | `headerClassName?` | `string` | — | 헤더 영역 클래스 |
20
20
  | `padding?` | `SSectionHeaderCardBodyPadding` | — | 바디 안쪽 여백. 기본은 `default`. 성격이 다른 요소가 세 종류 이상 섞인 영역에만 `wide`, 표를 가장자리까지 채울 때만 `none`. |
21
21
  | `background?` | `SSectionHeaderCardBodyBackground` | — | 본문 배경. 기본은 `frame` — 카드가 깐 흰 면을 그대로 쓴다. `neutral` 은 바탕을 한 단계 눌러앉혀, 흰 면 덩어리(표·리스트)가 여럿일 때 그것들이 **"면 위에 놓인 객체"로 읽히게** 한다. 위계를 한 단계 더 주고 싶을 때 고르는 선택지이며, **기본값이 틀린 것은 아니다** — 표·리스트는 테두리·라운드·헤더 줄과 간격을 이미 갖고 있어 흰 바탕에서도 경계가 읽힌다. 깔아도 **효과가 없는** 자리가 있다 — 덩어리가 가장자리까지 차는 경우(`padding="none"` 으로 표를 채우면 바탕이 완전히 가려진다), 덩어리에 회색 면이 섞인 경우(그 덩어리가 바탕에 묻힌다), 맨 텍스트나 폼 컨트롤만 있는 본문(떠오를 흰 면이 없다). |
22
+ | `footerLeft?` | `ReactNode` | — | 하단 액션 영역 좌측 슬롯. 보조 버튼(취소·목록 등)이 오는 자리다. `footerLeft` 또는 `button` 중 하나라도 있으면 하단 액션 영역이 렌더된다. |
23
+ | `button?` | `SFooterButton` | — | 하단 액션 영역 우측 주 액션 버튼 |
24
+ | `footerClassName?` | `string` | — | 하단 액션 영역 클래스 |
22
25
  | `children?` | `SSectionHeaderCardChildren` | — | 바디 콘텐츠. 특정 컴포넌트 타입으로 제한하지 않습니다. |
23
26
 
24
27
  ## Types
@@ -58,6 +61,7 @@ export type SSectionHeaderCardChildren = ReactNode;
58
61
  ### Depends on
59
62
 
60
63
  - [SBadge](../SBadge)
64
+ - [SFooter](../SFooter)
61
65
  - [SIcon](../SIcon)
62
66
  - [STooltip](../STooltip)
63
67
 
@@ -66,6 +70,7 @@ export type SSectionHeaderCardChildren = ReactNode;
66
70
  ```mermaid
67
71
  graph TD;
68
72
  SSectionHeaderCard --> SBadge
73
+ SSectionHeaderCard --> SFooter
69
74
  SSectionHeaderCard --> SIcon
70
75
  SSectionHeaderCard --> STooltip
71
76
  style SSectionHeaderCard fill:#f9f,stroke:#333,stroke-width:4px
@@ -1,4 +1,5 @@
1
1
  import { type HTMLAttributes, type ReactNode } from 'react';
2
+ import { type SFooterButton } from '../SFooter';
2
3
  export type SSectionHeaderCardBodyChildren = ReactNode;
3
4
  export type SSectionHeaderCardChildren = ReactNode;
4
5
  export type SSectionHeaderCardThickness = false | 'default' | 'accent';
@@ -42,6 +43,15 @@ export interface SSectionHeaderCardProps extends Omit<HTMLAttributes<HTMLDivElem
42
43
  * 맨 텍스트나 폼 컨트롤만 있는 본문(떠오를 흰 면이 없다).
43
44
  */
44
45
  background?: SSectionHeaderCardBodyBackground;
46
+ /**
47
+ * 하단 액션 영역 좌측 슬롯. 보조 버튼(취소·목록 등)이 오는 자리다.
48
+ * `footerLeft` 또는 `button` 중 하나라도 있으면 하단 액션 영역이 렌더된다.
49
+ */
50
+ footerLeft?: ReactNode;
51
+ /** 하단 액션 영역 우측 주 액션 버튼 */
52
+ button?: SFooterButton;
53
+ /** 하단 액션 영역 클래스 */
54
+ footerClassName?: string;
45
55
  /** 바디 콘텐츠. 특정 컴포넌트 타입으로 제한하지 않습니다. */
46
56
  children?: SSectionHeaderCardChildren;
47
57
  }
@@ -75,7 +75,6 @@ export type SSelectType = 'default' | 'multi' | 'default_depth' | 'multi_depth';
75
75
 
76
76
  ### Used by
77
77
 
78
- - [SChipFilter](../SChipFilter)
79
78
  - [SKeyValueTable](../SKeyValueTable)
80
79
  - [STable](../STable)
81
80
 
@@ -94,7 +93,6 @@ graph TD;
94
93
  SSelect --> SIcon
95
94
  SSelect --> SPortal
96
95
  SSelect --> SSearchInput
97
- SChipFilter --> SSelect
98
96
  SKeyValueTable --> SSelect
99
97
  STable --> SSelect
100
98
  style SSelect fill:#f9f,stroke:#333,stroke-width:4px
@@ -24,12 +24,11 @@
24
24
  | `noDataLabel?` | `string` | `'데이터가 없습니다.'` | |
25
25
  | `noDataSlot?` | `ReactNode` | — | 데이터가 없을 때 body 영역 전체를 대체하는 슬롯. 지정하면 `noDataLabel` 대신 이 콘텐츠가 헤더 아래 영역을 채우며, 버튼 등 인터랙션도 동작한다. |
26
26
  | `isLoading?` | `boolean` | `false` | |
27
- | `dense?` | `boolean` | `false` | 행 높이를 좁게 (세로 여백만 줄인다 — 좌우 패딩은 그대로) |
27
+ | `dense?` | `boolean` | `false` | 행 높이를 좁게 (세로 여백만 줄인다 — 좌우 패딩은 그대로). **시작 밀도이자 밀도 토글의 스위치다.** 켜면 하단 바 우측에 `좁게 보기` · `넓게 보기` 토글이 붙는다 — 페이지네이션이 없으면 이 바를 토글만 담아 그린다. 누른 뒤의 밀도는 테이블이 내부 상태로 들고 가므로 `onDenseChange` 를 받지 않아도 토글은 동작하고, 넓게 본 뒤에도 토글은 그대로 남는다(붙일지는 이 prop 이 정한다). 이 prop 값이 바뀌면 내부 밀도도 그 값으로 맞춰진다. |
28
28
  | `noHover?` | `boolean` | `false` | true면 행에 마우스를 올려도 hover 배경(grey_05)을 표시하지 않는다 |
29
29
  | `pagination?` | `STablePagination` | — | 페이지네이션 (있으면 하단 표시) |
30
30
  | `useInternalPagination?` | `boolean` | `false` | 테이블 내부에서 페이지네이션을 직접 관리 (rows를 내부 슬라이싱) |
31
31
  | `useRowsPerPageSelect?` | `boolean` | `false` | 페이지당 행 수 셀렉트 표시 |
32
- | `useDensityToggle?` | `boolean` | `false` | 페이지네이션 바에 밀도 토글(`좁게 보기` · `넓게 보기`) 표시. **페이지네이션이 있을 때만 나타난다** — 토글이 사는 곳이 그 바이기 때문이다. `onDenseChange` 와 함께 준다. 밀도는 컴포넌트가 갖지 않으므로, 핸들러 없이 켜면 눌러도 아무 일도 일어나지 않는다. |
33
32
  | `rowsPerPageOption?` | `SSelectOption[]` | `DEFAULT_ROWS_PER_PAGE_OPTION` | |
34
33
  | `useVirtualScroll?` | `boolean` | `false` | 가상 스크롤 |
35
34
  | `rowHeight?` | `number` | — | |
@@ -45,7 +44,7 @@
45
44
  |-------|------|-------------|
46
45
  | `onSelectedChange` | `(rows: SRow[]) => void` | |
47
46
  | `onSortChange` | `(sort: STableSort \| null) => void` | 정렬 헤더 클릭 (`asc → desc → 해제` 3단). 해제되면 `null` 이 온다. 다중 정렬은 1차 안에서 지원하지 않는다. |
48
- | `onDenseChange` | `(dense: boolean) => void` | 밀도 변경 (`useDensityToggle` 띄운 토글을 눌렀을 때). 컴포넌트는 밀도 상태를 갖지 않는다 `dense` 가 곧 현재 상태이고, 그 진실은 페이지에 있다. 사용자가 고른 밀도를 다음 방문까지 기억해 두는 것(로컬 저장 등) 페이지 몫이다. |
47
+ | `onDenseChange` | `(dense: boolean) => void` | 밀도 변경 (하단 바의 밀도 토글을 눌렀을 때). 표시는 테이블이 알아서 바꾸므로 받지 않아도 되고, 사용자가 고른 밀도를 다음 방문까지 기억해 두려는(로컬 저장 등) 페이지만 받으면 된다. |
49
48
  | `onPageChange` | `(page: number) => void` | |
50
49
  | `onRowsPerPageChange` | `(perPage: number) => void` | |
51
50
  | `onVirtualUpdate` | `(range: { from: number; to: number }) => void` | |
@@ -218,12 +218,20 @@ export interface STableProps {
218
218
  */
219
219
  noDataSlot?: ReactNode;
220
220
  isLoading?: boolean;
221
- /** 행 높이를 좁게 (세로 여백만 줄인다 — 좌우 패딩은 그대로) */
221
+ /**
222
+ * 행 높이를 좁게 (세로 여백만 줄인다 — 좌우 패딩은 그대로).
223
+ *
224
+ * **시작 밀도이자 밀도 토글의 스위치다.** 켜면 하단 바 우측에 `좁게 보기` · `넓게 보기` 토글이
225
+ * 붙는다 — 페이지네이션이 없으면 이 바를 토글만 담아 그린다.
226
+ *
227
+ * 누른 뒤의 밀도는 테이블이 내부 상태로 들고 가므로 `onDenseChange` 를 받지 않아도 토글은
228
+ * 동작하고, 넓게 본 뒤에도 토글은 그대로 남는다(붙일지는 이 prop 이 정한다). 이 prop 값이
229
+ * 바뀌면 내부 밀도도 그 값으로 맞춰진다.
230
+ */
222
231
  dense?: boolean;
223
232
  /**
224
- * 밀도 변경 (`useDensityToggle` 띄운 토글을 눌렀을 때). 컴포넌트는 밀도 상태를 갖지 않는다
225
- * `dense` 현재 상태이고, 진실은 페이지에 있다. 사용자가 고른 밀도를 다음 방문까지
226
- * 기억해 두는 것(로컬 저장 등)도 페이지 몫이다.
233
+ * 밀도 변경 (하단 바의 밀도 토글을 눌렀을 때). 표시는 테이블이 알아서 바꾸므로 받지 않아도 되고,
234
+ * 사용자가 고른 밀도를 다음 방문까지 기억해 두려는(로컬 저장 등) 페이지만 받으면 된다.
227
235
  */
228
236
  onDenseChange?: (dense: boolean) => void;
229
237
  /** true면 행에 마우스를 올려도 hover 배경(grey_05)을 표시하지 않는다 */
@@ -235,14 +243,6 @@ export interface STableProps {
235
243
  useInternalPagination?: boolean;
236
244
  /** 페이지당 행 수 셀렉트 표시 */
237
245
  useRowsPerPageSelect?: boolean;
238
- /**
239
- * 페이지네이션 바에 밀도 토글(`좁게 보기` · `넓게 보기`) 표시.
240
- *
241
- * **페이지네이션이 있을 때만 나타난다** — 토글이 사는 곳이 그 바이기 때문이다.
242
- * `onDenseChange` 와 함께 준다. 밀도는 컴포넌트가 갖지 않으므로, 핸들러 없이 켜면 눌러도
243
- * 아무 일도 일어나지 않는다.
244
- */
245
- useDensityToggle?: boolean;
246
246
  rowsPerPageOption?: SSelectOption[];
247
247
  onRowsPerPageChange?: (perPage: number) => void;
248
248
  /** 가상 스크롤 */