@sincpro/mobile-ui 0.4.1 → 0.5.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.
@@ -1,6 +1,9 @@
1
1
  import type { ReactNode } from "react";
2
2
  export interface AppBarActionProps {
3
- icon: string;
3
+ /** Ionicon name. Optional when `label` is set (text-only CTA). */
4
+ icon?: string;
5
+ /** Text label → renders a pill CTA ("Editar", "Nuevo", "Guardar") instead of an icon circle. */
6
+ label?: string;
4
7
  onPress: () => void;
5
8
  badge?: string | number;
6
9
  accessibilityLabel?: string;
@@ -8,15 +11,16 @@ export interface AppBarActionProps {
8
11
  tone?: "plain" | "tinted";
9
12
  /** Render for a dark surface (translucent light button + light icon). */
10
13
  onDark?: boolean;
11
- /** Drop the contained surface → bare icon (iOS-style plain back/action). */
14
+ /** Drop the contained surface → bare icon/text (iOS-style plain back/action). */
12
15
  bare?: boolean;
13
16
  testID?: string;
14
17
  }
15
18
  /**
16
19
  * "Contained" icon-button for the AppBar: circular touch area with a subtle surface
17
20
  * (merge of Material 3 state-layer + iOS 26 contained button). Optional badge.
21
+ * With `label`, renders a text pill CTA instead (for "Editar"/"Nuevo"/"Guardar").
18
22
  */
19
- declare function AppBarAction({ icon, onPress, badge, accessibilityLabel, tone, onDark, bare, testID, }: AppBarActionProps): import("react").JSX.Element;
23
+ declare function AppBarAction({ icon, label, onPress, badge, accessibilityLabel, tone, onDark, bare, testID, }: AppBarActionProps): import("react").JSX.Element;
20
24
  export interface AppBarProps {
21
25
  title?: string;
22
26
  subtitle?: string;
@@ -12,25 +12,41 @@ const HERO_DARK_FALLBACK = "#0E1714";
12
12
  /**
13
13
  * "Contained" icon-button for the AppBar: circular touch area with a subtle surface
14
14
  * (merge of Material 3 state-layer + iOS 26 contained button). Optional badge.
15
+ * With `label`, renders a text pill CTA instead (for "Editar"/"Nuevo"/"Guardar").
15
16
  */
16
- function AppBarAction({ icon, onPress, badge, accessibilityLabel, tone = "plain", onDark = false, bare = false, testID, }) {
17
+ function AppBarAction({ icon, label, onPress, badge, accessibilityLabel, tone = "plain", onDark = false, bare = false, testID, }) {
17
18
  const bg = bare
18
19
  ? "transparent"
19
20
  : onDark
20
21
  ? "rgba(255,255,255,0.14)"
21
22
  : tone === "tinted"
22
- ? theme.bg.accent
23
+ ? theme.accent // solid brand-green squircle (FAB-style)
23
24
  : theme.bg.card;
24
- const iconColor = tone === "tinted" ? theme.primary : onDark ? theme.icon.inverse : theme.icon.primary;
25
+ const iconColor = tone === "tinted"
26
+ ? theme.text.onAccent // dark token on green surface
27
+ : onDark
28
+ ? theme.icon.inverse
29
+ : theme.icon.primary;
25
30
  // Plain (white) squircle gets a hairline so it reads as a contained button on a light bar.
26
31
  const showBorder = !bare && !onDark && tone !== "tinted";
32
+ // Labeled (text) action → a pill CTA. Distinct from the icon-only circle; use for
33
+ // primary/secondary header actions like "Editar", "Nuevo", "Guardar".
34
+ if (label) {
35
+ return (_jsxs(Pressable, { accessibilityLabel: accessibilityLabel ?? label, accessibilityRole: "button", className: "flex-row items-center justify-center gap-1.5", hitSlop: 6, onPress: onPress, style: {
36
+ height: 36,
37
+ paddingHorizontal: 14,
38
+ borderRadius: 999,
39
+ backgroundColor: bg,
40
+ ...(showBorder ? { borderWidth: 1, borderColor: theme.border.light } : null),
41
+ }, testID: testID, children: [icon ? _jsx(Icon, { color: iconColor, name: icon, size: 16 }) : null, _jsx(Typography.Text, { semibold: true, style: { color: iconColor }, variant: "bodySmall", children: label })] }));
42
+ }
27
43
  return (_jsxs(Pressable, { accessibilityLabel: accessibilityLabel, accessibilityRole: "button", className: "items-center justify-center", hitSlop: 6, onPress: onPress, style: {
28
44
  width: 40,
29
45
  height: 40,
30
46
  borderRadius: 12,
31
47
  backgroundColor: bg,
32
48
  ...(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] }));
49
+ }, testID: testID, children: [_jsx(Icon, { color: iconColor, name: icon ?? "ellipse-outline", size: bare ? 24 : 20 }), badge != null ? (_jsx(View, { className: "absolute bg-danger rounded-full min-w-[16px] h-4 px-1 items-center justify-center", style: { top: -2, right: -2, borderWidth: 2, borderColor: theme.bg.card }, children: _jsx(Typography.Text, { style: { color: theme.text.inverse, fontSize: 9, lineHeight: 11 }, children: badge }) })) : null] }));
34
50
  }
