Files
paseo/packages/app/src/components/diff-viewer.tsx
T
Mohamed Boudra 0f7fa55a0f Centralize platform gating and fix iPad/tablet support
Add `packages/app/src/constants/platform.ts` with canonical gates
(isWeb, isNative, getIsElectron) and refactor all ~100 ad-hoc
Platform.OS checks across 69 files to use shared imports.

Fix sidebar hover crash on native (onPointerEnter → onHoverIn),
fix tooltip to use breakpoint instead of platform detection, make
sidebar action buttons always visible on native where hover doesn't
work, and document the platform gating decision matrix in CLAUDE.md.
2026-04-13 13:24:00 +07:00

182 lines
5.1 KiB
TypeScript

import React from "react";
import { View, Text, ScrollView as RNScrollView } from "react-native";
import { ScrollView as GHScrollView } from "react-native-gesture-handler";
import { StyleSheet } from "react-native-unistyles";
import { Fonts } from "@/constants/theme";
import type { DiffLine, DiffSegment } from "@/utils/tool-call-parsers";
import { getCodeInsets } from "./code-insets";
import { isWeb } from "@/constants/platform";
const ScrollView = isWeb ? RNScrollView : GHScrollView;
interface DiffViewerProps {
diffLines: DiffLine[];
maxHeight?: number;
emptyLabel?: string;
fillAvailableHeight?: boolean;
}
export function DiffViewer({
diffLines,
maxHeight,
emptyLabel = "No changes to display",
fillAvailableHeight = false,
}: DiffViewerProps) {
const [scrollViewWidth, setScrollViewWidth] = React.useState(0);
if (!diffLines.length) {
return (
<View style={styles.emptyState}>
<Text style={styles.emptyText}>{emptyLabel}</Text>
</View>
);
}
return (
<ScrollView
style={[
styles.verticalScroll,
maxHeight !== undefined && { maxHeight },
fillAvailableHeight && styles.fillHeight,
]}
contentContainerStyle={styles.verticalContent}
nestedScrollEnabled
showsVerticalScrollIndicator
>
<ScrollView
horizontal
nestedScrollEnabled
showsHorizontalScrollIndicator
contentContainerStyle={styles.horizontalContent}
onLayout={(e) => setScrollViewWidth(e.nativeEvent.layout.width)}
>
<View style={[styles.linesContainer, scrollViewWidth > 0 && { minWidth: scrollViewWidth }]}>
{diffLines.map((line, index) => (
<View
key={`${line.type}-${index}`}
style={[
styles.line,
line.type === "header" && styles.headerLine,
line.type === "add" && styles.addLine,
line.type === "remove" && styles.removeLine,
line.type === "context" && styles.contextLine,
]}
>
{line.segments ? (
<Text style={styles.lineText}>
<Text style={line.type === "add" ? styles.addText : styles.removeText}>
{line.content[0]}
</Text>
{line.segments.map((segment, segIdx) => (
<Text
key={segIdx}
style={[
line.type === "add" ? styles.addText : styles.removeText,
segment.changed &&
(line.type === "add" ? styles.addHighlight : styles.removeHighlight),
]}
>
{segment.text}
</Text>
))}
</Text>
) : (
<Text
style={[
styles.lineText,
line.type === "header" && styles.headerText,
line.type === "add" && styles.addText,
line.type === "remove" && styles.removeText,
line.type === "context" && styles.contextText,
]}
>
{line.content}
</Text>
)}
</View>
))}
</View>
</ScrollView>
</ScrollView>
);
}
const styles = StyleSheet.create((theme) => {
const insets = getCodeInsets(theme);
return {
verticalScroll: {},
fillHeight: {
flex: 1,
minHeight: 0,
},
verticalContent: {
flexGrow: 1,
paddingBottom: insets.extraBottom,
},
horizontalContent: {
flexDirection: "column" as const,
paddingRight: insets.extraRight,
},
linesContainer: {
alignSelf: "flex-start",
padding: insets.padding,
},
line: {
minWidth: "100%",
paddingHorizontal: 0,
paddingVertical: theme.spacing[1],
},
lineText: {
fontFamily: Fonts.mono,
fontSize: theme.fontSize.xs,
color: theme.colors.foreground,
...(isWeb
? {
whiteSpace: "pre",
overflowWrap: "normal",
}
: null),
},
headerLine: {
backgroundColor: theme.colors.surface1,
},
headerText: {
color: theme.colors.foregroundMuted,
},
addLine: {
backgroundColor: "rgba(46, 160, 67, 0.15)",
},
addText: {
color: theme.colors.foreground,
},
removeLine: {
backgroundColor: "rgba(248, 81, 73, 0.1)",
},
removeText: {
color: theme.colors.foreground,
},
addHighlight: {
backgroundColor: "rgba(46, 160, 67, 0.4)",
},
removeHighlight: {
backgroundColor: "rgba(248, 81, 73, 0.35)",
},
contextLine: {
backgroundColor: theme.colors.surface1,
},
contextText: {
color: theme.colors.foregroundMuted,
},
emptyState: {
padding: theme.spacing[4],
alignItems: "center" as const,
justifyContent: "center" as const,
},
emptyText: {
fontSize: theme.fontSize.sm,
color: theme.colors.foregroundMuted,
},
};
});