sellmate-design-system-react 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +20 -0
  2. package/dist/components/SDateRangePicker/SDateRangePicker.cjs +180 -37
  3. package/dist/components/SDateRangePicker/SDateRangePicker.cjs.map +1 -1
  4. package/dist/components/SDateRangePicker/SDateRangePicker.d.ts +6 -1
  5. package/dist/components/SDateRangePicker/SDateRangePicker.js +180 -37
  6. package/dist/components/SDateRangePicker/SDateRangePicker.js.map +1 -1
  7. package/dist/components/SGnb/SGnb.cjs +329 -100
  8. package/dist/components/SGnb/SGnb.cjs.map +1 -1
  9. package/dist/components/SGnb/SGnb.d.ts +24 -5
  10. package/dist/components/SGnb/SGnb.js +330 -101
  11. package/dist/components/SGnb/SGnb.js.map +1 -1
  12. package/dist/components/SGnb/gnb.config.d.ts +46 -12
  13. package/dist/components/SGnb/index.d.ts +1 -1
  14. package/dist/components/SKeyValueTable/SKeyValueTable.cjs +180 -37
  15. package/dist/components/SKeyValueTable/SKeyValueTable.cjs.map +1 -1
  16. package/dist/components/SKeyValueTable/SKeyValueTable.js +180 -37
  17. package/dist/components/SKeyValueTable/SKeyValueTable.js.map +1 -1
  18. package/dist/components/SLayout/SLayout.cjs +37 -21
  19. package/dist/components/SLayout/SLayout.cjs.map +1 -1
  20. package/dist/components/SLayout/SLayout.d.ts +20 -1
  21. package/dist/components/SLayout/SLayout.js +37 -21
  22. package/dist/components/SLayout/SLayout.js.map +1 -1
  23. package/dist/components/SPage/SPage.cjs +38 -22
  24. package/dist/components/SPage/SPage.cjs.map +1 -1
  25. package/dist/components/SPage/SPage.js +38 -22
  26. package/dist/components/SPage/SPage.js.map +1 -1
  27. package/dist/components/STabs/STabs.cjs +15 -26
  28. package/dist/components/STabs/STabs.cjs.map +1 -1
  29. package/dist/components/STabs/STabs.d.ts +1 -1
  30. package/dist/components/STabs/STabs.js +15 -26
  31. package/dist/components/STabs/STabs.js.map +1 -1
  32. package/dist/components/STimePicker/STimePicker.cjs.map +1 -1
  33. package/dist/components/STimePicker/STimePicker.js.map +1 -1
  34. package/dist/components/STimeRangePicker/STimeRangePicker.cjs +3 -1
  35. package/dist/components/STimeRangePicker/STimeRangePicker.cjs.map +1 -1
  36. package/dist/components/STimeRangePicker/STimeRangePicker.js +3 -1
  37. package/dist/components/STimeRangePicker/STimeRangePicker.js.map +1 -1
  38. package/dist/index.cjs +531 -164
  39. package/dist/index.cjs.map +1 -1
  40. package/dist/index.js +528 -165
  41. package/dist/index.js.map +1 -1
  42. package/dist/styles.css +73 -4
  43. package/dist/theme.css +1 -1
  44. package/package.json +3 -1
