@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 +1 -1
- package/dist/index.js +1 -1
- package/dist/react-native/appearance.d.ts +2 -0
- package/dist/react-native/appearance.js +2 -1
- package/dist/react-native/fab.js +14 -9
- package/dist/react-native/voice-bar.js +34 -12
- package/dist/react-native/voice-launcher.d.ts +5 -0
- package/dist/react-native/voice-launcher.js +38 -0
- package/dist/react-native/widget.js +8 -3
- package/dist/react-native/window-chrome.d.ts +30 -0
- package/dist/react-native/window-chrome.js +30 -0
- package/dist/theme.d.ts +17 -0
- package/dist/theme.js +864 -2
- package/package.json +1 -1
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,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" };
|
package/dist/react-native/fab.js
CHANGED
|
@@ -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,
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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 {
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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),
|
|
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:
|
|
416
|
-
...
|
|
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;
|