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