sellmate-design-system-react 9.0.0-beta.35 → 9.0.0-beta.37

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/dist/index.cjs CHANGED
@@ -6266,6 +6266,28 @@ var SSplitter2 = /* @__PURE__ */ Object.assign(SSplitterRoot, {
6266
6266
  Before: SSplitterBefore2,
6267
6267
  After: SSplitterAfter2
6268
6268
  });
6269
+ var SURFACE = {
6270
+ default: {
6271
+ bg: "var(--cmp-callout-default-bg)",
6272
+ border: "var(--cmp-callout-default-border)",
6273
+ content: "var(--cmp-callout-default-content)"
6274
+ },
6275
+ danger: {
6276
+ bg: "var(--cmp-callout-danger-bg)",
6277
+ border: "var(--cmp-callout-danger-border)",
6278
+ content: "var(--cmp-callout-danger-content)"
6279
+ },
6280
+ // default 와 같은 안내인데 배경만 흰 면이다 — 회색 면(neutral·screen) 위에서 default 의
6281
+ // 옅은 회색이 묻히는 자리에 쓴다.
6282
+ //
6283
+ // ⚠️ callout.white 토큰 그룹이 아직 없어 배경은 시스템 흰 면을, 테두리·본문색은 default 것을
6284
+ // 빌려 쓴다. 디자인팀이 전용 토큰을 내면 이 세 줄을 그것으로 갈아끼운다(토큰 키는 만들지 않는다).
6285
+ white: {
6286
+ bg: "var(--sys-color-bg-frame)",
6287
+ border: "var(--cmp-callout-default-border)",
6288
+ content: "var(--cmp-callout-default-content)"
6289
+ }
6290
+ };
6269
6291
  var DANGER_TITLE_LABEL = "\uC8FC\uC758\uC0AC\uD56D";
