Repository: Jacksondr5/monorepo
Pull request #72: add basic kanban board and board management
Author: login: Jacksondr5
Created: 2025-05-25T02:43:24Z | Merged: 2025-05-25T03:42:08Z

- **add kanban board**
- **bump chromatic**
- **connect kanban to db and add badge**
- **add board management to settings**


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced a Kanban board for managing candidates, including drag-and-drop functionality to move candidates between stages.
  - Added board management features, allowing users to create, edit, and delete boards and assign kanban stages.
  - Added visual components such as Badge and Sheet, with comprehensive Storybook stories for UI preview.
  - Added support for checkbox groups in forms.

- **Improvements**
  - Enhanced settings page with a new tab for board management.
  - Updated candidate creation flow to use a unified API namespace.

- **Chores**
  - Updated development dependencies to newer versions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/TODO.md b/TODO.md
index 80004f31..3842ebe7 100644
--- a/TODO.md
+++ b/TODO.md
@@ -6,6 +6,7 @@
 - Lint is missing
 - Find a way to handle the fact that organization ID is technically nullable. This causes annoying null checks throughout the app.
 - Generally add error handling in the UI and show toast notifications when failures occur
+- Deal with cascading deletion (e.g. deleting a kanban stage when there are boards using it)
 
 # Component Library
 
diff --git a/apps/hire/convex/_generated/api.d.ts b/apps/hire/convex/_generated/api.d.ts
index 0064781f..0afa9e68 100644
--- a/apps/hire/convex/_generated/api.d.ts
+++ b/apps/hire/convex/_generated/api.d.ts
@@ -13,7 +13,8 @@ import type {
   FilterApi,
   FunctionReference,
 } from "convex/server";
-import type * as candidate from "../candidate.js";
+import type * as boards from "../boards.js";
+import type * as candidates from "../candidates.js";
 import type * as companies from "../companies.js";
 import type * as kanbanStages from "../kanbanStages.js";
 import type * as model_candidates from "../model/candidates.js";
