sellmate-design-system-react 9.0.0-beta.105 → 9.0.0-beta.107

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
@@ -2555,6 +2555,13 @@ import { SLocaleProvider, SModalOutlet } from 'sellmate-design-system-react';
2555
2555
 
2556
2556
  **접으면 레일·메뉴가 통째로 빠져나가면서 그 슬롯들도 함께 사라진다.** 접힌 상태에서도 남겨야 할 것은 `foldedTop`·`foldedFooter` 로 따로 준다 — `header="fix"` 로 접혔을 때만 나타나며, 폭이 좁은 폴드 레일이므로 아이콘 버튼 하나 정도로 줄인다.
2557
2557
 
2558
+ **접힌 GNB 를 호버로 잠깐 펼치는 동작은 컴포넌트가 한다 — 앱이 따로 만들지 않는다.** 접힌 채 화면 왼쪽 가장자리(`header` 와 무관하게 `fix`·`full` 모두)에 마우스를 가져가면 페이지를 밀지 않고 그 위에 겹쳐 펼쳐지고, GNB 를 벗어나면 다시 접힌다. 키보드 포커스가 펼쳐진 GNB 안에 있으면 접히지 않는다. 이 동작을 켜고 끄는 prop 은 없다.
2559
+
2560
+ - **`folded` 는 그대로이고 `onFoldedChange` 도 오지 않는다.** 호버로 펼친 것은 보이는 모양뿐이라 앱의 접힘 상태·페이지 폭에 영향이 없다. 펼친 채로 두려면 폴드 버튼으로 편다 — 그때 `folded` 가 바뀐다.
2561
+ - **접힌 화면의 왼쪽 가장자리에는 호버 영역(투명 띠)이 깔린다.** 그 자리에 클릭해야 하는 것(페이지 왼쪽 끝에 붙인 버튼 등)을 두지 않는다. 띠의 폭은 컴포넌트가 정하므로 숫자를 직접 주지 않는다.
2562
+ - **펼쳐지는 동안 `foldedTop`·`foldedFooter` 와 접힌 레일의 `system` 은 펼친 모양(`menuTop`·`menuFooter`·메뉴 컬럼 바닥)으로 바뀐다.** 접힌 레일에서 열어 둔 계정·알림 패널은 그때 닫힌다(§4-1 `system`).
2563
+ - **터치에는 hover 가 없어 동작하지 않는다.** 터치 화면에서도 접힌 GNB 를 펴야 하면 폴드 버튼으로 연다.
2564
+
2558
2565
  ```tsx
2559
2566
  {/* 접히면 menuTop·menuFooter 가 함께 빠지므로, 폴드 레일에 남길 것만 foldedTop 으로 따로 준다 */}
2560
2567
  <SGnb
@@ -201,6 +201,9 @@ export interface SGnbProps extends Omit<HTMLAttributes<HTMLDivElement>, 'color'>
201
201
  * 접힘/펼침은 GNB_FOLD_MS 동안의 슬라이드다 — 레일·메뉴가 함께 왼쪽으로 빠져나가고 펼치면 오른쪽에서 들어온다.
202
202
  * 페이지 쪽 폭은 fix 면 이 컴포넌트의 폭 전환이, full 이면 SLayout 의 메뉴 열 전환이 같은 길이로 맞춘다.
203
203
  *
204
+ * 접힌 채 화면 왼쪽 가장자리(투명 띠)에 마우스를 가져가면 페이지를 밀지 않고 위에 겹쳐 펼쳐지고,
205
+ * GNB 를 벗어나면(키보드 포커스가 안에 있으면 유지) 다시 접힌다. `folded` 는 바뀌지 않는다.
206
+ *
204
207
  * SLayout 안에서 쓰면 type·header·folded 를 context 에서 읽어 앱이 중복으로 넘길 필요가 없고,
205
208
  * 자기 치수(컬럼 폭·상단바 높이·전환 길이)를 registerNav 로 되올려 프레임이 그 값으로 그리드를 깐다.
206
209
  *
@@ -118,6 +118,30 @@ export declare const GNB_FOLD_MS = 200;
118
118
  * 폴드와 같은 값을 써서 두 모션이 한 언어로 읽히게 한다.
119
119
  */
120
120
  export declare const GNB_EXPAND_MS = 200;
121
+ /**
122
+ * 접힌 GNB 의 호버 오버레이(peek) — 접힌 채 화면 왼쪽 가장자리에 마우스를 가져가면 GNB 가
123
+ * 페이지를 밀지 않고 위에 겹쳐 펼쳐진다. 전용 토큰이 없어 상수로 둔다.
124
+ * - 진입 지연: 가장자리를 스치기만 한 경우에 펼쳐지지 않게 한다.
125
+ * - 이탈 지연: 경계를 살짝 벗어났다 돌아오는 동안 깜빡이며 접히지 않게 한다.
126
+ * - 가장자리 폭: 여는 호버 영역인 화면 왼쪽 투명 띠의 폭. header="full" 은 접으면 메뉴가 화면 밖으로 나가
127
+ * 잡을 자리가 없고, header="fix" 는 접힌 레일 전체를 영역으로 삼으면 레일의 버튼으로 가는 길에 스치기만 해도
128
+ * 펼쳐져 누르려던 버튼이 바뀌므로 둘 다 띠로 연다.
129
+ */
130
+ export declare const GNB_PEEK_ENTER_DELAY_MS = 120;
131
+ export declare const GNB_PEEK_LEAVE_DELAY_MS = 250;
132
+ export declare const GNB_PEEK_EDGE_WIDTH = 12;
133
+ /**
134
+ * 호버 오버레이가 뜨는 z-index. 두 쪽 사이에 끼어야 한다.
135
+ * - 위로 못 넘는 것: 페이지 안의 로컬 값(SPageHeader·sticky 헤더 등 한 자리~30 대) — 오버레이가 그 아래로 가라앉으면 안 된다.
136
+ * - 아래로 못 내려가는 것: 전역 포털 레이어(`Z_INDEX.baseFloating`=50) — GNB 안에서 뜬 계정 목록·런처·라벨 칩이
137
+ * 오버레이에 가려지면 안 된다.
138
+ */
139
+ export declare const GNB_PEEK_Z_INDEX = 40;
140
+ /**
141
+ * 호버 오버레이의 그림자. 시안 값이 공용 그림자 토큰(--shadow-*)에 없어 상수로 둔다.
142
+ * 오버레이는 왼쪽 가장자리에 붙어 사방으로 고르게 퍼지는 그림자를 쓴다(오프셋 없음).
143
+ */
144
+ export declare const GNB_PEEK_SHADOW = "0px 0px 64px 0px rgba(34,34,34,0.12), 0px 0px 24px 0px rgba(34,34,34,0.08)";
121
145
  /**
122
146
  * 메뉴 고정 폭. 두 header 모두 메뉴는 이 폭으로 좌측에 붙는다(토큰 없음 → 상수).
123
147
  * header="full" 에서 SLayout 이 "메뉴 오른쪽 = 페이지" 영역을 잡을 때도 이 값을 쓴다.
@@ -87,6 +87,11 @@ export type SSelectChangeValue = any;
87
87
  export interface SSelectOption {
88
88
  value: string | number;
89
89
  label: string;
90
+ /**
91
+ * 주면 `label` 을 STag 로 그린다 — 목록 행과 단일 선택 트리거에 적용. 칩·multi 의 쉼표 트리거는 글자로 둔다.
92
+ * 태그는 글자만 받으므로 HTML 라벨과 함께 주면 태그를 걷어낸 글자로 그려진다.
93
+ */
94
+ tag?: SSelectOptionTag;
90
95
  disabled?: boolean;
91
96
  children?: SSelectOption[];
92
97
  }
@@ -106,6 +111,16 @@ export type SSelectType =
106
111
  export type SSelectValueDisplay = 'text' | 'chip';
107
112
  ```
