@ateam-ai-mobile/plugin-runtime 0.1.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.mts +1129 -0
- package/dist/index.d.ts +1129 -0
- package/dist/index.js +2388 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2369 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +31 -0
- package/src/api/plugins.ts +257 -0
- package/src/bridge/native.ts +266 -0
- package/src/bridge/types.ts +268 -0
- package/src/bundles/bundleManager.ts +396 -0
- package/src/bundles/storage.ts +68 -0
- package/src/hooks/usePlugin.ts +185 -0
- package/src/hooks/useRemotePlugins.ts +97 -0
- package/src/host.ts +117 -0
- package/src/index.ts +79 -0
- package/src/registry/SurfaceDispatcher.tsx +66 -0
- package/src/registry/store.ts +131 -0
- package/src/registry/types.ts +60 -0
- package/src/sdk/components.ts +370 -0
- package/src/sdk/index.ts +76 -0
- package/src/sdk/primitives.ts +22 -0
- package/src/sdk/registration.ts +52 -0
- package/src/sdk/types.ts +315 -0
- package/src/sdk/useApi.ts +78 -0
- package/src/sdk/usePluginTheme.ts +86 -0
- package/src/surfaces/AdaOrb.tsx +318 -0
- package/src/surfaces/SurfaceCard.tsx +137 -0
- package/src/surfaces/SurfaceDrawer.tsx +216 -0
- package/src/surfaces/SurfaceFullscreen.tsx +207 -0
- package/src/utils/sha256.ts +85 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { View, StyleSheet } from 'react-native';
|
|
3
|
+
import Animated, {
|
|
4
|
+
useSharedValue,
|
|
5
|
+
useAnimatedProps,
|
|
6
|
+
withRepeat,
|
|
7
|
+
withTiming,
|
|
8
|
+
withSequence,
|
|
9
|
+
withDelay,
|
|
10
|
+
Easing,
|
|
11
|
+
cancelAnimation,
|
|
12
|
+
} from 'react-native-reanimated';
|
|
13
|
+
import Svg, { Circle, Defs, RadialGradient, Stop, Text as SvgText } from 'react-native-svg';
|
|
14
|
+
|
|
15
|
+
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
|
|
16
|
+
|
|
17
|
+
export type AdaOrbState = 'idle' | 'listening' | 'thinking' | 'speaking';
|
|
18
|
+
|
|
19
|
+
interface AdaOrbProps {
|
|
20
|
+
size?: number;
|
|
21
|
+
state?: AdaOrbState;
|
|
22
|
+
/** Show the "iA" wordmark inside the orb. Suppressed below 60px because
|
|
23
|
+
* glyphs become unreadable at avatar/chevron sizes — render as a plain
|
|
24
|
+
* orb there. Default true. */
|
|
25
|
+
inscription?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default function AdaOrb({ size = 120, state = 'idle', inscription = true }: AdaOrbProps) {
|
|
29
|
+
const showInscription = inscription && size >= 60;
|
|
30
|
+
const center = size / 2;
|
|
31
|
+
const coreR = size * 0.32;
|
|
32
|
+
const innerHaloR = size * 0.42;
|
|
33
|
+
const outerHaloR = size * 0.5;
|
|
34
|
+
|
|
35
|
+
// Base breath layers
|
|
36
|
+
const coreScale = useSharedValue(1);
|
|
37
|
+
const innerScale = useSharedValue(1);
|
|
38
|
+
const outerScale = useSharedValue(1);
|
|
39
|
+
const outerOpacity = useSharedValue(0.55);
|
|
40
|
+
|
|
41
|
+
// Listening: continuous ripple cycle (one shared value, phase-shifted reads)
|
|
42
|
+
const rippleCycle = useSharedValue(0);
|
|
43
|
+
|
|
44
|
+
// Thinking: 3 orbiting sparks at different speeds + directions
|
|
45
|
+
const orbit1 = useSharedValue(0); // fast inner
|
|
46
|
+
const orbit2 = useSharedValue(0); // medium mid
|
|
47
|
+
const orbit3 = useSharedValue(0); // slow outer (counter)
|
|
48
|
+
|
|
49
|
+
// Speaking: secondary fast double-beat overlay
|
|
50
|
+
const beat = useSharedValue(1);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
cancelAnimation(coreScale);
|
|
54
|
+
cancelAnimation(innerScale);
|
|
55
|
+
cancelAnimation(outerScale);
|
|
56
|
+
cancelAnimation(outerOpacity);
|
|
57
|
+
cancelAnimation(rippleCycle);
|
|
58
|
+
cancelAnimation(orbit1);
|
|
59
|
+
cancelAnimation(orbit2);
|
|
60
|
+
cancelAnimation(orbit3);
|
|
61
|
+
cancelAnimation(beat);
|
|
62
|
+
|
|
63
|
+
// Reset state-specific drivers
|
|
64
|
+
rippleCycle.value = 0;
|
|
65
|
+
orbit1.value = 0;
|
|
66
|
+
orbit2.value = 0;
|
|
67
|
+
orbit3.value = 0;
|
|
68
|
+
beat.value = 1;
|
|
69
|
+
|
|
70
|
+
let breathMs = 3600;
|
|
71
|
+
let breathAmount = 0.025;
|
|
72
|
+
|
|
73
|
+
if (state === 'listening') {
|
|
74
|
+
breathMs = 2000;
|
|
75
|
+
breathAmount = 0.05;
|
|
76
|
+
// Continuous ripple loop — one cycle = one ripple lifetime
|
|
77
|
+
rippleCycle.value = withRepeat(
|
|
78
|
+
withTiming(1, { duration: 2000, easing: Easing.out(Easing.quad) }),
|
|
79
|
+
-1,
|
|
80
|
+
false,
|
|
81
|
+
);
|
|
82
|
+
} else if (state === 'thinking') {
|
|
83
|
+
breathMs = 2400;
|
|
84
|
+
breathAmount = 0.04;
|
|
85
|
+
// Three sparks orbiting at different speeds + directions — feels like processing
|
|
86
|
+
orbit1.value = withRepeat(
|
|
87
|
+
withTiming(1, { duration: 1100, easing: Easing.linear }),
|
|
88
|
+
-1,
|
|
89
|
+
false,
|
|
90
|
+
);
|
|
91
|
+
orbit2.value = withRepeat(
|
|
92
|
+
withTiming(1, { duration: 1900, easing: Easing.linear }),
|
|
93
|
+
-1,
|
|
94
|
+
false,
|
|
95
|
+
);
|
|
96
|
+
orbit3.value = withRepeat(
|
|
97
|
+
withTiming(-1, { duration: 2700, easing: Easing.linear }),
|
|
98
|
+
-1,
|
|
99
|
+
false,
|
|
100
|
+
);
|
|
101
|
+
} else if (state === 'speaking') {
|
|
102
|
+
breathMs = 700;
|
|
103
|
+
breathAmount = 0.13;
|
|
104
|
+
// Double-beat overlay — quick punch + secondary kick
|
|
105
|
+
beat.value = withRepeat(
|
|
106
|
+
withSequence(
|
|
107
|
+
withTiming(1.08, { duration: 90, easing: Easing.out(Easing.quad) }),
|
|
108
|
+
withTiming(1.0, { duration: 130, easing: Easing.in(Easing.quad) }),
|
|
109
|
+
withTiming(1.05, { duration: 80, easing: Easing.out(Easing.quad) }),
|
|
110
|
+
withTiming(1.0, { duration: 200, easing: Easing.in(Easing.quad) }),
|
|
111
|
+
withTiming(1.0, { duration: 280 }),
|
|
112
|
+
),
|
|
113
|
+
-1,
|
|
114
|
+
false,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Base breath — different easing per state
|
|
119
|
+
const breathEasing =
|
|
120
|
+
state === 'speaking' ? Easing.out(Easing.cubic) : Easing.inOut(Easing.sin);
|
|
121
|
+
|
|
122
|
+
coreScale.value = withRepeat(
|
|
123
|
+
withSequence(
|
|
124
|
+
withTiming(1 + breathAmount, { duration: breathMs, easing: breathEasing }),
|
|
125
|
+
withTiming(1, { duration: breathMs, easing: breathEasing }),
|
|
126
|
+
),
|
|
127
|
+
-1,
|
|
128
|
+
false,
|
|
129
|
+
);
|
|
130
|
+
innerScale.value = withRepeat(
|
|
131
|
+
withSequence(
|
|
132
|
+
withTiming(1 + breathAmount * 1.4, { duration: breathMs * 1.15, easing: breathEasing }),
|
|
133
|
+
withTiming(1, { duration: breathMs * 1.15, easing: breathEasing }),
|
|
134
|
+
),
|
|
135
|
+
-1,
|
|
136
|
+
false,
|
|
137
|
+
);
|
|
138
|
+
outerScale.value = withRepeat(
|
|
139
|
+
withSequence(
|
|
140
|
+
withTiming(1 + breathAmount * 1.8, { duration: breathMs * 1.3, easing: breathEasing }),
|
|
141
|
+
withTiming(1, { duration: breathMs * 1.3, easing: breathEasing }),
|
|
142
|
+
),
|
|
143
|
+
-1,
|
|
144
|
+
false,
|
|
145
|
+
);
|
|
146
|
+
outerOpacity.value = withRepeat(
|
|
147
|
+
withSequence(
|
|
148
|
+
withTiming(state === 'idle' ? 0.55 : 0.8, { duration: breathMs * 1.3, easing: breathEasing }),
|
|
149
|
+
withTiming(0.35, { duration: breathMs * 1.3, easing: breathEasing }),
|
|
150
|
+
),
|
|
151
|
+
-1,
|
|
152
|
+
false,
|
|
153
|
+
);
|
|
154
|
+
}, [state]);
|
|
155
|
+
|
|
156
|
+
// Animated props for base layers — multiplied by `beat` so speaking double-pulse stacks
|
|
157
|
+
const coreProps = useAnimatedProps(() => ({
|
|
158
|
+
r: coreR * coreScale.value * beat.value,
|
|
159
|
+
}));
|
|
160
|
+
const innerProps = useAnimatedProps(() => ({
|
|
161
|
+
r: innerHaloR * innerScale.value * beat.value,
|
|
162
|
+
}));
|
|
163
|
+
const outerProps = useAnimatedProps(() => ({
|
|
164
|
+
r: outerHaloR * outerScale.value,
|
|
165
|
+
opacity: outerOpacity.value,
|
|
166
|
+
}));
|
|
167
|
+
|
|
168
|
+
// Listening ripples — three phase-shifted reads of rippleCycle
|
|
169
|
+
const ripple1Props = useAnimatedProps(() => {
|
|
170
|
+
const t = rippleCycle.value;
|
|
171
|
+
return {
|
|
172
|
+
r: outerHaloR * (1 + t * 0.55),
|
|
173
|
+
opacity: state === 'listening' ? 0.55 * (1 - t) : 0,
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
const ripple2Props = useAnimatedProps(() => {
|
|
177
|
+
const t = (rippleCycle.value + 0.34) % 1;
|
|
178
|
+
return {
|
|
179
|
+
r: outerHaloR * (1 + t * 0.55),
|
|
180
|
+
opacity: state === 'listening' ? 0.45 * (1 - t) : 0,
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
const ripple3Props = useAnimatedProps(() => {
|
|
184
|
+
const t = (rippleCycle.value + 0.67) % 1;
|
|
185
|
+
return {
|
|
186
|
+
r: outerHaloR * (1 + t * 0.55),
|
|
187
|
+
opacity: state === 'listening' ? 0.35 * (1 - t) : 0,
|
|
188
|
+
};
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Thinking sparks — three at different radii and angular velocities
|
|
192
|
+
const spark1Props = useAnimatedProps(() => {
|
|
193
|
+
const angle = orbit1.value * Math.PI * 2 - Math.PI / 2;
|
|
194
|
+
const r = coreR * 0.55;
|
|
195
|
+
return {
|
|
196
|
+
cx: center + Math.cos(angle) * r,
|
|
197
|
+
cy: center + Math.sin(angle) * r,
|
|
198
|
+
opacity: state === 'thinking' ? 1 : 0,
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
const spark2Props = useAnimatedProps(() => {
|
|
202
|
+
const angle = orbit2.value * Math.PI * 2 + Math.PI * 0.6;
|
|
203
|
+
const r = coreR * 0.85;
|
|
204
|
+
return {
|
|
205
|
+
cx: center + Math.cos(angle) * r,
|
|
206
|
+
cy: center + Math.sin(angle) * r,
|
|
207
|
+
opacity: state === 'thinking' ? 0.9 : 0,
|
|
208
|
+
};
|
|
209
|
+
});
|
|
210
|
+
const spark3Props = useAnimatedProps(() => {
|
|
211
|
+
const angle = orbit3.value * Math.PI * 2 + Math.PI * 1.3;
|
|
212
|
+
const r = coreR * 1.05;
|
|
213
|
+
return {
|
|
214
|
+
cx: center + Math.cos(angle) * r,
|
|
215
|
+
cy: center + Math.sin(angle) * r,
|
|
216
|
+
opacity: state === 'thinking' ? 0.7 : 0,
|
|
217
|
+
};
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
<View style={[styles.wrap, { width: size, height: size }]}>
|
|
222
|
+
<Svg width={size} height={size}>
|
|
223
|
+
<Defs>
|
|
224
|
+
<RadialGradient id="adaCore" cx="36%" cy="30%" r="70%">
|
|
225
|
+
<Stop offset="0%" stopColor="#FFE0B0" stopOpacity="1" />
|
|
226
|
+
<Stop offset="28%" stopColor="#FFA85A" stopOpacity="1" />
|
|
227
|
+
<Stop offset="65%" stopColor="#FF7A28" stopOpacity="1" />
|
|
228
|
+
<Stop offset="100%" stopColor="#B8410A" stopOpacity="1" />
|
|
229
|
+
</RadialGradient>
|
|
230
|
+
<RadialGradient id="adaInner" cx="50%" cy="50%" r="50%">
|
|
231
|
+
<Stop offset="0%" stopColor="#FF8A3C" stopOpacity="0.55" />
|
|
232
|
+
<Stop offset="100%" stopColor="#FF7028" stopOpacity="0" />
|
|
233
|
+
</RadialGradient>
|
|
234
|
+
<RadialGradient id="adaOuter" cx="50%" cy="50%" r="50%">
|
|
235
|
+
<Stop offset="0%" stopColor="#FF7A28" stopOpacity="0.4" />
|
|
236
|
+
<Stop offset="100%" stopColor="#FF6018" stopOpacity="0" />
|
|
237
|
+
</RadialGradient>
|
|
238
|
+
<RadialGradient id="adaSpark" cx="50%" cy="50%" r="50%">
|
|
239
|
+
<Stop offset="0%" stopColor="#FFFAEB" stopOpacity="1" />
|
|
240
|
+
<Stop offset="60%" stopColor="#FFD095" stopOpacity="0.9" />
|
|
241
|
+
<Stop offset="100%" stopColor="#FFA85A" stopOpacity="0" />
|
|
242
|
+
</RadialGradient>
|
|
243
|
+
</Defs>
|
|
244
|
+
|
|
245
|
+
{/* Listening ripples — drawn behind orb body */}
|
|
246
|
+
<AnimatedCircle
|
|
247
|
+
cx={center}
|
|
248
|
+
cy={center}
|
|
249
|
+
stroke="#FF7A28"
|
|
250
|
+
strokeWidth={1.5}
|
|
251
|
+
fill="none"
|
|
252
|
+
animatedProps={ripple1Props}
|
|
253
|
+
/>
|
|
254
|
+
<AnimatedCircle
|
|
255
|
+
cx={center}
|
|
256
|
+
cy={center}
|
|
257
|
+
stroke="#FF7A28"
|
|
258
|
+
strokeWidth={1.5}
|
|
259
|
+
fill="none"
|
|
260
|
+
animatedProps={ripple2Props}
|
|
261
|
+
/>
|
|
262
|
+
<AnimatedCircle
|
|
263
|
+
cx={center}
|
|
264
|
+
cy={center}
|
|
265
|
+
stroke="#FF7A28"
|
|
266
|
+
strokeWidth={1.5}
|
|
267
|
+
fill="none"
|
|
268
|
+
animatedProps={ripple3Props}
|
|
269
|
+
/>
|
|
270
|
+
|
|
271
|
+
{/* Base orb body */}
|
|
272
|
+
<AnimatedCircle cx={center} cy={center} fill="url(#adaOuter)" animatedProps={outerProps} />
|
|
273
|
+
<AnimatedCircle cx={center} cy={center} fill="url(#adaInner)" animatedProps={innerProps} />
|
|
274
|
+
<AnimatedCircle cx={center} cy={center} fill="url(#adaCore)" animatedProps={coreProps} />
|
|
275
|
+
|
|
276
|
+
{/* Thinking sparks — three orbiting particles drawn on top */}
|
|
277
|
+
<AnimatedCircle
|
|
278
|
+
r={Math.max(3, size * 0.05)}
|
|
279
|
+
fill="url(#adaSpark)"
|
|
280
|
+
animatedProps={spark1Props}
|
|
281
|
+
/>
|
|
282
|
+
<AnimatedCircle
|
|
283
|
+
r={Math.max(2.5, size * 0.04)}
|
|
284
|
+
fill="url(#adaSpark)"
|
|
285
|
+
animatedProps={spark2Props}
|
|
286
|
+
/>
|
|
287
|
+
<AnimatedCircle
|
|
288
|
+
r={Math.max(2, size * 0.032)}
|
|
289
|
+
fill="url(#adaSpark)"
|
|
290
|
+
animatedProps={spark3Props}
|
|
291
|
+
/>
|
|
292
|
+
|
|
293
|
+
{/* "iA" wordmark — matches the app icon. Suppressed at small sizes
|
|
294
|
+
where the glyphs would be unreadable (see showInscription). */}
|
|
295
|
+
{showInscription && (
|
|
296
|
+
<SvgText
|
|
297
|
+
x={center}
|
|
298
|
+
y={center + size * 0.11}
|
|
299
|
+
fontSize={size * 0.34}
|
|
300
|
+
fontWeight="600"
|
|
301
|
+
fill="#FFF5E0"
|
|
302
|
+
textAnchor="middle"
|
|
303
|
+
letterSpacing={-size * 0.005}
|
|
304
|
+
>
|
|
305
|
+
iA
|
|
306
|
+
</SvgText>
|
|
307
|
+
)}
|
|
308
|
+
</Svg>
|
|
309
|
+
</View>
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const styles = StyleSheet.create({
|
|
314
|
+
wrap: {
|
|
315
|
+
alignItems: 'center',
|
|
316
|
+
justifyContent: 'center',
|
|
317
|
+
},
|
|
318
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SurfaceCard — a tappable tile shape for plugins that surface as cards
|
|
3
|
+
* (empty-state suggestions, dashboards, daily-brief items).
|
|
4
|
+
*
|
|
5
|
+
* Plugins declare icon + title + subtitle + onPress; the host provides
|
|
6
|
+
* the warm Ada chrome (paper-soft bg, soft border, arrow chevron, haptic).
|
|
7
|
+
*
|
|
8
|
+
* Optional `children` render below the title block for richer previews
|
|
9
|
+
* (e.g. small ring widget, last-meal photo, current weather).
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* <SurfaceCard
|
|
13
|
+
* icon="🥗"
|
|
14
|
+
* title="Today's plate"
|
|
15
|
+
* subtitle="2,100 kcal · 140g protein"
|
|
16
|
+
* onPress={() => sendPrompt('Tell me about my nutrition today')}
|
|
17
|
+
* />
|
|
18
|
+
*/
|
|
19
|
+
import { type ReactNode } from 'react';
|
|
20
|
+
import {
|
|
21
|
+
View,
|
|
22
|
+
Text,
|
|
23
|
+
Pressable,
|
|
24
|
+
StyleSheet,
|
|
25
|
+
Platform,
|
|
26
|
+
} from 'react-native';
|
|
27
|
+
import { getHaptics } from '../host';
|
|
28
|
+
import { ada, borderRadius } from '@ateam-ai-mobile/core';
|
|
29
|
+
|
|
30
|
+
const fontFam = Platform.OS === 'ios' ? 'SF Pro Rounded' : undefined;
|
|
31
|
+
|
|
32
|
+
export interface SurfaceCardProps {
|
|
33
|
+
/** Emoji or short string icon shown on the left */
|
|
34
|
+
icon?: string;
|
|
35
|
+
/** Card headline (e.g. "Today's plate") */
|
|
36
|
+
title: string;
|
|
37
|
+
/** Optional secondary line below title */
|
|
38
|
+
subtitle?: string;
|
|
39
|
+
/** Tap handler. Fires haptic before invoking. */
|
|
40
|
+
onPress?: () => void;
|
|
41
|
+
/** Long-press handler. Fires a heavier haptic. Used by user widgets for
|
|
42
|
+
* the Delete / (later) Hide / Rename context menu. */
|
|
43
|
+
onLongPress?: () => void;
|
|
44
|
+
/** Whether to show the ›-style trailing arrow (default: true if onPress) */
|
|
45
|
+
showArrow?: boolean;
|
|
46
|
+
/** Optional preview content rendered below the title block */
|
|
47
|
+
children?: ReactNode;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default function SurfaceCard({
|
|
51
|
+
icon,
|
|
52
|
+
title,
|
|
53
|
+
subtitle,
|
|
54
|
+
onPress,
|
|
55
|
+
onLongPress,
|
|
56
|
+
showArrow,
|
|
57
|
+
children,
|
|
58
|
+
}: SurfaceCardProps) {
|
|
59
|
+
const arrow = showArrow ?? !!onPress;
|
|
60
|
+
const handlePress = () => {
|
|
61
|
+
if (!onPress) return;
|
|
62
|
+
getHaptics()?.impact('light');
|
|
63
|
+
onPress();
|
|
64
|
+
};
|
|
65
|
+
const handleLongPress = () => {
|
|
66
|
+
if (!onLongPress) return;
|
|
67
|
+
getHaptics()?.impact('medium');
|
|
68
|
+
onLongPress();
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<Pressable
|
|
73
|
+
accessibilityRole="button"
|
|
74
|
+
accessibilityLabel={[icon, title, subtitle].filter(Boolean).join(', ')}
|
|
75
|
+
accessibilityHint={onLongPress ? 'Long-press for more options' : undefined}
|
|
76
|
+
style={styles.card}
|
|
77
|
+
onPress={onPress ? handlePress : undefined}
|
|
78
|
+
onLongPress={onLongPress ? handleLongPress : undefined}
|
|
79
|
+
disabled={!onPress && !onLongPress}
|
|
80
|
+
>
|
|
81
|
+
<View style={styles.row}>
|
|
82
|
+
{icon ? <Text style={styles.icon}>{icon}</Text> : null}
|
|
83
|
+
<View style={{ flex: 1 }}>
|
|
84
|
+
<Text style={styles.title}>{title}</Text>
|
|
85
|
+
{subtitle ? <Text style={styles.subtitle}>{subtitle}</Text> : null}
|
|
86
|
+
</View>
|
|
87
|
+
{arrow ? <Text style={styles.arrow}>{'\u2192'}</Text> : null}
|
|
88
|
+
</View>
|
|
89
|
+
{children ? <View style={styles.body}>{children}</View> : null}
|
|
90
|
+
</Pressable>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const styles = StyleSheet.create({
|
|
95
|
+
card: {
|
|
96
|
+
backgroundColor: ada.paperSoft,
|
|
97
|
+
borderWidth: 1,
|
|
98
|
+
borderColor: ada.border,
|
|
99
|
+
borderRadius: borderRadius.xl,
|
|
100
|
+
paddingHorizontal: 16,
|
|
101
|
+
paddingVertical: 14,
|
|
102
|
+
},
|
|
103
|
+
row: {
|
|
104
|
+
flexDirection: 'row',
|
|
105
|
+
alignItems: 'center',
|
|
106
|
+
gap: 14,
|
|
107
|
+
},
|
|
108
|
+
icon: {
|
|
109
|
+
fontSize: 22,
|
|
110
|
+
},
|
|
111
|
+
title: {
|
|
112
|
+
fontSize: 15,
|
|
113
|
+
fontWeight: '600',
|
|
114
|
+
color: ada.ink,
|
|
115
|
+
letterSpacing: -0.2,
|
|
116
|
+
fontFamily: fontFam,
|
|
117
|
+
},
|
|
118
|
+
subtitle: {
|
|
119
|
+
fontSize: 13,
|
|
120
|
+
fontWeight: '400',
|
|
121
|
+
color: ada.inkMuted,
|
|
122
|
+
marginTop: 2,
|
|
123
|
+
letterSpacing: -0.1,
|
|
124
|
+
fontFamily: fontFam,
|
|
125
|
+
},
|
|
126
|
+
arrow: {
|
|
127
|
+
fontSize: 18,
|
|
128
|
+
color: ada.inkMuted,
|
|
129
|
+
fontWeight: '400',
|
|
130
|
+
},
|
|
131
|
+
body: {
|
|
132
|
+
marginTop: 10,
|
|
133
|
+
paddingTop: 10,
|
|
134
|
+
borderTopWidth: 1,
|
|
135
|
+
borderTopColor: ada.border,
|
|
136
|
+
},
|
|
137
|
+
});
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SurfaceDrawer — generic bottom-sheet container for plugins.
|
|
3
|
+
*
|
|
4
|
+
* Plugins render their content; SurfaceDrawer provides the Ada chrome:
|
|
5
|
+
* drag handle, header (orb + title + subtitle + optional privacy pill),
|
|
6
|
+
* close button, motion, backdrop dismiss.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* <SurfaceDrawer
|
|
10
|
+
* visible={open}
|
|
11
|
+
* onClose={() => setOpen(false)}
|
|
12
|
+
* title="What I remember about you"
|
|
13
|
+
* subtitle="Only you can see this"
|
|
14
|
+
* privacy
|
|
15
|
+
* >
|
|
16
|
+
* <MyPluginContent />
|
|
17
|
+
* </SurfaceDrawer>
|
|
18
|
+
*/
|
|
19
|
+
import { useEffect, useRef, type ReactNode } from 'react';
|
|
20
|
+
import {
|
|
21
|
+
View,
|
|
22
|
+
Text,
|
|
23
|
+
Pressable,
|
|
24
|
+
StyleSheet,
|
|
25
|
+
Animated,
|
|
26
|
+
Dimensions,
|
|
27
|
+
Platform,
|
|
28
|
+
} from 'react-native';
|
|
29
|
+
import Svg, { Path } from 'react-native-svg';
|
|
30
|
+
import { ada } from '@ateam-ai-mobile/core';
|
|
31
|
+
import AdaOrb from './AdaOrb';
|
|
32
|
+
|
|
33
|
+
const SCREEN_H = Dimensions.get('window').height;
|
|
34
|
+
const fontFam = Platform.OS === 'ios' ? 'SF Pro Rounded' : undefined;
|
|
35
|
+
|
|
36
|
+
export interface SurfaceDrawerProps {
|
|
37
|
+
visible: boolean;
|
|
38
|
+
onClose: () => void;
|
|
39
|
+
title: string;
|
|
40
|
+
subtitle?: string;
|
|
41
|
+
/** Show the lock + "Only you can see this" privacy pill under the title */
|
|
42
|
+
privacy?: boolean;
|
|
43
|
+
/** Show the small Ada orb in the header (default: true) */
|
|
44
|
+
showOrb?: boolean;
|
|
45
|
+
/** Sheet height ratio of screen (default: 0.85) */
|
|
46
|
+
heightRatio?: number;
|
|
47
|
+
children: ReactNode;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default function SurfaceDrawer({
|
|
51
|
+
visible,
|
|
52
|
+
onClose,
|
|
53
|
+
title,
|
|
54
|
+
subtitle,
|
|
55
|
+
privacy,
|
|
56
|
+
showOrb = true,
|
|
57
|
+
heightRatio = 0.85,
|
|
58
|
+
children,
|
|
59
|
+
}: SurfaceDrawerProps) {
|
|
60
|
+
const sheetH = Math.round(SCREEN_H * heightRatio);
|
|
61
|
+
const fadeAnim = useRef(new Animated.Value(0)).current;
|
|
62
|
+
const slideAnim = useRef(new Animated.Value(sheetH)).current;
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (visible) {
|
|
66
|
+
Animated.parallel([
|
|
67
|
+
Animated.timing(fadeAnim, { toValue: 1, duration: 220, useNativeDriver: true }),
|
|
68
|
+
Animated.spring(slideAnim, { toValue: 0, useNativeDriver: true, tension: 80, friction: 12 }),
|
|
69
|
+
]).start();
|
|
70
|
+
} else {
|
|
71
|
+
fadeAnim.setValue(0);
|
|
72
|
+
slideAnim.setValue(sheetH);
|
|
73
|
+
}
|
|
74
|
+
}, [visible]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
75
|
+
|
|
76
|
+
const close = () => {
|
|
77
|
+
Animated.parallel([
|
|
78
|
+
Animated.timing(fadeAnim, { toValue: 0, duration: 160, useNativeDriver: true }),
|
|
79
|
+
Animated.timing(slideAnim, { toValue: sheetH, duration: 200, useNativeDriver: true }),
|
|
80
|
+
]).start(() => onClose());
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if (!visible) return null;
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<Animated.View style={[styles.overlay, { opacity: fadeAnim }]} pointerEvents="auto">
|
|
87
|
+
<Pressable style={StyleSheet.absoluteFill} onPress={close} />
|
|
88
|
+
<Animated.View
|
|
89
|
+
style={[
|
|
90
|
+
styles.sheet,
|
|
91
|
+
{ height: sheetH, transform: [{ translateY: slideAnim }] },
|
|
92
|
+
]}
|
|
93
|
+
>
|
|
94
|
+
{/* Drag handle */}
|
|
95
|
+
<View style={styles.handle} />
|
|
96
|
+
|
|
97
|
+
{/* Header */}
|
|
98
|
+
<View style={styles.header}>
|
|
99
|
+
{showOrb ? (
|
|
100
|
+
<View style={styles.headerOrb}>
|
|
101
|
+
<AdaOrb size={36} state="idle" />
|
|
102
|
+
</View>
|
|
103
|
+
) : null}
|
|
104
|
+
<View style={{ flex: 1 }}>
|
|
105
|
+
<Text style={styles.title}>{title}</Text>
|
|
106
|
+
{subtitle ? <Text style={styles.subtitle}>{subtitle}</Text> : null}
|
|
107
|
+
{privacy ? (
|
|
108
|
+
<View style={styles.privacyPill}>
|
|
109
|
+
<Svg width={11} height={11} viewBox="0 0 24 24" fill="none" stroke={ada.inkSoft} strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round">
|
|
110
|
+
<Path d="M19 11H5a2 2 0 00-2 2v7a2 2 0 002 2h14a2 2 0 002-2v-7a2 2 0 00-2-2z" />
|
|
111
|
+
<Path d="M7 11V7a5 5 0 0110 0v4" />
|
|
112
|
+
</Svg>
|
|
113
|
+
<Text style={styles.privacyText}>Only you can see this</Text>
|
|
114
|
+
</View>
|
|
115
|
+
) : null}
|
|
116
|
+
</View>
|
|
117
|
+
<Pressable
|
|
118
|
+
accessibilityRole="button"
|
|
119
|
+
accessibilityLabel="Close"
|
|
120
|
+
style={styles.closeBtn}
|
|
121
|
+
onPress={close}
|
|
122
|
+
hitSlop={12}
|
|
123
|
+
>
|
|
124
|
+
<Svg width={18} height={18} viewBox="0 0 24 24" fill="none" stroke={ada.inkSoft} strokeWidth={2.2} strokeLinecap="round">
|
|
125
|
+
<Path d="M18 6L6 18M6 6l12 12" />
|
|
126
|
+
</Svg>
|
|
127
|
+
</Pressable>
|
|
128
|
+
</View>
|
|
129
|
+
|
|
130
|
+
{/* Body — caller renders content */}
|
|
131
|
+
<View style={styles.body}>{children}</View>
|
|
132
|
+
</Animated.View>
|
|
133
|
+
</Animated.View>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const styles = StyleSheet.create({
|
|
138
|
+
overlay: {
|
|
139
|
+
position: 'absolute',
|
|
140
|
+
top: 0, left: 0, right: 0, bottom: 0,
|
|
141
|
+
backgroundColor: 'rgba(26, 20, 16, 0.35)',
|
|
142
|
+
justifyContent: 'flex-end',
|
|
143
|
+
zIndex: 999,
|
|
144
|
+
elevation: 999,
|
|
145
|
+
},
|
|
146
|
+
sheet: {
|
|
147
|
+
backgroundColor: ada.paper,
|
|
148
|
+
borderTopLeftRadius: 28,
|
|
149
|
+
borderTopRightRadius: 28,
|
|
150
|
+
overflow: 'hidden',
|
|
151
|
+
},
|
|
152
|
+
handle: {
|
|
153
|
+
width: 40,
|
|
154
|
+
height: 4,
|
|
155
|
+
borderRadius: 2,
|
|
156
|
+
backgroundColor: ada.inkMuted,
|
|
157
|
+
opacity: 0.3,
|
|
158
|
+
alignSelf: 'center',
|
|
159
|
+
marginTop: 10,
|
|
160
|
+
marginBottom: 6,
|
|
161
|
+
},
|
|
162
|
+
header: {
|
|
163
|
+
flexDirection: 'row',
|
|
164
|
+
alignItems: 'center',
|
|
165
|
+
paddingHorizontal: 22,
|
|
166
|
+
paddingVertical: 16,
|
|
167
|
+
gap: 12,
|
|
168
|
+
borderBottomWidth: 1,
|
|
169
|
+
borderBottomColor: ada.border,
|
|
170
|
+
},
|
|
171
|
+
headerOrb: {
|
|
172
|
+
width: 36,
|
|
173
|
+
height: 36,
|
|
174
|
+
alignItems: 'center',
|
|
175
|
+
justifyContent: 'center',
|
|
176
|
+
},
|
|
177
|
+
title: {
|
|
178
|
+
fontSize: 17,
|
|
179
|
+
fontWeight: '600',
|
|
180
|
+
color: ada.ink,
|
|
181
|
+
letterSpacing: -0.2,
|
|
182
|
+
fontFamily: fontFam,
|
|
183
|
+
},
|
|
184
|
+
subtitle: {
|
|
185
|
+
fontSize: 13,
|
|
186
|
+
fontWeight: '400',
|
|
187
|
+
color: ada.inkSoft,
|
|
188
|
+
marginTop: 2,
|
|
189
|
+
letterSpacing: -0.1,
|
|
190
|
+
fontFamily: fontFam,
|
|
191
|
+
},
|
|
192
|
+
privacyPill: {
|
|
193
|
+
flexDirection: 'row',
|
|
194
|
+
alignItems: 'center',
|
|
195
|
+
gap: 5,
|
|
196
|
+
marginTop: 4,
|
|
197
|
+
},
|
|
198
|
+
privacyText: {
|
|
199
|
+
fontSize: 11,
|
|
200
|
+
fontWeight: '500',
|
|
201
|
+
color: ada.inkSoft,
|
|
202
|
+
letterSpacing: 0.2,
|
|
203
|
+
fontFamily: fontFam,
|
|
204
|
+
},
|
|
205
|
+
closeBtn: {
|
|
206
|
+
width: 32,
|
|
207
|
+
height: 32,
|
|
208
|
+
borderRadius: 16,
|
|
209
|
+
backgroundColor: ada.paperSoft,
|
|
210
|
+
justifyContent: 'center',
|
|
211
|
+
alignItems: 'center',
|
|
212
|
+
},
|
|
213
|
+
body: {
|
|
214
|
+
flex: 1,
|
|
215
|
+
},
|
|
216
|
+
});
|