@@ -31,7 +32,8 @@ import type * as sources from "../sources.js";
  * ```
  */
 declare const fullApi: ApiFromModules<{
-  candidate: typeof candidate;
+  boards: typeof boards;
+  candidates: typeof candidates;
   companies: typeof companies;
   kanbanStages: typeof kanbanStages;
   "model/candidates": typeof model_candidates;
diff --git a/apps/hire/convex/boards.ts b/apps/hire/convex/boards.ts
new file mode 100644
index 00000000..2770dd17
--- /dev/null
+++ b/apps/hire/convex/boards.ts
@@ -0,0 +1,107 @@
+import { mutation, query } from "./_generated/server";
+import { getCompanyIdByClerkOrgId } from "./model/companies";
+import { zCustomQuery, zCustomMutation } from "convex-helpers/server/zod";
+import { NoOp } from "convex-helpers/server/customFunctions";
+import { z } from "zod";
+import {
+  BoardSchema,
+  CreateBoardSchema,
+  DeleteBoardSchema,
+  UpdateBoardSchema,
+} from "../src/server/zod/board";
+
+const boardQuery = zCustomQuery(query, NoOp);
+const boardMutation = zCustomMutation(mutation, NoOp);
+
+export const getBoardsByOrgId = boardQuery({
+  args: { orgId: z.string() },
+  async handler(ctx, args) {
+    const companyId = await getCompanyIdByClerkOrgId(ctx, {
+      clerkOrgId: args.orgId,
+    });
+
+    return await ctx.db
+      .query("boards")
+      .withIndex("by_company_order", (q) => q.eq("companyId", companyId))
+      .order("asc") // Assuming 'asc' for order
+      .collect();
+  },
+  returns: z.array(BoardSchema),
+});
+
+export const addBoard = boardMutation({
+  args: CreateBoardSchema.extend({ orgId: z.string() }),
+  async handler(ctx, args) {
+    const companyId = await getCompanyIdByClerkOrgId(ctx, {
+      clerkOrgId: args.orgId,
+    });
+
+    // Determine the next order value
+    const lastBoard = await ctx.db
+      .query("boards")
+      .withIndex("by_company_order", (q) => q.eq("companyId", companyId))
+      .order("desc")
+      .first();
+
+    const newOrder = lastBoard ? lastBoard.order + 1 : 0;
+
+    // Generate a simple slug
+    const slug = args.name
+      .toLowerCase()
+      .replace(/\s+/g, "-")
+      .replace(/[^\w-]+/g, "");
+
+    await ctx.db.insert("boards", {
+      name: args.name,
+      companyId,
+      order: newOrder,
+      slug: slug,
+      kanbanStageIds: [], // Initialize with empty array
+    });
+  },
+});
+
+export const deleteBoard = boardMutation({
+  args: DeleteBoardSchema,
+  async handler(ctx, args) {
+    const companyId = await getCompanyIdByClerkOrgId(ctx, {
+      clerkOrgId: args.orgId,
+    });
+
+    const boardToDelete = await ctx.db.get(args.boardId);
+    if (!boardToDelete) {
+      throw new Error("Board not found.");
+    }
+    if (boardToDelete.companyId !== companyId) {
+      throw new Error(
+        "Permission denied: Board does not belong to this organization.",
+      );
+    }
+
+    await ctx.db.delete(args.boardId);
+  },
+});
+
+export const updateBoard = boardMutation({
+  args: UpdateBoardSchema,
+  async handler(ctx, { orgId, _id, ...updates }) {
+    const companyId = await getCompanyIdByClerkOrgId(ctx, {
+      clerkOrgId: orgId,
+    });
+
+    const boardToUpdate = await ctx.db.get(_id);
+    if (!boardToUpdate) {
+      throw new Error("Board not found.");
+    }
+    if (boardToUpdate.companyId !== companyId) {
+      throw new Error(
+        "Permission denied: Board does not belong to this organization.",
+      );
+    }
+
+    await ctx.db.patch(_id, updates);
+  },
+});
+
+// Placeholder for future mutations if needed
+// export const reorderBoards = mutation({ ... });
diff --git a/apps/hire/convex/candidate.ts b/apps/hire/convex/candidates.ts
similarity index 85%
rename from apps/hire/convex/candidate.ts
rename to apps/hire/convex/candidates.ts
index 6629aac5..e91310e7 100644
--- a/apps/hire/convex/candidate.ts
+++ b/apps/hire/convex/candidates.ts
@@ -6,6 +6,7 @@ import {
   CandidateSchema,
   CreateCandidateSchema,
   UpdateCandidateSchema,
+  UpdateCandidateStageSchema,
 } from "../src/server/zod/candidate";
 import z from "zod";
 import { getCandidateById, verifyCandidateExists } from "./model/candidates";
@@ -76,3 +77,15 @@ export const deleteCandidate = candidateMutation({
     await ctx.db.delete(_id);
   },
 });
+
+// Mutation to update a candidate's Kanban stage
+export const updateCandidateStage = candidateMutation({
+  args: UpdateCandidateStageSchema,
+  handler: async (ctx, args) => {
+    await verifyCandidateExists(ctx, { _id: args.candidateId });
+    await ctx.db.patch(args.candidateId, {
+      kanbanStageId: args.kanbanStageId,
+      updatedAt: Date.now(),
+    });
+  },
+});
diff --git a/apps/hire/convex/schema.ts b/apps/hire/convex/schema.ts
index caaf4dc6..6dc89d43 100644
--- a/apps/hire/convex/schema.ts
+++ b/apps/hire/convex/schema.ts
@@ -2,6 +2,15 @@ import { defineSchema, defineTable } from "convex/server";
 import { v } from "convex/values";
 
 export default defineSchema({
+  boards: defineTable({
+    companyId: v.id("companies"),
+    kanbanStageIds: v.array(v.id("kanbanStages")),
+    name: v.string(),
+    order: v.float64(),
+    slug: v.string(),
+  })
+    .index("by_company_order", ["companyId", "order"])
+    .index("by_company_slug", ["companyId", "slug"]),
   candidates: defineTable({
     companyId: v.id("companies"),
     email: v.optional(v.string()),
diff --git a/apps/hire/src/app/candidate/add/page.tsx b/apps/hire/src/app/candidate/add/page.tsx
index 50b8c304..fcc9597f 100644
--- a/apps/hire/src/app/candidate/add/page.tsx
+++ b/apps/hire/src/app/candidate/add/page.tsx
@@ -8,7 +8,7 @@ import { ZodCreateCandidate } from "../../../server/zod/candidate";
 
 export default function AddCandidatePage() {
   const { organization } = useOrganization();
-  const addCandidateMutation = useMutation(api.candidate.createCandidate);
+  const addCandidateMutation = useMutation(api.candidates.createCandidate);
   const organizationId = organization?.id;
   if (!organizationId) {
     return null;
diff --git a/apps/hire/src/app/kanban/KanbanBoard.tsx b/apps/hire/src/app/kanban/KanbanBoard.tsx
new file mode 100644
index 00000000..51cdc2bd
--- /dev/null
+++ b/apps/hire/src/app/kanban/KanbanBoard.tsx
@@ -0,0 +1,93 @@
+"use client";
+
+import React from "react";
+import {
+  DndContext,
+  closestCenter,
+  PointerSensor,
+  useSensor,
+  useSensors,
+  DragEndEvent,
+} from "@dnd-kit/core";
+import { KanbanColumn } from "./KanbanColumn";
+import { useQuery, useMutation } from "convex/react";
+import { api } from "../../../convex/_generated/api";
+import type { Id } from "../../../convex/_generated/dataModel";
+
+export function KanbanBoard({ organizationId }: { organizationId: string }) {
+  const stages = useQuery(api.kanbanStages.getKanbanStages, {
+    orgId: organizationId,
+  });
+  const candidates = useQuery(api.candidates.listCandidates, {
+    orgId: organizationId,
+  });
+  const updateCandidateStage = useMutation(api.candidates.updateCandidateStage);
+
+  const sensors = useSensors(
+    useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
+  );
+
+  function handleDragEnd(event: DragEndEvent) {
+    const { active, over } = event;
+
+    if (!over || !active) return;
+
+    // Validate that the dragged item is a candidate
+    const candidateId = active.id;
+    const newStageId = over.id;
+    // Ensure the dragged item is actually a candidate
+    const candidate = candidates?.find((c) => c._id === candidateId);
+    if (!candidate) {
+      console.warn("Dragged item is not a valid candidate");
+      return;
+    }
+    // Ensure the drop target is a valid stage
+    const targetStage = stages?.find((s) => s._id === newStageId);
+    if (!targetStage) {
+      console.warn("Drop target is not a valid kanban stage");
+      return;
+    }
+
+    if (
+      candidate &&
+      stages?.some((s) => s._id === newStageId) &&
+      candidate.kanbanStageId !== newStageId
+    ) {
+      updateCandidateStage({
+        candidateId: candidateId as Id<"candidates">,
+        kanbanStageId: newStageId as Id<"kanbanStages">,
+      });
+    }
+  }
+
+  if (stages === undefined || candidates === undefined) {
+    return <div>Loading Kanban board...</div>;
+  }
+
+  if (!stages || stages.length === 0) {
+    return (
+      <div>
+        No Kanban stages found for this organization. Please configure stages.
+      </div>
+    );
+  }
+
+  return (
+    <div className="flex min-h-[80vh] gap-4 overflow-x-auto p-4">
+      <DndContext
+        sensors={sensors}
+        collisionDetection={closestCenter}
+        onDragEnd={handleDragEnd}
+      >
+        {stages.map((stage) => (
+          <KanbanColumn
+            key={stage._id}
+            id={stage._id}
+            title={stage.name}
+            candidates={candidates.filter((c) => c.kanbanStageId === stage._id)}
+          />
+        ))}
+      </DndContext>
+    </div>
+  );
+}
diff --git a/apps/hire/src/app/kanban/KanbanCard.tsx b/apps/hire/src/app/kanban/KanbanCard.tsx
new file mode 100644
index 00000000..3f755f63
--- /dev/null
+++ b/apps/hire/src/app/kanban/KanbanCard.tsx
@@ -0,0 +1,34 @@
+"use client";
+
+import React, { memo } from "react";
+import { useDraggable } from "@dnd-kit/core";
+import { ZodCandidate } from "~/server/zod/candidate";
+
+export const KanbanCard = memo(function KanbanCard({
+  candidate,
+}: {
+  candidate: ZodCandidate;
+}) {
+  const { attributes, listeners, setNodeRef, transform, isDragging } =
+    useDraggable({ id: candidate._id });
+  const style = {
+    transform: transform
+      ? `translate3d(${transform.x}px, ${transform.y}px, 0)`
+      : undefined,
+    opacity: isDragging ? 0.5 : 1,
+    cursor: isDragging ? "grabbing" : "grab",
+  };
+  return (
+    <div
+      ref={setNodeRef}
+      style={style}
+      {...listeners}
+      {...attributes}
+      className="select-none rounded border border-[var(--color-slate-7)] bg-[var(--color-olive-3)] p-3 text-[var(--color-slate-12)] shadow transition-colors hover:border-[var(--color-grass-9)]"
+    >
+      <span className="font-medium text-[var(--color-slate-12)]">
+        {candidate.name}
+      </span>
+    </div>
+  );
+});
diff --git a/apps/hire/src/app/kanban/KanbanColumn.tsx b/apps/hire/src/app/kanban/KanbanColumn.tsx
new file mode 100644
index 00000000..5dedae88
--- /dev/null
+++ b/apps/hire/src/app/kanban/KanbanColumn.tsx
@@ -0,0 +1,46 @@
+"use client";
+
+import React from "react";
+import { useDroppable } from "@dnd-kit/core";
+import {
+  SortableContext,
+  verticalListSortingStrategy,
+} from "@dnd-kit/sortable";
+import { KanbanCard } from "./KanbanCard";
+import { ZodCandidate } from "~/server/zod/candidate";
+
+interface KanbanColumnProps {
+  id: string;
+  title: string;
+  candidates: ZodCandidate[];
+}
+
+const getColumnStyles = (isOver: boolean) => {
+  const baseClasses =
+    "transition-border flex min-w-[250px] max-w-[300px] flex-1 flex-col rounded-md border-2 p-2 shadow-md";
+  const hoverClasses = isOver
+    ? "border-[var(--color-grass-9)] bg-[var(--color-olive-3)]"
+    : "border-[var(--color-slate-7)] bg-[var(--color-olive-2)]";
+  return `${baseClasses} ${hoverClasses}`;
+};
+
+export function KanbanColumn({ id, title, candidates }: KanbanColumnProps) {
+  const { setNodeRef, isOver } = useDroppable({ id });
+  return (
+    <div ref={setNodeRef} className={getColumnStyles(isOver)}>
+      <h2 className="mb-2 text-center text-lg font-bold text-[var(--color-slate-12)]">
+        {title}
+      </h2>
+      <SortableContext
+        items={candidates.map((c) => c._id)}
+        strategy={verticalListSortingStrategy}
+      >
+        <div className="flex flex-col gap-2">
+          {candidates.map((candidate) => (
+            <KanbanCard key={candidate._id} candidate={candidate} />
+          ))}
+        </div>
+      </SortableContext>
+    </div>
+  );
+}
diff --git a/apps/hire/src/app/kanban/page.tsx b/apps/hire/src/app/kanban/page.tsx
new file mode 100644
index 00000000..f3228546
--- /dev/null
+++ b/apps/hire/src/app/kanban/page.tsx
@@ -0,0 +1,23 @@
+"use client";
+
+import React from "react";
+import { KanbanBoard } from "./KanbanBoard";
+import { useOrganization } from "@clerk/nextjs";
+
+export default function KanbanPage() {
+  const { organization } = useOrganization();
+  const organizationId = organization?.id;
+  if (!organizationId) {
+    return (
+      <main className="p-6">
+        <div>Loading organization...</div>
+      </main>
+    );
+  }
+  return (
+    <main className="p-6">
+      <h1 className="mb-6 text-2xl font-bold">Candidate Kanban Board</h1>
+      <KanbanBoard organizationId={organizationId} />
+    </main>
+  );
+}
diff --git a/apps/hire/src/app/settings/_components/boards-tab.tsx b/apps/hire/src/app/settings/_components/boards-tab.tsx
new file mode 100644
index 00000000..e6565eb2
--- /dev/null
+++ b/apps/hire/src/app/settings/_components/boards-tab.tsx
@@ -0,0 +1,189 @@
+"use client";
+
+import {
+  Button,
+  Input,
+  Dialog,
+  DialogContent,
+  DialogHeader,
+  DialogTitle,
+  DialogFooter,
+  DialogClose,
+} from "@j5/component-library";
+import { useMutation, useQuery } from "convex/react";
+import { api } from "../../../../convex/_generated/api";
+import { useEffect, useState, useMemo } from "react";
+import type { Id } from "../../../../convex/_generated/dataModel";
+import { Trash2, Pencil } from "lucide-react";
+import { EditBoardSheet } from "./edit-board-sheet";
+import { ZodBoard } from "~/server/zod/board";
+
+// We might need a SortableList similar to SortableTagList if boards are orderable
+// For now, a simple list will do.
+
+export function BoardsTab({ orgId }: { orgId: string }) {
+  const [boardName, setBoardName] = useState("");
+  // Assuming a query like getBoardsByOrgId exists in your convex/boards.ts
+  const boards = useQuery(api.boards.getBoardsByOrgId, { orgId }) || [];
+  const allKanbanStages =
+    useQuery(api.kanbanStages.getKanbanStages, { orgId }) || []; // Fetch all stages
+  const [localBoards, setLocalBoards] = useState(boards);
+  const [isEditSheetOpen, setIsEditSheetOpen] = useState(false);
+  const [editingBoard, setEditingBoard] = useState<ZodBoard | null>(null);
+  const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
+  const [boardToDeleteId, setBoardToDeleteId] = useState<Id<"boards"> | null>(
+    null,
+  );
+
+  // Assuming a mutation like addBoard exists in your convex/boards.ts
+  const addBoard = useMutation(api.boards.addBoard);
+  const deleteBoardMutation = useMutation(api.boards.deleteBoard);
+  // TODO: add reorder functionality
+  // Placeholder for future mutations
+  // const reorderBoards = useMutation(api.boards.reorderBoards);
+
+  // Create a map for quick lookup of stage names by ID
+  const stageNameMap = useMemo(() => {
+    const map = new Map<Id<"kanbanStages">, string>();
+    allKanbanStages.forEach((stage) => {
+      map.set(stage._id, stage.name);
+    });
+    return map;
+  }, [allKanbanStages]);
+
+  useEffect(() => {
+    setLocalBoards(boards);
+  }, [boards]);
+
+  const handleAddBoard = async () => {
+    if (!boardName.trim()) return;
+    try {
+      // Assuming addBoard takes name and orgId
+      await addBoard({ name: boardName.trim(), orgId });
+      setBoardName("");
+    } catch (error) {
+      console.error("Failed to add board:", error);
+      // TODO: add toast
+    }
+  };
+
+  const handleDeleteBoard = (boardId: Id<"boards">) => {
+    setBoardToDeleteId(boardId);
+    setIsDeleteDialogOpen(true);
+  };
+
+  const executeDeleteBoard = async () => {
+    if (!boardToDeleteId || !orgId) return;
+    try {
+      await deleteBoardMutation({ boardId: boardToDeleteId, orgId });
+      // TODO: add toast success
+    } catch (error) {
+      console.error("Failed to delete board:", error);
+      // TODO: add toast error
+    }
+    setIsDeleteDialogOpen(false);
+    setBoardToDeleteId(null);
+  };
+
+  return (
+    <div className="space-y-4">
+      <h2 className="text-xl font-semibold">Manage Boards</h2>
+      <p className="text-muted-foreground text-sm">
+        Create and manage boards for your hiring pipelines.
+      </p>
+      <div className="flex items-center justify-between gap-4">
+        <Input
+          placeholder="New board name"
+          value={boardName}
+          onChange={(e) => setBoardName(e.target.value)}
+          onKeyDown={(e) => e.key === "Enter" && handleAddBoard()}
+          className="max-w-xs"
+        />
+        <Button onClick={handleAddBoard} disabled={!boardName.trim()}>
+          Add Board
+        </Button>
+      </div>
+      <div className="rounded-lg border">
+        {localBoards.length === 0 ? (
+          <p className="text-muted-foreground p-4 text-center">
+            No boards created yet.
+          </p>
+        ) : (
+          <ul className="divide-border divide-y">
+            {localBoards?.map((board) => (
+              <li
+                key={board._id}
+                className="flex items-center justify-between border-b p-2"
+              >
+                <div>
+                  <span className="font-medium">{board.name}</span>
+                  <span className="ml-2 text-sm text-gray-500">
+                    Slug: {board.slug}
+                  </span>
+                  <div className="ml-4 mt-1 text-xs text-gray-600">
+                    <span className="font-semibold">Stages: </span>
+                    {board.kanbanStageIds?.length > 0
+                      ? board.kanbanStageIds
+                          .map(
+                            (stageId) => stageNameMap.get(stageId) || stageId,
+                          )
+                          .join(", ")
+                      : "No stages"}
+                  </div>
+                </div>
+                <div className="space-x-2">
+                  <Button
+                    variant="outline"
+                    size="sm"
+                    onClick={() => {
+                      setEditingBoard(board);
+                      setIsEditSheetOpen(true);
+                    }}
+                    aria-label="Edit board"
+                  >
+                    <Pencil className="h-4 w-4" />
+                  </Button>
+                  <Button
+                    variant="destructive"
+                    size="sm"
+                    onClick={() => handleDeleteBoard(board._id)}
+                    aria-label="Delete board"
+                  >
+                    <Trash2 className="h-4 w-4" />
+                  </Button>
+                </div>
+              </li>
+            ))}
+          </ul>
+        )}
+      </div>
+      {editingBoard && (
+        <EditBoardSheet
+          isOpen={isEditSheetOpen}
+          onOpenChange={setIsEditSheetOpen}
+          board={editingBoard}
+          orgId={orgId}
+        />
+      )}
+      <Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
+        <DialogContent>
+          <DialogHeader>
+            <DialogTitle>Delete Board</DialogTitle>
+          </DialogHeader>
+          <p className="text-center">
+            Are you sure you want to delete this board? <br />
+            This action cannot be undone.
+          </p>
+          <DialogFooter>
+            <DialogClose asChild>
+              <Button variant="outline">Cancel</Button>
+            </DialogClose>
+            <Button variant="destructive" onClick={executeDeleteBoard}>
+              Delete
+            </Button>
+          </DialogFooter>
+        </DialogContent>
+      </Dialog>
+    </div>
+  );
+}
diff --git a/apps/hire/src/app/settings/_components/edit-board-sheet.tsx b/apps/hire/src/app/settings/_components/edit-board-sheet.tsx
new file mode 100644
index 00000000..a118d760
--- /dev/null
+++ b/apps/hire/src/app/settings/_components/edit-board-sheet.tsx
@@ -0,0 +1,129 @@
+"use client";
+
+import * as React from "react";
+import {
+  Button,
+  Sheet,
+  SheetClose,
+  SheetContent,
+  SheetDescription,
+  SheetFooter,
+  SheetHeader,
+  SheetTitle,
+  useAppForm,
+} from "@j5/component-library";
+import { useMutation, useQuery } from "convex/react";
+import { api } from "../../../../convex/_generated/api";
+import type { Doc } from "../../../../convex/_generated/dataModel";
+import { UpdateBoardSchema } from "../../../server/zod/board";
+
+export type EditBoardSheetProps = {
+  isOpen: boolean;
+  onOpenChange: (open: boolean) => void;
+  board: Doc<"boards">;
+  orgId: string;
+};
+
+export function EditBoardSheet({
+  isOpen,
+  onOpenChange,
+  board,
+  orgId,
+}: EditBoardSheetProps) {
+  const updateBoardMutation = useMutation(api.boards.updateBoard);
+  const allKanbanStages = useQuery(api.kanbanStages.getKanbanStages, { orgId });
+
+  const kanbanStageItems = React.useMemo(() => {
+    return (allKanbanStages || []).map((stage) => ({
+      id: stage._id,
+      label: stage.name,
+    }));
+  }, [allKanbanStages]);
+
+  const form = useAppForm({
+    defaultValues: {
+      _id: board._id,
+      name: board.name,
+      slug: board.slug,
+      kanbanStageIds: board.kanbanStageIds,
+    },
+    validators: {
+      onChange: ({ value }) => {
+        const result = UpdateBoardSchema.omit({ orgId: true }).safeParse(value);
+        if (!result.success) {
+          return result.error.flatten().fieldErrors;
+        }
+        return undefined;
+      },
+    },
+    onSubmit: async ({ value }) => {
+      try {
+        await updateBoardMutation({ orgId, ...value });
+        onOpenChange(false); // Close sheet on success
+      } catch (error) {
+        console.error("Failed to update board:", error);
+        // TODO: add toast
+      }
+    },
+  });
+
+  // Reset form if the board prop changes (e.g., user selects a different board to edit)
+  React.useEffect(() => {
+    form.reset({
+      _id: board._id,
+      name: board.name,
+      slug: board.slug,
+      kanbanStageIds: board.kanbanStageIds,
+    });
+  }, [board, form]);
+
+  return (
+    <Sheet open={isOpen} onOpenChange={onOpenChange}>
+      <SheetContent className="sm:max-w-lg">
+        <SheetHeader>
+          <SheetTitle>Edit Board</SheetTitle>
+          <SheetDescription>
+            Make changes to your board&apos;s details here. Click save when
+            you&apos;re done.
+          </SheetDescription>
+        </SheetHeader>
+        <form
+          onSubmit={(e) => {
+            e.preventDefault();
+            e.stopPropagation();
+            void form.handleSubmit();
+          }}
+          className="space-y-6 py-6"
+        >
+          <form.AppField name="name">
+            {(field) => <field.FieldInput label="Board Name" />}
+          </form.AppField>
+          <form.AppField name="slug">
+            {(field) => <field.FieldInput label="Slug" />}
+          </form.AppField>
+          <form.AppField name="kanbanStageIds">
+            {(field) => (
+              <field.FieldCheckboxGroup
+                label="Kanban Stages"
+                items={kanbanStageItems}
+                orientation="vertical"
+              />
+            )}
+          </form.AppField>
+          <SheetFooter className="mt-8">
+            <SheetClose asChild>
+              <Button type="button" variant="outline">
+                Cancel
+              </Button>
+            </SheetClose>
+            <form.AppForm>
+              <form.SubmitButton
+                label={form.state.isSubmitting ? "Saving..." : "Save Changes"}
+              />
+            </form.AppForm>
+          </SheetFooter>
+        </form>
+      </SheetContent>
+    </Sheet>
+  );
+}
diff --git a/apps/hire/src/app/settings/page.tsx b/apps/hire/src/app/settings/page.tsx
index e09a27a9..2977cdd2 100644
--- a/apps/hire/src/app/settings/page.tsx
+++ b/apps/hire/src/app/settings/page.tsx
@@ -1,7 +1,10 @@
+"use client";
+
 import { SeniorityTab } from "./_components/seniority-tab";
 import { RolesTab } from "./_components/roles-tab";
 import { SourcesTab } from "./_components/sources-tab";
 import { KanbanStagesTab } from "./_components/kanban-stages-tab";
+import { BoardsTab } from "./_components/boards-tab";
 import { useOrganization } from "@clerk/nextjs";
 
 export const dynamic = "force-dynamic";
@@ -15,7 +18,7 @@ export default function SettingsPage() {
       <div className="space-y-2">
         <h1 className="text-3xl font-bold tracking-tight">Settings</h1>
         <p className="text-muted-foreground">
-          Manage your company&apos;s hiring settings and configurations
+          Manage your organization settings and preferences.
         </p>
       </div>
 
@@ -23,6 +26,7 @@ export default function SettingsPage() {
       <RolesTab orgId={orgId} />
       <SourcesTab orgId={orgId} />
       <KanbanStagesTab orgId={orgId} />
+      <BoardsTab orgId={orgId} />
     </div>
   );
 }
diff --git a/apps/hire/src/components/candidate-form.tsx b/apps/hire/src/components/candidate-form.tsx
index bc321ad5..f41e8b62 100644
--- a/apps/hire/src/components/candidate-form.tsx
+++ b/apps/hire/src/components/candidate-form.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/no-children-prop */
 "use client";
 
 import {
@@ -68,23 +67,17 @@ export function CandidateForm({
         className="space-y-6"
       >
         <div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
-          <form.AppField
-            name="name"
-            children={(field) => <field.FieldInput label="Name" />}
-          />
-          <form.AppField
-            name="email"
-            children={(field) => (
-              <field.FieldInput type="email" label="Email" />
-            )}
-          />
-          <form.AppField
-            name="phone"
-            children={(field) => <field.FieldInput type="tel" label="Phone" />}
-          />
-          <form.AppField
-            name="seniorityId"
-            children={(field) => (
+          <form.AppField name="name">
+            {(field) => <field.FieldInput label="Name" />}
+          </form.AppField>
+          <form.AppField name="email">
+            {(field) => <field.FieldInput type="email" label="Email" />}
+          </form.AppField>
+          <form.AppField name="phone">
+            {(field) => <field.FieldInput type="tel" label="Phone" />}
+          </form.AppField>
+          <form.AppField name="seniorityId">
+            {(field) => (
               <field.FieldSelect label="Seniority">
                 <SelectTrigger>
                   <SelectValue placeholder="Select a seniority level" />
@@ -98,10 +91,9 @@ export function CandidateForm({
                 </SelectContent>
               </field.FieldSelect>
             )}
-          />
-          <form.AppField
-            name="sourceId"
-            children={(field) => (
+          </form.AppField>
+          <form.AppField name="sourceId">
+            {(field) => (
               <field.FieldSelect label="Source">
                 <SelectTrigger>
                   <SelectValue placeholder="Select a source" />
@@ -115,10 +107,9 @@ export function CandidateForm({
                 </SelectContent>
               </field.FieldSelect>
             )}
-          />
-          <form.AppField
-            name="kanbanStageId"
-            children={(field) => (
+          </form.AppField>
+          <form.AppField name="kanbanStageId">
+            {(field) => (
               <field.FieldSelect label="Kanban Stage">
                 <SelectTrigger>
                   <SelectValue placeholder="Select a kanban stage" />
@@ -132,24 +123,18 @@ export function CandidateForm({
                 </SelectContent>
               </field.FieldSelect>
             )}
-          />
-          <form.AppField
-            name="salaryExpectations"
-            children={(field) => (
-              <field.FieldInput label="Salary Expectations" />
-            )}
-          />
-          <form.AppField
-            name="nextSteps"
-            children={(field) => <field.FieldInput label="Next Steps" />}
-          />
-          <form.AppField
-            name="targetTeam"
-            children={(field) => <field.FieldInput label="Target Team" />}
-          />
-          <form.AppField
-            name="roleId"
-            children={(field) => (
+          </form.AppField>
+          <form.AppField name="salaryExpectations">
+            {(field) => <field.FieldInput label="Salary Expectations" />}
+          </form.AppField>
+          <form.AppField name="nextSteps">
+            {(field) => <field.FieldInput label="Next Steps" />}
+          </form.AppField>
+          <form.AppField name="targetTeam">
+            {(field) => <field.FieldInput label="Target Team" />}
+          </form.AppField>
+          <form.AppField name="roleId">
+            {(field) => (
               <field.FieldSelect label="Role">
                 <SelectTrigger>
                   <SelectValue placeholder="Select a role" />
@@ -163,7 +148,7 @@ export function CandidateForm({
                 </SelectContent>
               </field.FieldSelect>
             )}
-          />
+          </form.AppField>
         </div>
 
         <div className="space-y-2">
diff --git a/apps/hire/src/server/zod/board.ts b/apps/hire/src/server/zod/board.ts
new file mode 100644
index 00000000..d2e02084
--- /dev/null
+++ b/apps/hire/src/server/zod/board.ts
@@ -0,0 +1,33 @@
+import { z } from "zod";
+import { baseConvexFields } from "./utils";
+import { CompanyIdSchema } from "./company";
+import { KanbanStageIdSchema } from "./kanbanStage";
+
+export const BoardSchema = z.object({
+  ...baseConvexFields("boards"),
+  companyId: CompanyIdSchema,
+  kanbanStageIds: z.array(KanbanStageIdSchema),
+  name: z.string().min(1),
+  slug: z.string().min(1),
+  order: z.number(),
+});
+
+export const CreateBoardSchema = z.object({
+  name: z.string().min(1),
+  orgId: z.string(),
+});
+
+export const UpdateBoardSchema = BoardSchema.omit({
+  _creationTime: true,
+  companyId: true,
+  order: true,
+}).extend({
+  orgId: z.string(),
+});
+
+export const DeleteBoardSchema = z.object({
+  boardId: BoardSchema.shape._id,
+  orgId: z.string(),
+});
+
+export type ZodBoard = z.infer<typeof BoardSchema>;
diff --git a/apps/hire/src/server/zod/candidate.ts b/apps/hire/src/server/zod/candidate.ts
index 19cf8948..45ee3e8b 100644
--- a/apps/hire/src/server/zod/candidate.ts
+++ b/apps/hire/src/server/zod/candidate.ts
@@ -35,6 +35,11 @@ export const UpdateCandidateSchema = CandidateSchema.omit({
   _creationTime: true,
 });
 
+export const UpdateCandidateStageSchema = z.object({
+  candidateId: CandidateIdSchema,
+  kanbanStageId: zid("kanbanStages"),
+});
+
 export type CandidateId = z.infer<typeof CandidateIdSchema>;
 export type ZodCandidate = z.infer<typeof CandidateSchema>;
 export type ZodCreateCandidate = z.infer<typeof CreateCandidateSchema>;
diff --git a/libs/component-library/src/components/badge/badge.stories.tsx b/libs/component-library/src/components/badge/badge.stories.tsx
new file mode 100644
index 00000000..9929b65d
--- /dev/null
+++ b/libs/component-library/src/components/badge/badge.stories.tsx
@@ -0,0 +1,117 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import { Badge, BadgeProps, badgeVariants } from "./badge";
+import {
+  Info,
+  CheckCircle,
+  AlertTriangle,
+  XCircle,
+  Sparkles,
+  Tag as TagIcon,
+} from "lucide-react";
+import React from "react";
+
+const meta: Meta<typeof Badge> = {
+  title: "Components/Badge",
+  component: Badge,
+  parameters: {
+    layout: "centered",
+  },
+  tags: ["autodocs"],
+  argTypes: {
+    variant: {
+      control: "select",
+      options: Object.keys(badgeVariants.variant),
+    },
+    shape: {
+      control: "select",
+      options: Object.keys(badgeVariants.shape),
+    },
+    children: {
+      control: "text",
+    },
+    asChild: {
+      control: "boolean",
+    },
+    className: { control: false },
+  },
+};
+
+export default meta;
+type Story = StoryObj<typeof meta>;
+
+// Helper to get an appropriate icon for a variant
+const getIconForVariant = (variant: keyof typeof badgeVariants.variant) => {
+  if (variant.includes("green")) return <CheckCircle />;
+  if (variant.includes("red")) return <XCircle />;
+  if (variant.includes("amber")) return <AlertTriangle />;
+  if (variant.includes("blue")) return <Info />;
+  if (variant.includes("grass")) return <Sparkles />;
+  return <TagIcon />;
+};
+
+const shapes = Object.keys(
+  badgeVariants.shape,
+) as (keyof typeof badgeVariants.shape)[];
+
+const VariantDisplay = ({
+  variant,
+  ...args
+}: Omit<BadgeProps, "variant"> & {
+  variant: keyof typeof badgeVariants.variant;
+}) => {
+  return (
+    <>
+      <h2 className="border-slate-6 m-0 w-full border-b pb-2 text-lg font-semibold capitalize">
+        {variant.replace(/([A-Z])/g, " $1")}{" "}
+        {/* Add space before capital letters for display */}
+      </h2>
+      <div className="flex flex-row flex-wrap items-center gap-4">
+        {shapes.map((shape) => (
+          <React.Fragment key={`${variant}-${shape}`}>
+            <Badge {...args} variant={variant} shape={shape}>
+              {shape} Badge
+            </Badge>
+            <Badge {...args} variant={variant} shape={shape}>
+              {React.cloneElement(getIconForVariant(variant), {
+                className: "size-3",
+              })}
+              <span>{shape} Icon</span>
+            </Badge>
+          </React.Fragment>
+        ))}
+      </div>
+    </>
+  );
+};
+export const AllVariants: Story = {
+  parameters: {
+    controls: { hideNoControlsWarning: true, sort: "requiredFirst" },
+  },
+  render: (args) => {
+    const variants = Object.keys(badgeVariants.variant).filter(
+      (variant) => !variant.includes("Outline"),
+    ) as Array<keyof typeof badgeVariants.variant>;
+    return (
+      <div className="text-slate-11 flex w-full flex-col items-start gap-10 p-5">
+        {variants.map((variant) => (
+          <div
+            key={variant}
+            className="grid w-full grid-cols-2 items-start gap-5"
+          >
+            <div className="col-span-1 flex flex-col gap-4">
+              <VariantDisplay variant={variant} {...args} />
+            </div>
+            <div className="col-span-1 flex flex-col gap-4">
+              <VariantDisplay
+                variant={
+                  `${variant}Outline` as keyof typeof badgeVariants.variant
+                }
+                {...args}
+              />
+            </div>
+          </div>
+        ))}
+      </div>
+    );
+  },
+};
diff --git a/libs/component-library/src/components/badge/badge.tsx b/libs/component-library/src/components/badge/badge.tsx
new file mode 100644
index 00000000..98df6ed1
--- /dev/null
+++ b/libs/component-library/src/components/badge/badge.tsx
@@ -0,0 +1,68 @@
+import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "../../lib/utils";
+
+export const badgeVariants = {
+  variant: {
+    // Neutrals
+    olive: "bg-olive-3 text-olive-11",
+    oliveOutline: "border border-olive-7 text-olive-11 bg-transparent",
+    slate: "bg-slate-3 text-slate-11",
+    slateOutline: "border border-slate-7 text-slate-11 bg-transparent",
+    // Brand Accent
+    grass: "bg-grass-3 text-grass-11",
+    grassOutline: "border border-grass-7 text-grass-11 bg-transparent",
+    // Semantic
+    green: "bg-green-3 text-green-11", // Success
+    greenOutline: "border border-green-7 text-green-11 bg-transparent",
+    red: "bg-red-3 text-red-11", // Error
+    redOutline: "border border-red-7 text-red-11 bg-transparent",
+    amber: "bg-amber-3 text-amber-11", // Warning
+    amberOutline: "border border-amber-7 text-amber-11 bg-transparent",
+    blue: "bg-blue-3 text-blue-11", // Info
+    blueOutline: "border border-blue-7 text-blue-11 bg-transparent",
+  },
+  shape: {
+    default: "rounded-sm",
+    pill: "rounded-full",
+  },
+};
+
+const badgeClassName = cva(
+  "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap border border-transparent px-2 py-0.5 font-sans text-xs font-medium [&>svg]:pointer-events-none [&>svg]:size-3",
+  {
+    variants: badgeVariants,
+    defaultVariants: {
+      variant: "olive",
+      shape: "default",
+    },
+  },
+);
+
+export interface BadgeProps
+  extends React.HTMLAttributes<HTMLSpanElement>,
+    VariantProps<typeof badgeClassName> {
+  asChild?: boolean;
+}
+
+function Badge({
+  className,
+  variant,
+  shape,
+  asChild = false,
+  ...props
+}: BadgeProps) {
+  const Comp = asChild ? Slot : "span";
+
+  return (
+    <Comp
+      data-slot="badge"
+      className={cn(badgeClassName({ variant, shape }), className)}
+      {...props}
+    />
+  );
+}
+
+export { Badge };
diff --git a/libs/component-library/src/components/checkbox/field-checkbox-group.tsx b/libs/component-library/src/components/checkbox/field-checkbox-group.tsx
new file mode 100644
index 00000000..3b78a9f4
--- /dev/null
+++ b/libs/component-library/src/components/checkbox/field-checkbox-group.tsx
@@ -0,0 +1,102 @@
+"use client";
+
+import React, { useCallback } from "react";
+import { Checkbox, type CheckboxProps } from "./checkbox";
+import { useFieldContext } from "../form/form-contexts";
+import { Label } from "../label/label";
+import { cn } from "../../lib/utils";
+
+export interface CheckboxGroupItem {
+  id: string;
+  label: string;
+  // Allow any other props, e.g., disabled, custom styling, etc.
+  [key: string]: any;
+}
+
+export interface FieldCheckboxGroupProps
+  extends Omit<
+    CheckboxProps,
+    "value" | "onCheckedChange" | "checked" | "defaultChecked" | "id"
+  > {
+  label: string;
+  items: CheckboxGroupItem[];
+  orientation?: "vertical" | "horizontal";
+  itemClassName?: string;
+  labelClassName?: string;
+}
+
+export const FieldCheckboxGroup = React.forwardRef<
+  HTMLDivElement,
+  FieldCheckboxGroupProps
+>(
+  (
+    {
+      label,
+      items,
+      className,
+      itemClassName,
+      labelClassName,
+      orientation = "vertical",
+      ...props // Passes common CheckboxProps like 'size' to individual checkboxes
+    },
+    ref,
+  ) => {
+    const field = useFieldContext<string[]>(); // Expects the field value to be an array of strings (IDs)
+
+    const fieldItems = field.state.value || [];
+
+    const handleCheckedChange = useCallback(
+      (itemId: string, checked: boolean) => {
+        let newValue;
+        if (checked) {
+          newValue = [...fieldItems, itemId];
+        } else {
+          newValue = fieldItems.filter((id) => id !== itemId);
+        }
+        field.handleChange(newValue);
+      },
+      [fieldItems, field.handleChange],
+    );
+
+    return (
+      <div ref={ref} className={cn("space-y-2", className)} role="group">
+        <Label htmlFor={field.name} className={labelClassName}>
+          {label}
+        </Label>
+        <div
+          className={cn(
+            "flex",
+            orientation === "vertical"
+              ? "flex-col space-y-2"
+              : "flex-row space-x-4",
+          )}
+        >
+          {items.map((item) => (
+            <div
+              key={item.id}
+              className={cn("flex items-center", itemClassName)}
+            >
+              <Checkbox
+                {...props} // Pass down props like 'size'
+                id={`${field.name}-${item.id}`}
+                checked={fieldItems.includes(item.id)}
+                onCheckedChange={(checked) => {
+                  handleCheckedChange(item.id, Boolean(checked));
+                }}
+                onBlur={field.handleBlur} // Apply blur to the group or individual items as needed
+                aria-invalid={field.state.meta.errors.length > 0}
+              />
+              <Label
+                htmlFor={`${field.name}-${item.id}`}
+                className="ml-2 text-sm font-normal"
+              >
+                {item.label}
+              </Label>
+            </div>
+          ))}
+        </div>
+        {/* TODO: Consider how to display field errors for the group if needed */}
+      </div>
+    );
+  },
+);
diff --git a/libs/component-library/src/components/checkbox/index.ts b/libs/component-library/src/components/checkbox/index.ts
new file mode 100644
index 00000000..1ec45619
--- /dev/null
+++ b/libs/component-library/src/components/checkbox/index.ts
@@ -0,0 +1,2 @@
+export * from "./checkbox";
+export * from "./field-checkbox-group";
diff --git a/libs/component-library/src/components/form/use-app-form.ts b/libs/component-library/src/components/form/use-app-form.ts
index c9cb4a1e..16537806 100644
--- a/libs/component-library/src/components/form/use-app-form.ts
+++ b/libs/component-library/src/components/form/use-app-form.ts
@@ -5,6 +5,7 @@ import { fieldContext, formContext } from "./form-contexts";
 import { FieldInput } from "../input/field-input";
 import { FieldTextarea } from "../textarea/field-textarea";
 import { FieldSelect } from "../select/field-select";
+import { FieldCheckboxGroup } from "../checkbox/field-checkbox-group";
 import { SubmitButton } from "../button/submit-button";
 
 export const { useAppForm } = createFormHook({
@@ -12,6 +13,7 @@ export const { useAppForm } = createFormHook({
     FieldInput,
     FieldTextarea,
     FieldSelect,
+    FieldCheckboxGroup,
   },
   formComponents: {
     SubmitButton,
diff --git a/libs/component-library/src/components/index.ts b/libs/component-library/src/components/index.ts
index b34b9f0d..89abbbc1 100644
--- a/libs/component-library/src/components/index.ts
+++ b/libs/component-library/src/components/index.ts
@@ -2,7 +2,6 @@ export * from "./button/button";
 export * from "./button/submit-button";
 export * from "./form/use-app-form";
 export * from "./form/form-contexts";
-export * from "./checkbox/checkbox";
 export * from "./input/input";
 export * from "./input/field-input";
 export * from "./label/label";
@@ -17,3 +16,5 @@ export * from "./tag/tag";
 export * from "./sortable-list/sortable-list";
 export * from "./select/select";
 export * from "./select/field-select";
+export * from "./sheet/sheet";
+export * from "./checkbox";
diff --git a/libs/component-library/src/components/sheet/sheet.stories.tsx b/libs/component-library/src/components/sheet/sheet.stories.tsx
new file mode 100644
index 00000000..ddc574a8
--- /dev/null
+++ b/libs/component-library/src/components/sheet/sheet.stories.tsx
@@ -0,0 +1,149 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import { userEvent, within, expect, screen, waitFor } from "@storybook/test";
+import { Button } from "../button/button";
+import { Label } from "../label/label";
+import { Input } from "../input/input";
+import {
+  Sheet,
+  SheetClose,
+  SheetContent,
+  SheetDescription,
+  SheetFooter,
+  SheetHeader,
+  SheetTitle,
+  SheetTrigger,
+} from "./sheet";
+
+const meta: Meta<typeof Sheet> = {
+  title: "Components/Sheet",
+  component: Sheet,
+  parameters: {
+    layout: "centered",
+  },
+  tags: ["autodocs"],
+};
+
+export default meta;
+type Story = StoryObj<typeof Sheet>;
+
+const SheetDemoContent = () => (
+  <>
+    <SheetHeader>
+      <SheetTitle>Edit Profile</SheetTitle>
+      <SheetDescription>
+        Make changes to your profile here. Click save when you're done.
+      </SheetDescription>
+    </SheetHeader>
+    <div className="grid gap-4 py-4">
+      <div className="grid grid-cols-4 items-center gap-4">
+        <Label htmlFor="name" className="text-right">
+          Name
+        </Label>
+        <Input id="name" defaultValue="Pedro Duarte" className="col-span-3" />
+      </div>
+      <div className="grid grid-cols-4 items-center gap-4">
+        <Label htmlFor="username" className="text-right">
+          Username
+        </Label>
+        <Input id="username" defaultValue="@peduarte" className="col-span-3" />
+      </div>
+    </div>
+    <SheetFooter>
+      <SheetClose asChild>
+        <Button type="submit">Save changes</Button>
+      </SheetClose>
+    </SheetFooter>
+  </>
+);
+
+export const Default: Story = {
+  render: () => (
+    <div className="grid grid-cols-2 gap-4">
+      <Sheet>
+        <SheetTrigger asChild>
+          <Button variant="outline">Open Sheet</Button>
+        </SheetTrigger>
+        <SheetContent>
+          <SheetDemoContent />
+        </SheetContent>
+      </Sheet>
+    </div>
+  ),
+
+  play: async ({ canvasElement }) => {
+    const canvas = within(canvasElement);
+
+    // Test opening and closing each sheet variant
+    const triggerButton = await canvas.findByRole("button", {
+      name: `Open Sheet`,
+    });
+    await userEvent.click(triggerButton);
+    // Wait for sheet content to be visible and animations to complete
+    const sheetContent = await screen.findByText(`Edit Profile`);
+    await expect(sheetContent).toBeVisible();
+  },
+};
+
+export const InteractionTest: Story = {
+  name: "Open and Close Interaction",
+  args: {},
+  render: (args) => (
+    <Sheet {...args}>
+      <SheetTrigger asChild>
+        <Button variant="outline">Open Sheet</Button>
+      </SheetTrigger>
+      <SheetContent>
+        <SheetHeader>
+          <SheetTitle>Test Sheet</SheetTitle>
+          <SheetDescription>This is a test sheet.</SheetDescription>
+        </SheetHeader>
+        <div className="p-4">Your sheet content here.</div>
+        <SheetFooter>
+          <SheetClose asChild>
+            <Button variant="secondary">Cancel</Button>
+          </SheetClose>
+          <Button>Save</Button>
+        </SheetFooter>
+      </SheetContent>
+    </Sheet>
+  ),
+  play: async ({ canvasElement, step }) => {
+    const canvas = within(canvasElement);
+    const triggerButton = await canvas.findByRole("button", {
+      name: /open sheet/i,
+    });
+
+    // Open the sheet
+    await step("Open sheet", async () => {
+      await userEvent.click(triggerButton);
+      const sheetTitle = await screen.findByText("Test Sheet");
+      await expect(sheetTitle).toBeVisible();
+    });
+
+    // Close the sheet using the X button
+    await step("Close sheet with X button", async () => {
+      const sheetContent = await screen.findByText("Test Sheet");
+      const closeButton = await within(
+        sheetContent.closest('[data-slot="sheet-content"]')!.parentElement!,
+      ).findByRole("button", { name: /close/i });
+      await userEvent.click(closeButton);
+      await waitFor(() => expect(screen.queryByText("Test Sheet")).toBeNull());
+    });
+
+    // Re-open the sheet
+    await step("Re-open sheet", async () => {
+      await userEvent.click(triggerButton);
+      const sheetTitle = await screen.findByText("Test Sheet");
+      await expect(sheetTitle).toBeVisible();
+    });
+
+    // Close the sheet using the Cancel button in the footer
+    await step("Close sheet with Cancel button", async () => {
+      const cancelButton = await screen.findByRole("button", {
+        name: /cancel/i,
+      });
+      await userEvent.click(cancelButton);
+      await waitFor(() => expect(screen.queryByText("Test Sheet")).toBeNull());
+    });
+  },
+};
diff --git a/libs/component-library/src/components/sheet/sheet.tsx b/libs/component-library/src/components/sheet/sheet.tsx
new file mode 100644
index 00000000..3da65284
--- /dev/null
+++ b/libs/component-library/src/components/sheet/sheet.tsx
@@ -0,0 +1,147 @@
+"use client";
+
+import * as React from "react";
+import * as SheetPrimitive from "@radix-ui/react-dialog";
+import { XIcon } from "lucide-react";
+
+import { cn } from "../../lib/utils";
+
+function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
+  return <SheetPrimitive.Root data-slot="sheet" {...props} />;
+}
+
+function SheetTrigger({
+  ...props
+}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
+  return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
+}
+
+function SheetClose({
+  ...props
+}: React.ComponentProps<typeof SheetPrimitive.Close>) {
+  return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
+}
+
+function SheetPortal({
+  ...props
+}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
+  return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
+}
+
+function SheetOverlay({
+  className,
+  ...props
+}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
+  return (
+    <SheetPrimitive.Overlay
+      data-slot="sheet-overlay"
+      className={cn(
+        "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 bg-olive-a9 fixed inset-0 z-50",
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function SheetContent({
+  className,
+  children,
+  side = "right",
+  ...props
+}: React.ComponentProps<typeof SheetPrimitive.Content> & {
+  side?: "top" | "right" | "bottom" | "left";
+}) {
+  return (
+    <SheetPortal>
+      <SheetOverlay />
+      <SheetPrimitive.Content
+        data-slot="sheet-content"
+        className={cn(
+          "border-olive-6 bg-olive-3 text-olive-11 data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 p-6 shadow-xl transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
+          side === "right" &&
+            "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 rounded-l-lg border-l sm:max-w-sm",
+          side === "left" &&
+            "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 rounded-r-lg border-r sm:max-w-sm",
+          side === "top" &&
+            "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto rounded-b-lg border-b",
+          side === "bottom" &&
+            "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto rounded-t-lg border-t",
+          className,
+        )}
+        {...props}
+      >
+        {children}
+        <SheetPrimitive.Close className="ring-offset-olive-3 focus:ring-blue-8/50 data-[state=open]:bg-secondary rounded-xs text-olive-11 hover:text-olive-12 absolute right-4 top-4 opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
+          <XIcon className="size-4" />
+          <span className="sr-only">Close</span>
+        </SheetPrimitive.Close>
+      </SheetPrimitive.Content>
+    </SheetPortal>
+  );
+}
+
+function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="sheet-header"
+      className={cn(
+        "border-olive-6 flex flex-col gap-1.5 border-b pb-4",
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="sheet-footer"
+      className={cn(
+        "border-olive-6 mt-auto flex flex-col gap-2 border-t pt-4",
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function SheetTitle({
+  className,
+  ...props
+}: React.ComponentProps<typeof SheetPrimitive.Title>) {
+  return (
+    <SheetPrimitive.Title
+      data-slot="sheet-title"
+      className={cn("text-olive-12 font-sans text-lg font-medium", className)}
+      {...props}
+    />
+  );
+}
+
+function SheetDescription({
+  className,
+  ...props
+}: React.ComponentProps<typeof SheetPrimitive.Description>) {
+  return (
+    <SheetPrimitive.Description
+      data-slot="sheet-description"
+      className={cn("text-olive-11 font-sans text-sm", className)}
+      {...props}
+    />
+  );
+}
+
+export {
+  Sheet,
+  SheetTrigger,
+  SheetClose,
+  SheetPortal,
+  SheetOverlay,
+  SheetContent,
+  SheetHeader,
+  SheetFooter,
+  SheetTitle,
+  SheetDescription,
+};
diff --git a/libs/component-library/tsconfig.storybook.json b/libs/component-library/tsconfig.storybook.json
index dfa0821e..bada68fd 100644
--- a/libs/component-library/tsconfig.storybook.json
+++ b/libs/component-library/tsconfig.storybook.json
@@ -30,10 +30,5 @@
     {
       "path": "./tsconfig.lib.json"
     }
-  ],
-  "files": [
-    "../../node_modules/@nx/react/typings/styled-jsx.d.ts",
-    "../../node_modules/@nx/react/typings/cssmodule.d.ts",
-    "../../node_modules/@nx/react/typings/image.d.ts"
   ]
 }
diff --git a/package.json b/package.json
index 3ef487d1..43366819 100644
--- a/package.json
+++ b/package.json
@@ -58,14 +58,14 @@
     "@vitest/coverage-v8": "3.1.4",
     "@vitest/ui": "3.1.4",
     "autoprefixer": "10.4.21",
-    "chromatic": "11.29.0",
+    "chromatic": "12.0.0",
     "esbuild": "0.25.4",
     "eslint": "9.27.0",
     "eslint-config-next": "15.3.2",
     "eslint-config-prettier": "10.1.5",
     "eslint-plugin-import": "2.31.0",
     "eslint-plugin-jsx-a11y": "6.10.2",
-    "eslint-plugin-playwright": "1.8.3",
+    "eslint-plugin-playwright": "2.2.0",
     "eslint-plugin-react": "7.37.5",
     "eslint-plugin-react-hooks": "5.2.0",
     "jiti": "2.4.2",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 519c0e78..cbe790a2 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -157,8 +157,8 @@ importers:
         specifier: 10.4.21
         version: 10.4.21(postcss@8.5.3)
       chromatic:
-        specifier: 11.29.0
-        version: 11.29.0
+        specifier: 12.0.0
+        version: 12.0.0
       esbuild:
         specifier: 0.25.4
         version: 0.25.4
@@ -178,8 +178,8 @@ importers:
         specifier: 6.10.2
         version: 6.10.2(eslint@9.27.0(jiti@2.4.2))
       eslint-plugin-playwright:
-        specifier: 1.8.3
-        version: 1.8.3(eslint@9.27.0(jiti@2.4.2))
+        specifier: 2.2.0
+        version: 2.2.0(eslint@9.27.0(jiti@2.4.2))
       eslint-plugin-react:
         specifier: 7.37.5
         version: 7.37.5(eslint@9.27.0(jiti@2.4.2))
@@ -4648,8 +4648,8 @@ packages:
     resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
     engines: {node: '>=18'}
 
-  chromatic@11.29.0:
-    resolution: {integrity: sha512-yisBlntp9hHVj19lIQdpTlcYIXuU9H/DbFuu6tyWHmj6hWT2EtukCCcxYXL78XdQt1vm2GfIrtgtKpj/Rzmo4A==}
+  chromatic@12.0.0:
+    resolution: {integrity: sha512-X7v26BLfAzHCU92nxPsCuvt+MazmA7S6cN7pCRRhnDYChDxHES48YvXarOybLdlH9kNP/3lMyknu6iseTh0T9A==}
     hasBin: true
     peerDependencies:
       '@chromatic-com/cypress': ^0.*.* || ^1.0.0
@@ -5477,15 +5477,11 @@ packages:
     peerDependencies:
       eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9
 
-  eslint-plugin-playwright@1.8.3:
-    resolution: {integrity: sha512-h87JPFHkz8a6oPhn8GRGGhSQoAJjx0AkOv1jME6NoMk2FpEsfvfJJNaQDxLSqSALkCr0IJXPGTnp6SIRVu5Nqg==}
+  eslint-plugin-playwright@2.2.0:
+    resolution: {integrity: sha512-qSQpAw7RcSzE3zPp8FMGkthaCWovHZ/BsXtpmnGax9vQLIovlh1bsZHEa2+j2lv9DWhnyeLM/qZmp7ffQZfQvg==}
     engines: {node: '>=16.6.0'}
     peerDependencies:
       eslint: '>=8.40.0'
-      eslint-plugin-jest: '>=25'
-    peerDependenciesMeta:
-      eslint-plugin-jest:
-        optional: true
 
   eslint-plugin-react-hooks@5.2.0:
     resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==}
@@ -14304,7 +14300,7 @@ snapshots:
 
   chownr@3.0.0: {}
 
-  chromatic@11.29.0: {}
+  chromatic@12.0.0: {}
 
   chrome-trace-event@1.0.4: {}
 
@@ -15211,7 +15207,7 @@ snapshots:
       safe-regex-test: 1.1.0
       string.prototype.includes: 2.0.1
 
-  eslint-plugin-playwright@1.8.3(eslint@9.27.0(jiti@2.4.2)):
+  eslint-plugin-playwright@2.2.0(eslint@9.27.0(jiti@2.4.2)):
     dependencies:
       eslint: 9.27.0(jiti@2.4.2)
       globals: 13.24.0


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 8**

<details>
<summary>🧹 Nitpick comments (14)</summary><blockquote>

<details>
<summary>libs/component-library/src/components/badge/badge.tsx (1)</summary><blockquote>

`7-32`: **Consider consolidating variant definitions for better maintainability.**

The `badgeVariants` object structure is well-organized with clear semantic groupings. However, there's a potential for inconsistency between filled and outline variants.



Consider defining the variants more systematically to ensure outline variants always have corresponding filled variants:

```diff
export const badgeVariants = {
  variant: {
-    // Neutrals
-    olive: "bg-olive-3 text-olive-11",
-    slate: "bg-slate-3 text-slate-11",
-    // Brand Accent
-    grass: "bg-grass-3 text-grass-11",
-    // Semantic
-    green: "bg-green-3 text-green-11", // Success
-    red: "bg-red-3 text-red-11", // Error
-    amber: "bg-amber-3 text-amber-11", // Warning
-    blue: "bg-blue-3 text-blue-11", // Info
-    // Outlined variants
-    oliveOutline: "border-olive-7 text-olive-11",
-    slateOutline: "border-slate-7 text-slate-11",
-    grassOutline: "border-grass-7 text-grass-11",
-    greenOutline: "border-green-7 text-green-11",
-    redOutline: "border-red-7 text-red-11",
-    amberOutline: "border-amber-7 text-amber-11",
-    blueOutline: "border-blue-7 text-blue-11",
+    // Neutrals
+    olive: "bg-olive-3 text-olive-11",
+    oliveOutline: "border border-olive-7 text-olive-11 bg-transparent",
+    slate: "bg-slate-3 text-slate-11",
+    slateOutline: "border border-slate-7 text-slate-11 bg-transparent",
+    // Brand Accent
+    grass: "bg-grass-3 text-grass-11",
+    grassOutline: "border border-grass-7 text-grass-11 bg-transparent",
+    // Semantic
+    green: "bg-green-3 text-green-11", // Success
+    greenOutline: "border border-green-7 text-green-11 bg-transparent",
+    red: "bg-red-3 text-red-11", // Error
+    redOutline: "border border-red-7 text-red-11 bg-transparent",
+    amber: "bg-amber-3 text-amber-11", // Warning
+    amberOutline: "border border-amber-7 text-amber-11 bg-transparent",
+    blue: "bg-blue-3 text-blue-11", // Info
+    blueOutline: "border border-blue-7 text-blue-11 bg-transparent",
  },
```

This ensures outline variants explicitly set `bg-transparent` and `border` classes for consistency.

</blockquote></details>
<details>
<summary>libs/component-library/src/components/badge/badge.stories.tsx (1)</summary><blockquote>

`42-50`: **Improve icon mapping robustness.**

The `getIconForVariant` function uses string matching which could be fragile if variant names change.



Consider using a more explicit mapping approach:

```diff
-const getIconForVariant = (variant: keyof typeof badgeVariants.variant) => {
-  if (variant.includes("green")) return <CheckCircle />;
-  if (variant.includes("red")) return <XCircle />;
-  if (variant.includes("amber")) return <AlertTriangle />;
-  if (variant.includes("blue")) return <Info />;
-  if (variant.includes("grass")) return <Sparkles />;
-  return <TagIcon />;
-};
+const getIconForVariant = (variant: keyof typeof badgeVariants.variant) => {
+  const iconMap: Record<string, React.ReactElement> = {
+    green: <CheckCircle />,
+    greenOutline: <CheckCircle />,
+    red: <XCircle />,
+    redOutline: <XCircle />,
+    amber: <AlertTriangle />,
+    amberOutline: <AlertTriangle />,
+    blue: <Info />,
+    blueOutline: <Info />,
+    grass: <Sparkles />,
+    grassOutline: <Sparkles />,
+  };
+  return iconMap[variant] || <TagIcon />;
+};
```

</blockquote></details>
<details>
<summary>apps/hire/convex/schema.ts (1)</summary><blockquote>

`5-11`: **Well-designed schema addition.**

The new `boards` table is well-structured and follows existing patterns in the schema:

✅ Proper foreign key references to `companies` and `kanbanStages`
✅ Appropriate index on `companyId` and `order` for efficient querying
✅ Consistent naming and field types with other tables

Consider adding uniqueness constraints if needed:
- Should `slug` be unique within a company?
- Should `name` be unique within a company?




If uniqueness is required, you could add constraints like:
```diff
  boards: defineTable({
    companyId: v.id("companies"),
    kanbanStageIds: v.array(v.id("kanbanStages")),
    name: v.string(),
    order: v.float64(),
    slug: v.string(),
  }).index("by_company_order", ["companyId", "order"])
+   .index("by_company_slug", ["companyId", "slug"])
+   .index("by_company_name", ["companyId", "name"]),
```

</blockquote></details>
<details>
<summary>TODO.md (1)</summary><blockquote>

`9-9`: **Fix abbreviation formatting.**

Good addition of the cascading deletion concern - this is an important data integrity consideration for the new boards feature. However, please fix the grammatical issue:



```diff
- Deal with cascading deletion (eg. deleting a kanban stage when there are boards using it)
+ Deal with cascading deletion (e.g. deleting a kanban stage when there are boards using it)
```

The abbreviation "e.g." (exempli gratia) requires periods after each letter.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~9-~9: The abbreviation “e.g.” (= for example) requires two periods.
Context: ...s occur - Deal with cascading deletion (eg. deleting a kanban stage when there are ...

(E_G)

</details>

</details>

</blockquote></details>
<details>
<summary>apps/hire/src/app/kanban/page.tsx (1)</summary><blockquote>

`7-19`: **Consider improving the loading experience.**

The component structure is clean and follows React patterns well. However, the early return with `null` when no organization ID is available might not provide the best user experience.



Consider adding a loading indicator or informative message:

```diff
  if (!organizationId) {
-    return null;
+    return (
+      <main className="p-6">
+        <div>Loading organization...</div>
+      </main>
+    );
  }
```

Alternatively, you could use Clerk's `isLoaded` property to distinguish between loading and missing organization states.

</blockquote></details>
<details>
<summary>apps/hire/src/app/kanban/KanbanCard.tsx (2)</summary><blockquote>

`23-23`: **Consider extracting cursor style to CSS.**

While the inline cursor style works, consider moving it to the className for better maintainability:



```diff
-    cursor: "grab",
+    cursor: isDragging ? "grabbing" : "grab",
```

And add cursor styles to the className instead, or create a dedicated CSS class for draggable elements.

---

`7-30`: **Consider memoization for performance optimization.**

With potentially many candidate cards rendered simultaneously, consider memoizing this component to prevent unnecessary re-renders:



```diff
+import React, { memo } from "react";

-export function KanbanCard({ candidate }: { candidate: ZodCandidate }) {
+export const KanbanCard = memo(function KanbanCard({ candidate }: { candidate: ZodCandidate }) {
  // ... component implementation
-}
+});
```

This would be particularly beneficial when the kanban board contains many candidates.

</blockquote></details>
<details>
<summary>apps/hire/src/app/kanban/KanbanColumn.tsx (1)</summary><blockquote>

`21-24`: **Improve className readability.**

The conditional className string is very long and hard to read. Consider extracting it to a helper function or using a library like `clsx`.



```diff
+ const getColumnStyles = (isOver: boolean) => {
+   const baseClasses = "transition-border flex min-w-[250px] max-w-[300px] flex-1 flex-col rounded-md border-2 p-2 shadow-md";
+   const hoverClasses = isOver 
+     ? "border-[var(--color-grass-9)] bg-[var(--color-olive-3)]" 
+     : "border-[var(--color-slate-7)] bg-[var(--color-olive-2)]";
+   return `${baseClasses} ${hoverClasses}`;
+ };

  <div
    ref={setNodeRef}
-   className={`transition-border flex min-w-[250px] max-w-[300px] flex-1 flex-col rounded-md border-2 p-2 shadow-md ${isOver ? "border-[var(--color-grass-9)] bg-[var(--color-olive-3)]" : "border-[var(--color-slate-7)] bg-[var(--color-olive-2)]"} `}
+   className={getColumnStyles(isOver)}
  >
```

</blockquote></details>
<details>
<summary>apps/hire/src/app/settings/_components/boards-tab.tsx (2)</summary><blockquote>

`41-44`: **Remove commented out code.**

These commented-out mutation placeholders should be removed to keep the code clean, or implemented if they're needed.



```diff
- // Placeholder for future mutations
- // const reorderBoards = useMutation(api.boards.reorderBoards);
- // const deleteBoard = useMutation(api.boards.deleteBoard);
- // const updateBoard = useMutation(api.boards.updateBoard);
```

---

`65-68`: **Consider using proper error handling instead of console.error.**

While the TODO comment acknowledges this, consider implementing proper error handling with user feedback sooner rather than later.



```diff
  } catch (error) {
-   console.error("Failed to add board:", error);
-   // TODO: add toast
+   // Consider implementing proper error logging service
+   console.error("Failed to add board:", error);
+   // TODO: add toast notification for user feedback
  }
