@sincpro/mobile-ui 1.1.0 → 2.0.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 (123) hide show
  1. package/README.md +156 -29
  2. package/dist/Actions/Actions.Button.d.ts +10 -12
  3. package/dist/Actions/Actions.Button.js +9 -11
  4. package/dist/Actions/Actions.Dock.d.ts +20 -0
  5. package/dist/Actions/Actions.Dock.js +89 -0
  6. package/dist/Actions/Actions.FAB.d.ts +1 -2
  7. package/dist/Actions/Actions.FAB.js +5 -7
  8. package/dist/Actions/Actions.IconButton.d.ts +1 -2
  9. package/dist/Actions/Actions.IconButton.js +2 -2
  10. package/dist/Actions/index.d.ts +4 -0
  11. package/dist/Actions/index.js +3 -0
  12. package/dist/Dialog/BottomSheet/BottomSheet.Root.js +22 -34
  13. package/dist/Dialog/Dialog.ActionSheet.js +2 -16
  14. package/dist/Dialog/Dialog.Confirmation.js +7 -5
  15. package/dist/Dialog/Dialog.EditValue.js +3 -1
  16. package/dist/Dialog/Dialog.Sheet.d.ts +3 -3
  17. package/dist/Dialog/Dialog.Sheet.js +18 -25
  18. package/dist/Display/Display.Accordion.js +7 -3
  19. package/dist/Display/Display.AmountSummary.d.ts +28 -0
  20. package/dist/Display/Display.AmountSummary.js +25 -0
  21. package/dist/Display/Display.Carousel.d.ts +27 -4
  22. package/dist/Display/Display.Carousel.js +112 -12
  23. package/dist/Display/Display.ExpoImage.d.ts +8 -0
  24. package/dist/Display/Display.ExpoImage.js +15 -0
  25. package/dist/Display/Display.Image.d.ts +10 -6
  26. package/dist/Display/Display.Image.js +39 -6
  27. package/dist/Display/Display.ListItem.d.ts +1 -2
  28. package/dist/Display/Display.ListItem.js +3 -3
  29. package/dist/Display/Display.Logo.d.ts +2 -2
  30. package/dist/Display/Display.Logo.js +3 -2
  31. package/dist/Display/Display.MediaCard.d.ts +2 -2
  32. package/dist/Display/Display.MediaEmbed.d.ts +2 -2
  33. package/dist/Display/Display.ProductCard.d.ts +2 -2
  34. package/dist/Display/Display.Story.d.ts +3 -3
  35. package/dist/Display/Display.Story.js +5 -4
  36. package/dist/Display/Display.StoryTray.d.ts +4 -4
  37. package/dist/Display/Display.StoryTray.js +3 -2
  38. package/dist/Display/Display.StoryViewer.d.ts +3 -3
  39. package/dist/Display/Display.StoryViewer.js +6 -3
  40. package/dist/Display/index.d.ts +9 -0
  41. package/dist/Display/index.js +4 -0
  42. package/dist/Feedback/Feedback.DomainSplashScreen.d.ts +13 -4
  43. package/dist/Feedback/Feedback.DomainSplashScreen.js +68 -53
  44. package/dist/Feedback/Feedback.EmptyState.d.ts +3 -4
  45. package/dist/Feedback/Feedback.EmptyState.js +4 -3
  46. package/dist/Feedback/Feedback.Error.d.ts +1 -2
  47. package/dist/Feedback/Feedback.Error.js +2 -2
  48. package/dist/Feedback/Feedback.Loading.d.ts +6 -2
  49. package/dist/Feedback/Feedback.Loading.js +2 -2
  50. package/dist/Feedback/Feedback.Progress.d.ts +6 -3
  51. package/dist/Feedback/Feedback.Progress.js +29 -19
  52. package/dist/Feedback/Feedback.ProgressCircle.d.ts +3 -2
  53. package/dist/Feedback/Feedback.ProgressCircle.js +10 -7
  54. package/dist/Feedback/Feedback.Skeleton.d.ts +4 -4
  55. package/dist/Feedback/Feedback.Skeleton.js +15 -3
  56. package/dist/Feedback/Feedback.Spinner.d.ts +3 -3
  57. package/dist/Feedback/Feedback.Spinner.js +40 -9
  58. package/dist/Feedback/Feedback.Splash.d.ts +2 -2
  59. package/dist/Feedback/Feedback.Splash.js +5 -3
  60. package/dist/Feedback/Toast.context.d.ts +64 -3
  61. package/dist/Feedback/Toast.context.js +112 -31
  62. package/dist/Feedback/index.d.ts +15 -1
  63. package/dist/Feedback/index.js +4 -1
  64. package/dist/Form/Form.Checkbox.d.ts +1 -2
  65. package/dist/Form/Form.Checkbox.js +2 -2
  66. package/dist/Form/Form.DatePicker.d.ts +1 -2
  67. package/dist/Form/Form.DatePicker.js +2 -2
  68. package/dist/Form/Form.Dropdown.d.ts +15 -3
  69. package/dist/Form/Form.Dropdown.js +21 -6
  70. package/dist/Form/Form.ErrorSummary.d.ts +23 -0
  71. package/dist/Form/Form.ErrorSummary.js +53 -0
  72. package/dist/Form/Form.Input.d.ts +1 -2
  73. package/dist/Form/Form.Input.js +2 -2
  74. package/dist/Form/Form.Select.d.ts +1 -2
  75. package/dist/Form/Form.Select.js +6 -19
  76. package/dist/Form/index.d.ts +3 -0
  77. package/dist/Form/index.js +2 -0
  78. package/dist/Navigation/Navigation.AppBar.d.ts +2 -6
  79. package/dist/Navigation/Navigation.AppBar.js +9 -7
  80. package/dist/Navigation/Navigation.BottomNav.d.ts +1 -3
  81. package/dist/Navigation/Navigation.BottomNav.js +7 -6
  82. package/dist/Navigation/Navigation.Header.d.ts +2 -2
  83. package/dist/Navigation/Navigation.Menu.js +10 -3
  84. package/dist/Navigation/Navigation.Search.d.ts +41 -0
  85. package/dist/Navigation/Navigation.Search.js +47 -0
  86. package/dist/Navigation/Navigation.SearchBar.d.ts +1 -2
  87. package/dist/Navigation/Navigation.SearchBar.js +16 -19
  88. package/dist/Navigation/Navigation.SegmentedControl.d.ts +11 -3
  89. package/dist/Navigation/Navigation.SegmentedControl.js +29 -16
  90. package/dist/Navigation/index.d.ts +11 -2
  91. package/dist/Navigation/index.js +2 -0
  92. package/dist/branding.d.ts +3 -2
  93. package/dist/primitives/BaseButton.d.ts +10 -12
  94. package/dist/primitives/BaseButton.js +9 -11
  95. package/dist/theme/typography.d.ts +1 -1
  96. package/dist/utils/image.d.ts +3 -0
  97. package/dist/utils/image.js +13 -0
  98. package/dist/utils/keys.d.ts +1 -0
  99. package/dist/utils/keys.js +7 -0
  100. package/dist/utils/progress.d.ts +1 -0
  101. package/dist/utils/progress.js +5 -0
  102. package/dist/utils/reorder.js +2 -5
  103. package/dist/utils/useReducedMotion.d.ts +1 -0
  104. package/dist/utils/useReducedMotion.js +43 -0
  105. package/dist/utils/useSheetTranslation.d.ts +5 -0
  106. package/dist/utils/useSheetTranslation.js +23 -0
  107. package/dist/views/AppSplashView.d.ts +3 -2
  108. package/dist/views/AppSplashView.js +3 -2
  109. package/dist/views/AuthFormView.d.ts +2 -2
  110. package/dist/views/FormViewV2.d.ts +2 -2
  111. package/dist/views/ListViewV2.context.d.ts +3 -1
  112. package/dist/views/ListViewV2.context.js +41 -21
  113. package/dist/views/ListViewV2.d.ts +15 -7
  114. package/dist/views/ListViewV2.js +44 -23
  115. package/dist/views/types/IListView.js +6 -5
  116. package/dist/widgets/CollectionOptions.js +9 -7
  117. package/dist/widgets/FilterBar.d.ts +1 -1
  118. package/dist/widgets/FilterBar.js +1 -1
  119. package/dist/widgets/FilterBuilder.js +7 -7
  120. package/dist/widgets/HomeHeader.d.ts +2 -2
  121. package/dist/widgets/PromoModal.js +3 -1
  122. package/dist/widgets/ScreenHeader.d.ts +2 -2
  123. package/package.json +4 -1
