From 941c4f10d7790cbda56cff1cb956bea5abc85dda Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 18 Feb 2026 21:47:20 +0100 Subject: [PATCH] feat(gantt): render partial-date bars with gradient fade effect --- .../src/components/gantt/GanttRowBars.vue | 104 +++++++++++++++--- 1 file changed, 91 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/gantt/GanttRowBars.vue b/frontend/src/components/gantt/GanttRowBars.vue index 9438e9ec6..bc6434d0e 100644 --- a/frontend/src/components/gantt/GanttRowBars.vue +++ b/frontend/src/components/gantt/GanttRowBars.vue @@ -16,6 +16,42 @@ :timeline-end="dateToDate" :on-update="(id, start, end) => emit('updateTask', id, start, end)" > + + + + + + + + + + - + - + import {computed} from 'vue' import dayjs from 'dayjs' +import {useI18n} from 'vue-i18n' import type {GanttBarModel} from '@/composables/useGanttBar' import {getTextColor, LIGHT} from '@/helpers/color/getTextColor' @@ -110,6 +144,8 @@ import {roundToNaturalDayBoundary} from '@/helpers/time/roundToNaturalDayBoundar import GanttBarPrimitive from './primitives/GanttBarPrimitive.vue' +const {t} = useI18n({useScope: 'global'}) + const props = defineProps<{ bars: GanttBarModel[] totalWidth: number @@ -206,7 +242,23 @@ const getBarTextX = computed(() => (bar: GanttBarModel) => { return getBarX.value(bar) + 8 }) +function isPartialDate(bar: GanttBarModel) { + return bar.meta?.dateType === 'startOnly' || bar.meta?.dateType === 'endOnly' +} + +function isDateless(bar: GanttBarModel) { + return !bar.meta?.hasActualDates && !isPartialDate(bar) +} + function getBarFill(bar: GanttBarModel) { + // Partial dates still have "actual" dates on one side — use the task color + if (isPartialDate(bar)) { + if (bar.meta?.color) { + return bar.meta.color + } + return 'var(--primary)' + } + if (bar.meta?.hasActualDates) { if (bar.meta?.color) { return bar.meta.color @@ -217,22 +269,29 @@ function getBarFill(bar: GanttBarModel) { return 'var(--grey-100)' } +function getBarFillAttr(bar: GanttBarModel): string { + if (isPartialDate(bar)) { + return `url(#gradient-${bar.id})` + } + return getBarFill(bar) +} + function getBarStroke(bar: GanttBarModel) { - if (!bar.meta?.hasActualDates) { + if (isDateless(bar)) { return 'var(--grey-300)' // Gray for dashed border } return 'none' } function getBarStrokeWidth(bar: GanttBarModel) { - if (!bar.meta?.hasActualDates) { + if (isDateless(bar)) { return '2' } return '0' } function getBarTextColor(bar: GanttBarModel) { - if (!bar.meta?.hasActualDates) { + if (isDateless(bar)) { return 'var(--grey-800)' } @@ -243,6 +302,25 @@ function getBarTextColor(bar: GanttBarModel) { return LIGHT } +function getBarAriaLabel(bar: GanttBarModel): string { + const task = bar.meta?.label || bar.id + const startDate = bar.start.toLocaleDateString() + const endDate = bar.end.toLocaleDateString() + + let dateType: string + if (bar.meta?.dateType === 'startOnly') { + dateType = t('project.gantt.partialDatesStart') + } else if (bar.meta?.dateType === 'endOnly') { + dateType = t('project.gantt.partialDatesEnd') + } else if (bar.meta?.hasActualDates) { + dateType = t('project.gantt.scheduledDates') + } else { + dateType = t('project.gantt.estimatedDates') + } + + return t('project.gantt.taskBarLabel', {task, startDate, endDate, dateType}) +} + function handleBarPointerDown(bar: GanttBarModel, event: PointerEvent) { emit('barPointerDown', bar, event) }