From 49760a72daba21b83900bb7053ddd319ae353441 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 26 Sep 2026 21:03:44 +0200 Subject: [PATCH] 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. --- packages/app/e2e/support/helpers/settings.ts | 2 +- .../app/src/components/page-layout/index.tsx | 83 +++++++++++++++++++ packages/app/src/screens/settings-screen.tsx | 69 +++------------ 3 files changed, 95 insertions(+), 59 deletions(-) create mode 100644 packages/app/src/components/page-layout/index.tsx diff --git a/packages/app/e2e/support/helpers/settings.ts b/packages/app/e2e/support/helpers/settings.ts index ecfa2a4b09..1702f7fe44 100644 --- a/packages/app/e2e/support/helpers/settings.ts +++ b/packages/app/e2e/support/helpers/settings.ts @@ -74,7 +74,7 @@ export async function openSettingsHostSection( } export async function expectSettingsHeader(page: Page, title: string): Promise { - 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 { diff --git a/packages/app/src/components/page-layout/index.tsx b/packages/app/src/components/page-layout/index.tsx new file mode 100644 index 0000000000..edecda1ff8 --- /dev/null +++ b/packages/app/src/components/page-layout/index.tsx @@ -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 ( + + {isCompact ? ( + + ) : ( + + )} + + + {showTitleRow ? ( + + + {title} + + {actions} + + ) : null} + {children} + + + + ); +} + +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, + }, +})); diff --git a/packages/app/src/screens/settings-screen.tsx b/packages/app/src/screens/settings-screen.tsx index 913be879b2..0804ef5132 100644 --- a/packages/app/src/screens/settings-screen.tsx +++ b/packages/app/src/screens/settings-screen.tsx @@ -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 ? ( - - - {detailHeader.title} - - {detailHeader.titleAccessory} - - ) : null; - const addHostModals = ( <> - - - {content} - + + {content} + {addHostModals} ); @@ -1497,15 +1478,7 @@ export default function SettingsScreen({ view, openAddHostIntent = null }: Setti - {/* Keeps the titlebar drag region and window controls; the page - title lives in the content, like a document heading. */} - - - - {desktopPageTitle} - {content} - - + {content} @@ -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,