@sincpro/mobile-ui 1.0.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/README.md +701 -231
  2. package/dist/Actions/Actions.Button.d.ts +15 -12
  3. package/dist/Actions/Actions.Button.js +25 -14
  4. package/dist/Actions/Actions.Dock.d.ts +20 -0
  5. package/dist/Actions/Actions.Dock.js +89 -0
  6. package/dist/Actions/Actions.FAB.d.ts +2 -1
  7. package/dist/Actions/Actions.FAB.js +5 -3
  8. package/dist/Actions/Actions.FilterButton.d.ts +10 -2
  9. package/dist/Actions/Actions.FilterButton.js +21 -6
  10. package/dist/Actions/Actions.IconButton.d.ts +4 -2
  11. package/dist/Actions/Actions.IconButton.js +7 -3
  12. package/dist/Actions/index.d.ts +6 -2
  13. package/dist/Actions/index.js +4 -1
  14. package/dist/Dialog/BottomSheet/BottomSheet.Root.js +22 -34
  15. package/dist/Dialog/Dialog.ActionSheet.d.ts +2 -1
  16. package/dist/Dialog/Dialog.ActionSheet.js +17 -21
  17. package/dist/Dialog/Dialog.Confirmation.js +8 -6
  18. package/dist/Dialog/Dialog.EditValue.js +3 -1
  19. package/dist/Dialog/Dialog.NativeSheet.d.ts +10 -0
  20. package/dist/Dialog/Dialog.NativeSheet.js +21 -0
  21. package/dist/Dialog/Dialog.Sheet.d.ts +5 -1
  22. package/dist/Dialog/Dialog.Sheet.js +30 -31
  23. package/dist/Display/Display.Accordion.js +7 -3
  24. package/dist/Display/Display.AmountSummary.d.ts +28 -0
  25. package/dist/Display/Display.AmountSummary.js +25 -0
  26. package/dist/Display/Display.Carousel.d.ts +27 -4
  27. package/dist/Display/Display.Carousel.js +112 -12
  28. package/dist/Display/Display.CopyableText.d.ts +9 -2
  29. package/dist/Display/Display.CopyableText.js +75 -7
  30. package/dist/Display/Display.ExpoImage.d.ts +8 -0
  31. package/dist/Display/Display.ExpoImage.js +15 -0
  32. package/dist/Display/Display.Icon.js +2 -1
  33. package/dist/Display/Display.Image.d.ts +10 -6
  34. package/dist/Display/Display.Image.js +39 -6
  35. package/dist/Display/Display.KeyValue.d.ts +11 -4
  36. package/dist/Display/Display.KeyValue.js +6 -4
  37. package/dist/Display/Display.ListGroup.d.ts +23 -0
  38. package/dist/Display/Display.ListGroup.js +17 -0
  39. package/dist/Display/Display.ListItem.d.ts +15 -2
  40. package/dist/Display/Display.ListItem.js +25 -8
  41. package/dist/Display/Display.Logo.d.ts +2 -2
  42. package/dist/Display/Display.Logo.js +3 -2
  43. package/dist/Display/Display.MediaCard.d.ts +2 -2
  44. package/dist/Display/Display.MediaEmbed.d.ts +2 -2
  45. package/dist/Display/Display.ProductCard.d.ts +2 -2
  46. package/dist/Display/Display.SectionHeader.d.ts +11 -1
  47. package/dist/Display/Display.SectionHeader.js +11 -2
  48. package/dist/Display/Display.Story.d.ts +3 -3
  49. package/dist/Display/Display.Story.js +5 -4
  50. package/dist/Display/Display.StoryTray.d.ts +4 -4
  51. package/dist/Display/Display.StoryTray.js +3 -2
  52. package/dist/Display/Display.StoryViewer.d.ts +3 -3
  53. package/dist/Display/Display.StoryViewer.js +6 -3
  54. package/dist/Display/Display.SwipeableRow.d.ts +3 -1
  55. package/dist/Display/Display.SwipeableRow.js +3 -3
  56. package/dist/Display/index.d.ts +13 -0
  57. package/dist/Display/index.js +6 -0
  58. package/dist/Feedback/Feedback.Banner.js +1 -1
  59. package/dist/Feedback/Feedback.DomainSplashScreen.d.ts +13 -4
  60. package/dist/Feedback/Feedback.DomainSplashScreen.js +68 -53
  61. package/dist/Feedback/Feedback.EmptyState.d.ts +2 -2
  62. package/dist/Feedback/Feedback.EmptyState.js +3 -2
  63. package/dist/Feedback/Feedback.Error.d.ts +2 -1
  64. package/dist/Feedback/Feedback.Error.js +4 -3
  65. package/dist/Feedback/Feedback.Loading.d.ts +6 -2
  66. package/dist/Feedback/Feedback.Loading.js +2 -2
  67. package/dist/Feedback/Feedback.Progress.d.ts +6 -3
  68. package/dist/Feedback/Feedback.Progress.js +29 -19
  69. package/dist/Feedback/Feedback.ProgressCircle.d.ts +3 -2
  70. package/dist/Feedback/Feedback.ProgressCircle.js +10 -7
  71. package/dist/Feedback/Feedback.Skeleton.d.ts +4 -4
  72. package/dist/Feedback/Feedback.Skeleton.js +15 -3
  73. package/dist/Feedback/Feedback.Spinner.d.ts +3 -3
  74. package/dist/Feedback/Feedback.Spinner.js +40 -9
  75. package/dist/Feedback/Feedback.Splash.d.ts +2 -2
  76. package/dist/Feedback/Feedback.Splash.js +5 -3
  77. package/dist/Feedback/Toast.context.d.ts +64 -3
  78. package/dist/Feedback/Toast.context.js +112 -31
  79. package/dist/Feedback/index.d.ts +15 -1
  80. package/dist/Feedback/index.js +4 -1
  81. package/dist/Form/Form.Checkbox.d.ts +3 -1
  82. package/dist/Form/Form.Checkbox.js +2 -2
  83. package/dist/Form/Form.DatePicker.js +3 -2
  84. package/dist/Form/Form.Dropdown.d.ts +15 -3
  85. package/dist/Form/Form.Dropdown.js +21 -6
  86. package/dist/Form/Form.ErrorSummary.d.ts +23 -0
  87. package/dist/Form/Form.ErrorSummary.js +53 -0
  88. package/dist/Form/Form.FilterButton.d.ts +1 -1
  89. package/dist/Form/Form.Input.d.ts +4 -3
  90. package/dist/Form/Form.Input.js +2 -2
  91. package/dist/Form/Form.KeyboardForm.d.ts +11 -0
  92. package/dist/Form/Form.KeyboardForm.js +9 -0
  93. package/dist/Form/Form.KeyboardToolbar.d.ts +21 -0
  94. package/dist/Form/Form.KeyboardToolbar.js +70 -0
  95. package/dist/Form/Form.Select.d.ts +2 -1
  96. package/dist/Form/Form.Select.js +26 -26
  97. package/dist/Form/index.d.ts +9 -0
  98. package/dist/Form/index.js +6 -0
  99. package/dist/Navigation/Navigation.AppBar.js +8 -6
  100. package/dist/Navigation/Navigation.BottomNav.js +10 -5
  101. package/dist/Navigation/Navigation.Header.d.ts +2 -2
  102. package/dist/Navigation/Navigation.Menu.js +10 -3
  103. package/dist/Navigation/Navigation.RowItem.d.ts +1 -1
  104. package/dist/Navigation/Navigation.RowItem.js +2 -2
  105. package/dist/Navigation/Navigation.Search.d.ts +41 -0
  106. package/dist/Navigation/Navigation.Search.js +47 -0
  107. package/dist/Navigation/Navigation.SearchBar.d.ts +5 -1
  108. package/dist/Navigation/Navigation.SearchBar.js +25 -7
  109. package/dist/Navigation/Navigation.SearchSuggestions.d.ts +14 -0
  110. package/dist/Navigation/Navigation.SearchSuggestions.js +7 -0
  111. package/dist/Navigation/Navigation.SegmentedControl.d.ts +11 -1
  112. package/dist/Navigation/Navigation.SegmentedControl.js +29 -15
  113. package/dist/Navigation/index.d.ts +13 -0
  114. package/dist/Navigation/index.js +4 -0
  115. package/dist/Typography/Typography.Heading.js +1 -1
  116. package/dist/Typography/Typography.SelectableText.d.ts +10 -0
  117. package/dist/Typography/Typography.SelectableText.js +16 -0
  118. package/dist/Typography/Typography.Text.js +4 -20
  119. package/dist/Typography/index.d.ts +3 -0
  120. package/dist/Typography/index.js +3 -0
  121. package/dist/branding.d.ts +3 -2
  122. package/dist/layouts/AuthContainer.d.ts +4 -1
  123. package/dist/layouts/AuthContainer.js +3 -3
  124. package/dist/primitives/BaseButton.d.ts +15 -12
  125. package/dist/primitives/BaseButton.js +21 -9
  126. package/dist/theme/typography.d.ts +16 -1
  127. package/dist/theme/typography.js +19 -0
  128. package/dist/utils/criteria.d.ts +56 -0
  129. package/dist/utils/criteria.js +99 -0
  130. package/dist/utils/image.d.ts +3 -0
  131. package/dist/utils/image.js +13 -0
  132. package/dist/utils/keys.d.ts +1 -0
  133. package/dist/utils/keys.js +7 -0
  134. package/dist/utils/progress.d.ts +1 -0
  135. package/dist/utils/progress.js +5 -0
  136. package/dist/utils/reorder.d.ts +4 -0
  137. package/dist/utils/reorder.js +29 -0
  138. package/dist/utils/useReducedMotion.d.ts +1 -0
  139. package/dist/utils/useReducedMotion.js +43 -0
  140. package/dist/utils/useSheetTranslation.d.ts +5 -0
  141. package/dist/utils/useSheetTranslation.js +23 -0
  142. package/dist/views/AppSplashView.d.ts +3 -2
  143. package/dist/views/AppSplashView.js +3 -2
  144. package/dist/views/AuthFormView.d.ts +6 -3
  145. package/dist/views/AuthFormView.js +2 -2
  146. package/dist/views/FormViewV2.d.ts +9 -4
  147. package/dist/views/FormViewV2.js +35 -16
  148. package/dist/views/ListViewV2.context.d.ts +3 -1
  149. package/dist/views/ListViewV2.context.js +41 -21
  150. package/dist/views/ListViewV2.d.ts +18 -7
  151. package/dist/views/ListViewV2.js +69 -35
  152. package/dist/views/types/IListView.js +6 -5
  153. package/dist/widgets/CollectionOptions.d.ts +15 -0
  154. package/dist/widgets/CollectionOptions.js +129 -0
  155. package/dist/widgets/FilterBar.d.ts +21 -0
  156. package/dist/widgets/FilterBar.js +14 -0
  157. package/dist/widgets/FilterBuilder.d.ts +10 -0
  158. package/dist/widgets/FilterBuilder.js +115 -0
  159. package/dist/widgets/HomeHeader.d.ts +2 -2
  160. package/dist/widgets/PromoModal.js +3 -1
  161. package/dist/widgets/QueryChoice.d.ts +10 -0
  162. package/dist/widgets/QueryChoice.js +13 -0
  163. package/dist/widgets/ReorderableList.d.ts +51 -0
  164. package/dist/widgets/ReorderableList.js +138 -0
  165. package/dist/widgets/ScreenHeader.d.ts +2 -2
  166. package/dist/widgets/index.d.ts +4 -0
  167. package/dist/widgets/index.js +3 -0
  168. package/package.json +18 -1
