@graineai/inapp-react-native 0.37.0 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INTEGRATION.md +26 -12
- package/README.md +7 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.js +4 -1
- package/dist/react-native/fab.d.ts +7 -0
- package/dist/react-native/fab.js +6 -0
- package/dist/react-native/font-weights.d.ts +1 -0
- package/dist/react-native/font-weights.js +26 -0
- package/dist/react-native/fonts.d.ts +20 -0
- package/dist/react-native/fonts.js +69 -0
- package/dist/react-native/index.d.ts +10 -1
- package/dist/react-native/index.js +32 -3
- package/dist/react-native/use-design.d.ts +4 -0
- package/dist/react-native/use-design.js +14 -2
- package/dist/react-native/voice-bar.d.ts +16 -0
- package/dist/react-native/voice-bar.js +257 -0
- package/dist/react-native/voice-launcher.d.ts +2 -0
- package/dist/react-native/voice-launcher.js +11 -0
- package/dist/react-native/widget.js +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 +10 -0
- package/dist/theme.js +132 -1
- package/package.json +1 -1
package/INTEGRATION.md
CHANGED
|
@@ -560,8 +560,12 @@ your funnel.
|
|
|
560
560
|
|
|
561
561
|
## Captions and hanging up
|
|
562
562
|
|
|
563
|
-
`
|
|
564
|
-
|
|
563
|
+
`GraineVoiceBar` draws the caption, the microphone and the hang-up in the
|
|
564
|
+
brand's design — mount it in the render prop and you are done (**0.38.0**):
|
|
565
|
+
`{(api) => <GraineVoiceBar api={api} onClose={close} />}`.
|
|
566
|
+
|
|
567
|
+
Drawing your own instead: the render prop gives you the whole transcript and
|
|
568
|
+
the CC switch, so a caption box is a `map` and nothing else. **0.28.0.**
|
|
565
569
|
|
|
566
570
|
```tsx
|
|
567
571
|
<GraineVoiceLauncher webView={WebView} active={open} autoStart>
|
|
@@ -707,14 +711,20 @@ part. Everything else is unchanged.
|
|
|
707
711
|
|
|
708
712
|
## Making the bar yours
|
|
709
713
|
|
|
710
|
-
|
|
714
|
+
Most apps stop at the first level — and since 0.38.0 the first level is the
|
|
715
|
+
whole design.
|
|
711
716
|
|
|
712
|
-
**1. The dashboard.**
|
|
713
|
-
|
|
714
|
-
|
|
717
|
+
**1. The dashboard.** The brand chooses a design in Appearance — one of fifteen
|
|
718
|
+
philosophies, tuned for colour, corners, typeface, launcher, icons, motion and
|
|
719
|
+
bubbles — and the SDK draws it on every surface with no app release:
|
|
720
|
+
`GraineVoiceBar` for the call, `GraineAgentBar` for the chat, the launcher and
|
|
721
|
+
every component. Give the provider `loadFont={Font.loadAsync}` and
|
|
722
|
+
`svg={{ Svg, Path }}` once and the SDK loads the design's typeface and draws its
|
|
723
|
+
icons itself. This is the level a brand team should be working at.
|
|
715
724
|
|
|
716
|
-
**2. Props, when the dashboard cannot express it.**
|
|
717
|
-
the dashboard, so an app that needs something per-build
|
|
725
|
+
**2. Props, when the dashboard cannot express it.** On `GraineAgentBar`, any
|
|
726
|
+
prop passed wins over the dashboard, so an app that needs something per-build
|
|
727
|
+
still can.
|
|
718
728
|
|
|
719
729
|
```tsx
|
|
720
730
|
<GraineAgentBar
|
|
@@ -749,15 +759,16 @@ import type { GraineBarTheme } from "@graineai/inapp-react-native";
|
|
|
749
759
|
| `onAccent` | Text and glyphs sitting on the accent, and on `danger` |
|
|
750
760
|
| `danger` | Muted — the one state worth its own colour |
|
|
751
761
|
|
|
752
|
-
**4. Or draw your own.** Everything the bar does is built on the public hooks
|
|
753
|
-
|
|
762
|
+
**4. Or draw your own.** Everything the bar does is built on the public hooks —
|
|
763
|
+
but the design chosen in the dashboard then stops reaching that part of your
|
|
764
|
+
app, so match it with `useGraineTheme()`, which returns the design's tokens:
|
|
754
765
|
|
|
755
766
|
```tsx
|
|
756
767
|
const { connected, muted, setMuted, messages, send, caption } = useGraineAgent();
|
|
768
|
+
const theme = useGraineTheme(); // colours, type, corners, spacing, shadows
|
|
757
769
|
```
|
|
758
770
|
|
|
759
|
-
|
|
760
|
-
SDK depends on this component.
|
|
771
|
+
Nothing else in the SDK depends on these components.
|
|
761
772
|
|
|
762
773
|
---
|
|
763
774
|
|
|
@@ -1096,6 +1107,9 @@ told to ignore).
|
|
|
1096
1107
|
| `endOnBackground` | `boolean` | Default `true`. You want it on. |
|
|
1097
1108
|
| `appVersion` | `string` | Which build the conversation happened in. Lands on the call record. |
|
|
1098
1109
|
| `friction` | `FrictionConfig` | This app's friction rules, layered over the dashboard's. See *Friction detection*. |
|
|
1110
|
+
| `loadFont` | `(fonts) => Promise` | expo-font's `Font.loadAsync`. The SDK loads the design's typeface itself — only the faces it uses, from Graine. **0.38.0** |
|
|
1111
|
+
| `svg` | `{ Svg, Path }` | react-native-svg's components, so the SDK draws its icons in the design's style. **0.38.0** |
|
|
1112
|
+
| `fonts` | `Record<string, string>` | Families your app already bundles (dashboard name → your `fontFamily`); they win over a download. |
|
|
1099
1113
|
|
|
1100
1114
|
### `GraineInAppClient`
|
|
1101
1115
|
|
package/README.md
CHANGED
|
@@ -119,13 +119,17 @@ over the air.
|
|
|
119
119
|
|
|
120
120
|
```tsx
|
|
121
121
|
import { WebView } from "react-native-webview";
|
|
122
|
-
import
|
|
122
|
+
import * as Font from "expo-font";
|
|
123
|
+
import Svg, { Path } from "react-native-svg";
|
|
124
|
+
import { GraineProvider, GraineVoiceLauncher, GraineVoiceBar } from "@graineai/inapp-react-native";
|
|
123
125
|
|
|
124
126
|
<GraineProvider
|
|
125
127
|
baseUrl="…"
|
|
126
128
|
publishableKey="pk_live_…"
|
|
127
129
|
navigationRef={navigationRef}
|
|
128
130
|
autoConnect={false} // the launcher owns the connection
|
|
131
|
+
loadFont={Font.loadAsync} // the SDK loads the design's typeface itself
|
|
132
|
+
svg={{ Svg, Path }} // …and draws its icons in the design's style
|
|
129
133
|
>
|
|
130
134
|
<RootNavigator />
|
|
131
135
|
<GraineVoiceLauncher
|
|
@@ -133,7 +137,7 @@ import { GraineProvider, GraineVoiceLauncher, GraineAgentBar } from "@graineai/i
|
|
|
133
137
|
autoStart
|
|
134
138
|
onError={(m) => console.warn("[graine]", m)}
|
|
135
139
|
>
|
|
136
|
-
{() => <
|
|
140
|
+
{(api) => <GraineVoiceBar api={api} />}
|
|
137
141
|
</GraineVoiceLauncher>
|
|
138
142
|
</GraineProvider>
|
|
139
143
|
```
|
|
@@ -225,6 +229,7 @@ requests a permission on your behalf.
|
|
|
225
229
|
| `useGraineIdentify()` | Who the customer is — it reaches the greeting and the call record. |
|
|
226
230
|
| `useGraineHighlight()` / `useGraineWidget()` | Let the agent point at things and draw components. |
|
|
227
231
|
| `GraineVoiceLauncher` | The voice engine: WebView, microphone, WebRTC or WebSocket. |
|
|
232
|
+
| `GraineVoiceBar` | The voice bar in the brand's design, as the dashboard previews it. Mount inside `GraineVoiceLauncher`. |
|
|
228
233
|
| `fab={{ webView }}` on the provider / `GraineFab` | The floating button, mounted for you; opens the conversation, folds back on end. |
|
|
229
234
|
| `captureTaps` / `GraineTouchCapture` | Every tap tracked automatically — no per-button wrapping. |
|
|
230
235
|
| `useGraineInit()` | `{ isInitialized, error }`. |
|
package/dist/index.d.ts
CHANGED
|
@@ -23,14 +23,17 @@ export { FrictionDetector, resolveFriction, rulesFor, DEFAULT_FRICTION, type Fri
|
|
|
23
23
|
export { VoiceSession, liveAudioStreamAdapter, base64ToPcm16, CAPTURE_SAMPLE_RATE, type AudioAdapter, type VoiceSessionOptions, } from "./voice.js";
|
|
24
24
|
export { EchoGuard, decodeAgentAudio, decodeMuLaw, decodePcm16, base64ToBytes, type AudioFormat, type DecodedAudio, } from "./audio.js";
|
|
25
25
|
export { addMaskRule, maskDeep, maskString } from "./mask.js";
|
|
26
|
-
export { GraineProvider, useGraineAgent, useGraineSurface, useGraineConnection, useGraineVariables, useKeyboardLift, liftFor, useDraggablePlacement, clampOffset, offsetDuringDrag, isTap, DRAG_SLOP_PX, useGraineScreen, useGraineAction, useGraineVoice, useGraineIdentify, useGraineEvents, useGraineField, useGraineClient, setDefaultClient, useGraineTrack, useGraineTap, useGraineHighlight, useGraineWidget, useGraineReady, useGraineInit, useGraineTheme, type GraineEvent, type GraineProviderProps, type Turn, type Caption, } from "./react-native/index.js";
|
|
26
|
+
export { GraineProvider, useGraineAgent, useGraineSurface, useGraineConnection, useGraineVariables, useKeyboardLift, liftFor, useDraggablePlacement, clampOffset, offsetDuringDrag, isTap, DRAG_SLOP_PX, useGraineScreen, useGraineAction, useGraineVoice, useGraineIdentify, useGraineEvents, useGraineField, useGraineClient, setDefaultClient, useGraineTrack, useGraineTap, useGraineHighlight, useGraineWidget, useGraineReady, useGraineInit, useGraineTheme, type GraineEvent, type GraineProviderProps, type SvgKit, type Turn, type Caption, } from "./react-native/index.js";
|
|
27
27
|
export { requestMicrophonePermission } from "./react-native/permissions.js";
|
|
28
28
|
export { GraineFab, GraineTouchCapture, TAP_MAX_MOVE_PX, TAP_MAX_MS, type GraineFabProps, type GraineTouchCaptureProps, } from "./react-native/fab.js";
|
|
29
29
|
export { UNNAMED_TAP, resolveWidgetImages } from "./protocol.js";
|
|
30
30
|
export { resolveNativeAppearance, type NativeAppearance, type NativeAppearanceFallback, resolveWidgetPalette, type WidgetPalette, type WidgetMode, } from "./react-native/appearance.js";
|
|
31
31
|
export { RtcVoiceSessionController, RtcVoiceError, type RtcVoiceState, type RtcVoiceErrorCode, type RtcVoiceSession, type RtcVoiceOptions, } from "./react-native/voice-rtc.js";
|
|
32
32
|
export { GraineAgentBar, GraineLauncher, type GraineAgentBarProps, type GraineLauncherProps, type GraineBarTheme, } from "./react-native/ui.js";
|
|
33
|
+
export { GraineVoiceBar, type GraineVoiceBarProps } from "./react-native/voice-bar.js";
|
|
34
|
+
export { GraineLauncherButton, type GraineLauncherButtonProps } from "./react-native/fab.js";
|
|
35
|
+
export { faceFor, facesForDesign, fontUrl, FONT_ASSET_VERSION, type FontLoader, type FontFace } from "./react-native/fonts.js";
|
|
33
36
|
export { GraineVoiceLauncher, CAPTION_HISTORY, type GraineVoiceLauncherProps, type GraineVoiceApi, } from "./react-native/voice-launcher.js";
|
|
34
37
|
export { LauncherVisibilityTracker, activeRouteName, type LauncherInset, type LauncherContinuity, type LauncherDelayPolicy, type LauncherGroup, type LauncherVisibility, type VisibilityDecision, } from "./react-native/navigation.js";
|
|
35
|
-
export { DESIGN_PRESETS, DEFAULT_DESIGN_PRESET, DESIGN_FONTS, LAUNCHER_ICONS, designPreset, normaliseDesignConfig, designFromAppearance, resolveTokens, resolveDesign, pickTokens, contrastRatio, readableOn, glyphUrl, type DesignConfig, type DesignPreset, type ThemeTokens, type ThemeColors, type ThemeShadow, type ResolvedDesign, type DesignBubbles, type LauncherShape, type MotionEasing, type IconStyle, } from "./theme.js";
|
|
38
|
+
export { DESIGN_PRESETS, DEFAULT_DESIGN_PRESET, DESIGN_FONTS, LAUNCHER_ICONS, SDK_ICONS, 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";
|
|
36
39
|
export { sessionDesign } from "./design.js";
|
package/dist/index.js
CHANGED
|
@@ -21,7 +21,10 @@ export { UNNAMED_TAP, resolveWidgetImages } from "./protocol.js";
|
|
|
21
21
|
export { resolveNativeAppearance, resolveWidgetPalette, } from "./react-native/appearance.js";
|
|
22
22
|
export { RtcVoiceSessionController, RtcVoiceError, } from "./react-native/voice-rtc.js";
|
|
23
23
|
export { GraineAgentBar, GraineLauncher, } from "./react-native/ui.js";
|
|
24
|
+
export { GraineVoiceBar } from "./react-native/voice-bar.js";
|
|
25
|
+
export { GraineLauncherButton } from "./react-native/fab.js";
|
|
26
|
+
export { faceFor, facesForDesign, fontUrl, FONT_ASSET_VERSION } from "./react-native/fonts.js";
|
|
24
27
|
export { GraineVoiceLauncher, CAPTION_HISTORY, } from "./react-native/voice-launcher.js";
|
|
25
28
|
export { LauncherVisibilityTracker, activeRouteName, } from "./react-native/navigation.js";
|
|
26
|
-
export { DESIGN_PRESETS, DEFAULT_DESIGN_PRESET, DESIGN_FONTS, LAUNCHER_ICONS, designPreset, normaliseDesignConfig, designFromAppearance, resolveTokens, resolveDesign, pickTokens, contrastRatio, readableOn, glyphUrl, } from "./theme.js";
|
|
29
|
+
export { DESIGN_PRESETS, DEFAULT_DESIGN_PRESET, DESIGN_FONTS, LAUNCHER_ICONS, SDK_ICONS, DESIGN_CHROMES, designPreset, normaliseDesignConfig, designFromAppearance, resolveTokens, resolveDesign, pickTokens, contrastRatio, readableOn, glyphUrl, } from "./theme.js";
|
|
27
30
|
export { sessionDesign } from "./design.js";
|
|
@@ -22,3 +22,10 @@ export interface GraineFabProps {
|
|
|
22
22
|
prewarm?: boolean;
|
|
23
23
|
}
|
|
24
24
|
export declare function GraineFab({ webView, position, size, accent: accentProp, label, overlay, prewarm, onOpen, onClose }: GraineFabProps): React.JSX.Element;
|
|
25
|
+
export interface GraineLauncherButtonProps {
|
|
26
|
+
onPress: () => void;
|
|
27
|
+
style?: ViewStyle;
|
|
28
|
+
label?: string;
|
|
29
|
+
size?: number;
|
|
30
|
+
}
|
|
31
|
+
export declare function GraineLauncherButton({ onPress, style, label, size }: GraineLauncherButtonProps): React.JSX.Element;
|
package/dist/react-native/fab.js
CHANGED
|
@@ -83,6 +83,12 @@ export function GraineFab({ webView, position = "bottom-right", size = 56, accen
|
|
|
83
83
|
return _jsx(AboveNativeScreens, { overlay: overlay, children: button({ start: () => { } }) });
|
|
84
84
|
return (_jsx(GraineVoiceLauncher, { webView: webView, onEnded: () => { setOpen(false); onClose?.(); }, children: (api) => (_jsx(AboveNativeScreens, { overlay: overlay, children: open ? _jsx(GraineAgentBar, { accent: look.classic ? dashboardAccent : accentProp, name: name }) : button(api) })) }));
|
|
85
85
|
}
|
|
86
|
+
export function GraineLauncherButton({ onPress, style, label, size = 56 }) {
|
|
87
|
+
const { appearance, client } = useGraineAgent();
|
|
88
|
+
const look = useDesign();
|
|
89
|
+
const dashboard = resolveNativeAppearance(appearance, { accent: look.tokens.colors.accent, heading: "Assistant" });
|
|
90
|
+
return (_jsx(DesignedLauncher, { look: look, size: size, accent: look.tokens.colors.accent, name: dashboard.heading, label: label ?? `Talk to ${dashboard.heading}`, logoUrl: dashboard.logoUrl, origin: client.opts.baseUrl, place: style ?? {}, onPress: onPress }));
|
|
91
|
+
}
|
|
86
92
|
function DesignedLauncher({ look, size, accent, name, label, logoUrl, origin, place, onPress, }) {
|
|
87
93
|
const t = look.tokens;
|
|
88
94
|
const [failed, setFailed] = useState([]);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const FONT_WEIGHTS: Record<string, number[]>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const FONT_WEIGHTS = {
|
|
2
|
+
"Inter": [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
|
3
|
+
"Roboto": [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
|
4
|
+
"Geist": [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
|
5
|
+
"IBM Plex Sans": [100, 200, 300, 400, 500, 600, 700],
|
|
6
|
+
"IBM Plex Sans Devanagari": [100, 200, 300, 400, 500, 600, 700],
|
|
7
|
+
"DM Sans": [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
|
8
|
+
"Noto Sans": [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
|
9
|
+
"Noto Sans Devanagari": [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
|
10
|
+
"Noto Sans Tamil": [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
|
11
|
+
"Mukta": [200, 300, 400, 500, 600, 700, 800],
|
|
12
|
+
"Hind": [300, 400, 500, 600, 700],
|
|
13
|
+
"Anek Devanagari": [100, 200, 300, 400, 500, 600, 700, 800],
|
|
14
|
+
"Poppins": [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
|
15
|
+
"Baloo 2": [400, 500, 600, 700, 800],
|
|
16
|
+
"Nunito": [200, 300, 400, 500, 600, 700, 800, 900],
|
|
17
|
+
"Atkinson Hyperlegible Next": [200, 300, 400, 500, 600, 700, 800],
|
|
18
|
+
"Source Serif 4": [200, 300, 400, 500, 600, 700, 800, 900],
|
|
19
|
+
"Newsreader": [200, 300, 400, 500, 600, 700, 800],
|
|
20
|
+
"Fraunces": [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
|
21
|
+
"Playfair Display": [400, 500, 600, 700, 800, 900],
|
|
22
|
+
"Tiro Devanagari Hindi": [400],
|
|
23
|
+
"Noto Serif Devanagari": [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
|
24
|
+
"Martel": [200, 300, 400, 600, 700, 800, 900],
|
|
25
|
+
"JetBrains Mono": [100, 200, 300, 400, 500, 600, 700, 800],
|
|
26
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ResolvedDesign, ThemeTokens } from "../theme.js";
|
|
2
|
+
export type FontLoader = (fonts: Record<string, string>) => Promise<unknown>;
|
|
3
|
+
export interface FontFace {
|
|
4
|
+
family: string;
|
|
5
|
+
weight: number;
|
|
6
|
+
name: string;
|
|
7
|
+
url: string;
|
|
8
|
+
}
|
|
9
|
+
export type LoadedFaces = Record<string, string>;
|
|
10
|
+
export declare const faceKey: (family: string, weight: number) => string;
|
|
11
|
+
export declare function nearestWeight(available: number[], want: number): number | null;
|
|
12
|
+
export declare const FONT_ASSET_VERSION = 1;
|
|
13
|
+
export declare function fontUrl(origin: string, family: string, weight: number): string;
|
|
14
|
+
export declare function faceFor(family: string | null | undefined, weight: number, origin?: string): FontFace | null;
|
|
15
|
+
export declare function facesFor(t: ThemeTokens, origin?: string): FontFace[];
|
|
16
|
+
export declare function facesForDesign(design: ResolvedDesign, origin?: string): FontFace[];
|
|
17
|
+
export declare function textFace(family: string | null | undefined, weight: number, hostFonts: Record<string, string> | null | undefined, loaded: LoadedFaces | null | undefined, hostLookup: (family: string | null | undefined, fonts: Record<string, string> | null | undefined) => string | undefined): {
|
|
18
|
+
fontFamily?: string;
|
|
19
|
+
fontWeight: any;
|
|
20
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { FONT_WEIGHTS } from "./font-weights.js";
|
|
2
|
+
export const faceKey = (family, weight) => `${family}|${weight}`;
|
|
3
|
+
export function nearestWeight(available, want) {
|
|
4
|
+
if (!available.length)
|
|
5
|
+
return null;
|
|
6
|
+
let best = available[0];
|
|
7
|
+
for (const w of available) {
|
|
8
|
+
const d = Math.abs(w - want);
|
|
9
|
+
const bd = Math.abs(best - want);
|
|
10
|
+
if (d < bd || (d === bd && w > best))
|
|
11
|
+
best = w;
|
|
12
|
+
}
|
|
13
|
+
return best;
|
|
14
|
+
}
|
|
15
|
+
export const FONT_ASSET_VERSION = 1;
|
|
16
|
+
export function fontUrl(origin, family, weight) {
|
|
17
|
+
const base = String(origin || "https://www.graine.ai").replace(/\/+$/, "");
|
|
18
|
+
return `${base}/api/embed/font?f=${encodeURIComponent(family)}&w=${weight}&v=${FONT_ASSET_VERSION}`;
|
|
19
|
+
}
|
|
20
|
+
export function faceFor(family, weight, origin = "https://www.graine.ai") {
|
|
21
|
+
const want = String(family ?? "").trim();
|
|
22
|
+
if (!want || want.toLowerCase() === "system")
|
|
23
|
+
return null;
|
|
24
|
+
const canonical = Object.prototype.hasOwnProperty.call(FONT_WEIGHTS, want)
|
|
25
|
+
? want
|
|
26
|
+
: Object.keys(FONT_WEIGHTS).find((k) => k.toLowerCase() === want.toLowerCase());
|
|
27
|
+
if (!canonical)
|
|
28
|
+
return null;
|
|
29
|
+
const w = nearestWeight(FONT_WEIGHTS[canonical], weight);
|
|
30
|
+
if (w == null)
|
|
31
|
+
return null;
|
|
32
|
+
return {
|
|
33
|
+
family: canonical,
|
|
34
|
+
weight: w,
|
|
35
|
+
name: `Graine-${canonical.replace(/[^A-Za-z0-9]/g, "")}-${w}`,
|
|
36
|
+
url: fontUrl(origin, canonical, w),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function facesFor(t, origin) {
|
|
40
|
+
const out = new Map();
|
|
41
|
+
const add = (family, weight) => {
|
|
42
|
+
const f = faceFor(family, weight, origin);
|
|
43
|
+
if (f)
|
|
44
|
+
out.set(faceKey(f.family, f.weight), f);
|
|
45
|
+
};
|
|
46
|
+
add(t.font.body, t.font.weights.body);
|
|
47
|
+
add(t.font.body, t.font.weights.strong);
|
|
48
|
+
add(t.font.heading, t.font.weights.title);
|
|
49
|
+
add(t.font.heading, t.font.weights.strong);
|
|
50
|
+
return [...out.values()];
|
|
51
|
+
}
|
|
52
|
+
export function facesForDesign(design, origin) {
|
|
53
|
+
const out = new Map();
|
|
54
|
+
for (const t of [design.light, design.dark]) {
|
|
55
|
+
for (const f of facesFor(t, origin))
|
|
56
|
+
out.set(faceKey(f.family, f.weight), f);
|
|
57
|
+
}
|
|
58
|
+
return [...out.values()];
|
|
59
|
+
}
|
|
60
|
+
export function textFace(family, weight, hostFonts, loaded, hostLookup) {
|
|
61
|
+
const host = hostLookup(family, hostFonts);
|
|
62
|
+
if (host)
|
|
63
|
+
return { fontFamily: host, fontWeight: String(weight) };
|
|
64
|
+
const face = faceFor(family, weight);
|
|
65
|
+
const name = face && loaded ? loaded[faceKey(face.family, face.weight)] : undefined;
|
|
66
|
+
if (name)
|
|
67
|
+
return { fontFamily: name, fontWeight: "normal" };
|
|
68
|
+
return { fontWeight: String(weight) };
|
|
69
|
+
}
|
|
@@ -18,6 +18,7 @@ import { type AgentSurface, type ConnectionQuality } from "../protocol.js";
|
|
|
18
18
|
export type { ConnectionQuality, QualitySample } from "../protocol.js";
|
|
19
19
|
import type { FrictionConfig } from "../friction.js";
|
|
20
20
|
import type { ResolvedDesign } from "../theme.js";
|
|
21
|
+
import { type FontLoader, type LoadedFaces } from "./fonts.js";
|
|
21
22
|
export { useGraineTheme } from "./use-design.js";
|
|
22
23
|
import { type LauncherInset, type LauncherVisibility } from "./navigation.js";
|
|
23
24
|
interface GraineContextValue {
|
|
@@ -40,6 +41,8 @@ interface GraineContextValue {
|
|
|
40
41
|
appearance: Record<string, any> | null;
|
|
41
42
|
design: ResolvedDesign;
|
|
42
43
|
fonts: Record<string, string> | null;
|
|
44
|
+
faces: LoadedFaces;
|
|
45
|
+
svg: SvgKit | null;
|
|
43
46
|
sessionReady: boolean;
|
|
44
47
|
onEvent: (fn: (e: GraineEvent) => void) => () => void;
|
|
45
48
|
track: (name: string, data?: Record<string, unknown>) => void;
|
|
@@ -143,8 +146,14 @@ export interface GraineProviderProps extends Partial<GraineInAppOptions> {
|
|
|
143
146
|
launcherDelayMs?: number;
|
|
144
147
|
visibility?: LauncherVisibility;
|
|
145
148
|
fonts?: Record<string, string>;
|
|
149
|
+
loadFont?: FontLoader;
|
|
150
|
+
svg?: SvgKit;
|
|
146
151
|
}
|
|
147
|
-
export
|
|
152
|
+
export interface SvgKit {
|
|
153
|
+
Svg: React.ComponentType<any>;
|
|
154
|
+
Path: React.ComponentType<any>;
|
|
155
|
+
}
|
|
156
|
+
export declare function GraineProvider({ children, autoConnect, surface: surfaceProp, onSurfaceChange, onProactive, endOnBackground, navigationRef, fab, captureTaps, appVersion, friction, includeScreens, launcherDelayMs, visibility, fonts: fontsProp, loadFont, svg: svgProp, client: providedClient, ...options }: GraineProviderProps): React.JSX.Element;
|
|
148
157
|
export declare function useGraineAgent(): GraineContextValue;
|
|
149
158
|
export declare function useGraineSurface(): {
|
|
150
159
|
surface: AgentSurface;
|
|
@@ -13,7 +13,8 @@ export { widgetSurface } from "../protocol.js";
|
|
|
13
13
|
export { resolveSurface } from "../protocol.js";
|
|
14
14
|
import { resolveSurface } from "../protocol.js";
|
|
15
15
|
import { sessionDesign } from "../design.js";
|
|
16
|
-
import { sameValue } from "./design-math.js";
|
|
16
|
+
import { fontFamilyFor, sameValue } from "./design-math.js";
|
|
17
|
+
import { facesForDesign, faceKey } from "./fonts.js";
|
|
17
18
|
export { useGraineTheme } from "./use-design.js";
|
|
18
19
|
import { LauncherVisibilityTracker, activeRouteName, } from "./navigation.js";
|
|
19
20
|
export const PENDING_WIDGET_MS = 12000;
|
|
@@ -32,7 +33,7 @@ export function useGraineClient() {
|
|
|
32
33
|
}
|
|
33
34
|
return client;
|
|
34
35
|
}
|
|
35
|
-
export function GraineProvider({ children, autoConnect = true, surface: surfaceProp, onSurfaceChange, onProactive, endOnBackground = true, navigationRef, fab, captureTaps, appVersion, friction, includeScreens, launcherDelayMs = 0, visibility, fonts: fontsProp, client: providedClient, ...options }) {
|
|
36
|
+
export function GraineProvider({ children, autoConnect = true, surface: surfaceProp, onSurfaceChange, onProactive, endOnBackground = true, navigationRef, fab, captureTaps, appVersion, friction, includeScreens, launcherDelayMs = 0, visibility, fonts: fontsProp, loadFont, svg: svgProp, client: providedClient, ...options }) {
|
|
36
37
|
const clientRef = useRef(null);
|
|
37
38
|
if (!clientRef.current) {
|
|
38
39
|
clientRef.current =
|
|
@@ -73,6 +74,32 @@ export function GraineProvider({ children, autoConnect = true, surface: surfaceP
|
|
|
73
74
|
}, []);
|
|
74
75
|
const fontsKey = fontsProp ? JSON.stringify(fontsProp) : "";
|
|
75
76
|
const fonts = useMemo(() => (fontsKey ? JSON.parse(fontsKey) : null), [fontsKey]);
|
|
77
|
+
const [faces, setFaces] = useState({});
|
|
78
|
+
const svgKit = useMemo(() => (svgProp?.Svg && svgProp?.Path ? { Svg: svgProp.Svg, Path: svgProp.Path } : null), [svgProp?.Svg, svgProp?.Path]);
|
|
79
|
+
const askedRef = useRef(new Set());
|
|
80
|
+
const loadFontRef = useRef(loadFont);
|
|
81
|
+
loadFontRef.current = loadFont;
|
|
82
|
+
const hasLoader = typeof loadFont === "function";
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
const load = loadFontRef.current;
|
|
85
|
+
if (typeof load !== "function")
|
|
86
|
+
return;
|
|
87
|
+
let live = true;
|
|
88
|
+
for (const face of facesForDesign(design, client.opts.baseUrl)) {
|
|
89
|
+
const key = faceKey(face.family, face.weight);
|
|
90
|
+
if (fontFamilyFor(face.family, fonts))
|
|
91
|
+
continue;
|
|
92
|
+
if (askedRef.current.has(key))
|
|
93
|
+
continue;
|
|
94
|
+
askedRef.current.add(key);
|
|
95
|
+
Promise.resolve()
|
|
96
|
+
.then(() => load({ [face.name]: face.url }))
|
|
97
|
+
.then(() => { if (live)
|
|
98
|
+
setFaces((cur) => (cur[key] === face.name ? cur : { ...cur, [key]: face.name })); })
|
|
99
|
+
.catch(() => { });
|
|
100
|
+
}
|
|
101
|
+
return () => { live = false; askedRef.current.clear(); };
|
|
102
|
+
}, [client, design, fonts, hasLoader]);
|
|
76
103
|
const [sessionReady, setSessionReady] = useState(false);
|
|
77
104
|
const [connectionQuality, setConnectionQuality] = useState("unknown");
|
|
78
105
|
const [network, setNetwork] = useState("unknown");
|
|
@@ -350,6 +377,8 @@ export function GraineProvider({ children, autoConnect = true, surface: surfaceP
|
|
|
350
377
|
appearance,
|
|
351
378
|
design,
|
|
352
379
|
fonts,
|
|
380
|
+
faces,
|
|
381
|
+
svg: svgKit,
|
|
353
382
|
sessionReady,
|
|
354
383
|
onEvent,
|
|
355
384
|
track: (name, data) => client.track(name, data),
|
|
@@ -362,7 +391,7 @@ export function GraineProvider({ children, autoConnect = true, surface: surfaceP
|
|
|
362
391
|
network,
|
|
363
392
|
setVariables: (vars) => client.setVariables(vars),
|
|
364
393
|
}), [client, connected, connecting, error, open, messages, widgets, muted, agentSpeaking, caption,
|
|
365
|
-
launcherVisible, launcherInset, currentScreen, onEvent, appearance, design, fonts, sessionReady,
|
|
394
|
+
launcherVisible, launcherInset, currentScreen, onEvent, appearance, design, fonts, faces, svgKit, sessionReady,
|
|
366
395
|
surface, surfacePolicy, setSurface, connectionQuality, network]);
|
|
367
396
|
const wantsCapture = captureTaps ?? !!fab;
|
|
368
397
|
const body = fab || wantsCapture ? (_jsxs(GraineTouchCapture, { enabled: wantsCapture, children: [children, fab ? _jsx(GraineFab, { ...fab }) : null] })) : children;
|
|
@@ -6,6 +6,10 @@ export interface DesignLook {
|
|
|
6
6
|
tokens: ThemeTokens;
|
|
7
7
|
classic: boolean;
|
|
8
8
|
font: (family: string) => string | undefined;
|
|
9
|
+
face: (family: string, weight: number) => {
|
|
10
|
+
fontFamily?: string;
|
|
11
|
+
fontWeight: any;
|
|
12
|
+
};
|
|
9
13
|
durationMs: number;
|
|
10
14
|
reduceMotion: boolean;
|
|
11
15
|
easing: (t: number) => number;
|
|
@@ -4,7 +4,9 @@ import { Ctx } from "./index.js";
|
|
|
4
4
|
import { sessionDesign } from "../design.js";
|
|
5
5
|
import { pickTokens } from "../theme.js";
|
|
6
6
|
import { fontFamilyFor, isClassicLook } from "./design-math.js";
|
|
7
|
+
import { faceFor, faceKey, textFace } from "./fonts.js";
|
|
7
8
|
const DEFAULT_DESIGN = sessionDesign(null);
|
|
9
|
+
const NO_FACES = {};
|
|
8
10
|
const classicSeen = new WeakMap();
|
|
9
11
|
function classic(design) {
|
|
10
12
|
let v = classicSeen.get(design);
|
|
@@ -53,6 +55,7 @@ export function useDesign(scheme) {
|
|
|
53
55
|
const reduce = useReduceMotion();
|
|
54
56
|
const design = ctx?.design ?? DEFAULT_DESIGN;
|
|
55
57
|
const fonts = ctx?.fonts ?? null;
|
|
58
|
+
const faces = ctx?.faces ?? NO_FACES;
|
|
56
59
|
return useMemo(() => {
|
|
57
60
|
const tokens = scheme === "light" ? design.light : scheme === "dark" ? design.dark : pickTokens(design, device);
|
|
58
61
|
const durationMs = reduce || tokens.motion.easing === "none" ? 0 : Math.max(0, tokens.motion.durationMs);
|
|
@@ -60,12 +63,21 @@ export function useDesign(scheme) {
|
|
|
60
63
|
design,
|
|
61
64
|
tokens,
|
|
62
65
|
classic: classic(design),
|
|
63
|
-
font: (family) =>
|
|
66
|
+
font: (family) => {
|
|
67
|
+
const own = fontFamilyFor(family, fonts);
|
|
68
|
+
if (own)
|
|
69
|
+
return own;
|
|
70
|
+
const weight = family === tokens.font.heading && family !== tokens.font.body
|
|
71
|
+
? tokens.font.weights.title : tokens.font.weights.body;
|
|
72
|
+
const f = faceFor(family, weight);
|
|
73
|
+
return f ? faces[faceKey(f.family, f.weight)] : undefined;
|
|
74
|
+
},
|
|
75
|
+
face: (family, weight) => textFace(family, weight, fonts, faces, fontFamilyFor),
|
|
64
76
|
durationMs,
|
|
65
77
|
reduceMotion: reduce,
|
|
66
78
|
easing: easingOf(tokens.motion.easing, tokens.motion.bounce),
|
|
67
79
|
};
|
|
68
|
-
}, [design, fonts, device, reduce, scheme]);
|
|
80
|
+
}, [design, fonts, faces, device, reduce, scheme]);
|
|
69
81
|
}
|
|
70
82
|
export function useGraineTheme() {
|
|
71
83
|
return useDesign().tokens;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { GraineVoiceApi } from "./voice-launcher.js";
|
|
3
|
+
export interface GraineVoiceBarProps {
|
|
4
|
+
api: GraineVoiceApi;
|
|
5
|
+
onClose?: () => void;
|
|
6
|
+
bottomInset?: number;
|
|
7
|
+
draggable?: boolean;
|
|
8
|
+
placement?: {
|
|
9
|
+
bottomOffset?: number;
|
|
10
|
+
} | null;
|
|
11
|
+
onPlacementChange?: (placement: {
|
|
12
|
+
bottomOffset: number;
|
|
13
|
+
}) => void;
|
|
14
|
+
name?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function GraineVoiceBar({ api, onClose, bottomInset, draggable, placement, onPlacementChange, name: nameProp, }: GraineVoiceBarProps): React.JSX.Element;
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useContext, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { Animated, Easing, Image, PixelRatio, Platform, Pressable, ScrollView, Text, TextInput, View } from "react-native";
|
|
4
|
+
import { Ctx, useGraineAgent } from "./index.js";
|
|
5
|
+
import { GraineLoader } from "./loader.js";
|
|
6
|
+
import { GraineWidget } from "./widget.js";
|
|
7
|
+
import { resolveNativeAppearance } from "./appearance.js";
|
|
8
|
+
import { useDesign } from "./use-design.js";
|
|
9
|
+
import { glyphPixels, launcherRadius, letterSpacing, lineHeight, shadowStyle, surfaceFill } from "./design-math.js";
|
|
10
|
+
import { useKeyboardLift } from "./keyboard.js";
|
|
11
|
+
import { useDraggablePlacement } from "./placement.js";
|
|
12
|
+
import { glyphUrl, SDK_ICONS } from "../theme.js";
|
|
13
|
+
import { TitleBar, windowed } from "./window-chrome.js";
|
|
14
|
+
const BAR_HEIGHT = 60;
|
|
15
|
+
const BUTTON = 38;
|
|
16
|
+
const METER = [0.55, 1, 0.7, 0.4];
|
|
17
|
+
function statusOf(api, isChat, chatConnected) {
|
|
18
|
+
const conn = api.connectivity?.state;
|
|
19
|
+
const say = (text) => ({ text, waiting: null, meter: false });
|
|
20
|
+
if (api.error)
|
|
21
|
+
return say("Unavailable");
|
|
22
|
+
if (isChat)
|
|
23
|
+
return chatConnected ? say("Online") : { text: "Connecting…", waiting: "connecting", meter: false };
|
|
24
|
+
if (api.network === "offline")
|
|
25
|
+
return say("No connection");
|
|
26
|
+
if (api.reconnecting || conn === "retrying")
|
|
27
|
+
return { text: "Reconnecting…", waiting: "reconnecting", meter: false };
|
|
28
|
+
if (api.micDenied)
|
|
29
|
+
return say("Needs microphone");
|
|
30
|
+
if (conn === "failed")
|
|
31
|
+
return say("Couldn’t load · tap to retry");
|
|
32
|
+
if (!api.ready || api.connecting)
|
|
33
|
+
return { text: "Connecting…", waiting: "connecting", meter: false };
|
|
34
|
+
if (api.muted)
|
|
35
|
+
return say("Muted");
|
|
36
|
+
if (api.connected && api.phase === "thinking")
|
|
37
|
+
return { text: "Thinking…", waiting: "thinking", meter: false };
|
|
38
|
+
if (api.connected && api.caption?.role === "agent" && api.caption.live)
|
|
39
|
+
return { text: "Speaking", waiting: null, meter: true };
|
|
40
|
+
if (api.connected && api.quality === "poor")
|
|
41
|
+
return say("Weak connection");
|
|
42
|
+
if (api.connected)
|
|
43
|
+
return { text: "Listening", waiting: null, meter: true };
|
|
44
|
+
return say("Tap to talk");
|
|
45
|
+
}
|
|
46
|
+
export function GraineVoiceBar({ api, onClose, bottomInset, draggable = true, placement, onPlacementChange, name: nameProp, }) {
|
|
47
|
+
const ctx = useContext(Ctx);
|
|
48
|
+
const { messages, send, connected: chatConnected, appearance, surface, canSwitch, setSurface, launcherInset } = useGraineAgent();
|
|
49
|
+
const look = useDesign();
|
|
50
|
+
const t = look.tokens;
|
|
51
|
+
const c = t.colors;
|
|
52
|
+
const kit = ctx?.svg ?? null;
|
|
53
|
+
const origin = ctx?.client?.opts?.baseUrl || "https://www.graine.ai";
|
|
54
|
+
const dash = resolveNativeAppearance(appearance, { accent: c.accent, heading: "Assistant" });
|
|
55
|
+
const heading = dash.heading;
|
|
56
|
+
const spoken = nameProp || heading;
|
|
57
|
+
const isChat = surface === "chat";
|
|
58
|
+
const keyboardLift = useKeyboardLift();
|
|
59
|
+
const baseBottom = bottomInset ?? launcherInset?.bottom ?? 96;
|
|
60
|
+
const drag = useDraggablePlacement({
|
|
61
|
+
baseBottom,
|
|
62
|
+
enabled: draggable,
|
|
63
|
+
initialOffset: placement?.bottomOffset,
|
|
64
|
+
onChange: (bottomOffset) => onPlacementChange?.({ bottomOffset }),
|
|
65
|
+
});
|
|
66
|
+
const status = statusOf(api, isChat, chatConnected);
|
|
67
|
+
const conn = api.connectivity?.state;
|
|
68
|
+
const micBlocked = !!api.error || api.micDenied || api.connecting || (!api.ready && conn !== "failed");
|
|
69
|
+
const caption = !isChat && api.captionsOn && api.caption?.text ? api.caption : null;
|
|
70
|
+
const body = (size, weight) => ({
|
|
71
|
+
...look.face(t.font.body, weight),
|
|
72
|
+
fontSize: size,
|
|
73
|
+
letterSpacing: letterSpacing(size, t.font.letterSpacing.body),
|
|
74
|
+
});
|
|
75
|
+
const endCall = () => {
|
|
76
|
+
api.end();
|
|
77
|
+
setTimeout(() => onClose?.(), 400);
|
|
78
|
+
};
|
|
79
|
+
return (_jsxs(View, { pointerEvents: "box-none", ...drag.panHandlers, onLayout: drag.onLayout, style: {
|
|
80
|
+
position: "absolute", zIndex: 900, gap: 8, left: 12, right: 12,
|
|
81
|
+
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: {
|
|
83
|
+
height: BAR_HEIGHT, paddingHorizontal: 9, flexDirection: "row", alignItems: "center", gap: 10,
|
|
84
|
+
backgroundColor: surfaceFill(c.surface, t), borderWidth: t.shape.borderWidth, borderColor: c.border,
|
|
85
|
+
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: () => {
|
|
87
|
+
if (conn === "failed") {
|
|
88
|
+
api.retry();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (api.connected)
|
|
92
|
+
api.setMuted(!api.muted);
|
|
93
|
+
else
|
|
94
|
+
api.start();
|
|
95
|
+
}, children: _jsx(Icon, { name: api.muted ? "micOff" : "mic", t: t, color: api.muted ? "#FFFFFF" : c.onAccent, kit: kit }) })), _jsx(RoundButton, { label: api.connected ? "End call" : "Close", fill: api.connected ? c.danger : c.accentSoft, onPress: endCall, children: _jsx(Icon, { name: api.connected ? "end" : "close", t: t, color: api.connected ? "#FFFFFF" : c.textMuted, kit: kit }) })] }) })] }));
|
|
96
|
+
}
|
|
97
|
+
function softShadow(t, level) {
|
|
98
|
+
return t.elevation.hardOffset ? {} : shadowStyle(t.elevation[level]);
|
|
99
|
+
}
|
|
100
|
+
function Raised({ t, radius, children }) {
|
|
101
|
+
const hard = t.elevation.hardOffset;
|
|
102
|
+
if (!hard)
|
|
103
|
+
return _jsx(_Fragment, { children: children });
|
|
104
|
+
return (_jsxs(View, { children: [_jsx(View, { pointerEvents: "none", style: { position: "absolute", left: hard.x, top: hard.y, right: -hard.x, bottom: -hard.y, borderRadius: radius, backgroundColor: t.colors.text } }), children] }));
|
|
105
|
+
}
|
|
106
|
+
function RoundButton({ label, fill, disabled = false, onPress, children, }) {
|
|
107
|
+
return (_jsx(Pressable, { onPress: onPress, disabled: disabled, hitSlop: 8, accessibilityRole: "button", accessibilityLabel: label, style: {
|
|
108
|
+
width: BUTTON, height: BUTTON, borderRadius: BUTTON / 2, alignItems: "center", justifyContent: "center",
|
|
109
|
+
backgroundColor: fill, opacity: disabled ? 0.4 : 1,
|
|
110
|
+
}, children: children }));
|
|
111
|
+
}
|
|
112
|
+
const FALLBACK = {
|
|
113
|
+
mic: "🎙", micOff: "🔇", end: "✕", send: "↑", chat: "💬", call: "📞", chevron: "⌄", close: "✕",
|
|
114
|
+
};
|
|
115
|
+
function Icon({ name, t, color, size = 18, kit, rotate }) {
|
|
116
|
+
if (!kit) {
|
|
117
|
+
return _jsx(Text, { style: { color, fontSize: Math.round(size * 0.8), lineHeight: size, textAlign: "center" }, children: FALLBACK[name] });
|
|
118
|
+
}
|
|
119
|
+
const { Svg, Path } = kit;
|
|
120
|
+
const icon = SDK_ICONS[name];
|
|
121
|
+
const style = t.icons.style;
|
|
122
|
+
const sharp = style === "sharp";
|
|
123
|
+
const filled = style === "filled";
|
|
124
|
+
const stroke = t.icons.stroke;
|
|
125
|
+
const common = { stroke: color, strokeLinecap: sharp ? "square" : "round", strokeLinejoin: sharp ? "miter" : "round" };
|
|
126
|
+
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
|
+
}
|
|
128
|
+
function Mark({ t, size, glyph, kit, origin, logoUrl, initial, cover = false, }) {
|
|
129
|
+
const on = t.colors.onAccent;
|
|
130
|
+
const g = t.icons.launcher;
|
|
131
|
+
if (g) {
|
|
132
|
+
if (kit) {
|
|
133
|
+
const { Svg, Path } = kit;
|
|
134
|
+
return (_jsx(Svg, { width: glyph, height: glyph, viewBox: "0 0 24 24", children: _jsx(Path, { d: g.path, fill: g.stroke ? "none" : on, stroke: g.stroke ? on : "none", strokeWidth: g.stroke ? t.icons.stroke : 0, strokeLinecap: "round", strokeLinejoin: "round" }) }));
|
|
135
|
+
}
|
|
136
|
+
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
|
+
}
|
|
138
|
+
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 });
|
|
140
|
+
}
|
|
141
|
+
return _jsx(Text, { style: { color: on, fontSize: Math.round(size * 0.4), fontWeight: "700" }, children: initial });
|
|
142
|
+
}
|
|
143
|
+
function Meter({ look, color }) {
|
|
144
|
+
const still = look.durationMs <= 0;
|
|
145
|
+
const bars = useRef(METER.map(() => new Animated.Value(1))).current;
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
if (still) {
|
|
148
|
+
bars.forEach((b) => b.setValue(1));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const period = Math.max(420, look.durationMs * 3.2);
|
|
152
|
+
const ease = Easing.inOut(Easing.ease);
|
|
153
|
+
const runs = bars.map((b, i) => {
|
|
154
|
+
b.setValue(0.35);
|
|
155
|
+
return Animated.sequence([
|
|
156
|
+
Animated.delay(i * 90),
|
|
157
|
+
Animated.loop(Animated.sequence([
|
|
158
|
+
Animated.timing(b, { toValue: 1, duration: period, easing: ease, useNativeDriver: true }),
|
|
159
|
+
Animated.timing(b, { toValue: 0.35, duration: period, easing: ease, useNativeDriver: true }),
|
|
160
|
+
])),
|
|
161
|
+
]);
|
|
162
|
+
});
|
|
163
|
+
runs.forEach((r) => r.start());
|
|
164
|
+
return () => runs.forEach((r) => r.stop());
|
|
165
|
+
}, [still, look.durationMs, bars]);
|
|
166
|
+
return (_jsx(View, { style: { flexDirection: "row", alignItems: "flex-end", height: 12, gap: 2 }, accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", children: METER.map((h, i) => (_jsx(Animated.View, { style: {
|
|
167
|
+
width: 2.5, height: Math.round(12 * h * 10) / 10, borderRadius: 1.25, backgroundColor: color,
|
|
168
|
+
transform: [{ scaleY: bars[i] }], transformOrigin: "bottom",
|
|
169
|
+
} }, i))) }));
|
|
170
|
+
}
|
|
171
|
+
function useRise(look) {
|
|
172
|
+
const v = useRef(new Animated.Value(look.durationMs > 0 ? 0 : 1)).current;
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
if (look.durationMs <= 0) {
|
|
175
|
+
v.setValue(1);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const a = Animated.timing(v, { toValue: 1, duration: look.durationMs, easing: look.easing, useNativeDriver: true });
|
|
179
|
+
a.start();
|
|
180
|
+
return () => a.stop();
|
|
181
|
+
}, []);
|
|
182
|
+
return { opacity: v, transform: [{ translateY: v.interpolate({ inputRange: [0, 1], outputRange: [8, 0] }) }] };
|
|
183
|
+
}
|
|
184
|
+
function CaptionCard({ look, role, text, live }) {
|
|
185
|
+
const t = look.tokens;
|
|
186
|
+
const c = t.colors;
|
|
187
|
+
const rise = useRise(look);
|
|
188
|
+
const win = windowed(t);
|
|
189
|
+
return (_jsx(Animated.View, { style: rise, children: _jsx(Raised, { t: t, level: "card", radius: t.shape.radius.lg, children: _jsxs(View, { style: {
|
|
190
|
+
backgroundColor: surfaceFill(c.surface, t), borderWidth: t.shape.borderWidth, borderColor: c.border,
|
|
191
|
+
borderRadius: t.shape.radius.lg, paddingHorizontal: 12, paddingVertical: 8, ...softShadow(t, "card"),
|
|
192
|
+
}, 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
|
+
}
|
|
194
|
+
function ChatWindow({ look, kit, origin, heading, logoUrl, messages, send, online, lifted, onCollapse, }) {
|
|
195
|
+
const t = look.tokens;
|
|
196
|
+
const c = t.colors;
|
|
197
|
+
const r = t.shape.radius;
|
|
198
|
+
const bw = t.shape.borderWidth;
|
|
199
|
+
const pad = t.spacing.cardPadding;
|
|
200
|
+
const rise = useRise(look);
|
|
201
|
+
const [draft, setDraft] = useState("");
|
|
202
|
+
const scroll = useRef(null);
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
const id = setTimeout(() => scroll.current?.scrollToEnd?.({ animated: true }), 40);
|
|
205
|
+
return () => clearTimeout(id);
|
|
206
|
+
}, [messages.length]);
|
|
207
|
+
const submit = () => {
|
|
208
|
+
const text = draft.trim();
|
|
209
|
+
if (!text)
|
|
210
|
+
return;
|
|
211
|
+
setDraft("");
|
|
212
|
+
send(text);
|
|
213
|
+
};
|
|
214
|
+
const markShape = t.shape.launcher === "pill" ? "circle" : t.shape.launcher;
|
|
215
|
+
const text = (size, weight) => ({
|
|
216
|
+
...look.face(t.font.body, weight), fontSize: size,
|
|
217
|
+
letterSpacing: letterSpacing(size, t.font.letterSpacing.body),
|
|
218
|
+
});
|
|
219
|
+
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) => {
|
|
221
|
+
const b = bubble(t, m.role !== "agent");
|
|
222
|
+
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
|
+
})] }), _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: {
|
|
224
|
+
flex: 1, height: 40, paddingHorizontal: 14, backgroundColor: c.surfaceRaised, borderWidth: bw, borderColor: c.border,
|
|
225
|
+
borderRadius: r.xl, color: c.text, ...look.face(t.font.body, t.font.weights.body), fontSize: t.font.scale.body - 1,
|
|
226
|
+
} }), _jsx(Pressable, { onPress: submit, disabled: !draft.trim(), accessibilityRole: "button", accessibilityLabel: "Send", style: { width: 40, height: 40, borderRadius: 20, alignItems: "center", justifyContent: "center", backgroundColor: c.accent, opacity: draft.trim() ? 1 : 0.4 }, children: _jsx(Icon, { name: "send", t: t, color: c.onAccent, kit: kit }) })] })] }) }) }) }));
|
|
227
|
+
}
|
|
228
|
+
function bubble(t, user) {
|
|
229
|
+
const c = t.colors;
|
|
230
|
+
const r = t.shape.radius;
|
|
231
|
+
const pad = t.spacing.cardPadding;
|
|
232
|
+
const shape = {
|
|
233
|
+
alignSelf: user ? "flex-end" : "flex-start",
|
|
234
|
+
paddingHorizontal: pad, paddingVertical: Math.round(pad * 0.75), borderRadius: r.lg,
|
|
235
|
+
...(user ? { borderBottomRightRadius: r.sm } : { borderBottomLeftRadius: r.sm }),
|
|
236
|
+
};
|
|
237
|
+
if (t.bubbles === "outline") {
|
|
238
|
+
return { box: { ...shape, backgroundColor: "transparent", borderWidth: t.shape.borderWidth, borderColor: user ? c.accent : c.border }, color: c.text };
|
|
239
|
+
}
|
|
240
|
+
if (t.bubbles === "minimal") {
|
|
241
|
+
return user
|
|
242
|
+
? { box: { ...shape, backgroundColor: c.accentSoft }, color: c.text }
|
|
243
|
+
: { box: { alignSelf: "flex-start", paddingHorizontal: 2, paddingVertical: 2, backgroundColor: "transparent" }, color: c.text };
|
|
244
|
+
}
|
|
245
|
+
return user
|
|
246
|
+
? { box: { ...shape, backgroundColor: c.userBubble }, color: c.userBubbleText }
|
|
247
|
+
: { box: { ...shape, backgroundColor: c.agentBubble }, color: c.agentBubbleText };
|
|
248
|
+
}
|
|
249
|
+
class WidgetGuard extends React.Component {
|
|
250
|
+
constructor() {
|
|
251
|
+
super(...arguments);
|
|
252
|
+
this.state = { failed: false };
|
|
253
|
+
}
|
|
254
|
+
static getDerivedStateFromError() { return { failed: true }; }
|
|
255
|
+
componentDidCatch(error) { console.error("[graine] the agent's component failed to render", error); }
|
|
256
|
+
render() { return this.state.failed ? null : this.props.children; }
|
|
257
|
+
}
|
|
@@ -59,6 +59,8 @@ export interface GraineVoiceApi {
|
|
|
59
59
|
quality: ConnectionQuality;
|
|
60
60
|
network: "online" | "offline" | "unknown";
|
|
61
61
|
reconnecting: boolean;
|
|
62
|
+
error: string | null;
|
|
63
|
+
micDenied: boolean;
|
|
62
64
|
}
|
|
63
65
|
export declare const CAPTION_HISTORY = 12;
|
|
64
66
|
export declare function GraineVoiceLauncher({ webView: WebView, autoStart, active: activeProp, transport, onTransport, onCaption, onCallState, onMicDenied, onError, timing, onEnded, children, }: GraineVoiceLauncherProps): React.JSX.Element;
|
|
@@ -20,6 +20,8 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
20
20
|
const [connected, setConnected] = useState(false);
|
|
21
21
|
const [connecting, setConnecting] = useState(false);
|
|
22
22
|
const [muted, setMutedState] = useState(false);
|
|
23
|
+
const [fault, setFault] = useState(null);
|
|
24
|
+
const [micDenied, setMicDenied] = useState(false);
|
|
23
25
|
const [ready, setReady] = useState(false);
|
|
24
26
|
const readyRef = useRef(false);
|
|
25
27
|
readyRef.current = ready;
|
|
@@ -214,6 +216,7 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
214
216
|
}
|
|
215
217
|
if (!(await ensureMic())) {
|
|
216
218
|
setConnecting(false);
|
|
219
|
+
setMicDenied(true);
|
|
217
220
|
onMicDenied?.("NotAllowedError");
|
|
218
221
|
client.emit("mic_denied", { reason: "NotAllowedError" });
|
|
219
222
|
return;
|
|
@@ -260,6 +263,7 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
260
263
|
message: msg.message,
|
|
261
264
|
});
|
|
262
265
|
if (msg.reason === "not_configured" && msg.message) {
|
|
266
|
+
setFault(String(msg.message));
|
|
263
267
|
onError?.(String(msg.message));
|
|
264
268
|
}
|
|
265
269
|
return;
|
|
@@ -346,6 +350,7 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
346
350
|
return;
|
|
347
351
|
case "graine:mic-denied":
|
|
348
352
|
client.emit("mic_denied", { reason: String(msg.reason || "NotAllowedError") });
|
|
353
|
+
setMicDenied(true);
|
|
349
354
|
onMicDenied?.(String(msg.reason || "denied"));
|
|
350
355
|
return;
|
|
351
356
|
case "graine:widget": {
|
|
@@ -387,6 +392,8 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
387
392
|
clearCaptions: useCallback(() => setCaptions([]), []),
|
|
388
393
|
start: useCallback(() => {
|
|
389
394
|
setConnecting(true);
|
|
395
|
+
setMicDenied(false);
|
|
396
|
+
setFault(null);
|
|
390
397
|
setCaptions([]);
|
|
391
398
|
setAwaitingAgent(false);
|
|
392
399
|
client.clearPendingWidget();
|
|
@@ -402,6 +409,8 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
402
409
|
quality,
|
|
403
410
|
network,
|
|
404
411
|
reconnecting,
|
|
412
|
+
error: fault,
|
|
413
|
+
micDenied,
|
|
405
414
|
setMuted: useCallback((m) => {
|
|
406
415
|
setMutedState(m);
|
|
407
416
|
post({ type: "graine:set-muted", muted: m });
|
|
@@ -449,6 +458,8 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
449
458
|
setConnected(false);
|
|
450
459
|
setConnecting(false);
|
|
451
460
|
setCaptions([]);
|
|
461
|
+
setMicDenied(false);
|
|
462
|
+
setFault(null);
|
|
452
463
|
client.clearPendingWidget();
|
|
453
464
|
onCallState?.({ connected: false, connecting: false });
|
|
454
465
|
}, [active]);
|
|
@@ -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,7 @@ 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>;
|
|
161
166
|
export declare function normaliseHex(value: unknown): string | null;
|
|
162
167
|
export declare function mixHex(a: string, b: string, t: number): string;
|
|
163
168
|
export declare function luminanceOf(hex: string): number;
|
|
@@ -170,6 +175,11 @@ export declare const LAUNCHER_ICONS: ReadonlyArray<{
|
|
|
170
175
|
path: string;
|
|
171
176
|
stroke: boolean;
|
|
172
177
|
}>;
|
|
178
|
+
export type SdkIconName = "mic" | "micOff" | "end" | "send" | "chat" | "call" | "chevron" | "close";
|
|
179
|
+
export declare const SDK_ICONS: Readonly<Record<SdkIconName, {
|
|
180
|
+
body?: string;
|
|
181
|
+
lines?: string;
|
|
182
|
+
}>>;
|
|
173
183
|
export declare const DESIGN_FONTS: ReadonlyArray<{
|
|
174
184
|
family: string;
|
|
175
185
|
category: "sans" | "serif" | "mono" | "display";
|
package/dist/theme.js
CHANGED
|
@@ -6,6 +6,7 @@ export const DESIGN_NEUTRALS = ["preset", "cool", "neutral", "warm"];
|
|
|
6
6
|
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
|
+
export const DESIGN_CHROMES = ["none", "window"];
|
|
9
10
|
const HEX6 = /^#?([0-9a-f]{6})$/i;
|
|
10
11
|
const HEX3 = /^#?([0-9a-f]{3})$/i;
|
|
11
12
|
export function normaliseHex(value) {
|
|
@@ -75,9 +76,20 @@ export const LAUNCHER_ICONS = [
|
|
|
75
76
|
{ id: "star", label: "Star", stroke: true, path: "M11.525 2.295a0.53 0.53 0 0 1 0.95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166 0.756a0.53 0.53 0 0 1 0.294 0.904l-3.736 3.638a2.123 2.123 0 0 0 -0.611 1.878l0.882 5.14a0.53 0.53 0 0 1 -0.771 0.56l-4.618 -2.428a2.122 2.122 0 0 0 -1.973 0L6.396 21.01a0.53 0.53 0 0 1 -0.77 -0.56l0.881 -5.139a2.122 2.122 0 0 0 -0.611 -1.879L2.16 9.795a0.53 0.53 0 0 1 0.294 -0.906l5.165 -0.755a2.122 2.122 0 0 0 1.597 -1.16z" },
|
|
76
77
|
{ id: "orbit", label: "Orbit", stroke: true, path: "M20.341 6.484A10 10 0 0 1 10.266 21.85M3.659 17.516A10 10 0 0 1 13.74 2.152M9 12a3 3 0 1 0 6 0a3 3 0 1 0 -6 0zM17 5a2 2 0 1 0 4 0a2 2 0 1 0 -4 0zM3 19a2 2 0 1 0 4 0a2 2 0 1 0 -4 0z" },
|
|
77
78
|
{ id: "sparkle-filled", label: "Sparkle (filled)", stroke: false, path: "M19.5 13.5a1.479 1.479 0 0 1 -0.977 1.401L13.688 16.688l-1.781 4.839a1.492 1.492 0 0 1 -2.801 0L7.312 16.688l-4.839 -1.781a1.492 1.492 0 0 1 0 -2.801L7.312 10.312l1.781 -4.839a1.492 1.492 0 0 1 2.801 0L13.688 10.312l4.839 1.781A1.479 1.479 0 0 1 19.5 13.5ZM14.25 4.5h1.5V6a0.75 0.75 0 0 0 1.5 0V4.5h1.5a0.75 0.75 0 0 0 0 -1.5H17.25V1.5a0.75 0.75 0 0 0 -1.5 0V3H14.25a0.75 0.75 0 0 0 0 1.5Zm8.25 3h-0.75V6.75a0.75 0.75 0 0 0 -1.5 0v0.75h-0.75a0.75 0.75 0 0 0 0 1.5h0.75v0.75a0.75 0.75 0 0 0 1.5 0V9h0.75a0.75 0.75 0 0 0 0 -1.5Z" },
|
|
79
|
+
{ 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" },
|
|
78
80
|
{ id: "chat-dots-filled", label: "Chat dots (filled)", stroke: false, path: "M12 2.25A9.75 9.75 0 0 0 3.392 16.582L2.328 19.775a1.5 1.5 0 0 0 1.897 1.897l3.192 -1.064A9.75 9.75 0 1 0 12 2.25ZM7.875 13.125a1.125 1.125 0 1 1 1.125 -1.125A1.125 1.125 0 0 1 7.875 13.125Zm4.125 0a1.125 1.125 0 1 1 1.125 -1.125A1.125 1.125 0 0 1 12 13.125Zm4.125 0a1.125 1.125 0 1 1 1.125 -1.125A1.125 1.125 0 0 1 16.125 13.125Z" },
|
|
79
81
|
{ id: "graphic-eq", label: "Voice equalizer (filled)", stroke: false, path: "M7 17v-10q0 -0.425 0.288 -0.713T8 6q0.425 0 0.713 0.288T9 7v10q0 0.425 -0.288 0.713T8 18q-0.425 0 -0.713 -0.288T7 17Zm4 4v-18q0 -0.425 0.288 -0.713T12 2q0.425 0 0.713 0.288T13 3v18q0 0.425 -0.288 0.713T12 22q-0.425 0 -0.713 -0.288T11 21ZM3 13v-2q0 -0.425 0.288 -0.713T4 10q0.425 0 0.713 0.288T5 11v2q0 0.425 -0.288 0.713T4 14q-0.425 0 -0.713 -0.288T3 13Zm12 4v-10q0 -0.425 0.288 -0.713T16 6q0.425 0 0.713 0.288T17 7v10q0 0.425 -0.288 0.713T16 18q-0.425 0 -0.713 -0.288T15 17Zm4 -4v-2q0 -0.425 0.288 -0.713T20 10q0.425 0 0.713 0.288T21 11v2q0 0.425 -0.288 0.713T20 14q-0.425 0 -0.713 -0.288T19 13Z" },
|
|
80
82
|
];
|
|
83
|
+
export const SDK_ICONS = {
|
|
84
|
+
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" },
|
|
85
|
+
micOff: { body: "M12 2.5a3.5 3.5 0 0 0-3.5 3.5v6a3.5 3.5 0 0 0 7 0V6A3.5 3.5 0 0 0 12 2.5z", lines: "M18.5 11.5a6.5 6.5 0 0 1-13 0M12 18v3.5M4 4l16 16" },
|
|
86
|
+
end: { body: "M4.2 14.6c4.6-4.1 11-4.1 15.6 0 .6.5.6 1.4.1 2l-1.3 1.4c-.4.5-1.1.6-1.7.3l-2.2-1.2c-.5-.3-.8-.8-.7-1.4l.2-1.3a11 11 0 0 0-4.4 0l.2 1.3c.1.6-.2 1.1-.7 1.4l-2.2 1.2c-.6.3-1.3.2-1.7-.3L4.1 16.6c-.5-.6-.5-1.5.1-2z" },
|
|
87
|
+
send: { body: "M4 12 20 4l-4 16-4.5-6.5L4 12z", lines: "M11.5 13.5 20 4" },
|
|
88
|
+
chat: { body: "M20 12a8 8 0 0 1-11.6 7.1L4 20l1-4.2A8 8 0 1 1 20 12z" },
|
|
89
|
+
call: { lines: "M13.832 16.568a1 1 0 0 0 1.213 -0.303l0.355 -0.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1 -2 2A18 18 0 0 1 2 4a2 2 0 0 1 2 -2h3a2 2 0 0 1 2 2v3a2 2 0 0 1 -0.8 1.6l-0.468 0.351a1 1 0 0 0 -0.292 1.233a14 14 0 0 0 6.392 6.384" },
|
|
90
|
+
chevron: { lines: "M6 15l6-6 6 6" },
|
|
91
|
+
close: { lines: "M6 6l12 12M18 6 6 18" },
|
|
92
|
+
};
|
|
81
93
|
export const DESIGN_FONTS = [
|
|
82
94
|
{ family: "system", category: "sans", scripts: ["latin", "devanagari", "bengali", "tamil", "telugu", "kannada", "malayalam", "gujarati", "gurmukhi", "oriya"] },
|
|
83
95
|
{ family: "Inter", category: "sans", scripts: ["latin", "latin-ext", "cyrillic", "cyrillic-ext", "greek", "greek-ext", "vietnamese"] },
|
|
@@ -1061,6 +1073,121 @@ const P_SOFT_PLAYFUL = {
|
|
|
1061
1073
|
}
|
|
1062
1074
|
}
|
|
1063
1075
|
};
|
|
1076
|
+
const P_DESKTOP_BUDDY = {
|
|
1077
|
+
"id": "desktop-buddy",
|
|
1078
|
+
"name": "Desktop Buddy",
|
|
1079
|
+
"tagline": "A friendly little computer — clean white windows, a blue pointer and a bit of bounce.",
|
|
1080
|
+
"family": "modern",
|
|
1081
|
+
"inspiredBy": [
|
|
1082
|
+
"heyclicky.com — the playful desktop landing: Inter, glossy pills, a soft 0 10px 24px shadow, #0f7fff"
|
|
1083
|
+
],
|
|
1084
|
+
"font": {
|
|
1085
|
+
"body": "Inter",
|
|
1086
|
+
"heading": "Inter",
|
|
1087
|
+
"scale": {
|
|
1088
|
+
"caption": 12,
|
|
1089
|
+
"body": 15,
|
|
1090
|
+
"title": 18,
|
|
1091
|
+
"display": 28
|
|
1092
|
+
},
|
|
1093
|
+
"weights": {
|
|
1094
|
+
"body": 400,
|
|
1095
|
+
"strong": 600,
|
|
1096
|
+
"title": 600
|
|
1097
|
+
},
|
|
1098
|
+
"letterSpacing": {
|
|
1099
|
+
"body": 0,
|
|
1100
|
+
"title": -0.01
|
|
1101
|
+
},
|
|
1102
|
+
"lineHeight": {
|
|
1103
|
+
"body": 1.5,
|
|
1104
|
+
"title": 1.25
|
|
1105
|
+
}
|
|
1106
|
+
},
|
|
1107
|
+
"shape": {
|
|
1108
|
+
"radius": {
|
|
1109
|
+
"sm": 8,
|
|
1110
|
+
"md": 12,
|
|
1111
|
+
"lg": 16,
|
|
1112
|
+
"xl": 24
|
|
1113
|
+
},
|
|
1114
|
+
"borderWidth": 1,
|
|
1115
|
+
"launcher": "circle"
|
|
1116
|
+
},
|
|
1117
|
+
"spacing": {
|
|
1118
|
+
"unit": 4,
|
|
1119
|
+
"cardPadding": 16,
|
|
1120
|
+
"gap": 10
|
|
1121
|
+
},
|
|
1122
|
+
"elevation": {
|
|
1123
|
+
"card": {
|
|
1124
|
+
"y": 10,
|
|
1125
|
+
"blur": 24,
|
|
1126
|
+
"spread": 0,
|
|
1127
|
+
"opacity": 0.12,
|
|
1128
|
+
"color": "#000000"
|
|
1129
|
+
},
|
|
1130
|
+
"floating": {
|
|
1131
|
+
"y": 14,
|
|
1132
|
+
"blur": 32,
|
|
1133
|
+
"spread": 0,
|
|
1134
|
+
"opacity": 0.16,
|
|
1135
|
+
"color": "#000000"
|
|
1136
|
+
},
|
|
1137
|
+
"hardOffset": null
|
|
1138
|
+
},
|
|
1139
|
+
"motion": {
|
|
1140
|
+
"durationMs": 320,
|
|
1141
|
+
"easing": "spring",
|
|
1142
|
+
"bounce": 0.25
|
|
1143
|
+
},
|
|
1144
|
+
"icons": {
|
|
1145
|
+
"style": "outline",
|
|
1146
|
+
"stroke": 2,
|
|
1147
|
+
"launcher": "buddy"
|
|
1148
|
+
},
|
|
1149
|
+
"glass": null,
|
|
1150
|
+
"bubbles": "filled",
|
|
1151
|
+
"chrome": "window",
|
|
1152
|
+
"colors": {
|
|
1153
|
+
"light": {
|
|
1154
|
+
"background": "#FBFBFB",
|
|
1155
|
+
"surface": "#FFFFFF",
|
|
1156
|
+
"surfaceRaised": "#F5F5F5",
|
|
1157
|
+
"text": "#0A0A0A",
|
|
1158
|
+
"textMuted": "#626262",
|
|
1159
|
+
"border": "#E7E7E7",
|
|
1160
|
+
"accent": "#0F7FFF",
|
|
1161
|
+
"onAccent": "#FFFFFF",
|
|
1162
|
+
"success": "#1F9D55",
|
|
1163
|
+
"warning": "#FF7800",
|
|
1164
|
+
"danger": "#ED1B26",
|
|
1165
|
+
"userBubble": "#0F7FFF",
|
|
1166
|
+
"userBubbleText": "#FFFFFF",
|
|
1167
|
+
"agentBubble": "#F0F0F0",
|
|
1168
|
+
"agentBubbleText": "#0A0A0A",
|
|
1169
|
+
"scrim": "#000000"
|
|
1170
|
+
},
|
|
1171
|
+
"dark": {
|
|
1172
|
+
"background": "#0B0B0C",
|
|
1173
|
+
"surface": "#161618",
|
|
1174
|
+
"surfaceRaised": "#1F1F22",
|
|
1175
|
+
"text": "#F5F5F5",
|
|
1176
|
+
"textMuted": "#A1A1A6",
|
|
1177
|
+
"border": "#2C2C30",
|
|
1178
|
+
"accent": "#3D8BFF",
|
|
1179
|
+
"onAccent": "#FFFFFF",
|
|
1180
|
+
"success": "#34C759",
|
|
1181
|
+
"warning": "#FF9F0A",
|
|
1182
|
+
"danger": "#FF453A",
|
|
1183
|
+
"userBubble": "#3D8BFF",
|
|
1184
|
+
"userBubbleText": "#FFFFFF",
|
|
1185
|
+
"agentBubble": "#1F1F22",
|
|
1186
|
+
"agentBubbleText": "#F5F5F5",
|
|
1187
|
+
"scrim": "#000000"
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
};
|
|
1064
1191
|
const P_NEO_BRUTALISM = {
|
|
1065
1192
|
"id": "neo-brutalism",
|
|
1066
1193
|
"name": "Neo-Brutalist",
|
|
@@ -1751,7 +1878,7 @@ const P_HIGH_CONTRAST = {
|
|
|
1751
1878
|
}
|
|
1752
1879
|
}
|
|
1753
1880
|
};
|
|
1754
|
-
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_NEO_BRUTALISM, P_EDITORIAL, P_LUXURY, P_RETRO_TERMINAL, P_WARM_EARTHY, P_HIGH_CONTRAST];
|
|
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];
|
|
1755
1882
|
export const DEFAULT_DESIGN_PRESET = "graine-classic";
|
|
1756
1883
|
export function designPreset(id) {
|
|
1757
1884
|
const want = String(id ?? "").trim().toLowerCase();
|
|
@@ -1812,6 +1939,9 @@ export function normaliseDesignConfig(raw) {
|
|
|
1812
1939
|
const iconStyle = pick(r.iconStyle, ICON_STYLES);
|
|
1813
1940
|
if (iconStyle)
|
|
1814
1941
|
out.iconStyle = iconStyle;
|
|
1942
|
+
const chrome = pick(r.chrome, DESIGN_CHROMES);
|
|
1943
|
+
if (chrome)
|
|
1944
|
+
out.chrome = chrome;
|
|
1815
1945
|
return out;
|
|
1816
1946
|
}
|
|
1817
1947
|
export function designFromAppearance(appearance) {
|
|
@@ -1949,6 +2079,7 @@ export function resolveTokens(configInput, scheme) {
|
|
|
1949
2079
|
},
|
|
1950
2080
|
glass,
|
|
1951
2081
|
bubbles,
|
|
2082
|
+
chrome: config.chrome ?? p.chrome ?? "none",
|
|
1952
2083
|
};
|
|
1953
2084
|
}
|
|
1954
2085
|
export function resolveDesign(configInput) {
|
package/package.json
CHANGED