座席表SeatMapExperimental
座席や区画を2次元グリッドで表示し、空席、予約済み、確保中、選択状態を扱います。
プレビュー
A
B
C
D
E
F
空席特別席(非常口/足元ゆったり 等)選択中予約済確保中
選択中: 12A
状態とバリエーション
選択可能
空席を選択すると selectedIds が更新されます。特別席、予約済、確保中、選択中は Tooltip で理由を示します。
A
B
C
D
E
F
空席特別席(非常口/足元ゆったり 等)選択中予約済確保中
選択中: 12A
小さい座席表
通路の位置と列数を変えて小型の配置にも対応します。
A
B
C
D
空席特別席(非常口/足元ゆったり 等)選択中予約済確保中
選択中: 12A
凡例なし
周囲に凡例がある画面では hideLegend を使えます。状態の説明は周囲のUIに置く前提です。
A
B
C
D
E
F
選択中: 12A
プロパティ
表は横にスクロールできます
| プロパティ | 型 | 初期値 | 説明 |
|---|---|---|---|
| columns | (string | null)[] | - | 列IDの並びです。null は通路として空けます。 |
| seats | Seat[] | - | 座席データです。id/row/col/state/type/position/fee を持ちます。 |
| Seat.reason | ReactNode | function | - | 特別席、予約済、確保中、選択中など、座席状態の理由や補足をホバー/フォーカスで表示します。 |
| selectedIds | string[] | - | 選択中の座席IDです。 |
| maxSelectable | number | - | 選択上限です。 |
| onToggle | (seatId: string) => void | - | 操作可能な空席を選択/解除した時に呼ばれます。 |
| formatFee | (fee: number) => string | - | アクセシブル名に含める料金表示です。関数propのため Client Component からのみ渡すこと(Server Component から渡すと next build が落ちる)。RSC 安全な整形には feeFormat を使う。 |
| feeFormat | "number" | "compact" | "integer" | Intl.NumberFormatOptions | - | シリアライズ可能な料金フォーマット=formatFee の RSC 安全な代替(例: { style: "currency", currency: "JPY" })。formatFee 指定時は無視。en-US ロケール固定。(#338) |
| labels | SeatMapLabels | - | 状態、位置、凡例、座席名をローカライズします。 |
| showHeaders / hideLegend | boolean | - | 列/行ヘッダーと凡例の表示を制御します。 |
使い方
import * as React from "react";
import { SeatMap, type Seat } from "@gunjo/ui";
const columns = ["A", "B", "C", null, "D", "E", "F"];
const seats: Seat[] = [
{
id: "12A",
row: 12,
col: "A",
position: "window",
type: "非常口座席",
fee: 1500,
reason: (_seat, context) =>
context.selected
? "選択中です。非常口座席のため、緊急時の補助に同意できる利用者だけ選択できます。"
: "非常口座席のため、緊急時の補助に同意できる利用者だけ選択できます。",
},
{ id: "12B", row: 12, col: "B", state: "occupied", reason: "すでに予約済みのため選択できません。" },
{ id: "12C", row: 12, col: "C", position: "aisle" },
{ id: "12D", row: 12, col: "D", position: "aisle", type: "足元ゆったり", fee: 1200, reason: "足元ゆったり席です。追加料金がかかります。" },
{ id: "12E", row: 12, col: "E", state: "held", reason: "別の利用者が決済手続き中のため、数分間選択できません。" },
{ id: "12F", row: 12, col: "F", position: "window" },
{ id: "13A", row: 13, col: "A", position: "window" },
{ id: "13B", row: 13, col: "B" },
{ id: "13C", row: 13, col: "C", position: "aisle" },
{ id: "13D", row: 13, col: "D", position: "aisle" },
{ id: "13E", row: 13, col: "E" },
{ id: "13F", row: 13, col: "F", position: "window", state: "occupied", reason: "すでに予約済みのため選択できません。" },
];
export function SeatPicker() {
const [selectedIds, setSelectedIds] = React.useState(["12A"]);
const toggle = (seatId: string) =>
setSelectedIds((current) => current.includes(seatId) ? current.filter((id) => id !== seatId) : [...current, seatId].slice(0, 2));
return (
<div className="flex w-full max-w-lg flex-col gap-3 rounded-lg border bg-card p-4">
<SeatMap
columns={columns}
seats={seats}
selectedIds={selectedIds}
maxSelectable={2}
onToggle={toggle}
label="座席表"
formatFee={(fee) => "¥" + fee.toLocaleString("ja-JP")}
/>
<p className="rounded-md border bg-muted/30 px-3 py-2 text-sm text-muted-foreground" aria-live="polite">
選択中: {selectedIds.join(", ") || "未選択"}
</p>
</div>
);
}