@sincpro/mobile-ui 2.0.1 → 3.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 (198) hide show
  1. package/README.md +7 -2
  2. package/dist/Actions/Actions.Button.d.ts +2 -1
  3. package/dist/Actions/Actions.Button.js +31 -37
  4. package/dist/Actions/Actions.Dock.d.ts +6 -6
  5. package/dist/Actions/Actions.Dock.js +14 -72
  6. package/dist/Actions/Actions.FAB.d.ts +2 -1
  7. package/dist/Actions/Actions.FilterButton.js +1 -1
  8. package/dist/Actions/Actions.IconButton.d.ts +3 -4
  9. package/dist/Actions/Actions.IconButton.js +2 -2
  10. package/dist/Dialog/Confirmation.context.d.ts +5 -14
  11. package/dist/Dialog/Confirmation.context.js +11 -51
  12. package/dist/Dialog/Dialog.ActionSheet.d.ts +8 -7
  13. package/dist/Dialog/Dialog.ActionSheet.js +45 -19
  14. package/dist/Dialog/Dialog.Confirmation.d.ts +14 -11
  15. package/dist/Dialog/Dialog.Confirmation.js +38 -14
  16. package/dist/Dialog/Dialog.EditValue.d.ts +21 -16
  17. package/dist/Dialog/Dialog.EditValue.js +73 -14
  18. package/dist/Dialog/Dialog.Sheet.d.ts +8 -24
  19. package/dist/Dialog/Dialog.Sheet.js +15 -60
  20. package/dist/Dialog/index.d.ts +14 -14
  21. package/dist/Dialog/index.js +5 -3
  22. package/dist/Dialog/requestConfirmation.d.ts +3 -0
  23. package/dist/Dialog/requestConfirmation.js +13 -0
  24. package/dist/Display/Display.Accordion.js +1 -1
  25. package/dist/Display/Display.Avatar.d.ts +6 -6
  26. package/dist/Display/Display.Avatar.js +2 -2
  27. package/dist/Display/Display.Badge.js +4 -4
  28. package/dist/Display/Display.Card.js +5 -2
  29. package/dist/Display/Display.Carousel.js +1 -1
  30. package/dist/Display/Display.CircleButton.d.ts +3 -8
  31. package/dist/Display/Display.CircleButton.js +2 -2
  32. package/dist/Display/Display.CopyableText.js +1 -5
  33. package/dist/Display/Display.Date.d.ts +3 -2
  34. package/dist/Display/Display.Date.js +3 -3
  35. package/dist/Display/Display.GradientSurface.d.ts +52 -9
  36. package/dist/Display/Display.GradientSurface.js +9 -14
  37. package/dist/Display/Display.Icon.d.ts +7 -11
  38. package/dist/Display/Display.Icon.js +15 -30
  39. package/dist/Display/Display.Image.d.ts +2 -1
  40. package/dist/Display/Display.Image.js +1 -1
  41. package/dist/Display/Display.KeyValue.js +1 -1
  42. package/dist/Display/Display.ListItem.js +1 -1
  43. package/dist/Display/Display.MenuButton.js +4 -4
  44. package/dist/Display/Display.MenuCard.d.ts +3 -8
  45. package/dist/Display/Display.MenuCard.js +2 -2
  46. package/dist/Display/Display.MenuGrid.d.ts +2 -7
  47. package/dist/Display/Display.MenuGrid.js +1 -1
  48. package/dist/Display/Display.Monetary.d.ts +2 -6
  49. package/dist/Display/Display.Monetary.js +2 -2
  50. package/dist/Display/Display.PriceTag.js +1 -1
  51. package/dist/Display/Display.Rating.js +1 -1
  52. package/dist/Display/Display.SearchHistory.d.ts +2 -1
  53. package/dist/Display/Display.SearchHistory.js +1 -1
  54. package/dist/Display/Display.StatCard.d.ts +2 -1
  55. package/dist/Display/Display.StatCard.js +1 -1
  56. package/dist/Display/Display.StoryViewer.js +10 -6
  57. package/dist/Display/Display.SwipeableRow.d.ts +2 -1
  58. package/dist/Display/Display.Timeline.js +1 -1
  59. package/dist/Display/icon-map.d.ts +344 -0
  60. package/dist/Display/icon-map.js +86 -0
  61. package/dist/Display/index.d.ts +2 -0
  62. package/dist/Display/index.js +1 -0
  63. package/dist/Feedback/Feedback.Banner.js +13 -12
  64. package/dist/Feedback/Feedback.Empty.js +1 -1
  65. package/dist/Feedback/Feedback.EmptyState.d.ts +2 -1
  66. package/dist/Feedback/Feedback.EmptyState.js +1 -1
  67. package/dist/Feedback/Feedback.Error.js +1 -1
  68. package/dist/Feedback/Feedback.PermissionCard.d.ts +2 -1
  69. package/dist/Feedback/Feedback.PermissionCard.js +2 -2
  70. package/dist/Feedback/Toast.context.js +3 -3
  71. package/dist/Form/Form.ControlledDropdown.d.ts +5 -11
  72. package/dist/Form/Form.ControlledDropdown.js +5 -2
  73. package/dist/Form/Form.DatePicker.d.ts +5 -9
  74. package/dist/Form/Form.DatePicker.js +116 -21
  75. package/dist/Form/Form.Dropdown.d.ts +11 -16
  76. package/dist/Form/Form.Dropdown.js +54 -90
  77. package/dist/Form/Form.FractionalQuantityInput.js +4 -11
  78. package/dist/Form/Form.Input.d.ts +3 -3
  79. package/dist/Form/Form.KeyboardForm.d.ts +2 -2
  80. package/dist/Form/Form.KeyboardForm.js +2 -2
  81. package/dist/Form/Form.KeyboardToolbar.d.ts +2 -1
  82. package/dist/Form/Form.KeyboardToolbar.js +3 -7
  83. package/dist/Form/Form.PasswordInput.js +1 -1
  84. package/dist/Form/Form.QuantitySelector.js +1 -1
  85. package/dist/Form/Form.Select.d.ts +12 -8
  86. package/dist/Form/Form.Select.js +64 -31
  87. package/dist/Form/Form.Slider.d.ts +4 -7
  88. package/dist/Form/Form.Slider.js +11 -10
  89. package/dist/Form/Form.Switch.js +2 -1
  90. package/dist/Form/index.d.ts +6 -14
  91. package/dist/Form/index.js +0 -11
  92. package/dist/Navigation/Navigation.AppBar.d.ts +2 -1
  93. package/dist/Navigation/Navigation.AppBar.js +2 -2
  94. package/dist/Navigation/Navigation.BottomNav.d.ts +4 -8
  95. package/dist/Navigation/Navigation.BottomNav.js +2 -2
  96. package/dist/Navigation/Navigation.CollapsingHeader.js +3 -2
  97. package/dist/Navigation/Navigation.Menu.d.ts +16 -11
  98. package/dist/Navigation/Navigation.Menu.js +97 -42
  99. package/dist/Navigation/Navigation.Search.js +2 -2
  100. package/dist/Navigation/Navigation.SearchBar.js +3 -4
  101. package/dist/Navigation/Navigation.SegmentedControl.d.ts +7 -25
  102. package/dist/Navigation/Navigation.SegmentedControl.js +18 -39
  103. package/dist/Navigation/Navigation.Steps.js +1 -1
  104. package/dist/Navigation/Navigation.Tabs.d.ts +2 -1
  105. package/dist/Navigation/index.d.ts +0 -6
  106. package/dist/Navigation/index.js +0 -8
  107. package/dist/Typography/Typography.SelectableText.js +3 -7
  108. package/dist/Typography/Typography.Text.d.ts +1 -6
  109. package/dist/Typography/Typography.Text.js +3 -6
  110. package/dist/index.d.ts +1 -4
  111. package/dist/index.js +1 -4
  112. package/dist/layouts/AuthContainer.d.ts +4 -2
  113. package/dist/layouts/AuthContainer.js +3 -4
  114. package/dist/layouts/Container.d.ts +0 -4
  115. package/dist/layouts/Container.js +3 -4
  116. package/dist/layouts/ScrollContainer.d.ts +7 -3
  117. package/dist/layouts/ScrollContainer.js +5 -4
  118. package/dist/primitives/BaseButton.d.ts +2 -1
  119. package/dist/theme/build.js +0 -4
  120. package/dist/theme/css-vars.js +0 -4
  121. package/dist/theme/dark.js +2 -6
  122. package/dist/theme/default.js +1 -5
  123. package/dist/theme/index.d.ts +1 -1
  124. package/dist/theme/index.js +1 -1
  125. package/dist/theme/types.d.ts +0 -11
  126. package/dist/theme/typography.d.ts +8 -7
  127. package/dist/theme/typography.js +25 -4
  128. package/dist/utils/timezone.d.ts +0 -5
  129. package/dist/views/AuthFormView.d.ts +3 -1
  130. package/dist/views/AuthFormView.js +2 -2
  131. package/dist/views/FormViewV2.d.ts +6 -13
  132. package/dist/views/FormViewV2.js +29 -25
  133. package/dist/views/ListViewV2.context.d.ts +5 -5
  134. package/dist/views/ListViewV2.d.ts +23 -18
  135. package/dist/views/ListViewV2.js +45 -48
  136. package/dist/views/types/IListView.d.ts +5 -4
  137. package/dist/views/types/IListView.js +18 -15
  138. package/dist/widgets/CollectionOptions.d.ts +2 -2
  139. package/dist/widgets/CollectionOptions.js +92 -65
  140. package/dist/widgets/FilterBar.js +1 -1
  141. package/dist/widgets/PromoModal.js +10 -4
  142. package/dist/widgets/QuickActions.d.ts +2 -7
  143. package/dist/widgets/QuickActions.js +1 -1
  144. package/dist/widgets/ReorderableList.js +3 -3
  145. package/dist/widgets/TimeZoneSelectorModal.d.ts +1 -6
  146. package/dist/widgets/TimeZoneSelectorModal.js +17 -12
  147. package/dist/widgets/index.d.ts +0 -11
  148. package/dist/widgets/index.js +0 -7
  149. package/package.json +35 -38
  150. package/tailwind.preset.js +0 -4
  151. package/dist/Dialog/BottomSheet/BottomSheet.Root.d.ts +0 -324
  152. package/dist/Dialog/BottomSheet/BottomSheet.Root.js +0 -373
  153. package/dist/Dialog/BottomSheet/index.d.ts +0 -1
  154. package/dist/Dialog/BottomSheet/index.js +0 -1
  155. package/dist/Dialog/Dialog.NativeSheet.d.ts +0 -10
  156. package/dist/Dialog/Dialog.NativeSheet.js +0 -21
  157. package/dist/Form/Form.Button.d.ts +0 -3
  158. package/dist/Form/Form.Button.js +0 -1
  159. package/dist/Form/Form.FAB.d.ts +0 -3
  160. package/dist/Form/Form.FAB.js +0 -1
  161. package/dist/Form/Form.FilterButton.d.ts +0 -3
  162. package/dist/Form/Form.FilterButton.js +0 -1
  163. package/dist/Form/Form.IconButton.d.ts +0 -3
  164. package/dist/Form/Form.IconButton.js +0 -1
  165. package/dist/Form/Form.SearchBar.d.ts +0 -13
  166. package/dist/Form/Form.SearchBar.js +0 -26
  167. package/dist/Navigation/Navigation.Header.d.ts +0 -16
  168. package/dist/Navigation/Navigation.Header.js +0 -17
  169. package/dist/Navigation/Navigation.HeaderToolbar.d.ts +0 -15
  170. package/dist/Navigation/Navigation.HeaderToolbar.js +0 -18
  171. package/dist/Navigation/Navigation.TabIcon.d.ts +0 -18
  172. package/dist/Navigation/Navigation.TabIcon.js +0 -12
  173. package/dist/Navigation/Navigation.TabItem.d.ts +0 -19
  174. package/dist/Navigation/Navigation.TabItem.js +0 -13
  175. package/dist/icons/OdooIcon.d.ts +0 -11
  176. package/dist/icons/OdooIcon.js +0 -11
  177. package/dist/icons/PrinterIcon.d.ts +0 -9
  178. package/dist/icons/PrinterIcon.js +0 -9
  179. package/dist/layouts/TabNavigatorLayout.d.ts +0 -26
  180. package/dist/layouts/TabNavigatorLayout.js +0 -60
  181. package/dist/utils/icon.d.ts +0 -1
  182. package/dist/utils/icon.js +0 -1
  183. package/dist/widgets/CircleButton.d.ts +0 -3
  184. package/dist/widgets/CircleButton.js +0 -1
  185. package/dist/widgets/GeoPermissionCard.d.ts +0 -13
  186. package/dist/widgets/GeoPermissionCard.js +0 -24
  187. package/dist/widgets/HomeHeader.d.ts +0 -15
  188. package/dist/widgets/HomeHeader.js +0 -13
  189. package/dist/widgets/MenuCard.d.ts +0 -3
  190. package/dist/widgets/MenuCard.js +0 -1
  191. package/dist/widgets/MenuGrid.d.ts +0 -5
  192. package/dist/widgets/MenuGrid.js +0 -1
  193. package/dist/widgets/MenuSection.d.ts +0 -3
  194. package/dist/widgets/MenuSection.js +0 -1
  195. package/dist/widgets/ScreenHeader.d.ts +0 -25
  196. package/dist/widgets/ScreenHeader.js +0 -32
  197. package/dist/widgets/TimeZoneSelector.d.ts +0 -12
  198. package/dist/widgets/TimeZoneSelector.js +0 -24
