@graineai/inapp-react-native 0.36.0 → 0.38.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 +26 -12
- package/README.md +7 -2
- package/dist/client.d.ts +2 -0
- package/dist/design.d.ts +5 -0
- package/dist/design.js +50 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +6 -1
- package/dist/react-native/design-math.d.ts +13 -0
- package/dist/react-native/design-math.js +85 -0
- package/dist/react-native/fab.d.ts +7 -0
- package/dist/react-native/fab.js +63 -5
- package/dist/react-native/font-weights.d.ts +1 -0
- package/dist/react-native/font-weights.js +26 -0
- package/dist/react-native/fonts.d.ts +20 -0
- package/dist/react-native/fonts.js +69 -0
- package/dist/react-native/index.d.ts +15 -1
- package/dist/react-native/index.js +47 -2
- package/dist/react-native/loader.js +23 -7
- package/dist/react-native/ui.js +139 -6
- package/dist/react-native/use-design.d.ts +18 -0
- package/dist/react-native/use-design.js +84 -0
- package/dist/react-native/voice-bar.d.ts +16 -0
- package/dist/react-native/voice-bar.js +255 -0
- package/dist/react-native/voice-launcher.d.ts +2 -0
- package/dist/react-native/voice-launcher.js +11 -0
- package/dist/react-native/widget.js +171 -61
- package/dist/theme.d.ts +211 -0
- package/dist/theme.js +2009 -0
- package/package.json +1 -1
|
@@ -12,6 +12,10 @@ export { clampOffset, offsetDuringDrag, isTap, DRAG_SLOP_PX } from "./placement-
|
|
|
12
12
|
export { widgetSurface } from "../protocol.js";
|
|
13
13
|
export { resolveSurface } from "../protocol.js";
|
|
14
14
|
import { resolveSurface } from "../protocol.js";
|
|
15
|
+
import { sessionDesign } from "../design.js";
|
|
16
|
+
import { fontFamilyFor, sameValue } from "./design-math.js";
|
|
17
|
+
import { facesForDesign, faceKey } from "./fonts.js";
|
|
18
|
+
export { useGraineTheme } from "./use-design.js";
|
|
15
19
|
import { LauncherVisibilityTracker, activeRouteName, } from "./navigation.js";
|
|
16
20
|
export const PENDING_WIDGET_MS = 12000;
|
|
17
21
|
export const AWAITING_REPLY_MS = 12000;
|
|
@@ -29,7 +33,7 @@ export function useGraineClient() {
|
|
|
29
33
|
}
|
|
30
34
|
return client;
|
|
31
35
|
}
|
|
32
|
-
export function GraineProvider({ children, autoConnect = true, surface: surfaceProp, onSurfaceChange, onProactive, endOnBackground = true, navigationRef, fab, captureTaps, appVersion, friction, includeScreens, launcherDelayMs = 0, visibility, client: providedClient, ...options }) {
|
|
36
|
+
export function GraineProvider({ children, autoConnect = true, surface: surfaceProp, onSurfaceChange, onProactive, endOnBackground = true, navigationRef, fab, captureTaps, appVersion, friction, includeScreens, launcherDelayMs = 0, visibility, fonts: fontsProp, loadFont, svg: svgProp, client: providedClient, ...options }) {
|
|
33
37
|
const clientRef = useRef(null);
|
|
34
38
|
if (!clientRef.current) {
|
|
35
39
|
clientRef.current =
|
|
@@ -61,6 +65,41 @@ export function GraineProvider({ children, autoConnect = true, surface: surfaceP
|
|
|
61
65
|
return () => { listenersRef.current.delete(fn); };
|
|
62
66
|
}, []);
|
|
63
67
|
const [appearance, setAppearance] = useState(null);
|
|
68
|
+
const [design, setDesign] = useState(() => sessionDesign(client.config));
|
|
69
|
+
const adoptDesign = useCallback((cfg) => {
|
|
70
|
+
if (!cfg)
|
|
71
|
+
return;
|
|
72
|
+
const next = sessionDesign(cfg);
|
|
73
|
+
setDesign((cur) => (sameValue(cur, next) ? cur : next));
|
|
74
|
+
}, []);
|
|
75
|
+
const fontsKey = fontsProp ? JSON.stringify(fontsProp) : "";
|
|
76
|
+
const fonts = useMemo(() => (fontsKey ? JSON.parse(fontsKey) : null), [fontsKey]);
|
|
77
|
+
const [faces, setFaces] = useState({});
|
|
78
|
+
const svgKit = useMemo(() => (svgProp?.Svg && svgProp?.Path ? { Svg: svgProp.Svg, Path: svgProp.Path } : null), [svgProp?.Svg, svgProp?.Path]);
|
|
79
|
+
const askedRef = useRef(new Set());
|
|
80
|
+
const loadFontRef = useRef(loadFont);
|
|
81
|
+
loadFontRef.current = loadFont;
|
|
82
|
+
const hasLoader = typeof loadFont === "function";
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
const load = loadFontRef.current;
|
|
85
|
+
if (typeof load !== "function")
|
|
86
|
+
return;
|
|
87
|
+
let live = true;
|
|
88
|
+
for (const face of facesForDesign(design, client.opts.baseUrl)) {
|
|
89
|
+
const key = faceKey(face.family, face.weight);
|
|
90
|
+
if (fontFamilyFor(face.family, fonts))
|
|
91
|
+
continue;
|
|
92
|
+
if (askedRef.current.has(key))
|
|
93
|
+
continue;
|
|
94
|
+
askedRef.current.add(key);
|
|
95
|
+
Promise.resolve()
|
|
96
|
+
.then(() => load({ [face.name]: face.url }))
|
|
97
|
+
.then(() => { if (live)
|
|
98
|
+
setFaces((cur) => (cur[key] === face.name ? cur : { ...cur, [key]: face.name })); })
|
|
99
|
+
.catch(() => { });
|
|
100
|
+
}
|
|
101
|
+
return () => { live = false; askedRef.current.clear(); };
|
|
102
|
+
}, [client, design, fonts, hasLoader]);
|
|
64
103
|
const [sessionReady, setSessionReady] = useState(false);
|
|
65
104
|
const [connectionQuality, setConnectionQuality] = useState("unknown");
|
|
66
105
|
const [network, setNetwork] = useState("unknown");
|
|
@@ -170,6 +209,7 @@ export function GraineProvider({ children, autoConnect = true, surface: surfaceP
|
|
|
170
209
|
setError(null);
|
|
171
210
|
if (client.config?.appearance)
|
|
172
211
|
setAppearance(client.config.appearance);
|
|
212
|
+
adoptDesign(client.config);
|
|
173
213
|
emitEvent({ type: "conversation_started" });
|
|
174
214
|
}),
|
|
175
215
|
client.on("close", (info) => {
|
|
@@ -231,6 +271,7 @@ export function GraineProvider({ children, autoConnect = true, surface: surfaceP
|
|
|
231
271
|
const cfg = client.config ?? (await client.session());
|
|
232
272
|
if (cfg?.appearance)
|
|
233
273
|
setAppearance(cfg.appearance);
|
|
274
|
+
adoptDesign(cfg);
|
|
234
275
|
setSessionReady(true);
|
|
235
276
|
}
|
|
236
277
|
catch {
|
|
@@ -334,6 +375,10 @@ export function GraineProvider({ children, autoConnect = true, surface: surfaceP
|
|
|
334
375
|
launcherInset,
|
|
335
376
|
currentScreen,
|
|
336
377
|
appearance,
|
|
378
|
+
design,
|
|
379
|
+
fonts,
|
|
380
|
+
faces,
|
|
381
|
+
svg: svgKit,
|
|
337
382
|
sessionReady,
|
|
338
383
|
onEvent,
|
|
339
384
|
track: (name, data) => client.track(name, data),
|
|
@@ -346,7 +391,7 @@ export function GraineProvider({ children, autoConnect = true, surface: surfaceP
|
|
|
346
391
|
network,
|
|
347
392
|
setVariables: (vars) => client.setVariables(vars),
|
|
348
393
|
}), [client, connected, connecting, error, open, messages, widgets, muted, agentSpeaking, caption,
|
|
349
|
-
launcherVisible, launcherInset, currentScreen, onEvent, appearance, sessionReady,
|
|
394
|
+
launcherVisible, launcherInset, currentScreen, onEvent, appearance, design, fonts, faces, svgKit, sessionReady,
|
|
350
395
|
surface, surfacePolicy, setSurface, connectionQuality, network]);
|
|
351
396
|
const wantsCapture = captureTaps ?? !!fab;
|
|
352
397
|
const body = fab || wantsCapture ? (_jsxs(GraineTouchCapture, { enabled: wantsCapture, children: [children, fab ? _jsx(GraineFab, { ...fab }) : null] })) : children;
|
|
@@ -3,20 +3,26 @@ import { useContext, useEffect, useRef } from "react";
|
|
|
3
3
|
import { Animated, Easing, Text, View, useColorScheme } from "react-native";
|
|
4
4
|
import { Ctx } from "./index.js";
|
|
5
5
|
import { resolveNativeAppearance, resolveWidgetPalette } from "./appearance.js";
|
|
6
|
+
import { useDesign } from "./use-design.js";
|
|
6
7
|
import { resolveLoader } from "../protocol.js";
|
|
7
8
|
export function useGraineLoader() {
|
|
8
9
|
const appearance = useContext(Ctx)?.appearance ?? null;
|
|
10
|
+
const design = useDesign();
|
|
9
11
|
const look = resolveLoader(appearance?.loader);
|
|
10
12
|
const dash = resolveNativeAppearance(appearance, { accent: "#318CE7", heading: "" });
|
|
11
|
-
return { ...look, accent: look.color ?? dash.accent };
|
|
13
|
+
return { ...look, accent: look.color ?? (design.classic ? dash.accent : design.tokens.colors.accent) };
|
|
12
14
|
}
|
|
13
|
-
function useLoop(periodMs) {
|
|
15
|
+
function useLoop(periodMs, running = true) {
|
|
14
16
|
const v = useRef(new Animated.Value(0)).current;
|
|
15
17
|
useEffect(() => {
|
|
18
|
+
if (!running) {
|
|
19
|
+
v.setValue(0);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
16
22
|
const loop = Animated.loop(Animated.timing(v, { toValue: 1, duration: periodMs, easing: Easing.linear, useNativeDriver: true }));
|
|
17
23
|
loop.start();
|
|
18
24
|
return () => loop.stop();
|
|
19
|
-
}, [v, periodMs]);
|
|
25
|
+
}, [v, periodMs, running]);
|
|
20
26
|
return v;
|
|
21
27
|
}
|
|
22
28
|
function Dots({ px, color, t }) {
|
|
@@ -56,18 +62,28 @@ function Mark({ px, color, t, logoUrl }) {
|
|
|
56
62
|
export function GraineLoader(props) {
|
|
57
63
|
const appearance = useContext(Ctx)?.appearance ?? null;
|
|
58
64
|
const scheme = useColorScheme();
|
|
65
|
+
const design = useDesign();
|
|
59
66
|
const look = resolveLoader(appearance?.loader);
|
|
60
67
|
const dash = resolveNativeAppearance(appearance, { accent: "#318CE7", heading: "" });
|
|
61
|
-
const color = props.color ?? look.color ?? dash.accent;
|
|
68
|
+
const color = props.color ?? look.color ?? (design.classic ? dash.accent : design.tokens.colors.accent);
|
|
62
69
|
const px = props.size ?? look.px;
|
|
63
70
|
const kind = props.kind ?? look.kind;
|
|
64
|
-
const palette =
|
|
65
|
-
|
|
71
|
+
const palette = design.classic
|
|
72
|
+
? resolveWidgetPalette(appearance, { scheme, accent: color })
|
|
73
|
+
: { border: design.tokens.colors.border, muted: design.tokens.colors.textMuted };
|
|
74
|
+
const t = useLoop(look.periodMs, design.classic || !design.reduceMotion);
|
|
66
75
|
const label = props.label ?? (props.moment ? look.text[props.moment] : "");
|
|
76
|
+
const labelType = design.classic
|
|
77
|
+
? { fontSize: Math.max(12, Math.round(px * 0.65)), fontWeight: "600" }
|
|
78
|
+
: {
|
|
79
|
+
fontSize: Math.max(design.tokens.font.scale.caption, Math.round(px * 0.65)),
|
|
80
|
+
fontWeight: String(design.tokens.font.weights.strong),
|
|
81
|
+
fontFamily: design.font(design.tokens.font.body),
|
|
82
|
+
};
|
|
67
83
|
const mark = kind === "pulse" ? _jsx(Pulse, { px: px, color: color, t: t }) :
|
|
68
84
|
kind === "ring" ? _jsx(Ring, { px: px, color: color, t: t, track: palette.border }) :
|
|
69
85
|
kind === "bars" ? _jsx(Bars, { px: px, color: color, t: t }) :
|
|
70
86
|
kind === "mark" ? _jsx(Mark, { px: px, color: color, t: t, logoUrl: dash.logoUrl ?? "" }) :
|
|
71
87
|
_jsx(Dots, { px: px, color: color, t: t });
|
|
72
|
-
return (_jsxs(View, { accessibilityRole: "progressbar", accessibilityLabel: label || "Loading", style: { flexDirection: props.stacked ? "column" : "row", alignItems: "center", gap: props.stacked ? 8 : 10 }, children: [mark, label ? (_jsx(Text, { style: { color: palette.muted,
|
|
88
|
+
return (_jsxs(View, { accessibilityRole: "progressbar", accessibilityLabel: label || "Loading", style: { flexDirection: props.stacked ? "column" : "row", alignItems: "center", gap: props.stacked ? 8 : 10 }, children: [mark, label ? (_jsx(Text, { style: { color: palette.muted, ...labelType }, children: label })) : null] }));
|
|
73
89
|
}
|
package/dist/react-native/ui.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
-
import { Image, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, useColorScheme, View, } from "react-native";
|
|
3
|
+
import { Animated, Image, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, useColorScheme, View, } from "react-native";
|
|
4
4
|
import { useKeyboardLift } from "./keyboard.js";
|
|
5
5
|
import { useDraggablePlacement } from "./placement.js";
|
|
6
6
|
import { useGraineAgent } from "./index.js";
|
|
7
7
|
import { GraineLoader } from "./loader.js";
|
|
8
8
|
import { resolveNativeAppearance } from "./appearance.js";
|
|
9
|
+
import { useDesign } from "./use-design.js";
|
|
10
|
+
import { letterSpacing, lineHeight, onAccentFor, shadowStyle, softFor, surfaceFill } from "./design-math.js";
|
|
11
|
+
import { normaliseHex, readableOn } from "../theme.js";
|
|
9
12
|
const DARK = {
|
|
10
13
|
surface: "#17161A",
|
|
11
14
|
border: "rgba(255,255,255,0.09)",
|
|
@@ -31,14 +34,17 @@ export function GraineAgentBar({ accent: accentProp, name: nameProp, avatarUrl:
|
|
|
31
34
|
accent: "#318CE7",
|
|
32
35
|
heading: "Assistant",
|
|
33
36
|
});
|
|
34
|
-
const
|
|
37
|
+
const look = useDesign(scheme);
|
|
38
|
+
const accent = accentProp ?? (look.classic ? dashboard.accent : look.tokens.colors.accent);
|
|
35
39
|
const name = nameProp ?? dashboard.heading;
|
|
36
40
|
const avatarUrl = avatarProp ?? dashboard.logoUrl ?? undefined;
|
|
37
41
|
const deviceScheme = useColorScheme();
|
|
38
42
|
const styles = useMemo(() => {
|
|
43
|
+
if (!look.classic)
|
|
44
|
+
return makeDesignStyles(look, accent, themeProp);
|
|
39
45
|
const base = (scheme === "auto" ? deviceScheme === "light" : scheme === "light") ? LIGHT : DARK;
|
|
40
46
|
return makeStyles(themeProp ? { ...base, ...themeProp } : base);
|
|
41
|
-
}, [scheme, deviceScheme, themeProp]);
|
|
47
|
+
}, [scheme, deviceScheme, themeProp, look, accent]);
|
|
42
48
|
const [expanded, setExpanded] = useState(surface === "chat");
|
|
43
49
|
const keyboardLift = useKeyboardLift();
|
|
44
50
|
const baseBottom = launcherInset.bottom ?? bottomInset;
|
|
@@ -51,6 +57,21 @@ export function GraineAgentBar({ accent: accentProp, name: nameProp, avatarUrl:
|
|
|
51
57
|
const [captions, setCaptions] = useState(captionsOn);
|
|
52
58
|
const [draft, setDraft] = useState("");
|
|
53
59
|
const scrollRef = useRef(null);
|
|
60
|
+
const panelIn = useRef(new Animated.Value(1)).current;
|
|
61
|
+
const lookRef = useRef(look);
|
|
62
|
+
lookRef.current = look;
|
|
63
|
+
const still = look.classic || look.durationMs <= 0;
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (!expanded || still) {
|
|
66
|
+
panelIn.setValue(1);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
panelIn.setValue(0);
|
|
70
|
+
const { durationMs, easing } = lookRef.current;
|
|
71
|
+
const a = Animated.timing(panelIn, { toValue: 1, duration: durationMs, easing, useNativeDriver: true });
|
|
72
|
+
a.start();
|
|
73
|
+
return () => a.stop();
|
|
74
|
+
}, [expanded, still, panelIn]);
|
|
54
75
|
const last = messages[messages.length - 1];
|
|
55
76
|
const waiting = connecting;
|
|
56
77
|
const waitingMoment = "connecting";
|
|
@@ -89,13 +110,16 @@ export function GraineAgentBar({ accent: accentProp, name: nameProp, avatarUrl:
|
|
|
89
110
|
right: launcherInset.right ?? 12,
|
|
90
111
|
left: launcherInset.left ?? 12,
|
|
91
112
|
},
|
|
92
|
-
], children: [expanded && (_jsxs(
|
|
113
|
+
], children: [expanded && (_jsxs(PanelFrame, { animated: !look.classic, style: look.classic ? styles.panel : [styles.panel, {
|
|
114
|
+
opacity: panelIn,
|
|
115
|
+
transform: [{ translateY: panelIn.interpolate({ inputRange: [0, 1], outputRange: [8, 0] }) }],
|
|
116
|
+
}], children: [look.classic ? null : (_jsx(View, { style: styles.header, children: _jsx(Text, { numberOfLines: 1, style: styles.headerTitle, children: name }) })), _jsxs(ScrollView, { ref: scrollRef, style: [styles.transcript, keyboardLift ? styles.transcriptLifted : null], contentContainerStyle: styles.transcriptInner, showsVerticalScrollIndicator: false, children: [messages.length === 0 && (_jsx(Text, { style: styles.empty, children: isChat
|
|
93
117
|
? "I can see what you\u2019re working on \u2014 type a question, or I\u2019ll step in if you get stuck."
|
|
94
118
|
: "I can see what you\u2019re working on \u2014 ask me anything, or I\u2019ll step in if you get stuck." })), messages.map((m, i) => (_jsx(View, { style: [
|
|
95
119
|
styles.bubble,
|
|
96
|
-
m.role === "agent" ? styles.agentBubble : [styles.customerBubble, { backgroundColor: accent }],
|
|
120
|
+
m.role === "agent" ? styles.agentBubble : [styles.customerBubble, look.classic ? { backgroundColor: accent } : null],
|
|
97
121
|
], children: _jsx(Text, { style: m.role === "agent" ? styles.agentText : styles.customerText, children: m.text }) }, i)))] }), _jsx(View, { 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 && !isChat && 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: () => { if (!drag.dragging)
|
|
98
|
-
setExpanded((v) => !v); }, style: { flex: 1 }, accessibilityLabel: expanded ? "Collapse" : "Expand", children: [_jsx(Text, { style: styles.name, children: name }), waiting ? (_jsx(GraineLoader, { moment: waitingMoment, size: 15 })) : (_jsx(Text, { style: styles.status, children: status }))] }), canSwitch ? (_jsx(Pressable, { onPress: () => setSurface(isChat ? "voice" : "chat"), hitSlop: 8, accessibilityRole: "button", accessibilityLabel: isChat ? "Switch to a call" : "Switch to chat", style: styles.round, children: _jsx(Text, { style: styles.glyph, children: isChat ? "📞" : "💬" }) })) : null, waiting ? null : (_jsxs(_Fragment, { children: [isChat ? null : (_jsxs(_Fragment, { children: [_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 ? "⌄" : "⌃" }) })] }))] })] }));
|
|
122
|
+
setExpanded((v) => !v); }, style: { flex: 1 }, accessibilityLabel: expanded ? "Collapse" : "Expand", children: [_jsx(Text, { style: styles.name, children: name }), waiting ? (_jsx(GraineLoader, { moment: waitingMoment, size: 15 })) : (_jsx(Text, { style: styles.status, children: status }))] }), canSwitch ? (_jsx(Pressable, { onPress: () => setSurface(isChat ? "voice" : "chat"), hitSlop: 8, accessibilityRole: "button", accessibilityLabel: isChat ? "Switch to a call" : "Switch to chat", style: styles.round, children: _jsx(Text, { style: styles.glyph, children: isChat ? "📞" : "💬" }) })) : null, waiting ? null : (_jsxs(_Fragment, { children: [isChat ? null : (_jsxs(_Fragment, { children: [_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, !look.classic && styles.mic, muted && styles.muted, !connected && { opacity: 0.4 }], children: _jsx(Text, { style: [styles.glyph, !look.classic && styles.glyphOnAccent, muted && (look.classic ? styles.glyphOnAccent : styles.glyphOnDanger)], 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 ? "⌄" : "⌃" }) })] }))] })] }));
|
|
99
123
|
}
|
|
100
124
|
export const GraineLauncher = GraineAgentBar;
|
|
101
125
|
const makeStyles = (t) => StyleSheet.create({
|
|
@@ -148,3 +172,112 @@ const makeStyles = (t) => StyleSheet.create({
|
|
|
148
172
|
},
|
|
149
173
|
chevron: { color: t.text, fontSize: 16, lineHeight: 18 },
|
|
150
174
|
});
|
|
175
|
+
function PanelFrame({ animated, style, children }) {
|
|
176
|
+
return animated ? _jsx(Animated.View, { style: style, children: children }) : _jsx(View, { style: style, children: children });
|
|
177
|
+
}
|
|
178
|
+
function makeDesignStyles(look, accent, over) {
|
|
179
|
+
const t = look.tokens;
|
|
180
|
+
const c = t.colors;
|
|
181
|
+
const f = t.font;
|
|
182
|
+
const r = t.shape.radius;
|
|
183
|
+
const bw = t.shape.borderWidth;
|
|
184
|
+
const pad = t.spacing.cardPadding;
|
|
185
|
+
const surface = over?.surface ?? surfaceFill(c.surface, t);
|
|
186
|
+
const border = over?.border ?? c.border;
|
|
187
|
+
const text = over?.text ?? c.text;
|
|
188
|
+
const sub = over?.sub ?? c.textMuted;
|
|
189
|
+
const chip = over?.chip ?? softFor(accent, t);
|
|
190
|
+
const onAccent = over?.onAccent ?? onAccentFor(accent, t);
|
|
191
|
+
const danger = over?.danger ?? c.danger;
|
|
192
|
+
const bodyFont = look.font(f.body);
|
|
193
|
+
const headingFont = look.font(f.heading);
|
|
194
|
+
const body = {
|
|
195
|
+
fontSize: f.scale.body,
|
|
196
|
+
lineHeight: lineHeight(f.scale.body, f.lineHeight.body),
|
|
197
|
+
fontWeight: String(f.weights.body),
|
|
198
|
+
letterSpacing: letterSpacing(f.scale.body, f.letterSpacing.body),
|
|
199
|
+
fontFamily: bodyFont,
|
|
200
|
+
};
|
|
201
|
+
const mode = t.bubbles === "outline" || t.bubbles === "minimal" ? t.bubbles : "filled";
|
|
202
|
+
const followsAccent = normaliseHex(c.userBubble) === normaliseHex(c.accent);
|
|
203
|
+
const ownAccent = normaliseHex(accent) === normaliseHex(c.accent);
|
|
204
|
+
const userBubble = followsAccent ? accent : c.userBubble;
|
|
205
|
+
const userText = followsAccent
|
|
206
|
+
? (over?.onAccent ?? (ownAccent ? c.userBubbleText : readableOn(accent, [c.userBubbleText, "#FFFFFF", "#101318"])))
|
|
207
|
+
: c.userBubbleText;
|
|
208
|
+
const agentBubble = mode === "filled" ? { backgroundColor: over?.chip ?? c.agentBubble }
|
|
209
|
+
: mode === "outline" ? { backgroundColor: "transparent", borderWidth: bw, borderColor: border }
|
|
210
|
+
: { backgroundColor: "transparent", paddingHorizontal: 0, paddingVertical: 2, borderRadius: 0 };
|
|
211
|
+
const customerBubble = mode === "filled" ? { backgroundColor: userBubble }
|
|
212
|
+
: mode === "outline" ? { backgroundColor: "transparent", borderWidth: bw, borderColor: accent }
|
|
213
|
+
: { backgroundColor: softFor(accent, t) };
|
|
214
|
+
return StyleSheet.create({
|
|
215
|
+
root: { position: "absolute", left: 12, right: 12, zIndex: 900 },
|
|
216
|
+
panel: {
|
|
217
|
+
backgroundColor: c.background, borderRadius: r.xl, borderWidth: bw, borderColor: border,
|
|
218
|
+
marginBottom: 8, overflow: "hidden",
|
|
219
|
+
},
|
|
220
|
+
header: {
|
|
221
|
+
backgroundColor: surface, paddingHorizontal: pad, paddingVertical: Math.round(pad * 0.75),
|
|
222
|
+
borderBottomWidth: bw, borderBottomColor: border,
|
|
223
|
+
},
|
|
224
|
+
headerTitle: {
|
|
225
|
+
color: text, fontFamily: headingFont, fontSize: f.scale.title, fontWeight: String(f.weights.title),
|
|
226
|
+
letterSpacing: letterSpacing(f.scale.title, f.letterSpacing.title), lineHeight: lineHeight(f.scale.title, f.lineHeight.title),
|
|
227
|
+
},
|
|
228
|
+
transcript: { maxHeight: 240 },
|
|
229
|
+
transcriptLifted: { maxHeight: 150 },
|
|
230
|
+
transcriptInner: { padding: pad, gap: Math.max(4, Math.round(t.spacing.gap * 0.8)) },
|
|
231
|
+
empty: { color: sub, ...body },
|
|
232
|
+
bubble: {
|
|
233
|
+
maxWidth: "88%", paddingHorizontal: pad, paddingVertical: Math.round(pad * 0.75), borderRadius: r.lg,
|
|
234
|
+
},
|
|
235
|
+
agentBubble: { alignSelf: "flex-start", borderBottomLeftRadius: r.sm, ...agentBubble },
|
|
236
|
+
customerBubble: { alignSelf: "flex-end", borderBottomRightRadius: r.sm, ...customerBubble },
|
|
237
|
+
agentText: { color: mode === "filled" ? (over?.text ?? c.agentBubbleText) : text, ...body },
|
|
238
|
+
customerText: { color: mode === "filled" ? userText : text, ...body },
|
|
239
|
+
composer: {
|
|
240
|
+
flexDirection: "row", alignItems: "center", gap: 8,
|
|
241
|
+
paddingHorizontal: Math.round((pad * 6) / 7), paddingBottom: Math.round((pad * 6) / 7),
|
|
242
|
+
},
|
|
243
|
+
input: {
|
|
244
|
+
flex: 1, backgroundColor: c.surfaceRaised, borderWidth: bw, borderColor: border, borderRadius: r.xl,
|
|
245
|
+
paddingHorizontal: 14, paddingVertical: Platform.OS === "ios" ? 10 : 7, color: text,
|
|
246
|
+
fontSize: f.scale.body, fontFamily: bodyFont,
|
|
247
|
+
},
|
|
248
|
+
send: { width: 38, height: 38, borderRadius: 19, alignItems: "center", justifyContent: "center" },
|
|
249
|
+
sendLabel: { color: onAccent, fontSize: 17, lineHeight: 20 },
|
|
250
|
+
bar: {
|
|
251
|
+
flexDirection: "row", alignItems: "center", gap: 11,
|
|
252
|
+
backgroundColor: surface, borderRadius: r.xl, borderWidth: bw, borderColor: border,
|
|
253
|
+
paddingHorizontal: 8, paddingVertical: 8,
|
|
254
|
+
...shadowStyle(t.elevation.floating),
|
|
255
|
+
},
|
|
256
|
+
avatar: { width: 38, height: 38, borderRadius: 19, alignItems: "center", justifyContent: "center" },
|
|
257
|
+
avatarImage: { width: 38, height: 38, borderRadius: 19, backgroundColor: chip },
|
|
258
|
+
captionWrap: {
|
|
259
|
+
backgroundColor: surface, borderRadius: r.lg, borderWidth: bw, borderColor: border,
|
|
260
|
+
paddingHorizontal: pad, paddingVertical: Math.round(pad * 0.8), marginBottom: 8,
|
|
261
|
+
...shadowStyle(t.elevation.card),
|
|
262
|
+
},
|
|
263
|
+
caption: { color: text, ...body, fontWeight: String(f.weights.strong) },
|
|
264
|
+
captionLive: { opacity: 0.8 },
|
|
265
|
+
glyph: { color: text, fontSize: f.scale.caption + 1, fontWeight: String(f.weights.strong) },
|
|
266
|
+
glyphOnAccent: { color: onAccent },
|
|
267
|
+
glyphOnDanger: { color: "#FFFFFF" },
|
|
268
|
+
placeholder: { color: sub },
|
|
269
|
+
mic: { backgroundColor: accent },
|
|
270
|
+
muted: { backgroundColor: danger },
|
|
271
|
+
avatarGlyph: { color: onAccent, fontSize: 17, lineHeight: 20 },
|
|
272
|
+
name: {
|
|
273
|
+
color: text, fontSize: f.scale.body, fontWeight: String(f.weights.title), fontFamily: headingFont,
|
|
274
|
+
letterSpacing: letterSpacing(f.scale.body, f.letterSpacing.title),
|
|
275
|
+
},
|
|
276
|
+
status: { color: sub, fontSize: f.scale.caption, marginTop: 1, fontFamily: bodyFont },
|
|
277
|
+
round: {
|
|
278
|
+
width: 38, height: 38, borderRadius: 19, alignItems: "center", justifyContent: "center",
|
|
279
|
+
backgroundColor: chip,
|
|
280
|
+
},
|
|
281
|
+
chevron: { color: text, fontSize: 16, lineHeight: 18 },
|
|
282
|
+
});
|
|
283
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type MotionEasing, type ResolvedDesign, type ThemeTokens } from "../theme.js";
|
|
2
|
+
export declare function useReduceMotion(): boolean;
|
|
3
|
+
export declare function easingOf(easing: MotionEasing | string, bounce: number): (t: number) => number;
|
|
4
|
+
export interface DesignLook {
|
|
5
|
+
design: ResolvedDesign;
|
|
6
|
+
tokens: ThemeTokens;
|
|
7
|
+
classic: boolean;
|
|
8
|
+
font: (family: string) => string | undefined;
|
|
9
|
+
face: (family: string, weight: number) => {
|
|
10
|
+
fontFamily?: string;
|
|
11
|
+
fontWeight: any;
|
|
12
|
+
};
|
|
13
|
+
durationMs: number;
|
|
14
|
+
reduceMotion: boolean;
|
|
15
|
+
easing: (t: number) => number;
|
|
16
|
+
}
|
|
17
|
+
export declare function useDesign(scheme?: "light" | "dark" | "auto"): DesignLook;
|
|
18
|
+
export declare function useGraineTheme(): ThemeTokens;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { useContext, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { AccessibilityInfo, Easing, useColorScheme } from "react-native";
|
|
3
|
+
import { Ctx } from "./index.js";
|
|
4
|
+
import { sessionDesign } from "../design.js";
|
|
5
|
+
import { pickTokens } from "../theme.js";
|
|
6
|
+
import { fontFamilyFor, isClassicLook } from "./design-math.js";
|
|
7
|
+
import { faceFor, faceKey, textFace } from "./fonts.js";
|
|
8
|
+
const DEFAULT_DESIGN = sessionDesign(null);
|
|
9
|
+
const NO_FACES = {};
|
|
10
|
+
const classicSeen = new WeakMap();
|
|
11
|
+
function classic(design) {
|
|
12
|
+
let v = classicSeen.get(design);
|
|
13
|
+
if (v === undefined) {
|
|
14
|
+
v = isClassicLook(design);
|
|
15
|
+
classicSeen.set(design, v);
|
|
16
|
+
}
|
|
17
|
+
return v;
|
|
18
|
+
}
|
|
19
|
+
export function useReduceMotion() {
|
|
20
|
+
const [reduce, setReduce] = useState(false);
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
let live = true;
|
|
23
|
+
const info = AccessibilityInfo;
|
|
24
|
+
try {
|
|
25
|
+
info?.isReduceMotionEnabled?.()?.then?.((v) => { if (live)
|
|
26
|
+
setReduce(!!v); }, () => { });
|
|
27
|
+
}
|
|
28
|
+
catch { }
|
|
29
|
+
let sub = null;
|
|
30
|
+
try {
|
|
31
|
+
sub = info?.addEventListener?.("reduceMotionChanged", (v) => setReduce(!!v));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
sub = null;
|
|
35
|
+
}
|
|
36
|
+
return () => { live = false; try {
|
|
37
|
+
sub?.remove?.();
|
|
38
|
+
}
|
|
39
|
+
catch { } };
|
|
40
|
+
}, []);
|
|
41
|
+
return reduce;
|
|
42
|
+
}
|
|
43
|
+
export function easingOf(easing, bounce) {
|
|
44
|
+
if (easing === "linear" || easing === "none")
|
|
45
|
+
return Easing.linear;
|
|
46
|
+
if (easing === "emphasized")
|
|
47
|
+
return Easing.bezier(0.05, 0.7, 0.1, 1);
|
|
48
|
+
if (easing === "spring")
|
|
49
|
+
return Easing.out(Easing.back(Math.max(0.5, bounce * 4)));
|
|
50
|
+
return Easing.bezier(0.2, 0, 0, 1);
|
|
51
|
+
}
|
|
52
|
+
export function useDesign(scheme) {
|
|
53
|
+
const ctx = useContext(Ctx);
|
|
54
|
+
const device = useColorScheme();
|
|
55
|
+
const reduce = useReduceMotion();
|
|
56
|
+
const design = ctx?.design ?? DEFAULT_DESIGN;
|
|
57
|
+
const fonts = ctx?.fonts ?? null;
|
|
58
|
+
const faces = ctx?.faces ?? NO_FACES;
|
|
59
|
+
return useMemo(() => {
|
|
60
|
+
const tokens = scheme === "light" ? design.light : scheme === "dark" ? design.dark : pickTokens(design, device);
|
|
61
|
+
const durationMs = reduce || tokens.motion.easing === "none" ? 0 : Math.max(0, tokens.motion.durationMs);
|
|
62
|
+
return {
|
|
63
|
+
design,
|
|
64
|
+
tokens,
|
|
65
|
+
classic: classic(design),
|
|
66
|
+
font: (family) => {
|
|
67
|
+
const own = fontFamilyFor(family, fonts);
|
|
68
|
+
if (own)
|
|
69
|
+
return own;
|
|
70
|
+
const weight = family === tokens.font.heading && family !== tokens.font.body
|
|
71
|
+
? tokens.font.weights.title : tokens.font.weights.body;
|
|
72
|
+
const f = faceFor(family, weight);
|
|
73
|
+
return f ? faces[faceKey(f.family, f.weight)] : undefined;
|
|
74
|
+
},
|
|
75
|
+
face: (family, weight) => textFace(family, weight, fonts, faces, fontFamilyFor),
|
|
76
|
+
durationMs,
|
|
77
|
+
reduceMotion: reduce,
|
|
78
|
+
easing: easingOf(tokens.motion.easing, tokens.motion.bounce),
|
|
79
|
+
};
|
|
80
|
+
}, [design, fonts, faces, device, reduce, scheme]);
|
|
81
|
+
}
|
|
82
|
+
export function useGraineTheme() {
|
|
83
|
+
return useDesign().tokens;
|
|
84
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { GraineVoiceApi } from "./voice-launcher.js";
|
|
3
|
+
export interface GraineVoiceBarProps {
|
|
4
|
+
api: GraineVoiceApi;
|
|
5
|
+
onClose?: () => void;
|
|
6
|
+
bottomInset?: number;
|
|
7
|
+
draggable?: boolean;
|
|
8
|
+
placement?: {
|
|
9
|
+
bottomOffset?: number;
|
|
10
|
+
} | null;
|
|
11
|
+
onPlacementChange?: (placement: {
|
|
12
|
+
bottomOffset: number;
|
|
13
|
+
}) => void;
|
|
14
|
+
name?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function GraineVoiceBar({ api, onClose, bottomInset, draggable, placement, onPlacementChange, name: nameProp, }: GraineVoiceBarProps): React.JSX.Element;
|