diff --git a/packages/app/src/app/settings.tsx b/packages/app/src/app/settings.tsx
index 8afbf6f96..d5c85f2de 100644
--- a/packages/app/src/app/settings.tsx
+++ b/packages/app/src/app/settings.tsx
@@ -14,7 +14,6 @@ import Constants from "expo-constants";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
import { Sun, Moon, Monitor, MoreVertical, Globe } from "lucide-react-native";
-import { Fonts } from "@/constants/theme";
import { useAppSettings, type AppSettings } from "@/hooks/use-settings";
import { useDaemonRegistry, type HostProfile } from "@/contexts/daemon-registry-context";
import { useDaemonConnections, type ActiveConnection, type ConnectionStatus } from "@/contexts/daemon-connections-context";
@@ -34,6 +33,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
+import { Button } from "@/components/ui/button";
import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet";
const delay = (ms: number) =>
@@ -65,6 +65,9 @@ const styles = StyleSheet.create((theme) => ({
content: {
padding: theme.spacing[4],
paddingTop: theme.spacing[6],
+ width: "100%",
+ maxWidth: 720,
+ alignSelf: "center",
},
section: {
marginBottom: theme.spacing[6],
@@ -72,7 +75,7 @@ const styles = StyleSheet.create((theme) => ({
sectionTitle: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs,
- fontWeight: theme.fontWeight.semibold,
+ fontWeight: theme.fontWeight.normal,
letterSpacing: 0.6,
textTransform: "uppercase",
marginBottom: theme.spacing[3],
@@ -81,7 +84,7 @@ const styles = StyleSheet.create((theme) => ({
label: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs,
- fontWeight: theme.fontWeight.semibold,
+ fontWeight: theme.fontWeight.normal,
letterSpacing: 0.4,
textTransform: "uppercase",
marginBottom: theme.spacing[2],
@@ -95,9 +98,6 @@ const styles = StyleSheet.create((theme) => ({
borderColor: theme.colors.border,
fontSize: theme.fontSize.base,
},
- inputUrl: {
- fontFamily: Fonts.mono,
- },
// Host card styles
hostCard: {
backgroundColor: theme.colors.surface2,
@@ -124,7 +124,7 @@ const styles = StyleSheet.create((theme) => ({
hostLabel: {
color: theme.colors.foreground,
fontSize: theme.fontSize.base,
- fontWeight: theme.fontWeight.semibold,
+ fontWeight: theme.fontWeight.normal,
},
hostError: {
color: theme.colors.palette.red[300],
@@ -146,7 +146,7 @@ const styles = StyleSheet.create((theme) => ({
},
statusText: {
fontSize: theme.fontSize.xs,
- fontWeight: theme.fontWeight.semibold,
+ fontWeight: theme.fontWeight.normal,
},
connectionPill: {
flexDirection: "row",
@@ -162,7 +162,7 @@ const styles = StyleSheet.create((theme) => ({
},
connectionText: {
fontSize: theme.fontSize.xs,
- fontWeight: theme.fontWeight.semibold,
+ fontWeight: theme.fontWeight.normal,
color: theme.colors.foregroundMuted,
flexShrink: 1,
},
@@ -197,7 +197,7 @@ const styles = StyleSheet.create((theme) => ({
addButtonText: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.sm,
- fontWeight: theme.fontWeight.semibold,
+ fontWeight: theme.fontWeight.normal,
},
// Add/Edit form
formCard: {
@@ -212,7 +212,7 @@ const styles = StyleSheet.create((theme) => ({
formTitle: {
color: theme.colors.foreground,
fontSize: theme.fontSize.base,
- fontWeight: theme.fontWeight.semibold,
+ fontWeight: theme.fontWeight.normal,
},
formField: {
gap: theme.spacing[2],
@@ -236,7 +236,7 @@ const styles = StyleSheet.create((theme) => ({
formButtonText: {
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
- fontWeight: theme.fontWeight.semibold,
+ fontWeight: theme.fontWeight.normal,
},
formButtonPrimaryText: {
color: theme.colors.palette.white,
@@ -339,7 +339,7 @@ const styles = StyleSheet.create((theme) => ({
},
themeToggleText: {
fontSize: theme.fontSize.sm,
- fontWeight: theme.fontWeight.medium,
+ fontWeight: theme.fontWeight.normal,
color: theme.colors.foregroundMuted,
},
themeToggleTextActive: {
@@ -698,15 +698,19 @@ export default function SettingsScreen() {
Remove {pendingRemoveHost.label}? This will delete its saved connections.
- setPendingRemoveHost(null)}
disabled={isRemovingHost}
>
- Cancel
-
-
+
+ Remove
+
) : null}
@@ -771,13 +773,13 @@ export default function SettingsScreen() {
backgroundColor: theme.colors.surface2,
}}
>
-
+
{title}
void handleRemoveConnection(editingDaemon.serverId, conn.id)}
>
-
+
Remove
@@ -790,8 +792,10 @@ export default function SettingsScreen() {
{editingDaemon ? (
- {
const serverId = editingDaemon.serverId;
handleCloseEditDaemon();
@@ -801,30 +805,28 @@ export default function SettingsScreen() {
}}
testID="edit-host-add-connection"
>
-
- Add connection
-
-
+ Add connection
+
) : null}
-
- Cancel
-
-
+
+ {isSavingEdit ? "Saving..." : "Save"}
+
diff --git a/packages/app/src/components/adaptive-modal-sheet.tsx b/packages/app/src/components/adaptive-modal-sheet.tsx
index ff7a47cea..d1958efdb 100644
--- a/packages/app/src/components/adaptive-modal-sheet.tsx
+++ b/packages/app/src/components/adaptive-modal-sheet.tsx
@@ -51,7 +51,7 @@ const styles = StyleSheet.create((theme) => ({
title: {
color: theme.colors.foreground,
fontSize: theme.fontSize.lg,
- fontWeight: theme.fontWeight.semibold,
+ fontWeight: theme.fontWeight.medium,
},
closeButton: {
padding: theme.spacing[2],
@@ -121,13 +121,16 @@ export function AdaptiveModalSheet({
const isMobile =
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
const sheetRef = useRef(null);
+ const dismissingForVisibilityRef = useRef(false);
const resolvedSnapPoints = useMemo(() => snapPoints ?? ["65%", "90%"], [snapPoints]);
useEffect(() => {
if (!isMobile) return;
if (visible) {
+ dismissingForVisibilityRef.current = false;
sheetRef.current?.present();
} else {
+ dismissingForVisibilityRef.current = true;
sheetRef.current?.dismiss();
}
}, [visible, isMobile]);
@@ -135,6 +138,10 @@ export function AdaptiveModalSheet({
const handleSheetChange = useCallback(
(index: number) => {
if (index === -1) {
+ if (dismissingForVisibilityRef.current) {
+ dismissingForVisibilityRef.current = false;
+ return;
+ }
onClose();
}
},
diff --git a/packages/app/src/components/add-host-method-modal.tsx b/packages/app/src/components/add-host-method-modal.tsx
index 080c58e9c..834aa27f0 100644
--- a/packages/app/src/components/add-host-method-modal.tsx
+++ b/packages/app/src/components/add-host-method-modal.tsx
@@ -18,7 +18,7 @@ const styles = StyleSheet.create((theme) => ({
optionText: {
color: theme.colors.foreground,
fontSize: theme.fontSize.base,
- fontWeight: theme.fontWeight.semibold,
+ fontWeight: theme.fontWeight.normal,
},
optionSubtext: {
color: theme.colors.foregroundMuted,
diff --git a/packages/app/src/components/add-host-modal.tsx b/packages/app/src/components/add-host-modal.tsx
index f81931e66..7dd5b9038 100644
--- a/packages/app/src/components/add-host-modal.tsx
+++ b/packages/app/src/components/add-host-modal.tsx
@@ -1,5 +1,5 @@
import { useCallback, useMemo, useRef, useState } from "react";
-import { Alert, Pressable, Text, TextInput, View } from "react-native";
+import { Alert, Text, TextInput, View } from "react-native";
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
import { BottomSheetTextInput } from "@gorhom/bottom-sheet";
import { Link2 } from "lucide-react-native";
@@ -7,6 +7,7 @@ import { useDaemonRegistry, type HostProfile } from "@/contexts/daemon-registry-
import { normalizeHostPort } from "@/utils/daemon-endpoints";
import { DaemonConnectionTestError, probeDaemonEndpoint } from "@/utils/test-daemon-connection";
import { AdaptiveModalSheet } from "./adaptive-modal-sheet";
+import { Button } from "@/components/ui/button";
const styles = StyleSheet.create((theme) => ({
field: {
@@ -31,24 +32,6 @@ const styles = StyleSheet.create((theme) => ({
gap: theme.spacing[3],
marginTop: theme.spacing[2],
},
- button: {
- flex: 1,
- paddingVertical: theme.spacing[3],
- borderRadius: theme.borderRadius.lg,
- alignItems: "center",
- justifyContent: "center",
- backgroundColor: theme.colors.surface2,
- },
- primaryButton: {
- backgroundColor: theme.colors.palette.blue[500],
- },
- buttonText: {
- color: theme.colors.foreground,
- fontWeight: theme.fontWeight.semibold,
- },
- primaryButtonText: {
- color: theme.colors.palette.white,
- },
helper: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.sm,
@@ -57,12 +40,6 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.destructive,
fontSize: theme.fontSize.sm,
},
- connectRow: {
- flexDirection: "row",
- alignItems: "center",
- justifyContent: "center",
- gap: theme.spacing[2],
- },
}));
function isHostPortOnly(raw: string): boolean {
@@ -264,21 +241,18 @@ export function AddHostModal({ visible, onClose, onCancel, onSaved, targetServer
-
- {onCancel ? "Back" : "Cancel"}
-
-
+ Cancel
+
+
+ {isSaving ? "Connecting..." : "Connect"}
+
);
diff --git a/packages/app/src/components/name-host-modal.tsx b/packages/app/src/components/name-host-modal.tsx
index b1071f8e9..65ff9aaa9 100644
--- a/packages/app/src/components/name-host-modal.tsx
+++ b/packages/app/src/components/name-host-modal.tsx
@@ -1,8 +1,9 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
-import { Pressable, Text, TextInput, View } from "react-native";
+import { Text, TextInput, View } from "react-native";
import { BottomSheetTextInput } from "@gorhom/bottom-sheet";
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
import { AdaptiveModalSheet } from "./adaptive-modal-sheet";
+import { Button } from "@/components/ui/button";
const styles = StyleSheet.create((theme) => ({
helper: {
@@ -32,24 +33,6 @@ const styles = StyleSheet.create((theme) => ({
gap: theme.spacing[3],
marginTop: theme.spacing[4],
},
- button: {
- flex: 1,
- paddingVertical: theme.spacing[3],
- borderRadius: theme.borderRadius.lg,
- alignItems: "center",
- justifyContent: "center",
- backgroundColor: theme.colors.surface2,
- },
- primaryButton: {
- backgroundColor: theme.colors.palette.blue[500],
- },
- buttonText: {
- color: theme.colors.foreground,
- fontWeight: theme.fontWeight.semibold,
- },
- primaryButtonText: {
- color: theme.colors.palette.white,
- },
}));
export interface NameHostModalProps {
@@ -122,14 +105,13 @@ export function NameHostModal({ visible, serverId, hostname, onSkip, onSave }: N
-
- Skip
-
-
- Save
-
+
+
);
}
-
diff --git a/packages/app/src/components/pair-link-modal.tsx b/packages/app/src/components/pair-link-modal.tsx
index 74691c350..0a1c31ddb 100644
--- a/packages/app/src/components/pair-link-modal.tsx
+++ b/packages/app/src/components/pair-link-modal.tsx
@@ -1,5 +1,5 @@
import { useCallback, useMemo, useState } from "react";
-import { Alert, Pressable, Text, TextInput, View } from "react-native";
+import { Alert, Text, TextInput, View } from "react-native";
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
import { BottomSheetTextInput } from "@gorhom/bottom-sheet";
import { Link } from "lucide-react-native";
@@ -7,6 +7,7 @@ import { useDaemonRegistry, type HostProfile } from "@/contexts/daemon-registry-
import { decodeOfferFragmentPayload } from "@/utils/daemon-endpoints";
import { ConnectionOfferSchema } from "@server/shared/connection-offer";
import { AdaptiveModalSheet } from "./adaptive-modal-sheet";
+import { Button } from "@/components/ui/button";
const styles = StyleSheet.create((theme) => ({
helper: {
@@ -39,30 +40,6 @@ const styles = StyleSheet.create((theme) => ({
gap: theme.spacing[3],
marginTop: theme.spacing[2],
},
- button: {
- flex: 1,
- paddingVertical: theme.spacing[3],
- borderRadius: theme.borderRadius.lg,
- alignItems: "center",
- justifyContent: "center",
- backgroundColor: theme.colors.surface2,
- },
- primaryButton: {
- backgroundColor: theme.colors.palette.blue[500],
- },
- buttonText: {
- color: theme.colors.foreground,
- fontWeight: theme.fontWeight.semibold,
- },
- primaryButtonText: {
- color: theme.colors.palette.white,
- },
- connectRow: {
- flexDirection: "row",
- alignItems: "center",
- justifyContent: "center",
- gap: theme.spacing[2],
- },
}));
export interface PairLinkModalProps {
@@ -184,33 +161,29 @@ export function PairLinkModal({ visible, onClose, onCancel, onSaved, targetServe
-
- {onCancel ? "Back" : "Cancel"}
-
-
+
+ {isSaving ? "Pairing..." : "Pair"}
+
);
diff --git a/packages/app/src/components/ui/button.tsx b/packages/app/src/components/ui/button.tsx
new file mode 100644
index 000000000..03ca9c8cb
--- /dev/null
+++ b/packages/app/src/components/ui/button.tsx
@@ -0,0 +1,129 @@
+import type { PropsWithChildren, ReactElement } from "react";
+import { Pressable, Text, View } from "react-native";
+import type { PressableProps, StyleProp, TextStyle, ViewStyle } from "react-native";
+import { StyleSheet } from "react-native-unistyles";
+
+type ButtonVariant = "default" | "secondary" | "outline" | "ghost" | "destructive";
+type ButtonSize = "sm" | "md" | "lg";
+
+const styles = StyleSheet.create((theme) => ({
+ base: {
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "center",
+ gap: theme.spacing[2],
+ borderRadius: theme.borderRadius.lg,
+ borderWidth: 1,
+ borderColor: "transparent",
+ },
+ md: {
+ paddingVertical: theme.spacing[3],
+ paddingHorizontal: theme.spacing[4],
+ },
+ sm: {
+ paddingVertical: theme.spacing[2],
+ paddingHorizontal: theme.spacing[3],
+ borderRadius: theme.borderRadius.md,
+ },
+ lg: {
+ paddingVertical: theme.spacing[4],
+ paddingHorizontal: theme.spacing[6],
+ borderRadius: theme.borderRadius.xl,
+ },
+ default: {
+ backgroundColor: theme.colors.palette.blue[500],
+ borderColor: theme.colors.palette.blue[500],
+ },
+ secondary: {
+ backgroundColor: theme.colors.surface2,
+ borderColor: theme.colors.border,
+ },
+ outline: {
+ backgroundColor: "transparent",
+ borderColor: theme.colors.border,
+ },
+ ghost: {
+ backgroundColor: "transparent",
+ borderColor: "transparent",
+ },
+ destructive: {
+ backgroundColor: theme.colors.destructive,
+ borderColor: theme.colors.destructive,
+ },
+ pressed: {
+ opacity: 0.85,
+ },
+ disabled: {
+ opacity: theme.opacity[50],
+ },
+ text: {
+ color: theme.colors.foreground,
+ fontSize: theme.fontSize.sm,
+ fontWeight: theme.fontWeight.medium,
+ },
+ textDefault: {
+ color: theme.colors.palette.white,
+ },
+ textDestructive: {
+ color: theme.colors.palette.white,
+ },
+}));
+
+export function Button({
+ children,
+ variant = "secondary",
+ size = "md",
+ leftIcon,
+ style,
+ textStyle,
+ disabled,
+ accessibilityRole,
+ ...props
+}: PropsWithChildren<
+ Omit & {
+ variant?: ButtonVariant;
+ size?: ButtonSize;
+ leftIcon?: ReactElement | null;
+ style?: StyleProp;
+ textStyle?: StyleProp;
+ }
+>) {
+ const variantStyle =
+ variant === "default"
+ ? styles.default
+ : variant === "secondary"
+ ? styles.secondary
+ : variant === "outline"
+ ? styles.outline
+ : variant === "ghost"
+ ? styles.ghost
+ : styles.destructive;
+
+ const sizeStyle = size === "sm" ? styles.sm : size === "lg" ? styles.lg : styles.md;
+
+ const resolvedTextStyle = [
+ styles.text,
+ variant === "default" ? styles.textDefault : null,
+ variant === "destructive" ? styles.textDestructive : null,
+ textStyle,
+ ];
+
+ return (
+ [
+ styles.base,
+ sizeStyle,
+ variantStyle,
+ pressed ? styles.pressed : null,
+ disabled ? styles.disabled : null,
+ style,
+ ]}
+ >
+ {leftIcon ? {leftIcon} : null}
+ {children}
+
+ );
+}
diff --git a/packages/app/src/components/welcome-screen.tsx b/packages/app/src/components/welcome-screen.tsx
index d1c57f4e8..d40ef5194 100644
--- a/packages/app/src/components/welcome-screen.tsx
+++ b/packages/app/src/components/welcome-screen.tsx
@@ -26,7 +26,7 @@ const styles = StyleSheet.create((theme) => ({
title: {
color: theme.colors.foreground,
fontSize: theme.fontSize.xl,
- fontWeight: theme.fontWeight.bold,
+ fontWeight: theme.fontWeight.medium,
marginBottom: theme.spacing[3],
textAlign: "center",
},
@@ -59,7 +59,7 @@ const styles = StyleSheet.create((theme) => ({
actionText: {
color: theme.colors.foreground,
fontSize: theme.fontSize.base,
- fontWeight: theme.fontWeight.semibold,
+ fontWeight: theme.fontWeight.medium,
},
actionTextPrimary: {
color: theme.colors.palette.white,