sellmate-design-system-react 9.0.0-beta.104 → 9.0.0-beta.105

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
@@ -17321,6 +17321,48 @@ function indexAtOffset(metrics, y) {
17321
17321
  return lo;
17322
17322
  }
17323
17323
 
17324
+ // src/lib/grid-key-nav.ts
17325
+ var GRID_NAV_ATTR = "data-grid-nav";
17326
+ var TEXT_INPUT = [
17327
+ "input:not([type])",
17328
+ ...["text", "number", "search", "tel", "url", "email", "password"].map((t) => `input[type="${t}"]`)
17329
+ ];
17330
+ var FOCUSABLE_INPUT = TEXT_INPUT.map(
17331
+ (s) => `${s}:not(:disabled):not([readonly]):not([aria-disabled="true"])`
17332
+ ).join(",");
17333
+ function isTextInput(el) {
17334
+ return el.matches(TEXT_INPUT.join(","));
17335
+ }
17336
+ function focusInput(cell) {
17337
+ const input = cell.querySelector(FOCUSABLE_INPUT);
17338
+ if (!input) return false;
17339
+ if (input.closest('[inert],[aria-disabled="true"]')) return false;
17340
+ input.focus();
17341
+ if (input.ownerDocument.activeElement !== input) return false;
17342
+ input.select();
17343
+ return true;
17344
+ }
17345
+ function handleGridKeyNav(e) {
17346
+ if (e.key !== "Enter" || e.defaultPrevented) return;
17347
+ if (e.nativeEvent.isComposing || e.keyCode === 229 || e.ctrlKey || e.altKey || e.metaKey) return;
17348
+ const target = e.target;
17349
+ if (!isTextInput(target)) return;
17350
+ if (target.getAttribute("aria-expanded") === "true") return;
17351
+ const root = e.currentTarget;
17352
+ const cell = target.closest(`[${GRID_NAV_ATTR}]`);
17353
+ if (!cell || !root.contains(cell)) return;
17354
+ const column = cell.getAttribute(GRID_NAV_ATTR) ?? "";
17355
+ e.preventDefault();
17356
+ const table = cell.closest("table");
17357
+ const cells = [...root.querySelectorAll(`[${GRID_NAV_ATTR}]`)].filter(
17358
+ (c) => c.getAttribute(GRID_NAV_ATTR) === column && c.closest("table") === table
17359
+ );
17360
+ const step = e.shiftKey ? -1 : 1;
17361
+ for (let i = cells.indexOf(cell) + step; i >= 0 && i < cells.length; i += step) {
17362
+ if (focusInput(cells[i])) return;
17363
+ }
17364
+ }
17365
+
17324
17366
  // src/lib/depth-filter.ts
