1 estrella en GitHub = 1 componente nuevo.

Dar una estrella
App y Dashboardappdashboardsaasbillingplanusagemeter

Plan Usage Card

Resumen de facturación con plan, renovación, medidores de uso con umbrales, método de pago y mejora de plan.

Vista Previa en Vivo

Current plan

Pro €149 / month

Renews on Dec 1, 2026

Visa •••• 4242

  • Rooms42 of 50

    84% used — consider upgrading soon.

  • Team seats4 of 10
  • Channel connections3 of 5
  • Guest emails8,200 of 10,000 / month

    82% used — consider upgrading soon.

Uso

Copia y pega este código directamente en tu componente React:

tsx
import { PlanUsageCard } from 'react-component-kit-ui/app';

<PlanUsageCard plan="Pro" usage={[{ label: 'Rooms', used: 42, limit: 50 }]} />

Instalación Manual

Si no estás usando el paquete npm, crea un archivo nuevo y pega el siguiente código: src/components/PlanUsageCard.tsx + PlanUsageCard.css:

PlanUsageCard.tsx
'use client';

import type { CSSProperties } from 'react';
import './PlanUsageCard.css';

/** class:list → className */
function cx(...args: unknown[]): string {
  const out: string[] = [];
  const walk = (v: unknown) => {
    if (!v) return;
    if (typeof v === 'string' || typeof v === 'number') out.push(String(v));
    else if (Array.isArray(v)) v.forEach(walk);
    else if (typeof v === 'object') for (const [k, on] of Object.entries(v as Record<string, unknown>)) if (on) out.push(k);
  };
  args.forEach(walk);
  return out.join(' ');
}

