Monthly recurring revenue
+12.4%$84,210
vs last month
1 звезда на GitHub = 1 новый компонент.
Поставить звезду→Плитка KPI с крупным числом, изменением (иконка + текст), периодом сравнения и спарклайном.
$84,210
Скопируйте и вставьте этот код в ваш React-компонент:
import { KpiStatTile } from 'react-component-kit-ui/charts';
<KpiStatTile label="MRR" value="$84,210" change={12.4} period="vs last month" trend={[52, 60, 63, 72, 84]} />
<KpiStatTile label="Churn" value="2.3%" change={-0.6} invert variant="solid" size="sm" />Если вы не используете пакет npm, создайте файл и вставьте следующий код: src/components/KpiStatTile.tsx + KpiStatTile.css:
'use client';
import SparklineChart from './SparklineChart';
import './KpiStatTile.css';
/**
* KpiStatTile — Hero-number statistic tile: label, big value, change vs previous
* period (icon + text, never color alone) and an optional sparkline.
*
* @param {string} label - Metric name.
* @param {string} value - Formatted value.
* @param {number} change - Percentage change vs comparison period.
* @param {string} period - Comparison period label (e.g. 'vs last month').
* @param {boolean} invert - Treat a decrease as good (e.g. churn, latency).
* @param {number[]} trend - Optional sparkline data.
* @param {string} variant - Visual style (glass|solid|ghost).
* @param {string} size - Density (sm|md|lg).
* @param {string} className - Extra CSS class.
*/
export interface KpiStatTileProps {
label?: string;
value?: string;
change?: number;
period?: string;
invert?: boolean;
trend?: number[];
variant?: 'glass' | 'solid' | 'ghost';
size?: 'sm' | 'md' | 'lg';
className?: string;
}
export function KpiStatTile({
label = 'Monthly recurring revenue',
value = '$84,210',
change = 12.4,
period = 'vs last month',
invert = false,
trend = [52, 55, 54, 60, 63, 61, 68, 72, 70, 76, 80, 84],
variant = 'glass',
size = 'md',
className = '',
}: KpiStatTileProps) {
const up = change >= 0;
const good = invert ? !up : up;
return (
<>
<article className={`kpi kpi--${variant} kpi--${size} ${className}`} aria-label={`${label}: ${value}, ${up ? 'up' : 'down'} ${Math.abs(change)}% ${period}`} data-rk-727d83="">
<header className="kpi__head" data-rk-727d83="">
<h3 className="kpi__label" data-rk-727d83="">{label}</h3>
<span className={`kpi__chip ${good ? 'kpi__chip--good' : 'kpi__chip--bad'}`} data-rk-727d83="">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" data-rk-727d83="">
{up ? <path d="M7 17 17 7M9 7h8v8" data-rk-727d83="" /> : <path d="M7 7l10 10M17 9v8H9" data-rk-727d83="" />}
</svg>
{up ? '+' : '−'}{Math.abs(change)}%
</span>
</header>
<p className="kpi__value" data-rk-727d83="">{value}</p>
<div className="kpi__foot" data-rk-727d83="">
<span className="kpi__period" data-rk-727d83="">{period}</span>
{trend.length > 1 && (
<SparklineChart data={trend} size={size === 'lg' ? 'lg' : 'md'} color={good ? 'var(--chart-pos, #34c38f)' : 'var(--chart-neg, #f06a6a)'} label={`${label} trend`} />
)}
</div>
</article>
</>
);
}
export default KpiStatTile;/* KpiStatTile — ported from astro-component-kit (charts/KpiStatTile.astro) */
.kpi:where([data-rk-727d83]) {
--kpi-pad: var(--sp-6, 1.5rem);
--kpi-val: 2rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: var(--kpi-pad);
border-radius: var(--r-lg, 20px);
min-width: 0;
transition: border-color 0.25s ease, transform 0.25s ease;
}
.kpi--glass:where([data-rk-727d83]) {
background: linear-gradient(160deg, rgb(var(--ink-rgb, 255 255 255) / 0.05), rgb(var(--ink-rgb, 255 255 255) / 0.015));
border: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.07));
backdrop-filter: blur(14px);
}
.kpi--solid:where([data-rk-727d83]) { background: var(--chart-surface, #0d0d0f); border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.08); }
.kpi--ghost:where([data-rk-727d83]) { background: transparent; border: 1px solid transparent; padding-inline: 0; }
.kpi--glass:hover:where([data-rk-727d83]), .kpi--solid:hover:where([data-rk-727d83]) { border-color: rgb(var(--ink-rgb, 255 255 255) / 0.16); transform: translateY(-2px); }
.kpi--sm:where([data-rk-727d83]) { --kpi-pad: var(--sp-4, 1rem); --kpi-val: 1.5rem; }
.kpi--lg:where([data-rk-727d83]) { --kpi-pad: var(--sp-8, 2rem); --kpi-val: 2.5rem; }
.kpi__head:where([data-rk-727d83]) { display: flex; justify-content: space-between; align-items: center; gap: 0.75rem; }
.kpi__label:where([data-rk-727d83]) { margin: 0; font-size: 0.8125rem; font-weight: 500; color: var(--c-text-2, #a3a3a3); letter-spacing: 0; }
.kpi__chip:where([data-rk-727d83]) {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.2rem 0.5rem;
border-radius: var(--r-full, 999px);
font-size: 0.75rem;
font-weight: 650;
font-variant-numeric: tabular-nums;
color: var(--c-text-1, #fff);
}
.kpi__chip--good:where([data-rk-727d83]) { background: color-mix(in srgb, var(--chart-pos, #34c38f) 18%, transparent); }
.kpi__chip--good:where([data-rk-727d83]) svg:where([data-rk-727d83]) { color: var(--chart-pos, #34c38f); }
.kpi__chip--bad:where([data-rk-727d83]) { background: color-mix(in srgb, var(--chart-neg, #f06a6a) 18%, transparent); }
.kpi__chip--bad:where([data-rk-727d83]) svg:where([data-rk-727d83]) { color: var(--chart-neg, #f06a6a); }
.kpi__value:where([data-rk-727d83]) {
margin: 0;
font-size: var(--kpi-val);
font-weight: 750;
color: var(--c-text-1, #fff);
letter-spacing: -0.02em;
font-variant-numeric: tabular-nums;
line-height: 1.1;
}
.kpi__foot:where([data-rk-727d83]) { display: flex; justify-content: space-between; align-items: flex-end; gap: 1rem; }
.kpi__period:where([data-rk-727d83]) { font-size: 0.75rem; color: var(--c-text-3, #8a8a8a); }
@media (prefers-reduced-motion: reduce) { .kpi:where([data-rk-727d83]) { transition: none; } }KpiStatTile.tsx