package/dist/index.js CHANGED
@@ -4876,73 +4876,70 @@ function SLoadingModal({
4876
4876
  }
4877
4877
 
4878
4878
  // src/components/SGnb/gnb.config.ts
4879
- var belt = (p) => `var(--cmp-gnbBelt-${p})`;
4880
- var box = (p) => `var(--cmp-gnbBox-${p})`;
4881
- var light = (p) => `var(--cmp-light-${p})`;
4882
- var dark = (p) => `var(--cmp-dark-${p})`;
4879
+ var gnb = (p) => `var(--cmp-gnb-${p})`;
4880
+ var box = (p) => `var(--cmp-gnb-box-${p})`;
4881
+ var belt = (p) => `var(--cmp-gnb-belt-${p})`;
4882
+ var light = (p) => `var(--cmp-gnb-light-${p})`;
4883
+ var dark = (p) => `var(--cmp-gnb-dark-${p})`;
4884
+ var HEADER_TOP = {
4885
+ topHeight: gnb("top-height"),
4886
+ topPaddingX: gnb("top-paddingX"),
4887
+ topGap: gnb("top-gap"),
4888
+ logoHeight: gnb("logo-height")
4889
+ };
4883
4890
  var GNB_HEADER = {
4884
- fix: {
4885
- topHeight: belt("top-height"),
4886
- topPaddingX: belt("top-paddingX"),
4887
- topGap: belt("top-gap"),
4888
- logoHeight: belt("logo-height")
4889
- },
4890
- full: {
4891
- topHeight: box("top-height"),
4892
- topPaddingX: box("paddingX"),
4893
- topGap: box("gap"),
4894
- // 상단바 gap (버튼 간 세부 gap 은 --cmp-gnbBox-button-gap)
4895
- logoHeight: box("logo-height")
4896
- }
4891
+ fix: HEADER_TOP,
4892
+ full: HEADER_TOP
4897
4893
  };
4898
4894
  var GNB_MENU = {
4899
4895
  box: {
4900
- // menu.body.padding 전방향 토큰 세로 패딩과 아이템 좌우 여백 양쪽에 쓴다
4901
- bodyPaddingY: box("menu-body-padding"),
4902
- itemMarginX: box("menu-body-padding"),
4903
- itemPaddingY: box("item-paddingY"),
4896
+ // 바디 패딩·세로 패딩·chevron gnb 통합된 공통 토큰
4897
+ bodyPaddingY: gnb("body-padding"),
4898
+ itemMarginX: gnb("body-padding"),
4899
+ // 전방향 토큰 → 아이템 좌우 여백에도 사용
4900
+ itemPaddingY: gnb("item-paddingY"),
4904
4901
  itemPaddingRight: box("item-paddingRight"),
4905
4902
  itemGap: box("item-gap"),
4906
4903
  itemRadius: box("item-radius"),
4907
- itemArrow: box("item-arrow"),
4904
+ itemArrow: gnb("item-chevron-size"),
4908
4905
  depth1PaddingLeft: box("item-depth1-paddingLeft"),
4909
4906
  depth2PaddingLeft: box("item-depth2-paddingLeft"),
4910
4907
  depth3PaddingLeft: box("item-depth3-paddingLeft"),
4911
4908
  depth1Icon: box("item-depth1-icon"),
4912
4909
  depth1FontSize: 14,
4913
- // token: gnbBox.item.typography.bold.fontSize
4910
+ // token: gnb.box.item.typography.selected.fontSize
4914
4911
  depth1LineHeight: 24,
4915
- // token: gnbBox.item.typography.bold.lineHeight
4912
+ // token: gnb.box.item.typography.selected.lineHeight
4916
4913
  depth1FontWeight: 700,
4917
4914
  itemFontSize: 14,
4918
- // token: gnbBox.item.typography.default.fontSize
4915
+ // token: gnb.box.item.typography.default.fontSize
4919
4916
  itemLineHeight: 24
4920
- // token: gnbBox.item.typography.default.lineHeight
4917
+ // token: gnb.box.item.typography.default.lineHeight
4921
4918
  },
4922
4919
  belt: {
4923
- bodyPaddingY: belt("body-paddingY"),
4920
+ bodyPaddingY: gnb("body-padding"),
4924
4921
  itemMarginX: "0px",
4925
4922
  // belt 토큰에 가로 여백 없음 → 풀폭 행 (들여쓰기는 depth*-paddingLeft 전담)
4926
- itemPaddingY: belt("item-paddingY"),
4923
+ itemPaddingY: gnb("item-paddingY"),
4927
4924
  itemPaddingRight: belt("item-paddingRight"),
4928
4925
  itemGap: belt("item-gap"),
4929
4926
  itemRadius: "0px",
4930
4927
  // belt 토큰에 radius 없음 → 풀폭 행
4931
- itemArrow: belt("item-icon"),
4928
+ itemArrow: gnb("item-chevron-size"),
4932
4929
  depth1PaddingLeft: belt("item-depth1-paddingLeft"),
4933
4930
  depth2PaddingLeft: belt("item-depth2-paddingLeft"),
4934
4931
  depth3PaddingLeft: belt("item-depth3-paddingLeft"),
4935
- depth1Icon: box("item-depth1-icon"),
4936
- // belt 토큰에 depth1-icon 없음 → box 값(20) 공유
4932
+ depth1Icon: belt("item-depth1-icon"),
4933
+ // belt 전용 depth1 아이콘 토큰(16)
4937
4934
  depth1FontSize: 13,
4938
- // token: gnbBelt.item.depth1.typography.fontSize
4935
+ // token: gnb.belt.item.depth1.typography.fontSize
4939
4936
  depth1LineHeight: 22,
4940
- // token: gnbBelt.item.depth1.typography.lineHeight
4937
+ // token: gnb.belt.item.depth1.typography.lineHeight
4941
4938
  depth1FontWeight: 700,
4942
4939
  itemFontSize: 12,
4943
- // token: gnbBelt.item.typography.default.fontSize
4940
+ // token: gnb.belt.item.typography.default.fontSize
4944
4941
  itemLineHeight: 20
4945
- // token: gnbBelt.item.typography.default.lineHeight
4942
+ // token: gnb.belt.item.typography.default.lineHeight
4946
4943
  }
4947
4944
  };
4948
4945
  var GNB_COLORS = {
@@ -4955,7 +4952,9 @@ var GNB_COLORS = {
4955
4952
  itemColorDefault: light("item-fg-default"),
4956
4953
  itemColorHover: light("item-fg-hover"),
4957
4954
  itemColorActive: light("item-fg-selected"),
4958
- itemChevron: light("item-chevron")
4955
+ itemChevron: light("item-chevron"),
4956
+ itemIcon: "grey_65",
4957
+ railFgDefault: light("rail-fg-default")
4959
4958
  },
4960
4959
  dark: {
4961
4960
  panelBg: dark("bg"),
@@ -4966,19 +4965,41 @@ var GNB_COLORS = {
4966
4965
  itemColorDefault: dark("item-fg-default"),
4967
4966
  itemColorHover: dark("item-fg-hover"),
4968
4967
  itemColorActive: dark("item-fg-selected"),
4969
- itemChevron: dark("item-chevron")
4968
+ itemChevron: dark("item-chevron"),
4969
+ itemIcon: "grey_55",
4970
+ railFgDefault: dark("rail-fg-default")
4970
4971
  }
4971
4972
  };
4972
4973
  var GNB_COMMON = {
4973
4974
  /** 접힘 레일 폭. header="full" 은 접으면 메뉴만 사라져 레일이 없으므로 fix 전용. */
4974
- foldWidth: belt("fold-width"),
4975
- /**
4976
- * 상단바 아이콘 버튼 사이 간격. fix 레일(세로 스택)의 gap·상하 여백에 쓴다.
4977
- * 레일 전용 여백 토큰이 따로 없어 같은 값(8px)인 이 토큰으로 재해석했다.
4978
- */
4979
- buttonGap: box("button-gap")
4980
- };
4975
+ foldWidth: gnb("fold-width")
4976
+ };
4977
+ var GNB_RAIL = {
4978
+ padding: gnb("rail-padding"),
4979
+ gap: gnb("rail-gap"),
4980
+ itemWidth: gnb("rail-item-width"),
4981
+ itemPaddingTop: gnb("rail-item-paddingTop"),
4982
+ itemPaddingBottom: gnb("rail-item-paddingBottom"),
4983
+ itemRadius: gnb("rail-item-radius"),
4984
+ itemIcon: gnb("rail-item-icon"),
4985
+ // 타이포 (theme.css 에 gnb 타이포 변수 없음 → 토큰 값 직접 명시)
4986
+ itemFontSize: 11,
4987
+ // token: gnb.rail.item.typography.default.fontSize
4988
+ itemLineHeight: 18,
4989
+ // token: gnb.rail.item.typography.default.lineHeight
4990
+ itemFontWeight: 500,
4991
+ // token: gnb.rail.item.typography.default.fontWeight
4992
+ itemFontWeightSelected: 700
4993
+ // token: gnb.rail.item.typography.selected.fontWeight
4994
+ };
4995
+ var GNB_RAIL_WIDTH = 68;
4996
+ var GNB_FOLD_MS = 200;
4997
+ var GNB_EXPAND_MS = 200;
4981
4998
  var GNB_MENU_WIDTH = 240;
4999
+ function findGnbRailValue(items, value) {
5000
+ if (items.some((item) => item.value === value)) return value;
5001
+ return findGnbAncestors(items, value)[0];
5002
+ }
4982
5003
  function findGnbAncestors(items, value) {
4983
5004
  const walk = (list, trail) => {
4984
5005
  for (const item of list) {
@@ -5001,6 +5022,7 @@ function useSLayout() {
5001
5022
  var SLayout = forwardRef(function SLayout2({
5002
5023
  type = "box",
5003
5024
  header = "fix",
5025
+ useRail = false,
5004
5026
  folded,
5005
5027
  defaultFolded = false,
5006
5028
  onFoldedChange,
@@ -5019,21 +5041,40 @@ var SLayout = forwardRef(function SLayout2({
5019
5041
  },
5020
5042
  [isControlled, onFoldedChange]
5021
5043
  );
5044
+ const [gnbState, setGnbState] = useState({ forceFull: false, menuVisible: true });
5045
+ const reportGnbState = useCallback(
5046
+ (next) => setGnbState(
5047
+ (prev) => prev.forceFull === next.forceFull && prev.menuVisible === next.menuVisible ? prev : next
5048
+ ),
5049
+ []
5050
+ );
5051
+ const effectiveHeader = header === "full" || gnbState.forceFull ? "full" : "fix";
5022
5052
  const ctx = useMemo(
5023
- () => ({ type, header, folded: currentFolded, setFolded }),
5024
- [type, header, currentFolded, setFolded]
5053
+ () => ({
5054
+ type,
5055
+ header: effectiveHeader,
5056
+ useRail,
5057
+ folded: currentFolded,
5058
+ setFolded,
5059
+ reportGnbState
5060
+ }),
5061
+ [type, effectiveHeader, useRail, currentFolded, setFolded, reportGnbState]
5025
5062
  );
5026
- const isFullHeader = header === "full";
5063
+ const isFullHeader = effectiveHeader === "full";
5064
+ const gnbColumnWidth = (useRail ? GNB_RAIL_WIDTH : 0) + (gnbState.menuVisible ? GNB_MENU_WIDTH : 0);
5027
5065
  const frameStyle = isFullHeader ? {
5028
- gridTemplateColumns: `${currentFolded ? 0 : GNB_MENU_WIDTH}px 1fr`,
5029
- gridTemplateRows: `${GNB_HEADER[header].topHeight} 1fr`
5066
+ gridTemplateColumns: `${currentFolded ? 0 : gnbColumnWidth}px 1fr`,
5067
+ gridTemplateRows: `${GNB_HEADER[effectiveHeader].topHeight} 1fr`,
5068
+ // 메뉴가 미끄러지는 동안 페이지 열도 같은 길이로 따라와야 한 덩어리로 읽힌다.
5069
+ // (fix 는 SGnb 가 제 폭을 직접 전환하므로 프레임이 손댈 게 없다)
5070
+ transition: `grid-template-columns ${GNB_FOLD_MS}ms ease-out`
5030
5071
  } : void 0;
5031
5072
  return /* @__PURE__ */ jsx(SLayoutContext.Provider, { value: ctx, children: /* @__PURE__ */ jsx(
5032
5073
  "div",
5033
5074
  {
5034
5075
  ref,
5035
5076
  "data-type": type,
5036
- "data-header": header,
5077
+ "data-header": effectiveHeader,
5037
5078
  className: cn(
5038
5079
  "box-border h-full overflow-hidden",
5039
5080
  isFullHeader ? "grid" : "flex flex-row",
@@ -5051,8 +5092,10 @@ var SGnb = forwardRef(function SGnb2({
5051
5092
  header: headerProp,
5052
5093
  color = "light",
5053
5094
  items,
5095
+ useRail: useRailProp,
5054
5096
  value = "",
5055
5097
  onValueChange,
5098
+ onRailChange,
5056
5099
  folded: foldedProp,
5057
5100
  onFoldChange,
5058
5101
  onLauncherClick,
@@ -5064,8 +5107,10 @@ var SGnb = forwardRef(function SGnb2({
5064
5107
  }, ref) {
5065
5108
  const layout = useContext(SLayoutContext);
5066
5109
  const type = typeProp ?? layout?.type ?? "box";
5067
- const header = headerProp ?? layout?.header ?? "fix";
5068
5110
  const folded = foldedProp ?? layout?.folded ?? false;
5111
+ const useRail = useRailProp ?? layout?.useRail ?? false;
5112
+ const railHasLeaf = useRail && items.some((item) => !item.children || item.children.length === 0);
5113
+ const header = railHasLeaf ? "full" : headerProp ?? layout?.header ?? "fix";
5069
5114
  const H = GNB_HEADER[header];
5070
5115
  const M = GNB_MENU[type];
5071
5116
  const C = GNB_COLORS[color];
@@ -5073,6 +5118,9 @@ var SGnb = forwardRef(function SGnb2({
5073
5118
  () => new Set(findGnbAncestors(items, value))
5074
5119
  );
5075
5120
  const [hoveredKey, setHoveredKey] = useState(null);
5121
+ const [activeRail, setActiveRail] = useState(
5122
+ () => findGnbRailValue(items, value) ?? items[0]?.value
5123
+ );
5076
5124
  useEffect(() => {
5077
5125
  const ancestors = findGnbAncestors(items, value);
5078
5126
  if (ancestors.length === 0) return;
@@ -5083,6 +5131,15 @@ var SGnb = forwardRef(function SGnb2({
5083
5131
  return next;
5084
5132
  });
5085
5133
  }, [items, value]);
5134
+ useEffect(() => {
5135
+ const owner = findGnbRailValue(items, value);
5136
+ if (owner !== void 0) setActiveRail(owner);
5137
+ }, [items, value]);
5138
+ useEffect(() => {
5139
+ setActiveRail(
5140
+ (prev) => prev !== void 0 && items.some((item) => item.value === prev) ? prev : items[0]?.value
5141
+ );
5142
+ }, [items]);
5086
5143
  const toggleExpand = (key) => {
5087
5144
  setExpandedKeys((prev) => {
5088
5145
  const next = new Set(prev);
@@ -5099,16 +5156,58 @@ var SGnb = forwardRef(function SGnb2({
5099
5156
  }
5100
5157
  onValueChange?.(item.value);
5101
5158
  };
5159
+ const handleRailClick = (item) => {
5160
+ if (item.disabled) return;
5161
+ setActiveRail(item.value);
5162
+ onRailChange?.(item.value);
5163
+ if (!item.children || item.children.length === 0) onValueChange?.(item.value);
5164
+ };
5102
5165
  const handleFoldToggle = () => {
5103
5166
  const next = !folded;
5104
5167
  onFoldChange?.(next);
5105
5168
  if (foldedProp === void 0) layout?.setFolded(next);
5106
5169
  };
5107
- const renderItem = (item, depth) => {
5170
+ const renderRailItem = (item) => {
5171
+ const isActive = item.value === activeRail;
5172
+ const isHovered = hoveredKey === item.value && !item.disabled;
5173
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
5174
+ "button",
5175
+ {
5176
+ type: "button",
5177
+ disabled: item.disabled,
5178
+ "aria-current": isActive ? "true" : void 0,
5179
+ className: "box-border flex select-none flex-col items-center border-none bg-transparent transition-colors duration-150",
5180
+ style: {
5181
+ width: GNB_RAIL.itemWidth,
5182
+ paddingTop: GNB_RAIL.itemPaddingTop,
5183
+ paddingBottom: GNB_RAIL.itemPaddingBottom,
5184
+ borderRadius: GNB_RAIL.itemRadius,
5185
+ backgroundColor: isActive ? C.itemBgSelected : isHovered ? C.itemBgHover : C.itemBgDefault,
5186
+ color: isActive ? C.itemColorActive : C.railFgDefault,
5187
+ // <button> 의 UA 폰트를 끊고 앱 폰트를 잇는다 (메뉴 아이템과 같은 이유)
5188
+ fontFamily: "inherit",
5189
+ fontSize: GNB_RAIL.itemFontSize,
5190
+ lineHeight: `${GNB_RAIL.itemLineHeight}px`,
5191
+ fontWeight: isActive ? GNB_RAIL.itemFontWeightSelected : GNB_RAIL.itemFontWeight,
5192
+ opacity: item.disabled ? 0.4 : 1,
5193
+ cursor: item.disabled ? "not-allowed" : "pointer"
5194
+ },
5195
+ onClick: () => handleRailClick(item),
5196
+ onMouseEnter: () => setHoveredKey(item.value),
5197
+ onMouseLeave: () => setHoveredKey((prev) => prev === item.value ? null : prev),
5198
+ children: [
5199
+ item.icon && /* @__PURE__ */ jsx(SIcon, { name: item.icon, size: GNB_RAIL.itemIcon, className: "shrink-0" }),
5200
+ /* @__PURE__ */ jsx("span", { className: "w-full overflow-hidden text-ellipsis whitespace-nowrap text-center", children: item.label })
5201
+ ]
5202
+ }
5203
+ ) }, item.value);
5204
+ };
5205
+ const renderItem = (item, depth, branchHasIcon = false) => {
5108
5206
  const hasChildren = !!(item.children && item.children.length > 0);
5109
5207
  const isSelected = !hasChildren && item.value === value;
5110
5208
  const isExpanded = expandedKeys.has(item.value);
5111
5209
  const isHovered = hoveredKey === item.value && !item.disabled;
5210
+ const iconBranch = depth === 1 ? !!item.icon : branchHasIcon;
5112
5211
  const itemStyle = {
5113
5212
  gap: M.itemGap,
5114
5213
  paddingTop: M.itemPaddingY,
@@ -5145,8 +5244,11 @@ var SGnb = forwardRef(function SGnb2({
5145
5244
  onMouseEnter: () => setHoveredKey(item.value),
5146
5245
  onMouseLeave: () => setHoveredKey((prev) => prev === item.value ? null : prev),
5147
5246
  children: [
5148
- depth === 1 && item.icon && /* @__PURE__ */ jsx(SIcon, { name: item.icon, size: M.depth1Icon, className: "shrink-0" }),
5149
- /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap", children: item.label }),
5247
+ depth === 1 && item.icon && /* @__PURE__ */ jsx(SIcon, { name: item.icon, size: M.depth1Icon, color: C.itemIcon, className: "shrink-0" }),
5248
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap flex flex-no-wrap items-center", children: [
5249
+ depth > 1 && iconBranch ? /* @__PURE__ */ jsx("div", { className: "w-[20px] h-[24px] flex items-center justify-center mr-[8px]", children: "-" }) : null,
5250
+ item.label
5251
+ ] }),
5150
5252
  item.tag !== void 0 && item.tag !== "" && /* @__PURE__ */ jsx(
5151
5253
  STag,
5152
5254
  {
@@ -5164,94 +5266,221 @@ var SGnb = forwardRef(function SGnb2({
5164
5266
  size: M.itemArrow,
5165
5267
  color: C.itemChevron,
5166
5268
  rotate: isExpanded ? 180 : 0,
5167
- className: "shrink-0 transition-transform duration-150"
5269
+ className: "shrink-0 transition-transform duration-200"
5168
5270
  }
5169
5271
  )
5170
5272
  ]
5171
5273
  }
5172
5274
  ),
5173
- hasChildren && isExpanded && /* @__PURE__ */ jsx("ul", { className: "m-0 flex list-none flex-col p-0", children: item.children.map((child) => renderItem(child, depth + 1)) })
5275
+ hasChildren && /* @__PURE__ */ jsx(
5276
+ "div",
5277
+ {
5278
+ className: "grid",
5279
+ style: {
5280
+ gridTemplateRows: isExpanded ? "1fr" : "0fr",
5281
+ transition: `grid-template-rows ${GNB_EXPAND_MS}ms ease-out`
5282
+ },
5283
+ children: /* @__PURE__ */ jsx(
5284
+ "ul",
5285
+ {
5286
+ className: "m-0 flex min-h-0 list-none flex-col overflow-hidden p-0",
5287
+ style: {
5288
+ visibility: isExpanded ? "visible" : "hidden",
5289
+ transition: `visibility 0s linear ${isExpanded ? "0s" : `${GNB_EXPAND_MS}ms`}`
5290
+ },
5291
+ children: item.children.map((child) => renderItem(child, depth + 1, iconBranch))
5292
+ }
5293
+ )
5294
+ }
5295
+ )
5174
5296
  ] }, item.value)
5175
5297
  );
5176
5298
  };
5299
+ const railItems = useRail ? items : [];
5300
+ const menuItems = useRail ? items.find((item) => item.value === activeRail)?.children ?? [] : items;
5301
+ const showMenu = !useRail || menuItems.length > 0;
5302
+ const bodyWidth = (useRail ? GNB_RAIL_WIDTH : 0) + (showMenu ? GNB_MENU_WIDTH : 0);
5303
+ useEffect(() => {
5304
+ layout?.reportGnbState({ forceFull: railHasLeaf, menuVisible: showMenu });
5305
+ }, [layout, railHasLeaf, showMenu]);
5177
5306
  const railFolded = folded && header === "fix";
5178
- const rootWidth = header === "full" ? "100%" : folded ? GNB_COMMON.foldWidth : GNB_MENU_WIDTH;
5307
+ const rootWidth = header === "full" ? "100%" : folded ? GNB_COMMON.foldWidth : bodyWidth;
5179
5308
  const rootStyle = {
5180
5309
  width: rootWidth,
5310
+ transition: `width ${GNB_FOLD_MS}ms ease-out`,
5181
5311
  ...header === "full" ? { gridArea: "1 / 1 / -1 / -1", pointerEvents: "none" } : { backgroundColor: C.panelBg },
5182
5312
  ...style
5183
5313
  };
5314
+ const hideAfterSlide = (hidden) => `visibility 0s linear ${hidden ? `${GNB_FOLD_MS}ms` : "0s"}`;
5315
+ const foldInline = !!logo;
5316
+ const sideFoldStyle = {
5317
+ opacity: railFolded ? 0 : 1,
5318
+ position: railFolded ? "absolute" : "static",
5319
+ visibility: railFolded ? "hidden" : "visible",
5320
+ transition: `opacity ${GNB_FOLD_MS}ms ease-out, ${hideAfterSlide(railFolded)}`
5321
+ };
5184
5322
  return /* @__PURE__ */ jsxs(
5185
5323
  "div",
5186
5324
  {
5187
5325
  ref,
5188
- className: cn("box-border flex h-full shrink-0 flex-col", className),
5326
+ className: cn("box-border flex h-full shrink-0 flex-col overflow-hidden", className),
5189
5327
  style: rootStyle,
5190
5328
  "data-type": type,
5191
5329
  "data-header": header,
5192
5330
  "data-color": color,
5193
5331
  ...rest,
5194
5332
  children: [
5195
- /* @__PURE__ */ jsxs(
5333
+ /* @__PURE__ */ jsx(
5196
5334
  "div",
5197
5335
  {
5198
- className: cn("box-border flex items-center", railFolded && "flex-col"),
5336
+ className: "relative box-border flex shrink-0 items-center overflow-hidden",
5199
5337
  style: {
5200
- gap: railFolded ? GNB_COMMON.buttonGap : H.topGap,
5201
- height: railFolded ? "auto" : H.topHeight,
5202
- padding: railFolded ? `${GNB_COMMON.buttonGap} 0` : `0 ${H.topPaddingX}`,
5338
+ height: H.topHeight,
5339
+ // 인라인 간격은 컨테이너 gap 대신 각 요소 margin 으로 준다 — 런처↔폴드(8px)와
5340
+ // 폴드↔로고(top-gap)를 다르게 두기 위함. 접히면 런처·로고가 흐름에서 빠져 margin 도 무의미해진다.
5341
+ padding: `0 ${railFolded ? GNB_RAIL.padding : H.topPaddingX}`,
5203
5342
  borderBottom: `1px solid ${C.topBorder}`,
5204
5343
  backgroundColor: C.panelBg,
5205
5344
  pointerEvents: "auto",
5345
+ transition: `padding ${GNB_FOLD_MS}ms ease-out`,
5206
5346
  // 아이콘 버튼은 자체 ghostButton 토큰을 쓰므로, 이 색은 logo 슬롯 상속용
5207
5347
  color: C.itemColorDefault
5208
5348
  },
5209
- children: [
5210
- onLauncherClick && /* @__PURE__ */ jsx(
5211
- SGhostButton,
5212
- {
5213
- icon: "launcher",
5214
- size: "xs",
5215
- intent: color === "dark" ? "inverse" : "default",
5216
- ariaLabel: "app launcher",
5217
- onClick: onLauncherClick,
5218
- className: "shrink-0"
5219
- }
5220
- ),
5221
- !railFolded && logo && /* @__PURE__ */ jsx("div", { className: "flex items-center", style: { height: H.logoHeight }, children: logo }),
5222
- /* @__PURE__ */ jsx(
5223
- SGhostButton,
5224
- {
5225
- icon: "sidebar",
5226
- size: "xs",
5227
- intent: color === "dark" ? "inverse" : "default",
5228
- ariaLabel: "toggle navigation",
5229
- ariaPressed: folded,
5230
- onClick: handleFoldToggle,
5231
- className: cn("shrink-0", !railFolded && "ml-auto")
5232
- }
5233
- )
5234
- ]
5349
+ children: foldInline ? (
5350
+ // 런처(있을 때)·폴드·로고를 흐름에 나란히 둔다 → 폴드 버튼이 (런처와) 로고 사이에 선다.
5351
+ // 접히면 런처·로고가 페이드하며 흐름에서 빠지고(sideFoldStyle), 폴드 버튼만 남아 레일 가운데에 선다.
5352
+ /* @__PURE__ */ jsxs(Fragment, { children: [
5353
+ onLauncherClick && /* @__PURE__ */ jsx(
5354
+ SGhostButton,
5355
+ {
5356
+ icon: "launcher",
5357
+ size: "xs",
5358
+ intent: color === "dark" ? "inverse" : "default",
5359
+ ariaLabel: "app launcher",
5360
+ onClick: onLauncherClick,
5361
+ className: "shrink-0",
5362
+ style: { ...sideFoldStyle, marginRight: "8px" }
5363
+ }
5364
+ ),
5365
+ /* @__PURE__ */ jsx(
5366
+ SGhostButton,
5367
+ {
5368
+ icon: "sidebar",
5369
+ size: "xs",
5370
+ intent: color === "dark" ? "inverse" : "default",
5371
+ ariaLabel: "toggle navigation",
5372
+ ariaPressed: folded,
5373
+ onClick: handleFoldToggle,
5374
+ className: "shrink-0"
5375
+ }
5376
+ ),
5377
+ /* @__PURE__ */ jsx(
5378
+ "div",
5379
+ {
5380
+ className: "flex shrink-0 items-center",
5381
+ style: { height: H.logoHeight, marginLeft: H.topGap, ...sideFoldStyle },
5382
+ children: logo
5383
+ }
5384
+ )
5385
+ ] })
5386
+ ) : (
5387
+ // 런처·로고는 좌측 그룹(흐름에서 빼 폭 축소 중 폴드 버튼을 밀어내지 않는다),
5388
+ // 폴드 버튼은 우측 끝(ml-auto)이라 폭·패딩이 줄면 저절로 레일 가운데에 선다.
5389
+ /* @__PURE__ */ jsxs(Fragment, { children: [
5390
+ (onLauncherClick || logo) && /* @__PURE__ */ jsxs(
5391
+ "div",
5392
+ {
5393
+ className: "absolute inset-y-0 flex items-center",
5394
+ style: {
5395
+ left: H.topPaddingX,
5396
+ gap: H.topGap,
5397
+ opacity: railFolded ? 0 : 1,
5398
+ transition: `opacity ${GNB_FOLD_MS}ms ease-out, ${hideAfterSlide(railFolded)}, transform ${GNB_FOLD_MS}ms ease-out`,
5399
+ // 메뉴와 같은 방향으로 빠지되, 좁은 레일 안에서 꼬리가 비치지 않게 페이드가 거리를 대신한다
5400
+ transform: railFolded ? `translateX(-${H.topPaddingX})` : "none",
5401
+ visibility: railFolded ? "hidden" : "visible"
5402
+ },
5403
+ children: [
5404
+ onLauncherClick && /* @__PURE__ */ jsx(
5405
+ SGhostButton,
5406
+ {
5407
+ icon: "launcher",
5408
+ size: "xs",
5409
+ intent: color === "dark" ? "inverse" : "default",
5410
+ ariaLabel: "app launcher",
5411
+ onClick: onLauncherClick,
5412
+ className: "shrink-0"
5413
+ }
5414
+ ),
5415
+ logo && /* @__PURE__ */ jsx("div", { className: "flex items-center", style: { height: H.logoHeight }, children: logo })
5416
+ ]
5417
+ }
5418
+ ),
5419
+ /* @__PURE__ */ jsx(
5420
+ SGhostButton,
5421
+ {
5422
+ icon: "sidebar",
5423
+ size: "xs",
5424
+ intent: color === "dark" ? "inverse" : "default",
5425
+ ariaLabel: "toggle navigation",
5426
+ ariaPressed: folded,
5427
+ onClick: handleFoldToggle,
5428
+ className: "ml-auto shrink-0"
5429
+ }
5430
+ )
5431
+ ] })
5432
+ )
5235
5433
  }
5236
5434
  ),
5237
- !folded && /* @__PURE__ */ jsx(
5238
- "nav",
5435
+ /* @__PURE__ */ jsxs(
5436
+ "div",
5239
5437
  {
5240
- "aria-label": ariaLabel,
5241
- className: "flex flex-col overflow-y-auto",
5438
+ className: "flex flex-row",
5242
5439
  style: {
5243
- width: GNB_MENU_WIDTH,
5440
+ width: bodyWidth,
5244
5441
  alignSelf: "flex-start",
5245
5442
  flexGrow: 1,
5246
5443
  flexShrink: 1,
5247
5444
  flexBasis: "auto",
5248
5445
  minHeight: 0,
5249
- paddingTop: M.bodyPaddingY,
5250
- paddingBottom: M.bodyPaddingY,
5251
- backgroundColor: C.panelBg,
5252
- pointerEvents: "auto"
5446
+ transform: folded ? "translateX(-100%)" : "none",
5447
+ visibility: folded ? "hidden" : "visible",
5448
+ transition: `transform ${GNB_FOLD_MS}ms ease-out, ${hideAfterSlide(folded)}`
5253
5449
  },
5254
- children: /* @__PURE__ */ jsx("ul", { className: "m-0 flex list-none flex-col p-0", children: items.map((item) => renderItem(item, 1)) })
5450
+ children: [
5451
+ useRail && // 레일도 내비게이션 landmark — nav 로 세워야 aria-label(레일)이 스크린리더에 노출된다
5452
+ // (role 없는 ul 의 aria-label 은 영역명으로 읽히지 않는다). 메뉴 nav 와는 레이블로 구분된다.
5453
+ /* @__PURE__ */ jsx(
5454
+ "nav",
5455
+ {
5456
+ "aria-label": `${ariaLabel} rail`,
5457
+ className: "flex shrink-0 flex-col overflow-y-auto",
5458
+ style: {
5459
+ width: GNB_RAIL_WIDTH,
5460
+ padding: GNB_RAIL.padding,
5461
+ borderRight: `1px solid ${C.topBorder}`,
5462
+ backgroundColor: C.panelBg,
5463
+ pointerEvents: "auto"
5464
+ },
5465
+ children: /* @__PURE__ */ jsx("ul", { className: "m-0 flex list-none flex-col p-0", style: { gap: GNB_RAIL.gap }, children: railItems.map(renderRailItem) })
5466
+ }
5467
+ ),
5468
+ showMenu && /* @__PURE__ */ jsx(
5469
+ "nav",
5470
+ {
5471
+ "aria-label": ariaLabel,
5472
+ className: "flex shrink-0 flex-col overflow-y-auto",
5473
+ style: {
5474
+ width: GNB_MENU_WIDTH,
5475
+ paddingTop: M.bodyPaddingY,
5476
+ paddingBottom: M.bodyPaddingY,
5477
+ backgroundColor: C.panelBg,
5478
+ pointerEvents: "auto"
5479
+ },
5480
+ children: /* @__PURE__ */ jsx("ul", { className: "m-0 flex list-none flex-col p-0", children: menuItems.map((item) => renderItem(item, 1)) })
5481
+ }
5482
+ )
5483
+ ]
5255
5484
  }
5256
5485
  )
5257
5486
  ]
@@ -5265,7 +5494,7 @@ var SPage = forwardRef(function SPage2({ className, style, children, ...rest },
5265
5494
  "main",
5266
5495
  {
5267
5496
  ref,
5268
- className: cn("box-border min-h-0 min-w-0 flex-1 overflow-auto p-24", className),
5497
+ className: cn("box-border min-h-0 min-w-0 flex-1 overflow-auto p-20", className),
5269
5498
  style: { ...gridStyle, ...style },
5270
5499
  ...rest,
5271
5500
  children
@@ -5307,6 +5536,7 @@ function STabs({
5307
5536
  }) {
5308
5537
  const s = TAB_MAIN_SIZE[size];
5309
5538
  const blue = TAB_MAIN_COLORS.selected.border;
5539
+ const isVertical = isSub && vertical;
5310
5540
  const tabRefs = useRef([]);
5311
5541
  const moveTo = (target) => {
5312
5542
  if (target < 0 || target >= tabs.length) return;
@@ -5314,19 +5544,17 @@ function STabs({
5314
5544
  tabRefs.current[target]?.focus();
5315
5545
  };
5316
5546
  const containerStyle = { ...style };
5317
- if (!isSub) {
5318
- if (vertical) containerStyle.borderRight = `1px solid ${blue}`;
5319
- else containerStyle.borderBottom = `1px solid ${blue}`;
5320
- }
5547
+ if (!isSub) containerStyle.borderBottom = `1px solid ${blue}`;
5321
5548
  return /* @__PURE__ */ jsx(
5322
5549
  "div",
5323
5550
  {
5324
5551
  role: "tablist",
5325
- "aria-orientation": vertical ? "vertical" : "horizontal",
5552
+ "aria-orientation": isVertical ? "vertical" : "horizontal",
5326
5553
  className: cn(
5327
- "inline-flex w-fit",
5328
- vertical ? "flex-col items-stretch" : "flex-row",
5329
- isSub ? vertical ? "gap-[16px]" : "gap-[24px]" : "gap-[4px]",
5554
+ // main(가로): baseline(하단 divider)이 100% 너비로 채워지도록 w-full
5555
+ isSub ? "inline-flex w-fit" : "flex w-full",
5556
+ isVertical ? "flex-col items-stretch" : "flex-row",
5557
+ isSub ? isVertical ? "gap-[16px]" : "gap-[24px]" : "gap-[4px]",
5330
5558
  className
5331
5559
  ),
5332
5560
  style: containerStyle,
@@ -5355,22 +5583,12 @@ function STabs({
5355
5583
  color: selected ? TAB_MAIN_COLORS.selected.text : TAB_MAIN_COLORS.default.text,
5356
5584
  border: `1px solid ${border}`,
5357
5585
  // 폴더형: 미선택 탭은 baseline 위에, 선택 탭은 baseline(파란선)을 1px 덮어 연결된 것처럼 보이게
5358
- ...vertical ? {
5359
- borderTopLeftRadius: 6,
5360
- borderBottomLeftRadius: 6,
5361
- borderTopRightRadius: 0,
5362
- borderBottomRightRadius: 0,
5363
- justifyContent: "flex-start",
5364
- borderRight: selected ? `1px solid ${selectedBg}` : "none",
5365
- marginRight: selected ? -1 : 0
5366
- } : {
5367
- borderTopLeftRadius: 6,
5368
- borderTopRightRadius: 6,
5369
- borderBottomLeftRadius: 0,
5370
- borderBottomRightRadius: 0,
5371
- borderBottom: selected ? `1px solid ${selectedBg}` : "none",
5372
- marginBottom: selected ? -1 : 0
5373
- }
5586
+ borderTopLeftRadius: 6,
5587
+ borderTopRightRadius: 6,
5588
+ borderBottomLeftRadius: 0,
5589
+ borderBottomRightRadius: 0,
5590
+ borderBottom: selected ? `1px solid ${selectedBg}` : "none",
5591
+ marginBottom: selected ? -1 : 0
5374
5592
  };
5375
5593
  }
5376
5594
  return /* @__PURE__ */ jsxs(
@@ -5390,8 +5608,8 @@ function STabs({
5390
5608
  select();
5391
5609
  return;
5392
5610
  }
5393
- const nextKey = vertical ? "ArrowDown" : "ArrowRight";
5394
- const prevKey = vertical ? "ArrowUp" : "ArrowLeft";
5611
+ const nextKey = isVertical ? "ArrowDown" : "ArrowRight";
5612
+ const prevKey = isVertical ? "ArrowUp" : "ArrowLeft";
5395
5613
  if (e.key === nextKey) {
5396
5614
  e.preventDefault();
5397
5615
  moveTo((i + 1) % tabs.length);
@@ -7912,25 +8130,45 @@ function SDatePicker({
7912
8130
  }
7913
8131
  );
7914
8132
  }
8133
+ function splitDateTime(v) {
8134
+ if (!v) return { date: null, hour: "00", minute: "00" };
8135
+ const [date = "", time = ""] = v.split(" ");
8136
+ const [hour = "00", minute = "00"] = time.split(":");
8137
+ return { date: date || null, hour, minute };
8138
+ }
7915
8139
  function RangeCalendar({
7916
8140
  value,
7917
8141
  selectable,
7918
8142
  maxRange,
8143
+ useTimePicker = false,
7919
8144
  onSelect,
7920
8145
  onPendingStartChange,
7921
8146
  onViewChange
7922
8147
  }) {
7923
8148
  const today = todayStr();
7924
- const base = value?.[0] || today;
8149
+ const initial = splitDateTime(value?.[0]);
8150
+ const base = initial.date || today;
7925
8151
  const [y, setY] = useState(Number(base.split("-")[0]));
7926
8152
  const [m, setM] = useState(Number(base.split("-")[1]));
7927
- const [start, setStart] = useState(value?.[0] ?? null);
7928
- const [end, setEnd] = useState(value?.[1] ?? null);
8153
+ const [start, setStart] = useState(initial.date);
8154
+ const [end, setEnd] = useState(splitDateTime(value?.[1]).date);
7929
8155
  const [hover, setHover] = useState(null);
8156
+ const [sH, setSH] = useState(initial.hour);
8157
+ const [sM, setSM] = useState(initial.minute);
8158
+ const [eH, setEH] = useState(splitDateTime(value?.[1]).hour);
8159
+ const [eM, setEM] = useState(splitDateTime(value?.[1]).minute);
8160
+ const lastEmitted = useRef("");
7930
8161
  useEffect(() => {
7931
- setStart(value?.[0] ?? null);
7932
- setEnd(value?.[1] ?? null);
7933
- }, [value]);
8162
+ if (useTimePicker && JSON.stringify(value) === lastEmitted.current) return;
8163
+ const s = splitDateTime(value?.[0]);
8164
+ const e = splitDateTime(value?.[1]);
8165
+ setStart(s.date);
8166
+ setEnd(e.date);
8167
+ setSH(s.hour);
8168
+ setSM(s.minute);
8169
+ setEH(e.hour);
8170
+ setEM(e.minute);
8171
+ }, [value, useTimePicker]);
7934
8172
  const setBase = (ny, nm) => {
7935
8173
  setY(ny);
7936
8174
  setM(nm);
@@ -7974,18 +8212,53 @@ function RangeCalendar({
7974
8212
  const hasRange = !!(rStart && rEnd && rStart !== rEnd);
7975
8213
  const inRange = (d) => !!(rStart && rEnd && d > rStart && d < rEnd);
7976
8214
  const RANGE_BG = "var(--cmp-datepicker-calendar-range-bg)";
8215
+ const pad = (v) => String(Number(v) || 0).padStart(2, "0");
8216
+ const emit2 = (dateStart, dateEnd, time) => {
8217
+ const t = time ?? { sH, sM, eH, eM };
8218
+ const range = useTimePicker ? [`${dateStart} ${pad(t.sH)}:${pad(t.sM)}`, `${dateEnd} ${pad(t.eH)}:${pad(t.eM)}`] : [dateStart, dateEnd];
8219
+ lastEmitted.current = JSON.stringify(range);
8220
+ onSelect(range);
8221
+ };
7977
8222
  const click = (d) => {
7978
8223
  if (isDisabled(d)) return;
7979
8224
  if (!start || complete || d < start) {
7980
8225
  setStart(d);
7981
8226
  setEnd(null);
7982
8227
  setHover(null);
8228
+ if (useTimePicker) {
8229
+ setSH("00");
8230
+ setSM("00");
8231
+ setEH("23");
8232
+ setEM("59");
8233
+ }
7983
8234
  onPendingStartChange?.(d);
7984
8235
  return;
7985
8236
  }
7986
8237
  setEnd(d);
7987
8238
  onPendingStartChange?.(null);
7988
- onSelect([start, d]);
8239
+ emit2(start, d, useTimePicker ? { sH, sM, eH: "23", eM: "59" } : void 0);
8240
+ };
8241
+ const changeTime = (which, part, raw) => {
8242
+ const digits = raw.replace(/\D/g, "").slice(0, 2);
8243
+ const max = part === "h" ? 23 : 59;
8244
+ const next = digits !== "" && Number(digits) > max ? String(max) : digits;
8245
+ const nextTime = { sH, sM, eH, eM };
8246
+ if (which === "start") nextTime[part === "h" ? "sH" : "sM"] = next;
8247
+ else nextTime[part === "h" ? "eH" : "eM"] = next;
8248
+ setSH(nextTime.sH);
8249
+ setSM(nextTime.sM);
8250
+ setEH(nextTime.eH);
8251
+ setEM(nextTime.eM);
8252
+ if (start && end) emit2(start, end, nextTime);
8253
+ };
8254
+ const blurTime = (which, part) => {
8255
+ if (which === "start") {
8256
+ if (part === "h") setSH(pad(sH));
8257
+ else setSM(pad(sM));
8258
+ } else {
8259
+ if (part === "h") setEH(pad(eH));
8260
+ else setEM(pad(eM));
8261
+ }
7989
8262
  };
7990
8263
  const GRID_COLS = "grid grid-cols-[repeat(7,var(--cmp-datepicker-calendar-day-width))]";
7991
8264
  const renderPanel = (gy, gm, isLeft) => /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-[var(--cmp-datepicker-calendar-gap)]", children: [
@@ -8060,35 +8333,120 @@ function RangeCalendar({
8060
8333
  );
8061
8334
  }) })
8062
8335
  ] });
8063
- return /* @__PURE__ */ jsxs("div", { className: "inline-flex h-[364px] flex-col gap-[var(--cmp-datepicker-calendar-gap)] rounded-[var(--cmp-datepicker-calendar-radius)] bg-[var(--cmp-datepicker-calendar-bg)] p-[var(--cmp-datepicker-calendar-paddingXY)] shadow-[2px_2px_12px_2px_rgba(0,0,0,0.2)] select-none", children: [
8064
- /* @__PURE__ */ jsxs("div", { className: "relative flex items-center justify-center", children: [
8065
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center gap-[var(--cmp-datepicker-calendar-gap)] text-[14px] font-medium leading-[24px]", children: [
8066
- /* @__PURE__ */ jsx(SGhostButton, { icon: "chevronLeft", size: "xxs", ariaLabel: "prevYear", onClick: goPrevYear }),
8067
- /* @__PURE__ */ jsx("span", { className: "inline-flex min-w-[40px] items-center justify-center text-center", children: y }),
8068
- /* @__PURE__ */ jsx(SGhostButton, { icon: "chevronRight", size: "xxs", ariaLabel: "nextYear", onClick: goNextYear })
8069
- ] }),
8070
- /* @__PURE__ */ jsx(
8071
- "button",
8072
- {
8073
- type: "button",
8074
- onClick: goToday,
8075
- className: "absolute right-0 h-[24px] min-w-[37px] cursor-pointer bg-transparent text-center text-[12px] font-medium leading-[24px] text-[color:var(--cmp-datepicker-calendar-day-default-text)] hover:underline",
8076
- children: "\uC624\uB298"
8077
- }
8078
- )
8079
- ] }),
8080
- /* @__PURE__ */ jsxs("div", { className: "flex gap-[var(--cmp-datepicker-calendar-range-panelGap)]", children: [
8081
- renderPanel(y, m, true),
8082
- /* @__PURE__ */ jsx(
8083
- "span",
8084
- {
8085
- className: "w-px shrink-0 self-stretch bg-[var(--cmp-datepicker-calendar-range-divider)]",
8086
- "aria-hidden": true
8087
- }
8336
+ const renderTimeSection = (which, date, suffix) => {
8337
+ const active = !!date;
8338
+ const [yy = "", mm = "", dd = ""] = date ? date.split("-") : [];
8339
+ const hour = which === "start" ? sH : eH;
8340
+ const minute = which === "start" ? sM : eM;
8341
+ const num = (n, strip) => active && n !== "" ? strip ? String(Number(n)) : n : "-";
8342
+ const INPUT_CLS = "h-[var(--cmp-datepicker-sm-height)] w-[40px] rounded-[var(--cmp-datepicker-sm-radius)] border border-solid border-[color:var(--cmp-datepicker-border-default)] bg-[var(--cmp-datepicker-bg-default)] text-center text-[12px] font-medium leading-[20px] text-[color:var(--cmp-datepicker-text-default)] outline-none placeholder:text-[color:var(--cmp-datepicker-text-disabled)] focus:border-[color:var(--cmp-datepicker-border-focus)] disabled:cursor-not-allowed";
8343
+ const LABEL = "text-[color:var(--cmp-datepicker-calendar-time-date-label)]";
8344
+ const UNIT = "text-[color:var(--cmp-datepicker-calendar-time-date-unit)]";
8345
+ return /* @__PURE__ */ jsxs(
8346
+ "div",
8347
+ {
8348
+ className: cn(
8349
+ "flex flex-1 items-center justify-center gap-[var(--cmp-datepicker-calendar-time-section-gap)] rounded-[var(--cmp-datepicker-calendar-time-radius)] px-[var(--cmp-datepicker-calendar-time-paddingX)] py-[var(--cmp-datepicker-calendar-time-paddingY)] text-[12px] font-medium leading-[20px]",
8350
+ active ? "bg-[var(--cmp-datepicker-calendar-time-bg-selected)]" : "bg-[var(--cmp-datepicker-calendar-time-bg-default)]"
8351
+ ),
8352
+ children: [
8353
+ /* @__PURE__ */ jsxs("div", { className: "inline-flex items-center gap-[var(--cmp-datepicker-calendar-time-date-gap)]", children: [
8354
+ /* @__PURE__ */ jsx("span", { className: LABEL, children: num(yy, false) }),
8355
+ /* @__PURE__ */ jsx("span", { className: UNIT, children: "\uB144" }),
8356
+ /* @__PURE__ */ jsx("span", { className: LABEL, children: num(mm, true) }),
8357
+ /* @__PURE__ */ jsx("span", { className: UNIT, children: "\uC6D4" }),
8358
+ /* @__PURE__ */ jsx("span", { className: LABEL, children: num(dd, true) }),
8359
+ /* @__PURE__ */ jsx("span", { className: UNIT, children: "\uC77C" })
8360
+ ] }),
8361
+ /* @__PURE__ */ jsxs("div", { className: "inline-flex items-center gap-[var(--cmp-datepicker-calendar-time-unit-gap)]", children: [
8362
+ /* @__PURE__ */ jsx(
8363
+ "input",
8364
+ {
8365
+ type: "text",
8366
+ inputMode: "numeric",
8367
+ maxLength: 2,
8368
+ "aria-label": `${suffix} \uC2DC`,
8369
+ disabled: !active,
8370
+ value: active ? hour : "",
8371
+ placeholder: "--",
8372
+ onChange: (e) => changeTime(which, "h", e.target.value),
8373
+ onBlur: () => blurTime(which, "h"),
8374
+ className: INPUT_CLS
8375
+ }
8376
+ ),
8377
+ /* @__PURE__ */ jsx("span", { className: LABEL, children: ":" }),
8378
+ /* @__PURE__ */ jsx(
8379
+ "input",
8380
+ {
8381
+ type: "text",
8382
+ inputMode: "numeric",
8383
+ maxLength: 2,
8384
+ "aria-label": `${suffix} \uBD84`,
8385
+ disabled: !active,
8386
+ value: active ? minute : "",
8387
+ placeholder: "--",
8388
+ onChange: (e) => changeTime(which, "m", e.target.value),
8389
+ onBlur: () => blurTime(which, "m"),
8390
+ className: INPUT_CLS
8391
+ }
8392
+ )
8393
+ ] }),
8394
+ /* @__PURE__ */ jsx("span", { className: UNIT, children: suffix })
8395
+ ]
8396
+ }
8397
+ );
8398
+ };
8399
+ return /* @__PURE__ */ jsxs(
8400
+ "div",
8401
+ {
8402
+ className: cn(
8403
+ "inline-flex flex-col gap-[var(--cmp-datepicker-calendar-gap)] rounded-[var(--cmp-datepicker-calendar-radius)] bg-[var(--cmp-datepicker-calendar-bg)] p-[var(--cmp-datepicker-calendar-paddingXY)] shadow-[2px_2px_12px_2px_rgba(0,0,0,0.2)] select-none",
8404
+ useTimePicker ? "h-auto" : "h-[364px]"
8088
8405
  ),
8089
- renderPanel(nextMonthYM.year, nextMonthYM.month, false)
8090
- ] })
8091
- ] });
8406
+ children: [
8407
+ /* @__PURE__ */ jsxs("div", { className: "relative flex items-center justify-center", children: [
8408
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center gap-[var(--cmp-datepicker-calendar-gap)] text-[14px] font-medium leading-[24px]", children: [
8409
+ /* @__PURE__ */ jsx(SGhostButton, { icon: "chevronLeft", size: "xxs", ariaLabel: "prevYear", onClick: goPrevYear }),
8410
+ /* @__PURE__ */ jsx("span", { className: "inline-flex min-w-[40px] items-center justify-center text-center", children: y }),
8411
+ /* @__PURE__ */ jsx(SGhostButton, { icon: "chevronRight", size: "xxs", ariaLabel: "nextYear", onClick: goNextYear })
8412
+ ] }),
8413
+ /* @__PURE__ */ jsx(
8414
+ "button",
8415
+ {
8416
+ type: "button",
8417
+ onClick: goToday,
8418
+ className: "absolute right-0 h-[24px] min-w-[37px] cursor-pointer bg-transparent text-center text-[12px] font-medium leading-[24px] text-[color:var(--cmp-datepicker-calendar-day-default-text)] hover:underline",
8419
+ children: "\uC624\uB298"
8420
+ }
8421
+ )
8422
+ ] }),
8423
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-[var(--cmp-datepicker-calendar-range-panelGap)]", children: [
8424
+ renderPanel(y, m, true),
8425
+ /* @__PURE__ */ jsx(
8426
+ "span",
8427
+ {
8428
+ className: "w-px shrink-0 self-stretch bg-[var(--cmp-datepicker-calendar-range-divider)]",
8429
+ "aria-hidden": true
8430
+ }
8431
+ ),
8432
+ renderPanel(nextMonthYM.year, nextMonthYM.month, false)
8433
+ ] }),
8434
+ useTimePicker && /* @__PURE__ */ jsxs(Fragment, { children: [
8435
+ /* @__PURE__ */ jsx(
8436
+ "span",
8437
+ {
8438
+ className: "h-px w-full bg-[var(--cmp-datepicker-calendar-range-divider)]",
8439
+ "aria-hidden": true
8440
+ }
8441
+ ),
8442
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-[var(--cmp-datepicker-calendar-time-gap)]", children: [
8443
+ renderTimeSection("start", start, "\uBD80\uD130"),
8444
+ renderTimeSection("end", end, "\uAE4C\uC9C0")
8445
+ ] })
8446
+ ] })
8447
+ ]
8448
+ }
8449
+ );
8092
8450
  }
8093
8451
  function SDateRangePicker({
8094
8452
  value,
@@ -8098,6 +8456,7 @@ function SDateRangePicker({
8098
8456
  placeholder = "YYYY-MM-DD ~ YYYY-MM-DD",
8099
8457
  selectable,
8100
8458
  maxRange,
8459
+ useTimePicker = false,
8101
8460
  disabled = false,
8102
8461
  width,
8103
8462
  name,
@@ -8141,8 +8500,9 @@ function SDateRangePicker({
8141
8500
  setPendingStart(null);
8142
8501
  onValueChange?.(r);
8143
8502
  if (rules && rules.length > 0) setRuleError(runRules(r, rules));
8144
- setOpen(false);
8503
+ if (!useTimePicker) setOpen(false);
8145
8504
  };
8505
+ const resolvedPlaceholder = useTimePicker && placeholder === "YYYY-MM-DD ~ YYYY-MM-DD" ? "YYYY-MM-DD HH:mm ~ YYYY-MM-DD HH:mm" : placeholder;
8146
8506
  return /* @__PURE__ */ jsx(
8147
8507
  SField,
8148
8508
  {
@@ -8209,7 +8569,7 @@ function SDateRangePicker({
8209
8569
  /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-right", children: startText }),
8210
8570
  /* @__PURE__ */ jsx("span", { className: "px-[4px]", children: "~" }),
8211
8571
  /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-left", children: endText })
8212
- ] }) : placeholder
8572
+ ] }) : resolvedPlaceholder
8213
8573
  }
8214
8574
  )
8215
8575
  ]
@@ -8228,6 +8588,7 @@ function SDateRangePicker({
8228
8588
  value: value ?? null,
8229
8589
  selectable,
8230
8590
  maxRange,
8591
+ useTimePicker,
8231
8592
  onSelect: select,
8232
8593
  onPendingStartChange: setPendingStart,
8233
8594
  onViewChange
@@ -10262,7 +10623,9 @@ function RangeTimeSelector({
10262
10623
  };
10263
10624
  const commit = (side, hour, minute, direction = "unknown") => {
10264
10625
  const next = formatTimeValue(hour, minute);
10265
- onChange(normalizeNextRange(side === "start" ? [next, range[1]] : [range[0], next], side, direction));
10626
+ onChange(
10627
+ normalizeNextRange(side === "start" ? [next, range[1]] : [range[0], next], side, direction)
10628
+ );
10266
10629
  };
10267
10630
  const getTimeMinutes2 = (time) => {
10268
10631
  const { hour, minute } = parseTimeValue(time);
@@ -10654,6 +11017,6 @@ function STimeRangePicker({
10654
11017
  );
10655
11018
  }
10656
11019
 
10657
- export { BADGE_COLORS, BUTTON_COLORS, BUTTON_SIZES, COLOR_KEYS, GNB_COLORS, GNB_COMMON, GNB_HEADER, GNB_MENU, GNB_MENU_WIDTH, ICONS, SActionModal, SBadge, SBarcodeInput, SButton, SCalendar, SCallout, SCard, SCheckbox, SChip, SChipInput, SCircleProgress, SConfirmModal, SDatePicker, SDateRangePicker, SDivider, SDropdownButton, SField, SFilePicker, SForm, SGhostButton, SGnb, SGuide, SIcon, SInput, SKeyValueTable, SLayout, SLayoutContext, SLinearProgress, SLoadingContainer, SLoadingModal, SLoadingViewport, SNumberInput, SPage, SPagination, SPopover, SPopup, SPortal, SRadio, SRadioButton, SRadioGroup, SSelect, SSwitch, STable, STabs, STag, STextLink, STextarea, STimePicker, STimeRangePicker, SToast, SToastContainer, SToggle, STooltip, TAG_COLORS, TAG_SHAPES, TAG_SIZES, TIMEPICKER_SIZES, TIMEPICKER_SIZE_CONFIG, buttonPreset, cn, findGnbAncestors, isColorKey, loading, resolveColor, useSLayout };
11020
+ export { BADGE_COLORS, BUTTON_COLORS, BUTTON_SIZES, COLOR_KEYS, GNB_COLORS, GNB_COMMON, GNB_FOLD_MS, GNB_HEADER, GNB_MENU, GNB_MENU_WIDTH, GNB_RAIL, GNB_RAIL_WIDTH, ICONS, SActionModal, SBadge, SBarcodeInput, SButton, SCalendar, SCallout, SCard, SCheckbox, SChip, SChipInput, SCircleProgress, SConfirmModal, SDatePicker, SDateRangePicker, SDivider, SDropdownButton, SField, SFilePicker, SForm, SGhostButton, SGnb, SGuide, SIcon, SInput, SKeyValueTable, SLayout, SLayoutContext, SLinearProgress, SLoadingContainer, SLoadingModal, SLoadingViewport, SNumberInput, SPage, SPagination, SPopover, SPopup, SPortal, SRadio, SRadioButton, SRadioGroup, SSelect, SSwitch, STable, STabs, STag, STextLink, STextarea, STimePicker, STimeRangePicker, SToast, SToastContainer, SToggle, STooltip, TAG_COLORS, TAG_SHAPES, TAG_SIZES, TIMEPICKER_SIZES, TIMEPICKER_SIZE_CONFIG, buttonPreset, cn, findGnbAncestors, findGnbRailValue, isColorKey, loading, resolveColor, useSLayout };
10658
11021
  //# sourceMappingURL=index.js.map
10659
11022
  //# sourceMappingURL=index.js.map