diff --git a/src/components/common/Badge.module.css b/src/components/common/Badge.module.css
new file mode 100644
index 000000000..216c9c2ae
--- /dev/null
+++ b/src/components/common/Badge.module.css
@@ -0,0 +1,82 @@
+.badge {
+ display: inline-flex;
+ align-items: center;
+ align-self: flex-start;
+ gap: 6px;
+ padding: 2px 8px;
+ border-radius: 6px;
+ font-size: 12px;
+ font-weight: 500;
+ line-height: 1.4;
+ white-space: nowrap;
+}
+
+.dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ flex-shrink: 0;
+}
+
+.good {
+ color: #15803d;
+ background: #dcfce7;
+}
+
+.good .dot {
+ background: #16a34a;
+}
+
+.warning {
+ color: #a16207;
+ background: #fef9c3;
+}
+
+.warning .dot {
+ background: #ca8a04;
+}
+
+.danger {
+ color: #b91c1c;
+ background: #fee2e2;
+}
+
+.danger .dot {
+ background: #dc2626;
+}
+
+.gray {
+ color: var(--text-muted);
+ background: var(--surface-sunken);
+}
+
+.gray .dot {
+ background: var(--text-muted);
+}
+
+:global([data-theme='dark']) .good {
+ color: #86efac;
+ background: rgba(22, 163, 74, 0.15);
+}
+
+:global([data-theme='dark']) .good .dot {
+ background: #4ade80;
+}
+
+:global([data-theme='dark']) .warning {
+ color: #fde047;
+ background: rgba(202, 138, 4, 0.15);
+}
+
+:global([data-theme='dark']) .warning .dot {
+ background: #facc15;
+}
+
+:global([data-theme='dark']) .danger {
+ color: #fca5a5;
+ background: rgba(220, 38, 38, 0.15);
+}
+
+:global([data-theme='dark']) .danger .dot {
+ background: #f87171;
+}
diff --git a/src/components/common/Badge.tsx b/src/components/common/Badge.tsx
new file mode 100644
index 000000000..44becfe73
--- /dev/null
+++ b/src/components/common/Badge.tsx
@@ -0,0 +1,16 @@
+import styles from './Badge.module.css';
+
+export interface BadgeProps {
+ variant: 'good' | 'warning' | 'danger' | 'gray';
+ children: React.ReactNode;
+ dot?: boolean;
+}
+
+export function Badge({ variant, children, dot = true }: BadgeProps) {
+ return (
+
+ {dot && }
+ {children}
+
+ );
+}
diff --git a/src/components/metrics/PerformanceCard.module.css b/src/components/metrics/PerformanceCard.module.css
index 27b566c2d..9e0d1f296 100644
--- a/src/components/metrics/PerformanceCard.module.css
+++ b/src/components/metrics/PerformanceCard.module.css
@@ -3,21 +3,10 @@
}
.card:hover {
- box-shadow: 0 0 0 1px var(--primary);
+ box-shadow: 0 0 0 1px var(--text-primary);
}
.card.selected {
- box-shadow: 0 0 0 1px var(--primary);
+ box-shadow: 0 0 0 1px var(--text-primary);
}
-.good .rating {
- color: #0cce6b;
-}
-
-.needs-improvement .rating {
- color: #ffa400;
-}
-
-.poor .rating {
- color: #ff4e42;
-}
diff --git a/src/components/metrics/PerformanceCard.tsx b/src/components/metrics/PerformanceCard.tsx
index be745d47b..6a6345aaa 100644
--- a/src/components/metrics/PerformanceCard.tsx
+++ b/src/components/metrics/PerformanceCard.tsx
@@ -2,6 +2,7 @@ import { config, useSpring } from '@react-spring/web';
import { Column, Text } from '@umami/react-zen';
import { useRef } from 'react';
import { AnimatedDiv } from '@/components/common/AnimatedDiv';
+import { Badge } from '@/components/common/Badge';
import { useMessages } from '@/components/hooks';
import { WEB_VITALS_THRESHOLDS } from '@/lib/constants';
import { formatNumber } from '@/lib/format';
@@ -16,6 +17,12 @@ export interface PerformanceCardProps {
selected?: boolean;
}
+const RATING_VARIANTS = {
+ good: 'good',
+ 'needs-improvement': 'warning',
+ poor: 'danger',
+} as const;
+
function getRating(metric: string, value: number): 'good' | 'needs-improvement' | 'poor' {
const threshold = WEB_VITALS_THRESHOLDS[metric as keyof typeof WEB_VITALS_THRESHOLDS];
if (!threshold || value <= 0) return 'good';
@@ -45,7 +52,7 @@ export const PerformanceCard = ({
return (
{spring.value.to(n => formatValue(n))}
-
+
{t(labels[rating === 'needs-improvement' ? 'needsImprovement' : rating])}
-
+
);
};