Update files
This commit is contained in:
@@ -14,7 +14,6 @@ import Constants from "expo-constants";
|
|||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
||||||
import { Sun, Moon, Monitor, MoreVertical, Globe } from "lucide-react-native";
|
import { Sun, Moon, Monitor, MoreVertical, Globe } from "lucide-react-native";
|
||||||
import { Fonts } from "@/constants/theme";
|
|
||||||
import { useAppSettings, type AppSettings } from "@/hooks/use-settings";
|
import { useAppSettings, type AppSettings } from "@/hooks/use-settings";
|
||||||
import { useDaemonRegistry, type HostProfile } from "@/contexts/daemon-registry-context";
|
import { useDaemonRegistry, type HostProfile } from "@/contexts/daemon-registry-context";
|
||||||
import { useDaemonConnections, type ActiveConnection, type ConnectionStatus } from "@/contexts/daemon-connections-context";
|
import { useDaemonConnections, type ActiveConnection, type ConnectionStatus } from "@/contexts/daemon-connections-context";
|
||||||
@@ -34,6 +33,7 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet";
|
import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet";
|
||||||
|
|
||||||
const delay = (ms: number) =>
|
const delay = (ms: number) =>
|
||||||
@@ -65,6 +65,9 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
content: {
|
content: {
|
||||||
padding: theme.spacing[4],
|
padding: theme.spacing[4],
|
||||||
paddingTop: theme.spacing[6],
|
paddingTop: theme.spacing[6],
|
||||||
|
width: "100%",
|
||||||
|
maxWidth: 720,
|
||||||
|
alignSelf: "center",
|
||||||
},
|
},
|
||||||
section: {
|
section: {
|
||||||
marginBottom: theme.spacing[6],
|
marginBottom: theme.spacing[6],
|
||||||
@@ -72,7 +75,7 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
sectionTitle: {
|
sectionTitle: {
|
||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
fontSize: theme.fontSize.xs,
|
fontSize: theme.fontSize.xs,
|
||||||
fontWeight: theme.fontWeight.semibold,
|
fontWeight: theme.fontWeight.normal,
|
||||||
letterSpacing: 0.6,
|
letterSpacing: 0.6,
|
||||||
textTransform: "uppercase",
|
textTransform: "uppercase",
|
||||||
marginBottom: theme.spacing[3],
|
marginBottom: theme.spacing[3],
|
||||||
@@ -81,7 +84,7 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
label: {
|
label: {
|
||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
fontSize: theme.fontSize.xs,
|
fontSize: theme.fontSize.xs,
|
||||||
fontWeight: theme.fontWeight.semibold,
|
fontWeight: theme.fontWeight.normal,
|
||||||
letterSpacing: 0.4,
|
letterSpacing: 0.4,
|
||||||
textTransform: "uppercase",
|
textTransform: "uppercase",
|
||||||
marginBottom: theme.spacing[2],
|
marginBottom: theme.spacing[2],
|
||||||
@@ -95,9 +98,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
borderColor: theme.colors.border,
|
borderColor: theme.colors.border,
|
||||||
fontSize: theme.fontSize.base,
|
fontSize: theme.fontSize.base,
|
||||||
},
|
},
|
||||||
inputUrl: {
|
|
||||||
fontFamily: Fonts.mono,
|
|
||||||
},
|
|
||||||
// Host card styles
|
// Host card styles
|
||||||
hostCard: {
|
hostCard: {
|
||||||
backgroundColor: theme.colors.surface2,
|
backgroundColor: theme.colors.surface2,
|
||||||
@@ -124,7 +124,7 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
hostLabel: {
|
hostLabel: {
|
||||||
color: theme.colors.foreground,
|
color: theme.colors.foreground,
|
||||||
fontSize: theme.fontSize.base,
|
fontSize: theme.fontSize.base,
|
||||||
fontWeight: theme.fontWeight.semibold,
|
fontWeight: theme.fontWeight.normal,
|
||||||
},
|
},
|
||||||
hostError: {
|
hostError: {
|
||||||
color: theme.colors.palette.red[300],
|
color: theme.colors.palette.red[300],
|
||||||
@@ -146,7 +146,7 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
},
|
},
|
||||||
statusText: {
|
statusText: {
|
||||||
fontSize: theme.fontSize.xs,
|
fontSize: theme.fontSize.xs,
|
||||||
fontWeight: theme.fontWeight.semibold,
|
fontWeight: theme.fontWeight.normal,
|
||||||
},
|
},
|
||||||
connectionPill: {
|
connectionPill: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
@@ -162,7 +162,7 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
},
|
},
|
||||||
connectionText: {
|
connectionText: {
|
||||||
fontSize: theme.fontSize.xs,
|
fontSize: theme.fontSize.xs,
|
||||||
fontWeight: theme.fontWeight.semibold,
|
fontWeight: theme.fontWeight.normal,
|
||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
flexShrink: 1,
|
flexShrink: 1,
|
||||||
},
|
},
|
||||||
@@ -197,7 +197,7 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
addButtonText: {
|
addButtonText: {
|
||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
fontSize: theme.fontSize.sm,
|
fontSize: theme.fontSize.sm,
|
||||||
fontWeight: theme.fontWeight.semibold,
|
fontWeight: theme.fontWeight.normal,
|
||||||
},
|
},
|
||||||
// Add/Edit form
|
// Add/Edit form
|
||||||
formCard: {
|
formCard: {
|
||||||
@@ -212,7 +212,7 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
formTitle: {
|
formTitle: {
|
||||||
color: theme.colors.foreground,
|
color: theme.colors.foreground,
|
||||||
fontSize: theme.fontSize.base,
|
fontSize: theme.fontSize.base,
|
||||||
fontWeight: theme.fontWeight.semibold,
|
fontWeight: theme.fontWeight.normal,
|
||||||
},
|
},
|
||||||
formField: {
|
formField: {
|
||||||
gap: theme.spacing[2],
|
gap: theme.spacing[2],
|
||||||
@@ -236,7 +236,7 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
formButtonText: {
|
formButtonText: {
|
||||||
color: theme.colors.foreground,
|
color: theme.colors.foreground,
|
||||||
fontSize: theme.fontSize.sm,
|
fontSize: theme.fontSize.sm,
|
||||||
fontWeight: theme.fontWeight.semibold,
|
fontWeight: theme.fontWeight.normal,
|
||||||
},
|
},
|
||||||
formButtonPrimaryText: {
|
formButtonPrimaryText: {
|
||||||
color: theme.colors.palette.white,
|
color: theme.colors.palette.white,
|
||||||
@@ -339,7 +339,7 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
},
|
},
|
||||||
themeToggleText: {
|
themeToggleText: {
|
||||||
fontSize: theme.fontSize.sm,
|
fontSize: theme.fontSize.sm,
|
||||||
fontWeight: theme.fontWeight.medium,
|
fontWeight: theme.fontWeight.normal,
|
||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
},
|
},
|
||||||
themeToggleTextActive: {
|
themeToggleTextActive: {
|
||||||
@@ -698,15 +698,19 @@ export default function SettingsScreen() {
|
|||||||
Remove {pendingRemoveHost.label}? This will delete its saved connections.
|
Remove {pendingRemoveHost.label}? This will delete its saved connections.
|
||||||
</Text>
|
</Text>
|
||||||
<View style={[styles.formActionsRow, { marginTop: theme.spacing[4] }]}>
|
<View style={[styles.formActionsRow, { marginTop: theme.spacing[4] }]}>
|
||||||
<Pressable
|
<Button
|
||||||
style={[styles.formButton, isRemovingHost && styles.disabled]}
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
style={{ flex: 1 }}
|
||||||
onPress={() => setPendingRemoveHost(null)}
|
onPress={() => setPendingRemoveHost(null)}
|
||||||
disabled={isRemovingHost}
|
disabled={isRemovingHost}
|
||||||
>
|
>
|
||||||
<Text style={styles.formButtonText}>Cancel</Text>
|
Cancel
|
||||||
</Pressable>
|
</Button>
|
||||||
<Pressable
|
<Button
|
||||||
style={[styles.formButton, isRemovingHost && styles.disabled, { backgroundColor: theme.colors.destructive }]}
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
style={{ flex: 1 }}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
const serverId = pendingRemoveHost.serverId;
|
const serverId = pendingRemoveHost.serverId;
|
||||||
setIsRemovingHost(true);
|
setIsRemovingHost(true);
|
||||||
@@ -721,10 +725,8 @@ export default function SettingsScreen() {
|
|||||||
disabled={isRemovingHost}
|
disabled={isRemovingHost}
|
||||||
testID="remove-host-confirm"
|
testID="remove-host-confirm"
|
||||||
>
|
>
|
||||||
<Text style={[styles.formButtonText, { color: theme.colors.palette.white }]}>
|
Remove
|
||||||
Remove
|
</Button>
|
||||||
</Text>
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
</View>
|
||||||
</AdaptiveModalSheet>
|
</AdaptiveModalSheet>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -771,13 +773,13 @@ export default function SettingsScreen() {
|
|||||||
backgroundColor: theme.colors.surface2,
|
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}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => void handleRemoveConnection(editingDaemon.serverId, conn.id)}
|
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
|
Remove
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
@@ -790,8 +792,10 @@ export default function SettingsScreen() {
|
|||||||
|
|
||||||
{editingDaemon ? (
|
{editingDaemon ? (
|
||||||
<View style={styles.formField}>
|
<View style={styles.formField}>
|
||||||
<Pressable
|
<Button
|
||||||
style={[styles.formButton, styles.formButtonPrimary, { alignSelf: "flex-start" }]}
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
style={{ alignSelf: "flex-start" }}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
const serverId = editingDaemon.serverId;
|
const serverId = editingDaemon.serverId;
|
||||||
handleCloseEditDaemon();
|
handleCloseEditDaemon();
|
||||||
@@ -801,30 +805,28 @@ export default function SettingsScreen() {
|
|||||||
}}
|
}}
|
||||||
testID="edit-host-add-connection"
|
testID="edit-host-add-connection"
|
||||||
>
|
>
|
||||||
<Text style={[styles.formButtonText, styles.formButtonPrimaryText]}>
|
Add connection
|
||||||
Add connection
|
</Button>
|
||||||
</Text>
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<View style={styles.formActionsRow}>
|
<View style={styles.formActionsRow}>
|
||||||
<Pressable
|
<Button
|
||||||
style={[styles.formButton, isSavingEdit && styles.disabled]}
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
onPress={handleCloseEditDaemon}
|
onPress={handleCloseEditDaemon}
|
||||||
disabled={isSavingEdit}
|
disabled={isSavingEdit}
|
||||||
>
|
>
|
||||||
<Text style={styles.formButtonText}>Cancel</Text>
|
Cancel
|
||||||
</Pressable>
|
</Button>
|
||||||
<Pressable
|
<Button
|
||||||
style={[styles.formButton, styles.formButtonPrimary, isSavingEdit && styles.disabled]}
|
variant="default"
|
||||||
|
size="sm"
|
||||||
onPress={() => void handleSaveEditDaemon()}
|
onPress={() => void handleSaveEditDaemon()}
|
||||||
disabled={isSavingEdit}
|
disabled={isSavingEdit}
|
||||||
>
|
>
|
||||||
<Text style={[styles.formButtonText, styles.formButtonPrimaryText]}>
|
{isSavingEdit ? "Saving..." : "Save"}
|
||||||
{isSavingEdit ? "Saving..." : "Save"}
|
</Button>
|
||||||
</Text>
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
</View>
|
||||||
</AdaptiveModalSheet>
|
</AdaptiveModalSheet>
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
title: {
|
title: {
|
||||||
color: theme.colors.foreground,
|
color: theme.colors.foreground,
|
||||||
fontSize: theme.fontSize.lg,
|
fontSize: theme.fontSize.lg,
|
||||||
fontWeight: theme.fontWeight.semibold,
|
fontWeight: theme.fontWeight.medium,
|
||||||
},
|
},
|
||||||
closeButton: {
|
closeButton: {
|
||||||
padding: theme.spacing[2],
|
padding: theme.spacing[2],
|
||||||
@@ -121,13 +121,16 @@ export function AdaptiveModalSheet({
|
|||||||
const isMobile =
|
const isMobile =
|
||||||
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
||||||
const sheetRef = useRef<BottomSheetModal>(null);
|
const sheetRef = useRef<BottomSheetModal>(null);
|
||||||
|
const dismissingForVisibilityRef = useRef(false);
|
||||||
const resolvedSnapPoints = useMemo(() => snapPoints ?? ["65%", "90%"], [snapPoints]);
|
const resolvedSnapPoints = useMemo(() => snapPoints ?? ["65%", "90%"], [snapPoints]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isMobile) return;
|
if (!isMobile) return;
|
||||||
if (visible) {
|
if (visible) {
|
||||||
|
dismissingForVisibilityRef.current = false;
|
||||||
sheetRef.current?.present();
|
sheetRef.current?.present();
|
||||||
} else {
|
} else {
|
||||||
|
dismissingForVisibilityRef.current = true;
|
||||||
sheetRef.current?.dismiss();
|
sheetRef.current?.dismiss();
|
||||||
}
|
}
|
||||||
}, [visible, isMobile]);
|
}, [visible, isMobile]);
|
||||||
@@ -135,6 +138,10 @@ export function AdaptiveModalSheet({
|
|||||||
const handleSheetChange = useCallback(
|
const handleSheetChange = useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
|
if (dismissingForVisibilityRef.current) {
|
||||||
|
dismissingForVisibilityRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
optionText: {
|
optionText: {
|
||||||
color: theme.colors.foreground,
|
color: theme.colors.foreground,
|
||||||
fontSize: theme.fontSize.base,
|
fontSize: theme.fontSize.base,
|
||||||
fontWeight: theme.fontWeight.semibold,
|
fontWeight: theme.fontWeight.normal,
|
||||||
},
|
},
|
||||||
optionSubtext: {
|
optionSubtext: {
|
||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useMemo, useRef, useState } from "react";
|
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 { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
||||||
import { BottomSheetTextInput } from "@gorhom/bottom-sheet";
|
import { BottomSheetTextInput } from "@gorhom/bottom-sheet";
|
||||||
import { Link2 } from "lucide-react-native";
|
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 { normalizeHostPort } from "@/utils/daemon-endpoints";
|
||||||
import { DaemonConnectionTestError, probeDaemonEndpoint } from "@/utils/test-daemon-connection";
|
import { DaemonConnectionTestError, probeDaemonEndpoint } from "@/utils/test-daemon-connection";
|
||||||
import { AdaptiveModalSheet } from "./adaptive-modal-sheet";
|
import { AdaptiveModalSheet } from "./adaptive-modal-sheet";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
const styles = StyleSheet.create((theme) => ({
|
const styles = StyleSheet.create((theme) => ({
|
||||||
field: {
|
field: {
|
||||||
@@ -31,24 +32,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
gap: theme.spacing[3],
|
gap: theme.spacing[3],
|
||||||
marginTop: theme.spacing[2],
|
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: {
|
helper: {
|
||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
fontSize: theme.fontSize.sm,
|
fontSize: theme.fontSize.sm,
|
||||||
@@ -57,12 +40,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
color: theme.colors.destructive,
|
color: theme.colors.destructive,
|
||||||
fontSize: theme.fontSize.sm,
|
fontSize: theme.fontSize.sm,
|
||||||
},
|
},
|
||||||
connectRow: {
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
gap: theme.spacing[2],
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function isHostPortOnly(raw: string): boolean {
|
function isHostPortOnly(raw: string): boolean {
|
||||||
@@ -264,21 +241,18 @@ export function AddHostModal({ visible, onClose, onCancel, onSaved, targetServer
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.actions}>
|
<View style={styles.actions}>
|
||||||
<Pressable style={styles.button} onPress={handleCancel} disabled={isSaving}>
|
<Button style={{ flex: 1 }} variant="secondary" onPress={handleCancel} disabled={isSaving}>
|
||||||
<Text style={styles.buttonText}>{onCancel ? "Back" : "Cancel"}</Text>
|
Cancel
|
||||||
</Pressable>
|
</Button>
|
||||||
<Pressable
|
<Button
|
||||||
style={[styles.button, styles.primaryButton, isSaving ? { opacity: 0.7 } : null]}
|
style={{ flex: 1 }}
|
||||||
|
variant="default"
|
||||||
onPress={() => void handleSave()}
|
onPress={() => void handleSave()}
|
||||||
disabled={isSaving}
|
disabled={isSaving}
|
||||||
|
leftIcon={<Link2 size={16} color={theme.colors.palette.white} />}
|
||||||
>
|
>
|
||||||
<View style={styles.connectRow}>
|
{isSaving ? "Connecting..." : "Connect"}
|
||||||
<Link2 size={16} color={theme.colors.palette.white} />
|
</Button>
|
||||||
<Text style={[styles.buttonText, styles.primaryButtonText]}>
|
|
||||||
{isSaving ? "Connecting..." : "Connect & Save"}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
</View>
|
||||||
</AdaptiveModalSheet>
|
</AdaptiveModalSheet>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
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 { BottomSheetTextInput } from "@gorhom/bottom-sheet";
|
||||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
||||||
import { AdaptiveModalSheet } from "./adaptive-modal-sheet";
|
import { AdaptiveModalSheet } from "./adaptive-modal-sheet";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
const styles = StyleSheet.create((theme) => ({
|
const styles = StyleSheet.create((theme) => ({
|
||||||
helper: {
|
helper: {
|
||||||
@@ -32,24 +33,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
gap: theme.spacing[3],
|
gap: theme.spacing[3],
|
||||||
marginTop: theme.spacing[4],
|
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 {
|
export interface NameHostModalProps {
|
||||||
@@ -122,14 +105,13 @@ export function NameHostModal({ visible, serverId, hostname, onSkip, onSave }: N
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.actions}>
|
<View style={styles.actions}>
|
||||||
<Pressable style={styles.button} onPress={onSkip} testID="name-host-skip">
|
<Button style={{ flex: 1 }} variant="secondary" onPress={onSkip} testID="name-host-skip">
|
||||||
<Text style={styles.buttonText}>Skip</Text>
|
Skip
|
||||||
</Pressable>
|
</Button>
|
||||||
<Pressable style={[styles.button, styles.primaryButton]} onPress={handleSave} testID="name-host-save">
|
<Button style={{ flex: 1 }} variant="default" onPress={handleSave} testID="name-host-save">
|
||||||
<Text style={[styles.buttonText, styles.primaryButtonText]}>Save</Text>
|
Save
|
||||||
</Pressable>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
</AdaptiveModalSheet>
|
</AdaptiveModalSheet>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useMemo, useState } from "react";
|
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 { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
||||||
import { BottomSheetTextInput } from "@gorhom/bottom-sheet";
|
import { BottomSheetTextInput } from "@gorhom/bottom-sheet";
|
||||||
import { Link } from "lucide-react-native";
|
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 { decodeOfferFragmentPayload } from "@/utils/daemon-endpoints";
|
||||||
import { ConnectionOfferSchema } from "@server/shared/connection-offer";
|
import { ConnectionOfferSchema } from "@server/shared/connection-offer";
|
||||||
import { AdaptiveModalSheet } from "./adaptive-modal-sheet";
|
import { AdaptiveModalSheet } from "./adaptive-modal-sheet";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
const styles = StyleSheet.create((theme) => ({
|
const styles = StyleSheet.create((theme) => ({
|
||||||
helper: {
|
helper: {
|
||||||
@@ -39,30 +40,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
gap: theme.spacing[3],
|
gap: theme.spacing[3],
|
||||||
marginTop: theme.spacing[2],
|
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 {
|
export interface PairLinkModalProps {
|
||||||
@@ -184,33 +161,29 @@ export function PairLinkModal({ visible, onClose, onCancel, onSaved, targetServe
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.actions}>
|
<View style={styles.actions}>
|
||||||
<Pressable
|
<Button
|
||||||
style={styles.button}
|
style={{ flex: 1 }}
|
||||||
|
variant="secondary"
|
||||||
onPress={handleCancel}
|
onPress={handleCancel}
|
||||||
disabled={isSaving}
|
disabled={isSaving}
|
||||||
testID="pair-link-cancel"
|
testID="pair-link-cancel"
|
||||||
accessible
|
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Cancel"
|
accessibilityLabel="Cancel"
|
||||||
>
|
>
|
||||||
<Text style={styles.buttonText}>{onCancel ? "Back" : "Cancel"}</Text>
|
Cancel
|
||||||
</Pressable>
|
</Button>
|
||||||
<Pressable
|
<Button
|
||||||
style={[styles.button, styles.primaryButton, isSaving ? { opacity: 0.7 } : null]}
|
style={{ flex: 1 }}
|
||||||
|
variant="default"
|
||||||
onPress={() => void handleSave()}
|
onPress={() => void handleSave()}
|
||||||
disabled={isSaving}
|
disabled={isSaving}
|
||||||
testID="pair-link-submit"
|
testID="pair-link-submit"
|
||||||
accessible
|
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Pair"
|
accessibilityLabel="Pair"
|
||||||
|
leftIcon={<Link size={16} color={theme.colors.palette.white} />}
|
||||||
>
|
>
|
||||||
<View style={styles.connectRow}>
|
{isSaving ? "Pairing..." : "Pair"}
|
||||||
<Link size={16} color={theme.colors.palette.white} />
|
</Button>
|
||||||
<Text style={[styles.buttonText, styles.primaryButtonText]}>
|
|
||||||
{isSaving ? "Pairing..." : "Pair"}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
</View>
|
||||||
</AdaptiveModalSheet>
|
</AdaptiveModalSheet>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
title: {
|
title: {
|
||||||
color: theme.colors.foreground,
|
color: theme.colors.foreground,
|
||||||
fontSize: theme.fontSize.xl,
|
fontSize: theme.fontSize.xl,
|
||||||
fontWeight: theme.fontWeight.bold,
|
fontWeight: theme.fontWeight.medium,
|
||||||
marginBottom: theme.spacing[3],
|
marginBottom: theme.spacing[3],
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
},
|
},
|
||||||
@@ -59,7 +59,7 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
actionText: {
|
actionText: {
|
||||||
color: theme.colors.foreground,
|
color: theme.colors.foreground,
|
||||||
fontSize: theme.fontSize.base,
|
fontSize: theme.fontSize.base,
|
||||||
fontWeight: theme.fontWeight.semibold,
|
fontWeight: theme.fontWeight.medium,
|
||||||
},
|
},
|
||||||
actionTextPrimary: {
|
actionTextPrimary: {
|
||||||
color: theme.colors.palette.white,
|
color: theme.colors.palette.white,
|
||||||
|
|||||||
Reference in New Issue
Block a user