35
51
  /**
36
52
  * Flat, modern top bar (merge of Material 3 + iOS 26): tokenized surface, left-aligned
@@ -44,9 +60,11 @@ function AppBar({ title, subtitle, onBack, leading, actions, subheader, variant
44
60
  const large = variant === "large";
45
61
  const dark = tone === "dark";
46
62
  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;
63
+ // Large variant: back/leading + actions all live ON the big-title row (single cohesive
64
+ // block — no separate top toolbar that would push the title to a new line). The compact
65
+ // top toolbar is only for default/center.
66
+ const largeHasNav = large && (!!onBack || !!leading);
67
+ const showTopBar = !large;
50
68
  // Both `dark` and `gradient` are colored surfaces: no card bg, no hairline, light/dark text.
51
69
  const colored = dark || gradient;
52
70
  const onColoredText = gradient ? theme.text.onAccent : theme.text.inverse;
@@ -56,16 +74,19 @@ function AppBar({ title, subtitle, onBack, leading, actions, subheader, variant
56
74
  : dark
57
75
  ? { color: "rgba(255,255,255,0.7)" }
58
76
  : undefined;
59
- return (_jsxs(View, { className: cn(colored ? "" : "bg-bg-card", !colored && border && "border-b border-border-light", className), style: {
77
+ // Large variant on a plain surface should feel like the page itself (no card surface, no
78
+ // hairline). Compact variants (default/center) keep the card surface for detail screens.
79
+ const largePlain = large && !colored;
80
+ return (_jsxs(View, { className: cn(colored ? "" : largePlain ? "bg-bg-page" : "bg-bg-card", !colored && !largePlain && border && "border-b border-border-light", className), style: {
60
81
  paddingTop: safeArea ? insets.top : 0,
61
82
  ...(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: {
83
+ }, testID: testID, children: [gradient ? (_jsx(GradientSurface, { colors: theme.gradient.accent, padding: "none", radius: "none", style: StyleSheet.absoluteFill })) : null, showTopBar ? (_jsxs(View, { className: "flex-row items-center px-3 gap-2 min-h-[56px]", children: [_jsxs(View, { className: "flex-row items-center gap-2", style: { minWidth: 38 }, children: [onBack ? (_jsx(AppBarAction, { accessibilityLabel: "Volver", bare: backVariant === "plain", icon: "chevron-back", onDark: dark, onPress: onBack })) : null, leading] }), _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-row items-center justify-end gap-2", style: { minWidth: 38 }, children: actions })] })) : null, large && title ? (_jsxs(View, { className: cn("flex-row px-4 pt-2 pb-3", largeHasNav ? "items-center gap-2" : ""), children: [onBack ? (_jsx(AppBarAction, { accessibilityLabel: "Volver", bare: backVariant === "plain", icon: "chevron-back", onDark: dark, onPress: onBack })) : null, leading, accentBar && !largeHasNav ? (_jsx(View, { style: {
63
84
  width: 4,
64
85
  marginRight: 10,
65
86
  marginVertical: 2,
66
87
  borderRadius: 999,
67
88
  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] }));
89
+ } })) : 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] }), actions ? (_jsx(View, { className: cn("flex-row items-center justify-end gap-2 pl-2", largeHasNav ? "" : "self-center"), children: actions })) : null] })) : null, subheader ? _jsx(View, { className: "px-3 pb-3 pt-0.5", children: subheader }) : null] }));
69
90
  }
70
91
  const AppBarWithAction = Object.assign(AppBar, { Action: AppBarAction });
71
92
  export default AppBarWithAction;
@@ -10,7 +10,7 @@ import Steps from "../Navigation/Navigation.Steps";
10
10
  import TabIcon from "../Navigation/Navigation.TabIcon";
11
11
  export declare const Navigation: {
12
12
  AppBar: (({ title, subtitle, onBack, leading, actions, subheader, variant, tone, accentBar, backVariant, border, safeArea, className, testID, }: import("../Navigation/Navigation.AppBar").AppBarProps) => import("react").JSX.Element) & {
13
- Action: ({ icon, onPress, badge, accessibilityLabel, tone, onDark, bare, testID, }: import("../Navigation/Navigation.AppBar").AppBarActionProps) => import("react").JSX.Element;
13
+ Action: ({ icon, label, onPress, badge, accessibilityLabel, tone, onDark, bare, testID, }: import("../Navigation/Navigation.AppBar").AppBarActionProps) => import("react").JSX.Element;
14
14
  };
15
15
  BottomNav: typeof BottomNav;
16
16
  CollapsingHeader: typeof CollapsingHeader;
@@ -1,4 +1,4 @@
1
- import { EVariantScreenHeader } from "../widgets/ScreenHeader";
1
+ import { type SegmentedControlProps } from "../Navigation/Navigation.SegmentedControl";
2
2
  import React, { ReactNode } from "react";
3
3
  import { type ImageSourcePropType } from "react-native";
4
4
  interface FormViewProps<T> {
@@ -17,46 +17,124 @@ interface FormViewProps<T> {
17
17
  children: ReactNode;
18
18
  }
19
19
  declare function FormViewRoot<T>({ name, description, item, isLoading, isEmpty, hasError, errorMessage, readonly, onRefresh, onRetry, onBack, withContainer, children, }: FormViewProps<T>): React.JSX.Element;
20
- declare function Header({ children, }: {
21
- /** @deprecated The header is now flat (Navigation.AppBar); `variant`/`logoSource` are ignored. */
22
- variant?: EVariantScreenHeader;
23
- logoSource?: ImageSourcePropType;
20
+ interface HeaderProps {
21
+ /** AppBar display variant. `large` = big title (list/index screens). `center` = modal/detail. */
22
+ variant?: "default" | "large" | "center";
23
+ /** Surface tone. `dark` = always-dark hero. `gradient` = brand accent gradient. */
24
+ tone?: "default" | "dark" | "gradient";
25
+ /** Actions rendered in the AppBar action slot (AppBar.Action nodes). */
26
+ actions?: ReactNode;
27
+ /** Subheader content rendered below the title bar (SearchBar, SegmentedControl, etc.). */
24
28
  children?: ReactNode;
25
- }): React.JSX.Element;
29
+ /** @deprecated Pass `variant` prop instead. */
30
+ logoSource?: ImageSourcePropType;
31
+ }
32
+ declare function Header({ variant, tone, actions, children }: HeaderProps): React.JSX.Element;
26
33
  declare function HeaderActions({ children }: {
27
34
  children: ReactNode;
28
35
  }): React.JSX.Element;
