"use client"; import type { MatrixGroupConfig, FieldType } from "@/types/form"; import type { UseFormRegister, FieldValues } from "react-hook-form"; interface MatrixGroupProps { group: MatrixGroupConfig; register: UseFormRegister; } /** * Renders a matrix of repeating fields as an HTML table. * * - Row labels live in the leftmost column (sticky on horizontal scroll). * - Column headers carry the period labels (M1..M12, Q1..Q4, …). * - Each row's `type` controls the cell input rendering: currency cells get * a $ suffix on the row label rather than per-cell to save space; percent * cells append %. * - On narrow viewports the table scrolls horizontally; the metric column * stays pinned via position:sticky with a paper background and a hairline * ink rule on its right edge so the boundary reads cleanly during scroll. * * Cells are still individual react-hook-form fields, registered by * `field.fields[colIndex]`. The matrix only changes layout — submission and * validation behave exactly like the standalone-input version. */ export function MatrixGroup({ group, register }: MatrixGroupProps) { return (

{group.label}

{group.intro && (

{group.intro}

)}
{group.cols.map((c) => ( ))} {group.rows.map((row, rowIdx) => { const zebra = rowIdx % 2 === 1; return ( {row.fields.map((fieldName, colIdx) => ( ))} ); })}
Metric {c.label}
{decoratedRowLabel(row.label, row.type)}
); } function decoratedRowLabel(label: string, type: FieldType): string { if (type === "currency") return `${label} ($)`; if (type === "percent") return `${label} (%)`; return label; } interface CellInputProps { fieldName: string; type: FieldType; label: string; // for aria-label only register: UseFormRegister; } /** * A compact, table-cell-sized input. No visible label (the row + column * already provide context); a screen-reader label is set via aria-label. * Supports currency / percent / number / text — uniform sizing across cells. */ function CellInput({ fieldName, type, label, register }: CellInputProps) { const isNumeric = type === "currency" || type === "percent" || type === "number"; const baseClass = "w-full rounded border border-rule-soft bg-paper px-2 py-1 text-right text-ink " + "tabular-nums shadow-sm transition " + "focus:border-leaf-500 focus:outline-none focus:ring-2 focus:ring-leaf-500/30"; if (isNumeric) { return ( ); } return ( ); }