@graineai/inapp-react-native 0.39.0 → 0.40.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/dist/index.d.ts CHANGED
@@ -35,5 +35,5 @@ export { GraineLauncherButton, type GraineLauncherButtonProps } from "./react-na
35
35
  export { faceFor, facesForDesign, fontUrl, FONT_ASSET_VERSION, type FontLoader, type FontFace } from "./react-native/fonts.js";
36
36
  export { GraineVoiceLauncher, CAPTION_HISTORY, type GraineVoiceLauncherProps, type GraineVoiceApi, } from "./react-native/voice-launcher.js";
37
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, DESIGN_CHROMES, designPreset, normaliseDesignConfig, designFromAppearance, resolveTokens, resolveDesign, pickTokens, contrastRatio, readableOn, glyphUrl, type DesignConfig, type DesignPreset, type ThemeTokens, type ThemeColors, type ThemeShadow, type ResolvedDesign, type DesignBubbles, type DesignChrome, type LauncherShape, type MotionEasing, type IconStyle, type SdkIconName, } from "./theme.js";
38
+ export { DESIGN_PRESETS, DEFAULT_DESIGN_PRESET, DESIGN_FONTS, LAUNCHER_ICONS, SDK_ICONS, DESIGN_CHROMES, LOGO_PLATES, LOGO_PLATE_WHITE, normaliseLogoPlate, logoBox, logoPlateLook, designPreset, normaliseDesignConfig, designFromAppearance, resolveTokens, resolveDesign, pickTokens, contrastRatio, readableOn, glyphUrl, type DesignConfig, type DesignPreset, type ThemeTokens, type ThemeColors, type ThemeShadow, type ResolvedDesign, type DesignBubbles, type DesignChrome, type LogoPlate, type LauncherShape, type MotionEasing, type IconStyle, type SdkIconName, } from "./theme.js";
39
39
  export { sessionDesign } from "./design.js";
package/dist/index.js CHANGED
@@ -26,5 +26,5 @@ export { GraineLauncherButton } from "./react-native/fab.js";
26
26
  export { faceFor, facesForDesign, fontUrl, FONT_ASSET_VERSION } from "./react-native/fonts.js";
27
27
  export { GraineVoiceLauncher, CAPTION_HISTORY, } from "./react-native/voice-launcher.js";
28
28
  export { LauncherVisibilityTracker, activeRouteName, } from "./react-native/navigation.js";
29
- export { DESIGN_PRESETS, DEFAULT_DESIGN_PRESET, DESIGN_FONTS, LAUNCHER_ICONS, SDK_ICONS, DESIGN_CHROMES, 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, DESIGN_CHROMES, LOGO_PLATES, LOGO_PLATE_WHITE, normaliseLogoPlate, logoBox, logoPlateLook, designPreset, normaliseDesignConfig, designFromAppearance, resolveTokens, resolveDesign, pickTokens, contrastRatio, readableOn, glyphUrl, } from "./theme.js";
30
30
  export { sessionDesign } from "./design.js";
@@ -1,7 +1,9 @@
1
+ import { type LogoPlate } from "../theme.js";
1
2
  export interface NativeAppearance {
2
3
  accent: string;
3
4
  heading: string;
4
5
  logoUrl: string | null;
6
+ logoPlate: LogoPlate;
5
7
  radius: number;
6
8
  }
7
9
  export interface NativeAppearanceFallback {
@@ -1,3 +1,4 @@
1
+ import { normaliseLogoPlate } from "../theme.js";
1
2
  const HEX = /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
2
3
  const RADIUS_CEILING = 48;
3
4
  const DEFAULT_ORIGIN = "https://www.graine.ai";
@@ -25,7 +26,7 @@ export function resolveNativeAppearance(appearance, fallback) {
25
26
  const origin = String(fallback.origin || DEFAULT_ORIGIN).replace(/\/+$/, "");
26
27
  const logoUrl = logoKey ? `${origin}/api/embed/logo?k=${encodeURIComponent(logoKey)}` : null;
27
28
  const radius = clamp(numeric(a.radius) ?? numeric(fallback.radius) ?? 18, max);
28
- return { accent, heading, logoUrl, radius };
29
+ return { accent, heading, logoUrl, logoPlate: normaliseLogoPlate(a.logoPlate), radius };
29
30
  }
30
31
  const LIGHT = { surface: "#FFFFFF", border: "#E5E4E0", text: "#101318", muted: "#5F6672" };
31
32
  const DARK = { surface: "#17181B", border: "#2C2E33", text: "#F5F5F4", muted: "#A0A5AE" };
@@ -7,7 +7,7 @@ import { GraineAgentBar } from "./ui.js";
7
7
  import { resolveNativeAppearance } from "./appearance.js";
8
8
  import { useDesign } from "./use-design.js";
9
9
  import { glyphPixels, launcherRadius, onAccentFor, shadowStyle } from "./design-math.js";
10
- import { glyphUrl } from "../theme.js";
10
+ import { glyphUrl, LOGO_PLATE_WHITE, logoBox, logoPlateLook, readableOn } from "../theme.js";
11
11
  import { UNNAMED_TAP } from "../protocol.js";
12
12
  function AboveNativeScreens({ overlay: Overlay, children, }) {
13
13
  if (!Overlay)
@@ -69,15 +69,18 @@ export function GraineFab({ webView, position = "bottom-right", size = 56, accen
69
69
  return null;
70
70
  const side = position === "bottom-left" ? { left: launcherInset.right } : { right: launcherInset.right };
71
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?.(); } }));
72
+ return (_jsx(DesignedLauncher, { look: look, size: size, accent: accentProp ?? look.tokens.colors.accent, name: name, label: label ?? `Talk to ${name}`, logoUrl: dashboard.logoUrl, logoPlate: dashboard.logoPlate, origin: client.opts.baseUrl, place: { position: "absolute", bottom: launcherInset.bottom, ...side }, onPress: () => { setOpen(true); api.start(); onOpen?.(); } }));
73
73
  }
74
74
  return (_jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: label ?? `Talk to ${name}`, onPress: () => { setOpen(true); api.start(); onOpen?.(); }, style: ({ pressed }) => ({
75
75
  position: "absolute", bottom: launcherInset.bottom, ...side,
76
- width: size, height: size, borderRadius: size / 2, backgroundColor: dashboardAccent,
76
+ width: size, height: size, borderRadius: size / 2,
77
+ ...(dashboard.logoUrl && dashboard.logoPlate === "white"
78
+ ? { backgroundColor: LOGO_PLATE_WHITE, borderWidth: 1, borderColor: "#E5E7EB" }
79
+ : { backgroundColor: dashboardAccent }),
77
80
  alignItems: "center", justifyContent: "center",
78
81
  opacity: pressed ? 0.85 : 1,
79
82
  elevation: 6, shadowColor: "#000", shadowOpacity: 0.25, shadowRadius: 8, shadowOffset: { width: 0, height: 4 },
80
- }), children: dashboard.logoUrl ? (_jsx(Image, { source: { uri: dashboard.logoUrl }, style: { width: size * 0.6, height: size * 0.6, borderRadius: size * 0.3 } })) : (_jsx(Text, { style: { color: "#fff", fontSize: size * 0.4, fontWeight: "700" }, children: name.trim().charAt(0).toUpperCase() || "•" })) }));
83
+ }), children: dashboard.logoUrl ? (_jsx(Image, { source: { uri: dashboard.logoUrl }, resizeMode: "contain", style: { width: logoBox("circle", size), height: logoBox("circle", size) }, accessibilityIgnoresInvertColors: true })) : (_jsx(Text, { style: { color: "#fff", fontSize: size * 0.4, fontWeight: "700" }, children: name.trim().charAt(0).toUpperCase() || "•" })) }));
81
84
  };
