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.cjs CHANGED
@@ -4515,6 +4515,8 @@ var SGhostButton = /* @__PURE__ */ react.forwardRef(function SGhostButton2({
4515
4515
  ariaExpanded,
4516
4516
  tooltipText,
4517
4517
  disabled = false,
4518
+ href,
4519
+ target,
4518
4520
  onClick,
4519
4521
  anchorClassName,
4520
4522
  anchorStyle,
@@ -4538,51 +4540,74 @@ var SGhostButton = /* @__PURE__ */ react.forwardRef(function SGhostButton2({
4538
4540
  cancelHide();
4539
4541
  hideTimer.current = setTimeout(() => setOpen(false), TOOLTIP_HIDE_DELAY_MS);
4540
4542
  };
4541
- const s = SIZE[size];
4542
- const contentColor = disabled ? intent === "inverse" ? "var(--cmp-ghostButton-inverse-disabled)" : "var(--cmp-ghostButton-icon-disabled)" : `var(--cmp-ghostButton-${intent}-content)`;
4543
4543
  const text = (tooltipText ?? "").trim();
4544
4544
  const hasTooltip = text !== "";
4545
- const btn2 = /* @__PURE__ */ jsxRuntime.jsxs(
4545
+ const [prevHasTooltip, setPrevHasTooltip] = react.useState(hasTooltip);
4546
+ if (prevHasTooltip !== hasTooltip) {
4547
+ setPrevHasTooltip(hasTooltip);
4548
+ if (!hasTooltip) setOpen(false);
4549
+ }
4550
+ const s = SIZE[size];
4551
+ const contentColor = disabled ? intent === "inverse" ? "var(--cmp-ghostButton-inverse-disabled)" : "var(--cmp-ghostButton-icon-disabled)" : `var(--cmp-ghostButton-${intent}-content)`;
4552
+ const shared = {
4553
+ "aria-disabled": disabled ? "true" : void 0,
4554
+ "aria-label": ariaLabel.trim() || void 0,
4555
+ "tabIndex": disabled ? -1 : void 0,
4556
+ "onClick": (e) => {
4557
+ if (disabled) {
4558
+ e.preventDefault();
4559
+ return;
4560
+ }
4561
+ onClick?.(e);
4562
+ },
4563
+ "className": cn(
4564
+ "group/ghost-button relative box-border inline-flex items-center justify-center border-none bg-transparent p-0 transition-shadow",
4565
+ disabled ? "cursor-not-allowed" : "cursor-pointer",
4566
+ "focus-visible:outline-none focus-visible:shadow-[0_0_0_2px_var(--cmp-ghostButton-action-hover-bg)]",
4567
+ className
4568
+ ),
4569
+ "style": {
4570
+ width: `var(--cmp-ghostButton-${size}-size)`,
4571
+ height: `var(--cmp-ghostButton-${size}-size)`,
4572
+ borderRadius: `var(--cmp-ghostButton-radius-${s.radius})`,
4573
+ ...style
4574
+ }
4575
+ };
4576
+ const content = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4577
+ !disabled && /* @__PURE__ */ jsxRuntime.jsx(
4578
+ "span",
4579
+ {
4580
+ "aria-hidden": true,
4581
+ className: "pointer-events-none absolute inset-0 rounded-[inherit] opacity-0 transition-opacity duration-200 group-hover/ghost-button:opacity-5",
4582
+ style: { background: `var(--cmp-ghostButton-${intent}-hover-bg)` }
4583
+ }
4584
+ ),
4585
+ /* @__PURE__ */ jsxRuntime.jsx(SIcon, { name: icon, size: s.icon, color: contentColor })
4586
+ ] });
4587
+ const btn2 = href != null ? (
4588
+ // 비활성 링크는 주소를 뗀다 — `<a>` 에는 disabled 가 없어서 주소가 남으면 가운데 클릭으로 열린다.
4589
+ /* @__PURE__ */ jsxRuntime.jsx(
4590
+ "a",
4591
+ {
4592
+ ref,
4593
+ href: disabled ? void 0 : href,
4594
+ target,
4595
+ rel: target === "_blank" ? "noopener noreferrer" : void 0,
4596
+ role: disabled ? "link" : void 0,
4597
+ ...shared,
4598
+ children: content
4599
+ }
4600
+ )
4601
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
4546
4602
  "button",
4547
4603
  {
4548
4604
  ref,
4549
4605
  type: "button",
4550
4606
  disabled,
4551
- "aria-disabled": disabled ? "true" : void 0,
4552
- "aria-label": ariaLabel.trim() || void 0,
4553
4607
  "aria-pressed": ariaPressed,
4554
4608
  "aria-expanded": ariaExpanded,
4555
- tabIndex: disabled ? -1 : void 0,
4556
- onClick: (e) => {
4557
- if (disabled) {
4558
- e.preventDefault();
4559
- return;
4560
- }
4561
- onClick?.(e);
4562
- },
4563
- className: cn(
4564
- "group/ghost-button relative box-border inline-flex items-center justify-center border-none bg-transparent p-0 transition-shadow",
4565
- disabled ? "cursor-not-allowed" : "cursor-pointer",
4566
- "focus-visible:outline-none focus-visible:shadow-[0_0_0_2px_var(--cmp-ghostButton-action-hover-bg)]",
4567
- className
4568
- ),
4569
- style: {
4570
- width: `var(--cmp-ghostButton-${size}-size)`,
4571
- height: `var(--cmp-ghostButton-${size}-size)`,
4572
- borderRadius: `var(--cmp-ghostButton-radius-${s.radius})`,
4573
- ...style
4574
- },
4575
- children: [
4576
- !disabled && /* @__PURE__ */ jsxRuntime.jsx(
4577
- "span",
4578
- {
4579
- "aria-hidden": true,
4580
- className: "pointer-events-none absolute inset-0 rounded-[inherit] opacity-0 transition-opacity duration-200 group-hover/ghost-button:opacity-5",
4581
- style: { background: `var(--cmp-ghostButton-${intent}-hover-bg)` }
4582
- }
4583
- ),
4584
- /* @__PURE__ */ jsxRuntime.jsx(SIcon, { name: icon, size: s.icon, color: contentColor })
4585
- ]
4609
+ ...shared,
4610
+ children: content
4586
4611
  }
4587
4612
  );
4588
4613
  if (!hasTooltip) return btn2;
@@ -5310,6 +5335,9 @@ var FIELD_BORDER = 1;
5310
5335
  var TEXTAREA_MIN_HEIGHT = `calc(var(--cmp-chatInput-buttonFrame-height) - ${FIELD_BORDER * 2}px)`;
5311
5336
  var BUTTON_BOTTOM_INSET = `calc((var(--cmp-chatInput-buttonFrame-height) - var(--cmp-ghostButton-md-size)) / 2)`;
5312
5337
  var DEFAULT_MAX_ROWS = 5;
5338
+ function BarButton(props) {
5339
+ return /* @__PURE__ */ jsxRuntime.jsx(SGhostButton, { ...props, size: "md", style: { marginBottom: BUTTON_BOTTOM_INSET } });
5340
+ }
5313
5341
  var SChatInputImpl = /* @__PURE__ */ react.forwardRef(function SChatInput({
5314
5342
  value = "",
5315
5343
  onValueChange,
@@ -5317,7 +5345,13 @@ var SChatInputImpl = /* @__PURE__ */ react.forwardRef(function SChatInput({
5317
5345
  disabled = false,
5318
5346
  onSend,
5319
5347
  hasAttachments = false,
5348
+ onAttachFiles,
5349
+ attachAccept,
5350
+ attachMultiple = true,
5320
5351
  onAttach,
5352
+ attachDisabled = false,
5353
+ attachTooltip,
5354
+ sendDisabled = false,
5321
5355
  onPasteFiles,
5322
5356
  busy = false,
5323
5357
  onStop,
@@ -5329,11 +5363,23 @@ var SChatInputImpl = /* @__PURE__ */ react.forwardRef(function SChatInput({
5329
5363
  ...rest
5330
5364
  }, ref) {
5331
5365
  const hasText = value.trim() !== "";
5332
- const canSend = !disabled && !busy && (hasText || hasAttachments);
5333
- const send = react.useCallback(() => {
5366
+ const canSend = !disabled && !busy && !sendDisabled && (hasText || hasAttachments);
5367
+ const send = () => {
5334
5368
  if (!canSend) return;
5335
5369
  onSend?.(hasText ? value : "");
5336
- }, [canSend, hasText, onSend, value]);
5370
+ };
5371
+ const fileInputRef = react.useRef(null);
5372
+ const hasAttachButton = onAttachFiles != null || onAttach != null;
5373
+ const attachLocked = disabled || attachDisabled;
5374
+ const handleAttachClick = () => {
5375
+ if (onAttachFiles != null) fileInputRef.current?.click();
5376
+ else onAttach?.();
5377
+ };
5378
+ const handleFilesPicked = (e) => {
5379
+ const files = Array.from(e.target.files ?? []);
5380
+ e.target.value = "";
5381
+ if (files.length > 0) onAttachFiles?.(files);
5382
+ };
5337
5383
  const handleKeyDown = (e) => {
5338
5384
  rest.onKeyDown?.(e);
5339
5385
  if (e.key !== "Enter" || e.shiftKey || e.nativeEvent.isComposing) return;
@@ -5346,19 +5392,31 @@ var SChatInputImpl = /* @__PURE__ */ react.forwardRef(function SChatInput({
5346
5392
  const files = Array.from(e.clipboardData.files);
5347
5393
  if (files.length === 0) return;
5348
5394
  e.preventDefault();
5395
+ if (attachDisabled) return;
5349
5396
  onPasteFiles(files);
5350
5397
  };
5351
5398
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex flex-col bg-(--cmp-chatInput-bg)", className), style, children: [
5352
5399
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-end gap-(--cmp-chatInput-gap) p-(--cmp-chatInput-padding)", children: [
5353
- onAttach != null && /* @__PURE__ */ jsxRuntime.jsx(
5354
- SGhostButton,
5400
+ hasAttachButton && /* @__PURE__ */ jsxRuntime.jsx(
5401
+ BarButton,
5355
5402
  {
5356
5403
  icon: "attachFile",
5357
- size: "md",
5358
5404
  ariaLabel: "\uD30C\uC77C \uCCA8\uBD80",
5359
- disabled,
5360
- onClick: onAttach,
5361
- style: { marginBottom: BUTTON_BOTTOM_INSET }
5405
+ tooltipText: attachTooltip,
5406
+ disabled: attachLocked,
5407
+ onClick: handleAttachClick
5408
+ }
5409
+ ),
5410
+ onAttachFiles != null && /* @__PURE__ */ jsxRuntime.jsx(
5411
+ "input",
5412
+ {
5413
+ ref: fileInputRef,
5414
+ type: "file",
5415
+ className: "hidden",
5416
+ accept: attachAccept,
5417
+ multiple: attachMultiple,
5418
+ disabled: attachLocked,
5419
+ onChange: handleFilesPicked
5362
5420
  }
5363
5421
  ),
5364
5422
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -5382,26 +5440,26 @@ var SChatInputImpl = /* @__PURE__ */ react.forwardRef(function SChatInput({
5382
5440
  onPaste: handlePaste
5383
5441
  }
5384
5442
  ),
5385
- busy ? /* @__PURE__ */ jsxRuntime.jsx(
5386
- SGhostButton,
5387
- {
5388
- icon: "stop",
5389
- size: "md",
5390
- intent: "danger",
5391
- ariaLabel: "\uC751\uB2F5 \uC911\uC9C0",
5392
- onClick: onStop,
5393
- style: { marginBottom: BUTTON_BOTTOM_INSET }
5394
- }
5443
+ busy ? (
5444
+ // disabled 는 모두 잠근다 — 응답 도중 대화가 끝나면 정지도 할 수 없다
5445
+ /* @__PURE__ */ jsxRuntime.jsx(
5446
+ BarButton,
5447
+ {
5448
+ icon: "stop",
5449
+ intent: "danger",
5450
+ ariaLabel: "\uC751\uB2F5 \uC911\uC9C0",
5451
+ disabled,
5452
+ onClick: onStop
5453
+ }
5454
+ )
5395
5455
  ) : /* @__PURE__ */ jsxRuntime.jsx(
5396
- SGhostButton,
5456
+ BarButton,
5397
5457
  {
5398
5458
  icon: "send",
5399
- size: "md",
5400
5459
  intent: "subAction",
5401
5460
  ariaLabel: "\uC804\uC1A1",
5402
5461
  disabled: !canSend,
5403
- onClick: send,
5404
- style: { marginBottom: BUTTON_BOTTOM_INSET }
5462
+ onClick: send
5405
5463
  }
5406
5464
  )
5407
5465
  ] }),
@@ -6642,6 +6700,7 @@ var SExpansionItem = /* @__PURE__ */ react.forwardRef(
6642
6700
  depth = 1,
6643
6701
  leading,
6644
6702
  trailing,
6703
+ headerAction,
6645
6704
  bodyPadding = true,
6646
6705
  bordered = false,
6647
6706
  expanded,
@@ -6681,12 +6740,92 @@ var SExpansionItem = /* @__PURE__ */ react.forwardRef(
6681
6740
  );
6682
6741
  const leadingNode = typeof leading === "function" ? leading({ expanded: isExpanded, selected: isSelected, disabled }) : leading;
6683
6742
  const hasSupportingText = supportingText != null && supportingText !== "";
6743
+ const hasHeaderAction = headerAction != null && headerAction !== false;
6684
6744
  const id = react.useId();
6745
+ const surfaceClass = cn(
6746
+ isExpanded && "border-b border-(--cmp-listExpansionItem-title-border)",
6747
+ hasChevronInteraction ? "hover:bg-(--sys-color-bg-neutralBright)" : !isExpanded && "hover:bg-(--cmp-listExpansionItem-title-bg-hover)",
6748
+ // 펼침 배경(파랑)은 bordered 일 때만. 카드처럼 테두리로 경계가 잡힌
6749
+ // 아이템에서만 배경이 선택 상태를 나타낸다.
6750
+ // 테두리 없는 리스트에서는 accentStripe 가 그 역할을 하므로,
6751
+ // 배경까지 깔면 항목 경계가 없는 목록에서 선택 표시가 두 겹이 된다.
6752
+ isExpanded && isSelected && bordered && "bg-(--cmp-listExpansionItem-title-bg-expanded)"
6753
+ );
6754
+ const hoverHandlers = {
6755
+ onMouseEnter: () => setHovered(true),
6756
+ onMouseLeave: () => setHovered(false)
6757
+ };
6685
6758
  react.useEffect(() => {
6686
6759
  if (!isExpanded) return;
6687
6760
  parentDescendantContext?.reportExpandedDescendant(1);
6688
6761
  return () => parentDescendantContext?.reportExpandedDescendant(-1);
6689
6762
  }, [isExpanded, parentDescendantContext]);
6763
+ const toggleButton = /* @__PURE__ */ jsxRuntime.jsxs(
6764
+ "button",
6765
+ {
6766
+ type: "button",
6767
+ disabled,
6768
+ "aria-expanded": isExpanded,
6769
+ "aria-controls": id,
6770
+ ...hasHeaderAction ? {} : hoverHandlers,
6771
+ onClick: (event) => {
6772
+ if (disabled) return;
6773
+ const next = !isExpanded;
6774
+ if (expanded === void 0) setInternalExpanded(next);
6775
+ onToggle?.(next, event);
6776
+ },
6777
+ className: cn(
6778
+ "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)",
6779
+ titleTypo,
6780
+ // 헤더 액션이 있으면 면은 바깥 줄이 그린다 — 버튼은 남은 폭만 차지한다
6781
+ hasHeaderAction ? "min-w-0 flex-1" : surfaceClass,
6782
+ // 높이는 세로 패딩과 내용이 정한다 — 제목 줄에 높이를 못 박지 않는다 (SListItem 과 동일)
6783
+ dense ? "py-(--cmp-listExpansionItem-title-paddingY-dense)" : "py-(--cmp-listExpansionItem-title-paddingY-normal)",
6784
+ "gap-(--cmp-listExpansionItem-title-gap)",
6785
+ // 헤더 액션이 있으면 오른쪽 끝 여백은 액션 칸이 갖고, 버튼은 액션과의 간격만 남긴다
6786
+ hasHeaderAction ? "pr-(--cmp-listExpansionItem-title-gap)" : "pr-(--cmp-listExpansionItem-title-paddingX)",
6787
+ depth === 2 ? "pl-[36px]" : depth === 3 ? "pl-[56px]" : "pl-[16px]",
6788
+ isExpanded && "text-(--cmp-listExpansionItem-title-text-expanded)"
6789
+ ),
6790
+ children: [
6791
+ !leading && /* @__PURE__ */ jsxRuntime.jsx(
6792
+ "span",
6793
+ {
6794
+ className: cn(
6795
+ "flex transition-transform duration-200 ease-out motion-reduce:transition-none",
6796
+ isExpanded && "rotate-180"
6797
+ ),
6798
+ children: /* @__PURE__ */ jsxRuntime.jsx(SIcon, { name: "caretDown", size: 12, color: "var(--sys-color-fg-quaternary)" })
6799
+ }
6800
+ ),
6801
+ leadingNode,
6802
+ /* @__PURE__ */ jsxRuntime.jsxs(
6803
+ "span",
6804
+ {
6805
+ className: cn(
6806
+ "min-w-0 flex-1",
6807
+ supportingTextPosition === "bottom" ? "flex flex-col" : "flex items-center gap-[8px] whitespace-nowrap"
6808
+ ),
6809
+ children: [
6810
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 truncate", children: title }),
6811
+ hasSupportingText && /* @__PURE__ */ jsxRuntime.jsx(
6812
+ "span",
6813
+ {
6814
+ className: cn(
6815
+ "typo-body-sm-default min-w-0 truncate text-(--sys-color-fg-secondary)",
6816
+ supportingTextPosition === "right" && "flex-1"
6817
+ ),
6818
+ children: supportingText
6819
+ }
6820
+ )
6821
+ ]
6822
+ }
6823
+ ),
6824
+ trailing,
6825
+ showInteractionChevron && /* @__PURE__ */ jsxRuntime.jsx(SIcon, { name: "chevronRight", size: 12, color: interactionChevronColor })
6826
+ ]
6827
+ }
6828
+ );
6690
6829
  return /* @__PURE__ */ jsxRuntime.jsxs(
6691
6830
  "div",
6692
6831
  {
@@ -6709,79 +6848,13 @@ var SExpansionItem = /* @__PURE__ */ react.forwardRef(
6709
6848
  className: "pointer-events-none absolute bottom-[-1px] left-0 top-0 z-[1] w-(--cmp-listItem-accentStripe-width) bg-(--cmp-listItem-accentStripe-color)"
6710
6849
  }
6711
6850
  ),
6712
- /* @__PURE__ */ jsxRuntime.jsxs(
6713
- "button",
6714
- {
6715
- type: "button",
6716
- disabled,
6717
- "aria-expanded": isExpanded,
6718
- "aria-controls": id,
6719
- onMouseEnter: () => setHovered(true),
6720
- onMouseLeave: () => setHovered(false),
6721
- onClick: (event) => {
6722
- if (disabled) return;
6723
- const next = !isExpanded;
6724
- if (expanded === void 0) setInternalExpanded(next);
6725
- onToggle?.(next, event);
6726
- },
6727
- className: cn(
6728
- "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)",
6729
- titleTypo,
6730
- isExpanded && "border-b border-(--cmp-listExpansionItem-title-border)",
6731
- hasChevronInteraction ? "hover:bg-(--sys-color-bg-neutralBright)" : !isExpanded && "hover:bg-(--cmp-listExpansionItem-title-bg-hover)",
6732
- // 높이는 세로 패딩과 내용이 정한다 — 제목 줄에 높이를 못 박지 않는다 (SListItem 과 동일)
6733
- dense ? "py-(--cmp-listExpansionItem-title-paddingY-dense)" : "py-(--cmp-listExpansionItem-title-paddingY-normal)",
6734
- "gap-(--cmp-listExpansionItem-title-gap)",
6735
- "pr-(--cmp-listExpansionItem-title-paddingX)",
6736
- depth === 2 ? "pl-[36px]" : depth === 3 ? "pl-[56px]" : "pl-[16px]",
6737
- isExpanded && cn(
6738
- "text-(--cmp-listExpansionItem-title-text-expanded)",
6739
- // 펼침 배경(파랑)은 bordered 일 때만. 카드처럼 테두리로 경계가 잡힌
6740
- // 아이템에서만 배경이 선택 상태를 나타낸다.
6741
- // 테두리 없는 리스트에서는 accentStripe 가 그 역할을 하므로,
6742
- // 배경까지 깔면 항목 경계가 없는 목록에서 선택 표시가 두 겹이 된다.
6743
- isSelected && bordered && "bg-(--cmp-listExpansionItem-title-bg-expanded)"
6744
- )
6745
- ),
6746
- children: [
6747
- !leading && /* @__PURE__ */ jsxRuntime.jsx(
6748
- "span",
6749
- {
6750
- className: cn(
6751
- "flex transition-transform duration-200 ease-out motion-reduce:transition-none",
6752
- isExpanded && "rotate-180"
6753
- ),
6754
- children: /* @__PURE__ */ jsxRuntime.jsx(SIcon, { name: "caretDown", size: 12, color: "var(--sys-color-fg-quaternary)" })
6755
- }
6756
- ),
6757
- leadingNode,
6758
- /* @__PURE__ */ jsxRuntime.jsxs(
6759
- "span",
6760
- {
6761
- className: cn(
6762
- "min-w-0 flex-1",
6763
- supportingTextPosition === "bottom" ? "flex flex-col" : "flex items-center gap-[8px] whitespace-nowrap"
6764
- ),
6765
- children: [
6766
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 truncate", children: title }),
6767
- hasSupportingText && /* @__PURE__ */ jsxRuntime.jsx(
6768
- "span",
6769
- {
6770
- className: cn(
6771
- "typo-body-sm-default min-w-0 truncate text-(--sys-color-fg-secondary)",
6772
- supportingTextPosition === "right" && "flex-1"
6773
- ),
6774
- children: supportingText
6775
- }
6776
- )
6777
- ]
6778
- }
6779
- ),
6780
- trailing,
6781
- showInteractionChevron && /* @__PURE__ */ jsxRuntime.jsx(SIcon, { name: "chevronRight", size: 12, color: interactionChevronColor })
6782
- ]
6783
- }
6784
- ),
6851
+ hasHeaderAction ? (
6852
+ // 헤더 액션은 여닫는 버튼의 형제다 — 안에 넣으면 버튼이 중첩되고 누를 때마다 여닫힌다
6853
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex items-center", surfaceClass), ...hoverHandlers, children: [
6854
+ toggleButton,
6855
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex shrink-0 items-center gap-(--cmp-listExpansionItem-title-gap) pr-(--cmp-listExpansionItem-title-paddingX)", children: headerAction })
6856
+ ] })
6857
+ ) : toggleButton,
6785
6858
  /* @__PURE__ */ jsxRuntime.jsx(SExpansionItemDescendantContext.Provider, { value: descendantContextValue, children: /* @__PURE__ */ jsxRuntime.jsx(
6786
6859
  "div",
6787
6860
  {
@@ -13350,9 +13423,20 @@ var SystemButton = /* @__PURE__ */ react.forwardRef(function SystemButton2({
13350
13423
  }
13351
13424
  ),
13352
13425
  label && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "typo-body-sm-semibold min-w-0 truncate", children: label }),
13353
- count != null && count > 0 && // 개수 오른쪽은 버튼 여백(alertButton.paddingX)만으로는 좁다 — 시안이 4px 을 더 준다.
13426
+ count != null && count > 0 && // 개수는 채운 배지로 단다 — 배경·글자색·치수는 xs deep 태그 토큰이 쥔다(시안).
13427
+ // 배지 색은 고정이다: 흰 면은 파랑, 어두운 면(dark·darker)은 노랑 바탕에 검은 글자.
13428
+ // 배지 오른쪽은 버튼 여백(alertButton.paddingX)만으로는 좁다 — 시안이 4px 을 더 준다.
13354
13429
  // 대응 토큰이 없어 디자인 값을 그대로 쓴다.
13355
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "typo-body-md-bold me-[4px] shrink-0", style: { color: accentColor }, children: count.toLocaleString() })
13430
+ /* @__PURE__ */ jsxRuntime.jsx(
13431
+ STag,
13432
+ {
13433
+ size: "xs",
13434
+ deep: true,
13435
+ color: color === "light" ? "blue" : "yellow",
13436
+ label: count.toLocaleString(),
13437
+ className: "me-[4px] shrink-0"
13438
+ }
13439
+ )
13356
13440
  ]
13357
13441
  }
13358
13442
  );
@@ -14231,7 +14315,7 @@ var SStepper = /* @__PURE__ */ react.forwardRef(function SStepper2({
14231
14315
  items.findIndex((item4, index) => getItemValue(item4, index) === value)
14232
14316
  );
14233
14317
  const lastCompletedIndex = items.reduce(
14234
- (lastIndex, item4, index) => item4.completed === true ? index : lastIndex,
14318
+ (lastIndex2, item4, index) => item4.completed === true ? index : lastIndex2,
14235
14319
  -1
14236
14320
  );
14237
14321
  const resolvedClickableIndex = lastCompletedIndex + 1;
@@ -14673,12 +14757,25 @@ function monthCells(year, month) {
14673
14757
  return cells;
14674
14758
  }
14675
14759
  var WEEK_LABELS = ["\uC77C", "\uC6D4", "\uD654", "\uC218", "\uBAA9", "\uAE08", "\uD1A0"];
14760
+ var WEEKDAY_SUFFIX_EM = 0.25 + 3 * 1;
14761
+ var weekdaySuffixWidth = (fontSize) => Math.ceil(fontSize * WEEKDAY_SUFFIX_EM);
14762
+ function withWeekday(value) {
14763
+ const [date = "", ...rest] = value.split(" ");
14764
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
14765
+ if (!match) return value;
14766
+ const day = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3])).getDay();
14767
+ if (Number.isNaN(day)) return value;
14768
+ return [date, `${WEEK_LABELS[day]}\uC694\uC77C`, ...rest].join(" ");
14769
+ }
14676
14770
 
14677
14771
  // src/components/SCalendarBoard/calendar-board-layout.ts
14678
14772
  function eventEndDate(event) {
14679
14773
  const end = event.endDate;
14680
14774
  return end != null && end > event.date ? end : event.date;
14681
14775
  }
14776
+ function compareDate(a, b) {
14777
+ return a < b ? -1 : a > b ? 1 : 0;
14778
+ }
14682
14779
  function layoutCalendarBoard({
14683
14780
  cells,
14684
14781
  events,
@@ -14724,7 +14821,7 @@ function layoutCalendarBoard({
14724
14821
  flush(to);
14725
14822
  });
14726
14823
  runs.sort(
14727
- (a, b) => a.startIndex - b.startIndex || b.endIndex - b.startIndex - (a.endIndex - a.startIndex) || a.order - b.order
14824
+ (a, b) => a.startIndex - b.startIndex || compareDate(a.event.date, b.event.date) || compareDate(eventEndDate(b.event), eventEndDate(a.event)) || a.order - b.order
14728
14825
  );
14729
14826
  const weekLanes = Array.from({ length: weekCount }, () => []);
14730
14827
  for (const run of runs) {
@@ -17997,7 +18094,8 @@ var STableBar = /* @__PURE__ */ react.forwardRef(function STableBar2({ title, to
17997
18094
  );
17998
18095
  });
17999
18096
  var NO_KEYS = /* @__PURE__ */ new Set();
18000
- var ROW_ENTER_MS = 200;
18097
+ var EXPAND_MOTION = { duration: 220, easing: "ease-out" };
18098
+ var COLLAPSE_MOTION = { duration: 140, easing: "ease-in" };
18001
18099
  function STreeTable({
18002
18100
  rows = [],
18003
18101
  columns = [],
@@ -18044,32 +18142,88 @@ function STreeTable({
18044
18142
  if (expandedKeys === void 0) setInnerExpandedKeys(next);
18045
18143
  onExpandedChange?.(next, row);
18046
18144
  };
18047
- const visibleRows = react.useMemo(() => {
18048
- const out = [];
18049
- const walk = (nodes) => {
18050
- nodes.forEach((row) => {
18051
- out.push(row);
18052
- if (expandedSet.has(keyOf(row))) walk(childrenOf(row));
18053
- });
18054
- };
18055
- walk(rows);
18056
- return out;
18057
- }, [rows, expandedSet, rowKey, childrenKey]);
18058
- const [enteringKeys, setEnteringKeys] = react.useState(NO_KEYS);
18145
+ const visibleRows = react.useMemo(
18146
+ () => flattenRows(rows, (row) => expandedSet.has(keyOf(row)), childrenOf),
18147
+ // eslint-disable-next-line react-hooks/exhaustive-deps
18148
+ [rows, expandedSet, rowKey, childrenKey]
18149
+ );
18150
+ const tableRef = react.useRef(null);
18151
+ const [closingKeys, setClosingKeys] = react.useState(NO_KEYS);
18152
+ const [prevExpand, setPrevExpand] = react.useState({ rows, expandedSet });
18153
+ if (prevExpand.rows !== rows || prevExpand.expandedSet !== expandedSet) {
18154
+ setPrevExpand({ rows, expandedSet });
18155
+ let nextClosing = NO_KEYS;
18156
+ if (prevExpand.rows === rows && motionAllowed()) {
18157
+ const collapsed = [...prevExpand.expandedSet].filter((key) => !expandedSet.has(key));
18158
+ const expanded = [...expandedSet].filter((key) => !prevExpand.expandedSet.has(key));
18159
+ if (collapsed.length > 0 && expanded.length === 0) nextClosing = new Set(collapsed);
18160
+ }
18161
+ if (nextClosing !== closingKeys) setClosingKeys(nextClosing);
18162
+ }
18163
+ const renderedRows = react.useMemo(
18164
+ () => closingKeys.size === 0 ? visibleRows : flattenRows(
18165
+ rows,
18166
+ (row) => expandedSet.has(keyOf(row)) || closingKeys.has(keyOf(row)),
18167
+ childrenOf
18168
+ ),
18169
+ // eslint-disable-next-line react-hooks/exhaustive-deps
18170
+ [visibleRows, closingKeys]
18171
+ );
18172
+ const visibleKeys = react.useMemo(
18173
+ () => new Set(visibleRows.map(keyOf)),
18174
+ // eslint-disable-next-line react-hooks/exhaustive-deps
18175
+ [visibleRows]
18176
+ );
18177
+ const motionRef = react.useRef(null);
18059
18178
  const prevVisibleRef = react.useRef(null);
18060
18179
  useIsomorphicLayoutEffect(() => {
18061
- const keys = new Set(visibleRows.map(keyOf));
18062
18180
  const prev = prevVisibleRef.current;
18063
- prevVisibleRef.current = { rows, keys };
18064
- if (!prev || prev.rows !== rows) return;
18065
- const added = [...keys].filter((key) => !prev.keys.has(key));
18066
- if (added.length > 0) setEnteringKeys(new Set(added));
18067
- }, [visibleRows]);
18068
- react.useEffect(() => {
18069
- if (enteringKeys.size === 0) return;
18070
- const timer = setTimeout(() => setEnteringKeys(NO_KEYS), ROW_ENTER_MS);
18071
- return () => clearTimeout(timer);
18072
- }, [enteringKeys]);
18181
+ prevVisibleRef.current = { rows, keys: visibleKeys };
18182
+ const running = motionRef.current;
18183
+ if (running?.done) {
18184
+ running.cancel();
18185
+ motionRef.current = null;
18186
+ }
18187
+ if (!prev || prev.rows !== rows) {
18188
+ motionRef.current?.cancel();
18189
+ motionRef.current = null;
18190
+ return;
18191
+ }
18192
+ const added = new Set([...visibleKeys].filter((key) => !prev.keys.has(key)));
18193
+ const removed = new Set([...prev.keys].filter((key) => !visibleKeys.has(key)));
18194
+ if (added.size === 0 && removed.size === 0) return;
18195
+ let from = added.size > 0 ? 0 : 1;
18196
+ if (motionRef.current) {
18197
+ const current = motionRef.current;
18198
+ const reversing = current.to === 0 && sameKeys(added, current.keys) || current.to === 1 && sameKeys(removed, current.keys);
18199
+ if (reversing) from = current.openness();
18200
+ current.cancel();
18201
+ motionRef.current = null;
18202
+ }
18203
+ const scrollEl = tableRef.current?.getScrollElement();
18204
+ const keys = added.size > 0 ? added : removed;
18205
+ const canAnimate = scrollEl != null && motionAllowed() && (added.size === 0 || removed.size === 0);
18206
+ const motion = canAnimate ? startRowMotion({
18207
+ scrollEl,
18208
+ rowKeys: renderedRows.map(keyOf),
18209
+ blockKeys: keys,
18210
+ from,
18211
+ to: added.size > 0 ? 1 : 0
18212
+ }) : null;
18213
+ if (motion) {
18214
+ motionRef.current = motion;
18215
+ if (motion.to === 0) {
18216
+ void motion.finished.then((completed) => {
18217
+ if (!completed || motionRef.current !== motion) return;
18218
+ motion.done = true;
18219
+ setClosingKeys(NO_KEYS);
18220
+ });
18221
+ }
18222
+ } else if (closingKeys.size > 0) {
18223
+ setClosingKeys(NO_KEYS);
18224
+ }
18225
+ }, [renderedRows]);
18226
+ react.useEffect(() => () => motionRef.current?.cancel(), []);
18073
18227
  const collectSelectable = (row) => canSelectRow(row) ? [row, ...childrenOf(row).flatMap(collectSelectable)] : [];
18074
18228
  const selectionOf = (keys) => {
18075
18229
  const state2 = /* @__PURE__ */ new Map();
@@ -18165,8 +18319,9 @@ function STreeTable({
18165
18319
  STable2,
18166
18320
  {
18167
18321
  ...rest,
18322
+ ref: tableRef,
18168
18323
  columns: tableColumns,
18169
- rows: visibleRows,
18324
+ rows: renderedRows,
18170
18325
  rowKey,
18171
18326
  resizable,
18172
18327
  selectable,
@@ -18174,14 +18329,113 @@ function STreeTable({
18174
18329
  isRowIndeterminate: selectionState ? (row) => !!selectionState.get(keyOf(row))?.indeterminate : void 0,
18175
18330
  rowClassName: (row) => cn(
18176
18331
  rowClassName?.(row),
18177
- // 위에서 4px 내려오며 나타난다(theme.css --animate-table-row-in). 모션을 줄인 환경에서는 끈다
18178
- enteringKeys.has(keyOf(row)) && "motion-safe:animate-table-row-in"
18332
+ // 접히는 중인 행은 이미 접힌 행이다 — 사라지는 동안 눌리지 않게 한다
18333
+ !visibleKeys.has(keyOf(row)) && "pointer-events-none"
18179
18334
  ) || void 0,
18180
18335
  selected: tableSelected,
18181
18336
  onSelectedChange: handleSelectedChange
18182
18337
  }
18183
18338
  );
18184
18339
  }
18340
+ function flattenRows(nodes, isOpen, childrenOf) {
18341
+ const out = [];
18342
+ const walk = (list) => {
18343
+ list.forEach((row) => {
18344
+ out.push(row);
18345
+ if (isOpen(row)) walk(childrenOf(row));
18346
+ });
18347
+ };
18348
+ walk(nodes);
18349
+ return out;
18350
+ }
18351
+ function sameKeys(a, b) {
18352
+ return a.size === b.size && [...a].every((key) => b.has(key));
18353
+ }
18354
+ function motionAllowed() {
18355
+ if (typeof window === "undefined" || typeof Element.prototype.animate !== "function")
18356
+ return false;
18357
+ return !window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
18358
+ }
18359
+ function startRowMotion({
18360
+ scrollEl,
18361
+ rowKeys,
18362
+ blockKeys,
18363
+ from,
18364
+ to
18365
+ }) {
18366
+ const spec = to === 1 ? EXPAND_MOTION : COLLAPSE_MOTION;
18367
+ const duration = spec.duration * Math.abs(to - from);
18368
+ if (duration < 1) return null;
18369
+ const trs = [...scrollEl.querySelectorAll("tr[data-row-index]")].map((tr) => {
18370
+ const key = rowKeys[Number(tr.dataset.rowIndex)];
18371
+ return {
18372
+ tr,
18373
+ inBlock: key !== void 0 && blockKeys.has(key),
18374
+ height: tr.getBoundingClientRect().height
18375
+ };
18376
+ });
18377
+ const viewBottom = scrollEl.scrollTop + scrollEl.clientHeight;
18378
+ const openAt = (offset) => from + (to - from) * offset;
18379
+ const translate = (shift2, s) => `translateY(${-shift2 * (1 - s)}px)`;
18380
+ const timing = {
18381
+ duration,
18382
+ easing: spec.easing,
18383
+ // 접힘은 남겨 둔 행이 빠질 때까지 끝 위치를 붙잡는다 — 놓으면 그 사이 한 프레임 제자리로 튄다
18384
+ fill: to === 0 ? "forwards" : "none"
18385
+ };
18386
+ const animations = [];
18387
+ let shift = 0;
18388
+ let blockHeight = 0;
18389
+ let y = 0;
18390
+ trs.forEach(({ tr, inBlock, height }, i) => {
18391
+ if (inBlock && !trs[i - 1]?.inBlock) {
18392
+ blockHeight = 0;
18393
+ for (let j = i; j < trs.length && trs[j].inBlock; j++) blockHeight += trs[j].height;
18394
+ shift += blockHeight;
18395
+ y = 0;
18396
+ }
18397
+ if (shift === 0 || tr.offsetTop - shift > viewBottom) return;
18398
+ if (!inBlock) {
18399
+ animations.push(
18400
+ tr.animate(
18401
+ [{ transform: translate(shift, from) }, { transform: translate(shift, to) }],
18402
+ timing
18403
+ )
18404
+ );
18405
+ return;
18406
+ }
18407
+ const rowY = y;
18408
+ const clipAt = (s) => Math.min(height, Math.max(0, blockHeight * (1 - s) - rowY));
18409
+ const bends = [1 - rowY / blockHeight, 1 - (rowY + height) / blockHeight].map((s) => (s - from) / (to - from)).filter((offset) => offset > 0 && offset < 1);
18410
+ const offsets = [0, ...bends, 1].sort((a, b) => a - b);
18411
+ animations.push(
18412
+ tr.animate(
18413
+ offsets.map((offset) => ({
18414
+ offset,
18415
+ transform: translate(shift, openAt(offset)),
18416
+ clipPath: `inset(${clipAt(openAt(offset))}px 0 0 0)`
18417
+ })),
18418
+ timing
18419
+ )
18420
+ );
18421
+ y += height;
18422
+ });
18423
+ if (animations.length === 0) return null;
18424
+ return {
18425
+ keys: blockKeys,
18426
+ to,
18427
+ done: false,
18428
+ finished: Promise.all(animations.map((animation) => animation.finished)).then(
18429
+ () => true,
18430
+ () => false
18431
+ ),
18432
+ openness: () => {
18433
+ const progress = animations[0].effect?.getComputedTiming().progress;
18434
+ return progress == null ? to : openAt(progress);
18435
+ },
18436
+ cancel: () => animations.forEach((animation) => animation.cancel())
18437
+ };
18438
+ }
18185
18439
  var OWN_CONTROL_SELECTOR = 'a, button, input, select, textarea, label, [role="button"], [role="checkbox"], [contenteditable="true"]';
18186
18440
  function isOwnControl(target, cell) {
18187
18441
  if (!(target instanceof Element)) return false;
@@ -19160,13 +19414,21 @@ function SCalendar({
19160
19414
  }
19161
19415
  );
19162
19416
  }
19417
+
19418
+ // src/lib/mode-listbox.ts
19419
+ var MODE_LIST_HEIGHT = 240;
19420
+ var MODE_ITEM_HEIGHT = 28;
19421
+ var modeListItemClass = (selected, disabled) => cn(
19422
+ "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]",
19423
+ disabled && "cursor-not-allowed bg-[var(--cmp-datepicker-mode-item-bg-default)] font-medium text-[color:var(--cmp-datepicker-mode-item-content-disabled)]",
19424
+ !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)]",
19425
+ !disabled && selected && "cursor-pointer bg-[var(--cmp-datepicker-mode-item-bg-default)] font-bold text-[color:var(--cmp-datepicker-mode-item-content-selected)]"
19426
+ );
19163
19427
  var MODE_NOUN = {
19164
19428
  date: "\uB0A0\uC9DC",
19165
19429
  month: "\uC6D4",
19166
19430
  year: "\uC5F0\uB3C4"
19167
19431
  };
19168
- var DATEPICKER_MODE_LIST_HEIGHT = 240;
19169
- var DATEPICKER_MODE_ITEM_HEIGHT = 28;
19170
19432
  var YEAR_LIST_EDGE_BUFFER = 24;
19171
19433
  var YEAR_LIST_EXPAND_SIZE = 80;
19172
19434
  var MONTHS = Array.from({ length: 12 }, (_, i) => i + 1);
@@ -19217,7 +19479,7 @@ function YearWheel({ value, anchorYear, selectable, onCommit }) {
19217
19479
  if (!list) return;
19218
19480
  const index = anchorYearRef.current - rangeStartRef.current;
19219
19481
  syncingScrollRef.current = true;
19220
- list.scrollTop = index * DATEPICKER_MODE_ITEM_HEIGHT;
19482
+ list.scrollTop = index * MODE_ITEM_HEIGHT;
19221
19483
  requestAnimationFrame(() => {
19222
19484
  syncingScrollRef.current = false;
19223
19485
  });
@@ -19226,7 +19488,7 @@ function YearWheel({ value, anchorYear, selectable, onCommit }) {
19226
19488
  const list = listRef.current;
19227
19489
  if (!list) return;
19228
19490
  if (syncingScrollRef.current) return;
19229
- if (list.scrollTop < DATEPICKER_MODE_ITEM_HEIGHT * YEAR_LIST_EDGE_BUFFER) {
19491
+ if (list.scrollTop < MODE_ITEM_HEIGHT * YEAR_LIST_EDGE_BUFFER) {
19230
19492
  const previousHeight = list.scrollHeight;
19231
19493
  setRange((current) => ({
19232
19494
  start: current.start - YEAR_LIST_EXPAND_SIZE,
@@ -19242,7 +19504,7 @@ function YearWheel({ value, anchorYear, selectable, onCommit }) {
19242
19504
  });
19243
19505
  return;
19244
19506
  }
19245
- if (list.scrollHeight - list.scrollTop - list.clientHeight < DATEPICKER_MODE_ITEM_HEIGHT * YEAR_LIST_EDGE_BUFFER) {
19507
+ if (list.scrollHeight - list.scrollTop - list.clientHeight < MODE_ITEM_HEIGHT * YEAR_LIST_EDGE_BUFFER) {
19246
19508
  setRange((current) => ({
19247
19509
  start: current.start,
19248
19510
  end: current.end + YEAR_LIST_EXPAND_SIZE
@@ -19257,8 +19519,8 @@ function YearWheel({ value, anchorYear, selectable, onCommit }) {
19257
19519
  "aria-label": "year",
19258
19520
  className: "h-full overflow-y-auto overscroll-contain [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
19259
19521
  style: {
19260
- paddingTop: `calc((${DATEPICKER_MODE_LIST_HEIGHT}px - ${DATEPICKER_MODE_ITEM_HEIGHT}px) / 2)`,
19261
- paddingBottom: `calc((${DATEPICKER_MODE_LIST_HEIGHT}px - ${DATEPICKER_MODE_ITEM_HEIGHT}px) / 2)`
19522
+ paddingTop: `calc((${MODE_LIST_HEIGHT}px - ${MODE_ITEM_HEIGHT}px) / 2)`,
19523
+ paddingBottom: `calc((${MODE_LIST_HEIGHT}px - ${MODE_ITEM_HEIGHT}px) / 2)`
19262
19524
  },
19263
19525
  onScroll: handleScroll,
19264
19526
  children: years.map((year) => {
@@ -19271,12 +19533,7 @@ function YearWheel({ value, anchorYear, selectable, onCommit }) {
19271
19533
  role: "option",
19272
19534
  "aria-selected": selected,
19273
19535
  disabled,
19274
- className: cn(
19275
- "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]",
19276
- disabled && "cursor-not-allowed bg-[var(--cmp-datepicker-mode-item-bg-default)] font-medium text-[color:var(--cmp-datepicker-mode-item-content-disabled)]",
19277
- !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)]",
19278
- !disabled && selected && "cursor-pointer bg-[var(--cmp-datepicker-mode-item-bg-default)] font-bold text-[color:var(--cmp-datepicker-mode-item-content-selected)]"
19279
- ),
19536
+ className: modeListItemClass(selected, disabled),
19280
19537
  onClick: () => !disabled && onCommit(year),
19281
19538
  children: year
19282
19539
  },
@@ -19332,7 +19589,7 @@ function MonthWheel({ year, value, selectable, onCommit }) {
19332
19589
  if (!list) return;
19333
19590
  list.scrollTop = Math.max(
19334
19591
  0,
19335
- (initialValueRef.current - 1) * DATEPICKER_MODE_ITEM_HEIGHT - (list.clientHeight - DATEPICKER_MODE_ITEM_HEIGHT) / 2
19592
+ (initialValueRef.current - 1) * MODE_ITEM_HEIGHT - (list.clientHeight - MODE_ITEM_HEIGHT) / 2
19336
19593
  );
19337
19594
  }, []);
19338
19595
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-full w-full overflow-hidden bg-[var(--cmp-datepicker-mode-bg)]", children: /* @__PURE__ */ jsxRuntime.jsx(
@@ -19352,12 +19609,7 @@ function MonthWheel({ year, value, selectable, onCommit }) {
19352
19609
  role: "option",
19353
19610
  "aria-selected": selected,
19354
19611
  disabled,
19355
- className: cn(
19356
- "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]",
19357
- disabled && "cursor-not-allowed bg-[var(--cmp-datepicker-mode-item-bg-default)] font-medium text-[color:var(--cmp-datepicker-mode-item-content-disabled)]",
19358
- !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)]",
19359
- !disabled && selected && "cursor-pointer bg-[var(--cmp-datepicker-mode-item-bg-default)] font-bold text-[color:var(--cmp-datepicker-mode-item-content-selected)]"
19360
- ),
19612
+ className: modeListItemClass(selected, disabled),
19361
19613
  onClick: () => !disabled && onCommit(month),
19362
19614
  children: [
19363
19615
  month,
@@ -19436,6 +19688,7 @@ function SDatePicker({
19436
19688
  placeholder,
19437
19689
  selectable,
19438
19690
  disabled = false,
19691
+ showWeekday = false,
19439
19692
  clearable = false,
19440
19693
  width,
19441
19694
  maxWidth,
@@ -19465,6 +19718,7 @@ function SDatePicker({
19465
19718
  const [focused, setFocused] = react.useState(false);
19466
19719
  const [ruleError, setRuleError] = react.useState("");
19467
19720
  const hasValue = value != null && value !== "";
19721
+ const weekday = showWeekday && mode === "date";
19468
19722
  const resolvedPlaceholder = placeholder ?? (mode === "year" ? "YYYY" : mode === "month" ? "YYYY-MM" : "YYYY-MM-DD");
19469
19723
  useFormField({
19470
19724
  name,
@@ -19477,7 +19731,7 @@ function SDatePicker({
19477
19731
  });
19478
19732
  const resolvedError = error || ruleError !== "";
19479
19733
  const resolvedStatus = status ?? (rules && rules.length > 0 ? ruleError !== "" ? "error" : void 0 : void 0);
19480
- const dims = size === "md" ? {
19734
+ const baseDims = size === "md" ? {
19481
19735
  padX: 16,
19482
19736
  gap: 12,
19483
19737
  icon: 20,
@@ -19502,6 +19756,13 @@ function SDatePicker({
19502
19756
  lineHeight: 20,
19503
19757
  radius: 4
19504
19758
  };
19759
+ const weekdayW = weekday ? weekdaySuffixWidth(baseDims.fontSize) : 0;
19760
+ const dims = {
19761
+ ...baseDims,
19762
+ minW: baseDims.minW + weekdayW,
19763
+ fieldMinW: baseDims.fieldMinW + weekdayW,
19764
+ fieldMaxW: weekdayW ? `calc(${fieldWidthValue(baseDims.fieldMaxW)} + ${weekdayW}px)` : baseDims.fieldMaxW
19765
+ };
19505
19766
  const select = (date) => {
19506
19767
  onValueChange?.(date);
19507
19768
  if (rules && rules.length > 0) setRuleError(runRules(date, rules));
@@ -19586,7 +19847,7 @@ function SDatePicker({
19586
19847
  lineHeight: `${dims.lineHeight}px`,
19587
19848
  color: disabled ? "var(--cmp-datepicker-text-disabled)" : hasValue ? "var(--cmp-datepicker-text-default)" : "var(--cmp-field-hint-color)"
19588
19849
  },
19589
- children: hasValue ? value : resolvedPlaceholder
19850
+ children: hasValue ? weekday ? withWeekday(value) : value : resolvedPlaceholder
19590
19851
  }
19591
19852
  ),
19592
19853
  clearable && hasValue && !disabled && /* @__PURE__ */ jsxRuntime.jsx(
@@ -19980,6 +20241,7 @@ function SDateRangePicker({
19980
20241
  selectable,
19981
20242
  maxRange,
19982
20243
  showTimePicker = false,
20244
+ showWeekday = false,
19983
20245
  disabled = false,
19984
20246
  clearable = false,
19985
20247
  width,
@@ -20011,8 +20273,10 @@ function SDateRangePicker({
20011
20273
  const [pendingStart, setPendingStart] = react.useState(null);
20012
20274
  const hasValue = value != null && value[0] !== "" && value[1] !== "";
20013
20275
  const hasDisplay = pendingStart != null || hasValue;
20014
- const startText = pendingStart ?? (hasValue ? value[0] : "");
20015
- const endText = pendingStart ? "" : hasValue ? value[1] : "";
20276
+ const rawStart = pendingStart ?? (hasValue ? value[0] : "");
20277
+ const rawEnd = pendingStart ? "" : hasValue ? value[1] : "";
20278
+ const startText = showWeekday && rawStart ? withWeekday(rawStart) : rawStart;
20279
+ const endText = showWeekday && rawEnd ? withWeekday(rawEnd) : rawEnd;
20016
20280
  useFormField({
20017
20281
  name,
20018
20282
  validate: () => {
@@ -20024,7 +20288,7 @@ function SDateRangePicker({
20024
20288
  });
20025
20289
  const resolvedError = error || ruleError !== "";
20026
20290
  const resolvedStatus = status ?? (rules && rules.length > 0 ? ruleError !== "" ? "error" : void 0 : void 0);
20027
- const dims = size === "md" ? {
20291
+ const baseDims = size === "md" ? {
20028
20292
  padX: 16,
20029
20293
  gap: 12,
20030
20294
  icon: 20,
@@ -20048,6 +20312,13 @@ function SDateRangePicker({
20048
20312
  lineHeight: 20,
20049
20313
  radius: 4
20050
20314
  };
20315
+ const weekdayW = showWeekday ? weekdaySuffixWidth(baseDims.fontSize) * 2 : 0;
20316
+ const dims = {
20317
+ ...baseDims,
20318
+ minW: baseDims.minW + weekdayW,
20319
+ fieldMinW: baseDims.fieldMinW + weekdayW,
20320
+ fieldMaxW: weekdayW ? `calc(${fieldWidthValue(baseDims.fieldMaxW)} + ${weekdayW}px)` : baseDims.fieldMaxW
20321
+ };
20051
20322
  const select = (r3) => {
20052
20323
  setPendingStart(null);
20053
20324
  onValueChange?.(r3);
@@ -26049,7 +26320,7 @@ var isAtResetTarget = (field, value) => {
26049
26320
  var isInlineField = (field) => isInlineDateField(field) || field.type === "custom";
26050
26321
  var isFixedField = (field) => field.fixed === true || field.required === true;
26051
26322
  var isAddableField = (field) => !isFixedField(field);
26052
- var sameKeys = (a, b) => a.length === b.length && a.every((key, index) => key === b[index]);
26323
+ var sameKeys2 = (a, b) => a.length === b.length && a.every((key, index) => key === b[index]);
26053
26324
  var isGroupSatisfied = (group, values) => {
26054
26325
  if (!group.rule) return true;
26055
26326
  const keys = group.fields.map((f) => f.key);
@@ -26232,7 +26503,7 @@ var SChipFilter = /* @__PURE__ */ react.forwardRef(function SChipFilter2({
26232
26503
  const kept = current.filter((key) => known.has(key));
26233
26504
  const additions = fields.filter((field) => isFixedField(field) && !kept.includes(field.key)).map((field) => field.key);
26234
26505
  const next = [...kept, ...additions];
26235
- return sameKeys(current, next) ? current : next;
26506
+ return sameKeys2(current, next) ? current : next;
26236
26507
  });
26237
26508
  }, [fields, values]);
26238
26509
  react.useEffect(() => {
@@ -28425,212 +28696,171 @@ function formatTimeDisplay(value, type) {
28425
28696
  const displayHour = hour % 12 === 0 ? 12 : hour % 12;
28426
28697
  return `${meridiem} ${padTimePart(displayHour)}:${padTimePart(minute)}`;
28427
28698
  }
28428
- var TIME_SELECTOR_WIDTH = 136;
28429
- var MERIDIEM_TIME_SELECTOR_WIDTH = 154;
28430
- function TimeSelector({
28699
+ function scrollSelectedIntoView(list, index, center) {
28700
+ const top = index * MODE_ITEM_HEIGHT;
28701
+ if (center) {
28702
+ list.scrollTop = Math.max(0, top - (list.clientHeight - MODE_ITEM_HEIGHT) / 2);
28703
+ return;
28704
+ }
28705
+ if (top < list.scrollTop) list.scrollTop = top;
28706
+ else if (top + MODE_ITEM_HEIGHT > list.scrollTop + list.clientHeight)
28707
+ list.scrollTop = top + MODE_ITEM_HEIGHT - list.clientHeight;
28708
+ }
28709
+ function lastIndex(arr, pred) {
28710
+ for (let i = arr.length - 1; i >= 0; i--) if (pred(arr[i], i)) return i;
28711
+ return -1;
28712
+ }
28713
+ function ListColumn({ ariaLabel, items }) {
28714
+ const listRef = react.useRef(null);
28715
+ const mountedRef = react.useRef(false);
28716
+ const selectedIndex = items.findIndex((item4) => item4.selected);
28717
+ const tabIndexTarget = selectedIndex >= 0 ? selectedIndex : items.findIndex((item4) => !item4.disabled);
28718
+ react.useLayoutEffect(() => {
28719
+ const list = listRef.current;
28720
+ if (!list || selectedIndex < 0) return;
28721
+ scrollSelectedIntoView(list, selectedIndex, !mountedRef.current);
28722
+ mountedRef.current = true;
28723
+ }, [selectedIndex]);
28724
+ const onKeyDown = (event) => {
28725
+ const buttons = [...listRef.current?.querySelectorAll("button") ?? []];
28726
+ const current = buttons.indexOf(document.activeElement);
28727
+ if (current < 0) return;
28728
+ const enabled = (item4, i) => !item4.disabled && i !== current;
28729
+ let next;
28730
+ if (event.key === "ArrowDown") next = items.findIndex((it, i) => i > current && enabled(it, i));
28731
+ else if (event.key === "ArrowUp")
28732
+ next = lastIndex(items, (it, i) => i < current && enabled(it, i));
28733
+ else if (event.key === "Home") next = items.findIndex((it) => !it.disabled);
28734
+ else if (event.key === "End") next = lastIndex(items, (it) => !it.disabled);
28735
+ else return;
28736
+ event.preventDefault();
28737
+ if (next < 0) return;
28738
+ buttons[next]?.focus({ preventScroll: true });
28739
+ if (listRef.current) scrollSelectedIntoView(listRef.current, next, false);
28740
+ };
28741
+ return /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx(
28742
+ "div",
28743
+ {
28744
+ ref: listRef,
28745
+ role: "listbox",
28746
+ "aria-label": ariaLabel,
28747
+ tabIndex: -1,
28748
+ className: "h-full overflow-y-auto overscroll-contain [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
28749
+ onKeyDown,
28750
+ children: items.map((item4, index) => /* @__PURE__ */ jsxRuntime.jsx(
28751
+ "button",
28752
+ {
28753
+ type: "button",
28754
+ role: "option",
28755
+ "aria-selected": item4.selected,
28756
+ disabled: item4.disabled,
28757
+ tabIndex: index === tabIndexTarget ? 0 : -1,
28758
+ className: cn(modeListItemClass(item4.selected, item4.disabled), "outline-offset-[-2px]"),
28759
+ onClick: () => !item4.disabled && item4.onSelect(),
28760
+ children: item4.label
28761
+ },
28762
+ item4.key
28763
+ ))
28764
+ }
28765
+ ) });
28766
+ }
28767
+ var HOURS_24 = Array.from({ length: 24 }, (_, i) => i);
28768
+ var HOURS_12 = [12, ...Array.from({ length: 11 }, (_, i) => i + 1)];
28769
+ function TimeListbox({
28431
28770
  value,
28432
28771
  showMeridiem,
28433
28772
  minuteStep,
28434
- onChange
28773
+ isDisabled,
28774
+ onChange,
28775
+ labelPrefix,
28776
+ elevated = true
28435
28777
  }) {
28436
- const parsed = react.useMemo(() => parseTimeValue(value), [value]);
28437
- const [hour, setHour] = react.useState(parsed.hour);
28438
- const [minute, setMinute] = react.useState(parsed.minute);
28439
- const [editingPart, setEditingPart] = react.useState(null);
28440
- const [inputDraft, setInputDraft] = react.useState({});
28778
+ const toState = (v) => v ? parseTimeValue(v) : null;
28779
+ const [time, setTime] = react.useState(() => toState(value));
28441
28780
  react.useEffect(() => {
28442
- setHour(parsed.hour);
28443
- setMinute(parsed.minute);
28444
- }, [parsed.hour, parsed.minute]);
28445
- const meridiem = hour >= 12 ? "PM" : "AM";
28446
- const displayHour = showMeridiem ? hour % 12 === 0 ? 12 : hour % 12 : hour;
28447
- const commit = (nextHour, nextMinute) => {
28448
- const h = clampTimePart(nextHour, 0, 23);
28449
- const m = clampTimePart(nextMinute, 0, 59);
28450
- setHour(h);
28451
- setMinute(m);
28452
- onChange(formatTimeValue(h, m));
28453
- };
28454
- const setMeridiem = (next) => {
28455
- if (next === meridiem) return;
28456
- commit(next === "AM" ? hour - 12 : hour + 12, minute);
28457
- };
28458
- const changeMinute = (delta) => {
28459
- const next = minute + delta;
28460
- if (next > 59) {
28461
- commit((hour + 1) % 24, next - 60);
28462
- return;
28463
- }
28464
- if (next < 0) {
28465
- commit((hour + 23) % 24, next + 60);
28466
- return;
28467
- }
28468
- commit(hour, next);
28469
- };
28470
- const changeHour = (delta) => commit((hour + delta + 24) % 24, minute);
28471
- const sanitizeInput = (next) => next.replace(/\D/g, "").slice(0, 2);
28472
- const beginEdit = (part, currentValue) => {
28473
- setEditingPart(part);
28474
- setInputDraft((prev) => ({ ...prev, [part]: String(currentValue).padStart(2, "0") }));
28475
- };
28476
- const commitHourDraft = (draft) => {
28477
- const clamped = clampTimePart(Number(draft), showMeridiem ? 1 : 0, showMeridiem ? 12 : 23);
28478
- const nextHour = showMeridiem ? meridiem === "PM" ? clamped === 12 ? 12 : clamped + 12 : clamped === 12 ? 0 : clamped : clamped;
28479
- commit(nextHour, minute);
28480
- return clamped;
28481
- };
28482
- const commitMinuteDraft = (draft) => {
28483
- const clamped = clampTimePart(Number(draft), 0, 59);
28484
- commit(hour, clamped);
28485
- return clamped;
28486
- };
28487
- const endEdit = (part) => {
28488
- const draft = inputDraft[part];
28489
- if (part === "hour" && draft != null && draft !== "") {
28490
- commitHourDraft(draft);
28491
- }
28492
- if (part === "minute" && draft != null && draft !== "") {
28493
- commitMinuteDraft(draft);
28494
- }
28495
- setEditingPart(null);
28496
- setInputDraft({});
28497
- };
28498
- const inputStyle = {
28499
- width: "var(--cmp-timepicker-selector-time-input-width)",
28500
- height: "var(--cmp-textinput-sm-height)",
28501
- borderRadius: "var(--cmp-textinput-sm-radius)",
28502
- border: "var(--cmp-textinput-borderWidth) solid var(--cmp-textinput-border-default)",
28503
- color: "var(--cmp-textinput-text-default)",
28504
- fontSize: 12,
28505
- lineHeight: "20px"
28781
+ setTime(toState(value));
28782
+ }, [value]);
28783
+ const step = Math.max(1, Math.floor(minuteStep));
28784
+ const minutes = Array.from({ length: Math.ceil(60 / step) }, (_, i) => i * step);
28785
+ const blocked = (h, m) => isDisabled?.(h * 60 + m) ?? false;
28786
+ const hourBlocked = (h) => minutes.every((m) => blocked(h, m));
28787
+ const commit = (hours, preferred, part) => {
28788
+ let next = blocked(preferred.hour, preferred.minute) ? null : preferred;
28789
+ for (const h of hours) {
28790
+ if (next) break;
28791
+ const m = minutes.find((mm) => !blocked(h, mm));
28792
+ if (m !== void 0) next = { hour: h, minute: m };
28793
+ }
28794
+ if (!next) return;
28795
+ setTime(next);
28796
+ onChange(formatTimeValue(next.hour, next.minute), part);
28506
28797
  };
28507
- const renderColumn = (type) => {
28508
- const isHour = type === "hour";
28509
- const val = isHour ? displayHour : minute;
28510
- const displayValue2 = editingPart === type ? inputDraft[type] ?? "" : String(val).padStart(2, "0");
28511
- return /* @__PURE__ */ jsxRuntime.jsxs(
28512
- "div",
28513
- {
28514
- className: "flex flex-col items-center justify-center",
28515
- style: { gap: "var(--cmp-timepicker-selector-time-column-gap)" },
28516
- children: [
28517
- /* @__PURE__ */ jsxRuntime.jsx(
28518
- SGhostButton,
28519
- {
28520
- icon: "chevronUp",
28521
- size: "xs",
28522
- ariaLabel: `${isHour ? "\uC2DC" : "\uBD84"} \uC99D\uAC00`,
28523
- onClick: () => isHour ? changeHour(1) : changeMinute(minuteStep)
28524
- }
28525
- ),
28526
- /* @__PURE__ */ jsxRuntime.jsx(
28527
- "input",
28528
- {
28529
- "aria-label": isHour ? "\uC2DC" : "\uBD84",
28530
- inputMode: "numeric",
28531
- value: displayValue2,
28532
- placeholder: "-",
28533
- onFocus: () => beginEdit(type, val),
28534
- onBlur: () => endEdit(type),
28535
- onChange: (event) => {
28536
- const draft = sanitizeInput(event.target.value);
28537
- setInputDraft((prev) => ({ ...prev, [type]: draft }));
28538
- if (draft === "") return;
28539
- const raw = Number(draft);
28540
- if (isHour) {
28541
- if (draft.length < 2) return;
28542
- const clamped2 = commitHourDraft(draft);
28543
- const displayDraft2 = raw === clamped2 ? draft : String(clamped2);
28544
- setInputDraft((prev) => ({ ...prev, [type]: displayDraft2 }));
28545
- return;
28546
- }
28547
- if (draft.length < 2) return;
28548
- const clamped = commitMinuteDraft(draft);
28549
- const displayDraft = raw === clamped ? draft : String(clamped);
28550
- setInputDraft((prev) => ({ ...prev, [type]: displayDraft }));
28551
- },
28552
- className: "box-border bg-white text-center outline-none",
28553
- style: inputStyle
28554
- }
28555
- ),
28556
- /* @__PURE__ */ jsxRuntime.jsx(
28557
- SGhostButton,
28558
- {
28559
- icon: "chevronDown",
28560
- size: "xs",
28561
- ariaLabel: `${isHour ? "\uC2DC" : "\uBD84"} \uAC10\uC18C`,
28562
- onClick: () => isHour ? changeHour(-1) : changeMinute(-minuteStep)
28563
- }
28564
- )
28565
- ]
28798
+ const hour = time?.hour;
28799
+ const minute = time?.minute ?? 0;
28800
+ const meridiem = hour === void 0 ? void 0 : hour >= 12 ? "PM" : "AM";
28801
+ const prefix = labelPrefix ? `${labelPrefix} ` : "";
28802
+ const meridiemItems = ["AM", "PM"].map((item4) => {
28803
+ const base = item4 === "AM" ? 0 : 12;
28804
+ const range = HOURS_24.slice(base, base + 12);
28805
+ return {
28806
+ key: item4,
28807
+ label: item4 === "AM" ? "\uC624\uC804" : "\uC624\uD6C4",
28808
+ selected: meridiem === item4,
28809
+ disabled: range.every(hourBlocked),
28810
+ onSelect: () => {
28811
+ if (meridiem === item4) return;
28812
+ const h = hour === void 0 ? base : hour % 12 + base;
28813
+ commit(range, { hour: h, minute }, "meridiem");
28566
28814
  }
28567
- );
28568
- };
28815
+ };
28816
+ });
28817
+ const hourItems = showMeridiem ? HOURS_12.map((display) => {
28818
+ const h = display % 12 + (meridiem === "PM" ? 12 : 0);
28819
+ return {
28820
+ key: String(display),
28821
+ label: `${padTimePart(display)}\uC2DC`,
28822
+ selected: hour === h,
28823
+ disabled: hourBlocked(h),
28824
+ onSelect: () => commit([h], { hour: h, minute }, "hour")
28825
+ };
28826
+ }) : HOURS_24.map((h) => ({
28827
+ key: String(h),
28828
+ label: `${padTimePart(h)}\uC2DC`,
28829
+ selected: hour === h,
28830
+ disabled: hourBlocked(h),
28831
+ onSelect: () => commit([h], { hour: h, minute }, "hour")
28832
+ }));
28833
+ const minuteHour = hour ?? (meridiem === "PM" ? 12 : 0);
28834
+ const minuteItems = minutes.map((m) => ({
28835
+ key: String(m),
28836
+ label: `${padTimePart(m)}\uBD84`,
28837
+ selected: time != null && minute === m,
28838
+ disabled: blocked(minuteHour, m),
28839
+ onSelect: () => commit([minuteHour], { hour: minuteHour, minute: m }, "minute")
28840
+ }));
28841
+ const divider = /* @__PURE__ */ jsxRuntime.jsx(
28842
+ "div",
28843
+ {
28844
+ "aria-hidden": true,
28845
+ className: "w-[var(--border-width-1)] self-stretch bg-[var(--sys-color-divider-default)]"
28846
+ }
28847
+ );
28569
28848
  return /* @__PURE__ */ jsxRuntime.jsxs(
28570
28849
  "div",
28571
28850
  {
28572
- className: "flex flex-col items-stretch justify-center overflow-hidden",
28573
- style: {
28574
- background: "var(--cmp-timepicker-selector-bg)",
28575
- borderRadius: "var(--cmp-timepicker-selector-radius)",
28576
- boxShadow: "4px 8px 16px -4px rgba(34,34,34,0.1), 0 0 24px -6px rgba(34,34,34,0.12)"
28577
- },
28851
+ className: cn(
28852
+ "inline-flex gap-[var(--cmp-datepicker-mode-yearMonth-gap)] overflow-hidden bg-[var(--cmp-datepicker-mode-bg)]",
28853
+ elevated && "rounded-[var(--cmp-datepicker-mode-radius)] shadow-normal-sm"
28854
+ ),
28855
+ style: { height: MODE_LIST_HEIGHT },
28578
28856
  children: [
28579
- showMeridiem && /* @__PURE__ */ jsxRuntime.jsx(
28580
- "div",
28581
- {
28582
- className: "flex items-center justify-center",
28583
- style: {
28584
- padding: "var(--cmp-timepicker-selector-section-paddingY) var(--cmp-timepicker-selector-section-paddingX)"
28585
- },
28586
- children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "isolate flex", children: ["AM", "PM"].map((item4, index) => {
28587
- const selected = item4 === meridiem;
28588
- return /* @__PURE__ */ jsxRuntime.jsx(
28589
- "button",
28590
- {
28591
- type: "button",
28592
- onClick: () => setMeridiem(item4),
28593
- className: cn(
28594
- "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]",
28595
- index === 0 ? "rounded-l-[var(--cmp-select-sm-radius)]" : "rounded-r-[var(--cmp-select-sm-radius)]",
28596
- index === 0 ? "-mr-px" : "",
28597
- 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)]"
28598
- ),
28599
- children: item4 === "AM" ? "\uC624\uC804" : "\uC624\uD6C4"
28600
- },
28601
- item4
28602
- );
28603
- }) })
28604
- }
28605
- ),
28606
- showMeridiem && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-px w-full bg-[var(--sys-color-divider-default)]" }),
28607
- /* @__PURE__ */ jsxRuntime.jsxs(
28608
- "div",
28609
- {
28610
- className: "flex items-center justify-center",
28611
- style: {
28612
- gap: "var(--cmp-timepicker-selector-time-gap)",
28613
- padding: "var(--cmp-timepicker-selector-section-paddingY) var(--cmp-timepicker-selector-section-paddingX)"
28614
- },
28615
- children: [
28616
- renderColumn("hour"),
28617
- /* @__PURE__ */ jsxRuntime.jsx(
28618
- "span",
28619
- {
28620
- className: "text-center",
28621
- style: {
28622
- width: "var(--cmp-timepicker-selector-colon-size)",
28623
- color: "var(--cmp-field-hint-color)",
28624
- fontSize: "var(--cmp-timepicker-selector-colon-size)",
28625
- lineHeight: "var(--cmp-timepicker-selector-colon-size)"
28626
- },
28627
- children: ":"
28628
- }
28629
- ),
28630
- renderColumn("minute")
28631
- ]
28632
- }
28633
- )
28857
+ showMeridiem && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
28858
+ /* @__PURE__ */ jsxRuntime.jsx(ListColumn, { ariaLabel: `${prefix}\uC624\uC804/\uC624\uD6C4`, items: meridiemItems }),
28859
+ divider
28860
+ ] }),
28861
+ /* @__PURE__ */ jsxRuntime.jsx(ListColumn, { ariaLabel: `${prefix}\uC2DC`, items: hourItems }),
28862
+ divider,
28863
+ /* @__PURE__ */ jsxRuntime.jsx(ListColumn, { ariaLabel: `${prefix}\uBD84`, items: minuteItems })
28634
28864
  ]
28635
28865
  }
28636
28866
  );
@@ -28668,11 +28898,9 @@ function STimePicker({
28668
28898
  style
28669
28899
  }) {
28670
28900
  const portalContainer = usePortalContainer();
28671
- const triggerRef = react.useRef(null);
28672
28901
  const [open, setOpen] = react.useState(false);
28673
28902
  const triggerId = useFieldControlId();
28674
28903
  const triggerTextId = `${triggerId}-text`;
28675
- const [popoverAlignOffset, setPopoverAlignOffset] = react.useState(0);
28676
28904
  const [focused, setFocused] = react.useState(false);
28677
28905
  const [ruleError, setRuleError] = react.useState("");
28678
28906
  const dims = TIMEPICKER_SIZE_CONFIG[size] ?? TIMEPICKER_SIZE_CONFIG.sm;
@@ -28693,11 +28921,6 @@ function STimePicker({
28693
28921
  const resolvedStatus = status ?? (rules && rules.length > 0 ? ruleError !== "" ? "error" : void 0 : void 0);
28694
28922
  const commitOpen = (next) => {
28695
28923
  if (disabled) return;
28696
- if (next) {
28697
- const triggerWidth = triggerRef.current?.getBoundingClientRect().width ?? 0;
28698
- const selectorWidth = effectiveShowMeridiem ? MERIDIEM_TIME_SELECTOR_WIDTH : TIME_SELECTOR_WIDTH;
28699
- setPopoverAlignOffset(Math.max(0, (triggerWidth - selectorWidth) / 2));
28700
- }
28701
28924
  setOpen(next);
28702
28925
  onOpenChange?.(next);
28703
28926
  if (!next && rules && rules.length > 0) setRuleError(runRules(value, rules));
@@ -28736,7 +28959,6 @@ function STimePicker({
28736
28959
  /* @__PURE__ */ jsxRuntime.jsx(RPopover13__namespace.Trigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsxs(
28737
28960
  "div",
28738
28961
  {
28739
- ref: triggerRef,
28740
28962
  id: triggerId,
28741
28963
  role: "button",
28742
28964
  "aria-disabled": disabled,
@@ -28800,20 +29022,22 @@ function STimePicker({
28800
29022
  RPopover13__namespace.Content,
28801
29023
  {
28802
29024
  side: "bottom",
28803
- align: "start",
29025
+ align: "center",
28804
29026
  sideOffset: 4,
28805
- alignOffset: popoverAlignOffset,
28806
29027
  className: cn(
28807
29028
  "z-50 max-w-[var(--radix-popover-content-available-width)]",
28808
29029
  FLOATING_SLIDE_ANIM
28809
29030
  ),
28810
29031
  children: /* @__PURE__ */ jsxRuntime.jsx(
28811
- TimeSelector,
29032
+ TimeListbox,
28812
29033
  {
28813
29034
  value: normalized,
28814
29035
  showMeridiem: effectiveShowMeridiem,
28815
29036
  minuteStep,
28816
- onChange: commitValue
29037
+ onChange: (time, part) => {
29038
+ commitValue(time);
29039
+ if (part === "minute") commitOpen(false);
29040
+ }
28817
29041
  }
28818
29042
  )
28819
29043
  }
@@ -28843,7 +29067,6 @@ function formatTimeRangeDisplay(value, type) {
28843
29067
  if (value == null || value[0] === "" || value[1] === "") return "";
28844
29068
  return `${formatTimeDisplay(value[0], type)} ~ ${formatTimeDisplay(value[1], type)}`;
28845
29069
  }
28846
- var TIME_RANGE_SELECTOR_WIDTH = 345;
28847
29070
  var MIDDAY_FIELD_MIN_WIDTH = {
28848
29071
  sm: 178,
28849
29072
  md: 216
@@ -28852,6 +29075,10 @@ var MIDDAY_FIELD_MAX_WIDTH = {
28852
29075
  sm: "lg",
28853
29076
  md: "lg"
28854
29077
  };
29078
+ var toMinutes = (time) => {
29079
+ const { hour, minute } = parseTimeValue(time);
29080
+ return hour * 60 + minute;
29081
+ };
28855
29082
  function RangeTimeSelector({
28856
29083
  value,
28857
29084
  showMeridiem,
@@ -28859,233 +29086,32 @@ function RangeTimeSelector({
28859
29086
  rangeOrder,
28860
29087
  onChange
28861
29088
  }) {
28862
- const range = react.useMemo(() => value ?? ["00:00", "23:59"], [value]);
28863
- const [editingKey, setEditingKey] = react.useState(null);
28864
- const [inputDraft, setInputDraft] = react.useState({});
28865
- const autoAdjustedRef = react.useRef(null);
28866
- const getParts = (side) => parseTimeValue(side === "start" ? range[0] : range[1]);
28867
- const getMeridiem = (side) => getParts(side).hour >= 12 ? "PM" : "AM";
28868
- const getDisplayHour = (side) => {
28869
- const { hour } = getParts(side);
28870
- if (!showMeridiem) return hour;
28871
- const displayHour = hour % 12;
28872
- return displayHour === 0 ? 12 : displayHour;
28873
- };
28874
- const commit = (side, hour, minute, direction = "unknown") => {
28875
- const next = formatTimeValue(hour, minute);
28876
- onChange(
28877
- normalizeNextRange(side === "start" ? [next, range[1]] : [range[0], next], side, direction)
28878
- );
28879
- };
28880
- const getTimeMinutes2 = (time) => {
28881
- const { hour, minute } = parseTimeValue(time);
28882
- return hour * 60 + minute;
28883
- };
28884
- const normalizeNextRange = (nextRange, changedSide, direction) => {
28885
- if (rangeOrder === "allow-cross-day") return nextRange;
28886
- const [start, end] = nextRange;
28887
- const [currentStart, currentEnd] = range;
28888
- if (getTimeMinutes2(start) <= getTimeMinutes2(end)) {
28889
- const shouldKeepSynced = autoAdjustedRef.current?.side === changedSide && currentStart === currentEnd && start !== end && (direction === "unknown" || autoAdjustedRef.current.direction === direction);
28890
- if (shouldKeepSynced) {
28891
- return changedSide === "start" ? [start, start] : [end, end];
28892
- }
28893
- autoAdjustedRef.current = null;
28894
- return nextRange;
28895
- }
28896
- autoAdjustedRef.current = {
28897
- side: changedSide,
28898
- direction: changedSide === "start" ? "increase" : "decrease"
28899
- };
28900
- return changedSide === "start" ? [start, start] : [end, end];
28901
- };
28902
- const changeHour = (side, delta) => {
28903
- const { hour, minute } = getParts(side);
28904
- commit(side, (hour + delta + 24) % 24, minute, delta > 0 ? "increase" : "decrease");
28905
- };
28906
- const changeMinute = (side, delta) => {
28907
- const { hour, minute } = getParts(side);
28908
- const next = minute + delta;
28909
- if (next > 59) {
28910
- commit(side, (hour + 1) % 24, next - 60, delta > 0 ? "increase" : "decrease");
28911
- return;
28912
- }
28913
- if (next < 0) {
28914
- commit(side, (hour + 23) % 24, next + 60, delta > 0 ? "increase" : "decrease");
29089
+ const range = value ?? ["00:00", "23:59"];
29090
+ const strict = rangeOrder !== "allow-cross-day";
29091
+ const change = (side, time) => {
29092
+ if (side === "end") {
29093
+ onChange([range[0], time]);
28915
29094
  return;
28916
29095
  }
28917
- commit(side, hour, next, delta > 0 ? "increase" : "decrease");
28918
- };
28919
- const setMeridiem = (side, next) => {
28920
- const current = getMeridiem(side);
28921
- if (current === next) return;
28922
- const { hour, minute } = getParts(side);
28923
- commit(side, next === "AM" ? hour - 12 : hour + 12, minute);
28924
- };
28925
- const getEditingKey = (side, part) => `${side}-${part}`;
28926
- const sanitizeInput = (next) => next.replace(/\D/g, "").slice(0, 2);
28927
- const beginEdit = (side, part, currentValue) => {
28928
- const key = getEditingKey(side, part);
28929
- setEditingKey(key);
28930
- setInputDraft((prev) => ({ ...prev, [key]: String(currentValue).padStart(2, "0") }));
28931
- };
28932
- const commitHourDraft = (side, draft) => {
28933
- const clamped = clampTimePart(Number(draft), showMeridiem ? 1 : 0, showMeridiem ? 12 : 23);
28934
- const parts = getParts(side);
28935
- const meridiem = getMeridiem(side);
28936
- const nextHour = showMeridiem ? meridiem === "PM" ? clamped === 12 ? 12 : clamped + 12 : clamped === 12 ? 0 : clamped : clamped;
28937
- commit(side, nextHour, parts.minute);
28938
- return clamped;
28939
- };
28940
- const commitMinuteDraft = (side, draft) => {
28941
- const { hour } = getParts(side);
28942
- const clamped = clampTimePart(Number(draft), 0, 59);
28943
- commit(side, hour, clamped);
28944
- return clamped;
28945
- };
28946
- const endEdit = (side, part) => {
28947
- const key = getEditingKey(side, part);
28948
- const draft = inputDraft[key];
28949
- if (part === "hour" && draft != null && draft !== "") {
28950
- commitHourDraft(side, draft);
28951
- }
28952
- if (part === "minute" && draft != null && draft !== "") {
28953
- commitMinuteDraft(side, draft);
28954
- }
28955
- setEditingKey(null);
28956
- setInputDraft({});
28957
- };
28958
- const inputStyle = {
28959
- width: "var(--cmp-timepicker-selector-time-input-width)",
28960
- height: "var(--cmp-textinput-sm-height)",
28961
- borderRadius: "var(--cmp-textinput-sm-radius)",
28962
- border: "var(--cmp-textinput-borderWidth) solid var(--cmp-textinput-border-default)",
28963
- color: "var(--cmp-textinput-text-default)",
28964
- fontSize: 12,
28965
- lineHeight: "20px"
28966
- };
28967
- const renderColumn = (side, kind) => {
28968
- const isHour = kind === "hour";
28969
- const { minute } = getParts(side);
28970
- const val = isHour ? getDisplayHour(side) : minute;
28971
- const currentEditingKey = getEditingKey(side, kind);
28972
- const displayValue2 = editingKey === currentEditingKey ? inputDraft[currentEditingKey] ?? "" : String(val).padStart(2, "0");
28973
- return /* @__PURE__ */ jsxRuntime.jsxs(
28974
- "div",
28975
- {
28976
- className: "flex flex-col items-center justify-center",
28977
- style: { gap: "var(--cmp-timepicker-selector-time-column-gap)" },
28978
- children: [
28979
- /* @__PURE__ */ jsxRuntime.jsx(
28980
- SGhostButton,
28981
- {
28982
- icon: "chevronUp",
28983
- size: "xs",
28984
- ariaLabel: `${side === "start" ? "\uC2DC\uC791" : "\uC885\uB8CC"} ${isHour ? "\uC2DC" : "\uBD84"} \uC99D\uAC00`,
28985
- onClick: () => isHour ? changeHour(side, 1) : changeMinute(side, minuteStep)
28986
- }
28987
- ),
28988
- /* @__PURE__ */ jsxRuntime.jsx(
28989
- "input",
28990
- {
28991
- "aria-label": `${side === "start" ? "\uC2DC\uC791" : "\uC885\uB8CC"} ${isHour ? "\uC2DC" : "\uBD84"}`,
28992
- inputMode: "numeric",
28993
- value: displayValue2,
28994
- placeholder: "-",
28995
- onFocus: () => beginEdit(side, kind, val),
28996
- onBlur: () => endEdit(side, kind),
28997
- onChange: (event) => {
28998
- const draft = sanitizeInput(event.target.value);
28999
- setInputDraft((prev) => ({ ...prev, [currentEditingKey]: draft }));
29000
- if (draft === "") return;
29001
- const raw = Number(draft);
29002
- if (isHour) {
29003
- if (draft.length < 2) return;
29004
- const clamped2 = commitHourDraft(side, draft);
29005
- const displayDraft2 = raw === clamped2 ? draft : String(clamped2);
29006
- setInputDraft((prev) => ({ ...prev, [currentEditingKey]: displayDraft2 }));
29007
- return;
29008
- }
29009
- if (draft.length < 2) return;
29010
- const clamped = commitMinuteDraft(side, draft);
29011
- const displayDraft = raw === clamped ? draft : String(clamped);
29012
- setInputDraft((prev) => ({ ...prev, [currentEditingKey]: displayDraft }));
29013
- },
29014
- className: "box-border bg-white text-center outline-none",
29015
- style: inputStyle
29016
- }
29017
- ),
29018
- /* @__PURE__ */ jsxRuntime.jsx(
29019
- SGhostButton,
29020
- {
29021
- icon: "chevronDown",
29022
- size: "xs",
29023
- ariaLabel: `${side === "start" ? "\uC2DC\uC791" : "\uC885\uB8CC"} ${isHour ? "\uC2DC" : "\uBD84"} \uAC10\uC18C`,
29024
- onClick: () => isHour ? changeHour(side, -1) : changeMinute(side, -minuteStep)
29025
- }
29026
- )
29027
- ]
29028
- }
29029
- );
29096
+ onChange([time, strict && toMinutes(time) > toMinutes(range[1]) ? time : range[1]]);
29030
29097
  };
29031
29098
  const renderPanel = (side, title) => /* @__PURE__ */ jsxRuntime.jsxs(
29032
29099
  "div",
29033
29100
  {
29034
- className: "flex flex-col items-center justify-center",
29035
- style: { gap: "var(--cmp-timepicker-selector-range-section-gap)" },
29101
+ className: "flex flex-col items-center",
29102
+ style: { gap: "var(--cmp-timepicker-selector-range-heading-gap)" },
29036
29103
  children: [
29037
- /* @__PURE__ */ jsxRuntime.jsxs(
29038
- "div",
29039
- {
29040
- className: "flex flex-col items-center",
29041
- style: { gap: "var(--cmp-timepicker-selector-range-heading-gap)" },
29042
- children: [
29043
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "whitespace-nowrap text-center text-[12px] font-bold leading-[20px] text-[var(--cmp-timepicker-selector-range-heading-color)]", children: title }),
29044
- showMeridiem && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "isolate flex", children: ["AM", "PM"].map((item4, index) => {
29045
- const selected = item4 === getMeridiem(side);
29046
- return /* @__PURE__ */ jsxRuntime.jsx(
29047
- "button",
29048
- {
29049
- type: "button",
29050
- onClick: () => setMeridiem(side, item4),
29051
- className: cn(
29052
- "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]",
29053
- index === 0 ? "rounded-l-[var(--cmp-select-sm-radius)] -mr-px" : "rounded-r-[var(--cmp-select-sm-radius)]",
29054
- 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)]"
29055
- ),
29056
- children: item4 === "AM" ? "\uC624\uC804" : "\uC624\uD6C4"
29057
- },
29058
- item4
29059
- );
29060
- }) })
29061
- ]
29062
- }
29063
- ),
29064
- /* @__PURE__ */ jsxRuntime.jsxs(
29065
- "div",
29104
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "whitespace-nowrap text-center text-[12px] font-bold leading-[20px] text-[var(--cmp-timepicker-selector-range-heading-color)]", children: title }),
29105
+ /* @__PURE__ */ jsxRuntime.jsx(
29106
+ TimeListbox,
29066
29107
  {
29067
- className: "flex items-center justify-center",
29068
- style: {
29069
- gap: "var(--cmp-timepicker-selector-time-gap)",
29070
- padding: "0 var(--cmp-timepicker-selector-section-paddingX)"
29071
- },
29072
- children: [
29073
- renderColumn(side, "hour"),
29074
- /* @__PURE__ */ jsxRuntime.jsx(
29075
- "span",
29076
- {
29077
- className: "text-center",
29078
- style: {
29079
- width: "var(--cmp-timepicker-selector-colon-size)",
29080
- color: "var(--cmp-field-hint-color)",
29081
- fontSize: "var(--cmp-timepicker-selector-colon-size)",
29082
- lineHeight: "var(--cmp-timepicker-selector-colon-size)"
29083
- },
29084
- children: ":"
29085
- }
29086
- ),
29087
- renderColumn(side, "minute")
29088
- ]
29108
+ value: side === "start" ? range[0] : range[1],
29109
+ showMeridiem,
29110
+ minuteStep,
29111
+ labelPrefix: side === "start" ? "\uC2DC\uC791" : "\uC885\uB8CC",
29112
+ elevated: false,
29113
+ isDisabled: strict && side === "end" ? (minutes) => minutes < toMinutes(range[0]) : void 0,
29114
+ onChange: (time) => change(side, time)
29089
29115
  }
29090
29116
  )
29091
29117
  ]
@@ -29144,11 +29170,9 @@ function STimeRangePicker({
29144
29170
  style
29145
29171
  }) {
29146
29172
  const portalContainer = usePortalContainer();
29147
- const triggerRef = react.useRef(null);
29148
29173
  const [open, setOpen] = react.useState(false);
29149
29174
  const triggerId = useFieldControlId();
29150
29175
  const triggerTextId = `${triggerId}-text`;
29151
- const [popoverAlignOffset, setPopoverAlignOffset] = react.useState(0);
29152
29176
  const [focused, setFocused] = react.useState(false);
29153
29177
  const [ruleError, setRuleError] = react.useState("");
29154
29178
  const dims = TIMEPICKER_SIZE_CONFIG[size] ?? TIMEPICKER_SIZE_CONFIG.sm;
@@ -29172,10 +29196,6 @@ function STimeRangePicker({
29172
29196
  const resolvedStatus = status ?? (rules && rules.length > 0 ? ruleError !== "" ? "error" : void 0 : void 0);
29173
29197
  const commitOpen = (next) => {
29174
29198
  if (disabled) return;
29175
- if (next) {
29176
- const triggerWidth = triggerRef.current?.getBoundingClientRect().width ?? 0;
29177
- setPopoverAlignOffset(Math.max(0, (triggerWidth - TIME_RANGE_SELECTOR_WIDTH) / 2));
29178
- }
29179
29199
  setOpen(next);
29180
29200
  onOpenChange?.(next);
29181
29201
  if (!next && rules && rules.length > 0) setRuleError(runRules(value, rules));
@@ -29214,7 +29234,6 @@ function STimeRangePicker({
29214
29234
  /* @__PURE__ */ jsxRuntime.jsx(RPopover13__namespace.Trigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsxs(
29215
29235
  "div",
29216
29236
  {
29217
- ref: triggerRef,
29218
29237
  id: triggerId,
29219
29238
  role: "button",
29220
29239
  "aria-disabled": disabled,
@@ -29278,9 +29297,8 @@ function STimeRangePicker({
29278
29297
  RPopover13__namespace.Content,
29279
29298
  {
29280
29299
  side: "bottom",
29281
- align: "start",
29300
+ align: "center",
29282
29301
  sideOffset: 4,
29283
- alignOffset: popoverAlignOffset,
29284
29302
  className: cn(
29285
29303
  "z-50 max-w-[var(--radix-popover-content-available-width)]",
29286
29304
  FLOATING_SLIDE_ANIM