refactor(app): improve terminal profile modal form and zinc button contrast

- Extract terminal profile add/edit modal into a reusable component with shared FormField/FormTextInput primitives.
- Add inline validation, save-pending state, and keyboard navigation.
- Fix zinc theme primary button foreground contrast by using accentForeground/destructiveForeground tokens.

Closes #1484
This commit is contained in:
Mohamed Boudra
2026-06-12 19:30:14 +08:00
committed by GitHub
parent 00a6e636fb
commit a3951495d4
13 changed files with 787 additions and 185 deletions
@@ -319,8 +319,8 @@ export function AddHostModal({ visible, onClose, onCancel, onSaved }: AddHostMod
}, []);
const connectIcon = useMemo(
() => <Link2 size={16} color={theme.colors.palette.white} />,
[theme.colors.palette.white],
() => <Link2 size={16} color={theme.colors.accentForeground} />,
[theme.colors.accentForeground],
);
const hostFieldStyle = useMemo(() => [styles.field, styles.hostField], []);
const portFieldStyle = useMemo(() => [styles.field, styles.portField], []);
@@ -77,8 +77,8 @@ export function PairLinkModal({ visible, onClose, onCancel, onSaved }: PairLinkM
}, []);
const pairIcon = useMemo(
() => <Link size={16} color={theme.colors.palette.white} />,
[theme.colors.palette.white],
() => <Link size={16} color={theme.colors.accentForeground} />,
[theme.colors.accentForeground],
);
const handleClose = useCallback(() => {
+2 -2
View File
@@ -94,10 +94,10 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.xs,
},
textDefault: {
color: theme.colors.palette.white,
color: theme.colors.accentForeground,
},
textDestructive: {
color: theme.colors.palette.white,
color: theme.colors.destructiveForeground,
},
textGhost: {
color: theme.colors.foregroundMuted,
@@ -0,0 +1,82 @@
import { forwardRef, useMemo, type ReactNode } from "react";
import { Text, View, type TextInput } from "react-native";
import { StyleSheet } from "react-native-unistyles";
import { AdaptiveTextInput, type AdaptiveTextInputProps } from "@/components/adaptive-modal-sheet";
interface FormFieldProps {
label: string;
children: ReactNode;
hint?: string;
error?: string | null;
testID?: string;
}
export function FormField({ label, children, hint, error, testID }: FormFieldProps) {
const hintTestID = useMemo(() => (testID ? `${testID}-hint` : undefined), [testID]);
const errorTestID = useMemo(() => (testID ? `${testID}-error` : undefined), [testID]);
const hintOrError = useMemo(() => {
if (error) {
return (
<Text style={styles.errorText} testID={errorTestID}>
{error}
</Text>
);
}
if (hint) {
return (
<Text style={styles.hintText} testID={hintTestID}>
{hint}
</Text>
);
}
return null;
}, [error, hint, errorTestID, hintTestID]);
return (
<View style={styles.container} testID={testID}>
<Text style={styles.label}>{label}</Text>
{children}
{hintOrError}
</View>
);
}
export const FormTextInput = forwardRef<TextInput, AdaptiveTextInputProps>(function FormTextInput(
{ style, ...props },
ref,
) {
const inputStyle = useMemo(() => [formInputStyles.input, style], [style]);
return <AdaptiveTextInput ref={ref} {...props} style={inputStyle} />;
});
const styles = StyleSheet.create((theme) => ({
container: {
gap: theme.spacing[2],
},
label: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.medium,
},
hintText: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs,
},
errorText: {
color: theme.colors.palette.red[300],
fontSize: theme.fontSize.xs,
},
}));
const formInputStyles = StyleSheet.create((theme) => ({
input: {
backgroundColor: theme.colors.surface2,
borderRadius: theme.borderRadius.lg,
paddingHorizontal: theme.spacing[4],
paddingVertical: theme.spacing[3],
color: theme.colors.foreground,
borderWidth: 1,
borderColor: theme.colors.border,
fontSize: theme.fontSize.base,
},
}));