```

</blockquote></details>
<details>
<summary>apps/hire/convex/boards.ts (1)</summary><blockquote>

`107-108`: **Remove placeholder comments.**

Clean up commented placeholder code before merging to production.


Apply this diff:

```diff
-// Placeholder for future mutations if needed
-// export const reorderBoards = mutation({ ... });
-// export const updateBoard = mutation({ ... });
```

</blockquote></details>
<details>
<summary>libs/component-library/src/components/checkbox/field-checkbox-group.tsx (2)</summary><blockquote>

`48-56`: **Optimize checkbox state updates with useCallback.**

Consider memoizing the change handler to prevent unnecessary re-renders of checkbox items.


Apply this diff:

```diff
+  const handleCheckedChange = React.useCallback((itemId: string, checked: boolean) => {
-  const handleCheckedChange = (itemId: string, checked: boolean) => {
     let newValue;
     if (checked) {
       newValue = [...fieldItems, itemId];
     } else {
       newValue = fieldItems.filter((id) => id !== itemId);
     }
     field.handleChange(newValue);
-  };
+  }, [fieldItems, field.handleChange]);
```

---

`95-95`: **Consider implementing field error display.**

The TODO comment suggests field-level error handling should be addressed for completeness.


Would you like me to help implement field error display for the checkbox group?

</blockquote></details>
<details>
<summary>libs/component-library/src/components/sheet/sheet.tsx (1)</summary><blockquote>

`136-145`: **Consider exporting SheetPortal and SheetOverlay for advanced use cases.**

While SheetPortal and SheetOverlay are used internally by SheetContent, consider exporting them as well for users who might need more granular control over the Sheet composition.



Apply this diff to export the additional components:

```diff
 export {
   Sheet,
   SheetTrigger,
   SheetClose,
+  SheetPortal,
+  SheetOverlay,
   SheetContent,
   SheetHeader,
   SheetFooter,
   SheetTitle,
   SheetDescription,
 };
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 4e131dec667a79d7cd7f48c6468da2f15846b501 and d69c1b30939f285a2ed2a69cf499953cf5b2f635.

