@pallasoft/ps-table-components 1.26.0 → 1.27.1

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.
Files changed (43) hide show
  1. package/dist/components/ps-data-table.d.ts +9 -1
  2. package/dist/components/ps-data-table.js +81 -12
  3. package/dist/components/ps-select-all-matching-banner.d.ts +10 -0
  4. package/dist/components/ps-select-all-matching-banner.js +10 -0
  5. package/dist/components/ps-table-pagination.d.ts +3 -1
  6. package/dist/components/ps-table-pagination.js +12 -4
  7. package/dist/constants/ps-server-rows-config.d.ts +8 -0
  8. package/dist/constants/ps-server-rows-config.js +11 -0
  9. package/dist/constants/ps-table-pagination-config.d.ts +3 -0
  10. package/dist/constants/ps-table-pagination-config.js +6 -0
  11. package/dist/esm/components/ps-data-table.js +82 -13
  12. package/dist/esm/components/ps-select-all-matching-banner.js +6 -0
  13. package/dist/esm/components/ps-table-pagination.js +12 -4
  14. package/dist/esm/constants/ps-server-rows-config.js +8 -0
  15. package/dist/esm/constants/ps-table-pagination-config.js +3 -0
  16. package/dist/esm/hooks/use-ps-server-rows.js +209 -0
  17. package/dist/esm/index.js +11 -0
  18. package/dist/esm/interfaces/ps-server-entry.js +1 -0
  19. package/dist/esm/interfaces/ps-server-page.js +1 -0
  20. package/dist/esm/interfaces/ps-server-rows-input.js +1 -0
  21. package/dist/esm/interfaces/ps-server-rows-result.js +1 -0
  22. package/dist/esm/interfaces/ps-table-cursor-paging.js +1 -0
  23. package/dist/esm/interfaces/ps-table-select-all-matching.js +1 -0
  24. package/dist/esm/services/ps-server-rows-service.js +36 -0
  25. package/dist/hooks/use-ps-server-rows.d.ts +3 -0
  26. package/dist/hooks/use-ps-server-rows.js +213 -0
  27. package/dist/index.d.ts +11 -0
  28. package/dist/index.js +11 -0
  29. package/dist/interfaces/ps-server-entry.d.ts +6 -0
  30. package/dist/interfaces/ps-server-entry.js +2 -0
  31. package/dist/interfaces/ps-server-page.d.ts +7 -0
  32. package/dist/interfaces/ps-server-page.js +2 -0
  33. package/dist/interfaces/ps-server-rows-input.d.ts +13 -0
  34. package/dist/interfaces/ps-server-rows-input.js +2 -0
  35. package/dist/interfaces/ps-server-rows-result.d.ts +18 -0
  36. package/dist/interfaces/ps-server-rows-result.js +2 -0
  37. package/dist/interfaces/ps-table-cursor-paging.d.ts +12 -0
  38. package/dist/interfaces/ps-table-cursor-paging.js +2 -0
  39. package/dist/interfaces/ps-table-select-all-matching.d.ts +7 -0
  40. package/dist/interfaces/ps-table-select-all-matching.js +2 -0
  41. package/dist/services/ps-server-rows-service.d.ts +11 -0
  42. package/dist/services/ps-server-rows-service.js +40 -0
  43. package/package.json +1 -1
@@ -1,5 +1,7 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import { PsTablePaginationPosition } from '../constants/ps-table-pagination-position';
3
+ import type { PsTableCursorPaging } from '../interfaces/ps-table-cursor-paging';
4
+ import type { PsTableSelectAllMatching } from '../interfaces/ps-table-select-all-matching';
3
5
  import type { PsTableColumn } from '../interfaces/table-column';
4
6
  import type { PsTableGrouping } from '../interfaces/table-grouping';
5
7
  import type { PsTableQuery } from '../interfaces/table-query';
