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.cjs CHANGED
@@ -4904,73 +4904,70 @@ function SLoadingModal({
4904
4904
  }
4905
4905
 
4906
4906
  // src/components/SGnb/gnb.config.ts
4907
- var belt = (p) => `var(--cmp-gnbBelt-${p})`;
4908
- var box = (p) => `var(--cmp-gnbBox-${p})`;
4909
- var light = (p) => `var(--cmp-light-${p})`;
4910
- var dark = (p) => `var(--cmp-dark-${p})`;
4907
+ var gnb = (p) => `var(--cmp-gnb-${p})`;
4908
+ var box = (p) => `var(--cmp-gnb-box-${p})`;
4909
+ var belt = (p) => `var(--cmp-gnb-belt-${p})`;
4910
+ var light = (p) => `var(--cmp-gnb-light-${p})`;
4911
+ var dark = (p) => `var(--cmp-gnb-dark-${p})`;
4912
+ var HEADER_TOP = {
4913
+ topHeight: gnb("top-height"),
4914
+ topPaddingX: gnb("top-paddingX"),
4915
+ topGap: gnb("top-gap"),
4916
+ logoHeight: gnb("logo-height")
4917
+ };
4911
4918
  var GNB_HEADER = {
4912
- fix: {
4913
- topHeight: belt("top-height"),
4914
- topPaddingX: belt("top-paddingX"),
4915
- topGap: belt("top-gap"),
4916
- logoHeight: belt("logo-height")
4917
- },
4918
- full: {
4919
- topHeight: box("top-height"),
4920
- topPaddingX: box("paddingX"),
4921
- topGap: box("gap"),
4922
- // 상단바 gap (버튼 간 세부 gap 은 --cmp-gnbBox-button-gap)
4923
- logoHeight: box("logo-height")
4924
- }
4919
+ fix: HEADER_TOP,
4920
+ full: HEADER_TOP
4925
4921
  };
4926
4922
  var GNB_MENU = {
4927
4923
  box: {
4928
- // menu.body.padding 전방향 토큰 세로 패딩과 아이템 좌우 여백 양쪽에 쓴다
4929
- bodyPaddingY: box("menu-body-padding"),
4930
- itemMarginX: box("menu-body-padding"),
4931
- itemPaddingY: box("item-paddingY"),
4924
+ // 바디 패딩·세로 패딩·chevron gnb 통합된 공통 토큰
4925
+ bodyPaddingY: gnb("body-padding"),
4926
+ itemMarginX: gnb("body-padding"),
4927
+ // 전방향 토큰 → 아이템 좌우 여백에도 사용
4928
+ itemPaddingY: gnb("item-paddingY"),
4932
4929
  itemPaddingRight: box("item-paddingRight"),
4933
4930
  itemGap: box("item-gap"),
4934
4931
  itemRadius: box("item-radius"),
4935
- itemArrow: box("item-arrow"),
4932
+ itemArrow: gnb("item-chevron-size"),
4936
4933
  depth1PaddingLeft: box("item-depth1-paddingLeft"),
4937
4934
  depth2PaddingLeft: box("item-depth2-paddingLeft"),
4938
4935
  depth3PaddingLeft: box("item-depth3-paddingLeft"),
4939
4936
  depth1Icon: box("item-depth1-icon"),
4940
4937
  depth1FontSize: 14,
4941
- // token: gnbBox.item.typography.bold.fontSize
4938
+ // token: gnb.box.item.typography.selected.fontSize
4942
4939
  depth1LineHeight: 24,
4943
- // token: gnbBox.item.typography.bold.lineHeight
4940
+ // token: gnb.box.item.typography.selected.lineHeight
4944
4941
  depth1FontWeight: 700,
4945
4942
  itemFontSize: 14,
4946
- // token: gnbBox.item.typography.default.fontSize
4943
+ // token: gnb.box.item.typography.default.fontSize
4947
4944
  itemLineHeight: 24
4948
- // token: gnbBox.item.typography.default.lineHeight
4945
+ // token: gnb.box.item.typography.default.lineHeight
4949
4946
  },
4950
4947
  belt: {
4951
- bodyPaddingY: belt("body-paddingY"),
4948
+ bodyPaddingY: gnb("body-padding"),
4952
4949
  itemMarginX: "0px",
4953
4950
  // belt 토큰에 가로 여백 없음 → 풀폭 행 (들여쓰기는 depth*-paddingLeft 전담)
4954
- itemPaddingY: belt("item-paddingY"),
4951
+ itemPaddingY: gnb("item-paddingY"),
4955
4952
  itemPaddingRight: belt("item-paddingRight"),
4956
4953
  itemGap: belt("item-gap"),
4957
4954
  itemRadius: "0px",
4958
4955
  // belt 토큰에 radius 없음 → 풀폭 행
4959
- itemArrow: belt("item-icon"),
4956
+ itemArrow: gnb("item-chevron-size"),
4960
4957
  depth1PaddingLeft: belt("item-depth1-paddingLeft"),
4961
4958
  depth2PaddingLeft: belt("item-depth2-paddingLeft"),
4962
4959
  depth3PaddingLeft: belt("item-depth3-paddingLeft"),
4963
- depth1Icon: box("item-depth1-icon"),
4964
- // belt 토큰에 depth1-icon 없음 → box 값(20) 공유
4960
+ depth1Icon: belt("item-depth1-icon"),
4961
+ // belt 전용 depth1 아이콘 토큰(16)
4965
4962
  depth1FontSize: 13,
4966
- // token: gnbBelt.item.depth1.typography.fontSize
4963
+ // token: gnb.belt.item.depth1.typography.fontSize
4967
4964
  depth1LineHeight: 22,
4968
- // token: gnbBelt.item.depth1.typography.lineHeight
4965
+ // token: gnb.belt.item.depth1.typography.lineHeight
4969
4966
  depth1FontWeight: 700,
4970
4967
  itemFontSize: 12,
4971
- // token: gnbBelt.item.typography.default.fontSize
4968
+ // token: gnb.belt.item.typography.default.fontSize
4972
4969
  itemLineHeight: 20
4973
- // token: gnbBelt.item.typography.default.lineHeight
4970
+ // token: gnb.belt.item.typography.default.lineHeight
4974
4971
  }
4975
4972
  };
4976
4973
  var GNB_COLORS = {
@@ -4983,7 +4980,9 @@ var GNB_COLORS = {
4983
4980
  itemColorDefault: light("item-fg-default"),
4984
4981
  itemColorHover: light("item-fg-hover"),
4985
4982
  itemColorActive: light("item-fg-selected"),
4986
- itemChevron: light("item-chevron")
4983
+ itemChevron: light("item-chevron"),
4984
+ itemIcon: "grey_65",
4985
+ railFgDefault: light("rail-fg-default")
4987
4986
  },
4988
4987
  dark: {
4989
4988
  panelBg: dark("bg"),
@@ -4994,19 +4993,41 @@ var GNB_COLORS = {
4994
4993
  itemColorDefault: dark("item-fg-default"),
4995
4994
  itemColorHover: dark("item-fg-hover"),
4996
4995
  itemColorActive: dark("item-fg-selected"),
4997
- itemChevron: dark("item-chevron")
4996
+ itemChevron: dark("item-chevron"),
4997
+ itemIcon: "grey_55",
4998
+ railFgDefault: dark("rail-fg-default")
4998
4999
  }
4999
5000
  };
5000
5001
  var GNB_COMMON = {
5001
5002
  /** 접힘 레일 폭. header="full" 은 접으면 메뉴만 사라져 레일이 없으므로 fix 전용. */
5002
- foldWidth: belt("fold-width"),
5003
- /**
5004
- * 상단바 아이콘 버튼 사이 간격. fix 레일(세로 스택)의 gap·상하 여백에 쓴다.
5005
- * 레일 전용 여백 토큰이 따로 없어 같은 값(8px)인 이 토큰으로 재해석했다.
5006
- */
5007
- buttonGap: box("button-gap")
5008
- };
5003
+ foldWidth: gnb("fold-width")
5004
+ };
5005
+ var GNB_RAIL = {
5006
+ padding: gnb("rail-padding"),
5007
+ gap: gnb("rail-gap"),
5008
+ itemWidth: gnb("rail-item-width"),
5009
+ itemPaddingTop: gnb("rail-item-paddingTop"),
5010
+ itemPaddingBottom: gnb("rail-item-paddingBottom"),
5011
+ itemRadius: gnb("rail-item-radius"),
5012
+ itemIcon: gnb("rail-item-icon"),
5013
+ // 타이포 (theme.css 에 gnb 타이포 변수 없음 → 토큰 값 직접 명시)
5014
+ itemFontSize: 11,
5015
+ // token: gnb.rail.item.typography.default.fontSize
5016
+ itemLineHeight: 18,
5017
+ // token: gnb.rail.item.typography.default.lineHeight
5018
+ itemFontWeight: 500,
5019
+ // token: gnb.rail.item.typography.default.fontWeight
5020
+ itemFontWeightSelected: 700
5021
+ // token: gnb.rail.item.typography.selected.fontWeight
5022
+ };
5023
+ var GNB_RAIL_WIDTH = 68;
5024
+ var GNB_FOLD_MS = 200;
5025
+ var GNB_EXPAND_MS = 200;
5009
5026
  var GNB_MENU_WIDTH = 240;
5027
+ function findGnbRailValue(items, value) {
5028
+ if (items.some((item) => item.value === value)) return value;
5029
+ return findGnbAncestors(items, value)[0];
5030
+ }
5010
5031
  function findGnbAncestors(items, value) {
5011
5032
  const walk = (list, trail) => {
5012
5033
  for (const item of list) {
@@ -5029,6 +5050,7 @@ function useSLayout() {
5029
5050
  var SLayout = react.forwardRef(function SLayout2({
5030
5051
  type = "box",
5031
5052
  header = "fix",
5053
+ useRail = false,
5032
5054
  folded,
5033
5055
  defaultFolded = false,
5034
5056
  onFoldedChange,
@@ -5047,21 +5069,40 @@ var SLayout = react.forwardRef(function SLayout2({
5047
5069
  },
5048
5070
  [isControlled, onFoldedChange]
5049
5071
  );
5072
+ const [gnbState, setGnbState] = react.useState({ forceFull: false, menuVisible: true });
5073
+ const reportGnbState = react.useCallback(
5074
+ (next) => setGnbState(
5075
+ (prev) => prev.forceFull === next.forceFull && prev.menuVisible === next.menuVisible ? prev : next
5076
+ ),
5077
+ []
5078
+ );
5079
+ const effectiveHeader = header === "full" || gnbState.forceFull ? "full" : "fix";
5050
5080
  const ctx = react.useMemo(
5051
- () => ({ type, header, folded: currentFolded, setFolded }),
5052
- [type, header, currentFolded, setFolded]
5081
+ () => ({
5082
+ type,
5083
+ header: effectiveHeader,
5084
+ useRail,
5085
+ folded: currentFolded,
5086
+ setFolded,
5087
+ reportGnbState
5088
+ }),
5089
+ [type, effectiveHeader, useRail, currentFolded, setFolded, reportGnbState]
5053
5090
  );
5054
- const isFullHeader = header === "full";
5091
+ const isFullHeader = effectiveHeader === "full";
5092
+ const gnbColumnWidth = (useRail ? GNB_RAIL_WIDTH : 0) + (gnbState.menuVisible ? GNB_MENU_WIDTH : 0);
5055
5093
  const frameStyle = isFullHeader ? {
5056
- gridTemplateColumns: `${currentFolded ? 0 : GNB_MENU_WIDTH}px 1fr`,
5057
- gridTemplateRows: `${GNB_HEADER[header].topHeight} 1fr`
5094
+ gridTemplateColumns: `${currentFolded ? 0 : gnbColumnWidth}px 1fr`,
5095
+ gridTemplateRows: `${GNB_HEADER[effectiveHeader].topHeight} 1fr`,
5096
+ // 메뉴가 미끄러지는 동안 페이지 열도 같은 길이로 따라와야 한 덩어리로 읽힌다.
5097
+ // (fix 는 SGnb 가 제 폭을 직접 전환하므로 프레임이 손댈 게 없다)
5098
+ transition: `grid-template-columns ${GNB_FOLD_MS}ms ease-out`
5058
5099
  } : void 0;
5059
5100
  return /* @__PURE__ */ jsxRuntime.jsx(SLayoutContext.Provider, { value: ctx, children: /* @__PURE__ */ jsxRuntime.jsx(
5060
5101
  "div",
5061
5102
  {
5062
5103
  ref,
5063
5104
  "data-type": type,
5064
- "data-header": header,
5105
+ "data-header": effectiveHeader,
5065
5106
  className: cn(
5066
5107
  "box-border h-full overflow-hidden",
5067
5108
  isFullHeader ? "grid" : "flex flex-row",
@@ -5079,8 +5120,10 @@ var SGnb = react.forwardRef(function SGnb2({
5079
5120
  header: headerProp,
5080
5121
  color = "light",
5081
5122
  items,
5123
+ useRail: useRailProp,
5082
5124
  value = "",
5083
5125
  onValueChange,
5126
+ onRailChange,
5084
5127
  folded: foldedProp,
5085
5128
  onFoldChange,
5086
5129
  onLauncherClick,
@@ -5092,8 +5135,10 @@ var SGnb = react.forwardRef(function SGnb2({
5092
5135
  }, ref) {
5093
5136
  const layout = react.useContext(SLayoutContext);
5094
5137
  const type = typeProp ?? layout?.type ?? "box";
5095
- const header = headerProp ?? layout?.header ?? "fix";
5096
5138
  const folded = foldedProp ?? layout?.folded ?? false;
5139
+ const useRail = useRailProp ?? layout?.useRail ?? false;
5140
+ const railHasLeaf = useRail && items.some((item) => !item.children || item.children.length === 0);
5141
+ const header = railHasLeaf ? "full" : headerProp ?? layout?.header ?? "fix";
5097
5142
  const H = GNB_HEADER[header];
5098
5143
  const M = GNB_MENU[type];
5099
5144
  const C = GNB_COLORS[color];
@@ -5101,6 +5146,9 @@ var SGnb = react.forwardRef(function SGnb2({
5101
5146
  () => new Set(findGnbAncestors(items, value))
5102
5147
  );
5103
5148
  const [hoveredKey, setHoveredKey] = react.useState(null);
5149
+ const [activeRail, setActiveRail] = react.useState(
5150
+ () => findGnbRailValue(items, value) ?? items[0]?.value
5151
+ );
5104
5152
  react.useEffect(() => {
5105
5153
  const ancestors = findGnbAncestors(items, value);
5106
5154
  if (ancestors.length === 0) return;
@@ -5111,6 +5159,15 @@ var SGnb = react.forwardRef(function SGnb2({
5111
5159
  return next;
5112
5160
  });
5113
5161
  }, [items, value]);
5162
+ react.useEffect(() => {
5163
+ const owner = findGnbRailValue(items, value);
5164
+ if (owner !== void 0) setActiveRail(owner);
5165
+ }, [items, value]);
5166
+ react.useEffect(() => {
5167
+ setActiveRail(
5168
+ (prev) => prev !== void 0 && items.some((item) => item.value === prev) ? prev : items[0]?.value
5169
+ );
5170
+ }, [items]);
5114
5171
  const toggleExpand = (key) => {
5115
5172
  setExpandedKeys((prev) => {
5116
5173
  const next = new Set(prev);
@@ -5127,16 +5184,58 @@ var SGnb = react.forwardRef(function SGnb2({
5127
5184
  }
5128
5185
  onValueChange?.(item.value);
5129
5186
  };
5187
+ const handleRailClick = (item) => {
5188
+ if (item.disabled) return;
5189
+ setActiveRail(item.value);
5190
+ onRailChange?.(item.value);
5191
+ if (!item.children || item.children.length === 0) onValueChange?.(item.value);
5192
+ };
5130
5193
  const handleFoldToggle = () => {
5131
5194
  const next = !folded;
5132
5195
  onFoldChange?.(next);
5133
5196
  if (foldedProp === void 0) layout?.setFolded(next);
5134
5197
  };
5135
- const renderItem = (item, depth) => {
5198
+ const renderRailItem = (item) => {
5199
+ const isActive = item.value === activeRail;
5200
+ const isHovered = hoveredKey === item.value && !item.disabled;
5201
+ return /* @__PURE__ */ jsxRuntime.jsx("li", { children: /* @__PURE__ */ jsxRuntime.jsxs(
5202
+ "button",
5203
+ {
5204
+ type: "button",
5205
+ disabled: item.disabled,
5206
+ "aria-current": isActive ? "true" : void 0,
5207
+ className: "box-border flex select-none flex-col items-center border-none bg-transparent transition-colors duration-150",
5208
+ style: {
5209
+ width: GNB_RAIL.itemWidth,
5210
+ paddingTop: GNB_RAIL.itemPaddingTop,
5211
+ paddingBottom: GNB_RAIL.itemPaddingBottom,
5212
+ borderRadius: GNB_RAIL.itemRadius,
5213
+ backgroundColor: isActive ? C.itemBgSelected : isHovered ? C.itemBgHover : C.itemBgDefault,
5214
+ color: isActive ? C.itemColorActive : C.railFgDefault,
5215
+ // <button> 의 UA 폰트를 끊고 앱 폰트를 잇는다 (메뉴 아이템과 같은 이유)
5216
+ fontFamily: "inherit",
5217
+ fontSize: GNB_RAIL.itemFontSize,
5218
+ lineHeight: `${GNB_RAIL.itemLineHeight}px`,
5219
+ fontWeight: isActive ? GNB_RAIL.itemFontWeightSelected : GNB_RAIL.itemFontWeight,
5220
+ opacity: item.disabled ? 0.4 : 1,
5221
+ cursor: item.disabled ? "not-allowed" : "pointer"
5222
+ },
5223
+ onClick: () => handleRailClick(item),
5224
+ onMouseEnter: () => setHoveredKey(item.value),
5225
+ onMouseLeave: () => setHoveredKey((prev) => prev === item.value ? null : prev),
5226
+ children: [
5227
+ item.icon && /* @__PURE__ */ jsxRuntime.jsx(SIcon, { name: item.icon, size: GNB_RAIL.itemIcon, className: "shrink-0" }),
5228
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "w-full overflow-hidden text-ellipsis whitespace-nowrap text-center", children: item.label })
5229
+ ]
5230
+ }
5231
+ ) }, item.value);
5232
+ };
5233
+ const renderItem = (item, depth, branchHasIcon = false) => {
5136
5234
  const hasChildren = !!(item.children && item.children.length > 0);
5137
5235
  const isSelected = !hasChildren && item.value === value;
5138
5236
  const isExpanded = expandedKeys.has(item.value);
5139
5237
  const isHovered = hoveredKey === item.value && !item.disabled;
5238
+ const iconBranch = depth === 1 ? !!item.icon : branchHasIcon;
5140
5239
  const itemStyle = {
5141
5240
  gap: M.itemGap,
5142
5241
  paddingTop: M.itemPaddingY,
@@ -5173,8 +5272,11 @@ var SGnb = react.forwardRef(function SGnb2({
5173
5272
  onMouseEnter: () => setHoveredKey(item.value),
5174
5273
  onMouseLeave: () => setHoveredKey((prev) => prev === item.value ? null : prev),
5175
5274
  children: [
5176
- depth === 1 && item.icon && /* @__PURE__ */ jsxRuntime.jsx(SIcon, { name: item.icon, size: M.depth1Icon, className: "shrink-0" }),
5177
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap", children: item.label }),
5275
+ depth === 1 && item.icon && /* @__PURE__ */ jsxRuntime.jsx(SIcon, { name: item.icon, size: M.depth1Icon, color: C.itemIcon, className: "shrink-0" }),
5276
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap flex flex-no-wrap items-center", children: [
5277
+ depth > 1 && iconBranch ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-[20px] h-[24px] flex items-center justify-center mr-[8px]", children: "-" }) : null,
5278
+ item.label
5279
+ ] }),
5178
5280
  item.tag !== void 0 && item.tag !== "" && /* @__PURE__ */ jsxRuntime.jsx(
5179
5281
  STag,
5180
5282
  {
@@ -5192,94 +5294,221 @@ var SGnb = react.forwardRef(function SGnb2({
5192
5294
  size: M.itemArrow,
5193
5295
  color: C.itemChevron,
5194
5296
  rotate: isExpanded ? 180 : 0,
5195
- className: "shrink-0 transition-transform duration-150"
5297
+ className: "shrink-0 transition-transform duration-200"
5196
5298
  }
5197
5299
  )
5198
5300
  ]
5199
5301
  }
5200
5302
  ),
5201
- hasChildren && isExpanded && /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "m-0 flex list-none flex-col p-0", children: item.children.map((child) => renderItem(child, depth + 1)) })
5303
+ hasChildren && /* @__PURE__ */ jsxRuntime.jsx(
5304
+ "div",
5305
+ {
5306
+ className: "grid",
5307
+ style: {
5308
+ gridTemplateRows: isExpanded ? "1fr" : "0fr",
5309
+ transition: `grid-template-rows ${GNB_EXPAND_MS}ms ease-out`
5310
+ },
5311
+ children: /* @__PURE__ */ jsxRuntime.jsx(
5312
+ "ul",
5313
+ {
5314
+ className: "m-0 flex min-h-0 list-none flex-col overflow-hidden p-0",
5315
+ style: {
5316
+ visibility: isExpanded ? "visible" : "hidden",
5317
+ transition: `visibility 0s linear ${isExpanded ? "0s" : `${GNB_EXPAND_MS}ms`}`
5318
+ },
5319
+ children: item.children.map((child) => renderItem(child, depth + 1, iconBranch))
5320
+ }
5321
+ )
5322
+ }
5323
+ )
5202
5324
  ] }, item.value)
5203
5325
  );
5204
5326
  };
5327
+ const railItems = useRail ? items : [];
5328
+ const menuItems = useRail ? items.find((item) => item.value === activeRail)?.children ?? [] : items;
5329
+ const showMenu = !useRail || menuItems.length > 0;
5330
+ const bodyWidth = (useRail ? GNB_RAIL_WIDTH : 0) + (showMenu ? GNB_MENU_WIDTH : 0);
5331
+ react.useEffect(() => {
5332
+ layout?.reportGnbState({ forceFull: railHasLeaf, menuVisible: showMenu });
5333
+ }, [layout, railHasLeaf, showMenu]);
5205
5334
  const railFolded = folded && header === "fix";
5206
- const rootWidth = header === "full" ? "100%" : folded ? GNB_COMMON.foldWidth : GNB_MENU_WIDTH;
5335
+ const rootWidth = header === "full" ? "100%" : folded ? GNB_COMMON.foldWidth : bodyWidth;
5207
5336
  const rootStyle = {
5208
5337
  width: rootWidth,
5338
+ transition: `width ${GNB_FOLD_MS}ms ease-out`,
5209
5339
  ...header === "full" ? { gridArea: "1 / 1 / -1 / -1", pointerEvents: "none" } : { backgroundColor: C.panelBg },
5210
5340
  ...style
5211
5341
  };
5342
+ const hideAfterSlide = (hidden) => `visibility 0s linear ${hidden ? `${GNB_FOLD_MS}ms` : "0s"}`;
5343
+ const foldInline = !!logo;
5344
+ const sideFoldStyle = {
5345
+ opacity: railFolded ? 0 : 1,
5346
+ position: railFolded ? "absolute" : "static",
5347
+ visibility: railFolded ? "hidden" : "visible",
5348
+ transition: `opacity ${GNB_FOLD_MS}ms ease-out, ${hideAfterSlide(railFolded)}`
5349
+ };
5212
5350
  return /* @__PURE__ */ jsxRuntime.jsxs(
5213
5351
  "div",
5214
5352
  {
5215
5353
  ref,
5216
- className: cn("box-border flex h-full shrink-0 flex-col", className),
5354
+ className: cn("box-border flex h-full shrink-0 flex-col overflow-hidden", className),
5217
5355
  style: rootStyle,
5218
5356
  "data-type": type,
5219
5357
  "data-header": header,
5220
5358
  "data-color": color,
5221
5359
  ...rest,
5222
5360
  children: [
5223
- /* @__PURE__ */ jsxRuntime.jsxs(
5361
+ /* @__PURE__ */ jsxRuntime.jsx(
5224
5362
  "div",
5225
5363
  {
5226
- className: cn("box-border flex items-center", railFolded && "flex-col"),
5364
+ className: "relative box-border flex shrink-0 items-center overflow-hidden",
5227
5365
  style: {
5228
- gap: railFolded ? GNB_COMMON.buttonGap : H.topGap,
5229
- height: railFolded ? "auto" : H.topHeight,
5230
- padding: railFolded ? `${GNB_COMMON.buttonGap} 0` : `0 ${H.topPaddingX}`,
5366
+ height: H.topHeight,
5367
+ // 인라인 간격은 컨테이너 gap 대신 각 요소 margin 으로 준다 — 런처↔폴드(8px)와
5368
+ // 폴드↔로고(top-gap)를 다르게 두기 위함. 접히면 런처·로고가 흐름에서 빠져 margin 도 무의미해진다.
5369
+ padding: `0 ${railFolded ? GNB_RAIL.padding : H.topPaddingX}`,
5231
5370
  borderBottom: `1px solid ${C.topBorder}`,
5232
5371
  backgroundColor: C.panelBg,
5233
5372
  pointerEvents: "auto",
5373
+ transition: `padding ${GNB_FOLD_MS}ms ease-out`,
5234
5374
  // 아이콘 버튼은 자체 ghostButton 토큰을 쓰므로, 이 색은 logo 슬롯 상속용
5235
5375
  color: C.itemColorDefault
5236
5376
  },
5237
- children: [
5238
- onLauncherClick && /* @__PURE__ */ jsxRuntime.jsx(
5239
- SGhostButton,
5240
- {
5241
- icon: "launcher",
5242
- size: "xs",
5243
- intent: color === "dark" ? "inverse" : "default",
5244
- ariaLabel: "app launcher",
5245
- onClick: onLauncherClick,
5246
- className: "shrink-0"
5247
- }
5248
- ),
5249
- !railFolded && logo && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center", style: { height: H.logoHeight }, children: logo }),
5250
- /* @__PURE__ */ jsxRuntime.jsx(
5251
- SGhostButton,
5252
- {
5253
- icon: "sidebar",
5254
- size: "xs",
5255
- intent: color === "dark" ? "inverse" : "default",
5256
- ariaLabel: "toggle navigation",
5257
- ariaPressed: folded,
5258
- onClick: handleFoldToggle,
5259
- className: cn("shrink-0", !railFolded && "ml-auto")
5260
- }
5261
- )
5262
- ]
5377
+ children: foldInline ? (
5378
+ // 런처(있을 때)·폴드·로고를 흐름에 나란히 둔다 → 폴드 버튼이 (런처와) 로고 사이에 선다.
5379
+ // 접히면 런처·로고가 페이드하며 흐름에서 빠지고(sideFoldStyle), 폴드 버튼만 남아 레일 가운데에 선다.
5380
+ /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5381
+ onLauncherClick && /* @__PURE__ */ jsxRuntime.jsx(
5382
+ SGhostButton,
5383
+ {
5384
+ icon: "launcher",
5385
+ size: "xs",
5386
+ intent: color === "dark" ? "inverse" : "default",
5387
+ ariaLabel: "app launcher",
5388
+ onClick: onLauncherClick,
5389
+ className: "shrink-0",
5390
+ style: { ...sideFoldStyle, marginRight: "8px" }
5391
+ }
5392
+ ),
5393
+ /* @__PURE__ */ jsxRuntime.jsx(
5394
+ SGhostButton,
5395
+ {
5396
+ icon: "sidebar",
5397
+ size: "xs",
5398
+ intent: color === "dark" ? "inverse" : "default",
5399
+ ariaLabel: "toggle navigation",
5400
+ ariaPressed: folded,
5401
+ onClick: handleFoldToggle,
5402
+ className: "shrink-0"
5403
+ }
5404
+ ),
5405
+ /* @__PURE__ */ jsxRuntime.jsx(
5406
+ "div",
5407
+ {
5408
+ className: "flex shrink-0 items-center",
5409
+ style: { height: H.logoHeight, marginLeft: H.topGap, ...sideFoldStyle },
5410
+ children: logo
5411
+ }
5412
+ )
5413
+ ] })
5414
+ ) : (
5415
+ // 런처·로고는 좌측 그룹(흐름에서 빼 폭 축소 중 폴드 버튼을 밀어내지 않는다),
5416
+ // 폴드 버튼은 우측 끝(ml-auto)이라 폭·패딩이 줄면 저절로 레일 가운데에 선다.
5417
+ /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5418
+ (onLauncherClick || logo) && /* @__PURE__ */ jsxRuntime.jsxs(
5419
+ "div",
5420
+ {
5421
+ className: "absolute inset-y-0 flex items-center",
5422
+ style: {
5423
+ left: H.topPaddingX,
5424
+ gap: H.topGap,
5425
+ opacity: railFolded ? 0 : 1,
5426
+ transition: `opacity ${GNB_FOLD_MS}ms ease-out, ${hideAfterSlide(railFolded)}, transform ${GNB_FOLD_MS}ms ease-out`,
5427
+ // 메뉴와 같은 방향으로 빠지되, 좁은 레일 안에서 꼬리가 비치지 않게 페이드가 거리를 대신한다
5428
+ transform: railFolded ? `translateX(-${H.topPaddingX})` : "none",
5429
+ visibility: railFolded ? "hidden" : "visible"
5430
+ },
5431
+ children: [
5432
+ onLauncherClick && /* @__PURE__ */ jsxRuntime.jsx(
5433
+ SGhostButton,
5434
+ {
5435
+ icon: "launcher",
5436
+ size: "xs",
5437
+ intent: color === "dark" ? "inverse" : "default",
5438
+ ariaLabel: "app launcher",
5439
+ onClick: onLauncherClick,
5440
+ className: "shrink-0"
5441
+ }
5442
+ ),
5443
+ logo && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center", style: { height: H.logoHeight }, children: logo })
5444
+ ]
5445
+ }
5446
+ ),
5447
+ /* @__PURE__ */ jsxRuntime.jsx(
5448
+ SGhostButton,
5449
+ {
5450
+ icon: "sidebar",
5451
+ size: "xs",
5452
+ intent: color === "dark" ? "inverse" : "default",
5453
+ ariaLabel: "toggle navigation",
5454
+ ariaPressed: folded,
5455
+ onClick: handleFoldToggle,
5456
+ className: "ml-auto shrink-0"
5457
+ }
5458
+ )
5459
+ ] })
5460
+ )
5263
5461
  }
5264
5462
  ),
5265
- !folded && /* @__PURE__ */ jsxRuntime.jsx(
5266
- "nav",
5463
+ /* @__PURE__ */ jsxRuntime.jsxs(
5464
+ "div",
5267
5465
  {
5268
- "aria-label": ariaLabel,
5269
- className: "flex flex-col overflow-y-auto",
5466
+ className: "flex flex-row",
5270
5467
  style: {
5271
- width: GNB_MENU_WIDTH,
5468
+ width: bodyWidth,
5272
5469
  alignSelf: "flex-start",
5273
5470
  flexGrow: 1,
5274
5471
  flexShrink: 1,
5275
5472
  flexBasis: "auto",
5276
5473
  minHeight: 0,
5277
- paddingTop: M.bodyPaddingY,
5278
- paddingBottom: M.bodyPaddingY,
5279
- backgroundColor: C.panelBg,
5280
- pointerEvents: "auto"
5474
+ transform: folded ? "translateX(-100%)" : "none",
5475
+ visibility: folded ? "hidden" : "visible",
5476
+ transition: `transform ${GNB_FOLD_MS}ms ease-out, ${hideAfterSlide(folded)}`
5281
5477
  },
5282
- children: /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "m-0 flex list-none flex-col p-0", children: items.map((item) => renderItem(item, 1)) })
5478
+ children: [
5479
+ useRail && // 레일도 내비게이션 landmark — nav 로 세워야 aria-label(레일)이 스크린리더에 노출된다
5480
+ // (role 없는 ul 의 aria-label 은 영역명으로 읽히지 않는다). 메뉴 nav 와는 레이블로 구분된다.
5481
+ /* @__PURE__ */ jsxRuntime.jsx(
5482
+ "nav",
5483
+ {
5484
+ "aria-label": `${ariaLabel} rail`,
5485
+ className: "flex shrink-0 flex-col overflow-y-auto",
5486
+ style: {
5487
+ width: GNB_RAIL_WIDTH,
5488
+ padding: GNB_RAIL.padding,
5489
+ borderRight: `1px solid ${C.topBorder}`,
5490
+ backgroundColor: C.panelBg,
5491
+ pointerEvents: "auto"
5492
+ },
5493
+ children: /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "m-0 flex list-none flex-col p-0", style: { gap: GNB_RAIL.gap }, children: railItems.map(renderRailItem) })
5494
+ }
5495
+ ),
5496
+ showMenu && /* @__PURE__ */ jsxRuntime.jsx(
5497
+ "nav",
5498
+ {
5499
+ "aria-label": ariaLabel,
5500
+ className: "flex shrink-0 flex-col overflow-y-auto",
5501
+ style: {
5502
+ width: GNB_MENU_WIDTH,
5503
+ paddingTop: M.bodyPaddingY,
5504
+ paddingBottom: M.bodyPaddingY,
5505
+ backgroundColor: C.panelBg,
5506
+ pointerEvents: "auto"
5507
+ },
5508
+ children: /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "m-0 flex list-none flex-col p-0", children: menuItems.map((item) => renderItem(item, 1)) })
5509
+ }
5510
+ )
5511
+ ]
5283
5512
  }
5284
5513
  )
5285
5514
  ]
@@ -5293,7 +5522,7 @@ var SPage = react.forwardRef(function SPage2({ className, style, children, ...re
5293
5522
  "main",
5294
5523
  {
5295
5524
  ref,
5296
- className: cn("box-border min-h-0 min-w-0 flex-1 overflow-auto p-24", className),
5525
+ className: cn("box-border min-h-0 min-w-0 flex-1 overflow-auto p-20", className),
5297
5526
  style: { ...gridStyle, ...style },
5298
5527
  ...rest,
5299
5528
  children
@@ -5335,6 +5564,7 @@ function STabs({
5335
5564
  }) {
5336
5565
  const s = TAB_MAIN_SIZE[size];
5337
5566
  const blue = TAB_MAIN_COLORS.selected.border;
5567
+ const isVertical = isSub && vertical;
5338
5568
  const tabRefs = react.useRef([]);
5339
5569
  const moveTo = (target) => {
5340
5570
  if (target < 0 || target >= tabs.length) return;
@@ -5342,19 +5572,17 @@ function STabs({
5342
5572
  tabRefs.current[target]?.focus();
5343
5573
  };
5344
5574
  const containerStyle = { ...style };
5345
- if (!isSub) {
5346
- if (vertical) containerStyle.borderRight = `1px solid ${blue}`;
5347
- else containerStyle.borderBottom = `1px solid ${blue}`;
5348
- }
5575
+ if (!isSub) containerStyle.borderBottom = `1px solid ${blue}`;
5349
5576
  return /* @__PURE__ */ jsxRuntime.jsx(
5350
5577
  "div",
5351
5578
  {
5352
5579
  role: "tablist",
5353
- "aria-orientation": vertical ? "vertical" : "horizontal",
5580
+ "aria-orientation": isVertical ? "vertical" : "horizontal",
5354
5581
  className: cn(
5355
- "inline-flex w-fit",
5356
- vertical ? "flex-col items-stretch" : "flex-row",
5357
- isSub ? vertical ? "gap-[16px]" : "gap-[24px]" : "gap-[4px]",
5582
+ // main(가로): baseline(하단 divider)이 100% 너비로 채워지도록 w-full
5583
+ isSub ? "inline-flex w-fit" : "flex w-full",
5584
+ isVertical ? "flex-col items-stretch" : "flex-row",
5585
+ isSub ? isVertical ? "gap-[16px]" : "gap-[24px]" : "gap-[4px]",
5358
5586
  className
5359
5587
  ),
5360
5588
  style: containerStyle,
@@ -5383,22 +5611,12 @@ function STabs({
5383
5611
  color: selected ? TAB_MAIN_COLORS.selected.text : TAB_MAIN_COLORS.default.text,
5384
5612
  border: `1px solid ${border}`,
5385
5613
  // 폴더형: 미선택 탭은 baseline 위에, 선택 탭은 baseline(파란선)을 1px 덮어 연결된 것처럼 보이게
5386
- ...vertical ? {
5387
- borderTopLeftRadius: 6,
5388
- borderBottomLeftRadius: 6,
5389
- borderTopRightRadius: 0,
5390
- borderBottomRightRadius: 0,
5391
- justifyContent: "flex-start",
5392
- borderRight: selected ? `1px solid ${selectedBg}` : "none",
5393
- marginRight: selected ? -1 : 0
5394
- } : {
5395
- borderTopLeftRadius: 6,
5396
- borderTopRightRadius: 6,
5397
- borderBottomLeftRadius: 0,
5398
- borderBottomRightRadius: 0,
5399
- borderBottom: selected ? `1px solid ${selectedBg}` : "none",
5400
- marginBottom: selected ? -1 : 0
5401
- }
5614
+ borderTopLeftRadius: 6,
5615
+ borderTopRightRadius: 6,
5616
+ borderBottomLeftRadius: 0,
5617
+ borderBottomRightRadius: 0,
5618
+ borderBottom: selected ? `1px solid ${selectedBg}` : "none",
5619
+ marginBottom: selected ? -1 : 0
5402
5620
  };
5403
5621
  }
5404
5622
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -5418,8 +5636,8 @@ function STabs({
5418
5636
  select();
5419
5637
  return;
5420
5638
  }
5421
- const nextKey = vertical ? "ArrowDown" : "ArrowRight";
5422
- const prevKey = vertical ? "ArrowUp" : "ArrowLeft";
5639
+ const nextKey = isVertical ? "ArrowDown" : "ArrowRight";
5640
+ const prevKey = isVertical ? "ArrowUp" : "ArrowLeft";
5423
5641
  if (e.key === nextKey) {
5424
5642
  e.preventDefault();
5425
5643
  moveTo((i + 1) % tabs.length);
@@ -7940,25 +8158,45 @@ function SDatePicker({
7940
8158
  }
7941
8159
  );
7942
8160
  }
8161
+ function splitDateTime(v) {
8162
+ if (!v) return { date: null, hour: "00", minute: "00" };
8163
+ const [date = "", time = ""] = v.split(" ");
8164
+ const [hour = "00", minute = "00"] = time.split(":");
8165
+ return { date: date || null, hour, minute };
8166
+ }
7943
8167
  function RangeCalendar({
7944
8168
  value,
7945
8169
  selectable,
7946
8170
  maxRange,
8171
+ useTimePicker = false,
7947
8172
  onSelect,
7948
8173
  onPendingStartChange,
7949
8174
  onViewChange
7950
8175
  }) {
7951
8176
  const today = todayStr();
7952
- const base = value?.[0] || today;
8177
+ const initial = splitDateTime(value?.[0]);
8178
+ const base = initial.date || today;
7953
8179
  const [y, setY] = react.useState(Number(base.split("-")[0]));
7954
8180
  const [m, setM] = react.useState(Number(base.split("-")[1]));
7955
- const [start, setStart] = react.useState(value?.[0] ?? null);
7956
- const [end, setEnd] = react.useState(value?.[1] ?? null);
8181
+ const [start, setStart] = react.useState(initial.date);
8182
+ const [end, setEnd] = react.useState(splitDateTime(value?.[1]).date);
7957
8183
  const [hover, setHover] = react.useState(null);
8184
+ const [sH, setSH] = react.useState(initial.hour);
8185
+ const [sM, setSM] = react.useState(initial.minute);
8186
+ const [eH, setEH] = react.useState(splitDateTime(value?.[1]).hour);
8187
+ const [eM, setEM] = react.useState(splitDateTime(value?.[1]).minute);
8188
+ const lastEmitted = react.useRef("");
7958
8189
  react.useEffect(() => {
7959
- setStart(value?.[0] ?? null);
7960
- setEnd(value?.[1] ?? null);
7961
- }, [value]);
8190
+ if (useTimePicker && JSON.stringify(value) === lastEmitted.current) return;
8191
+ const s = splitDateTime(value?.[0]);
8192
+ const e = splitDateTime(value?.[1]);
8193
+ setStart(s.date);
8194
+ setEnd(e.date);
8195
+ setSH(s.hour);
8196
+ setSM(s.minute);
8197
+ setEH(e.hour);
8198
+ setEM(e.minute);
8199
+ }, [value, useTimePicker]);
7962
8200
  const setBase = (ny, nm) => {
7963
8201
  setY(ny);
7964
8202
  setM(nm);
@@ -8002,18 +8240,53 @@ function RangeCalendar({
8002
8240
  const hasRange = !!(rStart && rEnd && rStart !== rEnd);
8003
8241
  const inRange = (d) => !!(rStart && rEnd && d > rStart && d < rEnd);
8004
8242
  const RANGE_BG = "var(--cmp-datepicker-calendar-range-bg)";
8243
+ const pad = (v) => String(Number(v) || 0).padStart(2, "0");
8244
+ const emit2 = (dateStart, dateEnd, time) => {
8245
+ const t = time ?? { sH, sM, eH, eM };
8246
+ const range = useTimePicker ? [`${dateStart} ${pad(t.sH)}:${pad(t.sM)}`, `${dateEnd} ${pad(t.eH)}:${pad(t.eM)}`] : [dateStart, dateEnd];
8247
+ lastEmitted.current = JSON.stringify(range);
8248
+ onSelect(range);
8249
+ };
8005
8250
  const click = (d) => {
8006
8251
  if (isDisabled(d)) return;
8007
8252
  if (!start || complete || d < start) {
8008
8253
  setStart(d);
8009
8254
  setEnd(null);
8010
8255
  setHover(null);
8256
+ if (useTimePicker) {
8257
+ setSH("00");
8258
+ setSM("00");
8259
+ setEH("23");
8260
+ setEM("59");
8261
+ }
8011
8262
  onPendingStartChange?.(d);
8012
8263
  return;
8013
8264
  }
8014
8265
  setEnd(d);
8015
8266
  onPendingStartChange?.(null);
8016
- onSelect([start, d]);
8267
+ emit2(start, d, useTimePicker ? { sH, sM, eH: "23", eM: "59" } : void 0);
8268
+ };
8269
+ const changeTime = (which, part, raw) => {
8270
+ const digits = raw.replace(/\D/g, "").slice(0, 2);
8271
+ const max = part === "h" ? 23 : 59;
8272
+ const next = digits !== "" && Number(digits) > max ? String(max) : digits;
8273
+ const nextTime = { sH, sM, eH, eM };
8274
+ if (which === "start") nextTime[part === "h" ? "sH" : "sM"] = next;
8275
+ else nextTime[part === "h" ? "eH" : "eM"] = next;
8276
+ setSH(nextTime.sH);
8277
+ setSM(nextTime.sM);
8278
+ setEH(nextTime.eH);
8279
+ setEM(nextTime.eM);
8280
+ if (start && end) emit2(start, end, nextTime);
8281
+ };
8282
+ const blurTime = (which, part) => {
8283
+ if (which === "start") {
8284
+ if (part === "h") setSH(pad(sH));
8285
+ else setSM(pad(sM));
8286
+ } else {
8287
+ if (part === "h") setEH(pad(eH));
8288
+ else setEM(pad(eM));
8289
+ }
8017
8290
  };
8018
8291
  const GRID_COLS = "grid grid-cols-[repeat(7,var(--cmp-datepicker-calendar-day-width))]";
8019
8292
  const renderPanel = (gy, gm, isLeft) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-[var(--cmp-datepicker-calendar-gap)]", children: [
@@ -8088,35 +8361,120 @@ function RangeCalendar({
8088
8361
  );
8089
8362
  }) })
8090
8363
  ] });
8091
- return /* @__PURE__ */ jsxRuntime.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: [
8092
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex items-center justify-center", children: [
8093
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-center gap-[var(--cmp-datepicker-calendar-gap)] text-[14px] font-medium leading-[24px]", children: [
8094
- /* @__PURE__ */ jsxRuntime.jsx(SGhostButton, { icon: "chevronLeft", size: "xxs", ariaLabel: "prevYear", onClick: goPrevYear }),
8095
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex min-w-[40px] items-center justify-center text-center", children: y }),
8096
- /* @__PURE__ */ jsxRuntime.jsx(SGhostButton, { icon: "chevronRight", size: "xxs", ariaLabel: "nextYear", onClick: goNextYear })
8097
- ] }),
8098
- /* @__PURE__ */ jsxRuntime.jsx(
8099
- "button",
8100
- {
8101
- type: "button",
8102
- onClick: goToday,
8103
- 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",
8104
- children: "\uC624\uB298"
8105
- }
8106
- )
8107
- ] }),
8108
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-[var(--cmp-datepicker-calendar-range-panelGap)]", children: [
8109
- renderPanel(y, m, true),
8110
- /* @__PURE__ */ jsxRuntime.jsx(
8111
- "span",
8112
- {
8113
- className: "w-px shrink-0 self-stretch bg-[var(--cmp-datepicker-calendar-range-divider)]",
8114
- "aria-hidden": true
8115
- }
8364
+ const renderTimeSection = (which, date, suffix) => {
8365
+ const active = !!date;
8366
+ const [yy = "", mm = "", dd = ""] = date ? date.split("-") : [];
8367
+ const hour = which === "start" ? sH : eH;
8368
+ const minute = which === "start" ? sM : eM;
8369
+ const num = (n, strip) => active && n !== "" ? strip ? String(Number(n)) : n : "-";
8370
+ 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";
8371
+ const LABEL = "text-[color:var(--cmp-datepicker-calendar-time-date-label)]";
8372
+ const UNIT = "text-[color:var(--cmp-datepicker-calendar-time-date-unit)]";
8373
+ return /* @__PURE__ */ jsxRuntime.jsxs(
8374
+ "div",
8375
+ {
8376
+ className: cn(
8377
+ "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]",
8378
+ active ? "bg-[var(--cmp-datepicker-calendar-time-bg-selected)]" : "bg-[var(--cmp-datepicker-calendar-time-bg-default)]"
8379
+ ),
8380
+ children: [
8381
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "inline-flex items-center gap-[var(--cmp-datepicker-calendar-time-date-gap)]", children: [
8382
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: LABEL, children: num(yy, false) }),
8383
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: UNIT, children: "\uB144" }),
8384
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: LABEL, children: num(mm, true) }),
8385
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: UNIT, children: "\uC6D4" }),
8386
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: LABEL, children: num(dd, true) }),
8387
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: UNIT, children: "\uC77C" })
8388
+ ] }),
8389
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "inline-flex items-center gap-[var(--cmp-datepicker-calendar-time-unit-gap)]", children: [
8390
+ /* @__PURE__ */ jsxRuntime.jsx(
8391
+ "input",
8392
+ {
8393
+ type: "text",
8394
+ inputMode: "numeric",
8395
+ maxLength: 2,
8396
+ "aria-label": `${suffix} \uC2DC`,
8397
+ disabled: !active,
8398
+ value: active ? hour : "",
8399
+ placeholder: "--",
8400
+ onChange: (e) => changeTime(which, "h", e.target.value),
8401
+ onBlur: () => blurTime(which, "h"),
8402
+ className: INPUT_CLS
8403
+ }
8404
+ ),
8405
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: LABEL, children: ":" }),
8406
+ /* @__PURE__ */ jsxRuntime.jsx(
8407
+ "input",
8408
+ {
8409
+ type: "text",
8410
+ inputMode: "numeric",
8411
+ maxLength: 2,
8412
+ "aria-label": `${suffix} \uBD84`,
8413
+ disabled: !active,
8414
+ value: active ? minute : "",
8415
+ placeholder: "--",
8416
+ onChange: (e) => changeTime(which, "m", e.target.value),
8417
+ onBlur: () => blurTime(which, "m"),
8418
+ className: INPUT_CLS
8419
+ }
8420
+ )
8421
+ ] }),
8422
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: UNIT, children: suffix })
8423
+ ]
8424
+ }
8425
+ );
8426
+ };
8427
+ return /* @__PURE__ */ jsxRuntime.jsxs(
8428
+ "div",
8429
+ {
8430
+ className: cn(
8431
+ "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",
8432
+ useTimePicker ? "h-auto" : "h-[364px]"
8116
8433
  ),
8117
- renderPanel(nextMonthYM.year, nextMonthYM.month, false)
8118
- ] })
8119
- ] });
8434
+ children: [
8435
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex items-center justify-center", children: [
8436
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-center gap-[var(--cmp-datepicker-calendar-gap)] text-[14px] font-medium leading-[24px]", children: [
8437
+ /* @__PURE__ */ jsxRuntime.jsx(SGhostButton, { icon: "chevronLeft", size: "xxs", ariaLabel: "prevYear", onClick: goPrevYear }),
8438
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex min-w-[40px] items-center justify-center text-center", children: y }),
8439
+ /* @__PURE__ */ jsxRuntime.jsx(SGhostButton, { icon: "chevronRight", size: "xxs", ariaLabel: "nextYear", onClick: goNextYear })
8440
+ ] }),
8441
+ /* @__PURE__ */ jsxRuntime.jsx(
8442
+ "button",
8443
+ {
8444
+ type: "button",
8445
+ onClick: goToday,
8446
+ 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",
8447
+ children: "\uC624\uB298"
8448
+ }
8449
+ )
8450
+ ] }),
8451
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-[var(--cmp-datepicker-calendar-range-panelGap)]", children: [
8452
+ renderPanel(y, m, true),
8453
+ /* @__PURE__ */ jsxRuntime.jsx(
8454
+ "span",
8455
+ {
8456
+ className: "w-px shrink-0 self-stretch bg-[var(--cmp-datepicker-calendar-range-divider)]",
8457
+ "aria-hidden": true
8458
+ }
8459
+ ),
8460
+ renderPanel(nextMonthYM.year, nextMonthYM.month, false)
8461
+ ] }),
8462
+ useTimePicker && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
8463
+ /* @__PURE__ */ jsxRuntime.jsx(
8464
+ "span",
8465
+ {
8466
+ className: "h-px w-full bg-[var(--cmp-datepicker-calendar-range-divider)]",
8467
+ "aria-hidden": true
8468
+ }
8469
+ ),
8470
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-[var(--cmp-datepicker-calendar-time-gap)]", children: [
8471
+ renderTimeSection("start", start, "\uBD80\uD130"),
8472
+ renderTimeSection("end", end, "\uAE4C\uC9C0")
8473
+ ] })
8474
+ ] })
8475
+ ]
8476
+ }
8477
+ );
8120
8478
  }
