@graineai/inapp-react-native 0.11.1 → 0.12.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/INTEGRATION.md CHANGED
@@ -318,6 +318,62 @@ part. Everything else is unchanged.
318
318
 
319
319
  ---
320
320
 
321
+ ## Making the bar yours
322
+
323
+ Three levels, and most apps stop at the first.
324
+
325
+ **1. The dashboard.** Accent, title and logo come from Appearance on the agent
326
+ and reach every surface without an app release. Set the colour in Graine and it
327
+ changes here. This is the level a brand team should be working at.
328
+
329
+ **2. Props, when the dashboard cannot express it.** Any prop passed wins over
330
+ the dashboard, so an app that needs something per-build still can.
331
+
332
+ ```tsx
333
+ <GraineAgentBar
334
+ scheme="auto" // "dark" | "light" | "auto" (default) — follows the device
335
+ accent="#318CE7" // overrides the dashboard
336
+ name="Ring"
337
+ bottomInset={110} // clear of your tab bar
338
+ captionsOn
339
+ />
340
+ ```
341
+
342
+ `scheme` defaults to `auto` rather than dark: an agent that ignores the system
343
+ setting is the one piece of the screen that looks like it came from somewhere
344
+ else. Pin it only if your app pins its own.
345
+
346
+ **3. The palette, when your brand is not one of the two.** `theme` is merged
347
+ over the scheme's colours, so pass the one you care about:
348
+
349
+ ```tsx
350
+ import type { GraineBarTheme } from "@graineai/inapp-react-native";
351
+
352
+ <GraineAgentBar theme={{ surface: "#0E1116", border: "#232A33" }} />
353
+ ```
354
+
355
+ | Key | Where it lands |
356
+ |---|---|
357
+ | `surface` | The bar, the expanded panel, the caption bubble |
358
+ | `border` | Hairline around all three |
359
+ | `text` | The agent's name, its replies, what the customer types |
360
+ | `sub` | Status line, placeholder, empty state |
361
+ | `chip` | Quiet round buttons, and the agent's message bubbles |
362
+ | `onAccent` | Text and glyphs sitting on the accent, and on `danger` |
363
+ | `danger` | Muted — the one state worth its own colour |
364
+
365
+ **4. Or draw your own.** Everything the bar does is built on the public hooks,
366
+ so an app with a design system should ignore the component entirely:
367
+
368
+ ```tsx
369
+ const { connected, muted, setMuted, messages, send, caption } = useGraineAgent();
370
+ ```
371
+
372
+ That is the expected path for anyone with a brand team, and nothing else in the
373
+ SDK depends on this component.
374
+
375
+ ---
376
+
321
377
  ## Session lifecycle
322
378
 
323
379
  One session per client, and it follows the app.
