@sincpro/mobile-ui 1.0.1 → 1.1.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 (102) hide show
  1. package/README.md +572 -229
  2. package/dist/Actions/Actions.Button.d.ts +7 -2
  3. package/dist/Actions/Actions.Button.js +23 -10
  4. package/dist/Actions/Actions.FAB.d.ts +3 -1
  5. package/dist/Actions/Actions.FAB.js +7 -3
  6. package/dist/Actions/Actions.FilterButton.d.ts +10 -2
  7. package/dist/Actions/Actions.FilterButton.js +21 -6
  8. package/dist/Actions/Actions.IconButton.d.ts +5 -2
  9. package/dist/Actions/Actions.IconButton.js +7 -3
  10. package/dist/Actions/index.d.ts +2 -2
  11. package/dist/Actions/index.js +1 -1
  12. package/dist/Dialog/Dialog.ActionSheet.d.ts +2 -1
  13. package/dist/Dialog/Dialog.ActionSheet.js +15 -5
  14. package/dist/Dialog/Dialog.Confirmation.js +3 -3
  15. package/dist/Dialog/Dialog.NativeSheet.d.ts +10 -0
  16. package/dist/Dialog/Dialog.NativeSheet.js +21 -0
  17. package/dist/Dialog/Dialog.Sheet.d.ts +5 -1
  18. package/dist/Dialog/Dialog.Sheet.js +14 -8
  19. package/dist/Display/Display.CopyableText.d.ts +9 -2
  20. package/dist/Display/Display.CopyableText.js +75 -7
  21. package/dist/Display/Display.Icon.js +2 -1
  22. package/dist/Display/Display.KeyValue.d.ts +11 -4
  23. package/dist/Display/Display.KeyValue.js +6 -4
  24. package/dist/Display/Display.ListGroup.d.ts +23 -0
  25. package/dist/Display/Display.ListGroup.js +17 -0
  26. package/dist/Display/Display.ListItem.d.ts +16 -2
  27. package/dist/Display/Display.ListItem.js +25 -8
  28. package/dist/Display/Display.SectionHeader.d.ts +11 -1
  29. package/dist/Display/Display.SectionHeader.js +11 -2
  30. package/dist/Display/Display.SwipeableRow.d.ts +3 -1
  31. package/dist/Display/Display.SwipeableRow.js +3 -3
  32. package/dist/Display/index.d.ts +4 -0
  33. package/dist/Display/index.js +2 -0
  34. package/dist/Feedback/Feedback.Banner.js +1 -1
  35. package/dist/Feedback/Feedback.EmptyState.d.ts +2 -1
  36. package/dist/Feedback/Feedback.EmptyState.js +2 -2
  37. package/dist/Feedback/Feedback.Error.d.ts +3 -1
  38. package/dist/Feedback/Feedback.Error.js +4 -3
  39. package/dist/Form/Form.Checkbox.d.ts +4 -1
  40. package/dist/Form/Form.Checkbox.js +2 -2
  41. package/dist/Form/Form.DatePicker.d.ts +2 -1
  42. package/dist/Form/Form.DatePicker.js +4 -3
  43. package/dist/Form/Form.FilterButton.d.ts +1 -1
  44. package/dist/Form/Form.Input.d.ts +5 -3
  45. package/dist/Form/Form.Input.js +2 -2
  46. package/dist/Form/Form.KeyboardForm.d.ts +11 -0
  47. package/dist/Form/Form.KeyboardForm.js +9 -0
  48. package/dist/Form/Form.KeyboardToolbar.d.ts +21 -0
  49. package/dist/Form/Form.KeyboardToolbar.js +70 -0
  50. package/dist/Form/Form.Select.d.ts +3 -1
  51. package/dist/Form/Form.Select.js +23 -10
  52. package/dist/Form/index.d.ts +6 -0
  53. package/dist/Form/index.js +4 -0
  54. package/dist/Navigation/Navigation.AppBar.d.ts +6 -2
  55. package/dist/Navigation/Navigation.AppBar.js +7 -7
  56. package/dist/Navigation/Navigation.BottomNav.d.ts +3 -1
  57. package/dist/Navigation/Navigation.BottomNav.js +9 -5
  58. package/dist/Navigation/Navigation.RowItem.d.ts +1 -1
  59. package/dist/Navigation/Navigation.RowItem.js +2 -2
  60. package/dist/Navigation/Navigation.SearchBar.d.ts +6 -1
  61. package/dist/Navigation/Navigation.SearchBar.js +29 -8
  62. package/dist/Navigation/Navigation.SearchSuggestions.d.ts +14 -0
  63. package/dist/Navigation/Navigation.SearchSuggestions.js +7 -0
  64. package/dist/Navigation/Navigation.SegmentedControl.d.ts +3 -1
  65. package/dist/Navigation/Navigation.SegmentedControl.js +2 -1
  66. package/dist/Navigation/index.d.ts +6 -2
  67. package/dist/Navigation/index.js +2 -0
  68. package/dist/Typography/Typography.Heading.js +1 -1
  69. package/dist/Typography/Typography.SelectableText.d.ts +10 -0
  70. package/dist/Typography/Typography.SelectableText.js +16 -0
  71. package/dist/Typography/Typography.Text.js +4 -20
  72. package/dist/Typography/index.d.ts +3 -0
  73. package/dist/Typography/index.js +3 -0
  74. package/dist/layouts/AuthContainer.d.ts +4 -1
  75. package/dist/layouts/AuthContainer.js +3 -3
  76. package/dist/primitives/BaseButton.d.ts +7 -2
  77. package/dist/primitives/BaseButton.js +19 -5
  78. package/dist/theme/typography.d.ts +16 -1
  79. package/dist/theme/typography.js +19 -0
  80. package/dist/utils/criteria.d.ts +56 -0
  81. package/dist/utils/criteria.js +99 -0
  82. package/dist/utils/reorder.d.ts +4 -0
  83. package/dist/utils/reorder.js +32 -0
  84. package/dist/views/AuthFormView.d.ts +4 -1
  85. package/dist/views/AuthFormView.js +2 -2
  86. package/dist/views/FormViewV2.d.ts +7 -2
  87. package/dist/views/FormViewV2.js +35 -16
  88. package/dist/views/ListViewV2.d.ts +9 -6
  89. package/dist/views/ListViewV2.js +34 -21
  90. package/dist/widgets/CollectionOptions.d.ts +15 -0
  91. package/dist/widgets/CollectionOptions.js +127 -0
  92. package/dist/widgets/FilterBar.d.ts +21 -0
  93. package/dist/widgets/FilterBar.js +14 -0
  94. package/dist/widgets/FilterBuilder.d.ts +10 -0
  95. package/dist/widgets/FilterBuilder.js +115 -0
  96. package/dist/widgets/QueryChoice.d.ts +10 -0
  97. package/dist/widgets/QueryChoice.js +13 -0
  98. package/dist/widgets/ReorderableList.d.ts +51 -0
  99. package/dist/widgets/ReorderableList.js +138 -0
  100. package/dist/widgets/index.d.ts +4 -0
  101. package/dist/widgets/index.js +3 -0
  102. package/package.json +15 -1
