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.
@@ -41,14 +41,14 @@ const capture_1 = require("./capture");
41
41
  const upload_1 = require("./upload");
42
42
  const AuthOverlay_1 = require("./AuthOverlay");
43
43
  const QuickActionIcon_1 = require("./QuickActionIcon");
44
+ const auth_1 = require("./auth");
45
+ const preferences_1 = require("./preferences");
44
46
  const FeedbackModal = () => {
45
47
  const [visible, setVisible] = (0, react_1.useState)(false);
46
48
  const [action, setAction] = (0, react_1.useState)('idle');
47
49
  const [error, setError] = (0, react_1.useState)(null);
48
50
  const [toast, setToast] = (0, react_1.useState)(null);
49
51
  const [progress, setProgress] = (0, react_1.useState)(null);
50
- const [isRecordingVideo, setIsRecordingVideo] = (0, react_1.useState)(false);
51
- const [lastVideo, setLastVideo] = (0, react_1.useState)(null);
52
52
  // Tracks whether the user has hidden the QuickActionIcon via its
53
53
  // long-press menu. Shake is always available, so the feedback modal
54
54
  // is our guaranteed UI for bringing the icon back — we surface a
@@ -61,9 +61,113 @@ const FeedbackModal = () => {
61
61
  // the wrong project because the matcher grepped the prompt itself).
62
62
  const [showVibeInput, setShowVibeInput] = (0, react_1.useState)(false);
63
63
  const [vibePrompt, setVibePrompt] = (0, react_1.useState)('');
64
- const [showCaptureChoices, setShowCaptureChoices] = (0, react_1.useState)(false);
65
64
  const [lastVibeTaskId, setLastVibeTaskId] = (0, react_1.useState)(null);
65
+ const [quickIconColorPreset, setQuickIconColorPreset] = (0, react_1.useState)(null);
66
+ const [machineCard, setMachineCard] = (0, react_1.useState)({
67
+ device: null,
68
+ reachable: null,
69
+ loading: false,
70
+ status: 'none',
71
+ title: 'No machine selected',
72
+ detail: 'Pick a remote dev machine before using the feedback actions.',
73
+ });
66
74
  const mountedRef = (0, react_1.useRef)(true);
75
+ const loadSelectedMachine = (0, react_1.useCallback)(async () => {
76
+ const cfg = YaverFeedback_1.YaverFeedback.getConfig();
77
+ if (!cfg?.authToken) {
78
+ if (mountedRef.current) {
79
+ setMachineCard({
80
+ device: null,
81
+ reachable: null,
82
+ loading: false,
83
+ status: 'none',
84
+ title: 'Not signed in',
85
+ detail: 'Sign in to pick and monitor a remote dev machine.',
86
+ });
87
+ }
88
+ return;
89
+ }
90
+ if (!cfg.preferredDeviceId) {
91
+ if (mountedRef.current) {
92
+ setMachineCard({
93
+ device: null,
94
+ reachable: null,
95
+ loading: false,
96
+ status: 'none',
97
+ title: 'No machine selected',
98
+ detail: 'Choose which machine this SDK should talk to.',
99
+ });
100
+ }
101
+ return;
102
+ }
103
+ if (mountedRef.current) {
104
+ setMachineCard((prev) => ({ ...prev, loading: true }));
105
+ }
106
+ try {
107
+ const devices = await (0, auth_1.listReachableDevices)(cfg.authToken);
108
+ const all = [...devices.owned, ...devices.shared];
109
+ const device = all.find((candidate) => candidate.deviceId === cfg.preferredDeviceId) ?? null;
110
+ if (!device) {
111
+ if (mountedRef.current) {
112
+ setMachineCard({
113
+ device: null,
114
+ reachable: null,
115
+ loading: false,
116
+ status: 'offline',
117
+ title: 'Selected machine missing',
118
+ detail: 'The saved machine was not returned by the device list. Re-select it.',
119
+ });
120
+ }
121
+ return;
122
+ }
123
+ let reachable = null;
124
+ const client = YaverFeedback_1.YaverFeedback.getP2PClient();
125
+ if (device.isOnline && !device.needsAuth && client) {
126
+ reachable = await client.health();
127
+ }
128
+ const hostHint = device.hostEmail ? ` via ${device.hostEmail}` : '';
129
+ let status = 'live';
130
+ let detail = `${device.platform}${hostHint}`;
131
+ if (!device.isOnline) {
132
+ status = 'offline';
133
+ detail = 'Machine offline. Start `yaver serve` on the selected machine.';
134
+ }
135
+ else if (device.needsAuth) {
136
+ status = 'attention';
137
+ detail = 'Machine needs pairing again before feedback actions can run.';
138
+ }
139
+ else if (device.runnerDown) {
140
+ status = 'attention';
141
+ detail = 'Machine is online but the coding agent is down.';
142
+ }
143
+ else if (reachable === false) {
144
+ status = 'offline';
145
+ detail = 'Machine selected, but the agent is not responding.';
146
+ }
147
+ if (mountedRef.current) {
148
+ setMachineCard({
149
+ device,
150
+ reachable,
151
+ loading: false,
152
+ status,
153
+ title: device.name || device.deviceId,
154
+ detail,
155
+ });
156
+ }
157
+ }
158
+ catch (err) {
159
+ if (mountedRef.current) {
160
+ setMachineCard({
161
+ device: null,
162
+ reachable: null,
163
+ loading: false,
164
+ status: 'offline',
165
+ title: 'Machine status unavailable',
166
+ detail: err instanceof Error ? err.message : String(err),
167
+ });
168
+ }
169
+ }
170
+ }, []);
67
171
  (0, react_1.useEffect)(() => {
68
172
  mountedRef.current = true;
69
173
  const sub = react_native_1.DeviceEventEmitter.addListener('yaverFeedback:startReport', () => {
@@ -71,8 +175,10 @@ const FeedbackModal = () => {
71
175
  setVisible(true);
72
176
  setError(null);
73
177
  setToast(null);
178
+ setProgress(null);
74
179
  setAction('idle');
75
- setShowCaptureChoices(false);
180
+ setShowVibeInput(false);
181
+ setVibePrompt('');
76
182
  // Re-read the "user hid the quick icon" flag on every open so
77
183
  // the re-enable row reflects the latest preference (the user
78
184
  // might have hidden or shown it between opens).
@@ -82,6 +188,13 @@ const FeedbackModal = () => {
82
188
  setQuickIconHidden(v);
83
189
  })
84
190
  .catch(() => { });
191
+ YaverFeedback_1.YaverFeedback.getQuickIconColorPreset()
192
+ .then((preset) => {
193
+ if (mountedRef.current)
194
+ setQuickIconColorPreset(preset);
195
+ })
196
+ .catch(() => { });
197
+ void loadSelectedMachine();
85
198
  }
86
199
  });
87
200
  // Agent streams build / compile progress through the BlackBox
@@ -109,7 +222,15 @@ const FeedbackModal = () => {
109
222
  sub.remove();
110
223
  statusSub.remove();
111
224
  };
112
- }, []);
225
+ }, [loadSelectedMachine]);
226
+ (0, react_1.useEffect)(() => {
227
+ if (!visible)
228
+ return;
229
+ const interval = setInterval(() => {
230
+ void loadSelectedMachine();
231
+ }, 5000);
232
+ return () => clearInterval(interval);
233
+ }, [loadSelectedMachine, visible]);
113
234
  const closeSoon = (0, react_1.useCallback)((delayMs = 1200) => {
114
235
  setTimeout(() => {
115
236
  if (mountedRef.current)
@@ -120,8 +241,10 @@ const FeedbackModal = () => {
120
241
  setVisible(false);
121
242
  setError(null);
122
243
  setToast(null);
244
+ setProgress(null);
123
245
  setAction('idle');
124
- setShowCaptureChoices(false);
246
+ setShowVibeInput(false);
247
+ setVibePrompt('');
125
248
  }, []);
126
249
  // Helper: run a P2P call; on network failure, ask YaverFeedback to
127
250
  // re-query Convex for the fresh IP and retry once. Solves the common
@@ -177,31 +300,54 @@ const FeedbackModal = () => {
177
300
  setAction('hot-reloading');
178
301
  setError(null);
179
302
  setProgress(0);
180
- setToast('Sending…');
303
+ setToast('Contacting selected machine…');
181
304
  try {
305
+ await loadSelectedMachine();
306
+ const selected = await YaverFeedback_1.YaverFeedback.getSelectedRemoteDevice();
307
+ if (!selected) {
308
+ YaverFeedback_1.YaverFeedback.showMachinePicker();
309
+ throw new Error('No machine selected. Pick a machine and try again.');
310
+ }
311
+ if (selected.needsAuth) {
312
+ YaverFeedback_1.YaverFeedback.showMachinePicker();
313
+ throw new Error('Selected machine needs pairing again.');
314
+ }
315
+ if (!selected.isOnline) {
316
+ throw new Error('Selected machine is offline. Start `yaver serve` on it first.');
317
+ }
182
318
  // Default mode: bundle. Always rebuilds via the agent regardless
183
319
  // of Metro state. P2PClient.reloadApp auto-resolves projectName +
184
320
  // bundleId from expo-constants / NativeModules so the agent can
185
321
  // map this app to its MobileProject scan entry without needing
186
322
  // `yaver dev start` to have been run.
323
+ let ackMessage = 'Reload request acknowledged.';
187
324
  await runWithReconnect(async (client) => {
188
- await client.reloadApp('bundle');
325
+ const ack = await client.reloadApp('bundle');
326
+ ackMessage = ack.message;
327
+ setToast(ack.message);
328
+ setProgress(0.2);
189
329
  });
190
330
  // We don't auto-close here — the agent's BlackBox status pings
191
331
  // will keep the modal updated, and the on-device YaverBundleLoader
192
332
  // will reload the JS once the fresh bundle arrives. Modal stays
193
333
  // up for a beat so the user sees the final progress state.
334
+ setToast(ackMessage);
194
335
  closeSoon(2500);
195
336
  }
196
337
  catch (err) {
197
- setError(err instanceof Error ? err.message : String(err));
338
+ const message = err instanceof Error ? err.message : String(err);
339
+ setError(message);
340
+ setToast(message.toLowerCase().indexOf('session expired') >= 0
341
+ ? 'Session expired. Sign in again.'
342
+ : 'Hot reload did not start.');
343
+ await loadSelectedMachine();
198
344
  setProgress(null);
199
345
  }
200
346
  finally {
201
347
  if (mountedRef.current)
202
348
  setAction('idle');
203
349
  }
204
- }, [closeSoon, runWithReconnect]);
350
+ }, [closeSoon, loadSelectedMachine, runWithReconnect]);
205
351
  const uploadBundleWithOptionalFix = (0, react_1.useCallback)(async (bundle, fixOnUpload, successToast, failureToast) => {
206
352
  const client = YaverFeedback_1.YaverFeedback.getP2PClient();
207
353
  const config = YaverFeedback_1.YaverFeedback.getConfig();
@@ -235,14 +381,9 @@ const FeedbackModal = () => {
235
381
  setError(err instanceof Error ? err.message : String(err));
236
382
  }
237
383
  }, [closeSoon]);
