tp-react-elements 1.3.9 → 1.3.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -165,9 +165,33 @@ The library provides a comprehensive set of form components that integrate seaml
165
165
  - **RadioGroup** - Radio button selection groups
166
166
  - **Switch** - Toggle switches for boolean values
167
167
  - **Form** - Container component with built-in validation
168
+ - **GenericPaginatedGrid** - Server-paginated data grid with dynamic filter chips (see Storybook **Docs → Paginated Grid**)
168
169
 
169
170
  Refer to our Storybook documentation for detailed API documentation and examples.
170
171
 
172
+ ### Paginated grid (Storybook)
173
+
174
+ ```bash
175
+ npm run storybook
176
+ ```
177
+
178
+ - **Docs → Paginated Grid** — full API: grid height, header wrap (`headerMinHeight` / `headerMaxHeight`), `headerBgColor`, filters, troubleshooting
179
+ - **Components → PaginatedGrid → Sample Grids** — mock grids (no API): header wrap test, compact layout, viewport fill
180
+ - **Components → PaginatedGrid → Example Grid** — live invoice API demo
181
+
182
+ Example header styling:
183
+
184
+ ```tsx
185
+ <GenericPaginatedGrid
186
+ headerBgColor="#e8eaf6"
187
+ headerMinHeight="48px"
188
+ headerMaxHeight="96px"
189
+ tableHeight="calc(100vh - 200px)"
190
+ autoFillViewport={false}
191
+ {...gridProps}
192
+ />
193
+ ```
194
+
171
195
  ## 🏗️ Best Practices for Code Structure and Organization
172
196
 
173
197
  ### Component Organization
@@ -1,3 +1,3 @@
1
- var DefinedRange = {};
1
+ var DateRange = {};
2
2
 
3
- export { DefinedRange as __exports };
3
+ export { DateRange as __exports };
@@ -1,3 +1,3 @@
1
- var DateRange = {};
1
+ var DefinedRange = {};
2
2
 
3
- export { DateRange as __exports };
3
+ export { DefinedRange as __exports };
@@ -82,6 +82,10 @@ export interface DynamicFilterBarProps {
82
82
  availableColumns: AvailableColumn[];
83
83
  /** Maximum number of chips that may be visible at once. Default: 4. */
84
84
  maxVisibleFilters?: number;
85
+ uiDensity?: 'default' | 'compact';
86
+ filterChipFontSize?: number;
87
+ filterChipMinHeight?: number;
88
+ filterLabelFontSize?: number;
85
89
  }
86
- declare const DynamicFilterBar: ({ serverFilterDefs, visibleFilterIds, onToggleFilter, filterValues, onFilterValuesChange, columnFilters, onColumnFilterChange, availableColumns, maxVisibleFilters, }: DynamicFilterBarProps) => import("react/jsx-runtime").JSX.Element;
90
+ declare const DynamicFilterBar: ({ serverFilterDefs, visibleFilterIds, onToggleFilter, filterValues, onFilterValuesChange, columnFilters, onColumnFilterChange, availableColumns, maxVisibleFilters, uiDensity, filterChipFontSize, filterChipMinHeight, filterLabelFontSize, }: DynamicFilterBarProps) => import("react/jsx-runtime").JSX.Element;
87
91
  export default DynamicFilterBar;
@@ -38,19 +38,17 @@ const COLUMN_TYPE_PILL = {
38
38
  'searchable-select': { label: 'Search', color: '#bf360c', bg: '#fbe9e7' },
39
39
  number: { label: 'Number', color: '#e65100', bg: '#fff3e0' },
40
40
  };
