sellmate-design-system-react 9.0.0-beta.55 → 9.0.0-beta.57

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
@@ -605,6 +605,8 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
605
605
 
606
606
  **`SScrollArea` 로 페이지 본문 전체를 감싸지 않는다.** 페이지 스크롤은 `SPage` 의 `<main>` 몫이다 — 감싸면 스크롤바가 본문 패딩 안쪽으로 들어와 페이지 가장자리에서 떨어져 그려진다 (§2-2).
607
607
 
608
+ **`SScrollArea` 에는 상한을 정해 준다 — `maxHeight` 를 주거나, 부모가 높이를 정한 자리에 놓는다.** 둘 다 없으면 영역이 내용만큼 자라 스크롤이 생기지 않는다. 남은 높이를 쓰는 자리라면 `maxHeight` 가 아니라 `className="min-h-0 flex-1"` 로 사슬을 잇는다 — 픽셀로 박으면 창 높이가 바뀔 때 따라가지 못한다.
609
+
608
610
  #### E. 다른 곳으로 이동시킨다
609
611
 
610
612
  | 하려는 일 | 컴포넌트 | 갈림 |
@@ -621,7 +623,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
621
623
  | --- | --- | --- |
622
624
  | 실행 여부만 확정받는다 | `SModal.confirm()` | §3-3-1 |
623
625
  | 모달 안에서 작성·선택하게 한다 | `SActionModal` + `SModal.create()` | §3-3-1 |
624
- | 띄우는 것 자체가 하나의 화면이다 | `SPopup` | §3-3-1 |
626
+ | 띄우는 것 자체가 하나의 화면이다 | `SPopup` | §3-3-1 · 골격은 §4-7 |
625
627
  | 화면 옆에서 밀려 나오는 작업 패널을 연다 | `SDrawer` | §3-3-5 |
626
628
  | 확인 다이얼로그를 화면에 직접 배치한다 | `SConfirmModal` | §3-3-4 |
627
629
  | 클릭하면 상호작용 가능한 작은 콘텐츠를 띄운다 | `SPopover` | §3-3 |
@@ -773,30 +775,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
773
775
  - **본문 패딩은 `SPopup` 이 토큰으로 넣는다. 직접 주지 않는다** (`p-sd-*` 로 덮어쓰면 토큰이 바뀌어도 안 따라간다). 표를 가장자리까지 채우는 등 콘텐츠가 여백을 직접 다뤄야 할 때만 `padding="none"` 으로 끈다.
774
776
  - 그 밖에는 팝업 안도 일반 페이지와 같은 규칙(§4)을 따른다: 블록 간격 `gap-sd-12`, 표는 `SKeyValueTable` / `STable`.
775
777
 
776
- ```tsx
777
- // 1) 목록에서 별도 창을 연다 — 창 크기 = 콘텐츠 크기
778
- function openDetailPopup(orderId: string) {
779
- window.open(
780
- `${window.location.origin}/popup/transfer-orders/${orderId}`,
781
- `transfer-order-${orderId}`,
782
- 'width=1200, height=800, toolbar=no, menubar=no, location=no, resizable=no',
783
- );
784
- }
785
-
786
- // 2) 그 라우트의 루트에 SPopup 을 둔다 (조회만 → 푸터 없음)
787
- export default function TransferOrderPopupPage() {
788
- return (
789
- <SPopup popupTitle="이동 오더 상세">
790
- {/* 본문 패딩은 SPopup 이 토큰으로 넣는다 — p-sd-* 로 덮어쓰지 않는다 */}
791
- <div className="flex flex-col gap-sd-12">
792
- <SSectionHeaderCard>…</SSectionHeaderCard>
793
- <STabs value={tab} tabs={TABS} onValueChange={setTab} />
794
- <STable columns={columns} rows={rows} rowKey="id" />
795
- </div>
796
- </SPopup>
797
- );
798
- }
799
- ```
778
+ **창을 여는 코드와 그 라우트의 골격은 §4-7 에 있다.** 창 높이를 `SPopup` 까지 잇는 한 줄이 빠지면 헤더·푸터가 창 밖으로 밀려나므로, 팝업 라우트는 반드시 §4-7 골격에서 시작한다.
800
779
 
801
780
  #### 3-3-4. 모달 만드는 법
802
781
 
