sellmate-design-system-react 4.3.1 → 5.0.1

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 (37) hide show
  1. package/AGENTS.md +18 -0
  2. package/README.md +2 -2
  3. package/dist/components/SActionModal/README.md +6 -4
  4. package/dist/components/SActionModal/SActionModal.d.ts +3 -12
  5. package/dist/components/SActionModal/index.d.ts +1 -1
  6. package/dist/components/SButton/README.md +2 -6
  7. package/dist/components/SConfirmModal/README.md +1 -1
  8. package/dist/components/SConfirmModal/SConfirmModal.d.ts +1 -1
  9. package/dist/components/SDrawer/README.md +8 -5
  10. package/dist/components/SDrawer/SDrawer.d.ts +14 -11
  11. package/dist/components/SFooter/README.md +40 -0
  12. package/dist/components/SFooter/SFooter.d.ts +36 -0
  13. package/dist/components/SFooter/index.d.ts +1 -0
  14. package/dist/components/SGhostButton/README.md +2 -2
  15. package/dist/components/SGnb/README.md +0 -5
  16. package/dist/components/SGnb/SGnb.d.ts +6 -2
  17. package/dist/components/SGnb/gnb.config.d.ts +2 -0
  18. package/dist/components/SLayout/README.md +1 -6
  19. package/dist/components/SLayout/SLayout.d.ts +39 -17
  20. package/dist/components/SLoadingModal/README.md +1 -1
  21. package/dist/components/SLoadingModal/SLoadingModal.d.ts +1 -1
  22. package/dist/components/SModalContainer/README.md +1 -1
  23. package/dist/components/SModalContainer/SModalContainer.d.ts +6 -2
  24. package/dist/components/SOverlayHeader/README.md +45 -0
  25. package/dist/components/SOverlayHeader/SOverlayHeader.d.ts +28 -0
  26. package/dist/components/SOverlayHeader/index.d.ts +1 -0
  27. package/dist/components/SPage/SPage.d.ts +2 -2
  28. package/dist/components/SPopup/README.md +2 -2
  29. package/dist/index.cjs +405 -251
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.js +405 -251
  32. package/dist/index.js.map +1 -1
  33. package/dist/lib/isomorphic-layout-effect.d.ts +8 -0
  34. package/dist/llms-full.txt +109 -23
  35. package/dist/llms.txt +18 -0
  36. package/dist/styles.css +27 -17
  37. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -6015,6 +6015,53 @@ function SPortal({
6015
6015
  }
6016
6016
  );
6017
6017
  }
6018
+ function SFooter({
6019
+ bg = "white",
6020
+ left,
6021
+ button,
6022
+ children,
6023
+ className,
6024
+ leftClassName,
6025
+ style
6026
+ }) {
6027
+ const isGrey = bg === "grey";
6028
+ const hasSlot = left != null || children != null;
6029
+ const hasButton = button != null;
6030
+ return /* @__PURE__ */ jsxRuntime.jsxs(
6031
+ "footer",
6032
+ {
6033
+ className: cn(
6034
+ "relative flex w-full flex-shrink-0 items-center justify-end gap-[var(--cmp-overlay-footer-gap)] overflow-hidden px-[var(--cmp-overlay-footer-paddingX)] py-[var(--cmp-overlay-footer-paddingY)] shadow-[var(--shadow-spread-md)]",
6035
+ isGrey ? "bg-[var(--cmp-overlay-footer-bg-neutral)]" : "bg-[var(--cmp-overlay-footer-bg-default)]",
6036
+ className
6037
+ ),
6038
+ style,
6039
+ children: [
6040
+ hasSlot && /* @__PURE__ */ jsxRuntime.jsxs(
6041
+ "div",
6042
+ {
6043
+ className: cn("flex min-w-px flex-[1_0_0] items-center justify-between", leftClassName),
6044
+ children: [
6045
+ left,
6046
+ children
6047
+ ]
6048
+ }
6049
+ ),
6050
+ hasButton && /* @__PURE__ */ jsxRuntime.jsx(
6051
+ SButton,
6052
+ {
6053
+ color: button.color ?? "primary",
6054
+ outline: button.outline,
6055
+ size: button.size ?? "md",
6056
+ disabled: button.disabled,
6057
+ onClick: () => button.onClick?.(),
6058
+ label: button.label ?? "\uD655\uC778"
6059
+ }
6060
+ )
6061
+ ]
6062
+ }
6063
+ );
6064
+ }
6018
6065
  var SPopup = /* @__PURE__ */ react.forwardRef(function SPopup2({
6019
6066
  popupTitle = "",
6020
6067
  type = "default",
@@ -6063,29 +6110,7 @@ var SPopup = /* @__PURE__ */ react.forwardRef(function SPopup2({
6063
6110
  children
6064
6111
  }
6065
6112
  ),
6066
- useFooter && /* @__PURE__ */ jsxRuntime.jsxs(
6067
- "footer",
6068
- {
6069
- className: "relative z-[1] flex h-[52px] w-full flex-shrink-0 items-center gap-[var(--cmp-overlay-footer-gap)] px-[var(--cmp-overlay-footer-paddingX)] py-[var(--cmp-overlay-footer-paddingY)]",
6070
- style: {
6071
- background: "var(--cmp-overlay-footer-bg-default)",
6072
- boxShadow: "var(--shadow-spread-md)"
6073
- },
6074
- children: [
6075
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex min-w-0 flex-1 items-center", children: footerLeft }),
6076
- /* @__PURE__ */ jsxRuntime.jsx(
6077
- SButton,
6078
- {
6079
- color: submitButton?.color ?? "primary",
6080
- outline: submitButton?.outline,
6081
- size: submitButton?.size ?? "md",
6082
- onClick: onSubmit,
6083
- label: submitButton?.label ?? "\uD655\uC778"
6084
- }
6085
- )
6086
- ]
6087
- }
6088
- )
6113
+ useFooter && /* @__PURE__ */ jsxRuntime.jsx(SFooter, { left: footerLeft, button: { ...submitButton, onClick: onSubmit } })
6089
6114
  ]
6090
6115
  }
6091
6116
  );
@@ -6240,7 +6265,7 @@ function SModalContainer({
6240
6265
  ariaTitle = "\uBAA8\uB2EC",
6241
6266
  showClose = false,
6242
6267
  onClose,
6243
- persistent = false,
6268
+ persistent = true,
6244
6269
  width,
6245
6270
  height,
6246
6271
  children,
@@ -6468,7 +6493,7 @@ function SConfirmModal({
6468
6493
  )) })
6469
6494
  ] })