8121
8479
  function SDateRangePicker({
8122
8480
  value,
@@ -8126,6 +8484,7 @@ function SDateRangePicker({
8126
8484
  placeholder = "YYYY-MM-DD ~ YYYY-MM-DD",
8127
8485
  selectable,
8128
8486
  maxRange,
8487
+ useTimePicker = false,
8129
8488
  disabled = false,
8130
8489
  width,
8131
8490
  name,
@@ -8169,8 +8528,9 @@ function SDateRangePicker({
8169
8528
  setPendingStart(null);
8170
8529
  onValueChange?.(r);
8171
8530
  if (rules && rules.length > 0) setRuleError(runRules(r, rules));
8172
- setOpen(false);
8531
+ if (!useTimePicker) setOpen(false);
8173
8532
  };
8533
+ const resolvedPlaceholder = useTimePicker && placeholder === "YYYY-MM-DD ~ YYYY-MM-DD" ? "YYYY-MM-DD HH:mm ~ YYYY-MM-DD HH:mm" : placeholder;
8174
8534
  return /* @__PURE__ */ jsxRuntime.jsx(
8175
8535
  SField,
8176
8536
  {
@@ -8237,7 +8597,7 @@ function SDateRangePicker({
8237
8597
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 flex-1 truncate text-right", children: startText }),
8238
8598
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "px-[4px]", children: "~" }),
8239
8599
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 flex-1 truncate text-left", children: endText })
8240
- ] }) : placeholder
8600
+ ] }) : resolvedPlaceholder
8241
8601
  }
