ガントチャートGanttExperimental
リソース行と時間軸にバーを配置し、重なりや区間分割を含む予定を表示するガントチャートです。
プレビュー
21
22
23
24
6/25
26
27
28
第1ライン組立
第2ライン塗装
第3ライン検査
第4ライン梱包
第5ライン出荷前確認
保全チーム設備点検
状態とバリエーション
重なりレーン
同じ行で期間が重なるバーは自動で縦に積まれます。
21
22
23
24
6/25
26
27
28
第1ライン組立
第2ライン塗装
第3ライン検査
第4ライン梱包
第5ライン出荷前確認
保全チーム設備点検
区間分割
segments を渡すと、1本のバー内に工程や折返し区間を表現できます。
21
22
23
24
6/25
26
27
28
第1ライン組立
第2ライン塗装
第3ライン検査
第4ライン梱包
第5ライン出荷前確認
保全チーム設備点検
時間単位
resolution="hour" は1日の配車や運行表に使います。
08:00
10:00
12:00
14:00
16:00
車両 12運行日
プロパティ
表は横にスクロールできます
| プロパティ | 型 | 初期値 | 説明 |
|---|---|---|---|
| rows | GanttRow[] | - | 左軸のリソース行です。id、label、sublabel を渡します。 |
| items | GanttItem[] | - | 時間軸に配置するバーです。rowId、start、end、label、tone、segments を持てます。 |
| startDate / endDate | string | Date | - | 表示する時間範囲です。 |
| today | string | Date | - | 今日線として表示する日付です。SSR のため明示指定します。 |
| resolution | "day" | "hour" | "day" | 日単位または時間単位の軸を選びます。 |
| laneHeight | number | 28 | 重なりを積む1レーンの高さです。 |
| rowLabelWidth | number | 120 | 固定される行ラベル列の幅です。 |
| dayWidth / hourWidth | number | 44 | 横スクロール内の軸1単位の幅です。 |
| onSelectItem | (item: GanttItem) => void | - | バー選択時に呼びます。 |
使い方
import * as React from "react";
import {
Badge,
Gantt,
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
type GanttItem,
type GanttRow,
} from "@gunjo/ui";
const rows: GanttRow[] = [
{ id: "l1", label: "第1ライン", sublabel: "組立" },
{ id: "l2", label: "第2ライン", sublabel: "塗装" },
];
const items: GanttItem[] = [
{ id: "j1", rowId: "l1", start: "2026-06-22", end: "2026-06-24", label: "A-101", tone: "info" },
{ id: "j3", rowId: "l2", start: "2026-06-22", end: "2026-06-25", label: "B-201", tone: "success" },
{ id: "j4", rowId: "l2", start: "2026-06-23", end: "2026-06-26", label: "B-202", tone: "warning" },
];
const itemDetails = {
j1: { status: "進行中", owner: "生産管理 A", next: "塗装前検査" },
j3: { status: "進行中", owner: "塗装班", next: "乾燥室へ移動" },
j4: { status: "要確認", owner: "品質保証", next: "色差チェック" },
};
const toneStyles = {
default: { panel: "border-border bg-background" },
primary: { panel: "border-primary bg-primary/10" },
info: { panel: "border-info-border bg-info-subtle/55" },
success: { panel: "border-success-border bg-success-subtle/55" },
warning: { panel: "border-warning-border bg-warning-subtle/65" },
} as const;
export function ProductionGantt() {
const [selectedId, setSelectedId] = React.useState(items[0].id);
const [drawerOpen, setDrawerOpen] = React.useState(false);
const [previewRoot, setPreviewRoot] = React.useState<HTMLDivElement | null>(null);
const [portalContainer, setPortalContainer] = React.useState<HTMLElement | null>(null);
const [isCompactLayout, setIsCompactLayout] = React.useState(false);
const selectedItem = items.find((item) => item.id === selectedId) ?? items[0];
const selectedRow = rows.find((row) => row.id === selectedItem.rowId);
const selectedDetail = itemDetails[selectedItem.id as keyof typeof itemDetails];
const selectedTone = toneStyles[selectedItem.tone ?? "default"];
React.useEffect(() => {
if (!previewRoot) return;
const updateLayout = () => {
const width = previewRoot.offsetWidth || previewRoot.getBoundingClientRect().width;
setIsCompactLayout(width < 800);
};
updateLayout();
const observer = new ResizeObserver(updateLayout);
observer.observe(previewRoot);
return () => observer.disconnect();
}, [previewRoot]);
const setPreviewNode = React.useCallback((node: HTMLDivElement | null) => {
setPreviewRoot(node);
setPortalContainer(node);
}, []);
return (
<div ref={setPreviewNode} className="relative flex w-full max-w-4xl flex-col gap-4 overflow-visible rounded-lg border bg-card p-4">
<div className="grid min-w-0 gap-4">
<Gantt
rows={rows}
items={items}
startDate="2026-06-21"
endDate="2026-06-29"
today="2026-06-24"
label="生産ライン計画"
dayWidth={52}
onSelectItem={(item) => {
setSelectedId(item.id);
setDrawerOpen(true);
}}
/>
</div>
<Sheet open={drawerOpen} onOpenChange={setDrawerOpen}>
<SheetContent
side={isCompactLayout ? "bottom" : "right"}
portalContainer={portalContainer}
closeLabel="閉じる"
overlayClassName="bg-overlay/45"
className={
isCompactLayout
? "max-h-[calc(100%-2rem)] overflow-y-auto rounded-t-lg p-4"
: "w-[min(360px,calc(100%-2rem))] max-w-sm overflow-y-auto p-4"
}
>
<SheetHeader className="pr-8 text-left">
<SheetTitle>工程詳細 / {selectedItem.label}</SheetTitle>
</SheetHeader>
<div className={["mt-4 rounded-lg border border-l-4 p-2 text-sm shadow-sm", selectedTone.panel].join(" ")}>
<div className="flex justify-end">
<Badge variant="outline" className="shrink-0 border-border bg-background text-foreground shadow-sm">
{selectedDetail.status}
</Badge>
</div>
<dl className="mt-1.5 grid gap-1 text-xs">
<div><dt className="text-muted-foreground">担当ライン</dt><dd className="font-medium text-foreground">{selectedRow?.label}</dd></div>
<div><dt className="text-muted-foreground">期間</dt><dd className="font-medium text-foreground">{String(selectedItem.start)} - {String(selectedItem.end)}</dd></div>
<div><dt className="text-muted-foreground">責任者</dt><dd className="font-medium text-foreground">{selectedDetail.owner}</dd></div>
<div><dt className="text-muted-foreground">次の作業</dt><dd className="font-medium text-foreground">{selectedDetail.next}</dd></div>
</dl>
</div>
</SheetContent>
</Sheet>
</div>
);
}