@pagamio/frontend-commons-lib 0.8.264 → 0.8.266
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/lib/components/ui/Calendar.js +3 -3
- package/lib/components/ui/DatePicker.js +6 -11
- package/lib/components/ui/DateRangePickerWithPresets.d.ts +3 -23
- package/lib/components/ui/DateRangePickerWithPresets.js +90 -119
- package/lib/components/ui/TimerBadge.d.ts +2 -0
- package/lib/components/ui/TimerBadge.js +11 -5
- package/lib/styles.css +3 -3
- package/package.json +1 -1
|
@@ -14,12 +14,12 @@ const Calendar = ({ className, classNames, showOutsideDays = true, ...props }) =
|
|
|
14
14
|
nav_button_next: 'absolute right-1',
|
|
15
15
|
table: 'w-full border-collapse space-y-1',
|
|
16
16
|
head_row: 'flex',
|
|
17
|
-
head_cell: 'text-muted-foreground rounded-md w-
|
|
17
|
+
head_cell: 'text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]',
|
|
18
18
|
row: 'flex w-full mt-2',
|
|
19
|
-
cell: cn('relative p-0 text-center text-sm focus-within:relative focus-within:z-20', props.mode === 'range'
|
|
19
|
+
cell: cn('relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&>button]:cursor-pointer', props.mode === 'range'
|
|
20
20
|
? '[&:has([aria-selected].day-range-middle)]:bg-accent [&:has([aria-selected].day-range-start)]:bg-accent [&:has([aria-selected].day-range-end)]:bg-accent [&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md'
|
|
21
21
|
: '[&:has([aria-selected])]:rounded-md [&:has([aria-selected])]:bg-accent'),
|
|
22
|
-
day: cn(variantStyles.ghost, 'h-
|
|
22
|
+
day: cn(variantStyles.ghost, 'h-9 w-9 p-0 font-normal aria-selected:opacity-100 cursor-pointer'),
|
|
23
23
|
day_range_start: 'day-range-start',
|
|
24
24
|
day_range_end: 'day-range-end',
|
|
25
25
|
day_selected: 'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground',
|
|
@@ -2,20 +2,16 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { Datepicker } from 'flowbite-react';
|
|
3
3
|
import { useEffect, useState } from 'react';
|
|
4
4
|
function DatePicker({ value, disabled, name, onChange }) {
|
|
5
|
-
//
|
|
5
|
+
// Parse a date value into a valid Date or null
|
|
6
6
|
const getValidDate = (date) => {
|
|
7
|
-
if (
|
|
8
|
-
return
|
|
7
|
+
if (date === undefined || date === null)
|
|
8
|
+
return null;
|
|
9
9
|
const parsedDate = new Date(date);
|
|
10
|
-
return Number.isNaN(parsedDate.getTime()) ?
|
|
10
|
+
return Number.isNaN(parsedDate.getTime()) ? null : parsedDate;
|
|
11
11
|
};
|
|
12
|
-
|
|
13
|
-
const initialDate = value === undefined ? new Date() : getValidDate(value);
|
|
14
|
-
const [selectedDate, setSelectedDate] = useState(initialDate);
|
|
12
|
+
const [selectedDate, setSelectedDate] = useState(() => getValidDate(value));
|
|
15
13
|
// Update selectedDate when value prop changes
|
|
16
14
|
useEffect(() => {
|
|
17
|
-
if (value === undefined)
|
|
18
|
-
return;
|
|
19
15
|
setSelectedDate(getValidDate(value));
|
|
20
16
|
}, [value]);
|
|
21
17
|
// Initialize defaultValue for the Datepicker component
|
|
@@ -78,8 +74,7 @@ function DatePicker({ value, disabled, name, onChange }) {
|
|
|
78
74
|
},
|
|
79
75
|
},
|
|
80
76
|
};
|
|
81
|
-
return (_jsx("div", { children: _jsx(Datepicker, { value: selectedDate, onChange: (date) => {
|
|
82
|
-
// Ensure we're working with a valid Date object
|
|
77
|
+
return (_jsx("div", { children: _jsx(Datepicker, { value: selectedDate ?? undefined, onChange: (date) => {
|
|
83
78
|
const validDate = date ? new Date(date) : new Date();
|
|
84
79
|
setSelectedDate(validDate);
|
|
85
80
|
if (onChange && !Number.isNaN(validDate.getTime())) {
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
+
import 'react-date-range/dist/styles.css';
|
|
2
|
+
import 'react-date-range/dist/theme/default.css';
|
|
1
3
|
import type { DateRange } from 'react-day-picker';
|
|
2
4
|
import * as React from 'react';
|
|
3
5
|
export type DatePreset = {
|
|
4
|
-
/** Display label for the preset */
|
|
5
6
|
label: string;
|
|
6
|
-
/** Unique identifier for the preset */
|
|
7
7
|
value: string;
|
|
8
|
-
/** Function to compute the date range for this preset */
|
|
9
8
|
getRange: () => DateRange;
|
|
10
9
|
};
|
|
11
10
|
export interface DateRangePickerWithPresetsProps {
|
|
@@ -13,8 +12,6 @@ export interface DateRangePickerWithPresetsProps {
|
|
|
13
12
|
value?: DateRange;
|
|
14
13
|
/** Callback when date range changes */
|
|
15
14
|
onChange?: (range: DateRange | undefined, presetValue?: string) => void;
|
|
16
|
-
/** Custom preset options (defaults to standard presets) */
|
|
17
|
-
presets?: DatePreset[];
|
|
18
15
|
/** Currently selected preset value */
|
|
19
16
|
selectedPreset?: string;
|
|
20
17
|
/** Placeholder text when no date is selected */
|
|
@@ -34,24 +31,7 @@ export interface DateRangePickerWithPresetsProps {
|
|
|
34
31
|
/** Disable the picker */
|
|
35
32
|
disabled?: boolean;
|
|
36
33
|
}
|
|
34
|
+
/** Kept for backward compatibility — consumers that imported this can still use it */
|
|
37
35
|
export declare const defaultDatePresets: DatePreset[];
|
|
38
|
-
/**
|
|
39
|
-
* A date range picker with preset options and a dual-month calendar view.
|
|
40
|
-
*
|
|
41
|
-
* @example
|
|
42
|
-
* ```tsx
|
|
43
|
-
* const [dateRange, setDateRange] = useState<DateRange | undefined>();
|
|
44
|
-
* const [preset, setPreset] = useState('last_7_days');
|
|
45
|
-
*
|
|
46
|
-
* <DateRangePickerWithPresets
|
|
47
|
-
* value={dateRange}
|
|
48
|
-
* selectedPreset={preset}
|
|
49
|
-
* onChange={(range, presetValue) => {
|
|
50
|
-
* setDateRange(range);
|
|
51
|
-
* if (presetValue) setPreset(presetValue);
|
|
52
|
-
* }}
|
|
53
|
-
* />
|
|
54
|
-
* ```
|
|
55
|
-
*/
|
|
56
36
|
declare const DateRangePickerWithPresets: React.FC<DateRangePickerWithPresetsProps>;
|
|
57
37
|
export default DateRangePickerWithPresets;
|
|
@@ -1,174 +1,145 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { endOfDay, endOfMonth, format, startOfDay, startOfMonth, subDays, subMonths, } from 'date-fns';
|
|
2
|
+
import { endOfDay, endOfMonth, endOfWeek, format, isSameDay, startOfDay, startOfMonth, startOfWeek, subDays, subMonths, } from 'date-fns';
|
|
3
3
|
import { CalendarIcon, X } from 'lucide-react';
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import { DateRangePicker, createStaticRanges } from 'react-date-range';
|
|
5
|
+
import 'react-date-range/dist/styles.css';
|
|
6
|
+
import 'react-date-range/dist/theme/default.css';
|
|
7
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
6
8
|
import { cn } from '../../helpers';
|
|
7
9
|
import Button from './Button';
|
|
8
|
-
import Calendar from './Calendar';
|
|
9
10
|
import { Popover, PopoverContent, PopoverTrigger } from './Popover';
|
|
10
11
|
// =============================================================================
|
|
11
|
-
//
|
|
12
|
+
// PRESET DEFINITIONS (used for display text + preset detection)
|
|
12
13
|
// =============================================================================
|
|
13
|
-
|
|
14
|
+
const PRESET_RANGES = [
|
|
14
15
|
{
|
|
15
16
|
label: 'Today',
|
|
16
17
|
value: 'today',
|
|
17
|
-
|
|
18
|
-
from: startOfDay(new Date()),
|
|
19
|
-
to: endOfDay(new Date()),
|
|
20
|
-
}),
|
|
18
|
+
range: () => ({ startDate: startOfDay(new Date()), endDate: endOfDay(new Date()) }),
|
|
21
19
|
},
|
|
22
20
|
{
|
|
23
21
|
label: 'Yesterday',
|
|
24
22
|
value: 'yesterday',
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
range: () => ({ startDate: startOfDay(subDays(new Date(), 1)), endDate: endOfDay(subDays(new Date(), 1)) }),
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
label: 'This Week',
|
|
27
|
+
value: 'this_week',
|
|
28
|
+
range: () => ({ startDate: startOfWeek(new Date()), endDate: endOfWeek(new Date()) }),
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
label: 'Last 7 Days',
|
|
32
32
|
value: 'last_7_days',
|
|
33
|
-
|
|
34
|
-
from: startOfDay(subDays(new Date(), 6)),
|
|
35
|
-
to: endOfDay(new Date()),
|
|
36
|
-
}),
|
|
33
|
+
range: () => ({ startDate: startOfDay(subDays(new Date(), 6)), endDate: endOfDay(new Date()) }),
|
|
37
34
|
},
|
|
38
35
|
{
|
|
39
36
|
label: 'Last 30 Days',
|
|
40
37
|
value: 'last_30_days',
|
|
41
|
-
|
|
42
|
-
from: startOfDay(subDays(new Date(), 29)),
|
|
43
|
-
to: endOfDay(new Date()),
|
|
44
|
-
}),
|
|
38
|
+
range: () => ({ startDate: startOfDay(subDays(new Date(), 29)), endDate: endOfDay(new Date()) }),
|
|
45
39
|
},
|
|
46
40
|
{
|
|
47
41
|
label: 'This Month',
|
|
48
42
|
value: 'this_month',
|
|
49
|
-
|
|
50
|
-
from: startOfMonth(new Date()),
|
|
51
|
-
to: endOfDay(new Date()),
|
|
52
|
-
}),
|
|
43
|
+
range: () => ({ startDate: startOfMonth(new Date()), endDate: endOfDay(new Date()) }),
|
|
53
44
|
},
|
|
54
45
|
{
|
|
55
46
|
label: 'Last Month',
|
|
56
47
|
value: 'last_month',
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}),
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
label: 'Custom Range',
|
|
64
|
-
value: 'custom',
|
|
65
|
-
getRange: () => ({
|
|
66
|
-
from: undefined,
|
|
67
|
-
to: undefined,
|
|
48
|
+
range: () => ({
|
|
49
|
+
startDate: startOfMonth(subMonths(new Date(), 1)),
|
|
50
|
+
endDate: endOfMonth(subMonths(new Date(), 1)),
|
|
68
51
|
}),
|
|
69
52
|
},
|
|
70
53
|
];
|
|
54
|
+
/** Kept for backward compatibility — consumers that imported this can still use it */
|
|
55
|
+
export const defaultDatePresets = PRESET_RANGES.map((p) => ({
|
|
56
|
+
label: p.label,
|
|
57
|
+
value: p.value,
|
|
58
|
+
getRange: () => {
|
|
59
|
+
const r = p.range();
|
|
60
|
+
return { from: r.startDate, to: r.endDate };
|
|
61
|
+
},
|
|
62
|
+
}));
|
|
63
|
+
// Build react-date-range staticRanges from our definitions
|
|
64
|
+
const staticRanges = createStaticRanges(PRESET_RANGES.map((p) => ({
|
|
65
|
+
label: p.label,
|
|
66
|
+
range: p.range,
|
|
67
|
+
})));
|
|
68
|
+
/** Detect which preset matches a given date range */
|
|
69
|
+
const detectPreset = (from, to) => {
|
|
70
|
+
for (const p of PRESET_RANGES) {
|
|
71
|
+
const r = p.range();
|
|
72
|
+
if (isSameDay(from, r.startDate) && isSameDay(to, r.endDate)) {
|
|
73
|
+
return p.value;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return 'custom';
|
|
77
|
+
};
|
|
78
|
+
// =============================================================================
|
|
79
|
+
// HELPERS
|
|
80
|
+
// =============================================================================
|
|
81
|
+
const FALLBACK_PRIMARY = '#b45dae';
|
|
82
|
+
const getPrimaryColorFromTheme = () => {
|
|
83
|
+
if (typeof window === 'undefined')
|
|
84
|
+
return FALLBACK_PRIMARY;
|
|
85
|
+
try {
|
|
86
|
+
const probe = document.createElement('span');
|
|
87
|
+
probe.style.display = 'none';
|
|
88
|
+
probe.className = 'bg-primary';
|
|
89
|
+
document.body.appendChild(probe);
|
|
90
|
+
const computedColor = window.getComputedStyle(probe).backgroundColor;
|
|
91
|
+
document.body.removeChild(probe);
|
|
92
|
+
return computedColor || FALLBACK_PRIMARY;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return FALLBACK_PRIMARY;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
71
98
|
// =============================================================================
|
|
72
99
|
// COMPONENT
|
|
73
100
|
// =============================================================================
|
|
74
|
-
|
|
75
|
-
* A date range picker with preset options and a dual-month calendar view.
|
|
76
|
-
*
|
|
77
|
-
* @example
|
|
78
|
-
* ```tsx
|
|
79
|
-
* const [dateRange, setDateRange] = useState<DateRange | undefined>();
|
|
80
|
-
* const [preset, setPreset] = useState('last_7_days');
|
|
81
|
-
*
|
|
82
|
-
* <DateRangePickerWithPresets
|
|
83
|
-
* value={dateRange}
|
|
84
|
-
* selectedPreset={preset}
|
|
85
|
-
* onChange={(range, presetValue) => {
|
|
86
|
-
* setDateRange(range);
|
|
87
|
-
* if (presetValue) setPreset(presetValue);
|
|
88
|
-
* }}
|
|
89
|
-
* />
|
|
90
|
-
* ```
|
|
91
|
-
*/
|
|
92
|
-
const DateRangePickerWithPresets = ({ value, onChange, presets = defaultDatePresets, selectedPreset = 'last_7_days', placeholder = 'Select date range', minDate, maxDate, numberOfMonths = 2, className, showClearButton = true, dateFormat = 'MM/dd/yyyy', disabled = false, }) => {
|
|
101
|
+
const DateRangePickerWithPresets = ({ value, onChange, selectedPreset = 'last_7_days', placeholder = 'Select date range', minDate, maxDate, numberOfMonths = 2, className, showClearButton = true, dateFormat = 'MM/dd/yyyy', disabled = false, }) => {
|
|
93
102
|
const [isOpen, setIsOpen] = useState(false);
|
|
94
|
-
const [
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
setTempPreset(selectedPreset);
|
|
101
|
-
}
|
|
102
|
-
}, [isOpen, value, selectedPreset]);
|
|
103
|
-
// Format the display text
|
|
103
|
+
const [primaryColor, setPrimaryColor] = useState(FALLBACK_PRIMARY);
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
setPrimaryColor(getPrimaryColorFromTheme());
|
|
106
|
+
}, []);
|
|
107
|
+
const rangeColors = useMemo(() => [primaryColor], [primaryColor]);
|
|
108
|
+
// Display text for the trigger button
|
|
104
109
|
const displayText = useMemo(() => {
|
|
105
|
-
// Custom range with dates selected — show the formatted date range
|
|
106
110
|
if (value?.from) {
|
|
107
111
|
if (value.to) {
|
|
108
112
|
return `${format(value.from, dateFormat)} - ${format(value.to, dateFormat)}`;
|
|
109
113
|
}
|
|
110
114
|
return format(value.from, dateFormat);
|
|
111
115
|
}
|
|
112
|
-
// No custom dates — show the active preset label
|
|
113
116
|
if (selectedPreset && selectedPreset !== 'custom') {
|
|
114
|
-
const
|
|
115
|
-
if (
|
|
116
|
-
return
|
|
117
|
+
const match = PRESET_RANGES.find((p) => p.value === selectedPreset);
|
|
118
|
+
if (match)
|
|
119
|
+
return match.label;
|
|
117
120
|
}
|
|
118
121
|
return placeholder;
|
|
119
|
-
}, [value, dateFormat, placeholder, selectedPreset
|
|
120
|
-
//
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
setTempPreset(preset.value);
|
|
135
|
-
onChange?.(range, preset.value);
|
|
136
|
-
setIsOpen(false);
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
// Handle calendar selection
|
|
140
|
-
const handleCalendarSelect = (range) => {
|
|
141
|
-
setTempRange(range);
|
|
142
|
-
setTempPreset('custom'); // Switch to custom when manually selecting
|
|
143
|
-
};
|
|
144
|
-
// Handle apply
|
|
145
|
-
const handleApply = () => {
|
|
146
|
-
onChange?.(tempRange, tempPreset);
|
|
147
|
-
setIsOpen(false);
|
|
148
|
-
};
|
|
149
|
-
// Handle cancel
|
|
150
|
-
const handleCancel = () => {
|
|
151
|
-
setTempRange(value);
|
|
152
|
-
setTempPreset(selectedPreset);
|
|
153
|
-
setIsOpen(false);
|
|
122
|
+
}, [value, dateFormat, placeholder, selectedPreset]);
|
|
123
|
+
// Convert { from, to } to react-date-range format
|
|
124
|
+
const selectionRange = useMemo(() => ({
|
|
125
|
+
startDate: value?.from ?? new Date(),
|
|
126
|
+
endDate: value?.to ?? value?.from ?? new Date(),
|
|
127
|
+
key: 'selection',
|
|
128
|
+
}), [value]);
|
|
129
|
+
const handleRangeChange = (rangesByKey) => {
|
|
130
|
+
const sel = rangesByKey.selection;
|
|
131
|
+
if (!sel?.startDate)
|
|
132
|
+
return;
|
|
133
|
+
const from = startOfDay(sel.startDate);
|
|
134
|
+
const to = endOfDay(sel.endDate ?? sel.startDate);
|
|
135
|
+
const preset = detectPreset(from, to);
|
|
136
|
+
onChange?.({ from, to }, preset);
|
|
154
137
|
};
|
|
155
|
-
// Handle clear from trigger button
|
|
156
138
|
const handleClear = (e) => {
|
|
157
139
|
e.stopPropagation();
|
|
158
140
|
onChange?.(undefined, undefined);
|
|
159
141
|
};
|
|
160
|
-
return (_jsxs(Popover, { open: isOpen, onOpenChange: setIsOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", disabled: disabled, className: cn('w-auto justify-start text-left font-normal', !value && 'text-muted-foreground', className), children: [_jsx(CalendarIcon, { className: "mr-2 h-4 w-4" }), _jsx("span", { children: displayText }), showClearButton && value?.from && (_jsx(X, { className: "ml-2 h-4 w-4 opacity-50 hover:opacity-100 cursor-pointer", onClick: handleClear }))] }) }), _jsx(PopoverContent, { className: "w-auto p-0 bg-popover text-popover-foreground border border-border shadow-lg", align: "start", sideOffset: 8, children:
|
|
161
|
-
'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground'), children: preset.label }, preset.value))) }) }), tempPreset === 'custom' && (_jsxs("div", { className: "p-3", children: [_jsx(Calendar, { mode: "range", defaultMonth: tempRange?.from || new Date(), selected: tempRange, onSelect: handleCalendarSelect, numberOfMonths: numberOfMonths, disabled: (date) => {
|
|
162
|
-
if (minDate && date < minDate)
|
|
163
|
-
return true;
|
|
164
|
-
if (maxDate && date > maxDate)
|
|
165
|
-
return true;
|
|
166
|
-
return false;
|
|
167
|
-
}, initialFocus: true }), _jsxs("div", { className: "flex items-center justify-between border-t border-border pt-3 mt-3", children: [_jsx("div", { className: "text-sm text-muted-foreground", children: tempRange?.from && tempRange?.to
|
|
168
|
-
? `${format(tempRange.from, dateFormat)} - ${format(tempRange.to, dateFormat)}`
|
|
169
|
-
: tempRange?.from
|
|
170
|
-
? format(tempRange.from, dateFormat)
|
|
171
|
-
: 'Select a range' }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { variant: "outline", size: "sm", onClick: handleCancel, children: "Cancel" }), _jsx(Button, { size: "sm", onClick: handleApply, disabled: !tempRange?.from || !tempRange?.to, children: "Apply" })] })] })] }))] }) })] }));
|
|
142
|
+
return (_jsxs(Popover, { open: isOpen, onOpenChange: setIsOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", disabled: disabled, className: cn('w-auto justify-start text-left font-normal', !value && 'text-muted-foreground', className), children: [_jsx(CalendarIcon, { className: "mr-2 h-4 w-4" }), _jsx("span", { children: displayText }), showClearButton && value?.from && (_jsx(X, { className: "ml-2 h-4 w-4 opacity-50 hover:opacity-100 cursor-pointer", onClick: handleClear }))] }) }), _jsx(PopoverContent, { className: "w-auto p-0 bg-popover text-popover-foreground border border-border shadow-lg", align: "start", sideOffset: 8, children: _jsx(DateRangePicker, { ranges: [selectionRange], onChange: handleRangeChange, moveRangeOnFirstSelection: false, months: numberOfMonths, direction: "horizontal", showDateDisplay: false, showPreview: true, minDate: minDate, maxDate: maxDate, rangeColors: rangeColors, staticRanges: staticRanges, inputRanges: [] }) })] }));
|
|
172
143
|
};
|
|
173
144
|
DateRangePickerWithPresets.displayName = 'DateRangePickerWithPresets';
|
|
174
145
|
export default DateRangePickerWithPresets;
|
|
@@ -14,6 +14,8 @@ import * as React from 'react';
|
|
|
14
14
|
export interface TimerBadgeProps {
|
|
15
15
|
/** ISO date string or Date object marking when the timer started */
|
|
16
16
|
startTime: string | Date;
|
|
17
|
+
/** ISO date string or Date object marking when the timer stopped. When set, the timer freezes at this time instead of counting up. */
|
|
18
|
+
stopTime?: string | Date | null;
|
|
17
19
|
/** Minutes elapsed before turning amber (default: 15) */
|
|
18
20
|
warningMinutes?: number;
|
|
19
21
|
/** Minutes elapsed before turning red (default: 30) */
|
|
@@ -16,9 +16,10 @@ import { cn } from '../../helpers/utils';
|
|
|
16
16
|
// =============================================================================
|
|
17
17
|
// HELPERS
|
|
18
18
|
// =============================================================================
|
|
19
|
-
function getElapsedSeconds(startTime) {
|
|
19
|
+
function getElapsedSeconds(startTime, stopTime) {
|
|
20
20
|
const start = typeof startTime === 'string' ? new Date(startTime) : startTime;
|
|
21
|
-
|
|
21
|
+
const end = stopTime ? (typeof stopTime === 'string' ? new Date(stopTime) : stopTime).getTime() : Date.now();
|
|
22
|
+
return Math.max(0, Math.floor((end - start.getTime()) / 1000));
|
|
22
23
|
}
|
|
23
24
|
function formatElapsed(totalSeconds) {
|
|
24
25
|
const minutes = Math.floor(totalSeconds / 60);
|
|
@@ -28,14 +29,19 @@ function formatElapsed(totalSeconds) {
|
|
|
28
29
|
// =============================================================================
|
|
29
30
|
// COMPONENT
|
|
30
31
|
// =============================================================================
|
|
31
|
-
const TimerBadge = ({ startTime, warningMinutes = 15, dangerMinutes = 30, size = 'sm', className, }) => {
|
|
32
|
-
const
|
|
32
|
+
const TimerBadge = ({ startTime, stopTime, warningMinutes = 15, dangerMinutes = 30, size = 'sm', className, }) => {
|
|
33
|
+
const isStopped = !!stopTime;
|
|
34
|
+
const [elapsed, setElapsed] = React.useState(() => getElapsedSeconds(startTime, stopTime));
|
|
33
35
|
React.useEffect(() => {
|
|
36
|
+
if (isStopped) {
|
|
37
|
+
setElapsed(getElapsedSeconds(startTime, stopTime));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
34
40
|
const interval = setInterval(() => {
|
|
35
41
|
setElapsed(getElapsedSeconds(startTime));
|
|
36
42
|
}, 1000);
|
|
37
43
|
return () => clearInterval(interval);
|
|
38
|
-
}, [startTime]);
|
|
44
|
+
}, [startTime, stopTime, isStopped]);
|
|
39
45
|
const minutes = elapsed / 60;
|
|
40
46
|
const urgency = minutes >= dangerMinutes ? 'danger' : minutes >= warningMinutes ? 'warning' : 'safe';
|
|
41
47
|
return (_jsxs("span", { className: cn('inline-flex items-center gap-1 rounded-full font-mono font-medium tabular-nums', size === 'sm' ? 'px-2 py-0.5 text-xs' : 'px-2.5 py-1 text-sm', urgency === 'safe' && 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400', urgency === 'warning' && 'bg-amber-500/15 text-amber-600 dark:text-amber-400', urgency === 'danger' && 'bg-red-500/15 text-red-600 dark:text-red-400', className), children: [_jsxs("svg", { className: cn('h-3 w-3 flex-shrink-0', size === 'md' && 'h-3.5 w-3.5'), fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, "aria-hidden": "true", children: [_jsx("circle", { cx: "12", cy: "12", r: "10" }), _jsx("polyline", { points: "12 6 12 12 16 14" })] }), formatElapsed(elapsed)] }));
|
package/lib/styles.css
CHANGED
|
@@ -1437,9 +1437,6 @@ video {
|
|
|
1437
1437
|
.min-w-\[120px\] {
|
|
1438
1438
|
min-width: 120px;
|
|
1439
1439
|
}
|
|
1440
|
-
.min-w-\[140px\] {
|
|
1441
|
-
min-width: 140px;
|
|
1442
|
-
}
|
|
1443
1440
|
.min-w-\[150px\] {
|
|
1444
1441
|
min-width: 150px;
|
|
1445
1442
|
}
|
|
@@ -6542,6 +6539,9 @@ video {
|
|
|
6542
6539
|
.\[\&\>\*\]\:last\:border-b-0:last-child>* {
|
|
6543
6540
|
border-bottom-width: 0px;
|
|
6544
6541
|
}
|
|
6542
|
+
.\[\&\>button\]\:cursor-pointer>button {
|
|
6543
|
+
cursor: pointer;
|
|
6544
|
+
}
|
|
6545
6545
|
.\[\&\>input\]\:h-\[36px\]>input {
|
|
6546
6546
|
height: 36px;
|
|
6547
6547
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pagamio/frontend-commons-lib",
|
|
3
3
|
"description": "Pagamio library for Frontend reusable components like the form engine and table container",
|
|
4
|
-
"version": "0.8.
|
|
4
|
+
"version": "0.8.266",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
7
7
|
"provenance": false
|