@shutootaki/gwm
Version:
git worktree manager CLI
108 lines • 7.63 kB
JavaScript
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
import { useEffect, useState } from 'react';
import { Text, Box, useInput } from 'ink';
import { useEditableText } from '../hooks/useEditableText.js';
export const MultiSelectList = ({ items, onConfirm, onCancel, placeholder = 'Type to search, Space to select...', initialQuery = '', maxDisplayItems = 15, title = 'Multi-select', showStats = true, }) => {
const { value: query, cursorPosition } = useEditableText({
initialValue: initialQuery,
skipChars: [' '],
});
const [selectedIndex, setSelectedIndex] = useState(0);
const [selectedItems, setSelectedItems] = useState(new Set());
const [scrollOffset, setScrollOffset] = useState(0);
// フィルタリングされた項目
const filteredItems = items.filter((item) => item.label.toLowerCase().includes(query.toLowerCase()));
// 選択インデックスを範囲内に調整
useEffect(() => {
const maxIndex = Math.max(0, filteredItems.length - 1);
if (selectedIndex > maxIndex) {
setSelectedIndex(maxIndex);
}
}, [filteredItems.length, selectedIndex]);
// スクロール位置を項目数の範囲内に収める
useEffect(() => {
const maxScroll = Math.max(0, filteredItems.length - maxDisplayItems);
if (scrollOffset > maxScroll) {
setScrollOffset(Math.max(0, Math.min(maxScroll, selectedIndex)));
}
}, [filteredItems.length, maxDisplayItems, scrollOffset, selectedIndex]);
// ↓/↑ で選択を移動する共通ロジック
const moveSelection = (delta) => {
if (filteredItems.length === 0)
return;
let nextIndex = selectedIndex + delta;
nextIndex = Math.max(0, Math.min(filteredItems.length - 1, nextIndex));
if (nextIndex < scrollOffset) {
setScrollOffset(nextIndex);
}
else if (nextIndex >= scrollOffset + maxDisplayItems) {
setScrollOffset(nextIndex - maxDisplayItems + 1);
}
setSelectedIndex(nextIndex);
};
useInput((input, key) => {
if (key.escape) {
onCancel();
return;
}
if (key.return) {
const selected = items.filter((item) => selectedItems.has(item.value));
onConfirm(selected);
return;
}
if (key.ctrl && input === 'a') {
if (filteredItems.length > 0) {
const newSelected = new Set(selectedItems);
const allVisibleSelected = filteredItems.every((item) => newSelected.has(item.value));
if (allVisibleSelected) {
filteredItems.forEach((item) => newSelected.delete(item.value));
}
else {
filteredItems.forEach((item) => newSelected.add(item.value));
}
setSelectedItems(newSelected);
}
return;
}
if (key.upArrow || (key.ctrl && input === 'p')) {
moveSelection(-1);
return;
}
if (key.downArrow || (key.ctrl && input === 'n')) {
moveSelection(1);
return;
}
// Space: 現在行の選択トグル
if (input === ' ') {
const currentItem = filteredItems[selectedIndex];
if (currentItem) {
const newSelected = new Set(selectedItems);
if (newSelected.has(currentItem.value)) {
newSelected.delete(currentItem.value);
}
else {
newSelected.add(currentItem.value);
}
setSelectedItems(newSelected);
}
return;
}
});
const hasItems = filteredItems.length > 0;
// 可視アイテム計算
const visibleItems = filteredItems.slice(scrollOffset, scrollOffset + maxDisplayItems);
const hiddenAbove = scrollOffset;
const hiddenBelow = Math.max(0, filteredItems.length - (scrollOffset + visibleItems.length));
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", hasItems && _jsxs(_Fragment, { children: [" \u2022 cursor at ", selectedIndex + 1] }), " \u2022", ' ', _jsxs(Text, { color: "green", bold: true, children: [selectedItems.size, " selected"] })] }) })), _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: !hasItems ? (_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 isItemSelected = selectedItems.has(item.value);
const isCurrent = globalIndex === selectedIndex;
return (_jsx(Box, { children: _jsxs(Text, { color: isCurrent ? 'cyan' : 'white', children: [isCurrent ? '▶ ' : ' ', isItemSelected ? (_jsxs(Text, { color: "green", bold: true, children: ["[x]", ' '] })) : (_jsx(Text, { color: "gray", children: "[ ] " })), _jsx(Text, { color: isCurrent ? 'cyan' : 'white', bold: isCurrent, children: item.label })] }) }, item.value));
}), hiddenBelow > 0 && (_jsx(Box, { children: _jsxs(Text, { color: "yellow", children: ["\u2193 ", hiddenBelow, " more"] }) }))] })) }), selectedItems.size > 0 && (_jsx(Box, { marginBottom: 1, children: _jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "green", padding: 1, children: [_jsxs(Text, { color: "green", bold: true, children: ["Selected (", selectedItems.size, " items)"] }), _jsxs(Box, { flexDirection: "column", children: [Array.from(selectedItems)
.slice(0, 5)
.map((value) => {
const item = items.find((i) => i.value === value);
return item ? (_jsxs(Text, { color: "gray", children: ["\u2022 ", item.label] }, value)) : null;
}), selectedItems.size > 5 && (_jsxs(Text, { color: "gray", children: ["... ", selectedItems.size - 5, " more"] }))] })] }) })), _jsx(Box, { children: _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "gray", children: [_jsx(Text, { color: "cyan", children: "\u2191/\u2193" }), " navigate \u2022", ' ', _jsx(Text, { color: "yellow", children: "Space" }), " toggle \u2022", ' ', _jsx(Text, { color: "green", children: "Enter" }), " confirm \u2022", ' ', _jsx(Text, { color: "red", children: "Esc" }), " cancel"] }), _jsxs(Text, { color: "gray", children: [_jsx(Text, { color: "cyan", children: "Ctrl+A" }), " select all / clear all"] })] }) })] }));
};
//# sourceMappingURL=MultiSelectList.js.map