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.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
  {
@@ -14673,12 +14746,25 @@ function monthCells(year, month) {
14673
14746
  return cells;
14674
14747
  }
14675
14748
  var WEEK_LABELS = ["\uC77C", "\uC6D4", "\uD654", "\uC218", "\uBAA9", "\uAE08", "\uD1A0"];
14749
+ var WEEKDAY_SUFFIX_EM = 0.25 + 3 * 1;
14750
+ var weekdaySuffixWidth = (fontSize) => Math.ceil(fontSize * WEEKDAY_SUFFIX_EM);
14751
+ function withWeekday(value) {
14752
+ const [date = "", ...rest] = value.split(" ");
14753
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
14754
+ if (!match) return value;
14755
+ const day = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3])).getDay();
14756
+ if (Number.isNaN(day)) return value;
14757
+ return [date, `${WEEK_LABELS[day]}\uC694\uC77C`, ...rest].join(" ");
14758
+ }
14676
14759
 
14677
14760
  // src/components/SCalendarBoard/calendar-board-layout.ts
14678
14761
  function eventEndDate(event) {
14679
14762
  const end = event.endDate;
14680
14763
  return end != null && end > event.date ? end : event.date;
14681
14764
  }
14765
+ function compareDate(a, b) {
14766
+ return a < b ? -1 : a > b ? 1 : 0;
14767
+ }
14682
14768
  function layoutCalendarBoard({
14683
14769
  cells,
14684
14770
  events,
@@ -14724,7 +14810,7 @@ function layoutCalendarBoard({
14724
14810
  flush(to);
14725
14811
  });
14726
14812
  runs.sort(
14727
- (a, b) => a.startIndex - b.startIndex || b.endIndex - b.startIndex - (a.endIndex - a.startIndex) || a.order - b.order
14813
+ (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
14814
  );
14729
14815
  const weekLanes = Array.from({ length: weekCount }, () => []);
14730
14816
  for (const run of runs) {
@@ -17997,7 +18083,8 @@ var STableBar = /* @__PURE__ */ react.forwardRef(function STableBar2({ title, to
17997
18083
  );
17998
18084
  });
17999
18085
  var NO_KEYS = /* @__PURE__ */ new Set();
18000
- var ROW_ENTER_MS = 200;
18086
+ var EXPAND_MOTION = { duration: 220, easing: "ease-out" };
18087
+ var COLLAPSE_MOTION = { duration: 140, easing: "ease-in" };
18001
18088
  function STreeTable({
18002
18089
  rows = [],
18003
18090
  columns = [],
@@ -18044,32 +18131,88 @@ function STreeTable({
18044
18131
  if (expandedKeys === void 0) setInnerExpandedKeys(next);
18045
18132
  onExpandedChange?.(next, row);
18046
18133
  };
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);
18134
+ const visibleRows = react.useMemo(
18135
+ () => flattenRows(rows, (row) => expandedSet.has(keyOf(row)), childrenOf),
18136
+ // eslint-disable-next-line react-hooks/exhaustive-deps
18137
+ [rows, expandedSet, rowKey, childrenKey]
18138
+ );
18139
+ const tableRef = react.useRef(null);
18140
+ const [closingKeys, setClosingKeys] = react.useState(NO_KEYS);
18141
+ const [prevExpand, setPrevExpand] = react.useState({ rows, expandedSet });
18142
+ if (prevExpand.rows !== rows || prevExpand.expandedSet !== expandedSet) {
18143
+ setPrevExpand({ rows, expandedSet });
18144
+ let nextClosing = NO_KEYS;
18145
+ if (prevExpand.rows === rows && motionAllowed()) {
18146
+ const collapsed = [...prevExpand.expandedSet].filter((key) => !expandedSet.has(key));
18147
+ const expanded = [...expandedSet].filter((key) => !prevExpand.expandedSet.has(key));
18148
+ if (collapsed.length > 0 && expanded.length === 0) nextClosing = new Set(collapsed);
18149
+ }
18150
+ if (nextClosing !== closingKeys) setClosingKeys(nextClosing);
18151
+ }
18152
+ const renderedRows = react.useMemo(
18153
+ () => closingKeys.size === 0 ? visibleRows : flattenRows(
18154
+ rows,
18155
+ (row) => expandedSet.has(keyOf(row)) || closingKeys.has(keyOf(row)),
18156
+ childrenOf
18157
+ ),
18158
+ // eslint-disable-next-line react-hooks/exhaustive-deps
18159
+ [visibleRows, closingKeys]
18160
+ );
18161
+ const visibleKeys = react.useMemo(
18162
+ () => new Set(visibleRows.map(keyOf)),
18163
+ // eslint-disable-next-line react-hooks/exhaustive-deps
18164
+ [visibleRows]
18165
+ );
18166
+ const motionRef = react.useRef(null);
18059
18167
  const prevVisibleRef = react.useRef(null);
18060
18168
  useIsomorphicLayoutEffect(() => {
18061
- const keys = new Set(visibleRows.map(keyOf));
18062
18169
  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]);
18170
+ prevVisibleRef.current = { rows, keys: visibleKeys };
18171
+ const running = motionRef.current;
18172
+ if (running?.done) {
18173
+ running.cancel();
18174
+ motionRef.current = null;
18175
+ }
18176
+ if (!prev || prev.rows !== rows) {
18177
+ motionRef.current?.cancel();
18178
+ motionRef.current = null;
18179
+ return;
18180
+ }
18181
+ const added = new Set([...visibleKeys].filter((key) => !prev.keys.has(key)));
18182
+ const removed = new Set([...prev.keys].filter((key) => !visibleKeys.has(key)));
18183
+ if (added.size === 0 && removed.size === 0) return;
18184
+ let from = added.size > 0 ? 0 : 1;
18185
+ if (motionRef.current) {
18186
+ const current = motionRef.current;
18187
+ const reversing = current.to === 0 && sameKeys(added, current.keys) || current.to === 1 && sameKeys(removed, current.keys);
18188
+ if (reversing) from = current.openness();
18189
+ current.cancel();
18190
+ motionRef.current = null;
18191
+ }
18192
+ const scrollEl = tableRef.current?.getScrollElement();
18193
+ const keys = added.size > 0 ? added : removed;
18194
+ const canAnimate = scrollEl != null && motionAllowed() && (added.size === 0 || removed.size === 0);
18195
+ const motion = canAnimate ? startRowMotion({
18196
+ scrollEl,
18197
+ rowKeys: renderedRows.map(keyOf),
18198
+ blockKeys: keys,
18199
+ from,
18200
+ to: added.size > 0 ? 1 : 0
18201
+ }) : null;
18202
+ if (motion) {
18203
+ motionRef.current = motion;
18204
+ if (motion.to === 0) {
18205
+ void motion.finished.then((completed) => {
18206
+ if (!completed || motionRef.current !== motion) return;
18207
+ motion.done = true;
18208
+ setClosingKeys(NO_KEYS);
18209
+ });
18210
+ }
18211
+ } else if (closingKeys.size > 0) {
18212
+ setClosingKeys(NO_KEYS);
18213
+ }
18214
+ }, [renderedRows]);
18215
+ react.useEffect(() => () => motionRef.current?.cancel(), []);
18073
18216
  const collectSelectable = (row) => canSelectRow(row) ? [row, ...childrenOf(row).flatMap(collectSelectable)] : [];
18074
18217
  const selectionOf = (keys) => {
18075
18218
  const state2 = /* @__PURE__ */ new Map();
@@ -18165,8 +18308,9 @@ function STreeTable({
18165
18308
  STable2,
18166
18309
  {
18167
18310
  ...rest,
18311
+ ref: tableRef,
18168
18312
  columns: tableColumns,
18169
- rows: visibleRows,
18313
+ rows: renderedRows,
18170
18314
  rowKey,
18171
18315
  resizable,
18172
18316
  selectable,
@@ -18174,14 +18318,113 @@ function STreeTable({
18174
18318
  isRowIndeterminate: selectionState ? (row) => !!selectionState.get(keyOf(row))?.indeterminate : void 0,
18175
18319
  rowClassName: (row) => cn(
18176
18320
  rowClassName?.(row),
18177
- // 위에서 4px 내려오며 나타난다(theme.css --animate-table-row-in). 모션을 줄인 환경에서는 끈다
18178
- enteringKeys.has(keyOf(row)) && "motion-safe:animate-table-row-in"
18321
+ // 접히는 중인 행은 이미 접힌 행이다 — 사라지는 동안 눌리지 않게 한다
18322
+ !visibleKeys.has(keyOf(row)) && "pointer-events-none"
18179
18323
  ) || void 0,
18180
18324
  selected: tableSelected,
18181
18325
  onSelectedChange: handleSelectedChange
18182
18326
  }
18183
18327
  );
18184
18328
  }
18329
+ function flattenRows(nodes, isOpen, childrenOf) {
18330
+ const out = [];
18331
+ const walk = (list) => {
18332
+ list.forEach((row) => {
18333
+ out.push(row);
18334
+ if (isOpen(row)) walk(childrenOf(row));
18335
+ });
18336
+ };
18337
+ walk(nodes);
18338
+ return out;
18339
+ }
18340
+ function sameKeys(a, b) {
18341
+ return a.size === b.size && [...a].every((key) => b.has(key));
18342
+ }
18343
+ function motionAllowed() {
18344
+ if (typeof window === "undefined" || typeof Element.prototype.animate !== "function")
18345
+ return false;
18346
+ return !window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
18347
+ }
18348
+ function startRowMotion({
18349
+ scrollEl,
18350
+ rowKeys,
18351
+ blockKeys,
18352
+ from,
18353
+ to
18354
+ }) {
18355
+ const spec = to === 1 ? EXPAND_MOTION : COLLAPSE_MOTION;
18356
+ const duration = spec.duration * Math.abs(to - from);
18357
+ if (duration < 1) return null;
18358
+ const trs = [...scrollEl.querySelectorAll("tr[data-row-index]")].map((tr) => {
18359
+ const key = rowKeys[Number(tr.dataset.rowIndex)];
18360
+ return {
18361
+ tr,
18362
+ inBlock: key !== void 0 && blockKeys.has(key),
18363
+ height: tr.getBoundingClientRect().height
18364
+ };
18365
+ });
18366
+ const viewBottom = scrollEl.scrollTop + scrollEl.clientHeight;
18367
+ const openAt = (offset) => from + (to - from) * offset;
18368
+ const translate = (shift2, s) => `translateY(${-shift2 * (1 - s)}px)`;
18369
+ const timing = {
18370
+ duration,
18371
+ easing: spec.easing,
18372
+ // 접힘은 남겨 둔 행이 빠질 때까지 끝 위치를 붙잡는다 — 놓으면 그 사이 한 프레임 제자리로 튄다
18373
+ fill: to === 0 ? "forwards" : "none"
18374
+ };
18375
+ const animations = [];
18376
+ let shift = 0;
18377
+ let blockHeight = 0;
18378
+ let y = 0;
18379
+ trs.forEach(({ tr, inBlock, height }, i) => {
18380
+ if (inBlock && !trs[i - 1]?.inBlock) {
18381
+ blockHeight = 0;
18382
+ for (let j = i; j < trs.length && trs[j].inBlock; j++) blockHeight += trs[j].height;
18383
+ shift += blockHeight;
18384
+ y = 0;
18385
+ }
18386
+ if (shift === 0 || tr.offsetTop - shift > viewBottom) return;
18387
+ if (!inBlock) {
18388
+ animations.push(
18389
+ tr.animate(
18390
+ [{ transform: translate(shift, from) }, { transform: translate(shift, to) }],
18391
+ timing
18392
+ )
18393
+ );
18394
+ return;
18395
+ }
18396
+ const rowY = y;
18397
+ const clipAt = (s) => Math.min(height, Math.max(0, blockHeight * (1 - s) - rowY));
18398
+ const bends = [1 - rowY / blockHeight, 1 - (rowY + height) / blockHeight].map((s) => (s - from) / (to - from)).filter((offset) => offset > 0 && offset < 1);
18399
+ const offsets = [0, ...bends, 1].sort((a, b) => a - b);
18400
+ animations.push(
18401
+ tr.animate(
18402
+ offsets.map((offset) => ({
18403
+ offset,
18404
+ transform: translate(shift, openAt(offset)),
18405
+ clipPath: `inset(${clipAt(openAt(offset))}px 0 0 0)`
18406
+ })),
18407
+ timing
18408
+ )
18409
+ );
18410
+ y += height;
18411
+ });
18412
+ if (animations.length === 0) return null;
18413
+ return {
18414
+ keys: blockKeys,
18415
+ to,
18416
+ done: false,
18417
+ finished: Promise.all(animations.map((animation) => animation.finished)).then(
18418
+ () => true,
18419
+ () => false
18420
+ ),
18421
+ openness: () => {
18422
+ const progress = animations[0].effect?.getComputedTiming().progress;
18423
+ return progress == null ? to : openAt(progress);
18424
+ },
18425
+ cancel: () => animations.forEach((animation) => animation.cancel())
18426
+ };
18427
+ }
18185
18428
  var OWN_CONTROL_SELECTOR = 'a, button, input, select, textarea, label, [role="button"], [role="checkbox"], [contenteditable="true"]';
18186
18429
  function isOwnControl(target, cell) {
18187
18430
  if (!(target instanceof Element)) return false;
@@ -19436,6 +19679,7 @@ function SDatePicker({
19436
19679
  placeholder,
19437
19680
  selectable,
19438
19681
  disabled = false,
19682
+ showWeekday = false,
19439
19683
  clearable = false,
19440
19684
  width,
19441
19685
  maxWidth,
@@ -19465,6 +19709,7 @@ function SDatePicker({
19465
19709
  const [focused, setFocused] = react.useState(false);
19466
19710
  const [ruleError, setRuleError] = react.useState("");
19467
19711
  const hasValue = value != null && value !== "";
19712
+ const weekday = showWeekday && mode === "date";
19468
19713
  const resolvedPlaceholder = placeholder ?? (mode === "year" ? "YYYY" : mode === "month" ? "YYYY-MM" : "YYYY-MM-DD");
19469
19714
  useFormField({
19470
19715
  name,
@@ -19477,7 +19722,7 @@ function SDatePicker({
19477
19722
  });
19478
19723
  const resolvedError = error || ruleError !== "";
19479
19724
  const resolvedStatus = status ?? (rules && rules.length > 0 ? ruleError !== "" ? "error" : void 0 : void 0);
19480
- const dims = size === "md" ? {
19725
+ const baseDims = size === "md" ? {
19481
19726
  padX: 16,
19482
19727
  gap: 12,
19483
19728
  icon: 20,
@@ -19502,6 +19747,13 @@ function SDatePicker({
19502
19747
  lineHeight: 20,
19503
19748
  radius: 4
19504
19749
  };
19750
+ const weekdayW = weekday ? weekdaySuffixWidth(baseDims.fontSize) : 0;
19751
+ const dims = {
19752
+ ...baseDims,
19753
+ minW: baseDims.minW + weekdayW,
19754
+ fieldMinW: baseDims.fieldMinW + weekdayW,
19755
+ fieldMaxW: weekdayW ? `calc(${fieldWidthValue(baseDims.fieldMaxW)} + ${weekdayW}px)` : baseDims.fieldMaxW
19756
+ };
19505
19757
  const select = (date) => {
19506
19758
  onValueChange?.(date);
19507
19759
  if (rules && rules.length > 0) setRuleError(runRules(date, rules));
@@ -19586,7 +19838,7 @@ function SDatePicker({
19586
19838
  lineHeight: `${dims.lineHeight}px`,
19587
19839
  color: disabled ? "var(--cmp-datepicker-text-disabled)" : hasValue ? "var(--cmp-datepicker-text-default)" : "var(--cmp-field-hint-color)"
19588
19840
  },
19589
- children: hasValue ? value : resolvedPlaceholder
19841
+ children: hasValue ? weekday ? withWeekday(value) : value : resolvedPlaceholder
19590
19842
  }
19591
19843
  ),
19592
19844
  clearable && hasValue && !disabled && /* @__PURE__ */ jsxRuntime.jsx(
@@ -19980,6 +20232,7 @@ function SDateRangePicker({
19980
20232
  selectable,
19981
20233
  maxRange,
19982
20234
  showTimePicker = false,
20235
+ showWeekday = false,
19983
20236
  disabled = false,
19984
20237
  clearable = false,
19985
20238
  width,
@@ -20011,8 +20264,10 @@ function SDateRangePicker({
20011
20264
  const [pendingStart, setPendingStart] = react.useState(null);
20012
20265
  const hasValue = value != null && value[0] !== "" && value[1] !== "";
20013
20266
  const hasDisplay = pendingStart != null || hasValue;
20014
- const startText = pendingStart ?? (hasValue ? value[0] : "");
20015
- const endText = pendingStart ? "" : hasValue ? value[1] : "";
20267
+ const rawStart = pendingStart ?? (hasValue ? value[0] : "");
20268
+ const rawEnd = pendingStart ? "" : hasValue ? value[1] : "";
20269
+ const startText = showWeekday && rawStart ? withWeekday(rawStart) : rawStart;
20270
+ const endText = showWeekday && rawEnd ? withWeekday(rawEnd) : rawEnd;
20016
20271
  useFormField({
20017
20272
  name,
20018
20273
  validate: () => {
@@ -20024,7 +20279,7 @@ function SDateRangePicker({
20024
20279
  });
20025
20280
  const resolvedError = error || ruleError !== "";
20026
20281
  const resolvedStatus = status ?? (rules && rules.length > 0 ? ruleError !== "" ? "error" : void 0 : void 0);
20027
- const dims = size === "md" ? {
20282
+ const baseDims = size === "md" ? {
20028
20283
  padX: 16,
20029
20284
  gap: 12,
20030
20285
  icon: 20,
@@ -20048,6 +20303,13 @@ function SDateRangePicker({
20048
20303
  lineHeight: 20,
20049
20304
  radius: 4
20050
20305
  };
20306
+ const weekdayW = showWeekday ? weekdaySuffixWidth(baseDims.fontSize) * 2 : 0;
20307
+ const dims = {
20308
+ ...baseDims,
20309
+ minW: baseDims.minW + weekdayW,
20310
+ fieldMinW: baseDims.fieldMinW + weekdayW,
20311
+ fieldMaxW: weekdayW ? `calc(${fieldWidthValue(baseDims.fieldMaxW)} + ${weekdayW}px)` : baseDims.fieldMaxW
20312
+ };
20051
20313
  const select = (r3) => {
20052
20314
  setPendingStart(null);
20053
20315
  onValueChange?.(r3);
@@ -26049,7 +26311,7 @@ var isAtResetTarget = (field, value) => {
26049
26311
  var isInlineField = (field) => isInlineDateField(field) || field.type === "custom";
26050
26312
  var isFixedField = (field) => field.fixed === true || field.required === true;
26051
26313
  var isAddableField = (field) => !isFixedField(field);
26052
- var sameKeys = (a, b) => a.length === b.length && a.every((key, index) => key === b[index]);
26314
+ var sameKeys2 = (a, b) => a.length === b.length && a.every((key, index) => key === b[index]);
26053
26315
  var isGroupSatisfied = (group, values) => {
26054
26316
  if (!group.rule) return true;
26055
26317
  const keys = group.fields.map((f) => f.key);
@@ -26232,7 +26494,7 @@ var SChipFilter = /* @__PURE__ */ react.forwardRef(function SChipFilter2({
26232
26494
  const kept = current.filter((key) => known.has(key));
26233
26495
  const additions = fields.filter((field) => isFixedField(field) && !kept.includes(field.key)).map((field) => field.key);
26234
26496
  const next = [...kept, ...additions];
26235
- return sameKeys(current, next) ? current : next;
26497
+ return sameKeys2(current, next) ? current : next;
26236
26498
  });
26237
26499
  }, [fields, values]);
26238
26500
  react.useEffect(() => {