@pallasoft/ps-table-components 1.8.0 → 1.10.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.
Files changed (52) hide show
  1. package/dist/components/ps-data-table.d.ts +4 -1
  2. package/dist/components/ps-data-table.js +89 -23
  3. package/dist/components/ps-table-bulk-bar.js +7 -1
  4. package/dist/components/ps-table-card-list.d.ts +2 -1
  5. package/dist/components/ps-table-card-list.js +14 -6
  6. package/dist/components/ps-table-group-header.d.ts +9 -0
  7. package/dist/components/ps-table-group-header.js +8 -0
  8. package/dist/components/ps-table-toolbar.js +1 -1
  9. package/dist/constants/ps-table-search-config.js +1 -1
  10. package/dist/constants/table-card-role.d.ts +6 -0
  11. package/dist/constants/table-card-role.js +8 -0
  12. package/dist/constants/table-config.d.ts +3 -0
  13. package/dist/constants/table-config.js +3 -0
  14. package/dist/esm/components/ps-data-table.js +199 -0
  15. package/dist/esm/components/ps-table-bulk-bar.js +9 -0
  16. package/dist/esm/components/ps-table-card-list.js +36 -0
  17. package/dist/esm/components/ps-table-column-picker.js +16 -0
  18. package/dist/esm/components/ps-table-filter-field.js +12 -0
  19. package/dist/esm/components/ps-table-group-header.js +4 -0
  20. package/dist/esm/components/ps-table-pagination.js +8 -0
  21. package/dist/esm/components/ps-table-sort-header.js +14 -0
  22. package/dist/esm/components/ps-table-toolbar.js +14 -0
  23. package/dist/esm/constants/ps-table-search-config.js +6 -0
  24. package/dist/esm/constants/table-card-role.js +5 -0
  25. package/dist/esm/constants/table-config.js +8 -0
  26. package/dist/esm/constants/table-filter-kind.js +6 -0
  27. package/dist/esm/index.js +17 -0
  28. package/dist/esm/interfaces/table-column-filter.js +1 -0
  29. package/dist/esm/interfaces/table-column.js +1 -0
  30. package/dist/esm/interfaces/table-filter-option.js +1 -0
  31. package/dist/esm/interfaces/table-filter-value.js +1 -0
  32. package/dist/esm/interfaces/table-grouping.js +1 -0
  33. package/dist/esm/interfaces/table-query.js +1 -0
  34. package/dist/esm/interfaces/table-row-action.js +1 -0
  35. package/dist/esm/interfaces/table-row-group.js +1 -0
  36. package/dist/esm/interfaces/table-row-motion.js +1 -0
  37. package/dist/esm/interfaces/table-sort.js +1 -0
  38. package/dist/esm/services/ps-table-group-service.js +14 -0
  39. package/dist/esm/services/ps-table-query-service.js +93 -0
  40. package/dist/esm/services/ps-table-search-service.js +24 -0
  41. package/dist/index.d.ts +5 -0
  42. package/dist/index.js +5 -0
  43. package/dist/interfaces/table-column.d.ts +2 -0
  44. package/dist/interfaces/table-grouping.d.ts +7 -0
  45. package/dist/interfaces/table-grouping.js +2 -0
  46. package/dist/interfaces/table-row-group.d.ts +4 -0
  47. package/dist/interfaces/table-row-group.js +2 -0
  48. package/dist/interfaces/table-row-motion.d.ts +4 -0
  49. package/dist/interfaces/table-row-motion.js +2 -0
  50. package/dist/services/ps-table-group-service.d.ts +4 -0
  51. package/dist/services/ps-table-group-service.js +18 -0
  52. package/package.json +5 -2
@@ -1,5 +1,6 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import type { PsTableColumn } from '../interfaces/table-column';
3
+ import type { PsTableGrouping } from '../interfaces/table-grouping';
3
4
  import type { PsTableQuery } from '../interfaces/table-query';
4
5
  import type { PsTableRowAction } from '../interfaces/table-row-action';
5
6
  import type { PsTableSort } from '../interfaces/table-sort';
