@sincpro/mobile-ui 1.1.0 → 2.0.1

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 (129) hide show
  1. package/README.md +156 -29
  2. package/assets/brand-mesh.png +0 -0
  3. package/assets/fonts/FFL.txt +57 -0
  4. package/assets/fonts/Satoshi-Black.otf +0 -0
  5. package/assets/fonts/Satoshi-Bold.otf +0 -0
  6. package/assets/fonts/Satoshi-Medium.otf +0 -0
  7. package/assets/fonts/Satoshi-Regular.otf +0 -0
  8. package/dist/Actions/Actions.Button.d.ts +10 -12
  9. package/dist/Actions/Actions.Button.js +9 -11
  10. package/dist/Actions/Actions.Dock.d.ts +20 -0
  11. package/dist/Actions/Actions.Dock.js +89 -0
  12. package/dist/Actions/Actions.FAB.d.ts +1 -2
  13. package/dist/Actions/Actions.FAB.js +5 -7
  14. package/dist/Actions/Actions.IconButton.d.ts +1 -2
  15. package/dist/Actions/Actions.IconButton.js +2 -2
  16. package/dist/Actions/index.d.ts +4 -0
  17. package/dist/Actions/index.js +3 -0
  18. package/dist/Dialog/BottomSheet/BottomSheet.Root.js +22 -34
  19. package/dist/Dialog/Dialog.ActionSheet.js +2 -16
  20. package/dist/Dialog/Dialog.Confirmation.js +7 -5
  21. package/dist/Dialog/Dialog.EditValue.js +3 -1
  22. package/dist/Dialog/Dialog.Sheet.d.ts +3 -3
  23. package/dist/Dialog/Dialog.Sheet.js +18 -25
  24. package/dist/Display/Display.Accordion.js +7 -3
  25. package/dist/Display/Display.AmountSummary.d.ts +28 -0
  26. package/dist/Display/Display.AmountSummary.js +25 -0
  27. package/dist/Display/Display.Carousel.d.ts +27 -4
  28. package/dist/Display/Display.Carousel.js +112 -12
  29. package/dist/Display/Display.ExpoImage.d.ts +8 -0
  30. package/dist/Display/Display.ExpoImage.js +15 -0
  31. package/dist/Display/Display.Image.d.ts +10 -6
  32. package/dist/Display/Display.Image.js +39 -6
  33. package/dist/Display/Display.ListItem.d.ts +1 -2
  34. package/dist/Display/Display.ListItem.js +3 -3
  35. package/dist/Display/Display.Logo.d.ts +2 -2
  36. package/dist/Display/Display.Logo.js +3 -2
  37. package/dist/Display/Display.MediaCard.d.ts +2 -2
  38. package/dist/Display/Display.MediaEmbed.d.ts +2 -2
  39. package/dist/Display/Display.ProductCard.d.ts +2 -2
  40. package/dist/Display/Display.Story.d.ts +3 -3
  41. package/dist/Display/Display.Story.js +5 -4
  42. package/dist/Display/Display.StoryTray.d.ts +4 -4
  43. package/dist/Display/Display.StoryTray.js +3 -2
  44. package/dist/Display/Display.StoryViewer.d.ts +3 -3
  45. package/dist/Display/Display.StoryViewer.js +6 -3
  46. package/dist/Display/index.d.ts +9 -0
  47. package/dist/Display/index.js +4 -0
  48. package/dist/Feedback/Feedback.DomainSplashScreen.d.ts +13 -4
  49. package/dist/Feedback/Feedback.DomainSplashScreen.js +68 -53
  50. package/dist/Feedback/Feedback.EmptyState.d.ts +3 -4
  51. package/dist/Feedback/Feedback.EmptyState.js +4 -3
  52. package/dist/Feedback/Feedback.Error.d.ts +1 -2
  53. package/dist/Feedback/Feedback.Error.js +2 -2
  54. package/dist/Feedback/Feedback.Loading.d.ts +6 -2
  55. package/dist/Feedback/Feedback.Loading.js +2 -2
  56. package/dist/Feedback/Feedback.Progress.d.ts +6 -3
  57. package/dist/Feedback/Feedback.Progress.js +29 -19
  58. package/dist/Feedback/Feedback.ProgressCircle.d.ts +3 -2
  59. package/dist/Feedback/Feedback.ProgressCircle.js +10 -7
  60. package/dist/Feedback/Feedback.Skeleton.d.ts +4 -4
  61. package/dist/Feedback/Feedback.Skeleton.js +15 -3
  62. package/dist/Feedback/Feedback.Spinner.d.ts +3 -3
  63. package/dist/Feedback/Feedback.Spinner.js +40 -9
  64. package/dist/Feedback/Feedback.Splash.d.ts +2 -2
  65. package/dist/Feedback/Feedback.Splash.js +5 -3
  66. package/dist/Feedback/Toast.context.d.ts +64 -3
  67. package/dist/Feedback/Toast.context.js +112 -31
  68. package/dist/Feedback/index.d.ts +15 -1
  69. package/dist/Feedback/index.js +4 -1
  70. package/dist/Form/Form.Checkbox.d.ts +1 -2
  71. package/dist/Form/Form.Checkbox.js +2 -2
  72. package/dist/Form/Form.DatePicker.d.ts +1 -2
  73. package/dist/Form/Form.DatePicker.js +2 -2
  74. package/dist/Form/Form.Dropdown.d.ts +15 -3
  75. package/dist/Form/Form.Dropdown.js +21 -6
  76. package/dist/Form/Form.ErrorSummary.d.ts +23 -0
  77. package/dist/Form/Form.ErrorSummary.js +53 -0
  78. package/dist/Form/Form.Input.d.ts +1 -2
  79. package/dist/Form/Form.Input.js +2 -2
  80. package/dist/Form/Form.Select.d.ts +1 -2
  81. package/dist/Form/Form.Select.js +6 -19
  82. package/dist/Form/index.d.ts +3 -0
  83. package/dist/Form/index.js +2 -0
  84. package/dist/Navigation/Navigation.AppBar.d.ts +2 -6
  85. package/dist/Navigation/Navigation.AppBar.js +9 -7
  86. package/dist/Navigation/Navigation.BottomNav.d.ts +1 -3
  87. package/dist/Navigation/Navigation.BottomNav.js +7 -6
  88. package/dist/Navigation/Navigation.Header.d.ts +2 -2
  89. package/dist/Navigation/Navigation.Menu.js +10 -3
  90. package/dist/Navigation/Navigation.Search.d.ts +41 -0
  91. package/dist/Navigation/Navigation.Search.js +47 -0
  92. package/dist/Navigation/Navigation.SearchBar.d.ts +1 -2
  93. package/dist/Navigation/Navigation.SearchBar.js +16 -19
  94. package/dist/Navigation/Navigation.SegmentedControl.d.ts +11 -3
  95. package/dist/Navigation/Navigation.SegmentedControl.js +29 -16
  96. package/dist/Navigation/index.d.ts +11 -2
  97. package/dist/Navigation/index.js +2 -0
  98. package/dist/branding.d.ts +3 -2
  99. package/dist/primitives/BaseButton.d.ts +10 -12
  100. package/dist/primitives/BaseButton.js +9 -11
  101. package/dist/theme/typography.d.ts +1 -1
  102. package/dist/utils/image.d.ts +3 -0
  103. package/dist/utils/image.js +13 -0
  104. package/dist/utils/keys.d.ts +1 -0
  105. package/dist/utils/keys.js +7 -0
  106. package/dist/utils/progress.d.ts +1 -0
  107. package/dist/utils/progress.js +5 -0
  108. package/dist/utils/reorder.js +2 -5
  109. package/dist/utils/useReducedMotion.d.ts +1 -0
  110. package/dist/utils/useReducedMotion.js +43 -0
  111. package/dist/utils/useSheetTranslation.d.ts +5 -0
  112. package/dist/utils/useSheetTranslation.js +23 -0
  113. package/dist/views/AppSplashView.d.ts +3 -2
  114. package/dist/views/AppSplashView.js +3 -2
  115. package/dist/views/AuthFormView.d.ts +2 -2
  116. package/dist/views/FormViewV2.d.ts +2 -2
  117. package/dist/views/ListViewV2.context.d.ts +3 -1
  118. package/dist/views/ListViewV2.context.js +41 -21
  119. package/dist/views/ListViewV2.d.ts +15 -7
  120. package/dist/views/ListViewV2.js +44 -23
  121. package/dist/views/types/IListView.js +6 -5
  122. package/dist/widgets/CollectionOptions.js +9 -7
  123. package/dist/widgets/FilterBar.d.ts +1 -1
  124. package/dist/widgets/FilterBar.js +1 -1
  125. package/dist/widgets/FilterBuilder.js +7 -7
  126. package/dist/widgets/HomeHeader.d.ts +2 -2
  127. package/dist/widgets/PromoModal.js +3 -1
  128. package/dist/widgets/ScreenHeader.d.ts +2 -2
  129. package/package.json +5 -1