41
- // ─── Chip shared style ────────────────────────────────────────────────────────
42
- const chipSx = (active) => ({
41
+ const chipSx = (active, opts) => ({
43
42
  borderRadius: 5,
44
43
  textTransform: 'none',
45
- fontSize: 12,
44
+ fontSize: opts.fontSize,
46
45
  fontWeight: active ? 600 : 400,
47
46
  borderColor: active ? 'primary.main' : 'divider',
48
47
  color: active ? 'primary.main' : 'text.secondary',
49
48
  bgcolor: active ? 'rgba(25,118,210,0.08)' : 'transparent',
50
49
  whiteSpace: 'nowrap',
51
- px: 1.5,
52
- minHeight: 30,
53
- // Cap chip width so long option labels don't break the filter bar layout
50
+ px: opts.px ?? (opts.fontSize <= 11 ? 1 : 1.5),
51
+ minHeight: opts.minHeight,
54
52
  maxWidth: 260,
55
53
  overflow: 'hidden',
56
54
  '&:hover': { borderColor: 'primary.main', bgcolor: 'rgba(25,118,210,0.08)' },
@@ -78,7 +76,7 @@ const fmtDate = (iso) => {
78
76
  const yearSuffix = y !== thisYear ? ` '${y.slice(2)}` : '';
79
77
  return `${Number(d)} ${months[Number(m) - 1]}${yearSuffix}`;
80
78
  };
81
- const DateRangeChip = ({ label, fromDate, toDate, onApply, onRemove }) => {
79
+ const DateRangeChip = ({ label, fromDate, toDate, onApply, onRemove, chipStyle }) => {
82
80
  // Inject react-date-range CSS via JS (not CSS import) to prevent it from being
83
81
  // bundled into the library's extracted CSS file and leaking into consumer apps.
84
82
  useRdrStyles();
@@ -120,14 +118,14 @@ const DateRangeChip = ({ label, fromDate, toDate, onApply, onRemove }) => {
120
118
  setAnchor(null);
121
119
  }
122
120
  };
123
- return (jsxs(Fragment, { children: [jsx(Tooltip, { title: active ? chipLabel : '', placement: "top", arrow: true, children: jsx(Button, { variant: "outlined", size: "small", onClick: openPicker, startIcon: jsx(CalendarTodayIcon, { sx: { fontSize: 13 } }), endIcon: jsx(CloseIcon, { sx: { fontSize: '12px !important' }, onClick: (e) => { e.stopPropagation(); onRemove(); } }), sx: chipSx(active), children: jsx(ChipLabel, { text: chipLabel }) }) }), jsxs(Popover, { open: Boolean(anchor), anchorEl: anchor, onClose: () => setAnchor(null), anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, PaperProps: { sx: { mt: 0.75, borderRadius: 2, boxShadow: 6, overflow: 'hidden' } }, children: [jsx(distExports.DateRange, { editableDateInputs: false, onChange: handleRangeChange, moveRangeOnFirstSelection: false, ranges: selRange, months: 2, direction: "horizontal", showDateDisplay: false, dateDisplayFormat: "dd/MM/yyyy", rangeColors: ['#1976d2'] }), active && (jsx(Box, { sx: { display: 'flex', justifyContent: 'flex-end', px: 2, pb: 1.5 }, children: jsx(Button, { size: "small", color: "inherit", onClick: () => { onApply('', ''); setAnchor(null); }, children: "Clear" }) }))] })] }));
121
+ return (jsxs(Fragment, { children: [jsx(Tooltip, { title: active ? chipLabel : '', placement: "top", arrow: true, children: jsx(Button, { variant: "outlined", size: "small", onClick: openPicker, startIcon: jsx(CalendarTodayIcon, { sx: { fontSize: 13 } }), endIcon: jsx(CloseIcon, { sx: { fontSize: '12px !important' }, onClick: (e) => { e.stopPropagation(); onRemove(); } }), sx: chipSx(active, chipStyle), children: jsx(ChipLabel, { text: chipLabel }) }) }), jsxs(Popover, { open: Boolean(anchor), anchorEl: anchor, onClose: () => setAnchor(null), anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, PaperProps: { sx: { mt: 0.75, borderRadius: 2, boxShadow: 6, overflow: 'hidden' } }, children: [jsx(distExports.DateRange, { editableDateInputs: false, onChange: handleRangeChange, moveRangeOnFirstSelection: false, ranges: selRange, months: 2, direction: "horizontal", showDateDisplay: false, dateDisplayFormat: "dd/MM/yyyy", rangeColors: ['#1976d2'] }), active && (jsx(Box, { sx: { display: 'flex', justifyContent: 'flex-end', px: 2, pb: 1.5 }, children: jsx(Button, { size: "small", color: "inherit", onClick: () => { onApply('', ''); setAnchor(null); }, children: "Clear" }) }))] })] }));
124
122
  };
125
- const TextFilterChip = ({ label, value, onApply, onRemove, placeholder }) => {
123
+ const TextFilterChip = ({ label, value, onApply, onRemove, placeholder, chipStyle }) => {
126
124
  const [anchor, setAnchor] = useState(null);
127
125
  const [draft, setDraft] = useState('');
128
126
  const active = Boolean(value);
129
127
  const chipText = active ? `${label}: ${value}` : label;
130
- return (jsxs(Fragment, { children: [jsx(Tooltip, { title: active ? chipText : '', placement: "top", arrow: true, children: jsx(Button, { variant: "outlined", size: "small", onClick: (e) => { setDraft(value); setAnchor(e.currentTarget); }, endIcon: jsx(CloseIcon, { sx: { fontSize: '12px !important' }, onClick: (e) => { e.stopPropagation(); onRemove(); } }), sx: chipSx(active), children: jsx(ChipLabel, { text: chipText }) }) }), jsx(Popover, { open: Boolean(anchor), anchorEl: anchor, onClose: () => setAnchor(null), anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, PaperProps: { sx: { mt: 0.75, borderRadius: 2, boxShadow: 6, minWidth: 240 } }, children: jsxs(Box, { sx: { p: 2 }, children: [jsx(Typography, { variant: "caption", fontWeight: 600, color: "text.secondary", display: "block", mb: 1.5, children: label }), jsx(TextField, { value: draft, onChange: (e) => setDraft(e.target.value), placeholder: placeholder, size: "small", fullWidth: true, autoFocus: true, onKeyDown: (e) => {
128
+ return (jsxs(Fragment, { children: [jsx(Tooltip, { title: active ? chipText : '', placement: "top", arrow: true, children: jsx(Button, { variant: "outlined", size: "small", onClick: (e) => { setDraft(value); setAnchor(e.currentTarget); }, endIcon: jsx(CloseIcon, { sx: { fontSize: '12px !important' }, onClick: (e) => { e.stopPropagation(); onRemove(); } }), sx: chipSx(active, chipStyle), children: jsx(ChipLabel, { text: chipText }) }) }), jsx(Popover, { open: Boolean(anchor), anchorEl: anchor, onClose: () => setAnchor(null), anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, PaperProps: { sx: { mt: 0.75, borderRadius: 2, boxShadow: 6, minWidth: 240 } }, children: jsxs(Box, { sx: { p: 2 }, children: [jsx(Typography, { variant: "caption", fontWeight: 600, color: "text.secondary", display: "block", mb: 1.5, children: label }), jsx(TextField, { value: draft, onChange: (e) => setDraft(e.target.value), placeholder: placeholder, size: "small", fullWidth: true, autoFocus: true, onKeyDown: (e) => {
131
129
  if (e.key === 'Enter') {
132
130
  onApply(draft);
133
131
  setAnchor(null);
@@ -136,7 +134,7 @@ const TextFilterChip = ({ label, value, onApply, onRemove, placeholder }) => {
136
134
  setAnchor(null);
137
135
  } }), jsxs(Stack, { direction: "row", spacing: 1, justifyContent: "flex-end", mt: 1.5, children: [active && (jsx(Button, { size: "small", color: "inherit", onClick: () => { onApply(''); setAnchor(null); }, children: "Clear" })), jsx(Button, { size: "small", variant: "contained", disableElevation: true, onClick: () => { onApply(draft); setAnchor(null); }, children: "Apply" })] })] }) })] }));
138
136
  };
139
- const DatePickerChip = ({ label, value, onApply, onRemove }) => {
137
+ const DatePickerChip = ({ label, value, onApply, onRemove, chipStyle }) => {
140
138
  const [open, setOpen] = useState(false);
141
139
  const [anchorEl, setAnchorEl] = useState(null);
142
140
  const active = Boolean(value);
@@ -160,7 +158,7 @@ const DatePickerChip = ({ label, value, onApply, onRemove }) => {
160
158
  // LocalizationProvider must wrap DatePicker — scoped here so it only
161
159
  // loads when the chip is actually used (DatePickerChip is tree-shaken
162
160
  // if the 'date' filter type is never included in serverFilterDefs).
163
- jsxs(LocalizationProvider, { dateAdapter: AdapterDayjs, children: [jsx(Tooltip, { title: active ? chipText : '', placement: "top", arrow: true, children: jsx(Button, { variant: "outlined", size: "small", onClick: handleChipClick, startIcon: jsx(CalendarTodayIcon, { sx: { fontSize: 13 } }), endIcon: jsx(CloseIcon, { sx: { fontSize: '12px !important' }, onClick: (e) => { e.stopPropagation(); onRemove(); } }), sx: chipSx(active), children: jsx(ChipLabel, { text: chipText }) }) }), jsx(DatePicker, { value: dayjsValue, open: open, onClose: () => setOpen(false), onChange: handleChange, slotProps: {
161
+ jsxs(LocalizationProvider, { dateAdapter: AdapterDayjs, children: [jsx(Tooltip, { title: active ? chipText : '', placement: "top", arrow: true, children: jsx(Button, { variant: "outlined", size: "small", onClick: handleChipClick, startIcon: jsx(CalendarTodayIcon, { sx: { fontSize: 13 } }), endIcon: jsx(CloseIcon, { sx: { fontSize: '12px !important' }, onClick: (e) => { e.stopPropagation(); onRemove(); } }), sx: chipSx(active, chipStyle), children: jsx(ChipLabel, { text: chipText }) }) }), jsx(DatePicker, { value: dayjsValue, open: open, onClose: () => setOpen(false), onChange: handleChange, slotProps: {
164
162
  textField: { sx: { display: 'none' }, size: 'small' },
165
163
  popper: {
166
164
  anchorEl,
@@ -168,7 +166,7 @@ const DatePickerChip = ({ label, value, onApply, onRemove }) => {
168
166
  },
169
167
  } })] }));
170
168
  };
171
- const DropdownFilterChip = ({ label, value, options, onApply, onRemove }) => {
169
+ const DropdownFilterChip = ({ label, value, options, onApply, onRemove, chipStyle }) => {
172
170
  const [anchor, setAnchor] = useState(null);
173
171
  const active = Boolean(value);
174
172
  const selectedLabel = options.find((o) => o.value === value)?.label ?? '';
@@ -199,7 +197,7 @@ const DropdownFilterChip = ({ label, value, options, onApply, onRemove }) => {
199
197
  ],
200
198
  };
201
199
  const chipText = active ? `${label}: ${selectedLabel}` : label;
202
- return (jsxs(Fragment, { children: [jsx(Tooltip, { title: active ? chipText : '', placement: "top", arrow: true, children: jsx(Button, { variant: "outlined", size: "small", onClick: (e) => setAnchor(e.currentTarget), startIcon: jsx(ArrowDropDownIcon, { sx: { fontSize: '15px !important', mr: -0.5 } }), endIcon: jsx(CloseIcon, { sx: { fontSize: '12px !important' }, onClick: (e) => { e.stopPropagation(); onRemove(); } }), sx: chipSx(active), children: jsx(ChipLabel, { text: chipText }) }) }), jsx(Popover, { open: Boolean(anchor), anchorEl: anchor, onClose: () => setAnchor(null), anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, PaperProps: { sx: { mt: 0.75, borderRadius: 2, boxShadow: 6, p: 1.5, minWidth: 240 } }, children: jsx(SingleSelectNonAutoComplete, { props: {
200
+ return (jsxs(Fragment, { children: [jsx(Tooltip, { title: active ? chipText : '', placement: "top", arrow: true, children: jsx(Button, { variant: "outlined", size: "small", onClick: (e) => setAnchor(e.currentTarget), startIcon: jsx(ArrowDropDownIcon, { sx: { fontSize: '15px !important', mr: -0.5 } }), endIcon: jsx(CloseIcon, { sx: { fontSize: '12px !important' }, onClick: (e) => { e.stopPropagation(); onRemove(); } }), sx: chipSx(active, chipStyle), children: jsx(ChipLabel, { text: chipText }) }) }), jsx(Popover, { open: Boolean(anchor), anchorEl: anchor, onClose: () => setAnchor(null), anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, PaperProps: { sx: { mt: 0.75, borderRadius: 2, boxShadow: 6, p: 1.5, minWidth: 240 } }, children: jsx(SingleSelectNonAutoComplete, { props: {
203
201
  item,
204
202
  register,
205
203
  control,
@@ -210,7 +208,7 @@ const DropdownFilterChip = ({ label, value, options, onApply, onRemove }) => {
210
208
  watch,
211
209
  }, variant: "outlined" }) })] }));
212
210
  };
213
- const SearchableSelectChip = ({ label, value, options, onApply, onRemove, placeholder = 'Search…', }) => {
211
+ const SearchableSelectChip = ({ label, value, options, onApply, onRemove, placeholder = 'Search…', chipStyle, }) => {
214
212
  const [anchor, setAnchor] = useState(null);
215
213
  const [search, setSearch] = useState('');
216
214
  const active = Boolean(value);
@@ -228,7 +226,7 @@ const SearchableSelectChip = ({ label, value, options, onApply, onRemove, placeh
228
226
  ? options.filter((o) => o.label.toLowerCase().includes(search.toLowerCase()))
229
227
  : options;
230
228
  const chipText = active ? `${label}: ${selectedLabel}` : label;
231
- return (jsxs(Fragment, { children: [jsx(Tooltip, { title: active ? chipText : '', placement: "top", arrow: true, children: jsx(Button, { variant: "outlined", size: "small", onClick: open, startIcon: jsx(SearchIcon, { sx: { fontSize: '14px !important', mr: -0.25 } }), endIcon: jsx(CloseIcon, { sx: { fontSize: '12px !important' }, onClick: (e) => { e.stopPropagation(); onRemove(); } }), sx: chipSx(active), children: jsx(ChipLabel, { text: chipText }) }) }), jsxs(Popover, { open: Boolean(anchor), anchorEl: anchor, onClose: () => setAnchor(null), anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, PaperProps: {
229
+ return (jsxs(Fragment, { children: [jsx(Tooltip, { title: active ? chipText : '', placement: "top", arrow: true, children: jsx(Button, { variant: "outlined", size: "small", onClick: open, startIcon: jsx(SearchIcon, { sx: { fontSize: '14px !important', mr: -0.25 } }), endIcon: jsx(CloseIcon, { sx: { fontSize: '12px !important' }, onClick: (e) => { e.stopPropagation(); onRemove(); } }), sx: chipSx(active, chipStyle), children: jsx(ChipLabel, { text: chipText }) }) }), jsxs(Popover, { open: Boolean(anchor), anchorEl: anchor, onClose: () => setAnchor(null), anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, PaperProps: {
232
230
  sx: { mt: 0.75, borderRadius: 2, boxShadow: 6, width: 280, display: 'flex', flexDirection: 'column' },
233
231
  }, children: [jsx(Box, { sx: { px: 1.5, pt: 1.5, pb: 0.75 }, children: jsx(TextField, { size: "small", fullWidth: true, autoFocus: true, placeholder: placeholder, value: search, onChange: (e) => setSearch(e.target.value), onKeyDown: (e) => { if (e.key === 'Escape')
234
232
  setAnchor(null); }, InputProps: {
@@ -246,7 +244,7 @@ const SearchableSelectChip = ({ label, value, options, onApply, onRemove, placeh
246
244
  } }), isSelected && (jsx(CheckIcon, { sx: { fontSize: 15, color: 'primary.main', flexShrink: 0 } }))] }, opt.value));
247
245
  }))] })] })] }));
248
246
  };
249
- const ManageFiltersButton = ({ serverFilterDefs, visibleIds, availableColumns, onToggle, maxVisible = 4, }) => {
247
+ const ManageFiltersButton = ({ serverFilterDefs, visibleIds, availableColumns, onToggle, maxVisible = 4, chipStyle, }) => {
250
248
  const [anchor, setAnchor] = useState(null);
251
249
  const atLimit = visibleIds.length >= maxVisible;
252
250
  // Shared pill box renderer
@@ -262,9 +260,9 @@ const ManageFiltersButton = ({ serverFilterDefs, visibleIds, availableColumns, o
262
260
  lineHeight: 1.6,
263
261
  }, children: label }));
264
262
  return (jsxs(Fragment, { children: [jsx(Tooltip, { title: "Add or remove filter chips", children: jsx(Button, { size: "small", variant: "outlined", startIcon: jsx(TuneIcon, { sx: { fontSize: 14 } }), onClick: (e) => setAnchor(e.currentTarget), sx: {
265
- borderRadius: 5, textTransform: 'none', fontSize: 12,
263
+ borderRadius: 5, textTransform: 'none', fontSize: chipStyle.fontSize,
266
264
  borderColor: 'divider', color: 'text.secondary',
267
- px: 1.5, minHeight: 30,
265
+ px: chipStyle.px ?? 1.5, minHeight: chipStyle.minHeight,
268
266
  '&:hover': { borderColor: 'primary.main', color: 'primary.main' },
269
267
  }, children: "Manage Filters" }) }), jsxs(Popover, { open: Boolean(anchor), anchorEl: anchor, onClose: () => setAnchor(null), anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, PaperProps: { sx: { mt: 0.75, borderRadius: 2, boxShadow: 6, width: 280 } }, children: [atLimit && (jsx(Box, { sx: {
270
268
  px: 2, py: 0.75,
@@ -292,7 +290,12 @@ const ManageFiltersButton = ({ serverFilterDefs, visibleIds, availableColumns, o
292
290
  } }), selected && jsx(CheckIcon, { sx: { fontSize: 16, color: 'primary.main', flexShrink: 0 } })] }) }) }, col.id));
293
291
  }) })] }))] })] })] }));
294
292
  };
295
- const DynamicFilterBar = ({ serverFilterDefs, visibleFilterIds, onToggleFilter, filterValues, onFilterValuesChange, columnFilters, onColumnFilterChange, availableColumns, maxVisibleFilters = 4, }) => {
293
+ const DynamicFilterBar = ({ serverFilterDefs, visibleFilterIds, onToggleFilter, filterValues, onFilterValuesChange, columnFilters, onColumnFilterChange, availableColumns, maxVisibleFilters = 4, uiDensity = 'default', filterChipFontSize, filterChipMinHeight, filterLabelFontSize, }) => {
294
+ const chipStyle = {
295
+ fontSize: filterChipFontSize ?? (uiDensity === 'compact' ? 11 : 12),
296
+ minHeight: filterChipMinHeight ?? (uiDensity === 'compact' ? 26 : 30),
297
+ };
298
+ const labelFontSize = filterLabelFontSize ?? (uiDensity === 'compact' ? 11 : 12);
296
299
  const getColValue = (id) => columnFilters.find((f) => f.id === id)?.value ?? '';
297
300
  const applyColFilter = (id, value) => {
298
301
  const rest = columnFilters.filter((f) => f.id !== id);
@@ -316,36 +319,36 @@ const DynamicFilterBar = ({ serverFilterDefs, visibleFilterIds, onToggleFilter,
316
319
  }
317
320
  onToggleFilter(id);
318
321
  };
319
- return (jsxs(Box, { sx: { display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 0.75 }, children: [jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 0.4, mr: 0.25 }, children: [jsx(FilterListIcon, { sx: { fontSize: 15, color: 'text.disabled' } }), jsx(Typography, { variant: "body2", fontSize: 12, color: "text.secondary", fontWeight: 500, noWrap: true, children: "Filter by:" })] }), visibleFilterIds.map((id) => {
322
+ return (jsxs(Box, { sx: { display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 0.75 }, children: [jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 0.4, mr: 0.25 }, children: [jsx(FilterListIcon, { sx: { fontSize: labelFontSize + 3, color: 'text.disabled' } }), jsx(Typography, { sx: { fontSize: labelFontSize }, color: "text.secondary", fontWeight: 500, noWrap: true, children: "Filter by:" })] }), visibleFilterIds.map((id) => {
320
323
  const def = serverFilterDefs.find((d) => d.id === id);
321
324
  if (def?.type === 'dateRange') {
322
- return (jsx(DateRangeChip, { label: def.label, fromDate: filterValues[def.fromKey] ?? '', toDate: filterValues[def.toKey] ?? '', onApply: (from, to) => onFilterValuesChange({ ...filterValues, [def.fromKey]: from, [def.toKey]: to }), onRemove: () => removeFilter(id) }, id));
325
+ return (jsx(DateRangeChip, { label: def.label, fromDate: filterValues[def.fromKey] ?? '', toDate: filterValues[def.toKey] ?? '', onApply: (from, to) => onFilterValuesChange({ ...filterValues, [def.fromKey]: from, [def.toKey]: to }), onRemove: () => removeFilter(id), chipStyle: chipStyle }, id));
323
326
  }
324
327
  if (def?.type === 'text') {
325
- return (jsx(TextFilterChip, { label: def.label, value: filterValues[def.filterKey] ?? '', onApply: (v) => onFilterValuesChange({ ...filterValues, [def.filterKey]: v }), onRemove: () => removeFilter(id), placeholder: def.placeholder }, id));
328
+ return (jsx(TextFilterChip, { label: def.label, value: filterValues[def.filterKey] ?? '', onApply: (v) => onFilterValuesChange({ ...filterValues, [def.filterKey]: v }), onRemove: () => removeFilter(id), placeholder: def.placeholder, chipStyle: chipStyle }, id));
326
329
  }
327
330
  if (def?.type === 'dropdown') {
328
- return (jsx(DropdownFilterChip, { label: def.label, value: filterValues[def.filterKey] ?? '', options: def.options, onApply: (v) => onFilterValuesChange({ ...filterValues, [def.filterKey]: v }), onRemove: () => removeFilter(id) }, id));
331
+ return (jsx(DropdownFilterChip, { label: def.label, value: filterValues[def.filterKey] ?? '', options: def.options, onApply: (v) => onFilterValuesChange({ ...filterValues, [def.filterKey]: v }), onRemove: () => removeFilter(id), chipStyle: chipStyle }, id));
329
332
  }
330
333
  if (def?.type === 'searchableDropdown') {
331
- return (jsx(SearchableSelectChip, { label: def.label, value: filterValues[def.filterKey] ?? '', options: def.options, onApply: (v) => onFilterValuesChange({ ...filterValues, [def.filterKey]: v }), onRemove: () => removeFilter(id), placeholder: def.placeholder ?? 'Search…' }, id));
334
+ return (jsx(SearchableSelectChip, { label: def.label, value: filterValues[def.filterKey] ?? '', options: def.options, onApply: (v) => onFilterValuesChange({ ...filterValues, [def.filterKey]: v }), onRemove: () => removeFilter(id), placeholder: def.placeholder ?? 'Search…', chipStyle: chipStyle }, id));
332
335
  }
333
336
  if (def?.type === 'date') {
334
- return (jsx(DatePickerChip, { label: def.label, value: getColValue(def.columnKey), onApply: (v) => applyColFilter(def.columnKey, v), onRemove: () => removeFilter(id) }, id));
337
+ return (jsx(DatePickerChip, { label: def.label, value: getColValue(def.columnKey), onApply: (v) => applyColFilter(def.columnKey, v), onRemove: () => removeFilter(id), chipStyle: chipStyle }, id));
335
338
  }
336
339
  // Column filter chip — render the variant that matches the column's filterType
337
340
  const col = availableColumns.find((c) => c.id === id);
338
341
  if (!col)
339
342
  return null;
340
343
  if (col.filterType === 'date') {
341
- return (jsx(DatePickerChip, { label: col.label, value: getColValue(id), onApply: (v) => applyColFilter(id, v), onRemove: () => removeFilter(id) }, id));
344
+ return (jsx(DatePickerChip, { label: col.label, value: getColValue(id), onApply: (v) => applyColFilter(id, v), onRemove: () => removeFilter(id), chipStyle: chipStyle }, id));
342
345
  }
343
346
  if (col.filterType === 'select' || col.filterType === 'searchable-select') {
344
- return (jsx(SearchableSelectChip, { label: col.label, value: getColValue(id), options: col.filterOptions ?? [], onApply: (v) => applyColFilter(id, v), onRemove: () => removeFilter(id) }, id));
347
+ return (jsx(SearchableSelectChip, { label: col.label, value: getColValue(id), options: col.filterOptions ?? [], onApply: (v) => applyColFilter(id, v), onRemove: () => removeFilter(id), chipStyle: chipStyle }, id));
345
348
  }
346
349
  // Default: free-text input
347
- return (jsx(TextFilterChip, { label: col.label, value: getColValue(id), onApply: (v) => applyColFilter(id, v), onRemove: () => removeFilter(id), placeholder: `Filter by ${col.label}` }, id));
348
- }), jsx(ManageFiltersButton, { serverFilterDefs: serverFilterDefs, visibleIds: visibleFilterIds, availableColumns: availableColumns, onToggle: onToggleFilter, maxVisible: maxVisibleFilters })] }));
350
+ return (jsx(TextFilterChip, { label: col.label, value: getColValue(id), onApply: (v) => applyColFilter(id, v), onRemove: () => removeFilter(id), placeholder: `Filter by ${col.label}`, chipStyle: chipStyle }, id));
351
+ }), jsx(ManageFiltersButton, { serverFilterDefs: serverFilterDefs, visibleIds: visibleFilterIds, availableColumns: availableColumns, onToggle: onToggleFilter, maxVisible: maxVisibleFilters, chipStyle: chipStyle })] }));
349
352
  };
350
353
 
351
354
  export { DynamicFilterBar as default };
@@ -7,6 +7,21 @@ export interface PagedResult<TData> {
7
7
  data: TData[];
8
8
  totalRecords: number;
9
9
  }
10
+ /** Preset for toolbar, filter chips, and pagination footer sizing. */
11
+ export type GridUiDensity = 'default' | 'compact';
12
+ /** Fine-grained UI sizing overrides (merged on top of `uiDensity` preset). */
13
+ export interface GridUiDensityOptions {
14
+ footerMinHeight?: number;
15
+ footerFontSize?: number;
16
+ footerIconSize?: number;
17
+ footerPaddingY?: number;
18
+ filterChipFontSize?: number;
19
+ filterChipMinHeight?: number;
20
+ filterLabelFontSize?: number;
21
+ toolbarIconFontSize?: number | string;
22
+ /** MRT built-in toolbar control density (show/hide columns, density toggle, etc.). */
23
+ mrtDensity?: 'comfortable' | 'compact' | 'spacious';
24
+ }
10
25
  export interface GenericPaginatedGridProps<TData extends object> {
11
26
  /**
12
27
  * Column definitions. Use `TPColumnDef<TData>` (a superset of `MRT_ColumnDef`)
@@ -36,11 +51,35 @@ export interface GenericPaginatedGridProps<TData extends object> {
36
51
  /**
37
52
  * Height of the entire grid component.
38
53
  * When omitted (default) the grid auto-sizes to fill the remaining viewport
39
- * height from its top position — the page never needs to scroll.
40
- * Pass an explicit CSS value only when you need to override
41
- * (e.g. "600px", "calc(100vh - 300px)").
54
+ * height from its top position on mount and window resize only (not on scroll).
55
+ * Pass an explicit CSS value to pin height (e.g. "600px", "calc(100vh - 300px)").
42
56
  */
43
57
  tableHeight?: string;
58
+ /**
59
+ * Minimum height of the grid wrapper (CSS length).
60
+ * Used in auto-height `clamp()` and as `minHeight` when `tableHeight` is set.
61
+ * Default: `300px`.
62
+ */
63
+ tableMinHeight?: string;
64
+ /**
65
+ * Maximum height of the grid wrapper (CSS length).
66
+ * Caps auto-calculated viewport height. Default: none (only viewport clamp).
67
+ */
68
+ tableMaxHeight?: string;
69
+ /**
70
+ * When false, disables viewport auto-height; use `tableHeight` or `tableMinHeight` only.
71
+ * Default: true (unless `tableHeight` is provided).
72
+ */
73
+ autoFillViewport?: boolean;
74
+ /**
75
+ * MRT column layout. Default `grid` — columns grow to fill width (no trailing spacer column).
76
+ * Use `grid-no-grow` only when you need fixed column widths with horizontal scroll.
77
+ */
78
+ layoutMode?: 'grid' | 'semantic' | 'grid-no-grow';
79
+ /** Compact toolbar, filter chips, and footer. Default: `default`. */
80
+ uiDensity?: GridUiDensity;
81
+ /** Override individual sizes from the `uiDensity` preset. */
82
+ uiDensityOptions?: GridUiDensityOptions;
44
83
  /** Choices shown in the "Rows per page" footer dropdown. Default: [1 000, 5 000, 10 000]. */
45
84
  pageSizeOptions?: number[];
46
85
  /**
@@ -48,6 +87,21 @@ export interface GenericPaginatedGridProps<TData extends object> {
48
87
  * Accepts any valid CSS colour string.
49
88
  */
50
89
  headerBgColor?: string;
90
+ /**
91
+ * Minimum height of every header cell (CSS length or px number).
92
+ * Default: `40px` (`34px` when `uiDensity="compact"`).
93
+ */
94
+ headerMinHeight?: string | number;
95
+ /**
96
+ * Maximum height of the header row — **all columns use the same height**.
97
+ * Long titles wrap inside this box. Default: `72px` (`56px` compact).
98
+ */
99
+ headerMaxHeight?: string | number;
100
+ /**
101
+ * Optional max lines for header text (`-webkit-line-clamp`).
102
+ * Omit to allow text to use the full `headerMaxHeight` (recommended for tall headers).
103
+ */
104
+ headerMaxLines?: number;
51
105
  /**
52
106
  * Maximum number of filter chips that can be visible at once.
53
107
  * Prevents the filter bar from wrapping to a second row on typical screens.
@@ -55,5 +109,5 @@ export interface GenericPaginatedGridProps<TData extends object> {
55
109
  */
56
110
  maxVisibleFilters?: number;
57
111
  }
58
- declare function GenericPaginatedGrid<TData extends object>({ columns, fetchPage, queryKey, serverFilterDefs, defaultFilters, defaultVisibleFilterIds, batchSize, exportFilename, tableHeight, pageSizeOptions, headerBgColor, maxVisibleFilters, }: GenericPaginatedGridProps<TData>): import("react/jsx-runtime").JSX.Element;
112
+ declare function GenericPaginatedGrid<TData extends object>({ columns, fetchPage, queryKey, serverFilterDefs, defaultFilters, defaultVisibleFilterIds, batchSize, exportFilename, tableHeight, tableMinHeight, tableMaxHeight, autoFillViewport, layoutMode, uiDensity, uiDensityOptions, pageSizeOptions, headerBgColor, headerMinHeight, headerMaxHeight, headerMaxLines, maxVisibleFilters, }: GenericPaginatedGridProps<TData>): import("react/jsx-runtime").JSX.Element;
59
113
  export default GenericPaginatedGrid;
@@ -1,8 +1,8 @@
1
1
  import { jsx, jsxs } from 'react/jsx-runtime';
2
- import { useState, useRef, useLayoutEffect, useMemo } from 'react';
2
+ import { useMemo, useState, useRef, useLayoutEffect } from 'react';
3
3
  import { useMaterialReactTable, MaterialReactTable } from 'material-react-table';
4
4
  import { useQuery, keepPreviousData } from '@tanstack/react-query';
5
- import { Box, Tooltip, IconButton, Button, GlobalStyles, Typography, Select, MenuItem, CircularProgress } from '@mui/material';
5
+ import { Box, Tooltip, IconButton, GlobalStyles, Typography, Select, MenuItem, CircularProgress } from '@mui/material';
6
6
  import RefreshIcon from '@mui/icons-material/Refresh';
7
7
  import FileDownloadIcon from '@mui/icons-material/FileDownload';
8
8
  import FirstPageIcon from '@mui/icons-material/FirstPage';
@@ -11,6 +11,160 @@ import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
11
11
  import ChevronRightIcon from '@mui/icons-material/ChevronRight';
12
12
  import DynamicFilterBar from './DynamicFilterBar.esm.js';
13
13
 
14
+ const UI_DENSITY_PRESETS = {
15
+ default: {
16
+ footerMinHeight: 48,
17
+ footerFontSize: 14,
18
+ footerIconSize: 20,
19
+ footerPaddingY: 0.75,
20
+ filterChipFontSize: 12,
21
+ filterChipMinHeight: 30,
22
+ filterLabelFontSize: 12,
23
+ toolbarIconFontSize: 'small',
24
+ mrtDensity: 'comfortable',
25
+ },
26
+ compact: {
27
+ footerMinHeight: 32,
28
+ footerFontSize: 12,
29
+ footerIconSize: 18,
30
+ footerPaddingY: 0.25,
31
+ filterChipFontSize: 11,
32
+ filterChipMinHeight: 26,
33
+ filterLabelFontSize: 11,
34
+ toolbarIconFontSize: 'inherit',
35
+ mrtDensity: 'compact',
36
+ },
37
+ };
38
+ const toCssSize = (value, fallback) => {
39
+ if (value === undefined)
40
+ return fallback;
41
+ return typeof value === 'number' ? `${value}px` : value;
42
+ };
43
+ /**
44
+ * Header overrides applied via GlobalStyles (literal prop values).
45
+ * MRT sets `backgroundColor: 'inherit'` and fixed density padding — must use !important.
46
+ */
47
+ const buildHeaderGlobalStyles = (headerMin, headerMax, headerBgColor) => {
48
+ const headCell = {
49
+ height: `${headerMax} !important`,
50
+ minHeight: `${headerMin} !important`,
51
+ maxHeight: `${headerMax} !important`,
52
+ boxSizing: 'border-box !important',
53
+ alignSelf: 'stretch !important',
54
+ whiteSpace: 'normal !important',
55
+ verticalAlign: 'middle !important',
56
+ pt: '8px !important',
57
+ pb: '8px !important',
58
+ px: '14px !important',
59
+ };
60
+ if (headerBgColor) {
61
+ headCell.backgroundColor = `${headerBgColor} !important`;
62
+ headCell.backgroundImage = 'none !important';
63
+ }
64
+ return {
65
+ '.tp-gpg-root .MuiTableHead-root .MuiTableRow-root': {
66
+ height: `${headerMax} !important`,
67
+ minHeight: `${headerMin} !important`,
68
+ maxHeight: `${headerMax} !important`,
69
+ alignItems: 'stretch !important',
70
+ ...(headerBgColor ? { backgroundColor: headerBgColor } : {}),
71
+ },
72
+ '.tp-gpg-root .MuiTableHead-root .MuiTableCell-head': headCell,
73
+ '.tp-gpg-root .MuiTableHead-root .Mui-TableHeadCell-Content': {
74
+ height: '100% !important',
75
+ width: '100% !important',
76
+ minHeight: '0 !important',
77
+ alignItems: 'center !important',
78
+ },
79
+ '.tp-gpg-root .MuiTableHead-root .Mui-TableHeadCell-Content-Labels': {
80
+ overflow: 'visible !important',
81
+ flexWrap: 'wrap !important',
82
+ whiteSpace: 'normal !important',
83
+ textOverflow: 'clip !important',
84
+ width: '100% !important',
85
+ maxWidth: '100% !important',
86
+ },
87
+ '.tp-gpg-root .MuiTableHead-root .Mui-TableHeadCell-Content-Wrapper': {
88
+ overflow: 'visible !important',
89
+ whiteSpace: 'normal !important',
90
+ textOverflow: 'clip !important',
91
+ wordBreak: 'break-word !important',
92
+ overflowWrap: 'anywhere !important',
93
+ width: '100% !important',
94
+ maxWidth: '100% !important',
95
+ display: 'block !important',
96
+ WebkitLineClamp: 'unset !important',
97
+ WebkitBoxOrient: 'unset !important',
98
+ },
99
+ '.tp-gpg-root .MuiTableHead-root .MuiTableSortLabel-root': {
100
+ whiteSpace: 'normal !important',
101
+ wordBreak: 'break-word !important',
102
+ overflowWrap: 'anywhere !important',
103
+ width: '100% !important',
104
+ },
105
+ };
106
+ };
107
+ /** Per-cell sx merged into MRT (via muiTableHeadCellProps). */
108
+ const buildHeaderCellSx = (align, headerMin, headerMax, headerBgColor, headerMaxLines) => {
109
+ const textAlign = align;
110
+ const flexJustify = align === 'right' ? 'flex-end' : align === 'center' ? 'center' : 'flex-start';
111
+ const wrapperClamp = headerMaxLines != null && headerMaxLines > 0
112
+ ? {
113
+ display: '-webkit-box',
114
+ WebkitLineClamp: headerMaxLines,
115
+ WebkitBoxOrient: 'vertical',
116
+ overflow: 'hidden',
117
+ }
118
+ : {
119
+ display: 'block',
120
+ WebkitLineClamp: 'unset',
121
+ overflow: 'visible',
122
+ };
123
+ return {
124
+ ...(headerBgColor
125
+ ? { backgroundColor: `${headerBgColor} !important`, backgroundImage: 'none' }
126
+ : {}),
127
+ height: `${headerMax} !important`,
128
+ minHeight: `${headerMin} !important`,
129
+ maxHeight: `${headerMax} !important`,
130
+ boxSizing: 'border-box',
131
+ borderRight: '1px solid rgba(224,224,224,1)',
132
+ borderBottom: '2px solid rgba(0,0,0,0.15)',
133
+ '& button[aria-label="Move"]': { display: 'none !important' },
134
+ '& .MuiTableSortLabel-icon': { display: 'none !important' },
135
+ '& .Mui-TableHeadCell-ResizeHandle-Wrapper': { height: '100%', top: 0 },
136
+ '& .Mui-TableHeadCell-ResizeHandle-Divider': { borderRight: 'none' },
137
+ '& .Mui-TableHeadCell-Content': {
138
+ alignItems: 'center',
139
+ justifyContent: flexJustify,
140
+ height: '100%',
141
+ width: '100%',
142
+ },
143
+ '& .Mui-TableHeadCell-Content-Labels': {
144
+ flexWrap: 'wrap',
145
+ whiteSpace: 'normal',
146
+ textAlign,
147
+ width: '100%',
148
+ justifyContent: flexJustify,
149
+ overflow: 'visible',
150
+ },
151
+ '& .Mui-TableHeadCell-Content-Wrapper': {
152
+ whiteSpace: 'normal',
153
+ wordBreak: 'break-word',
154
+ overflowWrap: 'anywhere',
155
+ lineHeight: 1.25,
156
+ width: '100%',
157
+ textAlign,
158
+ ...wrapperClamp,
159
+ },
160
+ '& .MuiTableSortLabel-root': {
161
+ whiteSpace: 'normal',
162
+ wordBreak: 'break-word',
163
+ width: '100%',
164
+ margin: 0,
165
+ },
166
+ };
167
+ };
14
168
  // ─── Internal: overlay loader ─────────────────────────────────────────────────
15
169
  const OverlayLoader = ({ message }) => (jsxs(Box, { sx: {
16
170
  position: 'absolute', inset: 0,
@@ -19,31 +173,41 @@ const OverlayLoader = ({ message }) => (jsxs(Box, { sx: {
19
173
  bgcolor: 'rgba(255,255,255,0.80)', backdropFilter: 'blur(2px)',
20
174
  zIndex: 10, gap: 1.5,
21
175
  }, children: [jsx(CircularProgress, { size: 50, thickness: 4 }), jsx(Typography, { variant: "body2", color: "text.secondary", fontWeight: 500, children: message })] }));
22
- const PaginationFooter = ({ pageIndex, pageSize, totalRecords, isFetching, isLoading, canPrev, canNext, onFirst, onPrev, onNext, onLast, onPageSizeChange, pageSizeOptions, }) => {
176
+ const PaginationFooter = ({ pageIndex, pageSize, totalRecords, isFetching, isLoading, canPrev, canNext, onFirst, onPrev, onNext, onLast, onPageSizeChange, pageSizeOptions, density, }) => {
177
+ const fs = density.footerFontSize;
178
+ const selectH = density.footerMinHeight - 8;
23
179
  const startRow = totalRecords === 0 ? 0 : pageIndex * pageSize + 1;
24
180
  const endRow = Math.min((pageIndex + 1) * pageSize, totalRecords);
25
181
  return (jsxs(Box, { sx: {
26
182
  display: 'flex', alignItems: 'center', justifyContent: 'flex-end',
27
- gap: { xs: 1.5, md: 3 }, px: 2, py: 0.75,
183
+ gap: { xs: 1, md: 2 }, px: 1.5, py: density.footerPaddingY,
28
184
  borderTop: '1px solid', borderColor: 'divider',
29
- bgcolor: 'background.paper', flexWrap: 'wrap', minHeight: 48,
30
- }, children: [jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1 }, children: [jsx(Typography, { variant: "body2", color: "text.secondary", noWrap: true, children: "Rows per page:" }), jsx(Select, { value: pageSize, onChange: (e) => onPageSizeChange(Number(e.target.value)), size: "small", variant: "outlined", sx: {
31
- height: 28, fontSize: 13, minWidth: 80,
32
- '.MuiSelect-select': { py: '3px !important' },
185
+ bgcolor: 'background.paper', flexWrap: 'wrap',
186
+ minHeight: density.footerMinHeight,
187
+ flexShrink: 0,
188
+ }, children: [jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 0.75 }, children: [jsx(Typography, { sx: { fontSize: fs }, color: "text.secondary", noWrap: true, children: "Rows per page:" }), jsx(Select, { value: pageSize, onChange: (e) => onPageSizeChange(Number(e.target.value)), size: "small", variant: "outlined", sx: {
189
+ height: selectH, fontSize: fs - 1, minWidth: 72,
190
+ '.MuiSelect-select': { py: '2px !important' },
33
191
  '.MuiOutlinedInput-notchedOutline': { borderColor: 'divider' },
34
- }, children: pageSizeOptions.map((opt) => (jsx(MenuItem, { value: opt, sx: { fontSize: 13 }, children: opt.toLocaleString() }, opt))) })] }), jsx(Typography, { variant: "body2", color: "text.secondary", noWrap: true, children: isLoading || (isFetching && totalRecords === 0)
192
+ }, children: pageSizeOptions.map((opt) => (jsx(MenuItem, { value: opt, sx: { fontSize: fs - 1 }, children: opt.toLocaleString() }, opt))) })] }), jsx(Typography, { sx: { fontSize: fs }, color: "text.secondary", noWrap: true, children: isLoading || (isFetching && totalRecords === 0)
35
193
  ? 'Loading…'
36
194
  : totalRecords === 0
37
195
  ? 'No records'
38
- : `${startRow.toLocaleString()}–${endRow.toLocaleString()} of ${totalRecords.toLocaleString()}` }), jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 0.25 }, children: [jsx(Tooltip, { title: "First page", children: jsx("span", { children: jsx(IconButton, { size: "small", onClick: onFirst, disabled: !canPrev, children: jsx(FirstPageIcon, { sx: { fontSize: 20 } }) }) }) }), jsx(Tooltip, { title: "Previous page", children: jsx("span", { children: jsx(IconButton, { size: "small", onClick: onPrev, disabled: !canPrev, children: jsx(ChevronLeftIcon, { sx: { fontSize: 20 } }) }) }) }), jsx(Typography, { variant: "body2", fontWeight: 600, sx: {
39
- mx: 0.5, minWidth: 28, height: 28,
196
+ : `${startRow.toLocaleString()}–${endRow.toLocaleString()} of ${totalRecords.toLocaleString()}` }), jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 0.25 }, children: [jsx(Tooltip, { title: "First page", children: jsx("span", { children: jsx(IconButton, { size: "small", onClick: onFirst, disabled: !canPrev, sx: { p: 0.5 }, children: jsx(FirstPageIcon, { sx: { fontSize: density.footerIconSize } }) }) }) }), jsx(Tooltip, { title: "Previous page", children: jsx("span", { children: jsx(IconButton, { size: "small", onClick: onPrev, disabled: !canPrev, sx: { p: 0.5 }, children: jsx(ChevronLeftIcon, { sx: { fontSize: density.footerIconSize } }) }) }) }), jsx(Typography, { fontWeight: 600, sx: {
197
+ mx: 0.5,
198
+ minWidth: density.footerMinHeight - 12,
199
+ height: density.footerMinHeight - 12,
40
200
  display: 'flex', alignItems: 'center', justifyContent: 'center',
41
201
  borderRadius: 1, border: '1px solid', borderColor: 'primary.main',
42
- color: 'primary.main', bgcolor: 'rgba(25,118,210,0.07)', fontSize: 13,
43
- }, children: pageIndex + 1 }), jsx(Tooltip, { title: "Next page", children: jsx("span", { children: jsx(IconButton, { size: "small", onClick: onNext, disabled: !canNext, children: jsx(ChevronRightIcon, { sx: { fontSize: 20 } }) }) }) }), jsx(Tooltip, { title: "Last page", children: jsx("span", { children: jsx(IconButton, { size: "small", onClick: onLast, disabled: !canNext, children: jsx(LastPageIcon, { sx: { fontSize: 20 } }) }) }) })] })] }));
202
+ color: 'primary.main', bgcolor: 'rgba(25,118,210,0.07)', fontSize: fs - 1,
203
+ }, children: pageIndex + 1 }), jsx(Tooltip, { title: "Next page", children: jsx("span", { children: jsx(IconButton, { size: "small", onClick: onNext, disabled: !canNext, sx: { p: 0.5 }, children: jsx(ChevronRightIcon, { sx: { fontSize: density.footerIconSize } }) }) }) }), jsx(Tooltip, { title: "Last page", children: jsx("span", { children: jsx(IconButton, { size: "small", onClick: onLast, disabled: !canNext, sx: { p: 0.5 }, children: jsx(LastPageIcon, { sx: { fontSize: density.footerIconSize } }) }) }) })] })] }));
44
204
  };
45
205
  // ─── GenericPaginatedGrid ─────────────────────────────────────────────────────
46
- function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs, defaultFilters, defaultVisibleFilterIds, batchSize = 10000, exportFilename = 'export', tableHeight, pageSizeOptions = [1000, 5000, 10000], headerBgColor, maxVisibleFilters = 4, }) {
206
+ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs, defaultFilters, defaultVisibleFilterIds, batchSize = 10000, exportFilename = 'export', tableHeight, tableMinHeight = '300px', tableMaxHeight, autoFillViewport = true, layoutMode = 'grid', uiDensity = 'default', uiDensityOptions, pageSizeOptions = [1000, 5000, 10000], headerBgColor, headerMinHeight, headerMaxHeight, headerMaxLines, maxVisibleFilters = 4, }) {
207
+ const densityTokens = useMemo(() => ({ ...UI_DENSITY_PRESETS[uiDensity], ...uiDensityOptions }), [uiDensity, uiDensityOptions]);
208
+ const resolvedHeaderMinHeight = toCssSize(headerMinHeight, uiDensity === 'compact' ? '34px' : '40px');
209
+ const resolvedHeaderMaxHeight = toCssSize(headerMaxHeight, uiDensity === 'compact' ? '56px' : '72px');
210
+ const headerGlobalStyleOverrides = useMemo(() => buildHeaderGlobalStyles(resolvedHeaderMinHeight, resolvedHeaderMaxHeight, headerBgColor), [resolvedHeaderMinHeight, resolvedHeaderMaxHeight, headerBgColor]);
47
211
  const defaultVisible = defaultVisibleFilterIds
48
212
  ?? (serverFilterDefs.length > 0 ? [serverFilterDefs[0].id] : []);
49
213
  // ── State ──────────────────────────────────────────────────────────────────
@@ -57,35 +221,35 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
57
221
  const [columnFilters, setColumnFilters] = useState([]);
58
222
  const [globalFilter, setGlobalFilter] = useState('');
59
223
  const [columnPinning, setColumnPinning] = useState({});
60
- // ── Viewport-fill: measure this component's top offset and derive height ──────
224
+ // ── Viewport-fill height (mount + resize only — never on scroll) ─────────────
61
225
  //
62
- // When tableHeight is not provided, we measure how far the wrapper sits from
63
- // the top of the viewport and set height = 100vh − topOffset − 8px (tiny
64
- // bottom margin). A ResizeObserver on <html> keeps the value in sync if the
65
- // layout shifts (sidebar collapses, font loads, etc.).
66
- //
67
- // The MRT Paper + TableContainer flex chain distributes that height between
68
- // the toolbar, sticky column headers, scrollable rows, and pagination footer —
69
- // so the grid fills the window exactly and the page never needs to scroll.
226
+ // Previously a scroll listener recalculated height as the grid moved within the
227
+ // viewport, which grew the grid while scrolling and caused double scrollbars.
70
228
  const containerRef = useRef(null);
71
229
  const [autoHeight, setAutoHeight] = useState('');
230
+ const shouldAutoFill = autoFillViewport && !tableHeight;
72
231
  useLayoutEffect(() => {
73
- if (tableHeight)
74
- return; // explicit prop — skip auto calculation
232
+ if (!shouldAutoFill)
233
+ return;
75
234
  const el = containerRef.current;
76
235
  if (!el)
77
236
  return;
78
237
  const update = () => {
79
238
  const top = el.getBoundingClientRect().top;
80
- setAutoHeight(`calc(100vh - ${Math.ceil(top) + 8}px)`);
239
+ const preferred = `calc(100vh - ${Math.ceil(top) + 8}px)`;
240
+ const maxPart = tableMaxHeight ?? preferred;
241
+ setAutoHeight(`clamp(${tableMinHeight}, ${preferred}, ${maxPart})`);
81
242
  };
82
243
  update();
83
244
  const ro = new ResizeObserver(update);
84
- ro.observe(document.documentElement);
85
- return () => ro.disconnect();
86
- }, [tableHeight]);
87
- // Resolved height: explicit prop › auto-calculated › safe fallback
88
- const resolvedHeight = tableHeight || autoHeight || '60vh';
245
+ ro.observe(el);
246
+ window.addEventListener('resize', update, { passive: true });
247
+ return () => {
248
+ ro.disconnect();
249
+ window.removeEventListener('resize', update);
250
+ };
251
+ }, [shouldAutoFill, tableMinHeight, tableMaxHeight]);
252
+ const resolvedHeight = tableHeight || (shouldAutoFill ? autoHeight : undefined) || tableMinHeight;
89
253
  // ── Data fetching ──────────────────────────────────────────────────────────
90
254
  const { data, isLoading, isError, isFetching, refetch } = useQuery({
91
255
  queryKey: [...queryKey, filterValues, pagination.pageIndex],
@@ -101,9 +265,13 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
101
265
  // MRT doesn't know these props, so we strip them here. We also inject the
102
266
  // appropriate `filterFn` automatically so callers never have to write one.
103
267
  const processedColumns = useMemo(() => {
268
+ const allowGrow = layoutMode === 'grid' || layoutMode === 'semantic';
104
269
  return columns.map((col) => {
105
270
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
106
271
  const { filterType, options, ...mrtCol } = col;
272
+ if (allowGrow && mrtCol.grow === undefined) {
273
+ mrtCol.grow = true;
274
+ }
107
275
  // Auto-inject filterFn only when the user hasn't already supplied one.
108
276
  if (!mrtCol.filterFn && filterType) {
109
277
  if (filterType === 'date') {
@@ -122,7 +290,7 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
122
290
  }
123
291
  return mrtCol;
124
292
  });
125
- }, [columns]);
293
+ }, [columns, layoutMode]);
126
294
  // ── Derive column list for filter chip picker ──────────────────────────────
127
295
  //
128
296
  // A column is excluded from the "Column Filters" section of Manage Filters
@@ -184,12 +352,26 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
184
352
  a.click();
185
353
  URL.revokeObjectURL(url);
186
354
  };
187
- const hasAnyValue = Object.values(filterValues).some(Boolean) ||
188
- columnFilters.some((f) => f.value);
189
355
  // ── Table instance ─────────────────────────────────────────────────────────
190
356
  const table = useMaterialReactTable({
191
357
  columns: processedColumns,
192
358
  data: rows,
359
+ layoutMode,
360
+ displayColumnDefOptions: {
361
+ 'mrt-row-spacer': {
362
+ size: 0,
363
+ minSize: 0,
364
+ maxSize: 0,
365
+ grow: false,
366
+ enableResizing: false,
367
+ muiTableHeadCellProps: {
368
+ sx: { display: 'none !important', width: 0, maxWidth: 0, p: 0, border: 'none' },
369
+ },
370
+ muiTableBodyCellProps: {
371
+ sx: { display: 'none !important', width: 0, maxWidth: 0, p: 0, border: 'none' },
372
+ },
373
+ },
374
+ },
193
375
  manualPagination: true,
194
376
  rowCount: totalRecords,
195
377
  manualSorting: false,
@@ -225,71 +407,76 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
225
407
  enableHiding: true,
226
408
  enableStickyHeader: true,
227
409
  enableRowVirtualization: true,
228
- // The container fills whatever height is left after the top toolbar and
229
- // pagination footer. min-height: 0 is required so the flex child can
230
- // shrink below its natural content height and scroll its rows instead.
410
+ initialState: {
411
+ density: densityTokens.mrtDensity,
412
+ },
413
+ muiTableProps: {
414
+ sx: { width: '100%' },
415
+ },
231
416
  muiTableContainerProps: {
232
417
  sx: {
233
418
  flex: '1 1 0',
234
419
  minHeight: 0,
420
+ maxHeight: '100%',
421
+ overflow: 'auto',
235
422
  },
236
423
  },
237
- // Paper fills the wrapper Box height and acts as the flex column host for
238
- // toolbar → table-container → pagination.
239
424
  muiTablePaperProps: {
240
425
  sx: {
241
426
  height: '100%',
427
+ minHeight: 0,
242
428
  display: 'flex',
243
429
  flexDirection: 'column',
430
+ overflow: 'hidden',
431
+ boxShadow: 'none',
244
432
  },
245
433
  },
246
434
  muiTopToolbarProps: {
247
435
  sx: {
248
436
  alignItems: 'center',
249
437
  flexWrap: 'nowrap',
438
+ minHeight: densityTokens.filterChipMinHeight + 12,
439
+ py: uiDensity === 'compact' ? 0.25 : 0.5,
440
+ '& .MuiIconButton-root': { p: uiDensity === 'compact' ? 0.35 : 0.5 },
441
+ '& .MuiIconButton-root svg': { fontSize: densityTokens.toolbarIconFontSize === 'small' ? 18 : 20 },
442
+ },
443
+ },
444
+ muiTableHeadProps: {
445
+ sx: {
446
+ position: 'sticky',
447
+ top: 0,
448
+ zIndex: 2,
449
+ ...(headerBgColor ? { backgroundColor: headerBgColor } : {}),
450
+ },
451
+ },
452
+ muiTableHeadRowProps: {
453
+ sx: {
454
+ height: resolvedHeaderMaxHeight,
455
+ minHeight: resolvedHeaderMinHeight,
456
+ maxHeight: resolvedHeaderMaxHeight,
457
+ alignItems: 'stretch',
458
+ ...(headerBgColor ? { backgroundColor: headerBgColor } : {}),
250
459
  },
251
460
  },
252
461
  muiTableHeadCellProps: ({ column }) => {
253
462
  const colProps = column.columnDef.muiTableHeadCellProps;
254
- const align = colProps && typeof colProps !== 'function'
255
- ? colProps.align
256
- : undefined;
257
- const justifyContent = align === 'right' ? 'flex-start'
258
- : align === 'center' ? 'center'
259
- : 'flex-end';
463
+ let align = 'left';
464
+ let colSx;
465
+ if (typeof colProps === 'function') {
466
+ const resolved = colProps({ column });
467
+ align = resolved?.align ?? 'left';
468
+ colSx = resolved?.sx;
469
+ }
470
+ else if (colProps) {
471
+ align = colProps.align ?? 'left';
472
+ colSx = colProps.sx;
473
+ }
260
474
  return {
261
- align: (align ?? 'left'),
262
- sx: {
263
- ...(headerBgColor ? { backgroundColor: headerBgColor } : {}),
264
- verticalAlign: 'bottom',
265
- whiteSpace: 'normal',
266
- borderRight: '1px solid rgba(224,224,224,1)',
267
- borderBottom: '2px solid rgba(0,0,0,0.15)',
268
- '& button[aria-label="Move"]': { display: 'none !important' },
269
- '& .MuiTableSortLabel-icon': { display: 'none !important' },
270
- '& .Mui-TableHeadCell-ResizeHandle-Wrapper': {
271
- height: '100%',
272
- top: 0,
273
- },
274
- '& .Mui-TableHeadCell-ResizeHandle-Divider': {
275
- borderRight: 'none',
276
- },
277
- '& .Mui-TableHeadCell-Content': {
278
- alignItems: 'flex-end',
279
- justifyContent,
280
- },
281
- '& .Mui-TableHeadCell-Content-Labels': {
282
- flexWrap: 'wrap',
283
- whiteSpace: 'normal',
284
- overflowWrap: 'normal',
285
- lineHeight: 1.3,
286
- justifyContent,
287
- },
288
- '& .Mui-TableHeadCell-Content-Wrapper': {
289
- whiteSpace: 'normal',
290
- overflow: 'visible',
291
- },
292
- },
475
+ align,
476
+ sx: [
477
+ buildHeaderCellSx(align, resolvedHeaderMinHeight, resolvedHeaderMaxHeight, headerBgColor, headerMaxLines),
478
+ ...(colSx ? [colSx] : []),
479
+ ],
293
480
  };
294
481
  },
295
482
  muiTableBodyCellProps: {
@@ -301,27 +488,25 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
301
488
  muiToolbarAlertBannerProps: isError
302
489
  ? { color: 'error', children: 'Failed to load data. Please try again.' }
303
490
  : undefined,
304
- renderTopToolbarCustomActions: () => (jsxs(Box, { sx: { display: 'flex', alignItems: 'center', flex: 1, minWidth: 0, pr: 1, gap: 0.5 }, children: [jsxs(Box, { sx: { display: 'flex', alignItems: 'center', flexShrink: 0, gap: 0.25 }, children: [jsx(Tooltip, { title: "Refresh current batch", children: jsx("span", { children: jsx(IconButton, { size: "small", onClick: () => refetch(), disabled: isFetching, children: jsx(RefreshIcon, { fontSize: "small" }) }) }) }), jsx(Tooltip, { title: "Export batch as CSV", children: jsx("span", { children: jsx(IconButton, { size: "small", onClick: handleExport, disabled: rows.length === 0 || isFetching, children: jsx(FileDownloadIcon, { fontSize: "small" }) }) }) })] }), jsx(Box, { sx: { flex: 1, minWidth: 0 }, children: jsx(DynamicFilterBar, { serverFilterDefs: serverFilterDefs, visibleFilterIds: visibleFilterIds, onToggleFilter: handleToggleFilter, filterValues: filterValues, onFilterValuesChange: handleFilterValuesChange, columnFilters: columnFilters, onColumnFilterChange: setColumnFilters, availableColumns: availableColumns, maxVisibleFilters: maxVisibleFilters }) }), hasAnyValue && (jsx(Button, { size: "small", onClick: handleClearAll, sx: {
305
- flexShrink: 0,
306
- textTransform: 'none',
307
- fontSize: 12,
308
- color: 'error.main',
309
- minWidth: 'auto',
310
- px: 1,
311
- '&:hover': { bgcolor: 'rgba(211,47,47,0.06)' },
312
- }, children: "Clear All" }))] })),
313
- renderBottomToolbar: ({ table: t }) => (jsx(PaginationFooter, { pageIndex: t.getState().pagination.pageIndex, pageSize: t.getState().pagination.pageSize, totalRecords: totalRecords, isFetching: isFetching, isLoading: isLoading, canPrev: t.getCanPreviousPage() && !isFetching, canNext: t.getCanNextPage() && !isFetching, onFirst: () => t.firstPage(), onPrev: () => t.previousPage(), onNext: () => t.nextPage(), onLast: () => t.lastPage(), onPageSizeChange: (s) => t.setPageSize(s), pageSizeOptions: pageSizeOptions })),
491
+ renderTopToolbarCustomActions: () => (jsxs(Box, { sx: { display: 'flex', alignItems: 'center', flex: 1, minWidth: 0, pr: 1, gap: 0.5 }, children: [jsxs(Box, { sx: { display: 'flex', alignItems: 'center', flexShrink: 0, gap: 0.25 }, children: [jsx(Tooltip, { title: "Clear filters & refresh", children: jsx("span", { children: jsx(IconButton, { size: "small", onClick: () => { handleClearAll(); refetch(); }, disabled: isFetching, sx: { p: uiDensity === 'compact' ? 0.35 : 0.5 }, children: jsx(RefreshIcon, { sx: { fontSize: uiDensity === 'compact' ? 18 : 20 } }) }) }) }), jsx(Tooltip, { title: "Export batch as CSV", children: jsx("span", { children: jsx(IconButton, { size: "small", onClick: handleExport, disabled: rows.length === 0 || isFetching, sx: { p: uiDensity === 'compact' ? 0.35 : 0.5 }, children: jsx(FileDownloadIcon, { sx: { fontSize: uiDensity === 'compact' ? 18 : 20 } }) }) }) })] }), jsx(Box, { sx: { flex: 1, minWidth: 0 }, children: jsx(DynamicFilterBar, { serverFilterDefs: serverFilterDefs, visibleFilterIds: visibleFilterIds, onToggleFilter: handleToggleFilter, filterValues: filterValues, onFilterValuesChange: handleFilterValuesChange, columnFilters: columnFilters, onColumnFilterChange: setColumnFilters, availableColumns: availableColumns, maxVisibleFilters: maxVisibleFilters, uiDensity: uiDensity, filterChipFontSize: densityTokens.filterChipFontSize, filterChipMinHeight: densityTokens.filterChipMinHeight, filterLabelFontSize: densityTokens.filterLabelFontSize }) })] })),
492
+ renderBottomToolbar: ({ table: t }) => (jsx(PaginationFooter, { pageIndex: t.getState().pagination.pageIndex, pageSize: t.getState().pagination.pageSize, totalRecords: totalRecords, isFetching: isFetching, isLoading: isLoading, canPrev: t.getCanPreviousPage() && !isFetching, canNext: t.getCanNextPage() && !isFetching, onFirst: () => t.firstPage(), onPrev: () => t.previousPage(), onNext: () => t.nextPage(), onLast: () => t.lastPage(), onPageSizeChange: (s) => t.setPageSize(s), pageSizeOptions: pageSizeOptions, density: densityTokens })),
314
493
  });
315
494
  return (jsxs(Box, { ref: containerRef, className: "tp-gpg-root", sx: {
316
495
  position: 'relative',
317
496
  height: resolvedHeight,
497
+ minHeight: tableMinHeight,
498
+ maxHeight: tableMaxHeight,
318
499
  display: 'flex',
319
500
  flexDirection: 'column',
501
+ overflow: 'hidden',
320
502
  }, children: [jsx(GlobalStyles, { styles: {
321
503
  'button[aria-label="Pin to right"]': { display: 'none' },
322
504
  '.tp-gpg-root .MuiTableSortLabel-icon': { display: 'none !important' },
323
505
  '.tp-gpg-root button[aria-label="Move"]': { display: 'none !important' },
324
- } }), isLoading && jsx(OverlayLoader, { message: "Loading data\u2026" }), !isLoading && isFetching && jsx(OverlayLoader, { message: "Updating data\u2026" }), jsx(MaterialReactTable, { table: table })] }));
506
+ '.tp-gpg-root [data-column-id="mrt-row-spacer"]': { display: 'none !important' },
507
+ '.tp-gpg-root .Mui-TableHeadCell-ColumnSpacer': { display: 'none !important' },
508
+ ...headerGlobalStyleOverrides,
509
+ } }), isLoading && jsx(OverlayLoader, { message: "Loading data\u2026" }), !isLoading && isFetching && jsx(OverlayLoader, { message: "Updating data\u2026" }), jsx(Box, { sx: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }, children: jsx(MaterialReactTable, { table: table }) })] }));
325
510
  }
