@graineai/inapp-react-native 0.38.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, 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";
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, 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,18 +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";
14
+ import { TitleBar, windowed } from "./window-chrome.js";
13
15
  const BAR_HEIGHT = 60;
14
16
  const BUTTON = 38;
15
17
  const METER = [0.55, 1, 0.7, 0.4];
16
- 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) {
17
28
  const conn = api.connectivity?.state;
18
29
  const say = (text) => ({ text, waiting: null, meter: false });
19
30
  if (api.error)
@@ -28,8 +39,10 @@ function statusOf(api, isChat, chatConnected) {
28
39
  return say("Needs microphone");
29
40
  if (conn === "failed")
30
41
  return say("Couldn’t load · tap to retry");
42
+ if (api.callFailed)
43
+ return say("Couldn’t connect · tap to retry");
31
44
  if (!api.ready || api.connecting)
32
- return { text: "Connecting…", waiting: "connecting", meter: false };
45
+ return { ...connectingLook(api, connectingText), meter: false };
33
46
  if (api.muted)
34
47
  return say("Muted");
35
48
  if (api.connected && api.phase === "thinking")
@@ -62,7 +75,8 @@ export function GraineVoiceBar({ api, onClose, bottomInset, draggable = true, pl
62
75
  initialOffset: placement?.bottomOffset,
63
76
  onChange: (bottomOffset) => onPlacementChange?.({ bottomOffset }),
64
77
  });
65
- const status = statusOf(api, isChat, chatConnected);
78
+ const loader = useGraineLoader();
79
+ const status = statusOf(api, isChat, chatConnected, loader.text.connecting);
66
80
  const conn = api.connectivity?.state;
67
81
  const micBlocked = !!api.error || api.micDenied || api.connecting || (!api.ready && conn !== "failed");
68
82
  const caption = !isChat && api.captionsOn && api.caption?.text ? api.caption : null;
@@ -78,11 +92,11 @@ export function GraineVoiceBar({ api, onClose, bottomInset, draggable = true, pl
78
92
  return (_jsxs(View, { pointerEvents: "box-none", ...drag.panHandlers, onLayout: drag.onLayout, style: {
79
93
  position: "absolute", zIndex: 900, gap: 8, left: 12, right: 12,
80
94
  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: {
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: {
82
96
  height: BAR_HEIGHT, paddingHorizontal: 9, flexDirection: "row", alignItems: "center", gap: 10,
83
97
  backgroundColor: surfaceFill(c.surface, t), borderWidth: t.shape.borderWidth, borderColor: c.border,
84
98
  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: () => {
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: () => {
86
100
  if (conn === "failed") {
87
101
  api.retry();
88
102
  return;
@@ -124,7 +138,13 @@ function Icon({ name, t, color, size = 18, kit, rotate }) {
124
138
  const common = { stroke: color, strokeLinecap: sharp ? "square" : "round", strokeLinejoin: sharp ? "miter" : "round" };
125
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] }) }));
126
140
  }
127
- 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", }) {
128
148
  const on = t.colors.onAccent;
129
149
  const g = t.icons.launcher;
130
150
  if (g) {
@@ -135,7 +155,8 @@ function Mark({ t, size, glyph, kit, origin, logoUrl, initial, cover = false, })
135
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 }));
136
156
  }
137
157
  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 });
158
+ const box = logoBox(shape, size);
159
+ return _jsx(Image, { source: { uri: logoUrl }, style: { width: box, height: box }, resizeMode: "contain", accessibilityIgnoresInvertColors: true });
139
160
  }
140
161
  return _jsx(Text, { style: { color: on, fontSize: Math.round(size * 0.4), fontWeight: "700" }, children: initial });
141
162
  }
