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}
);
}