36
+ /** Secondary action bar docked below the AppBar — use for Save/Cancel, filter chips, etc. */
29
37
  declare function HeaderActionsBar({ children }: {
30
38
  children: ReactNode;
31
39
  }): React.JSX.Element;
40
+ /**
41
+ * Tab-like segmented switch for the header subheader (e.g. "Detalles" / "Historial").
42
+ * Use as a child of `<FormViewV2.Header>` — it lands in the AppBar subheader slot.
43
+ */
44
+ declare function HeaderSegmented<T extends string>(props: SegmentedControlProps<T>): React.JSX.Element;
32
45
  declare function Content({ withMargin, children, }: {
33
46
  withMargin?: boolean;
34
47
  children: ReactNode;
35
48
  }): React.JSX.Element;
49
+ /**
50
+ * @deprecated Content already handles isEmpty/!item by rendering Feedback.Empty internally.
51
+ * Use `<FormViewV2.Content>` directly — this wrapper is redundant.
52
+ */
36
53
  declare function Form({ children }: {
37
54
  children: ReactNode;
38
55
  }): React.JSX.Element;
56
+ /** Layout wrapper for a set of Group cards. Content already handles isEmpty/loading states. */
39
57
  declare function Groups({ children }: {
40
58
  children: ReactNode;
41
- }): React.JSX.Element | null;
42
- declare function Group({ children, withMargin, elevation, }: {
59
+ }): React.JSX.Element;
60
+ /**
61
+ * Card-bordered input group for form fields. Use inside `Content.Groups`.
62
+ *
63
+ * Separation — by DEFAULT each Group is its own inset card (`withMargin=true`:
64
+ * margin 16/4, iOS "grouped" style), so several Groups read as separate sections.
65
+ * If you DON'T want separation, put all fields in a SINGLE Group (one continuous
66
+ * card), or pass `withMargin={false}` to render flush/edge-to-edge — e.g. when
67
+ * embedding inside your own layout.
68
+ *
69
+ * `title` renders a SectionHeader label above the card.
70
+ */
71
+ declare function Group({ children, withMargin, elevation, padding, variant, title, }: {
72
+ children: ReactNode;
73
+ /** `true` (default) = inset card with margins; `false` = flush, no margin. */
74
+ withMargin?: boolean;
75
+ /**
76
+ * Frame style. `card` (default) wraps fields in a Display.Card. `plain` drops the
77
+ * frame so the content sits directly on the page — useful when the children already
78
+ * carry their own surface (Form.Input, Display.Card, custom rows) and a wrapping card
79
+ * would just double up.
80
+ */
81
+ variant?: "card" | "plain";
82
+ /**
83
+ * [card only] Shadow. `none` (default) = flat — the inputs already carry their own card
84
+ * surface, so a group shadow only adds a "floating" double-shadow. Opt into `sm`/`md`
85
+ * for a raised group when it sits directly on the page.
86
+ */
87
+ elevation?: "none" | "sm" | "md" | "lg";
88
+ /**
89
+ * Inner padding (Display.Card scale). `md` (default) aligns the title with the labels.
90
+ * Applies to BOTH variants, so `plain` keeps the exact same content position as `card`.
91
+ */
92
+ padding?: "none" | "sm" | "md" | "lg";
93
+ /** Label rendered above the group (SectionHeader style). */
94
+ title?: string;
95
+ }): React.JSX.Element;
96
+ /**
97
+ * Settings-style section: SectionHeader label + Card(padding="none") wrapper.
98
+ * Use inside FormViewV2.Content for preference/config screens where rows are
99
+ * Display.ListItem or Display.MenuButton (not form inputs).
100
+ */
101
+ declare function Section({ title, children, withMargin, }: {
102
+ title?: string;
43
103
  children: ReactNode;
44
104
  withMargin?: boolean;
45
- elevation?: "sm" | "md" | "lg";
46
105
  }): React.JSX.Element;