@@ -15,9 +16,11 @@ export interface PsDataTableProps<RowData> {
15
16
  getRowId: (row: RowData) => string;
16
17
  getRowLabel?: (row: RowData) => string;
17
18
  getSearchText?: (row: RowData) => string;
19
+ grouping?: PsTableGrouping<RowData>;
18
20
  onPageChange?: (page: number) => void;
19
21
  onQueryChange?: (query: PsTableQuery) => void;
20
22
  onRowClick?: (row: RowData) => void;
23
+ onRowHover?: (row: RowData) => void;
21
24
  page?: number;
22
25
  pageSize?: number;
23
26
  query?: PsTableQuery;
@@ -27,4 +30,4 @@ export interface PsDataTableProps<RowData> {
27
30
  toolbarActions?: ReactNode;
28
31
  totalRows?: number;
29
32
  }
30
- export declare const PsDataTable: <RowData>({ caption, className, columns, defaultSort, density, expandedRowIds, mobileLayout, getBulkActions, getRowId, getRowLabel, getSearchText, onPageChange, onQueryChange, onRowClick, page, pageSize, query, renderExpandedRow, rows, searchPlaceholder, toolbarActions, totalRows, }: PsDataTableProps<RowData>) => import("react").JSX.Element;
33
+ export declare const PsDataTable: <RowData>({ caption, className, columns, defaultSort, density, expandedRowIds, mobileLayout, getBulkActions, getRowId, getRowLabel, getSearchText, grouping, onPageChange, onQueryChange, onRowClick, onRowHover, page, pageSize, query, renderExpandedRow, rows, searchPlaceholder, toolbarActions, totalRows, }: PsDataTableProps<RowData>) => import("react").JSX.Element;
@@ -7,10 +7,12 @@ const ps_design_system_1 = require("@pallasoft/ps-design-system");
7
7
  const ps_input_components_1 = require("@pallasoft/ps-input-components");
8
8
  const ps_motion_components_1 = require("@pallasoft/ps-motion-components");
9
9
  const table_config_1 = require("../constants/table-config");
10
+ const ps_table_group_service_1 = require("../services/ps-table-group-service");
10
11
  const ps_table_query_service_1 = require("../services/ps-table-query-service");
11
12
  const ps_table_bulk_bar_1 = require("./ps-table-bulk-bar");
12
13
  const ps_table_card_list_1 = require("./ps-table-card-list");
13
14
  const ps_table_column_picker_1 = require("./ps-table-column-picker");
15
+ const ps_table_group_header_1 = require("./ps-table-group-header");
14
16
  const ps_table_pagination_1 = require("./ps-table-pagination");
15
17
  const ps_table_sort_header_1 = require("./ps-table-sort-header");
16
18
  const ps_table_toolbar_1 = require("./ps-table-toolbar");
@@ -30,11 +32,14 @@ const alignClassNames = {
30
32
  left: 'text-left',
31
33
  right: 'text-right tabular-nums',
32
34
  };
33
- const PsDataTable = ({ caption, className, columns, defaultSort, density = 'normal', expandedRowIds, mobileLayout = 'cards', getBulkActions, getRowId, getRowLabel, getSearchText, onPageChange, onQueryChange, onRowClick, page, pageSize, query, renderExpandedRow, rows, searchPlaceholder = table_config_1.PsTableConfig.searchPlaceholder, toolbarActions, totalRows, }) => {
35
+ const PsDataTable = ({ caption, className, columns, defaultSort, density = 'normal', expandedRowIds, mobileLayout = 'cards', getBulkActions, getRowId, getRowLabel, getSearchText, grouping, onPageChange, onQueryChange, onRowClick, onRowHover, page, pageSize, query, renderExpandedRow, rows, searchPlaceholder = table_config_1.PsTableConfig.searchPlaceholder, toolbarActions, totalRows, }) => {
34
36
  const containerRef = (0, react_1.useRef)(null);
35
37
  const [uncontrolledPage, setUncontrolledPage] = (0, react_1.useState)(1);
36
38
  const [uncontrolledQuery, setUncontrolledQuery] = (0, react_1.useState)(() => ps_table_query_service_1.PsTableQueryService.empty(defaultSort ? defaultSort : null));
37
39
  const [selectedIds, setSelectedIds] = (0, react_1.useState)([]);
40
+ const [toggledGroupKeys, setToggledGroupKeys] = (0, react_1.useState)([]);
41
+ const [closingGroupKeys, setClosingGroupKeys] = (0, react_1.useState)([]);
42
+ const [openedGroupKeys, setOpenedGroupKeys] = (0, react_1.useState)([]);
38
43
  const currentQuery = query ? query : uncontrolledQuery;
39
44
  const { filters, hiddenColumnIds, search, sort } = currentQuery;
40
45
  const visibleColumns = columns.filter((column) => !hiddenColumnIds.includes(column.id));
@@ -60,11 +65,56 @@ const PsDataTable = ({ caption, className, columns, defaultSort, density = 'norm
60
65
  const safePage = Math.min(Math.max(currentPage, 1), pageCount);
61
66
  const isQueried = search.trim() !== '' ||
62
67
  Object.values(filters).some((value) => ps_table_query_service_1.PsTableQueryService.isActive(value));
68
+ const groups = (0, react_1.useMemo)(() => (grouping ? ps_table_group_service_1.PsTableGroupService.group(queriedRows, grouping.getGroupKey) : []), [grouping, queriedRows]);
69
+ const initialExpandedCount = grouping && grouping.initialExpandedCount !== undefined ? grouping.initialExpandedCount : 1;
70
+ const isGroupExpanded = (groupKey, groupIndex) => isQueried || groupIndex < initialExpandedCount !== toggledGroupKeys.includes(groupKey);
71
+ const flipGroup = (groupKey) => setToggledGroupKeys((current) => current.includes(groupKey)
72
+ ? current.filter((key) => key !== groupKey)
73
+ : [...current, groupKey]);
74
+ const toggleGroup = (groupKey, isExpanded) => {
75
+ if (closingGroupKeys.includes(groupKey))
76
+ return;
77
+ if (!isExpanded) {
78
+ setOpenedGroupKeys((current) => [...current.filter((key) => key !== groupKey), groupKey]);
79
+ flipGroup(groupKey);
80
+ return;
81
+ }
82
+ setClosingGroupKeys((current) => [...current, groupKey]);
83
+ window.setTimeout(() => {
84
+ flipGroup(groupKey);
85
+ setOpenedGroupKeys((current) => current.filter((key) => key !== groupKey));
86
+ setClosingGroupKeys((current) => current.filter((key) => key !== groupKey));
87
+ }, table_config_1.PsTableConfig.groupCloseMs);
88
+ };
89
+ const groupRowMotion = (groupKey, rowIndex) => {
90
+ if (closingGroupKeys.includes(groupKey))
91
+ return { className: 'ps-table-row-exit' };
92
+ if (!openedGroupKeys.includes(groupKey))
93
+ return undefined;
94
+ return {
95
+ className: 'ps-table-row-enter',
96
+ delayMs: Math.min(rowIndex, table_config_1.PsTableConfig.groupStaggerLimit) * table_config_1.PsTableConfig.groupRowStaggerMs,
97
+ };
98
+ };
63
99
  const visibleRows = (0, react_1.useMemo)(() => {
100
+ if (grouping)
101
+ return groups.flatMap((group, groupIndex) => isQueried || groupIndex < initialExpandedCount !== toggledGroupKeys.includes(group.key)
102
+ ? group.rows
103
+ : []);
64
104
  if (!pageSize || isServerPaginated)
65
105
  return queriedRows;
66
106
  return queriedRows.slice((safePage - 1) * pageSize, safePage * pageSize);
67
- }, [isServerPaginated, pageSize, queriedRows, safePage]);
107
+ }, [
108
+ grouping,
109
+ groups,
110
+ initialExpandedCount,
111
+ isQueried,
112
+ isServerPaginated,
113
+ pageSize,
114
+ queriedRows,
115
+ safePage,
116
+ toggledGroupKeys,
117
+ ]);
68
118
  const changePage = (next) => {
69
119
  if (onPageChange) {
70
120
  onPageChange(next);
@@ -96,6 +146,24 @@ const PsDataTable = ({ caption, className, columns, defaultSort, density = 'norm
96
146
  ? current.filter((rowId) => !pageIds.includes(rowId))
97
147
  : [...new Set([...current, ...pageIds])]);
98
148
  const toggleRow = (rowId) => setSelectedIds((current) => current.includes(rowId) ? current.filter((id) => id !== rowId) : [...current, rowId]);
149
+ const renderCardList = (cardRows, emptyContent) => ((0, jsx_runtime_1.jsx)(ps_table_card_list_1.PsTableCardList, { columns: visibleColumns, emptyContent: emptyContent, expandedRowIds: expandedRowIds, footerColumns: hasFooter ? visibleColumns.filter((column) => column.footer !== undefined) : [], getRowId: getRowId, getRowLabel: (row) => (getRowLabel ? getRowLabel(row) : getRowId(row)), isSelectable: isSelectable, onRowClick: onRowClick, onRowHover: onRowHover, onToggleRow: toggleRow, renderExpandedRow: renderExpandedRow, rows: cardRows, selectedIds: selectedIds }));
150
+ const renderRow = (row, motion) => {
151
+ const rowId = getRowId(row);
152
+ const isExpanded = expandedRowIds ? expandedRowIds.includes(rowId) : false;
153
+ const rowLabel = getRowLabel ? getRowLabel(row) : rowId;
154
+ return ((0, jsx_runtime_1.jsxs)(react_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("tr", { "aria-expanded": renderExpandedRow ? isExpanded : undefined, "aria-label": onRowClick ? rowLabel : undefined, className: ps_design_system_1.PallasoftClassNameService.merge('align-middle transition-colors', motion ? motion.className : '', onRowClick ? 'ps-focus-ring cursor-pointer hover:bg-mist/60' : 'hover:bg-mist/40', isExpanded ? 'bg-mist/40' : ''), onClick: onRowClick ? () => onRowClick(row) : undefined, onFocus: onRowHover ? () => onRowHover(row) : undefined, onPointerEnter: onRowHover ? () => onRowHover(row) : undefined, onKeyDown: onRowClick
155
+ ? (event) => {
156
+ if (event.target !== event.currentTarget)
157
+ return;
158
+ if (event.key !== 'Enter' && event.key !== ' ')
159
+ return;
160
+ event.preventDefault();
161
+ onRowClick(row);
162
+ }
163
+ : undefined, style: motion && motion.delayMs !== undefined
164
+ ? { animationDelay: `${motion.delayMs}ms` }
165
+ : undefined, tabIndex: onRowClick ? 0 : undefined, children: [isSelectable ? ((0, jsx_runtime_1.jsx)("td", { className: 'w-10 pl-4 pr-0', onClick: (event) => event.stopPropagation(), onKeyDown: (event) => event.stopPropagation(), children: (0, jsx_runtime_1.jsx)(ps_input_components_1.PsCheckbox, { isSelected: selectedIds.includes(rowId), label: `Select ${rowLabel}`, onChange: () => toggleRow(rowId) }) })) : null, visibleColumns.map((column) => ((0, jsx_runtime_1.jsx)("td", { className: ps_design_system_1.PallasoftClassNameService.merge(densityClassNames[density].cell, alignClassNames[column.align !== null && column.align !== undefined ? column.align : 'left'], column.wrap ? '' : 'whitespace-nowrap', 'text-sm text-ink'), style: columnStyle(column), children: column.render(row) }, column.id)))] }), renderExpandedRow ? ((0, jsx_runtime_1.jsx)("tr", { "aria-hidden": !isExpanded, className: isExpanded ? 'border-t-0' : 'border-0', children: (0, jsx_runtime_1.jsx)("td", { className: 'p-0', colSpan: columnCount, children: (0, jsx_runtime_1.jsx)(ps_motion_components_1.PsCollapse, { isOpen: isExpanded, children: (0, jsx_runtime_1.jsx)("div", { className: 'w-0 min-w-full border-l-2 border-primary bg-mist/40 px-4 py-4', children: renderExpandedRow(row) }) }) }) })) : null] }, rowId));
166
+ };
99
167
  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
100
168
  .filter((column) => column.canHide)
101
169
  .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) => {
@@ -104,7 +172,14 @@ const PsDataTable = ({ caption, className, columns, defaultSort, density = 'norm
104
172
  }, onSearchChange: (next) => {
105
173
  updateQuery({ search: next });
106
174
  resetPage();
107
- }, 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: [mobileLayout === 'cards' ? ((0, jsx_runtime_1.jsx)("div", { className: 'sm:hidden', children: (0, jsx_runtime_1.jsx)(ps_table_card_list_1.PsTableCardList, { columns: visibleColumns, emptyContent: 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-focus-ring mt-2 rounded-full px-3 py-1.5 text-sm font-semibold text-primary', onClick: clearFilters, type: 'button', children: "Clear search and filters" })] })) : null, expandedRowIds: expandedRowIds, footerColumns: hasFooter ? visibleColumns.filter((column) => column.footer !== undefined) : [], getRowId: getRowId, getRowLabel: (row) => (getRowLabel ? getRowLabel(row) : getRowId(row)), isSelectable: isSelectable, onRowClick: onRowClick, onToggleRow: toggleRow, renderExpandedRow: renderExpandedRow, rows: visibleRows, selectedIds: selectedIds }) })) : null, (0, jsx_runtime_1.jsx)("div", { className: ps_design_system_1.PallasoftClassNameService.merge('ps-scrollbar overflow-x-auto', mobileLayout === 'cards' ? 'hidden sm:block' : ''), children: (0, jsx_runtime_1.jsxs)("table", { className: 'w-full border-collapse text-left', 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: [isSelectable ? ((0, jsx_runtime_1.jsx)("th", { className: 'w-10 pl-4 pr-0', scope: 'col', children: (0, jsx_runtime_1.jsx)(ps_input_components_1.PsCheckbox, { isIndeterminate: !isPageSelected && pageIds.some((rowId) => selectedIds.includes(rowId)), isSelected: isPageSelected, label: 'Select all rows on this page', onChange: togglePage }) })) : null, visibleColumns.map((column) => {
175
+ }, 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: [mobileLayout === 'cards' ? ((0, jsx_runtime_1.jsx)("div", { className: 'sm:hidden', children: grouping
176
+ ? groups.map((group, groupIndex) => {
177
+ const isExpanded = isGroupExpanded(group.key, groupIndex);
178
+ 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
179
+ ? grouping.renderGroupSummary(group.rows)
180
+ : 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}`));
181
+ })
182
+ : renderCardList(visibleRows, 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-focus-ring mt-2 rounded-full px-3 py-1.5 text-sm font-semibold text-primary', onClick: clearFilters, type: 'button', children: "Clear search and filters" })] })) : null) })) : null, (0, jsx_runtime_1.jsx)("div", { className: ps_design_system_1.PallasoftClassNameService.merge('ps-scrollbar overflow-x-auto', mobileLayout === 'cards' ? 'hidden sm:block' : ''), children: (0, jsx_runtime_1.jsxs)("table", { className: 'w-full border-collapse text-left', 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: [isSelectable ? ((0, jsx_runtime_1.jsx)("th", { className: 'w-10 pl-4 pr-0', scope: 'col', children: (0, jsx_runtime_1.jsx)(ps_input_components_1.PsCheckbox, { isIndeterminate: !isPageSelected && pageIds.some((rowId) => selectedIds.includes(rowId)), isSelected: isPageSelected, label: 'Select all rows on this page', onChange: togglePage }) })) : null, visibleColumns.map((column) => {
108
183
  const align = column.align !== null && column.align !== undefined ? column.align : 'left';
109
184
  const isSorted = sort !== null && sort.columnId === column.id;
110
185
  return ((0, jsx_runtime_1.jsx)("th", { "aria-sort": isSorted
@@ -112,26 +187,17 @@ const PsDataTable = ({ caption, className, columns, defaultSort, density = 'norm
112
187
  : column.sortValue && !isServerPaginated
113
188
  ? 'none'
114
189
  : undefined, className: ps_design_system_1.PallasoftClassNameService.merge(densityClassNames[density].head, alignClassNames[align], 'whitespace-nowrap'), scope: 'col', style: columnStyle(column), children: column.sortValue && !isServerPaginated ? ((0, jsx_runtime_1.jsx)(ps_table_sort_header_1.PsTableSortHeader, { align: align, columnId: column.id, label: column.label, onSort: updateSort, sort: sort })) : (column.label) }, column.id));
115
- })] }) }), (0, jsx_runtime_1.jsxs)("tbody", { className: 'divide-y divide-line', children: [visibleRows.length === 0 && isQueried ? ((0, jsx_runtime_1.jsx)("tr", { children: (0, jsx_runtime_1.jsxs)("td", { className: 'px-4 py-12 text-center', colSpan: columnCount, 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)("p", { className: 'mt-1 text-sm text-ink/55', children: table_config_1.PsTableConfig.noMatchesDescription }), (0, jsx_runtime_1.jsx)("button", { className: 'ps-focus-ring mt-3 rounded-full px-3 py-1.5 text-sm font-semibold text-primary hover:underline', onClick: clearFilters, type: 'button', children: "Clear search and filters" })] }) })) : null, visibleRows.map((row) => {
116
- const rowId = getRowId(row);
117
- const isExpanded = expandedRowIds ? expandedRowIds.includes(rowId) : false;
118
- const rowLabel = getRowLabel ? getRowLabel(row) : rowId;
119
- return ((0, jsx_runtime_1.jsxs)(react_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("tr", { "aria-expanded": renderExpandedRow ? isExpanded : undefined, "aria-label": onRowClick ? rowLabel : undefined, className: ps_design_system_1.PallasoftClassNameService.merge('align-middle transition-colors', onRowClick
120
- ? 'ps-focus-ring cursor-pointer hover:bg-mist/60'
121
- : 'hover:bg-mist/40', isExpanded ? 'bg-mist/40' : ''), onClick: onRowClick ? () => onRowClick(row) : undefined, onKeyDown: onRowClick
122
- ? (event) => {
123
- if (event.target !== event.currentTarget)
124
- return;
125
- if (event.key !== 'Enter' && event.key !== ' ')
126
- return;
127
- event.preventDefault();
128
- onRowClick(row);
129
- }
130
- : undefined, tabIndex: onRowClick ? 0 : undefined, children: [isSelectable ? ((0, jsx_runtime_1.jsx)("td", { className: 'w-10 pl-4 pr-0', onClick: (event) => event.stopPropagation(), onKeyDown: (event) => event.stopPropagation(), children: (0, jsx_runtime_1.jsx)(ps_input_components_1.PsCheckbox, { isSelected: selectedIds.includes(rowId), label: `Select ${rowLabel}`, onChange: () => toggleRow(rowId) }) })) : null, visibleColumns.map((column) => ((0, jsx_runtime_1.jsx)("td", { className: ps_design_system_1.PallasoftClassNameService.merge(densityClassNames[density].cell, alignClassNames[column.align !== null && column.align !== undefined
131
- ? column.align
132
- : 'left'], column.wrap ? '' : 'whitespace-nowrap', 'text-sm text-ink'), style: columnStyle(column), children: column.render(row) }, column.id)))] }), renderExpandedRow && isExpanded ? ((0, jsx_runtime_1.jsx)("tr", { className: 'border-t-0', children: (0, jsx_runtime_1.jsx)("td", { className: 'p-0', colSpan: columnCount, children: (0, jsx_runtime_1.jsx)(ps_motion_components_1.PsCollapse, { isOpen: true, children: (0, jsx_runtime_1.jsx)("div", { className: 'w-0 min-w-full border-l-2 border-primary bg-mist/40 px-4 py-4', children: renderExpandedRow(row) }) }) }) })) : null] }, rowId));
133
- })] }), hasFooter ? ((0, jsx_runtime_1.jsx)("tfoot", { className: 'border-t-2 border-line bg-mist/50 text-sm font-semibold text-ink', children: (0, jsx_runtime_1.jsxs)("tr", { children: [isSelectable ? (0, jsx_runtime_1.jsx)("td", {}) : null, visibleColumns.map((column) => ((0, jsx_runtime_1.jsx)("td", { className: ps_design_system_1.PallasoftClassNameService.merge(densityClassNames[density].cell, alignClassNames[column.align !== null && column.align !== undefined
190
+ })] }) }), (0, jsx_runtime_1.jsxs)("tbody", { className: 'divide-y divide-line', children: [visibleRows.length === 0 && isQueried ? ((0, jsx_runtime_1.jsx)("tr", { children: (0, jsx_runtime_1.jsxs)("td", { className: 'px-4 py-12 text-center', colSpan: columnCount, 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)("p", { className: 'mt-1 text-sm text-ink/55', children: table_config_1.PsTableConfig.noMatchesDescription }), (0, jsx_runtime_1.jsx)("button", { className: 'ps-focus-ring mt-3 rounded-full px-3 py-1.5 text-sm font-semibold text-primary hover:underline', onClick: clearFilters, type: 'button', children: "Clear search and filters" })] }) })) : null, grouping
191
+ ? groups.map((group, groupIndex) => {
192
+ const isExpanded = isGroupExpanded(group.key, groupIndex);
193
+ return ((0, jsx_runtime_1.jsxs)(react_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("tr", { children: (0, jsx_runtime_1.jsx)("td", { className: 'p-0', colSpan: columnCount, 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
194
+ ? grouping.renderGroupSummary(group.rows)
195
+ : undefined }) }) }), isExpanded
196
+ ? group.rows.map((row, rowIndex) => renderRow(row, groupRowMotion(group.key, rowIndex)))
197
+ : null] }, `group-${group.key}`));
198
+ })
199
+ : visibleRows.map((row) => renderRow(row))] }), hasFooter ? ((0, jsx_runtime_1.jsx)("tfoot", { className: 'border-t-2 border-line bg-mist/50 text-sm font-semibold text-ink', children: (0, jsx_runtime_1.jsxs)("tr", { children: [isSelectable ? (0, jsx_runtime_1.jsx)("td", {}) : null, visibleColumns.map((column) => ((0, jsx_runtime_1.jsx)("td", { className: ps_design_system_1.PallasoftClassNameService.merge(densityClassNames[density].cell, alignClassNames[column.align !== null && column.align !== undefined
134
200
  ? column.align
135
- : 'left'], 'whitespace-nowrap'), children: column.footer }, column.id)))] }) })) : null] }) }), pageSize ? ((0, jsx_runtime_1.jsx)(ps_table_pagination_1.PsTablePagination, { onPageChange: changePage, page: safePage, pageCount: pageCount, pageSize: pageSize, totalRows: rowCount })) : null] })] }));
201
+ : 'left'], 'whitespace-nowrap'), children: column.footer }, column.id)))] }) })) : null] }) }), pageSize && !grouping ? ((0, jsx_runtime_1.jsx)(ps_table_pagination_1.PsTablePagination, { onPageChange: changePage, page: safePage, pageCount: pageCount, pageSize: pageSize, totalRows: rowCount })) : null] })] }));
136
202
  };
137
203
  exports.PsDataTable = PsDataTable;
@@ -3,5 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PsTableBulkBar = 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
- const PsTableBulkBar = ({ actions, onClear, selectedCount }) => ((0, jsx_runtime_1.jsxs)("div", { "aria-live": 'polite', className: 'flex flex-wrap items-center gap-1 rounded-2xl border border-primary/30 bg-primary/10 px-2 py-1.5 sm:gap-2 sm:rounded-full', children: [(0, jsx_runtime_1.jsxs)("span", { className: 'px-2 text-sm font-semibold text-ink', children: [selectedCount, " selected"] }), actions.map((action) => ((0, jsx_runtime_1.jsx)("button", { className: ps_design_system_1.PallasoftClassNameService.merge('ps-focus-ring inline-flex min-h-8 items-center rounded-full px-3 text-sm font-semibold transition-colors disabled:opacity-40', action.isDanger ? 'text-danger hover:bg-danger/10' : 'text-ink hover:bg-ink/5'), disabled: action.isDisabled, onClick: action.onSelect, type: 'button', children: action.label }, action.id))), (0, jsx_runtime_1.jsx)("button", { className: 'ps-focus-ring ml-auto inline-flex min-h-8 items-center rounded-full px-3 text-sm font-semibold text-ink/65 hover:text-ink', onClick: onClear, type: 'button', children: "Clear selection" })] }));
6
+ const ps_icons_1 = require("@pallasoft/ps-icons");
7
+ const PsTableBulkBar = ({ actions, onClear, selectedCount }) => {
8
+ const safeActions = actions.filter((action) => !action.isDanger);
9
+ const dangerActions = actions.filter((action) => action.isDanger);
10
+ const renderAction = (action) => ((0, jsx_runtime_1.jsx)("button", { className: ps_design_system_1.PallasoftClassNameService.merge('ps-focus-ring inline-flex min-h-9 shrink-0 items-center whitespace-nowrap rounded-full px-3 text-sm font-semibold transition-colors disabled:opacity-40', action.isDanger ? 'text-danger hover:bg-danger/10' : 'text-ink hover:bg-ink/5'), disabled: action.isDisabled, onClick: action.onSelect, type: 'button', children: action.label }, action.id));
11
+ return ((0, jsx_runtime_1.jsxs)("div", { "aria-live": 'polite', className: 'flex min-h-12 items-center gap-2 rounded-2xl border border-primary/30 bg-primary/10 py-1.5 pl-2 pr-1.5', children: [(0, jsx_runtime_1.jsxs)("span", { className: 'inline-flex min-h-9 shrink-0 items-center rounded-full bg-primary px-3 text-sm font-semibold tabular-nums text-white', children: [selectedCount, " selected"] }), (0, jsx_runtime_1.jsxs)("div", { className: 'ps-scrollbar-hidden flex min-w-0 flex-1 items-center gap-1 overflow-x-auto', children: [safeActions.map(renderAction), dangerActions.length > 0 && safeActions.length > 0 ? ((0, jsx_runtime_1.jsx)("span", { "aria-hidden": 'true', className: 'mx-1 h-5 w-px shrink-0 bg-line' })) : null, dangerActions.map(renderAction)] }), (0, jsx_runtime_1.jsx)("button", { "aria-label": 'Clear selection', className: 'ps-focus-ring inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-ink/60 transition-colors hover:bg-ink/5 hover:text-ink', onClick: onClear, title: 'Clear selection', type: 'button', children: (0, jsx_runtime_1.jsx)(ps_icons_1.PsIcon, { "aria-hidden": 'true', className: 'h-4 w-4', iconKey: ps_icons_1.PsIconKey.Close }) })] }));
12
+ };
7
13
  exports.PsTableBulkBar = PsTableBulkBar;
@@ -9,9 +9,10 @@ export interface PsTableCardListProps<RowData> {
9
9
  getRowLabel: (row: RowData) => string;
10
10
  isSelectable: boolean;
11
11
  onRowClick?: (row: RowData) => void;
12
+ onRowHover?: (row: RowData) => void;
12
13
  onToggleRow: (rowId: string) => void;
13
14
  renderExpandedRow?: (row: RowData) => ReactNode;
14
15
  rows: RowData[];
15
16
  selectedIds: string[];
16
17
  }
17
- export declare const PsTableCardList: <RowData>({ columns, emptyContent, expandedRowIds, footerColumns, getRowId, getRowLabel, isSelectable, onRowClick, onToggleRow, renderExpandedRow, rows, selectedIds, }: PsTableCardListProps<RowData>) => import("react").JSX.Element;
18
+ export declare const PsTableCardList: <RowData>({ columns, emptyContent, expandedRowIds, footerColumns, getRowId, getRowLabel, isSelectable, onRowClick, onRowHover, onToggleRow, renderExpandedRow, rows, selectedIds, }: PsTableCardListProps<RowData>) => import("react").JSX.Element;
@@ -5,14 +5,19 @@ const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const ps_design_system_1 = require("@pallasoft/ps-design-system");
6
6
  const ps_input_components_1 = require("@pallasoft/ps-input-components");
7
7
  const ps_motion_components_1 = require("@pallasoft/ps-motion-components");
8
- const PsTableCardList = ({ columns, emptyContent, expandedRowIds, footerColumns, getRowId, getRowLabel, isSelectable, onRowClick, onToggleRow, renderExpandedRow, rows, selectedIds, }) => {
8
+ const table_card_role_1 = require("../constants/table-card-role");
9
+ const PsTableCardList = ({ columns, emptyContent, expandedRowIds, footerColumns, getRowId, getRowLabel, isSelectable, onRowClick, onRowHover, onToggleRow, renderExpandedRow, rows, selectedIds, }) => {
9
10
  const [primary, ...details] = columns.filter((column) => !column.hideOnCards);
10
11
  const trailing = details.find((column) => column.align === 'right');
11
- const rest = details.filter((column) => column !== trailing);
12
+ const others = details.filter((column) => column !== trailing);
13
+ const subtitles = others.filter((column) => column.cardRole === table_card_role_1.PsTableCardRole.subtitle);
14
+ const metas = others.filter((column) => column.cardRole === table_card_role_1.PsTableCardRole.meta);
15
+ const rest = others.filter((column) => column.cardRole === undefined || column.cardRole === table_card_role_1.PsTableCardRole.detail);
16
+ const hasValue = (value) => value !== null && value !== undefined && value !== '';
12
17
  return ((0, jsx_runtime_1.jsxs)("ul", { className: 'divide-y divide-line', children: [rows.length === 0 ? (0, jsx_runtime_1.jsx)("li", { children: emptyContent }) : null, rows.map((row) => {
13
18
  const rowId = getRowId(row);
14
19
  const isExpanded = expandedRowIds ? expandedRowIds.includes(rowId) : false;
15
- return ((0, jsx_runtime_1.jsxs)("li", { children: [(0, jsx_runtime_1.jsxs)("div", { "aria-label": onRowClick ? getRowLabel(row) : undefined, className: ps_design_system_1.PallasoftClassNameService.merge('flex gap-3 px-4 py-3.5 transition-colors', onRowClick ? 'ps-focus-ring cursor-pointer active:bg-mist/70' : ''), onClick: onRowClick ? () => onRowClick(row) : undefined, onKeyDown: onRowClick
20
+ return ((0, jsx_runtime_1.jsxs)("li", { children: [(0, jsx_runtime_1.jsxs)("div", { "aria-label": onRowClick ? getRowLabel(row) : undefined, className: ps_design_system_1.PallasoftClassNameService.merge('flex gap-3 px-4 py-3 transition-colors', onRowClick ? 'ps-focus-ring cursor-pointer active:bg-mist/70' : ''), onClick: onRowClick ? () => onRowClick(row) : undefined, onFocus: onRowHover ? () => onRowHover(row) : undefined, onPointerEnter: onRowHover ? () => onRowHover(row) : undefined, onKeyDown: onRowClick
16
21
  ? (event) => {
17
22
  if (event.target !== event.currentTarget)
18
23
  return;
@@ -21,12 +26,15 @@ const PsTableCardList = ({ columns, emptyContent, expandedRowIds, footerColumns,
21
26
  event.preventDefault();
22
27
  onRowClick(row);
23
28
  }
24
- : undefined, role: onRowClick ? 'button' : undefined, tabIndex: onRowClick ? 0 : undefined, children: [isSelectable ? ((0, jsx_runtime_1.jsx)("div", { className: 'pt-0.5', onClick: (event) => event.stopPropagation(), onKeyDown: (event) => event.stopPropagation(), children: (0, jsx_runtime_1.jsx)(ps_input_components_1.PsCheckbox, { isSelected: selectedIds.includes(rowId), label: `Select ${getRowLabel(row)}`, onChange: () => onToggleRow(rowId) }) })) : null, (0, jsx_runtime_1.jsxs)("div", { className: 'flex min-w-0 flex-1 flex-col gap-2', children: [(0, jsx_runtime_1.jsxs)("div", { className: 'flex items-start justify-between gap-3', children: [(0, jsx_runtime_1.jsx)("div", { className: 'min-w-0 text-sm font-semibold text-ink', children: primary ? primary.render(row) : null }), trailing ? ((0, jsx_runtime_1.jsx)("div", { className: 'shrink-0 text-right text-sm font-semibold tabular-nums text-ink', children: trailing.render(row) })) : null] }), rest.length > 0 ? ((0, jsx_runtime_1.jsx)("dl", { className: 'grid grid-cols-2 gap-x-4 gap-y-1.5', children: rest
29
+ : undefined, role: onRowClick ? 'button' : undefined, tabIndex: onRowClick ? 0 : undefined, children: [isSelectable ? ((0, jsx_runtime_1.jsx)("div", { className: 'pt-0.5', onClick: (event) => event.stopPropagation(), onKeyDown: (event) => event.stopPropagation(), children: (0, jsx_runtime_1.jsx)(ps_input_components_1.PsCheckbox, { isSelected: selectedIds.includes(rowId), label: `Select ${getRowLabel(row)}`, onChange: () => onToggleRow(rowId) }) })) : null, (0, jsx_runtime_1.jsxs)("div", { className: 'flex min-w-0 flex-1 flex-col gap-2', children: [(0, jsx_runtime_1.jsxs)("div", { className: 'flex items-start justify-between gap-3', children: [(0, jsx_runtime_1.jsxs)("div", { className: 'flex min-w-0 flex-col gap-0.5', children: [(0, jsx_runtime_1.jsx)("div", { className: 'min-w-0 text-sm font-semibold text-ink', children: primary ? primary.render(row) : null }), subtitles.map((column) => ((0, jsx_runtime_1.jsx)("div", { className: 'truncate text-sm text-ink/65', children: column.render(row) }, column.id)))] }), trailing ? ((0, jsx_runtime_1.jsx)("div", { className: 'shrink-0 text-right text-sm font-semibold tabular-nums text-ink', children: trailing.render(row) })) : null] }), rest.length > 0 ? ((0, jsx_runtime_1.jsx)("dl", { className: 'grid grid-cols-2 gap-x-4 gap-y-1.5', children: rest
25
30
  .map((column) => ({ column, value: column.render(row) }))
26
- .filter(({ value }) => value !== null && value !== undefined && value !== '')
31
+ .filter(({ value }) => hasValue(value))
27
32
  .map(({ column, value }) => ((0, jsx_runtime_1.jsxs)("div", { className: column.cardFullWidth ? 'col-span-2 min-w-0' : 'min-w-0', children: [(0, jsx_runtime_1.jsx)("dt", { className: 'text-[0.68rem] font-semibold uppercase tracking-[0.12em] text-ink/40', children: column.label }), (0, jsx_runtime_1.jsx)("dd", { className: column.cardFullWidth
28
33
  ? 'text-sm text-ink/80'
29
- : 'truncate text-sm text-ink/80', children: value })] }, column.id))) })) : null] })] }), renderExpandedRow ? ((0, jsx_runtime_1.jsx)(ps_motion_components_1.PsCollapse, { isOpen: isExpanded, children: (0, jsx_runtime_1.jsx)("div", { className: 'border-t border-line bg-mist/40 px-4 py-4', children: renderExpandedRow(row) }) })) : null] }, rowId));
34
+ : 'truncate text-sm text-ink/80', children: value })] }, column.id))) })) : null, metas.length > 0 ? ((0, jsx_runtime_1.jsx)("div", { className: 'flex flex-wrap items-center gap-x-4 gap-y-1.5 text-xs text-ink/70', children: metas
35
+ .map((column) => ({ column, value: column.render(row) }))
36
+ .filter(({ value }) => hasValue(value))
37
+ .map(({ column, value }) => ((0, jsx_runtime_1.jsxs)("div", { className: 'flex min-w-0 items-center gap-1.5', children: [(0, jsx_runtime_1.jsx)("span", { className: 'text-ink/40', children: column.label }), (0, jsx_runtime_1.jsx)("span", { className: 'min-w-0 truncate', children: value })] }, column.id))) })) : null] })] }), renderExpandedRow ? ((0, jsx_runtime_1.jsx)(ps_motion_components_1.PsCollapse, { isOpen: isExpanded, children: (0, jsx_runtime_1.jsx)("div", { className: 'border-t border-line bg-mist/40 px-4 py-4', children: renderExpandedRow(row) }) })) : null] }, rowId));
30
38
  }), footerColumns.length > 0 ? ((0, jsx_runtime_1.jsx)("li", { className: 'grid grid-cols-2 gap-x-4 gap-y-1.5 bg-mist/50 px-4 py-3', children: footerColumns.map((column) => ((0, jsx_runtime_1.jsxs)("div", { className: 'min-w-0', children: [(0, jsx_runtime_1.jsx)("p", { className: 'text-[0.68rem] font-semibold uppercase tracking-[0.12em] text-ink/40', children: column.label }), (0, jsx_runtime_1.jsx)("p", { className: 'truncate text-sm font-semibold text-ink', children: column.footer })] }, column.id))) })) : null] }));
31
39
  };
32
40
  exports.PsTableCardList = PsTableCardList;
@@ -0,0 +1,9 @@
1
+ import type { ReactNode } from 'react';
2
+ export interface PsTableGroupHeaderProps {
3
+ count: number;
4
+ isExpanded: boolean;
5
+ label: string;
6
+ onToggle: () => void;
7
+ summary?: ReactNode;
8
+ }
9
+ export declare const PsTableGroupHeader: ({ count, isExpanded, label, onToggle, summary, }: PsTableGroupHeaderProps) => import("react").JSX.Element;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PsTableGroupHeader = 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_icons_1 = require("@pallasoft/ps-icons");
7
+ const PsTableGroupHeader = ({ count, isExpanded, label, onToggle, summary, }) => ((0, jsx_runtime_1.jsxs)("button", { "aria-expanded": isExpanded, className: 'ps-focus-ring flex w-full flex-wrap items-center gap-x-3 gap-y-2 bg-mist/60 px-4 py-3 text-left transition-colors hover:bg-mist', onClick: onToggle, type: 'button', children: [(0, jsx_runtime_1.jsx)(ps_icons_1.PsIcon, { "aria-hidden": 'true', className: ps_design_system_1.PallasoftClassNameService.merge('h-4 w-4 shrink-0 text-ink/50 transition-transform duration-200', isExpanded ? '' : '-rotate-90'), iconKey: ps_icons_1.PsIconKey.Chevron }), (0, jsx_runtime_1.jsx)("span", { className: 'text-sm font-semibold text-ink', children: label }), (0, jsx_runtime_1.jsx)("span", { className: 'rounded-full bg-surface px-2 py-0.5 text-xs font-semibold tabular-nums text-ink/60', children: count }), summary ? ((0, jsx_runtime_1.jsx)("span", { className: 'ml-auto flex flex-wrap items-center justify-end gap-2', children: summary })) : null] }));
8
+ exports.PsTableGroupHeader = PsTableGroupHeader;
@@ -13,6 +13,6 @@ const PsTableToolbar = ({ actions, columns, filters, isSearchable, onClearFilter
13
13
  const filterableColumns = columns.filter((column) => column.filter);
14
14
  const zIndex = (0, ps_core_components_1.usePsLayerZIndex)(ps_design_system_1.PallasoftZIndex.Dropdown);
15
15
  const activeCount = filterableColumns.filter((column) => ps_table_query_service_1.PsTableQueryService.isActive(filters[column.id])).length;
16
- return ((0, jsx_runtime_1.jsxs)("div", { className: 'flex flex-wrap items-center gap-2', children: [isSearchable ? ((0, jsx_runtime_1.jsxs)("label", { className: 'relative basis-full sm:min-w-[12rem] sm:flex-1 sm:basis-auto sm:max-w-xs', children: [(0, jsx_runtime_1.jsx)("span", { className: 'sr-only', children: searchPlaceholder }), (0, jsx_runtime_1.jsx)(ps_icons_1.PsIcon, { "aria-hidden": 'true', className: 'pointer-events-none absolute left-3.5 top-1/2 h-4 w-4 -translate-y-1/2 text-ink/45', iconKey: ps_icons_1.PsIconKey.Search }), (0, jsx_runtime_1.jsx)("input", { className: 'ps-input-control ps-focus-ring min-h-10 rounded-full pl-10 text-sm', onChange: (event) => onSearchChange(event.target.value), placeholder: searchPlaceholder, type: 'search', value: search })] })) : null, filterableColumns.length > 0 ? ((0, jsx_runtime_1.jsxs)(react_aria_components_1.DialogTrigger, { children: [(0, jsx_runtime_1.jsxs)(react_aria_components_1.Button, { className: toolbarButtonClassName, children: [(0, jsx_runtime_1.jsx)(ps_icons_1.PsIcon, { "aria-hidden": 'true', className: 'block h-4 w-4', iconKey: ps_icons_1.PsIconKey.Filter }), (0, jsx_runtime_1.jsx)("span", { className: 'sr-only sm:not-sr-only', children: "Filters" }), activeCount > 0 ? ((0, jsx_runtime_1.jsx)("span", { className: 'inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-primary px-1.5 text-xs font-semibold text-[color:var(--ps-color-primary-contrast)]', children: activeCount })) : null] }), (0, jsx_runtime_1.jsx)(react_aria_components_1.Popover, { className: 'ps-floating-panel ps-scrollbar max-h-[min(34rem,80vh)] w-[min(22rem,calc(100vw-2rem))] overflow-y-auto outline-none', offset: 6, placement: 'bottom start', style: { zIndex }, children: (0, jsx_runtime_1.jsx)(react_aria_components_1.Dialog, { "aria-label": 'Filters', className: 'flex flex-col gap-4 p-4 outline-none', children: (0, jsx_runtime_1.jsxs)(ps_core_components_1.PsLayer, { zIndex: zIndex, children: [filterableColumns.map((column) => ((0, jsx_runtime_1.jsx)(ps_table_filter_field_1.PsTableFilterField, { kind: column.filter ? column.filter.kind : 'text', label: column.label, onChange: (value) => onFilterChange(column.id, value), options: ps_table_query_service_1.PsTableQueryService.options(rows, column), value: filters[column.id] ? filters[column.id] : {} }, column.id))), (0, jsx_runtime_1.jsx)("div", { className: 'flex justify-end border-t border-line pt-3', children: (0, jsx_runtime_1.jsx)("button", { className: 'ps-focus-ring rounded-lg px-2 py-1 text-sm font-semibold text-ink/65 hover:text-ink disabled:opacity-40', disabled: activeCount === 0, onClick: onClearFilters, type: 'button', children: "Clear all" }) })] }) }) })] })) : null, activeCount > 0 ? ((0, jsx_runtime_1.jsx)("button", { className: ps_design_system_1.PallasoftClassNameService.merge('ps-focus-ring rounded-full px-3 py-2 text-sm font-semibold text-ink/65 hover:text-ink'), onClick: onClearFilters, type: 'button', children: "Clear filters" })) : null, actions ? ((0, jsx_runtime_1.jsx)("div", { className: 'ml-auto flex min-w-0 flex-wrap items-center justify-end gap-2', children: actions })) : null] }));
16
+ return ((0, jsx_runtime_1.jsxs)("div", { className: 'flex flex-wrap items-center gap-2', children: [isSearchable ? ((0, jsx_runtime_1.jsxs)("label", { className: 'relative basis-full sm:min-w-[12rem] sm:flex-1 sm:basis-auto sm:max-w-xs', children: [(0, jsx_runtime_1.jsx)("span", { className: 'sr-only', children: searchPlaceholder }), (0, jsx_runtime_1.jsx)(ps_icons_1.PsIcon, { "aria-hidden": 'true', className: 'pointer-events-none absolute left-3.5 top-1/2 h-4 w-4 -translate-y-1/2 text-ink/45', iconKey: ps_icons_1.PsIconKey.Search }), (0, jsx_runtime_1.jsx)("input", { className: 'ps-input-control ps-focus-ring min-h-10 rounded-full pl-10 text-sm', onChange: (event) => onSearchChange(event.target.value), placeholder: searchPlaceholder, type: 'search', value: search })] })) : null, filterableColumns.length > 0 ? ((0, jsx_runtime_1.jsxs)(react_aria_components_1.DialogTrigger, { children: [(0, jsx_runtime_1.jsxs)(react_aria_components_1.Button, { className: toolbarButtonClassName, children: [(0, jsx_runtime_1.jsx)(ps_icons_1.PsIcon, { "aria-hidden": 'true', className: 'block h-4 w-4', iconKey: ps_icons_1.PsIconKey.Filter }), (0, jsx_runtime_1.jsx)("span", { className: 'sr-only sm:not-sr-only', children: "Filters" }), activeCount > 0 ? ((0, jsx_runtime_1.jsx)("span", { className: 'inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-primary px-1.5 text-xs font-semibold text-[color:var(--ps-color-primary-contrast)]', children: activeCount })) : null] }), (0, jsx_runtime_1.jsx)(react_aria_components_1.Popover, { className: 'ps-floating-panel ps-scrollbar-hidden max-h-[min(34rem,80vh)] w-[min(22rem,calc(100vw-2rem))] overflow-y-auto outline-none', offset: 6, placement: 'bottom start', style: { zIndex }, children: (0, jsx_runtime_1.jsx)(react_aria_components_1.Dialog, { "aria-label": 'Filters', className: 'flex flex-col gap-4 p-4 outline-none', children: (0, jsx_runtime_1.jsxs)(ps_core_components_1.PsLayer, { zIndex: zIndex, children: [filterableColumns.map((column) => ((0, jsx_runtime_1.jsx)(ps_table_filter_field_1.PsTableFilterField, { kind: column.filter ? column.filter.kind : 'text', label: column.label, onChange: (value) => onFilterChange(column.id, value), options: ps_table_query_service_1.PsTableQueryService.options(rows, column), value: filters[column.id] ? filters[column.id] : {} }, column.id))), (0, jsx_runtime_1.jsx)("div", { className: 'flex justify-end border-t border-line pt-3', children: (0, jsx_runtime_1.jsx)("button", { className: 'ps-focus-ring rounded-lg px-2 py-1 text-sm font-semibold text-ink/65 hover:text-ink disabled:opacity-40', disabled: activeCount === 0, onClick: onClearFilters, type: 'button', children: "Clear all" }) })] }) }) })] })) : null, activeCount > 0 ? ((0, jsx_runtime_1.jsx)("button", { className: ps_design_system_1.PallasoftClassNameService.merge('ps-focus-ring rounded-full px-3 py-2 text-sm font-semibold text-ink/65 hover:text-ink'), onClick: onClearFilters, type: 'button', children: "Clear filters" })) : null, actions ? ((0, jsx_runtime_1.jsx)("div", { className: 'ml-auto flex min-w-0 flex-wrap items-center justify-end gap-2', children: actions })) : null] }));
17
17
  };
18
18
  exports.PsTableToolbar = PsTableToolbar;
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PsTableSearchConfig = void 0;
4
4
  exports.PsTableSearchConfig = {
5
5
  currencySymbols: /[$€£¥]/g,
6
- diacritics: /[̀-ͯ]/g,
6
+ diacritics: /[\u0300-\u036f]/g,
7
7
  thousandsSeparator: /(\d),(?=\d{3}\b)/g,
8
8
  whitespace: /\s+/g,
9
9
  };
@@ -0,0 +1,6 @@
1
+ export declare const PsTableCardRole: {
2
+ readonly detail: "detail";
3
+ readonly meta: "meta";
4
+ readonly subtitle: "subtitle";
5
+ };
6
+ export type PsTableCardRole = (typeof PsTableCardRole)[keyof typeof PsTableCardRole];
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PsTableCardRole = void 0;
4
+ exports.PsTableCardRole = {
5
+ detail: 'detail',
6
+ meta: 'meta',
7
+ subtitle: 'subtitle',
8
+ };
@@ -1,4 +1,7 @@
1
1
  export declare const PsTableConfig: {
2
+ readonly groupCloseMs: 160;
3
+ readonly groupRowStaggerMs: 16;
4
+ readonly groupStaggerLimit: 12;
2
5
  readonly noMatchesDescription: "Try a different search or clear the filters.";
3
6
  readonly noMatchesTitle: "Nothing matches";
4
7
  readonly searchPlaceholder: "Search";
@@ -2,6 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PsTableConfig = void 0;
4
4
  exports.PsTableConfig = {
5
+ groupCloseMs: 160,
6
+ groupRowStaggerMs: 16,
7
+ groupStaggerLimit: 12,
5
8
  noMatchesDescription: 'Try a different search or clear the filters.',
6
9
  noMatchesTitle: 'Nothing matches',
7
10
  searchPlaceholder: 'Search',
@@ -0,0 +1,199 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Fragment, useMemo, useRef, useState } from 'react';
3
+ import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
4
+ import { PsCheckbox } from '@pallasoft/ps-input-components';
5
+ import { PsCollapse } from '@pallasoft/ps-motion-components';
6
+ import { PsTableConfig } from '../constants/table-config';
7
+ import { PsTableGroupService } from '../services/ps-table-group-service';
8
+ import { PsTableQueryService } from '../services/ps-table-query-service';
9
+ import { PsTableBulkBar } from './ps-table-bulk-bar';
10
+ import { PsTableCardList } from './ps-table-card-list';
11
+ import { PsTableColumnPicker } from './ps-table-column-picker';
12
+ import { PsTableGroupHeader } from './ps-table-group-header';
13
+ import { PsTablePagination } from './ps-table-pagination';
14
+ import { PsTableSortHeader } from './ps-table-sort-header';
15
+ import { PsTableToolbar } from './ps-table-toolbar';
16
+ const densityClassNames = {
17
+ compact: {
18
+ cell: 'px-3 py-1.5 text-[0.8125rem] leading-5',
19
+ head: 'px-3 py-2',
20
+ },
21
+ normal: {
22
+ cell: 'px-5 py-4',
23
+ head: 'px-5 py-3.5',
24
+ },
25
+ };
26
+ const columnStyle = (column) => column.width || column.minWidth ? { minWidth: column.minWidth, width: column.width } : undefined;
27
+ const alignClassNames = {
28
+ center: 'text-center',
29
+ left: 'text-left',
30
+ right: 'text-right tabular-nums',
31
+ };
32
+ export const PsDataTable = ({ caption, className, columns, defaultSort, density = 'normal', expandedRowIds, mobileLayout = 'cards', getBulkActions, getRowId, getRowLabel, getSearchText, grouping, onPageChange, onQueryChange, onRowClick, onRowHover, page, pageSize, query, renderExpandedRow, rows, searchPlaceholder = PsTableConfig.searchPlaceholder, toolbarActions, totalRows, }) => {
33
+ const containerRef = useRef(null);
34
+ const [uncontrolledPage, setUncontrolledPage] = useState(1);
35
+ const [uncontrolledQuery, setUncontrolledQuery] = useState(() => PsTableQueryService.empty(defaultSort ? defaultSort : null));
36
+ const [selectedIds, setSelectedIds] = useState([]);
37
+ const [toggledGroupKeys, setToggledGroupKeys] = useState([]);
38
+ const [closingGroupKeys, setClosingGroupKeys] = useState([]);
39
+ const [openedGroupKeys, setOpenedGroupKeys] = useState([]);
40
+ const currentQuery = query ? query : uncontrolledQuery;
41
+ const { filters, hiddenColumnIds, search, sort } = currentQuery;
42
+ const visibleColumns = columns.filter((column) => !hiddenColumnIds.includes(column.id));
43
+ const updateQuery = (patch) => {
44
+ const next = { ...currentQuery, ...patch };
45
+ if (!query)
46
+ setUncontrolledQuery(next);
47
+ onQueryChange?.(next);
48
+ };
49
+ const isServerPaginated = totalRows !== undefined;
50
+ const hasFooter = columns.some((column) => column.footer !== undefined);
51
+ const isSearchable = Boolean(getSearchText) && !isServerPaginated;
52
+ const hasFilters = columns.some((column) => column.filter) && !isServerPaginated;
53
+ const hasToolbar = isSearchable || hasFilters || Boolean(toolbarActions);
54
+ const isSelectable = Boolean(getBulkActions);
55
+ const columnCount = visibleColumns.length + (isSelectable ? 1 : 0);
56
+ const queriedRows = useMemo(() => isServerPaginated
57
+ ? rows
58
+ : PsTableQueryService.apply(rows, columns, filters, search, getSearchText, sort), [columns, filters, getSearchText, isServerPaginated, rows, search, sort]);
59
+ const currentPage = page !== null && page !== undefined ? page : uncontrolledPage;
60
+ const rowCount = isServerPaginated ? totalRows : queriedRows.length;
61
+ const pageCount = pageSize ? Math.max(Math.ceil(rowCount / pageSize), 1) : 1;
62
+ const safePage = Math.min(Math.max(currentPage, 1), pageCount);
63
+ const isQueried = search.trim() !== '' ||
64
+ Object.values(filters).some((value) => PsTableQueryService.isActive(value));
65
+ const groups = useMemo(() => (grouping ? PsTableGroupService.group(queriedRows, grouping.getGroupKey) : []), [grouping, queriedRows]);
66
+ const initialExpandedCount = grouping && grouping.initialExpandedCount !== undefined ? grouping.initialExpandedCount : 1;
67
+ const isGroupExpanded = (groupKey, groupIndex) => isQueried || groupIndex < initialExpandedCount !== toggledGroupKeys.includes(groupKey);
68
+ const flipGroup = (groupKey) => setToggledGroupKeys((current) => current.includes(groupKey)
69
+ ? current.filter((key) => key !== groupKey)
70
+ : [...current, groupKey]);
71
+ const toggleGroup = (groupKey, isExpanded) => {
72
+ if (closingGroupKeys.includes(groupKey))
73
+ return;
74
+ if (!isExpanded) {
75
+ setOpenedGroupKeys((current) => [...current.filter((key) => key !== groupKey), groupKey]);
76
+ flipGroup(groupKey);
77
+ return;
78
+ }
79
+ setClosingGroupKeys((current) => [...current, groupKey]);
80
+ window.setTimeout(() => {
81
+ flipGroup(groupKey);
82
+ setOpenedGroupKeys((current) => current.filter((key) => key !== groupKey));
83
+ setClosingGroupKeys((current) => current.filter((key) => key !== groupKey));
84
+ }, PsTableConfig.groupCloseMs);
85
+ };
86
+ const groupRowMotion = (groupKey, rowIndex) => {
87
+ if (closingGroupKeys.includes(groupKey))
88
+ return { className: 'ps-table-row-exit' };
89
+ if (!openedGroupKeys.includes(groupKey))
90
+ return undefined;
91
+ return {
92
+ className: 'ps-table-row-enter',
93
+ delayMs: Math.min(rowIndex, PsTableConfig.groupStaggerLimit) * PsTableConfig.groupRowStaggerMs,
94
+ };
95
+ };
96
+ const visibleRows = useMemo(() => {
97
+ if (grouping)
98
+ return groups.flatMap((group, groupIndex) => isQueried || groupIndex < initialExpandedCount !== toggledGroupKeys.includes(group.key)
99
+ ? group.rows
100
+ : []);
101
+ if (!pageSize || isServerPaginated)
102
+ return queriedRows;
103
+ return queriedRows.slice((safePage - 1) * pageSize, safePage * pageSize);
104
+ }, [
105
+ grouping,
106
+ groups,
107
+ initialExpandedCount,
108
+ isQueried,
109
+ isServerPaginated,
110
+ pageSize,
111
+ queriedRows,
112
+ safePage,
113
+ toggledGroupKeys,
114
+ ]);
115
+ const changePage = (next) => {
116
+ if (onPageChange) {
117
+ onPageChange(next);
118
+ return;
119
+ }
120
+ containerRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
121
+ setUncontrolledPage(next);
122
+ };
123
+ const resetPage = () => {
124
+ if (safePage === 1)
125
+ return;
126
+ if (onPageChange)
127
+ onPageChange(1);
128
+ else
129
+ setUncontrolledPage(1);
130
+ };
131
+ const updateSort = (columnId) => {
132
+ updateQuery({ sort: PsTableQueryService.nextSort(sort, columnId) });
133
+ resetPage();
134
+ };
135
+ const clearFilters = () => {
136
+ updateQuery({ filters: {}, search: '' });
137
+ resetPage();
138
+ };
139
+ const selectedRows = queriedRows.filter((row) => selectedIds.includes(getRowId(row)));
140
+ const pageIds = visibleRows.map((row) => getRowId(row));
141
+ const isPageSelected = pageIds.length > 0 && pageIds.every((rowId) => selectedIds.includes(rowId));
142
+ const togglePage = () => setSelectedIds((current) => isPageSelected
143
+ ? current.filter((rowId) => !pageIds.includes(rowId))
144
+ : [...new Set([...current, ...pageIds])]);
145
+ const toggleRow = (rowId) => setSelectedIds((current) => current.includes(rowId) ? current.filter((id) => id !== rowId) : [...current, rowId]);
146
+ const renderCardList = (cardRows, emptyContent) => (_jsx(PsTableCardList, { columns: visibleColumns, emptyContent: emptyContent, expandedRowIds: expandedRowIds, footerColumns: hasFooter ? visibleColumns.filter((column) => column.footer !== undefined) : [], getRowId: getRowId, getRowLabel: (row) => (getRowLabel ? getRowLabel(row) : getRowId(row)), isSelectable: isSelectable, onRowClick: onRowClick, onRowHover: onRowHover, onToggleRow: toggleRow, renderExpandedRow: renderExpandedRow, rows: cardRows, selectedIds: selectedIds }));
147
+ const renderRow = (row, motion) => {
148
+ const rowId = getRowId(row);
149
+ const isExpanded = expandedRowIds ? expandedRowIds.includes(rowId) : false;
150
+ const rowLabel = getRowLabel ? getRowLabel(row) : rowId;
151
+ return (_jsxs(Fragment, { children: [_jsxs("tr", { "aria-expanded": renderExpandedRow ? isExpanded : undefined, "aria-label": onRowClick ? rowLabel : undefined, className: PallasoftClassNameService.merge('align-middle transition-colors', motion ? motion.className : '', onRowClick ? 'ps-focus-ring cursor-pointer hover:bg-mist/60' : 'hover:bg-mist/40', isExpanded ? 'bg-mist/40' : ''), onClick: onRowClick ? () => onRowClick(row) : undefined, onFocus: onRowHover ? () => onRowHover(row) : undefined, onPointerEnter: onRowHover ? () => onRowHover(row) : undefined, onKeyDown: onRowClick
152
+ ? (event) => {
153
+ if (event.target !== event.currentTarget)
154
+ return;
155
+ if (event.key !== 'Enter' && event.key !== ' ')
156
+ return;
157
+ event.preventDefault();
158
+ onRowClick(row);
159
+ }
160
+ : undefined, style: motion && motion.delayMs !== undefined
161
+ ? { animationDelay: `${motion.delayMs}ms` }
162
+ : undefined, tabIndex: onRowClick ? 0 : undefined, children: [isSelectable ? (_jsx("td", { className: 'w-10 pl-4 pr-0', onClick: (event) => event.stopPropagation(), onKeyDown: (event) => event.stopPropagation(), children: _jsx(PsCheckbox, { isSelected: selectedIds.includes(rowId), label: `Select ${rowLabel}`, onChange: () => toggleRow(rowId) }) })) : null, visibleColumns.map((column) => (_jsx("td", { className: PallasoftClassNameService.merge(densityClassNames[density].cell, alignClassNames[column.align !== null && column.align !== undefined ? column.align : 'left'], column.wrap ? '' : 'whitespace-nowrap', 'text-sm text-ink'), style: columnStyle(column), children: column.render(row) }, column.id)))] }), renderExpandedRow ? (_jsx("tr", { "aria-hidden": !isExpanded, className: isExpanded ? 'border-t-0' : 'border-0', children: _jsx("td", { className: 'p-0', colSpan: columnCount, children: _jsx(PsCollapse, { isOpen: isExpanded, children: _jsx("div", { className: 'w-0 min-w-full border-l-2 border-primary bg-mist/40 px-4 py-4', children: renderExpandedRow(row) }) }) }) })) : null] }, rowId));
163
+ };
164
+ 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
165
+ .filter((column) => column.canHide)
166
+ .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) => {
167
+ updateQuery({ filters: { ...filters, [columnId]: value } });
168
+ resetPage();
169
+ }, onSearchChange: (next) => {
170
+ updateQuery({ search: next });
171
+ resetPage();
172
+ }, rows: rows, search: search, searchPlaceholder: searchPlaceholder })) : null, _jsxs("div", { className: 'min-w-0 overflow-hidden rounded-[1.25rem] border border-line bg-surface', children: [mobileLayout === 'cards' ? (_jsx("div", { className: 'sm:hidden', children: grouping
173
+ ? groups.map((group, groupIndex) => {
174
+ const isExpanded = isGroupExpanded(group.key, groupIndex);
175
+ 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
176
+ ? grouping.renderGroupSummary(group.rows)
177
+ : undefined }), _jsx(PsCollapse, { isOpen: isExpanded && !closingGroupKeys.includes(group.key), children: renderCardList(group.rows, null) })] }, `group-${group.key}`));
178
+ })
179
+ : renderCardList(visibleRows, 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: 'ps-focus-ring mt-2 rounded-full px-3 py-1.5 text-sm font-semibold text-primary', onClick: clearFilters, type: 'button', children: "Clear search and filters" })] })) : null) })) : null, _jsx("div", { className: PallasoftClassNameService.merge('ps-scrollbar overflow-x-auto', mobileLayout === 'cards' ? 'hidden sm:block' : ''), children: _jsxs("table", { className: 'w-full border-collapse text-left', 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: [isSelectable ? (_jsx("th", { className: 'w-10 pl-4 pr-0', scope: 'col', children: _jsx(PsCheckbox, { isIndeterminate: !isPageSelected && pageIds.some((rowId) => selectedIds.includes(rowId)), isSelected: isPageSelected, label: 'Select all rows on this page', onChange: togglePage }) })) : null, visibleColumns.map((column) => {
180
+ const align = column.align !== null && column.align !== undefined ? column.align : 'left';
181
+ const isSorted = sort !== null && sort.columnId === column.id;
182
+ return (_jsx("th", { "aria-sort": isSorted
183
+ ? sort.direction
184
+ : column.sortValue && !isServerPaginated
185
+ ? 'none'
186
+ : undefined, className: PallasoftClassNameService.merge(densityClassNames[density].head, alignClassNames[align], 'whitespace-nowrap'), scope: 'col', style: columnStyle(column), children: column.sortValue && !isServerPaginated ? (_jsx(PsTableSortHeader, { align: align, columnId: column.id, label: column.label, onSort: updateSort, sort: sort })) : (column.label) }, column.id));
187
+ })] }) }), _jsxs("tbody", { className: 'divide-y divide-line', children: [visibleRows.length === 0 && isQueried ? (_jsx("tr", { children: _jsxs("td", { className: 'px-4 py-12 text-center', colSpan: columnCount, children: [_jsx("p", { className: 'text-sm font-semibold text-ink', children: PsTableConfig.noMatchesTitle }), _jsx("p", { className: 'mt-1 text-sm text-ink/55', children: PsTableConfig.noMatchesDescription }), _jsx("button", { className: 'ps-focus-ring mt-3 rounded-full px-3 py-1.5 text-sm font-semibold text-primary hover:underline', onClick: clearFilters, type: 'button', children: "Clear search and filters" })] }) })) : null, grouping
188
+ ? groups.map((group, groupIndex) => {
189
+ const isExpanded = isGroupExpanded(group.key, groupIndex);
190
+ return (_jsxs(Fragment, { children: [_jsx("tr", { children: _jsx("td", { className: 'p-0', colSpan: columnCount, children: _jsx(PsTableGroupHeader, { count: group.rows.length, isExpanded: isExpanded, label: grouping.getGroupLabel(group.key, group.rows), onToggle: () => toggleGroup(group.key, isExpanded), summary: grouping.renderGroupSummary
191
+ ? grouping.renderGroupSummary(group.rows)
192
+ : undefined }) }) }), isExpanded
193
+ ? group.rows.map((row, rowIndex) => renderRow(row, groupRowMotion(group.key, rowIndex)))
194
+ : null] }, `group-${group.key}`));
195
+ })
196
+ : visibleRows.map((row) => renderRow(row))] }), hasFooter ? (_jsx("tfoot", { className: 'border-t-2 border-line bg-mist/50 text-sm font-semibold text-ink', children: _jsxs("tr", { children: [isSelectable ? _jsx("td", {}) : null, visibleColumns.map((column) => (_jsx("td", { className: PallasoftClassNameService.merge(densityClassNames[density].cell, alignClassNames[column.align !== null && column.align !== undefined
197
+ ? column.align
198
+ : 'left'], 'whitespace-nowrap'), children: column.footer }, column.id)))] }) })) : null] }) }), pageSize && !grouping ? (_jsx(PsTablePagination, { onPageChange: changePage, page: safePage, pageCount: pageCount, pageSize: pageSize, totalRows: rowCount })) : null] })] }));
199
+ };
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
3
+ import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
4
+ export const PsTableBulkBar = ({ actions, onClear, selectedCount }) => {
5
+ const safeActions = actions.filter((action) => !action.isDanger);
6
+ const dangerActions = actions.filter((action) => action.isDanger);
7
+ const renderAction = (action) => (_jsx("button", { className: PallasoftClassNameService.merge('ps-focus-ring inline-flex min-h-9 shrink-0 items-center whitespace-nowrap rounded-full px-3 text-sm font-semibold transition-colors disabled:opacity-40', action.isDanger ? 'text-danger hover:bg-danger/10' : 'text-ink hover:bg-ink/5'), disabled: action.isDisabled, onClick: action.onSelect, type: 'button', children: action.label }, action.id));
8
+ return (_jsxs("div", { "aria-live": 'polite', className: 'flex min-h-12 items-center gap-2 rounded-2xl border border-primary/30 bg-primary/10 py-1.5 pl-2 pr-1.5', children: [_jsxs("span", { className: 'inline-flex min-h-9 shrink-0 items-center rounded-full bg-primary px-3 text-sm font-semibold tabular-nums text-white', children: [selectedCount, " selected"] }), _jsxs("div", { className: 'ps-scrollbar-hidden flex min-w-0 flex-1 items-center gap-1 overflow-x-auto', children: [safeActions.map(renderAction), dangerActions.length > 0 && safeActions.length > 0 ? (_jsx("span", { "aria-hidden": 'true', className: 'mx-1 h-5 w-px shrink-0 bg-line' })) : null, dangerActions.map(renderAction)] }), _jsx("button", { "aria-label": 'Clear selection', className: 'ps-focus-ring inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-ink/60 transition-colors hover:bg-ink/5 hover:text-ink', onClick: onClear, title: 'Clear selection', type: 'button', children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'h-4 w-4', iconKey: PsIconKey.Close }) })] }));
9
+ };
@@ -0,0 +1,36 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
3
+ import { PsCheckbox } from '@pallasoft/ps-input-components';
4
+ import { PsCollapse } from '@pallasoft/ps-motion-components';
5
+ import { PsTableCardRole } from '../constants/table-card-role';
6
+ export const PsTableCardList = ({ columns, emptyContent, expandedRowIds, footerColumns, getRowId, getRowLabel, isSelectable, onRowClick, onRowHover, onToggleRow, renderExpandedRow, rows, selectedIds, }) => {
7
+ const [primary, ...details] = columns.filter((column) => !column.hideOnCards);
8
+ const trailing = details.find((column) => column.align === 'right');
9
+ const others = details.filter((column) => column !== trailing);
10
+ const subtitles = others.filter((column) => column.cardRole === PsTableCardRole.subtitle);
11
+ const metas = others.filter((column) => column.cardRole === PsTableCardRole.meta);
12
+ const rest = others.filter((column) => column.cardRole === undefined || column.cardRole === PsTableCardRole.detail);
13
+ const hasValue = (value) => value !== null && value !== undefined && value !== '';
14
+ return (_jsxs("ul", { className: 'divide-y divide-line', children: [rows.length === 0 ? _jsx("li", { children: emptyContent }) : null, rows.map((row) => {
15
+ const rowId = getRowId(row);
16
+ const isExpanded = expandedRowIds ? expandedRowIds.includes(rowId) : false;
17
+ return (_jsxs("li", { children: [_jsxs("div", { "aria-label": onRowClick ? getRowLabel(row) : undefined, className: PallasoftClassNameService.merge('flex gap-3 px-4 py-3 transition-colors', onRowClick ? 'ps-focus-ring cursor-pointer active:bg-mist/70' : ''), onClick: onRowClick ? () => onRowClick(row) : undefined, onFocus: onRowHover ? () => onRowHover(row) : undefined, onPointerEnter: onRowHover ? () => onRowHover(row) : undefined, onKeyDown: onRowClick
18
+ ? (event) => {
19
+ if (event.target !== event.currentTarget)
20
+ return;
21
+ if (event.key !== 'Enter' && event.key !== ' ')
22
+ return;
23
+ event.preventDefault();
24
+ onRowClick(row);
25
+ }
26
+ : undefined, role: onRowClick ? 'button' : undefined, tabIndex: onRowClick ? 0 : undefined, children: [isSelectable ? (_jsx("div", { className: 'pt-0.5', onClick: (event) => event.stopPropagation(), onKeyDown: (event) => event.stopPropagation(), children: _jsx(PsCheckbox, { isSelected: selectedIds.includes(rowId), label: `Select ${getRowLabel(row)}`, onChange: () => onToggleRow(rowId) }) })) : null, _jsxs("div", { className: 'flex min-w-0 flex-1 flex-col gap-2', children: [_jsxs("div", { className: 'flex items-start justify-between gap-3', children: [_jsxs("div", { className: 'flex min-w-0 flex-col gap-0.5', children: [_jsx("div", { className: 'min-w-0 text-sm font-semibold text-ink', children: primary ? primary.render(row) : null }), subtitles.map((column) => (_jsx("div", { className: 'truncate text-sm text-ink/65', children: column.render(row) }, column.id)))] }), trailing ? (_jsx("div", { className: 'shrink-0 text-right text-sm font-semibold tabular-nums text-ink', children: trailing.render(row) })) : null] }), rest.length > 0 ? (_jsx("dl", { className: 'grid grid-cols-2 gap-x-4 gap-y-1.5', children: rest
27
+ .map((column) => ({ column, value: column.render(row) }))
28
+ .filter(({ value }) => hasValue(value))
29
+ .map(({ column, value }) => (_jsxs("div", { className: column.cardFullWidth ? 'col-span-2 min-w-0' : 'min-w-0', children: [_jsx("dt", { className: 'text-[0.68rem] font-semibold uppercase tracking-[0.12em] text-ink/40', children: column.label }), _jsx("dd", { className: column.cardFullWidth
30
+ ? 'text-sm text-ink/80'
31
+ : 'truncate text-sm text-ink/80', children: value })] }, column.id))) })) : null, metas.length > 0 ? (_jsx("div", { className: 'flex flex-wrap items-center gap-x-4 gap-y-1.5 text-xs text-ink/70', children: metas
32
+ .map((column) => ({ column, value: column.render(row) }))
33
+ .filter(({ value }) => hasValue(value))
34
+ .map(({ column, value }) => (_jsxs("div", { className: 'flex min-w-0 items-center gap-1.5', children: [_jsx("span", { className: 'text-ink/40', children: column.label }), _jsx("span", { className: 'min-w-0 truncate', children: value })] }, column.id))) })) : null] })] }), renderExpandedRow ? (_jsx(PsCollapse, { isOpen: isExpanded, children: _jsx("div", { className: 'border-t border-line bg-mist/40 px-4 py-4', children: renderExpandedRow(row) }) })) : null] }, rowId));
35
+ }), footerColumns.length > 0 ? (_jsx("li", { className: 'grid grid-cols-2 gap-x-4 gap-y-1.5 bg-mist/50 px-4 py-3', children: footerColumns.map((column) => (_jsxs("div", { className: 'min-w-0', children: [_jsx("p", { className: 'text-[0.68rem] font-semibold uppercase tracking-[0.12em] text-ink/40', children: column.label }), _jsx("p", { className: 'truncate text-sm font-semibold text-ink', children: column.footer })] }, column.id))) })) : null] }));
36
+ };
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PsLayer, usePsLayerZIndex } from '@pallasoft/ps-core-components';
3
+ import { PallasoftZIndex } from '@pallasoft/ps-design-system';
4
+ import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
5
+ import { PsCheckbox } from '@pallasoft/ps-input-components';
6
+ import { Button, Dialog, DialogTrigger, Popover } from 'react-aria-components';
7
+ export const PsTableColumnPicker = ({ columns, hiddenColumnIds, onChange, }) => {
8
+ const zIndex = usePsLayerZIndex(PallasoftZIndex.Dropdown);
9
+ const visibleCount = columns.filter((column) => !hiddenColumnIds.includes(column.id)).length;
10
+ return (_jsxs(DialogTrigger, { children: [_jsxs(Button, { className: 'ps-focus-ring ps-liquid ps-liquid-control inline-flex min-h-10 min-w-10 items-center justify-center gap-2 rounded-full px-3 text-sm font-semibold text-ink outline-none transition-colors hover:border-primary/40 sm:px-4', children: [_jsx(PsIcon, { "aria-hidden": 'true', className: 'block h-4 w-4 sm:hidden', iconKey: PsIconKey.Columns }), _jsx("span", { className: 'sr-only sm:not-sr-only', children: "Columns" }), _jsx(PsIcon, { "aria-hidden": 'true', className: 'hidden h-4 w-4 sm:block', iconKey: PsIconKey.Chevron })] }), _jsx(Popover, { className: 'ps-floating-panel w-60 outline-none', offset: 6, placement: 'bottom end', style: { zIndex }, children: _jsx(Dialog, { "aria-label": 'Columns', className: 'flex flex-col gap-0.5 p-1.5 outline-none', children: _jsx(PsLayer, { zIndex: zIndex, children: columns.map((column) => {
11
+ const isVisible = !hiddenColumnIds.includes(column.id);
12
+ return (_jsx(PsCheckbox, { className: 'ps-floating-option w-full', isDisabled: isVisible && visibleCount <= 1, isSelected: isVisible, label: column.label, onChange: () => onChange(isVisible
13
+ ? [...hiddenColumnIds, column.id]
14
+ : hiddenColumnIds.filter((hiddenId) => hiddenId !== column.id)), children: column.label }, column.id));
15
+ }) }) }) })] }));
16
+ };
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PsDateInput, PsMultiSelectInput, PsTextInput } from '@pallasoft/ps-input-components';
3
+ import { PsTableFilterKind } from '../constants/table-filter-kind';
4
+ export const PsTableFilterField = ({ kind, label, onChange, options, value, }) => {
5
+ if (kind === PsTableFilterKind.select)
6
+ return (_jsx(PsMultiSelectInput, { label: label, onChange: (values) => onChange({ values }), options: options, placeholder: 'Any', value: value.values ? value.values : [] }));
7
+ if (kind === PsTableFilterKind.text)
8
+ return (_jsx(PsTextInput, { label: label, onChange: (event) => onChange({ text: event.target.value }), placeholder: 'Contains', value: value.text ? value.text : '' }));
9
+ if (kind === PsTableFilterKind.number)
10
+ return (_jsxs("fieldset", { className: 'grid grid-cols-2 gap-2', children: [_jsx("legend", { className: 'ps-input-label col-span-2 mb-2', children: label }), _jsx(PsTextInput, { hideLabel: true, inputMode: 'decimal', label: `${label} minimum`, onChange: (event) => onChange({ ...value, from: event.target.value }), placeholder: 'Min', value: value.from ? value.from : '' }), _jsx(PsTextInput, { hideLabel: true, inputMode: 'decimal', label: `${label} maximum`, onChange: (event) => onChange({ ...value, to: event.target.value }), placeholder: 'Max', value: value.to ? value.to : '' })] }));
11
+ return (_jsxs("fieldset", { className: 'grid grid-cols-2 gap-2', children: [_jsx("legend", { className: 'ps-input-label col-span-2 mb-2', children: label }), _jsx(PsDateInput, { hideLabel: true, label: `${label} from`, onChange: (from) => onChange({ ...value, from }), placeholder: 'From', value: value.from ? value.from : '' }), _jsx(PsDateInput, { hideLabel: true, label: `${label} to`, onChange: (to) => onChange({ ...value, to }), placeholder: 'To', value: value.to ? value.to : '' })] }));
12
+ };
@@ -0,0 +1,4 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
3
+ import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
4
+ export const PsTableGroupHeader = ({ count, isExpanded, label, onToggle, summary, }) => (_jsxs("button", { "aria-expanded": isExpanded, className: 'ps-focus-ring flex w-full flex-wrap items-center gap-x-3 gap-y-2 bg-mist/60 px-4 py-3 text-left transition-colors hover:bg-mist', onClick: onToggle, type: 'button', children: [_jsx(PsIcon, { "aria-hidden": 'true', className: PallasoftClassNameService.merge('h-4 w-4 shrink-0 text-ink/50 transition-transform duration-200', isExpanded ? '' : '-rotate-90'), iconKey: PsIconKey.Chevron }), _jsx("span", { className: 'text-sm font-semibold text-ink', children: label }), _jsx("span", { className: 'rounded-full bg-surface px-2 py-0.5 text-xs font-semibold tabular-nums text-ink/60', children: count }), summary ? (_jsx("span", { className: 'ml-auto flex flex-wrap items-center justify-end gap-2', children: summary })) : null] }));
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
3
+ export const PsTablePagination = ({ className, onPageChange, page, pageCount, pageSize, totalRows, }) => {
4
+ const firstRow = totalRows === 0 ? 0 : (page - 1) * pageSize + 1;
5
+ const lastRow = Math.min(page * pageSize, totalRows);
6
+ const buttonClassName = 'ps-focus-ring inline-flex min-h-9 items-center rounded-full border border-line px-3.5 text-sm font-semibold text-ink transition-colors disabled:cursor-not-allowed disabled:opacity-40 enabled:hover:border-primary/40';
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 ? 'No rows' : `${firstRow}–${lastRow} of ${totalRows}` }), _jsxs("div", { className: 'flex items-center gap-2', children: [_jsx("button", { className: buttonClassName, 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: buttonClassName, disabled: page >= pageCount, onClick: () => onPageChange(page + 1), type: 'button', children: "Next" })] })] }));
8
+ };
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
3
+ import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
4
+ export const PsTableSortHeader = ({ align, columnId, label, onSort, sort, }) => {
5
+ const isSorted = sort !== null && sort.columnId === columnId;
6
+ const iconKey = !isSorted
7
+ ? PsIconKey.Sort
8
+ : sort.direction === 'ascending'
9
+ ? PsIconKey.SortAscending
10
+ : PsIconKey.SortDescending;
11
+ return (_jsxs("button", { className: PallasoftClassNameService.merge('ps-focus-ring group inline-flex items-center gap-1 rounded-md uppercase tracking-[0.18em] transition-colors hover:text-ink', align === 'right' ? 'flex-row-reverse' : '', isSorted ? 'text-ink' : ''), onClick: () => onSort(columnId), type: 'button', children: [_jsx("span", { children: label }), _jsx(PsIcon, { "aria-hidden": 'true', className: PallasoftClassNameService.merge('block h-3.5 w-3.5 shrink-0', isSorted
12
+ ? 'text-primary'
13
+ : 'opacity-0 group-hover:opacity-60 group-focus-visible:opacity-60'), iconKey: iconKey })] }));
14
+ };
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PsLayer, usePsLayerZIndex } from '@pallasoft/ps-core-components';
3
+ import { PallasoftClassNameService, PallasoftZIndex } from '@pallasoft/ps-design-system';
4
+ import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
5
+ import { Button, Dialog, DialogTrigger, Popover } from 'react-aria-components';
6
+ import { PsTableQueryService } from '../services/ps-table-query-service';
7
+ import { PsTableFilterField } from './ps-table-filter-field';
8
+ const toolbarButtonClassName = 'ps-focus-ring ps-liquid ps-liquid-control inline-flex min-h-10 min-w-10 items-center justify-center gap-2 rounded-full px-3 text-sm font-semibold text-ink outline-none transition-colors hover:border-primary/40 sm:px-4';
9
+ export const PsTableToolbar = ({ actions, columns, filters, isSearchable, onClearFilters, onFilterChange, onSearchChange, rows, search, searchPlaceholder, }) => {
10
+ const filterableColumns = columns.filter((column) => column.filter);
11
+ const zIndex = usePsLayerZIndex(PallasoftZIndex.Dropdown);
12
+ const activeCount = filterableColumns.filter((column) => PsTableQueryService.isActive(filters[column.id])).length;
13
+ return (_jsxs("div", { className: 'flex flex-wrap items-center gap-2', children: [isSearchable ? (_jsxs("label", { className: 'relative basis-full sm:min-w-[12rem] sm:flex-1 sm:basis-auto sm:max-w-xs', children: [_jsx("span", { className: 'sr-only', children: searchPlaceholder }), _jsx(PsIcon, { "aria-hidden": 'true', className: 'pointer-events-none absolute left-3.5 top-1/2 h-4 w-4 -translate-y-1/2 text-ink/45', iconKey: PsIconKey.Search }), _jsx("input", { className: 'ps-input-control ps-focus-ring min-h-10 rounded-full pl-10 text-sm', onChange: (event) => onSearchChange(event.target.value), placeholder: searchPlaceholder, type: 'search', value: search })] })) : null, filterableColumns.length > 0 ? (_jsxs(DialogTrigger, { children: [_jsxs(Button, { className: toolbarButtonClassName, children: [_jsx(PsIcon, { "aria-hidden": 'true', className: 'block h-4 w-4', iconKey: PsIconKey.Filter }), _jsx("span", { className: 'sr-only sm:not-sr-only', children: "Filters" }), activeCount > 0 ? (_jsx("span", { className: 'inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-primary px-1.5 text-xs font-semibold text-[color:var(--ps-color-primary-contrast)]', children: activeCount })) : null] }), _jsx(Popover, { className: 'ps-floating-panel ps-scrollbar-hidden max-h-[min(34rem,80vh)] w-[min(22rem,calc(100vw-2rem))] overflow-y-auto outline-none', offset: 6, placement: 'bottom start', style: { zIndex }, children: _jsx(Dialog, { "aria-label": 'Filters', className: 'flex flex-col gap-4 p-4 outline-none', children: _jsxs(PsLayer, { zIndex: zIndex, children: [filterableColumns.map((column) => (_jsx(PsTableFilterField, { kind: column.filter ? column.filter.kind : 'text', label: column.label, onChange: (value) => onFilterChange(column.id, value), options: PsTableQueryService.options(rows, column), value: filters[column.id] ? filters[column.id] : {} }, column.id))), _jsx("div", { className: 'flex justify-end border-t border-line pt-3', children: _jsx("button", { className: 'ps-focus-ring rounded-lg px-2 py-1 text-sm font-semibold text-ink/65 hover:text-ink disabled:opacity-40', disabled: activeCount === 0, onClick: onClearFilters, type: 'button', children: "Clear all" }) })] }) }) })] })) : null, activeCount > 0 ? (_jsx("button", { className: PallasoftClassNameService.merge('ps-focus-ring rounded-full px-3 py-2 text-sm font-semibold text-ink/65 hover:text-ink'), onClick: onClearFilters, type: 'button', children: "Clear filters" })) : null, actions ? (_jsx("div", { className: 'ml-auto flex min-w-0 flex-wrap items-center justify-end gap-2', children: actions })) : null] }));
14
+ };
@@ -0,0 +1,6 @@
1
+ export const PsTableSearchConfig = {
2
+ currencySymbols: /[$€£¥]/g,
3
+ diacritics: /[\u0300-\u036f]/g,
4
+ thousandsSeparator: /(\d),(?=\d{3}\b)/g,
5
+ whitespace: /\s+/g,
6
+ };
@@ -0,0 +1,5 @@
1
+ export const PsTableCardRole = {
2
+ detail: 'detail',
3
+ meta: 'meta',
4
+ subtitle: 'subtitle',
5
+ };
@@ -0,0 +1,8 @@
1
+ export const PsTableConfig = {
2
+ groupCloseMs: 160,
3
+ groupRowStaggerMs: 16,
4
+ groupStaggerLimit: 12,
5
+ noMatchesDescription: 'Try a different search or clear the filters.',
6
+ noMatchesTitle: 'Nothing matches',
7
+ searchPlaceholder: 'Search',
8
+ };
@@ -0,0 +1,6 @@
1
+ export const PsTableFilterKind = {
2
+ date: 'date',
3
+ number: 'number',
4
+ select: 'select',
5
+ text: 'text',
6
+ };
@@ -0,0 +1,17 @@
1
+ export * from './components/ps-data-table';
2
+ export * from './components/ps-table-group-header';
3
+ export * from './components/ps-table-pagination';
4
+ export * from './constants/table-card-role';
5
+ export * from './constants/table-filter-kind';
6
+ export * from './interfaces/table-column';
7
+ export * from './interfaces/table-column-filter';
8
+ export * from './interfaces/table-filter-option';
9
+ export * from './interfaces/table-filter-value';
10
+ export * from './interfaces/table-grouping';
11
+ export * from './interfaces/table-query';
12
+ export * from './interfaces/table-row-action';
13
+ export * from './interfaces/table-row-group';
14
+ export * from './interfaces/table-sort';
15
+ export * from './services/ps-table-group-service';
16
+ export * from './services/ps-table-query-service';
17
+ export * from './services/ps-table-search-service';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ export class PsTableGroupService {
2
+ static group(rows, getGroupKey) {
3
+ const groups = new Map();
4
+ for (const row of rows) {
5
+ const key = getGroupKey(row);
6
+ const existing = groups.get(key);
7
+ if (existing)
8
+ existing.push(row);
9
+ else
10
+ groups.set(key, [row]);
11
+ }
12
+ return [...groups.entries()].map(([key, groupRows]) => ({ key, rows: groupRows }));
13
+ }
14
+ }
@@ -0,0 +1,93 @@
1
+ import { PsTableFilterKind } from '../constants/table-filter-kind';
2
+ import { PsTableSearchService } from './ps-table-search-service';
3
+ export class PsTableQueryService {
4
+ static isActive(value) {
5
+ if (!value)
6
+ return false;
7
+ return Boolean((value.text && value.text.trim() !== '') ||
8
+ (value.values && value.values.length > 0) ||
9
+ (value.from && value.from !== '') ||
10
+ (value.to && value.to !== ''));
11
+ }
12
+ static matchesFilter(row, column, value) {
13
+ if (!column.filter || !PsTableQueryService.isActive(value))
14
+ return true;
15
+ const raw = column.filter.getValue(row);
16
+ const cell = raw === null ? '' : String(raw);
17
+ if (column.filter.kind === PsTableFilterKind.text)
18
+ return cell.toLowerCase().includes((value.text ? value.text : '').trim().toLowerCase());
19
+ if (column.filter.kind === PsTableFilterKind.select)
20
+ return value.values ? value.values.includes(cell) : true;
21
+ if (column.filter.kind === PsTableFilterKind.number) {
22
+ const amount = Number(cell);
23
+ if (cell === '' || Number.isNaN(amount))
24
+ return false;
25
+ if (value.from && amount < Number(value.from))
26
+ return false;
27
+ if (value.to && amount > Number(value.to))
28
+ return false;
29
+ return true;
30
+ }
31
+ if (cell === '')
32
+ return false;
33
+ if (value.from && cell < value.from)
34
+ return false;
35
+ if (value.to && cell > value.to)
36
+ return false;
37
+ return true;
38
+ }
39
+ static apply(rows, columns, filters, search, getSearchText, sort) {
40
+ const filtered = rows.filter((row) => (!getSearchText || PsTableSearchService.matches(getSearchText(row), search)) &&
41
+ columns.every((column) => PsTableQueryService.matchesFilter(row, column, filters[column.id] ? filters[column.id] : {})));
42
+ const sortColumn = sort ? columns.find((column) => column.id === sort.columnId) : undefined;
43
+ if (!sort || !sortColumn || !sortColumn.sortValue)
44
+ return filtered;
45
+ const getValue = sortColumn.sortValue;
46
+ const direction = sort.direction === 'ascending' ? 1 : -1;
47
+ return [...filtered].sort((left, right) => {
48
+ const a = getValue(left);
49
+ const b = getValue(right);
50
+ if (a === b)
51
+ return 0;
52
+ if (a === null)
53
+ return 1;
54
+ if (b === null)
55
+ return -1;
56
+ if (typeof a === 'number' && typeof b === 'number')
57
+ return (a - b) * direction;
58
+ return String(a).localeCompare(String(b), undefined, { numeric: true }) * direction;
59
+ });
60
+ }
61
+ static options(rows, column) {
62
+ if (!column.filter)
63
+ return [];
64
+ if (column.filter.options)
65
+ return column.filter.options;
66
+ const getValue = column.filter.getValue;
67
+ const values = new Set(rows.map((row) => getValue(row)).filter((value) => value !== null));
68
+ return [...values]
69
+ .map((value) => String(value))
70
+ .sort((a, b) => a.localeCompare(b))
71
+ .map((value) => ({ label: value, value }));
72
+ }
73
+ static empty(sort) {
74
+ return { filters: {}, hiddenColumnIds: [], search: '', sort };
75
+ }
76
+ static isEqual(left, right) {
77
+ const activeFilters = (query) => Object.entries(query.filters)
78
+ .filter(([, value]) => PsTableQueryService.isActive(value))
79
+ .sort(([a], [b]) => a.localeCompare(b));
80
+ return (left.search.trim() === right.search.trim() &&
81
+ JSON.stringify(left.sort) === JSON.stringify(right.sort) &&
82
+ JSON.stringify([...left.hiddenColumnIds].sort()) ===
83
+ JSON.stringify([...right.hiddenColumnIds].sort()) &&
84
+ JSON.stringify(activeFilters(left)) === JSON.stringify(activeFilters(right)));
85
+ }
86
+ static nextSort(current, columnId) {
87
+ if (!current || current.columnId !== columnId)
88
+ return { columnId, direction: 'ascending' };
89
+ if (current.direction === 'ascending')
90
+ return { columnId, direction: 'descending' };
91
+ return null;
92
+ }
93
+ }
@@ -0,0 +1,24 @@
1
+ import { PsTableSearchConfig } from '../constants/ps-table-search-config';
2
+ export class PsTableSearchService {
3
+ static normalise(value) {
4
+ return value
5
+ .normalize('NFD')
6
+ .replace(PsTableSearchConfig.diacritics, '')
7
+ .toLowerCase()
8
+ .replace(PsTableSearchConfig.thousandsSeparator, '$1')
9
+ .replace(PsTableSearchConfig.currencySymbols, ' ')
10
+ .replace(PsTableSearchConfig.whitespace, ' ')
11
+ .trim();
12
+ }
13
+ static tokens(query) {
14
+ const normalised = PsTableSearchService.normalise(query);
15
+ return normalised === '' ? [] : normalised.split(' ');
16
+ }
17
+ static matches(text, query) {
18
+ const tokens = PsTableSearchService.tokens(query);
19
+ if (tokens.length === 0)
20
+ return true;
21
+ const haystack = PsTableSearchService.normalise(text);
22
+ return tokens.every((token) => haystack.includes(token));
23
+ }
24
+ }
package/dist/index.d.ts CHANGED
@@ -1,12 +1,17 @@
1
1
  export * from './components/ps-data-table';
2
+ export * from './components/ps-table-group-header';
2
3
  export * from './components/ps-table-pagination';
4
+ export * from './constants/table-card-role';
3
5
  export * from './constants/table-filter-kind';
4
6
  export * from './interfaces/table-column';
5
7
  export * from './interfaces/table-column-filter';
6
8
  export * from './interfaces/table-filter-option';
7
9
  export * from './interfaces/table-filter-value';
10
+ export * from './interfaces/table-grouping';
8
11
  export * from './interfaces/table-query';
9
12
  export * from './interfaces/table-row-action';
13
+ export * from './interfaces/table-row-group';
10
14
  export * from './interfaces/table-sort';
15
+ export * from './services/ps-table-group-service';
11
16
  export * from './services/ps-table-query-service';
12
17
  export * from './services/ps-table-search-service';
package/dist/index.js CHANGED
@@ -15,14 +15,19 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./components/ps-data-table"), exports);
18
+ __exportStar(require("./components/ps-table-group-header"), exports);
18
19
  __exportStar(require("./components/ps-table-pagination"), exports);
20
+ __exportStar(require("./constants/table-card-role"), exports);
19
21
  __exportStar(require("./constants/table-filter-kind"), exports);
20
22
  __exportStar(require("./interfaces/table-column"), exports);
21
23
  __exportStar(require("./interfaces/table-column-filter"), exports);
22
24
  __exportStar(require("./interfaces/table-filter-option"), exports);
23
25
  __exportStar(require("./interfaces/table-filter-value"), exports);
26
+ __exportStar(require("./interfaces/table-grouping"), exports);
24
27
  __exportStar(require("./interfaces/table-query"), exports);
25
28
  __exportStar(require("./interfaces/table-row-action"), exports);
29
+ __exportStar(require("./interfaces/table-row-group"), exports);
26
30
  __exportStar(require("./interfaces/table-sort"), exports);
31
+ __exportStar(require("./services/ps-table-group-service"), exports);
27
32
  __exportStar(require("./services/ps-table-query-service"), exports);
28
33
  __exportStar(require("./services/ps-table-search-service"), exports);
@@ -1,9 +1,11 @@
1
1
  import type { ReactNode } from 'react';
2
+ import type { PsTableCardRole } from '../constants/table-card-role';
2
3
  import type { PsTableColumnFilter } from './table-column-filter';
3
4
  export interface PsTableColumn<RowData> {
4
5
  align?: 'center' | 'left' | 'right';
5
6
  canHide?: boolean;
6
7
  cardFullWidth?: boolean;
8
+ cardRole?: PsTableCardRole;
7
9
  filter?: PsTableColumnFilter<RowData>;
8
10
  footer?: ReactNode;
9
11
  hideOnCards?: boolean;
@@ -0,0 +1,7 @@
1
+ import type { ReactNode } from 'react';
2
+ export interface PsTableGrouping<RowData> {
3
+ getGroupKey: (row: RowData) => string;
4
+ getGroupLabel: (groupKey: string, rows: RowData[]) => string;
5
+ initialExpandedCount?: number;
6
+ renderGroupSummary?: (rows: RowData[]) => ReactNode;
7
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,4 @@
1
+ export interface PsTableRowGroup<RowData> {
2
+ key: string;
3
+ rows: RowData[];
4
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,4 @@
1
+ export interface PsTableRowMotion {
2
+ className: string;
3
+ delayMs?: number;
4
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,4 @@
1
+ import type { PsTableRowGroup } from '../interfaces/table-row-group';
2
+ export declare class PsTableGroupService {
3
+ static group<RowData>(rows: RowData[], getGroupKey: (row: RowData) => string): PsTableRowGroup<RowData>[];
4
+ }
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PsTableGroupService = void 0;
4
+ class PsTableGroupService {
5
+ static group(rows, getGroupKey) {
6
+ const groups = new Map();
7
+ for (const row of rows) {
8
+ const key = getGroupKey(row);
9
+ const existing = groups.get(key);
10
+ if (existing)
11
+ existing.push(row);
12
+ else
13
+ groups.set(key, [row]);
14
+ }
15
+ return [...groups.entries()].map(([key, groupRows]) => ({ key, rows: groupRows }));
16
+ }
17
+ }
18
+ exports.PsTableGroupService = PsTableGroupService;
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "@pallasoft/ps-table-components",
3
- "version": "1.8.0",
3
+ "version": "1.10.0",
4
4
  "private": false,
5
5
  "main": "./dist/index.js",
6
+ "module": "./dist/esm/index.js",
6
7
  "types": "./dist/index.d.ts",
8
+ "sideEffects": false,
7
9
  "exports": {
8
10
  ".": {
9
11
  "types": "./dist/index.d.ts",
12
+ "module": "./dist/esm/index.js",
10
13
  "require": "./dist/index.js",
11
14
  "default": "./dist/index.js"
12
15
  }
@@ -16,7 +19,7 @@
16
19
  ],
17
20
  "scripts": {
18
21
  "husky:prepare": "npx husky",
19
- "build": "tsc",
22
+ "build": "tsc && tsc -p tsconfig.esm.json",
20
23
  "setup:graphify": "sh scripts/graphify-setup.sh",
21
24
  "update:graphify": "sh scripts/graphify-update.sh",
22
25
  "lint": "npm run lint:eslint",