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

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
  {
@@ -14644,12 +14717,25 @@ function monthCells(year, month) {
14644
14717
  return cells;
14645
14718
  }
14646
14719
  var WEEK_LABELS = ["\uC77C", "\uC6D4", "\uD654", "\uC218", "\uBAA9", "\uAE08", "\uD1A0"];
14720
+ var WEEKDAY_SUFFIX_EM = 0.25 + 3 * 1;
14721
+ var weekdaySuffixWidth = (fontSize) => Math.ceil(fontSize * WEEKDAY_SUFFIX_EM);
14722
+ function withWeekday(value) {
14723
+ const [date = "", ...rest] = value.split(" ");
14724
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
14725
+ if (!match) return value;
14726
+ const day = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3])).getDay();
14727
+ if (Number.isNaN(day)) return value;
14728
+ return [date, `${WEEK_LABELS[day]}\uC694\uC77C`, ...rest].join(" ");
14729
+ }
14647
14730
 
14648
14731
  // src/components/SCalendarBoard/calendar-board-layout.ts
14649
14732
  function eventEndDate(event) {
14650
14733
  const end = event.endDate;
14651
14734
  return end != null && end > event.date ? end : event.date;
14652
14735
  }
14736
+ function compareDate(a, b) {
14737
+ return a < b ? -1 : a > b ? 1 : 0;
14738
+ }
14653
14739
  function layoutCalendarBoard({
14654
14740
  cells,
14655
14741
  events,
@@ -14695,7 +14781,7 @@ function layoutCalendarBoard({
14695
14781
  flush(to);
14696
14782
  });
14697
14783
  runs.sort(
14698
- (a, b) => a.startIndex - b.startIndex || b.endIndex - b.startIndex - (a.endIndex - a.startIndex) || a.order - b.order
14784
+ (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
14785
  );
14700
14786
  const weekLanes = Array.from({ length: weekCount }, () => []);
14701
14787
  for (const run of runs) {
@@ -17968,7 +18054,8 @@ var STableBar = /* @__PURE__ */ forwardRef(function STableBar2({ title, total, s
17968
18054
  );
17969
18055
  });
17970
18056
  var NO_KEYS = /* @__PURE__ */ new Set();
17971
- var ROW_ENTER_MS = 200;
18057
+ var EXPAND_MOTION = { duration: 220, easing: "ease-out" };
18058
+ var COLLAPSE_MOTION = { duration: 140, easing: "ease-in" };
17972
18059
  function STreeTable({
17973
18060
  rows = [],
17974
18061
  columns = [],
@@ -18015,32 +18102,88 @@ function STreeTable({
18015
18102
  if (expandedKeys === void 0) setInnerExpandedKeys(next);
18016
18103
  onExpandedChange?.(next, row);
18017
18104
  };
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);
18105
+ const visibleRows = useMemo(
18106
+ () => flattenRows(rows, (row) => expandedSet.has(keyOf(row)), childrenOf),
18107
+ // eslint-disable-next-line react-hooks/exhaustive-deps
18108
+ [rows, expandedSet, rowKey, childrenKey]
18109
+ );
18110
+ const tableRef = useRef(null);
18111
+ const [closingKeys, setClosingKeys] = useState(NO_KEYS);
18112
+ const [prevExpand, setPrevExpand] = useState({ rows, expandedSet });
18113
+ if (prevExpand.rows !== rows || prevExpand.expandedSet !== expandedSet) {
18114
+ setPrevExpand({ rows, expandedSet });
18115
+ let nextClosing = NO_KEYS;
18116
+ if (prevExpand.rows === rows && motionAllowed()) {
18117
+ const collapsed = [...prevExpand.expandedSet].filter((key) => !expandedSet.has(key));
18118
+ const expanded = [...expandedSet].filter((key) => !prevExpand.expandedSet.has(key));
18119
+ if (collapsed.length > 0 && expanded.length === 0) nextClosing = new Set(collapsed);
18120
+ }
18121
+ if (nextClosing !== closingKeys) setClosingKeys(nextClosing);
18122
+ }
18123
+ const renderedRows = useMemo(
18124
+ () => closingKeys.size === 0 ? visibleRows : flattenRows(
18125
+ rows,
18126
+ (row) => expandedSet.has(keyOf(row)) || closingKeys.has(keyOf(row)),
18127
+ childrenOf
18128
+ ),
18129
+ // eslint-disable-next-line react-hooks/exhaustive-deps
18130
+ [visibleRows, closingKeys]
18131
+ );
18132
+ const visibleKeys = useMemo(
18133
+ () => new Set(visibleRows.map(keyOf)),
18134
+ // eslint-disable-next-line react-hooks/exhaustive-deps
18135
+ [visibleRows]
18136
+ );
18137
+ const motionRef = useRef(null);
18030
18138
  const prevVisibleRef = useRef(null);
18031
18139
  useIsomorphicLayoutEffect(() => {
18032
- const keys = new Set(visibleRows.map(keyOf));
18033
18140
  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]);
18141
+ prevVisibleRef.current = { rows, keys: visibleKeys };
18142
+ const running = motionRef.current;
18143
+ if (running?.done) {
18144
+ running.cancel();
18145
+ motionRef.current = null;
18146
+ }
18147
+ if (!prev || prev.rows !== rows) {
18148
+ motionRef.current?.cancel();
18149
+ motionRef.current = null;
18150
+ return;
18151
+ }
18152
+ const added = new Set([...visibleKeys].filter((key) => !prev.keys.has(key)));
18153
+ const removed = new Set([...prev.keys].filter((key) => !visibleKeys.has(key)));
18154
+ if (added.size === 0 && removed.size === 0) return;
18155
+ let from = added.size > 0 ? 0 : 1;
18156
+ if (motionRef.current) {
18157
+ const current = motionRef.current;
18158
+ const reversing = current.to === 0 && sameKeys(added, current.keys) || current.to === 1 && sameKeys(removed, current.keys);
18159
+ if (reversing) from = current.openness();
18160
+ current.cancel();
18161
+ motionRef.current = null;
18162
+ }
18163
+ const scrollEl = tableRef.current?.getScrollElement();
18164
+ const keys = added.size > 0 ? added : removed;
18165
+ const canAnimate = scrollEl != null && motionAllowed() && (added.size === 0 || removed.size === 0);
18166
+ const motion = canAnimate ? startRowMotion({
18167
+ scrollEl,
18168
+ rowKeys: renderedRows.map(keyOf),
18169
+ blockKeys: keys,
18170
+ from,
18171
+ to: added.size > 0 ? 1 : 0
18172
+ }) : null;
18173
+ if (motion) {
18174
+ motionRef.current = motion;
18175
+ if (motion.to === 0) {
18176
+ void motion.finished.then((completed) => {
18177
+ if (!completed || motionRef.current !== motion) return;
18178
+ motion.done = true;
18179
+ setClosingKeys(NO_KEYS);
18180
+ });
18181
+ }
18182
+ } else if (closingKeys.size > 0) {
18183
+ setClosingKeys(NO_KEYS);
18184
+ }
18185
+ }, [renderedRows]);
18186
+ useEffect(() => () => motionRef.current?.cancel(), []);
18044
18187
  const collectSelectable = (row) => canSelectRow(row) ? [row, ...childrenOf(row).flatMap(collectSelectable)] : [];
18045
18188
  const selectionOf = (keys) => {
18046
18189
  const state2 = /* @__PURE__ */ new Map();
@@ -18136,8 +18279,9 @@ function STreeTable({
18136
18279
  STable2,
18137
18280
  {
18138
18281
  ...rest,
18282
+ ref: tableRef,
18139
18283
  columns: tableColumns,
18140
- rows: visibleRows,
18284
+ rows: renderedRows,
18141
18285
  rowKey,
18142
18286
  resizable,
18143
18287
  selectable,
@@ -18145,14 +18289,113 @@ function STreeTable({
18145
18289
  isRowIndeterminate: selectionState ? (row) => !!selectionState.get(keyOf(row))?.indeterminate : void 0,
18146
18290
  rowClassName: (row) => cn(
18147
18291
  rowClassName?.(row),
18148
- // 위에서 4px 내려오며 나타난다(theme.css --animate-table-row-in). 모션을 줄인 환경에서는 끈다
18149
- enteringKeys.has(keyOf(row)) && "motion-safe:animate-table-row-in"
18292
+ // 접히는 중인 행은 이미 접힌 행이다 — 사라지는 동안 눌리지 않게 한다
18293
+ !visibleKeys.has(keyOf(row)) && "pointer-events-none"
18150
18294
  ) || void 0,
18151
18295
  selected: tableSelected,
18152
18296
  onSelectedChange: handleSelectedChange
18153
18297
  }
18154
18298
  );
18155
18299
  }
18300
+ function flattenRows(nodes, isOpen, childrenOf) {
18301
+ const out = [];
18302
+ const walk = (list) => {
18303
+ list.forEach((row) => {
18304
+ out.push(row);
18305
+ if (isOpen(row)) walk(childrenOf(row));
18306
+ });
18307
+ };
18308
+ walk(nodes);
18309
+ return out;
18310
+ }
18311
+ function sameKeys(a, b) {
18312
+ return a.size === b.size && [...a].every((key) => b.has(key));
18313
+ }
18314
+ function motionAllowed() {
18315
+ if (typeof window === "undefined" || typeof Element.prototype.animate !== "function")
18316
+ return false;
18317
+ return !window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
18318
+ }
18319
+ function startRowMotion({
18320
+ scrollEl,
18321
+ rowKeys,
18322
+ blockKeys,
18323
+ from,
18324
+ to
18325
+ }) {
18326
+ const spec = to === 1 ? EXPAND_MOTION : COLLAPSE_MOTION;
18327
+ const duration = spec.duration * Math.abs(to - from);
18328
+ if (duration < 1) return null;
18329
+ const trs = [...scrollEl.querySelectorAll("tr[data-row-index]")].map((tr) => {
18330
+ const key = rowKeys[Number(tr.dataset.rowIndex)];
18331
+ return {
18332
+ tr,
18333
+ inBlock: key !== void 0 && blockKeys.has(key),
18334
+ height: tr.getBoundingClientRect().height
18335
+ };
18336
+ });
18337
+ const viewBottom = scrollEl.scrollTop + scrollEl.clientHeight;
18338
+ const openAt = (offset) => from + (to - from) * offset;
18339
+ const translate = (shift2, s) => `translateY(${-shift2 * (1 - s)}px)`;
18340
+ const timing = {
18341
+ duration,
18342
+ easing: spec.easing,
18343
+ // 접힘은 남겨 둔 행이 빠질 때까지 끝 위치를 붙잡는다 — 놓으면 그 사이 한 프레임 제자리로 튄다
18344
+ fill: to === 0 ? "forwards" : "none"
18345
+ };
18346
+ const animations = [];
18347
+ let shift = 0;
18348
+ let blockHeight = 0;
18349
+ let y = 0;
18350
+ trs.forEach(({ tr, inBlock, height }, i) => {
18351
+ if (inBlock && !trs[i - 1]?.inBlock) {
18352
+ blockHeight = 0;
18353
+ for (let j = i; j < trs.length && trs[j].inBlock; j++) blockHeight += trs[j].height;
18354
+ shift += blockHeight;
18355
+ y = 0;
18356
+ }
18357
+ if (shift === 0 || tr.offsetTop - shift > viewBottom) return;
18358
+ if (!inBlock) {
18359
+ animations.push(
18360
+ tr.animate(
18361
+ [{ transform: translate(shift, from) }, { transform: translate(shift, to) }],
18362
+ timing
18363
+ )
18364
+ );
18365
+ return;
18366
+ }
18367
+ const rowY = y;
18368
+ const clipAt = (s) => Math.min(height, Math.max(0, blockHeight * (1 - s) - rowY));
18369
+ const bends = [1 - rowY / blockHeight, 1 - (rowY + height) / blockHeight].map((s) => (s - from) / (to - from)).filter((offset) => offset > 0 && offset < 1);
18370
+ const offsets = [0, ...bends, 1].sort((a, b) => a - b);
18371
+ animations.push(
18372
+ tr.animate(
18373
+ offsets.map((offset) => ({
18374
+ offset,
18375
+ transform: translate(shift, openAt(offset)),
18376
+ clipPath: `inset(${clipAt(openAt(offset))}px 0 0 0)`
18377
+ })),
18378
+ timing
18379
+ )
18380
+ );
18381
+ y += height;
18382
+ });
18383
+ if (animations.length === 0) return null;
18384
+ return {
18385
+ keys: blockKeys,
18386
+ to,
18387
+ done: false,
18388
+ finished: Promise.all(animations.map((animation) => animation.finished)).then(
18389
+ () => true,
18390
+ () => false
18391
+ ),
18392
+ openness: () => {
18393
+ const progress = animations[0].effect?.getComputedTiming().progress;
18394
+ return progress == null ? to : openAt(progress);
18395
+ },
18396
+ cancel: () => animations.forEach((animation) => animation.cancel())
18397
+ };
18398
+ }
18156
18399
  var OWN_CONTROL_SELECTOR = 'a, button, input, select, textarea, label, [role="button"], [role="checkbox"], [contenteditable="true"]';
18157
18400
  function isOwnControl(target, cell) {
18158
18401
  if (!(target instanceof Element)) return false;
@@ -19407,6 +19650,7 @@ function SDatePicker({
19407
19650
  placeholder,
19408
19651
  selectable,
19409
19652
  disabled = false,
19653
+ showWeekday = false,
19410
19654
  clearable = false,
19411
19655
  width,
19412
19656
  maxWidth,
@@ -19436,6 +19680,7 @@ function SDatePicker({
19436
19680
  const [focused, setFocused] = useState(false);
19437
19681
  const [ruleError, setRuleError] = useState("");
19438
19682
  const hasValue = value != null && value !== "";
19683
+ const weekday = showWeekday && mode === "date";
19439
19684
  const resolvedPlaceholder = placeholder ?? (mode === "year" ? "YYYY" : mode === "month" ? "YYYY-MM" : "YYYY-MM-DD");
19440
19685
  useFormField({
19441
19686
  name,
@@ -19448,7 +19693,7 @@ function SDatePicker({
19448
19693
  });
19449
19694
  const resolvedError = error || ruleError !== "";
19450
19695
  const resolvedStatus = status ?? (rules && rules.length > 0 ? ruleError !== "" ? "error" : void 0 : void 0);
19451
- const dims = size === "md" ? {
19696
+ const baseDims = size === "md" ? {
19452
19697
  padX: 16,
19453
19698
  gap: 12,
19454
19699
  icon: 20,
@@ -19473,6 +19718,13 @@ function SDatePicker({
19473
19718
  lineHeight: 20,
19474
19719
  radius: 4
19475
19720
  };
19721
+ const weekdayW = weekday ? weekdaySuffixWidth(baseDims.fontSize) : 0;
19722
+ const dims = {
19723
+ ...baseDims,
19724
+ minW: baseDims.minW + weekdayW,
19725
+ fieldMinW: baseDims.fieldMinW + weekdayW,
19726
+ fieldMaxW: weekdayW ? `calc(${fieldWidthValue(baseDims.fieldMaxW)} + ${weekdayW}px)` : baseDims.fieldMaxW
19727
+ };
19476
19728
  const select = (date) => {
19477
19729
  onValueChange?.(date);
19478
19730
  if (rules && rules.length > 0) setRuleError(runRules(date, rules));
@@ -19557,7 +19809,7 @@ function SDatePicker({
19557
19809
  lineHeight: `${dims.lineHeight}px`,
19558
19810
  color: disabled ? "var(--cmp-datepicker-text-disabled)" : hasValue ? "var(--cmp-datepicker-text-default)" : "var(--cmp-field-hint-color)"
19559
19811
  },
19560
- children: hasValue ? value : resolvedPlaceholder
19812
+ children: hasValue ? weekday ? withWeekday(value) : value : resolvedPlaceholder
19561
19813
  }
19562
19814
  ),
19563
19815
  clearable && hasValue && !disabled && /* @__PURE__ */ jsx(
@@ -19951,6 +20203,7 @@ function SDateRangePicker({
19951
20203
  selectable,
19952
20204
  maxRange,
19953
20205
  showTimePicker = false,
20206
+ showWeekday = false,
19954
20207
  disabled = false,
19955
20208
  clearable = false,
19956
20209
  width,
@@ -19982,8 +20235,10 @@ function SDateRangePicker({
19982
20235
  const [pendingStart, setPendingStart] = useState(null);
19983
20236
  const hasValue = value != null && value[0] !== "" && value[1] !== "";
19984
20237
  const hasDisplay = pendingStart != null || hasValue;
19985
- const startText = pendingStart ?? (hasValue ? value[0] : "");
19986
- const endText = pendingStart ? "" : hasValue ? value[1] : "";
20238
+ const rawStart = pendingStart ?? (hasValue ? value[0] : "");
20239
+ const rawEnd = pendingStart ? "" : hasValue ? value[1] : "";
20240
+ const startText = showWeekday && rawStart ? withWeekday(rawStart) : rawStart;
20241
+ const endText = showWeekday && rawEnd ? withWeekday(rawEnd) : rawEnd;
19987
20242
  useFormField({
19988
20243
  name,
19989
20244
  validate: () => {
@@ -19995,7 +20250,7 @@ function SDateRangePicker({
19995
20250
  });
19996
20251
  const resolvedError = error || ruleError !== "";
19997
20252
  const resolvedStatus = status ?? (rules && rules.length > 0 ? ruleError !== "" ? "error" : void 0 : void 0);
19998
- const dims = size === "md" ? {
20253
+ const baseDims = size === "md" ? {
19999
20254
  padX: 16,
20000
20255
  gap: 12,
20001
20256
  icon: 20,
@@ -20019,6 +20274,13 @@ function SDateRangePicker({
20019
20274
  lineHeight: 20,
20020
20275
  radius: 4
20021
20276
  };
20277
+ const weekdayW = showWeekday ? weekdaySuffixWidth(baseDims.fontSize) * 2 : 0;
20278
+ const dims = {
20279
+ ...baseDims,
20280
+ minW: baseDims.minW + weekdayW,
20281
+ fieldMinW: baseDims.fieldMinW + weekdayW,
20282
+ fieldMaxW: weekdayW ? `calc(${fieldWidthValue(baseDims.fieldMaxW)} + ${weekdayW}px)` : baseDims.fieldMaxW
20283
+ };
20022
20284
  const select = (r3) => {
20023
20285
  setPendingStart(null);
20024
20286
  onValueChange?.(r3);
@@ -26020,7 +26282,7 @@ var isAtResetTarget = (field, value) => {
26020
26282
  var isInlineField = (field) => isInlineDateField(field) || field.type === "custom";
26021
26283
  var isFixedField = (field) => field.fixed === true || field.required === true;
26022
26284
  var isAddableField = (field) => !isFixedField(field);
26023
- var sameKeys = (a, b) => a.length === b.length && a.every((key, index) => key === b[index]);
26285
+ var sameKeys2 = (a, b) => a.length === b.length && a.every((key, index) => key === b[index]);
26024
26286
  var isGroupSatisfied = (group, values) => {
26025
26287
  if (!group.rule) return true;
26026
26288
  const keys = group.fields.map((f) => f.key);
@@ -26203,7 +26465,7 @@ var SChipFilter = /* @__PURE__ */ forwardRef(function SChipFilter2({
26203
26465
  const kept = current.filter((key) => known.has(key));
26204
26466
  const additions = fields.filter((field) => isFixedField(field) && !kept.includes(field.key)).map((field) => field.key);
26205
26467
  const next = [...kept, ...additions];
26206
- return sameKeys(current, next) ? current : next;
26468
+ return sameKeys2(current, next) ? current : next;
26207
26469
  });
26208
26470
  }, [fields, values]);
26209
26471
  useEffect(() => {