import { Checkbox, Input, Select } from "@cloudflare/kumo"; import * as React from "react"; import type { FieldWidgetProps, GridAxisDef } from "../shared/types"; import { normalizeGrid } from "../shared/utils"; type CellType = "toggle" | "text" | "number" | "select"; interface SelectOption { label: string; value: string; } /** * Grid widget — a two-dimensional matrix of rows × columns with configurable * cell types. Stores as a nested JSON object. * * Seed usage: * { * "slug": "availability", * "type": "json", * "widget": "field-kit:grid", * "options": { * "rows": [ * { "key": "mon", "label": "Monday" }, * { "key": "tue", "label": "Tuesday" } * ], * "columns": [ * { "key": "morning", "label": "Morning" }, * { "key": "afternoon", "label": "Afternoon" } * ], * "cell": "toggle" * } * } * * Stored value: { "mon": { "morning": true, "afternoon": false }, ... } */ export function Grid({ value, onChange, label, required, options, minimal }: FieldWidgetProps) { const rows = (options?.rows as GridAxisDef[] | undefined) ?? []; const columns = (options?.columns as GridAxisDef[] | undefined) ?? []; const cellType = ((options?.cell as string | undefined) ?? "toggle") as CellType; const cellOptions = (options?.cellOptions as SelectOption[] | string[] | undefined) ?? []; const helpText = options?.helpText as string | undefined; const data = normalizeGrid(value, rows, columns); const dataRef = React.useRef(data); dataRef.current = data; const normalizedCellOptions: SelectOption[] = React.useMemo( () => cellOptions.map((opt) => (typeof opt === "string" ? { label: opt, value: opt } : opt)), [cellOptions], ); const updateCell = React.useCallback( (rowKey: string, colKey: string, cellValue: unknown) => { const rowData = { ...dataRef.current[rowKey], [colKey]: cellValue }; onChange({ ...dataRef.current, [rowKey]: rowData }); }, [onChange], ); const toggleCell = React.useCallback( (rowKey: string, colKey: string, next: boolean) => { const rowData = { ...dataRef.current[rowKey], [colKey]: next }; onChange({ ...dataRef.current, [rowKey]: rowData }); }, [onChange], ); if (rows.length === 0 || columns.length === 0) { return (
{!minimal && ( )}

Widget misconfigured

The field's options.rows and options.columns arrays are required. Define them in your seed file to use this widget.

); } return (
{!minimal && ( )}
{columns.map((col) => ( ))} {rows.map((row, rowIdx) => ( {columns.map((col) => { const cellValue = data[row.key]?.[col.key]; return ( ); })} ))}
 
{col.image && ( {col.label} )} {col.label}
{row.image && ( {row.label} )} {row.label}
{helpText &&

{helpText}

}
); } interface CellInputProps { type: CellType; value: unknown; options: SelectOption[]; rowKey: string; colKey: string; onToggle: (rowKey: string, colKey: string, next: boolean) => void; onUpdate: (rowKey: string, colKey: string, value: unknown) => void; ariaLabel: string; } function CellInput({ type, value, options, rowKey, colKey, onToggle, onUpdate, ariaLabel, }: CellInputProps) { switch (type) { case "toggle": return (
onToggle(rowKey, colKey, !!next)} />
); case "text": return ( onUpdate(rowKey, colKey, e.target.value)} /> ); case "number": return ( onUpdate(rowKey, colKey, e.target.value === "" ? undefined : Number(e.target.value)) } /> ); case "select": return (