238
- // ─── 3. Screenshot / Upload ───────────────────────────────────────
239
- const handleCaptureChoiceToggle = (0, react_1.useCallback)(() => {
240
- setShowCaptureChoices((v) => !v);
241
- }, []);
242
384
  const handleScreenshotAndFix = (0, react_1.useCallback)(async () => {
243
385
  setAction('capturing');
244
386
  setError(null);
245
- setShowCaptureChoices(false);
246
387
  setVisible(false);
247
388
  await new Promise((resolve) => setTimeout(resolve, 350));
248
389
  let path;
@@ -285,50 +426,11 @@ const FeedbackModal = () => {
285
426
  setAction('idle');
286
427
  }
287
428
  }, [uploadBundleWithOptionalFix]);
288
- const handleFileUpload = (0, react_1.useCallback)(async () => {
289
- setAction('capturing');
290
- setError(null);
291
- setShowCaptureChoices(false);
292
- try {
293
- const picked = await (0, capture_1.pickFeedbackFile)();
294
- const { Dimensions } = require('react-native');
295
- const { width, height } = Dimensions.get('window');
296
- const deviceInfo = {
297
- platform: react_native_1.Platform.OS,
298
- osVersion: String(react_native_1.Platform.Version),
299
- model: react_native_1.Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
300
- screenWidth: width,
301
- screenHeight: height,
302
- };
303
- const capturedErrors = YaverFeedback_1.YaverFeedback.getCapturedErrors();
304
- const bundle = {
305
- metadata: {
306
- timestamp: new Date().toISOString(),
307
- device: deviceInfo,
308
- app: {},
309
- userNote: `[Uploaded file] ${picked.name}`,
310
- },
311
- screenshots: picked.kind === 'image' ? [picked.path] : [],
312
- video: picked.kind === 'video' ? picked.path : undefined,
313
- audio: picked.kind === 'audio' ? picked.path : undefined,
314
- errors: capturedErrors.length > 0 ? capturedErrors : undefined,
315
- };
316
- if (picked.kind === 'unknown') {
317
- throw new Error('Pick an image, video, or audio file.');
318
- }
319
- await uploadBundleWithOptionalFix(bundle, picked.kind === 'image', picked.kind === 'image' ? 'Fix task started' : 'File uploaded');
320
- }
321
- catch (err) {
322
- const message = err instanceof Error ? err.message : String(err);
323
- if (message !== 'File selection canceled.') {
324
- setError(message);
325
- }
326
- }
327
- finally {
328
- if (mountedRef.current)
329
- setAction('idle');
330
- }
429
+ /*
430
+ const handleFileUpload = useCallback(async () => {
431
+ ...
331
432
  }, [uploadBundleWithOptionalFix]);
433
+ */
332
434
  // ─── 3. Vibing ─────────────────────────────────────────────────────
333
435
  // First tap expands the input; second submit fires the actual
334
436
  // /vibing/execute. Mirrors the Yaver mobile app's Vibing tab —
@@ -381,127 +483,23 @@ const FeedbackModal = () => {
381
483
  setAction('idle');
382
484
  }
383
485
  }, [vibePrompt]);
