feat(frontend): stack TaskBoard columns on mobile

Mobile-first layout for TaskBoard: stack Inbox/In Progress/Review/Done vertically to avoid horizontal scroll, while retaining the horizontal kanban layout on larger screens. Includes component tests and frontend README notes for mobile validation.
This commit is contained in:
Abhimanyu Saharan
2026-02-12 08:00:21 +00:00
parent 855885afaf
commit 3b6e2d98d3
3 changed files with 75 additions and 9 deletions

View File

@@ -0,0 +1,51 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { TaskBoard } from "./TaskBoard";
describe("TaskBoard", () => {
it("uses a mobile-first stacked layout (no horizontal scroll) with responsive kanban columns on larger screens", () => {
render(
<TaskBoard
tasks={[
{
id: "t1",
title: "Inbox item",
status: "inbox",
priority: "medium",
},
]}
/>,
);
const board = screen.getByTestId("task-board");
expect(board.className).toContain("overflow-x-hidden");
expect(board.className).toContain("sm:overflow-x-auto");
expect(board.className).toContain("grid-cols-1");
expect(board.className).toContain("sm:grid-flow-col");
});
it("only sticks column headers on larger screens (avoids weird stacked sticky headers on mobile)", () => {
render(
<TaskBoard
tasks={[
{
id: "t1",
title: "Inbox item",
status: "inbox",
priority: "medium",
},
]}
/>,
);
const header = screen
.getByRole("heading", { name: "Inbox" })
.closest(".column-header");
expect(header?.className).toContain("sm:sticky");
expect(header?.className).toContain("sm:top-0");
// Ensure we didn't accidentally keep unscoped sticky behavior.
expect(header?.className).not.toContain("sticky top-0");
});
});

View File

@@ -334,7 +334,13 @@ export const TaskBoard = memo(function TaskBoard({
return (
<div
ref={boardRef}
className="grid grid-flow-col auto-cols-[minmax(260px,320px)] gap-4 overflow-x-auto pb-6"
data-testid="task-board"
className={cn(
// Mobile-first: stack columns vertically to avoid horizontal scrolling.
"grid grid-cols-1 gap-4 overflow-x-hidden pb-6",
// Desktop/tablet: switch back to horizontally scrollable kanban columns.
"sm:grid-flow-col sm:auto-cols-[minmax(260px,320px)] sm:grid-cols-none sm:overflow-x-auto",
)}
>
{columns.map((column) => {
const columnTasks = grouped[column.status] ?? [];
@@ -385,18 +391,19 @@ export const TaskBoard = memo(function TaskBoard({
<div
key={column.title}
className={cn(
"kanban-column min-h-[calc(100vh-260px)]",
// On mobile, columns are stacked, so avoid forcing tall fixed heights.
"kanban-column min-h-0",
// On larger screens, keep columns tall to reduce empty space during drag.
"sm:min-h-[calc(100vh-260px)]",
activeColumn === column.status &&
!readOnly &&
"ring-2 ring-slate-200",
)}
onDrop={readOnly ? undefined : handleDrop(column.status)}
onDragOver={readOnly ? undefined : handleDragOver(column.status)}
onDragLeave={
readOnly ? undefined : handleDragLeave(column.status)
}
onDragLeave={readOnly ? undefined : handleDragLeave(column.status)}
>
<div className="column-header sticky top-0 z-10 rounded-t-xl border border-b-0 border-slate-200 bg-white/80 px-4 py-3 backdrop-blur">
<div className="column-header z-10 rounded-t-xl border border-b-0 border-slate-200 bg-white px-4 py-3 sm:sticky sm:top-0 sm:bg-white/80 sm:backdrop-blur">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className={cn("h-2 w-2 rounded-full", column.dot)} />
@@ -473,9 +480,7 @@ export const TaskBoard = memo(function TaskBoard({
onClick={() => onTaskSelect?.(task)}
draggable={!readOnly && !task.is_blocked}
isDragging={draggingId === task.id}
onDragStart={
readOnly ? undefined : handleDragStart(task)
}
onDragStart={readOnly ? undefined : handleDragStart(task)}
onDragEnd={readOnly ? undefined : handleDragEnd}
/>
</div>