club home redesign; deal multi-select delete; anchored card menus
This commit is contained in:
@@ -15,6 +15,8 @@ const props = withDefaults(
|
||||
accent: PipelineAccent;
|
||||
dragging?: boolean;
|
||||
copy?: PipelineUiCopy;
|
||||
selectMode?: boolean;
|
||||
selected?: boolean;
|
||||
}>(),
|
||||
{ copy: () => DEAL_BOARD_COPY },
|
||||
);
|
||||
@@ -24,6 +26,8 @@ const emit = defineEmits<{
|
||||
dragEnd: [];
|
||||
open: [dealId: string];
|
||||
menu: [dealId: string];
|
||||
toggleSelect: [dealId: string];
|
||||
bulkDelete: [];
|
||||
}>();
|
||||
|
||||
const showAmount = computed(
|
||||
@@ -42,30 +46,60 @@ const avatarSrc = computed(() =>
|
||||
const extraAssignees = computed(() =>
|
||||
Math.max(0, (props.deal.assigneeNames?.length ?? 0) - 1),
|
||||
);
|
||||
const showTrash = computed(() => props.selectMode && props.selected);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article
|
||||
class="deal-card"
|
||||
:class="{ dragging, 'no-amount': !showAmount }"
|
||||
:class="{
|
||||
dragging,
|
||||
'no-amount': !showAmount,
|
||||
'select-mode': selectMode,
|
||||
selected,
|
||||
}"
|
||||
:data-card-id="deal.id"
|
||||
draggable="true"
|
||||
:draggable="!selectMode"
|
||||
role="listitem"
|
||||
:aria-grabbed="dragging ? 'true' : 'false'"
|
||||
:style="{
|
||||
'--badge-bg': colors.soft,
|
||||
'--badge-fg': colors.text,
|
||||
}"
|
||||
@dragstart="emit('dragStart', deal, $event)"
|
||||
@dragstart="!selectMode && emit('dragStart', deal, $event)"
|
||||
@dragend="emit('dragEnd')"
|
||||
@dblclick="emit('open', deal.id)"
|
||||
@dblclick="!selectMode && emit('open', deal.id)"
|
||||
@click="selectMode && emit('toggleSelect', deal.id)"
|
||||
>
|
||||
<span class="badge" aria-hidden="true">{{ deal.initials }}</span>
|
||||
<button
|
||||
v-if="selectMode"
|
||||
type="button"
|
||||
class="tick"
|
||||
:class="{ on: selected }"
|
||||
:aria-label="selected ? 'لغو انتخاب' : 'انتخاب معامله'"
|
||||
:aria-pressed="selected"
|
||||
@click.stop="emit('toggleSelect', deal.id)"
|
||||
>
|
||||
<UiIcon v-if="selected" name="check" :size="14" />
|
||||
</button>
|
||||
<span v-else class="badge" aria-hidden="true">{{ deal.initials }}</span>
|
||||
<div class="titles">
|
||||
<h3 @click.stop="emit('open', deal.id)">{{ deal.title }}</h3>
|
||||
<h3 @click.stop="selectMode ? emit('toggleSelect', deal.id) : emit('open', deal.id)">
|
||||
{{ deal.title }}
|
||||
</h3>
|
||||
<p v-if="deal.description" class="desc">{{ deal.description }}</p>
|
||||
</div>
|
||||
<button
|
||||
v-if="showTrash"
|
||||
type="button"
|
||||
class="menu trash"
|
||||
aria-label="حذف انتخابشدهها"
|
||||
@click.stop="emit('bulkDelete')"
|
||||
>
|
||||
<UiIcon name="trash" :size="16" />
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="menu"
|
||||
:aria-label="copy.itemMenuAria"
|
||||
@@ -116,11 +150,21 @@ const extraAssignees = computed(() =>
|
||||
box-shadow 0.18s ease,
|
||||
border-color 0.18s ease,
|
||||
transform 0.18s ease,
|
||||
opacity 0.18s ease;
|
||||
opacity 0.18s ease,
|
||||
background 0.18s ease;
|
||||
text-align: right;
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
.deal-card.select-mode {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.deal-card.selected {
|
||||
border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
|
||||
background: color-mix(in srgb, var(--accent) 6%, #fff);
|
||||
}
|
||||
|
||||
.deal-card:hover {
|
||||
border-color: color-mix(in srgb, var(--accent) 28%, var(--line));
|
||||
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.07);
|
||||
@@ -130,6 +174,10 @@ const extraAssignees = computed(() =>
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.deal-card.select-mode:active {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.deal-card.dragging {
|
||||
opacity: 0.95;
|
||||
transform: scale(1.02);
|
||||
@@ -147,7 +195,8 @@ const extraAssignees = computed(() =>
|
||||
}
|
||||
|
||||
.badge,
|
||||
.avatar {
|
||||
.avatar,
|
||||
.tick {
|
||||
width: var(--face);
|
||||
height: var(--face);
|
||||
max-width: var(--face);
|
||||
@@ -165,6 +214,21 @@ const extraAssignees = computed(() =>
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.tick {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 2px solid #cbd5e1;
|
||||
background: #fff;
|
||||
color: #fff;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tick.on {
|
||||
border-color: var(--accent);
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
object-fit: cover;
|
||||
border: 1px solid var(--line);
|
||||
@@ -239,6 +303,15 @@ const extraAssignees = computed(() =>
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.menu.trash {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.menu.trash:hover,
|
||||
.menu.trash:focus-visible {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.date {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -12,8 +12,10 @@ withDefaults(
|
||||
dropStageId: string | null;
|
||||
canManage?: boolean;
|
||||
copy?: PipelineUiCopy;
|
||||
selectMode?: boolean;
|
||||
selectedIds?: Set<string>;
|
||||
}>(),
|
||||
{ copy: () => DEAL_BOARD_COPY },
|
||||
{ copy: () => DEAL_BOARD_COPY, selectedIds: () => new Set() },
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -24,6 +26,8 @@ const emit = defineEmits<{
|
||||
addDeal: [stageId: string];
|
||||
openDeal: [dealId: string];
|
||||
menuDeal: [dealId: string];
|
||||
toggleSelect: [dealId: string];
|
||||
bulkDelete: [];
|
||||
renameStage: [stageId: string, title: string];
|
||||
deleteStage: [stageId: string];
|
||||
moveStage: [stageId: string, dir: -1 | 1];
|
||||
@@ -69,6 +73,8 @@ function submitAdd() {
|
||||
:drop-active="dropStageId === stage.id"
|
||||
:can-manage="canManage"
|
||||
:copy="copy"
|
||||
:select-mode="selectMode"
|
||||
:selected-ids="selectedIds"
|
||||
:is-first="index === 0"
|
||||
:is-last="index === stages.length - 1"
|
||||
@drag-start="(d, e) => emit('dragStart', d, e)"
|
||||
@@ -78,6 +84,8 @@ function submitAdd() {
|
||||
@add-deal="emit('addDeal', $event)"
|
||||
@open-deal="emit('openDeal', $event)"
|
||||
@menu-deal="emit('menuDeal', $event)"
|
||||
@toggle-select="emit('toggleSelect', $event)"
|
||||
@bulk-delete="emit('bulkDelete')"
|
||||
@rename="(id, title) => emit('renameStage', id, title)"
|
||||
@delete="emit('deleteStage', $event)"
|
||||
@move="(id, dir) => emit('moveStage', id, dir)"
|
||||
|
||||
@@ -15,8 +15,10 @@ const props = withDefaults(
|
||||
isFirst?: boolean;
|
||||
isLast?: boolean;
|
||||
copy?: PipelineUiCopy;
|
||||
selectMode?: boolean;
|
||||
selectedIds?: Set<string>;
|
||||
}>(),
|
||||
{ copy: () => DEAL_BOARD_COPY },
|
||||
{ copy: () => DEAL_BOARD_COPY, selectedIds: () => new Set() },
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -27,6 +29,8 @@ const emit = defineEmits<{
|
||||
addDeal: [stageId: string];
|
||||
openDeal: [dealId: string];
|
||||
menuDeal: [dealId: string];
|
||||
toggleSelect: [dealId: string];
|
||||
bulkDelete: [];
|
||||
rename: [stageId: string, title: string];
|
||||
delete: [stageId: string];
|
||||
move: [stageId: string, dir: -1 | 1];
|
||||
@@ -220,10 +224,14 @@ onBeforeUnmount(() => {
|
||||
:accent="stage.color"
|
||||
:dragging="draggingId === deal.id"
|
||||
:copy="copy"
|
||||
:select-mode="selectMode"
|
||||
:selected="selectedIds.has(deal.id)"
|
||||
@drag-start="(d, e) => emit('dragStart', d, e)"
|
||||
@drag-end="emit('dragEnd')"
|
||||
@open="emit('openDeal', $event)"
|
||||
@menu="emit('menuDeal', $event)"
|
||||
@toggle-select="emit('toggleSelect', $event)"
|
||||
@bulk-delete="emit('bulkDelete')"
|
||||
/>
|
||||
</div>
|
||||
<p v-if="!stage.deals.length" class="empty">{{ copy.empty }}</p>
|
||||
|
||||
@@ -13,13 +13,17 @@ const props = withDefaults(
|
||||
defineProps<{
|
||||
stages: PipelineStage[];
|
||||
copy?: PipelineUiCopy;
|
||||
selectMode?: boolean;
|
||||
selectedIds?: Set<string>;
|
||||
}>(),
|
||||
{ copy: () => DEAL_BOARD_COPY },
|
||||
{ copy: () => DEAL_BOARD_COPY, selectedIds: () => new Set() },
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
openDeal: [dealId: string];
|
||||
menuDeal: [dealId: string];
|
||||
toggleSelect: [dealId: string];
|
||||
bulkDelete: [];
|
||||
}>();
|
||||
|
||||
const rows = computed(() =>
|
||||
@@ -51,6 +55,7 @@ const tableKey = computed(() => (hideAmount.value ? 'pipeline.leads' : 'pipeline
|
||||
<table v-table-cols="tableKey" class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-if="selectMode" class="tick-col"></th>
|
||||
<th>{{ copy.listColTitle }}</th>
|
||||
<th>مرحله</th>
|
||||
<th v-if="!hideAmount">مبلغ</th>
|
||||
@@ -59,12 +64,34 @@ const tableKey = computed(() => (hideAmount.value ? 'pipeline.leads' : 'pipeline
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in rows" :key="row.deal.id">
|
||||
<tr
|
||||
v-for="row in rows"
|
||||
:key="row.deal.id"
|
||||
:data-card-id="row.deal.id"
|
||||
:class="{ selected: selectedIds.has(row.deal.id) }"
|
||||
@click="selectMode && emit('toggleSelect', row.deal.id)"
|
||||
>
|
||||
<td v-if="selectMode" class="tick-col" @click.stop>
|
||||
<button
|
||||
type="button"
|
||||
class="tick"
|
||||
:class="{ on: selectedIds.has(row.deal.id) }"
|
||||
:aria-label="selectedIds.has(row.deal.id) ? 'لغو انتخاب' : 'انتخاب معامله'"
|
||||
:aria-pressed="selectedIds.has(row.deal.id)"
|
||||
@click="emit('toggleSelect', row.deal.id)"
|
||||
>
|
||||
<UiIcon v-if="selectedIds.has(row.deal.id)" name="check" :size="14" />
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
class="identity"
|
||||
@click="emit('openDeal', row.deal.id)"
|
||||
@click.stop="
|
||||
selectMode
|
||||
? emit('toggleSelect', row.deal.id)
|
||||
: emit('openDeal', row.deal.id)
|
||||
"
|
||||
>
|
||||
<span
|
||||
class="badge"
|
||||
@@ -103,8 +130,18 @@ const tableKey = computed(() => (hideAmount.value ? 'pipeline.leads' : 'pipeline
|
||||
{{ row.deal.date }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="menu-cell">
|
||||
<td class="menu-cell" @click.stop>
|
||||
<button
|
||||
v-if="selectMode && selectedIds.has(row.deal.id)"
|
||||
type="button"
|
||||
class="menu trash"
|
||||
aria-label="حذف انتخابشدهها"
|
||||
@click="emit('bulkDelete')"
|
||||
>
|
||||
<UiIcon name="trash" :size="18" />
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="menu"
|
||||
:aria-label="copy.itemMenuAria"
|
||||
@@ -123,25 +160,62 @@ const tableKey = computed(() => (hideAmount.value ? 'pipeline.leads' : 'pipeline
|
||||
v-for="row in rows"
|
||||
:key="row.deal.id"
|
||||
class="card"
|
||||
@dblclick="emit('openDeal', row.deal.id)"
|
||||
:class="{ selected: selectedIds.has(row.deal.id) }"
|
||||
:data-card-id="row.deal.id"
|
||||
@dblclick="!selectMode && emit('openDeal', row.deal.id)"
|
||||
@click="selectMode && emit('toggleSelect', row.deal.id)"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="identity"
|
||||
@click="emit('openDeal', row.deal.id)"
|
||||
>
|
||||
<span
|
||||
class="badge"
|
||||
:style="{
|
||||
background: PIPELINE_ACCENT[row.accent].soft,
|
||||
color: PIPELINE_ACCENT[row.accent].text,
|
||||
}"
|
||||
>{{ row.deal.initials }}</span>
|
||||
<span class="titles">
|
||||
<strong>{{ row.deal.title }}</strong>
|
||||
<em v-if="row.deal.description">{{ row.deal.description }}</em>
|
||||
</span>
|
||||
</button>
|
||||
<div class="card-top">
|
||||
<button
|
||||
v-if="selectMode"
|
||||
type="button"
|
||||
class="tick"
|
||||
:class="{ on: selectedIds.has(row.deal.id) }"
|
||||
:aria-label="selectedIds.has(row.deal.id) ? 'لغو انتخاب' : 'انتخاب معامله'"
|
||||
@click.stop="emit('toggleSelect', row.deal.id)"
|
||||
>
|
||||
<UiIcon v-if="selectedIds.has(row.deal.id)" name="check" :size="14" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="identity"
|
||||
@click.stop="
|
||||
selectMode
|
||||
? emit('toggleSelect', row.deal.id)
|
||||
: emit('openDeal', row.deal.id)
|
||||
"
|
||||
>
|
||||
<span
|
||||
class="badge"
|
||||
:style="{
|
||||
background: PIPELINE_ACCENT[row.accent].soft,
|
||||
color: PIPELINE_ACCENT[row.accent].text,
|
||||
}"
|
||||
>{{ row.deal.initials }}</span>
|
||||
<span class="titles">
|
||||
<strong>{{ row.deal.title }}</strong>
|
||||
<em v-if="row.deal.description">{{ row.deal.description }}</em>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="selectMode && selectedIds.has(row.deal.id)"
|
||||
type="button"
|
||||
class="menu trash"
|
||||
aria-label="حذف انتخابشدهها"
|
||||
@click.stop="emit('bulkDelete')"
|
||||
>
|
||||
<UiIcon name="trash" :size="18" />
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="menu"
|
||||
:aria-label="copy.itemMenuAria"
|
||||
@click.stop="emit('menuDeal', row.deal.id)"
|
||||
>
|
||||
<UiIcon name="dots-v" :size="18" />
|
||||
</button>
|
||||
</div>
|
||||
<span
|
||||
class="stage"
|
||||
:style="{
|
||||
@@ -216,6 +290,34 @@ const tableKey = computed(() => (hideAmount.value ? 'pipeline.leads' : 'pipeline
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.data-table tbody tr.selected td {
|
||||
background: color-mix(in srgb, var(--accent) 7%, #fff);
|
||||
}
|
||||
|
||||
.tick-col {
|
||||
width: 2.6rem;
|
||||
padding-inline: 0.55rem !important;
|
||||
}
|
||||
|
||||
.tick {
|
||||
width: 1.45rem;
|
||||
height: 1.45rem;
|
||||
border-radius: 6px;
|
||||
border: 2px solid #cbd5e1;
|
||||
background: #fff;
|
||||
color: #fff;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.tick.on {
|
||||
border-color: var(--accent);
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.identity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -329,6 +431,16 @@ const tableKey = computed(() => (hideAmount.value ? 'pipeline.leads' : 'pipeline
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.menu.trash {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.menu.trash:hover,
|
||||
.menu.trash:focus-visible {
|
||||
color: var(--danger);
|
||||
background: color-mix(in srgb, var(--danger) 10%, #fff);
|
||||
}
|
||||
|
||||
.cards {
|
||||
display: none;
|
||||
gap: 0.7rem;
|
||||
@@ -344,6 +456,22 @@ const tableKey = computed(() => (hideAmount.value ? 'pipeline.leads' : 'pipeline
|
||||
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.card.selected {
|
||||
border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
|
||||
background: color-mix(in srgb, var(--accent) 6%, #fff);
|
||||
}
|
||||
|
||||
.card-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.card-top .identity {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card-foot {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
Reference in New Issue
Block a user