sellmate-design-system-react 2.1.2 → 2.2.0

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.
@@ -3,6 +3,23 @@ import { type SColor } from '../../lib/color';
3
3
  import { type SIconName } from '../SIcon';
4
4
  import { type SSelectOption } from '../SSelect';
5
5
  export type SRow = Record<string, any>;
6
+ /** `STableColumn.renderCell`에 전달되는 컨텍스트 */
7
+ export interface STableCellContext {
8
+ row: SRow;
9
+ /** `field` + `format`이 적용된 표시 값 */
10
+ value: any;
11
+ column: STableColumn;
12
+ /** 보이는 컬럼 기준 인덱스 */
13
+ colIndex: number;
14
+ /** 현재 렌더 중인 행 목록 기준 인덱스 (페이지/가상 스크롤 윈도우 기준) */
15
+ rowIndex: number;
16
+ /** 행 선택 여부 */
17
+ isSelected: boolean;
18
+ /** STable이 이 셀에 적용하려던 클래스 (구분선·말줄임·sticky 그림자 포함) */
19
+ className: string;
20
+ /** STable이 이 셀에 적용하려던 스타일 (패딩·정렬·폭·sticky offset 포함) */
21
+ style: CSSProperties;
22
+ }
6
23
  export interface STableColumn {
7
24
  name: string;
8
25
  label: string;
@@ -21,8 +38,19 @@ export interface STableColumn {
21
38
  icon?: SIconName;
22
39
  /** 헤더 아이콘 색상. 팔레트 키(`grey_65`, `red_95` …) 또는 임의 CSS 색상 */
23
40
  iconColor?: SColor;
24
- /** 셀 커스텀 렌더 (React 확장) */
41
+ /** 셀 커스텀 렌더 (React 확장) — `<td>` 안쪽 내용만 교체한다 */
25
42
  render?: (row: SRow, value: any) => ReactNode;
43
+ /**
44
+ * 셀(`<td>`) 자체를 커스텀 렌더 (React 확장). 반환한 엘리먼트가 그대로 `<td>`가 되므로
45
+ * `colSpan` / `rowSpan`을 직접 지정할 수 있다. `render`보다 우선한다.
46
+ *
47
+ * - `ctx.className` / `ctx.style`을 그대로 펼쳐야 구분선·정렬·sticky 등 기본 스타일이 유지된다.
48
+ * - `null`(또는 `false`)을 반환하면 그 자리에는 `<td>`를 만들지 않는다.
49
+ * 다른 셀의 `colSpan` / `rowSpan`에 덮이는 자리를 이렇게 비운다.
50
+ * - 병합은 sticky 컬럼(인덱스 기준 offset 계산)과 함께 쓰지 않는 것을 전제로 한다.
51
+ * `rowSpan`은 행을 잘라 쓰는 가상 스크롤 / 내부 페이지네이션과 경계에서 어긋날 수 있다.
52
+ */
53
+ renderCell?: (ctx: STableCellContext) => ReactNode;
26
54
  /** 헤더 셀 커스텀 렌더 (React 확장) */
27
55
  renderHeader?: (column: STableColumn, index: number) => ReactNode;
28
56
  /** 본문 셀 커스텀 클래스. 함수를 주면 행마다 다른 클래스를 반환할 수 있다 */
@@ -82,8 +110,15 @@ export interface STableProps {
82
110
  /** border-radius 제어 */
83
111
  radius?: 'default' | 'useTop' | 'full';
84
112
  noDataLabel?: string;
113
+ /**
114
+ * 데이터가 없을 때 body 영역 전체를 대체하는 슬롯.
115
+ * 지정하면 `noDataLabel` 대신 이 콘텐츠가 헤더 아래 영역을 채우며, 버튼 등 인터랙션도 동작한다.
116
+ */
117
+ noDataSlot?: ReactNode;
85
118
  isLoading?: boolean;
86
119
  dense?: boolean;
120
+ /** true면 행에 마우스를 올려도 hover 배경(grey_05)을 표시하지 않는다 */
121
+ noHover?: boolean;
87
122
  /** 페이지네이션 (있으면 하단 표시) */
88
123
  pagination?: STablePagination;
89
124
  onPageChange?: (page: number) => void;
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { forwardRef, createContext, useState, useRef, useImperativeHandle, useMemo, useEffect, useId, useCallback, useContext } from 'react';
2
+ import { forwardRef, createContext, useState, useRef, useImperativeHandle, useMemo, useEffect, useId, useCallback, Fragment as Fragment$1, useContext } from 'react';
3
3
  import { clsx } from 'clsx';
4
4
  import { extendTailwindMerge } from 'tailwind-merge';
5
5
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
@@ -4509,8 +4509,10 @@ var STable = forwardRef(function STable2({
4509
4509
  stickyColumn,
4510
4510
  radius = "default",
4511
4511
  noDataLabel = "\uB370\uC774\uD130\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
4512
+ noDataSlot,
4512
4513
  isLoading = false,
4513
4514
  dense = false,
4515
+ noHover = false,
4514
4516
  pagination,
4515
4517
  onPageChange,
4516
4518
  useInternalPagination = false,
@@ -4596,7 +4598,10 @@ var STable = forwardRef(function STable2({
4596
4598
  );
4597
4599
  const handleScroll = useCallback(() => {
4598
4600
  const el = scrollRef.current;
4599
- if (!el) return;
4601
+ if (!el) {
4602
+ setHeaderGutter(0);
4603
+ return;
4604
+ }
4600
4605
  const header = headerScrollRef.current;
4601
4606
  if (header) header.scrollLeft = el.scrollLeft;
4602
4607
  setHeaderGutter(el.offsetWidth - el.clientWidth);
@@ -4728,6 +4733,7 @@ var STable = forwardRef(function STable2({
4728
4733
  bodyRows = rows.slice(pageInfo.startIndex, pageInfo.endIndex);
4729
4734
  }
4730
4735
  const isNoData = rowCount === 0 && !isLoading;
4736
+ const showNoDataSlot = isNoData && noDataSlot != null;
4731
4737
  const showPagination = !useVirtualScroll && (pagination != null || useInternalPagination);
4732
4738
  const lastPage = useInternalPagination ? Math.max(1, Math.ceil(rowCount / (innerRowsPerPage || 1))) : pagination?.lastPage ?? Math.max(1, Math.ceil(rowCount / (pagination?.rowsPerPage ?? 10)));
4733
4739
  const displayPage = useInternalPagination ? currentPage : pagination?.page ?? 1;
@@ -4789,6 +4795,12 @@ var STable = forwardRef(function STable2({
4789
4795
  isRightEdge && scrolledRight && "shadow-[-5px_0_8px_0_rgba(34,34,34,0.1)]"
4790
4796
  );
4791
4797
  };
4798
+ const bodyScrollClass = cn(
4799
+ "min-h-0 flex-1 overflow-auto border-t border-solid border-[color:var(--cmp-table-border-color)]",
4800
+ SCROLLBAR_CLASS,
4801
+ SCROLLBAR_TRACK_BG_CLASS,
4802
+ SCROLLBAR_TRACK_BORDER_CLASS
4803
+ );
4792
4804
  const colgroup = /* @__PURE__ */ jsxs("colgroup", { children: [
4793
4805
  selectable && /* @__PURE__ */ jsx("col", { style: { width: SELECTABLE_COLUMN_WIDTH } }),
4794
4806
  visibleCols.map((col, i) => /* @__PURE__ */ jsx("col", { style: col.autoWidth ? void 0 : { width: columnWidths[i] } }, col.name))
@@ -4910,97 +4922,106 @@ var STable = forwardRef(function STable2({
4910
4922
  )
4911
4923
  }
4912
4924
  ),
4913
- /* @__PURE__ */ jsx(
4914
- "div",
4915
- {
4916
- ref: scrollRef,
4917
- onScroll: handleScroll,
4918
- className: cn(
4919
- // 헤더 하단 구분선은 body 상단 border로 그린다. body 래퍼는 스크롤바를 포함한
4920
- // 전체 폭을 차지하므로(스크롤바는 border 안쪽) 헤더 거터의 border-right와 미터가
4921
- // 생기지 않아 색·길이가 균일한 한 줄이 된다.
4922
- "min-h-0 flex-1 overflow-auto border-t border-solid border-[color:var(--cmp-table-border-color)]",
4923
- SCROLLBAR_CLASS,
4924
- SCROLLBAR_TRACK_BG_CLASS,
4925
- SCROLLBAR_TRACK_BORDER_CLASS,
4926
- // 원본 sd-table__scroll-container--loading/--no-data: 로딩·데이터 없음 시 스크롤 차단
4927
- (isLoading || rowCount === 0) && "overflow-hidden"
4928
- ),
4929
- children: /* @__PURE__ */ jsxs(
4930
- "table",
4931
- {
4932
- className: "w-full table-fixed border-separate border-spacing-0 text-left",
4933
- style: { fontSize: 12 },
4934
- children: [
4935
- colgroup,
4936
- /* @__PURE__ */ jsxs("tbody", { children: [
4937
- useVirtualScroll && topSpacer > 0 && /* @__PURE__ */ jsx("tr", { "aria-hidden": true, style: { height: topSpacer }, children: /* @__PURE__ */ jsx("td", { colSpan: totalColSpan, className: "p-0" }) }),
4938
- rowCount === 0 ? null : bodyRows.map((row, ri) => {
4939
- const isSel = selectedKeys.has(row[rowKey]);
4940
- const rowBg = "bg-white";
4941
- return /* @__PURE__ */ jsxs(
4942
- "tr",
4943
- {
4944
- onClick: () => onRowClick?.(row),
4945
- className: cn(
4946
- "group/table-row hover:bg-[var(--color-grey-05)]",
4947
- onRowClick && "cursor-pointer"
4948
- ),
4949
- style: { height: useVirtualScroll ? effectiveRowHeight : rowH },
4950
- children: [
4951
- selectable && /* @__PURE__ */ jsx(
4952
- "td",
4953
- {
4954
- className: cn(
4955
- // 원본 .td--selected: 48px 고정폭, 항상 left sticky, 체크박스 중앙정렬
4956
- "sticky left-0 z-[2] border-b border-solid border-[color:var(--cmp-table-border-color)] align-middle group-hover/table-row:bg-[var(--color-grey-05)]",
4957
- rowBg,
4958
- stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
4959
- ),
4960
- style: {
4961
- width: SELECTABLE_COLUMN_WIDTH,
4962
- minWidth: SELECTABLE_COLUMN_WIDTH,
4963
- maxWidth: SELECTABLE_COLUMN_WIDTH
4964
- },
4965
- onClick: (e) => e.stopPropagation(),
4966
- children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx(SCheckbox, { value: isSel, onValueChange: () => toggleRow(row) }) })
4967
- }
4925
+ showNoDataSlot ? /* @__PURE__ */ jsx("div", { className: cn(bodyScrollClass, "pointer-events-auto bg-white"), children: /* @__PURE__ */ jsx("div", { className: "flex h-full min-h-[60px] w-full items-center justify-center", children: noDataSlot }) }) : (
4926
+ /* 바디: 세로 스크롤은 이 컨테이너에만 생긴다 (헤더 제외) */
4927
+ /* @__PURE__ */ jsx(
4928
+ "div",
4929
+ {
4930
+ ref: scrollRef,
4931
+ onScroll: handleScroll,
4932
+ className: cn(
4933
+ bodyScrollClass,
4934
+ // 원본 sd-table__scroll-container--loading/--no-data: 로딩·데이터 없음 스크롤 차단
4935
+ (isLoading || rowCount === 0) && "overflow-hidden"
4936
+ ),
4937
+ children: /* @__PURE__ */ jsxs(
4938
+ "table",
4939
+ {
4940
+ className: "w-full table-fixed border-separate border-spacing-0 text-left",
4941
+ style: { fontSize: 12 },
4942
+ children: [
4943
+ colgroup,
4944
+ /* @__PURE__ */ jsxs("tbody", { children: [
4945
+ useVirtualScroll && topSpacer > 0 && /* @__PURE__ */ jsx("tr", { "aria-hidden": true, style: { height: topSpacer }, children: /* @__PURE__ */ jsx("td", { colSpan: totalColSpan, className: "p-0" }) }),
4946
+ rowCount === 0 ? null : bodyRows.map((row, ri) => {
4947
+ const isSel = selectedKeys.has(row[rowKey]);
4948
+ const rowBg = "bg-white";
4949
+ return /* @__PURE__ */ jsxs(
4950
+ "tr",
4951
+ {
4952
+ onClick: () => onRowClick?.(row),
4953
+ className: cn(
4954
+ "group/table-row",
4955
+ !noHover && "hover:bg-[var(--color-grey-05)]",
4956
+ onRowClick && "cursor-pointer"
4968
4957
  ),
4969
- visibleCols.map((col, i) => /* @__PURE__ */ jsx(
4970
- "td",
4971
- {
4972
- className: cn(
4958
+ style: { height: useVirtualScroll ? effectiveRowHeight : rowH },
4959
+ children: [
4960
+ selectable && /* @__PURE__ */ jsx(
4961
+ "td",
4962
+ {
4963
+ className: cn(
4964
+ // 원본 .td--selected: 48px 고정폭, 항상 left sticky, 체크박스 중앙정렬
4965
+ "sticky left-0 z-[2] border-b border-solid border-[color:var(--cmp-table-border-color)] align-middle",
4966
+ rowBg,
4967
+ !noHover && "group-hover/table-row:bg-[var(--color-grey-05)]",
4968
+ stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
4969
+ ),
4970
+ style: {
4971
+ width: SELECTABLE_COLUMN_WIDTH,
4972
+ minWidth: SELECTABLE_COLUMN_WIDTH,
4973
+ maxWidth: SELECTABLE_COLUMN_WIDTH
4974
+ },
4975
+ onClick: (e) => e.stopPropagation(),
4976
+ children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx(SCheckbox, { value: isSel, onValueChange: () => toggleRow(row) }) })
4977
+ }
4978
+ ),
4979
+ visibleCols.map((col, i) => {
4980
+ const tdClassName = cn(
4973
4981
  // 원본 .td: 컬럼 폭에 맞춰 내용 말줄임(overflow hidden + ellipsis) + 행 구분선(border-b)
4974
4982
  // 셀 배경은 흰색으로 채워 뒤 배경이 비치지 않게 하고, hover 시 행 배경(grey_05)을 따른다.
4975
4983
  "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)]",
4976
4984
  rowBg,
4977
- "group-hover/table-row:bg-[var(--color-grey-05)]",
4985
+ !noHover && "group-hover/table-row:bg-[var(--color-grey-05)]",
4978
4986
  stickyShadowClass(i),
4979
4987
  typeof col.tdClass === "function" ? col.tdClass(row) : col.tdClass
4980
- ),
4981
- style: {
4988
+ );
4989
+ const tdStyle = {
4982
4990
  paddingLeft: cellPadX,
4983
4991
  paddingRight: cellPadX,
4984
4992
  paddingTop: cellPadY,
4985
4993
  paddingBottom: cellPadY,
4986
4994
  textAlign: col.align ?? "left",
4987
4995
  ...cellStyle(i)
4988
- },
4989
- children: col.render ? col.render(row, cellValue(col, row)) : cellValue(col, row)
4990
- },
4991
- col.name
4992
- ))
4993
- ]
4994
- },
4995
- row[rowKey] ?? ri
4996
- );
4997
- }),
4998
- useVirtualScroll && bottomSpacer > 0 && /* @__PURE__ */ jsx("tr", { "aria-hidden": true, style: { height: bottomSpacer }, children: /* @__PURE__ */ jsx("td", { colSpan: totalColSpan, className: "p-0" }) })
4999
- ] })
5000
- ]
5001
- }
5002
- )
5003
- }
4996
+ };
4997
+ if (col.renderCell) {
4998
+ const cell = col.renderCell({
4999
+ row,
5000
+ value: cellValue(col, row),
5001
+ column: col,
5002
+ colIndex: i,
5003
+ rowIndex: ri,
5004
+ isSelected: isSel,
5005
+ className: tdClassName,
5006
+ style: tdStyle
5007
+ });
5008
+ if (cell == null || cell === false) return null;
5009
+ return /* @__PURE__ */ jsx(Fragment$1, { children: cell }, col.name);
5010
+ }
5011
+ return /* @__PURE__ */ jsx("td", { className: tdClassName, style: tdStyle, children: col.render ? col.render(row, cellValue(col, row)) : cellValue(col, row) }, col.name);
5012
+ })
5013
+ ]
5014
+ },
5015
+ row[rowKey] ?? ri
5016
+ );
5017
+ }),
5018
+ useVirtualScroll && bottomSpacer > 0 && /* @__PURE__ */ jsx("tr", { "aria-hidden": true, style: { height: bottomSpacer }, children: /* @__PURE__ */ jsx("td", { colSpan: totalColSpan, className: "p-0" }) })
5019
+ ] })
5020
+ ]
5021
+ }
5022
+ )
5023
+ }
5024
+ )
5004
5025
  ),
5005
5026
  isNoData && /* @__PURE__ */ jsxs(Fragment, { children: [
5006
5027
  /* @__PURE__ */ jsx(
@@ -5010,7 +5031,7 @@ var STable = forwardRef(function STable2({
5010
5031
  className: "pointer-events-none absolute left-0 right-0 top-0 z-[31] h-[var(--cmp-table-header-height)] bg-white/60"
5011
5032
  }
5012
5033
  ),
5013
- /* @__PURE__ */ 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__ */ jsx("div", { className: "flex min-h-[60px] w-full items-center justify-center", children: noDataLabel }) })
5034
+ !showNoDataSlot && /* @__PURE__ */ 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__ */ jsx("div", { className: "flex min-h-[60px] w-full items-center justify-center", children: noDataLabel }) })
5014
5035
  ] }),
5015
5036
  isLoading && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 z-30 flex items-center justify-center bg-white/60", children: /* @__PURE__ */ jsx(SCircleProgress, { indeterminate: true }) })
5016
5037
  ]