sellmate-design-system-react 9.0.0-beta.90 → 9.0.0-beta.92

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.
package/dist/index.js CHANGED
@@ -4486,6 +4486,8 @@ var SGhostButton = /* @__PURE__ */ forwardRef(function SGhostButton2({
4486
4486
  ariaExpanded,
4487
4487
  tooltipText,
4488
4488
  disabled = false,
4489
+ href,
4490
+ target,
4489
4491
  onClick,
4490
4492
  anchorClassName,
4491
4493
  anchorStyle,
@@ -4509,51 +4511,74 @@ var SGhostButton = /* @__PURE__ */ forwardRef(function SGhostButton2({
4509
4511
  cancelHide();
4510
4512
  hideTimer.current = setTimeout(() => setOpen(false), TOOLTIP_HIDE_DELAY_MS);
4511
4513
  };
4512
- const s = SIZE[size];
4513
- const contentColor = disabled ? intent === "inverse" ? "var(--cmp-ghostButton-inverse-disabled)" : "var(--cmp-ghostButton-icon-disabled)" : `var(--cmp-ghostButton-${intent}-content)`;
4514
4514
  const text = (tooltipText ?? "").trim();
4515
4515
  const hasTooltip = text !== "";
4516
- const btn2 = /* @__PURE__ */ jsxs(
4516
+ const [prevHasTooltip, setPrevHasTooltip] = useState(hasTooltip);
4517
+ if (prevHasTooltip !== hasTooltip) {
4518
+ setPrevHasTooltip(hasTooltip);
4519
+ if (!hasTooltip) setOpen(false);
4520
+ }
4521
+ const s = SIZE[size];
4522
+ const contentColor = disabled ? intent === "inverse" ? "var(--cmp-ghostButton-inverse-disabled)" : "var(--cmp-ghostButton-icon-disabled)" : `var(--cmp-ghostButton-${intent}-content)`;
4523
+ const shared = {
4524
+ "aria-disabled": disabled ? "true" : void 0,
4525
+ "aria-label": ariaLabel.trim() || void 0,
4526
+ "tabIndex": disabled ? -1 : void 0,
4527
+ "onClick": (e) => {
4528
+ if (disabled) {
4529
+ e.preventDefault();
4530
+ return;
4531
+ }
4532
+ onClick?.(e);
4533
+ },
4534
+ "className": cn(
4535
+ "group/ghost-button relative box-border inline-flex items-center justify-center border-none bg-transparent p-0 transition-shadow",
4536
+ disabled ? "cursor-not-allowed" : "cursor-pointer",
4537
+ "focus-visible:outline-none focus-visible:shadow-[0_0_0_2px_var(--cmp-ghostButton-action-hover-bg)]",
4538
+ className
4539
+ ),
4540
+ "style": {
4541
+ width: `var(--cmp-ghostButton-${size}-size)`,
4542
+ height: `var(--cmp-ghostButton-${size}-size)`,
4543
+ borderRadius: `var(--cmp-ghostButton-radius-${s.radius})`,
4544
+ ...style
4545
+ }
4546
+ };
4547
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [
4548
+ !disabled && /* @__PURE__ */ jsx(
4549
+ "span",
4550
+ {
4551
+ "aria-hidden": true,
4552
+ className: "pointer-events-none absolute inset-0 rounded-[inherit] opacity-0 transition-opacity duration-200 group-hover/ghost-button:opacity-5",
4553
+ style: { background: `var(--cmp-ghostButton-${intent}-hover-bg)` }
4554
+ }
4555
+ ),
4556
+ /* @__PURE__ */ jsx(SIcon, { name: icon, size: s.icon, color: contentColor })
4557
+ ] });
4558
+ const btn2 = href != null ? (
4559
+ // 비활성 링크는 주소를 뗀다 — `<a>` 에는 disabled 가 없어서 주소가 남으면 가운데 클릭으로 열린다.
4560
+ /* @__PURE__ */ jsx(
4561
+ "a",
4562
+ {
4563
+ ref,
4564
+ href: disabled ? void 0 : href,
4565
+ target,
4566
+ rel: target === "_blank" ? "noopener noreferrer" : void 0,
4567
+ role: disabled ? "link" : void 0,
4568
+ ...shared,
4569
+ children: content
4570
+ }
4571
+ )
4572
+ ) : /* @__PURE__ */ jsx(
4517
4573
  "button",
4518
4574
  {
4519
4575
  ref,
4520
4576
  type: "button",
4521
4577
  disabled,
4522
- "aria-disabled": disabled ? "true" : void 0,
4523
- "aria-label": ariaLabel.trim() || void 0,
4524
4578
  "aria-pressed": ariaPressed,
4525
4579
  "aria-expanded": ariaExpanded,
4526
- tabIndex: disabled ? -1 : void 0,
4527
- onClick: (e) => {
4528
- if (disabled) {
4529
- e.preventDefault();
4530
- return;
4531
- }
4532
- onClick?.(e);
4533
- },
4534
- className: cn(
4535
- "group/ghost-button relative box-border inline-flex items-center justify-center border-none bg-transparent p-0 transition-shadow",
4536
- disabled ? "cursor-not-allowed" : "cursor-pointer",
4537
- "focus-visible:outline-none focus-visible:shadow-[0_0_0_2px_var(--cmp-ghostButton-action-hover-bg)]",
4538
- className
4539
- ),
4540
- style: {
4541
- width: `var(--cmp-ghostButton-${size}-size)`,
4542
- height: `var(--cmp-ghostButton-${size}-size)`,
4543
- borderRadius: `var(--cmp-ghostButton-radius-${s.radius})`,
4544
- ...style
4545
- },
4546
- children: [
4547
- !disabled && /* @__PURE__ */ jsx(
4548
- "span",
4549
- {
4550
- "aria-hidden": true,
4551
- className: "pointer-events-none absolute inset-0 rounded-[inherit] opacity-0 transition-opacity duration-200 group-hover/ghost-button:opacity-5",
4552
- style: { background: `var(--cmp-ghostButton-${intent}-hover-bg)` }
4553
- }
4554
- ),
4555
- /* @__PURE__ */ jsx(SIcon, { name: icon, size: s.icon, color: contentColor })
4556
- ]
4580
+ ...shared,
4581
+ children: content
4557
4582
  }
4558
4583
  );
4559
4584
  if (!hasTooltip) return btn2;
@@ -5281,6 +5306,9 @@ var FIELD_BORDER = 1;
5281
5306
  var TEXTAREA_MIN_HEIGHT = `calc(var(--cmp-chatInput-buttonFrame-height) - ${FIELD_BORDER * 2}px)`;
5282
5307
  var BUTTON_BOTTOM_INSET = `calc((var(--cmp-chatInput-buttonFrame-height) - var(--cmp-ghostButton-md-size)) / 2)`;
5283
5308
  var DEFAULT_MAX_ROWS = 5;
5309
+ function BarButton(props) {
5310
+ return /* @__PURE__ */ jsx(SGhostButton, { ...props, size: "md", style: { marginBottom: BUTTON_BOTTOM_INSET } });
5311
+ }
5284
5312
  var SChatInputImpl = /* @__PURE__ */ forwardRef(function SChatInput({
5285
5313
  value = "",
5286
5314
  onValueChange,
@@ -5288,7 +5316,13 @@ var SChatInputImpl = /* @__PURE__ */ forwardRef(function SChatInput({
5288
5316
  disabled = false,
5289
5317
  onSend,
5290
5318
  hasAttachments = false,
5319
+ onAttachFiles,
5320
+ attachAccept,
5321
+ attachMultiple = true,
5291
5322
  onAttach,
5323
+ attachDisabled = false,
5324
+ attachTooltip,
5325
+ sendDisabled = false,
5292
5326
  onPasteFiles,
5293
5327
  busy = false,
5294
5328
  onStop,
@@ -5300,11 +5334,23 @@ var SChatInputImpl = /* @__PURE__ */ forwardRef(function SChatInput({
5300
5334
  ...rest
5301
5335
  }, ref) {
5302
5336
  const hasText = value.trim() !== "";
5303
- const canSend = !disabled && !busy && (hasText || hasAttachments);
5304
- const send = useCallback(() => {
5337
+ const canSend = !disabled && !busy && !sendDisabled && (hasText || hasAttachments);
5338
+ const send = () => {
5305
5339
  if (!canSend) return;
5306
5340
  onSend?.(hasText ? value : "");
5307
- }, [canSend, hasText, onSend, value]);
5341
+ };
5342
+ const fileInputRef = useRef(null);
5343
+ const hasAttachButton = onAttachFiles != null || onAttach != null;
5344
+ const attachLocked = disabled || attachDisabled;
5345
+ const handleAttachClick = () => {
5346
+ if (onAttachFiles != null) fileInputRef.current?.click();
5347
+ else onAttach?.();
5348
+ };
5349
+ const handleFilesPicked = (e) => {
5350
+ const files = Array.from(e.target.files ?? []);
5351
+ e.target.value = "";
5352
+ if (files.length > 0) onAttachFiles?.(files);
5353
+ };
5308
5354
  const handleKeyDown = (e) => {
5309
5355
  rest.onKeyDown?.(e);
5310
5356
  if (e.key !== "Enter" || e.shiftKey || e.nativeEvent.isComposing) return;
@@ -5317,19 +5363,31 @@ var SChatInputImpl = /* @__PURE__ */ forwardRef(function SChatInput({
5317
5363
  const files = Array.from(e.clipboardData.files);
5318
5364
  if (files.length === 0) return;
5319
5365
  e.preventDefault();
5366
+ if (attachDisabled) return;
5320
5367
  onPasteFiles(files);
5321
5368
  };
5322
5369
  return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col bg-(--cmp-chatInput-bg)", className), style, children: [
5323
5370
  /* @__PURE__ */ jsxs("div", { className: "flex items-end gap-(--cmp-chatInput-gap) p-(--cmp-chatInput-padding)", children: [
5324
- onAttach != null && /* @__PURE__ */ jsx(
5325
- SGhostButton,
5371
+ hasAttachButton && /* @__PURE__ */ jsx(
5372
+ BarButton,
5326
5373
  {
5327
5374
  icon: "attachFile",
5328
- size: "md",
5329
5375
  ariaLabel: "\uD30C\uC77C \uCCA8\uBD80",
5330
- disabled,
5331
- onClick: onAttach,
5332
- style: { marginBottom: BUTTON_BOTTOM_INSET }
5376
+ tooltipText: attachTooltip,
5377
+ disabled: attachLocked,
5378
+ onClick: handleAttachClick
5379
+ }
5380
+ ),
5381
+ onAttachFiles != null && /* @__PURE__ */ jsx(
5382
+ "input",
5383
+ {
5384
+ ref: fileInputRef,
5385
+ type: "file",
5386
+ className: "hidden",
5387
+ accept: attachAccept,
5388
+ multiple: attachMultiple,
5389
+ disabled: attachLocked,
5390
+ onChange: handleFilesPicked
5333
5391
  }
5334
5392
  ),
5335
5393
  /* @__PURE__ */ jsx(
@@ -5353,26 +5411,26 @@ var SChatInputImpl = /* @__PURE__ */ forwardRef(function SChatInput({
5353
5411
  onPaste: handlePaste
5354
5412
  }
5355
5413
  ),
5356
- busy ? /* @__PURE__ */ jsx(
5357
- SGhostButton,
5358
- {
5359
- icon: "stop",
5360
- size: "md",
5361
- intent: "danger",
5362
- ariaLabel: "\uC751\uB2F5 \uC911\uC9C0",
5363
- onClick: onStop,
5364
- style: { marginBottom: BUTTON_BOTTOM_INSET }
5365
- }
5414
+ busy ? (
5415
+ // disabled 는 모두 잠근다 — 응답 도중 대화가 끝나면 정지도 할 수 없다
5416
+ /* @__PURE__ */ jsx(
5417
+ BarButton,
5418
+ {
5419
+ icon: "stop",
5420
+ intent: "danger",
5421
+ ariaLabel: "\uC751\uB2F5 \uC911\uC9C0",
5422
+ disabled,
5423
+ onClick: onStop
5424
+ }
5425
+ )
5366
5426
  ) : /* @__PURE__ */ jsx(
5367
- SGhostButton,
5427
+ BarButton,
5368
5428
  {
5369
5429
  icon: "send",
5370
- size: "md",
5371
5430
  intent: "subAction",
5372
5431
  ariaLabel: "\uC804\uC1A1",
5373
5432
  disabled: !canSend,
5374
- onClick: send,
5375
- style: { marginBottom: BUTTON_BOTTOM_INSET }
5433
+ onClick: send
5376
5434
  }
5377
5435
  )
5378
5436
  ] }),
@@ -6613,6 +6671,7 @@ var SExpansionItem = /* @__PURE__ */ forwardRef(
6613
6671
  depth = 1,
6614
6672
  leading,
6615
6673
  trailing,
6674
+ headerAction,
6616
6675
  bodyPadding = true,
6617
6676
  bordered = false,
6618
6677
  expanded,
@@ -6652,12 +6711,92 @@ var SExpansionItem = /* @__PURE__ */ forwardRef(
6652
6711
  );
6653
6712
  const leadingNode = typeof leading === "function" ? leading({ expanded: isExpanded, selected: isSelected, disabled }) : leading;
6654
6713
  const hasSupportingText = supportingText != null && supportingText !== "";
6714
+ const hasHeaderAction = headerAction != null && headerAction !== false;
6655
6715
  const id = useId();
6716
+ const surfaceClass = cn(
6717
+ isExpanded && "border-b border-(--cmp-listExpansionItem-title-border)",
6718
+ hasChevronInteraction ? "hover:bg-(--sys-color-bg-neutralBright)" : !isExpanded && "hover:bg-(--cmp-listExpansionItem-title-bg-hover)",
6719
+ // 펼침 배경(파랑)은 bordered 일 때만. 카드처럼 테두리로 경계가 잡힌
6720
+ // 아이템에서만 배경이 선택 상태를 나타낸다.
6721
+ // 테두리 없는 리스트에서는 accentStripe 가 그 역할을 하므로,
6722
+ // 배경까지 깔면 항목 경계가 없는 목록에서 선택 표시가 두 겹이 된다.
6723
+ isExpanded && isSelected && bordered && "bg-(--cmp-listExpansionItem-title-bg-expanded)"
6724
+ );
6725
+ const hoverHandlers = {
6726
+ onMouseEnter: () => setHovered(true),
6727
+ onMouseLeave: () => setHovered(false)
6728
+ };
6656
6729
  useEffect(() => {
6657
6730
  if (!isExpanded) return;
6658
6731
  parentDescendantContext?.reportExpandedDescendant(1);
6659
6732
  return () => parentDescendantContext?.reportExpandedDescendant(-1);
6660
6733
  }, [isExpanded, parentDescendantContext]);
6734
+ const toggleButton = /* @__PURE__ */ jsxs(
6735
+ "button",
6736
+ {
6737
+ type: "button",
6738
+ disabled,
6739
+ "aria-expanded": isExpanded,
6740
+ "aria-controls": id,
6741
+ ...hasHeaderAction ? {} : hoverHandlers,
6742
+ onClick: (event) => {
6743
+ if (disabled) return;
6744
+ const next = !isExpanded;
6745
+ if (expanded === void 0) setInternalExpanded(next);
6746
+ onToggle?.(next, event);
6747
+ },
6748
+ className: cn(
6749
+ "group/expansion-item-button flex w-full cursor-pointer items-center justify-between border-0 text-left text-(--cmp-listExpansionItem-title-text-default) focus-visible:outline focus-visible:outline-2 focus-visible:outline-(--sys-color-border-accent) disabled:cursor-not-allowed disabled:text-(--sys-color-disabled-text)",
6750
+ titleTypo,
6751
+ // 헤더 액션이 있으면 면은 바깥 줄이 그린다 — 버튼은 남은 폭만 차지한다
6752
+ hasHeaderAction ? "min-w-0 flex-1" : surfaceClass,
6753
+ // 높이는 세로 패딩과 내용이 정한다 — 제목 줄에 높이를 못 박지 않는다 (SListItem 과 동일)
6754
+ dense ? "py-(--cmp-listExpansionItem-title-paddingY-dense)" : "py-(--cmp-listExpansionItem-title-paddingY-normal)",
6755
+ "gap-(--cmp-listExpansionItem-title-gap)",
6756
+ // 헤더 액션이 있으면 오른쪽 끝 여백은 액션 칸이 갖고, 버튼은 액션과의 간격만 남긴다
6757
+ hasHeaderAction ? "pr-(--cmp-listExpansionItem-title-gap)" : "pr-(--cmp-listExpansionItem-title-paddingX)",
6758
+ depth === 2 ? "pl-[36px]" : depth === 3 ? "pl-[56px]" : "pl-[16px]",
6759
+ isExpanded && "text-(--cmp-listExpansionItem-title-text-expanded)"
6760
+ ),
6761
+ children: [
6762
+ !leading && /* @__PURE__ */ jsx(
6763
+ "span",
6764
+ {
6765
+ className: cn(
6766
+ "flex transition-transform duration-200 ease-out motion-reduce:transition-none",
6767
+ isExpanded && "rotate-180"
6768
+ ),
6769
+ children: /* @__PURE__ */ jsx(SIcon, { name: "caretDown", size: 12, color: "var(--sys-color-fg-quaternary)" })
6770
+ }
6771
+ ),
6772
+ leadingNode,
6773
+ /* @__PURE__ */ jsxs(
6774
+ "span",
6775
+ {
6776
+ className: cn(
6777
+ "min-w-0 flex-1",
6778
+ supportingTextPosition === "bottom" ? "flex flex-col" : "flex items-center gap-[8px] whitespace-nowrap"
6779
+ ),
6780
+ children: [
6781
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: title }),
6782
+ hasSupportingText && /* @__PURE__ */ jsx(
6783
+ "span",
6784
+ {
6785
+ className: cn(
6786
+ "typo-body-sm-default min-w-0 truncate text-(--sys-color-fg-secondary)",
6787
+ supportingTextPosition === "right" && "flex-1"
6788
+ ),
6789
+ children: supportingText
6790
+ }
6791
+ )
6792
+ ]
6793
+ }
6794
+ ),
6795
+ trailing,
6796
+ showInteractionChevron && /* @__PURE__ */ jsx(SIcon, { name: "chevronRight", size: 12, color: interactionChevronColor })
6797
+ ]
6798
+ }
6799
+ );
6661
6800
  return /* @__PURE__ */ jsxs(
6662
6801
  "div",
6663
6802
  {
@@ -6680,79 +6819,13 @@ var SExpansionItem = /* @__PURE__ */ forwardRef(
6680
6819
  className: "pointer-events-none absolute bottom-[-1px] left-0 top-0 z-[1] w-(--cmp-listItem-accentStripe-width) bg-(--cmp-listItem-accentStripe-color)"
6681
6820
  }
6682
6821
  ),
6683
- /* @__PURE__ */ jsxs(
6684
- "button",
6685
- {
6686
- type: "button",
6687
- disabled,
6688
- "aria-expanded": isExpanded,
6689
- "aria-controls": id,
6690
- onMouseEnter: () => setHovered(true),
6691
- onMouseLeave: () => setHovered(false),
6692
- onClick: (event) => {
6693
- if (disabled) return;
6694
- const next = !isExpanded;
6695
- if (expanded === void 0) setInternalExpanded(next);
6696
- onToggle?.(next, event);
6697
- },
6698
- className: cn(
6699
- "group/expansion-item-button flex w-full cursor-pointer items-center justify-between border-0 text-left text-(--cmp-listExpansionItem-title-text-default) focus-visible:outline focus-visible:outline-2 focus-visible:outline-(--sys-color-border-accent) disabled:cursor-not-allowed disabled:text-(--sys-color-disabled-text)",
6700
- titleTypo,
6701
- isExpanded && "border-b border-(--cmp-listExpansionItem-title-border)",
6702
- hasChevronInteraction ? "hover:bg-(--sys-color-bg-neutralBright)" : !isExpanded && "hover:bg-(--cmp-listExpansionItem-title-bg-hover)",
6703
- // 높이는 세로 패딩과 내용이 정한다 — 제목 줄에 높이를 못 박지 않는다 (SListItem 과 동일)
6704
- dense ? "py-(--cmp-listExpansionItem-title-paddingY-dense)" : "py-(--cmp-listExpansionItem-title-paddingY-normal)",
6705
- "gap-(--cmp-listExpansionItem-title-gap)",
6706
- "pr-(--cmp-listExpansionItem-title-paddingX)",
6707
- depth === 2 ? "pl-[36px]" : depth === 3 ? "pl-[56px]" : "pl-[16px]",
6708
- isExpanded && cn(
6709
- "text-(--cmp-listExpansionItem-title-text-expanded)",
6710
- // 펼침 배경(파랑)은 bordered 일 때만. 카드처럼 테두리로 경계가 잡힌
6711
- // 아이템에서만 배경이 선택 상태를 나타낸다.
6712
- // 테두리 없는 리스트에서는 accentStripe 가 그 역할을 하므로,
6713
- // 배경까지 깔면 항목 경계가 없는 목록에서 선택 표시가 두 겹이 된다.
6714
- isSelected && bordered && "bg-(--cmp-listExpansionItem-title-bg-expanded)"
6715
- )
6716
- ),
6717
- children: [
6718
- !leading && /* @__PURE__ */ jsx(
6719
- "span",
6720
- {
6721
- className: cn(
6722
- "flex transition-transform duration-200 ease-out motion-reduce:transition-none",
6723
- isExpanded && "rotate-180"
6724
- ),
6725
- children: /* @__PURE__ */ jsx(SIcon, { name: "caretDown", size: 12, color: "var(--sys-color-fg-quaternary)" })
6726
- }
6727
- ),
6728
- leadingNode,
6729
- /* @__PURE__ */ jsxs(
6730
- "span",
6731
- {
6732
- className: cn(
6733
- "min-w-0 flex-1",
6734
- supportingTextPosition === "bottom" ? "flex flex-col" : "flex items-center gap-[8px] whitespace-nowrap"
6735
- ),
6736
- children: [
6737
- /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: title }),
6738
- hasSupportingText && /* @__PURE__ */ jsx(
6739
- "span",
6740
- {
6741
- className: cn(
6742
- "typo-body-sm-default min-w-0 truncate text-(--sys-color-fg-secondary)",
6743
- supportingTextPosition === "right" && "flex-1"
6744
- ),
6745
- children: supportingText
6746
- }
6747
- )
6748
- ]
6749
- }
6750
- ),
6751
- trailing,
6752
- showInteractionChevron && /* @__PURE__ */ jsx(SIcon, { name: "chevronRight", size: 12, color: interactionChevronColor })
6753
- ]
6754
- }
6755
- ),
6822
+ hasHeaderAction ? (
6823
+ // 헤더 액션은 여닫는 버튼의 형제다 — 안에 넣으면 버튼이 중첩되고 누를 때마다 여닫힌다
6824
+ /* @__PURE__ */ jsxs("div", { className: cn("flex items-center", surfaceClass), ...hoverHandlers, children: [
6825
+ toggleButton,
6826
+ /* @__PURE__ */ jsx("div", { className: "flex shrink-0 items-center gap-(--cmp-listExpansionItem-title-gap) pr-(--cmp-listExpansionItem-title-paddingX)", children: headerAction })
6827
+ ] })
6828
+ ) : toggleButton,
6756
6829
  /* @__PURE__ */ jsx(SExpansionItemDescendantContext.Provider, { value: descendantContextValue, children: /* @__PURE__ */ jsx(
6757
6830
  "div",
6758
6831
  {
@@ -13321,9 +13394,20 @@ var SystemButton = /* @__PURE__ */ forwardRef(function SystemButton2({
13321
13394
  }
13322
13395
  ),
13323
13396
  label && /* @__PURE__ */ jsx("span", { className: "typo-body-sm-semibold min-w-0 truncate", children: label }),
13324
- count != null && count > 0 && // 개수 오른쪽은 버튼 여백(alertButton.paddingX)만으로는 좁다 — 시안이 4px 을 더 준다.
13397
+ count != null && count > 0 && // 개수는 채운 배지로 단다 — 배경·글자색·치수는 xs deep 태그 토큰이 쥔다(시안).
13398
+ // 배지 색은 고정이다: 흰 면은 파랑, 어두운 면(dark·darker)은 노랑 바탕에 검은 글자.
13399
+ // 배지 오른쪽은 버튼 여백(alertButton.paddingX)만으로는 좁다 — 시안이 4px 을 더 준다.
13325
13400
  // 대응 토큰이 없어 디자인 값을 그대로 쓴다.
13326
- /* @__PURE__ */ jsx("span", { className: "typo-body-md-bold me-[4px] shrink-0", style: { color: accentColor }, children: count.toLocaleString() })
13401
+ /* @__PURE__ */ jsx(
13402
+ STag,
13403
+ {
13404
+ size: "xs",
13405
+ deep: true,
13406
+ color: color === "light" ? "blue" : "yellow",
13407
+ label: count.toLocaleString(),
13408
+ className: "me-[4px] shrink-0"
13409
+ }
13410
+ )
13327
13411
  ]
13328
13412
  }
13329
13413
  );
@@ -14202,7 +14286,7 @@ var SStepper = /* @__PURE__ */ forwardRef(function SStepper2({
14202
14286
  items.findIndex((item4, index) => getItemValue(item4, index) === value)
14203
14287
  );
14204
14288
  const lastCompletedIndex = items.reduce(
14205
- (lastIndex, item4, index) => item4.completed === true ? index : lastIndex,
14289
+ (lastIndex2, item4, index) => item4.completed === true ? index : lastIndex2,
14206
14290
  -1
14207
14291
  );
14208
14292
  const resolvedClickableIndex = lastCompletedIndex + 1;
@@ -14644,12 +14728,25 @@ function monthCells(year, month) {
14644
14728
  return cells;
14645
14729
  }
14646
14730
  var WEEK_LABELS = ["\uC77C", "\uC6D4", "\uD654", "\uC218", "\uBAA9", "\uAE08", "\uD1A0"];
14731
+ var WEEKDAY_SUFFIX_EM = 0.25 + 3 * 1;
14732
+ var weekdaySuffixWidth = (fontSize) => Math.ceil(fontSize * WEEKDAY_SUFFIX_EM);
14733
+ function withWeekday(value) {
14734
+ const [date = "", ...rest] = value.split(" ");
14735
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
14736
+ if (!match) return value;
14737
+ const day = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3])).getDay();
14738
+ if (Number.isNaN(day)) return value;
14739
+ return [date, `${WEEK_LABELS[day]}\uC694\uC77C`, ...rest].join(" ");
14740
+ }
14647
14741
 
14648
14742
  // src/components/SCalendarBoard/calendar-board-layout.ts
14649
14743
  function eventEndDate(event) {
14650
14744
  const end = event.endDate;
14651
14745
  return end != null && end > event.date ? end : event.date;
14652
14746
  }
14747
+ function compareDate(a, b) {
14748
+ return a < b ? -1 : a > b ? 1 : 0;
14749
+ }
14653
14750
  function layoutCalendarBoard({
14654
14751
  cells,
14655
14752
  events,
@@ -14695,7 +14792,7 @@ function layoutCalendarBoard({
14695
14792
  flush(to);
14696
14793
  });
14697
14794
  runs.sort(
14698
- (a, b) => a.startIndex - b.startIndex || b.endIndex - b.startIndex - (a.endIndex - a.startIndex) || a.order - b.order
14795
+ (a, b) => a.startIndex - b.startIndex || compareDate(a.event.date, b.event.date) || compareDate(eventEndDate(b.event), eventEndDate(a.event)) || a.order - b.order
14699
14796
  );
14700
14797
  const weekLanes = Array.from({ length: weekCount }, () => []);
14701
14798
  for (const run of runs) {
@@ -17968,7 +18065,8 @@ var STableBar = /* @__PURE__ */ forwardRef(function STableBar2({ title, total, s
17968
18065
  );
17969
18066
  });
17970
18067
  var NO_KEYS = /* @__PURE__ */ new Set();
17971
- var ROW_ENTER_MS = 200;
18068
+ var EXPAND_MOTION = { duration: 220, easing: "ease-out" };
18069
+ var COLLAPSE_MOTION = { duration: 140, easing: "ease-in" };
17972
18070
  function STreeTable({
17973
18071
  rows = [],
17974
18072
  columns = [],
@@ -18015,32 +18113,88 @@ function STreeTable({
18015
18113
  if (expandedKeys === void 0) setInnerExpandedKeys(next);
18016
18114
  onExpandedChange?.(next, row);
18017
18115
  };
18018
- const visibleRows = useMemo(() => {
18019
- const out = [];
18020
- const walk = (nodes) => {
18021
- nodes.forEach((row) => {
18022
- out.push(row);
18023
- if (expandedSet.has(keyOf(row))) walk(childrenOf(row));
18024
- });
18025
- };
18026
- walk(rows);
18027
- return out;
18028
- }, [rows, expandedSet, rowKey, childrenKey]);
18029
- const [enteringKeys, setEnteringKeys] = useState(NO_KEYS);
18116
+ const visibleRows = useMemo(
18117
+ () => flattenRows(rows, (row) => expandedSet.has(keyOf(row)), childrenOf),
18118
+ // eslint-disable-next-line react-hooks/exhaustive-deps
18119
+ [rows, expandedSet, rowKey, childrenKey]
18120
+ );
18121
+ const tableRef = useRef(null);
18122
+ const [closingKeys, setClosingKeys] = useState(NO_KEYS);
18123
+ const [prevExpand, setPrevExpand] = useState({ rows, expandedSet });
18124
+ if (prevExpand.rows !== rows || prevExpand.expandedSet !== expandedSet) {
18125
+ setPrevExpand({ rows, expandedSet });
18126
+ let nextClosing = NO_KEYS;
18127
+ if (prevExpand.rows === rows && motionAllowed()) {
18128
+ const collapsed = [...prevExpand.expandedSet].filter((key) => !expandedSet.has(key));
18129
+ const expanded = [...expandedSet].filter((key) => !prevExpand.expandedSet.has(key));
18130
+ if (collapsed.length > 0 && expanded.length === 0) nextClosing = new Set(collapsed);
18131
+ }
18132
+ if (nextClosing !== closingKeys) setClosingKeys(nextClosing);
18133
+ }
18134
+ const renderedRows = useMemo(
18135
+ () => closingKeys.size === 0 ? visibleRows : flattenRows(
18136
+ rows,
18137
+ (row) => expandedSet.has(keyOf(row)) || closingKeys.has(keyOf(row)),
18138
+ childrenOf
18139
+ ),
18140
+ // eslint-disable-next-line react-hooks/exhaustive-deps
18141
+ [visibleRows, closingKeys]
18142
+ );
18143
+ const visibleKeys = useMemo(
18144
+ () => new Set(visibleRows.map(keyOf)),
18145
+ // eslint-disable-next-line react-hooks/exhaustive-deps
18146
+ [visibleRows]
18147
+ );
18148
+ const motionRef = useRef(null);
18030
18149
  const prevVisibleRef = useRef(null);
18031
18150
  useIsomorphicLayoutEffect(() => {
18032
- const keys = new Set(visibleRows.map(keyOf));
18033
18151
  const prev = prevVisibleRef.current;
18034
- prevVisibleRef.current = { rows, keys };
18035
- if (!prev || prev.rows !== rows) return;
18036
- const added = [...keys].filter((key) => !prev.keys.has(key));
18037
- if (added.length > 0) setEnteringKeys(new Set(added));
18038
- }, [visibleRows]);
18039
- useEffect(() => {
18040
- if (enteringKeys.size === 0) return;
18041
- const timer = setTimeout(() => setEnteringKeys(NO_KEYS), ROW_ENTER_MS);
18042
- return () => clearTimeout(timer);
18043
- }, [enteringKeys]);
18152
+ prevVisibleRef.current = { rows, keys: visibleKeys };
18153
+ const running = motionRef.current;
18154
+ if (running?.done) {
18155
+ running.cancel();
18156
+ motionRef.current = null;
18157
+ }
18158
+ if (!prev || prev.rows !== rows) {
18159
+ motionRef.current?.cancel();
18160
+ motionRef.current = null;
18161
+ return;
18162
+ }
18163
+ const added = new Set([...visibleKeys].filter((key) => !prev.keys.has(key)));
18164
+ const removed = new Set([...prev.keys].filter((key) => !visibleKeys.has(key)));
18165
+ if (added.size === 0 && removed.size === 0) return;
18166
+ let from = added.size > 0 ? 0 : 1;
18167
+ if (motionRef.current) {
18168
+ const current = motionRef.current;
18169
+ const reversing = current.to === 0 && sameKeys(added, current.keys) || current.to === 1 && sameKeys(removed, current.keys);
18170
+ if (reversing) from = current.openness();
18171
+ current.cancel();
18172
+ motionRef.current = null;
18173
+ }
18174
+ const scrollEl = tableRef.current?.getScrollElement();
18175
+ const keys = added.size > 0 ? added : removed;
18176
+ const canAnimate = scrollEl != null && motionAllowed() && (added.size === 0 || removed.size === 0);
18177
+ const motion = canAnimate ? startRowMotion({
18178
+ scrollEl,
18179
+ rowKeys: renderedRows.map(keyOf),
18180
+ blockKeys: keys,
18181
+ from,
18182
+ to: added.size > 0 ? 1 : 0
18183
+ }) : null;
18184
+ if (motion) {
18185
+ motionRef.current = motion;
18186
+ if (motion.to === 0) {
18187
+ void motion.finished.then((completed) => {
18188
+ if (!completed || motionRef.current !== motion) return;
18189
+ motion.done = true;
18190
+ setClosingKeys(NO_KEYS);
18191
+ });
18192
+ }
18193
+ } else if (closingKeys.size > 0) {
18194
+ setClosingKeys(NO_KEYS);
18195
+ }
18196
+ }, [renderedRows]);
18197
+ useEffect(() => () => motionRef.current?.cancel(), []);
18044
18198
  const collectSelectable = (row) => canSelectRow(row) ? [row, ...childrenOf(row).flatMap(collectSelectable)] : [];
18045
18199
  const selectionOf = (keys) => {
18046
18200
  const state2 = /* @__PURE__ */ new Map();
@@ -18136,8 +18290,9 @@ function STreeTable({
18136
18290
  STable2,
18137
18291
  {
18138
18292
  ...rest,
18293
+ ref: tableRef,
18139
18294
  columns: tableColumns,
18140
- rows: visibleRows,
18295
+ rows: renderedRows,
18141
18296
  rowKey,
18142
18297
  resizable,
18143
18298
  selectable,
@@ -18145,14 +18300,113 @@ function STreeTable({
18145
18300
  isRowIndeterminate: selectionState ? (row) => !!selectionState.get(keyOf(row))?.indeterminate : void 0,
18146
18301
  rowClassName: (row) => cn(
18147
18302
  rowClassName?.(row),
18148
- // 위에서 4px 내려오며 나타난다(theme.css --animate-table-row-in). 모션을 줄인 환경에서는 끈다
18149
- enteringKeys.has(keyOf(row)) && "motion-safe:animate-table-row-in"
18303
+ // 접히는 중인 행은 이미 접힌 행이다 — 사라지는 동안 눌리지 않게 한다
18304
+ !visibleKeys.has(keyOf(row)) && "pointer-events-none"
18150
18305
  ) || void 0,
18151
18306
  selected: tableSelected,
18152
18307
  onSelectedChange: handleSelectedChange
18153
18308
  }
18154
18309
  );
18155
18310
  }
18311
+ function flattenRows(nodes, isOpen, childrenOf) {
18312
+ const out = [];
18313
+ const walk = (list) => {
18314
+ list.forEach((row) => {
18315
+ out.push(row);
18316
+ if (isOpen(row)) walk(childrenOf(row));
18317
+ });
18318
+ };
18319
+ walk(nodes);
18320
+ return out;
18321
+ }
18322
+ function sameKeys(a, b) {
18323
+ return a.size === b.size && [...a].every((key) => b.has(key));
18324
+ }
18325
+ function motionAllowed() {
18326
+ if (typeof window === "undefined" || typeof Element.prototype.animate !== "function")
18327
+ return false;
18328
+ return !window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
18329
+ }
18330
+ function startRowMotion({
18331
+ scrollEl,
18332
+ rowKeys,
18333
+ blockKeys,
18334
+ from,
18335
+ to
18336
+ }) {
18337
+ const spec = to === 1 ? EXPAND_MOTION : COLLAPSE_MOTION;
18338
+ const duration = spec.duration * Math.abs(to - from);
18339
+ if (duration < 1) return null;
18340
+ const trs = [...scrollEl.querySelectorAll("tr[data-row-index]")].map((tr) => {
18341
+ const key = rowKeys[Number(tr.dataset.rowIndex)];
18342
+ return {
18343
+ tr,
18344
+ inBlock: key !== void 0 && blockKeys.has(key),
18345
+ height: tr.getBoundingClientRect().height
18346
+ };
18347
+ });
18348
+ const viewBottom = scrollEl.scrollTop + scrollEl.clientHeight;
18349
+ const openAt = (offset) => from + (to - from) * offset;
18350
+ const translate = (shift2, s) => `translateY(${-shift2 * (1 - s)}px)`;
18351
+ const timing = {
18352
+ duration,
18353
+ easing: spec.easing,
18354
+ // 접힘은 남겨 둔 행이 빠질 때까지 끝 위치를 붙잡는다 — 놓으면 그 사이 한 프레임 제자리로 튄다
18355
+ fill: to === 0 ? "forwards" : "none"
18356
+ };
18357
+ const animations = [];
18358
+ let shift = 0;
18359
+ let blockHeight = 0;
18360
+ let y = 0;
18361
+ trs.forEach(({ tr, inBlock, height }, i) => {
18362
+ if (inBlock && !trs[i - 1]?.inBlock) {
18363
+ blockHeight = 0;
18364
+ for (let j = i; j < trs.length && trs[j].inBlock; j++) blockHeight += trs[j].height;
18365
+ shift += blockHeight;
18366
+ y = 0;
18367
+ }
18368
+ if (shift === 0 || tr.offsetTop - shift > viewBottom) return;
18369
+ if (!inBlock) {
18370
+ animations.push(
18371
+ tr.animate(
18372
+ [{ transform: translate(shift, from) }, { transform: translate(shift, to) }],
18373
+ timing
18374
+ )
18375
+ );
18376
+ return;
18377
+ }
18378
+ const rowY = y;
18379
+ const clipAt = (s) => Math.min(height, Math.max(0, blockHeight * (1 - s) - rowY));
18380
+ const bends = [1 - rowY / blockHeight, 1 - (rowY + height) / blockHeight].map((s) => (s - from) / (to - from)).filter((offset) => offset > 0 && offset < 1);
18381
+ const offsets = [0, ...bends, 1].sort((a, b) => a - b);
18382
+ animations.push(
18383
+ tr.animate(
18384
+ offsets.map((offset) => ({
18385
+ offset,
18386
+ transform: translate(shift, openAt(offset)),
18387
+ clipPath: `inset(${clipAt(openAt(offset))}px 0 0 0)`
18388
+ })),
18389
+ timing
18390
+ )
18391
+ );
18392
+ y += height;
18393
+ });
18394
+ if (animations.length === 0) return null;
18395
+ return {
18396
+ keys: blockKeys,
18397
+ to,
18398
+ done: false,
18399
+ finished: Promise.all(animations.map((animation) => animation.finished)).then(
18400
+ () => true,
18401
+ () => false
18402
+ ),
18403
+ openness: () => {
18404
+ const progress = animations[0].effect?.getComputedTiming().progress;
18405
+ return progress == null ? to : openAt(progress);
18406
+ },
18407
+ cancel: () => animations.forEach((animation) => animation.cancel())
18408
+ };
18409
+ }
18156
18410
  var OWN_CONTROL_SELECTOR = 'a, button, input, select, textarea, label, [role="button"], [role="checkbox"], [contenteditable="true"]';
18157
18411
  function isOwnControl(target, cell) {
18158
18412
  if (!(target instanceof Element)) return false;
@@ -19131,13 +19385,21 @@ function SCalendar({
19131
19385
  }
19132
19386
  );
19133
19387
  }
19388
+
19389
+ // src/lib/mode-listbox.ts
19390
+ var MODE_LIST_HEIGHT = 240;
19391
+ var MODE_ITEM_HEIGHT = 28;
19392
+ var modeListItemClass = (selected, disabled) => cn(
19393
+ "flex w-full items-center justify-center gap-[var(--cmp-datepicker-mode-item-gap)] border-0 px-[var(--cmp-datepicker-mode-item-paddingX)] py-[var(--cmp-datepicker-mode-item-paddingY)] text-center font-base text-12 leading-[20px]",
19394
+ disabled && "cursor-not-allowed bg-[var(--cmp-datepicker-mode-item-bg-default)] font-medium text-[color:var(--cmp-datepicker-mode-item-content-disabled)]",
19395
+ !disabled && !selected && "cursor-pointer bg-[var(--cmp-datepicker-mode-item-bg-default)] font-medium text-[color:var(--cmp-datepicker-mode-item-content-default)] hover:bg-[var(--cmp-datepicker-mode-item-bg-hover)] hover:text-[color:var(--cmp-datepicker-mode-item-content-hover)]",
19396
+ !disabled && selected && "cursor-pointer bg-[var(--cmp-datepicker-mode-item-bg-default)] font-bold text-[color:var(--cmp-datepicker-mode-item-content-selected)]"
19397
+ );
19134
19398
  var MODE_NOUN = {
19135
19399
  date: "\uB0A0\uC9DC",
19136
19400
  month: "\uC6D4",
19137
19401
  year: "\uC5F0\uB3C4"
19138
19402
  };
19139
- var DATEPICKER_MODE_LIST_HEIGHT = 240;
19140
- var DATEPICKER_MODE_ITEM_HEIGHT = 28;
19141
19403
  var YEAR_LIST_EDGE_BUFFER = 24;
19142
19404
  var YEAR_LIST_EXPAND_SIZE = 80;
19143
19405
  var MONTHS = Array.from({ length: 12 }, (_, i) => i + 1);
@@ -19188,7 +19450,7 @@ function YearWheel({ value, anchorYear, selectable, onCommit }) {
19188
19450
  if (!list) return;
19189
19451
  const index = anchorYearRef.current - rangeStartRef.current;
19190
19452
  syncingScrollRef.current = true;
19191
- list.scrollTop = index * DATEPICKER_MODE_ITEM_HEIGHT;
19453
+ list.scrollTop = index * MODE_ITEM_HEIGHT;
19192
19454
  requestAnimationFrame(() => {
19193
19455
  syncingScrollRef.current = false;
19194
19456
  });
@@ -19197,7 +19459,7 @@ function YearWheel({ value, anchorYear, selectable, onCommit }) {
19197
19459
  const list = listRef.current;
19198
19460
  if (!list) return;
19199
19461
  if (syncingScrollRef.current) return;
19200
- if (list.scrollTop < DATEPICKER_MODE_ITEM_HEIGHT * YEAR_LIST_EDGE_BUFFER) {
19462
+ if (list.scrollTop < MODE_ITEM_HEIGHT * YEAR_LIST_EDGE_BUFFER) {
19201
19463
  const previousHeight = list.scrollHeight;
19202
19464
  setRange((current) => ({
19203
19465
  start: current.start - YEAR_LIST_EXPAND_SIZE,
@@ -19213,7 +19475,7 @@ function YearWheel({ value, anchorYear, selectable, onCommit }) {
19213
19475
  });
19214
19476
  return;
19215
19477
  }
19216
- if (list.scrollHeight - list.scrollTop - list.clientHeight < DATEPICKER_MODE_ITEM_HEIGHT * YEAR_LIST_EDGE_BUFFER) {
19478
+ if (list.scrollHeight - list.scrollTop - list.clientHeight < MODE_ITEM_HEIGHT * YEAR_LIST_EDGE_BUFFER) {
19217
19479
  setRange((current) => ({
19218
19480
  start: current.start,
19219
19481
  end: current.end + YEAR_LIST_EXPAND_SIZE
@@ -19228,8 +19490,8 @@ function YearWheel({ value, anchorYear, selectable, onCommit }) {
19228
19490
  "aria-label": "year",
19229
19491
  className: "h-full overflow-y-auto overscroll-contain [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
19230
19492
  style: {
19231
- paddingTop: `calc((${DATEPICKER_MODE_LIST_HEIGHT}px - ${DATEPICKER_MODE_ITEM_HEIGHT}px) / 2)`,
19232
- paddingBottom: `calc((${DATEPICKER_MODE_LIST_HEIGHT}px - ${DATEPICKER_MODE_ITEM_HEIGHT}px) / 2)`
19493
+ paddingTop: `calc((${MODE_LIST_HEIGHT}px - ${MODE_ITEM_HEIGHT}px) / 2)`,
19494
+ paddingBottom: `calc((${MODE_LIST_HEIGHT}px - ${MODE_ITEM_HEIGHT}px) / 2)`
19233
19495
  },
19234
19496
  onScroll: handleScroll,
19235
19497
  children: years.map((year) => {
@@ -19242,12 +19504,7 @@ function YearWheel({ value, anchorYear, selectable, onCommit }) {
19242
19504
  role: "option",
19243
19505
  "aria-selected": selected,
19244
19506
  disabled,
19245
- className: cn(
19246
- "flex w-full items-center justify-center gap-[var(--cmp-datepicker-mode-item-gap)] border-0 px-[var(--cmp-datepicker-mode-item-paddingX)] py-[var(--cmp-datepicker-mode-item-paddingY)] text-center font-base text-12 leading-[20px]",
19247
- disabled && "cursor-not-allowed bg-[var(--cmp-datepicker-mode-item-bg-default)] font-medium text-[color:var(--cmp-datepicker-mode-item-content-disabled)]",
19248
- !disabled && !selected && "cursor-pointer bg-[var(--cmp-datepicker-mode-item-bg-default)] font-medium text-[color:var(--cmp-datepicker-mode-item-content-default)] hover:bg-[var(--cmp-datepicker-mode-item-bg-hover)] hover:text-[color:var(--cmp-datepicker-mode-item-content-hover)]",
19249
- !disabled && selected && "cursor-pointer bg-[var(--cmp-datepicker-mode-item-bg-default)] font-bold text-[color:var(--cmp-datepicker-mode-item-content-selected)]"
19250
- ),
19507
+ className: modeListItemClass(selected, disabled),
19251
19508
  onClick: () => !disabled && onCommit(year),
19252
19509
  children: year
19253
19510
  },
@@ -19303,7 +19560,7 @@ function MonthWheel({ year, value, selectable, onCommit }) {
19303
19560
  if (!list) return;
19304
19561
  list.scrollTop = Math.max(
19305
19562
  0,
19306
- (initialValueRef.current - 1) * DATEPICKER_MODE_ITEM_HEIGHT - (list.clientHeight - DATEPICKER_MODE_ITEM_HEIGHT) / 2
19563
+ (initialValueRef.current - 1) * MODE_ITEM_HEIGHT - (list.clientHeight - MODE_ITEM_HEIGHT) / 2
19307
19564
  );
19308
19565
  }, []);
19309
19566
  return /* @__PURE__ */ jsx("div", { className: "h-full w-full overflow-hidden bg-[var(--cmp-datepicker-mode-bg)]", children: /* @__PURE__ */ jsx(
@@ -19323,12 +19580,7 @@ function MonthWheel({ year, value, selectable, onCommit }) {
19323
19580
  role: "option",
19324
19581
  "aria-selected": selected,
19325
19582
  disabled,
19326
- className: cn(
19327
- "flex w-full items-center justify-center gap-[var(--cmp-datepicker-mode-item-gap)] border-0 px-[var(--cmp-datepicker-mode-item-paddingX)] py-[var(--cmp-datepicker-mode-item-paddingY)] text-center font-base text-12 leading-[20px]",
19328
- disabled && "cursor-not-allowed bg-[var(--cmp-datepicker-mode-item-bg-default)] font-medium text-[color:var(--cmp-datepicker-mode-item-content-disabled)]",
19329
- !disabled && !selected && "cursor-pointer bg-[var(--cmp-datepicker-mode-item-bg-default)] font-medium text-[color:var(--cmp-datepicker-mode-item-content-default)] hover:bg-[var(--cmp-datepicker-mode-item-bg-hover)] hover:text-[color:var(--cmp-datepicker-mode-item-content-hover)]",
19330
- !disabled && selected && "cursor-pointer bg-[var(--cmp-datepicker-mode-item-bg-default)] font-bold text-[color:var(--cmp-datepicker-mode-item-content-selected)]"
19331
- ),
19583
+ className: modeListItemClass(selected, disabled),
19332
19584
  onClick: () => !disabled && onCommit(month),
19333
19585
  children: [
19334
19586
  month,
@@ -19407,6 +19659,7 @@ function SDatePicker({
19407
19659
  placeholder,
19408
19660
  selectable,
19409
19661
  disabled = false,
19662
+ showWeekday = false,
19410
19663
  clearable = false,
19411
19664
  width,
19412
19665
  maxWidth,
@@ -19436,6 +19689,7 @@ function SDatePicker({
19436
19689
  const [focused, setFocused] = useState(false);
19437
19690
  const [ruleError, setRuleError] = useState("");
19438
19691
  const hasValue = value != null && value !== "";
19692
+ const weekday = showWeekday && mode === "date";
19439
19693
  const resolvedPlaceholder = placeholder ?? (mode === "year" ? "YYYY" : mode === "month" ? "YYYY-MM" : "YYYY-MM-DD");
19440
19694
  useFormField({
19441
19695
  name,
@@ -19448,7 +19702,7 @@ function SDatePicker({
19448
19702
  });
19449
19703
  const resolvedError = error || ruleError !== "";
19450
19704
  const resolvedStatus = status ?? (rules && rules.length > 0 ? ruleError !== "" ? "error" : void 0 : void 0);
19451
- const dims = size === "md" ? {
19705
+ const baseDims = size === "md" ? {
19452
19706
  padX: 16,
19453
19707
  gap: 12,
19454
19708
  icon: 20,
@@ -19473,6 +19727,13 @@ function SDatePicker({
19473
19727
  lineHeight: 20,
19474
19728
  radius: 4
19475
19729
  };
19730
+ const weekdayW = weekday ? weekdaySuffixWidth(baseDims.fontSize) : 0;
19731
+ const dims = {
19732
+ ...baseDims,
19733
+ minW: baseDims.minW + weekdayW,
19734
+ fieldMinW: baseDims.fieldMinW + weekdayW,
19735
+ fieldMaxW: weekdayW ? `calc(${fieldWidthValue(baseDims.fieldMaxW)} + ${weekdayW}px)` : baseDims.fieldMaxW
19736
+ };
19476
19737
  const select = (date) => {
19477
19738
  onValueChange?.(date);
19478
19739
  if (rules && rules.length > 0) setRuleError(runRules(date, rules));
@@ -19557,7 +19818,7 @@ function SDatePicker({
19557
19818
  lineHeight: `${dims.lineHeight}px`,
19558
19819
  color: disabled ? "var(--cmp-datepicker-text-disabled)" : hasValue ? "var(--cmp-datepicker-text-default)" : "var(--cmp-field-hint-color)"
19559
19820
  },
19560
- children: hasValue ? value : resolvedPlaceholder
19821
+ children: hasValue ? weekday ? withWeekday(value) : value : resolvedPlaceholder
19561
19822
  }
19562
19823
  ),
19563
19824
  clearable && hasValue && !disabled && /* @__PURE__ */ jsx(
@@ -19951,6 +20212,7 @@ function SDateRangePicker({
19951
20212
  selectable,
19952
20213
  maxRange,
19953
20214
  showTimePicker = false,
20215
+ showWeekday = false,
19954
20216
  disabled = false,
19955
20217
  clearable = false,
19956
20218
  width,
@@ -19982,8 +20244,10 @@ function SDateRangePicker({
19982
20244
  const [pendingStart, setPendingStart] = useState(null);
19983
20245
  const hasValue = value != null && value[0] !== "" && value[1] !== "";
19984
20246
  const hasDisplay = pendingStart != null || hasValue;
19985
- const startText = pendingStart ?? (hasValue ? value[0] : "");
19986
- const endText = pendingStart ? "" : hasValue ? value[1] : "";
20247
+ const rawStart = pendingStart ?? (hasValue ? value[0] : "");
20248
+ const rawEnd = pendingStart ? "" : hasValue ? value[1] : "";
20249
+ const startText = showWeekday && rawStart ? withWeekday(rawStart) : rawStart;
20250
+ const endText = showWeekday && rawEnd ? withWeekday(rawEnd) : rawEnd;
19987
20251
  useFormField({
19988
20252
  name,
19989
20253
  validate: () => {
@@ -19995,7 +20259,7 @@ function SDateRangePicker({
19995
20259
  });
19996
20260
  const resolvedError = error || ruleError !== "";
19997
20261
  const resolvedStatus = status ?? (rules && rules.length > 0 ? ruleError !== "" ? "error" : void 0 : void 0);
19998
- const dims = size === "md" ? {
20262
+ const baseDims = size === "md" ? {
19999
20263
  padX: 16,
20000
20264
  gap: 12,
20001
20265
  icon: 20,
@@ -20019,6 +20283,13 @@ function SDateRangePicker({
20019
20283
  lineHeight: 20,
20020
20284
  radius: 4
20021
20285
  };
20286
+ const weekdayW = showWeekday ? weekdaySuffixWidth(baseDims.fontSize) * 2 : 0;
20287
+ const dims = {
20288
+ ...baseDims,
20289
+ minW: baseDims.minW + weekdayW,
20290
+ fieldMinW: baseDims.fieldMinW + weekdayW,
20291
+ fieldMaxW: weekdayW ? `calc(${fieldWidthValue(baseDims.fieldMaxW)} + ${weekdayW}px)` : baseDims.fieldMaxW
20292
+ };
20022
20293
  const select = (r3) => {
20023
20294
  setPendingStart(null);
20024
20295
  onValueChange?.(r3);
@@ -26020,7 +26291,7 @@ var isAtResetTarget = (field, value) => {
26020
26291
  var isInlineField = (field) => isInlineDateField(field) || field.type === "custom";
26021
26292
  var isFixedField = (field) => field.fixed === true || field.required === true;
26022
26293
  var isAddableField = (field) => !isFixedField(field);
26023
- var sameKeys = (a, b) => a.length === b.length && a.every((key, index) => key === b[index]);
26294
+ var sameKeys2 = (a, b) => a.length === b.length && a.every((key, index) => key === b[index]);
26024
26295
  var isGroupSatisfied = (group, values) => {
26025
26296
  if (!group.rule) return true;
26026
26297
  const keys = group.fields.map((f) => f.key);
@@ -26203,7 +26474,7 @@ var SChipFilter = /* @__PURE__ */ forwardRef(function SChipFilter2({
26203
26474
  const kept = current.filter((key) => known.has(key));
26204
26475
  const additions = fields.filter((field) => isFixedField(field) && !kept.includes(field.key)).map((field) => field.key);
26205
26476
  const next = [...kept, ...additions];
26206
- return sameKeys(current, next) ? current : next;
26477
+ return sameKeys2(current, next) ? current : next;
26207
26478
  });
26208
26479
  }, [fields, values]);
26209
26480
  useEffect(() => {
@@ -28396,212 +28667,171 @@ function formatTimeDisplay(value, type) {
28396
28667
  const displayHour = hour % 12 === 0 ? 12 : hour % 12;
28397
28668
  return `${meridiem} ${padTimePart(displayHour)}:${padTimePart(minute)}`;
28398
28669
  }
28399
- var TIME_SELECTOR_WIDTH = 136;
28400
- var MERIDIEM_TIME_SELECTOR_WIDTH = 154;
28401
- function TimeSelector({
28670
+ function scrollSelectedIntoView(list, index, center) {
28671
+ const top = index * MODE_ITEM_HEIGHT;
28672
+ if (center) {
28673
+ list.scrollTop = Math.max(0, top - (list.clientHeight - MODE_ITEM_HEIGHT) / 2);
28674
+ return;
28675
+ }
28676
+ if (top < list.scrollTop) list.scrollTop = top;
28677
+ else if (top + MODE_ITEM_HEIGHT > list.scrollTop + list.clientHeight)
28678
+ list.scrollTop = top + MODE_ITEM_HEIGHT - list.clientHeight;
28679
+ }
28680
+ function lastIndex(arr, pred) {
28681
+ for (let i = arr.length - 1; i >= 0; i--) if (pred(arr[i], i)) return i;
28682
+ return -1;
28683
+ }
28684
+ function ListColumn({ ariaLabel, items }) {
28685
+ const listRef = useRef(null);
28686
+ const mountedRef = useRef(false);
28687
+ const selectedIndex = items.findIndex((item4) => item4.selected);
28688
+ const tabIndexTarget = selectedIndex >= 0 ? selectedIndex : items.findIndex((item4) => !item4.disabled);
28689
+ useLayoutEffect(() => {
28690
+ const list = listRef.current;
28691
+ if (!list || selectedIndex < 0) return;
28692
+ scrollSelectedIntoView(list, selectedIndex, !mountedRef.current);
28693
+ mountedRef.current = true;
28694
+ }, [selectedIndex]);
28695
+ const onKeyDown = (event) => {
28696
+ const buttons = [...listRef.current?.querySelectorAll("button") ?? []];
28697
+ const current = buttons.indexOf(document.activeElement);
28698
+ if (current < 0) return;
28699
+ const enabled = (item4, i) => !item4.disabled && i !== current;
28700
+ let next;
28701
+ if (event.key === "ArrowDown") next = items.findIndex((it, i) => i > current && enabled(it, i));
28702
+ else if (event.key === "ArrowUp")
28703
+ next = lastIndex(items, (it, i) => i < current && enabled(it, i));
28704
+ else if (event.key === "Home") next = items.findIndex((it) => !it.disabled);
28705
+ else if (event.key === "End") next = lastIndex(items, (it) => !it.disabled);
28706
+ else return;
28707
+ event.preventDefault();
28708
+ if (next < 0) return;
28709
+ buttons[next]?.focus({ preventScroll: true });
28710
+ if (listRef.current) scrollSelectedIntoView(listRef.current, next, false);
28711
+ };
28712
+ return /* @__PURE__ */ jsx("div", { className: "h-full w-[var(--cmp-datepicker-mode-yearMonth-column-width)] min-w-0 overflow-hidden bg-[var(--cmp-datepicker-mode-bg)]", children: /* @__PURE__ */ jsx(
28713
+ "div",
28714
+ {
28715
+ ref: listRef,
28716
+ role: "listbox",
28717
+ "aria-label": ariaLabel,
28718
+ tabIndex: -1,
28719
+ className: "h-full overflow-y-auto overscroll-contain [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
28720
+ onKeyDown,
28721
+ children: items.map((item4, index) => /* @__PURE__ */ jsx(
28722
+ "button",
28723
+ {
28724
+ type: "button",
28725
+ role: "option",
28726
+ "aria-selected": item4.selected,
28727
+ disabled: item4.disabled,
28728
+ tabIndex: index === tabIndexTarget ? 0 : -1,
28729
+ className: cn(modeListItemClass(item4.selected, item4.disabled), "outline-offset-[-2px]"),
28730
+ onClick: () => !item4.disabled && item4.onSelect(),
28731
+ children: item4.label
28732
+ },
28733
+ item4.key
28734
+ ))
28735
+ }
28736
+ ) });
28737
+ }
28738
+ var HOURS_24 = Array.from({ length: 24 }, (_, i) => i);
28739
+ var HOURS_12 = [12, ...Array.from({ length: 11 }, (_, i) => i + 1)];
28740
+ function TimeListbox({
28402
28741
  value,
28403
28742
  showMeridiem,
28404
28743
  minuteStep,
28405
- onChange
28744
+ isDisabled,
28745
+ onChange,
28746
+ labelPrefix,
28747
+ elevated = true
28406
28748
  }) {
28407
- const parsed = useMemo(() => parseTimeValue(value), [value]);
28408
- const [hour, setHour] = useState(parsed.hour);
28409
- const [minute, setMinute] = useState(parsed.minute);
28410
- const [editingPart, setEditingPart] = useState(null);
28411
- const [inputDraft, setInputDraft] = useState({});
28749
+ const toState = (v) => v ? parseTimeValue(v) : null;
28750
+ const [time, setTime] = useState(() => toState(value));
28412
28751
  useEffect(() => {
28413
- setHour(parsed.hour);
28414
- setMinute(parsed.minute);
28415
- }, [parsed.hour, parsed.minute]);
28416
- const meridiem = hour >= 12 ? "PM" : "AM";
28417
- const displayHour = showMeridiem ? hour % 12 === 0 ? 12 : hour % 12 : hour;
28418
- const commit = (nextHour, nextMinute) => {
28419
- const h = clampTimePart(nextHour, 0, 23);
28420
- const m = clampTimePart(nextMinute, 0, 59);
28421
- setHour(h);
28422
- setMinute(m);
28423
- onChange(formatTimeValue(h, m));
28424
- };
28425
- const setMeridiem = (next) => {
28426
- if (next === meridiem) return;
28427
- commit(next === "AM" ? hour - 12 : hour + 12, minute);
28428
- };
28429
- const changeMinute = (delta) => {
28430
- const next = minute + delta;
28431
- if (next > 59) {
28432
- commit((hour + 1) % 24, next - 60);
28433
- return;
28434
- }
28435
- if (next < 0) {
28436
- commit((hour + 23) % 24, next + 60);
28437
- return;
28438
- }
28439
- commit(hour, next);
28440
- };
28441
- const changeHour = (delta) => commit((hour + delta + 24) % 24, minute);
28442
- const sanitizeInput = (next) => next.replace(/\D/g, "").slice(0, 2);
28443
- const beginEdit = (part, currentValue) => {
28444
- setEditingPart(part);
28445
- setInputDraft((prev) => ({ ...prev, [part]: String(currentValue).padStart(2, "0") }));
28446
- };
28447
- const commitHourDraft = (draft) => {
28448
- const clamped = clampTimePart(Number(draft), showMeridiem ? 1 : 0, showMeridiem ? 12 : 23);
28449
- const nextHour = showMeridiem ? meridiem === "PM" ? clamped === 12 ? 12 : clamped + 12 : clamped === 12 ? 0 : clamped : clamped;
28450
- commit(nextHour, minute);
28451
- return clamped;
28452
- };
28453
- const commitMinuteDraft = (draft) => {
28454
- const clamped = clampTimePart(Number(draft), 0, 59);
28455
- commit(hour, clamped);
28456
- return clamped;
28457
- };
28458
- const endEdit = (part) => {
28459
- const draft = inputDraft[part];
28460
- if (part === "hour" && draft != null && draft !== "") {
28461
- commitHourDraft(draft);
28462
- }
28463
- if (part === "minute" && draft != null && draft !== "") {
28464
- commitMinuteDraft(draft);
28465
- }
28466
- setEditingPart(null);
28467
- setInputDraft({});
28468
- };
28469
- const inputStyle = {
28470
- width: "var(--cmp-timepicker-selector-time-input-width)",
28471
- height: "var(--cmp-textinput-sm-height)",
28472
- borderRadius: "var(--cmp-textinput-sm-radius)",
28473
- border: "var(--cmp-textinput-borderWidth) solid var(--cmp-textinput-border-default)",
28474
- color: "var(--cmp-textinput-text-default)",
28475
- fontSize: 12,
28476
- lineHeight: "20px"
28752
+ setTime(toState(value));
28753
+ }, [value]);
28754
+ const step = Math.max(1, Math.floor(minuteStep));
28755
+ const minutes = Array.from({ length: Math.ceil(60 / step) }, (_, i) => i * step);
28756
+ const blocked = (h, m) => isDisabled?.(h * 60 + m) ?? false;
28757
+ const hourBlocked = (h) => minutes.every((m) => blocked(h, m));
28758
+ const commit = (hours, preferred, part) => {
28759
+ let next = blocked(preferred.hour, preferred.minute) ? null : preferred;
28760
+ for (const h of hours) {
28761
+ if (next) break;
28762
+ const m = minutes.find((mm) => !blocked(h, mm));
28763
+ if (m !== void 0) next = { hour: h, minute: m };
28764
+ }
28765
+ if (!next) return;
28766
+ setTime(next);
28767
+ onChange(formatTimeValue(next.hour, next.minute), part);
28477
28768
  };
28478
- const renderColumn = (type) => {
28479
- const isHour = type === "hour";
28480
- const val = isHour ? displayHour : minute;
28481
- const displayValue2 = editingPart === type ? inputDraft[type] ?? "" : String(val).padStart(2, "0");
28482
- return /* @__PURE__ */ jsxs(
28483
- "div",
28484
- {
28485
- className: "flex flex-col items-center justify-center",
28486
- style: { gap: "var(--cmp-timepicker-selector-time-column-gap)" },
28487
- children: [
28488
- /* @__PURE__ */ jsx(
28489
- SGhostButton,
28490
- {
28491
- icon: "chevronUp",
28492
- size: "xs",
28493
- ariaLabel: `${isHour ? "\uC2DC" : "\uBD84"} \uC99D\uAC00`,
28494
- onClick: () => isHour ? changeHour(1) : changeMinute(minuteStep)
28495
- }
28496
- ),
28497
- /* @__PURE__ */ jsx(
28498
- "input",
28499
- {
28500
- "aria-label": isHour ? "\uC2DC" : "\uBD84",
28501
- inputMode: "numeric",
28502
- value: displayValue2,
28503
- placeholder: "-",
28504
- onFocus: () => beginEdit(type, val),
28505
- onBlur: () => endEdit(type),
28506
- onChange: (event) => {
28507
- const draft = sanitizeInput(event.target.value);
28508
- setInputDraft((prev) => ({ ...prev, [type]: draft }));
28509
- if (draft === "") return;
28510
- const raw = Number(draft);
28511
- if (isHour) {
28512
- if (draft.length < 2) return;
28513
- const clamped2 = commitHourDraft(draft);
28514
- const displayDraft2 = raw === clamped2 ? draft : String(clamped2);
28515
- setInputDraft((prev) => ({ ...prev, [type]: displayDraft2 }));
28516
- return;
28517
- }
28518
- if (draft.length < 2) return;
28519
- const clamped = commitMinuteDraft(draft);
28520
- const displayDraft = raw === clamped ? draft : String(clamped);
28521
- setInputDraft((prev) => ({ ...prev, [type]: displayDraft }));
28522
- },
28523
- className: "box-border bg-white text-center outline-none",
28524
- style: inputStyle
28525
- }
28526
- ),
28527
- /* @__PURE__ */ jsx(
28528
- SGhostButton,
28529
- {
28530
- icon: "chevronDown",
28531
- size: "xs",
28532
- ariaLabel: `${isHour ? "\uC2DC" : "\uBD84"} \uAC10\uC18C`,
28533
- onClick: () => isHour ? changeHour(-1) : changeMinute(-minuteStep)
28534
- }
28535
- )
28536
- ]
28769
+ const hour = time?.hour;
28770
+ const minute = time?.minute ?? 0;
28771
+ const meridiem = hour === void 0 ? void 0 : hour >= 12 ? "PM" : "AM";
28772
+ const prefix = labelPrefix ? `${labelPrefix} ` : "";
28773
+ const meridiemItems = ["AM", "PM"].map((item4) => {
28774
+ const base = item4 === "AM" ? 0 : 12;
28775
+ const range = HOURS_24.slice(base, base + 12);
28776
+ return {
28777
+ key: item4,
28778
+ label: item4 === "AM" ? "\uC624\uC804" : "\uC624\uD6C4",
28779
+ selected: meridiem === item4,
28780
+ disabled: range.every(hourBlocked),
28781
+ onSelect: () => {
28782
+ if (meridiem === item4) return;
28783
+ const h = hour === void 0 ? base : hour % 12 + base;
28784
+ commit(range, { hour: h, minute }, "meridiem");
28537
28785
  }
28538
- );
28539
- };
28786
+ };
28787
+ });
28788
+ const hourItems = showMeridiem ? HOURS_12.map((display) => {
28789
+ const h = display % 12 + (meridiem === "PM" ? 12 : 0);
28790
+ return {
28791
+ key: String(display),
28792
+ label: `${padTimePart(display)}\uC2DC`,
28793
+ selected: hour === h,
28794
+ disabled: hourBlocked(h),
28795
+ onSelect: () => commit([h], { hour: h, minute }, "hour")
28796
+ };
28797
+ }) : HOURS_24.map((h) => ({
28798
+ key: String(h),
28799
+ label: `${padTimePart(h)}\uC2DC`,
28800
+ selected: hour === h,
28801
+ disabled: hourBlocked(h),
28802
+ onSelect: () => commit([h], { hour: h, minute }, "hour")
28803
+ }));
28804
+ const minuteHour = hour ?? (meridiem === "PM" ? 12 : 0);
28805
+ const minuteItems = minutes.map((m) => ({
28806
+ key: String(m),
28807
+ label: `${padTimePart(m)}\uBD84`,
28808
+ selected: time != null && minute === m,
28809
+ disabled: blocked(minuteHour, m),
28810
+ onSelect: () => commit([minuteHour], { hour: minuteHour, minute: m }, "minute")
28811
+ }));
28812
+ const divider = /* @__PURE__ */ jsx(
28813
+ "div",
28814
+ {
28815
+ "aria-hidden": true,
28816
+ className: "w-[var(--border-width-1)] self-stretch bg-[var(--sys-color-divider-default)]"
28817
+ }
28818
+ );
28540
28819
  return /* @__PURE__ */ jsxs(
28541
28820
  "div",
28542
28821
  {
28543
- className: "flex flex-col items-stretch justify-center overflow-hidden",
28544
- style: {
28545
- background: "var(--cmp-timepicker-selector-bg)",
28546
- borderRadius: "var(--cmp-timepicker-selector-radius)",
28547
- boxShadow: "4px 8px 16px -4px rgba(34,34,34,0.1), 0 0 24px -6px rgba(34,34,34,0.12)"
28548
- },
28822
+ className: cn(
28823
+ "inline-flex gap-[var(--cmp-datepicker-mode-yearMonth-gap)] overflow-hidden bg-[var(--cmp-datepicker-mode-bg)]",
28824
+ elevated && "rounded-[var(--cmp-datepicker-mode-radius)] shadow-normal-sm"
28825
+ ),
28826
+ style: { height: MODE_LIST_HEIGHT },
28549
28827
  children: [
28550
- showMeridiem && /* @__PURE__ */ jsx(
28551
- "div",
28552
- {
28553
- className: "flex items-center justify-center",
28554
- style: {
28555
- padding: "var(--cmp-timepicker-selector-section-paddingY) var(--cmp-timepicker-selector-section-paddingX)"
28556
- },
28557
- children: /* @__PURE__ */ jsx("div", { className: "isolate flex", children: ["AM", "PM"].map((item4, index) => {
28558
- const selected = item4 === meridiem;
28559
- return /* @__PURE__ */ jsx(
28560
- "button",
28561
- {
28562
- type: "button",
28563
- onClick: () => setMeridiem(item4),
28564
- className: cn(
28565
- "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]",
28566
- index === 0 ? "rounded-l-[var(--cmp-select-sm-radius)]" : "rounded-r-[var(--cmp-select-sm-radius)]",
28567
- index === 0 ? "-mr-px" : "",
28568
- 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)]"
28569
- ),
28570
- children: item4 === "AM" ? "\uC624\uC804" : "\uC624\uD6C4"
28571
- },
28572
- item4
28573
- );
28574
- }) })
28575
- }
28576
- ),
28577
- showMeridiem && /* @__PURE__ */ jsx("div", { className: "h-px w-full bg-[var(--sys-color-divider-default)]" }),
28578
- /* @__PURE__ */ jsxs(
28579
- "div",
28580
- {
28581
- className: "flex items-center justify-center",
28582
- style: {
28583
- gap: "var(--cmp-timepicker-selector-time-gap)",
28584
- padding: "var(--cmp-timepicker-selector-section-paddingY) var(--cmp-timepicker-selector-section-paddingX)"
28585
- },
28586
- children: [
28587
- renderColumn("hour"),
28588
- /* @__PURE__ */ jsx(
28589
- "span",
28590
- {
28591
- className: "text-center",
28592
- style: {
28593
- width: "var(--cmp-timepicker-selector-colon-size)",
28594
- color: "var(--cmp-field-hint-color)",
28595
- fontSize: "var(--cmp-timepicker-selector-colon-size)",
28596
- lineHeight: "var(--cmp-timepicker-selector-colon-size)"
28597
- },
28598
- children: ":"
28599
- }
28600
- ),
28601
- renderColumn("minute")
28602
- ]
28603
- }
28604
- )
28828
+ showMeridiem && /* @__PURE__ */ jsxs(Fragment, { children: [
28829
+ /* @__PURE__ */ jsx(ListColumn, { ariaLabel: `${prefix}\uC624\uC804/\uC624\uD6C4`, items: meridiemItems }),
28830
+ divider
28831
+ ] }),
28832
+ /* @__PURE__ */ jsx(ListColumn, { ariaLabel: `${prefix}\uC2DC`, items: hourItems }),
28833
+ divider,
28834
+ /* @__PURE__ */ jsx(ListColumn, { ariaLabel: `${prefix}\uBD84`, items: minuteItems })
28605
28835
  ]
28606
28836
  }
28607
28837
  );
@@ -28639,11 +28869,9 @@ function STimePicker({
28639
28869
  style
28640
28870
  }) {
28641
28871
  const portalContainer = usePortalContainer();
28642
- const triggerRef = useRef(null);
28643
28872
  const [open, setOpen] = useState(false);
28644
28873
  const triggerId = useFieldControlId();
28645
28874
  const triggerTextId = `${triggerId}-text`;
28646
- const [popoverAlignOffset, setPopoverAlignOffset] = useState(0);
28647
28875
  const [focused, setFocused] = useState(false);
28648
28876
  const [ruleError, setRuleError] = useState("");
28649
28877
  const dims = TIMEPICKER_SIZE_CONFIG[size] ?? TIMEPICKER_SIZE_CONFIG.sm;
@@ -28664,11 +28892,6 @@ function STimePicker({
28664
28892
  const resolvedStatus = status ?? (rules && rules.length > 0 ? ruleError !== "" ? "error" : void 0 : void 0);
28665
28893
  const commitOpen = (next) => {
28666
28894
  if (disabled) return;
28667
- if (next) {
28668
- const triggerWidth = triggerRef.current?.getBoundingClientRect().width ?? 0;
28669
- const selectorWidth = effectiveShowMeridiem ? MERIDIEM_TIME_SELECTOR_WIDTH : TIME_SELECTOR_WIDTH;
28670
- setPopoverAlignOffset(Math.max(0, (triggerWidth - selectorWidth) / 2));
28671
- }
28672
28895
  setOpen(next);
28673
28896
  onOpenChange?.(next);
28674
28897
  if (!next && rules && rules.length > 0) setRuleError(runRules(value, rules));
@@ -28707,7 +28930,6 @@ function STimePicker({
28707
28930
  /* @__PURE__ */ jsx(RPopover13.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(
28708
28931
  "div",
28709
28932
  {
28710
- ref: triggerRef,
28711
28933
  id: triggerId,
28712
28934
  role: "button",
28713
28935
  "aria-disabled": disabled,
@@ -28771,20 +28993,22 @@ function STimePicker({
28771
28993
  RPopover13.Content,
28772
28994
  {
28773
28995
  side: "bottom",
28774
- align: "start",
28996
+ align: "center",
28775
28997
  sideOffset: 4,
28776
- alignOffset: popoverAlignOffset,
28777
28998
  className: cn(
28778
28999
  "z-50 max-w-[var(--radix-popover-content-available-width)]",
28779
29000
  FLOATING_SLIDE_ANIM
28780
29001
  ),
28781
29002
  children: /* @__PURE__ */ jsx(
28782
- TimeSelector,
29003
+ TimeListbox,
28783
29004
  {
28784
29005
  value: normalized,
28785
29006
  showMeridiem: effectiveShowMeridiem,
28786
29007
  minuteStep,
28787
- onChange: commitValue
29008
+ onChange: (time, part) => {
29009
+ commitValue(time);
29010
+ if (part === "minute") commitOpen(false);
29011
+ }
28788
29012
  }
28789
29013
  )
28790
29014
  }
@@ -28814,7 +29038,6 @@ function formatTimeRangeDisplay(value, type) {
28814
29038
  if (value == null || value[0] === "" || value[1] === "") return "";
28815
29039
  return `${formatTimeDisplay(value[0], type)} ~ ${formatTimeDisplay(value[1], type)}`;
28816
29040
  }
28817
- var TIME_RANGE_SELECTOR_WIDTH = 345;
28818
29041
  var MIDDAY_FIELD_MIN_WIDTH = {
28819
29042
  sm: 178,
28820
29043
  md: 216
@@ -28823,6 +29046,10 @@ var MIDDAY_FIELD_MAX_WIDTH = {
28823
29046
  sm: "lg",
28824
29047
  md: "lg"
28825
29048
  };
29049
+ var toMinutes = (time) => {
29050
+ const { hour, minute } = parseTimeValue(time);
29051
+ return hour * 60 + minute;
29052
+ };
28826
29053
  function RangeTimeSelector({
28827
29054
  value,
28828
29055
  showMeridiem,
@@ -28830,233 +29057,32 @@ function RangeTimeSelector({
28830
29057
  rangeOrder,
28831
29058
  onChange
28832
29059
  }) {
28833
- const range = useMemo(() => value ?? ["00:00", "23:59"], [value]);
28834
- const [editingKey, setEditingKey] = useState(null);
28835
- const [inputDraft, setInputDraft] = useState({});
28836
- const autoAdjustedRef = useRef(null);
28837
- const getParts = (side) => parseTimeValue(side === "start" ? range[0] : range[1]);
28838
- const getMeridiem = (side) => getParts(side).hour >= 12 ? "PM" : "AM";
28839
- const getDisplayHour = (side) => {
28840
- const { hour } = getParts(side);
28841
- if (!showMeridiem) return hour;
28842
- const displayHour = hour % 12;
28843
- return displayHour === 0 ? 12 : displayHour;
28844
- };
28845
- const commit = (side, hour, minute, direction = "unknown") => {
28846
- const next = formatTimeValue(hour, minute);
28847
- onChange(
28848
- normalizeNextRange(side === "start" ? [next, range[1]] : [range[0], next], side, direction)
28849
- );
28850
- };
28851
- const getTimeMinutes2 = (time) => {
28852
- const { hour, minute } = parseTimeValue(time);
28853
- return hour * 60 + minute;
28854
- };
28855
- const normalizeNextRange = (nextRange, changedSide, direction) => {
28856
- if (rangeOrder === "allow-cross-day") return nextRange;
28857
- const [start, end] = nextRange;
28858
- const [currentStart, currentEnd] = range;
28859
- if (getTimeMinutes2(start) <= getTimeMinutes2(end)) {
28860
- const shouldKeepSynced = autoAdjustedRef.current?.side === changedSide && currentStart === currentEnd && start !== end && (direction === "unknown" || autoAdjustedRef.current.direction === direction);
28861
- if (shouldKeepSynced) {
28862
- return changedSide === "start" ? [start, start] : [end, end];
28863
- }
28864
- autoAdjustedRef.current = null;
28865
- return nextRange;
28866
- }
28867
- autoAdjustedRef.current = {
28868
- side: changedSide,
28869
- direction: changedSide === "start" ? "increase" : "decrease"
28870
- };
28871
- return changedSide === "start" ? [start, start] : [end, end];
28872
- };
28873
- const changeHour = (side, delta) => {
28874
- const { hour, minute } = getParts(side);
28875
- commit(side, (hour + delta + 24) % 24, minute, delta > 0 ? "increase" : "decrease");
28876
- };
28877
- const changeMinute = (side, delta) => {
28878
- const { hour, minute } = getParts(side);
28879
- const next = minute + delta;
28880
- if (next > 59) {
28881
- commit(side, (hour + 1) % 24, next - 60, delta > 0 ? "increase" : "decrease");
28882
- return;
28883
- }
28884
- if (next < 0) {
28885
- commit(side, (hour + 23) % 24, next + 60, delta > 0 ? "increase" : "decrease");
29060
+ const range = value ?? ["00:00", "23:59"];
29061
+ const strict = rangeOrder !== "allow-cross-day";
29062
+ const change = (side, time) => {
29063
+ if (side === "end") {
29064
+ onChange([range[0], time]);
28886
29065
  return;
28887
29066
  }
28888
- commit(side, hour, next, delta > 0 ? "increase" : "decrease");
28889
- };
28890
- const setMeridiem = (side, next) => {
28891
- const current = getMeridiem(side);
28892
- if (current === next) return;
28893
- const { hour, minute } = getParts(side);
28894
- commit(side, next === "AM" ? hour - 12 : hour + 12, minute);
28895
- };
28896
- const getEditingKey = (side, part) => `${side}-${part}`;
28897
- const sanitizeInput = (next) => next.replace(/\D/g, "").slice(0, 2);
28898
- const beginEdit = (side, part, currentValue) => {
28899
- const key = getEditingKey(side, part);
28900
- setEditingKey(key);
28901
- setInputDraft((prev) => ({ ...prev, [key]: String(currentValue).padStart(2, "0") }));
28902
- };
28903
- const commitHourDraft = (side, draft) => {
28904
- const clamped = clampTimePart(Number(draft), showMeridiem ? 1 : 0, showMeridiem ? 12 : 23);
28905
- const parts = getParts(side);
28906
- const meridiem = getMeridiem(side);
28907
- const nextHour = showMeridiem ? meridiem === "PM" ? clamped === 12 ? 12 : clamped + 12 : clamped === 12 ? 0 : clamped : clamped;
28908
- commit(side, nextHour, parts.minute);
28909
- return clamped;
28910
- };
28911
- const commitMinuteDraft = (side, draft) => {
28912
- const { hour } = getParts(side);
28913
- const clamped = clampTimePart(Number(draft), 0, 59);
28914
- commit(side, hour, clamped);
28915
- return clamped;
28916
- };
28917
- const endEdit = (side, part) => {
28918
- const key = getEditingKey(side, part);
28919
- const draft = inputDraft[key];
28920
- if (part === "hour" && draft != null && draft !== "") {
28921
- commitHourDraft(side, draft);
28922
- }
28923
- if (part === "minute" && draft != null && draft !== "") {
28924
- commitMinuteDraft(side, draft);
28925
- }
28926
- setEditingKey(null);
28927
- setInputDraft({});
28928
- };
28929
- const inputStyle = {
28930
- width: "var(--cmp-timepicker-selector-time-input-width)",
28931
- height: "var(--cmp-textinput-sm-height)",
28932
- borderRadius: "var(--cmp-textinput-sm-radius)",
28933
- border: "var(--cmp-textinput-borderWidth) solid var(--cmp-textinput-border-default)",
28934
- color: "var(--cmp-textinput-text-default)",
28935
- fontSize: 12,
28936
- lineHeight: "20px"
28937
- };
28938
- const renderColumn = (side, kind) => {
28939
- const isHour = kind === "hour";
28940
- const { minute } = getParts(side);
28941
- const val = isHour ? getDisplayHour(side) : minute;
28942
- const currentEditingKey = getEditingKey(side, kind);
28943
- const displayValue2 = editingKey === currentEditingKey ? inputDraft[currentEditingKey] ?? "" : String(val).padStart(2, "0");
28944
- return /* @__PURE__ */ jsxs(
28945
- "div",
28946
- {
28947
- className: "flex flex-col items-center justify-center",
28948
- style: { gap: "var(--cmp-timepicker-selector-time-column-gap)" },
28949
- children: [
28950
- /* @__PURE__ */ jsx(
28951
- SGhostButton,
28952
- {
28953
- icon: "chevronUp",
28954
- size: "xs",
28955
- ariaLabel: `${side === "start" ? "\uC2DC\uC791" : "\uC885\uB8CC"} ${isHour ? "\uC2DC" : "\uBD84"} \uC99D\uAC00`,
28956
- onClick: () => isHour ? changeHour(side, 1) : changeMinute(side, minuteStep)
28957
- }
28958
- ),
28959
- /* @__PURE__ */ jsx(
28960
- "input",
28961
- {
28962
- "aria-label": `${side === "start" ? "\uC2DC\uC791" : "\uC885\uB8CC"} ${isHour ? "\uC2DC" : "\uBD84"}`,
28963
- inputMode: "numeric",
28964
- value: displayValue2,
28965
- placeholder: "-",
28966
- onFocus: () => beginEdit(side, kind, val),
28967
- onBlur: () => endEdit(side, kind),
28968
- onChange: (event) => {
28969
- const draft = sanitizeInput(event.target.value);
28970
- setInputDraft((prev) => ({ ...prev, [currentEditingKey]: draft }));
28971
- if (draft === "") return;
28972
- const raw = Number(draft);
28973
- if (isHour) {
28974
- if (draft.length < 2) return;
28975
- const clamped2 = commitHourDraft(side, draft);
28976
- const displayDraft2 = raw === clamped2 ? draft : String(clamped2);
28977
- setInputDraft((prev) => ({ ...prev, [currentEditingKey]: displayDraft2 }));
28978
- return;
28979
- }
28980
- if (draft.length < 2) return;
28981
- const clamped = commitMinuteDraft(side, draft);
28982
- const displayDraft = raw === clamped ? draft : String(clamped);
28983
- setInputDraft((prev) => ({ ...prev, [currentEditingKey]: displayDraft }));
28984
- },
28985
- className: "box-border bg-white text-center outline-none",
28986
- style: inputStyle
28987
- }
28988
- ),
28989
- /* @__PURE__ */ jsx(
28990
- SGhostButton,
28991
- {
28992
- icon: "chevronDown",
28993
- size: "xs",
28994
- ariaLabel: `${side === "start" ? "\uC2DC\uC791" : "\uC885\uB8CC"} ${isHour ? "\uC2DC" : "\uBD84"} \uAC10\uC18C`,
28995
- onClick: () => isHour ? changeHour(side, -1) : changeMinute(side, -minuteStep)
28996
- }
28997
- )
28998
- ]
28999
- }
29000
- );
29067
+ onChange([time, strict && toMinutes(time) > toMinutes(range[1]) ? time : range[1]]);
29001
29068
  };
29002
29069
  const renderPanel = (side, title) => /* @__PURE__ */ jsxs(
29003
29070
  "div",
29004
29071
  {
29005
- className: "flex flex-col items-center justify-center",
29006
- style: { gap: "var(--cmp-timepicker-selector-range-section-gap)" },
29072
+ className: "flex flex-col items-center",
29073
+ style: { gap: "var(--cmp-timepicker-selector-range-heading-gap)" },
29007
29074
  children: [
29008
- /* @__PURE__ */ jsxs(
29009
- "div",
29010
- {
29011
- className: "flex flex-col items-center",
29012
- style: { gap: "var(--cmp-timepicker-selector-range-heading-gap)" },
29013
- children: [
29014
- /* @__PURE__ */ jsx("div", { className: "whitespace-nowrap text-center text-[12px] font-bold leading-[20px] text-[var(--cmp-timepicker-selector-range-heading-color)]", children: title }),
29015
- showMeridiem && /* @__PURE__ */ jsx("div", { className: "isolate flex", children: ["AM", "PM"].map((item4, index) => {
29016
- const selected = item4 === getMeridiem(side);
29017
- return /* @__PURE__ */ jsx(
29018
- "button",
29019
- {
29020
- type: "button",
29021
- onClick: () => setMeridiem(side, item4),
29022
- className: cn(
29023
- "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]",
29024
- index === 0 ? "rounded-l-[var(--cmp-select-sm-radius)] -mr-px" : "rounded-r-[var(--cmp-select-sm-radius)]",
29025
- 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)]"
29026
- ),
29027
- children: item4 === "AM" ? "\uC624\uC804" : "\uC624\uD6C4"
29028
- },
29029
- item4
29030
- );
29031
- }) })
29032
- ]
29033
- }
29034
- ),
29035
- /* @__PURE__ */ jsxs(
29036
- "div",
29075
+ /* @__PURE__ */ jsx("div", { className: "whitespace-nowrap text-center text-[12px] font-bold leading-[20px] text-[var(--cmp-timepicker-selector-range-heading-color)]", children: title }),
29076
+ /* @__PURE__ */ jsx(
29077
+ TimeListbox,
29037
29078
  {
29038
- className: "flex items-center justify-center",
29039
- style: {
29040
- gap: "var(--cmp-timepicker-selector-time-gap)",
29041
- padding: "0 var(--cmp-timepicker-selector-section-paddingX)"
29042
- },
29043
- children: [
29044
- renderColumn(side, "hour"),
29045
- /* @__PURE__ */ jsx(
29046
- "span",
29047
- {
29048
- className: "text-center",
29049
- style: {
29050
- width: "var(--cmp-timepicker-selector-colon-size)",
29051
- color: "var(--cmp-field-hint-color)",
29052
- fontSize: "var(--cmp-timepicker-selector-colon-size)",
29053
- lineHeight: "var(--cmp-timepicker-selector-colon-size)"
29054
- },
29055
- children: ":"
29056
- }
29057
- ),
29058
- renderColumn(side, "minute")
29059
- ]
29079
+ value: side === "start" ? range[0] : range[1],
29080
+ showMeridiem,
29081
+ minuteStep,
29082
+ labelPrefix: side === "start" ? "\uC2DC\uC791" : "\uC885\uB8CC",
29083
+ elevated: false,
29084
+ isDisabled: strict && side === "end" ? (minutes) => minutes < toMinutes(range[0]) : void 0,
29085
+ onChange: (time) => change(side, time)
29060
29086
  }
29061
29087
  )
29062
29088
  ]
@@ -29115,11 +29141,9 @@ function STimeRangePicker({
29115
29141
  style
29116
29142
  }) {
29117
29143
  const portalContainer = usePortalContainer();
29118
- const triggerRef = useRef(null);
29119
29144
  const [open, setOpen] = useState(false);
29120
29145
  const triggerId = useFieldControlId();
29121
29146
  const triggerTextId = `${triggerId}-text`;
29122
- const [popoverAlignOffset, setPopoverAlignOffset] = useState(0);
29123
29147
  const [focused, setFocused] = useState(false);
29124
29148
  const [ruleError, setRuleError] = useState("");
29125
29149
  const dims = TIMEPICKER_SIZE_CONFIG[size] ?? TIMEPICKER_SIZE_CONFIG.sm;
@@ -29143,10 +29167,6 @@ function STimeRangePicker({
29143
29167
  const resolvedStatus = status ?? (rules && rules.length > 0 ? ruleError !== "" ? "error" : void 0 : void 0);
29144
29168
  const commitOpen = (next) => {
29145
29169
  if (disabled) return;
29146
- if (next) {
29147
- const triggerWidth = triggerRef.current?.getBoundingClientRect().width ?? 0;
29148
- setPopoverAlignOffset(Math.max(0, (triggerWidth - TIME_RANGE_SELECTOR_WIDTH) / 2));
29149
- }
29150
29170
  setOpen(next);
29151
29171
  onOpenChange?.(next);
29152
29172
  if (!next && rules && rules.length > 0) setRuleError(runRules(value, rules));
@@ -29185,7 +29205,6 @@ function STimeRangePicker({
29185
29205
  /* @__PURE__ */ jsx(RPopover13.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(
29186
29206
  "div",
29187
29207
  {
29188
- ref: triggerRef,
29189
29208
  id: triggerId,
29190
29209
  role: "button",
29191
29210
  "aria-disabled": disabled,
@@ -29249,9 +29268,8 @@ function STimeRangePicker({
29249
29268
  RPopover13.Content,
29250
29269
  {
29251
29270
  side: "bottom",
29252
- align: "start",
29271
+ align: "center",
29253
29272
  sideOffset: 4,
29254
- alignOffset: popoverAlignOffset,
29255
29273
  className: cn(
29256
29274
  "z-50 max-w-[var(--radix-popover-content-available-width)]",
29257
29275
  FLOATING_SLIDE_ANIM