108
113
 
114
+ ### SSelectOptionTag
115
+
116
+ ```ts
117
+ /**
118
+ * 옵션 라벨을 STag 로 그릴 때 넘기는 값. 글자는 옵션의 `label` 이고, 크기는 SSelect 의 `size` 가
119
+ * 정한다(`md` 면 `sm`, 그 외 `xs`) — 그래서 `size`·`label` 은 받지 않는다. 나머지(`color`·`deep`·`shape`·`icon`…)는 STag 와 같다.
120
+ */
121
+ export type SSelectOptionTag = Omit<STagProps, 'size' | 'label'>;
122
+ ```
123
+
109
124
  ## Dependencies
110
125
 
111
126
  ### Used by
@@ -122,6 +137,7 @@ export type SSelectValueDisplay = 'text' | 'chip';
122
137
  - [SIcon](../SIcon)
123
138
  - [SPortal](../SPortal)
124
139
  - [SSearchInput](../SSearchInput)
140
+ - [STag](../STag)
125
141
 
126
142
  ### Graph
127
143
 
@@ -134,6 +150,7 @@ graph TD;
134
150
  SSelect --> SIcon
135
151
  SSelect --> SPortal
136
152
  SSelect --> SSearchInput
153
+ SSelect --> STag
137
154
  SKeyValueTable --> SSelect
138
155
  STable --> SSelect
139
156
  style SSelect fill:#f9f,stroke:#333,stroke-width:4px
@@ -6,7 +6,7 @@ import { type SColor } from '../../lib/color';
6
6
  import { type STooltipProps } from '../STooltip';
7
7
  import { type SSelectOption, type SSelectType, type SSelectValueDisplay } from './select.config';
8
8
  import { type SFieldWidth } from '../../lib/field-width';
