tp-react-elements 3.0.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,3 @@
1
- var Month = {};
1
+ var DateInput = {};
2
2
 
3
- export { Month as __exports };
3
+ export { DateInput as __exports };
@@ -1,3 +1,3 @@
1
- var DateInput = {};
1
+ var Month = {};
2
2
 
3
- export { DateInput as __exports };
3
+ export { Month as __exports };
@@ -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 Calendar = {};
1
+ var DefinedRange = {};
2
2
 
3
- export { Calendar as __exports };
3
+ export { DefinedRange as __exports };
@@ -1,3 +1,3 @@
1
- var DateRange = {};
1
+ var Calendar = {};
2
2
 
3
- export { DateRange as __exports };
3
+ export { Calendar as __exports };
@@ -1,3 +1,4 @@
1
1
  export { default as DatepickerWrapperV2 } from '../FormComponents/DatePicker/DatepickerWrapperV2';
2
2
  export { default as Monthpickerrender } from '../FormComponents/DatePicker/Monthpickerrender';
3
+ export { default as MonthPickerMultiRender } from '../FormComponents/DatePicker/MonthPickerMultiRender';
3
4
  export { default as YearPickerField } from '../FormComponents/YearPickerField/YearPickerField';
@@ -1,7 +1,8 @@
1
1
  import DatepickerWrapperV2 from '../FormComponents/DatePicker/DatepickerWrapperV2.esm.js';
2
2
  import Monthpickerrender from '../FormComponents/DatePicker/Monthpickerrender.esm.js';
3
+ import MonthPickerMultiRender from '../FormComponents/DatePicker/MonthPickerMultiRender.esm.js';
3
4
  export { default as YearPickerField } from '../FormComponents/YearPickerField/YearPickerField.esm.js';
4
5
 
5
6
 
6
7
 
