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

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
@@ -15686,6 +15686,93 @@ function SCalendarBoard({
15686
15686
  );
15687
15687
  }
15688
15688
 
15689
+ // src/lib/row-motion.ts
15690
+ var EXPAND_MOTION = { duration: 220, easing: "ease-out" };
15691
+ var COLLAPSE_MOTION = { duration: 140, easing: "ease-in" };
15692
+ function sameKeys(a, b) {
15693
+ return a.size === b.size && [...a].every((key) => b.has(key));
15694
+ }
15695
+ function motionAllowed() {
15696
+ if (typeof window === "undefined" || typeof Element.prototype.animate !== "function")
15697
+ return false;
15698
+ return !window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
15699
+ }
15700
+ function startRowMotion({
15701
+ scrollEl,
15702
+ isInBlock,
15703
+ blockKeys,
15704
+ from,
15705
+ to
15706
+ }) {
15707
+ const spec = to === 1 ? EXPAND_MOTION : COLLAPSE_MOTION;
15708
+ const duration = spec.duration * Math.abs(to - from);
15709
+ if (duration < 1) return null;
15710
+ const trs = [
15711
+ ...scrollEl.querySelectorAll("tr[data-row-index], tr[data-expansion-row]")
15712
+ ].map((tr) => ({ tr, inBlock: isInBlock(tr), height: tr.getBoundingClientRect().height }));
15713
+ const viewBottom = scrollEl.scrollTop + scrollEl.clientHeight;
15714
+ const openAt = (offset) => from + (to - from) * offset;
15715
+ const translate = (shift2, s) => `translateY(${-shift2 * (1 - s)}px)`;
15716
+ const timing = {
15717
+ duration,
15718
+ easing: spec.easing,
15719
+ // 접힘은 남겨 둔 행이 빠질 때까지 끝 위치를 붙잡는다 — 놓으면 그 사이 한 프레임 제자리로 튄다
15720
+ fill: to === 0 ? "forwards" : "none"
15721
+ };
15722
+ const animations = [];
15723
+ let shift = 0;
15724
+ let blockHeight = 0;
15725
+ let y = 0;
15726
+ trs.forEach(({ tr, inBlock, height }, i) => {
15727
+ if (inBlock && !trs[i - 1]?.inBlock) {
15728
+ blockHeight = 0;
15729
+ for (let j = i; j < trs.length && trs[j].inBlock; j++) blockHeight += trs[j].height;
15730
+ shift += blockHeight;
15731
+ y = 0;
15732
+ }
15733
+ if (shift === 0 || tr.offsetTop - shift > viewBottom) return;
15734
+ if (!inBlock) {
15735
+ animations.push(
15736
+ tr.animate(
15737
+ [{ transform: translate(shift, from) }, { transform: translate(shift, to) }],
15738
+ timing
15739
+ )
15740
+ );
15741
+ return;
15742
+ }
15743
+ const rowY = y;
15744
+ const clipAt = (s) => Math.min(height, Math.max(0, blockHeight * (1 - s) - rowY));
15745
+ const bends = [1 - rowY / blockHeight, 1 - (rowY + height) / blockHeight].map((s) => (s - from) / (to - from)).filter((offset) => offset > 0 && offset < 1);
15746
+ const offsets = [0, ...bends, 1].sort((a, b) => a - b);
15747
+ animations.push(
15748
+ tr.animate(
15749
+ offsets.map((offset) => ({
15750
+ offset,
15751
+ transform: translate(shift, openAt(offset)),
15752
+ clipPath: `inset(${clipAt(openAt(offset))}px 0 0 0)`
15753
+ })),
15754
+ timing
15755
+ )
15756
+ );
15757
+ y += height;
15758
+ });
15759
+ if (animations.length === 0) return null;
15760
+ return {
15761
+ keys: blockKeys,
15762
+ to,
15763
+ done: false,
15764
+ finished: Promise.all(animations.map((animation) => animation.finished)).then(
15765
+ () => true,
15766
+ () => false
15767
+ ),
15768
+ openness: () => {
15769
+ const progress = animations[0].effect?.getComputedTiming().progress;
15770
+ return progress == null ? to : openAt(progress);
15771
+ },
15772
+ cancel: () => animations.forEach((animation) => animation.cancel())
15773
+ };
15774
+ }
15775
+
15689
15776
  // src/lib/row-metrics.ts