/** "a: b; --c: d" → style object */
function st(css: string | undefined): CSSProperties {
  const o: Record<string, string> = {};
  for (const decl of String(css ?? '').split(';')) {
    const i = decl.indexOf(':');
    if (i < 0) continue;
    const k = decl.slice(0, i).trim();
    if (k) o[k.startsWith('--') ? k : k.replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = decl.slice(i + 1).trim();
  }
  return o as CSSProperties;
}

/**
 * PlanUsageCard — Billing summary: current plan and price, renewal date, usage meters with
 * warning thresholds, payment method and upgrade / manage actions.
 *
 * @param {string} plan - Plan name. Default 'Pro'.
 * @param {string} price - Price label. Default '€149 / month'.
 * @param {string} renews - Renewal text. Default 'Renews on Dec 1, 2026'.
 * @param {Array<{label: string, used: number, limit: number, unit?: string}>} usage - Meters.
 * @param {string} card - Payment method label (optional). Default 'Visa •••• 4242'.
 * @param {{label: string, href: string}} upgrade - Primary action. Default upgrade.
 * @param {{label: string, href: string}} manage - Secondary action (optional).
 * @param {number} warnAt - Percentage at which a meter turns amber. Default 80.
 * @param {string} variant - Style (card|accent). Default 'card'.
 * @param {string} className - Extra class.
 *
 * Accessibility: each meter is a role="meter" with aria-valuenow/min/max and a text value,
 * so usage is never conveyed by colour alone.
 */
export interface PlanUsageCardProps {
  plan?: string;
  price?: string;
  renews?: string;
  usage?: Array<{ label: string; used: number; limit: number; unit?: string }>;
  card?: string;
  upgrade?: { label: string; href: string };
  manage?: { label: string; href: string };
  warnAt?: number;
  variant?: 'card' | 'accent';
  className?: string;
}

export function PlanUsageCard({
  plan = 'Pro',
  price = '€149 / month',
  renews = 'Renews on Dec 1, 2026',
  usage = [
    { label: 'Rooms', used: 42, limit: 50 },
    { label: 'Team seats', used: 4, limit: 10 },
    { label: 'Channel connections', used: 3, limit: 5 },
    { label: 'Guest emails', used: 8200, limit: 10000, unit: '/ month' },
  ],
  card = 'Visa •••• 4242',
  upgrade = { label: 'Upgrade to Business', href: '#' },
  manage = { label: 'Manage billing', href: '#' },
  warnAt = 80,
  variant = 'card',
  className = '',
}: PlanUsageCardProps) {
  const fmt = (n: number) => new Intl.NumberFormat('en-GB').format(n);
  return (
    <>
      <section className={cx(['plan', `plan--${variant}`, className])} aria-labelledby="plan-title" data-rk-aac02d="">
        <header className="plan__head" data-rk-aac02d="">
          <div data-rk-aac02d="">
            <p className="plan__eyebrow" data-rk-aac02d="">Current plan</p>
            <h2 className="plan__title" id="plan-title" data-rk-aac02d="">{plan} <span data-rk-aac02d="">{price}</span></h2>
            <p className="plan__renew" data-rk-aac02d="">{renews}</p>
          </div>
          {card && <p className="plan__card" data-rk-aac02d=""><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" data-rk-aac02d=""><rect x="2" y="5" width="20" height="14" rx="2" data-rk-aac02d="" /><path d="M2 10h20" data-rk-aac02d="" /></svg>{card}</p>}
        </header>
        <ul className="plan__meters" data-rk-aac02d="">
          {usage.map((u) => {
            const pct = Math.min(100, Math.round((u.used / u.limit) * 100));
            const tone = pct >= 100 ? 'danger' : pct >= warnAt ? 'warning' : 'ok';
            return (
              <li data-rk-aac02d="">
                <div className="plan__meter-head" data-rk-aac02d="">
                  <span data-rk-aac02d="">{u.label}</span>
                  <strong data-rk-aac02d="">{fmt(u.used)} <small data-rk-aac02d="">of {fmt(u.limit)}{u.unit ? ` ${u.unit}` : ''}</small></strong>
                </div>
                <div className={cx(['plan__bar', `plan__bar--${tone}`])} role="meter" aria-label={u.label} aria-valuemin={0} aria-valuemax={u.limit} aria-valuenow={u.used} aria-valuetext={`${fmt(u.used)} of ${fmt(u.limit)}, ${pct}%`} data-rk-aac02d="">
                  <span style={st(`width: ${pct}%`)} data-rk-aac02d=""></span>
                </div>
                {tone === 'warning' && <p className="plan__warn" data-rk-aac02d="">{pct}% used — consider upgrading soon.</p>}
              </li>
            );
          })}
        </ul>
        <div className="plan__actions" data-rk-aac02d="">
          {upgrade && <a className="plan__btn plan__btn--primary" href={upgrade.href} data-rk-aac02d="">{upgrade.label}</a>}
          {manage && <a className="plan__btn" href={manage.href} data-rk-aac02d="">{manage.label}</a>}
        </div>
      </section>
    </>
  );
}

export default PlanUsageCard;
PlanUsageCard.css
/* PlanUsageCard — ported from astro-component-kit (app/PlanUsageCard.astro) */

.plan:where([data-rk-aac02d]) { container-type: inline-size; display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--sp-5, 1.25rem); padding: var(--sp-6, 1.5rem); border-radius: var(--r-lg, 16px); border: 1px solid var(--c-border, rgba(255, 255, 255, 0.08)); background: var(--c-bg-1, #0a0a0a); }
.plan--accent:where([data-rk-aac02d]) { background: radial-gradient(circle at 100% 0%, color-mix(in srgb, var(--app-accent, var(--c-primary-light, #d4a017)) 22%, transparent), transparent 55%), var(--c-bg-1, #0a0a0a); }
.plan__head:where([data-rk-aac02d]) { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-4, 1rem); }
.plan__eyebrow:where([data-rk-aac02d]) { margin: 0; font-size: 0.72rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--app-accent, var(--c-primary-light, #d4a017)); }
.plan__title:where([data-rk-aac02d]) { margin: 4px 0 0; font-size: 1.5rem; font-weight: 800; letter-spacing: -0.02em; color: var(--c-text-1, #fff); }
.plan__title:where([data-rk-aac02d]) span:where([data-rk-aac02d]) { font-size: 0.95rem; font-weight: 500; color: var(--c-text-2, #a3a3a3); }
.plan__renew:where([data-rk-aac02d]) { margin: 2px 0 0; font-size: 0.8rem; color: var(--c-text-3, #737373); }
.plan__card:where([data-rk-aac02d]) { display: inline-flex; align-items: center; gap: 6px; margin: 0; padding: 6px 10px; border-radius: 8px; border: 1px solid var(--c-border, rgba(255, 255, 255, 0.1)); font-size: 0.8rem; color: var(--c-text-1, #fff); white-space: nowrap; }
.plan__meters:where([data-rk-aac02d]) { display: grid; gap: var(--sp-4, 1rem); margin: 0; padding: 0; list-style: none; }
.plan__meter-head:where([data-rk-aac02d]) { display: flex; justify-content: space-between; gap: var(--sp-2, 0.5rem); margin-bottom: 6px; font-size: 0.85rem; color: var(--c-text-2, #a3a3a3); }
.plan__meter-head:where([data-rk-aac02d]) strong:where([data-rk-aac02d]) { color: var(--c-text-1, #fff); font-variant-numeric: tabular-nums; }
.plan__meter-head:where([data-rk-aac02d]) small:where([data-rk-aac02d]) { font-weight: 500; color: var(--c-text-3, #737373); }
.plan__bar:where([data-rk-aac02d]) { height: 8px; border-radius: 999px; background: rgb(var(--ink-rgb, 255 255 255) / 0.08); overflow: hidden; }
.plan__bar:where([data-rk-aac02d]) span:where([data-rk-aac02d]) { display: block; height: 100%; border-radius: inherit; background: var(--app-accent, var(--c-primary-light, #d4a017)); }
.plan__bar--warning:where([data-rk-aac02d]) span:where([data-rk-aac02d]) { background: var(--c-warning, #fbbf24); }
.plan__bar--danger:where([data-rk-aac02d]) span:where([data-rk-aac02d]) { background: var(--c-danger, #f87171); }
.plan__warn:where([data-rk-aac02d]) { margin: 4px 0 0; font-size: 0.75rem; color: var(--c-warning, #fbbf24); }
.plan__actions:where([data-rk-aac02d]) { display: flex; flex-wrap: wrap; gap: var(--sp-2, 0.5rem); }
.plan__btn:where([data-rk-aac02d]) { display: inline-flex; align-items: center; min-height: 40px; padding: 0 1rem; border-radius: 10px; border: 1px solid var(--c-border, rgba(255, 255, 255, 0.12)); color: var(--c-text-1, #fff); font-size: 0.85rem; font-weight: 600; text-decoration: none; }
.plan__btn:hover:where([data-rk-aac02d]) { background: rgb(var(--ink-rgb, 255 255 255) / 0.05); color: var(--c-text-1, #fff); }
.plan__btn--primary:where([data-rk-aac02d]) { border-color: transparent; background: var(--app-accent, var(--c-primary-light, #d4a017)); color: var(--c-on-primary, #050505); }
.plan__btn--primary:hover:where([data-rk-aac02d]) { background: var(--app-accent, var(--c-primary-light, #d4a017)); color: var(--c-on-primary, #050505); filter: brightness(1.08); }
.plan__btn:focus-visible:where([data-rk-aac02d]) { outline: 2px solid var(--app-accent, var(--c-primary-light, #d4a017)); outline-offset: 2px; }
@container (max-width: 420px) { .plan__head:where([data-rk-aac02d]) { flex-direction: column; } }

Información Rápida

Categoría
App y Dashboard
Nombre de archivo
PlanUsageCard.tsx
Dependencias
Ninguna — React + CSS puro
Etiquetas
appdashboardsaasbillingplanusagemeter