@checkmate-monitor/ui 0.1.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.
Files changed (68) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/bunfig.toml +2 -0
  3. package/package.json +39 -0
  4. package/src/components/Accordion.tsx +55 -0
  5. package/src/components/Alert.tsx +91 -0
  6. package/src/components/AmbientBackground.tsx +105 -0
  7. package/src/components/AnimatedCounter.tsx +54 -0
  8. package/src/components/BackLink.tsx +56 -0
  9. package/src/components/Badge.tsx +38 -0
  10. package/src/components/Button.tsx +55 -0
  11. package/src/components/Card.tsx +56 -0
  12. package/src/components/Checkbox.tsx +46 -0
  13. package/src/components/ColorPicker.tsx +69 -0
  14. package/src/components/CommandPalette.tsx +74 -0
  15. package/src/components/ConfirmationModal.tsx +134 -0
  16. package/src/components/DateRangeFilter.tsx +128 -0
  17. package/src/components/DateTimePicker.tsx +65 -0
  18. package/src/components/Dialog.tsx +134 -0
  19. package/src/components/DropdownMenu.tsx +96 -0
  20. package/src/components/DynamicForm/DynamicForm.tsx +126 -0
  21. package/src/components/DynamicForm/DynamicOptionsField.tsx +220 -0
  22. package/src/components/DynamicForm/FormField.tsx +690 -0
  23. package/src/components/DynamicForm/JsonField.tsx +98 -0
  24. package/src/components/DynamicForm/index.ts +11 -0
  25. package/src/components/DynamicForm/types.ts +95 -0
  26. package/src/components/DynamicForm/utils.ts +39 -0
  27. package/src/components/DynamicIcon.tsx +52 -0
  28. package/src/components/EditableText.tsx +141 -0
  29. package/src/components/EmptyState.tsx +32 -0
  30. package/src/components/HealthBadge.tsx +58 -0
  31. package/src/components/InfoBanner.tsx +98 -0
  32. package/src/components/Input.tsx +20 -0
  33. package/src/components/Label.tsx +17 -0
  34. package/src/components/LoadingSpinner.tsx +29 -0
  35. package/src/components/Markdown.tsx +206 -0
  36. package/src/components/NavItem.tsx +112 -0
  37. package/src/components/Page.tsx +58 -0
  38. package/src/components/PageLayout.tsx +76 -0
  39. package/src/components/PaginatedList.tsx +135 -0
  40. package/src/components/Pagination.tsx +195 -0
  41. package/src/components/PermissionDenied.tsx +31 -0
  42. package/src/components/PermissionGate.tsx +97 -0
  43. package/src/components/PluginConfigForm.tsx +91 -0
  44. package/src/components/SectionHeader.tsx +30 -0
  45. package/src/components/Select.tsx +157 -0
  46. package/src/components/StatusCard.tsx +78 -0
  47. package/src/components/StatusUpdateTimeline.tsx +222 -0
  48. package/src/components/StrategyConfigCard.tsx +333 -0
  49. package/src/components/SubscribeButton.tsx +96 -0
  50. package/src/components/Table.tsx +119 -0
  51. package/src/components/Tabs.tsx +141 -0
  52. package/src/components/TemplateEditor.test.ts +156 -0
  53. package/src/components/TemplateEditor.tsx +435 -0
  54. package/src/components/TerminalFeed.tsx +152 -0
  55. package/src/components/Textarea.tsx +22 -0
  56. package/src/components/ThemeProvider.tsx +76 -0
  57. package/src/components/Toast.tsx +118 -0
  58. package/src/components/ToastProvider.tsx +126 -0
  59. package/src/components/Toggle.tsx +47 -0
  60. package/src/components/Tooltip.tsx +20 -0
  61. package/src/components/UserMenu.tsx +79 -0
  62. package/src/hooks/usePagination.e2e.ts +275 -0
  63. package/src/hooks/usePagination.ts +231 -0
  64. package/src/index.ts +53 -0
  65. package/src/themes.css +204 -0
  66. package/src/utils/strip-markdown.ts +44 -0
  67. package/src/utils.ts +8 -0
  68. package/tsconfig.json +6 -0
