refactor(app): extract the settings page layout into PageLayout

Desktop puts the title in the page above a centered column; compact moves it
into a back header. Settings renders the same pixels through it.
This commit is contained in:
Mohamed Boudra
2026-09-26 21:03:44 +02:00
parent 2b93fe9497
commit 49760a72da
3 changed files with 95 additions and 59 deletions
+1 -1
View File
@@ -74,7 +74,7 @@ export async function openSettingsHostSection(
}
export async function expectSettingsHeader(page: Page, title: string): Promise<void> {
await expect(page.getByTestId("settings-detail-header-title")).toHaveText(title);
await expect(page.getByTestId("page-title")).toHaveText(title);
}
export async function openAddHostFlow(page: Page): Promise<void> {
@@ -0,0 +1,83 @@
import { useMemo, type ReactNode } from "react";
import { ScrollView, Text, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { StyleSheet } from "react-native-unistyles";
import { BackHeader } from "@/components/headers/back-header";
import { MenuHeader } from "@/components/headers/menu-header";
import { useIsCompactFormFactor } from "@/constants/layout";
interface PageLayoutProps {
title?: string;
/** Sits at the end of the title row on desktop and in the header on compact. */
actions?: ReactNode;
/** Compact back button. Defaults to navigating back. */
onBack?: () => void;
testID?: string;
children: ReactNode;
}
/**
* A full-width page with a centered content column. On desktop the title is a
* document heading inside the page and the header only keeps the titlebar drag
* region and window controls; on compact the title moves into a back header.
*/
export function PageLayout({ title, actions, onBack, testID, children }: PageLayoutProps) {
const isCompact = useIsCompactFormFactor();
const insets = useSafeAreaInsets();
const scrollContentStyle = useMemo(() => ({ paddingBottom: insets.bottom }), [insets.bottom]);
const showTitleRow = !isCompact && (title !== undefined || actions !== undefined);
return (
<View style={styles.container}>
{isCompact ? (
<BackHeader title={title} rightContent={actions} onBack={onBack} />
) : (
<MenuHeader borderless />
)}
<ScrollView style={styles.scroll} contentContainerStyle={scrollContentStyle} testID={testID}>
<View style={styles.content}>
{showTitleRow ? (
<View style={styles.titleRow}>
<Text style={styles.title} testID="page-title">
{title}
</Text>
{actions}
</View>
) : null}
{children}
</View>
</ScrollView>
</View>
);
}
const styles = StyleSheet.create((theme) => ({
container: {
flex: 1,
backgroundColor: theme.colors.surface0,
},
scroll: {
flex: 1,
},
content: {
padding: theme.spacing[4],
paddingTop: theme.spacing[6],
width: "100%",
maxWidth: 720,
alignSelf: "center",
},
titleRow: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[2],
// Line up with the section titles, which sit inset from their cards.
marginLeft: theme.spacing[1],
marginBottom: theme.spacing[6],
},
title: {
flex: 1,
fontSize: theme.fontSize["4xl"],
fontWeight: theme.fontWeight.medium,
color: theme.colors.foreground,
},
}));
+11 -58
View File
@@ -71,7 +71,7 @@ import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region";
import { WindowChromeRegion, WindowChromeSafeArea } from "@/utils/desktop-window";
import { confirmDialog } from "@/utils/confirm-dialog";
import { BackHeader } from "@/components/headers/back-header";
import { ScreenHeader } from "@/components/headers/screen-header";
import { PageLayout } from "@/components/page-layout";
import { AddHostMethodModal } from "@/components/add-host-method-modal";
import { AddHostModal } from "@/components/add-host-modal";
import { AddRemoteSshHostModal } from "@/components/add-remote-ssh-host-modal";
@@ -1305,30 +1305,25 @@ export default function SettingsScreen({ view, openAddHostIntent = null }: Setti
}, []);
const installedPlugins = useInstalledPlugins();
const detailHeader = ((): {
title: string;
titleAccessory?: ReactNode;
} | null => {
const detailTitle = ((): string | undefined => {
if (view.kind === "plugin") {
const screen = installedPlugins
.find((plugin) => plugin.serverId === view.serverId && plugin.id === view.pluginId)
?.settingsScreens.find((candidate) => candidate.id === view.screenId);
return { title: `${view.pluginId} · ${screen?.title ?? t("settings.title")}` };
return `${view.pluginId} · ${screen?.title ?? t("settings.title")}`;
}
if (view.kind === "host") {
const item = HOST_SECTION_ITEMS.find((s) => s.id === view.section);
if (!item) return null;
return { title: t(item.labelKey) };
return item ? t(item.labelKey) : undefined;
}
if (view.kind === "section") {
const item = SIDEBAR_SECTION_ITEMS.find((s) => s.id === view.section);
if (!item) return null;
return { title: t(item.labelKey) };
return item ? t(item.labelKey) : undefined;
}
if (view.kind === "project") {
return { title: t("settings.projects") };
return t("settings.projects");
}
return null;
return undefined;
})();
const content: ReactNode = (() => {
@@ -1399,15 +1394,6 @@ export default function SettingsScreen({ view, openAddHostIntent = null }: Setti
);
}
const desktopPageTitle = detailHeader ? (
<View style={styles.pageTitleRow}>
<Text style={styles.pageTitle} testID="settings-detail-header-title">
{detailHeader.title}
</Text>
{detailHeader.titleAccessory}
</View>
) : null;
const addHostModals = (
<>
<AddHostMethodModal
@@ -1464,14 +1450,9 @@ export default function SettingsScreen({ view, openAddHostIntent = null }: Setti
if (isCompactLayout) {
return (
<View style={styles.container}>
<BackHeader
title={detailHeader?.title}
titleAccessory={detailHeader?.titleAccessory}
onBack={handleBackFromDetail}
/>
<ScrollView style={styles.scrollView} contentContainerStyle={insetBottomStyle}>
<View style={styles.content}>{content}</View>
</ScrollView>
<PageLayout title={detailTitle} onBack={handleBackFromDetail}>
{content}
</PageLayout>
{addHostModals}
</View>
);
@@ -1497,15 +1478,7 @@ export default function SettingsScreen({ view, openAddHostIntent = null }: Setti
</WindowChromeRegion>
<WindowChromeRegion corners="top-right">
<View style={desktopStyles.contentPane} testID="settings-detail-pane">
{/* Keeps the titlebar drag region and window controls; the page
title lives in the content, like a document heading. */}
<ScreenHeader borderless />
<ScrollView style={styles.scrollView} contentContainerStyle={insetBottomStyle}>
<View style={styles.content}>
{desktopPageTitle}
{content}
</View>
</ScrollView>
<PageLayout title={detailTitle}>{content}</PageLayout>
</View>
</WindowChromeRegion>
</View>
@@ -1536,26 +1509,6 @@ const styles = StyleSheet.create((theme) => ({
scrollView: {
flex: 1,
},
content: {
padding: theme.spacing[4],
paddingTop: theme.spacing[6],
width: "100%",
maxWidth: 720,
alignSelf: "center",
},
pageTitleRow: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[2],
// Line up with the section titles, which sit inset from their cards.
marginLeft: theme.spacing[1],
marginBottom: theme.spacing[6],
},
pageTitle: {
fontSize: theme.fontSize["4xl"],
fontWeight: theme.fontWeight.medium,
color: theme.colors.foreground,
},
aboutValue: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.base,