sellmate-design-system-react 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +20 -0
  2. package/dist/components/SDateRangePicker/SDateRangePicker.cjs +180 -37
  3. package/dist/components/SDateRangePicker/SDateRangePicker.cjs.map +1 -1
  4. package/dist/components/SDateRangePicker/SDateRangePicker.d.ts +6 -1
  5. package/dist/components/SDateRangePicker/SDateRangePicker.js +180 -37
  6. package/dist/components/SDateRangePicker/SDateRangePicker.js.map +1 -1
  7. package/dist/components/SGnb/SGnb.cjs +145 -57
  8. package/dist/components/SGnb/SGnb.cjs.map +1 -1
  9. package/dist/components/SGnb/SGnb.d.ts +7 -1
  10. package/dist/components/SGnb/SGnb.js +146 -58
  11. package/dist/components/SGnb/SGnb.js.map +1 -1
  12. package/dist/components/SKeyValueTable/SKeyValueTable.cjs +180 -37
  13. package/dist/components/SKeyValueTable/SKeyValueTable.cjs.map +1 -1
  14. package/dist/components/SKeyValueTable/SKeyValueTable.js +180 -37
  15. package/dist/components/SKeyValueTable/SKeyValueTable.js.map +1 -1
  16. package/dist/components/SLayout/SLayout.cjs +21 -6
  17. package/dist/components/SLayout/SLayout.cjs.map +1 -1
  18. package/dist/components/SLayout/SLayout.d.ts +17 -2
  19. package/dist/components/SLayout/SLayout.js +21 -6
  20. package/dist/components/SLayout/SLayout.js.map +1 -1
  21. package/dist/components/SPage/SPage.cjs +21 -6
  22. package/dist/components/SPage/SPage.cjs.map +1 -1
  23. package/dist/components/SPage/SPage.js +21 -6
  24. package/dist/components/SPage/SPage.js.map +1 -1
  25. package/dist/components/SStepper/SStepper.cjs +3019 -0
  26. package/dist/components/SStepper/SStepper.cjs.map +1 -0
  27. package/dist/components/SStepper/SStepper.d.ts +16 -0
  28. package/dist/components/SStepper/SStepper.js +3017 -0
  29. package/dist/components/SStepper/SStepper.js.map +1 -0
  30. package/dist/components/SStepper/index.d.ts +2 -0
  31. package/dist/components/SStepper/stepper.config.d.ts +42 -0
  32. package/dist/index.cjs +500 -104
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.ts +1 -0
  35. package/dist/index.js +499 -105
  36. package/dist/index.js.map +1 -1
  37. package/dist/styles.css +61 -0
  38. package/package.json +3 -1
