fix(web): make kanban column titles readable

Widen board columns and keep counts inside the add button so stage and card titles have room.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-28 17:24:58 +03:30
co-authored by Cursor
parent 463a1652a6
commit 2cc2d358e4
4 changed files with 42 additions and 51 deletions
@@ -8,8 +8,9 @@ withDefaults(
defineProps<{ defineProps<{
accent: PipelineAccent; accent: PipelineAccent;
label?: string; label?: string;
count?: string;
}>(), }>(),
{ label: 'افزودن معامله' }, { label: 'افزودن معامله', count: '' },
); );
const emit = defineEmits<{ const emit = defineEmits<{
@@ -21,11 +22,18 @@ const emit = defineEmits<{
<a-button <a-button
type="dashed" type="dashed"
block block
:style="{ color: PIPELINE_ACCENT[accent].text }" :style="{
color: PIPELINE_ACCENT[accent].text,
display: 'flex',
justifyContent: 'space-between',
}"
:aria-label="label" :aria-label="label"
@click="emit('add')" @click="emit('add')"
> >
<template #icon><PlusOutlined /></template> <span :style="{ display: 'inline-flex', alignItems: 'center', gap: '8px' }">
{{ label }} <PlusOutlined />
{{ label }}
</span>
<span v-if="count">{{ count }}</span>
</a-button> </a-button>
</template> </template>
+9 -21
View File
@@ -18,11 +18,7 @@ import {
} from 'ant-design-vue'; } from 'ant-design-vue';
import { userAvatarSrc } from '@/lib/userAvatars'; import { userAvatarSrc } from '@/lib/userAvatars';
import type { PipelineAccent, PipelineDeal, PipelineUiCopy } from '@/lib/pipeline-board'; import type { PipelineAccent, PipelineDeal, PipelineUiCopy } from '@/lib/pipeline-board';
import { import { DEAL_BOARD_COPY, formatCurrencyParts } from '@/lib/pipeline-board';
DEAL_BOARD_COPY,
formatCurrencyParts,
PIPELINE_ACCENT,
} from '@/lib/pipeline-board';
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@@ -54,7 +50,6 @@ const showAmount = computed(
const money = computed(() => const money = computed(() =>
formatCurrencyParts(props.deal.amount ?? 0, props.deal.currencyUnit ?? 'ریال'), formatCurrencyParts(props.deal.amount ?? 0, props.deal.currencyUnit ?? 'ریال'),
); );
const colors = computed(() => PIPELINE_ACCENT[props.accent]);
const assigneeTitle = computed(() => const assigneeTitle = computed(() =>
(props.deal.assigneeNames ?? []).filter(Boolean).join('، ') || 'مسئول', (props.deal.assigneeNames ?? []).filter(Boolean).join('، ') || 'مسئول',
); );
@@ -113,24 +108,17 @@ const cardStyle = computed(() => ({
<CheckOutlined v-if="selected" /> <CheckOutlined v-if="selected" />
</template> </template>
</a-button> </a-button>
<a-avatar
v-else
:style="{
background: colors.soft,
color: colors.text,
flexShrink: 0,
fontWeight: 700,
fontSize: `${token.fontSizeSM}px`,
}"
>
{{ deal.initials }}
</a-avatar>
<div :style="{ flex: 1, minWidth: 0 }"> <div :style="{ flex: 1, minWidth: 0 }">
<a-typography-text <a-typography-text
strong
ellipsis ellipsis
:style="{ display: 'block', margin: 0, lineHeight: 1.35, cursor: 'pointer' }" :style="{
display: 'block',
margin: 0,
lineHeight: 1.35,
cursor: 'pointer',
fontSize: `${token.fontSizeSM}px`,
fontWeight: 600,
}"
:title="deal.title" :title="deal.title"
@click.stop="selectMode ? emit('toggleSelect', deal.id) : emit('open', deal.id)" @click.stop="selectMode ? emit('toggleSelect', deal.id) : emit('open', deal.id)"
> >
@@ -131,8 +131,8 @@ function submitAdd() {
<div <div
v-if="canManage" v-if="canManage"
:style="{ :style="{
flex: '0 0 280px', flex: '0 0 310px',
width: '280px', width: '310px',
alignSelf: 'stretch', alignSelf: 'stretch',
display: 'flex', display: 'flex',
alignItems: 'flex-start', alignItems: 'flex-start',
@@ -1,10 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, ref } from 'vue'; import { computed, nextTick, ref } from 'vue';
import { import { HolderOutlined, MoreOutlined } from '@ant-design/icons-vue';
HolderOutlined,
MoreOutlined,
PlusOutlined,
} from '@ant-design/icons-vue';
import { import {
Button as AButton, Button as AButton,
Dropdown as ADropdown, Dropdown as ADropdown,
@@ -17,7 +13,7 @@ import {
import AddDealButton from './AddDealButton.vue'; import AddDealButton from './AddDealButton.vue';
import DealCard from './DealCard.vue'; import DealCard from './DealCard.vue';
import type { PipelineDeal, PipelineStage, PipelineUiCopy } from '@/lib/pipeline-board'; import type { PipelineDeal, PipelineStage, PipelineUiCopy } from '@/lib/pipeline-board';
import { DEAL_BOARD_COPY, PIPELINE_ACCENT, toFaItemCount } from '@/lib/pipeline-board'; import { DEAL_BOARD_COPY, PIPELINE_ACCENT } from '@/lib/pipeline-board';
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@@ -67,8 +63,8 @@ const columnStyle = computed(() => ({
display: 'flex', display: 'flex',
flexDirection: 'column' as const, flexDirection: 'column' as const,
gap: '12px', gap: '12px',
flex: '0 0 280px', flex: '0 0 310px',
width: '280px', width: '310px',
alignSelf: 'stretch' as const, alignSelf: 'stretch' as const,
minHeight: 0, minHeight: 0,
padding: 0, padding: 0,
@@ -154,15 +150,21 @@ function onColumnDragStart(event: DragEvent) {
} }
const menuItems = computed(() => [ const menuItems = computed(() => [
{ key: 'add', label: 'افزودن' },
{ key: 'rename', label: 'تغییر نام' }, { key: 'rename', label: 'تغییر نام' },
{ key: 'right', label: 'انتقال به راست', disabled: props.isFirst }, { key: 'right', label: 'انتقال به راست', disabled: props.isFirst },
{ key: 'left', label: 'انتقال به چپ', disabled: props.isLast }, { key: 'left', label: 'انتقال به چپ', disabled: props.isLast },
{ key: 'delete', label: 'حذف ستون', danger: true }, { key: 'delete', label: 'حذف ستون', danger: true },
]); ]);
const itemCountLabel = computed(() =>
new Intl.NumberFormat('fa-IR').format(props.stage.total ?? props.stage.deals.length),
);
function onMenuClick({ key }: { key: string | number }) { function onMenuClick({ key }: { key: string | number }) {
menuOpen.value = false; menuOpen.value = false;
if (key === 'rename') startRename(); if (key === 'add') emit('addDeal', props.stage.id);
else if (key === 'rename') startRename();
else if (key === 'right') emit('move', props.stage.id, -1); else if (key === 'right') emit('move', props.stage.id, -1);
else if (key === 'left') emit('move', props.stage.id, 1); else if (key === 'left') emit('move', props.stage.id, 1);
else if (key === 'delete') emit('delete', props.stage.id); else if (key === 'delete') emit('delete', props.stage.id);
@@ -224,12 +226,13 @@ function onMenuClick({ key }: { key: string | number }) {
/> />
<a-typography-text <a-typography-text
v-else v-else
strong
ellipsis ellipsis
:style="{ :style="{
flex: 1, flex: 1,
minWidth: 0, minWidth: 0,
margin: 0, margin: 0,
fontSize: `${token.fontSizeSM}px`,
fontWeight: 600,
cursor: canManage ? 'text' : 'default', cursor: canManage ? 'text' : 'default',
}" }"
:title="canManage ? 'برای تغییر نام کلیک کنید' : stage.title" :title="canManage ? 'برای تغییر نام کلیک کنید' : stage.title"
@@ -242,10 +245,6 @@ function onMenuClick({ key }: { key: string | number }) {
{{ stage.badge }} {{ stage.badge }}
</a-tag> </a-tag>
<a-typography-text type="secondary" :style="{ whiteSpace: 'nowrap', flexShrink: 0 }">
{{ toFaItemCount(stage.total ?? stage.deals.length, copy.itemNoun) }}
</a-typography-text>
<a-dropdown <a-dropdown
v-if="canManage" v-if="canManage"
v-model:open="menuOpen" v-model:open="menuOpen"
@@ -259,15 +258,6 @@ function onMenuClick({ key }: { key: string | number }) {
<a-menu :items="menuItems" @click="onMenuClick" /> <a-menu :items="menuItems" @click="onMenuClick" />
</template> </template>
</a-dropdown> </a-dropdown>
<a-button
type="text"
:aria-label="copy.addItemAria"
:style="{ color: accent.solid }"
@click="emit('addDeal', stage.id)"
>
<template #icon><PlusOutlined /></template>
</a-button>
</div> </div>
<div role="list" :aria-label="stage.title" :style="bodyStyle"> <div role="list" :aria-label="stage.title" :style="bodyStyle">
@@ -311,7 +301,12 @@ function onMenuClick({ key }: { key: string | number }) {
</div> </div>
<div :style="{ padding: '0 12px 12px', flexShrink: 0 }"> <div :style="{ padding: '0 12px 12px', flexShrink: 0 }">
<AddDealButton :accent="stage.color" :label="copy.addItem" @add="emit('addDeal', stage.id)" /> <AddDealButton
:accent="stage.color"
:label="copy.addItem"
:count="itemCountLabel"
@add="emit('addDeal', stage.id)"
/>
</div> </div>
</section> </section>
</template> </template>