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 { formatConnectionStatus, getConnectionStatusTone } from "@/utils/daemons";
|
||||||
import { measureConnectionLatency } from "@/utils/test-daemon-connection";
|
import { measureConnectionLatency } from "@/utils/test-daemon-connection";
|
||||||
import { confirmDialog } from "@/utils/confirm-dialog";
|
import { confirmDialog } from "@/utils/confirm-dialog";
|
||||||
import { theme as defaultTheme } from "@/styles/theme";
|
|
||||||
import { MenuHeader } from "@/components/headers/menu-header";
|
import { MenuHeader } from "@/components/headers/menu-header";
|
||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
import { AddHostMethodModal } from "@/components/add-host-method-modal";
|
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 { PairLinkModal } from "@/components/pair-link-modal";
|
||||||
import { NameHostModal } from "@/components/name-host-modal";
|
import { NameHostModal } from "@/components/name-host-modal";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { SegmentedControl } from "@/components/ui/segmented-control";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -374,36 +374,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
fontSize: theme.fontSize.sm,
|
fontSize: theme.fontSize.sm,
|
||||||
textAlign: "center",
|
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
|
// Dev section
|
||||||
devCard: {
|
devCard: {
|
||||||
backgroundColor: theme.colors.surface2,
|
backgroundColor: theme.colors.surface2,
|
||||||
@@ -881,44 +851,28 @@ export default function SettingsScreen() {
|
|||||||
<View style={styles.audioRowContent}>
|
<View style={styles.audioRowContent}>
|
||||||
<Text style={styles.audioRowTitle}>Theme</Text>
|
<Text style={styles.audioRowTitle}>Theme</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.themeToggleContainer}>
|
<SegmentedControl
|
||||||
<Pressable
|
size="sm"
|
||||||
style={[
|
value={settings.theme}
|
||||||
styles.themeToggleButton,
|
onValueChange={handleThemeChange}
|
||||||
settings.theme === "light" && styles.themeToggleButtonActive,
|
options={[
|
||||||
]}
|
{
|
||||||
onPress={() => handleThemeChange("light")}
|
value: "light",
|
||||||
>
|
label: "Light",
|
||||||
<Sun size={14} color={settings.theme === "light" ? defaultTheme.colors.foreground : defaultTheme.colors.mutedForeground} />
|
icon: ({ color, size }) => <Sun size={size} color={color} />,
|
||||||
<Text style={[styles.themeToggleText, settings.theme === "light" && styles.themeToggleTextActive]}>
|
},
|
||||||
Light
|
{
|
||||||
</Text>
|
value: "dark",
|
||||||
</Pressable>
|
label: "Dark",
|
||||||
<Pressable
|
icon: ({ color, size }) => <Moon size={size} color={color} />,
|
||||||
style={[
|
},
|
||||||
styles.themeToggleButton,
|
{
|
||||||
settings.theme === "dark" && styles.themeToggleButtonActive,
|
value: "auto",
|
||||||
]}
|
label: "System",
|
||||||
onPress={() => handleThemeChange("dark")}
|
icon: ({ color, size }) => <Monitor size={size} color={color} />,
|
||||||
>
|
},
|
||||||
<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>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -1215,7 +1169,7 @@ function HostDetailModal({
|
|||||||
value={draftLabel}
|
value={draftLabel}
|
||||||
onChangeText={handleDraftLabelChange}
|
onChangeText={handleDraftLabelChange}
|
||||||
placeholder="My Host"
|
placeholder="My Host"
|
||||||
placeholderTextColor={defaultTheme.colors.mutedForeground}
|
placeholderTextColor={theme.colors.foregroundMuted}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user