package/dist/index.d.ts CHANGED
@@ -4,6 +4,6 @@ export { VoiceSession, liveAudioStreamAdapter, base64ToPcm16, CAPTURE_SAMPLE_RAT
4
4
  export { EchoGuard, decodeAgentAudio, decodeMuLaw, decodePcm16, base64ToBytes, type AudioFormat, type DecodedAudio, } from "./audio";
5
5
  export { addMaskRule, maskDeep, maskString } from "./mask";
6
6
  export { GraineProvider, useGraineAgent, useGraineScreen, useGraineAction, useGraineVoice, useGraineIdentify, useGraineEvents, useGraineTrack, useGraineTap, type GraineEvent, type GraineProviderProps, type Turn, type Caption, } from "./react-native/index";
7
- export { GraineAgentBar, GraineLauncher, type GraineAgentBarProps, type GraineLauncherProps } from "./react-native/ui";
7
+ export { GraineAgentBar, GraineLauncher, type GraineAgentBarProps, type GraineLauncherProps, type GraineBarTheme, } from "./react-native/ui";
8
8
  export { GraineVoiceLauncher, type GraineVoiceLauncherProps, type GraineVoiceApi, } from "./react-native/voice-launcher";
9
9
  export { LauncherVisibilityTracker, activeRouteName, type LauncherInset, type LauncherContinuity, type LauncherDelayPolicy, type LauncherGroup, type LauncherVisibility, type VisibilityDecision, } from "./react-native/navigation";
package/dist/index.js CHANGED
@@ -3,6 +3,6 @@ export { VoiceSession, liveAudioStreamAdapter, base64ToPcm16, CAPTURE_SAMPLE_RAT
3
3
  export { EchoGuard, decodeAgentAudio, decodeMuLaw, decodePcm16, base64ToBytes, } from "./audio";
4
4
  export { addMaskRule, maskDeep, maskString } from "./mask";
5
5
  export { GraineProvider, useGraineAgent, useGraineScreen, useGraineAction, useGraineVoice, useGraineIdentify, useGraineEvents, useGraineTrack, useGraineTap, } from "./react-native/index";
6
- export { GraineAgentBar, GraineLauncher } from "./react-native/ui";
6
+ export { GraineAgentBar, GraineLauncher, } from "./react-native/ui";
7
7
  export { GraineVoiceLauncher, } from "./react-native/voice-launcher";
8
8
  export { LauncherVisibilityTracker, activeRouteName, } from "./react-native/navigation";
@@ -1,4 +1,13 @@
1
1
  import React from "react";
2
+ export interface GraineBarTheme {
3
+ surface: string;
4
+ border: string;
5
+ text: string;
6
+ sub: string;
7
+ chip: string;
8
+ onAccent: string;
9
+ danger: string;
10
+ }
2
11
  export interface GraineAgentBarProps {
3
12
  accent?: string;
4
13
  name?: string;
@@ -6,7 +15,9 @@ export interface GraineAgentBarProps {
6
15
  bottomInset?: number;
7
16
  hidden?: boolean;
8
17
  captionsOn?: boolean;
18
+ scheme?: "dark" | "light" | "auto";
19
+ theme?: Partial<GraineBarTheme>;
9
20
  }
10
- export declare function GraineAgentBar({ accent: accentProp, name: nameProp, avatarUrl: avatarProp, bottomInset, hidden, captionsOn, }: GraineAgentBarProps): React.JSX.Element | null;
21
+ export declare function GraineAgentBar({ accent: accentProp, name: nameProp, avatarUrl: avatarProp, bottomInset, hidden, captionsOn, scheme, theme: themeProp, }: GraineAgentBarProps): React.JSX.Element | null;
11
22
  export declare const GraineLauncher: typeof GraineAgentBar;
12
23
  export type GraineLauncherProps = GraineAgentBarProps;
@@ -1,12 +1,35 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useRef, useState } from "react";
3
- import { ActivityIndicator, Image, KeyboardAvoidingView, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, View, } from "react-native";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ import { ActivityIndicator, Image, KeyboardAvoidingView, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, useColorScheme, View, } from "react-native";
4
4
  import { useGraineAgent } from "./index";
5
- export function GraineAgentBar({ accent: accentProp, name: nameProp, avatarUrl: avatarProp, bottomInset = 96, hidden = false, captionsOn = false, }) {
5
+ const DARK = {
6
+ surface: "#17161A",
7
+ border: "rgba(255,255,255,0.09)",
8
+ text: "#F2F3F5",
9
+ sub: "#9aa0a6",
10
+ chip: "rgba(255,255,255,0.08)",
11
+ onAccent: "#FFFFFF",
12
+ danger: "#E5484D",
13
+ };
14
+ const LIGHT = {
15
+ surface: "#FFFFFF",
16
+ border: "rgba(0,0,0,0.10)",
17
+ text: "#16181C",
18
+ sub: "#61666D",
19
+ chip: "rgba(0,0,0,0.05)",
20
+ onAccent: "#FFFFFF",
21
+ danger: "#D32F2F",
22
+ };
23
+ export function GraineAgentBar({ accent: accentProp, name: nameProp, avatarUrl: avatarProp, bottomInset = 96, hidden = false, captionsOn = false, scheme = "auto", theme: themeProp, }) {
6
24
  const { connected, connecting, error, messages, send, muted, setMuted, caption, agentSpeaking, launcherVisible, launcherInset, appearance, } = useGraineAgent();
7
25
  const accent = accentProp ?? appearance?.accent ?? "#318CE7";
8
26
  const name = nameProp ?? appearance?.title ?? "Assistant";
9
27
  const avatarUrl = avatarProp ?? appearance?.logo ?? undefined;
28
+ const deviceScheme = useColorScheme();
29
+ const styles = useMemo(() => {
30
+ const base = (scheme === "auto" ? deviceScheme === "light" : scheme === "light") ? LIGHT : DARK;
31
+ return makeStyles(themeProp ? { ...base, ...themeProp } : base);
32
+ }, [scheme, deviceScheme, themeProp]);
10
33
  const [expanded, setExpanded] = useState(false);
11
34
  const [captions, setCaptions] = useState(captionsOn);
12
35
  const [draft, setDraft] = useState("");
@@ -18,7 +41,7 @@ export function GraineAgentBar({ accent: accentProp, name: nameProp, avatarUrl:
18
41
  : muted ? "muted"
19
42
  : agentSpeaking || (last?.role === "agent" && last.live) ? "speaking"
20
43
  : connected ? "listening"
21
- : "starting…";
44
+ : "offline";
22
45
  useEffect(() => {
23
46
  if (expanded)
24
47
  requestAnimationFrame(() => scrollRef.current?.scrollToEnd({ animated: true }));
@@ -46,51 +69,55 @@ export function GraineAgentBar({ accent: accentProp, name: nameProp, avatarUrl:
46
69
  ], children: [expanded && (_jsxs(View, { style: styles.panel, children: [_jsxs(ScrollView, { ref: scrollRef, style: styles.transcript, contentContainerStyle: styles.transcriptInner, showsVerticalScrollIndicator: false, children: [messages.length === 0 && (_jsx(Text, { style: styles.empty, children: "I can see what you're working on \u2014 ask me anything, or I'll step in if you get stuck." })), messages.map((m, i) => (_jsx(View, { style: [
47
70
  styles.bubble,
48
71
  m.role === "agent" ? styles.agentBubble : [styles.customerBubble, { backgroundColor: accent }],
49
- ], children: _jsx(Text, { style: m.role === "agent" ? styles.agentText : styles.customerText, children: m.text }) }, i)))] }), _jsx(KeyboardAvoidingView, { behavior: Platform.OS === "ios" ? "padding" : undefined, children: _jsxs(View, { style: styles.composer, children: [_jsx(TextInput, { style: styles.input, value: draft, onChangeText: setDraft, placeholder: "Type a message", placeholderTextColor: "#8a8f98", onSubmitEditing: submit, returnKeyType: "send", blurOnSubmit: false }), _jsx(Pressable, { onPress: submit, disabled: !draft.trim(), accessibilityLabel: "Send", style: [styles.send, { backgroundColor: accent, opacity: draft.trim() ? 1 : 0.4 }], children: _jsx(Text, { style: styles.sendLabel, children: "\u2191" }) })] }) })] })), captions && caption?.text ? (_jsx(View, { style: styles.captionWrap, children: _jsx(Text, { numberOfLines: 3, style: [styles.caption, caption.live && styles.captionLive], children: caption.text }) })) : null, _jsxs(View, { style: styles.bar, children: [avatarUrl ? (_jsx(Image, { source: { uri: avatarUrl }, style: styles.avatarImage, accessibilityIgnoresInvertColors: true })) : (_jsx(View, { style: [styles.avatar, { backgroundColor: accent }], children: _jsx(Text, { style: styles.avatarGlyph, children: "\u2726" }) })), _jsxs(Pressable, { onPress: () => setExpanded((v) => !v), style: { flex: 1 }, accessibilityLabel: expanded ? "Collapse" : "Expand", children: [_jsx(Text, { style: styles.name, children: name }), _jsx(Text, { style: styles.status, children: status })] }), connecting ? _jsx(ActivityIndicator, { size: "small", color: accent }) : null, _jsx(Pressable, { onPress: () => setCaptions((v) => !v), hitSlop: 8, accessibilityLabel: captions ? "Hide captions" : "Show captions", style: [styles.round, captions && { backgroundColor: accent }], children: _jsx(Text, { style: [styles.glyph, captions && { color: "#fff" }], children: "CC" }) }), _jsx(Pressable, { onPress: () => setMuted(!muted), disabled: !connected, hitSlop: 8, accessibilityLabel: muted ? "Unmute" : "Mute", style: [styles.round, muted && styles.muted, !connected && { opacity: 0.4 }], children: _jsx(Text, { style: [styles.glyph, muted && { color: "#fff" }], children: muted ? "🔇" : "🎙" }) }), _jsx(Pressable, { onPress: () => setExpanded((v) => !v), hitSlop: 8, accessibilityLabel: expanded ? "Collapse" : "Expand", style: styles.round, children: _jsx(Text, { style: styles.chevron, children: expanded ? "⌄" : "⌃" }) })] })] }));
72
+ ], children: _jsx(Text, { style: m.role === "agent" ? styles.agentText : styles.customerText, children: m.text }) }, i)))] }), _jsx(KeyboardAvoidingView, { behavior: Platform.OS === "ios" ? "padding" : undefined, children: _jsxs(View, { style: styles.composer, children: [_jsx(TextInput, { style: styles.input, value: draft, onChangeText: setDraft, placeholder: "Type a message", placeholderTextColor: styles.placeholder.color, onSubmitEditing: submit, returnKeyType: "send", blurOnSubmit: false }), _jsx(Pressable, { onPress: submit, disabled: !draft.trim(), accessibilityLabel: "Send", style: [styles.send, { backgroundColor: accent, opacity: draft.trim() ? 1 : 0.4 }], children: _jsx(Text, { style: styles.sendLabel, children: "\u2191" }) })] }) })] })), captions && caption?.text ? (_jsx(View, { style: styles.captionWrap, children: _jsx(Text, { numberOfLines: 3, style: [styles.caption, caption.live && styles.captionLive], children: caption.text }) })) : null, _jsxs(View, { style: styles.bar, children: [avatarUrl ? (_jsx(Image, { source: { uri: avatarUrl }, style: styles.avatarImage, accessibilityIgnoresInvertColors: true })) : (_jsx(View, { style: [styles.avatar, { backgroundColor: accent }], children: _jsx(Text, { style: styles.avatarGlyph, children: "\u2726" }) })), _jsxs(Pressable, { onPress: () => setExpanded((v) => !v), style: { flex: 1 }, accessibilityLabel: expanded ? "Collapse" : "Expand", children: [_jsx(Text, { style: styles.name, children: name }), _jsx(Text, { style: styles.status, children: status })] }), connecting ? _jsx(ActivityIndicator, { size: "small", color: accent }) : null, _jsx(Pressable, { onPress: () => setCaptions((v) => !v), hitSlop: 8, accessibilityLabel: captions ? "Hide captions" : "Show captions", style: [styles.round, captions && { backgroundColor: accent }], children: _jsx(Text, { style: [styles.glyph, captions && styles.glyphOnAccent], children: "CC" }) }), _jsx(Pressable, { onPress: () => setMuted(!muted), disabled: !connected, hitSlop: 8, accessibilityLabel: muted ? "Unmute" : "Mute", style: [styles.round, muted && styles.muted, !connected && { opacity: 0.4 }], children: _jsx(Text, { style: [styles.glyph, muted && styles.glyphOnAccent], children: muted ? "🔇" : "🎙" }) }), _jsx(Pressable, { onPress: () => setExpanded((v) => !v), hitSlop: 8, accessibilityLabel: expanded ? "Collapse" : "Expand", style: styles.round, children: _jsx(Text, { style: styles.chevron, children: expanded ? "⌄" : "⌃" }) })] })] }));
50
73
  }
