askeroo
Version:
A modern CLI prompt library with flow control, history navigation, and conditional prompts
629 lines • 35.3 kB
JavaScript
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
import { useState, useEffect, useMemo, useRef } from "react";
import { Text, Box, useInput, Newline } from "ink";
import { TextInput } from "../../components/TextInput.js";
export const MultiField = ({ node, options: options, events, }) => {
// Use label if provided, fallback to message for compatibility
const label = options.label || options.message || "Select";
const onSearchQueryChange = events.onSearchQueryChange;
const NONE_VALUE = "__NONE__";
// Normalize options to support both string[] and MultiFieldOption[]
const normalizedOptions = useMemo(() => {
return (options.options || []).map((option) => typeof option === "string"
? { value: option, label: option }
: option);
}, [options.options]);
const getInitialValues = () => {
const vals = Array.isArray(options.initialValue)
? options.initialValue
: [];
if (!options.noneOption)
return vals;
const hasRegular = vals.some((v) => v !== NONE_VALUE);
return hasRegular
? vals.filter((v) => v !== NONE_VALUE)
: vals.length === 0
? [NONE_VALUE]
: vals;
};
const [selectedValues, setSelectedValues] = useState(getInitialValues());
const [validationError, setValidationError] = useState(null);
const [internalSearchQuery, setInternalSearchQuery] = useState("");
const [searchCursorPosition, setSearchCursorPosition] = useState(0);
const [filterModeActive, setFilterModeActive] = useState(false);
const hasHiddenSearch = options.searchable === true;
const hasFilterMode = options.searchable === "filter";
const showSearchInput = hasFilterMode && filterModeActive;
const isSearchActive = hasHiddenSearch || (hasFilterMode && filterModeActive);
const filteredOptions = useMemo(() => {
if (!isSearchActive || !internalSearchQuery.trim())
return normalizedOptions;
const q = internalSearchQuery.toLowerCase();
return normalizedOptions.filter((opt) => {
const isSelected = selectedValues.includes(opt.value);
const matches = opt.label.toLowerCase().includes(q) ||
opt.value.toLowerCase().includes(q);
return isSelected || matches;
});
}, [
normalizedOptions,
isSearchActive,
internalSearchQuery,
selectedValues,
]);
// Track if any options match the search (excluding just selected items)
const hasMatchingOptions = useMemo(() => {
if (!isSearchActive || !internalSearchQuery.trim())
return true;
const q = internalSearchQuery.toLowerCase();
return normalizedOptions.some((opt) => {
const matches = opt.label.toLowerCase().includes(q) ||
opt.value.toLowerCase().includes(q);
return matches;
});
}, [normalizedOptions, isSearchActive, internalSearchQuery]);
const totalOptions = filteredOptions.length + (options.noneOption ? 1 : 0);
// Simple navigation state
const [selectedIndex, setSelectedIndex] = useState(0);
// Track current window position for edge-scrolling
const [windowStart, setWindowStart] = useState(0);
const navigateUp = () => {
const newIndex = options.allowLoop ?? false
? selectedIndex > 0
? selectedIndex - 1
: totalOptions - 1
: Math.max(0, selectedIndex - 1);
setSelectedIndex(newIndex);
};
const navigateDown = () => {
const newIndex = options.allowLoop ?? false
? selectedIndex < totalOptions - 1
? selectedIndex + 1
: 0
: Math.min(totalOptions - 1, selectedIndex + 1);
setSelectedIndex(newIndex);
};
const handleShiftF = () => {
if (hasFilterMode) {
const wasActive = filterModeActive;
// Clear search first, then toggle mode
if (wasActive) {
setInternalSearchQuery("");
setSearchCursorPosition(0);
setValidationError(null);
}
setFilterModeActive(!filterModeActive);
}
};
const [submitted, setSubmitted] = useState(false);
const [error, setError] = useState(null);
// Toggle selection function
const toggleSelection = (optionValue) => {
const isNoneOption = options.noneOption && optionValue === NONE_VALUE;
let newSelectedValues;
if (isNoneOption) {
// None option - if not selected, select it; if already selected, do nothing (can't unselect)
if (!selectedValues.includes(NONE_VALUE)) {
newSelectedValues = [NONE_VALUE];
}
else {
// None is already selected, don't allow unselecting it
return;
}
}
else {
// Regular option - if None is selected, clear it first
const filteredValues = selectedValues.filter((v) => v !== NONE_VALUE);
newSelectedValues = filteredValues.includes(optionValue)
? filteredValues.filter((v) => v !== optionValue)
: [...filteredValues, optionValue];
// If all options are now unselected and noneOption exists, select it
if (newSelectedValues.length === 0 && options.noneOption) {
newSelectedValues = [NONE_VALUE];
}
}
setSelectedValues(newSelectedValues);
setError(null);
};
// Adjust focus when filtered options change to ensure it stays within bounds
useEffect(() => {
const maxIndex = filteredOptions.length + (options.noneOption ? 1 : 0) - 1;
if (selectedIndex > maxIndex) {
setSelectedIndex(Math.max(0, maxIndex));
}
}, [filteredOptions.length, options.noneOption]);
// Reset window position when options change significantly
useEffect(() => {
setWindowStart(0);
}, [filteredOptions.length, options.maxVisible]);
const disabled = node.state === "disabled";
useEffect(() => {
if (!disabled && submitted)
setSubmitted(false);
}, [disabled, submitted]);
// Calculate visible window for options (including none option)
// Combined list positions: 0=none (if present), 1=first_option, 2=second_option, etc.
const getVisibleWindow = () => {
const totalOptions = filteredOptions.length + (options.noneOption ? 1 : 0);
if (!options.maxVisible || totalOptions <= options.maxVisible) {
return {
showNoneOption: !!options.noneOption,
visibleOptions: filteredOptions,
startIndex: 0,
showStartEllipsis: false,
showEndEllipsis: false,
};
}
// Calculate visible window - scroll only when at edges
// The window operates on the combined list (none option + filtered options)
let currentWindowStart = windowStart;
let currentWindowEnd = Math.min(currentWindowStart + options.maxVisible, totalOptions);
// If selected index is at or beyond the bottom of current window, scroll down
if (selectedIndex >= currentWindowEnd) {
currentWindowStart = selectedIndex - options.maxVisible + 1;
currentWindowEnd = selectedIndex + 1;
}
// If selected index is before the top of current window, scroll up
else if (selectedIndex < currentWindowStart) {
currentWindowStart = selectedIndex;
currentWindowEnd = selectedIndex + options.maxVisible;
}
// Ensure we don't exceed bounds
currentWindowStart = Math.max(0, currentWindowStart);
currentWindowEnd = Math.min(totalOptions, currentWindowEnd);
// Adjust windowStart if we hit the end and have room to show more
if (currentWindowEnd - currentWindowStart < options.maxVisible &&
currentWindowStart > 0) {
currentWindowStart = Math.max(0, currentWindowEnd - options.maxVisible);
}
// Update window position state if it changed
if (currentWindowStart !== windowStart) {
setWindowStart(currentWindowStart);
}
// Determine if none option should be shown (it's at position 0 in combined list)
const showNoneOption = options.noneOption && currentWindowStart === 0;
// Calculate which regular options to show
// If none option is present, it occupies position 0, so regular options start at position 1
let optionsStart, optionsEnd;
if (options.noneOption) {
// With none option: positions 0=none, 1=first_option, 2=second_option, etc.
optionsStart = Math.max(0, currentWindowStart - 1);
optionsEnd = Math.min(filteredOptions.length, currentWindowEnd - 1);
}
else {
// Without none option: positions 0=first_option, 1=second_option, etc.
optionsStart = currentWindowStart;
optionsEnd = Math.min(filteredOptions.length, currentWindowEnd);
}
const visibleOptions = filteredOptions.slice(optionsStart, optionsEnd);
const showStartEllipsis = currentWindowStart > 0;
const showEndEllipsis = currentWindowEnd < totalOptions;
return {
showNoneOption,
visibleOptions,
startIndex: optionsStart,
showStartEllipsis,
showEndEllipsis,
};
};
const runValidation = async (vals) => {
if (!events.onValidate || node.state !== "active") {
setValidationError(null);
return true;
}
try {
const result = await events.onValidate(vals);
setValidationError(result);
return result === null;
}
catch {
setValidationError("Validation error occurred");
return false;
}
};
useEffect(() => {
if (!events.onHintChange)
return;
// Always show hints for multi (space select is always shown)
events.onHintChange(node.state === "active" ? (_jsxs(_Fragment, { children: [_jsx(Newline, {}), !node.isFirstRootPrompt && node.allowBack && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "yellow", children: "escape" }), " go back,", " "] })), _jsx(Text, { color: "yellow", children: "space" }), " select", hasHiddenSearch && (_jsxs(_Fragment, { children: [", ", _jsx(Text, { color: "yellow", children: "type" }), " to search"] })), hasFilterMode && (_jsxs(_Fragment, { children: [", ", _jsx(Text, { color: "yellow", children: "shift+f" }), " filter"] }))] })) : null);
}, [
node.state,
node.isFirstRootPrompt,
hasHiddenSearch,
hasFilterMode,
showSearchInput,
node.allowBack,
events.onHintChange,
]);
const stableInitial = useMemo(() => [...(options.initialValue || [])], [(options.initialValue || []).join(",")]);
const prevInitialRef = useRef([]);
useEffect(() => {
if (submitted || disabled)
return;
const changed = stableInitial.length !== prevInitialRef.current.length ||
stableInitial.some((v, i) => v !== prevInitialRef.current[i]);
if (changed) {
let vals = [...stableInitial];
if (options.noneOption) {
const hasRegular = vals.some((v) => v !== NONE_VALUE);
vals = hasRegular
? vals.filter((v) => v !== NONE_VALUE)
: vals.length === 0
? [NONE_VALUE]
: vals;
}
setSelectedValues(vals);
prevInitialRef.current = [...stableInitial];
}
}, [stableInitial, submitted, disabled, options.noneOption]);
useInput(async (input, key) => {
if (submitted || node.state !== "active")
return;
// Toggle filter mode with Shift+F
if (hasFilterMode &&
key.shift &&
(input === "f" || input === "F")) {
const wasActive = filterModeActive;
// Clear search first, then toggle mode
if (wasActive) {
setInternalSearchQuery("");
setSearchCursorPosition(0);
setValidationError(null);
}
setFilterModeActive(!filterModeActive);
return;
}
// Escape handling
if (key.escape) {
if (hasFilterMode && filterModeActive) {
// Close filter mode
setFilterModeActive(false);
setInternalSearchQuery("");
setSearchCursorPosition(0);
setValidationError(null);
return;
}
if (isSearchActive && internalSearchQuery.trim()) {
setInternalSearchQuery("");
setSearchCursorPosition(0);
return;
}
if (options.noneOption &&
selectedValues.some((v) => v !== NONE_VALUE)) {
setSelectedValues([NONE_VALUE]);
setError(null);
return;
}
if (node.allowBack && events.onBack) {
events.onBack();
return;
}
}
// Handle Ctrl+A (jump to top) and Ctrl+E (jump to bottom)
// Same shortcuts as cursor movement in text inputs
if (key.ctrl && input === "a") {
const { showNoneOption, startIndex } = getVisibleWindow();
// If none option is visible, go to index 0, otherwise go to first visible option
setSelectedIndex(showNoneOption
? 0
: startIndex + (options.noneOption ? 1 : 0));
return;
}
if (key.ctrl && input === "e") {
const { visibleOptions, startIndex } = getVisibleWindow();
const lastVisibleOptionIndex = startIndex + visibleOptions.length - 1;
// Calculate the actual index in the combined list (accounting for none option)
const lastVisibleIndex = lastVisibleOptionIndex + (options.noneOption ? 1 : 0);
setSelectedIndex(lastVisibleIndex);
return;
}
if (key.return) {
const vals = selectedValues.filter((v) => v !== NONE_VALUE);
if (!(await runValidation(vals)))
return;
setSubmitted(true);
// Call user's onSubmit callback if provided and use return value if any
let finalValue = vals;
if (options.onSubmit) {
const result = options.onSubmit(vals);
if (result !== undefined) {
finalValue = result;
}
}
events.onSubmit?.(finalValue);
return;
}
if (input === " ") {
const isNone = options.noneOption && selectedIndex === 0;
const opt = isNone
? { value: NONE_VALUE, label: options.noneOption.label }
: filteredOptions[selectedIndex - (options.noneOption ? 1 : 0)];
if (opt)
toggleSelection(opt.value);
return;
}
// Shift+A to toggle select all
if (key.shift && (input === "a" || input === "A")) {
const allValues = filteredOptions.map((opt) => opt.value);
// Check if all options are currently selected
const allSelected = allValues.every((val) => selectedValues.includes(val));
if (allSelected) {
// All are selected, deselect all
if (options.noneOption) {
setSelectedValues([NONE_VALUE]);
}
else {
setSelectedValues([]);
}
}
else {
// Not all are selected, select all
setSelectedValues(allValues);
}
setError(null);
return;
}
// Shift+I to invert selection
if (key.shift && (input === "i" || input === "I")) {
const allValues = filteredOptions.map((opt) => opt.value);
// Remove NONE_VALUE if present in current selection
const currentValues = selectedValues.filter((v) => v !== NONE_VALUE);
// Invert: select unselected options, unselect selected options
const invertedValues = allValues.filter((val) => !currentValues.includes(val));
// If after inversion we have no selections and noneOption exists, set to NONE
if (invertedValues.length === 0 && options.noneOption) {
setSelectedValues([NONE_VALUE]);
}
else {
setSelectedValues(invertedValues);
}
setError(null);
return;
}
// Visible search input - arrow navigation for options
if (showSearchInput) {
if (key.upArrow) {
navigateUp();
return;
}
if (key.downArrow) {
navigateDown();
return;
}
}
else {
// Hidden search input - only for searchable: true
if (hasHiddenSearch &&
input &&
input !== " " &&
input.length === 1 &&
!key.ctrl &&
!key.meta &&
!key.return &&
!key.escape &&
!key.upArrow &&
!key.downArrow &&
!key.leftArrow &&
!key.rightArrow) {
setInternalSearchQuery(internalSearchQuery + input);
return;
}
if (hasHiddenSearch &&
(key.backspace || key.delete || input === "\b")) {
setInternalSearchQuery(internalSearchQuery.slice(0, -1));
return;
}
// Arrow navigation
if (key.leftArrow) {
navigateUp();
return;
}
if (key.rightArrow) {
navigateDown();
return;
}
if (key.upArrow) {
navigateUp();
return;
}
if (key.downArrow) {
navigateDown();
return;
}
}
// Number selection (works in both modes)
if (options.showNumbers) {
const num = parseInt(input);
if (!isNaN(num) && num >= 1 && num <= totalOptions) {
const idx = num - 1;
setSelectedIndex(idx);
const isNone = options.noneOption && idx === 0;
const opt = isNone
? {
value: NONE_VALUE,
label: options.noneOption.label,
}
: filteredOptions[idx - (options.noneOption ? 1 : 0)];
if (opt)
toggleSelection(opt.value);
return;
}
}
}, { isActive: node.state === "active" && !submitted });
if (node.state === "completed") {
const val = (node.completedValue || []).length === 0 && options.noneOption
? options.noneOption.label
: (node.completedValue || []).join(", ");
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: label }), _jsx(Text, { color: "blue", children: val })] }));
}
if (node.state === "disabled") {
return (_jsxs(Box, { flexDirection: "row", gap: 1, children: [_jsx(Text, { dimColor: true, children: label }), _jsx(Text, { dimColor: true, color: "gray", children: "..." })] }));
}
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: label }), showSearchInput && (_jsx(Box, { children: _jsx(TextInput, { value: internalSearchQuery, onChange: setInternalSearchQuery, cursorPosition: searchCursorPosition, onCursorPositionChange: setSearchCursorPosition, isActive: node.state === "active" && !submitted, color: "cyan", onUpArrow: navigateUp, onDownArrow: navigateDown, onShiftF: handleShiftF }) })), options.hintPosition === "side" ? (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexDirection: "column", width: 25, children: (() => {
const { showNoneOption, visibleOptions, startIndex, showStartEllipsis, showEndEllipsis, } = getVisibleWindow();
return (_jsxs(_Fragment, { children: [showStartEllipsis && (_jsx(Text, { color: "gray", children: "\u22EF" })), showNoneOption && (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: selectedIndex === 0
? "cyan"
: selectedValues.includes(NONE_VALUE)
? "white"
: "gray", children: [selectedValues.includes(NONE_VALUE)
? "■"
: "□", " ", options.showNumbers && "1. ", options.noneOption.label] }), _jsx(Text, { color: "cyan", children: selectedIndex === 0
? " ⨞"
: " " })] })), visibleOptions.map((option, visibleIndex) => {
const actualIndex = startIndex + visibleIndex;
const displayIndex = actualIndex +
(options.noneOption ? 2 : 1);
const optionIndex = actualIndex +
(options.noneOption ? 1 : 0);
const isSelected = selectedValues.includes(option.value);
const isFocused = optionIndex === selectedIndex;
const color = isFocused
? "cyan"
: isSelected
? "white"
: option.color || "gray";
// Highlight matching text if searching
const renderLabel = () => {
if (!isSearchActive ||
!internalSearchQuery.trim()) {
return option.label;
}
const query = internalSearchQuery.toLowerCase();
const label = option.label;
const lowerLabel = label.toLowerCase();
const matchIndex = lowerLabel.indexOf(query);
if (matchIndex === -1) {
return label; // No match found, return original
}
const beforeMatch = label.slice(0, matchIndex);
const match = label.slice(matchIndex, matchIndex + query.length);
const afterMatch = label.slice(matchIndex + query.length);
// Use cyan for focused items, option color or white for non-focused
const highlightColor = isFocused
? "cyan"
: option.color || "white";
return (_jsxs(_Fragment, { children: [beforeMatch, _jsx(Text, { underline: true, color: highlightColor, children: match }), afterMatch] }));
};
return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: color, children: [isSelected ? "■" : "□", " ", options.showNumbers &&
`${displayIndex}. `, renderLabel()] }), _jsx(Text, { color: "cyan", children: isFocused
? " ⨞"
: " " })] }, option.value));
}), showEndEllipsis && (_jsx(Text, { color: "gray", children: "\u22EF" }))] }));
})() }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: (() => {
const { showNoneOption, visibleOptions, startIndex, showStartEllipsis, showEndEllipsis, } = getVisibleWindow();
return (_jsxs(_Fragment, { children: [showStartEllipsis && (_jsx(Text, { color: "gray" })), showNoneOption && (_jsx(Text, { color: "gray" })), visibleOptions.map((option, visibleIndex) => {
const actualIndex = startIndex + visibleIndex;
const optionIndex = actualIndex +
(options.noneOption ? 1 : 0);
const isFocused = optionIndex === selectedIndex;
return (_jsx(Text, { color: "gray", children: isFocused && option.hint
? option.hint
: "" }, option.value));
}), showEndEllipsis && (_jsx(Text, { color: "gray" }))] }));
})() })] })) : options.hintPosition === "inline-fixed" ? ((() => {
const { showNoneOption, visibleOptions, startIndex, showStartEllipsis, showEndEllipsis, } = getVisibleWindow();
return (_jsxs(_Fragment, { children: [showStartEllipsis && _jsx(Text, { color: "gray", children: "\u22EF" }), showNoneOption && (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Box, { width: 25, flexDirection: "row", children: [_jsxs(Text, { color: selectedIndex === 0
? "cyan"
: selectedValues.includes(NONE_VALUE)
? "white"
: "gray", children: [selectedValues.includes(NONE_VALUE)
? "■"
: "□", " ", options.showNumbers && "1. ", options.noneOption.label] }), _jsx(Text, { color: "cyan", children: selectedIndex === 0 ? " ⨞" : " " })] }), _jsx(Box, { flexGrow: 1, children: _jsx(Text, { color: "gray" }) })] })), visibleOptions.map((option, visibleIndex) => {
const actualIndex = startIndex + visibleIndex;
const displayIndex = actualIndex + (options.noneOption ? 2 : 1);
const optionIndex = actualIndex + (options.noneOption ? 1 : 0);
const isSelected = selectedValues.includes(option.value);
const isFocused = optionIndex === selectedIndex;
const color = isFocused
? "cyan"
: isSelected
? "white"
: option.color || "gray";
// Highlight matching text if searching
const renderLabel = () => {
if (!isSearchActive ||
!internalSearchQuery.trim()) {
return option.label;
}
const query = internalSearchQuery.toLowerCase();
const label = option.label;
const lowerLabel = label.toLowerCase();
const matchIndex = lowerLabel.indexOf(query);
if (matchIndex === -1) {
return label; // No match found, return original
}
const beforeMatch = label.slice(0, matchIndex);
const match = label.slice(matchIndex, matchIndex + query.length);
const afterMatch = label.slice(matchIndex + query.length);
// Use cyan for focused items, option color or white for non-focused
const highlightColor = isFocused
? "cyan"
: option.color || "white";
return (_jsxs(_Fragment, { children: [beforeMatch, _jsx(Text, { underline: true, color: highlightColor, children: match }), afterMatch] }));
};
return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Box, { width: 25, flexDirection: "row", children: [_jsxs(Text, { color: color, children: [isSelected ? "■" : "□", " ", options.showNumbers &&
`${displayIndex}. `, renderLabel()] }), _jsx(Text, { color: "cyan", children: isFocused ? " ⨞" : " " })] }), _jsx(Box, { flexGrow: 1, children: _jsx(Text, { color: "gray", children: isFocused && option.hint
? option.hint
: "" }) })] }, option.value));
}), showEndEllipsis && _jsx(Text, { color: "gray", children: "\u22EF" })] }));
})()) : ((() => {
const { showNoneOption, visibleOptions, startIndex, showStartEllipsis, showEndEllipsis, } = getVisibleWindow();
return (_jsxs(_Fragment, { children: [showStartEllipsis && _jsx(Text, { color: "gray", children: "\u22EF" }), showNoneOption && (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: selectedIndex === 0
? "cyan"
: selectedValues.includes(NONE_VALUE)
? "white"
: "gray", children: [selectedValues.includes(NONE_VALUE)
? "■"
: "□", " ", options.showNumbers && "1. ", options.noneOption.label] }), _jsx(Text, { color: "cyan", children: selectedIndex === 0 ? " ⨞" : " " })] })), visibleOptions.map((option, visibleIndex) => {
const actualIndex = startIndex + visibleIndex;
const displayIndex = actualIndex + (options.noneOption ? 2 : 1);
const optionIndex = actualIndex + (options.noneOption ? 1 : 0);
const isSelected = selectedValues.includes(option.value);
const isFocused = optionIndex === selectedIndex;
const color = isFocused
? "cyan"
: isSelected
? "white"
: option.color || "gray";
// Highlight matching text if searching
const renderLabel = () => {
if (!isSearchActive ||
!internalSearchQuery.trim()) {
return option.label;
}
const query = internalSearchQuery.toLowerCase();
const label = option.label;
const lowerLabel = label.toLowerCase();
const matchIndex = lowerLabel.indexOf(query);
if (matchIndex === -1) {
return label; // No match found, return original
}
const beforeMatch = label.slice(0, matchIndex);
const match = label.slice(matchIndex, matchIndex + query.length);
const afterMatch = label.slice(matchIndex + query.length);
// Use cyan for focused items, option color or white for non-focused
const highlightColor = isFocused
? "cyan"
: option.color || "white";
return (_jsxs(_Fragment, { children: [beforeMatch, _jsx(Text, { underline: true, color: highlightColor, children: match }), afterMatch] }));
};
return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: color, children: [isSelected ? "■" : "□", " ", options.showNumbers &&
`${displayIndex}. `, renderLabel()] }), _jsx(Text, { color: "cyan", children: isFocused ? " ⨞" : " " }), options.hintPosition === "inline" &&
isFocused &&
option.hint && (_jsxs(Text, { color: "gray", children: [" ", option.hint] }))] }, option.value));
}), showEndEllipsis && _jsx(Text, { color: "gray", children: "\u22EF" })] }));
})()), isSearchActive &&
!hasMatchingOptions &&
internalSearchQuery.trim() && (_jsxs(Text, { color: "red", children: ["No options match \"", internalSearchQuery, "\""] })), error && _jsx(Text, { color: "red", children: error }), options.hintPosition === "bottom" && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: (() => {
// Check if none option is focused first
if (options.noneOption && selectedIndex === 0) {
// None option doesn't support hints
return " ";
}
// Find the focused option from filteredOptions
const focusedOptionIndex = selectedIndex - (options.noneOption ? 1 : 0);
const focusedOption = filteredOptions[focusedOptionIndex];
return focusedOption?.hint || " ";
})() }) }, `hint-${selectedIndex}`)), validationError && (_jsx(Box, { children: _jsx(Text, { color: "red", children: validationError }) }))] }));
};
//# sourceMappingURL=MultiField.js.map