@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
package/INTEGRATION.md
CHANGED
|
@@ -560,8 +560,12 @@ your funnel.
|
|
|
560
560
|
|
|
561
561
|
## Captions and hanging up
|
|
562
562
|
|
|
563
|
-
`
|
|
564
|
-
|
|
563
|
+
`GraineVoiceBar` draws the caption, the microphone and the hang-up in the
|
|
564
|
+
brand's design — mount it in the render prop and you are done (**0.38.0**):
|
|
565
|
+
`{(api) => <GraineVoiceBar api={api} onClose={close} />}`.
|
|
566
|
+
|
|
567
|
+
Drawing your own instead: the render prop gives you the whole transcript and
|
|
568
|
+
the CC switch, so a caption box is a `map` and nothing else. **0.28.0.**
|
|
565
569
|
|
|
566
570
|
```tsx
|
|
567
571
|
<GraineVoiceLauncher webView={WebView} active={open} autoStart>
|
|
@@ -707,14 +711,20 @@ part. Everything else is unchanged.
|
|
|
707
711
|
|
|
708
712
|
## Making the bar yours
|
|
709
713
|
|
|
710
|
-
|
|
714
|
+
Most apps stop at the first level — and since 0.38.0 the first level is the
|
|
715
|
+
whole design.
|
|
711
716
|
|
|
712
|
-
**1. The dashboard.**
|
|
713
|
-
|
|
714
|
-
|
|
717
|
+
**1. The dashboard.** The brand chooses a design in Appearance — one of fifteen
|
|
718
|
+
philosophies, tuned for colour, corners, typeface, launcher, icons, motion and
|
|
719
|
+
bubbles — and the SDK draws it on every surface with no app release:
|
|
720
|
+
`GraineVoiceBar` for the call, `GraineAgentBar` for the chat, the launcher and
|
|
721
|
+
every component. Give the provider `loadFont={Font.loadAsync}` and
|
|
722
|
+
`svg={{ Svg, Path }}` once and the SDK loads the design's typeface and draws its
|
|
723
|
+
icons itself. This is the level a brand team should be working at.
|
|
715
724
|
|
|
716
|
-
**2. Props, when the dashboard cannot express it.**
|
|
717
|
-
the dashboard, so an app that needs something per-build
|
|
725
|
+
**2. Props, when the dashboard cannot express it.** On `GraineAgentBar`, any
|
|
726
|
+
prop passed wins over the dashboard, so an app that needs something per-build
|
|
727
|
+
still can.
|
|
718
728
|
|
|
719
729
|
```tsx
|
|
720
730
|
<GraineAgentBar
|
|
@@ -749,15 +759,16 @@ import type { GraineBarTheme } from "@graineai/inapp-react-native";
|
|
|
749
759
|
| `onAccent` | Text and glyphs sitting on the accent, and on `danger` |
|
|
750
760
|
| `danger` | Muted — the one state worth its own colour |
|
|
751
761
|
|
|
752
|
-
**4. Or draw your own.** Everything the bar does is built on the public hooks
|
|
753
|
-
|
|
762
|
+
**4. Or draw your own.** Everything the bar does is built on the public hooks —
|
|
763
|
+
but the design chosen in the dashboard then stops reaching that part of your
|
|
764
|
+
app, so match it with `useGraineTheme()`, which returns the design's tokens:
|
|
754
765
|
|
|
755
766
|
```tsx
|
|
756
767
|
const { connected, muted, setMuted, messages, send, caption } = useGraineAgent();
|
|
768
|
+
const theme = useGraineTheme(); // colours, type, corners, spacing, shadows
|
|
757
769
|
```
|
|
758
770
|
|
|
759
|
-
|
|
760
|
-
SDK depends on this component.
|
|
771
|
+
Nothing else in the SDK depends on these components.
|
|
761
772
|
|
|
762
773
|
---
|
|
763
774
|
|
|
@@ -1096,6 +1107,9 @@ told to ignore).
|
|
|
1096
1107
|
| `endOnBackground` | `boolean` | Default `true`. You want it on. |
|
|
1097
1108
|
| `appVersion` | `string` | Which build the conversation happened in. Lands on the call record. |
|
|
1098
1109
|
| `friction` | `FrictionConfig` | This app's friction rules, layered over the dashboard's. See *Friction detection*. |
|
|
1110
|
+
| `loadFont` | `(fonts) => Promise` | expo-font's `Font.loadAsync`. The SDK loads the design's typeface itself — only the faces it uses, from Graine. **0.38.0** |
|
|
1111
|
+
| `svg` | `{ Svg, Path }` | react-native-svg's components, so the SDK draws its icons in the design's style. **0.38.0** |
|
|
1112
|
+
| `fonts` | `Record<string, string>` | Families your app already bundles (dashboard name → your `fontFamily`); they win over a download. |
|
|
1099
1113
|
|
|
1100
1114
|
### `GraineInAppClient`
|
|
1101
1115
|
|
package/README.md
CHANGED
|
@@ -119,13 +119,17 @@ over the air.
|
|
|
119
119
|
|
|
120
120
|
```tsx
|
|
121
121
|
import { WebView } from "react-native-webview";
|
|
122
|
-
import
|
|
122
|
+
import * as Font from "expo-font";
|
|
123
|
+
import Svg, { Path } from "react-native-svg";
|
|
124
|
+
import { GraineProvider, GraineVoiceLauncher, GraineVoiceBar } from "@graineai/inapp-react-native";
|
|
123
125
|
|
|
124
126
|
<GraineProvider
|
|
125
127
|
baseUrl="…"
|
|
126
128
|
publishableKey="pk_live_…"
|
|
127
129
|
navigationRef={navigationRef}
|
|
128
130
|
autoConnect={false} // the launcher owns the connection
|
|
131
|
+
loadFont={Font.loadAsync} // the SDK loads the design's typeface itself
|
|
132
|
+
svg={{ Svg, Path }} // …and draws its icons in the design's style
|
|
129
133
|
>
|
|
130
134
|
<RootNavigator />
|
|
131
135
|
<GraineVoiceLauncher
|
|
@@ -133,7 +137,7 @@ import { GraineProvider, GraineVoiceLauncher, GraineAgentBar } from "@graineai/i
|
|
|
133
137
|
autoStart
|
|
134
138
|
onError={(m) => console.warn("[graine]", m)}
|
|
135
139
|
>
|
|
136
|
-
{() => <
|
|
140
|
+
{(api) => <GraineVoiceBar api={api} />}
|
|
137
141
|
</GraineVoiceLauncher>
|
|
138
142
|
</GraineProvider>
|
|
139
143
|
```
|
|
@@ -225,6 +229,7 @@ requests a permission on your behalf.
|
|
|
225
229
|
| `useGraineIdentify()` | Who the customer is — it reaches the greeting and the call record. |
|
|
226
230
|
| `useGraineHighlight()` / `useGraineWidget()` | Let the agent point at things and draw components. |
|
|
227
231
|
| `GraineVoiceLauncher` | The voice engine: WebView, microphone, WebRTC or WebSocket. |
|
|
232
|
+
| `GraineVoiceBar` | The voice bar in the brand's design, as the dashboard previews it. Mount inside `GraineVoiceLauncher`. |
|
|
228
233
|
| `fab={{ webView }}` on the provider / `GraineFab` | The floating button, mounted for you; opens the conversation, folds back on end. |
|
|
229
234
|
| `captureTaps` / `GraineTouchCapture` | Every tap tracked automatically — no per-button wrapping. |
|
|
230
235
|
| `useGraineInit()` | `{ isInitialized, error }`. |
|
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,12 +23,17 @@ 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 SvgKit, 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";
|
|
30
30
|
export { resolveNativeAppearance, type NativeAppearance, type NativeAppearanceFallback, resolveWidgetPalette, type WidgetPalette, type WidgetMode, } from "./react-native/appearance.js";
|
|
31
31
|
export { RtcVoiceSessionController, RtcVoiceError, type RtcVoiceState, type RtcVoiceErrorCode, type RtcVoiceSession, type RtcVoiceOptions, } from "./react-native/voice-rtc.js";
|
|
32
32
|
export { GraineAgentBar, GraineLauncher, type GraineAgentBarProps, type GraineLauncherProps, type GraineBarTheme, } from "./react-native/ui.js";
|
|
33
|
+
export { GraineVoiceBar, type GraineVoiceBarProps } from "./react-native/voice-bar.js";
|
|
34
|
+
export { GraineLauncherButton, type GraineLauncherButtonProps } from "./react-native/fab.js";
|
|
35
|
+
export { faceFor, facesForDesign, fontUrl, FONT_ASSET_VERSION, type FontLoader, type FontFace } from "./react-native/fonts.js";
|
|
33
36
|
export { GraineVoiceLauncher, CAPTION_HISTORY, type GraineVoiceLauncherProps, type GraineVoiceApi, } from "./react-native/voice-launcher.js";
|
|
34
37
|
export { LauncherVisibilityTracker, activeRouteName, type LauncherInset, type LauncherContinuity, type LauncherDelayPolicy, type LauncherGroup, type LauncherVisibility, type VisibilityDecision, } from "./react-native/navigation.js";
|
|
38
|
+
export { DESIGN_PRESETS, DEFAULT_DESIGN_PRESET, DESIGN_FONTS, LAUNCHER_ICONS, SDK_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, type SdkIconName, } from "./theme.js";
|
|
39
|
+
export { sessionDesign } from "./design.js";
|
package/dist/index.js
CHANGED
|
@@ -14,12 +14,17 @@ 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";
|
|
21
21
|
export { resolveNativeAppearance, resolveWidgetPalette, } from "./react-native/appearance.js";
|
|
22
22
|
export { RtcVoiceSessionController, RtcVoiceError, } from "./react-native/voice-rtc.js";
|
|
23
23
|
export { GraineAgentBar, GraineLauncher, } from "./react-native/ui.js";
|
|
24
|
+
export { GraineVoiceBar } from "./react-native/voice-bar.js";
|
|
25
|
+
export { GraineLauncherButton } from "./react-native/fab.js";
|
|
26
|
+
export { faceFor, facesForDesign, fontUrl, FONT_ASSET_VERSION } from "./react-native/fonts.js";
|
|
24
27
|
export { GraineVoiceLauncher, CAPTION_HISTORY, } from "./react-native/voice-launcher.js";
|
|
25
28
|
export { LauncherVisibilityTracker, activeRouteName, } from "./react-native/navigation.js";
|
|
29
|
+
export { DESIGN_PRESETS, DEFAULT_DESIGN_PRESET, DESIGN_FONTS, LAUNCHER_ICONS, SDK_ICONS, designPreset, normaliseDesignConfig, designFromAppearance, resolveTokens, resolveDesign, pickTokens, contrastRatio, readableOn, glyphUrl, } from "./theme.js";
|
|
30
|
+
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
|
+
}
|
|
@@ -22,3 +22,10 @@ export interface GraineFabProps {
|
|
|
22
22
|
prewarm?: boolean;
|
|
23
23
|
}
|
|
24
24
|
export declare function GraineFab({ webView, position, size, accent: accentProp, label, overlay, prewarm, onOpen, onClose }: GraineFabProps): React.JSX.Element;
|
|
25
|
+
export interface GraineLauncherButtonProps {
|
|
26
|
+
onPress: () => void;
|
|
27
|
+
style?: ViewStyle;
|
|
28
|
+
label?: string;
|
|
29
|
+
size?: number;
|
|
30
|
+
}
|
|
31
|
+
export declare function GraineLauncherButton({ onPress, style, label, size }: GraineLauncherButtonProps): React.JSX.Element;
|
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,56 @@ 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
|
+
export function GraineLauncherButton({ onPress, style, label, size = 56 }) {
|
|
87
|
+
const { appearance, client } = useGraineAgent();
|
|
88
|
+
const look = useDesign();
|
|
89
|
+
const dashboard = resolveNativeAppearance(appearance, { accent: look.tokens.colors.accent, heading: "Assistant" });
|
|
90
|
+
return (_jsx(DesignedLauncher, { look: look, size: size, accent: look.tokens.colors.accent, name: dashboard.heading, label: label ?? `Talk to ${dashboard.heading}`, logoUrl: dashboard.logoUrl, origin: client.opts.baseUrl, place: style ?? {}, onPress: onPress }));
|
|
91
|
+
}
|
|
92
|
+
function DesignedLauncher({ look, size, accent, name, label, logoUrl, origin, place, onPress, }) {
|
|
93
|
+
const t = look.tokens;
|
|
94
|
+
const [failed, setFailed] = useState([]);
|
|
95
|
+
const fail = (uri) => setFailed((cur) => (cur.includes(uri) ? cur : [...cur, uri]));
|
|
96
|
+
const shown = useRef(new Animated.Value(look.durationMs > 0 ? 0 : 1)).current;
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (look.durationMs <= 0) {
|
|
99
|
+
shown.setValue(1);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const a = Animated.timing(shown, { toValue: 1, duration: look.durationMs, easing: look.easing, useNativeDriver: true });
|
|
103
|
+
a.start();
|
|
104
|
+
return () => a.stop();
|
|
105
|
+
}, []);
|
|
106
|
+
const onAccent = onAccentFor(accent, t);
|
|
107
|
+
const pill = t.shape.launcher === "pill";
|
|
108
|
+
const radius = launcherRadius(t.shape.launcher, size, t);
|
|
109
|
+
const g = Math.round(size * 0.46 * 10) / 10;
|
|
110
|
+
const glyphUri = t.icons.launcher
|
|
111
|
+
? glyphUrl(origin, t.icons.launcher.id, onAccent, glyphPixels(size, PixelRatio.get()), t.icons.stroke)
|
|
112
|
+
: null;
|
|
113
|
+
const glyph = glyphUri && !failed.includes(glyphUri) ? glyphUri : null;
|
|
114
|
+
const logo = logoUrl && !failed.includes(logoUrl) ? logoUrl : null;
|
|
115
|
+
const hard = t.elevation.hardOffset;
|
|
116
|
+
const corners = {
|
|
117
|
+
borderRadius: radius,
|
|
118
|
+
...(t.shape.launcher === "squircle" && Platform.OS === "ios" ? { borderCurve: "continuous" } : null),
|
|
119
|
+
};
|
|
120
|
+
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: {
|
|
121
|
+
color: onAccent, fontSize: Math.round(size * 0.46), fontWeight: String(t.font.weights.title),
|
|
122
|
+
fontFamily: look.font(t.font.heading),
|
|
123
|
+
}, children: name.trim().charAt(0).toUpperCase() || "\u2022" }));
|
|
124
|
+
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 }) => ({
|
|
125
|
+
...corners,
|
|
126
|
+
height: size, minWidth: size, width: pill ? undefined : size,
|
|
127
|
+
flexDirection: "row", alignItems: "center", justifyContent: "center",
|
|
128
|
+
gap: pill ? 8 : 0, paddingHorizontal: pill ? Math.round(size * 0.32) : 0,
|
|
129
|
+
backgroundColor: accent,
|
|
130
|
+
opacity: pressed ? 0.85 : 1,
|
|
131
|
+
...(hard ? { borderWidth: t.shape.borderWidth, borderColor: t.colors.text } : shadowStyle(t.elevation.floating)),
|
|
132
|
+
}), children: [mark, pill ? (_jsx(Text, { numberOfLines: 1, style: {
|
|
133
|
+
color: onAccent, fontSize: t.font.scale.body, fontWeight: String(t.font.weights.strong),
|
|
134
|
+
fontFamily: look.font(t.font.body),
|
|
135
|
+
}, children: name })) : null] })] }));
|
|
78
136
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const FONT_WEIGHTS: Record<string, number[]>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const FONT_WEIGHTS = {
|
|
2
|
+
"Inter": [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
|
3
|
+
"Roboto": [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
|
4
|
+
"Geist": [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
|
5
|
+
"IBM Plex Sans": [100, 200, 300, 400, 500, 600, 700],
|
|
6
|
+
"IBM Plex Sans Devanagari": [100, 200, 300, 400, 500, 600, 700],
|
|
7
|
+
"DM Sans": [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
|
8
|
+
"Noto Sans": [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
|
9
|
+
"Noto Sans Devanagari": [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
|
10
|
+
"Noto Sans Tamil": [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
|
11
|
+
"Mukta": [200, 300, 400, 500, 600, 700, 800],
|
|
12
|
+
"Hind": [300, 400, 500, 600, 700],
|
|
13
|
+
"Anek Devanagari": [100, 200, 300, 400, 500, 600, 700, 800],
|
|
14
|
+
"Poppins": [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
|
15
|
+
"Baloo 2": [400, 500, 600, 700, 800],
|
|
16
|
+
"Nunito": [200, 300, 400, 500, 600, 700, 800, 900],
|
|
17
|
+
"Atkinson Hyperlegible Next": [200, 300, 400, 500, 600, 700, 800],
|
|
18
|
+
"Source Serif 4": [200, 300, 400, 500, 600, 700, 800, 900],
|
|
19
|
+
"Newsreader": [200, 300, 400, 500, 600, 700, 800],
|
|
20
|
+
"Fraunces": [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
|
21
|
+
"Playfair Display": [400, 500, 600, 700, 800, 900],
|
|
22
|
+
"Tiro Devanagari Hindi": [400],
|
|
23
|
+
"Noto Serif Devanagari": [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
|
24
|
+
"Martel": [200, 300, 400, 600, 700, 800, 900],
|
|
25
|
+
"JetBrains Mono": [100, 200, 300, 400, 500, 600, 700, 800],
|
|
26
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ResolvedDesign, ThemeTokens } from "../theme.js";
|
|
2
|
+
export type FontLoader = (fonts: Record<string, string>) => Promise<unknown>;
|
|
3
|
+
export interface FontFace {
|
|
4
|
+
family: string;
|
|
5
|
+
weight: number;
|
|
6
|
+
name: string;
|
|
7
|
+
url: string;
|
|
8
|
+
}
|
|
9
|
+
export type LoadedFaces = Record<string, string>;
|
|
10
|
+
export declare const faceKey: (family: string, weight: number) => string;
|
|
11
|
+
export declare function nearestWeight(available: number[], want: number): number | null;
|
|
12
|
+
export declare const FONT_ASSET_VERSION = 1;
|
|
13
|
+
export declare function fontUrl(origin: string, family: string, weight: number): string;
|
|
14
|
+
export declare function faceFor(family: string | null | undefined, weight: number, origin?: string): FontFace | null;
|
|
15
|
+
export declare function facesFor(t: ThemeTokens, origin?: string): FontFace[];
|
|
16
|
+
export declare function facesForDesign(design: ResolvedDesign, origin?: string): FontFace[];
|
|
17
|
+
export declare function textFace(family: string | null | undefined, weight: number, hostFonts: Record<string, string> | null | undefined, loaded: LoadedFaces | null | undefined, hostLookup: (family: string | null | undefined, fonts: Record<string, string> | null | undefined) => string | undefined): {
|
|
18
|
+
fontFamily?: string;
|
|
19
|
+
fontWeight: any;
|
|
20
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { FONT_WEIGHTS } from "./font-weights.js";
|
|
2
|
+
export const faceKey = (family, weight) => `${family}|${weight}`;
|
|
3
|
+
export function nearestWeight(available, want) {
|
|
4
|
+
if (!available.length)
|
|
5
|
+
return null;
|
|
6
|
+
let best = available[0];
|
|
7
|
+
for (const w of available) {
|
|
8
|
+
const d = Math.abs(w - want);
|
|
9
|
+
const bd = Math.abs(best - want);
|
|
10
|
+
if (d < bd || (d === bd && w > best))
|
|
11
|
+
best = w;
|
|
12
|
+
}
|
|
13
|
+
return best;
|
|
14
|
+
}
|
|
15
|
+
export const FONT_ASSET_VERSION = 1;
|
|
16
|
+
export function fontUrl(origin, family, weight) {
|
|
17
|
+
const base = String(origin || "https://www.graine.ai").replace(/\/+$/, "");
|
|
18
|
+
return `${base}/api/embed/font?f=${encodeURIComponent(family)}&w=${weight}&v=${FONT_ASSET_VERSION}`;
|
|
19
|
+
}
|
|
20
|
+
export function faceFor(family, weight, origin = "https://www.graine.ai") {
|
|
21
|
+
const want = String(family ?? "").trim();
|
|
22
|
+
if (!want || want.toLowerCase() === "system")
|
|
23
|
+
return null;
|
|
24
|
+
const canonical = Object.prototype.hasOwnProperty.call(FONT_WEIGHTS, want)
|
|
25
|
+
? want
|
|
26
|
+
: Object.keys(FONT_WEIGHTS).find((k) => k.toLowerCase() === want.toLowerCase());
|
|
27
|
+
if (!canonical)
|
|
28
|
+
return null;
|
|
29
|
+
const w = nearestWeight(FONT_WEIGHTS[canonical], weight);
|
|
30
|
+
if (w == null)
|
|
31
|
+
return null;
|
|
32
|
+
return {
|
|
33
|
+
family: canonical,
|
|
34
|
+
weight: w,
|
|
35
|
+
name: `Graine-${canonical.replace(/[^A-Za-z0-9]/g, "")}-${w}`,
|
|
36
|
+
url: fontUrl(origin, canonical, w),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function facesFor(t, origin) {
|
|
40
|
+
const out = new Map();
|
|
41
|
+
const add = (family, weight) => {
|
|
42
|
+
const f = faceFor(family, weight, origin);
|
|
43
|
+
if (f)
|
|
44
|
+
out.set(faceKey(f.family, f.weight), f);
|
|
45
|
+
};
|
|
46
|
+
add(t.font.body, t.font.weights.body);
|
|
47
|
+
add(t.font.body, t.font.weights.strong);
|
|
48
|
+
add(t.font.heading, t.font.weights.title);
|
|
49
|
+
add(t.font.heading, t.font.weights.strong);
|
|
50
|
+
return [...out.values()];
|
|
51
|
+
}
|
|
52
|
+
export function facesForDesign(design, origin) {
|
|
53
|
+
const out = new Map();
|
|
54
|
+
for (const t of [design.light, design.dark]) {
|
|
55
|
+
for (const f of facesFor(t, origin))
|
|
56
|
+
out.set(faceKey(f.family, f.weight), f);
|
|
57
|
+
}
|
|
58
|
+
return [...out.values()];
|
|
59
|
+
}
|
|
60
|
+
export function textFace(family, weight, hostFonts, loaded, hostLookup) {
|
|
61
|
+
const host = hostLookup(family, hostFonts);
|
|
62
|
+
if (host)
|
|
63
|
+
return { fontFamily: host, fontWeight: String(weight) };
|
|
64
|
+
const face = faceFor(family, weight);
|
|
65
|
+
const name = face && loaded ? loaded[faceKey(face.family, face.weight)] : undefined;
|
|
66
|
+
if (name)
|
|
67
|
+
return { fontFamily: name, fontWeight: "normal" };
|
|
68
|
+
return { fontWeight: String(weight) };
|
|
69
|
+
}
|
|
@@ -17,6 +17,9 @@ 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
|
+
import { type FontLoader, type LoadedFaces } from "./fonts.js";
|
|
22
|
+
export { useGraineTheme } from "./use-design.js";
|
|
20
23
|
import { type LauncherInset, type LauncherVisibility } from "./navigation.js";
|
|
21
24
|
interface GraineContextValue {
|
|
22
25
|
client: GraineInAppClient;
|
|
@@ -36,6 +39,10 @@ interface GraineContextValue {
|
|
|
36
39
|
launcherInset: LauncherInset;
|
|
37
40
|
currentScreen: string | null;
|
|
38
41
|
appearance: Record<string, any> | null;
|
|
42
|
+
design: ResolvedDesign;
|
|
43
|
+
fonts: Record<string, string> | null;
|
|
44
|
+
faces: LoadedFaces;
|
|
45
|
+
svg: SvgKit | null;
|
|
39
46
|
sessionReady: boolean;
|
|
40
47
|
onEvent: (fn: (e: GraineEvent) => void) => () => void;
|
|
41
48
|
track: (name: string, data?: Record<string, unknown>) => void;
|
|
@@ -138,8 +145,15 @@ export interface GraineProviderProps extends Partial<GraineInAppOptions> {
|
|
|
138
145
|
includeScreens?: string[];
|
|
139
146
|
launcherDelayMs?: number;
|
|
140
147
|
visibility?: LauncherVisibility;
|
|
148
|
+
fonts?: Record<string, string>;
|
|
149
|
+
loadFont?: FontLoader;
|
|
150
|
+
svg?: SvgKit;
|
|
141
151
|
}
|
|
142
|
-
export
|
|
152
|
+
export interface SvgKit {
|
|
153
|
+
Svg: React.ComponentType<any>;
|
|
154
|
+
Path: React.ComponentType<any>;
|
|
155
|
+
}
|
|
156
|
+
export declare function GraineProvider({ children, autoConnect, surface: surfaceProp, onSurfaceChange, onProactive, endOnBackground, navigationRef, fab, captureTaps, appVersion, friction, includeScreens, launcherDelayMs, visibility, fonts: fontsProp, loadFont, svg: svgProp, client: providedClient, ...options }: GraineProviderProps): React.JSX.Element;
|
|
143
157
|
export declare function useGraineAgent(): GraineContextValue;
|
|
144
158
|
export declare function useGraineSurface(): {
|
|
145
159
|
surface: AgentSurface;
|