8242
8602
  )
8243
8603
  ]
@@ -8256,6 +8616,7 @@ function SDateRangePicker({
8256
8616
  value: value ?? null,
8257
8617
  selectable,
8258
8618
  maxRange,
8619
+ useTimePicker,
8259
8620
  onSelect: select,
8260
8621
  onPendingStartChange: setPendingStart,
8261
8622
  onViewChange
@@ -10290,7 +10651,9 @@ function RangeTimeSelector({
10290
10651
  };
10291
10652
  const commit = (side, hour, minute, direction = "unknown") => {
10292
10653
  const next = formatTimeValue(hour, minute);
10293
- onChange(normalizeNextRange(side === "start" ? [next, range[1]] : [range[0], next], side, direction));
10654
+ onChange(
10655
+ normalizeNextRange(side === "start" ? [next, range[1]] : [range[0], next], side, direction)
10656
+ );
10294
10657
  };
10295
10658
  const getTimeMinutes2 = (time) => {
10296
10659
  const { hour, minute } = parseTimeValue(time);
@@ -10688,9 +11051,12 @@ exports.BUTTON_SIZES = BUTTON_SIZES;
10688
11051
  exports.COLOR_KEYS = COLOR_KEYS;
10689
11052
  exports.GNB_COLORS = GNB_COLORS;
10690
11053
  exports.GNB_COMMON = GNB_COMMON;
11054
+ exports.GNB_FOLD_MS = GNB_FOLD_MS;
10691
11055
  exports.GNB_HEADER = GNB_HEADER;
10692
11056
  exports.GNB_MENU = GNB_MENU;
10693
11057
  exports.GNB_MENU_WIDTH = GNB_MENU_WIDTH;
11058
+ exports.GNB_RAIL = GNB_RAIL;
11059
+ exports.GNB_RAIL_WIDTH = GNB_RAIL_WIDTH;
10694
11060
  exports.ICONS = ICONS;
10695
11061
  exports.SActionModal = SActionModal;
10696
11062
  exports.SBadge = SBadge;
@@ -10753,6 +11119,7 @@ exports.TIMEPICKER_SIZE_CONFIG = TIMEPICKER_SIZE_CONFIG;
10753
11119
  exports.buttonPreset = buttonPreset;
10754
11120
  exports.cn = cn;
10755
11121
  exports.findGnbAncestors = findGnbAncestors;
11122
+ exports.findGnbRailValue = findGnbRailValue;
10756
11123
  exports.isColorKey = isColorKey;
10757
11124
  exports.loading = loading;
10758
11125
  exports.resolveColor = resolveColor;