sellmate-design-system-react 7.0.0 → 8.0.0

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.
@@ -0,0 +1,29 @@
1
+ /**
2
+ * 공용 브라우저 자동완성 배경 차단 클래스.
3
+ * Stencil의 styles/reset.scss + sd-input/sd-barcode-input.scss 규칙과 짝을 이루는 단일 기준이며,
4
+ * 네이티브 <input>·<textarea>를 가진 컴포넌트는 그 요소에 이 클래스를 직접 붙여 쓴다.
5
+ *
6
+ * 자동완성이 걸린 필드에 브라우저가 UA 스타일시트로 파란 배경을 칠한다. 이 배경은
7
+ * background-color로 덮을 수 없어(UA 내부에서 !important로 걸린다), 필드 배경색과 같은 색의
8
+ * 거대한 inset box-shadow로 가리는 것이 유일한 방법이다. transition은 배경 재계산이 뒤늦게
9
+ * 걸리는 경우의 보험으로, 원본 규칙에 있던 것을 그대로 옮겼다.
10
+ *
11
+ * Stencil은 전역 reset.scss로 페이지의 모든 input을 덮지만 이 패키지에는 그럴 자리가 없다 —
12
+ * theme.css는 생성 산출물이라 손댈 수 없고, Tailwind 소비자(theme.css만 import)와 컴파일 CSS
13
+ * 소비자(styles.css) 양쪽에 동시에 닿는 곳은 컴포넌트가 붙이는 유틸 클래스뿐이다.
14
+ *
15
+ * 배경색을 토큰 var()로 참조하는 것이 핵심이다. SBarcodeInput은 래퍼에서
16
+ * --sys-color-field-bg-default를 --cmp-textinput-bg-barcode(노랑)로 재바인딩하므로, 같은
17
+ * 클래스가 바코드 필드에서는 저절로 노란 배경으로 해석된다. Stencil이 sd-input(#fff)과
18
+ * sd-barcode-input(#fafaa1)에 규칙을 통째로 복제해 둔 부분이 여기서는 하나로 합쳐진다.
19
+ * 글자색도 같은 이유로 currentColor를 쓴다 — 각 컴포넌트가 input에 지정한 color를 그대로 따른다.
20
+ *
21
+ * :autofill과 :-webkit-autofill을 각각 따로 적는다. 한쪽 셀렉터를 모르는 브라우저는 그 규칙을
22
+ * 통째로 버리므로 콤마로 묶으면 안 되는데, Tailwind는 클래스마다 규칙을 나누므로 variant를
23
+ * 둘로 적는 것으로 충족된다. Tailwind v4의 autofill: 은 &:autofill만 내보내고 접두사 셀렉터는
24
+ * 만들지 않아, -webkit- 쪽은 arbitrary variant로 직접 적는다.
25
+ *
26
+ * Tailwind는 소스에 문자열로 박힌 클래스만 수집한다. 선언을 배열로 두고 조립하면 스캐너가
27
+ * 보지 못하므로 아래처럼 완성형 클래스를 그대로 나열한다(scrollbar.ts와 같은 이유).
28
+ */
29
+ export declare const AUTOFILL_RESET_CLASS: string;
@@ -0,0 +1,2 @@
1
+ /** 개발 모드 여부 — 진단 로그를 프로덕션 번들에서 조용히 만든다 */
2
+ export declare function isDev(): boolean;
@@ -0,0 +1,53 @@
1
+ import type { ReactNode } from 'react';
2
+ /**
3
+ * SModal.confirm/loading/create 가 띄운 모달의 전역 목록.
4
+ *
5
+ * 왜 필요한가: 명령형 API 는 React 밖(버튼 핸들러·async 흐름)에서 호출되므로 렌더 트리에
6
+ * 노드를 끼워 넣을 방법이 없다. 그래서 원래는 호출할 때마다 `createRoot` 로 새 React 루트를
7
+ * 만들었는데, Context 는 DOM 이 아니라 렌더 트리를 타므로 그 루트에는 앱의 Provider
8
+ * (QueryClient·Router·Theme 등)가 하나도 닿지 않았다.
9
+ *
10
+ * 이 스토어가 그 사이를 잇는다 — 명령형 호출은 여기에 push 만 하고, 앱 트리 안에 한 번
11
+ * 렌더된 `SModalOutlet` 이 구독해 `createPortal` 로 그린다. DOM 위치는 예전과 같은 body
12
+ * 직속 host 지만 렌더 트리는 앱의 자식이 되어 Provider 가 전부 상속된다.
13
+ *
14
+ * outlet 이 없는 앱(구버전 부트스트랩)에서는 SModal 이 예전 `createRoot` 경로로 폴백한다.
15
+ */
16
+ export interface SModalEntry {
17
+ /** 스토어 내 식별자 — 제거 대상 지정에 쓴다 */
18
+ id: number;
19
+ /** body 직속 host. 모달 자신은 다시 Portal 로 modal host 에 나가므로 보통 비어 있다 */
20
+ host: HTMLElement;
21
+ /** 마운트 래퍼 엘리먼트 */
22
+ node: ReactNode;
23
+ /** outlet 이 사라져 더 그릴 수 없게 됐을 때 — 콜백 마무리·정리를 엔트리 주인에게 맡긴다 */
24
+ onOrphan: () => void;
25
+ }
26
+ /** @internal useSyncExternalStore 구독 */
27
+ export declare function subscribeModalEntries(listener: () => void): () => void;
28
+ /** @internal 현재 스냅샷 — 변경이 없으면 같은 참조를 유지해야 한다 */
29
+ export declare function getModalEntries(): readonly SModalEntry[];
30
+ /** @internal SSR 스냅샷 — 서버에서는 명령형 모달이 열려 있을 수 없다 */
31
+ export declare function getServerModalEntries(): readonly SModalEntry[];
32
+ /**
33
+ * @internal outlet 존재 표시 — **렌더 단계에서** 호출한다.
34
+ *
35
+ * 커밋 후 effect 로 등록하면 늦다. effect 는 자식→부모 순으로 돌기 때문에, 페이지가 최초
36
+ * 마운트 effect 에서 모달을 열면 트리 뒤쪽에 있는 outlet 의 effect 가 아직 돌지 않아
37
+ * "outlet 없음"으로 판정되고 폴백 경로를 탄다 — 이 구조가 없애려던 Context 단절이 그대로
38
+ * 재현된다. 렌더는 어떤 effect 보다도 앞서므로 여기서 표시한다. (플래그 세팅뿐이라 멱등)
39
+ */
40
+ export declare function markModalOutletRendered(): void;
41
+ /**
42
+ * @internal outlet 마운트 등록. 반환값을 effect cleanup 으로 호출한다.
43
+ * (StrictMode 의 mount→unmount→mount 를 카운터로 견딘다)
44
+ */
45
+ export declare function registerModalOutlet(): () => void;
46
+ /** @internal outlet 이 앱 트리에 있는가 — SModal 이 마운트 방식을 고르는 기준 */
47
+ export declare function hasModalOutlet(): boolean;
48
+ /** @internal outlet 이 이미 커밋돼 있는가 — 중복 outlet 개발 경고용 */
49
+ export declare function isModalOutletMounted(): boolean;
50
+ /** @internal 모달 추가 → outlet 이 그린다. 반환한 id 로 나중에 제거한다 */
51
+ export declare function addModalEntry(host: HTMLElement, node: ReactNode, onOrphan: () => void): number;
52
+ /** @internal 모달 제거 → outlet 이 언마운트하고 host 도 함께 정리한다 */
53
+ export declare function removeModalEntry(id: number): void;
@@ -36,12 +36,12 @@
36
36
  | **입력 (폼)** | `SForm` `SField` `SInput` `SNumberInput` `STextarea` `SSelect` `SCheckbox` `SRadio` `SRadioButton` `SChip` `SChipInput` `SBarcodeInput` `SFilePicker` |
