refactor(app): extract theme toggle to SegmentedControl component
This commit is contained in:
@@ -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<T extends string> = {
|
||||
value: T;
|
||||
label: string;
|
||||
icon?: SegmentedControlIconRenderer;
|
||||
disabled?: boolean;
|
||||
testID?: string;
|
||||
};
|
||||
|
||||
type SegmentedControlProps<T extends string> = {
|
||||
options: SegmentedControlOption<T>[];
|
||||
value: T;
|
||||
onValueChange: (value: T) => void;
|
||||
size?: SegmentedControlSize;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
testID?: string;
|
||||
};
|
||||
|
||||
export function SegmentedControl<T extends string>({
|
||||
options,
|
||||
value,
|
||||
onValueChange,
|
||||
size = "md",
|
||||
style,
|
||||
testID,
|
||||
}: SegmentedControlProps<T>) {
|
||||
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 (
|
||||
<View style={[styles.container, style]} testID={testID}>
|
||||
{options.map((option) => {
|
||||
const isSelected = option.value === value;
|
||||
const iconColor = isSelected ? theme.colors.foreground : theme.colors.foregroundMuted;
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
key={option.value}
|
||||
accessibilityRole="button"
|
||||
accessibilityState={{ selected: isSelected, disabled: option.disabled }}
|
||||
disabled={option.disabled}
|
||||
testID={option.testID}
|
||||
onPress={() => {
|
||||
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 ? (
|
||||
<View style={styles.iconContainer}>
|
||||
{option.icon({ color: iconColor, size: iconSize })}
|
||||
</View>
|
||||
) : null}
|
||||
<Text
|
||||
style={[styles.label, labelSizeStyle, isSelected && styles.labelSelected]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{option.label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
}));
|
||||
@@ -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() {
|
||||
<View style={styles.audioRowContent}>
|
||||
<Text style={styles.audioRowTitle}>Theme</Text>
|
||||
</View>
|
||||
<View style={styles.themeToggleContainer}>
|
||||
<Pressable
|
||||
style={[
|
||||
styles.themeToggleButton,
|
||||
settings.theme === "light" && styles.themeToggleButtonActive,
|
||||
]}
|
||||
onPress={() => handleThemeChange("light")}
|
||||
>
|
||||
<Sun size={14} color={settings.theme === "light" ? defaultTheme.colors.foreground : defaultTheme.colors.mutedForeground} />
|
||||
<Text style={[styles.themeToggleText, settings.theme === "light" && styles.themeToggleTextActive]}>
|
||||
Light
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={[
|
||||
styles.themeToggleButton,
|
||||
settings.theme === "dark" && styles.themeToggleButtonActive,
|
||||
]}
|
||||
onPress={() => handleThemeChange("dark")}
|
||||
>
|
||||
<Moon size={14} color={settings.theme === "dark" ? defaultTheme.colors.foreground : defaultTheme.colors.mutedForeground} />
|
||||
<Text style={[styles.themeToggleText, settings.theme === "dark" && styles.themeToggleTextActive]}>
|
||||
Dark
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={[
|
||||
styles.themeToggleButton,
|
||||
settings.theme === "auto" && styles.themeToggleButtonActive,
|
||||
]}
|
||||
onPress={() => handleThemeChange("auto")}
|
||||
>
|
||||
<Monitor size={14} color={settings.theme === "auto" ? defaultTheme.colors.foreground : defaultTheme.colors.mutedForeground} />
|
||||
<Text style={[styles.themeToggleText, settings.theme === "auto" && styles.themeToggleTextActive]}>
|
||||
System
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
<SegmentedControl
|
||||
size="sm"
|
||||
value={settings.theme}
|
||||
onValueChange={handleThemeChange}
|
||||
options={[
|
||||
{
|
||||
value: "light",
|
||||
label: "Light",
|
||||
icon: ({ color, size }) => <Sun size={size} color={color} />,
|
||||
},
|
||||
{
|
||||
value: "dark",
|
||||
label: "Dark",
|
||||
icon: ({ color, size }) => <Moon size={size} color={color} />,
|
||||
},
|
||||
{
|
||||
value: "auto",
|
||||
label: "System",
|
||||
icon: ({ color, size }) => <Monitor size={size} color={color} />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -1215,7 +1169,7 @@ function HostDetailModal({
|
||||
value={draftLabel}
|
||||
onChangeText={handleDraftLabelChange}
|
||||
placeholder="My Host"
|
||||
placeholderTextColor={defaultTheme.colors.mutedForeground}
|
||||
placeholderTextColor={theme.colors.foregroundMuted}
|
||||
/>
|
||||
</View>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user