package/dist/index.cjs CHANGED
@@ -5069,15 +5069,30 @@ var SLayout = react.forwardRef(function SLayout2({
5069
5069
  },
5070
5070
  [isControlled, onFoldedChange]
5071
5071
  );
5072
+ const [gnbState, setGnbState] = react.useState({ forceFull: false, menuVisible: true });
5073
+ const reportGnbState = react.useCallback(
5074
+ (next) => setGnbState(
5075
+ (prev) => prev.forceFull === next.forceFull && prev.menuVisible === next.menuVisible ? prev : next
5076
+ ),
5077
+ []
5078
+ );
5079
+ const effectiveHeader = header === "full" || gnbState.forceFull ? "full" : "fix";
5072
5080
  const ctx = react.useMemo(
5073
- () => ({ type, header, useRail, folded: currentFolded, setFolded }),
5074
- [type, header, useRail, currentFolded, setFolded]
5081
+ () => ({
5082
+ type,
5083
+ header: effectiveHeader,
5084
+ useRail,
5085
+ folded: currentFolded,
5086
+ setFolded,
5087
+ reportGnbState
5088
+ }),
5089
+ [type, effectiveHeader, useRail, currentFolded, setFolded, reportGnbState]
5075
5090
  );
5076
- const isFullHeader = header === "full";
5077
- const gnbColumnWidth = useRail ? GNB_RAIL_WIDTH + GNB_MENU_WIDTH : GNB_MENU_WIDTH;
5091
+ const isFullHeader = effectiveHeader === "full";
5092
+ const gnbColumnWidth = (useRail ? GNB_RAIL_WIDTH : 0) + (gnbState.menuVisible ? GNB_MENU_WIDTH : 0);
5078
5093
  const frameStyle = isFullHeader ? {
5079
5094
  gridTemplateColumns: `${currentFolded ? 0 : gnbColumnWidth}px 1fr`,
5080
- gridTemplateRows: `${GNB_HEADER[header].topHeight} 1fr`,
5095
+ gridTemplateRows: `${GNB_HEADER[effectiveHeader].topHeight} 1fr`,
5081
5096
  // 메뉴가 미끄러지는 동안 페이지 열도 같은 길이로 따라와야 한 덩어리로 읽힌다.
5082
5097
  // (fix 는 SGnb 가 제 폭을 직접 전환하므로 프레임이 손댈 게 없다)
5083
5098
  transition: `grid-template-columns ${GNB_FOLD_MS}ms ease-out`
@@ -5087,7 +5102,7 @@ var SLayout = react.forwardRef(function SLayout2({
5087
5102
  {
5088
5103
  ref,
5089
5104
  "data-type": type,
5090
- "data-header": header,
5105
+ "data-header": effectiveHeader,
5091
5106
  className: cn(
5092
5107
  "box-border h-full overflow-hidden",
5093
5108
  isFullHeader ? "grid" : "flex flex-row",
@@ -5120,9 +5135,10 @@ var SGnb = react.forwardRef(function SGnb2({
5120
5135
  }, ref) {
5121
5136
  const layout = react.useContext(SLayoutContext);
5122
5137
  const type = typeProp ?? layout?.type ?? "box";
5123
- const header = headerProp ?? layout?.header ?? "fix";
5124
5138
  const folded = foldedProp ?? layout?.folded ?? false;
5125
5139
  const useRail = useRailProp ?? layout?.useRail ?? false;
5140
+ const railHasLeaf = useRail && items.some((item) => !item.children || item.children.length === 0);
5141
+ const header = railHasLeaf ? "full" : headerProp ?? layout?.header ?? "fix";
5126
5142
  const H = GNB_HEADER[header];
5127
5143
  const M = GNB_MENU[type];
5128
5144
  const C = GNB_COLORS[color];
@@ -5256,9 +5272,24 @@ var SGnb = react.forwardRef(function SGnb2({
5256
5272
  onMouseEnter: () => setHoveredKey(item.value),
5257
5273
  onMouseLeave: () => setHoveredKey((prev) => prev === item.value ? null : prev),
5258
5274
  children: [
5259
- depth === 1 && item.icon && /* @__PURE__ */ jsxRuntime.jsx(SIcon, { name: item.icon, size: M.depth1Icon, color: C.itemIcon, className: "shrink-0" }),
5275
+ depth === 1 && item.icon && /* @__PURE__ */ jsxRuntime.jsx(
5276
+ SIcon,
5277
+ {
5278
+ name: item.icon,
5279
+ size: M.depth1Icon,
5280
+ color: isSelected ? C.itemColorActive : C.itemIcon,
5281
+ className: "shrink-0"
5282
+ }
5283
+ ),
5260
5284
  /* @__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,
5285
+ depth > 1 && iconBranch ? /* @__PURE__ */ jsxRuntime.jsx(
5286
+ "div",
5287
+ {
5288
+ className: "w-[20px] flex items-center justify-center mr-[8px]",
5289
+ style: { height: M.itemLineHeight },
5290
+ children: "-"
5291
+ }
5292
+ ) : null,
5262
5293
  item.label
5263
5294
  ] }),
5264
5295
  item.tag !== void 0 && item.tag !== "" && /* @__PURE__ */ jsxRuntime.jsx(
@@ -5310,16 +5341,28 @@ var SGnb = react.forwardRef(function SGnb2({
5310
5341
  };
5311
5342
  const railItems = useRail ? items : [];
5312
5343
  const menuItems = useRail ? items.find((item) => item.value === activeRail)?.children ?? [] : items;
5313
- const bodyWidth = useRail ? GNB_RAIL_WIDTH + GNB_MENU_WIDTH : GNB_MENU_WIDTH;
5344
+ const showMenu = !useRail || menuItems.length > 0;
5345
+ const bodyWidth = (useRail ? GNB_RAIL_WIDTH : 0) + (showMenu ? GNB_MENU_WIDTH : 0);
5346
+ react.useEffect(() => {
5347
+ layout?.reportGnbState({ forceFull: railHasLeaf, menuVisible: showMenu });
5348
+ }, [layout, railHasLeaf, showMenu]);
5314
5349
  const railFolded = folded && header === "fix";
5315
5350
  const rootWidth = header === "full" ? "100%" : folded ? GNB_COMMON.foldWidth : bodyWidth;
5351
+ const rightBorder = color === "light" ? `1px solid ${C.topBorder}` : void 0;
5316
5352
  const rootStyle = {
5317
5353
  width: rootWidth,
5318
5354
  transition: `width ${GNB_FOLD_MS}ms ease-out`,
5319
- ...header === "full" ? { gridArea: "1 / 1 / -1 / -1", pointerEvents: "none" } : { backgroundColor: C.panelBg },
5355
+ ...header === "full" ? { gridArea: "1 / 1 / -1 / -1", pointerEvents: "none" } : { backgroundColor: C.panelBg, borderRight: rightBorder },
5320
5356
  ...style
5321
5357
  };
5322
5358
  const hideAfterSlide = (hidden) => `visibility 0s linear ${hidden ? `${GNB_FOLD_MS}ms` : "0s"}`;
5359
+ const foldInline = !!logo;
5360
+ const sideFoldStyle = {
5361
+ opacity: railFolded ? 0 : 1,
5362
+ position: railFolded ? "absolute" : "static",
5363
+ visibility: railFolded ? "hidden" : "visible",
5364
+ transition: `opacity ${GNB_FOLD_MS}ms ease-out, ${hideAfterSlide(railFolded)}`
5365
+ };
5323
5366
  return /* @__PURE__ */ jsxRuntime.jsxs(
5324
5367
  "div",
5325
5368
  {
@@ -5331,12 +5374,14 @@ var SGnb = react.forwardRef(function SGnb2({
5331
5374
  "data-color": color,
5332
5375
  ...rest,
5333
5376
  children: [
5334
- /* @__PURE__ */ jsxRuntime.jsxs(
5377
+ /* @__PURE__ */ jsxRuntime.jsx(
5335
5378
  "div",
5336
5379
  {
5337
5380
  className: "relative box-border flex shrink-0 items-center overflow-hidden",
5338
5381
  style: {
5339
5382
  height: H.topHeight,
5383
+ // 인라인 간격은 컨테이너 gap 대신 각 요소 margin 으로 준다 — 런처↔폴드(8px)와
5384
+ // 폴드↔로고(top-gap)를 다르게 두기 위함. 접히면 런처·로고가 흐름에서 빠져 margin 도 무의미해진다.
5340
5385
  padding: `0 ${railFolded ? GNB_RAIL.padding : H.topPaddingX}`,
5341
5386
  borderBottom: `1px solid ${C.topBorder}`,
5342
5387
  backgroundColor: C.panelBg,
@@ -5345,49 +5390,90 @@ var SGnb = react.forwardRef(function SGnb2({
5345
5390
  // 아이콘 버튼은 자체 ghostButton 토큰을 쓰므로, 이 색은 logo 슬롯 상속용
5346
5391
  color: C.itemColorDefault
5347
5392
  },
5348
- children: [
5349
- (onLauncherClick || logo) && /* @__PURE__ */ jsxRuntime.jsxs(
5350
- "div",
5351
- {
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
- ]
5376
- }
5377
- ),
5378
- /* @__PURE__ */ jsxRuntime.jsx(
5379
- SGhostButton,
5380
- {
5381
- icon: "sidebar",
5382
- size: "xs",
5383
- intent: color === "dark" ? "inverse" : "default",
5384
- ariaLabel: "toggle navigation",
5385
- ariaPressed: folded,
5386
- onClick: handleFoldToggle,
5387
- className: "ml-auto shrink-0"
5388
- }
5389
- )
5390
- ]
5393
+ children: foldInline ? (
5394
+ // 런처(있을 )·폴드·로고를 흐름에 나란히 둔다 폴드 버튼이 (런처와) 로고 사이에 선다.
5395
+ // 접히면 런처·로고가 페이드하며 흐름에서 빠지고(sideFoldStyle), 폴드 버튼만 남아 레일 가운데에 선다.
5396
+ /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5397
+ onLauncherClick && /* @__PURE__ */ jsxRuntime.jsx(
5398
+ SGhostButton,
5399
+ {
5400
+ icon: "launcher",
5401
+ size: "xs",
5402
+ intent: color === "dark" ? "inverse" : "default",
5403
+ ariaLabel: "app launcher",
5404
+ onClick: onLauncherClick,
5405
+ className: "shrink-0",
5406
+ style: { ...sideFoldStyle, marginRight: "8px" }
5407
+ }
5408
+ ),
5409
+ /* @__PURE__ */ jsxRuntime.jsx(
5410
+ SGhostButton,
5411
+ {
5412
+ icon: "sidebar",
5413
+ size: "xs",
5414
+ intent: color === "dark" ? "inverse" : "default",
5415
+ ariaLabel: "toggle navigation",
5416
+ ariaPressed: folded,
5417
+ onClick: handleFoldToggle,
5418
+ className: "shrink-0"
5419
+ }
5420
+ ),
5421
+ /* @__PURE__ */ jsxRuntime.jsx(
5422
+ "div",
5423
+ {
5424
+ className: "flex shrink-0 items-center",
5425
+ style: { height: H.logoHeight, marginLeft: H.topGap, ...sideFoldStyle },
5426
+ children: logo
5427
+ }
5428
+ )
5429
+ ] })
5430
+ ) : (
5431
+ // 런처·로고는 좌측 그룹(흐름에서 빼 폭 축소 중 폴드 버튼을 밀어내지 않는다),
5432
+ // 폴드 버튼은 우측 끝(ml-auto)이라 폭·패딩이 줄면 저절로 레일 가운데에 선다.
5433
+ /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5434
+ (onLauncherClick || logo) && /* @__PURE__ */ jsxRuntime.jsxs(
5435
+ "div",
5436
+ {
5437
+ className: "absolute inset-y-0 flex items-center",
5438
+ style: {
5439
+ left: H.topPaddingX,
5440
+ gap: H.topGap,
5441
+ opacity: railFolded ? 0 : 1,
5442
+ transition: `opacity ${GNB_FOLD_MS}ms ease-out, ${hideAfterSlide(railFolded)}, transform ${GNB_FOLD_MS}ms ease-out`,
5443
+ // 메뉴와 같은 방향으로 빠지되, 좁은 레일 안에서 꼬리가 비치지 않게 페이드가 거리를 대신한다
5444
+ transform: railFolded ? `translateX(-${H.topPaddingX})` : "none",
5445
+ visibility: railFolded ? "hidden" : "visible"
5446
+ },
5447
+ children: [
5448
+ onLauncherClick && /* @__PURE__ */ jsxRuntime.jsx(
5449
+ SGhostButton,
5450
+ {
5451
+ icon: "launcher",
5452
+ size: "xs",
5453
+ intent: color === "dark" ? "inverse" : "default",
5454
+ ariaLabel: "app launcher",
5455
+ onClick: onLauncherClick,
5456
+ className: "shrink-0"
5457
+ }
5458
+ ),
5459
+ logo && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center", style: { height: H.logoHeight }, children: logo })
5460
+ ]
5461
+ }
5462
+ ),
5463
+ /* @__PURE__ */ jsxRuntime.jsx(
5464
+ SGhostButton,
5465
+ {
5466
+ icon: "sidebar",
5467
+ size: "xs",
5468
+ intent: color === "dark" ? "inverse" : "default",
5469
+ ariaLabel: "toggle navigation",
5470
+ ariaPressed: folded,
5471
+ onClick: handleFoldToggle,
5472
+ className: "ml-auto shrink-0"
5473
+ }
5474
+ )
5475
+ ] })
5476
+ )
5391
5477
  }
5392
5478
  ),
5393
5479
  /* @__PURE__ */ jsxRuntime.jsxs(
@@ -5423,7 +5509,7 @@ var SGnb = react.forwardRef(function SGnb2({
5423
5509
  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
5510
  }
5425
5511
  ),
5426
- /* @__PURE__ */ jsxRuntime.jsx(
5512
+ showMenu && /* @__PURE__ */ jsxRuntime.jsx(
5427
5513
  "nav",
5428
5514
  {
5429
5515
  "aria-label": ariaLabel,
@@ -5433,6 +5519,8 @@ var SGnb = react.forwardRef(function SGnb2({
5433
5519
  paddingTop: M.bodyPaddingY,
5434
5520
  paddingBottom: M.bodyPaddingY,
5435
5521
  backgroundColor: C.panelBg,
5522
+ // full 은 상단바가 전폭이라 루트가 오른쪽 선을 못 그린다 → 메뉴 오른쪽(페이지 경계)이 대신 그린다.
5523
+ borderRight: header === "full" ? rightBorder : void 0,
5436
5524
  pointerEvents: "auto"
5437
5525
  },
5438
5526
  children: /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "m-0 flex list-none flex-col p-0", children: menuItems.map((item) => renderItem(item, 1)) })
@@ -5689,6 +5777,169 @@ function SPagination({
5689
5777
  }
5690
5778
  );
5691
5779
  }
5780
+
5781
+ // src/components/SStepper/stepper.config.ts
5782
+ var STEPPER_SIZES = ["sm", "lg"];
5783
+ var V2 = (path) => `var(--cmp-stepper-${path})`;
5784
+ var STEPPER_SIZE_CONFIG = {
5785
+ sm: {
5786
+ sequenceSize: V2("sm-sequence-size"),
5787
+ fontSize: "12px",
5788
+ lineHeight: "20px",
5789
+ fontWeightActive: 700,
5790
+ fontWeightDefault: 500
5791
+ },
5792
+ lg: {
5793
+ sequenceSize: V2("lg-sequence-size"),
5794
+ fontSize: "14px",
5795
+ lineHeight: "24px",
5796
+ fontWeightActive: 700,
5797
+ fontWeightDefault: 500
5798
+ }
5799
+ };
5800
+ var STEPPER_LAYOUT = {
5801
+ containerGap: V2("container-gap"),
5802
+ itemGap: V2("item-gap"),
5803
+ dashWidth: V2("dash-width"),
5804
+ sequenceRadius: V2("sequence-radius"),
5805
+ sequenceBorderWidth: V2("sequence-border-witdh"),
5806
+ sequenceIcon: V2("sequence-icon")
5807
+ };
5808
+ var STEPPER_COLORS = {
5809
+ dash: V2("dash-color"),
5810
+ sequenceBorder: {
5811
+ active: V2("sequence-border-active"),
5812
+ default: V2("sequence-border-default"),
5813
+ completed: V2("sequence-border-completed")
5814
+ },
5815
+ sequenceBg: {
5816
+ active: V2("sequence-bg-active"),
5817
+ default: V2("sequence-bg-default"),
5818
+ completed: V2("sequence-bg-completed")
5819
+ },
5820
+ sequenceText: {
5821
+ active: V2("sequence-text-active"),
5822
+ default: V2("sequence-text-default")
5823
+ },
5824
+ text: {
5825
+ active: V2("text-active"),
5826
+ default: V2("text-default")
5827
+ }
5828
+ };
5829
+ function getItemValue(item, index) {
5830
+ return item.value ?? String(index + 1);
5831
+ }
5832
+ function SStepper({
5833
+ items,
5834
+ value,
5835
+ size = "sm",
5836
+ ariaLabel = "\uC9C4\uD589 \uB2E8\uACC4",
5837
+ className,
5838
+ style
5839
+ }) {
5840
+ const config = STEPPER_SIZE_CONFIG[size];
5841
+ const activeIndex = Math.max(
5842
+ 0,
5843
+ items.findIndex((item, index) => getItemValue(item, index) === value)
5844
+ );
5845
+ const getState = (index) => {
5846
+ if (index < activeIndex) return "completed";
5847
+ if (index === activeIndex) return "active";
5848
+ return "default";
5849
+ };
5850
+ return /* @__PURE__ */ jsxRuntime.jsx(
5851
+ "div",
5852
+ {
5853
+ role: "list",
5854
+ "aria-label": ariaLabel,
5855
+ className: cn("inline-flex items-center", className),
5856
+ style: {
5857
+ gap: STEPPER_LAYOUT.containerGap,
5858
+ fontSize: config.fontSize,
5859
+ lineHeight: config.lineHeight,
5860
+ ...style
5861
+ },
5862
+ children: items.map((item, index) => {
5863
+ const state2 = getState(index);
5864
+ const itemStyle = {
5865
+ gap: STEPPER_LAYOUT.itemGap,
5866
+ color: state2 === "active" ? STEPPER_COLORS.text.active : STEPPER_COLORS.text.default,
5867
+ fontWeight: state2 === "active" ? config.fontWeightActive : config.fontWeightDefault
5868
+ };
5869
+ const sequenceStyle = {
5870
+ width: config.sequenceSize,
5871
+ height: config.sequenceSize,
5872
+ flexBasis: config.sequenceSize,
5873
+ borderRadius: STEPPER_LAYOUT.sequenceRadius,
5874
+ borderWidth: STEPPER_LAYOUT.sequenceBorderWidth,
5875
+ borderColor: STEPPER_COLORS.sequenceBorder[state2],
5876
+ background: STEPPER_COLORS.sequenceBg[state2],
5877
+ color: state2 === "completed" ? STEPPER_COLORS.text.default : STEPPER_COLORS.sequenceText[state2]
5878
+ };
5879
+ const content = /* @__PURE__ */ jsxRuntime.jsxs(
5880
+ "span",
5881
+ {
5882
+ role: "listitem",
5883
+ "aria-current": state2 === "active" ? "step" : void 0,
5884
+ className: "inline-flex items-center whitespace-nowrap",
5885
+ style: itemStyle,
5886
+ children: [
5887
+ /* @__PURE__ */ jsxRuntime.jsx(
5888
+ "span",
5889
+ {
5890
+ "aria-hidden": true,
5891
+ className: "box-border inline-flex shrink-0 items-center justify-center border border-solid font-bold",
5892
+ style: sequenceStyle,
5893
+ children: state2 === "completed" ? /* @__PURE__ */ jsxRuntime.jsx(SIcon, { name: "check", size: STEPPER_LAYOUT.sequenceIcon, color: "currentColor" }) : index + 1
5894
+ }
5895
+ ),
5896
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: item.label })
5897
+ ]
5898
+ }
5899
+ );
5900
+ return /* @__PURE__ */ jsxRuntime.jsxs(
5901
+ "span",
5902
+ {
5903
+ className: "inline-flex items-center",
5904
+ style: { gap: STEPPER_LAYOUT.containerGap },
5905
+ children: [
5906
+ content,
5907
+ index < items.length - 1 && /* @__PURE__ */ jsxRuntime.jsx(
5908
+ "span",
5909
+ {
5910
+ "aria-hidden": true,
5911
+ className: "inline-flex h-px shrink-0 items-center",
5912
+ style: { width: STEPPER_LAYOUT.dashWidth, color: STEPPER_COLORS.dash },
5913
+ children: /* @__PURE__ */ jsxRuntime.jsx(
5914
+ "svg",
5915
+ {
5916
+ width: "20",
5917
+ height: "1",
5918
+ viewBox: "0 0 20 1",
5919
+ fill: "none",
5920
+ xmlns: "http://www.w3.org/2000/svg",
5921
+ children: /* @__PURE__ */ jsxRuntime.jsx(
5922
+ "line",
5923
+ {
5924
+ y1: "0.5",
5925
+ x2: "20",
5926
+ y2: "0.5",
5927
+ stroke: "#888888",
5928
+ strokeDasharray: "4 4"
5929
+ }
5930
+ )
5931
+ }
5932
+ )
5933
+ }
5934
+ )
5935
+ ]
5936
+ },
5937
+ getItemValue(item, index)
5938
+ );
5939
+ })
5940
+ }
5941
+ );
5942
+ }
5692
5943
  function SCheckbox({
5693
5944
  value = false,
5694
5945
  val,
@@ -8088,25 +8339,45 @@ function SDatePicker({
8088
8339
  }
8089
8340
  );
8090
8341
  }
8342
+ function splitDateTime(v) {
8343
+ if (!v) return { date: null, hour: "00", minute: "00" };
8344
+ const [date = "", time = ""] = v.split(" ");
8345
+ const [hour = "00", minute = "00"] = time.split(":");
8346
+ return { date: date || null, hour, minute };
8347
+ }
8091
8348
  function RangeCalendar({
8092
8349
  value,
8093
8350
  selectable,
8094
8351
  maxRange,
8352
+ useTimePicker = false,
8095
8353
  onSelect,
8096
8354
  onPendingStartChange,
8097
8355
  onViewChange
8098
8356
  }) {
8099
8357
  const today = todayStr();
8100
- const base = value?.[0] || today;
8358
+ const initial = splitDateTime(value?.[0]);
8359
+ const base = initial.date || today;
8101
8360
  const [y, setY] = react.useState(Number(base.split("-")[0]));
8102
8361
  const [m, setM] = react.useState(Number(base.split("-")[1]));
8103
- const [start, setStart] = react.useState(value?.[0] ?? null);
8104
- const [end, setEnd] = react.useState(value?.[1] ?? null);
8362
+ const [start, setStart] = react.useState(initial.date);
8363
+ const [end, setEnd] = react.useState(splitDateTime(value?.[1]).date);
8105
8364
  const [hover, setHover] = react.useState(null);
8365
+ const [sH, setSH] = react.useState(initial.hour);
8366
+ const [sM, setSM] = react.useState(initial.minute);
8367
+ const [eH, setEH] = react.useState(splitDateTime(value?.[1]).hour);
8368
+ const [eM, setEM] = react.useState(splitDateTime(value?.[1]).minute);
8369
+ const lastEmitted = react.useRef("");
8106
8370
  react.useEffect(() => {
8107
- setStart(value?.[0] ?? null);
8108
- setEnd(value?.[1] ?? null);
8109
- }, [value]);
8371
+ if (useTimePicker && JSON.stringify(value) === lastEmitted.current) return;
8372
+ const s = splitDateTime(value?.[0]);
8373
+ const e = splitDateTime(value?.[1]);
8374
+ setStart(s.date);
8375
+ setEnd(e.date);
8376
+ setSH(s.hour);
8377
+ setSM(s.minute);
8378
+ setEH(e.hour);
8379
+ setEM(e.minute);
8380
+ }, [value, useTimePicker]);
8110
8381
  const setBase = (ny, nm) => {
8111
8382
  setY(ny);
8112
8383
  setM(nm);
@@ -8150,18 +8421,53 @@ function RangeCalendar({
8150
8421
  const hasRange = !!(rStart && rEnd && rStart !== rEnd);
8151
8422
  const inRange = (d) => !!(rStart && rEnd && d > rStart && d < rEnd);
8152
8423
  const RANGE_BG = "var(--cmp-datepicker-calendar-range-bg)";
8424
+ const pad = (v) => String(Number(v) || 0).padStart(2, "0");
8425
+ const emit2 = (dateStart, dateEnd, time) => {
8426
+ const t = time ?? { sH, sM, eH, eM };
8427
+ const range = useTimePicker ? [`${dateStart} ${pad(t.sH)}:${pad(t.sM)}`, `${dateEnd} ${pad(t.eH)}:${pad(t.eM)}`] : [dateStart, dateEnd];
8428
+ lastEmitted.current = JSON.stringify(range);
8429
+ onSelect(range);
8430
+ };
8153
8431
  const click = (d) => {
8154
8432
  if (isDisabled(d)) return;
8155
8433
  if (!start || complete || d < start) {
8156
8434
  setStart(d);
8157
8435
  setEnd(null);
8158
8436
  setHover(null);
8437
+ if (useTimePicker) {
8438
+ setSH("00");
8439
+ setSM("00");
8440
+ setEH("23");
8441
+ setEM("59");
8442
+ }
8159
8443
  onPendingStartChange?.(d);
8160
8444
  return;
8161
8445
  }
8162
8446
  setEnd(d);
8163
8447
  onPendingStartChange?.(null);
8164
- onSelect([start, d]);
8448
+ emit2(start, d, useTimePicker ? { sH, sM, eH: "23", eM: "59" } : void 0);
8449
+ };
8450
+ const changeTime = (which, part, raw) => {
8451
+ const digits = raw.replace(/\D/g, "").slice(0, 2);
8452
+ const max = part === "h" ? 23 : 59;
8453
+ const next = digits !== "" && Number(digits) > max ? String(max) : digits;
8454
+ const nextTime = { sH, sM, eH, eM };
8455
+ if (which === "start") nextTime[part === "h" ? "sH" : "sM"] = next;
8456
+ else nextTime[part === "h" ? "eH" : "eM"] = next;
8457
+ setSH(nextTime.sH);
8458
+ setSM(nextTime.sM);
8459
+ setEH(nextTime.eH);
8460
+ setEM(nextTime.eM);
8461
+ if (start && end) emit2(start, end, nextTime);
8462
+ };
8463
+ const blurTime = (which, part) => {
8464
+ if (which === "start") {
8465
+ if (part === "h") setSH(pad(sH));
8466
+ else setSM(pad(sM));
8467
+ } else {
8468
+ if (part === "h") setEH(pad(eH));
8469
+ else setEM(pad(eM));
8470
+ }
8165
8471
  };
8166
8472
  const GRID_COLS = "grid grid-cols-[repeat(7,var(--cmp-datepicker-calendar-day-width))]";
8167
8473
  const renderPanel = (gy, gm, isLeft) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-[var(--cmp-datepicker-calendar-gap)]", children: [
@@ -8236,35 +8542,120 @@ function RangeCalendar({
8236
8542
  );
8237
8543
  }) })
8238
8544
  ] });
8239
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "inline-flex h-[364px] flex-col gap-[var(--cmp-datepicker-calendar-gap)] rounded-[var(--cmp-datepicker-calendar-radius)] bg-[var(--cmp-datepicker-calendar-bg)] p-[var(--cmp-datepicker-calendar-paddingXY)] shadow-[2px_2px_12px_2px_rgba(0,0,0,0.2)] select-none", children: [
8240
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex items-center justify-center", children: [
8241
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-center gap-[var(--cmp-datepicker-calendar-gap)] text-[14px] font-medium leading-[24px]", children: [
8242
- /* @__PURE__ */ jsxRuntime.jsx(SGhostButton, { icon: "chevronLeft", size: "xxs", ariaLabel: "prevYear", onClick: goPrevYear }),
8243
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex min-w-[40px] items-center justify-center text-center", children: y }),
8244
- /* @__PURE__ */ jsxRuntime.jsx(SGhostButton, { icon: "chevronRight", size: "xxs", ariaLabel: "nextYear", onClick: goNextYear })
8245
- ] }),
8246
- /* @__PURE__ */ jsxRuntime.jsx(
8247
- "button",
8248
- {
8249
- type: "button",
8250
- onClick: goToday,
8251
- className: "absolute right-0 h-[24px] min-w-[37px] cursor-pointer bg-transparent text-center text-[12px] font-medium leading-[24px] text-[color:var(--cmp-datepicker-calendar-day-default-text)] hover:underline",
8252
- children: "\uC624\uB298"
8253
- }
8254
- )
8255
- ] }),
8256
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-[var(--cmp-datepicker-calendar-range-panelGap)]", children: [
8257
- renderPanel(y, m, true),
8258
- /* @__PURE__ */ jsxRuntime.jsx(
8259
- "span",
8260
- {
8261
- className: "w-px shrink-0 self-stretch bg-[var(--cmp-datepicker-calendar-range-divider)]",
8262
- "aria-hidden": true
8263
- }
8545
+ const renderTimeSection = (which, date, suffix) => {
8546
+ const active = !!date;
8547
+ const [yy = "", mm = "", dd = ""] = date ? date.split("-") : [];
8548
+ const hour = which === "start" ? sH : eH;
8549
+ const minute = which === "start" ? sM : eM;
8550
+ const num = (n, strip) => active && n !== "" ? strip ? String(Number(n)) : n : "-";
8551
+ const INPUT_CLS = "h-[var(--cmp-datepicker-sm-height)] w-[40px] rounded-[var(--cmp-datepicker-sm-radius)] border border-solid border-[color:var(--cmp-datepicker-border-default)] bg-[var(--cmp-datepicker-bg-default)] text-center text-[12px] font-medium leading-[20px] text-[color:var(--cmp-datepicker-text-default)] outline-none placeholder:text-[color:var(--cmp-datepicker-text-disabled)] focus:border-[color:var(--cmp-datepicker-border-focus)] disabled:cursor-not-allowed";
8552
+ const LABEL = "text-[color:var(--cmp-datepicker-calendar-time-date-label)]";
8553
+ const UNIT = "text-[color:var(--cmp-datepicker-calendar-time-date-unit)]";
8554
+ return /* @__PURE__ */ jsxRuntime.jsxs(
8555
+ "div",
8556
+ {
8557
+ className: cn(
8558
+ "flex flex-1 items-center justify-center gap-[var(--cmp-datepicker-calendar-time-section-gap)] rounded-[var(--cmp-datepicker-calendar-time-radius)] px-[var(--cmp-datepicker-calendar-time-paddingX)] py-[var(--cmp-datepicker-calendar-time-paddingY)] text-[12px] font-medium leading-[20px]",
8559
+ active ? "bg-[var(--cmp-datepicker-calendar-time-bg-selected)]" : "bg-[var(--cmp-datepicker-calendar-time-bg-default)]"
8560
+ ),
8561
+ children: [
8562
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "inline-flex items-center gap-[var(--cmp-datepicker-calendar-time-date-gap)]", children: [
8563
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: LABEL, children: num(yy, false) }),
8564
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: UNIT, children: "\uB144" }),
8565
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: LABEL, children: num(mm, true) }),
8566
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: UNIT, children: "\uC6D4" }),
8567
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: LABEL, children: num(dd, true) }),
8568
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: UNIT, children: "\uC77C" })
8569
+ ] }),
8570
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "inline-flex items-center gap-[var(--cmp-datepicker-calendar-time-unit-gap)]", children: [
8571
+ /* @__PURE__ */ jsxRuntime.jsx(
8572
+ "input",
8573
+ {
8574
+ type: "text",
8575
+ inputMode: "numeric",
8576
+ maxLength: 2,
8577
+ "aria-label": `${suffix} \uC2DC`,
8578
+ disabled: !active,
8579
+ value: active ? hour : "",
8580
+ placeholder: "--",
8581
+ onChange: (e) => changeTime(which, "h", e.target.value),
8582
+ onBlur: () => blurTime(which, "h"),
8583
+ className: INPUT_CLS
8584
+ }
8585
+ ),
8586
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: LABEL, children: ":" }),
8587
+ /* @__PURE__ */ jsxRuntime.jsx(
8588
+ "input",
8589
+ {
8590
+ type: "text",
8591
+ inputMode: "numeric",
8592
+ maxLength: 2,
8593
+ "aria-label": `${suffix} \uBD84`,
8594
+ disabled: !active,
8595
+ value: active ? minute : "",
8596
+ placeholder: "--",
8597
+ onChange: (e) => changeTime(which, "m", e.target.value),
8598
+ onBlur: () => blurTime(which, "m"),
8599
+ className: INPUT_CLS
8600
+ }
8601
+ )
8602
+ ] }),
8603
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: UNIT, children: suffix })
8604
+ ]
8605
+ }
8606
+ );
8607
+ };
8608
+ return /* @__PURE__ */ jsxRuntime.jsxs(
8609
+ "div",
8610
+ {
8611
+ className: cn(
8612
+ "inline-flex flex-col gap-[var(--cmp-datepicker-calendar-gap)] rounded-[var(--cmp-datepicker-calendar-radius)] bg-[var(--cmp-datepicker-calendar-bg)] p-[var(--cmp-datepicker-calendar-paddingXY)] shadow-[2px_2px_12px_2px_rgba(0,0,0,0.2)] select-none",
8613
+ useTimePicker ? "h-auto" : "h-[364px]"
8264
8614
  ),
8265
- renderPanel(nextMonthYM.year, nextMonthYM.month, false)
8266
- ] })
8267
- ] });
8615
+ children: [
8616
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex items-center justify-center", children: [
8617
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-center gap-[var(--cmp-datepicker-calendar-gap)] text-[14px] font-medium leading-[24px]", children: [
8618
+ /* @__PURE__ */ jsxRuntime.jsx(SGhostButton, { icon: "chevronLeft", size: "xxs", ariaLabel: "prevYear", onClick: goPrevYear }),
8619
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex min-w-[40px] items-center justify-center text-center", children: y }),
8620
+ /* @__PURE__ */ jsxRuntime.jsx(SGhostButton, { icon: "chevronRight", size: "xxs", ariaLabel: "nextYear", onClick: goNextYear })
8621
+ ] }),
8622
+ /* @__PURE__ */ jsxRuntime.jsx(
8623
+ "button",
8624
+ {
8625
+ type: "button",
8626
+ onClick: goToday,
8627
+ className: "absolute right-0 h-[24px] min-w-[37px] cursor-pointer bg-transparent text-center text-[12px] font-medium leading-[24px] text-[color:var(--cmp-datepicker-calendar-day-default-text)] hover:underline",
8628
+ children: "\uC624\uB298"
8629
+ }
8630
+ )
8631
+ ] }),
8632
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-[var(--cmp-datepicker-calendar-range-panelGap)]", children: [
8633
+ renderPanel(y, m, true),
8634
+ /* @__PURE__ */ jsxRuntime.jsx(
8635
+ "span",
8636
+ {
8637
+ className: "w-px shrink-0 self-stretch bg-[var(--cmp-datepicker-calendar-range-divider)]",
8638
+ "aria-hidden": true
8639
+ }
8640
+ ),
8641
+ renderPanel(nextMonthYM.year, nextMonthYM.month, false)
8642
+ ] }),
8643
+ useTimePicker && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
8644
+ /* @__PURE__ */ jsxRuntime.jsx(
8645
+ "span",
8646
+ {
8647
+ className: "h-px w-full bg-[var(--cmp-datepicker-calendar-range-divider)]",
8648
+ "aria-hidden": true
8649
+ }
8650
+ ),
8651
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-[var(--cmp-datepicker-calendar-time-gap)]", children: [
8652
+ renderTimeSection("start", start, "\uBD80\uD130"),
8653
+ renderTimeSection("end", end, "\uAE4C\uC9C0")
8654
+ ] })
8655
+ ] })
8656
+ ]
8657
+ }
8658
+ );
8268
8659
  }
