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 - - + ) : 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 - - + 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 + + ); 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"} - - + ); 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,