sellmate-design-system-react 3.2.0 → 3.3.1

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
@@ -118,7 +118,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
118
118
 
119
119
  콤마 없는 `39000` 은 자릿수를 세어야 읽히지만 `39,000` 은 한눈에 읽힌다. 숫자를 그대로 출력하는 코드는 미완성으로 본다.
120
120
 
121
- 전화번호·사업자번호처럼 하이픈이 들어가는 값은 애초에 숫자가 아니라 **서식이 정해진 문자열**이므로 규칙의 대상이 아니다.
121
+ **번호·코드는 제외한다.** 전화번호·사업자번호·송장번호·상품코드처럼 대상을 가리키는 값은 크기를 비교하는 숫자가 아니라 **서식이 정해진 문자열**이다. 여기에 콤마를 넣으면 송장번호 `123456789` 가 `123,456,789` 로 보여 값 자체가 달라진다.
122
122
 
123
123
  ---
124
124
 
@@ -146,6 +146,8 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
146
146
 
147
147
  페이지 제목만 18px 로 크게 두고 그 아래는 14 / 12 로 촘촘하게 간다. 중간 크기(16px)는 기본 골격에서 쓰지 않는다.
148
148
 
149
+ **보조 설명의 색** — 기본은 `text-fg-tertiary`(`grey_65`) 다. 보조 설명 안에서 위계가 한 단계 더 필요할 때만 `text-fg-secondary`(`grey_80`) → `text-fg-tertiary`(`grey_65`) 순으로 내려 쓴다 (§2-3).
150
+
149
151
  ### 2-2. 간격 (spacing)
150
152
 
151
153
  - 유틸리티 숫자 = px. 스케일: `2 4 6 8 10 12 16 19 20 22 24 28 32 36 40 48 60`
@@ -157,8 +159,8 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
157
159
  | **페이지 콘텐츠 패딩** | **`p-20`** |
158
160
  | **섹션 ↔ 섹션** | **`gap-12`** |
159
161
  | **페이지 내 블록 ↔ 블록** (헤더·필터·툴바·테이블 사이) | **`gap-12`** |
160
- | 요소 ↔ 요소 (기본) | `gap-8` (`--sys-space-stack-gap-element-normal`) |
161
- | 제목 ↔ 설명 텍스트 | `gap-4` (`--sys-space-stack-gap-text-normal`) |
162
+ | 요소 ↔ 요소 | **기본 `gap-8`** (`--sys-space-stack-gap-element-normal`) · 타이트 `gap-4`(`-tight`) · 여유 `gap-12`(`-relaxed`) / `gap-16`(`-wide`) |
163
+ | 제목 ↔ 설명 텍스트 | **수직 배치 `gap-4`**(`--sys-space-stack-gap-text-normal`, 타이트 `gap-2`) · **가로 배치 `gap-8`**(`-relaxed`) |
162
164
 
163
165
  정보 밀도가 높은 서비스라 블록 간격을 넓게 벌리지 않는다. `gap-16` / `gap-24` 를 페이지 골격의 기본값으로 쓰지 않는다.
164
166
 
@@ -198,23 +200,28 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
198
200
 
199
201
  **시맨틱 유틸리티를 우선 사용한다** — 의미가 이름에 담긴 토큰이 이미 유틸리티로 존재한다.
200
202
 
201
- #### 텍스트 색은 단계뿐이다
203
+ #### 텍스트 회색 위계는 세 단계다
202
204
 
203
- | 용도 | 유틸리티 | 값 |
204
- | --- | --- | --- |
205
- | **본문·제목 (기본값)** | 지정하지 않는다 — 전역 기본색이 이미 적용된다 | `grey_95` `#222222` |
206
- | **보조 설명 · 비활성** | **`text-fg-tertiary`** | `grey_65` `#888888` |
205
+ | 단계 | 유틸리티 | 값 | 용도 |
206
+ | --- | --- | --- | --- |
207
+ | **기본** | 지정하지 않는다 — 전역 기본색이 이미 적용된다 (`text-fg-primary`) | `grey_95` `#222222` | 본문·제목 |
208
+ | **보조 1** | **`text-fg-secondary`** | `grey_80` `#555555` | 보조 정보 |
209
+ | **보조 2 / 비활성** | **`text-fg-tertiary`** | `grey_65` `#888888` | 부가 설명, 비활성 |
207
210
 