7
- export { DatepickerWrapperV2, Monthpickerrender };
8
+ export { DatepickerWrapperV2, MonthPickerMultiRender, Monthpickerrender };
@@ -282,6 +282,7 @@ const useFormValidatingContext = (formArray) => {
282
282
  renderCustomError(field);
283
283
  break;
284
284
  case 'monthpicker':
285
+ case 'monthpicker-multi':
285
286
  initialValues[field.name] = null;
286
287
  if (field.required && field.errorMessage) {
287
288
  validationShape[field.name] = (validationShape[field.name] || Yup.string())
@@ -1,5 +1,6 @@
1
1
  import { jsx, Fragment } from 'react/jsx-runtime';
2
2
  import require$$0__default, { lazy, Suspense } from 'react';
3
+ import FormCheckBoxGroup from '../FormComponents/FormCheckBoxGroup/FormCheckBoxGroup.esm.js';
3
4
  import SingleSelect from '../FormComponents/Select/SingleSelect.esm.js';
4
5
  import MultiSelectAutocomplete from '../FormComponents/Select/MultiSelectAutocomplete.esm.js';
5
6
  import SingleSelectNonAutoComplete from '../FormComponents/Select/SingleSelectNonAutoComplete.esm.js';
@@ -13,7 +14,6 @@ import DatetimepickerWrapper from '../FormComponents/DatePicker/DatetimepickerWr
13
14
  import FormTextAreaField from '../FormComponents/FormTextAreaField/FormTextAreaField.esm.js';
14
15
  import FormCheckBox from '../FormComponents/FormCheckBox/FormCheckBox.esm.js';
15
16
  import FormRadioGroup from '../FormComponents/FormRadioGroup/FormRadioGroup.esm.js';
16
- import FormCheckBoxGroup from '../FormComponents/FormCheckBoxGroup/FormCheckBoxGroup.esm.js';
17
17
  import FormNumberFieldDecimal from '../FormComponents/FormNumberField/FormNumberFieldDecimal.esm.js';
18
18
  import FormNumberField from '../FormComponents/FormNumberField/FormNumberField.esm.js';
19
19
  import PasswordField from '../FormComponents/PasswordField/PasswordField.esm.js';
@@ -30,6 +30,7 @@ const FormRenderMultiFileUpload = lazy(() => import('../FileUpload/index.esm.js'
30
30
  const MultiFileWithPreview = lazy(() => import('../FileUpload/index.esm.js').then((m) => ({ default: m.MultiFileWithPreview })));
31
31
  const DatepickerWrapperV2 = lazy(() => import('../DatePicker/index.esm.js').then((m) => ({ default: m.DatepickerWrapperV2 })));
32
32
  const Monthpickerrender = lazy(() => import('../DatePicker/index.esm.js').then((m) => ({ default: m.Monthpickerrender })));
33
+ const MonthPickerMultiRender = lazy(() => import('../DatePicker/index.esm.js').then((m) => ({ default: m.MonthPickerMultiRender })));
33
34
  const TimePickerFieldWrapper = lazy(() => import('../TimePicker/index.esm.js').then((m) => ({ default: m.TimePicker })));
34
35
  // Loading fallback component
35
36
  const LoadingFallback = () => jsx("div", { style: { minHeight: '36px' }, children: "Loading..." });
@@ -119,9 +120,12 @@ const RenderForm = (props) => {
119
120
  // Time picker
120
121
  case 'timepicker':
121
122
  return (jsx(Suspense, { fallback: jsx(LoadingFallback, {}), children: jsx(TimePickerFieldWrapper, { props: props, variant: variant }) }));
122
- // Month picker
123
+ // Month picker (single month)
123
124
  case 'monthpicker':
124
125
  return (jsx(Suspense, { fallback: jsx(LoadingFallback, {}), children: jsx(Monthpickerrender, { props: props, variant: variant }) }));
126
+ // Month picker (multiple months)
127
+ case 'monthpicker-multi':
128
+ return (jsx(Suspense, { fallback: jsx(LoadingFallback, {}), children: jsx(MonthPickerMultiRender, { props: props, variant: variant }) }));
125
129
  // Multi-select dropdown
126
130
  case 'multiselect':
127
131
  return (jsx(Suspense, { fallback: jsx(LoadingFallback, {}), children: jsx(MultiSelectV1, { props: props, variant: variant }) }));
@@ -0,0 +1,6 @@
1
+ import { FormRenderProps, VariantProps } from '../../../utils';
2
+ declare const MonthPickerMultiRender: ({ props, variant, }: {
3
+ props: FormRenderProps;
4
+ variant: VariantProps;
5
+ }) => import("react/jsx-runtime").JSX.Element;
6
+ export default MonthPickerMultiRender;
@@ -0,0 +1,117 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import { useState, useMemo, useCallback } from 'react';
3
+ import { FormControl, Box, TextField, InputAdornment, Popover, IconButton, Typography, Button } from '@mui/material';
4
+ import CalendarTodayIcon from '@mui/icons-material/CalendarToday';
5
+ import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
6
+ import ChevronRightIcon from '@mui/icons-material/ChevronRight';
7
+ import { useWatch } from 'react-hook-form';
8
+ import FormBottomField from '../FormBottomField/FormBottomField.esm.js';
9
+ import 'dayjs';
10
+ import { renderLabel } from '../../../utils/Constants/FormConstants.esm.js';
11
+
12
+ const MONTH_LABELS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
13
+ const parseMY = (s) => {
14
+ const m = /^(\d{2})\/(\d{4})$/.exec(s.trim());
15
+ return m ? [Number(m[1]), Number(m[2])] : null; // [month, year]
16
+ };
17
+ const toKey = (month, year) => year * 100 + month; // sortable comparable int
18
+ const toMY = (month, year) => `${String(month).padStart(2, '0')}/${year}`;
19
+ const parseValue = (value) => value ? value.split(',').map((s) => s.trim()).filter(Boolean) : [];
20
+ const MonthPickerMultiRender = ({ props, variant, }) => {
21
+ const [anchorEl, setAnchorEl] = useState(null);
22
+ const [viewYear, setViewYear] = useState(() => new Date().getFullYear());
23
+ const [draft, setDraft] = useState([]);
24
+ const value = useWatch({ control: props.control, name: props.item.name });
25
+ const committed = useMemo(() => parseValue(value), [value]);
26
+ const isError = !!props.errors?.[props.item.name];
27
+ const minKey = useMemo(() => {
28
+ const parsed = props.item.minDate ? parseMY(props.item.minDate) : null;
29
+ return parsed ? toKey(parsed[0], parsed[1]) : null;
30
+ }, [props.item.minDate]);
31
+ const maxKey = useMemo(() => {
32
+ const parsed = props.item.maxDate ? parseMY(props.item.maxDate) : null;
33
+ return parsed ? toKey(parsed[0], parsed[1]) : null;
34
+ }, [props.item.maxDate]);
35
+ const handleOpen = useCallback((e) => {
36
+ if (props.item.disable)
37
+ return;
38
+ const committedNow = parseValue(value);
39
+ setDraft(committedNow);
40
+ if (committedNow.length > 0) {
41
+ const years = committedNow.map((s) => Number(s.split('/')[1]));
42
+ setViewYear(Math.min(...years));
43
+ }
44
+ setAnchorEl(e.currentTarget);
45
+ }, [props.item.disable, value]);
46
+ const handleClose = useCallback(() => setAnchorEl(null), []);
47
+ const toggleMonth = useCallback((my) => {
48
+ setDraft((prev) => (prev.includes(my) ? prev.filter((v) => v !== my) : [...prev, my]));
49
+ }, []);
50
+ const handleApply = useCallback(() => {
51
+ const sorted = [...draft].sort((a, b) => {
52
+ const [am, ay] = a.split('/').map(Number);
53
+ const [bm, by] = b.split('/').map(Number);
54
+ return toKey(am, ay) - toKey(bm, by);
55
+ });
56
+ const next = sorted.join(',');
57
+ props.setValue(props.item.name, next);
58
+ props.item.onChangeFn && props.item.onChangeFn(next);
59
+ props.clearErrors && props.clearErrors(props.item.name);
60
+ setAnchorEl(null);
61
+ }, [draft, props]);
62
+ const displayValue = useMemo(() => {
63
+ if (committed.length === 0)
64
+ return '';
65
+ if (committed.length <= 3) {
66
+ return committed
67
+ .map((my) => {
68
+ const [m, y] = my.split('/').map(Number);
69
+ return `${MONTH_LABELS[m - 1]} ${y}`;
70
+ })
71
+ .join(', ');
72
+ }
73
+ return `${committed.length} months selected`;
74
+ }, [committed]);
75
+ const muiLabel = variant !== 'standard' ? `${props.item.label}${props.item.required ? ' *' : ''}` : '';
76
+ return (jsxs(FormControl, { fullWidth: true, sx: { position: 'relative' }, disabled: props.item.disable, children: [renderLabel(variant, props), jsx(Box, { onClick: handleOpen, sx: { cursor: props.item.disable ? 'default' : 'pointer' }, children: jsx(TextField, { fullWidth: true, value: displayValue, label: muiLabel, placeholder: props.item.placeholder || 'Select month(s)…', error: isError, size: "small", InputProps: {
77
+ readOnly: true,
78
+ endAdornment: (jsx(InputAdornment, { position: "end", children: jsx(CalendarTodayIcon, { sx: { fontSize: 15, color: 'text.disabled' } }) })),
79
+ sx: { cursor: 'inherit' },
80
+ }, inputProps: {
81
+ 'aria-labelledby': `${props.item.name}-label`,
82
+ 'aria-describedby': `${props.item.name}-helper ${props.item.name}-error`,
83
+ 'aria-required': props.item.required ? true : undefined,
84
+ style: { cursor: 'inherit' },
85
+ }, sx: {
86
+ '& .MuiFormLabel-root': { top: -10 },
87
+ '& .MuiInputLabel-shrink': { top: 0 },
88
+ } }) }), jsxs(Popover, { open: Boolean(anchorEl), anchorEl: anchorEl, onClose: handleClose, anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, PaperProps: { sx: { mt: 0.5, borderRadius: 2, boxShadow: 6, width: 280 } }, children: [jsxs(Box, { sx: {
89
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
90
+ px: 1.5, py: 0.75,
91
+ borderBottom: '1px solid', borderColor: 'divider',
92
+ }, children: [jsx(IconButton, { size: "small", onClick: () => setViewYear((y) => y - 1), children: jsx(ChevronLeftIcon, { sx: { fontSize: 18 } }) }), jsx(Typography, { fontWeight: 600, sx: { fontSize: 14, userSelect: 'none' }, children: viewYear }), jsx(IconButton, { size: "small", onClick: () => setViewYear((y) => y + 1), children: jsx(ChevronRightIcon, { sx: { fontSize: 18 } }) })] }), jsx(Box, { sx: { display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 0.75, p: 1.5 }, children: MONTH_LABELS.map((mon, idx) => {
93
+ const monthNum = idx + 1;
94
+ const my = toMY(monthNum, viewYear);
95
+ const key = toKey(monthNum, viewYear);
96
+ const outOfRange = (minKey !== null && key < minKey) || (maxKey !== null && key > maxKey);
97
+ const isSelected = draft.includes(my);
98
+ return (jsx(Button, { variant: isSelected ? 'contained' : 'outlined', size: "small", disableElevation: true, disabled: outOfRange, onClick: () => toggleMonth(my), sx: {
99
+ fontSize: 12,
100
+ textTransform: 'none',
101
+ borderRadius: 2,
102
+ py: 0.75,
103
+ borderColor: isSelected ? 'primary.main' : 'divider',
104
+ fontWeight: isSelected ? 700 : 400,
105
+ }, children: mon }, my));
106
+ }) }), draft.length > 0 && (jsxs(Box, { sx: {
107
+ px: 2, py: 0.75,
108
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
109
+ borderTop: '1px solid', borderColor: 'divider',
110
+ }, children: [jsxs(Typography, { sx: { fontSize: 11, color: 'primary.main', fontWeight: 600 }, children: [draft.length, " month", draft.length > 1 ? 's' : '', " selected"] }), jsx(Button, { size: "small", color: "inherit", sx: { fontSize: 11, py: 0, textTransform: 'none', minWidth: 'auto' }, onClick: () => setDraft([]), children: "Clear" })] })), jsx(Box, { sx: {
111
+ px: 1.5, py: 1,
112
+ display: 'flex', justifyContent: 'flex-end',
113
+ borderTop: '1px solid', borderColor: 'divider',
114
+ }, children: jsx(Button, { size: "small", variant: "contained", disableElevation: true, onClick: handleApply, children: "Apply" }) })] }), jsx(FormBottomField, { ...props })] }, props.item.name));
115
+ };
116
+
117
+ export { MonthPickerMultiRender as default };
@@ -4,11 +4,22 @@ export { default as SingleSelectSearchApi } from './Select/SingleSelectSearchApi
4
4
  export { default as MultiSelectV1 } from './Select/MultiSelectv1.esm.js';
5
5
  export { default as MultiSelectTable } from './Select/MultiSelectTable.esm.js';
6
6
  export { default as SearchableSelect } from './Select/SearchableSelect.esm.js';
7
+ export { default as FormCheckBoxGroup } from './FormCheckBoxGroup/FormCheckBoxGroup.esm.js';
7
8
  export { default as DatepickerWrapperV2 } from './DatePicker/DatepickerWrapperV2.esm.js';
9
+ export { default as Monthpickerrender } from './DatePicker/Monthpickerrender.esm.js';
10
+ import 'react/jsx-runtime';
11
+ import 'react';
12
+ import '@mui/material';
13
+ import '@mui/icons-material/CalendarToday';
14
+ import '@mui/icons-material/ChevronLeft';
15
+ import '@mui/icons-material/ChevronRight';
16
+ import 'react-hook-form';
8
17
  export { default as FormBottomField } from './FormBottomField/FormBottomField.esm.js';
9
- export { default as FormCheckBox } from './FormCheckBox/FormCheckBox.esm.js';
10
- export { default as FormCheckBoxGroup } from './FormCheckBoxGroup/FormCheckBoxGroup.esm.js';
18
+ import 'dayjs';
19
+ export { default as YearPickerField } from './YearPickerField/YearPickerField.esm.js';
11
20
  export { default as FormErrorField } from './FormErrorField/FormErrorField.esm.js';
21
+ export { default as HelperText } from './HelperText/HelperText.esm.js';
22
+ export { default as FormCheckBox } from './FormCheckBox/FormCheckBox.esm.js';
12
23
  export { default as FormNumberField } from './FormNumberField/FormNumberField.esm.js';
13
24
  export { default as FormNumberFieldDecimal } from './FormNumberField/FormNumberFieldDecimal.esm.js';
14
25
  export { default as FormRadioGroup } from './FormRadioGroup/FormRadioGroup.esm.js';
@@ -16,10 +27,7 @@ export { default as FormRenderFileUpload } from './FileUpload/FormRenderFileUplo
16
27
  export { default as FormRenderMultiFileUpload } from './FileUpload/FormRenderMultiFileUpload.esm.js';
17
28
  export { default as FormTextAreaField } from './FormTextAreaField/FormTextAreaField.esm.js';
18
29
  export { default as FormTextField } from './FormTextField/FormTextField.esm.js';
19
- export { default as HelperText } from './HelperText/HelperText.esm.js';
20
- export { default as Monthpickerrender } from './DatePicker/Monthpickerrender.esm.js';
21
30
  export { default as PasswordField } from './PasswordField/PasswordField.esm.js';
22
31
  export { default as RichTextEditor } from './RichTextEditor/RichTextEditor.esm.js';
23
32
  export { default as RichTextEditorWrapper } from './RichTextEditor/RichTextEditorWrapper.esm.js';
24
33
  export { default as TimePicker } from './TimePicker/TimePicker.esm.js';
25
- export { default as YearPickerField } from './YearPickerField/YearPickerField.esm.js';
@@ -98,10 +98,11 @@ export type ServerFilterDef = {
98
98
  defaultVisible?: boolean;
99
99
  } | {
100
100
  /**
101
- * Month-picker chip — lets users select **one or more months** across years.
102
- * Selected months are stored as a comma-separated string of `"YYYY-MM"` values
103
- * in `filterValues[filterKey]` (e.g. `"2025-01,2025-03"`).
104
- * Split on `","` in your `fetchPage` handler to build the server query.
101
+ * Month-picker chip — by default lets users select **one or more months**
102
+ * across years. Selected months are stored as a comma-separated string of
103
+ * `"YYYY-MM"` values in `filterValues[filterKey]` (e.g. `"2025-01,2025-03"`,
104
+ * or just `"2025-01"` when `single: true`). Split on `","` in your
105
+ * `fetchPage` handler to build the server query.
105
106
  */
106
107
  type: 'monthPicker';
107
108
  id: string;
@@ -109,6 +110,13 @@ export type ServerFilterDef = {
109
110
  filterKey: string;
110
111
  /** Show this chip in the filter bar on first render. Default: `false`. */
111
112
  defaultVisible?: boolean;
113
+ /**
114
+ * Restrict selection to a single month instead of the default
115
+ * multi-select behaviour. Clicking a month immediately applies and
116
+ * closes the popover (no Apply button), matching other single-value
117
+ * chips like the date picker. Default: `false` (multi-select).
118
+ */
119
+ single?: boolean;
112
120
  };
113
121
  interface AvailableColumn {
114
122
  id: string;
@@ -253,15 +253,28 @@ const MultiSelectFilterChip = ({ label, selectedValues, options, onApply, onRemo
253
253
  const [anchor, setAnchor] = useState(null);
254
254
  // Draft is only committed on Apply — prevents an API call per checkbox tick.
255
255
  const [draft, setDraft] = useState([]);
256
+ const [search, setSearch] = useState('');
256
257
  const active = selectedValues.length > 0;
257
258
  const openPopover = (e) => {
258
259
  setDraft(selectedValues); // always start from the committed state
260
+ setSearch(''); // always start with all options visible
259
261
  setAnchor(e.currentTarget);
260
262
  };
261
263
  const toggleDraft = (value) => setDraft((prev) => prev.includes(value) ? prev.filter((v) => v !== value) : [...prev, value]);
262
- const allSelected = draft.length === options.length && options.length > 0;
263
- const someSelected = draft.length > 0 && !allSelected;
264
- const toggleAll = () => setDraft(allSelected ? [] : options.map((o) => o.value));
264
+ // Case-insensitive filter — same pattern as SearchableSelectChip.
265
+ const filteredOptions = search
266
+ ? options.filter((o) => o.label.toLowerCase().includes(search.toLowerCase()))
267
+ : options;
268
+ // "All" reflects/toggles only the currently visible (filtered) options —
269
+ // degrades to the original whole-list behaviour when search is empty.
270
+ const allSelected = filteredOptions.length > 0 && filteredOptions.every((o) => draft.includes(o.value));
271
+ const someSelected = !allSelected && filteredOptions.some((o) => draft.includes(o.value));
272
+ const toggleAll = () => setDraft((prev) => {
273
+ const filteredValues = filteredOptions.map((o) => o.value);
274
+ return allSelected
275
+ ? prev.filter((v) => !filteredValues.includes(v))
276
+ : [...new Set([...prev, ...filteredValues])];
277
+ });
265
278
  const handleApply = () => {
266
279
  onApply(draft);
267
280
  setAnchor(null);
@@ -284,19 +297,22 @@ const MultiSelectFilterChip = ({ label, selectedValues, options, onApply, onRemo
284
297
  px: 2, py: 0.75,
285
298
  display: 'flex', alignItems: 'center', justifyContent: 'space-between',
286
299
  borderBottom: '1px solid', borderColor: 'divider',
287
- }, children: [jsx(Typography, { sx: { fontSize: 11, color: draft.length > 0 ? 'primary.main' : 'text.secondary', fontWeight: draft.length > 0 ? 600 : 400 }, children: draft.length > 0 ? `${draft.length} of ${options.length} selected` : 'Select options' }), draft.length > 0 && (jsx(Button, { size: "small", color: "inherit", sx: { fontSize: 11, py: 0, textTransform: 'none', minWidth: 'auto' }, onClick: () => setDraft([]), children: "Clear all" }))] }), jsxs(ListItemButton, { onClick: toggleAll, sx: { px: 1, py: 0.25 }, children: [jsx(Checkbox, { checked: allSelected, indeterminate: someSelected, size: "small", disableRipple: true, sx: { p: 0.5, mr: 0.5, '& .MuiSvgIcon-root': { fontSize: 16 } } }), jsx(ListItemText, { primary: "All", primaryTypographyProps: {
300
+ }, children: [jsx(Typography, { sx: { fontSize: 11, color: draft.length > 0 ? 'primary.main' : 'text.secondary', fontWeight: draft.length > 0 ? 600 : 400 }, children: draft.length > 0 ? `${draft.length} of ${options.length} selected` : 'Select options' }), draft.length > 0 && (jsx(Button, { size: "small", color: "inherit", sx: { fontSize: 11, py: 0, textTransform: 'none', minWidth: 'auto' }, onClick: () => setDraft([]), children: "Clear all" }))] }), jsx(Box, { sx: { px: 1.5, pt: 1, pb: 0.5 }, children: jsx(TextField, { size: "small", fullWidth: true, autoFocus: true, placeholder: "Search\u2026", value: search, onChange: (e) => setSearch(e.target.value), onKeyDown: (e) => { if (e.key === 'Escape')
301
+ setAnchor(null); }, InputProps: {
302
+ startAdornment: (jsx(InputAdornment, { position: "start", children: jsx(SearchIcon, { sx: { fontSize: 16, color: 'text.disabled' } }) })),
303
+ }, sx: { '& .MuiOutlinedInput-root': { borderRadius: 2, fontSize: 13 } } }) }), jsxs(ListItemButton, { onClick: toggleAll, sx: { px: 1, py: 0.25 }, children: [jsx(Checkbox, { checked: allSelected, indeterminate: someSelected, size: "small", disableRipple: true, sx: { p: 0.5, mr: 0.5, '& .MuiSvgIcon-root': { fontSize: 16 } } }), jsx(ListItemText, { primary: "All", primaryTypographyProps: {
288
304
  fontSize: 13,
289
305
  fontStyle: 'italic',
290
306
  color: allSelected || someSelected ? 'primary.main' : 'text.secondary',
291
307
  fontWeight: allSelected ? 600 : 400,
292
- } })] }), jsx(Divider, {}), jsx(List, { dense: true, disablePadding: true, sx: { maxHeight: 240, overflowY: 'auto', py: 0.5 }, children: options.map((opt) => {
308
+ } })] }), jsx(Divider, {}), jsx(List, { dense: true, disablePadding: true, sx: { maxHeight: 240, overflowY: 'auto', py: 0.5 }, children: filteredOptions.length === 0 ? (jsx(Box, { sx: { px: 2, py: 1.5 }, children: jsxs(Typography, { variant: "caption", color: "text.disabled", children: ["No options match \"", search, "\""] }) })) : (filteredOptions.map((opt) => {
293
309
  const checked = draft.includes(opt.value);
294
310
  return (jsxs(ListItemButton, { onClick: () => toggleDraft(opt.value), sx: { px: 1, py: 0.25 }, children: [jsx(Checkbox, { checked: checked, size: "small", disableRipple: true, sx: { p: 0.5, mr: 0.5, '& .MuiSvgIcon-root': { fontSize: 16 } } }), jsx(ListItemText, { primary: opt.label, primaryTypographyProps: {
295
311
  fontSize: 13,
296
312
  color: checked ? 'primary.main' : 'text.primary',
297
313
  fontWeight: checked ? 600 : 400,
298
314
  } })] }, opt.value));
299
- }) }), jsx(Box, { sx: {
315
+ })) }), jsx(Box, { sx: {
300
316
  px: 1.5, py: 1,
301
317
  display: 'flex', justifyContent: 'flex-end',
302
318
  borderTop: '1px solid', borderColor: 'divider',
@@ -304,14 +320,19 @@ const MultiSelectFilterChip = ({ label, selectedValues, options, onApply, onRemo
304
320
  };
305
321
  // ─── MonthPickerChip ─────────────────────────────────────────────────────────
306
322
  //
307
- // Multi-month selection chip. Users navigate by year and tick individual months.
308
- // Draft changes are buffered locally; `onApply` is called once when Apply is
309
- // clicked (single API call per filter action).
323
+ // By default a multi-month selection chip: users navigate by year and tick
324
+ // individual months, draft changes are buffered locally, and `onApply` is
325
+ // called once when Apply is clicked (single API call per filter action).
310
326
  //
311
- // Value format: comma-separated "YYYY-MM" strings, e.g. "2025-01,2025-03".
327
+ // Pass `single` to restrict to one month at a time — clicking a month then
328
+ // immediately applies + closes the popover instead of buffering, matching
329
+ // the other single-value chips (e.g. DatePickerChip).
330
+ //
331
+ // Value format: comma-separated "YYYY-MM" strings, e.g. "2025-01,2025-03"
332
+ // (or just "2025-01" when `single` is set).
312
333
  // ─────────────────────────────────────────────────────────────────────────────
313
334
  const MONTH_LABELS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
314
- const MonthPickerChip = ({ label, value, onApply, onRemove, chipStyle, baseId }) => {
335
+ const MonthPickerChip = ({ label, value, onApply, onRemove, chipStyle, baseId, single = false }) => {
315
336
  const parseYMs = (v) => v ? v.split(',').filter(Boolean) : [];
316
337
  const [anchor, setAnchor] = useState(null);
317
338
  const [viewYear, setViewYear] = useState(() => new Date().getFullYear());
@@ -328,7 +349,18 @@ const MonthPickerChip = ({ label, value, onApply, onRemove, chipStyle, baseId })
328
349
  }
329
350
  setAnchor(e.currentTarget);
330
351
  };
331
- const toggleMonth = (ym) => setDraft((prev) => prev.includes(ym) ? prev.filter((v) => v !== ym) : [...prev, ym]);
352
+ const toggleMonth = (ym) => {
353
+ if (single) {
354
+ // Clicking the already-selected month clears it; otherwise replace
355
+ // the selection and commit immediately (no Apply button in this mode).
356
+ const next = draft.includes(ym) ? [] : [ym];
357
+ setDraft(next);
358
+ onApply(next.join(','));
359
+ setAnchor(null);
360
+ return;
361
+ }
362
+ setDraft((prev) => prev.includes(ym) ? prev.filter((v) => v !== ym) : [...prev, ym]);
363
+ };
332
364
  const handleApply = () => {
333
365
  // Sort selected months chronologically before storing
334
366
  onApply([...draft].sort().join(','));
@@ -365,15 +397,15 @@ const MonthPickerChip = ({ label, value, onApply, onRemove, chipStyle, baseId })
365
397
  borderColor: isSelected ? 'primary.main' : 'divider',
366
398
  fontWeight: isSelected ? 700 : 400,
367
399
  }, children: mon }, ym));
368
- }) }), draft.length > 0 && (jsxs(Box, { sx: {
400
+ }) }), !single && draft.length > 0 && (jsxs(Box, { sx: {
369
401
  px: 2, py: 0.75,
370
402
  display: 'flex', alignItems: 'center', justifyContent: 'space-between',
371
403
  borderTop: '1px solid', borderColor: 'divider',
372
- }, children: [jsxs(Typography, { sx: { fontSize: 11, color: 'primary.main', fontWeight: 600 }, children: [draft.length, " month", draft.length > 1 ? 's' : '', " selected"] }), jsx(Button, { size: "small", color: "inherit", sx: { fontSize: 11, py: 0, textTransform: 'none', minWidth: 'auto' }, onClick: () => setDraft([]), children: "Clear" })] })), jsx(Box, { sx: {
404
+ }, children: [jsxs(Typography, { sx: { fontSize: 11, color: 'primary.main', fontWeight: 600 }, children: [draft.length, " month", draft.length > 1 ? 's' : '', " selected"] }), jsx(Button, { size: "small", color: "inherit", sx: { fontSize: 11, py: 0, textTransform: 'none', minWidth: 'auto' }, onClick: () => setDraft([]), children: "Clear" })] })), !single && (jsx(Box, { sx: {
373
405
  px: 1.5, py: 1,
374
406
  display: 'flex', justifyContent: 'flex-end',
375
407
  borderTop: '1px solid', borderColor: 'divider',
376
- }, children: jsx(Button, { size: "small", variant: "contained", disableElevation: true, onClick: handleApply, children: "Apply" }) })] })] }));
408
+ }, children: jsx(Button, { size: "small", variant: "contained", disableElevation: true, onClick: handleApply, children: "Apply" }) }))] })] }));
377
409
  };
