Files
abode/src/tui/components/ui/ListDisplay.tsx
T
2026-06-29 23:07:14 +00:00

92 lines
2.2 KiB
TypeScript

import { Box, Spacer, Text, useInput } from "ink";
import {
useEffect,
useMemo,
useState,
type ComponentType,
type ReactNode,
} from "react";
export function ListDisplay<T>({
items,
Component,
height,
isFocused,
status,
onSelect,
}: {
items: T[];
Component: ComponentType<{ item: T; selected: boolean }>;
height: number;
isFocused?: boolean;
status?: ReactNode;
onSelect?: (item: T) => void;
}) {
const [start, setStart] = useState(0);
const slice = Math.min(start + height - 1, items.length);
const [selected, setSelected] = useState(0);
useInput(
(input, key) => {
if (key.upArrow) {
setSelected((prev) => (prev > 0 ? prev - 1 : items.length - 1));
} else if (key.downArrow) {
setSelected((prev) => (prev + 1) % items.length);
} else if (input === " " || key.return) {
onSelect?.(items[selected]);
}
},
{ isActive: isFocused }
);
useEffect(() => {
if (selected < start) setStart(selected);
else if (selected >= slice)
setStart(
Math.max(Math.min(selected - slice + start + 1, items.length - 1), 0)
);
}, [selected, start, slice, items.length]);
useEffect(() => {
if (selected < 0) setSelected(Math.max(items.length - 1, 0));
if (selected >= items.length) setSelected(0);
}, [selected, items.length]);
useEffect(() => {
setSelected(0);
}, [items]);
const indexed = useMemo(
() => items.map((item, index) => ({ item, index })),
[items]
);
return (
<Box flexDirection="column">
<Box flexDirection="column">
{indexed.slice(start, slice).map(({ item, index }) => (
<Component
key={index}
item={item}
selected={selected === index && isFocused !== false}
/>
))}
{items.length < height - 1 && (
<Box height={height - 1 - items.length} />
)}
</Box>
<Box justifyContent="space-between">
{status}
<Spacer />
<Text color="magenta">
{items.length ? (
<>
{start + 1}-{slice}/{items.length}
</>
) : (
<>(empty)</>
)}
</Text>
</Box>
</Box>
);
}