211
+ - **위계는 순차 적용한다.** 기본 → 보조1 → 보조2 순으로 내려가며 중간 단계를 건너뛰지 않는다. 본문 바로 아래에 곧장 `text-fg-tertiary` 를 쓰지 않는다.
208
212
  - `theme.css` 가 `body` 에 `grey_95` 를 깔아두므로 **본문에 텍스트 색 클래스를 붙이지 않는다.** `text-fg-primary` 를 매번 쓰는 것은 불필요하다.
209
- - 흐리게 보여야 하는 것은 보조 설명이든 비활성이든 **전부 `text-fg-tertiary`** 다.
210
- - 중간 톤(`text-fg-secondary` `#555`)은 텍스트 위계에 쓰지 않는다. 두 단계로만 간다.
213
+ - 위계가 단계면 충분한 보조 설명은 `text-fg-tertiary` 하나로 끝낸다 (§2-1). 두 단계가 필요할 때만 `text-fg-secondary` 를 끼워 넣는다.
214
+ - 비활성 표시는 단계와 무관하게 `text-fg-tertiary` 다.
211
215
 
212
216
  ```tsx
213
217
  ✅ <p>주문이 접수되었습니다.</p> // 색 지정 없음 = grey_95
214
- ✅ <p className="text-fg-tertiary">최근 30일 기준</p> // 보조 설명
218
+ ✅ <p className="text-fg-tertiary">최근 30일 기준</p> // 보조 설명 한 단계
219
+ ✅ <> // 보조 설명 두 단계
220
+ <p className="text-fg-secondary">배송비 정책</p>
221
+ <p className="text-fg-tertiary">3만원 이상 무료</p>
222
+ </>
215
223
  ✅ <span className="text-fg-tertiary">-</span> // 빈 값 (§3-4)
216
224
  ❌ <p className="text-fg-primary">주문이 접수되었습니다.</p> // 불필요
217
- ❌ <p className="text-fg-secondary">최근 30일 기준</p> // 중간 톤 사용
218
225
  ```
219
226
 
220
227
  #### 그 밖의 색
