fix(desktop): show browser element annotation editor

The editor was trapped below the resident webview's body-level paint plane in split layouts. Render it through the shared overlay root while tracking the owning pane's bounds.
This commit is contained in:
Mohamed Boudra
2026-08-17 21:15:15 +02:00
parent ac6a0afdc7
commit 7d3212e365
2 changed files with 105 additions and 6 deletions
@@ -1,6 +1,7 @@
import {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
@@ -8,6 +9,7 @@ import {
type ReactNode,
createElement,
} from "react";
import { createPortal } from "react-dom";
import { Pressable, Text, TextInput, View, type StyleProp, type ViewStyle } from "react-native";
import {
ArrowLeft,
@@ -45,6 +47,7 @@ import {
import type { AttachmentMetadata, BrowserElementAttachment } from "@/attachments/types";
import { persistAttachmentFromDataUrl } from "@/attachments/service";
import { WORKSPACE_SECONDARY_HEADER_HEIGHT } from "@/constants/layout";
import { getOverlayRoot } from "@/lib/overlay-root";
import {
getDesktopHost,
isElectronRuntime,
@@ -1540,6 +1543,7 @@ export function BrowserPane({
})}
{pendingSelection ? (
<BrowserElementAnnotationCard
anchor={webviewClipRef.current}
selection={pendingSelection}
onSubmit={submitAnnotation}
onCancel={cancelAnnotation}
@@ -1551,15 +1555,18 @@ export function BrowserPane({
}
function BrowserElementAnnotationCard({
anchor,
selection,
onSubmit,
onCancel,
}: {
anchor: HTMLElement | null;
selection: BrowserElementSelection;
onSubmit: (annotation: BrowserElementAnnotation) => void;
onCancel: () => void;
}) {
const { t } = useTranslation();
const bounds = useElementBounds(anchor);
const [comment, setComment] = useState("");
const commentRef = useRef(comment);
commentRef.current = comment;
@@ -1591,8 +1598,12 @@ function BrowserElementAnnotationCard({
const elementText = truncateText(selection.text.trim().replace(/\s+/g, " "), 60);
const elementLabel = elementText ? `${selection.tag} · ${elementText}` : selection.tag;
return (
<View style={styles.annotationOverlay} pointerEvents="box-none">
if (!bounds) {
return null;
}
return createPortal(
<View style={[styles.annotationOverlay, bounds]} pointerEvents="box-none">
<View style={styles.annotationCard}>
<View style={styles.annotationHeader}>
<Text numberOfLines={1} style={styles.annotationTitle}>
@@ -1629,10 +1640,36 @@ function BrowserElementAnnotationCard({
</Button>
</View>
</View>
</View>
</View>,
getOverlayRoot(),
);
}
function useElementBounds(element: HTMLElement | null): ViewStyle | null {
const [bounds, setBounds] = useState<ViewStyle | null>(null);
useLayoutEffect(() => {
if (!element) {
setBounds(null);
return;
}
const update = () => {
const rect = element.getBoundingClientRect();
setBounds({ left: rect.left, top: rect.top, width: rect.width, height: rect.height });
};
update();
const observer = new ResizeObserver(update);
observer.observe(element);
window.addEventListener("resize", update);
return () => {
observer.disconnect();
window.removeEventListener("resize", update);
};
}, [element]);
return bounds;
}
const ThemedCloseIcon = withUnistyles(X);
const ThemedAnnotationInput = withUnistyles(TextInput);
const iconForegroundMutedMapping = (theme: { colors: { foregroundMuted: string } }) => ({
@@ -1740,11 +1777,9 @@ const styles = StyleSheet.create((theme) => ({
annotationOverlay: {
position: "absolute",
zIndex: 1,
left: 0,
right: 0,
bottom: 0,
padding: theme.spacing[3],
alignItems: "center",
justifyContent: "flex-end",
},
annotationCard: {
width: "100%",