sellmate-design-system-react 7.1.0 → 8.1.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.
Files changed (38) hide show
  1. package/AGENTS.md +412 -18
  2. package/README.md +16 -1
  3. package/dist/components/SDraggableItem/README.md +1 -0
  4. package/dist/components/SDraggableItem/SDraggableItem.d.ts +3 -0
  5. package/dist/components/SDrawer/README.md +5 -4
  6. package/dist/components/SDrawer/SDrawer.d.ts +16 -5
  7. package/dist/components/SExpansionItem/README.md +1 -0
  8. package/dist/components/SExpansionItem/SExpansionItem.d.ts +3 -0
  9. package/dist/components/SGnb/README.md +7 -0
  10. package/dist/components/SGnb/SGnb.d.ts +27 -0
  11. package/dist/components/SGnb/gnb.config.d.ts +13 -0
  12. package/dist/components/SIcon/SIcon.d.ts +1 -1
  13. package/dist/components/SIcon/icons.gen.d.ts +8 -3
  14. package/dist/components/SLayout/SLayout.d.ts +6 -0
  15. package/dist/components/SListItem/README.md +1 -0
  16. package/dist/components/SListItem/SListItem.d.ts +3 -0
  17. package/dist/components/SModal/README.md +41 -6
  18. package/dist/components/SModal/SModalOutlet.d.ts +18 -0
  19. package/dist/components/SModal/index.d.ts +1 -0
  20. package/dist/components/SSplitter/README.md +27 -0
  21. package/dist/components/SSplitter/SSplitter.d.ts +32 -0
  22. package/dist/components/SSplitter/index.d.ts +2 -0
  23. package/dist/components/SSplitter/splitter.config.d.ts +15 -0
  24. package/dist/components/STooltip/README.md +2 -0
  25. package/dist/index.cjs +1034 -221
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.js +1033 -223
  29. package/dist/index.js.map +1 -1
  30. package/dist/lib/autofill.d.ts +29 -0
  31. package/dist/lib/is-dev.d.ts +2 -0
  32. package/dist/lib/modal-outlet.d.ts +53 -0
  33. package/dist/llms-full.txt +502 -28
  34. package/dist/llms.txt +418 -20
  35. package/dist/styles.css +154 -17
  36. package/dist/theme.css +40 -10
  37. package/eslint/scale.gen.mjs +1 -1
  38. package/package.json +2 -1