51
74
  export const GraineLauncher = GraineAgentBar;
52
- const styles = StyleSheet.create({
75
+ const makeStyles = (t) => StyleSheet.create({
53
76
  root: { position: "absolute", left: 12, right: 12, zIndex: 900 },
54
77
  panel: {
55
- backgroundColor: "#17161A", borderRadius: 22, borderWidth: 1, borderColor: "rgba(255,255,255,0.09)",
78
+ backgroundColor: t.surface, borderRadius: 22, borderWidth: 1, borderColor: t.border,
56
79
  marginBottom: 8, overflow: "hidden",
57
80
  },
58
81
  transcript: { maxHeight: 240 },
59
82
  transcriptInner: { padding: 14, gap: 8 },
60
- empty: { color: "#9aa0a6", fontSize: 14, lineHeight: 21 },
83
+ empty: { color: t.sub, fontSize: 14, lineHeight: 21 },
61
84
  bubble: { maxWidth: "88%", paddingHorizontal: 12, paddingVertical: 9, borderRadius: 16 },
62
- agentBubble: { alignSelf: "flex-start", backgroundColor: "rgba(255,255,255,0.08)", borderBottomLeftRadius: 5 },
85
+ agentBubble: { alignSelf: "flex-start", backgroundColor: t.chip, borderBottomLeftRadius: 5 },
63
86
  customerBubble: { alignSelf: "flex-end", borderBottomRightRadius: 5 },
64
- agentText: { color: "#F2F3F5", fontSize: 14.5, lineHeight: 21 },
65
- customerText: { color: "#fff", fontSize: 14.5, lineHeight: 21 },
87
+ agentText: { color: t.text, fontSize: 14.5, lineHeight: 21 },
88
+ customerText: { color: t.onAccent, fontSize: 14.5, lineHeight: 21 },
66
89
  composer: { flexDirection: "row", alignItems: "center", gap: 8, paddingHorizontal: 12, paddingBottom: 12 },
67
90
  input: {
68
- flex: 1, borderWidth: 1, borderColor: "rgba(255,255,255,0.12)", borderRadius: 16,
69
- paddingHorizontal: 12, paddingVertical: Platform.OS === "ios" ? 10 : 7, color: "#F2F3F5", fontSize: 14.5,
91
+ flex: 1, borderWidth: 1, borderColor: t.border, borderRadius: 16,
92
+ paddingHorizontal: 12, paddingVertical: Platform.OS === "ios" ? 10 : 7, color: t.text, fontSize: 14.5,
70
93
  },
71
94
  send: { width: 38, height: 38, borderRadius: 19, alignItems: "center", justifyContent: "center" },
72
- sendLabel: { color: "#fff", fontSize: 17, lineHeight: 20 },
95
+ sendLabel: { color: t.onAccent, fontSize: 17, lineHeight: 20 },
73
96
  bar: {
74
97
  flexDirection: "row", alignItems: "center", gap: 11,
75
- backgroundColor: "#17161A", borderRadius: 26, borderWidth: 1, borderColor: "rgba(255,255,255,0.09)",
98
+ backgroundColor: t.surface, borderRadius: 26, borderWidth: 1, borderColor: t.border,
76
99
  paddingHorizontal: 8, paddingVertical: 8,
77
100
  shadowColor: "#000", shadowOpacity: 0.3, shadowRadius: 16, shadowOffset: { width: 0, height: 6 }, elevation: 12,
78
101
  },
79
102
  avatar: { width: 38, height: 38, borderRadius: 19, alignItems: "center", justifyContent: "center" },
80
- avatarImage: { width: 38, height: 38, borderRadius: 19, backgroundColor: "rgba(255,255,255,0.08)" },
103
+ avatarImage: { width: 38, height: 38, borderRadius: 19, backgroundColor: t.chip },
81
104
  captionWrap: {
82
- backgroundColor: "rgba(0,0,0,0.72)", borderRadius: 16, paddingHorizontal: 14, paddingVertical: 10, marginBottom: 8,
105
+ backgroundColor: t.surface, borderRadius: 18, borderWidth: 1, borderColor: t.border,
106
+ paddingHorizontal: 14, paddingVertical: 11, marginBottom: 8,
107
+ shadowColor: "#000", shadowOpacity: 0.18, shadowRadius: 12, shadowOffset: { width: 0, height: 4 }, elevation: 6,
83
108
  },
84
- caption: { color: "#fff", fontSize: 15, lineHeight: 21, fontWeight: "600" },
109
+ caption: { color: t.text, fontSize: 15, lineHeight: 21, fontWeight: "600" },
85
110
  captionLive: { opacity: 0.8 },
86
- glyph: { color: "#F2F3F5", fontSize: 13, fontWeight: "800" },
87
- muted: { backgroundColor: "#E5484D" },
88
- avatarGlyph: { color: "#fff", fontSize: 17, lineHeight: 20 },
89
- name: { color: "#F2F3F5", fontSize: 15, fontWeight: "800" },
90
- status: { color: "#9aa0a6", fontSize: 12.5, marginTop: 1 },
111
+ glyph: { color: t.text, fontSize: 13, fontWeight: "800" },
112
+ glyphOnAccent: { color: t.onAccent },
113
+ placeholder: { color: t.sub },
114
+ muted: { backgroundColor: t.danger },
115
+ avatarGlyph: { color: t.onAccent, fontSize: 17, lineHeight: 20 },
116
+ name: { color: t.text, fontSize: 15, fontWeight: "800" },
117
+ status: { color: t.sub, fontSize: 12.5, marginTop: 1 },
91
118
  round: {
92
119
  width: 38, height: 38, borderRadius: 19, alignItems: "center", justifyContent: "center",
93
- backgroundColor: "rgba(255,255,255,0.08)",
120
+ backgroundColor: t.chip,
94
121
  },
95
- chevron: { color: "#F2F3F5", fontSize: 16, lineHeight: 18 },
122
+ chevron: { color: t.text, fontSize: 16, lineHeight: 18 },
96
123
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graineai/inapp-react-native",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "description": "Graine in-app agent for React Native — an agent that sees the screen your customer is on and can act on it.",
5
5
  "license": "MIT",
6
6
  "private": false,