@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.
- package/README.md +156 -29
- package/dist/Actions/Actions.Button.d.ts +10 -12
- package/dist/Actions/Actions.Button.js +9 -11
- package/dist/Actions/Actions.Dock.d.ts +20 -0
- package/dist/Actions/Actions.Dock.js +89 -0
- package/dist/Actions/Actions.FAB.d.ts +1 -2
- package/dist/Actions/Actions.FAB.js +5 -7
- package/dist/Actions/Actions.IconButton.d.ts +1 -2
- package/dist/Actions/Actions.IconButton.js +2 -2
- package/dist/Actions/index.d.ts +4 -0
- package/dist/Actions/index.js +3 -0
- package/dist/Dialog/BottomSheet/BottomSheet.Root.js +22 -34
- package/dist/Dialog/Dialog.ActionSheet.js +2 -16
- package/dist/Dialog/Dialog.Confirmation.js +7 -5
- package/dist/Dialog/Dialog.EditValue.js +3 -1
- package/dist/Dialog/Dialog.Sheet.d.ts +3 -3
- package/dist/Dialog/Dialog.Sheet.js +18 -25
- package/dist/Display/Display.Accordion.js +7 -3
- package/dist/Display/Display.AmountSummary.d.ts +28 -0
- package/dist/Display/Display.AmountSummary.js +25 -0
- package/dist/Display/Display.Carousel.d.ts +27 -4
- package/dist/Display/Display.Carousel.js +112 -12
- package/dist/Display/Display.ExpoImage.d.ts +8 -0
- package/dist/Display/Display.ExpoImage.js +15 -0
- package/dist/Display/Display.Image.d.ts +10 -6
- package/dist/Display/Display.Image.js +39 -6
- package/dist/Display/Display.ListItem.d.ts +1 -2
- package/dist/Display/Display.ListItem.js +3 -3
- package/dist/Display/Display.Logo.d.ts +2 -2
- package/dist/Display/Display.Logo.js +3 -2
- package/dist/Display/Display.MediaCard.d.ts +2 -2
- package/dist/Display/Display.MediaEmbed.d.ts +2 -2
- package/dist/Display/Display.ProductCard.d.ts +2 -2
- package/dist/Display/Display.Story.d.ts +3 -3
- package/dist/Display/Display.Story.js +5 -4
- package/dist/Display/Display.StoryTray.d.ts +4 -4
- package/dist/Display/Display.StoryTray.js +3 -2
- package/dist/Display/Display.StoryViewer.d.ts +3 -3
- package/dist/Display/Display.StoryViewer.js +6 -3
- package/dist/Display/index.d.ts +9 -0
- package/dist/Display/index.js +4 -0
- package/dist/Feedback/Feedback.DomainSplashScreen.d.ts +13 -4
- package/dist/Feedback/Feedback.DomainSplashScreen.js +68 -53
- package/dist/Feedback/Feedback.EmptyState.d.ts +3 -4
- package/dist/Feedback/Feedback.EmptyState.js +4 -3
- package/dist/Feedback/Feedback.Error.d.ts +1 -2
- package/dist/Feedback/Feedback.Error.js +2 -2
- package/dist/Feedback/Feedback.Loading.d.ts +6 -2
- package/dist/Feedback/Feedback.Loading.js +2 -2
- package/dist/Feedback/Feedback.Progress.d.ts +6 -3
- package/dist/Feedback/Feedback.Progress.js +29 -19
- package/dist/Feedback/Feedback.ProgressCircle.d.ts +3 -2
- package/dist/Feedback/Feedback.ProgressCircle.js +10 -7
- package/dist/Feedback/Feedback.Skeleton.d.ts +4 -4
- package/dist/Feedback/Feedback.Skeleton.js +15 -3
- package/dist/Feedback/Feedback.Spinner.d.ts +3 -3
- package/dist/Feedback/Feedback.Spinner.js +40 -9
- package/dist/Feedback/Feedback.Splash.d.ts +2 -2
- package/dist/Feedback/Feedback.Splash.js +5 -3
- package/dist/Feedback/Toast.context.d.ts +64 -3
- package/dist/Feedback/Toast.context.js +112 -31
- package/dist/Feedback/index.d.ts +15 -1
- package/dist/Feedback/index.js +4 -1
- package/dist/Form/Form.Checkbox.d.ts +1 -2
- package/dist/Form/Form.Checkbox.js +2 -2
- package/dist/Form/Form.DatePicker.d.ts +1 -2
- package/dist/Form/Form.DatePicker.js +2 -2
- package/dist/Form/Form.Dropdown.d.ts +15 -3
- package/dist/Form/Form.Dropdown.js +21 -6
- package/dist/Form/Form.ErrorSummary.d.ts +23 -0
- package/dist/Form/Form.ErrorSummary.js +53 -0
- package/dist/Form/Form.Input.d.ts +1 -2
- package/dist/Form/Form.Input.js +2 -2
- package/dist/Form/Form.Select.d.ts +1 -2
- package/dist/Form/Form.Select.js +6 -19
- package/dist/Form/index.d.ts +3 -0
- package/dist/Form/index.js +2 -0
- package/dist/Navigation/Navigation.AppBar.d.ts +2 -6
- package/dist/Navigation/Navigation.AppBar.js +9 -7
- package/dist/Navigation/Navigation.BottomNav.d.ts +1 -3
- package/dist/Navigation/Navigation.BottomNav.js +7 -6
- package/dist/Navigation/Navigation.Header.d.ts +2 -2
- package/dist/Navigation/Navigation.Menu.js +10 -3
- package/dist/Navigation/Navigation.Search.d.ts +41 -0
- package/dist/Navigation/Navigation.Search.js +47 -0
- package/dist/Navigation/Navigation.SearchBar.d.ts +1 -2
- package/dist/Navigation/Navigation.SearchBar.js +16 -19
- package/dist/Navigation/Navigation.SegmentedControl.d.ts +11 -3
- package/dist/Navigation/Navigation.SegmentedControl.js +29 -16
- package/dist/Navigation/index.d.ts +11 -2
- package/dist/Navigation/index.js +2 -0
- package/dist/branding.d.ts +3 -2
- package/dist/primitives/BaseButton.d.ts +10 -12
- package/dist/primitives/BaseButton.js +9 -11
- package/dist/theme/typography.d.ts +1 -1
- package/dist/utils/image.d.ts +3 -0
- package/dist/utils/image.js +13 -0
- package/dist/utils/keys.d.ts +1 -0
- package/dist/utils/keys.js +7 -0
- package/dist/utils/progress.d.ts +1 -0
- package/dist/utils/progress.js +5 -0
- package/dist/utils/reorder.js +2 -5
- package/dist/utils/useReducedMotion.d.ts +1 -0
- package/dist/utils/useReducedMotion.js +43 -0
- package/dist/utils/useSheetTranslation.d.ts +5 -0
- package/dist/utils/useSheetTranslation.js +23 -0
- package/dist/views/AppSplashView.d.ts +3 -2
- package/dist/views/AppSplashView.js +3 -2
- package/dist/views/AuthFormView.d.ts +2 -2
- package/dist/views/FormViewV2.d.ts +2 -2
- package/dist/views/ListViewV2.context.d.ts +3 -1
- package/dist/views/ListViewV2.context.js +41 -21
- package/dist/views/ListViewV2.d.ts +15 -7
- package/dist/views/ListViewV2.js +44 -23
- package/dist/views/types/IListView.js +6 -5
- package/dist/widgets/CollectionOptions.js +9 -7
- package/dist/widgets/FilterBar.d.ts +1 -1
- package/dist/widgets/FilterBar.js +1 -1
- package/dist/widgets/FilterBuilder.js +7 -7
- package/dist/widgets/HomeHeader.d.ts +2 -2
- package/dist/widgets/PromoModal.js +3 -1
- package/dist/widgets/ScreenHeader.d.ts +2 -2
- 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-[
|
|
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
|
|
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-[
|
|
26
|
-
medium: "py-2.5 px-4 min-h-[
|
|
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,
|
|
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:
|
|
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:
|
|
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,
|
|
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:
|
|
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<"
|
|
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,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;
|
package/dist/utils/reorder.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
+
import { uniqueKeys } from "./keys.js";
|
|
1
2
|
export function reorderKeys(items, keyExtractor) {
|
|
2
|
-
|
|
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,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
|
|
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?:
|
|
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 {
|
|
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(
|
|
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?:
|
|
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?:
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
5
|
-
import {
|
|
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
|
-
|
|
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;
|
package/dist/views/ListViewV2.js
CHANGED
|
@@ -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 (
|
|
59
|
-
|
|
62
|
+
if (debouncedQuery !== undefined) {
|
|
63
|
+
onSearchRef.current?.(debouncedQuery);
|
|
60
64
|
}
|
|
61
|
-
}, [debouncedQuery
|
|
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
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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, {
|
|
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]
|
|
26
|
-
index
|
|
27
|
-
rowTitle: item[mapper.rowTitle]
|
|
28
|
-
rowSubtitle: item[mapper.rowSubtitle]
|
|
29
|
-
readonly: item[mapper.readOnly]
|
|
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
|
}
|