@@ -1,41 +1,74 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "nativewind/jsx-runtime";
2
+ import { Host, Picker } from "@expo/ui";
3
+ import { Actions } from "../Actions/index.js";
4
+ import Sheet from "../Dialog/Dialog.Sheet.js";
2
5
  import Icon from "../Display/Display.Icon.js";
3
6
  import Pressable from "../primitives/Pressable.js";
4
- import { themeToVars, useTheme } from "../theme/index.js";
7
+ import { relativeLuminance, useTheme } from "../theme/index.js";
5
8
  import { cn } from "../theme/tw.js";
6
9
  import { Typography } from "../Typography/index.js";
7
- import { useSheetTranslation } from "../utils/useSheetTranslation.js";
8
- import { vars } from "nativewind";
9
- import { useState } from "react";
10
- import { Animated, Modal, ScrollView, View } from "react-native";
11
- import { useSafeAreaInsets } from "react-native-safe-area-context";
12
- function Select({ options, value, onChange, label, placeholder = "Seleccionar…", title, disabled = false, className, testID, showCloseButton = true, }) {
10
+ import { useRef, useState } from "react";
11
+ import { ActivityIndicator, Platform, View } from "react-native";
12
+ function SelectOptions({ options, value, blocked, onSelect, }) {
13
13
  const theme = useTheme();
14
- const insets = useSafeAreaInsets();
14
+ if (!options.length) {
15
+ return _jsx(Typography.Text, { children: "No hay opciones disponibles." });
16
+ }
17
+ return (_jsx(View, { className: "gap-2", children: options.map((option, index) => {
18
+ const selected = option.value === value;
19
+ return (_jsxs(Pressable, { accessibilityLabel: option.label, accessibilityRole: "radio", accessibilityState: { disabled: blocked, selected }, className: cn("flex-row items-center gap-3 rounded-lg border px-3", selected ? "border-primary bg-primary/10" : "border-border-default"), disabled: blocked, onPress: () => onSelect(option.value), style: { minHeight: 48 }, children: [_jsx(Typography.Text, { className: "flex-1 text-text-primary", children: option.label }), selected ? _jsx(Icon, { color: theme.primary, name: "check", size: 18 }) : null] }, `${index}:${String(option.value)}`));
20
+ }) }));
21
+ }
22
+ function Select({ options, value, onChange, onBlur, label, placeholder = "Seleccionar…", title, confirmLabel = "Aplicar", disabled = false, readOnly = false, loading = false, error, className, testID, }) {
23
+ const theme = useTheme();
24
+ const appearance = relativeLuminance(theme.bg.card) < 0.179 ? "dark" : "light";
25
+ const saving = useRef(false);
15
26
  const [open, setOpen] = useState(false);
16
- const selected = options.find((o) => o.value === value) ?? null;
17
- const { translateY } = useSheetTranslation(open);
18
- return (_jsxs(View, { className: cn("gap-1", className), testID: testID, children: [label ? (_jsx(Typography.Text, { className: "font-medium text-text-primary", variant: "bodySmall", children: label })) : null, _jsxs(Pressable, { accessibilityHint: "Abre la lista de opciones", accessibilityLabel: label
27
+ const [draftValue, setDraftValue] = useState(null);
28
+ const [pending, setPending] = useState(false);
29
+ const [failure, setFailure] = useState();
30
+ const selected = options.find((option) => option.value === value);
31
+ const draftIndex = options.findIndex((option) => option.value === draftValue);
32
+ const blocked = disabled || readOnly || loading || pending;
33
+ const android = Platform.OS === "android";
34
+ const close = () => {
35
+ if (saving.current)
36
+ return;
37
+ setOpen(false);
38
+ onBlur?.();
39
+ };
40
+ const commit = async (next) => {
41
+ if (next === null || blocked || saving.current)
42
+ return;
43
+ saving.current = true;
44
+ setPending(true);
45
+ setFailure(undefined);
46
+ try {
47
+ await onChange(next);
48
+ setOpen(false);
49
+ onBlur?.();
50
+ }
51
+ catch (cause) {
52
+ setFailure(cause instanceof Error && cause.message.trim()
53
+ ? cause.message
54
+ : "No se pudo aplicar la selección. Vuelve a intentarlo.");
55
+ }
56
+ finally {
57
+ saving.current = false;
58
+ setPending(false);
59
+ }
60
+ };
61
+ return (_jsxs(View, { className: cn("gap-1", className), testID: testID, children: [label ? (_jsx(Typography.Text, { className: "text-text-primary", semibold: true, variant: "bodySmall", children: label })) : null, _jsxs(Pressable, { accessibilityHint: "Abre el selector de opciones", accessibilityLabel: label
19
62
  ? `${label}: ${selected?.label ?? placeholder}`
20
- : (selected?.label ?? placeholder), accessibilityRole: "button", accessibilityState: { disabled, expanded: open }, className: "flex-row items-center justify-between rounded-lg border border-input bg-bg-card px-3 min-h-[48px]", disabled: disabled, onPress: () => setOpen(true), style: { minHeight: 48, minWidth: 48 }, children: [_jsx(Typography.Text, { className: cn("flex-1 text-sm", selected ? "text-text-primary" : "text-text-tertiary"), style: { paddingVertical: 12 }, children: selected ? selected.label : placeholder }), _jsx(Icon, { color: theme.icon.secondary, name: "chevron-down", size: 18 })] }), _jsx(Modal, { animationType: "none", onRequestClose: () => setOpen(false), presentationStyle: "overFullScreen", transparent: true, visible: open, children: _jsxs(View, { className: "flex-1 justify-end", children: [_jsx(View, { accessibilityElementsHidden: true, className: "absolute inset-0 bg-black/50", importantForAccessibility: "no-hide-descendants", onResponderRelease: () => setOpen(false), onStartShouldSetResponder: () => true }), _jsxs(Animated.View, { accessibilityViewIsModal: true, className: "rounded-t-3xl", importantForAccessibility: "yes", onAccessibilityEscape: () => setOpen(false), style: [
21
- vars(themeToVars(theme)),
22
- {
23
- transform: [{ translateY }],
24
- maxHeight: "70%",
25
- backgroundColor: theme.bg.card,
26
- paddingBottom: insets.bottom + 8,
27
- },
28
- ], children: [_jsx(View, { className: "items-center pt-3 pb-1", children: _jsx(View, { className: "w-10 h-1 rounded-full bg-border-strong" }) }), title || showCloseButton ? (_jsxs(View, { className: "flex-row items-center justify-between px-4", children: [_jsx(Typography.Text, { accessibilityRole: "header", className: "flex-1 text-text-primary py-2", semibold: true, variant: "body", children: title ?? label ?? "Seleccionar" }), showCloseButton ? (_jsx(Pressable, { accessibilityLabel: "Cerrar opciones", onPress: () => setOpen(false), style: {
29
- minWidth: 48,
30
- minHeight: 48,
31
- justifyContent: "center",
32
- alignItems: "center",
33
- }, children: _jsx(Typography.Text, { className: "text-text-accent", variant: "bodySmall", children: "Cerrar" }) })) : null] })) : null, _jsx(ScrollView, { children: options.map((option) => {
34
- const isSelected = option.value === value;
35
- return (_jsxs(Pressable, { accessibilityRole: "menuitem", accessibilityState: { selected: isSelected }, className: "flex-row items-center justify-between px-4 py-3 border-b border-border-light", onPress: () => {
36
- onChange(option.value);
37
- setOpen(false);
38
- }, style: { minHeight: 48, minWidth: 48 }, children: [_jsx(Typography.Text, { className: cn("flex-1 text-sm", isSelected ? "text-text-accent font-semibold" : "text-text-primary"), children: option.label }), isSelected ? (_jsx(Icon, { color: theme.text.accent, name: "checkmark", size: 18 })) : null] }, String(option.value)));
39
- }) })] })] }) })] }));
63
+ : (selected?.label ?? placeholder), accessibilityRole: "button", accessibilityState: { disabled: blocked, expanded: open, busy: loading || pending }, className: "flex-row items-center gap-3 rounded-lg border border-input bg-bg-card px-3", disabled: blocked, onPress: () => {
64
+ setDraftValue(selected?.value ?? null);
65
+ setFailure(undefined);
66
+ setOpen(true);
67
+ }, style: { minHeight: 48, minWidth: 48 }, children: [_jsx(Typography.Text, { className: cn("flex-1", selected ? "text-text-primary" : "text-text-tertiary"), style: { paddingVertical: 12 }, children: selected?.label ?? placeholder }), loading || pending ? (_jsx(ActivityIndicator, { color: theme.primary })) : (_jsx(Icon, { color: theme.icon.secondary, name: "expand", size: 18 }))] }), error ? (_jsx(Typography.Text, { accessibilityLiveRegion: "polite", className: "text-danger", children: error })) : null, _jsxs(Sheet, { dismissible: !pending, footer: android ? undefined : (_jsx(Actions.Button, { disabled: blocked || draftIndex < 0, loading: pending, onPress: () => void commit(draftValue), title: confirmLabel })), onClose: close, title: title ?? label ?? "Seleccionar", visible: open, children: [open && android ? (_jsx(SelectOptions, { blocked: blocked, onSelect: (next) => void commit(next), options: options, value: value })) : null, open && !android ? (_jsx(Host, { colorScheme: appearance, matchContents: { vertical: true }, seedColor: theme.primary, style: { minHeight: 48 }, children: _jsxs(Picker, { appearance: "wheel", enabled: !blocked && options.length > 0, onValueChange: (index) => {
68
+ if (blocked || saving.current)
69
+ return;
70
+ setDraftValue(options.find((_, itemIndex) => String(itemIndex) === index)?.value ?? null);
71
+ setFailure(undefined);
72
+ }, selectedValue: draftIndex < 0 ? "" : String(draftIndex), testID: testID ? `${testID}-picker` : undefined, children: [_jsx(Picker.Item, { label: placeholder, value: "" }), options.map((option, index) => (_jsx(Picker.Item, { label: option.label, value: String(index) }, String(index))))] }) })) : null, open && !android && !options.length ? (_jsx(Typography.Text, { children: "No hay opciones disponibles." })) : null, draftValue !== null && draftIndex < 0 ? (_jsx(Typography.Text, { accessibilityRole: "alert", className: "text-danger", children: "La opci\u00F3n ya no est\u00E1 disponible. Selecciona otra." })) : null, failure ? (_jsx(Typography.Text, { accessibilityLiveRegion: "polite", accessibilityRole: "alert", className: "pt-3 text-danger", selectable: true, children: failure })) : null] })] }));
40
73
  }
41
74
  export default Select;
@@ -7,14 +7,11 @@ export interface SliderProps {
7
7
  label?: string;
8
8
  formatValue?: (value: number) => string;
9
9
  disabled?: boolean;
10
+ readOnly?: boolean;
11
+ accessibilityLabel?: string;
12
+ accessibilityHint?: string;
10
13
  className?: string;
11
14
  testID?: string;
12
15
  }
13
- /**
14
- * **Tokenized** slider (track in `primary`) on `@react-native-community/slider` — clean,
15
- * on-brand, consistent across iOS/Android. (The `@expo/ui` Slider was evaluated but its
16
- * Material style with ticks ignores the tokens → not a case where native wins.)
17
- * Subpath `Form/Slider`; **optional** peer dep.
18
- */
19
- declare function Slider({ value, onChange, min, max, step, label, formatValue, disabled, className, testID, }: SliderProps): import("react").JSX.Element;
16
+ declare function Slider({ value, onChange, min, max, step, label, formatValue, disabled, readOnly, accessibilityLabel, accessibilityHint, className, testID, }: SliderProps): import("react").JSX.Element;
20
17
  export default Slider;
@@ -1,16 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "nativewind/jsx-runtime";
2
- import RNSlider from "@react-native-community/slider";
3
- import { theme } from "../theme/index.js";
2
+ import NativeSlider from "@expo/ui/community/slider";
3
+ import { useTheme } from "../theme/index.js";
4
4
  import { cn } from "../theme/tw.js";
5
5
  import { Typography } from "../Typography/index.js";
6
6
  import { View } from "react-native";
7
- /**
8
- * **Tokenized** slider (track in `primary`) on `@react-native-community/slider` — clean,
9
- * on-brand, consistent across iOS/Android. (The `@expo/ui` Slider was evaluated but its
10
- * Material style with ticks ignores the tokens → not a case where native wins.)
11
- * Subpath `Form/Slider`; **optional** peer dep.
12
- */
13
- function Slider({ value, onChange, min = 0, max = 100, step = 1, label, formatValue, disabled = false, className, testID, }) {
14
- return (_jsxs(View, { className: cn("gap-1", className), testID: testID, children: [label || formatValue ? (_jsxs(View, { className: "flex-row items-center justify-between", children: [label ? (_jsx(Typography.Text, { className: "font-medium text-text-primary", variant: "bodySmall", children: label })) : null, formatValue ? (_jsx(Typography.Text, { className: "text-text-secondary", variant: "bodySmall", children: formatValue(value) })) : null] })) : null, _jsx(RNSlider, { disabled: disabled, maximumTrackTintColor: theme.bg.muted, maximumValue: max, minimumTrackTintColor: theme.primary, minimumValue: min, onValueChange: onChange, step: step, thumbTintColor: theme.primary, value: value })] }));
7
+ function Slider({ value, onChange, min = 0, max = 100, step = 1, label, formatValue, disabled = false, readOnly = false, accessibilityLabel, accessibilityHint, className, testID, }) {
8
+ const theme = useTheme();
9
+ const blocked = disabled || readOnly;
10
+ return (_jsxs(View, { className: cn("gap-1", className), testID: testID, children: [label || formatValue ? (_jsxs(View, { className: "flex-row items-center justify-between", children: [label ? (_jsx(Typography.Text, { className: "font-medium text-text-primary", variant: "bodySmall", children: label })) : null, formatValue ? (_jsx(Typography.Text, { className: "text-text-secondary", variant: "bodySmall", children: formatValue(value) })) : null] })) : null, _jsx(NativeSlider, { accessibilityHint: accessibilityHint, accessibilityLabel: accessibilityLabel ?? label ?? "Valor", disabled: blocked, maximumTrackTintColor: theme.container.primary, maximumValue: max, minimumTrackTintColor: theme.primary, minimumValue: min, onValueChange: (next) => {
11
+ if (blocked)
12
+ return;
13
+ const snapped = step > 0 ? min + Math.round((next - min) / step) * step : next;
14
+ onChange(Math.min(max, Math.max(min, snapped)));
15
+ }, step: step, style: { minHeight: 48 }, testID: testID ? `${testID}-slider` : undefined, thumbTintColor: theme.primary, value: value })] }));
15
16
  }
16
17
  export default Slider;
@@ -1,9 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "nativewind/jsx-runtime";
2
- import { theme } from "../theme/index.js";
2
+ import { useTheme } from "../theme/index.js";
3
3
  import { cn } from "../theme/tw.js";
4
4
  import { Typography } from "../Typography/index.js";
5
5
  import { Switch as RNSwitch, View } from "react-native";
6
6
  function Switch({ value, onValueChange, label, disabled = false, className, testID, }) {
7
+ const theme = useTheme();
7
8
  return (_jsxs(View, { className: cn("flex-row items-center justify-between gap-3", className), children: [label ? (_jsx(Typography.Text, { className: cn("flex-1 text-text-primary", disabled && "text-text-disabled"), children: label })) : null, _jsx(RNSwitch, { accessibilityLabel: label, accessibilityRole: "switch", accessibilityState: { checked: value, disabled }, disabled: disabled, ios_backgroundColor: theme.bg.muted, onValueChange: onValueChange, testID: testID, thumbColor: theme.bg.card, trackColor: { false: theme.bg.muted, true: theme.primary }, value: value })] }));
8
9
  }
9
10
  export default Switch;
@@ -1,16 +1,13 @@
1
1
  import Checkbox from "./Form.Checkbox.js";
2
2
  import ControlledCheckbox from "./Form.ControlledCheckbox.js";
3
- import ControlledDropdown, { type DropdownOption } from "./Form.ControlledDropdown.js";
3
+ import ControlledDropdown from "./Form.ControlledDropdown.js";
4
4
  import ControlledInput from "./Form.ControlledInput.js";
5
5
  import ControlledMonetaryInput from "./Form.ControlledMonetaryInput.js";
6
6
  import ControlledRadioButton from "./Form.ControlledRadioButton.js";
7
7
  import DatePicker from "./Form.DatePicker.js";
8
8
  import Dropdown from "./Form.Dropdown.js";
9
9
  import ErrorSummary from "./Form.ErrorSummary.js";
10
- import FAB from "./Form.FAB.js";
11
- import { FilterButtonGroup } from "./Form.FilterButton.js";
12
10
  import FractionalQuantityInput from "./Form.FractionalQuantityInput.js";
13
- import IconButton from "./Form.IconButton.js";
14
11
  import Input from "./Form.Input.js";
15
12
  import KeyboardForm from "./Form.KeyboardForm.js";
16
13
  import KeyboardToolbar from "./Form.KeyboardToolbar.js";
@@ -18,7 +15,6 @@ import MonetaryInput from "./Form.MonetaryInput.js";
18
15
  import PinInput from "./Form.PinInput.js";
19
16
  import QuantitySelector from "./Form.QuantitySelector.js";
20
17
  import RadioButton from "./Form.RadioButton.js";
21
- import SearchBar from "./Form.SearchBar.js";
22
18
  import Select from "./Form.Select.js";
23
19
  import Slider from "./Form.Slider.js";
24
20
  import Switch from "./Form.Switch.js";
@@ -29,10 +25,6 @@ export declare const Form: {
29
25
  KeyboardForm: typeof KeyboardForm;
30
26
  KeyboardToolbar: typeof KeyboardToolbar;
31
27
  ErrorSummary: typeof ErrorSummary;
32
- Button: import("react").FC<import("./Form.Button.js").ButtonProps> & {
33
- Icon: import("react").FC<import("./Form.Button.js").ButtonIconProps>;
34
- };
35
- IconButton: typeof IconButton;
36
28
  PasswordInput: import("react").FC<import("./Form.PasswordInput.js").PasswordFieldProps>;
37
29
  QuantitySelector: typeof QuantitySelector;
38
30
  FractionalQuantityInput: typeof FractionalQuantityInput;
@@ -43,8 +35,6 @@ export declare const Form: {
43
35
  PinInput: typeof PinInput;
44
36
  Slider: typeof Slider;
45
37
  DatePicker: typeof DatePicker;
46
- FAB: typeof FAB;
47
- SearchBar: typeof SearchBar;
48
38
  Dropdown: typeof Dropdown;
49
39
  MonetaryInput: typeof MonetaryInput;
50
40
  ControlledInput: typeof ControlledInput;
@@ -52,11 +42,13 @@ export declare const Form: {
52
42
  ControlledCheckbox: typeof ControlledCheckbox;
53
43
  ControlledDropdown: typeof ControlledDropdown;
54
44
  ControlledMonetaryInput: typeof ControlledMonetaryInput;
55
- FilterButton: import("react").FC<import("./Form.FilterButton.js").FilterButtonProps>;
56
- FilterButtonGroup: typeof FilterButtonGroup;
57
45
  };
58
- export { type DropdownOption };
46
+ export type { ControlledDropdownProps } from "./Form.ControlledDropdown.js";
47
+ export type { DatePickerProps } from "./Form.DatePicker.js";
48
+ export type { DropdownButtonProps, DropdownOption, } from "./Form.Dropdown.js";
59
49
  export type { ErrorSummaryItem, ErrorSummaryProps, } from "./Form.ErrorSummary.js";
60
50
  export type { KeyboardFormProps } from "./Form.KeyboardForm.js";
61
51
  export type { KeyboardToolbarAction, KeyboardToolbarProps, } from "./Form.KeyboardToolbar.js";
52
+ export type { SelectOption, SelectProps } from "./Form.Select.js";
53
+ export type { SliderProps } from "./Form.Slider.js";
62
54
  export default Form;
@@ -1,4 +1,3 @@
1
- import ButtonWithIcon from "./Form.Button.js";
2
1
  import Checkbox from "./Form.Checkbox.js";
3
2
  import ControlledCheckbox from "./Form.ControlledCheckbox.js";
4
3
  import ControlledDropdown from "./Form.ControlledDropdown.js";
@@ -8,10 +7,7 @@ import ControlledRadioButton from "./Form.ControlledRadioButton.js";
8
7
  import DatePicker from "./Form.DatePicker.js";
9
8
  import Dropdown from "./Form.Dropdown.js";
10
9
  import ErrorSummary from "./Form.ErrorSummary.js";
11
- import FAB from "./Form.FAB.js";
12
- import { FilterButton, FilterButtonGroup } from "./Form.FilterButton.js";
13
10
  import FractionalQuantityInput from "./Form.FractionalQuantityInput.js";
14
- import IconButton from "./Form.IconButton.js";
15
11
  import Input from "./Form.Input.js";
16
12
  import InputField from "./Form.InputField.js";
17
13
  import KeyboardForm from "./Form.KeyboardForm.js";
@@ -21,7 +17,6 @@ import PasswordInput from "./Form.PasswordInput.js";
21
17
  import PinInput from "./Form.PinInput.js";
22
18
  import QuantitySelector from "./Form.QuantitySelector.js";
23
19
  import RadioButton from "./Form.RadioButton.js";
24
- import SearchBar from "./Form.SearchBar.js";
25
20
  import Select from "./Form.Select.js";
26
21
  import Slider from "./Form.Slider.js";
27
22
  import Switch from "./Form.Switch.js";
@@ -33,8 +28,6 @@ export const Form = {
33
28
  KeyboardForm,
34
29
  KeyboardToolbar,
35
30
  ErrorSummary,
36
- Button: ButtonWithIcon,
37
- IconButton,
38
31
  PasswordInput,
39
32
  QuantitySelector,
40
33
  FractionalQuantityInput,
@@ -45,8 +38,6 @@ export const Form = {
45
38
  PinInput,
46
39
  Slider,
47
40
  DatePicker,
48
- FAB,
49
- SearchBar,
50
41
  Dropdown,
51
42
  MonetaryInput,
52
43
  ControlledInput,
@@ -54,7 +45,5 @@ export const Form = {
54
45
  ControlledCheckbox,
55
46
  ControlledDropdown,
56
47
  ControlledMonetaryInput,
57
- FilterButton,
58
- FilterButtonGroup,
59
48
  };
60
49
  export default Form;
@@ -1,10 +1,11 @@
1
+ import type { IconName } from "../Display/index.js";
1
2
  import { type GradientPreset } from "../Display/Display.GradientSurface.js";
2
3
  import { type PatternKind } from "../Display/Display.Pattern.js";
3
4
  import type { LinearGradientPoint } from "expo-linear-gradient";
4
5
  import type { ReactNode } from "react";
5
6
  export interface AppBarActionProps {
6
7
  /** Ionicon name. Optional when `label` is set (text-only CTA). */
7
- icon?: string;
8
+ icon?: IconName;
8
9
  /** Text label → renders a pill CTA ("Editar", "Nuevo", "Guardar") instead of an icon circle. */
9
10
  label?: string;
10
11
  onPress: () => void;
@@ -50,7 +50,7 @@ function AppBarAction({ icon, label, onPress, badge, accessibilityLabel, tone =
50
50
  borderRadius: 12,
51
51
  backgroundColor: bg,
52
52
  ...(showBorder ? { borderWidth: 1, borderColor: theme.border.light } : null),
53
- }, testID: testID, children: [_jsx(Icon, { color: iconColor, name: icon ?? "ellipse-outline", size: bare ? 24 : 20 }), badge != null ? (_jsx(View, { className: "absolute bg-danger rounded-full min-w-[16px] h-4 px-1 items-center justify-center", style: { top: -2, right: -2, borderWidth: 2, borderColor: theme.bg.card }, children: _jsx(Typography.Text, { style: { color: theme.text.inverse, fontSize: 9, lineHeight: 11 }, children: badge }) })) : null] }));
53
+ }, testID: testID, children: [_jsx(Icon, { color: iconColor, name: icon ?? "circle", size: bare ? 24 : 20 }), badge != null ? (_jsx(View, { className: "absolute bg-danger rounded-full min-w-[16px] h-4 px-1 items-center justify-center", style: { top: -2, right: -2, borderWidth: 2, borderColor: theme.bg.card }, children: _jsx(Typography.Text, { style: { color: theme.text.inverse, fontSize: 9, lineHeight: 11 }, children: badge }) })) : null] }));
54
54
  }
