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
@@ -150,7 +150,23 @@ export function Autocomplete({
); );
} }
const selectedOption = options[selectedIndex];
return ( return (
<View style={styles.outerWrapper}>
{selectedOption?.kind === "command" && selectedOption.description ? (
<View style={styles.detailCard}>
<Text style={styles.detailLabel}>
{removeBoltGlyphs(selectedOption.label) ?? selectedOption.label}
</Text>
<Text style={styles.detailDescription}>
{removeBoltGlyphs(selectedOption.description)}
</Text>
{selectedOption.detail ? (
<Text style={styles.detailHint}>{removeBoltGlyphs(selectedOption.detail)}</Text>
) : null}
</View>
) : null}
<View style={[styles.container, { maxHeight }]}> <View style={[styles.container, { maxHeight }]}>
<ScrollView <ScrollView
ref={scrollRef} ref={scrollRef}
@@ -167,8 +183,8 @@ export function Autocomplete({
{options.map((option, index) => { {options.map((option, index) => {
const isSelected = index === selectedIndex; const isSelected = index === selectedIndex;
const optionLabel = removeBoltGlyphs(option.label) ?? option.label; const optionLabel = removeBoltGlyphs(option.label) ?? option.label;
const optionDetail = removeBoltGlyphs(option.detail);
const optionDescription = removeBoltGlyphs(option.description); const optionDescription = removeBoltGlyphs(option.description);
const isFileOrDir = option.kind === "directory" || option.kind === "file";
return ( return (
<Pressable <Pressable
key={option.id} key={option.id}
@@ -179,7 +195,8 @@ export function Autocomplete({
(hovered || pressed || isSelected) && styles.itemActive, (hovered || pressed || isSelected) && styles.itemActive,
]} ]}
> >
{option.kind === "directory" || option.kind === "file" ? ( {isFileOrDir ? (
<>
<View style={styles.itemLeading}> <View style={styles.itemLeading}>
{option.kind === "directory" ? ( {option.kind === "directory" ? (
<Folder size={14} color={theme.colors.foregroundMuted} /> <Folder size={14} color={theme.colors.foregroundMuted} />
@@ -187,11 +204,12 @@ export function Autocomplete({
<File size={14} color={theme.colors.foregroundMuted} /> <File size={14} color={theme.colors.foregroundMuted} />
)} )}
</View> </View>
) : null}
<View style={styles.itemMain}> <View style={styles.itemMain}>
<View style={styles.itemHeader}> <View style={styles.itemHeader}>
<Text style={styles.itemLabel}>{optionLabel}</Text> <Text style={styles.itemLabel}>{optionLabel}</Text>
{optionDetail ? <Text style={styles.itemDetail}>{optionDetail}</Text> : null} {removeBoltGlyphs(option.detail) ? (
<Text style={styles.itemDetail}>{removeBoltGlyphs(option.detail)}</Text>
) : null}
</View> </View>
{optionDescription ? ( {optionDescription ? (
<Text style={styles.itemDescription} numberOfLines={1}> <Text style={styles.itemDescription} numberOfLines={1}>
@@ -199,21 +217,69 @@ export function Autocomplete({
</Text> </Text>
) : null} ) : null}
</View> </View>
</>
) : (
<View style={styles.itemMainRow}>
<Text style={styles.itemLabel}>{optionLabel}</Text>
{optionDescription ? (
<Text style={styles.itemDescriptionInline} numberOfLines={1}>
{optionDescription}
</Text>
) : null}
</View>
)}
</Pressable> </Pressable>
); );
})} })}
</ScrollView> </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],