384
- // ─── 4. Screen recording ───────────────────────────────────────────
385
- const handleScreenRecording = (0, react_1.useCallback)(async () => {
386
- setError(null);
387
- if (!isRecordingVideo && lastVideo) {
388
- const config = YaverFeedback_1.YaverFeedback.getConfig();
389
- if (!config?.agentUrl) {
390
- setError('Not connected to the agent yet.');
391
- return;
392
- }
393
- setAction('uploading-video');
394
- try {
395
- const { Dimensions } = require('react-native');
396
- const { width, height } = Dimensions.get('window');
397
- const deviceInfo = {
398
- platform: react_native_1.Platform.OS,
399
- osVersion: String(react_native_1.Platform.Version),
400
- model: react_native_1.Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
401
- screenWidth: width,
402
- screenHeight: height,
403
- };
404
- const bundle = {
405
- metadata: {
406
- timestamp: new Date().toISOString(),
407
- device: deviceInfo,
408
- app: {},
409
- userNote: '[Screen recording]',
410
- },
411
- screenshots: [],
412
- video: lastVideo.path,
413
- errors: YaverFeedback_1.YaverFeedback.getCapturedErrors().length
414
- ? YaverFeedback_1.YaverFeedback.getCapturedErrors()
415
- : undefined,
416
- };
417
- await (0, upload_1.uploadFeedback)(config.agentUrl, config.authToken ?? '', bundle);
418
- if (mountedRef.current) {
419
- setToast(`Recording uploaded — ${Math.round(lastVideo.duration)}s`);
420
- setLastVideo(null);
421
- }
422
- closeSoon(1200);
423
- }
424
- catch (err) {
425
- setError(err instanceof Error ? err.message : String(err));
426
- }
427
- finally {
428
- if (mountedRef.current)
429
- setAction('idle');
430
- }
431
- return;
432
- }
433
- if (isRecordingVideo) {
434
- try {
435
- const result = await (0, capture_1.stopVideoRecording)();
436
- if (mountedRef.current) {
437
- setIsRecordingVideo(false);
438
- setLastVideo(result);
439
- setAction('uploading-video');
440
- setToast('Uploading recording…');
441
- }
442
- const config = YaverFeedback_1.YaverFeedback.getConfig();
443
- if (!config?.agentUrl) {
444
- throw new Error('Not connected to the agent yet.');
445
- }
446
- const { Dimensions } = require('react-native');
447
- const { width, height } = Dimensions.get('window');
448
- const deviceInfo = {
449
- platform: react_native_1.Platform.OS,
450
- osVersion: String(react_native_1.Platform.Version),
451
- model: react_native_1.Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
452
- screenWidth: width,
453
- screenHeight: height,
454
- };
455
- const bundle = {
456
- metadata: {
457
- timestamp: new Date().toISOString(),
458
- device: deviceInfo,
459
- app: {},
460
- userNote: '[Screen recording]',
461
- },
462
- screenshots: [],
463
- video: result.path,
464
- errors: YaverFeedback_1.YaverFeedback.getCapturedErrors().length
465
- ? YaverFeedback_1.YaverFeedback.getCapturedErrors()
466
- : undefined,
467
- };
468
- await (0, upload_1.uploadFeedback)(config.agentUrl, config.authToken ?? '', bundle);
469
- if (mountedRef.current) {
470
- setToast(`Recording uploaded — ${Math.round(result.duration)}s`);
471
- setLastVideo(null);
472
- }
473
- closeSoon(1200);
474
- }
475
- catch (err) {
476
- setIsRecordingVideo(false);
477
- setError(err instanceof Error ? err.message : String(err));
478
- }
479
- finally {
480
- if (mountedRef.current)
481
- setAction('idle');
482
- }
483
- }
484
- else {
485
- try {
486
- await (0, capture_1.startVideoRecording)();
487
- if (mountedRef.current) {
488
- setIsRecordingVideo(true);
489
- setToast('Recording… tap again to stop and upload');
490
- setLastVideo(null);
491
- }
492
- }
493
- catch (err) {
494
- setError(err instanceof Error ? err.message : String(err));
495
- }
496
- }
486
+ /*
487
+ const handleScreenRecording = useCallback(async () => {
488
+ ...
497
489
  }, [closeSoon, isRecordingVideo, lastVideo]);
490
+ */
498
491
  const busy = action !== 'idle';
499
492
  return (<>
500
493
  <AuthOverlay_1.AuthOverlay />
501
494
  <QuickActionIcon_1.QuickActionIcon />
502
495
  {visible && (<react_native_1.Modal visible={visible} animationType="slide" transparent onRequestClose={handleClose}>
503
496
  <react_native_1.Pressable style={styles.overlay} onPress={handleClose}>
504
- <react_native_1.Pressable style={styles.modal} onPress={(e) => e.stopPropagation()}>
497
+ <react_native_1.KeyboardAvoidingView behavior={react_native_1.Platform.OS === 'ios' ? 'padding' : 'height'} style={styles.kbAvoider} pointerEvents="box-none">
498
+ <react_native_1.Pressable style={styles.modal} onPress={(e) => {
499
+ e.stopPropagation();
500
+ react_native_1.Keyboard.dismiss();
501
+ }}>
502
+ <react_native_1.ScrollView style={styles.scroll} contentContainerStyle={styles.scrollContent} keyboardShouldPersistTaps="handled">
505
503
  <react_native_1.View style={styles.header}>
506
504
  <react_native_1.Text style={styles.title}>Send Feedback</react_native_1.Text>
507
505
  <react_native_1.Pressable onPress={handleClose} hitSlop={12} style={styles.closeBtn} accessibilityRole="button" accessibilityLabel="Close">
@@ -509,6 +507,89 @@ const FeedbackModal = () => {
509
507
  </react_native_1.Pressable>
510
508
  </react_native_1.View>
511
509
 
510
+ <react_native_1.Pressable onPress={() => {
511
+ if (!YaverFeedback_1.YaverFeedback.isAuthed()) {
512
+ YaverFeedback_1.YaverFeedback.showLogin();
513
+ return;
514
+ }
515
+ YaverFeedback_1.YaverFeedback.showMachinePicker();
516
+ }} style={[
517
+ styles.machineCard,
518
+ machineCard.status === 'live' && styles.machineCardLive,
519
+ machineCard.status === 'attention' && styles.machineCardAttention,
520
+ machineCard.status === 'offline' && styles.machineCardOffline,
521
+ ]}>
522
+ <react_native_1.View style={styles.machineHeader}>
523
+ <react_native_1.View style={styles.machineTitleWrap}>
524
+ <react_native_1.View style={[
525
+ styles.machineDot,
526
+ machineCard.status === 'live' && styles.machineDotLive,
527
+ machineCard.status === 'attention' && styles.machineDotAttention,
528
+ machineCard.status === 'offline' && styles.machineDotOffline,
529
+ ]}/>
530
+ <react_native_1.Text style={styles.machineLabel}>Selected Machine</react_native_1.Text>
531
+ </react_native_1.View>
532
+ <react_native_1.Text style={styles.machineAction}>
533
+ {machineCard.loading ? 'Refreshing…' : 'Change'}
534
+ </react_native_1.Text>
535
+ </react_native_1.View>
536
+ <react_native_1.Text style={styles.machineName}>
537
+ {machineCard.loading ? 'Checking machine…' : machineCard.title}
538
+ </react_native_1.Text>
539
+ <react_native_1.Text style={styles.machineMeta}>{machineCard.detail}</react_native_1.Text>
540
+ </react_native_1.Pressable>
541
+
542
+ {quickIconHidden && (<react_native_1.View style={styles.quickIconNote}>
543
+ <react_native_1.Text style={styles.quickIconNoteText}>
544
+ Quick access icon is hidden. Shake the phone if you want feedback back fast.
545
+ </react_native_1.Text>
546
+ <react_native_1.Pressable onPress={() => {
547
+ void YaverFeedback_1.YaverFeedback.setQuickIconVisible(true);
548
+ setQuickIconHidden(false);
549
+ }} style={({ pressed }) => [
550
+ styles.quickIconToggle,
551
+ pressed && styles.buttonPressed,
552
+ ]}>
553
+ <react_native_1.Text style={styles.quickIconToggleText}>Show quick icon again</react_native_1.Text>
554
+ </react_native_1.Pressable>
555
+ </react_native_1.View>)}
556
+
557
+ <react_native_1.View style={styles.iconSelector}>
558
+ <react_native_1.Text style={styles.iconSelectorTitle}>Quick Icon Color</react_native_1.Text>
559
+ <react_native_1.Text style={styles.iconSelectorText}>
560
+ Pick a runtime color so the floating y icon does not overlap with your app UI.
561
+ </react_native_1.Text>
562
+ <react_native_1.View style={styles.iconSelectorGrid}>
563
+ {Object.entries(preferences_1.QUICK_ICON_COLOR_PRESETS).map(([preset, colors]) => {
564
+ const selected = quickIconColorPreset === preset;
565
+ return (<react_native_1.Pressable key={preset} onPress={() => {
566
+ setQuickIconColorPreset(preset);
567
+ void YaverFeedback_1.YaverFeedback.setQuickIconColorPreset(preset);
568
+ }} style={[
569
+ styles.iconOption,
570
+ selected && styles.iconOptionSelected,
571
+ ]}>
572
+ <react_native_1.View style={[
573
+ styles.iconOptionCircle,
574
+ {
575
+ backgroundColor: colors.backgroundColor,
576
+ borderColor: colors.borderColor,
577
+ shadowColor: colors.shadowColor,
578
+ },
579
+ ]}>
580
+ <react_native_1.Text style={[
581
+ styles.iconOptionLabel,
582
+ { color: colors.foregroundColor },
583
+ ]}>
584
+ y
585
+ </react_native_1.Text>
586
+ </react_native_1.View>
587
+ <react_native_1.Text style={styles.iconOptionText}>{colors.label}</react_native_1.Text>
588
+ </react_native_1.Pressable>);
589
+ })}
590
+ </react_native_1.View>
591
+ </react_native_1.View>
592
+
512
593
  {/* 1. Hot Reload — the common path */}
513
594
  <ActionRow label={action === 'hot-reloading' ? 'Reloading…' : 'Hot Reload'} tint="#fbbf24" onPress={handleHotReload} disabled={busy} busy={action === 'hot-reloading'}/>
514
595
 
@@ -537,22 +618,8 @@ const FeedbackModal = () => {
537
618
  Last vibing task: {lastVibeTaskId.slice(0, 12)}…
538
619
  </react_native_1.Text>)}
539
620
 
540
- {/* Screenshot / Upload */}
541
- {!showCaptureChoices ? (<ActionRow label={action === 'capturing'
542
- ? 'Working…'
543
- : 'Screenshot / Upload'} tint="#22c55e" onPress={handleCaptureChoiceToggle} disabled={busy} busy={action === 'capturing'}/>) : (<react_native_1.View style={styles.captureChoices}>
544
- <ActionRow label="Take Screenshot" tint="#22c55e" onPress={handleScreenshotAndFix} disabled={busy}/>
545
- <ActionRow label="Upload File" tint="#34d399" onPress={handleFileUpload} disabled={busy}/>
546
- </react_native_1.View>)}
547
-
548
- {/* 4. Screen recording */}
549
- <ActionRow label={action === 'uploading-video'
550
- ? 'Uploading…'
551
- : isRecordingVideo
552
- ? 'Stop & Upload Recording'
553
- : lastVideo
554
- ? `Retry Upload Recording · ${Math.round(lastVideo.duration)}s`
555
- : 'Screen Recording'} tint={isRecordingVideo ? '#ef4444' : '#60a5fa'} onPress={handleScreenRecording} disabled={busy && action !== 'uploading-video' && !isRecordingVideo} busy={action === 'uploading-video'}/>
621
+ {/* Screenshot & Fix */}
622
+ <ActionRow label={action === 'capturing' ? 'Working…' : 'Screenshot & Fix'} tint="#22c55e" onPress={handleScreenshotAndFix} disabled={busy} busy={action === 'capturing'}/>
556
623
 
557
624
  {progress !== null && (<react_native_1.View style={styles.progressTrack}>
558
625
  <react_native_1.View style={[
@@ -563,34 +630,15 @@ const FeedbackModal = () => {
563
630
  {toast && <react_native_1.Text style={styles.toast}>{toast}</react_native_1.Text>}
564
631
  {error && <react_native_1.Text style={styles.error}>{error}</react_native_1.Text>}
565
632
 
566
- {/* Quick-icon toggle. The user's three ways to control
567
- the floating icon are: (1) long-press the icon →
568
- Hide, (2) tap this row to toggle it on/off, (3) shake
569
- → this modal → tap this row. Shake is the unkillable
570
- back-door when the icon is hidden and the dev hasn't
571
- exposed their own settings UI. */}
572
- <react_native_1.Pressable onPress={async () => {
573
- const next = !quickIconHidden;
574
- setQuickIconHidden(next);
575
- await YaverFeedback_1.YaverFeedback.setQuickIconVisible(!next);
576
- }} style={({ pressed }) => [
577
- styles.quickIconToggle,
578
- pressed && { opacity: 0.7 },
579
- ]} accessibilityRole="button" accessibilityLabel={quickIconHidden ? 'Show quick icon' : 'Hide quick icon'}>
580
- <react_native_1.Text style={styles.quickIconToggleText}>
581
- {quickIconHidden
582
- ? '◯ Show quick-access icon'
583
- : '● Hide quick-access icon'}
584
- </react_native_1.Text>
585
- </react_native_1.Pressable>
586
-
587
633
  <react_native_1.Pressable onPress={handleClose} style={({ pressed }) => [
588
634
  styles.cancelBtn,
589
635
  pressed && styles.buttonPressed,
590
636
  ]} accessibilityRole="button" accessibilityLabel="Cancel">
591
637
  <react_native_1.Text style={styles.cancelBtnText}>Cancel</react_native_1.Text>
592
638
  </react_native_1.Pressable>
639
+ </react_native_1.ScrollView>
593
640
  </react_native_1.Pressable>
641
+ </react_native_1.KeyboardAvoidingView>
594
642
  </react_native_1.Pressable>
595
643
  </react_native_1.Modal>)}
596
644
  </>);
@@ -663,6 +711,10 @@ const styles = react_native_1.StyleSheet.create({
663
711
  backgroundColor: 'rgba(0,0,0,0.55)',
664
712
  justifyContent: 'flex-end',
665
713
  },
714
+ kbAvoider: {
715
+ width: '100%',
716
+ justifyContent: 'flex-end',
717
+ },
666
718
  modal: {
667
719
  backgroundColor: '#141422',
668
720
  borderTopLeftRadius: 22,
@@ -670,6 +722,14 @@ const styles = react_native_1.StyleSheet.create({
670
722
  padding: 22,
671
723
  paddingBottom: 36,
672
724
  gap: 12,
725
+ maxHeight: '92%',
726
+ },
727
+ scroll: {
728
+ maxHeight: '100%',
729
+ },
730
+ scrollContent: {
731
+ gap: 12,
732
+ paddingBottom: 8,
673
733
  },
674
734
  header: {
675
735
  flexDirection: 'row',
@@ -710,6 +770,74 @@ const styles = react_native_1.StyleSheet.create({
710
770
  fontSize: 15,
711
771
  fontWeight: '700',
712
772
  },
773
+ machineCard: {
774
+ borderRadius: 14,
775
+ borderWidth: 1,
776
+ padding: 14,
777
+ backgroundColor: 'rgba(255,255,255,0.04)',
778
+ borderColor: 'rgba(255,255,255,0.12)',
779
+ },
780
+ machineCardLive: {
781
+ backgroundColor: 'rgba(34,197,94,0.10)',
782
+ borderColor: 'rgba(34,197,94,0.35)',
783
+ },
784
+ machineCardAttention: {
785
+ backgroundColor: 'rgba(245,158,11,0.10)',
786
+ borderColor: 'rgba(245,158,11,0.35)',
787
+ },
788
+ machineCardOffline: {
789
+ backgroundColor: 'rgba(239,68,68,0.10)',
790
+ borderColor: 'rgba(239,68,68,0.35)',
791
+ },
792
+ machineHeader: {
793
+ flexDirection: 'row',
794
+ alignItems: 'center',
795
+ justifyContent: 'space-between',
796
+ marginBottom: 6,
797
+ },
798
+ machineTitleWrap: {
799
+ flexDirection: 'row',
800
+ alignItems: 'center',
801
+ gap: 8,
802
+ },
803
+ machineDot: {
804
+ width: 10,
805
+ height: 10,
806
+ borderRadius: 5,
807
+ backgroundColor: '#6b7280',
808
+ },
809
+ machineDotLive: {
810
+ backgroundColor: '#22c55e',
811
+ },
812
+ machineDotAttention: {
813
+ backgroundColor: '#f59e0b',
814
+ },
815
+ machineDotOffline: {
816
+ backgroundColor: '#ef4444',
817
+ },
818
+ machineLabel: {
819
+ color: '#cbd5e1',
820
+ fontSize: 12,
821
+ fontWeight: '700',
822
+ textTransform: 'uppercase',
823
+ letterSpacing: 0.8,
824
+ },
825
+ machineAction: {
826
+ color: '#a5b4fc',
827
+ fontSize: 12,
828
+ fontWeight: '700',
829
+ },
830
+ machineName: {
831
+ color: '#fff',
832
+ fontSize: 16,
833
+ fontWeight: '700',
834
+ },
835
+ machineMeta: {
836
+ color: '#cbd5e1',
837
+ fontSize: 12,
838
+ marginTop: 4,
839
+ lineHeight: 17,
840
+ },
713
841
  captureChoices: {
714
842
  gap: 10,
715
843
  },
@@ -738,15 +866,89 @@ const styles = react_native_1.StyleSheet.create({
738
866
  marginTop: 4,
739
867
  },
740
868
  quickIconToggle: {
741
- marginTop: 4,
742
- alignSelf: 'center',
869
+ marginTop: 6,
870
+ alignSelf: 'flex-start',
743
871
  paddingVertical: 6,
744
- paddingHorizontal: 12,
872
+ paddingHorizontal: 10,
873
+ borderRadius: 10,
874
+ backgroundColor: 'rgba(255,255,255,0.05)',
745
875
  },
746
876
  quickIconToggleText: {
747
- color: '#9ca3af',
877
+ color: '#cbd5e1',
748
878
  fontSize: 12,
749
- fontWeight: '500',
879
+ fontWeight: '700',
880
+ },
881
+ quickIconNote: {
882
+ borderRadius: 12,
883
+ borderWidth: 1,
884
+ borderColor: 'rgba(251,191,36,0.28)',
885
+ backgroundColor: 'rgba(251,191,36,0.08)',
886
+ padding: 12,
887
+ },
888
+ quickIconNoteText: {
889
+ color: '#fde68a',
890
+ fontSize: 12,
891
+ lineHeight: 17,
892
+ },
893
+ iconSelector: {
894
+ borderRadius: 12,
895
+ borderWidth: 1,
896
+ borderColor: 'rgba(255,255,255,0.09)',
897
+ backgroundColor: 'rgba(255,255,255,0.03)',
898
+ padding: 12,
899
+ gap: 10,
900
+ },
901
+ iconSelectorTitle: {
902
+ color: '#f8fafc',
903
+ fontSize: 13,
904
+ fontWeight: '700',
905
+ },
906
+ iconSelectorText: {
907
+ color: '#cbd5e1',
908
+ fontSize: 12,
909
+ lineHeight: 17,
910
+ },
911
+ iconSelectorGrid: {
912
+ flexDirection: 'row',
913
+ flexWrap: 'wrap',
914
+ gap: 10,
915
+ },
916
+ iconOption: {
917
+ width: '31%',
918
+ minWidth: 84,
919
+ borderRadius: 12,
920
+ borderWidth: 1,
921
+ borderColor: 'rgba(255,255,255,0.08)',
922
+ backgroundColor: 'rgba(255,255,255,0.02)',
923
+ paddingVertical: 10,
924
+ paddingHorizontal: 8,
925
+ alignItems: 'center',
926
+ gap: 8,
927
+ },
928
+ iconOptionSelected: {
929
+ borderColor: 'rgba(129,140,248,0.72)',
930
+ backgroundColor: 'rgba(129,140,248,0.12)',
931
+ },
932
+ iconOptionCircle: {
933
+ width: 38,
934
+ height: 38,
935
+ borderRadius: 19,
936
+ alignItems: 'center',
937
+ justifyContent: 'center',
938
+ borderWidth: 2,
939
+ shadowOffset: { width: 0, height: 2 },
940
+ shadowOpacity: 0.28,
941
+ shadowRadius: 5,
942
+ elevation: 5,
943
+ },
944
+ iconOptionLabel: {
945
+ fontSize: 18,
946
+ fontWeight: '700',
947
+ },
948
+ iconOptionText: {
949
+ color: '#e2e8f0',
950
+ fontSize: 11,
951
+ fontWeight: '600',
750
952
  },
751
953
  cancelBtn: {
752
954
  marginTop: 4,