@@ -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
  }
@@ -7,6 +7,7 @@ import Pressable from "../primitives/Pressable.js";
7
7
  import { themeToVars, useTheme } from "../theme/index.js";
8
8
  import { Typography } from "../Typography/index.js";
9
9
  import { describeCriteria, moveCollectionEntry, validateCriteria, } from "../utils/criteria.js";
10
+ import { useReducedMotion } from "../utils/useReducedMotion.js";
10
11
  import FilterBuilder from "./FilterBuilder.js";
11
12
  import QueryChoice from "./QueryChoice.js";
12
13
  import { vars } from "nativewind";
@@ -24,6 +25,7 @@ const sections = [
24
25
  ];
25
26
  function OptionsEditor({ value, fields, sortFields = [], groupFields = [], savedQueries = [], onApply, onClose, onSave, title = "Configurar colección", testID, }) {
26
27
  const theme = useTheme();
28
+ const reducedMotion = useReducedMotion();
27
29
  const insets = useSafeAreaInsets();
28
30
  const [draft, setDraft] = useState(value);
29
31
  const [section, setSection] = useState("filter");
@@ -56,7 +58,7 @@ function OptionsEditor({ value, fields, sortFields = [], groupFields = [], saved
56
58
  }),
57
59
  }
58
60
  : { ...current, groupBy: next });