@@ -9,7 +11,9 @@ import type { PsTableSort } from '../interfaces/table-sort';
9
11
  export interface PsDataTableProps<RowData> {
10
12
  caption?: string;
11
13
  className?: string;
14
+ clearSelectionKey?: number | string;
12
15
  columns: PsTableColumn<RowData>[];
16
+ cursorPaging?: PsTableCursorPaging;
13
17
  defaultSort?: PsTableSort;
14
18
  density?: 'compact' | 'normal';
15
19
  expandedRowIds?: string[];
@@ -21,6 +25,7 @@ export interface PsDataTableProps<RowData> {
21
25
  grouping?: PsTableGrouping<RowData>;
22
26
  isLoading?: boolean;
23
27
  isPending?: boolean;
28
+ isRefreshing?: boolean;
24
29
  layout?: 'auto' | 'fixed';
25
30
  onPageChange?: (page: number) => void;
26
31
  onQueryChange?: (query: PsTableQuery) => void;
@@ -36,7 +41,10 @@ export interface PsDataTableProps<RowData> {
36
41
  rows: RowData[];
37
42
  searchingLabel?: string;
38
43
  searchPlaceholder?: string;
44
+ selectAllMatching?: PsTableSelectAllMatching;
45
+ stickyClassName?: string;
46
+ stickyTop?: string;
39
47
  toolbarActions?: ReactNode;
40
48
  totalRows?: number;
41
49
  }
42
- export declare const PsDataTable: <RowData>({ caption, className, columns, defaultSort, density, expandedRowIds, mobileLayout, getBulkActions, getRowId, getRowLabel, getSearchText, grouping, isLoading, isPending, layout, onPageChange, onQueryChange, onRowClick, onRowHover, page, pageSize, paginationPosition, pinFirstColumn, query, renderExpandedRow, reorder, rows, searchingLabel, searchPlaceholder, toolbarActions, totalRows, }: PsDataTableProps<RowData>) => import("react").JSX.Element;
50
+ export declare const PsDataTable: <RowData>({ caption, className, clearSelectionKey, columns, cursorPaging, defaultSort, density, expandedRowIds, mobileLayout, getBulkActions, getRowId, getRowLabel, getSearchText, grouping, isLoading, isPending, isRefreshing, layout, onPageChange, onQueryChange, onRowClick, onRowHover, page, pageSize, paginationPosition, pinFirstColumn, query, renderExpandedRow, reorder, rows, searchingLabel, searchPlaceholder, selectAllMatching, stickyClassName, stickyTop, toolbarActions, totalRows, }: PsDataTableProps<RowData>) => import("react").JSX.Element;
@@ -42,6 +42,7 @@ const ps_input_components_1 = require("@pallasoft/ps-input-components");
42
42
  const ps_motion_components_1 = require("@pallasoft/ps-motion-components");
43
43
  const react_virtual_1 = require("@tanstack/react-virtual");
44
44
  const ps_table_control_classes_1 = require("../constants/ps-table-control-classes");
45
+ const ps_table_pagination_config_1 = require("../constants/ps-table-pagination-config");
45
46
  const ps_table_pagination_position_1 = require("../constants/ps-table-pagination-position");
46
47
  const table_breakpoint_1 = require("../constants/table-breakpoint");
47
48
  const table_config_1 = require("../constants/table-config");
@@ -56,6 +57,7 @@ const ps_table_pin_service_1 = require("../services/ps-table-pin-service");
56
57
  const ps_table_query_service_1 = require("../services/ps-table-query-service");
57
58
  const ps_table_reorder_service_1 = require("../services/ps-table-reorder-service");
58
59
  const ps_table_scroll_service_1 = require("../services/ps-table-scroll-service");
60
+ const ps_select_all_matching_banner_1 = require("./ps-select-all-matching-banner");
59
61
  const ps_table_bulk_bar_1 = require("./ps-table-bulk-bar");
60
62
  const ps_table_card_list_1 = require("./ps-table-card-list");
61
63
  const ps_table_column_picker_1 = require("./ps-table-column-picker");
@@ -85,7 +87,7 @@ const alignClassNames = {
85
87
  left: 'text-left',
86
88
  right: 'text-right tabular-nums',
87
89
  };
88
- const PsDataTable = ({ caption, className, columns, defaultSort, density = 'normal', expandedRowIds, mobileLayout = 'cards', getBulkActions, getRowId, getRowLabel, getSearchText, grouping, isLoading = false, isPending = false, layout = 'auto', onPageChange, onQueryChange, onRowClick, onRowHover, page, pageSize, paginationPosition = ps_table_pagination_position_1.PsTablePaginationPosition.bottom, pinFirstColumn = true, query, renderExpandedRow, reorder, rows, searchingLabel = table_config_1.PsTableConfig.searchingLabel, searchPlaceholder = table_config_1.PsTableConfig.searchPlaceholder, toolbarActions, totalRows, }) => {
90
+ const PsDataTable = ({ caption, className, clearSelectionKey, columns, cursorPaging, defaultSort, density = 'normal', expandedRowIds, mobileLayout = 'cards', getBulkActions, getRowId, getRowLabel, getSearchText, grouping, isLoading = false, isPending = false, isRefreshing = false, layout = 'auto', onPageChange, onQueryChange, onRowClick, onRowHover, page, pageSize, paginationPosition = ps_table_pagination_position_1.PsTablePaginationPosition.bottom, pinFirstColumn = true, query, renderExpandedRow, reorder, rows, searchingLabel = table_config_1.PsTableConfig.searchingLabel, searchPlaceholder = table_config_1.PsTableConfig.searchPlaceholder, selectAllMatching, stickyClassName, stickyTop, toolbarActions, totalRows, }) => {
89
91
  const containerRef = (0, react_1.useRef)(null);
90
92
  const [uncontrolledPage, setUncontrolledPage] = (0, react_1.useState)(1);
91
93
  const [uncontrolledQuery, setUncontrolledQuery] = (0, react_1.useState)(() => ps_table_query_service_1.PsTableQueryService.empty(defaultSort ? defaultSort : null));
@@ -93,7 +95,50 @@ const PsDataTable = ({ caption, className, columns, defaultSort, density = 'norm
93
95
  const [toggledGroupKeys, setToggledGroupKeys] = (0, react_1.useState)([]);
94
96
  const [closingGroupKeys, setClosingGroupKeys] = (0, react_1.useState)([]);
95
97
  const [openedGroupKeys, setOpenedGroupKeys] = (0, react_1.useState)([]);
98
+ const [seenClearKey, setSeenClearKey] = (0, react_1.useState)(clearSelectionKey);
99
+ const stickyRegionRef = (0, react_1.useRef)(null);
100
+ const stickySentinelRef = (0, react_1.useRef)(null);
101
+ const [stickyHeight, setStickyHeight] = (0, react_1.useState)(0);
102
+ const [isStuck, setIsStuck] = (0, react_1.useState)(false);
96
103
  const currentQuery = query ? query : uncontrolledQuery;
104
+ if (seenClearKey !== clearSelectionKey) {
105
+ setSeenClearKey(clearSelectionKey);
106
+ setSelectedIds([]);
107
+ }
108
+ (0, react_1.useLayoutEffect)(() => {
109
+ const region = stickyRegionRef.current;
110
+ if (stickyTop === undefined || region === null)
111
+ return;
112
+ const measure = () => setStickyHeight(region.offsetHeight);
113
+ const observer = new ResizeObserver(measure);
114
+ measure();
115
+ observer.observe(region);
116
+ return () => observer.disconnect();
117
+ }, [stickyTop]);
118
+ (0, react_1.useEffect)(() => {
119
+ const region = stickyRegionRef.current;
120
+ const sentinel = stickySentinelRef.current;
121
+ if (stickyTop === undefined || region === null || sentinel === null)
122
+ return;
123
+ let frame = 0;
124
+ const check = () => {
125
+ frame = 0;
126
+ setIsStuck(sentinel.getBoundingClientRect().top < Number.parseFloat(getComputedStyle(region).top));
127
+ };
128
+ const schedule = () => {
129
+ if (frame === 0)
130
+ frame = window.requestAnimationFrame(check);
131
+ };
132
+ check();
133
+ window.addEventListener('scroll', schedule, { capture: true, passive: true });
134
+ window.addEventListener('resize', schedule);
135
+ return () => {
136
+ window.removeEventListener('scroll', schedule, { capture: true });
137
+ window.removeEventListener('resize', schedule);
138
+ if (frame !== 0)
139
+ window.cancelAnimationFrame(frame);
140
+ };
141
+ }, [stickyTop]);
97
142
  const { filters, hiddenColumnIds, search, sort } = currentQuery;
98
143
  const visibleColumns = columns.filter((column) => !hiddenColumnIds.includes(column.id));
99
144
  const fittedWidths = (0, react_1.useMemo)(() => new Map(columns
@@ -235,7 +280,7 @@ const PsDataTable = ({ caption, className, columns, defaultSort, density = 'norm
235
280
  })
236
281
  : [], [getRowId, getRowLabel, reorder, reorderKey, visibleRows]);
237
282
  const showsPagination = Boolean(pageSize) && (!grouping || isServerPaginated);
238
- const renderPagination = (paginationClassName) => pageSize ? ((0, jsx_runtime_1.jsx)(ps_table_pagination_1.PsTablePagination, { className: paginationClassName, isPartial: isPartial, loadedPageCount: pageCountOf(loadedRowCount), onPageChange: changePage, page: safePage, pageCount: pageCount, pageSize: pageSize, totalRows: rowCount })) : null;
283
+ const renderPagination = (paginationClassName) => pageSize ? ((0, jsx_runtime_1.jsx)(ps_table_pagination_1.PsTablePagination, { className: paginationClassName, cursor: cursorPaging, isPartial: isPartial, loadedPageCount: pageCountOf(loadedRowCount), onPageChange: changePage, page: safePage, pageCount: pageCount, pageSize: pageSize, totalRows: rowCount })) : null;
239
284
  const renderOverlayRow = (rowId, size) => {
240
285
  const row = visibleRows.find((candidate) => getRowId(candidate) === rowId);
241
286
  return row === undefined ? null : ((0, jsx_runtime_1.jsxs)("table", { className: 'pointer-events-none w-full table-fixed border-collapse bg-surface text-left', style: size === null ? undefined : { width: size.width }, children: [size === null ? null : ((0, jsx_runtime_1.jsx)("colgroup", { children: size.columnWidths.map((width, index) => ((0, jsx_runtime_1.jsx)("col", { style: { width } }, index))) })), (0, jsx_runtime_1.jsx)("tbody", { children: renderRow(ps_table_reorder_service_1.PsTableReorderService.plain, row, undefined, undefined, true) })] }));
@@ -269,12 +314,16 @@ const PsDataTable = ({ caption, className, columns, defaultSort, density = 'norm
269
314
  const selectedRows = selectedIds.length === 0 ? [] : queriedRows.filter((row) => selectedIdSet.has(getRowId(row)));
270
315
  const pageIds = visibleRows.map((row) => getRowId(row));
271
316
  const isPageSelected = pageIds.length > 0 && pageIds.every((rowId) => selectedIdSet.has(rowId));
272
- const togglePage = () => setSelectedIds((current) => {
273
- const pageIdSet = new Set(pageIds);
274
- return isPageSelected
275
- ? current.filter((rowId) => !pageIdSet.has(rowId))
276
- : [...new Set([...current, ...pageIds])];
277
- });
317
+ const togglePage = () => {
318
+ if (isPageSelected && selectAllMatching !== undefined && selectAllMatching.isActive)
319
+ selectAllMatching.onDeactivate();
320
+ setSelectedIds((current) => {
321
+ const pageIdSet = new Set(pageIds);
322
+ return isPageSelected
323
+ ? current.filter((rowId) => !pageIdSet.has(rowId))
324
+ : [...new Set([...current, ...pageIds])];
325
+ });
326
+ };
278
327
  const bodyRef = (0, react_1.useRef)(null);
279
328
  const [scrollParent, setScrollParent] = (0, react_1.useState)(null);
280
329
  const [scrollMargin, setScrollMargin] = (0, react_1.useState)(0);
@@ -352,7 +401,18 @@ const PsDataTable = ({ caption, className, columns, defaultSort, density = 'norm
352
401
  : entries.map((entry) => renderRow(api, entry.row, entry.motion, entry.virtualItem));
353
402
  if (isLoading)
354
403
  return ((0, jsx_runtime_1.jsx)(ps_core_components_1.PsSkeletonTable, { className: className, columns: visibleColumns.length, hasToolbar: hasToolbar, rows: table_config_1.PsTableConfig.skeletonRows }));
355
- return ((0, jsx_runtime_1.jsxs)("div", { ref: containerRef, className: ps_design_system_1.PallasoftClassNameService.merge('flex min-w-0 flex-col gap-3', className), children: [isSelectable && selectedRows.length > 0 && getBulkActions ? ((0, jsx_runtime_1.jsx)(ps_table_bulk_bar_1.PsTableBulkBar, { actions: getBulkActions(selectedRows), onClear: () => setSelectedIds([]), selectedCount: selectedRows.length })) : hasToolbar || columns.some((column) => column.canHide) ? ((0, jsx_runtime_1.jsx)(ps_table_toolbar_1.PsTableToolbar, { actions: (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [toolbarActions, columns.some((column) => column.canHide) ? ((0, jsx_runtime_1.jsx)(ps_table_column_picker_1.PsTableColumnPicker, { columns: columns
404
+ const clearSelection = () => {
405
+ setSelectedIds([]);
406
+ if (selectAllMatching !== undefined && selectAllMatching.isActive)
407
+ selectAllMatching.onDeactivate();
408
+ };
409
+ const banner = selectAllMatching !== undefined &&
410
+ selectedRows.length > 0 &&
411
+ selectAllMatching.total > pageIds.length &&
412
+ (isPageSelected || selectAllMatching.isActive) ? ((0, jsx_runtime_1.jsx)(ps_select_all_matching_banner_1.PsSelectAllMatchingBanner, { isActive: selectAllMatching.isActive, noun: selectAllMatching.noun, onActivate: selectAllMatching.onActivate, onDeactivate: clearSelection, pageCount: pageIds.length, total: selectAllMatching.total })) : null;
413
+ const topContent = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [isSelectable && selectedRows.length > 0 && getBulkActions ? ((0, jsx_runtime_1.jsx)(ps_table_bulk_bar_1.PsTableBulkBar, { actions: getBulkActions(selectedRows), onClear: clearSelection, selectedCount: selectAllMatching !== undefined && selectAllMatching.isActive
414
+ ? selectAllMatching.total
415
+ : selectedRows.length })) : hasToolbar || columns.some((column) => column.canHide) ? ((0, jsx_runtime_1.jsx)(ps_table_toolbar_1.PsTableToolbar, { actions: (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [toolbarActions, columns.some((column) => column.canHide) ? ((0, jsx_runtime_1.jsx)(ps_table_column_picker_1.PsTableColumnPicker, { columns: columns
356
416
  .filter((column) => column.canHide)
357
417
  .map((column) => ({ id: column.id, label: column.label })), hiddenColumnIds: hiddenColumnIds, onChange: (nextHidden) => updateQuery({ hiddenColumnIds: nextHidden }) })) : null] }), columns: hasFilters ? columns : [], filters: filters, isSearchable: isSearchable, onClearFilters: clearFilters, onFilterChange: (columnId, value) => {
358
418
  updateQuery({ filters: { ...filters, [columnId]: value } });
@@ -360,16 +420,25 @@ const PsDataTable = ({ caption, className, columns, defaultSort, density = 'norm
360
420
  }, onSearchChange: (next) => {
361
421
  updateQuery({ search: next });
362
422
  resetPage();
363
- }, rows: rows, search: search, searchPlaceholder: searchPlaceholder })) : null, (0, jsx_runtime_1.jsxs)("div", { className: 'min-w-0 overflow-hidden rounded-[1.25rem] border border-line bg-surface', children: [showsPagination && paginationPosition !== ps_table_pagination_position_1.PsTablePaginationPosition.bottom
423
+ }, rows: rows, search: search, searchPlaceholder: searchPlaceholder })) : null, banner] }));
424
+ return ((0, jsx_runtime_1.jsxs)("div", { ref: containerRef, className: ps_design_system_1.PallasoftClassNameService.merge('flex min-w-0 flex-col gap-3', className), children: [stickyTop === undefined ? (topContent) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("div", { "aria-hidden": 'true', className: '-mb-3 h-0', ref: stickySentinelRef }), (0, jsx_runtime_1.jsx)("div", { className: ps_design_system_1.PallasoftClassNameService.merge('sticky z-sticky -mx-1 flex flex-col gap-3 px-1 pb-2 pt-1 transition-shadow', stickyClassName === undefined ? 'bg-canvas' : stickyClassName, isStuck ? 'shadow-[0_8px_8px_-8px_rgb(var(--ps-color-ink-rgb,15_23_42)/0.18)]' : ''), ref: stickyRegionRef, style: { top: stickyTop }, children: topContent })] })), (0, jsx_runtime_1.jsxs)("div", { "aria-busy": isRefreshing, className: ps_design_system_1.PallasoftClassNameService.merge('min-w-0 rounded-[1.25rem] border border-line bg-surface', stickyTop === undefined ? 'overflow-hidden' : 'overflow-clip'), children: [showsPagination &&
425
+ paginationPosition !== ps_table_pagination_position_1.PsTablePaginationPosition.bottom &&
426
+ rowCount > ps_table_pagination_config_1.PsTablePaginationConfig.topMinRows
364
427
  ? renderPagination('border-b border-t-0')
365
- : null, mobileLayout === 'cards' ? ((0, jsx_runtime_1.jsxs)("div", { className: 'sm:hidden', children: [reorder && reorder.cardsReason ? ((0, jsx_runtime_1.jsx)("p", { className: 'border-b border-line px-4 py-2 text-xs text-ink/55', children: reorder.cardsReason })) : null, grouping
428
+ : null, mobileLayout === 'cards' ? ((0, jsx_runtime_1.jsxs)("div", { className: ps_design_system_1.PallasoftClassNameService.merge('transition-opacity duration-150 sm:hidden', isRefreshing ? 'opacity-60' : ''), children: [reorder && reorder.cardsReason ? ((0, jsx_runtime_1.jsx)("p", { className: 'border-b border-line px-4 py-2 text-xs text-ink/55', children: reorder.cardsReason })) : null, grouping
366
429
  ? groups.map((group, groupIndex) => {
367
430
  const isExpanded = isGroupExpanded(group.key, groupIndex);
368
431
  return ((0, jsx_runtime_1.jsxs)("div", { className: 'border-b border-line last:border-b-0', children: [(0, jsx_runtime_1.jsx)(ps_table_group_header_1.PsTableGroupHeader, { count: group.rows.length, isExpanded: isExpanded, label: grouping.getGroupLabel(group.key, group.rows), onToggle: () => toggleGroup(group.key, isExpanded), summary: grouping.renderGroupSummary
369
432
  ? grouping.renderGroupSummary(group.rows)
370
433
  : undefined }), (0, jsx_runtime_1.jsx)(ps_motion_components_1.PsCollapse, { isOpen: isExpanded && !closingGroupKeys.includes(group.key), children: renderCardList(group.rows, null) })] }, `group-${group.key}`));
371
434
  })
372
- : renderCardList(shouldVirtualize ? visibleRows.slice(0, cards.count) : visibleRows, visibleRows.length === 0 && isSearching ? ((0, jsx_runtime_1.jsx)(ps_table_searching_state_1.PsTableSearchingState, { label: searchingLabel, skeletonRows: table_config_1.PsTableConfig.searchingSkeletonRows })) : visibleRows.length === 0 && isQueried ? ((0, jsx_runtime_1.jsxs)("div", { className: 'px-4 py-10 text-center', children: [(0, jsx_runtime_1.jsx)("p", { className: 'text-sm font-semibold text-ink', children: table_config_1.PsTableConfig.noMatchesTitle }), (0, jsx_runtime_1.jsx)("button", { className: ps_design_system_1.PallasoftClassNameService.merge(ps_table_control_classes_1.PsTableControlClasses.textButton, 'mt-2 text-primary'), onClick: clearFilters, type: 'button', children: "Clear search and filters" })] })) : null), !grouping && shouldVirtualize && cards.count < visibleRows.length ? ((0, jsx_runtime_1.jsx)("div", { "aria-hidden": 'true', className: 'h-px', ref: cards.sentinelRef })) : null] })) : null, (0, jsx_runtime_1.jsx)("div", { className: ps_design_system_1.PallasoftClassNameService.merge('ps-scrollbar overflow-x-auto overflow-y-hidden overscroll-x-contain', mobileLayout === 'cards' ? 'hidden sm:block' : ''), "data-overflow-end": overflow.edges.end, "data-overflow-start": overflow.edges.start, onScroll: overflow.update, ref: overflow.ref, children: renderReorder((api) => ((0, jsx_runtime_1.jsxs)("table", { className: ps_design_system_1.PallasoftClassNameService.merge('w-full border-collapse text-left', layout === 'fixed' ? 'table-fixed' : ''), children: [caption ? (0, jsx_runtime_1.jsx)("caption", { className: 'sr-only', children: caption }) : null, (0, jsx_runtime_1.jsx)("thead", { className: 'bg-mist/70 text-[0.68rem] font-semibold uppercase tracking-[0.18em] text-ink/45', children: (0, jsx_runtime_1.jsxs)("tr", { children: [reorder ? ((0, jsx_runtime_1.jsx)("th", { className: ps_design_system_1.PallasoftClassNameService.merge('w-9 min-w-9 pl-3 pr-0', pins.reorder), scope: 'col', children: reorder.enabled ? ((0, jsx_runtime_1.jsx)("span", { className: 'sr-only', children: reorder.columnLabel })) : reorder.disabledReason ? ((0, jsx_runtime_1.jsx)("span", { "aria-label": reorder.disabledReason, className: 'inline-flex cursor-help opacity-40', role: 'img', title: reorder.disabledReason, children: (0, jsx_runtime_1.jsx)(ps_core_components_1.PsDragGrip, {}) })) : null })) : null, isSelectable ? ((0, jsx_runtime_1.jsx)("th", { className: ps_design_system_1.PallasoftClassNameService.merge('w-10 min-w-10 pl-4 pr-0', pins.select), scope: 'col', children: (0, jsx_runtime_1.jsx)(ps_input_components_1.PsCheckbox, { isIndeterminate: !isPageSelected && pageIds.some((rowId) => selectedIdSet.has(rowId)), isSelected: isPageSelected, label: 'Select all rows on this page', onChange: togglePage }) })) : null, visibleColumns.map((column, columnIndex) => {
435
+ : renderCardList(shouldVirtualize ? visibleRows.slice(0, cards.count) : visibleRows, visibleRows.length === 0 && isSearching ? ((0, jsx_runtime_1.jsx)(ps_table_searching_state_1.PsTableSearchingState, { label: searchingLabel, skeletonRows: table_config_1.PsTableConfig.searchingSkeletonRows })) : visibleRows.length === 0 && isQueried ? ((0, jsx_runtime_1.jsxs)("div", { className: 'px-4 py-10 text-center', children: [(0, jsx_runtime_1.jsx)("p", { className: 'text-sm font-semibold text-ink', children: table_config_1.PsTableConfig.noMatchesTitle }), (0, jsx_runtime_1.jsx)("button", { className: ps_design_system_1.PallasoftClassNameService.merge(ps_table_control_classes_1.PsTableControlClasses.textButton, 'mt-2 text-primary'), onClick: clearFilters, type: 'button', children: "Clear search and filters" })] })) : null), !grouping && shouldVirtualize && cards.count < visibleRows.length ? ((0, jsx_runtime_1.jsx)("div", { "aria-hidden": 'true', className: 'h-px', ref: cards.sentinelRef })) : null] })) : null, (0, jsx_runtime_1.jsx)("div", { className: ps_design_system_1.PallasoftClassNameService.merge(stickyTop === undefined
436
+ ? 'ps-scrollbar overflow-x-auto overflow-y-hidden overscroll-x-contain'
437
+ : 'overflow-x-clip', 'transition-opacity duration-150', isRefreshing ? 'opacity-60' : '', mobileLayout === 'cards' ? 'hidden sm:block' : ''), "data-overflow-end": overflow.edges.end, "data-overflow-start": overflow.edges.start, onScroll: overflow.update, ref: overflow.ref, children: renderReorder((api) => ((0, jsx_runtime_1.jsxs)("table", { className: ps_design_system_1.PallasoftClassNameService.merge('w-full border-collapse text-left', layout === 'fixed' ? 'table-fixed' : ''), children: [caption ? (0, jsx_runtime_1.jsx)("caption", { className: 'sr-only', children: caption }) : null, (0, jsx_runtime_1.jsx)("thead", { className: ps_design_system_1.PallasoftClassNameService.merge('bg-mist/70 text-[0.68rem] font-semibold uppercase tracking-[0.18em] text-ink/45', stickyTop === undefined
438
+ ? ''
439
+ : 'sticky z-docked bg-surface [&_th]:border-b [&_th]:border-line [&>tr]:bg-mist/70'), style: stickyTop === undefined
440
+ ? undefined
441
+ : { top: `calc(${stickyTop} + ${stickyHeight}px)` }, children: (0, jsx_runtime_1.jsxs)("tr", { children: [reorder ? ((0, jsx_runtime_1.jsx)("th", { className: ps_design_system_1.PallasoftClassNameService.merge('w-9 min-w-9 pl-3 pr-0', pins.reorder), scope: 'col', children: reorder.enabled ? ((0, jsx_runtime_1.jsx)("span", { className: 'sr-only', children: reorder.columnLabel })) : reorder.disabledReason ? ((0, jsx_runtime_1.jsx)("span", { "aria-label": reorder.disabledReason, className: 'inline-flex cursor-help opacity-40', role: 'img', title: reorder.disabledReason, children: (0, jsx_runtime_1.jsx)(ps_core_components_1.PsDragGrip, {}) })) : null })) : null, isSelectable ? ((0, jsx_runtime_1.jsx)("th", { className: ps_design_system_1.PallasoftClassNameService.merge('w-10 min-w-10 pl-4 pr-0', pins.select), scope: 'col', children: (0, jsx_runtime_1.jsx)(ps_input_components_1.PsCheckbox, { isIndeterminate: !isPageSelected && pageIds.some((rowId) => selectedIdSet.has(rowId)), isSelected: isPageSelected, label: 'Select all rows on this page', onChange: togglePage }) })) : null, visibleColumns.map((column, columnIndex) => {
373
442
  const align = column.align !== null && column.align !== undefined ? column.align : 'left';
374
443
  const isSorted = sort !== null && sort.columnId === column.id;
375
444
  return ((0, jsx_runtime_1.jsx)("th", { "aria-sort": isSorted
@@ -0,0 +1,10 @@
1
+ export interface PsSelectAllMatchingBannerProps {
2
+ className?: string;
3
+ isActive: boolean;
4
+ noun?: string;
5
+ onActivate: () => void;
6
+ onDeactivate: () => void;
7
+ pageCount: number;
8
+ total: number;
9
+ }
10
+ export declare const PsSelectAllMatchingBanner: ({ className, isActive, noun, onActivate, onDeactivate, pageCount, total, }: PsSelectAllMatchingBannerProps) => import("react").JSX.Element;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PsSelectAllMatchingBanner = void 0;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const ps_design_system_1 = require("@pallasoft/ps-design-system");
6
+ const ps_table_control_classes_1 = require("../constants/ps-table-control-classes");
7
+ const PsSelectAllMatchingBanner = ({ className, isActive, noun = 'rows', onActivate, onDeactivate, pageCount, total, }) => ((0, jsx_runtime_1.jsxs)("div", { "aria-live": 'polite', className: ps_design_system_1.PallasoftClassNameService.merge('flex flex-wrap items-center justify-center gap-x-2 gap-y-1 rounded-ps-control bg-primary/10 px-3 py-2 text-sm text-ink', className), role: 'status', children: [(0, jsx_runtime_1.jsx)("span", { children: isActive
8
+ ? `All ${total.toLocaleString()} ${noun} matching are selected.`
9
+ : `${pageCount.toLocaleString()} ${noun} on this page are selected.` }), (0, jsx_runtime_1.jsx)("button", { className: ps_design_system_1.PallasoftClassNameService.merge(ps_table_control_classes_1.PsTableControlClasses.textButton, 'text-primary'), onClick: isActive ? onDeactivate : onActivate, type: 'button', children: isActive ? 'Clear selection' : `Select all ${total.toLocaleString()} matching` })] }));
10
+ exports.PsSelectAllMatchingBanner = PsSelectAllMatchingBanner;
@@ -1,5 +1,7 @@
1
+ import type { PsTableCursorPaging } from '../interfaces/ps-table-cursor-paging';
1
2
  export interface PsTablePaginationProps {
2
3
  className?: string;
4
+ cursor?: PsTableCursorPaging;
3
5
  isPartial?: boolean;
4
6
  loadedPageCount?: number;
5
7
  onPageChange: (page: number) => void;
@@ -8,4 +10,4 @@ export interface PsTablePaginationProps {
8
10
  pageSize: number;
9
11
  totalRows: number;
10
12
  }
11
- export declare const PsTablePagination: ({ className, isPartial, loadedPageCount, onPageChange, page, pageCount, pageSize, totalRows, }: PsTablePaginationProps) => import("react").JSX.Element;
13
+ export declare const PsTablePagination: ({ className, cursor, isPartial, loadedPageCount, onPageChange, page, pageCount, pageSize, totalRows, }: PsTablePaginationProps) => import("react").JSX.Element;
@@ -4,12 +4,20 @@ exports.PsTablePagination = void 0;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const ps_design_system_1 = require("@pallasoft/ps-design-system");
6
6
  const ps_table_control_classes_1 = require("../constants/ps-table-control-classes");
7
- const PsTablePagination = ({ className, isPartial = false, loadedPageCount, onPageChange, page, pageCount, pageSize, totalRows, }) => {
7
+ const PsTablePagination = ({ className, cursor, isPartial = false, loadedPageCount, onPageChange, page, pageCount, pageSize, totalRows, }) => {
8
8
  const firstRow = totalRows === 0 ? 0 : (page - 1) * pageSize + 1;
9
9
  const lastRow = Math.min(page * pageSize, totalRows);
10
- return ((0, jsx_runtime_1.jsxs)("nav", { "aria-label": 'Pagination', className: ps_design_system_1.PallasoftClassNameService.merge('flex flex-wrap items-center justify-between gap-3 border-t border-line px-4 py-3', className), children: [(0, jsx_runtime_1.jsx)("p", { className: 'text-sm text-ink/55', children: totalRows === 0
10
+ const approximately = cursor !== undefined && cursor.isEstimate ? 'about ' : '';
11
+ const pageNumber = cursor === undefined ? Math.min(page, pageCount) : cursor.pageNumber;
12
+ const cursorFirst = cursor === undefined ? firstRow : (cursor.pageNumber - 1) * pageSize + 1;
13
+ const cursorLast = cursor === undefined ? lastRow : Math.min(cursor.pageNumber * pageSize, totalRows);
14
+ return ((0, jsx_runtime_1.jsxs)("nav", { "aria-label": 'Pagination', className: ps_design_system_1.PallasoftClassNameService.merge('flex flex-wrap items-center justify-between gap-x-6 gap-y-3 border-t border-line px-5 py-3', className), children: [(0, jsx_runtime_1.jsx)("p", { className: 'shrink-0 whitespace-nowrap text-sm text-ink/55', children: totalRows === 0
11
15
  ? 'No rows'
12
- : `${firstRow}–${lastRow} of ${totalRows}${isPartial ? '+' : ''}` }), (0, jsx_runtime_1.jsxs)("div", { className: 'flex items-center gap-2', children: [(0, jsx_runtime_1.jsx)("button", { className: ps_table_control_classes_1.PsTableControlClasses.paginationButton, disabled: page <= 1, onClick: () => onPageChange(page - 1), type: 'button', children: "Previous" }), (0, jsx_runtime_1.jsxs)("span", { className: 'hidden text-sm text-ink/65 sm:inline', children: ["Page ", Math.min(page, pageCount), " of ", pageCount] }), (0, jsx_runtime_1.jsx)("button", { className: ps_table_control_classes_1.PsTableControlClasses.paginationButton, disabled: page >=
13
- (loadedPageCount === undefined ? pageCount : Math.min(loadedPageCount, pageCount)), onClick: () => onPageChange(page + 1), type: 'button', children: "Next" })] })] }));
16
+ : cursor === undefined
17
+ ? `${firstRow}–${lastRow} of ${totalRows}${isPartial ? '+' : ''}`
18
+ : `${cursorFirst}–${cursorLast} of ${approximately}${totalRows.toLocaleString()}` }), (0, jsx_runtime_1.jsxs)("div", { className: 'flex shrink-0 items-center gap-3', children: [cursor !== undefined && cursor.pageSizeOptions !== undefined && cursor.onPageSizeChange ? ((0, jsx_runtime_1.jsxs)("label", { className: 'flex shrink-0 items-center gap-2 whitespace-nowrap text-sm text-ink/65', children: [(0, jsx_runtime_1.jsx)("span", { className: 'hidden sm:inline', children: "Rows per page" }), (0, jsx_runtime_1.jsx)("select", { "aria-label": 'Rows per page', className: 'ps-input-control ps-focus-ring ps-control ps-control-padding w-auto shrink-0 rounded-ps-control text-sm', onChange: (event) => cursor.onPageSizeChange?.(Number(event.target.value)), value: pageSize, children: cursor.pageSizeOptions.map((option) => ((0, jsx_runtime_1.jsx)("option", { value: option, children: option }, option))) })] })) : null, (0, jsx_runtime_1.jsx)("button", { className: ps_table_control_classes_1.PsTableControlClasses.paginationButton, disabled: cursor === undefined ? page <= 1 : !cursor.hasPrevious, onClick: () => (cursor === undefined ? onPageChange(page - 1) : cursor.onPrevious()), type: 'button', children: "Previous" }), (0, jsx_runtime_1.jsxs)("span", { className: 'hidden whitespace-nowrap text-sm text-ink/65 sm:inline', children: ["Page ", pageNumber, " of ", approximately, pageCount] }), (0, jsx_runtime_1.jsx)("button", { className: ps_table_control_classes_1.PsTableControlClasses.paginationButton, disabled: cursor === undefined
19
+ ? page >=
20
+ (loadedPageCount === undefined ? pageCount : Math.min(loadedPageCount, pageCount))
21
+ : !cursor.hasNext, onClick: () => (cursor === undefined ? onPageChange(page + 1) : cursor.onNext()), onFocus: cursor === undefined ? undefined : cursor.onNextIntent, onPointerEnter: cursor === undefined ? undefined : cursor.onNextIntent, type: 'button', children: "Next" })] })] }));
14
22
  };
15
23
  exports.PsTablePagination = PsTablePagination;
@@ -0,0 +1,8 @@
1
+ export declare const PsServerRowsConfig: {
2
+ readonly cacheSize: 8;
3
+ readonly debounceMs: 250;
4
+ readonly defaultPageSize: 100;
5
+ readonly idleFallbackMs: 200;
6
+ readonly idleTimeoutMs: 1000;
7
+ readonly pageSizes: readonly [50, 100, 200];
8
+ };
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PsServerRowsConfig = void 0;
4
+ exports.PsServerRowsConfig = {
5
+ cacheSize: 8,
6
+ debounceMs: 250,
7
+ defaultPageSize: 100,
8
+ idleFallbackMs: 200,
9
+ idleTimeoutMs: 1000,
10
+ pageSizes: [50, 100, 200],
11
+ };
@@ -0,0 +1,3 @@
1
+ export declare const PsTablePaginationConfig: {
2
+ readonly topMinRows: 20;
3
+ };
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PsTablePaginationConfig = void 0;
4
+ exports.PsTablePaginationConfig = {
5
+ topMinRows: 20,
6
+ };
@@ -1,11 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { Fragment, Suspense, lazy, useCallback, useDeferredValue, useEffect, useMemo, useRef, useState, } from 'react';
2
+ import { Fragment, Suspense, lazy, useCallback, useDeferredValue, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
3
3
  import { PsDragGrip, PsSkeletonTable } from '@pallasoft/ps-core-components';
4
4
  import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
5
5
  import { PsCheckbox } from '@pallasoft/ps-input-components';
6
6
  import { PsCollapse } from '@pallasoft/ps-motion-components';
7
7
  import { useVirtualizer, useWindowVirtualizer } from '@tanstack/react-virtual';
8
8
  import { PsTableControlClasses } from '../constants/ps-table-control-classes';
9
+ import { PsTablePaginationConfig } from '../constants/ps-table-pagination-config';
9
10
  import { PsTablePaginationPosition } from '../constants/ps-table-pagination-position';
10
11
  import { PsTableHideBelowClasses } from '../constants/table-breakpoint';
11
12
  import { PsTableConfig } from '../constants/table-config';
@@ -20,6 +21,7 @@ import { PsTablePinService } from '../services/ps-table-pin-service';
20
21
  import { PsTableQueryService } from '../services/ps-table-query-service';
21
22
  import { PsTableReorderService } from '../services/ps-table-reorder-service';
22
23
  import { PsTableScrollService } from '../services/ps-table-scroll-service';
24
+ import { PsSelectAllMatchingBanner } from './ps-select-all-matching-banner';
23
25
  import { PsTableBulkBar } from './ps-table-bulk-bar';
24
26
  import { PsTableCardList } from './ps-table-card-list';
25
27
  import { PsTableColumnPicker } from './ps-table-column-picker';
@@ -49,7 +51,7 @@ const alignClassNames = {
49
51
  left: 'text-left',
50
52
  right: 'text-right tabular-nums',
51
53
  };
52
- export const PsDataTable = ({ caption, className, columns, defaultSort, density = 'normal', expandedRowIds, mobileLayout = 'cards', getBulkActions, getRowId, getRowLabel, getSearchText, grouping, isLoading = false, isPending = false, layout = 'auto', onPageChange, onQueryChange, onRowClick, onRowHover, page, pageSize, paginationPosition = PsTablePaginationPosition.bottom, pinFirstColumn = true, query, renderExpandedRow, reorder, rows, searchingLabel = PsTableConfig.searchingLabel, searchPlaceholder = PsTableConfig.searchPlaceholder, toolbarActions, totalRows, }) => {
54
+ export const PsDataTable = ({ caption, className, clearSelectionKey, columns, cursorPaging, defaultSort, density = 'normal', expandedRowIds, mobileLayout = 'cards', getBulkActions, getRowId, getRowLabel, getSearchText, grouping, isLoading = false, isPending = false, isRefreshing = false, layout = 'auto', onPageChange, onQueryChange, onRowClick, onRowHover, page, pageSize, paginationPosition = PsTablePaginationPosition.bottom, pinFirstColumn = true, query, renderExpandedRow, reorder, rows, searchingLabel = PsTableConfig.searchingLabel, searchPlaceholder = PsTableConfig.searchPlaceholder, selectAllMatching, stickyClassName, stickyTop, toolbarActions, totalRows, }) => {
53
55
  const containerRef = useRef(null);
54
56
  const [uncontrolledPage, setUncontrolledPage] = useState(1);
55
57
  const [uncontrolledQuery, setUncontrolledQuery] = useState(() => PsTableQueryService.empty(defaultSort ? defaultSort : null));
@@ -57,7 +59,50 @@ export const PsDataTable = ({ caption, className, columns, defaultSort, density
57
59
  const [toggledGroupKeys, setToggledGroupKeys] = useState([]);
58
60
  const [closingGroupKeys, setClosingGroupKeys] = useState([]);
59
61
  const [openedGroupKeys, setOpenedGroupKeys] = useState([]);
62
+ const [seenClearKey, setSeenClearKey] = useState(clearSelectionKey);
63
+ const stickyRegionRef = useRef(null);
64
+ const stickySentinelRef = useRef(null);
65
+ const [stickyHeight, setStickyHeight] = useState(0);
66
+ const [isStuck, setIsStuck] = useState(false);
60
67
  const currentQuery = query ? query : uncontrolledQuery;
68
+ if (seenClearKey !== clearSelectionKey) {
69
+ setSeenClearKey(clearSelectionKey);
70
+ setSelectedIds([]);
71
+ }
72
+ useLayoutEffect(() => {
73
+ const region = stickyRegionRef.current;
74
+ if (stickyTop === undefined || region === null)
75
+ return;
76
+ const measure = () => setStickyHeight(region.offsetHeight);
77
+ const observer = new ResizeObserver(measure);
78
+ measure();
79
+ observer.observe(region);
80
+ return () => observer.disconnect();
81
+ }, [stickyTop]);
82
+ useEffect(() => {
83
+ const region = stickyRegionRef.current;
84
+ const sentinel = stickySentinelRef.current;
85
+ if (stickyTop === undefined || region === null || sentinel === null)
86
+ return;
87
+ let frame = 0;
88
+ const check = () => {
89
+ frame = 0;
90
+ setIsStuck(sentinel.getBoundingClientRect().top < Number.parseFloat(getComputedStyle(region).top));
91
+ };
92
+ const schedule = () => {
93
+ if (frame === 0)
94
+ frame = window.requestAnimationFrame(check);
95
+ };
96
+ check();
97
+ window.addEventListener('scroll', schedule, { capture: true, passive: true });
98
+ window.addEventListener('resize', schedule);
99
+ return () => {
100
+ window.removeEventListener('scroll', schedule, { capture: true });
101
+ window.removeEventListener('resize', schedule);
102
+ if (frame !== 0)
103
+ window.cancelAnimationFrame(frame);
104
+ };
105
+ }, [stickyTop]);
61
106
  const { filters, hiddenColumnIds, search, sort } = currentQuery;
62
107
  const visibleColumns = columns.filter((column) => !hiddenColumnIds.includes(column.id));
63
108
  const fittedWidths = useMemo(() => new Map(columns
@@ -199,7 +244,7 @@ export const PsDataTable = ({ caption, className, columns, defaultSort, density
199
244
  })
200
245
  : [], [getRowId, getRowLabel, reorder, reorderKey, visibleRows]);
201
246
  const showsPagination = Boolean(pageSize) && (!grouping || isServerPaginated);
202
- const renderPagination = (paginationClassName) => pageSize ? (_jsx(PsTablePagination, { className: paginationClassName, isPartial: isPartial, loadedPageCount: pageCountOf(loadedRowCount), onPageChange: changePage, page: safePage, pageCount: pageCount, pageSize: pageSize, totalRows: rowCount })) : null;
247
+ const renderPagination = (paginationClassName) => pageSize ? (_jsx(PsTablePagination, { className: paginationClassName, cursor: cursorPaging, isPartial: isPartial, loadedPageCount: pageCountOf(loadedRowCount), onPageChange: changePage, page: safePage, pageCount: pageCount, pageSize: pageSize, totalRows: rowCount })) : null;
203
248
  const renderOverlayRow = (rowId, size) => {
204
249
  const row = visibleRows.find((candidate) => getRowId(candidate) === rowId);
205
250
  return row === undefined ? null : (_jsxs("table", { className: 'pointer-events-none w-full table-fixed border-collapse bg-surface text-left', style: size === null ? undefined : { width: size.width }, children: [size === null ? null : (_jsx("colgroup", { children: size.columnWidths.map((width, index) => (_jsx("col", { style: { width } }, index))) })), _jsx("tbody", { children: renderRow(PsTableReorderService.plain, row, undefined, undefined, true) })] }));
@@ -233,12 +278,16 @@ export const PsDataTable = ({ caption, className, columns, defaultSort, density
233
278
  const selectedRows = selectedIds.length === 0 ? [] : queriedRows.filter((row) => selectedIdSet.has(getRowId(row)));
234
279
  const pageIds = visibleRows.map((row) => getRowId(row));
235
280
  const isPageSelected = pageIds.length > 0 && pageIds.every((rowId) => selectedIdSet.has(rowId));
236
- const togglePage = () => setSelectedIds((current) => {
237
- const pageIdSet = new Set(pageIds);
238
- return isPageSelected
239
- ? current.filter((rowId) => !pageIdSet.has(rowId))
240
- : [...new Set([...current, ...pageIds])];
241
- });
281
+ const togglePage = () => {
282
+ if (isPageSelected && selectAllMatching !== undefined && selectAllMatching.isActive)
283
+ selectAllMatching.onDeactivate();
284
+ setSelectedIds((current) => {
285
+ const pageIdSet = new Set(pageIds);
286
+ return isPageSelected
287
+ ? current.filter((rowId) => !pageIdSet.has(rowId))
288
+ : [...new Set([...current, ...pageIds])];
289
+ });
290
+ };
242
291
  const bodyRef = useRef(null);
243
292
  const [scrollParent, setScrollParent] = useState(null);
244
293
  const [scrollMargin, setScrollMargin] = useState(0);
@@ -316,7 +365,18 @@ export const PsDataTable = ({ caption, className, columns, defaultSort, density
316
365
  : entries.map((entry) => renderRow(api, entry.row, entry.motion, entry.virtualItem));
317
366
  if (isLoading)
318
367
  return (_jsx(PsSkeletonTable, { className: className, columns: visibleColumns.length, hasToolbar: hasToolbar, rows: PsTableConfig.skeletonRows }));
319
- return (_jsxs("div", { ref: containerRef, className: PallasoftClassNameService.merge('flex min-w-0 flex-col gap-3', className), children: [isSelectable && selectedRows.length > 0 && getBulkActions ? (_jsx(PsTableBulkBar, { actions: getBulkActions(selectedRows), onClear: () => setSelectedIds([]), selectedCount: selectedRows.length })) : hasToolbar || columns.some((column) => column.canHide) ? (_jsx(PsTableToolbar, { actions: _jsxs(_Fragment, { children: [toolbarActions, columns.some((column) => column.canHide) ? (_jsx(PsTableColumnPicker, { columns: columns
368
+ const clearSelection = () => {
369
+ setSelectedIds([]);
370
+ if (selectAllMatching !== undefined && selectAllMatching.isActive)
371
+ selectAllMatching.onDeactivate();
372
+ };
373
+ const banner = selectAllMatching !== undefined &&
374
+ selectedRows.length > 0 &&
375
+ selectAllMatching.total > pageIds.length &&
376
+ (isPageSelected || selectAllMatching.isActive) ? (_jsx(PsSelectAllMatchingBanner, { isActive: selectAllMatching.isActive, noun: selectAllMatching.noun, onActivate: selectAllMatching.onActivate, onDeactivate: clearSelection, pageCount: pageIds.length, total: selectAllMatching.total })) : null;
377
+ const topContent = (_jsxs(_Fragment, { children: [isSelectable && selectedRows.length > 0 && getBulkActions ? (_jsx(PsTableBulkBar, { actions: getBulkActions(selectedRows), onClear: clearSelection, selectedCount: selectAllMatching !== undefined && selectAllMatching.isActive
378
+ ? selectAllMatching.total
379
+ : selectedRows.length })) : hasToolbar || columns.some((column) => column.canHide) ? (_jsx(PsTableToolbar, { actions: _jsxs(_Fragment, { children: [toolbarActions, columns.some((column) => column.canHide) ? (_jsx(PsTableColumnPicker, { columns: columns
320
380
  .filter((column) => column.canHide)
321
381
  .map((column) => ({ id: column.id, label: column.label })), hiddenColumnIds: hiddenColumnIds, onChange: (nextHidden) => updateQuery({ hiddenColumnIds: nextHidden }) })) : null] }), columns: hasFilters ? columns : [], filters: filters, isSearchable: isSearchable, onClearFilters: clearFilters, onFilterChange: (columnId, value) => {
322
382
  updateQuery({ filters: { ...filters, [columnId]: value } });
@@ -324,16 +384,25 @@ export const PsDataTable = ({ caption, className, columns, defaultSort, density
324
384
  }, onSearchChange: (next) => {
325
385
  updateQuery({ search: next });
326
386
  resetPage();
327
- }, rows: rows, search: search, searchPlaceholder: searchPlaceholder })) : null, _jsxs("div", { className: 'min-w-0 overflow-hidden rounded-[1.25rem] border border-line bg-surface', children: [showsPagination && paginationPosition !== PsTablePaginationPosition.bottom
387
+ }, rows: rows, search: search, searchPlaceholder: searchPlaceholder })) : null, banner] }));
388
+ return (_jsxs("div", { ref: containerRef, className: PallasoftClassNameService.merge('flex min-w-0 flex-col gap-3', className), children: [stickyTop === undefined ? (topContent) : (_jsxs(_Fragment, { children: [_jsx("div", { "aria-hidden": 'true', className: '-mb-3 h-0', ref: stickySentinelRef }), _jsx("div", { className: PallasoftClassNameService.merge('sticky z-sticky -mx-1 flex flex-col gap-3 px-1 pb-2 pt-1 transition-shadow', stickyClassName === undefined ? 'bg-canvas' : stickyClassName, isStuck ? 'shadow-[0_8px_8px_-8px_rgb(var(--ps-color-ink-rgb,15_23_42)/0.18)]' : ''), ref: stickyRegionRef, style: { top: stickyTop }, children: topContent })] })), _jsxs("div", { "aria-busy": isRefreshing, className: PallasoftClassNameService.merge('min-w-0 rounded-[1.25rem] border border-line bg-surface', stickyTop === undefined ? 'overflow-hidden' : 'overflow-clip'), children: [showsPagination &&
389
+ paginationPosition !== PsTablePaginationPosition.bottom &&
390
+ rowCount > PsTablePaginationConfig.topMinRows
328
391
  ? renderPagination('border-b border-t-0')
329
- : null, mobileLayout === 'cards' ? (_jsxs("div", { className: 'sm:hidden', children: [reorder && reorder.cardsReason ? (_jsx("p", { className: 'border-b border-line px-4 py-2 text-xs text-ink/55', children: reorder.cardsReason })) : null, grouping
392
+ : null, mobileLayout === 'cards' ? (_jsxs("div", { className: PallasoftClassNameService.merge('transition-opacity duration-150 sm:hidden', isRefreshing ? 'opacity-60' : ''), children: [reorder && reorder.cardsReason ? (_jsx("p", { className: 'border-b border-line px-4 py-2 text-xs text-ink/55', children: reorder.cardsReason })) : null, grouping
330
393
  ? groups.map((group, groupIndex) => {
331
394
  const isExpanded = isGroupExpanded(group.key, groupIndex);
332
395
  return (_jsxs("div", { className: 'border-b border-line last:border-b-0', children: [_jsx(PsTableGroupHeader, { count: group.rows.length, isExpanded: isExpanded, label: grouping.getGroupLabel(group.key, group.rows), onToggle: () => toggleGroup(group.key, isExpanded), summary: grouping.renderGroupSummary
333
396
  ? grouping.renderGroupSummary(group.rows)
334
397
  : undefined }), _jsx(PsCollapse, { isOpen: isExpanded && !closingGroupKeys.includes(group.key), children: renderCardList(group.rows, null) })] }, `group-${group.key}`));
335
398
  })
336
- : renderCardList(shouldVirtualize ? visibleRows.slice(0, cards.count) : visibleRows, visibleRows.length === 0 && isSearching ? (_jsx(PsTableSearchingState, { label: searchingLabel, skeletonRows: PsTableConfig.searchingSkeletonRows })) : visibleRows.length === 0 && isQueried ? (_jsxs("div", { className: 'px-4 py-10 text-center', children: [_jsx("p", { className: 'text-sm font-semibold text-ink', children: PsTableConfig.noMatchesTitle }), _jsx("button", { className: PallasoftClassNameService.merge(PsTableControlClasses.textButton, 'mt-2 text-primary'), onClick: clearFilters, type: 'button', children: "Clear search and filters" })] })) : null), !grouping && shouldVirtualize && cards.count < visibleRows.length ? (_jsx("div", { "aria-hidden": 'true', className: 'h-px', ref: cards.sentinelRef })) : null] })) : null, _jsx("div", { className: PallasoftClassNameService.merge('ps-scrollbar overflow-x-auto overflow-y-hidden overscroll-x-contain', mobileLayout === 'cards' ? 'hidden sm:block' : ''), "data-overflow-end": overflow.edges.end, "data-overflow-start": overflow.edges.start, onScroll: overflow.update, ref: overflow.ref, children: renderReorder((api) => (_jsxs("table", { className: PallasoftClassNameService.merge('w-full border-collapse text-left', layout === 'fixed' ? 'table-fixed' : ''), children: [caption ? _jsx("caption", { className: 'sr-only', children: caption }) : null, _jsx("thead", { className: 'bg-mist/70 text-[0.68rem] font-semibold uppercase tracking-[0.18em] text-ink/45', children: _jsxs("tr", { children: [reorder ? (_jsx("th", { className: PallasoftClassNameService.merge('w-9 min-w-9 pl-3 pr-0', pins.reorder), scope: 'col', children: reorder.enabled ? (_jsx("span", { className: 'sr-only', children: reorder.columnLabel })) : reorder.disabledReason ? (_jsx("span", { "aria-label": reorder.disabledReason, className: 'inline-flex cursor-help opacity-40', role: 'img', title: reorder.disabledReason, children: _jsx(PsDragGrip, {}) })) : null })) : null, isSelectable ? (_jsx("th", { className: PallasoftClassNameService.merge('w-10 min-w-10 pl-4 pr-0', pins.select), scope: 'col', children: _jsx(PsCheckbox, { isIndeterminate: !isPageSelected && pageIds.some((rowId) => selectedIdSet.has(rowId)), isSelected: isPageSelected, label: 'Select all rows on this page', onChange: togglePage }) })) : null, visibleColumns.map((column, columnIndex) => {
399
+ : renderCardList(shouldVirtualize ? visibleRows.slice(0, cards.count) : visibleRows, visibleRows.length === 0 && isSearching ? (_jsx(PsTableSearchingState, { label: searchingLabel, skeletonRows: PsTableConfig.searchingSkeletonRows })) : visibleRows.length === 0 && isQueried ? (_jsxs("div", { className: 'px-4 py-10 text-center', children: [_jsx("p", { className: 'text-sm font-semibold text-ink', children: PsTableConfig.noMatchesTitle }), _jsx("button", { className: PallasoftClassNameService.merge(PsTableControlClasses.textButton, 'mt-2 text-primary'), onClick: clearFilters, type: 'button', children: "Clear search and filters" })] })) : null), !grouping && shouldVirtualize && cards.count < visibleRows.length ? (_jsx("div", { "aria-hidden": 'true', className: 'h-px', ref: cards.sentinelRef })) : null] })) : null, _jsx("div", { className: PallasoftClassNameService.merge(stickyTop === undefined
400
+ ? 'ps-scrollbar overflow-x-auto overflow-y-hidden overscroll-x-contain'
401
+ : 'overflow-x-clip', 'transition-opacity duration-150', isRefreshing ? 'opacity-60' : '', mobileLayout === 'cards' ? 'hidden sm:block' : ''), "data-overflow-end": overflow.edges.end, "data-overflow-start": overflow.edges.start, onScroll: overflow.update, ref: overflow.ref, children: renderReorder((api) => (_jsxs("table", { className: PallasoftClassNameService.merge('w-full border-collapse text-left', layout === 'fixed' ? 'table-fixed' : ''), children: [caption ? _jsx("caption", { className: 'sr-only', children: caption }) : null, _jsx("thead", { className: PallasoftClassNameService.merge('bg-mist/70 text-[0.68rem] font-semibold uppercase tracking-[0.18em] text-ink/45', stickyTop === undefined
402
+ ? ''
403
+ : 'sticky z-docked bg-surface [&_th]:border-b [&_th]:border-line [&>tr]:bg-mist/70'), style: stickyTop === undefined
404
+ ? undefined
405
+ : { top: `calc(${stickyTop} + ${stickyHeight}px)` }, children: _jsxs("tr", { children: [reorder ? (_jsx("th", { className: PallasoftClassNameService.merge('w-9 min-w-9 pl-3 pr-0', pins.reorder), scope: 'col', children: reorder.enabled ? (_jsx("span", { className: 'sr-only', children: reorder.columnLabel })) : reorder.disabledReason ? (_jsx("span", { "aria-label": reorder.disabledReason, className: 'inline-flex cursor-help opacity-40', role: 'img', title: reorder.disabledReason, children: _jsx(PsDragGrip, {}) })) : null })) : null, isSelectable ? (_jsx("th", { className: PallasoftClassNameService.merge('w-10 min-w-10 pl-4 pr-0', pins.select), scope: 'col', children: _jsx(PsCheckbox, { isIndeterminate: !isPageSelected && pageIds.some((rowId) => selectedIdSet.has(rowId)), isSelected: isPageSelected, label: 'Select all rows on this page', onChange: togglePage }) })) : null, visibleColumns.map((column, columnIndex) => {
337
406
  const align = column.align !== null && column.align !== undefined ? column.align : 'left';
338
407
  const isSorted = sort !== null && sort.columnId === column.id;
339
408
  return (_jsx("th", { "aria-sort": isSorted
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
3
+ import { PsTableControlClasses } from '../constants/ps-table-control-classes';
4
+ export const PsSelectAllMatchingBanner = ({ className, isActive, noun = 'rows', onActivate, onDeactivate, pageCount, total, }) => (_jsxs("div", { "aria-live": 'polite', className: PallasoftClassNameService.merge('flex flex-wrap items-center justify-center gap-x-2 gap-y-1 rounded-ps-control bg-primary/10 px-3 py-2 text-sm text-ink', className), role: 'status', children: [_jsx("span", { children: isActive
5
+ ? `All ${total.toLocaleString()} ${noun} matching are selected.`
6
+ : `${pageCount.toLocaleString()} ${noun} on this page are selected.` }), _jsx("button", { className: PallasoftClassNameService.merge(PsTableControlClasses.textButton, 'text-primary'), onClick: isActive ? onDeactivate : onActivate, type: 'button', children: isActive ? 'Clear selection' : `Select all ${total.toLocaleString()} matching` })] }));
@@ -1,11 +1,19 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
3
3
  import { PsTableControlClasses } from '../constants/ps-table-control-classes';
4
- export const PsTablePagination = ({ className, isPartial = false, loadedPageCount, onPageChange, page, pageCount, pageSize, totalRows, }) => {
4
+ export const PsTablePagination = ({ className, cursor, isPartial = false, loadedPageCount, onPageChange, page, pageCount, pageSize, totalRows, }) => {
5
5
  const firstRow = totalRows === 0 ? 0 : (page - 1) * pageSize + 1;
6
6
  const lastRow = Math.min(page * pageSize, totalRows);
7
- return (_jsxs("nav", { "aria-label": 'Pagination', className: PallasoftClassNameService.merge('flex flex-wrap items-center justify-between gap-3 border-t border-line px-4 py-3', className), children: [_jsx("p", { className: 'text-sm text-ink/55', children: totalRows === 0
7
+ const approximately = cursor !== undefined && cursor.isEstimate ? 'about ' : '';
8
+ const pageNumber = cursor === undefined ? Math.min(page, pageCount) : cursor.pageNumber;
9
+ const cursorFirst = cursor === undefined ? firstRow : (cursor.pageNumber - 1) * pageSize + 1;
10
+ const cursorLast = cursor === undefined ? lastRow : Math.min(cursor.pageNumber * pageSize, totalRows);
11
+ return (_jsxs("nav", { "aria-label": 'Pagination', className: PallasoftClassNameService.merge('flex flex-wrap items-center justify-between gap-x-6 gap-y-3 border-t border-line px-5 py-3', className), children: [_jsx("p", { className: 'shrink-0 whitespace-nowrap text-sm text-ink/55', children: totalRows === 0
8
12
  ? 'No rows'
9
- : `${firstRow}–${lastRow} of ${totalRows}${isPartial ? '+' : ''}` }), _jsxs("div", { className: 'flex items-center gap-2', children: [_jsx("button", { className: PsTableControlClasses.paginationButton, disabled: page <= 1, onClick: () => onPageChange(page - 1), type: 'button', children: "Previous" }), _jsxs("span", { className: 'hidden text-sm text-ink/65 sm:inline', children: ["Page ", Math.min(page, pageCount), " of ", pageCount] }), _jsx("button", { className: PsTableControlClasses.paginationButton, disabled: page >=
10
- (loadedPageCount === undefined ? pageCount : Math.min(loadedPageCount, pageCount)), onClick: () => onPageChange(page + 1), type: 'button', children: "Next" })] })] }));
13
+ : cursor === undefined
14
+ ? `${firstRow}–${lastRow} of ${totalRows}${isPartial ? '+' : ''}`
15
+ : `${cursorFirst}–${cursorLast} of ${approximately}${totalRows.toLocaleString()}` }), _jsxs("div", { className: 'flex shrink-0 items-center gap-3', children: [cursor !== undefined && cursor.pageSizeOptions !== undefined && cursor.onPageSizeChange ? (_jsxs("label", { className: 'flex shrink-0 items-center gap-2 whitespace-nowrap text-sm text-ink/65', children: [_jsx("span", { className: 'hidden sm:inline', children: "Rows per page" }), _jsx("select", { "aria-label": 'Rows per page', className: 'ps-input-control ps-focus-ring ps-control ps-control-padding w-auto shrink-0 rounded-ps-control text-sm', onChange: (event) => cursor.onPageSizeChange?.(Number(event.target.value)), value: pageSize, children: cursor.pageSizeOptions.map((option) => (_jsx("option", { value: option, children: option }, option))) })] })) : null, _jsx("button", { className: PsTableControlClasses.paginationButton, disabled: cursor === undefined ? page <= 1 : !cursor.hasPrevious, onClick: () => (cursor === undefined ? onPageChange(page - 1) : cursor.onPrevious()), type: 'button', children: "Previous" }), _jsxs("span", { className: 'hidden whitespace-nowrap text-sm text-ink/65 sm:inline', children: ["Page ", pageNumber, " of ", approximately, pageCount] }), _jsx("button", { className: PsTableControlClasses.paginationButton, disabled: cursor === undefined
16
+ ? page >=
17
+ (loadedPageCount === undefined ? pageCount : Math.min(loadedPageCount, pageCount))
18
+ : !cursor.hasNext, onClick: () => (cursor === undefined ? onPageChange(page + 1) : cursor.onNext()), onFocus: cursor === undefined ? undefined : cursor.onNextIntent, onPointerEnter: cursor === undefined ? undefined : cursor.onNextIntent, type: 'button', children: "Next" })] })] }));
11
19
  };
@@ -0,0 +1,8 @@
1
+ export const PsServerRowsConfig = {
2
+ cacheSize: 8,
3
+ debounceMs: 250,
4
+ defaultPageSize: 100,
5
+ idleFallbackMs: 200,
6
+ idleTimeoutMs: 1000,
7
+ pageSizes: [50, 100, 200],
8
+ };
@@ -0,0 +1,3 @@
1
+ export const PsTablePaginationConfig = {
2
+ topMinRows: 20,
3
+ };