17325
17367
  function filterDepthRows(rows, matches) {
17326
17368
  const selfMatch = rows.map(matches);
@@ -19911,365 +19953,381 @@ var STableImpl = /* @__PURE__ */ react.forwardRef(function STable({
19911
19953
  ]
19912
19954
  }
19913
19955
  );
19914
- return /* @__PURE__ */ jsxRuntime.jsxs(
19915
- "div",
19916
- {
19917
- className: cn("flex flex-col", className),
19918
- style: { width, ...style },
19919
- "data-table-id": resolvedTableId,
19920
- children: [
19921
- /* @__PURE__ */ jsxRuntime.jsxs(
19922
- "div",
19923
- {
19924
- className: cn(
19925
- // isolate: no-data/loading 오버레이(z-30·31)를 테이블 안에서 끝낸다. relative + z-auto
19926
- // 만으로는 스태킹 컨텍스트가 생기지 않아, 모달 안에 들어가면 30·31이 모달 Content
19927
- // 레벨까지 올라가 같은 컨텍스트의 floating portal 밴드(z-2)를 덮는다.
19928
- // 자손은 이미 overflow-hidden으로 클리핑되므로 테이블 내부 순서는 달라지지 않는다.
19929
- "bg-white relative isolate flex flex-col overflow-hidden",
19930
- bordered && "border border-solid border-[color:var(--cmp-table-border-color)]",
19931
- // height가 없으면 루트가 받은 높이를 끝까지 채운다. 호스트가 className으로
19932
- // `flex-1 min-h-0` 등을 줘 루트 높이가 확정된 경우, 행이 적어도 테두리 박스가
19933
- // 남은 영역을 그대로 차지한다. basis는 auto로 둬 높이가 확정되지 않은
19934
- // 기본 사용(내용만큼 커지는 케이스)에서는 동작이 바뀌지 않는다.
19935
- !height && "min-h-0 grow",
19936
- radiusClass,
19937
- // 원본 sd-table__scroll-container--loading/--no-data: 로딩·데이터 없음 시 인터랙션 차단
19938
- (loading2 || rowCount === 0) && "pointer-events-none"
19939
- ),
19940
- style: {
19941
- height,
19942
- minHeight: isNoData ? "calc(var(--cmp-table-header-height) + 60px)" : void 0
19943
- },
19944
- children: [
19945
- showNoDataSlot ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
19946
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-none overflow-hidden", children: headerTable }),
19947
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn(bodyScrollClass, "pointer-events-auto bg-white"), children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full min-h-[60px] w-full items-center justify-center", children: emptySlot }) })
19948
- ] }) : (
19949
- /**
19950
- * 헤더와 body 가 한 스크롤 컨테이너에 있다 — 헤더는 위에 붙어(sticky) 세로로는 서 있고
19951
- * 가로로는 body 와 같은 스크롤로 움직인다. 헤더를 따로 두고 scroll 이벤트에서
19952
- * scrollLeft 를 맞추면, 브라우저가 body 스크롤을 먼저 그리고 이벤트를 나중에 돌려
19953
- * 헤더가 한 프레임 이상 늦게 따라온다.
19954
- *
19955
- * 행 좌표는 body 표 기준 그대로다 — `scrollTop` 이 y 이면 y 에 있는 행이 헤더 바로
19956
- * 아래에 선다. 보이는 높이만 헤더 높이만큼 줄어든다(`headerHeight`).
19957
- */
19958
- /* @__PURE__ */ jsxRuntime.jsxs(
19959
- "div",
19960
- {
19961
- ref: scrollRef,
19962
- onScroll: handleScrollEvent,
19963
- className: cn(
19964
- bodyScrollClass,
19965
- // 원본 sd-table__scroll-container--loading/--no-data: 로딩·데이터 없음 시 스크롤 차단
19966
- (loading2 || rowCount === 0) && "overflow-hidden"
19967
- ),
19968
- style: { scrollPaddingTop: headerHeight },
19969
- children: [
19970
- /* @__PURE__ */ jsxRuntime.jsx("div", { ref: headerRef, className: "sticky top-0 z-[8]", children: headerTable }),
19971
- /* @__PURE__ */ jsxRuntime.jsxs(
19972
- "table",
19973
- {
19974
- className: "w-full table-fixed border-separate border-spacing-0 text-left",
19975
- style: { fontSize: 12 },
19976
- children: [
19977
- colgroup,
19978
- /* @__PURE__ */ jsxRuntime.jsxs("tbody", { children: [
19979
- virtualScroll && topSpacer > 0 && /* @__PURE__ */ jsxRuntime.jsx("tr", { "aria-hidden": true, style: { height: topSpacer }, children: /* @__PURE__ */ jsxRuntime.jsx("td", { colSpan: totalColSpan, className: "p-0" }) }),
19980
- rowCount === 0 ? null : bodyRows.map((row, ri) => {
19981
- const rowIndex = rowOffset + ri;
19982
- const isSel = isViewSelected(row);
19983
- const dragSel = dragSelectOn && isSel;
19984
- const dragTop = dragSel && !isViewSelected(rows[rowIndex - 1]);
19985
- const dragBottom = dragSel && !isViewSelected(rows[rowIndex + 1]);
19986
- const rowLocked = (selectable || dragSelectable) && !canSelectRow(row);
19987
- const rowBg = "bg-white";
19988
- const rowHover = hoverable && !rowLocked;
19989
- const expansionShown = expansionOn && shownExpansionSet.has(row[rowKey]);
19990
- return /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
19991
- /* @__PURE__ */ jsxRuntime.jsxs(
19992
- "tr",
19993
- {
19994
- "data-row-index": rowIndex,
19995
- onClick: onRowClick ? (event) => {
19996
- const target = event.target;
19997
- if (target.closest(ROW_INTERACTIVE_SELECTOR)) return;
19998
- onRowClick(row);
19999
- } : void 0,
20000
- onDoubleClick: onRowDoubleClick ? (event) => {
20001
- const target = event.target;
20002
- if (target.closest(ROW_INTERACTIVE_SELECTOR)) return;
20003
- onRowDoubleClick(row);
20004
- } : void 0,
20005
- onMouseDown: dragSelectOn ? (event) => startDragSelect(rowIndex, event) : void 0,
20006
- onMouseEnter: dragSelectOn ? () => extendDragSelect(rowIndex) : void 0,
20007
- onContextMenu: contextMenuOn ? (event) => openContextMenu(row, event) : void 0,
20008
- className: cn(
20009
- "group/table-row",
20010
- rowHover && "hover:bg-[var(--color-grey-05)]",
20011
- (onRowClick || onRowDoubleClick) && "cursor-pointer",
20012
- dragSelectOn && "select-none",
20013
- rowClassName?.(row)
20014
- ),
20015
- style: { height: virtualScroll ? effectiveRowHeight : rowH },
20016
- children: [
20017
- selectable && /* @__PURE__ */ jsxRuntime.jsx(
20018
- "td",
20019
- {
20020
- className: cn(
20021
- // 원본 .td--selected: 48px 고정폭, 항상 left sticky, 체크박스 중앙정렬
20022
- "sticky left-0 z-[2] border-b border-solid border-[color:var(--cmp-table-border-color)] align-middle",
19956
+ const gridNavOn = columns.some((col) => col.gridNav);
19957
+ return (
19958
+ // 키 이벤트를 모아 받을 뿐 이 요소 자체는 상호작용 요소가 아니다 — role 을 바꾸지 않는다
19959
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions
19960
+ /* @__PURE__ */ jsxRuntime.jsxs(
19961
+ "div",
19962
+ {
19963
+ className: cn("flex flex-col", className),
19964
+ style: { width, ...style },
19965
+ "data-table-id": resolvedTableId,
19966
+ onKeyDown: gridNavOn ? handleGridKeyNav : void 0,
19967
+ children: [
19968
+ /* @__PURE__ */ jsxRuntime.jsxs(
19969
+ "div",
19970
+ {
19971
+ className: cn(
19972
+ // isolate: no-data/loading 오버레이(z-30·31)를 테이블 안에서 끝낸다. relative + z-auto
19973
+ // 만으로는 스태킹 컨텍스트가 생기지 않아, 모달 안에 들어가면 30·31이 모달 Content
19974
+ // 레벨까지 올라가 같은 컨텍스트의 floating portal 밴드(z-2)를 덮는다.
19975
+ // 자손은 이미 overflow-hidden으로 클리핑되므로 테이블 내부 순서는 달라지지 않는다.
19976
+ "bg-white relative isolate flex flex-col overflow-hidden",
19977
+ bordered && "border border-solid border-[color:var(--cmp-table-border-color)]",
19978
+ // height가 없으면 루트가 받은 높이를 끝까지 채운다. 호스트가 className으로
19979
+ // `flex-1 min-h-0` 등을 줘 루트 높이가 확정된 경우, 행이 적어도 테두리 박스가
19980
+ // 남은 영역을 그대로 차지한다. basis는 auto로 둬 높이가 확정되지 않은
19981
+ // 기본 사용(내용만큼 커지는 케이스)에서는 동작이 바뀌지 않는다.
19982
+ !height && "min-h-0 grow",
19983
+ radiusClass,
19984
+ // 원본 sd-table__scroll-container--loading/--no-data: 로딩·데이터 없음 시 인터랙션 차단
19985
+ (loading2 || rowCount === 0) && "pointer-events-none"
19986
+ ),
19987
+ style: {
19988
+ height,
19989
+ minHeight: isNoData ? "calc(var(--cmp-table-header-height) + 60px)" : void 0
19990
+ },
19991
+ children: [
19992
+ showNoDataSlot ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
19993
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-none overflow-hidden", children: headerTable }),
19994
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn(bodyScrollClass, "pointer-events-auto bg-white"), children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full min-h-[60px] w-full items-center justify-center", children: emptySlot }) })
19995
+ ] }) : (
19996
+ /**
19997
+ * 헤더와 body 가 한 스크롤 컨테이너에 있다 — 헤더는 위에 붙어(sticky) 세로로는 서 있고
19998
+ * 가로로는 body 와 같은 스크롤로 움직인다. 헤더를 따로 두고 scroll 이벤트에서
19999
+ * scrollLeft 를 맞추면, 브라우저가 body 스크롤을 먼저 그리고 이벤트를 나중에 돌려
20000
+ * 헤더가 한 프레임 이상 늦게 따라온다.
20001
+ *
20002
+ * 행 좌표는 body 표 기준 그대로다 — `scrollTop` 이 y 이면 y 에 있는 행이 헤더 바로
20003
+ * 아래에 선다. 보이는 높이만 헤더 높이만큼 줄어든다(`headerHeight`).
20004
+ */
20005
+ /* @__PURE__ */ jsxRuntime.jsxs(
20006
+ "div",
20007
+ {
20008
+ ref: scrollRef,
20009
+ onScroll: handleScrollEvent,
20010
+ className: cn(
20011
+ bodyScrollClass,
20012
+ // 원본 sd-table__scroll-container--loading/--no-data: 로딩·데이터 없음 시 스크롤 차단
20013
+ (loading2 || rowCount === 0) && "overflow-hidden"
20014
+ ),
20015
+ style: { scrollPaddingTop: headerHeight },
20016
+ children: [
20017
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ref: headerRef, className: "sticky top-0 z-[8]", children: headerTable }),
20018
+ /* @__PURE__ */ jsxRuntime.jsxs(
20019
+ "table",
20020
+ {
20021
+ className: "w-full table-fixed border-separate border-spacing-0 text-left",
20022
+ style: { fontSize: 12 },
20023
+ children: [
20024
+ colgroup,
20025
+ /* @__PURE__ */ jsxRuntime.jsxs("tbody", { children: [
20026
+ virtualScroll && topSpacer > 0 && /* @__PURE__ */ jsxRuntime.jsx("tr", { "aria-hidden": true, style: { height: topSpacer }, children: /* @__PURE__ */ jsxRuntime.jsx("td", { colSpan: totalColSpan, className: "p-0" }) }),
20027
+ rowCount === 0 ? null : bodyRows.map((row, ri) => {
20028
+ const rowIndex = rowOffset + ri;
20029
+ const isSel = isViewSelected(row);
20030
+ const dragSel = dragSelectOn && isSel;
20031
+ const dragTop = dragSel && !isViewSelected(rows[rowIndex - 1]);
20032
+ const dragBottom = dragSel && !isViewSelected(rows[rowIndex + 1]);
20033
+ const rowLocked = (selectable || dragSelectable) && !canSelectRow(row);
20034
+ const rowBg = "bg-white";
20035
+ const rowHover = hoverable && !rowLocked;
20036
+ const expansionShown = expansionOn && shownExpansionSet.has(row[rowKey]);
20037
+ return /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
20038
+ /* @__PURE__ */ jsxRuntime.jsxs(
20039
+ "tr",
20040
+ {
20041
+ "data-row-index": rowIndex,
20042
+ onClick: onRowClick ? (event) => {
20043
+ const target = event.target;
20044
+ if (target.closest(ROW_INTERACTIVE_SELECTOR)) return;
20045
+ onRowClick(row);
20046
+ } : void 0,
20047
+ onDoubleClick: onRowDoubleClick ? (event) => {
20048
+ const target = event.target;
20049
+ if (target.closest(ROW_INTERACTIVE_SELECTOR)) return;
20050
+ onRowDoubleClick(row);
20051
+ } : void 0,
20052
+ onMouseDown: dragSelectOn ? (event) => startDragSelect(rowIndex, event) : void 0,
20053
+ onMouseEnter: dragSelectOn ? () => extendDragSelect(rowIndex) : void 0,
20054
+ onContextMenu: contextMenuOn ? (event) => openContextMenu(row, event) : void 0,
20055
+ className: cn(
20056
+ "group/table-row",
20057
+ rowHover && "hover:bg-[var(--color-grey-05)]",
20058
+ (onRowClick || onRowDoubleClick) && "cursor-pointer",
20059
+ dragSelectOn && "select-none",
20060
+ rowClassName?.(row)
20061
+ ),
20062
+ style: { height: virtualScroll ? effectiveRowHeight : rowH },
20063
+ children: [
20064
+ selectable && /* @__PURE__ */ jsxRuntime.jsx(
20065
+ "td",
20066
+ {
20067
+ className: cn(
20068
+ // 원본 .td--selected: 48px 고정폭, 항상 left sticky, 체크박스 중앙정렬
20069
+ "sticky left-0 z-[2] border-b border-solid border-[color:var(--cmp-table-border-color)] align-middle",
20070
+ rowBg,
20071
+ rowHover && "group-hover/table-row:bg-[var(--color-grey-05)]",
20072
+ stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
20073
+ ),
20074
+ style: {
20075
+ width: SELECTABLE_COLUMN_WIDTH,
20076
+ minWidth: SELECTABLE_COLUMN_WIDTH,
20077
+ maxWidth: SELECTABLE_COLUMN_WIDTH,
20078
+ // 선택 열도 행의 일부다 — 여기만 희지 않으면 회색 줄이 끊긴다
20079
+ background: rowLocked ? LOCKED_BG : void 0
20080
+ },
20081
+ onClick: (e) => e.stopPropagation(),
20082
+ onDoubleClick: (e) => e.stopPropagation(),
20083
+ onClickCapture: (e) => {
20084
+ shiftPressedRef.current = e.shiftKey;
20085
+ if (e.shiftKey) window.getSelection()?.removeAllRanges();
20086
+ },
20087
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex select-none items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(
20088
+ SCheckbox,
20089
+ {
20090
+ value: isSel || !isRowIndeterminate?.(row) ? isSel : null,
20091
+ disabled: !canSelectRow(row),
20092
+ onValueChange: () => toggleRow(row, rowOffset + ri),
20093
+ ariaLabel: rowSelectLabel?.(row) ?? t.selectRow
20094
+ }
20095
+ ) })
20096
+ }
20097
+ ),
20098
+ visibleCols.map((col, i) => {
20099
+ const tdClassName = cn(
20100
+ // 원본 .td: 컬럼 폭에 맞춰 내용 말줄임(overflow hidden + ellipsis) + 행 구분선(border-b)
20101
+ // 셀 배경은 흰색으로 채워 뒤 배경이 비치지 않게 하고, hover 시 행 배경(grey_05)을 따른다.
20102
+ "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)]",
20023
20103
  rowBg,
20024
20104
  rowHover && "group-hover/table-row:bg-[var(--color-grey-05)]",
20025
- stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
20026
- ),
20027
- style: {
20028
- width: SELECTABLE_COLUMN_WIDTH,
20029
- minWidth: SELECTABLE_COLUMN_WIDTH,
20030
- maxWidth: SELECTABLE_COLUMN_WIDTH,
20031
- // 선택 열도 행의 일부다 — 여기만 희지 않으면 회색 줄이 끊긴다
20032
- background: rowLocked ? LOCKED_BG : void 0
20033
- },
20034
- onClick: (e) => e.stopPropagation(),
20035
- onDoubleClick: (e) => e.stopPropagation(),
20036
- onClickCapture: (e) => {
20037
- shiftPressedRef.current = e.shiftKey;
20038
- if (e.shiftKey) window.getSelection()?.removeAllRanges();
20039
- },
20040
- children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex select-none items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(
20041
- SCheckbox,
20042
- {
20043
- value: isSel || !isRowIndeterminate?.(row) ? isSel : null,
20044
- disabled: !canSelectRow(row),
20045
- onValueChange: () => toggleRow(row, rowOffset + ri),
20046
- ariaLabel: rowSelectLabel?.(row) ?? t.selectRow
20047
- }
20048
- ) })
20049
- }
20050
- ),
20051
- visibleCols.map((col, i) => {
20052
- const tdClassName = cn(
20053
- // 원본 .td: 컬럼 폭에 맞춰 내용 말줄임(overflow hidden + ellipsis) + 행 구분선(border-b)
20054
- // 셀 배경은 흰색으로 채워 뒤 배경이 비치지 않게 하고, hover 시 행 배경(grey_05)을 따른다.
20055
- "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)]",
20056
- rowBg,
20057
- rowHover && "group-hover/table-row:bg-[var(--color-grey-05)]",
20058
- typeof col.tdClass === "function" ? col.tdClass(row) : col.tdClass
20059
- );
20060
- const tdPadX = cellPadXOf(col);
20061
- const dragEdges = dragSel ? [
20062
- dragTop && `inset 0 ${DRAG_EDGE_W} 0 0 ${DRAG_EDGE_C}`,
20063
- dragBottom && `inset 0 ${DRAG_EDGE_NEG_W} 0 0 ${DRAG_EDGE_C}`,
20064
- i === 0 && `inset ${DRAG_EDGE_W} 0 0 0 ${DRAG_EDGE_C}`,
20065
- i === visibleCols.length - 1 && `inset ${DRAG_EDGE_NEG_W} 0 0 0 ${DRAG_EDGE_C}`
20066
- ].filter(Boolean) : [];
20067
- const shadows = [...dragEdges, stickyShadowValue(i)].filter(Boolean);
20068
- const tdStyle = {
20069
- paddingLeft: tdPadX,
20070
- paddingRight: tdPadX,
20071
- paddingTop: cellPadY,
20072
- paddingBottom: cellPadY,
20073
- textAlign: col.align ?? "left",
20074
- // 인라인이라 hover 배경보다 세다 — 고른 행은 hover 로 흐려지지 않는다.
20075
- background: dragSel ? DRAG_BG : rowLocked ? LOCKED_BG : void 0,
20076
- // 클래스의 기본 글자색을 덮는다. 셀이 자기 색을 정한 내용(SIcon 의
20077
- // color, STag 등)까지는 닿지 않는다 — 그건 소비 앱 몫이다.
20078
- color: rowLocked ? LOCKED_CONTENT : void 0,
20079
- boxShadow: shadows.length > 0 ? shadows.join(", ") : void 0,
20080
- ...cellStyle(i)
20081
- };
20082
- if (col.renderCell) {
20083
- const cell = col.renderCell({
20084
- row,
20085
- value: cellValue(col, row),
20086
- column: col,
20087
- colIndex: i,
20088
- rowIndex: ri,
20089
- isSelected: isSel,
20090
- className: tdClassName,
20091
- style: tdStyle
20092
- });
20093
- if (cell == null || cell === false) return null;
20094
- return /* @__PURE__ */ jsxRuntime.jsx(react.Fragment, { children: cell }, col.name);
20095
- }
20096
- const content = col.render ? col.render(row, cellValue(col, row)) : cellValue(col, row);
20097
- if (i === expansionColIndex) {
20105
+ typeof col.tdClass === "function" ? col.tdClass(row) : col.tdClass
20106
+ );
20107
+ const tdPadX = cellPadXOf(col);
20108
+ const dragEdges = dragSel ? [
20109
+ dragTop && `inset 0 ${DRAG_EDGE_W} 0 0 ${DRAG_EDGE_C}`,
20110
+ dragBottom && `inset 0 ${DRAG_EDGE_NEG_W} 0 0 ${DRAG_EDGE_C}`,
20111
+ i === 0 && `inset ${DRAG_EDGE_W} 0 0 0 ${DRAG_EDGE_C}`,
20112
+ i === visibleCols.length - 1 && `inset ${DRAG_EDGE_NEG_W} 0 0 0 ${DRAG_EDGE_C}`
20113
+ ].filter(Boolean) : [];
20114
+ const shadows = [...dragEdges, stickyShadowValue(i)].filter(Boolean);
20115
+ const tdStyle = {
20116
+ paddingLeft: tdPadX,
20117
+ paddingRight: tdPadX,
20118
+ paddingTop: cellPadY,
20119
+ paddingBottom: cellPadY,
20120
+ textAlign: col.align ?? "left",
20121
+ // 인라인이라 hover 배경보다 세다 — 고른 행은 hover 로 흐려지지 않는다.
20122
+ background: dragSel ? DRAG_BG : rowLocked ? LOCKED_BG : void 0,
20123
+ // 클래스의 기본 글자색을 덮는다. 셀이 자기 색을 정한 내용(SIcon 의
20124
+ // color, STag 등)까지는 닿지 않는다 — 그건 소비 앱 몫이다.
20125
+ color: rowLocked ? LOCKED_CONTENT : void 0,
20126
+ boxShadow: shadows.length > 0 ? shadows.join(", ") : void 0,
20127
+ ...cellStyle(i)
20128
+ };
20129
+ if (col.renderCell) {
20130
+ const cell = col.renderCell({
20131
+ row,
20132
+ value: cellValue(col, row),
20133
+ column: col,
20134
+ colIndex: i,
20135
+ rowIndex: ri,
20136
+ isSelected: isSel,
20137
+ className: tdClassName,
20138
+ style: tdStyle
20139
+ });
20140
+ if (cell == null || cell === false) return null;
20141
+ return /* @__PURE__ */ jsxRuntime.jsx(react.Fragment, { children: cell }, col.name);
20142
+ }
20143
+ const content = col.render ? col.render(row, cellValue(col, row)) : cellValue(col, row);
20144
+ if (i === expansionColIndex) {
20145
+ return /* @__PURE__ */ jsxRuntime.jsx(
20146
+ "td",
20147
+ {
20148
+ className: cn(tdClassName, "cursor-pointer"),
20149
+ style: tdStyle,
20150
+ "data-grid-nav": col.gridNav ? col.name : void 0,
20151
+ onClick: (event) => {
20152
+ if (isOwnControl(event.target, event.currentTarget)) return;
20153
+ event.stopPropagation();
20154
+ toggleExpanded(row);
20155
+ },
20156
+ onDoubleClick: (event) => event.stopPropagation(),
20157
+ children: /* @__PURE__ */ jsxRuntime.jsx(ExpansionToggleCell, { expanded: isRowExpanded(row), children: content })
20158
+ },
20159
+ col.name
20160
+ );
20161
+ }
20098
20162
  return /* @__PURE__ */ jsxRuntime.jsx(
20099
20163
  "td",
20100
20164
  {
20101
- className: cn(tdClassName, "cursor-pointer"),
20165
+ className: tdClassName,
20102
20166
  style: tdStyle,
20103
- onClick: (event) => {
20104
- if (isOwnControl(event.target, event.currentTarget)) return;
20105
- event.stopPropagation();
20106
- toggleExpanded(row);
20107
- },
20108
- onDoubleClick: (event) => event.stopPropagation(),
20109
- children: /* @__PURE__ */ jsxRuntime.jsx(ExpansionToggleCell, { expanded: isRowExpanded(row), children: content })
20167
+ "data-grid-nav": col.gridNav ? col.name : void 0,
20168
+ children: content
20110
20169
  },
20111
20170
  col.name
20112
20171
  );
20172
+ })
20173
+ ]
20174
+ }
20175
+ ),
20176
+ expansionShown && // 상세 영역 — 행이 아니므로 data-row-index 를 달지 않는다 (행 수·선택·실측 대상 밖)
20177
+ /* @__PURE__ */ jsxRuntime.jsx(
20178
+ "tr",
20179
+ {
20180
+ "data-expansion-row": "",
20181
+ "data-expansion-key": String(row[rowKey]),
20182
+ className: expandedSet.has(row[rowKey]) ? void 0 : "pointer-events-none",
20183
+ children: /* @__PURE__ */ jsxRuntime.jsx(
20184
+ "td",
20185
+ {
20186
+ colSpan: totalColSpan,
20187
+ 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)]",
20188
+ children: /* @__PURE__ */ jsxRuntime.jsx(
20189
+ "div",
20190
+ {
20191
+ className: "sticky left-0 box-border min-w-0",
20192
+ style: {
20193
+ // 가로 스크롤이 있어도 보이는 폭에 머문다. 재기 전(0)에는 표 폭을 따른다
20194
+ width: viewportWidth > 0 ? viewportWidth : void 0,
20195
+ paddingLeft: "var(--cmp-table-body-paddingX-wide)",
20196
+ paddingRight: "var(--cmp-table-body-paddingX-wide)",
20197
+ paddingTop: cellPadY,
20198
+ paddingBottom: cellPadY
20199
+ },
20200
+ children: renderExpansion?.(row)
20201
+ }
20202
+ )
20113
20203
  }
20114
- return /* @__PURE__ */ jsxRuntime.jsx("td", { className: tdClassName, style: tdStyle, children: content }, col.name);
20115
- })
20116
- ]
20117
- }
20118
- ),
20119
- expansionShown && // 상세 영역 — 행이 아니므로 data-row-index 를 달지 않는다 (행 수·선택·실측 대상 밖)
20120
- /* @__PURE__ */ jsxRuntime.jsx(
20121
- "tr",
20122
- {
20123
- "data-expansion-row": "",
20124
- "data-expansion-key": String(row[rowKey]),
20125
- className: expandedSet.has(row[rowKey]) ? void 0 : "pointer-events-none",
20126
- children: /* @__PURE__ */ jsxRuntime.jsx(
20127
- "td",
20128
- {
20129
- colSpan: totalColSpan,
20130
- 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)]",
20131
- children: /* @__PURE__ */ jsxRuntime.jsx(
20132
- "div",
20133
- {
20134
- className: "sticky left-0 box-border min-w-0",
20135
- style: {
20136
- // 가로 스크롤이 있어도 보이는 폭에 머문다. 재기 전(0)에는 표 폭을 따른다
20137
- width: viewportWidth > 0 ? viewportWidth : void 0,
20138
- paddingLeft: "var(--cmp-table-body-paddingX-wide)",
20139
- paddingRight: "var(--cmp-table-body-paddingX-wide)",
20140
- paddingTop: cellPadY,
20141
- paddingBottom: cellPadY
20142
- },
20143
- children: renderExpansion?.(row)
20144
- }
20145
- )
20146
- }
20147
- )
20148
- }
20149
- )
20150
- ] }, row[rowKey] ?? ri);
20151
- }),
20152
- virtualScroll && bottomSpacer > 0 && /* @__PURE__ */ jsxRuntime.jsx("tr", { "aria-hidden": true, style: { height: bottomSpacer }, children: /* @__PURE__ */ jsxRuntime.jsx("td", { colSpan: totalColSpan, className: "p-0" }) })
20153
- ] })
20154
- ]
20155
- }
20156
- )
20157
- ]
20158
- }
20159
- )
20160
- ),
20161
- isNoData && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
20162
- /* @__PURE__ */ jsxRuntime.jsx(
20163
- "div",
20164
- {
20165
- "aria-hidden": true,
20166
- className: "pointer-events-none absolute left-0 right-0 top-0 z-[31] h-[var(--cmp-table-header-height)] bg-white/60"
20167
- }
20168
- ),
20169
- !showNoDataSlot && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pointer-events-none absolute bottom-0 left-0 right-0 top-[var(--cmp-table-header-height)] z-[30] flex items-center justify-center bg-white/60 text-[12px] text-[color:var(--color-grey-65)]", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex min-h-[60px] w-full items-center justify-center", children: emptyLabel }) })
20170
- ] }),
20171
- loading2 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 z-30 flex items-center justify-center bg-white/60", children: /* @__PURE__ */ jsxRuntime.jsx(SCircleProgress, { indeterminate: true }) })
20172
- ]
20173
- }
20174
- ),
20175
- (showPagination || showRowsPerPageSelect) && // 원본 sd-table__pagination: 테이블 하단에 이어붙는 회색 바(높이 48px, margin-top:-1px로 테두리 겹침).
20176
- // SPagination은 전체 폭 기준 중앙, rows-per-page SSelect는 우측에 absolute 배치해 중앙 정렬을 유지한다.
20177
- // 루트 높이가 확정된 경우(테이블이 남은 영역을 채우는 배치) 페이지네이션 바가
20178
- // 눌리지 않도록 shrink-0으로 48px를 보장한다.
20179
- // 페이지네이션이 없어도 rows-per-page 셀렉트가 있으면 이 바를 그린다 — 셀렉트와
20180
- // 밀도 토글이 사는 곳이 여기다. dense 만으로는 그리지 않는다(빈 바가 남는다).
20181
- /* @__PURE__ */ jsxRuntime.jsxs(
20182
- "div",
20183
- {
20184
- className: cn(
20185
- "relative flex h-[48px] shrink-0 items-center justify-center rounded-[var(--cmp-table-radius)] bg-[var(--color-grey-05)]",
20186
- // 테두리를 끄면 겹칠 대상이 없어진다 — -mt-px 를 남기면 1px 어긋남만 생긴다.
20187
- bordered && "-mt-px border border-solid border-[color:var(--cmp-table-border-color)]"
20188
- ),
20189
- children: [
20190
- showPagination && /* @__PURE__ */ jsxRuntime.jsx(SPagination, { currentPage: displayPage, lastPage, onPageChange: changePage }),
20191
- showRowsPerPageSelect && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute right-[10px] top-1/2 flex -translate-y-1/2 items-center gap-[12px]", children: [
20192
- dense && /* @__PURE__ */ jsxRuntime.jsx(
20193
- STextLink,
20194
- {
20195
- size: "sm",
20196
- icon: innerDense ? "row" : "rowDense",
20197
- iconRotate: 90,
20198
- iconColor: "var(--sys-color-fg-quaternary)",
20199
- label: innerDense ? t.denseOff : t.denseOn,
20200
- onClick: () => changeDense(!innerDense)
20201
- }
20204
+ )
20205
+ }
20206
+ )
20207
+ ] }, row[rowKey] ?? ri);
20208
+ }),
20209
+ virtualScroll && bottomSpacer > 0 && /* @__PURE__ */ jsxRuntime.jsx("tr", { "aria-hidden": true, style: { height: bottomSpacer }, children: /* @__PURE__ */ jsxRuntime.jsx("td", { colSpan: totalColSpan, className: "p-0" }) })
20210
+ ] })
20211
+ ]
20212
+ }
20213
+ )
20214
+ ]
20215
+ }
20216
+ )
20202
20217
  ),
