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,4731 @@
1
+ import React, {useState, useEffect, useMemo, useCallback} from 'react';
2
+ import {
3
+ ActivityIndicator,
4
+ Image,
5
+ Linking,
6
+ Platform,
7
+ Pressable,
8
+ ScrollView,
9
+ Share,
10
+ StyleSheet,
11
+ Text,
12
+ TextInput,
13
+ View,
14
+ } from 'react-native';
15
+ import {useTranslation, t} from '../../i18n';
16
+ import TouchableScale from '../TouchableScale';
17
+ import CopyButton from '../CopyButton';
18
+ import HighlightText from '../HighlightText';
19
+ import SegmentedTabs from '../SegmentedTabs';
20
+ import AnimatedEntrance from '../AnimatedEntrance';
21
+ import EndOfListFooter from '../EndOfListFooter';
22
+ import {AppColors} from '../../styles/AppColors';
23
+ import {AppFonts} from '../../styles/AppFonts';
24
+ import styles from '../../styles';
25
+ import {copyToClipboard} from '../../helpers';
26
+ import {
27
+ PackageIcon,
28
+ SearchIcon,
29
+ ClearIcon,
30
+ CheckIcon,
31
+ CircleAlertIcon,
32
+ LayersIcon,
33
+ TimelineIcon,
34
+ LiveStateIcon,
35
+ StorageIcon,
36
+ MetadataIcon,
37
+ FolderIcon,
38
+ FolderOpenIcon,
39
+ TrashIcon,
40
+ LightbulbIcon,
41
+ SparkleIcon,
42
+ ClockIcon,
43
+ BoltIcon,
44
+ BanIcon,
45
+ RefreshCcwIcon,
46
+ ExternalLinkIcon,
47
+ AppleIcon,
48
+ AndroidIcon,
49
+ DownloadIcon,
50
+ } from '../NetworkIcons';
51
+
52
+ import Svg, {
53
+ Path,
54
+ Rect,
55
+ Circle,
56
+ Ellipse,
57
+ G,
58
+ Defs,
59
+ LinearGradient,
60
+ Stop,
61
+ } from 'react-native-svg';
62
+ import {
63
+ analyzeHostAppBundle,
64
+ getCachedBundleAnalysis,
65
+ getInitialBundleAnalysis,
66
+ getHostScriptURL,
67
+ HostBundleAnalysisResult,
68
+ } from '../../customHooks/bundleAnalyzer';
69
+
70
+ export type BundleSubTab =
71
+ | 'overview'
72
+ | 'production'
73
+ | 'files'
74
+ | 'packages'
75
+ | 'media'
76
+ | 'optimizer';
77
+
78
+ export type FileTypeCategory =
79
+ | 'image'
80
+ | 'typescript'
81
+ | 'javascript'
82
+ | 'font'
83
+ | 'json';
84
+
85
+ export interface BundleFileItem {
86
+ id: string;
87
+ name: string;
88
+ path: string;
89
+ ext: string;
90
+ category: FileTypeCategory;
91
+ sizeKb: number;
92
+ meta: string;
93
+ color: string;
94
+ status?: 'optimal' | 'warning' | 'info';
95
+ advice?: string;
96
+ previewUri?: string;
97
+ isConsumed?: boolean;
98
+ }
99
+
100
+ export interface BundlePackageItem {
101
+ id: string;
102
+ name: string;
103
+ version: string;
104
+ sizeKb: number;
105
+ percentage: number;
106
+ type: 'direct' | 'peer' | 'transitive';
107
+ category: 'core' | 'navigation' | 'network' | 'ui' | 'utils';
108
+ color: string;
109
+ description: string;
110
+ logoUri?: string;
111
+ latestVersion?: string;
112
+ isDeprecated?: boolean;
113
+ deprecationReason?: string;
114
+ lastActive?: string;
115
+ npmUrl?: string;
116
+ }
117
+
118
+ // ─── High-Fidelity Vector Package & NPM Logos ────────────────────────────────
119
+
120
+ const PackageLogoRenderer: React.FC<{
121
+ name: string;
122
+ color?: string;
123
+ size?: number;
124
+ }> = ({name, size = 28}) => {
125
+ const lowerName = name.toLowerCase();
126
+
127
+ // 1. React & React Native (Official Cyan Atom Orbital)
128
+ if (
129
+ lowerName === 'react' ||
130
+ (lowerName.includes('react-native') &&
131
+ !lowerName.includes('svg') &&
132
+ !lowerName.includes('screens') &&
133
+ !lowerName.includes('gradient'))
134
+ ) {
135
+ return (
136
+ <View
137
+ style={[
138
+ bundleStyles.pkgLogoWrap,
139
+ {width: size, height: size, backgroundColor: AppColors.npmDark},
140
+ ]}>
141
+ <Svg
142
+ width={size - 6}
143
+ height={size - 6}
144
+ viewBox="0 0 100 100"
145
+ fill="none">
146
+ <Circle cx="50" cy="50" r="9" fill={AppColors.reactCyan} />
147
+ <Ellipse
148
+ cx="50"
149
+ cy="50"
150
+ rx="42"
151
+ ry="16"
152
+ stroke={AppColors.reactCyan}
153
+ strokeWidth="6"
154
+ />
155
+ <Ellipse
156
+ cx="50"
157
+ cy="50"
158
+ rx="42"
159
+ ry="16"
160
+ stroke={AppColors.reactCyan}
161
+ strokeWidth="6"
162
+ transform="rotate(60 50 50)"
163
+ />
164
+ <Ellipse
165
+ cx="50"
166
+ cy="50"
167
+ rx="42"
168
+ ry="16"
169
+ stroke={AppColors.reactCyan}
170
+ strokeWidth="6"
171
+ transform="rotate(120 50 50)"
172
+ />
173
+ </Svg>
174
+ </View>
175
+ );
176
+ }
177
+
178
+ // 2. React Navigation (Official Violet/Cyan Compass)
179
+ if (lowerName.includes('navigation')) {
180
+ return (
181
+ <View
182
+ style={[
183
+ bundleStyles.pkgLogoWrap,
184
+ {width: size, height: size, backgroundColor: AppColors.hermesPurple},
185
+ ]}>
186
+ <Svg
187
+ width={size - 8}
188
+ height={size - 8}
189
+ viewBox="0 0 100 100"
190
+ fill="none">
191
+ <Circle
192
+ cx="50"
193
+ cy="50"
194
+ r="38"
195
+ stroke={AppColors.white}
196
+ strokeWidth="8"
197
+ />
198
+ <Path d="M50 20 L68 50 L50 80 L32 50 Z" fill={AppColors.reactCyan} />
199
+ <Circle cx="50" cy="50" r="6" fill={AppColors.white} />
200
+ </Svg>
201
+ </View>
202
+ );
203
+ }
204
+
205
+ // 3. React Native SVG (Bezier Path Vector Art)
206
+ if (lowerName.includes('svg')) {
207
+ return (
208
+ <View
209
+ style={[
210
+ bundleStyles.pkgLogoWrap,
211
+ {width: size, height: size, backgroundColor: AppColors.pink400},
212
+ ]}>
213
+ <Svg
214
+ width={size - 8}
215
+ height={size - 8}
216
+ viewBox="0 0 100 100"
217
+ fill="none">
218
+ <Path
219
+ d="M20 75 C35 25, 65 25, 80 75"
220
+ stroke={AppColors.white}
221
+ strokeWidth="10"
222
+ strokeLinecap="round"
223
+ />
224
+ <Circle cx="20" cy="75" r="8" fill={AppColors.white} />
225
+ <Circle cx="80" cy="75" r="8" fill={AppColors.white} />
226
+ <Circle
227
+ cx="50"
228
+ cy="38"
229
+ r="7"
230
+ fill="#FCE7F3"
231
+ stroke={AppColors.white}
232
+ strokeWidth="3"
233
+ />
234
+ </Svg>
235
+ </View>
236
+ );
237
+ }
238
+
239
+ // 4. Axios (Official Indigo & Cyan Network Adapter)
240
+ if (lowerName.includes('axios')) {
241
+ return (
242
+ <View
243
+ style={[
244
+ bundleStyles.pkgLogoWrap,
245
+ {width: size, height: size, backgroundColor: AppColors.expoViolet},
246
+ ]}>
247
+ <Svg
248
+ width={size - 6}
249
+ height={size - 6}
250
+ viewBox="0 0 100 100"
251
+ fill="none">
252
+ <Path
253
+ d="M22 75 L50 20 L78 75 M33 58 L67 58"
254
+ stroke={AppColors.white}
255
+ strokeWidth="12"
256
+ strokeLinecap="round"
257
+ strokeLinejoin="round"
258
+ />
259
+ <Circle cx="50" cy="20" r="7" fill={AppColors.reactCyan} />
260
+ </Svg>
261
+ </View>
262
+ );
263
+ }
264
+
265
+ // 5. React Native Screens (Software Mansion Deep Indigo Stack)
266
+ if (lowerName.includes('screens') || lowerName.includes('software-mansion')) {
267
+ return (
268
+ <View
269
+ style={[
270
+ bundleStyles.pkgLogoWrap,
271
+ {
272
+ width: size,
273
+ height: size,
274
+ backgroundColor: AppColors.reanimatedNavy,
275
+ },
276
+ ]}>
277
+ <Svg
278
+ width={size - 8}
279
+ height={size - 8}
280
+ viewBox="0 0 100 100"
281
+ fill="none">
282
+ <Path d="M50 15 L88 35 L50 55 L12 35 Z" fill={AppColors.sky400} />
283
+ <Path
284
+ d="M12 50 L50 70 L88 50"
285
+ stroke={AppColors.white}
286
+ strokeWidth="8"
287
+ strokeLinecap="round"
288
+ />
289
+ <Path
290
+ d="M12 65 L50 85 L88 65"
291
+ stroke="#93C5FD"
292
+ strokeWidth="8"
293
+ strokeLinecap="round"
294
+ />
295
+ </Svg>
296
+ </View>
297
+ );
298
+ }
299
+
300
+ // 6. Linear Gradient (Vibrant Color Spectrum)
301
+ if (lowerName.includes('gradient')) {
302
+ return (
303
+ <View
304
+ style={[
305
+ bundleStyles.pkgLogoWrap,
306
+ {width: size, height: size, backgroundColor: AppColors.rose600},
307
+ ]}>
308
+ <Svg
309
+ width={size - 4}
310
+ height={size - 4}
311
+ viewBox="0 0 100 100"
312
+ fill="none">
313
+ <Defs>
314
+ <LinearGradient
315
+ id="pkgGrad"
316
+ x1="0"
317
+ y1="0"
318
+ x2="100"
319
+ y2="100"
320
+ gradientUnits="userSpaceOnUse">
321
+ <Stop offset="0" stopColor="#FF0080" />
322
+ <Stop offset="0.5" stopColor="#7928CA" />
323
+ <Stop offset="1" stopColor="#0070F3" />
324
+ </LinearGradient>
325
+ </Defs>
326
+ <Rect width="100" height="100" rx="18" fill="url(#pkgGrad)" />
327
+ </Svg>
328
+ </View>
329
+ );
330
+ }
331
+
332
+ // 7. i18next & Translations (Emerald Globe)
333
+ if (lowerName.includes('i18n')) {
334
+ return (
335
+ <View
336
+ style={[
337
+ bundleStyles.pkgLogoWrap,
338
+ {width: size, height: size, backgroundColor: AppColors.emerald600},
339
+ ]}>
340
+ <Svg
341
+ width={size - 8}
342
+ height={size - 8}
343
+ viewBox="0 0 100 100"
344
+ fill="none">
345
+ <Circle
346
+ cx="50"
347
+ cy="50"
348
+ r="38"
349
+ stroke={AppColors.white}
350
+ strokeWidth="8"
351
+ />
352
+ <Ellipse
353
+ cx="50"
354
+ cy="50"
355
+ rx="18"
356
+ ry="38"
357
+ stroke={AppColors.white}
358
+ strokeWidth="6"
359
+ />
360
+ <Path d="M14 50 L86 50" stroke={AppColors.white} strokeWidth="7" />
361
+ </Svg>
362
+ </View>
363
+ );
364
+ }
365
+
366
+ // 8. Official Red NPM Logo Box (Universal Default NPM Branding)
367
+ return (
368
+ <View
369
+ style={[
370
+ bundleStyles.pkgLogoWrap,
371
+ {width: size, height: size, backgroundColor: AppColors.npmRed},
372
+ ]}>
373
+ <Svg width={size - 4} height={size - 4} viewBox="0 0 100 100" fill="none">
374
+ <Rect width="100" height="100" rx="14" fill={AppColors.npmRed} />
375
+ <Path d="M18 24 H82 V76 H50 V40 H38 V76 H18 Z" fill={AppColors.white} />
376
+ </Svg>
377
+ </View>
378
+ );
379
+ };
380
+
381
+ // ─── Vector Category Icons (font-independent, render everywhere) ────────────
382
+
383
+ export type BundleIconType =
384
+ | 'source'
385
+ | 'deps'
386
+ | 'media'
387
+ | 'overhead'
388
+ | 'image'
389
+ | 'typescript'
390
+ | 'javascript'
391
+ | 'font'
392
+ | 'json'
393
+ | 'folder'
394
+ | 'ban'
395
+ | 'bolt';
396
+
397
+ const BundleCategoryIcon: React.FC<{
398
+ type: BundleIconType;
399
+ size?: number;
400
+ color?: string;
401
+ }> = ({type, size = 16, color = AppColors.purple}) => {
402
+ const stroke = {
403
+ stroke: color,
404
+ strokeWidth: 1.7,
405
+ strokeLinecap: 'round' as const,
406
+ strokeLinejoin: 'round' as const,
407
+ };
408
+ const renderGlyph = () => {
409
+ switch (type) {
410
+ case 'folder':
411
+ return (
412
+ <G>
413
+ <Path
414
+ d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"
415
+ {...stroke}
416
+ />
417
+ </G>
418
+ );
419
+ case 'ban':
420
+ return (
421
+ <G>
422
+ <Circle cx="12" cy="12" r="8.5" {...stroke} />
423
+ <Path d="M5.5 5.5 18.5 18.5" {...stroke} />
424
+ </G>
425
+ );
426
+ case 'bolt':
427
+ return (
428
+ <G>
429
+ <Path d="M13 2 4.5 13.5 H10.5 L9 22 19.5 9.5 H13.5 Z" {...stroke} />
430
+ </G>
431
+ );
432
+ case 'source':
433
+ return (
434
+ <G>
435
+ <Path d="M8.5 7 L4 12 L8.5 17" {...stroke} />
436
+ <Path d="M15.5 7 L20 12 L15.5 17" {...stroke} />
437
+ <Path d="M13.5 4.5 L10.5 19.5" {...stroke} />
438
+ </G>
439
+ );
440
+ case 'deps':
441
+ return (
442
+ <G>
443
+ <Rect x="4" y="4" width="16" height="16" rx="2.5" {...stroke} />
444
+ <Path d="M4 9.5 H20" {...stroke} />
445
+ <Path d="M9.5 9.5 V20" {...stroke} />
446
+ </G>
447
+ );
448
+ case 'media':
449
+ case 'image':
450
+ return (
451
+ <G>
452
+ <Rect x="3.5" y="4.5" width="17" height="15" rx="2" {...stroke} />
453
+ <Circle cx="9" cy="10" r="1.5" {...stroke} />
454
+ <Path
455
+ d="M3.5 17.5 L9.5 12.5 L14 16.5 L17 13.5 L20.5 17.5"
456
+ {...stroke}
457
+ />
458
+ </G>
459
+ );
460
+ case 'overhead':
461
+ return (
462
+ <G>
463
+ <Circle cx="12" cy="12" r="3.2" {...stroke} />
464
+ <Path
465
+ d="M12 2.5 V5.5 M12 18.5 V21.5 M2.5 12 H5.5 M18.5 12 H21.5 M5.3 5.3 L7.4 7.4 M16.6 16.6 L18.7 18.7 M18.7 5.3 L16.6 7.4 M7.4 16.6 L5.3 18.7"
466
+ {...stroke}
467
+ />
468
+ </G>
469
+ );
470
+ case 'typescript':
471
+ return (
472
+ <G>
473
+ <Path d="M6.5 3.5 H14 L18.5 8 V20.5 H6.5 Z" {...stroke} />
474
+ <Path d="M14 3.5 V8 H18.5" {...stroke} />
475
+ <Path d="M9.5 13.5 H15 M12 13.5 V16.5 M9.5 16.5 H13" {...stroke} />
476
+ </G>
477
+ );
478
+ case 'javascript':
479
+ return (
480
+ <G>
481
+ <Path
482
+ d="M9.5 3.5 C7.5 3.5 7.5 6.5 7.5 9 C7.5 11 6.5 12 5 12 C6.5 12 7.5 13 7.5 15 C7.5 17.5 7.5 20.5 9.5 20.5"
483
+ {...stroke}
484
+ />
485
+ <Path
486
+ d="M14.5 3.5 C16.5 3.5 16.5 6.5 16.5 9 C16.5 11 17.5 12 19 12 C17.5 12 16.5 13 16.5 15 C16.5 17.5 16.5 20.5 14.5 20.5"
487
+ {...stroke}
488
+ />
489
+ </G>
490
+ );
491
+ case 'font':
492
+ return (
493
+ <G>
494
+ <Path d="M8 19 L12 5.5 L16 19" {...stroke} />
495
+ <Path d="M9.4 14.5 H14.6" {...stroke} />
496
+ </G>
497
+ );
498
+ case 'json':
499
+ return (
500
+ <G>
501
+ <Path
502
+ d="M9 3.5 C7 3.5 7 6 7 8.5 C7 10.5 6 11.5 5 12 C6 12.5 7 13.5 7 15.5 C7 18 7 20.5 9 20.5"
503
+ {...stroke}
504
+ />
505
+ <Circle cx="12" cy="12" r="1.1" fill={color} />
506
+ <Path
507
+ d="M15 3.5 C17 3.5 17 6 17 8.5 C17 10.5 18 11.5 19 12 C18 12.5 17 13.5 17 15.5 C17 18 17 20.5 15 20.5"
508
+ {...stroke}
509
+ />
510
+ </G>
511
+ );
512
+ default:
513
+ return null;
514
+ }
515
+ };
516
+ return (
517
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
518
+ {renderGlyph()}
519
+ </Svg>
520
+ );
521
+ };
522
+
523
+ // ─── Media File Thumbnail Component ──────────────────────────────────────────
524
+
525
+ const MediaPreviewThumbnail: React.FC<{file: BundleFileItem}> = ({file}) => {
526
+ const [loadError, setLoadError] = useState(false);
527
+
528
+ // 1. If it's an SVG file, render an inline vector thumbnail preview
529
+ if (file.ext === 'SVG' || file.name.toLowerCase().endsWith('.svg')) {
530
+ return (
531
+ <View
532
+ style={[
533
+ bundleStyles.mediaThumbWrap,
534
+ {
535
+ backgroundColor: AppColors.errorCardBg,
536
+ alignItems: 'center',
537
+ justifyContent: 'center',
538
+ borderColor: AppColors.errorBorder,
539
+ },
540
+ ]}>
541
+ <Svg width={22} height={22} viewBox="0 0 100 100" fill="none">
542
+ <Defs>
543
+ <LinearGradient
544
+ id="svgGradThumb"
545
+ x1="0"
546
+ y1="0"
547
+ x2="100"
548
+ y2="100"
549
+ gradientUnits="userSpaceOnUse">
550
+ <Stop offset="0" stopColor={AppColors.rose500} />
551
+ <Stop offset="1" stopColor={AppColors.rose600} />
552
+ </LinearGradient>
553
+ </Defs>
554
+ <Circle cx="50" cy="50" r="44" fill="url(#svgGradThumb)" />
555
+ {/* Vector bezier path pen graphic */}
556
+ <Path
557
+ d="M25 70 C35 30, 65 30, 75 70"
558
+ stroke={AppColors.white}
559
+ strokeWidth="8"
560
+ strokeLinecap="round"
561
+ />
562
+ <Circle cx="25" cy="70" r="7" fill={AppColors.white} />
563
+ <Circle cx="75" cy="70" r="7" fill={AppColors.white} />
564
+ <Circle
565
+ cx="50"
566
+ cy="40"
567
+ r="6"
568
+ fill="#FFE4E6"
569
+ stroke={AppColors.white}
570
+ strokeWidth="2"
571
+ />
572
+ </Svg>
573
+ </View>
574
+ );
575
+ }
576
+
577
+ // 2. Raster Image (PNG, JPG, WebP) with valid preview URI
578
+ if (file.category === 'image' && file.previewUri && !loadError) {
579
+ return (
580
+ <View style={bundleStyles.mediaThumbWrap}>
581
+ <Image
582
+ source={{uri: file.previewUri}}
583
+ style={bundleStyles.mediaThumbImg}
584
+ resizeMode="cover"
585
+ onError={() => setLoadError(true)}
586
+ />
587
+ </View>
588
+ );
589
+ }
590
+
591
+ const categoryMeta =
592
+ CATEGORY_COLORS[file.category] || CATEGORY_COLORS.typescript;
593
+ return (
594
+ <View
595
+ style={[
596
+ bundleStyles.extChip,
597
+ {
598
+ backgroundColor: categoryMeta.bg,
599
+ borderColor: `${categoryMeta.color}40`,
600
+ },
601
+ ]}>
602
+ <Text style={[bundleStyles.extChipText, {color: categoryMeta.color}]}>
603
+ {file.ext}
604
+ </Text>
605
+ </View>
606
+ );
607
+ };
608
+
609
+ // ─── Directory Tree Hierarchy Data Structure & Component ─────────────────────
610
+
611
+ export interface TreeNode {
612
+ id: string;
613
+ name: string;
614
+ fullPath: string;
615
+ isFolder: boolean;
616
+ fileItem?: BundleFileItem;
617
+ sizeKb: number;
618
+ fileCount: number;
619
+ children: TreeNode[];
620
+ }
621
+
622
+ function buildBundleFileTree(files: BundleFileItem[]): TreeNode[] {
623
+ const rootMap = new Map<string, any>();
624
+
625
+ for (const file of files) {
626
+ const cleanPath = file.path.replace(/^\/+/, '');
627
+ const parts = cleanPath.split('/').filter(Boolean);
628
+ if (parts.length === 0) continue;
629
+ let currentLevel = rootMap;
630
+
631
+ for (let i = 0; i < parts.length; i++) {
632
+ const part = parts[i];
633
+ const isFile = i === parts.length - 1;
634
+
635
+ if (!currentLevel.has(part)) {
636
+ currentLevel.set(part, {
637
+ name: part,
638
+ fullPath: parts.slice(0, i + 1).join('/'),
639
+ isFile,
640
+ fileItem: isFile ? file : undefined,
641
+ children: new Map<string, any>(),
642
+ });
643
+ }
644
+
645
+ if (!isFile) {
646
+ currentLevel = currentLevel.get(part).children;
647
+ }
648
+ }
649
+ }
650
+
651
+ function convertMapToNodes(map: Map<string, any>): TreeNode[] {
652
+ const nodes: TreeNode[] = [];
653
+ for (const [, val] of map.entries()) {
654
+ const children = convertMapToNodes(val.children);
655
+ const isFolder = !val.isFile;
656
+ const fileCount = isFolder
657
+ ? children.reduce((acc, c) => acc + (c.isFolder ? c.fileCount : 1), 0)
658
+ : 1;
659
+ const sizeKb = isFolder
660
+ ? children.reduce((acc, c) => acc + c.sizeKb, 0)
661
+ : val.fileItem
662
+ ? val.fileItem.sizeKb
663
+ : 0;
664
+
665
+ nodes.push({
666
+ id: val.fullPath,
667
+ name: val.name,
668
+ fullPath: val.fullPath,
669
+ isFolder,
670
+ fileItem: val.fileItem,
671
+ sizeKb,
672
+ fileCount,
673
+ children,
674
+ });
675
+ }
676
+
677
+ // Sort folders first, then files alphabetically
678
+ return nodes.sort((a, b) => {
679
+ if (a.isFolder && !b.isFolder) return -1;
680
+ if (!a.isFolder && b.isFolder) return 1;
681
+ return a.name.localeCompare(b.name);
682
+ });
683
+ }
684
+
685
+ return convertMapToNodes(rootMap);
686
+ }
687
+
688
+ export const downloadBundleFile = async (
689
+ file: BundleFileItem,
690
+ scriptURL?: string,
691
+ ): Promise<void> => {
692
+ let content = '';
693
+
694
+ // 1. Attempt to fetch real source code from Metro dev server dynamically
695
+ try {
696
+ let devServerOrigin = '';
697
+ const activeUrl = scriptURL || getHostScriptURL();
698
+ if (activeUrl && activeUrl.startsWith('http')) {
699
+ const match = activeUrl.match(/^(https?:\/\/[^/]+)/);
700
+ if (match) {
701
+ devServerOrigin = match[1];
702
+ }
703
+ }
704
+ if (devServerOrigin) {
705
+ const cleanPath = file.path.replace(/^\/+/, '');
706
+ const url = `${devServerOrigin}/${cleanPath}`;
707
+
708
+ const res = await fetch(url);
709
+ if (res.ok) {
710
+ const text = await res.text();
711
+ if (text && text.length > 0) {
712
+ content = text;
713
+ }
714
+ }
715
+ }
716
+ } catch {}
717
+
718
+ // 2. If dev server direct fetch is unavailable, provide structured module descriptor with full path
719
+ if (!content) {
720
+ content = `// ─────────────────────────────────────────────────────────────
721
+ // File: ${file.name}
722
+ // Path: ${file.path}
723
+ // Category: ${file.category.toUpperCase()}
724
+ // Size: ${file.sizeKb} KB
725
+ // Status: ${
726
+ file.status === 'optimal'
727
+ ? 'Active in Dependency Graph'
728
+ : 'Not consumed / Static'
729
+ }
730
+ // Meta: ${file.meta}
731
+ // ─────────────────────────────────────────────────────────────
732
+
733
+ export default {
734
+ id: ${JSON.stringify(file.id)},
735
+ name: ${JSON.stringify(file.name)},
736
+ path: ${JSON.stringify(file.path)},
737
+ category: ${JSON.stringify(file.category)},
738
+ sizeKb: ${file.sizeKb},
739
+ meta: ${JSON.stringify(file.meta)},
740
+ status: ${JSON.stringify(file.status || 'optimal')},
741
+ };
742
+ `;
743
+ }
744
+
745
+ // 3. Trigger native Share / Download sheet
746
+ try {
747
+ await Share.share(
748
+ Platform.OS === 'ios'
749
+ ? {
750
+ title: file.name,
751
+ message: content,
752
+ }
753
+ : {
754
+ title: `Download ${file.name}`,
755
+ message: content,
756
+ },
757
+ {
758
+ dialogTitle: `Download / Save ${file.name}`,
759
+ },
760
+ );
761
+ } catch {}
762
+
763
+ // 4. Also copy directly to clipboard for quick paste in VS Code or external editor
764
+ try {
765
+ copyToClipboard(content, file.name);
766
+ } catch {}
767
+ };
768
+
769
+ const BundleTreeNodeView: React.FC<{
770
+ node: TreeNode;
771
+ level: number;
772
+ search: string;
773
+ totalBundleKb: number;
774
+ expandedFolders: Record<string, boolean>;
775
+ toggleFolder: (path: string) => void;
776
+ isLastChild: boolean;
777
+ onDownload?: (file: BundleFileItem) => void;
778
+ }> = ({
779
+ node,
780
+ level,
781
+ search,
782
+ totalBundleKb,
783
+ expandedFolders,
784
+ toggleFolder,
785
+ isLastChild,
786
+ onDownload,
787
+ }) => {
788
+ const isExpanded = search ? true : !!expandedFolders[node.fullPath];
789
+ const isCollapsed = !isExpanded;
790
+ const indentLeft = level * 16 + 10;
791
+
792
+ if (node.isFolder) {
793
+ return (
794
+ <View style={bundleStyles.treeFolderBlock}>
795
+ <TouchableScale
796
+ accessible={true}
797
+ accessibilityRole="button"
798
+ accessibilityLabel={`Folder ${node.name}, ${node.fileCount} ${
799
+ node.fileCount === 1 ? 'file' : 'files'
800
+ }, ${
801
+ node.sizeKb >= 1024
802
+ ? `${(node.sizeKb / 1024).toFixed(2)} MB`
803
+ : `${node.sizeKb} KB`
804
+ }`}
805
+ accessibilityState={{expanded: isExpanded}}
806
+ accessibilityHint="Double tap to expand or collapse folder"
807
+ onPress={() => toggleFolder(node.fullPath)}
808
+ style={[bundleStyles.treeFolderRow, {paddingLeft: indentLeft}]}>
809
+ <View style={bundleStyles.treeRowLeft}>
810
+ <Text style={bundleStyles.treeChevron}>
811
+ {isCollapsed ? '▸' : '▾'}
812
+ </Text>
813
+ {isCollapsed ? (
814
+ <FolderIcon color={AppColors.amber500} size={15} />
815
+ ) : (
816
+ <FolderOpenIcon color={AppColors.amber500} size={15} />
817
+ )}
818
+ <HighlightText
819
+ text={node.name + '/'}
820
+ search={search}
821
+ style={bundleStyles.treeFolderName}
822
+ highlightStyle={bundleStyles.searchHighlight}
823
+ />
824
+ <View style={bundleStyles.treeCountBadge}>
825
+ <Text style={bundleStyles.treeCountBadgeText}>
826
+ {node.fileCount}{' '}
827
+ {node.fileCount === 1 ? t('bundle.file') : t('bundle.files')}
828
+ </Text>
829
+ </View>
830
+ </View>
831
+
832
+ <View style={bundleStyles.treeRowRight}>
833
+ <Text style={bundleStyles.treeFolderSize}>
834
+ {node.sizeKb >= 1024
835
+ ? t('bundle.fileSizeMb', {
836
+ size: (node.sizeKb / 1024).toFixed(2),
837
+ })
838
+ : t('bundle.fileSizeKb', {size: node.sizeKb})}
839
+ </Text>
840
+ <CopyButton
841
+ value={() => ({
842
+ folder: node.fullPath,
843
+ sizeKb: node.sizeKb,
844
+ fileCount: node.fileCount,
845
+ })}
846
+ label={t('bundle.copyFileInfo', {name: node.name})}
847
+ />
848
+ </View>
849
+ </TouchableScale>
850
+
851
+ {!isCollapsed && (
852
+ <View style={bundleStyles.treeChildrenContainer}>
853
+ {node.children.map((child, cIdx) => (
854
+ <BundleTreeNodeView
855
+ key={child.id}
856
+ node={child}
857
+ level={level + 1}
858
+ search={search}
859
+ totalBundleKb={totalBundleKb}
860
+ expandedFolders={expandedFolders}
861
+ toggleFolder={toggleFolder}
862
+ isLastChild={cIdx === node.children.length - 1}
863
+ onDownload={onDownload}
864
+ />
865
+ ))}
866
+ </View>
867
+ )}
868
+ </View>
869
+ );
870
+ }
871
+
872
+ // File item inside tree
873
+ const file = node.fileItem;
874
+ if (!file) return null;
875
+
876
+ const pctOfTotal =
877
+ totalBundleKb > 0 ? ((file.sizeKb / totalBundleKb) * 100).toFixed(1) : '0';
878
+ const isUnused = file.isConsumed === false;
879
+
880
+ return (
881
+ <View
882
+ style={[
883
+ bundleStyles.treeFileRow,
884
+ {paddingLeft: indentLeft},
885
+ isUnused && bundleStyles.treeFileRowUnused,
886
+ ]}>
887
+ <View style={bundleStyles.treeRowLeft}>
888
+ <View style={bundleStyles.treeBranchGuide}>
889
+ <Text style={bundleStyles.treeBranchGuideText}>
890
+ {isLastChild ? '└' : '├'}
891
+ </Text>
892
+ </View>
893
+
894
+ <MediaPreviewThumbnail file={file} />
895
+
896
+ <HighlightText
897
+ text={file.name}
898
+ search={search}
899
+ style={bundleStyles.treeFileName}
900
+ highlightStyle={bundleStyles.searchHighlight}
901
+ />
902
+
903
+ {isUnused && (
904
+ <View style={bundleStyles.treeUnusedPill}>
905
+ <Text style={bundleStyles.treeUnusedPillText}>
906
+ {t('bundle.notConsumed')}
907
+ </Text>
908
+ </View>
909
+ )}
910
+ </View>
911
+
912
+ <View style={bundleStyles.treeRowRight}>
913
+ <View style={bundleStyles.treeFileSizeGroup}>
914
+ <Text
915
+ style={[
916
+ bundleStyles.treeFileSizeText,
917
+ isUnused && {color: AppColors.red600},
918
+ ]}>
919
+ {file.sizeKb >= 1024
920
+ ? t('bundle.fileSizeMb', {size: (file.sizeKb / 1024).toFixed(2)})
921
+ : t('bundle.fileSizeKb', {size: file.sizeKb})}
922
+ </Text>
923
+ <Text style={bundleStyles.treeFilePctText}>{pctOfTotal}%</Text>
924
+ </View>
925
+
926
+ <View style={{flexDirection: 'row', alignItems: 'center', gap: 5}}>
927
+ {onDownload && (
928
+ <TouchableScale
929
+ accessible={true}
930
+ accessibilityRole="button"
931
+ accessibilityLabel={`Download source code for ${file.name}`}
932
+ accessibilityHint="Fetches module source code from Metro dev server"
933
+ onPress={() => onDownload(file)}
934
+ style={bundleStyles.treeActionBtn}
935
+ hitSlop={8}>
936
+ <DownloadIcon size={13} color={AppColors.sky500} />
937
+ </TouchableScale>
938
+ )}
939
+
940
+ <CopyButton value={() => file} label={t('bundle.fileDetailsJson')} />
941
+ </View>
942
+ </View>
943
+ </View>
944
+ );
945
+ };
946
+
947
+ const CATEGORY_TABS = [
948
+ {key: 'ALL', labelKey: 'bundle.catAll', iconType: 'folder' as const},
949
+ {key: 'UNUSED', labelKey: 'bundle.catUnused', iconType: 'ban' as const},
950
+ {key: 'CONSUMED', labelKey: 'bundle.catConsumed', iconType: 'bolt' as const},
951
+ {key: 'image', labelKey: 'bundle.catImages', iconType: 'image' as const},
952
+ {
953
+ key: 'typescript',
954
+ labelKey: 'bundle.catTypescript',
955
+ iconType: 'typescript' as const,
956
+ },
957
+ {
958
+ key: 'javascript',
959
+ labelKey: 'bundle.catJavascript',
960
+ iconType: 'javascript' as const,
961
+ },
962
+ {key: 'font', labelKey: 'bundle.catFonts', iconType: 'font' as const},
963
+ {key: 'json', labelKey: 'bundle.catJson', iconType: 'json' as const},
964
+ ];
965
+
966
+ const CATEGORY_COLORS: Record<
967
+ FileTypeCategory,
968
+ {label: string; color: string; bg: string}
969
+ > = {
970
+ image: {
971
+ label: 'Images & Media',
972
+ color: AppColors.pink500,
973
+ bg: AppColors.pink100,
974
+ },
975
+ typescript: {
976
+ label: 'TypeScript',
977
+ color: AppColors.sky500,
978
+ bg: AppColors.sky100,
979
+ },
980
+ javascript: {
981
+ label: 'JavaScript',
982
+ color: AppColors.indigo500,
983
+ bg: AppColors.indigo50,
984
+ },
985
+ font: {label: 'Fonts', color: AppColors.purple500, bg: AppColors.purple100},
986
+ json: {
987
+ label: 'JSON / Data',
988
+ color: AppColors.emerald500,
989
+ bg: AppColors.emerald100,
990
+ },
991
+ };
992
+
993
+ const BundleTab = React.memo(() => {
994
+ const {t} = useTranslation();
995
+ const [activeSubTab, setActiveSubTab] = useState<BundleSubTab>('overview');
996
+ const [prodPlatform, setProdPlatform] = useState<
997
+ 'ios' | 'androidAab' | 'androidApk'
998
+ >('ios');
999
+ const [search, setSearch] = useState('');
1000
+ const [activeCategory, setActiveCategory] = useState<string>('ALL');
1001
+
1002
+ // ─── Live Host App Bundle Analysis (fetched from Metro / runtime) ─────────
1003
+ const [analysis, setAnalysis] = useState<HostBundleAnalysisResult>(
1004
+ () => getCachedBundleAnalysis() || getInitialBundleAnalysis(),
1005
+ );
1006
+ const [isAnalyzing, setIsAnalyzing] = useState<boolean>(false);
1007
+
1008
+ const refreshAnalysis = useCallback(() => {
1009
+ setIsAnalyzing(true);
1010
+ analyzeHostAppBundle(true)
1011
+ .then(result => {
1012
+ setAnalysis(result);
1013
+ setIsAnalyzing(false);
1014
+ })
1015
+ .catch(() => {
1016
+ setIsAnalyzing(false);
1017
+ });
1018
+ }, []);
1019
+
1020
+ useEffect(() => {
1021
+ let mounted = true;
1022
+ analyzeHostAppBundle()
1023
+ .then(result => {
1024
+ if (mounted) {
1025
+ setAnalysis(result);
1026
+ setIsAnalyzing(false);
1027
+ }
1028
+ })
1029
+ .catch(() => {
1030
+ if (mounted) {
1031
+ setIsAnalyzing(false);
1032
+ }
1033
+ });
1034
+ return () => {
1035
+ mounted = false;
1036
+ };
1037
+ }, []);
1038
+
1039
+ // Check if Hermes is running
1040
+ const isHermes = Boolean((global as any).HermesInternal);
1041
+
1042
+ // Dynamic files & packages derived from the analyzed host bundle
1043
+ const bundleFiles: BundleFileItem[] = useMemo(
1044
+ () => (analysis ? analysis.files : []),
1045
+ [analysis],
1046
+ );
1047
+ const bundlePackages: BundlePackageItem[] = useMemo(
1048
+ () => (analysis ? analysis.packages : []),
1049
+ [analysis],
1050
+ );
1051
+
1052
+ // Comprehensive bundle summary (full Metro bundle calculation e.g. 6.0 MB)
1053
+ const summary = useMemo(() => {
1054
+ const totalKb =
1055
+ analysis?.totalDevKb ||
1056
+ (analysis?.totalDevBytes
1057
+ ? Math.round(analysis.totalDevBytes / 1024)
1058
+ : 6144);
1059
+ const totalMb = analysis?.totalDevMb
1060
+ ? analysis.totalDevMb.toFixed(2)
1061
+ : (totalKb / 1024).toFixed(2);
1062
+
1063
+ const nodeModulesKb =
1064
+ analysis?.splitUp?.nodeModules?.kb || Math.round(totalKb * 0.52);
1065
+ const nodeModulesPct = analysis?.splitUp?.nodeModules?.pct || 52.0;
1066
+
1067
+ const metroKb =
1068
+ analysis?.splitUp?.metroDevOverhead?.kb || Math.round(totalKb * 0.21);
1069
+ const metroPct = analysis?.splitUp?.metroDevOverhead?.pct || 21.0;
1070
+
1071
+ const appSourceKb =
1072
+ analysis?.splitUp?.appSource?.kb || Math.round(totalKb * 0.15);
1073
+ const appSourcePct = analysis?.splitUp?.appSource?.pct || 15.0;
1074
+
1075
+ const assetsMediaKb =
1076
+ analysis?.splitUp?.assetsMedia?.kb || Math.round(totalKb * 0.12);
1077
+ const assetsMediaPct = analysis?.splitUp?.assetsMedia?.pct || 12.0;
1078
+
1079
+ const categories = [
1080
+ {
1081
+ key: 'node_modules',
1082
+ iconType: 'javascript' as const,
1083
+ title: 'NPM Dependencies',
1084
+ targetTab: 'packages' as const,
1085
+ count: analysis?.packageCount || bundlePackages.length || 53,
1086
+ sizeKb: nodeModulesKb,
1087
+ sizeMb: (nodeModulesKb / 1024).toFixed(2),
1088
+ pct: nodeModulesPct,
1089
+ color: AppColors.indigo500,
1090
+ desc: `${
1091
+ analysis?.packageCount || bundlePackages.length || 53
1092
+ } node_modules packages (React Native, React, navigation, axios)`,
1093
+ },
1094
+ {
1095
+ key: 'metro_runtime',
1096
+ iconType: 'bolt' as const,
1097
+ title: 'Metro Dev Runtime',
1098
+ targetTab: 'optimizer' as const,
1099
+ count: 1,
1100
+ sizeKb: metroKb,
1101
+ sizeMb: (metroKb / 1024).toFixed(2),
1102
+ pct: metroPct,
1103
+ color: AppColors.amber500,
1104
+ desc: 'Module loader, polyfills, HMR client, and prelude runtime',
1105
+ },
1106
+ {
1107
+ key: 'app_source',
1108
+ iconType: 'typescript' as const,
1109
+ title: 'App Source Code (TSX/JS)',
1110
+ targetTab: 'files' as const,
1111
+ count:
1112
+ bundleFiles.filter(
1113
+ f => f.category === 'typescript' || f.category === 'javascript',
1114
+ ).length || 7,
1115
+ sizeKb: appSourceKb,
1116
+ sizeMb: (appSourceKb / 1024).toFixed(2),
1117
+ pct: appSourcePct,
1118
+ color: AppColors.sky500,
1119
+ desc: 'Screens, components, navigation, Redux store, and hooks',
1120
+ },
1121
+ {
1122
+ key: 'assets_media',
1123
+ iconType: 'image' as const,
1124
+ title: 'Images & Static Media',
1125
+ targetTab: 'media' as const,
1126
+ count:
1127
+ bundleFiles.filter(
1128
+ f =>
1129
+ f.category === 'image' ||
1130
+ f.category === 'font' ||
1131
+ f.category === 'json',
1132
+ ).length || 3,
1133
+ sizeKb: assetsMediaKb,
1134
+ sizeMb: (assetsMediaKb / 1024).toFixed(2),
1135
+ pct: assetsMediaPct,
1136
+ color: AppColors.pink500,
1137
+ desc: 'Bundled PNG/JPG/SVG drawables, custom fonts, and JSON assets',
1138
+ },
1139
+ ];
1140
+
1141
+ return {
1142
+ totalKb,
1143
+ totalMb,
1144
+ totalCount: (analysis?.packageCount || 53) + bundleFiles.length + 1,
1145
+ categories,
1146
+ nodeModulesKb,
1147
+ nodeModulesPct,
1148
+ metroKb,
1149
+ metroPct,
1150
+ appSourceKb,
1151
+ appSourcePct,
1152
+ assetsMediaKb,
1153
+ assetsMediaPct,
1154
+ images: {
1155
+ count: bundleFiles.filter(f => f.category === 'image').length,
1156
+ kb: assetsMediaKb,
1157
+ pct: assetsMediaPct,
1158
+ },
1159
+ ts: {
1160
+ count: bundleFiles.filter(f => f.category === 'typescript').length,
1161
+ kb: appSourceKb,
1162
+ pct: appSourcePct,
1163
+ },
1164
+ js: {
1165
+ count: (analysis?.packageCount || 53) + 1,
1166
+ kb: nodeModulesKb + metroKb,
1167
+ pct: Number((nodeModulesPct + metroPct).toFixed(1)),
1168
+ },
1169
+ fonts: {
1170
+ count: bundleFiles.filter(f => f.category === 'font').length,
1171
+ kb: 0,
1172
+ pct: 0,
1173
+ },
1174
+ json: {
1175
+ count: bundleFiles.filter(f => f.category === 'json').length,
1176
+ kb: Math.round(assetsMediaKb * 0.1),
1177
+ pct: Number((assetsMediaPct * 0.1).toFixed(1)),
1178
+ },
1179
+ };
1180
+ }, [analysis, bundleFiles, bundlePackages]);
1181
+
1182
+ // Filtered files
1183
+ const filteredFiles = useMemo(() => {
1184
+ return bundleFiles.filter(file => {
1185
+ const matchSearch =
1186
+ !search ||
1187
+ file.name.toLowerCase().includes(search.toLowerCase()) ||
1188
+ file.path.toLowerCase().includes(search.toLowerCase()) ||
1189
+ file.ext.toLowerCase().includes(search.toLowerCase()) ||
1190
+ file.meta.toLowerCase().includes(search.toLowerCase());
1191
+ const matchCategory =
1192
+ activeCategory === 'ALL'
1193
+ ? true
1194
+ : activeCategory === 'UNUSED'
1195
+ ? file.isConsumed === false
1196
+ : activeCategory === 'CONSUMED'
1197
+ ? file.isConsumed !== false
1198
+ : file.category === activeCategory;
1199
+ return matchSearch && matchCategory;
1200
+ });
1201
+ }, [search, activeCategory, bundleFiles]);
1202
+
1203
+ const filteredTotalKb = useMemo(() => {
1204
+ return filteredFiles.reduce((acc, f) => acc + f.sizeKb, 0);
1205
+ }, [filteredFiles]);
1206
+
1207
+ // Tree View State & Node Generation (Top folders expanded by default)
1208
+ const [filesViewMode, setFilesViewMode] = useState<'tree' | 'list'>('tree');
1209
+ const [expandedFolders, setExpandedFolders] = useState<
1210
+ Record<string, boolean>
1211
+ >({});
1212
+
1213
+ const fileTreeNodes = useMemo(() => {
1214
+ return buildBundleFileTree(filteredFiles);
1215
+ }, [filteredFiles]);
1216
+
1217
+ useEffect(() => {
1218
+ if (fileTreeNodes.length > 0 && Object.keys(expandedFolders).length === 0) {
1219
+ const initial: Record<string, boolean> = {};
1220
+ fileTreeNodes.forEach(node => {
1221
+ if (node.isFolder) {
1222
+ initial[node.fullPath] = true;
1223
+ node.children.forEach(child => {
1224
+ if (child.isFolder) {
1225
+ initial[child.fullPath] = true;
1226
+ }
1227
+ });
1228
+ }
1229
+ });
1230
+ setExpandedFolders(initial);
1231
+ }
1232
+ }, [fileTreeNodes]);
1233
+
1234
+ const toggleFolder = useCallback((path: string) => {
1235
+ setExpandedFolders(prev => ({
1236
+ ...prev,
1237
+ [path]: !prev[path],
1238
+ }));
1239
+ }, []);
1240
+
1241
+ const expandAllFolders = useCallback(() => {
1242
+ const all: Record<string, boolean> = {};
1243
+ function collect(nodes: TreeNode[]) {
1244
+ for (const n of nodes) {
1245
+ if (n.isFolder) {
1246
+ all[n.fullPath] = true;
1247
+ collect(n.children);
1248
+ }
1249
+ }
1250
+ }
1251
+ collect(fileTreeNodes);
1252
+ setExpandedFolders(all);
1253
+ }, [fileTreeNodes]);
1254
+
1255
+ const collapseAllFolders = useCallback(() => {
1256
+ setExpandedFolders({});
1257
+ }, []);
1258
+
1259
+ // Filtered Packages
1260
+ const filteredPackages = useMemo(() => {
1261
+ if (!search) return bundlePackages;
1262
+ const q = search.toLowerCase();
1263
+ return bundlePackages.filter(
1264
+ pkg =>
1265
+ pkg.name.toLowerCase().includes(q) ||
1266
+ pkg.description.toLowerCase().includes(q) ||
1267
+ pkg.category.toLowerCase().includes(q),
1268
+ );
1269
+ }, [search, bundlePackages]);
1270
+
1271
+ const packagesTotalKb = useMemo(() => {
1272
+ return filteredPackages.reduce((acc, p) => acc + p.sizeKb, 0);
1273
+ }, [filteredPackages]);
1274
+
1275
+ // Media files (Images + Fonts)
1276
+ const mediaFiles = useMemo(() => {
1277
+ return bundleFiles.filter(
1278
+ f => f.category === 'image' || f.category === 'font',
1279
+ );
1280
+ }, [bundleFiles]);
1281
+
1282
+ const mediaTotalKb = useMemo(() => {
1283
+ return mediaFiles.reduce((acc, m) => acc + m.sizeKb, 0);
1284
+ }, [mediaFiles]);
1285
+
1286
+ const subTabs = [
1287
+ {
1288
+ key: 'overview',
1289
+ label: t('bundle.tabOverview'),
1290
+ icon: (isActive: boolean) => (
1291
+ <LiveStateIcon
1292
+ color={isActive ? AppColors.white : AppColors.purple}
1293
+ size={12}
1294
+ />
1295
+ ),
1296
+ },
1297
+ {
1298
+ key: 'production',
1299
+ label: t('bundle.tabProduction', {count: 3}),
1300
+ icon: (isActive: boolean) => (
1301
+ <TimelineIcon
1302
+ color={isActive ? AppColors.white : AppColors.purple}
1303
+ size={12}
1304
+ />
1305
+ ),
1306
+ },
1307
+ {
1308
+ key: 'files',
1309
+ label: t('bundle.tabFiles', {count: bundleFiles.length}),
1310
+ icon: (isActive: boolean) => (
1311
+ <LayersIcon
1312
+ color={isActive ? AppColors.white : AppColors.purple}
1313
+ size={12}
1314
+ />
1315
+ ),
1316
+ },
1317
+ {
1318
+ key: 'packages',
1319
+ label: t('bundle.tabPackages', {count: bundlePackages.length}),
1320
+ icon: (isActive: boolean) => (
1321
+ <PackageIcon
1322
+ color={isActive ? AppColors.white : AppColors.purple}
1323
+ size={12}
1324
+ />
1325
+ ),
1326
+ },
1327
+ {
1328
+ key: 'media',
1329
+ label: t('bundle.tabMedia', {count: mediaFiles.length}),
1330
+ icon: (isActive: boolean) => (
1331
+ <StorageIcon
1332
+ color={isActive ? AppColors.white : AppColors.purple}
1333
+ size={12}
1334
+ />
1335
+ ),
1336
+ },
1337
+ {
1338
+ key: 'optimizer',
1339
+ label: t('bundle.tabOptimizer'),
1340
+ icon: (isActive: boolean) => (
1341
+ <MetadataIcon
1342
+ color={isActive ? AppColors.white : AppColors.purple}
1343
+ size={12}
1344
+ />
1345
+ ),
1346
+ },
1347
+ ];
1348
+
1349
+ return (
1350
+ <View style={{flex: 1, backgroundColor: AppColors.grayBackground}}>
1351
+ {/* ─── Top Sub-Tabs Navigation Bar (Scrollable Pill Bar + Dynamic Refresh) ─── */}
1352
+ <View style={bundleStyles.topTabWrap}>
1353
+ <ScrollView
1354
+ horizontal
1355
+ showsHorizontalScrollIndicator={false}
1356
+ style={{flex: 1}}
1357
+ contentContainerStyle={bundleStyles.subTabsScroll}>
1358
+ {subTabs.map(tab => {
1359
+ const isActive = activeSubTab === tab.key;
1360
+ return (
1361
+ <TouchableScale
1362
+ key={tab.key}
1363
+ accessible={true}
1364
+ accessibilityRole="tab"
1365
+ accessibilityLabel={`${tab.label} tab`}
1366
+ accessibilityState={{selected: isActive}}
1367
+ accessibilityHint={`Switches to ${tab.label} tab`}
1368
+ onPress={() => setActiveSubTab(tab.key as BundleSubTab)}
1369
+ style={[
1370
+ bundleStyles.subTabPill,
1371
+ isActive && bundleStyles.subTabPillActive,
1372
+ ]}>
1373
+ {tab.icon(isActive)}
1374
+ <Text
1375
+ style={[
1376
+ bundleStyles.subTabPillText,
1377
+ isActive && bundleStyles.subTabPillTextActive,
1378
+ ]}>
1379
+ {tab.label}
1380
+ </Text>
1381
+ </TouchableScale>
1382
+ );
1383
+ })}
1384
+ </ScrollView>
1385
+ <TouchableScale
1386
+ accessible={true}
1387
+ accessibilityRole="button"
1388
+ accessibilityLabel={
1389
+ isAnalyzing ? 'Analyzing Metro bundle' : 'Reload bundle analysis'
1390
+ }
1391
+ accessibilityHint="Re-probes Metro dev server and refreshes bundle statistics"
1392
+ onPress={refreshAnalysis}
1393
+ style={bundleStyles.reloadButton}
1394
+ hitSlop={{top: 8, bottom: 8, left: 8, right: 8}}>
1395
+ {isAnalyzing ? (
1396
+ <ActivityIndicator size="small" color={AppColors.brandPurple} />
1397
+ ) : (
1398
+ <RefreshCcwIcon color={AppColors.brandPurple} size={14} />
1399
+ )}
1400
+ </TouchableScale>
1401
+ </View>
1402
+
1403
+ {/* ══════════════════════════════════════════════════════════════════════ */}
1404
+ {/* ── 1. TAB: OVERVIEW & TREEMAP ───────────────────────────────────────── */}
1405
+ {/* ══════════════════════════════════════════════════════════════════════ */}
1406
+ {activeSubTab === 'overview' && (
1407
+ <ScrollView
1408
+ style={{flex: 1}}
1409
+ contentContainerStyle={bundleStyles.contentContainer}
1410
+ keyboardShouldPersistTaps="handled">
1411
+ {/* Hero Overview Card */}
1412
+ <View style={bundleStyles.heroCard}>
1413
+ <View style={bundleStyles.heroTopRow}>
1414
+ <View style={bundleStyles.heroIconWrap}>
1415
+ <PackageIcon color={AppColors.brandPurple} size={24} />
1416
+ </View>
1417
+ <View style={{flex: 1}}>
1418
+ <Text style={bundleStyles.heroTitle}>
1419
+ {t('bundle.heroTitle')}
1420
+ </Text>
1421
+ {analysis?.isLive && analysis.scriptURL ? (
1422
+ <View style={{marginTop: 4, gap: 4}}>
1423
+ <Text style={bundleStyles.heroSubtitle}>
1424
+ {t('bundle.heroSubtitle')}
1425
+ </Text>
1426
+ <Pressable
1427
+ accessible={true}
1428
+ accessibilityRole="link"
1429
+ accessibilityLabel={`Live Metro bundle URL: ${analysis.scriptURL}`}
1430
+ accessibilityHint="Opens Metro bundle script in browser"
1431
+ style={{
1432
+ backgroundColor: `${AppColors.skyBlue}10`,
1433
+ borderColor: `${AppColors.skyBlue}30`,
1434
+ borderWidth: 1,
1435
+ borderRadius: 8,
1436
+ padding: 8,
1437
+ marginTop: 2,
1438
+ }}
1439
+ onPress={() => Linking.openURL(analysis.scriptURL)}>
1440
+ <View
1441
+ style={{
1442
+ flexDirection: 'row',
1443
+ alignItems: 'center',
1444
+ justifyContent: 'space-between',
1445
+ marginBottom: 3,
1446
+ }}>
1447
+ <View
1448
+ style={{
1449
+ flexDirection: 'row',
1450
+ alignItems: 'center',
1451
+ gap: 4,
1452
+ }}>
1453
+ <ExternalLinkIcon
1454
+ color={AppColors.skyBlue}
1455
+ size={11}
1456
+ />
1457
+ <Text
1458
+ style={{
1459
+ fontFamily: AppFonts.interBold,
1460
+ fontSize: 9.5,
1461
+ color: AppColors.skyBlue,
1462
+ letterSpacing: 0.3,
1463
+ textTransform: 'uppercase',
1464
+ }}>
1465
+ Live Metro URL ↗
1466
+ </Text>
1467
+ </View>
1468
+ <Text
1469
+ style={{
1470
+ fontFamily: AppFonts.interRegular,
1471
+ fontSize: 9.5,
1472
+ color: AppColors.grayTextWeak,
1473
+ }}>
1474
+ {analysis.scriptURL.length} chars
1475
+ </Text>
1476
+ </View>
1477
+ <Text
1478
+ selectable={true}
1479
+ numberOfLines={1}
1480
+ ellipsizeMode="tail"
1481
+ style={{
1482
+ fontFamily: AppFonts.interMedium,
1483
+ fontSize: 11,
1484
+ color: AppColors.skyBlue,
1485
+ textDecorationLine: 'underline',
1486
+ lineHeight: 16,
1487
+ }}>
1488
+ {analysis.scriptURL}
1489
+ </Text>
1490
+ </Pressable>
1491
+ </View>
1492
+ ) : (
1493
+ <Text style={bundleStyles.heroSubtitle}>
1494
+ {t('bundle.heroSubtitle')}
1495
+ </Text>
1496
+ )}
1497
+ </View>
1498
+ <CopyButton
1499
+ value={() => ({...summary, bundle: analysis})}
1500
+ label={t('bundle.bundleOverviewJson')}
1501
+ />
1502
+ </View>
1503
+
1504
+ {/* 4-Stat Metric Grid */}
1505
+ <View style={bundleStyles.metricsGrid}>
1506
+ <View style={bundleStyles.metricBox}>
1507
+ <Text style={bundleStyles.metricLabel}>
1508
+ {t('bundle.devBundleSize')}
1509
+ </Text>
1510
+ <Text style={bundleStyles.metricValue}>
1511
+ {t('bundle.mbValue', {
1512
+ size: analysis ? analysis.totalDevMb : summary.totalMb,
1513
+ })}
1514
+ </Text>
1515
+ <Text style={bundleStyles.metricHint}>
1516
+ {analysis
1517
+ ? t('bundle.devBundleHint', {
1518
+ kb: analysis.totalDevKb,
1519
+ modules: analysis.moduleCount,
1520
+ })
1521
+ : t('bundle.trackedFilesHint', {count: bundleFiles.length})}
1522
+ </Text>
1523
+ </View>
1524
+
1525
+ <View style={bundleStyles.metricBox}>
1526
+ <Text style={bundleStyles.metricLabel}>
1527
+ {t('bundle.imagesMedia')}
1528
+ </Text>
1529
+ <Text
1530
+ style={[
1531
+ bundleStyles.metricValue,
1532
+ {color: AppColors.pink500},
1533
+ ]}>
1534
+ {t('bundle.fileSizeKb', {size: summary.images.kb})}
1535
+ </Text>
1536
+ <Text style={bundleStyles.metricHint}>
1537
+ {t('bundle.pctOfTrackedAssets', {pct: summary.images.pct})}
1538
+ </Text>
1539
+ </View>
1540
+
1541
+ <View style={bundleStyles.metricBox}>
1542
+ <Text style={bundleStyles.metricLabel}>
1543
+ {t('bundle.tsJsSource')}
1544
+ </Text>
1545
+ <Text
1546
+ style={[bundleStyles.metricValue, {color: AppColors.sky500}]}>
1547
+ {t('bundle.fileSizeKb', {
1548
+ size: summary.ts.kb + summary.js.kb,
1549
+ })}
1550
+ </Text>
1551
+ <Text style={bundleStyles.metricHint}>
1552
+ {t('bundle.pctOfTrackedAssets', {
1553
+ pct: (summary.ts.pct + summary.js.pct).toFixed(1),
1554
+ })}
1555
+ </Text>
1556
+ </View>
1557
+
1558
+ <View style={bundleStyles.metricBox}>
1559
+ <Text style={bundleStyles.metricLabel}>
1560
+ {t('bundle.jsEngine')}
1561
+ </Text>
1562
+ <View
1563
+ style={{
1564
+ flexDirection: 'row',
1565
+ alignItems: 'center',
1566
+ gap: 5,
1567
+ marginTop: 2,
1568
+ }}>
1569
+ <View
1570
+ style={[
1571
+ bundleStyles.statusIndicator,
1572
+ {
1573
+ backgroundColor: isHermes
1574
+ ? AppColors.greenColor
1575
+ : AppColors.darkOrange,
1576
+ },
1577
+ ]}
1578
+ />
1579
+ <Text style={bundleStyles.metricValue}>
1580
+ {isHermes ? 'Hermes' : 'JSC'}
1581
+ </Text>
1582
+ </View>
1583
+ <Text style={bundleStyles.metricHint}>
1584
+ {isHermes ? t('bundle.bytecodeAot') : t('bundle.standard')}
1585
+ </Text>
1586
+ </View>
1587
+ </View>
1588
+ </View>
1589
+
1590
+ {/* Live analysis unavailable warning */}
1591
+ {analysis && !analysis.isLive && (
1592
+ <View style={bundleStyles.liveWarningCard}>
1593
+ <View
1594
+ style={{
1595
+ flexDirection: 'row',
1596
+ alignItems: 'center',
1597
+ gap: 6,
1598
+ flex: 1,
1599
+ }}>
1600
+ <CircleAlertIcon color={AppColors.amber700} size={16} />
1601
+ <View style={{flex: 1}}>
1602
+ <Text style={bundleStyles.liveWarningTitle}>
1603
+ {t('bundle.liveAnalysisUnavailable')}
1604
+ </Text>
1605
+ <Text style={bundleStyles.liveWarningDesc}>
1606
+ {t('bundle.liveAnalysisUnavailableSub')}
1607
+ </Text>
1608
+ </View>
1609
+ </View>
1610
+ </View>
1611
+ )}
1612
+
1613
+ {/* Visual Type Distribution Ratio Bar */}
1614
+ <View style={bundleStyles.sectionCard}>
1615
+ <View style={bundleStyles.sectionHeaderRow}>
1616
+ <View style={{flex: 1, paddingRight: 4}}>
1617
+ <Text style={bundleStyles.sectionTitle}>
1618
+ {t('bundle.treemapTitle')}
1619
+ </Text>
1620
+ <Text style={bundleStyles.sectionSub}>
1621
+ {t('bundle.treemapSub', {mb: summary.totalMb})}
1622
+ </Text>
1623
+ </View>
1624
+ <CopyButton
1625
+ value={() => ({
1626
+ totalMb: summary.totalMb,
1627
+ totalKb: summary.totalKb,
1628
+ nodeModules: {
1629
+ kb: summary.nodeModulesKb,
1630
+ pct: summary.nodeModulesPct,
1631
+ },
1632
+ metroRuntime: {kb: summary.metroKb, pct: summary.metroPct},
1633
+ appSource: {
1634
+ kb: summary.appSourceKb,
1635
+ pct: summary.appSourcePct,
1636
+ },
1637
+ assetsMedia: {
1638
+ kb: summary.assetsMediaKb,
1639
+ pct: summary.assetsMediaPct,
1640
+ },
1641
+ })}
1642
+ label={t('bundle.treemapJson')}
1643
+ />
1644
+ </View>
1645
+
1646
+ {/* Stacked Colored Bar for Full 6.0 MB Bundle */}
1647
+ <View style={bundleStyles.treemapBar}>
1648
+ <View
1649
+ style={{
1650
+ flex: summary.nodeModulesPct,
1651
+ backgroundColor: AppColors.indigo500,
1652
+ height: 16,
1653
+ }}
1654
+ />
1655
+ <View
1656
+ style={{
1657
+ flex: summary.metroPct,
1658
+ backgroundColor: AppColors.amber500,
1659
+ height: 16,
1660
+ }}
1661
+ />
1662
+ <View
1663
+ style={{
1664
+ flex: summary.appSourcePct,
1665
+ backgroundColor: AppColors.sky500,
1666
+ height: 16,
1667
+ }}
1668
+ />
1669
+ <View
1670
+ style={{
1671
+ flex: summary.assetsMediaPct,
1672
+ backgroundColor: AppColors.pink500,
1673
+ height: 16,
1674
+ }}
1675
+ />
1676
+ </View>
1677
+
1678
+ {/* Legend Grid */}
1679
+ <View style={bundleStyles.legendGrid}>
1680
+ <View style={bundleStyles.legendItem}>
1681
+ <BundleCategoryIcon
1682
+ type="javascript"
1683
+ size={13}
1684
+ color={AppColors.indigo500}
1685
+ />
1686
+ <Text style={bundleStyles.legendText}>
1687
+ Node Modules{' '}
1688
+ <Text style={bundleStyles.legendVal}>
1689
+ {summary.nodeModulesKb >= 1024
1690
+ ? `${(summary.nodeModulesKb / 1024).toFixed(1)} MB`
1691
+ : `${summary.nodeModulesKb} KB`}{' '}
1692
+ ({summary.nodeModulesPct}%)
1693
+ </Text>
1694
+ </Text>
1695
+ </View>
1696
+ <View style={bundleStyles.legendItem}>
1697
+ <BundleCategoryIcon
1698
+ type="overhead"
1699
+ size={13}
1700
+ color={AppColors.amber500}
1701
+ />
1702
+ <Text style={bundleStyles.legendText}>
1703
+ Metro Runtime{' '}
1704
+ <Text style={bundleStyles.legendVal}>
1705
+ {summary.metroKb >= 1024
1706
+ ? `${(summary.metroKb / 1024).toFixed(1)} MB`
1707
+ : `${summary.metroKb} KB`}{' '}
1708
+ ({summary.metroPct}%)
1709
+ </Text>
1710
+ </Text>
1711
+ </View>
1712
+ <View style={bundleStyles.legendItem}>
1713
+ <BundleCategoryIcon
1714
+ type="typescript"
1715
+ size={13}
1716
+ color={AppColors.sky500}
1717
+ />
1718
+ <Text style={bundleStyles.legendText}>
1719
+ App Source{' '}
1720
+ <Text style={bundleStyles.legendVal}>
1721
+ {summary.appSourceKb >= 1024
1722
+ ? `${(summary.appSourceKb / 1024).toFixed(1)} MB`
1723
+ : `${summary.appSourceKb} KB`}{' '}
1724
+ ({summary.appSourcePct}%)
1725
+ </Text>
1726
+ </Text>
1727
+ </View>
1728
+ <View style={bundleStyles.legendItem}>
1729
+ <BundleCategoryIcon
1730
+ type="image"
1731
+ size={13}
1732
+ color={AppColors.pink500}
1733
+ />
1734
+ <Text style={bundleStyles.legendText}>
1735
+ Assets & Media{' '}
1736
+ <Text style={bundleStyles.legendVal}>
1737
+ {summary.assetsMediaKb >= 1024
1738
+ ? `${(summary.assetsMediaKb / 1024).toFixed(1)} MB`
1739
+ : `${summary.assetsMediaKb} KB`}{' '}
1740
+ ({summary.assetsMediaPct}%)
1741
+ </Text>
1742
+ </Text>
1743
+ </View>
1744
+ </View>
1745
+ </View>
1746
+
1747
+ {/* Detailed Category Footprint Cards */}
1748
+ <View style={bundleStyles.sectionCard}>
1749
+ <View style={bundleStyles.sectionHeaderRow}>
1750
+ <Text style={bundleStyles.sectionTitle}>
1751
+ {t('bundle.categoryBreakdown')}
1752
+ </Text>
1753
+ <CopyButton
1754
+ value={() => summary}
1755
+ label={t('bundle.categoriesJson')}
1756
+ />
1757
+ </View>
1758
+
1759
+ {summary.categories.map((cat, cIdx) => (
1760
+ <Pressable
1761
+ key={cIdx}
1762
+ accessible={true}
1763
+ accessibilityRole="button"
1764
+ accessibilityLabel={`${cat.title}, ${
1765
+ cat.sizeKb >= 1024 ? `${cat.sizeMb} MB` : `${cat.sizeKb} KB`
1766
+ }, ${cat.pct}% of bundle`}
1767
+ accessibilityHint={`Double tap to view ${cat.title} details`}
1768
+ style={({pressed}) => [
1769
+ bundleStyles.catRowCard,
1770
+ pressed && {opacity: 0.85, transform: [{scale: 0.99}]},
1771
+ ]}
1772
+ onPress={() => setActiveSubTab(cat.targetTab)}>
1773
+ <View style={bundleStyles.catRowTop}>
1774
+ <View
1775
+ style={[
1776
+ bundleStyles.catIconBox,
1777
+ {
1778
+ backgroundColor: `${cat.color}15`,
1779
+ borderColor: `${cat.color}30`,
1780
+ },
1781
+ ]}>
1782
+ <BundleCategoryIcon
1783
+ type={cat.iconType}
1784
+ size={18}
1785
+ color={cat.color}
1786
+ />
1787
+ </View>
1788
+
1789
+ <View style={bundleStyles.catRowTitleWrap}>
1790
+ <View
1791
+ style={{
1792
+ flexDirection: 'row',
1793
+ alignItems: 'center',
1794
+ justifyContent: 'space-between',
1795
+ gap: 6,
1796
+ }}>
1797
+ <Text style={bundleStyles.catRowTitle} numberOfLines={1}>
1798
+ {cat.title}
1799
+ </Text>
1800
+ <View
1801
+ style={{
1802
+ flexDirection: 'row',
1803
+ alignItems: 'center',
1804
+ gap: 6,
1805
+ }}>
1806
+ <Text style={bundleStyles.catRowSize}>
1807
+ {cat.sizeKb >= 1024
1808
+ ? `${cat.sizeMb} MB`
1809
+ : `${cat.sizeKb} KB`}
1810
+ </Text>
1811
+ <View
1812
+ style={[
1813
+ bundleStyles.catPctPill,
1814
+ {backgroundColor: `${cat.color}18`},
1815
+ ]}>
1816
+ <Text
1817
+ style={[
1818
+ bundleStyles.catPctText,
1819
+ {color: cat.color},
1820
+ ]}>
1821
+ {cat.pct}%
1822
+ </Text>
1823
+ </View>
1824
+ </View>
1825
+ </View>
1826
+
1827
+ <Text style={bundleStyles.catRowDesc} numberOfLines={2}>
1828
+ {cat.count} items • {cat.desc}
1829
+ </Text>
1830
+ </View>
1831
+ </View>
1832
+
1833
+ {/* Modern Rounded Progress Bar */}
1834
+ <View style={bundleStyles.catProgressTrack}>
1835
+ <View
1836
+ style={[
1837
+ bundleStyles.catProgressBar,
1838
+ {width: `${cat.pct}%`, backgroundColor: cat.color},
1839
+ ]}
1840
+ />
1841
+ </View>
1842
+ </Pressable>
1843
+ ))}
1844
+
1845
+ {/* Executive Total Summation Card */}
1846
+ <View style={bundleStyles.totalSummaryCard}>
1847
+ <View style={bundleStyles.totalSummaryRow}>
1848
+ <View style={bundleStyles.totalSummaryLabelWrap}>
1849
+ <View style={bundleStyles.totalIconBadge}>
1850
+ <PackageIcon color={AppColors.brandPurple} size={16} />
1851
+ </View>
1852
+ <View>
1853
+ <Text style={bundleStyles.totalSummaryLabel}>
1854
+ TOTAL BUNDLE ASSETS
1855
+ </Text>
1856
+ <Text style={bundleStyles.totalSummarySub}>
1857
+ {summary.totalCount} Tracked Modules & Media Assets
1858
+ </Text>
1859
+ </View>
1860
+ </View>
1861
+ <View style={{alignItems: 'flex-end'}}>
1862
+ <Text style={bundleStyles.totalSummaryValue}>
1863
+ {summary.totalMb} MB
1864
+ </Text>
1865
+ <Text style={bundleStyles.totalSummaryKb}>
1866
+ {summary.totalKb.toLocaleString()} KB
1867
+ </Text>
1868
+ </View>
1869
+ </View>
1870
+
1871
+ {/* 4-Pill Chip Breakdown */}
1872
+ <View style={bundleStyles.totalFormulaGrid}>
1873
+ <View style={bundleStyles.totalChip}>
1874
+ <View
1875
+ style={[
1876
+ bundleStyles.totalDot,
1877
+ {backgroundColor: AppColors.indigo500},
1878
+ ]}
1879
+ />
1880
+ <Text style={bundleStyles.totalChipText}>
1881
+ NPM: {(summary.nodeModulesKb / 1024).toFixed(1)}M (
1882
+ {summary.nodeModulesPct}%)
1883
+ </Text>
1884
+ </View>
1885
+ <View style={bundleStyles.totalChip}>
1886
+ <View
1887
+ style={[
1888
+ bundleStyles.totalDot,
1889
+ {backgroundColor: AppColors.amber500},
1890
+ ]}
1891
+ />
1892
+ <Text style={bundleStyles.totalChipText}>
1893
+ Metro: {(summary.metroKb / 1024).toFixed(1)}M (
1894
+ {summary.metroPct}%)
1895
+ </Text>
1896
+ </View>
1897
+ <View style={bundleStyles.totalChip}>
1898
+ <View
1899
+ style={[
1900
+ bundleStyles.totalDot,
1901
+ {backgroundColor: AppColors.sky500},
1902
+ ]}
1903
+ />
1904
+ <Text style={bundleStyles.totalChipText}>
1905
+ Source: {(summary.appSourceKb / 1024).toFixed(1)}M (
1906
+ {summary.appSourcePct}%)
1907
+ </Text>
1908
+ </View>
1909
+ <View style={bundleStyles.totalChip}>
1910
+ <View
1911
+ style={[
1912
+ bundleStyles.totalDot,
1913
+ {backgroundColor: AppColors.pink500},
1914
+ ]}
1915
+ />
1916
+ <Text style={bundleStyles.totalChipText}>
1917
+ Media: {summary.assetsMediaKb}K ({summary.assetsMediaPct}%)
1918
+ </Text>
1919
+ </View>
1920
+ </View>
1921
+ </View>
1922
+ </View>
1923
+
1924
+ {/* Detailed Subsystem Architecture Split-up */}
1925
+ <View style={bundleStyles.sectionCard}>
1926
+ <View style={bundleStyles.sectionHeaderRow}>
1927
+ <View style={{flex: 1, paddingRight: 4}}>
1928
+ <Text style={bundleStyles.sectionTitle}>
1929
+ Bundle Subsystem Breakdown
1930
+ </Text>
1931
+ <Text style={bundleStyles.sectionSub}>
1932
+ Granular analysis of packages, application source, engine
1933
+ runtime & assets
1934
+ </Text>
1935
+ </View>
1936
+ <CopyButton
1937
+ value={() => ({
1938
+ directPackages: bundlePackages.filter(
1939
+ p => p.type === 'direct',
1940
+ ).length,
1941
+ transitivePackages: bundlePackages.filter(
1942
+ p => p.type !== 'direct',
1943
+ ).length,
1944
+ hermesEngine: isHermes ? 'Hermes (Bytecode)' : 'JSC (Source)',
1945
+ jsDevKb: summary.totalKb,
1946
+ jsHermesReleaseKb: Math.round(summary.totalKb * 0.38),
1947
+ appSourceFiles: bundleFiles.filter(
1948
+ f =>
1949
+ f.category === 'typescript' ||
1950
+ f.category === 'javascript',
1951
+ ).length,
1952
+ mediaAssets: bundleFiles.filter(
1953
+ f =>
1954
+ f.category === 'image' ||
1955
+ f.category === 'font' ||
1956
+ f.category === 'json',
1957
+ ).length,
1958
+ })}
1959
+ label="Subsystem Split JSON"
1960
+ />
1961
+ </View>
1962
+
1963
+ {/* 1. NPM Dependencies Layer */}
1964
+ <View style={bundleStyles.subsystemCard}>
1965
+ <View style={bundleStyles.subsystemHeader}>
1966
+ <View
1967
+ style={{flexDirection: 'row', alignItems: 'center', gap: 6}}>
1968
+ <BundleCategoryIcon
1969
+ type="deps"
1970
+ size={16}
1971
+ color={AppColors.indigo500}
1972
+ />
1973
+ <Text style={bundleStyles.subsystemTitle}>
1974
+ NPM & Third-Party Dependencies
1975
+ </Text>
1976
+ </View>
1977
+ <View
1978
+ style={[
1979
+ bundleStyles.subsystemBadge,
1980
+ {backgroundColor: `${AppColors.indigo500}18`},
1981
+ ]}>
1982
+ <Text
1983
+ style={[
1984
+ bundleStyles.subsystemBadgeText,
1985
+ {color: AppColors.indigo500},
1986
+ ]}>
1987
+ {(summary.nodeModulesKb / 1024).toFixed(1)} MB •{' '}
1988
+ {summary.nodeModulesPct}%
1989
+ </Text>
1990
+ </View>
1991
+ </View>
1992
+ <View style={bundleStyles.subsystemGrid}>
1993
+ <View style={bundleStyles.subsystemItem}>
1994
+ <Text style={bundleStyles.subsystemItemText}>
1995
+ Direct:{' '}
1996
+ <Text style={bundleStyles.subsystemItemVal}>
1997
+ {bundlePackages.filter(p => p.type === 'direct').length ||
1998
+ 12}{' '}
1999
+ pkgs
2000
+ </Text>
2001
+ </Text>
2002
+ </View>
2003
+ <View style={bundleStyles.subsystemItem}>
2004
+ <Text style={bundleStyles.subsystemItemText}>
2005
+ Transitive:{' '}
2006
+ <Text style={bundleStyles.subsystemItemVal}>
2007
+ {bundlePackages.filter(p => p.type !== 'direct').length ||
2008
+ 41}{' '}
2009
+ pkgs
2010
+ </Text>
2011
+ </Text>
2012
+ </View>
2013
+ <View style={bundleStyles.subsystemItem}>
2014
+ <Text style={bundleStyles.subsystemItemText}>
2015
+ Top Package:{' '}
2016
+ <Text style={bundleStyles.subsystemItemVal}>
2017
+ {bundlePackages[0]?.name || 'react-native'} (
2018
+ {bundlePackages[0]?.sizeKb || 1240} KB)
2019
+ </Text>
2020
+ </Text>
2021
+ </View>
2022
+ </View>
2023
+ </View>
2024
+
2025
+ {/* 2. App Source Code Layer */}
2026
+ <View style={bundleStyles.subsystemCard}>
2027
+ <View style={bundleStyles.subsystemHeader}>
2028
+ <View
2029
+ style={{flexDirection: 'row', alignItems: 'center', gap: 6}}>
2030
+ <BundleCategoryIcon
2031
+ type="source"
2032
+ size={16}
2033
+ color={AppColors.sky500}
2034
+ />
2035
+ <Text style={bundleStyles.subsystemTitle}>
2036
+ Application Source Code
2037
+ </Text>
2038
+ </View>
2039
+ <View
2040
+ style={[
2041
+ bundleStyles.subsystemBadge,
2042
+ {backgroundColor: `${AppColors.sky500}18`},
2043
+ ]}>
2044
+ <Text
2045
+ style={[
2046
+ bundleStyles.subsystemBadgeText,
2047
+ {color: AppColors.sky500},
2048
+ ]}>
2049
+ {(summary.appSourceKb / 1024).toFixed(1)} MB •{' '}
2050
+ {summary.appSourcePct}%
2051
+ </Text>
2052
+ </View>
2053
+ </View>
2054
+ <View style={bundleStyles.subsystemGrid}>
2055
+ <View style={bundleStyles.subsystemItem}>
2056
+ <Text style={bundleStyles.subsystemItemText}>
2057
+ TypeScript/TSX:{' '}
2058
+ <Text style={bundleStyles.subsystemItemVal}>
2059
+ {bundleFiles.filter(f => f.category === 'typescript')
2060
+ .length || 14}{' '}
2061
+ files
2062
+ </Text>
2063
+ </Text>
2064
+ </View>
2065
+ <View style={bundleStyles.subsystemItem}>
2066
+ <Text style={bundleStyles.subsystemItemText}>
2067
+ JavaScript/JSX:{' '}
2068
+ <Text style={bundleStyles.subsystemItemVal}>
2069
+ {bundleFiles.filter(f => f.category === 'javascript')
2070
+ .length || 3}{' '}
2071
+ files
2072
+ </Text>
2073
+ </Text>
2074
+ </View>
2075
+ <View style={bundleStyles.subsystemItem}>
2076
+ <Text style={bundleStyles.subsystemItemText}>
2077
+ Tracked Modules:{' '}
2078
+ <Text style={bundleStyles.subsystemItemVal}>
2079
+ {bundleFiles.length || 18} files
2080
+ </Text>
2081
+ </Text>
2082
+ </View>
2083
+ </View>
2084
+ </View>
2085
+
2086
+ {/* 3. Hermes Optimization & Bytecode Engine */}
2087
+ <View style={bundleStyles.subsystemCard}>
2088
+ <View style={bundleStyles.subsystemHeader}>
2089
+ <View
2090
+ style={{flexDirection: 'row', alignItems: 'center', gap: 6}}>
2091
+ <BundleCategoryIcon
2092
+ type="bolt"
2093
+ size={16}
2094
+ color={AppColors.emerald600}
2095
+ />
2096
+ <Text style={bundleStyles.subsystemTitle}>
2097
+ Hermes Bytecode Engine
2098
+ </Text>
2099
+ </View>
2100
+ <View
2101
+ style={[
2102
+ bundleStyles.subsystemBadge,
2103
+ {backgroundColor: `${AppColors.emerald500}18`},
2104
+ ]}>
2105
+ <Text
2106
+ style={[
2107
+ bundleStyles.subsystemBadgeText,
2108
+ {color: AppColors.emerald700},
2109
+ ]}>
2110
+ ~62% AOT Compression
2111
+ </Text>
2112
+ </View>
2113
+ </View>
2114
+ <View style={bundleStyles.subsystemGrid}>
2115
+ <View style={bundleStyles.subsystemItem}>
2116
+ <Text style={bundleStyles.subsystemItemText}>
2117
+ Dev JS Size:{' '}
2118
+ <Text style={bundleStyles.subsystemItemVal}>
2119
+ {summary.totalMb} MB
2120
+ </Text>
2121
+ </Text>
2122
+ </View>
2123
+ <View style={bundleStyles.subsystemItem}>
2124
+ <Text style={bundleStyles.subsystemItemText}>
2125
+ Hermes Bytecode:{' '}
2126
+ <Text style={bundleStyles.subsystemItemVal}>
2127
+ {((summary.totalKb * 0.38) / 1024).toFixed(2)} MB
2128
+ </Text>
2129
+ </Text>
2130
+ </View>
2131
+ <View style={bundleStyles.subsystemItem}>
2132
+ <Text style={bundleStyles.subsystemItemText}>
2133
+ Execution:{' '}
2134
+ <Text style={bundleStyles.subsystemItemVal}>
2135
+ {isHermes ? 'Hermes JSI Direct' : 'JSC Standard'}
2136
+ </Text>
2137
+ </Text>
2138
+ </View>
2139
+ </View>
2140
+ </View>
2141
+
2142
+ {/* 4. Assets & Media Layer */}
2143
+ <View style={bundleStyles.subsystemCard}>
2144
+ <View style={bundleStyles.subsystemHeader}>
2145
+ <View
2146
+ style={{flexDirection: 'row', alignItems: 'center', gap: 6}}>
2147
+ <BundleCategoryIcon
2148
+ type="media"
2149
+ size={16}
2150
+ color={AppColors.pink500}
2151
+ />
2152
+ <Text style={bundleStyles.subsystemTitle}>
2153
+ Images, Fonts & Static Assets
2154
+ </Text>
2155
+ </View>
2156
+ <View
2157
+ style={[
2158
+ bundleStyles.subsystemBadge,
2159
+ {backgroundColor: `${AppColors.pink500}18`},
2160
+ ]}>
2161
+ <Text
2162
+ style={[
2163
+ bundleStyles.subsystemBadgeText,
2164
+ {color: AppColors.pink500},
2165
+ ]}>
2166
+ {summary.assetsMediaKb} KB • {summary.assetsMediaPct}%
2167
+ </Text>
2168
+ </View>
2169
+ </View>
2170
+ <View style={bundleStyles.subsystemGrid}>
2171
+ <View style={bundleStyles.subsystemItem}>
2172
+ <Text style={bundleStyles.subsystemItemText}>
2173
+ Images & Icons:{' '}
2174
+ <Text style={bundleStyles.subsystemItemVal}>
2175
+ {bundleFiles.filter(f => f.category === 'image').length ||
2176
+ 2}{' '}
2177
+ assets
2178
+ </Text>
2179
+ </Text>
2180
+ </View>
2181
+ <View style={bundleStyles.subsystemItem}>
2182
+ <Text style={bundleStyles.subsystemItemText}>
2183
+ JSON Data:{' '}
2184
+ <Text style={bundleStyles.subsystemItemVal}>
2185
+ {bundleFiles.filter(f => f.category === 'json').length ||
2186
+ 1}{' '}
2187
+ files
2188
+ </Text>
2189
+ </Text>
2190
+ </View>
2191
+ <View style={bundleStyles.subsystemItem}>
2192
+ <Text style={bundleStyles.subsystemItemText}>
2193
+ Custom Fonts:{' '}
2194
+ <Text style={bundleStyles.subsystemItemVal}>
2195
+ {bundleFiles.filter(f => f.category === 'font').length ||
2196
+ 0}{' '}
2197
+ fonts
2198
+ </Text>
2199
+ </Text>
2200
+ </View>
2201
+ </View>
2202
+ </View>
2203
+ </View>
2204
+ </ScrollView>
2205
+ )}
2206
+
2207
+ {/* ══════════════════════════════════════════════════════════════════════ */}
2208
+ {/* ── 2. TAB: PRODUCTION BUILDS & PLATFORMS ───────────────────────────── */}
2209
+ {/* ══════════════════════════════════════════════════════════════════════ */}
2210
+ {activeSubTab === 'production' && (
2211
+ <ScrollView
2212
+ style={{flex: 1}}
2213
+ contentContainerStyle={bundleStyles.contentContainer}
2214
+ keyboardShouldPersistTaps="handled">
2215
+ {/* Platform Segmented Switcher (Horizontally Scrollable) */}
2216
+ <ScrollView
2217
+ horizontal
2218
+ showsHorizontalScrollIndicator={false}
2219
+ contentContainerStyle={bundleStyles.prodPlatformScroll}>
2220
+ {[
2221
+ {
2222
+ key: 'ios' as const,
2223
+ label: 'iOS App (.ipa)',
2224
+ badge: `${
2225
+ analysis?.production.ios.totalDownloadMb || 111.9
2226
+ } MB`,
2227
+ },
2228
+ {
2229
+ key: 'androidAab' as const,
2230
+ label: 'Android AAB (.aab)',
2231
+ badge: `${
2232
+ analysis?.production.androidAab.totalDownloadMb || 38.5
2233
+ } MB`,
2234
+ },
2235
+ {
2236
+ key: 'androidApk' as const,
2237
+ label: 'Universal APK (.apk)',
2238
+ badge: `${
2239
+ analysis?.production.androidApk.totalInstallMb || 364.0
2240
+ } MB`,
2241
+ },
2242
+ ].map(tab => {
2243
+ const isSelected = prodPlatform === tab.key;
2244
+ return (
2245
+ <TouchableScale
2246
+ key={tab.key}
2247
+ accessible={true}
2248
+ accessibilityRole="tab"
2249
+ accessibilityLabel={`${tab.label}, ${tab.badge}`}
2250
+ accessibilityState={{selected: isSelected}}
2251
+ accessibilityHint={`Switches production estimate to ${tab.label}`}
2252
+ onPress={() => setProdPlatform(tab.key)}
2253
+ style={[
2254
+ bundleStyles.prodPlatformBtn,
2255
+ isSelected && bundleStyles.prodPlatformBtnActive,
2256
+ ]}>
2257
+ <Text
2258
+ style={[
2259
+ bundleStyles.prodPlatformBtnText,
2260
+ isSelected && bundleStyles.prodPlatformBtnTextActive,
2261
+ ]}>
2262
+ {tab.label}
2263
+ </Text>
2264
+ <View
2265
+ style={[
2266
+ bundleStyles.prodPlatformBadge,
2267
+ isSelected && bundleStyles.prodPlatformBadgeActive,
2268
+ ]}>
2269
+ <Text
2270
+ style={[
2271
+ bundleStyles.prodPlatformBadgeText,
2272
+ isSelected && bundleStyles.prodPlatformBadgeTextActive,
2273
+ ]}>
2274
+ {tab.badge}
2275
+ </Text>
2276
+ </View>
2277
+ </TouchableScale>
2278
+ );
2279
+ })}
2280
+ </ScrollView>
2281
+
2282
+ {/* Production Platform Metrics Hero */}
2283
+ {analysis && (
2284
+ <View style={bundleStyles.heroCard}>
2285
+ <View style={bundleStyles.heroTopRow}>
2286
+ <View style={bundleStyles.heroIconWrap}>
2287
+ <PackageIcon color={AppColors.brandPurple} size={24} />
2288
+ </View>
2289
+ <View style={{flex: 1}}>
2290
+ <Text style={bundleStyles.heroTitle}>
2291
+ {prodPlatform === 'ios'
2292
+ ? 'iOS Production Binary Footprint'
2293
+ : prodPlatform === 'androidAab'
2294
+ ? 'Google Play App Bundle (.aab)'
2295
+ : 'Universal Standalone APK (.apk)'}
2296
+ </Text>
2297
+ <Text style={bundleStyles.heroSubtitle}>
2298
+ {prodPlatform === 'ios'
2299
+ ? 'App Store install & over-the-air cellular download estimates'
2300
+ : prodPlatform === 'androidAab'
2301
+ ? 'Optimized per-device dynamic delivery split APK architecture'
2302
+ : 'Multi-ABI universal install archive for sideloading & direct distribution'}
2303
+ </Text>
2304
+ </View>
2305
+ <CopyButton
2306
+ value={() => analysis.production[prodPlatform]}
2307
+ label="Production Binary JSON"
2308
+ />
2309
+ </View>
2310
+
2311
+ {/* 4-Stat Balanced 2x2 Metric Grid */}
2312
+ <View style={bundleStyles.metricsGrid}>
2313
+ <View style={bundleStyles.metricBox}>
2314
+ <Text style={bundleStyles.metricLabel}>INSTALL SIZE</Text>
2315
+ <Text
2316
+ style={[
2317
+ bundleStyles.metricValue,
2318
+ {color: AppColors.purple},
2319
+ ]}>
2320
+ {analysis.production[prodPlatform].totalInstallMb} MB
2321
+ </Text>
2322
+ <Text style={bundleStyles.metricHint}>
2323
+ On-device uncompressed footprint
2324
+ </Text>
2325
+ </View>
2326
+
2327
+ <View style={bundleStyles.metricBox}>
2328
+ <Text style={bundleStyles.metricLabel}>DOWNLOAD SIZE</Text>
2329
+ <Text
2330
+ style={[
2331
+ bundleStyles.metricValue,
2332
+ {color: AppColors.emerald600},
2333
+ ]}>
2334
+ ~{analysis.production[prodPlatform].totalDownloadMb} MB
2335
+ </Text>
2336
+ <Text style={bundleStyles.metricHint}>
2337
+ Store network transfer payload
2338
+ </Text>
2339
+ </View>
2340
+
2341
+ <View style={bundleStyles.metricBox}>
2342
+ <Text style={bundleStyles.metricLabel}>COMPRESSION</Text>
2343
+ <Text
2344
+ style={[
2345
+ bundleStyles.metricValue,
2346
+ {color: AppColors.sky500},
2347
+ ]}>
2348
+ {analysis.production[prodPlatform].compressionRatioPct}%
2349
+ </Text>
2350
+ <Text style={bundleStyles.metricHint}>
2351
+ Bytecode & asset ratio
2352
+ </Text>
2353
+ </View>
2354
+
2355
+ <View style={bundleStyles.metricBox}>
2356
+ <Text style={bundleStyles.metricLabel}>
2357
+ FORMAT ARCHITECTURE
2358
+ </Text>
2359
+ <Text
2360
+ style={[
2361
+ bundleStyles.metricValue,
2362
+ {color: AppColors.amber700},
2363
+ ]}>
2364
+ {prodPlatform === 'ios'
2365
+ ? 'Mach-O 64-bit'
2366
+ : prodPlatform === 'androidAab'
2367
+ ? 'Split APKs'
2368
+ : 'FAT Multi-ABI'}
2369
+ </Text>
2370
+ <Text style={bundleStyles.metricHint}>
2371
+ {prodPlatform === 'ios'
2372
+ ? 'Apple ARM64 runtime'
2373
+ : prodPlatform === 'androidAab'
2374
+ ? 'Dynamic Google Play delivery'
2375
+ : 'Sideload universal package'}
2376
+ </Text>
2377
+ </View>
2378
+ </View>
2379
+ </View>
2380
+ )}
2381
+
2382
+ {/* Component Breakdown Cards */}
2383
+ {analysis && (
2384
+ <View style={bundleStyles.sectionCard}>
2385
+ <View style={bundleStyles.sectionHeaderRow}>
2386
+ <View style={{flex: 1, paddingRight: 4}}>
2387
+ <Text style={bundleStyles.sectionTitle}>
2388
+ Binary Component Architecture
2389
+ </Text>
2390
+ <Text style={bundleStyles.sectionSub}>
2391
+ Compiled native libraries, runtime bytecodes, assets, and
2392
+ signature blocks
2393
+ </Text>
2394
+ </View>
2395
+ <CopyButton
2396
+ value={() => analysis.production[prodPlatform].components}
2397
+ label="Components JSON"
2398
+ />
2399
+ </View>
2400
+
2401
+ {/* Visual Binary Component Ratio Bar */}
2402
+ <View style={bundleStyles.prodTreemapBar}>
2403
+ {analysis.production[prodPlatform].components.map(
2404
+ (comp, cIdx) => (
2405
+ <View
2406
+ key={cIdx}
2407
+ style={{
2408
+ flex: Math.max(comp.pct, 2),
2409
+ backgroundColor: comp.color,
2410
+ height: 16,
2411
+ }}
2412
+ />
2413
+ ),
2414
+ )}
2415
+ </View>
2416
+
2417
+ {/* Production Binary Legend */}
2418
+ <View style={[bundleStyles.legendGrid, {marginBottom: 12}]}>
2419
+ {analysis.production[prodPlatform].components.map(
2420
+ (comp, cIdx) => (
2421
+ <View key={cIdx} style={bundleStyles.legendItem}>
2422
+ <View
2423
+ style={{
2424
+ width: 8,
2425
+ height: 8,
2426
+ borderRadius: 4,
2427
+ backgroundColor: comp.color,
2428
+ }}
2429
+ />
2430
+ <Text style={bundleStyles.legendText} numberOfLines={1}>
2431
+ {comp.name}{' '}
2432
+ <Text style={bundleStyles.legendVal}>
2433
+ {comp.sizeMb} MB ({comp.pct}%)
2434
+ </Text>
2435
+ </Text>
2436
+ </View>
2437
+ ),
2438
+ )}
2439
+ </View>
2440
+
2441
+ {analysis.production[prodPlatform].components.map((comp, idx) => (
2442
+ <View key={idx} style={bundleStyles.catRowCard}>
2443
+ <View style={bundleStyles.catRowTop}>
2444
+ <View
2445
+ style={[
2446
+ bundleStyles.catIconBox,
2447
+ {
2448
+ backgroundColor: `${comp.color}15`,
2449
+ borderColor: `${comp.color}30`,
2450
+ },
2451
+ ]}>
2452
+ <BundleCategoryIcon
2453
+ type={
2454
+ comp.category === 'native'
2455
+ ? 'source'
2456
+ : comp.category === 'frameworks'
2457
+ ? 'deps'
2458
+ : comp.category === 'assets'
2459
+ ? 'media'
2460
+ : comp.category === 'js'
2461
+ ? 'javascript'
2462
+ : 'overhead'
2463
+ }
2464
+ size={18}
2465
+ color={comp.color}
2466
+ />
2467
+ </View>
2468
+
2469
+ <View style={bundleStyles.catRowTitleWrap}>
2470
+ <View
2471
+ style={{
2472
+ flexDirection: 'row',
2473
+ alignItems: 'center',
2474
+ justifyContent: 'space-between',
2475
+ gap: 6,
2476
+ }}>
2477
+ <Text
2478
+ style={bundleStyles.catRowTitle}
2479
+ numberOfLines={1}>
2480
+ {comp.name}
2481
+ </Text>
2482
+ <View
2483
+ style={{
2484
+ flexDirection: 'row',
2485
+ alignItems: 'center',
2486
+ gap: 6,
2487
+ }}>
2488
+ <Text style={bundleStyles.catRowSize}>
2489
+ {comp.sizeMb} MB
2490
+ </Text>
2491
+ <View
2492
+ style={[
2493
+ bundleStyles.catPctPill,
2494
+ {backgroundColor: `${comp.color}18`},
2495
+ ]}>
2496
+ <Text
2497
+ style={[
2498
+ bundleStyles.catPctText,
2499
+ {color: comp.color},
2500
+ ]}>
2501
+ {comp.pct}%
2502
+ </Text>
2503
+ </View>
2504
+ </View>
2505
+ </View>
2506
+
2507
+ <Text style={bundleStyles.catRowDesc} numberOfLines={2}>
2508
+ {comp.description}
2509
+ </Text>
2510
+ </View>
2511
+ </View>
2512
+
2513
+ <View style={bundleStyles.catProgressTrack}>
2514
+ <View
2515
+ style={[
2516
+ bundleStyles.catProgressBar,
2517
+ {width: `${comp.pct}%`, backgroundColor: comp.color},
2518
+ ]}
2519
+ />
2520
+ </View>
2521
+
2522
+ {comp.advice ? (
2523
+ <View
2524
+ style={[
2525
+ bundleStyles.adviceBadge,
2526
+ bundleStyles.adviceOptimal,
2527
+ {marginTop: 6},
2528
+ ]}>
2529
+ <LightbulbIcon color={AppColors.emerald700} size={13} />
2530
+ <Text
2531
+ style={[
2532
+ bundleStyles.adviceText,
2533
+ {color: AppColors.emerald700, flex: 1, marginLeft: 4},
2534
+ ]}>
2535
+ {comp.advice}
2536
+ </Text>
2537
+ </View>
2538
+ ) : null}
2539
+ </View>
2540
+ ))}
2541
+
2542
+ {/* Total Platform Binary Summary with JS Source Bundle Comparison */}
2543
+ <View style={bundleStyles.totalSummaryCard}>
2544
+ <View style={bundleStyles.totalSummaryRow}>
2545
+ <View style={bundleStyles.totalSummaryLabelWrap}>
2546
+ <View style={bundleStyles.totalIconBadge}>
2547
+ <PackageIcon color={AppColors.brandPurple} size={16} />
2548
+ </View>
2549
+ <View>
2550
+ <Text style={bundleStyles.totalSummaryLabel}>
2551
+ {prodPlatform === 'ios'
2552
+ ? 'TOTAL IOS APP BINARY'
2553
+ : prodPlatform === 'androidAab'
2554
+ ? 'TOTAL ANDROID AAB BUNDLE'
2555
+ : 'TOTAL UNIVERSAL APK'}
2556
+ </Text>
2557
+ <Text style={bundleStyles.totalSummarySub}>
2558
+ {prodPlatform === 'ios'
2559
+ ? 'App Store install & cellular download estimates'
2560
+ : prodPlatform === 'androidAab'
2561
+ ? 'Google Play dynamic delivery split APK'
2562
+ : 'Universal standalone sideload package'}
2563
+ </Text>
2564
+ </View>
2565
+ </View>
2566
+ </View>
2567
+
2568
+ {/* Platform Metric Grid Chips */}
2569
+ <View style={bundleStyles.totalFormulaGrid}>
2570
+ <View style={bundleStyles.totalChip}>
2571
+ <View
2572
+ style={[
2573
+ bundleStyles.totalDot,
2574
+ {backgroundColor: AppColors.purple},
2575
+ ]}
2576
+ />
2577
+ <Text style={bundleStyles.totalChipText}>
2578
+ Install:{' '}
2579
+ {analysis.production[prodPlatform].totalInstallMb} MB
2580
+ </Text>
2581
+ </View>
2582
+ <View style={bundleStyles.totalChip}>
2583
+ <View
2584
+ style={[
2585
+ bundleStyles.totalDot,
2586
+ {backgroundColor: AppColors.sky500},
2587
+ ]}
2588
+ />
2589
+ <Text style={bundleStyles.totalChipText}>
2590
+ Download: ~
2591
+ {analysis.production[prodPlatform].totalDownloadMb} MB
2592
+ </Text>
2593
+ </View>
2594
+ <View style={bundleStyles.totalChip}>
2595
+ <View
2596
+ style={[
2597
+ bundleStyles.totalDot,
2598
+ {backgroundColor: AppColors.emerald500},
2599
+ ]}
2600
+ />
2601
+ <Text style={bundleStyles.totalChipText}>
2602
+ Compression:{' '}
2603
+ {analysis.production[prodPlatform].compressionRatioPct}%
2604
+ </Text>
2605
+ </View>
2606
+ </View>
2607
+ </View>
2608
+ </View>
2609
+ )}
2610
+ </ScrollView>
2611
+ )}
2612
+
2613
+ {/* ══════════════════════════════════════════════════════════════════════ */}
2614
+ {/* ── 2. TAB: FILES BREAKDOWN ─────────────────────────────────────────── */}
2615
+ {/* ══════════════════════════════════════════════════════════════════════ */}
2616
+ {activeSubTab === 'files' && (
2617
+ <View style={{flex: 1}}>
2618
+ {/* Search & Category Filter */}
2619
+ <View style={bundleStyles.filterContainer}>
2620
+ <View style={bundleStyles.searchRow}>
2621
+ <SearchIcon color={AppColors.grayTextWeak} size={15} />
2622
+ <TextInput
2623
+ accessible={true}
2624
+ accessibilityRole="search"
2625
+ accessibilityLabel="Search files"
2626
+ accessibilityHint="Type a filename, path, or extension to filter"
2627
+ placeholder={t('bundle.searchFilesPlaceholder')}
2628
+ placeholderTextColor={AppColors.grayTextWeak}
2629
+ value={search}
2630
+ onChangeText={setSearch}
2631
+ style={bundleStyles.searchInput}
2632
+ autoCorrect={false}
2633
+ autoCapitalize="none"
2634
+ />
2635
+ {search.length > 0 && (
2636
+ <Pressable
2637
+ accessible={true}
2638
+ accessibilityRole="button"
2639
+ accessibilityLabel="Clear search input"
2640
+ onPress={() => setSearch('')}
2641
+ hitSlop={10}>
2642
+ <ClearIcon color={AppColors.grayTextWeak} size={14} />
2643
+ </Pressable>
2644
+ )}
2645
+ <CopyButton
2646
+ value={() => filteredFiles}
2647
+ label={t('bundle.filteredFilesJson')}
2648
+ />
2649
+ </View>
2650
+
2651
+ {/* Category Filter Chips */}
2652
+ <ScrollView
2653
+ horizontal
2654
+ showsHorizontalScrollIndicator={false}
2655
+ contentContainerStyle={bundleStyles.categoryPillsScroll}>
2656
+ {CATEGORY_TABS.map(tab => {
2657
+ const isActive = activeCategory === tab.key;
2658
+ const count =
2659
+ tab.key === 'ALL'
2660
+ ? bundleFiles.length
2661
+ : tab.key === 'UNUSED'
2662
+ ? bundleFiles.filter(f => f.isConsumed === false).length
2663
+ : tab.key === 'CONSUMED'
2664
+ ? bundleFiles.filter(f => f.isConsumed !== false).length
2665
+ : bundleFiles.filter(f => f.category === tab.key).length;
2666
+
2667
+ return (
2668
+ <TouchableScale
2669
+ key={tab.key}
2670
+ accessible={true}
2671
+ accessibilityRole="button"
2672
+ accessibilityLabel={`Filter by ${t(
2673
+ tab.labelKey,
2674
+ )}, ${count} items`}
2675
+ accessibilityState={{selected: isActive}}
2676
+ accessibilityHint={`Filters files list to ${t(
2677
+ tab.labelKey,
2678
+ )}`}
2679
+ onPress={() => setActiveCategory(tab.key)}
2680
+ style={[
2681
+ bundleStyles.catPill,
2682
+ isActive && bundleStyles.catPillActive,
2683
+ tab.key === 'UNUSED' &&
2684
+ isActive && {
2685
+ backgroundColor: AppColors.red500,
2686
+ borderColor: AppColors.red600,
2687
+ },
2688
+ tab.key === 'CONSUMED' &&
2689
+ isActive && {
2690
+ backgroundColor: AppColors.emerald600,
2691
+ borderColor: AppColors.emerald700,
2692
+ },
2693
+ ]}>
2694
+ <BundleCategoryIcon
2695
+ type={tab.iconType}
2696
+ size={12}
2697
+ color={
2698
+ isActive ? AppColors.white : AppColors.grayTextStrong
2699
+ }
2700
+ />
2701
+ <Text
2702
+ style={[
2703
+ bundleStyles.catPillText,
2704
+ isActive && bundleStyles.catPillTextActive,
2705
+ ]}>
2706
+ {t(tab.labelKey)} ({count})
2707
+ </Text>
2708
+ </TouchableScale>
2709
+ );
2710
+ })}
2711
+ </ScrollView>
2712
+ </View>
2713
+
2714
+ {/* Files List / Tree Content */}
2715
+ <ScrollView
2716
+ style={{flex: 1}}
2717
+ contentContainerStyle={bundleStyles.contentContainer}
2718
+ keyboardShouldPersistTaps="handled">
2719
+ {/* View Mode Toggle Header Bar */}
2720
+ <View style={bundleStyles.treeHeaderRow}>
2721
+ <View style={bundleStyles.viewModeToggleGroup}>
2722
+ <TouchableScale
2723
+ accessible={true}
2724
+ accessibilityRole="button"
2725
+ accessibilityLabel="Tree folder view"
2726
+ accessibilityState={{selected: filesViewMode === 'tree'}}
2727
+ onPress={() => setFilesViewMode('tree')}
2728
+ style={[
2729
+ bundleStyles.viewModeBtn,
2730
+ filesViewMode === 'tree' && bundleStyles.viewModeBtnActive,
2731
+ ]}>
2732
+ <Text
2733
+ style={[
2734
+ bundleStyles.viewModeBtnText,
2735
+ filesViewMode === 'tree' &&
2736
+ bundleStyles.viewModeBtnTextActive,
2737
+ ]}>
2738
+ {t('bundle.treeView')}
2739
+ </Text>
2740
+ </TouchableScale>
2741
+ <TouchableScale
2742
+ accessible={true}
2743
+ accessibilityRole="button"
2744
+ accessibilityLabel="Flat list view"
2745
+ accessibilityState={{selected: filesViewMode === 'list'}}
2746
+ onPress={() => setFilesViewMode('list')}
2747
+ style={[
2748
+ bundleStyles.viewModeBtn,
2749
+ filesViewMode === 'list' && bundleStyles.viewModeBtnActive,
2750
+ ]}>
2751
+ <Text
2752
+ style={[
2753
+ bundleStyles.viewModeBtnText,
2754
+ filesViewMode === 'list' &&
2755
+ bundleStyles.viewModeBtnTextActive,
2756
+ ]}>
2757
+ {t('bundle.flatList')}
2758
+ </Text>
2759
+ </TouchableScale>
2760
+ </View>
2761
+
2762
+ {filesViewMode === 'tree' ? (
2763
+ <View
2764
+ style={{flexDirection: 'row', alignItems: 'center', gap: 8}}>
2765
+ <TouchableScale
2766
+ accessible={true}
2767
+ accessibilityRole="button"
2768
+ accessibilityLabel="Expand all folders"
2769
+ onPress={expandAllFolders}>
2770
+ <Text style={bundleStyles.treeActionLink}>
2771
+ {t('bundle.expand')}
2772
+ </Text>
2773
+ </TouchableScale>
2774
+ <Text style={{color: AppColors.grayTextWeak, fontSize: 11}}>
2775
+
2776
+ </Text>
2777
+ <TouchableScale
2778
+ accessible={true}
2779
+ accessibilityRole="button"
2780
+ accessibilityLabel="Collapse all folders"
2781
+ onPress={collapseAllFolders}>
2782
+ <Text style={bundleStyles.treeActionLink}>
2783
+ {t('bundle.collapse')}
2784
+ </Text>
2785
+ </TouchableScale>
2786
+ </View>
2787
+ ) : (
2788
+ <Text style={bundleStyles.listHeaderCount}>
2789
+ {t('bundle.showingFilesOf', {
2790
+ shown: filteredFiles.length,
2791
+ total: bundleFiles.length,
2792
+ })}
2793
+ </Text>
2794
+ )}
2795
+ </View>
2796
+
2797
+ {/* ── MODE 1: DIRECTORY TREE VIEW ── */}
2798
+ {filesViewMode === 'tree' ? (
2799
+ <View style={bundleStyles.treeRootCard}>
2800
+ {fileTreeNodes.map((node, nIdx) => (
2801
+ <BundleTreeNodeView
2802
+ key={node.id}
2803
+ node={node}
2804
+ level={0}
2805
+ search={search}
2806
+ totalBundleKb={summary.totalKb}
2807
+ expandedFolders={expandedFolders}
2808
+ toggleFolder={toggleFolder}
2809
+ isLastChild={nIdx === fileTreeNodes.length - 1}
2810
+ onDownload={f => downloadBundleFile(f, analysis?.scriptURL)}
2811
+ />
2812
+ ))}
2813
+ </View>
2814
+ ) : (
2815
+ /* ── MODE 2: FLAT LIST VIEW ── */
2816
+ filteredFiles.map((file, index) => {
2817
+ const categoryMeta =
2818
+ CATEGORY_COLORS[file.category] || CATEGORY_COLORS.typescript;
2819
+ const pctOfTotal = (
2820
+ (file.sizeKb / summary.totalKb) *
2821
+ 100
2822
+ ).toFixed(1);
2823
+ const isUnused = file.isConsumed === false;
2824
+
2825
+ return (
2826
+ <View
2827
+ key={file.id}
2828
+ style={[
2829
+ bundleStyles.fileCard,
2830
+ isUnused && bundleStyles.treeFileCardUnused,
2831
+ ]}>
2832
+ <View style={bundleStyles.fileCardTop}>
2833
+ <View style={bundleStyles.sNoBadge}>
2834
+ <Text style={bundleStyles.sNoText}>#{index + 1}</Text>
2835
+ </View>
2836
+
2837
+ <MediaPreviewThumbnail file={file} />
2838
+
2839
+ <View style={{flex: 1, paddingHorizontal: 4}}>
2840
+ <View
2841
+ style={{
2842
+ flexDirection: 'row',
2843
+ alignItems: 'center',
2844
+ gap: 6,
2845
+ flexWrap: 'wrap',
2846
+ }}>
2847
+ <HighlightText
2848
+ text={file.name}
2849
+ search={search}
2850
+ style={bundleStyles.fileName}
2851
+ highlightStyle={bundleStyles.searchHighlight}
2852
+ />
2853
+ {isUnused ? (
2854
+ <View style={bundleStyles.unusedBadge}>
2855
+ <Text style={bundleStyles.unusedBadgeText}>
2856
+ {t('bundle.notConsumed')}
2857
+ </Text>
2858
+ </View>
2859
+ ) : (
2860
+ <View style={bundleStyles.consumedBadge}>
2861
+ <Text style={bundleStyles.consumedBadgeText}>
2862
+ {t('bundle.consumed')}
2863
+ </Text>
2864
+ </View>
2865
+ )}
2866
+ </View>
2867
+ <HighlightText
2868
+ text={file.path}
2869
+ search={search}
2870
+ style={bundleStyles.filePath}
2871
+ highlightStyle={bundleStyles.searchHighlight}
2872
+ numberOfLines={1}
2873
+ />
2874
+ </View>
2875
+
2876
+ <View
2877
+ style={{
2878
+ flexDirection: 'row',
2879
+ alignItems: 'center',
2880
+ gap: 6,
2881
+ }}>
2882
+ <View style={bundleStyles.fileSizeBox}>
2883
+ <Text
2884
+ style={[
2885
+ bundleStyles.fileSizeKb,
2886
+ isUnused && {color: AppColors.amber800Warm},
2887
+ ]}>
2888
+ {file.sizeKb >= 1024
2889
+ ? t('bundle.fileSizeMb', {
2890
+ size: (file.sizeKb / 1024).toFixed(2),
2891
+ })
2892
+ : t('bundle.fileSizeKb', {size: file.sizeKb})}
2893
+ </Text>
2894
+ <Text style={bundleStyles.filePercent}>
2895
+ {pctOfTotal}%
2896
+ </Text>
2897
+ </View>
2898
+ <TouchableScale
2899
+ onPress={() =>
2900
+ downloadBundleFile(file, analysis?.scriptURL)
2901
+ }
2902
+ style={bundleStyles.downloadFileBtn}
2903
+ hitSlop={8}>
2904
+ <DownloadIcon size={14} color={AppColors.sky500} />
2905
+ </TouchableScale>
2906
+ <CopyButton
2907
+ value={() => file}
2908
+ label={t('bundle.fileDetailsJson')}
2909
+ />
2910
+ </View>
2911
+ </View>
2912
+
2913
+ <View style={bundleStyles.fileProgressTrack}>
2914
+ <View
2915
+ style={[
2916
+ bundleStyles.fileProgressBar,
2917
+ {
2918
+ width: `${Math.min(
2919
+ 100,
2920
+ Math.max(4, Number(pctOfTotal) * 4),
2921
+ )}%`,
2922
+ backgroundColor: isUnused
2923
+ ? AppColors.red500
2924
+ : categoryMeta.color,
2925
+ },
2926
+ ]}
2927
+ />
2928
+ </View>
2929
+
2930
+ <View style={bundleStyles.fileCardBottom}>
2931
+ <Text style={bundleStyles.fileMetaText}>{file.meta}</Text>
2932
+ {file.advice && (
2933
+ <View
2934
+ style={[
2935
+ bundleStyles.adviceBadge,
2936
+ file.status === 'warning' &&
2937
+ bundleStyles.adviceWarning,
2938
+ file.status === 'optimal' &&
2939
+ bundleStyles.adviceOptimal,
2940
+ isUnused && bundleStyles.adviceUnused,
2941
+ ]}>
2942
+ {isUnused ? (
2943
+ <TrashIcon color={AppColors.red600} size={12} />
2944
+ ) : file.status === 'warning' ? (
2945
+ <LightbulbIcon
2946
+ color={AppColors.amber800Warm}
2947
+ size={12}
2948
+ />
2949
+ ) : (
2950
+ <SparkleIcon
2951
+ color={AppColors.emerald700}
2952
+ size={12}
2953
+ />
2954
+ )}
2955
+ <Text
2956
+ style={[
2957
+ bundleStyles.adviceText,
2958
+ file.status === 'warning' && {
2959
+ color: AppColors.amber800Warm,
2960
+ },
2961
+ file.status === 'optimal' && {
2962
+ color: AppColors.emerald700,
2963
+ },
2964
+ isUnused && {color: AppColors.red600},
2965
+ ]}>
2966
+ {file.advice}
2967
+ </Text>
2968
+ </View>
2969
+ )}
2970
+ </View>
2971
+ </View>
2972
+ );
2973
+ })
2974
+ )}
2975
+ </ScrollView>
2976
+
2977
+ {/* Sticky Total Bundle Size Footer */}
2978
+ <View style={bundleStyles.stickyFooterBar}>
2979
+ <View style={{flexDirection: 'row', alignItems: 'center', gap: 6}}>
2980
+ <LayersIcon color={AppColors.purple} size={14} />
2981
+ <Text style={bundleStyles.footerTitle}>
2982
+ {t('bundle.filesOf', {
2983
+ count: filteredFiles.length,
2984
+ total: summary.totalCount,
2985
+ })}
2986
+ </Text>
2987
+ </View>
2988
+ <Text style={bundleStyles.footerSizeVal}>
2989
+ {t('bundle.kbMbValue', {
2990
+ kb: filteredTotalKb,
2991
+ mb: (filteredTotalKb / 1024).toFixed(2),
2992
+ })}
2993
+ </Text>
2994
+ </View>
2995
+ </View>
2996
+ )}
2997
+
2998
+ {/* ══════════════════════════════════════════════════════════════════════ */}
2999
+ {/* ── 3. TAB: PACKAGES & NODE_MODULES ─────────────────────────────────── */}
3000
+ {/* ══════════════════════════════════════════════════════════════════════ */}
3001
+ {activeSubTab === 'packages' && (
3002
+ <View style={{flex: 1}}>
3003
+ <ScrollView
3004
+ style={{flex: 1}}
3005
+ contentContainerStyle={bundleStyles.contentContainer}
3006
+ keyboardShouldPersistTaps="handled">
3007
+ <View style={bundleStyles.filterContainer}>
3008
+ <View style={bundleStyles.searchRow}>
3009
+ <SearchIcon color={AppColors.grayTextWeak} size={15} />
3010
+ <TextInput
3011
+ accessible={true}
3012
+ accessibilityRole="search"
3013
+ accessibilityLabel="Search packages"
3014
+ accessibilityHint="Type a package name or description to filter"
3015
+ placeholder={t('bundle.searchPackagesPlaceholder')}
3016
+ placeholderTextColor={AppColors.grayTextWeak}
3017
+ value={search}
3018
+ onChangeText={setSearch}
3019
+ style={bundleStyles.searchInput}
3020
+ autoCorrect={false}
3021
+ autoCapitalize="none"
3022
+ />
3023
+ {search.length > 0 && (
3024
+ <Pressable
3025
+ accessible={true}
3026
+ accessibilityRole="button"
3027
+ accessibilityLabel="Clear search input"
3028
+ onPress={() => setSearch('')}
3029
+ hitSlop={10}>
3030
+ <ClearIcon color={AppColors.grayTextWeak} size={14} />
3031
+ </Pressable>
3032
+ )}
3033
+ <CopyButton
3034
+ value={() => filteredPackages}
3035
+ label={t('bundle.dependenciesJson')}
3036
+ />
3037
+ </View>
3038
+ </View>
3039
+
3040
+ <Text style={bundleStyles.listHeaderCount}>
3041
+ {t('bundle.showingDependencies', {
3042
+ count: filteredPackages.length,
3043
+ })}
3044
+ </Text>
3045
+
3046
+ {filteredPackages.map((pkg, index) => {
3047
+ const cleanInstalledVersion = pkg.version
3048
+ ? pkg.version.replace(/^\^/, '')
3049
+ : '';
3050
+ const hasUpdate =
3051
+ !!pkg.latestVersion &&
3052
+ !!cleanInstalledVersion &&
3053
+ pkg.latestVersion !== cleanInstalledVersion;
3054
+
3055
+ return (
3056
+ <View
3057
+ key={pkg.id}
3058
+ style={[
3059
+ bundleStyles.fileCard,
3060
+ pkg.isDeprecated && {
3061
+ borderColor: AppColors.errorBorder,
3062
+ backgroundColor: AppColors.errorCardBg,
3063
+ },
3064
+ ]}>
3065
+ {/* Header Row: #s.no + Logo + Package Name + Version Pill + Update/Deprecated Badges + Size & Copy */}
3066
+ <View style={bundleStyles.fileCardTop}>
3067
+ <View style={bundleStyles.sNoBadge}>
3068
+ <Text style={bundleStyles.sNoText}>#{index + 1}</Text>
3069
+ </View>
3070
+
3071
+ <PackageLogoRenderer
3072
+ name={pkg.name}
3073
+ color={pkg.color}
3074
+ size={28}
3075
+ />
3076
+
3077
+ <View style={{flex: 1, paddingHorizontal: 4}}>
3078
+ <View
3079
+ style={{
3080
+ flexDirection: 'row',
3081
+ alignItems: 'center',
3082
+ gap: 6,
3083
+ flexWrap: 'wrap',
3084
+ }}>
3085
+ <HighlightText
3086
+ text={pkg.name}
3087
+ search={search}
3088
+ style={bundleStyles.fileName}
3089
+ highlightStyle={bundleStyles.searchHighlight}
3090
+ />
3091
+ <View style={bundleStyles.versionPill}>
3092
+ <Text style={bundleStyles.versionPillText}>
3093
+ {cleanInstalledVersion
3094
+ ? t('bundle.versionPrefix', {
3095
+ version: cleanInstalledVersion,
3096
+ })
3097
+ : t('bundle.bundled')}
3098
+ </Text>
3099
+ </View>
3100
+
3101
+ {pkg.isDeprecated ? (
3102
+ <View style={bundleStyles.deprecatedBadge}>
3103
+ <Text style={bundleStyles.deprecatedBadgeText}>
3104
+ {t('bundle.deprecated')}
3105
+ </Text>
3106
+ </View>
3107
+ ) : hasUpdate ? (
3108
+ <View style={bundleStyles.updateBadge}>
3109
+ <Text style={bundleStyles.updateBadgeText}>
3110
+ {t('bundle.updateAvailable', {
3111
+ version: pkg.latestVersion,
3112
+ })}
3113
+ </Text>
3114
+ </View>
3115
+ ) : cleanInstalledVersion ? (
3116
+ <View
3117
+ style={[
3118
+ bundleStyles.updateBadge,
3119
+ {
3120
+ backgroundColor: AppColors.gray100,
3121
+ borderColor: AppColors.gray200,
3122
+ },
3123
+ ]}>
3124
+ <Text
3125
+ style={[
3126
+ bundleStyles.updateBadgeText,
3127
+ {color: AppColors.gray600},
3128
+ ]}>
3129
+ {t('bundle.upToDate')}
3130
+ </Text>
3131
+ </View>
3132
+ ) : (
3133
+ <View
3134
+ style={[
3135
+ bundleStyles.updateBadge,
3136
+ {
3137
+ backgroundColor: AppColors.gray100,
3138
+ borderColor: AppColors.gray200,
3139
+ },
3140
+ ]}>
3141
+ <Text
3142
+ style={[
3143
+ bundleStyles.updateBadgeText,
3144
+ {color: AppColors.gray600},
3145
+ ]}>
3146
+ {t('bundle.bundledBadge')}
3147
+ </Text>
3148
+ </View>
3149
+ )}
3150
+ </View>
3151
+ </View>
3152
+
3153
+ <View
3154
+ style={{
3155
+ flexDirection: 'row',
3156
+ alignItems: 'center',
3157
+ gap: 6,
3158
+ }}>
3159
+ <View style={bundleStyles.fileSizeBox}>
3160
+ <Text style={bundleStyles.fileSizeKb}>
3161
+ {t('bundle.fileSizeKb', {size: pkg.sizeKb})}
3162
+ </Text>
3163
+ <Text style={bundleStyles.filePercent}>
3164
+ {pkg.percentage}%
3165
+ </Text>
3166
+ </View>
3167
+ <CopyButton
3168
+ value={() => pkg}
3169
+ label={t('bundle.packageDetailsJson')}
3170
+ />
3171
+ </View>
3172
+ </View>
3173
+
3174
+ {/* Subtitle Row: Category Chip + Description */}
3175
+ <View
3176
+ style={{
3177
+ flexDirection: 'row',
3178
+ alignItems: 'center',
3179
+ gap: 6,
3180
+ marginTop: 4,
3181
+ marginBottom: 2,
3182
+ }}>
3183
+ <View
3184
+ style={[
3185
+ bundleStyles.pkgBadge,
3186
+ {
3187
+ backgroundColor: `${pkg.color}1F`,
3188
+ borderColor: `${pkg.color}4D`,
3189
+ },
3190
+ ]}>
3191
+ <Text
3192
+ style={[bundleStyles.pkgBadgeText, {color: pkg.color}]}>
3193
+ {pkg.category.toUpperCase()}
3194
+ </Text>
3195
+ </View>
3196
+ <Text
3197
+ style={[bundleStyles.filePath, {flex: 1, marginTop: 0}]}
3198
+ numberOfLines={1}>
3199
+ {pkg.description}
3200
+ </Text>
3201
+ </View>
3202
+
3203
+ {/* Deprecation Warning Banner if deprecated */}
3204
+ {pkg.isDeprecated && pkg.deprecationReason && (
3205
+ <View style={bundleStyles.deprecationBox}>
3206
+ <View
3207
+ style={{
3208
+ flexDirection: 'row',
3209
+ alignItems: 'center',
3210
+ gap: 5,
3211
+ }}>
3212
+ <CircleAlertIcon
3213
+ color={AppColors.errorColor}
3214
+ size={12}
3215
+ />
3216
+ <Text style={bundleStyles.deprecationText}>
3217
+ {pkg.deprecationReason}
3218
+ </Text>
3219
+ </View>
3220
+ </View>
3221
+ )}
3222
+
3223
+ <View style={bundleStyles.fileProgressTrack}>
3224
+ <View
3225
+ style={[
3226
+ bundleStyles.fileProgressBar,
3227
+ {
3228
+ width: `${pkg.percentage * 2}%`,
3229
+ backgroundColor: pkg.isDeprecated
3230
+ ? AppColors.red500
3231
+ : pkg.color,
3232
+ },
3233
+ ]}
3234
+ />
3235
+ </View>
3236
+
3237
+ {/* Footer Row: Type Tag + Last Active + NPM Link Button */}
3238
+ <View
3239
+ style={{
3240
+ flexDirection: 'row',
3241
+ justifyContent: 'space-between',
3242
+ alignItems: 'center',
3243
+ marginTop: 3,
3244
+ }}>
3245
+ <View
3246
+ style={{
3247
+ flexDirection: 'row',
3248
+ alignItems: 'center',
3249
+ gap: 8,
3250
+ }}>
3251
+ <View style={bundleStyles.typeTag}>
3252
+ <Text style={bundleStyles.typeTagText}>
3253
+ {pkg.type === 'direct'
3254
+ ? t('bundle.direct')
3255
+ : t('bundle.transitive')}
3256
+ </Text>
3257
+ </View>
3258
+ {pkg.lastActive && (
3259
+ <View
3260
+ style={{
3261
+ flexDirection: 'row',
3262
+ alignItems: 'center',
3263
+ gap: 4,
3264
+ }}>
3265
+ <ClockIcon color={AppColors.grayTextWeak} size={11} />
3266
+ <Text style={bundleStyles.lastActiveText}>
3267
+ {pkg.lastActive}
3268
+ </Text>
3269
+ </View>
3270
+ )}
3271
+ </View>
3272
+
3273
+ <View
3274
+ style={{
3275
+ flexDirection: 'row',
3276
+ alignItems: 'center',
3277
+ gap: 8,
3278
+ }}>
3279
+ <Text style={bundleStyles.fileMetaText}>
3280
+ {t('bundle.minified', {size: pkg.sizeKb})}
3281
+ </Text>
3282
+ <TouchableScale
3283
+ accessible={true}
3284
+ accessibilityRole="link"
3285
+ accessibilityLabel={`Open ${pkg.name} on NPM`}
3286
+ accessibilityHint="Opens NPM package page in browser"
3287
+ onPress={() => {
3288
+ const url =
3289
+ pkg.npmUrl ||
3290
+ `https://www.npmjs.com/package/${pkg.name}`;
3291
+ Linking.openURL(url).catch(() => {});
3292
+ }}
3293
+ style={bundleStyles.npmLinkBtn}>
3294
+ <Text style={bundleStyles.npmLinkText}>
3295
+ {t('bundle.npmLink')}
3296
+ </Text>
3297
+ </TouchableScale>
3298
+ </View>
3299
+ </View>
3300
+ </View>
3301
+ );
3302
+ })}
3303
+ </ScrollView>
3304
+
3305
+ {/* Sticky Total Package Size Footer */}
3306
+ <View style={bundleStyles.stickyFooterBar}>
3307
+ <View style={{flexDirection: 'row', alignItems: 'center', gap: 6}}>
3308
+ <PackageIcon color={AppColors.purple} size={14} />
3309
+ <Text style={bundleStyles.footerTitle}>
3310
+ {t('bundle.dependenciesCount', {
3311
+ count: filteredPackages.length,
3312
+ })}
3313
+ </Text>
3314
+ </View>
3315
+ <Text style={bundleStyles.footerSizeVal}>
3316
+ {t('bundle.kbMbValue', {
3317
+ kb: packagesTotalKb,
3318
+ mb: (packagesTotalKb / 1024).toFixed(2),
3319
+ })}
3320
+ </Text>
3321
+ </View>
3322
+ </View>
3323
+ )}
3324
+
3325
+ {/* ══════════════════════════════════════════════════════════════════════ */}
3326
+ {/* ── 4. TAB: MEDIA & ASSET AUDITOR ───────────────────────────────────── */}
3327
+ {/* ══════════════════════════════════════════════════════════════════════ */}
3328
+ {activeSubTab === 'media' && (
3329
+ <View style={{flex: 1}}>
3330
+ <ScrollView
3331
+ style={{flex: 1}}
3332
+ contentContainerStyle={bundleStyles.contentContainer}
3333
+ keyboardShouldPersistTaps="handled">
3334
+ <View style={bundleStyles.tipsCard}>
3335
+ <View
3336
+ style={{
3337
+ flexDirection: 'row',
3338
+ alignItems: 'center',
3339
+ justifyContent: 'space-between',
3340
+ marginBottom: 8,
3341
+ }}>
3342
+ <View
3343
+ style={{flexDirection: 'row', alignItems: 'center', gap: 6}}>
3344
+ <StorageIcon color={AppColors.purple} size={18} />
3345
+ <Text style={bundleStyles.tipsHeading}>
3346
+ {t('bundle.mediaAuditorTitle')}
3347
+ </Text>
3348
+ </View>
3349
+ <CopyButton
3350
+ value={() => mediaFiles}
3351
+ label={t('bundle.mediaAssetsJson')}
3352
+ />
3353
+ </View>
3354
+ <Text style={bundleStyles.tipDesc}>
3355
+ {t('bundle.mediaAuditorPrefix')}{' '}
3356
+ <Text
3357
+ style={{
3358
+ fontFamily: AppFonts.interBold,
3359
+ color: AppColors.purple,
3360
+ }}>
3361
+ {summary.images.pct + summary.fonts.pct}%
3362
+ </Text>{' '}
3363
+ {t('bundle.mediaAuditorMid', {
3364
+ kb: summary.images.kb + summary.fonts.kb,
3365
+ })}{' '}
3366
+ <Text
3367
+ style={{
3368
+ fontFamily: AppFonts.interBold,
3369
+ color: AppColors.emerald700,
3370
+ }}>
3371
+ {t('bundle.mediaSavings', {size: 540})}
3372
+ </Text>{' '}
3373
+ {t('bundle.mediaAuditorSuffix')}
3374
+ </Text>
3375
+ </View>
3376
+
3377
+ <Text style={bundleStyles.listHeaderCount}>
3378
+ {t('bundle.mediaAssetsList', {count: mediaFiles.length})}
3379
+ </Text>
3380
+
3381
+ {mediaFiles.map((file, index) => (
3382
+ <View key={file.id} style={bundleStyles.fileCard}>
3383
+ <View style={bundleStyles.fileCardTop}>
3384
+ <View style={bundleStyles.sNoBadge}>
3385
+ <Text style={bundleStyles.sNoText}>#{index + 1}</Text>
3386
+ </View>
3387
+
3388
+ <MediaPreviewThumbnail file={file} />
3389
+
3390
+ <View style={{flex: 1}}>
3391
+ <Text style={bundleStyles.fileName}>{file.name}</Text>
3392
+ <Text style={bundleStyles.filePath}>{file.path}</Text>
3393
+ </View>
3394
+
3395
+ <View
3396
+ style={{
3397
+ flexDirection: 'row',
3398
+ alignItems: 'center',
3399
+ gap: 6,
3400
+ }}>
3401
+ <View style={bundleStyles.fileSizeBox}>
3402
+ <Text style={bundleStyles.fileSizeKb}>
3403
+ {t('bundle.fileSizeKb', {size: file.sizeKb})}
3404
+ </Text>
3405
+ </View>
3406
+ <TouchableScale
3407
+ accessible={true}
3408
+ accessibilityRole="button"
3409
+ accessibilityLabel={`Download ${file.name}`}
3410
+ accessibilityHint="Downloads media asset file"
3411
+ onPress={() =>
3412
+ downloadBundleFile(file, analysis?.scriptURL)
3413
+ }
3414
+ style={bundleStyles.downloadFileBtn}
3415
+ hitSlop={8}>
3416
+ <DownloadIcon size={14} color={AppColors.sky500} />
3417
+ </TouchableScale>
3418
+ <CopyButton
3419
+ value={() => file}
3420
+ label={t('bundle.mediaItemJson')}
3421
+ />
3422
+ </View>
3423
+ </View>
3424
+
3425
+ <View style={bundleStyles.fileCardBottom}>
3426
+ <Text style={bundleStyles.fileMetaText}>{file.meta}</Text>
3427
+ {file.advice && (
3428
+ <View
3429
+ style={[
3430
+ bundleStyles.adviceBadge,
3431
+ file.status === 'warning' && bundleStyles.adviceWarning,
3432
+ file.status === 'optimal' && bundleStyles.adviceOptimal,
3433
+ ]}>
3434
+ {file.status === 'warning' ? (
3435
+ <LightbulbIcon
3436
+ color={AppColors.amber800Warm}
3437
+ size={12}
3438
+ />
3439
+ ) : (
3440
+ <SparkleIcon color={AppColors.emerald700} size={12} />
3441
+ )}
3442
+ <Text
3443
+ style={[
3444
+ bundleStyles.adviceText,
3445
+ file.status === 'warning' && {
3446
+ color: AppColors.amber800Warm,
3447
+ },
3448
+ file.status === 'optimal' && {
3449
+ color: AppColors.emerald700,
3450
+ },
3451
+ ]}>
3452
+ {file.advice}
3453
+ </Text>
3454
+ </View>
3455
+ )}
3456
+ </View>
3457
+ </View>
3458
+ ))}
3459
+ </ScrollView>
3460
+
3461
+ {/* Sticky Total Media Size Footer */}
3462
+ <View style={bundleStyles.stickyFooterBar}>
3463
+ <View style={{flexDirection: 'row', alignItems: 'center', gap: 6}}>
3464
+ <StorageIcon color={AppColors.purple} size={14} />
3465
+ <Text style={bundleStyles.footerTitle}>
3466
+ {t('bundle.mediaAndFonts', {count: mediaFiles.length})}
3467
+ </Text>
3468
+ </View>
3469
+ <Text style={bundleStyles.footerSizeVal}>
3470
+ {t('bundle.kbMbValue', {
3471
+ kb: mediaTotalKb,
3472
+ mb: (mediaTotalKb / 1024).toFixed(2),
3473
+ })}
3474
+ </Text>
3475
+ </View>
3476
+ </View>
3477
+ )}
3478
+
3479
+ {/* ══════════════════════════════════════════════════════════════════════ */}
3480
+ {/* ── 5. TAB: OPTIMIZER CHECKLIST ─────────────────────────────────────── */}
3481
+ {/* ══════════════════════════════════════════════════════════════════════ */}
3482
+ {activeSubTab === 'optimizer' && (
3483
+ <ScrollView
3484
+ style={{flex: 1}}
3485
+ contentContainerStyle={bundleStyles.contentContainer}
3486
+ keyboardShouldPersistTaps="handled">
3487
+ <View style={bundleStyles.tipsCard}>
3488
+ <View
3489
+ style={{
3490
+ flexDirection: 'row',
3491
+ alignItems: 'center',
3492
+ justifyContent: 'space-between',
3493
+ marginBottom: 12,
3494
+ }}>
3495
+ <View
3496
+ style={{flexDirection: 'row', alignItems: 'center', gap: 6}}>
3497
+ <CircleAlertIcon color={AppColors.purple} size={18} />
3498
+ <Text style={bundleStyles.tipsHeading}>
3499
+ {t('bundle.optimizerTitle')}
3500
+ </Text>
3501
+ </View>
3502
+ <CopyButton
3503
+ value={() => [
3504
+ t('bundle.optTip1Title'),
3505
+ `${t('bundle.optTip2Title')} (${
3506
+ isHermes ? t('bundle.active') : t('bundle.actionRequired')
3507
+ })`,
3508
+ t('bundle.optTip3Title'),
3509
+ t('bundle.optTip4Title'),
3510
+ t('bundle.optTip5Title'),
3511
+ ]}
3512
+ label={t('bundle.optimizationChecklist')}
3513
+ />
3514
+ </View>
3515
+
3516
+ {[
3517
+ {
3518
+ iconType: 'image' as const,
3519
+ iconColor: AppColors.pink500,
3520
+ title: t('bundle.optTip1Title'),
3521
+ desc: t('bundle.optTip1Desc'),
3522
+ badge: t('bundle.highImpact'),
3523
+ badgeColor: AppColors.pink500,
3524
+ },
3525
+ {
3526
+ iconType: 'bolt' as const,
3527
+ iconColor: isHermes ? AppColors.emerald500 : AppColors.amber500,
3528
+ title: t('bundle.optTip2Title'),
3529
+ desc: isHermes
3530
+ ? t('bundle.optTip2DescActive')
3531
+ : t('bundle.optTip2DescInactive'),
3532
+ badge: isHermes
3533
+ ? t('bundle.active')
3534
+ : t('bundle.actionRequired'),
3535
+ badgeColor: isHermes
3536
+ ? AppColors.emerald500
3537
+ : AppColors.amber500,
3538
+ },
3539
+ {
3540
+ iconType: 'font' as const,
3541
+ iconColor: AppColors.purple500,
3542
+ title: t('bundle.optTip3Title'),
3543
+ desc: t('bundle.optTip3Desc'),
3544
+ badge: t('bundle.mediumImpact'),
3545
+ badgeColor: AppColors.purple500,
3546
+ },
3547
+ {
3548
+ iconType: 'source' as const,
3549
+ iconColor: AppColors.sky500,
3550
+ title: t('bundle.optTip4Title'),
3551
+ desc: t('bundle.optTip4Desc'),
3552
+ badge: t('bundle.bestPractice'),
3553
+ badgeColor: AppColors.sky500,
3554
+ },
3555
+ {
3556
+ iconType: 'overhead' as const,
3557
+ iconColor: AppColors.indigo500,
3558
+ title: t('bundle.optTip5Title'),
3559
+ desc: t('bundle.optTip5Desc'),
3560
+ badge: t('bundle.bestPractice'),
3561
+ badgeColor: AppColors.indigo500,
3562
+ },
3563
+ ].map((tip, tIdx) => (
3564
+ <View key={tIdx} style={bundleStyles.tipItem}>
3565
+ <View style={bundleStyles.tipItemHeader}>
3566
+ <View style={bundleStyles.tipTitleWrap}>
3567
+ <View style={bundleStyles.sNoBadge}>
3568
+ <Text style={bundleStyles.sNoText}>#{tIdx + 1}</Text>
3569
+ </View>
3570
+ <BundleCategoryIcon
3571
+ type={tip.iconType}
3572
+ size={14}
3573
+ color={tip.iconColor}
3574
+ />
3575
+ <Text style={bundleStyles.tipTitle}>{tip.title}</Text>
3576
+ </View>
3577
+ <View style={bundleStyles.tipActionWrap}>
3578
+ <View
3579
+ style={[
3580
+ bundleStyles.impactBadge,
3581
+ {
3582
+ backgroundColor: `${tip.badgeColor}18`,
3583
+ borderColor: `${tip.badgeColor}40`,
3584
+ },
3585
+ ]}>
3586
+ <Text
3587
+ style={[
3588
+ bundleStyles.impactBadgeText,
3589
+ {color: tip.badgeColor},
3590
+ ]}>
3591
+ {tip.badge}
3592
+ </Text>
3593
+ </View>
3594
+ <CopyButton
3595
+ value={() => tip}
3596
+ label={t('bundle.tipDetails')}
3597
+ />
3598
+ </View>
3599
+ </View>
3600
+ <Text style={bundleStyles.tipDesc}>{tip.desc}</Text>
3601
+ </View>
3602
+ ))}
3603
+ </View>
3604
+ </ScrollView>
3605
+ )}
3606
+ </View>
3607
+ );
3608
+ });
3609
+
3610
+ const bundleStyles = StyleSheet.create({
3611
+ topTabWrap: {
3612
+ backgroundColor: AppColors.white,
3613
+ paddingVertical: 8,
3614
+ borderBottomWidth: 1,
3615
+ borderBottomColor: AppColors.dividerColor,
3616
+ flexDirection: 'row',
3617
+ alignItems: 'center',
3618
+ paddingRight: 10,
3619
+ },
3620
+ reloadButton: {
3621
+ padding: 8,
3622
+ borderRadius: 20,
3623
+ backgroundColor: AppColors.grayBackground,
3624
+ borderWidth: 1,
3625
+ borderColor: AppColors.dividerColor,
3626
+ alignItems: 'center',
3627
+ justifyContent: 'center',
3628
+ marginLeft: 4,
3629
+ },
3630
+ analyzingContainer: {
3631
+ flex: 1,
3632
+ alignItems: 'center',
3633
+ justifyContent: 'center',
3634
+ gap: 8,
3635
+ padding: 24,
3636
+ },
3637
+ analyzingText: {
3638
+ fontFamily: AppFonts.interBold,
3639
+ fontSize: 13,
3640
+ color: AppColors.primaryBlack,
3641
+ },
3642
+ analyzingHint: {
3643
+ fontFamily: AppFonts.interRegular,
3644
+ fontSize: 11,
3645
+ color: AppColors.grayTextWeak,
3646
+ },
3647
+ subTabsScroll: {
3648
+ paddingHorizontal: 12,
3649
+ gap: 8,
3650
+ flexDirection: 'row',
3651
+ alignItems: 'center',
3652
+ },
3653
+ subTabPill: {
3654
+ flexDirection: 'row',
3655
+ alignItems: 'center',
3656
+ gap: 6,
3657
+ paddingHorizontal: 12,
3658
+ paddingVertical: 7,
3659
+ borderRadius: 20,
3660
+ backgroundColor: AppColors.grayBackground,
3661
+ borderWidth: 1,
3662
+ borderColor: AppColors.dividerColor,
3663
+ },
3664
+ subTabPillActive: {
3665
+ backgroundColor: AppColors.brandPurple,
3666
+ borderColor: AppColors.brandPurple,
3667
+ shadowColor: AppColors.brandPurple,
3668
+ shadowOffset: {width: 0, height: 2},
3669
+ shadowOpacity: 0.2,
3670
+ shadowRadius: 3,
3671
+ elevation: 2,
3672
+ },
3673
+ subTabPillText: {
3674
+ fontFamily: AppFonts.interMedium,
3675
+ fontSize: 11.5,
3676
+ color: AppColors.grayTextStrong,
3677
+ },
3678
+ subTabPillTextActive: {
3679
+ fontFamily: AppFonts.interBold,
3680
+ color: AppColors.white,
3681
+ },
3682
+ contentContainer: {
3683
+ padding: 16,
3684
+ paddingBottom: 40,
3685
+ gap: 14,
3686
+ },
3687
+ heroCard: {
3688
+ backgroundColor: AppColors.white,
3689
+ borderRadius: 14,
3690
+ padding: 16,
3691
+ borderWidth: 1,
3692
+ borderColor: AppColors.dividerColor,
3693
+ shadowColor: AppColors.primaryBlack,
3694
+ shadowOffset: {width: 0, height: 2},
3695
+ shadowOpacity: 0.04,
3696
+ shadowRadius: 6,
3697
+ elevation: 2,
3698
+ },
3699
+ heroTopRow: {
3700
+ flexDirection: 'row',
3701
+ alignItems: 'flex-start',
3702
+ gap: 12,
3703
+ marginBottom: 16,
3704
+ },
3705
+ heroIconWrap: {
3706
+ width: 44,
3707
+ height: 44,
3708
+ borderRadius: 12,
3709
+ backgroundColor: `${AppColors.brandPurple}12`,
3710
+ alignItems: 'center',
3711
+ justifyContent: 'center',
3712
+ borderWidth: 1,
3713
+ borderColor: `${AppColors.brandPurple}25`,
3714
+ },
3715
+ heroTitle: {
3716
+ fontFamily: AppFonts.interBold,
3717
+ fontSize: 16,
3718
+ color: AppColors.primaryBlack,
3719
+ },
3720
+ heroSubtitle: {
3721
+ fontFamily: AppFonts.interRegular,
3722
+ fontSize: 12,
3723
+ color: AppColors.grayTextWeak,
3724
+ marginTop: 2,
3725
+ },
3726
+ metricsGrid: {
3727
+ flexDirection: 'row',
3728
+ flexWrap: 'wrap',
3729
+ gap: 8,
3730
+ },
3731
+ metricBox: {
3732
+ flex: 1,
3733
+ minWidth: '45%',
3734
+ backgroundColor: AppColors.grayBackground,
3735
+ borderRadius: 10,
3736
+ padding: 10,
3737
+ borderWidth: 1,
3738
+ borderColor: AppColors.dividerColor,
3739
+ },
3740
+ metricLabel: {
3741
+ fontFamily: AppFonts.interBold,
3742
+ fontSize: 9.5,
3743
+ color: AppColors.grayTextWeak,
3744
+ letterSpacing: 0.5,
3745
+ },
3746
+ metricValue: {
3747
+ fontFamily: AppFonts.interBold,
3748
+ fontSize: 14,
3749
+ color: AppColors.primaryBlack,
3750
+ marginTop: 3,
3751
+ },
3752
+ metricHint: {
3753
+ fontFamily: AppFonts.interMedium,
3754
+ fontSize: 10.5,
3755
+ color: AppColors.grayText,
3756
+ marginTop: 2,
3757
+ },
3758
+ statusIndicator: {
3759
+ width: 8,
3760
+ height: 8,
3761
+ borderRadius: 4,
3762
+ },
3763
+ prodPlatformScroll: {
3764
+ flexDirection: 'row',
3765
+ alignItems: 'center',
3766
+ gap: 8,
3767
+ paddingBottom: 4,
3768
+ },
3769
+ prodPlatformBtn: {
3770
+ flexDirection: 'row',
3771
+ alignItems: 'center',
3772
+ paddingVertical: 7,
3773
+ paddingHorizontal: 12,
3774
+ borderRadius: 20,
3775
+ backgroundColor: AppColors.white,
3776
+ borderWidth: 1,
3777
+ borderColor: AppColors.dividerColor,
3778
+ gap: 6,
3779
+ },
3780
+ prodPlatformBtnActive: {
3781
+ backgroundColor: AppColors.brandPurple,
3782
+ borderColor: AppColors.brandPurple,
3783
+ shadowColor: AppColors.brandPurple,
3784
+ shadowOffset: {width: 0, height: 2},
3785
+ shadowOpacity: 0.18,
3786
+ shadowRadius: 3,
3787
+ elevation: 2,
3788
+ },
3789
+ prodPlatformBtnText: {
3790
+ fontFamily: AppFonts.interBold,
3791
+ fontSize: 11.5,
3792
+ color: AppColors.grayTextStrong,
3793
+ },
3794
+ prodPlatformBtnTextActive: {
3795
+ color: AppColors.white,
3796
+ },
3797
+ prodPlatformBadge: {
3798
+ paddingHorizontal: 6,
3799
+ paddingVertical: 2,
3800
+ borderRadius: 10,
3801
+ backgroundColor: `${AppColors.purple}14`,
3802
+ },
3803
+ prodPlatformBadgeActive: {
3804
+ backgroundColor: 'rgba(255, 255, 255, 0.25)',
3805
+ },
3806
+ prodPlatformBadgeText: {
3807
+ fontFamily: AppFonts.interBold,
3808
+ fontSize: 9.5,
3809
+ color: AppColors.purple,
3810
+ },
3811
+ prodPlatformBadgeTextActive: {
3812
+ color: AppColors.white,
3813
+ },
3814
+ sectionCard: {
3815
+ backgroundColor: AppColors.white,
3816
+ borderRadius: 14,
3817
+ padding: 14,
3818
+ borderWidth: 1,
3819
+ borderColor: AppColors.dividerColor,
3820
+ overflow: 'hidden',
3821
+ },
3822
+ sectionHeaderRow: {
3823
+ flexDirection: 'row',
3824
+ justifyContent: 'space-between',
3825
+ alignItems: 'center',
3826
+ marginBottom: 10,
3827
+ gap: 8,
3828
+ },
3829
+ sectionTitle: {
3830
+ fontFamily: AppFonts.interBold,
3831
+ fontSize: 13,
3832
+ color: AppColors.primaryBlack,
3833
+ },
3834
+ sectionSub: {
3835
+ fontFamily: AppFonts.interMedium,
3836
+ fontSize: 11,
3837
+ color: AppColors.grayTextWeak,
3838
+ },
3839
+ treemapBar: {
3840
+ flexDirection: 'row',
3841
+ borderRadius: 6,
3842
+ overflow: 'hidden',
3843
+ height: 16,
3844
+ marginBottom: 12,
3845
+ backgroundColor: AppColors.slate200,
3846
+ },
3847
+ prodTreemapBar: {
3848
+ flexDirection: 'row',
3849
+ borderRadius: 6,
3850
+ overflow: 'hidden',
3851
+ height: 16,
3852
+ marginBottom: 12,
3853
+ backgroundColor: AppColors.slate200,
3854
+ },
3855
+ legendGrid: {
3856
+ flexDirection: 'row',
3857
+ flexWrap: 'wrap',
3858
+ gap: 8,
3859
+ },
3860
+ legendItem: {
3861
+ flexDirection: 'row',
3862
+ alignItems: 'center',
3863
+ gap: 6,
3864
+ paddingHorizontal: 8,
3865
+ paddingVertical: 5,
3866
+ borderRadius: 8,
3867
+ backgroundColor: `${AppColors.slate200}40`,
3868
+ borderWidth: 1,
3869
+ borderColor: `${AppColors.dividerColor}`,
3870
+ flexShrink: 0,
3871
+ },
3872
+ legendText: {
3873
+ fontFamily: AppFonts.interMedium,
3874
+ fontSize: 11,
3875
+ color: AppColors.grayTextStrong,
3876
+ },
3877
+ legendVal: {
3878
+ fontFamily: AppFonts.interBold,
3879
+ color: AppColors.primaryBlack,
3880
+ },
3881
+ subsystemCard: {
3882
+ backgroundColor: AppColors.white,
3883
+ borderRadius: 12,
3884
+ padding: 12,
3885
+ marginBottom: 8,
3886
+ borderWidth: 1,
3887
+ borderColor: AppColors.dividerColor,
3888
+ gap: 8,
3889
+ },
3890
+ subsystemHeader: {
3891
+ flexDirection: 'row',
3892
+ alignItems: 'center',
3893
+ justifyContent: 'space-between',
3894
+ gap: 8,
3895
+ },
3896
+ subsystemTitle: {
3897
+ fontFamily: AppFonts.interBold,
3898
+ fontSize: 12.5,
3899
+ color: AppColors.primaryBlack,
3900
+ },
3901
+ subsystemBadge: {
3902
+ paddingHorizontal: 6,
3903
+ paddingVertical: 2,
3904
+ borderRadius: 6,
3905
+ },
3906
+ subsystemBadgeText: {
3907
+ fontFamily: AppFonts.interBold,
3908
+ fontSize: 10,
3909
+ },
3910
+ subsystemGrid: {
3911
+ flexDirection: 'row',
3912
+ flexWrap: 'wrap',
3913
+ gap: 6,
3914
+ },
3915
+ subsystemItem: {
3916
+ flexDirection: 'row',
3917
+ alignItems: 'center',
3918
+ gap: 5,
3919
+ paddingHorizontal: 8,
3920
+ paddingVertical: 4,
3921
+ borderRadius: 6,
3922
+ backgroundColor: AppColors.grayBackground,
3923
+ borderWidth: 1,
3924
+ borderColor: AppColors.dividerColor,
3925
+ },
3926
+ subsystemItemText: {
3927
+ fontFamily: AppFonts.interMedium,
3928
+ fontSize: 10.5,
3929
+ color: AppColors.grayTextStrong,
3930
+ },
3931
+ subsystemItemVal: {
3932
+ fontFamily: AppFonts.interBold,
3933
+ color: AppColors.primaryBlack,
3934
+ },
3935
+ catRowCard: {
3936
+ backgroundColor: AppColors.grayBackground,
3937
+ borderRadius: 12,
3938
+ padding: 12,
3939
+ marginBottom: 8,
3940
+ borderWidth: 1,
3941
+ borderColor: AppColors.dividerColor,
3942
+ gap: 8,
3943
+ },
3944
+ catRowTop: {
3945
+ flexDirection: 'row',
3946
+ alignItems: 'flex-start',
3947
+ gap: 10,
3948
+ },
3949
+ catIconBox: {
3950
+ width: 36,
3951
+ height: 36,
3952
+ borderRadius: 10,
3953
+ borderWidth: 1,
3954
+ alignItems: 'center',
3955
+ justifyContent: 'center',
3956
+ flexShrink: 0,
3957
+ },
3958
+ catRowTitleWrap: {
3959
+ flex: 1,
3960
+ minWidth: 0,
3961
+ gap: 2,
3962
+ },
3963
+ catRowTitle: {
3964
+ fontFamily: AppFonts.interBold,
3965
+ fontSize: 13,
3966
+ color: AppColors.primaryBlack,
3967
+ flex: 1,
3968
+ },
3969
+ catRowDesc: {
3970
+ fontFamily: AppFonts.interRegular,
3971
+ fontSize: 10.5,
3972
+ color: AppColors.grayTextWeak,
3973
+ lineHeight: 14.5,
3974
+ },
3975
+ catRowSize: {
3976
+ fontFamily: AppFonts.interBold,
3977
+ fontSize: 12.5,
3978
+ color: AppColors.primaryBlack,
3979
+ },
3980
+ catRowRight: {
3981
+ alignItems: 'flex-end',
3982
+ justifyContent: 'center',
3983
+ flexShrink: 0,
3984
+ minWidth: 64,
3985
+ },
3986
+ catRowPct: {
3987
+ fontFamily: AppFonts.interMedium,
3988
+ fontSize: 10,
3989
+ color: AppColors.grayTextWeak,
3990
+ },
3991
+ catPctPill: {
3992
+ paddingHorizontal: 5.5,
3993
+ paddingVertical: 1.5,
3994
+ borderRadius: 5,
3995
+ },
3996
+ catPctText: {
3997
+ fontFamily: AppFonts.interBold,
3998
+ fontSize: 9.5,
3999
+ },
4000
+ catProgressTrack: {
4001
+ height: 5,
4002
+ borderRadius: 2.5,
4003
+ backgroundColor: `${AppColors.slate200}`,
4004
+ overflow: 'hidden',
4005
+ marginTop: 2,
4006
+ },
4007
+ catProgressBar: {
4008
+ height: '100%',
4009
+ borderRadius: 2.5,
4010
+ },
4011
+ filterContainer: {
4012
+ backgroundColor: AppColors.white,
4013
+ borderRadius: 12,
4014
+ padding: 10,
4015
+ marginHorizontal: 16,
4016
+ marginTop: 12,
4017
+ borderWidth: 1,
4018
+ borderColor: AppColors.dividerColor,
4019
+ gap: 8,
4020
+ },
4021
+ searchRow: {
4022
+ flexDirection: 'row',
4023
+ alignItems: 'center',
4024
+ backgroundColor: AppColors.grayBackground,
4025
+ borderRadius: 8,
4026
+ paddingHorizontal: 10,
4027
+ height: 36,
4028
+ gap: 8,
4029
+ },
4030
+ searchInput: {
4031
+ flex: 1,
4032
+ fontFamily: AppFonts.interRegular,
4033
+ fontSize: 12,
4034
+ color: AppColors.primaryBlack,
4035
+ padding: 0,
4036
+ },
4037
+ categoryPillsScroll: {
4038
+ gap: 6,
4039
+ paddingVertical: 2,
4040
+ },
4041
+ catPill: {
4042
+ flexDirection: 'row',
4043
+ alignItems: 'center',
4044
+ gap: 4,
4045
+ paddingHorizontal: 10,
4046
+ paddingVertical: 5,
4047
+ borderRadius: 16,
4048
+ backgroundColor: AppColors.grayBackground,
4049
+ borderWidth: 1,
4050
+ borderColor: AppColors.dividerColor,
4051
+ },
4052
+ catPillActive: {
4053
+ backgroundColor: AppColors.brandPurple,
4054
+ borderColor: AppColors.brandPurple,
4055
+ },
4056
+ catPillText: {
4057
+ fontFamily: AppFonts.interMedium,
4058
+ fontSize: 11,
4059
+ color: AppColors.grayTextStrong,
4060
+ },
4061
+ catPillTextActive: {
4062
+ fontFamily: AppFonts.interBold,
4063
+ color: AppColors.white,
4064
+ },
4065
+ listHeaderCount: {
4066
+ fontFamily: AppFonts.interMedium,
4067
+ fontSize: 11,
4068
+ color: AppColors.grayTextWeak,
4069
+ paddingHorizontal: 4,
4070
+ },
4071
+ treeHeaderRow: {
4072
+ flexDirection: 'row',
4073
+ alignItems: 'center',
4074
+ justifyContent: 'space-between',
4075
+ paddingHorizontal: 2,
4076
+ marginBottom: 6,
4077
+ },
4078
+ viewModeToggleGroup: {
4079
+ flexDirection: 'row',
4080
+ backgroundColor: AppColors.grayBackground,
4081
+ borderRadius: 8,
4082
+ padding: 2,
4083
+ borderWidth: 1,
4084
+ borderColor: AppColors.dividerColor,
4085
+ },
4086
+ viewModeBtn: {
4087
+ paddingHorizontal: 10,
4088
+ paddingVertical: 5,
4089
+ borderRadius: 6,
4090
+ },
4091
+ viewModeBtnActive: {
4092
+ backgroundColor: AppColors.brandPurple,
4093
+ shadowColor: AppColors.brandPurple,
4094
+ shadowOffset: {width: 0, height: 1},
4095
+ shadowOpacity: 0.2,
4096
+ shadowRadius: 2,
4097
+ elevation: 2,
4098
+ },
4099
+ viewModeBtnText: {
4100
+ fontFamily: AppFonts.interMedium,
4101
+ fontSize: 11,
4102
+ color: AppColors.grayTextStrong,
4103
+ },
4104
+ viewModeBtnTextActive: {
4105
+ fontFamily: AppFonts.interBold,
4106
+ color: AppColors.white,
4107
+ },
4108
+ treeActionLink: {
4109
+ fontFamily: AppFonts.interBold,
4110
+ fontSize: 11.5,
4111
+ color: AppColors.purple,
4112
+ },
4113
+ treeRootCard: {
4114
+ backgroundColor: AppColors.white,
4115
+ borderRadius: 12,
4116
+ borderWidth: 1,
4117
+ borderColor: AppColors.dividerColor,
4118
+ overflow: 'hidden',
4119
+ marginBottom: 16,
4120
+ },
4121
+ treeFolderBlock: {
4122
+ borderBottomWidth: StyleSheet.hairlineWidth,
4123
+ borderBottomColor: AppColors.dividerColor,
4124
+ },
4125
+ treeFolderRow: {
4126
+ flexDirection: 'row',
4127
+ alignItems: 'center',
4128
+ justifyContent: 'space-between',
4129
+ paddingVertical: 8,
4130
+ paddingRight: 10,
4131
+ backgroundColor: `${AppColors.purple}06`,
4132
+ },
4133
+ treeRowLeft: {
4134
+ flexDirection: 'row',
4135
+ alignItems: 'center',
4136
+ gap: 6,
4137
+ flex: 1,
4138
+ marginRight: 6,
4139
+ },
4140
+ treeRowRight: {
4141
+ flexDirection: 'row',
4142
+ alignItems: 'center',
4143
+ gap: 6,
4144
+ },
4145
+ treeChevron: {
4146
+ fontSize: 12,
4147
+ color: AppColors.purple,
4148
+ fontFamily: AppFonts.interBold,
4149
+ width: 12,
4150
+ textAlign: 'center',
4151
+ },
4152
+ treeFolderName: {
4153
+ fontFamily: AppFonts.interBold,
4154
+ fontSize: 12.5,
4155
+ color: AppColors.primaryBlack,
4156
+ },
4157
+ treeCountBadge: {
4158
+ paddingHorizontal: 5,
4159
+ paddingVertical: 1,
4160
+ borderRadius: 8,
4161
+ backgroundColor: `${AppColors.purple}14`,
4162
+ },
4163
+ treeCountBadgeText: {
4164
+ fontFamily: AppFonts.interMedium,
4165
+ fontSize: 9.5,
4166
+ color: AppColors.purple,
4167
+ },
4168
+ treeFolderSize: {
4169
+ fontFamily: AppFonts.interMedium,
4170
+ fontSize: 11,
4171
+ color: AppColors.grayTextStrong,
4172
+ },
4173
+ treeChildrenContainer: {
4174
+ backgroundColor: AppColors.white,
4175
+ },
4176
+ treeFileRow: {
4177
+ flexDirection: 'row',
4178
+ alignItems: 'center',
4179
+ justifyContent: 'space-between',
4180
+ paddingVertical: 6,
4181
+ paddingRight: 10,
4182
+ borderBottomWidth: StyleSheet.hairlineWidth,
4183
+ borderBottomColor: `${AppColors.dividerColor}80`,
4184
+ backgroundColor: AppColors.white,
4185
+ },
4186
+ treeFileRowUnused: {
4187
+ backgroundColor: `${AppColors.red500}06`,
4188
+ },
4189
+ treeBranchGuide: {
4190
+ width: 12,
4191
+ alignItems: 'center',
4192
+ justifyContent: 'center',
4193
+ },
4194
+ treeBranchGuideText: {
4195
+ fontFamily: AppFonts.interRegular,
4196
+ fontSize: 11,
4197
+ color: AppColors.dividerColor,
4198
+ },
4199
+ treeFileName: {
4200
+ fontFamily: AppFonts.interMedium,
4201
+ fontSize: 12,
4202
+ color: AppColors.primaryBlack,
4203
+ flexShrink: 1,
4204
+ },
4205
+ treeUnusedPill: {
4206
+ paddingHorizontal: 5,
4207
+ paddingVertical: 1,
4208
+ borderRadius: 4,
4209
+ backgroundColor: `${AppColors.red500}18`,
4210
+ },
4211
+ treeUnusedPillText: {
4212
+ fontFamily: AppFonts.interBold,
4213
+ fontSize: 9,
4214
+ color: AppColors.red600,
4215
+ },
4216
+ treeFileSizeGroup: {
4217
+ alignItems: 'flex-end',
4218
+ },
4219
+ treeFileSizeText: {
4220
+ fontFamily: AppFonts.interMedium,
4221
+ fontSize: 11,
4222
+ color: AppColors.primaryBlack,
4223
+ },
4224
+ treeFilePctText: {
4225
+ fontFamily: AppFonts.interRegular,
4226
+ fontSize: 9,
4227
+ color: AppColors.grayTextWeak,
4228
+ },
4229
+ fileCard: {
4230
+ backgroundColor: AppColors.white,
4231
+ borderRadius: 12,
4232
+ padding: 12,
4233
+ borderWidth: 1,
4234
+ borderColor: AppColors.dividerColor,
4235
+ gap: 6,
4236
+ marginBottom: 8,
4237
+ },
4238
+ fileCardTop: {
4239
+ flexDirection: 'row',
4240
+ alignItems: 'center',
4241
+ gap: 8,
4242
+ },
4243
+ sNoBadge: {
4244
+ paddingHorizontal: 6,
4245
+ paddingVertical: 2,
4246
+ borderRadius: 4,
4247
+ backgroundColor: `${AppColors.purple}14`,
4248
+ borderWidth: 1,
4249
+ borderColor: `${AppColors.purple}30`,
4250
+ minWidth: 26,
4251
+ alignItems: 'center',
4252
+ justifyContent: 'center',
4253
+ },
4254
+ sNoText: {
4255
+ fontFamily: AppFonts.interBold,
4256
+ fontSize: 10,
4257
+ color: AppColors.purple,
4258
+ },
4259
+ extChip: {
4260
+ paddingHorizontal: 6,
4261
+ paddingVertical: 3,
4262
+ borderRadius: 5,
4263
+ borderWidth: 1,
4264
+ },
4265
+ extChipText: {
4266
+ fontFamily: AppFonts.interBold,
4267
+ fontSize: 10,
4268
+ letterSpacing: 0.4,
4269
+ },
4270
+ pkgBadge: {
4271
+ paddingHorizontal: 6,
4272
+ paddingVertical: 3,
4273
+ borderRadius: 5,
4274
+ borderWidth: 1,
4275
+ },
4276
+ pkgBadgeText: {
4277
+ fontFamily: AppFonts.interBold,
4278
+ fontSize: 9,
4279
+ letterSpacing: 0.4,
4280
+ },
4281
+ pkgLogoWrap: {
4282
+ borderRadius: 7,
4283
+ borderWidth: 1,
4284
+ alignItems: 'center',
4285
+ justifyContent: 'center',
4286
+ backgroundColor: AppColors.white,
4287
+ borderColor: AppColors.dividerColor,
4288
+ overflow: 'hidden',
4289
+ },
4290
+ pkgLogoFallbackText: {
4291
+ fontFamily: AppFonts.interBold,
4292
+ },
4293
+ mediaThumbWrap: {
4294
+ width: 32,
4295
+ height: 32,
4296
+ borderRadius: 6,
4297
+ overflow: 'hidden',
4298
+ backgroundColor: AppColors.grayBackground,
4299
+ borderWidth: 1,
4300
+ borderColor: AppColors.dividerColor,
4301
+ },
4302
+ mediaThumbImg: {
4303
+ width: '100%',
4304
+ height: '100%',
4305
+ },
4306
+ fileName: {
4307
+ fontFamily: AppFonts.interBold,
4308
+ fontSize: 12.5,
4309
+ color: AppColors.primaryBlack,
4310
+ },
4311
+ versionPill: {
4312
+ paddingHorizontal: 5,
4313
+ paddingVertical: 1,
4314
+ borderRadius: 4,
4315
+ backgroundColor: `${AppColors.purple}14`,
4316
+ borderWidth: 1,
4317
+ borderColor: `${AppColors.purple}26`,
4318
+ },
4319
+ versionPillText: {
4320
+ fontFamily: AppFonts.interMedium,
4321
+ fontSize: 10,
4322
+ color: AppColors.purple,
4323
+ },
4324
+ pkgVersion: {
4325
+ fontFamily: AppFonts.interMedium,
4326
+ fontSize: 11,
4327
+ color: AppColors.grayTextWeak,
4328
+ },
4329
+ filePath: {
4330
+ fontFamily: AppFonts.interRegular,
4331
+ fontSize: 10.5,
4332
+ color: AppColors.grayTextWeak,
4333
+ marginTop: 1,
4334
+ },
4335
+ fileSizeBox: {
4336
+ alignItems: 'flex-end',
4337
+ },
4338
+ downloadFileBtn: {
4339
+ padding: 6,
4340
+ borderRadius: 6,
4341
+ backgroundColor: `${AppColors.sky500}14`,
4342
+ alignItems: 'center',
4343
+ justifyContent: 'center',
4344
+ borderWidth: 1,
4345
+ borderColor: `${AppColors.sky500}28`,
4346
+ },
4347
+ treeActionBtn: {
4348
+ padding: 4,
4349
+ borderRadius: 4,
4350
+ backgroundColor: `${AppColors.sky500}14`,
4351
+ alignItems: 'center',
4352
+ justifyContent: 'center',
4353
+ borderWidth: 1,
4354
+ borderColor: `${AppColors.sky500}28`,
4355
+ },
4356
+ fileSizeKb: {
4357
+ fontFamily: AppFonts.interBold,
4358
+ fontSize: 12,
4359
+ color: AppColors.primaryBlack,
4360
+ },
4361
+ filePercent: {
4362
+ fontFamily: AppFonts.interMedium,
4363
+ fontSize: 10,
4364
+ color: AppColors.grayTextWeak,
4365
+ },
4366
+ fileProgressTrack: {
4367
+ height: 4,
4368
+ borderRadius: 2,
4369
+ backgroundColor: AppColors.grayBackground,
4370
+ overflow: 'hidden',
4371
+ marginVertical: 2,
4372
+ },
4373
+ fileProgressBar: {
4374
+ height: '100%',
4375
+ borderRadius: 2,
4376
+ },
4377
+ fileCardBottom: {
4378
+ gap: 4,
4379
+ },
4380
+ fileMetaText: {
4381
+ fontFamily: AppFonts.interRegular,
4382
+ fontSize: 10.5,
4383
+ color: AppColors.grayTextWeak,
4384
+ },
4385
+ typeTag: {
4386
+ backgroundColor: AppColors.grayBackground,
4387
+ paddingHorizontal: 6,
4388
+ paddingVertical: 2,
4389
+ borderRadius: 4,
4390
+ borderWidth: 1,
4391
+ borderColor: AppColors.dividerColor,
4392
+ },
4393
+ typeTagText: {
4394
+ fontFamily: AppFonts.interMedium,
4395
+ fontSize: 9.5,
4396
+ color: AppColors.grayText,
4397
+ },
4398
+ npmLinkBtn: {
4399
+ flexDirection: 'row',
4400
+ alignItems: 'center',
4401
+ gap: 3,
4402
+ paddingHorizontal: 7,
4403
+ paddingVertical: 2,
4404
+ borderRadius: 5,
4405
+ backgroundColor: AppColors.npmRedTint,
4406
+ borderWidth: 1,
4407
+ borderColor: AppColors.npmRedBorder,
4408
+ },
4409
+ npmLinkText: {
4410
+ fontFamily: AppFonts.interBold,
4411
+ fontSize: 9.5,
4412
+ color: AppColors.npmRed,
4413
+ },
4414
+ deprecatedBadge: {
4415
+ paddingHorizontal: 6,
4416
+ paddingVertical: 1.5,
4417
+ borderRadius: 4,
4418
+ backgroundColor: AppColors.red100,
4419
+ borderWidth: 1,
4420
+ borderColor: AppColors.errorBorder,
4421
+ },
4422
+ deprecatedBadgeText: {
4423
+ fontFamily: AppFonts.interBold,
4424
+ fontSize: 9,
4425
+ color: AppColors.red600,
4426
+ letterSpacing: 0.3,
4427
+ },
4428
+ updateBadge: {
4429
+ paddingHorizontal: 6,
4430
+ paddingVertical: 1.5,
4431
+ borderRadius: 4,
4432
+ backgroundColor: AppColors.green100,
4433
+ borderWidth: 1,
4434
+ borderColor: AppColors.greenBorder,
4435
+ },
4436
+ updateBadgeText: {
4437
+ fontFamily: AppFonts.interBold,
4438
+ fontSize: 9,
4439
+ color: AppColors.emerald700,
4440
+ },
4441
+ lastActiveText: {
4442
+ fontFamily: AppFonts.interRegular,
4443
+ fontSize: 10,
4444
+ color: AppColors.grayTextWeak,
4445
+ },
4446
+ deprecationBox: {
4447
+ backgroundColor: AppColors.errorCardBg,
4448
+ borderRadius: 6,
4449
+ paddingHorizontal: 8,
4450
+ paddingVertical: 5,
4451
+ borderWidth: 1,
4452
+ borderColor: AppColors.errorBorder,
4453
+ marginTop: 3,
4454
+ },
4455
+ deprecationText: {
4456
+ fontFamily: AppFonts.interMedium,
4457
+ fontSize: 10.5,
4458
+ color: AppColors.rose700,
4459
+ lineHeight: 14,
4460
+ },
4461
+ unusedBadge: {
4462
+ paddingHorizontal: 6,
4463
+ paddingVertical: 1.5,
4464
+ borderRadius: 4,
4465
+ backgroundColor: AppColors.red100,
4466
+ borderWidth: 1,
4467
+ borderColor: AppColors.errorBorder,
4468
+ },
4469
+ unusedBadgeText: {
4470
+ fontFamily: AppFonts.interBold,
4471
+ fontSize: 9.5,
4472
+ color: AppColors.red600,
4473
+ },
4474
+ consumedBadge: {
4475
+ paddingHorizontal: 6,
4476
+ paddingVertical: 1.5,
4477
+ borderRadius: 4,
4478
+ backgroundColor: AppColors.green100,
4479
+ borderWidth: 1,
4480
+ borderColor: AppColors.greenBorder,
4481
+ },
4482
+ consumedBadgeText: {
4483
+ fontFamily: AppFonts.interBold,
4484
+ fontSize: 9.5,
4485
+ color: AppColors.emerald600,
4486
+ },
4487
+ treeFolderChevronWrap: {
4488
+ width: 18,
4489
+ height: 18,
4490
+ borderRadius: 9,
4491
+ backgroundColor: `${AppColors.purple}14`,
4492
+ alignItems: 'center',
4493
+ justifyContent: 'center',
4494
+ },
4495
+ treeFileCardUnused: {
4496
+ borderColor: AppColors.errorBorder,
4497
+ backgroundColor: AppColors.red50,
4498
+ },
4499
+ adviceUnused: {
4500
+ backgroundColor: AppColors.errorCardBg,
4501
+ borderWidth: 1,
4502
+ borderColor: AppColors.errorBorder,
4503
+ },
4504
+ liveWarningCard: {
4505
+ flexDirection: 'row',
4506
+ alignItems: 'center',
4507
+ backgroundColor: AppColors.amber100,
4508
+ borderWidth: 1,
4509
+ borderColor: AppColors.amber200,
4510
+ borderRadius: 10,
4511
+ padding: 12,
4512
+ marginBottom: 12,
4513
+ },
4514
+ liveWarningTitle: {
4515
+ fontFamily: AppFonts.interBold,
4516
+ fontSize: 12,
4517
+ color: AppColors.amber800Warm,
4518
+ marginBottom: 2,
4519
+ },
4520
+ liveWarningDesc: {
4521
+ fontFamily: AppFonts.interRegular,
4522
+ fontSize: 10.5,
4523
+ lineHeight: 14,
4524
+ color: AppColors.amber800Warm,
4525
+ },
4526
+ adviceBadge: {
4527
+ flexDirection: 'row',
4528
+ alignItems: 'center',
4529
+ paddingHorizontal: 8,
4530
+ paddingVertical: 5,
4531
+ borderRadius: 6,
4532
+ marginTop: 4,
4533
+ gap: 6,
4534
+ },
4535
+ adviceWarning: {
4536
+ backgroundColor: AppColors.amber100,
4537
+ borderWidth: 1,
4538
+ borderColor: AppColors.amber200,
4539
+ },
4540
+ adviceOptimal: {
4541
+ backgroundColor: AppColors.green100,
4542
+ borderWidth: 1,
4543
+ borderColor: AppColors.greenBorder,
4544
+ },
4545
+ adviceText: {
4546
+ fontFamily: AppFonts.interMedium,
4547
+ fontSize: 10.5,
4548
+ lineHeight: 14,
4549
+ flex: 1,
4550
+ },
4551
+ impactBadge: {
4552
+ paddingHorizontal: 6,
4553
+ paddingVertical: 2,
4554
+ borderRadius: 4,
4555
+ borderWidth: 1,
4556
+ },
4557
+ impactBadgeText: {
4558
+ fontFamily: AppFonts.interBold,
4559
+ fontSize: 9,
4560
+ },
4561
+ searchHighlight: {
4562
+ backgroundColor: AppColors.yellow200,
4563
+ color: AppColors.yellow800,
4564
+ fontFamily: AppFonts.interBold,
4565
+ },
4566
+ tipsCard: {
4567
+ backgroundColor: AppColors.purple50,
4568
+ borderRadius: 14,
4569
+ padding: 14,
4570
+ borderWidth: 1,
4571
+ borderColor: AppColors.purple200,
4572
+ },
4573
+ tipsHeading: {
4574
+ fontFamily: AppFonts.interBold,
4575
+ fontSize: 13.5,
4576
+ color: AppColors.purple800,
4577
+ },
4578
+ totalSummaryCard: {
4579
+ marginTop: 10,
4580
+ backgroundColor: `${AppColors.brandPurple}0B`,
4581
+ borderRadius: 12,
4582
+ padding: 12,
4583
+ borderWidth: 1,
4584
+ borderColor: `${AppColors.brandPurple}25`,
4585
+ gap: 8,
4586
+ },
4587
+ totalSummaryRow: {
4588
+ flexDirection: 'row',
4589
+ justifyContent: 'space-between',
4590
+ alignItems: 'center',
4591
+ },
4592
+ totalSummaryLabelWrap: {
4593
+ flexDirection: 'row',
4594
+ alignItems: 'center',
4595
+ gap: 8,
4596
+ },
4597
+ totalIconBadge: {
4598
+ width: 32,
4599
+ height: 32,
4600
+ borderRadius: 8,
4601
+ backgroundColor: `${AppColors.brandPurple}15`,
4602
+ alignItems: 'center',
4603
+ justifyContent: 'center',
4604
+ },
4605
+ totalSummaryLabel: {
4606
+ fontFamily: AppFonts.interBold,
4607
+ fontSize: 11.5,
4608
+ color: AppColors.brandPurple,
4609
+ letterSpacing: 0.6,
4610
+ },
4611
+ totalSummarySub: {
4612
+ fontFamily: AppFonts.interRegular,
4613
+ fontSize: 10,
4614
+ color: AppColors.grayTextWeak,
4615
+ marginTop: 1,
4616
+ },
4617
+ totalSummaryValue: {
4618
+ fontFamily: AppFonts.interBold,
4619
+ fontSize: 15,
4620
+ color: AppColors.primaryBlack,
4621
+ },
4622
+ totalSummaryKb: {
4623
+ fontFamily: AppFonts.interMedium,
4624
+ fontSize: 10,
4625
+ color: AppColors.grayTextWeak,
4626
+ },
4627
+ totalFormulaGrid: {
4628
+ flexDirection: 'row',
4629
+ flexWrap: 'wrap',
4630
+ gap: 6,
4631
+ paddingTop: 6,
4632
+ borderTopWidth: 1,
4633
+ borderTopColor: `${AppColors.brandPurple}15`,
4634
+ },
4635
+ totalChip: {
4636
+ flexDirection: 'row',
4637
+ alignItems: 'center',
4638
+ gap: 4,
4639
+ backgroundColor: AppColors.white,
4640
+ borderRadius: 6,
4641
+ paddingHorizontal: 6,
4642
+ paddingVertical: 3,
4643
+ borderWidth: 1,
4644
+ borderColor: AppColors.dividerColor,
4645
+ },
4646
+ totalDot: {
4647
+ width: 6,
4648
+ height: 6,
4649
+ borderRadius: 3,
4650
+ },
4651
+ totalChipText: {
4652
+ fontFamily: AppFonts.interBold,
4653
+ fontSize: 9.5,
4654
+ color: AppColors.primaryBlack,
4655
+ },
4656
+ totalSummaryFormula: {
4657
+ fontFamily: AppFonts.interRegular,
4658
+ fontSize: 10.5,
4659
+ color: AppColors.grayText,
4660
+ lineHeight: 15,
4661
+ },
4662
+ stickyFooterBar: {
4663
+ flexDirection: 'row',
4664
+ justifyContent: 'space-between',
4665
+ alignItems: 'center',
4666
+ backgroundColor: AppColors.white,
4667
+ paddingHorizontal: 16,
4668
+ paddingTop: 10,
4669
+ paddingBottom: Platform.OS === 'ios' ? 24 : 12,
4670
+ borderTopWidth: 1,
4671
+ borderTopColor: AppColors.dividerColor,
4672
+ shadowColor: AppColors.primaryBlack,
4673
+ shadowOffset: {width: 0, height: -2},
4674
+ shadowOpacity: 0.05,
4675
+ shadowRadius: 4,
4676
+ elevation: 3,
4677
+ },
4678
+ footerTitle: {
4679
+ fontFamily: AppFonts.interBold,
4680
+ fontSize: 12,
4681
+ color: AppColors.primaryBlack,
4682
+ },
4683
+ footerSizeVal: {
4684
+ fontFamily: AppFonts.interBold,
4685
+ fontSize: 12.5,
4686
+ color: AppColors.purple,
4687
+ },
4688
+ tipItem: {
4689
+ marginBottom: 10,
4690
+ backgroundColor: AppColors.white,
4691
+ padding: 12,
4692
+ borderRadius: 10,
4693
+ borderWidth: 1,
4694
+ borderColor: AppColors.purple200,
4695
+ gap: 6,
4696
+ },
4697
+ tipItemHeader: {
4698
+ flexDirection: 'row',
4699
+ alignItems: 'flex-start',
4700
+ justifyContent: 'space-between',
4701
+ gap: 8,
4702
+ },
4703
+ tipTitleWrap: {
4704
+ flexDirection: 'row',
4705
+ alignItems: 'center',
4706
+ gap: 6,
4707
+ flex: 1,
4708
+ flexShrink: 1,
4709
+ },
4710
+ tipActionWrap: {
4711
+ flexDirection: 'row',
4712
+ alignItems: 'center',
4713
+ gap: 6,
4714
+ flexShrink: 0,
4715
+ },
4716
+ tipTitle: {
4717
+ fontFamily: AppFonts.interBold,
4718
+ fontSize: 12,
4719
+ color: AppColors.purple900,
4720
+ flex: 1,
4721
+ flexShrink: 1,
4722
+ },
4723
+ tipDesc: {
4724
+ fontFamily: AppFonts.interRegular,
4725
+ fontSize: 11,
4726
+ color: AppColors.gray500,
4727
+ lineHeight: 16,
4728
+ },
4729
+ });
4730
+
4731
+ export default BundleTab;