@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,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
|
+
};
|