20203
- dense && /* @__PURE__ */ jsxRuntime.jsx(SDivider, { vertical: true, className: "my-[6px]" }),
20204
- /* @__PURE__ */ jsxRuntime.jsx(
20205
- SSelect2,
20206
- {
20207
- options: rowsPerPageOption,
20208
- value: internalPagination ? innerRowsPerPage : pagination?.rowsPerPage,
20209
- valueAsPrimitive: true,
20210
- onValueChange: (v) => changeRowsPerPage(Number(v)),
20211
- width: 128
20212
- }
20213
- )
20214
- ] })
20215
- ]
20216
- }
20217
- ),
20218
- contextMenuOn && typeof document !== "undefined" && reactDom.createPortal(
20218
+ isNoData && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
20219
+ /* @__PURE__ */ jsxRuntime.jsx(
20220
+ "div",
20221
+ {
20222
+ "aria-hidden": true,
20223
+ className: "pointer-events-none absolute left-0 right-0 top-0 z-[31] h-[var(--cmp-table-header-height)] bg-white/60"
20224
+ }
20225
+ ),
20226
+ !showNoDataSlot && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pointer-events-none absolute bottom-0 left-0 right-0 top-[var(--cmp-table-header-height)] z-[30] flex items-center justify-center bg-white/60 text-[12px] text-[color:var(--color-grey-65)]", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex min-h-[60px] w-full items-center justify-center", children: emptyLabel }) })
20227
+ ] }),
20228
+ loading2 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 z-30 flex items-center justify-center bg-white/60", children: /* @__PURE__ */ jsxRuntime.jsx(SCircleProgress, { indeterminate: true }) })
20229
+ ]
20230
+ }
20231
+ ),
20232
+ (showPagination || showRowsPerPageSelect) && // 원본 sd-table__pagination: 테이블 하단에 이어붙는 회색 바(높이 48px, margin-top:-1px로 테두리 겹침).
20233
+ // SPagination은 전체 폭 기준 중앙, rows-per-page SSelect는 우측에 absolute 배치해 중앙 정렬을 유지한다.
20234
+ // 루트 높이가 확정된 경우(테이블이 남은 영역을 채우는 배치) 페이지네이션 바가
20235
+ // 눌리지 않도록 shrink-0으로 48px를 보장한다.
20236
+ // 페이지네이션이 없어도 rows-per-page 셀렉트가 있으면 이 바를 그린다 — 셀렉트와
20237
+ // 밀도 토글이 사는 곳이 여기다. dense 만으로는 그리지 않는다(빈 바가 남는다).
20219
20238
  /* @__PURE__ */ jsxRuntime.jsxs(
20220
- RDropdown5__namespace.Root,
20239
+ "div",
20221
20240
  {
20222
- modal: false,
20223
- open: contextMenuOpen,
20224
- onOpenChange: (open) => {
20225
- if (!open) setContextMenu((m) => m == null ? null : { ...m, open: false });
20226
- },
20241
+ className: cn(
20242
+ "relative flex h-[48px] shrink-0 items-center justify-center rounded-[var(--cmp-table-radius)] bg-[var(--color-grey-05)]",
20243
+ // 테두리를 끄면 겹칠 대상이 없어진다 — -mt-px 를 남기면 1px 어긋남만 생긴다.
20244
+ bordered && "-mt-px border border-solid border-[color:var(--cmp-table-border-color)]"
20245
+ ),
20227
20246
  children: [
20228
- /* @__PURE__ */ jsxRuntime.jsx(RDropdown5__namespace.Trigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(
20229
- "span",
20230
- {
20231
- "aria-hidden": true,
20232
- className: "pointer-events-none fixed block h-0 w-0",
20233
- style: { left: contextMenu?.x ?? 0, top: contextMenu?.y ?? 0 }
20234
- }
20235
- ) }),
20236
- /* @__PURE__ */ jsxRuntime.jsx(
20237
- RDropdown5__namespace.Content,
20238
- {
20239
- align: "start",
20240
- side: "bottom",
20241
- sideOffset: CONTEXT_MENU_CURSOR_GAP,
20242
- alignOffset: CONTEXT_MENU_CURSOR_GAP,
20243
- collisionPadding: CONTEXT_MENU_VIEWPORT_PADDING,
20244
- style: contextMenuArmed ? void 0 : { pointerEvents: "none" },
20245
- className: cn(
20246
- "z-50 overflow-hidden rounded-[var(--cmp-listBox-radius)] bg-[var(--cmp-listBox-bg)] shadow-normal-sm",
20247
- "max-w-[var(--radix-dropdown-menu-content-available-width)]",
20248
- FLOATING_SLIDE_ANIM
20249
- ),
20250
- children: contextMenuItems?.map((item4) => /* @__PURE__ */ jsxRuntime.jsxs(
20251
- RDropdown5__namespace.Item,
20252
- {
20253
- disabled: item4.disabled,
20254
- onSelect: () => onContextMenuItemClick?.(item4.value, contextMenu?.rows ?? []),
20255
- className: CONTEXT_MENU_ITEM_CLASS,
20256
- children: [
20257
- item4.icon && /* @__PURE__ */ jsxRuntime.jsx(SIcon, { name: item4.icon, size: 12 }),
20258
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 break-words", children: item4.label })
20259
- ]
20260
- },
20261
- item4.value
20262
- ))
20263
- }
20264
- )
20247
+ showPagination && /* @__PURE__ */ jsxRuntime.jsx(SPagination, { currentPage: displayPage, lastPage, onPageChange: changePage }),
20248
+ showRowsPerPageSelect && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute right-[10px] top-1/2 flex -translate-y-1/2 items-center gap-[12px]", children: [
20249
+ dense && /* @__PURE__ */ jsxRuntime.jsx(
20250
+ STextLink,
20251
+ {
20252
+ size: "sm",
20253
+ icon: innerDense ? "row" : "rowDense",
20254
+ iconRotate: 90,
20255
+ iconColor: "var(--sys-color-fg-quaternary)",
20256
+ label: innerDense ? t.denseOff : t.denseOn,
20257
+ onClick: () => changeDense(!innerDense)
20258
+ }
20259
+ ),
20260
+ dense && /* @__PURE__ */ jsxRuntime.jsx(SDivider, { vertical: true, className: "my-[6px]" }),
20261
+ /* @__PURE__ */ jsxRuntime.jsx(
20262
+ SSelect2,
20263
+ {
20264
+ options: rowsPerPageOption,
20265
+ value: internalPagination ? innerRowsPerPage : pagination?.rowsPerPage,
20266
+ valueAsPrimitive: true,
20267
+ onValueChange: (v) => changeRowsPerPage(Number(v)),
20268
+ width: 128
20269
+ }
20270
+ )
20271
+ ] })
20265
20272
  ]
