@sincpro/mobile-ui 0.3.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.
- package/dist/Navigation/Navigation.AppBar.d.ts +7 -3
- package/dist/Navigation/Navigation.AppBar.js +31 -10
- package/dist/Navigation/index.d.ts +1 -1
- package/dist/views/FormViewV2.d.ts +87 -9
- package/dist/views/FormViewV2.js +72 -15
- package/dist/views/ListViewV2.d.ts +47 -5
- package/dist/views/ListViewV2.js +44 -3
- package/package.json +1 -1
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
export interface AppBarActionProps {
|
|
3
|
-
|
|
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.
|
|
23
|
+
? theme.accent // solid brand-green squircle (FAB-style)
|
|
23
24
|
: theme.bg.card;
|
|
24
|
-
const iconColor = tone === "tinted"
|
|
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
|
-
//
|
|
48
|
-
//
|
|
49
|
-
|
|
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
|
-
|
|
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,
|
|
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] }),
|
|
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 {
|
|
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
|
-
|
|
21
|
-
/**
|
|
22
|
-
variant?:
|
|
23
|
-
|
|
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
|
-
|
|
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
|
|
42
|
-
|
|
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
|
};
|
package/dist/views/FormViewV2.js
CHANGED
|
@@ -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({
|
|
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
|
|
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-
|
|
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
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
28
|
-
/**
|
|
29
|
-
variant?:
|
|
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
|
-
}
|
|
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;
|
package/dist/views/ListViewV2.js
CHANGED
|
@@ -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({
|
|
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
|
|
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-
|
|
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