UNPKG

@shutootaki/gwm

Version:
115 lines 7.84 kB
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