47
106
  declare function Footer({ children }: {
48
107
  children: ReactNode;
49
108
  }): React.JSX.Element | null;
109
+ /**
110
+ * Screen container for a single record (detail / create / edit / settings).
111
+ * Owns safe-area, scroll (KeyboardAware), refresh, and the loading/empty/error states.
112
+ *
113
+ * API map:
114
+ * FormViewV2.Root — provider + container; pass `item`, `isLoading`, `hasError`, `onBack`…
115
+ * FormViewV2.Header — AppBar (variant: default | large | center · tone: default | dark | gradient)
116
+ * .Header child → subheader slot: <Header.Segmented> or any node (SearchBar, etc.)
117
+ * .Header.Segmented — tab-like switch in the subheader (Detalles / Historial)
118
+ * .Header.ActionsBar — SIBLING bar below the header (Guardar / Cancelar)
119
+ * .Header.Actions — inline action row (legacy helper)
120
+ * FormViewV2.Content — scroll body; renders Feedback for loading/empty/error
121
+ * .Content.Groups — layout wrapper for Group cards
122
+ * .Content.Group — bordered card of inputs (optional `title`)
123
+ * .Content.Section — SectionHeader + Card (settings/menu rows)
124
+ * FormViewV2.Footer — sticky bottom bar (primary CTA)
125
+ */
50
126
  export declare const FormViewV2: {
51
127
  Root: typeof FormViewRoot;
52
128
  Header: typeof Header & {
53
129
  Actions: typeof HeaderActions;
54
130
  ActionsBar: typeof HeaderActionsBar;
131
+ Segmented: typeof HeaderSegmented;
55
132
  };
56
133
  Content: typeof Content & {
57
134
  Form: typeof Form;
58
135
  Groups: typeof Groups;
59
136
  Group: typeof Group;
137
+ Section: typeof Section;
60
138
  };
61
139
  Footer: typeof Footer;
62
140
  };
@@ -1,7 +1,9 @@
1
- import { jsx as _jsx, Fragment as _Fragment } from "nativewind/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "nativewind/jsx-runtime";
2
+ import { Display } from "../Display";
2
3
  import { ErrorBoundary, Feedback } from "../Feedback";
3
4
  import Container from "../layouts/Container";
4
5
  import { AppBar } from "../Navigation/Navigation.AppBar";
6
+ import { SegmentedControl, } from "../Navigation/Navigation.SegmentedControl";
5
7
  import { theme } from "../theme";
6
8
  import { FormViewProvider, useFormView } from "../views/FormViewV2.context";
7
9
  import React from "react";
@@ -16,15 +18,23 @@ function FormViewRoot({ name, description, item, isLoading = false, isEmpty = fa
16
18
  : { style: { flex: 1 } };
17
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 }) }) }));
18
20
  }
