diff --git a/packages/app/src/components/sidebar-workspace-list.tsx b/packages/app/src/components/sidebar-workspace-list.tsx
index 38395dc55..aa2f157a9 100644
--- a/packages/app/src/components/sidebar-workspace-list.tsx
+++ b/packages/app/src/components/sidebar-workspace-list.tsx
@@ -117,6 +117,7 @@ import {
useSidebarWorkspaceTrailing,
} from "@/components/sidebar/workspace-trailing";
import { Button } from "@/components/ui/button";
+import { PressHighlight } from "@/components/ui/press-highlight";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { Shortcut } from "@/components/ui/shortcut";
import type { ShortcutKey } from "@/utils/format-shortcut";
@@ -973,9 +974,10 @@ function ProjectHeaderRow({
onPointerEnter={handlePointerEnter}
onPointerLeave={handlePointerLeave}
>
-
{rowChildren}
-
+
);
}
@@ -1001,6 +1003,7 @@ function ProjectHeaderRow({
enabledOnMobile={false}
accessibilityRole="button"
style={projectRowStyle}
+ highlightStyle={styles.projectRowHovered}
onPressIn={interaction.handlePressIn}
onTouchMove={interaction.handleTouchMove}
onPressOut={interaction.handlePressOut}
@@ -1119,6 +1122,7 @@ function WorkspaceRowInner({
accessibilityRole="button"
accessibilityState={accessibilityState}
style={workspaceRowStyle}
+ highlightStyle={styles.workspaceRowHovered}
onPressIn={interaction.handlePressIn}
onTouchMove={interaction.handleTouchMove}
onPressOut={interaction.handlePressOut}
diff --git a/packages/app/src/components/sidebar/sidebar-status-list.tsx b/packages/app/src/components/sidebar/sidebar-status-list.tsx
index 596d14cb4..689ddcb35 100644
--- a/packages/app/src/components/sidebar/sidebar-status-list.tsx
+++ b/packages/app/src/components/sidebar/sidebar-status-list.tsx
@@ -729,6 +729,7 @@ function StatusWorkspaceRowInner({
accessibilityRole="button"
accessibilityState={accessibilityState}
style={workspaceRowStyle}
+ highlightStyle={styles.workspaceRowHovered}
onPress={onPress}
testID={`sidebar-workspace-row-${workspace.workspaceKey}`}
>
diff --git a/packages/app/src/components/sidebar/sidebar-workspace-menu.tsx b/packages/app/src/components/sidebar/sidebar-workspace-menu.tsx
index 3f0b00c6b..f5234a62a 100644
--- a/packages/app/src/components/sidebar/sidebar-workspace-menu.tsx
+++ b/packages/app/src/components/sidebar/sidebar-workspace-menu.tsx
@@ -239,7 +239,7 @@ export function SidebarWorkspaceMenu({
type ContextTriggerProps = Omit<
ComponentProps,
- "children" | "enabledOnMobile"
+ "children" | "enabledOnMobile" | "highlightStyle"
>;
export function SidebarWorkspaceContextMenu({
@@ -264,6 +264,7 @@ export function SidebarWorkspaceContextMenu({
onTogglePin,
openInFileManagerPath,
accessibilityLabel,
+ highlightStyle,
...triggerProps
}: PropsWithChildren<
SidebarWorkspaceMenuItemsProps &
@@ -274,6 +275,7 @@ export function SidebarWorkspaceContextMenu({
leadingProjectName?: string | null;
hostBadgeLabel?: string | null;
scriptSummary?: WorkspaceScriptSummary | null;
+ highlightStyle: ComponentProps["highlightStyle"];
}
>) {
const {
@@ -301,6 +303,7 @@ export function SidebarWorkspaceContextMenu({
{...triggerProps}
enabledOnMobile={false}
accessibilityLabel={accessibilityLabel ?? rowAccessibilityLabel}
+ highlightStyle={highlightStyle}
>
{children}
diff --git a/packages/app/src/components/sidebar/sidebar-workspace-row.tsx b/packages/app/src/components/sidebar/sidebar-workspace-row.tsx
index df4a27aba..41974e89e 100644
--- a/packages/app/src/components/sidebar/sidebar-workspace-row.tsx
+++ b/packages/app/src/components/sidebar/sidebar-workspace-row.tsx
@@ -324,6 +324,7 @@ function WorkspaceRowBody({
accessibilityRole="button"
accessibilityState={accessibilityState}
style={workspaceRowStyle}
+ highlightStyle={styles.workspaceRowHovered}
onPressIn={draggable ? interaction.handlePressIn : undefined}
onTouchMove={draggable ? interaction.handleTouchMove : undefined}
onPressOut={draggable ? interaction.handlePressOut : undefined}
diff --git a/packages/app/src/components/ui/context-menu.tsx b/packages/app/src/components/ui/context-menu.tsx
index b655c06e0..17a2aba75 100644
--- a/packages/app/src/components/ui/context-menu.tsx
+++ b/packages/app/src/components/ui/context-menu.tsx
@@ -7,7 +7,6 @@ import {
} from "react";
import {
Platform,
- Pressable,
StatusBar,
View,
type GestureResponderEvent,
@@ -28,6 +27,7 @@ import {
type MenuSurfaceProps,
type MenuTriggerState,
} from "@/components/ui/menu";
+import { PressHighlight } from "@/components/ui/press-highlight";
/**
* A menu opened by a long press or a right click, anchored to the point of the gesture rather
@@ -99,6 +99,7 @@ type TriggerStyleProp = StyleProp | ((state: MenuTriggerState) => Sty
export function ContextMenuTrigger({
children,
disabled,
+ highlightStyle,
style,
enabled = true,
enabledOnMobile = true,
@@ -108,6 +109,7 @@ export function ContextMenuTrigger({
...props
}: PropsWithChildren<
Omit & {
+ highlightStyle?: StyleProp;
style?: TriggerStyleProp;
enabled?: boolean;
enabledOnMobile?: boolean;
@@ -184,7 +186,7 @@ export function ContextMenuTrigger({
);
return (
-
{children}
-
+
);
}
diff --git a/packages/app/src/components/ui/press-highlight.native.tsx b/packages/app/src/components/ui/press-highlight.native.tsx
new file mode 100644
index 000000000..b809534fc
--- /dev/null
+++ b/packages/app/src/components/ui/press-highlight.native.tsx
@@ -0,0 +1,71 @@
+import { forwardRef, useMemo } from "react";
+import { Pressable, StyleSheet, View } from "react-native";
+import { Gesture, GestureDetector } from "react-native-gesture-handler";
+import Animated, { useAnimatedStyle, useSharedValue } from "react-native-reanimated";
+import type { PressHighlightProps } from "./press-highlight.types";
+
+export const PressHighlight = forwardRef(function PressHighlight(
+ { children, disabled, highlightStyle, ...props },
+ ref,
+) {
+ const highlighted = useSharedValue(0);
+ const animatedHighlightStyle = useAnimatedStyle(() => ({ opacity: highlighted.value }));
+ // This gesture owns visual acknowledgement only. Pressable remains the sole owner of
+ // activation, cancellation, accessibility, and the caller's drag/long-press callbacks.
+ const pressGesture = useMemo(
+ () =>
+ Gesture.Tap()
+ .enabled(disabled !== true && highlightStyle != null)
+ .maxDistance(8)
+ .shouldCancelWhenOutside(true)
+ .onBegin(() => {
+ highlighted.value = 1;
+ })
+ .onFinalize(() => {
+ highlighted.value = 0;
+ }),
+ [disabled, highlightStyle, highlighted],
+ );
+
+ const highlight = highlightStyle ? (
+
+ {/* Keep the themed Unistyles node separate from the node Reanimated patches. */}
+
+
+ ) : null;
+ const renderedChildren =
+ typeof children === "function" ? (
+ (state: Parameters[0]) => (
+ <>
+ {highlight}
+ {children(state)}
+ >
+ )
+ ) : (
+ <>
+ {highlight}
+ {children}
+ >
+ );
+
+ const pressable = (
+
+ {renderedChildren}
+
+ );
+
+ if (!highlightStyle) {
+ return pressable;
+ }
+
+ return {pressable};
+});
+
+const styles = StyleSheet.create({
+ highlight: {
+ ...StyleSheet.absoluteFillObject,
+ },
+ highlightFill: {
+ ...StyleSheet.absoluteFillObject,
+ },
+});
diff --git a/packages/app/src/components/ui/press-highlight.tsx b/packages/app/src/components/ui/press-highlight.tsx
new file mode 100644
index 000000000..bcbb2957b
--- /dev/null
+++ b/packages/app/src/components/ui/press-highlight.tsx
@@ -0,0 +1,11 @@
+import { forwardRef } from "react";
+import { Pressable } from "react-native";
+import type { View } from "react-native";
+import type { PressHighlightProps } from "./press-highlight.types";
+
+export const PressHighlight = forwardRef(function PressHighlight(
+ { highlightStyle: _highlightStyle, ...props },
+ ref,
+) {
+ return ;
+});
diff --git a/packages/app/src/components/ui/press-highlight.types.ts b/packages/app/src/components/ui/press-highlight.types.ts
new file mode 100644
index 000000000..2f0e3896b
--- /dev/null
+++ b/packages/app/src/components/ui/press-highlight.types.ts
@@ -0,0 +1,6 @@
+import type { PressableProps, StyleProp, ViewStyle } from "react-native";
+
+export interface PressHighlightProps extends PressableProps {
+ /** Native finger-down surface. Desktop hover remains owned by the caller. */
+ highlightStyle?: StyleProp;
+}