Decouples file drag-and-drop into a FileDropZone provider (drag target + backdrop) and a useFileDrop consumer hook, with drag state on a Reanimated shared value so dragging triggers no React re-renders. Fixes the collapsed/invisible backdrop (the drop zone had been moved into the composer with flex:0, collapsing the overlay to zero height) and restores whole-surface coverage across the agent panel, New Workspace screen, draft workspace tab, and workspace-setup dialog. The backdrop and copy cursor only appear when a drop will actually be accepted (a consumer is mounted and not submitting). Refs #520
72 lines
2.3 KiB
TypeScript
72 lines
2.3 KiB
TypeScript
import { View, Text, StyleSheet as RNStyleSheet } from "react-native";
|
|
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
|
import Animated, { useAnimatedStyle, withTiming } from "react-native-reanimated";
|
|
import { Upload } from "lucide-react-native";
|
|
import { useMemo } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import type { Theme } from "@/styles/theme";
|
|
import { useFileDropContext } from "./context";
|
|
|
|
const ThemedUpload = withUnistyles(Upload);
|
|
const primaryIconColorMapping = (theme: Theme) => ({ color: theme.colors.primary });
|
|
|
|
/**
|
|
* Drop overlay rendered by FileDropZone. Reads `isDragging` on the UI thread so the dim
|
|
* only ever repaints the backdrop — never the surrounding tree.
|
|
*/
|
|
export function FileDropBackdrop() {
|
|
const { t } = useTranslation();
|
|
const ctx = useFileDropContext();
|
|
const isDragging = ctx?.isDragging;
|
|
const suppressed = ctx?.suppressed;
|
|
const hasSink = ctx?.hasSink;
|
|
|
|
const animatedStyle = useAnimatedStyle(() => {
|
|
const active = isDragging?.value && hasSink?.value && !suppressed?.value;
|
|
return { opacity: withTiming(active ? 1 : 0, { duration: 150 }) };
|
|
});
|
|
|
|
const overlayStyle = useMemo(() => [positionStyles.overlay, animatedStyle], [animatedStyle]);
|
|
|
|
if (!ctx) return null;
|
|
|
|
// Animated.View keeps only plain-RN positioning; theme-dependent paint lives on the
|
|
// non-animated children (applying themed Unistyles styles to an Animated.View crashes
|
|
// on theme change — see docs/unistyles.md).
|
|
return (
|
|
<Animated.View style={overlayStyle} pointerEvents="none">
|
|
<View style={styles.backdrop} />
|
|
<View style={styles.content}>
|
|
<ThemedUpload size={32} uniProps={primaryIconColorMapping} />
|
|
<Text style={styles.text}>{t("composer.attachments.dropFilesHere")}</Text>
|
|
</View>
|
|
</Animated.View>
|
|
);
|
|
}
|
|
|
|
const positionStyles = RNStyleSheet.create({
|
|
overlay: {
|
|
...RNStyleSheet.absoluteFillObject,
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
zIndex: 1000,
|
|
},
|
|
});
|
|
|
|
const styles = StyleSheet.create((theme) => ({
|
|
backdrop: {
|
|
...StyleSheet.absoluteFillObject,
|
|
backgroundColor: theme.colors.surface0,
|
|
opacity: 0.7,
|
|
},
|
|
content: {
|
|
alignItems: "center",
|
|
gap: theme.spacing[2],
|
|
},
|
|
text: {
|
|
fontSize: theme.fontSize.base,
|
|
fontWeight: theme.fontWeight.medium,
|
|
color: theme.colors.foreground,
|
|
},
|
|
}));
|