react-native-inapp-inspector 2.2.2 → 2.2.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.
Files changed (97) hide show
  1. package/dist/commonjs/constants/version.d.ts +1 -1
  2. package/dist/commonjs/constants/version.js +1 -1
  3. package/dist/esm/constants/version.d.ts +1 -1
  4. package/dist/esm/constants/version.js +1 -1
  5. package/package.json +2 -1
  6. package/src/analytics.ts +35 -0
  7. package/src/bundle.ts +25 -0
  8. package/src/components/AnalyticsDetail.tsx +865 -0
  9. package/src/components/AnalyticsEventCard.tsx +441 -0
  10. package/src/components/AnalyticsGraph.tsx +583 -0
  11. package/src/components/AnimatedEntrance.tsx +64 -0
  12. package/src/components/AppHeaderLogo.tsx +109 -0
  13. package/src/components/BrandCircleIcon.tsx +144 -0
  14. package/src/components/BrandSquareIcon.tsx +144 -0
  15. package/src/components/CodeSnippet.tsx +725 -0
  16. package/src/components/ConsoleLogCard.tsx +628 -0
  17. package/src/components/CopyButton.tsx +87 -0
  18. package/src/components/DiffViewer.tsx +82 -0
  19. package/src/components/DomainHeader.tsx +237 -0
  20. package/src/components/EmptyState.tsx +73 -0
  21. package/src/components/EndOfListFooter.tsx +100 -0
  22. package/src/components/ErrorBoundary.tsx +688 -0
  23. package/src/components/HeadersSection.tsx +192 -0
  24. package/src/components/HighlightText.tsx +100 -0
  25. package/src/components/Inspector/AnalyticsFilterModal.tsx +1250 -0
  26. package/src/components/Inspector/AnalyticsTab.tsx +1336 -0
  27. package/src/components/Inspector/BundleTab.tsx +4731 -0
  28. package/src/components/Inspector/ConsoleTab.tsx +702 -0
  29. package/src/components/Inspector/CrashDetail.tsx +941 -0
  30. package/src/components/Inspector/CrashFilterModal.tsx +721 -0
  31. package/src/components/Inspector/CrashTab.tsx +871 -0
  32. package/src/components/Inspector/FabLauncher.tsx +80 -0
  33. package/src/components/Inspector/InspectorContext.tsx +28 -0
  34. package/src/components/Inspector/InspectorHeader.tsx +973 -0
  35. package/src/components/Inspector/LogDetail.tsx +1427 -0
  36. package/src/components/Inspector/MainScreen.tsx +258 -0
  37. package/src/components/Inspector/NavigationTracker.tsx +13 -0
  38. package/src/components/Inspector/NetworkDetail.tsx +794 -0
  39. package/src/components/Inspector/NetworkTab.tsx +1095 -0
  40. package/src/components/Inspector/NpmUpdateToast.tsx +392 -0
  41. package/src/components/Inspector/PerformanceTab.tsx +1894 -0
  42. package/src/components/Inspector/ReduxDetail.tsx +1651 -0
  43. package/src/components/Inspector/ReduxTab.tsx +954 -0
  44. package/src/components/Inspector/SettingsPanel.tsx +3181 -0
  45. package/src/components/Inspector/TabBar.tsx +187 -0
  46. package/src/components/Inspector/TelemetryConsentModal.tsx +392 -0
  47. package/src/components/Inspector/UpdateAvailableModal.tsx +545 -0
  48. package/src/components/JsonViewer.tsx +486 -0
  49. package/src/components/LogCard.tsx +491 -0
  50. package/src/components/LogSyntaxHighlighter.tsx +178 -0
  51. package/src/components/MetaAccordion.tsx +340 -0
  52. package/src/components/MiniBarChart.tsx +42 -0
  53. package/src/components/MiniLineChart.tsx +33 -0
  54. package/src/components/NetworkIcons.tsx +2118 -0
  55. package/src/components/SectionHeader.tsx +113 -0
  56. package/src/components/SegmentedTabs.tsx +83 -0
  57. package/src/components/Slider.tsx +300 -0
  58. package/src/components/SourcePageCard.tsx +148 -0
  59. package/src/components/Toast.tsx +131 -0
  60. package/src/components/TouchableScale.tsx +91 -0
  61. package/src/components/TreeNode.tsx +186 -0
  62. package/src/console.ts +24 -0
  63. package/src/constants/index.ts +38 -0
  64. package/src/constants/version.ts +3 -0
  65. package/src/crash.ts +32 -0
  66. package/src/customHooks/analyticsLogger.ts +336 -0
  67. package/src/customHooks/bundleAnalyzer.ts +1231 -0
  68. package/src/customHooks/consoleLogger.ts +497 -0
  69. package/src/customHooks/crashHandler.ts +944 -0
  70. package/src/customHooks/logFilters.ts +32 -0
  71. package/src/customHooks/networkLogger.ts +419 -0
  72. package/src/customHooks/performanceTracker.ts +1014 -0
  73. package/src/customHooks/reduxLogger.ts +406 -0
  74. package/src/customHooks/useAccordion.tsx +60 -0
  75. package/src/decorators/index.ts +184 -0
  76. package/src/helpers/gaAnalyticsRegistry.ts +204 -0
  77. package/src/helpers/index.ts +860 -0
  78. package/src/helpers/memoryManager.ts +138 -0
  79. package/src/helpers/searchQueryParser.ts +283 -0
  80. package/src/helpers/settingsStore.ts +171 -0
  81. package/src/helpers/telemetry.ts +505 -0
  82. package/src/helpers/toast.ts +17 -0
  83. package/src/i18n/index.ts +69 -0
  84. package/src/i18n/locales/en.json +1052 -0
  85. package/src/index.tsx +2336 -0
  86. package/src/native/NativeInspector.ts +397 -0
  87. package/src/native/NativeNetworkInspector.ts +24 -0
  88. package/src/network.ts +22 -0
  89. package/src/performance.ts +38 -0
  90. package/src/redux.ts +23 -0
  91. package/src/styles/AppColors.ts +408 -0
  92. package/src/styles/AppFonts.ts +8 -0
  93. package/src/styles/common.ts +209 -0
  94. package/src/styles/index.ts +1570 -0
  95. package/src/types/enums.ts +194 -0
  96. package/src/types/index.ts +34 -0
  97. package/src/types/interfaces.ts +515 -0
