feat(explorer-sidebar): add right-side file explorer and git diff panel
- Add swipe-left gesture to open explorer sidebar on mobile - Implement Changes (git diff) and Files tabs with toggle - Add responsive gallery columns based on container width - Handle hardware back button to close sidebar first - Add gesture conflict resolution for horizontal scroll in diffs - Add resize handle with cursor change on desktop - Add sorting selector (name, modified, size) in file explorer 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
9d7e44851c
commit
8c33df472f
@@ -0,0 +1,442 @@
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
import { View, Text, Pressable, Platform } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import Animated, {
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
runOnJS,
|
||||
} from "react-native-reanimated";
|
||||
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
||||
import { X, GitBranch, Folder, LayoutGrid, List as ListIcon } from "lucide-react-native";
|
||||
import {
|
||||
useExplorerSidebarStore,
|
||||
MIN_EXPLORER_SIDEBAR_WIDTH,
|
||||
MAX_EXPLORER_SIDEBAR_WIDTH,
|
||||
} from "@/stores/explorer-sidebar-store";
|
||||
import { useExplorerSidebarAnimation } from "@/contexts/explorer-sidebar-animation-context";
|
||||
import { GitDiffPane } from "./git-diff-pane";
|
||||
import { FileExplorerPane } from "./file-explorer-pane";
|
||||
|
||||
type ViewMode = "list" | "grid";
|
||||
|
||||
type ExplorerTab = "changes" | "files";
|
||||
|
||||
interface ExplorerSidebarProps {
|
||||
serverId: string;
|
||||
agentId: string;
|
||||
}
|
||||
|
||||
export function ExplorerSidebar({ serverId, agentId }: ExplorerSidebarProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { isOpen, activeTab, width, close, setActiveTab, setWidth } =
|
||||
useExplorerSidebarStore();
|
||||
const {
|
||||
translateX,
|
||||
backdropOpacity,
|
||||
windowWidth,
|
||||
animateToOpen,
|
||||
animateToClose,
|
||||
isGesturing,
|
||||
closeGestureRef,
|
||||
} = useExplorerSidebarAnimation();
|
||||
|
||||
const isMobile =
|
||||
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
||||
|
||||
// File explorer view mode state
|
||||
const [fileViewMode, setFileViewMode] = useState<ViewMode>("list");
|
||||
|
||||
// For resize drag, track the starting width
|
||||
const startWidthRef = useRef(width);
|
||||
const resizeWidth = useSharedValue(width);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
close();
|
||||
}, [close]);
|
||||
|
||||
const handleTabPress = useCallback(
|
||||
(tab: ExplorerTab) => {
|
||||
setActiveTab(tab);
|
||||
},
|
||||
[setActiveTab]
|
||||
);
|
||||
|
||||
// Swipe gesture to close (swipe right on mobile)
|
||||
const closeGesture = useMemo(
|
||||
() =>
|
||||
Gesture.Pan()
|
||||
.withRef(closeGestureRef)
|
||||
.enabled(isMobile && isOpen)
|
||||
// Only activate after 15px horizontal movement (creates deadzone for taps)
|
||||
.activeOffsetX([-15, 15])
|
||||
.failOffsetY([-10, 10])
|
||||
.onStart(() => {
|
||||
isGesturing.value = true;
|
||||
})
|
||||
.onUpdate((event) => {
|
||||
// Right sidebar: swipe right to close (positive translationX)
|
||||
const newTranslateX = Math.max(0, Math.min(windowWidth, event.translationX));
|
||||
translateX.value = newTranslateX;
|
||||
const progress = 1 - newTranslateX / windowWidth;
|
||||
backdropOpacity.value = Math.max(0, Math.min(1, progress));
|
||||
})
|
||||
.onEnd((event) => {
|
||||
isGesturing.value = false;
|
||||
const shouldClose =
|
||||
event.translationX > windowWidth / 3 || event.velocityX > 500;
|
||||
if (shouldClose) {
|
||||
animateToClose();
|
||||
runOnJS(handleClose)();
|
||||
} else {
|
||||
animateToOpen();
|
||||
}
|
||||
})
|
||||
.onFinalize(() => {
|
||||
isGesturing.value = false;
|
||||
}),
|
||||
[
|
||||
isMobile,
|
||||
isOpen,
|
||||
windowWidth,
|
||||
translateX,
|
||||
backdropOpacity,
|
||||
animateToOpen,
|
||||
animateToClose,
|
||||
handleClose,
|
||||
isGesturing,
|
||||
closeGestureRef,
|
||||
]
|
||||
);
|
||||
|
||||
// Desktop resize gesture (drag left edge)
|
||||
const resizeGesture = useMemo(
|
||||
() =>
|
||||
Gesture.Pan()
|
||||
.enabled(!isMobile)
|
||||
.hitSlop({ left: 8, right: 8, top: 0, bottom: 0 })
|
||||
.onStart(() => {
|
||||
startWidthRef.current = width;
|
||||
resizeWidth.value = width;
|
||||
})
|
||||
.onUpdate((event) => {
|
||||
// Dragging left (negative translationX) increases width
|
||||
const newWidth = startWidthRef.current - event.translationX;
|
||||
const clampedWidth = Math.max(
|
||||
MIN_EXPLORER_SIDEBAR_WIDTH,
|
||||
Math.min(MAX_EXPLORER_SIDEBAR_WIDTH, newWidth)
|
||||
);
|
||||
resizeWidth.value = clampedWidth;
|
||||
})
|
||||
.onEnd(() => {
|
||||
runOnJS(setWidth)(resizeWidth.value);
|
||||
}),
|
||||
[isMobile, width, resizeWidth, setWidth]
|
||||
);
|
||||
|
||||
const sidebarAnimatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ translateX: translateX.value }],
|
||||
}));
|
||||
|
||||
const backdropAnimatedStyle = useAnimatedStyle(() => ({
|
||||
opacity: backdropOpacity.value,
|
||||
pointerEvents: backdropOpacity.value > 0.01 ? "auto" : "none",
|
||||
}));
|
||||
|
||||
const resizeAnimatedStyle = useAnimatedStyle(() => ({
|
||||
width: resizeWidth.value,
|
||||
}));
|
||||
|
||||
// Mobile: full-screen overlay with gesture
|
||||
const overlayPointerEvents = Platform.OS === "web" ? "auto" : "box-none";
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<View style={StyleSheet.absoluteFillObject} pointerEvents={overlayPointerEvents}>
|
||||
{/* Backdrop */}
|
||||
<Animated.View style={[styles.backdrop, backdropAnimatedStyle]}>
|
||||
<Pressable style={styles.backdropPressable} onPress={handleClose} />
|
||||
</Animated.View>
|
||||
|
||||
<GestureDetector gesture={closeGesture} touchAction="pan-y">
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.mobileSidebar,
|
||||
{ width: windowWidth, paddingTop: insets.top, paddingBottom: insets.bottom },
|
||||
sidebarAnimatedStyle,
|
||||
]}
|
||||
pointerEvents="auto"
|
||||
>
|
||||
<SidebarContent
|
||||
activeTab={activeTab}
|
||||
onTabPress={handleTabPress}
|
||||
onClose={handleClose}
|
||||
serverId={serverId}
|
||||
agentId={agentId}
|
||||
fileViewMode={fileViewMode}
|
||||
onFileViewModeChange={setFileViewMode}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
</Animated.View>
|
||||
</GestureDetector>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// Desktop: fixed width sidebar with resize handle
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Animated.View style={[styles.desktopSidebar, resizeAnimatedStyle]}>
|
||||
{/* Resize handle on left edge */}
|
||||
<GestureDetector gesture={resizeGesture}>
|
||||
<View
|
||||
style={[
|
||||
styles.resizeHandle,
|
||||
Platform.OS === "web" && ({ cursor: "col-resize" } as any),
|
||||
]}
|
||||
>
|
||||
<View style={styles.resizeHandleInner} />
|
||||
</View>
|
||||
</GestureDetector>
|
||||
|
||||
<SidebarContent
|
||||
activeTab={activeTab}
|
||||
onTabPress={handleTabPress}
|
||||
onClose={handleClose}
|
||||
serverId={serverId}
|
||||
agentId={agentId}
|
||||
fileViewMode={fileViewMode}
|
||||
onFileViewModeChange={setFileViewMode}
|
||||
isMobile={false}
|
||||
/>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
interface SidebarContentProps {
|
||||
activeTab: ExplorerTab;
|
||||
onTabPress: (tab: ExplorerTab) => void;
|
||||
onClose: () => void;
|
||||
serverId: string;
|
||||
agentId: string;
|
||||
fileViewMode: ViewMode;
|
||||
onFileViewModeChange: (mode: ViewMode) => void;
|
||||
isMobile: boolean;
|
||||
}
|
||||
|
||||
function SidebarContent({
|
||||
activeTab,
|
||||
onTabPress,
|
||||
onClose,
|
||||
serverId,
|
||||
agentId,
|
||||
fileViewMode,
|
||||
onFileViewModeChange,
|
||||
isMobile,
|
||||
}: SidebarContentProps) {
|
||||
const { theme } = useUnistyles();
|
||||
|
||||
return (
|
||||
<View style={styles.sidebarContent} pointerEvents="auto">
|
||||
{/* Header with tabs and close button */}
|
||||
<View style={styles.header}>
|
||||
<View style={styles.tabsContainer}>
|
||||
<Pressable
|
||||
style={[styles.tab, activeTab === "changes" && styles.tabActive]}
|
||||
onPress={() => onTabPress("changes")}
|
||||
>
|
||||
<GitBranch
|
||||
size={16}
|
||||
color={
|
||||
activeTab === "changes"
|
||||
? theme.colors.foreground
|
||||
: theme.colors.mutedForeground
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.tabText,
|
||||
activeTab === "changes" && styles.tabTextActive,
|
||||
]}
|
||||
>
|
||||
Changes
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={[styles.tab, activeTab === "files" && styles.tabActive]}
|
||||
onPress={() => onTabPress("files")}
|
||||
>
|
||||
<Folder
|
||||
size={16}
|
||||
color={
|
||||
activeTab === "files"
|
||||
? theme.colors.foreground
|
||||
: theme.colors.mutedForeground
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.tabText,
|
||||
activeTab === "files" && styles.tabTextActive,
|
||||
]}
|
||||
>
|
||||
Files
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
<View style={styles.headerRightSection}>
|
||||
{activeTab === "files" && (
|
||||
<ViewToggle viewMode={fileViewMode} onChange={onFileViewModeChange} />
|
||||
)}
|
||||
{isMobile && (
|
||||
<Pressable onPress={onClose} style={styles.closeButton}>
|
||||
<X size={18} color={theme.colors.mutedForeground} />
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Content based on active tab */}
|
||||
<View style={styles.contentArea}>
|
||||
{activeTab === "changes" ? (
|
||||
<GitDiffPane serverId={serverId} agentId={agentId} />
|
||||
) : (
|
||||
<FileExplorerPane
|
||||
serverId={serverId}
|
||||
agentId={agentId}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function ViewToggle({
|
||||
viewMode,
|
||||
onChange,
|
||||
}: {
|
||||
viewMode: ViewMode;
|
||||
onChange: (mode: ViewMode) => void;
|
||||
}) {
|
||||
const { theme } = useUnistyles();
|
||||
|
||||
return (
|
||||
<View style={styles.viewToggleContainer}>
|
||||
<Pressable
|
||||
style={[styles.viewToggleButton, viewMode === "list" && styles.viewToggleActive]}
|
||||
onPress={() => onChange("list")}
|
||||
>
|
||||
<ListIcon size={14} color={theme.colors.foreground} />
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={[styles.viewToggleButton, viewMode === "grid" && styles.viewToggleActive]}
|
||||
onPress={() => onChange("grid")}
|
||||
>
|
||||
<LayoutGrid size={14} color={theme.colors.foreground} />
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
backdrop: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||
},
|
||||
backdropPressable: {
|
||||
flex: 1,
|
||||
},
|
||||
mobileSidebar: {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: theme.colors.background,
|
||||
overflow: "hidden",
|
||||
},
|
||||
desktopSidebar: {
|
||||
flexDirection: "row",
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: theme.colors.border,
|
||||
backgroundColor: theme.colors.background,
|
||||
},
|
||||
resizeHandle: {
|
||||
width: 8,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
resizeHandleInner: {
|
||||
width: 2,
|
||||
height: 32,
|
||||
backgroundColor: theme.colors.border,
|
||||
borderRadius: 1,
|
||||
opacity: 0.5,
|
||||
},
|
||||
sidebarContent: {
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
overflow: "hidden",
|
||||
},
|
||||
header: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
paddingVertical: theme.spacing[2],
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: theme.colors.border,
|
||||
},
|
||||
tabsContainer: {
|
||||
flexDirection: "row",
|
||||
gap: theme.spacing[1],
|
||||
},
|
||||
tab: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[2],
|
||||
paddingVertical: theme.spacing[2],
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
borderRadius: theme.borderRadius.md,
|
||||
},
|
||||
tabActive: {
|
||||
backgroundColor: theme.colors.muted,
|
||||
},
|
||||
tabText: {
|
||||
fontSize: theme.fontSize.sm,
|
||||
fontWeight: theme.fontWeight.normal,
|
||||
color: theme.colors.mutedForeground,
|
||||
},
|
||||
tabTextActive: {
|
||||
color: theme.colors.foreground,
|
||||
},
|
||||
headerRightSection: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[2],
|
||||
},
|
||||
closeButton: {
|
||||
padding: theme.spacing[2],
|
||||
borderRadius: theme.borderRadius.md,
|
||||
},
|
||||
contentArea: {
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
},
|
||||
viewToggleContainer: {
|
||||
flexDirection: "row",
|
||||
borderRadius: theme.borderRadius.md,
|
||||
borderWidth: theme.borderWidth[1],
|
||||
borderColor: theme.colors.border,
|
||||
overflow: "hidden",
|
||||
},
|
||||
viewToggleButton: {
|
||||
padding: theme.spacing[2],
|
||||
},
|
||||
viewToggleActive: {
|
||||
backgroundColor: theme.colors.muted,
|
||||
},
|
||||
}));
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,293 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { View, Text, ActivityIndicator, Platform } from "react-native";
|
||||
import { Gesture, GestureDetector, ScrollView } from "react-native-gesture-handler";
|
||||
import { StyleSheet, UnistylesRuntime } from "react-native-unistyles";
|
||||
import { useExplorerSidebarAnimation } from "@/contexts/explorer-sidebar-animation-context";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
|
||||
interface ParsedDiffFile {
|
||||
path: string;
|
||||
lines: Array<{
|
||||
type: "add" | "remove" | "context" | "header";
|
||||
content: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
function parseDiff(diffText: string): ParsedDiffFile[] {
|
||||
if (!diffText || diffText.trim().length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const files: ParsedDiffFile[] = [];
|
||||
const sections = diffText.split(/^diff --git /m).filter(Boolean);
|
||||
|
||||
for (const section of sections) {
|
||||
const lines = section.split("\n");
|
||||
const firstLine = lines[0];
|
||||
|
||||
const pathMatch = firstLine.match(/a\/(.*?) b\//);
|
||||
const path = pathMatch ? pathMatch[1] : "unknown";
|
||||
|
||||
const parsedLines: ParsedDiffFile["lines"] = [];
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
if (line.startsWith("+++") || line.startsWith("---") || line.startsWith("@@") || line.startsWith("index ")) {
|
||||
parsedLines.push({ type: "header", content: line });
|
||||
} else if (line.startsWith("+")) {
|
||||
parsedLines.push({ type: "add", content: line });
|
||||
} else if (line.startsWith("-")) {
|
||||
parsedLines.push({ type: "remove", content: line });
|
||||
} else {
|
||||
parsedLines.push({ type: "context", content: line });
|
||||
}
|
||||
}
|
||||
|
||||
files.push({ path, lines: parsedLines });
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
interface GitDiffPaneProps {
|
||||
serverId: string;
|
||||
agentId: string;
|
||||
}
|
||||
|
||||
export function GitDiffPane({ serverId, agentId }: GitDiffPaneProps) {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const hasRequestedRef = useRef<string | null>(null);
|
||||
const { closeGestureRef } = useExplorerSidebarAnimation();
|
||||
|
||||
const isMobile =
|
||||
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
||||
|
||||
// Pan gesture gate: only allow horizontal scroll after deliberate horizontal movement
|
||||
// This prevents diagonal scrolling from being captured by horizontal ScrollView
|
||||
const horizontalScrollGate = Gesture.Pan()
|
||||
.enabled(isMobile)
|
||||
.activeOffsetX([-15, 15]) // Require 15px horizontal movement to activate
|
||||
.failOffsetY([-10, 10]) // Fail if 10px vertical movement happens first
|
||||
.blocksExternalGesture(closeGestureRef); // Block sidebar close while scrolling
|
||||
|
||||
const agent = useSessionStore((state) =>
|
||||
state.sessions[serverId]?.agents?.get(agentId)
|
||||
);
|
||||
|
||||
const diffText = useSessionStore((state) =>
|
||||
state.sessions[serverId]?.gitDiffs?.get(agentId)
|
||||
);
|
||||
|
||||
const requestGitDiff = useSessionStore((state) =>
|
||||
state.sessions[serverId]?.methods?.requestGitDiff
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!agentId || !requestGitDiff) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent duplicate requests for the same agentId
|
||||
if (hasRequestedRef.current === agentId) {
|
||||
return;
|
||||
}
|
||||
hasRequestedRef.current = agentId;
|
||||
|
||||
setIsLoading(true);
|
||||
requestGitDiff(agentId);
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 5000);
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
}, [agentId, requestGitDiff]);
|
||||
|
||||
useEffect(() => {
|
||||
if (diffText !== undefined) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [diffText]);
|
||||
|
||||
if (!agent) {
|
||||
return (
|
||||
<View style={styles.errorContainer}>
|
||||
<Text style={styles.errorText}>Agent not found</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const isError = diffText?.startsWith("Error:");
|
||||
const parsedFiles = isError || !diffText ? [] : parseDiff(diffText);
|
||||
const hasChanges = parsedFiles.length > 0;
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.scrollView} contentContainerStyle={styles.contentContainer}>
|
||||
{isLoading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" />
|
||||
<Text style={styles.loadingText}>Loading changes...</Text>
|
||||
</View>
|
||||
) : isError ? (
|
||||
<View style={styles.errorContainer}>
|
||||
<Text style={styles.errorText}>{diffText}</Text>
|
||||
</View>
|
||||
) : !hasChanges ? (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Text style={styles.emptyText}>No changes</Text>
|
||||
</View>
|
||||
) : (
|
||||
parsedFiles.map((file, fileIndex) => (
|
||||
<View key={fileIndex} style={styles.fileSection}>
|
||||
<View style={styles.fileHeader}>
|
||||
<Text style={styles.filePath}>{file.path}</Text>
|
||||
</View>
|
||||
<View style={styles.diffContent}>
|
||||
<GestureDetector gesture={horizontalScrollGate}>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator
|
||||
nestedScrollEnabled
|
||||
directionalLockEnabled={Platform.OS === "ios"}
|
||||
>
|
||||
<View style={styles.diffLinesContainer}>
|
||||
{file.lines.map((line, lineIndex) => (
|
||||
<View
|
||||
key={lineIndex}
|
||||
style={[
|
||||
styles.diffLineContainer,
|
||||
line.type === "add" && styles.addLineContainer,
|
||||
line.type === "remove" && styles.removeLineContainer,
|
||||
line.type === "header" && styles.headerLineContainer,
|
||||
line.type === "context" && styles.contextLineContainer,
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.diffLineText,
|
||||
line.type === "add" && styles.addLineText,
|
||||
line.type === "remove" && styles.removeLineText,
|
||||
line.type === "header" && styles.headerLineText,
|
||||
line.type === "context" && styles.contextLineText,
|
||||
]}
|
||||
>
|
||||
{line.content}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</GestureDetector>
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
contentContainer: {
|
||||
padding: theme.spacing[4],
|
||||
paddingBottom: theme.spacing[8],
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
paddingTop: theme.spacing[16],
|
||||
gap: theme.spacing[4],
|
||||
},
|
||||
loadingText: {
|
||||
fontSize: theme.fontSize.base,
|
||||
color: theme.colors.mutedForeground,
|
||||
},
|
||||
errorContainer: {
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
paddingTop: theme.spacing[16],
|
||||
paddingHorizontal: theme.spacing[6],
|
||||
},
|
||||
errorText: {
|
||||
fontSize: theme.fontSize.base,
|
||||
color: theme.colors.destructive,
|
||||
textAlign: "center",
|
||||
},
|
||||
emptyContainer: {
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
paddingTop: theme.spacing[16],
|
||||
},
|
||||
emptyText: {
|
||||
fontSize: theme.fontSize.lg,
|
||||
color: theme.colors.mutedForeground,
|
||||
},
|
||||
fileSection: {
|
||||
marginBottom: theme.spacing[6],
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
overflow: "hidden",
|
||||
borderWidth: theme.borderWidth[1],
|
||||
borderColor: theme.colors.border,
|
||||
width: "100%",
|
||||
},
|
||||
fileHeader: {
|
||||
backgroundColor: theme.colors.muted,
|
||||
padding: theme.spacing[3],
|
||||
borderBottomWidth: theme.borderWidth[1],
|
||||
borderBottomColor: theme.colors.border,
|
||||
},
|
||||
filePath: {
|
||||
fontSize: theme.fontSize.sm,
|
||||
fontWeight: theme.fontWeight.semibold,
|
||||
color: theme.colors.foreground,
|
||||
fontFamily: "monospace",
|
||||
},
|
||||
diffContent: {
|
||||
backgroundColor: theme.colors.card,
|
||||
},
|
||||
diffLinesContainer: {
|
||||
minWidth: "100%",
|
||||
},
|
||||
diffLineContainer: {
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
paddingVertical: theme.spacing[1],
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-start",
|
||||
},
|
||||
diffLineText: {
|
||||
fontSize: theme.fontSize.xs,
|
||||
fontFamily: "monospace",
|
||||
color: theme.colors.foreground,
|
||||
},
|
||||
addLineContainer: {
|
||||
backgroundColor: theme.colors.palette.green[900],
|
||||
},
|
||||
addLineText: {
|
||||
color: theme.colors.palette.green[200],
|
||||
},
|
||||
removeLineContainer: {
|
||||
backgroundColor: theme.colors.palette.red[900],
|
||||
},
|
||||
removeLineText: {
|
||||
color: theme.colors.palette.red[200],
|
||||
},
|
||||
headerLineContainer: {
|
||||
backgroundColor: theme.colors.muted,
|
||||
},
|
||||
headerLineText: {
|
||||
color: theme.colors.mutedForeground,
|
||||
},
|
||||
contextLineContainer: {
|
||||
backgroundColor: theme.colors.card,
|
||||
},
|
||||
contextLineText: {
|
||||
color: theme.colors.mutedForeground,
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user