@@ -8,7 +8,7 @@ import { ActivityIndicator, TouchableOpacity, View } from "react-native";
8
8
  // STYLE VARIANTS
9
9
  // ============================================================================
10
10
  const button = tv({
11
- base: "rounded-md items-center justify-center flex-row min-h-[44px]",
11
+ base: "rounded-md items-center justify-center flex-row min-h-[48px]",
12
12
  variants: {
13
13
  variant: {
14
14
  primary: "bg-accent",
@@ -18,12 +18,12 @@ const button = tv({
18
18
  outlineDanger: "bg-transparent border border-danger",
19
19
  outlineBlack: "bg-transparent border border-border-strong",
20
20
  flat: "bg-text-primary/10",
21
- link: "bg-transparent min-h-0",
21
+ link: "bg-transparent",
22
22
  transparent: "bg-transparent border border-border-light",
23
23
  },
24
24
  size: {
25
- small: "py-1.5 px-3 min-h-[36px]",
26
- medium: "py-2.5 px-4 min-h-[44px]",
25
+ small: "py-1.5 px-3 min-h-[48px]",
26
+ medium: "py-2.5 px-4 min-h-[48px]",
27
27
  large: "py-3 px-5 min-h-[48px]",
28
28
  },
29
29
  disabled: {
@@ -192,7 +192,7 @@ function getIconSize(size) {
192
192
  function getTextVariant(size) {
193
193
  return textVariantMap[size] ?? "button";
194
194
  }
195
- export const BaseButton = ({ title, onPress, variant = "primary", size = "medium", loading = false, disabled = false, className, icon, accessibleSizing = false, testID, accessibilityLabel, accessibilityState, ...accessibilityProps }) => {
195
+ export const BaseButton = ({ title, onPress, variant = "primary", size = "medium", loading = false, disabled = false, className, icon, testID, accessibilityLabel, accessibilityState, ...accessibilityProps }) => {
196
196
  const textColorClass = textColor({
197
197
  variant: variant ?? "primary",
198
198
  disabled,
@@ -204,17 +204,15 @@ export const BaseButton = ({ title, onPress, variant = "primary", size = "medium
204
204
  ...accessibilityState,
205
205
  disabled: disabled || loading,
206
206
  busy: loading,
207
- }, style: accessibleSizing
208
- ? { minHeight: 48, minWidth: 48, justifyContent: "center" }
209
- : undefined, testID: testID, children: loading ? (_jsx(ActivityIndicator, { color: loadingColorValue, size: "small" })) : (_jsx(Typography.Text, { className: textColorClass, variant: textVariantValue, children: title })) }));
207
+ }, style: { minHeight: 48, minWidth: 48, justifyContent: "center" }, testID: testID, children: loading ? (_jsx(ActivityIndicator, { color: loadingColorValue, size: "small" })) : (_jsx(Typography.Text, { allowFontScaling: true, className: textColorClass, style: { flexShrink: 1 }, variant: textVariantValue, children: title })) }));
210
208
  }
211
209
  return (_jsxs(TouchableOpacity, { activeOpacity: 0.8, className: button({ variant, size, disabled, className }), disabled: disabled || loading, onPress: onPress, ...accessibilityProps, accessibilityLabel: accessibilityLabel ?? title, accessibilityRole: "button", accessibilityState: {
212
210
  ...accessibilityState,
213
211
  disabled: disabled || loading,
214
212
  busy: loading,
215
- }, style: accessibleSizing ? { minHeight: 48, minWidth: 48 } : undefined, testID: testID, children: [icon && !loading && _jsx(View, { className: "mr-2", children: icon }), loading ? (_jsx(ActivityIndicator, { color: loadingColorValue, size: "small" })) : (_jsx(Typography.Text, { className: textColorClass, semibold: variant === "primary", variant: textVariantValue, children: title }))] }));
213
+ }, style: { minHeight: 48, minWidth: 48 }, testID: testID, children: [icon && !loading && _jsx(View, { className: "mr-2", children: icon }), loading ? (_jsx(ActivityIndicator, { color: loadingColorValue, size: "small" })) : (_jsx(Typography.Text, { allowFontScaling: true, className: textColorClass, semibold: variant === "primary", style: { flexShrink: 1 }, variant: textVariantValue, children: title }))] }));
216
214
  };