@@ -16,14 +16,25 @@ export interface SDrawerProps {
16
16
  persistent?: boolean;
17
17
  /** 접근성 제목 및 헤더 제목 */
18
18
  title?: string;
19
- /** Drawer 너비. 기본값은 Figma drawer 기준 572px이다. */
19
+ /**
20
+ * Drawer 너비. 기본값은 Figma drawer 기준 572px이다.
21
+ * 창이 이보다 좁으면 창 폭까지만 넓어진다 — 드로어가 화면 밖으로 나가지 않는다.
22
+ * `resizable` 로 조절한 폭은 이 값이 바뀔 때 되돌아간다 — 앱이 폭을 저장해 두었다가
23
+ * 다시 열 때 넘겨주면 그 폭으로 열린다.
24
+ */
20
25
  width?: number | string;
21
- /** true면 왼쪽 테두리를 드래그해 너비를 조절할 수 있다. */
26
+ /**
27
+ * 왼쪽 테두리를 끌어 너비를 조절할 수 있게 한다. 조절선은 패널 왼쪽 경계 전체이며
28
+ * 평소엔 보이지 않다가 hover·포커스·조절 중에만 드러난다(SSplitter·SGnb 와 같은 선).
29
+ * 포커스를 받아 방향키(Shift 는 크게)·Home·End 로도 조절된다.
30
+ */
22
31
  resizable?: boolean;
23
- /** 리사이즈 가능한 최소 너비(px, %, vw, vh) */
32
+ /** 리사이즈 가능한 최소 너비(px, %, vw, vh). % 는 viewport 기준이며, 창 폭보다 클 수 없다 */
24
33
  minWidth?: number | string;
25
- /** 리사이즈 가능한 최대 너비(px, %, vw, vh) */
34
+ /** 리사이즈 가능한 최대 너비(px, %, vw, vh). % 는 viewport 기준이며, 주지 않으면 창 폭이 상한이다 */
26
35
  maxWidth?: number | string;
36
+ /** 너비가 확정될 때(드래그를 놓거나 방향키 조작). 드래그하는 동안에는 오지 않는다 */
37
+ onWidthChange?: (width: number) => void;
27
38
  /** footer 좌측 슬롯 */
28
39
  footerLeft?: ReactNode;
29
40
  /** 우측 기본 액션 버튼 */
@@ -39,4 +50,4 @@ export interface SDrawerProps {
39
50
  * 모달을 추가로 열면 새 모달이 Drawer 위에 쌓이고, Drawer 내부 floating은 전용 portal
40
51
  * container를 통해 Drawer의 backdrop 위에 표시된다.
41
52
  */
42
- export declare function SDrawer({ open, onOpenChange, onClose, persistent, title, width, resizable, minWidth, maxWidth, footerLeft, button, children, className, style, }: SDrawerProps): import("react").JSX.Element;
53
+ export declare function SDrawer({ open, onOpenChange, onClose, persistent, title, width, resizable, minWidth, maxWidth, onWidthChange, footerLeft, button, children, className, style, }: SDrawerProps): import("react").JSX.Element;
@@ -22,6 +22,7 @@
22
22
  | `interaction?` | `SExpansionItemInteraction` | — | hover/selected 상태에서 적용할 인터랙션 preset |
23
23
  | `accentStripe?` | `boolean` | `false` | 아이템 왼쪽 accent stripe 표시 여부 |
24
24
  | `dense?` | `boolean` | `false` | |
25
+ | `size?` | `SExpansionItemSize` | `'sm'` | 타이포그래피 크기 |
25
26
  | `disabled?` | `boolean` | `false` | |
26
27
 
27
28
  #### Events
@@ -7,6 +7,7 @@ export interface SExpansionItemRenderState {
7
7
  export type SExpansionItemRenderProp = (state: SExpansionItemRenderState) => ReactNode;
8
8
  export type SExpansionItemInteraction = 'chevron';
9
9
  export type SExpansionItemSupportingTextPosition = 'right' | 'bottom';
10
+ export type SExpansionItemSize = 'sm' | 'md';
10
11
  export interface SExpansionItemProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onToggle' | 'title'> {
11
12
  title: ReactNode;
12
13
  /** 제목을 보조하는 텍스트 */
@@ -31,6 +32,8 @@ export interface SExpansionItemProps extends Omit<HTMLAttributes<HTMLDivElement>
31
32
  /** 아이템 왼쪽 accent stripe 표시 여부 */
32
33
  accentStripe?: boolean;
33
34
  dense?: boolean;
35
+ /** 타이포그래피 크기 */
36
+ size?: SExpansionItemSize;
34
37
  disabled?: boolean;
35
38
  onToggle?: (expanded: boolean, event: MouseEvent<HTMLButtonElement>) => void;
36
39
  }
@@ -17,9 +17,15 @@
17
17
  | `folded?` | `boolean` | — | 접힘(레일) 상태. 미지정 시 SLayout 의 folded 를 따른다. |
18
18
  | `logo?` | `ReactNode` | — | 상단바 로고 영역 (slot). header="full" 이면 폭이 140px 로 고정된다. |
19
19
  | `topContent?` | `ReactNode` | — | 상단바 로고 오른쪽 슬롯 (검색·액션 등). 로고와 16px 띄고 남는 폭을 모두 차지하므로 안에서 자유롭게 정렬한다. 상단바가 전폭인 header="full" 에서만 렌더된다 (fix 는 상단바가 좁은 GNB 컬럼 안이라 놓을 자리가 없다). |
20
+ | `railTop?` | `ReactNode` | — | 레일 상단 고정 슬롯. 레일 아이템이 많아 넘치면 아이템 목록(ul)만 스크롤되고 이 슬롯은 레일 상단(상단바 바로 아래)에 붙어 고정된다. useRail 일 때만 렌더된다. |
20
21
  | `railFooter?` | `ReactNode` | — | 레일 하단 고정 슬롯. 레일 아이템이 많아 넘치면 아이템 목록(ul)만 스크롤되고 이 슬롯은 레일 하단에 붙어 고정된다. useRail 일 때만 렌더된다. |
22
+ | `menuTop?` | `ReactNode` | — | 메뉴 상단 고정 슬롯. 메뉴 아이템이 많아 넘치면 메뉴 목록(ul)만 스크롤되고 이 슬롯은 메뉴 상단에 붙어 고정된다. 메뉴가 렌더될 때만(showMenu) 나타난다. |
21
23
  | `menuFooter?` | `ReactNode` | — | 메뉴 하단 고정 슬롯. 메뉴 아이템이 많아 넘치면 메뉴 목록(ul)만 스크롤되고 이 슬롯은 메뉴 하단에 붙어 고정된다. 메뉴가 렌더될 때만(showMenu) 나타난다. |
24
+ | `foldedTop?` | `ReactNode` | — | 접힘(fix 레일) 상단 고정 슬롯. 접으면 본문이 빠져나가며 rail/menu top 도 사라지므로, 48px 폴드 레일 상단(상단바 바로 아래)에 붙는 별도 슬롯이다. header="fix" 로 접혔을 때만 나타난다. |
22
25
  | `foldedFooter?` | `ReactNode` | — | 접힘(fix 레일) 하단 고정 슬롯. 접으면 본문이 빠져나가며 rail/menu footer 도 사라지므로, 48px 폴드 레일 바닥에 붙는 별도 슬롯이다. header="fix" 로 접혔을 때만 나타난다. |
26
+ | `resizable?` | `boolean` | `false` | 메뉴 폭을 드래그로 조절할 수 있게 한다. 레일 폭은 고정이고 **메뉴 컬럼만** 늘고 준다. 조절선은 GNB 컬럼의 오른쪽 경계 전체다 — fix 는 상단바 높이까지, full 은 상단바가 전폭이라 본문 높이까지. 접혀 있거나 깔 메뉴가 없으면(레일 리프가 활성) 조절선이 나오지 않는다. |
27
+ | `menuWidth?` | `number` | — | 메뉴 폭(px). 주면 controlled — onMenuWidthChange 로 직접 갱신해야 움직인다 |
28
+ | `defaultMenuWidth?` | `number` | — | 메뉴 초기 폭(px). uncontrolled |
23
29
  | `ariaLabel?` | `string` | `'global navigation'` | 메뉴 landmark(nav) 의 접근성 레이블. 한 화면에 nav 가 여럿일 때 구분한다. |
24
30
 
25
31
  #### Events
@@ -30,6 +36,7 @@
30
36
  | `onRailChange` | `(value: string) => void` | 레일 선택 변경. 레일 아이템을 눌러 패널이 바뀔 때 알린다(선택 상태는 SGnb 가 자체 관리). |
31
37
  | `onFoldChange` | `(folded: boolean) => void` | 접힘 토글 (sdFoldChange) |
32
38
  | `onLauncherClick` | `() => void` | 앱런처(그리드) 버튼 클릭. 미지정 시 런처 버튼을 렌더하지 않는다. 접힘 레일(fix)에는 폴드 버튼만 남으므로 표시되지 않는다. |
39
+ | `onMenuWidthChange` | `(width: number) => void` | 메뉴 폭이 확정될 때(드래그를 놓거나 방향키 조작). 드래그하는 동안에는 오지 않는다 |
33
40
 
34
41
  ## Dependencies
35
42
 
@@ -38,21 +38,48 @@ export interface SGnbProps extends Omit<HTMLAttributes<HTMLDivElement>, 'color'>
38
38
  * (fix 는 상단바가 좁은 GNB 컬럼 안이라 놓을 자리가 없다).
39
39
  */
40
40
  topContent?: ReactNode;
41
+ /**
42
+ * 레일 상단 고정 슬롯. 레일 아이템이 많아 넘치면 아이템 목록(ul)만 스크롤되고
43
+ * 이 슬롯은 레일 상단(상단바 바로 아래)에 붙어 고정된다. useRail 일 때만 렌더된다.
44
+ */
45
+ railTop?: ReactNode;
41
46
  /**
42
47
  * 레일 하단 고정 슬롯. 레일 아이템이 많아 넘치면 아이템 목록(ul)만 스크롤되고
43
48
  * 이 슬롯은 레일 하단에 붙어 고정된다. useRail 일 때만 렌더된다.
44
49
  */
45
50
  railFooter?: ReactNode;
51
+ /**
52
+ * 메뉴 상단 고정 슬롯. 메뉴 아이템이 많아 넘치면 메뉴 목록(ul)만 스크롤되고
53
+ * 이 슬롯은 메뉴 상단에 붙어 고정된다. 메뉴가 렌더될 때만(showMenu) 나타난다.
54
+ */
55
+ menuTop?: ReactNode;
46
56
  /**
47
57
  * 메뉴 하단 고정 슬롯. 메뉴 아이템이 많아 넘치면 메뉴 목록(ul)만 스크롤되고
48
58
  * 이 슬롯은 메뉴 하단에 붙어 고정된다. 메뉴가 렌더될 때만(showMenu) 나타난다.
49
59
  */
50
60
  menuFooter?: ReactNode;
61
+ /**
62
+ * 접힘(fix 레일) 상단 고정 슬롯. 접으면 본문이 빠져나가며 rail/menu top 도 사라지므로,
63
+ * 48px 폴드 레일 상단(상단바 바로 아래)에 붙는 별도 슬롯이다. header="fix" 로 접혔을 때만 나타난다.
64
+ */
65
+ foldedTop?: ReactNode;
51
66
  /**
52
67
  * 접힘(fix 레일) 하단 고정 슬롯. 접으면 본문이 빠져나가며 rail/menu footer 도 사라지므로,
53
68
  * 48px 폴드 레일 바닥에 붙는 별도 슬롯이다. header="fix" 로 접혔을 때만 나타난다.
54
69
  */
55
70
  foldedFooter?: ReactNode;
71
+ /**
72
+ * 메뉴 폭을 드래그로 조절할 수 있게 한다. 레일 폭은 고정이고 **메뉴 컬럼만** 늘고 준다.
73
+ * 조절선은 GNB 컬럼의 오른쪽 경계 전체다 — fix 는 상단바 높이까지, full 은 상단바가 전폭이라 본문 높이까지.
74
+ * 접혀 있거나 깔 메뉴가 없으면(레일 리프가 활성) 조절선이 나오지 않는다.
75
+ */
76
+ resizable?: boolean;
77
+ /** 메뉴 폭(px). 주면 controlled — onMenuWidthChange 로 직접 갱신해야 움직인다 */
78
+ menuWidth?: number;
79
+ /** 메뉴 초기 폭(px). uncontrolled */
80
+ defaultMenuWidth?: number;
81
+ /** 메뉴 폭이 확정될 때(드래그를 놓거나 방향키 조작). 드래그하는 동안에는 오지 않는다 */
82
+ onMenuWidthChange?: (width: number) => void;
56
83
  /** 메뉴 landmark(nav) 의 접근성 레이블. 한 화면에 nav 가 여럿일 때 구분한다. */
57
84
  ariaLabel?: string;
58
85
  }
@@ -115,6 +115,19 @@ export declare const GNB_EXPAND_MS = 200;
115
115
  * header="full" 에서 SLayout 이 "메뉴 오른쪽 = 페이지" 영역을 잡을 때도 이 값을 쓴다.
116
116
  */
117
117
  export declare const GNB_MENU_WIDTH = 240;
118
+ /**
119
+ * resizable 일 때 메뉴 폭이 움직일 수 있는 범위(px). 레일 폭은 고정이고 메뉴 컬럼만 늘고 준다.
120
+ * 디자인이 아직 확정한 값이 아니라 임시다 — 확정되면 토큰으로 내려온다.
121
+ */
122
+ export declare const GNB_MENU_MIN_WIDTH = 200;
123
+ export declare const GNB_MENU_MAX_WIDTH = 300;
124
+ /** 메뉴 폭 조절에서 방향키 한 번에 움직이는 양(px). Shift 를 함께 누르면 large. */
125
+ export declare const GNB_MENU_RESIZE_KEY_STEP: {
126
+ normal: number;
127
+ large: number;
128
+ };
129
+ /** 메뉴 폭을 허용 범위 안으로 자른다. */
130
+ export declare const clampGnbMenuWidth: (width: number) => number;
118
131
  /**
119
132
  * header="full" 상단바의 로고 슬롯 고정 폭(토큰 없음 → 상수).
120
133
  * 전폭 상단바에서는 로고 오른쪽에 topContent 슬롯이 이어지므로, 로고 내용이 바뀌어도
@@ -17,6 +17,6 @@ export interface SIconProps {
17
17
  }
18
18
  /**
19
19
  * SIcon — sd-icon 포팅.
20
- * 89개 SVG(icons.gen.tsx)를 currentColor 기반으로 렌더한다.
20
+ * 94개 SVG(icons.gen.tsx)를 currentColor 기반으로 렌더한다.
21
21
  */
22
22
  export declare const SIcon: import("react").ForwardRefExoticComponent<SIconProps & import("react").RefAttributes<HTMLSpanElement>>;
@@ -11,6 +11,7 @@ export declare const ICONS: {
11
11
  readonly arrowLeft: (p: IconRenderProps) => import("react").JSX.Element;
12
12
  readonly arrowRight: (p: IconRenderProps) => import("react").JSX.Element;
13
13
  readonly arrowUp: (p: IconRenderProps) => import("react").JSX.Element;
14
+ readonly asterisk: (p: IconRenderProps) => import("react").JSX.Element;
14
15
  readonly attachFile: (p: IconRenderProps) => import("react").JSX.Element;
15
16
  readonly barcode: (p: IconRenderProps) => import("react").JSX.Element;
16
17
  readonly bell: (p: IconRenderProps) => import("react").JSX.Element;
@@ -53,10 +54,10 @@ export declare const ICONS: {
53
54
  readonly helpFill: (p: IconRenderProps) => import("react").JSX.Element;
54
55
  readonly helpOutline: (p: IconRenderProps) => import("react").JSX.Element;
55
56
  readonly history: (p: IconRenderProps) => import("react").JSX.Element;
56
- readonly imageFill: (p: IconRenderProps) => import("react").JSX.Element;
57
- readonly imageOutline: (p: IconRenderProps) => import("react").JSX.Element;
57
+ readonly image: (p: IconRenderProps) => import("react").JSX.Element;
58
58
  readonly inline: (p: IconRenderProps) => import("react").JSX.Element;
59
59
  readonly launcher: (p: IconRenderProps) => import("react").JSX.Element;
60
+ readonly leftRight: (p: IconRenderProps) => import("react").JSX.Element;
60
61
  readonly line: (p: IconRenderProps) => import("react").JSX.Element;
61
62
  readonly location: (p: IconRenderProps) => import("react").JSX.Element;
62
63
  readonly logout: (p: IconRenderProps) => import("react").JSX.Element;
@@ -77,14 +78,18 @@ export declare const ICONS: {
77
78
  readonly row: (p: IconRenderProps) => import("react").JSX.Element;
78
79
  readonly save: (p: IconRenderProps) => import("react").JSX.Element;
79
80
  readonly search: (p: IconRenderProps) => import("react").JSX.Element;
81
+ readonly send: (p: IconRenderProps) => import("react").JSX.Element;
80
82
  readonly setting: (p: IconRenderProps) => import("react").JSX.Element;
83
+ readonly settingOutline: (p: IconRenderProps) => import("react").JSX.Element;
81
84
  readonly shield: (p: IconRenderProps) => import("react").JSX.Element;
82
85
  readonly sidebar: (p: IconRenderProps) => import("react").JSX.Element;
83
- readonly star: (p: IconRenderProps) => import("react").JSX.Element;
84
86
  readonly store: (p: IconRenderProps) => import("react").JSX.Element;
85
87
  readonly synchronize: (p: IconRenderProps) => import("react").JSX.Element;
88
+ readonly thumbsDown: (p: IconRenderProps) => import("react").JSX.Element;
89
+ readonly thumbsUp: (p: IconRenderProps) => import("react").JSX.Element;
86
90
  readonly updown: (p: IconRenderProps) => import("react").JSX.Element;
87
91
  readonly user: (p: IconRenderProps) => import("react").JSX.Element;
92
+ readonly users: (p: IconRenderProps) => import("react").JSX.Element;
88
93
  readonly visibilityOff: (p: IconRenderProps) => import("react").JSX.Element;
89
94
  readonly visibilityOn: (p: IconRenderProps) => import("react").JSX.Element;
90
95
  readonly warehouseFill: (p: IconRenderProps) => import("react").JSX.Element;
@@ -26,6 +26,12 @@ export interface SLayoutNavState {
26
26
  * true 면 header prop 과 무관하게 full 로 선다.
27
27
  */
28
28
  requireFullHeader: boolean;
29
+ /**
30
+ * GNB 폭을 지금 드래그로 조절하는 중인지.
31
+ * 참이면 메뉴 열의 폭 전환을 끈다 — 접힘용 전환(foldMs)이 그대로 걸려 있으면
32
+ * 열이 손보다 그 길이만큼 늦게 따라와 끌리는 느낌이 사라진다.
33
+ */
34
+ resizing?: boolean;
29
35
  }
30
36
  export interface SLayoutContextValue {
31
37
  /** 메뉴 스타일 — 자식 SGnb 가 스타일을 맞추는 기준 */
@@ -20,6 +20,7 @@
20
20
  | `interaction?` | `SListItemInteraction` | — | hover/selected 상태에서 적용할 인터랙션 preset |
21
21
  | `accentStripe?` | `boolean` | `false` | 아이템 왼쪽 accent stripe 표시 여부 |
22
22
  | `dense?` | `boolean` | `false` | 조밀한 높이 사용 여부 |
23
+ | `size?` | `SListItemSize` | `'sm'` | 타이포그래피 크기 |
23
24
  | `disabled?` | `boolean` | `false` | 비활성 상태 여부 |
24
25
 
25
26
  ## Dependencies
@@ -7,6 +7,7 @@ export type SListItemRenderProp = (state: SListItemRenderState) => ReactNode;
7
7
  export type SListItemSlot = ReactNode | SListItemRenderProp;
8
8
  export type SListItemInteraction = 'chevron';
9
9
  export type SListItemSupportingTextPosition = 'right' | 'bottom';
10
+ export type SListItemSize = 'sm' | 'md';
10
11
  export interface SListItemProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'title'> {
11
12
  /** 리스트 아이템 제목 */
12
13
  title: SListItemSlot;
@@ -32,6 +33,8 @@ export interface SListItemProps extends Omit<HTMLAttributes<HTMLDivElement>, 'ch
32
33
  accentStripe?: boolean;
33
34
  /** 조밀한 높이 사용 여부 */
34
35
  dense?: boolean;
36
+ /** 타이포그래피 크기 */
37
+ size?: SListItemSize;
35
38
  /** 비활성 상태 여부 */
36
39
  disabled?: boolean;
37
40
  }
@@ -6,6 +6,8 @@
6
6
 
7
7
  호출 시마다 `document.body` 에 컨테이너를 만들어 모달을 렌더하고, 닫힘 애니메이션이 끝나면 자동으로 언마운트한다. 모든 메서드는 체이닝 핸들 [`SModalRef`](#smodalref) 를 반환한다.
8
8
 
9
+ > **앱 부트스트랩에 [`<SModalOutlet />`](#smodaloutlet) 을 한 번 렌더한다.** 그래야 명령형 모달이 앱 렌더 트리의 자식으로 그려져 QueryClient·Router·Theme 등 Context 를 상속한다. 없으면 예전처럼 별도 React 루트로 떠서 **앱의 Provider 가 하나도 닿지 않는다.**
10
+
9
11
  | 메서드 | 띄우는 모달 | 용도 | 주요 콜백/제어 |
10
12
  |---|---|---|---|
11
13
  | [`SModal.confirm(options)`](#smodalconfirm) | `SConfirmModal` | 확인/취소 | `onOk` / `onCancel` / `onClose` |
@@ -20,6 +22,29 @@ import { SModal } from 'sellmate-design-system-react';
20
22
 
21
23
  ---
22
24
 
25
+ ## SModalOutlet
26
+
27
+ 명령형 모달이 **그려지는 자리**. 앱 부트스트랩에서 Provider 안쪽에 **한 번만** 렌더한다. props 는 없다.
28
+
29
+ ```tsx
30
+ import { SModalOutlet } from 'sellmate-design-system-react';
31
+
32
+ <QueryClientProvider client={queryClient}>
33
+ <RouterProvider router={router} />
34
+ <SModalOutlet /> {/* 앱 전체에 하나 */}
35
+ </QueryClientProvider>;
36
+ ```
37
+
38
+ `SModal.confirm/loading/create` 는 전역 스토어에 모달을 넣기만 하고, outlet 이 그것을 `createPortal` 로 `body` 에 그린다. **DOM 위치·쌓임 순서는 outlet 유무와 무관하게 같고**, 달라지는 것은 렌더 트리다 — outlet 이 있으면 모달이 앱 트리의 자식이 되어 Context 를 상속한다.
39
+
40
+ - outlet 을 어디에 두든(Provider 안쪽이기만 하면) 모달은 `body` 로 portal 되므로 레이아웃·`overflow`·`transform` 의 영향을 받지 않는다.
41
+ - **호출부 API 는 그대로다.** outlet 도입 전 코드를 고칠 필요가 없다.
42
+ - outlet 이 없으면 예전처럼 별도 React 루트(`createRoot`)로 마운트되어 모달은 뜨지만 앱의 Provider 가 닿지 않는다 (개발 모드에서 1회 `console.warn`).
43
+ - 모달 본문이 렌더 중 예외를 던지면 모달만 닫히고 앱 트리는 유지된다 — 원인을 지목하는 `console.error` 가 함께 찍힌다.
44
+ - 모달이 떠 있는 채로 outlet 이 언마운트되면(앱 언마운트·Provider 교체) 그 모달은 정리되고 `onDismissed` 가 발화한다 — 남아서 되살아나지 않는다.
45
+
46
+ ---
47
+
23
48
  ## SModal.confirm
24
49
 
25
50
  아이콘 + 제목 + 메시지 + 확인/취소 버튼. `type` 에 따라 아이콘·메인 버튼 색이 결정된다.
@@ -121,30 +146,40 @@ SModal.create({ component: OrderModal, componentProps: { orderId: 'ORD-001' } })
121
146
 
122
147
  ### 비동기 제출 — 응답 보고 닫기
123
148
 
124
- SActionModal `button` 푸터 버튼은 클릭 **즉시 닫힌다**. 저장 API 응답에 따라 닫힘 여부를 정해야 하면 푸터 버튼 대신 **본문에 버튼을 두고** `modalRef` 로 닫힘 시점을 직접 제어한다.
149
+ **하단 버튼은 본문에 직접 두지 않는다.** 액션은 `button`(의도적으로 단수), 보조 버튼은 `footerLeft` 슬롯에 넣는다 그래야 푸터 배경·여백·양끝 분리가 컴포넌트 규칙대로 잡힌다.
150
+
151
+ `button` 은 클릭해도 **모달을 닫지 않는다.** `onClick` 만 발화하므로 저장 API 응답을 보고 `modalRef.ok()` 로 닫으면 된다.
125
152
 
126
153
  ```tsx
127
154
  function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderModalProps) {
128
155
  const [error, setError] = useState('');
156
+ const [saving, setSaving] = useState(false);
129
157
  const handleSubmit = async () => {
158
+ setSaving(true);
130
159
  try {
131
160
  await save(orderId);
132
161
  modalRef.ok(); // 성공 → onOk + 닫기
133
162
  } catch {
134
163
  setError('저장 실패'); // 실패 → 모달 유지
164
+ } finally {
165
+ setSaving(false);
135
166
  }
136
167
  };
137
168
  return (
138
- // button 주지 않으면 푸터가 렌더되지 않는다
139
- <SActionModal open={open} onOpenChange={onOpenChange} onClose={onClose} persistent modalTitle="주문 처리">
169
+ // button footerLeft 도 주지 않으면 푸터가 렌더되지 않는다
170
+ <SActionModal
171
+ open={open} onOpenChange={onOpenChange} onClose={onClose} persistent modalTitle="주문 처리"
172
+ button={{ label: saving ? '저장 중...' : '저장', disabled: saving, onClick: handleSubmit }}
173
+ footerLeft={<SButton color="neutral" outline size="md" label="취소" disabled={saving} onClick={() => modalRef.cancel()} />}
174
+ >
140
175
  {error && <p>{error}</p>}
141
- <SButton label="저장" onClick={handleSubmit} />
142
- <SButton label="취소" onClick={() => modalRef.cancel()} />
143
176
  </SActionModal>
144
177
  );
145
178
  }
146
179
  ```
147
180
 
181
+ `button` 은 `label` · `color`(기본 `primary`) · `outline` · `size`(기본 `md`) · `disabled` · `onClick` 을 받는다. `footerLeft` 는 슬롯이라 `SButton` 을 직접 배치하며, 푸터 규칙상 `size="md"` 를 명시한다.
182
+
148
183
  ---
149
184
 
150
185
  ## SModalRef
@@ -180,7 +215,7 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
180
215
 
181
216
  - **선언형과 공존**: 서비스는 추가 API다. open 상태가 앱 상태/라우트에 묶인 경우엔 선언형 `<SConfirmModal open>` / `<SLoadingModal open>` 이 더 적합하다.
182
217
  - **백드롭·ESC = 중립적 닫힘**: 특정 콜백(onClose 등) 없이 `onDismissed` 만 발화한다. 명시적 버튼·메서드만 onOk/onCancel/onClose 를 발화한다.
183
- - **Context 미상속**: `create` 의 커스텀 컴포넌트는 React 트리(createRoot)에서 렌더되어 부모의 Context Provider(Theme·Store 등)를 상속하지 않는다. 필요하면 컴포넌트 내부에서 직접 Provider 로 감싸라. (confirm/loading 은 토큰이 `:root` CSS 변수라 무관)
218
+ - **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 변수라 어느 쪽이든 무관)
184
219
  - **`create` 의 `component` 는 SActionModal 을 루트로**: `create` 는 컨테이너를 덧씌우지 않으므로, 본문만 렌더하는 컴포넌트를 넘기면 딤·카드 없이 콘텐츠가 그대로 화면에 붙는다. TypeScript 는 이를 막지 못한다(`component` 타입이 아무 컴포넌트나 허용). 개발 모드에서는 마운트 직후 렌더 결과로 이를 감지해 `console.warn` 으로 경고한다 — 세 모달은 모두 Portal 로 `body` 에 렌더되므로 `create` 가 만든 host 는 비어 있어야 하는데, host 에 엘리먼트가 남아 있으면 모달이 아닌 것으로 판정한다.
185
220
 
186
221
  ## Dependencies
@@ -0,0 +1,18 @@
1
+ /**
2
+ * SModalOutlet — 명령형 모달(`SModal.confirm` / `loading` / `create`)이 그려지는 자리.
3
+ *
4
+ * **앱 부트스트랩에서 Provider 안쪽에 한 번만 렌더한다.** 이 outlet 이 있으면 명령형 모달이
5
+ * 앱 렌더 트리의 자식으로 그려져 QueryClient·Router·Theme 등 앱의 Context 를 그대로 상속한다.
6
+ * DOM 상으로는 예전과 같이 `body` 직속으로 portal 되므로 위치·쌓임 순서는 달라지지 않는다.
7
+ *
8
+ * outlet 을 렌더하지 않아도 모달은 뜨지만, 예전처럼 별도 React 루트로 마운트되어 앱의
9
+ * Context 가 닿지 않는다(개발 모드에서 1회 경고).
10
+ *
11
+ * @example
12
+ * // 앱 부트스트랩 — 한 번만. Provider 안쪽이어야 모달이 상속받는다.
13
+ * <QueryClientProvider client={queryClient}>
14
+ * <RouterProvider router={router} />
15
+ * <SModalOutlet />
16
+ * </QueryClientProvider>
17
+ */
18
+ export declare function SModalOutlet(): import("react").JSX.Element;
@@ -1 +1,2 @@
1
1
  export { SModal, type SModalRef, type SConfirmOptions, type SLoadingOptions, type SCreateOptions, type SModalCreateComponentProps, } from './SModal';
2
+ export { SModalOutlet } from './SModalOutlet';
@@ -0,0 +1,27 @@
1
+ # SSplitter
2
+
3
+ > 자동 생성 문서 — `npm run docs:gen`. 소스: 각 컴포넌트의 Props/Handle 인터페이스 + import 의존성.
4
+
5
+ ### SSplitter
6
+
7
+ #### Props
8
+
9
+ | Prop | Type | Default | Description |
10
+ |------|------|---------|-------------|
11
+ | `value?` | `number` | — | 첫 패널 크기. unit 단위. 주면 controlled |
12
+ | `defaultValue?` | `number` | — | 첫 패널 초기 크기. uncontrolled. 기본은 '%' 면 50, 'px' 면 240 |
13
+ | `unit?` | `SSplitterUnit` | `'%'` | 모델·limits 를 읽는 단위 |
14
+ | `limits?` | `readonly [number, number]` | — | [최소, 최대]. 생략하면 '%' 는 [10, 90], 'px' 는 [50, Infinity] |
15
+ | `emitImmediately?` | `boolean` | `false` | 드래그하는 동안에도 onValueChange 를 계속 보낸다 |
16
+ | `vertical?` | `boolean` | `false` | true면 패널을 위아래로 쌓는다 (Quasar q-splitter 의 horizontal 에 해당) |
17
+ | `disabled?` | `boolean` | `false` | true면 크기를 바꿀 수 없다. 커서도 구분선도 나오지 않는다 |
18
+ | `dividerClassName?` | `string` | — | 구분선에 얹을 클래스 |
19
+ | `dividerStyle?` | `CSSProperties` | — | 구분선에 얹을 인라인 스타일 |
20
+ | `children?` | `ReactNode` | — | SSplitter.Before 와 SSplitter.After 둘 |
21
+
22
+ #### Events
23
+
24
+ | Event | Type | Description |
25
+ |-------|------|-------------|
26
+ | `onValueChange` | `(value: number) => void` | 크기가 확정될 때. emitImmediately 가 아니면 드래그를 놓는 순간 한 번만 온다 |
27
+
@@ -0,0 +1,32 @@
1
+ import { type CSSProperties, type HTMLAttributes, type ReactNode } from 'react';
2
+ import { type SSplitterUnit } from './splitter.config';
3
+ /** 패널은 순수 슬롯이다 — 크기·제약은 전부 루트가 소유한다. */
4
+ export type SSplitterPaneProps = HTMLAttributes<HTMLDivElement>;
5
+ export interface SSplitterProps extends Omit<HTMLAttributes<HTMLDivElement>, 'defaultValue'> {
6
+ /** 첫 패널 크기. unit 단위. 주면 controlled */
7
+ value?: number;
8
+ /** 첫 패널 초기 크기. uncontrolled. 기본은 '%' 면 50, 'px' 면 240 */
9
+ defaultValue?: number;
10
+ /** 크기가 확정될 때. emitImmediately 가 아니면 드래그를 놓는 순간 한 번만 온다 */
11
+ onValueChange?: (value: number) => void;
12
+ /** 모델·limits 를 읽는 단위 */
13
+ unit?: SSplitterUnit;
14
+ /** [최소, 최대]. 생략하면 '%' 는 [10, 90], 'px' 는 [50, Infinity] */
15
+ limits?: readonly [number, number];
16
+ /** 드래그하는 동안에도 onValueChange 를 계속 보낸다 */
17
+ emitImmediately?: boolean;
18
+ /** true면 패널을 위아래로 쌓는다 (Quasar q-splitter 의 horizontal 에 해당) */
19
+ vertical?: boolean;
20
+ /** true면 크기를 바꿀 수 없다. 커서도 구분선도 나오지 않는다 */
21
+ disabled?: boolean;
22
+ /** 구분선에 얹을 클래스 */
23
+ dividerClassName?: string;
24
+ /** 구분선에 얹을 인라인 스타일 */
25
+ dividerStyle?: CSSProperties;
26
+ /** SSplitter.Before 와 SSplitter.After 둘 */
27
+ children?: ReactNode;
28
+ }
29
+ export declare const SSplitter: import("react").ForwardRefExoticComponent<SSplitterProps & import("react").RefAttributes<HTMLDivElement>> & {
30
+ Before: import("react").ForwardRefExoticComponent<SSplitterPaneProps & import("react").RefAttributes<HTMLDivElement>>;
31
+ After: import("react").ForwardRefExoticComponent<SSplitterPaneProps & import("react").RefAttributes<HTMLDivElement>>;
32
+ };
@@ -0,0 +1,2 @@
1
+ export { SSplitter, type SSplitterPaneProps, type SSplitterProps } from './SSplitter';
2
+ export { SSPLITTER_UNITS, type SSplitterUnit } from './splitter.config';
@@ -0,0 +1,15 @@
1
+ export declare const SSPLITTER_UNITS: readonly ["%", "px"];
2
+ /** 모델·limits 를 읽는 단위. Quasar QSplitter 의 `unit` 과 같다. */
3
+ export type SSplitterUnit = (typeof SSPLITTER_UNITS)[number];
4
+ /** limits 를 주지 않았을 때의 기본 범위. Quasar 와 같은 값이다. */
5
+ export declare const SSPLITTER_DEFAULT_LIMITS: Record<SSplitterUnit, readonly [number, number]>;
6
+ /** value·defaultValue 를 주지 않았을 때의 시작값. */
7
+ export declare const SSPLITTER_DEFAULT_VALUE: Record<SSplitterUnit, number>;
8
+ /**
9
+ * 방향키 한 번에 움직이는 양. 단위가 다르면 체감도 달라야 하므로 단위별로 둔다.
10
+ * 두께·색과 달리 대응 토큰이 없어 상수다(SLAYOUT_MIN_WIDTH 와 같은 이유).
11
+ */
12
+ export declare const SSPLITTER_KEY_STEP: Record<SSplitterUnit, {
13
+ normal: number;
14
+ large: number;
15
+ }>;
@@ -47,6 +47,7 @@
47
47
  - [SField](../SField)
48
48
  - [SKeyValueTable](../SKeyValueTable)
49
49
  - [SSectionHeaderCard](../SSectionHeaderCard)
50
+ - [SStepper](../SStepper)
50
51
 
51
52
  ### Depends on
52
53
 
@@ -62,5 +63,6 @@ graph TD;
62
63
  SField --> STooltip
63
64
  SKeyValueTable --> STooltip
64
65
  SSectionHeaderCard --> STooltip
66
+ SStepper --> STooltip
65
67
  style STooltip fill:#f9f,stroke:#333,stroke-width:4px
66
68
  ```