"use client" import * as React from "react" import { PlusIcon } from "lucide-react" import { Button } from "@/components/ui/button" import { cn } from "@/lib/utils" export type SpreadsheetColumn = { key: string label: React.ReactNode width?: number | string align?: "left" | "center" | "right" readOnly?: boolean formatter?: (value: string, row: SpreadsheetRow) => React.ReactNode } export type SpreadsheetRow = { key: string cells: Record readOnly?: boolean } export type SpreadsheetSelection = { rowKey: string columnKey: string } export type SpreadsheetProps = Omit, "onChange"> & { columns: SpreadsheetColumn[] rows?: SpreadsheetRow[] defaultRows?: SpreadsheetRow[] onRowsChange?: (rows: SpreadsheetRow[], selection?: SpreadsheetSelection) => void onCellChange?: (value: string, selection: SpreadsheetSelection, rows: SpreadsheetRow[]) => void selectedCell?: SpreadsheetSelection defaultSelectedCell?: SpreadsheetSelection onSelectedCellChange?: (selection: SpreadsheetSelection) => void readOnly?: boolean showRowNumbers?: boolean showAddRow?: boolean addRowLabel?: React.ReactNode empty?: React.ReactNode } function alignClassName(align: SpreadsheetColumn["align"]) { if (align === "center") return "text-center" if (align === "right") return "text-right" return "text-left" } function createEmptyRow(columns: SpreadsheetColumn[], index: number): SpreadsheetRow { return { key: `row-${Date.now()}-${index}`, cells: Object.fromEntries(columns.map((column) => [column.key, ""])), } } function Spreadsheet({ columns, rows: rowsProp, defaultRows = [], onRowsChange, onCellChange, selectedCell, defaultSelectedCell, onSelectedCellChange, readOnly = false, showRowNumbers = true, showAddRow = true, addRowLabel = "Add row", empty = "No rows yet.", className, ...props }: SpreadsheetProps) { const [internalRows, setInternalRows] = React.useState(defaultRows) const [internalSelectedCell, setInternalSelectedCell] = React.useState(defaultSelectedCell) const rows = rowsProp ?? internalRows const activeCell = selectedCell ?? internalSelectedCell const commitRows = (nextRows: SpreadsheetRow[], selection?: SpreadsheetSelection) => { if (rowsProp === undefined) setInternalRows(nextRows) onRowsChange?.(nextRows, selection) } const selectCell = (selection: SpreadsheetSelection) => { if (selectedCell === undefined) setInternalSelectedCell(selection) onSelectedCellChange?.(selection) } const updateCell = (selection: SpreadsheetSelection, value: string) => { const nextRows = rows.map((row) => row.key === selection.rowKey ? { ...row, cells: { ...row.cells, [selection.columnKey]: value } } : row) commitRows(nextRows, selection) onCellChange?.(value, selection, nextRows) } const addRow = () => { const nextRows = [...rows, createEmptyRow(columns, rows.length + 1)] commitRows(nextRows) } return (
{showRowNumbers ? : null} {columns.map((column) => ( ))} {rows.length ? rows.map((row, rowIndex) => ( {showRowNumbers ? : null} {columns.map((column) => { const selection = { rowKey: row.key, columnKey: column.key } const selected = activeCell?.rowKey === row.key && activeCell.columnKey === column.key const value = row.cells[column.key] ?? "" const cellReadOnly = readOnly || row.readOnly || column.readOnly return ( ) })} )) : ( )}
# {column.label}
{rowIndex + 1} {cellReadOnly && column.formatter ? (
{column.formatter(value, row)}
) : ( selectCell(selection)} onChange={(event) => updateCell(selection, event.target.value)} /> )}
{empty}
{showAddRow && !readOnly ? : null}
) } export { Spreadsheet }