yaver-feedback-react-native 0.8.1 → 0.8.2
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/README.md +2 -2
- package/dist/AuthOverlay.js +1 -1
- package/dist/FeedbackModal.js +416 -214
- package/dist/P2PClient.d.ts +9 -3
- package/dist/P2PClient.js +23 -2
- package/dist/QuickActionIcon.js +16 -4
- package/dist/YaverFeedback.d.ts +5 -0
- package/dist/YaverFeedback.js +22 -0
- package/dist/__tests__/P2PClient.test.js +30 -0
- package/dist/__tests__/YaverFeedback.test.js +39 -0
- package/dist/index.d.ts +3 -4
- package/dist/index.js +3 -4
- package/dist/preferences.d.ts +11 -0
- package/dist/preferences.js +82 -0
- package/package.json +6 -4
- package/src/AuthOverlay.tsx +1 -0
- package/src/FeedbackModal.tsx +485 -257
- package/src/P2PClient.ts +36 -3
- package/src/QuickActionIcon.tsx +25 -5
- package/src/YaverFeedback.ts +29 -0
- package/src/__tests__/P2PClient.test.ts +40 -0
- package/src/__tests__/YaverFeedback.test.ts +42 -0
- package/src/index.ts +3 -4
- package/src/preferences.ts +96 -0
package/src/FeedbackModal.tsx
CHANGED
|
@@ -2,9 +2,12 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
2
2
|
import {
|
|
3
3
|
ActivityIndicator,
|
|
4
4
|
DeviceEventEmitter,
|
|
5
|
+
Keyboard,
|
|
6
|
+
KeyboardAvoidingView,
|
|
5
7
|
Modal,
|
|
6
8
|
Platform,
|
|
7
9
|
Pressable,
|
|
10
|
+
ScrollView,
|
|
8
11
|
StyleSheet,
|
|
9
12
|
Text,
|
|
10
13
|
TextInput,
|
|
@@ -13,40 +16,50 @@ import {
|
|
|
13
16
|
import { YaverFeedback } from './YaverFeedback';
|
|
14
17
|
import {
|
|
15
18
|
captureScreenshot,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
// Launch scope for the feedback test SDK is intentionally smaller for now.
|
|
20
|
+
// Keep the dormant file-upload and screen-recording helpers nearby, but
|
|
21
|
+
// comment them out until we bring them back with stronger test coverage.
|
|
22
|
+
// pickFeedbackFile,
|
|
23
|
+
// startVideoRecording,
|
|
24
|
+
// stopVideoRecording,
|
|
19
25
|
} from './capture';
|
|
20
26
|
import { uploadFeedback } from './upload';
|
|
21
27
|
import { DeviceInfo, FeedbackBundle } from './types';
|
|
22
28
|
import { AuthOverlay } from './AuthOverlay';
|
|
23
29
|
import { QuickActionIcon } from './QuickActionIcon';
|
|
30
|
+
import { listReachableDevices, RemoteDevice } from './auth';
|
|
31
|
+
import {
|
|
32
|
+
QUICK_ICON_COLOR_PRESETS,
|
|
33
|
+
QuickIconColorPreset,
|
|
34
|
+
} from './preferences';
|
|
24
35
|
|
|
25
36
|
/**
|
|
26
|
-
* Simplified feedback modal —
|
|
37
|
+
* Simplified feedback modal — launch scope is 3 actions:
|
|
27
38
|
*
|
|
28
39
|
* 1. Hot Reload — instant JS reload (most common use case)
|
|
29
40
|
* 2. Vibing — open a vibing session on the agent
|
|
30
|
-
* 3. Screenshot
|
|
31
|
-
* during capture)
|
|
32
|
-
*
|
|
33
|
-
* 4. Screen Recording — start recording, then stop + upload
|
|
41
|
+
* 3. Screenshot & Fix — capture the underlying app (modal hidden
|
|
42
|
+
* during capture), upload it, and trigger
|
|
43
|
+
* the fix loop
|
|
34
44
|
*
|
|
35
45
|
* The footer also has an explicit Cancel button so the icon tap path
|
|
36
46
|
* feels like a standard action sheet rather than a hidden modal.
|
|
37
47
|
*/
|
|
38
48
|
|
|
39
|
-
interface LastVideo {
|
|
40
|
-
path: string;
|
|
41
|
-
duration: number;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
49
|
type ActionState =
|
|
45
50
|
| 'idle'
|
|
46
51
|
| 'hot-reloading'
|
|
47
52
|
| 'capturing'
|
|
48
|
-
| 'vibing'
|
|
49
|
-
|
|
53
|
+
| 'vibing';
|
|
54
|
+
|
|
55
|
+
type MachineCardState = {
|
|
56
|
+
device: RemoteDevice | null;
|
|
57
|
+
reachable: boolean | null;
|
|
58
|
+
loading: boolean;
|
|
59
|
+
status: 'none' | 'live' | 'attention' | 'offline';
|
|
60
|
+
title: string;
|
|
61
|
+
detail: string;
|
|
62
|
+
};
|
|
50
63
|
|
|
51
64
|
export const FeedbackModal: React.FC = () => {
|
|
52
65
|
const [visible, setVisible] = useState(false);
|
|
@@ -54,8 +67,6 @@ export const FeedbackModal: React.FC = () => {
|
|
|
54
67
|
const [error, setError] = useState<string | null>(null);
|
|
55
68
|
const [toast, setToast] = useState<string | null>(null);
|
|
56
69
|
const [progress, setProgress] = useState<number | null>(null);
|
|
57
|
-
const [isRecordingVideo, setIsRecordingVideo] = useState(false);
|
|
58
|
-
const [lastVideo, setLastVideo] = useState<LastVideo | null>(null);
|
|
59
70
|
// Tracks whether the user has hidden the QuickActionIcon via its
|
|
60
71
|
// long-press menu. Shake is always available, so the feedback modal
|
|
61
72
|
// is our guaranteed UI for bringing the icon back — we surface a
|
|
@@ -68,10 +79,120 @@ export const FeedbackModal: React.FC = () => {
|
|
|
68
79
|
// the wrong project because the matcher grepped the prompt itself).
|
|
69
80
|
const [showVibeInput, setShowVibeInput] = useState(false);
|
|
70
81
|
const [vibePrompt, setVibePrompt] = useState('');
|
|
71
|
-
const [showCaptureChoices, setShowCaptureChoices] = useState(false);
|
|
72
82
|
const [lastVibeTaskId, setLastVibeTaskId] = useState<string | null>(null);
|
|
83
|
+
const [quickIconColorPreset, setQuickIconColorPreset] =
|
|
84
|
+
useState<QuickIconColorPreset | null>(null);
|
|
85
|
+
const [machineCard, setMachineCard] = useState<MachineCardState>({
|
|
86
|
+
device: null,
|
|
87
|
+
reachable: null,
|
|
88
|
+
loading: false,
|
|
89
|
+
status: 'none',
|
|
90
|
+
title: 'No machine selected',
|
|
91
|
+
detail: 'Pick a remote dev machine before using the feedback actions.',
|
|
92
|
+
});
|
|
73
93
|
const mountedRef = useRef(true);
|
|
74
94
|
|
|
95
|
+
const loadSelectedMachine = useCallback(async () => {
|
|
96
|
+
const cfg = YaverFeedback.getConfig();
|
|
97
|
+
if (!cfg?.authToken) {
|
|
98
|
+
if (mountedRef.current) {
|
|
99
|
+
setMachineCard({
|
|
100
|
+
device: null,
|
|
101
|
+
reachable: null,
|
|
102
|
+
loading: false,
|
|
103
|
+
status: 'none',
|
|
104
|
+
title: 'Not signed in',
|
|
105
|
+
detail: 'Sign in to pick and monitor a remote dev machine.',
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (!cfg.preferredDeviceId) {
|
|
111
|
+
if (mountedRef.current) {
|
|
112
|
+
setMachineCard({
|
|
113
|
+
device: null,
|
|
114
|
+
reachable: null,
|
|
115
|
+
loading: false,
|
|
116
|
+
status: 'none',
|
|
117
|
+
title: 'No machine selected',
|
|
118
|
+
detail: 'Choose which machine this SDK should talk to.',
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (mountedRef.current) {
|
|
125
|
+
setMachineCard((prev) => ({ ...prev, loading: true }));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const devices = await listReachableDevices(cfg.authToken);
|
|
130
|
+
const all = [...devices.owned, ...devices.shared];
|
|
131
|
+
const device =
|
|
132
|
+
all.find((candidate) => candidate.deviceId === cfg.preferredDeviceId) ?? null;
|
|
133
|
+
|
|
134
|
+
if (!device) {
|
|
135
|
+
if (mountedRef.current) {
|
|
136
|
+
setMachineCard({
|
|
137
|
+
device: null,
|
|
138
|
+
reachable: null,
|
|
139
|
+
loading: false,
|
|
140
|
+
status: 'offline',
|
|
141
|
+
title: 'Selected machine missing',
|
|
142
|
+
detail: 'The saved machine was not returned by the device list. Re-select it.',
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let reachable: boolean | null = null;
|
|
149
|
+
const client = YaverFeedback.getP2PClient();
|
|
150
|
+
if (device.isOnline && !device.needsAuth && client) {
|
|
151
|
+
reachable = await client.health();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const hostHint = device.hostEmail ? ` via ${device.hostEmail}` : '';
|
|
155
|
+
let status: MachineCardState['status'] = 'live';
|
|
156
|
+
let detail = `${device.platform}${hostHint}`;
|
|
157
|
+
|
|
158
|
+
if (!device.isOnline) {
|
|
159
|
+
status = 'offline';
|
|
160
|
+
detail = 'Machine offline. Start `yaver serve` on the selected machine.';
|
|
161
|
+
} else if (device.needsAuth) {
|
|
162
|
+
status = 'attention';
|
|
163
|
+
detail = 'Machine needs pairing again before feedback actions can run.';
|
|
164
|
+
} else if (device.runnerDown) {
|
|
165
|
+
status = 'attention';
|
|
166
|
+
detail = 'Machine is online but the coding agent is down.';
|
|
167
|
+
} else if (reachable === false) {
|
|
168
|
+
status = 'offline';
|
|
169
|
+
detail = 'Machine selected, but the agent is not responding.';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (mountedRef.current) {
|
|
173
|
+
setMachineCard({
|
|
174
|
+
device,
|
|
175
|
+
reachable,
|
|
176
|
+
loading: false,
|
|
177
|
+
status,
|
|
178
|
+
title: device.name || device.deviceId,
|
|
179
|
+
detail,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
} catch (err) {
|
|
183
|
+
if (mountedRef.current) {
|
|
184
|
+
setMachineCard({
|
|
185
|
+
device: null,
|
|
186
|
+
reachable: null,
|
|
187
|
+
loading: false,
|
|
188
|
+
status: 'offline',
|
|
189
|
+
title: 'Machine status unavailable',
|
|
190
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}, []);
|
|
195
|
+
|
|
75
196
|
useEffect(() => {
|
|
76
197
|
mountedRef.current = true;
|
|
77
198
|
const sub = DeviceEventEmitter.addListener('yaverFeedback:startReport', () => {
|
|
@@ -79,8 +200,10 @@ export const FeedbackModal: React.FC = () => {
|
|
|
79
200
|
setVisible(true);
|
|
80
201
|
setError(null);
|
|
81
202
|
setToast(null);
|
|
203
|
+
setProgress(null);
|
|
82
204
|
setAction('idle');
|
|
83
|
-
|
|
205
|
+
setShowVibeInput(false);
|
|
206
|
+
setVibePrompt('');
|
|
84
207
|
// Re-read the "user hid the quick icon" flag on every open so
|
|
85
208
|
// the re-enable row reflects the latest preference (the user
|
|
86
209
|
// might have hidden or shown it between opens).
|
|
@@ -89,6 +212,12 @@ export const FeedbackModal: React.FC = () => {
|
|
|
89
212
|
if (mountedRef.current) setQuickIconHidden(v);
|
|
90
213
|
})
|
|
91
214
|
.catch(() => {});
|
|
215
|
+
YaverFeedback.getQuickIconColorPreset()
|
|
216
|
+
.then((preset) => {
|
|
217
|
+
if (mountedRef.current) setQuickIconColorPreset(preset);
|
|
218
|
+
})
|
|
219
|
+
.catch(() => {});
|
|
220
|
+
void loadSelectedMachine();
|
|
92
221
|
}
|
|
93
222
|
});
|
|
94
223
|
// Agent streams build / compile progress through the BlackBox
|
|
@@ -117,7 +246,15 @@ export const FeedbackModal: React.FC = () => {
|
|
|
117
246
|
sub.remove();
|
|
118
247
|
statusSub.remove();
|
|
119
248
|
};
|
|
120
|
-
}, []);
|
|
249
|
+
}, [loadSelectedMachine]);
|
|
250
|
+
|
|
251
|
+
useEffect(() => {
|
|
252
|
+
if (!visible) return;
|
|
253
|
+
const interval = setInterval(() => {
|
|
254
|
+
void loadSelectedMachine();
|
|
255
|
+
}, 5000);
|
|
256
|
+
return () => clearInterval(interval);
|
|
257
|
+
}, [loadSelectedMachine, visible]);
|
|
121
258
|
|
|
122
259
|
const closeSoon = useCallback((delayMs = 1200) => {
|
|
123
260
|
setTimeout(() => {
|
|
@@ -129,8 +266,10 @@ export const FeedbackModal: React.FC = () => {
|
|
|
129
266
|
setVisible(false);
|
|
130
267
|
setError(null);
|
|
131
268
|
setToast(null);
|
|
269
|
+
setProgress(null);
|
|
132
270
|
setAction('idle');
|
|
133
|
-
|
|
271
|
+
setShowVibeInput(false);
|
|
272
|
+
setVibePrompt('');
|
|
134
273
|
}, []);
|
|
135
274
|
|
|
136
275
|
// Helper: run a P2P call; on network failure, ask YaverFeedback to
|
|
@@ -188,28 +327,54 @@ export const FeedbackModal: React.FC = () => {
|
|
|
188
327
|
setAction('hot-reloading');
|
|
189
328
|
setError(null);
|
|
190
329
|
setProgress(0);
|
|
191
|
-
setToast('
|
|
330
|
+
setToast('Contacting selected machine…');
|
|
192
331
|
try {
|
|
332
|
+
await loadSelectedMachine();
|
|
333
|
+
const selected = await YaverFeedback.getSelectedRemoteDevice();
|
|
334
|
+
if (!selected) {
|
|
335
|
+
YaverFeedback.showMachinePicker();
|
|
336
|
+
throw new Error('No machine selected. Pick a machine and try again.');
|
|
337
|
+
}
|
|
338
|
+
if (selected.needsAuth) {
|
|
339
|
+
YaverFeedback.showMachinePicker();
|
|
340
|
+
throw new Error('Selected machine needs pairing again.');
|
|
341
|
+
}
|
|
342
|
+
if (!selected.isOnline) {
|
|
343
|
+
throw new Error('Selected machine is offline. Start `yaver serve` on it first.');
|
|
344
|
+
}
|
|
345
|
+
|
|
193
346
|
// Default mode: bundle. Always rebuilds via the agent regardless
|
|
194
347
|
// of Metro state. P2PClient.reloadApp auto-resolves projectName +
|
|
195
348
|
// bundleId from expo-constants / NativeModules so the agent can
|
|
196
349
|
// map this app to its MobileProject scan entry without needing
|
|
197
350
|
// `yaver dev start` to have been run.
|
|
351
|
+
let ackMessage = 'Reload request acknowledged.';
|
|
198
352
|
await runWithReconnect(async (client) => {
|
|
199
|
-
await client.reloadApp('bundle');
|
|
353
|
+
const ack = await client.reloadApp('bundle');
|
|
354
|
+
ackMessage = ack.message;
|
|
355
|
+
setToast(ack.message);
|
|
356
|
+
setProgress(0.2);
|
|
200
357
|
});
|
|
201
358
|
// We don't auto-close here — the agent's BlackBox status pings
|
|
202
359
|
// will keep the modal updated, and the on-device YaverBundleLoader
|
|
203
360
|
// will reload the JS once the fresh bundle arrives. Modal stays
|
|
204
361
|
// up for a beat so the user sees the final progress state.
|
|
362
|
+
setToast(ackMessage);
|
|
205
363
|
closeSoon(2500);
|
|
206
364
|
} catch (err: unknown) {
|
|
207
|
-
|
|
365
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
366
|
+
setError(message);
|
|
367
|
+
setToast(
|
|
368
|
+
message.toLowerCase().indexOf('session expired') >= 0
|
|
369
|
+
? 'Session expired. Sign in again.'
|
|
370
|
+
: 'Hot reload did not start.',
|
|
371
|
+
);
|
|
372
|
+
await loadSelectedMachine();
|
|
208
373
|
setProgress(null);
|
|
209
374
|
} finally {
|
|
210
375
|
if (mountedRef.current) setAction('idle');
|
|
211
376
|
}
|
|
212
|
-
}, [closeSoon, runWithReconnect]);
|
|
377
|
+
}, [closeSoon, loadSelectedMachine, runWithReconnect]);
|
|
213
378
|
|
|
214
379
|
const uploadBundleWithOptionalFix = useCallback(async (
|
|
215
380
|
bundle: FeedbackBundle,
|
|
@@ -252,15 +417,9 @@ export const FeedbackModal: React.FC = () => {
|
|
|
252
417
|
}
|
|
253
418
|
}, [closeSoon]);
|
|
254
419
|
|
|
255
|
-
// ─── 3. Screenshot / Upload ───────────────────────────────────────
|
|
256
|
-
const handleCaptureChoiceToggle = useCallback(() => {
|
|
257
|
-
setShowCaptureChoices((v) => !v);
|
|
258
|
-
}, []);
|
|
259
|
-
|
|
260
420
|
const handleScreenshotAndFix = useCallback(async () => {
|
|
261
421
|
setAction('capturing');
|
|
262
422
|
setError(null);
|
|
263
|
-
setShowCaptureChoices(false);
|
|
264
423
|
|
|
265
424
|
setVisible(false);
|
|
266
425
|
await new Promise((resolve) => setTimeout(resolve, 350));
|
|
@@ -309,51 +468,11 @@ export const FeedbackModal: React.FC = () => {
|
|
|
309
468
|
}
|
|
310
469
|
}, [uploadBundleWithOptionalFix]);
|
|
311
470
|
|
|
471
|
+
/*
|
|
312
472
|
const handleFileUpload = useCallback(async () => {
|
|
313
|
-
|
|
314
|
-
setError(null);
|
|
315
|
-
setShowCaptureChoices(false);
|
|
316
|
-
try {
|
|
317
|
-
const picked = await pickFeedbackFile();
|
|
318
|
-
const { Dimensions } = require('react-native');
|
|
319
|
-
const { width, height } = Dimensions.get('window');
|
|
320
|
-
const deviceInfo: DeviceInfo = {
|
|
321
|
-
platform: Platform.OS,
|
|
322
|
-
osVersion: String(Platform.Version),
|
|
323
|
-
model: Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
|
|
324
|
-
screenWidth: width,
|
|
325
|
-
screenHeight: height,
|
|
326
|
-
};
|
|
327
|
-
const capturedErrors = YaverFeedback.getCapturedErrors();
|
|
328
|
-
const bundle: FeedbackBundle = {
|
|
329
|
-
metadata: {
|
|
330
|
-
timestamp: new Date().toISOString(),
|
|
331
|
-
device: deviceInfo,
|
|
332
|
-
app: {},
|
|
333
|
-
userNote: `[Uploaded file] ${picked.name}`,
|
|
334
|
-
},
|
|
335
|
-
screenshots: picked.kind === 'image' ? [picked.path] : [],
|
|
336
|
-
video: picked.kind === 'video' ? picked.path : undefined,
|
|
337
|
-
audio: picked.kind === 'audio' ? picked.path : undefined,
|
|
338
|
-
errors: capturedErrors.length > 0 ? capturedErrors : undefined,
|
|
339
|
-
};
|
|
340
|
-
if (picked.kind === 'unknown') {
|
|
341
|
-
throw new Error('Pick an image, video, or audio file.');
|
|
342
|
-
}
|
|
343
|
-
await uploadBundleWithOptionalFix(
|
|
344
|
-
bundle,
|
|
345
|
-
picked.kind === 'image',
|
|
346
|
-
picked.kind === 'image' ? 'Fix task started' : 'File uploaded',
|
|
347
|
-
);
|
|
348
|
-
} catch (err: unknown) {
|
|
349
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
350
|
-
if (message !== 'File selection canceled.') {
|
|
351
|
-
setError(message);
|
|
352
|
-
}
|
|
353
|
-
} finally {
|
|
354
|
-
if (mountedRef.current) setAction('idle');
|
|
355
|
-
}
|
|
473
|
+
...
|
|
356
474
|
}, [uploadBundleWithOptionalFix]);
|
|
475
|
+
*/
|
|
357
476
|
|
|
358
477
|
// ─── 3. Vibing ─────────────────────────────────────────────────────
|
|
359
478
|
// First tap expands the input; second submit fires the actual
|
|
@@ -407,113 +526,11 @@ export const FeedbackModal: React.FC = () => {
|
|
|
407
526
|
}
|
|
408
527
|
}, [vibePrompt]);
|
|
409
528
|
|
|
410
|
-
|
|
529
|
+
/*
|
|
411
530
|
const handleScreenRecording = useCallback(async () => {
|
|
412
|
-
|
|
413
|
-
if (!isRecordingVideo && lastVideo) {
|
|
414
|
-
const config = YaverFeedback.getConfig();
|
|
415
|
-
if (!config?.agentUrl) {
|
|
416
|
-
setError('Not connected to the agent yet.');
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
setAction('uploading-video');
|
|
420
|
-
try {
|
|
421
|
-
const { Dimensions } = require('react-native');
|
|
422
|
-
const { width, height } = Dimensions.get('window');
|
|
423
|
-
const deviceInfo: DeviceInfo = {
|
|
424
|
-
platform: Platform.OS,
|
|
425
|
-
osVersion: String(Platform.Version),
|
|
426
|
-
model: Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
|
|
427
|
-
screenWidth: width,
|
|
428
|
-
screenHeight: height,
|
|
429
|
-
};
|
|
430
|
-
const bundle: FeedbackBundle = {
|
|
431
|
-
metadata: {
|
|
432
|
-
timestamp: new Date().toISOString(),
|
|
433
|
-
device: deviceInfo,
|
|
434
|
-
app: {},
|
|
435
|
-
userNote: '[Screen recording]',
|
|
436
|
-
},
|
|
437
|
-
screenshots: [],
|
|
438
|
-
video: lastVideo.path,
|
|
439
|
-
errors: YaverFeedback.getCapturedErrors().length
|
|
440
|
-
? YaverFeedback.getCapturedErrors()
|
|
441
|
-
: undefined,
|
|
442
|
-
};
|
|
443
|
-
await uploadFeedback(config.agentUrl, config.authToken ?? '', bundle);
|
|
444
|
-
if (mountedRef.current) {
|
|
445
|
-
setToast(`Recording uploaded — ${Math.round(lastVideo.duration)}s`);
|
|
446
|
-
setLastVideo(null);
|
|
447
|
-
}
|
|
448
|
-
closeSoon(1200);
|
|
449
|
-
} catch (err: unknown) {
|
|
450
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
451
|
-
} finally {
|
|
452
|
-
if (mountedRef.current) setAction('idle');
|
|
453
|
-
}
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
if (isRecordingVideo) {
|
|
458
|
-
try {
|
|
459
|
-
const result = await stopVideoRecording();
|
|
460
|
-
if (mountedRef.current) {
|
|
461
|
-
setIsRecordingVideo(false);
|
|
462
|
-
setLastVideo(result);
|
|
463
|
-
setAction('uploading-video');
|
|
464
|
-
setToast('Uploading recording…');
|
|
465
|
-
}
|
|
466
|
-
const config = YaverFeedback.getConfig();
|
|
467
|
-
if (!config?.agentUrl) {
|
|
468
|
-
throw new Error('Not connected to the agent yet.');
|
|
469
|
-
}
|
|
470
|
-
const { Dimensions } = require('react-native');
|
|
471
|
-
const { width, height } = Dimensions.get('window');
|
|
472
|
-
const deviceInfo: DeviceInfo = {
|
|
473
|
-
platform: Platform.OS,
|
|
474
|
-
osVersion: String(Platform.Version),
|
|
475
|
-
model: Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
|
|
476
|
-
screenWidth: width,
|
|
477
|
-
screenHeight: height,
|
|
478
|
-
};
|
|
479
|
-
const bundle: FeedbackBundle = {
|
|
480
|
-
metadata: {
|
|
481
|
-
timestamp: new Date().toISOString(),
|
|
482
|
-
device: deviceInfo,
|
|
483
|
-
app: {},
|
|
484
|
-
userNote: '[Screen recording]',
|
|
485
|
-
},
|
|
486
|
-
screenshots: [],
|
|
487
|
-
video: result.path,
|
|
488
|
-
errors: YaverFeedback.getCapturedErrors().length
|
|
489
|
-
? YaverFeedback.getCapturedErrors()
|
|
490
|
-
: undefined,
|
|
491
|
-
};
|
|
492
|
-
await uploadFeedback(config.agentUrl, config.authToken ?? '', bundle);
|
|
493
|
-
if (mountedRef.current) {
|
|
494
|
-
setToast(`Recording uploaded — ${Math.round(result.duration)}s`);
|
|
495
|
-
setLastVideo(null);
|
|
496
|
-
}
|
|
497
|
-
closeSoon(1200);
|
|
498
|
-
} catch (err: unknown) {
|
|
499
|
-
setIsRecordingVideo(false);
|
|
500
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
501
|
-
} finally {
|
|
502
|
-
if (mountedRef.current) setAction('idle');
|
|
503
|
-
}
|
|
504
|
-
} else {
|
|
505
|
-
try {
|
|
506
|
-
await startVideoRecording();
|
|
507
|
-
if (mountedRef.current) {
|
|
508
|
-
setIsRecordingVideo(true);
|
|
509
|
-
setToast('Recording… tap again to stop and upload');
|
|
510
|
-
setLastVideo(null);
|
|
511
|
-
}
|
|
512
|
-
} catch (err: unknown) {
|
|
513
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
514
|
-
}
|
|
515
|
-
}
|
|
531
|
+
...
|
|
516
532
|
}, [closeSoon, isRecordingVideo, lastVideo]);
|
|
533
|
+
*/
|
|
517
534
|
|
|
518
535
|
const busy = action !== 'idle';
|
|
519
536
|
|
|
@@ -529,7 +546,23 @@ export const FeedbackModal: React.FC = () => {
|
|
|
529
546
|
onRequestClose={handleClose}
|
|
530
547
|
>
|
|
531
548
|
<Pressable style={styles.overlay} onPress={handleClose}>
|
|
532
|
-
<
|
|
549
|
+
<KeyboardAvoidingView
|
|
550
|
+
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|
551
|
+
style={styles.kbAvoider}
|
|
552
|
+
pointerEvents="box-none"
|
|
553
|
+
>
|
|
554
|
+
<Pressable
|
|
555
|
+
style={styles.modal}
|
|
556
|
+
onPress={(e) => {
|
|
557
|
+
e.stopPropagation();
|
|
558
|
+
Keyboard.dismiss();
|
|
559
|
+
}}
|
|
560
|
+
>
|
|
561
|
+
<ScrollView
|
|
562
|
+
style={styles.scroll}
|
|
563
|
+
contentContainerStyle={styles.scrollContent}
|
|
564
|
+
keyboardShouldPersistTaps="handled"
|
|
565
|
+
>
|
|
533
566
|
<View style={styles.header}>
|
|
534
567
|
<Text style={styles.title}>Send Feedback</Text>
|
|
535
568
|
<Pressable
|
|
@@ -543,6 +576,111 @@ export const FeedbackModal: React.FC = () => {
|
|
|
543
576
|
</Pressable>
|
|
544
577
|
</View>
|
|
545
578
|
|
|
579
|
+
<Pressable
|
|
580
|
+
onPress={() => {
|
|
581
|
+
if (!YaverFeedback.isAuthed()) {
|
|
582
|
+
YaverFeedback.showLogin();
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
YaverFeedback.showMachinePicker();
|
|
586
|
+
}}
|
|
587
|
+
style={[
|
|
588
|
+
styles.machineCard,
|
|
589
|
+
machineCard.status === 'live' && styles.machineCardLive,
|
|
590
|
+
machineCard.status === 'attention' && styles.machineCardAttention,
|
|
591
|
+
machineCard.status === 'offline' && styles.machineCardOffline,
|
|
592
|
+
]}
|
|
593
|
+
>
|
|
594
|
+
<View style={styles.machineHeader}>
|
|
595
|
+
<View style={styles.machineTitleWrap}>
|
|
596
|
+
<View
|
|
597
|
+
style={[
|
|
598
|
+
styles.machineDot,
|
|
599
|
+
machineCard.status === 'live' && styles.machineDotLive,
|
|
600
|
+
machineCard.status === 'attention' && styles.machineDotAttention,
|
|
601
|
+
machineCard.status === 'offline' && styles.machineDotOffline,
|
|
602
|
+
]}
|
|
603
|
+
/>
|
|
604
|
+
<Text style={styles.machineLabel}>Selected Machine</Text>
|
|
605
|
+
</View>
|
|
606
|
+
<Text style={styles.machineAction}>
|
|
607
|
+
{machineCard.loading ? 'Refreshing…' : 'Change'}
|
|
608
|
+
</Text>
|
|
609
|
+
</View>
|
|
610
|
+
<Text style={styles.machineName}>
|
|
611
|
+
{machineCard.loading ? 'Checking machine…' : machineCard.title}
|
|
612
|
+
</Text>
|
|
613
|
+
<Text style={styles.machineMeta}>{machineCard.detail}</Text>
|
|
614
|
+
</Pressable>
|
|
615
|
+
|
|
616
|
+
{quickIconHidden && (
|
|
617
|
+
<View style={styles.quickIconNote}>
|
|
618
|
+
<Text style={styles.quickIconNoteText}>
|
|
619
|
+
Quick access icon is hidden. Shake the phone if you want feedback back fast.
|
|
620
|
+
</Text>
|
|
621
|
+
<Pressable
|
|
622
|
+
onPress={() => {
|
|
623
|
+
void YaverFeedback.setQuickIconVisible(true);
|
|
624
|
+
setQuickIconHidden(false);
|
|
625
|
+
}}
|
|
626
|
+
style={({ pressed }) => [
|
|
627
|
+
styles.quickIconToggle,
|
|
628
|
+
pressed && styles.buttonPressed,
|
|
629
|
+
]}
|
|
630
|
+
>
|
|
631
|
+
<Text style={styles.quickIconToggleText}>Show quick icon again</Text>
|
|
632
|
+
</Pressable>
|
|
633
|
+
</View>
|
|
634
|
+
)}
|
|
635
|
+
|
|
636
|
+
<View style={styles.iconSelector}>
|
|
637
|
+
<Text style={styles.iconSelectorTitle}>Quick Icon Color</Text>
|
|
638
|
+
<Text style={styles.iconSelectorText}>
|
|
639
|
+
Pick a runtime color so the floating y icon does not overlap with your app UI.
|
|
640
|
+
</Text>
|
|
641
|
+
<View style={styles.iconSelectorGrid}>
|
|
642
|
+
{(Object.entries(QUICK_ICON_COLOR_PRESETS) as Array<
|
|
643
|
+
[QuickIconColorPreset, (typeof QUICK_ICON_COLOR_PRESETS)[QuickIconColorPreset]]
|
|
644
|
+
>).map(([preset, colors]) => {
|
|
645
|
+
const selected = quickIconColorPreset === preset;
|
|
646
|
+
return (
|
|
647
|
+
<Pressable
|
|
648
|
+
key={preset}
|
|
649
|
+
onPress={() => {
|
|
650
|
+
setQuickIconColorPreset(preset);
|
|
651
|
+
void YaverFeedback.setQuickIconColorPreset(preset);
|
|
652
|
+
}}
|
|
653
|
+
style={[
|
|
654
|
+
styles.iconOption,
|
|
655
|
+
selected && styles.iconOptionSelected,
|
|
656
|
+
]}
|
|
657
|
+
>
|
|
658
|
+
<View
|
|
659
|
+
style={[
|
|
660
|
+
styles.iconOptionCircle,
|
|
661
|
+
{
|
|
662
|
+
backgroundColor: colors.backgroundColor,
|
|
663
|
+
borderColor: colors.borderColor,
|
|
664
|
+
shadowColor: colors.shadowColor,
|
|
665
|
+
},
|
|
666
|
+
]}
|
|
667
|
+
>
|
|
668
|
+
<Text
|
|
669
|
+
style={[
|
|
670
|
+
styles.iconOptionLabel,
|
|
671
|
+
{ color: colors.foregroundColor },
|
|
672
|
+
]}
|
|
673
|
+
>
|
|
674
|
+
y
|
|
675
|
+
</Text>
|
|
676
|
+
</View>
|
|
677
|
+
<Text style={styles.iconOptionText}>{colors.label}</Text>
|
|
678
|
+
</Pressable>
|
|
679
|
+
);
|
|
680
|
+
})}
|
|
681
|
+
</View>
|
|
682
|
+
</View>
|
|
683
|
+
|
|
546
684
|
{/* 1. Hot Reload — the common path */}
|
|
547
685
|
<ActionRow
|
|
548
686
|
label={
|
|
@@ -613,51 +751,13 @@ export const FeedbackModal: React.FC = () => {
|
|
|
613
751
|
</Text>
|
|
614
752
|
)}
|
|
615
753
|
|
|
616
|
-
{/* Screenshot
|
|
617
|
-
{!showCaptureChoices ? (
|
|
618
|
-
<ActionRow
|
|
619
|
-
label={
|
|
620
|
-
action === 'capturing'
|
|
621
|
-
? 'Working…'
|
|
622
|
-
: 'Screenshot / Upload'
|
|
623
|
-
}
|
|
624
|
-
tint="#22c55e"
|
|
625
|
-
onPress={handleCaptureChoiceToggle}
|
|
626
|
-
disabled={busy}
|
|
627
|
-
busy={action === 'capturing'}
|
|
628
|
-
/>
|
|
629
|
-
) : (
|
|
630
|
-
<View style={styles.captureChoices}>
|
|
631
|
-
<ActionRow
|
|
632
|
-
label="Take Screenshot"
|
|
633
|
-
tint="#22c55e"
|
|
634
|
-
onPress={handleScreenshotAndFix}
|
|
635
|
-
disabled={busy}
|
|
636
|
-
/>
|
|
637
|
-
<ActionRow
|
|
638
|
-
label="Upload File"
|
|
639
|
-
tint="#34d399"
|
|
640
|
-
onPress={handleFileUpload}
|
|
641
|
-
disabled={busy}
|
|
642
|
-
/>
|
|
643
|
-
</View>
|
|
644
|
-
)}
|
|
645
|
-
|
|
646
|
-
{/* 4. Screen recording */}
|
|
754
|
+
{/* Screenshot & Fix */}
|
|
647
755
|
<ActionRow
|
|
648
|
-
label={
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
: lastVideo
|
|
654
|
-
? `Retry Upload Recording · ${Math.round(lastVideo.duration)}s`
|
|
655
|
-
: 'Screen Recording'
|
|
656
|
-
}
|
|
657
|
-
tint={isRecordingVideo ? '#ef4444' : '#60a5fa'}
|
|
658
|
-
onPress={handleScreenRecording}
|
|
659
|
-
disabled={busy && action !== 'uploading-video' && !isRecordingVideo}
|
|
660
|
-
busy={action === 'uploading-video'}
|
|
756
|
+
label={action === 'capturing' ? 'Working…' : 'Screenshot & Fix'}
|
|
757
|
+
tint="#22c55e"
|
|
758
|
+
onPress={handleScreenshotAndFix}
|
|
759
|
+
disabled={busy}
|
|
760
|
+
busy={action === 'capturing'}
|
|
661
761
|
/>
|
|
662
762
|
|
|
663
763
|
{progress !== null && (
|
|
@@ -673,34 +773,6 @@ export const FeedbackModal: React.FC = () => {
|
|
|
673
773
|
{toast && <Text style={styles.toast}>{toast}</Text>}
|
|
674
774
|
{error && <Text style={styles.error}>{error}</Text>}
|
|
675
775
|
|
|
676
|
-
{/* Quick-icon toggle. The user's three ways to control
|
|
677
|
-
the floating icon are: (1) long-press the icon →
|
|
678
|
-
Hide, (2) tap this row to toggle it on/off, (3) shake
|
|
679
|
-
→ this modal → tap this row. Shake is the unkillable
|
|
680
|
-
back-door when the icon is hidden and the dev hasn't
|
|
681
|
-
exposed their own settings UI. */}
|
|
682
|
-
<Pressable
|
|
683
|
-
onPress={async () => {
|
|
684
|
-
const next = !quickIconHidden;
|
|
685
|
-
setQuickIconHidden(next);
|
|
686
|
-
await YaverFeedback.setQuickIconVisible(!next);
|
|
687
|
-
}}
|
|
688
|
-
style={({ pressed }) => [
|
|
689
|
-
styles.quickIconToggle,
|
|
690
|
-
pressed && { opacity: 0.7 },
|
|
691
|
-
]}
|
|
692
|
-
accessibilityRole="button"
|
|
693
|
-
accessibilityLabel={
|
|
694
|
-
quickIconHidden ? 'Show quick icon' : 'Hide quick icon'
|
|
695
|
-
}
|
|
696
|
-
>
|
|
697
|
-
<Text style={styles.quickIconToggleText}>
|
|
698
|
-
{quickIconHidden
|
|
699
|
-
? '◯ Show quick-access icon'
|
|
700
|
-
: '● Hide quick-access icon'}
|
|
701
|
-
</Text>
|
|
702
|
-
</Pressable>
|
|
703
|
-
|
|
704
776
|
<Pressable
|
|
705
777
|
onPress={handleClose}
|
|
706
778
|
style={({ pressed }) => [
|
|
@@ -712,7 +784,9 @@ export const FeedbackModal: React.FC = () => {
|
|
|
712
784
|
>
|
|
713
785
|
<Text style={styles.cancelBtnText}>Cancel</Text>
|
|
714
786
|
</Pressable>
|
|
787
|
+
</ScrollView>
|
|
715
788
|
</Pressable>
|
|
789
|
+
</KeyboardAvoidingView>
|
|
716
790
|
</Pressable>
|
|
717
791
|
</Modal>
|
|
718
792
|
)}
|
|
@@ -814,6 +888,10 @@ const styles = StyleSheet.create({
|
|
|
814
888
|
backgroundColor: 'rgba(0,0,0,0.55)',
|
|
815
889
|
justifyContent: 'flex-end',
|
|
816
890
|
},
|
|
891
|
+
kbAvoider: {
|
|
892
|
+
width: '100%',
|
|
893
|
+
justifyContent: 'flex-end',
|
|
894
|
+
},
|
|
817
895
|
modal: {
|
|
818
896
|
backgroundColor: '#141422',
|
|
819
897
|
borderTopLeftRadius: 22,
|
|
@@ -821,6 +899,14 @@ const styles = StyleSheet.create({
|
|
|
821
899
|
padding: 22,
|
|
822
900
|
paddingBottom: 36,
|
|
823
901
|
gap: 12,
|
|
902
|
+
maxHeight: '92%',
|
|
903
|
+
},
|
|
904
|
+
scroll: {
|
|
905
|
+
maxHeight: '100%',
|
|
906
|
+
},
|
|
907
|
+
scrollContent: {
|
|
908
|
+
gap: 12,
|
|
909
|
+
paddingBottom: 8,
|
|
824
910
|
},
|
|
825
911
|
header: {
|
|
826
912
|
flexDirection: 'row',
|
|
@@ -861,6 +947,74 @@ const styles = StyleSheet.create({
|
|
|
861
947
|
fontSize: 15,
|
|
862
948
|
fontWeight: '700',
|
|
863
949
|
},
|
|
950
|
+
machineCard: {
|
|
951
|
+
borderRadius: 14,
|
|
952
|
+
borderWidth: 1,
|
|
953
|
+
padding: 14,
|
|
954
|
+
backgroundColor: 'rgba(255,255,255,0.04)',
|
|
955
|
+
borderColor: 'rgba(255,255,255,0.12)',
|
|
956
|
+
},
|
|
957
|
+
machineCardLive: {
|
|
958
|
+
backgroundColor: 'rgba(34,197,94,0.10)',
|
|
959
|
+
borderColor: 'rgba(34,197,94,0.35)',
|
|
960
|
+
},
|
|
961
|
+
machineCardAttention: {
|
|
962
|
+
backgroundColor: 'rgba(245,158,11,0.10)',
|
|
963
|
+
borderColor: 'rgba(245,158,11,0.35)',
|
|
964
|
+
},
|
|
965
|
+
machineCardOffline: {
|
|
966
|
+
backgroundColor: 'rgba(239,68,68,0.10)',
|
|
967
|
+
borderColor: 'rgba(239,68,68,0.35)',
|
|
968
|
+
},
|
|
969
|
+
machineHeader: {
|
|
970
|
+
flexDirection: 'row',
|
|
971
|
+
alignItems: 'center',
|
|
972
|
+
justifyContent: 'space-between',
|
|
973
|
+
marginBottom: 6,
|
|
974
|
+
},
|
|
975
|
+
machineTitleWrap: {
|
|
976
|
+
flexDirection: 'row',
|
|
977
|
+
alignItems: 'center',
|
|
978
|
+
gap: 8,
|
|
979
|
+
},
|
|
980
|
+
machineDot: {
|
|
981
|
+
width: 10,
|
|
982
|
+
height: 10,
|
|
983
|
+
borderRadius: 5,
|
|
984
|
+
backgroundColor: '#6b7280',
|
|
985
|
+
},
|
|
986
|
+
machineDotLive: {
|
|
987
|
+
backgroundColor: '#22c55e',
|
|
988
|
+
},
|
|
989
|
+
machineDotAttention: {
|
|
990
|
+
backgroundColor: '#f59e0b',
|
|
991
|
+
},
|
|
992
|
+
machineDotOffline: {
|
|
993
|
+
backgroundColor: '#ef4444',
|
|
994
|
+
},
|
|
995
|
+
machineLabel: {
|
|
996
|
+
color: '#cbd5e1',
|
|
997
|
+
fontSize: 12,
|
|
998
|
+
fontWeight: '700',
|
|
999
|
+
textTransform: 'uppercase',
|
|
1000
|
+
letterSpacing: 0.8,
|
|
1001
|
+
},
|
|
1002
|
+
machineAction: {
|
|
1003
|
+
color: '#a5b4fc',
|
|
1004
|
+
fontSize: 12,
|
|
1005
|
+
fontWeight: '700',
|
|
1006
|
+
},
|
|
1007
|
+
machineName: {
|
|
1008
|
+
color: '#fff',
|
|
1009
|
+
fontSize: 16,
|
|
1010
|
+
fontWeight: '700',
|
|
1011
|
+
},
|
|
1012
|
+
machineMeta: {
|
|
1013
|
+
color: '#cbd5e1',
|
|
1014
|
+
fontSize: 12,
|
|
1015
|
+
marginTop: 4,
|
|
1016
|
+
lineHeight: 17,
|
|
1017
|
+
},
|
|
864
1018
|
captureChoices: {
|
|
865
1019
|
gap: 10,
|
|
866
1020
|
},
|
|
@@ -889,15 +1043,89 @@ const styles = StyleSheet.create({
|
|
|
889
1043
|
marginTop: 4,
|
|
890
1044
|
},
|
|
891
1045
|
quickIconToggle: {
|
|
892
|
-
marginTop:
|
|
893
|
-
alignSelf: '
|
|
1046
|
+
marginTop: 6,
|
|
1047
|
+
alignSelf: 'flex-start',
|
|
894
1048
|
paddingVertical: 6,
|
|
895
|
-
paddingHorizontal:
|
|
1049
|
+
paddingHorizontal: 10,
|
|
1050
|
+
borderRadius: 10,
|
|
1051
|
+
backgroundColor: 'rgba(255,255,255,0.05)',
|
|
896
1052
|
},
|
|
897
1053
|
quickIconToggleText: {
|
|
898
|
-
color: '#
|
|
1054
|
+
color: '#cbd5e1',
|
|
1055
|
+
fontSize: 12,
|
|
1056
|
+
fontWeight: '700',
|
|
1057
|
+
},
|
|
1058
|
+
quickIconNote: {
|
|
1059
|
+
borderRadius: 12,
|
|
1060
|
+
borderWidth: 1,
|
|
1061
|
+
borderColor: 'rgba(251,191,36,0.28)',
|
|
1062
|
+
backgroundColor: 'rgba(251,191,36,0.08)',
|
|
1063
|
+
padding: 12,
|
|
1064
|
+
},
|
|
1065
|
+
quickIconNoteText: {
|
|
1066
|
+
color: '#fde68a',
|
|
899
1067
|
fontSize: 12,
|
|
900
|
-
|
|
1068
|
+
lineHeight: 17,
|
|
1069
|
+
},
|
|
1070
|
+
iconSelector: {
|
|
1071
|
+
borderRadius: 12,
|
|
1072
|
+
borderWidth: 1,
|
|
1073
|
+
borderColor: 'rgba(255,255,255,0.09)',
|
|
1074
|
+
backgroundColor: 'rgba(255,255,255,0.03)',
|
|
1075
|
+
padding: 12,
|
|
1076
|
+
gap: 10,
|
|
1077
|
+
},
|
|
1078
|
+
iconSelectorTitle: {
|
|
1079
|
+
color: '#f8fafc',
|
|
1080
|
+
fontSize: 13,
|
|
1081
|
+
fontWeight: '700',
|
|
1082
|
+
},
|
|
1083
|
+
iconSelectorText: {
|
|
1084
|
+
color: '#cbd5e1',
|
|
1085
|
+
fontSize: 12,
|
|
1086
|
+
lineHeight: 17,
|
|
1087
|
+
},
|
|
1088
|
+
iconSelectorGrid: {
|
|
1089
|
+
flexDirection: 'row',
|
|
1090
|
+
flexWrap: 'wrap',
|
|
1091
|
+
gap: 10,
|
|
1092
|
+
},
|
|
1093
|
+
iconOption: {
|
|
1094
|
+
width: '31%',
|
|
1095
|
+
minWidth: 84,
|
|
1096
|
+
borderRadius: 12,
|
|
1097
|
+
borderWidth: 1,
|
|
1098
|
+
borderColor: 'rgba(255,255,255,0.08)',
|
|
1099
|
+
backgroundColor: 'rgba(255,255,255,0.02)',
|
|
1100
|
+
paddingVertical: 10,
|
|
1101
|
+
paddingHorizontal: 8,
|
|
1102
|
+
alignItems: 'center',
|
|
1103
|
+
gap: 8,
|
|
1104
|
+
},
|
|
1105
|
+
iconOptionSelected: {
|
|
1106
|
+
borderColor: 'rgba(129,140,248,0.72)',
|
|
1107
|
+
backgroundColor: 'rgba(129,140,248,0.12)',
|
|
1108
|
+
},
|
|
1109
|
+
iconOptionCircle: {
|
|
1110
|
+
width: 38,
|
|
1111
|
+
height: 38,
|
|
1112
|
+
borderRadius: 19,
|
|
1113
|
+
alignItems: 'center',
|
|
1114
|
+
justifyContent: 'center',
|
|
1115
|
+
borderWidth: 2,
|
|
1116
|
+
shadowOffset: { width: 0, height: 2 },
|
|
1117
|
+
shadowOpacity: 0.28,
|
|
1118
|
+
shadowRadius: 5,
|
|
1119
|
+
elevation: 5,
|
|
1120
|
+
},
|
|
1121
|
+
iconOptionLabel: {
|
|
1122
|
+
fontSize: 18,
|
|
1123
|
+
fontWeight: '700',
|
|
1124
|
+
},
|
|
1125
|
+
iconOptionText: {
|
|
1126
|
+
color: '#e2e8f0',
|
|
1127
|
+
fontSize: 11,
|
|
1128
|
+
fontWeight: '600',
|
|
901
1129
|
},
|
|
902
1130
|
cancelBtn: {
|
|
903
1131
|
marginTop: 4,
|