@sincpro/mobile-ui 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/Dialog/BottomSheet/BottomSheet.Root.d.ts +2 -2
  2. package/dist/Dialog/BottomSheet/BottomSheet.Root.js +2 -2
  3. package/dist/Dialog/Dialog.Confirmation.js +5 -1
  4. package/dist/Dialog/Dialog.EditValue.js +1 -1
  5. package/dist/Display/Display.GradientSurface.d.ts +116 -0
  6. package/dist/Display/Display.GradientSurface.js +62 -0
  7. package/dist/Display/Display.KeyValue.js +1 -1
  8. package/dist/Display/Display.ListItem.d.ts +5 -1
  9. package/dist/Display/Display.ListItem.js +6 -2
  10. package/dist/Display/Display.Monetary.js +1 -1
  11. package/dist/Display/Display.Pattern.d.ts +26 -0
  12. package/dist/Display/Display.Pattern.js +27 -0
  13. package/dist/Display/Display.PriceTag.js +3 -3
  14. package/dist/Display/Display.Stat.js +1 -1
  15. package/dist/Display/Display.StatCard.js +1 -1
  16. package/dist/Display/index.d.ts +4 -0
  17. package/dist/Display/index.js +4 -0
  18. package/dist/Feedback/Feedback.DomainSplashScreen.js +3 -2
  19. package/dist/Feedback/Feedback.Loading.d.ts +6 -0
  20. package/dist/Feedback/Feedback.Loading.js +7 -1
  21. package/dist/Feedback/Feedback.Spinner.js +2 -1
  22. package/dist/Feedback/Feedback.Splash.d.ts +7 -1
  23. package/dist/Feedback/Feedback.Splash.js +11 -2
  24. package/dist/Feedback/Toast.context.d.ts +8 -1
  25. package/dist/Feedback/Toast.context.js +25 -23
  26. package/dist/Form/Form.Button.d.ts +4 -4
  27. package/dist/Form/Form.Button.js +19 -19
  28. package/dist/Form/Form.Checkbox.js +1 -1
  29. package/dist/Form/Form.Input.js +1 -1
  30. package/dist/Form/Form.InputField.js +1 -1
  31. package/dist/Form/Form.Switch.js +1 -1
  32. package/dist/Navigation/Navigation.AppBar.d.ts +18 -2
  33. package/dist/Navigation/Navigation.AppBar.js +48 -9
  34. package/dist/Navigation/Navigation.BottomNav.d.ts +33 -5
  35. package/dist/Navigation/Navigation.BottomNav.js +90 -17
  36. package/dist/Navigation/Navigation.SearchBar.js +7 -1
  37. package/dist/Navigation/Navigation.TabIcon.d.ts +4 -0
  38. package/dist/Navigation/Navigation.TabIcon.js +4 -0
  39. package/dist/Navigation/Navigation.TabItem.d.ts +5 -0
  40. package/dist/Navigation/Navigation.TabItem.js +5 -0
  41. package/dist/Navigation/index.d.ts +2 -2
  42. package/dist/Typography/Typography.Text.d.ts +1 -1
  43. package/dist/Typography/Typography.Text.js +16 -6
  44. package/dist/icons/OdooIcon.d.ts +5 -0
  45. package/dist/icons/OdooIcon.js +5 -0
  46. package/dist/icons/PrinterIcon.d.ts +5 -0
  47. package/dist/icons/PrinterIcon.js +5 -0
  48. package/dist/layouts/AuthContainer.js +2 -2
  49. package/dist/theme/dark.d.ts +3 -3
  50. package/dist/theme/dark.js +54 -53
  51. package/dist/theme/default.js +1 -0
  52. package/dist/theme/index.d.ts +1 -0
  53. package/dist/theme/index.js +1 -0
  54. package/dist/theme/types.d.ts +1 -0
  55. package/dist/theme/typography.d.ts +117 -60
  56. package/dist/theme/typography.js +115 -29
  57. package/dist/utils/timezone.d.ts +5 -0
  58. package/dist/views/FormViewV2.d.ts +2 -1
  59. package/dist/views/FormViewV2.js +8 -4
  60. package/dist/views/ListViewV2.context.d.ts +5 -1
  61. package/dist/views/ListViewV2.context.js +3 -1
  62. package/dist/views/ListViewV2.d.ts +7 -2
  63. package/dist/views/ListViewV2.js +18 -20
  64. package/dist/widgets/GeoPermissionCard.d.ts +5 -0
  65. package/dist/widgets/GeoPermissionCard.js +8 -3
  66. package/dist/widgets/JSONViewer.js +3 -6
  67. package/dist/widgets/TimeZoneSelector.d.ts +5 -0
  68. package/dist/widgets/TimeZoneSelector.js +6 -1
  69. package/dist/widgets/TimeZoneSelectorModal.d.ts +5 -0
  70. package/dist/widgets/TimeZoneSelectorModal.js +8 -3
  71. package/package.json +12 -7
@@ -6,26 +6,18 @@ import { tv } from "../theme/tw";
6
6
  import { Typography } from "../Typography";
7
7
  import { createContext, useCallback, useContext, useMemo, useRef, useState, } from "react";
8
8
  import { ActivityIndicator, View } from "react-native";
9
- import Animated, { FadeInDown, FadeOutDown } from "react-native-reanimated";
9
+ import Animated, { FadeInDown, FadeInUp, FadeOutDown, FadeOutUp, } from "react-native-reanimated";
10
10
  import { useSafeAreaInsets } from "react-native-safe-area-context";
11
11
  const ToastContext = createContext(null);
12
12
  const DEFAULT_DURATION = 3500;
