fix: correct ModalHeaderSubmitButton naming and enhance UI components
- Renamed `ModalHeaderShubmitButton` to `ModalHeaderSubmitButton` for consistency. - Updated `SwipeableItem` to include animated text for action labels. - Refactored `TextField` to support an input postfix element and improved styling. - Enhanced `GroupedInsetListCard` to conditionally show separators between children. - Adjusted `MarkdownWeb` to prevent vertical overflow. - Updated `FollowFeed`, `FollowList`, and `rsshub-form` to use the corrected `ModalHeaderSubmitButton`. - Added navigation functionality to the `AddListButton` in `ListsScreen`. - Improved color validation in utility functions. These changes improve code consistency and enhance the user interface across various components. Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
parent
a084c2bc7d
commit
55dcfb06df
|
|
@ -30,16 +30,16 @@ const ModalHeaderCloseButtonImpl = () => {
|
|||
)
|
||||
}
|
||||
|
||||
export interface ModalHeaderShubmitButtonProps {
|
||||
export interface ModalHeaderSubmitButtonProps {
|
||||
isValid: boolean
|
||||
onPress: () => void
|
||||
isLoading?: boolean
|
||||
}
|
||||
export const ModalHeaderShubmitButton = ({
|
||||
export const ModalHeaderSubmitButton = ({
|
||||
isValid,
|
||||
onPress,
|
||||
isLoading,
|
||||
}: ModalHeaderShubmitButtonProps) => {
|
||||
}: ModalHeaderSubmitButtonProps) => {
|
||||
return <ModalHeaderShubmitButtonImpl isValid={isValid} onPress={onPress} isLoading={isLoading} />
|
||||
}
|
||||
|
||||
|
|
@ -47,8 +47,9 @@ const ModalHeaderShubmitButtonImpl = ({
|
|||
isValid,
|
||||
onPress,
|
||||
isLoading,
|
||||
}: ModalHeaderShubmitButtonProps) => {
|
||||
}: ModalHeaderSubmitButtonProps) => {
|
||||
const label = useColor("label")
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress} disabled={!isValid || isLoading}>
|
||||
{isLoading ? (
|
||||
|
|
|
|||
|
|
@ -118,6 +118,10 @@ export const SwipeableItem: React.FC<SwipeableItemProps> = ({
|
|||
const renderRightActions = (progress: Animated.AnimatedInterpolation<number>) => {
|
||||
const width = rightActions?.length ? rightActions.length * 74 : 74
|
||||
|
||||
const parallaxX = progress.interpolate({
|
||||
inputRange: [0, 1, 1.2],
|
||||
outputRange: [0, 0, 10],
|
||||
})
|
||||
return (
|
||||
<>
|
||||
<View
|
||||
|
|
@ -167,7 +171,15 @@ export const SwipeableItem: React.FC<SwipeableItemProps> = ({
|
|||
onPress={action.onPress}
|
||||
>
|
||||
{action.icon}
|
||||
<Text style={styles.actionText}>{action.label}</Text>
|
||||
<Animated.Text
|
||||
style={[
|
||||
styles.actionText,
|
||||
{ color: action.color ?? "#fff" },
|
||||
{ transform: [{ translateX: parallaxX }] },
|
||||
]}
|
||||
>
|
||||
{action.label}
|
||||
</Animated.Text>
|
||||
</RectButton>
|
||||
</Animated.View>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,25 +7,36 @@ import { accentColor } from "@/src/theme/colors"
|
|||
|
||||
import { FormLabel } from "./Label"
|
||||
|
||||
interface TextFieldProps {
|
||||
interface BaseFieldProps {
|
||||
wrapperClassName?: string
|
||||
wrapperStyle?: StyleProp<ViewStyle>
|
||||
|
||||
label?: string
|
||||
description?: string
|
||||
required?: boolean
|
||||
|
||||
inputPostfixElement?: React.ReactNode
|
||||
}
|
||||
|
||||
export const TextField = forwardRef<TextInput, TextInputProps & TextFieldProps>(
|
||||
const BaseField = forwardRef<TextInput, TextInputProps & BaseFieldProps>(
|
||||
(
|
||||
{ className, style, wrapperClassName, wrapperStyle, label, description, required, ...rest },
|
||||
{
|
||||
className,
|
||||
style,
|
||||
wrapperClassName,
|
||||
wrapperStyle,
|
||||
label,
|
||||
description,
|
||||
required,
|
||||
inputPostfixElement,
|
||||
...rest
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<>
|
||||
{!!label && <FormLabel className="pl-1" label={label} optional={!required} />}
|
||||
<View className="flex-1">
|
||||
{!!label && <FormLabel className="pl-2.5" label={label} optional={!required} />}
|
||||
{!!description && (
|
||||
<Text className="text-secondary-label mb-1 pl-1 text-sm">{description}</Text>
|
||||
<Text className="text-secondary-label mb-1 pl-2.5 text-sm">{description}</Text>
|
||||
)}
|
||||
<View
|
||||
className={cn(
|
||||
|
|
@ -37,16 +48,42 @@ export const TextField = forwardRef<TextInput, TextInputProps & TextFieldProps>(
|
|||
<TextInput
|
||||
selectionColor={accentColor}
|
||||
ref={ref}
|
||||
className={cn("text-text placeholder:text-secondary-label w-full flex-1", className)}
|
||||
className={cn("text-label placeholder:text-secondary-label w-full flex-1", className)}
|
||||
style={StyleSheet.flatten([styles.textField, style])}
|
||||
{...rest}
|
||||
/>
|
||||
{inputPostfixElement}
|
||||
</View>
|
||||
</>
|
||||
</View>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export const TextField = forwardRef<TextInput, TextInputProps & BaseFieldProps>((props, ref) => (
|
||||
<BaseField {...props} ref={ref} />
|
||||
))
|
||||
|
||||
interface NumberFieldProps extends BaseFieldProps {
|
||||
value?: number
|
||||
onChangeNumber?: (value: number) => void
|
||||
defaultValue?: number
|
||||
}
|
||||
|
||||
export const NumberField = forwardRef<
|
||||
TextInput,
|
||||
Omit<TextInputProps, "keyboardType" | "value" | "onChangeText" | "defaultValue"> &
|
||||
NumberFieldProps
|
||||
>(({ value, onChangeNumber, defaultValue, ...rest }, ref) => (
|
||||
<BaseField
|
||||
{...rest}
|
||||
ref={ref}
|
||||
keyboardType="number-pad"
|
||||
value={value?.toString()}
|
||||
onChangeText={(text) => onChangeNumber?.(Math.min(Number(text), Number.MAX_SAFE_INTEGER))}
|
||||
defaultValue={defaultValue?.toString()}
|
||||
/>
|
||||
))
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
textField: {
|
||||
fontSize: 16,
|
||||
|
|
|
|||
|
|
@ -9,11 +9,13 @@ import { MingcuteRightLine } from "@/src/icons/mingcute_right_line"
|
|||
import { RightCuteReIcon } from "@/src/icons/right_cute_re"
|
||||
import { useColor } from "@/src/theme/colors"
|
||||
|
||||
export const GroupedInsetListCard: FC<PropsWithChildren & ViewProps> = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
type GroupedInsetListCardProps = {
|
||||
showSeparator?: boolean
|
||||
}
|
||||
|
||||
export const GroupedInsetListCard: FC<
|
||||
PropsWithChildren & ViewProps & GroupedInsetListCardProps
|
||||
> = ({ children, className, showSeparator = true, ...props }) => {
|
||||
return (
|
||||
<View
|
||||
{...props}
|
||||
|
|
@ -22,26 +24,28 @@ export const GroupedInsetListCard: FC<PropsWithChildren & ViewProps> = ({
|
|||
className,
|
||||
)}
|
||||
>
|
||||
{React.Children.map(children, (child, index) => {
|
||||
const isLast = index === React.Children.count(children) - 1
|
||||
{showSeparator
|
||||
? React.Children.map(children, (child, index) => {
|
||||
const isLast = index === React.Children.count(children) - 1
|
||||
|
||||
const isNavigationLink =
|
||||
React.isValidElement(child) &&
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
(child.type as Function).name === GroupedInsetListNavigationLink.name
|
||||
const isNavigationLink =
|
||||
React.isValidElement(child) &&
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
(child.type as Function).name === GroupedInsetListNavigationLink.name
|
||||
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
{child}
|
||||
{!isLast && (
|
||||
<View
|
||||
className={cn("bg-opaque-separator", isNavigationLink ? "ml-16" : "mx-4")}
|
||||
style={{ height: StyleSheet.hairlineWidth }}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
{child}
|
||||
{!isLast && (
|
||||
<View
|
||||
className={cn("bg-opaque-separator", isNavigationLink ? "ml-16" : "mx-4")}
|
||||
style={{ height: StyleSheet.hairlineWidth }}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
)
|
||||
})
|
||||
: children}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ const MarkdownWeb: WebComponent<{
|
|||
overscroll-behavior: none;
|
||||
}
|
||||
body, html {
|
||||
overflow: hidden;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
`,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { z } from "zod"
|
|||
|
||||
import {
|
||||
ModalHeaderCloseButton,
|
||||
ModalHeaderShubmitButton,
|
||||
ModalHeaderSubmitButton,
|
||||
} from "@/src/components/common/ModalSharedComponents"
|
||||
import { FormProvider } from "@/src/components/ui/form/FormProvider"
|
||||
import { FormLabel } from "@/src/components/ui/form/Label"
|
||||
|
|
@ -108,7 +108,7 @@ function FollowImpl() {
|
|||
headerLeft: ModalHeaderCloseButton,
|
||||
gestureEnabled: !isDirty,
|
||||
headerRight: () => (
|
||||
<ModalHeaderShubmitButton
|
||||
<ModalHeaderSubmitButton
|
||||
isValid={isValid}
|
||||
onPress={form.handleSubmit(submit)}
|
||||
isLoading={isLoading}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import type { FeedViewType } from "@follow/constants"
|
||||
import { cn } from "@follow/utils"
|
||||
import { Text, TouchableOpacity, View } from "react-native"
|
||||
|
||||
import { Grid } from "@/src/components/ui/grid"
|
||||
|
|
@ -34,10 +33,10 @@ export const FeedViewSelector = ({ value, onChange, className, readOnly }: Props
|
|||
width={18}
|
||||
/>
|
||||
<Text
|
||||
className={cn(
|
||||
"mt-1 whitespace-nowrap text-[8px] font-medium",
|
||||
isSelected ? "text-accent" : "text-secondary-label",
|
||||
)}
|
||||
className={"mt-1 whitespace-nowrap text-[8px] font-medium"}
|
||||
style={{
|
||||
color: isSelected ? view.activeColor : secondaryLabelColor,
|
||||
}}
|
||||
>
|
||||
{view.name}
|
||||
</Text>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { z } from "zod"
|
|||
|
||||
import {
|
||||
ModalHeaderCloseButton,
|
||||
ModalHeaderShubmitButton,
|
||||
ModalHeaderSubmitButton,
|
||||
} from "@/src/components/common/ModalSharedComponents"
|
||||
import { FormProvider } from "@/src/components/ui/form/FormProvider"
|
||||
import { FormLabel } from "@/src/components/ui/form/Label"
|
||||
|
|
@ -118,7 +118,7 @@ const Impl = (props: { id: string }) => {
|
|||
headerLeft: ModalHeaderCloseButton,
|
||||
gestureEnabled: !isDirty,
|
||||
headerRight: () => (
|
||||
<ModalHeaderShubmitButton
|
||||
<ModalHeaderSubmitButton
|
||||
isValid={isValid}
|
||||
onPress={form.handleSubmit(submit)}
|
||||
isLoading={isLoading}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useQuery } from "@tanstack/react-query"
|
||||
import { router } from "expo-router"
|
||||
import { createElement, useCallback } from "react"
|
||||
import type { ListRenderItem } from "react-native"
|
||||
import { Image, StyleSheet, Text, TouchableOpacity, View } from "react-native"
|
||||
|
|
@ -84,7 +85,7 @@ export const ListsScreen = () => {
|
|||
const AddListButton = () => {
|
||||
const labelColor = useColor("label")
|
||||
return (
|
||||
<TouchableOpacity hitSlop={10}>
|
||||
<TouchableOpacity hitSlop={10} onPress={() => router.push("/list")}>
|
||||
<AddCuteReIcon height={20} width={20} color={labelColor} />
|
||||
</TouchableOpacity>
|
||||
)
|
||||
|
|
@ -109,7 +110,7 @@ const ListItemCell: ListRenderItem<HonoApiClient.List_List_Get> = ({ item: list
|
|||
{
|
||||
label: "Edit",
|
||||
onPress: () => {
|
||||
// TODO
|
||||
router.push(`/list?id=${list.id}`)
|
||||
},
|
||||
backgroundColor: "#0ea5e9",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -21,6 +21,12 @@ export default function ModalLayout() {
|
|||
title: "Follow",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="list"
|
||||
options={{
|
||||
title: "List",
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,162 @@
|
|||
import { FeedViewType } from "@follow/constants"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { Stack, useLocalSearchParams } from "expo-router"
|
||||
import { memo } from "react"
|
||||
import { Controller, useForm } from "react-hook-form"
|
||||
import { View } from "react-native"
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-controller"
|
||||
import { z } from "zod"
|
||||
|
||||
import {
|
||||
ModalHeaderCloseButton,
|
||||
ModalHeaderSubmitButton,
|
||||
} from "@/src/components/common/ModalSharedComponents"
|
||||
import { FormProvider, useFormContext } from "@/src/components/ui/form/FormProvider"
|
||||
import { FormLabel } from "@/src/components/ui/form/Label"
|
||||
import { NumberField, TextField } from "@/src/components/ui/form/TextField"
|
||||
import { GroupedInsetListCard } from "@/src/components/ui/grouped/GroupedList"
|
||||
import { PowerIcon } from "@/src/icons/power"
|
||||
import { FeedViewSelector } from "@/src/modules/feed/view-selector"
|
||||
import { useList } from "@/src/store/list/hooks"
|
||||
import { accentColor } from "@/src/theme/colors"
|
||||
|
||||
const listSchema = z.object({
|
||||
title: z.string().min(1),
|
||||
description: z.string().min(1),
|
||||
image: z.string().url(),
|
||||
fee: z.number().min(0),
|
||||
view: z.nativeEnum(FeedViewType),
|
||||
})
|
||||
export default function ListScreen() {
|
||||
const listId = useLocalSearchParams<{ id?: string }>().id
|
||||
|
||||
const list = useList(listId || "")
|
||||
const form = useForm({
|
||||
defaultValues: list,
|
||||
resolver: zodResolver(listSchema),
|
||||
mode: "all",
|
||||
})
|
||||
return (
|
||||
<FormProvider form={form}>
|
||||
<KeyboardAwareScrollView className="bg-system-grouped-background flex-1 pb-safe">
|
||||
<ScreenOptions title={list?.title} />
|
||||
|
||||
<GroupedInsetListCard showSeparator={false} className="mt-6 px-3 py-6">
|
||||
<Controller
|
||||
name="title"
|
||||
control={form.control}
|
||||
rules={{
|
||||
required: true,
|
||||
}}
|
||||
render={({ field: { onChange, onBlur, ref, value } }) => (
|
||||
<TextField
|
||||
label="Title"
|
||||
required={true}
|
||||
wrapperClassName="mt-2"
|
||||
placeholder=""
|
||||
onBlur={onBlur}
|
||||
onChangeText={onChange}
|
||||
defaultValue={list?.title ?? ""}
|
||||
value={value ?? ""}
|
||||
ref={ref}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<View className="mt-4">
|
||||
<Controller
|
||||
name="description"
|
||||
control={form.control}
|
||||
render={({ field: { onChange, onBlur, ref, value } }) => (
|
||||
<TextField
|
||||
label="Description"
|
||||
wrapperClassName="mt-2"
|
||||
placeholder=""
|
||||
onBlur={onBlur}
|
||||
onChangeText={onChange}
|
||||
defaultValue={list?.description ?? ""}
|
||||
value={value ?? ""}
|
||||
ref={ref}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className="mt-4">
|
||||
<Controller
|
||||
name="image"
|
||||
control={form.control}
|
||||
render={({ field: { onChange, onBlur, ref, value } }) => (
|
||||
<TextField
|
||||
label="Image"
|
||||
wrapperClassName="mt-2"
|
||||
placeholder="https://"
|
||||
onBlur={onBlur}
|
||||
onChangeText={onChange}
|
||||
defaultValue={list?.image ?? ""}
|
||||
value={value ?? ""}
|
||||
ref={ref}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className="mt-4">
|
||||
<FormLabel label="View" className="mb-4 pl-2.5" optional />
|
||||
<Controller
|
||||
name="view"
|
||||
control={form.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<FeedViewSelector value={value as any as FeedViewType} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className="mt-4">
|
||||
<Controller
|
||||
name="fee"
|
||||
control={form.control}
|
||||
render={({ field: { onChange, onBlur, ref, value } }) => (
|
||||
<NumberField
|
||||
label="Fee"
|
||||
wrapperClassName="mt-2"
|
||||
placeholder="0"
|
||||
onBlur={onBlur}
|
||||
onChangeNumber={onChange}
|
||||
defaultValue={list?.fee ?? 0}
|
||||
value={value ?? 0}
|
||||
inputPostfixElement={<PowerIcon color={accentColor} />}
|
||||
ref={ref}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
</GroupedInsetListCard>
|
||||
</KeyboardAwareScrollView>
|
||||
</FormProvider>
|
||||
)
|
||||
}
|
||||
|
||||
interface ScreenOptionsProps {
|
||||
title?: string
|
||||
}
|
||||
const ScreenOptions = memo(({ title }: ScreenOptionsProps) => {
|
||||
const form = useFormContext()
|
||||
|
||||
return (
|
||||
<Stack.Screen
|
||||
options={{
|
||||
headerLeft: ModalHeaderCloseButton,
|
||||
gestureEnabled: !form.formState.isDirty,
|
||||
|
||||
headerRight: () => (
|
||||
<FormProvider form={form}>
|
||||
<ModalHeaderSubmitButton isValid onPress={() => {}} />
|
||||
</FormProvider>
|
||||
),
|
||||
|
||||
headerTitle: title ? `Edit List - ${title}` : "Create List",
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
|
@ -18,7 +18,7 @@ import { z } from "zod"
|
|||
import { HeaderTitleExtra } from "@/src/components/common/HeaderTitleExtra"
|
||||
import {
|
||||
ModalHeaderCloseButton,
|
||||
ModalHeaderShubmitButton,
|
||||
ModalHeaderSubmitButton,
|
||||
} from "@/src/components/common/ModalSharedComponents"
|
||||
import { FormProvider, useFormContext } from "@/src/components/ui/form/FormProvider"
|
||||
import { Select } from "@/src/components/ui/form/Select"
|
||||
|
|
@ -247,7 +247,7 @@ const ScreenOptions = memo(({ name, routeName, route, routePrefix }: ScreenOptio
|
|||
|
||||
headerRight: () => (
|
||||
<FormProvider form={form}>
|
||||
<ModalHeaderSubmitButton routePrefix={routePrefix} route={route} />
|
||||
<ModalHeaderSubmitButtonImpl routePrefix={routePrefix} route={route} />
|
||||
</FormProvider>
|
||||
),
|
||||
|
||||
|
|
@ -267,17 +267,15 @@ const Title = ({ name, routeName, route, routePrefix }: ScreenOptionsProps) => {
|
|||
)
|
||||
}
|
||||
|
||||
type ModalHeaderSubmitButtonProps = {
|
||||
routePrefix: string
|
||||
route: string
|
||||
}
|
||||
const ModalHeaderSubmitButton = ({ routePrefix, route }: ModalHeaderSubmitButtonProps) => {
|
||||
return <ModalHeaderSubmitButtonImpl routePrefix={routePrefix} route={route} />
|
||||
}
|
||||
|
||||
const routeParamsKeyPrefix = "route-params-"
|
||||
|
||||
const ModalHeaderSubmitButtonImpl = ({ routePrefix, route }: ModalHeaderSubmitButtonProps) => {
|
||||
const ModalHeaderSubmitButtonImpl = ({
|
||||
routePrefix,
|
||||
route,
|
||||
}: {
|
||||
routePrefix: string
|
||||
route: string
|
||||
}) => {
|
||||
const form = useFormContext()
|
||||
const { isValid } = form.formState
|
||||
|
||||
|
|
@ -339,5 +337,5 @@ const ModalHeaderSubmitButtonImpl = ({ routePrefix, route }: ModalHeaderSubmitBu
|
|||
}
|
||||
})
|
||||
|
||||
return <ModalHeaderShubmitButton isLoading={isLoading} isValid={isValid} onPress={submit} />
|
||||
return <ModalHeaderSubmitButton isLoading={isLoading} isValid={isValid} onPress={submit} />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ export const isRGBColor = (color: string) => {
|
|||
return /^rgb\(\d{1,3},\s*\d{1,3},\s*\d{1,3}\)$/.test(color)
|
||||
}
|
||||
export const isRGBAColor = (color: string) => {
|
||||
return /^rgba\(\d{1,3},\s*\d{1,3},\s*\d{1,3},\s*0?\.\d+\)$/.test(color)
|
||||
return /^rgba\(\d{1,3},\s*\d{1,3},\s*\d{1,3},\s*(?:0?\.\d+|1(?:\.0+)?)\)$/.test(color)
|
||||
}
|
||||
|
||||
export const withOpacity = (color: string, opacity: number) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue