Match Leads stats cards to the mockup and fix Contacts double menus.

Stage summary uses the green total card plus paired/solo white cells; people row menus use separate mobile/desktop keys.
This commit is contained in:
2026-09-27 20:35:57 +03:30
parent 3833788aa8
commit ec5c7639e3
2 changed files with 189 additions and 110 deletions
+4 -4
View File
@@ -406,9 +406,9 @@ onMounted(load);
{{ p.fullName }}
</RouterLink>
<UiDropdownMenu
:open="menuId === p.id"
:open="menuId === `m:${p.id}`"
align="end"
@update:open="(v) => (menuId = v ? p.id : null)"
@update:open="(v) => (menuId = v ? `m:${p.id}` : null)"
>
<template #trigger="{ toggle }">
<button class="more" type="button" @click.stop="toggle">
@@ -491,9 +491,9 @@ onMounted(load);
<td>{{ formatDate(p.createdAt) }}</td>
<td class="ops" @click.stop>
<UiDropdownMenu
:open="menuId === p.id"
:open="menuId === `t:${p.id}`"
align="end"
@update:open="(v) => (menuId = v ? p.id : null)"
@update:open="(v) => (menuId = v ? `t:${p.id}` : null)"
>
<template #trigger="{ toggle }">
<button type="button" class="more" @click.stop="toggle">
+185 -106
View File
@@ -148,6 +148,13 @@ const stageCards = computed(() => {
return board.slice(0, 5);
});
const pairStages = computed(() =>
stageCards.value.length >= 2 ? stageCards.value.slice(0, 2) : [],
);
const soloStages = computed(() =>
stageCards.value.length >= 2 ? stageCards.value.slice(2) : stageCards.value,
);
const pageNumbers = computed(() => {
const totalPages = pageCount.value;
const current = page.value;
@@ -809,31 +816,47 @@ onBeforeUnmount(stopImportPoll);
<div class="stats" role="list">
<button
type="button"
class="stat mint"
class="stat-total"
:class="{ on: !stageId }"
role="listitem"
@click="setStageFilter('')"
>
<span class="stat-ico"><UiIcon name="users" :size="18" /></span>
<span class="stat-body">
<span class="stat-total-ico" aria-hidden="true">
<UiIcon name="users" :size="18" />
</span>
<span class="stat-total-body">
<span class="stat-label">همه لیدها</span>
<strong class="stat-num">{{ formatCount(stats?.total ?? total) }}</strong>
</span>
</button>
<div v-if="pairStages.length" class="stat-pair" role="listitem">
<button
v-for="s in pairStages"
:key="s.id"
type="button"
class="stat-cell"
:class="{ on: stageId === s.id }"
@click="setStageFilter(s.id)"
>
<i class="stat-dot" :data-tint="stageTint(s.color)" aria-hidden="true" />
<span class="stat-label">{{ s.name }}</span>
<strong class="stat-num">{{ formatCount(s.leadCount ?? 0) }}</strong>
</button>
</div>
<button
v-for="s in stageCards"
v-for="s in soloStages"
:key="s.id"
type="button"
class="stat"
:class="[stageTint(s.color), { on: stageId === s.id }]"
class="stat-cell alone"
:class="{ on: stageId === s.id }"
role="listitem"
@click="setStageFilter(s.id)"
>
<span class="stat-ico"><UiIcon name="target" :size="18" /></span>
<span class="stat-body">
<span class="stat-label">{{ s.name }}</span>
<strong class="stat-num">{{ formatCount(s.leadCount ?? 0) }}</strong>
</span>
<i class="stat-dot" :data-tint="stageTint(s.color)" aria-hidden="true" />
<span class="stat-label">{{ s.name }}</span>
<strong class="stat-num">{{ formatCount(s.leadCount ?? 0) }}</strong>
</button>
</div>
@@ -1289,135 +1312,191 @@ onBeforeUnmount(stopImportPoll);
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
gap: 0.65rem;
display: flex;
flex-wrap: wrap;
align-items: stretch;
gap: 0.75rem;
}
.stat {
.stat-total {
display: flex;
align-items: center;
gap: 0.7rem;
text-align: start;
padding: 0.85rem 0.95rem;
border-radius: 12px;
border: 1px solid transparent;
background: #fff;
gap: 0.75rem;
min-width: 9.5rem;
padding: 0.85rem 1rem;
border: 1px solid #bbf7d0;
border-radius: 14px;
background: #ecfdf5;
cursor: pointer;
font: inherit;
color: var(--ink);
text-align: start;
color: #047857;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
transition: border-color 0.15s, box-shadow 0.15s, transform 0.15s;
transition: box-shadow 0.15s, border-color 0.15s;
}
.stat:hover {
transform: translateY(-1px);
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.08);
.stat-total:hover,
.stat-total.on {
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.22);
border-color: #86efac;
}
.stat.on {
border-color: color-mix(in srgb, var(--accent) 45%, transparent);
box-shadow: 0 0 0 2px var(--accent-soft);
}
.stat-ico {
width: 2.35rem;
height: 2.35rem;
.stat-total-ico {
width: 2.4rem;
height: 2.4rem;
border-radius: 10px;
background: #fff;
color: #10b981;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06);
}
.stat-body {
.stat-total-body {
display: grid;
gap: 0.15rem;
gap: 0.1rem;
min-width: 0;
}
.stat-label {
font-size: 0.78rem;
color: var(--muted);
.stat-total .stat-label {
color: #059669;
font-size: 0.82rem;
font-weight: 600;
}
.stat-total .stat-num {
color: #047857;
font-size: 1.35rem;
font-weight: 800;
letter-spacing: -0.02em;
font-variant-numeric: tabular-nums;
line-height: 1.15;
}
.stat-pair {
display: flex;
align-items: stretch;
flex: 1 1 14rem;
min-width: 12rem;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 14px;
overflow: hidden;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
}
.stat-pair .stat-cell {
flex: 1 1 0;
border: 0;
border-radius: 0;
box-shadow: none;
min-width: 0;
}
.stat-pair .stat-cell + .stat-cell {
border-inline-start: 1px solid #e5e7eb;
}
.stat-cell {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.2rem;
min-width: 6.5rem;
padding: 0.85rem 0.9rem 0.75rem;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 14px;
cursor: pointer;
font: inherit;
color: #0f172a;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
transition: box-shadow 0.15s, border-color 0.15s;
}
.stat-cell.alone {
flex: 0 1 7.5rem;
}
.stat-cell:hover,
.stat-cell.on {
border-color: color-mix(in srgb, var(--accent) 40%, #e5e7eb);
box-shadow: 0 0 0 2px var(--accent-soft);
}
.stat-pair:has(.stat-cell.on) {
border-color: color-mix(in srgb, var(--accent) 40%, #e5e7eb);
box-shadow: 0 0 0 2px var(--accent-soft);
}
.stat-dot {
width: 0.55rem;
height: 0.55rem;
border-radius: 999px;
margin-bottom: 0.15rem;
background: #94a3b8;
box-shadow: 0 0 0 4px color-mix(in srgb, #94a3b8 18%, transparent);
}
.stat-dot[data-tint='mint'],
.stat-dot[data-tint='teal'] {
background: #10b981;
box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.18);
}
.stat-dot[data-tint='amber'] {
background: #f59e0b;
box-shadow: 0 0 0 4px rgba(245, 158, 11, 0.18);
}
.stat-dot[data-tint='peach'] {
background: #f97316;
box-shadow: 0 0 0 4px rgba(249, 115, 22, 0.18);
}
.stat-dot[data-tint='sky'] {
background: #0ea5e9;
box-shadow: 0 0 0 4px rgba(14, 165, 233, 0.18);
}
.stat-dot[data-tint='violet'] {
background: #8b5cf6;
box-shadow: 0 0 0 4px rgba(139, 92, 246, 0.18);
}
.stat-dot[data-tint='rose'] {
background: #ef4444;
box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.18);
}
.stat-cell .stat-label {
font-size: 0.8rem;
font-weight: 500;
color: #64748b;
text-align: center;
white-space: nowrap;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
.stat-num {
.stat-cell .stat-num {
font-size: 1.25rem;
font-weight: 800;
color: #0f172a;
letter-spacing: -0.02em;
font-variant-numeric: tabular-nums;
line-height: 1.15;
}
.stat.mint {
background: linear-gradient(180deg, #f0fdf6 0%, #fff 100%);
}
.stat.mint .stat-ico {
background: #d1fae5;
color: #059669;
}
.stat.mint .stat-num {
color: #047857;
}
@media (max-width: 720px) {
.stat-pair {
flex: 1 1 100%;
}
.stat.amber {
background: linear-gradient(180deg, #fffbeb 0%, #fff 100%);
}
.stat.amber .stat-ico {
background: #fef3c7;
color: #d97706;
}
.stat.amber .stat-num {
color: #b45309;
}
.stat.peach {
background: linear-gradient(180deg, #fff7ed 0%, #fff 100%);
}
.stat.peach .stat-ico {
background: #ffedd5;
color: #ea580c;
}
.stat.peach .stat-num {
color: #c2410c;
}
.stat.sky,
.stat.teal {
background: linear-gradient(180deg, #ecfdf5 0%, #fff 100%);
}
.stat.sky .stat-ico,
.stat.teal .stat-ico {
background: #ccfbf1;
color: #0d9488;
}
.stat.sky .stat-num,
.stat.teal .stat-num {
color: #0f766e;
}
.stat.violet {
background: linear-gradient(180deg, #f5f3ff 0%, #fff 100%);
}
.stat.violet .stat-ico {
background: #ede9fe;
color: #7c3aed;
}
.stat.violet .stat-num {
color: #6d28d9;
}
.stat.rose {
background: linear-gradient(180deg, #fef2f2 0%, #fff 100%);
}
.stat.rose .stat-ico {
background: #fee2e2;
color: #dc2626;
}
.stat.rose .stat-num {
color: #b91c1c;
.stat-cell.alone,
.stat-total {
flex: 1 1 calc(50% - 0.4rem);
}
}
.filters {