@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/INTEGRATION.md
CHANGED
|
@@ -815,9 +815,9 @@ is not happening, and the table says which.
|
|
|
815
815
|
| Wait | What it is | When it is paid | Typical |
|
|
816
816
|
|---|---|---|---|
|
|
817
817
|
| Page load | The voice engine is a page in a WebView | **Before the tap** — `GraineFab` mounts the engine on open; a custom bar mounts `GraineVoiceLauncher` at launch with `active={false}` and flips it on open (0.27.5) | ~4s, hidden |
|
|
818
|
-
| Mint + REGISTER | A 120-second SIP credential, then registration on the SBC | **Before the tap**, re-warmed as it nears expiry | ~
|
|
818
|
+
| Mint + REGISTER | A 120-second SIP credential, then registration on the SBC | **Before the tap**, re-warmed as it nears expiry and whenever the app comes back to the foreground (0.36.0) | ~1s, hidden |
|
|
819
819
|
| Microphone | The OS permission prompt, first time only | At the tap | 0 once granted |
|
|
820
|
-
| INVITE → answer | The call itself | At the tap | ~0.
|
|
820
|
+
| INVITE → answer | The call itself, on a network path prepared before the tap | At the tap | ~0.2s |
|
|
821
821
|
|
|
822
822
|
**The INVITE is bounded, not left to the browser.** WebRTC gathers network
|
|
823
823
|
candidates before dialling, and Chrome does not report gathering complete
|
package/dist/client.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ConnectionQuality, type QualitySample, type ActionHandler, type AppActionResult, type AppEvent, type ScreenContext, type ScreenField, type AgentSurface } from "./protocol.js";
|
|
2
2
|
import { type FrictionConfig } from "./friction.js";
|
|
3
|
+
import type { ResolvedDesign } from "./theme.js";
|
|
3
4
|
export declare const SUBPROTOCOL = "graine.embed.v1";
|
|
4
5
|
export interface GraineInAppOptions {
|
|
5
6
|
baseUrl: string;
|
|
@@ -17,6 +18,7 @@ export interface SessionConfig {
|
|
|
17
18
|
webcallAgentId?: string;
|
|
18
19
|
wsHost?: string;
|
|
19
20
|
appearance?: Record<string, any>;
|
|
21
|
+
design?: ResolvedDesign;
|
|
20
22
|
variables?: Record<string, string>;
|
|
21
23
|
components?: any[];
|
|
22
24
|
appActions?: Array<{
|
package/dist/design.d.ts
ADDED
package/dist/design.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { designFromAppearance, normaliseHex, resolveDesign } from "./theme.js";
|
|
2
|
+
const isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
3
|
+
const NULLABLE = {
|
|
4
|
+
"elevation.card": { y: 0, blur: 0, spread: 0, opacity: 0, color: "#000000" },
|
|
5
|
+
"elevation.floating": { y: 0, blur: 0, spread: 0, opacity: 0, color: "#000000" },
|
|
6
|
+
"elevation.hardOffset": { x: 0, y: 0 },
|
|
7
|
+
"icons.launcher": { id: "", path: "", stroke: true },
|
|
8
|
+
glass: { blur: 0, surfaceOpacity: 1 },
|
|
9
|
+
};
|
|
10
|
+
function fill(fallback, given, path) {
|
|
11
|
+
const template = NULLABLE[path];
|
|
12
|
+
if (template) {
|
|
13
|
+
if (given === null)
|
|
14
|
+
return null;
|
|
15
|
+
if (!isObject(given))
|
|
16
|
+
return fallback;
|
|
17
|
+
return fillObject(isObject(fallback) ? fallback : template, given, path);
|
|
18
|
+
}
|
|
19
|
+
if (isObject(fallback))
|
|
20
|
+
return isObject(given) ? fillObject(fallback, given, path) : fallback;
|
|
21
|
+
if (typeof fallback === "number")
|
|
22
|
+
return typeof given === "number" && Number.isFinite(given) ? given : fallback;
|
|
23
|
+
if (typeof fallback === "boolean")
|
|
24
|
+
return typeof given === "boolean" ? given : fallback;
|
|
25
|
+
if (typeof fallback === "string") {
|
|
26
|
+
if (typeof given !== "string" || !given)
|
|
27
|
+
return fallback;
|
|
28
|
+
if (/^#[0-9a-f]{6}$/i.test(fallback))
|
|
29
|
+
return normaliseHex(given) ?? fallback;
|
|
30
|
+
return given;
|
|
31
|
+
}
|
|
32
|
+
return fallback;
|
|
33
|
+
}
|
|
34
|
+
function fillObject(fallback, given, path) {
|
|
35
|
+
const out = {};
|
|
36
|
+
for (const key of Object.keys(fallback))
|
|
37
|
+
out[key] = fill(fallback[key], given[key], path ? `${path}.${key}` : key);
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
export function sessionDesign(session) {
|
|
41
|
+
const raw = session?.design;
|
|
42
|
+
if (!isObject(raw) || !isObject(raw.config))
|
|
43
|
+
return resolveDesign(designFromAppearance(session?.appearance));
|
|
44
|
+
const local = resolveDesign(raw.config);
|
|
45
|
+
return {
|
|
46
|
+
config: local.config,
|
|
47
|
+
light: fill(local.light, raw.light, ""),
|
|
48
|
+
dark: fill(local.dark, raw.dark, ""),
|
|
49
|
+
};
|
|
50
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export { FrictionDetector, resolveFriction, rulesFor, DEFAULT_FRICTION, type Fri
|
|
|
23
23
|
export { VoiceSession, liveAudioStreamAdapter, base64ToPcm16, CAPTURE_SAMPLE_RATE, type AudioAdapter, type VoiceSessionOptions, } from "./voice.js";
|
|
24
24
|
export { EchoGuard, decodeAgentAudio, decodeMuLaw, decodePcm16, base64ToBytes, type AudioFormat, type DecodedAudio, } from "./audio.js";
|
|
25
25
|
export { addMaskRule, maskDeep, maskString } from "./mask.js";
|
|
26
|
-
export { GraineProvider, useGraineAgent, useGraineSurface, useGraineConnection, useGraineVariables, useKeyboardLift, liftFor, useDraggablePlacement, clampOffset, offsetDuringDrag, isTap, DRAG_SLOP_PX, useGraineScreen, useGraineAction, useGraineVoice, useGraineIdentify, useGraineEvents, useGraineField, useGraineClient, setDefaultClient, useGraineTrack, useGraineTap, useGraineHighlight, useGraineWidget, useGraineReady, useGraineInit, type GraineEvent, type GraineProviderProps, type Turn, type Caption, } from "./react-native/index.js";
|
|
26
|
+
export { GraineProvider, useGraineAgent, useGraineSurface, useGraineConnection, useGraineVariables, useKeyboardLift, liftFor, useDraggablePlacement, clampOffset, offsetDuringDrag, isTap, DRAG_SLOP_PX, useGraineScreen, useGraineAction, useGraineVoice, useGraineIdentify, useGraineEvents, useGraineField, useGraineClient, setDefaultClient, useGraineTrack, useGraineTap, useGraineHighlight, useGraineWidget, useGraineReady, useGraineInit, useGraineTheme, type GraineEvent, type GraineProviderProps, type Turn, type Caption, } from "./react-native/index.js";
|
|
27
27
|
export { requestMicrophonePermission } from "./react-native/permissions.js";
|
|
28
28
|
export { GraineFab, GraineTouchCapture, TAP_MAX_MOVE_PX, TAP_MAX_MS, type GraineFabProps, type GraineTouchCaptureProps, } from "./react-native/fab.js";
|
|
29
29
|
export { UNNAMED_TAP, resolveWidgetImages } from "./protocol.js";
|
|
@@ -32,3 +32,5 @@ export { RtcVoiceSessionController, RtcVoiceError, type RtcVoiceState, type RtcV
|
|
|
32
32
|
export { GraineAgentBar, GraineLauncher, type GraineAgentBarProps, type GraineLauncherProps, type GraineBarTheme, } from "./react-native/ui.js";
|
|
33
33
|
export { GraineVoiceLauncher, CAPTION_HISTORY, type GraineVoiceLauncherProps, type GraineVoiceApi, } from "./react-native/voice-launcher.js";
|
|
34
34
|
export { LauncherVisibilityTracker, activeRouteName, type LauncherInset, type LauncherContinuity, type LauncherDelayPolicy, type LauncherGroup, type LauncherVisibility, type VisibilityDecision, } from "./react-native/navigation.js";
|
|
35
|
+
export { DESIGN_PRESETS, DEFAULT_DESIGN_PRESET, DESIGN_FONTS, LAUNCHER_ICONS, designPreset, normaliseDesignConfig, designFromAppearance, resolveTokens, resolveDesign, pickTokens, contrastRatio, readableOn, glyphUrl, type DesignConfig, type DesignPreset, type ThemeTokens, type ThemeColors, type ThemeShadow, type ResolvedDesign, type DesignBubbles, type LauncherShape, type MotionEasing, type IconStyle, } from "./theme.js";
|
|
36
|
+
export { sessionDesign } from "./design.js";
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ export { FrictionDetector, resolveFriction, rulesFor, DEFAULT_FRICTION, } from "
|
|
|
14
14
|
export { VoiceSession, liveAudioStreamAdapter, base64ToPcm16, CAPTURE_SAMPLE_RATE, } from "./voice.js";
|
|
15
15
|
export { EchoGuard, decodeAgentAudio, decodeMuLaw, decodePcm16, base64ToBytes, } from "./audio.js";
|
|
16
16
|
export { addMaskRule, maskDeep, maskString } from "./mask.js";
|
|
17
|
-
export { GraineProvider, useGraineAgent, useGraineSurface, useGraineConnection, useGraineVariables, useKeyboardLift, liftFor, useDraggablePlacement, clampOffset, offsetDuringDrag, isTap, DRAG_SLOP_PX, useGraineScreen, useGraineAction, useGraineVoice, useGraineIdentify, useGraineEvents, useGraineField, useGraineClient, setDefaultClient, useGraineTrack, useGraineTap, useGraineHighlight, useGraineWidget, useGraineReady, useGraineInit, } from "./react-native/index.js";
|
|
17
|
+
export { GraineProvider, useGraineAgent, useGraineSurface, useGraineConnection, useGraineVariables, useKeyboardLift, liftFor, useDraggablePlacement, clampOffset, offsetDuringDrag, isTap, DRAG_SLOP_PX, useGraineScreen, useGraineAction, useGraineVoice, useGraineIdentify, useGraineEvents, useGraineField, useGraineClient, setDefaultClient, useGraineTrack, useGraineTap, useGraineHighlight, useGraineWidget, useGraineReady, useGraineInit, useGraineTheme, } from "./react-native/index.js";
|
|
18
18
|
export { requestMicrophonePermission } from "./react-native/permissions.js";
|
|
19
19
|
export { GraineFab, GraineTouchCapture, TAP_MAX_MOVE_PX, TAP_MAX_MS, } from "./react-native/fab.js";
|
|
20
20
|
export { UNNAMED_TAP, resolveWidgetImages } from "./protocol.js";
|
|
@@ -23,3 +23,5 @@ export { RtcVoiceSessionController, RtcVoiceError, } from "./react-native/voice-
|
|
|
23
23
|
export { GraineAgentBar, GraineLauncher, } from "./react-native/ui.js";
|
|
24
24
|
export { GraineVoiceLauncher, CAPTION_HISTORY, } from "./react-native/voice-launcher.js";
|
|
25
25
|
export { LauncherVisibilityTracker, activeRouteName, } from "./react-native/navigation.js";
|
|
26
|
+
export { DESIGN_PRESETS, DEFAULT_DESIGN_PRESET, DESIGN_FONTS, LAUNCHER_ICONS, designPreset, normaliseDesignConfig, designFromAppearance, resolveTokens, resolveDesign, pickTokens, contrastRatio, readableOn, glyphUrl, } from "./theme.js";
|
|
27
|
+
export { sessionDesign } from "./design.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ResolvedDesign, type ThemeShadow, type ThemeTokens } from "../theme.js";
|
|
2
|
+
export declare function sameValue(a: unknown, b: unknown): boolean;
|
|
3
|
+
export declare function isClassicLook(design: ResolvedDesign): boolean;
|
|
4
|
+
export declare function fontFamilyFor(family: string | null | undefined, fonts: Record<string, string> | null | undefined): string | undefined;
|
|
5
|
+
export declare function alpha(hex: string, a: number): string;
|
|
6
|
+
export declare function shadowStyle(s: ThemeShadow | null | undefined): Record<string, unknown>;
|
|
7
|
+
export declare function launcherRadius(shape: string, height: number, tokens: ThemeTokens): number;
|
|
8
|
+
export declare function glyphPixels(size: number, pixelRatio: number): number;
|
|
9
|
+
export declare function onAccentFor(accent: string, tokens: ThemeTokens): string;
|
|
10
|
+
export declare function softFor(accent: string, tokens: ThemeTokens): string;
|
|
11
|
+
export declare function surfaceFill(color: string, tokens: ThemeTokens): string;
|
|
12
|
+
export declare function letterSpacing(size: number, em: number): number | undefined;
|
|
13
|
+
export declare function lineHeight(size: number, multiplier: number): number;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { DEFAULT_DESIGN_PRESET, mixHex, normaliseHex, readableOn, resolveDesign, } from "../theme.js";
|
|
2
|
+
export function sameValue(a, b) {
|
|
3
|
+
if (a === b)
|
|
4
|
+
return true;
|
|
5
|
+
if (!a || !b || typeof a !== "object" || typeof b !== "object")
|
|
6
|
+
return false;
|
|
7
|
+
if (Array.isArray(a) !== Array.isArray(b))
|
|
8
|
+
return false;
|
|
9
|
+
const ka = Object.keys(a), kb = Object.keys(b);
|
|
10
|
+
if (ka.length !== kb.length)
|
|
11
|
+
return false;
|
|
12
|
+
return ka.every((k) => sameValue(a[k], b[k]));
|
|
13
|
+
}
|
|
14
|
+
export function isClassicLook(design) {
|
|
15
|
+
const c = design.config;
|
|
16
|
+
if (c.launcherIcon !== undefined)
|
|
17
|
+
return false;
|
|
18
|
+
const legacy = { preset: DEFAULT_DESIGN_PRESET };
|
|
19
|
+
if (c.accent !== undefined)
|
|
20
|
+
legacy.accent = c.accent;
|
|
21
|
+
if (c.scheme !== undefined)
|
|
22
|
+
legacy.scheme = c.scheme;
|
|
23
|
+
if (c.roundness !== undefined)
|
|
24
|
+
legacy.roundness = c.roundness;
|
|
25
|
+
const classic = resolveDesign(legacy);
|
|
26
|
+
return sameValue(design.light, classic.light) && sameValue(design.dark, classic.dark);
|
|
27
|
+
}
|
|
28
|
+
export function fontFamilyFor(family, fonts) {
|
|
29
|
+
const want = String(family ?? "").trim();
|
|
30
|
+
if (!want || want.toLowerCase() === "system" || !fonts)
|
|
31
|
+
return undefined;
|
|
32
|
+
const hit = Object.prototype.hasOwnProperty.call(fonts, want)
|
|
33
|
+
? fonts[want]
|
|
34
|
+
: Object.entries(fonts).find(([k]) => k.toLowerCase() === want.toLowerCase())?.[1];
|
|
35
|
+
return typeof hit === "string" && hit.trim() ? hit : undefined;
|
|
36
|
+
}
|
|
37
|
+
export function alpha(hex, a) {
|
|
38
|
+
const n = parseInt((normaliseHex(hex) ?? "#000000").slice(1), 16);
|
|
39
|
+
return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${Math.round(a * 1000) / 1000})`;
|
|
40
|
+
}
|
|
41
|
+
export function shadowStyle(s) {
|
|
42
|
+
if (!s)
|
|
43
|
+
return {};
|
|
44
|
+
return {
|
|
45
|
+
shadowColor: s.color,
|
|
46
|
+
shadowOpacity: s.opacity,
|
|
47
|
+
shadowRadius: Math.round((s.blur / 2) * 10) / 10,
|
|
48
|
+
shadowOffset: { width: 0, height: s.y },
|
|
49
|
+
elevation: Math.max(1, Math.round((s.y + s.blur / 2) / 2)),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function launcherRadius(shape, height, tokens) {
|
|
53
|
+
if (shape === "squircle")
|
|
54
|
+
return Math.round(height * 0.3);
|
|
55
|
+
if (shape === "rounded-square")
|
|
56
|
+
return tokens.shape.radius.lg;
|
|
57
|
+
if (shape === "square")
|
|
58
|
+
return tokens.shape.radius.sm;
|
|
59
|
+
return height / 2;
|
|
60
|
+
}
|
|
61
|
+
export function glyphPixels(size, pixelRatio) {
|
|
62
|
+
return Math.round(size * 0.46 * (pixelRatio > 0 ? pixelRatio : 1));
|
|
63
|
+
}
|
|
64
|
+
export function onAccentFor(accent, tokens) {
|
|
65
|
+
const a = normaliseHex(accent);
|
|
66
|
+
if (!a || a === normaliseHex(tokens.colors.accent))
|
|
67
|
+
return tokens.colors.onAccent;
|
|
68
|
+
return readableOn(a, [tokens.colors.onAccent, "#FFFFFF", "#101318"]);
|
|
69
|
+
}
|
|
70
|
+
export function softFor(accent, tokens) {
|
|
71
|
+
const a = normaliseHex(accent);
|
|
72
|
+
if (!a || a === normaliseHex(tokens.colors.accent))
|
|
73
|
+
return tokens.colors.accentSoft;
|
|
74
|
+
return mixHex(tokens.colors.surface, a, tokens.scheme === "light" ? 0.12 : 0.22);
|
|
75
|
+
}
|
|
76
|
+
export function surfaceFill(color, tokens) {
|
|
77
|
+
return tokens.glass ? alpha(color, tokens.glass.surfaceOpacity) : color;
|
|
78
|
+
}
|
|
79
|
+
export function letterSpacing(size, em) {
|
|
80
|
+
const v = Math.round(size * em * 100) / 100;
|
|
81
|
+
return v ? v : undefined;
|
|
82
|
+
}
|
|
83
|
+
export function lineHeight(size, multiplier) {
|
|
84
|
+
return Math.round(size * multiplier * 10) / 10;
|
|
85
|
+
}
|
package/dist/react-native/fab.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useContext, useRef, useState } from "react";
|
|
3
|
-
import { Image, Pressable, Text, View } from "react-native";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useContext, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { Animated, Image, PixelRatio, Platform, Pressable, Text, View } from "react-native";
|
|
4
4
|
import { Ctx, useGraineAgent, useGraineClient } from "./index.js";
|
|
5
5
|
import { GraineVoiceLauncher } from "./voice-launcher.js";
|
|
6
6
|
import { GraineAgentBar } from "./ui.js";
|
|
7
7
|
import { resolveNativeAppearance } from "./appearance.js";
|
|
8
|
+
import { useDesign } from "./use-design.js";
|
|
9
|
+
import { glyphPixels, launcherRadius, onAccentFor, shadowStyle } from "./design-math.js";
|
|
10
|
+
import { glyphUrl } from "../theme.js";
|
|
8
11
|
import { UNNAMED_TAP } from "../protocol.js";
|
|
9
12
|
function AboveNativeScreens({ overlay: Overlay, children, }) {
|
|
10
13
|
if (!Overlay)
|
|
@@ -54,8 +57,9 @@ export function GraineTouchCapture({ children, enabled = true, style }) {
|
|
|
54
57
|
return (_jsx(View, { style: [{ flex: 1 }, style], onTouchStart: onTouchStart, onTouchEnd: onTouchEnd, children: children }));
|
|
55
58
|
}
|
|
56
59
|
export function GraineFab({ webView, position = "bottom-right", size = 56, accent: accentProp, label, overlay, prewarm = true, onOpen, onClose }) {
|
|
57
|
-
const { launcherVisible, launcherInset, appearance, error } = useGraineAgent();
|
|
60
|
+
const { launcherVisible, launcherInset, appearance, error, client } = useGraineAgent();
|
|
58
61
|
const [open, setOpen] = useState(false);
|
|
62
|
+
const look = useDesign();
|
|
59
63
|
const dashboard = resolveNativeAppearance(appearance, { accent: "#318CE7", heading: "Assistant", radius: 18 });
|
|
60
64
|
const accent = accentProp ?? dashboard.accent;
|
|
61
65
|
const name = dashboard.heading;
|
|
@@ -64,6 +68,9 @@ export function GraineFab({ webView, position = "bottom-right", size = 56, accen
|
|
|
64
68
|
if (error || !launcherVisible)
|
|
65
69
|
return null;
|
|
66
70
|
const side = position === "bottom-left" ? { left: launcherInset.right } : { right: launcherInset.right };
|
|
71
|
+
if (!look.classic) {
|
|
72
|
+
return (_jsx(DesignedLauncher, { look: look, size: size, accent: accentProp ?? look.tokens.colors.accent, name: name, label: label ?? `Talk to ${name}`, logoUrl: dashboard.logoUrl, origin: client.opts.baseUrl, place: { position: "absolute", bottom: launcherInset.bottom, ...side }, onPress: () => { setOpen(true); api.start(); onOpen?.(); } }));
|
|
73
|
+
}
|
|
67
74
|
return (_jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: label ?? `Talk to ${name}`, onPress: () => { setOpen(true); api.start(); onOpen?.(); }, style: ({ pressed }) => ({
|
|
68
75
|
position: "absolute", bottom: launcherInset.bottom, ...side,
|
|
69
76
|
width: size, height: size, borderRadius: size / 2, backgroundColor: dashboardAccent,
|
|
@@ -74,5 +81,50 @@ export function GraineFab({ webView, position = "bottom-right", size = 56, accen
|
|
|
74
81
|
};
|
|
75
82
|
if (!prewarm && !open)
|
|
76
83
|
return _jsx(AboveNativeScreens, { overlay: overlay, children: button({ start: () => { } }) });
|
|
77
|
-
return (_jsx(GraineVoiceLauncher, { webView: webView, onEnded: () => { setOpen(false); onClose?.(); }, children: (api) => (_jsx(AboveNativeScreens, { overlay: overlay, children: open ? _jsx(GraineAgentBar, { accent: dashboardAccent, name: name }) : button(api) })) }));
|
|
84
|
+
return (_jsx(GraineVoiceLauncher, { webView: webView, onEnded: () => { setOpen(false); onClose?.(); }, children: (api) => (_jsx(AboveNativeScreens, { overlay: overlay, children: open ? _jsx(GraineAgentBar, { accent: look.classic ? dashboardAccent : accentProp, name: name }) : button(api) })) }));
|
|
85
|
+
}
|
|
86
|
+
function DesignedLauncher({ look, size, accent, name, label, logoUrl, origin, place, onPress, }) {
|
|
87
|
+
const t = look.tokens;
|
|
88
|
+
const [failed, setFailed] = useState([]);
|
|
89
|
+
const fail = (uri) => setFailed((cur) => (cur.includes(uri) ? cur : [...cur, uri]));
|
|
90
|
+
const shown = useRef(new Animated.Value(look.durationMs > 0 ? 0 : 1)).current;
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (look.durationMs <= 0) {
|
|
93
|
+
shown.setValue(1);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const a = Animated.timing(shown, { toValue: 1, duration: look.durationMs, easing: look.easing, useNativeDriver: true });
|
|
97
|
+
a.start();
|
|
98
|
+
return () => a.stop();
|
|
99
|
+
}, []);
|
|
100
|
+
const onAccent = onAccentFor(accent, t);
|
|
101
|
+
const pill = t.shape.launcher === "pill";
|
|
102
|
+
const radius = launcherRadius(t.shape.launcher, size, t);
|
|
103
|
+
const g = Math.round(size * 0.46 * 10) / 10;
|
|
104
|
+
const glyphUri = t.icons.launcher
|
|
105
|
+
? glyphUrl(origin, t.icons.launcher.id, onAccent, glyphPixels(size, PixelRatio.get()), t.icons.stroke)
|
|
106
|
+
: null;
|
|
107
|
+
const glyph = glyphUri && !failed.includes(glyphUri) ? glyphUri : null;
|
|
108
|
+
const logo = logoUrl && !failed.includes(logoUrl) ? logoUrl : null;
|
|
109
|
+
const hard = t.elevation.hardOffset;
|
|
110
|
+
const corners = {
|
|
111
|
+
borderRadius: radius,
|
|
112
|
+
...(t.shape.launcher === "squircle" && Platform.OS === "ios" ? { borderCurve: "continuous" } : null),
|
|
113
|
+
};
|
|
114
|
+
const mark = glyph ? (_jsx(Image, { source: { uri: glyph }, style: { width: g, height: g }, onError: () => fail(glyph), accessibilityIgnoresInvertColors: true }, glyph)) : logo ? (_jsx(Image, { source: { uri: logo }, resizeMode: "contain", style: { width: g, height: g }, onError: () => fail(logo), accessibilityIgnoresInvertColors: true }, logo)) : (_jsx(Text, { style: {
|
|
115
|
+
color: onAccent, fontSize: Math.round(size * 0.46), fontWeight: String(t.font.weights.title),
|
|
116
|
+
fontFamily: look.font(t.font.heading),
|
|
117
|
+
}, children: name.trim().charAt(0).toUpperCase() || "\u2022" }));
|
|
118
|
+
return (_jsxs(Animated.View, { pointerEvents: "box-none", style: [place, { opacity: shown, transform: [{ scale: shown.interpolate({ inputRange: [0, 1], outputRange: [0.85, 1] }) }] }], children: [hard ? (_jsx(View, { pointerEvents: "none", style: { ...corners, position: "absolute", top: hard.y, left: hard.x, right: -hard.x, bottom: -hard.y, backgroundColor: t.colors.text } })) : null, _jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: label, onPress: onPress, style: ({ pressed }) => ({
|
|
119
|
+
...corners,
|
|
120
|
+
height: size, minWidth: size, width: pill ? undefined : size,
|
|
121
|
+
flexDirection: "row", alignItems: "center", justifyContent: "center",
|
|
122
|
+
gap: pill ? 8 : 0, paddingHorizontal: pill ? Math.round(size * 0.32) : 0,
|
|
123
|
+
backgroundColor: accent,
|
|
124
|
+
opacity: pressed ? 0.85 : 1,
|
|
125
|
+
...(hard ? { borderWidth: t.shape.borderWidth, borderColor: t.colors.text } : shadowStyle(t.elevation.floating)),
|
|
126
|
+
}), children: [mark, pill ? (_jsx(Text, { numberOfLines: 1, style: {
|
|
127
|
+
color: onAccent, fontSize: t.font.scale.body, fontWeight: String(t.font.weights.strong),
|
|
128
|
+
fontFamily: look.font(t.font.body),
|
|
129
|
+
}, children: name })) : null] })] }));
|
|
78
130
|
}
|
|
@@ -17,6 +17,8 @@ export type { AgentSurface, SurfacePolicy } from "../protocol.js";
|
|
|
17
17
|
import { type AgentSurface, type ConnectionQuality } from "../protocol.js";
|
|
18
18
|
export type { ConnectionQuality, QualitySample } from "../protocol.js";
|
|
19
19
|
import type { FrictionConfig } from "../friction.js";
|
|
20
|
+
import type { ResolvedDesign } from "../theme.js";
|
|
21
|
+
export { useGraineTheme } from "./use-design.js";
|
|
20
22
|
import { type LauncherInset, type LauncherVisibility } from "./navigation.js";
|
|
21
23
|
interface GraineContextValue {
|
|
22
24
|
client: GraineInAppClient;
|
|
@@ -36,6 +38,8 @@ interface GraineContextValue {
|
|
|
36
38
|
launcherInset: LauncherInset;
|
|
37
39
|
currentScreen: string | null;
|
|
38
40
|
appearance: Record<string, any> | null;
|
|
41
|
+
design: ResolvedDesign;
|
|
42
|
+
fonts: Record<string, string> | null;
|
|
39
43
|
sessionReady: boolean;
|
|
40
44
|
onEvent: (fn: (e: GraineEvent) => void) => () => void;
|
|
41
45
|
track: (name: string, data?: Record<string, unknown>) => void;
|
|
@@ -138,8 +142,9 @@ export interface GraineProviderProps extends Partial<GraineInAppOptions> {
|
|
|
138
142
|
includeScreens?: string[];
|
|
139
143
|
launcherDelayMs?: number;
|
|
140
144
|
visibility?: LauncherVisibility;
|
|
145
|
+
fonts?: Record<string, string>;
|
|
141
146
|
}
|
|
142
|
-
export declare function GraineProvider({ children, autoConnect, surface: surfaceProp, onSurfaceChange, onProactive, endOnBackground, navigationRef, fab, captureTaps, appVersion, friction, includeScreens, launcherDelayMs, visibility, client: providedClient, ...options }: GraineProviderProps): React.JSX.Element;
|
|
147
|
+
export declare function GraineProvider({ children, autoConnect, surface: surfaceProp, onSurfaceChange, onProactive, endOnBackground, navigationRef, fab, captureTaps, appVersion, friction, includeScreens, launcherDelayMs, visibility, fonts: fontsProp, client: providedClient, ...options }: GraineProviderProps): React.JSX.Element;
|
|
143
148
|
export declare function useGraineAgent(): GraineContextValue;
|
|
144
149
|
export declare function useGraineSurface(): {
|
|
145
150
|
surface: AgentSurface;
|
|
@@ -12,6 +12,9 @@ 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 { sameValue } from "./design-math.js";
|
|
17
|
+
export { useGraineTheme } from "./use-design.js";
|
|
15
18
|
import { LauncherVisibilityTracker, activeRouteName, } from "./navigation.js";
|
|
16
19
|
export const PENDING_WIDGET_MS = 12000;
|
|
17
20
|
export const AWAITING_REPLY_MS = 12000;
|
|
@@ -29,7 +32,7 @@ export function useGraineClient() {
|
|
|
29
32
|
}
|
|
30
33
|
return client;
|
|
31
34
|
}
|
|
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 }) {
|
|
35
|
+
export function GraineProvider({ children, autoConnect = true, surface: surfaceProp, onSurfaceChange, onProactive, endOnBackground = true, navigationRef, fab, captureTaps, appVersion, friction, includeScreens, launcherDelayMs = 0, visibility, fonts: fontsProp, client: providedClient, ...options }) {
|
|
33
36
|
const clientRef = useRef(null);
|
|
34
37
|
if (!clientRef.current) {
|
|
35
38
|
clientRef.current =
|
|
@@ -61,6 +64,15 @@ export function GraineProvider({ children, autoConnect = true, surface: surfaceP
|
|
|
61
64
|
return () => { listenersRef.current.delete(fn); };
|
|
62
65
|
}, []);
|
|
63
66
|
const [appearance, setAppearance] = useState(null);
|
|
67
|
+
const [design, setDesign] = useState(() => sessionDesign(client.config));
|
|
68
|
+
const adoptDesign = useCallback((cfg) => {
|
|
69
|
+
if (!cfg)
|
|
70
|
+
return;
|
|
71
|
+
const next = sessionDesign(cfg);
|
|
72
|
+
setDesign((cur) => (sameValue(cur, next) ? cur : next));
|
|
73
|
+
}, []);
|
|
74
|
+
const fontsKey = fontsProp ? JSON.stringify(fontsProp) : "";
|
|
75
|
+
const fonts = useMemo(() => (fontsKey ? JSON.parse(fontsKey) : null), [fontsKey]);
|
|
64
76
|
const [sessionReady, setSessionReady] = useState(false);
|
|
65
77
|
const [connectionQuality, setConnectionQuality] = useState("unknown");
|
|
66
78
|
const [network, setNetwork] = useState("unknown");
|
|
@@ -170,6 +182,7 @@ export function GraineProvider({ children, autoConnect = true, surface: surfaceP
|
|
|
170
182
|
setError(null);
|
|
171
183
|
if (client.config?.appearance)
|
|
172
184
|
setAppearance(client.config.appearance);
|
|
185
|
+
adoptDesign(client.config);
|
|
173
186
|
emitEvent({ type: "conversation_started" });
|
|
174
187
|
}),
|
|
175
188
|
client.on("close", (info) => {
|
|
@@ -231,6 +244,7 @@ export function GraineProvider({ children, autoConnect = true, surface: surfaceP
|
|
|
231
244
|
const cfg = client.config ?? (await client.session());
|
|
232
245
|
if (cfg?.appearance)
|
|
233
246
|
setAppearance(cfg.appearance);
|
|
247
|
+
adoptDesign(cfg);
|
|
234
248
|
setSessionReady(true);
|
|
235
249
|
}
|
|
236
250
|
catch {
|
|
@@ -334,6 +348,8 @@ export function GraineProvider({ children, autoConnect = true, surface: surfaceP
|
|
|
334
348
|
launcherInset,
|
|
335
349
|
currentScreen,
|
|
336
350
|
appearance,
|
|
351
|
+
design,
|
|
352
|
+
fonts,
|
|
337
353
|
sessionReady,
|
|
338
354
|
onEvent,
|
|
339
355
|
track: (name, data) => client.track(name, data),
|
|
@@ -346,7 +362,7 @@ export function GraineProvider({ children, autoConnect = true, surface: surfaceP
|
|
|
346
362
|
network,
|
|
347
363
|
setVariables: (vars) => client.setVariables(vars),
|
|
348
364
|
}), [client, connected, connecting, error, open, messages, widgets, muted, agentSpeaking, caption,
|
|
349
|
-
launcherVisible, launcherInset, currentScreen, onEvent, appearance, sessionReady,
|
|
365
|
+
launcherVisible, launcherInset, currentScreen, onEvent, appearance, design, fonts, sessionReady,
|
|
350
366
|
surface, surfacePolicy, setSurface, connectionQuality, network]);
|
|
351
367
|
const wantsCapture = captureTaps ?? !!fab;
|
|
352
368
|
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
|
}
|