6270
6292
  function renderItems(message, depth = 1) {
6271
6293
  const out = [];
@@ -6299,10 +6321,11 @@ function renderItems(message, depth = 1) {
6299
6321
  }
6300
6322
  var SCallout = /* @__PURE__ */ react.forwardRef(function SCallout2({ type = "default", message = [], children, className, style }, ref) {
6301
6323
  const isDanger = type === "danger";
6324
+ const surface = SURFACE[type];
6302
6325
  const vars = {
6303
- "--co-bg": `var(--cmp-callout-${type}-bg)`,
6304
- "--co-border": `var(--cmp-callout-${type}-border)`,
6305
- "--co-content": `var(--cmp-callout-${type}-content)`
6326
+ "--co-bg": surface.bg,
6327
+ "--co-border": surface.border,
6328
+ "--co-content": surface.content
6306
6329
  };
6307
6330
  return /* @__PURE__ */ jsxRuntime.jsxs(
6308
6331
  "div",
@@ -6468,10 +6491,19 @@ var SLoadingContainer = /* @__PURE__ */ react.forwardRef(
6468
6491
  var state = { open: false, message: void 0 };
6469
6492
  var listeners = /* @__PURE__ */ new Set();
6470
6493
  var emit = () => listeners.forEach((l) => l());
6471
- var setState = (patch) => {
6472
- state = { ...state, ...patch };
6494
+ var entries = [];
6495
+ var seq = 0;
6496
+ var sync = () => {
6497
+ const top = entries[entries.length - 1];
6498
+ state = { open: entries.length > 0, message: top?.message };
6473
6499
  emit();
6474
6500
  };
6501
+ var release = (id) => {
6502
+ const next = entries.filter((e) => e.id !== id);
6503
+ if (next.length === entries.length) return;
6504
+ entries = next;
6505
+ sync();
6506
+ };
6475
6507
  var subscribe = (cb) => {
6476
6508
  listeners.add(cb);
6477
6509
  return () => listeners.delete(cb);
@@ -6491,14 +6523,41 @@ function ensureMounted() {
6491
6523
  client.createRoot(host).render(/* @__PURE__ */ jsxRuntime.jsx(SLoadingViewport, {}));
6492
6524
  }
6493
6525
  var loading = {
6494
- /** 로딩 오버레이 표시 (메시지 갱신도 동일 호출) */
6526
+ /**
6527
+ * 로딩 오버레이 표시. 겹쳐 부르면 그만큼 쌓이고 **마지막 하나가 걷힐 때** 닫힌다.
6528
+ * 반환값은 이 호출 하나만 걷는 함수다(두 번 불러도 한 번만 걷힌다) — `hide()` 대신 이것을
6529
+ * 쓰면 짝이 어긋나지 않는다.
6530
+ *
6531
+ * ⚠️ 메시지만 바꾸려면 `update()` 를 쓴다. `show()` 를 다시 부르면 하나 더 쌓여
6532
+ * `hide()` 한 번으로는 닫히지 않는다.
6533
+ */
6495
6534
  show(options = {}) {
6496
6535
  ensureMounted();
6497
- setState({ open: true, message: options.message });
6536
+ const id = ++seq;
6537
+ entries = [...entries, { id, message: options.message }];
6538
+ sync();
6539
+ return () => release(id);
6498
6540
  },
6499
- /** 로딩 오버레이 숨김 */
6541
+ /** 표시 중인 메시지 갱신 — 쌓인 수는 그대로다 */
6542
+ update(options) {
6543
+ const top = entries[entries.length - 1];
6544
+ if (!top) return;
6545
+ entries = [...entries.slice(0, -1), { ...top, message: options.message }];
6546
+ sync();
6547
+ },
6548
+ /** 가장 나중의 show 하나를 걷는다. 남은 것이 없을 때만 오버레이가 닫힌다 */
6500
6549
  hide() {
6501
- setState({ open: false });
6550
+ const top = entries[entries.length - 1];
6551
+ if (top) release(top.id);
6552
+ },
6553
+ /**
6554
+ * 쌓인 것을 모두 걷고 즉시 닫는다. 화면 전환·에러 복구처럼 진행 중이던 작업의 짝을 더는
6555
+ * 맞출 수 없을 때만 쓴다 — 평소에는 `hide()` 나 `show()` 가 준 함수로 하나씩 걷는다.
6556
+ */
6557
+ hideAll() {
6558
+ if (entries.length === 0) return;
6559
+ entries = [];
6560
+ sync();
6502
6561
  }
6503
6562
  };
6504
6563
  var SIZE_CONFIG = {
@@ -7917,7 +7976,7 @@ var MODAL_HOST_Z_INDEX = Z_INDEX.modalHost;
7917
7976
  var MODAL_HOST_ATTRIBUTE = "data-s-modal-host";
7918
7977
  var nextId = 0;
7919
7978
  var nextLayerIndex = 0;
7920
- var entries = [];
7979
+ var entries2 = [];
7921
7980
  function getModalHost() {
7922
7981
  if (typeof document === "undefined" || !document.body) return void 0;
7923
7982
  const existing = document.body.querySelector(`[${MODAL_HOST_ATTRIBUTE}]`);
@@ -7938,16 +7997,16 @@ function registerModalStackEntry() {
7938
7997
  id: ++nextId,
7939
7998
  layerIndex: nextLayerIndex++
7940
7999
  };
7941
- entries.push(entry);
8000
+ entries2.push(entry);
7942
8001
  let registered = true;
7943
8002
  return {
7944
8003
  layerIndex: entry.layerIndex,
7945
- isTop: () => entries[entries.length - 1] === entry,
8004
+ isTop: () => entries2[entries2.length - 1] === entry,
7946
8005
  unregister: () => {
7947
8006
  if (!registered) return;
7948
8007
  registered = false;
7949
- const index = entries.indexOf(entry);
7950
- if (index !== -1) entries.splice(index, 1);
8008
+ const index = entries2.indexOf(entry);
8009
+ if (index !== -1) entries2.splice(index, 1);
7951
8010
  }
7952
8011
  };
7953
8012
  }
@@ -7971,8 +8030,10 @@ function SModalContainer({
7971
8030
  const contentRef = react.useRef(null);
7972
8031
  const stackRegistrationRef = react.useRef(null);
7973
8032
  const [layerIndex, setLayerIndex] = react.useState(null);
8033
+ const closeRequested = react.useRef(false);
7974
8034
  react.useEffect(() => {
7975
8035
  if (open) {
8036
+ closeRequested.current = false;
7976
8037
  setRendered(true);
7977
8038
  return;
7978
8039
  }
@@ -8028,7 +8089,7 @@ function SModalContainer({
8028
8089
  {
8029
8090
  forceMount: rendered ? true : void 0,
8030
8091
  ref: contentRef,
8031
- className: "pointer-events-auto outline-none data-[state=open]:animate-modal-in data-[state=closed]:animate-modal-out",
8092
+ className: "pointer-events-auto outline-none data-[state=open]:animate-modal-in data-[state=closed]:pointer-events-none data-[state=closed]:animate-modal-out",
8032
8093
  style: { animationFillMode: "forwards" },
8033
8094
  onOpenAutoFocus: (e) => {
8034
8095
  e.preventDefault();
@@ -8059,6 +8120,8 @@ function SModalContainer({
8059
8120
  icon: "close",
8060
8121
  ariaLabel: "\uB2EB\uAE30",
8061
8122
  onClick: () => {
8123
+ if (closeRequested.current) return;
8124
+ closeRequested.current = true;
8062
8125
  onClose?.();
8063
8126
  onOpenChange?.(false);
8064
8127
  },
@@ -8132,6 +8195,16 @@ function SConfirmModal({
8132
8195
  }) {
8133
8196
  const iconName = TYPE_ICON[type];
8134
8197
  const mainStyle = MAIN_BUTTON_STYLE[mainButtonName ?? DEFAULT_MAIN_BUTTON[type]];
8198
+ const [settled, setSettled] = react.useState(false);
8199
+ react.useEffect(() => {
8200
+ if (open) setSettled(false);
8201
+ }, [open]);
8202
+ const settle = (emit3) => {
8203
+ if (settled) return;
8204
+ setSettled(true);
8205
+ emit3?.();
8206
+ onOpenChange?.(false);
8207
+ };
8135
8208
  const tagNode = tagSlot ?? (tagLabel ? /* @__PURE__ */ jsxRuntime.jsx(STag, { shape: tagShape, size: tagSize, color: tagColor, label: tagLabel }) : null);
8136
8209
  const optionNode = optionSlot ?? (slotLabel ? /* @__PURE__ */ jsxRuntime.jsx("span", { children: slotLabel }) : null);
8137
8210
  const hasContentBox = tagNode != null || optionNode != null;
@@ -8144,7 +8217,7 @@ function SConfirmModal({
8144
8217
  persistent,
8145
8218
  ariaTitle: modalTitle,
8146
8219
  showClose: true,
8147
- onClose,
8220
+ onClose: () => settle(onClose),
8148
8221
  className: "w-fit min-w-[min(520px,calc(100dvw-48px))]",
8149
8222
  children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-0 flex-auto flex-col items-center gap-[40px] px-[32px] py-[40px]", children: [
8150
8223
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex w-full min-h-0 flex-col items-center gap-[20px] overflow-y-auto", children: [
@@ -8194,10 +8267,8 @@ function SConfirmModal({
8194
8267
  color: "neutral",
8195
8268
  outline: true,
8196
8269
  size: "md",
8197
- onClick: () => {
8198
- onCancel?.();
8199
- onOpenChange?.(false);
8200
- },
8270
+ disabled: settled,
8271
+ onClick: () => settle(onCancel),
8201
8272
  label: subButtonLabel
8202
8273
  }
8203
8274
  ),
@@ -8206,10 +8277,8 @@ function SConfirmModal({
8206
8277
  {
8207
8278
  ...mainStyle,
8208
8279
  size: "md",
8209
- onClick: () => {
8210
- onOk?.();
8211
- onOpenChange?.(false);
8212
- },
8280
+ disabled: settled,
8281
+ onClick: () => settle(onOk),
8213
8282
  label: mainButtonLabel
8214
8283
  }
8215
8284
  )
@@ -8452,10 +8521,10 @@ function SDrawer({
8452
8521
  }, []);
8453
8522
  react.useEffect(() => {
8454
8523
  if (!rendered) return;
8455
- const sync = () => setViewportWidth(window.innerWidth);
8456
- sync();
8457
- window.addEventListener("resize", sync);
8458
- return () => window.removeEventListener("resize", sync);
8524
+ const sync2 = () => setViewportWidth(window.innerWidth);
8525
+ sync2();
8526
+ window.addEventListener("resize", sync2);
8527
+ return () => window.removeEventListener("resize", sync2);
8459
8528
  }, [rendered]);
8460
8529
  react.useEffect(() => {
8461
8530
  const panel2 = panelRef.current;
@@ -8701,7 +8770,7 @@ function SDrawer({
8701
8770
 
8702
8771
  // src/lib/modal-outlet.ts
8703
8772
  var EMPTY = [];
8704
- var entries2 = EMPTY;
8773
+ var entries3 = EMPTY;
8705
8774
  var listeners2 = /* @__PURE__ */ new Set();
8706
8775
  var outletRendered = false;
8707
8776
  var outletCount = 0;
@@ -8716,7 +8785,7 @@ function subscribeModalEntries(listener) {
8716
8785
  };
8717
8786
  }
8718
8787
  function getModalEntries() {
8719
- return entries2;
8788
+ return entries3;
8720
8789
  }
8721
8790
  function getServerModalEntries() {
8722
8791
  return EMPTY;
@@ -8735,9 +8804,9 @@ function registerModalOutlet() {
8735
8804
  function sweepOrphans() {
8736
8805
  if (outletCount > 0) return;
8737
8806
  outletRendered = false;
8738
- if (entries2 === EMPTY) return;
8739
- const orphans = entries2;
8740
- entries2 = EMPTY;
8807
+ if (entries3 === EMPTY) return;
8808
+ const orphans = entries3;
8809
+ entries3 = EMPTY;
8741
8810
  emit2();
8742
8811
  for (const entry of orphans) entry.onOrphan();
8743
8812
  }
@@ -8749,14 +8818,14 @@ function isModalOutletMounted() {
8749
8818
  }
8750
8819
  function addModalEntry(host, node, onOrphan) {
8751
8820
  const id = ++nextId2;
8752
- entries2 = [...entries2, { id, host, node, onOrphan }];
8821
+ entries3 = [...entries3, { id, host, node, onOrphan }];
8753
8822
  emit2();
8754
8823
  return id;
8755
8824
  }
8756
8825
  function removeModalEntry(id) {
8757
- const next = entries2.filter((entry) => entry.id !== id);
8758
- if (next.length === entries2.length) return;
8759
- entries2 = next;
8826
+ const next = entries3.filter((entry) => entry.id !== id);
8827
+ if (next.length === entries3.length) return;
8828
+ entries3 = next;
8760
8829
  emit2();
8761
8830
  }
8762
8831
  var EXIT_DURATION = 300;
@@ -8797,11 +8866,22 @@ var SModalRefImpl = class {
8797
8866
  else this.pendingPatches.push(patch);
8798
8867
  return this;
8799
8868
  }
8800
- // 닫힘 트리거 — 이미 닫힘이 시작됐으면 무시(중복 콜백 방지)
8801
- settle(emit3) {
8802
- if (this.dismissRequested) return;
8869
+ /**
8870
+ * 확정 콜백 1회 발화 — 이미 닫힘이 시작됐으면 무시한다. 발화했으면 true.
8871
+ *
8872
+ * 한 번 띄운 모달의 결말은 하나다(ok 이거나 cancel 이거나 close 다). 닫힘은 시작만 하고
8873
+ * 퇴장 애니메이션 동안 카드가 남아 있으므로, 그동안의 재클릭·키 반복이 두 번째 결말을
8874
+ * 만들지 않도록 여기서 막는다.
8875
+ */
8876
+ settleEmit(emit3) {
8877
+ if (this.dismissRequested) return false;
8803
8878
  this.dismissRequested = true;
8804
8879
  emit3();
8880
+ return true;
8881
+ }
8882
+ // 닫힘 트리거 — 이미 닫힘이 시작됐으면 무시(중복 콜백 방지)
8883
+ settle(emit3) {
8884
+ if (!this.settleEmit(emit3)) return;
8805
8885
  if (this.closeBinding) this.closeBinding();
8806
8886
  else this.pendingClose = true;
8807
8887
  }
@@ -8854,17 +8934,18 @@ var SModalRefImpl = class {
8854
8934
  _markDismissed() {
8855
8935
  this.dismissRequested = true;
8856
8936
  }
8857
- /** @internal confirm 버튼 경로에서 onOk 발화 */
8937
+ // 아래 세 경로는 컴포넌트가 스스로 닫으므로(onOpenChange(false)) 발화 가드만 태운다.
8938
+ /** @internal confirm 버튼 경로에서 onOk 발화 (닫히는 중의 재클릭·키 반복은 무시) */
8858
8939
  _emitOk() {
8859
- this.okFn?.();
8940
+ this.settleEmit(() => this.okFn?.());
8860
8941
  }
8861
- /** @internal confirm 버튼 경로에서 onCancel 발화 */
8942
+ /** @internal confirm 버튼 경로에서 onCancel 발화 (닫히는 중의 재클릭·키 반복은 무시) */
8862
8943
  _emitCancel() {
8863
- this.cancelFn?.();
8944
+ this.settleEmit(() => this.cancelFn?.());
8864
8945
  }
8865
- /** @internal 닫기(X) 경로에서 onClose 발화 */
8946
+ /** @internal 닫기(X) 경로에서 onClose 발화 (닫히는 중의 재클릭·키 반복은 무시) */
8866
8947
  _emitClose() {
8867
- this.closeFn?.();
8948
+ this.settleEmit(() => this.closeFn?.());
8868
8949
  }
8869
8950
  /** @internal loading 버튼 경로에서 onClick 발화 */
8870
8951
  _emitClick() {
@@ -9060,7 +9141,7 @@ function SModalEntryPortal({ entry }) {
9060
9141
  }
9061
9142
  function SModalOutlet() {
9062
9143
  markModalOutletRendered();
9063
- const entries3 = react.useSyncExternalStore(
9144
+ const entries4 = react.useSyncExternalStore(
9064
9145
  subscribeModalEntries,
9065
9146
  getModalEntries,
9066
9147
  getServerModalEntries
@@ -9073,11 +9154,25 @@ function SModalOutlet() {
9073
9154
  }
9074
9155
  return registerModalOutlet();
9075
9156
  }, []);
9076
- return /* @__PURE__ */ jsxRuntime.jsx(PortalContainerProvider, { value: null, children: /* @__PURE__ */ jsxRuntime.jsx(InsideModalProvider, { value: false, children: entries3.map((entry) => /* @__PURE__ */ jsxRuntime.jsx(SModalEntryPortal, { entry }, entry.id)) }) });
9157
+ return /* @__PURE__ */ jsxRuntime.jsx(PortalContainerProvider, { value: null, children: /* @__PURE__ */ jsxRuntime.jsx(InsideModalProvider, { value: false, children: entries4.map((entry) => /* @__PURE__ */ jsxRuntime.jsx(SModalEntryPortal, { entry }, entry.id)) }) });
9077
9158
  }
9078
9159
  var useIsomorphicLayoutEffect = typeof window !== "undefined" ? react.useLayoutEffect : react.useEffect;
9160
+ var SPageColumnContext = /* @__PURE__ */ react.createContext(null);
9161
+ function SPageColumn({ full, children }) {
9162
+ const [scrolled, setScrolled] = react.useState(false);
9163
+ const value = react.useMemo(() => ({ scrolled, setScrolled }), [scrolled]);
9164
+ return /* @__PURE__ */ jsxRuntime.jsx(SPageColumnContext.Provider, { value, children: /* @__PURE__ */ jsxRuntime.jsx(
9165
+ "div",
9166
+ {
9167
+ className: "flex min-h-0 min-w-0 flex-1 flex-col",
9168
+ style: full ? { gridArea: "page" } : void 0,
9169
+ children
9170
+ }
9171
+ ) });
9172
+ }
9079
9173
  var isSameNav = (a, b) => a === b || !!a && !!b && a.columnWidth === b.columnWidth && a.topHeight === b.topHeight && a.foldMs === b.foldMs && a.requireFullHeader === b.requireFullHeader && a.resizing === b.resizing;
9080
9174
  var SLayoutContext = /* @__PURE__ */ react.createContext(null);
9175
+ var isPageHeaderElement = (node) => react.isValidElement(node) && node.type?.displayName === "SPageHeader";
9081
9176
  var SLAYOUT_MIN_WIDTH = 1366;
9082
9177
  var SLAYOUT_MIN_WIDTH_VAR = "--s-layout-min-width";
9083
9178
  function useSLayout() {
@@ -9139,6 +9234,15 @@ var SLayout = /* @__PURE__ */ react.forwardRef(function SLayout2({
9139
9234
  // 단 폭을 끄는 중에는 전환을 끈다 — 그러지 않으면 열이 손보다 늦게 따라온다.
9140
9235
  transition: nav?.resizing ? "none" : `grid-template-columns ${nav?.foldMs ?? 0}ms ease-out`
9141
9236
  } : void 0;
9237
+ const laidOutChildren = react.useMemo(() => {
9238
+ const kids = react.Children.toArray(children);
9239
+ const at = kids.findIndex(isPageHeaderElement);
9240
+ if (at === -1) return children;
9241
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
9242
+ kids.slice(0, at),
9243
+ /* @__PURE__ */ jsxRuntime.jsx(SPageColumn, { full: isFullHeader, children: kids.slice(at) })
9244
+ ] });
9245
+ }, [children, isFullHeader]);
9142
9246
  return /* @__PURE__ */ jsxRuntime.jsx(SLayoutContext.Provider, { value: ctx, children: /* @__PURE__ */ jsxRuntime.jsx(
9143
9247
  "div",
9144
9248
  {
@@ -9152,7 +9256,7 @@ var SLayout = /* @__PURE__ */ react.forwardRef(function SLayout2({
9152
9256
  ),
9153
9257
  style: { ...minWidthStyle, ...frameStyle, ...style },
9154
9258
  ...rest,
9155
- children
9259
+ children: laidOutChildren
9156
9260
  }
9157
9261
  ) });
9158
9262
  });
@@ -10517,63 +10621,12 @@ var SGnbSystem = /* @__PURE__ */ react.forwardRef(function SGnbSystem2({
10517
10621
  }
10518
10622
  );
10519
10623
  });
10520
- var SPageHeader = /* @__PURE__ */ withDisplayName(
10521
- react.forwardRef(function SPageHeader2({ title, subText, fix = false, onBack, slot, scrolled = false, className, style, ...rest }, ref) {
10522
- const hasBack = onBack != null;
10523
- return /* @__PURE__ */ jsxRuntime.jsxs(
10524
- "header",
10525
- {
10526
- ref,
10527
- className: cn(
10528
- "relative z-10 box-border flex shrink-0 items-center gap-(--cmp-pageHeader-gap) px-(--cmp-pageHeader-paddingX)",
10529
- fix ? "h-(--cmp-pageHeader-bar-height) border-b border-(--cmp-pageHeader-bar-border) bg-(--cmp-pageHeader-bar-bg) py-(--cmp-pageHeader-bar-paddingY)" : cn(
10530
- // 배경은 항상 투명 — 페이지 배경을 따라갈지는 fix 가 정하지 scrolled 가 정하지 않는다.
10531
- // 스크롤되면 그림자만 더해 콘텐츠 위에 뜬 것처럼 보이게 한다.
10532
- "bg-transparent pt-(--cmp-pageHeader-plain-paddingTop) pb-(--cmp-pageHeader-plain-paddingBottom)",
10533
- scrolled && "shadow-floating"
10534
- ),
10535
- className
10536
- ),
10537
- style,
10538
- ...rest,
10539
- children: [
10540
- hasBack && /* @__PURE__ */ jsxRuntime.jsx(
10541
- SGhostButton,
10542
- {
10543
- icon: "chevronLeft",
10544
- size: "xs",
10545
- ariaLabel: "\uB4A4\uB85C\uAC00\uAE30",
10546
- onClick: onBack,
10547
- className: "shrink-0"
10548
- }
10549
- ),
10550
- /* @__PURE__ */ jsxRuntime.jsxs(
10551
- "div",
10552
- {
10553
- className: cn(
10554
- "flex min-w-0 flex-1 items-center",
10555
- fix ? "flex-row gap-(--cmp-pageHeader-gap)" : "flex-col items-start gap-(--cmp-pageHeader-plain-title-gap)",
10556
- !hasBack && "pl-(--cmp-pageHeader-title-paddingLeft)"
10557
- ),
10558
- children: [
10559
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "typo-heading-lg min-w-0 truncate text-(--cmp-pageHeader-title-text)", children: title }),
10560
- subText && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "typo-body-md-default min-w-0 truncate text-(--cmp-pageHeader-subtitle-text)", children: subText })
10561
- ]
10562
- }
10563
- ),
10564
- slot && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ml-auto flex shrink-0 items-center", children: slot })
10565
- ]
10566
- }
10567
- );
10568
- }),
10569
- "SPageHeader"
10570
- );
10571
- var MIN_CONTENT_WIDTH = `calc(var(${SLAYOUT_MIN_WIDTH_VAR}, 0px) - 100cqw + 100%)`;
10572
10624
  var BACKGROUND_CLASS = {
10573
10625
  frame: "bg-[var(--sys-color-bg-frame)]",
10574
10626
  neutral: "bg-[var(--sys-color-bg-neutralLight)]",
10575
10627
  screen: "bg-[var(--sys-color-bg-screen)]"
10576
10628
  };
10629
+ var MIN_CONTENT_WIDTH = `calc(var(${SLAYOUT_MIN_WIDTH_VAR}, 0px) - 100cqw + 100%)`;
10577
10630
  var NATIVE_SCROLLBAR_BACKGROUND_CLASS = {
10578
10631
  frame: `${SCROLLBAR_TRACK_BORDER_CLASS} ${SCROLLBAR_TRACK_BG_CLASS}`,
10579
10632
  neutral: `${SCROLLBAR_TRACK_BORDER_CLASS} ${SCROLLBAR_TRACK_BG_WHITE_CLASS}`,
@@ -10720,7 +10773,6 @@ var SPage = /* @__PURE__ */ react.forwardRef(function SPage2({
10720
10773
  scrollEndSpacing = false,
10721
10774
  overlayScrollbar = false,
10722
10775
  contentHeight = "auto",
10723
- header,
10724
10776
  className,
10725
10777
  style,
10726
10778
  children,
@@ -10728,7 +10780,8 @@ var SPage = /* @__PURE__ */ react.forwardRef(function SPage2({
10728
10780
  ...rest
10729
10781
  }, ref) {
10730
10782
  const layout = react.useContext(SLayoutContext);
10731
- const gridStyle = layout?.header === "full" ? { gridArea: "page" } : void 0;
10783
+ const column = react.useContext(SPageColumnContext);
10784
+ const gridStyle = layout?.header === "full" && !column ? { gridArea: "page" } : void 0;
10732
10785
  const scrollRef = react.useRef(null);
10733
10786
  const setMainRef = react.useCallback(
10734
10787
  (node) => {
@@ -10737,10 +10790,10 @@ var SPage = /* @__PURE__ */ react.forwardRef(function SPage2({
10737
10790
  },
10738
10791
  [ref]
10739
10792
  );
10740
- const [scrolled, setScrolled] = react.useState(false);
10793
+ const setColumnScrolled = column?.setScrolled;
10741
10794
  const handleScroll = (e) => {
10742
10795
  onScroll?.(e);
10743
- if (header != null && !header.fix) setScrolled(e.currentTarget.scrollTop > 0);
10796
+ setColumnScrolled?.(e.currentTarget.scrollTop > 0);
10744
10797
  };
10745
10798
  const fill = contentHeight === "fill";
10746
10799
  const main = /* @__PURE__ */ jsxRuntime.jsx(
@@ -10755,69 +10808,86 @@ var SPage = /* @__PURE__ */ react.forwardRef(function SPage2({
10755
10808
  BACKGROUND_CLASS[background],
10756
10809
  className
10757
10810
  ),
10758
- style: header == null && !overlayScrollbar ? { ...gridStyle, ...style } : style,
10811
+ style: !overlayScrollbar ? { ...gridStyle, ...style } : style,
10759
10812
  onScroll: handleScroll,
10760
10813
  ...rest,
10761
10814
  children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn(fill && "h-full"), style: { minWidth: MIN_CONTENT_WIDTH }, children })
10762
10815
  }
10763
10816
  );
10764
- if (!overlayScrollbar) {
10765
- if (header == null) return main;
10766
- return (
10767
- // fix 가 아닌 헤더는 자기 배경이 투명이라(SPageHeader.tsx) 이 배경이 그대로 비친다.
10768
- /* @__PURE__ */ jsxRuntime.jsxs(
10769
- "div",
10770
- {
10771
- className: cn("flex min-h-0 min-w-0 flex-1 flex-col", BACKGROUND_CLASS[background]),
10772
- style: gridStyle,
10773
- children: [
10774
- /* @__PURE__ */ jsxRuntime.jsx(SPageHeader, { ...header, scrolled }),
10775
- main
10776
- ]
10777
- }
10778
- )
10779
- );
10780
- }
10781
- if (header == null) {
10817
+ if (!overlayScrollbar) return main;
10818
+ return /* @__PURE__ */ jsxRuntime.jsxs(
10819
+ "div",
10820
+ {
10821
+ className: cn(
10822
+ "relative min-h-0 min-w-0 flex-1 overflow-hidden",
10823
+ BACKGROUND_CLASS[background]
10824
+ ),
10825
+ style: gridStyle,
10826
+ children: [
10827
+ main,
10828
+ /* @__PURE__ */ jsxRuntime.jsx(SPageOverlayScrollbars, { scrollRef })
10829
+ ]
10830
+ }
10831
+ );
10832
+ });
10833
+
10834
+ // src/components/SPage/page.config.ts
10835
+ var PAGE_BACKGROUNDS = ["frame", "neutral", "screen"];
10836
+ var PAGE_CONTENT_HEIGHTS = ["auto", "fill"];
10837
+ var SPageHeader = /* @__PURE__ */ withDisplayName(
10838
+ react.forwardRef(function SPageHeader2({ title, subText, fix = false, onBack, slot, scrolled, className, style, ...rest }, ref) {
10839
+ const hasBack = onBack != null;
10840
+ const column = react.useContext(SPageColumnContext);
10841
+ const isScrolled = scrolled ?? column?.scrolled ?? false;
10782
10842
  return /* @__PURE__ */ jsxRuntime.jsxs(
10783
- "div",
10843
+ "header",
10784
10844
  {
10845
+ ref,
10785
10846
  className: cn(
10786
- "relative min-h-0 min-w-0 flex-1 overflow-hidden",
10787
- BACKGROUND_CLASS[background]
10847
+ "relative z-10 box-border flex shrink-0 items-center gap-(--cmp-pageHeader-gap) px-(--cmp-pageHeader-paddingX)",
10848
+ // 배경은 두 베리에이션이 같은 흰 면이다 — fix 가 가르는 것은 높이·구분선·서브 텍스트 자리다.
10849
+ // plain 에는 전용 배경 토큰이 없어 시스템 흰 표면을 쓴다(토큰이 생기면 그것으로 갈아끼운다).
10850
+ fix ? "h-(--cmp-pageHeader-bar-height) border-b border-(--cmp-pageHeader-bar-border) bg-(--cmp-pageHeader-bar-bg) py-(--cmp-pageHeader-bar-paddingY)" : cn(
10851
+ // 스크롤되면 그림자를 더해 콘텐츠 위에 뜬 것처럼 보이게 한다.
10852
+ "bg-(--sys-color-bg-frame) pt-(--cmp-pageHeader-plain-paddingTop) pb-(--cmp-pageHeader-plain-paddingBottom)",
10853
+ isScrolled && "shadow-floating"
10854
+ ),
10855
+ className
10788
10856
  ),
10789
- style: gridStyle,
10857
+ style,
10858
+ ...rest,
10790
10859
  children: [
10791
- main,
10792
- /* @__PURE__ */ jsxRuntime.jsx(SPageOverlayScrollbars, { scrollRef })
10860
+ hasBack && /* @__PURE__ */ jsxRuntime.jsx(
10861
+ SGhostButton,
10862
+ {
10863
+ icon: "chevronLeft",
10864
+ size: "xs",
10865
+ ariaLabel: "\uB4A4\uB85C\uAC00\uAE30",
10866
+ onClick: onBack,
10867
+ className: "shrink-0"
10868
+ }
10869
+ ),
10870
+ /* @__PURE__ */ jsxRuntime.jsxs(
10871
+ "div",
10872
+ {
10873
+ className: cn(
10874
+ "flex min-w-0 flex-1 items-center",
10875
+ fix ? "flex-row gap-(--cmp-pageHeader-gap)" : "flex-col items-start gap-(--cmp-pageHeader-plain-title-gap)",
10876
+ !hasBack && "pl-(--cmp-pageHeader-title-paddingLeft)"
10877
+ ),
10878
+ children: [
10879
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "typo-heading-lg min-w-0 truncate text-(--cmp-pageHeader-title-text)", children: title }),
10880
+ subText && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "typo-body-md-default min-w-0 truncate text-(--cmp-pageHeader-subtitle-text)", children: subText })
10881
+ ]
10882
+ }
10883
+ ),
10884
+ slot && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ml-auto flex shrink-0 items-center", children: slot })
10793
10885
  ]
10794
10886
  }
10795
10887
  );
10796
- }
10797
- return (
10798
- // fix 가 아닌 헤더는 자기 배경이 투명이라(SPageHeader.tsx) 이 배경이 그대로 비친다 —
10799
- // 그래서 SPage 의 background 를 main 뿐 아니라 여기에도 칠한다(스크롤바 관련 클래스는
10800
- // 스크롤 요소가 아닌 여기서는 그냥 무시된다). fix 헤더는 자체 흰 배경이 덮으므로 무관하다.
10801
- /* @__PURE__ */ jsxRuntime.jsxs(
10802
- "div",
10803
- {
10804
- className: cn("flex min-h-0 min-w-0 flex-1 flex-col", BACKGROUND_CLASS[background]),
10805
- style: gridStyle,
10806
- children: [
10807
- /* @__PURE__ */ jsxRuntime.jsx(SPageHeader, { ...header, scrolled }),
10808
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative min-h-0 min-w-0 flex-1 overflow-hidden", children: [
10809
- main,
10810
- /* @__PURE__ */ jsxRuntime.jsx(SPageOverlayScrollbars, { scrollRef })
10811
- ] })
10812
- ]
10813
- }
10814
- )
10815
- );
10816
- });
10817
-
10818
- // src/components/SPage/page.config.ts
10819
- var PAGE_BACKGROUNDS = ["frame", "neutral", "screen"];
10820
- var PAGE_CONTENT_HEIGHTS = ["auto", "fill"];
10888
+ }),
10889
+ "SPageHeader"
10890
+ );
10821
10891
 
10822
10892
  // src/components/STabs/tabs.config.ts
10823
10893
  var TAB_MAIN_SIZE = { height: 36, paddingX: 24 };
@@ -17858,10 +17928,10 @@ var useIsMobile = () => {
17858
17928
  react.useEffect(() => {
17859
17929
  if (typeof window.matchMedia !== "function") return;
17860
17930
  const query = window.matchMedia(`(max-width: ${MOBILE_MAX_WIDTH - 1}px)`);
17861
- const sync = () => setIsMobile(query.matches);
17862
- sync();
17863
- query.addEventListener("change", sync);
17864
- return () => query.removeEventListener("change", sync);
17931
+ const sync2 = () => setIsMobile(query.matches);
17932
+ sync2();
17933
+ query.addEventListener("change", sync2);
17934
+ return () => query.removeEventListener("change", sync2);
17865
17935
  }, []);
17866
17936
  return isMobile;
17867
17937
  };
@@ -19361,9 +19431,9 @@ var keywordEntries = (value) => {
19361
19431
  return [];
19362
19432
  };
19363
19433
  var CSV_PASTE_SEPARATOR = /[,\r\n\t]+/;
19364
- var newKeywordEntries = (entries3, candidates) => candidates.reduce((acc, raw) => {
19434
+ var newKeywordEntries = (entries4, candidates) => candidates.reduce((acc, raw) => {
19365
19435
  const next = raw.trim();
19366
- if (next === "" || entries3.includes(next) || acc.includes(next)) return acc;
19436
+ if (next === "" || entries4.includes(next) || acc.includes(next)) return acc;
19367
19437
  return [...acc, next];
19368
19438
  }, []);
19369
19439
  var withKeywordEntries = (field, current, nextEntries) => field.matchModes ? { keywords: nextEntries, mode: isKeywordValue(current) ? current.mode : "contains" } : nextEntries;
@@ -19627,13 +19697,13 @@ var SChipFilter = /* @__PURE__ */ react.forwardRef(function SChipFilter2({
19627
19697
  const field = fields.find((f) => f.key === key);
19628
19698
  if (field == null || field.type !== "keyword" || disabled || field.disabled) return null;
19629
19699
  const current = valuesRef.current[key];
19630
- const entries3 = keywordEntries(current);
19700
+ const entries4 = keywordEntries(current);
19631
19701
  const added = newKeywordEntries(
19632
- entries3,
19702
+ entries4,
19633
19703
  field.input === "csv" ? draft.text.split(CSV_PASTE_SEPARATOR) : [draft.text]
19634
19704
  );
19635
19705
  if (added.length === 0) return null;
19636
- const nextValue = withKeywordEntries(field, current, [...entries3, ...added]);
19706
+ const nextValue = withKeywordEntries(field, current, [...entries4, ...added]);
19637
19707
  const next = { ...valuesRef.current, [key]: nextValue };
19638
19708
  onValueChange?.(next);
19639
19709
  onFilterChange?.({ key, value: nextValue, values: next });
@@ -20654,7 +20724,7 @@ function KeywordEditor({
20654
20724
  onValueCommit,
20655
20725
  onDraftChange
20656
20726
  }) {
20657
- const entries3 = keywordEntries(value);
20727
+ const entries4 = keywordEntries(value);
20658
20728
  const [draftText, setDraftText] = react.useState("");
20659
20729
  const [mode, setMode] = react.useState(
20660
20730
  isKeywordValue(value) ? value.mode : "contains"
@@ -20676,16 +20746,16 @@ function KeywordEditor({
20676
20746
  };
20677
20747
  const changeMode = (next) => {
20678
20748
  setMode(next);
20679
- emit3(entries3, next);
20749
+ emit3(entries4, next);
20680
20750
  };
20681
- const newEntries = (candidates) => newKeywordEntries(entries3, candidates);
20751
+ const newEntries = (candidates) => newKeywordEntries(entries4, candidates);
20682
20752
  const addEntry = (raw) => {
20683
20753
  const added = newEntries([raw]);
20684
20754
  if (added.length === 0) {
20685
20755
  setDraft("");
20686
20756
  return;
20687
20757
  }
20688
- commit([...entries3, ...added]);
20758
+ commit([...entries4, ...added]);
20689
20759
  setDraft("");
20690
20760
  };
20691
20761
  const changeDraft = (next) => {
@@ -20696,7 +20766,7 @@ function KeywordEditor({
20696
20766
  const parts = next.split(",");
20697
20767
  const tail = parts.pop() ?? "";
20698
20768
  const added = newEntries(parts);
20699
- if (added.length > 0) commit([...entries3, ...added]);
20769
+ if (added.length > 0) commit([...entries4, ...added]);
20700
20770
  setDraft(tail);
20701
20771
  };
20702
20772
  const onPaste = (event) => {
@@ -20705,11 +20775,11 @@ function KeywordEditor({
20705
20775
  if (!CSV_PASTE_SEPARATOR.test(text)) return;
20706
20776
  event.preventDefault();
20707
20777
  const added = newEntries((draftText + text).split(CSV_PASTE_SEPARATOR));
20708
- if (added.length > 0) commit([...entries3, ...added]);
20778
+ if (added.length > 0) commit([...entries4, ...added]);
20709
20779
  setDraft("");
20710
20780
  };
20711
20781
  const removeEntry = (entry) => {
20712
- emit3(entries3.filter((e) => e !== entry));
20782
+ emit3(entries4.filter((e) => e !== entry));
20713
20783
  };
20714
20784
  const onKeyDown = (event) => {
20715
20785
  if (event.key === "Enter") {
@@ -20758,12 +20828,12 @@ function KeywordEditor({
20758
20828
  )
20759
20829
  ] })
20760
20830
  ] }),
20761
- entries3.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
20831
+ entries4.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
20762
20832
  "div",
20763
20833
  {
20764
20834
  className: "min-h-0 flex-1 overflow-auto border-t border-solid py-[var(--cmp-chipFilter-listbox-list-paddingY)]",
20765
20835
  style: { borderColor: CHIPFILTER.listboxBorder },
20766
- children: entries3.map((entry) => /* @__PURE__ */ jsxRuntime.jsx(
20836
+ children: entries4.map((entry) => /* @__PURE__ */ jsxRuntime.jsx(
20767
20837
  KeywordEntryRow,
20768
20838
  {
20769
20839
  label: optionLabel(field, entry),
@@ -22569,6 +22639,7 @@ exports.SModal = SModal;
22569
22639
  exports.SModalOutlet = SModalOutlet;
22570
22640
  exports.SNumberInput = SNumberInput;
22571
22641
  exports.SPage = SPage;
22642
+ exports.SPageHeader = SPageHeader;
22572
22643
  exports.SPagination = SPagination;
22573
22644
  exports.SPopover = SPopover;
22574
22645
  exports.SPopup = SPopup;