@shutootaki/gwm
Version:
git worktree manager CLI
115 lines • 7.84 kB
JavaScript
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
import { useEffect, useMemo, useCallback, useReducer } from 'react';
import { Text, Box, useInput } from 'ink';
import { useEditableText } from '../hooks/useEditableText.js';
import { formatRelativeTime } from '../utils/formatting.js';
export const SelectList = ({ items, onSelect, onCancel, placeholder = 'Type to search...', initialQuery = '', maxDisplayItems = 15, title = 'Select', showStats = true, }) => {
const { value: query, cursorPosition } = useEditableText({
initialValue: initialQuery,
});
const listReducer = (state, action) => {
switch (action.type) {
case 'RESET':
return { selectedIndex: 0, scrollOffset: 0 };
case 'MOVE': {
if (action.listLength === 0)
return state;
let nextIndex = state.selectedIndex + action.delta;
nextIndex = Math.max(0, Math.min(action.listLength - 1, nextIndex));
// スクロール位置を調整してカーソルを可視範囲内へ
let nextScrollOffset = state.scrollOffset;
if (nextIndex < nextScrollOffset) {
nextScrollOffset = nextIndex;
}
else if (nextIndex >= nextScrollOffset + action.maxDisplayItems) {
nextScrollOffset = nextIndex - action.maxDisplayItems + 1;
}
// 変更がなければ同じオブジェクトを返して React の再レンダリングを防ぐ
if (nextIndex === state.selectedIndex &&
nextScrollOffset === state.scrollOffset) {
return state;
}
return { selectedIndex: nextIndex, scrollOffset: nextScrollOffset };
}
default:
return state;
}
};
const [{ selectedIndex, scrollOffset }, dispatch] = useReducer(listReducer, {
selectedIndex: 0,
scrollOffset: 0,
});
// フィルタリングされた項目をメモ化して無駄な再計算を防ぐ
const filteredItems = useMemo(() => {
const lower = query.toLowerCase();
return items.filter((item) => item.label.toLowerCase().includes(lower));
}, [items, query]);
// 選択インデックスを範囲内に調整
useEffect(() => {
const maxIndex = Math.max(0, filteredItems.length - 1);
if (selectedIndex > maxIndex) {
dispatch({
type: 'MOVE',
delta: -1,
listLength: filteredItems.length,
maxDisplayItems,
});
}
}, [filteredItems.length, selectedIndex, maxDisplayItems]);
// スクロール位置を項目数の範囲内に収める
useEffect(() => {
const maxScroll = Math.max(0, filteredItems.length - maxDisplayItems);
if (scrollOffset > maxScroll) {
dispatch({
type: 'MOVE',
delta: -1,
listLength: filteredItems.length,
maxDisplayItems,
});
}
}, [filteredItems.length, maxDisplayItems, scrollOffset]);
// ↓/↑ で選択を移動する共通ロジック(useCallback でメモ化)
const moveSelection = useCallback((delta) => {
dispatch({
type: 'MOVE',
delta,
listLength: filteredItems.length,
maxDisplayItems,
});
}, [filteredItems.length, maxDisplayItems]);
useInput((input, key) => {
if (key.escape) {
onCancel();
return;
}
if (key.return) {
if (filteredItems.length > 0) {
onSelect(filteredItems[selectedIndex]);
}
return;
}
if (key.upArrow || (key.ctrl && input === 'p')) {
moveSelection(-1);
return;
}
if (key.downArrow || (key.ctrl && input === 'n')) {
moveSelection(1);
return;
}
});
const currentItem = filteredItems[selectedIndex];
const hasSelection = filteredItems.length > 0;
// 可視アイテム計算(メモ化)
const { visibleItems, hiddenAbove, hiddenBelow } = useMemo(() => {
const vis = filteredItems.slice(scrollOffset, scrollOffset + maxDisplayItems);
const above = scrollOffset;
const below = Math.max(0, filteredItems.length - (scrollOffset + vis.length));
return { visibleItems: vis, hiddenAbove: above, hiddenBelow: below };
}, [filteredItems, scrollOffset, maxDisplayItems]);
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "cyan", bold: true, children: title }) }), showStats && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: "gray", children: [filteredItems.length, " / ", items.length, " items", hasSelection && (_jsxs(_Fragment, { children: [' ', "\u2022 ", selectedIndex + 1, " of ", filteredItems.length] }))] }) })), _jsx(Box, { marginBottom: 1, children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "gray", children: placeholder }), _jsxs(Box, { marginTop: 0, children: [_jsxs(Text, { color: "cyan", bold: true, children: ["\u276F", ' '] }), _jsx(Text, { children: query.slice(0, cursorPosition) }), _jsx(Text, { color: "cyan", children: "\u2588" }), _jsx(Text, { children: query.slice(cursorPosition) })] })] }) }), _jsx(Box, { marginBottom: 1, children: filteredItems.length === 0 ? (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: "red", children: "No matches found" }) })) : (_jsxs(Box, { flexDirection: "column", children: [hiddenAbove > 0 && (_jsx(Box, { children: _jsxs(Text, { color: "yellow", children: ["\u2191 ", hiddenAbove, " more"] }) })), visibleItems.map((item, index) => {
const globalIndex = scrollOffset + index;
const isSelected = globalIndex === selectedIndex;
return (_jsx(Box, { children: _jsxs(Text, { color: isSelected ? 'cyan' : 'white', children: [isSelected ? '▶ ' : ' ', _jsx(Text, { color: isSelected ? 'cyan' : 'white', bold: isSelected, children: item.label })] }) }, item.value));
}), hiddenBelow > 0 && (_jsx(Box, { children: _jsxs(Text, { color: "yellow", children: ["\u2193 ", hiddenBelow, " more"] }) }))] })) }), hasSelection && currentItem && (_jsx(Box, { marginBottom: 1, children: _jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", padding: 1, children: [_jsx(Text, { color: "yellow", bold: true, children: "Preview" }), _jsx(Text, { color: "white", children: _jsx(Text, { color: "cyan", bold: true, children: currentItem.label }) }), currentItem.value !== currentItem.label && (_jsxs(Text, { color: "gray", children: ["Value: ", currentItem.value] })), currentItem.metadata && (_jsxs(_Fragment, { children: [currentItem.metadata.lastCommitDate && (_jsxs(Text, { color: "gray", children: ["Updated:", ' ', formatRelativeTime(String(currentItem.metadata.lastCommitDate))] })), currentItem.metadata.lastCommitterName && (_jsxs(Text, { color: "gray", children: ["By: ", String(currentItem.metadata.lastCommitterName)] })), currentItem.metadata.lastCommitMessage && (_jsxs(Text, { color: "gray", children: ["Last commit:", ' ', String(currentItem.metadata.lastCommitMessage)] }))] }))] }) })), _jsx(Box, { children: _jsxs(Text, { color: "gray", children: [_jsx(Text, { color: "cyan", children: "\u2191/\u2193" }), " navigate \u2022", ' ', _jsx(Text, { color: "green", children: "Enter" }), " select \u2022 ", _jsx(Text, { color: "red", children: "Esc" }), ' ', "cancel"] }) })] }));
};
//# sourceMappingURL=SelectList.js.map