sellmate-design-system-react 0.3.0 → 0.5.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 (48) hide show
  1. package/README.md +84 -21
  2. package/dist/components/SGnb/SGnb.cjs +238 -79
  3. package/dist/components/SGnb/SGnb.cjs.map +1 -1
  4. package/dist/components/SGnb/SGnb.d.ts +17 -4
  5. package/dist/components/SGnb/SGnb.js +238 -79
  6. package/dist/components/SGnb/SGnb.js.map +1 -1
  7. package/dist/components/SGnb/gnb.config.d.ts +46 -12
  8. package/dist/components/SGnb/index.d.ts +1 -1
  9. package/dist/components/SLayout/SLayout.cjs +20 -19
  10. package/dist/components/SLayout/SLayout.cjs.map +1 -1
  11. package/dist/components/SLayout/SLayout.d.ts +4 -0
  12. package/dist/components/SLayout/SLayout.js +20 -19
  13. package/dist/components/SLayout/SLayout.js.map +1 -1
  14. package/dist/components/SPage/SPage.cjs +21 -20
  15. package/dist/components/SPage/SPage.cjs.map +1 -1
  16. package/dist/components/SPage/SPage.js +21 -20
  17. package/dist/components/SPage/SPage.js.map +1 -1
  18. package/dist/components/STable/STable.cjs +1 -1
  19. package/dist/components/STable/STable.cjs.map +1 -1
  20. package/dist/components/STable/STable.js +1 -1
  21. package/dist/components/STable/STable.js.map +1 -1
  22. package/dist/components/STabs/STabs.cjs +15 -26
  23. package/dist/components/STabs/STabs.cjs.map +1 -1
  24. package/dist/components/STabs/STabs.d.ts +1 -1
  25. package/dist/components/STabs/STabs.js +15 -26
  26. package/dist/components/STabs/STabs.js.map +1 -1
  27. package/dist/components/STimePicker/STimePicker.cjs +3834 -0
  28. package/dist/components/STimePicker/STimePicker.cjs.map +1 -0
  29. package/dist/components/STimePicker/STimePicker.d.ts +50 -0
  30. package/dist/components/STimePicker/STimePicker.js +3811 -0
  31. package/dist/components/STimePicker/STimePicker.js.map +1 -0
  32. package/dist/components/STimePicker/index.d.ts +2 -0
  33. package/dist/components/STimePicker/timepicker.config.d.ts +23 -0
  34. package/dist/components/STimeRangePicker/STimeRangePicker.cjs +3910 -0
  35. package/dist/components/STimeRangePicker/STimeRangePicker.cjs.map +1 -0
  36. package/dist/components/STimeRangePicker/STimeRangePicker.d.ts +53 -0
  37. package/dist/components/STimeRangePicker/STimeRangePicker.js +3887 -0
  38. package/dist/components/STimeRangePicker/STimeRangePicker.js.map +1 -0
  39. package/dist/components/STimeRangePicker/index.d.ts +1 -0
  40. package/dist/components/STimeRangePicker/time-range-picker.config.d.ts +9 -0
  41. package/dist/index.cjs +1114 -108
  42. package/dist/index.cjs.map +1 -1
  43. package/dist/index.d.ts +3 -0
  44. package/dist/index.js +1107 -109
  45. package/dist/index.js.map +1 -1
  46. package/dist/styles.css +143 -72
  47. package/dist/theme.css +81 -69
  48. package/package.json +4 -1
package/dist/index.cjs CHANGED
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  'use strict';
2
3
 
3
4
  var clsx = require('clsx');
@@ -40,7 +41,6 @@ var RDialog__namespace = /*#__PURE__*/_interopNamespace(RDialog);
40
41
  var RCheckbox__namespace = /*#__PURE__*/_interopNamespace(RCheckbox);
41
42
  var Ariakit__namespace = /*#__PURE__*/_interopNamespace(Ariakit);
42
43
 
