Add Badge component and use it for performance page ratings
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<span className={`${styles.badge} ${styles[variant]}`}>
|
||||||
|
{dot && <span className={styles.dot} />}
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,21 +3,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card:hover {
|
.card:hover {
|
||||||
box-shadow: 0 0 0 1px var(--primary);
|
box-shadow: 0 0 0 1px var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card.selected {
|
.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;
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,7 @@ import { config, useSpring } from '@react-spring/web';
|
|||||||
import { Column, Text } from '@umami/react-zen';
|
import { Column, Text } from '@umami/react-zen';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { AnimatedDiv } from '@/components/common/AnimatedDiv';
|
import { AnimatedDiv } from '@/components/common/AnimatedDiv';
|
||||||
|
import { Badge } from '@/components/common/Badge';
|
||||||
import { useMessages } from '@/components/hooks';
|
import { useMessages } from '@/components/hooks';
|
||||||
import { WEB_VITALS_THRESHOLDS } from '@/lib/constants';
|
import { WEB_VITALS_THRESHOLDS } from '@/lib/constants';
|
||||||
import { formatNumber } from '@/lib/format';
|
import { formatNumber } from '@/lib/format';
|
||||||
@@ -16,6 +17,12 @@ export interface PerformanceCardProps {
|
|||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const RATING_VARIANTS = {
|
||||||
|
good: 'good',
|
||||||
|
'needs-improvement': 'warning',
|
||||||
|
poor: 'danger',
|
||||||
|
} as const;
|
||||||
|
|
||||||
function getRating(metric: string, value: number): 'good' | 'needs-improvement' | 'poor' {
|
function getRating(metric: string, value: number): 'good' | 'needs-improvement' | 'poor' {
|
||||||
const threshold = WEB_VITALS_THRESHOLDS[metric as keyof typeof WEB_VITALS_THRESHOLDS];
|
const threshold = WEB_VITALS_THRESHOLDS[metric as keyof typeof WEB_VITALS_THRESHOLDS];
|
||||||
if (!threshold || value <= 0) return 'good';
|
if (!threshold || value <= 0) return 'good';
|
||||||
@@ -45,7 +52,7 @@ export const PerformanceCard = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Column
|
<Column
|
||||||
className={`${styles.card} ${styles[rating]} ${selected ? styles.selected : ''}`}
|
className={`${styles.card} ${selected ? styles.selected : ''}`}
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
paddingX="6"
|
paddingX="6"
|
||||||
paddingY="4"
|
paddingY="4"
|
||||||
@@ -62,9 +69,9 @@ export const PerformanceCard = ({
|
|||||||
<Text size="4xl" weight="bold" wrap="nowrap">
|
<Text size="4xl" weight="bold" wrap="nowrap">
|
||||||
<AnimatedDiv>{spring.value.to(n => formatValue(n))}</AnimatedDiv>
|
<AnimatedDiv>{spring.value.to(n => formatValue(n))}</AnimatedDiv>
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="sm" className={styles.rating}>
|
<Badge variant={RATING_VARIANTS[rating]}>
|
||||||
{t(labels[rating === 'needs-improvement' ? 'needsImprovement' : rating])}
|
{t(labels[rating === 'needs-improvement' ? 'needsImprovement' : rating])}
|
||||||
</Text>
|
</Badge>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user