@@ -184,12 +205,13 @@ function CaptionCard({ look, role, text, live }) {
184
205
  const t = look.tokens;
185
206
  const c = t.colors;
186
207
  const rise = useRise(look);
208
+ const win = windowed(t);
187
209
  return (_jsx(Animated.View, { style: rise, children: _jsx(Raised, { t: t, level: "card", radius: t.shape.radius.lg, children: _jsxs(View, { style: {
188
210
  backgroundColor: surfaceFill(c.surface, t), borderWidth: t.shape.borderWidth, borderColor: c.border,
189
211
  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 })] }) }) }));
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 })] }) }) }));
191
213
  }
192
- 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, }) {
193
215
  const t = look.tokens;
194
216
  const c = t.colors;
195
217
  const r = t.shape.radius;
@@ -215,7 +237,7 @@ function ChatWindow({ look, kit, origin, heading, logoUrl, messages, send, onlin
215
237
  letterSpacing: letterSpacing(size, t.font.letterSpacing.body),
216
238
  });
217
239
  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) => {
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) => {
219
241
  const b = bubble(t, m.role !== "agent");
220
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));
221
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]);
@@ -7,6 +7,7 @@ import { resolveNativeAppearance, resolveWidgetPalette } from "./appearance.js";
7
7
  import { useDesign } from "./use-design.js";
8
8
  import { alpha, letterSpacing, lineHeight, onAccentFor, shadowStyle, softFor } from "./design-math.js";
9
9
  import { DEFAULT_DESIGN_PRESET, designPreset } from "../theme.js";
10
+ import { TitleBar, windowed } from "./window-chrome.js";
10
11
  import { PLATFORM_GRAMMAR, manySummary, resolveLoader, resolvePlatform, resolveWidgetLook, widgetSurface } from "../protocol.js";
11
12
  function tint(hex, alpha) {
12
13
  const m = /^#?([0-9a-f]{6})$/i.exec(String(hex).trim());
@@ -19,6 +20,7 @@ const CLASSIC_SCALE = designPreset(DEFAULT_DESIGN_PRESET).font.scale;
19
20
  function classicSkin(theme, accent, radius) {
20
21
  return {
21
22
  classic: true,
23
+ window: null,
22
24
  size: (today) => today,
23
25
  weight: (today) => today,
24
26
  family: () => undefined,
@@ -50,6 +52,7 @@ function designSkin(look, theme, accent, radius, ownRadius, surfaceOverride) {
50
52
  const density = t.spacing.cardPadding / 14;
51
53
  return {
52
54
  classic: false,
55
+ window: windowed(t) ? t : null,
53
56
  size: (today, role, exact) => (exact ? f.scale[role] : Math.round(((today * f.scale[role]) / CLASSIC_SCALE[role]) * 10) / 10),
54
57
  weight: (_today, which) => String(f.weights[which]),
55
58
  family: (heading) => look.font(heading ? f.heading : f.body),
@@ -411,9 +414,11 @@ export function GraineWidget(props) {
411
414
  paddingHorizontal: 16, borderRadius: 999, backgroundColor: tint(theme.border, 0.4),
412
415
  }, children: _jsx(Text, { style: { color: theme.text, fontSize: k.size(13.5, "body"), fontWeight: k.weight("700", "strong"), fontFamily: k.family() }, children: "None of these" }) })] })] }) })] }));
413
416
  }