</details>

<details>
<summary>⛔ Files ignored due to path filters (2)</summary>

* `apps/hire/convex/_generated/api.d.ts` is excluded by `!**/_generated/**`
* `pnpm-lock.yaml` is excluded by `!**/pnpm-lock.yaml`

</details>

<details>
<summary>📒 Files selected for processing (24)</summary>

* `TODO.md` (1 hunks)
* `apps/hire/convex/boards.ts` (1 hunks)
* `apps/hire/convex/candidates.ts` (2 hunks)
* `apps/hire/convex/schema.ts` (1 hunks)
* `apps/hire/src/app/candidate/add/page.tsx` (1 hunks)
* `apps/hire/src/app/kanban/KanbanBoard.tsx` (1 hunks)
* `apps/hire/src/app/kanban/KanbanCard.tsx` (1 hunks)
* `apps/hire/src/app/kanban/KanbanColumn.tsx` (1 hunks)
* `apps/hire/src/app/kanban/page.tsx` (1 hunks)
* `apps/hire/src/app/settings/_components/boards-tab.tsx` (1 hunks)
* `apps/hire/src/app/settings/_components/edit-board-sheet.tsx` (1 hunks)
* `apps/hire/src/app/settings/page.tsx` (2 hunks)
* `apps/hire/src/server/zod/board.ts` (1 hunks)
* `apps/hire/src/server/zod/candidate.ts` (1 hunks)
* `libs/component-library/src/components/badge/badge.stories.tsx` (1 hunks)
* `libs/component-library/src/components/badge/badge.tsx` (1 hunks)
* `libs/component-library/src/components/checkbox/field-checkbox-group.tsx` (1 hunks)
* `libs/component-library/src/components/checkbox/index.ts` (1 hunks)
* `libs/component-library/src/components/form/use-app-form.ts` (1 hunks)
* `libs/component-library/src/components/index.ts` (1 hunks)
* `libs/component-library/src/components/sheet/sheet.stories.tsx` (1 hunks)
* `libs/component-library/src/components/sheet/sheet.tsx` (1 hunks)
* `libs/component-library/tsconfig.storybook.json` (0 hunks)
* `package.json` (1 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (1)</summary>

* libs/component-library/tsconfig.storybook.json

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (11)</summary>

<details>
<summary>apps/hire/convex/candidates.ts (1)</summary><blockquote>

<details>
<summary>apps/hire/src/server/zod/candidate.ts (1)</summary>

* `UpdateCandidateStageSchema` (38-41)

</details>

</blockquote></details>
<details>
<summary>apps/hire/src/app/kanban/page.tsx (1)</summary><blockquote>

<details>
<summary>apps/hire/src/app/kanban/KanbanBoard.tsx (1)</summary>

* `KanbanBoard` (17-78)

</details>

</blockquote></details>
<details>
<summary>apps/hire/src/app/kanban/KanbanCard.tsx (1)</summary><blockquote>

<details>
<summary>apps/hire/src/server/zod/candidate.ts (1)</summary>

* `ZodCandidate` (44-44)

</details>

</blockquote></details>
<details>
<summary>apps/hire/src/app/kanban/KanbanBoard.tsx (3)</summary><blockquote>

<details>
<summary>apps/hire/convex/candidates.ts (1)</summary>

* `updateCandidateStage` (82-90)

</details>
<details>
<summary>apps/hire/convex/_generated/dataModel.d.ts (1)</summary>

* `Id` (48-49)

</details>
<details>
<summary>apps/hire/src/app/kanban/KanbanColumn.tsx (1)</summary>

* `KanbanColumn` (18-40)

</details>

</blockquote></details>
<details>
<summary>libs/component-library/src/components/badge/badge.tsx (1)</summary><blockquote>

<details>
<summary>libs/component-library/src/lib/utils.ts (1)</summary>

* `cn` (4-6)

</details>

</blockquote></details>
<details>
<summary>libs/component-library/src/components/sheet/sheet.stories.tsx (4)</summary><blockquote>

<details>
<summary>libs/component-library/src/components/sheet/sheet.tsx (8)</summary>

* `Sheet` (137-137)
* `SheetHeader` (141-141)
* `SheetTitle` (143-143)
* `SheetDescription` (144-144)
* `SheetFooter` (142-142)
* `SheetClose` (139-139)
* `SheetTrigger` (138-138)
* `SheetContent` (140-140)

</details>
<details>
<summary>libs/component-library/src/components/label/label.tsx (1)</summary>

* `Label` (26-26)

</details>
<details>
<summary>libs/component-library/src/components/input/input.tsx (1)</summary>

* `Input` (43-43)

</details>
<details>
<summary>libs/component-library/src/components/button/button.tsx (1)</summary>

* `Button` (77-77)

</details>

</blockquote></details>
<details>
<summary>apps/hire/src/app/kanban/KanbanColumn.tsx (2)</summary><blockquote>

<details>
<summary>apps/hire/src/server/zod/candidate.ts (1)</summary>

* `ZodCandidate` (44-44)

</details>
<details>
<summary>apps/hire/src/app/kanban/KanbanCard.tsx (1)</summary>

* `KanbanCard` (7-30)

</details>

</blockquote></details>
<details>
<summary>apps/hire/convex/boards.ts (2)</summary><blockquote>

<details>
<summary>apps/hire/convex/model/companies.ts (1)</summary>

* `getCompanyIdByClerkOrgId` (3-15)

</details>
<details>
<summary>apps/hire/src/server/zod/board.ts (4)</summary>

* `BoardSchema` (6-13)
* `CreateBoardSchema` (15-18)
* `DeleteBoardSchema` (28-31)
* `UpdateBoardSchema` (20-26)

</details>

</blockquote></details>
<details>
<summary>apps/hire/src/server/zod/board.ts (3)</summary><blockquote>

<details>
<summary>apps/hire/src/server/zod/utils.ts (1)</summary>

* `baseConvexFields` (7-10)

</details>
<details>
<summary>apps/hire/src/server/zod/company.ts (1)</summary>

* `CompanyIdSchema` (6-6)

</details>
<details>
<summary>apps/hire/src/server/zod/kanbanStage.ts (1)</summary>

* `KanbanStageIdSchema` (8-8)

</details>

</blockquote></details>
<details>
<summary>libs/component-library/src/components/badge/badge.stories.tsx (1)</summary><blockquote>

<details>
<summary>libs/component-library/src/components/badge/badge.tsx (3)</summary>

* `Badge` (69-69)
* `badgeVariants` (7-32)
* `BadgeProps` (45-49)

</details>

</blockquote></details>
<details>
<summary>libs/component-library/src/components/sheet/sheet.tsx (1)</summary><blockquote>

<details>
<summary>libs/component-library/src/lib/utils.ts (1)</summary>

* `cn` (4-6)

</details>

</blockquote></details>

</details><details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>TODO.md</summary>

[uncategorized] ~9-~9: The abbreviation “e.g.” (= for example) requires two periods.
Context: ...s occur - Deal with cascading deletion (eg. deleting a kanban stage when there are ...

(E_G)

</details>

</details>
<details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>apps/hire/src/app/settings/_components/edit-board-sheet.tsx</summary>

[error] 104-104: Avoid passing children using a prop

The canonical way to pass children in React is to use JSX elements

(lint/correctness/noChildrenProp)

---

[error] 108-108: Avoid passing children using a prop

The canonical way to pass children in React is to use JSX elements

(lint/correctness/noChildrenProp)

---

[error] 112-112: Avoid passing children using a prop

The canonical way to pass children in React is to use JSX elements

(lint/correctness/noChildrenProp)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: main

</details>

<details>
<summary>🔇 Additional comments (35)</summary><blockquote>

<details>
<summary>libs/component-library/src/components/badge/badge.tsx (3)</summary>

`34-43`: **The CVA configuration looks well-structured.**

The base styles and variant configuration are comprehensive and follow good practices. The base classes handle layout, typography, and icon styling appropriately.

---

`45-49`: **Badge interface design is well-typed.**

The interface properly extends HTML span attributes and includes variant props. The `asChild` prop enables flexible composition patterns.

---

`51-67`: **Component implementation follows React best practices.**

The component correctly implements the asChild pattern with Radix UI Slot and properly merges class names using the cn utility. The data-slot attribute is a nice touch for potential testing hooks.

</details>
<details>
<summary>libs/component-library/src/components/badge/badge.stories.tsx (2)</summary>

`13-37`: **Storybook meta configuration is comprehensive.**

The meta configuration properly sets up controls for all Badge props and provides good organization for the component documentation.

---

`56-85`: **VariantDisplay component provides good organization.**

The component effectively demonstrates badge variants with and without icons. The formatting logic for variant names is helpful for readability.

</details>
<details>
<summary>package.json (1)</summary>

`64-64`: <details>
<summary>❓ Verification inconclusive</summary>

**Verify the use of canary pre-release version.**

The `eslint-config-next` package has been updated to a canary pre-release version (`15.4.0-canary.51`). Canary versions are potentially unstable and may introduce breaking changes. Please confirm this is intentional and consider using a stable release unless specific features from the canary version are required.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if there are any recent stable versions available
curl -s https://registry.npmjs.org/eslint-config-next | jq '.["dist-tags"]'

# Check for any known issues with this canary version
npm view eslint-config-next@15.4.0-canary.51
```

Length of output: 1630

---



</details>

**Confirm intentional use of canary release for eslint-config-next**

The `latest` dist-tag for `eslint-config-next` now points to `15.4.0-canary.51`, so installing without a tag will pull the canary build by default. Canary releases can include breaking changes or unfinished features. Please verify that you intend to track this pre-release:

- If you need a stable build, pin to a specific version, for example:
  ```json
  "eslint-config-next": "15.3.2"
  ```
- Or reference a different dist-tag, e.g.:
  ```json
  "eslint-config-next": "next-14"
  ```

</details>
<details>
<summary>apps/hire/src/app/candidate/add/page.tsx (1)</summary>

`11-11`: <details>
<summary>✅ Verification successful</summary>

**Verify API namespace consistency across the codebase.**

The change from `api.candidate.createCandidate` to `api.candidates.createCandidate` follows RESTful naming conventions (plural resource names). However, please ensure this change is consistent throughout the codebase.



---

🏁 Script executed:

```shell
#!/bin/bash
# Check for any remaining references to the old api.candidate namespace
rg "api\.candidate\." --type ts --type tsx

# Verify the new api.candidates namespace exists and is properly implemented
rg "api\.candidates\." --type ts --type tsx -A 2

# Check the Convex API definitions to ensure the endpoint exists
fd "candidates.ts" apps/hire/convex/ --exec cat {}
```

Length of output: 3377

---

I’ll rerun the search with proper file globs and check the generated API file:

---

🏁 Script executed:

```shell
#!/bin/bash
# Search for any references to the old singular namespace in .ts/.tsx files
rg "api\\.candidate\\." -n -g '*.{ts,tsx}'

# Search for any references to the new plural namespace in .ts/.tsx files
rg "api\\.candidates\\." -n -g '*.{ts,tsx}'

# Print the top of any generated api.ts to confirm the plural namespace is defined
fd api.ts -t f -g '*.ts' -x sed -n '1,50p' {}
```

Length of output: 578

---

I’ll locate and inspect the generated client‐side API definitions to confirm the `candidates` namespace and its methods really exist:

---

🏁 Script executed:

```shell
#!/bin/bash
# Locate the generated client API file (commonly named api.ts)
API_FILE=$(fd -t f api.ts | head -n 1)
if [ -n "$API_FILE" ]; then
  echo "Found client API at: $API_FILE"
  echo "----- Top of file -----"
  sed -n '1,200p' "$API_FILE"
else
  echo "No api.ts file found—trying fallback search for 'export const api'"
  rg "export const api" -n
fi
```

Length of output: 299

---

I’ll inspect the generated client API to confirm the `candidates` namespace and its methods:

---

🏁 Script executed:

```shell
#!/bin/bash
# Search for the 'candidates' namespace in the generated client API
rg "candidates" -n apps/hire/convex/_generated/api.js

# Look for createCandidate under that namespace for context
rg "createCandidate" -n apps/hire/convex/_generated/api.js -C 3
```

Length of output: 117

---

🏁 Script executed:

```shell
#!/bin/bash
# List all generated client files
echo "Generated files in apps/hire/convex/_generated/:"
ls -1 apps/hire/convex/_generated

# Search for 'createCandidate' in any .d.ts file
echo "Searching for createCandidate in .d.ts definitions:"
rg "createCandidate" -n apps/hire/convex/_generated -g '*.d.ts'

# If we find a .d.ts, print its first 200 lines for namespace context
echo "Displaying contents of any found .d.ts file:"
fd -t f '*.d.ts' apps/hire/convex/_generated | xargs -I {} sh -c 'echo "---- {} ----"; sed -n "1,200p" "{}"'
```

Length of output: 870

---

I’ll inspect the generated TypeScript definitions to confirm the `candidates` namespace and its methods exist:

---

🏁 Script executed:

```shell
#!/bin/bash
# Search for the 'candidates' namespace in the client d.ts
echo "Looking for 'namespace candidates' in api.d.ts"
rg -n "namespace candidates" apps/hire/convex/_generated/api.d.ts

# Show context around createCandidate
echo "Context around 'createCandidate':"
rg -n -C2 "createCandidate" apps/hire/convex/_generated/api.d.ts

# If still no hits, dump the first 200 lines for manual inspection
echo "Dumping top of api.d.ts for manual review:"
sed -n '1,200p' apps/hire/convex/_generated/api.d.ts
```

Length of output: 1896

---



</details>

**API namespace consistency verified**

All references to `api.candidate` have been replaced with `api.candidates`, and the `candidates` module (including `createCandidate` and other mutations/queries) is present in both your Convex server definitions and the generated client API (`apps/hire/convex/_generated/api.d.ts`). No further changes are needed.

</details>
<details>
<summary>libs/component-library/src/components/index.ts (1)</summary>

`19-20`: **LGTM! Clean export reorganization.**

The addition of the sheet export and the consolidation of checkbox exports through an index file follows standard component library patterns and improves the module structure.

</details>
<details>
<summary>libs/component-library/src/components/checkbox/index.ts (1)</summary>

`1-2`: **LGTM! Standard export consolidation pattern.**

The index file properly consolidates exports for checkbox-related components, providing a clean single entry point for consumers.

</details>
<details>
<summary>libs/component-library/src/components/form/use-app-form.ts (2)</summary>

`8-8`: **LGTM! Consistent import pattern.**

The import follows the established pattern for field components.

---

`16-16`: **LGTM! Extends form capabilities properly.**

Adding FieldCheckboxGroup to fieldComponents follows the same pattern as other field components and properly extends the form hook's capabilities.

</details>
<details>
<summary>apps/hire/convex/candidates.ts (1)</summary>

`9-9`: **LGTM! Proper schema import.**

The import of UpdateCandidateStageSchema is consistent with other schema imports in the file.

</details>
<details>
<summary>apps/hire/src/server/zod/candidate.ts (1)</summary>

`38-41`: **Well-structured schema for candidate stage updates.**

The `UpdateCandidateStageSchema` follows established patterns by reusing existing schemas (`CandidateIdSchema`) and the `zid` helper. This ensures consistency and type safety for the kanban drag-and-drop functionality.

</details>
<details>
<summary>apps/hire/src/app/settings/page.tsx (3)</summary>

`1-1`: **Appropriate addition of client directive.**

Adding the `"use client";` directive is correct since the component uses the `useOrganization` hook, which requires client-side execution.

---

`7-7`: **Consistent integration of BoardsTab component.**

The BoardsTab import and usage follows the established pattern of other tab components, maintaining consistency in the codebase structure and prop passing.




Also applies to: 29-29

---

`21-21`: **Improved description reflects broader scope.**

The updated description "Manage your organization settings and preferences" better reflects the expanded functionality beyond just company hiring settings.

</details>
<details>
<summary>apps/hire/src/app/kanban/KanbanCard.tsx (1)</summary>

`10-16`: **Well-implemented drag-and-drop styling.**

The inline styles for drag-and-drop are correctly implemented. Using `translate3d` is optimal for performance, and the opacity feedback provides clear visual indication of the drag state.

</details>
<details>
<summary>apps/hire/src/app/kanban/KanbanBoard.tsx (2)</summary>

`38-45`: **Good validation logic with minor optimization opportunity.**

The validation logic correctly checks for candidate existence and stage validity before updating. The condition `candidate.kanbanStageId !== newStageId` prevents unnecessary API calls.



Consider extracting the candidate and stage lookups to improve readability:

```diff
  const candidate = candidates?.find((c) => c._id === candidateId);
+ const targetStage = stages?.find((s) => s._id === newStageId);
  if (
    candidate &&
-   stages?.some((s) => s._id === newStageId) &&
+   targetStage &&
    candidate.kanbanStageId !== newStageId
  ) {
    updateCandidateStage({ candidateId, kanbanStageId: newStageId });
  }
```

---

`26-28`: **Good drag sensor configuration.**

The activation constraint with 5px distance prevents accidental drags and improves user experience.

</details>
<details>
<summary>apps/hire/src/app/kanban/KanbanColumn.tsx (1)</summary>

`28-37`: **Excellent use of SortableContext and vertical sorting strategy.**

The implementation correctly wraps candidate cards in SortableContext with the appropriate strategy and item mapping. The key prop usage is also correct.

</details>
<details>
<summary>apps/hire/src/app/settings/_components/boards-tab.tsx (3)</summary>

`47-53`: **Efficient stage name mapping with useMemo.**

Great use of useMemo to create a lookup map for stage names, preventing unnecessary recalculations on each render.

---

`124-133`: **Good handling of stage display with fallback.**

The implementation correctly handles cases where stage names might not be found and provides a fallback to display the stage ID.

---

`169-187`: **Well-structured delete confirmation dialog.**

The delete confirmation dialog follows good UX patterns with clear messaging and proper action buttons.

</details>
<details>
<summary>apps/hire/src/server/zod/board.ts (3)</summary>

`6-13`: **Well-structured BoardSchema with proper field composition.**

The schema correctly combines base Convex fields with board-specific properties. The use of `baseConvexFields` promotes consistency across schemas.

---

`20-26`: **Excellent use of schema composition for UpdateBoardSchema.**

The combination of `omit()` and `extend()` creates a clean update schema that excludes immutable fields while adding the organization context.

---

`28-31`: **Proper DeleteBoardSchema structure.**

The schema correctly references the board ID type from the main schema and includes the necessary organization context.

</details>
<details>
<summary>libs/component-library/src/components/sheet/sheet.stories.tsx (1)</summary>

`1-149`: **Excellent Storybook implementation with comprehensive interaction testing!**

This stories file follows Storybook v7+ best practices with well-structured stories and thorough interaction tests. The `InteractionTest` story provides valuable step-by-step testing of the sheet's open/close behavior via different interaction methods.

</details>
<details>
<summary>apps/hire/convex/boards.ts (1)</summary>

`16-30`: **LGTM! Well-structured query with proper validation.**

The query correctly resolves company ID and uses appropriate indexing for efficient retrieval.

</details>
<details>
<summary>libs/component-library/src/components/checkbox/field-checkbox-group.tsx (1)</summary>

`58-97`: **Excellent accessibility and form integration!**

The component properly implements ARIA attributes, form field integration, and follows React best practices for controlled components.

</details>
<details>
<summary>libs/component-library/src/components/sheet/sheet.tsx (6)</summary>

`1-7`: **LGTM! Clean imports and proper client directive.**

The imports are well-organized and the "use client" directive is appropriate for this React component that will be used in Next.js client-side rendering.

---

`9-29`: **LGTM! Consistent wrapper pattern for basic components.**

The Sheet, SheetTrigger, SheetClose, and SheetPortal components follow a clean and consistent pattern of wrapping Radix primitives while adding data-slot attributes for styling hooks.

---

`31-45`: **LGTM! Well-implemented overlay with proper animations.**

The SheetOverlay component correctly implements fade animations using Radix's data-state attributes and provides a properly styled backdrop overlay.

---

`47-82`: **LGTM! Comprehensive SheetContent implementation with excellent accessibility.**

The SheetContent component is well-designed with:
- Proper conditional styling for all four slide directions
- Accessible close button with screen reader text
- Good default prop value for the side prop
- Correct z-index layering and positioning

The implementation correctly handles all slide-in/slide-out animations and positioning for each side.

---

`84-108`: **LGTM! Clean layout components with appropriate styling.**

SheetHeader and SheetFooter provide logical layout containers with proper borders and spacing that complement each other (header with bottom border, footer with top border and margin-auto).

---

`110-134`: **LGTM! Consistent typography components.**

SheetTitle and SheetDescription properly wrap Radix primitives with appropriate typography styling that maintains visual hierarchy.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-25T02:47:48Z

Review comments:
diff_hunk: @@ -0,0 +1,113 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import { Badge, BadgeProps, badgeVariants } from "./badge";
+import {
+  Info,
+  CheckCircle,
+  AlertTriangle,
+  XCircle,
+  Sparkles,
+  Tag as TagIcon,
+} from "lucide-react";
+import React from "react";
+
+const meta: Meta<typeof Badge> = {
+  title: "Components/Badge",
+  component: Badge,
+  parameters: {
+    layout: "centered",
+  },
+  tags: ["autodocs"],
+  argTypes: {
+    variant: {
+      control: "select",
+      options: Object.keys(badgeVariants.variant),
+    },
+    shape: {
+      control: "select",
+      options: Object.keys(badgeVariants.shape),
+    },
+    children: {
+      control: "text",
+    },
+    asChild: {
+      control: "boolean",
+    },
+    className: { control: false },
+  },
+};
+
+export default meta;
+type Story = StoryObj<typeof meta>;
+
+// Helper to get an appropriate icon for a variant
+const getIconForVariant = (variant: keyof typeof badgeVariants.variant) => {
+  if (variant.includes("green")) return <CheckCircle />;
+  if (variant.includes("red")) return <XCircle />;
+  if (variant.includes("amber")) return <AlertTriangle />;
+  if (variant.includes("blue")) return <Info />;
+  if (variant.includes("grass")) return <Sparkles />;
+  return <TagIcon />;
+};
+
+const shapes = Object.keys(
+  badgeVariants.shape,
+) as (keyof typeof badgeVariants.shape)[];
+
+const VariantDisplay = ({
+  variant,
+  ...args
+}: Omit<BadgeProps, "variant"> & {
+  variant: keyof typeof badgeVariants.variant;
+}) => {
+  return (
+    <>
+      <h2 className="border-slate-6 m-0 w-full border-b pb-2 text-lg font-semibold capitalize">
+        {variant.replace(/([A-Z])/g, " $1")}{" "}
+        {/* Add space before capital letters for display */}
+      </h2>
+      <div className="flex flex-row flex-wrap items-center gap-4">
+        {shapes.map((shape) => (
+          <React.Fragment key={`${variant}-${shape}`}>
+            <Badge {...args} variant={variant} shape={shape}>
+              {shape} Badge
+            </Badge>
+            <Badge {...args} variant={variant} shape={shape}>
+              {React.cloneElement(getIconForVariant(variant), {
+                className: "size-3",
+              })}
+              <span>{shape} Icon</span>
+            </Badge>
+          </React.Fragment>
+        ))}
+      </div>
+    </>
+  );
+};
+export const AllVariants: Story = {
+  parameters: {
+    controls: { hideNoControlsWarning: true, sort: "requiredFirst" },
+  },
+  render: (args) => {
+    const variants = Object.keys(badgeVariants.variant).filter(
+      (variant) => !variant.includes("Outline"),
+    ) as ("olive" | "slate" | "grass" | "green" | "red" | "amber" | "blue")[];
+
+    return (
+      <div className="text-slate-11 flex w-full flex-col items-start gap-10 p-5">
+        {variants.map((variant) => (
+          <div
+            key={variant}
+            className="grid w-full grid-cols-2 items-start gap-5"
+          >
+            <div className="col-span-1 flex flex-col gap-4">
+              <VariantDisplay variant={variant} {...args} />
+            </div>
+            <div className="col-span-1 flex flex-col gap-4">
+              <VariantDisplay variant={`${variant}Outline`} {...args} />
+            </div>
+          </div>
+        ))}
+      </div>
+    );
+  },
+};
path: libs/component-library/src/components/badge/badge.stories.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Improve type safety in variant filtering.**

The manual type assertion on line 93 could become outdated if variants are added or removed.



Use more robust type derivation:

```diff
export const AllVariants: Story = {
  parameters: {
    controls: { hideNoControlsWarning: true, sort: "requiredFirst" },
  },
  render: (args) => {
-    const variants = Object.keys(badgeVariants.variant).filter(
-      (variant) => !variant.includes("Outline"),
-    ) as ("olive" | "slate" | "grass" | "green" | "red" | "amber" | "blue")[];
+    const variants = Object.keys(badgeVariants.variant).filter(
+      (variant) => !variant.includes("Outline"),
+    ) as Array<keyof typeof badgeVariants.variant>;

    return (
      <div className="text-slate-11 flex w-full flex-col items-start gap-10 p-5">
        {variants.map((variant) => (
          <div
            key={variant}
            className="grid w-full grid-cols-2 items-start gap-5"
          >
            <div className="col-span-1 flex flex-col gap-4">
              <VariantDisplay variant={variant} {...args} />
            </div>
            <div className="col-span-1 flex flex-col gap-4">
-              <VariantDisplay variant={`${variant}Outline`} {...args} />
+              <VariantDisplay 
+                variant={`${variant}Outline` as keyof typeof badgeVariants.variant} 
+                {...args} 
+              />
            </div>
          </div>
        ))}
      </div>
    );
  },
};
```

This approach is more maintainable and type-safe.


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
export const AllVariants: Story = {
  parameters: {
    controls: { hideNoControlsWarning: true, sort: "requiredFirst" },
  },
  render: (args) => {
    const variants = Object.keys(badgeVariants.variant).filter(
      (variant) => !variant.includes("Outline"),
    ) as Array<keyof typeof badgeVariants.variant>;

    return (
      <div className="text-slate-11 flex w-full flex-col items-start gap-10 p-5">
        {variants.map((variant) => (
          <div
            key={variant}
            className="grid w-full grid-cols-2 items-start gap-5"
          >
            <div className="col-span-1 flex flex-col gap-4">
              <VariantDisplay variant={variant} {...args} />
            </div>
            <div className="col-span-1 flex flex-col gap-4">
              <VariantDisplay
                variant={`${variant}Outline` as keyof typeof badgeVariants.variant}
                {...args}
              />
            </div>
          </div>
        ))}
      </div>
    );
  },
};
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In libs/component-library/src/components/badge/badge.stories.tsx around lines 86
to 113, the manual type assertion for the variants array on line 93 is brittle
and can become outdated if badge variants change. Replace the explicit type
assertion with a type-safe derivation by extracting the variant keys directly
from the badgeVariants.variant object type, filtering out unwanted variants, and
letting TypeScript infer the correct union type automatically. This will improve
maintainability and prevent type mismatches when variants are updated.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-25T02:47:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -76,3 +77,14 @@ export const deleteCandidate = candidateMutation({
     await ctx.db.delete(_id);
   },
 });
+
+// Mutation to update a candidate's Kanban stage
+export const updateCandidateStage = candidateMutation({
+  args: UpdateCandidateStageSchema,
+  handler: async (ctx, args) => {
+    await ctx.db.patch(args.candidateId, {
+      kanbanStageId: args.kanbanStageId,
+      updatedAt: Date.now(),
+    });
+  },
+});
path: apps/hire/convex/candidates.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Critical: Missing candidate existence verification.**

The mutation lacks candidate existence verification, which is inconsistent with other mutations in this file and could lead to security issues.



Both `updateCandidate` and `deleteCandidate` call `verifyCandidateExists()` before performing operations to ensure the candidate exists and belongs to the correct company. This mutation should follow the same pattern.

Apply this diff to add proper verification:

```diff
 export const updateCandidateStage = candidateMutation({
   args: UpdateCandidateStageSchema,
   handler: async (ctx, args) => {
+    await verifyCandidateExists(ctx, { _id: args.candidateId });
     await ctx.db.patch(args.candidateId, {
       kanbanStageId: args.kanbanStageId,
       updatedAt: Date.now(),
     });
   },
 });
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
export const updateCandidateStage = candidateMutation({
  args: UpdateCandidateStageSchema,
  handler: async (ctx, args) => {
    await verifyCandidateExists(ctx, { _id: args.candidateId });
    await ctx.db.patch(args.candidateId, {
      kanbanStageId: args.kanbanStageId,
      updatedAt: Date.now(),
    });
  },
});
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/hire/convex/candidates.ts around lines 82 to 90, the
updateCandidateStage mutation does not verify if the candidate exists before
updating, unlike other mutations in this file. To fix this, add a call to
verifyCandidateExists() at the start of the handler function, passing the
context and candidateId from args, to ensure the candidate exists and belongs to
the correct company before proceeding with the update.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-25T02:47:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,78 @@
+"use client";
+
+import React from "react";
+import {
+  DndContext,
+  closestCenter,
+  PointerSensor,
+  useSensor,
+  useSensors,
+  DragEndEvent,
+} from "@dnd-kit/core";
+import { KanbanColumn } from "./KanbanColumn";
+import { useQuery, useMutation } from "convex/react";
+import { api } from "../../../convex/_generated/api";
+import type { Id } from "../../../convex/_generated/dataModel";
+
+export function KanbanBoard({ organizationId }: { organizationId: string }) {
+  const stages = useQuery(api.kanbanStages.getKanbanStages, {
+    orgId: organizationId,
+  });
+  const candidates = useQuery(api.candidates.listCandidates, {
+    orgId: organizationId,
+  });
+  const updateCandidateStage = useMutation(api.candidates.updateCandidateStage);
+
+  const sensors = useSensors(
+    useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
+  );
+
+  function handleDragEnd(event: DragEndEvent) {
+    const { active, over } = event;
+
+    if (!over || !active) return;
+
+    const candidateId = active.id as Id<"candidates">;
+    const newStageId = over.id as Id<"kanbanStages">;
path: apps/hire/src/app/kanban/KanbanBoard.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Improve type safety in drag operations.**

The type assertions here are unsafe and could lead to runtime errors if dragged items aren't actually candidates or if drop targets aren't kanban stages.



Consider adding proper type guards:

```diff
- const candidateId = active.id as Id<"candidates">;
- const newStageId = over.id as Id<"kanbanStages">;
+ // Validate that the dragged item is a candidate
+ const candidateId = active.id;
+ const newStageId = over.id;
+ 
+ // Ensure the dragged item is actually a candidate
+ const candidate = candidates?.find((c) => c._id === candidateId);
+ if (!candidate) {
+   console.warn("Dragged item is not a valid candidate");
+   return;
+ }
+ 
+ // Ensure the drop target is a valid stage
+ const targetStage = stages?.find((s) => s._id === newStageId);
+ if (!targetStage) {
+   console.warn("Drop target is not a valid kanban stage");
+   return;
+ }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
// Validate that the dragged item is a candidate
const candidateId = active.id;
const newStageId = over.id;

// Ensure the dragged item is actually a candidate
const candidate = candidates?.find((c) => c._id === candidateId);
if (!candidate) {
  console.warn("Dragged item is not a valid candidate");
  return;
}

// Ensure the drop target is a valid stage
const targetStage = stages?.find((s) => s._id === newStageId);
if (!targetStage) {
  console.warn("Drop target is not a valid kanban stage");
  return;
}
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/hire/src/app/kanban/KanbanBoard.tsx around lines 35 to 36, the current
use of type assertions for candidateId and newStageId is unsafe and may cause
runtime errors if the dragged items or drop targets are not of the expected
types. Replace these assertions with proper type guards that check the type of
active.id and over.id before assigning them, ensuring they are valid
Id<"candidates"> and Id<"kanbanStages"> respectively. This will improve type
safety and prevent potential runtime issues during drag operations.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-25T02:47:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,136 @@
+/* eslint-disable react/no-children-prop */
path: apps/hire/src/app/settings/_components/edit-board-sheet.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Remove ESLint disable comment and fix the underlying issues.**

The ESLint disable comment indicates underlying code quality issues that should be addressed rather than suppressed.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/hire/src/app/settings/_components/edit-board-sheet.tsx at line 1, remove
the ESLint disable comment for react/no-children-prop and refactor the code to
avoid passing children as a prop directly. Instead, use proper JSX children
syntax to pass child elements, ensuring compliance with ESLint rules and
improving code quality.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-25T02:47:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,136 @@
+/* eslint-disable react/no-children-prop */
+"use client";
+
+import * as React from "react";
+import {
+  Button,
+  Sheet,
+  SheetClose,
+  SheetContent,
+  SheetDescription,
+  SheetFooter,
+  SheetHeader,
+  SheetTitle,
+  useAppForm,
+} from "@j5/component-library";
+import { useMutation, useQuery } from "convex/react";
+import { api } from "../../../../convex/_generated/api";
+import type { Doc } from "../../../../convex/_generated/dataModel";
+import { UpdateBoardSchema } from "../../../server/zod/board";
+
+export type EditBoardSheetProps = {
+  isOpen: boolean;
+  onOpenChange: (open: boolean) => void;
+  board: Doc<"boards">;
+  orgId: string;
+};
+
+export function EditBoardSheet({
+  isOpen,
+  onOpenChange,
+  board,
+  orgId,
+}: EditBoardSheetProps) {
+  const updateBoardMutation = useMutation(api.boards.updateBoard);
+  const allKanbanStages = useQuery(api.kanbanStages.getKanbanStages, { orgId });
+
+  const kanbanStageItems = React.useMemo(() => {
+    return (allKanbanStages || []).map((stage) => ({
+      id: stage._id,
+      label: stage.name,
+    }));
+  }, [allKanbanStages]);
+
+  const form = useAppForm({
+    defaultValues: {
+      _id: board._id,
+      name: board.name,
+      slug: board.slug,
+      kanbanStageIds: board.kanbanStageIds,
+      order: board.order,
+    },
+    validators: {
+      onChange: ({ value }) => {
+        const result = UpdateBoardSchema.omit({ orgId: true }).safeParse(value);
+        console.log(result.error?.flatten().fieldErrors);
+        if (!result.success) {
+          return result.error.flatten().fieldErrors;
+        }
+        return undefined;
+      },
+    },
+    onSubmit: async ({ value }) => {
+      try {
+        await updateBoardMutation({ orgId, ...value });
+        onOpenChange(false); // Close sheet on success
+      } catch (error) {
+        console.error("Failed to update board:", error);
+        // Handle error display to user if necessary
+      }
path: apps/hire/src/app/settings/_components/edit-board-sheet.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Enhance error handling with user-facing feedback.**

Currently, errors are only logged to the console. Consider adding toast notifications or form-level error display to inform users of failures.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/hire/src/app/settings/_components/edit-board-sheet.tsx around lines 66
to 69, the catch block only logs errors to the console without informing the
user. Enhance error handling by adding a user-facing notification such as a
toast message or updating the form state to display an error message, so users
receive clear feedback when the update fails.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-25T02:47:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,136 @@
+/* eslint-disable react/no-children-prop */
+"use client";
+
+import * as React from "react";
+import {
+  Button,
+  Sheet,
+  SheetClose,
+  SheetContent,
+  SheetDescription,
+  SheetFooter,
+  SheetHeader,
+  SheetTitle,
+  useAppForm,
+} from "@j5/component-library";
+import { useMutation, useQuery } from "convex/react";
+import { api } from "../../../../convex/_generated/api";
+import type { Doc } from "../../../../convex/_generated/dataModel";
+import { UpdateBoardSchema } from "../../../server/zod/board";
+
+export type EditBoardSheetProps = {
+  isOpen: boolean;
+  onOpenChange: (open: boolean) => void;
+  board: Doc<"boards">;
+  orgId: string;
+};
+
+export function EditBoardSheet({
+  isOpen,
+  onOpenChange,
+  board,
+  orgId,
+}: EditBoardSheetProps) {
+  const updateBoardMutation = useMutation(api.boards.updateBoard);
+  const allKanbanStages = useQuery(api.kanbanStages.getKanbanStages, { orgId });
+
+  const kanbanStageItems = React.useMemo(() => {
+    return (allKanbanStages || []).map((stage) => ({
+      id: stage._id,
+      label: stage.name,
+    }));
+  }, [allKanbanStages]);
+
+  const form = useAppForm({
+    defaultValues: {
+      _id: board._id,
+      name: board.name,
+      slug: board.slug,
+      kanbanStageIds: board.kanbanStageIds,
+      order: board.order,
+    },
+    validators: {
+      onChange: ({ value }) => {
+        const result = UpdateBoardSchema.omit({ orgId: true }).safeParse(value);
+        console.log(result.error?.flatten().fieldErrors);
+        if (!result.success) {
path: apps/hire/src/app/settings/_components/edit-board-sheet.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Remove console.log statement from production code.**

Console logging should not be present in production code, especially for form validation feedback.


Apply this diff to remove the console.log:

```diff
-        const result = UpdateBoardSchema.omit({ orgId: true }).safeParse(value);
-        console.log(result.error?.flatten().fieldErrors);
-        if (!result.success) {
+        const result = UpdateBoardSchema.omit({ orgId: true }).safeParse(value);
+        if (!result.success) {
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
        const result = UpdateBoardSchema.omit({ orgId: true }).safeParse(value);
        if (!result.success) {
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/hire/src/app/settings/_components/edit-board-sheet.tsx around lines 54
to 56, remove the console.log statement that outputs validation errors to avoid
logging sensitive or unnecessary information in production. Simply delete the
line containing console.log(result.error?.flatten().fieldErrors) to clean up the
code.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-25T02:47:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,136 @@
+/* eslint-disable react/no-children-prop */
+"use client";
+
+import * as React from "react";
+import {
+  Button,
+  Sheet,
+  SheetClose,
+  SheetContent,
+  SheetDescription,
+  SheetFooter,
+  SheetHeader,
+  SheetTitle,
+  useAppForm,
+} from "@j5/component-library";
+import { useMutation, useQuery } from "convex/react";
+import { api } from "../../../../convex/_generated/api";
+import type { Doc } from "../../../../convex/_generated/dataModel";
+import { UpdateBoardSchema } from "../../../server/zod/board";
+
+export type EditBoardSheetProps = {
+  isOpen: boolean;
+  onOpenChange: (open: boolean) => void;
+  board: Doc<"boards">;
+  orgId: string;
+};
+
+export function EditBoardSheet({
+  isOpen,
+  onOpenChange,
+  board,
+  orgId,
+}: EditBoardSheetProps) {
+  const updateBoardMutation = useMutation(api.boards.updateBoard);
+  const allKanbanStages = useQuery(api.kanbanStages.getKanbanStages, { orgId });
+
+  const kanbanStageItems = React.useMemo(() => {
+    return (allKanbanStages || []).map((stage) => ({
+      id: stage._id,
+      label: stage.name,
+    }));
+  }, [allKanbanStages]);
+
+  const form = useAppForm({
+    defaultValues: {
+      _id: board._id,
+      name: board.name,
+      slug: board.slug,
+      kanbanStageIds: board.kanbanStageIds,
+      order: board.order,
+    },
+    validators: {
+      onChange: ({ value }) => {
+        const result = UpdateBoardSchema.omit({ orgId: true }).safeParse(value);
+        console.log(result.error?.flatten().fieldErrors);
+        if (!result.success) {
+          return result.error.flatten().fieldErrors;
+        }
+        return undefined;
+      },
+    },
+    onSubmit: async ({ value }) => {
+      try {
+        await updateBoardMutation({ orgId, ...value });
+        onOpenChange(false); // Close sheet on success
+      } catch (error) {
+        console.error("Failed to update board:", error);
+        // Handle error display to user if necessary
+      }
+    },
+  });
+
+  // Reset form if the board prop changes (e.g., user selects a different board to edit)
+  React.useEffect(() => {
+    form.reset({
+      _id: board._id,
+      name: board.name,
+      slug: board.slug,
+      kanbanStageIds: board.kanbanStageIds,
+      order: board.order,
+    });
+  }, [board, form]);
+
+  return (
+    <Sheet open={isOpen} onOpenChange={onOpenChange}>
+      <SheetContent className="sm:max-w-lg">
+        <SheetHeader>
+          <SheetTitle>Edit Board</SheetTitle>
+          <SheetDescription>
+            Make changes to your board&apos;s details here. Click save when
+            you&apos;re done.
+          </SheetDescription>
+        </SheetHeader>
+        <form
+          onSubmit={(e) => {
+            e.preventDefault();
+            e.stopPropagation();
+            void form.handleSubmit();
+          }}
+          className="space-y-6 py-6"
+        >
+          <form.AppField
+            name="name"
+            children={(field) => <field.FieldInput label="Board Name" />}
+          />
+          <form.AppField
+            name="slug"
+            children={(field) => <field.FieldInput label="Slug" />}
+          />
+          <form.AppField
+            name="kanbanStageIds"
+            children={(field) => (
+              <field.FieldCheckboxGroup
+                label="Kanban Stages"
+                items={kanbanStageItems}
+                orientation="vertical"
+              />
+            )}
+          />
path: apps/hire/src/app/settings/_components/edit-board-sheet.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix children prop pattern to follow React best practices.**

The static analysis correctly identifies that passing `children` as a prop violates React conventions. Use JSX children syntax instead.


Apply this diff to fix the prop pattern:

```diff
-          <form.AppField
-            name="name"
-            children={(field) => <field.FieldInput label="Board Name" />}
-          />
-          <form.AppField
-            name="slug"
-            children={(field) => <field.FieldInput label="Slug" />}
-          />
-          <form.AppField
-            name="kanbanStageIds"
-            children={(field) => (
-              <field.FieldCheckboxGroup
-                label="Kanban Stages"
-                items={kanbanStageItems}
-                orientation="vertical"
-              />
-            )}
-          />
+          <form.AppField name="name">
+            {(field) => <field.FieldInput label="Board Name" />}
+          </form.AppField>
+          <form.AppField name="slug">
+            {(field) => <field.FieldInput label="Slug" />}
+          </form.AppField>
+          <form.AppField name="kanbanStageIds">
+            {(field) => (
+              <field.FieldCheckboxGroup
+                label="Kanban Stages"
+                items={kanbanStageItems}
+                orientation="vertical"
+              />
+            )}
+          </form.AppField>
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
          <form.AppField name="name">
            {(field) => <field.FieldInput label="Board Name" />}
          </form.AppField>
          <form.AppField name="slug">
            {(field) => <field.FieldInput label="Slug" />}
          </form.AppField>
          <form.AppField name="kanbanStageIds">
            {(field) => (
              <field.FieldCheckboxGroup
                label="Kanban Stages"
                items={kanbanStageItems}
                orientation="vertical"
              />
            )}
          </form.AppField>
```

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 104-104: Avoid passing children using a prop

The canonical way to pass children in React is to use JSX elements

(lint/correctness/noChildrenProp)

---

[error] 108-108: Avoid passing children using a prop

The canonical way to pass children in React is to use JSX elements

(lint/correctness/noChildrenProp)

---

[error] 112-112: Avoid passing children using a prop

The canonical way to pass children in React is to use JSX elements

(lint/correctness/noChildrenProp)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/hire/src/app/settings/_components/edit-board-sheet.tsx around lines 102
to 119, the form.AppField components are passing the children prop explicitly,
which violates React best practices. Refactor these components to use JSX
children syntax by placing the function as the child between the opening and
closing tags of form.AppField instead of passing it as a children prop.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-25T02:47:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,108 @@
+import { mutation, query } from "./_generated/server";
+import { getCompanyIdByClerkOrgId } from "./model/companies";
+import { zCustomQuery, zCustomMutation } from "convex-helpers/server/zod";
+import { NoOp } from "convex-helpers/server/customFunctions";
+import { z } from "zod";
+import {
+  BoardSchema,
+  CreateBoardSchema,
+  DeleteBoardSchema,
+  UpdateBoardSchema,
+} from "../src/server/zod/board";
+
+const boardQuery = zCustomQuery(query, NoOp);
+const boardMutation = zCustomMutation(mutation, NoOp);
+
+export const getBoardsByOrgId = boardQuery({
+  args: { orgId: z.string() },
+  async handler(ctx, args) {
+    const companyId = await getCompanyIdByClerkOrgId(ctx, {
+      clerkOrgId: args.orgId,
+    });
+
+    return await ctx.db
+      .query("boards")
+      .withIndex("by_company_order", (q) => q.eq("companyId", companyId))
+      .order("asc") // Assuming 'asc' for order
+      .collect();
+  },
+  returns: z.array(BoardSchema),
+});
+
+export const addBoard = boardMutation({
+  args: CreateBoardSchema.extend({ orgId: z.string() }),
+  async handler(ctx, args) {
+    const companyId = await getCompanyIdByClerkOrgId(ctx, {
+      clerkOrgId: args.orgId,
+    });
+
+    // Determine the next order value
+    const lastBoard = await ctx.db
+      .query("boards")
+      .withIndex("by_company_order", (q) => q.eq("companyId", companyId))
+      .order("desc")
+      .first();
+
+    const newOrder = lastBoard ? lastBoard.order + 1 : 0;
+
+    // Generate a simple slug
+    const slug = args.name
+      .toLowerCase()
+      .replace(/\s+/g, "-")
+      .replace(/[^\w-]+/g, "");
+
path: apps/hire/convex/boards.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Enhance slug generation to handle duplicates.**

The current slug generation could create duplicate slugs when multiple boards have similar names. Consider adding uniqueness validation or suffix numbering.


Apply this diff to improve slug generation:

```diff
-    // Generate a simple slug
-    const slug = args.name
-      .toLowerCase()
-      .replace(/\s+/g, "-")
-      .replace(/[^\w-]+/g, "");
+    // Generate a unique slug
+    let baseSlug = args.name
+      .toLowerCase()
+      .replace(/\s+/g, "-")
+      .replace(/[^\w-]+/g, "");
+    
+    // Check for existing slugs and append number if needed
+    let slug = baseSlug;
+    let counter = 1;
+    while (await ctx.db
+      .query("boards")
+      .withIndex("by_company_order", (q) => q.eq("companyId", companyId))
+      .filter((q) => q.eq(q.field("slug"), slug))
+      .first()
+    ) {
+      slug = `${baseSlug}-${counter}`;
+      counter++;
+    }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
    // Generate a unique slug
    let baseSlug = args.name
      .toLowerCase()
      .replace(/\s+/g, "-")
      .replace(/[^\w-]+/g, "");
    
    // Check for existing slugs and append number if needed
    let slug = baseSlug;
    let counter = 1;
    while (await ctx.db
      .query("boards")
      .withIndex("by_company_order", (q) => q.eq("companyId", companyId))
      .filter((q) => q.eq(q.field("slug"), slug))
      .first()
    ) {
      slug = `${baseSlug}-${counter}`;
      counter++;
    }
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/hire/convex/boards.ts around lines 48 to 53, the slug generation logic
does not handle duplicate slugs, which can cause conflicts. Modify the code to
check if the generated slug already exists and, if so, append a numeric suffix
that increments until a unique slug is found. This ensures each board has a
distinct slug even if names are similar.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-25T02:47:48Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-05-25T02:43:29Z
body: [vc]: #wwZjiFcnEfsN6HOGYYzEjh5+aHRSHKKv6PdvqCm5XBY=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJjb21pbmctc29vbiIsInJvb3REaXJlY3RvcnkiOiJhcHBzL2NvbWluZy1zb29uIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL2phY2tzb24tbWlsbGVycy1wcm9qZWN0cy9jb21pbmctc29vbi80a0IyaDc5M2pUMVk1Z29TYXJhaFZlaUpCVGpGIiwicHJldmlld1VybCI6ImNvbWluZy1zb29uLWdpdC1mZWF0dXJlLWthbmJhbi1ib2FyZC1qYWNrc29uLW1pbGxlcnMtcHJvamVjdHMudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJjb21pbmctc29vbi1naXQtZmVhdHVyZS1rYW5iYW4tYm9hcmQtamFja3Nvbi1taWxsZXJzLXByb2plY3RzLnZlcmNlbC5hcHAifX0seyJuYW1lIjoiaGlyZSIsInJvb3REaXJlY3RvcnkiOiJhcHBzL2hpcmUiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vamFja3Nvbi1taWxsZXJzLXByb2plY3RzL2hpcmUvNHBYRnNtU240dHZxWFl1VXViN2hQYk1ONHR6ZyIsInByZXZpZXdVcmwiOiIiLCJuZXh0Q29tbWl0U3RhdHVzIjoiQ0FOQ0VMRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiIn19XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **coming-soon** | ✅ Ready ([Inspect](https://vercel.com/jackson-millers-projects/coming-soon/4kB2h793jT1Y5goSarahVeiJBTjF)) | [Visit Preview](https://coming-soon-git-feature-kanban-board-jackson-millers-projects.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/coming-soon-git-feature-kanban-board-jackson-millers-projects.vercel.app?via=pr-comment-feedback-link) | May 25, 2025 3:26am |
| **hire** | 🛑 Canceled ([Inspect](https://vercel.com/jackson-millers-projects/hire/4pXFsmSn4tvqXYuUub7hPbMN4tzg)) |  |  | May 25, 2025 3:26am |



author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-05-25T02:43:31Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This update introduces a kanban board feature for candidate management, including backend schema, API endpoints, and React components for boards, columns, and cards. Supporting UI components such as Badge, Checkbox Group, and Sheet are added to the component library with Storybook stories. Settings and form handling are updated for board management. Additional refinements include client mode enablement, mutation hook namespace updates, and a TODO for cascading deletion handling.

## Changes

| File(s)                                                                                     | Change Summary |
|---------------------------------------------------------------------------------------------|---------------|
| apps/hire/convex/boards.ts, apps/hire/convex/schema.ts, apps/hire/src/server/zod/board.ts   | Added backend schema, queries, mutations, and Zod validation for board management. |
| apps/hire/convex/candidates.ts, apps/hire/src/server/zod/candidate.ts                       | Added mutation and schema for updating candidate kanban stage. |
| apps/hire/src/app/kanban/KanbanBoard.tsx, apps/hire/src/app/kanban/KanbanColumn.tsx, apps/hire/src/app/kanban/KanbanCard.tsx, apps/hire/src/app/kanban/page.tsx | Introduced React components for kanban board, columns, cards, and page. |
| apps/hire/src/app/settings/_components/boards-tab.tsx, apps/hire/src/app/settings/_components/edit-board-sheet.tsx | Added React components for managing and editing boards in settings. |
| apps/hire/src/app/settings/page.tsx                                                         | Enabled client mode, updated description, and added BoardsTab to settings. |
| apps/hire/src/app/candidate/add/page.tsx                                                    | Updated mutation hook namespace for candidate creation. |
| apps/hire/src/components/candidate-form.tsx                                                | Refactored JSX syntax for form field rendering without changing logic. |
| libs/component-library/src/components/badge/badge.tsx, libs/component-library/src/components/badge/badge.stories.tsx | Added customizable Badge component and Storybook stories. |
| libs/component-library/src/components/checkbox/field-checkbox-group.tsx, libs/component-library/src/components/checkbox/index.ts | Added FieldCheckboxGroup component and index re-exports. |
| libs/component-library/src/components/form/use-app-form.ts                                  | Integrated FieldCheckboxGroup into form hook. |
| libs/component-library/src/components/index.ts                                              | Adjusted export paths for checkbox and added sheet component export. |
| libs/component-library/src/components/sheet/sheet.tsx, libs/component-library/src/components/sheet/sheet.stories.tsx | Added Sheet component suite and Storybook stories. |
| libs/component-library/tsconfig.storybook.json                                              | Removed explicit type declaration files from tsconfig. |
| package.json                                                                                | Updated `chromatic` and `eslint-plugin-playwright` dev dependencies versions. |
| TODO.md                                                                                     | Added TODO for handling cascading deletion scenarios. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant KanbanPage
    participant KanbanBoard
    participant API
    participant Backend

    User->>KanbanPage: Loads page
    KanbanPage->>KanbanBoard: Renders with orgId
    KanbanBoard->>API: Fetch kanban stages & candidates
    API->>Backend: Query data
    Backend-->>API: Return stages & candidates
    API-->>KanbanBoard: Provide data
    KanbanBoard->>User: Display kanban columns/cards

    User->>KanbanBoard: Drag candidate card to new stage
    KanbanBoard->>API: updateCandidateStage mutation
    API->>Backend: Update candidate's kanbanStageId
    Backend-->>API: Confirm update
    API-->>KanbanBoard: Success response
    KanbanBoard->>User: Update UI
```

```mermaid
sequenceDiagram
    participant User
    participant BoardsTab
    participant API
    participant Backend

    User->>BoardsTab: Add/Edit/Delete board
    BoardsTab->>API: addBoard / updateBoard / deleteBoard mutation
    API->>Backend: Perform DB operation
    Backend-->>API: Confirm operation
    API-->>BoardsTab: Success/failure response
    BoardsTab->>User: Update board list or show error
```

## Poem

> 🐇  
> A board appears, a card is moved,  
> New badges shine, their style improved.  
> Sheets now slide with gentle grace,  
> Checkboxes group in tidy space.  
> With kanban flow and settings neat,  
> This rabbit’s work is quite a feat!  
> —Hop, hop, hooray!

<!-- walkthrough_end -->
<!-- This is an auto-generated comment: all tool run failures by coderabbit.ai -->

> [!WARNING]
> There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.
> 
> <details>
> <summary>🔧 ESLint</summary>
> 
> > If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.
> 
> <details>
> <summary>apps/hire/convex/boards.ts</summary>
> 
> 
> Oops! Something went wrong! :(
> 
> ESLint: 9.27.0
> 
> ESLint couldn't find the plugin "@typescript-eslint/eslint-plugin".
> 
> (The package "@typescript-eslint/eslint-plugin" was not found when loaded as a Node module from the directory "".)
> 
> It's likely that the plugin isn't installed correctly. Try reinstalling by running the following:
> 
>     npm install @typescript-eslint/eslint-plugin@latest --save-dev
> 
> The plugin "@typescript-eslint/eslint-plugin" was referenced from the config file in " » plugin:@nx/typescript".
> 
> If you still can't figure out the problem, please see https://eslint.org/docs/latest/use/troubleshooting.
> 
> 
> </details>
> 
> <details>
> <summary>apps/hire/convex/candidates.ts</summary>
> 
> 
> Oops! Something went wrong! :(
> 
> ESLint: 9.27.0
> 
> ESLint couldn't find the plugin "@typescript-eslint/eslint-plugin".
> 
> (The package "@typescript-eslint/eslint-plugin" was not found when loaded as a Node module from the directory "".)
> 
> It's likely that the plugin isn't installed correctly. Try reinstalling by running the following:
> 
>     npm install @typescript-eslint/eslint-plugin@latest --save-dev
> 
> The plugin "@typescript-eslint/eslint-plugin" was referenced from the config file in " » plugin:@nx/typescript".
> 
> If you still can't figure out the problem, please see https://eslint.org/docs/latest/use/troubleshooting.
> 
> 
> </details>
> 
> <details>
> <summary>apps/hire/src/app/kanban/KanbanCard.tsx</summary>
> 
> 
> Oops! Something went wrong! :(
> 
> ESLint: 9.27.0
> 
> ESLint couldn't find the plugin "@typescript-eslint/eslint-plugin".
> 
> (The package "@typescript-eslint/eslint-plugin" was not found when loaded as a Node module from the directory "".)
> 
> It's likely that the plugin isn't installed correctly. Try reinstalling by running the following:
> 
>     npm install @typescript-eslint/eslint-plugin@latest --save-dev
> 
> The plugin "@typescript-eslint/eslint-plugin" was referenced from the config file in " » plugin:@nx/typescript".
> 
> If you still can't figure out the problem, please see https://eslint.org/docs/latest/use/troubleshooting.
> 
> 
> </details>
> 
> + 11 others
> 
> </details>

<!-- end of auto-generated comment: all tool run failures by coderabbit.ai -->


---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between d69c1b30939f285a2ed2a69cf499953cf5b2f635 and c14e3261ec1e9b733aa1d548f2fd5768f6c20192.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `pnpm-lock.yaml` is excluded by `!**/pnpm-lock.yaml`

</details>

<details>
<summary>📒 Files selected for processing (16)</summary>

* `TODO.md` (1 hunks)
* `apps/hire/convex/boards.ts` (1 hunks)
* `apps/hire/convex/candidates.ts` (2 hunks)
* `apps/hire/convex/schema.ts` (1 hunks)
* `apps/hire/src/app/kanban/KanbanBoard.tsx` (1 hunks)
* `apps/hire/src/app/kanban/KanbanCard.tsx` (1 hunks)
* `apps/hire/src/app/kanban/KanbanColumn.tsx` (1 hunks)
* `apps/hire/src/app/kanban/page.tsx` (1 hunks)
* `apps/hire/src/app/settings/_components/boards-tab.tsx` (1 hunks)
* `apps/hire/src/app/settings/_components/edit-board-sheet.tsx` (1 hunks)
* `apps/hire/src/components/candidate-form.tsx` (5 hunks)
* `libs/component-library/src/components/badge/badge.stories.tsx` (1 hunks)
* `libs/component-library/src/components/badge/badge.tsx` (1 hunks)
* `libs/component-library/src/components/checkbox/field-checkbox-group.tsx` (1 hunks)
* `libs/component-library/src/components/sheet/sheet.tsx` (1 hunks)
* `package.json` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* apps/hire/src/components/candidate-form.tsx

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (15)</summary>

* TODO.md
* package.json
* apps/hire/convex/candidates.ts
* apps/hire/src/app/kanban/page.tsx
* apps/hire/src/app/kanban/KanbanCard.tsx
* libs/component-library/src/components/badge/badge.tsx
* apps/hire/convex/schema.ts
* apps/hire/src/app/kanban/KanbanBoard.tsx
* apps/hire/src/app/settings/_components/boards-tab.tsx
* apps/hire/src/app/kanban/KanbanColumn.tsx
* apps/hire/convex/boards.ts
* apps/hire/src/app/settings/_components/edit-board-sheet.tsx
* libs/component-library/src/components/checkbox/field-checkbox-group.tsx
* libs/component-library/src/components/badge/badge.stories.tsx
* libs/component-library/src/components/sheet/sheet.tsx

</details>

</details>
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=Jacksondr5/monorepo&utm_content=72):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: nx-cloud[bot]
created_at: 2025-05-25T02:43:46Z
body: 
View your [CI Pipeline Execution ↗](https://cloud.nx.app/cipes/68328c89343ab56de53b2fd4?utm_source=pull-request&utm_medium=comment) for commit c14e3261ec1e9b733aa1d548f2fd5768f6c20192.

| Command | Status | Duration | Result |
|---------|--------|----------:|--------|
| `nx affected -t lint test build build-storybook ...` | ✅ Succeeded | 1m 18s | [View ↗](https://cloud.nx.app/runs/5nYyyALoNQ?utm_source=pull-request&utm_medium=comment) |
| `nx-cloud record -- nx format:check` | ✅ Succeeded | 5s | [View ↗](https://cloud.nx.app/runs/HrXxvtIK0T?utm_source=pull-request&utm_medium=comment) |

---

☁️ [Nx Cloud](https://cloud.nx.app?utm_source=pull-request&utm_medium=comment) last updated this comment at `2025-05-25 03:22:27` UTC

<!-- NX_CLOUD_APP_COMMENT_END -->
author_association: CONTRIBUTOR