13
+ // iOS-style card: rounded, hairline border + soft shadow, neutral surface. The variant is
14
+ // conveyed by the colored icon (no left-accent stripe — it doesn't read well on iOS).
13
15
  const toast = tv({
14
16
  slots: {
15
- root: "flex-row items-center gap-3 rounded-lg px-4 py-3 bg-bg-card border-l-4 shadow-lg",
17
+ root: "flex-row items-center gap-3 rounded-2xl px-4 py-3 bg-bg-card border border-border-light shadow-lg",
16
18
  title: "text-sm font-semibold text-text-primary",
17
19
  message: "text-sm text-text-secondary",
18
20
  },
19
- variants: {
20
- variant: {
21
- info: { root: "border-info" },
22
- success: { root: "border-success" },
23
- warning: { root: "border-warning" },
24
- danger: { root: "border-danger" },
25
- loading: { root: "border-primary" },
26
- },
27
- },
28
- defaultVariants: { variant: "info" },
29
21
  });
30
22
  const iconByVariant = {
31
23
  info: { name: "information-circle", color: theme.info },
@@ -33,15 +25,19 @@ const iconByVariant = {
33
25
  warning: { name: "warning", color: theme.warning },
34
26
  danger: { name: "close-circle", color: theme.danger },
35
27
  };
36
- function ToastItem({ entry, onHide }) {
28
+ function ToastItem({ entry, onHide, position, }) {
37
29
  const variant = entry.variant ?? "info";
38
- const styles = toast({ variant });
39
- return (_jsxs(Animated.View, { accessibilityLiveRegion: "polite", accessible: true, className: styles.root(), entering: FadeInDown.duration(220), exiting: FadeOutDown.duration(180), children: [variant === "loading" ? (_jsx(ActivityIndicator, { color: theme.primary, size: "small" })) : (_jsx(Icon, { color: iconByVariant[variant].color, name: iconByVariant[variant].name, size: 22 })), _jsxs(View, { className: "flex-1", children: [entry.title ? (_jsx(Typography.Text, { className: styles.title(), children: entry.title })) : null, _jsx(Typography.Text, { className: styles.message(), children: entry.message })] }), entry.action ? (_jsx(Pressable, { accessibilityLabel: entry.action.label, onPress: () => {
30
+ const styles = toast();
31
+ const entering = position === "top" ? FadeInDown : FadeInUp;
32
+ const exiting = position === "top" ? FadeOutUp : FadeOutDown;
33
+ const resolvedDuration = entry.duration ?? (variant === "loading" ? 0 : DEFAULT_DURATION);
34
+ const showClose = entry.dismissible ?? resolvedDuration === 0;
35
+ return (_jsxs(Animated.View, { accessibilityLiveRegion: "polite", accessible: true, className: styles.root(), entering: entering.duration(220), exiting: exiting.duration(180), children: [variant === "loading" ? (_jsx(ActivityIndicator, { color: theme.primary, size: "small" })) : (_jsx(Icon, { color: iconByVariant[variant].color, name: iconByVariant[variant].name, size: 22 })), _jsxs(View, { className: "flex-1", children: [entry.title ? (_jsx(Typography.Text, { className: styles.title(), children: entry.title })) : null, _jsx(Typography.Text, { className: styles.message(), children: entry.message })] }), entry.action ? (_jsx(Pressable, { accessibilityLabel: entry.action.label, onPress: () => {
40
36
  entry.action?.onPress();
41
37
  onHide(entry.id);
42
- }, children: _jsx(Typography.Text, { className: "text-primary text-sm font-semibold", children: entry.action.label }) })) : null] }));
38
+ }, children: _jsx(Typography.Text, { className: "text-primary text-sm font-semibold", children: entry.action.label }) })) : null, showClose ? (_jsx(Pressable, { accessibilityLabel: "Cerrar", hitSlop: 8, onPress: () => onHide(entry.id), children: _jsx(Icon, { color: theme.text.tertiary, name: "close", size: 18 }) })) : null] }));
43
39
  }
44
- export function ToastProvider({ children }) {
40
+ export function ToastProvider({ children, position = "bottom", }) {
45
41
  const insets = useSafeAreaInsets();
46
42
  const [toasts, setToasts] = useState([]);
47
43
  const counter = useRef(0);
@@ -65,11 +61,17 @@ export function ToastProvider({ children }) {
65
61
  }
66
62
  }, [clearTimer, hide]);
67
63
  const show = useCallback((options) => {
68
- counter.current += 1;
69
- const id = `toast-${counter.current}`;
70
- setToasts((prev) => [...prev, { ...options, id }]);
71
- scheduleDismiss(id, options.variant, options.duration);
72
- return id;
64
+ let id = options.id;
65
+ if (!id) {
66
+ counter.current += 1;
67
+ id = `toast-${counter.current}`;
68
+ }
69
+ const toastId = id;
70
+ setToasts((prev) => prev.some((t) => t.id === toastId)
71
+ ? prev.map((t) => (t.id === toastId ? { ...t, ...options, id: toastId } : t))
72
+ : [...prev, { ...options, id: toastId }]);
73
+ scheduleDismiss(toastId, options.variant, options.duration);
74
+ return toastId;
73
75
  }, [scheduleDismiss]);
74
76
  const update = useCallback((id, options) => {
75
77
  setToasts((prev) => prev.map((t) => (t.id === id ? { ...t, ...options } : t)));
@@ -108,7 +110,7 @@ export function ToastProvider({ children }) {
108
110
  promise,
109
111
  };
110
112
  }, [show, update, hide]);
111
- return (_jsxs(ToastContext.Provider, { value: value, children: [children, _jsx(View, { className: "absolute left-0 right-0 px-4 gap-2 z-toast", pointerEvents: "box-none", style: { bottom: insets.bottom + 12 }, children: toasts.map((entry) => (_jsx(ToastItem, { entry: entry, onHide: hide }, entry.id))) })] }));
113
+ return (_jsxs(ToastContext.Provider, { value: value, children: [children, _jsx(View, { className: "absolute left-0 right-0 px-4 gap-2 z-toast", pointerEvents: "box-none", style: position === "top" ? { top: insets.top + 12 } : { bottom: insets.bottom + 12 }, children: toasts.map((entry) => (_jsx(ToastItem, { entry: entry, onHide: hide, position: position }, entry.id))) })] }));
112
114
  }
113
115
  export function useToast() {
114
116
  const ctx = useContext(ToastContext);
@@ -21,7 +21,7 @@ declare const button: import("tailwind-variants").TVReturnType<{
21
21
  disabled: {
22
22
  true: string;
23
23
  };
24
- }, undefined, "rounded-lg items-center justify-center flex-row min-h-[44px]", {
24
+ }, undefined, "rounded-[13px] items-center justify-center flex-row min-h-[44px]", {
25
25
  variant: {
26
26
  primary: string;
27
27
  accent: string;
@@ -63,7 +63,7 @@ declare const button: import("tailwind-variants").TVReturnType<{
63
63
  disabled: {
64
64
  true: string;
65
65
  };
66
- }, undefined, "rounded-lg items-center justify-center flex-row min-h-[44px]", unknown, unknown, undefined>>;
66
+ }, undefined, "rounded-[13px] items-center justify-center flex-row min-h-[44px]", unknown, unknown, undefined>>;
67
67
  declare const buttonIcon: import("tailwind-variants").TVReturnType<{
68
68
  variant: {
69
69
  primary: string;
@@ -85,7 +85,7 @@ declare const buttonIcon: import("tailwind-variants").TVReturnType<{
85
85
  disabled: {
86
86
  true: string;
87
87
  };
88
- }, undefined, "rounded-lg items-center justify-center aspect-square", {
88
+ }, undefined, "rounded-[13px] items-center justify-center aspect-square", {
89
89
  variant: {
90
90
  primary: string;
91
91
  accent: string;
@@ -127,7 +127,7 @@ declare const buttonIcon: import("tailwind-variants").TVReturnType<{
127
127
  disabled: {
128
128
  true: string;
129
129
  };
130
- }, undefined, "rounded-lg items-center justify-center aspect-square", unknown, unknown, undefined>>;
130
+ }, undefined, "rounded-[13px] items-center justify-center aspect-square", unknown, unknown, undefined>>;
131
131
  export interface ButtonProps extends VariantProps<typeof button> {
132
132
  title: string;
133
133
  onPress?: () => void;
@@ -12,13 +12,13 @@ import { runOnJS } from "react-native-reanimated";
12
12
  // STYLE VARIANTS
13
13
  // ============================================================================
14
14
  const button = tv({
15
- base: "rounded-lg items-center justify-center flex-row min-h-[44px]",
15
+ base: "rounded-[13px] items-center justify-center flex-row min-h-[44px]",
16
16
  variants: {
17
17
  variant: {
18
18
  primary: "shadow-sm",
19
19
  accent: "shadow-sm",
20
20
  cta: "shadow-sm",
21
- secondary: "bg-bg-card border border-primary shadow-sm",
21
+ secondary: "bg-bg-muted",
22
22
  outline: "bg-transparent border border-primary",
23
23
  outlineDanger: "bg-transparent border border-danger",
24
24
  outlineBlack: "bg-transparent border border-border-strong",
@@ -41,13 +41,13 @@ const button = tv({
41
41
  },
42
42
  });
43
43
  const buttonIcon = tv({
44
- base: "rounded-lg items-center justify-center aspect-square",
44
+ base: "rounded-[13px] items-center justify-center aspect-square",
45
45
  variants: {
46
46
  variant: {
47
47
  primary: "shadow-sm",
48
48
  accent: "shadow-sm",
49
49
  cta: "shadow-sm",
50
- secondary: "bg-bg-card border border-primary shadow-sm",
50
+ secondary: "bg-bg-muted",
51
51
  outline: "bg-transparent border border-primary",
52
52
  outlineDanger: "bg-transparent border border-danger",
53
53
  outlineBlack: "bg-transparent border border-border-strong",
@@ -73,9 +73,9 @@ const textColor = tv({
73
73
  variants: {
74
74
  variant: {
75
75
  primary: "text-white",
76
- accent: "text-white",
77
- cta: "text-white",
78
- secondary: "text-primary",
76
+ accent: "text-text-on-accent",
77
+ cta: "text-text-on-accent",
78
+ secondary: "text-text-primary",
79
79
  outline: "text-primary",
80
80
  outlineDanger: "text-danger",
81
81
  outlineBlack: "text-text-primary",
@@ -96,12 +96,12 @@ const textColor = tv({
96
96
  {
97
97
  variant: "accent",
98
98
  disabled: true,
99
- className: "text-white/70",
99
+ className: "text-text-on-accent",
100
100
  },
101
101
  {
102
102
  variant: "cta",
103
103
  disabled: true,
104
- className: "text-white/70",
104
+ className: "text-text-on-accent",
105
105
  },
106
106
  {
107
107
  variant: "transparent",
@@ -142,7 +142,7 @@ function getLoadingColorDynamic(variant) {
142
142
  primary: theme.text.inverse,
143
143
  accent: theme.text.inverse,
144
144
  cta: theme.text.inverse,
145
- secondary: theme.primary,
145
+ secondary: theme.text.primary,
146
146
  outline: theme.primary,
147
147
  outlineDanger: theme.danger,
148
148
  outlineBlack: theme.text.primary,
@@ -156,9 +156,9 @@ function getIconColorDynamic(variant, disabled) {
156
156
  if (disabled) {
157
157
  const disabledMap = {
158
158
  primary: theme.icon.inverse + "70",
159
- accent: theme.icon.inverse + "70",
160
- cta: theme.icon.inverse + "70",
161
- secondary: theme.icon.disabled,
159
+ accent: theme.text.onAccent + "70",
160
+ cta: theme.text.onAccent + "70",
161
+ secondary: theme.text.disabled,
162
162
  outline: theme.icon.disabled,
163
163
  outlineDanger: theme.danger + "7f",
164
164
  outlineBlack: theme.text.primary + "7f",
@@ -170,9 +170,9 @@ function getIconColorDynamic(variant, disabled) {
170
170
  }
171
171
  const enabledMap = {
172
172
  primary: theme.icon.inverse,
173
- accent: theme.icon.inverse,
174
- cta: theme.icon.inverse,
175
- secondary: theme.primary,
173
+ accent: theme.text.onAccent,
174
+ cta: theme.text.onAccent,
175
+ secondary: theme.text.primary,
176
176
  outline: theme.primary,
177
177
  outlineDanger: theme.danger,
178
178
  outlineBlack: theme.text.primary,
@@ -239,7 +239,7 @@ const Button = ({ title, onPress, confirm, variant = "primary", size = "medium",
239
239
  if (variant === "primary" || variant === "accent" || variant === "cta") {
240
240
  const gradientColors = variant === "primary" ? theme.gradient.primary : theme.gradient.accent;
241
241
  return (_jsx(LinearGradient, { colors: gradientColors, end: { x: 1, y: 1 }, start: { x: 0, y: 0 }, style: {
242
- borderRadius: 8,
242
+ borderRadius: 13,
243
243
  opacity: disabled ? 0.7 : 1,
244
244
  }, children: _jsx(TouchableOpacity, { activeOpacity: 0.8, disabled: disabled || loading, onPress: handlePress, children: _jsx(View, { className: button({ size, disabled }), style: { backgroundColor: "transparent" }, children: loading ? (_jsx(ActivityIndicator, { color: loadingColorValue, size: "small" })) : (_jsxs(_Fragment, { children: [icon ? _jsx(View, { className: "mr-2", children: icon }) : null, _jsx(Typography.Text, { allowFontScaling: false, bold: true, className: textColorClass, ellipsizeMode: "tail", numberOfLines: 1, variant: textVariantValue, children: title })] })) }) }) }));
245
245
  }
@@ -269,8 +269,8 @@ const ButtonIcon = ({ icon, onPress, confirm, variant = "primary", size = "mediu
269
269
  }
270
270
  }, [onPress, confirm, show, hide]);
271
271
  if (variant === "primary") {
272
- return (_jsx(LinearGradient, { colors: disabled ? ["#93c5fd", "#60a5fa"] : ["#2563eb", "#1d4ed8"], end: { x: 1, y: 1 }, start: { x: 0, y: 0 }, style: {
273
- borderRadius: 8,
272
+ return (_jsx(LinearGradient, { colors: theme.gradient.primary, end: { x: 1, y: 1 }, start: { x: 0, y: 0 }, style: {
273
+ borderRadius: 13,
274
274
  opacity: disabled ? 0.7 : 1,
275
275
  }, children: _jsx(TouchableOpacity, { activeOpacity: 0.8, disabled: disabled || loading, onPress: handlePress, children: _jsx(View, { className: buttonIcon({ size, disabled }), style: { backgroundColor: "transparent" }, children: loading ? (_jsx(ActivityIndicator, { color: loadingColorValue, size: "small" })) : (_jsx(Display.Icon, { color: iconColorValue, name: icon, size: iconSizeValue })) }) }) }));
276
276
  }
@@ -25,6 +25,6 @@ function RowCheckBox({ mainText, subText, rightText, rightComponent, leftCompone
25
25
  }
26
26
  return null;
27
27
  };
28
- return (_jsxs(Pressable, { className: cn("flex-row justify-between items-center", disabled && "opacity-50", className), disabled: disabled, onPress: handlePress, children: [_jsx(View, { children: renderLeftComponent() }), _jsxs(View, { className: "flex-row items-center", children: [renderRightComponent(), _jsx(Checkbox, { className: "ml-2", color: theme.primary, disabled: disabled, pointerEvents: "none", value: isChecked })] })] }));
28
+ return (_jsxs(Pressable, { className: cn("flex-row justify-between items-center", disabled && "opacity-50", className), disabled: disabled, onPress: handlePress, children: [_jsx(View, { children: renderLeftComponent() }), _jsxs(View, { className: "flex-row items-center", children: [renderRightComponent(), _jsx(Checkbox, { className: "ml-2", color: theme.accent, disabled: disabled, pointerEvents: "none", value: isChecked })] })] }));
29
29
  }
30
30
  export default RowCheckBox;
@@ -9,7 +9,7 @@ const inputContainer = tv({
9
9
  variants: {
10
10
  state: {
11
11
  error: "border-2 border-danger",
12
- focused: "border-2 border-ring",
12
+ focused: "border-2 border-border-focus",
13
13
  default: "border border-input",
14
14
  },
15
15
  },
@@ -9,7 +9,7 @@ const inputFieldBorder = tv({
9
9
  variants: {
10
10
  state: {
11
11
  error: "border-2 border-danger",
12
- focused: "border-2 border-ring",
12
+ focused: "border-2 border-border-focus",
13
13
  default: "border border-input",
14
14
  },
15
15
  },
@@ -4,6 +4,6 @@ import { cn } from "../theme/tw";
4
4
  import { Typography } from "../Typography";
5
5
  import { Switch as RNSwitch, View } from "react-native";
6
6
  function Switch({ value, onValueChange, label, disabled = false, className, testID, }) {
7
- 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 })] }));
7
+ 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.accent }, value: value })] }));
8
8
  }
9
9
  export default Switch;
@@ -6,13 +6,17 @@ export interface AppBarActionProps {
6
6
  accessibilityLabel?: string;
7
7
  /** `tinted` highlights (primary action); `plain` is the neutral contained default. */
8
8
  tone?: "plain" | "tinted";
9
+ /** Render for a dark surface (translucent light button + light icon). */
10
+ onDark?: boolean;
11
+ /** Drop the contained surface → bare icon (iOS-style plain back/action). */
12
+ bare?: boolean;
9
13
  testID?: string;
10
14
  }
11
15
  /**
12
16
  * "Contained" icon-button for the AppBar: circular touch area with a subtle surface
13
17
  * (merge of Material 3 state-layer + iOS 26 contained button). Optional badge.
14
18
  */
15
- declare function AppBarAction({ icon, onPress, badge, accessibilityLabel, tone, testID, }: AppBarActionProps): import("react").JSX.Element;
19
+ declare function AppBarAction({ icon, onPress, badge, accessibilityLabel, tone, onDark, bare, testID, }: AppBarActionProps): import("react").JSX.Element;
16
20
  export interface AppBarProps {
17
21
  title?: string;
18
22
  subtitle?: string;
@@ -21,6 +25,18 @@ export interface AppBarProps {
21
25
  actions?: ReactNode;
22
26
  subheader?: ReactNode;
23
27
  variant?: "default" | "large" | "center";
28
+ /**
29
+ * Surface tone:
30
+ * - `default` — tokenized light card.
31
+ * - `dark` — always-dark hero (light text + light contained actions).
32
+ * - `gradient` — brand-accent gradient hero for greeting headers ("Buenos días, …"),
33
+ * with dark text via `text.onAccent` (reads `theme.gradient.accent`).
34
+ */
35
+ tone?: "default" | "dark" | "gradient";
36
+ /** [large variant] Show a left vertical accent bar beside the large title. */
37
+ accentBar?: boolean;
38
+ /** Back button style: `contained` (circular surface) or `plain` (bare iOS chevron). */
39
+ backVariant?: "contained" | "plain";
24
40
  border?: boolean;
25
41
  /** Reserves the status-bar space (insets.top). `false` to embed/demo. */
26
42
  safeArea?: boolean;
@@ -33,7 +49,7 @@ export interface AppBarProps {
33
49
  * a `subheader` slot (for SearchBar/Segmented below the title), and a `large` variant.
34
50
  * Replaces the gradient look of `Navigation.Header`/`ScreenHeader`.
35
51
  */
36
- declare function AppBar({ title, subtitle, onBack, leading, actions, subheader, variant, border, safeArea, className, testID, }: AppBarProps): import("react").JSX.Element;
52
+ declare function AppBar({ title, subtitle, onBack, leading, actions, subheader, variant, tone, accentBar, backVariant, border, safeArea, className, testID, }: AppBarProps): import("react").JSX.Element;
37
53
  declare const AppBarWithAction: typeof AppBar & {
38
54
  Action: typeof AppBarAction;
39
55
  };
@@ -1,21 +1,36 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "nativewind/jsx-runtime";
2
+ import GradientSurface from "../Display/Display.GradientSurface";
2
3
  import Icon from "../Display/Display.Icon";
3
4
  import Pressable from "../primitives/Pressable";
4
5
  import { theme } from "../theme";
5
6
  import { cn } from "../theme/tw";
6
7
  import { Typography } from "../Typography";
7
- import { View } from "react-native";
8
+ import { StyleSheet, View } from "react-native";
8
9
  import { useSafeAreaInsets } from "react-native-safe-area-context";
10
+ /** Fallback for the `tone="dark"` hero surface when the theme has no `bg.inverse` token. */
11
+ const HERO_DARK_FALLBACK = "#0E1714";
9
12
  /**
10
13
  * "Contained" icon-button for the AppBar: circular touch area with a subtle surface
11
14
  * (merge of Material 3 state-layer + iOS 26 contained button). Optional badge.
12
15
  */
13
- function AppBarAction({ icon, onPress, badge, accessibilityLabel, tone = "plain", testID, }) {
14
- return (_jsxs(Pressable, { accessibilityLabel: accessibilityLabel, accessibilityRole: "button", className: "items-center justify-center rounded-full", hitSlop: 6, onPress: onPress, style: {
15
- width: 38,
16
- height: 38,
17
- backgroundColor: tone === "tinted" ? theme.bg.accent : theme.bg.muted,
18
- }, testID: testID, children: [_jsx(Icon, { color: tone === "tinted" ? theme.primary : theme.icon.primary, name: icon, size: 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] }));
16
+ function AppBarAction({ icon, onPress, badge, accessibilityLabel, tone = "plain", onDark = false, bare = false, testID, }) {
17
+ const bg = bare
18
+ ? "transparent"
19
+ : onDark
20
+ ? "rgba(255,255,255,0.14)"
21
+ : tone === "tinted"
22
+ ? theme.bg.accent
23
+ : theme.bg.card;
24
+ const iconColor = tone === "tinted" ? theme.primary : onDark ? theme.icon.inverse : theme.icon.primary;
25
+ // Plain (white) squircle gets a hairline so it reads as a contained button on a light bar.
26
+ const showBorder = !bare && !onDark && tone !== "tinted";
27
+ return (_jsxs(Pressable, { accessibilityLabel: accessibilityLabel, accessibilityRole: "button", className: "items-center justify-center", hitSlop: 6, onPress: onPress, style: {
28
+ width: 40,
29
+ height: 40,
30
+ borderRadius: 12,
31
+ backgroundColor: bg,
32
+ ...(showBorder ? { borderWidth: 1, borderColor: theme.border.light } : null),
33
+ }, testID: testID, children: [_jsx(Icon, { color: iconColor, name: icon, 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] }));
19
34
  }
20
35
  /**
21
36
  * Flat, modern top bar (merge of Material 3 + iOS 26): tokenized surface, left-aligned
@@ -23,10 +38,34 @@ function AppBarAction({ icon, onPress, badge, accessibilityLabel, tone = "plain"
23
38
  * a `subheader` slot (for SearchBar/Segmented below the title), and a `large` variant.
24
39
  * Replaces the gradient look of `Navigation.Header`/`ScreenHeader`.
25
40
  */
26
- function AppBar({ title, subtitle, onBack, leading, actions, subheader, variant = "default", border = true, safeArea = true, className, testID, }) {
41
+ function AppBar({ title, subtitle, onBack, leading, actions, subheader, variant = "default", tone = "default", accentBar = false, backVariant = "contained", border = true, safeArea = true, className, testID, }) {
27
42
  const insets = useSafeAreaInsets();
28
43
  const centered = variant === "center";
29
- return (_jsxs(View, { className: cn("bg-bg-card", border && "border-b border-border-light", className), style: { paddingTop: safeArea ? insets.top : 0 }, testID: testID, children: [_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", icon: "chevron-back", onPress: onBack })) : null, leading] }), variant !== "large" ? (_jsxs(View, { className: cn("flex-1", centered && "items-center"), children: [title ? (_jsx(Typography.Text, { className: "text-text-primary", numberOfLines: 1, semibold: true, variant: "subtitle", children: title })) : null, subtitle ? (_jsx(Typography.Text, { className: "text-text-secondary", numberOfLines: 1, variant: "caption", children: subtitle })) : null] })) : (_jsx(View, { className: "flex-1" })), _jsx(View, { className: "flex-row items-center justify-end gap-2", style: { minWidth: 38 }, children: actions })] }), variant === "large" && title ? (_jsxs(View, { className: "px-4 pb-2 pt-0.5", 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] })) : null, subheader ? _jsx(View, { className: "px-3 pb-3 pt-0.5", children: subheader }) : null] }));
44
+ const large = variant === "large";
45
+ const dark = tone === "dark";
46
+ const gradient = tone === "gradient";
47
+ // Greeting/balance hero (large, no back/leading): drop the empty top toolbar row and put
48
+ // the action(s) inline beside the big title — avoids a tall dead zone above the title.
49
+ const largeInlineActions = large && !onBack && !leading;
50
+ // Both `dark` and `gradient` are colored surfaces: no card bg, no hairline, light/dark text.
51
+ const colored = dark || gradient;
52
+ const onColoredText = gradient ? theme.text.onAccent : theme.text.inverse;
53
+ const titleStyle = colored ? { color: onColoredText } : undefined;
54
+ const subtitleStyle = gradient
55
+ ? { color: theme.text.onAccent, opacity: 0.72 }
56
+ : dark
57
+ ? { color: "rgba(255,255,255,0.7)" }
58
+ : undefined;
59
+ return (_jsxs(View, { className: cn(colored ? "" : "bg-bg-card", !colored && border && "border-b border-border-light", className), style: {
60
+ paddingTop: safeArea ? insets.top : 0,
61
+ ...(dark ? { backgroundColor: theme.bg.inverse ?? HERO_DARK_FALLBACK } : null),
62
+ }, testID: testID, children: [gradient ? (_jsx(GradientSurface, { colors: theme.gradient.accent, padding: "none", radius: "none", style: StyleSheet.absoluteFill })) : null, largeInlineActions ? null : (_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, onPress: onBack })) : null, leading] }), !large ? (_jsxs(View, { className: cn("flex-1", centered && "items-center"), children: [title ? (_jsx(Typography.Text, { className: colored ? "" : "text-text-primary", numberOfLines: 1, semibold: true, style: titleStyle, variant: "subtitle", children: title })) : null, subtitle ? (_jsx(Typography.Text, { className: colored ? "" : "text-text-secondary", numberOfLines: 1, style: subtitleStyle, variant: "caption", children: subtitle })) : null] })) : (_jsx(View, { className: "flex-1" })), _jsx(View, { className: "flex-row items-center justify-end gap-2", style: { minWidth: 38 }, children: actions })] })), large && title ? (_jsxs(View, { className: cn("flex-row px-4 pb-3", largeInlineActions ? "pt-2" : "pt-0.5"), children: [accentBar ? (_jsx(View, { style: {
63
+ width: 4,
64
+ marginRight: 10,
65
+ marginVertical: 2,
66
+ borderRadius: 999,
67
+ backgroundColor: theme.accent,
68
+ } })) : null, _jsxs(View, { className: "flex-1", children: [_jsx(Typography.Text, { className: colored ? "" : "text-text-primary", style: titleStyle, variant: "h2", children: title }), subtitle ? (_jsx(Typography.Text, { className: colored ? "" : "text-text-secondary", style: subtitleStyle, variant: "bodySmall", children: subtitle })) : null] }), largeInlineActions && actions ? (_jsx(View, { className: "flex-row items-center justify-end gap-2 pl-2", children: actions })) : null] })) : null, subheader ? _jsx(View, { className: "px-3 pb-3 pt-0.5", children: subheader }) : null] }));
30
69
  }
31
70
  const AppBarWithAction = Object.assign(AppBar, { Action: AppBarAction });
32
71
  export default AppBarWithAction;
@@ -1,8 +1,13 @@
1
+ import type { CustomIconProps } from "../icons/props";
2
+ import type { ComponentType } from "react";
1
3
  export interface BottomNavItem {
2
4
  key: string;
3
5
  label?: string;
4
- icon: string;
6
+ /** Icon font name (Display.Icon). Provide this OR `customIcon`. */
7
+ icon?: string;
5
8
  activeIcon?: string;
9
+ /** Brand/custom SVG icon component. Takes precedence over `icon` (for non-font brand marks). */
10
+ customIcon?: ComponentType<CustomIconProps>;
6
11
  badge?: string | number;
7
12
  }
8
13
  export interface BottomNavCenterAction {
@@ -10,20 +15,43 @@ export interface BottomNavCenterAction {
10
15
  onPress: () => void;
11
16
  accessibilityLabel?: string;
12
17
  }
18
+ /**
19
+ * Active-tab indicator style:
20
+ * - `none`: color only.
21
+ * - `pill`: filled round pill behind the icon.
22
+ * - `pill-text`: extended pill with icon + label inside (inactive = icon only).
23
+ * - `bar`: top accent bar over the active item (label below).
24
+ * - `glow`: glowing dot under the active icon (pairs with `tone="dark"`).
25
+ */
26
+ export type BottomNavIndicator = "none" | "pill" | "pill-text" | "bar" | "glow";
13
27
  export interface BottomNavProps {
14
28
  items: BottomNavItem[];
15
29
  value: string;
16
30
  onChange: (key: string) => void;
31
+ /** Show the text label under each icon. `false` = icon-only. */
17
32
  showLabels?: boolean;
33
+ /** Elevated center FAB (delivery/social apps). Splits items left/right around it. */
18
34
  centerAction?: BottomNavCenterAction;
19
- /** Reserva el espacio inferior (insets.bottom). `false` al embeber en FloatingBar. */
35
+ /** Reserve the bottom inset (insets.bottom). Auto-disabled for `floating`/`dark`. */
20
36
  safeArea?: boolean;
37
+ /** `bar` = full-width docked bar; `floating` = self-contained rounded pill (shadow, margins). */
38
+ shape?: "bar" | "floating";
39
+ /** Surface tone. `dark` renders a dark rounded bar (pairs with `glow`). */
40
+ tone?: "light" | "dark";
41
+ /** Active-tab indicator style (see BottomNavIndicator). */
42
+ indicator?: BottomNavIndicator;
43
+ /** [light bar only] Top hairline + card surface. */
44
+ bordered?: boolean;
45
+ /** Token for the active color. `primary` (default) or `accent` (e.g. green CTA brand). */
46
+ activeColor?: "primary" | "accent";
21
47
  className?: string;
22
48
  testID?: string;
23
49
  }
24
50
  /**
25
- * Bottom navigation flat y moderna: superficie tokenizada + hairline superior, activo en
26
- * `primary`. `centerAction` agrega un FAB central elevado (estilo apps de delivery/social).
51
+ * The single bottom-navigation component. Extensible by props, not by forking:
52
+ * `shape` (bar/floating), `tone` (light/dark), `indicator` (none/pill/pill-text/bar/glow),
53
+ * `showLabels`, `centerAction` (FAB), `activeColor`. Replaces the legacy `TabNavigatorLayout`.
27
54
  */
28
- declare function BottomNav({ items, value, onChange, showLabels, centerAction, safeArea, className, testID, }: BottomNavProps): import("react").JSX.Element;
55
+ declare function BottomNav({ items, value, onChange, showLabels, centerAction, safeArea, shape, tone, indicator, bordered, activeColor, className, testID, }: BottomNavProps): import("react").JSX.Element;
29
56
  export default BottomNav;
57
+ export { BottomNav };
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "nativewind/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "nativewind/jsx-runtime";
2
2
  import Icon from "../Display/Display.Icon";
3
3
  import Pressable from "../primitives/Pressable";
4
4
  import { theme } from "../theme";
@@ -6,30 +6,103 @@ import { cn } from "../theme/tw";
6
6
  import { Typography } from "../Typography";
7
7
  import { View } from "react-native";
8
8
  import { useSafeAreaInsets } from "react-native-safe-area-context";
9
+ const DARK_SURFACE = "#15181B";
9
10
  /**
10
- * Bottom navigation flat y moderna: superficie tokenizada + hairline superior, activo en
11
- * `primary`. `centerAction` agrega un FAB central elevado (estilo apps de delivery/social).
11
+ * The single bottom-navigation component. Extensible by props, not by forking:
12
+ * `shape` (bar/floating), `tone` (light/dark), `indicator` (none/pill/pill-text/bar/glow),
13
+ * `showLabels`, `centerAction` (FAB), `activeColor`. Replaces the legacy `TabNavigatorLayout`.
12
14
  */
13
- function BottomNav({ items, value, onChange, showLabels = true, centerAction, safeArea = true, className, testID, }) {
15
+ function BottomNav({ items, value, onChange, showLabels = true, centerAction, safeArea = true, shape = "bar", tone = "light", indicator = "none", bordered = true, activeColor = "primary", className, testID, }) {
14
16
  const insets = useSafeAreaInsets();
15
- const bottomPad = safeArea ? insets.bottom : 0;
17
+ const dark = tone === "dark";
18
+ const rounded = shape === "floating" || dark;
19
+ const bottomPad = rounded ? 6 : safeArea ? insets.bottom : 0;
20
+ const activeTint = activeColor === "accent" ? theme.accent : theme.primary;
21
+ const onActive = activeColor === "accent" ? theme.text.onAccent : theme.icon.inverse;
22
+ const inactiveColor = dark ? "rgba(255,255,255,0.55)" : theme.icon.secondary;
23
+ const surfaceColor = dark ? DARK_SURFACE : theme.bg.card;
24
+ const containerStyle = rounded
25
+ ? {
26
+ borderRadius: 32,
27
+ paddingBottom: bottomPad,
28
+ paddingTop: 6,
29
+ paddingHorizontal: 6,
30
+ minHeight: 64,
31
+ backgroundColor: surfaceColor,
32
+ ...theme.shadow.lg,
33
+ }
34
+ : {
35
+ paddingBottom: bottomPad,
36
+ minHeight: 56,
37
+ backgroundColor: surfaceColor,
38
+ ...(bordered ? { borderTopWidth: 1, borderTopColor: theme.border.light } : null),
39
+ };
40
+ const renderIconNode = (item, active, color) => {
41
+ 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 }));
42
+ 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] }));
43
+ };
16
44
  const renderItem = (item) => {
17
45
  const active = item.key === value;
18
- const color = active ? theme.primary : theme.icon.secondary;
19
- return (_jsxs(Pressable, { accessibilityLabel: item.label ?? item.key, accessibilityRole: "tab", accessibilityState: { selected: active }, className: "flex-1 items-center justify-center py-1.5", onPress: () => onChange(item.key), children: [_jsxs(View, { children: [_jsx(Icon, { color: color, name: active ? (item.activeIcon ?? item.icon) : item.icon, size: 24 }), 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: theme.bg.card }, children: _jsx(Typography.Text, { style: { color: theme.text.inverse, fontSize: 9, lineHeight: 11 }, children: item.badge }) })) : null] }), showLabels && item.label ? (_jsx(Typography.Text, { numberOfLines: 1, style: { color, fontSize: 11, marginTop: 3 }, children: item.label })) : null] }, item.key));
46
+ // Variant A — extended pill with text (active = horizontal pill, inactive = icon only).
47
+ if (indicator === "pill-text") {
48
+ if (active) {
49
+ return (_jsxs(Pressable, { accessibilityLabel: item.label ?? item.key, accessibilityRole: "tab", accessibilityState: { selected: true }, className: "flex-row items-center px-4 py-2", onPress: () => onChange(item.key), style: { borderRadius: 999, backgroundColor: activeTint }, children: [renderIconNode(item, true, onActive), item.label ? (_jsx(Typography.Text, { semibold: true, style: { color: onActive, fontSize: 13, marginLeft: 8 }, children: item.label })) : null] }, item.key));
50
+ }
51
+ return (_jsx(Pressable, { accessibilityLabel: item.label ?? item.key, accessibilityRole: "tab", className: "items-center justify-center px-3 py-2", onPress: () => onChange(item.key), children: renderIconNode(item, false, inactiveColor) }, item.key));
52
+ }
53
+ // Variants none / pill / bar / glow — vertical icon + optional indicator + optional label.
54
+ const inPill = indicator === "pill" && active;
55
+ const iconColor = inPill
56
+ ? onActive
57
+ : active
58
+ ? indicator === "glow"
59
+ ? activeTint
60
+ : activeTint
61
+ : inactiveColor;
62
+ const labelColor = active ? (dark ? "#FFFFFF" : activeTint) : inactiveColor;
63
+ return (_jsxs(Pressable, { accessibilityLabel: item.label ?? item.key, accessibilityRole: "tab", accessibilityState: { selected: active }, className: "flex-1 items-center justify-center py-1.5", onPress: () => onChange(item.key), children: [indicator === "bar" && active ? (_jsx(View, { style: {
64
+ position: "absolute",
65
+ top: 0,
66
+ width: 26,
67
+ height: 3,
68
+ borderRadius: 999,
69
+ backgroundColor: activeTint,
70
+ } })) : null, _jsx(View, { className: inPill ? "px-5 py-1.5" : undefined, style: inPill ? { borderRadius: 999, backgroundColor: activeTint } : undefined, children: renderIconNode(item, active, iconColor) }), indicator === "glow" && active ? (_jsx(View, { style: {
71
+ width: 5,
72
+ height: 5,
73
+ borderRadius: 999,
74
+ marginTop: 4,
75
+ backgroundColor: activeTint,
76
+ shadowColor: activeTint,
77
+ shadowOpacity: 0.9,
78
+ shadowRadius: 5,
79
+ elevation: 4,
80
+ } })) : null, showLabels && item.label && indicator !== "glow" ? (_jsx(Typography.Text, { numberOfLines: 1, semibold: active, style: { color: labelColor, fontSize: 11, marginTop: 3 }, children: item.label })) : null] }, item.key));
20
81
  };
82
+ const renderCenterFab = () => (_jsx(Pressable, { accessibilityLabel: centerAction?.accessibilityLabel ?? "Acción", accessibilityRole: "button", className: "items-center justify-center", onPress: centerAction?.onPress, style: {
83
+ width: 52,
84
+ height: 52,
85
+ borderRadius: 18,
86
+ marginTop: -20,
87
+ marginHorizontal: 6,
88
+ backgroundColor: activeTint,
89
+ // Glow halo en el color de acento (iOS shadow + Android elevation).
90
+ shadowColor: activeTint,
91
+ shadowOffset: { width: 0, height: 4 },
92
+ shadowOpacity: 0.55,
93
+ shadowRadius: 14,
94
+ elevation: 10,
95
+ }, children: _jsx(Icon, { color: onActive, name: centerAction?.icon ?? "add", size: 26 }) }));
96
+ let content;
21
97
  if (centerAction) {
22
98
  const mid = Math.ceil(items.length / 2);
23
- const left = items.slice(0, mid);
24
- const right = items.slice(mid);
25
- return (_jsxs(View, { className: cn("flex-row items-center bg-bg-card border-t border-border-light", className), style: { paddingBottom: bottomPad, minHeight: 56 }, testID: testID, children: [left.map(renderItem), _jsx(Pressable, { accessibilityLabel: centerAction.accessibilityLabel ?? "Acción", accessibilityRole: "button", className: "items-center justify-center bg-primary rounded-full", onPress: centerAction.onPress, style: {
26
- width: 52,
27
- height: 52,
28
- marginTop: -20,
29
- marginHorizontal: 6,
30
- ...theme.shadow.md,
31
- }, children: _jsx(Icon, { color: theme.icon.inverse, name: centerAction.icon, size: 26 }) }), right.map(renderItem)] }));
99
+ content = (_jsxs(_Fragment, { children: [items.slice(0, mid).map(renderItem), renderCenterFab(), items.slice(mid).map(renderItem)] }));
100
+ }
101
+ else {
102
+ content = items.map(renderItem);
32
103
  }
33
- return (_jsx(View, { className: cn("flex-row bg-bg-card border-t border-border-light", className), style: { paddingBottom: bottomPad, minHeight: 56 }, testID: testID, children: items.map(renderItem) }));
104
+ const justify = indicator === "pill-text" ? "justify-around" : "";
105
+ return (_jsx(View, { className: cn("flex-row items-center", justify, className), style: containerStyle, testID: testID, children: content }));
34
106
  }
35
107
  export default BottomNav;
108
+ export { BottomNav };