82
85
  if (!prewarm && !open)
83
86
  return _jsx(AboveNativeScreens, { overlay: overlay, children: button({ start: () => { } }) });
@@ -87,9 +90,9 @@ export function GraineLauncherButton({ onPress, style, label, size = 56 }) {
87
90
  const { appearance, client } = useGraineAgent();
88
91
  const look = useDesign();
89
92
  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 }));
93
+ return (_jsx(DesignedLauncher, { look: look, size: size, accent: look.tokens.colors.accent, name: dashboard.heading, label: label ?? `Talk to ${dashboard.heading}`, logoUrl: dashboard.logoUrl, logoPlate: dashboard.logoPlate, origin: client.opts.baseUrl, place: style ?? {}, onPress: onPress }));
91
94
  }
92
- function DesignedLauncher({ look, size, accent, name, label, logoUrl, origin, place, onPress, }) {
95
+ function DesignedLauncher({ look, size, accent, name, label, logoUrl, logoPlate = "accent", origin, place, onPress, }) {
93
96
  const t = look.tokens;
94
97
  const [failed, setFailed] = useState([]);
95
98
  const fail = (uri) => setFailed((cur) => (cur.includes(uri) ? cur : [...cur, uri]));
@@ -113,11 +116,12 @@ function DesignedLauncher({ look, size, accent, name, label, logoUrl, origin, pl
113
116
  const glyph = glyphUri && !failed.includes(glyphUri) ? glyphUri : null;
114
117
  const logo = logoUrl && !failed.includes(logoUrl) ? logoUrl : null;
115
118
  const hard = t.elevation.hardOffset;
119
+ const plate = logoPlateLook(!glyph && logo ? logoPlate : "accent", { ...t.colors, accent }, t.shape.borderWidth);
116
120
  const corners = {
117
121
  borderRadius: radius,
118
122
  ...(t.shape.launcher === "squircle" && Platform.OS === "ios" ? { borderCurve: "continuous" } : null),
119
123
  };
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: {
124
+ 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: logoBox(pill ? "circle" : t.shape.launcher, size), height: logoBox(pill ? "circle" : t.shape.launcher, size) }, onError: () => fail(logo), accessibilityIgnoresInvertColors: true }, logo)) : (_jsx(Text, { style: {
121
125
  color: onAccent, fontSize: Math.round(size * 0.46), fontWeight: String(t.font.weights.title),
122
126
  fontFamily: look.font(t.font.heading),
123
127
  }, children: name.trim().charAt(0).toUpperCase() || "\u2022" }));
@@ -126,11 +130,12 @@ function DesignedLauncher({ look, size, accent, name, label, logoUrl, origin, pl
126
130
  height: size, minWidth: size, width: pill ? undefined : size,
127
131
  flexDirection: "row", alignItems: "center", justifyContent: "center",
128
132
  gap: pill ? 8 : 0, paddingHorizontal: pill ? Math.round(size * 0.32) : 0,
129
- backgroundColor: accent,
133
+ backgroundColor: plate.fill,
134
+ ...(plate.outline && !hard ? { borderWidth: plate.outline.width, borderColor: plate.outline.color } : null),
130
135
  opacity: pressed ? 0.85 : 1,
131
136
  ...(hard ? { borderWidth: t.shape.borderWidth, borderColor: t.colors.text } : shadowStyle(t.elevation.floating)),
132
137
  }), children: [mark, pill ? (_jsx(Text, { numberOfLines: 1, style: {
133
- color: onAccent, fontSize: t.font.scale.body, fontWeight: String(t.font.weights.strong),
138
+ color: plate.outline ? readableOn(LOGO_PLATE_WHITE) : onAccent, fontSize: t.font.scale.body, fontWeight: String(t.font.weights.strong),
134
139
  fontFamily: look.font(t.font.body),
135
140
  }, children: name })) : null] })] }));
136
141
  }
@@ -2,19 +2,29 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
2
2
  import React, { useContext, useEffect, useRef, useState } from "react";
3
3
  import { Animated, Easing, Image, PixelRatio, Platform, Pressable, ScrollView, Text, TextInput, View } from "react-native";
4
4
  import { Ctx, useGraineAgent } from "./index.js";
5
- import { GraineLoader } from "./loader.js";
5
+ import { GraineLoader, useGraineLoader } from "./loader.js";
6
6
  import { GraineWidget } from "./widget.js";
7
7
  import { resolveNativeAppearance } from "./appearance.js";
8
8
  import { useDesign } from "./use-design.js";
9
9
  import { glyphPixels, launcherRadius, letterSpacing, lineHeight, shadowStyle, surfaceFill } from "./design-math.js";
10
10
  import { useKeyboardLift } from "./keyboard.js";
11
11
  import { useDraggablePlacement } from "./placement.js";
12
- import { glyphUrl, SDK_ICONS } from "../theme.js";
12
+ import { glyphUrl, logoBox, logoPlateLook, SDK_ICONS } from "../theme.js";
13
+ import { DEFAULT_LOADER_TEXT } from "../protocol.js";
13
14
  import { TitleBar, windowed } from "./window-chrome.js";
14
15
  const BAR_HEIGHT = 60;
15
16
  const BUTTON = 38;
16
17
  const METER = [0.55, 1, 0.7, 0.4];
