Files
OpenQode/bin/ui/components/SearchOverlay.mjs

106 lines
3.6 KiB
JavaScript

import React, { useEffect, useMemo, useState } from 'react';
import { Box, Text, useInput } from 'ink';
import TextInput from 'ink-text-input';
import SelectInput from 'ink-select-input';
const h = React.createElement;
const SearchOverlay = ({
isOpen = false,
initialQuery = '',
results = [],
isSearching = false,
error = null,
onClose,
onSearch,
onOpenResult,
width = 80,
height = 24
}) => {
const [query, setQuery] = useState(initialQuery || '');
const [mode, setMode] = useState('query'); // 'query' | 'results'
useEffect(() => {
if (!isOpen) return;
setQuery(initialQuery || '');
setMode('query');
}, [isOpen, initialQuery]);
useInput((input, key) => {
if (!isOpen) return;
if (key.escape) {
if (typeof onClose === 'function') onClose();
}
if (key.tab) {
setMode(m => (m === 'query' ? 'results' : 'query'));
}
if (key.ctrl && input.toLowerCase() === 'c') {
if (typeof onClose === 'function') onClose();
}
}, { isActive: isOpen });
const items = useMemo(() => {
const max = Math.max(0, height - 8);
return results.slice(0, Math.min(200, max)).map((r, idx) => ({
label: `${r.rel}:${r.line}${r.text ? ` ${r.text}` : ''}`.slice(0, Math.max(10, width - 6)),
value: idx
}));
}, [results, width, height]);
if (!isOpen) return null;
return h(Box, {
flexDirection: 'column',
width,
height,
borderStyle: 'double',
borderColor: 'magenta',
paddingX: 1,
paddingY: 0
},
h(Box, { justifyContent: 'space-between' },
h(Text, { color: 'magenta', bold: true }, 'Search (ripgrep)'),
h(Text, { color: 'gray', dimColor: true }, 'Esc close · Enter search/open · Tab switch')
),
h(Box, { marginTop: 1, flexDirection: 'row' },
h(Text, { color: 'yellow' }, 'Query: '),
h(Box, { flexGrow: 1 },
h(TextInput, {
value: query,
focus: mode === 'query',
onChange: setQuery,
onSubmit: async () => {
if (typeof onSearch === 'function') {
setMode('results');
await onSearch(query);
}
},
placeholder: 'e.g. function handleSubmit'
})
)
),
h(Box, { marginTop: 1 },
isSearching ? h(Text, { color: 'yellow' }, 'Searching...') : null,
error ? h(Text, { color: 'red' }, error) : null,
(!isSearching && !error) ? h(Text, { color: 'gray', dimColor: true }, `${results.length} result(s)`) : null
),
h(Box, { flexDirection: 'column', flexGrow: 1, marginTop: 1 },
items.length > 0
? h(SelectInput, {
items,
isFocused: mode === 'results',
onSelect: (item) => {
const r = results[item.value];
if (r && typeof onOpenResult === 'function') onOpenResult(r);
},
itemComponent: ({ isSelected, label }) =>
h(Text, { color: isSelected ? 'cyan' : 'white', bold: isSelected, wrap: 'truncate-end' }, label)
})
: h(Text, { color: 'gray', dimColor: true }, 'No results yet. Type a query and press Enter.')
)
);
};
export default SearchOverlay;