217
- export const BaseButtonIcon = ({ icon, onPress, variant = "primary", size = "medium", loading = false, disabled = false, className, accessibleSizing = false, testID, accessibilityState, ...accessibilityProps }) => {
215
+ export const BaseButtonIcon = ({ icon, onPress, variant = "primary", size = "medium", loading = false, disabled = false, className, testID, accessibilityState, ...accessibilityProps }) => {
218
216
  const iconColorValue = getIconColor(variant ?? "primary", disabled);
219
217
  const loadingColorValue = getLoadingColor(variant ?? "primary");
220
218
  const iconSizeValue = getIconSize(size ?? "medium");
@@ -222,7 +220,7 @@ export const BaseButtonIcon = ({ icon, onPress, variant = "primary", size = "med
222
220
  ...accessibilityState,
223
221
  disabled: disabled || loading,
224
222
  busy: loading,
225
- }, style: accessibleSizing ? { minHeight: 48, minWidth: 48 } : undefined, testID: testID, children: loading ? (_jsx(ActivityIndicator, { color: loadingColorValue, size: "small" })) : (_jsx(Icon, { color: iconColorValue, name: icon, size: iconSizeValue })) }));
223
+ }, style: { minHeight: 48, minWidth: 48 }, testID: testID, children: loading ? (_jsx(ActivityIndicator, { color: loadingColorValue, size: "small" })) : (_jsx(Icon, { color: iconColorValue, name: icon, size: iconSizeValue })) }));
226
224
  };
227
225
  // ============================================================================
228
226
  // COMPOUND COMPONENT
@@ -436,7 +436,7 @@ export declare const Typography: {
436
436
  readonly letterSpacing: 0;
437
437
  };
438
438
  };
439
- readonly variantFontRole: Record<"label" | "button" | "title" | "display" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "bodyReadable" | "bodyLarge" | "body" | "bodySmall" | "labelSmall" | "data" | "dataLarge" | "overline" | "caption" | "captionSmall" | "buttonSmall" | "buttonLarge" | "subtitle", FontRole>;
439
+ readonly variantFontRole: Record<"display" | "label" | "button" | "title" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "bodyReadable" | "bodyLarge" | "body" | "bodySmall" | "labelSmall" | "data" | "dataLarge" | "overline" | "caption" | "captionSmall" | "buttonSmall" | "buttonLarge" | "subtitle", FontRole>;
440
440
  readonly createTextStyle: (variant: keyof typeof typographyVariants, overrides?: Partial<TextStyle>) => TextStyle;
