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.js CHANGED
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { clsx } from 'clsx';
2
3
  import { extendTailwindMerge } from 'tailwind-merge';
3
4
  import { forwardRef, useState, useEffect, useRef, useCallback, createContext, useImperativeHandle, useMemo, useContext, useId, useLayoutEffect, useSyncExternalStore, Fragment as Fragment$1 } from 'react';
@@ -12,7 +13,6 @@ import * as RDialog from '@radix-ui/react-dialog';
12
13
  import * as RCheckbox from '@radix-ui/react-checkbox';
13
14
  import * as Ariakit from '@ariakit/react';
14
15
 
15
- // src/lib/cn.ts
16
16
  var twMerge = extendTailwindMerge({
17
17
  extend: {
18
18
  classGroups: {
@@ -4876,73 +4876,70 @@ function SLoadingModal({
4876
4876
  }
4877
4877
 
4878
4878
  // src/components/SGnb/gnb.config.ts
4879
- var belt = (p) => `var(--cmp-gnbBelt-${p})`;
4880
- var box = (p) => `var(--cmp-gnbBox-${p})`;
4881
- var light = (p) => `var(--cmp-light-${p})`;
4882
- var dark = (p) => `var(--cmp-dark-${p})`;
4879
+ var gnb = (p) => `var(--cmp-gnb-${p})`;
4880
+ var box = (p) => `var(--cmp-gnb-box-${p})`;
4881
+ var belt = (p) => `var(--cmp-gnb-belt-${p})`;
4882
+ var light = (p) => `var(--cmp-gnb-light-${p})`;
4883
+ var dark = (p) => `var(--cmp-gnb-dark-${p})`;
4884
+ var HEADER_TOP = {
4885
+ topHeight: gnb("top-height"),
4886
+ topPaddingX: gnb("top-paddingX"),
4887
+ topGap: gnb("top-gap"),
4888
+ logoHeight: gnb("logo-height")
4889
+ };
4883
4890
  var GNB_HEADER = {
4884
- fix: {
4885
- topHeight: belt("top-height"),
4886
- topPaddingX: belt("top-paddingX"),
4887
- topGap: belt("top-gap"),
4888
- logoHeight: belt("logo-height")
4889
- },
4890
- full: {
4891
- topHeight: box("top-height"),
4892
- topPaddingX: box("paddingX"),
4893
- topGap: box("gap"),
4894
- // 상단바 gap (버튼 간 세부 gap 은 --cmp-gnbBox-button-gap)
4895
- logoHeight: box("logo-height")
4896
- }
4891
+ fix: HEADER_TOP,
4892
+ full: HEADER_TOP
4897
4893
  };
4898
4894
  var GNB_MENU = {
4899
4895
  box: {
4900
- // menu.body.padding 전방향 토큰 세로 패딩과 아이템 좌우 여백 양쪽에 쓴다
4901
- bodyPaddingY: box("menu-body-padding"),
4902
- itemMarginX: box("menu-body-padding"),
4903
- itemPaddingY: box("item-paddingY"),
4896
+ // 바디 패딩·세로 패딩·chevron gnb 통합된 공통 토큰
4897
+ bodyPaddingY: gnb("body-padding"),
4898
+ itemMarginX: gnb("body-padding"),
4899
+ // 전방향 토큰 → 아이템 좌우 여백에도 사용
4900
+ itemPaddingY: gnb("item-paddingY"),
4904
4901
  itemPaddingRight: box("item-paddingRight"),
4905
4902
  itemGap: box("item-gap"),
4906
4903
  itemRadius: box("item-radius"),
4907
- itemArrow: box("item-arrow"),
4904
+ itemArrow: gnb("item-chevron-size"),
4908
4905
  depth1PaddingLeft: box("item-depth1-paddingLeft"),
4909
4906
  depth2PaddingLeft: box("item-depth2-paddingLeft"),
4910
4907
  depth3PaddingLeft: box("item-depth3-paddingLeft"),
4911
4908
  depth1Icon: box("item-depth1-icon"),
4912
4909
  depth1FontSize: 14,
4913
- // token: gnbBox.item.typography.bold.fontSize
4910
+ // token: gnb.box.item.typography.selected.fontSize
4914
4911
  depth1LineHeight: 24,
4915
- // token: gnbBox.item.typography.bold.lineHeight
4912
+ // token: gnb.box.item.typography.selected.lineHeight
4916
4913
  depth1FontWeight: 700,
4917
4914
  itemFontSize: 14,
4918
- // token: gnbBox.item.typography.default.fontSize
4915
+ // token: gnb.box.item.typography.default.fontSize
4919
4916
  itemLineHeight: 24
4920
- // token: gnbBox.item.typography.default.lineHeight
4917
+ // token: gnb.box.item.typography.default.lineHeight
4921
4918
  },
4922
4919
  belt: {
4923
- bodyPaddingY: belt("body-paddingY"),
4920
+ bodyPaddingY: gnb("body-padding"),
4924
4921
  itemMarginX: "0px",
4925
4922
  // belt 토큰에 가로 여백 없음 → 풀폭 행 (들여쓰기는 depth*-paddingLeft 전담)
4926
- itemPaddingY: belt("item-paddingY"),
4923
+ itemPaddingY: gnb("item-paddingY"),
4927
4924
  itemPaddingRight: belt("item-paddingRight"),
4928
4925
  itemGap: belt("item-gap"),
4929
4926
  itemRadius: "0px",
4930
4927
  // belt 토큰에 radius 없음 → 풀폭 행
4931
- itemArrow: belt("item-icon"),
4928
+ itemArrow: gnb("item-chevron-size"),
4932
4929
  depth1PaddingLeft: belt("item-depth1-paddingLeft"),
4933
4930
  depth2PaddingLeft: belt("item-depth2-paddingLeft"),
4934
4931
  depth3PaddingLeft: belt("item-depth3-paddingLeft"),
4935
- depth1Icon: box("item-depth1-icon"),
4936
- // belt 토큰에 depth1-icon 없음 → box 값(20) 공유
4932
+ depth1Icon: belt("item-depth1-icon"),
4933
+ // belt 전용 depth1 아이콘 토큰(16)
4937
4934
  depth1FontSize: 13,
4938
- // token: gnbBelt.item.depth1.typography.fontSize
4935
+ // token: gnb.belt.item.depth1.typography.fontSize
4939
4936
  depth1LineHeight: 22,
4940
- // token: gnbBelt.item.depth1.typography.lineHeight
4937
+ // token: gnb.belt.item.depth1.typography.lineHeight
4941
4938
  depth1FontWeight: 700,
4942
4939
  itemFontSize: 12,
4943
- // token: gnbBelt.item.typography.default.fontSize
4940
+ // token: gnb.belt.item.typography.default.fontSize
4944
4941
  itemLineHeight: 20
4945
- // token: gnbBelt.item.typography.default.lineHeight
4942
+ // token: gnb.belt.item.typography.default.lineHeight
4946
4943
  }
4947
4944
  };
4948
4945
  var GNB_COLORS = {
@@ -4955,7 +4952,9 @@ var GNB_COLORS = {
4955
4952
  itemColorDefault: light("item-fg-default"),
4956
4953
  itemColorHover: light("item-fg-hover"),
4957
4954
  itemColorActive: light("item-fg-selected"),
4958
- itemChevron: light("item-chevron")
4955
+ itemChevron: light("item-chevron"),
4956
+ itemIcon: "grey_65",
4957
+ railFgDefault: light("rail-fg-default")
4959
4958
  },
4960
4959
  dark: {
4961
4960
  panelBg: dark("bg"),
@@ -4966,19 +4965,41 @@ var GNB_COLORS = {
4966
4965
  itemColorDefault: dark("item-fg-default"),
4967
4966
  itemColorHover: dark("item-fg-hover"),
4968
4967
  itemColorActive: dark("item-fg-selected"),
4969
- itemChevron: dark("item-chevron")
4968
+ itemChevron: dark("item-chevron"),
4969
+ itemIcon: "grey_55",
4970
+ railFgDefault: dark("rail-fg-default")
4970
4971
  }
4971
4972
  };
4972
4973
  var GNB_COMMON = {
4973
4974
  /** 접힘 레일 폭. header="full" 은 접으면 메뉴만 사라져 레일이 없으므로 fix 전용. */
4974
- foldWidth: belt("fold-width"),
4975
- /**
4976
- * 상단바 아이콘 버튼 사이 간격. fix 레일(세로 스택)의 gap·상하 여백에 쓴다.
4977
- * 레일 전용 여백 토큰이 따로 없어 같은 값(8px)인 이 토큰으로 재해석했다.
4978
- */
4979
- buttonGap: box("button-gap")
4980
- };
4975
+ foldWidth: gnb("fold-width")
4976
+ };
4977
+ var GNB_RAIL = {
4978
+ padding: gnb("rail-padding"),
4979
+ gap: gnb("rail-gap"),
4980
+ itemWidth: gnb("rail-item-width"),
4981
+ itemPaddingTop: gnb("rail-item-paddingTop"),
4982
+ itemPaddingBottom: gnb("rail-item-paddingBottom"),
4983
+ itemRadius: gnb("rail-item-radius"),
4984
+ itemIcon: gnb("rail-item-icon"),
4985
+ // 타이포 (theme.css 에 gnb 타이포 변수 없음 → 토큰 값 직접 명시)
4986
+ itemFontSize: 11,
4987
+ // token: gnb.rail.item.typography.default.fontSize
4988
+ itemLineHeight: 18,
4989
+ // token: gnb.rail.item.typography.default.lineHeight
4990
+ itemFontWeight: 500,
4991
+ // token: gnb.rail.item.typography.default.fontWeight
4992
+ itemFontWeightSelected: 700
4993
+ // token: gnb.rail.item.typography.selected.fontWeight
4994
+ };
4995
+ var GNB_RAIL_WIDTH = 68;
4996
+ var GNB_FOLD_MS = 200;
4997
+ var GNB_EXPAND_MS = 200;
4981
4998
  var GNB_MENU_WIDTH = 240;
4999
+ function findGnbRailValue(items, value) {
5000
+ if (items.some((item) => item.value === value)) return value;
5001
+ return findGnbAncestors(items, value)[0];
5002
+ }
4982
5003
  function findGnbAncestors(items, value) {
4983
5004
  const walk = (list, trail) => {
4984
5005
  for (const item of list) {
@@ -5001,6 +5022,7 @@ function useSLayout() {
5001
5022
  var SLayout = forwardRef(function SLayout2({
5002
5023
  type = "box",
5003
5024
  header = "fix",
5025
+ useRail = false,
5004
5026
  folded,
5005
5027
  defaultFolded = false,
5006
5028
  onFoldedChange,
@@ -5020,13 +5042,17 @@ var SLayout = forwardRef(function SLayout2({
5020
5042
  [isControlled, onFoldedChange]
5021
5043
  );
5022
5044
  const ctx = useMemo(
5023
- () => ({ type, header, folded: currentFolded, setFolded }),
5024
- [type, header, currentFolded, setFolded]
5045
+ () => ({ type, header, useRail, folded: currentFolded, setFolded }),
5046
+ [type, header, useRail, currentFolded, setFolded]
5025
5047
  );
5026
5048
  const isFullHeader = header === "full";
5049
+ const gnbColumnWidth = useRail ? GNB_RAIL_WIDTH + GNB_MENU_WIDTH : GNB_MENU_WIDTH;
5027
5050
  const frameStyle = isFullHeader ? {
5028
- gridTemplateColumns: `${currentFolded ? 0 : GNB_MENU_WIDTH}px 1fr`,
5029
- gridTemplateRows: `${GNB_HEADER[header].topHeight} 1fr`
5051
+ gridTemplateColumns: `${currentFolded ? 0 : gnbColumnWidth}px 1fr`,
5052
+ gridTemplateRows: `${GNB_HEADER[header].topHeight} 1fr`,
5053
+ // 메뉴가 미끄러지는 동안 페이지 열도 같은 길이로 따라와야 한 덩어리로 읽힌다.
5054
+ // (fix 는 SGnb 가 제 폭을 직접 전환하므로 프레임이 손댈 게 없다)
5055
+ transition: `grid-template-columns ${GNB_FOLD_MS}ms ease-out`
5030
5056
  } : void 0;
5031
5057
  return /* @__PURE__ */ jsx(SLayoutContext.Provider, { value: ctx, children: /* @__PURE__ */ jsx(
5032
5058
  "div",
@@ -5051,8 +5077,10 @@ var SGnb = forwardRef(function SGnb2({
5051
5077
  header: headerProp,
5052
5078
  color = "light",
5053
5079
  items,
5080
+ useRail: useRailProp,
5054
5081
  value = "",
5055
5082
  onValueChange,
5083
+ onRailChange,
5056
5084
  folded: foldedProp,
5057
5085
  onFoldChange,
5058
5086
  onLauncherClick,
@@ -5066,6 +5094,7 @@ var SGnb = forwardRef(function SGnb2({
5066
5094
  const type = typeProp ?? layout?.type ?? "box";
5067
5095
  const header = headerProp ?? layout?.header ?? "fix";
5068
5096
  const folded = foldedProp ?? layout?.folded ?? false;
5097
+ const useRail = useRailProp ?? layout?.useRail ?? false;
5069
5098
  const H = GNB_HEADER[header];
5070
5099
  const M = GNB_MENU[type];
5071
5100
  const C = GNB_COLORS[color];
@@ -5073,6 +5102,9 @@ var SGnb = forwardRef(function SGnb2({
5073
5102
  () => new Set(findGnbAncestors(items, value))
5074
5103
  );
5075
5104
  const [hoveredKey, setHoveredKey] = useState(null);
5105
+ const [activeRail, setActiveRail] = useState(
5106
+ () => findGnbRailValue(items, value) ?? items[0]?.value
5107
+ );
5076
5108
  useEffect(() => {
5077
5109
  const ancestors = findGnbAncestors(items, value);
5078
5110
  if (ancestors.length === 0) return;
@@ -5083,6 +5115,15 @@ var SGnb = forwardRef(function SGnb2({
5083
5115
  return next;
5084
5116
  });
5085
5117
  }, [items, value]);
5118
+ useEffect(() => {
5119
+ const owner = findGnbRailValue(items, value);
5120
+ if (owner !== void 0) setActiveRail(owner);
5121
+ }, [items, value]);
5122
+ useEffect(() => {
5123
+ setActiveRail(
5124
+ (prev) => prev !== void 0 && items.some((item) => item.value === prev) ? prev : items[0]?.value
5125
+ );
5126
+ }, [items]);
5086
5127
  const toggleExpand = (key) => {
5087
5128
  setExpandedKeys((prev) => {
5088
5129
  const next = new Set(prev);
@@ -5099,16 +5140,58 @@ var SGnb = forwardRef(function SGnb2({
5099
5140
  }
5100
5141
  onValueChange?.(item.value);
5101
5142
  };
5143
+ const handleRailClick = (item) => {
5144
+ if (item.disabled) return;
5145
+ setActiveRail(item.value);
5146
+ onRailChange?.(item.value);
5147
+ if (!item.children || item.children.length === 0) onValueChange?.(item.value);
5148
+ };
5102
5149
  const handleFoldToggle = () => {
5103
5150
  const next = !folded;
5104
5151
  onFoldChange?.(next);
5105
5152
  if (foldedProp === void 0) layout?.setFolded(next);
5106
5153
  };
5107
- const renderItem = (item, depth) => {
5154
+ const renderRailItem = (item) => {
5155
+ const isActive = item.value === activeRail;
5156
+ const isHovered = hoveredKey === item.value && !item.disabled;
5157
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
5158
+ "button",
5159
+ {
5160
+ type: "button",
5161
+ disabled: item.disabled,
5162
+ "aria-current": isActive ? "true" : void 0,
5163
+ className: "box-border flex select-none flex-col items-center border-none bg-transparent transition-colors duration-150",
5164
+ style: {
5165
+ width: GNB_RAIL.itemWidth,
5166
+ paddingTop: GNB_RAIL.itemPaddingTop,
5167
+ paddingBottom: GNB_RAIL.itemPaddingBottom,
5168
+ borderRadius: GNB_RAIL.itemRadius,
5169
+ backgroundColor: isActive ? C.itemBgSelected : isHovered ? C.itemBgHover : C.itemBgDefault,
5170
+ color: isActive ? C.itemColorActive : C.railFgDefault,
5171
+ // <button> 의 UA 폰트를 끊고 앱 폰트를 잇는다 (메뉴 아이템과 같은 이유)
5172
+ fontFamily: "inherit",
5173
+ fontSize: GNB_RAIL.itemFontSize,
5174
+ lineHeight: `${GNB_RAIL.itemLineHeight}px`,
5175
+ fontWeight: isActive ? GNB_RAIL.itemFontWeightSelected : GNB_RAIL.itemFontWeight,
5176
+ opacity: item.disabled ? 0.4 : 1,
5177
+ cursor: item.disabled ? "not-allowed" : "pointer"
5178
+ },
5179
+ onClick: () => handleRailClick(item),
5180
+ onMouseEnter: () => setHoveredKey(item.value),
5181
+ onMouseLeave: () => setHoveredKey((prev) => prev === item.value ? null : prev),
5182
+ children: [
5183
+ item.icon && /* @__PURE__ */ jsx(SIcon, { name: item.icon, size: GNB_RAIL.itemIcon, className: "shrink-0" }),
5184
+ /* @__PURE__ */ jsx("span", { className: "w-full overflow-hidden text-ellipsis whitespace-nowrap text-center", children: item.label })
5185
+ ]
5186
+ }
5187
+ ) }, item.value);
5188
+ };
5189
+ const renderItem = (item, depth, branchHasIcon = false) => {
5108
5190
  const hasChildren = !!(item.children && item.children.length > 0);
5109
5191
  const isSelected = !hasChildren && item.value === value;
5110
5192
  const isExpanded = expandedKeys.has(item.value);
5111
5193
  const isHovered = hoveredKey === item.value && !item.disabled;
5194
+ const iconBranch = depth === 1 ? !!item.icon : branchHasIcon;
5112
5195
  const itemStyle = {
5113
5196
  gap: M.itemGap,
5114
5197
  paddingTop: M.itemPaddingY,
@@ -5145,8 +5228,11 @@ var SGnb = forwardRef(function SGnb2({
5145
5228
  onMouseEnter: () => setHoveredKey(item.value),
5146
5229
  onMouseLeave: () => setHoveredKey((prev) => prev === item.value ? null : prev),
5147
5230
  children: [
5148
- depth === 1 && item.icon && /* @__PURE__ */ jsx(SIcon, { name: item.icon, size: M.depth1Icon, className: "shrink-0" }),
5149
- /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap", children: item.label }),
5231
+ depth === 1 && item.icon && /* @__PURE__ */ jsx(SIcon, { name: item.icon, size: M.depth1Icon, color: C.itemIcon, className: "shrink-0" }),
5232
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap flex flex-no-wrap items-center", children: [
5233
+ depth > 1 && iconBranch ? /* @__PURE__ */ jsx("div", { className: "w-[20px] h-[24px] flex items-center justify-center mr-[8px]", children: "-" }) : null,
5234
+ item.label
5235
+ ] }),
5150
5236
  item.tag !== void 0 && item.tag !== "" && /* @__PURE__ */ jsx(
5151
5237
  STag,
5152
5238
  {
@@ -5164,28 +5250,53 @@ var SGnb = forwardRef(function SGnb2({
5164
5250
  size: M.itemArrow,
5165
5251
  color: C.itemChevron,
5166
5252
  rotate: isExpanded ? 180 : 0,
5167
- className: "shrink-0 transition-transform duration-150"
5253
+ className: "shrink-0 transition-transform duration-200"
5168
5254
  }
5169
5255
  )
5170
5256
  ]
5171
5257
  }
5172
5258
  ),
5173
- hasChildren && isExpanded && /* @__PURE__ */ jsx("ul", { className: "m-0 flex list-none flex-col p-0", children: item.children.map((child) => renderItem(child, depth + 1)) })
5259
+ hasChildren && /* @__PURE__ */ jsx(
5260
+ "div",
5261
+ {
5262
+ className: "grid",
5263
+ style: {
5264
+ gridTemplateRows: isExpanded ? "1fr" : "0fr",
5265
+ transition: `grid-template-rows ${GNB_EXPAND_MS}ms ease-out`
5266
+ },
5267
+ children: /* @__PURE__ */ jsx(
5268
+ "ul",
5269
+ {
5270
+ className: "m-0 flex min-h-0 list-none flex-col overflow-hidden p-0",
5271
+ style: {
5272
+ visibility: isExpanded ? "visible" : "hidden",
5273
+ transition: `visibility 0s linear ${isExpanded ? "0s" : `${GNB_EXPAND_MS}ms`}`
5274
+ },
5275
+ children: item.children.map((child) => renderItem(child, depth + 1, iconBranch))
5276
+ }
5277
+ )
5278
+ }
5279
+ )
5174
5280
  ] }, item.value)
5175
5281
  );
5176
5282
  };
5283
+ const railItems = useRail ? items : [];
5284
+ const menuItems = useRail ? items.find((item) => item.value === activeRail)?.children ?? [] : items;
5285
+ const bodyWidth = useRail ? GNB_RAIL_WIDTH + GNB_MENU_WIDTH : GNB_MENU_WIDTH;
5177
5286
  const railFolded = folded && header === "fix";
5178
- const rootWidth = header === "full" ? "100%" : folded ? GNB_COMMON.foldWidth : GNB_MENU_WIDTH;
5287
+ const rootWidth = header === "full" ? "100%" : folded ? GNB_COMMON.foldWidth : bodyWidth;
5179
5288
  const rootStyle = {
5180
5289
  width: rootWidth,
5290
+ transition: `width ${GNB_FOLD_MS}ms ease-out`,
5181
5291
  ...header === "full" ? { gridArea: "1 / 1 / -1 / -1", pointerEvents: "none" } : { backgroundColor: C.panelBg },
5182
5292
  ...style
5183
5293
  };
5294
+ const hideAfterSlide = (hidden) => `visibility 0s linear ${hidden ? `${GNB_FOLD_MS}ms` : "0s"}`;
5184
5295
  return /* @__PURE__ */ jsxs(
5185
5296
  "div",
5186
5297
  {
5187
5298
  ref,
5188
- className: cn("box-border flex h-full shrink-0 flex-col", className),
5299
+ className: cn("box-border flex h-full shrink-0 flex-col overflow-hidden", className),
5189
5300
  style: rootStyle,
5190
5301
  "data-type": type,
5191
5302
  "data-header": header,
@@ -5195,30 +5306,47 @@ var SGnb = forwardRef(function SGnb2({
5195
5306
  /* @__PURE__ */ jsxs(
5196
5307
  "div",
5197
5308
  {
5198
- className: cn("box-border flex items-center", railFolded && "flex-col"),
5309
+ className: "relative box-border flex shrink-0 items-center overflow-hidden",
5199
5310
  style: {
5200
- gap: railFolded ? GNB_COMMON.buttonGap : H.topGap,
5201
- height: railFolded ? "auto" : H.topHeight,
5202
- padding: railFolded ? `${GNB_COMMON.buttonGap} 0` : `0 ${H.topPaddingX}`,
5311
+ height: H.topHeight,
5312
+ padding: `0 ${railFolded ? GNB_RAIL.padding : H.topPaddingX}`,
5203
5313
  borderBottom: `1px solid ${C.topBorder}`,
5204
5314
  backgroundColor: C.panelBg,
5205
5315
  pointerEvents: "auto",
5316
+ transition: `padding ${GNB_FOLD_MS}ms ease-out`,
5206
5317
  // 아이콘 버튼은 자체 ghostButton 토큰을 쓰므로, 이 색은 logo 슬롯 상속용
5207
5318
  color: C.itemColorDefault
5208
5319
  },
5209
5320
  children: [
5210
- onLauncherClick && /* @__PURE__ */ jsx(
5211
- SGhostButton,
5321
+ (onLauncherClick || logo) && /* @__PURE__ */ jsxs(
5322
+ "div",
5212
5323
  {
5213
- icon: "launcher",
5214
- size: "xs",
5215
- intent: color === "dark" ? "inverse" : "default",
5216
- ariaLabel: "app launcher",
5217
- onClick: onLauncherClick,
5218
- className: "shrink-0"
5324
+ className: "absolute inset-y-0 flex items-center",
5325
+ style: {
5326
+ left: H.topPaddingX,
5327
+ gap: H.topGap,
5328
+ opacity: railFolded ? 0 : 1,
5329
+ transition: `opacity ${GNB_FOLD_MS}ms ease-out, ${hideAfterSlide(railFolded)}, transform ${GNB_FOLD_MS}ms ease-out`,
5330
+ // 메뉴와 같은 방향으로 빠지되, 좁은 레일 안에서 꼬리가 비치지 않게 페이드가 거리를 대신한다
5331
+ transform: railFolded ? `translateX(-${H.topPaddingX})` : "none",
5332
+ visibility: railFolded ? "hidden" : "visible"
5333
+ },
5334
+ children: [
5335
+ onLauncherClick && /* @__PURE__ */ jsx(
5336
+ SGhostButton,
5337
+ {
5338
+ icon: "launcher",
5339
+ size: "xs",
5340
+ intent: color === "dark" ? "inverse" : "default",
5341
+ ariaLabel: "app launcher",
5342
+ onClick: onLauncherClick,
5343
+ className: "shrink-0"
5344
+ }
5345
+ ),
5346
+ logo && /* @__PURE__ */ jsx("div", { className: "flex items-center", style: { height: H.logoHeight }, children: logo })
5347
+ ]
5219
5348
  }
5220
5349
  ),
5221
- !railFolded && logo && /* @__PURE__ */ jsx("div", { className: "flex items-center", style: { height: H.logoHeight }, children: logo }),
5222
5350
  /* @__PURE__ */ jsx(
5223
5351
  SGhostButton,
5224
5352
  {
@@ -5228,30 +5356,61 @@ var SGnb = forwardRef(function SGnb2({
5228
5356
  ariaLabel: "toggle navigation",
5229
5357
  ariaPressed: folded,
5230
5358
  onClick: handleFoldToggle,
5231
- className: cn("shrink-0", !railFolded && "ml-auto")
5359
+ className: "ml-auto shrink-0"
5232
5360
  }
5233
5361
  )
5234
5362
  ]
5235
5363
  }
5236
5364
  ),
5237
- !folded && /* @__PURE__ */ jsx(
5238
- "nav",
5365
+ /* @__PURE__ */ jsxs(
5366
+ "div",
5239
5367
  {
5240
- "aria-label": ariaLabel,
5241
- className: "flex flex-col overflow-y-auto",
5368
+ className: "flex flex-row",
5242
5369
  style: {
5243
- width: GNB_MENU_WIDTH,
5370
+ width: bodyWidth,
5244
5371
  alignSelf: "flex-start",
5245
5372
  flexGrow: 1,
5246
5373
  flexShrink: 1,
5247
5374
  flexBasis: "auto",
5248
5375
  minHeight: 0,
5249
- paddingTop: M.bodyPaddingY,
5250
- paddingBottom: M.bodyPaddingY,
5251
- backgroundColor: C.panelBg,
5252
- pointerEvents: "auto"
5376
+ transform: folded ? "translateX(-100%)" : "none",
5377
+ visibility: folded ? "hidden" : "visible",
5378
+ transition: `transform ${GNB_FOLD_MS}ms ease-out, ${hideAfterSlide(folded)}`
5253
5379
  },
5254
- children: /* @__PURE__ */ jsx("ul", { className: "m-0 flex list-none flex-col p-0", children: items.map((item) => renderItem(item, 1)) })
5380
+ children: [
5381
+ useRail && // 레일도 내비게이션 landmark — nav 로 세워야 aria-label(레일)이 스크린리더에 노출된다
5382
+ // (role 없는 ul 의 aria-label 은 영역명으로 읽히지 않는다). 메뉴 nav 와는 레이블로 구분된다.
5383
+ /* @__PURE__ */ jsx(
5384
+ "nav",
5385
+ {
5386
+ "aria-label": `${ariaLabel} rail`,
5387
+ className: "flex shrink-0 flex-col overflow-y-auto",
5388
+ style: {
5389
+ width: GNB_RAIL_WIDTH,
5390
+ padding: GNB_RAIL.padding,
5391
+ borderRight: `1px solid ${C.topBorder}`,
5392
+ backgroundColor: C.panelBg,
5393
+ pointerEvents: "auto"
5394
+ },
5395
+ children: /* @__PURE__ */ jsx("ul", { className: "m-0 flex list-none flex-col p-0", style: { gap: GNB_RAIL.gap }, children: railItems.map(renderRailItem) })
5396
+ }
5397
+ ),
5398
+ /* @__PURE__ */ jsx(
5399
+ "nav",
5400
+ {
5401
+ "aria-label": ariaLabel,
5402
+ className: "flex shrink-0 flex-col overflow-y-auto",
5403
+ style: {
5404
+ width: GNB_MENU_WIDTH,
5405
+ paddingTop: M.bodyPaddingY,
5406
+ paddingBottom: M.bodyPaddingY,
5407
+ backgroundColor: C.panelBg,
5408
+ pointerEvents: "auto"
5409
+ },
5410
+ children: /* @__PURE__ */ jsx("ul", { className: "m-0 flex list-none flex-col p-0", children: menuItems.map((item) => renderItem(item, 1)) })
5411
+ }
5412
+ )
5413
+ ]
5255
5414
  }
5256
5415
  )
5257
5416
  ]
@@ -5265,7 +5424,7 @@ var SPage = forwardRef(function SPage2({ className, style, children, ...rest },
5265
5424
  "main",
5266
5425
  {
5267
5426
  ref,
5268
- className: cn("box-border min-h-0 min-w-0 flex-1 overflow-auto p-24", className),
5427
+ className: cn("box-border min-h-0 min-w-0 flex-1 overflow-auto p-20", className),
5269
5428
  style: { ...gridStyle, ...style },
5270
5429
  ...rest,
5271
5430
  children
@@ -5307,6 +5466,7 @@ function STabs({
5307
5466
  }) {
5308
5467
  const s = TAB_MAIN_SIZE[size];
5309
5468
  const blue = TAB_MAIN_COLORS.selected.border;
5469
+ const isVertical = isSub && vertical;
5310
5470
  const tabRefs = useRef([]);
5311
5471
  const moveTo = (target) => {
5312
5472
  if (target < 0 || target >= tabs.length) return;
@@ -5314,19 +5474,17 @@ function STabs({
5314
5474
  tabRefs.current[target]?.focus();
5315
5475
  };
5316
5476
  const containerStyle = { ...style };
5317
- if (!isSub) {
5318
- if (vertical) containerStyle.borderRight = `1px solid ${blue}`;
5319
- else containerStyle.borderBottom = `1px solid ${blue}`;
5320
- }
5477
+ if (!isSub) containerStyle.borderBottom = `1px solid ${blue}`;
5321
5478
  return /* @__PURE__ */ jsx(
5322
5479
  "div",
5323
5480
  {
5324
5481
  role: "tablist",
5325
- "aria-orientation": vertical ? "vertical" : "horizontal",
5482
+ "aria-orientation": isVertical ? "vertical" : "horizontal",
5326
5483
  className: cn(
5327
- "inline-flex w-fit",
5328
- vertical ? "flex-col items-stretch" : "flex-row",
5329
- isSub ? vertical ? "gap-[16px]" : "gap-[24px]" : "gap-[4px]",
5484
+ // main(가로): baseline(하단 divider)이 100% 너비로 채워지도록 w-full
5485
+ isSub ? "inline-flex w-fit" : "flex w-full",
5486
+ isVertical ? "flex-col items-stretch" : "flex-row",
5487
+ isSub ? isVertical ? "gap-[16px]" : "gap-[24px]" : "gap-[4px]",
5330
5488
  className
5331
5489
  ),
5332
5490
  style: containerStyle,
@@ -5355,22 +5513,12 @@ function STabs({
5355
5513
  color: selected ? TAB_MAIN_COLORS.selected.text : TAB_MAIN_COLORS.default.text,
5356
5514
  border: `1px solid ${border}`,
5357
5515
  // 폴더형: 미선택 탭은 baseline 위에, 선택 탭은 baseline(파란선)을 1px 덮어 연결된 것처럼 보이게
5358
- ...vertical ? {
5359
- borderTopLeftRadius: 6,
5360
- borderBottomLeftRadius: 6,
5361
- borderTopRightRadius: 0,
5362
- borderBottomRightRadius: 0,
5363
- justifyContent: "flex-start",
5364
- borderRight: selected ? `1px solid ${selectedBg}` : "none",
5365
- marginRight: selected ? -1 : 0
5366
- } : {
5367
- borderTopLeftRadius: 6,
5368
- borderTopRightRadius: 6,
5369
- borderBottomLeftRadius: 0,
5370
- borderBottomRightRadius: 0,
5371
- borderBottom: selected ? `1px solid ${selectedBg}` : "none",
5372
- marginBottom: selected ? -1 : 0
5373
- }
5516
+ borderTopLeftRadius: 6,
5517
+ borderTopRightRadius: 6,
5518
+ borderBottomLeftRadius: 0,
5519
+ borderBottomRightRadius: 0,
5520
+ borderBottom: selected ? `1px solid ${selectedBg}` : "none",
5521
+ marginBottom: selected ? -1 : 0
5374
5522
  };
5375
5523
  }
5376
5524
  return /* @__PURE__ */ jsxs(
@@ -5390,8 +5538,8 @@ function STabs({
5390
5538
  select();
5391
5539
  return;
5392
5540
  }
5393
- const nextKey = vertical ? "ArrowDown" : "ArrowRight";
5394
- const prevKey = vertical ? "ArrowUp" : "ArrowLeft";
5541
+ const nextKey = isVertical ? "ArrowDown" : "ArrowRight";
5542
+ const prevKey = isVertical ? "ArrowUp" : "ArrowLeft";
5395
5543
  if (e.key === nextKey) {
5396
5544
  e.preventDefault();
5397
5545
  moveTo((i + 1) % tabs.length);
@@ -6632,7 +6780,7 @@ var STable = forwardRef(function STable2({
6632
6780
  ref: scrollRef,
6633
6781
  onScroll: handleScroll,
6634
6782
  className: cn(
6635
- "relative overflow-auto border border-solid border-[color:var(--cmp-table-border-color)]",
6783
+ "bg-white relative overflow-auto border border-solid border-[color:var(--cmp-table-border-color)]",
6636
6784
  radiusClass,
6637
6785
  // 원본 sd-table__scroll-container--loading/--no-data: 로딩·데이터 없음 시 스크롤·인터랙션 차단
6638
6786
  (isLoading || rowCount === 0) && "overflow-hidden pointer-events-none"
@@ -9806,6 +9954,856 @@ var SBarcodeInput = forwardRef(
9806
9954
  }
9807
9955
  );
9808
9956
 
9809
- export { BADGE_COLORS, BUTTON_COLORS, BUTTON_SIZES, COLOR_KEYS, GNB_COLORS, GNB_COMMON, GNB_HEADER, GNB_MENU, GNB_MENU_WIDTH, ICONS, SActionModal, SBadge, SBarcodeInput, SButton, SCalendar, SCallout, SCard, SCheckbox, SChip, SChipInput, SCircleProgress, SConfirmModal, SDatePicker, SDateRangePicker, SDivider, SDropdownButton, SField, SFilePicker, SForm, SGhostButton, SGnb, SGuide, SIcon, SInput, SKeyValueTable, SLayout, SLayoutContext, SLinearProgress, SLoadingContainer, SLoadingModal, SLoadingViewport, SNumberInput, SPage, SPagination, SPopover, SPopup, SPortal, SRadio, SRadioButton, SRadioGroup, SSelect, SSwitch, STable, STabs, STag, STextLink, STextarea, SToast, SToastContainer, SToggle, STooltip, TAG_COLORS, TAG_SHAPES, TAG_SIZES, buttonPreset, cn, findGnbAncestors, isColorKey, loading, resolveColor, useSLayout };
9957
+ // src/components/STimePicker/timepicker.config.ts
9958
+ var V2 = (path) => `var(--cmp-timepicker-${path})`;
9959
+ var TIMEPICKER_SIZES = ["sm", "md"];
9960
+ var TIMEPICKER_SIZE_CONFIG = {
9961
+ sm: {
9962
+ height: V2("sm-height"),
9963
+ paddingX: V2("sm-paddingX"),
9964
+ gap: V2("sm-gap"),
9965
+ icon: V2("sm-icon"),
9966
+ radius: V2("sm-radius"),
9967
+ // TODO: component.timepicker 토큰에 typography가 추가되면 CSS 변수로 교체한다.
9968
+ fontSize: 12,
9969
+ lineHeight: 20,
9970
+ minWidth: 80,
9971
+ fieldMinWidth: 128
9972
+ },
9973
+ md: {
9974
+ height: V2("md-height"),
9975
+ paddingX: V2("md-paddingX"),
9976
+ gap: V2("md-gap"),
9977
+ icon: V2("md-icon"),
9978
+ radius: V2("md-radius"),
9979
+ // TODO: component.timepicker 토큰에 typography가 추가되면 CSS 변수로 교체한다.
9980
+ fontSize: 14,
9981
+ lineHeight: 24,
9982
+ minWidth: 96,
9983
+ fieldMinWidth: 160
9984
+ }
9985
+ };
9986
+ function clampTimePart(value, min, max) {
9987
+ if (!Number.isFinite(value)) return min;
9988
+ return Math.min(Math.max(value, min), max);
9989
+ }
9990
+ function padTimePart(value) {
9991
+ return String(value).padStart(2, "0");
9992
+ }
9993
+ function parseTimeValue(value) {
9994
+ const match = /^(\d{1,2}):(\d{1,2})$/.exec(value ?? "");
9995
+ if (!match) return { hour: 0, minute: 0 };
9996
+ return {
9997
+ hour: clampTimePart(Number(match[1]), 0, 23),
9998
+ minute: clampTimePart(Number(match[2]), 0, 59)
9999
+ };
10000
+ }
10001
+ function formatTimeValue(hour, minute) {
10002
+ return `${padTimePart(clampTimePart(hour, 0, 23))}:${padTimePart(clampTimePart(minute, 0, 59))}`;
10003
+ }
10004
+ function formatTimeDisplay(value, type) {
10005
+ if (value == null || value === "") return "";
10006
+ const { hour, minute } = parseTimeValue(value);
10007
+ if (type === "default") return formatTimeValue(hour, minute);
10008
+ const meridiem = hour >= 12 ? "\uC624\uD6C4" : "\uC624\uC804";
10009
+ const displayHour = hour % 12 === 0 ? 12 : hour % 12;
10010
+ return `${meridiem} ${padTimePart(displayHour)}:${padTimePart(minute)}`;
10011
+ }
10012
+ function TimeSelector({
10013
+ value,
10014
+ useMeridiem,
10015
+ minuteStep,
10016
+ onChange
10017
+ }) {
10018
+ const parsed = useMemo(() => parseTimeValue(value), [value]);
10019
+ const [hour, setHour] = useState(parsed.hour);
10020
+ const [minute, setMinute] = useState(parsed.minute);
10021
+ const [editingPart, setEditingPart] = useState(null);
10022
+ const [inputDraft, setInputDraft] = useState({});
10023
+ useEffect(() => {
10024
+ setHour(parsed.hour);
10025
+ setMinute(parsed.minute);
10026
+ }, [parsed.hour, parsed.minute]);
10027
+ const meridiem = hour >= 12 ? "PM" : "AM";
10028
+ const displayHour = useMeridiem ? hour % 12 === 0 ? 12 : hour % 12 : hour;
10029
+ const commit = (nextHour, nextMinute) => {
10030
+ const h = clampTimePart(nextHour, 0, 23);
10031
+ const m = clampTimePart(nextMinute, 0, 59);
10032
+ setHour(h);
10033
+ setMinute(m);
10034
+ onChange(formatTimeValue(h, m));
10035
+ };
10036
+ const setMeridiem = (next) => {
10037
+ if (next === meridiem) return;
10038
+ commit(next === "AM" ? hour - 12 : hour + 12, minute);
10039
+ };
10040
+ const changeMinute = (delta) => {
10041
+ const next = minute + delta;
10042
+ if (next > 59) {
10043
+ commit((hour + 1) % 24, next - 60);
10044
+ return;
10045
+ }
10046
+ if (next < 0) {
10047
+ commit((hour + 23) % 24, next + 60);
10048
+ return;
10049
+ }
10050
+ commit(hour, next);
10051
+ };
10052
+ const changeHour = (delta) => commit((hour + delta + 24) % 24, minute);
10053
+ const sanitizeInput = (next) => next.replace(/\D/g, "").slice(0, 2);
10054
+ const beginEdit = (part, currentValue) => {
10055
+ setEditingPart(part);
10056
+ setInputDraft((prev) => ({ ...prev, [part]: String(currentValue).padStart(2, "0") }));
10057
+ };
10058
+ const commitHourDraft = (draft) => {
10059
+ const clamped = clampTimePart(Number(draft), useMeridiem ? 1 : 0, useMeridiem ? 12 : 23);
10060
+ const nextHour = useMeridiem ? meridiem === "PM" ? clamped === 12 ? 12 : clamped + 12 : clamped === 12 ? 0 : clamped : clamped;
10061
+ commit(nextHour, minute);
10062
+ return clamped;
10063
+ };
10064
+ const commitMinuteDraft = (draft) => {
10065
+ const clamped = clampTimePart(Number(draft), 0, 59);
10066
+ commit(hour, clamped);
10067
+ return clamped;
10068
+ };
10069
+ const endEdit = (part) => {
10070
+ const draft = inputDraft[part];
10071
+ if (part === "hour" && draft != null && draft !== "") {
10072
+ commitHourDraft(draft);
10073
+ }
10074
+ if (part === "minute" && draft != null && draft !== "") {
10075
+ commitMinuteDraft(draft);
10076
+ }
10077
+ setEditingPart(null);
10078
+ setInputDraft({});
10079
+ };
10080
+ const inputStyle = {
10081
+ width: "var(--cmp-timepicker-selector-input-width)",
10082
+ height: "var(--cmp-textinput-sm-height)",
10083
+ borderRadius: "var(--cmp-textinput-sm-radius)",
10084
+ border: "var(--cmp-textinput-borderWidth) solid var(--cmp-textinput-border-default)",
10085
+ color: "var(--cmp-textinput-text-default)",
10086
+ fontSize: 12,
10087
+ lineHeight: "20px"
10088
+ };
10089
+ const renderColumn = (type) => {
10090
+ const isHour = type === "hour";
10091
+ const val = isHour ? displayHour : minute;
10092
+ const displayValue = editingPart === type ? inputDraft[type] ?? "" : String(val).padStart(2, "0");
10093
+ return /* @__PURE__ */ jsxs(
10094
+ "div",
10095
+ {
10096
+ className: "flex flex-col items-center justify-center",
10097
+ style: { gap: "var(--cmp-timepicker-selector-time-column-gap)" },
10098
+ children: [
10099
+ /* @__PURE__ */ jsx(
10100
+ SGhostButton,
10101
+ {
10102
+ icon: "chevronUp",
10103
+ size: "xs",
10104
+ ariaLabel: `${isHour ? "\uC2DC" : "\uBD84"} \uC99D\uAC00`,
10105
+ onClick: () => isHour ? changeHour(1) : changeMinute(minuteStep)
10106
+ }
10107
+ ),
10108
+ /* @__PURE__ */ jsx(
10109
+ "input",
10110
+ {
10111
+ "aria-label": isHour ? "\uC2DC" : "\uBD84",
10112
+ inputMode: "numeric",
10113
+ value: displayValue,
10114
+ placeholder: "-",
10115
+ onFocus: () => beginEdit(type, val),
10116
+ onBlur: () => endEdit(type),
10117
+ onChange: (event) => {
10118
+ const draft = sanitizeInput(event.target.value);
10119
+ setInputDraft((prev) => ({ ...prev, [type]: draft }));
10120
+ if (draft === "") return;
10121
+ const raw = Number(draft);
10122
+ if (isHour) {
10123
+ if (draft.length < 2) return;
10124
+ const clamped2 = commitHourDraft(draft);
10125
+ const displayDraft2 = raw === clamped2 ? draft : String(clamped2);
10126
+ setInputDraft((prev) => ({ ...prev, [type]: displayDraft2 }));
10127
+ return;
10128
+ }
10129
+ if (draft.length < 2) return;
10130
+ const clamped = commitMinuteDraft(draft);
10131
+ const displayDraft = raw === clamped ? draft : String(clamped);
10132
+ setInputDraft((prev) => ({ ...prev, [type]: displayDraft }));
10133
+ },
10134
+ className: "box-border bg-white text-center outline-none",
10135
+ style: inputStyle
10136
+ }
10137
+ ),
10138
+ /* @__PURE__ */ jsx(
10139
+ SGhostButton,
10140
+ {
10141
+ icon: "chevronDown",
10142
+ size: "xs",
10143
+ ariaLabel: `${isHour ? "\uC2DC" : "\uBD84"} \uAC10\uC18C`,
10144
+ onClick: () => isHour ? changeHour(-1) : changeMinute(-minuteStep)
10145
+ }
10146
+ )
10147
+ ]
10148
+ }
10149
+ );
10150
+ };
10151
+ return /* @__PURE__ */ jsxs(
10152
+ "div",
10153
+ {
10154
+ className: "flex flex-col items-stretch justify-center overflow-hidden",
10155
+ style: {
10156
+ background: "var(--cmp-timepicker-selector-bg)",
10157
+ borderRadius: "var(--cmp-timepicker-selector-radius)",
10158
+ boxShadow: "4px 8px 16px -4px rgba(34,34,34,0.1), 0 0 24px -6px rgba(34,34,34,0.12)"
10159
+ },
10160
+ children: [
10161
+ useMeridiem && /* @__PURE__ */ jsx(
10162
+ "div",
10163
+ {
10164
+ className: "flex items-center justify-center",
10165
+ style: {
10166
+ padding: "var(--cmp-timepicker-selector-section-paddingY) var(--cmp-timepicker-selector-section-paddingX)"
10167
+ },
10168
+ children: /* @__PURE__ */ jsx("div", { className: "isolate flex", children: ["AM", "PM"].map((item, index) => {
10169
+ const selected = item === meridiem;
10170
+ return /* @__PURE__ */ jsx(
10171
+ "button",
10172
+ {
10173
+ type: "button",
10174
+ onClick: () => setMeridiem(item),
10175
+ className: cn(
10176
+ "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]",
10177
+ index === 0 ? "rounded-l-[var(--cmp-select-sm-radius)]" : "rounded-r-[var(--cmp-select-sm-radius)]",
10178
+ index === 0 ? "-mr-px" : "",
10179
+ 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)]"
10180
+ ),
10181
+ children: item === "AM" ? "\uC624\uC804" : "\uC624\uD6C4"
10182
+ },
10183
+ item
10184
+ );
10185
+ }) })
10186
+ }
10187
+ ),
10188
+ useMeridiem && /* @__PURE__ */ jsx("div", { className: "h-px w-full bg-[var(--sys-color-divider-default)]" }),
10189
+ /* @__PURE__ */ jsxs(
10190
+ "div",
10191
+ {
10192
+ className: "flex items-center justify-center",
10193
+ style: {
10194
+ gap: "var(--cmp-timepicker-selector-time-gap)",
10195
+ padding: "var(--cmp-timepicker-selector-section-paddingY) var(--cmp-timepicker-selector-section-paddingX)"
10196
+ },
10197
+ children: [
10198
+ renderColumn("hour"),
10199
+ /* @__PURE__ */ jsx(
10200
+ "span",
10201
+ {
10202
+ className: "text-center",
10203
+ style: {
10204
+ width: "var(--cmp-timepicker-selector-colon-size)",
10205
+ color: "var(--cmp-field-hint-color)",
10206
+ fontSize: "var(--cmp-timepicker-selector-colon-size)",
10207
+ lineHeight: "var(--cmp-timepicker-selector-colon-size)"
10208
+ },
10209
+ children: ":"
10210
+ }
10211
+ ),
10212
+ renderColumn("minute")
10213
+ ]
10214
+ }
10215
+ )
10216
+ ]
10217
+ }
10218
+ );
10219
+ }
10220
+ function STimePicker({
10221
+ value,
10222
+ onValueChange,
10223
+ onOpenChange,
10224
+ type = "default",
10225
+ size = "sm",
10226
+ placeholder = "00:00",
10227
+ disabled = false,
10228
+ clearable = false,
10229
+ useMeridiem,
10230
+ minuteStep = 1,
10231
+ width,
10232
+ name,
10233
+ rules,
10234
+ status,
10235
+ label,
10236
+ labelWidth,
10237
+ icon,
10238
+ iconColor,
10239
+ labelTooltip,
10240
+ labelTooltipProps,
10241
+ addonLabel,
10242
+ addonAlign,
10243
+ hint,
10244
+ error,
10245
+ errorMessage,
10246
+ className,
10247
+ style
10248
+ }) {
10249
+ const portalContainer = usePortalContainer();
10250
+ const [open, setOpen] = useState(false);
10251
+ const [focused, setFocused] = useState(false);
10252
+ const [ruleError, setRuleError] = useState("");
10253
+ const dims = TIMEPICKER_SIZE_CONFIG[size] ?? TIMEPICKER_SIZE_CONFIG.sm;
10254
+ const normalized = value ? formatTimeValue(parseTimeValue(value).hour, parseTimeValue(value).minute) : null;
10255
+ const effectiveUseMeridiem = useMeridiem ?? type === "midday";
10256
+ const displayText = formatTimeDisplay(normalized, effectiveUseMeridiem ? "midday" : "default");
10257
+ const hasValue = normalized != null && normalized !== "";
10258
+ useFormField({
10259
+ name,
10260
+ validate: () => {
10261
+ const msg = runRules(value, rules);
10262
+ setRuleError(msg);
10263
+ return msg === "";
10264
+ },
10265
+ resetValidation: () => setRuleError("")
10266
+ });
10267
+ const resolvedError = error || ruleError !== "";
10268
+ const resolvedStatus = status ?? (rules && rules.length > 0 ? ruleError !== "" ? "error" : void 0 : void 0);
10269
+ const commitOpen = (next) => {
10270
+ if (disabled) return;
10271
+ setOpen(next);
10272
+ onOpenChange?.(next);
10273
+ if (!next && rules && rules.length > 0) setRuleError(runRules(value, rules));
10274
+ };
10275
+ const commitValue = (next) => {
10276
+ onValueChange?.(next);
10277
+ if (rules && rules.length > 0) setRuleError(runRules(next, rules));
10278
+ };
10279
+ return /* @__PURE__ */ jsx(
10280
+ SField,
10281
+ {
10282
+ label,
10283
+ labelWidth,
10284
+ icon,
10285
+ iconColor,
10286
+ labelTooltip,
10287
+ labelTooltipProps,
10288
+ addonLabel,
10289
+ addonAlign,
10290
+ size,
10291
+ hint,
10292
+ error: resolvedError,
10293
+ status: resolvedStatus,
10294
+ focused: focused || open,
10295
+ errorMessage: ruleError !== "" ? ruleError : errorMessage,
10296
+ width,
10297
+ disabled,
10298
+ className,
10299
+ style: { minWidth: dims.fieldMinWidth, ...style },
10300
+ children: /* @__PURE__ */ jsxs(RPopover2.Root, { open, onOpenChange: commitOpen, children: [
10301
+ /* @__PURE__ */ jsx(RPopover2.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(
10302
+ "button",
10303
+ {
10304
+ type: "button",
10305
+ name,
10306
+ disabled,
10307
+ onFocus: () => setFocused(true),
10308
+ onBlur: () => setFocused(false),
10309
+ className: "flex h-full w-full items-center overflow-hidden bg-transparent outline-none disabled:cursor-not-allowed",
10310
+ style: { padding: `0 ${dims.paddingX}`, gap: dims.gap, borderRadius: dims.radius },
10311
+ children: [
10312
+ /* @__PURE__ */ jsx(
10313
+ SIcon,
10314
+ {
10315
+ name: "clockOutline",
10316
+ size: dims.icon,
10317
+ color: disabled ? "var(--cmp-timepicker-icon-disabled)" : "var(--cmp-timepicker-icon-default)",
10318
+ className: "flex-shrink-0"
10319
+ }
10320
+ ),
10321
+ /* @__PURE__ */ jsx(
10322
+ "span",
10323
+ {
10324
+ className: "flex-1 truncate text-center",
10325
+ style: {
10326
+ minWidth: dims.minWidth,
10327
+ fontSize: dims.fontSize,
10328
+ lineHeight: `${dims.lineHeight}px`,
10329
+ color: disabled ? "var(--cmp-timepicker-text-disabled)" : hasValue ? "var(--cmp-timepicker-text-default)" : "var(--cmp-field-hint-color)"
10330
+ },
10331
+ children: hasValue ? displayText : placeholder
10332
+ }
10333
+ ),
10334
+ clearable && hasValue && !disabled && /* @__PURE__ */ jsx(
10335
+ SGhostButton,
10336
+ {
10337
+ icon: "close",
10338
+ size: "xs",
10339
+ ariaLabel: "\uC2DC\uAC04 \uC9C0\uC6B0\uAE30",
10340
+ onClick: (event) => {
10341
+ event.stopPropagation();
10342
+ commitValue(null);
10343
+ }
10344
+ }
10345
+ )
10346
+ ]
10347
+ }
10348
+ ) }),
10349
+ /* @__PURE__ */ jsx(RPopover2.Portal, { container: portalContainer, children: /* @__PURE__ */ jsx(
10350
+ RPopover2.Content,
10351
+ {
10352
+ side: "bottom",
10353
+ align: "start",
10354
+ sideOffset: 4,
10355
+ className: cn("z-50", FLOATING_SLIDE_ANIM),
10356
+ children: /* @__PURE__ */ jsx(
10357
+ TimeSelector,
10358
+ {
10359
+ value: normalized,
10360
+ useMeridiem: effectiveUseMeridiem,
10361
+ minuteStep,
10362
+ onChange: commitValue
10363
+ }
10364
+ )
10365
+ }
10366
+ ) })
10367
+ ] })
10368
+ }
10369
+ );
10370
+ }
10371
+
10372
+ // src/components/STimeRangePicker/time-range-picker.config.ts
10373
+ function getTimeMinutes(value) {
10374
+ const { hour, minute } = parseTimeValue(value);
10375
+ return hour * 60 + minute;
10376
+ }
10377
+ function normalizeTimeRangeValue(value, rangeOrder = "strict") {
10378
+ if (value == null) return null;
10379
+ const normalized = value.map((item) => {
10380
+ const parsed = parseTimeValue(item);
10381
+ return formatTimeValue(parsed.hour, parsed.minute);
10382
+ });
10383
+ if (rangeOrder === "strict" && getTimeMinutes(normalized[0]) > getTimeMinutes(normalized[1])) {
10384
+ return [normalized[0], normalized[0]];
10385
+ }
10386
+ return normalized;
10387
+ }
10388
+ function formatTimeRangeDisplay(value, type) {
10389
+ if (value == null || value[0] === "" || value[1] === "") return "";
10390
+ return `${formatTimeDisplay(value[0], type)} ~ ${formatTimeDisplay(value[1], type)}`;
10391
+ }
10392
+ function RangeTimeSelector({
10393
+ value,
10394
+ useMeridiem,
10395
+ minuteStep,
10396
+ rangeOrder,
10397
+ onChange
10398
+ }) {
10399
+ const range = useMemo(() => value ?? ["00:00", "23:59"], [value]);
10400
+ const [editingKey, setEditingKey] = useState(null);
10401
+ const [inputDraft, setInputDraft] = useState({});
10402
+ const autoAdjustedRef = useRef(null);
10403
+ const getParts = (side) => parseTimeValue(side === "start" ? range[0] : range[1]);
10404
+ const getMeridiem = (side) => getParts(side).hour >= 12 ? "PM" : "AM";
10405
+ const getDisplayHour = (side) => {
10406
+ const { hour } = getParts(side);
10407
+ if (!useMeridiem) return hour;
10408
+ const displayHour = hour % 12;
10409
+ return displayHour === 0 ? 12 : displayHour;
10410
+ };
10411
+ const commit = (side, hour, minute, direction = "unknown") => {
10412
+ const next = formatTimeValue(hour, minute);
10413
+ onChange(
10414
+ normalizeNextRange(side === "start" ? [next, range[1]] : [range[0], next], side, direction)
10415
+ );
10416
+ };
10417
+ const getTimeMinutes2 = (time) => {
10418
+ const { hour, minute } = parseTimeValue(time);
10419
+ return hour * 60 + minute;
10420
+ };
10421
+ const normalizeNextRange = (nextRange, changedSide, direction) => {
10422
+ if (rangeOrder === "allow-cross-day") return nextRange;
10423
+ const [start, end] = nextRange;
10424
+ const [currentStart, currentEnd] = range;
10425
+ if (getTimeMinutes2(start) <= getTimeMinutes2(end)) {
10426
+ const shouldKeepSynced = autoAdjustedRef.current?.side === changedSide && currentStart === currentEnd && start !== end && (direction === "unknown" || autoAdjustedRef.current.direction === direction);
10427
+ if (shouldKeepSynced) {
10428
+ return changedSide === "start" ? [start, start] : [end, end];
10429
+ }
10430
+ autoAdjustedRef.current = null;
10431
+ return nextRange;
10432
+ }
10433
+ autoAdjustedRef.current = {
10434
+ side: changedSide,
10435
+ direction: changedSide === "start" ? "increase" : "decrease"
10436
+ };
10437
+ return changedSide === "start" ? [start, start] : [end, end];
10438
+ };
10439
+ const changeHour = (side, delta) => {
10440
+ const { hour, minute } = getParts(side);
10441
+ commit(side, (hour + delta + 24) % 24, minute, delta > 0 ? "increase" : "decrease");
10442
+ };
10443
+ const changeMinute = (side, delta) => {
10444
+ const { hour, minute } = getParts(side);
10445
+ const next = minute + delta;
10446
+ if (next > 59) {
10447
+ commit(side, (hour + 1) % 24, next - 60, delta > 0 ? "increase" : "decrease");
10448
+ return;
10449
+ }
10450
+ if (next < 0) {
10451
+ commit(side, (hour + 23) % 24, next + 60, delta > 0 ? "increase" : "decrease");
10452
+ return;
10453
+ }
10454
+ commit(side, hour, next, delta > 0 ? "increase" : "decrease");
10455
+ };
10456
+ const setMeridiem = (side, next) => {
10457
+ const current = getMeridiem(side);
10458
+ if (current === next) return;
10459
+ const { hour, minute } = getParts(side);
10460
+ commit(side, next === "AM" ? hour - 12 : hour + 12, minute);
10461
+ };
10462
+ const getEditingKey = (side, part) => `${side}-${part}`;
10463
+ const sanitizeInput = (next) => next.replace(/\D/g, "").slice(0, 2);
10464
+ const beginEdit = (side, part, currentValue) => {
10465
+ const key = getEditingKey(side, part);
10466
+ setEditingKey(key);
10467
+ setInputDraft((prev) => ({ ...prev, [key]: String(currentValue).padStart(2, "0") }));
10468
+ };
10469
+ const commitHourDraft = (side, draft) => {
10470
+ const clamped = clampTimePart(Number(draft), useMeridiem ? 1 : 0, useMeridiem ? 12 : 23);
10471
+ const parts = getParts(side);
10472
+ const meridiem = getMeridiem(side);
10473
+ const nextHour = useMeridiem ? meridiem === "PM" ? clamped === 12 ? 12 : clamped + 12 : clamped === 12 ? 0 : clamped : clamped;
10474
+ commit(side, nextHour, parts.minute);
10475
+ return clamped;
10476
+ };
10477
+ const commitMinuteDraft = (side, draft) => {
10478
+ const { hour } = getParts(side);
10479
+ const clamped = clampTimePart(Number(draft), 0, 59);
10480
+ commit(side, hour, clamped);
10481
+ return clamped;
10482
+ };
10483
+ const endEdit = (side, part) => {
10484
+ const key = getEditingKey(side, part);
10485
+ const draft = inputDraft[key];
10486
+ if (part === "hour" && draft != null && draft !== "") {
10487
+ commitHourDraft(side, draft);
10488
+ }
10489
+ if (part === "minute" && draft != null && draft !== "") {
10490
+ commitMinuteDraft(side, draft);
10491
+ }
10492
+ setEditingKey(null);
10493
+ setInputDraft({});
10494
+ };
10495
+ const inputStyle = {
10496
+ width: "var(--cmp-timepicker-selector-input-width)",
10497
+ height: "var(--cmp-textinput-sm-height)",
10498
+ borderRadius: "var(--cmp-textinput-sm-radius)",
10499
+ border: "var(--cmp-textinput-borderWidth) solid var(--cmp-textinput-border-default)",
10500
+ color: "var(--cmp-textinput-text-default)",
10501
+ fontSize: 12,
10502
+ lineHeight: "20px"
10503
+ };
10504
+ const renderColumn = (side, kind) => {
10505
+ const isHour = kind === "hour";
10506
+ const { minute } = getParts(side);
10507
+ const val = isHour ? getDisplayHour(side) : minute;
10508
+ const currentEditingKey = getEditingKey(side, kind);
10509
+ const displayValue = editingKey === currentEditingKey ? inputDraft[currentEditingKey] ?? "" : String(val).padStart(2, "0");
10510
+ return /* @__PURE__ */ jsxs(
10511
+ "div",
10512
+ {
10513
+ className: "flex flex-col items-center justify-center",
10514
+ style: { gap: "var(--cmp-timepicker-selector-time-column-gap)" },
10515
+ children: [
10516
+ /* @__PURE__ */ jsx(
10517
+ SGhostButton,
10518
+ {
10519
+ icon: "chevronUp",
10520
+ size: "xs",
10521
+ ariaLabel: `${side === "start" ? "\uC2DC\uC791" : "\uC885\uB8CC"} ${isHour ? "\uC2DC" : "\uBD84"} \uC99D\uAC00`,
10522
+ onClick: () => isHour ? changeHour(side, 1) : changeMinute(side, minuteStep)
10523
+ }
10524
+ ),
10525
+ /* @__PURE__ */ jsx(
10526
+ "input",
10527
+ {
10528
+ "aria-label": `${side === "start" ? "\uC2DC\uC791" : "\uC885\uB8CC"} ${isHour ? "\uC2DC" : "\uBD84"}`,
10529
+ inputMode: "numeric",
10530
+ value: displayValue,
10531
+ placeholder: "-",
10532
+ onFocus: () => beginEdit(side, kind, val),
10533
+ onBlur: () => endEdit(side, kind),
10534
+ onChange: (event) => {
10535
+ const draft = sanitizeInput(event.target.value);
10536
+ setInputDraft((prev) => ({ ...prev, [currentEditingKey]: draft }));
10537
+ if (draft === "") return;
10538
+ const raw = Number(draft);
10539
+ if (isHour) {
10540
+ if (draft.length < 2) return;
10541
+ const clamped2 = commitHourDraft(side, draft);
10542
+ const displayDraft2 = raw === clamped2 ? draft : String(clamped2);
10543
+ setInputDraft((prev) => ({ ...prev, [currentEditingKey]: displayDraft2 }));
10544
+ return;
10545
+ }
10546
+ if (draft.length < 2) return;
10547
+ const clamped = commitMinuteDraft(side, draft);
10548
+ const displayDraft = raw === clamped ? draft : String(clamped);
10549
+ setInputDraft((prev) => ({ ...prev, [currentEditingKey]: displayDraft }));
10550
+ },
10551
+ className: "box-border bg-white text-center outline-none",
10552
+ style: inputStyle
10553
+ }
10554
+ ),
10555
+ /* @__PURE__ */ jsx(
10556
+ SGhostButton,
10557
+ {
10558
+ icon: "chevronDown",
10559
+ size: "xs",
10560
+ ariaLabel: `${side === "start" ? "\uC2DC\uC791" : "\uC885\uB8CC"} ${isHour ? "\uC2DC" : "\uBD84"} \uAC10\uC18C`,
10561
+ onClick: () => isHour ? changeHour(side, -1) : changeMinute(side, -minuteStep)
10562
+ }
10563
+ )
10564
+ ]
10565
+ }
10566
+ );
10567
+ };
10568
+ const renderPanel = (side, title) => /* @__PURE__ */ jsxs(
10569
+ "div",
10570
+ {
10571
+ className: "flex flex-col items-center justify-center",
10572
+ style: { gap: "var(--cmp-timepicker-selector-range-section-gap)" },
10573
+ children: [
10574
+ /* @__PURE__ */ jsxs(
10575
+ "div",
10576
+ {
10577
+ className: "flex flex-col items-center",
10578
+ style: { gap: "var(--cmp-timepicker-selector-range-heading-gap)" },
10579
+ children: [
10580
+ /* @__PURE__ */ jsx("div", { className: "whitespace-nowrap text-center text-[12px] font-bold leading-[20px] text-[var(--cmp-timepicker-selector-range-heading-color)]", children: title }),
10581
+ useMeridiem && /* @__PURE__ */ jsx("div", { className: "isolate flex", children: ["AM", "PM"].map((item, index) => {
10582
+ const selected = item === getMeridiem(side);
10583
+ return /* @__PURE__ */ jsx(
10584
+ "button",
10585
+ {
10586
+ type: "button",
10587
+ onClick: () => setMeridiem(side, item),
10588
+ className: cn(
10589
+ "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]",
10590
+ index === 0 ? "rounded-l-[var(--cmp-select-sm-radius)] -mr-px" : "rounded-r-[var(--cmp-select-sm-radius)]",
10591
+ 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)]"
10592
+ ),
10593
+ children: item === "AM" ? "\uC624\uC804" : "\uC624\uD6C4"
10594
+ },
10595
+ item
10596
+ );
10597
+ }) })
10598
+ ]
10599
+ }
10600
+ ),
10601
+ /* @__PURE__ */ jsxs(
10602
+ "div",
10603
+ {
10604
+ className: "flex items-center justify-center",
10605
+ style: {
10606
+ gap: "var(--cmp-timepicker-selector-time-gap)",
10607
+ padding: "0 var(--cmp-timepicker-selector-section-paddingX)"
10608
+ },
10609
+ children: [
10610
+ renderColumn(side, "hour"),
10611
+ /* @__PURE__ */ jsx(
10612
+ "span",
10613
+ {
10614
+ className: "text-center",
10615
+ style: {
10616
+ width: "var(--cmp-timepicker-selector-colon-size)",
10617
+ color: "var(--cmp-field-hint-color)",
10618
+ fontSize: "var(--cmp-timepicker-selector-colon-size)",
10619
+ lineHeight: "var(--cmp-timepicker-selector-colon-size)"
10620
+ },
10621
+ children: ":"
10622
+ }
10623
+ ),
10624
+ renderColumn(side, "minute")
10625
+ ]
10626
+ }
10627
+ )
10628
+ ]
10629
+ }
10630
+ );
10631
+ return /* @__PURE__ */ jsxs(
10632
+ "div",
10633
+ {
10634
+ className: "flex items-start justify-center overflow-hidden",
10635
+ style: {
10636
+ gap: "var(--cmp-timepicker-selector-range-panelGap)",
10637
+ padding: "var(--cmp-timepicker-selector-range-paddingXY)",
10638
+ background: "var(--cmp-timepicker-selector-bg)",
10639
+ borderRadius: "var(--cmp-timepicker-selector-radius)",
10640
+ boxShadow: "var(--shadow-spread-sm), var(--shadow-spread-md)"
10641
+ },
10642
+ children: [
10643
+ renderPanel("start", "\uC2DC\uC791 \uC2DC\uAC04"),
10644
+ /* @__PURE__ */ jsx("div", { className: "self-stretch w-px bg-[var(--sys-color-divider-default)]" }),
10645
+ renderPanel("end", "\uC885\uB8CC \uC2DC\uAC04")
10646
+ ]
10647
+ }
10648
+ );
10649
+ }
10650
+ function STimeRangePicker({
10651
+ value,
10652
+ onValueChange,
10653
+ onOpenChange,
10654
+ type = "default",
10655
+ size = "sm",
10656
+ placeholder = "00:00 ~ 23:59",
10657
+ disabled = false,
10658
+ clearable = false,
10659
+ useMeridiem,
10660
+ minuteStep = 1,
10661
+ rangeOrder = "strict",
10662
+ width,
10663
+ name,
10664
+ rules,
10665
+ status,
10666
+ label,
10667
+ labelWidth,
10668
+ icon,
10669
+ iconColor,
10670
+ labelTooltip,
10671
+ labelTooltipProps,
10672
+ addonLabel,
10673
+ addonAlign,
10674
+ hint,
10675
+ error,
10676
+ errorMessage,
10677
+ className,
10678
+ style
10679
+ }) {
10680
+ const portalContainer = usePortalContainer();
10681
+ const [open, setOpen] = useState(false);
10682
+ const [focused, setFocused] = useState(false);
10683
+ const [ruleError, setRuleError] = useState("");
10684
+ const dims = TIMEPICKER_SIZE_CONFIG[size] ?? TIMEPICKER_SIZE_CONFIG.sm;
10685
+ const normalized = normalizeTimeRangeValue(value ?? null, rangeOrder);
10686
+ const effectiveUseMeridiem = useMeridiem ?? type === "midday";
10687
+ const displayText = formatTimeRangeDisplay(
10688
+ normalized,
10689
+ effectiveUseMeridiem ? "midday" : "default"
10690
+ );
10691
+ const hasValue = normalized != null && normalized[0] !== "" && normalized[1] !== "";
10692
+ useFormField({
10693
+ name,
10694
+ validate: () => {
10695
+ const msg = runRules(value, rules);
10696
+ setRuleError(msg);
10697
+ return msg === "";
10698
+ },
10699
+ resetValidation: () => setRuleError("")
10700
+ });
10701
+ const resolvedError = error || ruleError !== "";
10702
+ const resolvedStatus = status ?? (rules && rules.length > 0 ? ruleError !== "" ? "error" : void 0 : void 0);
10703
+ const commitOpen = (next) => {
10704
+ if (disabled) return;
10705
+ setOpen(next);
10706
+ onOpenChange?.(next);
10707
+ if (!next && rules && rules.length > 0) setRuleError(runRules(value, rules));
10708
+ };
10709
+ const commitValue = (next) => {
10710
+ onValueChange?.(next);
10711
+ if (rules && rules.length > 0) setRuleError(runRules(next, rules));
10712
+ };
10713
+ return /* @__PURE__ */ jsx(
10714
+ SField,
10715
+ {
10716
+ label,
10717
+ labelWidth,
10718
+ icon,
10719
+ iconColor,
10720
+ labelTooltip,
10721
+ labelTooltipProps,
10722
+ addonLabel,
10723
+ addonAlign,
10724
+ size,
10725
+ hint,
10726
+ error: resolvedError,
10727
+ status: resolvedStatus,
10728
+ focused: focused || open,
10729
+ errorMessage: ruleError !== "" ? ruleError : errorMessage,
10730
+ width,
10731
+ disabled,
10732
+ className,
10733
+ style: { minWidth: dims.fieldMinWidth, ...style },
10734
+ children: /* @__PURE__ */ jsxs(RPopover2.Root, { open, onOpenChange: commitOpen, children: [
10735
+ /* @__PURE__ */ jsx(RPopover2.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(
10736
+ "button",
10737
+ {
10738
+ type: "button",
10739
+ name,
10740
+ disabled,
10741
+ onFocus: () => setFocused(true),
10742
+ onBlur: () => setFocused(false),
10743
+ className: "flex h-full w-full items-center overflow-hidden bg-transparent outline-none disabled:cursor-not-allowed",
10744
+ style: { padding: `0 ${dims.paddingX}`, gap: dims.gap, borderRadius: dims.radius },
10745
+ children: [
10746
+ /* @__PURE__ */ jsx(
10747
+ SIcon,
10748
+ {
10749
+ name: "clockOutline",
10750
+ size: dims.icon,
10751
+ color: disabled ? "var(--cmp-timepicker-icon-disabled)" : "var(--cmp-timepicker-icon-default)",
10752
+ className: "flex-shrink-0"
10753
+ }
10754
+ ),
10755
+ /* @__PURE__ */ jsx(
10756
+ "span",
10757
+ {
10758
+ className: "flex-1 truncate text-center",
10759
+ style: {
10760
+ minWidth: dims.minWidth,
10761
+ fontSize: dims.fontSize,
10762
+ lineHeight: `${dims.lineHeight}px`,
10763
+ color: disabled ? "var(--cmp-timepicker-text-disabled)" : hasValue ? "var(--cmp-timepicker-text-default)" : "var(--cmp-field-hint-color)"
10764
+ },
10765
+ children: hasValue ? displayText : placeholder
10766
+ }
10767
+ ),
10768
+ clearable && hasValue && !disabled && /* @__PURE__ */ jsx(
10769
+ SGhostButton,
10770
+ {
10771
+ icon: "close",
10772
+ size: "xs",
10773
+ ariaLabel: "\uC2DC\uAC04 \uBC94\uC704 \uC9C0\uC6B0\uAE30",
10774
+ onClick: (event) => {
10775
+ event.stopPropagation();
10776
+ commitValue(null);
10777
+ }
10778
+ }
10779
+ )
10780
+ ]
10781
+ }
10782
+ ) }),
10783
+ /* @__PURE__ */ jsx(RPopover2.Portal, { container: portalContainer, children: /* @__PURE__ */ jsx(
10784
+ RPopover2.Content,
10785
+ {
10786
+ side: "bottom",
10787
+ align: "start",
10788
+ sideOffset: 4,
10789
+ className: cn("z-50", FLOATING_SLIDE_ANIM),
10790
+ children: /* @__PURE__ */ jsx(
10791
+ RangeTimeSelector,
10792
+ {
10793
+ value: normalized,
10794
+ useMeridiem: effectiveUseMeridiem,
10795
+ minuteStep,
10796
+ rangeOrder,
10797
+ onChange: commitValue
10798
+ }
10799
+ )
10800
+ }
10801
+ ) })
10802
+ ] })
10803
+ }
10804
+ );
10805
+ }
10806
+
10807
+ export { BADGE_COLORS, BUTTON_COLORS, BUTTON_SIZES, COLOR_KEYS, GNB_COLORS, GNB_COMMON, GNB_FOLD_MS, GNB_HEADER, GNB_MENU, GNB_MENU_WIDTH, GNB_RAIL, GNB_RAIL_WIDTH, ICONS, SActionModal, SBadge, SBarcodeInput, SButton, SCalendar, SCallout, SCard, SCheckbox, SChip, SChipInput, SCircleProgress, SConfirmModal, SDatePicker, SDateRangePicker, SDivider, SDropdownButton, SField, SFilePicker, SForm, SGhostButton, SGnb, SGuide, SIcon, SInput, SKeyValueTable, SLayout, SLayoutContext, SLinearProgress, SLoadingContainer, SLoadingModal, SLoadingViewport, SNumberInput, SPage, SPagination, SPopover, SPopup, SPortal, SRadio, SRadioButton, SRadioGroup, SSelect, SSwitch, STable, STabs, STag, STextLink, STextarea, STimePicker, STimeRangePicker, SToast, SToastContainer, SToggle, STooltip, TAG_COLORS, TAG_SHAPES, TAG_SIZES, TIMEPICKER_SIZES, TIMEPICKER_SIZE_CONFIG, buttonPreset, cn, findGnbAncestors, findGnbRailValue, isColorKey, loading, resolveColor, useSLayout };
9810
10808
  //# sourceMappingURL=index.js.map
9811
10809
  //# sourceMappingURL=index.js.map