@graineai/inapp-react-native 0.37.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/index.d.ts +5 -2
- package/dist/index.js +4 -1
- package/dist/react-native/fab.d.ts +7 -0
- package/dist/react-native/fab.js +6 -0
- 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 +10 -1
- package/dist/react-native/index.js +32 -3
- package/dist/react-native/use-design.d.ts +4 -0
- package/dist/react-native/use-design.js +14 -2
- 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/theme.d.ts +5 -0
- package/dist/theme.js +10 -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/index.d.ts
CHANGED
|
@@ -23,14 +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, useGraineTheme, 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";
|
|
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";
|
|
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";
|
|
36
39
|
export { sessionDesign } from "./design.js";
|
package/dist/index.js
CHANGED
|
@@ -21,7 +21,10 @@ 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";
|
|
26
|
-
export { DESIGN_PRESETS, DEFAULT_DESIGN_PRESET, DESIGN_FONTS, LAUNCHER_ICONS, designPreset, normaliseDesignConfig, designFromAppearance, resolveTokens, resolveDesign, pickTokens, contrastRatio, readableOn, glyphUrl, } from "./theme.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";
|
|
27
30
|
export { sessionDesign } from "./design.js";
|
|
@@ -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
|
@@ -83,6 +83,12 @@ export function GraineFab({ webView, position = "bottom-right", size = 56, accen
|
|
|
83
83
|
return _jsx(AboveNativeScreens, { overlay: overlay, children: button({ start: () => { } }) });
|
|
84
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
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
|
+
}
|
|
86
92
|
function DesignedLauncher({ look, size, accent, name, label, logoUrl, origin, place, onPress, }) {
|
|
87
93
|
const t = look.tokens;
|
|
88
94
|
const [failed, setFailed] = useState([]);
|
|
@@ -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
|
+
}
|
|
@@ -18,6 +18,7 @@ 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
20
|
import type { ResolvedDesign } from "../theme.js";
|
|
21
|
+
import { type FontLoader, type LoadedFaces } from "./fonts.js";
|
|
21
22
|
export { useGraineTheme } from "./use-design.js";
|
|
22
23
|
import { type LauncherInset, type LauncherVisibility } from "./navigation.js";
|
|
23
24
|
interface GraineContextValue {
|
|
@@ -40,6 +41,8 @@ interface GraineContextValue {
|
|
|
40
41
|
appearance: Record<string, any> | null;
|
|
41
42
|
design: ResolvedDesign;
|
|
42
43
|
fonts: Record<string, string> | null;
|
|
44
|
+
faces: LoadedFaces;
|
|
45
|
+
svg: SvgKit | null;
|
|
43
46
|
sessionReady: boolean;
|
|
44
47
|
onEvent: (fn: (e: GraineEvent) => void) => () => void;
|
|
45
48
|
track: (name: string, data?: Record<string, unknown>) => void;
|
|
@@ -143,8 +146,14 @@ export interface GraineProviderProps extends Partial<GraineInAppOptions> {
|
|
|
143
146
|
launcherDelayMs?: number;
|
|
144
147
|
visibility?: LauncherVisibility;
|
|
145
148
|
fonts?: Record<string, string>;
|
|
149
|
+
loadFont?: FontLoader;
|
|
150
|
+
svg?: SvgKit;
|
|
146
151
|
}
|
|
147
|
-
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;
|
|
148
157
|
export declare function useGraineAgent(): GraineContextValue;
|
|
149
158
|
export declare function useGraineSurface(): {
|
|
150
159
|
surface: AgentSurface;
|
|
@@ -13,7 +13,8 @@ export { widgetSurface } from "../protocol.js";
|
|
|
13
13
|
export { resolveSurface } from "../protocol.js";
|
|
14
14
|
import { resolveSurface } from "../protocol.js";
|
|
15
15
|
import { sessionDesign } from "../design.js";
|
|
16
|
-
import { sameValue } from "./design-math.js";
|
|
16
|
+
import { fontFamilyFor, sameValue } from "./design-math.js";
|
|
17
|
+
import { facesForDesign, faceKey } from "./fonts.js";
|
|
17
18
|
export { useGraineTheme } from "./use-design.js";
|
|
18
19
|
import { LauncherVisibilityTracker, activeRouteName, } from "./navigation.js";
|
|
19
20
|
export const PENDING_WIDGET_MS = 12000;
|
|
@@ -32,7 +33,7 @@ export function useGraineClient() {
|
|
|
32
33
|
}
|
|
33
34
|
return client;
|
|
34
35
|
}
|
|
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 }) {
|
|
36
|
+
export function GraineProvider({ children, autoConnect = true, surface: surfaceProp, onSurfaceChange, onProactive, endOnBackground = true, navigationRef, fab, captureTaps, appVersion, friction, includeScreens, launcherDelayMs = 0, visibility, fonts: fontsProp, loadFont, svg: svgProp, client: providedClient, ...options }) {
|
|
36
37
|
const clientRef = useRef(null);
|
|
37
38
|
if (!clientRef.current) {
|
|
38
39
|
clientRef.current =
|
|
@@ -73,6 +74,32 @@ export function GraineProvider({ children, autoConnect = true, surface: surfaceP
|
|
|
73
74
|
}, []);
|
|
74
75
|
const fontsKey = fontsProp ? JSON.stringify(fontsProp) : "";
|
|
75
76
|
const fonts = useMemo(() => (fontsKey ? JSON.parse(fontsKey) : null), [fontsKey]);
|
|
77
|
+
const [faces, setFaces] = useState({});
|
|
78
|
+
const svgKit = useMemo(() => (svgProp?.Svg && svgProp?.Path ? { Svg: svgProp.Svg, Path: svgProp.Path } : null), [svgProp?.Svg, svgProp?.Path]);
|
|
79
|
+
const askedRef = useRef(new Set());
|
|
80
|
+
const loadFontRef = useRef(loadFont);
|
|
81
|
+
loadFontRef.current = loadFont;
|
|
82
|
+
const hasLoader = typeof loadFont === "function";
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
const load = loadFontRef.current;
|
|
85
|
+
if (typeof load !== "function")
|
|
86
|
+
return;
|
|
87
|
+
let live = true;
|
|
88
|
+
for (const face of facesForDesign(design, client.opts.baseUrl)) {
|
|
89
|
+
const key = faceKey(face.family, face.weight);
|
|
90
|
+
if (fontFamilyFor(face.family, fonts))
|
|
91
|
+
continue;
|
|
92
|
+
if (askedRef.current.has(key))
|
|
93
|
+
continue;
|
|
94
|
+
askedRef.current.add(key);
|
|
95
|
+
Promise.resolve()
|
|
96
|
+
.then(() => load({ [face.name]: face.url }))
|
|
97
|
+
.then(() => { if (live)
|
|
98
|
+
setFaces((cur) => (cur[key] === face.name ? cur : { ...cur, [key]: face.name })); })
|
|
99
|
+
.catch(() => { });
|
|
100
|
+
}
|
|
101
|
+
return () => { live = false; askedRef.current.clear(); };
|
|
102
|
+
}, [client, design, fonts, hasLoader]);
|
|
76
103
|
const [sessionReady, setSessionReady] = useState(false);
|
|
77
104
|
const [connectionQuality, setConnectionQuality] = useState("unknown");
|
|
78
105
|
const [network, setNetwork] = useState("unknown");
|
|
@@ -350,6 +377,8 @@ export function GraineProvider({ children, autoConnect = true, surface: surfaceP
|
|
|
350
377
|
appearance,
|
|
351
378
|
design,
|
|
352
379
|
fonts,
|
|
380
|
+
faces,
|
|
381
|
+
svg: svgKit,
|
|
353
382
|
sessionReady,
|
|
354
383
|
onEvent,
|
|
355
384
|
track: (name, data) => client.track(name, data),
|
|
@@ -362,7 +391,7 @@ export function GraineProvider({ children, autoConnect = true, surface: surfaceP
|
|
|
362
391
|
network,
|
|
363
392
|
setVariables: (vars) => client.setVariables(vars),
|
|
364
393
|
}), [client, connected, connecting, error, open, messages, widgets, muted, agentSpeaking, caption,
|
|
365
|
-
launcherVisible, launcherInset, currentScreen, onEvent, appearance, design, fonts, sessionReady,
|
|
394
|
+
launcherVisible, launcherInset, currentScreen, onEvent, appearance, design, fonts, faces, svgKit, sessionReady,
|
|
366
395
|
surface, surfacePolicy, setSurface, connectionQuality, network]);
|
|
367
396
|
const wantsCapture = captureTaps ?? !!fab;
|
|
368
397
|
const body = fab || wantsCapture ? (_jsxs(GraineTouchCapture, { enabled: wantsCapture, children: [children, fab ? _jsx(GraineFab, { ...fab }) : null] })) : children;
|
|
@@ -6,6 +6,10 @@ export interface DesignLook {
|
|
|
6
6
|
tokens: ThemeTokens;
|
|
7
7
|
classic: boolean;
|
|
8
8
|
font: (family: string) => string | undefined;
|
|
9
|
+
face: (family: string, weight: number) => {
|
|
10
|
+
fontFamily?: string;
|
|
11
|
+
fontWeight: any;
|
|
12
|
+
};
|
|
9
13
|
durationMs: number;
|
|
10
14
|
reduceMotion: boolean;
|
|
11
15
|
easing: (t: number) => number;
|
|
@@ -4,7 +4,9 @@ import { Ctx } from "./index.js";
|
|
|
4
4
|
import { sessionDesign } from "../design.js";
|
|
5
5
|
import { pickTokens } from "../theme.js";
|
|
6
6
|
import { fontFamilyFor, isClassicLook } from "./design-math.js";
|
|
7
|
+
import { faceFor, faceKey, textFace } from "./fonts.js";
|
|
7
8
|
const DEFAULT_DESIGN = sessionDesign(null);
|
|
9
|
+
const NO_FACES = {};
|
|
8
10
|
const classicSeen = new WeakMap();
|
|
9
11
|
function classic(design) {
|
|
10
12
|
let v = classicSeen.get(design);
|
|
@@ -53,6 +55,7 @@ export function useDesign(scheme) {
|
|
|
53
55
|
const reduce = useReduceMotion();
|
|
54
56
|
const design = ctx?.design ?? DEFAULT_DESIGN;
|
|
55
57
|
const fonts = ctx?.fonts ?? null;
|
|
58
|
+
const faces = ctx?.faces ?? NO_FACES;
|
|
56
59
|
return useMemo(() => {
|
|
57
60
|
const tokens = scheme === "light" ? design.light : scheme === "dark" ? design.dark : pickTokens(design, device);
|
|
58
61
|
const durationMs = reduce || tokens.motion.easing === "none" ? 0 : Math.max(0, tokens.motion.durationMs);
|
|
@@ -60,12 +63,21 @@ export function useDesign(scheme) {
|
|
|
60
63
|
design,
|
|
61
64
|
tokens,
|
|
62
65
|
classic: classic(design),
|
|
63
|
-
font: (family) =>
|
|
66
|
+
font: (family) => {
|
|
67
|
+
const own = fontFamilyFor(family, fonts);
|
|
68
|
+
if (own)
|
|
69
|
+
return own;
|
|
70
|
+
const weight = family === tokens.font.heading && family !== tokens.font.body
|
|
71
|
+
? tokens.font.weights.title : tokens.font.weights.body;
|
|
72
|
+
const f = faceFor(family, weight);
|
|
73
|
+
return f ? faces[faceKey(f.family, f.weight)] : undefined;
|
|
74
|
+
},
|
|
75
|
+
face: (family, weight) => textFace(family, weight, fonts, faces, fontFamilyFor),
|
|
64
76
|
durationMs,
|
|
65
77
|
reduceMotion: reduce,
|
|
66
78
|
easing: easingOf(tokens.motion.easing, tokens.motion.bounce),
|
|
67
79
|
};
|
|
68
|
-
}, [design, fonts, device, reduce, scheme]);
|
|
80
|
+
}, [design, fonts, faces, device, reduce, scheme]);
|
|
69
81
|
}
|
|
70
82
|
export function useGraineTheme() {
|
|
71
83
|
return useDesign().tokens;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { GraineVoiceApi } from "./voice-launcher.js";
|
|
3
|
+
export interface GraineVoiceBarProps {
|
|
4
|
+
api: GraineVoiceApi;
|
|
5
|
+
onClose?: () => void;
|
|
6
|
+
bottomInset?: number;
|
|
7
|
+
draggable?: boolean;
|
|
8
|
+
placement?: {
|
|
9
|
+
bottomOffset?: number;
|
|
10
|
+
} | null;
|
|
11
|
+
onPlacementChange?: (placement: {
|
|
12
|
+
bottomOffset: number;
|
|
13
|
+
}) => void;
|
|
14
|
+
name?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function GraineVoiceBar({ api, onClose, bottomInset, draggable, placement, onPlacementChange, name: nameProp, }: GraineVoiceBarProps): React.JSX.Element;
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useContext, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { Animated, Easing, Image, PixelRatio, Platform, Pressable, ScrollView, Text, TextInput, View } from "react-native";
|
|
4
|
+
import { Ctx, useGraineAgent } from "./index.js";
|
|
5
|
+
import { GraineLoader } from "./loader.js";
|
|
6
|
+
import { GraineWidget } from "./widget.js";
|
|
7
|
+
import { resolveNativeAppearance } from "./appearance.js";
|
|
8
|
+
import { useDesign } from "./use-design.js";
|
|
9
|
+
import { glyphPixels, launcherRadius, letterSpacing, lineHeight, shadowStyle, surfaceFill } from "./design-math.js";
|
|
10
|
+
import { useKeyboardLift } from "./keyboard.js";
|
|
11
|
+
import { useDraggablePlacement } from "./placement.js";
|
|
12
|
+
import { glyphUrl, SDK_ICONS } from "../theme.js";
|
|
13
|
+
const BAR_HEIGHT = 60;
|
|
14
|
+
const BUTTON = 38;
|
|
15
|
+
const METER = [0.55, 1, 0.7, 0.4];
|
|
16
|
+
function statusOf(api, isChat, chatConnected) {
|
|
17
|
+
const conn = api.connectivity?.state;
|
|
18
|
+
const say = (text) => ({ text, waiting: null, meter: false });
|
|
19
|
+
if (api.error)
|
|
20
|
+
return say("Unavailable");
|
|
21
|
+
if (isChat)
|
|
22
|
+
return chatConnected ? say("Online") : { text: "Connecting…", waiting: "connecting", meter: false };
|
|
23
|
+
if (api.network === "offline")
|
|
24
|
+
return say("No connection");
|
|
25
|
+
if (api.reconnecting || conn === "retrying")
|
|
26
|
+
return { text: "Reconnecting…", waiting: "reconnecting", meter: false };
|
|
27
|
+
if (api.micDenied)
|
|
28
|
+
return say("Needs microphone");
|
|
29
|
+
if (conn === "failed")
|
|
30
|
+
return say("Couldn’t load · tap to retry");
|
|
31
|
+
if (!api.ready || api.connecting)
|
|
32
|
+
return { text: "Connecting…", waiting: "connecting", meter: false };
|
|
33
|
+
if (api.muted)
|
|
34
|
+
return say("Muted");
|
|
35
|
+
if (api.connected && api.phase === "thinking")
|
|
36
|
+
return { text: "Thinking…", waiting: "thinking", meter: false };
|
|
37
|
+
if (api.connected && api.caption?.role === "agent" && api.caption.live)
|
|
38
|
+
return { text: "Speaking", waiting: null, meter: true };
|
|
39
|
+
if (api.connected && api.quality === "poor")
|
|
40
|
+
return say("Weak connection");
|
|
41
|
+
if (api.connected)
|
|
42
|
+
return { text: "Listening", waiting: null, meter: true };
|
|
43
|
+
return say("Tap to talk");
|
|
44
|
+
}
|
|
45
|
+
export function GraineVoiceBar({ api, onClose, bottomInset, draggable = true, placement, onPlacementChange, name: nameProp, }) {
|
|
46
|
+
const ctx = useContext(Ctx);
|
|
47
|
+
const { messages, send, connected: chatConnected, appearance, surface, canSwitch, setSurface, launcherInset } = useGraineAgent();
|
|
48
|
+
const look = useDesign();
|
|
49
|
+
const t = look.tokens;
|
|
50
|
+
const c = t.colors;
|
|
51
|
+
const kit = ctx?.svg ?? null;
|
|
52
|
+
const origin = ctx?.client?.opts?.baseUrl || "https://www.graine.ai";
|
|
53
|
+
const dash = resolveNativeAppearance(appearance, { accent: c.accent, heading: "Assistant" });
|
|
54
|
+
const heading = dash.heading;
|
|
55
|
+
const spoken = nameProp || heading;
|
|
56
|
+
const isChat = surface === "chat";
|
|
57
|
+
const keyboardLift = useKeyboardLift();
|
|
58
|
+
const baseBottom = bottomInset ?? launcherInset?.bottom ?? 96;
|
|
59
|
+
const drag = useDraggablePlacement({
|
|
60
|
+
baseBottom,
|
|
61
|
+
enabled: draggable,
|
|
62
|
+
initialOffset: placement?.bottomOffset,
|
|
63
|
+
onChange: (bottomOffset) => onPlacementChange?.({ bottomOffset }),
|
|
64
|
+
});
|
|
65
|
+
const status = statusOf(api, isChat, chatConnected);
|
|
66
|
+
const conn = api.connectivity?.state;
|
|
67
|
+
const micBlocked = !!api.error || api.micDenied || api.connecting || (!api.ready && conn !== "failed");
|
|
68
|
+
const caption = !isChat && api.captionsOn && api.caption?.text ? api.caption : null;
|
|
69
|
+
const body = (size, weight) => ({
|
|
70
|
+
...look.face(t.font.body, weight),
|
|
71
|
+
fontSize: size,
|
|
72
|
+
letterSpacing: letterSpacing(size, t.font.letterSpacing.body),
|
|
73
|
+
});
|
|
74
|
+
const endCall = () => {
|
|
75
|
+
api.end();
|
|
76
|
+
setTimeout(() => onClose?.(), 400);
|
|
77
|
+
};
|
|
78
|
+
return (_jsxs(View, { pointerEvents: "box-none", ...drag.panHandlers, onLayout: drag.onLayout, style: {
|
|
79
|
+
position: "absolute", zIndex: 900, gap: 8, left: 12, right: 12,
|
|
80
|
+
bottom: baseBottom + drag.offset + keyboardLift,
|
|
81
|
+
}, children: [_jsx(WidgetGuard, { children: _jsx(GraineWidget, {}) }), isChat ? (_jsx(ChatWindow, { look: look, kit: kit, origin: origin, heading: heading, logoUrl: dash.logoUrl ?? undefined, messages: messages, send: send, online: chatConnected, lifted: keyboardLift > 0, onCollapse: () => (canSwitch ? setSurface("voice") : endCall()) })) : caption ? (_jsx(CaptionCard, { look: look, role: caption.role, text: caption.text, live: !!caption.live })) : null, _jsx(Raised, { t: t, level: "floating", radius: t.shape.radius.xl, children: _jsxs(View, { style: {
|
|
82
|
+
height: BAR_HEIGHT, paddingHorizontal: 9, flexDirection: "row", alignItems: "center", gap: 10,
|
|
83
|
+
backgroundColor: surfaceFill(c.surface, t), borderWidth: t.shape.borderWidth, borderColor: c.border,
|
|
84
|
+
borderRadius: t.shape.radius.xl, ...softShadow(t, "floating"),
|
|
85
|
+
}, children: [_jsx(View, { style: { width: BUTTON, height: BUTTON, borderRadius: BUTTON / 2, backgroundColor: c.accent, alignItems: "center", justifyContent: "center", overflow: "hidden" }, children: _jsx(Mark, { t: t, size: BUTTON, glyph: 18, kit: kit, origin: origin, logoUrl: dash.logoUrl ?? undefined, initial: heading.charAt(0).toUpperCase() || "A", cover: true }) }), _jsxs(View, { style: { flex: 1, minWidth: 0 }, children: [_jsx(Text, { numberOfLines: 1, style: { ...body(14, t.font.weights.strong), color: c.text, lineHeight: 17.5 }, children: heading }), _jsx(View, { style: { flexDirection: "row", alignItems: "center", gap: 6, marginTop: 1 }, children: status.waiting ? (_jsx(GraineLoader, { moment: status.waiting, size: 14 })) : (_jsxs(_Fragment, { children: [status.meter ? _jsx(Meter, { look: look, color: c.accent }) : null, _jsx(Text, { numberOfLines: 1, style: { ...body(t.font.scale.caption, t.font.weights.body), color: c.textMuted, lineHeight: lineHeight(t.font.scale.caption, 1.3) }, children: status.text })] })) })] }), canSwitch ? (_jsx(RoundButton, { label: isChat ? "Switch to a call" : "Switch to chat", fill: c.accentSoft, onPress: () => setSurface(isChat ? "voice" : "chat"), children: _jsx(Icon, { name: isChat ? "call" : "chat", t: t, color: c.text, kit: kit }) })) : null, isChat ? null : (_jsx(RoundButton, { label: !api.connected ? `Start talking to ${spoken}` : api.muted ? "Unmute" : "Mute", fill: api.muted ? c.danger : c.accent, disabled: micBlocked, onPress: () => {
|
|
86
|
+
if (conn === "failed") {
|
|
87
|
+
api.retry();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (api.connected)
|
|
91
|
+
api.setMuted(!api.muted);
|
|
92
|
+
else
|
|
93
|
+
api.start();
|
|
94
|
+
}, children: _jsx(Icon, { name: api.muted ? "micOff" : "mic", t: t, color: api.muted ? "#FFFFFF" : c.onAccent, kit: kit }) })), _jsx(RoundButton, { label: api.connected ? "End call" : "Close", fill: api.connected ? c.danger : c.accentSoft, onPress: endCall, children: _jsx(Icon, { name: api.connected ? "end" : "close", t: t, color: api.connected ? "#FFFFFF" : c.textMuted, kit: kit }) })] }) })] }));
|
|
95
|
+
}
|
|
96
|
+
function softShadow(t, level) {
|
|
97
|
+
return t.elevation.hardOffset ? {} : shadowStyle(t.elevation[level]);
|
|
98
|
+
}
|
|
99
|
+
function Raised({ t, radius, children }) {
|
|
100
|
+
const hard = t.elevation.hardOffset;
|
|
101
|
+
if (!hard)
|
|
102
|
+
return _jsx(_Fragment, { children: children });
|
|
103
|
+
return (_jsxs(View, { children: [_jsx(View, { pointerEvents: "none", style: { position: "absolute", left: hard.x, top: hard.y, right: -hard.x, bottom: -hard.y, borderRadius: radius, backgroundColor: t.colors.text } }), children] }));
|
|
104
|
+
}
|
|
105
|
+
function RoundButton({ label, fill, disabled = false, onPress, children, }) {
|
|
106
|
+
return (_jsx(Pressable, { onPress: onPress, disabled: disabled, hitSlop: 8, accessibilityRole: "button", accessibilityLabel: label, style: {
|
|
107
|
+
width: BUTTON, height: BUTTON, borderRadius: BUTTON / 2, alignItems: "center", justifyContent: "center",
|
|
108
|
+
backgroundColor: fill, opacity: disabled ? 0.4 : 1,
|
|
109
|
+
}, children: children }));
|
|
110
|
+
}
|
|
111
|
+
const FALLBACK = {
|
|
112
|
+
mic: "🎙", micOff: "🔇", end: "✕", send: "↑", chat: "💬", call: "📞", chevron: "⌄", close: "✕",
|
|
113
|
+
};
|
|
114
|
+
function Icon({ name, t, color, size = 18, kit, rotate }) {
|
|
115
|
+
if (!kit) {
|
|
116
|
+
return _jsx(Text, { style: { color, fontSize: Math.round(size * 0.8), lineHeight: size, textAlign: "center" }, children: FALLBACK[name] });
|
|
117
|
+
}
|
|
118
|
+
const { Svg, Path } = kit;
|
|
119
|
+
const icon = SDK_ICONS[name];
|
|
120
|
+
const style = t.icons.style;
|
|
121
|
+
const sharp = style === "sharp";
|
|
122
|
+
const filled = style === "filled";
|
|
123
|
+
const stroke = t.icons.stroke;
|
|
124
|
+
const common = { stroke: color, strokeLinecap: sharp ? "square" : "round", strokeLinejoin: sharp ? "miter" : "round" };
|
|
125
|
+
return (_jsx(View, { style: rotate ? { transform: [{ rotate: "180deg" }] } : undefined, children: _jsxs(Svg, { width: size, height: size, viewBox: "0 0 24 24", children: [icon.body ? (_jsx(Path, { d: icon.body, ...common, fill: filled || style === "duotone" ? color : "none", fillOpacity: style === "duotone" ? 0.28 : 1, strokeWidth: filled ? Math.max(1, stroke * 0.75) : stroke })) : null, icon.lines ? _jsx(Path, { d: icon.lines, ...common, fill: "none", strokeWidth: stroke }) : null] }) }));
|
|
126
|
+
}
|
|
127
|
+
function Mark({ t, size, glyph, kit, origin, logoUrl, initial, cover = false, }) {
|
|
128
|
+
const on = t.colors.onAccent;
|
|
129
|
+
const g = t.icons.launcher;
|
|
130
|
+
if (g) {
|
|
131
|
+
if (kit) {
|
|
132
|
+
const { Svg, Path } = kit;
|
|
133
|
+
return (_jsx(Svg, { width: glyph, height: glyph, viewBox: "0 0 24 24", children: _jsx(Path, { d: g.path, fill: g.stroke ? "none" : on, stroke: g.stroke ? on : "none", strokeWidth: g.stroke ? t.icons.stroke : 0, strokeLinecap: "round", strokeLinejoin: "round" }) }));
|
|
134
|
+
}
|
|
135
|
+
return (_jsx(Image, { source: { uri: glyphUrl(origin, g.id, on, glyphPixels(size, PixelRatio.get()), t.icons.stroke) }, style: { width: glyph, height: glyph }, accessibilityIgnoresInvertColors: true }));
|
|
136
|
+
}
|
|
137
|
+
if (logoUrl) {
|
|
138
|
+
return _jsx(Image, { source: { uri: logoUrl }, style: cover ? { width: size, height: size } : { width: glyph, height: glyph }, resizeMode: cover ? "cover" : "contain", accessibilityIgnoresInvertColors: true });
|
|
139
|
+
}
|
|
140
|
+
return _jsx(Text, { style: { color: on, fontSize: Math.round(size * 0.4), fontWeight: "700" }, children: initial });
|
|
141
|
+
}
|
|
142
|
+
function Meter({ look, color }) {
|
|
143
|
+
const still = look.durationMs <= 0;
|
|
144
|
+
const bars = useRef(METER.map(() => new Animated.Value(1))).current;
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
if (still) {
|
|
147
|
+
bars.forEach((b) => b.setValue(1));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const period = Math.max(420, look.durationMs * 3.2);
|
|
151
|
+
const ease = Easing.inOut(Easing.ease);
|
|
152
|
+
const runs = bars.map((b, i) => {
|
|
153
|
+
b.setValue(0.35);
|
|
154
|
+
return Animated.sequence([
|
|
155
|
+
Animated.delay(i * 90),
|
|
156
|
+
Animated.loop(Animated.sequence([
|
|
157
|
+
Animated.timing(b, { toValue: 1, duration: period, easing: ease, useNativeDriver: true }),
|
|
158
|
+
Animated.timing(b, { toValue: 0.35, duration: period, easing: ease, useNativeDriver: true }),
|
|
159
|
+
])),
|
|
160
|
+
]);
|
|
161
|
+
});
|
|
162
|
+
runs.forEach((r) => r.start());
|
|
163
|
+
return () => runs.forEach((r) => r.stop());
|
|
164
|
+
}, [still, look.durationMs, bars]);
|
|
165
|
+
return (_jsx(View, { style: { flexDirection: "row", alignItems: "flex-end", height: 12, gap: 2 }, accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", children: METER.map((h, i) => (_jsx(Animated.View, { style: {
|
|
166
|
+
width: 2.5, height: Math.round(12 * h * 10) / 10, borderRadius: 1.25, backgroundColor: color,
|
|
167
|
+
transform: [{ scaleY: bars[i] }], transformOrigin: "bottom",
|
|
168
|
+
} }, i))) }));
|
|
169
|
+
}
|
|
170
|
+
function useRise(look) {
|
|
171
|
+
const v = useRef(new Animated.Value(look.durationMs > 0 ? 0 : 1)).current;
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
if (look.durationMs <= 0) {
|
|
174
|
+
v.setValue(1);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const a = Animated.timing(v, { toValue: 1, duration: look.durationMs, easing: look.easing, useNativeDriver: true });
|
|
178
|
+
a.start();
|
|
179
|
+
return () => a.stop();
|
|
180
|
+
}, []);
|
|
181
|
+
return { opacity: v, transform: [{ translateY: v.interpolate({ inputRange: [0, 1], outputRange: [8, 0] }) }] };
|
|
182
|
+
}
|
|
183
|
+
function CaptionCard({ look, role, text, live }) {
|
|
184
|
+
const t = look.tokens;
|
|
185
|
+
const c = t.colors;
|
|
186
|
+
const rise = useRise(look);
|
|
187
|
+
return (_jsx(Animated.View, { style: rise, children: _jsx(Raised, { t: t, level: "card", radius: t.shape.radius.lg, children: _jsxs(View, { style: {
|
|
188
|
+
backgroundColor: surfaceFill(c.surface, t), borderWidth: t.shape.borderWidth, borderColor: c.border,
|
|
189
|
+
borderRadius: t.shape.radius.lg, paddingHorizontal: 12, paddingVertical: 8, ...softShadow(t, "card"),
|
|
190
|
+
}, children: [role === "customer" ? (_jsx(Text, { style: { ...look.face(t.font.body, t.font.weights.strong), color: c.accent, fontSize: 10.5, letterSpacing: 0.6, textTransform: "uppercase", marginBottom: 2 }, children: "You" })) : null, _jsx(Text, { numberOfLines: 2, style: { ...look.face(t.font.body, t.font.weights.strong), color: c.text, fontSize: 13, lineHeight: 17.5, opacity: live ? 0.75 : 1 }, children: text })] }) }) }));
|
|
191
|
+
}
|
|
192
|
+
function ChatWindow({ look, kit, origin, heading, logoUrl, messages, send, online, lifted, onCollapse, }) {
|
|
193
|
+
const t = look.tokens;
|
|
194
|
+
const c = t.colors;
|
|
195
|
+
const r = t.shape.radius;
|
|
196
|
+
const bw = t.shape.borderWidth;
|
|
197
|
+
const pad = t.spacing.cardPadding;
|
|
198
|
+
const rise = useRise(look);
|
|
199
|
+
const [draft, setDraft] = useState("");
|
|
200
|
+
const scroll = useRef(null);
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
const id = setTimeout(() => scroll.current?.scrollToEnd?.({ animated: true }), 40);
|
|
203
|
+
return () => clearTimeout(id);
|
|
204
|
+
}, [messages.length]);
|
|
205
|
+
const submit = () => {
|
|
206
|
+
const text = draft.trim();
|
|
207
|
+
if (!text)
|
|
208
|
+
return;
|
|
209
|
+
setDraft("");
|
|
210
|
+
send(text);
|
|
211
|
+
};
|
|
212
|
+
const markShape = t.shape.launcher === "pill" ? "circle" : t.shape.launcher;
|
|
213
|
+
const text = (size, weight) => ({
|
|
214
|
+
...look.face(t.font.body, weight), fontSize: size,
|
|
215
|
+
letterSpacing: letterSpacing(size, t.font.letterSpacing.body),
|
|
216
|
+
});
|
|
217
|
+
const titleSize = Math.min(t.font.scale.title, 16);
|
|
218
|
+
return (_jsx(Animated.View, { style: rise, children: _jsx(Raised, { t: t, level: "floating", radius: r.xl, children: _jsx(View, { style: { backgroundColor: c.background, borderWidth: bw, borderColor: c.border, borderRadius: r.xl, ...softShadow(t, "floating") }, children: _jsxs(View, { style: { borderRadius: Math.max(0, r.xl - bw), overflow: "hidden" }, children: [_jsxs(View, { style: { height: 50, flexDirection: "row", alignItems: "center", gap: 10, paddingHorizontal: 12, backgroundColor: surfaceFill(c.surface, t), borderBottomWidth: bw, borderBottomColor: c.border }, children: [_jsx(View, { style: { width: 30, height: 30, borderRadius: launcherRadius(markShape, 30, t), backgroundColor: c.accent, alignItems: "center", justifyContent: "center", overflow: "hidden", ...(Platform.OS === "ios" && markShape === "squircle" ? { borderCurve: "continuous" } : null) }, children: _jsx(Mark, { t: t, size: 30, glyph: 14, kit: kit, origin: origin, logoUrl: logoUrl, initial: heading.charAt(0).toUpperCase() || "A" }) }), _jsxs(View, { style: { flex: 1, minWidth: 0 }, children: [_jsx(Text, { numberOfLines: 1, style: { ...look.face(t.font.heading, t.font.weights.title), color: c.text, fontSize: titleSize, lineHeight: lineHeight(titleSize, t.font.lineHeight.title), letterSpacing: letterSpacing(titleSize, t.font.letterSpacing.title) }, children: heading }), _jsxs(View, { style: { flexDirection: "row", alignItems: "center", gap: 4 }, children: [online ? _jsx(View, { style: { width: 6, height: 6, borderRadius: 3, backgroundColor: c.success } }) : null, _jsx(Text, { style: { ...text(t.font.scale.caption, t.font.weights.body), color: c.textMuted, lineHeight: lineHeight(t.font.scale.caption, 1.2) }, children: online ? "Online" : "Connecting…" })] })] }), _jsx(Pressable, { onPress: onCollapse, hitSlop: 10, accessibilityRole: "button", accessibilityLabel: "Collapse", children: _jsx(Icon, { name: "chevron", t: t, color: c.textMuted, kit: kit, rotate: true }) })] }), _jsxs(ScrollView, { ref: scroll, style: { maxHeight: lifted ? 150 : 260 }, contentContainerStyle: { padding: 12, gap: t.spacing.gap }, keyboardShouldPersistTaps: "handled", children: [messages.length === 0 ? (_jsx(Text, { style: { ...text(t.font.scale.body, t.font.weights.body), color: c.textMuted, lineHeight: lineHeight(t.font.scale.body, t.font.lineHeight.body) }, children: online ? `${heading} can see what you’re looking at — ask it anything.` : "Connecting…" })) : null, messages.map((m, i) => {
|
|
219
|
+
const b = bubble(t, m.role !== "agent");
|
|
220
|
+
return (_jsx(View, { style: [{ maxWidth: "84%" }, b.box], children: _jsx(Text, { style: { ...text(t.font.scale.body, t.font.weights.body), color: b.color, lineHeight: lineHeight(t.font.scale.body, t.font.lineHeight.body), opacity: m.live ? 0.75 : 1 }, children: m.text }) }, i));
|
|
221
|
+
})] }), _jsxs(View, { style: { height: 58, flexDirection: "row", alignItems: "center", gap: 8, paddingHorizontal: 10, backgroundColor: c.background }, children: [_jsx(TextInput, { value: draft, onChangeText: setDraft, onSubmitEditing: submit, returnKeyType: "send", blurOnSubmit: false, placeholder: "Message\u2026", placeholderTextColor: c.textMuted, style: {
|
|
222
|
+
flex: 1, height: 40, paddingHorizontal: 14, backgroundColor: c.surfaceRaised, borderWidth: bw, borderColor: c.border,
|
|
223
|
+
borderRadius: r.xl, color: c.text, ...look.face(t.font.body, t.font.weights.body), fontSize: t.font.scale.body - 1,
|
|
224
|
+
} }), _jsx(Pressable, { onPress: submit, disabled: !draft.trim(), accessibilityRole: "button", accessibilityLabel: "Send", style: { width: 40, height: 40, borderRadius: 20, alignItems: "center", justifyContent: "center", backgroundColor: c.accent, opacity: draft.trim() ? 1 : 0.4 }, children: _jsx(Icon, { name: "send", t: t, color: c.onAccent, kit: kit }) })] })] }) }) }) }));
|
|
225
|
+
}
|
|
226
|
+
function bubble(t, user) {
|
|
227
|
+
const c = t.colors;
|
|
228
|
+
const r = t.shape.radius;
|
|
229
|
+
const pad = t.spacing.cardPadding;
|
|
230
|
+
const shape = {
|
|
231
|
+
alignSelf: user ? "flex-end" : "flex-start",
|
|
232
|
+
paddingHorizontal: pad, paddingVertical: Math.round(pad * 0.75), borderRadius: r.lg,
|
|
233
|
+
...(user ? { borderBottomRightRadius: r.sm } : { borderBottomLeftRadius: r.sm }),
|
|
234
|
+
};
|
|
235
|
+
if (t.bubbles === "outline") {
|
|
236
|
+
return { box: { ...shape, backgroundColor: "transparent", borderWidth: t.shape.borderWidth, borderColor: user ? c.accent : c.border }, color: c.text };
|
|
237
|
+
}
|
|
238
|
+
if (t.bubbles === "minimal") {
|
|
239
|
+
return user
|
|
240
|
+
? { box: { ...shape, backgroundColor: c.accentSoft }, color: c.text }
|
|
241
|
+
: { box: { alignSelf: "flex-start", paddingHorizontal: 2, paddingVertical: 2, backgroundColor: "transparent" }, color: c.text };
|
|
242
|
+
}
|
|
243
|
+
return user
|
|
244
|
+
? { box: { ...shape, backgroundColor: c.userBubble }, color: c.userBubbleText }
|
|
245
|
+
: { box: { ...shape, backgroundColor: c.agentBubble }, color: c.agentBubbleText };
|
|
246
|
+
}
|
|
247
|
+
class WidgetGuard extends React.Component {
|
|
248
|
+
constructor() {
|
|
249
|
+
super(...arguments);
|
|
250
|
+
this.state = { failed: false };
|
|
251
|
+
}
|
|
252
|
+
static getDerivedStateFromError() { return { failed: true }; }
|
|
253
|
+
componentDidCatch(error) { console.error("[graine] the agent's component failed to render", error); }
|
|
254
|
+
render() { return this.state.failed ? null : this.props.children; }
|
|
255
|
+
}
|
|
@@ -59,6 +59,8 @@ export interface GraineVoiceApi {
|
|
|
59
59
|
quality: ConnectionQuality;
|
|
60
60
|
network: "online" | "offline" | "unknown";
|
|
61
61
|
reconnecting: boolean;
|
|
62
|
+
error: string | null;
|
|
63
|
+
micDenied: boolean;
|
|
62
64
|
}
|
|
63
65
|
export declare const CAPTION_HISTORY = 12;
|
|
64
66
|
export declare function GraineVoiceLauncher({ webView: WebView, autoStart, active: activeProp, transport, onTransport, onCaption, onCallState, onMicDenied, onError, timing, onEnded, children, }: GraineVoiceLauncherProps): React.JSX.Element;
|
|
@@ -20,6 +20,8 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
20
20
|
const [connected, setConnected] = useState(false);
|
|
21
21
|
const [connecting, setConnecting] = useState(false);
|
|
22
22
|
const [muted, setMutedState] = useState(false);
|
|
23
|
+
const [fault, setFault] = useState(null);
|
|
24
|
+
const [micDenied, setMicDenied] = useState(false);
|
|
23
25
|
const [ready, setReady] = useState(false);
|
|
24
26
|
const readyRef = useRef(false);
|
|
25
27
|
readyRef.current = ready;
|
|
@@ -214,6 +216,7 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
214
216
|
}
|
|
215
217
|
if (!(await ensureMic())) {
|
|
216
218
|
setConnecting(false);
|
|
219
|
+
setMicDenied(true);
|
|
217
220
|
onMicDenied?.("NotAllowedError");
|
|
218
221
|
client.emit("mic_denied", { reason: "NotAllowedError" });
|
|
219
222
|
return;
|
|
@@ -260,6 +263,7 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
260
263
|
message: msg.message,
|
|
261
264
|
});
|
|
262
265
|
if (msg.reason === "not_configured" && msg.message) {
|
|
266
|
+
setFault(String(msg.message));
|
|
263
267
|
onError?.(String(msg.message));
|
|
264
268
|
}
|
|
265
269
|
return;
|
|
@@ -346,6 +350,7 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
346
350
|
return;
|
|
347
351
|
case "graine:mic-denied":
|
|
348
352
|
client.emit("mic_denied", { reason: String(msg.reason || "NotAllowedError") });
|
|
353
|
+
setMicDenied(true);
|
|
349
354
|
onMicDenied?.(String(msg.reason || "denied"));
|
|
350
355
|
return;
|
|
351
356
|
case "graine:widget": {
|
|
@@ -387,6 +392,8 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
387
392
|
clearCaptions: useCallback(() => setCaptions([]), []),
|
|
388
393
|
start: useCallback(() => {
|
|
389
394
|
setConnecting(true);
|
|
395
|
+
setMicDenied(false);
|
|
396
|
+
setFault(null);
|
|
390
397
|
setCaptions([]);
|
|
391
398
|
setAwaitingAgent(false);
|
|
392
399
|
client.clearPendingWidget();
|
|
@@ -402,6 +409,8 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
402
409
|
quality,
|
|
403
410
|
network,
|
|
404
411
|
reconnecting,
|
|
412
|
+
error: fault,
|
|
413
|
+
micDenied,
|
|
405
414
|
setMuted: useCallback((m) => {
|
|
406
415
|
setMutedState(m);
|
|
407
416
|
post({ type: "graine:set-muted", muted: m });
|
|
@@ -449,6 +458,8 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
449
458
|
setConnected(false);
|
|
450
459
|
setConnecting(false);
|
|
451
460
|
setCaptions([]);
|
|
461
|
+
setMicDenied(false);
|
|
462
|
+
setFault(null);
|
|
452
463
|
client.clearPendingWidget();
|
|
453
464
|
onCallState?.({ connected: false, connecting: false });
|
|
454
465
|
}, [active]);
|
package/dist/theme.d.ts
CHANGED
|
@@ -170,6 +170,11 @@ export declare const LAUNCHER_ICONS: ReadonlyArray<{
|
|
|
170
170
|
path: string;
|
|
171
171
|
stroke: boolean;
|
|
172
172
|
}>;
|
|
173
|
+
export type SdkIconName = "mic" | "micOff" | "end" | "send" | "chat" | "call" | "chevron" | "close";
|
|
174
|
+
export declare const SDK_ICONS: Readonly<Record<SdkIconName, {
|
|
175
|
+
body?: string;
|
|
176
|
+
lines?: string;
|
|
177
|
+
}>>;
|
|
173
178
|
export declare const DESIGN_FONTS: ReadonlyArray<{
|
|
174
179
|
family: string;
|
|
175
180
|
category: "sans" | "serif" | "mono" | "display";
|
package/dist/theme.js
CHANGED
|
@@ -78,6 +78,16 @@ export const LAUNCHER_ICONS = [
|
|
|
78
78
|
{ id: "chat-dots-filled", label: "Chat dots (filled)", stroke: false, path: "M12 2.25A9.75 9.75 0 0 0 3.392 16.582L2.328 19.775a1.5 1.5 0 0 0 1.897 1.897l3.192 -1.064A9.75 9.75 0 1 0 12 2.25ZM7.875 13.125a1.125 1.125 0 1 1 1.125 -1.125A1.125 1.125 0 0 1 7.875 13.125Zm4.125 0a1.125 1.125 0 1 1 1.125 -1.125A1.125 1.125 0 0 1 12 13.125Zm4.125 0a1.125 1.125 0 1 1 1.125 -1.125A1.125 1.125 0 0 1 16.125 13.125Z" },
|
|
79
79
|
{ id: "graphic-eq", label: "Voice equalizer (filled)", stroke: false, path: "M7 17v-10q0 -0.425 0.288 -0.713T8 6q0.425 0 0.713 0.288T9 7v10q0 0.425 -0.288 0.713T8 18q-0.425 0 -0.713 -0.288T7 17Zm4 4v-18q0 -0.425 0.288 -0.713T12 2q0.425 0 0.713 0.288T13 3v18q0 0.425 -0.288 0.713T12 22q-0.425 0 -0.713 -0.288T11 21ZM3 13v-2q0 -0.425 0.288 -0.713T4 10q0.425 0 0.713 0.288T5 11v2q0 0.425 -0.288 0.713T4 14q-0.425 0 -0.713 -0.288T3 13Zm12 4v-10q0 -0.425 0.288 -0.713T16 6q0.425 0 0.713 0.288T17 7v10q0 0.425 -0.288 0.713T16 18q-0.425 0 -0.713 -0.288T15 17Zm4 -4v-2q0 -0.425 0.288 -0.713T20 10q0.425 0 0.713 0.288T21 11v2q0 0.425 -0.288 0.713T20 14q-0.425 0 -0.713 -0.288T19 13Z" },
|
|
80
80
|
];
|
|
81
|
+
export const SDK_ICONS = {
|
|
82
|
+
mic: { body: "M12 2.5a3.5 3.5 0 0 0-3.5 3.5v6a3.5 3.5 0 0 0 7 0V6A3.5 3.5 0 0 0 12 2.5z", lines: "M18.5 11.5a6.5 6.5 0 0 1-13 0M12 18v3.5" },
|
|
83
|
+
micOff: { body: "M12 2.5a3.5 3.5 0 0 0-3.5 3.5v6a3.5 3.5 0 0 0 7 0V6A3.5 3.5 0 0 0 12 2.5z", lines: "M18.5 11.5a6.5 6.5 0 0 1-13 0M12 18v3.5M4 4l16 16" },
|
|
84
|
+
end: { body: "M4.2 14.6c4.6-4.1 11-4.1 15.6 0 .6.5.6 1.4.1 2l-1.3 1.4c-.4.5-1.1.6-1.7.3l-2.2-1.2c-.5-.3-.8-.8-.7-1.4l.2-1.3a11 11 0 0 0-4.4 0l.2 1.3c.1.6-.2 1.1-.7 1.4l-2.2 1.2c-.6.3-1.3.2-1.7-.3L4.1 16.6c-.5-.6-.5-1.5.1-2z" },
|
|
85
|
+
send: { body: "M4 12 20 4l-4 16-4.5-6.5L4 12z", lines: "M11.5 13.5 20 4" },
|
|
86
|
+
chat: { body: "M20 12a8 8 0 0 1-11.6 7.1L4 20l1-4.2A8 8 0 1 1 20 12z" },
|
|
87
|
+
call: { lines: "M13.832 16.568a1 1 0 0 0 1.213 -0.303l0.355 -0.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1 -2 2A18 18 0 0 1 2 4a2 2 0 0 1 2 -2h3a2 2 0 0 1 2 2v3a2 2 0 0 1 -0.8 1.6l-0.468 0.351a1 1 0 0 0 -0.292 1.233a14 14 0 0 0 6.392 6.384" },
|
|
88
|
+
chevron: { lines: "M6 15l6-6 6 6" },
|
|
89
|
+
close: { lines: "M6 6l12 12M18 6 6 18" },
|
|
90
|
+
};
|
|
81
91
|
export const DESIGN_FONTS = [
|
|
82
92
|
{ family: "system", category: "sans", scripts: ["latin", "devanagari", "bengali", "tamil", "telugu", "kannada", "malayalam", "gujarati", "gurmukhi", "oriya"] },
|
|
83
93
|
{ family: "Inter", category: "sans", scripts: ["latin", "latin-ext", "cyrillic", "cyrillic-ext", "greek", "greek-ext", "vietnamese"] },
|
package/package.json
CHANGED