@rufous/ui 0.2.105 → 0.3.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.
package/dist/main.cjs CHANGED
@@ -3616,15 +3616,27 @@ var DateField = ({
3616
3616
  const spaceBelow = window.innerHeight - rect.bottom - GAP2;
3617
3617
  const spaceAbove = rect.top - GAP2;
3618
3618
  const useDropUp = spaceBelow < PICKER_H && spaceAbove > spaceBelow;
3619
- const top = useDropUp ? Math.max(4, rect.top - PICKER_H - GAP2) : rect.bottom + GAP2;
3619
+ if (useDropUp) {
3620
+ return {
3621
+ position: "fixed",
3622
+ left: rect.left,
3623
+ // bottom anchors picker's bottom edge exactly to field top — no gap regardless of actual height
3624
+ bottom: window.innerHeight - rect.top + GAP2,
3625
+ // prevent going above viewport
3626
+ maxHeight: rect.top - GAP2 - 4,
3627
+ overflowY: "auto",
3628
+ zIndex: 99999,
3629
+ animationName: "rf-date-picker-appear-up",
3630
+ transformOrigin: "bottom left"
3631
+ };
3632
+ }
3620
3633
  return {
3621
3634
  position: "fixed",
3622
3635
  left: rect.left,
3623
- top,
3636
+ top: rect.bottom + GAP2,
3624
3637
  zIndex: 99999,
3625
- // Drive animation from same decision — no separate state/re-render needed
3626
- animationName: useDropUp ? "rf-date-picker-appear-up" : "rf-date-picker-appear",
3627
- transformOrigin: useDropUp ? "bottom left" : "top left"
3638
+ animationName: "rf-date-picker-appear",
3639
+ transformOrigin: "top left"
3628
3640
  };
3629
3641
  })(),
3630
3642
  onMouseDown: (e) => e.preventDefault()