17
- function statusOf(api, isChat, chatConnected) {
18
+ function connectingLook(api, connectingText) {
19
+ if (api.connectStage === "retrying" || api.connectStage === "switching") {
20
+ return { text: "Reconnecting…", waiting: "reconnecting" };
21
+ }
22
+ if (api.slowConnect && connectingText === DEFAULT_LOADER_TEXT.connecting) {
23
+ return { text: "Still connecting…", waiting: "connecting", label: "Still connecting…" };
24
+ }
25
+ return { text: connectingText, waiting: "connecting" };
26
+ }
27
+ function statusOf(api, isChat, chatConnected, connectingText = DEFAULT_LOADER_TEXT.connecting) {
18
28
  const conn = api.connectivity?.state;
19
29
  const say = (text) => ({ text, waiting: null, meter: false });
20
30
  if (api.error)
@@ -29,8 +39,10 @@ function statusOf(api, isChat, chatConnected) {
29
39
  return say("Needs microphone");
30
40
  if (conn === "failed")
31
41
  return say("Couldn’t load · tap to retry");
42
+ if (api.callFailed)
43
+ return say("Couldn’t connect · tap to retry");
32
44
  if (!api.ready || api.connecting)
33
- return { text: "Connecting…", waiting: "connecting", meter: false };
45
+ return { ...connectingLook(api, connectingText), meter: false };
34
46
  if (api.muted)
35
47
  return say("Muted");
36
48
  if (api.connected && api.phase === "thinking")
@@ -63,7 +75,8 @@ export function GraineVoiceBar({ api, onClose, bottomInset, draggable = true, pl
63
75
  initialOffset: placement?.bottomOffset,
64
76
  onChange: (bottomOffset) => onPlacementChange?.({ bottomOffset }),
65
77
  });
66
- const status = statusOf(api, isChat, chatConnected);
78
+ const loader = useGraineLoader();
79
+ const status = statusOf(api, isChat, chatConnected, loader.text.connecting);
67
80
  const conn = api.connectivity?.state;
68
81
  const micBlocked = !!api.error || api.micDenied || api.connecting || (!api.ready && conn !== "failed");
69
82
  const caption = !isChat && api.captionsOn && api.caption?.text ? api.caption : null;
@@ -79,11 +92,11 @@ export function GraineVoiceBar({ api, onClose, bottomInset, draggable = true, pl
79
92
  return (_jsxs(View, { pointerEvents: "box-none", ...drag.panHandlers, onLayout: drag.onLayout, style: {
80
93
  position: "absolute", zIndex: 900, gap: 8, left: 12, right: 12,
81
94
  bottom: baseBottom + drag.offset + keyboardLift,
82
- }, 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: {
95
+ }, children: [_jsx(WidgetGuard, { children: _jsx(GraineWidget, {}) }), isChat ? (_jsx(ChatWindow, { look: look, kit: kit, origin: origin, heading: heading, logoUrl: dash.logoUrl ?? undefined, logoPlate: dash.logoPlate, 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: {
83
96
  height: BAR_HEIGHT, paddingHorizontal: 9, flexDirection: "row", alignItems: "center", gap: 10,
84
97
  backgroundColor: surfaceFill(c.surface, t), borderWidth: t.shape.borderWidth, borderColor: c.border,
85
98
  borderRadius: t.shape.radius.xl, ...softShadow(t, "floating"),
86
- }, 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: () => {
99
+ }, children: [_jsx(View, { style: { width: BUTTON, height: BUTTON, borderRadius: BUTTON / 2, ...markFill(t, dash.logoPlate, !t.icons.launcher && !!dash.logoUrl), 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" }) }), _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, label: status.label, 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: () => {
87
100
  if (conn === "failed") {
88
101
  api.retry();
89
102
  return;
@@ -125,7 +138,13 @@ function Icon({ name, t, color, size = 18, kit, rotate }) {
125
138
  const common = { stroke: color, strokeLinecap: sharp ? "square" : "round", strokeLinejoin: sharp ? "miter" : "round" };
126
139
  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] }) }));
127
140
  }
128
- function Mark({ t, size, glyph, kit, origin, logoUrl, initial, cover = false, }) {
141
+ function markFill(t, plate, showsLogo) {
142
+ const look = logoPlateLook(showsLogo ? plate : "accent", t.colors, t.shape.borderWidth);
143
+ return look.outline
144
+ ? { backgroundColor: look.fill, borderWidth: look.outline.width, borderColor: look.outline.color }
145
+ : { backgroundColor: look.fill };
146
+ }
147
+ function Mark({ t, size, glyph, kit, origin, logoUrl, initial, shape = "circle", }) {
129
148
  const on = t.colors.onAccent;
130
149
  const g = t.icons.launcher;
131
150
  if (g) {
@@ -136,7 +155,8 @@ function Mark({ t, size, glyph, kit, origin, logoUrl, initial, cover = false, })
136
155
  return (_jsx(Image, { source: { uri: glyphUrl(origin, g.id, on, glyphPixels(size, PixelRatio.get()), t.icons.stroke) }, style: { width: glyph, height: glyph }, accessibilityIgnoresInvertColors: true }));
137
156
  }
138
157
  if (logoUrl) {
139
- return _jsx(Image, { source: { uri: logoUrl }, style: cover ? { width: size, height: size } : { width: glyph, height: glyph }, resizeMode: cover ? "cover" : "contain", accessibilityIgnoresInvertColors: true });
158
+ const box = logoBox(shape, size);
159
+ return _jsx(Image, { source: { uri: logoUrl }, style: { width: box, height: box }, resizeMode: "contain", accessibilityIgnoresInvertColors: true });
140
160
  }
141
161
  return _jsx(Text, { style: { color: on, fontSize: Math.round(size * 0.4), fontWeight: "700" }, children: initial });
142
162
  }
@@ -191,7 +211,7 @@ function CaptionCard({ look, role, text, live }) {
191
211
  borderRadius: t.shape.radius.lg, paddingHorizontal: 12, paddingVertical: 8, ...softShadow(t, "card"),
192
212
  }, children: [win ? (_jsx(View, { style: { marginBottom: 6 }, children: _jsx(TitleBar, { t: t, title: "captions", size: "caption", radius: t.shape.radius.lg, bleed: { horizontal: 12, top: 8 } }) })) : null, 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 })] }) }) }));
193
213
  }
194
- function ChatWindow({ look, kit, origin, heading, logoUrl, messages, send, online, lifted, onCollapse, }) {
214
+ function ChatWindow({ look, kit, origin, heading, logoUrl, logoPlate = "accent", messages, send, online, lifted, onCollapse, }) {
195
215
  const t = look.tokens;
196
216
  const c = t.colors;
197
217
  const r = t.shape.radius;
@@ -217,7 +237,7 @@ function ChatWindow({ look, kit, origin, heading, logoUrl, messages, send, onlin
217
237
  letterSpacing: letterSpacing(size, t.font.letterSpacing.body),
218
238
  });
219
239
  const titleSize = Math.min(t.font.scale.title, 16);
220
- 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: [windowed(t) ? (_jsx(TitleBar, { t: t, title: heading, online: online, radius: r.xl, right: _jsx(Pressable, { onPress: onCollapse, hitSlop: 10, accessibilityRole: "button", accessibilityLabel: "Collapse", children: _jsx(Icon, { name: "chevron", t: t, color: c.textMuted, kit: kit, size: 16, rotate: true }) }) })) : (_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) => {
240
+ 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: [windowed(t) ? (_jsx(TitleBar, { t: t, title: heading, online: online, radius: r.xl, right: _jsx(Pressable, { onPress: onCollapse, hitSlop: 10, accessibilityRole: "button", accessibilityLabel: "Collapse", children: _jsx(Icon, { name: "chevron", t: t, color: c.textMuted, kit: kit, size: 16, rotate: true }) }) })) : (_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), ...markFill(t, logoPlate, !t.icons.launcher && !!logoUrl), 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", shape: markShape }) }), _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) => {
221
241
  const b = bubble(t, m.role !== "agent");
222
242
  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));
223
243
  })] }), _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: {
@@ -31,6 +31,7 @@ export interface GraineVoiceLauncherProps {
31
31
  bootMs?: number;
32
32
  hardCapMs?: number;
33
33
  endWatchdogMs?: number;
34
+ slowConnectMs?: number;
34
35
  };
35
36
  children?: (api: GraineVoiceApi) => React.ReactNode;
36
37
  }
@@ -61,6 +62,10 @@ export interface GraineVoiceApi {
61
62
  reconnecting: boolean;
62
63
  error: string | null;
63
64
  micDenied: boolean;
65
+ connectStage: "registering" | "dialling" | "retrying" | "switching" | null;
66
+ slowConnect: boolean;
67
+ callFailed: boolean;
64
68
  }
65
69
  export declare const CAPTION_HISTORY = 12;
70
+ export declare const SLOW_CONNECT_MS = 4000;
66
71
  export declare function GraineVoiceLauncher({ webView: WebView, autoStart, active: activeProp, transport, onTransport, onCaption, onCallState, onMicDenied, onError, timing, onEnded, children, }: GraineVoiceLauncherProps): React.JSX.Element;
@@ -4,6 +4,7 @@ import { AppState, View } from "react-native";
4
4
  import { useGraineAgent } from "./index.js";
5
5
  import { requestMicrophonePermission } from "./permissions.js";
6
6
  export const CAPTION_HISTORY = 12;