@@ -1113,11 +1092,57 @@ const columns: STableColumn[] = [
1113
1092
  <STable selectable isRowSelectable={row => row.failedCount > 0} … />
1114
1093
  ```
1115
1094
 
1095
+ - **잠긴 행은 회색으로 가라앉는다.** 배경과 글자색이 `--cmp-table-body-disabled-*` 로 바뀌고 hover 도 꺼진다. `selectable` · `dragSelectable` 어느 쪽이든 같다. **그 회색을 직접 그리지 않는다** — `tdClass` 로 따로 칠하면 토큰이 바뀔 때 그 열만 어긋난 채 남는다.
1096
+ - **셀이 자기 색을 정한 내용까지는 닿지 않는다.** `column.render` 안의 `STag` · `SIcon` 처럼 색을 직접 받는 것은 그대로 선명하게 남는다. 잠긴 행에서 그것도 가라앉혀야 하면 `render` 에서 행 상태를 보고 정한다.
1116
1097
  - **체크박스를 직접 잠그려 하지 않는다.** 색·커서·hover 가 한 벌로 움직이므로 밖에서 속성만 바꾸면 "잠기지 않았는데 멀쩡해 보이는" 상태가 된다. 이 prop 하나로 셋이 함께 잡힌다.
1117
1098
  - **전체 선택과 Shift 구간 선택의 셈에서도 빠진다.** 잠긴 행이 셈에 남으면 "전부 선택됨"에 닿지 못해 헤더 체크박스가 해제 방향으로 못 가고 한 방향이 된다. DS 가 이걸 처리하므로 직접 보정하지 않는다.
1118
1099
  - **아예 대상이 아닌 행이라면 `rows` 에서 거르는 편이 낫다.** 잠긴 행이 잔뜩 섞이면 무엇을 고를 수 있는지가 오히려 안 읽힌다. 고를 수 있는 행이 한 줄도 없으면 헤더까지 잠기는데, 그 상태라면 `selectable` 을 켤 자리가 아니다.
1119
1100
  - 잠금은 그리는 시점의 판정이라, **이미 `selected` 에 든 행이 나중에 잠겨도 DS 가 빼지 않는다** — 제어 상태를 말없이 바꾸지 않기 때문이다. 헤더의 전체 해제로는 걷힌다.
1120
1101
 
1102
+ #### 체크박스 대신 드래그로 고르게 하려면 `dragSelectable`
1103
+
1104
+ 고르는 **수단만** 다른 같은 선택이다. `selected` · `onSelectedChange` · `isRowSelectable` 을 그대로 쓰고, 체크박스 열 대신 고른 구간이 배경색과 바깥 테두리로 표시된다.
1105
+
1106
+ ```tsx
1107
+ <STable dragSelectable selected={selected} onSelectedChange={setSelected} … />
1108
+ ```
1109
+
1110
+ | | `selectable` | `dragSelectable` |
1111
+ | --- | --- | --- |
1112
+ | 고르는 법 | 체크박스 클릭 · Shift 구간 | 행을 눌러 끌기 |
1113
+ | 흩어진 행 모으기 | 하나씩 체크 | `Ctrl`(macOS `Cmd`)을 짚고 끌기 |
1114
+ | 선택 열 | 생긴다 (48px, 왼쪽 고정) | 없다 |
1115
+ | 헤더 전체 선택 | 있다 | 없다 |
1116
+ | 셀 글자 복사 | 된다 | **안 된다** |
1117
+
1118
+ - **둘 다 켜면 `selectable` 이 이긴다.** 체크박스가 있는 표에서 드래그까지 걸리면 글자를 긁으려던 손이 선택을 통째로 갈아치운다. 어느 쪽이 맞는지 정해서 하나만 켠다.
1119
+ - **값을 복사해 가는 표에는 켜지 않는다.** 끌기가 곧 선택이라 글자 선택과 같은 손짓을 두고 다투고, 그래서 셀 안의 텍스트를 긁을 수 없다. 주문번호·송장번호처럼 복사해 쓰는 열이 있으면 `selectable` 이다.
1120
+ - **새로 끌면 이전 선택은 풀린다.** 여러 구간을 모으려면 `Ctrl`/`Cmd` 를 짚고 끈다. 이 규칙을 화면에 안내할 자리가 없으면, 흩어진 행을 자주 고르는 표에는 맞지 않는다.
1121
+ - **한 번에 이어진 구간을 고르는 표에 쓴다.** 목록에서 연속한 기간·회차를 통째로 집어 처리하는 화면이 제자리다.
1122
+ - 셀 안의 버튼·입력·링크는 그대로 눌린다 — 거기서 시작한 손짓은 드래그로 세지 않는다. 그래도 컨트롤이 빽빽한 표라면 끌 여백이 없어 잘 맞지 않는다.
1123
+ - 잠긴 행(`isRowSelectable`)은 구간 안에 있어도 그냥 지나간다 — 체크박스 쪽과 같은 규칙이다.
1124
+
1125
+ ##### 고른 행에 바로 할 일을 붙이려면 `contextMenuItems`
1126
+
1127
+ 행을 **오른쪽 클릭**하면 커서 자리에 메뉴가 뜬다. 고른 항목의 `value` 와 **그 메뉴가 다루는 행들**이 함께 온다.
1128
+
1129
+ ```tsx
1130
+ <STable
1131
+ dragSelectable
1132
+ contextMenuItems={[
1133
+ { value: 'export', label: '내보내기', icon: 'download' },
1134
+ { value: 'delete', label: '삭제', icon: 'remove' },
1135
+ ]}
1136
+ onContextMenuItemClick={(value, rows) => run(value, rows)}
1137
+ … />
1138
+ ```
1139
+
1140
+ - **`dragSelectable` 전용이다.** 체크박스 모드에 주면 무시된다. 체크박스 표에서 일괄 작업을 붙이는 자리는 표 위의 `STableBar` 다 (§4 페이지 레시피).
1141
+ - **행 목록을 인자로 받는다. `selected` 를 따로 읽지 않는다.** 오른쪽 클릭이 선택을 바꾸는 경우가 있어서, 그때 앱이 든 `selected` 는 아직 이전 값일 수 있다.
1142
+ - **고르지 않은 행에서 누르면 그 행만 골라진 뒤 열린다.** 메뉴가 다룰 대상과 화면에 칠해진 것이 어긋나지 않게 하기 위함이다. 잠긴 행 위에서는 열리지 않고 브라우저 기본 메뉴가 그대로 나온다.
1143
+ - **오른쪽 클릭에만 있는 기능을 두지 않는다.** 뜨는 것을 모르면 닿을 수 없고, 키보드로도 열 수 없다. 여기 넣는 것은 표 위 버튼이나 행 안 메뉴에도 있는 **지름길**이어야 한다.
1144
+ - 항목을 주지 않으면 오른쪽 클릭을 가로채지 않는다 — 브라우저 기본 메뉴가 그대로 뜬다.
1145
+
1121
1146
  #### 헤더 전체 선택이 집는 범위
1122
1147
 
1123
1148
  **모드가 정한다. 앱이 보정하지 않는다.**
@@ -1475,6 +1500,7 @@ tableRef.current.scrollToRow(row); // 복원
1475
1500
  - 크기 단위는 `unit` 이 정한다. 기본 `'%'` 는 창이 바뀌어도 비율을 유지하고, `'px'` 는 폭을 유지한다. **사이드바처럼 폭이 고정돼야 하는 자리는 `'px'`**, 화면을 비율로 나누는 자리는 기본값 그대로 둔다.
1476
1501
  - 본문이 읽을 수 없을 만큼 좁아지지 않도록 `limits={[최소, 최대]}` 를 준다. 생략하면 `'%'` 는 `[10, 90]`, `'px'` 는 `[50, Infinity]`.
1477
1502
  - 각 패널은 넘치는 만큼 **스스로 스크롤한다.** 패널 안에 `SScrollArea` 를 겹쳐 넣지 않는다.
1503
+ - **높이는 놓는 자리가 준다.** `SSplitter` 는 부모를 채우기만 하므로, 부모 높이가 `auto` 면 패널이 내용 높이로 자라 스크롤이 생기지 않는다 (`vertical` 은 위아래 비율 자체가 무의미해진다). 페이지 본문의 남은 높이를 쓰려면 `contentHeight="fill"` 에 스택을 `min-h-0 flex-1` 로 이어 준다 — `SCalendarBoard`·`STable` 과 같은 사슬이다 (§2-2).
1478
1504
  - 모델은 항상 **첫 패널**(`SSplitter.Before`) 크기다. 사이드가 기준인 화면이면 사이드를 `Before` 에 둔다.
1479
1505
  - 앱 셸의 GNB 폭은 `SGnb` 가 소유한다. `SLayout`/`SGnb` 를 `SSplitter` 로 감싸지 않는다 — **GNB 폭을 끌 수 있게 하려면 `SGnb` 에 `resizable` 을 준다**(§4-1).
1480
1506
 
@@ -1929,6 +1955,17 @@ export default function AppShell({
1929
1955
  }
1930
1956
  ```
1931
1957
 
1958
+ **셸을 마운트하는 자리가 뷰포트 높이를 준다 — 앱 루트에 `h-screen` 을 둔다.** `SLayout` 은 부모 높이를 채우도록만 되어 있어서, 마운트 지점의 높이가 `auto` 면 100% 가 풀려 셸이 내용 높이로 줄어든다. 그러면 GNB 가 화면 바닥까지 내려오지 않고, `SPage` 안에서 일어나야 할 스크롤이 문서(브라우저 창) 스크롤이 되어 `contentHeight="fill"` 도 최소 너비의 가로 스크롤 규칙도 함께 무너진다. `html`·`body` 에 전역 CSS 를 걸 필요는 없다 — 루트 한 겹이면 된다.
1959
+
1960
+ ```tsx
1961
+ // 앱 진입점 (main.tsx) — 셸이 창 높이를 받는 자리는 여기 하나다
1962
+ createRoot(document.getElementById('root')!).render(
1963
+ <div className="h-screen">
1964
+ <AppShell header={{ variant: 'bar', title: '주문 목록' }}>…</AppShell>
1965
+ </div>,
1966
+ );
1967
+ ```
1968
+
1932
1969
  **페이지는 `AppShell` 을 직접 호출하며 자기 `header` 를 넘긴다** — 셸은 앱에 하나뿐이므로, 페이지 제목이 페이지마다 다르다는 사실은 이렇게 프레임 컴포넌트를 통해 흘려보낸다(§4-2·§4-3·§4-4 참고).
1933
1970
 
1934
1971
  **자식 순서는 `SGnb` → `SPageHeader` → `SPage` 다.** `SLayout` 은 `SPageHeader` 자식을 보면 **그 자식부터 뒤를** 하나의 페이지 열로 묶어 헤더를 페이지 위에 고정한다 — 스크롤도 페이지 패딩도 그 아래 `SPage` 안에서만 일어난다. 그래서 순서가 규칙이다: 헤더를 `SGnb` 앞에 두면 GNB 까지 페이지 열로 딸려 들어가고, `SPage` 의 `children` 안에 넣으면 본문 패딩 안으로 들어가 스크롤과 함께 밀려 올라간다.
@@ -2445,7 +2482,7 @@ export default function ProductDetailPage() {
2445
2482
 
2446
2483
  ### 4-6. 로그인 화면 — SLoginCard
2447
2484
 
2448
- 앱 셸이 아직 없는 유일한 화면이다. `SLayout`·`SGnb`·`SPage` 가 없고, 회색 바탕 위에 카드 하나만 선다.
2485
+ 로그인 전이라 앱 셸이 아직 없다. `SLayout`·`SGnb`·`SPage` 가 없고, 회색 바탕 위에 카드 하나만 선다. (앱 셸 없이 서는 화면은 이것과 팝업 라우트(§4-7) 둘뿐이다.)
2449
2486
 
2450
2487
  ```tsx
2451
2488
  <div className="flex h-screen items-stretch justify-center bg-(--sys-color-bg-neutralLight) p-sd-24">
@@ -2487,6 +2524,66 @@ export default function ProductDetailPage() {
2487
2524
 
2488
2525
  **로그인 카드 옆에 다른 블록을 두지 않는다.** 이 화면에서 할 일은 로그인 하나이고, 옆에 공지·배너가 붙는 순간 그 뜻이 깨진다. 안내가 필요하면 `description` 이나 `footerNote` 로 넣는다.
2489
2526
 
2527
+ ### 4-7. 팝업 창 라우트 — SPopup
2528
+
2529
+ 앱 셸 없이 서는 또 하나의 화면이다. 별도 브라우저 창으로 열리는 전용 라우트이므로 `SLayout`·`SGnb`·`SPage` 가 없고, 창을 통째로 `SPopup` 하나가 채운다. 무엇을 팝업으로 열지는 §3-3-1·§3-3-3 에서 고르고, 여기서는 그 라우트의 골격만 다룬다.
2530
+
2531
+ ```tsx
2532
+ // 1) 목록에서 별도 창을 연다 — 창 크기 = 콘텐츠 크기
2533
+ function openDetailPopup(orderId: string) {
2534
+ window.open(
2535
+ `${window.location.origin}/popup/transfer-orders/${orderId}`,
2536
+ `transfer-order-${orderId}`,
2537
+ 'width=1200, height=800, toolbar=no, menubar=no, location=no, resizable=no',
2538
+ );
2539
+ }
2540
+
2541
+ // 2) 그 라우트의 루트는 SPopup 하나다 (조회만 → 푸터 없음)
2542
+ export default function TransferOrderPopupPage() {
2543
+ return (
2544
+ // h-screen 으로 창 높이를 잡는다 — 이게 없으면 본문이 창 밖으로 자란다
2545
+ <SPopup className="h-screen" popupTitle="이동 오더 상세">
2546
+ {/* 본문 패딩은 SPopup 이 토큰으로 넣는다 — p-sd-* 로 덮어쓰지 않는다 */}
2547
+ <div className="flex flex-col gap-sd-12">
2548
+ <SSectionHeaderCard>…</SSectionHeaderCard>
2549
+ <STabs value={tab} tabs={TABS} onValueChange={setTab} />
2550
+ <STable columns={columns} rows={rows} rowKey="id" />
2551
+ </div>
2552
+ </SPopup>
2553
+ );
2554
+ }
2555
+ ```
2556
+
2557
+ **높이는 라우트가 준다 — `SPopup` 에 `h-screen` 을 준다.** `SPopup` 자체는 부모 높이를 채우도록만 되어 있어서, 부모(라우트 루트·`body`)가 높이를 정해 주지 않으면 100% 가 풀려 본문이 창 밖으로 자란다. 그러면 헤더·푸터가 위아래로 밀려 창 안에 보이지 않는다. `h-screen` 이면 `html`·`body`·마운트 루트에 전역 CSS 를 걸지 않고도 창 높이가 바로 들어온다. (`body` 여백은 0 이어야 한다 — Tailwind preflight 가 이미 0 으로 만든다.)
2558
+
2559
+ **창 높이를 고정으로 가정하지 않는다.** `window.open` 의 `resizable=no` 는 브라우저가 무시하는 경우가 많아 사용자가 창을 늘리고 줄일 수 있고, 화면 해상도에 따라 처음 열리는 높이도 요청값과 달라진다. `h-screen` 으로 이어 두면 본문의 가용 높이가 창과 함께 변하고, 넘칠 때만 **본문만** 스크롤한다 — 헤더와 푸터는 자리에 남는다. 높이를 `px` 로 박거나 리사이즈를 JS 로 따라가지 않는다.
2560
+
2561
+ **표를 담으면 본문이 아니라 표가 스크롤한다.** 본문이 통째로 스크롤되면 표 헤더와 페이지네이션 바가 위아래로 밀려 사라진다. `SPopup` 의 본문은 이미 남은 높이를 잡고 있으므로, 본문 루트에 `h-full min-h-0` 으로 세로 축을 잇고 표에 `min-h-0 flex-1` 을 준다. **툴바+표를 묶은 `div` 도 사슬의 한 칸이라 거기에도 `min-h-0 flex-1` 이 필요하다** (§4-2 목록 페이지와 같은 사슬이다 — 한 칸만 끊겨도 표가 높이를 못 잡는데 실패가 조용하다).
2562
+
2563
+ ```tsx
2564
+ <SPopup
2565
+ className="h-screen"
2566
+ popupTitle="엑셀 파일 관리"
2567
+ showFooter // 확정할 작업이 있을 때만 (§3-3-3)
2568
+ submitButton={{ label: '저장' }}
2569
+ onSubmit={save}
2570
+ >
2571
+ <div className="flex h-full min-h-0 flex-col gap-sd-12">
2572
+ <SKeyValueTable fields={filterFields} values={filters} search
2573
+ onChange={({ values }) => setFilters(values)} onSearch={fetchList} />
2574
+
2575
+ {/* 툴바+표 = 한 블록. 이 div 가 사슬의 한 칸이다 (§2-0) */}
2576
+ <div className="flex min-h-0 flex-1 flex-col">
2577
+ <STableBar className="border-b-0" total={total} />
2578
+ {/* 표만 자기 안에서 스크롤한다 — 필터·툴바·푸터는 늘 보인다 */}
2579
+ <STable className="min-h-0 flex-1" columns={columns} rows={rows} rowKey="id"
2580
+ pagination={{ currentPage, lastPage }} />
2581
+ </div>
2582
+ </div>
2583
+ </SPopup>
2584
+ ```
2585
+
2586
+ 조회만 하고 표가 없는 팝업은 이 사슬이 필요 없다 — 본문이 통째로 스크롤되어도 헤더는 고정이고 밀려날 푸터가 없다.
2490
2587
 
2491
2588
  ---
2492
2589
 
@@ -2506,6 +2603,8 @@ export default function ProductDetailPage() {
2506
2603
  - [ ] `SSectionHeaderCard` 의 여백을 `p-sd-*` 가 아니라 `padding` prop 으로 줬는가
2507
2604
  - [ ] 페이지에 `contentHeight="fill"` 을 넘겼는가 (§2-2 표준 — 페이지 스크롤을 쓰는 화면에서만 `auto` + `scrollEndSpacing`)
2508
2605
  - [ ] `fill` 을 쓴 블록에서 **자식까지 `min-h-0 flex-1` 이 끊기지 않았는가** (한 군데만 끊겨도 자식이 높이를 못 잡는데 실패가 조용하다)
2606
+ - [ ] 셸을 마운트하는 앱 루트가 뷰포트 높이인가 (§4-1 — `h-screen` 이 없으면 GNB 가 바닥까지 안 오고 페이지 스크롤이 문서 스크롤이 된다)
2607
+ - [ ] 팝업 라우트라면 `SPopup` 에 `h-screen` 을 줬는가 (§4-7 — 없으면 본문이 창 밖으로 자라 헤더·푸터가 밀려난다), 표를 담았다면 표만 스크롤하는가
2509
2608
  - [ ] 자체 스크롤하는 패널의 하단에 `pb-[var(--cmp-pageBody-padding-scrollEnd)]` 이 있는가
2510
2609
  - [ ] 한 페이지에 섹션 카드가 둘 이상이면 `marker` 를 켰는가, 하나뿐이면 껐는가 (§4-5)
2511
2610
  - [ ] 같은 컴포넌트를 나열할 때 §2-2 그룹 간격을 썼는가 (체크박스 가로 `gap-sd-24` 등)
@@ -6,7 +6,12 @@ export declare const ICONS: {
6
6
  readonly account: (p: IconRenderProps) => import("react").JSX.Element;
7
7
  readonly add: (p: IconRenderProps) => import("react").JSX.Element;
8
8
  readonly alert: (p: IconRenderProps) => import("react").JSX.Element;
9
+ readonly alignCenter: (p: IconRenderProps) => import("react").JSX.Element;
9
10
  readonly alignKorean: (p: IconRenderProps) => import("react").JSX.Element;
11
+ readonly alignLeft: (p: IconRenderProps) => import("react").JSX.Element;
12
+ readonly alignMiddle: (p: IconRenderProps) => import("react").JSX.Element;
13
+ readonly alignRight: (p: IconRenderProps) => import("react").JSX.Element;
14
+ readonly alignTop: (p: IconRenderProps) => import("react").JSX.Element;
10
15
  readonly archive: (p: IconRenderProps) => import("react").JSX.Element;
11
16
  readonly arrowDown: (p: IconRenderProps) => import("react").JSX.Element;
12
17
  readonly arrowLeft: (p: IconRenderProps) => import("react").JSX.Element;
@@ -13,6 +13,8 @@
13
13
  | `rows?` | `SRow[]` | `[]` | |
14
14
  | `rowKey?` | `string` | `'id'` | 행 식별 필드 |
15
15
  | `selectable?` | `boolean` | `false` | 행 선택 체크박스 |
16
+ | `dragSelectable?` | `boolean` | `false` | 행을 드래그해서 고른다. 기본 `false`. 고르는 수단만 다를 뿐 `selectable` 과 같은 선택이다 — `selected` · `onSelectedChange` · `isRowSelectable` 을 그대로 쓴다. 체크박스 열은 생기지 않고, 대신 고른 구간이 배경색과 바깥 테두리로 표시된다. **`selectable` 과 함께 켜면 `selectable` 이 이긴다** — 체크박스가 있는 표에서 드래그까지 걸리면 글자를 긁으려던 손이 선택을 갈아치운다. 둘 다 필요해 보이면 체크박스 쪽만 남긴다. 누른 자리가 구간의 기준점이고, 끌어간 자리까지가 구간이다. **새로 끌면 이전 선택은 풀린다** — 기존 선택에 더하려면 `Ctrl`(macOS 는 `Cmd`)을 짚고 끈다. 잠긴 행(`isRowSelectable`)은 구간 안에 있어도 그냥 지나간다. **켜 두면 셀 안의 글자를 긁어 복사할 수 없다** — 끌기가 곧 선택이라 글자 선택과 같은 손짓을 두고 다툰다. 값을 복사해 가는 표에는 켜지 않는다. 셀 안의 버튼·입력은 그대로 눌린다. |
17
+ | `contextMenuItems?` | `STableContextMenuItem[]` | — | 드래그 선택에서 **행을 오른쪽 클릭했을 때** 커서 자리에 뜨는 메뉴의 항목. 주지 않거나 비면 메뉴가 뜨지 않고 브라우저 기본 메뉴가 나온다. **`dragSelectable` 에서만 동작한다** — 체크박스 모드에서는 무시된다. 고르지 않은 행에서 누르면 **그 행만 고른 뒤** 열린다. 메뉴가 다룰 대상과 화면에 칠해진 것이 어긋나지 않게 하기 위함이다. 잠긴 행(`isRowSelectable`) 위에서는 열리지 않는다 — 고를 수 없는 행을 대상으로 삼을 수 없기 때문이다. |
16
18
  | `isRowSelectable?` | `(row: SRow) => boolean` | — | 이 행을 고를 수 있는가. 주지 않으면 모든 행을 고를 수 있다. 고를 수 없는 행은 체크박스가 잠기고, **전체 선택·Shift 구간 선택의 셈에서도 빠진다** — 잠긴 행이 셈에 남으면 "전부 선택됨"에 닿지 못해 헤더 체크박스가 해제 방향으로 가지 못한다. 목록에서 지워 버리는 것과 다르다. 실패 0건인 차수처럼 **자리는 보여야 하지만 대상이 될 수는 없는 행**에 쓴다. 아예 대상이 아니라면 `rows` 에서 거르는 편이 낫다. 잠금은 그리기 시점의 판정일 뿐이라, 이미 `selected` 에 든 행이 나중에 잠겨도 DS 가 빼지 않는다(제어 상태를 말없이 바꾸지 않는다). 다만 헤더의 전체 해제로는 걷어낼 수 있다. |
17
19
  | `selected?` | `SRow[]` | `[]` | |
18
20
  | `sort?` | `STableSort \| null` | `null` | 정렬 상태 (controlled). `null`·미지정이면 정렬 없음. 컴포넌트는 정렬 상태를 갖지 않는다 — 서버 정렬이면 이 값이 곧 조회 조건이고, 뒤로가기·새로고침·링크 공유로 복원돼야 하므로 진실은 URL·store 쪽에 있어야 한다. 행을 실제로 정렬하는 것도 소비 앱 몫이다 (`STable` 은 받은 순서대로 그린다). |
@@ -43,6 +45,7 @@
43
45
 
44
46
  | Event | Type | Description |
45
47
  |-------|------|-------------|
48
+ | `onContextMenuItemClick` | `(value: string \| number, rows: SRow[]) => void` | 메뉴 항목을 골랐을 때. 고른 항목의 `value` 와 **그 메뉴가 다루는 행들**이 함께 온다. 행 목록은 메뉴가 열린 시점에 확정되므로 `selected` 를 따로 읽지 않아도 된다 — 오른쪽 클릭이 선택을 바꾸는 경우(고르지 않은 행)에도 바뀐 뒤의 행이 온다. |
46
49
  | `onSelectedChange` | `(rows: SRow[]) => void` | |
47
50
  | `onSortChange` | `(sort: STableSort \| null) => void` | 정렬 헤더 클릭 (`asc → desc → 해제` 3단). 해제되면 `null` 이 온다. 다중 정렬은 1차 안에서 지원하지 않는다. |
48
51
  | `onDenseChange` | `(dense: boolean) => void` | 밀도 변경 (하단 바의 밀도 토글을 눌렀을 때). 표시는 테이블이 알아서 바꾸므로 받지 않아도 되고, 사용자가 고른 밀도를 다음 방문까지 기억해 두려는(로컬 저장 등) 페이지만 받으면 된다. |
@@ -104,6 +107,19 @@ export interface STableHeaderGroup {
104
107
  export type SRow = Record<string, any>;
105
108
  ```
106
109
 
110
+ ### STableContextMenuItem
111
+
112
+ ```ts
113
+ /** 오른쪽 클릭 메뉴의 항목 한 줄 */
114
+ export interface STableContextMenuItem {
115
+ /** 클릭 시 `onContextMenuItemClick` 으로 돌아오는 값 */
116
+ value: string | number;
117
+ label: string;
118
+ icon?: SIconName;
119
+ disabled?: boolean;
120
+ }
121
+ ```
122
+
107
123
  ### STableSort
108
124
 
109
125
  ```ts
@@ -1,6 +1,15 @@
1
1
  import { type CSSProperties, type UIEvent as ReactUIEvent, type ReactNode } from 'react';
2
+ import { type SIconName } from '../SIcon';
2
3
  import { type SSelectOption } from '../SSelect';
3
4
  export type SRow = Record<string, any>;
5
+ /** 오른쪽 클릭 메뉴의 항목 한 줄 */
6
+ export interface STableContextMenuItem {
7
+ /** 클릭 시 `onContextMenuItemClick` 으로 돌아오는 값 */
8
+ value: string | number;
9
+ label: string;
10
+ icon?: SIconName;
11
+ disabled?: boolean;
12
+ }
4
13
  /** `STableColumn.renderCell`에 전달되는 컨텍스트 */
5
14
  export interface STableCellContext {
6
15
  row: SRow;
@@ -217,6 +226,42 @@ export interface STableProps {
217
226
  rowKey?: string;
218
227
  /** 행 선택 체크박스 */
219
228
  selectable?: boolean;
229
+ /**
230
+ * 행을 드래그해서 고른다. 기본 `false`.
231
+ *
232
+ * 고르는 수단만 다를 뿐 `selectable` 과 같은 선택이다 — `selected` · `onSelectedChange` ·
233
+ * `isRowSelectable` 을 그대로 쓴다. 체크박스 열은 생기지 않고, 대신 고른 구간이 배경색과
234
+ * 바깥 테두리로 표시된다.
235
+ *
236
+ * **`selectable` 과 함께 켜면 `selectable` 이 이긴다** — 체크박스가 있는 표에서 드래그까지
237
+ * 걸리면 글자를 긁으려던 손이 선택을 갈아치운다. 둘 다 필요해 보이면 체크박스 쪽만 남긴다.
238
+ *
239
+ * 누른 자리가 구간의 기준점이고, 끌어간 자리까지가 구간이다. **새로 끌면 이전 선택은 풀린다** —
240
+ * 기존 선택에 더하려면 `Ctrl`(macOS 는 `Cmd`)을 짚고 끈다. 잠긴 행(`isRowSelectable`)은
241
+ * 구간 안에 있어도 그냥 지나간다.
242
+ *
243
+ * **켜 두면 셀 안의 글자를 긁어 복사할 수 없다** — 끌기가 곧 선택이라 글자 선택과 같은 손짓을
244
+ * 두고 다툰다. 값을 복사해 가는 표에는 켜지 않는다. 셀 안의 버튼·입력은 그대로 눌린다.
245
+ */
246
+ dragSelectable?: boolean;
247
+ /**
248
+ * 드래그 선택에서 **행을 오른쪽 클릭했을 때** 커서 자리에 뜨는 메뉴의 항목.
249
+ * 주지 않거나 비면 메뉴가 뜨지 않고 브라우저 기본 메뉴가 나온다.
250
+ *
251
+ * **`dragSelectable` 에서만 동작한다** — 체크박스 모드에서는 무시된다.
252
+ *
253
+ * 고르지 않은 행에서 누르면 **그 행만 고른 뒤** 열린다. 메뉴가 다룰 대상과 화면에 칠해진
254
+ * 것이 어긋나지 않게 하기 위함이다. 잠긴 행(`isRowSelectable`) 위에서는 열리지 않는다 —
255
+ * 고를 수 없는 행을 대상으로 삼을 수 없기 때문이다.
256
+ */
257
+ contextMenuItems?: STableContextMenuItem[];
258
+ /**
259
+ * 메뉴 항목을 골랐을 때. 고른 항목의 `value` 와 **그 메뉴가 다루는 행들**이 함께 온다.
260
+ *
261
+ * 행 목록은 메뉴가 열린 시점에 확정되므로 `selected` 를 따로 읽지 않아도 된다 — 오른쪽
262
+ * 클릭이 선택을 바꾸는 경우(고르지 않은 행)에도 바뀐 뒤의 행이 온다.
263
+ */
264
+ onContextMenuItemClick?: (value: string | number, rows: SRow[]) => void;
220
265
  /**
221
266
  * 이 행을 고를 수 있는가. 주지 않으면 모든 행을 고를 수 있다.
222
267
  *
@@ -61,6 +61,7 @@ export const TAG_COLORS = [
61
61
  'blue',
62
62
  'darkblue',
63
63
  'indigo',
64
+ 'purple',
64
65
  ] as const;
65
66
  ```
66
67
 
@@ -1,6 +1,6 @@
1
1
  export declare const TAG_SHAPES: readonly ["square", "pill"];
2
2
  export declare const TAG_SIZES: readonly ["xs", "sm", "md"];
3
- export declare const TAG_COLORS: readonly ["grey", "red", "orange", "yellow", "green", "blue", "darkblue", "indigo"];
3
+ export declare const TAG_COLORS: readonly ["grey", "red", "orange", "yellow", "green", "blue", "darkblue", "indigo", "purple"];
4
4
  export type STagShape = (typeof TAG_SHAPES)[number];
5
5
  export type STagSize = (typeof TAG_SIZES)[number];
6
6
  export type STagColor = (typeof TAG_COLORS)[number];