8269
8660
  function SDateRangePicker({
8270
8661
  value,
@@ -8274,6 +8665,7 @@ function SDateRangePicker({
8274
8665
  placeholder = "YYYY-MM-DD ~ YYYY-MM-DD",
8275
8666
  selectable,
8276
8667
  maxRange,
8668
+ useTimePicker = false,
8277
8669
  disabled = false,
8278
8670
  width,
8279
8671
  name,
@@ -8317,8 +8709,9 @@ function SDateRangePicker({
8317
8709
  setPendingStart(null);
8318
8710
  onValueChange?.(r);
8319
8711
  if (rules && rules.length > 0) setRuleError(runRules(r, rules));
8320
- setOpen(false);
8712
+ if (!useTimePicker) setOpen(false);
8321
8713
  };
8714
+ const resolvedPlaceholder = useTimePicker && placeholder === "YYYY-MM-DD ~ YYYY-MM-DD" ? "YYYY-MM-DD HH:mm ~ YYYY-MM-DD HH:mm" : placeholder;
8322
8715
  return /* @__PURE__ */ jsxRuntime.jsx(
8323
8716
  SField,
8324
8717
  {
@@ -8385,7 +8778,7 @@ function SDateRangePicker({
8385
8778
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 flex-1 truncate text-right", children: startText }),
8386
8779
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "px-[4px]", children: "~" }),
8387
8780
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 flex-1 truncate text-left", children: endText })
8388
- ] }) : placeholder
8781
+ ] }) : resolvedPlaceholder
8389
8782
  }
