Repository: operately/operately
Pull request #2387: feat: Bootstrap Task Board prototype
Author: login: markoa
Created: 2025-05-06T12:01:09Z | Merged: 2025-05-06T17:05:56Z

First time creating turboui components, looking for general feedback and any tips for maximizing the value of these components in later app integration before proceeding further.


Diff:
diff --git a/turboui/src/StatusBadge/index.tsx b/turboui/src/StatusBadge/index.tsx
index f00e2622bd..b7e3ecd764 100644
--- a/turboui/src/StatusBadge/index.tsx
+++ b/turboui/src/StatusBadge/index.tsx
@@ -1,7 +1,7 @@
 import React from "react";
 import { StatusBadgeProps } from "./types";
 
-export function StatusBadge({ status, hideIcon = false, className = "", style }: StatusBadgeProps): React.ReactElement {
+export function StatusBadge({ status, hideIcon = false, className = "", style, customLabel }: StatusBadgeProps): React.ReactElement {
   let bgColor: string,
     textColor: string,
     dotColor: string,
@@ -186,7 +186,7 @@ export function StatusBadge({ status, hideIcon = false, className = "", style }:
       style={style}
     >
       {!hideIcon && getStatusIcon()}
-      {label}
+      {customLabel || label}
     </span>
   );
 }
diff --git a/turboui/src/StatusBadge/types.ts b/turboui/src/StatusBadge/types.ts
index fdaa14bb29..a29d518bca 100644
--- a/turboui/src/StatusBadge/types.ts
+++ b/turboui/src/StatusBadge/types.ts
@@ -26,4 +26,5 @@ export interface StatusBadgeProps {
   hideIcon?: boolean; // Option to hide the status icon
   className?: string;
   style?: React.CSSProperties;
+  customLabel?: string; // Optional custom label to override default
 }
diff --git a/turboui/src/TaskBoard/components/index.tsx b/turboui/src/TaskBoard/components/index.tsx
new file mode 100644
index 0000000000..b0936d6550
--- /dev/null
+++ b/turboui/src/TaskBoard/components/index.tsx
@@ -0,0 +1,284 @@
+import React, { useState } from "react";
+import { StatusBadge } from "../../StatusBadge";
+import { SecondaryButton } from "../../Button";
+import { BlackLink } from "../../Link";
+import { AvatarWithName } from "../../Avatar/AvatarWithName";
+import { IconFileText, IconMessageCircle, IconClock } from "@tabler/icons-react";
+
+export namespace TaskBoard {
+  export type Status = "pending" | "in_progress" | "done";
+
+  export interface Person {
+    id: string;
+    fullName: string;
+    avatarUrl?: string;
+  }
+
+  export interface Milestone {
+    id: string;
+    name: string;
+    dueDate?: Date;
+  }
+
+  export interface Label {
+    id: string;
+    name: string;
+    color: string;
+  }
+
+  export interface Task {
+    id: string;
+    title: string;
+    status: Status;
+    description?: string;
+    assignees?: Person[];
+    labels?: Label[];
+    milestone?: Milestone;
+    points?: number;
+    dueDate?: Date;
+    hasComments?: boolean;
+    hasDescription?: boolean;
+    commentCount?: number;
+  }
+
+  export type TaskViewMode = "table" | "kanban" | "timeline";
+
+  export interface Props {
+    title: string;
+    tasks: Task[];
+    viewMode?: TaskViewMode;
+  }
+}
+
+// Helper to group tasks by status
+const groupTasksByStatus = (tasks: TaskBoard.Task[]) => {
+  const grouped: Record<string, TaskBoard.Task[]> = {
+    pending: [],
+    in_progress: [],
+    done: [],
+  };
+
+  tasks.forEach((task) => {
+    if (grouped[task.status]) {
+      grouped[task.status].push(task);
+    } else {
+      grouped.pending.push(task);
+    }
+  });
+
+  return grouped;
+};
+
+// Map task status to badge status and labels
+const taskStatusConfig: Record<TaskBoard.Status, { status: string; label: string }> = {
+  pending: { status: "paused", label: "Not started" },
+  in_progress: { status: "pending", label: "In progress" },
+  done: { status: "completed", label: "Done" },
+};
+
+// Helper to get the display name for a status
+const getStatusDisplayName = (status: TaskBoard.Status): string => {
+  switch (status) {
+    case "pending":
+      return "Not started";
+    case "in_progress":
+      return "In progress";
+    case "done":
+      return "Done";
+    default:
+      return status;
+  }
+};
+
+// Helper component to display due date with appropriate formatting
+function DueDateDisplay({ dueDate }: { dueDate: Date }) {
+  // Check if the due date is in the past
+  const isOverdue = dueDate < new Date();
+
+  // Format the date to a readable format
+  const formatDate = (date: Date) => {
+    return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
+  };
+
+  return (
+    <span
+      className={`text-sm flex items-center ${isOverdue ? "text-red-600 font-medium" : "text-content-base"}`}
+      title={isOverdue ? "Overdue" : ""}
+    >
+      {isOverdue && (
+        <IconClock 
+          size={14} 
+          className="mr-1 text-red-500" 
+        />
+      )}
+      {formatDate(dueDate)}
+    </span>
+  );
+}
+
+export function TaskBoard({ title, tasks, viewMode = "table" }: TaskBoard.Props) {
+  const [currentViewMode, setCurrentViewMode] = useState<TaskBoard.TaskViewMode>(viewMode);
+
+  return (
+    <div className="flex flex-col w-full h-full bg-surface-base rounded-lg">
+      <header className="flex flex-col sm:flex-row sm:items-center sm:justify-between px-4 sm:px-6 py-2 border-b border-surface-outline">
+        <div className="flex flex-row items-center gap-4">
+          <h1 className="text-sm sm:text-base font-bold text-content-accent">{title}</h1>
+          <SecondaryButton size="xs">+ Add Task</SecondaryButton>
+        </div>
+        <div className="flex mt-2 sm:mt-0">
+          <div className="flex space-x-2">
+            <button
+              onClick={() => setCurrentViewMode("table")}
+              className={`px-3 py-1 text-xs rounded-md ${
+                currentViewMode === "table"
+                  ? "bg-primary-base text-white"
+                  : "bg-surface-accent text-content-base hover:bg-surface-accent-hover"
+              }`}
+            >
+              Table
+            </button>
+            <button
+              onClick={() => setCurrentViewMode("kanban")}
+              className={`px-3 py-1 text-xs rounded-md ${
+                currentViewMode === "kanban"
+                  ? "bg-primary-base text-white"
+                  : "bg-surface-accent text-content-base hover:bg-surface-accent-hover"
+              }`}
+            >
+              Kanban
+            </button>
+            <button
+              onClick={() => setCurrentViewMode("timeline")}
+              className={`px-3 py-1 text-xs rounded-md ${
+                currentViewMode === "timeline"
+                  ? "bg-primary-base text-white"
+                  : "bg-surface-accent text-content-base hover:bg-surface-accent-hover"
+              }`}
+            >
+              Timeline
+            </button>
+          </div>
+        </div>
+      </header>
+      <div className="flex-1 overflow-auto">
+        {currentViewMode === "table" && (
+          <div className="overflow-x-auto bg-surface-base">
+            <table className="min-w-full">
+              <thead>
+                <tr className="border-b border-surface-outline bg-surface-dimmed text-content-base text-xs sm:text-sm sticky top-0">
+                  <th className="text-left py-1.5 px-4 font-semibold">Task</th>
+                  <th className="text-left py-1.5 px-4 font-semibold">Status</th>
+                  <th className="text-left py-1.5 px-4 font-semibold">Assignee</th>
+                  <th className="text-left py-1.5 px-4 font-semibold">Due</th>
+                  <th className="text-left py-1.5 px-4 font-semibold">Milestone</th>
+                  <th className="text-left py-1.5 px-4 font-semibold">Labels</th>
+                </tr>
+              </thead>
+              <tbody>
+                {tasks.map((task) => (
+                  <tr
+                    key={task.id}
+                    className="group border-b border-stroke-base bg-surface-base hover:bg-surface-highlight"
+                  >
+                    <td className="py-1 px-4">
+                      <div className="flex items-center">
+                        <BlackLink
+                          to={`/tasks/${task.id}`}
+                          className="text-sm text-content-base hover:text-link-hover transition-colors"
+                          underline="hover"
+                        >
+                          {task.title}
+                        </BlackLink>
+                        {task.hasDescription && (
+                          <span className="ml-2 text-content-subtle">
+                            <IconFileText size={16} />
+                          </span>
+                        )}
+                        {task.hasComments && (
+                          <span className="ml-2 text-content-subtle flex items-center">
+                            <IconMessageCircle size={16} />
+                            {task.commentCount && (
+                              <span className="ml-1 text-xs text-content-subtle">{task.commentCount}</span>
+                            )}
+                          </span>
+                        )}
+                      </div>
+                    </td>
+                    <td className="py-1 px-4">
+                      <StatusBadge
+                        status={taskStatusConfig[task.status].status}
+                        customLabel={taskStatusConfig[task.status].label}
+                      />
+                    </td>
+                    <td className="py-1 px-4">
+                      {task.assignees && task.assignees.length > 0 ? (
+                        <AvatarWithName person={task.assignees[0]} size="tiny" nameFormat="short" className="text-sm" />
+                      ) : (
+                        <span className="text-sm text-content-subtle">—</span>
+                      )}
+                    </td>
+                    <td className="py-1 px-4">
+                      {task.dueDate ? (
+                        <DueDateDisplay dueDate={task.dueDate} />
+                      ) : (
+                        <span className="text-sm text-content-subtle">—</span>
+                      )}
+                    </td>
+                    <td className="py-1 px-4">
+                      {task.milestone ? (
+                        <span className="text-sm text-content-base">{task.milestone.name}</span>
+                      ) : (
+                        <span className="text-sm text-content-subtle">—</span>
+                      )}
+                    </td>
+                    <td className="py-1 px-4">
+                      {task.labels && task.labels.length > 0 ? (
+                        <div className="flex flex-wrap gap-1">
+                          {task.labels.map((label) => (
+                            <span
+                              key={label.id}
+                              className="px-2 py-0.5 text-xs rounded-full"
+                              style={{ backgroundColor: `${label.color}20`, color: label.color }}
+                            >
+                              {label.name}
+                            </span>
+                          ))}
+                        </div>
+                      ) : (
+                        <span className="text-sm text-content-subtle">—</span>
+                      )}
+                    </td>
+                  </tr>
+                ))}
+                {tasks.length === 0 && (
+                  <tr>
+                    <td colSpan={6} className="py-4 text-center text-content-subtle">
+                      No tasks available
+                    </td>
+                  </tr>
+                )}
+              </tbody>
+            </table>
+          </div>
+        )}
+        {currentViewMode === "kanban" && (
+          <div className="flex h-full text-center text-content-subtle">
+            <div className="m-auto p-6 border border-dashed border-surface-outline rounded-lg">
+              Kanban view will be implemented here
+            </div>
+          </div>
+        )}
+        {currentViewMode === "timeline" && (
+          <div className="flex h-full text-center text-content-subtle">
+            <div className="m-auto p-6 border border-dashed border-surface-outline rounded-lg">
+              Timeline view will be implemented here
+            </div>
+          </div>
+        )}
+      </div>
+    </div>
+  );
+}
+
+export default TaskBoard;
diff --git a/turboui/src/TaskBoard/index.tsx b/turboui/src/TaskBoard/index.tsx
new file mode 100644
index 0000000000..40b494c5f8
--- /dev/null
+++ b/turboui/src/TaskBoard/index.tsx
@@ -0,0 +1 @@
+export * from "./components";
diff --git a/turboui/src/TaskBoard/stories/TaskBoard.stories.tsx b/turboui/src/TaskBoard/stories/TaskBoard.stories.tsx
new file mode 100644
index 0000000000..d2cad23385
--- /dev/null
+++ b/turboui/src/TaskBoard/stories/TaskBoard.stories.tsx
@@ -0,0 +1,93 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import { TaskBoard } from "../components";
+import { Page } from "../../Page";
+import { mockTasks, mockEmptyTasks } from "../tests/mockData";
+
+/**
+ * TaskBoard is a comprehensive task management component designed for teams.
+ */
+const meta = {
+  title: "Components/TaskBoard",
+  component: TaskBoard,
+  parameters: {
+    layout: "fullscreen",
+  },
+  argTypes: {
+    tasks: { control: "object" },
+    viewMode: { control: "select", options: ["table", "kanban", "timeline"] },
+  },
+  decorators: [
+    (Story, context) => (
+      <div className="h-[800px] py-[4.5rem] px-2">
+        <Page title={context.args.title} size="fullwidth">
+          <Story />
+        </Page>
+      </div>
+    ),
+  ],
+} satisfies Meta<typeof TaskBoard>;
+
+export default meta;
+type Story = StoryObj<typeof meta>;
+
+/**
+ * Default table view of the TaskBoard with multiple tasks in different statuses
+ */
+export const Default: Story = {
+  tags: ["autodocs"],
+  args: {
+    title: "Task Board",
+    tasks: mockTasks,
+    viewMode: "table",
+  },
+};
+
+/**
+ * Empty TaskBoard with no tasks
+ */
+export const EmptyState: Story = {
+  tags: ["autodocs"],
+  args: {
+    title: "Project Tasks",
+    tasks: mockEmptyTasks,
+    viewMode: "table",
+  },
+};
+
+/**
+ * Kanban view of the TaskBoard
+ */
+export const KanbanView: Story = {
+  tags: ["autodocs"],
+  render: (args) => (
+    <div className="h-[800px] py-4">
+      <Page title={args.title} size="fullwidth">
+        <TaskBoard {...args} />
+      </Page>
+    </div>
+  ),
+  args: {
+    title: "Project Tasks",
+    tasks: mockTasks,
+    viewMode: "kanban",
+  },
+};
+
+/**
+ * Timeline view of the TaskBoard
+ */
+export const TimelineView: Story = {
+  tags: ["autodocs"],
+  render: (args) => (
+    <div className="h-[800px] py-4">
+      <Page title={args.title} size="fullwidth">
+        <TaskBoard {...args} />
+      </Page>
+    </div>
+  ),
+  args: {
+    title: "Project Tasks",
+    tasks: mockTasks,
+    viewMode: "timeline",
+  },
+};
diff --git a/turboui/src/TaskBoard/tests/mockData.ts b/turboui/src/TaskBoard/tests/mockData.ts
new file mode 100644
index 0000000000..0ea99800a9
--- /dev/null
+++ b/turboui/src/TaskBoard/tests/mockData.ts
@@ -0,0 +1,146 @@
+import { TaskBoard } from "../components";
+import { genPeople } from "../../utils/storybook/genPeople";
+
+// Mock labels
+export const mockLabels = {
+  highPriority: { id: "1", name: "High Priority", color: "#EF4444" },
+  mediumPriority: { id: "2", name: "Medium Priority", color: "#F59E0B" },
+  lowPriority: { id: "3", name: "Low Priority", color: "#10B981" },
+  bug: { id: "4", name: "Bug", color: "#8B5CF6" },
+  feature: { id: "5", name: "Feature", color: "#3B82F6" },
+  documentation: { id: "6", name: "Documentation", color: "#6366F1" },
+};
+
+// Generate a consistent set of people for our mock data
+const generatedPeople = genPeople(4);
+
+// Mock people - mapped into an object for easy reference with proper TaskBoard.Person typing
+export const mockPeople: Record<string, TaskBoard.Person> = {
+  john: {
+    id: generatedPeople[0].id,
+    fullName: generatedPeople[0].fullName,
+    avatarUrl: generatedPeople[0].avatarUrl
+  },
+  jane: {
+    id: generatedPeople[1].id,
+    fullName: generatedPeople[1].fullName,
+    avatarUrl: generatedPeople[1].avatarUrl
+  },
+  bob: {
+    id: generatedPeople[2].id,
+    fullName: generatedPeople[2].fullName,
+    avatarUrl: generatedPeople[2].avatarUrl
+  },
+  alice: {
+    id: generatedPeople[3].id,
+    fullName: generatedPeople[3].fullName,
+    avatarUrl: generatedPeople[3].avatarUrl
+  },
+};
+
+// Mock milestones
+export const mockMilestones = {
+  q2Release: { id: "1", name: "Q2 Release", dueDate: new Date("2025-06-30") },
+  productLaunch: { id: "2", name: "Product Launch", dueDate: new Date("2025-08-15") },
+  marketExpansion: { id: "3", name: "Market Expansion", dueDate: new Date("2025-09-30") },
+};
+
+// Mock tasks
+export const mockTasks: TaskBoard.Task[] = [
+  {
+    id: "task-1",
+    title: "Implement user authentication",
+    status: "pending",
+    description: "Create login/signup flows with OAuth integration",
+    assignees: [mockPeople.john, mockPeople.jane],
+    labels: [mockLabels.feature, mockLabels.highPriority],
+    milestone: mockMilestones.q2Release,
+    dueDate: new Date("2025-05-10"), // Future date
+    points: 8,
+    hasDescription: true,
+    hasComments: true,
+    commentCount: 3,
+  },
+  {
+    id: "task-2",
+    title: "Fix navigation bug on mobile",
+    status: "in_progress",
+    assignees: [mockPeople.bob],
+    labels: [mockLabels.bug, mockLabels.highPriority],
+    milestone: mockMilestones.q2Release,
+    dueDate: new Date("2025-05-01"), // Past due
+    points: 3,
+    hasDescription: true,
+    hasComments: false,
+  },
+  {
+    id: "task-3",
+    title: "Update documentation for API v2",
+    status: "in_progress",
+    assignees: [mockPeople.alice],
+    labels: [mockLabels.documentation, mockLabels.mediumPriority],
+    milestone: mockMilestones.q2Release,
+    points: 5,
+    hasDescription: true,
+    hasComments: true,
+  },
+  {
+    id: "task-4",
+    title: "Design marketing landing page",
+    status: "done",
+    assignees: [mockPeople.jane],
+    labels: [mockLabels.feature, mockLabels.mediumPriority],
+    milestone: mockMilestones.productLaunch,
+    points: 5,
+    hasDescription: false,
+    hasComments: true,
+    commentCount: 2,
+  },
+  {
+    id: "task-5",
+    title: "Research competitor pricing strategies",
+    status: "pending",
+    labels: [mockLabels.lowPriority],
+    milestone: mockMilestones.marketExpansion,
+    dueDate: new Date("2025-06-15"), // Future date
+    points: 3,
+    hasDescription: false,
+    hasComments: false,
+  },
+  {
+    id: "task-6",
+    title: "Implement dark mode toggle",
+    status: "pending",
+    assignees: [mockPeople.bob, mockPeople.alice],
+    labels: [mockLabels.feature, mockLabels.lowPriority],
+    points: 3,
+    hasDescription: true,
+    hasComments: false,
+  },
+  {
+    id: "task-7",
+    title: "Optimize database queries",
+    status: "in_progress",
+    assignees: [mockPeople.john],
+    labels: [mockLabels.bug, mockLabels.highPriority],
+    dueDate: new Date("2025-04-30"), // Past due
+    points: 5,
+    hasDescription: true,
+    hasComments: true,
+    commentCount: 5,
+  },
+  {
+    id: "task-8",
+    title: "Set up CI/CD pipeline",
+    status: "done",
+    assignees: [mockPeople.bob],
+    labels: [mockLabels.feature, mockLabels.highPriority],
+    milestone: mockMilestones.q2Release,
+    points: 8,
+    hasDescription: true,
+    hasComments: true,
+  },
+];
+
+// Mock empty tasks array
+export const mockEmptyTasks: TaskBoard.Task[] = [];


Reviews:
user: login: sourcery-ai[bot]
body: Hey @markoa - I've reviewed your changes - here's some feedback:

- Expose an `onAddTask` callback prop from `TaskBoard` to enable applications to define task creation logic.
- Consider an `onTaskSelect` callback for task items to provide more flexible handling of task interactions than hardcoded navigation.
- As `TaskBoard` functionality expands, consider separating view-specific rendering (Table, Kanban, Timeline) into dedicated sub-components.

<details>
<summary>Here's what I looked at during the review</summary>

- 🟡 **General issues**: 2 issues found
- 🟢 **Security**: all looks good
- 🟢 **Review instructions**: all looks good
- 🟡 **Testing**: 1 issue found
- 🟡 **Complexity**: 1 issue found
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T12:04:24Z
user: login: shiroyasha
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T12:10:47Z
user: login: shiroyasha
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T12:11:07Z
user: login: shiroyasha
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T12:14:23Z
user: login: shiroyasha
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T12:14:43Z
user: login: shiroyasha
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T12:16:12Z
user: login: shiroyasha
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T12:17:15Z
user: login: markoa
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T14:47:49Z

Review comments:
diff_hunk: @@ -26,4 +26,5 @@ export interface StatusBadgeProps {
   hideIcon?: boolean; // Option to hide the status icon
   className?: string;
   style?: React.CSSProperties;
+  customLabel?: string; // Optional custom label to override default
path: turboui/src/StatusBadge/types.ts
user: login: sourcery-ai[bot]
body: **suggestion:** New customLabel prop addition is clear.

Document how falsy customLabel values (e.g., '') fall back to the default label.

```suggestion
  customLabel?: string; // Optional custom label to override default. If falsy (e.g., ''), it falls back to the default label.
```
created_at: 2025-05-06T12:04:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,321 @@
+import React, { useState } from "react";
+import { StatusBadge } from "../../StatusBadge";
+import { SecondaryButton } from "../../Button";
+import { BlackLink } from "../../Link";
+import { AvatarWithName } from "../../Avatar/AvatarWithName";
+
+export namespace TaskBoard {
+  export type Status = "pending" | "in_progress" | "done";
+
+  export interface Person {
+    id: string;
+    fullName: string;
+    avatarUrl?: string;
+  }
path: turboui/src/TaskBoard/components/index.tsx
user: login: sourcery-ai[bot]
body: **suggestion:** Consider reducing repeated styling logic in view mode buttons.

Extract the shared classNames into a common variable or reusable component to reduce duplication.

Suggested implementation:

```typescript
  const viewModeButtonStyles = {
    base: "px-3 py-1 text-xs rounded-md",
    active: "bg-primary-base text-white",
    inactive: "bg-surface-accent text-content-base hover:bg-surface-accent-hover"
  };

  return (

```

```typescript
            <button
              onClick={() => setCurrentViewMode("table")}
              className={`${viewModeButtonStyles.base} ${
                currentViewMode === "table"
                  ? viewModeButtonStyles.active
                  : viewModeButtonStyles.inactive
              }`}
            >
              Table
            </button>

```

```typescript
            <button
              onClick={() => setCurrentViewMode("grid")}
              className={`${viewModeButtonStyles.base} ${
                currentViewMode === "grid"
                  ? viewModeButtonStyles.active
                  : viewModeButtonStyles.inactive
              }`}
            >
              Grid
            </button>

```

If there are other view mode buttons or similar styling logic in this file, apply the same pattern using viewModeButtonStyles.
created_at: 2025-05-06T12:04:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,142 @@
+import { TaskBoard } from "../components";
+
+// Mock labels
+export const mockLabels = {
+  highPriority: { id: "1", name: "High Priority", color: "#EF4444" },
+  mediumPriority: { id: "2", name: "Medium Priority", color: "#F59E0B" },
+  lowPriority: { id: "3", name: "Low Priority", color: "#10B981" },
+  bug: { id: "4", name: "Bug", color: "#8B5CF6" },
+  feature: { id: "5", name: "Feature", color: "#3B82F6" },
+  documentation: { id: "6", name: "Documentation", color: "#6366F1" },
+};
+
+// Mock people
+export const mockPeople = {
+  john: { 
+    id: "1", 
+    fullName: "John Doe", 
+    avatarUrl: "https://i.pravatar.cc/100?u=johndoe@example.com" 
+  },
+  jane: { 
+    id: "2", 
+    fullName: "Jane Smith", 
+    avatarUrl: "https://i.pravatar.cc/100?u=janesmith@example.com" 
+  },
+  bob: { 
+    id: "3", 
+    fullName: "Bob Johnson", 
+    avatarUrl: "https://i.pravatar.cc/100?u=bobjohnson@example.com" 
+  },
+  alice: { 
+    id: "4", 
+    fullName: "Alice Williams", 
+    avatarUrl: "https://i.pravatar.cc/100?u=alicewilliams@example.com" 
+  },
+};
+
+// Mock milestones
+export const mockMilestones = {
+  q2Release: { id: "1", name: "Q2 Release", dueDate: "2025-06-30" },
+  productLaunch: { id: "2", name: "Product Launch", dueDate: "2025-08-15" },
+  marketExpansion: { id: "3", name: "Market Expansion", dueDate: "2025-09-30" },
+};
+
+// Mock tasks
+export const mockTasks: TaskBoard.Task[] = [
path: turboui/src/TaskBoard/tests/mockData.ts
user: login: sourcery-ai[bot]
body: **suggestion (testing):** Consider adding a mock task with an unrecognized status value.

Add a mock task with an unknown status (e.g., 'archived') to verify `groupTasksByStatus` and `StatusBadge` handle undefined statuses safely instead of defaulting to 'pending' and causing an error on `taskStatusConfig['archived']`.

Suggested implementation:

```typescript
export const mockTasks: TaskBoard.Task[] = [
  {
    id: "task-1",
    title: "Implement user authentication",
    status: "pending",
    description: "Create login/signup flows with OAuth integration",
    assignees: [mockPeople.john, mockPeople.jane],
    labels: [mockLabels.feature, mockLabels.highPriority],
    milestone: mockMilestones.q2Release,
    dueDate: "2025-05-10", // Future date
    points: 8,
    hasDescription: true,
    hasComments: true,
  },
  {
    id: "task-unknown",
    title: "Test unknown status handling",
    status: "archived", // Unrecognized status used for testing
    description: "This task has an unrecognized status to verify proper handling in groupTasksByStatus and StatusBadge",
    assignees: [],
    labels: [mockLabels.feature],
    milestone: mockMilestones.productLaunch,
    dueDate: "2025-07-01",
    points: 5,
    hasDescription: false,
    hasComments: false,

```

If there are other mock tasks or additional configuration in the file, please ensure that this new task entry fits within the expected data shape and does not conflict with any other test data. Verify that your test suite now includes a test case that checks for the handling of unknown statuses.
created_at: 2025-05-06T12:04:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,321 @@
+import React, { useState } from "react";
+import { StatusBadge } from "../../StatusBadge";
+import { SecondaryButton } from "../../Button";
+import { BlackLink } from "../../Link";
+import { AvatarWithName } from "../../Avatar/AvatarWithName";
+
+export namespace TaskBoard {
+  export type Status = "pending" | "in_progress" | "done";
+
+  export interface Person {
+    id: string;
+    fullName: string;
+    avatarUrl?: string;
+  }
+
+  export interface Milestone {
+    id: string;
+    name: string;
+    dueDate?: string;
+  }
+
+  export interface Label {
+    id: string;
+    name: string;
+    color: string;
+  }
+
+  export interface Task {
+    id: string;
+    title: string;
+    status: Status;
+    description?: string;
+    assignees?: Person[];
+    labels?: Label[];
+    milestone?: Milestone;
+    points?: number;
+    dueDate?: string;
+    hasComments?: boolean;
+    hasDescription?: boolean;
+    commentCount?: number;
+  }
+
+  export type TaskViewMode = "table" | "kanban" | "timeline";
+
+  export interface Props {
+    title: string;
+    tasks: Task[];
+    viewMode?: TaskViewMode;
+  }
+}
+
+// Helper to group tasks by status
+const groupTasksByStatus = (tasks: TaskBoard.Task[]) => {
+  const grouped: Record<string, TaskBoard.Task[]> = {
+    pending: [],
+    in_progress: [],
+    done: [],
+  };
+
+  tasks.forEach((task) => {
+    if (grouped[task.status]) {
+      grouped[task.status].push(task);
+    } else {
+      grouped.pending.push(task);
+    }
+  });
+
+  return grouped;
+};
+
+// Map task status to badge status and labels
+const taskStatusConfig: Record<TaskBoard.Status, { status: string; label: string }> = {
+  pending: { status: "paused", label: "Not started" },
+  in_progress: { status: "pending", label: "In progress" },
+  done: { status: "completed", label: "Done" },
+};
+
+// Helper to get the display name for a status
+const getStatusDisplayName = (status: TaskBoard.Status): string => {
+  switch (status) {
+    case "pending":
+      return "Not started";
+    case "in_progress":
+      return "In progress";
+    case "done":
+      return "Done";
+    default:
+      return status;
+  }
+};
+
+// Helper component to display due date with appropriate formatting
+function DueDateDisplay({ dueDate }: { dueDate: string }) {
+  // Check if the due date is in the past
+  const isOverdue = new Date(dueDate) < new Date();
+
+  // Format the date (YYYY-MM-DD to more readable format)
+  const formatDate = (dateString: string) => {
+    const date = new Date(dateString);
+    return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
+  };
+
+  return (
+    <span
+      className={`text-sm flex items-center ${isOverdue ? "text-red-600 font-medium" : "text-content-base"}`}
+      title={isOverdue ? "Overdue" : ""}
+    >
+      {isOverdue && (
+        <svg
+          xmlns="http://www.w3.org/2000/svg"
+          className="w-3.5 h-3.5 mr-1 text-red-500"
+          viewBox="0 0 24 24"
+          fill="none"
+          stroke="currentColor"
+          strokeWidth="2"
+          strokeLinecap="round"
+          strokeLinejoin="round"
+        >
+          <circle cx="12" cy="12" r="10" />
+          <polyline points="12 6 12 12 16 14" />
+        </svg>
+      )}
+      {formatDate(dueDate)}
+    </span>
+  );
+}
+
+export function TaskBoard({ title, tasks, viewMode = "table" }: TaskBoard.Props) {
+  const [currentViewMode, setCurrentViewMode] = useState<TaskBoard.TaskViewMode>(viewMode);
+
+  return (
+    <div className="flex flex-col w-full h-full bg-surface-base rounded-lg">
+      <header className="flex flex-col sm:flex-row sm:items-center sm:justify-between px-4 sm:px-6 py-2 border-b border-surface-outline">
+        <div className="flex flex-row items-center gap-4">
+          <h1 className="text-sm sm:text-base font-bold text-content-accent">{title}</h1>
+          <SecondaryButton size="xs">+ Add Task</SecondaryButton>
+        </div>
+        <div className="flex mt-2 sm:mt-0">
+          <div className="flex space-x-2">
+            <button
+              onClick={() => setCurrentViewMode("table")}
+              className={`px-3 py-1 text-xs rounded-md ${
+                currentViewMode === "table"
+                  ? "bg-primary-base text-white"
+                  : "bg-surface-accent text-content-base hover:bg-surface-accent-hover"
+              }`}
+            >
+              Table
+            </button>
+            <button
+              onClick={() => setCurrentViewMode("kanban")}
+              className={`px-3 py-1 text-xs rounded-md ${
+                currentViewMode === "kanban"
+                  ? "bg-primary-base text-white"
+                  : "bg-surface-accent text-content-base hover:bg-surface-accent-hover"
+              }`}
+            >
+              Kanban
+            </button>
+            <button
+              onClick={() => setCurrentViewMode("timeline")}
+              className={`px-3 py-1 text-xs rounded-md ${
+                currentViewMode === "timeline"
+                  ? "bg-primary-base text-white"
+                  : "bg-surface-accent text-content-base hover:bg-surface-accent-hover"
+              }`}
+            >
+              Timeline
+            </button>
+          </div>
+        </div>
+      </header>
path: turboui/src/TaskBoard/components/index.tsx
user: login: sourcery-ai[bot]
body: **issue (complexity):** Consider extracting each view (table, kanban, timeline) into its own dedicated subcomponent to reduce complexity.

You can reduce complexity by extracting each view into its own dedicated subcomponent. For example, move the table view markup into a separate `TableView` component. This will avoid nesting inline conditionals in the main TaskBoard component and make each view easier to understand and maintain.

**Actionable Steps:**

1. Create a new component for the table view:

```tsx
// TableView.tsx
import React from "react";
import { StatusBadge } from "../../StatusBadge";
import { BlackLink } from "../../Link";
import { AvatarWithName } from "../../Avatar/AvatarWithName";
import DueDateDisplay from "./DueDateDisplay"; // or inline if preferred
import { TaskBoard } from "./TaskBoardTypes"; // adjust type import as needed

interface TableViewProps {
  tasks: TaskBoard.Task[];
}

export function TableView({ tasks }: TableViewProps) {
  return (
    <div className="overflow-x-auto bg-surface-base">
      <table className="min-w-full">
        <thead>
          <tr className="border-b border-surface-outline bg-surface-dimmed text-content-base text-xs sm:text-sm sticky top-0">
            <th className="text-left py-1.5 px-4 font-semibold">Task</th>
            <th className="text-left py-1.5 px-4 font-semibold">Status</th>
            <th className="text-left py-1.5 px-4 font-semibold">Assignee</th>
            <th className="text-left py-1.5 px-4 font-semibold">Due</th>
            <th className="text-left py-1.5 px-4 font-semibold">Milestone</th>
            <th className="text-left py-1.5 px-4 font-semibold">Labels</th>
          </tr>
        </thead>
        <tbody>
          {tasks.map((task) => (
            <tr
              key={task.id}
              className="group border-b border-stroke-base bg-surface-base hover:bg-surface-highlight"
            >
              {/* ...rest of table row rendering */}
            </tr>
          ))}
          {tasks.length === 0 && (
            <tr>
              <td colSpan={6} className="py-4 text-center text-content-subtle">
                No tasks available
              </td>
            </tr>
          )}
        </tbody>
      </table>
    </div>
  );
}
```

2. Similarly, create stub components for Kanban and Timeline views:

```tsx
// KanbanView.tsx
import React from "react";

interface KanbanViewProps {
  tasks: any; // Adjust as needed
}

export function KanbanView({ tasks }: KanbanViewProps) {
  return (
    <div className="flex h-full text-center text-content-subtle">
      <div className="m-auto p-6 border border-dashed border-surface-outline rounded-lg">
        Kanban view will be implemented here
      </div>
    </div>
  );
}
```

```tsx
// TimelineView.tsx
import React from "react";

interface TimelineViewProps {
  tasks: any; // Adjust as needed
}

export function TimelineView({ tasks }: TimelineViewProps) {
  return (
    <div className="flex h-full text-center text-content-subtle">
      <div className="m-auto p-6 border border-dashed border-surface-outline rounded-lg">
        Timeline view will be implemented here
      </div>
    </div>
  );
}
```

3. Update the `TaskBoard` component to use these subcomponents:

```tsx
// TaskBoard.tsx (excerpt)
import React, { useState } from "react";
import { TableView } from "./TableView";
import { KanbanView } from "./KanbanView";
import { TimelineView } from "./TimelineView";
import { TaskBoard as TaskBoardTypes } from "./TaskBoardTypes";

export function TaskBoard({ title, tasks, viewMode = "table" }: TaskBoardTypes.Props) {
  const [currentViewMode, setCurrentViewMode] = useState<TaskBoardTypes.TaskViewMode>(viewMode);

  const renderView = () => {
    switch (currentViewMode) {
      case "table":
        return <TableView tasks={tasks} />;
      case "kanban":
        return <KanbanView tasks={tasks} />;
      case "timeline":
        return <TimelineView tasks={tasks} />;
      default:
        return null;
    }
  };

  return (
    <div className="flex flex-col w-full h-full bg-surface-base rounded-lg">
      {/* ...header code */}
      <div className="flex-1 overflow-auto">
        {renderView()}
      </div>
    </div>
  );
}
```

This refactoring simplifies the main component's logic and improves maintainability while preserving functionality.
created_at: 2025-05-06T12:04:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,321 @@
+import React, { useState } from "react";
+import { StatusBadge } from "../../StatusBadge";
+import { SecondaryButton } from "../../Button";
+import { BlackLink } from "../../Link";
+import { AvatarWithName } from "../../Avatar/AvatarWithName";
+
+export namespace TaskBoard {
+  export type Status = "pending" | "in_progress" | "done";
+
+  export interface Person {
+    id: string;
+    fullName: string;
+    avatarUrl?: string;
+  }
+
+  export interface Milestone {
+    id: string;
+    name: string;
+    dueDate?: string;
path: turboui/src/TaskBoard/components/index.tsx
user: login: shiroyasha
body: Use `Date`.

reasonning: Lift the responsibility for Date validation/parsability to the app, no need to worry and handle unparspable dates in turboui components.
created_at: 2025-05-06T12:10:47Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,321 @@
+import React, { useState } from "react";
+import { StatusBadge } from "../../StatusBadge";
+import { SecondaryButton } from "../../Button";
+import { BlackLink } from "../../Link";
+import { AvatarWithName } from "../../Avatar/AvatarWithName";
+
+export namespace TaskBoard {
+  export type Status = "pending" | "in_progress" | "done";
+
+  export interface Person {
+    id: string;
+    fullName: string;
+    avatarUrl?: string;
+  }
+
+  export interface Milestone {
+    id: string;
+    name: string;
+    dueDate?: string;
+  }
+
+  export interface Label {
+    id: string;
+    name: string;
+    color: string;
+  }
+
+  export interface Task {
+    id: string;
+    title: string;
+    status: Status;
+    description?: string;
+    assignees?: Person[];
+    labels?: Label[];
+    milestone?: Milestone;
+    points?: number;
+    dueDate?: string;
+    hasComments?: boolean;
+    hasDescription?: boolean;
+    commentCount?: number;
+  }
+
+  export type TaskViewMode = "table" | "kanban" | "timeline";
+
+  export interface Props {
+    title: string;
+    tasks: Task[];
+    viewMode?: TaskViewMode;
+  }
+}
+
+// Helper to group tasks by status
+const groupTasksByStatus = (tasks: TaskBoard.Task[]) => {
+  const grouped: Record<string, TaskBoard.Task[]> = {
+    pending: [],
+    in_progress: [],
+    done: [],
+  };
+
+  tasks.forEach((task) => {
+    if (grouped[task.status]) {
+      grouped[task.status].push(task);
+    } else {
+      grouped.pending.push(task);
+    }
+  });
+
+  return grouped;
+};
+
+// Map task status to badge status and labels
+const taskStatusConfig: Record<TaskBoard.Status, { status: string; label: string }> = {
+  pending: { status: "paused", label: "Not started" },
+  in_progress: { status: "pending", label: "In progress" },
+  done: { status: "completed", label: "Done" },
+};
+
+// Helper to get the display name for a status
+const getStatusDisplayName = (status: TaskBoard.Status): string => {
+  switch (status) {
+    case "pending":
+      return "Not started";
+    case "in_progress":
+      return "In progress";
+    case "done":
+      return "Done";
+    default:
+      return status;
+  }
+};
+
+// Helper component to display due date with appropriate formatting
+function DueDateDisplay({ dueDate }: { dueDate: string }) {
+  // Check if the due date is in the past
+  const isOverdue = new Date(dueDate) < new Date();
+
+  // Format the date (YYYY-MM-DD to more readable format)
+  const formatDate = (dateString: string) => {
+    const date = new Date(dateString);
+    return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
+  };
+
+  return (
+    <span
+      className={`text-sm flex items-center ${isOverdue ? "text-red-600 font-medium" : "text-content-base"}`}
+      title={isOverdue ? "Overdue" : ""}
+    >
+      {isOverdue && (
+        <svg
+          xmlns="http://www.w3.org/2000/svg"
+          className="w-3.5 h-3.5 mr-1 text-red-500"
+          viewBox="0 0 24 24"
+          fill="none"
+          stroke="currentColor"
+          strokeWidth="2"
+          strokeLinecap="round"
+          strokeLinejoin="round"
+        >
+          <circle cx="12" cy="12" r="10" />
+          <polyline points="12 6 12 12 16 14" />
+        </svg>
path: turboui/src/TaskBoard/components/index.tsx
user: login: shiroyasha
body: Use tabler-icons instead of hardcoding SVGs. You have examples in other storybooks, search "tabler".
created_at: 2025-05-06T12:11:07Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,321 @@
+import React, { useState } from "react";
+import { StatusBadge } from "../../StatusBadge";
+import { SecondaryButton } from "../../Button";
+import { BlackLink } from "../../Link";
+import { AvatarWithName } from "../../Avatar/AvatarWithName";
+
+export namespace TaskBoard {
+  export type Status = "pending" | "in_progress" | "done";
+
+  export interface Person {
+    id: string;
+    fullName: string;
+    avatarUrl?: string;
+  }
+
+  export interface Milestone {
+    id: string;
+    name: string;
+    dueDate?: string;
+  }
+
+  export interface Label {
+    id: string;
+    name: string;
+    color: string;
+  }
+
+  export interface Task {
+    id: string;
+    title: string;
+    status: Status;
+    description?: string;
+    assignees?: Person[];
+    labels?: Label[];
+    milestone?: Milestone;
+    points?: number;
+    dueDate?: string;
+    hasComments?: boolean;
+    hasDescription?: boolean;
+    commentCount?: number;
+  }
+
+  export type TaskViewMode = "table" | "kanban" | "timeline";
+
+  export interface Props {
+    title: string;
+    tasks: Task[];
+    viewMode?: TaskViewMode;
+  }
+}
+
+// Helper to group tasks by status
+const groupTasksByStatus = (tasks: TaskBoard.Task[]) => {
+  const grouped: Record<string, TaskBoard.Task[]> = {
+    pending: [],
+    in_progress: [],
+    done: [],
+  };
+
+  tasks.forEach((task) => {
+    if (grouped[task.status]) {
+      grouped[task.status].push(task);
+    } else {
+      grouped.pending.push(task);
+    }
+  });
+
+  return grouped;
+};
+
+// Map task status to badge status and labels
+const taskStatusConfig: Record<TaskBoard.Status, { status: string; label: string }> = {
+  pending: { status: "paused", label: "Not started" },
+  in_progress: { status: "pending", label: "In progress" },
+  done: { status: "completed", label: "Done" },
+};
+
+// Helper to get the display name for a status
+const getStatusDisplayName = (status: TaskBoard.Status): string => {
+  switch (status) {
+    case "pending":
+      return "Not started";
+    case "in_progress":
+      return "In progress";
+    case "done":
+      return "Done";
+    default:
+      return status;
+  }
+};
+
+// Helper component to display due date with appropriate formatting
+function DueDateDisplay({ dueDate }: { dueDate: string }) {
+  // Check if the due date is in the past
+  const isOverdue = new Date(dueDate) < new Date();
+
+  // Format the date (YYYY-MM-DD to more readable format)
+  const formatDate = (dateString: string) => {
+    const date = new Date(dateString);
+    return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
+  };
+
+  return (
+    <span
+      className={`text-sm flex items-center ${isOverdue ? "text-red-600 font-medium" : "text-content-base"}`}
+      title={isOverdue ? "Overdue" : ""}
+    >
+      {isOverdue && (
+        <svg
+          xmlns="http://www.w3.org/2000/svg"
+          className="w-3.5 h-3.5 mr-1 text-red-500"
+          viewBox="0 0 24 24"
+          fill="none"
+          stroke="currentColor"
+          strokeWidth="2"
+          strokeLinecap="round"
+          strokeLinejoin="round"
+        >
+          <circle cx="12" cy="12" r="10" />
+          <polyline points="12 6 12 12 16 14" />
+        </svg>
+      )}
+      {formatDate(dueDate)}
+    </span>
+  );
+}
+
+export function TaskBoard({ title, tasks, viewMode = "table" }: TaskBoard.Props) {
+  const [currentViewMode, setCurrentViewMode] = useState<TaskBoard.TaskViewMode>(viewMode);
+
+  return (
+    <div className="flex flex-col w-full h-full bg-surface-base rounded-lg">
+      <header className="flex flex-col sm:flex-row sm:items-center sm:justify-between px-4 sm:px-6 py-2 border-b border-surface-outline">
+        <div className="flex flex-row items-center gap-4">
+          <h1 className="text-sm sm:text-base font-bold text-content-accent">{title}</h1>
+          <SecondaryButton size="xs">+ Add Task</SecondaryButton>
+        </div>
+        <div className="flex mt-2 sm:mt-0">
+          <div className="flex space-x-2">
+            <button
+              onClick={() => setCurrentViewMode("table")}
+              className={`px-3 py-1 text-xs rounded-md ${
+                currentViewMode === "table"
+                  ? "bg-primary-base text-white"
+                  : "bg-surface-accent text-content-base hover:bg-surface-accent-hover"
+              }`}
+            >
+              Table
+            </button>
+            <button
+              onClick={() => setCurrentViewMode("kanban")}
+              className={`px-3 py-1 text-xs rounded-md ${
+                currentViewMode === "kanban"
+                  ? "bg-primary-base text-white"
+                  : "bg-surface-accent text-content-base hover:bg-surface-accent-hover"
+              }`}
+            >
+              Kanban
+            </button>
+            <button
+              onClick={() => setCurrentViewMode("timeline")}
+              className={`px-3 py-1 text-xs rounded-md ${
+                currentViewMode === "timeline"
+                  ? "bg-primary-base text-white"
+                  : "bg-surface-accent text-content-base hover:bg-surface-accent-hover"
+              }`}
+            >
+              Timeline
+            </button>
+          </div>
+        </div>
+      </header>
+      <div className="flex-1 overflow-auto">
+        {currentViewMode === "table" && (
+          <div className="overflow-x-auto bg-surface-base">
+            <table className="min-w-full">
path: turboui/src/TaskBoard/components/index.tsx
user: login: shiroyasha
body: Would be nice to have a generalized (unstyled) data table in the turboui that we can use for workmaps and tasks.
created_at: 2025-05-06T12:14:22Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,321 @@
+import React, { useState } from "react";
+import { StatusBadge } from "../../StatusBadge";
+import { SecondaryButton } from "../../Button";
+import { BlackLink } from "../../Link";
+import { AvatarWithName } from "../../Avatar/AvatarWithName";
+
+export namespace TaskBoard {
+  export type Status = "pending" | "in_progress" | "done";
+
+  export interface Person {
+    id: string;
+    fullName: string;
+    avatarUrl?: string;
+  }
+
+  export interface Milestone {
+    id: string;
+    name: string;
+    dueDate?: string;
+  }
+
+  export interface Label {
+    id: string;
+    name: string;
+    color: string;
+  }
+
+  export interface Task {
+    id: string;
+    title: string;
+    status: Status;
+    description?: string;
+    assignees?: Person[];
+    labels?: Label[];
+    milestone?: Milestone;
+    points?: number;
+    dueDate?: string;
+    hasComments?: boolean;
+    hasDescription?: boolean;
+    commentCount?: number;
+  }
+
+  export type TaskViewMode = "table" | "kanban" | "timeline";
+
+  export interface Props {
+    title: string;
+    tasks: Task[];
+    viewMode?: TaskViewMode;
+  }
+}
+
+// Helper to group tasks by status
+const groupTasksByStatus = (tasks: TaskBoard.Task[]) => {
+  const grouped: Record<string, TaskBoard.Task[]> = {
+    pending: [],
+    in_progress: [],
+    done: [],
+  };
+
+  tasks.forEach((task) => {
+    if (grouped[task.status]) {
+      grouped[task.status].push(task);
+    } else {
+      grouped.pending.push(task);
+    }
+  });
+
+  return grouped;
+};
+
+// Map task status to badge status and labels
+const taskStatusConfig: Record<TaskBoard.Status, { status: string; label: string }> = {
+  pending: { status: "paused", label: "Not started" },
+  in_progress: { status: "pending", label: "In progress" },
+  done: { status: "completed", label: "Done" },
+};
+
+// Helper to get the display name for a status
+const getStatusDisplayName = (status: TaskBoard.Status): string => {
+  switch (status) {
+    case "pending":
+      return "Not started";
+    case "in_progress":
+      return "In progress";
+    case "done":
+      return "Done";
+    default:
+      return status;
+  }
+};
+
+// Helper component to display due date with appropriate formatting
+function DueDateDisplay({ dueDate }: { dueDate: string }) {
+  // Check if the due date is in the past
+  const isOverdue = new Date(dueDate) < new Date();
+
+  // Format the date (YYYY-MM-DD to more readable format)
+  const formatDate = (dateString: string) => {
+    const date = new Date(dateString);
+    return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
+  };
+
+  return (
+    <span
+      className={`text-sm flex items-center ${isOverdue ? "text-red-600 font-medium" : "text-content-base"}`}
+      title={isOverdue ? "Overdue" : ""}
+    >
+      {isOverdue && (
+        <svg
+          xmlns="http://www.w3.org/2000/svg"
+          className="w-3.5 h-3.5 mr-1 text-red-500"
+          viewBox="0 0 24 24"
+          fill="none"
+          stroke="currentColor"
+          strokeWidth="2"
+          strokeLinecap="round"
+          strokeLinejoin="round"
+        >
+          <circle cx="12" cy="12" r="10" />
+          <polyline points="12 6 12 12 16 14" />
+        </svg>
+      )}
+      {formatDate(dueDate)}
+    </span>
+  );
+}
+
+export function TaskBoard({ title, tasks, viewMode = "table" }: TaskBoard.Props) {
+  const [currentViewMode, setCurrentViewMode] = useState<TaskBoard.TaskViewMode>(viewMode);
+
+  return (
+    <div className="flex flex-col w-full h-full bg-surface-base rounded-lg">
+      <header className="flex flex-col sm:flex-row sm:items-center sm:justify-between px-4 sm:px-6 py-2 border-b border-surface-outline">
+        <div className="flex flex-row items-center gap-4">
+          <h1 className="text-sm sm:text-base font-bold text-content-accent">{title}</h1>
+          <SecondaryButton size="xs">+ Add Task</SecondaryButton>
+        </div>
+        <div className="flex mt-2 sm:mt-0">
+          <div className="flex space-x-2">
+            <button
+              onClick={() => setCurrentViewMode("table")}
+              className={`px-3 py-1 text-xs rounded-md ${
+                currentViewMode === "table"
+                  ? "bg-primary-base text-white"
+                  : "bg-surface-accent text-content-base hover:bg-surface-accent-hover"
+              }`}
+            >
+              Table
+            </button>
+            <button
+              onClick={() => setCurrentViewMode("kanban")}
+              className={`px-3 py-1 text-xs rounded-md ${
+                currentViewMode === "kanban"
+                  ? "bg-primary-base text-white"
+                  : "bg-surface-accent text-content-base hover:bg-surface-accent-hover"
+              }`}
+            >
+              Kanban
+            </button>
+            <button
+              onClick={() => setCurrentViewMode("timeline")}
+              className={`px-3 py-1 text-xs rounded-md ${
+                currentViewMode === "timeline"
+                  ? "bg-primary-base text-white"
+                  : "bg-surface-accent text-content-base hover:bg-surface-accent-hover"
+              }`}
+            >
+              Timeline
+            </button>
+          </div>
+        </div>
+      </header>
+      <div className="flex-1 overflow-auto">
+        {currentViewMode === "table" && (
+          <div className="overflow-x-auto bg-surface-base">
+            <table className="min-w-full">
+              <thead>
+                <tr className="border-b border-surface-outline bg-surface-dimmed text-content-base text-xs sm:text-sm sticky top-0">
+                  <th className="text-left py-1.5 px-4 font-semibold">Task</th>
+                  <th className="text-left py-1.5 px-4 font-semibold">Status</th>
+                  <th className="text-left py-1.5 px-4 font-semibold">Assignee</th>
+                  <th className="text-left py-1.5 px-4 font-semibold">Due</th>
+                  <th className="text-left py-1.5 px-4 font-semibold">Milestone</th>
+                  <th className="text-left py-1.5 px-4 font-semibold">Labels</th>
+                </tr>
+              </thead>
+              <tbody>
+                {tasks.map((task) => (
+                  <tr
+                    key={task.id}
+                    className="group border-b border-stroke-base bg-surface-base hover:bg-surface-highlight"
+                  >
+                    <td className="py-1 px-4">
+                      <div className="flex items-center">
+                        <BlackLink
+                          to={`/tasks/${task.id}`}
+                          className="text-sm text-content-base hover:text-link-hover transition-colors"
+                          underline="hover"
+                        >
+                          {task.title}
+                        </BlackLink>
+                        {task.hasDescription && (
+                          <span className="ml-2 text-content-subtle">
+                            <svg
+                              xmlns="http://www.w3.org/2000/svg"
+                              width="16"
+                              height="16"
+                              viewBox="0 0 24 24"
+                              fill="none"
+                              stroke="currentColor"
+                              strokeWidth="2"
+                              strokeLinecap="round"
+                              strokeLinejoin="round"
+                            >
+                              <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
+                              <polyline points="14 2 14 8 20 8" />
+                              <line x1="16" y1="13" x2="8" y2="13" />
+                              <line x1="16" y1="17" x2="8" y2="17" />
+                              <polyline points="10 9 9 9 8 9" />
+                            </svg>
path: turboui/src/TaskBoard/components/index.tsx
user: login: shiroyasha
body: ditto TablerIcons
created_at: 2025-05-06T12:14:43Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,98 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import { TaskBoard } from "../components";
+import { Page } from "../../Page";
+import { mockTasks, mockEmptyTasks } from "../tests/mockData";
+
+/**
+ * TaskBoard is a comprehensive task management component designed for teams.
+ */
+const meta = {
+  title: "Components/TaskBoard",
+  component: TaskBoard,
+  parameters: {
+    layout: "fullscreen",
+  },
+  argTypes: {
+    tasks: { control: "object" },
+    viewMode: { control: "select", options: ["table", "kanban", "timeline"] },
+  },
+} satisfies Meta<typeof TaskBoard>;
+
+export default meta;
+type Story = StoryObj<typeof meta>;
+
+/**
+ * Default table view of the TaskBoard with multiple tasks in different statuses
+ */
+export const Default: Story = {
+  tags: ["autodocs"],
+  render: (args) => (
+    <div className="h-[800px] py-[4.5rem] px-2">
+      <Page title={args.title} size="fullwidth">
+        <TaskBoard {...args} />
+      </Page>
+    </div>
+  ),
path: turboui/src/TaskBoard/stories/TaskBoard.stories.tsx
user: login: shiroyasha
body: Add a decorator to the meta that wraps the component into a page instead of repeating. See examples on the miniworkmap for example.
created_at: 2025-05-06T12:16:12Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,142 @@
+import { TaskBoard } from "../components";
+
+// Mock labels
+export const mockLabels = {
+  highPriority: { id: "1", name: "High Priority", color: "#EF4444" },
+  mediumPriority: { id: "2", name: "Medium Priority", color: "#F59E0B" },
+  lowPriority: { id: "3", name: "Low Priority", color: "#10B981" },
+  bug: { id: "4", name: "Bug", color: "#8B5CF6" },
+  feature: { id: "5", name: "Feature", color: "#3B82F6" },
+  documentation: { id: "6", name: "Documentation", color: "#6366F1" },
+};
+
+// Mock people
+export const mockPeople = {
path: turboui/src/TaskBoard/tests/mockData.ts
user: login: shiroyasha
body: We have a people generator in turboui/src/utils/storybooks/genPeople. See examples in the GoalPage stories.
created_at: 2025-05-06T12:17:14Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,321 @@
+import React, { useState } from "react";
+import { StatusBadge } from "../../StatusBadge";
+import { SecondaryButton } from "../../Button";
+import { BlackLink } from "../../Link";
+import { AvatarWithName } from "../../Avatar/AvatarWithName";
+
+export namespace TaskBoard {
+  export type Status = "pending" | "in_progress" | "done";
+
+  export interface Person {
+    id: string;
+    fullName: string;
+    avatarUrl?: string;
+  }
+
+  export interface Milestone {
+    id: string;
+    name: string;
+    dueDate?: string;
+  }
+
+  export interface Label {
+    id: string;
+    name: string;
+    color: string;
+  }
+
+  export interface Task {
+    id: string;
+    title: string;
+    status: Status;
+    description?: string;
+    assignees?: Person[];
+    labels?: Label[];
+    milestone?: Milestone;
+    points?: number;
+    dueDate?: string;
+    hasComments?: boolean;
+    hasDescription?: boolean;
+    commentCount?: number;
+  }
+
+  export type TaskViewMode = "table" | "kanban" | "timeline";
+
+  export interface Props {
+    title: string;
+    tasks: Task[];
+    viewMode?: TaskViewMode;
+  }
+}
+
+// Helper to group tasks by status
+const groupTasksByStatus = (tasks: TaskBoard.Task[]) => {
+  const grouped: Record<string, TaskBoard.Task[]> = {
+    pending: [],
+    in_progress: [],
+    done: [],
+  };
+
+  tasks.forEach((task) => {
+    if (grouped[task.status]) {
+      grouped[task.status].push(task);
+    } else {
+      grouped.pending.push(task);
+    }
+  });
+
+  return grouped;
+};
+
+// Map task status to badge status and labels
+const taskStatusConfig: Record<TaskBoard.Status, { status: string; label: string }> = {
+  pending: { status: "paused", label: "Not started" },
+  in_progress: { status: "pending", label: "In progress" },
+  done: { status: "completed", label: "Done" },
+};
+
+// Helper to get the display name for a status
+const getStatusDisplayName = (status: TaskBoard.Status): string => {
+  switch (status) {
+    case "pending":
+      return "Not started";
+    case "in_progress":
+      return "In progress";
+    case "done":
+      return "Done";
+    default:
+      return status;
+  }
+};
+
+// Helper component to display due date with appropriate formatting
+function DueDateDisplay({ dueDate }: { dueDate: string }) {
+  // Check if the due date is in the past
+  const isOverdue = new Date(dueDate) < new Date();
+
+  // Format the date (YYYY-MM-DD to more readable format)
+  const formatDate = (dateString: string) => {
+    const date = new Date(dateString);
+    return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
+  };
+
+  return (
+    <span
+      className={`text-sm flex items-center ${isOverdue ? "text-red-600 font-medium" : "text-content-base"}`}
+      title={isOverdue ? "Overdue" : ""}
+    >
+      {isOverdue && (
+        <svg
+          xmlns="http://www.w3.org/2000/svg"
+          className="w-3.5 h-3.5 mr-1 text-red-500"
+          viewBox="0 0 24 24"
+          fill="none"
+          stroke="currentColor"
+          strokeWidth="2"
+          strokeLinecap="round"
+          strokeLinejoin="round"
+        >
+          <circle cx="12" cy="12" r="10" />
+          <polyline points="12 6 12 12 16 14" />
+        </svg>
+      )}
+      {formatDate(dueDate)}
+    </span>
+  );
+}
+
+export function TaskBoard({ title, tasks, viewMode = "table" }: TaskBoard.Props) {
+  const [currentViewMode, setCurrentViewMode] = useState<TaskBoard.TaskViewMode>(viewMode);
+
+  return (
+    <div className="flex flex-col w-full h-full bg-surface-base rounded-lg">
+      <header className="flex flex-col sm:flex-row sm:items-center sm:justify-between px-4 sm:px-6 py-2 border-b border-surface-outline">
+        <div className="flex flex-row items-center gap-4">
+          <h1 className="text-sm sm:text-base font-bold text-content-accent">{title}</h1>
+          <SecondaryButton size="xs">+ Add Task</SecondaryButton>
+        </div>
+        <div className="flex mt-2 sm:mt-0">
+          <div className="flex space-x-2">
+            <button
+              onClick={() => setCurrentViewMode("table")}
+              className={`px-3 py-1 text-xs rounded-md ${
+                currentViewMode === "table"
+                  ? "bg-primary-base text-white"
+                  : "bg-surface-accent text-content-base hover:bg-surface-accent-hover"
+              }`}
+            >
+              Table
+            </button>
+            <button
+              onClick={() => setCurrentViewMode("kanban")}
+              className={`px-3 py-1 text-xs rounded-md ${
+                currentViewMode === "kanban"
+                  ? "bg-primary-base text-white"
+                  : "bg-surface-accent text-content-base hover:bg-surface-accent-hover"
+              }`}
+            >
+              Kanban
+            </button>
+            <button
+              onClick={() => setCurrentViewMode("timeline")}
+              className={`px-3 py-1 text-xs rounded-md ${
+                currentViewMode === "timeline"
+                  ? "bg-primary-base text-white"
+                  : "bg-surface-accent text-content-base hover:bg-surface-accent-hover"
+              }`}
+            >
+              Timeline
+            </button>
+          </div>
+        </div>
+      </header>
+      <div className="flex-1 overflow-auto">
+        {currentViewMode === "table" && (
+          <div className="overflow-x-auto bg-surface-base">
+            <table className="min-w-full">
path: turboui/src/TaskBoard/components/index.tsx
user: login: markoa
body: Will keep this on the backburner until task board is more complete.
created_at: 2025-05-06T14:47:49Z
author_association: MEMBER

PR comments:
user: login: cloudflare-workers-and-pages[bot]
created_at: 2025-05-06T15:00:51Z
body: ## Deploying storybooks with &nbsp;<a href="https://pages.dev"><img alt="Cloudflare Pages" src="https://user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png" width="16"></a> &nbsp;Cloudflare Pages

<table><tr><td><strong>Latest commit:</strong> </td><td>
<code>4129b8f</code>
</td></tr>
<tr><td><strong>Status:</strong></td><td>&nbsp;✅&nbsp; Deploy successful!</td></tr>
<tr><td><strong>Preview URL:</strong></td><td>
<a href='https://a4f98d9f.storybooks-aq1.pages.dev'>https://a4f98d9f.storybooks-aq1.pages.dev</a>
</td></tr>
<tr><td><strong>Branch Preview URL:</strong></td><td>
<a href='https://taskboard-start.storybooks-aq1.pages.dev'>https://taskboard-start.storybooks-aq1.pages.dev</a>
</td></tr>
</table>

[View logs](https://dash.cloudflare.com/ae90af09f224146877d86e9d35d0a8ce/pages/view/storybooks/a4f98d9f-c542-4270-82c4-f4cc134015f7)

author_association: NONE
user: login: cloudflare-workers-and-pages[bot]
created_at: 2025-05-06T15:02:22Z
body: ## Deploying operately-design with &nbsp;<a href="https://pages.dev"><img alt="Cloudflare Pages" src="https://user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png" width="16"></a> &nbsp;Cloudflare Pages

<table><tr><td><strong>Latest commit:</strong> </td><td>
<code>4129b8f</code>
</td></tr>
<tr><td><strong>Status:</strong></td><td>&nbsp;✅&nbsp; Deploy successful!</td></tr>
<tr><td><strong>Preview URL:</strong></td><td>
<a href='https://6b1dc7f6.operately-design.pages.dev'>https://6b1dc7f6.operately-design.pages.dev</a>
</td></tr>
<tr><td><strong>Branch Preview URL:</strong></td><td>
<a href='https://taskboard-start.operately-design.pages.dev'>https://taskboard-start.operately-design.pages.dev</a>
</td></tr>
</table>

[View logs](https://dash.cloudflare.com/ae90af09f224146877d86e9d35d0a8ce/pages/view/operately-design/6b1dc7f6-1f4e-43b0-9a2b-9da46c55f1e8)

author_association: NONE