opendesk-react-native 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,163 @@
1
+ import { Image, Text, View, type ViewStyle } from 'react-native';
2
+ import type { BotLook } from '../types';
3
+
4
+ /** The widget's default bot when none is configured: a violet blob with pill eyes. */
5
+ export const defaultBotLook: BotLook = {
6
+ kind: 'built',
7
+ shape: 'blob',
8
+ fill: { type: 'gradient', color: '#c9bcff', to: '#4a2bc4' },
9
+ eyes: 'pills',
10
+ mouth: 'none',
11
+ };
12
+
13
+ /**
14
+ * The bot avatar the web widget draws from its `look` settings: an uploaded image, an emoji,
15
+ * or a built face. The face is laid out on the widget's 64-unit grid (body from 7 to 57) with
16
+ * plain Views, so it needs no SVG library.
17
+ */
18
+ export function BotFace({ size, look, avatarUrl }: { size: number; look?: BotLook | null; avatarUrl?: string | null }) {
19
+ const resolved = look ?? defaultBotLook;
20
+ const u = size / 64;
21
+
22
+ if (avatarUrl && (resolved.kind === 'image' || !look)) {
23
+ return <Image source={{ uri: avatarUrl }} style={{ width: size, height: size, borderRadius: size / 2 }} />;
24
+ }
25
+
26
+ const body = 50 * u;
27
+ const radius = { circle: body / 2, blob: body / 2, cloud: body * 0.42, squircle: body * 0.38, rounded: body * 0.28, hexagon: body * 0.3 }[
28
+ resolved.shape ?? 'blob'
29
+ ] ?? body / 2;
30
+ const top = resolved.fill?.color ?? '#c9bcff';
31
+ const bottom = resolved.fill?.type === 'gradient' ? (resolved.fill.to ?? top) : top;
32
+
33
+ if (resolved.kind === 'emoji' && resolved.emoji) {
34
+ return (
35
+ <View style={{ width: size, height: size, alignItems: 'center', justifyContent: 'center' }}>
36
+ <Text style={{ fontSize: size * 0.72, lineHeight: size }}>{resolved.emoji}</Text>
37
+ </View>
38
+ );
39
+ }
40
+
41
+ const at = (x: number, y: number, w: number, h: number): ViewStyle => ({
42
+ position: 'absolute',
43
+ left: x * u,
44
+ top: y * u,
45
+ width: w * u,
46
+ height: h * u,
47
+ });
48
+
49
+ return (
50
+ <View style={{ width: size, height: size }}>
51
+ <View style={[at(7, 7, 50, 50), { borderRadius: radius, overflow: 'hidden', backgroundColor: bottom }]}>
52
+ {top === bottom
53
+ ? null
54
+ : BANDS.map((index) => (
55
+ <View
56
+ key={index}
57
+ style={{
58
+ position: 'absolute',
59
+ left: 0,
60
+ right: 0,
61
+ top: `${(index * 100) / BANDS.length}%`,
62
+ height: `${100 / BANDS.length + 1}%`,
63
+ backgroundColor: mixHex(top, bottom, index / (BANDS.length - 1)),
64
+ }}
65
+ />
66
+ ))}
67
+ </View>
68
+ <View style={[at(15.5, 12.8, 13, 7), { borderRadius: 4 * u, backgroundColor: '#ffffff', opacity: 0.38, transform: [{ rotate: '-24deg' }] }]} />
69
+ <Eyes kind={resolved.eyes ?? 'pills'} at={at} u={u} />
70
+ <Mouth kind={resolved.mouth ?? 'none'} at={at} u={u} />
71
+ </View>
72
+ );
73
+ }
74
+
75
+ type At = (x: number, y: number, w: number, h: number) => ViewStyle;
76
+
77
+ /** Enough horizontal bands that a two-stop gradient reads smooth at avatar sizes, without an SVG library. */
78
+ const BANDS = Array.from({ length: 24 }, (_, index) => index);
79
+
80
+ /** Interpolates two hex colours; `amount` 0 is `from`, 1 is `to`. */
81
+ function mixHex(from: string, to: string, amount: number): string {
82
+ const parse = (hex: string) => {
83
+ const h = hex.replace('#', '');
84
+ const full = h.length === 3 ? h.split('').map((c) => c + c).join('') : h;
85
+ const n = parseInt(full, 16);
86
+
87
+ return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
88
+ };
89
+ const a = parse(from);
90
+ const b = parse(to);
91
+
92
+ return `#${a.map((channel, i) => Math.round(channel + (b[i]! - channel) * amount).toString(16).padStart(2, '0')).join('')}`;
93
+ }
94
+
95
+ function Eyes({ kind, at, u }: { kind: string; at: At; u: number }) {
96
+ const eye = (style: ViewStyle) => <View style={[style, { backgroundColor: '#ffffff' }]} />;
97
+
98
+ switch (kind) {
99
+ case 'dots':
100
+ return (
101
+ <>
102
+ {eye({ ...at(20.6, 26.6, 6.8, 6.8), borderRadius: 3.4 * u })}
103
+ {eye({ ...at(36.6, 26.6, 6.8, 6.8), borderRadius: 3.4 * u })}
104
+ </>
105
+ );
106
+ case 'ovals':
107
+ case 'glasses':
108
+ case 'sparkle':
109
+ case 'star':
110
+ return (
111
+ <>
112
+ {eye({ ...at(21.3, 25.9, 5.4, 8.2), borderRadius: 2.7 * u })}
113
+ {eye({ ...at(37.3, 25.9, 5.4, 8.2), borderRadius: 2.7 * u })}
114
+ </>
115
+ );
116
+ case 'happy':
117
+ case 'sleepy':
118
+ case 'wink':
119
+ return (
120
+ <>
121
+ {eye({ ...at(19.5, 27, 9, 5), borderTopLeftRadius: 4.5 * u, borderTopRightRadius: 4.5 * u })}
122
+ {eye({ ...at(35.5, 27, 9, 5), borderTopLeftRadius: 4.5 * u, borderTopRightRadius: 4.5 * u })}
123
+ </>
124
+ );
125
+ default:
126
+ return (
127
+ <>
128
+ {eye({ ...at(24.4, 28.35, 4.4, 7.3), borderRadius: 2.2 * u })}
129
+ {eye({ ...at(35.2, 28.35, 4.4, 7.3), borderRadius: 2.2 * u })}
130
+ </>
131
+ );
132
+ }
133
+ }
134
+
135
+ function Mouth({ kind, at, u }: { kind: string; at: At; u: number }) {
136
+ switch (kind) {
137
+ case 'smile':
138
+ case 'cat':
139
+ return (
140
+ <View
141
+ style={[
142
+ at(26.4, 37.6, 11.2, 6.2),
143
+ { borderBottomWidth: 1.6 * u, borderColor: '#ffffff', borderBottomLeftRadius: 6 * u, borderBottomRightRadius: 6 * u },
144
+ ]}
145
+ />
146
+ );
147
+ case 'grin':
148
+ case 'tongue':
149
+ return (
150
+ <View
151
+ style={[at(25.4, 38.4, 13.2, 8.2), { backgroundColor: '#ffffff', borderBottomLeftRadius: 6.6 * u, borderBottomRightRadius: 6.6 * u }]}
152
+ />
153
+ );
154
+ case 'open':
155
+ return <View style={[at(28.7, 38.1, 6.6, 7.8), { backgroundColor: '#ffffff', borderRadius: 3.3 * u }]} />;
156
+ case 'flat':
157
+ case 'tiny':
158
+ case 'smirk':
159
+ return <View style={[at(28, 41, 8, 1.6), { backgroundColor: '#ffffff', borderRadius: 0.8 * u }]} />;
160
+ default:
161
+ return null;
162
+ }
163
+ }