@@ -283,7 +290,7 @@ function OrderModal({ orderId, open, onOpenChange, onClose, modalRef }: OrderMod
283
290
  width={720}
284
291
  // 주 액션은 button(단수), 보조 버튼은 footerLeft — 하단 버튼 양끝 분리 규칙과 같다
285
292
  button={{ label: '접수', onClick: () => modalRef.ok() }}
286
- footerLeft={<SButton color="secondary" label="취소" onClick={() => modalRef.cancel()} />}
293
+ footerLeft={<SButton color="neutral" outline label="취소" onClick={() => modalRef.cancel()} />}
287
294
  >
288
295
  <SKeyValueTable fields={orderFields} values={order} />
289
296
  </SActionModal>
@@ -298,19 +305,26 @@ SModal.create({ component: OrderModal, componentProps: { orderId } })
298
305
 
299
306
  ### 3-4. 테이블 컬럼 정렬
300
307
 
301
- **숫자가 들어가는 컬럼은 예외 없이 오른쪽 정렬한다** (`align: 'right'`).
308
+ **값의 크기를 비교하는 숫자 컬럼은 예외 없이 오른쪽 정렬한다** (`align: 'right'`).
302
309
  자릿수가 세로로 맞아야 값의 크기를 눈으로 비교할 수 있기 때문이다.
303
310
 
304
- | 컬럼 내용 | 정렬 | |
311
+ **판별 기준은 "숫자인가"가 아니라 "크기를 비교하는가"다.** 자릿수 차이가 거의 없고 값끼리 대소를 견줄 일이 없으면 숫자로만 이루어져 있어도 우측 정렬하지 않는다.
312
+
313
+ | 값 성격 | 정렬 | 예 |
305
314
  | --- | --- | --- |
306
- | **금액·수량·개수·비율 등 단위가 붙은 숫자** | **`'right'`** | `39,000원` · `12개` · `3건` · `15%` |
307
- | **숫자만 있는 값** | **`'right'`** | `1024` · `39000` |
315
+ | **금액·수량·개수·비율 등 양을 나타내는 값** | **`'right'`** | `39,000원` · `12개` · `3건` · `15%` |
316
+ | 코드·식별자 (주문번호, 상품코드, 순번) | **`'center'`** | `RV20250728-000010` · `1024` |
317
+ | 전화번호·사업자번호 | **`'center'`** | `010-1234-5678` |
318
+ | 일자·일시 | **`'center'`** | `2024-10-23` |
308
319
  | 텍스트 | 생략(기본 `left`) | 상품명, 카테고리 |
309
320
  | 상태 태그·아이콘·체크박스 등 고정폭 요소 | `'center'` | `STag`, `SIcon` |
310
321
 
322
+ **중앙 정렬은 `align: 'center'` 를 명시한다.** 기본값이 좌측이라 생략하면 중앙이 되지 않는다.
323
+
311
324
  ```tsx
312
325
  const columns: STableColumn[] = [
313
- { name: 'id', label: 'ID', field: 'id', width: '80px', align: 'right' },
326
+ { name: 'orderNo', label: '주문번호', field: 'orderNo', width: '140px', align: 'center' },
327
+ { name: 'orderedAt', label: '주문일자', field: 'orderedAt', width: '100px', align: 'center' },
314
328
  { name: 'name', label: '상품명', field: 'name' }, // 텍스트 → 생략
315
329
  { name: 'qty', label: '수량', field: 'qty', width: '80px', align: 'right',
316
330
  format: (v: number) => `${Number(v).toLocaleString()}개` },
@@ -321,8 +335,16 @@ const columns: STableColumn[] = [
321
335
  ];
322
336
  ```
323
337
 
324
- - `format` 으로 단위를 붙이더라도 **숫자 기반이면 오른쪽 정렬**이다. 단위 때문에 문자열이 되는 것은 정렬 판단과 무관하다.
325
- - 숫자는 §1-4 대로 **`toLocaleString()` 이 필수**다. 세 자리 콤마 없이 출력하지 않는다.
338
+ - `format` 으로 단위를 붙이더라도 **양을 나타내면 오른쪽 정렬**이다. 단위 때문에 문자열이 되는 것은 정렬 판단과 무관하다.
339
+ - 양을 나타내는 숫자는 §1-4 대로 **`toLocaleString()` 이 필수**다. 세 자리 콤마 없이 출력하지 않는다.
340
+ - **번호·코드에는 세 자리 콤마를 넣지 않는다.** 송장번호 `123456789` 를 `123,456,789` 로 표시하면 값 자체가 달라 보인다.
341
+ - **헤더는 가운데, 셀만 우측**으로 두려면 `align` 이 아니라 `tdClass` 를 쓴다. `align` 은 `<th>` 와 `<td>` 에 함께 적용된다.
342
+
343
+ ```tsx
344
+ { name: 'views', label: '조회수', field: 'views', align: 'center', tdClass: 'text-right!',
345
+ format: (v: number) => Number(v).toLocaleString() },
346
+ ```
347
+
326
348
  - `SKeyValueTable` 의 값 셀도 같은 기준을 따른다.
327
349
 
328
350
  #### 값이 없는 셀은 회색 하이픈
@@ -350,13 +372,58 @@ const columns: STableColumn[] = [
350
372
 
351
373
  | 상황 | 사용 |
352
374
  | --- | --- |
353
- | 일반 액션 | `SButton` (`color`: `primary` / `secondary` / `neutral` / `danger`, `size`: xs~lg) |
354
- | **삭제 등 파괴적 액션** | **`SButton color="danger"`** 또는 **`color="danger" outline`** |
375
+ | 일반 액션 | `SButton` (`color`: `primary` / `secondary` / `neutral` / `danger`, `outline`, `size`: xs~md) |
376
+ | **삭제 등 파괴적 액션** | **`SButton color="danger"`** 또는 **`color="danger" outline`** (아래 3-5-3) |
355
377
  | 텍스트만 있는 저강조 액션 | `SGhostButton` |
356
378
  | 본문 속 이동 링크 | `STextLink` |
357
379
  | 메인 액션 + 부가 메뉴 | `SDropdownButton` |
358
380
 
359
- 화면의 primary 버튼 개수에 대한 제약은 없다. 다만 **삭제·초기화 등 되돌릴 수 없는 액션은 반드시 `danger`** 로 표시한다.
381
+ #### 3-5-1. 채움 버튼은 페이지당 개수 제한이 있다
382
+
383
+ `outline` 이 위계를 한 단계 낮춘다. **채움(outline 없음)이 그 페이지의 최상위 액션**이다.
384
+
385
+ | 조합 | 위계 | 페이지당 |
386
+ | --- | --- | --- |
387
+ | `color="primary"` (진남색 채움) | 최상위 실행 | **1개** |
388
+ | `color="danger"` (빨강 채움) | 최상위 파괴 | **1개** |
389
+ | `color="secondary"` (밝은 파랑 채움) | 중간 | N개 — **같은 속성 연속 배치 금지** |
390
+ | `primary outline` · `neutral outline` · `neutral`(흰 채움) · `danger outline` | 낮음 | N개 |
391
+
392
+ `SDropdownButton` 도 같은 규칙을 따르며, **페이지당 `primary` 채움 1개 계산에 포함**된다.
393
+
394
+ #### 3-5-2. `size` 는 놓이는 위치가 정한다
395
+
396
+ | 위치 | size |
397
+ | --- | --- |
398
+ | 테이블 **행 내부** 인라인 액션 | `xs` |
399
+ | 화면·목록 액션 (툴바, `STableBar`) | `sm` |
400
+ | **모달·위저드 푸터** | `md` |
401
+
402
+ `lg` 는 현재 사용처가 없다. 페이지 골격에서 쓰지 않는다.
403
+
404
+ #### 3-5-3. 파괴적 액션의 채움 / outline
405
+
406
+ 물리적 위치가 아니라 **적용 범위 + 위험도**로 가른다.
407
+
408
+ | 상황 | 스타일 |
409
+ | --- | --- |
410
+ | 대상이 **전체**이거나 기능적 위험도가 높음 (계정 삭제, 전체 초기화) | `color="danger"` **채움** + 다른 버튼과 공간 분리 |
411
+ | **항목별**로 실행되거나 위험도가 상대적으로 낮음 (행 삭제, 연동 해제) | `color="danger" outline` |
412
+
413
+ #### 3-5-4. `SDropdownButton` — `split` 기준
414
+
415
+ | 설정 | 동작 | 쓰는 때 |
416
+ | --- | --- | --- |
417
+ | `split={false}` (기본) | 버튼 전체가 메뉴 토글. 클릭만으로는 실행되지 않는다 | 실행 전에 **범위·옵션을 골라야** 할 때 — `적치 지시서 출력 ▾` → 전체 항목 / 선택 항목만 |
418
+ | `split` | 좌측 = 기본 동작 **즉시 실행**, 우측 `⋯` = 변형 동작 | 기본 동작이 명확하고 **변형·부가 동작**을 곁들일 때 — `저장 \| 저장 후 신규 등록` |
419
+
420
+ 메뉴 항목은 **기본 동작의 변형·부가 동작으로 한정**한다. 무관한 액션 여러 개를 한 드롭다운에 묶지 않는다.
421
+
422
+ #### 3-5-5. 그 밖
423
+
424
+ - 라벨은 **동사(+목적어)** — `저장` · `적치 지시` · `송장 재출력`
425
+ - 완료된 작업 버튼은 제거하지 않고 **`disabled` 로 남긴다.** 자리가 사라지면 행 높이가 흔들린다.
426
+ - **모달 보조 액션은 `color="neutral" outline`** 이 표준이다.
360
427
 
361
428
  **하단 버튼 배치 (모든 화면 공통)** — 그룹을 모아 정렬하지 않고 **양끝으로 벌린다**(`justify-between`).
362
429
 
@@ -366,10 +433,10 @@ const columns: STableColumn[] = [
366
433
 
367
434
  ```tsx
368
435
  <div className="flex items-center justify-between">
369
- <SButton color="secondary" label="취소" />
436
+ <SButton color="neutral" outline label="취소" /> {/* 되돌리기 = 낮은 위계 */}
370
437
  <div className="flex items-center gap-8">
371
438
  <SCheckbox label="계속 등록하기" value={keep} onValueChange={setKeep} /> {/* 부가 요소 */}
372
- <SButton label="저장" />
439
+ <SButton label="저장" /> {/* 페이지 유일한 primary 채움 */}
373
440
  </div>
374
441
  </div>
375
442
  ```
@@ -475,10 +542,14 @@ export default function ProductListPage() {
475
542
  total={total}
476
543
  selected={selected.length}
477
544
  actions={
478
- <SButton size="sm" color="secondary" label="선택 삭제"
545
+ /* 선택 항목 단위 파괴 액션 → danger outline (§3-5-3) */
546
+ <SButton size="sm" color="danger" outline label="선택 삭제"
479
547
  disabled={!selected.length} onClick={removeSelected} />
480
548
  }
481
- rightActions={<SButton size="sm" label="상품 등록" onClick={goCreate} />}
549
+ rightActions={
550
+ /* 이 페이지의 유일한 primary 채움 (§3-5-1) */
551
+ <SButton size="sm" label="상품 등록" onClick={goCreate} />
552
+ }
482
553
  />
483
554
 
484
555
  <STable
@@ -548,7 +619,7 @@ export default function ProductCreatePage() {
548
619
 
549
620
  {/* 하단 버튼은 양끝으로 벌린다. 부가 요소(체크박스 등)는 저장 바로 왼쪽 */}
550
621
  <div className="flex items-center justify-between">
551
- <SButton type="button" color="secondary" label="취소" onClick={goBack} />
622
+ <SButton type="button" color="neutral" outline label="취소" onClick={goBack} />
552
623
  <div className="flex items-center gap-8">
553
624
  <SCheckbox label="계속 등록하기" value={keepOpen} onValueChange={v => setKeepOpen(v as boolean)} />
554
625
  <SButton type="submit" label="저장" />
@@ -621,7 +692,7 @@ export default function ProductDetailPage() {
621
692
 
622
693
  {/* 액션은 하단 — 목록(되돌리기)은 왼쪽 끝, 실행 액션은 오른쪽 끝 */}
623
694
  <div className="flex items-center justify-between">
624
- <SButton color="secondary" label="목록" onClick={goList} />
695
+ <SButton color="neutral" outline label="목록" onClick={goList} />
625
696
  <div className="flex items-center gap-8">
626
697
  <SButton color="danger" outline label="삭제" onClick={confirmDelete} />
627
698
  <SButton label="수정" onClick={goEdit} />
@@ -656,7 +727,8 @@ export default function ProductDetailPage() {
656
727
  - [ ] `text-[14px]`, `bg-[#...]` 같은 리터럴 임의 값이 없는가 (`var(--sys-*)` 참조는 허용)
657
728
  - [ ] 텍스트에 `typo-*` 프리셋을 썼는가
658
729
  - [ ] 간격이 전부 토큰 스케일 값인가 (`gap-13` ❌ → `gap-12` ✅)
659
- - [ ] 본문이 12px(`typo-body-sm-default`)이고 보조 설명이 `text-fg-tertiary` 인가 (14px 본문 ❌)
730
+ - [ ] 본문이 12px(`typo-body-sm-default`)인가 (14px 본문 ❌)
731
+ - [ ] 텍스트 회색 위계를 순차 적용했는가 (기본 → `text-fg-secondary` → `text-fg-tertiary`, 단계 건너뛰기 ❌)
660
732
  - [ ] 페이지 패딩이 `p-20`, 블록·섹션 간격이 `gap-12` 인가 (`gap-16`/`gap-24` ❌)
661
733
  - [ ] 같은 컴포넌트를 나열할 때 §2-2 그룹 간격을 썼는가 (체크박스 가로 `gap-24` 등)
662
734
  - [ ] 페이지가 §4의 표준 골격에서 시작했는가
@@ -664,11 +736,13 @@ export default function ProductDetailPage() {
664
736
  - [ ] 섹션 구분에 `SSectionHeaderCard` 를 썼는가 (직접 만든 카드가 아니라)
665
737
  - [ ] 목록의 주요 액션이 `STableBar` 의 `rightActions` 에 있는가 (페이지 제목 줄이 아니라)
666
738
  - [ ] 상태 표시에 `STag size="sm"` 을 썼는가
667
- - [ ] 테이블의 숫자 컬럼(금액·수량·개수 등)이 전부 `align: 'right'` 인가
668
- - [ ] 화면에 표시하는 모든 숫자에 `toLocaleString()` 써서 자리 콤마를 넣었는가
669
- - [ ] 금액·수량 등 양을 나타내는 숫자에 `toLocaleString()` 을 썼는가 (번호·코드는 제외)
739
+ - [ ] 테이블에서 양을 나타내는 컬럼(금액·수량·개수 등)이 전부 `align: 'right'` 인가
740
+ - [ ] 번호·코드·전화번호·일자 컬럼에 `align: 'center'` **명시**했는가 (생략하면 좌측이 된다)
741
+ - [ ] 금액·수량 등 양을 나타내는 숫자에 빠짐없이 `toLocaleString()` 을 썼는가 (번호·코드는 제외)
670
742
  - [ ] 하단 버튼이 양끝 분리(`justify-between`)이고, 되돌리기가 왼쪽 끝 · 실행이 오른쪽 끝인가
671
- - [ ] 삭제 등 파괴적 액션에 `color="danger"` 썼는가
743
+ - [ ] 페이지에 `color="primary"` 채움 버튼이 **1개뿐**인가 (`danger` 채움도 1개, `SDropdownButton` 포함)
744
+ - [ ] 버튼 `size` 가 위치 규칙과 맞는가 (행 내부 `xs` / 화면 액션 `sm` / 모달 푸터 `md`)
745
+ - [ ] 파괴 액션의 채움/outline 이 **적용 범위 + 위험도** 기준과 맞는가
672
746
  - [ ] 작업용 모달을 `SActionModal` + `SModal.create` 로 만들었는가 (직접 오버레이 ❌)
673
747
  - [ ] 상태 표시·알림·확인 다이얼로그가 §3의 선택 규칙을 따르는가
674
748
 
@@ -685,9 +759,8 @@ export default function ProductDetailPage() {
685
759
  | `sellmate/no-arbitrary-class` | §1-2 토큰 있는 속성의 임의 값 (`text-[14px]`, `bg-[#eee]`) |
686
760
  | `sellmate/no-off-scale-spacing` | §2-2 스케일 밖 간격 (`gap-13`) |
687
761
  | `sellmate/table-numeric-align` | §3-4 숫자 컬럼의 `align: 'right'` 누락 (`--fix` 지원) |
688
- | `sellmate/require-locale-number` | §1-4 숫자 컬럼의 `toLocaleString()` 누락 |
762
+ | `sellmate/require-locale-number` | §1-4 금액·수량 등 수량 컬럼의 `toLocaleString()` 누락 |
689
763
  | `sellmate/component-group-gap` | §2-2 컴포넌트 그룹 간격 (체크박스 가로 24 / 세로 8 등) |
690
- | `sellmate/require-locale-number` | §1-4 금액·수량 등의 `toLocaleString()` 누락 |
691
764
  | `sellmate/prefer-typo-preset` | §1-3 낱개 폰트 조합 (`text-14 font-bold`) |
692
765
 
693
766
  `gap-13` 처럼 스케일 밖 값은 Tailwind v4 에서 **에러 없이 조용히 무시된다**. "간격이 왜 안 먹지" 로만 보이므로 반드시 스케일 값을 쓴다.
package/README.md CHANGED
@@ -160,8 +160,8 @@ export default [
160
160
  | `sellmate/no-raw-html-control` | error | 대응 컴포넌트가 있는 생 HTML (`<button>` `<input>` `<select>` `<textarea>` `<table>` `<form>` `<dialog>` `<hr>` `<details>` `<progress>`), `alert()`/`confirm()` |
161
161
  | `sellmate/no-arbitrary-class` | error | **토큰이 있는 속성**(색·타이포·간격·모서리)의 임의 값 — `text-[14px]`, `bg-[#eee]`, `gap-[13px]`, `rounded-[5px]` |
162
162
  | `sellmate/no-off-scale-spacing` | error | 스케일 밖 간격 (`gap-13`, `p-15`) — Tailwind v4 에서 **조용히 무시되는** 값이라 눈으로 찾기 어렵다 |
163
- | `sellmate/table-numeric-align` | error | 숫자 컬럼(금액·수량 등)에 `align: 'right'` 누락 — **`--fix` 로 자동 교정** |
164
- | `sellmate/require-locale-number` | error | 숫자 컬럼의 `toLocaleString()` 누락 — 세 자리 콤마는 필수 |
163
+ | `sellmate/table-numeric-align` | error | 수량 컬럼(금액·수량 등)에 `align: 'right'` 누락 — **`--fix` 로 자동 교정** |
164
+ | `sellmate/require-locale-number` | error | 수량 컬럼의 `toLocaleString()` 누락 — 세 자리 콤마는 필수 |
165
165
  | `sellmate/component-group-gap` | error | 같은 컴포넌트를 나열할 때의 그룹 간격 — 배열 방향에 따라 값이 다르다(체크박스 가로 24 / 세로 8) |
166
166
  | `sellmate/prefer-typo-preset` | warn | `text-14 font-bold` 같은 낱개 조합 → `typo-*` 프리셋 |
167
167
 
@@ -188,6 +188,30 @@ export default [
188
188
 
189
189
  핵심은 **"디자인 토큰을 하드코딩하지 마라"** 이지 "임의 값을 절대 쓰지 마라" 가 아닙니다.
190
190
 
191
+ 테이블 두 규칙(`table-numeric-align` · `require-locale-number`)도 아래는 통과시킵니다.
192
+
193
+ ```tsx
194
+ // 번호·코드 등 식별자 — 크기를 비교하지 않으므로 우측 정렬도 콤마도 요구하지 않는다
195
+ { name: 'waybillNo', label: '송장번호', field: 'waybillNo', align: 'center' }
196
+
197
+ // 헤더 가운데 + 셀 우측 — align 은 th·td 공통이라 tdClass 로만 표현된다
198
+ { name: 'views', label: '조회수', field: 'views', align: 'center', tdClass: 'text-right!',
199
+ format: (v: number) => Number(v).toLocaleString() }
200
+
201
+ // render / renderCell 로 셀을 직접 그리는 컬럼 — 라벨만으로 숫자 여부를 판단하지 않는다
202
+ { name: 'unitPrice', label: '단가', field: 'unitPrice', align: 'right',
203
+ renderCell: ({ row }) => `${row.unitPrice.toLocaleString()}원` }
204
+
205
+ // 수량 어휘를 부분 문자열로 포함하지만 값은 불리언인 상태 컬럼
206
+ { name: 'isCostInput', label: '비용 입력 여부', field: 'isCostInput', align: 'center' }
207
+ ```
208
+
209
+ 그래도 남는 예외는 컬럼 `name` 을 `allow` 로 지정해 뺍니다.
210
+
211
+ ```js
212
+ 'sellmate/table-numeric-align': ['error', { allow: ['rank'] }],
213
+ ```
214
+
191
215
  ### 더 엄격하게 / 더 느슨하게
192
216
 
193
217
  `configs.strict` 는 전 규칙을 error 로 올리고 `<ul>` `<ol>` `<li>` `<svg>` `<label>` 까지 검사합니다.
@@ -11,6 +11,7 @@
11
11
  | `open?` | `boolean` | — | |
12
12
  | `persistent?` | `boolean` | — | true면 백드롭·ESC로 안 닫히고 흔들림 (sd-modal-container persistent) |
13
13
  | `modalTitle?` | `string` | `''` | |
14
+ | `description?` | `ReactNode` | — | 제목 오른쪽에 붙는 보조 설명 (sd-action-modal 의 header-sub-title 슬롯). 문자열이면 서브텍스트 스타일로 렌더하고, 노드를 넘기면 그대로 배치한다. |
14
15
  | `button?` | `SActionModalButton` | — | 하단 액션 버튼 (주 액션 1개). 의도적으로 단수다 — 보조 버튼(취소·삭제 등)은 `footerLeft` 슬롯에 직접 배치한다. |
15
16
  | `footerLeft?` | `ReactNode` | — | footer 좌측 영역 (sd-action-modal 의 bottom-sub-content 슬롯). 보조 버튼이나 안내 문구를 넣는다. 버튼을 하나 더 쓰고 싶을 때 여기에 SButton 을 넣는다. |
16
17
  | `width?` | `number \| string` | — | |
@@ -3350,6 +3350,7 @@ function SActionModal({
3350
3350
  onClose,
3351
3351
  persistent,
3352
3352
  modalTitle = "",
3353
+ description,
3353
3354
  button,
3354
3355
  footerLeft,
3355
3356
  width,
@@ -3369,7 +3370,10 @@ function SActionModal({
3369
3370
  height,
3370
3371
  className: "w-fit min-w-[480px] min-h-[min(320px,calc(100dvh-48px))]",
3371
3372
  children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-0 flex-auto flex-col", children: [
3372
- /* @__PURE__ */ jsxRuntime.jsx("header", { className: "flex flex-shrink-0 items-center gap-[var(--cmp-overlay-header-gap)] py-[var(--cmp-overlay-header-paddingY)] pl-[var(--cmp-overlay-header-paddingX)] pr-[calc(var(--cmp-overlay-header-paddingX)+20px)]", children: /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-[16px] font-bold leading-[26px] text-[color:var(--cmp-overlay-header-title-color)]", children: modalTitle }) }),
3373
+ /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex flex-shrink-0 items-center gap-[var(--cmp-overlay-header-gap)] py-[var(--cmp-overlay-header-paddingY)] pl-[var(--cmp-overlay-header-paddingX)] pr-[calc(var(--cmp-overlay-header-paddingX)+20px)]", children: [
3374
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "whitespace-nowrap text-[16px] font-bold leading-[26px] text-[color:var(--cmp-overlay-header-title-color)]", children: modalTitle }),
3375
+ description != null && description !== "" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex min-w-0 flex-1 items-center gap-2 text-[12px] font-normal leading-[20px] text-[color:var(--color-grey-65)]", children: description })
3376
+ ] }),
3373
3377
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1 overflow-auto px-[var(--cmp-modal-action-body-paddingX)] pb-[var(--cmp-modal-action-body-paddingBottom)]", children }),
3374
3378
  (button || footerLeft) && /* @__PURE__ */ jsxRuntime.jsxs(
3375
3379
  "footer",