@@ -0,0 +1,115 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "nativewind/jsx-runtime";
2
+ import Button from "../Actions/Actions.Button.js";
3
+ import DatePicker from "../Form/Form.DatePicker.js";
4
+ import Input from "../Form/Form.Input.js";
5
+ import { Typography } from "../Typography/index.js";
6
+ import { describeCriteria, parseCriteriaDate, replaceCriteria, validateCondition, } from "../utils/criteria.js";
7
+ import QueryChoice from "./QueryChoice.js";
8
+ import { useEffect, useState } from "react";
9
+ import { View } from "react-native";
10
+ export default function FilterBuilder({ fields, value, onChange, onEditingChange, className, testID, }) {
11
+ const [path, setPath] = useState([]);
12
+ const [editing, setEditing] = useState(null);
13
+ const [error, setError] = useState();
14
+ const isEditing = editing !== null;
15
+ useEffect(() => {
16
+ onEditingChange?.(isEditing);
17
+ return () => onEditingChange?.(false);
18
+ }, [isEditing, onEditingChange]);
19
+ let current = value;
20
+ for (const index of path) {
21
+ current = current?.kind === "group" ? current.children[index] : undefined;
22
+ }
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, { onPress: () => {
25
+ setPath([]);
26
+ setEditing(null);
27
+ }, title: "Volver a condiciones", variant: "secondary" })] }));
28
+ const group = current;
29
+ const changeGroup = (next) => onChange(replaceCriteria(value, path, next));
30
+ const condition = editing?.condition;
31
+ const field = fields.find((item) => item.id === condition?.field);
32
+ const operator = field?.operators.find((item) => item.id === condition?.operator);
33
+ const changeCondition = (next) => {
34
+ setError(undefined);
35
+ setEditing((current) => (current ? { ...current, condition: next } : null));
36
+ };
37
+ const addCondition = () => {
38
+ const first = fields.find((item) => item.operators.length);
39
+ if (!first)
40
+ throw new Error("No configurable filter fields");
41
+ setError(undefined);
42
+ setEditing({
43
+ index: null,
44
+ condition: {
45
+ kind: "condition",
46
+ field: first.id,
47
+ operator: first.operators[0].id,
48
+ values: [],
49
+ },
50
+ });
51
+ };
52
+ return (_jsxs(View, { className: className, style: { gap: 16 }, testID: testID, children: [_jsx(Typography.Text, { accessibilityRole: "header", semibold: true, variant: "bodyReadable", children: path.length ? `Grupo ${path.map((index) => index + 1).join(" › ")}` : "Condiciones" }), editing && condition ? (_jsxs(_Fragment, { children: [_jsx(QueryChoice, { label: "Campo", onChange: (id) => {
53
+ const next = fields.find((item) => item.id === id);
54
+ if (!next?.operators.length)
55
+ throw new Error("Field has no operators");
56
+ changeCondition({
57
+ ...condition,
58
+ field: id,
59
+ operator: next.operators[0].id,
60
+ values: [],
61
+ });
62
+ }, options: fields
63
+ .filter((item) => item.operators.length)
64
+ .map((item) => ({ label: item.label, value: item.id })), value: condition.field }), _jsx(QueryChoice, { label: "Comparaci\u00F3n", onChange: (id) => changeCondition({ ...condition, operator: id, values: [] }), options: (field?.operators ?? []).map((item) => ({
65
+ label: item.label,
66
+ value: item.id,
67
+ })), value: condition.operator }), Array.from({ length: operator?.arity === "range" ? 2 : operator?.arity === "one" ? 1 : 0 }, (_, index) => {
68
+ const label = operator?.arity === "range" ? (index === 0 ? "Desde" : "Hasta") : "Valor";
69
+ const update = (next) => {
70
+ const values = [...condition.values];
71
+ values[index] = next;
72
+ changeCondition({ ...condition, values });
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, { 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
+ Number.isNaN(condition.values[index])
76
+ ? ""
77
+ : String(condition.values[index]) }, index));
78
+ }), error ? (_jsx(Typography.Text, { accessibilityLiveRegion: "polite", className: "text-danger", children: error })) : null, _jsx(Button, { onPress: () => {
79
+ const next = {
80
+ ...condition,
81
+ values: field?.type === "number"
82
+ ? Array.from(condition.values, (value) => typeof value === "string" && !value.trim() ? NaN : Number(value))
83
+ : condition.values,
84
+ };
85
+ const problem = validateCondition(next, fields);
86
+ if (problem) {
87
+ setError(problem);
88
+ return;
89
+ }
90
+ const children = [...group.children];
91
+ if (editing.index === null)
92
+ children.push(next);
93
+ else
94
+ children[editing.index] = next;
95
+ changeGroup({ ...group, children });
96
+ setEditing(null);
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
+ { label: "Todas las condiciones", value: "all" },
99
+ { label: "Cualquiera de las condiciones", value: "any" },
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, { onPress: () => child.kind === "group"
102
+ ? setPath([...path, index])
103
+ : (setError(undefined), setEditing({ index, condition: child })), title: child.kind === "group"
104
+ ? `Abrir grupo ${index + 1}`
105
+ : `Editar condición ${index + 1}`, variant: "secondary" }), _jsx(Button, { onPress: () => changeGroup({
106
+ ...group,
107
+ children: group.children.filter((_, position) => position !== index),
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
+ changeGroup({
110
+ ...group,
111
+ children: [...group.children, { kind: "group", join: "any", children: [] }],
112
+ });
113
+ setPath([...path, group.children.length]);
114
+ }, title: "A\u00F1adir grupo", variant: "secondary" })] }))] }));
115
+ }
@@ -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",
@@ -0,0 +1,10 @@
1
+ export interface QueryChoiceProps<T extends string | number | boolean> {
2
+ label: string;
3
+ value: T | undefined;
4
+ options: readonly {
5
+ label: string;
6
+ value: T;
7
+ }[];
8
+ onChange: (value: T) => void;
9
+ }
10
+ export default function QueryChoice<T extends string | number | boolean>({ label, value, options, onChange, }: QueryChoiceProps<T>): import("react").JSX.Element;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "nativewind/jsx-runtime";
2
+ import Pressable from "../primitives/Pressable.js";
3
+ import { Typography } from "../Typography/index.js";
4
+ import { useState } from "react";
5
+ import { View } from "react-native";
6
+ export default function QueryChoice({ label, value, options, onChange, }) {
7
+ const [open, setOpen] = useState(false);
8
+ const selected = options.find((option) => option.value === value);
9
+ return (_jsxs(View, { className: "gap-1", children: [_jsxs(Pressable, { accessibilityLabel: `${label}: ${selected?.label ?? "Seleccionar"}`, accessibilityState: { expanded: open }, className: "rounded-lg border border-input px-3 py-2", onPress: () => setOpen(!open), style: { minHeight: 48, justifyContent: "center" }, children: [_jsx(Typography.Text, { className: "text-text-secondary", variant: "bodySmall", children: label }), _jsxs(Typography.Text, { variant: "bodyReadable", children: [selected?.label ?? "Seleccionar", " ", open ? "−" : "+"] })] }), open ? (_jsx(View, { accessibilityRole: "radiogroup", className: "border-l border-border-default pl-3", children: options.map((option) => (_jsx(Pressable, { accessibilityLabel: option.label, accessibilityRole: "radio", accessibilityState: { checked: option.value === value }, onPress: () => {
10
+ onChange(option.value);
11
+ setOpen(false);
12
+ }, style: { minHeight: 48, justifyContent: "center" }, children: _jsxs(Typography.Text, { className: option.value === value ? "text-text-accent" : "text-text-primary", variant: "bodyReadable", children: [option.value === value ? "✓ " : "", option.label] }) }, String(option.value)))) })) : null] }));
13
+ }
@@ -0,0 +1,51 @@
1
+ import { type ReactNode } from "react";
2
+ import { type StyleProp, type ViewStyle } from "react-native";
3
+ import { type SortableGridProps } from "react-native-sortables";
4
+ export interface ReorderEvent {
5
+ key: string;
6
+ fromIndex: number;
7
+ toIndex: number;
8
+ source: "drag" | "button";
9
+ }
10
+ export interface ReorderItemContext<T> {
11
+ item: T;
12
+ index: number;
13
+ canMoveUp: boolean;
14
+ canMoveDown: boolean;
15
+ moveUp: () => void;
16
+ moveDown: () => void;
17
+ }
18
+ export interface ReorderControlsContext<T> extends ReorderItemContext<T> {
19
+ dragHandle: ReactNode;
20
+ }
21
+ export interface ReorderableListProps<T> {
22
+ items: readonly T[];
23
+ keyExtractor: (item: T) => string;
24
+ getItemLabel: (item: T) => string;
25
+ onChange: (items: T[], event: ReorderEvent) => void;
26
+ renderItem: (context: ReorderItemContext<T>) => ReactNode;
27
+ renderControls?: (context: ReorderControlsContext<T>) => ReactNode;
28
+ renderHandleContent?: (context: ReorderItemContext<T>) => ReactNode;
29
+ disabled?: boolean;
30
+ className?: string;
31
+ style?: StyleProp<ViewStyle>;
32
+ itemStyle?: StyleProp<ViewStyle>;
33
+ contentStyle?: StyleProp<ViewStyle>;
34
+ controlsStyle?: StyleProp<ViewStyle>;
35
+ handleStyle?: StyleProp<ViewStyle>;
36
+ actionStyle?: StyleProp<ViewStyle>;
37
+ testID?: string;
38
+ labels?: {
39
+ drag?: (label: string) => string;
40
+ up?: (label: string) => string;
41
+ down?: (label: string) => string;
42
+ moved?: (label: string, position: number, count: number) => string;
43
+ changedDuringDrag?: string;
44
+ disabledDuringDrag?: string;
45
+ };
46
+ onDragStart?: (key: string) => void;
47
+ onDragEnd?: () => void;
48
+ onReorderRejected?: (reason: "data-changed" | "disabled") => void;
49
+ sortableProps?: Omit<SortableGridProps<T>, "data" | "renderItem" | "keyExtractor" | "rows" | "rowHeight" | "columns" | "onDragStart" | "onDragEnd" | "onOrderChange" | "sortEnabled" | "customHandle" | "strategy">;
50
+ }
51
+ export default function ReorderableList<T>({ items, keyExtractor, getItemLabel, onChange, renderItem, renderControls, renderHandleContent, disabled, className, style, itemStyle, contentStyle, controlsStyle, handleStyle, actionStyle, testID, labels, onDragStart, onDragEnd, onReorderRejected, sortableProps, }: ReorderableListProps<T>): import("react").JSX.Element;
@@ -0,0 +1,138 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "nativewind/jsx-runtime";
2
+ import Icon from "../Display/Display.Icon.js";
3
+ import Pressable from "../primitives/Pressable.js";
4
+ import { cn } from "../theme/tw.js";
5
+ import { Typography } from "../Typography/index.js";
6
+ import { moveCollectionEntry } from "../utils/criteria.js";
7
+ import { applyReorder, moveReorderItem, reorderKeys, sameOrder, } from "../utils/reorder.js";
8
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
9
+ import { AccessibilityInfo, useWindowDimensions, View, } from "react-native";
10
+ import Sortable from "react-native-sortables";
11
+ export default function ReorderableList({ items, keyExtractor, getItemLabel, onChange, renderItem, renderControls, renderHandleContent, disabled = false, className, style, itemStyle, contentStyle, controlsStyle, handleStyle, actionStyle, testID, labels, onDragStart, onDragEnd, onReorderRejected, sortableProps, }) {
12
+ const { fontScale } = useWindowDimensions();
13
+ const keys = useMemo(() => reorderKeys(items, keyExtractor), [items, keyExtractor]);
14
+ const data = useMemo(() => [...items], [items]);
15
+ const dragOrder = useRef(null);
16
+ const [dragging, setDragging] = useState(false);
17
+ const [error, setError] = useState();
18
+ const [revision, setRevision] = useState(0);
19
+ const busy = disabled || dragging;
20
+ const commit = (order, event) => {
21
+ if (sameOrder(keys, order))
22
+ return;
23
+ const next = applyReorder(items, keyExtractor, order);
24
+ onChange(next, event);
25
+ AccessibilityInfo.announceForAccessibility(labels?.moved?.(getItemLabel(next[event.toIndex]), event.toIndex + 1, items.length) ??
26
+ `${getItemLabel(next[event.toIndex])}, posición ${event.toIndex + 1} de ${items.length}`);
27
+ };
28
+ const reject = useCallback((reason) => {
29
+ const message = reason === "disabled"
30
+ ? (labels?.disabledDuringDrag ??
31
+ "El orden se bloqueó durante el arrastre. No se aplicó el cambio.")
32
+ : (labels?.changedDuringDrag ??
33
+ "La lista cambió durante el arrastre. Revisa el orden e inténtalo de nuevo.");
34
+ setError(message);
35
+ setRevision((value) => value + 1);
36
+ AccessibilityInfo.announceForAccessibility(message);
37
+ onReorderRejected?.(reason);
38
+ }, [labels?.changedDuringDrag, labels?.disabledDuringDrag, onReorderRejected]);
39
+ useEffect(() => {
40
+ if (dragOrder.current && (disabled || !sameOrder(dragOrder.current, keys))) {
41
+ dragOrder.current = null;
42
+ setDragging(false);
43
+ reject(disabled ? "disabled" : "data-changed");
44
+ onDragEnd?.();
45
+ }
46
+ }, [disabled, keys, onDragEnd, reject]);
47
+ return (_jsxs(View, { className: cn("gap-2", className), style: style, testID: testID, children: [_jsx(Sortable.Grid, { activeItemScale: 1, activeItemShadowOpacity: 0, rowGap: 8, ...sortableProps, columns: 1, customHandle: true, data: data, keyExtractor: keyExtractor, onDragEnd: ({ fromIndex, toIndex }) => {
48
+ const started = dragOrder.current;
49
+ if (!started)
50
+ return;
51
+ dragOrder.current = null;
52
+ setDragging(false);
53
+ if (started && !sameOrder(started, keys))
54
+ reject("data-changed");
55
+ else if (disabled)
56
+ reject("disabled");
57
+ else if (started)
58
+ commit(moveReorderItem(keys, fromIndex, toIndex), {
59
+ key: keys[fromIndex],
60
+ fromIndex,
61
+ toIndex,
62
+ source: "drag",
63
+ });
64
+ onDragEnd?.();
65
+ }, onDragStart: ({ fromIndex }) => {
66
+ dragOrder.current = keys;
67
+ setDragging(true);
68
+ setError(undefined);
69
+ onDragStart?.(keys[fromIndex]);
70
+ }, renderItem: ({ item, index }) => {
71
+ const key = keys[index];
72
+ const label = getItemLabel(item);
73
+ const move = (direction) => {
74
+ if (busy || index + direction < 0 || index + direction >= items.length)
75
+ return;
76
+ setError(undefined);
77
+ commit(moveCollectionEntry(keys, index, direction), {
78
+ key,
79
+ fromIndex: index,
80
+ toIndex: index + direction,
81
+ source: "button",
82
+ });
83
+ };
84
+ const context = {
85
+ item,
86
+ index,
87
+ canMoveUp: !busy && index > 0,
88
+ canMoveDown: !busy && index < items.length - 1,
89
+ moveUp: () => move(-1),
90
+ moveDown: () => move(1),
91
+ };
92
+ const dragHandle = (_jsx(Sortable.Handle, { mode: disabled ? "non-draggable" : "draggable", children: _jsx(View, { accessibilityActions: [
93
+ ...(context.canMoveUp ? [{ name: "decrement" }] : []),
94
+ ...(context.canMoveDown ? [{ name: "increment" }] : []),
95
+ ], accessibilityLabel: labels?.drag?.(label) ?? `Arrastrar ${label}`, accessibilityRole: "adjustable", accessibilityState: { disabled, busy: dragging }, accessibilityValue: { min: 1, max: items.length, now: index + 1 }, accessible: true, onAccessibilityAction: ({ nativeEvent }) => {
96
+ if (nativeEvent.actionName === "increment")
97
+ move(1);
98
+ if (nativeEvent.actionName === "decrement")
99
+ move(-1);
100
+ }, style: [
101
+ {
102
+ minHeight: 48,
103
+ minWidth: 48,
104
+ alignItems: "center",
105
+ justifyContent: "center",
106
+ },
107
+ handleStyle,
108
+ ], testID: testID ? `${testID}-handle-${key}` : undefined, children: renderHandleContent ? (renderHandleContent(context)) : (_jsx(Icon, { name: "reorder-two-outline", size: 20 })) }) }));
109
+ return (_jsxs(View, { style: [
110
+ {
111
+ width: "100%",
112
+ flexDirection: fontScale > 1.3 ? "column" : "row",
113
+ alignItems: "center",
114
+ gap: 8,
115
+ },
116
+ itemStyle,
117
+ ], testID: testID ? `${testID}-item-${key}` : undefined, children: [_jsx(View, { style: [
118
+ fontScale > 1.3 ? { alignSelf: "stretch" } : { flex: 1, minWidth: 0 },
119
+ contentStyle,
120
+ ], children: renderItem(context) }), _jsx(View, { style: [{ flexDirection: "row", alignItems: "center" }, controlsStyle], children: renderControls ? (renderControls({ ...context, dragHandle })) : (_jsxs(_Fragment, { children: [dragHandle, _jsx(Pressable, { accessibilityLabel: labels?.up?.(label) ?? `Subir ${label}`, disabled: !context.canMoveUp, onPress: context.moveUp, style: [
121
+ {
122
+ minWidth: 48,
123
+ minHeight: 48,
124
+ alignItems: "center",
125
+ justifyContent: "center",
126
+ },
127
+ actionStyle,
128
+ ], children: _jsx(Icon, { name: "arrow-up-outline", size: 18 }) }), _jsx(Pressable, { accessibilityLabel: labels?.down?.(label) ?? `Bajar ${label}`, disabled: !context.canMoveDown, onPress: context.moveDown, style: [
129
+ {
130
+ minWidth: 48,
131
+ minHeight: 48,
132
+ alignItems: "center",
133
+ justifyContent: "center",
134
+ },
135
+ actionStyle,
136
+ ], children: _jsx(Icon, { name: "arrow-down-outline", size: 18 }) })] })) })] }, key));
137
+ }, sortEnabled: !disabled, strategy: "insert" }, revision), error ? (_jsx(Typography.Text, { accessibilityRole: "alert", className: "text-danger", children: error })) : null] }));
138
+ }
@@ -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;
@@ -1,4 +1,8 @@
1
+ export type { CollectionField, CollectionQuery, CollectionSort, CriteriaCondition, CriteriaField, CriteriaGroup, CriteriaNode, CriteriaOperator, CriteriaValue, SavedCollectionQuery, } from "../utils/criteria.js";
1
2
  export { CircleButton } from "./CircleButton.js";
3
+ export { default as CollectionOptions, type CollectionOptionsProps, } from "./CollectionOptions.js";
4
+ export { FilterBar, type FilterBarOption, type FilterBarProps, } from "./FilterBar.js";
5
+ export { default as FilterBuilder, type FilterBuilderProps, } from "./FilterBuilder.js";
2
6
  export { default as GeoPermissionCard } from "./GeoPermissionCard.js";
3
7
  export { HomeHeader } from "./HomeHeader.js";
4
8
  export { MenuCard } from "./MenuCard.js";
@@ -1,4 +1,7 @@
1
1
  export { CircleButton } from "./CircleButton.js";
2
+ export { default as CollectionOptions, } from "./CollectionOptions.js";
3
+ export { FilterBar, } from "./FilterBar.js";
4
+ export { default as FilterBuilder, } from "./FilterBuilder.js";
2
5
  export { default as GeoPermissionCard } from "./GeoPermissionCard.js";
3
6
  export { HomeHeader } from "./HomeHeader.js";
4
7
  export { MenuCard } from "./MenuCard.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sincpro/mobile-ui",
3
- "version": "1.0.1",
3
+ "version": "2.0.0",
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",
@@ -19,8 +19,10 @@
19
19
  "./Dialog": "./dist/Dialog/index.js",
20
20
  "./Dialog/BottomSheet": "./dist/Dialog/BottomSheet/index.js",
21
21
  "./Dialog/Sheet": "./dist/Dialog/Dialog.Sheet.js",
22
+ "./Dialog/NativeSheet": "./dist/Dialog/Dialog.NativeSheet.js",
22
23
  "./Actions": "./dist/Actions/index.js",
23
24
  "./Display": "./dist/Display/index.js",
25
+ "./Display/ExpoImage": "./dist/Display/Display.ExpoImage.js",
24
26
  "./Feedback": "./dist/Feedback/index.js",
25
27
  "./Form": "./dist/Form/index.js",
26
28
  "./Navigation": "./dist/Navigation/index.js",
@@ -28,6 +30,7 @@
28
30
  "./primitives": "./dist/primitives/index.js",
29
31
  "./tokens": "./dist/tokens/index.js",
30
32
  "./widgets": "./dist/widgets/index.js",
33
+ "./widgets/ReorderableList": "./dist/widgets/ReorderableList.js",
31
34
  "./views/Wizard": "./dist/views/Wizard/index.js",
32
35
  "./tailwind.preset": "./tailwind.preset.js",
33
36
  "./theme/globals.css": "./dist/theme/globals.css",
@@ -61,21 +64,27 @@
61
64
  "expo-checkbox": ">=58.0.0",
62
65
  "expo-clipboard": ">=58.0.0",
63
66
  "expo-linear-gradient": ">=58.0.0",
67
+ "expo-image": ">=58.0.0",
64
68
  "expo-localization": ">=58.0.0",
65
69
  "nativewind": ">=4.2.7",
66
70
  "react": ">=19.3",
67
71
  "react-hook-form": ">=7",
68
72
  "react-native": ">=0.88",
69
73
  "react-native-currency-input": ">=1.1",
74
+ "react-native-gesture-handler": ">=3.3",
70
75
  "react-native-keyboard-controller": ">=1.22",
71
76
  "react-native-reanimated": ">=4.7",
72
77
  "react-native-safe-area-context": ">=5.11",
78
+ "react-native-sortables": ">=1.10.1 <2",
73
79
  "react-native-svg": ">=15.16",
74
80
  "react-native-worklets": ">=0.13"
75
81
  },