7
+ export const SLOW_CONNECT_MS = 4000;
7
8
  export function GraineVoiceLauncher({ webView: WebView, autoStart = false, active: activeProp = true, transport, onTransport, onCaption, onCallState, onMicDenied, onError, timing, onEnded, children, }) {
8
9
  const { client, appearance, surface, surfaces } = useGraineAgent();
9
10
  const active = activeProp && surface !== "chat";
@@ -22,6 +23,9 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
22
23
  const [muted, setMutedState] = useState(false);
23
24
  const [fault, setFault] = useState(null);
24
25
  const [micDenied, setMicDenied] = useState(false);
26
+ const [connectStage, setConnectStage] = useState(null);
27
+ const [slowConnect, setSlowConnect] = useState(false);
28
+ const [callFailed, setCallFailed] = useState(false);
25
29
  const [ready, setReady] = useState(false);
26
30
  const readyRef = useRef(false);
27
31
  readyRef.current = ready;
@@ -291,6 +295,8 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
291
295
  if (msg.connected) {
292
296
  endedNotifiedRef.current = false;
293
297
  setReconnecting(false);
298
+ setConnectStage(null);
299
+ setCallFailed(false);
294
300
  }
295
301
  setConnected(!!msg.connected);
296
302
  setConnecting(!!msg.connecting);
@@ -299,6 +305,22 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
299
305
  startedRef.current = false;
300
306
  onCallState?.({ connected: !!msg.connected, connecting: !!msg.connecting });
301
307
  return;
308
+ case "graine:progress": {
309
+ const stage = String(msg.stage || "");
310
+ if (stage === "failed") {
311
+ setConnectStage(null);
312
+ setConnecting(false);
313
+ setCallFailed(true);
314
+ startedRef.current = false;
315
+ pendingStartRef.current = false;
316
+ onCallState?.({ connected: false, connecting: false });
317
+ return;
318
+ }
319
+ if (stage === "registering" || stage === "dialling" || stage === "retrying" || stage === "switching") {
320
+ setConnectStage(stage);
321
+ }
322
+ return;
323
+ }
302
324
  case "graine:muted":
303
325
  setMutedState(!!msg.muted);
304
326
  return;
@@ -372,6 +394,15 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
372
394
  }
373
395
  }, [autoStart, beginCall, client, onCaption, onCallState, onMicDenied, onTransport, onError, onEnded, post, pushContext, foldCaption]);
374
396
  const pendingStartRef = useRef(false);
397
+ const waitingOnConnect = active && !connected && !callFailed && (connecting || !ready);
398
+ useEffect(() => {
399
+ if (!waitingOnConnect) {
400
+ setSlowConnect(false);
401
+ return;
402
+ }
403
+ const t = setTimeout(() => setSlowConnect(true), timing?.slowConnectMs ?? SLOW_CONNECT_MS);
404
+ return () => clearTimeout(t);
405
+ }, [waitingOnConnect]);
375
406
  const lastCaption = captions.length ? captions[captions.length - 1] : null;
376
407
  const phase = reconnecting || connectivity.state === "failed" || connectivity.state === "retrying" ? "reconnecting"
377
408
  : connecting || (!ready && !!voiceAgentId) ? "connecting"
@@ -394,6 +425,8 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
394
425
  setConnecting(true);
395
426
  setMicDenied(false);
396
427
  setFault(null);
428
+ setCallFailed(false);
429
+ setConnectStage(null);
397
430
  setCaptions([]);
398
431
  setAwaitingAgent(false);
399
432
  client.clearPendingWidget();
@@ -411,6 +444,9 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
411
444
  reconnecting,
412
445
  error: fault,
413
446
  micDenied,
447
+ connectStage,
448
+ slowConnect,
449
+ callFailed,
414
450
  setMuted: useCallback((m) => {
415
451
  setMutedState(m);
416
452
  post({ type: "graine:set-muted", muted: m });
@@ -460,6 +496,8 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
460
496
  setCaptions([]);
461
497
  setMicDenied(false);
462
498
  setFault(null);
499
+ setCallFailed(false);
500
+ setConnectStage(null);
463
501
  client.clearPendingWidget();
464
502
  onCallState?.({ connected: false, connecting: false });
465
503
  }, [active]);
package/dist/theme.d.ts CHANGED
@@ -163,6 +163,18 @@ export declare const DESIGN_BUBBLES: ReadonlyArray<DesignBubbles>;
163
163
  export declare const LAUNCHER_SHAPES: ReadonlyArray<LauncherShape>;
164
164
  export declare const ICON_STYLES: ReadonlyArray<IconStyle>;
165
165
  export declare const DESIGN_CHROMES: ReadonlyArray<DesignChrome>;
166
+ export type LogoPlate = "accent" | "white";
167
+ export declare const LOGO_PLATES: ReadonlyArray<LogoPlate>;
168
+ export declare const LOGO_PLATE_WHITE = "#FFFFFF";
169
+ export declare function normaliseLogoPlate(value: unknown): LogoPlate;
170
+ export declare function logoBox(shape: string, size: number): number;
171
+ export declare function logoPlateLook(plate: LogoPlate, colors: Pick<ThemeColors, "accent" | "border">, borderWidth?: number): {
172
+ fill: string;
173
+ outline: {
174
+ width: number;
175
+ color: string;
176
+ } | null;
177
+ };
166
178
  export declare function normaliseHex(value: unknown): string | null;
167
179
  export declare function mixHex(a: string, b: string, t: number): string;
168
180
  export declare function luminanceOf(hex: string): number;
package/dist/theme.js CHANGED
@@ -7,6 +7,20 @@ export const DESIGN_BUBBLES = ["filled", "outline", "minimal"];
7
7
  export const LAUNCHER_SHAPES = ["circle", "squircle", "rounded-square", "square", "pill"];
8
8
  export const ICON_STYLES = ["outline", "filled", "duotone", "sharp"];
9
9
  export const DESIGN_CHROMES = ["none", "window"];
10
+ export const LOGO_PLATES = ["accent", "white"];
11
+ export const LOGO_PLATE_WHITE = "#FFFFFF";
12
+ export function normaliseLogoPlate(value) {
13
+ return value === "white" ? "white" : "accent";
14
+ }
15
+ export function logoBox(shape, size) {
16
+ const k = shape === "rounded-square" || shape === "square" ? 0.8 : 0.7;
17
+ return Math.max(1, Math.floor(size * k));
18
+ }
19
+ export function logoPlateLook(plate, colors, borderWidth = 1) {
20
+ return plate === "white"
21
+ ? { fill: LOGO_PLATE_WHITE, outline: { width: Math.max(1, borderWidth), color: colors.border } }
22
+ : { fill: colors.accent, outline: null };
23
+ }
10
24
  const HEX6 = /^#?([0-9a-f]{6})$/i;
11
25
  const HEX3 = /^#?([0-9a-f]{3})$/i;
