Improve Leads mobile UX and nest leads under Customers nav.

Restore the 5-tab mobile bar, add responsive lead cards with long-press select, and fix stats/filters layout.
This commit is contained in:
2026-09-28 11:50:44 +03:30
parent 7e73ba9936
commit 8effd4cbc6
4 changed files with 727 additions and 108 deletions
+57 -4
View File
@@ -1,17 +1,29 @@
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue';
import UiIcon from '@/components/UiIcon.vue';
import type { NavHubDef } from '@/lib/nav-hubs';
import type { NavHubDef, NavHubKey } from '@/lib/nav-hubs';
defineProps<{
hub: NavHubDef;
/** اگر از هاب والد آمده (مثلاً مشتریان → لیدها) */
canBack?: boolean;
}>();
const emit = defineEmits<{
close: [];
back: [];
go: [to: string];
openHub: [key: NavHubKey];
}>();
function onCard(card: NavHubDef['cards'][number]) {
if (card.hub) {
emit('openHub', card.hub);
return;
}
emit('go', card.to);
}
function onKey(event: KeyboardEvent) {
if (event.key === 'Escape') emit('close');
}
@@ -29,7 +41,18 @@ onUnmounted(() => {
<section class="hub" role="dialog" aria-modal="true" :aria-label="hub.title">
<header class="head">
<div class="copy">
<h1>{{ hub.title }}</h1>
<div class="title-row">
<button
v-if="canBack"
type="button"
class="back-btn"
aria-label="بازگشت"
@click="emit('back')"
>
<UiIcon name="chevron" :size="16" class="back-chev" />
</button>
<h1>{{ hub.title }}</h1>
</div>
<p>{{ hub.desc }}</p>
</div>
<span class="head-ico" :class="hub.tint" aria-hidden="true">
@@ -40,10 +63,10 @@ onUnmounted(() => {
<div class="grid">
<button
v-for="card in hub.cards"
:key="card.to + card.title"
:key="(card.hub ?? card.to) + card.title"
class="card"
type="button"
@click="emit('go', card.to)"
@click="onCard(card)"
>
<span class="card-top">
<span class="card-title">
@@ -105,6 +128,36 @@ onUnmounted(() => {
color: #0f172a;
}
.title-row {
display: flex;
align-items: center;
gap: 0.35rem;
}
.back-btn {
width: 1.75rem;
height: 1.75rem;
border: 0;
border-radius: 999px;
background: #f1f5f9;
color: #64748b;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
padding: 0;
flex-shrink: 0;
}
.back-btn:hover {
background: #e2e8f0;
color: #0f172a;
}
.back-chev {
transform: none;
}
.copy p {
margin: 0.15rem 0 0;
color: #94a3b8;
+83 -32
View File
@@ -32,6 +32,8 @@ const SIDEBAR_KEY = 'mento_sidebar_collapsed';
const collapsed = ref(false);
const openHubKey = ref<NavHubKey | null>(null);
const hubStack = ref<NavHubKey[]>([]);
const isMobileNav = ref(false);
const globalSearch = ref('');
const activityBadge = ref(0);
@@ -44,10 +46,12 @@ function pathIn(prefixes: string[]) {
const dashboardActive = computed(() => route.path === '/');
const loyaltyActive = computed(() => pathIn(['/loyalty']));
const reportsActive = computed(() => pathIn(['/reports']));
const customersActive = computed(() => pathIn(['/contacts']));
const leadsActive = computed(() =>
pathIn(['/leads', '/settings/lead-stages', '/settings/lead-forms']),
);
const customersActive = computed(
() => pathIn(['/contacts']) || (isMobileNav.value && leadsActive.value),
);
const salesActive = computed(() =>
pathIn([
'/pipeline',
@@ -111,23 +115,6 @@ function moduleAllowed(module?: string) {
return enabledModules.value.includes(module as never);
}
const openHub = computed<NavHubDef | null>(() => {
if (!openHubKey.value) return null;
const hub = NAV_HUBS.find((item) => item.key === openHubKey.value);
if (!hub) return null;
return {
...hub,
cards: hub.cards.filter(
(card) =>
(!card.admin || canSettings.value) && moduleAllowed(card.module),
),
shortcuts: hub.shortcuts.filter(
(item) =>
(!item.admin || canSettings.value) && moduleAllowed(item.module),
),
};
});
const hubVisible = computed(() => {
const map: Record<string, boolean> = {};
for (const hub of NAV_HUBS) {
@@ -139,6 +126,41 @@ const hubVisible = computed(() => {
return map;
});
const openHub = computed<NavHubDef | null>(() => {
if (!openHubKey.value) return null;
const hub = NAV_HUBS.find((item) => item.key === openHubKey.value);
if (!hub) return null;
const cards = hub.cards.filter(
(card) =>
(!card.admin || canSettings.value) && moduleAllowed(card.module),
);
if (
hub.key === 'customers' &&
isMobileNav.value &&
hubVisible.value.leads
) {
cards.push({
to: '/leads',
hub: 'leads',
title: 'لیدها',
desc: 'مخاطبین بالقوه قبل از تبدیل به مشتری',
icon: 'user-plus',
tint: 'sky',
module: 'leads',
});
}
return {
...hub,
cards,
shortcuts: hub.shortcuts.filter(
(item) =>
(!item.admin || canSettings.value) && moduleAllowed(item.module),
),
};
});
const hubCanBack = computed(() => hubStack.value.length > 0);
function givenName(fullName: string): string {
const parts = fullName.trim().split(/\s+/).filter(Boolean);
if (parts.length <= 1) return fullName.trim();
@@ -177,12 +199,31 @@ watch([profileFullName, collapsed], () => {
function closeHub() {
openHubKey.value = null;
hubStack.value = [];
}
function onHubClick(key: NavHubKey) {
hubStack.value = [];
openHubKey.value = openHubKey.value === key ? null : key;
}
function openNestedHub(key: NavHubKey) {
if (openHubKey.value) {
hubStack.value = [...hubStack.value, openHubKey.value];
}
openHubKey.value = key;
}
function backHub() {
const prev = hubStack.value[hubStack.value.length - 1];
if (!prev) {
closeHub();
return;
}
hubStack.value = hubStack.value.slice(0, -1);
openHubKey.value = prev;
}
function openTabHub(key: NavHubKey) {
moreOpen.value = false;
notifOpen.value = false;
@@ -233,7 +274,11 @@ function closeNotif() {
}
function onMobileMqChange(event: MediaQueryListEvent) {
if (!event.matches) closeMore();
isMobileNav.value = event.matches;
if (!event.matches) {
closeMore();
hubStack.value = [];
}
}
function onLogout() {
@@ -358,6 +403,7 @@ onMounted(() => {
if (meta) profileNameObserver.observe(meta);
}
mobileMq = window.matchMedia('(max-width: 900px)');
isMobileNav.value = mobileMq.matches;
mobileMq.addEventListener('change', onMobileMqChange);
});
@@ -717,7 +763,14 @@ onUnmounted(() => {
<div v-if="openHub" class="hub-layer">
<button class="hub-dim" type="button" aria-label="بستن منو" @click="closeHub" />
<NavHubPanel :hub="openHub" @close="closeHub" @go="goHub" />
<NavHubPanel
:hub="openHub"
:can-back="hubCanBack"
@close="closeHub"
@back="backHub"
@go="goHub"
@open-hub="openNestedHub"
/>
</div>
<nav class="mobile-tabbar mobile-only" aria-label="منوی اصلی">
@@ -733,23 +786,21 @@ onUnmounted(() => {
<button
class="tab"
type="button"
:class="{ on: !moreOpen && (customersActive || openHubKey === 'customers') }"
:aria-expanded="openHubKey === 'customers'"
:class="{
on:
!moreOpen &&
(customersActive ||
openHubKey === 'customers' ||
(isMobileNav && openHubKey === 'leads')),
}"
:aria-expanded="
openHubKey === 'customers' || (isMobileNav && openHubKey === 'leads')
"
@click="openTabHub('customers')"
>
<UiIcon name="users" :size="20" />
<span>مشتریان</span>
</button>
<button
class="tab"
type="button"
:class="{ on: !moreOpen && (leadsActive || openHubKey === 'leads') }"
:aria-expanded="openHubKey === 'leads'"
@click="openTabHub('leads')"
>
<UiIcon name="user-plus" :size="20" />
<span>لیدها</span>
</button>
<button
class="tab"
type="button"
+2
View File
@@ -48,6 +48,8 @@ export type NavHubCard = {
tint: NavHubTint;
admin?: boolean;
module?: OrgModuleKey;
/** اگر ست باشد، به‌جای مسیر، هاب تو در تو باز می‌شود (موبایل: مشتریان → لیدها) */
hub?: NavHubKey;
};
export type NavHubShortcut = {
+585 -72
View File
@@ -500,6 +500,64 @@ function toggleAll(checked: boolean) {
selected.value = new Set(data.value?.items.map((l) => l.id) ?? []);
}
const LONG_PRESS_MS = 450;
const LONG_PRESS_MOVE_PX = 12;
let cardPressTimer: ReturnType<typeof setTimeout> | null = null;
let cardPressId: string | null = null;
let cardPressTriggered = false;
let cardPressX = 0;
let cardPressY = 0;
function clearCardPress() {
if (cardPressTimer) clearTimeout(cardPressTimer);
cardPressTimer = null;
cardPressId = null;
}
function onCardPointerDown(leadId: string, event: PointerEvent) {
if (event.pointerType === 'mouse' && event.button !== 0) return;
clearCardPress();
cardPressTriggered = false;
cardPressId = leadId;
cardPressX = event.clientX;
cardPressY = event.clientY;
cardPressTimer = setTimeout(() => {
cardPressTriggered = true;
cardPressTimer = null;
toggleOne(leadId, !selected.value.has(leadId));
try {
navigator.vibrate?.(18);
} catch {
// ignore
}
}, LONG_PRESS_MS);
}
function onCardPointerMove(event: PointerEvent) {
if (!cardPressTimer || !cardPressId) return;
const dx = event.clientX - cardPressX;
const dy = event.clientY - cardPressY;
if (dx * dx + dy * dy > LONG_PRESS_MOVE_PX * LONG_PRESS_MOVE_PX) {
clearCardPress();
}
}
function onCardPointerEnd() {
clearCardPress();
}
function onCardClick(lead: LeadDto) {
if (cardPressTriggered) {
cardPressTriggered = false;
return;
}
if (selected.value.has(lead.id)) {
toggleOne(lead.id, false);
return;
}
goDetail(lead.id);
}
function mergeFacets(items: LeadDto[]) {
const cities = new Set(cityOptions.value);
const sources = new Set(sourceOptions.value);
@@ -1079,30 +1137,32 @@ onBeforeUnmount(() => {
<UiIcon name="search" :size="16" />
<input v-model="q" type="search" placeholder="جستجو..." />
</form>
<select
class="filter-select"
:value="cityFilter"
@change="setCityFilter(($event.target as HTMLSelectElement).value)"
>
<option value="">همه مناطق</option>
<option v-for="c in cityOptions" :key="c" :value="c">{{ c }}</option>
</select>
<select
class="filter-select"
:value="sourceFilter"
@change="setSourceFilter(($event.target as HTMLSelectElement).value)"
>
<option value="">همه منابع</option>
<option v-for="s in sourceOptions" :key="s" :value="s">{{ s }}</option>
</select>
<select
class="filter-select"
:value="stageId"
@change="setStageFilter(($event.target as HTMLSelectElement).value)"
>
<option value="">همه وضعیت</option>
<option v-for="s in stages" :key="s.id" :value="s.id">{{ s.name }}</option>
</select>
<div class="filter-selects">
<select
class="filter-select"
:value="cityFilter"
@change="setCityFilter(($event.target as HTMLSelectElement).value)"
>
<option value="">همه مناطق</option>
<option v-for="c in cityOptions" :key="c" :value="c">{{ c }}</option>
</select>
<select
class="filter-select"
:value="sourceFilter"
@change="setSourceFilter(($event.target as HTMLSelectElement).value)"
>
<option value="">همه منابع</option>
<option v-for="s in sourceOptions" :key="s" :value="s">{{ s }}</option>
</select>
<select
class="filter-select"
:value="stageId"
@change="setStageFilter(($event.target as HTMLSelectElement).value)"
>
<option value="">همه وضعیت</option>
<option v-for="s in stages" :key="s.id" :value="s.id">{{ s.name }}</option>
</select>
</div>
</div>
<div class="filters-end">
<div class="pop">
@@ -1189,10 +1249,135 @@ onBeforeUnmount(() => {
<p v-else-if="loading" class="muted">در حال بارگذاری…</p>
<template v-else-if="data?.items.length">
<div class="cards">
<article
v-for="(lead, idx) in data.items"
:key="`m-${lead.id}`"
class="m-card"
:class="{ selected: selected.has(lead.id) }"
:data-tint="stageTintForLead(lead)"
@pointerdown="onCardPointerDown(lead.id, $event)"
@pointermove="onCardPointerMove"
@pointerup="onCardPointerEnd"
@pointercancel="onCardPointerEnd"
@contextmenu.prevent
@click="onCardClick(lead)"
>
<div class="m-head">
<span class="m-badge" :data-tint="stageTintForLead(lead)">
{{ toFaDigits(rangeFrom + idx) }}
</span>
<UiDropdownMenu
:open="logoMenuId === `m:${lead.id}`"
align="start"
@update:open="(v) => (logoMenuId = v ? `m:${lead.id}` : null)"
>
<template #trigger="{ toggle }">
<button
type="button"
class="row-ico"
:class="{ busy: logoBusyId === lead.id }"
:data-tint="stageTintForLead(lead)"
:title="lead.hasLogo ? 'تغییر لوگو' : 'افزودن لوگو'"
@click.stop="toggle"
>
<img
v-if="logoUrls[lead.id]"
:src="logoUrls[lead.id]"
:alt="lead.title"
class="row-logo"
/>
<UiIcon v-else :name="rowIcon(lead)" :size="16" />
</button>
</template>
<button type="button" @click="openLogoPicker(lead.id)">
{{ lead.hasLogo ? 'تغییر لوگو' : 'افزودن لوگو' }}
</button>
<button
v-if="lead.hasLogo"
type="button"
class="danger-item"
@click="removeLeadLogo(lead.id)"
>
حذف لوگو
</button>
</UiDropdownMenu>
<div class="m-title">
<strong class="name">{{ lead.title }}</strong>
<span v-if="lead.contactName" class="sub">{{ lead.contactName }}</span>
</div>
<UiDropdownMenu
:open="rowMenuId === `m:${lead.id}`"
align="end"
@update:open="(v) => (rowMenuId = v ? `m:${lead.id}` : null)"
>
<template #trigger="{ toggle }">
<button
type="button"
class="act-btn"
title="بیشتر"
@click.stop="toggle"
>
<UiIcon name="dots-v" :size="16" />
</button>
</template>
<button type="button" @click="goDetail(lead.id)">مشاهده</button>
<button type="button" @click="openEdit(lead)">ویرایش</button>
<a :href="`tel:${lead.mobile}`">تماس</a>
<button type="button" class="danger-item" @click="deleteLead(lead)">
حذف
</button>
</UiDropdownMenu>
</div>
<div class="m-meta-row">
<button
v-if="lead.mobile"
type="button"
class="m-line m-phone"
dir="ltr"
@click.stop="callLead(lead)"
>
<UiIcon name="phone" :size="13" />
<span>{{ formatPhone(lead.mobile) }}</span>
</button>
<p v-if="lead.city || lead.region" class="m-line m-place">
<UiIcon name="building" :size="13" />
<span>{{ [lead.city, lead.region].filter(Boolean).join(' · ') }}</span>
</p>
</div>
<div class="m-foot">
<span v-if="lead.assigneeName" class="assignee">
<span class="avatar">{{ initials(lead.assigneeName) }}</span>
{{ lead.assigneeName }}
</span>
<span v-else class="muted-dash">بدون مسئول</span>
<span class="m-activity">{{ lastActivity(lead).when }}</span>
</div>
<div class="m-pills">
<span class="stage-pill" :data-tint="stageTintForLead(lead)">
{{ lead.stageName }}
</span>
<span v-if="lead.source" class="source-pill">{{ lead.source }}</span>
</div>
</article>
</div>
<div class="table-wrap">
<table class="data-table">
<thead>
<tr>
<th class="check-col">
<input
type="checkbox"
:checked="allSelected"
@click.stop
@change="toggleAll(($event.target as HTMLInputElement).checked)"
/>
</th>
<th class="num-col">#</th>
<th class="icon-col"></th>
<th>نام / شرکت</th>
<th>شماره تماس</th>
@@ -1202,23 +1387,23 @@ onBeforeUnmount(() => {
<th>مسئول</th>
<th>آخرین فعالیت</th>
<th class="actions-col">اقدامات</th>
<th class="check-col">
<input
type="checkbox"
:checked="allSelected"
@click.stop
@change="toggleAll(($event.target as HTMLInputElement).checked)"
/>
</th>
</tr>
</thead>
<tbody>
<tr
v-for="lead in data.items"
v-for="(lead, idx) in data.items"
:key="lead.id"
class="row-click"
@click="goDetail(lead.id)"
>
<td class="check-col" @click.stop>
<input
type="checkbox"
:checked="selected.has(lead.id)"
@change="toggleOne(lead.id, ($event.target as HTMLInputElement).checked)"
/>
</td>
<td class="num-col">{{ toFaDigits(rangeFrom + idx) }}</td>
<td class="icon-col" @click.stop>
<UiDropdownMenu
:open="logoMenuId === lead.id"
@@ -1287,9 +1472,6 @@ onBeforeUnmount(() => {
<strong>{{ lastActivity(lead).label }}</strong>
<span class="sub">{{ lastActivity(lead).when }}</span>
</span>
<span class="activity-ico">
<UiIcon :name="lastActivity(lead).icon" :size="14" />
</span>
</span>
</td>
<td class="actions-col" @click.stop>
@@ -1334,13 +1516,6 @@ onBeforeUnmount(() => {
</button>
</div>
</td>
<td class="check-col" @click.stop>
<input
type="checkbox"
:checked="selected.has(lead.id)"
@change="toggleOne(lead.id, ($event.target as HTMLInputElement).checked)"
/>
</td>
</tr>
</tbody>
</table>
@@ -1764,22 +1939,85 @@ onBeforeUnmount(() => {
@media (max-width: 900px) {
.stats {
flex-wrap: wrap;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.65rem;
overflow: visible;
align-items: stretch;
}
/* جدا کردن جفت تا هر کارت یک سلول گرید شود */
.stat-pair {
display: contents;
}
.stat-total,
.stat-pair,
.stat-cell.alone {
flex: 1 1 calc(50% - 0.4rem);
min-width: 9rem;
.stat-cell {
width: 100%;
min-width: 0;
height: 5.5rem;
min-height: 5.5rem;
max-height: 5.5rem;
flex: none;
box-sizing: border-box;
}
}
@media (max-width: 520px) {
.stat-total,
.stat-pair,
.stat-cell.alone {
flex: 1 1 100%;
.stat-total {
padding: 0.65rem 0.75rem;
gap: 0.55rem;
}
.stat-total-ico {
width: 2.15rem;
height: 2.15rem;
}
.stat-total .stat-label {
font-size: 0.72rem;
}
.stat-total .stat-num {
font-size: 1.2rem;
}
.stat-cell {
padding: 1rem 0.45rem 0.6rem;
}
.stat-cell .stat-label {
white-space: normal;
font-size: 0.72rem;
line-height: 1.2;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.stat-cell .stat-num {
font-size: 1.2rem;
}
.stat-pair .stat-cell {
border: 1px solid #e5e7eb !important;
border-radius: 12px !important;
background: #fff;
box-shadow: none !important;
}
.stat-pair .stat-cell.wide {
flex: none;
}
.stat-pair .stat-cell + .stat-cell {
border-inline-start: 1px solid #e5e7eb !important;
}
.stat-pair .stat-cell:hover:not(:disabled),
.stat-pair .stat-cell.on {
border-color: color-mix(in srgb, var(--accent) 35%, #e5e7eb) !important;
box-shadow: 0 0 0 2px var(--accent-soft) !important;
background: color-mix(in srgb, var(--accent-soft) 70%, #fff);
}
}
@@ -1803,6 +2041,10 @@ onBeforeUnmount(() => {
align-items: center;
}
.filter-selects {
display: contents;
}
.filters-label {
display: inline-flex;
align-items: center;
@@ -1854,6 +2096,70 @@ onBeforeUnmount(() => {
cursor: pointer;
}
@media (max-width: 900px) {
.filters {
flex-direction: column;
align-items: stretch;
gap: 0.5rem;
}
.filters-start {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
gap: 0.5rem;
width: 100%;
}
.filters-label {
grid-column: 1;
grid-row: 1;
align-self: center;
padding-inline-end: 0;
}
.search {
grid-column: 2;
grid-row: 1;
min-width: 0;
width: 100%;
}
.search input {
width: 100%;
min-width: 0;
flex: 1;
}
.filter-selects {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.5rem;
grid-column: 1 / -1;
grid-row: 2;
width: 100%;
}
.filter-select {
width: 100%;
min-width: 0;
max-width: 100%;
padding-inline: 0.45rem;
font-size: 0.78rem;
}
.filters-end {
width: 100%;
flex-wrap: nowrap;
justify-content: space-between;
}
.filters-end .sort-btn {
flex: 1;
min-width: 0;
justify-content: center;
}
}
.sort-btn {
display: inline-flex;
align-items: center;
@@ -1967,6 +2273,213 @@ onBeforeUnmount(() => {
background: #fff;
}
.cards {
display: none;
gap: 0.7rem;
}
.m-card {
position: relative;
display: grid;
gap: 0.55rem;
padding: 0.85rem;
background: #fff;
border: 1px solid var(--line);
border-radius: 14px;
cursor: pointer;
min-width: 0;
overflow: hidden;
transition: background 0.15s, border-color 0.15s, box-shadow 0.15s;
touch-action: pan-y;
user-select: none;
-webkit-user-select: none;
}
.m-card:active {
background: #f8fafc;
}
.m-card.selected {
background: #ecfdf5;
border-color: #6ee7b7;
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.18);
}
.m-card.selected:active {
background: #d1fae5;
}
.m-badge {
width: 1.55rem;
height: 1.55rem;
border-radius: 999px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.68rem;
font-weight: 800;
font-variant-numeric: tabular-nums;
background: #e2e8f0;
color: #334155;
flex-shrink: 0;
}
.m-badge[data-tint='mint'],
.m-badge[data-tint='teal'] {
background: #d1fae5;
color: #047857;
}
.m-badge[data-tint='amber'] {
background: #fef3c7;
color: #b45309;
}
.m-badge[data-tint='peach'] {
background: #ffedd5;
color: #c2410c;
}
.m-badge[data-tint='sky'] {
background: #e0f2fe;
color: #0369a1;
}
.m-badge[data-tint='violet'] {
background: #ede9fe;
color: #6d28d9;
}
.m-badge[data-tint='rose'] {
background: #fee2e2;
color: #b91c1c;
}
.m-head {
display: flex;
align-items: center;
gap: 0.45rem;
min-width: 0;
}
.m-title {
flex: 1 1 auto;
min-width: 0;
display: grid;
gap: 0.05rem;
}
.m-title .name {
overflow-wrap: anywhere;
line-height: 1.35;
}
.m-title .sub {
margin-top: 0;
}
.m-meta-row {
display: flex;
direction: ltr;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
width: 100%;
min-width: 0;
}
.m-line {
margin: 0;
display: flex;
align-items: center;
gap: 0.35rem;
font-size: 0.8rem;
color: #475569;
min-width: 0;
}
.m-meta-row .m-place {
flex: 0 1 auto;
max-width: 55%;
direction: rtl;
justify-content: flex-end;
}
.m-meta-row .m-phone {
flex: 0 1 auto;
max-width: 55%;
justify-content: flex-start;
}
button.m-line {
border: 0;
background: transparent;
padding: 0;
font: inherit;
text-align: start;
cursor: pointer;
width: auto;
}
.m-phone {
color: #059669;
font-weight: 700;
}
.m-phone:active {
opacity: 0.75;
}
.m-line :deep(svg) {
flex-shrink: 0;
color: #94a3b8;
}
.m-phone :deep(svg) {
color: #10b981;
}
.m-line span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.m-pills {
display: flex;
direction: ltr;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
width: 100%;
min-width: 0;
flex-wrap: nowrap;
}
.m-pills .stage-pill {
flex: 0 1 auto;
max-width: 55%;
}
.m-pills .source-pill {
flex: 0 1 auto;
max-width: 55%;
margin-left: auto;
direction: rtl;
}
.m-foot {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 0.45rem;
padding-top: 0.1rem;
}
.m-activity {
font-size: 0.78rem;
font-weight: 600;
color: #64748b;
white-space: nowrap;
}
.data-table {
width: 100%;
border-collapse: collapse;
@@ -2001,6 +2514,14 @@ onBeforeUnmount(() => {
width: 2.75rem;
}
.num-col {
width: 2.5rem;
text-align: center;
color: var(--muted);
font-variant-numeric: tabular-nums;
font-size: 0.8rem;
}
.check-col {
width: 2.5rem;
text-align: center;
@@ -2161,8 +2682,6 @@ onBeforeUnmount(() => {
.activity {
display: inline-flex;
align-items: center;
gap: 0.55rem;
justify-content: space-between;
min-width: 8rem;
}
@@ -2171,18 +2690,6 @@ onBeforeUnmount(() => {
font-size: 0.84rem;
}
.activity-ico {
width: 1.7rem;
height: 1.7rem;
border-radius: 8px;
background: #f1f5f9;
color: #64748b;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.row-actions {
display: inline-flex;
align-items: center;
@@ -2216,6 +2723,16 @@ onBeforeUnmount(() => {
background: #d1fae5;
}
@media (max-width: 900px) {
.table-wrap {
display: none;
}
.cards {
display: grid;
}
}
:deep(.ui-dd-panel) .danger-item,
.danger-item {
color: #b91c1c !important;
@@ -2463,10 +2980,6 @@ onBeforeUnmount(() => {
grid-template-columns: 1fr;
}
.search input {
width: 6rem;
}
:deep(.actions) .btn-full {
display: none;
}