76
82
  "peerDependenciesMeta": {
77
83
  "@expo/ui": {
78
84
  "optional": true
85
+ },
86
+ "react-native-sortables": {
87
+ "optional": true
79
88
  }
80
89
  },
81
90
  "dependencies": {
@@ -86,6 +95,12 @@
86
95
  "tailwind-variants": "^3.2.2"
87
96
  },
88
97
  "devDependencies": {
98
+ "@jest/globals": "^30.0.0",
99
+ "@testing-library/react-native": "14.1.0",
100
+ "@react-native/jest-preset": "^0.88.1",
101
+ "jest": "^30.0.0",
102
+ "jest-expo": "~58.0.14",
103
+ "test-renderer": "^1.0.0",
89
104
  "@expo-google-fonts/ibm-plex-mono": "^0.4.1",
90
105
  "@expo-google-fonts/ibm-plex-sans": "^0.4.1",
91
106
  "@expo/ui": "~58.0.27",
@@ -111,6 +126,7 @@
111
126
  "expo-checkbox": "~58.0.2",
112
127
  "expo-clipboard": "~58.0.4",
113
128
  "expo-font": "~58.0.11",
129
+ "expo-image": "~58.0.18",
114
130
  "expo-linear-gradient": "~58.0.3",
115
131
  "expo-localization": "~58.0.5",
116
132
  "expo-status-bar": "~58.0.4",
@@ -125,6 +141,7 @@
125
141
  "react-native-keyboard-controller": "1.22.7",
126
142
  "react-native-reanimated": "4.7.1",
127
143
  "react-native-safe-area-context": "~5.11.1",
144
+ "react-native-sortables": "^1.10.1",
128
145
  "react-native-svg": "15.16.0",
129
146
  "react-native-worklets": "0.13.0",
130
147
  "storybook": "10.6.6",