From bcae95c372daa3d001ed1fb6e9f8e9cece7b1bbb Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 18 Feb 2026 20:38:46 +0700 Subject: [PATCH] refactor(app): extract theme toggle to SegmentedControl component --- .../src/components/ui/segmented-control.tsx | 145 ++++++++++++++++++ packages/app/src/screens/settings-screen.tsx | 94 +++--------- 2 files changed, 169 insertions(+), 70 deletions(-) create mode 100644 packages/app/src/components/ui/segmented-control.tsx diff --git a/packages/app/src/components/ui/segmented-control.tsx b/packages/app/src/components/ui/segmented-control.tsx new file mode 100644 index 000000000..019bc4e3d --- /dev/null +++ b/packages/app/src/components/ui/segmented-control.tsx @@ -0,0 +1,145 @@ +import type { ReactNode } from "react"; +import { Pressable, Text, View } from "react-native"; +import type { StyleProp, ViewStyle } from "react-native"; +import { StyleSheet, useUnistyles } from "react-native-unistyles"; + +type SegmentedControlSize = "sm" | "md"; + +type SegmentedControlIconRenderer = (props: { + color: string; + size: number; +}) => ReactNode; + +export type SegmentedControlOption = { + value: T; + label: string; + icon?: SegmentedControlIconRenderer; + disabled?: boolean; + testID?: string; +}; + +type SegmentedControlProps = { + options: SegmentedControlOption[]; + value: T; + onValueChange: (value: T) => void; + size?: SegmentedControlSize; + style?: StyleProp; + testID?: string; +}; + +export function SegmentedControl({ + options, + value, + onValueChange, + size = "md", + style, + testID, +}: SegmentedControlProps) { + const { theme } = useUnistyles(); + const segmentSizeStyle = size === "sm" ? styles.segmentSm : styles.segmentMd; + const labelSizeStyle = size === "sm" ? styles.labelSm : styles.labelMd; + const iconSize = size === "sm" ? 14 : 16; + + return ( + + {options.map((option) => { + const isSelected = option.value === value; + const iconColor = isSelected ? theme.colors.foreground : theme.colors.foregroundMuted; + + return ( + { + if (!option.disabled && option.value !== value) { + onValueChange(option.value); + } + }} + style={({ hovered, pressed }) => [ + styles.segment, + segmentSizeStyle, + isSelected && styles.segmentSelected, + hovered && !isSelected && styles.segmentHover, + pressed && !isSelected && styles.segmentPressed, + option.disabled && styles.segmentDisabled, + ]} + > + {option.icon ? ( + + {option.icon({ color: iconColor, size: iconSize })} + + ) : null} + + {option.label} + + + ); + })} + + ); +} + +const styles = StyleSheet.create((theme) => ({ + container: { + flexDirection: "row", + alignItems: "center", + maxWidth: "100%", + gap: 4, + }, + segment: { + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + flexShrink: 0, + borderRadius: theme.borderRadius.full, + borderWidth: 1, + borderColor: "transparent", + gap: theme.spacing[1], + }, + segmentSm: { + paddingVertical: theme.spacing[1], + paddingHorizontal: theme.spacing[2], + }, + segmentMd: { + paddingVertical: theme.spacing[2], + paddingHorizontal: theme.spacing[3], + }, + segmentSelected: { + backgroundColor: theme.colors.surface3, + borderColor: theme.colors.border, + }, + segmentHover: { + backgroundColor: theme.colors.surface3, + borderColor: theme.colors.borderAccent, + }, + segmentPressed: { + backgroundColor: theme.colors.surface3, + borderColor: theme.colors.borderAccent, + }, + segmentDisabled: { + opacity: theme.opacity[50], + }, + iconContainer: { + alignItems: "center", + justifyContent: "center", + }, + label: { + color: theme.colors.foregroundMuted, + fontWeight: theme.fontWeight.normal, + }, + labelSm: { + fontSize: theme.fontSize.xs, + }, + labelMd: { + fontSize: theme.fontSize.sm, + }, + labelSelected: { + color: theme.colors.foreground, + }, +})); diff --git a/packages/app/src/screens/settings-screen.tsx b/packages/app/src/screens/settings-screen.tsx index 939ee4ba6..92c0cb955 100644 --- a/packages/app/src/screens/settings-screen.tsx +++ b/packages/app/src/screens/settings-screen.tsx @@ -20,7 +20,6 @@ import { useDaemonConnections, type ActiveConnection, type ConnectionStatus } fr import { formatConnectionStatus, getConnectionStatusTone } from "@/utils/daemons"; import { measureConnectionLatency } from "@/utils/test-daemon-connection"; import { confirmDialog } from "@/utils/confirm-dialog"; -import { theme as defaultTheme } from "@/styles/theme"; import { MenuHeader } from "@/components/headers/menu-header"; import { useSessionStore } from "@/stores/session-store"; import { AddHostMethodModal } from "@/components/add-host-method-modal"; @@ -28,6 +27,7 @@ import { AddHostModal } from "@/components/add-host-modal"; import { PairLinkModal } from "@/components/pair-link-modal"; import { NameHostModal } from "@/components/name-host-modal"; import { Button } from "@/components/ui/button"; +import { SegmentedControl } from "@/components/ui/segmented-control"; import { DropdownMenu, DropdownMenuContent, @@ -374,36 +374,6 @@ const styles = StyleSheet.create((theme) => ({ fontSize: theme.fontSize.sm, textAlign: "center", }, - // Theme toggle - themeToggleContainer: { - flexDirection: "row", - backgroundColor: theme.colors.surface3, - borderRadius: theme.borderRadius.md, - borderWidth: 1, - borderColor: theme.colors.border, - padding: 2, - gap: 2, - }, - themeToggleButton: { - flexDirection: "row", - alignItems: "center", - justifyContent: "center", - gap: theme.spacing[1], - paddingVertical: theme.spacing[2], - paddingHorizontal: theme.spacing[2], - borderRadius: theme.borderRadius.sm, - }, - themeToggleButtonActive: { - backgroundColor: theme.colors.surface2, - }, - themeToggleText: { - fontSize: theme.fontSize.xs, - fontWeight: theme.fontWeight.normal, - color: theme.colors.foregroundMuted, - }, - themeToggleTextActive: { - color: theme.colors.foreground, - }, // Dev section devCard: { backgroundColor: theme.colors.surface2, @@ -881,44 +851,28 @@ export default function SettingsScreen() { Theme - - handleThemeChange("light")} - > - - - Light - - - handleThemeChange("dark")} - > - - - Dark - - - handleThemeChange("auto")} - > - - - System - - - + , + }, + { + value: "dark", + label: "Dark", + icon: ({ color, size }) => , + }, + { + value: "auto", + label: "System", + icon: ({ color, size }) => , + }, + ]} + /> @@ -1215,7 +1169,7 @@ function HostDetailModal({ value={draftLabel} onChangeText={handleDraftLabelChange} placeholder="My Host" - placeholderTextColor={defaultTheme.colors.mutedForeground} + placeholderTextColor={theme.colors.foregroundMuted} />