20266
- },
20267
- contextMenu == null ? "closed" : `${contextMenu.x},${contextMenu.y}`
20273
+ }
20268
20274
  ),
20269
- portalContainer ?? document.body
20270
- )
20271
- ]
20272
- }
20275
+ contextMenuOn && typeof document !== "undefined" && reactDom.createPortal(
20276
+ /* @__PURE__ */ jsxRuntime.jsxs(
20277
+ RDropdown5__namespace.Root,
20278
+ {
20279
+ modal: false,
20280
+ open: contextMenuOpen,
20281
+ onOpenChange: (open) => {
20282
+ if (!open) setContextMenu((m) => m == null ? null : { ...m, open: false });
20283
+ },
20284
+ children: [
20285
+ /* @__PURE__ */ jsxRuntime.jsx(RDropdown5__namespace.Trigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(
20286
+ "span",
20287
+ {
20288
+ "aria-hidden": true,
20289
+ className: "pointer-events-none fixed block h-0 w-0",
20290
+ style: { left: contextMenu?.x ?? 0, top: contextMenu?.y ?? 0 }
20291
+ }
20292
+ ) }),
20293
+ /* @__PURE__ */ jsxRuntime.jsx(
20294
+ RDropdown5__namespace.Content,
20295
+ {
20296
+ align: "start",
20297
+ side: "bottom",
20298
+ sideOffset: CONTEXT_MENU_CURSOR_GAP,
20299
+ alignOffset: CONTEXT_MENU_CURSOR_GAP,
20300
+ collisionPadding: CONTEXT_MENU_VIEWPORT_PADDING,
20301
+ style: contextMenuArmed ? void 0 : { pointerEvents: "none" },
20302
+ className: cn(
20303
+ "z-50 overflow-hidden rounded-[var(--cmp-listBox-radius)] bg-[var(--cmp-listBox-bg)] shadow-normal-sm",
20304
+ "max-w-[var(--radix-dropdown-menu-content-available-width)]",
20305
+ FLOATING_SLIDE_ANIM
20306
+ ),
20307
+ children: contextMenuItems?.map((item4) => /* @__PURE__ */ jsxRuntime.jsxs(
20308
+ RDropdown5__namespace.Item,
20309
+ {
20310
+ disabled: item4.disabled,
20311
+ onSelect: () => onContextMenuItemClick?.(item4.value, contextMenu?.rows ?? []),
20312
+ className: CONTEXT_MENU_ITEM_CLASS,
20313
+ children: [
20314
+ item4.icon && /* @__PURE__ */ jsxRuntime.jsx(SIcon, { name: item4.icon, size: 12 }),
20315
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 break-words", children: item4.label })
20316
+ ]
20317
+ },
20318
+ item4.value
20319
+ ))
20320
+ }
20321
+ )
20322
+ ]
20323
+ },
20324
+ contextMenu == null ? "closed" : `${contextMenu.x},${contextMenu.y}`
20325
+ ),
20326
+ portalContainer ?? document.body
20327
+ )
20328
+ ]
20329
+ }
20330
+ )
20273
20331
  );
20274
20332
  });
20275
20333
  var STable2 = STableImpl;