55
55
  /**
56
56
  * Flat, modern top bar (merge of Material 3 + iOS 26): tokenized surface, left-aligned
@@ -108,7 +108,7 @@ function AppBar({ title, subtitle, onBack, leading, actions, subheader, variant
108
108
  ? { borderBottomLeftRadius: 24, borderBottomRightRadius: 24, overflow: "hidden" }
109
109
  : null),
110
110
  ...(showShadow ? theme.shadow.md : null),
111
- }, testID: testID, children: [hasBackground ? (_jsx(GradientSurface, { colors: background.colors, end: background.end, padding: "none", preset: background.surface ?? "warm", radius: "none", start: background.start, style: StyleSheet.absoluteFill })) : gradient ? (_jsx(GradientSurface, { colors: theme.gradient.accent, padding: "none", radius: "none", style: StyleSheet.absoluteFill })) : null, background?.pattern ? (_jsx(Pattern, { color: background.patternColor ?? "#FFFFFF", kind: background.pattern, opacity: background.patternOpacity ?? 0.18 })) : null, showTopBar ? (_jsxs(View, { className: "flex-row items-center px-3 gap-2 min-h-[56px]", children: [_jsxs(View, { className: "flex-row items-center gap-2", style: { minWidth: 38 }, children: [onBack ? (_jsx(AppBarAction, { accessibilityLabel: "Volver", bare: backVariant === "plain", icon: "chevron-back", onDark: dark || gradient || hasBackground, onPress: onBack })) : null, leading] }), _jsxs(View, { className: cn("flex-1", centered && "items-center"), children: [title ? (_jsx(Typography.Text, { accessibilityRole: "header", className: colored ? "" : "text-text-primary", semibold: true, style: titleStyle, variant: "subtitle", children: title })) : null, subtitle ? (_jsx(Typography.Text, { className: colored ? "" : "text-text-secondary", style: subtitleStyle, variant: "caption", children: subtitle })) : null] }), _jsx(View, { className: "flex-row items-center justify-end gap-2", style: { minWidth: 38 }, children: actions })] })) : null, large && title ? (_jsxs(View, { className: cn("flex-row px-4 pb-3", largeHasNav ? "items-center gap-2" : ""), style: { paddingTop: topSpacing != null ? topSpacing : 8 }, children: [onBack ? (_jsx(AppBarAction, { accessibilityLabel: "Volver", bare: backVariant === "plain", icon: "chevron-back", onDark: dark || gradient || hasBackground, onPress: onBack })) : null, leading, accentBar && !largeHasNav ? (_jsx(View, { style: {
111
+ }, testID: testID, children: [hasBackground ? (_jsx(GradientSurface, { colors: background.colors, end: background.end, padding: "none", preset: background.surface ?? "warm", radius: "none", start: background.start, style: StyleSheet.absoluteFill })) : gradient ? (_jsx(GradientSurface, { colors: theme.gradient.accent, padding: "none", radius: "none", style: StyleSheet.absoluteFill })) : null, background?.pattern ? (_jsx(Pattern, { color: background.patternColor ?? "#FFFFFF", kind: background.pattern, opacity: background.patternOpacity ?? 0.18 })) : null, showTopBar ? (_jsxs(View, { className: "flex-row items-center px-3 gap-2 min-h-[56px]", children: [_jsxs(View, { className: "flex-row items-center gap-2", style: { minWidth: 38 }, children: [onBack ? (_jsx(AppBarAction, { accessibilityLabel: "Volver", bare: backVariant === "plain", icon: "back", onDark: dark || gradient || hasBackground, onPress: onBack })) : null, leading] }), _jsxs(View, { className: cn("flex-1", centered && "items-center"), children: [title ? (_jsx(Typography.Text, { accessibilityRole: "header", className: colored ? "" : "text-text-primary", semibold: true, style: titleStyle, variant: "subtitle", children: title })) : null, subtitle ? (_jsx(Typography.Text, { className: colored ? "" : "text-text-secondary", style: subtitleStyle, variant: "caption", children: subtitle })) : null] }), _jsx(View, { className: "flex-row items-center justify-end gap-2", style: { minWidth: 38 }, children: actions })] })) : null, large && title ? (_jsxs(View, { className: cn("flex-row px-4 pb-3", largeHasNav ? "items-center gap-2" : ""), style: { paddingTop: topSpacing != null ? topSpacing : 8 }, children: [onBack ? (_jsx(AppBarAction, { accessibilityLabel: "Volver", bare: backVariant === "plain", icon: "back", onDark: dark || gradient || hasBackground, onPress: onBack })) : null, leading, accentBar && !largeHasNav ? (_jsx(View, { style: {
112
112
  width: 4,
113
113
  marginRight: 10,
114
114
  marginVertical: 2,
@@ -1,17 +1,13 @@
1
- import type { CustomIconProps } from "../icons/props.js";
2
- import type { ComponentType } from "react";
1
+ import type { IconName } from "../Display/index.js";
3
2
  export interface BottomNavItem {
4
3
  key: string;
5
4
  label?: string;
6
- /** Icon font name (Display.Icon). Provide this OR `customIcon`. */
7
- icon?: string;
8
- activeIcon?: string;
9
- /** Brand/custom SVG icon component. Takes precedence over `icon` (for non-font brand marks). */
10
- customIcon?: ComponentType<CustomIconProps>;
5
+ icon?: IconName;
6
+ activeIcon?: IconName;
11
7
  badge?: string | number;
