refactor: extract settings styles and improve badge/button UI

This commit is contained in:
Mohamed Boudra
2026-03-10 12:17:37 +07:00
parent f760255d50
commit 965704da20
7 changed files with 280 additions and 273 deletions
+210 -148
View File
@@ -646,6 +646,9 @@ const expandableBadgeStylesheet = StyleSheet.create((theme) => ({
marginLeft: theme.spacing[1], marginLeft: theme.spacing[1],
flexShrink: 0, flexShrink: 0,
}, },
chevronExpanded: {
transform: [{ rotate: "90deg" }],
},
detailWrapper: { detailWrapper: {
borderBottomLeftRadius: theme.borderRadius.lg, borderBottomLeftRadius: theme.borderRadius.lg,
borderBottomRightRadius: theme.borderRadius.lg, borderBottomRightRadius: theme.borderRadius.lg,
@@ -1289,6 +1292,8 @@ const ExpandableBadge = memo(function ExpandableBadge({
const { theme } = useUnistyles(); const { theme } = useUnistyles();
const resolvedDisableOuterSpacing = const resolvedDisableOuterSpacing =
useDisableOuterSpacing(disableOuterSpacing); useDisableOuterSpacing(disableOuterSpacing);
const [isHovered, setIsHovered] = useState(false);
const [isPressed, setIsPressed] = useState(false);
const isInteractive = Boolean(onToggle); const isInteractive = Boolean(onToggle);
const hasDetailContent = Boolean(renderDetails); const hasDetailContent = Boolean(renderDetails);
const detailContent = const detailContent =
@@ -1479,6 +1484,107 @@ const ExpandableBadge = memo(function ExpandableBadge({
} as never) } as never)
: null; : null;
const containerStyle = useMemo(
() => [
expandableBadgeStylesheet.container,
!resolvedDisableOuterSpacing &&
(isLastInSequence
? expandableBadgeStylesheet.containerLastInSequence
: expandableBadgeStylesheet.containerSpacing),
style,
],
[isLastInSequence, resolvedDisableOuterSpacing, style]
);
const pressableStyle = useMemo(
() => [
expandableBadgeStylesheet.pressable,
isPressed && isInteractive
? expandableBadgeStylesheet.pressablePressed
: null,
isExpanded && expandableBadgeStylesheet.pressableExpanded,
],
[isExpanded, isInteractive, isPressed]
);
const accessibilityState = useMemo(
() => (isInteractive ? { expanded: isExpanded } : undefined),
[isExpanded, isInteractive]
);
const labelStyle = useMemo(
() => [
expandableBadgeStylesheet.label,
isLoading && expandableBadgeStylesheet.labelLoading,
],
[isLoading]
);
const shimmerLabelTextStyle = useMemo(
() => [
expandableBadgeStylesheet.label,
isLoading && expandableBadgeStylesheet.labelLoading,
expandableBadgeStylesheet.shimmerText,
shimmerLabelStyle,
],
[isLoading, shimmerLabelStyle]
);
const shimmerSecondaryTextStyle = useMemo(
() => [
expandableBadgeStylesheet.secondaryLabel,
expandableBadgeStylesheet.shimmerText,
shimmerSecondaryStyle,
],
[shimmerSecondaryStyle]
);
const nativeShimmerTrackStyle = useMemo(
() => [
expandableBadgeStylesheet.nativeShimmerTrack,
{ width: labelRowWidth, height: labelRowHeight },
],
[labelRowHeight, labelRowWidth]
);
const nativeShimmerMaskStyle = useMemo(
() => [
expandableBadgeStylesheet.shimmerMaskRow,
{ width: labelRowWidth, height: labelRowHeight },
],
[labelRowHeight, labelRowWidth]
);
const nativeLabelMaskStyle = useMemo(
() => [expandableBadgeStylesheet.label, { color: "#000000", opacity: 1 }],
[]
);
const nativeSecondaryMaskStyle = useMemo(
() => [
expandableBadgeStylesheet.secondaryLabel,
{ color: "#000000", opacity: 1 },
],
[]
);
const nativeShimmerPeakCombinedStyle = useMemo(
() => [
expandableBadgeStylesheet.nativeShimmerPeak,
nativeShimmerPeakStyle,
{ width: nativeShimmerPeakWidth, height: labelRowHeight },
],
[labelRowHeight, nativeShimmerPeakStyle, nativeShimmerPeakWidth]
);
const chevronStyle = useMemo(
() => [
expandableBadgeStylesheet.chevron,
isExpanded && expandableBadgeStylesheet.chevronExpanded,
],
[isExpanded]
);
const IconComponent = icon; const IconComponent = icon;
const iconColor = isError const iconColor = isError
? theme.colors.destructive ? theme.colors.destructive
@@ -1493,186 +1599,142 @@ const ExpandableBadge = memo(function ExpandableBadge({
return ( return (
<View <View
style={[ style={containerStyle}
expandableBadgeStylesheet.container,
!resolvedDisableOuterSpacing &&
(isLastInSequence
? expandableBadgeStylesheet.containerLastInSequence
: expandableBadgeStylesheet.containerSpacing),
style,
]}
testID={testID} testID={testID}
> >
<Pressable <Pressable
onPress={isInteractive ? onToggle : undefined} onPress={isInteractive ? onToggle : undefined}
onHoverIn={isInteractive ? () => setIsHovered(true) : undefined}
onHoverOut={
isInteractive
? () => {
setIsHovered(false);
setIsPressed(false);
}
: undefined
}
onPressIn={isInteractive ? () => setIsPressed(true) : undefined}
onPressOut={isInteractive ? () => setIsPressed(false) : undefined}
disabled={!isInteractive} disabled={!isInteractive}
accessibilityRole={isInteractive ? "button" : undefined} accessibilityRole={isInteractive ? "button" : undefined}
accessibilityState={isInteractive ? { expanded: isExpanded } : undefined} accessibilityState={accessibilityState}
style={({ pressed }) => [ style={pressableStyle}
expandableBadgeStylesheet.pressable,
pressed && isInteractive
? expandableBadgeStylesheet.pressablePressed
: null,
isExpanded && expandableBadgeStylesheet.pressableExpanded,
]}
> >
{({ hovered }) => ( <View style={expandableBadgeStylesheet.headerRow}>
<> <View style={expandableBadgeStylesheet.iconBadge}>{iconNode}</View>
<View style={expandableBadgeStylesheet.headerRow}> <View
<View style={expandableBadgeStylesheet.iconBadge}>{iconNode}</View> style={expandableBadgeStylesheet.labelRow}
onLayout={shouldMeasureNativeShimmer ? handleLabelRowLayout : undefined}
>
<Text
style={labelStyle}
numberOfLines={1}
onLayout={shouldMeasureWebShimmer ? handleLabelLayout : undefined}
>
{label}
</Text>
{secondaryLabel ? (
<Text
style={expandableBadgeStylesheet.secondaryLabel}
numberOfLines={1}
onLayout={shouldMeasureWebShimmer ? handleSecondaryLayout : undefined}
>
{secondaryLabel}
</Text>
) : (
<View style={expandableBadgeStylesheet.spacer} />
)}
{isWebShimmer ? (
<View <View
style={expandableBadgeStylesheet.labelRow} style={expandableBadgeStylesheet.shimmerOverlay}
onLayout={shouldMeasureNativeShimmer ? handleLabelRowLayout : undefined} pointerEvents="none"
> >
<Text <Text
style={[ style={shimmerLabelTextStyle}
expandableBadgeStylesheet.label,
isLoading && expandableBadgeStylesheet.labelLoading,
]}
numberOfLines={1} numberOfLines={1}
onLayout={shouldMeasureWebShimmer ? handleLabelLayout : undefined}
> >
{label} {label}
</Text> </Text>
{secondaryLabel ? ( {secondaryLabel ? (
<Text <Text
style={expandableBadgeStylesheet.secondaryLabel} style={shimmerSecondaryTextStyle}
numberOfLines={1} numberOfLines={1}
onLayout={shouldMeasureWebShimmer ? handleSecondaryLayout : undefined}
> >
{secondaryLabel} {secondaryLabel}
</Text> </Text>
) : ( ) : (
<View style={expandableBadgeStylesheet.spacer} /> <View style={expandableBadgeStylesheet.spacer} />
)} )}
{isWebShimmer ? ( </View>
<View ) : null}
style={expandableBadgeStylesheet.shimmerOverlay} {isNativeShimmer ? (
pointerEvents="none" <View
> style={expandableBadgeStylesheet.shimmerOverlay}
<Text pointerEvents="none"
style={[ >
expandableBadgeStylesheet.label, <MaskedView
isLoading && expandableBadgeStylesheet.labelLoading, style={nativeShimmerTrackStyle}
expandableBadgeStylesheet.shimmerText, maskElement={
shimmerLabelStyle, <View style={nativeShimmerMaskStyle}>
]}
numberOfLines={1}
>
{label}
</Text>
{secondaryLabel ? (
<Text <Text
style={[ style={nativeLabelMaskStyle}
expandableBadgeStylesheet.secondaryLabel,
expandableBadgeStylesheet.shimmerText,
shimmerSecondaryStyle,
]}
numberOfLines={1} numberOfLines={1}
> >
{secondaryLabel} {label}
</Text> </Text>
) : ( {secondaryLabel ? (
<View style={expandableBadgeStylesheet.spacer} /> <Text
)} style={nativeSecondaryMaskStyle}
</View> numberOfLines={1}
) : null} >
{isNativeShimmer ? ( {secondaryLabel}
</Text>
) : (
<View style={expandableBadgeStylesheet.spacer} />
)}
</View>
}
>
<View <View
style={expandableBadgeStylesheet.shimmerOverlay} style={nativeShimmerTrackStyle}
pointerEvents="none"
> >
<MaskedView <Animated.View style={nativeShimmerPeakCombinedStyle}>
style={[ <Svg width="100%" height="100%" preserveAspectRatio="none">
expandableBadgeStylesheet.nativeShimmerTrack, <Defs>
{ width: labelRowWidth, height: labelRowHeight }, <SvgLinearGradient
]} id={nativeGradientIdRef.current}
maskElement={ x1="0%"
<View y1="0%"
style={[ x2="100%"
expandableBadgeStylesheet.shimmerMaskRow, y2="0%"
{ width: labelRowWidth, height: labelRowHeight },
]}
>
<Text
style={[
expandableBadgeStylesheet.label,
{ color: "#000000", opacity: 1 },
]}
numberOfLines={1}
> >
{label} <Stop offset="0%" stopColor="#ffffff" stopOpacity={0} />
</Text> <Stop offset="50%" stopColor="#ffffff" stopOpacity={1} />
{secondaryLabel ? ( <Stop offset="100%" stopColor="#ffffff" stopOpacity={0} />
<Text </SvgLinearGradient>
style={[ </Defs>
expandableBadgeStylesheet.secondaryLabel, <Rect
{ color: "#000000", opacity: 1 }, x="0"
]} y="0"
numberOfLines={1} width="100%"
> height="100%"
{secondaryLabel} fill={`url(#${nativeGradientIdRef.current})`}
</Text> />
) : ( </Svg>
<View style={expandableBadgeStylesheet.spacer} /> </Animated.View>
)}
</View>
}
>
<View
style={[
expandableBadgeStylesheet.nativeShimmerTrack,
{ width: labelRowWidth, height: labelRowHeight },
]}
>
<Animated.View
style={[
expandableBadgeStylesheet.nativeShimmerPeak,
nativeShimmerPeakStyle,
{ width: nativeShimmerPeakWidth, height: labelRowHeight },
]}
>
<Svg width="100%" height="100%" preserveAspectRatio="none">
<Defs>
<SvgLinearGradient
id={nativeGradientIdRef.current}
x1="0%"
y1="0%"
x2="100%"
y2="0%"
>
<Stop offset="0%" stopColor="#ffffff" stopOpacity={0} />
<Stop offset="50%" stopColor="#ffffff" stopOpacity={1} />
<Stop offset="100%" stopColor="#ffffff" stopOpacity={0} />
</SvgLinearGradient>
</Defs>
<Rect
x="0"
y="0"
width="100%"
height="100%"
fill={`url(#${nativeGradientIdRef.current})`}
/>
</Svg>
</Animated.View>
</View>
</MaskedView>
</View> </View>
) : null} </MaskedView>
</View> </View>
{isInteractive && hovered ? ( ) : null}
<ChevronRight </View>
size={14} {isInteractive && isHovered ? (
color={theme.colors.foregroundMuted} <ChevronRight
style={[ size={14}
expandableBadgeStylesheet.chevron, color={theme.colors.foregroundMuted}
{ transform: [{ rotate: isExpanded ? "90deg" : "0deg" }] }, style={chevronStyle}
]} />
/> ) : null}
) : null} </View>
</View>
</>
)}
</Pressable> </Pressable>
{detailContent ? ( {detailContent ? (
<Pressable <Pressable
+2 -2
View File
@@ -40,7 +40,7 @@ const styles = StyleSheet.create((theme) => ({
}, },
outline: { outline: {
backgroundColor: "transparent", backgroundColor: "transparent",
borderColor: theme.colors.border, borderColor: theme.colors.borderAccent,
}, },
ghost: { ghost: {
backgroundColor: "transparent", backgroundColor: "transparent",
@@ -59,7 +59,7 @@ const styles = StyleSheet.create((theme) => ({
text: { text: {
color: theme.colors.foreground, color: theme.colors.foreground,
fontSize: theme.fontSize.sm, fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.medium, fontWeight: theme.fontWeight.normal,
}, },
textDefault: { textDefault: {
color: theme.colors.palette.white, color: theme.colors.palette.white,
@@ -41,7 +41,7 @@ export function DesktopPermissionRow({
<Text style={styles.permissionStatusText}>Granted</Text> <Text style={styles.permissionStatusText}>Granted</Text>
</View> </View>
) : ( ) : (
<Button variant="secondary" size="sm" onPress={onRequest} disabled={isRequesting}> <Button variant="outline" size="sm" onPress={onRequest} disabled={isRequesting}>
{isRequesting ? "Requesting..." : "Request"} {isRequesting ? "Requesting..." : "Request"}
</Button> </Button>
)} )}
@@ -1,8 +1,10 @@
import { View, Text, Pressable } from "react-native"; import { View, Text } from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { RotateCw } from "lucide-react-native"; import { RotateCw } from "lucide-react-native";
import { Button } from "@/components/ui/button";
import { DesktopPermissionRow } from "@/desktop/components/desktop-permission-row"; import { DesktopPermissionRow } from "@/desktop/components/desktop-permission-row";
import { useDesktopPermissions } from "@/desktop/permissions/use-desktop-permissions"; import { useDesktopPermissions } from "@/desktop/permissions/use-desktop-permissions";
import { settingsStyles } from "@/styles/settings";
export function DesktopPermissionsSection() { export function DesktopPermissionsSection() {
const { theme } = useUnistyles(); const { theme } = useUnistyles();
@@ -22,26 +24,23 @@ export function DesktopPermissionsSection() {
const isBusy = isRefreshing || requestingPermission !== null; const isBusy = isRefreshing || requestingPermission !== null;
return ( return (
<View style={styles.section}> <View style={settingsStyles.section}>
<View style={styles.permissionSectionHeader}> <View style={styles.permissionSectionHeader}>
<Text style={styles.sectionTitle}>Desktop Permissions</Text> <Text style={settingsStyles.sectionTitle}>Desktop Permissions</Text>
<Pressable <Button
style={({ pressed }) => [ variant="ghost"
styles.permissionRefreshButton, size="sm"
isBusy && styles.permissionRefreshButtonDisabled, leftIcon={<RotateCw size={theme.iconSize.md} color={theme.colors.foregroundMuted} />}
pressed && { opacity: 0.85 },
]}
onPress={() => { onPress={() => {
void refreshPermissions(); void refreshPermissions();
}} }}
disabled={isBusy} disabled={isBusy}
accessibilityRole="button"
accessibilityLabel="Refresh desktop permissions" accessibilityLabel="Refresh desktop permissions"
> >
<RotateCw size={theme.iconSize.md} color={theme.colors.foregroundMuted} /> {isRefreshing ? "Refreshing..." : "Refresh"}
</Pressable> </Button>
</View> </View>
<View style={styles.audioCard}> <View style={settingsStyles.card}>
<DesktopPermissionRow <DesktopPermissionRow
title="Notifications" title="Notifications"
status={snapshot?.notifications ?? null} status={snapshot?.notifications ?? null}
@@ -65,16 +64,6 @@ export function DesktopPermissionsSection() {
} }
const styles = StyleSheet.create((theme) => ({ const styles = StyleSheet.create((theme) => ({
section: {
marginBottom: theme.spacing[6],
},
sectionTitle: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.normal,
marginBottom: 0,
marginLeft: theme.spacing[1],
},
permissionSectionHeader: { permissionSectionHeader: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
@@ -82,24 +71,4 @@ const styles = StyleSheet.create((theme) => ({
gap: theme.spacing[2], gap: theme.spacing[2],
marginBottom: theme.spacing[3], marginBottom: theme.spacing[3],
}, },
permissionRefreshButton: {
width: 34,
height: 34,
borderRadius: theme.borderRadius.md,
borderWidth: 1,
borderColor: theme.colors.border,
backgroundColor: theme.colors.surface2,
alignItems: "center",
justifyContent: "center",
},
permissionRefreshButtonDisabled: {
opacity: theme.opacity[50],
},
audioCard: {
backgroundColor: theme.colors.surface2,
borderRadius: theme.borderRadius.lg,
borderWidth: 1,
borderColor: theme.colors.border,
overflow: "hidden",
},
})); }));
@@ -3,8 +3,9 @@ import { ActivityIndicator, Alert, Image, Pressable, Text, View } from "react-na
import * as Clipboard from "expo-clipboard"; import * as Clipboard from "expo-clipboard";
import * as QRCode from "qrcode"; import * as QRCode from "qrcode";
import { useFocusEffect } from "@react-navigation/native"; import { useFocusEffect } from "@react-navigation/native";
import { StyleSheet } from "react-native-unistyles"; import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { ArrowUpRight } from "lucide-react-native"; import { settingsStyles } from "@/styles/settings";
import { ArrowUpRight, Play, Pause, RotateCw, Terminal, Copy, FileText, Smartphone } from "lucide-react-native";
import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet"; import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useAppSettings } from "@/hooks/use-settings"; import { useAppSettings } from "@/hooks/use-settings";
@@ -35,6 +36,7 @@ export interface LocalDaemonSectionProps {
} }
export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) { export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) {
const { theme } = useUnistyles();
const showSection = shouldUseManagedDesktopDaemon(); const showSection = shouldUseManagedDesktopDaemon();
const { settings, updateSettings } = useAppSettings(); const { settings, updateSettings } = useAppSettings();
const [managedStatus, setManagedStatus] = useState<ManagedDaemonStatus | null>(null); const [managedStatus, setManagedStatus] = useState<ManagedDaemonStatus | null>(null);
@@ -331,9 +333,9 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) {
} }
return ( return (
<View style={styles.section}> <View style={settingsStyles.section}>
<View style={styles.sectionHeader}> <View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}>Built-in daemon</Text> <Text style={settingsStyles.sectionTitle}>Built-in daemon</Text>
<Pressable <Pressable
accessibilityRole="link" accessibilityRole="link"
onPress={() => void openExternalUrl(ADVANCED_DAEMON_SETTINGS_URL)} onPress={() => void openExternalUrl(ADVANCED_DAEMON_SETTINGS_URL)}
@@ -343,7 +345,7 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) {
<ArrowUpRight size={14} color={styles.sectionLinkText.color} /> <ArrowUpRight size={14} color={styles.sectionLinkText.color} />
</Pressable> </Pressable>
</View> </View>
<View style={styles.card}> <View style={settingsStyles.card}>
<View style={styles.row}> <View style={styles.row}>
<View style={styles.rowContent}> <View style={styles.rowContent}>
<Text style={styles.rowTitle}>Status</Text> <Text style={styles.rowTitle}>Status</Text>
@@ -364,9 +366,11 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) {
</Text> </Text>
</View> </View>
<Button <Button
variant="secondary" variant="outline"
size="sm" size="sm"
style={styles.primaryActionButton} leftIcon={isDaemonManagementPaused
? <Play size={theme.iconSize.sm} color={theme.colors.foreground} />
: <Pause size={theme.iconSize.sm} color={theme.colors.foreground} />}
onPress={handleToggleDaemonManagement} onPress={handleToggleDaemonManagement}
disabled={isUpdatingDaemonManagement} disabled={isUpdatingDaemonManagement}
> >
@@ -388,9 +392,9 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) {
) : null} ) : null}
</View> </View>
<Button <Button
variant="secondary" variant="outline"
size="sm" size="sm"
style={styles.primaryActionButton} leftIcon={<RotateCw size={theme.iconSize.sm} color={theme.colors.foreground} />}
onPress={handleUpdateLocalDaemon} onPress={handleUpdateLocalDaemon}
disabled={isRestartingDaemon} disabled={isRestartingDaemon}
> >
@@ -410,9 +414,9 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) {
{cliStatusMessage ? <Text style={styles.statusText}>{cliStatusMessage}</Text> : null} {cliStatusMessage ? <Text style={styles.statusText}>{cliStatusMessage}</Text> : null}
</View> </View>
<Button <Button
variant="secondary" variant="outline"
size="sm" size="sm"
style={styles.secondaryActionButton} leftIcon={<Terminal size={theme.iconSize.sm} color={theme.colors.foreground} />}
onPress={handleToggleCliShim} onPress={handleToggleCliShim}
disabled={isInstallingCli} disabled={isInstallingCli}
> >
@@ -434,13 +438,14 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) {
</View> </View>
<View style={styles.actionGroup}> <View style={styles.actionGroup}>
{(managedLogs?.logPath ?? managedStatus?.logPath) ? ( {(managedLogs?.logPath ?? managedStatus?.logPath) ? (
<Button variant="secondary" size="sm" onPress={handleCopyLogPath}> <Button variant="outline" size="sm" leftIcon={<Copy size={theme.iconSize.sm} color={theme.colors.foreground} />} onPress={handleCopyLogPath}>
Copy path Copy path
</Button> </Button>
) : null} ) : null}
<Button <Button
variant="secondary" variant="outline"
size="sm" size="sm"
leftIcon={<FileText size={theme.iconSize.sm} color={theme.colors.foreground} />}
onPress={handleOpenLogs} onPress={handleOpenLogs}
disabled={!managedLogs} disabled={!managedLogs}
> >
@@ -455,7 +460,7 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) {
Connect your phone to this computer. Connect your phone to this computer.
</Text> </Text>
</View> </View>
<Button variant="secondary" size="sm" style={styles.secondaryActionButton} onPress={handleOpenPairingModal}> <Button variant="outline" size="sm" leftIcon={<Smartphone size={theme.iconSize.sm} color={theme.colors.foreground} />} onPress={handleOpenPairingModal}>
Pair device Pair device
</Button> </Button>
</View> </View>
@@ -488,7 +493,7 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) {
{cliInstallInstructions?.commands ?? ""} {cliInstallInstructions?.commands ?? ""}
</Text> </Text>
<View style={styles.modalActions}> <View style={styles.modalActions}>
<Button variant="secondary" size="sm" onPress={() => setIsCliInstallModalOpen(false)}> <Button variant="outline" size="sm" onPress={() => setIsCliInstallModalOpen(false)}>
Close Close
</Button> </Button>
<Button size="sm" onPress={handleCopyCliInstallCommands}> <Button size="sm" onPress={handleCopyCliInstallCommands}>
@@ -627,7 +632,7 @@ function PairingOfferDialogContent(input: {
{pairingOffer.url} {pairingOffer.url}
</Text> </Text>
<View style={styles.modalActions}> <View style={styles.modalActions}>
<Button variant="secondary" size="sm" onPress={onCopyLink}> <Button variant="outline" size="sm" onPress={onCopyLink}>
Copy link Copy link
</Button> </Button>
</View> </View>
@@ -636,9 +641,6 @@ function PairingOfferDialogContent(input: {
} }
const styles = StyleSheet.create((theme) => ({ const styles = StyleSheet.create((theme) => ({
section: {
marginBottom: theme.spacing[6],
},
sectionHeader: { sectionHeader: {
alignItems: "center", alignItems: "center",
flexDirection: "row", flexDirection: "row",
@@ -646,11 +648,6 @@ const styles = StyleSheet.create((theme) => ({
marginBottom: theme.spacing[3], marginBottom: theme.spacing[3],
marginLeft: theme.spacing[1], marginLeft: theme.spacing[1],
}, },
sectionTitle: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.normal,
},
sectionLink: { sectionLink: {
alignItems: "center", alignItems: "center",
flexDirection: "row", flexDirection: "row",
@@ -660,13 +657,6 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.foregroundMuted, color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs, fontSize: theme.fontSize.xs,
}, },
card: {
backgroundColor: theme.colors.surface2,
borderRadius: theme.borderRadius.lg,
borderWidth: 1,
borderColor: theme.colors.border,
overflow: "hidden",
},
row: { row: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
@@ -692,12 +682,6 @@ const styles = StyleSheet.create((theme) => ({
alignItems: "flex-end", alignItems: "flex-end",
gap: 2, gap: 2,
}, },
primaryActionButton: {
minWidth: 124,
},
secondaryActionButton: {
minWidth: 112,
},
rowTitle: { rowTitle: {
color: theme.colors.foreground, color: theme.colors.foreground,
fontSize: theme.fontSize.base, fontSize: theme.fontSize.base,
+13 -42
View File
@@ -42,6 +42,7 @@ import { LocalDaemonSection } from "@/desktop/components/desktop-updates-section
import { useDesktopAppUpdater } from "@/desktop/updates/use-desktop-app-updater"; import { useDesktopAppUpdater } from "@/desktop/updates/use-desktop-app-updater";
import { formatVersionWithPrefix } from "@/desktop/updates/desktop-updates"; import { formatVersionWithPrefix } from "@/desktop/updates/desktop-updates";
import { resolveAppVersion } from "@/utils/app-version"; import { resolveAppVersion } from "@/utils/app-version";
import { settingsStyles } from "@/styles/settings";
const delay = (ms: number) => const delay = (ms: number) =>
new Promise<void>((resolve) => { new Promise<void>((resolve) => {
@@ -112,16 +113,6 @@ const styles = StyleSheet.create((theme) => ({
maxWidth: 720, maxWidth: 720,
alignSelf: "center", alignSelf: "center",
}, },
section: {
marginBottom: theme.spacing[6],
},
sectionTitle: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.normal,
marginBottom: theme.spacing[3],
marginLeft: theme.spacing[1],
},
label: { label: {
color: theme.colors.foregroundMuted, color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs, fontSize: theme.fontSize.xs,
@@ -139,10 +130,6 @@ const styles = StyleSheet.create((theme) => ({
}, },
// Host card styles // Host card styles
hostCard: { hostCard: {
backgroundColor: theme.colors.surface2,
borderRadius: theme.borderRadius.lg,
borderWidth: 1,
borderColor: theme.colors.border,
marginBottom: theme.spacing[3], marginBottom: theme.spacing[3],
overflow: "hidden", overflow: "hidden",
}, },
@@ -286,10 +273,6 @@ const styles = StyleSheet.create((theme) => ({
}, },
// Add/Edit form // Add/Edit form
formCard: { formCard: {
backgroundColor: theme.colors.surface2,
borderRadius: theme.borderRadius.lg,
borderWidth: 1,
borderColor: theme.colors.border,
padding: theme.spacing[4], padding: theme.spacing[4],
marginBottom: theme.spacing[3], marginBottom: theme.spacing[3],
gap: theme.spacing[4], gap: theme.spacing[4],
@@ -328,10 +311,6 @@ const styles = StyleSheet.create((theme) => ({
}, },
// Audio settings card // Audio settings card
audioCard: { audioCard: {
backgroundColor: theme.colors.surface2,
borderRadius: theme.borderRadius.lg,
borderWidth: 1,
borderColor: theme.colors.border,
overflow: "hidden", overflow: "hidden",
}, },
audioRow: { audioRow: {
@@ -375,10 +354,6 @@ const styles = StyleSheet.create((theme) => ({
}, },
// Empty state // Empty state
emptyCard: { emptyCard: {
backgroundColor: theme.colors.surface2,
borderRadius: theme.borderRadius.lg,
borderWidth: 1,
borderColor: theme.colors.border,
padding: theme.spacing[4], padding: theme.spacing[4],
marginBottom: theme.spacing[3], marginBottom: theme.spacing[3],
}, },
@@ -389,10 +364,6 @@ const styles = StyleSheet.create((theme) => ({
}, },
// Dev section // Dev section
devCard: { devCard: {
backgroundColor: theme.colors.surface2,
borderRadius: theme.borderRadius.lg,
borderWidth: 1,
borderColor: theme.colors.border,
overflow: "hidden", overflow: "hidden",
}, },
devButton: { devButton: {
@@ -501,7 +472,7 @@ function DesktopAppUpdateRow() {
</View> </View>
<View style={styles.aboutUpdateActions}> <View style={styles.aboutUpdateActions}>
<Button <Button
variant="secondary" variant="outline"
size="sm" size="sm"
onPress={handleCheckForUpdates} onPress={handleCheckForUpdates}
disabled={isChecking || isInstalling} disabled={isChecking || isInstalling}
@@ -722,11 +693,11 @@ export default function SettingsScreen() {
<ScrollView style={styles.scrollView} contentContainerStyle={{ paddingBottom: insets.bottom }}> <ScrollView style={styles.scrollView} contentContainerStyle={{ paddingBottom: insets.bottom }}>
<View style={styles.content}> <View style={styles.content}>
{/* Host Management */} {/* Host Management */}
<View style={styles.section}> <View style={settingsStyles.section}>
<Text style={styles.sectionTitle}>Hosts</Text> <Text style={settingsStyles.sectionTitle}>Hosts</Text>
{daemons.length === 0 ? ( {daemons.length === 0 ? (
<View style={styles.emptyCard}> <View style={[settingsStyles.card, styles.emptyCard]}>
<Text style={styles.emptyText}>No hosts configured</Text> <Text style={styles.emptyText}>No hosts configured</Text>
</View> </View>
) : ( ) : (
@@ -878,9 +849,9 @@ export default function SettingsScreen() {
/> />
{/* Appearance */} {/* Appearance */}
<View style={styles.section}> <View style={settingsStyles.section}>
<Text style={styles.sectionTitle}>Appearance</Text> <Text style={settingsStyles.sectionTitle}>Appearance</Text>
<View style={styles.audioCard}> <View style={[settingsStyles.card, styles.audioCard]}>
<View style={styles.audioRow}> <View style={styles.audioRow}>
<View style={styles.audioRowContent}> <View style={styles.audioRowContent}>
<Text style={styles.audioRowTitle}>Theme</Text> <Text style={styles.audioRowTitle}>Theme</Text>
@@ -915,9 +886,9 @@ export default function SettingsScreen() {
{isDesktop ? <LocalDaemonSection appVersion={appVersion} /> : null} {isDesktop ? <LocalDaemonSection appVersion={appVersion} /> : null}
{/* About */} {/* About */}
<View style={styles.section}> <View style={settingsStyles.section}>
<Text style={styles.sectionTitle}>About</Text> <Text style={settingsStyles.sectionTitle}>About</Text>
<View style={styles.audioCard}> <View style={[settingsStyles.card, styles.audioCard]}>
<View style={styles.audioRow}> <View style={styles.audioRow}>
<View style={styles.audioRowContent}> <View style={styles.audioRowContent}>
<Text style={styles.audioRowTitle}>Version</Text> <Text style={styles.audioRowTitle}>Version</Text>
@@ -1332,7 +1303,7 @@ function ConnectionRow({
borderRadius: 12, borderRadius: 12,
borderWidth: 1, borderWidth: 1,
borderColor: theme.colors.border, borderColor: theme.colors.border,
backgroundColor: theme.colors.surface2, backgroundColor: theme.colors.surface1,
}} }}
> >
<Text style={{ color: theme.colors.foreground, fontSize: 12, flex: 1 }}> <Text style={{ color: theme.colors.foreground, fontSize: 12, flex: 1 }}>
@@ -1400,7 +1371,7 @@ function DaemonCard({
return ( return (
<View <View
style={styles.hostCard} style={[settingsStyles.card, styles.hostCard]}
testID={`daemon-card-${daemon.serverId}`} testID={`daemon-card-${daemon.serverId}`}
> >
<View style={styles.hostCardContent}> <View style={styles.hostCardContent}>
+21
View File
@@ -0,0 +1,21 @@
import { StyleSheet } from "react-native-unistyles";
export const settingsStyles = StyleSheet.create((theme) => ({
section: {
marginBottom: theme.spacing[6],
},
sectionTitle: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.normal,
marginBottom: theme.spacing[3],
marginLeft: theme.spacing[1],
},
card: {
backgroundColor: theme.colors.surface1,
borderRadius: theme.borderRadius.lg,
borderWidth: 1,
borderColor: theme.colors.border,
overflow: "hidden",
},
}));