yaver-feedback-react-native 0.8.1 → 0.8.3
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 +33 -3
- package/dist/Discovery.js +2 -0
- package/dist/FeedbackModal.js +438 -215
- package/dist/GuestOnboardingScreen.d.ts +8 -0
- package/dist/GuestOnboardingScreen.js +282 -0
- package/dist/LoginScreen.d.ts +5 -1
- package/dist/LoginScreen.js +5 -2
- package/dist/MachinePickerScreen.js +1 -1
- package/dist/P2PClient.d.ts +22 -3
- package/dist/P2PClient.js +45 -2
- package/dist/QuickActionIcon.js +16 -4
- package/dist/YaverFeedback.d.ts +7 -0
- package/dist/YaverFeedback.js +68 -4
- package/dist/__tests__/P2PClient.test.js +30 -0
- package/dist/__tests__/YaverFeedback.test.js +39 -0
- package/dist/auth.d.ts +52 -0
- package/dist/auth.js +75 -0
- package/dist/index.d.ts +7 -6
- package/dist/index.js +10 -5
- package/dist/preferences.d.ts +11 -0
- package/dist/preferences.js +82 -0
- package/dist/types.d.ts +7 -0
- package/package.json +6 -4
- package/src/AuthOverlay.tsx +47 -2
- package/src/Discovery.ts +2 -0
- package/src/FeedbackModal.tsx +507 -258
- package/src/GuestOnboardingScreen.tsx +307 -0
- package/src/LoginScreen.tsx +21 -2
- package/src/MachinePickerScreen.tsx +3 -1
- package/src/P2PClient.ts +69 -3
- package/src/QuickActionIcon.tsx +25 -5
- package/src/YaverFeedback.ts +79 -4
- package/src/__tests__/P2PClient.test.ts +40 -0
- package/src/__tests__/YaverFeedback.test.ts +42 -0
- package/src/auth.ts +137 -0
- package/src/index.ts +14 -4
- package/src/preferences.ts +96 -0
- package/src/types.ts +7 -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
|
|
@@ -361,8 +480,29 @@ export const FeedbackModal: React.FC = () => {
|
|
|
361
480
|
// user types what they want, hits Send, sees the task id back. If
|
|
362
481
|
// left blank, we default to "pick the next small improvement"
|
|
363
482
|
// so a one-tap workflow still works for lazy days.
|
|
364
|
-
const handleVibingButton = useCallback(() => {
|
|
483
|
+
const handleVibingButton = useCallback(async () => {
|
|
365
484
|
if (!showVibeInput) {
|
|
485
|
+
const client = YaverFeedback.getP2PClient();
|
|
486
|
+
if (!client) {
|
|
487
|
+
setError('Not connected to the agent yet.');
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
setError(null);
|
|
491
|
+
try {
|
|
492
|
+
const eligibility = await client.getVibingEligibility();
|
|
493
|
+
if (!eligibility.canVibe) {
|
|
494
|
+
const message =
|
|
495
|
+
eligibility.guidance && eligibility.guidance.trim()
|
|
496
|
+
? `${eligibility.reason ?? 'Vibe coding is unavailable.'} ${eligibility.guidance}`
|
|
497
|
+
: eligibility.reason ?? 'Vibe coding is unavailable.';
|
|
498
|
+
setError(message);
|
|
499
|
+
setToast('Vibe coding unavailable for this project.');
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
} catch (err: unknown) {
|
|
503
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
366
506
|
setShowVibeInput(true);
|
|
367
507
|
return;
|
|
368
508
|
}
|
|
@@ -407,113 +547,11 @@ export const FeedbackModal: React.FC = () => {
|
|
|
407
547
|
}
|
|
408
548
|
}, [vibePrompt]);
|
|
409
549
|
|
|
410
|
-
|
|
550
|
+
/*
|
|
411
551
|
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
|
-
}
|
|
552
|
+
...
|
|
516
553
|
}, [closeSoon, isRecordingVideo, lastVideo]);
|
|
554
|
+
*/
|
|
517
555
|
|
|
518
556
|
const busy = action !== 'idle';
|
|
519
557
|
|
|
@@ -529,7 +567,23 @@ export const FeedbackModal: React.FC = () => {
|
|
|
529
567
|
onRequestClose={handleClose}
|
|
530
568
|
>
|
|
531
569
|
<Pressable style={styles.overlay} onPress={handleClose}>
|
|
532
|
-
<
|
|
570
|
+
<KeyboardAvoidingView
|
|
571
|
+
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|
572
|
+
style={styles.kbAvoider}
|
|
573
|
+
pointerEvents="box-none"
|
|
574
|
+
>
|
|
575
|
+
<Pressable
|
|
576
|
+
style={styles.modal}
|
|
577
|
+
onPress={(e) => {
|
|
578
|
+
e.stopPropagation();
|
|
579
|
+
Keyboard.dismiss();
|
|
580
|
+
}}
|
|
581
|
+
>
|
|
582
|
+
<ScrollView
|
|
583
|
+
style={styles.scroll}
|
|
584
|
+
contentContainerStyle={styles.scrollContent}
|
|
585
|
+
keyboardShouldPersistTaps="handled"
|
|
586
|
+
>
|
|
533
587
|
<View style={styles.header}>
|
|
534
588
|
<Text style={styles.title}>Send Feedback</Text>
|
|
535
589
|
<Pressable
|
|
@@ -543,6 +597,111 @@ export const FeedbackModal: React.FC = () => {
|
|
|
543
597
|
</Pressable>
|
|
544
598
|
</View>
|
|
545
599
|
|
|
600
|
+
<Pressable
|
|
601
|
+
onPress={() => {
|
|
602
|
+
if (!YaverFeedback.isAuthed()) {
|
|
603
|
+
YaverFeedback.showLogin();
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
YaverFeedback.showMachinePicker();
|
|
607
|
+
}}
|
|
608
|
+
style={[
|
|
609
|
+
styles.machineCard,
|
|
610
|
+
machineCard.status === 'live' && styles.machineCardLive,
|
|
611
|
+
machineCard.status === 'attention' && styles.machineCardAttention,
|
|
612
|
+
machineCard.status === 'offline' && styles.machineCardOffline,
|
|
613
|
+
]}
|
|
614
|
+
>
|
|
615
|
+
<View style={styles.machineHeader}>
|
|
616
|
+
<View style={styles.machineTitleWrap}>
|
|
617
|
+
<View
|
|
618
|
+
style={[
|
|
619
|
+
styles.machineDot,
|
|
620
|
+
machineCard.status === 'live' && styles.machineDotLive,
|
|
621
|
+
machineCard.status === 'attention' && styles.machineDotAttention,
|
|
622
|
+
machineCard.status === 'offline' && styles.machineDotOffline,
|
|
623
|
+
]}
|
|
624
|
+
/>
|
|
625
|
+
<Text style={styles.machineLabel}>Selected Machine</Text>
|
|
626
|
+
</View>
|
|
627
|
+
<Text style={styles.machineAction}>
|
|
628
|
+
{machineCard.loading ? 'Refreshing…' : 'Change'}
|
|
629
|
+
</Text>
|
|
630
|
+
</View>
|
|
631
|
+
<Text style={styles.machineName}>
|
|
632
|
+
{machineCard.loading ? 'Checking machine…' : machineCard.title}
|
|
633
|
+
</Text>
|
|
634
|
+
<Text style={styles.machineMeta}>{machineCard.detail}</Text>
|
|
635
|
+
</Pressable>
|
|
636
|
+
|
|
637
|
+
{quickIconHidden && (
|
|
638
|
+
<View style={styles.quickIconNote}>
|
|
639
|
+
<Text style={styles.quickIconNoteText}>
|
|
640
|
+
Quick access icon is hidden. Shake the phone if you want feedback back fast.
|
|
641
|
+
</Text>
|
|
642
|
+
<Pressable
|
|
643
|
+
onPress={() => {
|
|
644
|
+
void YaverFeedback.setQuickIconVisible(true);
|
|
645
|
+
setQuickIconHidden(false);
|
|
646
|
+
}}
|
|
647
|
+
style={({ pressed }) => [
|
|
648
|
+
styles.quickIconToggle,
|
|
649
|
+
pressed && styles.buttonPressed,
|
|
650
|
+
]}
|
|
651
|
+
>
|
|
652
|
+
<Text style={styles.quickIconToggleText}>Show quick icon again</Text>
|
|
653
|
+
</Pressable>
|
|
654
|
+
</View>
|
|
655
|
+
)}
|
|
656
|
+
|
|
657
|
+
<View style={styles.iconSelector}>
|
|
658
|
+
<Text style={styles.iconSelectorTitle}>Quick Icon Color</Text>
|
|
659
|
+
<Text style={styles.iconSelectorText}>
|
|
660
|
+
Pick a runtime color so the floating y icon does not overlap with your app UI.
|
|
661
|
+
</Text>
|
|
662
|
+
<View style={styles.iconSelectorGrid}>
|
|
663
|
+
{(Object.entries(QUICK_ICON_COLOR_PRESETS) as Array<
|
|
664
|
+
[QuickIconColorPreset, (typeof QUICK_ICON_COLOR_PRESETS)[QuickIconColorPreset]]
|
|
665
|
+
>).map(([preset, colors]) => {
|
|
666
|
+
const selected = quickIconColorPreset === preset;
|
|
667
|
+
return (
|
|
668
|
+
<Pressable
|
|
669
|
+
key={preset}
|
|
670
|
+
onPress={() => {
|
|
671
|
+
setQuickIconColorPreset(preset);
|
|
672
|
+
void YaverFeedback.setQuickIconColorPreset(preset);
|
|
673
|
+
}}
|
|
674
|
+
style={[
|
|
675
|
+
styles.iconOption,
|
|
676
|
+
selected && styles.iconOptionSelected,
|
|
677
|
+
]}
|
|
678
|
+
>
|
|
679
|
+
<View
|
|
680
|
+
style={[
|
|
681
|
+
styles.iconOptionCircle,
|
|
682
|
+
{
|
|
683
|
+
backgroundColor: colors.backgroundColor,
|
|
684
|
+
borderColor: colors.borderColor,
|
|
685
|
+
shadowColor: colors.shadowColor,
|
|
686
|
+
},
|
|
687
|
+
]}
|
|
688
|
+
>
|
|
689
|
+
<Text
|
|
690
|
+
style={[
|
|
691
|
+
styles.iconOptionLabel,
|
|
692
|
+
{ color: colors.foregroundColor },
|
|
693
|
+
]}
|
|
694
|
+
>
|
|
695
|
+
y
|
|
696
|
+
</Text>
|
|
697
|
+
</View>
|
|
698
|
+
<Text style={styles.iconOptionText}>{colors.label}</Text>
|
|
699
|
+
</Pressable>
|
|
700
|
+
);
|
|
701
|
+
})}
|
|
702
|
+
</View>
|
|
703
|
+
</View>
|
|
704
|
+
|
|
546
705
|
{/* 1. Hot Reload — the common path */}
|
|
547
706
|
<ActionRow
|
|
548
707
|
label={
|
|
@@ -613,51 +772,13 @@ export const FeedbackModal: React.FC = () => {
|
|
|
613
772
|
</Text>
|
|
614
773
|
)}
|
|
615
774
|
|
|
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 */}
|
|
775
|
+
{/* Screenshot & Fix */}
|
|
647
776
|
<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'}
|
|
777
|
+
label={action === 'capturing' ? 'Working…' : 'Screenshot & Fix'}
|
|
778
|
+
tint="#22c55e"
|
|
779
|
+
onPress={handleScreenshotAndFix}
|
|
780
|
+
disabled={busy}
|
|
781
|
+
busy={action === 'capturing'}
|
|
661
782
|
/>
|
|
662
783
|
|
|
663
784
|
{progress !== null && (
|
|
@@ -673,34 +794,6 @@ export const FeedbackModal: React.FC = () => {
|
|
|
673
794
|
{toast && <Text style={styles.toast}>{toast}</Text>}
|
|
674
795
|
{error && <Text style={styles.error}>{error}</Text>}
|
|
675
796
|
|
|
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
797
|
<Pressable
|
|
705
798
|
onPress={handleClose}
|
|
706
799
|
style={({ pressed }) => [
|
|
@@ -712,7 +805,9 @@ export const FeedbackModal: React.FC = () => {
|
|
|
712
805
|
>
|
|
713
806
|
<Text style={styles.cancelBtnText}>Cancel</Text>
|
|
714
807
|
</Pressable>
|
|
808
|
+
</ScrollView>
|
|
715
809
|
</Pressable>
|
|
810
|
+
</KeyboardAvoidingView>
|
|
716
811
|
</Pressable>
|
|
717
812
|
</Modal>
|
|
718
813
|
)}
|
|
@@ -814,6 +909,10 @@ const styles = StyleSheet.create({
|
|
|
814
909
|
backgroundColor: 'rgba(0,0,0,0.55)',
|
|
815
910
|
justifyContent: 'flex-end',
|
|
816
911
|
},
|
|
912
|
+
kbAvoider: {
|
|
913
|
+
width: '100%',
|
|
914
|
+
justifyContent: 'flex-end',
|
|
915
|
+
},
|
|
817
916
|
modal: {
|
|
818
917
|
backgroundColor: '#141422',
|
|
819
918
|
borderTopLeftRadius: 22,
|
|
@@ -821,6 +920,14 @@ const styles = StyleSheet.create({
|
|
|
821
920
|
padding: 22,
|
|
822
921
|
paddingBottom: 36,
|
|
823
922
|
gap: 12,
|
|
923
|
+
maxHeight: '92%',
|
|
924
|
+
},
|
|
925
|
+
scroll: {
|
|
926
|
+
maxHeight: '100%',
|
|
927
|
+
},
|
|
928
|
+
scrollContent: {
|
|
929
|
+
gap: 12,
|
|
930
|
+
paddingBottom: 8,
|
|
824
931
|
},
|
|
825
932
|
header: {
|
|
826
933
|
flexDirection: 'row',
|
|
@@ -861,6 +968,74 @@ const styles = StyleSheet.create({
|
|
|
861
968
|
fontSize: 15,
|
|
862
969
|
fontWeight: '700',
|
|
863
970
|
},
|
|
971
|
+
machineCard: {
|
|
972
|
+
borderRadius: 14,
|
|
973
|
+
borderWidth: 1,
|
|
974
|
+
padding: 14,
|
|
975
|
+
backgroundColor: 'rgba(255,255,255,0.04)',
|
|
976
|
+
borderColor: 'rgba(255,255,255,0.12)',
|
|
977
|
+
},
|
|
978
|
+
machineCardLive: {
|
|
979
|
+
backgroundColor: 'rgba(34,197,94,0.10)',
|
|
980
|
+
borderColor: 'rgba(34,197,94,0.35)',
|
|
981
|
+
},
|
|
982
|
+
machineCardAttention: {
|
|
983
|
+
backgroundColor: 'rgba(245,158,11,0.10)',
|
|
984
|
+
borderColor: 'rgba(245,158,11,0.35)',
|
|
985
|
+
},
|
|
986
|
+
machineCardOffline: {
|
|
987
|
+
backgroundColor: 'rgba(239,68,68,0.10)',
|
|
988
|
+
borderColor: 'rgba(239,68,68,0.35)',
|
|
989
|
+
},
|
|
990
|
+
machineHeader: {
|
|
991
|
+
flexDirection: 'row',
|
|
992
|
+
alignItems: 'center',
|
|
993
|
+
justifyContent: 'space-between',
|
|
994
|
+
marginBottom: 6,
|
|
995
|
+
},
|
|
996
|
+
machineTitleWrap: {
|
|
997
|
+
flexDirection: 'row',
|
|
998
|
+
alignItems: 'center',
|
|
999
|
+
gap: 8,
|
|
1000
|
+
},
|
|
1001
|
+
machineDot: {
|
|
1002
|
+
width: 10,
|
|
1003
|
+
height: 10,
|
|
1004
|
+
borderRadius: 5,
|
|
1005
|
+
backgroundColor: '#6b7280',
|
|
1006
|
+
},
|
|
1007
|
+
machineDotLive: {
|
|
1008
|
+
backgroundColor: '#22c55e',
|
|
1009
|
+
},
|
|
1010
|
+
machineDotAttention: {
|
|
1011
|
+
backgroundColor: '#f59e0b',
|
|
1012
|
+
},
|
|
1013
|
+
machineDotOffline: {
|
|
1014
|
+
backgroundColor: '#ef4444',
|
|
1015
|
+
},
|
|
1016
|
+
machineLabel: {
|
|
1017
|
+
color: '#cbd5e1',
|
|
1018
|
+
fontSize: 12,
|
|
1019
|
+
fontWeight: '700',
|
|
1020
|
+
textTransform: 'uppercase',
|
|
1021
|
+
letterSpacing: 0.8,
|
|
1022
|
+
},
|
|
1023
|
+
machineAction: {
|
|
1024
|
+
color: '#a5b4fc',
|
|
1025
|
+
fontSize: 12,
|
|
1026
|
+
fontWeight: '700',
|
|
1027
|
+
},
|
|
1028
|
+
machineName: {
|
|
1029
|
+
color: '#fff',
|
|
1030
|
+
fontSize: 16,
|
|
1031
|
+
fontWeight: '700',
|
|
1032
|
+
},
|
|
1033
|
+
machineMeta: {
|
|
1034
|
+
color: '#cbd5e1',
|
|
1035
|
+
fontSize: 12,
|
|
1036
|
+
marginTop: 4,
|
|
1037
|
+
lineHeight: 17,
|
|
1038
|
+
},
|
|
864
1039
|
captureChoices: {
|
|
865
1040
|
gap: 10,
|
|
866
1041
|
},
|
|
@@ -889,15 +1064,89 @@ const styles = StyleSheet.create({
|
|
|
889
1064
|
marginTop: 4,
|
|
890
1065
|
},
|
|
891
1066
|
quickIconToggle: {
|
|
892
|
-
marginTop:
|
|
893
|
-
alignSelf: '
|
|
1067
|
+
marginTop: 6,
|
|
1068
|
+
alignSelf: 'flex-start',
|
|
894
1069
|
paddingVertical: 6,
|
|
895
|
-
paddingHorizontal:
|
|
1070
|
+
paddingHorizontal: 10,
|
|
1071
|
+
borderRadius: 10,
|
|
1072
|
+
backgroundColor: 'rgba(255,255,255,0.05)',
|
|
896
1073
|
},
|
|
897
1074
|
quickIconToggleText: {
|
|
898
|
-
color: '#
|
|
1075
|
+
color: '#cbd5e1',
|
|
1076
|
+
fontSize: 12,
|
|
1077
|
+
fontWeight: '700',
|
|
1078
|
+
},
|
|
1079
|
+
quickIconNote: {
|
|
1080
|
+
borderRadius: 12,
|
|
1081
|
+
borderWidth: 1,
|
|
1082
|
+
borderColor: 'rgba(251,191,36,0.28)',
|
|
1083
|
+
backgroundColor: 'rgba(251,191,36,0.08)',
|
|
1084
|
+
padding: 12,
|
|
1085
|
+
},
|
|
1086
|
+
quickIconNoteText: {
|
|
1087
|
+
color: '#fde68a',
|
|
1088
|
+
fontSize: 12,
|
|
1089
|
+
lineHeight: 17,
|
|
1090
|
+
},
|
|
1091
|
+
iconSelector: {
|
|
1092
|
+
borderRadius: 12,
|
|
1093
|
+
borderWidth: 1,
|
|
1094
|
+
borderColor: 'rgba(255,255,255,0.09)',
|
|
1095
|
+
backgroundColor: 'rgba(255,255,255,0.03)',
|
|
1096
|
+
padding: 12,
|
|
1097
|
+
gap: 10,
|
|
1098
|
+
},
|
|
1099
|
+
iconSelectorTitle: {
|
|
1100
|
+
color: '#f8fafc',
|
|
1101
|
+
fontSize: 13,
|
|
1102
|
+
fontWeight: '700',
|
|
1103
|
+
},
|
|
1104
|
+
iconSelectorText: {
|
|
1105
|
+
color: '#cbd5e1',
|
|
899
1106
|
fontSize: 12,
|
|
900
|
-
|
|
1107
|
+
lineHeight: 17,
|
|
1108
|
+
},
|
|
1109
|
+
iconSelectorGrid: {
|
|
1110
|
+
flexDirection: 'row',
|
|
1111
|
+
flexWrap: 'wrap',
|
|
1112
|
+
gap: 10,
|
|
1113
|
+
},
|
|
1114
|
+
iconOption: {
|
|
1115
|
+
width: '31%',
|
|
1116
|
+
minWidth: 84,
|
|
1117
|
+
borderRadius: 12,
|
|
1118
|
+
borderWidth: 1,
|
|
1119
|
+
borderColor: 'rgba(255,255,255,0.08)',
|
|
1120
|
+
backgroundColor: 'rgba(255,255,255,0.02)',
|
|
1121
|
+
paddingVertical: 10,
|
|
1122
|
+
paddingHorizontal: 8,
|
|
1123
|
+
alignItems: 'center',
|
|
1124
|
+
gap: 8,
|
|
1125
|
+
},
|
|
1126
|
+
iconOptionSelected: {
|
|
1127
|
+
borderColor: 'rgba(129,140,248,0.72)',
|
|
1128
|
+
backgroundColor: 'rgba(129,140,248,0.12)',
|
|
1129
|
+
},
|
|
1130
|
+
iconOptionCircle: {
|
|
1131
|
+
width: 38,
|
|
1132
|
+
height: 38,
|
|
1133
|
+
borderRadius: 19,
|
|
1134
|
+
alignItems: 'center',
|
|
1135
|
+
justifyContent: 'center',
|
|
1136
|
+
borderWidth: 2,
|
|
1137
|
+
shadowOffset: { width: 0, height: 2 },
|
|
1138
|
+
shadowOpacity: 0.28,
|
|
1139
|
+
shadowRadius: 5,
|
|
1140
|
+
elevation: 5,
|
|
1141
|
+
},
|
|
1142
|
+
iconOptionLabel: {
|
|
1143
|
+
fontSize: 18,
|
|
1144
|
+
fontWeight: '700',
|
|
1145
|
+
},
|
|
1146
|
+
iconOptionText: {
|
|
1147
|
+
color: '#e2e8f0',
|
|
1148
|
+
fontSize: 11,
|
|
1149
|
+
fontWeight: '600',
|
|
901
1150
|
},
|
|
902
1151
|
cancelBtn: {
|
|
903
1152
|
marginTop: 4,
|