sellmate-design-system-react 9.0.0-beta.43 → 9.0.0-beta.44

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.
@@ -484,7 +484,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
484
484
  | 여러 줄 텍스트를 받는다 | `STextarea` | §3-7-1 |
485
485
  | 제목·굵게·목록·색 같은 **서식이 남아야 하는** 글을 받는다 | `SEditor` | §3-7-1 |
486
486
  | 목록·결과를 검색어로 좁힌다 | `SSearchInput` | §3-7-1 |
487
- | 숫자(수량·금액)를 받는다 | `SNumberInput` | |
487
+ | 숫자(수량·금액)를 받는다 | `SNumberInput` | §3-7-13 |
488
488
  | 바코드를 스캔해 받는다 | `SBarcodeInput` | |
489
489
  | 목록에서 하나 고르게 한다 | `SSelect` | §3-7-2 |
490
490
  | 선택지를 항상 펼쳐 두고 하나 고르게 한다 | `SRadioGroup` | §3-7-2 |
@@ -1470,6 +1470,23 @@ const [from, setFrom] = useState<string | null>(null);
1470
1470
  ❌ <SListItem title="일반 문의" selected /> {/* clickable 없으면 선택 표시가 안 나온다 */}
1471
1471
  ```
1472
1472
 
1473
+ ##### 행 안에 컨트롤을 둘 때
1474
+
1475
+ `renderItem` 이 앱 몫이라 행 안에 입력·버튼이 들어오는 것은 예외가 아니다 — 순서를 정하는 목록은 대개 「몇 번째로 보낼지」를 숫자로도 받는다. 그냥 두면 된다.
1476
+
1477
+ - **행 선택은 옵트인이다.** `selectedKey`·`defaultSelectedKey`·`onSelectedKeyChange` 중 하나라도 주면 행이 `role="button"` 이 되어 클릭·Enter·Space 로 선택된다. 하나도 주지 않으면 행은 아무 상호작용도 갖지 않는다. **순서만 바꾸는 목록에 선택 prop 을 습관적으로 붙이지 않는다** — 붙이지 않아야 행 안의 컨트롤이 중첩 인터랙티브가 되지 않고 스크린리더에 그대로 노출된다.
1478
+ - **선택을 함께 쓰더라도 자손 컨트롤의 이벤트를 끊지 않는다.** 입력·버튼·링크에서 난 클릭과 Enter·Space 는 선택으로 새지 않는다 — 행의 제목 텍스트를 눌러 고르는 것만 선택으로 간다. `stopPropagation` 을 직접 넣을 자리가 아니다.
1479
+
1480
+ ```tsx
1481
+ ✅ <SDraggableList items={rows} getKey={r => r.id} {/* 선택을 안 쓰면 선택 prop 도 없다 */}
1482
+ renderItem={(row, _i, { onDragHandleMouseDown }) => (
1483
+ <SDraggableItem title={row.name} onDragHandleMouseDown={onDragHandleMouseDown}
1484
+ trailing={<SNumberInput width={72} value={row.order} onValueChange={…} />} />
1485
+ )} />
1486
+
1487
+ ❌ <div onKeyDown={e => { e.stopPropagation(); … }}> {/* 리스트가 이미 걸러 준다 */}
1488
+ ```
1489
+
1473
1490
  #### 3-7-7. 펼치는 셋 — SExpansionItem vs SExpansionList vs STree
1474
1491
 
1475
1492
  | 판별 | 사용 |
@@ -1577,6 +1594,29 @@ const [selectedId, setSelectedId] = useState<string>();
1577
1594
  ❌ {loading ? <SCircleProgress indeterminate /> : <SImage src={url} />} {/* SImage 가 이미 한다 */}
1578
1595
  ```
1579
1596
 
