@rufous/ui 0.1.67 → 0.1.69

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.
@@ -19,24 +19,38 @@ import {
19
19
  Plus
20
20
  } from "lucide-react";
21
21
  function DataGrid({
22
- columns: initialColumns,
22
+ columns: initialColumnsProp,
23
23
  data,
24
24
  actions,
25
25
  pageSize: initialPageSize = 10,
26
26
  pageSizeOptions = [5, 10, 25, 50],
27
27
  title
28
28
  }) {
29
- const [columns, setColumns] = useState(initialColumns);
29
+ const [columnOverrides, setColumnOverrides] = useState({});
30
+ const resolvedColumns = useMemo(() => {
31
+ return initialColumnsProp.map((col) => {
32
+ const field = String(col.field || col.key || "");
33
+ const override = columnOverrides[field] || {};
34
+ return {
35
+ ...col,
36
+ field,
37
+ headerName: col.headerName || col.header || "",
38
+ hidden: override.hidden !== void 0 ? override.hidden : col.hidden,
39
+ pinned: override.pinned !== void 0 ? override.pinned : col.pinned
40
+ };
41
+ });
42
+ }, [initialColumnsProp, columnOverrides]);
30
43
  const [columnWidths, setColumnWidths] = useState(() => {
31
44
  const widths = {};
32
- initialColumns.forEach((col) => {
45
+ initialColumnsProp.forEach((col) => {
46
+ const field = String(col.field || col.key || "");
33
47
  const w = col.width || 200;
34
- widths[String(col.key)] = typeof w === "number" ? w : parseInt(w);
48
+ widths[field] = typeof w === "number" ? w : parseInt(w);
35
49
  });
36
50
  return widths;
37
51
  });
38
52
  const [pageSize, setPageSize] = useState(initialPageSize);
39
- const [sortKey, setSortKey] = useState(null);
53
+ const [sortField, setSortField] = useState(null);
40
54
  const [sortDirection, setSortDirection] = useState(null);
41
55
  const [filterText, setFilterText] = useState("");
42
56
  const [currentPage, setCurrentPage] = useState(1);
@@ -49,14 +63,15 @@ function DataGrid({
49
63
  const menuRef = useRef(null);
50
64
  const [showManageColumns, setShowManageColumns] = useState(false);
51
65
  const [showAdvancedFilter, setShowAdvancedFilter] = useState(false);
66
+ const initialFilterCol = String(initialColumnsProp[0]?.field || initialColumnsProp[0]?.key || "");
52
67
  const [advancedFilters, setAdvancedFilters] = useState([
53
- { column: String(initialColumns[0].key), operator: "contains", value: "", logic: "AND" }
68
+ { column: initialFilterCol, operator: "contains", value: "", logic: "AND" }
54
69
  ]);
55
70
  const [colSearch, setColSearch] = useState("");
56
71
  useEffect(() => {
57
72
  const handleMouseMove = (e) => {
58
73
  if (!resizingColumn) return;
59
- const col = columns.find((c) => String(c.key) === resizingColumn);
74
+ const col = resolvedColumns.find((c) => String(c.field) === resizingColumn);
60
75
  const diff = e.clientX - startX;
61
76
  const minW = col?.minWidth ? typeof col.minWidth === "number" ? col.minWidth : parseInt(col.minWidth) : 80;
62
77
  const maxW = col?.maxWidth ? typeof col.maxWidth === "number" ? col.maxWidth : parseInt(col.maxWidth) : Infinity;
@@ -72,7 +87,7 @@ function DataGrid({
72
87
  document.removeEventListener("mousemove", handleMouseMove);
73
88
  document.removeEventListener("mouseup", handleMouseUp);
74
89
  };
75
- }, [resizingColumn, startX, startWidth, columns]);
90
+ }, [resizingColumn, startX, startWidth, resolvedColumns]);
76
91
  useEffect(() => {
77
92
  const handleClickOutside = (e) => {
78
93
  if (menuRef.current && !menuRef.current.contains(e.target)) {
@@ -82,32 +97,55 @@ function DataGrid({
82
97
  document.addEventListener("mousedown", handleClickOutside);
83
98
  return () => document.removeEventListener("mousedown", handleClickOutside);
84
99
  }, []);
85
- const handleSort = (key, dir) => {
100
+ useEffect(() => {
101
+ setColumnWidths((prev) => {
102
+ const next = { ...prev };
103
+ initialColumnsProp.forEach((col) => {
104
+ const field = String(col.field || col.key || "");
105
+ if (next[field] === void 0) {
106
+ const w = col.width || 200;
107
+ next[field] = typeof w === "number" ? w : parseInt(w);
108
+ }
109
+ });
110
+ return next;
111
+ });
112
+ }, [initialColumnsProp]);
113
+ const handleSort = (fieldKey, dir) => {
86
114
  if (dir !== void 0) {
87
- setSortKey(key);
115
+ setSortField(fieldKey);
88
116
  setSortDirection(dir);
89
- } else if (sortKey === key) {
117
+ } else if (sortField === fieldKey) {
90
118
  if (sortDirection === "asc") setSortDirection("desc");
91
119
  else {
92
- setSortKey(null);
120
+ setSortField(null);
93
121
  setSortDirection(null);
94
122
  }
95
123
  } else {
96
- setSortKey(key);
124
+ setSortField(fieldKey);
97
125
  setSortDirection("asc");
98
126
  }
99
127
  setActiveMenu(null);
100
128
  };
101
- const togglePin = (key, side) => {
102
- setColumns((prev) => prev.map(
103
- (col) => String(col.key) === key ? { ...col, pinned: col.pinned === side ? void 0 : side } : col
104
- ));
129
+ const togglePin = (fieldKey, side) => {
130
+ setColumnOverrides((prev) => {
131
+ const current = prev[fieldKey] || {};
132
+ return {
133
+ ...prev,
134
+ [fieldKey]: { ...current, pinned: current.pinned === side ? void 0 : side }
135
+ };
136
+ });
105
137
  setActiveMenu(null);
106
138
  };
107
- const toggleHide = (key) => {
108
- setColumns((prev) => prev.map(
109
- (col) => String(col.key) === key ? { ...col, hidden: !col.hidden } : col
110
- ));
139
+ const toggleHide = (fieldKey) => {
140
+ setColumnOverrides((prev) => {
141
+ const current = prev[fieldKey] || {};
142
+ const col = resolvedColumns.find((c) => String(c.field) === fieldKey);
143
+ if (col?.hideable === false) return prev;
144
+ return {
145
+ ...prev,
146
+ [fieldKey]: { ...current, hidden: !current.hidden }
147
+ };
148
+ });
111
149
  setActiveMenu(null);
112
150
  };
113
151
  const filteredData = useMemo(() => {
@@ -149,30 +187,30 @@ function DataGrid({
149
187
  });
150
188
  }, [data, filterText, advancedFilters]);
151
189
  const sortedData = useMemo(() => {
152
- if (!sortKey || !sortDirection) return filteredData;
153
- const col = columns.find((c) => c.key === sortKey);
190
+ if (!sortField || !sortDirection) return filteredData;
191
+ const col = resolvedColumns.find((c) => c.field === sortField);
154
192
  return [...filteredData].sort((a, b) => {
155
- let aVal = a[sortKey];
156
- let bVal = b[sortKey];
193
+ let aVal = a[sortField];
194
+ let bVal = b[sortField];
157
195
  if (col?.valueGetter) {
158
- aVal = col.valueGetter({ value: aVal, row: a, field: String(sortKey) });
159
- bVal = col.valueGetter({ value: bVal, row: b, field: String(sortKey) });
196
+ aVal = col.valueGetter({ value: aVal, row: a, field: String(sortField) });
197
+ bVal = col.valueGetter({ value: bVal, row: b, field: String(sortField) });
160
198
  }
161
199
  if (aVal < bVal) return sortDirection === "asc" ? -1 : 1;
162
200
  if (aVal > bVal) return sortDirection === "asc" ? 1 : -1;
163
201
  return 0;
164
202
  });
165
- }, [filteredData, sortKey, sortDirection, columns]);
203
+ }, [filteredData, sortField, sortDirection, resolvedColumns]);
166
204
  const totalPages = Math.max(1, Math.ceil(sortedData.length / pageSize));
167
205
  const paginatedData = useMemo(() => {
168
206
  const start = (currentPage - 1) * pageSize;
169
207
  return sortedData.slice(start, start + pageSize);
170
208
  }, [sortedData, currentPage, pageSize]);
171
209
  const handleExport = () => {
172
- const visibleCols = columns.filter((c) => !c.hidden);
173
- const headers = visibleCols.map((c) => c.header).join(",");
210
+ const visibleCols = resolvedColumns.filter((c) => !c.hidden);
211
+ const headers = visibleCols.map((c) => c.headerName).join(",");
174
212
  const rows = sortedData.map(
175
- (item) => visibleCols.map((c) => `"${String(item[c.key]).replace(/"/g, '""')}"`).join(",")
213
+ (item) => visibleCols.map((c) => `"${String(item[c.field]).replace(/"/g, '""')}"`).join(",")
176
214
  );
177
215
  const csv = [headers, ...rows].join("\n");
178
216
  const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
@@ -183,24 +221,24 @@ function DataGrid({
183
221
  link.click();
184
222
  document.body.removeChild(link);
185
223
  };
186
- const handleMenuOpen = (e, key) => {
224
+ const handleMenuOpen = (e, keyStr) => {
187
225
  e.stopPropagation();
188
226
  const rect = e.currentTarget.getBoundingClientRect();
189
227
  setMenuPosition({ top: rect.bottom + 4, left: rect.left });
190
- setActiveMenu(key);
228
+ setActiveMenu(keyStr);
191
229
  };
192
230
  const visibleColumns = useMemo(() => {
193
- const left = columns.filter((c) => !c.hidden && c.pinned === "left");
194
- const mid = columns.filter((c) => !c.hidden && !c.pinned);
195
- const right = columns.filter((c) => !c.hidden && c.pinned === "right");
231
+ const left = resolvedColumns.filter((c) => !c.hidden && c.pinned === "left");
232
+ const mid = resolvedColumns.filter((c) => !c.hidden && !c.pinned);
233
+ const right = resolvedColumns.filter((c) => !c.hidden && c.pinned === "right");
196
234
  return [...left, ...mid, ...right];
197
- }, [columns]);
235
+ }, [resolvedColumns]);
198
236
  const getLeftOffset = (col, idx) => {
199
237
  if (col.pinned !== "left") return void 0;
200
238
  let offset = 0;
201
239
  for (let i = 0; i < idx; i++) {
202
240
  if (visibleColumns[i].pinned === "left") {
203
- offset += columnWidths[String(visibleColumns[i].key)] || 200;
241
+ offset += columnWidths[String(visibleColumns[i].field)] || 200;
204
242
  }
205
243
  }
206
244
  return offset;
@@ -234,14 +272,14 @@ function DataGrid({
234
272
  },
235
273
  /* @__PURE__ */ React.createElement(Columns, { size: 16 })
236
274
  ), /* @__PURE__ */ React.createElement("button", { className: "dg-action-btn", onClick: handleExport }, /* @__PURE__ */ React.createElement(Download, { size: 14 }), " Export CSV"))), /* @__PURE__ */ React.createElement("div", { className: "dg-table-wrap" }, /* @__PURE__ */ React.createElement("table", { className: "dg-table" }, /* @__PURE__ */ React.createElement("thead", null, /* @__PURE__ */ React.createElement("tr", null, visibleColumns.map((col, idx) => {
237
- const colKey = String(col.key);
238
- const width = columnWidths[colKey] || 200;
275
+ const colField = String(col.field);
276
+ const width = columnWidths[colField] || 200;
239
277
  const leftOffset = getLeftOffset(col, idx);
240
- const isSorted = sortKey === col.key;
278
+ const isSorted = sortField === col.field;
241
279
  return /* @__PURE__ */ React.createElement(
242
280
  "th",
243
281
  {
244
- key: colKey,
282
+ key: colField,
245
283
  className: `dg-thead-cell${col.pinned === "left" ? " pinned-left" : col.pinned === "right" ? " pinned-right" : ""} ${col.headerClassName || ""}`,
246
284
  style: { width, minWidth: width, left: leftOffset, flex: col.flex }
247
285
  },
@@ -249,25 +287,25 @@ function DataGrid({
249
287
  "div",
250
288
  {
251
289
  className: `dg-th-label${col.sortable === false ? " no-sort" : ""}`,
252
- onClick: () => col.sortable !== false && handleSort(col.key)
290
+ onClick: () => col.sortable !== false && handleSort(col.field || "")
253
291
  },
254
- col.header,
292
+ col.headerName,
255
293
  isSorted && sortDirection === "asc" && /* @__PURE__ */ React.createElement(ChevronUp, { size: 12 }),
256
294
  isSorted && sortDirection === "desc" && /* @__PURE__ */ React.createElement(ChevronDown, { size: 12 })
257
295
  ), /* @__PURE__ */ React.createElement("div", { className: "dg-th-actions" }, !col.disableColumnMenu && /* @__PURE__ */ React.createElement(
258
296
  "button",
259
297
  {
260
298
  className: "dg-th-menu-btn",
261
- onClick: (e) => handleMenuOpen(e, colKey)
299
+ onClick: (e) => handleMenuOpen(e, colField)
262
300
  },
263
301
  /* @__PURE__ */ React.createElement(MoreVertical, { size: 13 })
264
302
  ), /* @__PURE__ */ React.createElement(
265
303
  "div",
266
304
  {
267
- className: `dg-resizer${resizingColumn === colKey ? " resizing" : ""}`,
305
+ className: `dg-resizer${resizingColumn === colField ? " resizing" : ""}`,
268
306
  onMouseDown: (e) => {
269
307
  e.preventDefault();
270
- setResizingColumn(colKey);
308
+ setResizingColumn(colField);
271
309
  setStartX(e.clientX);
272
310
  setStartWidth(width);
273
311
  }
@@ -275,19 +313,19 @@ function DataGrid({
275
313
  )))
276
314
  );
277
315
  }), actions && /* @__PURE__ */ React.createElement("th", { style: { width: 0, padding: 0 } }))), /* @__PURE__ */ React.createElement("tbody", null, paginatedData.length === 0 ? /* @__PURE__ */ React.createElement("tr", null, /* @__PURE__ */ React.createElement("td", { colSpan: visibleColumns.length + (actions ? 1 : 0), className: "dg-empty" }, "No records found")) : paginatedData.map((item) => /* @__PURE__ */ React.createElement("tr", { key: item.id, className: "dg-tbody-row" }, visibleColumns.map((col, idx) => {
278
- const colKey = String(col.key);
279
- const width = columnWidths[colKey] || 200;
316
+ const colField = String(col.field);
317
+ const width = columnWidths[colField] || 200;
280
318
  const leftOffset = getLeftOffset(col, idx);
281
319
  return /* @__PURE__ */ React.createElement(
282
320
  "td",
283
321
  {
284
- key: `${item.id}-${colKey}`,
322
+ key: `${item.id}-${colField}`,
285
323
  className: `dg-td${col.pinned === "left" ? " pinned-left" : ""} ${col.cellClassName || ""}`,
286
324
  style: { width, minWidth: width, maxWidth: width, left: leftOffset, flex: col.flex }
287
325
  },
288
326
  (() => {
289
- const field = String(col.key);
290
- const rawValue = item[col.key];
327
+ const field = String(col.field);
328
+ const rawValue = item[col.field || ""];
291
329
  let value = col.valueGetter ? col.valueGetter({ value: rawValue, row: item, field }) : rawValue;
292
330
  const formattedValue = col.valueFormatter ? col.valueFormatter({ value, row: item, field }) : value;
293
331
  if (col.renderCell) {
@@ -355,7 +393,27 @@ function DataGrid({
355
393
  value: colSearch,
356
394
  onChange: (e) => setColSearch(e.target.value)
357
395
  }
358
- )), columns.filter((c) => c.header.toLowerCase().includes(colSearch.toLowerCase()) && c.hideable !== false).map((col) => /* @__PURE__ */ React.createElement("div", { key: String(col.key), className: "dg-col-row" }, /* @__PURE__ */ React.createElement("div", { className: "dg-col-label" }, /* @__PURE__ */ React.createElement("div", { className: "dg-col-dot", style: { background: col.hidden ? "var(--border-color)" : "var(--primary-color)" } }), col.header), /* @__PURE__ */ React.createElement("button", { className: "dg-icon-btn", onClick: () => toggleHide(String(col.key)) }, col.hidden ? /* @__PURE__ */ React.createElement(EyeOff, { size: 14 }) : /* @__PURE__ */ React.createElement(EyeOff, { size: 14, style: { opacity: 0.4 } }))))), /* @__PURE__ */ React.createElement("div", { className: "dg-modal-footer" }, /* @__PURE__ */ React.createElement("button", { className: "dg-action-btn", onClick: () => setColumns((p) => p.map((c) => ({ ...c, hidden: false }))) }, "Show All"), /* @__PURE__ */ React.createElement("button", { className: "dg-action-btn", onClick: () => setColumns((p) => p.map((c) => c.hideable !== false ? { ...c, hidden: true } : c)) }, "Hide All")))), showAdvancedFilter && /* @__PURE__ */ React.createElement("div", { className: "dg-modal-overlay", onClick: () => setShowAdvancedFilter(false) }, /* @__PURE__ */ React.createElement("div", { className: "dg-modal dg-modal-wide", onClick: (e) => e.stopPropagation() }, /* @__PURE__ */ React.createElement("div", { className: "dg-modal-header" }, /* @__PURE__ */ React.createElement("h3", null, "Filters"), /* @__PURE__ */ React.createElement("button", { className: "dg-icon-btn", onClick: () => setShowAdvancedFilter(false) }, /* @__PURE__ */ React.createElement(X, { size: 18 }))), /* @__PURE__ */ React.createElement("div", { className: "dg-modal-body" }, advancedFilters.map((f, idx) => /* @__PURE__ */ React.createElement("div", { key: idx }, idx > 0 && /* @__PURE__ */ React.createElement("div", { className: "dg-filter-logic" }, /* @__PURE__ */ React.createElement(
396
+ )), resolvedColumns.filter((c) => c.header.toLowerCase().includes(colSearch.toLowerCase())).map((col) => {
397
+ const key = String(col.key);
398
+ const isUnhideable = col.hideable === false;
399
+ return /* @__PURE__ */ React.createElement("div", { key, className: `dg-col-row${isUnhideable ? " disabled" : ""}` }, /* @__PURE__ */ React.createElement("div", { className: "dg-col-label" }, /* @__PURE__ */ React.createElement("div", { className: "dg-col-dot", style: { background: col.hidden ? "var(--border-color)" : "var(--primary-color)" } }), col.header), !isUnhideable && /* @__PURE__ */ React.createElement("button", { className: "dg-icon-btn", onClick: () => toggleHide(key) }, col.hidden ? /* @__PURE__ */ React.createElement(EyeOff, { size: 14 }) : /* @__PURE__ */ React.createElement(EyeOff, { size: 14, style: { opacity: 0.4 } })));
400
+ })), /* @__PURE__ */ React.createElement("div", { className: "dg-modal-footer" }, /* @__PURE__ */ React.createElement("button", { className: "dg-action-btn", onClick: () => setColumnOverrides((prev) => {
401
+ const next = { ...prev };
402
+ resolvedColumns.forEach((c) => {
403
+ const k = String(c.key);
404
+ next[k] = { ...next[k], hidden: false };
405
+ });
406
+ return next;
407
+ }) }, "Show All"), /* @__PURE__ */ React.createElement("button", { className: "dg-action-btn", onClick: () => {
408
+ const newOverrides = { ...columnOverrides };
409
+ resolvedColumns.forEach((c) => {
410
+ if (c.hideable !== false) {
411
+ const key = String(c.key);
412
+ newOverrides[key] = { ...newOverrides[key], hidden: true };
413
+ }
414
+ });
415
+ setColumnOverrides(newOverrides);
416
+ } }, "Hide All")))), showAdvancedFilter && /* @__PURE__ */ React.createElement("div", { className: "dg-modal-overlay", onClick: () => setShowAdvancedFilter(false) }, /* @__PURE__ */ React.createElement("div", { className: "dg-modal dg-modal-wide", onClick: (e) => e.stopPropagation() }, /* @__PURE__ */ React.createElement("div", { className: "dg-modal-header" }, /* @__PURE__ */ React.createElement("h3", null, "Filters"), /* @__PURE__ */ React.createElement("button", { className: "dg-icon-btn", onClick: () => setShowAdvancedFilter(false) }, /* @__PURE__ */ React.createElement(X, { size: 18 }))), /* @__PURE__ */ React.createElement("div", { className: "dg-modal-body" }, advancedFilters.map((f, idx) => /* @__PURE__ */ React.createElement("div", { key: idx }, idx > 0 && /* @__PURE__ */ React.createElement("div", { className: "dg-filter-logic" }, /* @__PURE__ */ React.createElement(
359
417
  "button",
360
418
  {
361
419
  className: `dg-logic-btn${f.logic === "AND" ? " active" : ""}`,
@@ -376,7 +434,7 @@ function DataGrid({
376
434
  value: f.column,
377
435
  onChange: (e) => setAdvancedFilters((p) => p.map((fi, i) => i === idx ? { ...fi, column: e.target.value } : fi))
378
436
  },
379
- columns.map((c) => /* @__PURE__ */ React.createElement("option", { key: String(c.key), value: String(c.key) }, c.header))
437
+ resolvedColumns.map((c) => /* @__PURE__ */ React.createElement("option", { key: String(c.key), value: String(c.key) }, c.header))
380
438
  ), /* @__PURE__ */ React.createElement(
381
439
  "select",
382
440
  {
@@ -403,7 +461,7 @@ function DataGrid({
403
461
  {
404
462
  className: "dg-action-btn",
405
463
  style: { alignSelf: "flex-start", marginTop: 4 },
406
- onClick: () => setAdvancedFilters((p) => [...p, { column: String(columns[0].key), operator: "contains", value: "", logic: "AND" }])
464
+ onClick: () => setAdvancedFilters((p) => [...p, { column: String(resolvedColumns[0].key), operator: "contains", value: "", logic: "AND" }])
407
465
  },
408
466
  /* @__PURE__ */ React.createElement(Plus, { size: 14 }),
409
467
  " Add Filter"
@@ -411,7 +469,7 @@ function DataGrid({
411
469
  "button",
412
470
  {
413
471
  className: "dg-action-btn",
414
- onClick: () => setAdvancedFilters([{ column: String(columns[0].key), operator: "contains", value: "", logic: "AND" }])
472
+ onClick: () => setAdvancedFilters([{ column: String(resolvedColumns[0].key), operator: "contains", value: "", logic: "AND" }])
415
473
  },
416
474
  /* @__PURE__ */ React.createElement(Trash2, { size: 14 }),
417
475
  " Reset"