37
37
  | **날짜·시간** | `SCalendar` `SDatePicker` `SDateRangePicker` `STimePicker` `STimeRangePicker` |
38
38
  | **표·목록** | `STable` `STableBar` `SKeyValueTable` `SList` `SListItem` `SDraggableItem` |
39
- | **레이아웃** | `SLayout` `SGnb` `SPage` `SSectionHeaderCard` `SCard` `SDivider` `SScrollArea` `SExpansionItem` |
39
+ | **레이아웃** | `SLayout` `SGnb` `SPage` `SSectionHeaderCard` `SCard` `SDivider` `SSplitter` `SScrollArea` `SExpansionItem` |
40
40
  | **내비게이션** | `STabs` `SPagination` `SStepper` |
41
41
  | **표시·상태** | `STag` `SBadge` `SIcon` `SCallout` `SGuide` |
42
42
  | **진행·로딩** | `SLinearProgress` `SCircleProgress` `SLoadingContainer` `SLoadingModal` |
43
43
  | **오버레이** | `STooltip` `SPopover` `SPopup` `SPortal` |
44
- | **모달** | `SModal.confirm()` `SModal.create()` + `SActionModal` `SConfirmModal` |
44
+ | **모달** | `SModal.confirm()` `SModal.create()` + `SActionModal` `SConfirmModal` `SModalOutlet`(앱 루트 1회) |
45
45
  | **알림** | `SToast` `SToastContainer` |
46
46
 
47
47
  표에 없는 UI 를 만들어야 할 때만 `div` 로 직접 조립하고, 그때도 §1-2 · §2 의 토큰 규칙을 지킨다.