9
- export type { SSelectOption, SSelectType, SSelectValueDisplay } from './select.config';
9
+ export type { SSelectOption, SSelectOptionTag, SSelectType, SSelectValueDisplay, } from './select.config';
10
10
  export interface SSelectHandle {
11
11
  /** 트리거(combobox)에 포커스 (sdFocus) */
12
12
  focus: () => void;
@@ -1,6 +1,17 @@
1
+ import { type STagProps } from '../STag';
2
+ /**
3
+ * 옵션 라벨을 STag 로 그릴 때 넘기는 값. 글자는 옵션의 `label` 이고, 크기는 SSelect 의 `size` 가
4
+ * 정한다(`md` 면 `sm`, 그 외 `xs`) — 그래서 `size`·`label` 은 받지 않는다. 나머지(`color`·`deep`·`shape`·`icon`…)는 STag 와 같다.
5
+ */
6
+ export type SSelectOptionTag = Omit<STagProps, 'size' | 'label'>;
1
7
  export interface SSelectOption {
2
8
  value: string | number;
3
9
  label: string;
10
+ /**
11
+ * 주면 `label` 을 STag 로 그린다 — 목록 행과 단일 선택 트리거에 적용. 칩·multi 의 쉼표 트리거는 글자로 둔다.
12
+ * 태그는 글자만 받으므로 HTML 라벨과 함께 주면 태그를 걷어낸 글자로 그려진다.
13
+ */
14
+ tag?: SSelectOptionTag;
4
15
  disabled?: boolean;
5
16
  children?: SSelectOption[];
6
17
  }
@@ -10,6 +21,7 @@ export type SSelectValueDisplay = 'text' | 'chip';
10
21
  export interface FlatOption {
11
22
  value: string;
12
23
  label: string;
24
+ tag?: SSelectOptionTag;
13
25
  disabled: boolean;
14
26
  depth: number;
15
27
  /** children 을 가진 그룹 노드 — 그 자체가 값이 되는 것은 `tree` 타입뿐이다 */
@@ -79,6 +79,7 @@ export const TAG_COLORS = [
79
79
  - [SLauncherListBox](../SLauncherListBox)
80
80
  - [SNumberInput](../SNumberInput)
81
81
  - [SRadioCard](../SRadioCard)
82
+ - [SSelect](../SSelect)
82
83
  - [SSystemActionButton](../SSystemActionButton)
83
84
  - [STabs](../STabs)
84
85
  - [STextLink](../STextLink)
@@ -102,6 +103,7 @@ graph TD;
102
103
  SLauncherListBox --> STag
103
104
  SNumberInput --> STag
104
105
  SRadioCard --> STag
106
+ SSelect --> STag
105
107
  SSystemActionButton --> STag
106
108
  STabs --> STag
107
109
  STextLink --> STag
package/dist/index.cjs CHANGED
@@ -3107,43 +3107,52 @@ var Icon_helpFill = (p) => {
3107
3107
  }
3108
3108
  );
3109
3109
  };
3110
+ var HELP_OUTLINE_VARIANTS = {
3111
+ 16: {
3112
+ stroke: 1.25,
3113
+ ring: "M14.6666 8.00004C14.6666 4.31814 11.6819 1.33337 7.99998 1.33337C4.31808 1.33337 1.33331 4.31814 1.33331 8.00004C1.33331 11.6819 4.31808 14.6667 7.99998 14.6667C11.6819 14.6667 14.6666 11.6819 14.6666 8.00004Z",
3114
+ mark: "M5.99988 6.06806C5.99988 5.462 6.39988 4.24988 7.99988 4.24988C9.59988 4.24988 9.99988 5.462 9.99988 6.06806C9.99988 6.58025 9.71419 7.52529 8.65996 7.80576C8.30414 7.90042 7.99988 8.18472 7.99988 8.55291V9.24988",
3115
+ dot: "M7.99986 10.4999C8.36805 10.4999 8.66652 10.7984 8.66652 11.1665C8.66652 11.5347 8.36805 11.8332 7.99986 11.8332C7.63167 11.8332 7.33319 11.5347 7.33319 11.1665C7.33319 10.7984 7.63167 10.4999 7.99986 10.4999Z"
3116
+ },
3117
+ 20: {
3118
+ stroke: 1.25,
3119
+ ring: "M18.3334 9.99996C18.3334 5.39759 14.6024 1.66663 10 1.66663C5.39765 1.66663 1.66669 5.39759 1.66669 9.99996C1.66669 14.6023 5.39765 18.3333 10 18.3333C14.6024 18.3333 18.3334 14.6023 18.3334 9.99996Z",
3120
+ mark: "M7.49988 7.58498C7.49988 6.82741 7.99988 5.31226 9.99988 5.31226C11.9999 5.31226 12.4999 6.82741 12.4999 7.58498C12.4999 8.22522 12.1428 9.40652 10.825 9.75711C10.3802 9.87543 9.99988 10.2308 9.99988 10.691V11.5623",
3121
+ dot: "M9.99984 13.1248C10.4601 13.1248 10.8332 13.4979 10.8332 13.9581C10.8332 14.4183 10.4601 14.7914 9.99984 14.7914C9.5396 14.7914 9.1665 14.4183 9.1665 13.9581C9.1665 13.4979 9.5396 13.1248 9.99984 13.1248Z"
3122
+ },
3123
+ 24: {
3124
+ stroke: 1.5,
3125
+ ring: "M22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12Z",
3126
+ mark: "M8.99982 9.10203C8.99982 8.19294 9.59982 6.37476 11.9998 6.37476C14.3998 6.37476 14.9998 8.19294 14.9998 9.10203C14.9998 9.87031 14.5713 11.2879 12.9899 11.7086C12.4562 11.8506 11.9998 12.277 11.9998 12.8293V13.8748",
3127
+ dot: "M11.9998 15.7498C12.5521 15.7498 12.9998 16.1975 12.9998 16.7498C12.9998 17.302 12.5521 17.7498 11.9998 17.7498C11.4475 17.7498 10.9998 17.302 10.9998 16.7498C10.9998 16.1975 11.4475 15.7498 11.9998 15.7498Z"
3128
+ }
3129
+ };
3130
+ var helpOutlineGrid = (size) => {
3131
+ const px = typeof size === "number" ? size : /^\d+(\.\d+)?(px)?$/.test(size) ? parseFloat(size) : void 0;
3132
+ if (px === void 0) return 24;
3133
+ if (px <= 16) return 16;
3134
+ if (px <= 20) return 20;
3135
+ return 24;
3136
+ };
3110
3137
  var Icon_helpOutline = (p) => {
3111
3138
  const { size = 24, strokeWidth, ...rest } = p;
3112
- const stroke = strokeProps(strokeWidth);
3139
+ const grid = helpOutlineGrid(size);
3140
+ const v = HELP_OUTLINE_VARIANTS[grid];
3141
+ const native = Number(size) === grid || size === `${grid}px`;
3142
+ const stroke = native ? { stroke: "currentColor", strokeWidth: v.stroke } : { ...strokeProps(strokeWidth), strokeWidth: strokeWidth ?? v.stroke };
3113
3143
  return /* @__PURE__ */ jsxRuntime.jsxs(
3114
3144
  "svg",
3115
3145
  {
3116
3146
  width: size,
3117
3147
  height: size,
3118
- viewBox: "0 0 24 24",
3148
+ viewBox: `0 0 ${grid} ${grid}`,
3119
3149
  fill: "none",
3120
3150
  xmlns: "http://www.w3.org/2000/svg",
3121
3151
  ...rest,
3122
3152
  children: [
3123
- /* @__PURE__ */ jsxRuntime.jsx(
3124
- "path",
3125
- {
3126
- d: "M22.1998 11.9998C22.1998 6.3665 17.6331 1.7998 11.9998 1.7998C6.3665 1.7998 1.7998 6.3665 1.7998 11.9998C1.7998 17.6331 6.3665 22.1998 11.9998 22.1998C17.6331 22.1998 22.1998 17.6331 22.1998 11.9998Z",
3127
- strokeLinecap: "round",
3128
- ...stroke
3129
- }
3130
- ),
3131
- /* @__PURE__ */ jsxRuntime.jsx(
3132
- "path",
3133
- {
3134
- d: "M8.99982 9.10203C8.99982 8.19294 9.59982 6.37476 11.9998 6.37476C14.3998 6.37476 14.9998 8.19294 14.9998 9.10203C14.9998 9.87031 14.5713 11.2879 12.9899 11.7086C12.4562 11.8506 11.9998 12.277 11.9998 12.8293V13.8748",
3135
- strokeLinecap: "round",
3136
- strokeLinejoin: "round",
3137
- ...stroke
3138
- }
3139
- ),
3140
- /* @__PURE__ */ jsxRuntime.jsx(
3141
- "path",
3142
- {
3143
- d: "M11.9998 15.7498C12.5521 15.7498 12.9998 16.1975 12.9998 16.7498C12.9998 17.302 12.5521 17.7498 11.9998 17.7498C11.4475 17.7498 10.9998 17.302 10.9998 16.7498C10.9998 16.1975 11.4475 15.7498 11.9998 15.7498Z",
3144
- fill: "currentColor"
3145
- }
3146
- )
3153
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: v.ring, strokeLinecap: "round", ...stroke }),
3154
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: v.mark, strokeLinecap: "round", strokeLinejoin: "round", ...stroke }),
3155
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: v.dot, fill: "currentColor" })
3147
3156
  ]
