@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.
Files changed (68) hide show
  1. package/CHANGELOG.md +153 -0
  2. package/bunfig.toml +2 -0
  3. package/package.json +40 -0
  4. package/src/components/Accordion.tsx +55 -0
  5. package/src/components/Alert.tsx +90 -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 +45 -0
  28. package/src/components/EditableText.tsx +141 -0
  29. package/src/components/EmptyState.tsx +32 -0
  30. package/src/components/HealthBadge.tsx +57 -0
  31. package/src/components/InfoBanner.tsx +97 -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 +83 -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,195 @@
1
+ import { ChevronLeft, ChevronRight } from "lucide-react";
2
+ import { Button } from "./Button";
3
+ import {
4
+ Select,
5
+ SelectContent,
6
+ SelectItem,
7
+ SelectTrigger,
8
+ SelectValue,
9
+ } from "./Select";
10
+ import { cn } from "../utils";
11
+
12
+ export interface PaginationProps {
13
+ /** Current page (1-indexed) */
14
+ page: number;
15
+ /** Total number of pages */
16
+ totalPages: number;
17
+ /** Callback when page changes */
18
+ onPageChange: (page: number) => void;
19
+ /** Total number of items (optional, for display) */
20
+ total?: number;
21
+ /** Items per page (for page size selector) */
22
+ limit?: number;
23
+ /** Available page sizes */
24
+ pageSizes?: number[];
25
+ /** Callback when page size changes */
26
+ onPageSizeChange?: (limit: number) => void;
27
+ /** Show page size selector */
28
+ showPageSize?: boolean;
29
+ /** Show total items count */
30
+ showTotal?: boolean;
31
+ /** Additional class name */
32
+ className?: string;
33
+ }
34
+
35
+ /**
36
+ * Pagination component for navigating through paginated data.
37
+ * Works seamlessly with usePagination hook.
38
+ *
39
+ * @example
40
+ * ```tsx
41
+ * const { items, pagination } = usePagination({ ... });
42
+ *
43
+ * <Pagination
44
+ * page={pagination.page}
45
+ * totalPages={pagination.totalPages}
46
+ * onPageChange={pagination.setPage}
47
+ * limit={pagination.limit}
48
+ * onPageSizeChange={pagination.setLimit}
49
+ * total={pagination.total}
50
+ * showPageSize
51
+ * showTotal
52
+ * />
53
+ * ```
54
+ */
55
+ export function Pagination({
56
+ page,
57
+ totalPages,
58
+ onPageChange,
59
+ total,
60
+ limit,
61
+ pageSizes = [10, 25, 50, 100],
62
+ onPageSizeChange,
63
+ showPageSize = false,
64
+ showTotal = false,
65
+ className,
66
+ }: PaginationProps) {
67
+ const hasPrev = page > 1;
68
+ const hasNext = page < totalPages;
69
+
70
+ // Generate page numbers to show
71
+ const getPageNumbers = (): (number | "ellipsis")[] => {
72
+ const pages: (number | "ellipsis")[] = [];
73
+ const maxVisible = 5;
74
+
75
+ if (totalPages <= maxVisible) {
76
+ // Show all pages
77
+ for (let i = 1; i <= totalPages; i++) {
78
+ pages.push(i);
79
+ }
80
+ } else {
81
+ // Always show first page
82
+ pages.push(1);
83
+
84
+ if (page > 3) {
85
+ pages.push("ellipsis");
86
+ }
87
+
88
+ // Show pages around current
89
+ const start = Math.max(2, page - 1);
90
+ const end = Math.min(totalPages - 1, page + 1);
91
+
92
+ for (let i = start; i <= end; i++) {
93
+ pages.push(i);
94
+ }
95
+
96
+ if (page < totalPages - 2) {
97
+ pages.push("ellipsis");
98
+ }
99
+
100
+ // Always show last page
101
+ if (totalPages > 1) {
102
+ pages.push(totalPages);
103
+ }
104
+ }
105
+
106
+ return pages;
107
+ };
108
+
109
+ return (
110
+ <div
111
+ className={cn(
112
+ "flex items-center justify-between gap-4 flex-wrap",
113
+ className
114
+ )}
115
+ >
116
+ {/* Left side: Page size selector and total */}
117
+ <div className="flex items-center gap-4">
118
+ {showPageSize && limit && onPageSizeChange && (
119
+ <div className="flex items-center gap-2">
120
+ <span className="text-sm text-muted-foreground">Show</span>
121
+ <Select
122
+ value={String(limit)}
123
+ onValueChange={(value) => onPageSizeChange(Number(value))}
124
+ >
125
+ <SelectTrigger className="h-8 w-[70px]">
126
+ <SelectValue />
127
+ </SelectTrigger>
128
+ <SelectContent>
129
+ {pageSizes.map((size) => (
130
+ <SelectItem key={size} value={String(size)}>
131
+ {size}
132
+ </SelectItem>
133
+ ))}
134
+ </SelectContent>
135
+ </Select>
136
+ </div>
137
+ )}
138
+
139
+ {showTotal && total !== undefined && (
140
+ <span className="text-sm text-muted-foreground">
141
+ {total} {total === 1 ? "item" : "items"}
142
+ </span>
143
+ )}
144
+ </div>
145
+
146
+ {/* Right side: Page navigation */}
147
+ <div className="flex items-center gap-1">
148
+ <Button
149
+ variant="outline"
150
+ size="icon"
151
+ className="h-8 w-8"
152
+ onClick={() => onPageChange(page - 1)}
153
+ disabled={!hasPrev}
154
+ aria-label="Previous page"
155
+ >
156
+ <ChevronLeft className="h-4 w-4" />
157
+ </Button>
158
+
159
+ {getPageNumbers().map((pageNum, idx) =>
160
+ pageNum === "ellipsis" ? (
161
+ <span
162
+ key={`ellipsis-${idx}`}
163
+ className="px-2 text-muted-foreground"
164
+ >
165
+ ...
166
+ </span>
167
+ ) : (
168
+ <Button
169
+ key={pageNum}
170
+ variant={pageNum === page ? "primary" : "outline"}
171
+ size="sm"
172
+ className="h-8 w-8 p-0"
173
+ onClick={() => onPageChange(pageNum)}
174
+ aria-label={`Page ${pageNum}`}
175
+ aria-current={pageNum === page ? "page" : undefined}
176
+ >
177
+ {pageNum}
178
+ </Button>
179
+ )
180
+ )}
181
+
182
+ <Button
183
+ variant="outline"
184
+ size="icon"
185
+ className="h-8 w-8"
186
+ onClick={() => onPageChange(page + 1)}
187
+ disabled={!hasNext}
188
+ aria-label="Next page"
189
+ >
190
+ <ChevronRight className="h-4 w-4" />
191
+ </Button>
192
+ </div>
193
+ </div>
194
+ );
195
+ }
@@ -0,0 +1,31 @@
1
+ import React from "react";
2
+ import { ShieldAlert } from "lucide-react";
3
+ import { Card, CardHeader, CardTitle, CardContent } from "./Card";
4
+ import { cn } from "../utils";
5
+
6
+ export const PermissionDenied: React.FC<{
7
+ message?: string;
8
+ className?: string;
9
+ }> = ({
10
+ message = "You do not have permission to view this page.",
11
+ className,
12
+ }) => {
13
+ return (
14
+ <div
15
+ className={cn(
16
+ "flex items-center justify-center min-h-[50vh] p-4",
17
+ className
18
+ )}
19
+ >
20
+ <Card className="max-w-md w-full border-destructive/30 bg-destructive/10">
21
+ <CardHeader className="flex flex-row items-center gap-4 pb-2">
22
+ <ShieldAlert className="w-8 h-8 text-destructive" />
23
+ <CardTitle className="text-destructive">Access Denied</CardTitle>
24
+ </CardHeader>
25
+ <CardContent>
26
+ <p className="text-destructive">{message}</p>
27
+ </CardContent>
28
+ </Card>
29
+ </div>
30
+ );
31
+ };
@@ -0,0 +1,97 @@
1
+ import React from "react";
2
+ import { PermissionDenied } from "./PermissionDenied";
3
+ import { LoadingSpinner } from "./LoadingSpinner";
4
+
5
+ /**
6
+ * Props for the PermissionGate component.
7
+ */
8
+ export interface PermissionGateProps {
9
+ /**
10
+ * The permission ID to check for access.
11
+ */
12
+ permission: string;
13
+ /**
14
+ * Content to render when permission is granted.
15
+ */
16
+ children: React.ReactNode;
17
+ /**
18
+ * Custom fallback to render when permission is denied.
19
+ * If not provided and showDenied is false, renders nothing.
20
+ */
21
+ fallback?: React.ReactNode;
22
+ /**
23
+ * If true, shows a PermissionDenied component when access is denied.
24
+ * Useful for entire page sections. Overridden by fallback if provided.
25
+ */
26
+ showDenied?: boolean;
27
+ /**
28
+ * Custom message to show in the PermissionDenied component.
29
+ */
30
+ deniedMessage?: string;
31
+ /**
32
+ * Hook to check permissions. Must be provided by the consumer.
33
+ * This allows the component to be used without depending on auth-frontend directly.
34
+ */
35
+ usePermission: (permission: string) => { loading: boolean; allowed: boolean };
36
+ }
37
+
38
+ /**
39
+ * Conditionally renders children based on whether the user has a required permission.
40
+ *
41
+ * @example
42
+ * // Hide content if permission denied
43
+ * <PermissionGate permission="catalog.manage" usePermission={permissionApi.usePermission}>
44
+ * <ManageButton />
45
+ * </PermissionGate>
46
+ *
47
+ * @example
48
+ * // Show permission denied message
49
+ * <PermissionGate
50
+ * permission="catalog.read"
51
+ * usePermission={permissionApi.usePermission}
52
+ * showDenied
53
+ * deniedMessage="You don't have access to view the catalog."
54
+ * >
55
+ * <CatalogList />
56
+ * </PermissionGate>
57
+ *
58
+ * @example
59
+ * // Custom fallback
60
+ * <PermissionGate
61
+ * permission="admin.manage"
62
+ * usePermission={permissionApi.usePermission}
63
+ * fallback={<p>Admin access required</p>}
64
+ * >
65
+ * <AdminPanel />
66
+ * </PermissionGate>
67
+ */
68
+ export const PermissionGate: React.FC<PermissionGateProps> = ({
69
+ permission,
70
+ children,
71
+ fallback,
72
+ showDenied = false,
73
+ deniedMessage,
74
+ usePermission,
75
+ }) => {
76
+ const { loading, allowed } = usePermission(permission);
77
+
78
+ if (loading) {
79
+ return <LoadingSpinner size="sm" />;
80
+ }
81
+
82
+ if (!allowed) {
83
+ if (fallback) {
84
+ return <>{fallback}</>;
85
+ }
86
+ if (showDenied) {
87
+ return (
88
+ <PermissionDenied
89
+ message={deniedMessage ?? `You don't have permission: ${permission}`}
90
+ />
91
+ );
92
+ }
93
+ return <></>;
94
+ }
95
+
96
+ return <>{children}</>;
97
+ };
@@ -0,0 +1,91 @@
1
+ import React from "react";
2
+ import {
3
+ Label,
4
+ Select,
5
+ SelectContent,
6
+ SelectItem,
7
+ SelectTrigger,
8
+ SelectValue,
9
+ DynamicForm,
10
+ } from "..";
11
+ import { JsonSchema } from "./DynamicForm";
12
+
13
+ export interface PluginOption {
14
+ id: string;
15
+ displayName: string;
16
+ description?: string;
17
+ configSchema: JsonSchema;
18
+ }
19
+
20
+ interface PluginConfigFormProps {
21
+ label: string;
22
+ plugins: PluginOption[];
23
+ selectedPluginId: string;
24
+ onPluginChange: (id: string) => void;
25
+ config: Record<string, unknown>;
26
+ onConfigChange: (config: Record<string, unknown>) => void;
27
+ onValidChange?: (isValid: boolean) => void;
28
+ disabled?: boolean;
29
+ }
30
+
31
+ export const PluginConfigForm: React.FC<PluginConfigFormProps> = ({
32
+ label,
33
+ plugins,
34
+ selectedPluginId,
35
+ onPluginChange,
36
+ config,
37
+ onConfigChange,
38
+ onValidChange,
39
+ disabled,
40
+ }) => {
41
+ const selectedPlugin = plugins.find((p) => p.id === selectedPluginId);
42
+
43
+ return (
44
+ <div className="space-y-6">
45
+ <div className="space-y-2">
46
+ <Label htmlFor="plugin-select">{label}</Label>
47
+ <Select
48
+ value={selectedPluginId}
49
+ onValueChange={onPluginChange}
50
+ disabled={disabled}
51
+ >
52
+ <SelectTrigger id="plugin-select">
53
+ <SelectValue placeholder={`Select ${label.toLowerCase()}`} />
54
+ </SelectTrigger>
55
+ <SelectContent>
56
+ {plugins.map((plugin) => (
57
+ <SelectItem key={plugin.id} value={plugin.id}>
58
+ <div>
59
+ <div className="font-medium">{plugin.displayName}</div>
60
+ {plugin.description && (
61
+ <div className="text-xs text-muted-foreground">
62
+ {plugin.description}
63
+ </div>
64
+ )}
65
+ </div>
66
+ </SelectItem>
67
+ ))}
68
+ </SelectContent>
69
+ </Select>
70
+ </div>
71
+
72
+ {selectedPlugin && (
73
+ <div className="space-y-4">
74
+ <div className="pt-4 border-t border-border">
75
+ <h3 className="text-lg font-semibold">Configuration</h3>
76
+ <p className="text-sm text-muted-foreground">
77
+ Configure the settings for {selectedPlugin.displayName}
78
+ </p>
79
+ </div>
80
+
81
+ <DynamicForm
82
+ schema={selectedPlugin.configSchema}
83
+ value={config}
84
+ onChange={onConfigChange}
85
+ onValidChange={onValidChange}
86
+ />
87
+ </div>
88
+ )}
89
+ </div>
90
+ );
91
+ };
@@ -0,0 +1,30 @@
1
+ import React from "react";
2
+ import { cn } from "../utils";
3
+
4
+ interface SectionHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
5
+ title: string;
6
+ icon?: React.ReactNode;
7
+ description?: string;
8
+ }
9
+
10
+ export const SectionHeader: React.FC<SectionHeaderProps> = ({
11
+ title,
12
+ icon,
13
+ description,
14
+ className,
15
+ ...props
16
+ }) => {
17
+ return (
18
+ <div className={cn("flex items-center gap-2 mb-6", className)} {...props}>
19
+ {icon && <div className="text-primary">{icon}</div>}
20
+ <div>
21
+ <h2 className="text-xl font-semibold tracking-tight text-foreground">
22
+ {title}
23
+ </h2>
24
+ {description && (
25
+ <p className="text-sm text-muted-foreground mt-1">{description}</p>
26
+ )}
27
+ </div>
28
+ </div>
29
+ );
30
+ };
@@ -0,0 +1,157 @@
1
+ import * as React from "react";
2
+ import * as SelectPrimitive from "@radix-ui/react-select";
3
+ import { Check, ChevronDown, ChevronUp } from "lucide-react";
4
+ import { cn } from "../utils";
5
+
6
+ const Select = SelectPrimitive.Root;
7
+
8
+ const SelectGroup = SelectPrimitive.Group;
9
+
10
+ const SelectValue = SelectPrimitive.Value;
11
+
12
+ const SelectTrigger = React.forwardRef<
13
+ React.ElementRef<typeof SelectPrimitive.Trigger>,
14
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
15
+ >(({ className, children, ...props }, ref) => (
16
+ <SelectPrimitive.Trigger
17
+ ref={ref}
18
+ className={cn(
19
+ "flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm text-left ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
20
+ className
21
+ )}
22
+ {...props}
23
+ >
24
+ {children}
25
+ <SelectPrimitive.Icon asChild>
26
+ <ChevronDown className="h-4 w-4 opacity-50" />
27
+ </SelectPrimitive.Icon>
28
+ </SelectPrimitive.Trigger>
29
+ ));
30
+ SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
31
+
32
+ const SelectScrollUpButton = React.forwardRef<
33
+ React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
34
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
35
+ >(({ className, ...props }, ref) => (
36
+ <SelectPrimitive.ScrollUpButton
37
+ ref={ref}
38
+ className={cn(
39
+ "flex cursor-default items-center justify-center py-1",
40
+ className
41
+ )}
42
+ {...props}
43
+ >
44
+ <ChevronUp className="h-4 w-4" />
45
+ </SelectPrimitive.ScrollUpButton>
46
+ ));
47
+ SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
48
+
49
+ const SelectScrollDownButton = React.forwardRef<
50
+ React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
51
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
52
+ >(({ className, ...props }, ref) => (
53
+ <SelectPrimitive.ScrollDownButton
54
+ ref={ref}
55
+ className={cn(
56
+ "flex cursor-default items-center justify-center py-1",
57
+ className
58
+ )}
59
+ {...props}
60
+ >
61
+ <ChevronDown className="h-4 w-4" />
62
+ </SelectPrimitive.ScrollDownButton>
63
+ ));
64
+ SelectScrollDownButton.displayName =
65
+ SelectPrimitive.ScrollDownButton.displayName;
66
+
67
+ const SelectContent = React.forwardRef<
68
+ React.ElementRef<typeof SelectPrimitive.Content>,
69
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
70
+ >(({ className, children, position = "popper", ...props }, ref) => (
71
+ <SelectPrimitive.Portal>
72
+ <SelectPrimitive.Content
73
+ ref={ref}
74
+ className={cn(
75
+ "relative z-[100] max-h-96 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
76
+ position === "popper" &&
77
+ "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
78
+ className
79
+ )}
80
+ position={position}
81
+ {...props}
82
+ >
83
+ <SelectScrollUpButton />
84
+ <SelectPrimitive.Viewport
85
+ className={cn(
86
+ "p-1",
87
+ position === "popper" &&
88
+ "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
89
+ )}
90
+ >
91
+ {children}
92
+ </SelectPrimitive.Viewport>
93
+ <SelectScrollDownButton />
94
+ </SelectPrimitive.Content>
95
+ </SelectPrimitive.Portal>
96
+ ));
97
+ SelectContent.displayName = SelectPrimitive.Content.displayName;
98
+
99
+ const SelectLabel = React.forwardRef<
100
+ React.ElementRef<typeof SelectPrimitive.Label>,
101
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
102
+ >(({ className, ...props }, ref) => (
103
+ <SelectPrimitive.Label
104
+ ref={ref}
105
+ className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
106
+ {...props}
107
+ />
108
+ ));
109
+ SelectLabel.displayName = SelectPrimitive.Label.displayName;
110
+
111
+ const SelectItem = React.forwardRef<
112
+ React.ElementRef<typeof SelectPrimitive.Item>,
113
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
114
+ >(({ className, children, ...props }, ref) => (
115
+ <SelectPrimitive.Item
116
+ ref={ref}
117
+ className={cn(
118
+ "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
119
+ className
120
+ )}
121
+ {...props}
122
+ >
123
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
124
+ <SelectPrimitive.ItemIndicator>
125
+ <Check className="h-4 w-4" />
126
+ </SelectPrimitive.ItemIndicator>
127
+ </span>
128
+
129
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
130
+ </SelectPrimitive.Item>
131
+ ));
132
+ SelectItem.displayName = SelectPrimitive.Item.displayName;
133
+
134
+ const SelectSeparator = React.forwardRef<
135
+ React.ElementRef<typeof SelectPrimitive.Separator>,
136
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
137
+ >(({ className, ...props }, ref) => (
138
+ <SelectPrimitive.Separator
139
+ ref={ref}
140
+ className={cn("-mx-1 my-1 h-px bg-muted", className)}
141
+ {...props}
142
+ />
143
+ ));
144
+ SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
145
+
146
+ export {
147
+ Select,
148
+ SelectGroup,
149
+ SelectValue,
150
+ SelectTrigger,
151
+ SelectContent,
152
+ SelectLabel,
153
+ SelectItem,
154
+ SelectSeparator,
155
+ SelectScrollUpButton,
156
+ SelectScrollDownButton,
157
+ };
@@ -0,0 +1,78 @@
1
+ import React from "react";
2
+ import { Card, CardHeader, CardTitle, CardContent } from "./Card";
3
+ import { cn } from "../utils";
4
+
5
+ interface StatusCardProps extends React.HTMLAttributes<HTMLDivElement> {
6
+ title: string;
7
+ value: React.ReactNode;
8
+ description?: string;
9
+ icon?: React.ReactNode;
10
+ variant?: "default" | "gradient";
11
+ }
12
+
13
+ export const StatusCard: React.FC<StatusCardProps> = ({
14
+ title,
15
+ value,
16
+ description,
17
+ icon,
18
+ variant = "default",
19
+ className,
20
+ ...props
21
+ }) => {
22
+ const isGradient = variant === "gradient";
23
+
24
+ return (
25
+ <Card
26
+ className={cn(
27
+ "border-none shadow-sm transition-all duration-200",
28
+ isGradient
29
+ ? "bg-gradient-to-br from-indigo-500 to-purple-600 text-white shadow-md active:scale-[0.98]"
30
+ : "bg-card hover:border-border",
31
+ className
32
+ )}
33
+ {...props}
34
+ >
35
+ <CardHeader className="pb-2">
36
+ <CardTitle
37
+ className={cn(
38
+ "text-lg font-medium",
39
+ isGradient ? "opacity-90 text-white" : "text-muted-foreground"
40
+ )}
41
+ >
42
+ {title}
43
+ </CardTitle>
44
+ </CardHeader>
45
+ <CardContent>
46
+ <div className="flex items-baseline gap-2">
47
+ <span
48
+ className={cn(
49
+ "text-2xl font-semibold",
50
+ isGradient ? "text-3xl font-bold" : "text-foreground"
51
+ )}
52
+ >
53
+ {value}
54
+ </span>
55
+ {icon && (
56
+ <div
57
+ className={cn(
58
+ isGradient ? "text-white" : "text-muted-foreground/60"
59
+ )}
60
+ >
61
+ {icon}
62
+ </div>
63
+ )}
64
+ </div>
65
+ {description && (
66
+ <p
67
+ className={cn(
68
+ "mt-1 text-sm",
69
+ isGradient ? "opacity-80" : "text-muted-foreground"
70
+ )}
71
+ >
72
+ {description}
73
+ </p>
74
+ )}
75
+ </CardContent>
76
+ </Card>
77
+ );
78
+ };