@@ -4427,6 +4439,12 @@ function DataGrid({
4427
4439
  columns: initialColumnsProp,
4428
4440
  data,
4429
4441
  actions,
4442
+ loading = false,
4443
+ pagination = true,
4444
+ paginationMode = "client",
4445
+ rowCount,
4446
+ paginationModel,
4447
+ onPaginationModelChange,
4430
4448
  pageSize: initialPageSize = 10,
4431
4449
  pageSizeOptions = [5, 10, 25, 50],
4432
4450
  title,
@@ -4467,6 +4485,23 @@ function DataGrid({
4467
4485
  const [sortDirection, setSortDirection] = (0, import_react23.useState)(null);
4468
4486
  const [filterText, setFilterText] = (0, import_react23.useState)("");
4469
4487
  const [currentPage, setCurrentPage] = (0, import_react23.useState)(1);
4488
+ const activePage = paginationModel ? paginationModel.page + 1 : currentPage;
4489
+ const activePageSize = paginationModel ? paginationModel.pageSize : pageSize;
4490
+ const handlePageChange = (newPage) => {
4491
+ if (onPaginationModelChange) {
4492
+ onPaginationModelChange({ page: newPage - 1, pageSize: activePageSize });
4493
+ } else {
4494
+ setCurrentPage(newPage);
4495
+ }
4496
+ };
4497
+ const handlePageSizeChange = (newSize) => {
4498
+ if (onPaginationModelChange) {
4499
+ onPaginationModelChange({ page: 0, pageSize: newSize });
4500
+ } else {
4501
+ setPageSize(newSize);
4502
+ setCurrentPage(1);
4503
+ }
4504
+ };
4470
4505
  const [resizingColumn, setResizingColumn] = (0, import_react23.useState)(null);
4471
4506
  const [startX, setStartX] = (0, import_react23.useState)(0);
4472
4507
  const [startWidth, setStartWidth] = (0, import_react23.useState)(0);
@@ -4699,11 +4734,14 @@ function DataGrid({
4699
4734
  return 0;
4700
4735
  });
4701
4736
  }, [filteredData, sortField, sortDirection, resolvedColumns]);
4702
- const totalPages = Math.max(1, Math.ceil(sortedData.length / pageSize));
4737
+ const isServer = paginationMode === "server";
4738
+ const totalRows = isServer ? rowCount ?? data.length : filteredData.length;
4739
+ const totalPages = Math.max(1, Math.ceil(totalRows / activePageSize));
4703
4740
  const paginatedData = (0, import_react23.useMemo)(() => {
4704
- const start = (currentPage - 1) * pageSize;
4705
- return sortedData.slice(start, start + pageSize);
4706
- }, [sortedData, currentPage, pageSize]);
4741
+ if (isServer) return data;
4742
+ const start = (activePage - 1) * activePageSize;
4743
+ return sortedData.slice(start, start + activePageSize);
4744
+ }, [isServer, data, sortedData, activePage, activePageSize]);
4707
4745
  const handleExport = () => {
4708
4746
  const exportableCols = resolvedColumns.filter((c) => !c.hidden && c.isExportable !== false);
4709
4747
  const headers = exportableCols.map((c) => c.headerName).join(",");
@@ -4798,7 +4836,7 @@ function DataGrid({
4798
4836
  onClick: () => setShowManageColumns(true)
4799
4837
  },
4800
4838
  /* @__PURE__ */ import_react23.default.createElement(import_lucide_react2.Columns, { size: 16 })
4801
- )), /* @__PURE__ */ import_react23.default.createElement("button", { className: "dg-action-btn", onClick: handleExport }, /* @__PURE__ */ import_react23.default.createElement(import_lucide_react2.Download, { size: 14 }), " Export CSV"), headerActions && /* @__PURE__ */ import_react23.default.createElement("div", { className: `dg-header-slot ${alignClass(headerActions.align)}` }, headerActions.content))), /* @__PURE__ */ import_react23.default.createElement("div", { className: `dg-toolbar ${alignClass(toolbarContent?.align)}` }, toolbarContent?.content || ""), /* @__PURE__ */ import_react23.default.createElement("div", { className: `dg-table-wrap${paginatedData.length === 0 ? " dg-table-wrap--empty" : ""}` }, /* @__PURE__ */ import_react23.default.createElement("table", { className: "dg-table" }, /* @__PURE__ */ import_react23.default.createElement("thead", null, /* @__PURE__ */ import_react23.default.createElement("tr", null, visibleColumns.map((col, idx) => {
4839
+ )), /* @__PURE__ */ import_react23.default.createElement("button", { className: "dg-action-btn", onClick: handleExport }, /* @__PURE__ */ import_react23.default.createElement(import_lucide_react2.Download, { size: 14 }), " Export CSV"), headerActions && /* @__PURE__ */ import_react23.default.createElement("div", { className: `dg-header-slot ${alignClass(headerActions.align)}` }, headerActions.content))), /* @__PURE__ */ import_react23.default.createElement("div", { className: `dg-toolbar ${alignClass(toolbarContent?.align)}` }, toolbarContent?.content || ""), /* @__PURE__ */ import_react23.default.createElement("div", { className: `dg-table-wrap${paginatedData.length === 0 && !loading ? " dg-table-wrap--empty" : ""}` }, loading && /* @__PURE__ */ import_react23.default.createElement("div", { className: "dg-loading-overlay" }, /* @__PURE__ */ import_react23.default.createElement("div", { className: "dg-loading-spinner" })), /* @__PURE__ */ import_react23.default.createElement("table", { className: "dg-table" }, /* @__PURE__ */ import_react23.default.createElement("thead", null, /* @__PURE__ */ import_react23.default.createElement("tr", null, visibleColumns.map((col, idx) => {
4802
4840
  const colField = String(col.field);
4803
4841
  const width = columnWidths[colField] || 200;
4804
4842
  const leftOffset = getLeftOffset(col, idx);
@@ -4912,17 +4950,14 @@ function DataGrid({
4912
4950
  },
4913
4951
  action.icon
4914
4952
  )))));
4915
- })()))))), paginatedData.length === 0 && /* @__PURE__ */ import_react23.default.createElement("div", { className: "dg-empty-state" }, /* @__PURE__ */ import_react23.default.createElement("svg", { className: "dg-empty-icon", viewBox: "0 0 200 160", fill: "none", xmlns: "http://www.w3.org/2000/svg" }, /* @__PURE__ */ import_react23.default.createElement("rect", { x: "20", y: "30", width: "160", height: "100", rx: "8", fill: "var(--hover-color)", stroke: "var(--border-color)", strokeWidth: "1.5" }), /* @__PURE__ */ import_react23.default.createElement("rect", { x: "20", y: "30", width: "160", height: "28", rx: "8", fill: "var(--border-color)", opacity: "0.5" }), /* @__PURE__ */ import_react23.default.createElement("rect", { x: "20", y: "50", width: "160", height: "8", rx: "0", fill: "var(--border-color)", opacity: "0.5" }), /* @__PURE__ */ import_react23.default.createElement("line", { x1: "72", y1: "30", x2: "72", y2: "130", stroke: "var(--border-color)", strokeWidth: "1" }), /* @__PURE__ */ import_react23.default.createElement("line", { x1: "128", y1: "30", x2: "128", y2: "130", stroke: "var(--border-color)", strokeWidth: "1" }), /* @__PURE__ */ import_react23.default.createElement("line", { x1: "20", y1: "78", x2: "180", y2: "78", stroke: "var(--border-color)", strokeWidth: "1" }), /* @__PURE__ */ import_react23.default.createElement("line", { x1: "20", y1: "104", x2: "180", y2: "104", stroke: "var(--border-color)", strokeWidth: "1" }), /* @__PURE__ */ import_react23.default.createElement("rect", { x: "32", y: "87", width: "28", height: "6", rx: "3", fill: "var(--border-color)", opacity: "0.4" }), /* @__PURE__ */ import_react23.default.createElement("rect", { x: "84", y: "87", width: "28", height: "6", rx: "3", fill: "var(--border-color)", opacity: "0.4" }), /* @__PURE__ */ import_react23.default.createElement("rect", { x: "140", y: "87", width: "28", height: "6", rx: "3", fill: "var(--border-color)", opacity: "0.4" }), /* @__PURE__ */ import_react23.default.createElement("rect", { x: "32", y: "113", width: "20", height: "6", rx: "3", fill: "var(--border-color)", opacity: "0.3" }), /* @__PURE__ */ import_react23.default.createElement("rect", { x: "84", y: "113", width: "32", height: "6", rx: "3", fill: "var(--border-color)", opacity: "0.3" }), /* @__PURE__ */ import_react23.default.createElement("rect", { x: "140", y: "113", width: "20", height: "6", rx: "3", fill: "var(--border-color)", opacity: "0.3" }), /* @__PURE__ */ import_react23.default.createElement("circle", { cx: "148", cy: "108", r: "26", fill: "var(--surface-color)", stroke: "var(--border-color)", strokeWidth: "1.5" }), /* @__PURE__ */ import_react23.default.createElement("circle", { cx: "145", cy: "105", r: "10", stroke: "var(--text-secondary)", strokeWidth: "2.5", opacity: "0.5" }), /* @__PURE__ */ import_react23.default.createElement("line", { x1: "152", y1: "113", x2: "161", y2: "122", stroke: "var(--text-secondary)", strokeWidth: "2.5", strokeLinecap: "round", opacity: "0.5" }), /* @__PURE__ */ import_react23.default.createElement("line", { x1: "141", y1: "101", x2: "149", y2: "109", stroke: "var(--text-secondary)", strokeWidth: "2", strokeLinecap: "round", opacity: "0.5" }), /* @__PURE__ */ import_react23.default.createElement("line", { x1: "149", y1: "101", x2: "141", y2: "109", stroke: "var(--text-secondary)", strokeWidth: "2", strokeLinecap: "round", opacity: "0.5" })), /* @__PURE__ */ import_react23.default.createElement("p", { className: "dg-empty-title" }, "No data found"), /* @__PURE__ */ import_react23.default.createElement("p", { className: "dg-empty-subtitle" }, filterText || hasActiveFilters ? "Try adjusting your search or filters" : "No records to display"))), /* @__PURE__ */ import_react23.default.createElement("div", { className: "dg-pagination" }, /* @__PURE__ */ import_react23.default.createElement("div", { className: "dg-page-info" }, /* @__PURE__ */ import_react23.default.createElement("div", { className: "dg-per-page" }, /* @__PURE__ */ import_react23.default.createElement("span", null, "Rows per page:"), /* @__PURE__ */ import_react23.default.createElement(
4953
+ })()))))), paginatedData.length === 0 && /* @__PURE__ */ import_react23.default.createElement("div", { className: "dg-empty-state" }, /* @__PURE__ */ import_react23.default.createElement("svg", { className: "dg-empty-icon", viewBox: "0 0 200 160", fill: "none", xmlns: "http://www.w3.org/2000/svg" }, /* @__PURE__ */ import_react23.default.createElement("rect", { x: "20", y: "30", width: "160", height: "100", rx: "8", fill: "var(--hover-color)", stroke: "var(--border-color)", strokeWidth: "1.5" }), /* @__PURE__ */ import_react23.default.createElement("rect", { x: "20", y: "30", width: "160", height: "28", rx: "8", fill: "var(--border-color)", opacity: "0.5" }), /* @__PURE__ */ import_react23.default.createElement("rect", { x: "20", y: "50", width: "160", height: "8", rx: "0", fill: "var(--border-color)", opacity: "0.5" }), /* @__PURE__ */ import_react23.default.createElement("line", { x1: "72", y1: "30", x2: "72", y2: "130", stroke: "var(--border-color)", strokeWidth: "1" }), /* @__PURE__ */ import_react23.default.createElement("line", { x1: "128", y1: "30", x2: "128", y2: "130", stroke: "var(--border-color)", strokeWidth: "1" }), /* @__PURE__ */ import_react23.default.createElement("line", { x1: "20", y1: "78", x2: "180", y2: "78", stroke: "var(--border-color)", strokeWidth: "1" }), /* @__PURE__ */ import_react23.default.createElement("line", { x1: "20", y1: "104", x2: "180", y2: "104", stroke: "var(--border-color)", strokeWidth: "1" }), /* @__PURE__ */ import_react23.default.createElement("rect", { x: "32", y: "87", width: "28", height: "6", rx: "3", fill: "var(--border-color)", opacity: "0.4" }), /* @__PURE__ */ import_react23.default.createElement("rect", { x: "84", y: "87", width: "28", height: "6", rx: "3", fill: "var(--border-color)", opacity: "0.4" }), /* @__PURE__ */ import_react23.default.createElement("rect", { x: "140", y: "87", width: "28", height: "6", rx: "3", fill: "var(--border-color)", opacity: "0.4" }), /* @__PURE__ */ import_react23.default.createElement("rect", { x: "32", y: "113", width: "20", height: "6", rx: "3", fill: "var(--border-color)", opacity: "0.3" }), /* @__PURE__ */ import_react23.default.createElement("rect", { x: "84", y: "113", width: "32", height: "6", rx: "3", fill: "var(--border-color)", opacity: "0.3" }), /* @__PURE__ */ import_react23.default.createElement("rect", { x: "140", y: "113", width: "20", height: "6", rx: "3", fill: "var(--border-color)", opacity: "0.3" }), /* @__PURE__ */ import_react23.default.createElement("circle", { cx: "148", cy: "108", r: "26", fill: "var(--surface-color)", stroke: "var(--border-color)", strokeWidth: "1.5" }), /* @__PURE__ */ import_react23.default.createElement("circle", { cx: "145", cy: "105", r: "10", stroke: "var(--text-secondary)", strokeWidth: "2.5", opacity: "0.5" }), /* @__PURE__ */ import_react23.default.createElement("line", { x1: "152", y1: "113", x2: "161", y2: "122", stroke: "var(--text-secondary)", strokeWidth: "2.5", strokeLinecap: "round", opacity: "0.5" }), /* @__PURE__ */ import_react23.default.createElement("line", { x1: "141", y1: "101", x2: "149", y2: "109", stroke: "var(--text-secondary)", strokeWidth: "2", strokeLinecap: "round", opacity: "0.5" }), /* @__PURE__ */ import_react23.default.createElement("line", { x1: "149", y1: "101", x2: "141", y2: "109", stroke: "var(--text-secondary)", strokeWidth: "2", strokeLinecap: "round", opacity: "0.5" })), /* @__PURE__ */ import_react23.default.createElement("p", { className: "dg-empty-title" }, "No data found"), /* @__PURE__ */ import_react23.default.createElement("p", { className: "dg-empty-subtitle" }, filterText || hasActiveFilters ? "Try adjusting your search or filters" : "No records to display"))), pagination && /* @__PURE__ */ import_react23.default.createElement("div", { className: "dg-pagination" }, /* @__PURE__ */ import_react23.default.createElement("div", { className: "dg-page-info" }, /* @__PURE__ */ import_react23.default.createElement("div", { className: "dg-per-page" }, /* @__PURE__ */ import_react23.default.createElement("span", null, "Rows per page:"), /* @__PURE__ */ import_react23.default.createElement(
4916
4954
  "select",
4917
4955
  {
4918
- value: pageSize,
4919
- onChange: (e) => {
4920
- setPageSize(Number(e.target.value));
4921
- setCurrentPage(1);
4922
- }
4956
+ value: activePageSize,
4957
+ onChange: (e) => handlePageSizeChange(Number(e.target.value))
4923
4958
  },
4924
4959
  pageSizeOptions.map((o) => /* @__PURE__ */ import_react23.default.createElement("option", { key: o, value: o }, o))
4925
- )), /* @__PURE__ */ import_react23.default.createElement("span", null, (currentPage - 1) * pageSize + 1, "\u2013", Math.min(currentPage * pageSize, filteredData.length), " of ", filteredData.length)), /* @__PURE__ */ import_react23.default.createElement("div", { className: "dg-page-nav" }, /* @__PURE__ */ import_react23.default.createElement("button", { className: "dg-page-btn", disabled: currentPage === 1, onClick: () => setCurrentPage((p) => p - 1) }, /* @__PURE__ */ import_react23.default.createElement(import_lucide_react2.ChevronLeft, { size: 15 })), /* @__PURE__ */ import_react23.default.createElement("span", { className: "dg-page-fraction" }, currentPage, " / ", totalPages), /* @__PURE__ */ import_react23.default.createElement("button", { className: "dg-page-btn", disabled: currentPage === totalPages, onClick: () => setCurrentPage((p) => p + 1) }, /* @__PURE__ */ import_react23.default.createElement(import_lucide_react2.ChevronRight, { size: 15 })))), activeMenu && /* @__PURE__ */ import_react23.default.createElement(
4960
+ )), /* @__PURE__ */ import_react23.default.createElement("span", null, (activePage - 1) * activePageSize + 1, "\u2013", Math.min(activePage * activePageSize, totalRows), " of ", totalRows)), /* @__PURE__ */ import_react23.default.createElement("div", { className: "dg-page-nav" }, /* @__PURE__ */ import_react23.default.createElement("button", { className: "dg-page-btn", disabled: activePage === 1, onClick: () => handlePageChange(activePage - 1) }, /* @__PURE__ */ import_react23.default.createElement(import_lucide_react2.ChevronLeft, { size: 15 })), /* @__PURE__ */ import_react23.default.createElement("span", { className: "dg-page-fraction" }, activePage, " / ", totalPages), /* @__PURE__ */ import_react23.default.createElement("button", { className: "dg-page-btn", disabled: activePage === totalPages, onClick: () => handlePageChange(activePage + 1) }, /* @__PURE__ */ import_react23.default.createElement(import_lucide_react2.ChevronRight, { size: 15 })))), activeMenu && /* @__PURE__ */ import_react23.default.createElement(
4926
4961
  "div",
4927
4962
  {
4928
4963
  ref: menuRef,
package/dist/main.css CHANGED
@@ -443,6 +443,7 @@
443
443
  overflow-x: auto;
444
444
  overflow-y: auto;
445
445
  flex: 1;
446
+ position: relative;
446
447
  }
447
448
  .dg-table-wrap--empty {
448
449
  display: flex;
@@ -936,6 +937,30 @@
936
937
  --tf-hover-border-color: var(--text-secondary);
937
938
  --tf-primary-color: var(--primary-color);
938
939
  }
940
+ .dg-loading-overlay {
941
+ position: absolute;
942
+ inset: 0;
943
+ top: 41px;
944
+ background: rgba(255, 255, 255, 0.65);
945
+ display: flex;
946
+ align-items: center;
947
+ justify-content: center;
948
+ z-index: 20;
949
+ pointer-events: all;
950
+ }
951
+ .dg-loading-spinner {
952
+ width: 32px;
953
+ height: 32px;
954
+ border: 3px solid rgba(0, 0, 0, 0.1);
955
+ border-top-color: var(--primary-color, #f15b24);
956
+ border-radius: 50%;
957
+ animation: dg-spin 0.7s linear infinite;
958
+ }
959
+ @keyframes dg-spin {
960
+ to {
961
+ transform: rotate(360deg);
962
+ }
963
+ }
939
964
  .dg-empty-state {
940
965
  flex: 1;
941
966
  display: flex;
@@ -6321,11 +6346,11 @@ pre {
6321
6346
  z-index: 2;
6322
6347
  }
6323
6348
  .rf-text-field__adornment--start {
6324
- margin-left: 14px;
6349
+ margin-left: 0px;
6325
6350
  margin-right: -6px;
6326
6351
  }
6327
6352
  .rf-text-field__adornment--end {
6328
- margin-right: 14px;
6353
+ margin-right: 0px;
6329
6354
  margin-left: -6px;
6330
6355
  }
6331
6356
  .rf-text-field--standard .rf-text-field__adornment--start {
package/dist/main.d.cts CHANGED
@@ -863,10 +863,37 @@ interface DataGridToolbarSlot {
863
863
  content: React__default.ReactNode;
864
864
  align?: 'left' | 'center' | 'right';
865
865
  }
866
+ interface PaginationModel {
867
+ /** 0-indexed current page */
868
+ page: number;
869
+ pageSize: number;
870
+ }
866
871
  interface DataGridProps<T> {
867
872
  columns: Column<T>[];
868
873
  data: T[];
869
874
  actions?: Action<T>[] | ((item: T) => Action<T>[]);
875
+ /** Show a loading overlay over the table body */
876
+ loading?: boolean;
877
+ /** Show the pagination bar. Defaults to true. */
878
+ pagination?: boolean;
879
+ /**
880
+ * 'client' (default) — DataGrid filters, sorts, and paginates data internally.
881
+ * 'server' — data is already the current page; DataGrid skips client-side
882
+ * filtering/sorting/slicing and delegates everything to the server.
883
+ */
884
+ paginationMode?: 'client' | 'server';
885
+ /**
886
+ * Total number of rows across all pages (server mode).
887
+ * Used to compute total page count when paginationMode="server".
888
+ */
889
+ rowCount?: number;
890
+ /**
891
+ * Controlled pagination state. page is 0-indexed.
892
+ * When provided, the DataGrid uses these values instead of internal state.
893
+ */
894
+ paginationModel?: PaginationModel;
895
+ /** Called whenever the page or pageSize changes. page is 0-indexed. */
896
+ onPaginationModelChange?: (model: PaginationModel) => void;
870
897
  pageSize?: number;
871
898
  pageSizeOptions?: number[];
872
899
  title?: string;
@@ -886,7 +913,7 @@ interface DataGridProps<T> {
886
913
 
887
914
  declare function DataGrid<T extends {
888
915
  id: string | number;
889
- }>({ columns: initialColumnsProp, data, actions, pageSize: initialPageSize, pageSizeOptions, title, className, sx, onRowDoubleClick, onCellDoubleClick, headerActions, toolbarContent, }: DataGridProps<T>): React__default.JSX.Element;
916
+ }>({ columns: initialColumnsProp, data, actions, loading, pagination, paginationMode, rowCount, paginationModel, onPaginationModelChange, pageSize: initialPageSize, pageSizeOptions, title, className, sx, onRowDoubleClick, onCellDoubleClick, headerActions, toolbarContent, }: DataGridProps<T>): React__default.JSX.Element;
890
917
 
891
918
  type SelectOption = {
892
919
  value: string | number;
package/dist/main.d.ts CHANGED
@@ -863,10 +863,37 @@ interface DataGridToolbarSlot {
863
863
  content: React__default.ReactNode;
864
864
  align?: 'left' | 'center' | 'right';
865
865
  }
866
+ interface PaginationModel {
867
+ /** 0-indexed current page */
868
+ page: number;
869
+ pageSize: number;
870
+ }
866
871
  interface DataGridProps<T> {
867
872
  columns: Column<T>[];
868
873
  data: T[];
869
874
  actions?: Action<T>[] | ((item: T) => Action<T>[]);
875
+ /** Show a loading overlay over the table body */
876
+ loading?: boolean;
877
+ /** Show the pagination bar. Defaults to true. */
878
+ pagination?: boolean;
879
+ /**
880
+ * 'client' (default) — DataGrid filters, sorts, and paginates data internally.
881
+ * 'server' — data is already the current page; DataGrid skips client-side
882
+ * filtering/sorting/slicing and delegates everything to the server.
883
+ */
884
+ paginationMode?: 'client' | 'server';
885
+ /**
886
+ * Total number of rows across all pages (server mode).
887
+ * Used to compute total page count when paginationMode="server".
888
+ */
889
+ rowCount?: number;
890
+ /**
891
+ * Controlled pagination state. page is 0-indexed.
892
+ * When provided, the DataGrid uses these values instead of internal state.
893
+ */
894
+ paginationModel?: PaginationModel;
895
+ /** Called whenever the page or pageSize changes. page is 0-indexed. */
896
+ onPaginationModelChange?: (model: PaginationModel) => void;
870
897
  pageSize?: number;
871
898
  pageSizeOptions?: number[];
872
899
  title?: string;
@@ -886,7 +913,7 @@ interface DataGridProps<T> {
886
913
 
887
914
  declare function DataGrid<T extends {
888
915
  id: string | number;
889
- }>({ columns: initialColumnsProp, data, actions, pageSize: initialPageSize, pageSizeOptions, title, className, sx, onRowDoubleClick, onCellDoubleClick, headerActions, toolbarContent, }: DataGridProps<T>): React__default.JSX.Element;
916
+ }>({ columns: initialColumnsProp, data, actions, loading, pagination, paginationMode, rowCount, paginationModel, onPaginationModelChange, pageSize: initialPageSize, pageSizeOptions, title, className, sx, onRowDoubleClick, onCellDoubleClick, headerActions, toolbarContent, }: DataGridProps<T>): React__default.JSX.Element;
890
917
 
891
918
  type SelectOption = {
892
919
  value: string | number;
package/dist/main.js CHANGED
@@ -3467,15 +3467,27 @@ var DateField = ({
3467
3467
  const spaceBelow = window.innerHeight - rect.bottom - GAP2;
3468
3468
  const spaceAbove = rect.top - GAP2;
3469
3469
  const useDropUp = spaceBelow < PICKER_H && spaceAbove > spaceBelow;
3470
- const top = useDropUp ? Math.max(4, rect.top - PICKER_H - GAP2) : rect.bottom + GAP2;
3470
+ if (useDropUp) {
3471
+ return {
3472
+ position: "fixed",
3473
+ left: rect.left,
3474
+ // bottom anchors picker's bottom edge exactly to field top — no gap regardless of actual height
3475
+ bottom: window.innerHeight - rect.top + GAP2,
3476
+ // prevent going above viewport
3477
+ maxHeight: rect.top - GAP2 - 4,
3478
+ overflowY: "auto",
3479
+ zIndex: 99999,
3480
+ animationName: "rf-date-picker-appear-up",
3481
+ transformOrigin: "bottom left"
3482
+ };
3483
+ }
3471
3484
  return {
3472
3485
  position: "fixed",
3473
3486
  left: rect.left,
3474
- top,
3487
+ top: rect.bottom + GAP2,
3475
3488
  zIndex: 99999,
3476
- // Drive animation from same decision — no separate state/re-render needed
3477
- animationName: useDropUp ? "rf-date-picker-appear-up" : "rf-date-picker-appear",
3478
- transformOrigin: useDropUp ? "bottom left" : "top left"
3489
+ animationName: "rf-date-picker-appear",
3490
+ transformOrigin: "top left"
3479
3491
  };
3480
3492
  })(),
3481
3493
  onMouseDown: (e) => e.preventDefault()
@@ -4299,6 +4311,12 @@ function DataGrid({
4299
4311
  columns: initialColumnsProp,
4300
4312
  data,
4301
4313
  actions,
4314
+ loading = false,
4315
+ pagination = true,
4316
+ paginationMode = "client",
4317
+ rowCount,
4318
+ paginationModel,
4319
+ onPaginationModelChange,
4302
4320
  pageSize: initialPageSize = 10,
4303
4321
  pageSizeOptions = [5, 10, 25, 50],
4304
4322
  title,
@@ -4339,6 +4357,23 @@ function DataGrid({
4339
4357
  const [sortDirection, setSortDirection] = useState9(null);
4340
4358
  const [filterText, setFilterText] = useState9("");
4341
4359
  const [currentPage, setCurrentPage] = useState9(1);
4360
+ const activePage = paginationModel ? paginationModel.page + 1 : currentPage;
4361
+ const activePageSize = paginationModel ? paginationModel.pageSize : pageSize;
4362
+ const handlePageChange = (newPage) => {
4363
+ if (onPaginationModelChange) {
4364
+ onPaginationModelChange({ page: newPage - 1, pageSize: activePageSize });
4365
+ } else {
4366
+ setCurrentPage(newPage);
4367
+ }
4368
+ };
4369
+ const handlePageSizeChange = (newSize) => {
4370
+ if (onPaginationModelChange) {
4371
+ onPaginationModelChange({ page: 0, pageSize: newSize });
4372
+ } else {
4373
+ setPageSize(newSize);
4374
+ setCurrentPage(1);
4375
+ }
4376
+ };
4342
4377
  const [resizingColumn, setResizingColumn] = useState9(null);
4343
4378
  const [startX, setStartX] = useState9(0);
4344
4379
  const [startWidth, setStartWidth] = useState9(0);
@@ -4571,11 +4606,14 @@ function DataGrid({
4571
4606
  return 0;
4572
4607
  });
4573
4608
  }, [filteredData, sortField, sortDirection, resolvedColumns]);
4574
- const totalPages = Math.max(1, Math.ceil(sortedData.length / pageSize));
4609
+ const isServer = paginationMode === "server";
4610
+ const totalRows = isServer ? rowCount ?? data.length : filteredData.length;
4611
+ const totalPages = Math.max(1, Math.ceil(totalRows / activePageSize));
4575
4612
  const paginatedData = useMemo2(() => {
4576
- const start = (currentPage - 1) * pageSize;
4577
- return sortedData.slice(start, start + pageSize);
4578
- }, [sortedData, currentPage, pageSize]);
4613
+ if (isServer) return data;
4614
+ const start = (activePage - 1) * activePageSize;
4615
+ return sortedData.slice(start, start + activePageSize);
4616
+ }, [isServer, data, sortedData, activePage, activePageSize]);
4579
4617
  const handleExport = () => {
4580
4618
  const exportableCols = resolvedColumns.filter((c) => !c.hidden && c.isExportable !== false);
4581
4619
  const headers = exportableCols.map((c) => c.headerName).join(",");
@@ -4670,7 +4708,7 @@ function DataGrid({
4670
4708
  onClick: () => setShowManageColumns(true)
4671
4709
  },
4672
4710
  /* @__PURE__ */ React75.createElement(Columns, { size: 16 })
4673
- )), /* @__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 ? " dg-table-wrap--empty" : ""}` }, /* @__PURE__ */ React75.createElement("table", { className: "dg-table" }, /* @__PURE__ */ React75.createElement("thead", null, /* @__PURE__ */ React75.createElement("tr", null, visibleColumns.map((col, idx) => {
4711
+ )), /* @__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) => {
4674
4712
  const colField = String(col.field);
4675
4713
  const width = columnWidths[colField] || 200;
4676
4714
  const leftOffset = getLeftOffset(col, idx);
@@ -4784,17 +4822,14 @@ function DataGrid({
4784
4822
  },
4785
4823
  action.icon
4786
4824
  )))));
4787
- })()))))), 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"))), /* @__PURE__ */ React75.createElement("div", { className: "dg-pagination" }, /* @__PURE__ */ React75.createElement("div", { className: "dg-page-info" }, /* @__PURE__ */ React75.createElement("div", { className: "dg-per-page" }, /* @__PURE__ */ React75.createElement("span", null, "Rows per page:"), /* @__PURE__ */ React75.createElement(
4825
+ })()))))), 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("div", { className: "dg-per-page" }, /* @__PURE__ */ React75.createElement("span", null, "Rows per page:"), /* @__PURE__ */ React75.createElement(
4788
4826
  "select",
4789
4827
  {
4790
- value: pageSize,
4791
- onChange: (e) => {
4792
- setPageSize(Number(e.target.value));
4793
- setCurrentPage(1);
4794
- }
4828
+ value: activePageSize,
4829
+ onChange: (e) => handlePageSizeChange(Number(e.target.value))
4795
4830
  },
4796
4831
  pageSizeOptions.map((o) => /* @__PURE__ */ React75.createElement("option", { key: o, value: o }, o))
4797
- )), /* @__PURE__ */ React75.createElement("span", null, (currentPage - 1) * pageSize + 1, "\u2013", Math.min(currentPage * pageSize, filteredData.length), " of ", filteredData.length)), /* @__PURE__ */ React75.createElement("div", { className: "dg-page-nav" }, /* @__PURE__ */ React75.createElement("button", { className: "dg-page-btn", disabled: currentPage === 1, onClick: () => setCurrentPage((p) => p - 1) }, /* @__PURE__ */ React75.createElement(ChevronLeft, { size: 15 })), /* @__PURE__ */ React75.createElement("span", { className: "dg-page-fraction" }, currentPage, " / ", totalPages), /* @__PURE__ */ React75.createElement("button", { className: "dg-page-btn", disabled: currentPage === totalPages, onClick: () => setCurrentPage((p) => p + 1) }, /* @__PURE__ */ React75.createElement(ChevronRight, { size: 15 })))), activeMenu && /* @__PURE__ */ React75.createElement(
4832
+ )), /* @__PURE__ */ React75.createElement("span", null, (activePage - 1) * activePageSize + 1, "\u2013", Math.min(activePage * activePageSize, totalRows), " of ", totalRows)), /* @__PURE__ */ React75.createElement("div", { className: "dg-page-nav" }, /* @__PURE__ */ React75.createElement("button", { className: "dg-page-btn", disabled: activePage === 1, onClick: () => handlePageChange(activePage - 1) }, /* @__PURE__ */ React75.createElement(ChevronLeft, { size: 15 })), /* @__PURE__ */ React75.createElement("span", { className: "dg-page-fraction" }, activePage, " / ", totalPages), /* @__PURE__ */ React75.createElement("button", { className: "dg-page-btn", disabled: activePage === totalPages, onClick: () => handlePageChange(activePage + 1) }, /* @__PURE__ */ React75.createElement(ChevronRight, { size: 15 })))), activeMenu && /* @__PURE__ */ React75.createElement(
4798
4833
  "div",
4799
4834
  {
4800
4835
  ref: menuRef,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rufous/ui",
3
3
  "private": false,
4
- "version": "0.2.105",
4
+ "version": "0.3.0",
5
5
  "type": "module",
6
6
  "description": "Experimental: A lightweight React UI component library (Beta)",
7
7
  "style": "./dist/main.css",
@@ -93,4 +93,4 @@
93
93
  "react": "^18.0.0 || ^19.0.0",
94
94
  "react-dom": "^18.0.0 || ^19.0.0"
95
95
  }
96
- }
96
+ }