feat(app): redesign command autocomplete with detail card and dropdown styling

- Add detail card above command list showing full description and argument hint
- Compact command rows to single line (label + truncated description)
- Match dropdown component styling (surface1 bg, borderAccent border, shadow)
This commit is contained in:
Mohamed Boudra
2026-03-24 13:35:59 +07:00
parent fa0346921a
commit cbab9332a9
+134 -56
View File
@@ -150,70 +150,136 @@ export function Autocomplete({
); );
} }
const selectedOption = options[selectedIndex];
return ( return (
<View style={[styles.container, { maxHeight }]}> <View style={styles.outerWrapper}>
<ScrollView {selectedOption?.kind === "command" && selectedOption.description ? (
ref={scrollRef} <View style={styles.detailCard}>
onLayout={handleScrollViewLayout} <Text style={styles.detailLabel}>
onContentSizeChange={pinToBottom} {removeBoltGlyphs(selectedOption.label) ?? selectedOption.label}
onScroll={(event) => { </Text>
scrollOffsetRef.current = event.nativeEvent.contentOffset.y; <Text style={styles.detailDescription}>
}} {removeBoltGlyphs(selectedOption.description)}
scrollEventThrottle={16} </Text>
style={styles.scrollView} {selectedOption.detail ? (
contentContainerStyle={styles.scrollContent} <Text style={styles.detailHint}>{removeBoltGlyphs(selectedOption.detail)}</Text>
keyboardShouldPersistTaps="always" ) : null}
> </View>
{options.map((option, index) => { ) : null}
const isSelected = index === selectedIndex; <View style={[styles.container, { maxHeight }]}>
const optionLabel = removeBoltGlyphs(option.label) ?? option.label; <ScrollView
const optionDetail = removeBoltGlyphs(option.detail); ref={scrollRef}
const optionDescription = removeBoltGlyphs(option.description); onLayout={handleScrollViewLayout}
return ( onContentSizeChange={pinToBottom}
<Pressable onScroll={(event) => {
key={option.id} scrollOffsetRef.current = event.nativeEvent.contentOffset.y;
onLayout={(event) => handleRowLayout(index, event)} }}
onPress={() => onSelect(option)} scrollEventThrottle={16}
style={({ hovered = false, pressed }) => [ style={styles.scrollView}
styles.item, contentContainerStyle={styles.scrollContent}
(hovered || pressed || isSelected) && styles.itemActive, keyboardShouldPersistTaps="always"
]} >
> {options.map((option, index) => {
{option.kind === "directory" || option.kind === "file" ? ( const isSelected = index === selectedIndex;
<View style={styles.itemLeading}> const optionLabel = removeBoltGlyphs(option.label) ?? option.label;
{option.kind === "directory" ? ( const optionDescription = removeBoltGlyphs(option.description);
<Folder size={14} color={theme.colors.foregroundMuted} /> const isFileOrDir = option.kind === "directory" || option.kind === "file";
) : ( return (
<File size={14} color={theme.colors.foregroundMuted} /> <Pressable
)} key={option.id}
</View> onLayout={(event) => handleRowLayout(index, event)}
) : null} onPress={() => onSelect(option)}
<View style={styles.itemMain}> style={({ hovered = false, pressed }) => [
<View style={styles.itemHeader}> styles.item,
<Text style={styles.itemLabel}>{optionLabel}</Text> (hovered || pressed || isSelected) && styles.itemActive,
{optionDetail ? <Text style={styles.itemDetail}>{optionDetail}</Text> : null} ]}
</View> >
{optionDescription ? ( {isFileOrDir ? (
<Text style={styles.itemDescription} numberOfLines={1}> <>
{optionDescription} <View style={styles.itemLeading}>
</Text> {option.kind === "directory" ? (
) : null} <Folder size={14} color={theme.colors.foregroundMuted} />
</View> ) : (
</Pressable> <File size={14} color={theme.colors.foregroundMuted} />
); )}
})} </View>
</ScrollView> <View style={styles.itemMain}>
<View style={styles.itemHeader}>
<Text style={styles.itemLabel}>{optionLabel}</Text>
{removeBoltGlyphs(option.detail) ? (
<Text style={styles.itemDetail}>{removeBoltGlyphs(option.detail)}</Text>
) : null}
</View>
{optionDescription ? (
<Text style={styles.itemDescription} numberOfLines={1}>
{optionDescription}
</Text>
) : null}
</View>
</>
) : (
<View style={styles.itemMainRow}>
<Text style={styles.itemLabel}>{optionLabel}</Text>
{optionDescription ? (
<Text style={styles.itemDescriptionInline} numberOfLines={1}>
{optionDescription}
</Text>
) : null}
</View>
)}
</Pressable>
);
})}
</ScrollView>
</View>
</View> </View>
); );
} }
const styles = StyleSheet.create(((theme: Theme) => ({ const styles = StyleSheet.create(((theme: Theme) => ({
container: { outerWrapper: {
backgroundColor: theme.colors.surface0, gap: theme.spacing[1],
},
detailCard: {
backgroundColor: theme.colors.surface1,
borderWidth: theme.borderWidth[1], borderWidth: theme.borderWidth[1],
borderColor: theme.colors.border, borderColor: theme.colors.borderAccent,
borderRadius: theme.borderRadius.lg,
paddingHorizontal: theme.spacing[3],
paddingVertical: theme.spacing[3],
shadowColor: "#000",
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.2,
shadowRadius: 8,
elevation: 8,
},
detailLabel: {
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.normal,
},
detailDescription: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs,
marginTop: theme.spacing[1],
},
detailHint: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs,
marginTop: theme.spacing[1],
},
container: {
backgroundColor: theme.colors.surface1,
borderWidth: theme.borderWidth[1],
borderColor: theme.colors.borderAccent,
borderRadius: theme.borderRadius.lg, borderRadius: theme.borderRadius.lg,
overflow: "hidden", overflow: "hidden",
shadowColor: "#000",
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.2,
shadowRadius: 8,
elevation: 8,
}, },
scrollView: { scrollView: {
flexGrow: 0, flexGrow: 0,
@@ -236,12 +302,19 @@ const styles = StyleSheet.create(((theme: Theme) => ({
marginRight: theme.spacing[1], marginRight: theme.spacing[1],
}, },
itemActive: { itemActive: {
backgroundColor: theme.colors.surface1, backgroundColor: theme.colors.surface2,
}, },
itemMain: { itemMain: {
flex: 1, flex: 1,
minWidth: 0, minWidth: 0,
}, },
itemMainRow: {
flex: 1,
minWidth: 0,
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[2],
},
itemHeader: { itemHeader: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
@@ -261,6 +334,11 @@ const styles = StyleSheet.create(((theme: Theme) => ({
fontSize: theme.fontSize.xs, fontSize: theme.fontSize.xs,
marginTop: 2, marginTop: 2,
}, },
itemDescriptionInline: {
flex: 1,
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs,
},
emptyItem: { emptyItem: {
paddingHorizontal: theme.spacing[3], paddingHorizontal: theme.spacing[3],
paddingVertical: theme.spacing[3], paddingVertical: theme.spacing[3],