378
410
  const GlobalSearchInput = memo(({ value, onChange, chipStyle, placeholder = 'Search…' }) => {
379
411
  const [draft, setDraft] = useState(value);
@@ -531,7 +563,7 @@ const DynamicFilterBar = ({ serverFilterDefs, visibleFilterIds, onToggleFilter,
531
563
  }), onRemove: () => removeFilter(id), chipStyle: chipStyle, baseId: gridId ? `${gridId}_${id}` : undefined }, id));
532
564
  }
533
565
  if (def?.type === 'monthPicker') {
534
- return (jsx(MonthPickerChip, { label: def.label, value: filterValues[def.filterKey] ?? '', onApply: (v) => onFilterValuesChange({ ...filterValues, [def.filterKey]: v }), onRemove: () => removeFilter(id), chipStyle: chipStyle, baseId: gridId ? `${gridId}_${id}` : undefined }, id));
566
+ return (jsx(MonthPickerChip, { label: def.label, value: filterValues[def.filterKey] ?? '', onApply: (v) => onFilterValuesChange({ ...filterValues, [def.filterKey]: v }), onRemove: () => removeFilter(id), chipStyle: chipStyle, baseId: gridId ? `${gridId}_${id}` : undefined, single: def.single }, id));
535
567
  }
536
568
  if (def?.type === 'date') {
537
569
  return (jsx(DatePickerChip, { label: def.label, value: getColValue(def.columnKey), onApply: (v) => applyColFilter(def.columnKey, v), onRemove: () => removeFilter(id), chipStyle: chipStyle, baseId: gridId ? `${gridId}_${id}` : undefined }, id));
@@ -2,7 +2,7 @@ import { jsxs, jsx } from 'react/jsx-runtime';
2
2
  import { useMemo, useState, useRef, useLayoutEffect, useCallback, useEffect } from 'react';
3
3
  import { MRT_ToggleGlobalFilterButton, MRT_ShowHideColumnsButton, MRT_ToggleDensePaddingButton, MRT_ToggleFullScreenButton, useMaterialReactTable, MaterialReactTable } from 'material-react-table';
4
4
  import { useQuery, keepPreviousData } from '@tanstack/react-query';
5
- import { Box, Tooltip, IconButton, GlobalStyles, Typography, Select, MenuItem, Skeleton, CircularProgress } from '@mui/material';
5
+ import { Box, Tooltip, IconButton, GlobalStyles, Typography, Select, MenuItem, Skeleton } 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';
@@ -12,6 +12,11 @@ import ChevronRightIcon from '@mui/icons-material/ChevronRight';
12
12
  import DynamicFilterBar from './DynamicFilterBar.esm.js';
13
13
  import { getXLSX } from '../../utils/Constants/xlsx-js-style.esm.js';
14
14
 
15
+ // MRT's default overscan (rows rendered outside the visible viewport) is
16
+ // modest; bumping it reduces blank-row flash during fast wheel-scrolling
17
+ // through large virtualized batches. Static (no prop dependency), so it's
18
+ // hoisted for a stable reference across renders.
19
+ const ROW_VIRTUALIZER_OPTIONS = { overscan: 8 };
15
20
  const UI_DENSITY_PRESETS = {
16
21
  default: {
17
22
  footerMinHeight: 48,
@@ -186,19 +191,19 @@ const buildHeaderCellSx = (align, headerMin, headerMax, headerFont, headerBgColo
186
191
  },
187
192
  };
188
193
  };
189
- // ─── Internal: overlay loader ─────────────────────────────────────────────────
190
- const OverlayLoader = ({ message }) => (jsxs(Box, { sx: {
194
+ // ─── Internal: skeleton loader ────────────────────────────────────────────────
195
+ //
196
+ // Used for BOTH loading states:
197
+ // - Initial load (isLoading, no data on screen yet) → opaque background
198
+ // - Refetch with existing data (isFetching, page/filter/sort change)
199
+ // → translucent + blurred so the stale rows are still faintly visible
200
+ // underneath, matching the previous OverlayLoader's backdrop treatment
201
+ // but with skeleton rows instead of a generic spinner.
202
+ const SkeletonOverlay = ({ columnCount, rowCount = 10, translucent = false, }) => (jsx(Box, { sx: {
191
203
  position: 'absolute', inset: 0,
192
204
  display: 'flex', flexDirection: 'column',
193
- alignItems: 'center', justifyContent: 'center',
194
- bgcolor: 'rgba(255,255,255,0.80)', backdropFilter: 'blur(2px)',
195
- zIndex: 10, gap: 1.5,
196
- }, children: [jsx(CircularProgress, { size: 50, thickness: 4 }), jsx(Typography, { variant: "body2", color: "text.secondary", fontWeight: 500, children: message })] }));
197
- // ─── Internal: skeleton loader (initial load — no data on screen yet) ────────
198
- const SkeletonOverlay = ({ columnCount, rowCount = 10, }) => (jsx(Box, { sx: {
199
- position: 'absolute', inset: 0,
200
- display: 'flex', flexDirection: 'column',
201
- bgcolor: 'background.paper',
205
+ bgcolor: translucent ? 'rgba(255,255,255,0.80)' : 'background.paper',
206
+ ...(translucent ? { backdropFilter: 'blur(2px)' } : {}),
202
207
  zIndex: 10, p: 1.5, gap: 1,
203
208
  overflow: 'hidden',
204
209
  }, children: Array.from({ length: rowCount }).map((_, rowIdx) => (jsx(Box, { sx: { display: 'flex', gap: 1.5 }, children: Array.from({ length: Math.max(columnCount, 1) }).map((_, colIdx) => (jsx(Skeleton, { variant: "rounded", height: 28, sx: { flex: 1, minWidth: 0 }, animation: "wave" }, colIdx))) }, rowIdx))) }));
@@ -731,6 +736,11 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
731
736
  enableHiding: true,
732
737
  enableStickyHeader: true,
733
738
  enableRowVirtualization: true,
739
+ // A larger overscan renders more off-screen rows up front, reducing the
740
+ // brief blank-row flash that can appear when scrolling quickly through
741
+ // a large in-memory batch (rows are already fetched — virtualization
742
+ // only governs what's mounted to the DOM at any given moment).
743
+ rowVirtualizerOptions: ROW_VIRTUALIZER_OPTIONS,
734
744
  initialState: {
735
745
  density: densityTokens.mrtDensity,
736
746
  showGlobalFilter: showGlobalSearchByDefault,
@@ -824,7 +834,7 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
824
834
  display: 'flex',
825
835
  flexDirection: 'column',
826
836
  overflow: 'hidden',
827
- }, children: [jsx(GlobalStyles, { styles: tableStyleOverrides }), isLoading && jsx(SkeletonOverlay, { columnCount: processedColumns.length }), !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 }) })] }));
837
+ }, children: [jsx(GlobalStyles, { styles: tableStyleOverrides }), isLoading && jsx(SkeletonOverlay, { columnCount: processedColumns.length }), !isLoading && isFetching && (jsx(SkeletonOverlay, { columnCount: processedColumns.length, translucent: true })), jsx(Box, { sx: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }, children: jsx(MaterialReactTable, { table: table }) })] }));
828
838
  }
