Files
paseo/packages/app/src/file-pane/bar.tsx
T
Mohamed BoudraandGitHub b6f1274f47 Clarify changed and deleted files in the file viewer (#2670)
* fix(app): distinguish changed and deleted files

Successful reads now reconcile transient missing events without letting stale
cached data mask a deletion. File notices expose only actions valid for the
current buffer state.

* fix(app): preserve file state ordering

* fix(app): allow retrying file checks

* fix(app): clear unchanged file conflicts

* fix(app): keep file conflict recovery consistent

Own disk observation and read ordering in one model so stale reads and failed subscriptions cannot produce contradictory actions.

* fix(app): allow retrying file preview errors

Preserve daemon read errors and expose retry for initial and non-editable file loads.
2026-07-30 22:50:58 +02:00

163 lines
4.9 KiB
TypeScript

import { Text, View } from "react-native";
import { StyleSheet, withUnistyles } from "react-native-unistyles";
import { useTranslation } from "react-i18next";
import { LoadingSpinner } from "@/components/ui/loading-spinner";
import { SegmentedControl } from "@/components/ui/segmented-control";
import type { Theme } from "@/styles/theme";
import { FileConflictAlert, type FileConflictAlertState } from "./conflict-alert";
import type { FileEditorStatus } from "./editor/model";
const ThemedSpinner = withUnistyles(LoadingSpinner);
const spinnerMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
export function FilePanelBar({
size,
lineCount,
mode,
onModeChange,
editorStatus,
cursor,
vimMode,
conflict,
}: {
size: number;
lineCount?: number;
mode?: "preview" | "source";
onModeChange?(mode: "preview" | "source"): void;
editorStatus?: FileEditorStatus;
cursor?: { line: number; column: number };
vimMode?: string | null;
conflict?: FileConflictAlertState;
}) {
const { t } = useTranslation();
const markdownModes = [
{
value: "preview" as const,
label: t("panels.file.editor.preview"),
testID: "file-mode-preview",
},
{ value: "source" as const, label: t("panels.file.editor.source"), testID: "file-mode-source" },
];
return (
<View style={styles.chrome} testID="file-panel-bar">
<View style={styles.row}>
<View style={styles.metadata}>
<Text
style={styles.whisper}
accessibilityLabel={t("panels.file.editor.fileSize", { size: formatFileSize(size) })}
>
{formatFileSize(size)}
</Text>
{lineCount !== undefined ? (
<Text
style={styles.whisper}
accessibilityLabel={t("panels.file.editor.lines", { count: lineCount })}
>
{t("panels.file.editor.lines", { count: lineCount })}
</Text>
) : null}
</View>
<View
style={styles.status}
accessibilityLabel={
editorStatus
? t("panels.file.editor.editorStatus", { status: editorStatus })
: undefined
}
>
{editorStatus === "dirty" ? (
<View
style={styles.dirtyDot}
accessibilityLabel={t("panels.file.editor.unsavedChanges")}
/>
) : null}
{editorStatus === "saving" ? (
<>
<ThemedSpinner size={14} uniProps={spinnerMapping} />
<Text style={styles.secondary}>{t("panels.file.editor.saving")}</Text>
</>
) : null}
{editorStatus === "error" ? (
<Text style={styles.error}>{t("panels.file.editor.saveFailed")}</Text>
) : null}
{vimMode ? (
<Text
style={styles.vim}
accessibilityLabel={t("panels.file.editor.vimMode", { mode: vimMode })}
>
{vimMode}
</Text>
) : null}
{cursor ? (
<Text
style={styles.whisper}
accessibilityLabel={t("panels.file.editor.cursor", cursor)}
>
Ln {cursor.line}, Col {cursor.column}
</Text>
) : null}
</View>
{mode && onModeChange ? (
<SegmentedControl
size="xs"
value={mode}
onValueChange={onModeChange}
testID="file-markdown-mode"
options={markdownModes}
/>
) : null}
</View>
{conflict ? <FileConflictAlert state={conflict} /> : null}
</View>
);
}
function formatFileSize(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`;
}
const styles = StyleSheet.create((theme) => ({
chrome: {
flexShrink: 0,
backgroundColor: theme.colors.surface1,
},
row: {
minHeight: 32,
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[3],
paddingHorizontal: theme.spacing[3],
borderBottomWidth: 1,
borderBottomColor: theme.colors.border,
},
metadata: {
flex: 1,
minWidth: 0,
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[2],
},
secondary: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.xs },
whisper: { color: theme.colors.foregroundExtraMuted, fontSize: theme.fontSize.xs },
error: { color: theme.colors.palette.red[300], fontSize: theme.fontSize.xs },
dirtyDot: {
width: 6,
height: 6,
borderRadius: theme.borderRadius.full,
backgroundColor: theme.colors.foregroundExtraMuted,
},
status: {
flexShrink: 0,
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[2],
},
vim: {
color: theme.colors.foregroundMuted,
fontFamily: theme.fontFamily.mono,
fontSize: theme.fontSize.xs,
},
}));