refactor: standardize Input component prop naming

Replaces `className` prop with `extraClassName` across Input and PasswordInput components for consistency.

Updates PasswordInput to use numeric `topOffset` instead of string `topPosition` for better type safety and clearer intent.

Adds uncontrolled mode support to PasswordInput with internal state management and optional `defaultShowPassword` prop.

Removes unnecessary margin classes from various View components to clean up spacing.
This commit is contained in:
Uruk
2025-09-01 22:54:11 +02:00
parent be884ce6e6
commit 29d3360a10
4 changed files with 35 additions and 23 deletions

View File

@@ -273,7 +273,7 @@ const Login: React.FC = () => {
textContentType='oneTimeCode'
clearButtonMode='while-editing'
maxLength={500}
extraClassName='mb-4'
extraClassName=''
/>
{/* Password */}
@@ -286,15 +286,15 @@ const Login: React.FC = () => {
placeholder={t("login.password_placeholder")}
showPassword={showPassword}
onShowPasswordChange={setShowPassword}
topPosition='15'
topOffset={15}
layout='tv'
/>
</View>
<View className='mt-4'>
<View className=''>
<Button onPress={handleLogin}>{t("login.login_button")}</Button>
</View>
<View className='mt-3'>
<View className=''>
<Button
onPress={handleQuickConnect}
className='bg-neutral-800 border border-neutral-700'
@@ -348,7 +348,7 @@ const Login: React.FC = () => {
</View>
{/* Lists stay full width but inside max width container */}
<View className='mt-2'>
<View className='mt-4'>
<JellyfinServerDiscovery
onServerSelect={async (server: any) => {
setServerURL(server.address);
@@ -402,10 +402,10 @@ const Login: React.FC = () => {
textContentType='oneTimeCode'
clearButtonMode='while-editing'
maxLength={500}
extraClassName='mb-4'
extraClassName=''
/>
<View className='relative mb-0.5'>
<View className='relative'>
<PasswordInput
value={credentials.password}
onChangeText={(text) =>
@@ -414,7 +414,7 @@ const Login: React.FC = () => {
placeholder={t("login.password_placeholder")}
showPassword={showPassword}
onShowPasswordChange={setShowPassword}
topPosition='10'
topOffset={12}
layout='mobile'
/>
</View>

View File

@@ -1,5 +1,6 @@
import { Ionicons } from "@expo/vector-icons";
import type React from "react";
import { useState } from "react";
import { TouchableOpacity } from "react-native";
import { Input } from "./common/Input";
@@ -10,7 +11,7 @@ type PasswordVisibilityControlled = {
placeholder: string;
showPassword: boolean;
onShowPasswordChange: (show: boolean) => void;
topPosition?: string;
topOffset?: number;
layout?: "tv" | "mobile";
};
@@ -20,8 +21,9 @@ type PasswordVisibilityUncontrolled = {
placeholder: string;
showPassword?: never;
onShowPasswordChange?: never;
topPosition?: string;
topOffset?: number;
layout?: "tv" | "mobile";
defaultShowPassword?: boolean;
};
type PasswordInputProps =
@@ -33,7 +35,7 @@ export const PasswordInput: React.FC<PasswordInputProps> = (props) => {
value = "",
onChangeText,
placeholder,
topPosition = "3.5",
topOffset = 14, // Default 14px for mobile
layout = "mobile",
} = props;
@@ -41,26 +43,36 @@ export const PasswordInput: React.FC<PasswordInputProps> = (props) => {
const isControlled =
"showPassword" in props && "onShowPasswordChange" in props;
// For controlled mode, use the provided props
// For uncontrolled mode, use internal state (but we need to handle this differently)
// Internal state for uncontrolled mode
const [internalShowPassword, setInternalShowPassword] = useState(() =>
!isControlled && "defaultShowPassword" in props
? ((props as PasswordVisibilityUncontrolled).defaultShowPassword ?? false)
: false,
);
// Use controlled value if available, otherwise use internal state
const showPassword = isControlled
? (props as PasswordVisibilityControlled).showPassword
: false;
: internalShowPassword;
const handleTogglePassword = () => {
if (isControlled) {
(props as PasswordVisibilityControlled).onShowPasswordChange(
!showPassword,
);
} else {
// For uncontrolled mode, toggle internal state
setInternalShowPassword(!showPassword);
}
// For uncontrolled mode, we could add internal state handling here if needed
};
// Generate top position with pixel precision
// Generate top position style with validation
const getTopStyle = () => {
// Use pixel values directly
const positionInPx = parseFloat(topPosition);
return { top: positionInPx };
if (typeof topOffset !== "number" || Number.isNaN(topOffset)) {
console.warn(`Invalid topOffset value: ${topOffset}`);
return { top: 14 }; // Default fallback (14px for mobile)
}
return { top: topOffset };
};
return (
@@ -70,7 +82,7 @@ export const PasswordInput: React.FC<PasswordInputProps> = (props) => {
onChangeText={onChangeText}
value={value}
secureTextEntry={!showPassword}
className='pr-4'
extraClassName='pr-4'
/>
<TouchableOpacity
onPress={handleTogglePassword}

View File

@@ -177,7 +177,7 @@ export const FilterSheet = <T,>({
{showSearch && (
<Input
placeholder={t("search.search")}
className='my-2 border-neutral-800 border'
extraClassName='my-2 border-neutral-800 border'
value={search}
onChangeText={(text) => {
setSearch(text);

View File

@@ -131,7 +131,7 @@ export const JellyseerrSettings = () => {
</Text>
</View>
<Input
className='border border-neutral-800 mb-2'
extraClassName='border border-neutral-800 mb-2'
placeholder={t(
"home.settings.plugins.jellyseerr.server_url_placeholder",
)}
@@ -161,7 +161,7 @@ export const JellyseerrSettings = () => {
showPassword={showJellyseerrPassword}
onShowPasswordChange={setShowJellyseerrPassword}
layout='mobile'
topPosition='11'
topOffset={11}
/>
</View>
<Button