import { useCallback, useEffect, useMemo, useRef, type ReactElement, type RefObject } from "react"; import { useQuery } from "@tanstack/react-query"; import { useTranslation } from "react-i18next"; import { FlatList, ListRenderItemInfo, Pressable, Text, View, type PressableStateCallbackType, type StyleProp, type ViewStyle, } from "react-native"; import { StyleSheet, useUnistyles, withUnistyles } from "react-native-unistyles"; import { WORKSPACE_SECONDARY_HEADER_HEIGHT } from "@/constants/layout"; import * as Clipboard from "expo-clipboard"; import { ChevronDown, Eye, EyeOff, RotateCw } from "lucide-react-native"; import { MaterialFileIcon } from "@/components/material-file-icon"; import { TreeChevron, TreeIndentGuides, treeRowPaddingLeft, WORKSPACE_FILE_ROW_VERTICAL_PADDING, } from "@/components/tree-primitives"; import { LoadingSpinner } from "@/components/ui/loading-spinner"; import type { Theme } from "@/styles/theme"; import type { AgentFileExplorerState, ExplorerDirectory, ExplorerEntry, } from "@/stores/session-store"; import { useSessionStore } from "@/stores/session-store"; import { FileActionsMenu } from "@/components/file-actions-menu"; import { useFileDownload } from "@/hooks/use-file-download"; import { useFileExplorerActions } from "@/hooks/use-file-explorer-actions"; import { buildWorkspaceExplorerStateKey } from "@/hooks/use-file-explorer-actions"; import { usePanelStore, type ExpandedPathsUpdate, type SortOption } from "@/stores/panel-store"; import { formatTimeAgo } from "@/utils/time"; import { buildAbsoluteExplorerPath } from "@/utils/explorer-paths"; import { isHiddenExplorerPath } from "@/file-explorer/visibility"; import { flattenExplorerTree, reconcileRestoredExpandedPaths, restoreExpandedDirectories, setExpandedDirectoryPath, showHiddenFilesAndRestoreExpandedDirectories, type ExplorerTreeRow, } from "@/file-explorer/tree"; import { useWorkspaceFileDragSource } from "@/attachments/use-workspace-file-drag-source"; const SORT_OPTIONS: { value: SortOption }[] = [ { value: "name" }, { value: "modified" }, { value: "size" }, ]; const ThemedLoadingSpinner = withUnistyles(LoadingSpinner); const foregroundMutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted, }); function formatFileSize({ size }: { size: number }): string { if (size < 1024) { return `${size} B`; } if (size < 1024 * 1024) { return `${(size / 1024).toFixed(1)} KB`; } return `${(size / (1024 * 1024)).toFixed(1)} MB`; } interface TreeRowItemProps { serverId: string; workspaceId?: string | null; entry: ExplorerEntry; depth: number; isExpanded: boolean; isSelected: boolean; loading: boolean; onEntryPress: (entry: ExplorerEntry) => void; onCopyPath: (path: string) => void; onDownloadEntry: (entry: ExplorerEntry) => void; onAddToChat?: (path: string) => void; testID?: string; } function sortTriggerStyle({ hovered, pressed, }: PressableStateCallbackType & { hovered?: boolean }) { return [styles.sortTrigger, (Boolean(hovered) || pressed) && styles.sortTriggerHovered]; } function iconButtonStyle({ hovered, pressed }: PressableStateCallbackType & { hovered?: boolean }) { return [styles.iconButton, (Boolean(hovered) || pressed) && styles.iconButtonHovered]; } function treeRowKeyExtractor(row: ExplorerTreeRow) { return row.entry.path; } function TreeRowItem({ serverId, workspaceId, entry, depth, isExpanded, isSelected, loading, onEntryPress, onCopyPath, onDownloadEntry, onAddToChat, testID, }: TreeRowItemProps) { const { t } = useTranslation(); const isDirectory = entry.kind === "directory"; const dragSourceRef = useWorkspaceFileDragSource({ enabled: !isDirectory, serverId, workspaceId, path: entry.path, }); const handlePress = useCallback(() => { onEntryPress(entry); }, [onEntryPress, entry]); const pressableStyle = useCallback( ({ hovered, pressed }: PressableStateCallbackType & { hovered?: boolean }) => [ styles.entryRow, { paddingLeft: treeRowPaddingLeft(depth) }, (Boolean(hovered) || pressed || isSelected) && styles.entryRowActive, ], [depth, isSelected], ); const handleCopy = useCallback(() => { onCopyPath(entry.path); }, [onCopyPath, entry.path]); const handleDownload = useCallback(() => { onDownloadEntry(entry); }, [onDownloadEntry, entry]); const handleAddToChat = useCallback(() => { onAddToChat?.(entry.path); }, [onAddToChat, entry.path]); const metaHeader = useMemo( () => ( {t("workspace.fileExplorer.context.size")} {formatFileSize({ size: entry.size })} {t("workspace.fileExplorer.context.modified")} {formatTimeAgo(new Date(entry.modifiedAt))} ), [entry.modifiedAt, entry.size, t], ); return ( {(() => { if (!isDirectory) { return ; } if (loading) { return ; } return ; })()} {entry.name} ); } interface FileExplorerPaneProps { serverId: string; workspaceId?: string | null; workspaceRoot: string; onOpenFile?: (filePath: string) => void; onAddToChat?: (path: string) => void; } export function FileExplorerPane({ serverId, workspaceId, workspaceRoot, onOpenFile, onAddToChat, }: FileExplorerPaneProps) { const { t } = useTranslation(); const normalizedWorkspaceRoot = useMemo(() => workspaceRoot.trim(), [workspaceRoot]); const workspaceStateKey = useMemo( () => buildWorkspaceExplorerStateKey({ workspaceId, workspaceRoot: normalizedWorkspaceRoot, }), [normalizedWorkspaceRoot, workspaceId], ); const hasWorkspaceScope = Boolean(workspaceStateKey && normalizedWorkspaceRoot); const explorerState = useSessionStore((state) => workspaceStateKey && state.sessions[serverId] ? state.sessions[serverId]?.fileExplorer.get(workspaceStateKey) : undefined, ); const { requestDirectoryListing, selectExplorerEntry } = useFileExplorerActions({ serverId, workspaceId, workspaceRoot: normalizedWorkspaceRoot, }); const downloadFile = useFileDownload({ serverId, workspaceId, workspaceRoot: normalizedWorkspaceRoot, }); const sortOption = usePanelStore((state) => state.explorerSortOption); const showHiddenFiles = usePanelStore((state) => state.explorerShowHiddenFiles); const setSortOption = usePanelStore((state) => state.setExplorerSortOption); const toggleExplorerShowHiddenFiles = usePanelStore( (state) => state.toggleExplorerShowHiddenFiles, ); const expandedPathsArray = usePanelStore((state) => workspaceStateKey ? state.expandedPathsByWorkspace[workspaceStateKey] : undefined, ); const setExpandedPathsForWorkspace = usePanelStore((state) => state.setExpandedPathsForWorkspace); const expandedPaths = useMemo( () => new Set(expandedPathsArray && expandedPathsArray.length > 0 ? expandedPathsArray : ["."]), [expandedPathsArray], ); const explorerDerived = useMemo(() => deriveExplorerFields(explorerState), [explorerState]); const { directories, pendingRequest, isExplorerLoading, error, selectedEntryPath } = explorerDerived; const isDirectoryLoading = useCallback( (path: string) => isPendingListForPath({ isExplorerLoading, pendingRequest, path }), [isExplorerLoading, pendingRequest], ); const treeListRef = useRef>(null); const hasInitializedRef = useRef(false); useEffect(() => { hasInitializedRef.current = false; }, [workspaceStateKey]); useEffect(() => { void initializeExplorer({ hasWorkspaceScope, hasInitializedRef, workspaceStateKey, persistedExpandedPaths: expandedPaths, showHiddenFiles, requestDirectoryListing, setExpandedPathsForWorkspace, }); }, [ expandedPaths, hasWorkspaceScope, requestDirectoryListing, setExpandedPathsForWorkspace, showHiddenFiles, workspaceStateKey, ]); const handleToggleDirectory = useCallback( (entry: ExplorerEntry) => toggleDirectory({ entry, workspaceStateKey, expandedPaths, directories, requestDirectoryListing, setExpandedPathsForWorkspace, }), [ workspaceStateKey, expandedPaths, directories, requestDirectoryListing, setExpandedPathsForWorkspace, ], ); const handleOpenFile = useCallback( (entry: ExplorerEntry) => { if (!hasWorkspaceScope) { return; } selectExplorerEntry(entry.path); onOpenFile?.(entry.path); }, [hasWorkspaceScope, onOpenFile, selectExplorerEntry], ); const handleEntryPress = useCallback( (entry: ExplorerEntry) => { if (entry.kind === "directory") { handleToggleDirectory(entry); return; } handleOpenFile(entry); }, [handleOpenFile, handleToggleDirectory], ); const handleCopyPath = useCallback( async (path: string) => { await Clipboard.setStringAsync( buildAbsoluteExplorerPath({ workspaceRoot: normalizedWorkspaceRoot, entryPath: path, }), ); }, [normalizedWorkspaceRoot], ); const handleDownloadEntry = useCallback( (entry: ExplorerEntry) => { if (entry.kind !== "file") { return; } downloadFile({ fileName: entry.name, path: entry.path }); }, [downloadFile], ); const handleSortCycle = useCallback(() => { const currentIndex = SORT_OPTIONS.findIndex((opt) => opt.value === sortOption); const nextIndex = (currentIndex + 1) % SORT_OPTIONS.length; setSortOption(SORT_OPTIONS[nextIndex].value); }, [sortOption, setSortOption]); const handleToggleHiddenFiles = useCallback(() => { const willShow = !usePanelStore.getState().explorerShowHiddenFiles; if (!willShow) { toggleExplorerShowHiddenFiles(); return; } const rootDirectory = directories.get("."); if (!rootDirectory || !workspaceStateKey) { toggleExplorerShowHiddenFiles(); return; } void showHiddenFilesAndRestoreExpandedDirectories({ rootDirectory, persistedExpandedPaths: expandedPaths, showHiddenFiles: toggleExplorerShowHiddenFiles, requestDirectoryListing: (path) => requestDirectoryListing(path, { recordHistory: false, setCurrentPath: false, }), }).then((restoredPaths) => { setExpandedPathsForWorkspace(workspaceStateKey, (currentPaths) => reconcileRestoredExpandedPaths({ persistedExpandedPaths: expandedPaths, currentExpandedPaths: new Set(currentPaths), restoredExpandedPaths: restoredPaths, }), ); return null; }); }, [ directories, expandedPaths, requestDirectoryListing, setExpandedPathsForWorkspace, toggleExplorerShowHiddenFiles, workspaceStateKey, ]); const refreshExplorer = useCallback( () => refreshExplorerDirectories({ hasWorkspaceScope, expandedPaths, requestDirectoryListing, }), [expandedPaths, hasWorkspaceScope, requestDirectoryListing], ); const { refetch: refetchExplorer, isFetching: isRefreshFetching } = useQuery({ queryKey: ["fileExplorerRefresh", serverId, workspaceStateKey], queryFn: refreshExplorer, enabled: false, }); const handleRefresh = useCallback(() => { void refetchExplorer(); }, [refetchExplorer]); const sortLabels = useMemo( () => ({ name: t("workspace.fileExplorer.sort.name"), modified: t("workspace.fileExplorer.sort.modified"), size: t("workspace.fileExplorer.sort.size"), }), [t], ); const currentSortLabel = resolveCurrentSortLabel(sortOption, sortLabels); const treeRows = useMemo( () => flattenExplorerTree({ directories, expandedPaths, sortOption, showHiddenFiles }), [directories, expandedPaths, showHiddenFiles, sortOption], ); const showInitialLoading = resolveShowInitialLoading({ directories, isExplorerLoading, pendingRequest, }); const showBackFromError = Boolean(error && selectedEntryPath); const errorRecoveryPath = useMemo(() => getErrorRecoveryPath(explorerState), [explorerState]); const renderTreeRow = useCallback( (info: ListRenderItemInfo) => ( ), [ expandedPaths, handleEntryPress, handleCopyPath, handleDownloadEntry, isDirectoryLoading, selectedEntryPath, onAddToChat, serverId, workspaceId, ], ); const handleBackFromError = useCallback(() => { if (!hasWorkspaceScope) { return; } selectExplorerEntry(null); void requestDirectoryListing(errorRecoveryPath, { recordHistory: false, setCurrentPath: true, }); }, [errorRecoveryPath, hasWorkspaceScope, requestDirectoryListing, selectExplorerEntry]); const handleRetry = useCallback(() => { void requestDirectoryListing(".", { recordHistory: false, setCurrentPath: false, }); }, [requestDirectoryListing]); if (!hasWorkspaceScope) { return ( {t("workspace.fileExplorer.states.unavailable")} ); } return ( ); } interface FileExplorerPaneContentProps { error: string | null; showInitialLoading: boolean; showBackFromError: boolean; treeRows: ExplorerTreeRow[]; currentSortLabel: string; isRefreshFetching: boolean; treeListRef: RefObject | null>; renderTreeRow: (info: ListRenderItemInfo) => ReactElement; handleSortCycle: () => void; handleToggleHiddenFiles: () => void; handleRefresh: () => void; handleBackFromError: () => void; handleRetry: () => void; sortTriggerStyle: (state: PressableStateCallbackType) => StyleProp; iconButtonStyle: (state: PressableStateCallbackType) => StyleProp; } function FileExplorerPaneContent(props: FileExplorerPaneContentProps) { const { theme } = useUnistyles(); const { t } = useTranslation(); const { error, showInitialLoading, showBackFromError, treeRows, currentSortLabel, isRefreshFetching, treeListRef, renderTreeRow, handleSortCycle, handleToggleHiddenFiles, handleRefresh, handleBackFromError, handleRetry, sortTriggerStyle: sortTriggerStyleProp, iconButtonStyle: iconButtonStyleProp, } = props; const showHiddenFiles = usePanelStore((state) => state.explorerShowHiddenFiles); const hiddenFilesToggleAccessibilityLabel = showHiddenFiles ? t("workspace.fileExplorer.actions.hideHiddenFiles") : t("workspace.fileExplorer.actions.showHiddenFiles"); const emptyLabel = showHiddenFiles ? t("workspace.fileExplorer.empty.noFiles") : t("workspace.fileExplorer.empty.noVisibleFiles"); const hiddenFilesToggleStyle = useCallback( (state: PressableStateCallbackType) => [ iconButtonStyleProp(state), !showHiddenFiles && styles.iconButtonActive, ], [showHiddenFiles, iconButtonStyleProp], ); const hiddenFilesToggleAccessibilityState = useMemo( () => ({ selected: !showHiddenFiles }), [showHiddenFiles], ); if (error) { return ( {error} {showBackFromError ? ( {t("workspace.fileExplorer.actions.back")} ) : null} {t("workspace.fileExplorer.actions.retry")} ); } if (showInitialLoading) { return ( {t("workspace.fileExplorer.states.loading")} ); } return ( {currentSortLabel} {showHiddenFiles ? ( ) : ( )} {isRefreshFetching ? ( ) : ( )} {treeRows.length === 0 ? ( {emptyLabel} ) : ( )} ); } function deriveExplorerFields(state: AgentFileExplorerState | undefined) { return { directories: state?.directories ?? new Map(), pendingRequest: state?.pendingRequest ?? null, isExplorerLoading: state?.isLoading ?? false, error: state?.lastError ?? null, selectedEntryPath: state?.selectedEntryPath ?? null, }; } function isPendingListForPath({ isExplorerLoading, pendingRequest, path, }: { isExplorerLoading: boolean; pendingRequest: AgentFileExplorerState["pendingRequest"] | null; path: string; }): boolean { return Boolean( isExplorerLoading && pendingRequest?.mode === "list" && pendingRequest?.path === path, ); } function resolveShowInitialLoading({ directories, isExplorerLoading, pendingRequest, }: { directories: Map; isExplorerLoading: boolean; pendingRequest: AgentFileExplorerState["pendingRequest"] | null; }): boolean { if (directories.has(".")) { return false; } return Boolean( isExplorerLoading && pendingRequest?.mode === "list" && pendingRequest?.path === ".", ); } function resolveCurrentSortLabel( sortOption: SortOption, labels: Record, ): string { return labels[sortOption] ?? labels.name; } function toggleDirectory({ entry, workspaceStateKey, expandedPaths, directories, requestDirectoryListing, setExpandedPathsForWorkspace, }: { entry: ExplorerEntry; workspaceStateKey: string | null; expandedPaths: Set; directories: Map; requestDirectoryListing: ( path: string, opts?: { recordHistory?: boolean; setCurrentPath?: boolean }, ) => Promise; setExpandedPathsForWorkspace: (workspaceStateKey: string, paths: ExpandedPathsUpdate) => void; }): void { if (!workspaceStateKey) { return; } const isExpanded = expandedPaths.has(entry.path); setExpandedPathsForWorkspace(workspaceStateKey, (currentPaths) => setExpandedDirectoryPath({ currentExpandedPaths: currentPaths, directoryPath: entry.path, expanded: !isExpanded, }), ); if (!isExpanded && !directories.has(entry.path)) { void requestDirectoryListing(entry.path, { recordHistory: false, setCurrentPath: false, }); } } function TreeRowDispatcher({ serverId, workspaceId, info, expandedPaths, selectedEntryPath, isDirectoryLoading, onEntryPress, onCopyPath, onDownloadEntry, onAddToChat, }: { serverId: string; workspaceId?: string | null; info: ListRenderItemInfo; expandedPaths: Set; selectedEntryPath: string | null; isDirectoryLoading: (path: string) => boolean; onEntryPress: (entry: ExplorerEntry) => void; onCopyPath: (path: string) => void | Promise; onDownloadEntry: (entry: ExplorerEntry) => void; onAddToChat?: (path: string) => void; }) { const entry = info.item.entry; const depth = info.item.depth; const isDirectory = entry.kind === "directory"; const isExpanded = isDirectory && expandedPaths.has(entry.path); const isSelected = selectedEntryPath === entry.path; const loading = isDirectory && isDirectoryLoading(entry.path); return ( ); } async function initializeExplorer({ hasWorkspaceScope, hasInitializedRef, workspaceStateKey, persistedExpandedPaths, showHiddenFiles, requestDirectoryListing, setExpandedPathsForWorkspace, }: { hasWorkspaceScope: boolean; hasInitializedRef: RefObject; workspaceStateKey: string | null; persistedExpandedPaths: ReadonlySet; showHiddenFiles: boolean; requestDirectoryListing: ( path: string, opts?: { recordHistory?: boolean; setCurrentPath?: boolean }, ) => Promise; setExpandedPathsForWorkspace: (workspaceStateKey: string, paths: ExpandedPathsUpdate) => void; }): Promise { if (!hasWorkspaceScope || hasInitializedRef.current) { return; } hasInitializedRef.current = true; const rootDirectory = await requestDirectoryListing(".", { recordHistory: false, setCurrentPath: false, }); if (!rootDirectory) { hasInitializedRef.current = false; return; } if (!workspaceStateKey) { return; } const restoredPaths = await restoreExpandedDirectories({ rootDirectory, persistedExpandedPaths, showHiddenFiles, requestDirectoryListing: (path) => requestDirectoryListing(path, { recordHistory: false, setCurrentPath: false, }), }); const hiddenPersistedPaths = showHiddenFiles ? [] : Array.from(persistedExpandedPaths).filter(isHiddenExplorerPath); const restoredPathsWithHidden = [...restoredPaths, ...hiddenPersistedPaths]; setExpandedPathsForWorkspace(workspaceStateKey, (currentPaths) => reconcileRestoredExpandedPaths({ persistedExpandedPaths, currentExpandedPaths: new Set(currentPaths), restoredExpandedPaths: restoredPathsWithHidden, }), ); } async function refreshExplorerDirectories({ hasWorkspaceScope, expandedPaths, requestDirectoryListing, }: { hasWorkspaceScope: boolean; expandedPaths: Set; requestDirectoryListing: ( path: string, opts?: { recordHistory?: boolean; setCurrentPath?: boolean }, ) => Promise; }): Promise { if (!hasWorkspaceScope) { return null; } const showHiddenFiles = usePanelStore.getState().explorerShowHiddenFiles; const directoryPaths = Array.from(expandedPaths).filter( (path) => showHiddenFiles || !isHiddenExplorerPath(path), ); if (!directoryPaths.includes(".")) { directoryPaths.unshift("."); } await Promise.all( directoryPaths.map((path) => requestDirectoryListing(path, { recordHistory: false, setCurrentPath: false, }), ), ); return null; } function getErrorRecoveryPath(state: AgentFileExplorerState | undefined): string { if (!state) { return "."; } const currentHistoryPath = state.history.length > 0 ? state.history[state.history.length - 1] : null; const candidate = currentHistoryPath ?? state.lastVisitedPath ?? state.currentPath; if (!candidate || candidate.length === 0) { return "."; } return candidate; } const styles = StyleSheet.create((theme) => ({ container: { flex: 1, backgroundColor: theme.colors.surfaceSidebar, }, desktopSplit: { flex: 1, flexDirection: "row", minHeight: 0, }, treePane: { minWidth: 0, position: "relative", }, treePaneFill: { flex: 1, }, treePaneWithPreview: { flex: 0, flexGrow: 0, flexShrink: 0, borderLeftWidth: 1, borderLeftColor: theme.colors.border, }, splitResizeHandle: { position: "absolute", left: -5, top: 0, bottom: 0, width: 10, zIndex: 20, }, previewPane: { flex: 1, minWidth: 0, }, paneHeader: { height: WORKSPACE_SECONDARY_HEADER_HEIGHT, flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingRight: theme.spacing[3], borderBottomWidth: 1, borderBottomColor: theme.colors.border, }, sortTrigger: { flexDirection: "row", alignItems: "center", justifyContent: "center", gap: theme.spacing[1], marginLeft: theme.spacing[3] - theme.spacing[1], paddingHorizontal: theme.spacing[1], height: 24, borderRadius: theme.borderRadius.base, }, sortTriggerHovered: { backgroundColor: theme.colors.surface2, }, sortTriggerText: { fontSize: theme.fontSize.xs, color: theme.colors.foregroundMuted, }, headerActions: { flexDirection: "row", alignItems: "center", gap: theme.spacing[1], }, treeList: { flex: 1, minHeight: 0, }, entriesContent: { paddingTop: theme.spacing[2], paddingBottom: theme.spacing[4], }, centerState: { flex: 1, alignItems: "center", justifyContent: "center", gap: theme.spacing[2], padding: theme.spacing[4], }, loadingText: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.sm, }, errorText: { color: theme.colors.destructive, fontSize: theme.fontSize.base, textAlign: "center", }, retryButton: { borderRadius: theme.borderRadius.full, borderWidth: theme.borderWidth[1], borderColor: theme.colors.border, paddingHorizontal: theme.spacing[3], paddingVertical: theme.spacing[1], }, retryButtonText: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.semibold, }, errorActions: { flexDirection: "row", alignItems: "center", gap: theme.spacing[2], }, emptyText: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.base, textAlign: "center", }, binaryMetaText: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.sm, }, entryRow: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingVertical: WORKSPACE_FILE_ROW_VERTICAL_PADDING, paddingRight: theme.spacing[3], }, entryRowActive: { backgroundColor: theme.colors.surfaceSidebarHover, }, entryInfo: { flex: 1, flexDirection: "row", alignItems: "center", gap: theme.spacing[2], minWidth: 0, }, entryIcon: { flexShrink: 0, }, entryName: { flex: 1, color: theme.colors.foreground, fontSize: theme.fontSize.sm, }, contextMetaBlock: { paddingVertical: theme.spacing[1], }, contextMetaRow: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", minHeight: 32, paddingHorizontal: theme.spacing[3], }, contextMetaLabel: { fontSize: theme.fontSize.sm, color: theme.colors.foregroundMuted, flexShrink: 0, }, contextMetaValue: { fontSize: theme.fontSize.sm, color: theme.colors.foreground, fontWeight: theme.fontWeight.medium, flex: 1, minWidth: 0, textAlign: "right", }, previewHeaderText: { flex: 1, color: theme.colors.foreground, fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.normal, }, iconButton: { width: 22, height: 22, borderRadius: theme.borderRadius.md, alignItems: "center", justifyContent: "center", }, iconButtonHovered: { backgroundColor: theme.colors.surface2, }, iconButtonActive: { backgroundColor: theme.colors.surface2, }, refreshIcon: { width: 16, height: 16, alignItems: "center", justifyContent: "center", }, previewContent: { flex: 1, }, previewScrollContainer: { flex: 1, minHeight: 0, position: "relative", }, previewCodeScrollContent: { paddingTop: theme.spacing[3], paddingHorizontal: theme.spacing[3], paddingBottom: theme.spacing[3] + theme.spacing[2], }, codeText: { color: theme.colors.foreground, fontFamily: theme.fontFamily.mono, fontSize: theme.fontSize.code, flexShrink: 0, }, previewImageScrollContent: { flex: 1, alignItems: "center", justifyContent: "center", padding: theme.spacing[3], }, previewImage: { width: "100%", aspectRatio: 1, }, sheetBackground: { backgroundColor: theme.colors.surface2, }, handleIndicator: { backgroundColor: theme.colors.palette.zinc[600], }, sheetHeader: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingHorizontal: theme.spacing[4], paddingVertical: theme.spacing[3], borderBottomWidth: theme.borderWidth[1], borderBottomColor: theme.colors.border, }, sheetTitle: { fontSize: theme.fontSize.lg, fontWeight: theme.fontWeight.semibold, color: theme.colors.foreground, flex: 1, }, sheetCloseButton: { padding: theme.spacing[2], }, sheetCenterState: { flex: 1, alignItems: "center", justifyContent: "center", gap: theme.spacing[2], padding: theme.spacing[4], }, }));