829
839
 
830
840
  export { GenericPaginatedGrid as default };
@@ -1,4 +1,4 @@
1
- import { __exports as Calendar } from '../../../../../_virtual/index.esm6.js';
1
+ import { __exports as Calendar } from '../../../../../_virtual/index.esm7.js';
2
2
  import require$$0__default from 'react';
3
3
  import { __require as requirePropTypes } from '../../../../prop-types/index.esm.js';
4
4
  import { __require as requireDayCell } from '../DayCell/index.esm.js';
@@ -1,4 +1,4 @@
1
- import { __exports as DateInput } from '../../../../../_virtual/index.esm14.js';
1
+ import { __exports as DateInput } from '../../../../../_virtual/index.esm13.js';
2
2
  import require$$0__default from 'react';
3
3
  import { __require as requirePropTypes } from '../../../../prop-types/index.esm.js';
4
4
  import { __require as requireClassnames } from '../../../../classnames/index.esm.js';
@@ -1,4 +1,4 @@
1
- import { __exports as DateRange } from '../../../../../_virtual/index.esm7.js';
1
+ import { __exports as DateRange } from '../../../../../_virtual/index.esm4.js';
2
2
  import require$$0__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.esm4.js';
1
+ import { __exports as DefinedRange } from '../../../../../_virtual/index.esm6.js';
2
2
  import require$$0__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';