12
26
  export function normaliseHex(value) {
@@ -79,6 +93,8 @@ export const LAUNCHER_ICONS = [
79
93
  { id: "buddy", label: "Buddy", stroke: true, path: "M12 3c4.97 0 8 3.03 8 8.5S16.97 21 12 21 4 17.97 4 11.5 7.03 3 12 3z M9.25 10.25v1.75 M14.75 10.25v1.75 M9.5 15.1c1.4 1.15 3.6 1.15 5 0" },
80
94
  { 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" },
81
95
  { 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" },
96
+ { id: "grain", label: "Grain", stroke: true, path: "M7.34 5.5H16.66M4.69 8.75H19.31M4 12H20M4.69 15.25H19.31M7.34 18.5H16.66" },
97
+ { id: "grain-voice", label: "Grain voice", stroke: true, path: "M8.5 5.5H15.5M5 8.75H19M7 12H17M3.5 15.25H20.5M8 18.5H16" },
82
98
  ];
83
99
  export const SDK_ICONS = {
84
100
  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" },
@@ -1878,10 +1894,735 @@ const P_HIGH_CONTRAST = {
1878
1894
  }
1879
1895
  }
1880
1896
  };
1881
- export const DESIGN_PRESETS = [GRAINE_CLASSIC, P_CHAT_NEUTRAL, P_MATERIAL_3, P_IOS_NATIVE, P_MINIMAL_MONO, P_FLUENT_2, P_CARBON, P_LIQUID_GLASS, P_SOFT_PLAYFUL, P_DESKTOP_BUDDY, P_NEO_BRUTALISM, P_EDITORIAL, P_LUXURY, P_RETRO_TERMINAL, P_WARM_EARTHY, P_HIGH_CONTRAST];
1897
+ const P_SIGNATURE = {
1898
+ id: "signature",
1899
+ name: "Signature",
1900
+ tagline: "Graine's own look — quiet greys, a slate-and-indigo voice, the grain.",
1901
+ family: "graine",
1902
+ inspiredBy: [
1903
+ "Graine's design system, after Sarvam's (sarvam.ai/brand) — neutral surfaces #FCFCFC / #E6E6E6, ink #1F1F1F, slate #2A2C33, indigo #4250D5, a 0 0 40px ambient shadow, ease cubic-bezier(0.2,0,0,1) 350ms; the grain motif is Graine's own",
1904
+ ],
1905
+ font: {
1906
+ body: "DM Sans",
1907
+ heading: "Newsreader",
1908
+ scale: { caption: 12, body: 15, title: 19, display: 30 },
1909
+ weights: { body: 400, strong: 500, title: 400 },
1910
+ letterSpacing: { body: 0, title: -0.015 },
1911
+ lineHeight: { body: 1.5, title: 1.2 },
1912
+ },
1913
+ shape: { radius: { sm: 10, md: 16, lg: 24, xl: 32 }, borderWidth: 1, launcher: "circle" },
1914
+ spacing: { unit: 4, cardPadding: 16, gap: 10 },
1915
+ elevation: {
1916
+ card: { y: 0, blur: 40, spread: 0, opacity: 0.06, color: "#000000" },
1917
+ floating: { y: 12, blur: 40, spread: -8, opacity: 0.18, color: "#000000" },
1918
+ hardOffset: null,
1919
+ },
1920
+ motion: { durationMs: 350, easing: "emphasized", bounce: 0 },
1921
+ icons: { style: "outline", stroke: 1.75, launcher: "grain" },
1922
+ glass: null,
1923
+ bubbles: "filled",
1924
+ colors: {
1925
+ light: {
1926
+ background: "#FCFCFC", surface: "#FFFFFF", surfaceRaised: "#F5F5F5", text: "#1F1F1F", textMuted: "#666666",
1927
+ border: "#E6E6E6", accent: "#4250D5", onAccent: "#FFFFFF", success: "#496D21", warning: "#8F6418", danger: "#C43D2B",
1928
+ userBubble: "#2A2C33", userBubbleText: "#FFFFFF", agentBubble: "#F0F0F0", agentBubbleText: "#1F1F1F", scrim: "#000000",
1929
+ },
1930
+ dark: {
1931
+ background: "#141414", surface: "#1C1C1C", surfaceRaised: "#262626", text: "#F5F5F5", textMuted: "#A3A3A3",
1932
+ border: "#333333", accent: "#81A0E9", onAccent: "#11115B", success: "#90C85B", warning: "#FFB94A", danger: "#EBA18F",
1933
+ userBubble: "#E8EFFC", userBubbleText: "#11115B", agentBubble: "#262626", agentBubbleText: "#F5F5F5", scrim: "#000000",
1934
+ },
1935
+ },
1936
+ };
1937
+ const P_SWISS_GRID = {
1938
+ "id": "swiss-grid",
1939
+ "name": "Swiss Grid",
1940
+ "tagline": "Black type on white, square corners and hairline rules, with Swiss red for the customer's own messages.",
1941
+ "family": "modern",
1942
+ "inspiredBy": [
1943
+ "International Typographic Style (Swiss Style, 1950s)",
1944
+ "Josef Müller-Brockmann, Grid Systems in Graphic Design (1981)",
1945
+ "Swiss red, Pantone 485 C (#DA291C)"
1946
+ ],
1947
+ "font": {
1948
+ "body": "Inter",
1949
+ "heading": "Inter",
1950
+ "scale": {
1951
+ "caption": 12,
1952
+ "body": 15,
1953
+ "title": 21,
1954
+ "display": 34
1955
+ },
1956
+ "weights": {
1957
+ "body": 400,
1958
+ "strong": 600,
1959
+ "title": 700
1960
+ },
1961
+ "letterSpacing": {
1962
+ "body": -0.006,
1963
+ "title": -0.022
1964
+ },
1965
+ "lineHeight": {
1966
+ "body": 1.42,
1967
+ "title": 1.12
1968
+ }
1969
+ },
1970
+ "shape": {
1971
+ "radius": {
1972
+ "sm": 0,
1973
+ "md": 0,
1974
+ "lg": 0,
1975
+ "xl": 0
1976
+ },
1977
+ "borderWidth": 1,
1978
+ "launcher": "square"
1979
+ },
1980
+ "spacing": {
1981
+ "unit": 4,
1982
+ "cardPadding": 16,
1983
+ "gap": 8
1984
+ },
1985
+ "elevation": {
1986
+ "card": null,
1987
+ "floating": null,
1988
+ "hardOffset": null
1989
+ },
1990
+ "motion": {
1991
+ "durationMs": 160,
1992
+ "easing": "standard",
1993
+ "bounce": 0
1994
+ },
1995
+ "icons": {
1996
+ "style": "sharp",
1997
+ "stroke": 1.75,
1998
+ "launcher": "audio-lines"
1999
+ },
2000
+ "glass": null,
2001
+ "bubbles": "filled",
2002
+ "chrome": "none",
2003
+ "colors": {
2004
+ "light": {
2005
+ "background": "#FFFFFF",
2006
+ "surface": "#FFFFFF",
2007
+ "surfaceRaised": "#F3F3F3",
2008
+ "text": "#111111",
2009
+ "textMuted": "#545454",
2010
+ "border": "#1A1A1A",
2011
+ "accent": "#111111",
2012
+ "onAccent": "#FFFFFF",
2013
+ "success": "#1C7C3A",
2014
+ "warning": "#9E5200",
2015
+ "danger": "#DA291C",
2016
+ "userBubble": "#DA291C",
2017
+ "userBubbleText": "#FFFFFF",
2018
+ "agentBubble": "#F3F3F3",
2019
+ "agentBubbleText": "#111111",
2020
+ "scrim": "#000000"
2021
+ },
2022
+ "dark": {
2023
+ "background": "#0A0A0A",
2024
+ "surface": "#111111",
2025
+ "surfaceRaised": "#1C1C1C",
2026
+ "text": "#F5F5F5",
2027
+ "textMuted": "#A8A8A8",
2028
+ "border": "#D4D4D4",
2029
+ "accent": "#F5F5F5",
2030
+ "onAccent": "#0A0A0A",
2031
+ "success": "#3DBA68",
2032
+ "warning": "#F0A23B",
2033
+ "danger": "#DA291C",
2034
+ "userBubble": "#DA291C",
2035
+ "userBubbleText": "#FFFFFF",
2036
+ "agentBubble": "#1C1C1C",
2037
+ "agentBubbleText": "#F5F5F5",
2038
+ "scrim": "#000000"
2039
+ }
2040
+ }
2041
+ };
2042
+ const P_BAUHAUS = {
2043
+ "id": "bauhaus",
2044
+ "name": "Bauhaus",
2045
+ "tagline": "Circles and squares in the three primaries, set in geometric type on warm paper.",
2046
+ "family": "expressive",
2047
+ "inspiredBy": [
2048
+ "Bauhaus school, Weimar and Dessau (1919–1933)",
2049
+ "Kandinsky's 1923 colour–form questionnaire (yellow triangle, red square, blue circle)",
2050
+ "Herbert Bayer's universal alphabet (1925), a geometric sans"
2051
+ ],
2052
+ "font": {
2053
+ "body": "DM Sans",
2054
+ "heading": "Poppins",
2055
+ "scale": {
2056
+ "caption": 12,
2057
+ "body": 15,
2058
+ "title": 22,
2059
+ "display": 32
2060
+ },
2061
+ "weights": {
2062
+ "body": 400,
2063
+ "strong": 600,
2064
+ "title": 700
2065
+ },
2066
+ "letterSpacing": {
2067
+ "body": 0,
2068
+ "title": -0.01
2069
+ },
2070
+ "lineHeight": {
2071
+ "body": 1.45,
2072
+ "title": 1.15
2073
+ }
2074
+ },
2075
+ "shape": {
2076
+ "radius": {
2077
+ "sm": 0,
2078
+ "md": 24,
2079
+ "lg": 0,
2080
+ "xl": 30
2081
+ },
2082
+ "borderWidth": 0,
2083
+ "launcher": "circle"
2084
+ },
2085
+ "spacing": {
2086
+ "unit": 4,
2087
+ "cardPadding": 16,
2088
+ "gap": 10
2089
+ },
2090
+ "elevation": {
2091
+ "card": {
2092
+ "y": 2,
2093
+ "blur": 6,
2094
+ "spread": 0,
2095
+ "opacity": 0.1,
2096
+ "color": "#000000"
2097
+ },
2098
+ "floating": {
2099
+ "y": 10,
2100
+ "blur": 28,
2101
+ "spread": -6,
2102
+ "opacity": 0.2,
2103
+ "color": "#000000"
2104
+ },
2105
+ "hardOffset": null
2106
+ },
2107
+ "motion": {
2108
+ "durationMs": 200,
2109
+ "easing": "standard",
2110
+ "bounce": 0
2111
+ },
2112
+ "icons": {
2113
+ "style": "filled",
2114
+ "stroke": 2,
2115
+ "launcher": "graphic-eq"
2116
+ },
2117
+ "glass": null,
2118
+ "bubbles": "filled",
2119
+ "chrome": "none",
2120
+ "colors": {
2121
+ "light": {
2122
+ "background": "#F4EFE6",
2123
+ "surface": "#FBF8F2",
2124
+ "surfaceRaised": "#FFFFFF",
2125
+ "text": "#141414",
2126
+ "textMuted": "#55514A",
2127
+ "border": "#D8D0C1",
2128
+ "accent": "#21409A",
2129
+ "onAccent": "#FFFFFF",
2130
+ "success": "#2B7A3D",
2131
+ "warning": "#985A00",
2132
+ "danger": "#C62A1F",
2133
+ "userBubble": "#F2C230",
2134
+ "userBubbleText": "#141414",
2135
+ "agentBubble": "#ECE5D8",
2136
+ "agentBubbleText": "#141414",
2137
+ "scrim": "#000000"
2138
+ },
2139
+ "dark": {
2140
+ "background": "#121212",
2141
+ "surface": "#1B1B1B",
2142
+ "surfaceRaised": "#252525",
2143
+ "text": "#F4EFE6",
2144
+ "textMuted": "#B2ABA0",
2145
+ "border": "#3A3733",
2146
+ "accent": "#F2C230",
2147
+ "onAccent": "#141414",
2148
+ "success": "#5DBE6D",
2149
+ "warning": "#E8A33A",
2150
+ "danger": "#D0332A",
2151
+ "userBubble": "#2F4FB8",
2152
+ "userBubbleText": "#FFFFFF",
2153
+ "agentBubble": "#252525",
2154
+ "agentBubbleText": "#F4EFE6",
2155
+ "scrim": "#000000"
2156
+ }
2157
+ }
2158
+ };
2159
+ const P_PRECISION = {
2160
+ "id": "precision",
2161
+ "name": "Precision",
2162
+ "tagline": "Dark-first and dense: near-black neutrals, hairline borders and a single indigo accent.",
2163
+ "family": "modern",
2164
+ "inspiredBy": [
2165
+ "Linear, How we redesigned the Linear UI (2024)",
2166
+ "Inter and Inter Display by Rasmus Andersson"
2167
+ ],
2168
+ "font": {
2169
+ "body": "Inter",
2170
+ "heading": "Inter",
2171
+ "scale": {
2172
+ "caption": 12,
2173
+ "body": 14,
2174
+ "title": 17,
2175
+ "display": 24
2176
+ },
2177
+ "weights": {
2178
+ "body": 400,
2179
+ "strong": 500,
2180
+ "title": 600
2181
+ },
2182
+ "letterSpacing": {
2183
+ "body": -0.005,
2184
+ "title": -0.014
2185
+ },
2186
+ "lineHeight": {
2187
+ "body": 1.5,
2188
+ "title": 1.3
2189
+ }
2190
+ },
2191
+ "shape": {
2192
+ "radius": {
2193
+ "sm": 4,
2194
+ "md": 6,
2195
+ "lg": 8,
2196
+ "xl": 12
2197
+ },
2198
+ "borderWidth": 1,
2199
+ "launcher": "rounded-square"
2200
+ },
2201
+ "spacing": {
2202
+ "unit": 4,
2203
+ "cardPadding": 12,
2204
+ "gap": 8
2205
+ },
2206
+ "elevation": {
2207
+ "card": {
2208
+ "y": 1,
2209
+ "blur": 2,
2210
+ "spread": 0,
2211
+ "opacity": 0.12,
2212
+ "color": "#000000"
2213
+ },
2214
+ "floating": {
2215
+ "y": 12,
2216
+ "blur": 32,
2217
+ "spread": -8,
2218
+ "opacity": 0.28,
2219
+ "color": "#000000"
2220
+ },
2221
+ "hardOffset": null
2222
+ },
2223
+ "motion": {
2224
+ "durationMs": 140,
2225
+ "easing": "standard",
2226
+ "bounce": 0
2227
+ },
2228
+ "icons": {
2229
+ "style": "outline",
2230
+ "stroke": 1.5,
2231
+ "launcher": "sparkles"
2232
+ },
2233
+ "glass": null,
2234
+ "bubbles": "filled",
2235
+ "chrome": "none",
2236
+ "colors": {
2237
+ "light": {
2238
+ "background": "#F7F8F8",
2239
+ "surface": "#FFFFFF",
2240
+ "surfaceRaised": "#FFFFFF",
2241
+ "text": "#0F1011",
2242
+ "textMuted": "#5C616A",
2243
+ "border": "#E2E4E7",
2244
+ "accent": "#5E6AD2",
2245
+ "onAccent": "#FFFFFF",
2246
+ "success": "#2E8A57",
2247
+ "warning": "#B0560B",
2248
+ "danger": "#CF3B41",
2249
+ "userBubble": "#5E6AD2",
2250
+ "userBubbleText": "#FFFFFF",
2251
+ "agentBubble": "#EFF0F2",
2252
+ "agentBubbleText": "#0F1011",
2253
+ "scrim": "#000000"
2254
+ },
2255
+ "dark": {
2256
+ "background": "#08090A",
2257
+ "surface": "#111214",
2258
+ "surfaceRaised": "#191A1D",
2259
+ "text": "#F7F8F8",
2260
+ "textMuted": "#8E939C",
2261
+ "border": "#26282C",
2262
+ "accent": "#5E6AD2",
2263
+ "onAccent": "#FFFFFF",
2264
+ "success": "#4CB782",
2265
+ "warning": "#F2994A",
2266
+ "danger": "#D5434A",
2267
+ "userBubble": "#5E6AD2",
2268
+ "userBubbleText": "#FFFFFF",
2269
+ "agentBubble": "#1A1B1E",
2270
+ "agentBubbleText": "#F7F8F8",
2271
+ "scrim": "#000000"
2272
+ }
2273
+ }
2274
+ };
2275
+ const P_JAPANDI_CALM = {
2276
+ "id": "japandi-calm",
2277
+ "name": "Japandi Calm",
2278
+ "tagline": "Warm stone, muted sage and soft serif titles, with slow, quiet motion that stays out of the way.",
2279
+ "family": "modern",
2280
+ "inspiredBy": [
2281
+ "Leonard Koren, Wabi-Sabi for Artists, Designers, Poets & Philosophers (1994)",
2282
+ "Mark Weiser & John Seely Brown, Designing Calm Technology (Xerox PARC, 1995)",
2283
+ "Amber Case, Principles of Calm Technology"
2284
+ ],
2285
+ "font": {
2286
+ "body": "Noto Sans",
2287
+ "heading": "Noto Serif Devanagari",
2288
+ "scale": {
2289
+ "caption": 12,
2290
+ "body": 16,
2291
+ "title": 20,
2292
+ "display": 28
2293
+ },
2294
+ "weights": {
2295
+ "body": 400,
2296
+ "strong": 500,
2297
+ "title": 400
2298
+ },
2299
+ "letterSpacing": {
2300
+ "body": 0,
2301
+ "title": 0
2302
+ },
2303
+ "lineHeight": {
2304
+ "body": 1.6,
2305
+ "title": 1.35
2306
+ }
2307
+ },
2308
+ "shape": {
2309
+ "radius": {
2310
+ "sm": 6,
2311
+ "md": 10,
2312
+ "lg": 14,
2313
+ "xl": 22
2314
+ },
2315
+ "borderWidth": 1,
2316
+ "launcher": "circle"
2317
+ },
2318
+ "spacing": {
2319
+ "unit": 4,
2320
+ "cardPadding": 18,
2321
+ "gap": 14
2322
+ },
2323
+ "elevation": {
2324
+ "card": null,
2325
+ "floating": {
2326
+ "y": 8,
2327
+ "blur": 28,
2328
+ "spread": -8,
2329
+ "opacity": 0.12,
2330
+ "color": "#3B342B"
2331
+ },
2332
+ "hardOffset": null
2333
+ },
2334
+ "motion": {
2335
+ "durationMs": 360,
2336
+ "easing": "standard",
2337
+ "bounce": 0
2338
+ },
2339
+ "icons": {
2340
+ "style": "outline",
2341
+ "stroke": 1.25,
2342
+ "launcher": "waveform"
2343
+ },
2344
+ "glass": null,
2345
+ "bubbles": "minimal",
2346
+ "chrome": "none",
2347
+ "colors": {
2348
+ "light": {
2349
+ "background": "#F5F1EA",
2350
+ "surface": "#FBF8F3",
2351
+ "surfaceRaised": "#FFFFFF",
2352
+ "text": "#2B2824",
2353
+ "textMuted": "#625C54",
2354
+ "border": "#E2DBD0",
2355
+ "accent": "#56654D",
2356
+ "onAccent": "#FBF8F3",
2357
+ "success": "#4A7646",
2358
+ "warning": "#94661C",
2359
+ "danger": "#A5422D",
2360
+ "userBubble": "#E2E7DA",
2361
+ "userBubbleText": "#2B2824",
2362
+ "agentBubble": "#EEE9E0",
2363
+ "agentBubbleText": "#2B2824",
2364
+ "scrim": "#1A1814"
2365
+ },
2366
+ "dark": {
2367
+ "background": "#1A1917",
2368
+ "surface": "#22201D",
2369
+ "surfaceRaised": "#2B2925",
2370
+ "text": "#EEE8DF",
2371
+ "textMuted": "#ADA59A",
2372
+ "border": "#3A3631",
2373
+ "accent": "#A6B596",
2374
+ "onAccent": "#1A1917",
2375
+ "success": "#8DB487",
2376
+ "warning": "#D9A55B",
2377
+ "danger": "#B9503A",
2378
+ "userBubble": "#3A4234",
2379
+ "userBubbleText": "#EEE8DF",
2380
+ "agentBubble": "#2B2925",
2381
+ "agentBubbleText": "#EEE8DF",
2382
+ "scrim": "#000000"
2383
+ }
2384
+ }
2385
+ };
2386
+ const P_GAMIFIED = {
2387
+ "id": "gamified",
2388
+ "name": "Gamified",
2389
+ "tagline": "Chunky and bright, with thick outlines, pressable bottom edges and bold rounded type.",
2390
+ "family": "expressive",
2391
+ "inspiredBy": [
2392
+ "Duolingo brand guidelines, colour (Feather Green #58CC02, Eel, Swan, Polar)",
2393
+ "Yu-kai Chou, Octalysis gamification framework",
2394
+ "Nunito, a rounded sans by Vernon Adams"
2395
+ ],
2396
+ "font": {
2397
+ "body": "Nunito",
2398
+ "heading": "Nunito",
2399
+ "scale": {
2400
+ "caption": 13,
2401
+ "body": 16,
2402
+ "title": 20,
2403
+ "display": 30
2404
+ },
2405
+ "weights": {
2406
+ "body": 500,
2407
+ "strong": 700,
2408
+ "title": 800
2409
+ },
2410
+ "letterSpacing": {
2411
+ "body": 0,
2412
+ "title": 0
2413
+ },
2414
+ "lineHeight": {
2415
+ "body": 1.45,
2416
+ "title": 1.2
2417
+ }
2418
+ },
2419
+ "shape": {
2420
+ "radius": {
2421
+ "sm": 8,
2422
+ "md": 14,
2423
+ "lg": 16,
2424
+ "xl": 20
2425
+ },
2426
+ "borderWidth": 2,
2427
+ "launcher": "rounded-square"
2428
+ },
2429
+ "spacing": {
2430
+ "unit": 4,
2431
+ "cardPadding": 16,
2432
+ "gap": 10
2433
+ },
2434
+ "elevation": {
2435
+ "card": {
2436
+ "y": 4,
2437
+ "blur": 0,
2438
+ "spread": 0,
2439
+ "opacity": 0.14,
2440
+ "color": "#000000"
2441
+ },
2442
+ "floating": {
2443
+ "y": 5,
2444
+ "blur": 0,
2445
+ "spread": 0,
2446
+ "opacity": 0.18,
2447
+ "color": "#000000"
2448
+ },
2449
+ "hardOffset": null
2450
+ },
2451
+ "motion": {
2452
+ "durationMs": 280,
2453
+ "easing": "spring",
2454
+ "bounce": 0.4
2455
+ },
2456
+ "icons": {
2457
+ "style": "filled",
2458
+ "stroke": 2.5,
2459
+ "launcher": "star"
2460
+ },
2461
+ "glass": null,
2462
+ "bubbles": "filled",
2463
+ "chrome": "none",
2464
+ "colors": {
2465
+ "light": {
2466
+ "background": "#FFFFFF",
2467
+ "surface": "#FFFFFF",
2468
+ "surfaceRaised": "#F7F7F7",
2469
+ "text": "#3C3C3C",
2470
+ "textMuted": "#646464",
2471
+ "border": "#E5E5E5",
2472
+ "accent": "#3A8400",
2473
+ "onAccent": "#FFFFFF",
2474
+ "success": "#3A8400",
2475
+ "warning": "#A86F00",
2476
+ "danger": "#D83434",
2477
+ "userBubble": "#58CC02",
2478
+ "userBubbleText": "#102B00",
2479
+ "agentBubble": "#F1F1F1",
2480
+ "agentBubbleText": "#3C3C3C",
2481
+ "scrim": "#000000"
2482
+ },
2483
+ "dark": {
2484
+ "background": "#131F24",
2485
+ "surface": "#1A2A31",
2486
+ "surfaceRaised": "#22343C",
2487
+ "text": "#F1F7FB",
2488
+ "textMuted": "#A5B5BD",
2489
+ "border": "#37464F",
2490
+ "accent": "#58CC02",
2491
+ "onAccent": "#102B00",
2492
+ "success": "#79D634",
2493
+ "warning": "#FFC800",
2494
+ "danger": "#D83434",
2495
+ "userBubble": "#58CC02",
2496
+ "userBubbleText": "#102B00",
2497
+ "agentBubble": "#22343C",
2498
+ "agentBubbleText": "#F1F7FB",
2499
+ "scrim": "#000000"
2500
+ }
2501
+ }
2502
+ };
2503
+ const P_FINTECH_CLEAR = {
2504
+ "id": "fintech-clear",
2505
+ "name": "Fintech Clear",
2506
+ "tagline": "Navy ink, a steady teal accent and soft layered shadows, for screens about money.",
2507
+ "family": "enterprise",
2508
+ "inspiredBy": [
2509
+ "Stripe, Designing accessible color systems (2019)",
2510
+ "Stripe Elements Appearance API: themes and variables",
2511
+ "IBM Plex Sans Devanagari (bilingual Latin + Devanagari)"
2512
+ ],
2513
+ "font": {
2514
+ "body": "IBM Plex Sans Devanagari",
2515
+ "heading": "IBM Plex Sans Devanagari",
2516
+ "scale": {
2517
+ "caption": 12,
2518
+ "body": 15,
2519
+ "title": 18,
2520
+ "display": 26
2521
+ },
2522
+ "weights": {
2523
+ "body": 400,
2524
+ "strong": 500,
2525
+ "title": 600
2526
+ },
2527
+ "letterSpacing": {
2528
+ "body": 0,
2529
+ "title": 0
2530
+ },
2531
+ "lineHeight": {
2532
+ "body": 1.55,
2533
+ "title": 1.3
2534
+ }
2535
+ },
2536
+ "shape": {
2537
+ "radius": {
2538
+ "sm": 4,
2539
+ "md": 6,
2540
+ "lg": 10,
2541
+ "xl": 14
2542
+ },
2543
+ "borderWidth": 1,
2544
+ "launcher": "circle"
2545
+ },
2546
+ "spacing": {
2547
+ "unit": 4,
2548
+ "cardPadding": 16,
2549
+ "gap": 12
2550
+ },
2551
+ "elevation": {
2552
+ "card": {
2553
+ "y": 2,
2554
+ "blur": 5,
2555
+ "spread": -1,
2556
+ "opacity": 0.25,
2557
+ "color": "#32325D"
2558
+ },
2559
+ "floating": {
2560
+ "y": 13,
2561
+ "blur": 27,
2562
+ "spread": -5,
2563
+ "opacity": 0.25,
2564
+ "color": "#32325D"
2565
+ },
2566
+ "hardOffset": null
2567
+ },
2568
+ "motion": {
2569
+ "durationMs": 200,
2570
+ "easing": "standard",
2571
+ "bounce": 0
2572
+ },
2573
+ "icons": {
2574
+ "style": "outline",
2575
+ "stroke": 1.75,
2576
+ "launcher": "headset"
2577
+ },
2578
+ "glass": null,
2579
+ "bubbles": "filled",
2580
+ "chrome": "none",
2581
+ "colors": {
2582
+ "light": {
2583
+ "background": "#F6F9FC",
2584
+ "surface": "#FFFFFF",
2585
+ "surfaceRaised": "#FFFFFF",
2586
+ "text": "#0A2540",
2587
+ "textMuted": "#425466",
2588
+ "border": "#E3E8EE",
2589
+ "accent": "#0B7A75",
2590
+ "onAccent": "#FFFFFF",
2591
+ "success": "#16794A",
2592
+ "warning": "#A65400",
2593
+ "danger": "#DF1B41",
2594
+ "userBubble": "#0A2540",
2595
+ "userBubbleText": "#FFFFFF",
2596
+ "agentBubble": "#EEF2F6",
2597
+ "agentBubbleText": "#0A2540",
2598
+ "scrim": "#0A2540"
2599
+ },
2600
+ "dark": {
2601
+ "background": "#081624",
2602
+ "surface": "#0E2032",
2603
+ "surfaceRaised": "#15293D",
2604
+ "text": "#F3F6FA",
2605
+ "textMuted": "#A3B3C4",
2606
+ "border": "#22384F",
2607
+ "accent": "#3CC2B8",
2608
+ "onAccent": "#04211F",
2609
+ "success": "#3DCB86",
2610
+ "warning": "#F5A623",
2611
+ "danger": "#D7304A",
2612
+ "userBubble": "#24507A",
2613
+ "userBubbleText": "#FFFFFF",
2614
+ "agentBubble": "#15293D",
2615
+ "agentBubbleText": "#F3F6FA",
2616
+ "scrim": "#000000"
2617
+ }
2618
+ }
2619
+ };
2620
+ export const DESIGN_PRESETS = [GRAINE_CLASSIC, P_SIGNATURE, P_CHAT_NEUTRAL, P_MATERIAL_3, P_IOS_NATIVE, P_MINIMAL_MONO, P_FLUENT_2, P_CARBON, P_FINTECH_CLEAR, P_LIQUID_GLASS, P_SOFT_PLAYFUL, P_DESKTOP_BUDDY, P_SWISS_GRID, P_PRECISION, P_JAPANDI_CALM, P_NEO_BRUTALISM, P_EDITORIAL, P_LUXURY, P_RETRO_TERMINAL, P_WARM_EARTHY, P_BAUHAUS, P_GAMIFIED, P_HIGH_CONTRAST];
1882
2621
  export const DEFAULT_DESIGN_PRESET = "graine-classic";
2622
+ const PRESET_ALIASES = { gateway: "signature" };
1883
2623
  export function designPreset(id) {
1884
- const want = String(id ?? "").trim().toLowerCase();
2624
+ const raw = String(id ?? "").trim().toLowerCase();
2625
+ const want = PRESET_ALIASES[raw] ?? raw;
1885
2626
  return DESIGN_PRESETS.find((p) => p.id === want) ?? DESIGN_PRESETS[0];
1886
2627
  }
1887
2628
  function pick(v, allowed) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graineai/inapp-react-native",
3
- "version": "0.39.0",
3
+ "version": "0.40.0",
4
4
  "type": "module",
5
5
  "description": "Graine in-app agent for React Native — an agent that sees the screen your customer is on and can act on it.",
6
6
  "license": "MIT",