@atlure/ui 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -2
- package/src/components/button/button.tsx +6 -3
- package/src/components/button/types.ts +17 -7
- package/src/components/card/card-section.tsx +17 -1
- package/src/components/card/card.tsx +2 -0
- package/src/components/empty-state/empty-state.tsx +42 -0
- package/src/components/error-state/error-state.tsx +45 -0
- package/src/components/form-field/form-field-context.tsx +28 -0
- package/src/components/form-field/form-field.tsx +74 -0
- package/src/components/form-scroll-view/form-scroll-view.tsx +36 -0
- package/src/components/icon-button/icon-button.tsx +41 -0
- package/src/components/input/input.tsx +61 -10
- package/src/components/label/label.tsx +12 -2
- package/src/components/list-row/list-row.tsx +78 -0
- package/src/components/screen-state/screen-state.tsx +35 -0
- package/src/components/search-bar/hooks/use-debounced-callback.ts +35 -0
- package/src/components/search-bar/search-bar.tsx +60 -0
- package/src/components/separator/separator.tsx +2 -0
- package/src/components/skeleton/hooks/use-reduced-motion.ts +28 -0
- package/src/components/skeleton/skeleton-compositions.tsx +68 -0
- package/src/components/skeleton/skeleton.tsx +4 -1
- package/src/components/skeleton/utils.ts +11 -0
- package/src/components/spinner/spinner.tsx +37 -0
- package/src/components/text/text-class-context.tsx +19 -0
- package/src/components/text/text.tsx +13 -4
- package/src/components/text/utils.ts +16 -0
- package/src/components/textarea/textarea.tsx +50 -0
- package/src/index.ts +20 -0
- package/src/lib/cn.ts +7 -3
- package/src/variants/button-variants.ts +4 -0
- package/src/variants/form-field-variants.ts +17 -0
- package/src/variants/form-scroll-view-variants.ts +3 -0
- package/src/variants/input-variants.ts +26 -1
- package/src/variants/label-variants.ts +2 -0
- package/src/variants/list-row-variants.ts +24 -0
- package/src/variants/search-bar-variants.ts +1 -0
- package/src/variants/skeleton-variants.ts +15 -0
- package/src/variants/spinner-variants.ts +15 -0
- package/src/variants/state-variants.ts +7 -0
- package/src/variants/text-variants.ts +8 -6
- package/src/variants/textarea-variants.ts +20 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atlure/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Atlure React Native component library: NativeWind primitives built on @atlure/tokens, shipped as untranspiled TypeScript source",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "David Moreira",
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"class-variance-authority": "0.7.1",
|
|
38
38
|
"clsx": "2.1.1",
|
|
39
39
|
"tailwind-merge": "2.6.0",
|
|
40
|
-
"@atlure/
|
|
40
|
+
"@atlure/icons": "0.4.0",
|
|
41
|
+
"@atlure/tokens": "0.4.0"
|
|
41
42
|
},
|
|
42
43
|
"peerDependencies": {
|
|
43
44
|
"nativewind": "^4.2.6",
|
|
@@ -45,6 +46,7 @@
|
|
|
45
46
|
"react-native": ">=0.81.0",
|
|
46
47
|
"react-native-reanimated": ">=3.16.0",
|
|
47
48
|
"react-native-safe-area-context": ">=4.10.0",
|
|
49
|
+
"react-native-svg": ">=15.0.0",
|
|
48
50
|
"tailwindcss": "^3.4.19"
|
|
49
51
|
},
|
|
50
52
|
"peerDependenciesMeta": {
|
|
@@ -3,6 +3,7 @@ import { ActivityIndicator, Pressable } from "react-native";
|
|
|
3
3
|
import { cn } from "../../lib/cn";
|
|
4
4
|
import { touchTargetHitSlop } from "../../lib/touch-target";
|
|
5
5
|
import { buttonLabelVariants, buttonVariants } from "../../variants/button-variants";
|
|
6
|
+
import { TextClassProvider } from "../text/text-class-context";
|
|
6
7
|
import { Text } from "../text/text";
|
|
7
8
|
import type { ButtonProps } from "./types";
|
|
8
9
|
|
|
@@ -39,9 +40,11 @@ export function Button({
|
|
|
39
40
|
)}
|
|
40
41
|
{...pressableProps}
|
|
41
42
|
>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
<TextClassProvider className={buttonLabelVariants({ variant, size })}>
|
|
44
|
+
{isLoading ? <ActivityIndicator size="small" /> : leadingIcon}
|
|
45
|
+
<Text className={labelClassName}>{label}</Text>
|
|
46
|
+
{!isLoading && trailingIcon}
|
|
47
|
+
</TextClassProvider>
|
|
45
48
|
</Pressable>
|
|
46
49
|
);
|
|
47
50
|
}
|
|
@@ -1,21 +1,31 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
2
|
-
import type { PressableProps } from "react-native";
|
|
1
|
+
import type { ComponentRef, ReactNode, Ref } from "react";
|
|
2
|
+
import type { Pressable, PressableProps } from "react-native";
|
|
3
3
|
|
|
4
4
|
import type { ControlSize } from "../../lib/touch-target";
|
|
5
5
|
import type { ButtonVariantProps } from "../../variants/button-variants";
|
|
6
6
|
|
|
7
|
+
export type ButtonVariant = NonNullable<ButtonVariantProps["variant"]>;
|
|
7
8
|
export type ButtonSize = Extract<ControlSize, "sm" | "md" | "lg">;
|
|
8
9
|
|
|
9
|
-
export interface
|
|
10
|
+
export interface PressableButtonProps
|
|
10
11
|
extends Omit<PressableProps, "children" | "disabled" | "accessibilityState"> {
|
|
11
|
-
|
|
12
|
-
variant?: NonNullable<ButtonVariantProps["variant"]>;
|
|
13
|
-
size?: ButtonSize;
|
|
12
|
+
variant?: ButtonVariant;
|
|
14
13
|
isFullWidth?: boolean;
|
|
15
14
|
isDisabled?: boolean;
|
|
16
15
|
isLoading?: boolean;
|
|
16
|
+
className?: string;
|
|
17
|
+
ref?: Ref<ComponentRef<typeof Pressable>>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ButtonProps extends PressableButtonProps {
|
|
21
|
+
label: string;
|
|
22
|
+
size?: ButtonSize;
|
|
17
23
|
leadingIcon?: ReactNode;
|
|
18
24
|
trailingIcon?: ReactNode;
|
|
19
|
-
className?: string;
|
|
20
25
|
labelClassName?: string;
|
|
21
26
|
}
|
|
27
|
+
|
|
28
|
+
export interface IconButtonProps extends PressableButtonProps {
|
|
29
|
+
icon: ReactNode;
|
|
30
|
+
accessibilityLabel: string;
|
|
31
|
+
}
|
|
@@ -1,14 +1,30 @@
|
|
|
1
|
+
import type { ComponentRef, Ref } from "react";
|
|
1
2
|
import { View, type ViewProps } from "react-native";
|
|
2
3
|
|
|
3
4
|
import { cn } from "../../lib/cn";
|
|
4
5
|
import { cardSectionVariants } from "../../variants/card-variants";
|
|
6
|
+
import { Text, type TextProps } from "../text/text";
|
|
5
7
|
|
|
6
|
-
export interface CardSectionProps extends ViewProps {
|
|
8
|
+
export interface CardSectionProps extends ViewProps {
|
|
9
|
+
ref?: Ref<ComponentRef<typeof View>>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type CardTitleProps = Omit<TextProps, "variant">;
|
|
13
|
+
|
|
14
|
+
export type CardDescriptionProps = Omit<TextProps, "variant">;
|
|
7
15
|
|
|
8
16
|
export function CardHeader({ className, ...viewProps }: CardSectionProps) {
|
|
9
17
|
return <View className={cn(cardSectionVariants({ section: "header" }), className)} {...viewProps} />;
|
|
10
18
|
}
|
|
11
19
|
|
|
20
|
+
export function CardTitle(textProps: CardTitleProps) {
|
|
21
|
+
return <Text accessibilityRole="header" variant="h3" {...textProps} />;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function CardDescription(textProps: CardDescriptionProps) {
|
|
25
|
+
return <Text variant="bodySm" tone="muted" {...textProps} />;
|
|
26
|
+
}
|
|
27
|
+
|
|
12
28
|
export function CardContent({ className, ...viewProps }: CardSectionProps) {
|
|
13
29
|
return (
|
|
14
30
|
<View className={cn(cardSectionVariants({ section: "content" }), className)} {...viewProps} />
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ComponentRef, Ref } from "react";
|
|
1
2
|
import { Pressable, View, type ViewProps } from "react-native";
|
|
2
3
|
|
|
3
4
|
import { cn } from "../../lib/cn";
|
|
@@ -7,6 +8,7 @@ export interface CardProps extends ViewProps, Pick<CardVariantProps, "variant">
|
|
|
7
8
|
onPress?: () => void;
|
|
8
9
|
accessibilityLabel?: string;
|
|
9
10
|
isDisabled?: boolean;
|
|
11
|
+
ref?: Ref<ComponentRef<typeof View>>;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
export function Card({
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { View, type ViewProps } from "react-native";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../../lib/cn";
|
|
5
|
+
import {
|
|
6
|
+
stateActionClassName,
|
|
7
|
+
stateIconClassName,
|
|
8
|
+
stateLayoutClassName,
|
|
9
|
+
stateTextClassName,
|
|
10
|
+
} from "../../variants/state-variants";
|
|
11
|
+
import { Text } from "../text/text";
|
|
12
|
+
|
|
13
|
+
export interface EmptyStateProps extends Omit<ViewProps, "children"> {
|
|
14
|
+
title: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
icon?: ReactNode;
|
|
17
|
+
action?: ReactNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function EmptyState({
|
|
21
|
+
title,
|
|
22
|
+
description,
|
|
23
|
+
icon,
|
|
24
|
+
action,
|
|
25
|
+
className,
|
|
26
|
+
...viewProps
|
|
27
|
+
}: EmptyStateProps) {
|
|
28
|
+
return (
|
|
29
|
+
<View className={cn(stateLayoutClassName, className)} {...viewProps}>
|
|
30
|
+
{icon ? <View className={stateIconClassName}>{icon}</View> : null}
|
|
31
|
+
<Text accessibilityRole="header" variant="h3" className={stateTextClassName}>
|
|
32
|
+
{title}
|
|
33
|
+
</Text>
|
|
34
|
+
{description ? (
|
|
35
|
+
<Text variant="bodySm" tone="muted" className={stateTextClassName}>
|
|
36
|
+
{description}
|
|
37
|
+
</Text>
|
|
38
|
+
) : null}
|
|
39
|
+
{action ? <View className={stateActionClassName}>{action}</View> : null}
|
|
40
|
+
</View>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { View, type ViewProps } from "react-native";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../../lib/cn";
|
|
5
|
+
import {
|
|
6
|
+
stateActionClassName,
|
|
7
|
+
stateIconClassName,
|
|
8
|
+
stateLayoutClassName,
|
|
9
|
+
stateTextClassName,
|
|
10
|
+
} from "../../variants/state-variants";
|
|
11
|
+
import { Button } from "../button/button";
|
|
12
|
+
import { Text } from "../text/text";
|
|
13
|
+
|
|
14
|
+
export interface ErrorStateProps extends Omit<ViewProps, "children"> {
|
|
15
|
+
title: string;
|
|
16
|
+
message: string;
|
|
17
|
+
retryLabel: string;
|
|
18
|
+
onRetry: () => void;
|
|
19
|
+
icon?: ReactNode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function ErrorState({
|
|
23
|
+
title,
|
|
24
|
+
message,
|
|
25
|
+
retryLabel,
|
|
26
|
+
onRetry,
|
|
27
|
+
icon,
|
|
28
|
+
className,
|
|
29
|
+
...viewProps
|
|
30
|
+
}: ErrorStateProps) {
|
|
31
|
+
return (
|
|
32
|
+
<View accessibilityRole="alert" className={cn(stateLayoutClassName, className)} {...viewProps}>
|
|
33
|
+
{icon ? <View className={stateIconClassName}>{icon}</View> : null}
|
|
34
|
+
<Text accessibilityRole="header" variant="h3" className={stateTextClassName}>
|
|
35
|
+
{title}
|
|
36
|
+
</Text>
|
|
37
|
+
<Text variant="bodySm" tone="muted" className={stateTextClassName}>
|
|
38
|
+
{message}
|
|
39
|
+
</Text>
|
|
40
|
+
<View className={stateActionClassName}>
|
|
41
|
+
<Button variant="secondary" label={retryLabel} onPress={onRetry} />
|
|
42
|
+
</View>
|
|
43
|
+
</View>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createContext, useContext, type ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export interface FormFieldControl {
|
|
4
|
+
nativeID: string;
|
|
5
|
+
labelledBy: string | undefined;
|
|
6
|
+
describedBy: string | undefined;
|
|
7
|
+
isInvalid: boolean;
|
|
8
|
+
isDisabled: boolean;
|
|
9
|
+
isRequired: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const FormFieldContext = createContext<FormFieldControl | undefined>(undefined);
|
|
13
|
+
|
|
14
|
+
export function FormFieldProvider({
|
|
15
|
+
control,
|
|
16
|
+
children,
|
|
17
|
+
}: {
|
|
18
|
+
control: FormFieldControl;
|
|
19
|
+
children: ReactNode;
|
|
20
|
+
}) {
|
|
21
|
+
return <FormFieldContext.Provider value={control}>{children}</FormFieldContext.Provider>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function useFormFieldControl(): FormFieldControl | undefined {
|
|
25
|
+
return useContext(FormFieldContext);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { FormFieldContext };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useId, type ReactNode } from "react";
|
|
2
|
+
import { View, type ViewProps } from "react-native";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../../lib/cn";
|
|
5
|
+
import { formFieldClassName, formFieldMessageVariants } from "../../variants/form-field-variants";
|
|
6
|
+
import { Label } from "../label/label";
|
|
7
|
+
import { Text } from "../text/text";
|
|
8
|
+
import { FormFieldProvider } from "./form-field-context";
|
|
9
|
+
|
|
10
|
+
export interface FormFieldProps extends Omit<ViewProps, "children"> {
|
|
11
|
+
label?: string;
|
|
12
|
+
helperText?: string;
|
|
13
|
+
error?: string;
|
|
14
|
+
isRequired?: boolean;
|
|
15
|
+
isDisabled?: boolean;
|
|
16
|
+
nativeID?: string;
|
|
17
|
+
children: ReactNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function FormField({
|
|
21
|
+
label,
|
|
22
|
+
helperText,
|
|
23
|
+
error,
|
|
24
|
+
isRequired = false,
|
|
25
|
+
isDisabled = false,
|
|
26
|
+
nativeID,
|
|
27
|
+
className,
|
|
28
|
+
children,
|
|
29
|
+
...viewProps
|
|
30
|
+
}: FormFieldProps) {
|
|
31
|
+
const generatedID = useId();
|
|
32
|
+
const controlID = nativeID ?? generatedID;
|
|
33
|
+
const labelID = `${controlID}-label`;
|
|
34
|
+
const messageID = `${controlID}-message`;
|
|
35
|
+
const isInvalid = Boolean(error);
|
|
36
|
+
const message = error ?? helperText;
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<View className={cn(formFieldClassName, className)} {...viewProps}>
|
|
40
|
+
{label ? (
|
|
41
|
+
<Label
|
|
42
|
+
nativeID={labelID}
|
|
43
|
+
isDisabled={isDisabled}
|
|
44
|
+
isInvalid={isInvalid}
|
|
45
|
+
isRequired={isRequired}
|
|
46
|
+
>
|
|
47
|
+
{label}
|
|
48
|
+
</Label>
|
|
49
|
+
) : null}
|
|
50
|
+
<FormFieldProvider
|
|
51
|
+
control={{
|
|
52
|
+
nativeID: controlID,
|
|
53
|
+
labelledBy: label ? labelID : undefined,
|
|
54
|
+
describedBy: message ? messageID : undefined,
|
|
55
|
+
isInvalid,
|
|
56
|
+
isDisabled,
|
|
57
|
+
isRequired,
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
{children}
|
|
61
|
+
</FormFieldProvider>
|
|
62
|
+
{message ? (
|
|
63
|
+
<Text
|
|
64
|
+
nativeID={messageID}
|
|
65
|
+
variant="caption"
|
|
66
|
+
className={formFieldMessageVariants({ isInvalid })}
|
|
67
|
+
accessibilityLiveRegion={isInvalid ? "polite" : "none"}
|
|
68
|
+
>
|
|
69
|
+
{message}
|
|
70
|
+
</Text>
|
|
71
|
+
) : null}
|
|
72
|
+
</View>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { KeyboardAvoidingView, Platform, ScrollView, type ScrollViewProps } from "react-native";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../../lib/cn";
|
|
5
|
+
import {
|
|
6
|
+
formScrollViewClassName,
|
|
7
|
+
formScrollViewContentClassName,
|
|
8
|
+
} from "../../variants/form-scroll-view-variants";
|
|
9
|
+
|
|
10
|
+
export interface FormScrollViewProps extends Omit<ScrollViewProps, "children"> {
|
|
11
|
+
contentContainerClassName?: string;
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function FormScrollView({
|
|
16
|
+
className,
|
|
17
|
+
contentContainerClassName,
|
|
18
|
+
children,
|
|
19
|
+
...scrollViewProps
|
|
20
|
+
}: FormScrollViewProps) {
|
|
21
|
+
return (
|
|
22
|
+
<KeyboardAvoidingView
|
|
23
|
+
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
|
24
|
+
className={cn(formScrollViewClassName, className)}
|
|
25
|
+
>
|
|
26
|
+
<ScrollView
|
|
27
|
+
keyboardShouldPersistTaps="handled"
|
|
28
|
+
keyboardDismissMode="interactive"
|
|
29
|
+
contentContainerClassName={cn(formScrollViewContentClassName, contentContainerClassName)}
|
|
30
|
+
{...scrollViewProps}
|
|
31
|
+
>
|
|
32
|
+
{children}
|
|
33
|
+
</ScrollView>
|
|
34
|
+
</KeyboardAvoidingView>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ActivityIndicator, Pressable } from "react-native";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
import { touchTargetHitSlop } from "../../lib/touch-target";
|
|
5
|
+
import { buttonLabelVariants, buttonVariants } from "../../variants/button-variants";
|
|
6
|
+
import { TextClassProvider } from "../text/text-class-context";
|
|
7
|
+
import type { IconButtonProps } from "../button/types";
|
|
8
|
+
|
|
9
|
+
export function IconButton({
|
|
10
|
+
icon,
|
|
11
|
+
accessibilityLabel,
|
|
12
|
+
variant = "primary",
|
|
13
|
+
isFullWidth = false,
|
|
14
|
+
isDisabled = false,
|
|
15
|
+
isLoading = false,
|
|
16
|
+
className,
|
|
17
|
+
...pressableProps
|
|
18
|
+
}: IconButtonProps) {
|
|
19
|
+
const isPressBlocked = isDisabled || isLoading;
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<Pressable
|
|
23
|
+
accessibilityRole="button"
|
|
24
|
+
accessibilityLabel={accessibilityLabel}
|
|
25
|
+
accessibilityState={{ disabled: isPressBlocked, busy: isLoading }}
|
|
26
|
+
aria-disabled={isPressBlocked}
|
|
27
|
+
aria-busy={isLoading}
|
|
28
|
+
disabled={isPressBlocked}
|
|
29
|
+
hitSlop={touchTargetHitSlop("icon")}
|
|
30
|
+
className={cn(
|
|
31
|
+
buttonVariants({ variant, size: "icon", isFullWidth, isDisabled: isPressBlocked }),
|
|
32
|
+
className,
|
|
33
|
+
)}
|
|
34
|
+
{...pressableProps}
|
|
35
|
+
>
|
|
36
|
+
<TextClassProvider className={buttonLabelVariants({ variant, size: "icon" })}>
|
|
37
|
+
{isLoading ? <ActivityIndicator size="small" /> : icon}
|
|
38
|
+
</TextClassProvider>
|
|
39
|
+
</Pressable>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -1,34 +1,85 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ComponentRef, ReactNode, Ref } from "react";
|
|
2
|
+
import { TextInput, type TextInputProps, View } from "react-native";
|
|
2
3
|
|
|
3
4
|
import { cn } from "../../lib/cn";
|
|
4
5
|
import { touchTargetHitSlop } from "../../lib/touch-target";
|
|
5
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
inputFieldWrapperClassName,
|
|
8
|
+
inputIconSlotVariants,
|
|
9
|
+
inputVariants,
|
|
10
|
+
type InputVariantProps,
|
|
11
|
+
} from "../../variants/input-variants";
|
|
12
|
+
import { useFormFieldControl } from "../form-field/form-field-context";
|
|
6
13
|
|
|
7
14
|
export interface InputProps extends Omit<TextInputProps, "editable" | "multiline"> {
|
|
8
15
|
size?: NonNullable<InputVariantProps["size"]>;
|
|
9
16
|
isDisabled?: boolean;
|
|
10
17
|
isInvalid?: boolean;
|
|
18
|
+
isRequired?: boolean;
|
|
11
19
|
isMultiline?: boolean;
|
|
20
|
+
leadingIcon?: ReactNode;
|
|
21
|
+
trailingIcon?: ReactNode;
|
|
22
|
+
ref?: Ref<ComponentRef<typeof TextInput>>;
|
|
12
23
|
}
|
|
13
24
|
|
|
14
25
|
export function Input({
|
|
15
26
|
size = "md",
|
|
16
|
-
isDisabled
|
|
17
|
-
isInvalid
|
|
27
|
+
isDisabled,
|
|
28
|
+
isInvalid,
|
|
29
|
+
isRequired,
|
|
18
30
|
isMultiline = false,
|
|
31
|
+
leadingIcon,
|
|
32
|
+
trailingIcon,
|
|
19
33
|
className,
|
|
34
|
+
nativeID,
|
|
20
35
|
...textInputProps
|
|
21
36
|
}: InputProps) {
|
|
22
|
-
|
|
37
|
+
const field = useFormFieldControl();
|
|
38
|
+
const isControlDisabled = isDisabled ?? field?.isDisabled ?? false;
|
|
39
|
+
const isControlInvalid = isInvalid ?? field?.isInvalid ?? false;
|
|
40
|
+
const isControlRequired = isRequired ?? field?.isRequired ?? false;
|
|
41
|
+
|
|
42
|
+
const control = (
|
|
23
43
|
<TextInput
|
|
24
|
-
accessibilityState={{ disabled:
|
|
25
|
-
|
|
26
|
-
aria-
|
|
27
|
-
|
|
44
|
+
accessibilityState={{ disabled: isControlDisabled }}
|
|
45
|
+
accessibilityLabelledBy={field?.labelledBy}
|
|
46
|
+
aria-labelledby={field?.labelledBy}
|
|
47
|
+
aria-describedby={field?.describedBy}
|
|
48
|
+
aria-disabled={isControlDisabled}
|
|
49
|
+
aria-invalid={isControlInvalid}
|
|
50
|
+
aria-required={isControlRequired}
|
|
51
|
+
nativeID={nativeID ?? field?.nativeID}
|
|
52
|
+
editable={!isControlDisabled}
|
|
28
53
|
multiline={isMultiline}
|
|
29
54
|
hitSlop={touchTargetHitSlop(size)}
|
|
30
|
-
className={cn(
|
|
55
|
+
className={cn(
|
|
56
|
+
inputVariants({
|
|
57
|
+
size,
|
|
58
|
+
isInvalid: isControlInvalid,
|
|
59
|
+
isDisabled: isControlDisabled,
|
|
60
|
+
isMultiline,
|
|
61
|
+
hasLeadingIcon: Boolean(leadingIcon),
|
|
62
|
+
hasTrailingIcon: Boolean(trailingIcon),
|
|
63
|
+
}),
|
|
64
|
+
className,
|
|
65
|
+
)}
|
|
31
66
|
{...textInputProps}
|
|
32
67
|
/>
|
|
33
68
|
);
|
|
69
|
+
|
|
70
|
+
if (!leadingIcon && !trailingIcon) {
|
|
71
|
+
return control;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<View className={inputFieldWrapperClassName}>
|
|
76
|
+
{leadingIcon ? (
|
|
77
|
+
<View className={inputIconSlotVariants({ slot: "leading" })}>{leadingIcon}</View>
|
|
78
|
+
) : null}
|
|
79
|
+
{control}
|
|
80
|
+
{trailingIcon ? (
|
|
81
|
+
<View className={inputIconSlotVariants({ slot: "trailing" })}>{trailingIcon}</View>
|
|
82
|
+
) : null}
|
|
83
|
+
</View>
|
|
84
|
+
);
|
|
34
85
|
}
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { cn } from "../../lib/cn";
|
|
2
|
-
import { labelVariants } from "../../variants/label-variants";
|
|
2
|
+
import { labelRequiredMarkerClassName, labelVariants } from "../../variants/label-variants";
|
|
3
3
|
import { Text, type TextProps } from "../text/text";
|
|
4
4
|
|
|
5
5
|
export interface LabelProps extends Omit<TextProps, "variant" | "tone"> {
|
|
6
6
|
isDisabled?: boolean;
|
|
7
7
|
isInvalid?: boolean;
|
|
8
|
+
isRequired?: boolean;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export function Label({
|
|
11
12
|
isDisabled = false,
|
|
12
13
|
isInvalid = false,
|
|
14
|
+
isRequired = false,
|
|
13
15
|
className,
|
|
16
|
+
children,
|
|
14
17
|
...textProps
|
|
15
18
|
}: LabelProps) {
|
|
16
19
|
return (
|
|
@@ -18,6 +21,13 @@ export function Label({
|
|
|
18
21
|
accessibilityRole="text"
|
|
19
22
|
className={cn(labelVariants({ isDisabled, isInvalid }), className)}
|
|
20
23
|
{...textProps}
|
|
21
|
-
|
|
24
|
+
>
|
|
25
|
+
{children}
|
|
26
|
+
{isRequired ? (
|
|
27
|
+
<Text aria-hidden accessibilityElementsHidden className={labelRequiredMarkerClassName}>
|
|
28
|
+
{" *"}
|
|
29
|
+
</Text>
|
|
30
|
+
) : null}
|
|
31
|
+
</Text>
|
|
22
32
|
);
|
|
23
33
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { ChevronRight, iconSize } from "@atlure/icons";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { Pressable, type PressableProps, View } from "react-native";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../lib/cn";
|
|
6
|
+
import { touchTargetHitSlop } from "../../lib/touch-target";
|
|
7
|
+
import {
|
|
8
|
+
listRowChevronClassName,
|
|
9
|
+
listRowTextClassName,
|
|
10
|
+
listRowTrailingClassName,
|
|
11
|
+
listRowVariants,
|
|
12
|
+
} from "../../variants/list-row-variants";
|
|
13
|
+
import { Text } from "../text/text";
|
|
14
|
+
|
|
15
|
+
export interface ListRowProps
|
|
16
|
+
extends Omit<PressableProps, "children" | "disabled" | "accessibilityState"> {
|
|
17
|
+
title: string;
|
|
18
|
+
subtitle?: string;
|
|
19
|
+
leading?: ReactNode;
|
|
20
|
+
trailing?: ReactNode;
|
|
21
|
+
hasChevron?: boolean;
|
|
22
|
+
isDisabled?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function ListRow({
|
|
26
|
+
title,
|
|
27
|
+
subtitle,
|
|
28
|
+
leading,
|
|
29
|
+
trailing,
|
|
30
|
+
hasChevron = false,
|
|
31
|
+
isDisabled = false,
|
|
32
|
+
onPress,
|
|
33
|
+
accessibilityLabel,
|
|
34
|
+
className,
|
|
35
|
+
...pressableProps
|
|
36
|
+
}: ListRowProps) {
|
|
37
|
+
const content = (
|
|
38
|
+
<>
|
|
39
|
+
{leading}
|
|
40
|
+
<View className={listRowTextClassName}>
|
|
41
|
+
<Text variant="body">{title}</Text>
|
|
42
|
+
{subtitle ? (
|
|
43
|
+
<Text variant="bodySm" tone="muted">
|
|
44
|
+
{subtitle}
|
|
45
|
+
</Text>
|
|
46
|
+
) : null}
|
|
47
|
+
</View>
|
|
48
|
+
{trailing ? <View className={listRowTrailingClassName}>{trailing}</View> : null}
|
|
49
|
+
{hasChevron ? (
|
|
50
|
+
<ChevronRight size={iconSize.md} className={listRowChevronClassName} />
|
|
51
|
+
) : null}
|
|
52
|
+
</>
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
if (!onPress) {
|
|
56
|
+
return (
|
|
57
|
+
<View className={cn(listRowVariants({ isDisabled }), className)}>
|
|
58
|
+
{content}
|
|
59
|
+
</View>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<Pressable
|
|
65
|
+
accessibilityRole="button"
|
|
66
|
+
accessibilityLabel={accessibilityLabel ?? title}
|
|
67
|
+
accessibilityState={{ disabled: isDisabled }}
|
|
68
|
+
aria-disabled={isDisabled}
|
|
69
|
+
disabled={isDisabled}
|
|
70
|
+
hitSlop={touchTargetHitSlop("md")}
|
|
71
|
+
onPress={onPress}
|
|
72
|
+
className={cn(listRowVariants({ isDisabled }), className)}
|
|
73
|
+
{...pressableProps}
|
|
74
|
+
>
|
|
75
|
+
{content}
|
|
76
|
+
</Pressable>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export type ScreenStatus = "loading" | "error" | "ready";
|
|
4
|
+
|
|
5
|
+
export interface ScreenStateProps {
|
|
6
|
+
status: ScreenStatus;
|
|
7
|
+
isEmpty?: boolean;
|
|
8
|
+
loadingState: ReactNode;
|
|
9
|
+
errorState: ReactNode;
|
|
10
|
+
emptyState: ReactNode;
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function ScreenState({
|
|
15
|
+
status,
|
|
16
|
+
isEmpty = false,
|
|
17
|
+
loadingState,
|
|
18
|
+
errorState,
|
|
19
|
+
emptyState,
|
|
20
|
+
children,
|
|
21
|
+
}: ScreenStateProps) {
|
|
22
|
+
if (status === "loading") {
|
|
23
|
+
return loadingState;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (status === "error") {
|
|
27
|
+
return errorState;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (isEmpty) {
|
|
31
|
+
return emptyState;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return children;
|
|
35
|
+
}
|