6470
6495
  ] }),
6471
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-shrink-0 gap-[8px]", children: [
6496
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-shrink-0 gap-[var(--cmp-modal-confirm-button-gap)]", children: [
6472
6497
  subButtonLabel && /* @__PURE__ */ jsxRuntime.jsx(
6473
6498
  SButton,
6474
6499
  {
@@ -6499,6 +6524,64 @@ function SConfirmModal({
6499
6524
  }
6500
6525
  );
6501
6526
  }
6527
+ function SOverlayHeader({
6528
+ title,
6529
+ description,
6530
+ showClose = false,
6531
+ onClose,
6532
+ reserveCloseSpace = false,
6533
+ className,
6534
+ titleClassName,
6535
+ descriptionClassName,
6536
+ style
6537
+ }) {
6538
+ const hasDescription = description != null && description !== "";
6539
+ return /* @__PURE__ */ jsxRuntime.jsxs(
6540
+ "header",
6541
+ {
6542
+ className: cn(
6543
+ "relative flex w-full flex-shrink-0 items-center",
6544
+ "gap-[var(--cmp-overlay-header-gap)] bg-[var(--cmp-overlay-header-bg)] py-[var(--cmp-overlay-header-paddingY)] pl-[var(--cmp-overlay-header-paddingX)]",
6545
+ showClose || reserveCloseSpace ? "pr-[calc(var(--cmp-overlay-header-paddingX)+20px)]" : "pr-[var(--cmp-overlay-header-paddingX)]",
6546
+ className
6547
+ ),
6548
+ style,
6549
+ children: [
6550
+ /* @__PURE__ */ jsxRuntime.jsx(
6551
+ "h2",
6552
+ {
6553
+ className: cn(
6554
+ "m-0 overflow-hidden text-ellipsis whitespace-nowrap",
6555
+ "text-[16px] font-bold leading-[26px]",
6556
+ titleClassName
6557
+ ),
6558
+ style: { color: "var(--cmp-overlay-header-title-color)" },
6559
+ children: title
6560
+ }
6561
+ ),
6562
+ hasDescription && /* @__PURE__ */ jsxRuntime.jsx(
6563
+ "div",
6564
+ {
6565
+ className: cn(
6566
+ "flex min-w-0 flex-1 items-center gap-sd-2 text-[12px] font-normal leading-[20px] text-[color:var(--color-grey-65)]",
6567
+ descriptionClassName
6568
+ ),
6569
+ children: description
6570
+ }
6571
+ ),
6572
+ showClose && /* @__PURE__ */ jsxRuntime.jsx(
6573
+ SGhostButton,
6574
+ {
6575
+ icon: "close",
6576
+ ariaLabel: "\uB2EB\uAE30",
6577
+ onClick: () => onClose?.(),
6578
+ className: "absolute right-[12px] top-[12px]"
6579
+ }
6580
+ )
6581
+ ]
6582
+ }
6583
+ );
6584
+ }
6502
6585
  function SActionModal({
6503
6586
  open,
6504
6587
  onOpenChange,
@@ -6525,35 +6608,9 @@ function SActionModal({
6525
6608
  height,
6526
6609
  className: "w-fit min-w-[min(480px,calc(100dvw-48px))] min-h-[min(320px,calc(100dvh-48px))]",
6527
6610
  children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-0 flex-auto flex-col", children: [
6528
- /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex flex-shrink-0 items-center gap-[var(--cmp-overlay-header-gap)] py-[var(--cmp-overlay-header-paddingY)] pl-[var(--cmp-overlay-header-paddingX)] pr-[calc(var(--cmp-overlay-header-paddingX)+20px)]", children: [
6529
- /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "whitespace-nowrap text-[16px] font-bold leading-[26px] text-[color:var(--cmp-overlay-header-title-color)]", children: modalTitle }),
6530
- description != null && description !== "" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex min-w-0 flex-1 items-center gap-sd-2 text-[12px] font-normal leading-[20px] text-[color:var(--color-grey-65)]", children: description })
6531
- ] }),
6611
+ /* @__PURE__ */ jsxRuntime.jsx(SOverlayHeader, { title: modalTitle, description, reserveCloseSpace: true }),
6532
6612
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1 overflow-auto px-[var(--cmp-modal-action-body-paddingX)] pb-[var(--cmp-modal-action-body-paddingBottom)]", children }),
6533
- (button || footerLeft) && /* @__PURE__ */ jsxRuntime.jsxs(
6534
- "footer",
6535
- {
6536
- className: "flex flex-shrink-0 items-center justify-end gap-[var(--cmp-modal-action-footer-gap)] border-t border-solid px-[var(--cmp-modal-action-footer-paddingX)] py-[var(--cmp-modal-action-footer-paddingY)]",
6537
- style: {
6538
- background: "var(--cmp-modal-action-footer-bg)",
6539
- borderTopColor: "var(--cmp-modal-action-footer-border)"
6540
- },
6541
- children: [
6542
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex min-w-0 flex-1 items-center gap-sd-2", children: footerLeft }),
6543
- button && /* @__PURE__ */ jsxRuntime.jsx(
6544
- SButton,
6545
- {
6546
- color: button.color ?? "primary",
6547
- outline: button.outline,
6548
- size: button.size ?? "md",
6549
- disabled: button.disabled,
6550
- onClick: () => button.onClick?.(),
6551
- label: button.label ?? "\uD655\uC778"
6552
- }
6553
- )
6554
- ]
6555
- }
6556
- )
6613
+ (button || footerLeft) && /* @__PURE__ */ jsxRuntime.jsx(SFooter, { bg: "grey", left: footerLeft, button })
6557
6614
  ] })