12
8
  }
13
9
  export interface BottomNavCenterAction {
14
- icon: string;
10
+ icon: IconName;
15
11
  onPress: () => void;
16
12
  accessibilityLabel?: string;
17
13
  }
@@ -44,8 +44,8 @@ function BottomNav({ items, value, onChange, showLabels = true, centerAction, sa
44
44
  ...(bordered ? { borderTopWidth: 1, borderTopColor: theme.border.light } : null),
45
45
  };
46
46
  const renderIconNode = (item, active, color) => {
47
- const node = item.customIcon ? (_jsx(Icon, { color: color, customIcon: item.customIcon, size: 24, type: "custom" })) : (_jsx(Icon, { color: color, name: (active ? (item.activeIcon ?? item.icon) : item.icon) ?? "", size: 24 }));
48
- return (_jsxs(View, { children: [node, item.badge != null ? (_jsx(View, { className: "absolute bg-danger rounded-full min-w-[16px] h-4 px-1 items-center justify-center", style: { top: -4, right: -10, borderWidth: 2, borderColor: surfaceColor }, children: _jsx(Typography.Text, { style: { color: theme.text.inverse, fontSize: 9, lineHeight: 11 }, children: item.badge }) })) : null] }));
47
+ const name = active ? (item.activeIcon ?? item.icon) : item.icon;
48
+ return (_jsxs(View, { children: [name ? _jsx(Icon, { color: color, name: name, size: 24 }) : null, item.badge != null ? (_jsx(View, { className: "absolute bg-danger rounded-full min-w-[16px] h-4 px-1 items-center justify-center", style: { top: -4, right: -10, borderWidth: 2, borderColor: surfaceColor }, children: _jsx(Typography.Text, { style: { color: theme.text.inverse, fontSize: 9, lineHeight: 11 }, children: item.badge }) })) : null] }));
49
49
  };
50
50
  const renderItem = (item) => {
51
51
  const active = item.key === value;
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "nativewind/jsx-runtime";
2
2
  import AppBar from "./Navigation.AppBar.js";
3
- import { theme } from "../theme/index.js";
3
+ import { useTheme } from "../theme/index.js";
4
4
  import { Typography } from "../Typography/index.js";
5
5
  import { Animated, View } from "react-native";
6
6
  import { useSafeAreaInsets } from "react-native-safe-area-context";
@@ -21,6 +21,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
21
21
  * ```
22
22
  */
23
23
  function CollapsingHeader({ title, subtitle, scrollY, onBack, actions, largeHeight = subtitle ? 72 : 56, safeArea = true, testID, }) {
24
+ const theme = useTheme();
24
25
  const insets = useSafeAreaInsets();
25
26
  const largeOpacity = scrollY.interpolate({
26
27
  inputRange: [0, largeHeight * 0.6],
@@ -42,6 +43,6 @@ function CollapsingHeader({ title, subtitle, scrollY, onBack, actions, largeHeig
42
43
  outputRange: [0, 1],
43
44
  extrapolate: "clamp",
44
45
  });
45
- return (_jsxs(View, { style: { backgroundColor: theme.bg.card }, testID: testID, children: [_jsx(View, { style: { paddingTop: safeArea ? insets.top : 0 }, children: _jsxs(View, { className: "flex-row items-center px-3 gap-2 min-h-[52px]", children: [_jsx(View, { style: { minWidth: 38 }, children: onBack ? (_jsx(AppBar.Action, { accessibilityLabel: "Volver", icon: "chevron-back", onPress: onBack })) : null }), _jsx(Animated.View, { className: "flex-1", style: { opacity: compactTitleOpacity }, children: _jsx(Typography.Text, { className: "text-text-primary", numberOfLines: 1, semibold: true, variant: "subtitle", children: title }) }), _jsx(View, { className: "flex-row items-center justify-end gap-2", style: { minWidth: 38 }, children: actions })] }) }), _jsx(Animated.View, { style: { height: largeContainerHeight, overflow: "hidden" }, children: _jsxs(Animated.View, { className: "px-4 pb-2", style: { opacity: largeOpacity, transform: [{ translateY: largeTranslate }] }, children: [_jsx(Typography.Text, { className: "text-text-primary", variant: "h2", children: title }), subtitle ? (_jsx(Typography.Text, { className: "text-text-secondary", variant: "bodySmall", children: subtitle })) : null] }) }), _jsx(View, { className: "border-b border-border-light" })] }));
46
+ return (_jsxs(View, { style: { backgroundColor: theme.bg.card }, testID: testID, children: [_jsx(View, { style: { paddingTop: safeArea ? insets.top : 0 }, children: _jsxs(View, { className: "flex-row items-center px-3 gap-2 min-h-[52px]", children: [_jsx(View, { style: { minWidth: 38 }, children: onBack ? (_jsx(AppBar.Action, { accessibilityLabel: "Volver", icon: "back", onPress: onBack })) : null }), _jsx(Animated.View, { className: "flex-1", style: { opacity: compactTitleOpacity }, children: _jsx(Typography.Text, { className: "text-text-primary", numberOfLines: 1, semibold: true, variant: "subtitle", children: title }) }), _jsx(View, { className: "flex-row items-center justify-end gap-2", style: { minWidth: 38 }, children: actions })] }) }), _jsx(Animated.View, { style: { height: largeContainerHeight, overflow: "hidden" }, children: _jsxs(Animated.View, { className: "px-4 pb-2", style: { opacity: largeOpacity, transform: [{ translateY: largeTranslate }] }, children: [_jsx(Typography.Text, { className: "text-text-primary", variant: "h2", children: title }), subtitle ? (_jsx(Typography.Text, { className: "text-text-secondary", variant: "bodySmall", children: subtitle })) : null] }) }), _jsx(View, { className: "border-b border-border-light" })] }));
46
47
  }
47
48
  export default CollapsingHeader;
@@ -1,21 +1,26 @@
1
+ import { type IconName } from "../Display/icon-map.js";
1
2
  import { type ReactNode } from "react";
2
- export interface MenuItem {
3
+ import { type StyleProp, type ViewStyle } from "react-native";
4
+ export type MenuItem = {
3
5
  label: string;
4
- icon?: string;
6
+ icon?: IconName;
5
7
  destructive?: boolean;
6
8
  disabled?: boolean;
7
- onPress: () => void;
8
- }
9
- export interface MenuSeparator {
9
+ onPress: () => void | Promise<void>;
10
+ };
11
+ export type MenuSeparator = {
10
12
  separator: true;
11
- }
13
+ };
12
14
  export type MenuEntry = MenuItem | MenuSeparator;
13
- export interface MenuProps {
14
- items: MenuEntry[];
15
+ export type MenuProps = {
16
+ items: readonly MenuEntry[];
15
17
  trigger?: ReactNode;
16
- menuWidth?: number;
18
+ title?: string;
19
+ disabled?: boolean;
20
+ accessibilityLabel?: string;
21
+ style?: StyleProp<ViewStyle>;
17
22
  testID?: string;
18
- }
19
- declare function Menu({ items, trigger, menuWidth, testID }: MenuProps): import("react").JSX.Element;
23
+ };
24
+ declare function Menu({ items, trigger, title, disabled, accessibilityLabel, style, testID, }: MenuProps): import("react").JSX.Element;
20
25
  export default Menu;
21
26
  export { Menu };
@@ -1,52 +1,107 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "nativewind/jsx-runtime";
1
+ import { jsx as _jsx } from "nativewind/jsx-runtime";
2
+ import { MenuView } from "@expo/ui/community/menu";
2
3
  import Icon from "../Display/Display.Icon.js";
3
- import Pressable from "../primitives/Pressable.js";
4
- import { useTheme } from "../theme/index.js";
5
- import { cn } from "../theme/tw.js";
6
- import { Typography } from "../Typography/index.js";
7
- import { useReducedMotion } from "../utils/useReducedMotion.js";
8
- import { cloneElement, useRef, useState } from "react";
9
- import { Dimensions, Modal, Pressable as RNPressable, View, } from "react-native";
10
- function Menu({ items, trigger, menuWidth = 220, testID }) {
4
+ import { ICON_MAP } from "../Display/icon-map.js";
5
+ import { relativeLuminance, useTheme } from "../theme/index.js";
6
+ import { unstable_getMaterialSymbolSourceAsync } from "expo-symbols";
7
+ import { useEffect, useMemo, useRef, useState } from "react";
8
+ import { ActivityIndicator, Alert, Platform, View, } from "react-native";
9
+ function Menu({ items, trigger, title, disabled = false, accessibilityLabel = "Más opciones", style, testID, }) {
11
10
  const theme = useTheme();
12
- const reducedMotion = useReducedMotion();
13
- const triggerRef = useRef(null);
14
- const [anchor, setAnchor] = useState(null);
15
- const open = () => {
16
- triggerRef.current?.measureInWindow((x, y, width, height) => {
17
- setAnchor({ x, y, width, height });
11
+ const running = useRef(false);
12
+ const [pending, setPending] = useState(false);
13
+ const blocked = disabled || pending || !items.some((entry) => !("separator" in entry));
14
+ const [sources, setSources] = useState(() => new Map());
15
+ const requests = useMemo(() => items.flatMap((entry) => "separator" in entry || !entry.icon
16
+ ? []
17
+ : [
18
+ {
19
+ icon: entry.icon,
20
+ color: entry.destructive ? theme.danger : theme.icon.secondary,
21
+ },
22
+ ]), [items, theme.danger, theme.icon.secondary]);
23
+ useEffect(() => {
24
+ if (Platform.OS !== "android" || !requests.length)
25
+ return;
26
+ let active = true;
27
+ void Promise.all(requests.map(async ({ icon, color }) => ({
28
+ key: `${icon}:${color}`,
29
+ source: await unstable_getMaterialSymbolSourceAsync(ICON_MAP[icon].md, 24, color),
30
+ }))).then((results) => {
31
+ if (!active)
32
+ return;
33
+ const next = new Map();
34
+ for (const { key, source } of results) {
35
+ if (source)
36
+ next.set(key, source);
37
+ }
38
+ setSources(next);
39
+ }, (cause) => {
40
+ console.error("[Navigation.Menu] Could not render native icons", cause);
18
41
  });
19
- };
20
- const close = () => setAnchor(null);
21
- const screen = Dimensions.get("window");
22
- const right = anchor ? Math.max(8, screen.width - (anchor.x + anchor.width)) : 8;
23
- const top = anchor ? anchor.y + anchor.height + 4 : 0;
24
- return (_jsxs(View, { collapsable: false, ref: triggerRef, testID: testID, children: [trigger ? (
25
- // eslint-disable-next-line react-hooks/refs
26
- cloneElement(trigger, { onPress: open })) : (_jsx(Pressable, { accessibilityLabel: "M\u00E1s opciones", accessibilityRole: "button", onPress: open, style: {
42
+ return () => {
43
+ active = false;
44
+ };
45
+ }, [requests]);
46
+ const groups = [[]];
47
+ items.forEach((entry, index) => {
48
+ if ("separator" in entry) {
49
+ if (groups[groups.length - 1].length)
50
+ groups.push([]);
51
+ return;
52
+ }
53
+ const color = entry.disabled
54
+ ? theme.text.disabled
55
+ : entry.destructive
56
+ ? theme.danger
57
+ : theme.text.primary;
58
+ const imageColor = entry.destructive ? theme.danger : theme.icon.secondary;
59
+ groups[groups.length - 1].push({
60
+ id: String(index),
61
+ title: entry.label,
62
+ titleColor: color,
63
+ image: entry.icon
64
+ ? Platform.OS === "ios"
65
+ ? ICON_MAP[entry.icon].sf
66
+ : sources.get(`${entry.icon}:${imageColor}`)
67
+ : undefined,
68
+ imageColor,
69
+ attributes: { disabled: blocked || entry.disabled, destructive: entry.destructive },
70
+ });
71
+ });
72
+ const sections = groups.filter((group) => group.length > 0);
73
+ const actions = sections.length > 1
74
+ ? sections.map((subactions, index) => ({
75
+ id: `section-${index}`,
76
+ title: "",
77
+ displayInline: true,
78
+ subactions,
79
+ }))
80
+ : (sections[0] ?? []);
81
+ return (_jsx(View, { pointerEvents: blocked ? "none" : "auto", style: style, testID: testID, children: _jsx(MenuView, { actions: actions, colorScheme: relativeLuminance(theme.bg.card) < 0.179 ? "dark" : "light", onPressAction: ({ nativeEvent }) => {
82
+ const entry = items.find((_, index) => String(index) === nativeEvent.event);
83
+ if (!entry || "separator" in entry || blocked || entry.disabled || running.current)
84
+ return;
85
+ running.current = true;
86
+ setPending(true);
87
+ void Promise.resolve()
88
+ .then(() => entry.onPress())
89
+ .catch((cause) => {
90
+ Alert.alert("No se pudo completar la acción", cause instanceof Error && cause.message.trim()
91
+ ? cause.message
92
+ : "Vuelve a intentarlo.");
93
+ })
94
+ .finally(() => {
95
+ running.current = false;
96
+ setPending(false);
97
+ });
98
+ }, style: { minWidth: 48, minHeight: 48 }, testID: testID ? `${testID}-native-menu` : undefined, title: title, children: _jsx(View, { accessibilityLabel: accessibilityLabel, accessibilityRole: "button", accessibilityState: { disabled: blocked, busy: pending }, accessible: true, pointerEvents: "none", style: {
27
99
  minWidth: 48,
28
100
  minHeight: 48,
29
101
  alignItems: "center",
30
102
  justifyContent: "center",
31
- }, children: _jsx(Icon, { color: theme.icon.secondary, name: "ellipsis-vertical", size: 22 }) })), _jsx(Modal, { animationType: reducedMotion ? "none" : "fade", onRequestClose: close, presentationStyle: "overFullScreen", transparent: true, visible: anchor !== null, children: _jsx(RNPressable, { onPress: close, style: { flex: 1 }, children: _jsx(View, { accessibilityViewIsModal: true, className: "rounded-lg py-1", onAccessibilityEscape: close, style: {
32
- position: "absolute",
33
- top,
34
- right,
35
- width: menuWidth,
36
- backgroundColor: theme.bg.popover,
37
- borderWidth: 1,
38
- borderColor: theme.border.light,
39
- ...theme.shadow.lg,
40
- }, children: items.map((entry, index) => {
41
- if ("separator" in entry && entry.separator) {
42
- return _jsx(View, { className: "my-1 h-px bg-border-default" }, `sep-${index}`);
43
- }
44
- const item = entry;
45
- return (_jsxs(Pressable, { accessibilityLabel: item.label, accessibilityRole: "menuitem", accessibilityState: { disabled: item.disabled }, className: "flex-row items-center gap-3 px-4 py-3", disabled: item.disabled, onPress: () => {
46
- close();
47
- item.onPress();
48
- }, style: { minHeight: 48 }, children: [item.icon ? (_jsx(Icon, { color: item.destructive ? theme.danger : theme.icon.secondary, name: item.icon, size: 18 })) : null, _jsx(Typography.Text, { className: cn("flex-1 text-sm", item.destructive ? "text-danger" : "text-text-primary", item.disabled && "text-text-disabled"), children: item.label })] }, item.label));
49
- }) }) }) })] }));
103
+ opacity: blocked ? 0.5 : 1,
104
+ }, children: pending ? (_jsx(ActivityIndicator, { color: theme.primary })) : ((trigger ?? _jsx(Icon, { color: theme.icon.secondary, name: "more-vertical", size: 22 }))) }) }) }));
50
105
  }
51
106
  export default Menu;
52
107
  export { Menu };
@@ -25,10 +25,10 @@ function FilterSheet({ children, activeCount = 0, title = "Filtros", triggerLabe
25
25
  const [visible, setVisible] = useState(false);
26
26
  const theme = useTheme();
27
27
  const countLabel = activeCount > 0 ? `, ${activeCount} activos` : "";
28
- return (_jsxs(_Fragment, { children: [_jsxs(Pressable, { accessibilityLabel: `${triggerLabel}${countLabel}`, accessibilityRole: "button", className: "min-h-11 flex-row items-center justify-center gap-2 self-start rounded-full bg-bg-muted px-4", onPress: () => setVisible(true), style: { minHeight: 44 }, testID: testID ? `${testID}-trigger` : undefined, children: [_jsx(Icon, { color: theme.icon.secondary, name: "options-outline", size: 18 }), _jsx(Typography.Text, { className: "text-text-primary", variant: "buttonSmall", children: triggerLabel }), activeCount > 0 ? (_jsx(Typography.Text, { accessibilityLabel: `${activeCount} filtros activos`, className: "text-text-accent", variant: "caption", children: activeCount })) : null] }), _jsx(Sheet, { dismissOnBackdrop: true, footer: _jsxs(View, { className: "flex-row gap-3", children: [onClear ? (_jsx(View, { className: "flex-1", children: _jsx(Button, { onPress: onClear, title: clearLabel, variant: "secondary" }) })) : null, _jsx(View, { className: "flex-1", children: _jsx(Button, { onPress: () => {
28
+ return (_jsxs(_Fragment, { children: [_jsxs(Pressable, { accessibilityLabel: `${triggerLabel}${countLabel}`, accessibilityRole: "button", className: "min-h-11 flex-row items-center justify-center gap-2 self-start rounded-full bg-bg-muted px-4", onPress: () => setVisible(true), style: { minHeight: 44 }, testID: testID ? `${testID}-trigger` : undefined, children: [_jsx(Icon, { color: theme.icon.secondary, name: "filter", size: 18 }), _jsx(Typography.Text, { className: "text-text-primary", variant: "buttonSmall", children: triggerLabel }), activeCount > 0 ? (_jsx(Typography.Text, { accessibilityLabel: `${activeCount} filtros activos`, className: "text-text-accent", variant: "caption", children: activeCount })) : null] }), _jsx(Sheet, { footer: _jsxs(View, { className: "flex-row gap-3", children: [onClear ? (_jsx(View, { className: "flex-1", children: _jsx(Button, { onPress: onClear, title: clearLabel, variant: "secondary" }) })) : null, _jsx(View, { className: "flex-1", children: _jsx(Button, { onPress: () => {
29
29
  onApply();
30
30
  setVisible(false);
31
- }, title: applyLabel }) })] }), onClose: () => setVisible(false), showCloseButton: true, testID: testID ? `${testID}-sheet` : undefined, title: title, visible: visible, children: children })] }));
31
+ }, title: applyLabel }) })] }), onClose: () => setVisible(false), testID: testID ? `${testID}-sheet` : undefined, title: title, visible: visible, children: children })] }));
32
32
  }
33
33
  function History(props) {
34
34
  return _jsx(SearchHistory, { ...props });