@@ -1,4 +1,4 @@
1
- import { __exports as Month } from '../../../../../_virtual/index.esm13.js';
1
+ import { __exports as Month } from '../../../../../_virtual/index.esm14.js';
2
2
  import require$$0__default from 'react';
3
3
  import { __require as requirePropTypes } from '../../../../prop-types/index.esm.js';
4
4
  import { __require as requireDayCell } from '../DayCell/index.esm.js';
@@ -11,7 +11,7 @@ export interface TextFieldInputProps {
11
11
  export interface FormSectionPropsItem {
12
12
  name: string;
13
13
  label: string;
14
- inputType: 'text' | 'password' | 'number' | 'select' | 'autocomplete-select' | 'autocomplete-multi-select' | 'single-select-search' | 'datepicker' | 'datetimepicker' | 'multiselect' | 'select-v2' | 'basic-select' | 'searchable-select' | 'decimal' | 'alpha-numerical' | 'yearpicker' | 'dateRangePicker' | 'monthpicker' | 'multiselect' | 'file' | 'multifile' | 'multifile-with-preview' | 'textarea' | 'phoneNumber' | 'pincode' | 'email' | 'toggleSwitch' | 'rich-text-editor' | 'multiEmail' | 'timepicker' | 'checkbox-group' | 'radio-group' | 'checkbox' | 'custom' | 'input-select' | 'date-range-picker' | 'multiselect-table' | '';
14
+ inputType: 'text' | 'password' | 'number' | 'select' | 'autocomplete-select' | 'autocomplete-multi-select' | 'single-select-search' | 'datepicker' | 'datetimepicker' | 'multiselect' | 'select-v2' | 'basic-select' | 'searchable-select' | 'decimal' | 'alpha-numerical' | 'yearpicker' | 'dateRangePicker' | 'monthpicker' | 'monthpicker-multi' | 'multiselect' | 'file' | 'multifile' | 'multifile-with-preview' | 'textarea' | 'phoneNumber' | 'pincode' | 'email' | 'toggleSwitch' | 'rich-text-editor' | 'multiEmail' | 'timepicker' | 'checkbox-group' | 'radio-group' | 'checkbox' | 'custom' | 'input-select' | 'date-range-picker' | 'multiselect-table' | '';
15
15
  options?: OptionsProps[];
16
16
  required?: boolean;
17
17
  errorMessage?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tp-react-elements",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "React form components library built with React Hook Form and Yup",