From 3e557f07295ec049e43e25bb18bb552c88f563d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?= Date: Sat, 12 Sep 2026 20:41:33 +0100 Subject: [PATCH] fix(kanban): compact multiple workflow swimlane heights (#3604) --- apps/web/components/kanban-column.tsx | 34 ++- .../kanban/adaptive-desktop-kanban.tsx | 30 ++- .../components/kanban/swimlane-container.tsx | 7 +- .../kanban/swimlane-kanban-content.tsx | 17 ++ .../kanban/virtualized-column-task-list.tsx | 8 +- .../e2e/tests/kanban/mobile-kanban.spec.ts | 58 +++++ .../tests/kanban/swimlane-height-helpers.ts | 72 ++++++ .../e2e/tests/kanban/swimlane-height.spec.ts | 238 ++++++++++++++++++ .../kanban/use-column-natural-height.test.ts | 57 +++++ .../kanban/use-column-natural-height.ts | 46 ++++ .../use-compact-swimlane-height.test.ts | 58 +++++ .../kanban/use-compact-swimlane-height.ts | 40 +++ .../kanban/use-shared-kanban-layout-props.ts | 6 + apps/web/lib/kanban/view-registry.ts | 1 + docs/plans/kanban-swimlane-height/plan.md | 153 +++++++++++ .../task-01-compact-workflow-heights.md | 187 ++++++++++++++ docs/specs/ui/requirements/adaptive-kanban.md | 20 +- .../specs/ui/system-design/adaptive-kanban.md | 66 ++++- 18 files changed, 1079 insertions(+), 19 deletions(-) create mode 100644 apps/web/e2e/tests/kanban/swimlane-height-helpers.ts create mode 100644 apps/web/e2e/tests/kanban/swimlane-height.spec.ts create mode 100644 apps/web/hooks/domains/kanban/use-column-natural-height.test.ts create mode 100644 apps/web/hooks/domains/kanban/use-column-natural-height.ts create mode 100644 apps/web/hooks/domains/kanban/use-compact-swimlane-height.test.ts create mode 100644 apps/web/hooks/domains/kanban/use-compact-swimlane-height.ts create mode 100644 docs/plans/kanban-swimlane-height/plan.md create mode 100644 docs/plans/kanban-swimlane-height/task-01-compact-workflow-heights.md diff --git a/apps/web/components/kanban-column.tsx b/apps/web/components/kanban-column.tsx index c5007f1a23..a9e3876921 100644 --- a/apps/web/components/kanban-column.tsx +++ b/apps/web/components/kanban-column.tsx @@ -1,6 +1,7 @@ "use client"; -import { memo, useMemo } from "react"; +import { memo, useCallback, useMemo, type Ref } from "react"; +import { useColumnNaturalHeight } from "@/hooks/domains/kanban/use-column-natural-height"; import { useDroppable } from "@dnd-kit/core"; import { Task, type KanbanPresentation } from "./kanban-card"; import { Badge } from "@kandev/ui/badge"; @@ -26,6 +27,7 @@ export interface WorkflowStep { } interface KanbanColumnProps { + onNaturalHeightChange?: (stepId: string, height: number) => void; step: WorkflowStep; tasks: Task[]; presentation?: KanbanPresentation; @@ -48,7 +50,15 @@ interface KanbanColumnProps { externalLinkAvailability: KanbanExternalLinkAvailability; } -function ColumnHeader({ step, tasks }: { step: WorkflowStep; tasks: Task[] }) { +function ColumnHeader({ + step, + tasks, + headerRef, +}: { + step: WorkflowStep; + tasks: Task[]; + headerRef: Ref; +}) { const { t } = useTranslation(); const admittedTaskCount = countAdmittedTasks(tasks); const overWipLimit = isOverWipLimit(admittedTaskCount, step.wip_limit); @@ -56,7 +66,7 @@ function ColumnHeader({ step, tasks }: { step: WorkflowStep; tasks: Task[] }) { const queuedCount = partitionWipTasks(tasks, step.id).queued.length; return ( -
+

{step.title}

@@ -102,6 +112,7 @@ function externalLinkAvailabilityEqual( function columnCallbacksEqual(previous: KanbanColumnProps, next: KanbanColumnProps): boolean { return ( + previous.onNaturalHeightChange === next.onNaturalHeightChange && previous.onPreviewTask === next.onPreviewTask && previous.onOpenTask === next.onOpenTask && previous.onEditTask === next.onEditTask && @@ -137,6 +148,7 @@ function kanbanColumnPropsEqual(previous: KanbanColumnProps, next: KanbanColumnP } export const KanbanColumn = memo(function KanbanColumn({ + onNaturalHeightChange, step, tasks, presentation = "desktop", @@ -160,6 +172,17 @@ export const KanbanColumn = memo(function KanbanColumn({ const { setNodeRef, isOver } = useDroppable({ id: step.id, }); + const { columnRef, headerRef, onContentHeightChange } = useColumnNaturalHeight( + step.id, + onNaturalHeightChange, + ); + const setColumnRef = useCallback( + (element: HTMLDivElement | null) => { + columnRef.current = element; + setNodeRef(element); + }, + [columnRef, setNodeRef], + ); const activeWorkspaceId = useAppStore((state) => state.workspaces.activeId); // Access repositories from store to pass repository names to cards @@ -176,7 +199,7 @@ export const KanbanColumn = memo(function KanbanColumn({ return (
{/* Column Header */} - {!hideHeader && } + {!hideHeader && } ReactNode; @@ -34,6 +35,7 @@ type PanStart = { }; export function AdaptiveDesktopKanban({ + columnHeight, steps, isDragging = false, renderColumn, @@ -81,7 +83,11 @@ export function AdaptiveDesktopKanban({ }; return ( -
+
))}
- {isDragging && ( -
); } +function DragEndReserve() { + return ( +