@graineai/inapp-react-native 0.35.0 → 0.37.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 +2 -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 +3 -1
- package/dist/index.js +3 -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.js +57 -5
- package/dist/react-native/index.d.ts +6 -1
- package/dist/react-native/index.js +18 -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 +14 -0
- package/dist/react-native/use-design.js +72 -0
- package/dist/react-native/voice-launcher.js +6 -1
- package/dist/react-native/widget.js +171 -61
- package/dist/theme.d.ts +206 -0
- package/dist/theme.js +1999 -0
- package/package.json +1 -1
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,14 @@
|
|
|
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
|
+
durationMs: number;
|
|
10
|
+
reduceMotion: boolean;
|
|
11
|
+
easing: (t: number) => number;
|
|
12
|
+
}
|
|
13
|
+
export declare function useDesign(scheme?: "light" | "dark" | "auto"): DesignLook;
|
|
14
|
+
export declare function useGraineTheme(): ThemeTokens;
|
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
const DEFAULT_DESIGN = sessionDesign(null);
|
|
8
|
+
const classicSeen = new WeakMap();
|
|
9
|
+
function classic(design) {
|
|
10
|
+
let v = classicSeen.get(design);
|
|
11
|
+
if (v === undefined) {
|
|
12
|
+
v = isClassicLook(design);
|
|
13
|
+
classicSeen.set(design, v);
|
|
14
|
+
}
|
|
15
|
+
return v;
|
|
16
|
+
}
|
|
17
|
+
export function useReduceMotion() {
|
|
18
|
+
const [reduce, setReduce] = useState(false);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
let live = true;
|
|
21
|
+
const info = AccessibilityInfo;
|
|
22
|
+
try {
|
|
23
|
+
info?.isReduceMotionEnabled?.()?.then?.((v) => { if (live)
|
|
24
|
+
setReduce(!!v); }, () => { });
|
|
25
|
+
}
|
|
26
|
+
catch { }
|
|
27
|
+
let sub = null;
|
|
28
|
+
try {
|
|
29
|
+
sub = info?.addEventListener?.("reduceMotionChanged", (v) => setReduce(!!v));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
sub = null;
|
|
33
|
+
}
|
|
34
|
+
return () => { live = false; try {
|
|
35
|
+
sub?.remove?.();
|
|
36
|
+
}
|
|
37
|
+
catch { } };
|
|
38
|
+
}, []);
|
|
39
|
+
return reduce;
|
|
40
|
+
}
|
|
41
|
+
export function easingOf(easing, bounce) {
|
|
42
|
+
if (easing === "linear" || easing === "none")
|
|
43
|
+
return Easing.linear;
|
|
44
|
+
if (easing === "emphasized")
|
|
45
|
+
return Easing.bezier(0.05, 0.7, 0.1, 1);
|
|
46
|
+
if (easing === "spring")
|
|
47
|
+
return Easing.out(Easing.back(Math.max(0.5, bounce * 4)));
|
|
48
|
+
return Easing.bezier(0.2, 0, 0, 1);
|
|
49
|
+
}
|
|
50
|
+
export function useDesign(scheme) {
|
|
51
|
+
const ctx = useContext(Ctx);
|
|
52
|
+
const device = useColorScheme();
|
|
53
|
+
const reduce = useReduceMotion();
|
|
54
|
+
const design = ctx?.design ?? DEFAULT_DESIGN;
|
|
55
|
+
const fonts = ctx?.fonts ?? null;
|
|
56
|
+
return useMemo(() => {
|
|
57
|
+
const tokens = scheme === "light" ? design.light : scheme === "dark" ? design.dark : pickTokens(design, device);
|
|
58
|
+
const durationMs = reduce || tokens.motion.easing === "none" ? 0 : Math.max(0, tokens.motion.durationMs);
|
|
59
|
+
return {
|
|
60
|
+
design,
|
|
61
|
+
tokens,
|
|
62
|
+
classic: classic(design),
|
|
63
|
+
font: (family) => fontFamilyFor(family, fonts),
|
|
64
|
+
durationMs,
|
|
65
|
+
reduceMotion: reduce,
|
|
66
|
+
easing: easingOf(tokens.motion.easing, tokens.motion.bounce),
|
|
67
|
+
};
|
|
68
|
+
}, [design, fonts, device, reduce, scheme]);
|
|
69
|
+
}
|
|
70
|
+
export function useGraineTheme() {
|
|
71
|
+
return useDesign().tokens;
|
|
72
|
+
}
|
|
@@ -21,6 +21,8 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
21
21
|
const [connecting, setConnecting] = useState(false);
|
|
22
22
|
const [muted, setMutedState] = useState(false);
|
|
23
23
|
const [ready, setReady] = useState(false);
|
|
24
|
+
const readyRef = useRef(false);
|
|
25
|
+
readyRef.current = ready;
|
|
24
26
|
const [captions, setCaptions] = useState([]);
|
|
25
27
|
const [awaitingAgent, setAwaitingAgent] = useState(false);
|
|
26
28
|
const [captionsOn, setCaptionsOn] = useState(true);
|
|
@@ -132,7 +134,8 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
132
134
|
scheduleRetry("load-cap");
|
|
133
135
|
return;
|
|
134
136
|
}
|
|
135
|
-
|
|
137
|
+
const floor = Math.min(250, limit / 4);
|
|
138
|
+
readyTimerRef.current = setTimeout(tick, Math.max(floor, Math.min(limit - since, HARD_CAP_MS - total)));
|
|
136
139
|
};
|
|
137
140
|
readyTimerRef.current = setTimeout(tick, Math.min(STALL_TIMEOUT_MS, 1000));
|
|
138
141
|
}, [scheduleRetry, STALL_TIMEOUT_MS, BOOT_TIMEOUT_MS, HARD_CAP_MS]);
|
|
@@ -462,6 +465,8 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
462
465
|
pending = null;
|
|
463
466
|
}
|
|
464
467
|
pushContext();
|
|
468
|
+
if (readyRef.current && !startedRef.current && !connectedRef.current)
|
|
469
|
+
post({ type: "graine:warm" });
|
|
465
470
|
return;
|
|
466
471
|
}
|
|
467
472
|
if (next === "background" && wasActive && !pending) {
|