スケジュールグリッドScheduleGridExperimental

A 2-D matrix grid: a row axis × a column axis of rich content cells with a frozen first column + sticky header row, role=grid semantics (rowheaders / columnheaders / gridcells with composed accessible names), roving-tabindex arrow-key navigation, per-cell tone (a destructive flag ring), an unavailable slot treatment, and a contained horizontal scroll that does not push the page on mobile. For any rows×columns matrix of rich navigable/editable cells — timetables (periods×days), gradebooks (students×subjects), shift rosters, comparison/cohort matrices, availability and room/resource booking grids. (Not for sortable list data — that is DataTable; not for value-by-color heatmaps — that is HeatmapChart.)

プレビュー

時限
1限8:50
数学
佐藤2-A
英語
鈴木2-A
国語
高橋2-A
2限9:50
理科
田中理科室
数学競合
佐藤2-A
数学競合
佐藤視聴覚室
3限10:50
体育
渡辺体育館
数学
佐藤2-A
英語
鈴木2-A
4限11:50
音楽
山本音楽室
理科
田中理科室

セルを選ぶと表示。矢印キーで移動・Enter/Spaceで選択。佐藤先生が火2限と木2限で重複=競合。

Props

表は横にスクロールできます
プロパティ初期値説明
rowsScheduleAxisItem[]-Row axis (e.g. periods) — rendered as rowheaders down the left. { id, label, sublabel?, ariaLabel? }.
columnsScheduleAxisItem[]-Column axis (e.g. days) — rendered as columnheaders across the top.
cellsScheduleCell[]-Cells addressed by rowId + colId. Slots with no cell render renderEmpty.
ScheduleCell{ rowId; colId; content?; tone?; description?; ariaLabel?; onSelect?; unavailable? }-content = cell body; tone (destructive adds a conflict ring); onSelect makes it an activatable button; unavailable = dashed non-interactive; description is appended to the auto '<column> <row>' accessible name; ariaLabel overrides it fully.
labelReactNode-Accessible name for the whole grid (required).
cornerLabelReactNode-Top-left corner header.
minColumnWidthnumber112Min px per column before horizontal scroll kicks in.
rowHeaderWidthnumber72Width (px) of the sticky row-header column.
renderEmpty(row, column) => ReactNode-Render a slot that has no cell (available/empty). Default a muted dash.
unavailableLabelstring"利用不可"Announced (and shown) for an unavailable slot.

Usage

import { ScheduleGrid, type ScheduleAxisItem, type ScheduleCell } from "@gunjo/ui"

const periods: ScheduleAxisItem[] = [
  { id: "p1", label: "1限", sublabel: "8:50" },
  { id: "p2", label: "2限", sublabel: "9:50" },
]
const days: ScheduleAxisItem[] = [
  { id: "mon", label: "月", ariaLabel: "月曜" },
  { id: "tue", label: "火", ariaLabel: "火曜" },
]

const cells: ScheduleCell[] = [
  {
    rowId: "p1", colId: "mon",
    content: <LessonCard subject="数学" teacher="佐藤" room="2-A" />,
    description: "数学、佐藤、2-A",       // appended to the "<col> <row>" a11y name
    onSelect: () => openEditor("p1", "mon"),
  },
  {
    rowId: "p2", colId: "tue",
    tone: "destructive",                  // conflict → ring + tone
    description: "数学、佐藤、競合あり",
    content: <LessonCard subject="数学" teacher="佐藤" conflict />,
    onSelect: () => openEditor("p2", "tue"),
  },
  { rowId: "p2", colId: "mon", unavailable: true }, // e.g. no class this slot
]

<ScheduleGrid label="2年A組 週間時間割" cornerLabel="時限" rows={periods} columns={days} cells={cells} />