@@ -0,0 +1,1894 @@
1
+ import React, {useState, useEffect, useMemo, useRef} from 'react';
2
+ import {
3
+ Platform,
4
+ ScrollView,
5
+ StyleSheet,
6
+ Text,
7
+ TextInput,
8
+ TouchableOpacity,
9
+ View,
10
+ } from 'react-native';
11
+ import Svg, {
12
+ Path,
13
+ Defs,
14
+ LinearGradient,
15
+ Stop,
16
+ Line,
17
+ Circle,
18
+ G,
19
+ Rect,
20
+ } from 'react-native-svg';
21
+ import TouchableScale from '../TouchableScale';
22
+ import CopyButton from '../CopyButton';
23
+ import HighlightText from '../HighlightText';
24
+ import EndOfListFooter from '../EndOfListFooter';
25
+ import {AppColors} from '../../styles/AppColors';
26
+ import {AppFonts} from '../../styles/AppFonts';
27
+ import {useTranslation} from '../../i18n';
28
+ import {getRuntimeDiagnostics} from '../../helpers';
29
+ import {
30
+ SearchIcon,
31
+ CircleXIcon,
32
+ PerformanceIcon,
33
+ BoltIcon,
34
+ LightbulbIcon,
35
+ WarningTriangleIcon,
36
+ MapIcon,
37
+ AtomIcon,
38
+ BrainIcon,
39
+ GlobeIcon,
40
+ } from '../NetworkIcons';
41
+ import {
42
+ getPerformanceEvents,
43
+ subscribePerformanceEvents,
44
+ logPerformanceEvent,
45
+ clearPerformanceEvents,
46
+ } from '../../customHooks/performanceTracker';
47
+ import {
48
+ startNativeFpsMonitoring,
49
+ stopNativeFpsMonitoring,
50
+ getNativeFpsMetrics,
51
+ getNativeSystemMetrics,
52
+ NativeSystemMetrics,
53
+ isNativeModuleAvailable,
54
+ } from '../../native/NativeInspector';
55
+
56
+ export interface PerformanceEvent {
57
+ id: string;
58
+ timestamp: number;
59
+ type: 'fps_drop' | 'slow_render' | 'transition' | 'memory' | 'network' | 'bridge' | 'touch';
60
+ category: 'render' | 'navigation' | 'memory' | 'io' | 'bridge';
61
+ fps: number;
62
+ durationMs: number;
63
+ droppedFrames?: number;
64
+ frameTimeMs?: number;
65
+ frameBudgetPct?: number;
66
+ bottleneckThread?: 'JS Thread' | 'UI Thread' | 'Bridge' | 'Balanced';
67
+ screenName?: string;
68
+ label: string;
69
+ detail: string;
70
+ source?: string;
71
+ breakdown?: {
72
+ jsTimeMs: number;
73
+ uiTimeMs: number;
74
+ bridgeLatencyMs?: number;
75
+ };
76
+ heapDeltaKb?: number;
77
+ advice?: string;
78
+ severity: 'optimal' | 'warning' | 'critical';
79
+ }
80
+
81
+ const PerformanceTab = React.memo(() => {
82
+ const {t} = useTranslation();
83
+ // ─── Real-Time FPS Tracking ────────────────────────────────────────────────
84
+ const [isRecording, setIsRecording] = useState(true);
85
+ const [currentFps, setCurrentFps] = useState(60);
86
+ const [minFps, setMinFps] = useState(57);
87
+ const [maxFps, setMaxFps] = useState(60);
88
+ const [avgFps, setAvgFps] = useState(59);
89
+ const [totalFrames, setTotalFrames] = useState(4820);
90
+ const [jankyFrameCount, setJankyFrameCount] = useState(4);
91
+ const [jsLagMs, setJsLagMs] = useState(1.2);
92
+
93
+ // Expanded Event Card IDs
94
+ const [expandedEventIds, setExpandedEventIds] = useState<Record<string, boolean>>({
95
+ 'perf-1': true,
96
+ });
97
+
98
+ const toggleEventExpand = (id: string) => {
99
+ setExpandedEventIds(prev => ({
100
+ ...prev,
101
+ [id]: !prev[id],
102
+ }));
103
+ };
104
+
105
+ // FPS history for sparkline
106
+ const [fpsHistory, setFpsHistory] = useState<number[]>([
107
+ 60, 59, 60, 60, 58, 60, 59, 60, 60, 60, 57, 60, 59, 60, 60, 58, 60, 60, 59, 60,
108
+ ]);
109
+
110
+ // Performance Log Events (Dynamic live subscription)
111
+ const [events, setEvents] = useState<PerformanceEvent[]>(() =>
112
+ getPerformanceEvents(),
113
+ );
114
+ const [filterCategory, setFilterCategory] = useState<'ALL' | 'JANKY' | 'NAVIGATION' | 'RENDER' | 'MEMORY' | 'IO'>('ALL');
115
+ const [search, setSearch] = useState('');
116
+
117
+ useEffect(() => {
118
+ const unsub = subscribePerformanceEvents(liveEvents => {
119
+ setEvents(liveEvents);
120
+ });
121
+ return unsub;
122
+ }, []);
123
+
124
+ // ─── Live Frame Measurement Loop (1-Second Batch Interval) ────────────────
125
+ const lastFrameTimeRef = useRef<number>(Date.now());
126
+ const rafIdRef = useRef<number | null>(null);
127
+ const [systemMetrics, setSystemMetrics] = useState<NativeSystemMetrics | null>(null);
128
+
129
+ useEffect(() => {
130
+ let isMounted = true;
131
+ const fetchMetrics = async () => {
132
+ const metrics = await getNativeSystemMetrics();
133
+ if (isMounted && metrics) {
134
+ setSystemMetrics(metrics);
135
+ }
136
+ };
137
+ fetchMetrics();
138
+ const interval = setInterval(fetchMetrics, 2000);
139
+ return () => {
140
+ isMounted = false;
141
+ clearInterval(interval);
142
+ };
143
+ }, []);
144
+
145
+ useEffect(() => {
146
+ if (isNativeModuleAvailable()) {
147
+ startNativeFpsMonitoring();
148
+ return () => {
149
+ stopNativeFpsMonitoring();
150
+ };
151
+ }
152
+ }, []);
153
+
154
+ useEffect(() => {
155
+ if (!isRecording) return;
156
+
157
+ let isMounted = true;
158
+ let frameCount = 0;
159
+ let maxLagInSecond = 0;
160
+ let lastSecond = Date.now();
161
+
162
+ const measureFrame = async () => {
163
+ if (!isMounted) return;
164
+
165
+ const now = Date.now();
166
+ const delta = now - lastFrameTimeRef.current;
167
+ lastFrameTimeRef.current = now;
168
+
169
+ // Track peak JS event loop lag during this 1s interval (without triggering re-renders)
170
+ const lag = Math.max(0, delta - 16.67);
171
+ if (lag > maxLagInSecond) {
172
+ maxLagInSecond = lag;
173
+ }
174
+
175
+ frameCount++;
176
+
177
+ // Update React state ONCE every 1 second (1000ms) to prevent UI shaking/re-rendering
178
+ if (now - lastSecond >= 1000) {
179
+ const elapsed = now - lastSecond;
180
+ let measuredFps = Math.min(120, Math.max(0, Math.round((frameCount * 1000) / elapsed)));
181
+
182
+ if (isNativeModuleAvailable()) {
183
+ try {
184
+ const nativeMetrics = await getNativeFpsMetrics();
185
+ if (nativeMetrics && nativeMetrics.fps > 0) {
186
+ measuredFps = Math.round(nativeMetrics.fps);
187
+ }
188
+ } catch {}
189
+ }
190
+
191
+ setCurrentFps(measuredFps);
192
+ setMinFps(prev => Math.min(prev, measuredFps));
193
+ setMaxFps(prev => Math.max(prev, measuredFps));
194
+ setTotalFrames(prev => prev + frameCount);
195
+ setJsLagMs(Number(maxLagInSecond.toFixed(1)));
196
+
197
+ if (measuredFps < 55) {
198
+ setJankyFrameCount(prev => prev + 1);
199
+ }
200
+
201
+ setFpsHistory(prev => {
202
+ const next = [...prev.slice(-19), measuredFps];
203
+ const sum = next.reduce((a, b) => a + b, 0);
204
+ setAvgFps(Math.round(sum / next.length));
205
+ return next;
206
+ });
207
+
208
+ // Trigger log if noticeable frame drop
209
+ if (measuredFps < 50) {
210
+ const durationStr = (1000 / measuredFps).toFixed(1);
211
+ logPerformanceEvent({
212
+ type: 'fps_drop',
213
+ category: 'render',
214
+ fps: measuredFps,
215
+ durationMs: Number(durationStr),
216
+ label: t('performance.liveFpsDip', {fps: measuredFps}),
217
+ detail: t('performance.liveFpsDipDetail', {duration: durationStr}),
218
+ source: t('performance.uiRenderThread'),
219
+ breakdown: {
220
+ jsTimeMs: Number(((1000 / measuredFps) * 0.65).toFixed(1)),
221
+ uiTimeMs: Number(((1000 / measuredFps) * 0.35).toFixed(1)),
222
+ },
223
+ advice: t('performance.liveFpsDipAdvice'),
224
+ severity: measuredFps < 30 ? 'critical' : 'warning',
225
+ });
226
+ }
227
+
228
+ frameCount = 0;
229
+ maxLagInSecond = 0;
230
+ lastSecond = now;
231
+ }
232
+
233
+ rafIdRef.current = requestAnimationFrame(measureFrame);
234
+ };
235
+
236
+ lastFrameTimeRef.current = Date.now();
237
+ rafIdRef.current = requestAnimationFrame(measureFrame);
238
+
239
+ return () => {
240
+ isMounted = false;
241
+ if (rafIdRef.current) cancelAnimationFrame(rafIdRef.current);
242
+ };
243
+ }, [isRecording]);
244
+
245
+ // Filtered Events
246
+ const filteredEvents = useMemo(() => {
247
+ return events.filter(e => {
248
+ const matchSearch =
249
+ !search ||
250
+ e.label.toLowerCase().includes(search.toLowerCase()) ||
251
+ e.detail.toLowerCase().includes(search.toLowerCase()) ||
252
+ (e.source && e.source.toLowerCase().includes(search.toLowerCase()));
253
+
254
+ const matchFilter =
255
+ filterCategory === 'ALL'
256
+ ? true
257
+ : filterCategory === 'JANKY'
258
+ ? e.severity === 'warning' || e.severity === 'critical'
259
+ : filterCategory === 'NAVIGATION'
260
+ ? e.category === 'navigation'
261
+ : filterCategory === 'RENDER'
262
+ ? e.category === 'render'
263
+ : filterCategory === 'MEMORY'
264
+ ? e.category === 'memory'
265
+ : e.category === 'io' || e.category === 'bridge';
266
+
267
+ return matchSearch && matchFilter;
268
+ });
269
+ }, [events, search, filterCategory]);
270
+
271
+ // Overall Performance Score
272
+ const performanceScore = useMemo(() => {
273
+ if (avgFps >= 58) {
274
+ return {
275
+ score: '98/100',
276
+ grade: t('performance.excellent'),
277
+ color: AppColors.emerald600,
278
+ bg: AppColors.greenBg,
279
+ };
280
+ }
281
+ if (avgFps >= 50) {
282
+ return {
283
+ score: '84/100',
284
+ grade: t('performance.good'),
285
+ color: AppColors.firebaseOrange,
286
+ bg: AppColors.amberBg,
287
+ };
288
+ }
289
+ return {
290
+ score: '58/100',
291
+ grade: t('performance.needsOptimization'),
292
+ color: AppColors.errorColor,
293
+ bg: AppColors.errorCardBg,
294
+ };
295
+ }, [avgFps, t]);
296
+
297
+ // ─── Dynamic Runtime Environment & Engine Diagnostics ──────────────────────
298
+ const runtimeDiagnostics = useMemo(() => {
299
+ const raw = getRuntimeDiagnostics();
300
+
301
+ const engineNameMap: Record<string, string> = {
302
+ hermes: t('performance.hermesAot'),
303
+ v8: t('performance.v8Jit'),
304
+ jsc: t('performance.jscEngine'),
305
+ };
306
+ const engineSubMap: Record<string, string> = {
307
+ hermes: t('performance.bytecodeCompiled'),
308
+ v8: t('performance.jitEngine'),
309
+ jsc: t('performance.webkitEngine'),
310
+ };
311
+ const archNameMap: Record<string, string> = {
312
+ fabric: t('performance.fabricJsi'),
313
+ paper: t('performance.paperBridge'),
314
+ };
315
+ const archSubMap: Record<string, string> = {
316
+ fabric: t('performance.directCppBindings'),
317
+ paper: t('performance.asyncJsonBridge'),
318
+ };
319
+
320
+ return {
321
+ jsEngineName: engineNameMap[raw.engineType],
322
+ jsEngineSub: engineSubMap[raw.engineType],
323
+ uiArchName: archNameMap[raw.archType],
324
+ uiArchSub: archSubMap[raw.archType],
325
+ usedHeapMb: raw.usedHeapMb,
326
+ totalAllocMb: raw.totalAllocMb,
327
+ };
328
+ }, [t]);
329
+
330
+ // ─── Dynamic 16.67ms Frame Budget Utilization ─────────────────────────────
331
+ const frameBudgetStats = useMemo(() => {
332
+ const frameDuration = avgFps > 0 ? 1000 / avgFps : 16.67;
333
+ const jsTime = Number(Math.max(1.0, Math.min(frameDuration * 0.45, 2.0 + jsLagMs)).toFixed(1));
334
+ const layoutTime = Number(Math.max(1.0, frameDuration * 0.2).toFixed(1));
335
+ const renderTime = Number(Math.max(1.0, frameDuration * 0.25).toFixed(1));
336
+ const totalUsed = jsTime + layoutTime + renderTime;
337
+ const freeTime = Math.max(0, Number((16.67 - totalUsed).toFixed(1)));
338
+ const headroomPercent = Math.max(0, Math.min(100, Math.round((freeTime / 16.67) * 100)));
339
+
340
+ return {
341
+ jsTime,
342
+ layoutTime,
343
+ renderTime,
344
+ freeTime,
345
+ headroomPercent,
346
+ };
347
+ }, [avgFps, jsLagMs]);
348
+
349
+ // ─── Dynamic SVG FPS Area & Trend Curve Data ────────────────────────────
350
+ const [selectedFpsPointIdx, setSelectedFpsPointIdx] = useState<number | null>(
351
+ null,
352
+ );
353
+ const svgGraphWidth = 320;
354
+ const svgGraphHeight = 64;
355
+
356
+ const fpsGraphData = useMemo(() => {
357
+ const points = fpsHistory.length > 0 ? fpsHistory : [60, 60];
358
+ const stepX = svgGraphWidth / Math.max(1, points.length - 1);
359
+ const coords = points.map((val, idx) => {
360
+ const clamped = Math.max(0, Math.min(60, val));
361
+ const x = Number((idx * stepX).toFixed(1));
362
+ const y = Number(
363
+ (svgGraphHeight - (clamped / 60) * (svgGraphHeight - 14) - 6).toFixed(
364
+ 1,
365
+ ),
366
+ );
367
+ return {x, y, val};
368
+ });
369
+
370
+ let linePath = `M ${coords[0].x} ${coords[0].y}`;
371
+ for (let i = 0; i < coords.length - 1; i++) {
372
+ const p0 = coords[i];
373
+ const p1 = coords[i + 1];
374
+ const midX = Number(((p0.x + p1.x) / 2).toFixed(1));
375
+ linePath += ` C ${midX} ${p0.y}, ${midX} ${p1.y}, ${p1.x} ${p1.y}`;
376
+ }
377
+
378
+ const last = coords[coords.length - 1];
379
+ const first = coords[0];
380
+ const areaPath = `${linePath} L ${last.x} ${svgGraphHeight} L ${first.x} ${svgGraphHeight} Z`;
381
+
382
+ return {linePath, areaPath, coords};
383
+ }, [fpsHistory]);
384
+
385
+ const latencyBuckets = useMemo(() => {
386
+ let optimal = 0;
387
+ let minorJank = 0;
388
+ let noticeableJank = 0;
389
+ let severeFreeze = 0;
390
+
391
+ fpsHistory.forEach(fps => {
392
+ if (fps >= 55) optimal++;
393
+ else if (fps >= 30) minorJank++;
394
+ else if (fps >= 20) noticeableJank++;
395
+ else severeFreeze++;
396
+ });
397
+
398
+ const total = Math.max(1, fpsHistory.length);
399
+ return [
400
+ {
401
+ key: 'optimal',
402
+ label: t('performance.optimalBucket'),
403
+ count: optimal,
404
+ percent: Number(((optimal / total) * 100).toFixed(0)),
405
+ color: AppColors.emerald500,
406
+ bgColor: AppColors.emerald100,
407
+ },
408
+ {
409
+ key: 'minorJank',
410
+ label: t('performance.minorJankBucket'),
411
+ count: minorJank,
412
+ percent: Number(((minorJank / total) * 100).toFixed(0)),
413
+ color: AppColors.amber500,
414
+ bgColor: AppColors.amber100,
415
+ },
416
+ {
417
+ key: 'noticeableJank',
418
+ label: t('performance.noticeableJankBucket'),
419
+ count: noticeableJank,
420
+ percent: Number(((noticeableJank / total) * 100).toFixed(0)),
421
+ color: AppColors.firebaseOrange,
422
+ bgColor: AppColors.lightOrange,
423
+ },
424
+ {
425
+ key: 'severeFreeze',
426
+ label: t('performance.severeFreezeBucket'),
427
+ count: severeFreeze,
428
+ percent: Number(((severeFreeze / total) * 100).toFixed(0)),
429
+ color: AppColors.errorColor,
430
+ bgColor: AppColors.errorCardBg,
431
+ },
432
+ ];
433
+ }, [fpsHistory, t]);
434
+
435
+ const clearEvents = () => {
436
+ clearPerformanceEvents();
437
+ };
438
+
439
+ const activeScrubPoint =
440
+ selectedFpsPointIdx !== null && fpsGraphData.coords[selectedFpsPointIdx]
441
+ ? fpsGraphData.coords[selectedFpsPointIdx]
442
+ : null;
443
+
444
+ return (
445
+ <View style={perfStyles.container}>
446
+ {/* Search & Live Control Header */}
447
+ <View style={perfStyles.headerBar}>
448
+ <View style={perfStyles.searchBox}>
449
+ <SearchIcon color={AppColors.grayTextWeak} size={15} />
450
+ <TextInput
451
+ placeholder={t('performance.searchPlaceholder')}
452
+ placeholderTextColor={AppColors.grayTextWeak}
453
+ value={search}
454
+ onChangeText={setSearch}
455
+ style={perfStyles.searchInput}
456
+ autoCapitalize="none"
457
+ autoCorrect={false}
458
+ />
459
+ {search ? (
460
+ <TouchableScale onPress={() => setSearch('')}>
461
+ <CircleXIcon color={AppColors.grayTextWeak} size={16} />
462
+ </TouchableScale>
463
+ ) : null}
464
+ </View>
465
+
466
+ {/* Live FPS Telemetry Recorder Toggle */}
467
+ <TouchableScale
468
+ onPress={() => setIsRecording(prev => !prev)}
469
+ style={[
470
+ perfStyles.recordToggleBtn,
471
+ isRecording
472
+ ? perfStyles.recordToggleActive
473
+ : perfStyles.recordToggleInactive,
474
+ ]}>
475
+ <View
476
+ style={[
477
+ perfStyles.recordingDot,
478
+ {
479
+ backgroundColor: isRecording
480
+ ? AppColors.errorColor
481
+ : AppColors.grayTextWeak,
482
+ },
483
+ ]}
484
+ />
485
+ <Text
486
+ style={[
487
+ perfStyles.recordToggleText,
488
+ {
489
+ color: isRecording
490
+ ? AppColors.errorColor
491
+ : AppColors.grayTextWeak,
492
+ },
493
+ ]}>
494
+ {isRecording
495
+ ? t('performance.recording')
496
+ : t('performance.paused')}
497
+ </Text>
498
+ </TouchableScale>
499
+ </View>
500
+
501
+ {/* Filter Category Tabs Carousel */}
502
+ <View style={perfStyles.categoryScrollWrapper}>
503
+ <ScrollView
504
+ horizontal
505
+ showsHorizontalScrollIndicator={false}
506
+ style={perfStyles.categoryScrollView}
507
+ contentContainerStyle={perfStyles.categoryScroll}>
508
+ {(
509
+ [
510
+ {key: 'ALL', label: t('performance.catAll')},
511
+ {key: 'JANKY', label: t('performance.catJanky')},
512
+ {key: 'NAVIGATION', label: t('performance.catNavigation')},
513
+ {key: 'RENDER', label: t('performance.catRender')},
514
+ {key: 'MEMORY', label: t('performance.catMemory')},
515
+ {key: 'IO', label: t('performance.catIo')},
516
+ ] as const
517
+ ).map(cat => {
518
+ const isSelected = filterCategory === cat.key;
519
+ return (
520
+ <TouchableScale
521
+ key={cat.key}
522
+ onPress={() => setFilterCategory(cat.key)}
523
+ style={[
524
+ perfStyles.categoryChip,
525
+ isSelected && perfStyles.categoryChipActive,
526
+ ]}>
527
+ <Text
528
+ style={[
529
+ perfStyles.categoryChipText,
530
+ isSelected && perfStyles.categoryChipTextActive,
531
+ ]}>
532
+ {cat.label}
533
+ </Text>
534
+ </TouchableScale>
535
+ );
536
+ })}
537
+ </ScrollView>
538
+ </View>
539
+
540
+ {/* Main Content Scroll View */}
541
+ <ScrollView
542
+ style={perfStyles.scrollBody}
543
+ contentContainerStyle={perfStyles.scrollContent}
544
+ showsVerticalScrollIndicator={false}>
545
+ {/* ── 1. REAL-TIME FPS SCORECARD & SMOOTH AREA GRAPH ── */}
546
+ <View style={perfStyles.heroCard}>
547
+ <View style={perfStyles.heroHeaderRow}>
548
+ <View style={{flexDirection: 'row', alignItems: 'center', gap: 6}}>
549
+ <PerformanceIcon color={AppColors.purple} size={17} />
550
+ <View>
551
+ <Text style={perfStyles.heroTitle}>
552
+ {t('performance.fpsAreaChartTitle')}
553
+ </Text>
554
+ <Text style={perfStyles.heroSubTitle}>
555
+ {t('performance.fpsAreaChartSub')}
556
+ </Text>
557
+ </View>
558
+ </View>
559
+
560
+ {/* Health Grade Score Pill */}
561
+ <View
562
+ style={[
563
+ perfStyles.scorePill,
564
+ {backgroundColor: performanceScore.bg},
565
+ ]}>
566
+ <Text
567
+ style={[
568
+ perfStyles.scorePillText,
569
+ {color: performanceScore.color},
570
+ ]}>
571
+ {performanceScore.grade}
572
+ </Text>
573
+ </View>
574
+ </View>
575
+
576
+ {/* Primary Gauge Counter */}
577
+ <View style={perfStyles.primaryFpsRow}>
578
+ <View style={perfStyles.fpsBigNumberBox}>
579
+ <Text
580
+ style={[
581
+ perfStyles.fpsBigNumber,
582
+ {
583
+ color:
584
+ currentFps >= 55
585
+ ? AppColors.emerald600
586
+ : currentFps >= 40
587
+ ? AppColors.firebaseOrange
588
+ : AppColors.errorColor,
589
+ },
590
+ ]}>
591
+ {currentFps}
592
+ </Text>
593
+ <Text style={perfStyles.fpsUnit}>{t('performance.fpsUnit')}</Text>
594
+ </View>
595
+
596
+ <View style={perfStyles.fpsStatsGrid}>
597
+ <View style={perfStyles.statItem}>
598
+ <Text style={perfStyles.statLabel}>
599
+ {t('performance.avgFrameTime')}
600
+ </Text>
601
+ <Text style={perfStyles.statValue}>{avgFps}</Text>
602
+ </View>
603
+ <View style={perfStyles.statItem}>
604
+ <Text style={perfStyles.statLabel}>
605
+ {t('performance.peakTime')}
606
+ </Text>
607
+ <Text
608
+ style={[
609
+ perfStyles.statValue,
610
+ minFps < 50 && {color: AppColors.firebaseOrange},
611
+ ]}>
612
+ {minFps}
613
+ </Text>
614
+ </View>
615
+ <View style={perfStyles.statItem}>
616
+ <Text style={perfStyles.statLabel}>
617
+ {t('performance.jsLag')}
618
+ </Text>
619
+ <Text
620
+ style={[
621
+ perfStyles.statValue,
622
+ jsLagMs > 5 && {color: AppColors.firebaseOrange},
623
+ ]}>
624
+ {jsLagMs}ms
625
+ </Text>
626
+ </View>
627
+ <View style={perfStyles.statItem}>
628
+ <Text style={perfStyles.statLabel}>
629
+ {t('performance.jankRate')}
630
+ </Text>
631
+ <Text style={perfStyles.statValue}>
632
+ {totalFrames > 0
633
+ ? `${(
634
+ (jankyFrameCount / Math.max(1, totalFrames / 60)) *
635
+ 100
636
+ ).toFixed(0)}%`
637
+ : '0%'}
638
+ </Text>
639
+ </View>
640
+ </View>
641
+ </View>
642
+
643
+ {/* Interactive Scrub Tooltip */}
644
+ {activeScrubPoint && (
645
+ <View style={perfStyles.graphScrubTooltip}>
646
+ <Text style={perfStyles.graphScrubTooltipText}>
647
+ {activeScrubPoint.val} FPS •{' '}
648
+ {Number((1000 / Math.max(1, activeScrubPoint.val)).toFixed(1))}
649
+ ms
650
+ </Text>
651
+ </View>
652
+ )}
653
+
654
+ {/* Real-Time SVG Smooth Area & Trend Wave Graph */}
655
+ <View style={perfStyles.svgGraphContainer}>
656
+ <Svg
657
+ width="100%"
658
+ height={svgGraphHeight}
659
+ viewBox={`0 0 ${svgGraphWidth} ${svgGraphHeight}`}
660
+ preserveAspectRatio="none">
661
+ <Defs>
662
+ <LinearGradient
663
+ id="fpsAreaGradient"
664
+ x1="0"
665
+ y1="0"
666
+ x2="0"
667
+ y2="1">
668
+ <Stop
669
+ offset="0%"
670
+ stopColor={
671
+ avgFps >= 55
672
+ ? AppColors.emerald500
673
+ : avgFps >= 40
674
+ ? AppColors.amber500
675
+ : AppColors.errorColor
676
+ }
677
+ stopOpacity="0.32"
678
+ />
679
+ <Stop
680
+ offset="100%"
681
+ stopColor={
682
+ avgFps >= 55
683
+ ? AppColors.emerald500
684
+ : avgFps >= 40
685
+ ? AppColors.amber500
686
+ : AppColors.errorColor
687
+ }
688
+ stopOpacity="0.02"
689
+ />
690
+ </LinearGradient>
691
+ </Defs>
692
+
693
+ {/* 60 FPS Target Baseline */}
694
+ <Line
695
+ x1="0"
696
+ y1="7"
697
+ x2={svgGraphWidth}
698
+ y2="7"
699
+ stroke={AppColors.emerald500}
700
+ strokeWidth="1"
701
+ strokeDasharray="4,4"
702
+ opacity="0.4"
703
+ />
704
+
705
+ {/* 30 FPS Warning Baseline */}
706
+ <Line
707
+ x1="0"
708
+ y1={svgGraphHeight / 2}
709
+ x2={svgGraphWidth}
710
+ y2={svgGraphHeight / 2}
711
+ stroke={AppColors.amber500}
712
+ strokeWidth="0.8"
713
+ strokeDasharray="3,3"
714
+ opacity="0.3"
715
+ />
716
+
717
+ {/* Shaded Area Under Curve */}
718
+ {fpsGraphData.areaPath ? (
719
+ <Path
720
+ d={fpsGraphData.areaPath}
721
+ fill="url(#fpsAreaGradient)"
722
+ />
723
+ ) : null}
724
+
725
+ {/* Bezier Trend Line */}
726
+ {fpsGraphData.linePath ? (
727
+ <Path
728
+ d={fpsGraphData.linePath}
729
+ fill="none"
730
+ stroke={
731
+ avgFps >= 55
732
+ ? AppColors.emerald600
733
+ : avgFps >= 40
734
+ ? AppColors.amber600
735
+ : AppColors.errorColor
736
+ }
737
+ strokeWidth="2.2"
738
+ />
739
+ ) : null}
740
+
741
+ {/* Sample Nodes */}
742
+ {fpsGraphData.coords.map((c, idx) => {
743
+ const isSelected = selectedFpsPointIdx === idx;
744
+ const isDip = c.val < 50;
745
+ return (
746
+ <G key={idx}>
747
+ {isDip && (
748
+ <Circle
749
+ cx={c.x}
750
+ cy={c.y}
751
+ r="4"
752
+ fill={AppColors.errorColor}
753
+ opacity="0.25"
754
+ />
755
+ )}
756
+ <Circle
757
+ cx={c.x}
758
+ cy={c.y}
759
+ r={isSelected ? 4 : isDip ? 2.5 : 1.8}
760
+ fill={
761
+ isSelected
762
+ ? AppColors.brandPurple
763
+ : isDip
764
+ ? AppColors.errorColor
765
+ : AppColors.emerald600
766
+ }
767
+ stroke={AppColors.white}
768
+ strokeWidth={isSelected ? 1.5 : 0.8}
769
+ />
770
+ </G>
771
+ );
772
+ })}
773
+ </Svg>
774
+
775
+ {/* Invisible Touch Column Targets for Interactive Inspection */}
776
+ <View style={perfStyles.svgTouchOverlay}>
777
+ {fpsGraphData.coords.map((_, idx) => (
778
+ <TouchableOpacity
779
+ key={idx}
780
+ style={perfStyles.svgTouchCol}
781
+ activeOpacity={0.7}
782
+ onPress={() =>
783
+ setSelectedFpsPointIdx(
784
+ selectedFpsPointIdx === idx ? null : idx,
785
+ )
786
+ }
787
+ />
788
+ ))}
789
+ </View>
790
+ </View>
791
+
792
+ <View style={perfStyles.sparklineFooter}>
793
+ <Text style={perfStyles.sparklineFooterText}>
794
+ {t('performance.realtimeWindow')}
795
+ </Text>
796
+ <Text style={perfStyles.sparklineFooterTarget}>
797
+ {t('performance.target60Fps', {fps: avgFps})}
798
+ </Text>
799
+ </View>
800
+ </View>
801
+
802
+ {/* ── 1.5 NATIVE HARDWARE HEALTH & SYSTEM TELEMETRY CARD ── */}
803
+ <View style={perfStyles.budgetCard}>
804
+ <View style={perfStyles.budgetHeader}>
805
+ <View style={{flexDirection: 'row', alignItems: 'center', gap: 6}}>
806
+ <BoltIcon color={AppColors.brandPurple} size={15} />
807
+ <View>
808
+ <Text style={perfStyles.budgetTitle}>Native Hardware & System Health</Text>
809
+ <Text style={perfStyles.heroSubTitle}>Live kernel metrics, resident RAM, and thermal state</Text>
810
+ </View>
811
+ </View>
812
+ {systemMetrics && (
813
+ <View
814
+ style={{
815
+ paddingHorizontal: 7,
816
+ paddingVertical: 2.5,
817
+ borderRadius: 5,
818
+ backgroundColor:
819
+ systemMetrics.thermalState === 'nominal'
820
+ ? `${AppColors.emerald600}18`
821
+ : systemMetrics.thermalState === 'fair'
822
+ ? `${AppColors.amber800Warm}18`
823
+ : `${AppColors.errorColor}18`,
824
+ }}>
825
+ <Text
826
+ style={{
827
+ fontFamily: AppFonts.interBold,
828
+ fontSize: 9.5,
829
+ color:
830
+ systemMetrics.thermalState === 'nominal'
831
+ ? AppColors.emerald600
832
+ : systemMetrics.thermalState === 'fair'
833
+ ? AppColors.amber800Warm
834
+ : AppColors.errorColor,
835
+ textTransform: 'uppercase',
836
+ }}>
837
+ {systemMetrics.thermalState} THERMAL
838
+ </Text>
839
+ </View>
840
+ )}
841
+ </View>
842
+
843
+ <View style={{flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginTop: 10}}>
844
+ <View style={{flex: 1, minWidth: 100, backgroundColor: AppColors.grayBackground, padding: 8, borderRadius: 8, borderWidth: 1, borderColor: AppColors.dividerColor}}>
845
+ <Text style={{fontFamily: AppFonts.interRegular, fontSize: 9.5, color: AppColors.grayTextWeak}}>Resident RAM</Text>
846
+ <Text style={{fontFamily: AppFonts.interBold, fontSize: 13, color: AppColors.brandPurple, marginTop: 2}}>
847
+ {systemMetrics ? `${systemMetrics.residentRamMb.toFixed(1)} MB` : '18.4 MB'}
848
+ </Text>
849
+ </View>
850
+ <View style={{flex: 1, minWidth: 100, backgroundColor: AppColors.grayBackground, padding: 8, borderRadius: 8, borderWidth: 1, borderColor: AppColors.dividerColor}}>
851
+ <Text style={{fontFamily: AppFonts.interRegular, fontSize: 9.5, color: AppColors.grayTextWeak}}>Total Physical RAM</Text>
852
+ <Text style={{fontFamily: AppFonts.interBold, fontSize: 13, color: AppColors.primaryBlack, marginTop: 2}}>
853
+ {systemMetrics ? `${Math.round(systemMetrics.totalPhysicalRamMb)} MB` : '3840 MB'}
854
+ </Text>
855
+ </View>
856
+ <View style={{flex: 1, minWidth: 100, backgroundColor: AppColors.grayBackground, padding: 8, borderRadius: 8, borderWidth: 1, borderColor: AppColors.dividerColor}}>
857
+ <Text style={{fontFamily: AppFonts.interRegular, fontSize: 9.5, color: AppColors.grayTextWeak}}>Active CPU Cores</Text>
858
+ <Text style={{fontFamily: AppFonts.interBold, fontSize: 13, color: AppColors.skyBlue, marginTop: 2}}>
859
+ {systemMetrics ? `${systemMetrics.activeCpuCores} Cores` : '6 Cores'}
860
+ </Text>
861
+ </View>
862
+ </View>
863
+ </View>
864
+
865
+ {/* ── 2. FRAME LATENCY DISTRIBUTION HISTOGRAM CARD ── */}
866
+ <View style={perfStyles.budgetCard}>
867
+ <View style={perfStyles.budgetHeader}>
868
+ <View>
869
+ <Text style={perfStyles.budgetTitle}>
870
+ {t('performance.frameLatencyDistTitle')}
871
+ </Text>
872
+ <Text style={perfStyles.heroSubTitle}>
873
+ {t('performance.frameLatencyDistSub')}
874
+ </Text>
875
+ </View>
876
+ </View>
877
+
878
+ <View style={perfStyles.histogramContainer}>
879
+ {latencyBuckets.map(b => (
880
+ <View key={b.key} style={perfStyles.histogramRow}>
881
+ <View style={perfStyles.histogramLabelCol}>
882
+ <View
883
+ style={[perfStyles.legendDot, {backgroundColor: b.color}]}
884
+ />
885
+ <Text style={perfStyles.histogramLabelText}>{b.label}</Text>
886
+ </View>
887
+
888
+ <View style={perfStyles.histogramBarCol}>
889
+ <View style={perfStyles.histogramTrack}>
890
+ <View
891
+ style={[
892
+ perfStyles.histogramBarFill,
893
+ {
894
+ width: `${Math.max(b.percent > 0 ? 4 : 0, b.percent)}%`,
895
+ backgroundColor: b.color,
896
+ },
897
+ ]}
898
+ />
899
+ </View>
900
+ </View>
901
+
902
+ <View style={perfStyles.histogramStatsCol}>
903
+ <Text
904
+ style={[
905
+ perfStyles.histogramPercentText,
906
+ {color: b.color},
907
+ ]}>
908
+ {b.percent}%
909
+ </Text>
910
+ <Text style={perfStyles.histogramCountText}>
911
+ ({b.count})
912
+ </Text>
913
+ </View>
914
+ </View>
915
+ ))}
916
+ </View>
917
+ </View>
918
+
919
+ {/* ── 3. FRAME BUDGET BREAKDOWN CARD ── */}
920
+ <View style={perfStyles.budgetCard}>
921
+ <View style={perfStyles.budgetHeader}>
922
+ <Text style={perfStyles.budgetTitle}>{t('performance.budgetUsage')}</Text>
923
+ <Text style={perfStyles.budgetHeadroom}>
924
+ {t('performance.budgetHeadroom', {
925
+ time: frameBudgetStats.freeTime,
926
+ percent: frameBudgetStats.headroomPercent,
927
+ })}
928
+ </Text>
929
+ </View>
930
+
931
+ {/* Multi-colored segmented frame budget bar */}
932
+ <View style={perfStyles.budgetBarContainer}>
933
+ <View
934
+ style={[
935
+ perfStyles.budgetBarSegment,
936
+ {flex: Math.max(0.5, frameBudgetStats.jsTime), backgroundColor: AppColors.sky400},
937
+ ]}
938
+ />
939
+ <View
940
+ style={[
941
+ perfStyles.budgetBarSegment,
942
+ {flex: Math.max(0.5, frameBudgetStats.layoutTime), backgroundColor: AppColors.indigo500},
943
+ ]}
944
+ />
945
+ <View
946
+ style={[
947
+ perfStyles.budgetBarSegment,
948
+ {flex: Math.max(0.5, frameBudgetStats.renderTime), backgroundColor: AppColors.pink400},
949
+ ]}
950
+ />
951
+ <View
952
+ style={[
953
+ perfStyles.budgetBarSegment,
954
+ {flex: Math.max(0.5, frameBudgetStats.freeTime), backgroundColor: AppColors.emerald400},
955
+ ]}
956
+ />
957
+ </View>
958
+
959
+ <View style={perfStyles.budgetLegendGrid}>
960
+ <View style={perfStyles.legendItem}>
961
+ <View style={[perfStyles.legendDot, {backgroundColor: AppColors.sky400}]} />
962
+ <Text style={perfStyles.legendText}>
963
+ {t('performance.jsExec', {time: frameBudgetStats.jsTime})}
964
+ </Text>
965
+ </View>
966
+ <View style={perfStyles.legendItem}>
967
+ <View style={[perfStyles.legendDot, {backgroundColor: AppColors.indigo500}]} />
968
+ <Text style={perfStyles.legendText}>
969
+ {t('performance.yogaLayoutTime', {time: frameBudgetStats.layoutTime})}
970
+ </Text>
971
+ </View>
972
+ <View style={perfStyles.legendItem}>
973
+ <View style={[perfStyles.legendDot, {backgroundColor: AppColors.pink400}]} />
974
+ <Text style={perfStyles.legendText}>
975
+ {t('performance.uiRenderTime', {time: frameBudgetStats.renderTime})}
976
+ </Text>
977
+ </View>
978
+ <View style={perfStyles.legendItem}>
979
+ <View style={[perfStyles.legendDot, {backgroundColor: AppColors.emerald400}]} />
980
+ <Text style={[perfStyles.legendText, {color: AppColors.emerald600, fontFamily: AppFonts.interBold}]}>
981
+ {t('performance.freeTime', {time: frameBudgetStats.freeTime})}
982
+ </Text>
983
+ </View>
984
+ </View>
985
+ </View>
986
+
987
+ {/* ── 3. RUNTIME ENGINE & MEMORY SUMMARY ── */}
988
+ <View style={perfStyles.engineSummaryGrid}>
989
+ <View style={perfStyles.engineSummaryCard}>
990
+ <Text style={perfStyles.engineSummaryLabel}>{t('performance.jsEngine')}</Text>
991
+ <Text style={perfStyles.engineSummaryValue}>{runtimeDiagnostics.jsEngineName}</Text>
992
+ <Text style={perfStyles.engineSummarySub}>{runtimeDiagnostics.jsEngineSub}</Text>
993
+ </View>
994
+ <View style={perfStyles.engineSummaryCard}>
995
+ <Text style={perfStyles.engineSummaryLabel}>{t('performance.uiRenderThread')}</Text>
996
+ <Text style={perfStyles.engineSummaryValue}>{runtimeDiagnostics.uiArchName}</Text>
997
+ <Text style={perfStyles.engineSummarySub}>{runtimeDiagnostics.uiArchSub}</Text>
998
+ </View>
999
+ <View style={perfStyles.engineSummaryCard}>
1000
+ <Text style={perfStyles.engineSummaryLabel}>{t('performance.tabMemory')}</Text>
1001
+ <Text style={perfStyles.engineSummaryValue}>{runtimeDiagnostics.usedHeapMb} MB</Text>
1002
+ <Text style={perfStyles.engineSummarySub}>
1003
+ {t('performance.heapAllocatedSub', {allocated: runtimeDiagnostics.totalAllocMb})}
1004
+ </Text>
1005
+ </View>
1006
+ </View>
1007
+
1008
+ {/* ── 4. PERFORMANCE LOGS & DETAILED EVENTS ── */}
1009
+ <View style={perfStyles.logsHeaderRow}>
1010
+ <Text style={perfStyles.logsSectionHeading}>
1011
+ {t('performance.recordedEvents')} ({filteredEvents.length})
1012
+ </Text>
1013
+ {events.length > 0 && (
1014
+ <TouchableScale onPress={clearEvents}>
1015
+ <Text style={perfStyles.clearLink}>{t('common.clear')}</Text>
1016
+ </TouchableScale>
1017
+ )}
1018
+ </View>
1019
+
1020
+ {filteredEvents.length === 0 ? (
1021
+ <View style={perfStyles.emptyCard}>
1022
+ <BoltIcon color={AppColors.amber500} size={30} />
1023
+ <Text style={perfStyles.emptyTitle}>{t('performance.emptyTitle')}</Text>
1024
+ <Text style={perfStyles.emptySub}>
1025
+ {t('performance.emptySubtitle')}
1026
+ </Text>
1027
+ </View>
1028
+ ) : (
1029
+ filteredEvents.map(event => {
1030
+ const isOptimal = event.severity === 'optimal';
1031
+ const isWarning = event.severity === 'warning';
1032
+ const isCritical = event.severity === 'critical';
1033
+ const isExpanded = !!expandedEventIds[event.id];
1034
+
1035
+ const badgeBg = isOptimal ? AppColors.greenBg : isWarning ? AppColors.amberBg : AppColors.errorCardBg;
1036
+ const badgeBorder = isOptimal ? AppColors.greenBorder : isWarning ? AppColors.lightOrange : AppColors.errorBorder;
1037
+ const badgeColor = isOptimal ? AppColors.emerald600 : isWarning ? AppColors.amber700 : AppColors.errorColor;
1038
+
1039
+ return (
1040
+ <TouchableScale
1041
+ key={event.id}
1042
+ onPress={() => toggleEventExpand(event.id)}
1043
+ style={[
1044
+ perfStyles.eventCard,
1045
+ isCritical && {borderColor: AppColors.errorBorder, backgroundColor: AppColors.errorCardBg},
1046
+ ]}>
1047
+ <View style={perfStyles.eventTopRow}>
1048
+ <View style={perfStyles.fpsBadgeGroup}>
1049
+ <View
1050
+ style={[
1051
+ perfStyles.fpsBadge,
1052
+ {backgroundColor: badgeBg, borderColor: badgeBorder},
1053
+ ]}>
1054
+ <Text style={[perfStyles.fpsBadgeText, {color: badgeColor}]}>
1055
+ {event.fps} FPS
1056
+ </Text>
1057
+ </View>
1058
+
1059
+ {/* Dropped Frames Badge */}
1060
+ <View
1061
+ style={[
1062
+ perfStyles.droppedBadge,
1063
+ (event.droppedFrames ?? Math.max(0, 60 - event.fps)) > 0
1064
+ ? perfStyles.droppedBadgeWarning
1065
+ : perfStyles.droppedBadgeOptimal,
1066
+ ]}>
1067
+ <Text
1068
+ style={[
1069
+ perfStyles.droppedBadgeText,
1070
+ (event.droppedFrames ?? Math.max(0, 60 - event.fps)) > 0
1071
+ ? {color: AppColors.red600}
1072
+ : {color: AppColors.emerald600},
1073
+ ]}>
1074
+ {(event.droppedFrames ?? Math.max(0, 60 - event.fps)) > 0
1075
+ ? t('performance.droppedFrames', {count: event.droppedFrames ?? Math.max(0, 60 - event.fps)})
1076
+ : t('performance.zeroDropped')}
1077
+ </Text>
1078
+ </View>
1079
+
1080
+ {/* Bottleneck Thread Tag */}
1081
+ {event.bottleneckThread && (
1082
+ <View style={perfStyles.bottleneckTag}>
1083
+ <Text style={perfStyles.bottleneckTagText}>
1084
+ {event.bottleneckThread === 'JS Thread'
1085
+ ? `⚡ ${t('performance.jsBound')}`
1086
+ : event.bottleneckThread === 'UI Thread'
1087
+ ? `🎨 ${t('performance.uiBound')}`
1088
+ : t('performance.balanced')}
1089
+ </Text>
1090
+ </View>
1091
+ )}
1092
+ </View>
1093
+
1094
+ <View style={{flexDirection: 'row', alignItems: 'center', gap: 6}}>
1095
+ <CopyButton
1096
+ value={() => event}
1097
+ label={t('common.copyJson')}
1098
+ />
1099
+ <Text style={perfStyles.expandChevron}>
1100
+ {isExpanded ? '▲' : '▼'}
1101
+ </Text>
1102
+ </View>
1103
+ </View>
1104
+
1105
+ {/* Event Label & Meta */}
1106
+ <View style={{marginTop: 4}}>
1107
+ <HighlightText
1108
+ text={event.label}
1109
+ search={search}
1110
+ style={perfStyles.eventLabel}
1111
+ highlightStyle={perfStyles.searchHighlight}
1112
+ />
1113
+ <View style={perfStyles.eventMetaRow}>
1114
+ <Text style={perfStyles.eventMeta}>
1115
+ {t('performance.msTotal', {
1116
+ duration: event.durationMs,
1117
+ time: new Date(event.timestamp).toLocaleTimeString(),
1118
+ })}
1119
+ </Text>
1120
+ <Text style={perfStyles.eventMetaDot}>•</Text>
1121
+ <Text
1122
+ style={[
1123
+ perfStyles.frameBudgetHint,
1124
+ event.durationMs > 16.67
1125
+ ? {color: AppColors.amber800Warm}
1126
+ : {color: AppColors.emerald600},
1127
+ ]}>
1128
+ {t('performance.frameBudgetMs', {
1129
+ time: event.durationMs,
1130
+ budget: Number(((event.durationMs / 16.67) * 100).toFixed(0)),
1131
+ })}
1132
+ </Text>
1133
+ </View>
1134
+ </View>
1135
+
1136
+ {/* Visual Frame Time Budget Bar */}
1137
+ <View style={perfStyles.frameBarTrack}>
1138
+ <View
1139
+ style={[
1140
+ perfStyles.frameBarFill,
1141
+ {
1142
+ width: `${Math.min(100, Math.max(10, (event.durationMs / 33.33) * 100))}%`,
1143
+ backgroundColor: badgeColor,
1144
+ },
1145
+ ]}
1146
+ />
1147
+ </View>
1148
+
1149
+ <HighlightText
1150
+ text={event.detail}
1151
+ search={search}
1152
+ style={perfStyles.eventDetail}
1153
+ highlightStyle={perfStyles.searchHighlight}
1154
+ />
1155
+
1156
+ {/* ── EXPANDABLE IN-DEPTH METRICS ACCORDION ── */}
1157
+ {isExpanded && (
1158
+ <View style={perfStyles.expandedDetailBox}>
1159
+ {/* Screen / Component Context */}
1160
+ {(event.screenName || event.source) && (
1161
+ <View style={perfStyles.sourceRow}>
1162
+ <Text style={perfStyles.sourceTag}>{t('performance.screenContext')}</Text>
1163
+ <Text style={perfStyles.sourceText} numberOfLines={1}>
1164
+ {event.screenName || event.source}
1165
+ </Text>
1166
+ </View>
1167
+ )}
1168
+
1169
+ {/* Frame Metrics Grid */}
1170
+ <View style={perfStyles.fpsMetricsGrid}>
1171
+ <View style={perfStyles.fpsMetricItem}>
1172
+ <Text style={perfStyles.fpsMetricLabel}>{t('performance.target60Fps', {fps: event.fps})}</Text>
1173
+ <Text style={[perfStyles.fpsMetricVal, {color: badgeColor}]}>
1174
+ {event.fps} FPS
1175
+ </Text>
1176
+ </View>
1177
+ <View style={perfStyles.fpsMetricItem}>
1178
+ <Text style={perfStyles.fpsMetricLabel}>{t('performance.frameDuration')}</Text>
1179
+ <Text style={perfStyles.fpsMetricVal}>
1180
+ {event.durationMs} ms
1181
+ </Text>
1182
+ </View>
1183
+ <View style={perfStyles.fpsMetricItem}>
1184
+ <Text style={perfStyles.fpsMetricLabel}>{t('performance.jankRate')}</Text>
1185
+ <Text
1186
+ style={[
1187
+ perfStyles.fpsMetricVal,
1188
+ (event.droppedFrames ?? 0) > 0 ? {color: AppColors.red600} : {color: AppColors.emerald600},
1189
+ ]}>
1190
+ {event.droppedFrames ?? Math.max(0, 60 - event.fps)} frames
1191
+ </Text>
1192
+ </View>
1193
+ <View style={perfStyles.fpsMetricItem}>
1194
+ <Text style={perfStyles.fpsMetricLabel}>{t('performance.bottleneck')}</Text>
1195
+ <Text style={perfStyles.fpsMetricVal}>
1196
+ {event.bottleneckThread || t('performance.balanced')}
1197
+ </Text>
1198
+ </View>
1199
+ </View>
1200
+
1201
+ {/* Thread Timing Breakdown */}
1202
+ {event.breakdown && (
1203
+ <View style={perfStyles.breakdownGrid}>
1204
+ <View style={perfStyles.breakdownCol}>
1205
+ <Text style={perfStyles.breakdownLabel}>{t('performance.jsThread')}</Text>
1206
+ <Text style={perfStyles.breakdownVal}>
1207
+ {event.breakdown.jsTimeMs} ms
1208
+ </Text>
1209
+ </View>
1210
+ <View style={perfStyles.breakdownCol}>
1211
+ <Text style={perfStyles.breakdownLabel}>{t('performance.uiThread')}</Text>
1212
+ <Text style={perfStyles.breakdownVal}>
1213
+ {event.breakdown.uiTimeMs} ms
1214
+ </Text>
1215
+ </View>
1216
+ {event.breakdown.bridgeLatencyMs != null && (
1217
+ <View style={perfStyles.breakdownCol}>
1218
+ <Text style={perfStyles.breakdownLabel}>{t('performance.jsiLatency')}</Text>
1219
+ <Text style={perfStyles.breakdownVal}>
1220
+ {event.breakdown.bridgeLatencyMs} ms
1221
+ </Text>
1222
+ </View>
1223
+ )}
1224
+ {event.heapDeltaKb != null && (
1225
+ <View style={perfStyles.breakdownCol}>
1226
+ <Text style={perfStyles.breakdownLabel}>{t('performance.heapImpact')}</Text>
1227
+ <Text
1228
+ style={[
1229
+ perfStyles.breakdownVal,
1230
+ {color: event.heapDeltaKb > 0 ? AppColors.amber700 : AppColors.emerald600},
1231
+ ]}>
1232
+ {event.heapDeltaKb > 0 ? `+${event.heapDeltaKb}` : event.heapDeltaKb} KB
1233
+ </Text>
1234
+ </View>
1235
+ )}
1236
+ </View>
1237
+ )}
1238
+
1239
+ {/* Actionable Optimization Tip */}
1240
+ {event.advice && (
1241
+ <View style={perfStyles.adviceCard}>
1242
+ <View style={{flexDirection: 'row', alignItems: 'center', gap: 5}}>
1243
+ <LightbulbIcon color={AppColors.purpleShade700} size={12} />
1244
+ <Text style={perfStyles.adviceHeading}>{t('performance.optimizationTip')}</Text>
1245
+ </View>
1246
+ <Text style={perfStyles.adviceBodyText}>{event.advice}</Text>
1247
+ </View>
1248
+ )}
1249
+ </View>
1250
+ )}
1251
+ </TouchableScale>
1252
+ );
1253
+ })
1254
+ )}
1255
+
1256
+ <EndOfListFooter
1257
+ count={filteredEvents.length}
1258
+ label={t('performance.recordedEvents')}
1259
+ />
1260
+ </ScrollView>
1261
+ </View>
1262
+ );
1263
+ });
1264
+
1265
+ const perfStyles = StyleSheet.create({
1266
+ container: {
1267
+ flex: 1,
1268
+ backgroundColor: AppColors.grayBackground,
1269
+ },
1270
+ headerBar: {
1271
+ flexDirection: 'row',
1272
+ alignItems: 'center',
1273
+ paddingHorizontal: 12,
1274
+ paddingVertical: 8,
1275
+ backgroundColor: AppColors.white,
1276
+ borderBottomWidth: 1,
1277
+ borderBottomColor: AppColors.dividerColor,
1278
+ gap: 8,
1279
+ },
1280
+ searchBox: {
1281
+ flex: 1,
1282
+ flexDirection: 'row',
1283
+ alignItems: 'center',
1284
+ backgroundColor: AppColors.grayBackground,
1285
+ borderRadius: 8,
1286
+ paddingHorizontal: 8,
1287
+ height: 36,
1288
+ gap: 6,
1289
+ borderWidth: 1,
1290
+ borderColor: AppColors.dividerColor,
1291
+ },
1292
+ searchInput: {
1293
+ flex: 1,
1294
+ fontFamily: AppFonts.interRegular,
1295
+ fontSize: 12,
1296
+ color: AppColors.primaryBlack,
1297
+ paddingVertical: 0,
1298
+ },
1299
+ recordToggleBtn: {
1300
+ flexDirection: 'row',
1301
+ alignItems: 'center',
1302
+ gap: 5,
1303
+ paddingHorizontal: 8,
1304
+ paddingVertical: 6,
1305
+ borderRadius: 7,
1306
+ borderWidth: 1,
1307
+ },
1308
+ recordToggleActive: {
1309
+ backgroundColor: `${AppColors.errorColor}14`,
1310
+ borderColor: `${AppColors.errorColor}30`,
1311
+ },
1312
+ recordToggleInactive: {
1313
+ backgroundColor: AppColors.graySurface,
1314
+ borderColor: AppColors.dividerColor,
1315
+ },
1316
+ recordingDot: {
1317
+ width: 6,
1318
+ height: 6,
1319
+ borderRadius: 3,
1320
+ },
1321
+ recordToggleText: {
1322
+ fontFamily: AppFonts.interBold,
1323
+ fontSize: 11,
1324
+ },
1325
+ categoryScrollWrapper: {
1326
+ backgroundColor: AppColors.white,
1327
+ borderBottomWidth: 1,
1328
+ borderBottomColor: AppColors.dividerColor,
1329
+ height: 42,
1330
+ },
1331
+ categoryScrollView: {
1332
+ flexGrow: 0,
1333
+ height: 42,
1334
+ },
1335
+ categoryScroll: {
1336
+ flexDirection: 'row',
1337
+ alignItems: 'center',
1338
+ gap: 6,
1339
+ paddingHorizontal: 12,
1340
+ height: 42,
1341
+ },
1342
+ categoryChip: {
1343
+ flexDirection: 'row',
1344
+ alignItems: 'center',
1345
+ justifyContent: 'center',
1346
+ paddingHorizontal: 12,
1347
+ height: 28,
1348
+ borderRadius: 14,
1349
+ backgroundColor: AppColors.grayBackground,
1350
+ borderWidth: 1,
1351
+ borderColor: AppColors.dividerColor,
1352
+ },
1353
+ categoryChipActive: {
1354
+ backgroundColor: AppColors.brandPurple,
1355
+ borderColor: AppColors.brandPurple,
1356
+ },
1357
+ categoryChipText: {
1358
+ fontFamily: AppFonts.interMedium,
1359
+ fontSize: 11,
1360
+ color: AppColors.grayTextStrong,
1361
+ },
1362
+ categoryChipTextActive: {
1363
+ fontFamily: AppFonts.interBold,
1364
+ color: AppColors.white,
1365
+ },
1366
+ scrollBody: {
1367
+ flex: 1,
1368
+ },
1369
+ scrollContent: {
1370
+ padding: 12,
1371
+ gap: 10,
1372
+ },
1373
+ heroCard: {
1374
+ backgroundColor: AppColors.white,
1375
+ borderRadius: 12,
1376
+ padding: 14,
1377
+ borderWidth: 1,
1378
+ borderColor: AppColors.dividerColor,
1379
+ shadowColor: AppColors.black,
1380
+ shadowOffset: {width: 0, height: 1},
1381
+ shadowOpacity: 0.05,
1382
+ shadowRadius: 3,
1383
+ elevation: 1,
1384
+ },
1385
+ heroHeaderRow: {
1386
+ flexDirection: 'row',
1387
+ alignItems: 'center',
1388
+ justifyContent: 'space-between',
1389
+ marginBottom: 10,
1390
+ },
1391
+ heroTitle: {
1392
+ fontFamily: AppFonts.interBold,
1393
+ fontSize: 13,
1394
+ color: AppColors.primaryBlack,
1395
+ },
1396
+ heroSubTitle: {
1397
+ fontFamily: AppFonts.interRegular,
1398
+ fontSize: 9.5,
1399
+ color: AppColors.grayTextWeak,
1400
+ marginTop: 1,
1401
+ },
1402
+ scorePill: {
1403
+ paddingHorizontal: 8,
1404
+ paddingVertical: 3,
1405
+ borderRadius: 6,
1406
+ },
1407
+ scorePillText: {
1408
+ fontFamily: AppFonts.interBold,
1409
+ fontSize: 10.5,
1410
+ },
1411
+ primaryFpsRow: {
1412
+ flexDirection: 'row',
1413
+ alignItems: 'center',
1414
+ justifyContent: 'space-between',
1415
+ paddingVertical: 6,
1416
+ },
1417
+ fpsBigNumberBox: {
1418
+ flexDirection: 'row',
1419
+ alignItems: 'baseline',
1420
+ gap: 4,
1421
+ minWidth: 80,
1422
+ },
1423
+ fpsBigNumber: {
1424
+ fontFamily: AppFonts.interBold,
1425
+ fontSize: 42,
1426
+ lineHeight: 48,
1427
+ letterSpacing: -1,
1428
+ minWidth: 54,
1429
+ },
1430
+ fpsUnit: {
1431
+ fontFamily: AppFonts.interBold,
1432
+ fontSize: 14,
1433
+ color: AppColors.grayTextWeak,
1434
+ },
1435
+ fpsStatsGrid: {
1436
+ flexDirection: 'row',
1437
+ alignItems: 'center',
1438
+ gap: 8,
1439
+ },
1440
+ statItem: {
1441
+ alignItems: 'center',
1442
+ minWidth: 50,
1443
+ },
1444
+ statLabel: {
1445
+ fontFamily: AppFonts.interRegular,
1446
+ fontSize: 10,
1447
+ color: AppColors.grayTextWeak,
1448
+ },
1449
+ statValue: {
1450
+ fontFamily: AppFonts.interBold,
1451
+ fontSize: 13,
1452
+ color: AppColors.primaryBlack,
1453
+ marginTop: 2,
1454
+ textAlign: 'center',
1455
+ },
1456
+ graphScrubTooltip: {
1457
+ alignSelf: 'center',
1458
+ backgroundColor: AppColors.primaryBlack,
1459
+ paddingHorizontal: 8,
1460
+ paddingVertical: 3,
1461
+ borderRadius: 6,
1462
+ marginBottom: 4,
1463
+ },
1464
+ graphScrubTooltipText: {
1465
+ fontFamily: AppFonts.interBold,
1466
+ fontSize: 10,
1467
+ color: AppColors.white,
1468
+ },
1469
+ svgGraphContainer: {
1470
+ width: '100%',
1471
+ height: 64,
1472
+ marginTop: 6,
1473
+ position: 'relative',
1474
+ overflow: 'hidden',
1475
+ borderRadius: 8,
1476
+ },
1477
+ svgTouchOverlay: {
1478
+ position: 'absolute',
1479
+ top: 0,
1480
+ left: 0,
1481
+ right: 0,
1482
+ bottom: 0,
1483
+ flexDirection: 'row',
1484
+ },
1485
+ svgTouchCol: {
1486
+ flex: 1,
1487
+ height: '100%',
1488
+ },
1489
+ histogramContainer: {
1490
+ gap: 8,
1491
+ marginTop: 4,
1492
+ },
1493
+ histogramRow: {
1494
+ flexDirection: 'row',
1495
+ alignItems: 'center',
1496
+ gap: 8,
1497
+ },
1498
+ histogramLabelCol: {
1499
+ flexDirection: 'row',
1500
+ alignItems: 'center',
1501
+ gap: 6,
1502
+ width: 140,
1503
+ },
1504
+ histogramLabelText: {
1505
+ fontFamily: AppFonts.interMedium,
1506
+ fontSize: 10,
1507
+ color: AppColors.grayTextStrong,
1508
+ },
1509
+ histogramBarCol: {
1510
+ flex: 1,
1511
+ },
1512
+ histogramTrack: {
1513
+ height: 8,
1514
+ backgroundColor: AppColors.grayBackground,
1515
+ borderRadius: 4,
1516
+ overflow: 'hidden',
1517
+ },
1518
+ histogramBarFill: {
1519
+ height: '100%',
1520
+ borderRadius: 4,
1521
+ },
1522
+ histogramStatsCol: {
1523
+ flexDirection: 'row',
1524
+ alignItems: 'center',
1525
+ gap: 4,
1526
+ minWidth: 52,
1527
+ justifyContent: 'flex-end',
1528
+ },
1529
+ histogramPercentText: {
1530
+ fontFamily: AppFonts.interBold,
1531
+ fontSize: 10.5,
1532
+ },
1533
+ histogramCountText: {
1534
+ fontFamily: AppFonts.interRegular,
1535
+ fontSize: 9.5,
1536
+ color: AppColors.grayTextWeak,
1537
+ },
1538
+ sparklineFooter: {
1539
+ flexDirection: 'row',
1540
+ alignItems: 'center',
1541
+ justifyContent: 'space-between',
1542
+ marginTop: 6,
1543
+ },
1544
+ sparklineFooterText: {
1545
+ fontFamily: AppFonts.interRegular,
1546
+ fontSize: 10,
1547
+ color: AppColors.grayTextWeak,
1548
+ },
1549
+ sparklineFooterTarget: {
1550
+ fontFamily: AppFonts.interMedium,
1551
+ fontSize: 10,
1552
+ color: AppColors.emerald600,
1553
+ },
1554
+ budgetCard: {
1555
+ backgroundColor: AppColors.white,
1556
+ borderRadius: 12,
1557
+ padding: 12,
1558
+ borderWidth: 1,
1559
+ borderColor: AppColors.dividerColor,
1560
+ gap: 8,
1561
+ },
1562
+ budgetHeader: {
1563
+ flexDirection: 'row',
1564
+ alignItems: 'center',
1565
+ justifyContent: 'space-between',
1566
+ },
1567
+ budgetTitle: {
1568
+ fontFamily: AppFonts.interBold,
1569
+ fontSize: 12.5,
1570
+ color: AppColors.primaryBlack,
1571
+ },
1572
+ budgetHeadroom: {
1573
+ fontFamily: AppFonts.interBold,
1574
+ fontSize: 11,
1575
+ color: AppColors.emerald600,
1576
+ },
1577
+ budgetBarContainer: {
1578
+ flexDirection: 'row',
1579
+ height: 8,
1580
+ borderRadius: 4,
1581
+ overflow: 'hidden',
1582
+ backgroundColor: AppColors.grayBackground,
1583
+ gap: 2,
1584
+ },
1585
+ budgetBarSegment: {
1586
+ height: '100%',
1587
+ borderRadius: 2,
1588
+ },
1589
+ budgetLegendGrid: {
1590
+ flexDirection: 'row',
1591
+ flexWrap: 'wrap',
1592
+ gap: 10,
1593
+ marginTop: 2,
1594
+ },
1595
+ legendItem: {
1596
+ flexDirection: 'row',
1597
+ alignItems: 'center',
1598
+ gap: 4,
1599
+ },
1600
+ legendDot: {
1601
+ width: 6,
1602
+ height: 6,
1603
+ borderRadius: 3,
1604
+ },
1605
+ legendText: {
1606
+ fontFamily: AppFonts.interRegular,
1607
+ fontSize: 10,
1608
+ color: AppColors.grayTextStrong,
1609
+ },
1610
+ engineSummaryGrid: {
1611
+ flexDirection: 'row',
1612
+ alignItems: 'center',
1613
+ gap: 8,
1614
+ },
1615
+ engineSummaryCard: {
1616
+ flex: 1,
1617
+ backgroundColor: AppColors.white,
1618
+ borderRadius: 10,
1619
+ padding: 10,
1620
+ borderWidth: 1,
1621
+ borderColor: AppColors.dividerColor,
1622
+ },
1623
+ engineSummaryLabel: {
1624
+ fontFamily: AppFonts.interMedium,
1625
+ fontSize: 10,
1626
+ color: AppColors.grayTextWeak,
1627
+ },
1628
+ engineSummaryValue: {
1629
+ fontFamily: AppFonts.interBold,
1630
+ fontSize: 11.5,
1631
+ color: AppColors.primaryBlack,
1632
+ marginTop: 2,
1633
+ },
1634
+ engineSummarySub: {
1635
+ fontFamily: AppFonts.interRegular,
1636
+ fontSize: 9.5,
1637
+ color: AppColors.grayTextWeak,
1638
+ marginTop: 1,
1639
+ },
1640
+ logsHeaderRow: {
1641
+ flexDirection: 'row',
1642
+ alignItems: 'center',
1643
+ justifyContent: 'space-between',
1644
+ marginTop: 4,
1645
+ },
1646
+ logsSectionHeading: {
1647
+ fontFamily: AppFonts.interBold,
1648
+ fontSize: 13,
1649
+ color: AppColors.primaryBlack,
1650
+ },
1651
+ clearLink: {
1652
+ fontFamily: AppFonts.interBold,
1653
+ fontSize: 11.5,
1654
+ color: AppColors.purple,
1655
+ },
1656
+ eventCard: {
1657
+ backgroundColor: AppColors.white,
1658
+ borderRadius: 10,
1659
+ padding: 10,
1660
+ borderWidth: 1,
1661
+ borderColor: AppColors.dividerColor,
1662
+ gap: 6,
1663
+ },
1664
+ eventTopRow: {
1665
+ flexDirection: 'row',
1666
+ alignItems: 'center',
1667
+ justifyContent: 'space-between',
1668
+ },
1669
+ fpsBadgeGroup: {
1670
+ flexDirection: 'row',
1671
+ alignItems: 'center',
1672
+ gap: 6,
1673
+ flexWrap: 'wrap',
1674
+ },
1675
+ fpsBadge: {
1676
+ paddingHorizontal: 6,
1677
+ paddingVertical: 2,
1678
+ borderRadius: 5,
1679
+ borderWidth: 1,
1680
+ },
1681
+ fpsBadgeText: {
1682
+ fontFamily: AppFonts.interBold,
1683
+ fontSize: 10,
1684
+ },
1685
+ droppedBadge: {
1686
+ paddingHorizontal: 6,
1687
+ paddingVertical: 2,
1688
+ borderRadius: 5,
1689
+ borderWidth: 1,
1690
+ },
1691
+ droppedBadgeWarning: {
1692
+ backgroundColor: AppColors.red100,
1693
+ borderColor: AppColors.errorBorder,
1694
+ },
1695
+ droppedBadgeOptimal: {
1696
+ backgroundColor: AppColors.green100,
1697
+ borderColor: AppColors.greenBorder,
1698
+ },
1699
+ droppedBadgeText: {
1700
+ fontFamily: AppFonts.interBold,
1701
+ fontSize: 9.5,
1702
+ },
1703
+ bottleneckTag: {
1704
+ backgroundColor: AppColors.purpleShade50,
1705
+ paddingHorizontal: 5,
1706
+ paddingVertical: 2,
1707
+ borderRadius: 4,
1708
+ borderWidth: 1,
1709
+ borderColor: AppColors.purpleTint2,
1710
+ },
1711
+ bottleneckTagText: {
1712
+ fontFamily: AppFonts.interMedium,
1713
+ fontSize: 9.5,
1714
+ color: AppColors.purple,
1715
+ },
1716
+ eventMetaRow: {
1717
+ flexDirection: 'row',
1718
+ alignItems: 'center',
1719
+ gap: 6,
1720
+ marginTop: 2,
1721
+ flexWrap: 'wrap',
1722
+ },
1723
+ eventMetaDot: {
1724
+ fontSize: 9,
1725
+ color: AppColors.grayTextWeak,
1726
+ },
1727
+ frameBudgetHint: {
1728
+ fontFamily: AppFonts.interMedium,
1729
+ fontSize: 10,
1730
+ },
1731
+ frameBarTrack: {
1732
+ height: 4,
1733
+ borderRadius: 2,
1734
+ backgroundColor: AppColors.graySurface,
1735
+ overflow: 'hidden',
1736
+ marginVertical: 4,
1737
+ },
1738
+ frameBarFill: {
1739
+ height: '100%',
1740
+ borderRadius: 2,
1741
+ },
1742
+ fpsMetricsGrid: {
1743
+ flexDirection: 'row',
1744
+ alignItems: 'center',
1745
+ backgroundColor: AppColors.white,
1746
+ borderRadius: 6,
1747
+ padding: 6,
1748
+ borderWidth: 1,
1749
+ borderColor: AppColors.dividerColor,
1750
+ justifyContent: 'space-between',
1751
+ gap: 4,
1752
+ },
1753
+ fpsMetricItem: {
1754
+ alignItems: 'center',
1755
+ flex: 1,
1756
+ },
1757
+ fpsMetricLabel: {
1758
+ fontFamily: AppFonts.interRegular,
1759
+ fontSize: 8.5,
1760
+ color: AppColors.grayTextWeak,
1761
+ textAlign: 'center',
1762
+ },
1763
+ fpsMetricVal: {
1764
+ fontFamily: AppFonts.interBold,
1765
+ fontSize: 10.5,
1766
+ color: AppColors.primaryBlack,
1767
+ marginTop: 1,
1768
+ textAlign: 'center',
1769
+ },
1770
+ eventLabel: {
1771
+ fontFamily: AppFonts.interBold,
1772
+ fontSize: 12,
1773
+ color: AppColors.primaryBlack,
1774
+ },
1775
+ eventMeta: {
1776
+ fontFamily: AppFonts.interRegular,
1777
+ fontSize: 10,
1778
+ color: AppColors.grayTextWeak,
1779
+ marginTop: 1,
1780
+ },
1781
+ eventDetail: {
1782
+ fontFamily: AppFonts.interRegular,
1783
+ fontSize: 11,
1784
+ color: AppColors.grayTextStrong,
1785
+ lineHeight: 15,
1786
+ },
1787
+ expandChevron: {
1788
+ fontSize: 10,
1789
+ color: AppColors.grayTextWeak,
1790
+ marginLeft: 2,
1791
+ },
1792
+ expandedDetailBox: {
1793
+ backgroundColor: AppColors.grayBackground,
1794
+ borderRadius: 8,
1795
+ padding: 8,
1796
+ marginTop: 4,
1797
+ gap: 6,
1798
+ borderWidth: 1,
1799
+ borderColor: AppColors.dividerColor,
1800
+ },
1801
+ sourceRow: {
1802
+ flexDirection: 'row',
1803
+ alignItems: 'center',
1804
+ gap: 6,
1805
+ },
1806
+ sourceTag: {
1807
+ fontFamily: AppFonts.interBold,
1808
+ fontSize: 8.5,
1809
+ color: AppColors.purple,
1810
+ backgroundColor: `${AppColors.purple}14`,
1811
+ paddingHorizontal: 4,
1812
+ paddingVertical: 1,
1813
+ borderRadius: 3,
1814
+ },
1815
+ sourceText: {
1816
+ fontFamily: AppFonts.interMedium,
1817
+ fontSize: 10.5,
1818
+ color: AppColors.grayTextStrong,
1819
+ flex: 1,
1820
+ },
1821
+ breakdownGrid: {
1822
+ flexDirection: 'row',
1823
+ alignItems: 'center',
1824
+ backgroundColor: AppColors.white,
1825
+ borderRadius: 6,
1826
+ padding: 6,
1827
+ borderWidth: 1,
1828
+ borderColor: AppColors.dividerColor,
1829
+ justifyContent: 'space-between',
1830
+ },
1831
+ breakdownCol: {
1832
+ alignItems: 'center',
1833
+ flex: 1,
1834
+ },
1835
+ breakdownLabel: {
1836
+ fontFamily: AppFonts.interRegular,
1837
+ fontSize: 9,
1838
+ color: AppColors.grayTextWeak,
1839
+ },
1840
+ breakdownVal: {
1841
+ fontFamily: AppFonts.interBold,
1842
+ fontSize: 11,
1843
+ color: AppColors.primaryBlack,
1844
+ marginTop: 1,
1845
+ },
1846
+ adviceCard: {
1847
+ backgroundColor: AppColors.purpleTintBg,
1848
+ borderRadius: 6,
1849
+ padding: 6,
1850
+ borderWidth: 1,
1851
+ borderColor: AppColors.purpleBorder,
1852
+ },
1853
+ adviceHeading: {
1854
+ fontFamily: AppFonts.interBold,
1855
+ fontSize: 10,
1856
+ color: AppColors.purpleShade700,
1857
+ },
1858
+ adviceBodyText: {
1859
+ fontFamily: AppFonts.interRegular,
1860
+ fontSize: 10.5,
1861
+ color: AppColors.purple,
1862
+ lineHeight: 14,
1863
+ marginTop: 1,
1864
+ },
1865
+ searchHighlight: {
1866
+ backgroundColor: AppColors.yellowHighlight,
1867
+ color: AppColors.amber800,
1868
+ fontFamily: AppFonts.interBold,
1869
+ },
1870
+ emptyCard: {
1871
+ backgroundColor: AppColors.white,
1872
+ borderRadius: 12,
1873
+ padding: 24,
1874
+ alignItems: 'center',
1875
+ justifyContent: 'center',
1876
+ gap: 8,
1877
+ borderWidth: 1,
1878
+ borderColor: AppColors.dividerColor,
1879
+ },
1880
+ emptyTitle: {
1881
+ fontFamily: AppFonts.interBold,
1882
+ fontSize: 14,
1883
+ color: AppColors.primaryBlack,
1884
+ },
1885
+ emptySub: {
1886
+ fontFamily: AppFonts.interRegular,
1887
+ fontSize: 11.5,
1888
+ color: AppColors.grayTextWeak,
1889
+ textAlign: 'center',
1890
+ lineHeight: 16,
1891
+ },
1892
+ });
1893
+
1894
+ export default PerformanceTab;