1597
+ #### 3-7-13. 숫자 — SNumberInput 의 min·max 는 검증 경계다
1598
+
1599
+ `min`·`max` 는 `<input type="number">` 의 그것과 같다 — **값을 고쳐 쓰지 않는 검증 경계**다. 범위를 벗어나면 blur·제출 시 에러 상태가 서고, 사용자가 친 값은 그대로 남아 앱의 범위 가드에 도달한다.
1600
+
1601
+ - **문구를 앱이 대지 않아도 된다.** `rules` 도 `SForm` 도 없는 화면에서 `min`·`max` 만 주면 DS 가 「1~99 사이로 입력해 주세요.」 같은 기본 안내를 자동으로 붙인다. 범위 이탈이 아무 표시 없이 지나가는 경로가 없다.
1602
+ - **화면의 말로 바꾸려면 `rules` 를 준다.** 「우선순위는 1부터」처럼 그 화면에서만 통하는 문구가 있으면 `rules` 가 DS 기본 문구를 이긴다. 앱이 직접 준 `errorMessage` 도 마찬가지다.
1603
+ - **`SForm` 안에서는 범위 이탈이 제출을 막는다.** `rules` 를 따로 걸지 않아도 그렇다 — 네이티브 폼과 같다.
1604
+ - **빈 칸(`null`)은 범위 판정 대상이 아니다.** 필수 입력은 `min` 이 아니라 `rules` 로 막는다.
1605
+ - **경계로 붙여도 사용자가 놀라지 않는 값에서만 `clampOnBlur` 를 켠다.** 켜면 blur 시 값이 경계로 **말없이 바뀌고** `onValueChange` 로 되쏘아진다 — 그래서 앱의 범위 가드에는 범위 밖 값이 도달하지 않는다. 재고 조정 수량처럼 보정이 자연스러운 자리에만 쓰고, 업무 규칙(최소 주문 수량·우선순위 시작값)에는 쓰지 않는다.
1606
+ - 스테퍼(`showButton`)와 위/아래 화살표는 `clampOnBlur` 와 무관하게 언제나 `min`·`max` 를 한계로 삼는다.
1607
+
1608
+ ```tsx
1609
+ ✅ <SNumberInput label="우선순위" min={1} max={99} value={v} onValueChange={setV} />
1610
+ {/* 0 을 넣고 blur 하면 0 이 남고 에러가 선다 — 저장 가드의 v < 1 이 그대로 걸린다 */}
1611
+
1612
+ ✅ <SNumberInput label="우선순위" min={1} max={99} value={v} onValueChange={setV}
1613
+ rules={[v => (typeof v === 'number' && (v < 1 || v > 99) ? '우선순위는 1부터 99까지입니다.' : true)]} />
1614
+ {/* 문구만 화면의 말로 바꾼다 */}
1615
+
1616
+ ❌ <SNumberInput label="우선순위" min={1} clampOnBlur value={v} onValueChange={setV} />
1617
+ {/* 0 이 1 로 바뀐 뒤 도착하므로 저장 직전의 v < 1 가드는 영원히 걸리지 않는다 */}
1618
+ ```
1619
+
1580
1620
  ---
1581
1621
 
1582
1622
  ## 4. 페이지 레시피 — 표준 골격