@@ -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
+ };
@@ -0,0 +1,134 @@
1
+ import * as React from "react";
2
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+ import { cn } from "../utils";
5
+
6
+ const Dialog = DialogPrimitive.Root;
7
+
8
+ const DialogTrigger = DialogPrimitive.Trigger;
9
+
10
+ const DialogPortal = DialogPrimitive.Portal;
11
+
12
+ const DialogClose = DialogPrimitive.Close;
13
+
14
+ const DialogOverlay = React.forwardRef<
15
+ React.ElementRef<typeof DialogPrimitive.Overlay>,
16
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
17
+ >(({ className, ...props }, ref) => (
18
+ <DialogPrimitive.Overlay
19
+ ref={ref}
20
+ className={cn(
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
23
+ )}
24
+ {...props}
25
+ />
26
+ ));
27
+ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
28
+
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",
31
+ {
32
+ variants: {
33
+ size: {
34
+ sm: "max-w-sm",
35
+ default: "max-w-lg",
36
+ lg: "max-w-2xl",
37
+ xl: "max-w-4xl",
38
+ full: "max-w-[90vw]",
39
+ },
40
+ },
41
+ defaultVariants: {
42
+ size: "default",
43
+ },
44
+ }
45
+ );
46
+
47
+ interface DialogContentProps
48
+ extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>,
49
+ VariantProps<typeof dialogContentVariants> {}
50
+
51
+ const DialogContent = React.forwardRef<
52
+ React.ElementRef<typeof DialogPrimitive.Content>,
53
+ DialogContentProps
54
+ >(({ className, children, size, ...props }, ref) => (
55
+ <DialogPortal>
56
+ <DialogOverlay />
57
+ <DialogPrimitive.Content
58
+ ref={ref}
59
+ className={cn(dialogContentVariants({ size }), className)}
60
+ {...props}
61
+ >
62
+ {children}
63
+ </DialogPrimitive.Content>
64
+ </DialogPortal>
65
+ ));
66
+ DialogContent.displayName = DialogPrimitive.Content.displayName;
67
+
68
+ const DialogHeader = ({
69
+ className,
70
+ ...props
71
+ }: React.HTMLAttributes<HTMLDivElement>) => (
72
+ <div
73
+ className={cn(
74
+ "flex flex-col space-y-1.5 text-center sm:text-left",
75
+ className
76
+ )}
77
+ {...props}
78
+ />
79
+ );
80
+ DialogHeader.displayName = "DialogHeader";
81
+
82
+ const DialogFooter = ({
83
+ className,
84
+ ...props
85
+ }: React.HTMLAttributes<HTMLDivElement>) => (
86
+ <div
87
+ className={cn(
88
+ "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
89
+ className
90
+ )}
91
+ {...props}
92
+ />
93
+ );
94
+ DialogFooter.displayName = "DialogFooter";
95
+
96
+ const DialogTitle = React.forwardRef<
97
+ React.ElementRef<typeof DialogPrimitive.Title>,
98
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
99
+ >(({ className, ...props }, ref) => (
100
+ <DialogPrimitive.Title
101
+ ref={ref}
102
+ className={cn(
103
+ "text-lg font-semibold leading-none tracking-tight",
104
+ className
105
+ )}
106
+ {...props}
107
+ />
108
+ ));
109
+ DialogTitle.displayName = DialogPrimitive.Title.displayName;
110
+
111
+ const DialogDescription = React.forwardRef<
112
+ React.ElementRef<typeof DialogPrimitive.Description>,
113
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
114
+ >(({ className, ...props }, ref) => (
115
+ <DialogPrimitive.Description
116
+ ref={ref}
117
+ className={cn("text-sm text-muted-foreground", className)}
118
+ {...props}
119
+ />
120
+ ));
121
+ DialogDescription.displayName = DialogPrimitive.Description.displayName;
122
+
123
+ export {
124
+ Dialog,
125
+ DialogPortal,
126
+ DialogOverlay,
127
+ DialogClose,
128
+ DialogTrigger,
129
+ DialogContent,
130
+ DialogHeader,
131
+ DialogFooter,
132
+ DialogTitle,
133
+ DialogDescription,
134
+ };
@@ -0,0 +1,96 @@
1
+ import React, { useRef, useEffect } from "react";
2
+ import { cn } from "../utils";
3
+
4
+ export const DropdownMenu: React.FC<{ children: React.ReactNode }> = ({
5
+ children,
6
+ }) => {
7
+ return <div className="relative inline-block text-left">{children}</div>;
8
+ };
9
+
10
+ export const DropdownMenuTrigger: React.FC<{
11
+ children: React.ReactNode;
12
+ asChild?: boolean;
13
+ onClick?: () => void;
14
+ }> = ({ children, onClick }) => {
15
+ return (
16
+ <div onClick={onClick} className="cursor-pointer">
17
+ {children}
18
+ </div>
19
+ );
20
+ };
21
+
22
+ export const DropdownMenuContent: React.FC<{
23
+ children: React.ReactNode;
24
+ isOpen: boolean;
25
+ onClose: () => void;
26
+ className?: string;
27
+ }> = ({ children, isOpen, onClose, className }) => {
28
+ const contentRef = useRef<HTMLDivElement>(null);
29
+
30
+ useEffect(() => {
31
+ const handleClickOutside = (event: MouseEvent) => {
32
+ if (
33
+ contentRef.current &&
34
+ !contentRef.current.contains(event.target as Node)
35
+ ) {
36
+ onClose();
37
+ }
38
+ };
39
+
40
+ if (isOpen) {
41
+ document.addEventListener("mousedown", handleClickOutside);
42
+ }
43
+ return () => {
44
+ document.removeEventListener("mousedown", handleClickOutside);
45
+ };
46
+ }, [isOpen, onClose]);
47
+
48
+ if (!isOpen) return <React.Fragment />;
49
+
50
+ return (
51
+ <div
52
+ ref={contentRef}
53
+ className={cn(
54
+ "absolute right-0 mt-2 w-56 origin-top-right rounded-md bg-popover shadow-lg ring-1 ring-border focus:outline-none z-[100] animate-in fade-in zoom-in-95 duration-100",
55
+ className
56
+ )}
57
+ >
58
+ <div className="py-1" role="none">
59
+ {children}
60
+ </div>
61
+ </div>
62
+ );
63
+ };
64
+
65
+ export const DropdownMenuItem: React.FC<{
66
+ children: React.ReactNode;
67
+ onClick?: () => void;
68
+ className?: string;
69
+ icon?: React.ReactNode;
70
+ }> = ({ children, onClick, className, icon }) => {
71
+ return (
72
+ <button
73
+ onClick={onClick}
74
+ className={cn(
75
+ "flex items-center w-full px-4 py-2 text-sm text-popover-foreground hover:bg-accent hover:text-accent-foreground transition-colors",
76
+ className
77
+ )}
78
+ role="menuitem"
79
+ >
80
+ {icon && <span className="mr-3 text-muted-foreground">{icon}</span>}
81
+ {children}
82
+ </button>
83
+ );
84
+ };
85
+
86
+ export const DropdownMenuSeparator: React.FC = () => (
87
+ <div className="my-1 h-px bg-border" />
88
+ );
89
+
90
+ export const DropdownMenuLabel: React.FC<{ children: React.ReactNode }> = ({
91
+ children,
92
+ }) => (
93
+ <div className="px-4 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
94
+ {children}
95
+ </div>
96
+ );