@checkstack/ui 0.0.2
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 +153 -0
- package/bunfig.toml +2 -0
- package/package.json +40 -0
- package/src/components/Accordion.tsx +55 -0
- package/src/components/Alert.tsx +90 -0
- package/src/components/AmbientBackground.tsx +105 -0
- package/src/components/AnimatedCounter.tsx +54 -0
- package/src/components/BackLink.tsx +56 -0
- package/src/components/Badge.tsx +38 -0
- package/src/components/Button.tsx +55 -0
- package/src/components/Card.tsx +56 -0
- package/src/components/Checkbox.tsx +46 -0
- package/src/components/ColorPicker.tsx +69 -0
- package/src/components/CommandPalette.tsx +74 -0
- package/src/components/ConfirmationModal.tsx +134 -0
- package/src/components/DateRangeFilter.tsx +128 -0
- package/src/components/DateTimePicker.tsx +65 -0
- package/src/components/Dialog.tsx +134 -0
- package/src/components/DropdownMenu.tsx +96 -0
- package/src/components/DynamicForm/DynamicForm.tsx +126 -0
- package/src/components/DynamicForm/DynamicOptionsField.tsx +220 -0
- package/src/components/DynamicForm/FormField.tsx +690 -0
- package/src/components/DynamicForm/JsonField.tsx +98 -0
- package/src/components/DynamicForm/index.ts +11 -0
- package/src/components/DynamicForm/types.ts +95 -0
- package/src/components/DynamicForm/utils.ts +39 -0
- package/src/components/DynamicIcon.tsx +45 -0
- package/src/components/EditableText.tsx +141 -0
- package/src/components/EmptyState.tsx +32 -0
- package/src/components/HealthBadge.tsx +57 -0
- package/src/components/InfoBanner.tsx +97 -0
- package/src/components/Input.tsx +20 -0
- package/src/components/Label.tsx +17 -0
- package/src/components/LoadingSpinner.tsx +29 -0
- package/src/components/Markdown.tsx +206 -0
- package/src/components/NavItem.tsx +112 -0
- package/src/components/Page.tsx +58 -0
- package/src/components/PageLayout.tsx +83 -0
- package/src/components/PaginatedList.tsx +135 -0
- package/src/components/Pagination.tsx +195 -0
- package/src/components/PermissionDenied.tsx +31 -0
- package/src/components/PermissionGate.tsx +97 -0
- package/src/components/PluginConfigForm.tsx +91 -0
- package/src/components/SectionHeader.tsx +30 -0
- package/src/components/Select.tsx +157 -0
- package/src/components/StatusCard.tsx +78 -0
- package/src/components/StatusUpdateTimeline.tsx +222 -0
- package/src/components/StrategyConfigCard.tsx +333 -0
- package/src/components/SubscribeButton.tsx +96 -0
- package/src/components/Table.tsx +119 -0
- package/src/components/Tabs.tsx +141 -0
- package/src/components/TemplateEditor.test.ts +156 -0
- package/src/components/TemplateEditor.tsx +435 -0
- package/src/components/TerminalFeed.tsx +152 -0
- package/src/components/Textarea.tsx +22 -0
- package/src/components/ThemeProvider.tsx +76 -0
- package/src/components/Toast.tsx +118 -0
- package/src/components/ToastProvider.tsx +126 -0
- package/src/components/Toggle.tsx +47 -0
- package/src/components/Tooltip.tsx +20 -0
- package/src/components/UserMenu.tsx +79 -0
- package/src/hooks/usePagination.e2e.ts +275 -0
- package/src/hooks/usePagination.ts +231 -0
- package/src/index.ts +53 -0
- package/src/themes.css +204 -0
- package/src/utils/strip-markdown.ts +44 -0
- package/src/utils.ts +8 -0
- package/tsconfig.json +6 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../utils";
|
|
3
|
+
|
|
4
|
+
export const Card = ({
|
|
5
|
+
className,
|
|
6
|
+
...props
|
|
7
|
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
8
|
+
<div
|
|
9
|
+
className={cn(
|
|
10
|
+
"rounded-lg border border-border bg-card text-card-foreground shadow-sm",
|
|
11
|
+
className
|
|
12
|
+
)}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
export const CardHeader = ({
|
|
18
|
+
className,
|
|
19
|
+
...props
|
|
20
|
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
21
|
+
<div className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export const CardTitle = ({
|
|
25
|
+
className,
|
|
26
|
+
...props
|
|
27
|
+
}: React.HTMLAttributes<HTMLHeadingElement>) => (
|
|
28
|
+
<h3
|
|
29
|
+
className={cn(
|
|
30
|
+
"text-2xl font-semibold leading-none tracking-tight",
|
|
31
|
+
className
|
|
32
|
+
)}
|
|
33
|
+
{...props}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
export const CardDescription = ({
|
|
38
|
+
className,
|
|
39
|
+
...props
|
|
40
|
+
}: React.HTMLAttributes<HTMLParagraphElement>) => (
|
|
41
|
+
<p className={cn("text-sm text-muted-foreground", className)} {...props} />
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
export const CardContent = ({
|
|
45
|
+
className,
|
|
46
|
+
...props
|
|
47
|
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
48
|
+
<div className={cn("p-6 pt-0", className)} {...props} />
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
export const CardFooter = ({
|
|
52
|
+
className,
|
|
53
|
+
...props
|
|
54
|
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
55
|
+
<div className={cn("flex items-center p-6 pt-0", className)} {...props} />
|
|
56
|
+
);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Check } from "lucide-react";
|
|
3
|
+
import { cn } from "../utils";
|
|
4
|
+
|
|
5
|
+
interface CheckboxProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
6
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const Checkbox: React.FC<CheckboxProps> = ({
|
|
10
|
+
className,
|
|
11
|
+
checked,
|
|
12
|
+
onCheckedChange,
|
|
13
|
+
...props
|
|
14
|
+
}) => {
|
|
15
|
+
// Compute styles to avoid nested ternary
|
|
16
|
+
const getBackgroundStyles = () => {
|
|
17
|
+
if (props.disabled) {
|
|
18
|
+
return "bg-muted border-border cursor-not-allowed";
|
|
19
|
+
}
|
|
20
|
+
if (checked) {
|
|
21
|
+
return "bg-primary border-primary cursor-pointer";
|
|
22
|
+
}
|
|
23
|
+
return "bg-background border-input cursor-pointer";
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
className={cn(
|
|
29
|
+
"peer h-4 w-4 shrink-0 rounded-sm border ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 flex items-center justify-center transition-colors",
|
|
30
|
+
getBackgroundStyles(),
|
|
31
|
+
className
|
|
32
|
+
)}
|
|
33
|
+
onClick={() => !props.disabled && onCheckedChange?.(!checked)}
|
|
34
|
+
>
|
|
35
|
+
{checked && (
|
|
36
|
+
<Check
|
|
37
|
+
className={cn(
|
|
38
|
+
"h-3 w-3",
|
|
39
|
+
props.disabled ? "text-muted-foreground" : "text-primary-foreground"
|
|
40
|
+
)}
|
|
41
|
+
strokeWidth={3}
|
|
42
|
+
/>
|
|
43
|
+
)}
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../utils";
|
|
3
|
+
|
|
4
|
+
interface ColorPickerProps {
|
|
5
|
+
id?: string;
|
|
6
|
+
value: string;
|
|
7
|
+
onChange: (value: string) => void;
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const ColorPicker: React.FC<ColorPickerProps> = ({
|
|
13
|
+
id,
|
|
14
|
+
value,
|
|
15
|
+
onChange,
|
|
16
|
+
placeholder = "#000000",
|
|
17
|
+
className,
|
|
18
|
+
}) => {
|
|
19
|
+
const colorInputRef = React.useRef<HTMLInputElement>(null);
|
|
20
|
+
|
|
21
|
+
// Normalize hex color (add # if missing, ensure valid format for color input)
|
|
22
|
+
const normalizedValue = React.useMemo(() => {
|
|
23
|
+
if (!value) return "#000000";
|
|
24
|
+
const hex = value.startsWith("#") ? value : `#${value}`;
|
|
25
|
+
// Validate hex format for the color input (must be #RRGGBB)
|
|
26
|
+
if (/^#[0-9A-Fa-f]{6}$/.test(hex)) {
|
|
27
|
+
return hex;
|
|
28
|
+
}
|
|
29
|
+
// Handle 3-character hex
|
|
30
|
+
if (/^#[0-9A-Fa-f]{3}$/.test(hex)) {
|
|
31
|
+
const r = hex[1];
|
|
32
|
+
const g = hex[2];
|
|
33
|
+
const b = hex[3];
|
|
34
|
+
return `#${r}${r}${g}${g}${b}${b}`;
|
|
35
|
+
}
|
|
36
|
+
return "#000000";
|
|
37
|
+
}, [value]);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className={cn("flex items-center gap-2", className)}>
|
|
41
|
+
<button
|
|
42
|
+
type="button"
|
|
43
|
+
className="h-10 w-10 shrink-0 rounded-md border border-input overflow-hidden focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 transition-shadow"
|
|
44
|
+
style={{ backgroundColor: normalizedValue }}
|
|
45
|
+
onClick={() => colorInputRef.current?.click()}
|
|
46
|
+
aria-label="Pick color"
|
|
47
|
+
>
|
|
48
|
+
<input
|
|
49
|
+
ref={colorInputRef}
|
|
50
|
+
type="color"
|
|
51
|
+
value={normalizedValue}
|
|
52
|
+
onChange={(e) => onChange(e.target.value)}
|
|
53
|
+
className="sr-only"
|
|
54
|
+
tabIndex={-1}
|
|
55
|
+
/>
|
|
56
|
+
</button>
|
|
57
|
+
<input
|
|
58
|
+
id={id}
|
|
59
|
+
type="text"
|
|
60
|
+
value={value || ""}
|
|
61
|
+
onChange={(e) => onChange(e.target.value)}
|
|
62
|
+
placeholder={placeholder}
|
|
63
|
+
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm font-mono placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent disabled:cursor-not-allowed disabled:opacity-50"
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
ColorPicker.displayName = "ColorPicker";
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { cn } from "../utils";
|
|
3
|
+
import { Command } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
interface CommandPaletteProps {
|
|
6
|
+
onClick?: () => void;
|
|
7
|
+
placeholder?: string;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* CommandPalette - Hero search bar with keyboard shortcut hint
|
|
13
|
+
* Displays a prominent search input that triggers a search dialog on click
|
|
14
|
+
*/
|
|
15
|
+
export const CommandPalette: React.FC<CommandPaletteProps> = ({
|
|
16
|
+
onClick,
|
|
17
|
+
placeholder = "Search systems, incidents, or run commands...",
|
|
18
|
+
className,
|
|
19
|
+
}) => {
|
|
20
|
+
return (
|
|
21
|
+
<button
|
|
22
|
+
onClick={onClick}
|
|
23
|
+
className={cn(
|
|
24
|
+
// Base styles
|
|
25
|
+
"w-full flex items-center gap-3 px-4 py-3 rounded-xl",
|
|
26
|
+
// Glassmorphism effect
|
|
27
|
+
"bg-card/50 backdrop-blur-sm border border-border/50",
|
|
28
|
+
// Glow and shadow
|
|
29
|
+
"shadow-lg shadow-primary/5 hover:shadow-xl hover:shadow-primary/10",
|
|
30
|
+
// Hover state
|
|
31
|
+
"hover:border-primary/30 hover:bg-card/70",
|
|
32
|
+
// Transition
|
|
33
|
+
"transition-all duration-300 ease-out",
|
|
34
|
+
// Focus ring
|
|
35
|
+
"focus:outline-none focus:ring-2 focus:ring-primary/50 focus:ring-offset-2 focus:ring-offset-background",
|
|
36
|
+
// Cursor
|
|
37
|
+
"cursor-text",
|
|
38
|
+
className
|
|
39
|
+
)}
|
|
40
|
+
>
|
|
41
|
+
{/* Search icon */}
|
|
42
|
+
<svg
|
|
43
|
+
className="w-5 h-5 text-muted-foreground flex-shrink-0"
|
|
44
|
+
fill="none"
|
|
45
|
+
stroke="currentColor"
|
|
46
|
+
viewBox="0 0 24 24"
|
|
47
|
+
>
|
|
48
|
+
<path
|
|
49
|
+
strokeLinecap="round"
|
|
50
|
+
strokeLinejoin="round"
|
|
51
|
+
strokeWidth={2}
|
|
52
|
+
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
53
|
+
/>
|
|
54
|
+
</svg>
|
|
55
|
+
|
|
56
|
+
{/* Placeholder text with typewriter effect */}
|
|
57
|
+
<span className="flex-1 text-left text-muted-foreground text-sm font-medium truncate">
|
|
58
|
+
{placeholder}
|
|
59
|
+
</span>
|
|
60
|
+
|
|
61
|
+
{/* Keyboard shortcut badge */}
|
|
62
|
+
<kbd
|
|
63
|
+
className={cn(
|
|
64
|
+
"hidden sm:flex items-center gap-1 px-2 py-1 rounded-md",
|
|
65
|
+
"bg-muted/50 border border-border/50",
|
|
66
|
+
"text-xs text-muted-foreground font-mono"
|
|
67
|
+
)}
|
|
68
|
+
>
|
|
69
|
+
<Command className="w-3 h-3" />
|
|
70
|
+
<span>K</span>
|
|
71
|
+
</kbd>
|
|
72
|
+
</button>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { cn } from "../utils";
|
|
3
|
+
import { Button } from "./Button";
|
|
4
|
+
import { AlertTriangle, X } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
export interface ConfirmationModalProps {
|
|
7
|
+
isOpen: boolean;
|
|
8
|
+
onClose: () => void;
|
|
9
|
+
onConfirm: () => void;
|
|
10
|
+
title: string;
|
|
11
|
+
message: string;
|
|
12
|
+
confirmText?: string;
|
|
13
|
+
cancelText?: string;
|
|
14
|
+
variant?: "danger" | "warning" | "info";
|
|
15
|
+
isLoading?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
|
|
19
|
+
isOpen,
|
|
20
|
+
onClose,
|
|
21
|
+
onConfirm,
|
|
22
|
+
title,
|
|
23
|
+
message,
|
|
24
|
+
confirmText = "Confirm",
|
|
25
|
+
cancelText = "Cancel",
|
|
26
|
+
variant = "danger",
|
|
27
|
+
isLoading = false,
|
|
28
|
+
}) => {
|
|
29
|
+
if (!isOpen) return;
|
|
30
|
+
|
|
31
|
+
const handleConfirm = () => {
|
|
32
|
+
onConfirm();
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
36
|
+
if (e.target === e.currentTarget) {
|
|
37
|
+
onClose();
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const variantStyles = {
|
|
42
|
+
danger: {
|
|
43
|
+
icon: "text-destructive",
|
|
44
|
+
iconBg: "bg-destructive/10",
|
|
45
|
+
button:
|
|
46
|
+
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
47
|
+
},
|
|
48
|
+
warning: {
|
|
49
|
+
icon: "text-warning",
|
|
50
|
+
iconBg: "bg-warning/10",
|
|
51
|
+
button: "bg-warning text-warning-foreground hover:bg-warning/90",
|
|
52
|
+
},
|
|
53
|
+
info: {
|
|
54
|
+
icon: "text-info",
|
|
55
|
+
iconBg: "bg-info/10",
|
|
56
|
+
button: "bg-info text-info-foreground hover:bg-info/90",
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const styles = variantStyles[variant];
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div
|
|
64
|
+
className="fixed inset-0 z-[60] !m-0 flex items-center justify-center bg-black/50 transition-opacity pointer-events-auto"
|
|
65
|
+
onClick={handleBackdropClick}
|
|
66
|
+
>
|
|
67
|
+
<div
|
|
68
|
+
className="bg-background rounded-lg shadow-xl max-w-md w-full mx-4 animate-in fade-in zoom-in duration-200 pointer-events-auto"
|
|
69
|
+
role="dialog"
|
|
70
|
+
aria-modal="true"
|
|
71
|
+
aria-labelledby="modal-title"
|
|
72
|
+
aria-describedby="modal-description"
|
|
73
|
+
onClick={(e) => e.stopPropagation()}
|
|
74
|
+
>
|
|
75
|
+
{/* Header */}
|
|
76
|
+
<div className="flex items-start justify-between p-6 pb-4">
|
|
77
|
+
<div className="flex items-start gap-4">
|
|
78
|
+
<div className={cn("rounded-full p-2", styles.iconBg)}>
|
|
79
|
+
<AlertTriangle className={cn("w-6 h-6", styles.icon)} />
|
|
80
|
+
</div>
|
|
81
|
+
<div className="flex-1">
|
|
82
|
+
<h3
|
|
83
|
+
id="modal-title"
|
|
84
|
+
className="text-lg font-semibold text-foreground"
|
|
85
|
+
>
|
|
86
|
+
{title}
|
|
87
|
+
</h3>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
<button
|
|
91
|
+
onClick={onClose}
|
|
92
|
+
className="text-muted-foreground hover:text-foreground transition-colors"
|
|
93
|
+
disabled={isLoading}
|
|
94
|
+
>
|
|
95
|
+
<X className="w-5 h-5" />
|
|
96
|
+
</button>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
{/* Body */}
|
|
100
|
+
<div className="px-6 pb-6">
|
|
101
|
+
<p
|
|
102
|
+
id="modal-description"
|
|
103
|
+
className="text-sm text-muted-foreground pl-14"
|
|
104
|
+
>
|
|
105
|
+
{message}
|
|
106
|
+
</p>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
{/* Footer */}
|
|
110
|
+
<div className="flex items-center justify-end gap-3 px-6 py-4 bg-muted/30 rounded-b-lg">
|
|
111
|
+
<Button
|
|
112
|
+
variant="ghost"
|
|
113
|
+
onClick={onClose}
|
|
114
|
+
disabled={isLoading}
|
|
115
|
+
type="button"
|
|
116
|
+
>
|
|
117
|
+
{cancelText}
|
|
118
|
+
</Button>
|
|
119
|
+
<button
|
|
120
|
+
onClick={handleConfirm}
|
|
121
|
+
disabled={isLoading}
|
|
122
|
+
className={cn(
|
|
123
|
+
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none h-10 px-4 py-2",
|
|
124
|
+
styles.button
|
|
125
|
+
)}
|
|
126
|
+
type="button"
|
|
127
|
+
>
|
|
128
|
+
{isLoading ? "Processing..." : confirmText}
|
|
129
|
+
</button>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import React, { useState, useMemo } from "react";
|
|
2
|
+
import { subDays, subHours, startOfDay } from "date-fns";
|
|
3
|
+
import { DateTimePicker } from "./DateTimePicker";
|
|
4
|
+
import { Button } from "./Button";
|
|
5
|
+
import { Calendar } from "lucide-react";
|
|
6
|
+
|
|
7
|
+
export interface DateRange {
|
|
8
|
+
startDate: Date;
|
|
9
|
+
endDate: Date;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type DateRangePreset = "24h" | "7d" | "30d" | "custom";
|
|
13
|
+
|
|
14
|
+
export interface DateRangeFilterProps {
|
|
15
|
+
value: DateRange;
|
|
16
|
+
onChange: (range: DateRange) => void;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
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" },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function getPresetRange(preset: DateRangePreset): DateRange {
|
|
28
|
+
const now = new Date();
|
|
29
|
+
switch (preset) {
|
|
30
|
+
case "24h": {
|
|
31
|
+
return { startDate: subHours(now, 24), endDate: now };
|
|
32
|
+
}
|
|
33
|
+
case "7d": {
|
|
34
|
+
return { startDate: startOfDay(subDays(now, 7)), endDate: now };
|
|
35
|
+
}
|
|
36
|
+
case "30d": {
|
|
37
|
+
return { startDate: startOfDay(subDays(now, 30)), endDate: now };
|
|
38
|
+
}
|
|
39
|
+
case "custom": {
|
|
40
|
+
return { startDate: startOfDay(subDays(now, 7)), endDate: now };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function detectPreset(range: DateRange): DateRangePreset {
|
|
46
|
+
const now = new Date();
|
|
47
|
+
const diffMs = now.getTime() - range.startDate.getTime();
|
|
48
|
+
const diffHours = diffMs / (1000 * 60 * 60);
|
|
49
|
+
const diffDays = diffHours / 24;
|
|
50
|
+
|
|
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";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Date range filter with preset buttons and custom date pickers.
|
|
59
|
+
*/
|
|
60
|
+
export const DateRangeFilter: React.FC<DateRangeFilterProps> = ({
|
|
61
|
+
value,
|
|
62
|
+
onChange,
|
|
63
|
+
className,
|
|
64
|
+
}) => {
|
|
65
|
+
const activePreset = useMemo(() => detectPreset(value), [value]);
|
|
66
|
+
const [showCustom, setShowCustom] = useState(activePreset === "custom");
|
|
67
|
+
|
|
68
|
+
const handlePresetClick = (preset: DateRangePreset) => {
|
|
69
|
+
if (preset === "custom") {
|
|
70
|
+
setShowCustom(true);
|
|
71
|
+
} else {
|
|
72
|
+
setShowCustom(false);
|
|
73
|
+
onChange(getPresetRange(preset));
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div className={`space-y-3 ${className ?? ""}`}>
|
|
79
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
80
|
+
<Calendar className="h-4 w-4 text-muted-foreground" />
|
|
81
|
+
<span className="text-sm font-medium text-muted-foreground">
|
|
82
|
+
Time range:
|
|
83
|
+
</span>
|
|
84
|
+
<div className="flex gap-1">
|
|
85
|
+
{PRESETS.map((preset) => (
|
|
86
|
+
<Button
|
|
87
|
+
key={preset.id}
|
|
88
|
+
variant={
|
|
89
|
+
activePreset === preset.id && !showCustom
|
|
90
|
+
? "primary"
|
|
91
|
+
: preset.id === "custom" && showCustom
|
|
92
|
+
? "primary"
|
|
93
|
+
: "outline"
|
|
94
|
+
}
|
|
95
|
+
size="sm"
|
|
96
|
+
onClick={() => handlePresetClick(preset.id)}
|
|
97
|
+
>
|
|
98
|
+
{preset.label}
|
|
99
|
+
</Button>
|
|
100
|
+
))}
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{showCustom && (
|
|
105
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
106
|
+
<span className="text-sm text-muted-foreground">From:</span>
|
|
107
|
+
<DateTimePicker
|
|
108
|
+
value={value.startDate}
|
|
109
|
+
onChange={(startDate) => onChange({ ...value, startDate })}
|
|
110
|
+
maxDate={value.endDate}
|
|
111
|
+
/>
|
|
112
|
+
<span className="text-sm text-muted-foreground">To:</span>
|
|
113
|
+
<DateTimePicker
|
|
114
|
+
value={value.endDate}
|
|
115
|
+
onChange={(endDate) => onChange({ ...value, endDate })}
|
|
116
|
+
minDate={value.startDate}
|
|
117
|
+
maxDate={new Date()}
|
|
118
|
+
/>
|
|
119
|
+
</div>
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/** Create a default date range (last 7 days) */
|
|
126
|
+
export function getDefaultDateRange(): DateRange {
|
|
127
|
+
return getPresetRange("7d");
|
|
128
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { format } from "date-fns";
|
|
3
|
+
import { Input } from "./Input";
|
|
4
|
+
import { Calendar, Clock } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
export interface DateTimePickerProps {
|
|
7
|
+
value: Date;
|
|
8
|
+
onChange: (date: Date) => void;
|
|
9
|
+
minDate?: Date;
|
|
10
|
+
maxDate?: Date;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Combined date and time picker component
|
|
16
|
+
*/
|
|
17
|
+
export const DateTimePicker: React.FC<DateTimePickerProps> = ({
|
|
18
|
+
value,
|
|
19
|
+
onChange,
|
|
20
|
+
minDate,
|
|
21
|
+
maxDate,
|
|
22
|
+
className,
|
|
23
|
+
}) => {
|
|
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);
|
|
32
|
+
};
|
|
33
|
+
|
|
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);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
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" />
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
};
|