59
- return (_jsx(Modal, { animationType: "slide", onRequestClose: close, presentationStyle: "fullScreen", visible: true, children: _jsxs(View, { accessibilityViewIsModal: true, onAccessibilityEscape: close, style: [
61
+ return (_jsx(Modal, { animationType: reducedMotion ? "none" : "slide", onRequestClose: close, presentationStyle: "fullScreen", visible: true, children: _jsxs(View, { accessibilityViewIsModal: true, onAccessibilityEscape: close, style: [
60
62
  vars(themeToVars(theme)),
61
63
  {
62
64
  flex: 1,
@@ -64,7 +66,7 @@ function OptionsEditor({ value, fields, sortFields = [], groupFields = [], saved
64
66
  paddingTop: insets.top,
65
67
  paddingBottom: insets.bottom,
66
68
  },
67
- ], testID: testID, children: [_jsxs(View, { className: "flex-row items-center justify-between gap-3 px-4", children: [_jsx(Typography.Text, { accessibilityRole: "header", semibold: true, style: { flex: 1, minWidth: 0 }, variant: "bodyReadable", children: title }), _jsx(IconButton, { accessibilityLabel: "Cancelar configuraci\u00F3n", accessibleSizing: true, disabled: saving, icon: "close", onPress: close })] }), _jsx(View, { accessibilityRole: "tablist", className: "flex-row flex-wrap px-4 gap-2", children: sections.map((item) => (_jsx(Pressable, { accessibilityLabel: item.label, accessibilityRole: "tab", accessibilityState: {
69
+ ], testID: testID, children: [_jsxs(View, { className: "flex-row items-center justify-between gap-3 px-4", children: [_jsx(Typography.Text, { accessibilityRole: "header", semibold: true, style: { flex: 1, minWidth: 0 }, variant: "bodyReadable", children: title }), _jsx(IconButton, { accessibilityLabel: "Cancelar configuraci\u00F3n", disabled: saving, icon: "close", onPress: close })] }), _jsx(View, { accessibilityRole: "tablist", className: "flex-row flex-wrap px-4 gap-2", children: sections.map((item) => (_jsx(Pressable, { accessibilityLabel: item.label, accessibilityRole: "tab", accessibilityState: {
68
70
  selected: item.id === section,
69
71
  disabled: editing || saving,
70
72
  }, disabled: editing || saving, onPress: () => {
@@ -75,11 +77,11 @@ function OptionsEditor({ value, fields, sortFields = [], groupFields = [], saved
75
77
  }, style: { minHeight: 48, justifyContent: "center", paddingHorizontal: 8 }, children: _jsx(Typography.Text, { className: section === item.id ? "text-text-accent" : "text-text-secondary", semibold: section === item.id, variant: "bodyLarge", children: item.label }) }, item.id))) }), _jsxs(KeyboardForm, { contentContainerStyle: { padding: 16, gap: 16 }, children: [!editing ? (_jsx(Typography.Text, { className: "text-text-secondary", variant: "bodyLarge", children: describeCriteria(draft.filter, fields) })) : null, section === "filter" ? (_jsx(FilterBuilder, { fields: fields, onChange: (filter) => {
76
78
  setError(undefined);
77
79
  setDraft({ ...draft, filter });
78
- }, onEditingChange: setEditing, value: draft.filter })) : section === "saved" ? (_jsxs(_Fragment, { children: [_jsx(Typography.Text, { variant: "bodyReadable", children: "Una configuraci\u00F3n incluye b\u00FAsqueda, condiciones, orden y niveles de agrupaci\u00F3n. Cargarla todav\u00EDa no modifica la colecci\u00F3n." }), !savedQueries.length ? (_jsx(Typography.Text, { children: "No hay configuraciones guardadas." })) : (savedQueries.map((saved) => (_jsx(Button, { accessibleSizing: true, disabled: saving, onPress: () => {
80
+ }, onEditingChange: setEditing, value: draft.filter })) : section === "saved" ? (_jsxs(_Fragment, { children: [_jsx(Typography.Text, { variant: "bodyReadable", children: "Una configuraci\u00F3n incluye b\u00FAsqueda, condiciones, orden y niveles de agrupaci\u00F3n. Cargarla todav\u00EDa no modifica la colecci\u00F3n." }), !savedQueries.length ? (_jsx(Typography.Text, { children: "No hay configuraciones guardadas." })) : (savedQueries.map((saved) => (_jsx(Button, { disabled: saving, onPress: () => {
79
81
  setDraft(saved.query);
80
82
  setError(undefined);
81
83
  setStatus(`Configuración ${saved.label} cargada. Pulsa Aplicar configuración.`);
82
- }, title: `Cargar ${saved.label}`, variant: "secondary" }, saved.id)))), onSave ? (_jsxs(_Fragment, { children: [_jsx(Input, { accessibleSizing: true, editable: !saving, label: "Nombre de la configuraci\u00F3n", onChangeText: setLabel, testID: "collection-query-name", value: label }), _jsx(Button, { accessibleSizing: true, loading: saving, onPress: () => {
84
+ }, title: `Cargar ${saved.label}`, variant: "secondary" }, saved.id)))), onSave ? (_jsxs(_Fragment, { children: [_jsx(Input, { editable: !saving, label: "Nombre de la configuraci\u00F3n", onChangeText: setLabel, testID: "collection-query-name", value: label }), _jsx(Button, { loading: saving, onPress: () => {
83
85
  const invalid = problem();
84
86
  if (invalid || !label.trim()) {
85
87
  setError(invalid ?? "Escribe un nombre para guardar.");
@@ -102,7 +104,7 @@ function OptionsEditor({ value, fields, sortFields = [], groupFields = [], saved
102
104
  : "Los grupos se anidan en este orden. La app aplica los niveles al conjunto completo." }), !ordered.length ? (_jsx(Typography.Text, { children: section === "sort" ? "Sin orden configurado." : "Sin agrupación." })) : null, ordered.map((id, index) => {
103
105
  const field = available.find((item) => item.id === id);
104
106
  const sort = draft.sort.find((entry) => entry.field === id);
105
- return (_jsxs(View, { className: "gap-2 border-b border-border-light pb-3", children: [_jsxs(Typography.Text, { semibold: true, variant: "bodyReadable", children: [index + 1, ". ", field?.label ?? `${id} (no disponible)`] }), section === "sort" ? (_jsx(Button, { accessibleSizing: true, onPress: () => setDraft({
107
+ return (_jsxs(View, { className: "gap-2 border-b border-border-light pb-3", children: [_jsxs(Typography.Text, { semibold: true, variant: "bodyReadable", children: [index + 1, ". ", field?.label ?? `${id} (no disponible)`] }), section === "sort" ? (_jsx(Button, { onPress: () => setDraft({
106
108
  ...draft,
107
109
  sort: draft.sort.map((entry) => entry.field === id
108
110
  ? {
@@ -110,12 +112,12 @@ function OptionsEditor({ value, fields, sortFields = [], groupFields = [], saved
110
112
  direction: entry.direction === "asc" ? "desc" : "asc",
111
113
  }
112
114
  : entry),
113
- }), title: `${field?.label ?? id}: ${sort?.direction === "asc" ? (field?.ascLabel ?? "Ascendente") : (field?.descLabel ?? "Descendente")}`, variant: "secondary" })) : null, _jsxs(View, { className: "flex-row flex-wrap gap-2", children: [_jsx(Button, { accessibilityLabel: `Subir ${field?.label ?? id}`, accessibleSizing: true, disabled: index === 0, onPress: () => updateOrder(moveCollectionEntry(ordered, index, -1)), title: "Subir", variant: "link" }), _jsx(Button, { accessibilityLabel: `Bajar ${field?.label ?? id}`, accessibleSizing: true, disabled: index === ordered.length - 1, onPress: () => updateOrder(moveCollectionEntry(ordered, index, 1)), title: "Bajar", variant: "link" }), _jsx(Button, { accessibilityLabel: `Quitar ${field?.label ?? id}`, accessibleSizing: true, onPress: () => updateOrder(ordered.filter((key) => key !== id)), title: "Quitar", variant: "link" })] })] }, id));
115
+ }), title: `${field?.label ?? id}: ${sort?.direction === "asc" ? (field?.ascLabel ?? "Ascendente") : (field?.descLabel ?? "Descendente")}`, variant: "secondary" })) : null, _jsxs(View, { className: "flex-row flex-wrap gap-2", children: [_jsx(Button, { accessibilityLabel: `Subir ${field?.label ?? id}`, disabled: index === 0, onPress: () => updateOrder(moveCollectionEntry(ordered, index, -1)), title: "Subir", variant: "link" }), _jsx(Button, { accessibilityLabel: `Bajar ${field?.label ?? id}`, disabled: index === ordered.length - 1, onPress: () => updateOrder(moveCollectionEntry(ordered, index, 1)), title: "Bajar", variant: "link" }), _jsx(Button, { accessibilityLabel: `Quitar ${field?.label ?? id}`, onPress: () => updateOrder(ordered.filter((key) => key !== id)), title: "Quitar", variant: "link" })] })] }, id));
114
116
  }), available.some((field) => !ordered.includes(field.id)) ? (_jsx(QueryChoice, { label: section === "sort"
115
117
  ? "Añadir criterio de orden"
116
118
  : "Añadir nivel de agrupación", onChange: (id) => updateOrder([...ordered, id]), options: available
117
119
  .filter((field) => !ordered.includes(field.id))
118
- .map((field) => ({ label: field.label, value: field.id })), value: undefined })) : null] })), status ? (_jsx(Typography.Text, { accessibilityLiveRegion: "polite", children: status })) : null, error ? (_jsx(Typography.Text, { accessibilityLiveRegion: "polite", className: "text-danger", children: error })) : null] }), _jsxs(View, { className: "px-4 py-3 border-t border-border-light gap-2", children: [editing ? (_jsx(Typography.Text, { children: "Guarda o cancela la condici\u00F3n antes de aplicar." })) : null, _jsx(Button, { accessibleSizing: true, disabled: editing || saving, onPress: () => {
120
+ .map((field) => ({ label: field.label, value: field.id })), value: undefined })) : null] })), status ? (_jsx(Typography.Text, { accessibilityLiveRegion: "polite", children: status })) : null, error ? (_jsx(Typography.Text, { accessibilityLiveRegion: "polite", className: "text-danger", children: error })) : null] }), _jsxs(View, { className: "px-4 py-3 border-t border-border-light gap-2", children: [editing ? (_jsx(Typography.Text, { children: "Guarda o cancela la condici\u00F3n antes de aplicar." })) : null, _jsx(Button, { disabled: editing || saving, onPress: () => {
119
121
  const invalid = problem();
120
122
  if (invalid) {
121
123
  setError(invalid);
@@ -8,7 +8,7 @@ export interface FilterBarOption {
8
8
  export interface FilterBarProps {
9
9
  filters: readonly FilterBarOption[];
10
10
  onFilterPress: (id: string) => void;
11
- search?: Omit<SearchBarProps, "accessibleSizing">;
11
+ search?: SearchBarProps;
12
12
  onOpenFilters?: () => void;
13
13
  filtersLabel?: string;
14
14
  onClearFilters?: () => void;
@@ -10,5 +10,5 @@ export function FilterBar({ filters, onFilterPress, search, onOpenFilters, filte
10
10
  const { fontScale } = useWindowDimensions();
11
11
  const selectedCount = filters.filter((filter) => filter.selected).length;
12
12
  const filterLayout = layout === "auto" ? (fontScale > 1.3 ? "wrap" : "scroll") : layout;
13
- return (_jsxs(View, { className: cn("gap-2", className), testID: testID, children: [search || onOpenFilters ? (_jsxs(View, { className: "flex-row items-center gap-2", children: [search ? (_jsx(View, { className: "flex-1 min-w-0", children: _jsx(SearchBar, { ...search, accessibleSizing: true }) })) : null, onOpenFilters ? (_jsx(IconButton, { accessibilityLabel: filtersLabel, accessibleSizing: true, icon: "sliders", onPress: onOpenFilters })) : null] })) : null, _jsx(FilterButtonGroup, { className: "p-0", layout: filterLayout, testID: testID ? `${testID}-options` : undefined, children: filters.map((filter) => (_jsx(FilterButton, { active: filter.selected, disabled: filter.disabled, onPress: () => onFilterPress(filter.id), testID: testID ? `${testID}-${filter.id}` : undefined, title: filter.label, variant: "mobile" }, filter.id))) }), resultsLabel || (selectedCount > 0 && onClearFilters) ? (_jsxs(View, { className: "flex-row flex-wrap items-center justify-between gap-x-3", children: [resultsLabel ? (_jsx(Typography.Text, { accessibilityLiveRegion: "polite", className: "text-text-secondary", variant: "bodyLarge", children: resultsLabel })) : null, selectedCount > 0 && onClearFilters ? (_jsx(Pressable, { accessibilityLabel: clearLabel, accessibilityRole: "button", onPress: onClearFilters, style: { minHeight: 48, minWidth: 48, justifyContent: "center" }, children: _jsx(Typography.Text, { className: "text-text-accent", variant: "bodyLarge", children: clearLabel }) })) : null] })) : null] }));
13
+ return (_jsxs(View, { className: cn("gap-2", className), testID: testID, children: [search || onOpenFilters ? (_jsxs(View, { className: "flex-row items-center gap-2", children: [search ? (_jsx(View, { className: "flex-1 min-w-0", children: _jsx(SearchBar, { ...search }) })) : null, onOpenFilters ? (_jsx(IconButton, { accessibilityLabel: filtersLabel, icon: "sliders", onPress: onOpenFilters })) : null] })) : null, _jsx(FilterButtonGroup, { className: "p-0", layout: filterLayout, testID: testID ? `${testID}-options` : undefined, children: filters.map((filter) => (_jsx(FilterButton, { active: filter.selected, disabled: filter.disabled, onPress: () => onFilterPress(filter.id), testID: testID ? `${testID}-${filter.id}` : undefined, title: filter.label, variant: "mobile" }, filter.id))) }), resultsLabel || (selectedCount > 0 && onClearFilters) ? (_jsxs(View, { className: "flex-row flex-wrap items-center justify-between gap-x-3", children: [resultsLabel ? (_jsx(Typography.Text, { accessibilityLiveRegion: "polite", className: "text-text-secondary", variant: "bodyLarge", children: resultsLabel })) : null, selectedCount > 0 && onClearFilters ? (_jsx(Pressable, { accessibilityLabel: clearLabel, accessibilityRole: "button", onPress: onClearFilters, style: { minHeight: 48, minWidth: 48, justifyContent: "center" }, children: _jsx(Typography.Text, { className: "text-text-accent", variant: "bodyLarge", children: clearLabel }) })) : null] })) : null] }));
14
14
  }
@@ -21,7 +21,7 @@ export default function FilterBuilder({ fields, value, onChange, onEditingChange
21
21
  current = current?.kind === "group" ? current.children[index] : undefined;
22
22
  }
23
23
  if (current?.kind !== "group")
24
- return (_jsxs(View, { className: className, testID: testID, children: [_jsx(Typography.Text, { accessibilityLiveRegion: "polite", children: "Este grupo ya no est\u00E1 disponible. La configuraci\u00F3n cambi\u00F3." }), _jsx(Button, { accessibleSizing: true, onPress: () => {
24
+ return (_jsxs(View, { className: className, testID: testID, children: [_jsx(Typography.Text, { accessibilityLiveRegion: "polite", children: "Este grupo ya no est\u00E1 disponible. La configuraci\u00F3n cambi\u00F3." }), _jsx(Button, { onPress: () => {
25
25
  setPath([]);
26
26
  setEditing(null);
27
27
  }, title: "Volver a condiciones", variant: "secondary" })] }));
@@ -71,11 +71,11 @@ export default function FilterBuilder({ fields, value, onChange, onEditingChange
71
71
  values[index] = next;
72
72
  changeCondition({ ...condition, values });
73
73
  };
74
- return field?.type === "choice" ? (_jsx(QueryChoice, { label: label, onChange: update, options: field.options ?? [], value: condition.values[index] }, index)) : field?.type === "date" ? (_jsx(DatePicker, { accessibleSizing: true, label: label, onChange: (date) => update(`${String(date.getFullYear()).padStart(4, "0")}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`), value: parseCriteriaDate(condition.values[index]) }, index)) : (_jsx(Input, { accessibleSizing: true, keyboardType: field?.type === "number" ? "numbers-and-punctuation" : "default", label: label, onChangeText: update, testID: `criteria-value-${index}`, value: condition.values[index] === undefined ||
74
+ return field?.type === "choice" ? (_jsx(QueryChoice, { label: label, onChange: update, options: field.options ?? [], value: condition.values[index] }, index)) : field?.type === "date" ? (_jsx(DatePicker, { label: label, onChange: (date) => update(`${String(date.getFullYear()).padStart(4, "0")}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`), value: parseCriteriaDate(condition.values[index]) }, index)) : (_jsx(Input, { keyboardType: field?.type === "number" ? "numbers-and-punctuation" : "default", label: label, onChangeText: update, testID: `criteria-value-${index}`, value: condition.values[index] === undefined ||
75
75
  Number.isNaN(condition.values[index])
76
76
  ? ""
77
77
  : String(condition.values[index]) }, index));
78
- }), error ? (_jsx(Typography.Text, { accessibilityLiveRegion: "polite", className: "text-danger", children: error })) : null, _jsx(Button, { accessibleSizing: true, onPress: () => {
78
+ }), error ? (_jsx(Typography.Text, { accessibilityLiveRegion: "polite", className: "text-danger", children: error })) : null, _jsx(Button, { onPress: () => {
79
79
  const next = {
80
80
  ...condition,
81
81
  values: field?.type === "number"
@@ -94,18 +94,18 @@ export default function FilterBuilder({ fields, value, onChange, onEditingChange
94
94
  children[editing.index] = next;
95
95
  changeGroup({ ...group, children });
96
96
  setEditing(null);
97
- }, title: "Guardar condici\u00F3n" }), _jsx(Button, { accessibleSizing: true, onPress: () => setEditing(null), title: "Cancelar condici\u00F3n", variant: "secondary" })] })) : (_jsxs(_Fragment, { children: [path.length ? (_jsx(Button, { accessibleSizing: true, onPress: () => setPath(path.slice(0, -1)), title: "Volver al grupo anterior", variant: "link" })) : null, _jsx(QueryChoice, { label: "Coinciden", onChange: (join) => changeGroup({ ...group, join }), options: [
97
+ }, title: "Guardar condici\u00F3n" }), _jsx(Button, { onPress: () => setEditing(null), title: "Cancelar condici\u00F3n", variant: "secondary" })] })) : (_jsxs(_Fragment, { children: [path.length ? (_jsx(Button, { onPress: () => setPath(path.slice(0, -1)), title: "Volver al grupo anterior", variant: "link" })) : null, _jsx(QueryChoice, { label: "Coinciden", onChange: (join) => changeGroup({ ...group, join }), options: [
98
98
  { label: "Todas las condiciones", value: "all" },
99
99
  { label: "Cualquiera de las condiciones", value: "any" },
100
100
  { label: "Ninguna de las condiciones", value: "none" },
101
- ], value: group.join }), !group.children.length ? (_jsx(Typography.Text, { className: "text-text-secondary", children: "Sin condiciones en este grupo." })) : null, group.children.map((child, index) => (_jsxs(View, { className: "border-b border-border-light pb-3 gap-2", children: [_jsx(Typography.Text, { variant: "bodyReadable", children: describeCriteria(child, fields) }), _jsxs(View, { className: "flex-row flex-wrap gap-2", children: [_jsx(Button, { accessibleSizing: true, onPress: () => child.kind === "group"
101
+ ], value: group.join }), !group.children.length ? (_jsx(Typography.Text, { className: "text-text-secondary", children: "Sin condiciones en este grupo." })) : null, group.children.map((child, index) => (_jsxs(View, { className: "border-b border-border-light pb-3 gap-2", children: [_jsx(Typography.Text, { variant: "bodyReadable", children: describeCriteria(child, fields) }), _jsxs(View, { className: "flex-row flex-wrap gap-2", children: [_jsx(Button, { onPress: () => child.kind === "group"
102
102
  ? setPath([...path, index])
103
103
  : (setError(undefined), setEditing({ index, condition: child })), title: child.kind === "group"
104
104
  ? `Abrir grupo ${index + 1}`
105
- : `Editar condición ${index + 1}`, variant: "secondary" }), _jsx(Button, { accessibleSizing: true, onPress: () => changeGroup({
105
+ : `Editar condición ${index + 1}`, variant: "secondary" }), _jsx(Button, { onPress: () => changeGroup({
106
106
  ...group,
107
107
  children: group.children.filter((_, position) => position !== index),
108
- }), title: `Eliminar ${child.kind === "group" ? "grupo" : "condición"} ${index + 1}`, variant: "link" })] })] }, index))), _jsx(Button, { accessibleSizing: true, disabled: !fields.some((item) => item.operators.length), onPress: addCondition, title: "A\u00F1adir condici\u00F3n", variant: "secondary" }), _jsx(Button, { accessibleSizing: true, onPress: () => {
108
+ }), title: `Eliminar ${child.kind === "group" ? "grupo" : "condición"} ${index + 1}`, variant: "link" })] })] }, index))), _jsx(Button, { disabled: !fields.some((item) => item.operators.length), onPress: addCondition, title: "A\u00F1adir condici\u00F3n", variant: "secondary" }), _jsx(Button, { onPress: () => {
109
109
  changeGroup({
110
110
  ...group,
111
111
  children: [...group.children, { kind: "group", join: "any", children: [] }],
@@ -1,9 +1,9 @@
1
+ import type { ImageSource } from "../utils/image.js";
1
2
  import React, { ReactNode } from "react";
2
- import { type ImageSourcePropType } from "react-native";
3
3
  interface HomeHeaderProps {
4
4
  readonly title?: string;
5
5
  readonly logo?: ReactNode;
6
- readonly logoSource?: ImageSourcePropType;
6
+ readonly logoSource?: ImageSource;
7
7
  readonly backButton?: boolean;
8
8
  readonly onBack?: () => void;
9
9
  }
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "nativewind/jsx-runtime";
2
2
  import GradientSurface from "../Display/Display.GradientSurface.js";
3
3
  import { useTheme } from "../theme/index.js";
4
4
  import { Typography } from "../Typography/index.js";
5
+ import { useReducedMotion } from "../utils/useReducedMotion.js";
5
6
  import { Modal, TouchableOpacity, TouchableWithoutFeedback, View } from "react-native";
6
7
  import { useSafeAreaInsets } from "react-native-safe-area-context";
7
8
  /**
@@ -23,9 +24,10 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
23
24
  */
24
25
  export function PromoModal({ visible, onDismiss, title, body, icon: Icon, headerGradient, ctaLabel, onPressCta, dismissLabel = "Ahora no", badge, accentColor, }) {
25
26
  const theme = useTheme();
27
+ const reducedMotion = useReducedMotion();
26
28
  const insets = useSafeAreaInsets();
27
29
  const accent = accentColor ?? theme.accent;
28
- return (_jsxs(Modal, { animationType: "fade", onRequestClose: onDismiss, presentationStyle: "overFullScreen", statusBarTranslucent: true, transparent: true, visible: visible, children: [_jsx(TouchableWithoutFeedback, { onPress: onDismiss, children: _jsx(View, { style: {
30
+ return (_jsxs(Modal, { animationType: reducedMotion ? "none" : "fade", onRequestClose: onDismiss, presentationStyle: "overFullScreen", statusBarTranslucent: true, transparent: true, visible: visible, children: [_jsx(TouchableWithoutFeedback, { onPress: onDismiss, children: _jsx(View, { style: {
29
31
  flex: 1,
30
32
  backgroundColor: "rgba(0,0,0,0.55)",
31
33
  alignItems: "center",
@@ -1,4 +1,4 @@
1
- import { type ImageSourcePropType } from "react-native";
1
+ import type { ImageSource } from "../utils/image.js";
2
2
  export declare enum EVariantScreenHeader {
3
3
  FLAT_HEADER = "FLAT_HEADER",
4
4
  ROUNDED_HEADER = "ROUNDED_HEADER",
@@ -11,7 +11,7 @@ interface ScreenHeaderProps {
11
11
  centerTitle?: boolean;
12
12
  subtitle?: string;
13
13
  logo?: boolean;
14
- logoSource?: ImageSourcePropType;
14
+ logoSource?: ImageSource;
15
15
  rightComponent?: React.ReactNode;
16
16
  children?: React.ReactNode;
17
17
  withGradientContainer?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sincpro/mobile-ui",
3
- "version": "1.1.0",
3
+ "version": "2.0.1",
4
4
  "description": "Sincpro Mobile design system: generic, presentational React Native components (no domain logic)",
5
5
  "main": "expo/AppEntry.js",
6
6
  "types": "./dist/index.d.ts",
@@ -9,6 +9,7 @@
9
9
  },
10
10
  "files": [
11
11
  "dist",
12
+ "assets",
12
13
  "tailwind.preset.js",
13
14
  "README.md"
14
15
  ],
@@ -22,6 +23,7 @@
22
23
  "./Dialog/NativeSheet": "./dist/Dialog/Dialog.NativeSheet.js",
23
24
  "./Actions": "./dist/Actions/index.js",
24
25
  "./Display": "./dist/Display/index.js",
26
+ "./Display/ExpoImage": "./dist/Display/Display.ExpoImage.js",
25
27
  "./Feedback": "./dist/Feedback/index.js",
26
28
  "./Form": "./dist/Form/index.js",
27
29
  "./Navigation": "./dist/Navigation/index.js",
@@ -63,6 +65,7 @@
63
65
  "expo-checkbox": ">=58.0.0",
64
66
  "expo-clipboard": ">=58.0.0",
65
67
  "expo-linear-gradient": ">=58.0.0",
68
+ "expo-image": ">=58.0.0",
66
69
  "expo-localization": ">=58.0.0",
67
70
  "nativewind": ">=4.2.7",
68
71
  "react": ">=19.3",
@@ -124,6 +127,7 @@
124
127
  "expo-checkbox": "~58.0.2",
125
128
  "expo-clipboard": "~58.0.4",
126
129
  "expo-font": "~58.0.11",
130
+ "expo-image": "~58.0.18",
127
131
  "expo-linear-gradient": "~58.0.3",
128
132
  "expo-localization": "~58.0.5",
129
133
  "expo-status-bar": "~58.0.4",