import type { ReactNode } from 'react'; import { useCallback, useState } from 'react'; import type { DimensionValue, StyleProp, ViewStyle } from 'react-native'; import { Pressable, TextInput, View } from 'react-native'; import { AddLine as Plus } from 'rn-motion-ui-icons/icons/add-line'; import { Delete2Line as Trash2 } from 'rn-motion-ui-icons/icons/delete-2-line'; import { useIsRTL } from '../../../hooks/use-direction'; import { cn } from '../../../lib/cn'; import { MotiView } from '../../../moti/components/view'; import { TIMING_INSTANT } from '../../../theme/motion'; import { useThemeColor } from '../../../theme/use-theme-color'; import { Checkbox } from '../../form/Checkbox/checkbox'; import { Text } from '../../typography/Text/text'; import type { TableColumn } from './table-types'; import { alignToItemsClass, alignToTextClass, CHECKBOX_COLUMN_WIDTH, columnLayoutStyle, readCellValue } from './table-utils'; // ─── Editable cell input ────────────────────────────────────────────────────── // biome-ignore lint/style/useExportsLast: props type before EditableCellInput — collocated for readability export type EditableCellInputProps = { value: string; onCommit: (next: string) => void; testID?: string }; function EditableCellInput({ value, onCommit, testID }: EditableCellInputProps) { const [draft, setDraft] = useState(value); const commit = useCallback(() => onCommit(draft), [onCommit, draft]); const mutedForeground = useThemeColor('muted-foreground'); return ( ); } // ─── Skeleton cell pulse ────────────────────────────────────────────────────── // ─── Skeleton cell pulse ────────────────────────────────────────────────────── export type SkeletonCellPulseProps = { /** Column width spec — passed directly to `columnLayoutClass` for flex/px resolution. */ columnWidth: number | string | undefined; /** Resolved pixel width for this column (from `computeColumnWidths`). */ colWidth?: number; /** Floor for the column's width during the pre-layout render (see `TableColumn.minWidth`). */ minWidth?: number; align: TableColumn['align']; /** Override skeleton bar width. Defaults to `'60%'` (or `40` for right-aligned). */ skeletonWidth?: DimensionValue; reduce: boolean; /** UniWind classes merged onto the skeleton pulse bar. */ skeletonClassName?: string; }; export function SkeletonCellPulse({ columnWidth, colWidth, minWidth, align, skeletonWidth, reduce, skeletonClassName, }: SkeletonCellPulseProps) { const barWidth: DimensionValue = skeletonWidth ?? (align === 'right' ? 40 : '60%'); return ( ); } // ─── Row cell ───────────────────────────────────────────────────────────────── // One data cell. Owns the per-cell edit-commit callback so it stays stable and // the parent row never binds an inline arrow per column. export type RowCellProps = { row: T; column: TableColumn; id: string; /** Resolved pixel width for this column (from `computeColumnWidths`). */ colWidth?: number; onCellEdit?: (rowId: string, key: string, value: string) => void; /** UniWind classes merged onto this cell. */ cellClassName?: string; testID?: string; }; export function RowCell({ row, column, id, colWidth, onCellEdit, cellClassName, testID }: RowCellProps) { const isRTL = useIsRTL(); const textClass = alignToTextClass(column.align, isRTL); const rawValue = readCellValue(row, column); const handleCommit = useCallback((v: string) => onCellEdit?.(id, column.key, v), [onCellEdit, id, column.key]); let cellContent: ReactNode; if (column.cell) cellContent = column.cell(row); else if (column.editable) cellContent = ( ); else cellContent = ( {rawValue === null ? '' : String(rawValue)} ); return ( {cellContent} ); } // ─── Table row ──────────────────────────────────────────────────────────────── // A single data row. All interactive callbacks are stable useCallbacks bound to // this row's id/index so the JSX never binds inline arrows. export type TableRowProps = { row: T; id: string; index: number; isSelected: boolean; isRowPressed: boolean; selectable: boolean; orderedColumns: TableColumn[]; /** Resolved pixel widths per column key (from `computeColumnWidths`). */ colWidths: Record; rowHeight: number; reduce: boolean; hasRowMenu: boolean; /** When true this row gets the `stripedStyle` background tint. */ isStriped?: boolean; /** Style applied when `isStriped` is true. Falls back to a subtle grey tint. */ stripedStyle?: StyleProp; /** UniWind classes merged onto the row. */ rowClassName?: string; /** UniWind classes merged onto each cell in this row. */ cellClassName?: string; /** UniWind classes merged onto the selected-row background overlay. */ selectedClassName?: string; setPressedRowId: (id: string | null) => void; toggleRow: (id: string) => void; onCellEdit?: (rowId: string, key: string, value: string) => void; onInsertRow?: (index: number, position: 'before' | 'after') => void; onDeleteRow?: (rowId: string) => void; testID?: string; }; // Subtle alternating-row tint — exempt from theming (too faint to need a token). const STRIPED_FALLBACK: ViewStyle = { backgroundColor: 'rgba(0,0,0,0.02)' }; export function TableRow({ row, id, index, isSelected, isRowPressed, selectable, orderedColumns, colWidths, rowHeight, reduce, hasRowMenu, isStriped = false, stripedStyle, rowClassName, cellClassName, selectedClassName, setPressedRowId, toggleRow, onCellEdit, onInsertRow, onDeleteRow, testID, }: TableRowProps) { const isRTL = useIsRTL(); const handleLongPress = useCallback(() => { if (hasRowMenu) setPressedRowId(isRowPressed ? null : id); }, [hasRowMenu, isRowPressed, id, setPressedRowId]); const handlePress = useCallback(() => { if (isRowPressed) setPressedRowId(null); }, [isRowPressed, setPressedRowId]); const handleToggleRow = useCallback(() => toggleRow(id), [toggleRow, id]); const handleInsertRow = useCallback(() => { onInsertRow?.(index, 'before'); setPressedRowId(null); }, [onInsertRow, index, setPressedRowId]); const handleDeleteRow = useCallback(() => { onDeleteRow?.(id); setPressedRowId(null); }, [onDeleteRow, id, setPressedRowId]); const primaryForeground = useThemeColor('primary-foreground'); return ( {/* Selected row background — spring fade */} {selectable ? ( ) : null} {orderedColumns.map((column) => ( ))} {/* Row action buttons shown on long-press, pinned to the trailing edge — which is the left one under RTL, since `right` stays physical. */} {isRowPressed && hasRowMenu ? ( {onInsertRow ? ( ) : null} {onDeleteRow ? ( ) : null} ) : null} ); }