326
511
 
327
512
  export { GenericPaginatedGrid as default };
@@ -1,4 +1,5 @@
1
1
  export { default as GenericPaginatedGrid } from './GenericPaginatedGrid';
2
- export type { GenericPaginatedGridProps, PagedResult } from './GenericPaginatedGrid';
2
+ export type { GenericPaginatedGridProps, PagedResult, GridUiDensity, GridUiDensityOptions, } from './GenericPaginatedGrid';
3
3
  export { default as DynamicFilterBar } from './DynamicFilterBar';
4
4
  export type { ServerFilterDef } from './DynamicFilterBar';
5
+ export type { TPColumnDef, ColumnFilterType } from './types';
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Mock data + fetch adapters for Storybook sample grids (no API required).
3
+ */
4
+ import type { TPColumnDef } from './types';
5
+ import type { PagedResult } from './GenericPaginatedGrid';
6
+ import type { ServerFilterDef } from './DynamicFilterBar';
7
+ export interface EmployeeRow {
8
+ id: number;
9
+ name: string;
10
+ department: string;
11
+ costCenter: string;
12
+ workLocation: string;
13
+ reportingManager: string;
14
+ employmentType: string;
15
+ status: string;
16
+ joinedOn: string;
17
+ }
18
+ /** Columns with long headers + narrow widths to exercise header wrapping in Storybook. */
19
+ export declare const employeeColumns: TPColumnDef<EmployeeRow>[];
20
+ export declare const employeeFilterDefs: ServerFilterDef[];
21
+ export declare const employeeDefaultFilters: Record<string, string>;
22
+ export declare function fetchEmployeePage(filters: Record<string, string>, pageIndex: number, pageSize?: number): Promise<PagedResult<EmployeeRow>>;
23
+ export interface ProductRow {
24
+ sku: string;
25
+ title: string;
26
+ category: string;
27
+ price: number;
28
+ inStock: boolean;
29
+ }
30
+ export declare const productColumns: TPColumnDef<ProductRow>[];
31
+ export declare const productFilterDefs: ServerFilterDef[];
32
+ export declare const productDefaultFilters: Record<string, string>;
33
+ export declare function fetchProductPage(filters: Record<string, string>, pageIndex: number, pageSize?: number): Promise<PagedResult<ProductRow>>;
package/dist/index.d.ts CHANGED
@@ -15,3 +15,4 @@ export * as Theme from './theme';
15
15
  export type { FormRenderProps } from './utils';
