refactor: redesign current plan UI with features panel and expandable capacity limits view

This commit is contained in:
2026-07-18 11:05:26 -03:00
parent 81616c7c1c
commit f6e2bb8372
2 changed files with 178 additions and 69 deletions
@@ -128,8 +128,7 @@
box-shadow: 0 18px 34px color-mix(in srgb, var(--black-dark) 14%, transparent);
}
.summaryGrid,
.limitsGrid {
.summaryGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
gap: 14px;
@@ -199,8 +198,7 @@
white-space: nowrap;
}
.summaryItem,
.limitItem {
.summaryItem {
position: relative;
overflow: hidden;
background: var(--white);
@@ -211,15 +209,13 @@
transition: transform 180ms ease, box-shadow 180ms ease, border-color 180ms ease;
}
.summaryItem:hover,
.limitItem:hover {
.summaryItem:hover {
transform: translateY(-2px);
border-color: var(--wine-lighter);
box-shadow: 0 18px 34px color-mix(in srgb, var(--wine-darkest) 12%, transparent);
}
.summaryItem::before,
.limitItem::before {
.summaryItem::before {
content: "";
position: absolute;
inset: 0 auto 0 0;
@@ -227,8 +223,7 @@
background: linear-gradient(180deg, var(--wine-red), var(--wine-dark));
}
.summaryItem span,
.limitItem span {
.summaryItem span {
display: block;
margin-bottom: 9px;
color: var(--gray-dark);
@@ -238,16 +233,14 @@
letter-spacing: 0.08em;
}
.summaryItem strong,
.limitItem strong {
.summaryItem strong {
display: block;
color: var(--black);
font-size: 24px;
line-height: 1.1;
}
.summaryItem small,
.limitItem small {
.summaryItem small {
display: block;
margin-top: 9px;
color: var(--gray-darkest);
@@ -306,10 +299,31 @@
letter-spacing: -0.03em;
}
.features {
.featuresPanel {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 12px;
grid-template-columns: minmax(180px, 0.45fr) minmax(0, 1fr);
gap: clamp(16px, 3vw, 28px);
align-items: start;
padding: clamp(18px, 3vw, 24px);
border: 1px solid var(--wine-lighterX2);
border-radius: 22px;
background:
radial-gradient(circle at 0% 0%, var(--wine-lighterX2), transparent 30%),
linear-gradient(135deg, var(--white) 0%, var(--white-dark) 100%);
}
.featuresPanel p {
margin: 0;
color: var(--gray-dark);
font-size: 15px;
font-weight: 700;
line-height: 1.55;
}
.features {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 0;
padding: 0;
list-style: none;
@@ -317,57 +331,87 @@
.features li {
position: relative;
overflow: hidden;
display: flex;
align-items: center;
gap: 12px;
min-height: 68px;
padding: 14px 16px 14px 54px;
border: 1px solid var(--wine-lighterX2);
border-radius: 18px;
background:
radial-gradient(circle at 100% 0%, var(--wine-lighterX2), transparent 34%),
linear-gradient(135deg, var(--white) 0%, var(--white-dark) 100%);
gap: 8px;
padding: 9px 12px;
border: 1px solid var(--white-darkest);
border-radius: 999px;
background: color-mix(in srgb, var(--white) 78%, var(--wine-lighterX2));
color: var(--black);
font-size: 14px;
font-size: 13px;
font-weight: 800;
line-height: 1.25;
box-shadow: 0 12px 28px color-mix(in srgb, var(--wine-darkest) 8%, transparent);
transition: transform 180ms ease, box-shadow 180ms ease, border-color 180ms ease;
}
.features li::before {
content: "✓";
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
display: grid;
place-items: center;
width: 26px;
height: 26px;
flex: 0 0 auto;
width: 18px;
height: 18px;
border-radius: 999px;
background: linear-gradient(135deg, var(--wine-red), var(--wine-dark));
color: var(--white);
background: var(--wine-lighterX2);
color: var(--wine-red);
font-size: 12px;
font-weight: 900;
}
.limitsPanel {
overflow: hidden;
border: 1px solid var(--white-darkest);
border-radius: 22px;
background: var(--white);
box-shadow: 0 14px 36px color-mix(in srgb, var(--wine-darkest) 8%, transparent);
}
.limitRow {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 16px;
align-items: center;
padding: 16px 18px;
border-bottom: 1px solid var(--white-darkest);
}
.limitRow:last-child {
border-bottom: 0;
}
.limitRow span {
display: block;
color: var(--black);
font-size: 15px;
font-weight: 900;
box-shadow: 0 8px 18px color-mix(in srgb, var(--wine-darkest) 18%, transparent);
}
.features li::after {
content: "";
position: absolute;
inset: auto 14px 10px 54px;
height: 3px;
border-radius: 999px;
background: linear-gradient(90deg, var(--wine-red), var(--wine-lighter));
opacity: 0.45;
.limitRow small {
display: block;
margin-top: 4px;
color: var(--gray-dark);
font-size: 13px;
font-weight: 700;
}
.features li:hover {
transform: translateY(-2px);
border-color: var(--wine-lighter);
box-shadow: 0 18px 34px color-mix(in srgb, var(--wine-darkest) 12%, transparent);
.limitValues {
min-width: 116px;
padding: 10px 12px;
border-radius: 16px;
background: var(--white-dark);
text-align: right;
}
.limitValues strong {
display: block;
color: var(--wine-red);
font-size: 24px;
line-height: 1;
}
.limitValues small {
margin-top: 6px;
color: var(--gray-darkest);
}
.message {
@@ -380,6 +424,7 @@
position: relative;
overflow: hidden;
display: grid;
width: 100%;
grid-template-columns: minmax(190px, 0.8fr) minmax(0, 1.2fr);
gap: 18px;
align-items: stretch;
@@ -390,6 +435,15 @@
radial-gradient(circle at 8% 20%, var(--yellow-light), transparent 30%),
linear-gradient(135deg, var(--wine-superdark) 0%, var(--wine-darkest) 45%, var(--wine-dark) 100%);
box-shadow: 0 18px 42px color-mix(in srgb, var(--wine-darkest) 16%, transparent);
color: inherit;
font: inherit;
text-align: left;
cursor: pointer;
}
.usageShowcase:focus-visible {
outline: 3px solid var(--yellow);
outline-offset: 3px;
}
.usageShowcase::after {
@@ -434,6 +488,17 @@
letter-spacing: -0.04em;
}
.usageIntro small {
width: fit-content;
margin-top: 4px;
padding: 7px 11px;
border-radius: 999px;
background: color-mix(in srgb, var(--white) 14%, transparent);
color: var(--white);
font-size: 13px;
font-weight: 900;
}
.usageStats {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
@@ -521,10 +586,20 @@
}
.usageShowcase,
.usageStats {
.usageStats,
.featuresPanel {
grid-template-columns: 1fr;
}
.limitRow {
grid-template-columns: 1fr;
}
.limitValues {
width: 100%;
text-align: left;
}
.usageStat {
min-height: 104px;
}
+52 -18
View File
@@ -23,6 +23,21 @@ import styles from "./page.module.css";
const formatLimit = (value: number) => (value === -1 ? "Ilimitado" : value.toString());
const getLimitDetail = (limit: number, current?: number) => {
if (limit === -1) return "Sin tope definido";
if (typeof current !== "number") return "Tope disponible";
const available = Math.max(limit - current, 0);
return `${available} disponibles`;
};
const getLimitValue = (limit: number, current?: number) => (typeof current === "number" ? current.toString() : formatLimit(limit));
const getLimitCaption = (limit: number, current?: number) => {
if (typeof current !== "number") return limit === -1 ? "sin tope" : "límite total";
return `de ${formatLimit(limit)}`;
};
const formatDate = (date?: Date) => (date ? dayjs(date).format("DD/MM/YYYY") : "No disponible");
const getLimitCards = (plan: ISubscriptionInfo["plan"], metrics: DashboardMetrics | null) => [
@@ -45,6 +60,7 @@ export default function CurrentPlanPage() {
const [isOwner, setIsOwner] = useState(false);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [isCapacityExpanded, setIsCapacityExpanded] = useState(false);
useEffect(() => {
if (!SessionInfo.userId) {
@@ -212,9 +228,12 @@ export default function CurrentPlanPage() {
<h2>Características</h2>
</div>
{plan.features.length > 0 ? (
<ul className={styles.features}>
{plan.features.map((feature) => <li key={feature}>{feature}</li>)}
</ul>
<div className={styles.featuresPanel}>
<p>Un resumen rápido de los beneficios incluidos para operar tu organización.</p>
<ul className={styles.features}>
{plan.features.map((feature) => <li key={feature}>{feature}</li>)}
</ul>
</div>
) : (
<p className={styles.message}>Este plan no tiene características cargadas.</p>
)}
@@ -223,22 +242,21 @@ export default function CurrentPlanPage() {
<div className={styles.section}>
<div className={styles.sectionHeader}>
<span>Capacidad del plan</span>
<h2>Límites principales</h2>
<h2>Uso y límites principales</h2>
</div>
<div className={styles.limitsGrid}>
{getLimitCards(plan, metrics).map((item) => (
<div className={styles.limitItem} key={item.label}>
<span>{item.label}</span>
<strong>{formatLimit(item.limit)}</strong>
</div>
))}
</div>
{metrics && (
<div className={styles.usageShowcase}>
<div className={styles.usageIntro}>
<span>Uso actual</span>
<strong>Actividad real de tu organización</strong>
</div>
<button
className={styles.usageShowcase}
type="button"
aria-expanded={isCapacityExpanded}
aria-controls="capacity-details"
onClick={() => setIsCapacityExpanded((current) => !current)}
>
<div className={styles.usageIntro}>
<span>Uso actual</span>
<strong>Actividad real de tu organización</strong>
<small>{isCapacityExpanded ? "Ocultar límites del plan" : "Ver límites del plan"}</small>
</div>
{metrics && (
<div className={styles.usageStats}>
<div className={styles.usageStat}>
<span>Clientes</span>
@@ -253,6 +271,22 @@ export default function CurrentPlanPage() {
<strong>{metrics.employeesCount}</strong>
</div>
</div>
)}
</button>
{isCapacityExpanded && (
<div className={styles.limitsPanel} id="capacity-details">
{getLimitCards(plan, metrics).map((item) => (
<div className={styles.limitRow} key={item.label}>
<div>
<span>{item.label}</span>
<small>{getLimitDetail(item.limit, item.current)}</small>
</div>
<div className={styles.limitValues}>
<strong>{getLimitValue(item.limit, item.current)}</strong>
<small>{getLimitCaption(item.limit, item.current)}</small>
</div>
</div>
))}
</div>
)}
</div>