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