@@ -3971,8 +4011,8 @@ export interface SDraggableItemRenderState {
3971
4011
  | `items` | `T[]` | — | 현재 순서대로 렌더링할 아이템 목록 |
3972
4012
  | `getKey` | `(item: T, index: number) => string` | — | 아이템을 식별할 안정적인 key |
3973
4013
  | `renderItem` | `(item: T, index: number, state: SDraggableListRenderState) => ReactNode` | — | 아이템 렌더링 함수 |
3974
- | `selectedKey?` | `string` | — | 외부에서 제어하는 selected 아이템 key |
3975
- | `defaultSelectedKey?` | `string` | — | 초기 selected 아이템 key |
4014
+ | `selectedKey?` | `string` | — | 외부에서 제어하는 selected 아이템 key. 이 셋(`selectedKey`·`defaultSelectedKey`·`onSelectedKeyChange`) 중 하나라도 주면 **행 선택이 켜진다** — 행이 `role="button"` 이 되어 클릭·Enter·Space 로 선택된다. 아무것도 주지 않으면 행은 아무 상호작용도 갖지 않는다. |
4015
+ | `defaultSelectedKey?` | `string` | — | 초기 selected 아이템 key. 주면 행 선택이 켜진다 (`selectedKey` 참조) |
3976
4016
  | `getDisabled?` | `(item: T, index: number) => boolean` | — | disabled 아이템은 선택 및 드래그에서 제외 |
3977
4017
  | `getDepth?` | `(item: T, index: number) => number` | — | 아이템의 중첩 단계. 기본값은 1 |
3978
4018
  | `setDepth?` | `(item: T, depth: number) => T` | — | depth 변경이 필요한 드롭에서 다음 아이템을 만드는 함수 |
@@ -3986,7 +4026,7 @@ export interface SDraggableItemRenderState {
3986
4026
  | Event | Type | Description |
3987
4027
  |-------|------|-------------|
3988
4028
  | `onChange` | `(items: T[]) => void` | 단독 리스트에서 드래그가 끝난 뒤 변경된 순서 |
3989
- | `onSelectedKeyChange` | `(key: string) => void` | selected 아이템이 변경될 때 호출 |
4029
+ | `onSelectedKeyChange` | `(key: string) => void` | selected 아이템이 변경될 때 호출. 주면 행 선택이 켜진다 (`selectedKey` 참조) |
3990
4030
 
3991
4031
  ### SDraggableProvider
3992
4032
 
@@ -6002,8 +6042,9 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
6002
6042
  |------|------|---------|-------------|
6003
6043
  | `value?` | `string \| number \| null` | — | 입력 값 |
6004
6044
  | `size?` | `SNumberInputSize` | `'sm'` | 크기 |
6005
- | `min?` | `number` | `Number.NEGATIVE_INFINITY` | 최솟값 |
6006
- | `max?` | `number` | `Number.POSITIVE_INFINITY` | 최댓값 |
6045
+ | `min?` | `number` | `Number.NEGATIVE_INFINITY` | 최솟값 — **검증 경계**다 (`<input type="number">` 의 `min` 과 같다). 값을 고쳐 쓰지 않는다. 벗어나면 blur·제출 시 에러 상태가 서고 안내 문구가 자동으로 붙는다. 스테퍼(`showButton`)와 위/아래 화살표는 이 값을 한계로 삼는다. |
6046
+ | `max?` | `number` | `Number.POSITIVE_INFINITY` | 최댓값 — **검증 경계**다 (`<input type="number">` 의 `max` 와 같다). 값을 고쳐 쓰지 않는다. 벗어나면 blur·제출 시 에러 상태가 서고 안내 문구가 자동으로 붙는다. 스테퍼(`showButton`)와 위/아래 화살표는 이 값을 한계로 삼는다. |
6047
+ | `clampOnBlur?` | `boolean` | `false` | blur 시 값을 `[min, max]` 경계로 끌어당길지. 기본 `false`. 기본값에서는 사용자가 친 값이 그대로 보존되고, 범위를 벗어나면 에러 상태로 알린다 — 그래야 소비자의 범위 검증에 실제 입력값이 도달한다. `true` 로 켜면 범위 밖 값이 경계값으로 **말없이 바뀌고** `onValueChange` 로 되쏘아진다 (`min={1}` 인 칸에 `0` 을 넣고 blur 하면 `onValueChange(1)` 이 발화한다). 경계로 붙여도 사용자가 놀라지 않는 값에만 켠다. 켜면 범위 에러는 서지 않는다 — 고쳐 쓴 값은 언제나 범위 안이기 때문이다. |
6007
6048
  | `step?` | `number` | `1` | 스텝 단위 |
6008
6049
  | `showButton?` | `boolean` | `false` | 증감 버튼 표시 |
6009
6050
  | `allowDecimal?` | `boolean` | `false` | 소수점 입력 허용 |
@@ -7303,7 +7344,7 @@ export interface SSystemActionButtonMenuOption {
7303
7344
  | `emptySlot?` | `ReactNode` | — | 데이터가 없을 때 body 영역 전체를 대체하는 슬롯. 지정하면 `emptyLabel` 대신 이 콘텐츠가 헤더 아래 영역을 채우며, 버튼 등 인터랙션도 동작한다. |
7304
7345
  | `loading?` | `boolean` | `false` | |
7305
7346
  | `dense?` | `boolean` | `false` | 행 높이를 좁게 (세로 여백만 줄인다 — 좌우 패딩은 그대로). **시작 밀도이자 밀도 토글의 스위치다.** 켜면 하단 바 우측에 `좁게 보기` · `넓게 보기` 토글이 붙는다 — 페이지네이션이 없으면 이 바를 토글만 담아 그린다. 누른 뒤의 밀도는 테이블이 내부 상태로 들고 가므로 `onDenseChange` 를 받지 않아도 토글은 동작하고, 넓게 본 뒤에도 토글은 그대로 남는다(붙일지는 이 prop 이 정한다). 이 prop 값이 바뀌면 내부 밀도도 그 값으로 맞춰진다. |
7306
- | `hoverable?` | `boolean` | `true` | true면 행에 마우스를 올려도 hover 배경(grey_05)을 표시하지 않는다 |
7347
+ | `hoverable?` | `boolean` | `true` | 행에 마우스를 올렸을 때 hover 배경(grey_05)을 표시할지. 기본 `true` — `false` 면 표시하지 않는다 |
7307
7348
  | `pagination?` | `STablePagination` | — | 페이지네이션 (있으면 하단 표시) |
7308
7349
  | `internalPagination?` | `boolean` | `false` | 테이블 내부에서 페이지네이션을 직접 관리 (rows를 내부 슬라이싱) |
7309
7350
  | `showRowsPerPageSelect?` | `boolean` | `false` | 페이지당 행 수 셀렉트 표시 |
package/dist/llms.txt CHANGED
@@ -485,7 +485,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
485
485
  | 여러 줄 텍스트를 받는다 | `STextarea` | §3-7-1 |
486
486
  | 제목·굵게·목록·색 같은 **서식이 남아야 하는** 글을 받는다 | `SEditor` | §3-7-1 |
487
487
  | 목록·결과를 검색어로 좁힌다 | `SSearchInput` | §3-7-1 |
488
- | 숫자(수량·금액)를 받는다 | `SNumberInput` | |
488
+ | 숫자(수량·금액)를 받는다 | `SNumberInput` | §3-7-13 |
489
489
  | 바코드를 스캔해 받는다 | `SBarcodeInput` | |
490
490
  | 목록에서 하나 고르게 한다 | `SSelect` | §3-7-2 |
491
491
  | 선택지를 항상 펼쳐 두고 하나 고르게 한다 | `SRadioGroup` | §3-7-2 |
@@ -1471,6 +1471,23 @@ const [from, setFrom] = useState<string | null>(null);
1471
1471
  ❌ <SListItem title="일반 문의" selected /> {/* clickable 없으면 선택 표시가 안 나온다 */}
1472
1472
  ```
1473
1473
 
1474
+ ##### 행 안에 컨트롤을 둘 때
1475
+
1476
+ `renderItem` 이 앱 몫이라 행 안에 입력·버튼이 들어오는 것은 예외가 아니다 — 순서를 정하는 목록은 대개 「몇 번째로 보낼지」를 숫자로도 받는다. 그냥 두면 된다.
1477
+
1478
+ - **행 선택은 옵트인이다.** `selectedKey`·`defaultSelectedKey`·`onSelectedKeyChange` 중 하나라도 주면 행이 `role="button"` 이 되어 클릭·Enter·Space 로 선택된다. 하나도 주지 않으면 행은 아무 상호작용도 갖지 않는다. **순서만 바꾸는 목록에 선택 prop 을 습관적으로 붙이지 않는다** — 붙이지 않아야 행 안의 컨트롤이 중첩 인터랙티브가 되지 않고 스크린리더에 그대로 노출된다.
1479
+ - **선택을 함께 쓰더라도 자손 컨트롤의 이벤트를 끊지 않는다.** 입력·버튼·링크에서 난 클릭과 Enter·Space 는 선택으로 새지 않는다 — 행의 제목 텍스트를 눌러 고르는 것만 선택으로 간다. `stopPropagation` 을 직접 넣을 자리가 아니다.
1480
+
1481
+ ```tsx
1482
+ ✅ <SDraggableList items={rows} getKey={r => r.id} {/* 선택을 안 쓰면 선택 prop 도 없다 */}
1483
+ renderItem={(row, _i, { onDragHandleMouseDown }) => (
1484
+ <SDraggableItem title={row.name} onDragHandleMouseDown={onDragHandleMouseDown}
1485
+ trailing={<SNumberInput width={72} value={row.order} onValueChange={…} />} />
1486
+ )} />
1487
+
1488
+ ❌ <div onKeyDown={e => { e.stopPropagation(); … }}> {/* 리스트가 이미 걸러 준다 */}
1489
+ ```
1490
+
1474
1491
  #### 3-7-7. 펼치는 셋 — SExpansionItem vs SExpansionList vs STree
1475
1492
 
1476
1493
  | 판별 | 사용 |
@@ -1578,6 +1595,29 @@ const [selectedId, setSelectedId] = useState<string>();
1578
1595
  ❌ {loading ? <SCircleProgress indeterminate /> : <SImage src={url} />} {/* SImage 가 이미 한다 */}
1579
1596
  ```
1580
1597
 
1598
+ #### 3-7-13. 숫자 — SNumberInput 의 min·max 는 검증 경계다
1599
+
1600
+ `min`·`max` 는 `<input type="number">` 의 그것과 같다 — **값을 고쳐 쓰지 않는 검증 경계**다. 범위를 벗어나면 blur·제출 시 에러 상태가 서고, 사용자가 친 값은 그대로 남아 앱의 범위 가드에 도달한다.
1601
+
1602
+ - **문구를 앱이 대지 않아도 된다.** `rules` 도 `SForm` 도 없는 화면에서 `min`·`max` 만 주면 DS 가 「1~99 사이로 입력해 주세요.」 같은 기본 안내를 자동으로 붙인다. 범위 이탈이 아무 표시 없이 지나가는 경로가 없다.
1603
+ - **화면의 말로 바꾸려면 `rules` 를 준다.** 「우선순위는 1부터」처럼 그 화면에서만 통하는 문구가 있으면 `rules` 가 DS 기본 문구를 이긴다. 앱이 직접 준 `errorMessage` 도 마찬가지다.
1604
+ - **`SForm` 안에서는 범위 이탈이 제출을 막는다.** `rules` 를 따로 걸지 않아도 그렇다 — 네이티브 폼과 같다.
1605
+ - **빈 칸(`null`)은 범위 판정 대상이 아니다.** 필수 입력은 `min` 이 아니라 `rules` 로 막는다.
1606
+ - **경계로 붙여도 사용자가 놀라지 않는 값에서만 `clampOnBlur` 를 켠다.** 켜면 blur 시 값이 경계로 **말없이 바뀌고** `onValueChange` 로 되쏘아진다 — 그래서 앱의 범위 가드에는 범위 밖 값이 도달하지 않는다. 재고 조정 수량처럼 보정이 자연스러운 자리에만 쓰고, 업무 규칙(최소 주문 수량·우선순위 시작값)에는 쓰지 않는다.
1607
+ - 스테퍼(`showButton`)와 위/아래 화살표는 `clampOnBlur` 와 무관하게 언제나 `min`·`max` 를 한계로 삼는다.
1608
+
1609
+ ```tsx
1610
+ ✅ <SNumberInput label="우선순위" min={1} max={99} value={v} onValueChange={setV} />
1611
+ {/* 0 을 넣고 blur 하면 0 이 남고 에러가 선다 — 저장 가드의 v < 1 이 그대로 걸린다 */}
1612
+
1613
+ ✅ <SNumberInput label="우선순위" min={1} max={99} value={v} onValueChange={setV}
1614
+ rules={[v => (typeof v === 'number' && (v < 1 || v > 99) ? '우선순위는 1부터 99까지입니다.' : true)]} />
1615
+ {/* 문구만 화면의 말로 바꾼다 */}
1616
+
1617
+ ❌ <SNumberInput label="우선순위" min={1} clampOnBlur value={v} onValueChange={setV} />
1618
+ {/* 0 이 1 로 바뀐 뒤 도착하므로 저장 직전의 v < 1 가드는 영원히 걸리지 않는다 */}
1619
+ ```
1620
+
1581
1621
  ---
1582
1622
 
1583
1623
  ## 4. 페이지 레시피 — 표준 골격
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sellmate-design-system-react",
3
- "version": "9.0.0-beta.43",
3
+ "version": "9.0.0-beta.44",
4
4
  "description": "Sellmate Design System — React (TypeScript + Tailwind v4) port",
5
5
  "license": "UNLICENSED",
6
6
  "keywords": [