8390
8783
  )
8391
8784
  ]
@@ -8404,6 +8797,7 @@ function SDateRangePicker({
8404
8797
  value: value ?? null,
8405
8798
  selectable,
8406
8799
  maxRange,
8800
+ useTimePicker,
8407
8801
  onSelect: select,
8408
8802
  onPendingStartChange: setPendingStart,
8409
8803
  onViewChange
@@ -9983,15 +10377,15 @@ var SBarcodeInput = react.forwardRef(
9983
10377
  );
9984
10378
 
9985
10379
  // src/components/STimePicker/timepicker.config.ts
9986
- var V2 = (path) => `var(--cmp-timepicker-${path})`;
10380
+ var V3 = (path) => `var(--cmp-timepicker-${path})`;
9987
10381
  var TIMEPICKER_SIZES = ["sm", "md"];
9988
10382
  var TIMEPICKER_SIZE_CONFIG = {
9989
10383
  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"),
10384
+ height: V3("sm-height"),
10385
+ paddingX: V3("sm-paddingX"),
10386
+ gap: V3("sm-gap"),
10387
+ icon: V3("sm-icon"),
10388
+ radius: V3("sm-radius"),
9995
10389
  // TODO: component.timepicker 토큰에 typography가 추가되면 CSS 변수로 교체한다.
9996
10390
  fontSize: 12,
9997
10391
  lineHeight: 20,
@@ -9999,11 +10393,11 @@ var TIMEPICKER_SIZE_CONFIG = {
9999
10393
  fieldMinWidth: 128
10000
10394
  },
10001
10395
  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"),
10396
+ height: V3("md-height"),
10397
+ paddingX: V3("md-paddingX"),
10398
+ gap: V3("md-gap"),
10399
+ icon: V3("md-icon"),
10400
+ radius: V3("md-radius"),
10007
10401
  // TODO: component.timepicker 토큰에 typography가 추가되면 CSS 변수로 교체한다.
10008
10402
  fontSize: 14,
10009
10403
  lineHeight: 24,
@@ -10886,7 +11280,9 @@ exports.SRadio = SRadio;
10886
11280
  exports.SRadioButton = SRadioButton;
10887
11281
  exports.SRadioGroup = SRadioGroup;
10888
11282
  exports.SSelect = SSelect;
11283
+ exports.SStepper = SStepper;
10889
11284
  exports.SSwitch = SSwitch;
11285
+ exports.STEPPER_SIZES = STEPPER_SIZES;
10890
11286
  exports.STable = STable;
10891
11287
  exports.STabs = STabs;
10892
11288
  exports.STag = STag;