417
+ const cardPad = k.pad(14, 12);
418
+ const cardFill = k.window && !props.theme?.surface ? k.window.colors.surface : theme.surface;
414
419
  return (_jsxs(_Fragment, { children: [chosen, _jsxs(Raised, { k: k, style: {
415
- backgroundColor: theme.surface, borderRadius: radius, borderWidth: k.borderWidth, borderColor: theme.border,
416
- ...k.pad(14, 12), marginBottom: 8,
420
+ backgroundColor: cardFill, borderRadius: radius, borderWidth: k.borderWidth, borderColor: theme.border,
421
+ ...cardPad, marginBottom: 8,
417
422
  ...(k.classic ? k.depth.shadow : null),
418
- }, children: [title ? (_jsx(Text, { style: { color: theme.text, ...titleType(14.5), marginBottom: 2 }, children: title })) : null, prompt ? (_jsx(Text, { style: { color: theme.muted, fontSize: k.size(12.5, "caption", true), lineHeight: k.line(17, k.size(12.5, "caption", true)), fontFamily: k.family(), marginBottom: 8 }, children: prompt })) : _jsx(View, { style: { height: 6 } }), body, _jsx(Pressable, { onPress: dismiss, accessibilityRole: "button", accessibilityLabel: "Dismiss", hitSlop: 8, style: { alignSelf: "flex-end", marginTop: 10, minHeight: 44, justifyContent: "center" }, children: _jsx(Text, { style: { color: theme.muted, fontSize: k.size(12.5, "caption", true), fontWeight: k.weight("600", "body"), fontFamily: k.family() }, children: "Dismiss" }) })] })] }));
423
+ }, children: [k.window ? (_jsx(View, { style: { marginBottom: 10 }, children: _jsx(TitleBar, { t: k.window, title: title, radius: radius, bleed: { horizontal: cardPad.paddingHorizontal, top: cardPad.paddingVertical } }) })) : title ? (_jsx(Text, { style: { color: theme.text, ...titleType(14.5), marginBottom: 2 }, children: title })) : null, prompt ? (_jsx(Text, { style: { color: theme.muted, fontSize: k.size(12.5, "caption", true), lineHeight: k.line(17, k.size(12.5, "caption", true)), fontFamily: k.family(), marginBottom: 8 }, children: prompt })) : _jsx(View, { style: { height: 6 } }), body, _jsx(Pressable, { onPress: dismiss, accessibilityRole: "button", accessibilityLabel: "Dismiss", hitSlop: 8, style: { alignSelf: "flex-end", marginTop: 10, minHeight: 44, justifyContent: "center" }, children: _jsx(Text, { style: { color: theme.muted, fontSize: k.size(12.5, "caption", true), fontWeight: k.weight("600", "body"), fontFamily: k.family() }, children: "Dismiss" }) })] })] }));
419
424
  }
