feat: implement new design system variables across new components and add skeletons (#9193)

This commit is contained in:
Alejandro Bailo
2025-11-10 09:19:10 +01:00
committed by GitHub
parent 66a04b5547
commit ee2d3ed052
18 changed files with 348 additions and 410 deletions
@@ -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>
);
}
+9 -24
View File
@@ -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>
+5 -5
View File
@@ -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",
}}
+20 -17
View File
@@ -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>
+9 -17
View File
@@ -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}%
+5 -6
View File
@@ -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 };
+41 -10
View File
@@ -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 -3
View File
@@ -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
View File
@@ -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;
}
}
}