@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.
Files changed (41) hide show
  1. package/package.json +4 -2
  2. package/src/components/button/button.tsx +6 -3
  3. package/src/components/button/types.ts +17 -7
  4. package/src/components/card/card-section.tsx +17 -1
  5. package/src/components/card/card.tsx +2 -0
  6. package/src/components/empty-state/empty-state.tsx +42 -0
  7. package/src/components/error-state/error-state.tsx +45 -0
  8. package/src/components/form-field/form-field-context.tsx +28 -0
  9. package/src/components/form-field/form-field.tsx +74 -0
  10. package/src/components/form-scroll-view/form-scroll-view.tsx +36 -0
  11. package/src/components/icon-button/icon-button.tsx +41 -0
  12. package/src/components/input/input.tsx +61 -10
  13. package/src/components/label/label.tsx +12 -2
  14. package/src/components/list-row/list-row.tsx +78 -0
  15. package/src/components/screen-state/screen-state.tsx +35 -0
  16. package/src/components/search-bar/hooks/use-debounced-callback.ts +35 -0
  17. package/src/components/search-bar/search-bar.tsx +60 -0
  18. package/src/components/separator/separator.tsx +2 -0
  19. package/src/components/skeleton/hooks/use-reduced-motion.ts +28 -0
  20. package/src/components/skeleton/skeleton-compositions.tsx +68 -0
  21. package/src/components/skeleton/skeleton.tsx +4 -1
  22. package/src/components/skeleton/utils.ts +11 -0
  23. package/src/components/spinner/spinner.tsx +37 -0
  24. package/src/components/text/text-class-context.tsx +19 -0
  25. package/src/components/text/text.tsx +13 -4
  26. package/src/components/text/utils.ts +16 -0
  27. package/src/components/textarea/textarea.tsx +50 -0
  28. package/src/index.ts +20 -0
  29. package/src/lib/cn.ts +7 -3
  30. package/src/variants/button-variants.ts +4 -0
  31. package/src/variants/form-field-variants.ts +17 -0
  32. package/src/variants/form-scroll-view-variants.ts +3 -0
  33. package/src/variants/input-variants.ts +26 -1
  34. package/src/variants/label-variants.ts +2 -0
  35. package/src/variants/list-row-variants.ts +24 -0
  36. package/src/variants/search-bar-variants.ts +1 -0
  37. package/src/variants/skeleton-variants.ts +15 -0
  38. package/src/variants/spinner-variants.ts +15 -0
  39. package/src/variants/state-variants.ts +7 -0
  40. package/src/variants/text-variants.ts +8 -6
  41. 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.2.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/tokens": "0.2.0"
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
- {isLoading ? <ActivityIndicator size="small" /> : leadingIcon}
43
- <Text className={cn(buttonLabelVariants({ variant, size }), labelClassName)}>{label}</Text>
44
- {!isLoading && trailingIcon}
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 ButtonProps
10
+ export interface PressableButtonProps
10
11
  extends Omit<PressableProps, "children" | "disabled" | "accessibilityState"> {
11
- label: string;
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 { TextInput, type TextInputProps } from "react-native";
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 { inputVariants, type InputVariantProps } from "../../variants/input-variants";
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 = false,
17
- isInvalid = false,
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
- return (
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: isDisabled }}
25
- aria-disabled={isDisabled}
26
- aria-invalid={isInvalid}
27
- editable={!isDisabled}
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(inputVariants({ size, isInvalid, isDisabled, isMultiline }), className)}
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
+ }