@checkstack/ui 1.3.5 → 1.4.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.
- package/CHANGELOG.md +41 -0
- package/package.json +7 -5
- package/src/components/DateRangeFilter.tsx +2 -2
- package/src/components/DropdownMenu.tsx +111 -23
- package/src/components/MetricTile.tsx +50 -0
- package/src/components/PageLayout.tsx +1 -1
- package/src/components/Sheet.tsx +141 -0
- package/src/components/StatusCard.tsx +6 -6
- package/src/components/UserMenu.tsx +6 -1
- package/src/index.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# @checkstack/ui
|
|
2
2
|
|
|
3
|
+
## 1.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- bb1fea0: Redesign system detail page with hero banner, two-column layout, plugin metric tiles, and health check slide-over drawer.
|
|
8
|
+
|
|
9
|
+
### New Components
|
|
10
|
+
|
|
11
|
+
- **MetricTile** (`@checkstack/ui`): Compact stat tile with icon, label, value, variant coloring
|
|
12
|
+
- **Sheet** (`@checkstack/ui`): Slide-over drawer built on Radix Dialog primitives
|
|
13
|
+
|
|
14
|
+
### New Extension Slot
|
|
15
|
+
|
|
16
|
+
- **SystemOverviewMetricsSlot** (`@checkstack/catalog-common`): Plugin-contributed at-a-glance metric tiles in the system detail hero banner
|
|
17
|
+
|
|
18
|
+
### Layout Changes
|
|
19
|
+
|
|
20
|
+
- System detail page now uses a hero banner with breadcrumb, status badges, and metric tile strip
|
|
21
|
+
- Two-column layout: monitoring content (left) and system context (right)
|
|
22
|
+
- Health checks rendered as compact card rows instead of heavy accordions
|
|
23
|
+
- Clicking a health check opens a slide-over drawer with summary tiles, timeline charts, and recent runs
|
|
24
|
+
- Right column uses lightweight borderless sections with dividers instead of heavy Card wrappers
|
|
25
|
+
|
|
26
|
+
### Plugin Extensions
|
|
27
|
+
|
|
28
|
+
- Health check, SLO, Incident, and Maintenance plugins each contribute a metric tile to the hero banner
|
|
29
|
+
|
|
30
|
+
### Patch Changes
|
|
31
|
+
|
|
32
|
+
- bb1fea0: feat: implement active incident and maintenance overview sheets on dashboard
|
|
33
|
+
|
|
34
|
+
- Replaces direct routing on status cards with slide-out overview sheets to gracefully degrade for users without manage permissions
|
|
35
|
+
- Refactors dashboard system groups into a clean table-style list layout for better density
|
|
36
|
+
- Makes global status cards more compact
|
|
37
|
+
|
|
38
|
+
## 1.3.6
|
|
39
|
+
|
|
40
|
+
### Patch Changes
|
|
41
|
+
|
|
42
|
+
- 4b0934d: Refactored UserMenu to use a responsive grid layout, improved menu item alignment, and implemented a full-screen scrollable portal for mobile devices. Fixed an issue where the UserMenu would instantly close and reopen when clicking the trigger while the menu was open.
|
|
43
|
+
|
|
3
44
|
## 1.3.5
|
|
4
45
|
|
|
5
46
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"dependencies": {
|
|
@@ -21,18 +21,20 @@
|
|
|
21
21
|
"monaco-editor": "^0.55.1",
|
|
22
22
|
"react": "^18.2.0",
|
|
23
23
|
"react-day-picker": "^9.13.0",
|
|
24
|
+
"react-dom": "^19.2.5",
|
|
24
25
|
"react-markdown": "^10.1.0",
|
|
25
26
|
"react-router-dom": "^6.20.0",
|
|
26
27
|
"recharts": "^3.6.0",
|
|
27
28
|
"tailwind-merge": "^2.2.0"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
30
|
-
"
|
|
31
|
-
"@types/react": "^18.2.0",
|
|
32
|
-
"@testing-library/react": "^16.0.0",
|
|
31
|
+
"@checkstack/scripts": "0.1.2",
|
|
33
32
|
"@checkstack/test-utils-frontend": "0.0.4",
|
|
34
33
|
"@checkstack/tsconfig": "0.0.5",
|
|
35
|
-
"@
|
|
34
|
+
"@testing-library/react": "^16.0.0",
|
|
35
|
+
"@types/react": "^18.2.0",
|
|
36
|
+
"@types/react-dom": "^19.2.3",
|
|
37
|
+
"typescript": "^5.0.0"
|
|
36
38
|
},
|
|
37
39
|
"scripts": {
|
|
38
40
|
"typecheck": "tsc --noEmit",
|
|
@@ -27,7 +27,7 @@ export interface DateRangeFilterProps {
|
|
|
27
27
|
className?: string;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
const PRESETS: Array<{
|
|
30
|
+
export const PRESETS: Array<{
|
|
31
31
|
id: DateRangePreset;
|
|
32
32
|
label: string;
|
|
33
33
|
shortLabel: string;
|
|
@@ -56,7 +56,7 @@ export function getPresetRange(preset: DateRangePreset): DateRange {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
function detectPreset(range: DateRange): DateRangePreset {
|
|
59
|
+
export function detectPreset(range: DateRange): DateRangePreset {
|
|
60
60
|
const now = new Date();
|
|
61
61
|
const diffMs = now.getTime() - range.startDate.getTime();
|
|
62
62
|
const diffHours = diffMs / (1000 * 60 * 60);
|
|
@@ -1,11 +1,43 @@
|
|
|
1
|
-
import React, { useRef, useEffect } from "react";
|
|
1
|
+
import React, { useRef, useEffect, createContext, useContext, useState } from "react";
|
|
2
|
+
import { createPortal } from "react-dom";
|
|
3
|
+
import { X } from "lucide-react";
|
|
2
4
|
import { cn } from "../utils";
|
|
3
5
|
import { usePerformance } from "./PerformanceProvider";
|
|
4
6
|
|
|
7
|
+
export const DropdownMenuContext = createContext<{ onClose?: () => void }>({});
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Custom hook to detect mobile viewport.
|
|
11
|
+
*/
|
|
12
|
+
export function useIsMobile() {
|
|
13
|
+
const [isMobile, setIsMobile] = useState(false);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
// Initial check
|
|
17
|
+
const mql = globalThis.matchMedia("(max-width: 640px)");
|
|
18
|
+
setIsMobile(mql.matches);
|
|
19
|
+
|
|
20
|
+
// Listener for changes
|
|
21
|
+
const handler = (e: MediaQueryListEvent) => setIsMobile(e.matches);
|
|
22
|
+
mql.addEventListener("change", handler);
|
|
23
|
+
return () => mql.removeEventListener("change", handler);
|
|
24
|
+
}, []);
|
|
25
|
+
|
|
26
|
+
return isMobile;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
export const DropdownMenuRootContext = createContext<{ rootRef?: React.RefObject<HTMLDivElement> }>({});
|
|
31
|
+
|
|
5
32
|
export const DropdownMenu: React.FC<{ children: React.ReactNode }> = ({
|
|
6
33
|
children,
|
|
7
34
|
}) => {
|
|
8
|
-
|
|
35
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
36
|
+
return (
|
|
37
|
+
<DropdownMenuRootContext.Provider value={{ rootRef }}>
|
|
38
|
+
<div ref={rootRef} className="relative inline-block text-left">{children}</div>
|
|
39
|
+
</DropdownMenuRootContext.Provider>
|
|
40
|
+
);
|
|
9
41
|
};
|
|
10
42
|
|
|
11
43
|
export const DropdownMenuTrigger: React.FC<{
|
|
@@ -25,44 +57,83 @@ export const DropdownMenuContent: React.FC<{
|
|
|
25
57
|
isOpen: boolean;
|
|
26
58
|
onClose: () => void;
|
|
27
59
|
className?: string;
|
|
28
|
-
|
|
60
|
+
innerClassName?: string;
|
|
61
|
+
}> = ({ children, isOpen, onClose, className, innerClassName }) => {
|
|
29
62
|
const { isLowPower } = usePerformance();
|
|
30
63
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
64
|
+
const { rootRef } = useContext(DropdownMenuRootContext);
|
|
65
|
+
const isMobile = useIsMobile();
|
|
31
66
|
|
|
32
67
|
useEffect(() => {
|
|
33
68
|
const handleClickOutside = (event: MouseEvent) => {
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
69
|
+
// Don't auto-close if clicking outside on mobile (full screen modal style)
|
|
70
|
+
if (isMobile) return;
|
|
71
|
+
|
|
72
|
+
const target = event.target as Node;
|
|
73
|
+
|
|
74
|
+
// Don't close if clicking inside the menu content
|
|
75
|
+
if (contentRef.current && contentRef.current.contains(target)) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Don't close if clicking inside the wrapper (e.g. the trigger button)
|
|
80
|
+
// This allows the trigger's onClick to handle toggling the menu without conflict.
|
|
81
|
+
if (rootRef?.current && rootRef.current.contains(target)) {
|
|
82
|
+
return;
|
|
39
83
|
}
|
|
84
|
+
|
|
85
|
+
onClose();
|
|
40
86
|
};
|
|
41
87
|
|
|
42
88
|
if (isOpen) {
|
|
43
89
|
document.addEventListener("mousedown", handleClickOutside);
|
|
90
|
+
// Lock body scroll on mobile
|
|
91
|
+
if (isMobile) {
|
|
92
|
+
document.body.style.overflow = "hidden";
|
|
93
|
+
}
|
|
44
94
|
}
|
|
45
95
|
return () => {
|
|
46
96
|
document.removeEventListener("mousedown", handleClickOutside);
|
|
97
|
+
if (isMobile) {
|
|
98
|
+
document.body.style.overflow = "";
|
|
99
|
+
}
|
|
47
100
|
};
|
|
48
|
-
}, [isOpen, onClose]);
|
|
101
|
+
}, [isOpen, onClose, isMobile, rootRef]);
|
|
49
102
|
|
|
50
103
|
if (!isOpen) return <React.Fragment />;
|
|
51
104
|
|
|
52
|
-
|
|
105
|
+
const content = (
|
|
53
106
|
<div
|
|
54
107
|
ref={contentRef}
|
|
55
108
|
className={cn(
|
|
56
|
-
|
|
109
|
+
isMobile
|
|
110
|
+
? "fixed inset-0 z-[100] bg-background w-full h-full p-4 overflow-y-auto"
|
|
111
|
+
: "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] max-h-[calc(100vh-4rem)] overflow-y-auto",
|
|
57
112
|
!isLowPower && "animate-in fade-in zoom-in-95 duration-100",
|
|
58
113
|
className
|
|
59
114
|
)}
|
|
60
115
|
>
|
|
61
|
-
<
|
|
62
|
-
{
|
|
63
|
-
|
|
116
|
+
<DropdownMenuContext.Provider value={{ onClose }}>
|
|
117
|
+
<div className={cn(isMobile ? "flex flex-col pt-4 pb-12" : "py-1", innerClassName)} role="none">
|
|
118
|
+
{isMobile && (
|
|
119
|
+
<div className="flex justify-between items-center mb-6 col-span-full px-2">
|
|
120
|
+
<h2 className="text-xl font-bold text-foreground">Menu</h2>
|
|
121
|
+
<button
|
|
122
|
+
onClick={onClose}
|
|
123
|
+
className="p-2 rounded-full hover:bg-accent text-muted-foreground transition-colors"
|
|
124
|
+
aria-label="Close menu"
|
|
125
|
+
>
|
|
126
|
+
<X className="w-6 h-6" />
|
|
127
|
+
</button>
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
{children}
|
|
131
|
+
</div>
|
|
132
|
+
</DropdownMenuContext.Provider>
|
|
64
133
|
</div>
|
|
65
134
|
);
|
|
135
|
+
|
|
136
|
+
return isMobile ? createPortal(content, document.body) : content;
|
|
66
137
|
};
|
|
67
138
|
|
|
68
139
|
export const DropdownMenuItem: React.FC<{
|
|
@@ -70,30 +141,47 @@ export const DropdownMenuItem: React.FC<{
|
|
|
70
141
|
onClick?: () => void;
|
|
71
142
|
className?: string;
|
|
72
143
|
icon?: React.ReactNode;
|
|
73
|
-
|
|
144
|
+
description?: React.ReactNode;
|
|
145
|
+
closeOnClick?: boolean;
|
|
146
|
+
}> = ({ children, onClick, className, icon, description, closeOnClick = true }) => {
|
|
147
|
+
const { onClose } = useContext(DropdownMenuContext);
|
|
148
|
+
|
|
149
|
+
const handleClick = () => {
|
|
150
|
+
if (onClick) onClick();
|
|
151
|
+
if (closeOnClick && onClose) onClose();
|
|
152
|
+
};
|
|
153
|
+
|
|
74
154
|
return (
|
|
75
155
|
<button
|
|
76
|
-
onClick={
|
|
156
|
+
onClick={handleClick}
|
|
77
157
|
className={cn(
|
|
78
|
-
"flex items-
|
|
158
|
+
"flex flex-col items-start w-full px-4 py-2 text-sm text-popover-foreground hover:bg-accent hover:text-accent-foreground transition-colors rounded-sm overflow-hidden",
|
|
79
159
|
className
|
|
80
160
|
)}
|
|
81
161
|
role="menuitem"
|
|
82
162
|
>
|
|
83
|
-
|
|
84
|
-
|
|
163
|
+
<div className="flex items-center w-full overflow-hidden">
|
|
164
|
+
{icon && <span className="mr-3 text-muted-foreground shrink-0">{icon}</span>}
|
|
165
|
+
<span className="flex-1 text-left truncate">{children}</span>
|
|
166
|
+
</div>
|
|
167
|
+
{description && (
|
|
168
|
+
<span className={cn("text-[10px] text-muted-foreground mt-1 leading-tight text-left truncate w-full", icon ? "pl-7" : "")}>
|
|
169
|
+
{description}
|
|
170
|
+
</span>
|
|
171
|
+
)}
|
|
85
172
|
</button>
|
|
86
173
|
);
|
|
87
174
|
};
|
|
88
175
|
|
|
89
|
-
export const DropdownMenuSeparator: React.FC = () => (
|
|
90
|
-
<div className="my-1 h-px bg-border" />
|
|
176
|
+
export const DropdownMenuSeparator: React.FC<{ className?: string }> = ({ className }) => (
|
|
177
|
+
<div className={cn("my-1 h-px bg-border col-span-full", className)} />
|
|
91
178
|
);
|
|
92
179
|
|
|
93
|
-
export const DropdownMenuLabel: React.FC<{ children: React.ReactNode }> = ({
|
|
180
|
+
export const DropdownMenuLabel: React.FC<{ children: React.ReactNode; className?: string }> = ({
|
|
94
181
|
children,
|
|
182
|
+
className
|
|
95
183
|
}) => (
|
|
96
|
-
<div className="px-4 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
|
184
|
+
<div className={cn("px-4 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wider col-span-full", className)}>
|
|
97
185
|
{children}
|
|
98
186
|
</div>
|
|
99
187
|
);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { cn } from "../utils";
|
|
4
|
+
|
|
5
|
+
const metricTileVariants = cva(
|
|
6
|
+
"flex items-center gap-3 rounded-lg border bg-card p-3 min-w-0",
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default: "border-border",
|
|
11
|
+
success: "border-success/30 bg-success/5",
|
|
12
|
+
warning: "border-warning/30 bg-warning/5",
|
|
13
|
+
destructive: "border-destructive/30 bg-destructive/5",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
defaultVariants: {
|
|
17
|
+
variant: "default",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
interface MetricTileProps
|
|
23
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
24
|
+
VariantProps<typeof metricTileVariants> {
|
|
25
|
+
icon: React.ElementType;
|
|
26
|
+
label: string;
|
|
27
|
+
value: string;
|
|
28
|
+
subtitle?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const MetricTile: React.FC<MetricTileProps> = ({
|
|
32
|
+
icon: Icon,
|
|
33
|
+
label,
|
|
34
|
+
value,
|
|
35
|
+
subtitle,
|
|
36
|
+
variant,
|
|
37
|
+
className,
|
|
38
|
+
...props
|
|
39
|
+
}) => (
|
|
40
|
+
<div className={cn(metricTileVariants({ variant }), className)} {...props}>
|
|
41
|
+
<Icon className="h-4 w-4 text-muted-foreground shrink-0" />
|
|
42
|
+
<div className="min-w-0">
|
|
43
|
+
<p className="text-xs text-muted-foreground truncate">{label}</p>
|
|
44
|
+
<p className="text-sm font-semibold truncate">{value}</p>
|
|
45
|
+
{subtitle && (
|
|
46
|
+
<p className="text-xs text-muted-foreground truncate">{subtitle}</p>
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
@@ -38,7 +38,7 @@ export const PageLayout: React.FC<PageLayoutProps> = ({
|
|
|
38
38
|
loading,
|
|
39
39
|
allowed,
|
|
40
40
|
children,
|
|
41
|
-
maxWidth = "
|
|
41
|
+
maxWidth = "7xl",
|
|
42
42
|
}) => {
|
|
43
43
|
// If loading is explicitly true, show loading state
|
|
44
44
|
// If loading is undefined and allowed is false, also show loading state
|
|
@@ -0,0 +1,141 @@
|
|
|
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 { X } from "lucide-react";
|
|
5
|
+
import { cn } from "../utils";
|
|
6
|
+
import { usePerformance } from "./PerformanceProvider";
|
|
7
|
+
|
|
8
|
+
const Sheet = DialogPrimitive.Root;
|
|
9
|
+
const SheetTrigger = DialogPrimitive.Trigger;
|
|
10
|
+
const SheetClose = DialogPrimitive.Close;
|
|
11
|
+
const SheetPortal = DialogPrimitive.Portal;
|
|
12
|
+
|
|
13
|
+
const SheetOverlay = React.forwardRef<
|
|
14
|
+
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
|
15
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
|
16
|
+
>(({ className, ...props }, ref) => {
|
|
17
|
+
const { isLowPower } = usePerformance();
|
|
18
|
+
return (
|
|
19
|
+
<DialogPrimitive.Overlay
|
|
20
|
+
ref={ref}
|
|
21
|
+
className={cn(
|
|
22
|
+
"fixed inset-0 z-50 bg-black/50",
|
|
23
|
+
!isLowPower &&
|
|
24
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
25
|
+
className,
|
|
26
|
+
)}
|
|
27
|
+
{...props}
|
|
28
|
+
/>
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
SheetOverlay.displayName = "SheetOverlay";
|
|
32
|
+
|
|
33
|
+
const sheetContentVariants = cva(
|
|
34
|
+
"fixed z-50 flex flex-col bg-background shadow-lg border-l border-border",
|
|
35
|
+
{
|
|
36
|
+
variants: {
|
|
37
|
+
size: {
|
|
38
|
+
default: "w-full sm:max-w-lg",
|
|
39
|
+
lg: "w-full sm:max-w-2xl",
|
|
40
|
+
xl: "w-full sm:max-w-4xl",
|
|
41
|
+
full: "w-full",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
defaultVariants: {
|
|
45
|
+
size: "default",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
interface SheetContentProps
|
|
51
|
+
extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>,
|
|
52
|
+
VariantProps<typeof sheetContentVariants> {}
|
|
53
|
+
|
|
54
|
+
const SheetContent = React.forwardRef<
|
|
55
|
+
React.ElementRef<typeof DialogPrimitive.Content>,
|
|
56
|
+
SheetContentProps
|
|
57
|
+
>(({ className, children, size, ...props }, ref) => {
|
|
58
|
+
const { isLowPower } = usePerformance();
|
|
59
|
+
return (
|
|
60
|
+
<SheetPortal>
|
|
61
|
+
<SheetOverlay />
|
|
62
|
+
<DialogPrimitive.Content
|
|
63
|
+
ref={ref}
|
|
64
|
+
className={cn(
|
|
65
|
+
sheetContentVariants({ size }),
|
|
66
|
+
"inset-y-0 right-0 h-full",
|
|
67
|
+
!isLowPower &&
|
|
68
|
+
"duration-300 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right",
|
|
69
|
+
className,
|
|
70
|
+
)}
|
|
71
|
+
{...props}
|
|
72
|
+
>
|
|
73
|
+
{children}
|
|
74
|
+
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2">
|
|
75
|
+
<X className="h-4 w-4" />
|
|
76
|
+
<span className="sr-only">Close</span>
|
|
77
|
+
</DialogPrimitive.Close>
|
|
78
|
+
</DialogPrimitive.Content>
|
|
79
|
+
</SheetPortal>
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
SheetContent.displayName = "SheetContent";
|
|
83
|
+
|
|
84
|
+
const SheetHeader = ({
|
|
85
|
+
className,
|
|
86
|
+
...props
|
|
87
|
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
88
|
+
<div
|
|
89
|
+
className={cn(
|
|
90
|
+
"flex flex-col gap-1.5 p-6 pb-4 border-b border-border",
|
|
91
|
+
className,
|
|
92
|
+
)}
|
|
93
|
+
{...props}
|
|
94
|
+
/>
|
|
95
|
+
);
|
|
96
|
+
SheetHeader.displayName = "SheetHeader";
|
|
97
|
+
|
|
98
|
+
const SheetTitle = React.forwardRef<
|
|
99
|
+
React.ElementRef<typeof DialogPrimitive.Title>,
|
|
100
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
|
101
|
+
>(({ className, ...props }, ref) => (
|
|
102
|
+
<DialogPrimitive.Title
|
|
103
|
+
ref={ref}
|
|
104
|
+
className={cn("text-lg font-semibold", className)}
|
|
105
|
+
{...props}
|
|
106
|
+
/>
|
|
107
|
+
));
|
|
108
|
+
SheetTitle.displayName = "SheetTitle";
|
|
109
|
+
|
|
110
|
+
const SheetDescription = React.forwardRef<
|
|
111
|
+
React.ElementRef<typeof DialogPrimitive.Description>,
|
|
112
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
|
113
|
+
>(({ className, ...props }, ref) => (
|
|
114
|
+
<DialogPrimitive.Description
|
|
115
|
+
ref={ref}
|
|
116
|
+
className={cn("text-sm text-muted-foreground", className)}
|
|
117
|
+
{...props}
|
|
118
|
+
/>
|
|
119
|
+
));
|
|
120
|
+
SheetDescription.displayName = "SheetDescription";
|
|
121
|
+
|
|
122
|
+
const SheetBody = ({
|
|
123
|
+
className,
|
|
124
|
+
...props
|
|
125
|
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
126
|
+
<div className={cn("flex-1 overflow-y-auto p-6", className)} {...props} />
|
|
127
|
+
);
|
|
128
|
+
SheetBody.displayName = "SheetBody";
|
|
129
|
+
|
|
130
|
+
export {
|
|
131
|
+
Sheet,
|
|
132
|
+
SheetPortal,
|
|
133
|
+
SheetOverlay,
|
|
134
|
+
SheetTrigger,
|
|
135
|
+
SheetClose,
|
|
136
|
+
SheetContent,
|
|
137
|
+
SheetHeader,
|
|
138
|
+
SheetBody,
|
|
139
|
+
SheetTitle,
|
|
140
|
+
SheetDescription,
|
|
141
|
+
};
|
|
@@ -32,22 +32,22 @@ export const StatusCard: React.FC<StatusCardProps> = ({
|
|
|
32
32
|
)}
|
|
33
33
|
{...props}
|
|
34
34
|
>
|
|
35
|
-
<CardHeader className="pb-2">
|
|
35
|
+
<CardHeader className="p-4 pb-2">
|
|
36
36
|
<CardTitle
|
|
37
37
|
className={cn(
|
|
38
|
-
"text-
|
|
38
|
+
"text-sm font-medium",
|
|
39
39
|
isGradient ? "opacity-90 text-white" : "text-muted-foreground"
|
|
40
40
|
)}
|
|
41
41
|
>
|
|
42
42
|
{title}
|
|
43
43
|
</CardTitle>
|
|
44
44
|
</CardHeader>
|
|
45
|
-
<CardContent>
|
|
45
|
+
<CardContent className="p-4 pt-0">
|
|
46
46
|
<div className="flex items-baseline gap-2">
|
|
47
47
|
<span
|
|
48
48
|
className={cn(
|
|
49
|
-
"text-2xl font-
|
|
50
|
-
isGradient ? "text-
|
|
49
|
+
"text-2xl font-bold tracking-tight",
|
|
50
|
+
isGradient ? "text-white" : "text-foreground"
|
|
51
51
|
)}
|
|
52
52
|
>
|
|
53
53
|
{value}
|
|
@@ -65,7 +65,7 @@ export const StatusCard: React.FC<StatusCardProps> = ({
|
|
|
65
65
|
{description && (
|
|
66
66
|
<p
|
|
67
67
|
className={cn(
|
|
68
|
-
"mt-1 text-
|
|
68
|
+
"mt-1 text-xs",
|
|
69
69
|
isGradient ? "opacity-80" : "text-muted-foreground"
|
|
70
70
|
)}
|
|
71
71
|
>
|
|
@@ -60,7 +60,12 @@ export const UserMenu: React.FC<UserMenuProps> = ({
|
|
|
60
60
|
</button>
|
|
61
61
|
</DropdownMenuTrigger>
|
|
62
62
|
|
|
63
|
-
<DropdownMenuContent
|
|
63
|
+
<DropdownMenuContent
|
|
64
|
+
isOpen={isOpen}
|
|
65
|
+
onClose={() => setIsOpen(false)}
|
|
66
|
+
className="w-full sm:w-[400px] md:w-[460px]"
|
|
67
|
+
innerClassName="grid grid-cols-1 sm:grid-cols-2 p-2 gap-2 sm:gap-1"
|
|
68
|
+
>
|
|
64
69
|
<DropdownMenuLabel>
|
|
65
70
|
<div className="flex flex-col">
|
|
66
71
|
<span className="text-sm font-bold text-foreground truncate">
|
package/src/index.ts
CHANGED