Update files

This commit is contained in:
Mohamed Boudra
2026-02-05 12:35:22 +07:00
parent 45869e699f
commit 1d0b2808c7
8 changed files with 215 additions and 148 deletions

View File

@@ -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.
</Text>
<View style={[styles.formActionsRow, { marginTop: theme.spacing[4] }]}>
<Pressable
style={[styles.formButton, isRemovingHost && styles.disabled]}
<Button
variant="secondary"
size="sm"
style={{ flex: 1 }}
onPress={() => setPendingRemoveHost(null)}
disabled={isRemovingHost}
>
<Text style={styles.formButtonText}>Cancel</Text>
</Pressable>
<Pressable
style={[styles.formButton, isRemovingHost && styles.disabled, { backgroundColor: theme.colors.destructive }]}
Cancel
</Button>
<Button
variant="destructive"
size="sm"
style={{ flex: 1 }}
onPress={() => {
const serverId = pendingRemoveHost.serverId;
setIsRemovingHost(true);
@@ -721,10 +725,8 @@ export default function SettingsScreen() {
disabled={isRemovingHost}
testID="remove-host-confirm"
>
<Text style={[styles.formButtonText, { color: theme.colors.palette.white }]}>
Remove
</Text>
</Pressable>
Remove
</Button>
</View>
</AdaptiveModalSheet>
) : null}
@@ -771,13 +773,13 @@ export default function SettingsScreen() {
backgroundColor: theme.colors.surface2,
}}
>
<Text style={{ color: theme.colors.foreground, fontSize: 12, fontFamily: Fonts.mono, flex: 1 }}>
<Text style={{ color: theme.colors.foreground, fontSize: 12, flex: 1 }}>
{title}
</Text>
<Pressable
onPress={() => void handleRemoveConnection(editingDaemon.serverId, conn.id)}
>
<Text style={{ color: theme.colors.destructive, fontSize: 12, fontWeight: "600" }}>
<Text style={{ color: theme.colors.destructive, fontSize: 12, fontWeight: "500" }}>
Remove
</Text>
</Pressable>
@@ -790,8 +792,10 @@ export default function SettingsScreen() {
{editingDaemon ? (
<View style={styles.formField}>
<Pressable
style={[styles.formButton, styles.formButtonPrimary, { alignSelf: "flex-start" }]}
<Button
variant="default"
size="sm"
style={{ alignSelf: "flex-start" }}
onPress={() => {
const serverId = editingDaemon.serverId;
handleCloseEditDaemon();
@@ -801,30 +805,28 @@ export default function SettingsScreen() {
}}
testID="edit-host-add-connection"
>
<Text style={[styles.formButtonText, styles.formButtonPrimaryText]}>
Add connection
</Text>
</Pressable>
Add connection
</Button>
</View>
) : null}
<View style={styles.formActionsRow}>
<Pressable
style={[styles.formButton, isSavingEdit && styles.disabled]}
<Button
variant="secondary"
size="sm"
onPress={handleCloseEditDaemon}
disabled={isSavingEdit}
>
<Text style={styles.formButtonText}>Cancel</Text>
</Pressable>
<Pressable
style={[styles.formButton, styles.formButtonPrimary, isSavingEdit && styles.disabled]}
Cancel
</Button>
<Button
variant="default"
size="sm"
onPress={() => void handleSaveEditDaemon()}
disabled={isSavingEdit}
>
<Text style={[styles.formButtonText, styles.formButtonPrimaryText]}>
{isSavingEdit ? "Saving..." : "Save"}
</Text>
</Pressable>
{isSavingEdit ? "Saving..." : "Save"}
</Button>
</View>
</AdaptiveModalSheet>

View File

@@ -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<BottomSheetModal>(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();
}
},

View File

@@ -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,

View File

@@ -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
</View>
<View style={styles.actions}>
<Pressable style={styles.button} onPress={handleCancel} disabled={isSaving}>
<Text style={styles.buttonText}>{onCancel ? "Back" : "Cancel"}</Text>
</Pressable>
<Pressable
style={[styles.button, styles.primaryButton, isSaving ? { opacity: 0.7 } : null]}
<Button style={{ flex: 1 }} variant="secondary" onPress={handleCancel} disabled={isSaving}>
Cancel
</Button>
<Button
style={{ flex: 1 }}
variant="default"
onPress={() => void handleSave()}
disabled={isSaving}
leftIcon={<Link2 size={16} color={theme.colors.palette.white} />}
>
<View style={styles.connectRow}>
<Link2 size={16} color={theme.colors.palette.white} />
<Text style={[styles.buttonText, styles.primaryButtonText]}>
{isSaving ? "Connecting..." : "Connect & Save"}
</Text>
</View>
</Pressable>
{isSaving ? "Connecting..." : "Connect"}
</Button>
</View>
</AdaptiveModalSheet>
);

View File

@@ -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
</View>
<View style={styles.actions}>
<Pressable style={styles.button} onPress={onSkip} testID="name-host-skip">
<Text style={styles.buttonText}>Skip</Text>
</Pressable>
<Pressable style={[styles.button, styles.primaryButton]} onPress={handleSave} testID="name-host-save">
<Text style={[styles.buttonText, styles.primaryButtonText]}>Save</Text>
</Pressable>
<Button style={{ flex: 1 }} variant="secondary" onPress={onSkip} testID="name-host-skip">
Skip
</Button>
<Button style={{ flex: 1 }} variant="default" onPress={handleSave} testID="name-host-save">
Save
</Button>
</View>
</AdaptiveModalSheet>
);
}

View File

@@ -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
</View>
<View style={styles.actions}>
<Pressable
style={styles.button}
<Button
style={{ flex: 1 }}
variant="secondary"
onPress={handleCancel}
disabled={isSaving}
testID="pair-link-cancel"
accessible
accessibilityRole="button"
accessibilityLabel="Cancel"
>
<Text style={styles.buttonText}>{onCancel ? "Back" : "Cancel"}</Text>
</Pressable>
<Pressable
style={[styles.button, styles.primaryButton, isSaving ? { opacity: 0.7 } : null]}
Cancel
</Button>
<Button
style={{ flex: 1 }}
variant="default"
onPress={() => void handleSave()}
disabled={isSaving}
testID="pair-link-submit"
accessible
accessibilityRole="button"
accessibilityLabel="Pair"
leftIcon={<Link size={16} color={theme.colors.palette.white} />}
>
<View style={styles.connectRow}>
<Link size={16} color={theme.colors.palette.white} />
<Text style={[styles.buttonText, styles.primaryButtonText]}>
{isSaving ? "Pairing..." : "Pair"}
</Text>
</View>
</Pressable>
{isSaving ? "Pairing..." : "Pair"}
</Button>
</View>
</AdaptiveModalSheet>
);

View File

@@ -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<PressableProps, "style"> & {
variant?: ButtonVariant;
size?: ButtonSize;
leftIcon?: ReactElement | null;
style?: StyleProp<ViewStyle>;
textStyle?: StyleProp<TextStyle>;
}
>) {
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 (
<Pressable
{...props}
accessibilityRole={accessibilityRole ?? "button"}
disabled={disabled}
style={({ pressed }) => [
styles.base,
sizeStyle,
variantStyle,
pressed ? styles.pressed : null,
disabled ? styles.disabled : null,
style,
]}
>
{leftIcon ? <View>{leftIcon}</View> : null}
<Text style={resolvedTextStyle}>{children}</Text>
</Pressable>
);
}

View File

@@ -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,