カンバンボードKanbanBoardExperimental
フラットな項目を列へ分類し、件数、空列、選択可能カードを含むカンバンとして表示します。
プレビュー
未着手
1進行中
3レビュー
1完了
1状態とバリエーション
選択可能カード
onCardSelect を渡すとカードがボタンになります。
未着手
1進行中
3レビュー
1完了
1空列
該当カードがない列には emptyLabel を表示します。
未着手
1進行中
3レビュー
0なし
完了
1件数なし
列数が十分に少ない場合は showCount={false} で件数を隠せます。
未着手
進行中
レビュー
完了
プロパティ
表は横にスクロールできます
| プロパティ | 型 | 初期値 | 説明 |
|---|---|---|---|
| columns | KanbanColumn[] | - | 順序付きの列です。tone は見出しのアクセント点に使います。 |
| items | T[] | - | 列分け前のフラットなカード配列です。 |
| getItemId | (item: T) => string | - | カードの安定したキーを返します。 |
| getColumnId | (item: T) => string | - | カードが入る列 id を返します。 |
| renderCard | (item: T) => ReactNode | - | カード本体を描画します。関数propのため Client Component からのみ渡すこと(Server Component から渡すと next build が落ちる)。JSX を返すため serializable な代替は無く、RSC からは "use client" ラッパーで包む。(#338) |
| onCardSelect | (item: T) => void | - | 渡すとカードがボタンになり、クリックやキーボードで選択できます。 |
| showCount | boolean | true | 列見出しに件数 Badge を表示します。 |
| emptyLabel | ReactNode | "なし" | 空列のプレースホルダーです。 |
| columnWidth | number | 260 | 横スクロール内の列幅です。 |
使い方
import * as React from "react";
import {
Badge,
KanbanBoard,
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
type KanbanColumn,
} from "@gunjo/ui";
type Task = { id: string; status: string; title: string; assignee: string; priority: string; due: string; summary: string };
const columns: KanbanColumn[] = [
{ id: "todo", title: "未着手", tone: "muted" },
{ id: "doing", title: "進行中", tone: "info" },
{ id: "review", title: "レビュー", tone: "warning" },
{ id: "done", title: "完了", tone: "success" },
];
const tasks: Task[] = [
{
id: "t1",
status: "todo",
title: "申請フォームの入力確認",
assignee: "青井",
priority: "通常",
due: "7月10日",
summary: "必須項目、エラー文言、入力補助の整合性を確認します。",
},
{
id: "t2",
status: "doing",
title: "通知メールの文面調整",
assignee: "三宅",
priority: "高",
due: "7月8日",
summary: "審査完了と差し戻し時のメール文面を最終確認します。",
},
{
id: "t5",
status: "doing",
title: "権限変更ログの表示",
assignee: "宇佐美",
priority: "通常",
due: "7月8日",
summary: "管理者が確認できる変更履歴の表示項目を整理します。",
},
{
id: "t6",
status: "doing",
title: "添付ファイルの容量確認",
assignee: "青井",
priority: "通常",
due: "7月9日",
summary: "アップロード上限とエラー表示が仕様どおりか確認します。",
},
{
id: "t3",
status: "review",
title: "公開前チェックリスト",
assignee: "黒川",
priority: "高",
due: "7月9日",
summary: "公開前レビューの残項目と承認者を確認します。",
},
{
id: "t4",
status: "done",
title: "権限ロールの棚卸し",
assignee: "宇佐美",
priority: "通常",
due: "完了",
summary: "既存ロールの利用状況を整理し、不要な権限を削除しました。",
},
];
export function ReviewKanban() {
const [selectedId, setSelectedId] = React.useState<string | null>(null);
const [
portalContainer,
setPortalContainer,
] = React.useState<HTMLDivElement | null>(null);
const selectedTask = selectedId ? tasks.find((task) => task.id === selectedId) : undefined;
const selectedColumn = selectedTask ? columns.find((column) => column.id === selectedTask.status) : undefined;
return (
<div ref={setPortalContainer} className="relative flex w-full max-w-4xl flex-col gap-4 overflow-hidden rounded-lg border bg-card p-4">
<KanbanBoard<Task>
columns={columns}
items={tasks}
getItemId={(task) => task.id}
getColumnId={(task) => task.status}
emptyLabel="なし"
onCardSelect={(task) => setSelectedId(task.id)}
renderCard={({ title, assignee, priority }) => (
<div className="flex flex-col gap-1">
<span className="text-sm font-medium">{title}</span>
<span className="text-xs text-muted-foreground">担当: {assignee}</span>
<Badge variant={priority === "高" ? "destructive" : "secondary"}>{priority}</Badge>
</div>
)}
/>
<Sheet modal={false} open={selectedTask != null} onOpenChange={(open) => !open && setSelectedId(null)}>
<SheetContent
portalContainer={portalContainer}
overlayClassName="rounded-lg"
closeLabel="閉じる"
className="w-80 max-w-[calc(100%-1rem)] overflow-y-auto p-4"
>
{selectedTask ? (
<>
<SheetHeader className="pr-8">
<SheetTitle>タスク詳細</SheetTitle>
</SheetHeader>
<div className="rounded-lg border border-l-4 border-warning-border bg-warning-subtle/65 p-3 text-sm shadow-sm">
<div className="flex items-start justify-between gap-3">
<p className="min-w-0 truncate text-base font-semibold text-foreground">{selectedTask.title}</p>
<Badge variant={selectedTask.priority === "高" ? "destructive" : "outline"}>{selectedTask.priority}</Badge>
</div>
<p className="mt-2 text-sm text-muted-foreground">{selectedTask.summary}</p>
<dl className="mt-3 grid gap-2 text-xs">
<div><dt className="text-muted-foreground">ステータス</dt><dd className="font-medium text-foreground">{selectedColumn?.title}</dd></div>
<div><dt className="text-muted-foreground">担当</dt><dd className="font-medium text-foreground">{selectedTask.assignee}</dd></div>
<div><dt className="text-muted-foreground">期限</dt><dd className="font-medium text-foreground">{selectedTask.due}</dd></div>
</dl>
</div>
</>
) : null}
</SheetContent>
</Sheet>
</div>
);
}