@@ -0,0 +1,30 @@
1
+ import React from "react";
2
+ import type { ThemeTokens } from "../theme.js";
3
+ export declare const WINDOW_CHROME: {
4
+ readonly height: {
5
+ readonly window: 32;
6
+ readonly caption: 24;
7
+ };
8
+ readonly dot: {
9
+ readonly window: 10;
10
+ readonly caption: 8;
11
+ };
12
+ readonly gap: 6;
13
+ readonly inset: 12;
14
+ readonly lights: readonly ["#FF5F57", "#FEBC2E", "#28C840"];
15
+ readonly online: 6;
16
+ };
17
+ export declare const windowed: (t: ThemeTokens) => boolean;
18
+ export declare const MONO: string;
19
+ export declare function TitleBar({ t, title, size, online, right, radius, bleed, }: {
20
+ t: ThemeTokens;
21
+ title: string;
22
+ size?: "window" | "caption";
23
+ online?: boolean;
24
+ right?: React.ReactNode;
25
+ radius?: number;
26
+ bleed?: {
27
+ horizontal: number;
28
+ top: number;
29
+ };
30
+ }): React.JSX.Element;
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Platform, Text, View } from "react-native";
3
+ export const WINDOW_CHROME = {
4
+ height: { window: 32, caption: 24 },
5
+ dot: { window: 10, caption: 8 },
6
+ gap: 6,
7
+ inset: 12,
8
+ lights: ["#FF5F57", "#FEBC2E", "#28C840"],
9
+ online: 6,
10
+ };
11
+ export const windowed = (t) => t.chrome === "window";
12
+ export const MONO = Platform.select({ ios: "Menlo", android: "monospace", default: "monospace" });
13
+ export function TitleBar({ t, title, size = "window", online = false, right, radius = 0, bleed, }) {
14
+ const c = t.colors;
15
+ const h = WINDOW_CHROME.height[size];
16
+ const d = WINDOW_CHROME.dot[size];
17
+ const inner = Math.max(0, radius - t.shape.borderWidth);
18
+ return (_jsxs(View, { accessibilityRole: "header", style: {
19
+ height: h,
20
+ flexDirection: "row",
21
+ alignItems: "center",
22
+ paddingHorizontal: WINDOW_CHROME.inset,
23
+ backgroundColor: c.surfaceRaised,
24
+ borderBottomWidth: t.shape.borderWidth,
25
+ borderBottomColor: c.border,
26
+ borderTopLeftRadius: inner,
27
+ borderTopRightRadius: inner,
28
+ ...(bleed ? { marginHorizontal: -bleed.horizontal, marginTop: -bleed.top } : null),
29
+ }, children: [_jsx(View, { style: { flexDirection: "row", gap: WINDOW_CHROME.gap }, accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", children: WINDOW_CHROME.lights.map((color) => (_jsx(View, { style: { width: d, height: d, borderRadius: d / 2, backgroundColor: color } }, color))) }), _jsxs(View, { pointerEvents: "none", style: { position: "absolute", left: 0, right: 0, top: 0, bottom: 0, flexDirection: "row", alignItems: "center", justifyContent: "center", paddingHorizontal: 64 }, children: [online ? (_jsx(View, { style: { width: WINDOW_CHROME.online, height: WINDOW_CHROME.online, borderRadius: WINDOW_CHROME.online / 2, backgroundColor: c.success, marginRight: 6 } })) : null, _jsx(Text, { numberOfLines: 1, style: { fontFamily: MONO, fontSize: t.font.scale.caption, color: c.textMuted }, children: title.toLowerCase() })] }), right ? _jsx(View, { style: { marginLeft: "auto" }, children: right }) : null] }));
30
+ }
package/dist/theme.d.ts CHANGED
@@ -8,6 +8,7 @@ export type LauncherShape = "circle" | "squircle" | "rounded-square" | "square"
8
8
  export type IconStyle = "outline" | "filled" | "duotone" | "sharp";
9
9
  export type MotionEasing = "standard" | "emphasized" | "spring" | "linear" | "none";
10
10
  export type DesignFamily = "graine" | "native" | "modern" | "enterprise" | "expressive";
11
+ export type DesignChrome = "none" | "window";
11
12
  export interface DesignConfig {
12
13
  preset: string;
13
14
  accent?: string;
@@ -24,6 +25,7 @@ export interface DesignConfig {
24
25
  launcherIcon?: string;
25
26
  launcherShape?: LauncherShape;
26
27
  iconStyle?: IconStyle;
28
+ chrome?: DesignChrome;
27
29
  }
28
30
  export interface ThemeColors {
29
31
  background: string;
@@ -121,6 +123,7 @@ export interface ThemeTokens {
121
123
  surfaceOpacity: number;
122
124
  } | null;
123
125
  bubbles: DesignBubbles;
126
+ chrome: DesignChrome;
124
127
  }
125
128
  export interface ResolvedDesign {
126
129
  config: DesignConfig;
@@ -145,6 +148,7 @@ export interface DesignPreset {
145
148
  };
146
149
  glass: ThemeTokens["glass"];
147
150
  bubbles: DesignBubbles;
151
+ chrome?: DesignChrome;
148
152
  colors: {
149
153
  light: Omit<ThemeColors, "accentSoft">;
150
154
  dark: Omit<ThemeColors, "accentSoft">;
@@ -158,6 +162,19 @@ export declare const DESIGN_NEUTRALS: ReadonlyArray<DesignNeutral>;
158
162
  export declare const DESIGN_BUBBLES: ReadonlyArray<DesignBubbles>;
159
163
  export declare const LAUNCHER_SHAPES: ReadonlyArray<LauncherShape>;
160
164
  export declare const ICON_STYLES: ReadonlyArray<IconStyle>;
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
+ };
161
178
  export declare function normaliseHex(value: unknown): string | null;
162
179
  export declare function mixHex(a: string, b: string, t: number): string;
163
180
  export declare function luminanceOf(hex: string): number;