refactor: tighten tool call badge hover affordances
Move the open-file button inline after the summary text so the click target sits next to what it acts on, and replace the tool icon with the chevron on hover instead of showing a separate chevron at the row's end. Hovering the icon slot keeps the tool icon visible for predictability.
This commit is contained in:
@@ -997,7 +997,8 @@ const expandableBadgeStylesheet = StyleSheet.create((theme) => ({
|
|||||||
opacity: 0.72,
|
opacity: 0.72,
|
||||||
},
|
},
|
||||||
secondaryLabel: {
|
secondaryLabel: {
|
||||||
flex: 1,
|
flexShrink: 1,
|
||||||
|
minWidth: 0,
|
||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
fontSize: theme.fontSize.base,
|
fontSize: theme.fontSize.base,
|
||||||
fontWeight: theme.fontWeight.normal,
|
fontWeight: theme.fontWeight.normal,
|
||||||
@@ -2064,7 +2065,7 @@ function ExpandableBadgeSecondaryLabel({
|
|||||||
onSecondaryLayout,
|
onSecondaryLayout,
|
||||||
}: ExpandableBadgeSecondaryLabelProps) {
|
}: ExpandableBadgeSecondaryLabelProps) {
|
||||||
if (!secondaryLabel) {
|
if (!secondaryLabel) {
|
||||||
return <View style={expandableBadgeStylesheet.spacer} />;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
@@ -2125,6 +2126,11 @@ interface ExpandableBadgeLabelRowProps {
|
|||||||
onLabelRowLayout: (event: LayoutChangeEvent) => void;
|
onLabelRowLayout: (event: LayoutChangeEvent) => void;
|
||||||
onLabelLayout: (event: LayoutChangeEvent) => void;
|
onLabelLayout: (event: LayoutChangeEvent) => void;
|
||||||
onSecondaryLayout: (event: LayoutChangeEvent) => void;
|
onSecondaryLayout: (event: LayoutChangeEvent) => void;
|
||||||
|
showOpenFileButton: boolean;
|
||||||
|
isOpenFileHovered: boolean;
|
||||||
|
onOpenFilePress: (event: GestureResponderEvent) => void;
|
||||||
|
onOpenFileHoverIn: () => void;
|
||||||
|
onOpenFileHoverOut: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ExpandableBadgeLabelRow({
|
function ExpandableBadgeLabelRow({
|
||||||
@@ -2146,7 +2152,13 @@ function ExpandableBadgeLabelRow({
|
|||||||
onLabelRowLayout,
|
onLabelRowLayout,
|
||||||
onLabelLayout,
|
onLabelLayout,
|
||||||
onSecondaryLayout,
|
onSecondaryLayout,
|
||||||
|
showOpenFileButton,
|
||||||
|
isOpenFileHovered,
|
||||||
|
onOpenFilePress,
|
||||||
|
onOpenFileHoverIn,
|
||||||
|
onOpenFileHoverOut,
|
||||||
}: ExpandableBadgeLabelRowProps) {
|
}: ExpandableBadgeLabelRowProps) {
|
||||||
|
const { theme } = useUnistyles();
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={expandableBadgeStylesheet.labelRow}
|
style={expandableBadgeStylesheet.labelRow}
|
||||||
@@ -2165,6 +2177,23 @@ function ExpandableBadgeLabelRow({
|
|||||||
shouldMeasureWebShimmer={shouldMeasureWebShimmer}
|
shouldMeasureWebShimmer={shouldMeasureWebShimmer}
|
||||||
onSecondaryLayout={onSecondaryLayout}
|
onSecondaryLayout={onSecondaryLayout}
|
||||||
/>
|
/>
|
||||||
|
{showOpenFileButton ? (
|
||||||
|
<Pressable
|
||||||
|
onPress={onOpenFilePress}
|
||||||
|
onHoverIn={onOpenFileHoverIn}
|
||||||
|
onHoverOut={onOpenFileHoverOut}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Open file"
|
||||||
|
testID="tool-call-open-file"
|
||||||
|
style={expandableBadgeStylesheet.openFileButton}
|
||||||
|
hitSlop={6}
|
||||||
|
>
|
||||||
|
<FileSymlink
|
||||||
|
size={14}
|
||||||
|
color={isOpenFileHovered ? theme.colors.foreground : theme.colors.foregroundMuted}
|
||||||
|
/>
|
||||||
|
</Pressable>
|
||||||
|
) : null}
|
||||||
{isWebShimmer ? (
|
{isWebShimmer ? (
|
||||||
<ExpandableBadgeWebShimmerOverlay
|
<ExpandableBadgeWebShimmerOverlay
|
||||||
label={label}
|
label={label}
|
||||||
@@ -2221,6 +2250,23 @@ function renderExpandableBadgeIcon({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderExpandableBadgeIconSlot({
|
||||||
|
showChevron,
|
||||||
|
chevronColor,
|
||||||
|
chevronStyle,
|
||||||
|
iconNode,
|
||||||
|
}: {
|
||||||
|
showChevron: boolean;
|
||||||
|
chevronColor: string;
|
||||||
|
chevronStyle: StyleProp<ViewStyle>;
|
||||||
|
iconNode: ReactNode;
|
||||||
|
}): ReactNode {
|
||||||
|
if (showChevron) {
|
||||||
|
return <ChevronRight size={14} color={chevronColor} style={chevronStyle} />;
|
||||||
|
}
|
||||||
|
return iconNode;
|
||||||
|
}
|
||||||
|
|
||||||
function computeShimmerMetrics(input: {
|
function computeShimmerMetrics(input: {
|
||||||
label: string;
|
label: string;
|
||||||
secondaryLabel: string | undefined;
|
secondaryLabel: string | undefined;
|
||||||
@@ -2343,6 +2389,7 @@ const ExpandableBadge = memo(function ExpandableBadge({
|
|||||||
const resolvedDisableOuterSpacing = useDisableOuterSpacing(disableOuterSpacing);
|
const resolvedDisableOuterSpacing = useDisableOuterSpacing(disableOuterSpacing);
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
const [isOpenFileHovered, setIsOpenFileHovered] = useState(false);
|
const [isOpenFileHovered, setIsOpenFileHovered] = useState(false);
|
||||||
|
const [isIconHovered, setIsIconHovered] = useState(false);
|
||||||
const [isPressed, setIsPressed] = useState(false);
|
const [isPressed, setIsPressed] = useState(false);
|
||||||
const isInteractive = Boolean(onToggle);
|
const isInteractive = Boolean(onToggle);
|
||||||
const hasDetailContent = Boolean(renderDetails);
|
const hasDetailContent = Boolean(renderDetails);
|
||||||
@@ -2371,6 +2418,8 @@ const ExpandableBadge = memo(function ExpandableBadge({
|
|||||||
);
|
);
|
||||||
const handleOpenFileHoverIn = useCallback(() => setIsOpenFileHovered(true), []);
|
const handleOpenFileHoverIn = useCallback(() => setIsOpenFileHovered(true), []);
|
||||||
const handleOpenFileHoverOut = useCallback(() => setIsOpenFileHovered(false), []);
|
const handleOpenFileHoverOut = useCallback(() => setIsOpenFileHovered(false), []);
|
||||||
|
const handleIconHoverIn = useCallback(() => setIsIconHovered(true), []);
|
||||||
|
const handleIconHoverOut = useCallback(() => setIsIconHovered(false), []);
|
||||||
|
|
||||||
const nativeGradientIdRef = useRef(
|
const nativeGradientIdRef = useRef(
|
||||||
`shimmer-gradient-${Math.random().toString(36).substring(2, 9)}`,
|
`shimmer-gradient-${Math.random().toString(36).substring(2, 9)}`,
|
||||||
@@ -2567,6 +2616,12 @@ const ExpandableBadge = memo(function ExpandableBadge({
|
|||||||
|
|
||||||
const iconColor = resolveExpandableBadgeIconColor({ isError, isActive, theme });
|
const iconColor = resolveExpandableBadgeIconColor({ isError, isActive, theme });
|
||||||
const iconNode = renderExpandableBadgeIcon({ isError, iconColor, icon });
|
const iconNode = renderExpandableBadgeIcon({ isError, iconColor, icon });
|
||||||
|
const iconSlotNode = renderExpandableBadgeIconSlot({
|
||||||
|
showChevron: isInteractive && isHovered && !isIconHovered,
|
||||||
|
chevronColor: theme.colors.foreground,
|
||||||
|
chevronStyle,
|
||||||
|
iconNode,
|
||||||
|
});
|
||||||
|
|
||||||
const pressHandlers = isInteractive
|
const pressHandlers = isInteractive
|
||||||
? {
|
? {
|
||||||
@@ -2591,7 +2646,13 @@ const ExpandableBadge = memo(function ExpandableBadge({
|
|||||||
style={pressableStyle}
|
style={pressableStyle}
|
||||||
>
|
>
|
||||||
<View style={expandableBadgeStylesheet.headerRow}>
|
<View style={expandableBadgeStylesheet.headerRow}>
|
||||||
<View style={expandableBadgeStylesheet.iconBadge}>{iconNode}</View>
|
<View
|
||||||
|
style={expandableBadgeStylesheet.iconBadge}
|
||||||
|
onPointerEnter={isWeb ? handleIconHoverIn : undefined}
|
||||||
|
onPointerLeave={isWeb ? handleIconHoverOut : undefined}
|
||||||
|
>
|
||||||
|
{iconSlotNode}
|
||||||
|
</View>
|
||||||
<ExpandableBadgeLabelRow
|
<ExpandableBadgeLabelRow
|
||||||
label={label}
|
label={label}
|
||||||
labelStyle={labelStyle}
|
labelStyle={labelStyle}
|
||||||
@@ -2611,27 +2672,12 @@ const ExpandableBadge = memo(function ExpandableBadge({
|
|||||||
onLabelRowLayout={handleLabelRowLayout}
|
onLabelRowLayout={handleLabelRowLayout}
|
||||||
onLabelLayout={handleLabelLayout}
|
onLabelLayout={handleLabelLayout}
|
||||||
onSecondaryLayout={handleSecondaryLayout}
|
onSecondaryLayout={handleSecondaryLayout}
|
||||||
|
showOpenFileButton={Boolean(onOpenFile && isHovered)}
|
||||||
|
isOpenFileHovered={isOpenFileHovered}
|
||||||
|
onOpenFilePress={handleOpenFilePress}
|
||||||
|
onOpenFileHoverIn={handleOpenFileHoverIn}
|
||||||
|
onOpenFileHoverOut={handleOpenFileHoverOut}
|
||||||
/>
|
/>
|
||||||
{onOpenFile && isHovered ? (
|
|
||||||
<Pressable
|
|
||||||
onPress={handleOpenFilePress}
|
|
||||||
onHoverIn={handleOpenFileHoverIn}
|
|
||||||
onHoverOut={handleOpenFileHoverOut}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel="Open file"
|
|
||||||
testID="tool-call-open-file"
|
|
||||||
style={expandableBadgeStylesheet.openFileButton}
|
|
||||||
hitSlop={6}
|
|
||||||
>
|
|
||||||
<FileSymlink
|
|
||||||
size={14}
|
|
||||||
color={isOpenFileHovered ? theme.colors.foreground : theme.colors.foregroundMuted}
|
|
||||||
/>
|
|
||||||
</Pressable>
|
|
||||||
) : null}
|
|
||||||
{isInteractive && isHovered ? (
|
|
||||||
<ChevronRight size={14} color={theme.colors.foreground} style={chevronStyle} />
|
|
||||||
) : null}
|
|
||||||
</View>
|
</View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
{detailContent ? (
|
{detailContent ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user