import { createContext, use, useEffect, useState, type ReactNode } from "react"; import { View, Text } from "react-native"; import { useShortcut } from "../lib/shortcuts/hooks"; import type { Key } from "../lib/shortcuts"; const HEADER_COLOR = "#7158e2"; const COLORS = { focused: "#ddd", selected: "#eaebf6", focused_selected: "#d5d7ef", }; const EXTRA = 5; export type ValidRecord = Record; interface TableState { data: unknown[]; columns: Column[]; columnMap: Map; idx: number; selectedIdx: Set; } const INITAL_STATE = { data: [], columns: [], columnMap: new Map(), idx: 0, selectedIdx: new Set(), } satisfies TableState; export const Context = createContext(INITAL_STATE); export type Column = { name: string; label: string; render?: (i: number | string) => string; }; function renderCell(row: ValidRecord, column: Column): string { const cell = row[column.name]; if (cell == undefined) return "n/a"; if (cell == null) return "null"; if (column.render) return column.render(cell); return cell.toString(); } interface TableShortcut { key: Key; handler: (params: { selected: T[]; index: number }) => void; } export interface ProviderProps { data: T[]; columns: Column[]; children: ReactNode; shortcuts?: TableShortcut[]; } export function Provider({ data, columns, children, shortcuts, }: ProviderProps) { const [idx, setIdx] = useState(0); const [selectedIdx, setSelectedIdx] = useState(new Set()); useShortcut("j", () => { setIdx((prev) => Math.min(prev + 1, data.length - 1)); }); useShortcut("down", () => { setIdx((prev) => Math.min(prev + 1, data.length - 1)); }); useShortcut("k", () => { setIdx((prev) => Math.max(prev - 1, 0)); }); useShortcut("up", () => { setIdx((prev) => Math.max(prev - 1, 0)); }); useShortcut("escape", () => { setSelectedIdx(new Set()); }); useShortcut("x", () => { setSelectedIdx((last) => { const newSelected = new Set(last); newSelected.add(idx); return newSelected; }); }); useEffect(() => { setIdx((prev) => Math.max(Math.min(prev, data.length - 1), 0)); }, [data]); if (shortcuts) { for (const shortcut of shortcuts) { useShortcut(shortcut.key, () => { const selected = data.filter( (_, index) => idx == index || selectedIdx.has(index), ); shortcut.handler({ selected, index: idx }); }); } } const columnMap = new Map( columns.map((col) => { return [ col.name, Math.max( col.label.length, ...data.map((row) => renderCell(row, col).length), ), ]; }), ); return ( {children} ); } export function Body() { const { columns, data, columnMap, idx, selectedIdx } = use(Context); return ( {columns.map((column) => ( {rpad( column.label, columnMap.get(column.name)! - column.label.length + EXTRA, )} ))} {data.map((row, index) => { const isSelected = selectedIdx.has(index); const isFocused = index == idx; return ( ); })} ); } interface RowProps { row: T; index: number; isSelected: boolean; } function TableRow({ row, isSelected }: RowProps) { const { columns, columnMap } = use(Context); return ( {columns.map((column) => { const rendered = renderCell(row, column); return ( {rpad( rendered, columnMap.get(column.name)! - rendered.length + EXTRA, )} ); })} ); } function rpad(input: string, length: number): string { return ( input + Array.from({ length }) .map((_) => " ") .join("") ); }