Make the sidebar resize handle usable by touch (#2953)

* fix(app): make the sidebar resize handle usable by touch

The drag handle was a 10px strip at `right: -5`, so half of it sat outside
the sidebar's bounds. Web hit-testing does not clip to the parent box, but
iOS `hitTest:` and Gesture Handler's Android traversal both do, leaving a
~5px target with no visible affordance — hover is the only cue, and hover
never fires on native.

Split the handle by input capability rather than by platform: a fine pointer
keeps the hairline straddling the border, and touch gets a 24x88 grab target
that sits fully inside the sidebar with a permanent grip. The pan gesture now
requires horizontal intent and fails on vertical drift, so a finger on the
grip still scrolls the list behind it.

`useHasFinePointer` replaces the copy of the same media query that lived in
the review surface.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(app): simplify touch resize handle styles

* fix(app): reveal touch resize grip while pressed

Keep the touch grip out of sidebar content while idle, then expose it for the full resize gesture lifecycle so it remains visible during an active drag.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
Ivan Malison
2026-08-08 22:32:24 +02:00
committed by GitHub
co-authored by Claude Opus 5 Mohamed Boudra
parent b9b3a24d58
commit 1e0ce69c54
7 changed files with 276 additions and 48 deletions
+2 -28
View File
@@ -16,6 +16,7 @@ import { StyleSheet, withUnistyles } from "react-native-unistyles";
import { Button } from "@/components/ui/button";
import { Shortcut } from "@/components/ui/shortcut";
import { isWeb } from "@/constants/platform";
import { useHasFinePointer } from "@/hooks/use-fine-pointer";
import { inlineUnistylesStyle } from "@/styles/unistyles-inline-style";
import type { Theme } from "@/styles/theme";
import type { ShortcutKey } from "@/utils/format-shortcut";
@@ -46,33 +47,6 @@ function getWebTextInputElement(input: TextInput | null): HTMLElement | null {
return element instanceof HTMLElement ? element : null;
}
function getCanShowReviewKeyboardHints(): boolean {
if (!isWeb || typeof window === "undefined" || typeof window.matchMedia !== "function") {
return false;
}
return window.matchMedia("(hover: hover) and (pointer: fine)").matches;
}
function useCanShowReviewKeyboardHints(): boolean {
const [canShowHints, setCanShowHints] = useState(getCanShowReviewKeyboardHints);
useEffect(() => {
if (!isWeb || typeof window === "undefined" || typeof window.matchMedia !== "function") {
return;
}
const mediaQuery = window.matchMedia("(hover: hover) and (pointer: fine)");
const handleChange = () => setCanShowHints(mediaQuery.matches);
handleChange();
mediaQuery.addEventListener?.("change", handleChange);
return () => {
mediaQuery.removeEventListener?.("change", handleChange);
};
}, []);
return canShowHints;
}
export const INLINE_REVIEW_COMMENT_HEIGHT = 72;
export const INLINE_REVIEW_EDITOR_HEIGHT = 132;
const INLINE_REVIEW_GAP = 6;
@@ -533,7 +507,7 @@ export function InlineReviewEditor({
const { t } = useTranslation();
const inputRef = useRef<TextInput | null>(null);
const focus = useWorkspaceFocusRestoration();
const canShowKeyboardHints = useCanShowReviewKeyboardHints();
const canShowKeyboardHints = useHasFinePointer();
const [body, setBody] = useState(initialBody);
const [isFocused, setIsFocused] = useState(false);
const trimmedBody = body.trim();