mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
feat: implement new design system variables across new components and add skeletons (#9193)
This commit is contained in:
@@ -3,10 +3,11 @@
|
||||
import { HorizontalBarChart } from "@/components/graphs/horizontal-bar-chart";
|
||||
import { BarDataPoint } from "@/components/graphs/types";
|
||||
import {
|
||||
BaseCard,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Skeleton,
|
||||
} from "@/components/shadcn";
|
||||
import { calculatePercentage } from "@/lib/utils";
|
||||
|
||||
@@ -58,7 +59,10 @@ export const RiskSeverityChart = ({
|
||||
];
|
||||
|
||||
return (
|
||||
<BaseCard className="flex min-h-[372px] min-w-[312px] flex-1 flex-col md:min-w-[380px]">
|
||||
<Card
|
||||
variant="base"
|
||||
className="flex min-h-[372px] min-w-[312px] flex-1 flex-col md:min-w-[380px]"
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle>Risk Severity</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -66,6 +70,31 @@ export const RiskSeverityChart = ({
|
||||
<CardContent className="flex flex-1 items-center justify-start px-6">
|
||||
<HorizontalBarChart data={chartData} />
|
||||
</CardContent>
|
||||
</BaseCard>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export function RiskSeverityChartSkeleton() {
|
||||
return (
|
||||
<Card
|
||||
variant="base"
|
||||
className="flex min-h-[372px] min-w-[312px] flex-1 flex-col md:min-w-[380px]"
|
||||
>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-7 w-[260px] rounded-xl" />
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex flex-1 items-center justify-start px-6">
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
{/* 5 horizontal bar skeletons */}
|
||||
{Array.from({ length: 5 }).map((_, index) => (
|
||||
<div key={index} className="flex h-7 w-full gap-6">
|
||||
<Skeleton className="h-full w-28 shrink-0 rounded-xl" />
|
||||
<Skeleton className="h-full flex-1 rounded-xl" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,13 +5,13 @@ import { Bell, BellOff, ShieldCheck, TriangleAlert } from "lucide-react";
|
||||
import { DonutChart } from "@/components/graphs/donut-chart";
|
||||
import { DonutDataPoint } from "@/components/graphs/types";
|
||||
import {
|
||||
BaseCard,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardVariant,
|
||||
ResourceStatsCard,
|
||||
ResourceStatsCardContainer,
|
||||
Skeleton,
|
||||
} from "@/components/shadcn";
|
||||
import { calculatePercentage } from "@/lib/utils";
|
||||
|
||||
@@ -60,27 +60,30 @@ export const StatusChart = ({
|
||||
{
|
||||
name: "Fail Findings",
|
||||
value: failFindingsData.total,
|
||||
color: "#f43f5e", // Rose-500
|
||||
color: "var(--bg-fail-primary)",
|
||||
percentage: Number(failPercentage),
|
||||
change: Number(failChange),
|
||||
},
|
||||
{
|
||||
name: "Pass Findings",
|
||||
value: passFindingsData.total,
|
||||
color: "#4ade80", // Green-400
|
||||
color: "var(--bg-pass-primary)",
|
||||
percentage: Number(passPercentage),
|
||||
change: Number(passChange),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<BaseCard className="flex min-h-[372px] min-w-[312px] flex-1 flex-col justify-between md:min-w-[380px]">
|
||||
<Card
|
||||
variant="base"
|
||||
className="flex min-h-[372px] min-w-[312px] flex-1 flex-col justify-between md:min-w-[380px]"
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle>Check Findings</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-2">
|
||||
<div className="mx-auto max-h-[200px] max-w-[200px]">
|
||||
<CardContent className="flex flex-1 flex-col justify-between space-y-4">
|
||||
<div className="mx-auto h-[172px] w-[172px]">
|
||||
<DonutChart
|
||||
data={donutData}
|
||||
showLegend={false}
|
||||
@@ -93,7 +96,11 @@ export const StatusChart = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ResourceStatsCardContainer className="flex w-full flex-col items-start justify-center gap-4 lg:flex-row lg:justify-between">
|
||||
<Card
|
||||
variant="inner"
|
||||
padding="md"
|
||||
className="flex w-full flex-col items-start justify-center gap-4 lg:flex-row lg:justify-between"
|
||||
>
|
||||
<ResourceStatsCard
|
||||
containerless
|
||||
badge={{
|
||||
@@ -115,7 +122,7 @@ export const StatusChart = ({
|
||||
/>
|
||||
|
||||
<div className="flex w-full items-center justify-center lg:w-auto lg:self-stretch">
|
||||
<div className="h-px w-full bg-slate-300 lg:h-full lg:w-px dark:bg-[rgba(39,39,42,1)]" />
|
||||
<div className="bg-border-neutral-primary h-px w-full lg:h-full lg:w-px" />
|
||||
</div>
|
||||
|
||||
<ResourceStatsCard
|
||||
@@ -137,8 +144,31 @@ export const StatusChart = ({
|
||||
}
|
||||
className="w-full lg:min-w-0 lg:flex-1"
|
||||
/>
|
||||
</ResourceStatsCardContainer>
|
||||
</Card>
|
||||
</CardContent>
|
||||
</BaseCard>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export function StatusChartSkeleton() {
|
||||
return (
|
||||
<Card
|
||||
variant="base"
|
||||
className="flex min-h-[372px] min-w-[312px] flex-1 flex-col justify-between md:min-w-[380px]"
|
||||
>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-7 w-[260px] rounded-xl" />
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex flex-1 flex-col justify-between space-y-4">
|
||||
{/* Circular skeleton for donut chart */}
|
||||
<div className="mx-auto h-[172px] w-[172px]">
|
||||
<Skeleton className="size-[172px] rounded-full" />
|
||||
</div>
|
||||
|
||||
{/* Bottom info box skeleton */}
|
||||
<Skeleton className="h-[97px] w-full shrink-0 rounded-xl" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { MessageCircleWarning, ThumbsUp } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
import type {
|
||||
CriticalRequirement,
|
||||
@@ -9,50 +8,33 @@ import type {
|
||||
} from "@/actions/overview/types";
|
||||
import { RadialChart } from "@/components/graphs/radial-chart";
|
||||
import {
|
||||
SEVERITY_COLORS,
|
||||
STATUS_COLORS,
|
||||
} from "@/components/graphs/shared/constants";
|
||||
import {
|
||||
BaseCard,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Skeleton,
|
||||
} from "@/components/shadcn";
|
||||
|
||||
const THREAT_LEVEL_CONFIG = {
|
||||
CRITICAL: {
|
||||
DANGER: {
|
||||
label: "Critical Risk",
|
||||
color: "text-red-500",
|
||||
chartColor: SEVERITY_COLORS.Critical,
|
||||
color: "var(--bg-fail-primary)",
|
||||
chartColor: "var(--bg-fail-primary)",
|
||||
minScore: 0,
|
||||
maxScore: 20,
|
||||
maxScore: 30,
|
||||
},
|
||||
HIGH: {
|
||||
label: "High Risk",
|
||||
color: "text-orange-500",
|
||||
chartColor: SEVERITY_COLORS.High,
|
||||
minScore: 21,
|
||||
maxScore: 40,
|
||||
},
|
||||
MODERATE: {
|
||||
label: "Moderately Secure",
|
||||
color: "text-yellow-500",
|
||||
chartColor: SEVERITY_COLORS.Medium,
|
||||
minScore: 41,
|
||||
WARNING: {
|
||||
label: "Moderate Risk",
|
||||
color: "var(--bg-warning-primary)",
|
||||
chartColor: "var(--bg-warning-primary)",
|
||||
minScore: 31,
|
||||
maxScore: 60,
|
||||
},
|
||||
LOW: {
|
||||
label: "Low Risk",
|
||||
color: "text-blue-500",
|
||||
chartColor: SEVERITY_COLORS.Low,
|
||||
SUCCESS: {
|
||||
label: "Secure",
|
||||
color: "var(--bg-pass-primary)",
|
||||
chartColor: "var(--bg-pass-primary)",
|
||||
minScore: 61,
|
||||
maxScore: 80,
|
||||
},
|
||||
SECURE: {
|
||||
label: "Highly Secure",
|
||||
color: "text-green-500",
|
||||
chartColor: STATUS_COLORS.Success,
|
||||
minScore: 81,
|
||||
maxScore: 100,
|
||||
},
|
||||
} as const;
|
||||
@@ -74,7 +56,7 @@ function getThreatLevel(score: number): ThreatLevelKey {
|
||||
return key as ThreatLevelKey;
|
||||
}
|
||||
}
|
||||
return "MODERATE";
|
||||
return "WARNING";
|
||||
}
|
||||
|
||||
// Convert section scores to tooltip data for the radial chart
|
||||
@@ -84,16 +66,13 @@ function convertSectionScoresToTooltipData(
|
||||
if (!sectionScores) return [];
|
||||
|
||||
return Object.entries(sectionScores).map(([name, value]) => {
|
||||
// Determine color based on score value
|
||||
let color: string = SEVERITY_COLORS.Critical;
|
||||
if (value >= 80) color = STATUS_COLORS.Success;
|
||||
else if (value >= 60) color = SEVERITY_COLORS.Low;
|
||||
else if (value >= 40) color = SEVERITY_COLORS.Medium;
|
||||
else if (value >= 20) color = SEVERITY_COLORS.High;
|
||||
|
||||
// Round to nearest integer
|
||||
const roundedValue = Math.round(value);
|
||||
|
||||
// Determine color based on the same ranges as THREAT_LEVEL_CONFIG
|
||||
const threatLevel = getThreatLevel(roundedValue);
|
||||
const color = THREAT_LEVEL_CONFIG[threatLevel].chartColor;
|
||||
|
||||
return { name, value: roundedValue, color };
|
||||
});
|
||||
}
|
||||
@@ -122,22 +101,10 @@ export function ThreatScore({
|
||||
sectionScores,
|
||||
criticalRequirements,
|
||||
}: ThreatScoreProps) {
|
||||
if (score === null || score === undefined) {
|
||||
return (
|
||||
<BaseCard className="flex min-h-[372px] min-w-[312px] flex-col justify-between md:max-w-[312px]">
|
||||
<CardHeader>
|
||||
<CardTitle>Prowler Threat Score</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-1 items-center justify-center">
|
||||
<p className="text-chart-text-primary">
|
||||
No ThreatScore data available
|
||||
</p>
|
||||
</CardContent>
|
||||
</BaseCard>
|
||||
);
|
||||
}
|
||||
const hasData = score !== null && score !== undefined;
|
||||
const displayScore = hasData ? score : 0;
|
||||
|
||||
const threatLevel = getThreatLevel(score);
|
||||
const threatLevel = getThreatLevel(displayScore);
|
||||
const config = THREAT_LEVEL_CONFIG[threatLevel];
|
||||
|
||||
// Convert section scores to tooltip data
|
||||
@@ -147,20 +114,23 @@ export function ThreatScore({
|
||||
const gaps = extractTopGaps(criticalRequirements, 2);
|
||||
|
||||
return (
|
||||
<BaseCard className="flex min-h-[372px] min-w-[328px] flex-col justify-between md:max-w-[312px]">
|
||||
<Card
|
||||
variant="base"
|
||||
className="flex min-h-[372px] min-w-[328px] flex-col justify-between md:max-w-[312px]"
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle>Prowler Threat Score</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-2">
|
||||
<CardContent className="flex flex-1 flex-col justify-between space-y-4">
|
||||
{/* Radial Chart */}
|
||||
<div className="relative mx-auto h-[150px] w-full max-w-[250px]">
|
||||
<div className="absolute top-0 left-1/2 z-10 h-full w-full -translate-x-1/2">
|
||||
<div className="relative mx-auto h-[172px] w-full max-w-[250px]">
|
||||
<div className="absolute top-0 left-1/2 z-1 w-full -translate-x-1/2">
|
||||
<RadialChart
|
||||
percentage={score}
|
||||
percentage={displayScore}
|
||||
label="Score"
|
||||
color={config.chartColor}
|
||||
backgroundColor="rgba(100, 100, 100, 0.2)"
|
||||
backgroundColor="var(--bg-neutral-tertiary)"
|
||||
height={206}
|
||||
innerRadius={90}
|
||||
outerRadius={115}
|
||||
@@ -171,54 +141,87 @@ export function ThreatScore({
|
||||
/>
|
||||
</div>
|
||||
{/* Overlaid Text (centered) */}
|
||||
<div className="pointer-events-none absolute top-[75%] left-1/2 z-0 -translate-x-1/2 -translate-y-1/2 text-center">
|
||||
<p className="text-sm text-nowrap text-slate-900 dark:text-zinc-300">
|
||||
{config.label}
|
||||
</p>
|
||||
</div>
|
||||
{hasData && (
|
||||
<div className="pointer-events-none absolute top-[65%] left-1/2 z-0 -translate-x-1/2 -translate-y-1/2 text-center">
|
||||
<p className="text-text-neutral-secondary text-sm text-nowrap">
|
||||
{config.label}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Info Box */}
|
||||
<div className="flex-1 rounded-xl border border-slate-300 bg-[#F8FAFC80] px-3 py-[9px] backdrop-blur-[46px] dark:border-[rgba(38,38,38,0.70)] dark:bg-[rgba(23,23,23,0.50)]">
|
||||
<div className="flex flex-col gap-1.5 text-sm leading-6 text-zinc-800 dark:text-zinc-300">
|
||||
{/* Improvement Message */}
|
||||
{scoreDelta !== undefined &&
|
||||
scoreDelta !== null &&
|
||||
scoreDelta !== 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<ThumbsUp size={14} className="flex-shrink-0" />
|
||||
{/* Info Box or Empty State */}
|
||||
{hasData ? (
|
||||
<Card
|
||||
variant="inner"
|
||||
padding="md"
|
||||
className="items-center justify-center"
|
||||
>
|
||||
<div className="text-text-neutral-secondary flex flex-col gap-1.5 text-sm leading-6">
|
||||
{/* Improvement Message */}
|
||||
{scoreDelta !== undefined &&
|
||||
scoreDelta !== null &&
|
||||
scoreDelta !== 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<ThumbsUp size={14} className="flex-shrink-0" />
|
||||
<p>
|
||||
Threat score has{" "}
|
||||
{scoreDelta > 0 ? "improved" : "decreased"} by{" "}
|
||||
{Math.abs(scoreDelta)}%
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Gaps Message */}
|
||||
{gaps.length > 0 && (
|
||||
<div className="flex items-start gap-1">
|
||||
<MessageCircleWarning
|
||||
size={14}
|
||||
className="mt-1 flex-shrink-0"
|
||||
/>
|
||||
<p>
|
||||
Threat score has {scoreDelta > 0 ? "improved" : "decreased"}{" "}
|
||||
by {Math.abs(scoreDelta)}%
|
||||
Major gaps include {gaps.slice(0, 2).join(", ")}
|
||||
{gaps.length > 2 && ` & ${gaps.length - 2} more...`}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Gaps Message */}
|
||||
{gaps.length > 0 && (
|
||||
<div className="flex items-start gap-1">
|
||||
<MessageCircleWarning
|
||||
size={14}
|
||||
className="mt-1 flex-shrink-0"
|
||||
/>
|
||||
<p>
|
||||
Major gaps include {gaps.slice(0, 2).join(", ")}
|
||||
{gaps.length > 2 && ` & ${gaps.length - 2} more...`}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* View Remediation Plan Button */}
|
||||
<div className="flex justify-center">
|
||||
<Link href="/compliance">
|
||||
<span className="text-sm font-medium text-blue-600 hover:underline dark:text-blue-300">
|
||||
View Remediation Plan
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
<Card
|
||||
variant="inner"
|
||||
padding="md"
|
||||
className="items-center justify-center"
|
||||
>
|
||||
<p className="text-text-neutral-secondary text-sm">
|
||||
Threat Score Data Unavailable
|
||||
</p>
|
||||
</Card>
|
||||
)}
|
||||
</CardContent>
|
||||
</BaseCard>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export function ThreatScoreSkeleton() {
|
||||
return (
|
||||
<Card
|
||||
variant="base"
|
||||
className="flex min-h-[372px] min-w-[328px] flex-col justify-between md:max-w-[312px]"
|
||||
>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-7 w-36 rounded-xl" />
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex flex-1 flex-col justify-between space-y-4">
|
||||
{/* Circular skeleton for radial chart */}
|
||||
<div className="relative mx-auto h-[172px] w-full max-w-[250px]">
|
||||
<Skeleton className="mx-auto size-[170px] rounded-full" />
|
||||
</div>
|
||||
|
||||
{/* Bottom info box skeleton */}
|
||||
<Skeleton className="h-[97px] w-full shrink-0 rounded-xl" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,9 +11,12 @@ import { SearchParamsProps } from "@/types";
|
||||
|
||||
import { AccountsSelector } from "./components/accounts-selector";
|
||||
import { ProviderTypeSelector } from "./components/provider-type-selector";
|
||||
import { RiskSeverityChart } from "./components/risk-severity-chart";
|
||||
import { StatusChart } from "./components/status-chart";
|
||||
import { ThreatScore } from "./components/threat-score";
|
||||
import {
|
||||
RiskSeverityChart,
|
||||
RiskSeverityChartSkeleton,
|
||||
} from "./components/risk-severity-chart";
|
||||
import { StatusChart, StatusChartSkeleton } from "./components/status-chart";
|
||||
import { ThreatScore, ThreatScoreSkeleton } from "./components/threat-score";
|
||||
|
||||
const FILTER_PREFIX = "filter[";
|
||||
|
||||
@@ -42,33 +45,15 @@ export default async function NewOverviewPage({
|
||||
<AccountsSelector providers={providersData?.data ?? []} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-6 md:flex-row md:flex-wrap md:items-stretch">
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex h-[400px] w-full items-center justify-center rounded-xl border border-zinc-900 bg-stone-950">
|
||||
<p className="text-zinc-400">Loading...</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Suspense fallback={<ThreatScoreSkeleton />}>
|
||||
<SSRThreatScore searchParams={resolvedSearchParams} />
|
||||
</Suspense>
|
||||
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex h-[400px] w-full items-center justify-center rounded-xl border border-zinc-900 bg-stone-950">
|
||||
<p className="text-zinc-400">Loading...</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Suspense fallback={<StatusChartSkeleton />}>
|
||||
<SSRCheckFindings searchParams={resolvedSearchParams} />
|
||||
</Suspense>
|
||||
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex h-[400px] w-full items-center justify-center rounded-xl border border-zinc-900 bg-stone-950">
|
||||
<p className="text-zinc-400">Loading...</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Suspense fallback={<RiskSeverityChartSkeleton />}>
|
||||
<SSRRiskSeverityChart searchParams={resolvedSearchParams} />
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
@@ -30,7 +30,7 @@ const CustomTooltip = ({ active, payload }: any) => {
|
||||
const change = entry.payload?.change;
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white px-3 py-1.5 shadow-lg dark:border-[#202020] dark:bg-[#121110]">
|
||||
<div className="border-border-neutral-tertiary bg-bg-neutral-tertiary rounded-xl border px-3 py-1.5 shadow-lg">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{/* Title with color chip */}
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -38,7 +38,7 @@ const CustomTooltip = ({ active, payload }: any) => {
|
||||
className="size-3 shrink-0 rounded"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
<p className="text-sm leading-5 font-medium text-slate-900 dark:text-[#f4f4f5]">
|
||||
<p className="text-text-neutral-primary text-xs leading-5 font-medium">
|
||||
{percentage}% {name}
|
||||
</p>
|
||||
</div>
|
||||
@@ -46,7 +46,7 @@ const CustomTooltip = ({ active, payload }: any) => {
|
||||
{/* Change percentage row */}
|
||||
{change !== undefined && (
|
||||
<div className="flex items-start">
|
||||
<p className="text-sm leading-5 font-medium text-slate-600 dark:text-[#d4d4d8]">
|
||||
<p className="text-text-neutral-primary text-xs leading-5 font-medium">
|
||||
{change > 0 ? "+" : ""}
|
||||
{change}% Since last scan
|
||||
</p>
|
||||
@@ -171,7 +171,7 @@ export function DonutChart({
|
||||
<tspan
|
||||
x={viewBox.cx}
|
||||
y={(viewBox.cy || 0) - 6}
|
||||
className="text-2xl font-bold text-zinc-800 dark:text-zinc-300"
|
||||
className="text-text-neutral-secondary text-2xl font-bold"
|
||||
style={{
|
||||
fill: "currentColor",
|
||||
}}
|
||||
@@ -181,7 +181,7 @@ export function DonutChart({
|
||||
<tspan
|
||||
x={viewBox.cx}
|
||||
y={(viewBox.cy || 0) + 24}
|
||||
className="text-sm text-nowrap text-zinc-800 dark:text-zinc-300"
|
||||
className="text-text-neutral-secondary text-sm text-nowrap"
|
||||
style={{
|
||||
fill: "currentColor",
|
||||
}}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { Bell } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { CHART_COLORS, SEVERITY_ORDER } from "./shared/constants";
|
||||
import { SEVERITY_ORDER } from "./shared/constants";
|
||||
import { getSeverityColorByName } from "./shared/utils";
|
||||
import { BarDataPoint } from "./types";
|
||||
|
||||
@@ -24,6 +24,7 @@ export function HorizontalBarChart({ data, title }: HorizontalBarChartProps) {
|
||||
{ name: "High", value: 1, percentage: 100 },
|
||||
{ name: "Medium", value: 1, percentage: 100 },
|
||||
{ name: "Low", value: 1, percentage: 100 },
|
||||
{ name: "Informational", value: 1, percentage: 100 },
|
||||
];
|
||||
|
||||
const sortedData = (isEmpty ? emptyData : [...data]).sort((a, b) => {
|
||||
@@ -38,7 +39,7 @@ export function HorizontalBarChart({ data, title }: HorizontalBarChartProps) {
|
||||
<div>
|
||||
<h3
|
||||
className="text-lg font-semibold"
|
||||
style={{ color: "var(--chart-text-primary)" }}
|
||||
style={{ color: "var(--text-neutral-primary)" }}
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
@@ -50,15 +51,15 @@ export function HorizontalBarChart({ data, title }: HorizontalBarChartProps) {
|
||||
const isHovered = !isEmpty && hoveredIndex === index;
|
||||
const isFaded = !isEmpty && hoveredIndex !== null && !isHovered;
|
||||
const barColor = isEmpty
|
||||
? CHART_COLORS.gridLine
|
||||
? "var(--bg-neutral-tertiary)"
|
||||
: item.color ||
|
||||
getSeverityColorByName(item.name) ||
|
||||
CHART_COLORS.defaultColor;
|
||||
"var(--bg-neutral-tertiary)";
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center gap-6"
|
||||
key={item.name}
|
||||
className="flex gap-6"
|
||||
onMouseEnter={() => !isEmpty && setHoveredIndex(index)}
|
||||
onMouseLeave={() => !isEmpty && setHoveredIndex(null)}
|
||||
>
|
||||
@@ -67,18 +68,18 @@ export function HorizontalBarChart({ data, title }: HorizontalBarChartProps) {
|
||||
<span
|
||||
className="text-sm font-medium"
|
||||
style={{
|
||||
color: "var(--chart-text-primary)",
|
||||
color: "var(--text-neutral-secondary)",
|
||||
opacity: isFaded ? 0.5 : 1,
|
||||
transition: "opacity 0.2s",
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
{item.name === "Informational" ? "Info" : item.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Bar - flexible */}
|
||||
<div className="relative flex-1">
|
||||
<div className="absolute inset-0 h-[22px] w-full rounded-sm bg-[#FAFAFA] dark:bg-black" />
|
||||
<div className="bg-bg-neutral-tertiary absolute inset-0 h-[22px] w-full rounded-sm" />
|
||||
{(item.value > 0 || isEmpty) && (
|
||||
<div
|
||||
className="relative h-[22px] rounded-sm border border-black/10 transition-all duration-300"
|
||||
@@ -93,7 +94,7 @@ export function HorizontalBarChart({ data, title }: HorizontalBarChartProps) {
|
||||
)}
|
||||
|
||||
{isHovered && (
|
||||
<div className="absolute top-10 left-0 z-10 rounded-xl border border-slate-200 bg-white px-3 py-1.5 shadow-lg dark:border-[#202020] dark:bg-[#121110]">
|
||||
<div className="border-border-neutral-tertiary bg-bg-neutral-tertiary absolute top-10 left-0 z-10 rounded-xl border px-3 py-1.5 shadow-lg">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{/* Title with color chip */}
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -101,8 +102,10 @@ export function HorizontalBarChart({ data, title }: HorizontalBarChartProps) {
|
||||
className="size-3 shrink-0 rounded"
|
||||
style={{ backgroundColor: barColor }}
|
||||
/>
|
||||
<p className="text-sm leading-5 font-medium text-slate-900 dark:text-[#f4f4f5]">
|
||||
{item.value.toLocaleString()} {item.name} Risk
|
||||
<p className="text-text-neutral-primary text-xs leading-5 font-medium">
|
||||
{item.value.toLocaleString()}{" "}
|
||||
{item.name === "Informational" ? "Info" : item.name}{" "}
|
||||
Risk
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -111,9 +114,9 @@ export function HorizontalBarChart({ data, title }: HorizontalBarChartProps) {
|
||||
<div className="flex items-center gap-1">
|
||||
<Bell
|
||||
size={12}
|
||||
className="shrink-0 text-slate-600 dark:text-[#d4d4d8]"
|
||||
className="text-text-neutral-secondary shrink-0"
|
||||
/>
|
||||
<p className="text-sm leading-5 font-medium text-slate-600 dark:text-[#d4d4d8]">
|
||||
<p className="text-text-neutral-secondary text-xs leading-5 font-medium">
|
||||
{item.newFindings} New Findings
|
||||
</p>
|
||||
</div>
|
||||
@@ -122,7 +125,7 @@ export function HorizontalBarChart({ data, title }: HorizontalBarChartProps) {
|
||||
{/* Change percentage row */}
|
||||
{item.change !== undefined && (
|
||||
<div className="flex items-start">
|
||||
<p className="text-sm leading-5 font-medium text-slate-600 dark:text-[#d4d4d8]">
|
||||
<p className="text-text-neutral-secondary text-xs leading-5 font-medium">
|
||||
{item.change > 0 ? "+" : ""}
|
||||
{item.change}% Since last scan
|
||||
</p>
|
||||
@@ -137,7 +140,7 @@ export function HorizontalBarChart({ data, title }: HorizontalBarChartProps) {
|
||||
<div
|
||||
className="flex w-[90px] shrink-0 items-center gap-2 text-sm"
|
||||
style={{
|
||||
color: "var(--chart-text-primary)",
|
||||
color: "var(--text-neutral-secondary)",
|
||||
opacity: isFaded ? 0.5 : 1,
|
||||
transition: "opacity 0.2s",
|
||||
}}
|
||||
@@ -147,7 +150,7 @@ export function HorizontalBarChart({ data, title }: HorizontalBarChartProps) {
|
||||
</span>
|
||||
<span
|
||||
className="font-medium"
|
||||
style={{ color: "var(--chart-text-secondary)" }}
|
||||
style={{ color: "var(--text-neutral-secondary)" }}
|
||||
>
|
||||
•
|
||||
</span>
|
||||
|
||||
@@ -8,8 +8,6 @@ import {
|
||||
Tooltip,
|
||||
} from "recharts";
|
||||
|
||||
import { CHART_COLORS } from "./shared/constants";
|
||||
|
||||
export interface TooltipItem {
|
||||
name: string;
|
||||
value: number;
|
||||
@@ -42,18 +40,18 @@ const CustomTooltip = ({ active, payload }: any) => {
|
||||
return null;
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white px-3 py-1.5 shadow-lg dark:border-[#202020] dark:bg-[#121110]">
|
||||
<div className="bg-bg-neutral-tertiary border-border-neutral-tertiary rounded-xl border px-3 py-1.5 shadow-lg">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{tooltipItems.map((item: TooltipItem, index: number) => (
|
||||
<div key={index} className="flex items-end gap-1">
|
||||
<p className="text-xs leading-5 font-medium text-slate-900 dark:text-[#f4f4f5]">
|
||||
<p className="text-text-neutral-primary text-xs leading-5 font-medium">
|
||||
{item.name}
|
||||
</p>
|
||||
<div className="mb-[4px] flex-1 border-b border-dotted border-slate-400 dark:border-slate-600" />
|
||||
<div className="border-text-neutral-primary mb-[4px] flex-1 border-b border-dotted" />
|
||||
<p
|
||||
className="text-xs leading-5 font-medium"
|
||||
style={{
|
||||
color: item.color || "var(--chart-text-primary)",
|
||||
color: item.color || "var(--text-neutral-primary)",
|
||||
}}
|
||||
>
|
||||
{item.value}%
|
||||
@@ -67,8 +65,8 @@ const CustomTooltip = ({ active, payload }: any) => {
|
||||
|
||||
export function RadialChart({
|
||||
percentage,
|
||||
color = "var(--chart-success-color)",
|
||||
backgroundColor = CHART_COLORS.tooltipBackground,
|
||||
color = "var(--bg-pass-primary)",
|
||||
backgroundColor = "var(--bg-neutral-tertiary)",
|
||||
height = 250,
|
||||
innerRadius = 60,
|
||||
outerRadius = 100,
|
||||
@@ -154,24 +152,18 @@ export function RadialChart({
|
||||
const y = centerY - middleRadius * Math.sin(currentAngleRad);
|
||||
|
||||
return (
|
||||
<circle
|
||||
key={i}
|
||||
cx={x}
|
||||
cy={y}
|
||||
r={2}
|
||||
fill="rgba(255, 255, 255, 0.3)"
|
||||
/>
|
||||
<circle key={i} cx={x} cy={y} r={2} fill="var(--chart-dots)" />
|
||||
);
|
||||
})}
|
||||
|
||||
<text
|
||||
x="50%"
|
||||
y="40%"
|
||||
y="38%"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
className="text-2xl font-bold"
|
||||
style={{
|
||||
fill: "var(--chart-text-primary)",
|
||||
fill: "var(--text-neutral-secondary)",
|
||||
}}
|
||||
>
|
||||
{percentage}%
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
export const SEVERITY_COLORS = {
|
||||
Informational: "var(--chart-info)",
|
||||
Info: "var(--chart-info)",
|
||||
Low: "var(--chart-warning)",
|
||||
Medium: "var(--chart-warning-emphasis)",
|
||||
High: "var(--chart-danger)",
|
||||
Critical: "var(--chart-danger-emphasis)",
|
||||
Informational: "var(--bg-data-info)",
|
||||
Low: "var(--bg-data-low)",
|
||||
Medium: "var(--bg-data-medium)",
|
||||
High: "var(--bg-data-high)",
|
||||
Critical: "var(--bg-data-critical)",
|
||||
} as const;
|
||||
|
||||
export const PROVIDER_COLORS = {
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { Card } from "../card";
|
||||
|
||||
const baseCardVariants = cva("", {
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-slate-200 bg-white dark:border-zinc-900 dark:bg-stone-950",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
});
|
||||
|
||||
interface BaseCardProps
|
||||
extends React.ComponentProps<typeof Card>,
|
||||
VariantProps<typeof baseCardVariants> {}
|
||||
|
||||
const BaseCard = ({ className, variant, ...props }: BaseCardProps) => {
|
||||
return (
|
||||
<Card
|
||||
className={cn(
|
||||
baseCardVariants({ variant }),
|
||||
"gap-2 px-[18px] pt-3 pb-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { BaseCard };
|
||||
@@ -1,3 +1,5 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const CardVariant = {
|
||||
@@ -10,14 +12,44 @@ export const CardVariant = {
|
||||
|
||||
export type CardVariant = (typeof CardVariant)[keyof typeof CardVariant];
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const cardVariants = cva("flex flex-col gap-6 rounded-xl border", {
|
||||
variants: {
|
||||
variant: {
|
||||
default: "",
|
||||
base: "border-border-neutral-secondary bg-bg-neutral-secondary px-[18px] pt-3 pb-4",
|
||||
inner:
|
||||
"rounded-[12px] backdrop-blur-[46px] border-border-neutral-tertiary bg-bg-neutral-tertiary",
|
||||
},
|
||||
padding: {
|
||||
default: "",
|
||||
sm: "px-3 py-2",
|
||||
md: "px-4 py-3",
|
||||
lg: "px-5 py-4",
|
||||
none: "p-0",
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
variant: "inner",
|
||||
padding: "default",
|
||||
className: "px-4 py-3", // md padding by default for inner
|
||||
},
|
||||
],
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
padding: "default",
|
||||
},
|
||||
});
|
||||
|
||||
interface CardProps
|
||||
extends React.ComponentProps<"div">,
|
||||
VariantProps<typeof cardVariants> {}
|
||||
|
||||
function Card({ className, variant, padding, ...props }: CardProps) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6",
|
||||
className,
|
||||
)}
|
||||
className={cn(cardVariants({ variant, padding }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -28,7 +60,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
"@container/card-header mb-6 grid auto-rows-min grid-rows-[auto_auto] items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -40,10 +72,7 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn(
|
||||
"my-2 text-[18px] leading-none text-slate-900 dark:text-white",
|
||||
className,
|
||||
)}
|
||||
className={cn("mt-2 text-[18px] leading-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -96,4 +125,6 @@ export {
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
cardVariants,
|
||||
};
|
||||
export type { CardProps };
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const containerVariants = cva(
|
||||
[
|
||||
"flex",
|
||||
"rounded-[12px]",
|
||||
"border",
|
||||
"backdrop-blur-[46px]",
|
||||
"border-slate-300",
|
||||
"bg-[#F8FAFC80]",
|
||||
"dark:border-[rgba(38,38,38,0.70)]",
|
||||
"dark:bg-[rgba(23,23,23,0.50)]",
|
||||
],
|
||||
{
|
||||
variants: {
|
||||
padding: {
|
||||
sm: "px-3 py-2",
|
||||
md: "px-[19px] py-[9px]",
|
||||
lg: "px-6 py-3",
|
||||
none: "p-0",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
padding: "md",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface ResourceStatsCardContainerProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof containerVariants> {
|
||||
ref?: React.Ref<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export const ResourceStatsCardContainer = ({
|
||||
className,
|
||||
children,
|
||||
padding,
|
||||
ref,
|
||||
...props
|
||||
}: ResourceStatsCardContainerProps) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(containerVariants({ padding }), className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ResourceStatsCardContainer.displayName = "ResourceStatsCardContainer";
|
||||
@@ -11,11 +11,11 @@ export interface StatItem {
|
||||
}
|
||||
|
||||
const variantColors = {
|
||||
default: "#868994",
|
||||
fail: "#f54280",
|
||||
pass: "#4ade80",
|
||||
warning: "#fbbf24",
|
||||
info: "#60a5fa",
|
||||
default: "var(--bg-neutral-tertiary)",
|
||||
fail: "var(--bg-fail-primary)",
|
||||
pass: "var(--bg-pass-primary)",
|
||||
warning: "var(--bg-warning-primary)",
|
||||
info: "var(--bg-data-info)",
|
||||
} as const;
|
||||
|
||||
type BadgeVariant = keyof typeof variantColors;
|
||||
@@ -26,8 +26,8 @@ const badgeVariants = cva(
|
||||
variants: {
|
||||
variant: {
|
||||
[CardVariant.default]: "bg-slate-100 dark:bg-[#535359]",
|
||||
[CardVariant.fail]: "bg-red-100 dark:bg-[#432232]",
|
||||
[CardVariant.pass]: "bg-green-100 dark:bg-[#204237]",
|
||||
[CardVariant.fail]: "bg-bg-fail-secondary",
|
||||
[CardVariant.pass]: "bg-bg-pass-secondary",
|
||||
[CardVariant.warning]: "bg-amber-100 dark:bg-[#3d3520]",
|
||||
[CardVariant.info]: "bg-blue-100 dark:bg-[#1e3a5f]",
|
||||
},
|
||||
@@ -58,7 +58,7 @@ const badgeIconVariants = cva("", {
|
||||
});
|
||||
|
||||
const labelTextVariants = cva(
|
||||
"leading-6 font-semibold text-slate-900 dark:text-zinc-300 whitespace-nowrap",
|
||||
"leading-6 font-semibold text-text-neutral-secondary whitespace-nowrap",
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
@@ -73,7 +73,7 @@ const labelTextVariants = cva(
|
||||
},
|
||||
);
|
||||
|
||||
const statIconVariants = cva("text-slate-600 dark:text-zinc-300", {
|
||||
const statIconVariants = cva("text-text-neutral-secondary", {
|
||||
variants: {
|
||||
size: {
|
||||
sm: "h-2.5 w-2.5",
|
||||
@@ -87,7 +87,7 @@ const statIconVariants = cva("text-slate-600 dark:text-zinc-300", {
|
||||
});
|
||||
|
||||
const statLabelVariants = cva(
|
||||
"leading-5 font-medium text-slate-700 dark:text-zinc-300",
|
||||
"leading-5 font-medium text-text-neutral-secondary",
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const dividerVariants = cva("flex items-center justify-center", {
|
||||
variants: {
|
||||
spacing: {
|
||||
sm: "px-2",
|
||||
md: "px-[23px]",
|
||||
lg: "px-8",
|
||||
},
|
||||
orientation: {
|
||||
vertical: "h-full",
|
||||
horizontal: "w-full",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
spacing: "md",
|
||||
orientation: "vertical",
|
||||
},
|
||||
});
|
||||
|
||||
const lineVariants = cva("bg-[rgba(39,39,42,1)]", {
|
||||
variants: {
|
||||
orientation: {
|
||||
vertical: "h-full w-px",
|
||||
horizontal: "w-full h-px",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
orientation: "vertical",
|
||||
},
|
||||
});
|
||||
|
||||
export interface ResourceStatsCardDividerProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof dividerVariants> {
|
||||
ref?: React.Ref<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export const ResourceStatsCardDivider = ({
|
||||
className,
|
||||
spacing,
|
||||
orientation,
|
||||
ref,
|
||||
...props
|
||||
}: ResourceStatsCardDividerProps) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(dividerVariants({ spacing, orientation }), className)}
|
||||
{...props}
|
||||
>
|
||||
<div className={lineVariants({ orientation })} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ResourceStatsCardDivider.displayName = "ResourceStatsCardDivider";
|
||||
@@ -16,7 +16,7 @@ const headerVariants = cva("flex w-full items-center gap-1", {
|
||||
},
|
||||
});
|
||||
|
||||
const iconVariants = cva("text-zinc-300 dark:text-zinc-300", {
|
||||
const iconVariants = cva("text-text-neutral-secondary", {
|
||||
variants: {
|
||||
size: {
|
||||
sm: "h-3.5 w-3.5",
|
||||
@@ -30,7 +30,7 @@ const iconVariants = cva("text-zinc-300 dark:text-zinc-300", {
|
||||
});
|
||||
|
||||
const titleVariants = cva(
|
||||
"leading-7 font-semibold text-zinc-300 dark:text-zinc-300",
|
||||
"leading-7 font-semibold text-text-neutral-secondary",
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
@@ -45,21 +45,18 @@ const titleVariants = cva(
|
||||
},
|
||||
);
|
||||
|
||||
const countVariants = cva(
|
||||
"leading-4 font-normal text-zinc-300 dark:text-zinc-300",
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
sm: "text-[9px]",
|
||||
md: "text-[10px]",
|
||||
lg: "text-xs",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "md",
|
||||
const countVariants = cva("leading-4 font-normal text-text-neutral-secondary", {
|
||||
variants: {
|
||||
size: {
|
||||
sm: "text-[9px]",
|
||||
md: "text-[10px]",
|
||||
lg: "text-xs",
|
||||
},
|
||||
},
|
||||
);
|
||||
defaultVariants: {
|
||||
size: "md",
|
||||
},
|
||||
});
|
||||
|
||||
export interface ResourceStatsCardHeaderProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
|
||||
@@ -3,17 +3,12 @@ import { LucideIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { CardVariant } from "../card";
|
||||
import { ResourceStatsCardContainer } from "./resource-stats-card-container";
|
||||
import { Card, CardVariant } from "../card";
|
||||
import type { StatItem } from "./resource-stats-card-content";
|
||||
import { ResourceStatsCardContent } from "./resource-stats-card-content";
|
||||
import { ResourceStatsCardHeader } from "./resource-stats-card-header";
|
||||
|
||||
export type { StatItem };
|
||||
|
||||
// Todo: when the design system is ready, we must use the colors from the design system (semantic colors)
|
||||
// Variant styles using CVA for type safety and consistency
|
||||
// Colors are exact HEX values from Figma design system
|
||||
const cardVariants = cva("", {
|
||||
variants: {
|
||||
variant: {
|
||||
@@ -109,7 +104,7 @@ export const ResourceStatsCard = ({
|
||||
{header && <ResourceStatsCardHeader {...header} size={resolvedSize} />}
|
||||
{emptyState ? (
|
||||
<div className="flex h-[51px] w-full flex-col items-center justify-center">
|
||||
<p className="text-center text-sm leading-5 font-medium text-slate-600 dark:text-zinc-300">
|
||||
<p className="text-text-neutral-secondary text-center text-sm leading-5 font-medium">
|
||||
{emptyState.message}
|
||||
</p>
|
||||
</div>
|
||||
@@ -131,15 +126,16 @@ export const ResourceStatsCard = ({
|
||||
|
||||
// Otherwise, render with container
|
||||
return (
|
||||
<ResourceStatsCardContainer
|
||||
<Card
|
||||
ref={ref}
|
||||
variant="inner"
|
||||
className={cn(cardVariants({ variant, size }), "flex-col", className)}
|
||||
{...props}
|
||||
>
|
||||
{header && <ResourceStatsCardHeader {...header} size={resolvedSize} />}
|
||||
{emptyState ? (
|
||||
<div className="flex h-[51px] w-full flex-col items-center justify-center">
|
||||
<p className="text-center text-sm leading-5 font-medium text-slate-600 dark:text-zinc-300">
|
||||
<p className="text-text-neutral-secondary text-center text-sm leading-5 font-medium">
|
||||
{emptyState.message}
|
||||
</p>
|
||||
</div>
|
||||
@@ -155,7 +151,7 @@ export const ResourceStatsCard = ({
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</ResourceStatsCardContainer>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
export * from "./badge/badge";
|
||||
export * from "./button/button";
|
||||
export * from "./card/base-card/base-card";
|
||||
export * from "./card/card";
|
||||
export * from "./card/resource-stats-card/resource-stats-card";
|
||||
export * from "./card/resource-stats-card/resource-stats-card-container";
|
||||
export * from "./card/resource-stats-card/resource-stats-card-content";
|
||||
export * from "./card/resource-stats-card/resource-stats-card-divider";
|
||||
export * from "./card/resource-stats-card/resource-stats-card-header";
|
||||
export * from "./dropdown/dropdown";
|
||||
export * from "./select/select";
|
||||
export * from "./separator/separator";
|
||||
export * from "./skeleton/skeleton";
|
||||
export * from "./tabs/generic-tabs";
|
||||
export * from "./tabs/tabs";
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="skeleton"
|
||||
className={cn(
|
||||
"bg-border-neutral-tertiary animate-pulse rounded-md",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Skeleton };
|
||||
+35
-26
@@ -6,11 +6,11 @@
|
||||
/* ===== LIGHT THEME (ROOT) ===== */
|
||||
:root {
|
||||
/* ===== LEGACY VARIABLES (CHART COLORS) ===== */
|
||||
--chart-info: #3C8DFF;
|
||||
--chart-warning: #FDFBD4;
|
||||
--chart-warning-emphasis: #FEC94D;
|
||||
--chart-danger: #F77852;
|
||||
--chart-danger-emphasis: #FF006A;
|
||||
--chart-info: #3c8dff;
|
||||
--chart-warning: #fdfbd4;
|
||||
--chart-warning-emphasis: #fec94d;
|
||||
--chart-danger: #f77852;
|
||||
--chart-danger-emphasis: #ff006a;
|
||||
--chart-success-color: #16a34a;
|
||||
--chart-fail: #dc2626;
|
||||
--chart-radar-primary: #9d174d;
|
||||
@@ -69,36 +69,40 @@
|
||||
|
||||
/* Text Colors */
|
||||
--text-neutral-primary: var(--color-slate-950);
|
||||
--text-neutral-secondary: var(--color-zinc-700);
|
||||
--text-neutral-secondary: var(--color-zinc-800);
|
||||
--text-neutral-tertiary: var(--color-zinc-500);
|
||||
--text-error-primary: var(--color-red-600);
|
||||
|
||||
/* Background Colors */
|
||||
--bg-neutral-primary: #FDFDFD;
|
||||
--bg-neutral-primary: #fdfdfd;
|
||||
--bg-neutral-secondary: var(--color-white);
|
||||
--bg-neutral-tertiary: #FBFDFD;
|
||||
--bg-neutral-tertiary: #fbfdfd;
|
||||
--bg-tag-primary: var(--color-slate-50);
|
||||
--bg-pass-primary: var(--color-emerald-400);
|
||||
--bg-pass-secondary: var(--color-emerald-50);
|
||||
--bg-warning-primary: var(--color-orange-500);
|
||||
--bg-fail-primary: var(--color-rose-500);
|
||||
--bg-fail-secondary: var(--color-rose-50);
|
||||
|
||||
/* Severity Colors */
|
||||
--bg-data-critical: #FF006A;
|
||||
--bg-data-high: #F77852;
|
||||
--bg-data-medium: #FDD34F;
|
||||
--bg-data-low: #F5F3CE;
|
||||
--bg-data-info: #3C8DFF;
|
||||
--bg-data-critical: #ff006a;
|
||||
--bg-data-high: #f77852;
|
||||
--bg-data-medium: #fdd34f;
|
||||
--bg-data-low: #f5f3ce;
|
||||
--bg-data-info: #3c8dff;
|
||||
|
||||
/* Chart Dots */
|
||||
--chart-dots: var(--color-neutral-200);
|
||||
}
|
||||
|
||||
/* ===== DARK THEME ===== */
|
||||
.dark {
|
||||
/* ===== LEGACY VARIABLES (CHART COLORS) ===== */
|
||||
--chart-info: #3C8DFF;
|
||||
--chart-warning: #FDFBD4;
|
||||
--chart-warning-emphasis: #FEC94D;
|
||||
--chart-danger: #F77852;
|
||||
--chart-danger-emphasis: #FF006A;
|
||||
--chart-info: #3c8dff;
|
||||
--chart-warning: #fdfbd4;
|
||||
--chart-warning-emphasis: #fec94d;
|
||||
--chart-danger: #f77852;
|
||||
--chart-danger-emphasis: #ff006a;
|
||||
--chart-success-color: #86da26;
|
||||
--chart-fail: #db2b49;
|
||||
--chart-radar-primary: #b51c80;
|
||||
@@ -157,26 +161,30 @@
|
||||
|
||||
/* Text Colors */
|
||||
--text-neutral-primary: var(--color-zinc-100);
|
||||
--text-neutral-secondary: var(--color-zinc-400);
|
||||
--text-neutral-secondary: var(--color-zinc-300);
|
||||
--text-neutral-tertiary: var(--color-zinc-500);
|
||||
--text-error-primary: var(--color-rose-300);
|
||||
|
||||
/* Background Colors */
|
||||
--bg-neutral-primary: var(--color-zinc-950);
|
||||
--bg-neutral-secondary: var(--color-slate-950);
|
||||
--bg-neutral-secondary: var(--color-stone-950);
|
||||
--bg-neutral-tertiary: #121110;
|
||||
--bg-tag-primary: var(--color-slate-950);
|
||||
--bg-warning-primary: var(--color-orange-400);
|
||||
--bg-pass-primary: var(--color-green-400);
|
||||
--bg-pass-secondary: var(--color-emerald-900);
|
||||
--bg-fail-primary: var(--color-rose-500);
|
||||
--bg-fail-secondary: #432232;
|
||||
|
||||
/* Severity Colors */
|
||||
--bg-data-critical: #FF006A;
|
||||
--bg-data-high: #F77852;
|
||||
--bg-data-medium: #FEC94D;
|
||||
--bg-data-low: #FDFBD4;
|
||||
--bg-data-info: #3C8DFF;
|
||||
--bg-data-critical: #ff006a;
|
||||
--bg-data-high: #f77852;
|
||||
--bg-data-medium: #fec94d;
|
||||
--bg-data-low: #fdfbd4;
|
||||
--bg-data-info: #3c8dff;
|
||||
|
||||
/* Chart Dots */
|
||||
--chart-dots: var(--text-neutral-primary);
|
||||
}
|
||||
|
||||
/* ===== TAILWIND THEME MAPPINGS ===== */
|
||||
@@ -248,6 +256,7 @@
|
||||
--color-bg-tag: var(--bg-tag-primary);
|
||||
--color-bg-pass: var(--bg-pass-primary);
|
||||
--color-bg-pass-secondary: var(--bg-pass-secondary);
|
||||
--color-bg-warning: var(--bg-warning-primary);
|
||||
--color-bg-fail: var(--bg-fail-primary);
|
||||
--color-bg-fail-secondary: var(--bg-fail-secondary);
|
||||
}
|
||||
@@ -357,4 +366,4 @@
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user