16
16
  export type { FormRenderWrapperProps } from './components/Form/FormRenderWrapper';
17
17
  export type { FormSectionPropsItem, VariantProps } from './utils';
18
+ export type { GenericPaginatedGridProps, GridUiDensity, GridUiDensityOptions, PagedResult, ServerFilterDef, TPColumnDef, } from './components/PaginatedGrid';
@@ -1,4 +1,4 @@
1
- import { __exports as DateRange } from '../../../../../_virtual/index.esm7.js';
1
+ import { __exports as DateRange } from '../../../../../_virtual/index.esm6.js';
2
2
  import React__default from 'react';
3
3
  import { __require as requirePropTypes } from '../../../../prop-types/index.esm.js';
4
4
  import { __require as requireCalendar } from '../Calendar/index.esm.js';
@@ -1,4 +1,4 @@
1
- import { __exports as DefinedRange } from '../../../../../_virtual/index.esm6.js';
1
+ import { __exports as DefinedRange } from '../../../../../_virtual/index.esm7.js';
2
2
  import React__default from 'react';
3
3
  import { __require as requirePropTypes } from '../../../../prop-types/index.esm.js';
4
4
  import { __require as requireStyles } from '../../styles.esm.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tp-react-elements",
3
- "version": "1.3.9",
3
+ "version": "1.3.11",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "React form components library built with React Hook Form and Yup",