43
- // src/lib/cn.ts
44
44
  var twMerge = tailwindMerge.extendTailwindMerge({
45
45
  extend: {
46
46
  classGroups: {
@@ -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,
@@ -5048,13 +5070,17 @@ var SLayout = react.forwardRef(function SLayout2({
5048
5070
  [isControlled, onFoldedChange]
5049
5071
  );
5050
5072
  const ctx = react.useMemo(
5051
- () => ({ type, header, folded: currentFolded, setFolded }),
5052
- [type, header, currentFolded, setFolded]
5073
+ () => ({ type, header, useRail, folded: currentFolded, setFolded }),
5074
+ [type, header, useRail, currentFolded, setFolded]
5053
5075
  );
5054
5076
  const isFullHeader = header === "full";
5077
+ const gnbColumnWidth = useRail ? GNB_RAIL_WIDTH + GNB_MENU_WIDTH : GNB_MENU_WIDTH;
5055
5078
  const frameStyle = isFullHeader ? {
5056
- gridTemplateColumns: `${currentFolded ? 0 : GNB_MENU_WIDTH}px 1fr`,
5057
- gridTemplateRows: `${GNB_HEADER[header].topHeight} 1fr`
5079
+ gridTemplateColumns: `${currentFolded ? 0 : gnbColumnWidth}px 1fr`,
5080
+ gridTemplateRows: `${GNB_HEADER[header].topHeight} 1fr`,
5081
+ // 메뉴가 미끄러지는 동안 페이지 열도 같은 길이로 따라와야 한 덩어리로 읽힌다.
5082
+ // (fix 는 SGnb 가 제 폭을 직접 전환하므로 프레임이 손댈 게 없다)
5083
+ transition: `grid-template-columns ${GNB_FOLD_MS}ms ease-out`
5058
5084
  } : void 0;
5059
5085
  return /* @__PURE__ */ jsxRuntime.jsx(SLayoutContext.Provider, { value: ctx, children: /* @__PURE__ */ jsxRuntime.jsx(
5060
5086
  "div",
@@ -5079,8 +5105,10 @@ var SGnb = react.forwardRef(function SGnb2({
5079
5105
  header: headerProp,
5080
5106
  color = "light",
5081
5107
  items,
5108
+ useRail: useRailProp,
5082
5109
  value = "",
5083
5110
  onValueChange,
5111
+ onRailChange,
5084
5112
  folded: foldedProp,
5085
5113
  onFoldChange,
5086
5114
  onLauncherClick,
@@ -5094,6 +5122,7 @@ var SGnb = react.forwardRef(function SGnb2({
5094
5122
  const type = typeProp ?? layout?.type ?? "box";
5095
5123
  const header = headerProp ?? layout?.header ?? "fix";
5096
5124
  const folded = foldedProp ?? layout?.folded ?? false;
5125
+ const useRail = useRailProp ?? layout?.useRail ?? false;
5097
5126
  const H = GNB_HEADER[header];
5098
5127
  const M = GNB_MENU[type];
5099
5128
  const C = GNB_COLORS[color];
@@ -5101,6 +5130,9 @@ var SGnb = react.forwardRef(function SGnb2({
5101
5130
  () => new Set(findGnbAncestors(items, value))
5102
5131
  );
5103
5132
  const [hoveredKey, setHoveredKey] = react.useState(null);
5133
+ const [activeRail, setActiveRail] = react.useState(
5134
+ () => findGnbRailValue(items, value) ?? items[0]?.value
5135
+ );
5104
5136
  react.useEffect(() => {
5105
5137
  const ancestors = findGnbAncestors(items, value);
5106
5138
  if (ancestors.length === 0) return;
@@ -5111,6 +5143,15 @@ var SGnb = react.forwardRef(function SGnb2({
5111
5143
  return next;
5112
5144
  });
5113
5145
  }, [items, value]);
5146
+ react.useEffect(() => {
5147
+ const owner = findGnbRailValue(items, value);
5148
+ if (owner !== void 0) setActiveRail(owner);
5149
+ }, [items, value]);
5150
+ react.useEffect(() => {
5151
+ setActiveRail(
5152
+ (prev) => prev !== void 0 && items.some((item) => item.value === prev) ? prev : items[0]?.value
5153
+ );
5154
+ }, [items]);
5114
5155
  const toggleExpand = (key) => {
5115
5156
  setExpandedKeys((prev) => {
5116
5157
  const next = new Set(prev);
@@ -5127,16 +5168,58 @@ var SGnb = react.forwardRef(function SGnb2({
5127
5168
  }
5128
5169
  onValueChange?.(item.value);
5129
5170
  };
5171
+ const handleRailClick = (item) => {
5172
+ if (item.disabled) return;
5173
+ setActiveRail(item.value);
5174
+ onRailChange?.(item.value);
5175
+ if (!item.children || item.children.length === 0) onValueChange?.(item.value);
5176
+ };
5130
5177
  const handleFoldToggle = () => {
5131
5178
  const next = !folded;
5132
5179
  onFoldChange?.(next);
5133
5180
  if (foldedProp === void 0) layout?.setFolded(next);
5134
5181
  };
5135
- const renderItem = (item, depth) => {
5182
+ const renderRailItem = (item) => {
5183
+ const isActive = item.value === activeRail;
5184
+ const isHovered = hoveredKey === item.value && !item.disabled;
5185
+ return /* @__PURE__ */ jsxRuntime.jsx("li", { children: /* @__PURE__ */ jsxRuntime.jsxs(
5186
+ "button",
5187
+ {
5188
+ type: "button",
5189
+ disabled: item.disabled,
5190
+ "aria-current": isActive ? "true" : void 0,
5191
+ className: "box-border flex select-none flex-col items-center border-none bg-transparent transition-colors duration-150",
5192
+ style: {
5193
+ width: GNB_RAIL.itemWidth,
5194
+ paddingTop: GNB_RAIL.itemPaddingTop,
5195
+ paddingBottom: GNB_RAIL.itemPaddingBottom,
5196
+ borderRadius: GNB_RAIL.itemRadius,
5197
+ backgroundColor: isActive ? C.itemBgSelected : isHovered ? C.itemBgHover : C.itemBgDefault,
5198
+ color: isActive ? C.itemColorActive : C.railFgDefault,
5199
+ // <button> 의 UA 폰트를 끊고 앱 폰트를 잇는다 (메뉴 아이템과 같은 이유)
5200
+ fontFamily: "inherit",
5201
+ fontSize: GNB_RAIL.itemFontSize,
5202
+ lineHeight: `${GNB_RAIL.itemLineHeight}px`,
5203
+ fontWeight: isActive ? GNB_RAIL.itemFontWeightSelected : GNB_RAIL.itemFontWeight,
5204
+ opacity: item.disabled ? 0.4 : 1,
5205
+ cursor: item.disabled ? "not-allowed" : "pointer"
5206
+ },
5207
+ onClick: () => handleRailClick(item),
5208
+ onMouseEnter: () => setHoveredKey(item.value),
5209
+ onMouseLeave: () => setHoveredKey((prev) => prev === item.value ? null : prev),
5210
+ children: [
5211
+ item.icon && /* @__PURE__ */ jsxRuntime.jsx(SIcon, { name: item.icon, size: GNB_RAIL.itemIcon, className: "shrink-0" }),
5212
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "w-full overflow-hidden text-ellipsis whitespace-nowrap text-center", children: item.label })
5213
+ ]
5214
+ }
5215
+ ) }, item.value);
5216
+ };
5217
+ const renderItem = (item, depth, branchHasIcon = false) => {
5136
5218
  const hasChildren = !!(item.children && item.children.length > 0);
5137
5219
  const isSelected = !hasChildren && item.value === value;
5138
5220
  const isExpanded = expandedKeys.has(item.value);
5139
5221
  const isHovered = hoveredKey === item.value && !item.disabled;
5222
+ const iconBranch = depth === 1 ? !!item.icon : branchHasIcon;
5140
5223
  const itemStyle = {
5141
5224
  gap: M.itemGap,
5142
5225
  paddingTop: M.itemPaddingY,
@@ -5173,8 +5256,11 @@ var SGnb = react.forwardRef(function SGnb2({
5173
5256
  onMouseEnter: () => setHoveredKey(item.value),
5174
5257
  onMouseLeave: () => setHoveredKey((prev) => prev === item.value ? null : prev),
5175
5258
  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 }),
5259
+ depth === 1 && item.icon && /* @__PURE__ */ jsxRuntime.jsx(SIcon, { name: item.icon, size: M.depth1Icon, color: C.itemIcon, className: "shrink-0" }),
5260
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap flex flex-no-wrap items-center", children: [
5261
+ depth > 1 && iconBranch ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-[20px] h-[24px] flex items-center justify-center mr-[8px]", children: "-" }) : null,
5262
+ item.label
5263
+ ] }),
5178
5264
  item.tag !== void 0 && item.tag !== "" && /* @__PURE__ */ jsxRuntime.jsx(
5179
5265
  STag,
5180
5266
  {
@@ -5192,28 +5278,53 @@ var SGnb = react.forwardRef(function SGnb2({
5192
5278
  size: M.itemArrow,
5193
5279
  color: C.itemChevron,
5194
5280
  rotate: isExpanded ? 180 : 0,
5195
- className: "shrink-0 transition-transform duration-150"
5281
+ className: "shrink-0 transition-transform duration-200"
5196
5282
  }
5197
5283
  )
5198
5284
  ]
5199
5285
  }
5200
5286
  ),
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)) })
5287
+ hasChildren && /* @__PURE__ */ jsxRuntime.jsx(
5288
+ "div",
5289
+ {
5290
+ className: "grid",
5291
+ style: {
5292
+ gridTemplateRows: isExpanded ? "1fr" : "0fr",
5293
+ transition: `grid-template-rows ${GNB_EXPAND_MS}ms ease-out`
5294
+ },
5295
+ children: /* @__PURE__ */ jsxRuntime.jsx(
5296
+ "ul",
5297
+ {
5298
+ className: "m-0 flex min-h-0 list-none flex-col overflow-hidden p-0",
5299
+ style: {
5300
+ visibility: isExpanded ? "visible" : "hidden",
5301
+ transition: `visibility 0s linear ${isExpanded ? "0s" : `${GNB_EXPAND_MS}ms`}`
5302
+ },
5303
+ children: item.children.map((child) => renderItem(child, depth + 1, iconBranch))
5304
+ }
5305
+ )
5306
+ }
5307
+ )
5202
5308
  ] }, item.value)
5203
5309
  );
5204
5310
  };
5311
+ const railItems = useRail ? items : [];
5312
+ const menuItems = useRail ? items.find((item) => item.value === activeRail)?.children ?? [] : items;
5313
+ const bodyWidth = useRail ? GNB_RAIL_WIDTH + GNB_MENU_WIDTH : GNB_MENU_WIDTH;
5205
5314
  const railFolded = folded && header === "fix";
5206
- const rootWidth = header === "full" ? "100%" : folded ? GNB_COMMON.foldWidth : GNB_MENU_WIDTH;
5315
+ const rootWidth = header === "full" ? "100%" : folded ? GNB_COMMON.foldWidth : bodyWidth;
5207
5316
  const rootStyle = {
5208
5317
  width: rootWidth,
5318
+ transition: `width ${GNB_FOLD_MS}ms ease-out`,
5209
5319
  ...header === "full" ? { gridArea: "1 / 1 / -1 / -1", pointerEvents: "none" } : { backgroundColor: C.panelBg },
5210
5320
  ...style
5211
5321
  };
5322
+ const hideAfterSlide = (hidden) => `visibility 0s linear ${hidden ? `${GNB_FOLD_MS}ms` : "0s"}`;
5212
5323
  return /* @__PURE__ */ jsxRuntime.jsxs(
5213
5324
  "div",
5214
5325
  {
5215
5326
  ref,
5216
- className: cn("box-border flex h-full shrink-0 flex-col", className),
5327
+ className: cn("box-border flex h-full shrink-0 flex-col overflow-hidden", className),
5217
5328
  style: rootStyle,
5218
5329
  "data-type": type,
5219
5330
  "data-header": header,
@@ -5223,30 +5334,47 @@ var SGnb = react.forwardRef(function SGnb2({
5223
5334
  /* @__PURE__ */ jsxRuntime.jsxs(
5224
5335
  "div",
5225
5336
  {
5226
- className: cn("box-border flex items-center", railFolded && "flex-col"),
5337
+ className: "relative box-border flex shrink-0 items-center overflow-hidden",
5227
5338
  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}`,
5339
+ height: H.topHeight,
5340
+ padding: `0 ${railFolded ? GNB_RAIL.padding : H.topPaddingX}`,
5231
5341
  borderBottom: `1px solid ${C.topBorder}`,
5232
5342
  backgroundColor: C.panelBg,
5233
5343
  pointerEvents: "auto",
5344
+ transition: `padding ${GNB_FOLD_MS}ms ease-out`,
5234
5345
  // 아이콘 버튼은 자체 ghostButton 토큰을 쓰므로, 이 색은 logo 슬롯 상속용
5235
5346
  color: C.itemColorDefault
5236
5347
  },
5237
5348
  children: [
5238
- onLauncherClick && /* @__PURE__ */ jsxRuntime.jsx(
5239
- SGhostButton,
5349
+ (onLauncherClick || logo) && /* @__PURE__ */ jsxRuntime.jsxs(
5350
+ "div",
5240
5351
  {
5241
- icon: "launcher",
5242
- size: "xs",
5243
- intent: color === "dark" ? "inverse" : "default",
5244
- ariaLabel: "app launcher",
5245
- onClick: onLauncherClick,
5246
- className: "shrink-0"
5352
+ className: "absolute inset-y-0 flex items-center",
5353
+ style: {
5354
+ left: H.topPaddingX,
5355
+ gap: H.topGap,
5356
+ opacity: railFolded ? 0 : 1,
5357
+ transition: `opacity ${GNB_FOLD_MS}ms ease-out, ${hideAfterSlide(railFolded)}, transform ${GNB_FOLD_MS}ms ease-out`,
5358
+ // 메뉴와 같은 방향으로 빠지되, 좁은 레일 안에서 꼬리가 비치지 않게 페이드가 거리를 대신한다
5359
+ transform: railFolded ? `translateX(-${H.topPaddingX})` : "none",
5360
+ visibility: railFolded ? "hidden" : "visible"
5361
+ },
5362
+ children: [
5363
+ onLauncherClick && /* @__PURE__ */ jsxRuntime.jsx(
5364
+ SGhostButton,
5365
+ {
5366
+ icon: "launcher",
5367
+ size: "xs",
5368
+ intent: color === "dark" ? "inverse" : "default",
5369
+ ariaLabel: "app launcher",
5370
+ onClick: onLauncherClick,
5371
+ className: "shrink-0"
5372
+ }
5373
+ ),
5374
+ logo && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center", style: { height: H.logoHeight }, children: logo })
5375
+ ]
5247
5376
  }
5248
5377
  ),
5249
- !railFolded && logo && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center", style: { height: H.logoHeight }, children: logo }),
5250
5378
  /* @__PURE__ */ jsxRuntime.jsx(
5251
5379
  SGhostButton,
5252
5380
  {
@@ -5256,30 +5384,61 @@ var SGnb = react.forwardRef(function SGnb2({
5256
5384
  ariaLabel: "toggle navigation",
5257
5385
  ariaPressed: folded,
5258
5386
  onClick: handleFoldToggle,
5259
- className: cn("shrink-0", !railFolded && "ml-auto")
5387
+ className: "ml-auto shrink-0"
5260
5388
  }
5261
5389
  )
5262
5390
  ]
5263
5391
  }
5264
5392
  ),
5265
- !folded && /* @__PURE__ */ jsxRuntime.jsx(
5266
- "nav",
5393
+ /* @__PURE__ */ jsxRuntime.jsxs(
5394
+ "div",
5267
5395
  {
5268
- "aria-label": ariaLabel,
5269
- className: "flex flex-col overflow-y-auto",
5396
+ className: "flex flex-row",
5270
5397
  style: {
5271
- width: GNB_MENU_WIDTH,
5398
+ width: bodyWidth,
5272
5399
  alignSelf: "flex-start",
5273
5400
  flexGrow: 1,
5274
5401
  flexShrink: 1,
5275
5402
  flexBasis: "auto",
5276
5403
  minHeight: 0,
5277
- paddingTop: M.bodyPaddingY,
5278
- paddingBottom: M.bodyPaddingY,
5279
- backgroundColor: C.panelBg,
5280
- pointerEvents: "auto"
5404
+ transform: folded ? "translateX(-100%)" : "none",
5405
+ visibility: folded ? "hidden" : "visible",
5406
+ transition: `transform ${GNB_FOLD_MS}ms ease-out, ${hideAfterSlide(folded)}`
5281
5407
  },
5282
- children: /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "m-0 flex list-none flex-col p-0", children: items.map((item) => renderItem(item, 1)) })
5408
+ children: [
5409
+ useRail && // 레일도 내비게이션 landmark — nav 로 세워야 aria-label(레일)이 스크린리더에 노출된다
5410
+ // (role 없는 ul 의 aria-label 은 영역명으로 읽히지 않는다). 메뉴 nav 와는 레이블로 구분된다.
5411
+ /* @__PURE__ */ jsxRuntime.jsx(
5412
+ "nav",
5413
+ {
5414
+ "aria-label": `${ariaLabel} rail`,
5415
+ className: "flex shrink-0 flex-col overflow-y-auto",
5416
+ style: {
5417
+ width: GNB_RAIL_WIDTH,
5418
+ padding: GNB_RAIL.padding,
5419
+ borderRight: `1px solid ${C.topBorder}`,
5420
+ backgroundColor: C.panelBg,
5421
+ pointerEvents: "auto"
5422
+ },
5423
+ children: /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "m-0 flex list-none flex-col p-0", style: { gap: GNB_RAIL.gap }, children: railItems.map(renderRailItem) })
5424
+ }
5425
+ ),
5426
+ /* @__PURE__ */ jsxRuntime.jsx(
5427
+ "nav",
5428
+ {
5429
+ "aria-label": ariaLabel,
5430
+ className: "flex shrink-0 flex-col overflow-y-auto",
5431
+ style: {
5432
+ width: GNB_MENU_WIDTH,
5433
+ paddingTop: M.bodyPaddingY,
5434
+ paddingBottom: M.bodyPaddingY,
5435
+ backgroundColor: C.panelBg,
5436
+ pointerEvents: "auto"
5437
+ },
5438
+ children: /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "m-0 flex list-none flex-col p-0", children: menuItems.map((item) => renderItem(item, 1)) })
5439
+ }
5440
+ )
5441
+ ]
5283
5442
  }
5284
5443
  )
5285
5444
  ]
@@ -5293,7 +5452,7 @@ var SPage = react.forwardRef(function SPage2({ className, style, children, ...re
5293
5452
  "main",
5294
5453
  {
5295
5454
  ref,
5296
- className: cn("box-border min-h-0 min-w-0 flex-1 overflow-auto p-24", className),
5455
+ className: cn("box-border min-h-0 min-w-0 flex-1 overflow-auto p-20", className),
5297
5456
  style: { ...gridStyle, ...style },
5298
5457
  ...rest,
5299
5458
  children
@@ -5335,6 +5494,7 @@ function STabs({
5335
5494
  }) {
5336
5495
  const s = TAB_MAIN_SIZE[size];
5337
5496
  const blue = TAB_MAIN_COLORS.selected.border;
5497
+ const isVertical = isSub && vertical;
5338
5498
  const tabRefs = react.useRef([]);
5339
5499
  const moveTo = (target) => {
5340
5500
  if (target < 0 || target >= tabs.length) return;
@@ -5342,19 +5502,17 @@ function STabs({
5342
5502
  tabRefs.current[target]?.focus();
5343
5503
  };
5344
5504
  const containerStyle = { ...style };
5345
- if (!isSub) {
5346
- if (vertical) containerStyle.borderRight = `1px solid ${blue}`;
5347
- else containerStyle.borderBottom = `1px solid ${blue}`;
5348
- }
5505
+ if (!isSub) containerStyle.borderBottom = `1px solid ${blue}`;
5349
5506
  return /* @__PURE__ */ jsxRuntime.jsx(
5350
5507
  "div",
5351
5508
  {
5352
5509
  role: "tablist",
5353
- "aria-orientation": vertical ? "vertical" : "horizontal",
5510
+ "aria-orientation": isVertical ? "vertical" : "horizontal",
5354
5511
  className: cn(
5355
- "inline-flex w-fit",
5356
- vertical ? "flex-col items-stretch" : "flex-row",
5357
- isSub ? vertical ? "gap-[16px]" : "gap-[24px]" : "gap-[4px]",
5512
+ // main(가로): baseline(하단 divider)이 100% 너비로 채워지도록 w-full
5513
+ isSub ? "inline-flex w-fit" : "flex w-full",
5514
+ isVertical ? "flex-col items-stretch" : "flex-row",
5515
+ isSub ? isVertical ? "gap-[16px]" : "gap-[24px]" : "gap-[4px]",
5358
5516
  className
5359
5517
  ),
5360
5518
  style: containerStyle,
@@ -5383,22 +5541,12 @@ function STabs({
5383
5541
  color: selected ? TAB_MAIN_COLORS.selected.text : TAB_MAIN_COLORS.default.text,
5384
5542
  border: `1px solid ${border}`,
5385
5543
  // 폴더형: 미선택 탭은 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
- }
5544
+ borderTopLeftRadius: 6,
5545
+ borderTopRightRadius: 6,
5546
+ borderBottomLeftRadius: 0,
5547
+ borderBottomRightRadius: 0,
5548
+ borderBottom: selected ? `1px solid ${selectedBg}` : "none",
5549
+ marginBottom: selected ? -1 : 0
5402
5550
  };
5403
5551
  }
5404
5552
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -5418,8 +5566,8 @@ function STabs({
5418
5566
  select();
5419
5567
  return;
5420
5568
  }
5421
- const nextKey = vertical ? "ArrowDown" : "ArrowRight";
5422
- const prevKey = vertical ? "ArrowUp" : "ArrowLeft";
5569
+ const nextKey = isVertical ? "ArrowDown" : "ArrowRight";
5570
+ const prevKey = isVertical ? "ArrowUp" : "ArrowLeft";
5423
5571
  if (e.key === nextKey) {
5424
5572
  e.preventDefault();
5425
5573
  moveTo((i + 1) % tabs.length);
@@ -6660,7 +6808,7 @@ var STable = react.forwardRef(function STable2({
6660
6808
  ref: scrollRef,
6661
6809
  onScroll: handleScroll,
6662
6810
  className: cn(
6663
- "relative overflow-auto border border-solid border-[color:var(--cmp-table-border-color)]",
6811
+ "bg-white relative overflow-auto border border-solid border-[color:var(--cmp-table-border-color)]",
6664
6812
  radiusClass,
6665
6813
  // 원본 sd-table__scroll-container--loading/--no-data: 로딩·데이터 없음 시 스크롤·인터랙션 차단
6666
6814
  (isLoading || rowCount === 0) && "overflow-hidden pointer-events-none"
@@ -9834,15 +9982,868 @@ var SBarcodeInput = react.forwardRef(
9834
9982
  }
9835
9983
  );
9836
9984
 
9985
+ // src/components/STimePicker/timepicker.config.ts
9986
+ var V2 = (path) => `var(--cmp-timepicker-${path})`;
9987
+ var TIMEPICKER_SIZES = ["sm", "md"];
9988
+ var TIMEPICKER_SIZE_CONFIG = {
9989
+ sm: {
9990
+ height: V2("sm-height"),
9991
+ paddingX: V2("sm-paddingX"),
9992
+ gap: V2("sm-gap"),
9993
+ icon: V2("sm-icon"),
9994
+ radius: V2("sm-radius"),
9995
+ // TODO: component.timepicker 토큰에 typography가 추가되면 CSS 변수로 교체한다.
9996
+ fontSize: 12,
9997
+ lineHeight: 20,
9998
+ minWidth: 80,
9999
+ fieldMinWidth: 128
10000
+ },
10001
+ md: {
10002
+ height: V2("md-height"),
10003
+ paddingX: V2("md-paddingX"),
10004
+ gap: V2("md-gap"),
10005
+ icon: V2("md-icon"),
10006
+ radius: V2("md-radius"),
10007
+ // TODO: component.timepicker 토큰에 typography가 추가되면 CSS 변수로 교체한다.
10008
+ fontSize: 14,
10009
+ lineHeight: 24,
10010
+ minWidth: 96,
10011
+ fieldMinWidth: 160
10012
+ }
10013
+ };
10014
+ function clampTimePart(value, min, max) {
10015
+ if (!Number.isFinite(value)) return min;
10016
+ return Math.min(Math.max(value, min), max);
10017
+ }
10018
+ function padTimePart(value) {
10019
+ return String(value).padStart(2, "0");
10020
+ }
10021
+ function parseTimeValue(value) {
10022
+ const match = /^(\d{1,2}):(\d{1,2})$/.exec(value ?? "");
10023
+ if (!match) return { hour: 0, minute: 0 };
10024
+ return {
10025
+ hour: clampTimePart(Number(match[1]), 0, 23),
10026
+ minute: clampTimePart(Number(match[2]), 0, 59)
10027
+ };
10028
+ }
10029
+ function formatTimeValue(hour, minute) {
10030
+ return `${padTimePart(clampTimePart(hour, 0, 23))}:${padTimePart(clampTimePart(minute, 0, 59))}`;
10031
+ }
10032
+ function formatTimeDisplay(value, type) {
10033
+ if (value == null || value === "") return "";
10034
+ const { hour, minute } = parseTimeValue(value);
10035
+ if (type === "default") return formatTimeValue(hour, minute);
10036
+ const meridiem = hour >= 12 ? "\uC624\uD6C4" : "\uC624\uC804";
10037
+ const displayHour = hour % 12 === 0 ? 12 : hour % 12;
10038
+ return `${meridiem} ${padTimePart(displayHour)}:${padTimePart(minute)}`;
10039
+ }
10040
+ function TimeSelector({
10041
+ value,
10042
+ useMeridiem,
10043
+ minuteStep,
10044
+ onChange
10045
+ }) {
10046
+ const parsed = react.useMemo(() => parseTimeValue(value), [value]);
10047
+ const [hour, setHour] = react.useState(parsed.hour);
10048
+ const [minute, setMinute] = react.useState(parsed.minute);
10049
+ const [editingPart, setEditingPart] = react.useState(null);
10050
+ const [inputDraft, setInputDraft] = react.useState({});
10051
+ react.useEffect(() => {
10052
+ setHour(parsed.hour);
10053
+ setMinute(parsed.minute);
10054
+ }, [parsed.hour, parsed.minute]);
10055
+ const meridiem = hour >= 12 ? "PM" : "AM";
10056
+ const displayHour = useMeridiem ? hour % 12 === 0 ? 12 : hour % 12 : hour;
10057
+ const commit = (nextHour, nextMinute) => {
10058
+ const h = clampTimePart(nextHour, 0, 23);
10059
+ const m = clampTimePart(nextMinute, 0, 59);
10060
+ setHour(h);
10061
+ setMinute(m);
10062
+ onChange(formatTimeValue(h, m));
10063
+ };
10064
+ const setMeridiem = (next) => {
10065
+ if (next === meridiem) return;
10066
+ commit(next === "AM" ? hour - 12 : hour + 12, minute);
10067
+ };
10068
+ const changeMinute = (delta) => {
10069
+ const next = minute + delta;
10070
+ if (next > 59) {
10071
+ commit((hour + 1) % 24, next - 60);
10072
+ return;
10073
+ }
10074
+ if (next < 0) {
10075
+ commit((hour + 23) % 24, next + 60);
10076
+ return;
10077
+ }
10078
+ commit(hour, next);
10079
+ };
10080
+ const changeHour = (delta) => commit((hour + delta + 24) % 24, minute);
10081
+ const sanitizeInput = (next) => next.replace(/\D/g, "").slice(0, 2);
10082
+ const beginEdit = (part, currentValue) => {
10083
+ setEditingPart(part);
10084
+ setInputDraft((prev) => ({ ...prev, [part]: String(currentValue).padStart(2, "0") }));
10085
+ };
10086
+ const commitHourDraft = (draft) => {
10087
+ const clamped = clampTimePart(Number(draft), useMeridiem ? 1 : 0, useMeridiem ? 12 : 23);
10088
+ const nextHour = useMeridiem ? meridiem === "PM" ? clamped === 12 ? 12 : clamped + 12 : clamped === 12 ? 0 : clamped : clamped;
10089
+ commit(nextHour, minute);
10090
+ return clamped;
10091
+ };
10092
+ const commitMinuteDraft = (draft) => {
10093
+ const clamped = clampTimePart(Number(draft), 0, 59);
10094
+ commit(hour, clamped);
10095
+ return clamped;
10096
+ };
10097
+ const endEdit = (part) => {
10098
+ const draft = inputDraft[part];
10099
+ if (part === "hour" && draft != null && draft !== "") {
10100
+ commitHourDraft(draft);
10101
+ }
10102
+ if (part === "minute" && draft != null && draft !== "") {
10103
+ commitMinuteDraft(draft);
10104
+ }
10105
+ setEditingPart(null);
10106
+ setInputDraft({});
10107
+ };
10108
+ const inputStyle = {
10109
+ width: "var(--cmp-timepicker-selector-input-width)",
10110
+ height: "var(--cmp-textinput-sm-height)",
10111
+ borderRadius: "var(--cmp-textinput-sm-radius)",
10112
+ border: "var(--cmp-textinput-borderWidth) solid var(--cmp-textinput-border-default)",
10113
+ color: "var(--cmp-textinput-text-default)",
10114
+ fontSize: 12,
10115
+ lineHeight: "20px"
10116
+ };
10117
+ const renderColumn = (type) => {
10118
+ const isHour = type === "hour";
10119
+ const val = isHour ? displayHour : minute;
10120
+ const displayValue = editingPart === type ? inputDraft[type] ?? "" : String(val).padStart(2, "0");
10121
+ return /* @__PURE__ */ jsxRuntime.jsxs(
10122
+ "div",
10123
+ {
10124
+ className: "flex flex-col items-center justify-center",
10125
+ style: { gap: "var(--cmp-timepicker-selector-time-column-gap)" },
10126
+ children: [
10127
+ /* @__PURE__ */ jsxRuntime.jsx(
10128
+ SGhostButton,
10129
+ {
10130
+ icon: "chevronUp",
10131
+ size: "xs",
10132
+ ariaLabel: `${isHour ? "\uC2DC" : "\uBD84"} \uC99D\uAC00`,
10133
+ onClick: () => isHour ? changeHour(1) : changeMinute(minuteStep)
10134
+ }
10135
+ ),
10136
+ /* @__PURE__ */ jsxRuntime.jsx(
10137
+ "input",
10138
+ {
10139
+ "aria-label": isHour ? "\uC2DC" : "\uBD84",
10140
+ inputMode: "numeric",
10141
+ value: displayValue,
10142
+ placeholder: "-",
10143
+ onFocus: () => beginEdit(type, val),
10144
+ onBlur: () => endEdit(type),
10145
+ onChange: (event) => {
10146
+ const draft = sanitizeInput(event.target.value);
10147
+ setInputDraft((prev) => ({ ...prev, [type]: draft }));
10148
+ if (draft === "") return;
10149
+ const raw = Number(draft);
10150
+ if (isHour) {
10151
+ if (draft.length < 2) return;
10152
+ const clamped2 = commitHourDraft(draft);
10153
+ const displayDraft2 = raw === clamped2 ? draft : String(clamped2);
10154
+ setInputDraft((prev) => ({ ...prev, [type]: displayDraft2 }));
10155
+ return;
10156
+ }
10157
+ if (draft.length < 2) return;
10158
+ const clamped = commitMinuteDraft(draft);
10159
+ const displayDraft = raw === clamped ? draft : String(clamped);
10160
+ setInputDraft((prev) => ({ ...prev, [type]: displayDraft }));
10161
+ },
10162
+ className: "box-border bg-white text-center outline-none",
10163
+ style: inputStyle
10164
+ }
10165
+ ),
10166
+ /* @__PURE__ */ jsxRuntime.jsx(
10167
+ SGhostButton,
10168
+ {
10169
+ icon: "chevronDown",
10170
+ size: "xs",
10171
+ ariaLabel: `${isHour ? "\uC2DC" : "\uBD84"} \uAC10\uC18C`,
10172
+ onClick: () => isHour ? changeHour(-1) : changeMinute(-minuteStep)
10173
+ }
10174
+ )
10175
+ ]
10176
+ }
10177
+ );
10178
+ };
10179
+ return /* @__PURE__ */ jsxRuntime.jsxs(
10180
+ "div",
10181
+ {
10182
+ className: "flex flex-col items-stretch justify-center overflow-hidden",
10183
+ style: {
10184
+ background: "var(--cmp-timepicker-selector-bg)",
10185
+ borderRadius: "var(--cmp-timepicker-selector-radius)",
10186
+ boxShadow: "4px 8px 16px -4px rgba(34,34,34,0.1), 0 0 24px -6px rgba(34,34,34,0.12)"
10187
+ },
10188
+ children: [
10189
+ useMeridiem && /* @__PURE__ */ jsxRuntime.jsx(
10190
+ "div",
10191
+ {
10192
+ className: "flex items-center justify-center",
10193
+ style: {
10194
+ padding: "var(--cmp-timepicker-selector-section-paddingY) var(--cmp-timepicker-selector-section-paddingX)"
10195
+ },
10196
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "isolate flex", children: ["AM", "PM"].map((item, index) => {
10197
+ const selected = item === meridiem;
10198
+ return /* @__PURE__ */ jsxRuntime.jsx(
10199
+ "button",
10200
+ {
10201
+ type: "button",
10202
+ onClick: () => setMeridiem(item),
10203
+ className: cn(
10204
+ "box-border h-[var(--cmp-textinput-sm-height)] w-[61px] cursor-pointer border border-solid bg-white text-center text-[12px] font-medium leading-[20px]",
10205
+ index === 0 ? "rounded-l-[var(--cmp-select-sm-radius)]" : "rounded-r-[var(--cmp-select-sm-radius)]",
10206
+ index === 0 ? "-mr-px" : "",
10207
+ selected ? "z-[1] border-[var(--cmp-timepicker-border-focus)] text-[var(--cmp-timepicker-border-focus)]" : "border-[var(--cmp-timepicker-border-default)] text-[var(--cmp-field-hint-color)]"
10208
+ ),
10209
+ children: item === "AM" ? "\uC624\uC804" : "\uC624\uD6C4"
10210
+ },
10211
+ item
10212
+ );
10213
+ }) })
10214
+ }
10215
+ ),
10216
+ useMeridiem && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-px w-full bg-[var(--sys-color-divider-default)]" }),
10217
+ /* @__PURE__ */ jsxRuntime.jsxs(
10218
+ "div",
10219
+ {
10220
+ className: "flex items-center justify-center",
10221
+ style: {
10222
+ gap: "var(--cmp-timepicker-selector-time-gap)",
10223
+ padding: "var(--cmp-timepicker-selector-section-paddingY) var(--cmp-timepicker-selector-section-paddingX)"
10224
+ },
10225
+ children: [
10226
+ renderColumn("hour"),
10227
+ /* @__PURE__ */ jsxRuntime.jsx(
10228
+ "span",
10229
+ {
10230
+ className: "text-center",
10231
+ style: {
10232
+ width: "var(--cmp-timepicker-selector-colon-size)",
10233
+ color: "var(--cmp-field-hint-color)",
10234
+ fontSize: "var(--cmp-timepicker-selector-colon-size)",
10235
+ lineHeight: "var(--cmp-timepicker-selector-colon-size)"
10236
+ },
10237
+ children: ":"
10238
+ }
10239
+ ),
10240
+ renderColumn("minute")
10241
+ ]
10242
+ }
10243
+ )
10244
+ ]
10245
+ }
10246
+ );
10247
+ }
10248
+ function STimePicker({
10249
+ value,
10250
+ onValueChange,
10251
+ onOpenChange,
10252
+ type = "default",
10253
+ size = "sm",
10254
+ placeholder = "00:00",
10255
+ disabled = false,
10256
+ clearable = false,
10257
+ useMeridiem,
10258
+ minuteStep = 1,
10259
+ width,
10260
+ name,
10261
+ rules,
10262
+ status,
10263
+ label,
10264
+ labelWidth,
10265
+ icon,
10266
+ iconColor,
10267
+ labelTooltip,
10268
+ labelTooltipProps,
10269
+ addonLabel,
10270
+ addonAlign,
10271
+ hint,
10272
+ error,
10273
+ errorMessage,
10274
+ className,
10275
+ style
10276
+ }) {
10277
+ const portalContainer = usePortalContainer();
10278
+ const [open, setOpen] = react.useState(false);
10279
+ const [focused, setFocused] = react.useState(false);
10280
+ const [ruleError, setRuleError] = react.useState("");
10281
+ const dims = TIMEPICKER_SIZE_CONFIG[size] ?? TIMEPICKER_SIZE_CONFIG.sm;
10282
+ const normalized = value ? formatTimeValue(parseTimeValue(value).hour, parseTimeValue(value).minute) : null;
10283
+ const effectiveUseMeridiem = useMeridiem ?? type === "midday";
10284
+ const displayText = formatTimeDisplay(normalized, effectiveUseMeridiem ? "midday" : "default");
10285
+ const hasValue = normalized != null && normalized !== "";
10286
+ useFormField({
10287
+ name,
10288
+ validate: () => {
10289
+ const msg = runRules(value, rules);
10290
+ setRuleError(msg);
10291
+ return msg === "";
10292
+ },
10293
+ resetValidation: () => setRuleError("")
10294
+ });
10295
+ const resolvedError = error || ruleError !== "";
10296
+ const resolvedStatus = status ?? (rules && rules.length > 0 ? ruleError !== "" ? "error" : void 0 : void 0);
10297
+ const commitOpen = (next) => {
10298
+ if (disabled) return;
10299
+ setOpen(next);
10300
+ onOpenChange?.(next);
10301
+ if (!next && rules && rules.length > 0) setRuleError(runRules(value, rules));
10302
+ };
10303
+ const commitValue = (next) => {
10304
+ onValueChange?.(next);
10305
+ if (rules && rules.length > 0) setRuleError(runRules(next, rules));
10306
+ };
10307
+ return /* @__PURE__ */ jsxRuntime.jsx(
10308
+ SField,
10309
+ {
10310
+ label,
10311
+ labelWidth,
10312
+ icon,
10313
+ iconColor,
10314
+ labelTooltip,
10315
+ labelTooltipProps,
10316
+ addonLabel,
10317
+ addonAlign,
10318
+ size,
10319
+ hint,
10320
+ error: resolvedError,
10321
+ status: resolvedStatus,
10322
+ focused: focused || open,
10323
+ errorMessage: ruleError !== "" ? ruleError : errorMessage,
10324
+ width,
10325
+ disabled,
10326
+ className,
10327
+ style: { minWidth: dims.fieldMinWidth, ...style },
10328
+ children: /* @__PURE__ */ jsxRuntime.jsxs(RPopover2__namespace.Root, { open, onOpenChange: commitOpen, children: [
10329
+ /* @__PURE__ */ jsxRuntime.jsx(RPopover2__namespace.Trigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsxs(
10330
+ "button",
10331
+ {
10332
+ type: "button",
10333
+ name,
10334
+ disabled,
10335
+ onFocus: () => setFocused(true),
10336
+ onBlur: () => setFocused(false),
10337
+ className: "flex h-full w-full items-center overflow-hidden bg-transparent outline-none disabled:cursor-not-allowed",
10338
+ style: { padding: `0 ${dims.paddingX}`, gap: dims.gap, borderRadius: dims.radius },
10339
+ children: [
10340
+ /* @__PURE__ */ jsxRuntime.jsx(
10341
+ SIcon,
10342
+ {
10343
+ name: "clockOutline",
10344
+ size: dims.icon,
10345
+ color: disabled ? "var(--cmp-timepicker-icon-disabled)" : "var(--cmp-timepicker-icon-default)",
10346
+ className: "flex-shrink-0"
10347
+ }
10348
+ ),
10349
+ /* @__PURE__ */ jsxRuntime.jsx(
10350
+ "span",
10351
+ {
10352
+ className: "flex-1 truncate text-center",
10353
+ style: {
10354
+ minWidth: dims.minWidth,
10355
+ fontSize: dims.fontSize,
10356
+ lineHeight: `${dims.lineHeight}px`,
10357
+ color: disabled ? "var(--cmp-timepicker-text-disabled)" : hasValue ? "var(--cmp-timepicker-text-default)" : "var(--cmp-field-hint-color)"
10358
+ },
10359
+ children: hasValue ? displayText : placeholder
10360
+ }
10361
+ ),
10362
+ clearable && hasValue && !disabled && /* @__PURE__ */ jsxRuntime.jsx(
10363
+ SGhostButton,
10364
+ {
10365
+ icon: "close",
10366
+ size: "xs",
10367
+ ariaLabel: "\uC2DC\uAC04 \uC9C0\uC6B0\uAE30",
10368
+ onClick: (event) => {
10369
+ event.stopPropagation();
10370
+ commitValue(null);
10371
+ }
10372
+ }
10373
+ )
10374
+ ]
10375
+ }
10376
+ ) }),
10377
+ /* @__PURE__ */ jsxRuntime.jsx(RPopover2__namespace.Portal, { container: portalContainer, children: /* @__PURE__ */ jsxRuntime.jsx(
10378
+ RPopover2__namespace.Content,
10379
+ {
10380
+ side: "bottom",
10381
+ align: "start",
10382
+ sideOffset: 4,
10383
+ className: cn("z-50", FLOATING_SLIDE_ANIM),
10384
+ children: /* @__PURE__ */ jsxRuntime.jsx(
10385
+ TimeSelector,
10386
+ {
10387
+ value: normalized,
10388
+ useMeridiem: effectiveUseMeridiem,
10389
+ minuteStep,
10390
+ onChange: commitValue
10391
+ }
10392
+ )
10393
+ }
10394
+ ) })
10395
+ ] })
10396
+ }
10397
+ );
10398
+ }
10399
+
10400
+ // src/components/STimeRangePicker/time-range-picker.config.ts
10401
+ function getTimeMinutes(value) {
10402
+ const { hour, minute } = parseTimeValue(value);
10403
+ return hour * 60 + minute;
10404
+ }
10405
+ function normalizeTimeRangeValue(value, rangeOrder = "strict") {
10406
+ if (value == null) return null;
10407
+ const normalized = value.map((item) => {
10408
+ const parsed = parseTimeValue(item);
10409
+ return formatTimeValue(parsed.hour, parsed.minute);
10410
+ });
10411
+ if (rangeOrder === "strict" && getTimeMinutes(normalized[0]) > getTimeMinutes(normalized[1])) {
10412
+ return [normalized[0], normalized[0]];
10413
+ }
10414
+ return normalized;
10415
+ }
10416
+ function formatTimeRangeDisplay(value, type) {
10417
+ if (value == null || value[0] === "" || value[1] === "") return "";
10418
+ return `${formatTimeDisplay(value[0], type)} ~ ${formatTimeDisplay(value[1], type)}`;
10419
+ }
10420
+ function RangeTimeSelector({
10421
+ value,
10422
+ useMeridiem,
10423
+ minuteStep,
10424
+ rangeOrder,
10425
+ onChange
10426
+ }) {
10427
+ const range = react.useMemo(() => value ?? ["00:00", "23:59"], [value]);
10428
+ const [editingKey, setEditingKey] = react.useState(null);
10429
+ const [inputDraft, setInputDraft] = react.useState({});
10430
+ const autoAdjustedRef = react.useRef(null);
10431
+ const getParts = (side) => parseTimeValue(side === "start" ? range[0] : range[1]);
10432
+ const getMeridiem = (side) => getParts(side).hour >= 12 ? "PM" : "AM";
10433
+ const getDisplayHour = (side) => {
10434
+ const { hour } = getParts(side);
10435
+ if (!useMeridiem) return hour;
10436
+ const displayHour = hour % 12;
10437
+ return displayHour === 0 ? 12 : displayHour;
10438
+ };
10439
+ const commit = (side, hour, minute, direction = "unknown") => {
10440
+ const next = formatTimeValue(hour, minute);
10441
+ onChange(
10442
+ normalizeNextRange(side === "start" ? [next, range[1]] : [range[0], next], side, direction)
10443
+ );
10444
+ };
10445
+ const getTimeMinutes2 = (time) => {
10446
+ const { hour, minute } = parseTimeValue(time);
10447
+ return hour * 60 + minute;
10448
+ };
10449
+ const normalizeNextRange = (nextRange, changedSide, direction) => {
10450
+ if (rangeOrder === "allow-cross-day") return nextRange;
10451
+ const [start, end] = nextRange;
10452
+ const [currentStart, currentEnd] = range;
10453
+ if (getTimeMinutes2(start) <= getTimeMinutes2(end)) {
10454
+ const shouldKeepSynced = autoAdjustedRef.current?.side === changedSide && currentStart === currentEnd && start !== end && (direction === "unknown" || autoAdjustedRef.current.direction === direction);
10455
+ if (shouldKeepSynced) {
10456
+ return changedSide === "start" ? [start, start] : [end, end];
10457
+ }
10458
+ autoAdjustedRef.current = null;
10459
+ return nextRange;
10460
+ }
10461
+ autoAdjustedRef.current = {
10462
+ side: changedSide,
10463
+ direction: changedSide === "start" ? "increase" : "decrease"
10464
+ };
10465
+ return changedSide === "start" ? [start, start] : [end, end];
10466
+ };
10467
+ const changeHour = (side, delta) => {
10468
+ const { hour, minute } = getParts(side);
10469
+ commit(side, (hour + delta + 24) % 24, minute, delta > 0 ? "increase" : "decrease");
10470
+ };
10471
+ const changeMinute = (side, delta) => {
10472
+ const { hour, minute } = getParts(side);
10473
+ const next = minute + delta;
10474
+ if (next > 59) {
10475
+ commit(side, (hour + 1) % 24, next - 60, delta > 0 ? "increase" : "decrease");
10476
+ return;
10477
+ }
10478
+ if (next < 0) {
10479
+ commit(side, (hour + 23) % 24, next + 60, delta > 0 ? "increase" : "decrease");
10480
+ return;
10481
+ }
10482
+ commit(side, hour, next, delta > 0 ? "increase" : "decrease");
10483
+ };
10484
+ const setMeridiem = (side, next) => {
10485
+ const current = getMeridiem(side);
10486
+ if (current === next) return;
10487
+ const { hour, minute } = getParts(side);
10488
+ commit(side, next === "AM" ? hour - 12 : hour + 12, minute);
10489
+ };
10490
+ const getEditingKey = (side, part) => `${side}-${part}`;
10491
+ const sanitizeInput = (next) => next.replace(/\D/g, "").slice(0, 2);
10492
+ const beginEdit = (side, part, currentValue) => {
10493
+ const key = getEditingKey(side, part);
10494
+ setEditingKey(key);
10495
+ setInputDraft((prev) => ({ ...prev, [key]: String(currentValue).padStart(2, "0") }));
10496
+ };
10497
+ const commitHourDraft = (side, draft) => {
10498
+ const clamped = clampTimePart(Number(draft), useMeridiem ? 1 : 0, useMeridiem ? 12 : 23);
10499
+ const parts = getParts(side);
10500
+ const meridiem = getMeridiem(side);
10501
+ const nextHour = useMeridiem ? meridiem === "PM" ? clamped === 12 ? 12 : clamped + 12 : clamped === 12 ? 0 : clamped : clamped;
10502
+ commit(side, nextHour, parts.minute);
10503
+ return clamped;
10504
+ };
10505
+ const commitMinuteDraft = (side, draft) => {
10506
+ const { hour } = getParts(side);
10507
+ const clamped = clampTimePart(Number(draft), 0, 59);
10508
+ commit(side, hour, clamped);
10509
+ return clamped;
10510
+ };
10511
+ const endEdit = (side, part) => {
10512
+ const key = getEditingKey(side, part);
10513
+ const draft = inputDraft[key];
10514
+ if (part === "hour" && draft != null && draft !== "") {
10515
+ commitHourDraft(side, draft);
10516
+ }
10517
+ if (part === "minute" && draft != null && draft !== "") {
10518
+ commitMinuteDraft(side, draft);
10519
+ }
10520
+ setEditingKey(null);
10521
+ setInputDraft({});
10522
+ };
10523
+ const inputStyle = {
10524
+ width: "var(--cmp-timepicker-selector-input-width)",
10525
+ height: "var(--cmp-textinput-sm-height)",
10526
+ borderRadius: "var(--cmp-textinput-sm-radius)",
10527
+ border: "var(--cmp-textinput-borderWidth) solid var(--cmp-textinput-border-default)",
10528
+ color: "var(--cmp-textinput-text-default)",
10529
+ fontSize: 12,
10530
+ lineHeight: "20px"
10531
+ };
10532
+ const renderColumn = (side, kind) => {
10533
+ const isHour = kind === "hour";
10534
+ const { minute } = getParts(side);
10535
+ const val = isHour ? getDisplayHour(side) : minute;
10536
+ const currentEditingKey = getEditingKey(side, kind);
10537
+ const displayValue = editingKey === currentEditingKey ? inputDraft[currentEditingKey] ?? "" : String(val).padStart(2, "0");
10538
+ return /* @__PURE__ */ jsxRuntime.jsxs(
10539
+ "div",
10540
+ {
10541
+ className: "flex flex-col items-center justify-center",
10542
+ style: { gap: "var(--cmp-timepicker-selector-time-column-gap)" },
10543
+ children: [
10544
+ /* @__PURE__ */ jsxRuntime.jsx(
10545
+ SGhostButton,
10546
+ {
10547
+ icon: "chevronUp",
10548
+ size: "xs",
10549
+ ariaLabel: `${side === "start" ? "\uC2DC\uC791" : "\uC885\uB8CC"} ${isHour ? "\uC2DC" : "\uBD84"} \uC99D\uAC00`,
10550
+ onClick: () => isHour ? changeHour(side, 1) : changeMinute(side, minuteStep)
10551
+ }
10552
+ ),
10553
+ /* @__PURE__ */ jsxRuntime.jsx(
10554
+ "input",
10555
+ {
10556
+ "aria-label": `${side === "start" ? "\uC2DC\uC791" : "\uC885\uB8CC"} ${isHour ? "\uC2DC" : "\uBD84"}`,
10557
+ inputMode: "numeric",
10558
+ value: displayValue,
10559
+ placeholder: "-",
10560
+ onFocus: () => beginEdit(side, kind, val),
10561
+ onBlur: () => endEdit(side, kind),
10562
+ onChange: (event) => {
10563
+ const draft = sanitizeInput(event.target.value);
10564
+ setInputDraft((prev) => ({ ...prev, [currentEditingKey]: draft }));
10565
+ if (draft === "") return;
10566
+ const raw = Number(draft);
10567
+ if (isHour) {
10568
+ if (draft.length < 2) return;
10569
+ const clamped2 = commitHourDraft(side, draft);
10570
+ const displayDraft2 = raw === clamped2 ? draft : String(clamped2);
10571
+ setInputDraft((prev) => ({ ...prev, [currentEditingKey]: displayDraft2 }));
10572
+ return;
10573
+ }
10574
+ if (draft.length < 2) return;
10575
+ const clamped = commitMinuteDraft(side, draft);
10576
+ const displayDraft = raw === clamped ? draft : String(clamped);
10577
+ setInputDraft((prev) => ({ ...prev, [currentEditingKey]: displayDraft }));
10578
+ },
10579
+ className: "box-border bg-white text-center outline-none",
10580
+ style: inputStyle
10581
+ }
10582
+ ),
10583
+ /* @__PURE__ */ jsxRuntime.jsx(
10584
+ SGhostButton,
10585
+ {
10586
+ icon: "chevronDown",
10587
+ size: "xs",
10588
+ ariaLabel: `${side === "start" ? "\uC2DC\uC791" : "\uC885\uB8CC"} ${isHour ? "\uC2DC" : "\uBD84"} \uAC10\uC18C`,
10589
+ onClick: () => isHour ? changeHour(side, -1) : changeMinute(side, -minuteStep)
10590
+ }
10591
+ )
10592
+ ]
10593
+ }
10594
+ );
10595
+ };
10596
+ const renderPanel = (side, title) => /* @__PURE__ */ jsxRuntime.jsxs(
10597
+ "div",
10598
+ {
10599
+ className: "flex flex-col items-center justify-center",
10600
+ style: { gap: "var(--cmp-timepicker-selector-range-section-gap)" },
10601
+ children: [
10602
+ /* @__PURE__ */ jsxRuntime.jsxs(
10603
+ "div",
10604
+ {
10605
+ className: "flex flex-col items-center",
10606
+ style: { gap: "var(--cmp-timepicker-selector-range-heading-gap)" },
10607
+ children: [
10608
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "whitespace-nowrap text-center text-[12px] font-bold leading-[20px] text-[var(--cmp-timepicker-selector-range-heading-color)]", children: title }),
10609
+ useMeridiem && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "isolate flex", children: ["AM", "PM"].map((item, index) => {
10610
+ const selected = item === getMeridiem(side);
10611
+ return /* @__PURE__ */ jsxRuntime.jsx(
10612
+ "button",
10613
+ {
10614
+ type: "button",
10615
+ onClick: () => setMeridiem(side, item),
10616
+ className: cn(
10617
+ "box-border h-[var(--cmp-textinput-sm-height)] w-[61px] cursor-pointer border border-solid bg-white text-center text-[12px] font-medium leading-[20px]",
10618
+ index === 0 ? "rounded-l-[var(--cmp-select-sm-radius)] -mr-px" : "rounded-r-[var(--cmp-select-sm-radius)]",
10619
+ selected ? "z-[1] border-[var(--cmp-timepicker-border-focus)] text-[var(--cmp-timepicker-border-focus)]" : "border-[var(--cmp-timepicker-border-default)] text-[var(--cmp-field-hint-color)]"
10620
+ ),
10621
+ children: item === "AM" ? "\uC624\uC804" : "\uC624\uD6C4"
10622
+ },
10623
+ item
10624
+ );
10625
+ }) })
10626
+ ]
10627
+ }
10628
+ ),
10629
+ /* @__PURE__ */ jsxRuntime.jsxs(
10630
+ "div",
10631
+ {
10632
+ className: "flex items-center justify-center",
10633
+ style: {
10634
+ gap: "var(--cmp-timepicker-selector-time-gap)",
10635
+ padding: "0 var(--cmp-timepicker-selector-section-paddingX)"
10636
+ },
10637
+ children: [
10638
+ renderColumn(side, "hour"),
10639
+ /* @__PURE__ */ jsxRuntime.jsx(
10640
+ "span",
10641
+ {
10642
+ className: "text-center",
10643
+ style: {
10644
+ width: "var(--cmp-timepicker-selector-colon-size)",
10645
+ color: "var(--cmp-field-hint-color)",
10646
+ fontSize: "var(--cmp-timepicker-selector-colon-size)",
10647
+ lineHeight: "var(--cmp-timepicker-selector-colon-size)"
10648
+ },
10649
+ children: ":"
10650
+ }
10651
+ ),
10652
+ renderColumn(side, "minute")
10653
+ ]
10654
+ }
10655
+ )
10656
+ ]
10657
+ }
10658
+ );
10659
+ return /* @__PURE__ */ jsxRuntime.jsxs(
10660
+ "div",
10661
+ {
10662
+ className: "flex items-start justify-center overflow-hidden",
10663
+ style: {
10664
+ gap: "var(--cmp-timepicker-selector-range-panelGap)",
10665
+ padding: "var(--cmp-timepicker-selector-range-paddingXY)",
10666
+ background: "var(--cmp-timepicker-selector-bg)",
10667
+ borderRadius: "var(--cmp-timepicker-selector-radius)",
10668
+ boxShadow: "var(--shadow-spread-sm), var(--shadow-spread-md)"
10669
+ },
10670
+ children: [
10671
+ renderPanel("start", "\uC2DC\uC791 \uC2DC\uAC04"),
10672
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "self-stretch w-px bg-[var(--sys-color-divider-default)]" }),
10673
+ renderPanel("end", "\uC885\uB8CC \uC2DC\uAC04")
10674
+ ]
10675
+ }
10676
+ );
10677
+ }
10678
+ function STimeRangePicker({
10679
+ value,
10680
+ onValueChange,
10681
+ onOpenChange,
10682
+ type = "default",
10683
+ size = "sm",
10684
+ placeholder = "00:00 ~ 23:59",
10685
+ disabled = false,
10686
+ clearable = false,
10687
+ useMeridiem,
10688
+ minuteStep = 1,
10689
+ rangeOrder = "strict",
10690
+ width,
10691
+ name,
10692
+ rules,
10693
+ status,
10694
+ label,
10695
+ labelWidth,
10696
+ icon,
10697
+ iconColor,
10698
+ labelTooltip,
10699
+ labelTooltipProps,
10700
+ addonLabel,
10701
+ addonAlign,
10702
+ hint,
10703
+ error,
10704
+ errorMessage,
10705
+ className,
10706
+ style
10707
+ }) {
10708
+ const portalContainer = usePortalContainer();
10709
+ const [open, setOpen] = react.useState(false);
10710
+ const [focused, setFocused] = react.useState(false);
10711
+ const [ruleError, setRuleError] = react.useState("");
10712
+ const dims = TIMEPICKER_SIZE_CONFIG[size] ?? TIMEPICKER_SIZE_CONFIG.sm;
10713
+ const normalized = normalizeTimeRangeValue(value ?? null, rangeOrder);
10714
+ const effectiveUseMeridiem = useMeridiem ?? type === "midday";
10715
+ const displayText = formatTimeRangeDisplay(
10716
+ normalized,
10717
+ effectiveUseMeridiem ? "midday" : "default"
10718
+ );
10719
+ const hasValue = normalized != null && normalized[0] !== "" && normalized[1] !== "";
10720
+ useFormField({
10721
+ name,
10722
+ validate: () => {
10723
+ const msg = runRules(value, rules);
10724
+ setRuleError(msg);
10725
+ return msg === "";
10726
+ },
10727
+ resetValidation: () => setRuleError("")
10728
+ });
10729
+ const resolvedError = error || ruleError !== "";
10730
+ const resolvedStatus = status ?? (rules && rules.length > 0 ? ruleError !== "" ? "error" : void 0 : void 0);
10731
+ const commitOpen = (next) => {
10732
+ if (disabled) return;
10733
+ setOpen(next);
10734
+ onOpenChange?.(next);
10735
+ if (!next && rules && rules.length > 0) setRuleError(runRules(value, rules));
10736
+ };
10737
+ const commitValue = (next) => {
10738
+ onValueChange?.(next);
10739
+ if (rules && rules.length > 0) setRuleError(runRules(next, rules));
10740
+ };
10741
+ return /* @__PURE__ */ jsxRuntime.jsx(
10742
+ SField,
10743
+ {
10744
+ label,
10745
+ labelWidth,
10746
+ icon,
10747
+ iconColor,
10748
+ labelTooltip,
10749
+ labelTooltipProps,
10750
+ addonLabel,
10751
+ addonAlign,
10752
+ size,
10753
+ hint,
10754
+ error: resolvedError,
10755
+ status: resolvedStatus,
10756
+ focused: focused || open,
10757
+ errorMessage: ruleError !== "" ? ruleError : errorMessage,
10758
+ width,
10759
+ disabled,
10760
+ className,
10761
+ style: { minWidth: dims.fieldMinWidth, ...style },
10762
+ children: /* @__PURE__ */ jsxRuntime.jsxs(RPopover2__namespace.Root, { open, onOpenChange: commitOpen, children: [
10763
+ /* @__PURE__ */ jsxRuntime.jsx(RPopover2__namespace.Trigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsxs(
10764
+ "button",
10765
+ {
10766
+ type: "button",
10767
+ name,
10768
+ disabled,
10769
+ onFocus: () => setFocused(true),
10770
+ onBlur: () => setFocused(false),
10771
+ className: "flex h-full w-full items-center overflow-hidden bg-transparent outline-none disabled:cursor-not-allowed",
10772
+ style: { padding: `0 ${dims.paddingX}`, gap: dims.gap, borderRadius: dims.radius },
10773
+ children: [
10774
+ /* @__PURE__ */ jsxRuntime.jsx(
10775
+ SIcon,
10776
+ {
10777
+ name: "clockOutline",
10778
+ size: dims.icon,
10779
+ color: disabled ? "var(--cmp-timepicker-icon-disabled)" : "var(--cmp-timepicker-icon-default)",
10780
+ className: "flex-shrink-0"
10781
+ }
10782
+ ),
10783
+ /* @__PURE__ */ jsxRuntime.jsx(
10784
+ "span",
10785
+ {
10786
+ className: "flex-1 truncate text-center",
10787
+ style: {
10788
+ minWidth: dims.minWidth,
10789
+ fontSize: dims.fontSize,
10790
+ lineHeight: `${dims.lineHeight}px`,
10791
+ color: disabled ? "var(--cmp-timepicker-text-disabled)" : hasValue ? "var(--cmp-timepicker-text-default)" : "var(--cmp-field-hint-color)"
10792
+ },
10793
+ children: hasValue ? displayText : placeholder
10794
+ }
10795
+ ),
10796
+ clearable && hasValue && !disabled && /* @__PURE__ */ jsxRuntime.jsx(
10797
+ SGhostButton,
10798
+ {
10799
+ icon: "close",
10800
+ size: "xs",
10801
+ ariaLabel: "\uC2DC\uAC04 \uBC94\uC704 \uC9C0\uC6B0\uAE30",
10802
+ onClick: (event) => {
10803
+ event.stopPropagation();
10804
+ commitValue(null);
10805
+ }
10806
+ }
10807
+ )
10808
+ ]
10809
+ }
10810
+ ) }),
10811
+ /* @__PURE__ */ jsxRuntime.jsx(RPopover2__namespace.Portal, { container: portalContainer, children: /* @__PURE__ */ jsxRuntime.jsx(
10812
+ RPopover2__namespace.Content,
10813
+ {
10814
+ side: "bottom",
10815
+ align: "start",
10816
+ sideOffset: 4,
10817
+ className: cn("z-50", FLOATING_SLIDE_ANIM),
10818
+ children: /* @__PURE__ */ jsxRuntime.jsx(
10819
+ RangeTimeSelector,
10820
+ {
10821
+ value: normalized,
10822
+ useMeridiem: effectiveUseMeridiem,
10823
+ minuteStep,
10824
+ rangeOrder,
10825
+ onChange: commitValue
10826
+ }
10827
+ )
10828
+ }
10829
+ ) })
10830
+ ] })
10831
+ }
10832
+ );
10833
+ }
10834
+
9837
10835
  exports.BADGE_COLORS = BADGE_COLORS;
9838
10836
  exports.BUTTON_COLORS = BUTTON_COLORS;
9839
10837
  exports.BUTTON_SIZES = BUTTON_SIZES;
9840
10838
  exports.COLOR_KEYS = COLOR_KEYS;
9841
10839
  exports.GNB_COLORS = GNB_COLORS;
9842
10840
  exports.GNB_COMMON = GNB_COMMON;
10841
+ exports.GNB_FOLD_MS = GNB_FOLD_MS;
9843
10842
  exports.GNB_HEADER = GNB_HEADER;
9844
10843
  exports.GNB_MENU = GNB_MENU;
9845
10844
  exports.GNB_MENU_WIDTH = GNB_MENU_WIDTH;
10845
+ exports.GNB_RAIL = GNB_RAIL;
10846
+ exports.GNB_RAIL_WIDTH = GNB_RAIL_WIDTH;
9846
10847
  exports.ICONS = ICONS;
9847
10848
  exports.SActionModal = SActionModal;
9848
10849
  exports.SBadge = SBadge;
@@ -9891,6 +10892,8 @@ exports.STabs = STabs;
9891
10892
  exports.STag = STag;
9892
10893
  exports.STextLink = STextLink;
9893
10894
  exports.STextarea = STextarea;
10895
+ exports.STimePicker = STimePicker;
10896
+ exports.STimeRangePicker = STimeRangePicker;
9894
10897
  exports.SToast = SToast;
9895
10898
  exports.SToastContainer = SToastContainer;
9896
10899
  exports.SToggle = SToggle;
@@ -9898,9 +10901,12 @@ exports.STooltip = STooltip;
9898
10901
  exports.TAG_COLORS = TAG_COLORS;
9899
10902
  exports.TAG_SHAPES = TAG_SHAPES;
9900
10903
  exports.TAG_SIZES = TAG_SIZES;
10904
+ exports.TIMEPICKER_SIZES = TIMEPICKER_SIZES;
10905
+ exports.TIMEPICKER_SIZE_CONFIG = TIMEPICKER_SIZE_CONFIG;
9901
10906
  exports.buttonPreset = buttonPreset;
9902
10907
  exports.cn = cn;
9903
10908
  exports.findGnbAncestors = findGnbAncestors;
10909
+ exports.findGnbRailValue = findGnbRailValue;
9904
10910
  exports.isColorKey = isColorKey;
9905
10911
  exports.loading = loading;
9906
10912
  exports.resolveColor = resolveColor;