@checkstack/ui 1.3.6 → 1.5.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 +46 -0
- package/package.json +1 -1
- package/src/components/DateRangeFilter.tsx +2 -2
- package/src/components/MetricTile.tsx +50 -0
- package/src/components/NotFound.tsx +154 -0
- package/src/components/PageLayout.tsx +1 -1
- package/src/components/Sheet.tsx +141 -0
- package/src/components/StatusCard.tsx +6 -6
- package/src/index.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
# @checkstack/ui
|
|
2
2
|
|
|
3
|
+
## 1.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 3da7582: Fix favicon not loading in production container and add NotFound page
|
|
8
|
+
|
|
9
|
+
- **Backend**: Fix static file serving so root-level files like `/favicon.svg` are served from the dist directory before the SPA fallback catches them
|
|
10
|
+
- **UI**: Add `NotFound` component with stacked-checkmark logo, physics-inspired falling "4" animation, and low-power device fallback
|
|
11
|
+
- **Frontend**: Add catch-all `*` route to display the NotFound page for unmatched routes, and add the Checkstack logo to the navbar
|
|
12
|
+
- **Favicon**: Redesign with stacked checkmarks in the brand purple/indigo palette
|
|
13
|
+
|
|
14
|
+
## 1.4.0
|
|
15
|
+
|
|
16
|
+
### Minor Changes
|
|
17
|
+
|
|
18
|
+
- bb1fea0: Redesign system detail page with hero banner, two-column layout, plugin metric tiles, and health check slide-over drawer.
|
|
19
|
+
|
|
20
|
+
### New Components
|
|
21
|
+
|
|
22
|
+
- **MetricTile** (`@checkstack/ui`): Compact stat tile with icon, label, value, variant coloring
|
|
23
|
+
- **Sheet** (`@checkstack/ui`): Slide-over drawer built on Radix Dialog primitives
|
|
24
|
+
|
|
25
|
+
### New Extension Slot
|
|
26
|
+
|
|
27
|
+
- **SystemOverviewMetricsSlot** (`@checkstack/catalog-common`): Plugin-contributed at-a-glance metric tiles in the system detail hero banner
|
|
28
|
+
|
|
29
|
+
### Layout Changes
|
|
30
|
+
|
|
31
|
+
- System detail page now uses a hero banner with breadcrumb, status badges, and metric tile strip
|
|
32
|
+
- Two-column layout: monitoring content (left) and system context (right)
|
|
33
|
+
- Health checks rendered as compact card rows instead of heavy accordions
|
|
34
|
+
- Clicking a health check opens a slide-over drawer with summary tiles, timeline charts, and recent runs
|
|
35
|
+
- Right column uses lightweight borderless sections with dividers instead of heavy Card wrappers
|
|
36
|
+
|
|
37
|
+
### Plugin Extensions
|
|
38
|
+
|
|
39
|
+
- Health check, SLO, Incident, and Maintenance plugins each contribute a metric tile to the hero banner
|
|
40
|
+
|
|
41
|
+
### Patch Changes
|
|
42
|
+
|
|
43
|
+
- bb1fea0: feat: implement active incident and maintenance overview sheets on dashboard
|
|
44
|
+
|
|
45
|
+
- Replaces direct routing on status cards with slide-out overview sheets to gracefully degrade for users without manage permissions
|
|
46
|
+
- Refactors dashboard system groups into a clean table-style list layout for better density
|
|
47
|
+
- Makes global status cards more compact
|
|
48
|
+
|
|
3
49
|
## 1.3.6
|
|
4
50
|
|
|
5
51
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -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);
|
|
@@ -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
|
+
);
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import React, { useState, useEffect, useMemo } from "react";
|
|
2
|
+
import { useNavigate } from "react-router-dom";
|
|
3
|
+
import { ArrowLeft } from "lucide-react";
|
|
4
|
+
import { cn } from "../utils";
|
|
5
|
+
import { usePerformance } from "./PerformanceProvider";
|
|
6
|
+
|
|
7
|
+
/** Rotating tech-insider quips shown on the 404 page. */
|
|
8
|
+
const NOT_FOUND_QUIPS = [
|
|
9
|
+
// DevOps / infra humor
|
|
10
|
+
"Looks like this route didn't pass the health check.",
|
|
11
|
+
"This endpoint has an uptime of exactly 0%.",
|
|
12
|
+
"We checked the stack. This page isn't in it.",
|
|
13
|
+
"Our monitoring confirms: this page is down. Permanently.",
|
|
14
|
+
"This page was last seen in a git stash from 2019.",
|
|
15
|
+
String.raw`Incident report: page not found. Severity: ¯\_(ツ)_/¯`,
|
|
16
|
+
"DNS resolved. TCP connected. Page? Gone.",
|
|
17
|
+
"kubectl get page — error: resource not found.",
|
|
18
|
+
"This page is in a pending state. It may never resolve.",
|
|
19
|
+
"The deployment was successful. The page was not.",
|
|
20
|
+
"This route has been deprecated without notice.",
|
|
21
|
+
"Alert triggered: page_exists = false.",
|
|
22
|
+
// Programming jokes
|
|
23
|
+
"The page you're looking for is in another castle.",
|
|
24
|
+
"404: The page was found, then garbage collected.",
|
|
25
|
+
"Segfault at 0x00000404.",
|
|
26
|
+
"The page exists in the dev environment, we promise.",
|
|
27
|
+
"Works on my machine ™",
|
|
28
|
+
"Have you tried turning the URL off and on again?",
|
|
29
|
+
"This page is not a bug, it's an undocumented feature.",
|
|
30
|
+
"Error 404: Coffee not found. Page also missing.",
|
|
31
|
+
"git log --all --oneline | grep 'this page' → no results.",
|
|
32
|
+
// Pop culture references
|
|
33
|
+
"These aren't the pages you're looking for. — Obi-Wan Kenobi",
|
|
34
|
+
"I am inevitable. This page is not. — Thanos",
|
|
35
|
+
"One does not simply navigate to a page that doesn't exist.",
|
|
36
|
+
"In case I don't see ya: good afternoon, good evening, and good 404.",
|
|
37
|
+
"It's a feature, not a bug. — Every PM ever",
|
|
38
|
+
"Ah yes, the 404. The page that lived… briefly.",
|
|
39
|
+
"I've seen things you people wouldn't believe. But not this page.",
|
|
40
|
+
"To 404, or not to 404. That is the question.",
|
|
41
|
+
"Houston, we have a 404.",
|
|
42
|
+
] as const;
|
|
43
|
+
|
|
44
|
+
export const NotFound: React.FC<{
|
|
45
|
+
message?: string;
|
|
46
|
+
className?: string;
|
|
47
|
+
}> = ({ message, className }) => {
|
|
48
|
+
const { isLowPower } = usePerformance();
|
|
49
|
+
const navigate = useNavigate();
|
|
50
|
+
const [hasFallen, setHasFallen] = useState(false);
|
|
51
|
+
|
|
52
|
+
const quip = useMemo(
|
|
53
|
+
() =>
|
|
54
|
+
message ??
|
|
55
|
+
NOT_FOUND_QUIPS[Math.floor(Math.random() * NOT_FOUND_QUIPS.length)],
|
|
56
|
+
[message],
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (isLowPower) return;
|
|
61
|
+
const timer = setTimeout(() => setHasFallen(true), 1800);
|
|
62
|
+
return () => clearTimeout(timer);
|
|
63
|
+
}, [isLowPower]);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
className={cn(
|
|
68
|
+
"flex flex-col items-center justify-center min-h-[60vh] p-8 select-none overflow-hidden",
|
|
69
|
+
className,
|
|
70
|
+
)}
|
|
71
|
+
>
|
|
72
|
+
{/* Physics keyframes for the falling "4" */}
|
|
73
|
+
{!isLowPower && (
|
|
74
|
+
<style>{`
|
|
75
|
+
@keyframes wobble-fall {
|
|
76
|
+
0% { transform: rotate(0deg) translateY(0); }
|
|
77
|
+
15% { transform: rotate(-2deg) translateY(0); }
|
|
78
|
+
30% { transform: rotate(3deg) translateY(0); }
|
|
79
|
+
42% { transform: rotate(6deg) translateY(2px); }
|
|
80
|
+
65% { transform: rotate(55deg) translateY(30px); }
|
|
81
|
+
78% { transform: rotate(80deg) translateY(50px); }
|
|
82
|
+
86% { transform: rotate(72deg) translateY(45px); }
|
|
83
|
+
93% { transform: rotate(78deg) translateY(52px); }
|
|
84
|
+
100% { transform: rotate(76deg) translateY(50px); opacity: 0.25; }
|
|
85
|
+
}
|
|
86
|
+
`}</style>
|
|
87
|
+
)}
|
|
88
|
+
|
|
89
|
+
{/* 404 display */}
|
|
90
|
+
<div className="relative mb-8">
|
|
91
|
+
{/* Glow effect */}
|
|
92
|
+
{!isLowPower && (
|
|
93
|
+
<div
|
|
94
|
+
className="absolute inset-0 blur-3xl opacity-15 bg-primary rounded-full scale-150"
|
|
95
|
+
aria-hidden="true"
|
|
96
|
+
/>
|
|
97
|
+
)}
|
|
98
|
+
<div className="relative grid grid-cols-[1fr_auto_1fr] items-center">
|
|
99
|
+
<span className="text-center text-[8rem] md:text-[12rem] font-black leading-none tabular-nums text-muted-foreground/50">
|
|
100
|
+
4
|
|
101
|
+
</span>
|
|
102
|
+
{/* Checkstack logo as the "0" */}
|
|
103
|
+
<div className="flex items-center justify-center w-28 h-28 md:w-44 md:h-44 mx-1 md:mx-3">
|
|
104
|
+
<img
|
|
105
|
+
src="/favicon.svg"
|
|
106
|
+
alt=""
|
|
107
|
+
aria-hidden="true"
|
|
108
|
+
className="w-full h-full"
|
|
109
|
+
/>
|
|
110
|
+
</div>
|
|
111
|
+
<span
|
|
112
|
+
className="text-center text-[8rem] md:text-[12rem] font-black leading-none tabular-nums text-muted-foreground/50"
|
|
113
|
+
style={
|
|
114
|
+
!isLowPower && hasFallen
|
|
115
|
+
? {
|
|
116
|
+
animation:
|
|
117
|
+
"wobble-fall 1.4s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards",
|
|
118
|
+
transformOrigin: "bottom left",
|
|
119
|
+
}
|
|
120
|
+
: undefined
|
|
121
|
+
}
|
|
122
|
+
>
|
|
123
|
+
4
|
|
124
|
+
</span>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
{/* Text content */}
|
|
129
|
+
<div className="text-center space-y-3 max-w-md">
|
|
130
|
+
<h2 className="text-xl md:text-2xl font-semibold text-foreground">
|
|
131
|
+
Route not found
|
|
132
|
+
</h2>
|
|
133
|
+
<p className="text-sm md:text-base text-muted-foreground leading-relaxed">
|
|
134
|
+
{quip}
|
|
135
|
+
</p>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
{/* Action */}
|
|
139
|
+
<button
|
|
140
|
+
type="button"
|
|
141
|
+
onClick={() => navigate("/")}
|
|
142
|
+
className={cn(
|
|
143
|
+
"mt-8 inline-flex items-center gap-2 px-5 py-2.5 rounded-lg text-sm font-medium cursor-pointer",
|
|
144
|
+
"bg-primary/10 text-primary border border-primary/20",
|
|
145
|
+
"hover:bg-primary/20 hover:border-primary/30",
|
|
146
|
+
!isLowPower && "transition-colors duration-200",
|
|
147
|
+
)}
|
|
148
|
+
>
|
|
149
|
+
<ArrowLeft className="w-4 h-4" />
|
|
150
|
+
Back to Dashboard
|
|
151
|
+
</button>
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
};
|
|
@@ -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
|
>
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ export * from "./components/Card";
|
|
|
4
4
|
export * from "./components/Label";
|
|
5
5
|
export * from "./components/NavItem";
|
|
6
6
|
export * from "./components/AccessDenied";
|
|
7
|
+
export * from "./components/NotFound";
|
|
7
8
|
export * from "./components/AccessGate";
|
|
8
9
|
export * from "./components/SectionHeader";
|
|
9
10
|
export * from "./components/StatusCard";
|
|
@@ -55,3 +56,5 @@ export * from "./components/CodeEditor";
|
|
|
55
56
|
export * from "./components/AnimatedNumber";
|
|
56
57
|
export * from "./hooks/useAnimatedNumber";
|
|
57
58
|
export * from "./components/IDELayout";
|
|
59
|
+
export * from "./components/MetricTile";
|
|
60
|
+
export * from "./components/Sheet";
|