@checkstack/ui 0.3.1 → 0.4.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,51 @@
1
1
  # @checkstack/ui
2
2
 
3
+ ## 0.4.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 538e45d: Fixed 24-hour date range not returning correct data and improved chart display
8
+
9
+ - Fixed missing `endDate` parameter in raw data queries causing data to extend beyond selected time range
10
+ - Fixed incorrect 24-hour date calculation using `setHours()` - now uses `date-fns` `subHours()` for correct date math
11
+ - Refactored `DateRangePreset` from string union to enum for improved type safety and IDE support
12
+ - Exported `getPresetRange` function for reuse across components
13
+ - Changed chart x-axis domain from `["auto", "auto"]` to `["dataMin", "dataMax"]` to remove padding gaps
14
+
15
+ - Updated dependencies [db1f56f]
16
+ - @checkstack/common@0.6.0
17
+ - @checkstack/frontend-api@0.3.3
18
+
19
+ ## 0.4.0
20
+
21
+ ### Minor Changes
22
+
23
+ - d1324e6: Enhanced DateTimePicker with calendar popup and independent field editing
24
+
25
+ - Added calendar popup using `react-day-picker` and Radix Popover for date selection
26
+ - Implemented independent input fields for day, month, year, hour, and minute
27
+ - Added input validation with proper clamping on blur (respects leap years)
28
+ - Updated `onChange` signature to `Date | undefined` to handle invalid states
29
+ - Fixed Dialog focus ring clipping by adding wrapper with negative margin/padding
30
+
31
+ ### Patch Changes
32
+
33
+ - 2c0822d: ### Queue System
34
+
35
+ - Added cron pattern support to `scheduleRecurring()` - accepts either `intervalSeconds` or `cronPattern`
36
+ - BullMQ backend uses native cron scheduling via `pattern` option
37
+ - InMemoryQueue implements wall-clock cron scheduling with `cron-parser`
38
+
39
+ ### Maintenance Backend
40
+
41
+ - Auto status transitions now use cron pattern `* * * * *` for precise second-0 scheduling
42
+ - User notifications are now sent for auto-started and auto-completed maintenances
43
+ - Refactored to call `addUpdate` RPC for status changes, centralizing hook/signal/notification logic
44
+
45
+ ### UI
46
+
47
+ - DateTimePicker now resets seconds and milliseconds to 0 when time is changed
48
+
3
49
  ## 0.3.1
4
50
 
5
51
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/ui",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
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",
@@ -9,7 +9,12 @@ export interface DateRange {
9
9
  endDate: Date;
10
10
  }
11
11
 
12
- export type DateRangePreset = "24h" | "7d" | "30d" | "custom";
12
+ export enum DateRangePreset {
13
+ Last24Hours = "24h",
14
+ Last7Days = "7d",
15
+ Last30Days = "30d",
16
+ Custom = "custom",
17
+ }
13
18
 