@@ -2,14 +2,14 @@ import { jsx as _jsx, jsxs as _jsxs } from "nativewind/jsx-runtime";
2
2
  import { Display } from "../Display/index.js";
3
3
  import GradientSurface from "../Display/Display.GradientSurface.js";
4
4
  import { Pattern } from "../Display/Display.Pattern.js";
5
+ import KeyboardForm from "../Form/Form.KeyboardForm.js";
5
6
  import { useTheme } from "../theme/index.js";
6
7
  import { cn } from "../theme/tw.js";
7
8
  import { Keyboard, TouchableOpacity, TouchableWithoutFeedback, useWindowDimensions, View, } from "react-native";
8
- import { KeyboardAwareScrollView } from "react-native-keyboard-controller";
9
9
  import { SafeAreaView } from "react-native-safe-area-context";
10
- function AuthContainer({ header, children, onBackPress, background, className, }) {
10
+ function AuthContainer({ header, children, onBackPress, background, className, keyboardToolbar = false, keyboardFormProps, }) {
11
11
  const { height: screenHeight } = useWindowDimensions();
12
12
  const theme = useTheme();
13
- return (_jsxs(GradientSurface, { className: cn("flex-1", className), colors: background?.colors, end: background?.end ?? (background?.colors ? { x: 0, y: 1 } : undefined), preset: background?.surface ?? "warm", radius: "none", start: background?.start ?? (background?.colors ? { x: 0, y: 0 } : undefined), style: { flex: 1 }, children: [background?.pattern ? (_jsx(Pattern, { color: background.patternColor ?? "#FFFFFF", kind: background.pattern, opacity: background.patternOpacity ?? 0.18 })) : null, _jsx(SafeAreaView, { className: "flex-1", edges: ["top"], children: _jsxs(View, { className: "flex-1", children: [_jsx(TouchableWithoutFeedback, { onPress: Keyboard.dismiss, children: _jsxs(View, { className: "justify-center items-center relative", style: { height: screenHeight * 0.3 }, children: [onBackPress && (_jsx(TouchableOpacity, { className: "absolute top-4 left-4 p-2 z-[1]", onPress: onBackPress, children: _jsx(Display.Icon, { color: background?.textColor ?? theme.text.inverse, name: "arrow-back", size: 24, type: "ionicons" }) })), header] }) }), _jsx(View, { className: "flex-1 bg-bg-card rounded-t-3xl shadow-lg", style: { minHeight: screenHeight * 0.7 }, children: _jsx(KeyboardAwareScrollView, { bottomOffset: 50, bounces: false, className: "flex-1", contentContainerClassName: "flex-grow pb-10", keyboardShouldPersistTaps: "handled", showsVerticalScrollIndicator: false, children: _jsx(View, { className: "p-4", children: children }) }) })] }) })] }));
13
+ return (_jsxs(GradientSurface, { className: cn("flex-1", className), colors: background?.colors, end: background?.end ?? (background?.colors ? { x: 0, y: 1 } : undefined), preset: background?.surface ?? "warm", radius: "none", start: background?.start ?? (background?.colors ? { x: 0, y: 0 } : undefined), style: { flex: 1 }, children: [background?.pattern ? (_jsx(Pattern, { color: background.patternColor ?? "#FFFFFF", kind: background.pattern, opacity: background.patternOpacity ?? 0.18 })) : null, _jsx(SafeAreaView, { className: "flex-1", edges: ["top"], children: _jsxs(View, { className: "flex-1", children: [_jsx(TouchableWithoutFeedback, { onPress: Keyboard.dismiss, children: _jsxs(View, { className: "justify-center items-center relative", style: { height: screenHeight * 0.3 }, children: [onBackPress && (_jsx(TouchableOpacity, { className: "absolute top-4 left-4 p-2 z-[1]", onPress: onBackPress, children: _jsx(Display.Icon, { color: background?.textColor ?? theme.text.inverse, name: "arrow-back", size: 24, type: "ionicons" }) })), header] }) }), _jsx(View, { className: "flex-1 bg-bg-card rounded-t-3xl shadow-lg", style: { minHeight: screenHeight * 0.7 }, children: _jsx(KeyboardForm, { bottomOffset: keyboardToolbar ? undefined : 50, bounces: false, className: "flex-1", contentContainerClassName: "flex-grow pb-10", keyboardShouldPersistTaps: "handled", showsVerticalScrollIndicator: false, ...keyboardFormProps, toolbar: keyboardToolbar, children: _jsx(View, { className: "p-4", children: children }) }) })] }) })] }));
14
14
  }
15
15
  export default AuthContainer;
@@ -1,6 +1,7 @@
1
1
  import { type VariantProps } from "../theme/tw.js";
2
2
  import type React from "react";
3
3
  import { FC } from "react";
4
+ import type { AccessibilityProps } from "react-native";
4
5
  declare const button: import("tailwind-variants").TVReturnType<{
5
6
  variant: {
6
7
  primary: "bg-accent";
@@ -123,21 +124,25 @@ declare const buttonIcon: import("tailwind-variants").TVReturnType<{
123
124
  true: "opacity-70";
124
125
  };
125
126
  }, undefined>>;
126
- export interface BaseButtonProps extends VariantProps<typeof button> {
127
+ export interface BaseButtonProps extends VariantProps<typeof button>, AccessibilityProps {
127
128
  title: string;
128
129
  onPress?: () => void;
129
130
  loading?: boolean;
130
131
  disabled?: boolean;
131
132
  className?: string;
132
133
  icon?: React.ReactNode;
134
+ accessibleSizing?: boolean;
135
+ testID?: string;
133
136
  }
134
137
  export declare const BaseButton: FC<BaseButtonProps>;
135
- export interface BaseButtonIconProps extends VariantProps<typeof buttonIcon> {
138
+ export interface BaseButtonIconProps extends VariantProps<typeof buttonIcon>, AccessibilityProps {
136
139
  icon: string;
137
140
  onPress?: () => void;
138
141
  loading?: boolean;
139
142
  disabled?: boolean;
140
143
  className?: string;
144
+ accessibleSizing?: boolean;
145
+ testID?: string;
141
146
  }
142
147
  export declare const BaseButtonIcon: FC<BaseButtonIconProps>;
143
148
  declare const BaseButtonWithIcon: React.FC<BaseButtonProps> & {
@@ -192,7 +192,7 @@ function getIconSize(size) {
192
192
  function getTextVariant(size) {
193
193
  return textVariantMap[size] ?? "button";
194
194
  }
195
- export const BaseButton = ({ title, onPress, variant = "primary", size = "medium", loading = false, disabled = false, className, icon, }) => {
195
+ export const BaseButton = ({ title, onPress, variant = "primary", size = "medium", loading = false, disabled = false, className, icon, accessibleSizing = false, testID, accessibilityLabel, accessibilityState, ...accessibilityProps }) => {
196
196
  const textColorClass = textColor({
197
197
  variant: variant ?? "primary",
198
198
  disabled,
@@ -200,15 +200,29 @@ export const BaseButton = ({ title, onPress, variant = "primary", size = "medium
200
200
  const loadingColorValue = getLoadingColor(variant ?? "primary");
201
201
  const textVariantValue = getTextVariant(size ?? "medium");
202
202
  if (variant === "link") {
203
- return (_jsx(TouchableOpacity, { activeOpacity: 0.7, className: className, disabled: disabled || loading, onPress: onPress, children: loading ? (_jsx(ActivityIndicator, { color: loadingColorValue, size: "small" })) : (_jsx(Typography.Text, { className: textColorClass, variant: textVariantValue, children: title })) }));
203
+ return (_jsx(TouchableOpacity, { activeOpacity: 0.7, className: className, disabled: disabled || loading, onPress: onPress, ...accessibilityProps, accessibilityLabel: accessibilityLabel ?? title, accessibilityRole: "button", accessibilityState: {
204
+ ...accessibilityState,
205
+ disabled: disabled || loading,
206
+ busy: loading,
207
+ }, style: accessibleSizing
208
+ ? { minHeight: 48, minWidth: 48, justifyContent: "center" }
209
+ : undefined, testID: testID, children: loading ? (_jsx(ActivityIndicator, { color: loadingColorValue, size: "small" })) : (_jsx(Typography.Text, { className: textColorClass, variant: textVariantValue, children: title })) }));
204
210
  }
205
- return (_jsxs(TouchableOpacity, { activeOpacity: 0.8, className: button({ variant, size, disabled, className }), disabled: disabled || loading, onPress: onPress, children: [icon && !loading && _jsx(View, { className: "mr-2", children: icon }), loading ? (_jsx(ActivityIndicator, { color: loadingColorValue, size: "small" })) : (_jsx(Typography.Text, { className: textColorClass, semibold: variant === "primary", variant: textVariantValue, children: title }))] }));
211
+ return (_jsxs(TouchableOpacity, { activeOpacity: 0.8, className: button({ variant, size, disabled, className }), disabled: disabled || loading, onPress: onPress, ...accessibilityProps, accessibilityLabel: accessibilityLabel ?? title, accessibilityRole: "button", accessibilityState: {
212
+ ...accessibilityState,
213
+ disabled: disabled || loading,
214
+ busy: loading,
215
+ }, style: accessibleSizing ? { minHeight: 48, minWidth: 48 } : undefined, testID: testID, children: [icon && !loading && _jsx(View, { className: "mr-2", children: icon }), loading ? (_jsx(ActivityIndicator, { color: loadingColorValue, size: "small" })) : (_jsx(Typography.Text, { className: textColorClass, semibold: variant === "primary", variant: textVariantValue, children: title }))] }));
206
216
  };
207
- export const BaseButtonIcon = ({ icon, onPress, variant = "primary", size = "medium", loading = false, disabled = false, className, }) => {
217
+ export const BaseButtonIcon = ({ icon, onPress, variant = "primary", size = "medium", loading = false, disabled = false, className, accessibleSizing = false, testID, accessibilityState, ...accessibilityProps }) => {
208
218
  const iconColorValue = getIconColor(variant ?? "primary", disabled);
209
219
  const loadingColorValue = getLoadingColor(variant ?? "primary");
210
220
  const iconSizeValue = getIconSize(size ?? "medium");
211
- return (_jsx(TouchableOpacity, { activeOpacity: 0.8, className: buttonIcon({ variant, size, disabled, className }), disabled: disabled || loading, onPress: onPress, children: loading ? (_jsx(ActivityIndicator, { color: loadingColorValue, size: "small" })) : (_jsx(Icon, { color: iconColorValue, name: icon, size: iconSizeValue })) }));
221
+ return (_jsx(TouchableOpacity, { activeOpacity: 0.8, className: buttonIcon({ variant, size, disabled, className }), disabled: disabled || loading, onPress: onPress, ...accessibilityProps, accessibilityRole: "button", accessibilityState: {
222
+ ...accessibilityState,
223
+ disabled: disabled || loading,
224
+ busy: loading,
225
+ }, style: accessibleSizing ? { minHeight: 48, minWidth: 48 } : undefined, testID: testID, children: loading ? (_jsx(ActivityIndicator, { color: loadingColorValue, size: "small" })) : (_jsx(Icon, { color: iconColorValue, name: icon, size: iconSizeValue })) }));
212
226
  };
213
227
  // ============================================================================
214
228
  // COMPOUND COMPONENT
@@ -10,6 +10,7 @@ export declare const fontFamilies: Record<FontRole, string>;
10
10
  export declare function configureFonts(overrides: Partial<Record<FontRole, string>>): void;
11
11
  /** Reset all font roles to the defaults (IBM Plex Sans + IBM Plex Mono). Useful in tests and stories. */
12
12
  export declare function resetFonts(): void;
13
+ export declare function resolveTextFontFamily(variant: keyof typeof typographyVariants, bold?: boolean, semibold?: boolean): string;
13
14
  export declare const fontWeights: {
14
15
  readonly light: "300";
15
16
  readonly regular: "400";
@@ -103,6 +104,13 @@ export declare const typographyVariants: {
103
104
  readonly lineHeight: number;
104
105
  readonly letterSpacing: 0;
105
106
  };
107
+ readonly bodyReadable: {
108
+ readonly fontSize: 17;
109
+ readonly fontFamily: string;
110
+ readonly fontWeight: "400";
111
+ readonly lineHeight: number;
112
+ readonly letterSpacing: 0;
113
+ };
106
114
  readonly bodyLarge: {
107
115
  readonly fontSize: 16;
108
116
  readonly fontFamily: string;
@@ -314,6 +322,13 @@ export declare const Typography: {
314
322
  readonly lineHeight: number;
315
323
  readonly letterSpacing: 0;
316
324
  };
325
+ readonly bodyReadable: {
326
+ readonly fontSize: 17;
327
+ readonly fontFamily: string;
328
+ readonly fontWeight: "400";
329
+ readonly lineHeight: number;
330
+ readonly letterSpacing: 0;
331
+ };
317
332
  readonly bodyLarge: {
318
333
  readonly fontSize: 16;
319
334
  readonly fontFamily: string;
@@ -421,7 +436,7 @@ export declare const Typography: {
421
436
  readonly letterSpacing: 0;
422
437
  };
423
438
  };
424
- readonly variantFontRole: Record<"label" | "button" | "title" | "display" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "bodyLarge" | "body" | "bodySmall" | "labelSmall" | "data" | "dataLarge" | "overline" | "caption" | "captionSmall" | "buttonSmall" | "buttonLarge" | "subtitle", FontRole>;
439
+ readonly variantFontRole: Record<"label" | "button" | "title" | "display" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "bodyReadable" | "bodyLarge" | "body" | "bodySmall" | "labelSmall" | "data" | "dataLarge" | "overline" | "caption" | "captionSmall" | "buttonSmall" | "buttonLarge" | "subtitle", FontRole>;
425
440
  readonly createTextStyle: (variant: keyof typeof typographyVariants, overrides?: Partial<TextStyle>) => TextStyle;
426
441
  readonly createCustomTextStyle: (fontSize: keyof typeof fontSizes, fontFamily: keyof typeof fontFamilies, options?: {
427
442
  lineHeight?: keyof typeof lineHeights;
@@ -26,6 +26,17 @@ export function configureFonts(overrides) {
26
26
  export function resetFonts() {
27
27
  Object.assign(fontFamilies, DEFAULT_FONT_FAMILIES);
28
28
  }
29
+ export function resolveTextFontFamily(variant, bold = false, semibold = false) {
30
+ const variantFamily = fontFamilies[variantFontRole[variant]] ?? typographyVariants[variant].fontFamily;
31
+ const isBrandedFace = !variantFamily.startsWith("Inter_") && !variantFamily.startsWith("IBMPlexSans_");
32
+ return isBrandedFace
33
+ ? variantFamily
34
+ : bold
35
+ ? fontFamilies.extraBold
36
+ : semibold
37
+ ? fontFamilies.semiBold
38
+ : variantFamily;
39
+ }
29
40
  export const fontWeights = {
30
41
  light: "300",
31
42
  regular: "400",
@@ -126,6 +137,13 @@ export const typographyVariants = {
126
137
  letterSpacing: letterSpacing.normal,
127
138
  },
128
139
  // Body
140
+ bodyReadable: {
141
+ fontSize: 17,
142
+ fontFamily: fontFamilies.regular,
143
+ fontWeight: fontWeights.regular,
144
+ lineHeight: 17 * lineHeights.normal,
145
+ letterSpacing: letterSpacing.normal,
146
+ },
129
147
  bodyLarge: {
130
148
  fontSize: fontSizes.lg,
131
149
  fontFamily: fontFamilies.regular,
@@ -251,6 +269,7 @@ export const variantFontRole = {
251
269
  h4: "medium",
252
270
  h5: "medium",
253
271
  h6: "medium",
272
+ bodyReadable: "regular",
254
273
  bodyLarge: "regular",
255
274
  body: "regular",
256
275
  bodySmall: "regular",
@@ -0,0 +1,56 @@
1
+ export type CriteriaValue = string | number | boolean;
2
+ export interface CriteriaOperator {
3
+ id: string;
4
+ label: string;
5
+ arity: "none" | "one" | "range";
6
+ }
7
+ export interface CriteriaField {
8
+ id: string;
9
+ label: string;
10
+ type: "text" | "number" | "date" | "choice";
11
+ operators: readonly CriteriaOperator[];
12
+ options?: readonly {
13
+ label: string;
14
+ value: CriteriaValue;
15
+ }[];
16
+ }
17
+ export interface CriteriaCondition {
18
+ kind: "condition";
19
+ field: string;
20
+ operator: string;
21
+ values: readonly CriteriaValue[];
22
+ }
23
+ export interface CriteriaGroup {
24
+ kind: "group";
25
+ join: "all" | "any" | "none";
26
+ children: readonly CriteriaNode[];
27
+ }
28
+ export type CriteriaNode = CriteriaCondition | CriteriaGroup;
29
+ export interface CollectionSort {
30
+ field: string;
31
+ direction: "asc" | "desc";
32
+ }
33
+ export interface CollectionQuery {
34
+ search: string;
35
+ filter: CriteriaGroup;
36
+ sort: readonly CollectionSort[];
37
+ groupBy: readonly string[];
38
+ }
39
+ export interface CollectionField {
40
+ id: string;
41
+ label: string;
42
+ ascLabel?: string;
43
+ descLabel?: string;
44
+ }
45
+ export interface SavedCollectionQuery {
46
+ id: string;
47
+ label: string;
48
+ query: CollectionQuery;
49
+ }
50
+ export declare function parseCriteriaDate(value: CriteriaValue | undefined): Date | null;
51
+ export declare function criteriaAt(root: CriteriaNode, path: readonly number[]): CriteriaNode;
52
+ export declare function replaceCriteria(root: CriteriaGroup, path: readonly number[], replacement: CriteriaNode | null): CriteriaGroup;
53
+ export declare function describeCriteria(node: CriteriaNode, fields: readonly CriteriaField[]): string;
54
+ export declare function validateCondition(condition: CriteriaCondition, fields: readonly CriteriaField[]): string | undefined;
55
+ export declare function validateCriteria(group: CriteriaGroup, fields: readonly CriteriaField[]): string | undefined;
56
+ export declare function moveCollectionEntry<T>(items: readonly T[], index: number, direction: -1 | 1): T[];
@@ -0,0 +1,99 @@
1
+ export function parseCriteriaDate(value) {
2
+ if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value))
3
+ return null;
4
+ const date = new Date(`${value}T00:00:00Z`);
5
+ if (!Number.isFinite(date.getTime()) || date.toISOString().slice(0, 10) !== value)
6
+ return null;
7
+ return new Date(`${value}T12:00:00`);
8
+ }
9
+ export function criteriaAt(root, path) {
10
+ let node = root;
11
+ for (const index of path) {
12
+ if (node.kind !== "group" || !node.children[index])
13
+ throw new Error("Invalid criteria path");
14
+ node = node.children[index];
15
+ }
16
+ return node;
17
+ }
18
+ export function replaceCriteria(root, path, replacement) {
19
+ if (path.length === 0) {
20
+ if (replacement?.kind !== "group")
21
+ throw new Error("Criteria root must remain a group");
22
+ return replacement;
23
+ }
24
+ const [index, ...rest] = path;
25
+ const child = root.children[index];
26
+ if (!child)
27
+ throw new Error("Invalid criteria path");
28
+ if (rest.length && child.kind !== "group")
29
+ throw new Error("Invalid criteria path");
30
+ const next = rest.length && child.kind === "group"
31
+ ? replaceCriteria(child, rest, replacement)
32
+ : replacement;
33
+ return {
34
+ ...root,
35
+ children: root.children.flatMap((item, position) => position !== index ? [item] : next ? [next] : []),
36
+ };
37
+ }
38
+ export function describeCriteria(node, fields) {
39
+ if (node.kind === "group") {
40
+ if (!node.children.length)
41
+ return "Sin condiciones";
42
+ const join = { all: "Todas", any: "Cualquiera", none: "Ninguna" }[node.join];
43
+ return `${join}: (${node.children.map((child) => describeCriteria(child, fields)).join("; ")})`;
44
+ }
45
+ const field = fields.find((item) => item.id === node.field);
46
+ const operator = field?.operators.find((item) => item.id === node.operator);
47
+ const values = node.values.map((value) => field?.options?.find((option) => option.value === value)?.label ?? String(value));
48
+ return `${field?.label ?? node.field} ${operator?.label ?? node.operator}${values.length ? ` ${values.join(" — ")}` : ""}`;
49
+ }
50
+ export function validateCondition(condition, fields) {
51
+ const field = fields.find((item) => item.id === condition.field);
52
+ if (!field)
53
+ return "El campo ya no está disponible.";
54
+ const operator = field.operators.find((item) => item.id === condition.operator);
55
+ if (!operator)
56
+ return "El operador no está disponible para este campo.";
57
+ const count = { none: 0, one: 1, range: 2 }[operator.arity];
58
+ if (condition.values.length !== count)
59
+ return "Completa los valores de la condición.";
60
+ for (const value of condition.values) {
61
+ if (field.type === "number" && (typeof value !== "number" || !Number.isFinite(value)))
62
+ return "Introduce un número válido.";
63
+ if (field.type === "text" && (typeof value !== "string" || !value.trim()))
64
+ return "Introduce un texto.";
65
+ if (field.type === "date" && !parseCriteriaDate(value))
66
+ return "Introduce una fecha válida (AAAA-MM-DD).";
67
+ if (field.type === "choice" && !field.options?.some((option) => option.value === value))
68
+ return "Selecciona un valor disponible.";
69
+ }
70
+ if (operator.arity === "range" &&
71
+ field.type === "number" &&
72
+ Number(condition.values[0]) > Number(condition.values[1]))
73
+ return "El inicio no puede superar el final.";
74
+ if (operator.arity === "range" &&
75
+ field.type === "date" &&
76
+ String(condition.values[0]) > String(condition.values[1]))
77
+ return "El inicio no puede superar el final.";
78
+ return undefined;
79
+ }
80
+ export function validateCriteria(group, fields) {
81
+ for (const child of group.children) {
82
+ if (child.kind === "group" && !child.children.length)
83
+ return "Completa o elimina el grupo vacío.";
84
+ const error = child.kind === "group"
85
+ ? validateCriteria(child, fields)
86
+ : validateCondition(child, fields);
87
+ if (error)
88
+ return error;
89
+ }
90
+ return undefined;
91
+ }
92
+ export function moveCollectionEntry(items, index, direction) {
93
+ const target = index + direction;
94
+ if (index < 0 || index >= items.length || target < 0 || target >= items.length)
95
+ throw new Error("Invalid collection reorder");
96
+ const next = [...items];
97
+ [next[index], next[target]] = [next[target], next[index]];
98
+ return next;
99
+ }
@@ -0,0 +1,4 @@
1
+ export declare function reorderKeys<T>(items: readonly T[], keyExtractor: (item: T) => string): string[];
2
+ export declare function sameOrder(left: readonly string[], right: readonly string[]): boolean;
3
+ export declare function moveReorderItem<T>(items: readonly T[], fromIndex: number, toIndex: number): T[];
4
+ export declare function applyReorder<T>(items: readonly T[], keyExtractor: (item: T) => string, order: readonly string[]): T[];
@@ -0,0 +1,32 @@
1
+ export function reorderKeys(items, keyExtractor) {
2
+ const keys = items.map(keyExtractor);
3
+ if (keys.some((key) => typeof key !== "string" || !key.trim()) ||
4
+ new Set(keys).size !== keys.length)
5
+ throw new Error("ReorderableList requires unique, non-empty string keys.");
6
+ return keys;
7
+ }
8
+ export function sameOrder(left, right) {
9
+ return left.length === right.length && left.every((key, index) => key === right[index]);
10
+ }
11
+ export function moveReorderItem(items, fromIndex, toIndex) {
12
+ if (!Number.isInteger(fromIndex) ||
13
+ !Number.isInteger(toIndex) ||
14
+ fromIndex < 0 ||
15
+ fromIndex >= items.length ||
16
+ toIndex < 0 ||
17
+ toIndex >= items.length)
18
+ throw new Error("ReorderableList received an invalid move.");
19
+ const next = [...items];
20
+ const [item] = next.splice(fromIndex, 1);
21
+ next.splice(toIndex, 0, item);
22
+ return next;
23
+ }
24
+ export function applyReorder(items, keyExtractor, order) {
25
+ const keys = reorderKeys(items, keyExtractor);
26
+ const indices = new Map(keys.map((key, index) => [key, index]));
27
+ if (order.length !== keys.length ||
28
+ new Set(order).size !== order.length ||
29
+ order.some((key) => !indices.has(key)))
30
+ throw new Error("ReorderableList received an order that does not match the current items.");
31
+ return order.map((key) => items[indices.get(key)]);
32
+ }
@@ -1,3 +1,4 @@
1
+ import type { KeyboardFormProps } from "../Form/Form.KeyboardForm.js";
1
2
  import type { AppBarBackground } from "../Navigation/Navigation.AppBar.js";
2
3
  import { ReactNode } from "react";
3
4
  import { type ImageSourcePropType } from "react-native";
@@ -8,6 +9,8 @@ interface AuthFormProps {
8
9
  onBack?: () => void;
9
10
  FormNode: ReactNode;
10
11
  background?: AppBarBackground;
12
+ keyboardToolbar?: boolean;
13
+ keyboardFormProps?: Omit<KeyboardFormProps, "children" | "toolbar">;
11
14
  }
12
- declare function AuthFormView({ title, description, logoSource, onBack, FormNode, background, }: AuthFormProps): import("react").JSX.Element;
15
+ declare function AuthFormView({ title, description, logoSource, onBack, FormNode, background, keyboardToolbar, keyboardFormProps, }: AuthFormProps): import("react").JSX.Element;
13
16
  export default AuthFormView;
@@ -5,7 +5,7 @@ import { Typography } from "../Typography/index.js";
5
5
  import { View } from "react-native";
6
6
  const Logo = Display.Logo;
7
7
  const Text = Typography.Text;
8
- function AuthFormView({ title, description, logoSource, onBack, FormNode, background, }) {
8
+ function AuthFormView({ title, description, logoSource, onBack, FormNode, background, keyboardToolbar, keyboardFormProps, }) {
9
9
  const renderTitle = () => {
10
10
  if (!title)
11
11
  return null;
@@ -16,6 +16,6 @@ function AuthFormView({ title, description, logoSource, onBack, FormNode, backgr
16
16
  return null;
17
17
  return _jsx(Text, { variant: "body", children: description });
18
18
  };
19
- return (_jsx(AuthContainer, { background: background, header: _jsx(Logo, { size: "large", source: logoSource }), onBackPress: onBack, children: _jsxs(View, { className: "m-4", children: [_jsxs(View, { className: "mb-5", children: [renderTitle(), renderDescription()] }), FormNode] }) }));
19
+ return (_jsx(AuthContainer, { background: background, header: _jsx(Logo, { size: "large", source: logoSource }), keyboardFormProps: keyboardFormProps, keyboardToolbar: keyboardToolbar, onBackPress: onBack, children: _jsxs(View, { className: "m-4", children: [_jsxs(View, { className: "mb-5", children: [renderTitle(), renderDescription()] }), FormNode] }) }));
20
20
  }
21
21
  export default AuthFormView;
@@ -1,3 +1,4 @@
1
+ import { type KeyboardFormProps } from "../Form/Form.KeyboardForm.js";
1
2
  import type { AppBarBackground } from "../Navigation/Navigation.AppBar.js";
2
3
  import { type SegmentedControlProps } from "../Navigation/Navigation.SegmentedControl.js";
3
4
  import React, { ReactNode } from "react";
@@ -12,12 +13,14 @@ interface FormViewProps<T> {
12
13
  errorMessage?: string;
13
14
  readonly?: boolean;
14
15
  onRefresh?: () => Promise<void>;
16
+ onRefreshError?: (error: unknown) => void;
15
17
  onRetry?: () => void;
16
18
  onBack?: () => void;
17
19
  withContainer?: boolean;
18
20
  children: ReactNode;
19
21
  }
20
- declare function FormViewRoot<T>({ name, description, item, isLoading, isEmpty, hasError, errorMessage, readonly, onRefresh, onRetry, onBack, withContainer, children, }: FormViewProps<T>): React.JSX.Element;
22
+ export declare function refreshFormData(onRefresh: (() => Promise<void>) | undefined, onRefreshError: ((error: unknown) => void) | undefined, setShowRefreshing: (value: boolean) => void): Promise<void>;
23
+ declare function FormViewRoot<T>({ name, description, item, isLoading, isEmpty, hasError, errorMessage, readonly, onRefresh, onRefreshError, onRetry, onBack, withContainer, children, }: FormViewProps<T>): React.JSX.Element;
21
24
  interface HeaderProps {
22
25
  /** AppBar display variant. `large` = big title (list/index screens). `center` = modal/detail. */
23
26
  variant?: "default" | "large" | "center";
@@ -53,8 +56,10 @@ declare function HeaderActionsBar({ children }: {
53
56
  * Use as a child of `<FormViewV2.Header>` — it lands in the AppBar subheader slot.
54
57
  */
55
58
  declare function HeaderSegmented<T extends string>(props: SegmentedControlProps<T>): React.JSX.Element;
56
- declare function Content({ withMargin, children, }: {
59
+ declare function Content({ withMargin, keyboardToolbar, keyboardFormProps, children, }: {
57
60
  withMargin?: boolean;
61
+ keyboardToolbar?: boolean;
62
+ keyboardFormProps?: Omit<KeyboardFormProps, "children" | "toolbar">;
58
63
  children: ReactNode;
59
64
  }): React.JSX.Element;
60
65
  /**
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "nativewind/jsx-runtime";
2
2
  import { Display } from "../Display/index.js";
3
3
  import { ErrorBoundary, Feedback } from "../Feedback/index.js";
4
+ import KeyboardForm from "../Form/Form.KeyboardForm.js";
4
5
  import { useBottomInset } from "../layouts/BottomInset.js";
5
6
  import Container from "../layouts/Container.js";
6
7
  import { AppBar } from "../Navigation/Navigation.AppBar.js";
@@ -10,13 +11,31 @@ import { useTheme } from "../theme/index.js";
10
11
  import { FormViewProvider, useFormView } from "./FormViewV2.context.js";
11
12
  import React from "react";
12
13
  import { RefreshControl, View } from "react-native";
13
- import { KeyboardAwareScrollView } from "react-native-keyboard-controller";
14
- function FormViewRoot({ name, description, item, isLoading = false, isEmpty = false, hasError = false, errorMessage, readonly = false, onRefresh, onRetry, onBack, withContainer = true, children, }) {
14
+ const RefreshErrorContext = React.createContext(undefined);
15
+ export async function refreshFormData(onRefresh, onRefreshError, setShowRefreshing) {
16
+ setShowRefreshing(true);
17
+ try {
18
+ await onRefresh?.();
19
+ }
20
+ catch (error) {
21
+ console.error("[FormViewV2] Refresh failed", error);
22
+ try {
23
+ onRefreshError?.(error);
24
+ }
25
+ catch (callbackError) {
26
+ console.error("[FormViewV2] onRefreshError callback failed", callbackError);
27
+ }
28
+ }
29
+ finally {
30
+ setShowRefreshing(false);
31
+ }
32
+ }
33
+ function FormViewRoot({ name, description, item, isLoading = false, isEmpty = false, hasError = false, errorMessage, readonly = false, onRefresh, onRefreshError, onRetry, onBack, withContainer = true, children, }) {
15
34
  const Wrapper = withContainer ? Container : View;
16
35
  const wrapperProps = withContainer
17
36
  ? { edges: ["bottom", "left", "right"] }
18
37
  : { style: { flex: 1 } };
19
- return (_jsx(FormViewProvider, { description: description, errorMessage: errorMessage, hasError: hasError, isEmpty: isEmpty, isLoading: isLoading, item: item, name: name, onBack: onBack, onRefresh: onRefresh, onRetry: onRetry, readonly: readonly, children: _jsx(ErrorBoundary, { children: _jsx(Wrapper, { ...wrapperProps, children: children }) }) }));
38
+ return (_jsx(FormViewProvider, { description: description, errorMessage: errorMessage, hasError: hasError, isEmpty: isEmpty, isLoading: isLoading, item: item, name: name, onBack: onBack, onRefresh: onRefresh, onRetry: onRetry, readonly: readonly, children: _jsx(RefreshErrorContext.Provider, { value: onRefreshError, children: _jsx(ErrorBoundary, { children: _jsx(Wrapper, { ...wrapperProps, children: children }) }) }) }));
20
39
  }
21
40
  function Header({ variant = "default", tone, actions, children, topSpacing, background, }) {
22
41
  const { name, description, onBack } = useFormView();
@@ -36,24 +55,24 @@ function HeaderActionsBar({ children }) {
36
55
  function HeaderSegmented(props) {
37
56
  return _jsx(SegmentedControl, { ...props });
38
57
  }
39
- function Content({ withMargin = false, children, }) {
58
+ function Content({ withMargin = false, keyboardToolbar = false, keyboardFormProps, children, }) {
40
59
  const { name, isLoading, isEmpty, hasError, errorMessage, item, onRefresh, onRetry, onBack, showRefreshing, setShowRefreshing, } = useFormView();
41
60
  const bottomInset = useBottomInset();
42
61
  const theme = useTheme();
62
+ const onRefreshError = React.useContext(RefreshErrorContext);
43
63
  const handleRefresh = React.useCallback(async () => {
44
- setShowRefreshing(true);
45
- if (onRefresh) {
46
- await onRefresh();
47
- }
48
- setShowRefreshing(false);
49
- }, [onRefresh, setShowRefreshing]);
64
+ await refreshFormData(onRefresh, onRefreshError, setShowRefreshing);
65
+ }, [onRefresh, onRefreshError, setShowRefreshing]);
50
66
  const isCenteredContent = isLoading || isEmpty || !item || hasError;
51
- return (_jsx(KeyboardAwareScrollView, { bottomOffset: 20, contentContainerStyle: {
52
- flexGrow: 1,
53
- justifyContent: isCenteredContent ? "center" : "flex-start",
54
- alignItems: isCenteredContent ? "center" : "stretch",
55
- paddingBottom: bottomInset,
56
- }, keyboardShouldPersistTaps: "handled", refreshControl: _jsx(RefreshControl, { colors: [theme.primary], onRefresh: handleRefresh, refreshing: showRefreshing }), style: { flex: 1, backgroundColor: theme.bg.page }, children: isCenteredContent ? (_jsx(_Fragment, { children: isLoading ? (_jsx(Feedback.Loading, { message: `Cargando ${name}...` })) : hasError ? (_jsx(Feedback.Error, { message: errorMessage, onBack: onBack, onRetry: onRetry })) : (_jsx(Feedback.Empty, {})) })) : (_jsx(View, { style: withMargin ? { paddingHorizontal: 16 } : undefined, children: children })) }));
67
+ return (_jsx(KeyboardForm, { ...keyboardFormProps, contentContainerStyle: [
68
+ {
69
+ flexGrow: 1,
70
+ justifyContent: isCenteredContent ? "center" : "flex-start",
71
+ alignItems: isCenteredContent ? "center" : "stretch",
72
+ paddingBottom: bottomInset,
73
+ },
74
+ keyboardFormProps?.contentContainerStyle,
75
+ ], keyboardShouldPersistTaps: keyboardFormProps?.keyboardShouldPersistTaps ?? "handled", refreshControl: keyboardFormProps?.refreshControl ?? (_jsx(RefreshControl, { colors: [theme.primary], onRefresh: handleRefresh, refreshing: showRefreshing })), style: [{ flex: 1, backgroundColor: theme.bg.page }, keyboardFormProps?.style], toolbar: keyboardToolbar, children: isCenteredContent ? (_jsx(_Fragment, { children: isLoading ? (_jsx(Feedback.Loading, { message: `Cargando ${name}...` })) : hasError ? (_jsx(Feedback.Error, { message: errorMessage, onBack: onBack, onRetry: onRetry })) : (_jsx(Feedback.Empty, {})) })) : (_jsx(View, { style: withMargin ? { paddingHorizontal: 16 } : undefined, children: children })) }));
57
76
  }
58
77
  /**
59
78
  * @deprecated Content already handles isEmpty/!item by rendering Feedback.Empty internally.
@@ -2,7 +2,7 @@ import { type SearchHistoryProps } from "../Display/Display.SearchHistory.js";
2
2
  import type { AppBarBackground } from "../Navigation/Navigation.AppBar.js";
3
3
  import { type SegmentedControlProps } from "../Navigation/Navigation.SegmentedControl.js";
4
4
  import React, { ReactNode } from "react";
5
- import { StyleProp, ViewStyle } from "react-native";
5
+ import { type ScrollViewProps, StyleProp, ViewStyle } from "react-native";
6
6
  interface ListViewProps<T> {
7
7
  name: string;
8
8
  description?: string;
@@ -12,6 +12,7 @@ interface ListViewProps<T> {
12
12
  errorMessage?: string;
13
13
  readonly?: boolean;
14
14
  onRefresh?: () => Promise<void>;
15
+ onRefreshError?: (error: unknown) => void;
15
16
  onRetry?: () => void;
16
17
  onPressItem?: (item: T) => void;
17
18
  onDeleteItem?: (item: T) => void;
@@ -25,7 +26,8 @@ interface ListViewProps<T> {
25
26
  withContainer?: boolean;
26
27
  children: ReactNode;
27
28
  }
28
- declare function ListViewRoot<T>({ name, description, items, isLoading, hasError, errorMessage, readonly, onRefresh, onRetry, onPressItem, onDeleteItem, onSearch, onBack, onLoadMore, loadingMore, backgroundColor, withContainer, children, }: ListViewProps<T>): React.JSX.Element;
29
+ export declare function refreshListData(onRefresh: (() => Promise<void>) | undefined, onRefreshError: ((error: unknown) => void) | undefined, setShowRefreshing: (value: boolean) => void): Promise<void>;
30
+ declare function ListViewRoot<T>({ name, description, items, isLoading, hasError, errorMessage, readonly, onRefresh, onRefreshError, onRetry, onPressItem, onDeleteItem, onSearch, onBack, onLoadMore, loadingMore, backgroundColor, withContainer, children, }: ListViewProps<T>): React.JSX.Element;
29
31
  interface HeaderProps {
30
32
  /** AppBar display variant. `large` = big title (list/index). `center` = modal/detail. */
31
33
  variant?: "default" | "large" | "center";
@@ -79,14 +81,15 @@ declare function FilterChip({ label, active, onPress }: FilterChipProps): React.
79
81
  declare function FilterChips({ children }: {
80
82
  children: ReactNode;
81
83
  }): React.JSX.Element;
82
- declare function Content({ children, withMargin, }: {
84
+ type ListScrollProps = Pick<ScrollViewProps, "keyboardShouldPersistTaps" | "keyboardDismissMode" | "testID">;
85
+ declare function Content({ children, withMargin, keyboardShouldPersistTaps, keyboardDismissMode, testID, }: {
83
86
  children?: (item: any) => ReactNode;
84
87
  withMargin?: boolean;
85
- }): React.JSX.Element;
86
- declare function ContentList({ children, contentWithMargin, }: {
88
+ } & ListScrollProps): React.JSX.Element;
89
+ declare function ContentList({ children, contentWithMargin, keyboardShouldPersistTaps, keyboardDismissMode, testID, }: {
87
90
  children?: (item: any) => ReactNode;
88
91
  contentWithMargin: boolean;
89
- }): React.JSX.Element;
92
+ } & ListScrollProps): React.JSX.Element;
90
93
  declare function Footer({ children }: {
91
94
  children: ReactNode;
92
95
  }): React.JSX.Element | null;