@vvedantb/native-ui 0.12.0 → 0.13.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.
@@ -0,0 +1,162 @@
1
+ import { Text, View } from "react-native";
2
+ import { IconTool } from "@tabler/icons-react-native";
3
+ import { cn } from "../cn";
4
+ import { cardStyle, sizeStyle, textStyle, typeStyle } from "../controlStyles";
5
+ import { withAlpha, palette } from "../palette";
6
+ import { useLargerText } from "../theme";
7
+ import { tokens } from "../tokens";
8
+ import { useScheme } from "../useScheme";
9
+ import { CardButton } from "./card-button";
10
+ import { chatTextSize, MONO_FONT } from "./chat-text";
11
+
12
+ /** What the reader chose. `edit` is offered by `ToolDraftCard` only. */
13
+ export type ToolDecision = "approve" | "deny" | "edit";
14
+
15
+ export type ToolApprovalCardProps = {
16
+ /** The tool the model wants to run. */
17
+ toolName: string;
18
+ /** The arguments, as a short string or an object. Shown cut to `ARGS_MAX_CHARS`. */
19
+ args?: string | Record<string, unknown>;
20
+ /** Called with the choice. */
21
+ onDecide?: (decision: ToolDecision) => void;
22
+ /** True while the app sends the decision: the buttons are off and the card says so. */
23
+ pending?: boolean;
24
+ /** Turns the buttons off without the pending state. */
25
+ disabled?: boolean;
26
+ approveLabel?: string;
27
+ denyLabel?: string;
28
+ className?: string;
29
+ /** Default `tool-approval-card`. The buttons add `-approve`, `-deny` and `-edit`. */
30
+ testID?: string;
31
+ };
32
+
33
+ /** Characters of the args summary before it is cut with an ellipsis. */
34
+ export const ARGS_MAX_CHARS = 160;
35
+
36
+ /** One line summary of tool arguments, cut at `max` characters. */
37
+ export function summarizeArgs(args: string | Record<string, unknown> | undefined, max: number = ARGS_MAX_CHARS): string {
38
+ if (args == null) return "";
39
+ let text: string;
40
+ if (typeof args === "string") {
41
+ text = args;
42
+ } else {
43
+ try {
44
+ text = JSON.stringify(args) ?? "";
45
+ } catch {
46
+ text = "[unserialisable arguments]";
47
+ }
48
+ }
49
+ text = text.replace(/\s+/g, " ").trim();
50
+ return text.length > max ? `${text.slice(0, Math.max(0, max - 1))}…` : text;
51
+ }
52
+
53
+ type BaseProps = ToolApprovalCardProps & { allowEdit?: boolean; editLabel?: string; heading: string };
54
+
55
+ function ToolCardBase({
56
+ toolName,
57
+ args,
58
+ onDecide,
59
+ pending = false,
60
+ disabled = false,
61
+ approveLabel = "Approve",
62
+ denyLabel = "Deny",
63
+ editLabel = "Edit",
64
+ allowEdit = false,
65
+ heading,
66
+ className,
67
+ testID = "tool-approval-card",
68
+ }: BaseProps) {
69
+ const scheme = useScheme();
70
+ const largerText = useLargerText();
71
+ const body = chatTextSize("body", largerText);
72
+ const small = chatTextSize("small", largerText);
73
+ const caption = chatTextSize("caption", largerText);
74
+ const off = pending || disabled;
75
+ const summary = summarizeArgs(args);
76
+ return (
77
+ <View
78
+ testID={testID}
79
+ accessible={false}
80
+ accessibilityLabel={`${heading}: ${toolName}`}
81
+ className={cn("w-full gap-3 p-4", className)}
82
+ style={[
83
+ cardStyle(scheme),
84
+ { borderWidth: 1, borderColor: withAlpha(palette.primary[scheme], 0.25) },
85
+ ]}
86
+ >
87
+ <View className="flex-row items-center gap-2">
88
+ <IconTool size={largerText ? 20 : 16} color={tokens.color.muted[scheme]} />
89
+ <Text
90
+ className={cn(caption, "font-instrument-sans-semibold")}
91
+ style={[sizeStyle(caption), typeStyle("semibold"), textStyle("muted", scheme)]}
92
+ >
93
+ {pending ? "Sending decision…" : heading}
94
+ </Text>
95
+ </View>
96
+ <Text
97
+ testID={`${testID}-name`}
98
+ numberOfLines={2}
99
+ className={cn(body, "font-instrument-sans-semibold")}
100
+ style={[sizeStyle(body), typeStyle("semibold"), textStyle("foreground", scheme)]}
101
+ >
102
+ {toolName}
103
+ </Text>
104
+ {summary ? (
105
+ <Text
106
+ testID={`${testID}-args`}
107
+ numberOfLines={3}
108
+ className={small}
109
+ style={[sizeStyle(small), textStyle("muted", scheme), { fontFamily: MONO_FONT }]}
110
+ >
111
+ {summary}
112
+ </Text>
113
+ ) : null}
114
+ <View className="flex-row flex-wrap items-center justify-end gap-2" accessibilityState={{ busy: pending }}>
115
+ <CardButton
116
+ testID={`${testID}-deny`}
117
+ label={denyLabel}
118
+ accessibilityLabel={`${denyLabel} ${toolName}`}
119
+ variant="outline"
120
+ disabled={off}
121
+ onPress={() => onDecide?.("deny")}
122
+ />
123
+ {allowEdit ? (
124
+ <CardButton
125
+ testID={`${testID}-edit`}
126
+ label={editLabel}
127
+ accessibilityLabel={`${editLabel} ${toolName}`}
128
+ variant="secondary"
129
+ disabled={off}
130
+ onPress={() => onDecide?.("edit")}
131
+ />
132
+ ) : null}
133
+ <CardButton
134
+ testID={`${testID}-approve`}
135
+ label={approveLabel}
136
+ accessibilityLabel={`${approveLabel} ${toolName}`}
137
+ variant="primary"
138
+ disabled={off}
139
+ onPress={() => onDecide?.("approve")}
140
+ />
141
+ </View>
142
+ </View>
143
+ );
144
+ }
145
+
146
+ /**
147
+ * A pending tool call that needs a yes or no: the tool name, a cut summary of
148
+ * the arguments, and Deny and Approve buttons (44 point targets). With
149
+ * `pending` or `disabled` both buttons are off.
150
+ */
151
+ export function ToolApprovalCard(props: ToolApprovalCardProps) {
152
+ return <ToolCardBase {...props} heading="Approval needed" />;
153
+ }
154
+
155
+ export type ToolDraftCardProps = ToolApprovalCardProps & {
156
+ editLabel?: string;
157
+ };
158
+
159
+ /** As `ToolApprovalCard`, plus an Edit button that reports `"edit"`, for a draft the reader may change first. */
160
+ export function ToolDraftCard({ testID = "tool-draft-card", ...props }: ToolDraftCardProps) {
161
+ return <ToolCardBase {...props} testID={testID} allowEdit heading="Draft for review" />;
162
+ }
@@ -0,0 +1,12 @@
1
+ import { StyleSheet, type StyleProp, type ViewStyle } from "react-native";
2
+
3
+ /**
4
+ * True when `style` gives a view a definite height or a flex grow. A style with
5
+ * only `maxHeight`, `maxWidth` or `minHeight` does not: a `flex: 1` child of
6
+ * such a parent collapses to height 0 on Android.
7
+ */
8
+ export function hasDefiniteHeight(style: StyleProp<ViewStyle>): boolean {
9
+ const flat = StyleSheet.flatten(style);
10
+ if (flat == null) return false;
11
+ return flat.height != null || flat.flex != null || flat.flexGrow != null;
12
+ }