@checkstack/ui 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/package.json +3 -1
- package/src/components/DateRangeFilter.tsx +12 -4
- package/src/components/DateTimePicker.tsx +332 -39
- package/src/components/Dialog.tsx +10 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# @checkstack/ui
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- d1324e6: Enhanced DateTimePicker with calendar popup and independent field editing
|
|
8
|
+
|
|
9
|
+
- Added calendar popup using `react-day-picker` and Radix Popover for date selection
|
|
10
|
+
- Implemented independent input fields for day, month, year, hour, and minute
|
|
11
|
+
- Added input validation with proper clamping on blur (respects leap years)
|
|
12
|
+
- Updated `onChange` signature to `Date | undefined` to handle invalid states
|
|
13
|
+
- Fixed Dialog focus ring clipping by adding wrapper with negative margin/padding
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- 2c0822d: ### Queue System
|
|
18
|
+
|
|
19
|
+
- Added cron pattern support to `scheduleRecurring()` - accepts either `intervalSeconds` or `cronPattern`
|
|
20
|
+
- BullMQ backend uses native cron scheduling via `pattern` option
|
|
21
|
+
- InMemoryQueue implements wall-clock cron scheduling with `cron-parser`
|
|
22
|
+
|
|
23
|
+
### Maintenance Backend
|
|
24
|
+
|
|
25
|
+
- Auto status transitions now use cron pattern `* * * * *` for precise second-0 scheduling
|
|
26
|
+
- User notifications are now sent for auto-started and auto-completed maintenances
|
|
27
|
+
- Refactored to call `addUpdate` RPC for status changes, centralizing hook/signal/notification logic
|
|
28
|
+
|
|
29
|
+
### UI
|
|
30
|
+
|
|
31
|
+
- DateTimePicker now resets seconds and milliseconds to 0 when time is changed
|
|
32
|
+
|
|
3
33
|
## 0.3.1
|
|
4
34
|
|
|
5
35
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"dependencies": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"@codemirror/view": "^6.39.11",
|
|
17
17
|
"@radix-ui/react-accordion": "^1.2.12",
|
|
18
18
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
19
|
+
"@radix-ui/react-popover": "^1.1.15",
|
|
19
20
|
"@radix-ui/react-select": "^2.2.6",
|
|
20
21
|
"@radix-ui/react-slot": "^1.2.4",
|
|
21
22
|
"@uiw/react-codemirror": "^4.25.4",
|
|
@@ -26,6 +27,7 @@
|
|
|
26
27
|
"date-fns": "^4.1.0",
|
|
27
28
|
"lucide-react": "0.562.0",
|
|
28
29
|
"react": "^18.2.0",
|
|
30
|
+
"react-day-picker": "^9.13.0",
|
|
29
31
|
"react-markdown": "^10.1.0",
|
|
30
32
|
"react-router-dom": "^6.20.0",
|
|
31
33
|
"recharts": "^3.6.0",
|
|
@@ -89,8 +89,8 @@ export const DateRangeFilter: React.FC<DateRangeFilterProps> = ({
|
|
|
89
89
|
activePreset === preset.id && !showCustom
|
|
90
90
|
? "primary"
|
|
91
91
|
: preset.id === "custom" && showCustom
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
? "primary"
|
|
93
|
+
: "outline"
|
|
94
94
|
}
|
|
95
95
|
size="sm"
|
|
96
96
|
onClick={() => handlePresetClick(preset.id)}
|
|
@@ -106,13 +106,21 @@ export const DateRangeFilter: React.FC<DateRangeFilterProps> = ({
|
|
|
106
106
|
<span className="text-sm text-muted-foreground">From:</span>
|
|
107
107
|
<DateTimePicker
|
|
108
108
|
value={value.startDate}
|
|
109
|
-
onChange={(startDate) =>
|
|
109
|
+
onChange={(startDate) => {
|
|
110
|
+
if (startDate) {
|
|
111
|
+
onChange({ ...value, startDate });
|
|
112
|
+
}
|
|
113
|
+
}}
|
|
110
114
|
maxDate={value.endDate}
|
|
111
115
|
/>
|
|
112
116
|
<span className="text-sm text-muted-foreground">To:</span>
|
|
113
117
|
<DateTimePicker
|
|
114
118
|
value={value.endDate}
|
|
115
|
-
onChange={(endDate) =>
|
|
119
|
+
onChange={(endDate) => {
|
|
120
|
+
if (endDate) {
|
|
121
|
+
onChange({ ...value, endDate });
|
|
122
|
+
}
|
|
123
|
+
}}
|
|
116
124
|
minDate={value.startDate}
|
|
117
125
|
maxDate={new Date()}
|
|
118
126
|
/>
|
|
@@ -1,18 +1,36 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
1
|
+
import React, { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import * as Popover from "@radix-ui/react-popover";
|
|
3
|
+
import { DayPicker } from "react-day-picker";
|
|
4
|
+
import { Button } from "./Button";
|
|
4
5
|
import { Calendar, Clock } from "lucide-react";
|
|
6
|
+
import "react-day-picker/style.css";
|
|
5
7
|
|
|
6
8
|
export interface DateTimePickerProps {
|
|
7
|
-
value: Date;
|
|
8
|
-
onChange: (date: Date) => void;
|
|
9
|
+
value: Date | undefined;
|
|
10
|
+
onChange: (date: Date | undefined) => void;
|
|
9
11
|
minDate?: Date;
|
|
10
12
|
maxDate?: Date;
|
|
11
13
|
className?: string;
|
|
12
14
|
}
|
|
13
15
|
|
|
16
|
+
interface FieldState {
|
|
17
|
+
day: string;
|
|
18
|
+
month: string;
|
|
19
|
+
year: string;
|
|
20
|
+
hour: string;
|
|
21
|
+
minute: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const padZero = (value: number, length: number = 2): string =>
|
|
25
|
+
String(value).padStart(length, "0");
|
|
26
|
+
|
|
27
|
+
// Only allow numeric characters
|
|
28
|
+
const filterNumeric = (value: string): string => {
|
|
29
|
+
return value.replaceAll(/[^0-9]/g, "");
|
|
30
|
+
};
|
|
31
|
+
|
|
14
32
|
/**
|
|
15
|
-
* Combined date and time picker component
|
|
33
|
+
* Combined date and time picker component with independent fields and calendar popup
|
|
16
34
|
*/
|
|
17
35
|
export const DateTimePicker: React.FC<DateTimePickerProps> = ({
|
|
18
36
|
value,
|
|
@@ -21,44 +39,319 @@ export const DateTimePicker: React.FC<DateTimePickerProps> = ({
|
|
|
21
39
|
maxDate,
|
|
22
40
|
className,
|
|
23
41
|
}) => {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
42
|
+
const isValidDate = value instanceof Date && !Number.isNaN(value.getTime());
|
|
43
|
+
|
|
44
|
+
// Track if the change came from internal editing
|
|
45
|
+
const isInternalChange = React.useRef(false);
|
|
46
|
+
|
|
47
|
+
// Initialize state from value
|
|
48
|
+
const [fields, setFields] = useState<FieldState>(() => ({
|
|
49
|
+
day: isValidDate ? padZero(value.getDate()) : "",
|
|
50
|
+
month: isValidDate ? padZero(value.getMonth() + 1) : "",
|
|
51
|
+
year: isValidDate ? String(value.getFullYear()) : "",
|
|
52
|
+
hour: isValidDate ? padZero(value.getHours()) : "",
|
|
53
|
+
minute: isValidDate ? padZero(value.getMinutes()) : "",
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
const [calendarOpen, setCalendarOpen] = useState(false);
|
|
57
|
+
|
|
58
|
+
// Sync state when value prop changes from outside (not from internal editing)
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (isInternalChange.current) {
|
|
61
|
+
isInternalChange.current = false;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (isValidDate) {
|
|
65
|
+
setFields({
|
|
66
|
+
day: padZero(value.getDate()),
|
|
67
|
+
month: padZero(value.getMonth() + 1),
|
|
68
|
+
year: String(value.getFullYear()),
|
|
69
|
+
hour: padZero(value.getHours()),
|
|
70
|
+
minute: padZero(value.getMinutes()),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}, [value, isValidDate]);
|
|
74
|
+
|
|
75
|
+
// Build date from fields or return undefined if any field is invalid
|
|
76
|
+
const buildDate = useCallback((f: FieldState): Date | undefined => {
|
|
77
|
+
const day = Number.parseInt(f.day, 10);
|
|
78
|
+
const month = Number.parseInt(f.month, 10);
|
|
79
|
+
const year = Number.parseInt(f.year, 10);
|
|
80
|
+
const hour = Number.parseInt(f.hour, 10);
|
|
81
|
+
const minute = Number.parseInt(f.minute, 10);
|
|
82
|
+
|
|
83
|
+
if (
|
|
84
|
+
Number.isNaN(day) ||
|
|
85
|
+
Number.isNaN(month) ||
|
|
86
|
+
Number.isNaN(year) ||
|
|
87
|
+
Number.isNaN(hour) ||
|
|
88
|
+
Number.isNaN(minute)
|
|
89
|
+
) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Validate ranges
|
|
94
|
+
if (
|
|
95
|
+
day < 1 ||
|
|
96
|
+
day > 31 ||
|
|
97
|
+
month < 1 ||
|
|
98
|
+
month > 12 ||
|
|
99
|
+
year < 1 ||
|
|
100
|
+
hour < 0 ||
|
|
101
|
+
hour > 23 ||
|
|
102
|
+
minute < 0 ||
|
|
103
|
+
minute > 59
|
|
104
|
+
) {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Create date and check if it's valid (handles Feb 30, etc.)
|
|
109
|
+
const date = new Date(year, month - 1, day, hour, minute, 0, 0);
|
|
110
|
+
// If the date rolled over to the next month, it was invalid
|
|
111
|
+
if (date.getMonth() !== month - 1 || date.getDate() !== day) {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return date;
|
|
116
|
+
}, []);
|
|
117
|
+
|
|
118
|
+
const handleFieldChange = (field: keyof FieldState, inputValue: string) => {
|
|
119
|
+
const filtered = filterNumeric(inputValue);
|
|
120
|
+
const newFields = { ...fields, [field]: filtered };
|
|
121
|
+
setFields(newFields);
|
|
122
|
+
isInternalChange.current = true;
|
|
123
|
+
onChange(buildDate(newFields));
|
|
32
124
|
};
|
|
33
125
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
126
|
+
// Helper to get days in a month (respects leap years)
|
|
127
|
+
const getDaysInMonth = (month: number, year: number): number => {
|
|
128
|
+
// Use Date to calculate (day 0 of next month = last day of current month)
|
|
129
|
+
return new Date(year, month, 0).getDate();
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// Validate and clamp value on blur
|
|
133
|
+
const handleFieldBlur = (field: keyof FieldState) => {
|
|
134
|
+
const value = Number.parseInt(fields[field], 10);
|
|
135
|
+
if (Number.isNaN(value)) return;
|
|
136
|
+
|
|
137
|
+
let clamped: number;
|
|
138
|
+
let formatted: string;
|
|
139
|
+
|
|
140
|
+
switch (field) {
|
|
141
|
+
case "day": {
|
|
142
|
+
const month = Number.parseInt(fields.month, 10);
|
|
143
|
+
const year = Number.parseInt(fields.year, 10);
|
|
144
|
+
// Use 31 as fallback if month/year aren't valid yet
|
|
145
|
+
const maxDay =
|
|
146
|
+
!Number.isNaN(month) && !Number.isNaN(year)
|
|
147
|
+
? getDaysInMonth(month, year)
|
|
148
|
+
: 31;
|
|
149
|
+
clamped = Math.min(Math.max(value, 1), maxDay);
|
|
150
|
+
formatted = padZero(clamped);
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
case "month": {
|
|
154
|
+
clamped = Math.min(Math.max(value, 1), 12);
|
|
155
|
+
formatted = padZero(clamped);
|
|
156
|
+
// Also re-clamp day if month changes and day is now out of range
|
|
157
|
+
{
|
|
158
|
+
const day = Number.parseInt(fields.day, 10);
|
|
159
|
+
const year = Number.parseInt(fields.year, 10);
|
|
160
|
+
if (!Number.isNaN(day) && !Number.isNaN(year)) {
|
|
161
|
+
const maxDay = getDaysInMonth(clamped, year);
|
|
162
|
+
if (day > maxDay) {
|
|
163
|
+
const newFields = {
|
|
164
|
+
...fields,
|
|
165
|
+
month: padZero(clamped),
|
|
166
|
+
day: padZero(maxDay),
|
|
167
|
+
};
|
|
168
|
+
setFields(newFields);
|
|
169
|
+
isInternalChange.current = true;
|
|
170
|
+
onChange(buildDate(newFields));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
case "year": {
|
|
178
|
+
clamped = Math.max(value, 1);
|
|
179
|
+
formatted = String(clamped);
|
|
180
|
+
// Also re-clamp day if year changes (for leap year Feb 29 -> 28)
|
|
181
|
+
{
|
|
182
|
+
const day = Number.parseInt(fields.day, 10);
|
|
183
|
+
const month = Number.parseInt(fields.month, 10);
|
|
184
|
+
if (!Number.isNaN(day) && !Number.isNaN(month)) {
|
|
185
|
+
const maxDay = getDaysInMonth(month, clamped);
|
|
186
|
+
if (day > maxDay) {
|
|
187
|
+
const newFields = {
|
|
188
|
+
...fields,
|
|
189
|
+
year: String(clamped),
|
|
190
|
+
day: padZero(maxDay),
|
|
191
|
+
};
|
|
192
|
+
setFields(newFields);
|
|
193
|
+
isInternalChange.current = true;
|
|
194
|
+
onChange(buildDate(newFields));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
case "hour": {
|
|
202
|
+
clamped = Math.min(Math.max(value, 0), 23);
|
|
203
|
+
formatted = padZero(clamped);
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
case "minute": {
|
|
207
|
+
clamped = Math.min(Math.max(value, 0), 59);
|
|
208
|
+
formatted = padZero(clamped);
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
default: {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (fields[field] !== formatted) {
|
|
217
|
+
const newFields = { ...fields, [field]: formatted };
|
|
218
|
+
setFields(newFields);
|
|
219
|
+
isInternalChange.current = true;
|
|
220
|
+
onChange(buildDate(newFields));
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const handleCalendarSelect = (date: Date | undefined) => {
|
|
225
|
+
if (date) {
|
|
226
|
+
const newFields = {
|
|
227
|
+
...fields,
|
|
228
|
+
day: padZero(date.getDate()),
|
|
229
|
+
month: padZero(date.getMonth() + 1),
|
|
230
|
+
year: String(date.getFullYear()),
|
|
231
|
+
};
|
|
232
|
+
setFields(newFields);
|
|
233
|
+
onChange(buildDate(newFields));
|
|
234
|
+
setCalendarOpen(false);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// Get selected date for calendar (only if date fields are valid)
|
|
239
|
+
const getSelectedDate = (): Date | undefined => {
|
|
240
|
+
const day = Number.parseInt(fields.day, 10);
|
|
241
|
+
const month = Number.parseInt(fields.month, 10);
|
|
242
|
+
const year = Number.parseInt(fields.year, 10);
|
|
243
|
+
if (Number.isNaN(day) || Number.isNaN(month) || Number.isNaN(year)) {
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
return new Date(year, month - 1, day);
|
|
39
247
|
};
|
|
40
248
|
|
|
41
249
|
return (
|
|
42
|
-
<div className={`flex
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
250
|
+
<div className={`flex items-center ${className ?? ""}`}>
|
|
251
|
+
{/* Combined date and time container */}
|
|
252
|
+
<div className="flex items-center border rounded-lg bg-background overflow-hidden">
|
|
253
|
+
{/* Date section with calendar popup */}
|
|
254
|
+
<Popover.Root open={calendarOpen} onOpenChange={setCalendarOpen}>
|
|
255
|
+
<Popover.Trigger asChild>
|
|
256
|
+
<Button
|
|
257
|
+
variant="ghost"
|
|
258
|
+
size="icon"
|
|
259
|
+
className="h-9 w-9 rounded-none border-r"
|
|
260
|
+
type="button"
|
|
261
|
+
>
|
|
262
|
+
<Calendar className="h-4 w-4" />
|
|
263
|
+
</Button>
|
|
264
|
+
</Popover.Trigger>
|
|
265
|
+
|
|
266
|
+
<div className="flex items-center px-2">
|
|
267
|
+
<input
|
|
268
|
+
type="text"
|
|
269
|
+
inputMode="numeric"
|
|
270
|
+
value={fields.day}
|
|
271
|
+
onChange={(e) => handleFieldChange("day", e.target.value)}
|
|
272
|
+
onBlur={() => handleFieldBlur("day")}
|
|
273
|
+
placeholder="DD"
|
|
274
|
+
className="w-7 text-center bg-transparent border-none outline-none text-sm font-mono"
|
|
275
|
+
maxLength={2}
|
|
276
|
+
/>
|
|
277
|
+
<span className="text-muted-foreground">/</span>
|
|
278
|
+
<input
|
|
279
|
+
type="text"
|
|
280
|
+
inputMode="numeric"
|
|
281
|
+
value={fields.month}
|
|
282
|
+
onChange={(e) => handleFieldChange("month", e.target.value)}
|
|
283
|
+
onBlur={() => handleFieldBlur("month")}
|
|
284
|
+
placeholder="MM"
|
|
285
|
+
className="w-7 text-center bg-transparent border-none outline-none text-sm font-mono"
|
|
286
|
+
maxLength={2}
|
|
287
|
+
/>
|
|
288
|
+
<span className="text-muted-foreground">/</span>
|
|
289
|
+
<input
|
|
290
|
+
type="text"
|
|
291
|
+
inputMode="numeric"
|
|
292
|
+
value={fields.year}
|
|
293
|
+
onChange={(e) => handleFieldChange("year", e.target.value)}
|
|
294
|
+
onBlur={() => handleFieldBlur("year")}
|
|
295
|
+
placeholder="YYYY"
|
|
296
|
+
className="w-11 text-center bg-transparent border-none outline-none text-sm font-mono"
|
|
297
|
+
maxLength={4}
|
|
298
|
+
/>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
<Popover.Portal>
|
|
302
|
+
<Popover.Content
|
|
303
|
+
className="z-50 rounded-md border bg-popover p-3 shadow-md"
|
|
304
|
+
sideOffset={5}
|
|
305
|
+
align="start"
|
|
306
|
+
>
|
|
307
|
+
<DayPicker
|
|
308
|
+
mode="single"
|
|
309
|
+
selected={getSelectedDate()}
|
|
310
|
+
onSelect={handleCalendarSelect}
|
|
311
|
+
fromDate={minDate}
|
|
312
|
+
toDate={maxDate}
|
|
313
|
+
classNames={{
|
|
314
|
+
root: "rdp-root",
|
|
315
|
+
day: "rdp-day hover:bg-accent rounded-md",
|
|
316
|
+
selected: "bg-primary text-primary-foreground",
|
|
317
|
+
today: "font-bold text-primary",
|
|
318
|
+
chevron: "fill-foreground",
|
|
319
|
+
button_previous: "hover:bg-accent rounded-md p-1",
|
|
320
|
+
button_next: "hover:bg-accent rounded-md p-1",
|
|
321
|
+
}}
|
|
322
|
+
/>
|
|
323
|
+
</Popover.Content>
|
|
324
|
+
</Popover.Portal>
|
|
325
|
+
</Popover.Root>
|
|
326
|
+
|
|
327
|
+
{/* Separator */}
|
|
328
|
+
<div className="w-px h-6 bg-border" />
|
|
329
|
+
|
|
330
|
+
{/* Time section */}
|
|
331
|
+
<div className="flex items-center px-2">
|
|
332
|
+
<Clock className="h-4 w-4 text-muted-foreground mr-2" />
|
|
333
|
+
<input
|
|
334
|
+
type="text"
|
|
335
|
+
inputMode="numeric"
|
|
336
|
+
value={fields.hour}
|
|
337
|
+
onChange={(e) => handleFieldChange("hour", e.target.value)}
|
|
338
|
+
onBlur={() => handleFieldBlur("hour")}
|
|
339
|
+
placeholder="HH"
|
|
340
|
+
className="w-6 text-center bg-transparent border-none outline-none text-sm font-mono"
|
|
341
|
+
maxLength={2}
|
|
342
|
+
/>
|
|
343
|
+
<span className="text-muted-foreground">:</span>
|
|
344
|
+
<input
|
|
345
|
+
type="text"
|
|
346
|
+
inputMode="numeric"
|
|
347
|
+
value={fields.minute}
|
|
348
|
+
onChange={(e) => handleFieldChange("minute", e.target.value)}
|
|
349
|
+
onBlur={() => handleFieldBlur("minute")}
|
|
350
|
+
placeholder="MM"
|
|
351
|
+
className="w-6 text-center bg-transparent border-none outline-none text-sm font-mono"
|
|
352
|
+
maxLength={2}
|
|
353
|
+
/>
|
|
354
|
+
</div>
|
|
62
355
|
</div>
|
|
63
356
|
</div>
|
|
64
357
|
);
|
|
@@ -19,7 +19,7 @@ const DialogOverlay = React.forwardRef<
|
|
|
19
19
|
ref={ref}
|
|
20
20
|
className={cn(
|
|
21
21
|
"fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
22
|
-
className
|
|
22
|
+
className,
|
|
23
23
|
)}
|
|
24
24
|
{...props}
|
|
25
25
|
/>
|
|
@@ -27,7 +27,7 @@ const DialogOverlay = React.forwardRef<
|
|
|
27
27
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
|
28
28
|
|
|
29
29
|
const dialogContentVariants = cva(
|
|
30
|
-
"fixed left-[50%] top-[50%] z-50 grid w-full translate-x-[-50%] translate-y-[-50%] gap-4 border border-border bg-background text-foreground p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg max-h-[85vh] overflow-y-auto",
|
|
30
|
+
"fixed left-[50%] top-[50%] z-50 grid w-full translate-x-[-50%] translate-y-[-50%] gap-4 border border-border bg-background text-foreground p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg max-h-[85vh] overflow-y-auto overflow-x-visible",
|
|
31
31
|
{
|
|
32
32
|
variants: {
|
|
33
33
|
size: {
|
|
@@ -41,11 +41,12 @@ const dialogContentVariants = cva(
|
|
|
41
41
|
defaultVariants: {
|
|
42
42
|
size: "default",
|
|
43
43
|
},
|
|
44
|
-
}
|
|
44
|
+
},
|
|
45
45
|
);
|
|
46
46
|
|
|
47
47
|
interface DialogContentProps
|
|
48
|
-
extends
|
|
48
|
+
extends
|
|
49
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>,
|
|
49
50
|
VariantProps<typeof dialogContentVariants> {}
|
|
50
51
|
|
|
51
52
|
const DialogContent = React.forwardRef<
|
|
@@ -59,7 +60,8 @@ const DialogContent = React.forwardRef<
|
|
|
59
60
|
className={cn(dialogContentVariants({ size }), className)}
|
|
60
61
|
{...props}
|
|
61
62
|
>
|
|
62
|
-
{
|
|
63
|
+
{/* Wrapper with negative margin and positive padding to allow focus rings to extend */}
|
|
64
|
+
<div className="-mx-2 px-2">{children}</div>
|
|
63
65
|
</DialogPrimitive.Content>
|
|
64
66
|
</DialogPortal>
|
|
65
67
|
));
|
|
@@ -72,7 +74,7 @@ const DialogHeader = ({
|
|
|
72
74
|
<div
|
|
73
75
|
className={cn(
|
|
74
76
|
"flex flex-col space-y-1.5 text-center sm:text-left",
|
|
75
|
-
className
|
|
77
|
+
className,
|
|
76
78
|
)}
|
|
77
79
|
{...props}
|
|
78
80
|
/>
|
|
@@ -86,7 +88,7 @@ const DialogFooter = ({
|
|
|
86
88
|
<div
|
|
87
89
|
className={cn(
|
|
88
90
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
|
89
|
-
className
|
|
91
|
+
className,
|
|
90
92
|
)}
|
|
91
93
|
{...props}
|
|
92
94
|
/>
|
|
@@ -101,7 +103,7 @@ const DialogTitle = React.forwardRef<
|
|
|
101
103
|
ref={ref}
|
|
102
104
|
className={cn(
|
|
103
105
|
"text-lg font-semibold leading-none tracking-tight",
|
|
104
|
-
className
|
|
106
|
+
className,
|
|
105
107
|
)}
|
|
106
108
|
{...props}
|
|
107
109
|
/>
|