旅程ItineraryExperimental
日別またはフラットな旅程を、時刻、種別アイコン、詳細、操作付きの縦タイムラインで表示します。
プレビュー
1日目
6月27日(土)・東京 → ホノルル2日目
6月28日(日)・オアフ島状態とバリエーション
日別グループ
days を渡すと、日ごとの見出しとタイムラインで表示します。
1日目
6月27日(土)・東京 → ホノルル2日目
6月28日(日)・オアフ島フラット
items だけを渡すと、日別見出しなしの短い移動予定として使えます。
印の色みのすべて
tone は6種類。色みは種類の目印にすぎないので、印の中には必ずアイコンを置き、時刻は「終日」のような文字も受け取ります。
- 09:00集合第2ターミナル 3階 Fカウンター前
- 10:20NH852 便羽田 → 那覇
- 13:40離島フェリー泊港 3番桟橋
- 15:00チェックイン予約は確定済み
- 18:05乗り継ぎ 25分遅延すると次の便に間に合いません
- 終日自由行動予定は入れていません
プロパティ
表は横にスクロールできます
| プロパティ | 型 | 初期値 | 説明 |
|---|---|---|---|
| days | ItineraryDay[] | - | 日別グループです。各日が見出しと timeline を持ちます。 |
| items | ItineraryItem[] | - | 日別グループなしのフラットな項目です。days がある場合は無視します。 |
| ItineraryItem.time | ReactNode | - | 時刻や終日などのラベルです。 |
| ItineraryItem.icon | ReactNode | - | 種別を示すマーカーアイコンです。 |
| ItineraryItem.tone | "default" | "primary" | "info" | "success" | "warning" | "muted" | "default" | マーカーのトーンです。 |
| ItineraryItem.title / description | ReactNode | - | 主要行と副次行です。 |
| ItineraryItem.content | ReactNode | - | 便詳細や地図などのリッチ内容を置くスロットです。 |
| ItineraryItem.trailing | ReactNode | - | 右側に置く Badge やメタ情報です。 |
| ItineraryItem.onSelect | () => void | - | 行を44px以上のクリック可能領域にします。 |
使い方
import * as React from "react";
import { Badge, Itinerary, Sheet, SheetContent, SheetHeader, SheetTitle, type ItineraryDay } from "@gunjo/ui";
import { IconBuildingPavilion, IconPlane } from "@tabler/icons-react";
export function TripItinerary() {
const [selectedId, setSelectedId] = React.useState<string | null>(null);
const [portalContainer, setPortalContainer] = React.useState<HTMLDivElement | null>(null);
const days: ItineraryDay[] = [
{
label: "1日目",
sublabel: "6月27日(土)・東京 → ホノルル",
items: [
{
id: "flight-out",
time: "21:55 発 → 10:25 着(現地)",
icon: <IconPlane className="size-4" />,
tone: "primary",
title: "NH182 羽田(HND) → ホノルル(HNL)",
description: "往路・所要 約7時間30分・座席 32K",
trailing: <Badge variant="secondary">往路</Badge>,
onSelect: () => setSelectedId("flight-out"),
},
{
id: "hotel",
time: "チェックイン 15:00",
icon: <IconBuildingPavilion className="size-4" />,
tone: "success",
title: "ハイアット リージェンシー ワイキキ",
description: "3泊・予約番号 RZ8K4P",
onSelect: () => setSelectedId("hotel"),
},
],
},
];
const itemDetails = {
"flight-out": {
status: "確定",
location: "羽田空港 第3ターミナル",
reservation: "ANA 予約番号 8K2PQA / 座席 32K",
next: "出発3時間前までにオンラインチェックインを完了",
},
hotel: {
status: "予約済み",
location: "2424 Kalakaua Ave, Honolulu",
reservation: "RZ8K4P / 3泊 / オーシャンビュー",
next: "到着後にパスポートと予約番号を提示",
},
};
const toneStyles = {
primary: "border-primary bg-primary/10",
success: "border-success-border bg-success-subtle/55",
} as const;
const visibleItems = days.flatMap((day) => day.items);
const selectedItem = selectedId ? visibleItems.find((item) => item.id === selectedId) : undefined;
const selectedDetail = selectedId ? itemDetails[selectedId as keyof typeof itemDetails] : undefined;
const selectedTone = selectedItem?.tone === "success" ? toneStyles.success : toneStyles.primary;
return (
<div ref={setPortalContainer} className="relative flex w-full max-w-xl flex-col gap-4 overflow-hidden rounded-lg border bg-card p-4">
<Itinerary days={days} />
<Sheet modal={false} open={selectedItem != null && selectedDetail != 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">
{selectedItem && selectedDetail ? (
<>
<SheetHeader className="pr-8">
<SheetTitle>旅程詳細</SheetTitle>
</SheetHeader>
<div className={["rounded-lg border border-l-4 p-3 text-sm shadow-sm", selectedTone].join(" ")}>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<p className="mt-1 truncate text-base font-semibold text-foreground">{selectedItem.title}</p>
</div>
<Badge variant="outline" className="shrink-0 border-border bg-background text-foreground shadow-sm">
{selectedDetail.status}
</Badge>
</div>
<dl className="mt-3 grid gap-2 text-xs">
<div>
<dt className="text-muted-foreground">予定時刻</dt>
<dd className="font-medium text-foreground">{selectedItem.time}</dd>
</div>
<div>
<dt className="text-muted-foreground">場所</dt>
<dd className="font-medium text-foreground">{selectedDetail.location}</dd>
</div>
<div>
<dt className="text-muted-foreground">予約情報</dt>
<dd className="font-medium text-foreground">{selectedDetail.reservation}</dd>
</div>
<div>
<dt className="text-muted-foreground">次の行動</dt>
<dd className="font-medium text-foreground">{selectedDetail.next}</dd>
</div>
</dl>
</div>
</>
) : null}
</SheetContent>
</Sheet>
</div>
);
}