92 lines
2.2 KiB
TypeScript
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>
|
|
);
|
|
}
|