@@ -460,7 +460,7 @@ function OrderModal({ orderId, open, onOpenChange, onClose, modalRef }: OrderMod
460
460
  width={720}
461
461
  // 주 액션은 button(단수), 보조 버튼은 footerLeft — 하단 버튼 양끝 분리 규칙과 같다
462
462
  button={{ label: '접수', onClick: () => modalRef.ok() }}
463
- footerLeft={<SButton color="neutral" outline label="취소" onClick={() => modalRef.cancel()} />}
463
+ footerLeft={<SButton color="neutral" outline size="md" label="취소" onClick={() => modalRef.cancel()} />}
464
464
  >
465
465
  <SKeyValueTable fields={orderFields} values={order} />
466
466
  </SActionModal>
@@ -473,6 +473,10 @@ SModal.create({ component: OrderModal, componentProps: { orderId } })
473
473
  .onDismissed(() => {});
474
474
  ```
475
475
 
476
+ **하단 버튼을 본문(children)에 직접 두지 않는다.** 주 액션은 `button`, 보조 버튼은 `footerLeft` 로 넘긴다 — 푸터 배경·여백·양끝 분리가 컴포넌트 규칙대로 잡히는 자리다. `button` 은 클릭해도 **모달을 닫지 않으므로**(`onClick` 만 발화) 저장 API 응답을 보고 `modalRef.ok()` 로 닫으면 되고, 그 때문에 본문에 버튼을 따로 둘 이유가 없다. `footerLeft` 는 슬롯이라 `SButton` 을 직접 배치하며 `size="md"` 를 명시한다(§3-5-2).
477
+
478
+ **모달 안에서도 앱의 훅을 그냥 쓴다 — 단, 앱 루트에 `SModalOutlet` 이 있어야 한다 (§4-1).** outlet 이 있으면 명령형 모달이 앱 렌더 트리의 자식으로 그려지므로 `useQuery`·`useNavigate`·`useTheme` 같은 Context 기반 훅이 페이지에서와 똑같이 동작한다. **모달 컴포넌트를 Provider 로 다시 감싸지 않는다.** outlet 없이 띄우면 모달이 별도 React 루트로 떠서 Provider 가 하나도 닿지 않고, `No QueryClient set` 처럼 모달을 여는 순간에만 터진다.
479
+
476
480
  #### 3-3-5. 닫기 경로 — `persistent` 기본값은 컴포넌트마다 다르다
477
481
 
478
482
  **입력을 담는 모달은 백드롭 클릭·ESC 로 닫히지 않는 것이 기본**이다. 닫기 시도는 흔들림(shake)으로만 반응한다. 작성 중인 내용을 실수로 잃지 않게 하기 위한 것이다. 반면 `SConfirmModal` 은 잃을 입력이 없으므로 **백드롭·ESC 로 닫히는 것이 기본**이다.
@@ -723,6 +727,41 @@ const columns: STableColumn[] = [
723
727
  </div>
724
728
  ```
725
729
 
730
+ ### 3-6. 영역 나누기 — SDivider vs SSplitter
731
+
732
+ | 상황 | 사용 |
733
+ | --- | --- |
734
+ | 두 영역 사이에 **선만** 그을 때 | `SDivider` — 위치·두께가 고정된 구분선이다 |
735
+ | 사용자가 **경계를 끌어 넓이를 바꿀 수 있어야** 할 때 | `SSplitter` — 두 패널을 감싸고 경계 위치를 소유한다 |
736
+
737
+ `SSplitter` 의 구분선은 평소 자리만 잡고 칠해지지 않다가, 경계에 커서를 올리거나 포커스를 주면 그때 드러난다 — 조절 가능한 자리라는 신호다. **항상 보이는 선이 필요하면 `SDivider` 를 쓴다.** 선 색·두께·주변 여백은 토큰이 정하므로 직접 주지 않는다.
738
+
739
+ ```tsx
740
+ <SSplitter defaultValue={30} limits={[20, 60]}>
741
+ <SSplitter.Before>내비게이션</SSplitter.Before>
742
+ <SSplitter.After>본문</SSplitter.After>
743
+ </SSplitter>
744
+ ```
745
+
746
+ - 자식은 `SSplitter.Before` 와 `SSplitter.After` **둘뿐**이고, 루트의 직접 자식이어야 한다.
747
+ - 크기 단위는 `unit` 이 정한다. 기본 `'%'` 는 창이 바뀌어도 비율을 유지하고, `'px'` 는 폭을 유지한다. **사이드바처럼 폭이 고정돼야 하는 자리는 `'px'`**, 화면을 비율로 나누는 자리는 기본값 그대로 둔다.
748
+ - 본문이 읽을 수 없을 만큼 좁아지지 않도록 `limits={[최소, 최대]}` 를 준다. 생략하면 `'%'` 는 `[10, 90]`, `'px'` 는 `[50, Infinity]`.
749
+ - 각 패널은 넘치는 만큼 **스스로 스크롤한다.** 패널 안에 `SScrollArea` 를 겹쳐 넣지 않는다.
750
+ - 모델은 항상 **첫 패널**(`SSplitter.Before`) 크기다. 사이드가 기준인 화면이면 사이드를 `Before` 에 둔다.
751
+ - 앱 셸의 GNB 폭은 `SGnb` 가 소유한다. `SLayout`/`SGnb` 를 `SSplitter` 로 감싸지 않는다 — **GNB 폭을 끌 수 있게 하려면 `SGnb` 에 `resizable` 을 준다**(§4-1).
752
+
753
+ **Quasar `q-splitter` 에서 옮겨올 때** — `unit` · `limits` · `emitImmediately` 는 이름과 뜻이 같고, 나머지는 아래처럼 바뀐다. `emitImmediately` 를 주지 않으면 **드래그를 놓는 순간 한 번만** `onValueChange` 가 온다.
754
+
755
+ | q-splitter | SSplitter |
756
+ | --- | --- |
757
+ | `horizontal` (상/하 분할) | **`vertical`** — 이 저장소는 `STabs`·`SStepper` 와 같이 "세로 **배치**" 로 읽는다 |
758
+ | `v-model` | `value` + `onValueChange` (또는 `defaultValue`) |
759
+ | `disable` | `disabled` |
760
+ | `before` / `after` 슬롯 | `SSplitter.Before` / `SSplitter.After` |
761
+ | `before-class` / `after-class` | 각 슬롯에 `className` 을 직접 |
762
+ | `separator-class` / `separator-style` | `dividerClassName` / `dividerStyle` |
763
+ | `reverse` · `dark` | 없음 — 모델은 항상 첫 패널 크기다 |
764
+
726
765
  ---
727
766
 
728
767
  ## 4. 페이지 레시피 — 표준 골격
@@ -758,6 +797,30 @@ export default function AppShell({
758
797
  }
759
798
  ```
760
799
 
800
+ **GNB 폭을 사용자가 조절하게 하려면 `SGnb` 에 `resizable` 을 준다.** 메뉴 오른쪽 경계가 조절선이 되고, 레일 폭은 고정된 채 메뉴 컬럼만 늘고 준다. 범위는 컴포넌트가 정하므로 숫자를 직접 주지 않는다.
801
+
802
+ ```tsx
803
+ {/* 폭을 기억해야 하면 menuWidth 를 앱이 쥐고 onMenuWidthChange 로 되받아 저장한다.
804
+ 초기값만 정하면 되면 defaultMenuWidth 하나로 끝난다. */}
805
+ <SGnb items={MENU} value={current} onValueChange={navigate} resizable defaultMenuWidth={240} />
806
+ ```
807
+
808
+ `onMenuWidthChange` 는 **드래그를 놓는 순간**(또는 방향키 조작) 한 번만 온다 — 저장 로직을 그대로 붙여도 프레임마다 쓰이지 않는다. 접혀 있거나 레일 리프가 활성이라 깔 메뉴가 없으면 조절선은 나오지 않는다.
809
+
810
+ **앱 부트스트랩에는 `<SModalOutlet />` 을 한 번 렌더한다 — Provider 안쪽에 둔다.**
811
+
812
+ ```tsx
813
+ import { SModalOutlet } from 'sellmate-design-system-react';
814
+
815
+ // 앱 진입점 (main.tsx / App.tsx) — 앱 전체에 하나. 위치는 Provider 안쪽이기만 하면 된다
816
+ <QueryClientProvider client={queryClient}>
817
+ <RouterProvider router={router} />
818
+ <SModalOutlet />
819
+ </QueryClientProvider>;
820
+ ```
821
+
822
+ `SModal.confirm/loading/create` 로 띄운 모달이 그려지는 자리다. outlet 이 앱 트리 안에 있어야 모달이 앱의 Context(QueryClient·Router·Theme 등)를 상속한다 — outlet 이 없으면 모달은 뜨지만 별도 React 루트라 Provider 가 닿지 않는다(§3-3-4). 모달은 언제나 `body` 로 portal 되므로 outlet 을 어디에 두든 레이아웃에는 영향이 없고, **셸 안에 넣을 필요도 없다.** 토스트를 쓴다면 `SToastContainer` 도 같은 자리에 둔다.
823
+
761
824
  **최소 너비는 `SLayout` 이 보장한다 — 앱이 `min-w-*` 를 직접 주지 않는다.** 창이 최소 너비(`SLAYOUT_MIN_WIDTH`, GNB 포함한 전체 기준)보다 좁아지면 GNB·상단바는 제자리에 남고 **`SPage` 안에만 가로 스크롤이 생긴다.** 문서(브라우저 창)에는 가로 스크롤이 생기지 않는다. 그래서 셸을 감싸는 요소에 `min-width` 나 `overflow-x` 를 걸지 않는다 — 걸면 창 전체가 스크롤되어 GNB 가 화면 밖으로 밀린다.
762
825
 
763
826
  **셸의 `SPage` 는 모든 페이지가 공유하므로, 스크롤 끝 여백을 끄려면 페이지가 셸에 알려야 한다.** 위처럼 프레임 컴포넌트가 `scrollEndSpacing` 을 받아 그대로 넘기고, 페이지네이션이 있는 목록 페이지만 `false` 를 준다 (§4-2). 나머지 페이지는 넘기지 않으면 기본값(켬)이 적용된다.
@@ -1077,6 +1140,8 @@ export default function ProductDetailPage() {
1077
1140
  - [ ] `SGhostButton` 의 `intent` 가 조작 성격과 맞는가 (되돌릴 수 없는 삭제만 `danger`, 진입·추가는 `action`, 나머지는 `default`)
1078
1141
  - [ ] 창을 띄울 때 §3-3-1 판별 순서를 따랐는가 (그 자체가 화면 → `SPopup` / 실행 여부만 확정 → `SModal.confirm` / 모달 안에서 작성 → `SActionModal`)
1079
1142
  - [ ] 작업용 모달을 `SActionModal` + `SModal.create` 로 만들었는가 (직접 오버레이 ❌)
1143
+ - [ ] 모달·드로어의 하단 버튼을 본문이 아니라 `button` · `footerLeft` prop 으로 넘겼는가 (§3-3-4)
1144
+ - [ ] 앱 부트스트랩의 Provider 안쪽에 `<SModalOutlet />` 이 한 번 렌더되어 있는가 (§4-1 — 없으면 모달 안에서 앱 훅이 죽는다), 그 대신으로 모달 컴포넌트를 Provider 로 다시 감싸지 않았는가
1080
1145
  - [ ] 상태 표시·알림·확인 다이얼로그가 §3의 선택 규칙을 따르는가
1081
1146
 
1082
1147
  ---
@@ -2435,6 +2500,9 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2435
2500
  | `railFooter?` | `ReactNode` | — | 레일 하단 고정 슬롯. 레일 아이템이 많아 넘치면 아이템 목록(ul)만 스크롤되고 이 슬롯은 레일 하단에 붙어 고정된다. useRail 일 때만 렌더된다. |
2436
2501
  | `menuFooter?` | `ReactNode` | — | 메뉴 하단 고정 슬롯. 메뉴 아이템이 많아 넘치면 메뉴 목록(ul)만 스크롤되고 이 슬롯은 메뉴 하단에 붙어 고정된다. 메뉴가 렌더될 때만(showMenu) 나타난다. |
2437
2502
  | `foldedFooter?` | `ReactNode` | — | 접힘(fix 레일) 하단 고정 슬롯. 접으면 본문이 빠져나가며 rail/menu footer 도 사라지므로, 48px 폴드 레일 바닥에 붙는 별도 슬롯이다. header="fix" 로 접혔을 때만 나타난다. |
2503
+ | `resizable?` | `boolean` | `false` | 메뉴 폭을 드래그로 조절할 수 있게 한다. 레일 폭은 고정이고 **메뉴 컬럼만** 늘고 준다. 조절선은 GNB 컬럼의 오른쪽 경계 전체다 — fix 는 상단바 높이까지, full 은 상단바가 전폭이라 본문 높이까지. 접혀 있거나 깔 메뉴가 없으면(레일 리프가 활성) 조절선이 나오지 않는다. |
2504
+ | `menuWidth?` | `number` | — | 메뉴 폭(px). 주면 controlled — onMenuWidthChange 로 직접 갱신해야 움직인다 |
2505
+ | `defaultMenuWidth?` | `number` | — | 메뉴 초기 폭(px). uncontrolled |
2438
2506
  | `ariaLabel?` | `string` | `'global navigation'` | 메뉴 landmark(nav) 의 접근성 레이블. 한 화면에 nav 가 여럿일 때 구분한다. |
2439
2507
 
2440
2508
  #### Events
@@ -2445,6 +2513,7 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2445
2513
  | `onRailChange` | `(value: string) => void` | 레일 선택 변경. 레일 아이템을 눌러 패널이 바뀔 때 알린다(선택 상태는 SGnb 가 자체 관리). |
2446
2514
  | `onFoldChange` | `(folded: boolean) => void` | 접힘 토글 (sdFoldChange) |
2447
2515
  | `onLauncherClick` | `() => void` | 앱런처(그리드) 버튼 클릭. 미지정 시 런처 버튼을 렌더하지 않는다. 접힘 레일(fix)에는 폴드 버튼만 남으므로 표시되지 않는다. |
2516
+ | `onMenuWidthChange` | `(width: number) => void` | 메뉴 폭이 확정될 때(드래그를 놓거나 방향키 조작). 드래그하는 동안에는 오지 않는다 |
2448
2517
 
2449
2518
  ## Dependencies
2450
2519
 
@@ -2847,6 +2916,8 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2847
2916
 
2848
2917
  호출 시마다 `document.body` 에 컨테이너를 만들어 모달을 렌더하고, 닫힘 애니메이션이 끝나면 자동으로 언마운트한다. 모든 메서드는 체이닝 핸들 [`SModalRef`](#smodalref) 를 반환한다.
2849
2918
 
2919
+ > **앱 부트스트랩에 [`<SModalOutlet />`](#smodaloutlet) 을 한 번 렌더한다.** 그래야 명령형 모달이 앱 렌더 트리의 자식으로 그려져 QueryClient·Router·Theme 등 Context 를 상속한다. 없으면 예전처럼 별도 React 루트로 떠서 **앱의 Provider 가 하나도 닿지 않는다.**
2920
+
2850
2921
  | 메서드 | 띄우는 모달 | 용도 | 주요 콜백/제어 |
2851
2922
  |---|---|---|---|
2852
2923
  | [`SModal.confirm(options)`](#smodalconfirm) | `SConfirmModal` | 확인/취소 | `onOk` / `onCancel` / `onClose` |
@@ -2861,6 +2932,29 @@ import { SModal } from 'sellmate-design-system-react';
2861
2932
 
2862
2933
  ---
2863
2934
 
2935
+ ## SModalOutlet
2936
+
2937
+ 명령형 모달이 **그려지는 자리**. 앱 부트스트랩에서 Provider 안쪽에 **한 번만** 렌더한다. props 는 없다.
2938
+
2939
+ ```tsx
2940
+ import { SModalOutlet } from 'sellmate-design-system-react';
2941
+
2942
+ <QueryClientProvider client={queryClient}>
2943
+ <RouterProvider router={router} />
2944
+ <SModalOutlet /> {/* 앱 전체에 하나 */}
2945
+ </QueryClientProvider>;
2946
+ ```
2947
+
2948
+ `SModal.confirm/loading/create` 는 전역 스토어에 모달을 넣기만 하고, outlet 이 그것을 `createPortal` 로 `body` 에 그린다. **DOM 위치·쌓임 순서는 outlet 유무와 무관하게 같고**, 달라지는 것은 렌더 트리다 — outlet 이 있으면 모달이 앱 트리의 자식이 되어 Context 를 상속한다.
2949
+
2950
+ - outlet 을 어디에 두든(Provider 안쪽이기만 하면) 모달은 `body` 로 portal 되므로 레이아웃·`overflow`·`transform` 의 영향을 받지 않는다.
2951
+ - **호출부 API 는 그대로다.** outlet 도입 전 코드를 고칠 필요가 없다.
2952
+ - outlet 이 없으면 예전처럼 별도 React 루트(`createRoot`)로 마운트되어 모달은 뜨지만 앱의 Provider 가 닿지 않는다 (개발 모드에서 1회 `console.warn`).
2953
+ - 모달 본문이 렌더 중 예외를 던지면 모달만 닫히고 앱 트리는 유지된다 — 원인을 지목하는 `console.error` 가 함께 찍힌다.
2954
+ - 모달이 떠 있는 채로 outlet 이 언마운트되면(앱 언마운트·Provider 교체) 그 모달은 정리되고 `onDismissed` 가 발화한다 — 남아서 되살아나지 않는다.
2955
+
2956
+ ---
2957
+
2864
2958
  ## SModal.confirm
2865
2959
 
2866
2960
  아이콘 + 제목 + 메시지 + 확인/취소 버튼. `type` 에 따라 아이콘·메인 버튼 색이 결정된다.
@@ -2962,30 +3056,40 @@ SModal.create({ component: OrderModal, componentProps: { orderId: 'ORD-001' } })
2962
3056
 
2963
3057
  ### 비동기 제출 — 응답 보고 닫기
2964
3058
 
2965
- SActionModal `button` 푸터 버튼은 클릭 **즉시 닫힌다**. 저장 API 응답에 따라 닫힘 여부를 정해야 하면 푸터 버튼 대신 **본문에 버튼을 두고** `modalRef` 로 닫힘 시점을 직접 제어한다.
3059
+ **하단 버튼은 본문에 직접 두지 않는다.** 액션은 `button`(의도적으로 단수), 보조 버튼은 `footerLeft` 슬롯에 넣는다 그래야 푸터 배경·여백·양끝 분리가 컴포넌트 규칙대로 잡힌다.
3060
+
3061
+ `button` 은 클릭해도 **모달을 닫지 않는다.** `onClick` 만 발화하므로 저장 API 응답을 보고 `modalRef.ok()` 로 닫으면 된다.
2966
3062
 
2967
3063
  ```tsx
2968
3064
  function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderModalProps) {
2969
3065
  const [error, setError] = useState('');
3066
+ const [saving, setSaving] = useState(false);
2970
3067
  const handleSubmit = async () => {
3068
+ setSaving(true);
2971
3069
  try {
2972
3070
  await save(orderId);
2973
3071
  modalRef.ok(); // 성공 → onOk + 닫기
2974
3072
  } catch {
2975
3073
  setError('저장 실패'); // 실패 → 모달 유지
3074
+ } finally {
3075
+ setSaving(false);
2976
3076
  }
2977
3077
  };
2978
3078
  return (
2979
- // button 주지 않으면 푸터가 렌더되지 않는다
2980
- <SActionModal open={open} onOpenChange={onOpenChange} onClose={onClose} persistent modalTitle="주문 처리">
3079
+ // button footerLeft 도 주지 않으면 푸터가 렌더되지 않는다
3080
+ <SActionModal
3081
+ open={open} onOpenChange={onOpenChange} onClose={onClose} persistent modalTitle="주문 처리"
3082
+ button={{ label: saving ? '저장 중...' : '저장', disabled: saving, onClick: handleSubmit }}
3083
+ footerLeft={<SButton color="neutral" outline size="md" label="취소" disabled={saving} onClick={() => modalRef.cancel()} />}
3084
+ >
2981
3085
  {error && <p>{error}</p>}
2982
- <SButton label="저장" onClick={handleSubmit} />
2983
- <SButton label="취소" onClick={() => modalRef.cancel()} />
2984
3086
  </SActionModal>
2985
3087
  );
2986
3088
  }
2987
3089
  ```
2988
3090
 
3091
+ `button` 은 `label` · `color`(기본 `primary`) · `outline` · `size`(기본 `md`) · `disabled` · `onClick` 을 받는다. `footerLeft` 는 슬롯이라 `SButton` 을 직접 배치하며, 푸터 규칙상 `size="md"` 를 명시한다.
3092
+
2989
3093
  ---
2990
3094
 
2991
3095
  ## SModalRef
@@ -3021,7 +3125,7 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
3021
3125
 
3022
3126
  - **선언형과 공존**: 서비스는 추가 API다. open 상태가 앱 상태/라우트에 묶인 경우엔 선언형 `<SConfirmModal open>` / `<SLoadingModal open>` 이 더 적합하다.
3023
3127
  - **백드롭·ESC = 중립적 닫힘**: 특정 콜백(onClose 등) 없이 `onDismissed` 만 발화한다. 명시적 버튼·메서드만 onOk/onCancel/onClose 를 발화한다.
3024
- - **Context 미상속**: `create` 의 커스텀 컴포넌트는 React 트리(createRoot)에서 렌더되어 부모의 Context Provider(Theme·Store 등)를 상속하지 않는다. 필요하면 컴포넌트 내부에서 직접 Provider 로 감싸라. (confirm/loading 은 토큰이 `:root` CSS 변수라 무관)
3128
+ - **Context outlet 이 있어야 상속된다**: [`<SModalOutlet />`](#smodaloutlet) 을 앱 부트스트랩에 렌더하면 `create` 의 커스텀 컴포넌트(와 `confirm/loading` `contentSlot`) 트리의 자식으로 렌더되어 QueryClient·Router·Theme 등을 그대로 쓴다. outlet 없으면 별도 React 트리에서 렌더되어 Provider 가 닿지 않는다 — `useQuery` 는 `No QueryClient set`, `useNavigate` 는 `may be used only in the context of a <Router>` 죽는다. (토큰은 `:root` CSS 변수라 어느 쪽이든 무관)
3025
3129
  - **`create` 의 `component` 는 SActionModal 을 루트로**: `create` 는 컨테이너를 덧씌우지 않으므로, 본문만 렌더하는 컴포넌트를 넘기면 딤·카드 없이 콘텐츠가 그대로 화면에 붙는다. TypeScript 는 이를 막지 못한다(`component` 타입이 아무 컴포넌트나 허용). 개발 모드에서는 마운트 직후 렌더 결과로 이를 감지해 `console.warn` 으로 경고한다 — 세 모달은 모두 Portal 로 `body` 에 렌더되므로 `create` 가 만든 host 는 비어 있어야 하는데, host 에 엘리먼트가 남아 있으면 모달이 아닌 것으로 판정한다.
3026
3130
 
3027
3131
  ## Dependencies
@@ -3593,6 +3697,35 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
3593
3697
 
3594
3698
  ---
3595
3699
 
3700
+ # SSplitter
3701
+
3702
+ > 자동 생성 문서 — `npm run docs:gen`. 소스: 각 컴포넌트의 Props/Handle 인터페이스 + import 의존성.
3703
+
3704
+ ### SSplitter
3705
+
3706
+ #### Props
3707
+
3708
+ | Prop | Type | Default | Description |
3709
+ |------|------|---------|-------------|
3710
+ | `value?` | `number` | — | 첫 패널 크기. unit 단위. 주면 controlled |
3711
+ | `defaultValue?` | `number` | — | 첫 패널 초기 크기. uncontrolled. 기본은 '%' 면 50, 'px' 면 240 |
3712
+ | `unit?` | `SSplitterUnit` | `'%'` | 모델·limits 를 읽는 단위 |
3713
+ | `limits?` | `readonly [number, number]` | — | [최소, 최대]. 생략하면 '%' 는 [10, 90], 'px' 는 [50, Infinity] |
3714
+ | `emitImmediately?` | `boolean` | `false` | 드래그하는 동안에도 onValueChange 를 계속 보낸다 |
3715
+ | `vertical?` | `boolean` | `false` | true면 패널을 위아래로 쌓는다 (Quasar q-splitter 의 horizontal 에 해당) |
3716
+ | `disabled?` | `boolean` | `false` | true면 크기를 바꿀 수 없다. 커서도 구분선도 나오지 않는다 |
3717
+ | `dividerClassName?` | `string` | — | 구분선에 얹을 클래스 |
3718
+ | `dividerStyle?` | `CSSProperties` | — | 구분선에 얹을 인라인 스타일 |
3719
+ | `children?` | `ReactNode` | — | SSplitter.Before 와 SSplitter.After 둘 |
3720
+
3721
+ #### Events
3722
+
3723
+ | Event | Type | Description |
3724
+ |-------|------|-------------|
3725
+ | `onValueChange` | `(value: number) => void` | 크기가 확정될 때. emitImmediately 가 아니면 드래그를 놓는 순간 한 번만 온다 |
3726
+
3727
+ ---
3728
+
3596
3729
  # SStepper
3597
3730
 
3598
3731
  > 자동 생성 문서 — `npm run docs:gen`. 소스: 각 컴포넌트의 Props/Handle 인터페이스 + import 의존성.
@@ -3606,15 +3739,31 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
3606
3739
  | `items` | `SStepperItem[]` | — | 단계 목록 |
3607
3740
  | `value?` | `string` | — | 현재 활성 단계 value |
3608
3741
  | `size?` | `SStepperSize` | `'sm'` | 크기 |
3742
+ | `vertical?` | `boolean` | `false` | 세로형 여부 |
3743
+ | `clickable?` | `boolean` | — | 단계 클릭 가능 여부. 미지정 시 가로형=false, 세로형=true |
3609
3744
  | `ariaLabel?` | `string` | `'진행 단계'` | 접근성 레이블 |
3610
3745
  | `className?` | `string` | — | |
3611
3746
  | `style?` | `CSSProperties` | — | |
3612
3747
 
3748
+ #### Events
3749
+
3750
+ | Event | Type | Description |
3751
+ |-------|------|-------------|
3752
+ | `onValueChange` | `(value: string, item: SStepperItem, index: number) => void` | 단계 선택 시 호출 |
3753
+
3754
+ #### Methods (ref)
3755
+
3756
+ | Method | Type | Description |
3757
+ |--------|------|-------------|
3758
+ | `showItemTooltip` | `() => void` | error 아이템 중 첫 번째에 툴팁을 연다. error 아이템이 없으면 아무 일도 일어나지 않는다. |
3759
+ | `hideItemTooltip` | `() => void` | 열려있는 커스텀 툴팁을 닫는다. 활성 단계(value prop)가 바뀌면 별도 호출 없이도 자동으로 닫힌다. |
3760
+
3613
3761
  ## Dependencies
3614
3762
 
3615
3763
  ### Depends on
3616
3764
 
3617
3765
  - [SIcon](../SIcon)
3766
+ - [STooltip](../STooltip)
3618
3767
 
3619
3768
  ### Graph
3620
3769
 
@@ -4168,6 +4317,7 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4168
4317
  - [SField](../SField)
4169
4318
  - [SKeyValueTable](../SKeyValueTable)
4170
4319
  - [SSectionHeaderCard](../SSectionHeaderCard)
4320
+ - [SStepper](../SStepper)
4171
4321
 
4172
4322
  ### Depends on
4173
4323
 
package/dist/llms.txt CHANGED
@@ -37,12 +37,12 @@
37
37
  | **입력 (폼)** | `SForm` `SField` `SInput` `SNumberInput` `STextarea` `SSelect` `SCheckbox` `SRadio` `SRadioButton` `SChip` `SChipInput` `SBarcodeInput` `SFilePicker` |
38
38
  | **날짜·시간** | `SCalendar` `SDatePicker` `SDateRangePicker` `STimePicker` `STimeRangePicker` |
39
39
  | **표·목록** | `STable` `STableBar` `SKeyValueTable` `SList` `SListItem` `SDraggableItem` |
40
- | **레이아웃** | `SLayout` `SGnb` `SPage` `SSectionHeaderCard` `SCard` `SDivider` `SScrollArea` `SExpansionItem` |
40
+ | **레이아웃** | `SLayout` `SGnb` `SPage` `SSectionHeaderCard` `SCard` `SDivider` `SSplitter` `SScrollArea` `SExpansionItem` |
41
41
  | **내비게이션** | `STabs` `SPagination` `SStepper` |
42
42
  | **표시·상태** | `STag` `SBadge` `SIcon` `SCallout` `SGuide` |
43
43
  | **진행·로딩** | `SLinearProgress` `SCircleProgress` `SLoadingContainer` `SLoadingModal` |
44
44
  | **오버레이** | `STooltip` `SPopover` `SPopup` `SPortal` |
45
- | **모달** | `SModal.confirm()` `SModal.create()` + `SActionModal` `SConfirmModal` |
45
+ | **모달** | `SModal.confirm()` `SModal.create()` + `SActionModal` `SConfirmModal` `SModalOutlet`(앱 루트 1회) |
46
46
  | **알림** | `SToast` `SToastContainer` |
47
47
 
48
48
  표에 없는 UI 를 만들어야 할 때만 `div` 로 직접 조립하고, 그때도 §1-2 · §2 의 토큰 규칙을 지킨다.
@@ -461,7 +461,7 @@ function OrderModal({ orderId, open, onOpenChange, onClose, modalRef }: OrderMod
461
461
  width={720}
462
462
  // 주 액션은 button(단수), 보조 버튼은 footerLeft — 하단 버튼 양끝 분리 규칙과 같다
463
463
  button={{ label: '접수', onClick: () => modalRef.ok() }}
464
- footerLeft={<SButton color="neutral" outline label="취소" onClick={() => modalRef.cancel()} />}
464
+ footerLeft={<SButton color="neutral" outline size="md" label="취소" onClick={() => modalRef.cancel()} />}
465
465
  >
466
466
  <SKeyValueTable fields={orderFields} values={order} />
467
467
  </SActionModal>
@@ -474,6 +474,10 @@ SModal.create({ component: OrderModal, componentProps: { orderId } })
474
474
  .onDismissed(() => {});
475
475
  ```
476
476
 
477
+ **하단 버튼을 본문(children)에 직접 두지 않는다.** 주 액션은 `button`, 보조 버튼은 `footerLeft` 로 넘긴다 — 푸터 배경·여백·양끝 분리가 컴포넌트 규칙대로 잡히는 자리다. `button` 은 클릭해도 **모달을 닫지 않으므로**(`onClick` 만 발화) 저장 API 응답을 보고 `modalRef.ok()` 로 닫으면 되고, 그 때문에 본문에 버튼을 따로 둘 이유가 없다. `footerLeft` 는 슬롯이라 `SButton` 을 직접 배치하며 `size="md"` 를 명시한다(§3-5-2).
478
+
479
+ **모달 안에서도 앱의 훅을 그냥 쓴다 — 단, 앱 루트에 `SModalOutlet` 이 있어야 한다 (§4-1).** outlet 이 있으면 명령형 모달이 앱 렌더 트리의 자식으로 그려지므로 `useQuery`·`useNavigate`·`useTheme` 같은 Context 기반 훅이 페이지에서와 똑같이 동작한다. **모달 컴포넌트를 Provider 로 다시 감싸지 않는다.** outlet 없이 띄우면 모달이 별도 React 루트로 떠서 Provider 가 하나도 닿지 않고, `No QueryClient set` 처럼 모달을 여는 순간에만 터진다.
480
+
477
481
  #### 3-3-5. 닫기 경로 — `persistent` 기본값은 컴포넌트마다 다르다
478
482
 
479
483
  **입력을 담는 모달은 백드롭 클릭·ESC 로 닫히지 않는 것이 기본**이다. 닫기 시도는 흔들림(shake)으로만 반응한다. 작성 중인 내용을 실수로 잃지 않게 하기 위한 것이다. 반면 `SConfirmModal` 은 잃을 입력이 없으므로 **백드롭·ESC 로 닫히는 것이 기본**이다.
@@ -724,6 +728,41 @@ const columns: STableColumn[] = [
724
728
  </div>
725
729
  ```
726
730
 
731
+ ### 3-6. 영역 나누기 — SDivider vs SSplitter
732
+
733
+ | 상황 | 사용 |
734
+ | --- | --- |
735
+ | 두 영역 사이에 **선만** 그을 때 | `SDivider` — 위치·두께가 고정된 구분선이다 |
736
+ | 사용자가 **경계를 끌어 넓이를 바꿀 수 있어야** 할 때 | `SSplitter` — 두 패널을 감싸고 경계 위치를 소유한다 |
737
+
738
+ `SSplitter` 의 구분선은 평소 자리만 잡고 칠해지지 않다가, 경계에 커서를 올리거나 포커스를 주면 그때 드러난다 — 조절 가능한 자리라는 신호다. **항상 보이는 선이 필요하면 `SDivider` 를 쓴다.** 선 색·두께·주변 여백은 토큰이 정하므로 직접 주지 않는다.
739
+
740
+ ```tsx
741
+ <SSplitter defaultValue={30} limits={[20, 60]}>
742
+ <SSplitter.Before>내비게이션</SSplitter.Before>
743
+ <SSplitter.After>본문</SSplitter.After>
744
+ </SSplitter>
745
+ ```
746
+
747
+ - 자식은 `SSplitter.Before` 와 `SSplitter.After` **둘뿐**이고, 루트의 직접 자식이어야 한다.
748
+ - 크기 단위는 `unit` 이 정한다. 기본 `'%'` 는 창이 바뀌어도 비율을 유지하고, `'px'` 는 폭을 유지한다. **사이드바처럼 폭이 고정돼야 하는 자리는 `'px'`**, 화면을 비율로 나누는 자리는 기본값 그대로 둔다.
749
+ - 본문이 읽을 수 없을 만큼 좁아지지 않도록 `limits={[최소, 최대]}` 를 준다. 생략하면 `'%'` 는 `[10, 90]`, `'px'` 는 `[50, Infinity]`.
750
+ - 각 패널은 넘치는 만큼 **스스로 스크롤한다.** 패널 안에 `SScrollArea` 를 겹쳐 넣지 않는다.
751
+ - 모델은 항상 **첫 패널**(`SSplitter.Before`) 크기다. 사이드가 기준인 화면이면 사이드를 `Before` 에 둔다.
752
+ - 앱 셸의 GNB 폭은 `SGnb` 가 소유한다. `SLayout`/`SGnb` 를 `SSplitter` 로 감싸지 않는다 — **GNB 폭을 끌 수 있게 하려면 `SGnb` 에 `resizable` 을 준다**(§4-1).
753
+
754
+ **Quasar `q-splitter` 에서 옮겨올 때** — `unit` · `limits` · `emitImmediately` 는 이름과 뜻이 같고, 나머지는 아래처럼 바뀐다. `emitImmediately` 를 주지 않으면 **드래그를 놓는 순간 한 번만** `onValueChange` 가 온다.
755
+
756
+ | q-splitter | SSplitter |
757
+ | --- | --- |
758
+ | `horizontal` (상/하 분할) | **`vertical`** — 이 저장소는 `STabs`·`SStepper` 와 같이 "세로 **배치**" 로 읽는다 |
759
+ | `v-model` | `value` + `onValueChange` (또는 `defaultValue`) |
760
+ | `disable` | `disabled` |
761
+ | `before` / `after` 슬롯 | `SSplitter.Before` / `SSplitter.After` |
762
+ | `before-class` / `after-class` | 각 슬롯에 `className` 을 직접 |
763
+ | `separator-class` / `separator-style` | `dividerClassName` / `dividerStyle` |
764
+ | `reverse` · `dark` | 없음 — 모델은 항상 첫 패널 크기다 |
765
+
727
766
  ---
728
767
 
729
768
  ## 4. 페이지 레시피 — 표준 골격
@@ -759,6 +798,30 @@ export default function AppShell({
759
798
  }
760
799
  ```
761
800
 
801
+ **GNB 폭을 사용자가 조절하게 하려면 `SGnb` 에 `resizable` 을 준다.** 메뉴 오른쪽 경계가 조절선이 되고, 레일 폭은 고정된 채 메뉴 컬럼만 늘고 준다. 범위는 컴포넌트가 정하므로 숫자를 직접 주지 않는다.
802
+
803
+ ```tsx
804
+ {/* 폭을 기억해야 하면 menuWidth 를 앱이 쥐고 onMenuWidthChange 로 되받아 저장한다.
805
+ 초기값만 정하면 되면 defaultMenuWidth 하나로 끝난다. */}
806
+ <SGnb items={MENU} value={current} onValueChange={navigate} resizable defaultMenuWidth={240} />
807
+ ```
808
+
809
+ `onMenuWidthChange` 는 **드래그를 놓는 순간**(또는 방향키 조작) 한 번만 온다 — 저장 로직을 그대로 붙여도 프레임마다 쓰이지 않는다. 접혀 있거나 레일 리프가 활성이라 깔 메뉴가 없으면 조절선은 나오지 않는다.
810
+
811
+ **앱 부트스트랩에는 `<SModalOutlet />` 을 한 번 렌더한다 — Provider 안쪽에 둔다.**
812
+
813
+ ```tsx
814
+ import { SModalOutlet } from 'sellmate-design-system-react';
815
+
816
+ // 앱 진입점 (main.tsx / App.tsx) — 앱 전체에 하나. 위치는 Provider 안쪽이기만 하면 된다
817
+ <QueryClientProvider client={queryClient}>
818
+ <RouterProvider router={router} />
819
+ <SModalOutlet />
820
+ </QueryClientProvider>;
821
+ ```
822
+
823
+ `SModal.confirm/loading/create` 로 띄운 모달이 그려지는 자리다. outlet 이 앱 트리 안에 있어야 모달이 앱의 Context(QueryClient·Router·Theme 등)를 상속한다 — outlet 이 없으면 모달은 뜨지만 별도 React 루트라 Provider 가 닿지 않는다(§3-3-4). 모달은 언제나 `body` 로 portal 되므로 outlet 을 어디에 두든 레이아웃에는 영향이 없고, **셸 안에 넣을 필요도 없다.** 토스트를 쓴다면 `SToastContainer` 도 같은 자리에 둔다.
824
+
762
825
  **최소 너비는 `SLayout` 이 보장한다 — 앱이 `min-w-*` 를 직접 주지 않는다.** 창이 최소 너비(`SLAYOUT_MIN_WIDTH`, GNB 포함한 전체 기준)보다 좁아지면 GNB·상단바는 제자리에 남고 **`SPage` 안에만 가로 스크롤이 생긴다.** 문서(브라우저 창)에는 가로 스크롤이 생기지 않는다. 그래서 셸을 감싸는 요소에 `min-width` 나 `overflow-x` 를 걸지 않는다 — 걸면 창 전체가 스크롤되어 GNB 가 화면 밖으로 밀린다.
763
826
 
764
827
  **셸의 `SPage` 는 모든 페이지가 공유하므로, 스크롤 끝 여백을 끄려면 페이지가 셸에 알려야 한다.** 위처럼 프레임 컴포넌트가 `scrollEndSpacing` 을 받아 그대로 넘기고, 페이지네이션이 있는 목록 페이지만 `false` 를 준다 (§4-2). 나머지 페이지는 넘기지 않으면 기본값(켬)이 적용된다.
@@ -1078,6 +1141,8 @@ export default function ProductDetailPage() {
1078
1141
  - [ ] `SGhostButton` 의 `intent` 가 조작 성격과 맞는가 (되돌릴 수 없는 삭제만 `danger`, 진입·추가는 `action`, 나머지는 `default`)
1079
1142
  - [ ] 창을 띄울 때 §3-3-1 판별 순서를 따랐는가 (그 자체가 화면 → `SPopup` / 실행 여부만 확정 → `SModal.confirm` / 모달 안에서 작성 → `SActionModal`)
1080
1143
  - [ ] 작업용 모달을 `SActionModal` + `SModal.create` 로 만들었는가 (직접 오버레이 ❌)
1144
+ - [ ] 모달·드로어의 하단 버튼을 본문이 아니라 `button` · `footerLeft` prop 으로 넘겼는가 (§3-3-4)
1145
+ - [ ] 앱 부트스트랩의 Provider 안쪽에 `<SModalOutlet />` 이 한 번 렌더되어 있는가 (§4-1 — 없으면 모달 안에서 앱 훅이 죽는다), 그 대신으로 모달 컴포넌트를 Provider 로 다시 감싸지 않았는가
1081
1146
  - [ ] 상태 표시·알림·확인 다이얼로그가 §3의 선택 규칙을 따르는가
1082
1147
 
1083
1148
  ---
@@ -1278,7 +1343,7 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
1278
1343
  --sys-color-link-accent
1279
1344
  --sys-color-navigation-gnb-bg-dark
1280
1345
 
1281
- ## 3. 컴포넌트 인덱스 (62종)
1346
+ ## 3. 컴포넌트 인덱스 (63종)
1282
1347
 
1283
1348
  **Props 는 이 문서에 없다.** 쓸 컴포넌트를 정했으면 그 컴포넌트의 README 를 읽는다.
1284
1349
 
@@ -1287,4 +1352,4 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
1287
1352
  예) SButton 을 쓴다면 `dist/components/SButton/README.md`.
1288
1353
  prop 을 추측해서 쓰지 말고 반드시 README 로 확인한다 (분류별 용도는 §1 의 "전체 컴포넌트 인덱스" 참조).
1289
1354
 
1290
- SActionModal · SBadge · SBarcodeInput · SButton · SCalendar · SCallout · SCard · SCheckbox · SChip · SChipInput · SCircleProgress · SConfirmModal · SDatePicker · SDateRangePicker · SDivider · SDraggableItem · SDraggableList · SDrawer · SDropdownButton · SExpansionItem · SExpansionList · SField · SFilePicker · SForm · SGhostButton · SGnb · SGuide · SIcon · SInput · SKeyValueTable · SLayout · SLinearProgress · SList · SListItem · SLoadingContainer · SLoadingModal · SModal · SNumberInput · SPage · SPagination · SPopover · SPopup · SPortal · SRadio · SRadioButton · SScrollArea · SSectionHeaderCard · SSelect · SStepper · SSwitch · STable · STableBar · STabs · STag · STextLink · STextarea · STimePicker · STimeRangePicker · SToast · SToggle · STooltip · STree
1355
+ SActionModal · SBadge · SBarcodeInput · SButton · SCalendar · SCallout · SCard · SCheckbox · SChip · SChipInput · SCircleProgress · SConfirmModal · SDatePicker · SDateRangePicker · SDivider · SDraggableItem · SDraggableList · SDrawer · SDropdownButton · SExpansionItem · SExpansionList · SField · SFilePicker · SForm · SGhostButton · SGnb · SGuide · SIcon · SInput · SKeyValueTable · SLayout · SLinearProgress · SList · SListItem · SLoadingContainer · SLoadingModal · SModal · SNumberInput · SPage · SPagination · SPopover · SPopup · SPortal · SRadio · SRadioButton · SScrollArea · SSectionHeaderCard · SSelect · SSplitter · SStepper · SSwitch · STable · STableBar · STabs · STag · STextLink · STextarea · STimePicker · STimeRangePicker · SToast · SToggle · STooltip · STree