19
- function Header({ children, }) {
21
+ function Header({ variant = "default", tone, actions, children }) {
20
22
  const { name, description, onBack } = useFormView();
21
- return (_jsx(AppBar, { onBack: onBack, safeArea: false, subheader: children, subtitle: description ? description : `Seleccionar ${name}`, title: name }));
23
+ return (_jsx(AppBar, { actions: actions, onBack: onBack, safeArea: false, subheader: children, subtitle: description ?? undefined, title: name, tone: tone, variant: variant }));
22
24
  }
23
25
  function HeaderActions({ children }) {
24
- return (_jsx(View, { className: "flex-row px-4 my-4 items-center justify-between flex-1 gap-2", children: children }));
26
+ return (_jsx(View, { className: "flex-row px-4 my-2 items-center justify-between flex-1 gap-2", children: children }));
25
27
  }
28
+ /** Secondary action bar docked below the AppBar — use for Save/Cancel, filter chips, etc. */
26
29
  function HeaderActionsBar({ children }) {
27
- return (_jsx(View, { className: "bg-bg-card px-4 py-3 border-b border-border-default flex-row gap-2", children: children }));
30
+ return (_jsx(View, { className: "bg-bg-card px-4 pt-1 pb-3 border-b border-border-light flex-row gap-2 items-center", children: children }));
31
+ }
32
+ /**
33
+ * Tab-like segmented switch for the header subheader (e.g. "Detalles" / "Historial").
34
+ * Use as a child of `<FormViewV2.Header>` — it lands in the AppBar subheader slot.
35
+ */
36
+ function HeaderSegmented(props) {
37
+ return _jsx(SegmentedControl, { ...props });
28
38
  }
29
39
  function Content({ withMargin = false, children, }) {
30
40
  const { name, isLoading, isEmpty, hasError, errorMessage, item, onRefresh, onRetry, onBack, showRefreshing, setShowRefreshing, } = useFormView();
@@ -42,6 +52,10 @@ function Content({ withMargin = false, children, }) {
42
52
  alignItems: isCenteredContent ? "center" : "stretch",
43
53
  }, keyboardShouldPersistTaps: "handled", refreshControl: _jsx(RefreshControl, { colors: [theme.primary], onRefresh: handleRefresh, refreshing: showRefreshing }), style: { flex: 1 }, 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 })) }));
44
54
  }
