@rufous/ui 0.3.23 → 0.3.24

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/main.js CHANGED
@@ -4472,8 +4472,57 @@ import {
4472
4472
  ArrowDown,
4473
4473
  Trash2,
4474
4474
  Plus,
4475
- ChevronsUpDown
4475
+ ChevronsUpDown,
4476
+ Layers
4476
4477
  } from "lucide-react";
4478
+ function getAllGroupIds(rows, fields, getKey, depth = 0, parentId = "") {
4479
+ if (!fields.length || !rows.length) return [];
4480
+ const [field, ...rest] = fields;
4481
+ const buckets = /* @__PURE__ */ new Map();
4482
+ rows.forEach((row) => {
4483
+ const k = getKey(row, field) || "(Blank)";
4484
+ if (!buckets.has(k)) buckets.set(k, []);
4485
+ buckets.get(k).push(row);
4486
+ });
4487
+ const out = [];
4488
+ buckets.forEach((children, k) => {
4489
+ const id = `${parentId}::${field}::${k}`;
4490
+ out.push({ id, field, key: k, depth, count: children.length });
4491
+ if (rest.length) out.push(...getAllGroupIds(children, rest, getKey, depth + 1, id));
4492
+ });
4493
+ return out;
4494
+ }
4495
+ function buildFlatEntries(rows, fields, getKey, expanded, depth = 0, parentId = "") {
4496
+ if (!fields.length) return rows.map((row) => ({ kind: "data", row, depth }));
4497
+ const [field, ...rest] = fields;
4498
+ const buckets = /* @__PURE__ */ new Map();
4499
+ rows.forEach((row) => {
4500
+ const k = getKey(row, field) || "(Blank)";
4501
+ if (!buckets.has(k)) buckets.set(k, []);
4502
+ buckets.get(k).push(row);
4503
+ });
4504
+ const out = [];
4505
+ buckets.forEach((children, k) => {
4506
+ const id = `${parentId}::${field}::${k}`;
4507
+ out.push({ kind: "group", id, field, key: k, depth, leafCount: children.length });
4508
+ if (expanded.has(id)) {
4509
+ out.push(...buildFlatEntries(children, rest, getKey, expanded, depth + 1, id));
4510
+ }
4511
+ });
4512
+ return out;
4513
+ }
4514
+ function buildTreeEntries(rows, getChildren, expanded, depth = 0) {
4515
+ const out = [];
4516
+ rows.forEach((row) => {
4517
+ const children = getChildren(row) ?? [];
4518
+ const hasChildren = children.length > 0;
4519
+ out.push({ kind: "data", row, depth, hasChildren });
4520
+ if (hasChildren && expanded.has(row.id)) {
4521
+ out.push(...buildTreeEntries(children, getChildren, expanded, depth + 1));
4522
+ }
4523
+ });
4524
+ return out;
4525
+ }
4477
4526
  function FilterSelect({
4478
4527
  value,
4479
4528
  onChange,
@@ -4571,7 +4620,21 @@ function DataGrid({
4571
4620
  searchableColumns,
4572
4621
  onSearchChange,
4573
4622
  onFiltersChange,
4574
- hideTopExport = false
4623
+ initialFilters,
4624
+ toolbarOptions,
4625
+ customToolbar,
4626
+ customFooter,
4627
+ hideTopExport = false,
4628
+ rowGroupingModel,
4629
+ defaultRowGroupingModel,
4630
+ onRowGroupingModelChange,
4631
+ rowGroupingColumnMode = "single",
4632
+ groupingColDef,
4633
+ defaultGroupingExpansionDepth = 0,
4634
+ isGroupExpandedByDefault,
4635
+ disableRowGrouping = false,
4636
+ treeData = false,
4637
+ getChildRows
4575
4638
  }) {
4576
4639
  const sxClass = useSx(sx);
4577
4640
  const [editingCell, setEditingCell] = useState9(null);
@@ -4631,10 +4694,46 @@ function DataGrid({
4631
4694
  const [focusFilterIdx, setFocusFilterIdx] = useState9(-1);
4632
4695
  const filterableColumnsProp = initialColumnsProp.filter((c) => c.filterable !== false);
4633
4696
  const initialFilterCol = String(filterableColumnsProp[0]?.field || filterableColumnsProp[0]?.key || "");
4634
- const [advancedFilters, setAdvancedFilters] = useState9([
4635
- { column: initialFilterCol, operator: getDefaultOperator(filterableColumnsProp[0]?.type), value: "", logic: "AND" }
4636
- ]);
4697
+ const [advancedFilters, setAdvancedFilters] = useState9(() => {
4698
+ if (initialFilters && initialFilters.length > 0) return initialFilters;
4699
+ return [{ column: initialFilterCol, operator: getDefaultOperator(filterableColumnsProp[0]?.type), value: "", logic: "AND" }];
4700
+ });
4637
4701
  const [colSearch, setColSearch] = useState9("");
4702
+ const [internalGroupingModel, setInternalGroupingModel] = useState9(
4703
+ defaultRowGroupingModel ?? []
4704
+ );
4705
+ const activeGroupingModel = rowGroupingModel ?? internalGroupingModel;
4706
+ const isGroupingActive = activeGroupingModel.length > 0;
4707
+ const setGroupingModel = (model) => {
4708
+ if (!rowGroupingModel) setInternalGroupingModel(model);
4709
+ onRowGroupingModelChange?.(model);
4710
+ };
4711
+ const addToGrouping = (field) => {
4712
+ if (activeGroupingModel.includes(field)) return;
4713
+ setGroupingModel([...activeGroupingModel, field]);
4714
+ };
4715
+ const removeFromGrouping = (field) => {
4716
+ setGroupingModel(activeGroupingModel.filter((f) => f !== field));
4717
+ };
4718
+ const [expandedGroups, setExpandedGroups] = useState9(/* @__PURE__ */ new Set());
4719
+ const prevGroupModelKeyRef = useRef10("");
4720
+ const toggleGroup = (groupId) => {
4721
+ setExpandedGroups((prev) => {
4722
+ const next = new Set(prev);
4723
+ if (next.has(groupId)) next.delete(groupId);
4724
+ else next.add(groupId);
4725
+ return next;
4726
+ });
4727
+ };
4728
+ const [treeExpandedRows, setTreeExpandedRows] = useState9(/* @__PURE__ */ new Set());
4729
+ const toggleTreeRow = (id) => {
4730
+ setTreeExpandedRows((prev) => {
4731
+ const next = new Set(prev);
4732
+ if (next.has(id)) next.delete(id);
4733
+ else next.add(id);
4734
+ return next;
4735
+ });
4736
+ };
4638
4737
  useEffect9(() => {
4639
4738
  const handleMouseMove = (e) => {
4640
4739
  if (!resizingColumn) return;
@@ -4677,6 +4776,13 @@ function DataGrid({
4677
4776
  return next;
4678
4777
  });
4679
4778
  }, [initialColumnsProp]);
4779
+ const getGroupValue = (item, field) => {
4780
+ const col = resolvedColumns.find((c) => String(c.field) === field || String(c.key) === field);
4781
+ const raw = item[field];
4782
+ if (col?.groupingValueGetter) return String(col.groupingValueGetter(raw, item) ?? "");
4783
+ if (col?.valueGetter) return String(col.valueGetter({ value: raw, row: item, field }) ?? "");
4784
+ return raw == null ? "" : String(raw);
4785
+ };
4680
4786
  const onFiltersChangeRef = useRef10(onFiltersChange);
4681
4787
  useEffect9(() => {
4682
4788
  onFiltersChangeRef.current = onFiltersChange;
@@ -4864,14 +4970,36 @@ function DataGrid({
4864
4970
  return 0;
4865
4971
  });
4866
4972
  }, [filteredData, sortField, sortDirection, resolvedColumns]);
4973
+ useEffect9(() => {
4974
+ const key = activeGroupingModel.join("\0");
4975
+ if (key === prevGroupModelKeyRef.current) return;
4976
+ prevGroupModelKeyRef.current = key;
4977
+ if (!activeGroupingModel.length) {
4978
+ setExpandedGroups(/* @__PURE__ */ new Set());
4979
+ return;
4980
+ }
4981
+ const allGroups = getAllGroupIds(sortedData, activeGroupingModel, getGroupValue);
4982
+ setExpandedGroups(new Set(
4983
+ allGroups.filter((g) => isGroupExpandedByDefault ? isGroupExpandedByDefault({ id: g.id, field: g.field, key: g.key, depth: g.depth, count: g.count }) : defaultGroupingExpansionDepth === -1 || g.depth < defaultGroupingExpansionDepth).map((g) => g.id)
4984
+ ));
4985
+ }, [activeGroupingModel.join("\0")]);
4986
+ const resolveChildren = getChildRows ?? ((row) => row.children ?? null);
4987
+ const flatEntries = useMemo2(() => {
4988
+ if (treeData) {
4989
+ return buildTreeEntries(sortedData, resolveChildren, treeExpandedRows);
4990
+ }
4991
+ if (!isGroupingActive) return null;
4992
+ return buildFlatEntries(sortedData, activeGroupingModel, getGroupValue, expandedGroups);
4993
+ }, [treeData, isGroupingActive, sortedData, activeGroupingModel.join("\0"), expandedGroups, treeExpandedRows]);
4867
4994
  const isServer = paginationMode === "server";
4868
- const totalRows = isServer ? rowCount ?? data.length : filteredData.length;
4995
+ const totalRows = isServer ? rowCount ?? data.length : flatEntries ? flatEntries.length : filteredData.length;
4869
4996
  const totalPages = Math.max(1, Math.ceil(totalRows / activePageSize));
4870
- const paginatedData = useMemo2(() => {
4871
- if (isServer) return data;
4997
+ const displayRows = useMemo2(() => {
4998
+ if (isServer) return data.map((row) => ({ kind: "data", row, depth: 0 }));
4999
+ const source = flatEntries ?? sortedData.map((row) => ({ kind: "data", row, depth: 0 }));
4872
5000
  const start = (activePage - 1) * activePageSize;
4873
- return sortedData.slice(start, start + activePageSize);
4874
- }, [isServer, data, sortedData, activePage, activePageSize]);
5001
+ return source.slice(start, start + activePageSize);
5002
+ }, [isServer, data, flatEntries, sortedData, activePage, activePageSize]);
4875
5003
  const handleExport = () => {
4876
5004
  const exportableCols = resolvedColumns.filter((c) => !c.hidden && c.isExportable !== false);
4877
5005
  const headers = exportableCols.map((c) => c.headerName).join(",");
@@ -4921,8 +5049,57 @@ function DataGrid({
4921
5049
  const left = resolvedColumns.filter((c) => !c.hidden && c.pinned === "left");
4922
5050
  const mid = resolvedColumns.filter((c) => !c.hidden && !c.pinned);
4923
5051
  const right = resolvedColumns.filter((c) => !c.hidden && c.pinned === "right");
4924
- return [...left, ...mid, ...right];
4925
- }, [resolvedColumns]);
5052
+ const dataCols = [...left, ...mid, ...right];
5053
+ if (treeData) {
5054
+ const treeCol = {
5055
+ key: "__tree__",
5056
+ field: "__tree__",
5057
+ header: "",
5058
+ headerName: "",
5059
+ width: 44,
5060
+ sortable: false,
5061
+ filterable: false,
5062
+ disableColumnMenu: true,
5063
+ hideable: false
5064
+ };
5065
+ return [treeCol, ...dataCols];
5066
+ }
5067
+ if (!isGroupingActive) return dataCols;
5068
+ if (rowGroupingColumnMode === "multiple") {
5069
+ const groupCols = activeGroupingModel.map((gField, i) => {
5070
+ const src = resolvedColumns.find((c) => String(c.field) === gField || String(c.key) === gField);
5071
+ const def2 = typeof groupingColDef === "function" ? groupingColDef(gField) : groupingColDef;
5072
+ return {
5073
+ key: `__group_${i}__`,
5074
+ field: `__group_${i}__`,
5075
+ header: def2?.headerName ?? src?.header ?? src?.headerName ?? gField,
5076
+ headerName: def2?.headerName ?? src?.header ?? src?.headerName ?? gField,
5077
+ width: def2?.width ?? 160,
5078
+ sortable: false,
5079
+ filterable: false,
5080
+ disableColumnMenu: true,
5081
+ hideable: false,
5082
+ __groupField: gField,
5083
+ __groupIndex: i
5084
+ };
5085
+ });
5086
+ return [...groupCols, ...dataCols];
5087
+ }
5088
+ const def = typeof groupingColDef === "function" ? groupingColDef(activeGroupingModel[0]) : groupingColDef;
5089
+ const singleSrc = resolvedColumns.find((c) => String(c.field) === activeGroupingModel[0] || String(c.key) === activeGroupingModel[0]);
5090
+ const singleGroupCol = {
5091
+ key: "__group__",
5092
+ field: "__group__",
5093
+ header: def?.headerName ?? (activeGroupingModel.length === 1 ? singleSrc?.header ?? singleSrc?.headerName ?? "Group" : "Group"),
5094
+ headerName: def?.headerName ?? (activeGroupingModel.length === 1 ? singleSrc?.header ?? singleSrc?.headerName ?? "Group" : "Group"),
5095
+ width: def?.width ?? 200,
5096
+ sortable: false,
5097
+ filterable: false,
5098
+ disableColumnMenu: true,
5099
+ hideable: false
5100
+ };
5101
+ return [singleGroupCol, ...dataCols];
5102
+ }, [resolvedColumns, isGroupingActive, activeGroupingModel, rowGroupingColumnMode, groupingColDef]);
4926
5103
  const getLeftOffset = (col, idx) => {
4927
5104
  if (col.pinned !== "left") return void 0;
4928
5105
  let offset2 = 0;
@@ -4957,7 +5134,14 @@ function DataGrid({
4957
5134
  };
4958
5135
  const activeMenuCol = activeMenu ? resolvedColumns.find((c) => String(c.field) === activeMenu) : null;
4959
5136
  const alignClass = (align) => align === "center" ? "dg-slot--center" : align === "right" ? "dg-slot--right" : "dg-slot--left";
4960
- return /* @__PURE__ */ React75.createElement("div", { className: ["dg-root", sxClass, className].filter(Boolean).join(" ") }, /* @__PURE__ */ React75.createElement("div", { className: "dg-header" }, /* @__PURE__ */ React75.createElement("div", { className: "dg-header-info" }, /* @__PURE__ */ React75.createElement("h2", null, title), /* @__PURE__ */ React75.createElement("p", null, filteredData.length, " total records")), /* @__PURE__ */ React75.createElement("div", { className: "dg-header-actions" }, /* @__PURE__ */ React75.createElement("div", { className: "dg-search-wrap" }, /* @__PURE__ */ React75.createElement(Search, { size: 15 }), /* @__PURE__ */ React75.createElement(
5137
+ const tOpts = toolbarOptions ?? {};
5138
+ const showSearch = !tOpts.hideSearch;
5139
+ const showFilterBtn = !tOpts.hideFilter;
5140
+ const showColumnsBtn = !tOpts.hideColumns;
5141
+ const showExportBtn = !tOpts.hideExport && !hideTopExport;
5142
+ const showTitle = !tOpts.hideTitle;
5143
+ const showRecordCount = !tOpts.hideRecordCount;
5144
+ return /* @__PURE__ */ React75.createElement("div", { className: ["dg-root", sxClass, className].filter(Boolean).join(" ") }, !tOpts.hideHeader && /* @__PURE__ */ React75.createElement("div", { className: `dg-header${customToolbar ? " dg-header--custom" : ""}` }, !customToolbar && (showTitle || showRecordCount) && /* @__PURE__ */ React75.createElement("div", { className: "dg-header-info" }, showTitle && /* @__PURE__ */ React75.createElement("h2", null, title), showRecordCount && /* @__PURE__ */ React75.createElement("p", null, filteredData.length, " total records")), /* @__PURE__ */ React75.createElement("div", { className: "dg-header-actions" }, customToolbar ?? /* @__PURE__ */ React75.createElement(React75.Fragment, null, showSearch && /* @__PURE__ */ React75.createElement("div", { className: "dg-search-wrap" }, /* @__PURE__ */ React75.createElement(Search, { size: 15 }), /* @__PURE__ */ React75.createElement(
4961
5145
  "input",
4962
5146
  {
4963
5147
  className: "dg-search",
@@ -4968,23 +5152,35 @@ function DataGrid({
4968
5152
  setCurrentPage(1);
4969
5153
  }
4970
5154
  }
4971
- )), /* @__PURE__ */ React75.createElement(Tooltip, { title: "Filters", placement: "top" }, /* @__PURE__ */ React75.createElement(
5155
+ )), showFilterBtn && /* @__PURE__ */ React75.createElement(Tooltip, { title: "Filters", placement: "top" }, /* @__PURE__ */ React75.createElement(
4972
5156
  "button",
4973
5157
  {
4974
5158
  className: `dg-icon-btn ${hasActiveFilters ? "active" : ""}`,
4975
5159
  onClick: () => setShowAdvancedFilter(true)
4976
5160
  },
4977
5161
  /* @__PURE__ */ React75.createElement(Filter, { size: 16 })
4978
- )), /* @__PURE__ */ React75.createElement(Tooltip, { title: "Manage Columns", placement: "top" }, /* @__PURE__ */ React75.createElement(
5162
+ )), showColumnsBtn && /* @__PURE__ */ React75.createElement(Tooltip, { title: "Manage Columns", placement: "top" }, /* @__PURE__ */ React75.createElement(
4979
5163
  "button",
4980
5164
  {
4981
5165
  className: "dg-icon-btn",
4982
5166
  onClick: () => setShowManageColumns(true)
4983
5167
  },
4984
5168
  /* @__PURE__ */ React75.createElement(Columns, { size: 16 })
4985
- )), !hideTopExport && /* @__PURE__ */ React75.createElement("button", { className: "dg-action-btn", onClick: handleExport }, /* @__PURE__ */ React75.createElement(Download, { size: 14 }), " Export CSV"), headerActions && /* @__PURE__ */ React75.createElement("div", { className: `dg-header-slot ${alignClass(headerActions.align)}` }, headerActions.content))), /* @__PURE__ */ React75.createElement("div", { className: `dg-toolbar ${alignClass(toolbarContent?.align)}` }, toolbarContent?.content || ""), /* @__PURE__ */ React75.createElement("div", { className: `dg-table-wrap${paginatedData.length === 0 && !loading ? " dg-table-wrap--empty" : ""}` }, loading && /* @__PURE__ */ React75.createElement("div", { className: "dg-loading-overlay" }, /* @__PURE__ */ React75.createElement("div", { className: "dg-loading-spinner" })), /* @__PURE__ */ React75.createElement("table", { className: "dg-table" }, /* @__PURE__ */ React75.createElement("thead", null, /* @__PURE__ */ React75.createElement("tr", null, visibleColumns.map((col, idx) => {
5169
+ )), showExportBtn && /* @__PURE__ */ React75.createElement("button", { className: "dg-action-btn", onClick: handleExport }, /* @__PURE__ */ React75.createElement(Download, { size: 14 }), " Export CSV")), headerActions && /* @__PURE__ */ React75.createElement("div", { className: `dg-header-slot ${alignClass(headerActions.align)}` }, headerActions.content))), !tOpts.hideHeader && /* @__PURE__ */ React75.createElement("div", { className: `dg-toolbar ${alignClass(toolbarContent?.align)}` }, toolbarContent?.content || ""), isGroupingActive && /* @__PURE__ */ React75.createElement("div", { className: "dg-grouping-bar" }, /* @__PURE__ */ React75.createElement("span", { className: "dg-grouping-bar-label" }, "Grouped by"), activeGroupingModel.map((gField) => {
5170
+ const col = resolvedColumns.find((c) => String(c.field) === gField || String(c.key) === gField);
5171
+ return /* @__PURE__ */ React75.createElement("div", { key: gField, className: "dg-group-chip" }, /* @__PURE__ */ React75.createElement(Layers, { size: 11 }), /* @__PURE__ */ React75.createElement("span", null, col?.header ?? col?.headerName ?? gField), !disableRowGrouping && /* @__PURE__ */ React75.createElement(
5172
+ "button",
5173
+ {
5174
+ className: "dg-group-chip-remove",
5175
+ onClick: () => removeFromGrouping(gField),
5176
+ title: `Remove grouping by ${col?.header ?? gField}`
5177
+ },
5178
+ /* @__PURE__ */ React75.createElement(X2, { size: 10 })
5179
+ ));
5180
+ })), /* @__PURE__ */ React75.createElement("div", { className: `dg-table-wrap${filteredData.length === 0 && !loading ? " dg-table-wrap--empty" : ""} ${tOpts.hideHeader ? "rm-top-border" : ""} ${tOpts.hideFooter ? "rm-bottom-border" : ""}` }, loading && /* @__PURE__ */ React75.createElement("div", { className: "dg-loading-overlay" }, /* @__PURE__ */ React75.createElement("div", { className: "dg-loading-spinner" })), /* @__PURE__ */ React75.createElement("table", { className: "dg-table" }, /* @__PURE__ */ React75.createElement("thead", null, /* @__PURE__ */ React75.createElement("tr", null, visibleColumns.map((col, idx) => {
4986
5181
  const colField = String(col.field);
4987
- const width = columnWidths[colField] || 200;
5182
+ const colNativeWidth = col.width ? typeof col.width === "number" ? col.width : parseInt(String(col.width)) : 200;
5183
+ const width = columnWidths[colField] || colNativeWidth;
4988
5184
  const leftOffset = getLeftOffset(col, idx);
4989
5185
  const rightOffset = getRightOffset(col, idx);
4990
5186
  const isSorted = sortField === col.field;
@@ -5035,79 +5231,150 @@ function DataGrid({
5035
5231
  }
5036
5232
  )))
5037
5233
  );
5038
- }), actions && /* @__PURE__ */ React75.createElement("th", { style: { width: 0, padding: 0 } }))), /* @__PURE__ */ React75.createElement("tbody", null, paginatedData.length > 0 && paginatedData.map((item) => /* @__PURE__ */ React75.createElement("tr", { key: item.id, className: "dg-tbody-row", onDoubleClick: () => onRowDoubleClick?.(item) }, visibleColumns.map((col, idx) => {
5039
- const colField = String(col.field);
5040
- const width = columnWidths[colField] || 200;
5041
- const leftOffset = getLeftOffset(col, idx);
5042
- const rightOffset = getRightOffset(col, idx);
5043
- return /* @__PURE__ */ React75.createElement(
5044
- "td",
5045
- {
5046
- key: `${item.id}-${colField}`,
5047
- className: `dg-td${col.pinned === "left" ? " pinned-left" : col.pinned === "right" ? " pinned-right" : ""}${col.editable ? " dg-td--editable" : ""} ${col.cellClassName || ""}`,
5048
- style: { width, minWidth: width, maxWidth: width, left: leftOffset, right: rightOffset, flex: col.flex },
5049
- onDoubleClick: () => onCellDoubleClick?.({ row: item, field: colField, value: item[col.field || ""] }),
5050
- onClick: col.editable ? () => {
5051
- const field = String(col.field);
5052
- const rawValue = item[col.field || ""];
5053
- const value = col.valueGetter ? col.valueGetter({ value: rawValue, row: item, field }) : rawValue;
5054
- setEditingCell({ rowId: item.id, field, value });
5055
- } : void 0
5056
- },
5057
- (() => {
5058
- const field = String(col.field);
5059
- const rawValue = item[col.field || ""];
5060
- let value = col.valueGetter ? col.valueGetter({ value: rawValue, row: item, field }) : rawValue;
5061
- if (col.editable && editingCell?.rowId === item.id && editingCell?.field === field) {
5062
- const inputType = col.type === "number" ? "number" : col.type === "date" ? "date" : "text";
5063
- const commit = (finalValue) => {
5064
- setEditingCell(null);
5065
- col.onEnter?.({ value: finalValue, row: item, field });
5066
- };
5067
- return /* @__PURE__ */ React75.createElement(
5068
- "input",
5234
+ }), actions && /* @__PURE__ */ React75.createElement("th", { style: { width: 0, padding: 0 } }))), /* @__PURE__ */ React75.createElement("tbody", null, displayRows.length > 0 && displayRows.map((entry) => {
5235
+ if (entry.kind === "group") {
5236
+ const isExpanded = expandedGroups.has(entry.id);
5237
+ const colDef = typeof groupingColDef === "function" ? groupingColDef(entry.field) : groupingColDef;
5238
+ const fieldLabel = resolvedColumns.find(
5239
+ (c) => String(c.field) === entry.field || String(c.key) === entry.field
5240
+ )?.header ?? entry.field;
5241
+ const totalCols = visibleColumns.length + (actions ? 1 : 0);
5242
+ return /* @__PURE__ */ React75.createElement("tr", { key: entry.id, className: "dg-group-row" }, /* @__PURE__ */ React75.createElement("td", { colSpan: totalCols, className: "dg-group-cell" }, /* @__PURE__ */ React75.createElement(
5243
+ "div",
5244
+ {
5245
+ className: "dg-group-cell-inner",
5246
+ style: { paddingLeft: entry.depth * 20 },
5247
+ onClick: () => toggleGroup(entry.id)
5248
+ },
5249
+ /* @__PURE__ */ React75.createElement("button", { className: "dg-group-toggle", onClick: (e) => {
5250
+ e.stopPropagation();
5251
+ toggleGroup(entry.id);
5252
+ } }, isExpanded ? /* @__PURE__ */ React75.createElement(ChevronDown, { size: 15 }) : /* @__PURE__ */ React75.createElement(ChevronRight, { size: 15 })),
5253
+ activeGroupingModel.length > 1 && /* @__PURE__ */ React75.createElement("span", { className: "dg-group-field-label" }, fieldLabel, ":"),
5254
+ colDef?.renderCell ? colDef.renderCell({ groupKey: entry.key, field: entry.field, depth: entry.depth, leafCount: entry.leafCount }) : /* @__PURE__ */ React75.createElement("span", { className: "dg-group-key" }, entry.key),
5255
+ !colDef?.hideDescendantCount && /* @__PURE__ */ React75.createElement("span", { className: "dg-group-count" }, entry.leafCount)
5256
+ )));
5257
+ }
5258
+ const item = entry.row;
5259
+ const rowDepth = entry.depth;
5260
+ return /* @__PURE__ */ React75.createElement("tr", { key: item.id, className: "dg-tbody-row", onDoubleClick: () => onRowDoubleClick?.(item) }, visibleColumns.map((col, idx) => {
5261
+ const colField = String(col.field);
5262
+ if (colField === "__tree__") {
5263
+ const treeColWidth = columnWidths["__tree__"] || 44;
5264
+ const isExpanded = treeExpandedRows.has(item.id);
5265
+ return /* @__PURE__ */ React75.createElement(
5266
+ "td",
5267
+ {
5268
+ key: `${item.id}-__tree__`,
5269
+ className: "dg-tree-cell",
5270
+ style: { width: treeColWidth, minWidth: treeColWidth, maxWidth: treeColWidth }
5271
+ },
5272
+ /* @__PURE__ */ React75.createElement("div", { className: "dg-tree-cell-inner", style: { paddingLeft: rowDepth * 20 } }, entry.hasChildren ? /* @__PURE__ */ React75.createElement(
5273
+ "button",
5069
5274
  {
5070
- className: "dg-cell-editor",
5071
- type: inputType,
5072
- autoFocus: true,
5073
- defaultValue: editingCell.value ?? "",
5074
- onClick: (e) => e.stopPropagation(),
5075
- onKeyDown: (e) => {
5076
- if (e.key === "Enter") commit(e.target.value);
5077
- if (e.key === "Escape") setEditingCell(null);
5078
- },
5079
- onBlur: (e) => {
5080
- const finalValue = e.target.value;
5081
- setEditingCell(null);
5082
- col.onBlur?.({ value: finalValue, row: item, field });
5275
+ className: "dg-group-toggle",
5276
+ onClick: (e) => {
5277
+ e.stopPropagation();
5278
+ toggleTreeRow(item.id);
5083
5279
  }
5084
- }
5085
- );
5086
- }
5087
- const formattedValue = col.valueFormatter ? col.valueFormatter({ value, row: item, field }) : value;
5088
- if (col.renderCell) {
5089
- return col.renderCell({ value, row: item, field });
5090
- }
5091
- if (col.render) {
5092
- return col.render(value, item);
5280
+ },
5281
+ isExpanded ? /* @__PURE__ */ React75.createElement(ChevronDown, { size: 15 }) : /* @__PURE__ */ React75.createElement(ChevronRight, { size: 15 })
5282
+ ) : /* @__PURE__ */ React75.createElement("span", { style: { display: "inline-block", width: 22 } }))
5283
+ );
5284
+ }
5285
+ if (colField === "__group__" || colField.startsWith("__group_")) {
5286
+ const colDef = typeof groupingColDef === "function" ? groupingColDef(colField) : groupingColDef;
5287
+ const leafField = colDef?.leafField;
5288
+ let leafContent = null;
5289
+ if (leafField) {
5290
+ const leafCol = resolvedColumns.find((c) => String(c.field) === leafField || String(c.key) === leafField);
5291
+ if (leafCol) {
5292
+ const raw = item[String(leafCol.field)];
5293
+ let val = leafCol.valueGetter ? leafCol.valueGetter({ value: raw, row: item, field: leafField }) : raw;
5294
+ if (leafCol.valueFormatter) val = leafCol.valueFormatter({ value: val, row: item, field: leafField });
5295
+ if (leafCol.renderCell) leafContent = leafCol.renderCell({ value: val, row: item, field: leafField });
5296
+ else leafContent = val == null ? "" : String(val);
5297
+ }
5093
5298
  }
5094
- return String(formattedValue ?? "");
5095
- })()
5096
- );
5097
- }), actions && /* @__PURE__ */ React75.createElement("td", { className: "dg-row-actions-cell" }, (() => {
5098
- const resolvedActions = typeof actions === "function" ? actions(item) : actions;
5099
- const visibleActions = resolvedActions.filter((a) => !a.show || a.show(item));
5100
- if (visibleActions.length === 0) return null;
5101
- return /* @__PURE__ */ React75.createElement("div", { className: "dg-row-actions" }, /* @__PURE__ */ React75.createElement("div", { className: "dg-action-group" }, visibleActions.map((action, i) => /* @__PURE__ */ React75.createElement(Tooltip, { key: i, title: action.label, placement: "top" }, /* @__PURE__ */ React75.createElement(
5102
- "button",
5103
- {
5104
- className: "dg-row-action-btn",
5105
- style: { color: action.color || "var(--text-secondary)" },
5106
- onClick: () => action.onClick(item)
5107
- },
5108
- action.icon
5109
- )))));
5110
- })()))))), paginatedData.length === 0 && /* @__PURE__ */ React75.createElement("div", { className: "dg-empty-state" }, /* @__PURE__ */ React75.createElement("svg", { className: "dg-empty-icon", viewBox: "0 0 200 160", fill: "none", xmlns: "http://www.w3.org/2000/svg" }, /* @__PURE__ */ React75.createElement("rect", { x: "20", y: "30", width: "160", height: "100", rx: "8", fill: "var(--hover-color)", stroke: "var(--border-color)", strokeWidth: "1.5" }), /* @__PURE__ */ React75.createElement("rect", { x: "20", y: "30", width: "160", height: "28", rx: "8", fill: "var(--border-color)", opacity: "0.5" }), /* @__PURE__ */ React75.createElement("rect", { x: "20", y: "50", width: "160", height: "8", rx: "0", fill: "var(--border-color)", opacity: "0.5" }), /* @__PURE__ */ React75.createElement("line", { x1: "72", y1: "30", x2: "72", y2: "130", stroke: "var(--border-color)", strokeWidth: "1" }), /* @__PURE__ */ React75.createElement("line", { x1: "128", y1: "30", x2: "128", y2: "130", stroke: "var(--border-color)", strokeWidth: "1" }), /* @__PURE__ */ React75.createElement("line", { x1: "20", y1: "78", x2: "180", y2: "78", stroke: "var(--border-color)", strokeWidth: "1" }), /* @__PURE__ */ React75.createElement("line", { x1: "20", y1: "104", x2: "180", y2: "104", stroke: "var(--border-color)", strokeWidth: "1" }), /* @__PURE__ */ React75.createElement("rect", { x: "32", y: "87", width: "28", height: "6", rx: "3", fill: "var(--border-color)", opacity: "0.4" }), /* @__PURE__ */ React75.createElement("rect", { x: "84", y: "87", width: "28", height: "6", rx: "3", fill: "var(--border-color)", opacity: "0.4" }), /* @__PURE__ */ React75.createElement("rect", { x: "140", y: "87", width: "28", height: "6", rx: "3", fill: "var(--border-color)", opacity: "0.4" }), /* @__PURE__ */ React75.createElement("rect", { x: "32", y: "113", width: "20", height: "6", rx: "3", fill: "var(--border-color)", opacity: "0.3" }), /* @__PURE__ */ React75.createElement("rect", { x: "84", y: "113", width: "32", height: "6", rx: "3", fill: "var(--border-color)", opacity: "0.3" }), /* @__PURE__ */ React75.createElement("rect", { x: "140", y: "113", width: "20", height: "6", rx: "3", fill: "var(--border-color)", opacity: "0.3" }), /* @__PURE__ */ React75.createElement("circle", { cx: "148", cy: "108", r: "26", fill: "var(--surface-color)", stroke: "var(--border-color)", strokeWidth: "1.5" }), /* @__PURE__ */ React75.createElement("circle", { cx: "145", cy: "105", r: "10", stroke: "var(--text-secondary)", strokeWidth: "2.5", opacity: "0.5" }), /* @__PURE__ */ React75.createElement("line", { x1: "152", y1: "113", x2: "161", y2: "122", stroke: "var(--text-secondary)", strokeWidth: "2.5", strokeLinecap: "round", opacity: "0.5" }), /* @__PURE__ */ React75.createElement("line", { x1: "141", y1: "101", x2: "149", y2: "109", stroke: "var(--text-secondary)", strokeWidth: "2", strokeLinecap: "round", opacity: "0.5" }), /* @__PURE__ */ React75.createElement("line", { x1: "149", y1: "101", x2: "141", y2: "109", stroke: "var(--text-secondary)", strokeWidth: "2", strokeLinecap: "round", opacity: "0.5" })), /* @__PURE__ */ React75.createElement("p", { className: "dg-empty-title" }, "No data found"), /* @__PURE__ */ React75.createElement("p", { className: "dg-empty-subtitle" }, filterText || hasActiveFilters ? "Try adjusting your search or filters" : "No records to display"))), pagination && /* @__PURE__ */ React75.createElement("div", { className: "dg-pagination" }, /* @__PURE__ */ React75.createElement("div", { className: "dg-page-info" }, /* @__PURE__ */ React75.createElement(Tooltip, { title: "Export CSV", placement: "top" }, /* @__PURE__ */ React75.createElement("button", { className: "dg-icon-btn", onClick: handleExport }, /* @__PURE__ */ React75.createElement(Download, { size: 14 }))), /* @__PURE__ */ React75.createElement("div", { className: "dg-per-page" }, /* @__PURE__ */ React75.createElement("span", null, "Rows per page:"), /* @__PURE__ */ React75.createElement(
5299
+ const groupColWidth = columnWidths[colField] || 200;
5300
+ return /* @__PURE__ */ React75.createElement(
5301
+ "td",
5302
+ {
5303
+ key: `${item.id}-${colField}`,
5304
+ className: "dg-group-leaf-cell",
5305
+ style: { width: groupColWidth, minWidth: groupColWidth, maxWidth: groupColWidth, paddingLeft: rowDepth * 20 + 32 }
5306
+ },
5307
+ leafContent
5308
+ );
5309
+ }
5310
+ const width = columnWidths[colField] || 200;
5311
+ const leftOffset = getLeftOffset(col, idx);
5312
+ const rightOffset = getRightOffset(col, idx);
5313
+ return /* @__PURE__ */ React75.createElement(
5314
+ "td",
5315
+ {
5316
+ key: `${item.id}-${colField}`,
5317
+ className: `dg-td${col.pinned === "left" ? " pinned-left" : col.pinned === "right" ? " pinned-right" : ""}${col.editable ? " dg-td--editable" : ""} ${col.cellClassName || ""}`,
5318
+ style: { width, minWidth: width, maxWidth: width, left: leftOffset, right: rightOffset, flex: col.flex },
5319
+ onDoubleClick: () => onCellDoubleClick?.({ row: item, field: colField, value: item[col.field || ""] }),
5320
+ onClick: col.editable ? () => {
5321
+ const field = String(col.field);
5322
+ const rawValue = item[col.field || ""];
5323
+ const value = col.valueGetter ? col.valueGetter({ value: rawValue, row: item, field }) : rawValue;
5324
+ setEditingCell({ rowId: item.id, field, value });
5325
+ } : void 0
5326
+ },
5327
+ (() => {
5328
+ const field = String(col.field);
5329
+ const rawValue = item[col.field || ""];
5330
+ let value = col.valueGetter ? col.valueGetter({ value: rawValue, row: item, field }) : rawValue;
5331
+ if (col.editable && editingCell?.rowId === item.id && editingCell?.field === field) {
5332
+ const inputType = col.type === "number" ? "number" : col.type === "date" ? "date" : "text";
5333
+ const commit = (finalValue) => {
5334
+ setEditingCell(null);
5335
+ col.onEnter?.({ value: finalValue, row: item, field });
5336
+ };
5337
+ return /* @__PURE__ */ React75.createElement(
5338
+ "input",
5339
+ {
5340
+ className: "dg-cell-editor",
5341
+ type: inputType,
5342
+ autoFocus: true,
5343
+ defaultValue: editingCell.value ?? "",
5344
+ onClick: (e) => e.stopPropagation(),
5345
+ onKeyDown: (e) => {
5346
+ if (e.key === "Enter") commit(e.target.value);
5347
+ if (e.key === "Escape") setEditingCell(null);
5348
+ },
5349
+ onBlur: (e) => {
5350
+ const finalValue = e.target.value;
5351
+ setEditingCell(null);
5352
+ col.onBlur?.({ value: finalValue, row: item, field });
5353
+ }
5354
+ }
5355
+ );
5356
+ }
5357
+ const formattedValue = col.valueFormatter ? col.valueFormatter({ value, row: item, field }) : value;
5358
+ if (col.renderCell) return col.renderCell({ value, row: item, field });
5359
+ if (col.render) return col.render(value, item);
5360
+ return String(formattedValue ?? "");
5361
+ })()
5362
+ );
5363
+ }), actions && /* @__PURE__ */ React75.createElement("td", { className: "dg-row-actions-cell" }, (() => {
5364
+ const resolvedActions = typeof actions === "function" ? actions(item) : actions;
5365
+ const visibleActions = resolvedActions.filter((a) => !a.show || a.show(item));
5366
+ if (visibleActions.length === 0) return null;
5367
+ return /* @__PURE__ */ React75.createElement("div", { className: "dg-row-actions" }, /* @__PURE__ */ React75.createElement("div", { className: "dg-action-group" }, visibleActions.map((action, i) => /* @__PURE__ */ React75.createElement(Tooltip, { key: i, title: action.label, placement: "top" }, /* @__PURE__ */ React75.createElement(
5368
+ "button",
5369
+ {
5370
+ className: "dg-row-action-btn",
5371
+ style: { color: action.color || "var(--text-secondary)" },
5372
+ onClick: () => action.onClick(item)
5373
+ },
5374
+ action.icon
5375
+ )))));
5376
+ })()));
5377
+ }))), filteredData.length === 0 && /* @__PURE__ */ React75.createElement("div", { className: "dg-empty-state" }, /* @__PURE__ */ React75.createElement("svg", { className: "dg-empty-icon", viewBox: "0 0 200 160", fill: "none", xmlns: "http://www.w3.org/2000/svg" }, /* @__PURE__ */ React75.createElement("rect", { x: "20", y: "30", width: "160", height: "100", rx: "8", fill: "var(--hover-color)", stroke: "var(--border-color)", strokeWidth: "1.5" }), /* @__PURE__ */ React75.createElement("rect", { x: "20", y: "30", width: "160", height: "28", rx: "8", fill: "var(--border-color)", opacity: "0.5" }), /* @__PURE__ */ React75.createElement("rect", { x: "20", y: "50", width: "160", height: "8", rx: "0", fill: "var(--border-color)", opacity: "0.5" }), /* @__PURE__ */ React75.createElement("line", { x1: "72", y1: "30", x2: "72", y2: "130", stroke: "var(--border-color)", strokeWidth: "1" }), /* @__PURE__ */ React75.createElement("line", { x1: "128", y1: "30", x2: "128", y2: "130", stroke: "var(--border-color)", strokeWidth: "1" }), /* @__PURE__ */ React75.createElement("line", { x1: "20", y1: "78", x2: "180", y2: "78", stroke: "var(--border-color)", strokeWidth: "1" }), /* @__PURE__ */ React75.createElement("line", { x1: "20", y1: "104", x2: "180", y2: "104", stroke: "var(--border-color)", strokeWidth: "1" }), /* @__PURE__ */ React75.createElement("rect", { x: "32", y: "87", width: "28", height: "6", rx: "3", fill: "var(--border-color)", opacity: "0.4" }), /* @__PURE__ */ React75.createElement("rect", { x: "84", y: "87", width: "28", height: "6", rx: "3", fill: "var(--border-color)", opacity: "0.4" }), /* @__PURE__ */ React75.createElement("rect", { x: "140", y: "87", width: "28", height: "6", rx: "3", fill: "var(--border-color)", opacity: "0.4" }), /* @__PURE__ */ React75.createElement("rect", { x: "32", y: "113", width: "20", height: "6", rx: "3", fill: "var(--border-color)", opacity: "0.3" }), /* @__PURE__ */ React75.createElement("rect", { x: "84", y: "113", width: "32", height: "6", rx: "3", fill: "var(--border-color)", opacity: "0.3" }), /* @__PURE__ */ React75.createElement("rect", { x: "140", y: "113", width: "20", height: "6", rx: "3", fill: "var(--border-color)", opacity: "0.3" }), /* @__PURE__ */ React75.createElement("circle", { cx: "148", cy: "108", r: "26", fill: "var(--surface-color)", stroke: "var(--border-color)", strokeWidth: "1.5" }), /* @__PURE__ */ React75.createElement("circle", { cx: "145", cy: "105", r: "10", stroke: "var(--text-secondary)", strokeWidth: "2.5", opacity: "0.5" }), /* @__PURE__ */ React75.createElement("line", { x1: "152", y1: "113", x2: "161", y2: "122", stroke: "var(--text-secondary)", strokeWidth: "2.5", strokeLinecap: "round", opacity: "0.5" }), /* @__PURE__ */ React75.createElement("line", { x1: "141", y1: "101", x2: "149", y2: "109", stroke: "var(--text-secondary)", strokeWidth: "2", strokeLinecap: "round", opacity: "0.5" }), /* @__PURE__ */ React75.createElement("line", { x1: "149", y1: "101", x2: "141", y2: "109", stroke: "var(--text-secondary)", strokeWidth: "2", strokeLinecap: "round", opacity: "0.5" })), /* @__PURE__ */ React75.createElement("p", { className: "dg-empty-title" }, "No data found"), /* @__PURE__ */ React75.createElement("p", { className: "dg-empty-subtitle" }, filterText || hasActiveFilters ? "Try adjusting your search or filters" : "No records to display"))), customFooter ? /* @__PURE__ */ React75.createElement("div", { className: "dg-pagination dg-pagination--custom" }, customFooter) : pagination && !tOpts.hideFooter && /* @__PURE__ */ React75.createElement("div", { className: "dg-pagination" }, /* @__PURE__ */ React75.createElement("div", { className: "dg-page-info" }, /* @__PURE__ */ React75.createElement(Tooltip, { title: "Export CSV", placement: "top" }, /* @__PURE__ */ React75.createElement("button", { className: "dg-icon-btn", onClick: handleExport }, /* @__PURE__ */ React75.createElement(Download, { size: 14 }))), /* @__PURE__ */ React75.createElement("div", { className: "dg-per-page" }, /* @__PURE__ */ React75.createElement("span", null, "Rows per page:"), /* @__PURE__ */ React75.createElement(
5111
5378
  FilterSelect,
5112
5379
  {
5113
5380
  placement: "top",
@@ -5148,6 +5415,17 @@ function DataGrid({
5148
5415
  setShowAdvancedFilter(true);
5149
5416
  setActiveMenu(null);
5150
5417
  } }, /* @__PURE__ */ React75.createElement(Filter, { size: 14 }), " Filter\u2026"),
5418
+ !disableRowGrouping && activeMenuCol?.groupable !== false && (() => {
5419
+ const gField = String(activeMenuCol?.field ?? "");
5420
+ const isGrouped = activeGroupingModel.includes(gField);
5421
+ return isGrouped ? /* @__PURE__ */ React75.createElement("button", { className: "dg-menu-item", onClick: () => {
5422
+ removeFromGrouping(gField);
5423
+ setActiveMenu(null);
5424
+ } }, /* @__PURE__ */ React75.createElement(Layers, { size: 14 }), " Ungroup") : /* @__PURE__ */ React75.createElement("button", { className: "dg-menu-item", onClick: () => {
5425
+ addToGrouping(gField);
5426
+ setActiveMenu(null);
5427
+ } }, /* @__PURE__ */ React75.createElement(Layers, { size: 14 }), " Group by");
5428
+ })(),
5151
5429
  /* @__PURE__ */ React75.createElement("button", { className: "dg-menu-item", onClick: () => toggleHide(activeMenu) }, /* @__PURE__ */ React75.createElement(EyeOff, { size: 14 }), " Hide column"),
5152
5430
  /* @__PURE__ */ React75.createElement("button", { className: "dg-menu-item", onClick: () => {
5153
5431
  setShowManageColumns(true);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rufous/ui",
3
3
  "private": false,
4
- "version": "0.3.23",
4
+ "version": "0.3.24",
5
5
  "type": "module",
6
6
  "description": "Experimental: A lightweight React UI component library (Beta)",
7
7
  "style": "./dist/main.css",