3148
3157
  }
3149
3158
  );
@@ -5652,7 +5661,11 @@ var STag = /* @__PURE__ */ react.forwardRef(function STag2({
5652
5661
  // 레이블에 line-height 가 필요 없다. 오히려 line-height 를 주면 브라우저가
5653
5662
  // 폰트 ascent/descent 를 정수 px 로 반올림한 뒤 남는 half-leading 을 내림 처리해
5654
5663
  // 베이스라인이 최대 1px 위로 밀린다(xs 에서 특히 눈에 띈다).
5655
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex min-w-0 items-center justify-center leading-none", children: label }),
5664
+ // truncate: 폭이 모자라면(`width` 지정, 부모의 max-w 등) 글자에 말줄임이 걸린다. text-overflow 는
5665
+ // inline-flex 에서 듣지 않아 block 으로 둔다 — flex 아이템이라 폭·정렬은 그대로다.
5666
+ // overflow:hidden 이 line-height:1 의 줄 상자 밖으로 나온 글자(디센더)를 자르지 않도록
5667
+ // py 로 잘리는 영역만 넓히고 -my 로 레이아웃 높이는 되돌린다.
5668
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "-my-[2px] block min-w-0 truncate py-[2px] leading-none", children: label }),
5656
5669
  icon && !iconLeft && iconNode
5657
5670
  ]
5658
5671
  }
