mirror of
https://github.com/go-vikunja/vikunja.git
synced 2026-03-11 17:48:44 -05:00
fix(gantt): improve parent task bar styling and visual grouping
- Make parent task bars full height (32px) matching regular bars, instead of the thin 8px summary bar - Move collapse/expand chevron to sit right before the bar start position so it moves with the task during drag/resize - Remove fixed-position indent indicator lines - Add light background band with rounded border to visually group parent tasks with their children, sized to fit the bars
This commit is contained in:
@@ -32,6 +32,18 @@
|
||||
>
|
||||
<template #default="{ focusedRow, focusedCell }">
|
||||
<div class="gantt-rows-container">
|
||||
<!-- Group background bands for parent-child visual grouping -->
|
||||
<div
|
||||
v-for="(band, bandIndex) in parentGroupBands"
|
||||
:key="`band-${bandIndex}`"
|
||||
class="gantt-group-band"
|
||||
:style="{
|
||||
top: `${band.startIndex * ROW_HEIGHT}px`,
|
||||
height: `${(band.endIndex - band.startIndex + 1) * ROW_HEIGHT}px`,
|
||||
left: `${band.left}px`,
|
||||
width: `${band.width}px`,
|
||||
}"
|
||||
/>
|
||||
<div class="gantt-rows">
|
||||
<GanttRow
|
||||
v-for="(rowId, index) in ganttRows"
|
||||
@@ -52,7 +64,6 @@
|
||||
:focused-row="focusedRow ?? null"
|
||||
:focused-cell="focusedCell"
|
||||
:row-id="rowId"
|
||||
:indent-level="ganttBars[index]?.[0]?.meta?.indentLevel ?? 0"
|
||||
:is-parent="ganttBars[index]?.[0]?.meta?.isParent ?? false"
|
||||
:is-collapsed="collapsedTaskIds.has(Number(ganttBars[index]?.[0]?.id))"
|
||||
@barPointerDown="handleBarPointerDown"
|
||||
@@ -393,6 +404,57 @@ const relationArrows = computed<GanttArrow[]>(() => {
|
||||
|
||||
const totalHeight = computed(() => ganttRows.value.length * ROW_HEIGHT)
|
||||
|
||||
// Compute parent-child group bands for visual grouping background
|
||||
const GROUP_BAND_PADDING = 12
|
||||
|
||||
const parentGroupBands = computed(() => {
|
||||
const bands: Array<{ startIndex: number; endIndex: number; left: number; width: number }> = []
|
||||
const bars = ganttBars.value
|
||||
const positions = barPositions.value
|
||||
|
||||
for (let i = 0; i < bars.length; i++) {
|
||||
const bar = bars[i]?.[0]
|
||||
if (!bar?.meta?.isParent) continue
|
||||
|
||||
const parentLevel = bar.meta.indentLevel ?? 0
|
||||
let endIndex = i
|
||||
|
||||
// Find last consecutive child with deeper indent
|
||||
for (let j = i + 1; j < bars.length; j++) {
|
||||
const childBar = bars[j]?.[0]
|
||||
const childLevel = childBar?.meta?.indentLevel ?? 0
|
||||
if (childLevel <= parentLevel) break
|
||||
endIndex = j
|
||||
}
|
||||
|
||||
// Only create a band if there are actual children visible
|
||||
if (endIndex > i) {
|
||||
// Compute horizontal extent from bar positions
|
||||
let minX = Infinity
|
||||
let maxX = -Infinity
|
||||
|
||||
for (let k = i; k <= endIndex; k++) {
|
||||
const taskId = Number(bars[k]?.[0]?.id)
|
||||
const pos = positions.get(taskId)
|
||||
if (!pos) continue
|
||||
minX = Math.min(minX, pos.x)
|
||||
maxX = Math.max(maxX, pos.x + pos.width)
|
||||
}
|
||||
|
||||
if (minX < Infinity) {
|
||||
bands.push({
|
||||
startIndex: i,
|
||||
endIndex,
|
||||
left: minX - GROUP_BAND_PADDING,
|
||||
width: maxX - minX + GROUP_BAND_PADDING * 2,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bands
|
||||
})
|
||||
|
||||
function updateGanttTask(id: string, newStart: Date, newEnd: Date) {
|
||||
const task = tasks.value.get(Number(id))
|
||||
if (!task) return
|
||||
@@ -701,6 +763,15 @@ onUnmounted(() => {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gantt-group-band {
|
||||
position: absolute;
|
||||
background: hsla(var(--primary-h), var(--primary-s), var(--primary-l), 0.06);
|
||||
border: 1px solid hsla(var(--primary-h), var(--primary-s), var(--primary-l), 0.12);
|
||||
border-radius: 6px;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.gantt-rows {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
@@ -8,24 +8,11 @@
|
||||
:aria-label="$t('project.gantt.taskBarsForRow', { rowId })"
|
||||
:data-row-id="rowId"
|
||||
>
|
||||
<!-- Indent indicator: thin vertical lines for nesting depth -->
|
||||
<line
|
||||
v-for="level in indentLevel"
|
||||
:key="`indent-${level}`"
|
||||
:x1="(level - 1) * 16 + 8"
|
||||
:y1="0"
|
||||
:x2="(level - 1) * 16 + 8"
|
||||
:y2="40"
|
||||
stroke="var(--grey-300)"
|
||||
stroke-width="1"
|
||||
class="gantt-indent-line"
|
||||
/>
|
||||
|
||||
<!-- Collapse/expand chevron for parent tasks -->
|
||||
<!-- Collapse/expand chevron for parent tasks — positioned just before the bar -->
|
||||
<g
|
||||
v-if="isParent"
|
||||
v-if="isParent && bars[0]"
|
||||
class="gantt-collapse-toggle"
|
||||
:transform="`translate(${indentLevel * 16 + 2}, 12)`"
|
||||
:transform="`translate(${getBarX(bars[0]) - 18}, 12)`"
|
||||
role="button"
|
||||
:aria-label="isCollapsed
|
||||
? $t('project.gantt.expandGroup', { task: bars[0]?.meta?.label || '' })
|
||||
@@ -117,7 +104,7 @@
|
||||
@pointerdown="handleBarPointerDown(bar, $event)"
|
||||
/>
|
||||
|
||||
<!-- Parent summary bar (thinner with diamond endpoints) -->
|
||||
<!-- Parent summary bar (full height with diamond endpoints) -->
|
||||
<g
|
||||
v-if="bar.meta?.isParent"
|
||||
class="gantt-bar gantt-parent-bar"
|
||||
@@ -126,12 +113,12 @@
|
||||
:aria-pressed="isRowFocused"
|
||||
@pointerdown="handleBarPointerDown(bar, $event)"
|
||||
>
|
||||
<!-- Thin horizontal bar -->
|
||||
<rect
|
||||
:x="getBarX(bar) + 6"
|
||||
:y="16"
|
||||
:width="Math.max(0, getBarWidth(bar) - 12)"
|
||||
:height="8"
|
||||
:x="getBarX(bar)"
|
||||
:y="4"
|
||||
:width="getBarWidth(bar)"
|
||||
:height="32"
|
||||
:rx="4"
|
||||
:fill="getBarFillAttr(bar)"
|
||||
:opacity="bar.meta?.isDone ? 0.5 : 1"
|
||||
:stroke="getBarStroke(bar)"
|
||||
@@ -141,18 +128,14 @@
|
||||
<!-- Left diamond -->
|
||||
<polygon
|
||||
:points="getLeftDiamondPoints(bar)"
|
||||
:fill="getBarFillAttr(bar)"
|
||||
:fill="getParentDiamondFill(bar)"
|
||||
:opacity="bar.meta?.isDone ? 0.5 : 1"
|
||||
:stroke="getBarStroke(bar)"
|
||||
:stroke-width="getBarStrokeWidth(bar)"
|
||||
/>
|
||||
<!-- Right diamond -->
|
||||
<polygon
|
||||
:points="getRightDiamondPoints(bar)"
|
||||
:fill="getBarFillAttr(bar)"
|
||||
:fill="getParentDiamondFill(bar)"
|
||||
:opacity="bar.meta?.isDone ? 0.5 : 1"
|
||||
:stroke="getBarStroke(bar)"
|
||||
:stroke-width="getBarStrokeWidth(bar)"
|
||||
/>
|
||||
</g>
|
||||
|
||||
@@ -249,7 +232,6 @@ const props = defineProps<{
|
||||
focusedRow: string | null
|
||||
focusedCell: number | null
|
||||
rowId: string
|
||||
indentLevel: number
|
||||
isParent: boolean
|
||||
isCollapsed: boolean
|
||||
}>()
|
||||
@@ -334,17 +316,15 @@ const getBarTextX = computed(() => (bar: GanttBarModel) => {
|
||||
}
|
||||
// When the bar starts before the visible range, clamp text to the left edge
|
||||
// so the title remains visible within the visible portion of the bar.
|
||||
// For parent bars, offset by the chevron width
|
||||
const baseOffset = bar.meta?.isParent ? 24 : 8
|
||||
return Math.max(getBarX.value(bar) + baseOffset, 8)
|
||||
return Math.max(getBarX.value(bar) + 8, 8)
|
||||
})
|
||||
|
||||
// Diamond endpoint helpers for parent summary bars
|
||||
const DIAMOND_SIZE = 6
|
||||
const DIAMOND_SIZE = 5
|
||||
|
||||
function getLeftDiamondPoints(bar: GanttBarModel): string {
|
||||
const x = getBarX.value(bar)
|
||||
const cy = 20 // vertical center of the thin bar
|
||||
const cy = 20 // vertical center of the bar
|
||||
return `${x},${cy} ${x + DIAMOND_SIZE},${cy - DIAMOND_SIZE} ${x + DIAMOND_SIZE * 2},${cy} ${x + DIAMOND_SIZE},${cy + DIAMOND_SIZE}`
|
||||
}
|
||||
|
||||
@@ -354,6 +334,14 @@ function getRightDiamondPoints(bar: GanttBarModel): string {
|
||||
return `${x - DIAMOND_SIZE * 2},${cy} ${x - DIAMOND_SIZE},${cy - DIAMOND_SIZE} ${x},${cy} ${x - DIAMOND_SIZE},${cy + DIAMOND_SIZE}`
|
||||
}
|
||||
|
||||
function getParentDiamondFill(bar: GanttBarModel): string {
|
||||
// Use a darker shade for contrast on the full-height bar
|
||||
if (bar.meta?.color) {
|
||||
return 'var(--white)'
|
||||
}
|
||||
return 'var(--white)'
|
||||
}
|
||||
|
||||
function isPartialDate(bar: GanttBarModel) {
|
||||
return bar.meta?.dateType === 'startOnly' || bar.meta?.dateType === 'endOnly'
|
||||
}
|
||||
@@ -469,11 +457,6 @@ function startResize(bar: GanttBarModel, edge: 'start' | 'end', event: PointerEv
|
||||
}
|
||||
}
|
||||
|
||||
.gantt-indent-line {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.gantt-collapse-toggle {
|
||||
pointer-events: all;
|
||||
cursor: pointer;
|
||||
|
||||
Reference in New Issue
Block a user