55
+ /**
56
+ * @deprecated Content already handles isEmpty/!item by rendering Feedback.Empty internally.
57
+ * Use `<FormViewV2.Content>` directly — this wrapper is redundant.
58
+ */
45
59
  function Form({ children }) {
46
60
  const { isEmpty, item } = useFormView();
47
61
  if (isEmpty || !item) {
@@ -49,37 +63,80 @@ function Form({ children }) {
49
63
  }
50
64
  return _jsx(_Fragment, { children: children });
51
65
  }
66
+ /** Layout wrapper for a set of Group cards. Content already handles isEmpty/loading states. */
52
67
  function Groups({ children }) {
53
- const { isEmpty } = useFormView();
54
- if (isEmpty) {
55
- return null;
56
- }
57
68
  return _jsx(_Fragment, { children: children });
58
69
  }
59
- function Group({ children, withMargin = true, elevation = "md", }) {
60
- const groupClass = withMargin
61
- ? "bg-bg-card mx-4 my-1 p-1 rounded-lg border border-border-light"
62
- : "bg-bg-card p-1 rounded-lg border border-border-light";
63
- return (_jsx(View, { className: groupClass, style: theme.shadow[elevation], children: children }));
70
+ /**
71
+ * Card-bordered input group for form fields. Use inside `Content.Groups`.
72
+ *
73
+ * Separation — by DEFAULT each Group is its own inset card (`withMargin=true`:
74
+ * margin 16/4, iOS "grouped" style), so several Groups read as separate sections.
75
+ * If you DON'T want separation, put all fields in a SINGLE Group (one continuous
76
+ * card), or pass `withMargin={false}` to render flush/edge-to-edge — e.g. when
77
+ * embedding inside your own layout.
78
+ *
79
+ * `title` renders a SectionHeader label above the card.
80
+ */
81
+ function Group({ children, withMargin = true, elevation = "none", padding = "md", variant = "card", title, }) {
82
+ // `plain` mirrors the card's inner padding so the content lands in the SAME position
83
+ // as `card` — only the frame (border/bg/shadow) is dropped.
84
+ const paddingClass = { none: "p-0", sm: "p-3", md: "p-4", lg: "p-6" }[padding];
85
+ const body = variant === "plain" ? (_jsx(View, { className: paddingClass, children: children })) : (_jsx(Display.Card, { elevation: elevation, padding: padding, children: children }));
86
+ // Title alignment is identical in both variants (same inset as the card's content).
87
+ const header = title ? _jsx(Display.SectionHeader, { title: title }) : null;
88
+ if (withMargin) {
89
+ return (_jsxs(View, { style: { marginHorizontal: 16, marginVertical: 6 }, children: [header, body] }));
90
+ }
91
+ return (_jsxs(_Fragment, { children: [header, body] }));
92
+ }
93
+ /**
94
+ * Settings-style section: SectionHeader label + Card(padding="none") wrapper.
95
+ * Use inside FormViewV2.Content for preference/config screens where rows are
96
+ * Display.ListItem or Display.MenuButton (not form inputs).
97
+ */
98
+ function Section({ title, children, withMargin = true, }) {
99
+ return (_jsxs(View, { style: withMargin ? { marginHorizontal: 16 } : undefined, children: [title ? _jsx(Display.SectionHeader, { title: title }) : null, _jsx(Display.Card, { padding: "none", children: children })] }));
64
100
  }
65
101
  function Footer({ children }) {
66
102
  const { isEmpty, isLoading } = useFormView();
67
103
  if (isEmpty || isLoading) {
68
104
  return null;
69
105
  }
70
- return (_jsx(View, { className: "bg-bg-card px-4 py-3 border-t border-border-default", style: theme.shadow.sm, children: children }));
106
+ // Sticky bottom bar (lives outside the scroll). No shadow — a top hairline integrates it
107
+ // with the form instead of reading as a detached floating card.
108
+ return (_jsx(View, { className: "bg-bg-card px-4 py-3 border-t border-border-light", children: children }));
71
109
  }
72
110
  // Nested Header components
73
111
  const HeaderWithSubComponents = Object.assign(Header, {
74
112
  Actions: HeaderActions,
75
113
  ActionsBar: HeaderActionsBar,
114
+ Segmented: HeaderSegmented,
76
115
  });
77
116
  // Nested Content components
78
117
  const ContentWithSubComponents = Object.assign(Content, {
79
118
  Form,
80
119
  Groups,
81
120
  Group,
121
+ Section,
82
122
  });
123
+ /**
124
+ * Screen container for a single record (detail / create / edit / settings).
125
+ * Owns safe-area, scroll (KeyboardAware), refresh, and the loading/empty/error states.
126
+ *
127
+ * API map:
128
+ * FormViewV2.Root — provider + container; pass `item`, `isLoading`, `hasError`, `onBack`…
129
+ * FormViewV2.Header — AppBar (variant: default | large | center · tone: default | dark | gradient)
130
+ * .Header child → subheader slot: <Header.Segmented> or any node (SearchBar, etc.)
131
+ * .Header.Segmented — tab-like switch in the subheader (Detalles / Historial)
132
+ * .Header.ActionsBar — SIBLING bar below the header (Guardar / Cancelar)
133
+ * .Header.Actions — inline action row (legacy helper)
134
+ * FormViewV2.Content — scroll body; renders Feedback for loading/empty/error
135
+ * .Content.Groups — layout wrapper for Group cards
136
+ * .Content.Group — bordered card of inputs (optional `title`)
137
+ * .Content.Section — SectionHeader + Card (settings/menu rows)
138
+ * FormViewV2.Footer — sticky bottom bar (primary CTA)
139
+ */
83
140
  export const FormViewV2 = {
84
141
  Root: FormViewRoot,
85
142
  Header: HeaderWithSubComponents,
@@ -1,4 +1,5 @@
1
- import { EVariantScreenHeader } from "../widgets/ScreenHeader";
1
+ import { type SearchHistoryProps } from "../Display/Display.SearchHistory";
2
+ import { type SegmentedControlProps } from "../Navigation/Navigation.SegmentedControl";
2
3
  import React, { ReactNode } from "react";
3
4
  import { StyleProp, ViewStyle } from "react-native";
4
5
  interface ListViewProps<T> {
@@ -24,17 +25,37 @@ interface ListViewProps<T> {
24
25
  children: ReactNode;
25
26
  }
26
27
  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;
27
- declare function Header({ children, }: {
28
- /** @deprecated The header is now flat (Navigation.AppBar); `variant` is ignored. */
29
- variant?: EVariantScreenHeader;
28
+ interface HeaderProps {
29
+ /** AppBar display variant. `large` = big title (list/index). `center` = modal/detail. */
30
+ variant?: "default" | "large" | "center";
31
+ /** Surface tone. `dark` = always-dark hero. `gradient` = brand accent gradient. */
32
+ tone?: "default" | "dark" | "gradient";
33
+ /** Actions rendered in the AppBar action slot (AppBar.Action nodes). */
34
+ actions?: ReactNode;
35
+ /** Subheader content rendered below the title bar (SearchBar, SegmentedControl, etc.). */
30
36
  children?: ReactNode;
31
- }): React.JSX.Element;
37
+ }
38
+ declare function Header({ variant, tone, actions, children }: HeaderProps): React.JSX.Element;
32
39
  declare function HeaderSearch({ searchQuery }: {
33
40
  searchQuery?: string;
34
41
  }): React.JSX.Element | null;
35
42
  declare function HeaderActions({ children }: {
36
43
  children: ReactNode;
37
44
  }): React.JSX.Element;
45
+ /** Secondary action bar docked below the AppBar — use for Save/Cancel, filter chips, etc. */
46
+ declare function HeaderActionsBar({ children }: {
47
+ children: ReactNode;
48
+ }): React.JSX.Element;
49
+ /**
50
+ * Tab-like segmented switch for the header subheader (e.g. "Activos" / "Archivados").
51
+ * Use as a child of `<ListViewV2.Header>` — it lands in the AppBar subheader slot.
52
+ */
53
+ declare function HeaderSegmented<T extends string>(props: SegmentedControlProps<T>): React.JSX.Element;
54
+ /**
55
+ * Recent-searches list. Render as a SIBLING of the Header (like Filters/ActionsBar) —
56
+ * typically while the search query is empty. Delegates to `Display.SearchHistory`.
57
+ */
58
+ declare function HeaderSearchHistory(props: SearchHistoryProps): React.JSX.Element;
38
59
  declare function HeaderFilters({ children }: {
39
60
  children: ReactNode;
40
61
  }): React.JSX.Element;
@@ -108,11 +129,32 @@ interface RowActionButtonProps {
108
129
  onPress?: () => void;
109
130
  }
110
131
  declare function RowActionButton({ icon, onPress }: RowActionButtonProps): React.JSX.Element;
132
+ /**
133
+ * Screen container for a collection (list / index / search results).
134
+ * Owns safe-area, the FlatList (pagination + pull-to-refresh), record count, and the
135
+ * loading/empty/error states.
136
+ *
137
+ * API map:
138
+ * ListViewV2.Root — provider + container; pass `items`, `onSearch`, `onLoadMore`, `onBack`…
139
+ * ListViewV2.Header — AppBar (variant: default | large | center · tone: default | dark | gradient)
140
+ * .Header.Search — debounced SearchBar in the subheader (wires `onSearch`)
141
+ * .Header.SearchHistory — recent-searches list (SIBLING; show while query is empty)
142
+ * .Header.Segmented — tab-like switch in the subheader (Activos / Archivados)
143
+ * .Header.Filters(.Chips/.Chip) — horizontal filter pills (SIBLING below the header)
144
+ * .Header.ActionsBar — SIBLING bar below the header (bulk select, etc.)
145
+ * ListViewV2.Content — FlatList body; `children` is an optional custom row renderer
146
+ * .Content.Row(.Avatar/.Content/.Title/.Subtitle/.Footer/.Actions/.ActionButton)
147
+ * ListViewV2.Footer — pinned footer (hidden when empty)
148
+ * ListViewV2.FloatingButton — absolute FAB overlay
149
+ */
111
150
  export declare const ListViewV2: {
112
151
  Root: typeof ListViewRoot;
113
152
  Header: typeof Header & {
114
153
  Search: typeof HeaderSearch;
154
+ SearchHistory: typeof HeaderSearchHistory;
155
+ Segmented: typeof HeaderSegmented;
115
156
  Actions: typeof HeaderActions;
157
+ ActionsBar: typeof HeaderActionsBar;
116
158
  Filters: typeof HeaderFilters & {
117
159
  Chip: typeof FilterChip;
118
160
  Chips: typeof FilterChips;
@@ -1,8 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "nativewind/jsx-runtime";
2
2
  import { Display } from "../Display";
3
+ import SearchHistory from "../Display/Display.SearchHistory";
3
4
  import { ErrorBoundary, Feedback } from "../Feedback";
4
5
  import Container from "../layouts/Container";
5
6
  import { Navigation } from "../Navigation";
7
+ import { SegmentedControl, } from "../Navigation/Navigation.SegmentedControl";
6
8
  import { theme } from "../theme";
7
9
  import { cn, tv } from "../theme/tw";
8
10
  import { Typography } from "../Typography";
@@ -20,9 +22,9 @@ function ListViewRoot({ name, description, items, isLoading = false, hasError =
20
22
  // Propagate contentWithMargin to ContentList via context
21
23
  return (_jsx(ListViewProvider, { description: description, errorMessage: errorMessage, hasError: hasError, isLoading: isLoading, items: items, loadingMore: loadingMore, name: name, onBack: onBack, onDeleteItem: onDeleteItem, onLoadMore: onLoadMore, onPressItem: onPressItem, onRefresh: onRefresh, onRetry: onRetry, onSearch: onSearch, readonly: readonly, children: _jsx(ErrorBoundary, { children: _jsx(Wrapper, { ...wrapperProps, children: children }) }) }));
22
24
  }
23
- function Header({ children, }) {
25
+ function Header({ variant = "large", tone, actions, children }) {
24
26
  const { name, description, onBack } = useListView();
25
- return (_jsx(Navigation.AppBar, { onBack: onBack, safeArea: false, subheader: children, subtitle: description ? description : `Seleccionar ${name}`, title: name }));
27
+ return (_jsx(Navigation.AppBar, { actions: actions, onBack: onBack, safeArea: false, subheader: children, subtitle: description ?? undefined, title: name, tone: tone, variant: variant }));
26
28
  }
27
29
  function HeaderSearch({ searchQuery = "" }) {
28
30
  const { name, onSearch } = useListView();
@@ -45,7 +47,25 @@ function HeaderSearch({ searchQuery = "" }) {
45
47
  return (_jsx(View, { className: "pb-2", children: _jsx(Navigation.SearchBar, { onChangeText: setQuery, placeholder: `Buscar ${name}`, value: query }) }));
46
48
  }
47
49
  function HeaderActions({ children }) {
48
- return (_jsx(View, { className: "flex-row px-4 my-3 items-center justify-between flex-1 gap-2", children: children }));
50
+ return (_jsx(View, { className: "flex-row px-4 my-2 items-center justify-between flex-1 gap-2", children: children }));
51
+ }
52
+ /** Secondary action bar docked below the AppBar — use for Save/Cancel, filter chips, etc. */
53
+ function HeaderActionsBar({ children }) {
54
+ return (_jsx(View, { className: "bg-bg-card px-4 pt-1 pb-3 border-b border-border-light flex-row gap-2 items-center", children: children }));
55
+ }
56
+ /**
57
+ * Tab-like segmented switch for the header subheader (e.g. "Activos" / "Archivados").
58
+ * Use as a child of `<ListViewV2.Header>` — it lands in the AppBar subheader slot.
59
+ */
60
+ function HeaderSegmented(props) {
61
+ return _jsx(SegmentedControl, { ...props });
62
+ }
63
+ /**
64
+ * Recent-searches list. Render as a SIBLING of the Header (like Filters/ActionsBar) —
65
+ * typically while the search query is empty. Delegates to `Display.SearchHistory`.
66
+ */
67
+ function HeaderSearchHistory(props) {
68
+ return _jsx(SearchHistory, { ...props });
49
69
  }
50
70
  function HeaderFilters({ children }) {
51
71
  return _jsx(View, { className: "px-4 py-2 items-center", children: children });
@@ -178,7 +198,10 @@ const FiltersWithSubComponents = Object.assign(HeaderFilters, {
178
198
  });
179
199
  const HeaderWithSubComponents = Object.assign(Header, {
180
200
  Search: HeaderSearch,
201
+ SearchHistory: HeaderSearchHistory,
202
+ Segmented: HeaderSegmented,
181
203
  Actions: HeaderActions,
204
+ ActionsBar: HeaderActionsBar,
182
205
  Filters: FiltersWithSubComponents,
183
206
  });
184
207
  const RowWithSubComponents = Object.assign(RowRoot, {
@@ -194,6 +217,24 @@ const ContentWithSubComponents = Object.assign(Content, {
194
217
  List: ContentList,
195
218
  Row: RowWithSubComponents,
196
219
  });
220
+ /**
221
+ * Screen container for a collection (list / index / search results).
222
+ * Owns safe-area, the FlatList (pagination + pull-to-refresh), record count, and the
223
+ * loading/empty/error states.
224
+ *
225
+ * API map:
226
+ * ListViewV2.Root — provider + container; pass `items`, `onSearch`, `onLoadMore`, `onBack`…
227
+ * ListViewV2.Header — AppBar (variant: default | large | center · tone: default | dark | gradient)
228
+ * .Header.Search — debounced SearchBar in the subheader (wires `onSearch`)
229
+ * .Header.SearchHistory — recent-searches list (SIBLING; show while query is empty)
230
+ * .Header.Segmented — tab-like switch in the subheader (Activos / Archivados)
231
+ * .Header.Filters(.Chips/.Chip) — horizontal filter pills (SIBLING below the header)
232
+ * .Header.ActionsBar — SIBLING bar below the header (bulk select, etc.)
233
+ * ListViewV2.Content — FlatList body; `children` is an optional custom row renderer
234
+ * .Content.Row(.Avatar/.Content/.Title/.Subtitle/.Footer/.Actions/.ActionButton)
235
+ * ListViewV2.Footer — pinned footer (hidden when empty)
236
+ * ListViewV2.FloatingButton — absolute FAB overlay
237
+ */
197
238
  export const ListViewV2 = {
198
239
  Root: ListViewRoot,
199
240
  Header: HeaderWithSubComponents,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sincpro/mobile-ui",
3
- "version": "0.4.1",
3
+ "version": "0.5.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",