14
19
  export interface DateRangeFilterProps {
15
20
  value: DateRange;
@@ -18,25 +23,25 @@ export interface DateRangeFilterProps {
18
23
  }
19
24
 
20
25
  const PRESETS: Array<{ id: DateRangePreset; label: string }> = [
21
- { id: "24h", label: "Last 24h" },
22
- { id: "7d", label: "Last 7 days" },
23
- { id: "30d", label: "Last 30 days" },
24
- { id: "custom", label: "Custom" },
26
+ { id: DateRangePreset.Last24Hours, label: "Last 24h" },
27
+ { id: DateRangePreset.Last7Days, label: "Last 7 days" },
28
+ { id: DateRangePreset.Last30Days, label: "Last 30 days" },
29
+ { id: DateRangePreset.Custom, label: "Custom" },
25
30
  ];
26
31
 
27
- function getPresetRange(preset: DateRangePreset): DateRange {
32
+ export function getPresetRange(preset: DateRangePreset): DateRange {
28
33
  const now = new Date();
29
34
  switch (preset) {
30
- case "24h": {
35
+ case DateRangePreset.Last24Hours: {
31
36
  return { startDate: subHours(now, 24), endDate: now };
32
37
  }
33
- case "7d": {
38
+ case DateRangePreset.Last7Days: {
34
39
  return { startDate: startOfDay(subDays(now, 7)), endDate: now };
35
40
  }
36
- case "30d": {
41
+ case DateRangePreset.Last30Days: {
37
42
  return { startDate: startOfDay(subDays(now, 30)), endDate: now };
38
43
  }
39
- case "custom": {
44
+ case DateRangePreset.Custom: {
40
45
  return { startDate: startOfDay(subDays(now, 7)), endDate: now };
41
46
  }
42
47
  }
@@ -48,10 +53,10 @@ function detectPreset(range: DateRange): DateRangePreset {
48
53
  const diffHours = diffMs / (1000 * 60 * 60);
49
54
  const diffDays = diffHours / 24;
50
55
 
51
- if (diffHours <= 25 && diffHours >= 23) return "24h";
52
- if (diffDays <= 8 && diffDays >= 6) return "7d";
53
- if (diffDays <= 31 && diffDays >= 29) return "30d";
54
- return "custom";
56
+ if (diffHours <= 25 && diffHours >= 23) return DateRangePreset.Last24Hours;
57
+ if (diffDays <= 8 && diffDays >= 6) return DateRangePreset.Last7Days;
58
+ if (diffDays <= 31 && diffDays >= 29) return DateRangePreset.Last30Days;
59
+ return DateRangePreset.Custom;
55
60
  }
56
61
 
57
62
  /**
@@ -63,10 +68,12 @@ export const DateRangeFilter: React.FC<DateRangeFilterProps> = ({
63
68
  className,
64
69
  }) => {
65
70
  const activePreset = useMemo(() => detectPreset(value), [value]);
66
- const [showCustom, setShowCustom] = useState(activePreset === "custom");
71
+ const [showCustom, setShowCustom] = useState(
72
+ activePreset === DateRangePreset.Custom,
73
+ );
67
74
 
68
75
  const handlePresetClick = (preset: DateRangePreset) => {
69
- if (preset === "custom") {
76
+ if (preset === DateRangePreset.Custom) {
70
77
  setShowCustom(true);
71
78
  } else {
72
79
  setShowCustom(false);
@@ -88,9 +95,9 @@ export const DateRangeFilter: React.FC<DateRangeFilterProps> = ({
88
95
  variant={
89
96
  activePreset === preset.id && !showCustom
90
97
  ? "primary"
91
- : preset.id === "custom" && showCustom
92
- ? "primary"
93
- : "outline"
98
+ : preset.id === DateRangePreset.Custom && showCustom
99
+ ? "primary"
100
+ : "outline"
94
101
  }
95
102
  size="sm"
96
103
  onClick={() => handlePresetClick(preset.id)}
@@ -106,13 +113,21 @@ export const DateRangeFilter: React.FC<DateRangeFilterProps> = ({
106
113
  <span className="text-sm text-muted-foreground">From:</span>
107
114
  <DateTimePicker
108
115
  value={value.startDate}
109
- onChange={(startDate) => onChange({ ...value, startDate })}
116
+ onChange={(startDate) => {
117
+ if (startDate) {
118
+ onChange({ ...value, startDate });
119
+ }
120
+ }}
110
121
  maxDate={value.endDate}
111
122
  />
112
123
  <span className="text-sm text-muted-foreground">To:</span>
113
124
  <DateTimePicker
114
125
  value={value.endDate}
115
- onChange={(endDate) => onChange({ ...value, endDate })}
126
+ onChange={(endDate) => {
127
+ if (endDate) {
128
+ onChange({ ...value, endDate });
129
+ }
130
+ }}
116
131
  minDate={value.startDate}
117
132
  maxDate={new Date()}
118
133
  />
@@ -124,5 +139,5 @@ export const DateRangeFilter: React.FC<DateRangeFilterProps> = ({
124
139
 
125
140
  /** Create a default date range (last 7 days) */
126
141
  export function getDefaultDateRange(): DateRange {
127
- return getPresetRange("7d");
142
+ return getPresetRange(DateRangePreset.Last7Days);
128
143
  }
@@ -1,18 +1,36 @@
1
- import React from "react";
2
- import { format } from "date-fns";
3
- import { Input } from "./Input";
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 dateString = format(value, "yyyy-MM-dd");
25
- const timeString = format(value, "HH:mm");
26
-
27
- const handleDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
28
- const [year, month, day] = e.target.value.split("-").map(Number);
29
- const newDate = new Date(value);
30
- newDate.setFullYear(year, month - 1, day);
31
- onChange(newDate);
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
- const handleTimeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
35
- const [hours, minutes] = e.target.value.split(":").map(Number);
36
- const newDate = new Date(value);
37
- newDate.setHours(hours, minutes);
38
- onChange(newDate);
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 gap-2 ${className ?? ""}`}>
43
- <div className="relative flex-1">
44
- <Input
45
- type="date"
46
- value={dateString}
47
- onChange={handleDateChange}
48
- min={minDate ? format(minDate, "yyyy-MM-dd") : undefined}
49
- max={maxDate ? format(maxDate, "yyyy-MM-dd") : undefined}
50
- className="pl-9"
51
- />
52
- <Calendar className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" />
53
- </div>
54
- <div className="relative w-28">
55
- <Input
56
- type="time"
57
- value={timeString}
58
- onChange={handleTimeChange}
59
- className="pl-9"
60
- />
61
- <Clock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" />
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 React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>,
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
- {children}
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
  />