6558
6615
  }
6559
6616
  );
@@ -6622,13 +6679,43 @@ function SLoadingModal({
6622
6679
  );
6623
6680
  }
6624
6681
  var CLOSE_ANIMATION_DURATION2 = 300;
6682
+ function toPx(value, percentBase) {
6683
+ if (typeof value === "number") return value;
6684
+ if (typeof value !== "string") return void 0;
6685
+ const normalized = value.trim();
6686
+ if (normalized.endsWith("px")) {
6687
+ const numeric = Number(normalized.slice(0, -2));
6688
+ return Number.isFinite(numeric) ? numeric : void 0;
6689
+ }
6690
+ if (normalized.endsWith("%")) {
6691
+ const numeric = Number(normalized.slice(0, -1));
6692
+ return Number.isFinite(numeric) ? percentBase * numeric / 100 : void 0;
6693
+ }
6694
+ if (normalized.endsWith("vw")) {
6695
+ const numeric = Number(normalized.slice(0, -2));
6696
+ return Number.isFinite(numeric) ? window.innerWidth * numeric / 100 : void 0;
6697
+ }
6698
+ if (normalized.endsWith("vh")) {
6699
+ const numeric = Number(normalized.slice(0, -2));
6700
+ return Number.isFinite(numeric) ? window.innerHeight * numeric / 100 : void 0;
6701
+ }
6702
+ return void 0;
6703
+ }
6704
+ function clamp3(value, min, max) {
6705
+ const minValue = min ?? Number.NEGATIVE_INFINITY;
6706
+ const maxValue = max ?? Number.POSITIVE_INFINITY;
6707
+ return Math.min(Math.max(value, minValue), maxValue);
6708
+ }
6625
6709
  function SDrawer({
6626
6710
  open,
6627
6711
  onOpenChange,
6628
6712
  onClose,
6629
- persistent = false,
6713
+ persistent = true,
6630
6714
  title = "",
6631
6715
  width = 572,
6716
+ resizable = false,
6717
+ minWidth,
6718
+ maxWidth,
6632
6719
  footerLeft,
6633
6720
  button,
6634
6721
  children,
@@ -6642,7 +6729,13 @@ function SDrawer({
6642
6729
  const backdropPointerDownRef = react.useRef(false);
6643
6730
  const contentRef = react.useRef(null);
6644
6731
  const stackRegistrationRef = react.useRef(null);
6732
+ const resizeCleanupRef = react.useRef(null);
6733
+ const resizeStateRef = react.useRef(null);
6645
6734
  const [layerIndex, setLayerIndex] = react.useState(null);
6735
+ const [currentWidth, setCurrentWidth] = react.useState(width);
6736
+ react.useEffect(() => {
6737
+ setCurrentWidth(width);
6738
+ }, [width]);
6646
6739
  react.useEffect(() => {
6647
6740
  if (open) {
6648
6741
  setRendered(true);
@@ -6652,6 +6745,9 @@ function SDrawer({
6652
6745
  const timer = window.setTimeout(() => setRendered(false), CLOSE_ANIMATION_DURATION2);
6653
6746
  return () => window.clearTimeout(timer);
6654
6747
  }, [open, rendered]);
6748
+ react.useEffect(() => {
6749
+ return () => resizeCleanupRef.current?.();
6750
+ }, []);
6655
6751
  react.useEffect(() => {
6656
6752
  if (!open) {
6657
6753
  setEntered(false);
@@ -6693,6 +6789,40 @@ function SDrawer({
6693
6789
  }
6694
6790
  onOpenChange?.(false);
6695
6791
  };
6792
+ const handleResizeStart = (event) => {
6793
+ event.preventDefault();
6794
+ event.stopPropagation();
6795
+ const panel = contentRef.current?.querySelector("[data-s-drawer-panel]");
6796
+ if (!panel) return;
6797
+ resizeStateRef.current = {
6798
+ startX: event.clientX,
6799
+ startWidth: panel.getBoundingClientRect().width,
6800
+ minWidth: toPx(minWidth, window.innerWidth),
6801
+ maxWidth: toPx(maxWidth, window.innerWidth)
6802
+ };
6803
+ const onMove = (moveEvent) => {
6804
+ const resizeState = resizeStateRef.current;
6805
+ if (!resizeState) return;
6806
+ const nextWidth = resizeState.startWidth + resizeState.startX - moveEvent.clientX;
6807
+ setCurrentWidth(clamp3(nextWidth, resizeState.minWidth, resizeState.maxWidth));
6808
+ };
6809
+ resizeCleanupRef.current?.();
6810
+ const previousCursor = document.body.style.cursor;
6811
+ const previousUserSelect = document.body.style.userSelect;
6812
+ const cleanupResize = () => {
6813
+ resizeStateRef.current = null;
6814
+ document.body.style.cursor = previousCursor;
6815
+ document.body.style.userSelect = previousUserSelect;
6816
+ document.removeEventListener("mousemove", onMove);
6817
+ document.removeEventListener("mouseup", cleanupResize);
6818
+ resizeCleanupRef.current = null;
6819
+ };
6820
+ resizeCleanupRef.current = cleanupResize;
6821
+ document.body.style.cursor = "col-resize";
6822
+ document.body.style.userSelect = "none";
6823
+ document.addEventListener("mousemove", onMove);
6824
+ document.addEventListener("mouseup", cleanupResize);
6825
+ };
6696
6826
  const modalHost = getModalHost();
6697
6827
  return /* @__PURE__ */ jsxRuntime.jsx(RDialog__namespace.Root, { open, onOpenChange, children: /* @__PURE__ */ jsxRuntime.jsx(RDialog__namespace.Portal, { container: modalHost, forceMount: rendered ? true : void 0, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pointer-events-none absolute inset-0", style: { zIndex: layerIndex ?? 0 }, children: [
6698
6828
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -6722,7 +6852,7 @@ function SDrawer({
6722
6852
  {
6723
6853
  forceMount: rendered ? true : void 0,
6724
6854
  ref: contentRef,
6725
- className: "pointer-events-auto absolute inset-y-0 right-0 w-fit max-w-full outline-none",
6855
+ className: "pointer-events-none absolute inset-y-0 right-0 flex w-fit max-w-full justify-end outline-none",
6726
6856
  onOpenAutoFocus: (event) => {
6727
6857
  event.preventDefault();
6728
6858
  contentRef.current?.focus({ preventScroll: true });
@@ -6740,42 +6870,35 @@ function SDrawer({
6740
6870
  shaking && "animate-drawer-shake",
6741
6871
  className
6742
6872
  ),
6873
+ "data-s-drawer-panel": true,
6743
6874
  "data-state": open && entered ? "open" : "closed",
6744
- style: { width, ...style },
6875
+ style: { width: currentWidth, minWidth, maxWidth, ...style },
6745
6876
  onAnimationEnd: (event) => {
6746
6877
  if (event.animationName === "drawer-shake") setShaking(false);
6747
6878
  },
6748
6879
  children: [
6749
- /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "relative flex flex-shrink-0 items-center gap-[var(--cmp-overlay-header-gap)] bg-[var(--cmp-overlay-header-bg)] px-[var(--cmp-overlay-header-paddingX)] py-[var(--cmp-overlay-header-paddingY)]", children: [
6750
- /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "m-0 whitespace-nowrap text-[16px] font-bold leading-[26px] text-[color:var(--cmp-overlay-header-title-color)]", children: title }),
6751
- /* @__PURE__ */ jsxRuntime.jsx(
6752
- SGhostButton,
6753
- {
6754
- icon: "close",
6755
- ariaLabel: "\uB2EB\uAE30",
6756
- onClick: () => {
6757
- onClose?.();
6758
- onOpenChange?.(false);
6759
- },
6760
- className: "absolute right-[12px] top-[12px]"
6880
+ resizable && /* @__PURE__ */ jsxRuntime.jsx(
6881
+ "button",
6882
+ {
6883
+ type: "button",
6884
+ "aria-label": "Drawer \uB108\uBE44 \uC870\uC808",
6885
+ className: "absolute inset-y-0 left-0 z-[1] w-[8px] -translate-x-1/2 cursor-col-resize touch-none border-0 bg-transparent p-0",
6886
+ onMouseDown: handleResizeStart
6887
+ }
6888
+ ),
6889
+ /* @__PURE__ */ jsxRuntime.jsx(
6890
+ SOverlayHeader,
6891
+ {
6892
+ title,
6893
+ showClose: true,
6894
+ onClose: () => {
6895
+ onClose?.();
6896
+ onOpenChange?.(false);
6761
6897
  }
6762
- )
6763
- ] }),
6898
+ }
6899
+ ),
6764
6900
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1 overflow-auto border-t border-solid border-[color:var(--cmp-drawer-body-border)]", children: /* @__PURE__ */ jsxRuntime.jsx(InsideModalProvider, { value: true, children: /* @__PURE__ */ jsxRuntime.jsx(PortalContainerProvider, { value: portalContainer, children }) }) }),
6765
- (button || footerLeft) && /* @__PURE__ */ jsxRuntime.jsxs("footer", { className: "flex flex-shrink-0 items-center justify-end gap-[var(--cmp-overlay-footer-gap)] bg-[var(--cmp-overlay-footer-bg-default)] px-[var(--cmp-overlay-footer-paddingX)] py-[var(--cmp-overlay-footer-paddingY)] shadow-[var(--shadow-overlay)]", children: [
6766
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex min-w-0 flex-1 items-center justify-between", children: footerLeft }),
6767
- button && /* @__PURE__ */ jsxRuntime.jsx(
6768
- SButton,
6769
- {
6770
- color: button.color ?? "primary",
6771
- outline: button.outline,
6772
- size: button.size ?? "md",
6773
- disabled: button.disabled,
6774
- onClick: () => button.onClick?.(),
6775
- label: button.label ?? "\uD655\uC778"
6776
- }
6777
- )
6778
- ] })
6901
+ (button || footerLeft) && /* @__PURE__ */ jsxRuntime.jsx(SFooter, { left: footerLeft, leftClassName: "justify-between", button })
6779
6902
  ]
6780
6903
  }
6781
6904
  ),
@@ -7030,6 +7153,80 @@ var SModal = {
7030
7153
  return mount((refImpl) => /* @__PURE__ */ jsxRuntime.jsx(CreateMount, { options, refImpl }));
7031
7154
  }
7032
7155
  };
7156
+ var useIsomorphicLayoutEffect = typeof window !== "undefined" ? react.useLayoutEffect : react.useEffect;
7157
+ var isSameNav = (a, b) => a === b || !!a && !!b && a.columnWidth === b.columnWidth && a.topHeight === b.topHeight && a.foldMs === b.foldMs && a.requireFullHeader === b.requireFullHeader;
7158
+ var SLayoutContext = /* @__PURE__ */ react.createContext(null);
7159
+ function useSLayout() {
7160
+ const ctx = react.useContext(SLayoutContext);
7161
+ if (!ctx) throw new Error("useSLayout \uC740 <SLayout> \uC548\uC5D0\uC11C\uB9CC \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
7162
+ return ctx;
7163
+ }
7164
+ var SLayout = /* @__PURE__ */ react.forwardRef(function SLayout2({
7165
+ type = "box",
7166
+ header = "fix",
7167
+ useRail = false,
7168
+ folded,
7169
+ defaultFolded = false,
7170
+ onFoldedChange,
7171
+ className,
7172
+ style,
7173
+ children,
7174
+ ...rest
7175
+ }, ref) {
7176
+ const [uncontrolledFolded, setUncontrolledFolded] = react.useState(defaultFolded);
7177
+ const isControlled = folded !== void 0;
7178
+ const currentFolded = isControlled ? folded : uncontrolledFolded;
7179
+ const setFolded = react.useCallback(
7180
+ (next) => {
7181
+ if (!isControlled) setUncontrolledFolded(next);
7182
+ onFoldedChange?.(next);
7183
+ },
7184
+ [isControlled, onFoldedChange]
7185
+ );
7186
+ const [nav, setNav] = react.useState(null);
7187
+ const registerNav = react.useCallback(
7188
+ (next) => setNav((prev) => isSameNav(prev, next) ? prev : next),
7189
+ []
7190
+ );
7191
+ const effectiveHeader = header === "full" || nav?.requireFullHeader ? "full" : "fix";
7192
+ const ctx = react.useMemo(
7193
+ () => ({
7194
+ type,
7195
+ header: effectiveHeader,
7196
+ useRail,
7197
+ folded: currentFolded,
7198
+ setFolded,
7199
+ registerNav
7200
+ }),
7201
+ [type, effectiveHeader, useRail, currentFolded, setFolded, registerNav]
7202
+ );
7203
+ const isFullHeader = effectiveHeader === "full";
7204
+ const frameStyle = isFullHeader ? {
7205
+ // 자식이 좌표가 아니라 이름으로 자리를 잡게 한다 — 열·행이 늘어도 자식을 고칠 일이 없다.
7206
+ gridTemplateAreas: '"header header" "nav page"',
7207
+ gridTemplateColumns: `${currentFolded ? 0 : nav?.columnWidth ?? 0}px 1fr`,
7208
+ gridTemplateRows: `${nav?.topHeight ?? "0px"} 1fr`,
7209
+ // 메뉴가 미끄러지는 동안 페이지 열도 같은 길이로 따라와야 한 덩어리로 읽힌다.
7210
+ // (fix 는 SGnb 가 제 폭을 직접 전환하므로 프레임이 손댈 게 없다)
7211
+ transition: `grid-template-columns ${nav?.foldMs ?? 0}ms ease-out`
7212
+ } : void 0;
7213
+ return /* @__PURE__ */ jsxRuntime.jsx(SLayoutContext.Provider, { value: ctx, children: /* @__PURE__ */ jsxRuntime.jsx(
7214
+ "div",
7215
+ {
7216
+ ref,
7217
+ "data-type": type,
7218
+ "data-header": effectiveHeader,
7219
+ className: cn(
7220
+ "box-border h-full overflow-hidden",
7221
+ isFullHeader ? "grid" : "flex flex-row",
7222
+ className
7223
+ ),
7224
+ style: { ...frameStyle, ...style },
7225
+ ...rest,
7226
+ children
7227
+ }
7228
+ ) });
7229
+ });
7033
7230
 
7034
7231
  // src/components/SGnb/gnb.config.ts
7035
7232
  var gnb = (p) => `var(--cmp-gnb-${p})`;
@@ -7162,6 +7359,21 @@ function findGnbRailValue(items, value) {
7162
7359
  if (items.some((item) => item.value === value)) return value;
7163
7360
  return findGnbAncestors(items, value)[0];
7164
7361
  }
7362
+ function findGnbSiblingValues(items, value) {
7363
+ const walk = (list) => {
7364
+ if (list.some((item) => item.value === value)) {
7365
+ return list.filter((item) => item.value !== value).map((item) => item.value);
7366
+ }
7367
+ for (const item of list) {
7368
+ if (item.children) {
7369
+ const found = walk(item.children);
7370
+ if (found) return found;
7371
+ }
7372
+ }
7373
+ return null;
7374
+ };
7375
+ return walk(items) ?? [];
7376
+ }
7165
7377
  function findGnbAncestors(items, value) {
7166
7378
  const walk = (list, trail) => {
7167
7379
  for (const item of list) {
@@ -7175,79 +7387,6 @@ function findGnbAncestors(items, value) {
7175
7387
  };
7176
7388
  return walk(items, []) ?? [];
7177
7389
  }
7178
- var SLayoutContext = /* @__PURE__ */ react.createContext(null);
7179
- function useSLayout() {
7180
- const ctx = react.useContext(SLayoutContext);
7181
- if (!ctx) throw new Error("useSLayout \uC740 <SLayout> \uC548\uC5D0\uC11C\uB9CC \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
7182
- return ctx;
7183
- }
7184
- var SLayout = /* @__PURE__ */ react.forwardRef(function SLayout2({
7185
- type = "box",
7186
- header = "fix",
7187
- useRail = false,
7188
- folded,
7189
- defaultFolded = false,
7190
- onFoldedChange,
7191
- className,
7192
- style,
7193
- children,
7194
- ...rest
7195
- }, ref) {
7196
- const [uncontrolledFolded, setUncontrolledFolded] = react.useState(defaultFolded);
7197
- const isControlled = folded !== void 0;
7198
- const currentFolded = isControlled ? folded : uncontrolledFolded;
7199
- const setFolded = react.useCallback(
7200
- (next) => {
7201
- if (!isControlled) setUncontrolledFolded(next);
7202
- onFoldedChange?.(next);
7203
- },
7204
- [isControlled, onFoldedChange]
7205
- );
7206
- const [gnbState, setGnbState] = react.useState({ forceFull: false, menuVisible: true });
7207
- const reportGnbState = react.useCallback(
7208
- (next) => setGnbState(
7209
- (prev) => prev.forceFull === next.forceFull && prev.menuVisible === next.menuVisible ? prev : next
7210
- ),
7211
- []
7212
- );
7213
- const effectiveHeader = header === "full" || gnbState.forceFull ? "full" : "fix";
7214
- const ctx = react.useMemo(
7215
- () => ({
7216
- type,
7217
- header: effectiveHeader,
7218
- useRail,
7219
- folded: currentFolded,
7220
- setFolded,
7221
- reportGnbState
7222
- }),
7223
- [type, effectiveHeader, useRail, currentFolded, setFolded, reportGnbState]
7224
- );
7225
- const isFullHeader = effectiveHeader === "full";
7226
- const gnbColumnWidth = (useRail ? GNB_RAIL_WIDTH : 0) + (gnbState.menuVisible ? GNB_MENU_WIDTH : 0);
7227
- const frameStyle = isFullHeader ? {
7228
- gridTemplateColumns: `${currentFolded ? 0 : gnbColumnWidth}px 1fr`,
7229
- gridTemplateRows: `${GNB_HEADER[effectiveHeader].topHeight} 1fr`,
7230
- // 메뉴가 미끄러지는 동안 페이지 열도 같은 길이로 따라와야 한 덩어리로 읽힌다.
7231
- // (fix 는 SGnb 가 제 폭을 직접 전환하므로 프레임이 손댈 게 없다)
7232
- transition: `grid-template-columns ${GNB_FOLD_MS}ms ease-out`
7233
- } : void 0;
7234
- return /* @__PURE__ */ jsxRuntime.jsx(SLayoutContext.Provider, { value: ctx, children: /* @__PURE__ */ jsxRuntime.jsx(
7235
- "div",
7236
- {
7237
- ref,
7238
- "data-type": type,
7239
- "data-header": effectiveHeader,
7240
- className: cn(
7241
- "box-border h-full overflow-hidden",
7242
- isFullHeader ? "grid" : "flex flex-row",
7243
- className
7244
- ),
7245
- style: { ...frameStyle, ...style },
7246
- ...rest,
7247
- children
7248
- }
7249
- ) });
7250
- });
7251
7390
  var DEPTH_PADDING_LEFT = (M, depth) => depth === 1 ? M.depth1PaddingLeft : depth === 2 ? M.depth2PaddingLeft : M.depth3PaddingLeft;
7252
7391
  var SGnb = /* @__PURE__ */ react.forwardRef(function SGnb2({
7253
7392
  type: typeProp,
@@ -7309,8 +7448,12 @@ var SGnb = /* @__PURE__ */ react.forwardRef(function SGnb2({
7309
7448
  const toggleExpand = (key) => {
7310
7449
  setExpandedKeys((prev) => {
7311
7450
  const next = new Set(prev);
7312
- if (next.has(key)) next.delete(key);
7313
- else next.add(key);
7451
+ if (next.has(key)) {
7452
+ next.delete(key);
7453
+ } else {
7454
+ findGnbSiblingValues(items, key).forEach((sibling) => next.delete(sibling));
7455
+ next.add(key);
7456
+ }
7314
7457
  return next;
7315
7458
  });
7316
7459
  };
@@ -7481,16 +7624,30 @@ var SGnb = /* @__PURE__ */ react.forwardRef(function SGnb2({
7481
7624
  const menuItems = useRail ? items.find((item) => item.value === activeRail)?.children ?? [] : items;
7482
7625
  const showMenu = !useRail || menuItems.length > 0;
7483
7626
  const bodyWidth = (useRail ? GNB_RAIL_WIDTH : 0) + (showMenu ? GNB_MENU_WIDTH : 0);
7484
- react.useEffect(() => {
7485
- layout?.reportGnbState({ forceFull: railHasLeaf, menuVisible: showMenu });
7486
- }, [layout, railHasLeaf, showMenu]);
7627
+ const registerNav = layout?.registerNav;
7628
+ useIsomorphicLayoutEffect(() => {
7629
+ if (!registerNav) return;
7630
+ registerNav({
7631
+ columnWidth: bodyWidth,
7632
+ topHeight: H.topHeight,
7633
+ foldMs: GNB_FOLD_MS,
7634
+ requireFullHeader: railHasLeaf
7635
+ });
7636
+ return () => registerNav(null);
7637
+ }, [registerNav, bodyWidth, H.topHeight, railHasLeaf]);
7487
7638
  const railFolded = folded && header === "fix";
7488
- const rootWidth = header === "full" ? "100%" : folded ? GNB_COMMON.foldWidth : bodyWidth;
7639
+ const isFull = header === "full";
7640
+ const isFullFrame = isFull && !!layout;
7489
7641
  const rightBorder = color === "light" ? `1px solid ${C.topBorder}` : void 0;
7490
- const rootStyle = {
7491
- width: rootWidth,
7642
+ const rootStyle = isFullFrame ? { display: "contents", ...style } : isFull ? (
7643
+ // 단독 full — 상단바가 전폭이어야 하므로 루트도 전폭이고, 배경·선은 상단바·메뉴가 각자 그린다.
7644
+ { width: "100%", ...style }
7645
+ ) : {
7646
+ // fix 를 닫으면 48px 레일로 좁혀진다.
7647
+ width: folded ? GNB_COMMON.foldWidth : bodyWidth,
7492
7648
  transition: `width ${GNB_FOLD_MS}ms ease-out`,
7493
- ...header === "full" ? { gridArea: "1 / 1 / -1 / -1", pointerEvents: "none" } : { backgroundColor: C.panelBg, borderRight: rightBorder },
7649
+ backgroundColor: C.panelBg,
7650
+ borderRight: rightBorder,
7494
7651
  ...style
7495
7652
  };
7496
7653
  const hideAfterSlide = (hidden) => `visibility 0s linear ${hidden ? `${GNB_FOLD_MS}ms` : "0s"}`;
@@ -7563,7 +7720,11 @@ var SGnb = /* @__PURE__ */ react.forwardRef(function SGnb2({
7563
7720
  "div",
7564
7721
  {
7565
7722
  ref,
7566
- className: cn("box-border flex h-full shrink-0 flex-col overflow-hidden", className),
7723
+ className: cn(
7724
+ "box-border",
7725
+ !isFullFrame && "flex h-full shrink-0 flex-col overflow-hidden",
7726
+ className
7727
+ ),
7567
7728
  style: rootStyle,
7568
7729
  "data-type": type,
7569
7730
  "data-header": header,
@@ -7575,6 +7736,7 @@ var SGnb = /* @__PURE__ */ react.forwardRef(function SGnb2({
7575
7736
  {
7576
7737
  className: "relative box-border flex shrink-0 items-center overflow-hidden",
7577
7738
  style: {
7739
+ ...isFullFrame ? { gridArea: "header" } : null,
7578
7740
  height: H.topHeight,
7579
7741
  // 간격은 컨테이너 gap 대신 각 요소 margin 으로 준다 — 순서가 header 마다 달라
7580
7742
  // 이웃 쌍마다 값이 다르기 때문(위 launcherButton/logoBox 주석).
@@ -7582,7 +7744,6 @@ var SGnb = /* @__PURE__ */ react.forwardRef(function SGnb2({
7582
7744
  padding: `0 ${railFolded ? GNB_RAIL.padding : H.topPaddingX}`,
7583
7745
  borderBottom: `1px solid ${C.topBorder}`,
7584
7746
  backgroundColor: C.panelBg,
7585
- pointerEvents: "auto",
7586
7747
  transition: `padding ${GNB_FOLD_MS}ms ease-out`,
7587
7748
  // 아이콘 버튼은 자체 ghostButton 토큰을 쓰므로, 이 색은 logo 슬롯 상속용
7588
7749
  color: C.itemColorDefault
@@ -7599,94 +7760,87 @@ var SGnb = /* @__PURE__ */ react.forwardRef(function SGnb2({
7599
7760
  ] })
7600
7761
  }
7601
7762
  ),
7602
- /* @__PURE__ */ jsxRuntime.jsxs(
7763
+ /* @__PURE__ */ jsxRuntime.jsx(
7603
7764
  "div",
7604
7765
  {
7605
- className: "flex flex-row",
7606
- style: {
7607
- width: bodyWidth,
7608
- alignSelf: "flex-start",
7609
- flexGrow: 1,
7610
- flexShrink: 1,
7611
- flexBasis: "auto",
7612
- minHeight: 0,
7613
- transform: folded ? "translateX(-100%)" : "none",
7614
- visibility: folded ? "hidden" : "visible",
7615
- transition: `transform ${GNB_FOLD_MS}ms ease-out, ${hideAfterSlide(folded)}`
7616
- },
7617
- children: [
7618
- useRail && // 레일 컬럼 — 스크롤되는 아이템 목록(nav)과 하단 고정 footer 를 한 컬럼에 담는다.
7619
- // footer 는 내비게이션이 아니므로 nav(landmark) 밖에 두고, 컬럼 폭·구분선·배경은 이 래퍼가
7620
- // 전체 높이에 걸쳐 책임진다(그래야 footer 자리까지 배경·오른쪽 선이 이어진다).
7621
- /* @__PURE__ */ jsxRuntime.jsxs(
7622
- "div",
7623
- {
7624
- className: "flex shrink-0 flex-col overflow-hidden",
7625
- style: {
7626
- width: GNB_RAIL_WIDTH,
7627
- borderRight: `1px solid ${C.topBorder}`,
7628
- backgroundColor: C.panelBg,
7629
- pointerEvents: "auto"
7630
- },
7631
- children: [
7632
- /* @__PURE__ */ jsxRuntime.jsx(
7633
- "nav",
7634
- {
7635
- "aria-label": `${ariaLabel} rail`,
7636
- className: cn(
7637
- "flex min-h-0 flex-1 flex-col overflow-x-hidden overflow-y-auto",
7638
- SCROLLBAR_HIDDEN_CLASS
7766
+ className: cn("overflow-hidden", !isFullFrame && "min-h-0 flex-1"),
7767
+ style: isFullFrame ? { gridArea: "nav" } : void 0,
7768
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
7769
+ "div",
7770
+ {
7771
+ className: "flex h-full flex-row",
7772
+ style: {
7773
+ width: bodyWidth,
7774
+ transform: folded ? "translateX(-100%)" : "none",
7775
+ visibility: folded ? "hidden" : "visible",
7776
+ transition: `transform ${GNB_FOLD_MS}ms ease-out, ${hideAfterSlide(folded)}`
7777
+ },
7778
+ children: [
7779
+ useRail && // 레일 컬럼 — 스크롤되는 아이템 목록(nav)과 하단 고정 footer 를 한 컬럼에 담는다.
7780
+ // footer 는 내비게이션이 아니므로 nav(landmark) 밖에 두고, 컬럼 폭·구분선·배경은 이 래퍼가
7781
+ // 전체 높이에 걸쳐 책임진다(그래야 footer 자리까지 배경·오른쪽 선이 이어진다).
7782
+ /* @__PURE__ */ jsxRuntime.jsxs(
7783
+ "div",
7784
+ {
7785
+ className: "flex shrink-0 flex-col overflow-hidden",
7786
+ style: {
7787
+ width: GNB_RAIL_WIDTH,
7788
+ borderRight: `1px solid ${C.topBorder}`,
7789
+ backgroundColor: C.panelBg
7790
+ },
7791
+ children: [
7792
+ /* @__PURE__ */ jsxRuntime.jsx(
7793
+ "nav",
7794
+ {
7795
+ "aria-label": `${ariaLabel} rail`,
7796
+ className: cn(
7797
+ "flex min-h-0 flex-1 flex-col overflow-x-hidden overflow-y-auto",
7798
+ SCROLLBAR_HIDDEN_CLASS
7799
+ ),
7800
+ style: { padding: GNB_RAIL.padding },
7801
+ children: /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "m-0 flex list-none flex-col p-0", style: { gap: GNB_RAIL.gap }, children: railItems.map(renderRailItem) })
7802
+ }
7639
7803
  ),
7640
- style: { padding: GNB_RAIL.padding },
7641
- children: /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "m-0 flex list-none flex-col p-0", style: { gap: GNB_RAIL.gap }, children: railItems.map(renderRailItem) })
7642
- }
7643
- ),
7644
- railFooter && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "shrink-0", children: railFooter })
7645
- ]
7646
- }
7647
- ),
7648
- showMenu && /* @__PURE__ */ jsxRuntime.jsxs(
7649
- "div",
7650
- {
7651
- className: "flex shrink-0 flex-col overflow-hidden",
7652
- style: {
7653
- width: GNB_MENU_WIDTH,
7654
- backgroundColor: C.panelBg,
7655
- // full 상단바가 전폭이라 루트가 오른쪽 선을 못 그린다 → 메뉴 오른쪽(페이지 경계)이 대신 그린다.
7656
- borderRight: header === "full" ? rightBorder : void 0,
7657
- pointerEvents: "auto"
7658
- },
7659
- children: [
7660
- /* @__PURE__ */ jsxRuntime.jsx(
7661
- "nav",
7662
- {
7663
- "aria-label": ariaLabel,
7664
- className: cn(
7665
- "flex min-h-0 flex-1 flex-col overflow-x-hidden overflow-y-auto",
7666
- SCROLLBAR_HIDDEN_CLASS
7804
+ railFooter && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "shrink-0", children: railFooter })
7805
+ ]
7806
+ }
7807
+ ),
7808
+ showMenu && /* @__PURE__ */ jsxRuntime.jsxs(
7809
+ "div",
7810
+ {
7811
+ className: "flex shrink-0 flex-col overflow-hidden",
7812
+ style: {
7813
+ width: GNB_MENU_WIDTH,
7814
+ backgroundColor: C.panelBg,
7815
+ // full 루트가 오른쪽 선을 못 그리므로(전폭이거나 아예 없다) 메뉴 오른쪽(페이지 경계)이 대신 그린다.
7816
+ borderRight: isFull ? rightBorder : void 0
7817
+ },
7818
+ children: [
7819
+ /* @__PURE__ */ jsxRuntime.jsx(
7820
+ "nav",
7821
+ {
7822
+ "aria-label": ariaLabel,
7823
+ className: cn(
7824
+ "flex min-h-0 flex-1 flex-col overflow-x-hidden overflow-y-auto",
7825
+ SCROLLBAR_HIDDEN_CLASS
7826
+ ),
7827
+ style: {
7828
+ paddingTop: M.bodyPaddingY,
7829
+ paddingBottom: M.bodyPaddingY
7830
+ },
7831
+ children: /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "m-0 flex list-none flex-col p-0", children: menuItems.map((item) => renderItem(item, 1)) })
7832
+ }
7667
7833
  ),
7668
- style: {
7669
- paddingTop: M.bodyPaddingY,
7670
- paddingBottom: M.bodyPaddingY
7671
- },
7672
- children: /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "m-0 flex list-none flex-col p-0", children: menuItems.map((item) => renderItem(item, 1)) })
7673
- }
7674
- ),
7675
- menuFooter && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "shrink-0", children: menuFooter })
7676
- ]
7677
- }
7678
- )
7679
- ]
7834
+ menuFooter && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "shrink-0", children: menuFooter })
7835
+ ]
7836
+ }
7837
+ )
7838
+ ]
7839
+ }
7840
+ )
7680
7841
  }
7681
7842
  ),
7682
- railFolded && foldedFooter && /* @__PURE__ */ jsxRuntime.jsx(
7683
- "div",
7684
- {
7685
- className: "flex shrink-0 items-center justify-center mb-[24px]",
7686
- style: { pointerEvents: "auto" },
7687
- children: foldedFooter
7688
- }
7689
- )
7843
+ railFolded && foldedFooter && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex shrink-0 items-center justify-center mb-[24px]", children: foldedFooter })
7690
7844
  ]
7691
7845
  }
7692
7846
  );
@@ -7698,7 +7852,7 @@ var BACKGROUND_CLASS = {
7698
7852
  };
7699
7853
  var SPage = /* @__PURE__ */ react.forwardRef(function SPage2({ background = "frame", className, style, children, ...rest }, ref) {
7700
7854
  const layout = react.useContext(SLayoutContext);
7701
- const gridStyle = layout?.header === "full" ? { gridColumn: 2, gridRow: 2 } : void 0;
7855
+ const gridStyle = layout?.header === "full" ? { gridArea: "page" } : void 0;
7702
7856
  return /* @__PURE__ */ jsxRuntime.jsx(
7703
7857
  "main",
7704
7858
  {