441
441
  readonly createCustomTextStyle: (fontSize: keyof typeof fontSizes, fontFamily: keyof typeof fontFamilies, options?: {
442
442
  lineHeight?: keyof typeof lineHeights;
@@ -0,0 +1,3 @@
1
+ import type { ImageProps } from "expo-image";
2
+ export type ImageSource = NonNullable<ImageProps["source"]>;
3
+ export declare function imageSourceKey(source: ImageProps["source"]): string | undefined;
@@ -0,0 +1,13 @@
1
+ const sharedSourceKeys = new WeakMap();
2
+ let nextSharedSourceKey = 0;
3
+ export function imageSourceKey(source) {
4
+ if (source && typeof source === "object" && "nativeRefType" in source) {
5
+ let key = sharedSourceKeys.get(source);
6
+ if (key === undefined) {
7
+ key = nextSharedSourceKey++;
8
+ sharedSourceKeys.set(source, key);
9
+ }
10
+ return `shared-image-${key}`;
11
+ }
12
+ return JSON.stringify(source);
13
+ }
@@ -0,0 +1 @@
1
+ export declare function uniqueKeys<T>(items: readonly T[], keyExtractor: (item: T, index: number) => string, component: string): string[];
@@ -0,0 +1,7 @@
1
+ export function uniqueKeys(items, keyExtractor, component) {
2
+ const keys = items.map(keyExtractor);
3
+ if (keys.some((key) => typeof key !== "string" || !key.trim()) ||
4
+ new Set(keys).size !== keys.length)
5
+ throw new Error(`${component} requires unique, non-empty string keys.`);
6
+ return keys;
7
+ }
@@ -0,0 +1 @@
1
+ export declare function normalizeProgress(value: number): number;
@@ -0,0 +1,5 @@
1
+ export function normalizeProgress(value) {
2
+ if (!Number.isFinite(value))
3
+ throw new RangeError("Progress value must be a finite number");
4
+ return Math.max(0, Math.min(1, value));
5
+ }
@@ -1,9 +1,6 @@
1
+ import { uniqueKeys } from "./keys.js";
1
2
  export function reorderKeys(items, keyExtractor) {
2
- const keys = items.map(keyExtractor);
3
- if (keys.some((key) => typeof key !== "string" || !key.trim()) ||
4
- new Set(keys).size !== keys.length)
5
- throw new Error("ReorderableList requires unique, non-empty string keys.");
6
- return keys;
3
+ return uniqueKeys(items, keyExtractor, "ReorderableList");
7
4
  }
8
5
  export function sameOrder(left, right) {
9
6
  return left.length === right.length && left.every((key, index) => key === right[index]);
@@ -0,0 +1 @@
1
+ export declare function useReducedMotion(): boolean;
@@ -0,0 +1,43 @@
1
+ import { useSyncExternalStore } from "react";
2
+ import { AccessibilityInfo } from "react-native";
3
+ const listeners = new Set();
4
+ let reducedMotion = true;
5
+ let generation = 0;
6
+ let subscription;
7
+ function publish(enabled) {
8
+ if (reducedMotion === enabled)
9
+ return;
10
+ reducedMotion = enabled;
11
+ for (const listener of [...listeners])
12
+ listener();
13
+ }
14
+ function subscribe(listener) {
15
+ listeners.add(listener);
16
+ if (listeners.size === 1) {
17
+ const request = ++generation;
18
+ let receivedChange = false;
19
+ subscription = AccessibilityInfo.addEventListener("reduceMotionChanged", (enabled) => {
20
+ receivedChange = true;
21
+ publish(enabled);
22
+ });
23
+ AccessibilityInfo.isReduceMotionEnabled().then((enabled) => {
24
+ if (request === generation && !receivedChange)
25
+ publish(enabled);
26
+ }, (error) => {
27
+ if (request === generation)
28
+ console.warn("[useReducedMotion] Could not read accessibility preference", error);
29
+ });
30
+ }
31
+ return () => {
32
+ listeners.delete(listener);
33
+ if (listeners.size === 0) {
34
+ generation += 1;
35
+ subscription?.remove();
36
+ subscription = undefined;
37
+ reducedMotion = true;
38
+ }
39
+ };
40
+ }
41
+ export function useReducedMotion() {
42
+ return useSyncExternalStore(subscribe, () => reducedMotion, () => true);
43
+ }
@@ -0,0 +1,5 @@
1
+ import { Animated } from "react-native";
2
+ export declare function useSheetTranslation(visible: boolean, hiddenOffset?: number): {
3
+ translateY: Animated.Value;
4
+ reducedMotion: boolean;
5
+ };
@@ -0,0 +1,23 @@
1
+ import { useReducedMotion } from "./useReducedMotion.js";
2
+ import { useEffect, useState } from "react";
3
+ import { Animated } from "react-native";
4
+ export function useSheetTranslation(visible, hiddenOffset = 800) {
5
+ const reducedMotion = useReducedMotion();
6
+ const [translateY] = useState(() => new Animated.Value(visible ? 0 : hiddenOffset));
7
+ useEffect(() => {
8
+ translateY.stopAnimation();
9
+ if (visible && !reducedMotion) {
10
+ const animation = Animated.spring(translateY, {
11
+ toValue: 0,
12
+ useNativeDriver: true,
13
+ damping: 20,
14
+ stiffness: 300,
15
+ });
16
+ animation.start();
17
+ return () => animation.stop();
18
+ }
19
+ translateY.setValue(visible ? 0 : hiddenOffset);
20
+ return () => translateY.stopAnimation();
21
+ }, [visible, hiddenOffset, reducedMotion, translateY]);
22
+ return { translateY, reducedMotion };
23
+ }
@@ -1,12 +1,13 @@
1
1
  import { type SpinnerVariant } from "../Feedback/Feedback.Spinner.js";
2
2
  import type { AppBarBackground } from "../Navigation/Navigation.AppBar.js";
3
+ import type { ImageSource } from "../utils/image.js";
3
4
  import type { ReactNode } from "react";
4
- import { type ImageSourcePropType, type ViewStyle } from "react-native";
5
+ import { type ViewStyle } from "react-native";
5
6
  export interface AppSplashViewProps {
6
7
  /** Gradient + pattern background. Defaults to `night-green` preset. */
7
8
  background?: AppBarBackground;
8
9
  /** App logo shown centered above the loading indicator. */
9
- logo?: ImageSourcePropType;
10
+ logo?: ImageSource;
10
11
  /** Built-in loading animation variant. Ignored when `loader` is provided. Defaults to `"dots"`. */
11
12
  loadingVariant?: SpinnerVariant;
12
13
  /** Fully custom loading element. Takes precedence over `loadingVariant`. */
@@ -1,9 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "nativewind/jsx-runtime";
2
+ import ExpoImage from "../Display/Display.ExpoImage.js";
2
3
  import GradientSurface from "../Display/Display.GradientSurface.js";
3
4
  import { Pattern } from "../Display/Display.Pattern.js";
4
5
  import Spinner from "../Feedback/Feedback.Spinner.js";
5
- import { Image, View } from "react-native";
6
+ import { View } from "react-native";
6
7
  export function AppSplashView({ background, logo, loadingVariant = "dots", loader, style, }) {
7
- return (_jsxs(GradientSurface, { className: "flex-1", colors: background?.colors, end: background?.end, padding: "none", preset: background?.surface ?? "night-green", radius: "none", start: background?.start, style: { flex: 1, ...style }, children: [background?.pattern ? (_jsx(Pattern, { color: background.patternColor ?? "#FFFFFF", kind: background.pattern, opacity: background.patternOpacity ?? 0.18 })) : null, _jsxs(View, { style: { flex: 1, alignItems: "center", justifyContent: "center" }, children: [logo ? (_jsx(Image, { source: logo, style: { width: 140, height: 140, resizeMode: "contain", marginBottom: 40 } })) : null, loader ?? (_jsx(Spinner, { color: "rgba(255,255,255,0.9)", size: "large", variant: loadingVariant }))] })] }));
8
+ return (_jsxs(GradientSurface, { className: "flex-1", colors: background?.colors, end: background?.end, padding: "none", preset: background?.surface ?? "night-green", radius: "none", start: background?.start, style: { flex: 1, ...style }, children: [background?.pattern ? (_jsx(Pattern, { color: background.patternColor ?? "#FFFFFF", kind: background.pattern, opacity: background.patternOpacity ?? 0.18 })) : null, _jsxs(View, { style: { flex: 1, alignItems: "center", justifyContent: "center" }, children: [logo ? (_jsx(ExpoImage, { contentFit: "contain", source: logo, style: { width: 140, height: 140, marginBottom: 40 } })) : null, loader ?? (_jsx(Spinner, { color: "rgba(255,255,255,0.9)", size: "large", variant: loadingVariant }))] })] }));
8
9
  }
9
10
  export default AppSplashView;
@@ -1,11 +1,11 @@
1
1
  import type { KeyboardFormProps } from "../Form/Form.KeyboardForm.js";
2
2
  import type { AppBarBackground } from "../Navigation/Navigation.AppBar.js";
3
+ import type { ImageSource } from "../utils/image.js";
3
4
  import { ReactNode } from "react";
4
- import { type ImageSourcePropType } from "react-native";
5
5
  interface AuthFormProps {
6
6
  title?: string;
7
7
  description?: string;
8
- logoSource?: ImageSourcePropType;
8
+ logoSource?: ImageSource;
9
9
  onBack?: () => void;
10
10
  FormNode: ReactNode;
11
11
  background?: AppBarBackground;
@@ -1,8 +1,8 @@
1
1
  import { type KeyboardFormProps } from "../Form/Form.KeyboardForm.js";
2
2
  import type { AppBarBackground } from "../Navigation/Navigation.AppBar.js";
3
3
  import { type SegmentedControlProps } from "../Navigation/Navigation.SegmentedControl.js";
4
+ import type { ImageSource } from "../utils/image.js";
4
5
  import React, { ReactNode } from "react";
5
- import { type ImageSourcePropType } from "react-native";
6
6
  interface FormViewProps<T> {
7
7
  name: string;
8
8
  description?: string;
@@ -41,7 +41,7 @@ interface HeaderProps {
41
41
  */
42
42
  background?: AppBarBackground;
43
43
  /** @deprecated Pass `variant` prop instead. */
44
- logoSource?: ImageSourcePropType;
44
+ logoSource?: ImageSource;
45
45
  }
46
46
  declare function Header({ variant, tone, actions, children, topSpacing, background, }: HeaderProps): React.JSX.Element;
47
47
  declare function HeaderActions({ children }: {
@@ -17,8 +17,9 @@ interface ListViewContextValue<T> {
17
17
  onBack?: () => void;
18
18
  onLoadMore?: () => void;
19
19
  loadingMore?: boolean;
20
+ hasMore: boolean;
20
21
  }
21
- export declare function ListViewProvider<T>({ name, description, items, isLoading, hasError, errorMessage, readonly, onRefresh, onRetry, onPressItem, onDeleteItem, onSearch, onBack, onLoadMore, loadingMore, children, }: {
22
+ export declare function ListViewProvider<T>({ name, description, items, isLoading, hasError, errorMessage, readonly, onRefresh, onRetry, onPressItem, onDeleteItem, onSearch, onBack, onLoadMore, loadingMore, hasMore, children, }: {
22
23
  name: string;
23
24
  description?: string;
24
25
  items: T[];
@@ -34,6 +35,7 @@ export declare function ListViewProvider<T>({ name, description, items, isLoadin
34
35
  onBack?: () => void;
35
36
  onLoadMore?: () => void;
36
37
  loadingMore?: boolean;
38
+ hasMore?: boolean;
37
39
  children: ReactNode;
38
40
  }): import("react").JSX.Element;
39
41
  export declare function useListView<T>(): ListViewContextValue<T>;
@@ -1,27 +1,47 @@
1
1
  import { jsx as _jsx } from "nativewind/jsx-runtime";
2
- import { createContext, useContext, useState } from "react";
2
+ import { createContext, useContext, useMemo, useState } from "react";
3
3
  const Context = createContext(null);
4
- export function ListViewProvider({ name, description, items, isLoading = false, hasError = false, errorMessage, readonly = false, onRefresh, onRetry, onPressItem, onDeleteItem, onSearch, onBack, onLoadMore, loadingMore = false, children, }) {
4
+ export function ListViewProvider({ name, description, items, isLoading = false, hasError = false, errorMessage, readonly = false, onRefresh, onRetry, onPressItem, onDeleteItem, onSearch, onBack, onLoadMore, loadingMore = false, hasMore = true, children, }) {
5
5
  const [showRefreshing, setShowRefreshing] = useState(false);
6
- return (_jsx(Context.Provider, { value: {
7
- name,
8
- description,
9
- items,
10
- isLoading,
11
- hasError,
12
- errorMessage,
13
- readonly,
14
- showRefreshing,
15
- setShowRefreshing,
16
- onRefresh,
17
- onRetry,
18
- onPressItem,
19
- onDeleteItem,
20
- onSearch,
21
- onBack,
22
- onLoadMore,
23
- loadingMore,
24
- }, children: children }));
6
+ const value = useMemo(() => ({
7
+ name,
8
+ description,
9
+ items,
10
+ isLoading,
11
+ hasError,
12
+ errorMessage,
13
+ readonly,
14
+ showRefreshing,
15
+ setShowRefreshing,
16
+ onRefresh,
17
+ onRetry,
18
+ onPressItem,
19
+ onDeleteItem,
20
+ onSearch,
21
+ onBack,
22
+ onLoadMore,
23
+ loadingMore,
24
+ hasMore,
25
+ }), [
26
+ name,
27
+ description,
28
+ items,
29
+ isLoading,
30
+ hasError,
31
+ errorMessage,
32
+ readonly,
33
+ showRefreshing,
34
+ onRefresh,
35
+ onRetry,
36
+ onPressItem,
37
+ onDeleteItem,
38
+ onSearch,
39
+ onBack,
40
+ onLoadMore,
41
+ loadingMore,
42
+ hasMore,
43
+ ]);
44
+ return _jsx(Context.Provider, { value: value, children: children });
25
45
  }
26
46
  export function useListView() {
27
47
  const context = useContext(Context);
@@ -1,8 +1,9 @@
1
1
  import { type SearchHistoryProps } from "../Display/Display.SearchHistory.js";
2
2
  import type { AppBarBackground } from "../Navigation/Navigation.AppBar.js";
3
3
  import { type SegmentedControlProps } from "../Navigation/Navigation.SegmentedControl.js";
4
- import React, { ReactNode } from "react";
5
- import { type ScrollViewProps, StyleProp, ViewStyle } from "react-native";
4
+ import { IRowItem } from "./types/IListView.js";
5
+ import React, { ReactNode, type Ref } from "react";
6
+ import { FlatList, type FlatListProps, type ScrollViewProps, StyleProp, ViewStyle } from "react-native";
6
7
  interface ListViewProps<T> {
7
8
  name: string;
8
9
  description?: string;
@@ -22,12 +23,13 @@ interface ListViewProps<T> {
22
23
  onLoadMore?: () => void;
23
24
  /** Show a footer spinner while the next page loads. */
24
25
  loadingMore?: boolean;
26
+ hasMore?: boolean;
25
27
  backgroundColor?: string;
26
28
  withContainer?: boolean;
27
29
  children: ReactNode;
28
30
  }
29
31
  export declare function refreshListData(onRefresh: (() => Promise<void>) | undefined, onRefreshError: ((error: unknown) => void) | undefined, setShowRefreshing: (value: boolean) => void): Promise<void>;
30
- declare function ListViewRoot<T>({ name, description, items, isLoading, hasError, errorMessage, readonly, onRefresh, onRefreshError, onRetry, onPressItem, onDeleteItem, onSearch, onBack, onLoadMore, loadingMore, backgroundColor, withContainer, children, }: ListViewProps<T>): React.JSX.Element;
32
+ declare function ListViewRoot<T>({ name, description, items, isLoading, hasError, errorMessage, readonly, onRefresh, onRefreshError, onRetry, onPressItem, onDeleteItem, onSearch, onBack, onLoadMore, loadingMore, hasMore, backgroundColor, withContainer, children, }: ListViewProps<T>): React.JSX.Element;
31
33
  interface HeaderProps {
32
34
  /** AppBar display variant. `large` = big title (list/index). `center` = modal/detail. */
33
35
  variant?: "default" | "large" | "center";
@@ -82,14 +84,20 @@ declare function FilterChips({ children }: {
82
84
  children: ReactNode;
83
85
  }): React.JSX.Element;
84
86
  type ListScrollProps = Pick<ScrollViewProps, "keyboardShouldPersistTaps" | "keyboardDismissMode" | "testID">;
85
- declare function Content({ children, withMargin, keyboardShouldPersistTaps, keyboardDismissMode, testID, }: {
87
+ export type ListViewVirtualizationProps = Pick<FlatListProps<IRowItem<unknown>>, "initialNumToRender" | "maxToRenderPerBatch" | "windowSize" | "updateCellsBatchingPeriod" | "removeClippedSubviews" | "extraData" | "getItemLayout" | "onViewableItemsChanged" | "viewabilityConfig" | "onEndReachedThreshold" | "maintainVisibleContentPosition">;
88
+ interface ListContentOptions {
89
+ listProps?: ListViewVirtualizationProps;
90
+ listRef?: Ref<FlatList<IRowItem<unknown>>>;
91
+ contentContainerStyle?: StyleProp<ViewStyle>;
92
+ }
93
+ declare function Content({ children, withMargin, keyboardShouldPersistTaps, keyboardDismissMode, testID, listProps, listRef, contentContainerStyle, }: {
86
94
  children?: (item: any) => ReactNode;
87
95
  withMargin?: boolean;
88
- } & ListScrollProps): React.JSX.Element;
89
- declare function ContentList({ children, contentWithMargin, keyboardShouldPersistTaps, keyboardDismissMode, testID, }: {
96
+ } & ListScrollProps & ListContentOptions): React.JSX.Element;
97
+ declare function ContentList({ children, contentWithMargin, keyboardShouldPersistTaps, keyboardDismissMode, testID, listProps, listRef, contentContainerStyle, }: {
90
98
  children?: (item: any) => ReactNode;
91
99
  contentWithMargin: boolean;
92
- } & ListScrollProps): React.JSX.Element;
100
+ } & ListScrollProps & ListContentOptions): React.JSX.Element;
93
101
  declare function Footer({ children }: {
94
102
  children: ReactNode;
95
103
  }): React.JSX.Element | null;
@@ -11,7 +11,7 @@ import { cn, tv } from "../theme/tw.js";
11
11
  import { Typography } from "../Typography/index.js";
12
12
  import { ListViewProvider, useListView } from "./ListViewV2.context.js";
13
13
  import { toRowItems } from "./types/IListView.js";
14
- import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from "react";
14
+ import React, { createContext, useCallback, useContext, useEffect, useMemo, useState, } from "react";
15
15
  import { FlatList, RefreshControl, ScrollView, TouchableOpacity, View, } from "react-native";
16
16
  const RefreshErrorContext = createContext(undefined);
17
17
  export async function refreshListData(onRefresh, onRefreshError, setShowRefreshing) {
@@ -32,13 +32,13 @@ export async function refreshListData(onRefresh, onRefreshError, setShowRefreshi
32
32
  setShowRefreshing(false);
33
33
  }
34
34
  }
35
- function ListViewRoot({ name, description, items, isLoading = false, hasError = false, errorMessage, readonly = false, onRefresh, onRefreshError, onRetry, onPressItem, onDeleteItem, onSearch, onBack, onLoadMore, loadingMore = false, backgroundColor, withContainer = true, children, }) {
35
+ function ListViewRoot({ name, description, items, isLoading = false, hasError = false, errorMessage, readonly = false, onRefresh, onRefreshError, onRetry, onPressItem, onDeleteItem, onSearch, onBack, onLoadMore, loadingMore = false, hasMore = true, backgroundColor, withContainer = true, children, }) {
36
36
  const Wrapper = withContainer ? Container : View;
37
37
  const wrapperProps = withContainer
38
38
  ? { edges: ["bottom", "left", "right"] }
39
39
  : { className: "flex-1 bg-bg-page" };
40
40
  // Propagate contentWithMargin to ContentList via context
41
- return (_jsx(ListViewProvider, { description: description, errorMessage: errorMessage, hasError: hasError, isLoading: isLoading, items: items, loadingMore: loadingMore, name: name, onBack: onBack, onDeleteItem: onDeleteItem, onLoadMore: onLoadMore, onPressItem: onPressItem, onRefresh: onRefresh, onRetry: onRetry, onSearch: onSearch, readonly: readonly, children: _jsx(RefreshErrorContext.Provider, { value: onRefreshError, children: _jsx(ErrorBoundary, { children: _jsx(Wrapper, { ...wrapperProps, children: children }) }) }) }));
41
+ return (_jsx(ListViewProvider, { description: description, errorMessage: errorMessage, hasError: hasError, hasMore: hasMore, isLoading: isLoading, items: items, loadingMore: loadingMore, name: name, onBack: onBack, onDeleteItem: onDeleteItem, onLoadMore: onLoadMore, onPressItem: onPressItem, onRefresh: onRefresh, onRetry: onRetry, onSearch: onSearch, readonly: readonly, children: _jsx(RefreshErrorContext.Provider, { value: onRefreshError, children: _jsx(ErrorBoundary, { children: _jsx(Wrapper, { ...wrapperProps, children: children }) }) }) }));
42
42
  }
43
43
  function Header({ variant = "large", tone, actions, children, background, topSpacing, }) {
44
44
  const { name, description, onBack } = useListView();
@@ -48,6 +48,10 @@ function HeaderSearch({ searchQuery = "" }) {
48
48
  const { name, onSearch } = useListView();
49
49
  const [query, setQuery] = useState(searchQuery);
50
50
  const [debouncedQuery, setDebouncedQuery] = useState(searchQuery);
51
+ const onSearchRef = React.useRef(onSearch);
52
+ useEffect(() => {
53
+ onSearchRef.current = onSearch;
54
+ }, [onSearch]);
51
55
  useEffect(() => {
52
56
  const timeout = setTimeout(() => {
53
57
  setDebouncedQuery(query);
@@ -55,10 +59,10 @@ function HeaderSearch({ searchQuery = "" }) {
55
59
  return () => clearTimeout(timeout);
56
60
  }, [query]);
57
61
  useEffect(() => {
58
- if (onSearch && debouncedQuery !== undefined) {
59
- onSearch(debouncedQuery);
62
+ if (debouncedQuery !== undefined) {
63
+ onSearchRef.current?.(debouncedQuery);
60
64
  }
61
- }, [debouncedQuery, onSearch]);
65
+ }, [debouncedQuery]);
62
66
  if (!onSearch) {
63
67
  return null;
64
68
  }
@@ -114,7 +118,10 @@ function FilterChip({ label, active = false, onPress }) {
114
118
  function FilterChips({ children }) {
115
119
  return (_jsx(ScrollView, { contentContainerStyle: { paddingHorizontal: 16, gap: 8 }, horizontal: true, showsHorizontalScrollIndicator: false, style: { flexGrow: 0 }, children: children }));
116
120
  }
117
- function Content({ children, withMargin = true, keyboardShouldPersistTaps = "handled", keyboardDismissMode = "none", testID, }) {
121
+ function rowKey(item) {
122
+ return String(item.id ?? item.index);
123
+ }
124
+ function Content({ children, withMargin = true, keyboardShouldPersistTaps = "handled", keyboardDismissMode = "none", testID, listProps, listRef, contentContainerStyle, }) {
118
125
  const { name, items, isLoading, hasError, errorMessage, onRefresh, onRetry, onBack, showRefreshing, setShowRefreshing, } = useListView();
119
126
  const bottomInset = useBottomInset();
120
127
  const theme = useTheme();
@@ -126,25 +133,31 @@ function Content({ children, withMargin = true, keyboardShouldPersistTaps = "han
126
133
  return _jsx(Feedback.Loading, { message: `Cargando ${name}...` });
127
134
  }
128
135
  if (hasError) {
129
- return (_jsx(ScrollView, { contentContainerStyle: {
130
- flexGrow: 1,
131
- justifyContent: "center",
132
- alignItems: "center",
133
- paddingBottom: bottomInset,
134
- }, keyboardDismissMode: keyboardDismissMode, keyboardShouldPersistTaps: keyboardShouldPersistTaps, refreshControl: _jsx(RefreshControl, { colors: [theme.primary], onRefresh: handleRefresh, refreshing: showRefreshing }), style: { backgroundColor: theme.bg.page }, testID: testID, children: _jsx(Feedback.Error, { message: errorMessage, onBack: onBack, onRetry: onRetry }) }));
136
+ return (_jsx(ScrollView, { contentContainerStyle: [
137
+ {
138
+ flexGrow: 1,
139
+ justifyContent: "center",
140
+ alignItems: "center",
141
+ paddingBottom: bottomInset,
142
+ },
143
+ contentContainerStyle,
144
+ ], keyboardDismissMode: keyboardDismissMode, keyboardShouldPersistTaps: keyboardShouldPersistTaps, refreshControl: _jsx(RefreshControl, { colors: [theme.primary], onRefresh: handleRefresh, refreshing: showRefreshing }), style: { backgroundColor: theme.bg.page }, testID: testID, children: _jsx(Feedback.Error, { message: errorMessage, onBack: onBack, onRetry: onRetry }) }));
135
145
  }
136
146
  if (items.length === 0) {
137
- return (_jsx(ScrollView, { contentContainerStyle: {
138
- flexGrow: 1,
139
- justifyContent: "center",
140
- alignItems: "center",
141
- paddingBottom: bottomInset,
142
- }, keyboardDismissMode: keyboardDismissMode, keyboardShouldPersistTaps: keyboardShouldPersistTaps, refreshControl: _jsx(RefreshControl, { colors: [theme.primary], onRefresh: handleRefresh, refreshing: showRefreshing }), style: { backgroundColor: theme.bg.page }, testID: testID, children: _jsx(Feedback.Empty, {}) }));
147
+ return (_jsx(ScrollView, { contentContainerStyle: [
148
+ {
149
+ flexGrow: 1,
150
+ justifyContent: "center",
151
+ alignItems: "center",
152
+ paddingBottom: bottomInset,
153
+ },
154
+ contentContainerStyle,
155
+ ], keyboardDismissMode: keyboardDismissMode, keyboardShouldPersistTaps: keyboardShouldPersistTaps, refreshControl: _jsx(RefreshControl, { colors: [theme.primary], onRefresh: handleRefresh, refreshing: showRefreshing }), style: { backgroundColor: theme.bg.page }, testID: testID, children: _jsx(Feedback.Empty, {}) }));
143
156
  }
144
- return (_jsxs(View, { style: { flex: 1, backgroundColor: theme.bg.page }, children: [_jsx(Display.CountRecords, { count: items.length || 0, name: name }), _jsx(ContentList, { contentWithMargin: withMargin, keyboardDismissMode: keyboardDismissMode, keyboardShouldPersistTaps: keyboardShouldPersistTaps, testID: testID, children: children })] }));
157
+ return (_jsxs(View, { style: { flex: 1, backgroundColor: theme.bg.page }, children: [_jsx(Display.CountRecords, { count: items.length || 0, name: name }), _jsx(ContentList, { contentContainerStyle: contentContainerStyle, contentWithMargin: withMargin, keyboardDismissMode: keyboardDismissMode, keyboardShouldPersistTaps: keyboardShouldPersistTaps, listProps: listProps, listRef: listRef, testID: testID, children: children })] }));
145
158
  }
146
- function ContentList({ children, contentWithMargin, keyboardShouldPersistTaps, keyboardDismissMode, testID, }) {
147
- const { items, onRefresh, showRefreshing, setShowRefreshing, onPressItem, onDeleteItem, readonly, onLoadMore, loadingMore, } = useListView();
159
+ function ContentList({ children, contentWithMargin, keyboardShouldPersistTaps, keyboardDismissMode, testID, listProps, listRef, contentContainerStyle, }) {
160
+ const { items, onRefresh, showRefreshing, setShowRefreshing, onPressItem, onDeleteItem, readonly, onLoadMore, loadingMore, hasMore, } = useListView();
148
161
  const bottomInset = useBottomInset();
149
162
  const theme = useTheme();
150
163
  const rowItems = useMemo(() => toRowItems(items), [items]);
@@ -166,7 +179,15 @@ function ContentList({ children, contentWithMargin, keyboardShouldPersistTaps, k
166
179
  marginTop: 2,
167
180
  paddingHorizontal: contentWithMargin ? 4 : 0,
168
181
  backgroundColor: theme.bg.page,
169
- }, children: _jsx(FlatList, { contentContainerStyle: bottomInset ? { paddingBottom: bottomInset } : undefined, data: rowItems, keyboardDismissMode: keyboardDismissMode, keyboardShouldPersistTaps: keyboardShouldPersistTaps, keyExtractor: (item) => String(item.index), ListFooterComponent: loadingMore ? (_jsx(View, { className: "py-4", children: _jsx(Feedback.Spinner, { size: "small" }) })) : undefined, onEndReached: onLoadMore ? () => onLoadMore() : undefined, onEndReachedThreshold: 0.5, refreshControl: _jsx(RefreshControl, { colors: [theme.primary], onRefresh: handleRefresh, refreshing: showRefreshing }), renderItem: renderItem, scrollEnabled: true, showsVerticalScrollIndicator: false, testID: testID }) }));
182
+ }, children: _jsx(FlatList, { ...listProps, contentContainerStyle: [
183
+ bottomInset ? { paddingBottom: bottomInset } : undefined,
184
+ contentContainerStyle,
185
+ ], data: rowItems, keyboardDismissMode: keyboardDismissMode, keyboardShouldPersistTaps: keyboardShouldPersistTaps, keyExtractor: rowKey, ListFooterComponent: loadingMore ? (_jsx(View, { className: "py-4", children: _jsx(Feedback.Spinner, { size: "small" }) })) : undefined, onEndReached: onLoadMore
186
+ ? () => {
187
+ if (hasMore && !loadingMore && !showRefreshing)
188
+ onLoadMore();
189
+ }
190
+ : undefined, onEndReachedThreshold: listProps?.onEndReachedThreshold ?? 0.5, ref: listRef, refreshControl: _jsx(RefreshControl, { colors: [theme.primary], onRefresh: handleRefresh, refreshing: showRefreshing }), renderItem: renderItem, scrollEnabled: true, showsVerticalScrollIndicator: false, testID: testID }) }));
170
191
  }
171
192
  function Footer({ children }) {
172
193
  const { items } = useListView();
@@ -15,6 +15,7 @@ export function toRowItem(item, index, mapKeys) {
15
15
  if (typeof item === "string" || typeof item === "number") {
16
16
  return {
17
17
  id: item,
18
+ index,
18
19
  rowTitle: String(item),
19
20
  rowSubtitle: undefined,
20
21
  readonly: false,
@@ -22,11 +23,11 @@ export function toRowItem(item, index, mapKeys) {
22
23
  }
23
24
  const rowItem = Object.create(Object.getPrototypeOf(item));
24
25
  Object.assign(rowItem, item, {
25
- id: item[mapper.id] || "N/A",
26
- index: index,
27
- rowTitle: item[mapper.rowTitle] || "N/A",
28
- rowSubtitle: item[mapper.rowSubtitle] || undefined,
29
- readonly: item[mapper.readOnly] || false,
26
+ id: item[mapper.id] ?? index,
27
+ index,
28
+ rowTitle: item[mapper.rowTitle] ?? "N/A",
29
+ rowSubtitle: item[mapper.rowSubtitle] ?? undefined,
30
+ readonly: item[mapper.readOnly] ?? false,
30
31
  });
31
32
  return rowItem;
32
33
  }