sellmate-design-system-react 9.0.0-beta.21 → 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 +33 -10
- package/README.md +15 -14
- package/dist/components/SDivider/README.md +1 -0
- package/dist/components/SDivider/SDivider.d.ts +11 -0
- package/dist/components/STable/README.md +2 -3
- package/dist/components/STable/STable.d.ts +12 -12
- package/dist/index.cjs +30 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +30 -25
- package/dist/index.js.map +1 -1
- package/dist/llms-full.txt +36 -13
- package/dist/llms.txt +33 -10
- package/dist/styles.css +20 -3
- package/eslint/index.mjs +4 -0
- package/eslint/rules/divider-vertical-height.mjs +137 -0
- package/package.json +1 -1
package/dist/llms-full.txt
CHANGED
|
@@ -1161,6 +1161,25 @@ const columns: STableColumn[] = [
|
|
|
1161
1161
|
|
|
1162
1162
|
`SSplitter` 의 구분선은 평소 자리만 잡고 칠해지지 않다가, 경계에 커서를 올리거나 포커스를 주면 그때 드러난다 — 조절 가능한 자리라는 신호다. **항상 보이는 선이 필요하면 `SDivider` 를 쓴다.** 선 색·두께·주변 여백은 토큰이 정하므로 직접 주지 않는다.
|
|
1163
1163
|
|
|
1164
|
+
**세로 구분선의 길이는 `height` prop 으로 준다 — `className` 의 `h-*` 로 주지 않는다.**
|
|
1165
|
+
|
|
1166
|
+
```tsx
|
|
1167
|
+
// 줄 높이를 그대로 채운다 (기본)
|
|
1168
|
+
<div className="flex items-center gap-sd-8">
|
|
1169
|
+
<span>총 주문 3건</span>
|
|
1170
|
+
<SDivider vertical />
|
|
1171
|
+
<span>총 품목 12건</span>
|
|
1172
|
+
</div>
|
|
1173
|
+
|
|
1174
|
+
// 양옆 글자보다 짧은 선이 필요할 때만 길이를 정한다
|
|
1175
|
+
<SDivider vertical height={20} />
|
|
1176
|
+
|
|
1177
|
+
// 하지 말 것 — 선이 줄 맨 위에 붙는다
|
|
1178
|
+
<SDivider vertical className="h-sd-20" />
|
|
1179
|
+
```
|
|
1180
|
+
|
|
1181
|
+
길이를 주지 않으면 `align-self: stretch` 로 부모 줄 높이를 채우는데, `stretch` 는 높이가 `auto` 일 때만 늘린다. `className` 으로 높이를 정하면 `stretch` 가 조용히 무효가 되어 선이 위로 솟는다. `height` 는 길이와 교차축 가운데 정렬을 함께 적용하므로 이 함정이 없다 — 위·아래 정렬이 필요하면 `className="self-start"` 처럼 명시한다. (`sellmate/divider-vertical-height` 규칙이 잡는다.)
|
|
1182
|
+
|
|
1164
1183
|
```tsx
|
|
1165
1184
|
<SSplitter defaultValue={30} limits={[20, 60]}>
|
|
1166
1185
|
<SSplitter.Before>내비게이션</SSplitter.Before>
|
|
@@ -1669,24 +1688,28 @@ export default function ProductListPage() {
|
|
|
1669
1688
|
|
|
1670
1689
|
행 높이를 줄이는 것은 `dense` 다. 세로 여백만 줄고 좌우 패딩은 그대로라, 값이 잘리지 않으면서 한 화면에 들어가는 행 수가 늘어난다.
|
|
1671
1690
|
|
|
1672
|
-
**어느 쪽이 편한지는 화면이 아니라 사용자가 안다.** 그래서
|
|
1691
|
+
**어느 쪽이 편한지는 화면이 아니라 사용자가 안다.** 그래서 `dense` 는 밀도를 고정하는 스위치가 아니라 **시작 밀도이자 밀도 토글의 스위치**다 — 켜면 하단 바 우측(rows per page 셀렉트 왼쪽)에 `좁게 보기` · `넓게 보기` 링크가 붙는다. 켜고 끄는 별도 prop 은 없다.
|
|
1673
1692
|
|
|
1674
1693
|
```tsx
|
|
1675
|
-
// 사용자가
|
|
1676
|
-
|
|
1694
|
+
// 좁게 시작하고, 사용자가 바꾸는 밀도는 표가 알아서 들고 간다
|
|
1695
|
+
<STable dense useRowsPerPageSelect pagination={{ currentPage, lastPage }} />;
|
|
1696
|
+
|
|
1697
|
+
// 사용자가 고른 밀도를 다음 방문에도 남기려는 화면만 받아서 저장한다.
|
|
1698
|
+
// 저장한 값은 시작 밀도로만 돌려준다 — 되돌려 넣지 않는다
|
|
1699
|
+
const [initialDense] = useState(() => loadPref('list.dense', true));
|
|
1677
1700
|
|
|
1678
1701
|
<STable
|
|
1679
|
-
dense={
|
|
1680
|
-
onDenseChange={next =>
|
|
1681
|
-
useDensityToggle
|
|
1702
|
+
dense={initialDense}
|
|
1703
|
+
onDenseChange={next => savePref('list.dense', next)}
|
|
1682
1704
|
useRowsPerPageSelect
|
|
1683
1705
|
pagination={{ currentPage, lastPage }}
|
|
1684
1706
|
/>;
|
|
1685
1707
|
```
|
|
1686
1708
|
|
|
1687
|
-
-
|
|
1688
|
-
-
|
|
1689
|
-
-
|
|
1709
|
+
- **누른 뒤의 밀도는 `STable` 이 내부 상태로 들고 간다.** `onDenseChange` 없이도 토글은 동작한다. 저장이 필요한 화면만 받아서 저장하면 된다.
|
|
1710
|
+
- **`dense` 에 `onDenseChange` 값을 되돌려 넣지 않는다.** 토글을 붙일지는 이 prop 이 정하므로, 넓게 본 순간 `dense` 가 `false` 가 되면 토글이 사라져 다시 좁힐 길이 없다. prop 값을 바꿔 넘기는 것은 외부 버튼 등으로 밀도를 **되돌릴 때**만 쓴다 — 내부 밀도가 그 값으로 맞춰진다.
|
|
1711
|
+
- **`dense` 면 페이지네이션이 없어도 토글이 나온다** — 하단 바를 토글만 담아 그린다. `dense` 가 아니면 토글도 없다.
|
|
1712
|
+
- 라벨과 아이콘은 현재 상태가 아니라 **누르면 되는 상태**를 가리킨다. 좁게 보고 있으면 `넓게 보기` 다.
|
|
1690
1713
|
|
|
1691
1714
|
### 4-3. 폼 페이지 (등록/수정)
|
|
1692
1715
|
|
|
@@ -1902,7 +1925,7 @@ export default function ProductDetailPage() {
|
|
|
1902
1925
|
- [ ] 필터·폼·상세 정보를 `SKeyValueTable` 로 만들었는가 (컨트롤을 `div` 로 나열하지 않았는가). 필터를 `SChipFilter` 로 했다면 §3-7-11 의 판정을 거쳤는가
|
|
1903
1926
|
- [ ] 섹션 구분에 `SSectionHeaderCard` 를 썼는가 (직접 만든 카드가 아니라)
|
|
1904
1927
|
- [ ] 목록의 주요 액션이 `STableBar` 의 `rightActions` 에 있는가 (페이지 제목 줄이 아니라)
|
|
1905
|
-
- [ ] 목록 페이지 표에 `
|
|
1928
|
+
- [ ] 목록 페이지 표에 `dense` 로 밀도 토글을 띄웠는가, 그 값을 `onDenseChange` 결과로 되돌려 넣지는 않았는가 (§4-2 — 되돌려 넣으면 넓게 본 순간 토글이 사라진다)
|
|
1906
1929
|
- [ ] 상태 표시에 `STag size="sm"` 을 썼는가
|
|
1907
1930
|
- [ ] 테이블에서 양을 나타내는 컬럼(금액·수량·개수 등)이 전부 `align: 'right'` 인가
|
|
1908
1931
|
- [ ] 번호·코드·전화번호·일자 컬럼에 `align: 'center'` 를 **명시**했는가 (생략하면 좌측이 된다)
|
|
@@ -3337,6 +3360,7 @@ export type SDateRangePickerSize = SFieldSize;
|
|
|
3337
3360
|
| Prop | Type | Default | Description |
|
|
3338
3361
|
|------|------|---------|-------------|
|
|
3339
3362
|
| `vertical?` | `boolean` | `false` | true면 수직 분할선, false면 수평 분할선 |
|
|
3363
|
+
| `height?` | `number \| string` | — | 수직 분할선의 길이. 숫자는 px, 문자열은 CSS 값(토큰 var 참조 등)으로 쓴다. 주지 않으면 `align-self: stretch` 로 부모 줄 높이를 그대로 채운다. 값을 주면 그 높이로 고정하고 교차축 가운데(`self-center`)에 맞춘다 — `align-self: stretch` 는 높이가 auto 일 때만 늘리므로, 높이를 정하는 순간 정렬을 함께 정해야 한다. 그래서 높이는 `className="h-*"` 가 아니라 이 prop 으로 준다. 수평 분할선에는 적용되지 않는다 (두께는 1px 로 고정). |
|
|
3340
3364
|
|
|
3341
3365
|
## Dependencies
|
|
3342
3366
|
|
|
@@ -6261,12 +6285,11 @@ export const STEPPER_SIZES = ['sm', 'lg'] as const;
|
|
|
6261
6285
|
| `noDataLabel?` | `string` | `'데이터가 없습니다.'` | |
|
|
6262
6286
|
| `noDataSlot?` | `ReactNode` | — | 데이터가 없을 때 body 영역 전체를 대체하는 슬롯. 지정하면 `noDataLabel` 대신 이 콘텐츠가 헤더 아래 영역을 채우며, 버튼 등 인터랙션도 동작한다. |
|
|
6263
6287
|
| `isLoading?` | `boolean` | `false` | |
|
|
6264
|
-
| `dense?` | `boolean` | `false` | 행 높이를 좁게 (세로 여백만 줄인다 — 좌우 패딩은 그대로) |
|
|
6288
|
+
| `dense?` | `boolean` | `false` | 행 높이를 좁게 (세로 여백만 줄인다 — 좌우 패딩은 그대로). **시작 밀도이자 밀도 토글의 스위치다.** 켜면 하단 바 우측에 `좁게 보기` · `넓게 보기` 토글이 붙는다 — 페이지네이션이 없으면 이 바를 토글만 담아 그린다. 누른 뒤의 밀도는 테이블이 내부 상태로 들고 가므로 `onDenseChange` 를 받지 않아도 토글은 동작하고, 넓게 본 뒤에도 토글은 그대로 남는다(붙일지는 이 prop 이 정한다). 이 prop 값이 바뀌면 내부 밀도도 그 값으로 맞춰진다. |
|
|
6265
6289
|
| `noHover?` | `boolean` | `false` | true면 행에 마우스를 올려도 hover 배경(grey_05)을 표시하지 않는다 |
|
|
6266
6290
|
| `pagination?` | `STablePagination` | — | 페이지네이션 (있으면 하단 표시) |
|
|
6267
6291
|
| `useInternalPagination?` | `boolean` | `false` | 테이블 내부에서 페이지네이션을 직접 관리 (rows를 내부 슬라이싱) |
|
|
6268
6292
|
| `useRowsPerPageSelect?` | `boolean` | `false` | 페이지당 행 수 셀렉트 표시 |
|
|
6269
|
-
| `useDensityToggle?` | `boolean` | `false` | 페이지네이션 바에 밀도 토글(`좁게 보기` · `넓게 보기`) 표시. **페이지네이션이 있을 때만 나타난다** — 토글이 사는 곳이 그 바이기 때문이다. `onDenseChange` 와 함께 준다. 밀도는 컴포넌트가 갖지 않으므로, 핸들러 없이 켜면 눌러도 아무 일도 일어나지 않는다. |
|
|
6270
6293
|
| `rowsPerPageOption?` | `SSelectOption[]` | `DEFAULT_ROWS_PER_PAGE_OPTION` | |
|
|
6271
6294
|
| `useVirtualScroll?` | `boolean` | `false` | 가상 스크롤 |
|
|
6272
6295
|
| `rowHeight?` | `number` | — | |
|
|
@@ -6282,7 +6305,7 @@ export const STEPPER_SIZES = ['sm', 'lg'] as const;
|
|
|
6282
6305
|
|-------|------|-------------|
|
|
6283
6306
|
| `onSelectedChange` | `(rows: SRow[]) => void` | |
|
|
6284
6307
|
| `onSortChange` | `(sort: STableSort \| null) => void` | 정렬 헤더 클릭 (`asc → desc → 해제` 3단). 해제되면 `null` 이 온다. 다중 정렬은 1차 안에서 지원하지 않는다. |
|
|
6285
|
-
| `onDenseChange` | `(dense: boolean) => void` | 밀도 변경 (
|
|
6308
|
+
| `onDenseChange` | `(dense: boolean) => void` | 밀도 변경 (하단 바의 밀도 토글을 눌렀을 때). 표시는 테이블이 알아서 바꾸므로 받지 않아도 되고, 사용자가 고른 밀도를 다음 방문까지 기억해 두려는(로컬 저장 등) 페이지만 받으면 된다. |
|
|
6286
6309
|
| `onPageChange` | `(page: number) => void` | |
|
|
6287
6310
|
| `onRowsPerPageChange` | `(perPage: number) => void` | |
|
|
6288
6311
|
| `onVirtualUpdate` | `(range: { from: number; to: number }) => void` | |
|
package/dist/llms.txt
CHANGED
|
@@ -1162,6 +1162,25 @@ const columns: STableColumn[] = [
|
|
|
1162
1162
|
|
|
1163
1163
|
`SSplitter` 의 구분선은 평소 자리만 잡고 칠해지지 않다가, 경계에 커서를 올리거나 포커스를 주면 그때 드러난다 — 조절 가능한 자리라는 신호다. **항상 보이는 선이 필요하면 `SDivider` 를 쓴다.** 선 색·두께·주변 여백은 토큰이 정하므로 직접 주지 않는다.
|
|
1164
1164
|
|
|
1165
|
+
**세로 구분선의 길이는 `height` prop 으로 준다 — `className` 의 `h-*` 로 주지 않는다.**
|
|
1166
|
+
|
|
1167
|
+
```tsx
|
|
1168
|
+
// 줄 높이를 그대로 채운다 (기본)
|
|
1169
|
+
<div className="flex items-center gap-sd-8">
|
|
1170
|
+
<span>총 주문 3건</span>
|
|
1171
|
+
<SDivider vertical />
|
|
1172
|
+
<span>총 품목 12건</span>
|
|
1173
|
+
</div>
|
|
1174
|
+
|
|
1175
|
+
// 양옆 글자보다 짧은 선이 필요할 때만 길이를 정한다
|
|
1176
|
+
<SDivider vertical height={20} />
|
|
1177
|
+
|
|
1178
|
+
// 하지 말 것 — 선이 줄 맨 위에 붙는다
|
|
1179
|
+
<SDivider vertical className="h-sd-20" />
|
|
1180
|
+
```
|
|
1181
|
+
|
|
1182
|
+
길이를 주지 않으면 `align-self: stretch` 로 부모 줄 높이를 채우는데, `stretch` 는 높이가 `auto` 일 때만 늘린다. `className` 으로 높이를 정하면 `stretch` 가 조용히 무효가 되어 선이 위로 솟는다. `height` 는 길이와 교차축 가운데 정렬을 함께 적용하므로 이 함정이 없다 — 위·아래 정렬이 필요하면 `className="self-start"` 처럼 명시한다. (`sellmate/divider-vertical-height` 규칙이 잡는다.)
|
|
1183
|
+
|
|
1165
1184
|
```tsx
|
|
1166
1185
|
<SSplitter defaultValue={30} limits={[20, 60]}>
|
|
1167
1186
|
<SSplitter.Before>내비게이션</SSplitter.Before>
|
|
@@ -1670,24 +1689,28 @@ export default function ProductListPage() {
|
|
|
1670
1689
|
|
|
1671
1690
|
행 높이를 줄이는 것은 `dense` 다. 세로 여백만 줄고 좌우 패딩은 그대로라, 값이 잘리지 않으면서 한 화면에 들어가는 행 수가 늘어난다.
|
|
1672
1691
|
|
|
1673
|
-
**어느 쪽이 편한지는 화면이 아니라 사용자가 안다.** 그래서
|
|
1692
|
+
**어느 쪽이 편한지는 화면이 아니라 사용자가 안다.** 그래서 `dense` 는 밀도를 고정하는 스위치가 아니라 **시작 밀도이자 밀도 토글의 스위치**다 — 켜면 하단 바 우측(rows per page 셀렉트 왼쪽)에 `좁게 보기` · `넓게 보기` 링크가 붙는다. 켜고 끄는 별도 prop 은 없다.
|
|
1674
1693
|
|
|
1675
1694
|
```tsx
|
|
1676
|
-
// 사용자가
|
|
1677
|
-
|
|
1695
|
+
// 좁게 시작하고, 사용자가 바꾸는 밀도는 표가 알아서 들고 간다
|
|
1696
|
+
<STable dense useRowsPerPageSelect pagination={{ currentPage, lastPage }} />;
|
|
1697
|
+
|
|
1698
|
+
// 사용자가 고른 밀도를 다음 방문에도 남기려는 화면만 받아서 저장한다.
|
|
1699
|
+
// 저장한 값은 시작 밀도로만 돌려준다 — 되돌려 넣지 않는다
|
|
1700
|
+
const [initialDense] = useState(() => loadPref('list.dense', true));
|
|
1678
1701
|
|
|
1679
1702
|
<STable
|
|
1680
|
-
dense={
|
|
1681
|
-
onDenseChange={next =>
|
|
1682
|
-
useDensityToggle
|
|
1703
|
+
dense={initialDense}
|
|
1704
|
+
onDenseChange={next => savePref('list.dense', next)}
|
|
1683
1705
|
useRowsPerPageSelect
|
|
1684
1706
|
pagination={{ currentPage, lastPage }}
|
|
1685
1707
|
/>;
|
|
1686
1708
|
```
|
|
1687
1709
|
|
|
1688
|
-
-
|
|
1689
|
-
-
|
|
1690
|
-
-
|
|
1710
|
+
- **누른 뒤의 밀도는 `STable` 이 내부 상태로 들고 간다.** `onDenseChange` 없이도 토글은 동작한다. 저장이 필요한 화면만 받아서 저장하면 된다.
|
|
1711
|
+
- **`dense` 에 `onDenseChange` 값을 되돌려 넣지 않는다.** 토글을 붙일지는 이 prop 이 정하므로, 넓게 본 순간 `dense` 가 `false` 가 되면 토글이 사라져 다시 좁힐 길이 없다. prop 값을 바꿔 넘기는 것은 외부 버튼 등으로 밀도를 **되돌릴 때**만 쓴다 — 내부 밀도가 그 값으로 맞춰진다.
|
|
1712
|
+
- **`dense` 면 페이지네이션이 없어도 토글이 나온다** — 하단 바를 토글만 담아 그린다. `dense` 가 아니면 토글도 없다.
|
|
1713
|
+
- 라벨과 아이콘은 현재 상태가 아니라 **누르면 되는 상태**를 가리킨다. 좁게 보고 있으면 `넓게 보기` 다.
|
|
1691
1714
|
|
|
1692
1715
|
### 4-3. 폼 페이지 (등록/수정)
|
|
1693
1716
|
|
|
@@ -1903,7 +1926,7 @@ export default function ProductDetailPage() {
|
|
|
1903
1926
|
- [ ] 필터·폼·상세 정보를 `SKeyValueTable` 로 만들었는가 (컨트롤을 `div` 로 나열하지 않았는가). 필터를 `SChipFilter` 로 했다면 §3-7-11 의 판정을 거쳤는가
|
|
1904
1927
|
- [ ] 섹션 구분에 `SSectionHeaderCard` 를 썼는가 (직접 만든 카드가 아니라)
|
|
1905
1928
|
- [ ] 목록의 주요 액션이 `STableBar` 의 `rightActions` 에 있는가 (페이지 제목 줄이 아니라)
|
|
1906
|
-
- [ ] 목록 페이지 표에 `
|
|
1929
|
+
- [ ] 목록 페이지 표에 `dense` 로 밀도 토글을 띄웠는가, 그 값을 `onDenseChange` 결과로 되돌려 넣지는 않았는가 (§4-2 — 되돌려 넣으면 넓게 본 순간 토글이 사라진다)
|
|
1907
1930
|
- [ ] 상태 표시에 `STag size="sm"` 을 썼는가
|
|
1908
1931
|
- [ ] 테이블에서 양을 나타내는 컬럼(금액·수량·개수 등)이 전부 `align: 'right'` 인가
|
|
1909
1932
|
- [ ] 번호·코드·전화번호·일자 컬럼에 `align: 'center'` 를 **명시**했는가 (생략하면 좌측이 된다)
|
package/dist/styles.css
CHANGED
|
@@ -767,6 +767,9 @@
|
|
|
767
767
|
.my-auto {
|
|
768
768
|
margin-block: auto;
|
|
769
769
|
}
|
|
770
|
+
.my-sd-8 {
|
|
771
|
+
margin-block: var(--spacing-sd-8);
|
|
772
|
+
}
|
|
770
773
|
.-mt-8 {
|
|
771
774
|
margin-top: calc(var(--spacing) * -8);
|
|
772
775
|
}
|
|
@@ -803,6 +806,9 @@
|
|
|
803
806
|
.mt-\[var\(--cmp-guide-contents-gap\)\] {
|
|
804
807
|
margin-top: var(--cmp-guide-contents-gap);
|
|
805
808
|
}
|
|
809
|
+
.mt-auto {
|
|
810
|
+
margin-top: auto;
|
|
811
|
+
}
|
|
806
812
|
.mt-sd-2 {
|
|
807
813
|
margin-top: var(--spacing-sd-2);
|
|
808
814
|
}
|
|
@@ -839,6 +845,9 @@
|
|
|
839
845
|
.mb-\[24px\] {
|
|
840
846
|
margin-bottom: 24px;
|
|
841
847
|
}
|
|
848
|
+
.mb-auto {
|
|
849
|
+
margin-bottom: auto;
|
|
850
|
+
}
|
|
842
851
|
.mb-sd-8 {
|
|
843
852
|
margin-bottom: var(--spacing-sd-8);
|
|
844
853
|
}
|
|
@@ -1020,9 +1029,6 @@
|
|
|
1020
1029
|
.h-\[var\(--cmp-datepicker-calendar-day-dot-size\)\] {
|
|
1021
1030
|
height: var(--cmp-datepicker-calendar-day-dot-size);
|
|
1022
1031
|
}
|
|
1023
|
-
.h-\[var\(--cmp-datepicker-calendar-header-divider-height\)\] {
|
|
1024
|
-
height: var(--cmp-datepicker-calendar-header-divider-height);
|
|
1025
|
-
}
|
|
1026
1032
|
.h-\[var\(--cmp-datepicker-calendar-range-height\)\] {
|
|
1027
1033
|
height: var(--cmp-datepicker-calendar-range-height);
|
|
1028
1034
|
}
|
|
@@ -1053,6 +1059,9 @@
|
|
|
1053
1059
|
.h-\[var\(--sys-size-control-md-height\)\] {
|
|
1054
1060
|
height: var(--sys-size-control-md-height);
|
|
1055
1061
|
}
|
|
1062
|
+
.h-\[var\(--x\)\] {
|
|
1063
|
+
height: var(--x);
|
|
1064
|
+
}
|
|
1056
1065
|
.h-auto {
|
|
1057
1066
|
height: auto;
|
|
1058
1067
|
}
|
|
@@ -1071,6 +1080,9 @@
|
|
|
1071
1080
|
.h-sd-8 {
|
|
1072
1081
|
height: var(--spacing-sd-8);
|
|
1073
1082
|
}
|
|
1083
|
+
.h-sd-20 {
|
|
1084
|
+
height: var(--spacing-sd-20);
|
|
1085
|
+
}
|
|
1074
1086
|
.h-sd-32 {
|
|
1075
1087
|
height: var(--spacing-sd-32);
|
|
1076
1088
|
}
|
|
@@ -4496,6 +4508,11 @@
|
|
|
4496
4508
|
transition-property: none;
|
|
4497
4509
|
}
|
|
4498
4510
|
}
|
|
4511
|
+
.md\:h-sd-20 {
|
|
4512
|
+
@media (width >= 48rem) {
|
|
4513
|
+
height: var(--spacing-sd-20);
|
|
4514
|
+
}
|
|
4515
|
+
}
|
|
4499
4516
|
.md\:p-4 {
|
|
4500
4517
|
@media (width >= 48rem) {
|
|
4501
4518
|
padding: calc(var(--spacing) * 4);
|
package/eslint/index.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import requireLocaleNumber from "./rules/require-locale-number.mjs";
|
|
|
13
13
|
import componentGroupGap from "./rules/component-group-gap.mjs";
|
|
14
14
|
import fieldWidthGrade from "./rules/field-width-grade.mjs";
|
|
15
15
|
import tableColumnWidth from "./rules/table-column-width.mjs";
|
|
16
|
+
import dividerVerticalHeight from "./rules/divider-vertical-height.mjs";
|
|
16
17
|
|
|
17
18
|
const PLUGIN_NAME = "sellmate";
|
|
18
19
|
|
|
@@ -28,6 +29,7 @@ const plugin = {
|
|
|
28
29
|
"component-group-gap": componentGroupGap,
|
|
29
30
|
"field-width-grade": fieldWidthGrade,
|
|
30
31
|
"table-column-width": tableColumnWidth,
|
|
32
|
+
"divider-vertical-height": dividerVerticalHeight,
|
|
31
33
|
},
|
|
32
34
|
};
|
|
33
35
|
|
|
@@ -52,6 +54,8 @@ const recommended = [
|
|
|
52
54
|
[`${PLUGIN_NAME}/no-off-scale-spacing`]: "error",
|
|
53
55
|
// 생 <button>/<table> 은 디자인 시스템을 통째로 우회하는 것 (스타일 자유와 무관)
|
|
54
56
|
[`${PLUGIN_NAME}/no-raw-html-control`]: "error",
|
|
57
|
+
// 세로 SDivider 에 className 으로 높이를 주면 self-stretch 가 무효가 되어 선이 위로 솟는다
|
|
58
|
+
[`${PLUGIN_NAME}/divider-vertical-height`]: "error",
|
|
55
59
|
|
|
56
60
|
// 아래는 디자인 컨벤션 — 권고만 한다
|
|
57
61
|
[`${PLUGIN_NAME}/prefer-typo-preset`]: "warn",
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 세로 `SDivider` 의 높이를 `className="h-*"` 로 주면 정렬이 조용히 깨지는 것을 잡는다.
|
|
3
|
+
*
|
|
4
|
+
* 세로 분할선의 기본값은 `h-auto ... self-stretch` 라, 높이를 주지 않으면 부모 줄 높이를
|
|
5
|
+
* 그대로 채운다. 그런데 `align-self: stretch` 는 **교차축 크기가 auto 일 때만** 늘리므로
|
|
6
|
+
* className 으로 높이가 정해지는 순간 stretch 가 무효가 되고 flex-start 처럼 줄 맨 위에
|
|
7
|
+
* 붙는다. tailwind-merge 는 `h-auto` 만 교체하고 `self-stretch` 는 남기므로 경고도 없다.
|
|
8
|
+
*
|
|
9
|
+
* 높이와 정렬은 같이 정해져야 하는 값이라 컴포넌트가 함께 맡는다 — `height` prop 을 쓴다.
|
|
10
|
+
*
|
|
11
|
+
* <SDivider vertical height={20} /> // 높이 + self-center
|
|
12
|
+
* <SDivider vertical className="h-sd-20" /> // 위로 솟는다
|
|
13
|
+
*
|
|
14
|
+
* 정렬을 **직접 지정한 경우**(`self-center` `self-start` `my-auto` …)는 알고 쓰는 것으로
|
|
15
|
+
* 보고 잡지 않는다.
|
|
16
|
+
*
|
|
17
|
+
* AGENTS.md §3-6.
|
|
18
|
+
*/
|
|
19
|
+
import { collectClassTokens, stripVariants, stripModifiers } from "../lib/class-names.mjs";
|
|
20
|
+
|
|
21
|
+
/** 높이를 정하는 유틸리티인가 — `h-auto` 만 예외(기본값과 같아 stretch 가 살아 있다) */
|
|
22
|
+
function heightUtil(util) {
|
|
23
|
+
const m = /^h-(.+)$/.exec(util);
|
|
24
|
+
if (!m || m[1] === "auto") return null;
|
|
25
|
+
return m[1];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 교차축 정렬을 이미 직접 지정했는가 */
|
|
29
|
+
function alignsSelf(util) {
|
|
30
|
+
return /^self-/.test(util) || util === "my-auto" || util === "mt-auto" || util === "mb-auto";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 높이 유틸리티 값을 `height` prop 값으로 옮긴다. 옮길 수 없으면 null (수정 제안 없음).
|
|
35
|
+
* h-sd-20 → {20}
|
|
36
|
+
* h-[20px] → {20}
|
|
37
|
+
* h-[var(--x)] → "var(--x)"
|
|
38
|
+
*/
|
|
39
|
+
function toHeightProp(value) {
|
|
40
|
+
const scaled = /^sd-(\d+)$/.exec(value);
|
|
41
|
+
if (scaled) return `{${scaled[1]}}`;
|
|
42
|
+
const arbitrary = /^\[(.+)\]$/.exec(value);
|
|
43
|
+
if (!arbitrary) return null;
|
|
44
|
+
const inner = arbitrary[1].replace(/_/g, " ");
|
|
45
|
+
const px = /^(\d+(?:\.\d+)?)px$/.exec(inner);
|
|
46
|
+
if (px) return `{${px[1]}}`;
|
|
47
|
+
if (inner.includes('"')) return null;
|
|
48
|
+
return `"${inner}"`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const isClassAttr = (a) =>
|
|
52
|
+
a.type === "JSXAttribute" && (a.name?.name === "className" || a.name?.name === "class");
|
|
53
|
+
|
|
54
|
+
export default {
|
|
55
|
+
meta: {
|
|
56
|
+
type: "problem",
|
|
57
|
+
docs: {
|
|
58
|
+
description: "세로 SDivider 의 높이는 className 이 아니라 height prop 으로 지정",
|
|
59
|
+
},
|
|
60
|
+
schema: [],
|
|
61
|
+
messages: {
|
|
62
|
+
useHeightProp:
|
|
63
|
+
"세로 SDivider 의 높이는 `height` prop 으로 주세요{{suggestion}} — `className` 으로 주면 `align-self: stretch` 가 무효가 되어 선이 줄 맨 위에 붙습니다. (AGENTS.md §3-6)",
|
|
64
|
+
},
|
|
65
|
+
fixable: "code",
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
create(context) {
|
|
69
|
+
return {
|
|
70
|
+
JSXOpeningElement(node) {
|
|
71
|
+
if (node.name.type !== "JSXIdentifier" || node.name.name !== "SDivider") return;
|
|
72
|
+
|
|
73
|
+
const attrs = node.attributes;
|
|
74
|
+
const named = (n) => attrs.find((a) => a.type === "JSXAttribute" && a.name?.name === n);
|
|
75
|
+
|
|
76
|
+
// 세로 분할선만 대상. `vertical={expr}` 처럼 값이 정적이지 않으면 판단하지 않는다.
|
|
77
|
+
const vertical = named("vertical");
|
|
78
|
+
if (!vertical) return;
|
|
79
|
+
if (vertical.value !== null && vertical.value?.expression?.value !== true) return;
|
|
80
|
+
|
|
81
|
+
// 이미 height prop 을 쓰고 있으면 컴포넌트가 정렬까지 맡는다
|
|
82
|
+
if (named("height")) return;
|
|
83
|
+
|
|
84
|
+
const classAttr = attrs.find(isClassAttr);
|
|
85
|
+
if (!classAttr) return;
|
|
86
|
+
|
|
87
|
+
const tokens = collectClassTokens(classAttr);
|
|
88
|
+
let height = null;
|
|
89
|
+
for (const { token } of tokens) {
|
|
90
|
+
const util = stripModifiers(stripVariants(token));
|
|
91
|
+
if (alignsSelf(util)) return; // 정렬을 직접 정했다 — 의도한 것으로 본다
|
|
92
|
+
// variant 가 붙은 높이(`md:h-sd-20`)는 prop 으로 옮길 수 없어 판단에서 뺀다
|
|
93
|
+
if (height === null && token === util) height = heightUtil(util);
|
|
94
|
+
}
|
|
95
|
+
if (height === null) return;
|
|
96
|
+
|
|
97
|
+
const propValue = toHeightProp(height);
|
|
98
|
+
|
|
99
|
+
context.report({
|
|
100
|
+
node,
|
|
101
|
+
messageId: "useHeightProp",
|
|
102
|
+
data: { suggestion: propValue ? ` (\`height=${propValue}\`)` : "" },
|
|
103
|
+
fix(fixer) {
|
|
104
|
+
if (!propValue) return null;
|
|
105
|
+
// className 이 통짜 문자열일 때만 안전하게 손댄다 (cn()·템플릿 리터럴은 제외)
|
|
106
|
+
const raw =
|
|
107
|
+
classAttr.value?.type === "Literal"
|
|
108
|
+
? classAttr.value
|
|
109
|
+
: classAttr.value?.type === "JSXExpressionContainer" &&
|
|
110
|
+
classAttr.value.expression.type === "Literal"
|
|
111
|
+
? classAttr.value.expression
|
|
112
|
+
: null;
|
|
113
|
+
if (!raw || typeof raw.value !== "string") return null;
|
|
114
|
+
|
|
115
|
+
const rest = raw.value
|
|
116
|
+
.split(/\s+/)
|
|
117
|
+
.filter(Boolean)
|
|
118
|
+
.filter((t) => t !== `h-${height}`);
|
|
119
|
+
if (rest.length === raw.value.split(/\s+/).filter(Boolean).length) return null;
|
|
120
|
+
|
|
121
|
+
const fixes = [fixer.insertTextAfter(vertical, ` height=${propValue}`)];
|
|
122
|
+
if (rest.length > 0) {
|
|
123
|
+
fixes.push(fixer.replaceText(raw, `"${rest.join(" ")}"`));
|
|
124
|
+
} else {
|
|
125
|
+
// 속성만 지우면 공백이 두 칸 남는다 — 앞 공백까지 함께 지운다
|
|
126
|
+
const src = context.sourceCode ?? context.getSourceCode();
|
|
127
|
+
const [start, end] = classAttr.range;
|
|
128
|
+
const from = /\s/.test(src.text[start - 1]) ? start - 1 : start;
|
|
129
|
+
fixes.push(fixer.removeRange([from, end]));
|
|
130
|
+
}
|
|
131
|
+
return fixes;
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
},
|
|
137
|
+
};
|