@@ -13825,6 +13838,11 @@ var GNB_RAIL = {
13825
13838
  var GNB_RAIL_WIDTH = 68;
13826
13839
  var GNB_FOLD_MS = 200;
13827
13840
  var GNB_EXPAND_MS = 200;
13841
+ var GNB_PEEK_ENTER_DELAY_MS = 120;
13842
+ var GNB_PEEK_LEAVE_DELAY_MS = 250;
13843
+ var GNB_PEEK_EDGE_WIDTH = 12;
13844
+ var GNB_PEEK_Z_INDEX = 40;
13845
+ var GNB_PEEK_SHADOW = "0px 0px 64px 0px rgba(34,34,34,0.12), 0px 0px 24px 0px rgba(34,34,34,0.08)";
13828
13846
  var GNB_MENU_WIDTH = 214;
13829
13847
  var GNB_MENU_MIN_WIDTH = 200;
13830
13848
  var GNB_MENU_MAX_WIDTH = 240;
@@ -13921,6 +13939,10 @@ var SGnb = /* @__PURE__ */ react.forwardRef(function SGnb2({
13921
13939
  ariaLabel = "global navigation",
13922
13940
  className,
13923
13941
  style,
13942
+ onPointerEnter: onPointerEnterProp,
13943
+ onPointerLeave: onPointerLeaveProp,
13944
+ onFocus: onFocusProp,
13945
+ onBlur: onBlurProp,
13924
13946
  ...rest
13925
13947
  }, ref) {
13926
13948
  const layout = react.useContext(SLayoutContext);
@@ -14277,7 +14299,80 @@ var SGnb = /* @__PURE__ */ react.forwardRef(function SGnb2({
14277
14299
  event.preventDefault();
14278
14300
  commitMenuWidth(next);
14279
14301
  };
14280
- const railFolded = folded && header === "fix";
14302
+ const [peeking, setPeeking] = react.useState(false);
14303
+ const peekingRef = react.useRef(false);
14304
+ const peekHoverRef = react.useRef(false);
14305
+ const peekFocusRef = react.useRef(false);
14306
+ const peekTimerRef = react.useRef(null);
14307
+ const clearPeekTimer = () => {
14308
+ if (peekTimerRef.current === null) return;
14309
+ clearTimeout(peekTimerRef.current);
14310
+ peekTimerRef.current = null;
14311
+ };
14312
+ const commitPeek = (next) => {
14313
+ peekingRef.current = next;
14314
+ if (next) peekFocusRef.current = false;
14315
+ setPeeking(next);
14316
+ };
14317
+ const schedulePeek = () => {
14318
+ clearPeekTimer();
14319
+ const want = peekHoverRef.current || peekingRef.current && peekFocusRef.current;
14320
+ if (want === peekingRef.current) return;
14321
+ peekTimerRef.current = setTimeout(
14322
+ () => {
14323
+ peekTimerRef.current = null;
14324
+ commitPeek(want);
14325
+ },
14326
+ want ? GNB_PEEK_ENTER_DELAY_MS : GNB_PEEK_LEAVE_DELAY_MS
14327
+ );
14328
+ };
14329
+ react.useEffect(() => {
14330
+ if (folded) return;
14331
+ clearPeekTimer();
14332
+ peekHoverRef.current = false;
14333
+ peekFocusRef.current = false;
14334
+ commitPeek(false);
14335
+ }, [folded]);
14336
+ react.useEffect(() => clearPeekTimer, []);
14337
+ const handlePeekEdgeEnter = (event) => {
14338
+ if (!folded || event.pointerType === "touch") return;
14339
+ peekHoverRef.current = true;
14340
+ schedulePeek();
14341
+ };
14342
+ const handlePeekEnter = (event) => {
14343
+ if (!peekingRef.current) return;
14344
+ handlePeekEdgeEnter(event);
14345
+ };
14346
+ const handlePeekLeave = () => {
14347
+ peekHoverRef.current = false;
14348
+ schedulePeek();
14349
+ };
14350
+ const handlePeekFocus = (event) => {
14351
+ peekFocusRef.current = event.target.matches(":focus-visible");
14352
+ schedulePeek();
14353
+ };
14354
+ const handlePeekBlur = (event) => {
14355
+ if (event.currentTarget.contains(event.relatedTarget)) return;
14356
+ peekFocusRef.current = false;
14357
+ schedulePeek();
14358
+ };
14359
+ const [peekClosing, setPeekClosing] = react.useState(false);
14360
+ const wasPeekingRef = react.useRef(false);
14361
+ react.useEffect(() => {
14362
+ if (peeking) {
14363
+ wasPeekingRef.current = true;
14364
+ setPeekClosing(false);
14365
+ return void 0;
14366
+ }
14367
+ if (!wasPeekingRef.current) return void 0;
14368
+ wasPeekingRef.current = false;
14369
+ setPeekClosing(true);
14370
+ const timer = setTimeout(() => setPeekClosing(false), GNB_FOLD_MS);
14371
+ return () => clearTimeout(timer);
14372
+ }, [peeking]);
14373
+ const peekOverlay = folded && peeking;
14374
+ const visuallyFolded = folded && !peeking;
14375
+ const railFolded = visuallyFolded && header === "fix";
14281
14376
  react.useEffect(() => {
14282
14377
  if (!railFolded || !launcherOpen) return;
14283
14378
  setLauncherOpen(false);
@@ -14324,14 +14419,50 @@ var SGnb = /* @__PURE__ */ react.forwardRef(function SGnb2({
14324
14419
  { width: "100%", ...style }
14325
14420
  ) : {
14326
14421
  // fix 를 닫으면 48px 레일로 좁혀진다.
14327
- width: folded ? GNB_COMMON.foldWidth : bodyWidth,
14422
+ width: visuallyFolded ? GNB_COMMON.foldWidth : bodyWidth,
14423
+ // 호버 오버레이: 폭은 펼치되 음수 margin 으로 레이아웃 자리는 레일 폭 그대로 둔다 — 페이지가 밀리지 않는다.
14424
+ // 폭과 margin 이 같은 길이·곡선으로 움직여 둘의 합(=페이지가 보는 자리)은 전환 내내 변하지 않는다.
14425
+ marginRight: peekOverlay ? `calc(${GNB_COMMON.foldWidth} - ${bodyWidth}px)` : "0px",
14426
+ // 페이지 위에 뜬다(접히는 전환이 끝날 때까지). 그 밖에는 주지 않는다 — 주면 펼친 GNB 에도 stacking context 가 새로 생긴다.
14427
+ // (폴드 버튼으로 펴서 folded 가 풀리면 접히는 중이 아니다 — 펼친 GNB 에는 주지 않는다)
14428
+ zIndex: peekOverlay || peekClosing && folded ? GNB_PEEK_Z_INDEX : void 0,
14429
+ boxShadow: peekOverlay ? GNB_PEEK_SHADOW : "none",
14328
14430
  // 폭을 끄는 중에는 전환을 끈다 — 접힘용 전환이 걸려 있으면 컬럼이 손보다 늦게 따라온다.
14329
- transition: resizing ? "none" : `width ${GNB_FOLD_MS}ms ease-out`,
14431
+ transition: resizing ? "none" : `width ${GNB_FOLD_MS}ms ease-out, margin-right ${GNB_FOLD_MS}ms ease-out, box-shadow ${GNB_FOLD_MS}ms ease-out`,
14330
14432
  backgroundColor: C.panelBg,
14331
14433
  borderRight: rightBorder,
14332
14434
  ...style
14333
14435
  };
14334
14436
  const hideAfterSlide = (hidden) => `visibility 0s linear ${hidden ? `${GNB_FOLD_MS}ms` : "0s"}`;
14437
+ const peekEdge = visuallyFolded ? /* @__PURE__ */ jsxRuntime.jsx(
14438
+ "div",
14439
+ {
14440
+ "aria-hidden": true,
14441
+ "data-gnb-peek-edge": "",
14442
+ className: "absolute inset-y-0 left-0 z-[2]",
14443
+ style: { width: GNB_PEEK_EDGE_WIDTH },
14444
+ onPointerEnter: handlePeekEdgeEnter,
14445
+ onPointerLeave: handlePeekLeave
14446
+ }
14447
+ ) : null;
14448
+ const rootPeekProps = {
14449
+ onPointerEnter: (event) => {
14450
+ onPointerEnterProp?.(event);
14451
+ if (!isFull) handlePeekEnter(event);
14452
+ },
14453
+ onPointerLeave: (event) => {
14454
+ onPointerLeaveProp?.(event);
14455
+ if (!isFull) handlePeekLeave();
14456
+ },
14457
+ onFocus: (event) => {
14458
+ onFocusProp?.(event);
14459
+ if (!isFull) handlePeekFocus(event);
14460
+ },
14461
+ onBlur: (event) => {
14462
+ onBlurProp?.(event);
14463
+ if (!isFull) handlePeekBlur(event);
14464
+ }
14465
+ };
14335
14466
  const sideFoldStyle = railFolded ? {
14336
14467
  opacity: 0,
14337
14468
  position: "absolute",
@@ -14481,7 +14612,9 @@ var SGnb = /* @__PURE__ */ react.forwardRef(function SGnb2({
14481
14612
  "data-header": header,
14482
14613
  "data-color": color,
14483
14614
  ...rest,
14615
+ ...rootPeekProps,
14484
14616
  children: [
14617
+ !isFull && peekEdge,
14485
14618
  /* @__PURE__ */ jsxRuntime.jsx(
14486
14619
  "div",
14487
14620
  {
@@ -14516,19 +14649,33 @@ var SGnb = /* @__PURE__ */ react.forwardRef(function SGnb2({
14516
14649
  /* @__PURE__ */ jsxRuntime.jsxs(
14517
14650
  "div",
14518
14651
  {
14519
- className: cn("relative overflow-hidden", !isFullFrame && "min-h-0 flex-1"),
14520
- style: isFullFrame ? { gridArea: "nav" } : void 0,
14652
+ className: cn(
14653
+ "relative",
14654
+ (!isFull || !folded) && "overflow-hidden",
14655
+ !isFullFrame && "min-h-0 flex-1"
14656
+ ),
14657
+ style: {
14658
+ ...isFullFrame ? { gridArea: "nav" } : null,
14659
+ // 접힌 동안 띠와 펼쳐진 본문이 페이지 위에 서도록 이 칸을 올린다
14660
+ ...isFull && folded ? { zIndex: GNB_PEEK_Z_INDEX } : null
14661
+ },
14521
14662
  children: [
14663
+ isFull && peekEdge,
14522
14664
  /* @__PURE__ */ jsxRuntime.jsxs(
14523
14665
  "div",
14524
14666
  {
14525
14667
  className: "flex h-full flex-row",
14526
14668
  style: {
14527
14669
  width: bodyWidth,
14528
- transform: folded ? "translateX(-100%)" : "none",
14529
- visibility: folded ? "hidden" : "visible",
14530
- transition: `transform ${GNB_FOLD_MS}ms ease-out, ${hideAfterSlide(folded)}`
14670
+ transform: visuallyFolded ? "translateX(-100%)" : "none",
14671
+ visibility: visuallyFolded ? "hidden" : "visible",
14672
+ boxShadow: peekOverlay ? GNB_PEEK_SHADOW : "none",
14673
+ transition: `transform ${GNB_FOLD_MS}ms ease-out, box-shadow ${GNB_FOLD_MS}ms ease-out, ${hideAfterSlide(visuallyFolded)}`
14531
14674
  },
14675
+ onPointerEnter: isFull ? handlePeekEnter : void 0,
14676
+ onPointerLeave: isFull ? handlePeekLeave : void 0,
14677
+ onFocus: isFull ? handlePeekFocus : void 0,
14678
+ onBlur: isFull ? handlePeekBlur : void 0,
14532
14679
  children: [
14533
14680
  showRail && // 레일 컬럼 — 스크롤되는 아이템 목록(nav)을 가운데 두고 위아래에 고정 슬롯(top/footer)을 붙인다.
14534
14681
  // 두 슬롯은 내비게이션이 아니므로 nav(landmark) 밖에 두고, 컬럼 폭·구분선·배경은 이 래퍼가
@@ -17530,6 +17677,7 @@ function flattenOptions(options, depth = 1, inheritDisabled = true) {
17530
17677
  out.push({
17531
17678
  value: String(o.value),
17532
17679
  label: o.label,
17680
+ tag: o.tag,
17533
17681
  disabled: !!o.disabled || allChildrenDisabled,
17534
17682
  depth,
17535
17683
  isGroup: true,
@@ -17540,6 +17688,7 @@ function flattenOptions(options, depth = 1, inheritDisabled = true) {
17540
17688
  out.push({
17541
17689
  value: String(o.value),
17542
17690
  label: o.label,
17691
+ tag: o.tag,
17543
17692
  disabled: !!o.disabled,
17544
17693
  depth,
17545
17694
  isGroup: false,
@@ -17744,9 +17893,15 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
17744
17893
  const effectiveDisabled = disabled || allOptionsDisabled;
17745
17894
  const searchVisible = showSearch && (partialOptions || selectable.length >= SEARCH_THRESHOLD);
17746
17895
  const labelByValue = react.useMemo(() => new Map(flat.map((o) => [o.value, o.label])), [flat]);
17896
+ const tagByValue = react.useMemo(() => new Map(flat.map((o) => [o.value, o.tag])), [flat]);
17747
17897
  const rawByValue = react.useMemo(() => new Map(flat.map((o) => [o.value, o.raw])), [flat]);
17748
17898
  const optionByValue = react.useMemo(
17749
- () => new Map(flat.map((o) => [o.value, { value: o.raw, label: o.label }])),
17899
+ () => new Map(
17900
+ flat.map((o) => [
17901
+ o.value,
17902
+ { value: o.raw, label: o.label, ...o.tag && { tag: o.tag } }
17903
+ ])
17904
+ ),
17750
17905
  [flat]
17751
17906
  );
17752
17907
  const selected = react.useMemo(() => {
@@ -17759,7 +17914,11 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
17759
17914
  if (value == null) return map;
17760
17915
  for (const v of Array.isArray(value) ? value : [value]) {
17761
17916
  if (v != null && typeof v === "object") {
17762
- map.set(String(v.value), { value: v.value, label: v.label });
17917
+ map.set(String(v.value), {
17918
+ value: v.value,
17919
+ label: v.label,
17920
+ ...v.tag && { tag: v.tag }
17921
+ });
17763
17922
  }
17764
17923
  }
17765
17924
  return map;
@@ -18100,6 +18259,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
18100
18259
  });
18101
18260
  }
18102
18261
  }
18262
+ const tagSize = size === "md" ? "sm" : "xs";
18103
18263
  const isPlaceholder = selected.length === 0;
18104
18264
  const showTriggerSpinner = loading2 && !effectiveDisabled;
18105
18265
  const showClear = clearable && !effectiveDisabled && !isPlaceholder;
@@ -18444,7 +18604,15 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
18444
18604
  ))
18445
18605
  }
18446
18606
  )
18447
- ) : /* @__PURE__ */ jsxRuntime.jsx(TriggerLabel, { label: triggerText }),
18607
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
18608
+ TriggerLabel,
18609
+ {
18610
+ label: triggerText,
18611
+ tag: multi ? void 0 : tagByValue.get(selected[0]) ?? valueOptions.get(selected[0])?.tag,
18612
+ tagSize,
18613
+ tagDisabled: effectiveDisabled
18614
+ }
18615
+ ),
18448
18616
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex shrink-0 items-center gap-[8px]", children: [
18449
18617
  showClear && /* @__PURE__ */ jsxRuntime.jsx(TriggerRemoveButton, { ariaLabel: t.clear, onRemove: handleClear }),
18450
18618
  showTriggerSpinner ? /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": true, className: "inline-flex shrink-0", "data-select-trigger-spinner": true, children: /* @__PURE__ */ jsxRuntime.jsx(SCircleProgress, { indeterminate: true, type: "neutral", size: 12 }) }) : /* @__PURE__ */ jsxRuntime.jsx(
@@ -18537,6 +18705,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
18537
18705
  hasItems ? /* @__PURE__ */ jsxRuntime.jsx(
18538
18706
  ItemList,
18539
18707
  {
18708
+ tagSize,
18540
18709
  listboxId,
18541
18710
  activeOptionId,
18542
18711
  items: visibleItems,
@@ -18594,6 +18763,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
18594
18763
  });
18595
18764
  var SSelect2 = /* @__PURE__ */ withDisplayName(SSelectImpl, "SSelect");
18596
18765
  function ItemList({
18766
+ tagSize,
18597
18767
  listboxId,
18598
18768
  activeOptionId,
18599
18769
  items,
@@ -18776,6 +18946,9 @@ function ItemList({
18776
18946
  OptionLabel,
18777
18947
  {
18778
18948
  label: o.label,
18949
+ tag: o.tag,
18950
+ tagSize,
18951
+ tagDisabled: rowDisabled,
18779
18952
  className: treeLabelClass("sm", {
18780
18953
  // 고른 줄은 굵게 — multi_tree 는 일부만 고른 그룹도 굵다 (STree 와 같다)
18781
18954
  emphasized: checkState !== false,
@@ -18910,7 +19083,16 @@ function ItemList({
18910
19083
  focused: focusedValue === o.value
18911
19084
  }
18912
19085
  ),
18913
- /* @__PURE__ */ jsxRuntime.jsx(OptionLabel, { label: o.label, className: "flex-1 truncate" })
19086
+ /* @__PURE__ */ jsxRuntime.jsx(
19087
+ OptionLabel,
19088
+ {
19089
+ label: o.label,
19090
+ tag: o.tag,
19091
+ tagSize,
19092
+ tagDisabled: o.disabled,
19093
+ className: "flex-1 truncate"
19094
+ }
19095
+ )
18914
19096
  ]
18915
19097
  },
18916
19098
  rowKey
@@ -18942,11 +19124,52 @@ function TreeRowsMotion({ opening, children }) {
18942
19124
  react.useEffect(() => () => animationRef.current?.cancel(), []);
18943
19125
  return /* @__PURE__ */ jsxRuntime.jsx("div", { ref, className: "overflow-hidden", "aria-hidden": opening ? void 0 : true, children });
18944
19126
  }
18945
- function OptionLabel({ label, className }) {
19127
+ function OptionLabel({
19128
+ label,
19129
+ tag,
19130
+ tagSize,
19131
+ tagDisabled,
19132
+ className
19133
+ }) {
19134
+ if (tag) {
19135
+ return /* @__PURE__ */ jsxRuntime.jsx(
19136
+ "span",
19137
+ {
19138
+ className: cn(
19139
+ className,
19140
+ "min-w-0 overflow-hidden",
19141
+ tagDisabled && "opacity-(--opacity-050)"
19142
+ ),
19143
+ children: /* @__PURE__ */ jsxRuntime.jsx(
19144
+ STag,
19145
+ {
19146
+ ...tag,
19147
+ size: tagSize,
19148
+ label: isHtmlLabel(label) ? extractText(label) : label,
19149
+ className: cn("max-w-full min-w-0", tag.className)
19150
+ }
19151
+ )
19152
+ }
19153
+ );
19154
+ }
18946
19155
  return isHtmlLabel(label) ? /* @__PURE__ */ jsxRuntime.jsx("span", { className, dangerouslySetInnerHTML: { __html: sanitizeInlineHtml(label) } }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className, children: label });
18947
19156
  }
18948
- function TriggerLabel({ label }) {
18949
- return /* @__PURE__ */ jsxRuntime.jsx(OptionLabel, { label, className: "truncate" });
19157
+ function TriggerLabel({
19158
+ label,
19159
+ tag,
19160
+ tagSize,
19161
+ tagDisabled
19162
+ }) {
19163
+ return /* @__PURE__ */ jsxRuntime.jsx(
19164
+ OptionLabel,
19165
+ {
19166
+ label,
19167
+ tag,
19168
+ tagSize,
19169
+ tagDisabled,
19170
+ className: "truncate"
19171
+ }
19172
+ );
18950
19173
  }
18951
19174
  function TriggerRemoveButton({ ariaLabel, onRemove }) {
18952
19175
  return /* @__PURE__ */ jsxRuntime.jsx(