15690
15777
  function createRowMetrics(count, estimate) {
15691
15778
  const safeCount = Math.max(0, Math.floor(count));
@@ -16105,6 +16192,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
16105
16192
  onSearchChange,
16106
16193
  searchDebounce = DEFAULT_SEARCH_DEBOUNCE,
16107
16194
  label,
16195
+ ariaLabel,
16108
16196
  labelPosition,
16109
16197
  subLabel,
16110
16198
  labelWidth,
@@ -16807,6 +16895,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
16807
16895
  style,
16808
16896
  children: [
16809
16897
  name && /* @__PURE__ */ jsxRuntime.jsx("input", { type: "hidden", name, value: selected.join(",") }),
16898
+ !label && ariaLabel && /* @__PURE__ */ jsxRuntime.jsx("span", { id: `${triggerId}-aria-label`, hidden: true, children: ariaLabel }),
16810
16899
  /* @__PURE__ */ jsxRuntime.jsxs(
16811
16900
  "div",
16812
16901
  {
@@ -16815,7 +16904,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
16815
16904
  role: "combobox",
16816
16905
  tabIndex: effectiveDisabled ? -1 : 0,
16817
16906
  "aria-disabled": effectiveDisabled || void 0,
16818
- "aria-labelledby": label ? `${fieldLabelId(triggerId)} ${triggerId}` : triggerId,
16907
+ "aria-labelledby": label ? `${fieldLabelId(triggerId)} ${triggerId}` : ariaLabel ? `${triggerId}-aria-label ${triggerId}` : triggerId,
16819
16908
  "aria-haspopup": isTree ? "tree" : "listbox",
16820
16909
  "aria-expanded": open,
16821
16910
  "aria-controls": open ? listboxId : void 0,
@@ -17423,6 +17512,7 @@ function CheckboxBox({
17423
17512
  }
17424
17513
  );
17425
17514
  }
17515
+ var NO_KEYS = /* @__PURE__ */ new Set();
17426
17516
  var DEFAULT_ROW_HEIGHT = 44;
17427
17517
  var DENSE_ROW_HEIGHT = 32;
17428
17518
  var SELECTABLE_COLUMN_WIDTH = 48;
@@ -17453,6 +17543,11 @@ var CONTEXT_MENU_ITEM_CLASS = cn(
17453
17543
  "data-[highlighted]:bg-[var(--sys-color-bg-accent)] data-[highlighted]:text-[color:var(--sys-color-fg-inverse)]"
17454
17544
  );
17455
17545
  var ROW_INTERACTIVE_SELECTOR = 'button, a, input, select, textarea, label, [role="button"], [role="checkbox"], [contenteditable="true"]';
17546
+ function rowBlockHeight(tr) {
17547
+ const next = tr.nextElementSibling;
17548
+ const expansion = next instanceof HTMLElement && next.dataset.expansionRow != null ? next.offsetHeight : 0;
17549
+ return tr.offsetHeight + expansion;
17550
+ }
17456
17551
  var colWidthFromDef = (c) => {
17457
17552
  if (c.autoWidth) return 0;
17458
17553
  if (c.width == null) return DEFAULT_COLUMN_WIDTH;
@@ -17465,6 +17560,8 @@ var STableImpl = /* @__PURE__ */ react.forwardRef(function STable({
17465
17560
  rows = [],
17466
17561
  rowKey = "id",
17467
17562
  selectable = false,
17563
+ rowSelectLabel,
17564
+ selectAllLabel = "\uC804\uCCB4 \uC120\uD0DD",
17468
17565
  dragSelectable = false,
17469
17566
  contextMenuItems,
17470
17567
  onContextMenuItemClick,
@@ -17503,6 +17600,11 @@ var STableImpl = /* @__PURE__ */ react.forwardRef(function STable({
17503
17600
  tableId,
17504
17601
  onRowClick,
17505
17602
  onRowDoubleClick,
17603
+ renderExpansion,
17604
+ expansionColumn,
17605
+ expandedKeys,
17606
+ defaultExpandedKeys,
17607
+ onExpandedChange,
17506
17608
  className,
17507
17609
  style
17508
17610
  }, ref) {
@@ -17517,6 +17619,44 @@ var STableImpl = /* @__PURE__ */ react.forwardRef(function STable({
17517
17619
  onDenseChange?.(next);
17518
17620
  };
17519
17621
  const visibleCols = react.useMemo(() => columns.filter((c) => c.visible !== false), [columns]);
17622
+ const expansionOn = renderExpansion != null;
17623
+ const [innerExpandedKeys, setInnerExpandedKeys] = react.useState(
17624
+ () => defaultExpandedKeys ?? []
17625
+ );
17626
+ const currentExpandedKeys = expandedKeys ?? innerExpandedKeys;
17627
+ const expandedSet = react.useMemo(() => new Set(currentExpandedKeys), [currentExpandedKeys]);
17628
+ const isRowExpanded = (row) => expansionOn && expandedSet.has(row[rowKey]);
17629
+ const [closingKeys, setClosingKeys] = react.useState(NO_KEYS);
17630
+ const [prevExpandedSet, setPrevExpandedSet] = react.useState(expandedSet);
17631
+ if (prevExpandedSet !== expandedSet) {
17632
+ setPrevExpandedSet(expandedSet);
17633
+ let nextClosing = NO_KEYS;
17634
+ if (expansionOn && motionAllowed()) {
17635
+ const collapsed = [...prevExpandedSet].filter((key) => !expandedSet.has(key));
17636
+ const opened = [...expandedSet].filter((key) => !prevExpandedSet.has(key));
17637
+ if (collapsed.length > 0 && opened.length === 0) nextClosing = new Set(collapsed);
17638
+ }
17639
+ if (nextClosing !== closingKeys) setClosingKeys(nextClosing);
17640
+ }
17641
+ const shownExpansionSet = react.useMemo(
17642
+ () => closingKeys.size === 0 ? expandedSet : /* @__PURE__ */ new Set([...expandedSet, ...closingKeys]),
17643
+ [expandedSet, closingKeys]
17644
+ );
17645
+ const toggleExpanded = (row) => {
17646
+ const key = row[rowKey];
17647
+ const next = expandedSet.has(key) ? currentExpandedKeys.filter((k) => k !== key) : [...currentExpandedKeys, key];
17648
+ if (expandedKeys === void 0) setInnerExpandedKeys(next);
17649
+ onExpandedChange?.(next, row);
17650
+ };
17651
+ const expansionColIndex = !expansionOn ? -1 : expansionColumn == null ? 0 : visibleCols.findIndex((c) => c.name === expansionColumn);
17652
+ if (expansionColIndex >= 0) {
17653
+ const col = visibleCols[expansionColIndex];
17654
+ if (col?.renderCell) {
17655
+ throw new Error(
17656
+ `[STable] \uD3BC\uCE68 \uCEEC\uB7FC(${col.name})\uC5D0\uB294 renderCell \uC744 \uC4F8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. render \uB97C \uC4F0\uC138\uC694.`
17657
+ );
17658
+ }
17659
+ }
17520
17660
  const headerGroupInfo = react.useMemo(() => {
17521
17661
  if (!headerGroups || headerGroups.length === 0) return null;
17522
17662
  const indexByName = new Map(visibleCols.map((col, index) => [col.name, index]));
@@ -17621,6 +17761,7 @@ var STableImpl = /* @__PURE__ */ react.forwardRef(function STable({
17621
17761
  metricsRef.current ?? (metricsRef.current = createRowMetrics(0, effectiveRowHeight));
17622
17762
  const metrics = metricsRef.current;
17623
17763
  const measuredKeyRef = react.useRef("");
17764
+ const measuredExpansionRef = react.useRef(shownExpansionSet);
17624
17765
  const pendingRowRef = react.useRef(null);
17625
17766
  const pinnedTopRef = react.useRef(-1);
17626
17767
  const lastEstimateRef = react.useRef(effectiveRowHeight);
@@ -17634,6 +17775,7 @@ var STableImpl = /* @__PURE__ */ react.forwardRef(function STable({
17634
17775
  }
17635
17776
  const [scrolledLeft, setScrolledLeft] = react.useState(false);
17636
17777
  const [scrolledRight, setScrolledRight] = react.useState(false);
17778
+ const [viewportWidth, setViewportWidth] = react.useState(0);
17637
17779
  const setRowCount = react.useCallback((count) => {
17638
17780
  const safe = Math.max(0, Math.floor(Number.isFinite(count) ? count : 0));
17639
17781
  lastReachEndRef.current = -1;
@@ -17659,6 +17801,7 @@ var STableImpl = /* @__PURE__ */ react.forwardRef(function STable({
17659
17801
  setHeaderHeight(headerH);
17660
17802
  setScrolledLeft(el.scrollLeft > 0);
17661
17803
  setScrolledRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 1);
17804
+ setViewportWidth(el.clientWidth);
17662
17805
  if (virtualScroll) {
17663
17806
  const { start, end } = calcRange(el.scrollTop, el.clientHeight - headerH);
17664
17807
  setVsRange((prev) => prev.start === start && prev.end === end ? prev : { start, end });
@@ -17676,6 +17819,13 @@ var STableImpl = /* @__PURE__ */ react.forwardRef(function STable({
17676
17819
  react.useEffect(() => {
17677
17820
  handleScroll();
17678
17821
  }, [handleScroll, rowCount, columnWidths, loading2, height]);
17822
+ react.useEffect(() => {
17823
+ const el = scrollRef.current;
17824
+ if (!expansionOn || !el || typeof ResizeObserver === "undefined") return;
17825
+ const observer = new ResizeObserver(() => handleScroll());
17826
+ observer.observe(el);
17827
+ return () => observer.disconnect();
17828
+ }, [expansionOn, handleScroll]);
17679
17829
  const handleScrollRef = react.useRef(handleScroll);
17680
17830
  handleScrollRef.current = handleScroll;
17681
17831
  useIsomorphicLayoutEffect(() => {
@@ -17685,9 +17835,10 @@ var STableImpl = /* @__PURE__ */ react.forwardRef(function STable({
17685
17835
  if (!el || m == null) return;
17686
17836
  const pending2 = pendingRowRef.current;
17687
17837
  const key = `${vsRange.start}:${vsRange.end}:${rowCount}`;
17688
- const alreadyMeasured = measuredKeyRef.current === key;
17838
+ const alreadyMeasured = measuredKeyRef.current === key && measuredExpansionRef.current === shownExpansionSet;
17689
17839
  if (alreadyMeasured && pending2 == null) return;
17690
17840
  measuredKeyRef.current = key;
17841
+ measuredExpansionRef.current = shownExpansionSet;
17691
17842
  const anchor = vsRange.start;
17692
17843
  const anchorBefore = offsetOf(m, anchor);
17693
17844
  let changed = false;
@@ -17695,7 +17846,7 @@ var STableImpl = /* @__PURE__ */ react.forwardRef(function STable({
17695
17846
  for (const tr of el.querySelectorAll("tr[data-row-index]")) {
17696
17847
  const index = Number(tr.dataset.rowIndex);
17697
17848
  if (!Number.isInteger(index)) continue;
17698
- const h = tr.offsetHeight;
17849
+ const h = rowBlockHeight(tr);
17699
17850
  if (h <= 0) continue;
17700
17851
  if (setRowSize(m, index, h)) changed = true;
17701
17852
  }
@@ -17717,6 +17868,54 @@ var STableImpl = /* @__PURE__ */ react.forwardRef(function STable({
17717
17868
  if (delta !== 0 && el.scrollTop > 0) el.scrollTop += delta;
17718
17869
  handleScrollRef.current();
17719
17870
  });
17871
+ const expansionMotionRef = react.useRef(null);
17872
+ const prevExpansionRef = react.useRef(null);
17873
+ useIsomorphicLayoutEffect(() => {
17874
+ if (!expansionOn) return;
17875
+ const prev = prevExpansionRef.current;
17876
+ prevExpansionRef.current = { rows, keys: expandedSet };
17877
+ const running = expansionMotionRef.current;
17878
+ if (running?.done) {
17879
+ running.cancel();
17880
+ expansionMotionRef.current = null;
17881
+ }
17882
+ if (!prev || prev.keys === expandedSet) return;
17883
+ const added = new Set([...expandedSet].filter((key) => !prev.keys.has(key)));
17884
+ const removed = new Set([...prev.keys].filter((key) => !expandedSet.has(key)));
17885
+ if (added.size === 0 && removed.size === 0) return;
17886
+ let from = added.size > 0 ? 0 : 1;
17887
+ if (expansionMotionRef.current) {
17888
+ const current = expansionMotionRef.current;
17889
+ const reversing = current.to === 0 && sameKeys(added, current.keys) || current.to === 1 && sameKeys(removed, current.keys);
17890
+ if (reversing) from = current.openness();
17891
+ current.cancel();
17892
+ expansionMotionRef.current = null;
17893
+ }
17894
+ const scrollEl = scrollRef.current;
17895
+ const keys = added.size > 0 ? added : removed;
17896
+ const keyNames = new Set([...keys].map(String));
17897
+ const canAnimate = scrollEl != null && prev.rows === rows && motionAllowed() && (added.size === 0 || removed.size === 0);
17898
+ const motion = canAnimate ? startRowMotion({
17899
+ scrollEl,
17900
+ isInBlock: (tr) => tr.dataset.expansionRow != null && keyNames.has(tr.dataset.expansionKey ?? ""),
17901
+ blockKeys: keys,
17902
+ from,
17903
+ to: added.size > 0 ? 1 : 0
17904
+ }) : null;
17905
+ if (motion) {
17906
+ expansionMotionRef.current = motion;
17907
+ if (motion.to === 0) {
17908
+ void motion.finished.then((completed) => {
17909
+ if (!completed || expansionMotionRef.current !== motion) return;
17910
+ motion.done = true;
17911
+ setClosingKeys(NO_KEYS);
17912
+ });
17913
+ }
17914
+ } else if (closingKeys.size > 0) {
17915
+ setClosingKeys(NO_KEYS);
17916
+ }
17917
+ }, [expandedSet, closingKeys]);
17918
+ react.useEffect(() => () => expansionMotionRef.current?.cancel(), []);
17720
17919
  react.useEffect(() => {
17721
17920
  if (!virtualScroll) return;
17722
17921
  cbRef.current.onVirtualUpdate?.({ from: vsRange.start, to: vsRange.end });
@@ -17773,7 +17972,7 @@ var STableImpl = /* @__PURE__ */ react.forwardRef(function STable({
17773
17972
  if (virtualScroll && m != null) return indexAtOffset(m, el.scrollTop);
17774
17973
  const top = el.scrollTop;
17775
17974
  for (const tr of el.querySelectorAll("tr[data-row-index]")) {
17776
- if (tr.offsetTop + tr.offsetHeight > top) return Number(tr.dataset.rowIndex) || 0;
17975
+ if (tr.offsetTop + rowBlockHeight(tr) > top) return Number(tr.dataset.rowIndex) || 0;
17777
17976
  }
17778
17977
  return 0;
17779
17978
  },
@@ -18101,7 +18300,8 @@ var STableImpl = /* @__PURE__ */ react.forwardRef(function STable({
18101
18300
  {
18102
18301
  value: headerCheck,
18103
18302
  disabled: !hasSelectableRow,
18104
- onValueChange: toggleAll
18303
+ onValueChange: toggleAll,
18304
+ ariaLabel: selectAllLabel
18105
18305
  }
18106
18306
  ) })
18107
18307
  }
@@ -18147,7 +18347,8 @@ var STableImpl = /* @__PURE__ */ react.forwardRef(function STable({
18147
18347
  {
18148
18348
  value: headerCheck,
18149
18349
  disabled: !hasSelectableRow,
18150
- onValueChange: toggleAll
18350
+ onValueChange: toggleAll,
18351
+ ariaLabel: selectAllLabel
18151
18352
  }
18152
18353
  ) }),
18153
18354
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -18347,112 +18548,168 @@ var STableImpl = /* @__PURE__ */ react.forwardRef(function STable({
18347
18548
  const rowLocked = (selectable || dragSelectable) && !canSelectRow(row);
18348
18549
  const rowBg = "bg-white";
18349
18550
  const rowHover = hoverable && !rowLocked;
18350
- return /* @__PURE__ */ jsxRuntime.jsxs(
18351
- "tr",
18352
- {
18353
- "data-row-index": rowIndex,
18354
- onClick: () => onRowClick?.(row),
18355
- onDoubleClick: onRowDoubleClick ? (event) => {
18356
- const target = event.target;
18357
- if (target.closest(ROW_INTERACTIVE_SELECTOR)) return;
18358
- onRowDoubleClick(row);
18359
- } : void 0,
18360
- onMouseDown: dragSelectOn ? (event) => startDragSelect(rowIndex, event) : void 0,
18361
- onMouseEnter: dragSelectOn ? () => extendDragSelect(rowIndex) : void 0,
18362
- onContextMenu: contextMenuOn ? (event) => openContextMenu(row, event) : void 0,
18363
- className: cn(
18364
- "group/table-row",
18365
- rowHover && "hover:bg-[var(--color-grey-05)]",
18366
- (onRowClick || onRowDoubleClick) && "cursor-pointer",
18367
- dragSelectOn && "select-none",
18368
- rowClassName?.(row)
18369
- ),
18370
- style: { height: virtualScroll ? effectiveRowHeight : rowH },
18371
- children: [
18372
- selectable && /* @__PURE__ */ jsxRuntime.jsx(
18373
- "td",
18374
- {
18375
- className: cn(
18376
- // 원본 .td--selected: 48px 고정폭, 항상 left sticky, 체크박스 중앙정렬
18377
- "sticky left-0 z-[2] border-b border-solid border-[color:var(--cmp-table-border-color)] align-middle",
18551
+ const expansionShown = expansionOn && shownExpansionSet.has(row[rowKey]);
18552
+ return /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
18553
+ /* @__PURE__ */ jsxRuntime.jsxs(
18554
+ "tr",
18555
+ {
18556
+ "data-row-index": rowIndex,
18557
+ onClick: onRowClick ? (event) => {
18558
+ const target = event.target;
18559
+ if (target.closest(ROW_INTERACTIVE_SELECTOR)) return;
18560
+ onRowClick(row);
18561
+ } : void 0,
18562
+ onDoubleClick: onRowDoubleClick ? (event) => {
18563
+ const target = event.target;
18564
+ if (target.closest(ROW_INTERACTIVE_SELECTOR)) return;
18565
+ onRowDoubleClick(row);
18566
+ } : void 0,
18567
+ onMouseDown: dragSelectOn ? (event) => startDragSelect(rowIndex, event) : void 0,
18568
+ onMouseEnter: dragSelectOn ? () => extendDragSelect(rowIndex) : void 0,
18569
+ onContextMenu: contextMenuOn ? (event) => openContextMenu(row, event) : void 0,
18570
+ className: cn(
18571
+ "group/table-row",
18572
+ rowHover && "hover:bg-[var(--color-grey-05)]",
18573
+ (onRowClick || onRowDoubleClick) && "cursor-pointer",
18574
+ dragSelectOn && "select-none",
18575
+ rowClassName?.(row)
18576
+ ),
18577
+ style: { height: virtualScroll ? effectiveRowHeight : rowH },
18578
+ children: [
18579
+ selectable && /* @__PURE__ */ jsxRuntime.jsx(
18580
+ "td",
18581
+ {
18582
+ className: cn(
18583
+ // 원본 .td--selected: 48px 고정폭, 항상 left sticky, 체크박스 중앙정렬
18584
+ "sticky left-0 z-[2] border-b border-solid border-[color:var(--cmp-table-border-color)] align-middle",
18585
+ rowBg,
18586
+ rowHover && "group-hover/table-row:bg-[var(--color-grey-05)]",
18587
+ stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
18588
+ ),
18589
+ style: {
18590
+ width: SELECTABLE_COLUMN_WIDTH,
18591
+ minWidth: SELECTABLE_COLUMN_WIDTH,
18592
+ maxWidth: SELECTABLE_COLUMN_WIDTH,
18593
+ // 선택 열도 행의 일부다 — 여기만 희지 않으면 회색 줄이 끊긴다
18594
+ background: rowLocked ? LOCKED_BG : void 0
18595
+ },
18596
+ onClick: (e) => e.stopPropagation(),
18597
+ onDoubleClick: (e) => e.stopPropagation(),
18598
+ onClickCapture: (e) => {
18599
+ shiftPressedRef.current = e.shiftKey;
18600
+ if (e.shiftKey) window.getSelection()?.removeAllRanges();
18601
+ },
18602
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex select-none items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(
18603
+ SCheckbox,
18604
+ {
18605
+ value: isSel || !isRowIndeterminate?.(row) ? isSel : null,
18606
+ disabled: !canSelectRow(row),
18607
+ onValueChange: () => toggleRow(row, rowOffset + ri),
18608
+ ariaLabel: rowSelectLabel?.(row) ?? "\uD589 \uC120\uD0DD"
18609
+ }
18610
+ ) })
18611
+ }
18612
+ ),
18613
+ visibleCols.map((col, i) => {
18614
+ const tdClassName = cn(
18615
+ // 원본 .td: 컬럼 폭에 맞춰 내용 말줄임(overflow hidden + ellipsis) + 행 구분선(border-b)
18616
+ // 셀 배경은 흰색으로 채워 뒤 배경이 비치지 않게 하고, hover 시 행 배경(grey_05)을 따른다.
18617
+ "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)]",
18378
18618
  rowBg,
18379
18619
  rowHover && "group-hover/table-row:bg-[var(--color-grey-05)]",
18380
- stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
18381
- ),
18382
- style: {
18383
- width: SELECTABLE_COLUMN_WIDTH,
18384
- minWidth: SELECTABLE_COLUMN_WIDTH,
18385
- maxWidth: SELECTABLE_COLUMN_WIDTH,
18386
- // 선택 열도 행의 일부다 — 여기만 희지 않으면 회색 줄이 끊긴다
18387
- background: rowLocked ? LOCKED_BG : void 0
18388
- },
18389
- onClick: (e) => e.stopPropagation(),
18390
- onDoubleClick: (e) => e.stopPropagation(),
18391
- onClickCapture: (e) => {
18392
- shiftPressedRef.current = e.shiftKey;
18393
- if (e.shiftKey) window.getSelection()?.removeAllRanges();
18394
- },
18395
- children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex select-none items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(
18396
- SCheckbox,
18620
+ typeof col.tdClass === "function" ? col.tdClass(row) : col.tdClass
18621
+ );
18622
+ const tdPadX = cellPadXOf(col);
18623
+ const dragEdges = dragSel ? [
18624
+ dragTop && `inset 0 ${DRAG_EDGE_W} 0 0 ${DRAG_EDGE_C}`,
18625
+ dragBottom && `inset 0 ${DRAG_EDGE_NEG_W} 0 0 ${DRAG_EDGE_C}`,
18626
+ i === 0 && `inset ${DRAG_EDGE_W} 0 0 0 ${DRAG_EDGE_C}`,
18627
+ i === visibleCols.length - 1 && `inset ${DRAG_EDGE_NEG_W} 0 0 0 ${DRAG_EDGE_C}`
18628
+ ].filter(Boolean) : [];
18629
+ const shadows = [...dragEdges, stickyShadowValue(i)].filter(Boolean);
18630
+ const tdStyle = {
18631
+ paddingLeft: tdPadX,
18632
+ paddingRight: tdPadX,
18633
+ paddingTop: cellPadY,
18634
+ paddingBottom: cellPadY,
18635
+ textAlign: col.align ?? "left",
18636
+ // 인라인이라 hover 배경보다 세다 — 고른 행은 hover 로 흐려지지 않는다.
18637
+ background: dragSel ? DRAG_BG : rowLocked ? LOCKED_BG : void 0,
18638
+ // 클래스의 기본 글자색을 덮는다. 셀이 자기 색을 정한 내용(SIcon 의
18639
+ // color, STag 등)까지는 닿지 않는다 — 그건 소비 앱 몫이다.
18640
+ color: rowLocked ? LOCKED_CONTENT : void 0,
18641
+ boxShadow: shadows.length > 0 ? shadows.join(", ") : void 0,
18642
+ ...cellStyle(i)
18643
+ };
18644
+ if (col.renderCell) {
18645
+ const cell = col.renderCell({
18646
+ row,
18647
+ value: cellValue(col, row),
18648
+ column: col,
18649
+ colIndex: i,
18650
+ rowIndex: ri,
18651
+ isSelected: isSel,
18652
+ className: tdClassName,
18653
+ style: tdStyle
18654
+ });
18655
+ if (cell == null || cell === false) return null;
18656
+ return /* @__PURE__ */ jsxRuntime.jsx(react.Fragment, { children: cell }, col.name);
18657
+ }
18658
+ const content = col.render ? col.render(row, cellValue(col, row)) : cellValue(col, row);
18659
+ if (i === expansionColIndex) {
18660
+ return /* @__PURE__ */ jsxRuntime.jsx(
18661
+ "td",
18662
+ {
18663
+ className: cn(tdClassName, "cursor-pointer"),
18664
+ style: tdStyle,
18665
+ onClick: (event) => {
18666
+ if (isOwnControl(event.target, event.currentTarget)) return;
18667
+ event.stopPropagation();
18668
+ toggleExpanded(row);
18669
+ },
18670
+ onDoubleClick: (event) => event.stopPropagation(),
18671
+ children: /* @__PURE__ */ jsxRuntime.jsx(ExpansionToggleCell, { expanded: isRowExpanded(row), children: content })
18672
+ },
18673
+ col.name
18674
+ );
18675
+ }
18676
+ return /* @__PURE__ */ jsxRuntime.jsx("td", { className: tdClassName, style: tdStyle, children: content }, col.name);
18677
+ })
18678
+ ]
18679
+ }
18680
+ ),
18681
+ expansionShown && // 상세 영역 — 행이 아니므로 data-row-index 를 달지 않는다 (행 수·선택·실측 대상 밖)
18682
+ /* @__PURE__ */ jsxRuntime.jsx(
18683
+ "tr",
18684
+ {
18685
+ "data-expansion-row": "",
18686
+ "data-expansion-key": String(row[rowKey]),
18687
+ className: expandedSet.has(row[rowKey]) ? void 0 : "pointer-events-none",
18688
+ children: /* @__PURE__ */ jsxRuntime.jsx(
18689
+ "td",
18690
+ {
18691
+ colSpan: totalColSpan,
18692
+ 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)]",
18693
+ children: /* @__PURE__ */ jsxRuntime.jsx(
18694
+ "div",
18397
18695
  {
18398
- value: isSel || !isRowIndeterminate?.(row) ? isSel : null,
18399
- disabled: !canSelectRow(row),
18400
- onValueChange: () => toggleRow(row, rowOffset + ri)
18696
+ className: "sticky left-0 box-border min-w-0",
18697
+ style: {
18698
+ // 가로 스크롤이 있어도 보이는 폭에 머문다. 재기 전(0)에는 표 폭을 따른다
18699
+ width: viewportWidth > 0 ? viewportWidth : void 0,
18700
+ paddingLeft: "var(--cmp-table-body-paddingX-wide)",
18701
+ paddingRight: "var(--cmp-table-body-paddingX-wide)",
18702
+ paddingTop: cellPadY,
18703
+ paddingBottom: cellPadY
18704
+ },
18705
+ children: renderExpansion?.(row)
18401
18706
  }
18402
- ) })
18403
- }
18404
- ),
18405
- visibleCols.map((col, i) => {
18406
- const tdClassName = cn(
18407
- // 원본 .td: 컬럼 폭에 맞춰 내용 말줄임(overflow hidden + ellipsis) + 행 구분선(border-b)
18408
- // 셀 배경은 흰색으로 채워 뒤 배경이 비치지 않게 하고, hover 시 행 배경(grey_05)을 따른다.
18409
- "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)]",
18410
- rowBg,
18411
- rowHover && "group-hover/table-row:bg-[var(--color-grey-05)]",
18412
- typeof col.tdClass === "function" ? col.tdClass(row) : col.tdClass
18413
- );
18414
- const tdPadX = cellPadXOf(col);
18415
- const dragEdges = dragSel ? [
18416
- dragTop && `inset 0 ${DRAG_EDGE_W} 0 0 ${DRAG_EDGE_C}`,
18417
- dragBottom && `inset 0 ${DRAG_EDGE_NEG_W} 0 0 ${DRAG_EDGE_C}`,
18418
- i === 0 && `inset ${DRAG_EDGE_W} 0 0 0 ${DRAG_EDGE_C}`,
18419
- i === visibleCols.length - 1 && `inset ${DRAG_EDGE_NEG_W} 0 0 0 ${DRAG_EDGE_C}`
18420
- ].filter(Boolean) : [];
18421
- const shadows = [...dragEdges, stickyShadowValue(i)].filter(Boolean);
18422
- const tdStyle = {
18423
- paddingLeft: tdPadX,
18424
- paddingRight: tdPadX,
18425
- paddingTop: cellPadY,
18426
- paddingBottom: cellPadY,
18427
- textAlign: col.align ?? "left",
18428
- // 인라인이라 hover 배경보다 세다 — 고른 행은 hover 로 흐려지지 않는다.
18429
- background: dragSel ? DRAG_BG : rowLocked ? LOCKED_BG : void 0,
18430
- // 클래스의 기본 글자색을 덮는다. 셀이 자기 색을 정한 내용(SIcon 의
18431
- // color, STag 등)까지는 닿지 않는다 — 그건 소비 앱 몫이다.
18432
- color: rowLocked ? LOCKED_CONTENT : void 0,
18433
- boxShadow: shadows.length > 0 ? shadows.join(", ") : void 0,
18434
- ...cellStyle(i)
18435
- };
18436
- if (col.renderCell) {
18437
- const cell = col.renderCell({
18438
- row,
18439
- value: cellValue(col, row),
18440
- column: col,
18441
- colIndex: i,
18442
- rowIndex: ri,
18443
- isSelected: isSel,
18444
- className: tdClassName,
18445
- style: tdStyle
18446
- });
18447
- if (cell == null || cell === false) return null;
18448
- return /* @__PURE__ */ jsxRuntime.jsx(react.Fragment, { children: cell }, col.name);
18707
+ )
18449
18708
  }
18450
- return /* @__PURE__ */ jsxRuntime.jsx("td", { className: tdClassName, style: tdStyle, children: col.render ? col.render(row, cellValue(col, row)) : cellValue(col, row) }, col.name);
18451
- })
18452
- ]
18453
- },
18454
- row[rowKey] ?? ri
18455
- );
18709
+ )
18710
+ }
18711
+ )
18712
+ ] }, row[rowKey] ?? ri);
18456
18713
  }),
18457
18714
  virtualScroll && bottomSpacer > 0 && /* @__PURE__ */ jsxRuntime.jsx("tr", { "aria-hidden": true, style: { height: bottomSpacer }, children: /* @__PURE__ */ jsxRuntime.jsx("td", { colSpan: totalColSpan, className: "p-0" }) })
18458
18715
  ] })
@@ -18578,6 +18835,26 @@ var STableImpl = /* @__PURE__ */ react.forwardRef(function STable({
18578
18835
  );
18579
18836
  });
18580
18837
  var STable2 = STableImpl;
18838
+ function isOwnControl(target, cell) {
18839
+ if (!(target instanceof Element)) return false;
18840
+ if (target.closest("[data-expansion-toggle]")) return false;
18841
+ const control = target.closest(ROW_INTERACTIVE_SELECTOR);
18842
+ return control != null && cell.contains(control);
18843
+ }
18844
+ function ExpansionToggleCell({ expanded, children }) {
18845
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 items-center gap-(--cmp-tree-item-contentGap)", children: [
18846
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "data-expansion-toggle": true, className: "flex shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx(
18847
+ SGhostButton,
18848
+ {
18849
+ icon: expanded ? "caretDown" : "caretRight",
18850
+ size: "xxs",
18851
+ ariaLabel: expanded ? "collapse row" : "expand row",
18852
+ ariaExpanded: expanded
18853
+ }
18854
+ ) }),
18855
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 flex-1 truncate", children })
18856
+ ] });
18857
+ }
18581
18858
  var COUNT_NULL = "var(--cmp-table-bar-count-null)";
18582
18859
  var totalColor = (n) => n > 0 ? "var(--cmp-table-bar-count-total)" : COUNT_NULL;
18583
18860
  var selectedColor = (n) => n > 0 ? "var(--color-brilliantblue-75)" : COUNT_NULL;
@@ -18646,9 +18923,7 @@ var STableBar = /* @__PURE__ */ react.forwardRef(function STableBar2({ title, to
18646
18923
  }
18647
18924
  );
18648
18925
  });
18649
- var NO_KEYS = /* @__PURE__ */ new Set();
18650
- var EXPAND_MOTION = { duration: 220, easing: "ease-out" };
18651
- var COLLAPSE_MOTION = { duration: 140, easing: "ease-in" };
18926
+ var NO_KEYS2 = /* @__PURE__ */ new Set();
18652
18927
  function STreeTable({
18653
18928
  rows = [],
18654
18929
  columns = [],
@@ -18701,11 +18976,11 @@ function STreeTable({
18701
18976
  [rows, expandedSet, rowKey, childrenKey]
18702
18977
  );
18703
18978
  const tableRef = react.useRef(null);
18704
- const [closingKeys, setClosingKeys] = react.useState(NO_KEYS);
18979
+ const [closingKeys, setClosingKeys] = react.useState(NO_KEYS2);
18705
18980
  const [prevExpand, setPrevExpand] = react.useState({ rows, expandedSet });
18706
18981
  if (prevExpand.rows !== rows || prevExpand.expandedSet !== expandedSet) {
18707
18982
  setPrevExpand({ rows, expandedSet });
18708
- let nextClosing = NO_KEYS;
18983
+ let nextClosing = NO_KEYS2;
18709
18984
  if (prevExpand.rows === rows && motionAllowed()) {
18710
18985
  const collapsed = [...prevExpand.expandedSet].filter((key) => !expandedSet.has(key));
18711
18986
  const expanded = [...expandedSet].filter((key) => !prevExpand.expandedSet.has(key));
@@ -18754,11 +19029,15 @@ function STreeTable({
18754
19029
  motionRef.current = null;
18755
19030
  }
18756
19031
  const scrollEl = tableRef.current?.getScrollElement();
19032
+ const rowKeys = renderedRows.map(keyOf);
18757
19033
  const keys = added.size > 0 ? added : removed;
18758
19034
  const canAnimate = scrollEl != null && motionAllowed() && (added.size === 0 || removed.size === 0);
18759
19035
  const motion = canAnimate ? startRowMotion({
18760
19036
  scrollEl,
18761
- rowKeys: renderedRows.map(keyOf),
19037
+ isInBlock: (tr) => {
19038
+ const key = rowKeys[Number(tr.dataset.rowIndex)];
19039
+ return key !== void 0 && keys.has(key);
19040
+ },
18762
19041
  blockKeys: keys,
18763
19042
  from,
18764
19043
  to: added.size > 0 ? 1 : 0
@@ -18769,11 +19048,11 @@ function STreeTable({
18769
19048
  void motion.finished.then((completed) => {
18770
19049
  if (!completed || motionRef.current !== motion) return;
18771
19050
  motion.done = true;
18772
- setClosingKeys(NO_KEYS);
19051
+ setClosingKeys(NO_KEYS2);
18773
19052
  });
18774
19053
  }
18775
19054
  } else if (closingKeys.size > 0) {
18776
- setClosingKeys(NO_KEYS);
19055
+ setClosingKeys(NO_KEYS2);
18777
19056
  }
18778
19057
  }, [renderedRows]);
18779
19058
  react.useEffect(() => () => motionRef.current?.cancel(), []);
@@ -18846,7 +19125,7 @@ function STreeTable({
18846
19125
  className: cn(className, hasChildren && "cursor-pointer"),
18847
19126
  style,
18848
19127
  onClick: hasChildren ? (event) => {
18849
- if (isOwnControl(event.target, event.currentTarget)) return;
19128
+ if (isOwnControl2(event.target, event.currentTarget)) return;
18850
19129
  event.stopPropagation();
18851
19130
  toggleExpanded(row);
18852
19131
  } : void 0,
@@ -18901,96 +19180,8 @@ function flattenRows(nodes, isOpen, childrenOf) {
18901
19180
  walk(nodes);
18902
19181
  return out;
18903
19182
  }
18904
- function sameKeys(a, b) {
18905
- return a.size === b.size && [...a].every((key) => b.has(key));
18906
- }
18907
- function motionAllowed() {
18908
- if (typeof window === "undefined" || typeof Element.prototype.animate !== "function")
18909
- return false;
18910
- return !window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
18911
- }
18912
- function startRowMotion({
18913
- scrollEl,
18914
- rowKeys,
18915
- blockKeys,
18916
- from,
18917
- to
18918
- }) {
18919
- const spec = to === 1 ? EXPAND_MOTION : COLLAPSE_MOTION;
18920
- const duration = spec.duration * Math.abs(to - from);
18921
- if (duration < 1) return null;
18922
- const trs = [...scrollEl.querySelectorAll("tr[data-row-index]")].map((tr) => {
18923
- const key = rowKeys[Number(tr.dataset.rowIndex)];
18924
- return {
18925
- tr,
18926
- inBlock: key !== void 0 && blockKeys.has(key),
18927
- height: tr.getBoundingClientRect().height
18928
- };
18929
- });
18930
- const viewBottom = scrollEl.scrollTop + scrollEl.clientHeight;
18931
- const openAt = (offset) => from + (to - from) * offset;
18932
- const translate = (shift2, s) => `translateY(${-shift2 * (1 - s)}px)`;
18933
- const timing = {
18934
- duration,
18935
- easing: spec.easing,
18936
- // 접힘은 남겨 둔 행이 빠질 때까지 끝 위치를 붙잡는다 — 놓으면 그 사이 한 프레임 제자리로 튄다
18937
- fill: to === 0 ? "forwards" : "none"
18938
- };
18939
- const animations = [];
18940
- let shift = 0;
18941
- let blockHeight = 0;
18942
- let y = 0;
18943
- trs.forEach(({ tr, inBlock, height }, i) => {
18944
- if (inBlock && !trs[i - 1]?.inBlock) {
18945
- blockHeight = 0;
18946
- for (let j = i; j < trs.length && trs[j].inBlock; j++) blockHeight += trs[j].height;
18947
- shift += blockHeight;
18948
- y = 0;
18949
- }
18950
- if (shift === 0 || tr.offsetTop - shift > viewBottom) return;
18951
- if (!inBlock) {
18952
- animations.push(
18953
- tr.animate(
18954
- [{ transform: translate(shift, from) }, { transform: translate(shift, to) }],
18955
- timing
18956
- )
18957
- );
18958
- return;
18959
- }
18960
- const rowY = y;
18961
- const clipAt = (s) => Math.min(height, Math.max(0, blockHeight * (1 - s) - rowY));
18962
- const bends = [1 - rowY / blockHeight, 1 - (rowY + height) / blockHeight].map((s) => (s - from) / (to - from)).filter((offset) => offset > 0 && offset < 1);
18963
- const offsets = [0, ...bends, 1].sort((a, b) => a - b);
18964
- animations.push(
18965
- tr.animate(
18966
- offsets.map((offset) => ({
18967
- offset,
18968
- transform: translate(shift, openAt(offset)),
18969
- clipPath: `inset(${clipAt(openAt(offset))}px 0 0 0)`
18970
- })),
18971
- timing
18972
- )
18973
- );
18974
- y += height;
18975
- });
18976
- if (animations.length === 0) return null;
18977
- return {
18978
- keys: blockKeys,
18979
- to,
18980
- done: false,
18981
- finished: Promise.all(animations.map((animation) => animation.finished)).then(
18982
- () => true,
18983
- () => false
18984
- ),
18985
- openness: () => {
18986
- const progress = animations[0].effect?.getComputedTiming().progress;
18987
- return progress == null ? to : openAt(progress);
18988
- },
18989
- cancel: () => animations.forEach((animation) => animation.cancel())
18990
- };
18991
- }
18992
19183
  var OWN_CONTROL_SELECTOR = 'a, button, input, select, textarea, label, [role="button"], [role="checkbox"], [contenteditable="true"]';
18993
- function isOwnControl(target, cell) {
19184
+ function isOwnControl2(target, cell) {
18994
19185
  if (!(target instanceof Element)) return false;
18995
19186
  if (target.closest("[data-tree-toggle]")) return false;
18996
19187
  const control = target.closest(OWN_CONTROL_SELECTOR);