sellmate-design-system-react 9.0.0-beta.95 → 9.0.0-beta.98

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
@@ -4232,6 +4232,7 @@ var SButton = /* @__PURE__ */ forwardRef(function SButton2({
4232
4232
  icon,
4233
4233
  rightIcon,
4234
4234
  label,
4235
+ pill = false,
4235
4236
  className,
4236
4237
  style,
4237
4238
  type = "button",
@@ -4262,7 +4263,8 @@ var SButton = /* @__PURE__ */ forwardRef(function SButton2({
4262
4263
  "minHeight": s.height,
4263
4264
  "minWidth": iconOnly ? s.iconOnlyWidth : hasLabel ? s.minWidth : void 0,
4264
4265
  "padding": iconOnly ? 0 : `0 ${s.paddingX}`,
4265
- "borderRadius": s.radius,
4266
+ // 버튼 전용 pill 반경 토큰이 아직 없어 공용 반경을 쓴다
4267
+ "borderRadius": pill ? "var(--radius-full)" : s.radius,
4266
4268
  "gap": s.gap,
4267
4269
  "fontSize": s.fontSize,
4268
4270
  "fontWeight": s.fontWeight,
@@ -15657,6 +15659,93 @@ function SCalendarBoard({
15657
15659
  );
15658
15660
  }
15659
15661
 
15662
+ // src/lib/row-motion.ts
15663
+ var EXPAND_MOTION = { duration: 220, easing: "ease-out" };
15664
+ var COLLAPSE_MOTION = { duration: 140, easing: "ease-in" };
15665
+ function sameKeys(a, b) {
15666
+ return a.size === b.size && [...a].every((key) => b.has(key));
15667
+ }
15668
+ function motionAllowed() {
15669
+ if (typeof window === "undefined" || typeof Element.prototype.animate !== "function")
15670
+ return false;
15671
+ return !window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
15672
+ }
15673
+ function startRowMotion({
15674
+ scrollEl,
15675
+ isInBlock,
15676
+ blockKeys,
15677
+ from,
15678
+ to
15679
+ }) {
15680
+ const spec = to === 1 ? EXPAND_MOTION : COLLAPSE_MOTION;
15681
+ const duration = spec.duration * Math.abs(to - from);
15682
+ if (duration < 1) return null;
15683
+ const trs = [
15684
+ ...scrollEl.querySelectorAll("tr[data-row-index], tr[data-expansion-row]")
15685
+ ].map((tr) => ({ tr, inBlock: isInBlock(tr), height: tr.getBoundingClientRect().height }));
15686
+ const viewBottom = scrollEl.scrollTop + scrollEl.clientHeight;
15687
+ const openAt = (offset) => from + (to - from) * offset;
15688
+ const translate = (shift2, s) => `translateY(${-shift2 * (1 - s)}px)`;
15689
+ const timing = {
15690
+ duration,
15691
+ easing: spec.easing,
15692
+ // 접힘은 남겨 둔 행이 빠질 때까지 끝 위치를 붙잡는다 — 놓으면 그 사이 한 프레임 제자리로 튄다
15693
+ fill: to === 0 ? "forwards" : "none"
15694
+ };
15695
+ const animations = [];
15696
+ let shift = 0;
15697
+ let blockHeight = 0;
15698
+ let y = 0;
15699
+ trs.forEach(({ tr, inBlock, height }, i) => {
15700
+ if (inBlock && !trs[i - 1]?.inBlock) {
15701
+ blockHeight = 0;
15702
+ for (let j = i; j < trs.length && trs[j].inBlock; j++) blockHeight += trs[j].height;
15703
+ shift += blockHeight;
15704
+ y = 0;
15705
+ }
15706
+ if (shift === 0 || tr.offsetTop - shift > viewBottom) return;
15707
+ if (!inBlock) {
15708
+ animations.push(
15709
+ tr.animate(
15710
+ [{ transform: translate(shift, from) }, { transform: translate(shift, to) }],
15711
+ timing
15712
+ )
15713
+ );
15714
+ return;
15715
+ }
15716
+ const rowY = y;
15717
+ const clipAt = (s) => Math.min(height, Math.max(0, blockHeight * (1 - s) - rowY));
15718
+ const bends = [1 - rowY / blockHeight, 1 - (rowY + height) / blockHeight].map((s) => (s - from) / (to - from)).filter((offset) => offset > 0 && offset < 1);
15719
+ const offsets = [0, ...bends, 1].sort((a, b) => a - b);
15720
+ animations.push(
15721
+ tr.animate(
15722
+ offsets.map((offset) => ({
15723
+ offset,
15724
+ transform: translate(shift, openAt(offset)),
15725
+ clipPath: `inset(${clipAt(openAt(offset))}px 0 0 0)`
15726
+ })),
15727
+ timing
15728
+ )
15729
+ );
15730
+ y += height;
15731
+ });
15732
+ if (animations.length === 0) return null;
15733
+ return {
15734
+ keys: blockKeys,
15735
+ to,
15736
+ done: false,
15737
+ finished: Promise.all(animations.map((animation) => animation.finished)).then(
15738
+ () => true,
15739
+ () => false
15740
+ ),
15741
+ openness: () => {
15742
+ const progress = animations[0].effect?.getComputedTiming().progress;
15743
+ return progress == null ? to : openAt(progress);
15744
+ },
15745
+ cancel: () => animations.forEach((animation) => animation.cancel())
15746
+ };
15747
+ }
15748
+
15660
15749
  // src/lib/row-metrics.ts
15661
15750
  function createRowMetrics(count, estimate) {
15662
15751
  const safeCount = Math.max(0, Math.floor(count));
@@ -16076,6 +16165,7 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
16076
16165
  onSearchChange,
16077
16166
  searchDebounce = DEFAULT_SEARCH_DEBOUNCE,
16078
16167
  label,
16168
+ ariaLabel,
16079
16169
  labelPosition,
16080
16170
  subLabel,
16081
16171
  labelWidth,
@@ -16778,6 +16868,7 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
16778
16868
  style,
16779
16869
  children: [
16780
16870
  name && /* @__PURE__ */ jsx("input", { type: "hidden", name, value: selected.join(",") }),
16871
+ !label && ariaLabel && /* @__PURE__ */ jsx("span", { id: `${triggerId}-aria-label`, hidden: true, children: ariaLabel }),
16781
16872
  /* @__PURE__ */ jsxs(
16782
16873
  "div",
16783
16874
  {
@@ -16786,7 +16877,7 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
16786
16877
  role: "combobox",
16787
16878
  tabIndex: effectiveDisabled ? -1 : 0,
16788
16879
  "aria-disabled": effectiveDisabled || void 0,
16789
- "aria-labelledby": label ? `${fieldLabelId(triggerId)} ${triggerId}` : triggerId,
16880
+ "aria-labelledby": label ? `${fieldLabelId(triggerId)} ${triggerId}` : ariaLabel ? `${triggerId}-aria-label ${triggerId}` : triggerId,
16790
16881
  "aria-haspopup": isTree ? "tree" : "listbox",
16791
16882
  "aria-expanded": open,
16792
16883
  "aria-controls": open ? listboxId : void 0,
@@ -17394,6 +17485,7 @@ function CheckboxBox({
17394
17485
  }
17395
17486
  );
17396
17487
  }
17488
+ var NO_KEYS = /* @__PURE__ */ new Set();
17397
17489
  var DEFAULT_ROW_HEIGHT = 44;
17398
17490
  var DENSE_ROW_HEIGHT = 32;
17399
17491
  var SELECTABLE_COLUMN_WIDTH = 48;
@@ -17424,6 +17516,11 @@ var CONTEXT_MENU_ITEM_CLASS = cn(
17424
17516
  "data-[highlighted]:bg-[var(--sys-color-bg-accent)] data-[highlighted]:text-[color:var(--sys-color-fg-inverse)]"
17425
17517
  );
17426
17518
  var ROW_INTERACTIVE_SELECTOR = 'button, a, input, select, textarea, label, [role="button"], [role="checkbox"], [contenteditable="true"]';
17519
+ function rowBlockHeight(tr) {
17520
+ const next = tr.nextElementSibling;
17521
+ const expansion = next instanceof HTMLElement && next.dataset.expansionRow != null ? next.offsetHeight : 0;
17522
+ return tr.offsetHeight + expansion;
17523
+ }
17427
17524
  var colWidthFromDef = (c) => {
17428
17525
  if (c.autoWidth) return 0;
17429
17526
  if (c.width == null) return DEFAULT_COLUMN_WIDTH;
@@ -17436,6 +17533,8 @@ var STableImpl = /* @__PURE__ */ forwardRef(function STable({
17436
17533
  rows = [],
17437
17534
  rowKey = "id",
17438
17535
  selectable = false,
17536
+ rowSelectLabel,
17537
+ selectAllLabel = "\uC804\uCCB4 \uC120\uD0DD",
17439
17538
  dragSelectable = false,
17440
17539
  contextMenuItems,
17441
17540
  onContextMenuItemClick,
@@ -17474,6 +17573,11 @@ var STableImpl = /* @__PURE__ */ forwardRef(function STable({
17474
17573
  tableId,
17475
17574
  onRowClick,
17476
17575
  onRowDoubleClick,
17576
+ renderExpansion,
17577
+ expansionColumn,
17578
+ expandedKeys,
17579
+ defaultExpandedKeys,
17580
+ onExpandedChange,
17477
17581
  className,
17478
17582
  style
17479
17583
  }, ref) {
@@ -17488,6 +17592,44 @@ var STableImpl = /* @__PURE__ */ forwardRef(function STable({
17488
17592
  onDenseChange?.(next);
17489
17593
  };
17490
17594
  const visibleCols = useMemo(() => columns.filter((c) => c.visible !== false), [columns]);
17595
+ const expansionOn = renderExpansion != null;
17596
+ const [innerExpandedKeys, setInnerExpandedKeys] = useState(
17597
+ () => defaultExpandedKeys ?? []
17598
+ );
17599
+ const currentExpandedKeys = expandedKeys ?? innerExpandedKeys;
17600
+ const expandedSet = useMemo(() => new Set(currentExpandedKeys), [currentExpandedKeys]);
17601
+ const isRowExpanded = (row) => expansionOn && expandedSet.has(row[rowKey]);
17602
+ const [closingKeys, setClosingKeys] = useState(NO_KEYS);
17603
+ const [prevExpandedSet, setPrevExpandedSet] = useState(expandedSet);
17604
+ if (prevExpandedSet !== expandedSet) {
17605
+ setPrevExpandedSet(expandedSet);
17606
+ let nextClosing = NO_KEYS;
17607
+ if (expansionOn && motionAllowed()) {
17608
+ const collapsed = [...prevExpandedSet].filter((key) => !expandedSet.has(key));
17609
+ const opened = [...expandedSet].filter((key) => !prevExpandedSet.has(key));
17610
+ if (collapsed.length > 0 && opened.length === 0) nextClosing = new Set(collapsed);
17611
+ }
17612
+ if (nextClosing !== closingKeys) setClosingKeys(nextClosing);
17613
+ }
17614
+ const shownExpansionSet = useMemo(
17615
+ () => closingKeys.size === 0 ? expandedSet : /* @__PURE__ */ new Set([...expandedSet, ...closingKeys]),
17616
+ [expandedSet, closingKeys]
17617
+ );
17618
+ const toggleExpanded = (row) => {
17619
+ const key = row[rowKey];
17620
+ const next = expandedSet.has(key) ? currentExpandedKeys.filter((k) => k !== key) : [...currentExpandedKeys, key];
17621
+ if (expandedKeys === void 0) setInnerExpandedKeys(next);
17622
+ onExpandedChange?.(next, row);
17623
+ };
17624
+ const expansionColIndex = !expansionOn ? -1 : expansionColumn == null ? 0 : visibleCols.findIndex((c) => c.name === expansionColumn);
17625
+ if (expansionColIndex >= 0) {
17626
+ const col = visibleCols[expansionColIndex];
17627
+ if (col?.renderCell) {
17628
+ throw new Error(
17629
+ `[STable] \uD3BC\uCE68 \uCEEC\uB7FC(${col.name})\uC5D0\uB294 renderCell \uC744 \uC4F8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. render \uB97C \uC4F0\uC138\uC694.`
17630
+ );
17631
+ }
17632
+ }
17491
17633
  const headerGroupInfo = useMemo(() => {
17492
17634
  if (!headerGroups || headerGroups.length === 0) return null;
17493
17635
  const indexByName = new Map(visibleCols.map((col, index) => [col.name, index]));
@@ -17592,6 +17734,7 @@ var STableImpl = /* @__PURE__ */ forwardRef(function STable({
17592
17734
  metricsRef.current ?? (metricsRef.current = createRowMetrics(0, effectiveRowHeight));
17593
17735
  const metrics = metricsRef.current;
17594
17736
  const measuredKeyRef = useRef("");
17737
+ const measuredExpansionRef = useRef(shownExpansionSet);
17595
17738
  const pendingRowRef = useRef(null);
17596
17739
  const pinnedTopRef = useRef(-1);
17597
17740
  const lastEstimateRef = useRef(effectiveRowHeight);
@@ -17605,6 +17748,7 @@ var STableImpl = /* @__PURE__ */ forwardRef(function STable({
17605
17748
  }
17606
17749
  const [scrolledLeft, setScrolledLeft] = useState(false);
17607
17750
  const [scrolledRight, setScrolledRight] = useState(false);
17751
+ const [viewportWidth, setViewportWidth] = useState(0);
17608
17752
  const setRowCount = useCallback((count) => {
17609
17753
  const safe = Math.max(0, Math.floor(Number.isFinite(count) ? count : 0));
17610
17754
  lastReachEndRef.current = -1;
@@ -17630,6 +17774,7 @@ var STableImpl = /* @__PURE__ */ forwardRef(function STable({
17630
17774
  setHeaderHeight(headerH);
17631
17775
  setScrolledLeft(el.scrollLeft > 0);
17632
17776
  setScrolledRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 1);
17777
+ setViewportWidth(el.clientWidth);
17633
17778
  if (virtualScroll) {
17634
17779
  const { start, end } = calcRange(el.scrollTop, el.clientHeight - headerH);
17635
17780
  setVsRange((prev) => prev.start === start && prev.end === end ? prev : { start, end });
@@ -17647,6 +17792,13 @@ var STableImpl = /* @__PURE__ */ forwardRef(function STable({
17647
17792
  useEffect(() => {
17648
17793
  handleScroll();
17649
17794
  }, [handleScroll, rowCount, columnWidths, loading2, height]);
17795
+ useEffect(() => {
17796
+ const el = scrollRef.current;
17797
+ if (!expansionOn || !el || typeof ResizeObserver === "undefined") return;
17798
+ const observer = new ResizeObserver(() => handleScroll());
17799
+ observer.observe(el);
17800
+ return () => observer.disconnect();
17801
+ }, [expansionOn, handleScroll]);
17650
17802
  const handleScrollRef = useRef(handleScroll);
17651
17803
  handleScrollRef.current = handleScroll;
17652
17804
  useIsomorphicLayoutEffect(() => {
@@ -17656,9 +17808,10 @@ var STableImpl = /* @__PURE__ */ forwardRef(function STable({
17656
17808
  if (!el || m == null) return;
17657
17809
  const pending2 = pendingRowRef.current;
17658
17810
  const key = `${vsRange.start}:${vsRange.end}:${rowCount}`;
17659
- const alreadyMeasured = measuredKeyRef.current === key;
17811
+ const alreadyMeasured = measuredKeyRef.current === key && measuredExpansionRef.current === shownExpansionSet;
17660
17812
  if (alreadyMeasured && pending2 == null) return;
17661
17813
  measuredKeyRef.current = key;
17814
+ measuredExpansionRef.current = shownExpansionSet;
17662
17815
  const anchor = vsRange.start;
17663
17816
  const anchorBefore = offsetOf(m, anchor);
17664
17817
  let changed = false;
@@ -17666,7 +17819,7 @@ var STableImpl = /* @__PURE__ */ forwardRef(function STable({
17666
17819
  for (const tr of el.querySelectorAll("tr[data-row-index]")) {
17667
17820
  const index = Number(tr.dataset.rowIndex);
17668
17821
  if (!Number.isInteger(index)) continue;
17669
- const h = tr.offsetHeight;
17822
+ const h = rowBlockHeight(tr);
17670
17823
  if (h <= 0) continue;
17671
17824
  if (setRowSize(m, index, h)) changed = true;
17672
17825
  }
@@ -17688,6 +17841,54 @@ var STableImpl = /* @__PURE__ */ forwardRef(function STable({
17688
17841
  if (delta !== 0 && el.scrollTop > 0) el.scrollTop += delta;
17689
17842
  handleScrollRef.current();
17690
17843
  });
17844
+ const expansionMotionRef = useRef(null);
17845
+ const prevExpansionRef = useRef(null);
17846
+ useIsomorphicLayoutEffect(() => {
17847
+ if (!expansionOn) return;
17848
+ const prev = prevExpansionRef.current;
17849
+ prevExpansionRef.current = { rows, keys: expandedSet };
17850
+ const running = expansionMotionRef.current;
17851
+ if (running?.done) {
17852
+ running.cancel();
17853
+ expansionMotionRef.current = null;
17854
+ }
17855
+ if (!prev || prev.keys === expandedSet) return;
17856
+ const added = new Set([...expandedSet].filter((key) => !prev.keys.has(key)));
17857
+ const removed = new Set([...prev.keys].filter((key) => !expandedSet.has(key)));
17858
+ if (added.size === 0 && removed.size === 0) return;
17859
+ let from = added.size > 0 ? 0 : 1;
17860
+ if (expansionMotionRef.current) {
17861
+ const current = expansionMotionRef.current;
17862
+ const reversing = current.to === 0 && sameKeys(added, current.keys) || current.to === 1 && sameKeys(removed, current.keys);
17863
+ if (reversing) from = current.openness();
17864
+ current.cancel();
17865
+ expansionMotionRef.current = null;
17866
+ }
17867
+ const scrollEl = scrollRef.current;
17868
+ const keys = added.size > 0 ? added : removed;
17869
+ const keyNames = new Set([...keys].map(String));
17870
+ const canAnimate = scrollEl != null && prev.rows === rows && motionAllowed() && (added.size === 0 || removed.size === 0);
17871
+ const motion = canAnimate ? startRowMotion({
17872
+ scrollEl,
17873
+ isInBlock: (tr) => tr.dataset.expansionRow != null && keyNames.has(tr.dataset.expansionKey ?? ""),
17874
+ blockKeys: keys,
17875
+ from,
17876
+ to: added.size > 0 ? 1 : 0
17877
+ }) : null;
17878
+ if (motion) {
17879
+ expansionMotionRef.current = motion;
17880
+ if (motion.to === 0) {
17881
+ void motion.finished.then((completed) => {
17882
+ if (!completed || expansionMotionRef.current !== motion) return;
17883
+ motion.done = true;
17884
+ setClosingKeys(NO_KEYS);
17885
+ });
17886
+ }
17887
+ } else if (closingKeys.size > 0) {
17888
+ setClosingKeys(NO_KEYS);
17889
+ }
17890
+ }, [expandedSet, closingKeys]);
17891
+ useEffect(() => () => expansionMotionRef.current?.cancel(), []);
17691
17892
  useEffect(() => {
17692
17893
  if (!virtualScroll) return;
17693
17894
  cbRef.current.onVirtualUpdate?.({ from: vsRange.start, to: vsRange.end });
@@ -17744,7 +17945,7 @@ var STableImpl = /* @__PURE__ */ forwardRef(function STable({
17744
17945
  if (virtualScroll && m != null) return indexAtOffset(m, el.scrollTop);
17745
17946
  const top = el.scrollTop;
17746
17947
  for (const tr of el.querySelectorAll("tr[data-row-index]")) {
17747
- if (tr.offsetTop + tr.offsetHeight > top) return Number(tr.dataset.rowIndex) || 0;
17948
+ if (tr.offsetTop + rowBlockHeight(tr) > top) return Number(tr.dataset.rowIndex) || 0;
17748
17949
  }
17749
17950
  return 0;
17750
17951
  },
@@ -18072,7 +18273,8 @@ var STableImpl = /* @__PURE__ */ forwardRef(function STable({
18072
18273
  {
18073
18274
  value: headerCheck,
18074
18275
  disabled: !hasSelectableRow,
18075
- onValueChange: toggleAll
18276
+ onValueChange: toggleAll,
18277
+ ariaLabel: selectAllLabel
18076
18278
  }
18077
18279
  ) })
18078
18280
  }
@@ -18118,7 +18320,8 @@ var STableImpl = /* @__PURE__ */ forwardRef(function STable({
18118
18320
  {
18119
18321
  value: headerCheck,
18120
18322
  disabled: !hasSelectableRow,
18121
- onValueChange: toggleAll
18323
+ onValueChange: toggleAll,
18324
+ ariaLabel: selectAllLabel
18122
18325
  }
18123
18326
  ) }),
18124
18327
  /* @__PURE__ */ jsx(
@@ -18318,112 +18521,168 @@ var STableImpl = /* @__PURE__ */ forwardRef(function STable({
18318
18521
  const rowLocked = (selectable || dragSelectable) && !canSelectRow(row);
18319
18522
  const rowBg = "bg-white";
18320
18523
  const rowHover = hoverable && !rowLocked;
18321
- return /* @__PURE__ */ jsxs(
18322
- "tr",
18323
- {
18324
- "data-row-index": rowIndex,
18325
- onClick: () => onRowClick?.(row),
18326
- onDoubleClick: onRowDoubleClick ? (event) => {
18327
- const target = event.target;
18328
- if (target.closest(ROW_INTERACTIVE_SELECTOR)) return;
18329
- onRowDoubleClick(row);
18330
- } : void 0,
18331
- onMouseDown: dragSelectOn ? (event) => startDragSelect(rowIndex, event) : void 0,
18332
- onMouseEnter: dragSelectOn ? () => extendDragSelect(rowIndex) : void 0,
18333
- onContextMenu: contextMenuOn ? (event) => openContextMenu(row, event) : void 0,
18334
- className: cn(
18335
- "group/table-row",
18336
- rowHover && "hover:bg-[var(--color-grey-05)]",
18337
- (onRowClick || onRowDoubleClick) && "cursor-pointer",
18338
- dragSelectOn && "select-none",
18339
- rowClassName?.(row)
18340
- ),
18341
- style: { height: virtualScroll ? effectiveRowHeight : rowH },
18342
- children: [
18343
- selectable && /* @__PURE__ */ jsx(
18344
- "td",
18345
- {
18346
- className: cn(
18347
- // 원본 .td--selected: 48px 고정폭, 항상 left sticky, 체크박스 중앙정렬
18348
- "sticky left-0 z-[2] border-b border-solid border-[color:var(--cmp-table-border-color)] align-middle",
18524
+ const expansionShown = expansionOn && shownExpansionSet.has(row[rowKey]);
18525
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
18526
+ /* @__PURE__ */ jsxs(
18527
+ "tr",
18528
+ {
18529
+ "data-row-index": rowIndex,
18530
+ onClick: onRowClick ? (event) => {
18531
+ const target = event.target;
18532
+ if (target.closest(ROW_INTERACTIVE_SELECTOR)) return;
18533
+ onRowClick(row);
18534
+ } : void 0,
18535
+ onDoubleClick: onRowDoubleClick ? (event) => {
18536
+ const target = event.target;
18537
+ if (target.closest(ROW_INTERACTIVE_SELECTOR)) return;
18538
+ onRowDoubleClick(row);
18539
+ } : void 0,
18540
+ onMouseDown: dragSelectOn ? (event) => startDragSelect(rowIndex, event) : void 0,
18541
+ onMouseEnter: dragSelectOn ? () => extendDragSelect(rowIndex) : void 0,
18542
+ onContextMenu: contextMenuOn ? (event) => openContextMenu(row, event) : void 0,
18543
+ className: cn(
18544
+ "group/table-row",
18545
+ rowHover && "hover:bg-[var(--color-grey-05)]",
18546
+ (onRowClick || onRowDoubleClick) && "cursor-pointer",
18547
+ dragSelectOn && "select-none",
18548
+ rowClassName?.(row)
18549
+ ),
18550
+ style: { height: virtualScroll ? effectiveRowHeight : rowH },
18551
+ children: [
18552
+ selectable && /* @__PURE__ */ jsx(
18553
+ "td",
18554
+ {
18555
+ className: cn(
18556
+ // 원본 .td--selected: 48px 고정폭, 항상 left sticky, 체크박스 중앙정렬
18557
+ "sticky left-0 z-[2] border-b border-solid border-[color:var(--cmp-table-border-color)] align-middle",
18558
+ rowBg,
18559
+ rowHover && "group-hover/table-row:bg-[var(--color-grey-05)]",
18560
+ stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
18561
+ ),
18562
+ style: {
18563
+ width: SELECTABLE_COLUMN_WIDTH,
18564
+ minWidth: SELECTABLE_COLUMN_WIDTH,
18565
+ maxWidth: SELECTABLE_COLUMN_WIDTH,
18566
+ // 선택 열도 행의 일부다 — 여기만 희지 않으면 회색 줄이 끊긴다
18567
+ background: rowLocked ? LOCKED_BG : void 0
18568
+ },
18569
+ onClick: (e) => e.stopPropagation(),
18570
+ onDoubleClick: (e) => e.stopPropagation(),
18571
+ onClickCapture: (e) => {
18572
+ shiftPressedRef.current = e.shiftKey;
18573
+ if (e.shiftKey) window.getSelection()?.removeAllRanges();
18574
+ },
18575
+ children: /* @__PURE__ */ jsx("div", { className: "flex select-none items-center justify-center", children: /* @__PURE__ */ jsx(
18576
+ SCheckbox,
18577
+ {
18578
+ value: isSel || !isRowIndeterminate?.(row) ? isSel : null,
18579
+ disabled: !canSelectRow(row),
18580
+ onValueChange: () => toggleRow(row, rowOffset + ri),
18581
+ ariaLabel: rowSelectLabel?.(row) ?? "\uD589 \uC120\uD0DD"
18582
+ }
18583
+ ) })
18584
+ }
18585
+ ),
18586
+ visibleCols.map((col, i) => {
18587
+ const tdClassName = cn(
18588
+ // 원본 .td: 컬럼 폭에 맞춰 내용 말줄임(overflow hidden + ellipsis) + 행 구분선(border-b)
18589
+ // 셀 배경은 흰색으로 채워 뒤 배경이 비치지 않게 하고, hover 시 행 배경(grey_05)을 따른다.
18590
+ "overflow-hidden text-ellipsis whitespace-nowrap border-b border-solid border-[color:var(--cmp-table-border-color)] text-[12px] leading-[20px] text-[color:var(--sys-color-fg-primary)]",
18349
18591
  rowBg,
18350
18592
  rowHover && "group-hover/table-row:bg-[var(--color-grey-05)]",
18351
- stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
18352
- ),
18353
- style: {
18354
- width: SELECTABLE_COLUMN_WIDTH,
18355
- minWidth: SELECTABLE_COLUMN_WIDTH,
18356
- maxWidth: SELECTABLE_COLUMN_WIDTH,
18357
- // 선택 열도 행의 일부다 — 여기만 희지 않으면 회색 줄이 끊긴다
18358
- background: rowLocked ? LOCKED_BG : void 0
18359
- },
18360
- onClick: (e) => e.stopPropagation(),
18361
- onDoubleClick: (e) => e.stopPropagation(),
18362
- onClickCapture: (e) => {
18363
- shiftPressedRef.current = e.shiftKey;
18364
- if (e.shiftKey) window.getSelection()?.removeAllRanges();
18365
- },
18366
- children: /* @__PURE__ */ jsx("div", { className: "flex select-none items-center justify-center", children: /* @__PURE__ */ jsx(
18367
- SCheckbox,
18593
+ typeof col.tdClass === "function" ? col.tdClass(row) : col.tdClass
18594
+ );
18595
+ const tdPadX = cellPadXOf(col);
18596
+ const dragEdges = dragSel ? [
18597
+ dragTop && `inset 0 ${DRAG_EDGE_W} 0 0 ${DRAG_EDGE_C}`,
18598
+ dragBottom && `inset 0 ${DRAG_EDGE_NEG_W} 0 0 ${DRAG_EDGE_C}`,
18599
+ i === 0 && `inset ${DRAG_EDGE_W} 0 0 0 ${DRAG_EDGE_C}`,
18600
+ i === visibleCols.length - 1 && `inset ${DRAG_EDGE_NEG_W} 0 0 0 ${DRAG_EDGE_C}`
18601
+ ].filter(Boolean) : [];
18602
+ const shadows = [...dragEdges, stickyShadowValue(i)].filter(Boolean);
18603
+ const tdStyle = {
18604
+ paddingLeft: tdPadX,
18605
+ paddingRight: tdPadX,
18606
+ paddingTop: cellPadY,
18607
+ paddingBottom: cellPadY,
18608
+ textAlign: col.align ?? "left",
18609
+ // 인라인이라 hover 배경보다 세다 — 고른 행은 hover 로 흐려지지 않는다.
18610
+ background: dragSel ? DRAG_BG : rowLocked ? LOCKED_BG : void 0,
18611
+ // 클래스의 기본 글자색을 덮는다. 셀이 자기 색을 정한 내용(SIcon 의
18612
+ // color, STag 등)까지는 닿지 않는다 — 그건 소비 앱 몫이다.
18613
+ color: rowLocked ? LOCKED_CONTENT : void 0,
18614
+ boxShadow: shadows.length > 0 ? shadows.join(", ") : void 0,
18615
+ ...cellStyle(i)
18616
+ };
18617
+ if (col.renderCell) {
18618
+ const cell = col.renderCell({
18619
+ row,
18620
+ value: cellValue(col, row),
18621
+ column: col,
18622
+ colIndex: i,
18623
+ rowIndex: ri,
18624
+ isSelected: isSel,
18625
+ className: tdClassName,
18626
+ style: tdStyle
18627
+ });
18628
+ if (cell == null || cell === false) return null;
18629
+ return /* @__PURE__ */ jsx(Fragment$1, { children: cell }, col.name);
18630
+ }
18631
+ const content = col.render ? col.render(row, cellValue(col, row)) : cellValue(col, row);
18632
+ if (i === expansionColIndex) {
18633
+ return /* @__PURE__ */ jsx(
18634
+ "td",
18635
+ {
18636
+ className: cn(tdClassName, "cursor-pointer"),
18637
+ style: tdStyle,
18638
+ onClick: (event) => {
18639
+ if (isOwnControl(event.target, event.currentTarget)) return;
18640
+ event.stopPropagation();
18641
+ toggleExpanded(row);
18642
+ },
18643
+ onDoubleClick: (event) => event.stopPropagation(),
18644
+ children: /* @__PURE__ */ jsx(ExpansionToggleCell, { expanded: isRowExpanded(row), children: content })
18645
+ },
18646
+ col.name
18647
+ );
18648
+ }
18649
+ return /* @__PURE__ */ jsx("td", { className: tdClassName, style: tdStyle, children: content }, col.name);
18650
+ })
18651
+ ]
18652
+ }
18653
+ ),
18654
+ expansionShown && // 상세 영역 — 행이 아니므로 data-row-index 를 달지 않는다 (행 수·선택·실측 대상 밖)
18655
+ /* @__PURE__ */ jsx(
18656
+ "tr",
18657
+ {
18658
+ "data-expansion-row": "",
18659
+ "data-expansion-key": String(row[rowKey]),
18660
+ className: expandedSet.has(row[rowKey]) ? void 0 : "pointer-events-none",
18661
+ children: /* @__PURE__ */ jsx(
18662
+ "td",
18663
+ {
18664
+ colSpan: totalColSpan,
18665
+ className: "whitespace-normal border-b border-solid border-[color:var(--cmp-table-border-color)] bg-white p-0 text-[12px] leading-[20px] text-[color:var(--sys-color-fg-primary)]",
18666
+ children: /* @__PURE__ */ jsx(
18667
+ "div",
18368
18668
  {
18369
- value: isSel || !isRowIndeterminate?.(row) ? isSel : null,
18370
- disabled: !canSelectRow(row),
18371
- onValueChange: () => toggleRow(row, rowOffset + ri)
18669
+ className: "sticky left-0 box-border min-w-0",
18670
+ style: {
18671
+ // 가로 스크롤이 있어도 보이는 폭에 머문다. 재기 전(0)에는 표 폭을 따른다
18672
+ width: viewportWidth > 0 ? viewportWidth : void 0,
18673
+ paddingLeft: "var(--cmp-table-body-paddingX-wide)",
18674
+ paddingRight: "var(--cmp-table-body-paddingX-wide)",
18675
+ paddingTop: cellPadY,
18676
+ paddingBottom: cellPadY
18677
+ },
18678
+ children: renderExpansion?.(row)
18372
18679
  }
18373
- ) })
18374
- }
18375
- ),
18376
- visibleCols.map((col, i) => {
18377
- const tdClassName = cn(
18378
- // 원본 .td: 컬럼 폭에 맞춰 내용 말줄임(overflow hidden + ellipsis) + 행 구분선(border-b)
18379
- // 셀 배경은 흰색으로 채워 뒤 배경이 비치지 않게 하고, hover 시 행 배경(grey_05)을 따른다.
18380
- "overflow-hidden text-ellipsis whitespace-nowrap border-b border-solid border-[color:var(--cmp-table-border-color)] text-[12px] leading-[20px] text-[color:var(--sys-color-fg-primary)]",
18381
- rowBg,
18382
- rowHover && "group-hover/table-row:bg-[var(--color-grey-05)]",
18383
- typeof col.tdClass === "function" ? col.tdClass(row) : col.tdClass
18384
- );
18385
- const tdPadX = cellPadXOf(col);
18386
- const dragEdges = dragSel ? [
18387
- dragTop && `inset 0 ${DRAG_EDGE_W} 0 0 ${DRAG_EDGE_C}`,
18388
- dragBottom && `inset 0 ${DRAG_EDGE_NEG_W} 0 0 ${DRAG_EDGE_C}`,
18389
- i === 0 && `inset ${DRAG_EDGE_W} 0 0 0 ${DRAG_EDGE_C}`,
18390
- i === visibleCols.length - 1 && `inset ${DRAG_EDGE_NEG_W} 0 0 0 ${DRAG_EDGE_C}`
18391
- ].filter(Boolean) : [];
18392
- const shadows = [...dragEdges, stickyShadowValue(i)].filter(Boolean);
18393
- const tdStyle = {
18394
- paddingLeft: tdPadX,
18395
- paddingRight: tdPadX,
18396
- paddingTop: cellPadY,
18397
- paddingBottom: cellPadY,
18398
- textAlign: col.align ?? "left",
18399
- // 인라인이라 hover 배경보다 세다 — 고른 행은 hover 로 흐려지지 않는다.
18400
- background: dragSel ? DRAG_BG : rowLocked ? LOCKED_BG : void 0,
18401
- // 클래스의 기본 글자색을 덮는다. 셀이 자기 색을 정한 내용(SIcon 의
18402
- // color, STag 등)까지는 닿지 않는다 — 그건 소비 앱 몫이다.
18403
- color: rowLocked ? LOCKED_CONTENT : void 0,
18404
- boxShadow: shadows.length > 0 ? shadows.join(", ") : void 0,
18405
- ...cellStyle(i)
18406
- };
18407
- if (col.renderCell) {
18408
- const cell = col.renderCell({
18409
- row,
18410
- value: cellValue(col, row),
18411
- column: col,
18412
- colIndex: i,
18413
- rowIndex: ri,
18414
- isSelected: isSel,
18415
- className: tdClassName,
18416
- style: tdStyle
18417
- });
18418
- if (cell == null || cell === false) return null;
18419
- return /* @__PURE__ */ jsx(Fragment$1, { children: cell }, col.name);
18680
+ )
18420
18681
  }
18421
- return /* @__PURE__ */ jsx("td", { className: tdClassName, style: tdStyle, children: col.render ? col.render(row, cellValue(col, row)) : cellValue(col, row) }, col.name);
18422
- })
18423
- ]
18424
- },
18425
- row[rowKey] ?? ri
18426
- );
18682
+ )
18683
+ }
18684
+ )
18685
+ ] }, row[rowKey] ?? ri);
18427
18686
  }),
18428
18687
  virtualScroll && bottomSpacer > 0 && /* @__PURE__ */ jsx("tr", { "aria-hidden": true, style: { height: bottomSpacer }, children: /* @__PURE__ */ jsx("td", { colSpan: totalColSpan, className: "p-0" }) })
18429
18688
  ] })
@@ -18549,6 +18808,26 @@ var STableImpl = /* @__PURE__ */ forwardRef(function STable({
18549
18808
  );
18550
18809
  });
18551
18810
  var STable2 = STableImpl;
18811
+ function isOwnControl(target, cell) {
18812
+ if (!(target instanceof Element)) return false;
18813
+ if (target.closest("[data-expansion-toggle]")) return false;
18814
+ const control = target.closest(ROW_INTERACTIVE_SELECTOR);
18815
+ return control != null && cell.contains(control);
18816
+ }
18817
+ function ExpansionToggleCell({ expanded, children }) {
18818
+ return /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-(--cmp-tree-item-contentGap)", children: [
18819
+ /* @__PURE__ */ jsx("span", { "data-expansion-toggle": true, className: "flex shrink-0", children: /* @__PURE__ */ jsx(
18820
+ SGhostButton,
18821
+ {
18822
+ icon: expanded ? "caretDown" : "caretRight",
18823
+ size: "xxs",
18824
+ ariaLabel: expanded ? "collapse row" : "expand row",
18825
+ ariaExpanded: expanded
18826
+ }
18827
+ ) }),
18828
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children })
18829
+ ] });
18830
+ }
18552
18831
  var COUNT_NULL = "var(--cmp-table-bar-count-null)";
18553
18832
  var totalColor = (n) => n > 0 ? "var(--cmp-table-bar-count-total)" : COUNT_NULL;
18554
18833
  var selectedColor = (n) => n > 0 ? "var(--color-brilliantblue-75)" : COUNT_NULL;
@@ -18617,9 +18896,7 @@ var STableBar = /* @__PURE__ */ forwardRef(function STableBar2({ title, total, s
18617
18896
  }
18618
18897
  );
18619
18898
  });
18620
- var NO_KEYS = /* @__PURE__ */ new Set();
18621
- var EXPAND_MOTION = { duration: 220, easing: "ease-out" };
18622
- var COLLAPSE_MOTION = { duration: 140, easing: "ease-in" };
18899
+ var NO_KEYS2 = /* @__PURE__ */ new Set();
18623
18900
  function STreeTable({
18624
18901
  rows = [],
18625
18902
  columns = [],
@@ -18672,11 +18949,11 @@ function STreeTable({
18672
18949
  [rows, expandedSet, rowKey, childrenKey]
18673
18950
  );
18674
18951
  const tableRef = useRef(null);
18675
- const [closingKeys, setClosingKeys] = useState(NO_KEYS);
18952
+ const [closingKeys, setClosingKeys] = useState(NO_KEYS2);
18676
18953
  const [prevExpand, setPrevExpand] = useState({ rows, expandedSet });
18677
18954
  if (prevExpand.rows !== rows || prevExpand.expandedSet !== expandedSet) {
18678
18955
  setPrevExpand({ rows, expandedSet });
18679
- let nextClosing = NO_KEYS;
18956
+ let nextClosing = NO_KEYS2;
18680
18957
  if (prevExpand.rows === rows && motionAllowed()) {
18681
18958
  const collapsed = [...prevExpand.expandedSet].filter((key) => !expandedSet.has(key));
18682
18959
  const expanded = [...expandedSet].filter((key) => !prevExpand.expandedSet.has(key));
@@ -18725,11 +19002,15 @@ function STreeTable({
18725
19002
  motionRef.current = null;
18726
19003
  }
18727
19004
  const scrollEl = tableRef.current?.getScrollElement();
19005
+ const rowKeys = renderedRows.map(keyOf);
18728
19006
  const keys = added.size > 0 ? added : removed;
18729
19007
  const canAnimate = scrollEl != null && motionAllowed() && (added.size === 0 || removed.size === 0);
18730
19008
  const motion = canAnimate ? startRowMotion({
18731
19009
  scrollEl,
18732
- rowKeys: renderedRows.map(keyOf),
19010
+ isInBlock: (tr) => {
19011
+ const key = rowKeys[Number(tr.dataset.rowIndex)];
19012
+ return key !== void 0 && keys.has(key);
19013
+ },
18733
19014
  blockKeys: keys,
18734
19015
  from,
18735
19016
  to: added.size > 0 ? 1 : 0
@@ -18740,11 +19021,11 @@ function STreeTable({
18740
19021
  void motion.finished.then((completed) => {
18741
19022
  if (!completed || motionRef.current !== motion) return;
18742
19023
  motion.done = true;
18743
- setClosingKeys(NO_KEYS);
19024
+ setClosingKeys(NO_KEYS2);
18744
19025
  });
18745
19026
  }
18746
19027
  } else if (closingKeys.size > 0) {
18747
- setClosingKeys(NO_KEYS);
19028
+ setClosingKeys(NO_KEYS2);
18748
19029
  }
18749
19030
  }, [renderedRows]);
18750
19031
  useEffect(() => () => motionRef.current?.cancel(), []);
@@ -18817,7 +19098,7 @@ function STreeTable({
18817
19098
  className: cn(className, hasChildren && "cursor-pointer"),
18818
19099
  style,
18819
19100
  onClick: hasChildren ? (event) => {
18820
- if (isOwnControl(event.target, event.currentTarget)) return;
19101
+ if (isOwnControl2(event.target, event.currentTarget)) return;
18821
19102
  event.stopPropagation();
18822
19103
  toggleExpanded(row);
18823
19104
  } : void 0,
@@ -18872,96 +19153,8 @@ function flattenRows(nodes, isOpen, childrenOf) {
18872
19153
  walk(nodes);
18873
19154
  return out;
18874
19155
  }
18875
- function sameKeys(a, b) {
18876
- return a.size === b.size && [...a].every((key) => b.has(key));
18877
- }
18878
- function motionAllowed() {
18879
- if (typeof window === "undefined" || typeof Element.prototype.animate !== "function")
18880
- return false;
18881
- return !window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
18882
- }
18883
- function startRowMotion({
18884
- scrollEl,
18885
- rowKeys,
18886
- blockKeys,
18887
- from,
18888
- to
18889
- }) {
18890
- const spec = to === 1 ? EXPAND_MOTION : COLLAPSE_MOTION;
18891
- const duration = spec.duration * Math.abs(to - from);
18892
- if (duration < 1) return null;
18893
- const trs = [...scrollEl.querySelectorAll("tr[data-row-index]")].map((tr) => {
18894
- const key = rowKeys[Number(tr.dataset.rowIndex)];
18895
- return {
18896
- tr,
18897
- inBlock: key !== void 0 && blockKeys.has(key),
18898
- height: tr.getBoundingClientRect().height
18899
- };
18900
- });
18901
- const viewBottom = scrollEl.scrollTop + scrollEl.clientHeight;
18902
- const openAt = (offset) => from + (to - from) * offset;
18903
- const translate = (shift2, s) => `translateY(${-shift2 * (1 - s)}px)`;
18904
- const timing = {
18905
- duration,
18906
- easing: spec.easing,
18907
- // 접힘은 남겨 둔 행이 빠질 때까지 끝 위치를 붙잡는다 — 놓으면 그 사이 한 프레임 제자리로 튄다
18908
- fill: to === 0 ? "forwards" : "none"
18909
- };
18910
- const animations = [];
18911
- let shift = 0;
18912
- let blockHeight = 0;
18913
- let y = 0;
18914
- trs.forEach(({ tr, inBlock, height }, i) => {
18915
- if (inBlock && !trs[i - 1]?.inBlock) {
18916
- blockHeight = 0;
18917
- for (let j = i; j < trs.length && trs[j].inBlock; j++) blockHeight += trs[j].height;
18918
- shift += blockHeight;
18919
- y = 0;
18920
- }
18921
- if (shift === 0 || tr.offsetTop - shift > viewBottom) return;
18922
- if (!inBlock) {
18923
- animations.push(
18924
- tr.animate(
18925
- [{ transform: translate(shift, from) }, { transform: translate(shift, to) }],
18926
- timing
18927
- )
18928
- );
18929
- return;
18930
- }
18931
- const rowY = y;
18932
- const clipAt = (s) => Math.min(height, Math.max(0, blockHeight * (1 - s) - rowY));
18933
- const bends = [1 - rowY / blockHeight, 1 - (rowY + height) / blockHeight].map((s) => (s - from) / (to - from)).filter((offset) => offset > 0 && offset < 1);
18934
- const offsets = [0, ...bends, 1].sort((a, b) => a - b);
18935
- animations.push(
18936
- tr.animate(
18937
- offsets.map((offset) => ({
18938
- offset,
18939
- transform: translate(shift, openAt(offset)),
18940
- clipPath: `inset(${clipAt(openAt(offset))}px 0 0 0)`
18941
- })),
18942
- timing
18943
- )
18944
- );
18945
- y += height;
18946
- });
18947
- if (animations.length === 0) return null;
18948
- return {
18949
- keys: blockKeys,
18950
- to,
18951
- done: false,
18952
- finished: Promise.all(animations.map((animation) => animation.finished)).then(
18953
- () => true,
18954
- () => false
18955
- ),
18956
- openness: () => {
18957
- const progress = animations[0].effect?.getComputedTiming().progress;
18958
- return progress == null ? to : openAt(progress);
18959
- },
18960
- cancel: () => animations.forEach((animation) => animation.cancel())
18961
- };
18962
- }
18963
19156
  var OWN_CONTROL_SELECTOR = 'a, button, input, select, textarea, label, [role="button"], [role="checkbox"], [contenteditable="true"]';
18964
- function isOwnControl(target, cell) {
19157
+ function isOwnControl2(target, cell) {
18965
19158
  if (!(target instanceof Element)) return false;
18966
19159
  if (target.closest("[data-tree-toggle]")) return false;
18967
19160
  const control = target.closest(OWN_CONTROL_SELECTOR);