react-native-inapp-inspector 2.2.2 → 2.2.4

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 (159) hide show
  1. package/dist/commonjs/components/AnalyticsDetail.js +5 -1
  2. package/dist/commonjs/components/AnalyticsEventCard.js +2 -1
  3. package/dist/commonjs/components/DiffViewer.js +1 -1
  4. package/dist/commonjs/components/Inspector/BundleTab.js +22 -0
  5. package/dist/commonjs/components/Inspector/DeviceInfoTab.d.ts +3 -0
  6. package/dist/commonjs/components/Inspector/DeviceInfoTab.js +835 -0
  7. package/dist/commonjs/components/Inspector/InspectorHeader.js +12 -14
  8. package/dist/commonjs/components/Inspector/LogDetail.js +6 -3
  9. package/dist/commonjs/components/Inspector/MainScreen.js +7 -3
  10. package/dist/commonjs/components/Inspector/NetworkTab.js +92 -15
  11. package/dist/commonjs/components/Inspector/PerformanceTab.js +38 -12
  12. package/dist/commonjs/components/Inspector/SettingsPanel.js +440 -229
  13. package/dist/commonjs/components/Inspector/StorageTab.d.ts +3 -0
  14. package/dist/commonjs/components/Inspector/StorageTab.js +892 -0
  15. package/dist/commonjs/components/Inspector/TabBar.js +14 -0
  16. package/dist/commonjs/components/Inspector/TelemetryConsentModal.js +11 -2
  17. package/dist/commonjs/components/Inspector/UpdateAvailableModal.js +4 -1
  18. package/dist/commonjs/components/NetworkIcons.d.ts +6 -0
  19. package/dist/commonjs/components/NetworkIcons.js +30 -1
  20. package/dist/commonjs/constants/version.d.ts +1 -1
  21. package/dist/commonjs/constants/version.js +1 -1
  22. package/dist/commonjs/customHooks/bundleAnalyzer.js +44 -22
  23. package/dist/commonjs/customHooks/storageInspector.d.ts +67 -0
  24. package/dist/commonjs/customHooks/storageInspector.js +486 -0
  25. package/dist/commonjs/i18n/locales/en.json +8 -2
  26. package/dist/commonjs/index.d.ts +1 -0
  27. package/dist/commonjs/index.js +16 -1
  28. package/dist/commonjs/storage.d.ts +5 -0
  29. package/dist/commonjs/storage.js +18 -0
  30. package/dist/commonjs/types/enums.d.ts +5 -0
  31. package/dist/commonjs/types/enums.js +5 -0
  32. package/dist/esm/components/AnalyticsDetail.js +6 -2
  33. package/dist/esm/components/AnalyticsEventCard.js +2 -1
  34. package/dist/esm/components/DiffViewer.js +1 -1
  35. package/dist/esm/components/Inspector/BundleTab.js +22 -0
  36. package/dist/esm/components/Inspector/DeviceInfoTab.d.ts +3 -0
  37. package/dist/esm/components/Inspector/DeviceInfoTab.js +796 -0
  38. package/dist/esm/components/Inspector/InspectorHeader.js +13 -15
  39. package/dist/esm/components/Inspector/LogDetail.js +7 -4
  40. package/dist/esm/components/Inspector/MainScreen.js +7 -3
  41. package/dist/esm/components/Inspector/NetworkTab.js +93 -16
  42. package/dist/esm/components/Inspector/PerformanceTab.js +39 -13
  43. package/dist/esm/components/Inspector/SettingsPanel.js +441 -230
  44. package/dist/esm/components/Inspector/StorageTab.d.ts +3 -0
  45. package/dist/esm/components/Inspector/StorageTab.js +853 -0
  46. package/dist/esm/components/Inspector/TabBar.js +15 -1
  47. package/dist/esm/components/Inspector/TelemetryConsentModal.js +11 -2
  48. package/dist/esm/components/Inspector/UpdateAvailableModal.js +4 -1
  49. package/dist/esm/components/NetworkIcons.d.ts +6 -0
  50. package/dist/esm/components/NetworkIcons.js +23 -0
  51. package/dist/esm/constants/version.d.ts +1 -1
  52. package/dist/esm/constants/version.js +1 -1
  53. package/dist/esm/customHooks/bundleAnalyzer.js +44 -22
  54. package/dist/esm/customHooks/storageInspector.d.ts +67 -0
  55. package/dist/esm/customHooks/storageInspector.js +469 -0
  56. package/dist/esm/i18n/locales/en.json +8 -2
  57. package/dist/esm/index.d.ts +1 -0
  58. package/dist/esm/index.js +5 -0
  59. package/dist/esm/storage.d.ts +5 -0
  60. package/dist/esm/storage.js +5 -0
  61. package/dist/esm/types/enums.d.ts +5 -0
  62. package/dist/esm/types/enums.js +5 -0
  63. package/package.json +9 -1
  64. package/src/analytics.ts +35 -0
  65. package/src/bundle.ts +25 -0
  66. package/src/components/AnalyticsDetail.tsx +870 -0
  67. package/src/components/AnalyticsEventCard.tsx +442 -0
  68. package/src/components/AnalyticsGraph.tsx +583 -0
  69. package/src/components/AnimatedEntrance.tsx +64 -0
  70. package/src/components/AppHeaderLogo.tsx +109 -0
  71. package/src/components/BrandCircleIcon.tsx +144 -0
  72. package/src/components/BrandSquareIcon.tsx +144 -0
  73. package/src/components/CodeSnippet.tsx +725 -0
  74. package/src/components/ConsoleLogCard.tsx +628 -0
  75. package/src/components/CopyButton.tsx +87 -0
  76. package/src/components/DiffViewer.tsx +82 -0
  77. package/src/components/DomainHeader.tsx +237 -0
  78. package/src/components/EmptyState.tsx +73 -0
  79. package/src/components/EndOfListFooter.tsx +100 -0
  80. package/src/components/ErrorBoundary.tsx +688 -0
  81. package/src/components/HeadersSection.tsx +192 -0
  82. package/src/components/HighlightText.tsx +100 -0
  83. package/src/components/Inspector/AnalyticsFilterModal.tsx +1250 -0
  84. package/src/components/Inspector/AnalyticsTab.tsx +1336 -0
  85. package/src/components/Inspector/BundleTab.tsx +4760 -0
  86. package/src/components/Inspector/ConsoleTab.tsx +702 -0
  87. package/src/components/Inspector/CrashDetail.tsx +941 -0
  88. package/src/components/Inspector/CrashFilterModal.tsx +721 -0
  89. package/src/components/Inspector/CrashTab.tsx +871 -0
  90. package/src/components/Inspector/DeviceInfoTab.tsx +1204 -0
  91. package/src/components/Inspector/FabLauncher.tsx +80 -0
  92. package/src/components/Inspector/InspectorContext.tsx +28 -0
  93. package/src/components/Inspector/InspectorHeader.tsx +972 -0
  94. package/src/components/Inspector/LogDetail.tsx +1431 -0
  95. package/src/components/Inspector/MainScreen.tsx +262 -0
  96. package/src/components/Inspector/NavigationTracker.tsx +13 -0
  97. package/src/components/Inspector/NetworkDetail.tsx +794 -0
  98. package/src/components/Inspector/NetworkTab.tsx +1183 -0
  99. package/src/components/Inspector/NpmUpdateToast.tsx +392 -0
  100. package/src/components/Inspector/PerformanceTab.tsx +1928 -0
  101. package/src/components/Inspector/ReduxDetail.tsx +1651 -0
  102. package/src/components/Inspector/ReduxTab.tsx +954 -0
  103. package/src/components/Inspector/SettingsPanel.tsx +3447 -0
  104. package/src/components/Inspector/StorageTab.tsx +1048 -0
  105. package/src/components/Inspector/TabBar.tsx +207 -0
  106. package/src/components/Inspector/TelemetryConsentModal.tsx +416 -0
  107. package/src/components/Inspector/UpdateAvailableModal.tsx +557 -0
  108. package/src/components/JsonViewer.tsx +486 -0
  109. package/src/components/LogCard.tsx +491 -0
  110. package/src/components/LogSyntaxHighlighter.tsx +178 -0
  111. package/src/components/MetaAccordion.tsx +340 -0
  112. package/src/components/MiniBarChart.tsx +42 -0
  113. package/src/components/MiniLineChart.tsx +33 -0
  114. package/src/components/NetworkIcons.tsx +2221 -0
  115. package/src/components/SectionHeader.tsx +113 -0
  116. package/src/components/SegmentedTabs.tsx +83 -0
  117. package/src/components/Slider.tsx +300 -0
  118. package/src/components/SourcePageCard.tsx +148 -0
  119. package/src/components/Toast.tsx +131 -0
  120. package/src/components/TouchableScale.tsx +91 -0
  121. package/src/components/TreeNode.tsx +186 -0
  122. package/src/console.ts +24 -0
  123. package/src/constants/index.ts +38 -0
  124. package/src/constants/version.ts +3 -0
  125. package/src/crash.ts +32 -0
  126. package/src/customHooks/analyticsLogger.ts +336 -0
  127. package/src/customHooks/bundleAnalyzer.ts +1256 -0
  128. package/src/customHooks/consoleLogger.ts +497 -0
  129. package/src/customHooks/crashHandler.ts +944 -0
  130. package/src/customHooks/logFilters.ts +32 -0
  131. package/src/customHooks/networkLogger.ts +419 -0
  132. package/src/customHooks/performanceTracker.ts +1014 -0
  133. package/src/customHooks/reduxLogger.ts +406 -0
  134. package/src/customHooks/storageInspector.ts +509 -0
  135. package/src/customHooks/useAccordion.tsx +60 -0
  136. package/src/decorators/index.ts +184 -0
  137. package/src/helpers/gaAnalyticsRegistry.ts +204 -0
  138. package/src/helpers/index.ts +860 -0
  139. package/src/helpers/memoryManager.ts +138 -0
  140. package/src/helpers/searchQueryParser.ts +283 -0
  141. package/src/helpers/settingsStore.ts +171 -0
  142. package/src/helpers/telemetry.ts +505 -0
  143. package/src/helpers/toast.ts +17 -0
  144. package/src/i18n/index.ts +69 -0
  145. package/src/i18n/locales/en.json +1058 -0
  146. package/src/index.tsx +2356 -0
  147. package/src/native/NativeInspector.ts +397 -0
  148. package/src/native/NativeNetworkInspector.ts +24 -0
  149. package/src/network.ts +22 -0
  150. package/src/performance.ts +38 -0
  151. package/src/redux.ts +23 -0
  152. package/src/storage.ts +18 -0
  153. package/src/styles/AppColors.ts +408 -0
  154. package/src/styles/AppFonts.ts +8 -0
  155. package/src/styles/common.ts +209 -0
  156. package/src/styles/index.ts +1570 -0
  157. package/src/types/enums.ts +199 -0
  158. package/src/types/index.ts +34 -0
  159. package/src/types/interfaces.ts +515 -0
@@ -0,0 +1,4760 @@
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
+ <TouchableScale
1610
+ onPress={refreshAnalysis}
1611
+ style={{
1612
+ backgroundColor: `${AppColors.amber700}18`,
1613
+ paddingHorizontal: 8,
1614
+ paddingVertical: 5,
1615
+ borderRadius: 6,
1616
+ borderWidth: 1,
1617
+ borderColor: `${AppColors.amber700}30`,
1618
+ flexDirection: 'row',
1619
+ alignItems: 'center',
1620
+ gap: 4,
1621
+ }}>
1622
+ {isAnalyzing ? (
1623
+ <ActivityIndicator size="small" color={AppColors.amber700} />
1624
+ ) : (
1625
+ <>
1626
+ <RefreshCcwIcon color={AppColors.amber700} size={11} />
1627
+ <Text
1628
+ style={{
1629
+ fontSize: 10.5,
1630
+ fontFamily: AppFonts.interBold,
1631
+ color: AppColors.amber700,
1632
+ }}>
1633
+ Retry
1634
+ </Text>
1635
+ </>
1636
+ )}
1637
+ </TouchableScale>
1638
+ </View>
1639
+ </View>
1640
+ )}
1641
+
1642
+ {/* Visual Type Distribution Ratio Bar */}
1643
+ <View style={bundleStyles.sectionCard}>
1644
+ <View style={bundleStyles.sectionHeaderRow}>
1645
+ <View style={{flex: 1, paddingRight: 4}}>
1646
+ <Text style={bundleStyles.sectionTitle}>
1647
+ {t('bundle.treemapTitle')}
1648
+ </Text>
1649
+ <Text style={bundleStyles.sectionSub}>
1650
+ {t('bundle.treemapSub', {mb: summary.totalMb})}
1651
+ </Text>
1652
+ </View>
1653
+ <CopyButton
1654
+ value={() => ({
1655
+ totalMb: summary.totalMb,
1656
+ totalKb: summary.totalKb,
1657
+ nodeModules: {
1658
+ kb: summary.nodeModulesKb,
1659
+ pct: summary.nodeModulesPct,
1660
+ },
1661
+ metroRuntime: {kb: summary.metroKb, pct: summary.metroPct},
1662
+ appSource: {
1663
+ kb: summary.appSourceKb,
1664
+ pct: summary.appSourcePct,
1665
+ },
1666
+ assetsMedia: {
1667
+ kb: summary.assetsMediaKb,
1668
+ pct: summary.assetsMediaPct,
1669
+ },
1670
+ })}
1671
+ label={t('bundle.treemapJson')}
1672
+ />
1673
+ </View>
1674
+
1675
+ {/* Stacked Colored Bar for Full 6.0 MB Bundle */}
1676
+ <View style={bundleStyles.treemapBar}>
1677
+ <View
1678
+ style={{
1679
+ flex: summary.nodeModulesPct,
1680
+ backgroundColor: AppColors.indigo500,
1681
+ height: 16,
1682
+ }}
1683
+ />
1684
+ <View
1685
+ style={{
1686
+ flex: summary.metroPct,
1687
+ backgroundColor: AppColors.amber500,
1688
+ height: 16,
1689
+ }}
1690
+ />
1691
+ <View
1692
+ style={{
1693
+ flex: summary.appSourcePct,
1694
+ backgroundColor: AppColors.sky500,
1695
+ height: 16,
1696
+ }}
1697
+ />
1698
+ <View
1699
+ style={{
1700
+ flex: summary.assetsMediaPct,
1701
+ backgroundColor: AppColors.pink500,
1702
+ height: 16,
1703
+ }}
1704
+ />
1705
+ </View>
1706
+
1707
+ {/* Legend Grid */}
1708
+ <View style={bundleStyles.legendGrid}>
1709
+ <View style={bundleStyles.legendItem}>
1710
+ <BundleCategoryIcon
1711
+ type="javascript"
1712
+ size={13}
1713
+ color={AppColors.indigo500}
1714
+ />
1715
+ <Text style={bundleStyles.legendText}>
1716
+ Node Modules{' '}
1717
+ <Text style={bundleStyles.legendVal}>
1718
+ {summary.nodeModulesKb >= 1024
1719
+ ? `${(summary.nodeModulesKb / 1024).toFixed(1)} MB`
1720
+ : `${summary.nodeModulesKb} KB`}{' '}
1721
+ ({summary.nodeModulesPct}%)
1722
+ </Text>
1723
+ </Text>
1724
+ </View>
1725
+ <View style={bundleStyles.legendItem}>
1726
+ <BundleCategoryIcon
1727
+ type="overhead"
1728
+ size={13}
1729
+ color={AppColors.amber500}
1730
+ />
1731
+ <Text style={bundleStyles.legendText}>
1732
+ Metro Runtime{' '}
1733
+ <Text style={bundleStyles.legendVal}>
1734
+ {summary.metroKb >= 1024
1735
+ ? `${(summary.metroKb / 1024).toFixed(1)} MB`
1736
+ : `${summary.metroKb} KB`}{' '}
1737
+ ({summary.metroPct}%)
1738
+ </Text>
1739
+ </Text>
1740
+ </View>
1741
+ <View style={bundleStyles.legendItem}>
1742
+ <BundleCategoryIcon
1743
+ type="typescript"
1744
+ size={13}
1745
+ color={AppColors.sky500}
1746
+ />
1747
+ <Text style={bundleStyles.legendText}>
1748
+ App Source{' '}
1749
+ <Text style={bundleStyles.legendVal}>
1750
+ {summary.appSourceKb >= 1024
1751
+ ? `${(summary.appSourceKb / 1024).toFixed(1)} MB`
1752
+ : `${summary.appSourceKb} KB`}{' '}
1753
+ ({summary.appSourcePct}%)
1754
+ </Text>
1755
+ </Text>
1756
+ </View>
1757
+ <View style={bundleStyles.legendItem}>
1758
+ <BundleCategoryIcon
1759
+ type="image"
1760
+ size={13}
1761
+ color={AppColors.pink500}
1762
+ />
1763
+ <Text style={bundleStyles.legendText}>
1764
+ Assets & Media{' '}
1765
+ <Text style={bundleStyles.legendVal}>
1766
+ {summary.assetsMediaKb >= 1024
1767
+ ? `${(summary.assetsMediaKb / 1024).toFixed(1)} MB`
1768
+ : `${summary.assetsMediaKb} KB`}{' '}
1769
+ ({summary.assetsMediaPct}%)
1770
+ </Text>
1771
+ </Text>
1772
+ </View>
1773
+ </View>
1774
+ </View>
1775
+
1776
+ {/* Detailed Category Footprint Cards */}
1777
+ <View style={bundleStyles.sectionCard}>
1778
+ <View style={bundleStyles.sectionHeaderRow}>
1779
+ <Text style={bundleStyles.sectionTitle}>
1780
+ {t('bundle.categoryBreakdown')}
1781
+ </Text>
1782
+ <CopyButton
1783
+ value={() => summary}
1784
+ label={t('bundle.categoriesJson')}
1785
+ />
1786
+ </View>
1787
+
1788
+ {summary.categories.map((cat, cIdx) => (
1789
+ <Pressable
1790
+ key={cIdx}
1791
+ accessible={true}
1792
+ accessibilityRole="button"
1793
+ accessibilityLabel={`${cat.title}, ${
1794
+ cat.sizeKb >= 1024 ? `${cat.sizeMb} MB` : `${cat.sizeKb} KB`
1795
+ }, ${cat.pct}% of bundle`}
1796
+ accessibilityHint={`Double tap to view ${cat.title} details`}
1797
+ style={({pressed}) => [
1798
+ bundleStyles.catRowCard,
1799
+ pressed && {opacity: 0.85, transform: [{scale: 0.99}]},
1800
+ ]}
1801
+ onPress={() => setActiveSubTab(cat.targetTab)}>
1802
+ <View style={bundleStyles.catRowTop}>
1803
+ <View
1804
+ style={[
1805
+ bundleStyles.catIconBox,
1806
+ {
1807
+ backgroundColor: `${cat.color}15`,
1808
+ borderColor: `${cat.color}30`,
1809
+ },
1810
+ ]}>
1811
+ <BundleCategoryIcon
1812
+ type={cat.iconType}
1813
+ size={18}
1814
+ color={cat.color}
1815
+ />
1816
+ </View>
1817
+
1818
+ <View style={bundleStyles.catRowTitleWrap}>
1819
+ <View
1820
+ style={{
1821
+ flexDirection: 'row',
1822
+ alignItems: 'center',
1823
+ justifyContent: 'space-between',
1824
+ gap: 6,
1825
+ }}>
1826
+ <Text style={bundleStyles.catRowTitle} numberOfLines={1}>
1827
+ {cat.title}
1828
+ </Text>
1829
+ <View
1830
+ style={{
1831
+ flexDirection: 'row',
1832
+ alignItems: 'center',
1833
+ gap: 6,
1834
+ }}>
1835
+ <Text style={bundleStyles.catRowSize}>
1836
+ {cat.sizeKb >= 1024
1837
+ ? `${cat.sizeMb} MB`
1838
+ : `${cat.sizeKb} KB`}
1839
+ </Text>
1840
+ <View
1841
+ style={[
1842
+ bundleStyles.catPctPill,
1843
+ {backgroundColor: `${cat.color}18`},
1844
+ ]}>
1845
+ <Text
1846
+ style={[
1847
+ bundleStyles.catPctText,
1848
+ {color: cat.color},
1849
+ ]}>
1850
+ {cat.pct}%
1851
+ </Text>
1852
+ </View>
1853
+ </View>
1854
+ </View>
1855
+
1856
+ <Text style={bundleStyles.catRowDesc} numberOfLines={2}>
1857
+ {cat.count} items • {cat.desc}
1858
+ </Text>
1859
+ </View>
1860
+ </View>
1861
+
1862
+ {/* Modern Rounded Progress Bar */}
1863
+ <View style={bundleStyles.catProgressTrack}>
1864
+ <View
1865
+ style={[
1866
+ bundleStyles.catProgressBar,
1867
+ {width: `${cat.pct}%`, backgroundColor: cat.color},
1868
+ ]}
1869
+ />
1870
+ </View>
1871
+ </Pressable>
1872
+ ))}
1873
+
1874
+ {/* Executive Total Summation Card */}
1875
+ <View style={bundleStyles.totalSummaryCard}>
1876
+ <View style={bundleStyles.totalSummaryRow}>
1877
+ <View style={bundleStyles.totalSummaryLabelWrap}>
1878
+ <View style={bundleStyles.totalIconBadge}>
1879
+ <PackageIcon color={AppColors.brandPurple} size={16} />
1880
+ </View>
1881
+ <View>
1882
+ <Text style={bundleStyles.totalSummaryLabel}>
1883
+ TOTAL BUNDLE ASSETS
1884
+ </Text>
1885
+ <Text style={bundleStyles.totalSummarySub}>
1886
+ {summary.totalCount} Tracked Modules & Media Assets
1887
+ </Text>
1888
+ </View>
1889
+ </View>
1890
+ <View style={{alignItems: 'flex-end'}}>
1891
+ <Text style={bundleStyles.totalSummaryValue}>
1892
+ {summary.totalMb} MB
1893
+ </Text>
1894
+ <Text style={bundleStyles.totalSummaryKb}>
1895
+ {summary.totalKb.toLocaleString()} KB
1896
+ </Text>
1897
+ </View>
1898
+ </View>
1899
+
1900
+ {/* 4-Pill Chip Breakdown */}
1901
+ <View style={bundleStyles.totalFormulaGrid}>
1902
+ <View style={bundleStyles.totalChip}>
1903
+ <View
1904
+ style={[
1905
+ bundleStyles.totalDot,
1906
+ {backgroundColor: AppColors.indigo500},
1907
+ ]}
1908
+ />
1909
+ <Text style={bundleStyles.totalChipText}>
1910
+ NPM: {(summary.nodeModulesKb / 1024).toFixed(1)}M (
1911
+ {summary.nodeModulesPct}%)
1912
+ </Text>
1913
+ </View>
1914
+ <View style={bundleStyles.totalChip}>
1915
+ <View
1916
+ style={[
1917
+ bundleStyles.totalDot,
1918
+ {backgroundColor: AppColors.amber500},
1919
+ ]}
1920
+ />
1921
+ <Text style={bundleStyles.totalChipText}>
1922
+ Metro: {(summary.metroKb / 1024).toFixed(1)}M (
1923
+ {summary.metroPct}%)
1924
+ </Text>
1925
+ </View>
1926
+ <View style={bundleStyles.totalChip}>
1927
+ <View
1928
+ style={[
1929
+ bundleStyles.totalDot,
1930
+ {backgroundColor: AppColors.sky500},
1931
+ ]}
1932
+ />
1933
+ <Text style={bundleStyles.totalChipText}>
1934
+ Source: {(summary.appSourceKb / 1024).toFixed(1)}M (
1935
+ {summary.appSourcePct}%)
1936
+ </Text>
1937
+ </View>
1938
+ <View style={bundleStyles.totalChip}>
1939
+ <View
1940
+ style={[
1941
+ bundleStyles.totalDot,
1942
+ {backgroundColor: AppColors.pink500},
1943
+ ]}
1944
+ />
1945
+ <Text style={bundleStyles.totalChipText}>
1946
+ Media: {summary.assetsMediaKb}K ({summary.assetsMediaPct}%)
1947
+ </Text>
1948
+ </View>
1949
+ </View>
1950
+ </View>
1951
+ </View>
1952
+
1953
+ {/* Detailed Subsystem Architecture Split-up */}
1954
+ <View style={bundleStyles.sectionCard}>
1955
+ <View style={bundleStyles.sectionHeaderRow}>
1956
+ <View style={{flex: 1, paddingRight: 4}}>
1957
+ <Text style={bundleStyles.sectionTitle}>
1958
+ Bundle Subsystem Breakdown
1959
+ </Text>
1960
+ <Text style={bundleStyles.sectionSub}>
1961
+ Granular analysis of packages, application source, engine
1962
+ runtime & assets
1963
+ </Text>
1964
+ </View>
1965
+ <CopyButton
1966
+ value={() => ({
1967
+ directPackages: bundlePackages.filter(
1968
+ p => p.type === 'direct',
1969
+ ).length,
1970
+ transitivePackages: bundlePackages.filter(
1971
+ p => p.type !== 'direct',
1972
+ ).length,
1973
+ hermesEngine: isHermes ? 'Hermes (Bytecode)' : 'JSC (Source)',
1974
+ jsDevKb: summary.totalKb,
1975
+ jsHermesReleaseKb: Math.round(summary.totalKb * 0.38),
1976
+ appSourceFiles: bundleFiles.filter(
1977
+ f =>
1978
+ f.category === 'typescript' ||
1979
+ f.category === 'javascript',
1980
+ ).length,
1981
+ mediaAssets: bundleFiles.filter(
1982
+ f =>
1983
+ f.category === 'image' ||
1984
+ f.category === 'font' ||
1985
+ f.category === 'json',
1986
+ ).length,
1987
+ })}
1988
+ label="Subsystem Split JSON"
1989
+ />
1990
+ </View>
1991
+
1992
+ {/* 1. NPM Dependencies Layer */}
1993
+ <View style={bundleStyles.subsystemCard}>
1994
+ <View style={bundleStyles.subsystemHeader}>
1995
+ <View
1996
+ style={{flexDirection: 'row', alignItems: 'center', gap: 6}}>
1997
+ <BundleCategoryIcon
1998
+ type="deps"
1999
+ size={16}
2000
+ color={AppColors.indigo500}
2001
+ />
2002
+ <Text style={bundleStyles.subsystemTitle}>
2003
+ NPM & Third-Party Dependencies
2004
+ </Text>
2005
+ </View>
2006
+ <View
2007
+ style={[
2008
+ bundleStyles.subsystemBadge,
2009
+ {backgroundColor: `${AppColors.indigo500}18`},
2010
+ ]}>
2011
+ <Text
2012
+ style={[
2013
+ bundleStyles.subsystemBadgeText,
2014
+ {color: AppColors.indigo500},
2015
+ ]}>
2016
+ {(summary.nodeModulesKb / 1024).toFixed(1)} MB •{' '}
2017
+ {summary.nodeModulesPct}%
2018
+ </Text>
2019
+ </View>
2020
+ </View>
2021
+ <View style={bundleStyles.subsystemGrid}>
2022
+ <View style={bundleStyles.subsystemItem}>
2023
+ <Text style={bundleStyles.subsystemItemText}>
2024
+ Direct:{' '}
2025
+ <Text style={bundleStyles.subsystemItemVal}>
2026
+ {bundlePackages.filter(p => p.type === 'direct').length ||
2027
+ 12}{' '}
2028
+ pkgs
2029
+ </Text>
2030
+ </Text>
2031
+ </View>
2032
+ <View style={bundleStyles.subsystemItem}>
2033
+ <Text style={bundleStyles.subsystemItemText}>
2034
+ Transitive:{' '}
2035
+ <Text style={bundleStyles.subsystemItemVal}>
2036
+ {bundlePackages.filter(p => p.type !== 'direct').length ||
2037
+ 41}{' '}
2038
+ pkgs
2039
+ </Text>
2040
+ </Text>
2041
+ </View>
2042
+ <View style={bundleStyles.subsystemItem}>
2043
+ <Text style={bundleStyles.subsystemItemText}>
2044
+ Top Package:{' '}
2045
+ <Text style={bundleStyles.subsystemItemVal}>
2046
+ {bundlePackages[0]?.name || 'react-native'} (
2047
+ {bundlePackages[0]?.sizeKb || 1240} KB)
2048
+ </Text>
2049
+ </Text>
2050
+ </View>
2051
+ </View>
2052
+ </View>
2053
+
2054
+ {/* 2. App Source Code Layer */}
2055
+ <View style={bundleStyles.subsystemCard}>
2056
+ <View style={bundleStyles.subsystemHeader}>
2057
+ <View
2058
+ style={{flexDirection: 'row', alignItems: 'center', gap: 6}}>
2059
+ <BundleCategoryIcon
2060
+ type="source"
2061
+ size={16}
2062
+ color={AppColors.sky500}
2063
+ />
2064
+ <Text style={bundleStyles.subsystemTitle}>
2065
+ Application Source Code
2066
+ </Text>
2067
+ </View>
2068
+ <View
2069
+ style={[
2070
+ bundleStyles.subsystemBadge,
2071
+ {backgroundColor: `${AppColors.sky500}18`},
2072
+ ]}>
2073
+ <Text
2074
+ style={[
2075
+ bundleStyles.subsystemBadgeText,
2076
+ {color: AppColors.sky500},
2077
+ ]}>
2078
+ {(summary.appSourceKb / 1024).toFixed(1)} MB •{' '}
2079
+ {summary.appSourcePct}%
2080
+ </Text>
2081
+ </View>
2082
+ </View>
2083
+ <View style={bundleStyles.subsystemGrid}>
2084
+ <View style={bundleStyles.subsystemItem}>
2085
+ <Text style={bundleStyles.subsystemItemText}>
2086
+ TypeScript/TSX:{' '}
2087
+ <Text style={bundleStyles.subsystemItemVal}>
2088
+ {bundleFiles.filter(f => f.category === 'typescript')
2089
+ .length || 14}{' '}
2090
+ files
2091
+ </Text>
2092
+ </Text>
2093
+ </View>
2094
+ <View style={bundleStyles.subsystemItem}>
2095
+ <Text style={bundleStyles.subsystemItemText}>
2096
+ JavaScript/JSX:{' '}
2097
+ <Text style={bundleStyles.subsystemItemVal}>
2098
+ {bundleFiles.filter(f => f.category === 'javascript')
2099
+ .length || 3}{' '}
2100
+ files
2101
+ </Text>
2102
+ </Text>
2103
+ </View>
2104
+ <View style={bundleStyles.subsystemItem}>
2105
+ <Text style={bundleStyles.subsystemItemText}>
2106
+ Tracked Modules:{' '}
2107
+ <Text style={bundleStyles.subsystemItemVal}>
2108
+ {bundleFiles.length || 18} files
2109
+ </Text>
2110
+ </Text>
2111
+ </View>
2112
+ </View>
2113
+ </View>
2114
+
2115
+ {/* 3. Hermes Optimization & Bytecode Engine */}
2116
+ <View style={bundleStyles.subsystemCard}>
2117
+ <View style={bundleStyles.subsystemHeader}>
2118
+ <View
2119
+ style={{flexDirection: 'row', alignItems: 'center', gap: 6}}>
2120
+ <BundleCategoryIcon
2121
+ type="bolt"
2122
+ size={16}
2123
+ color={AppColors.emerald600}
2124
+ />
2125
+ <Text style={bundleStyles.subsystemTitle}>
2126
+ Hermes Bytecode Engine
2127
+ </Text>
2128
+ </View>
2129
+ <View
2130
+ style={[
2131
+ bundleStyles.subsystemBadge,
2132
+ {backgroundColor: `${AppColors.emerald500}18`},
2133
+ ]}>
2134
+ <Text
2135
+ style={[
2136
+ bundleStyles.subsystemBadgeText,
2137
+ {color: AppColors.emerald700},
2138
+ ]}>
2139
+ ~62% AOT Compression
2140
+ </Text>
2141
+ </View>
2142
+ </View>
2143
+ <View style={bundleStyles.subsystemGrid}>
2144
+ <View style={bundleStyles.subsystemItem}>
2145
+ <Text style={bundleStyles.subsystemItemText}>
2146
+ Dev JS Size:{' '}
2147
+ <Text style={bundleStyles.subsystemItemVal}>
2148
+ {summary.totalMb} MB
2149
+ </Text>
2150
+ </Text>
2151
+ </View>
2152
+ <View style={bundleStyles.subsystemItem}>
2153
+ <Text style={bundleStyles.subsystemItemText}>
2154
+ Hermes Bytecode:{' '}
2155
+ <Text style={bundleStyles.subsystemItemVal}>
2156
+ {((summary.totalKb * 0.38) / 1024).toFixed(2)} MB
2157
+ </Text>
2158
+ </Text>
2159
+ </View>
2160
+ <View style={bundleStyles.subsystemItem}>
2161
+ <Text style={bundleStyles.subsystemItemText}>
2162
+ Execution:{' '}
2163
+ <Text style={bundleStyles.subsystemItemVal}>
2164
+ {isHermes ? 'Hermes JSI Direct' : 'JSC Standard'}
2165
+ </Text>
2166
+ </Text>
2167
+ </View>
2168
+ </View>
2169
+ </View>
2170
+
2171
+ {/* 4. Assets & Media Layer */}
2172
+ <View style={bundleStyles.subsystemCard}>
2173
+ <View style={bundleStyles.subsystemHeader}>
2174
+ <View
2175
+ style={{flexDirection: 'row', alignItems: 'center', gap: 6}}>
2176
+ <BundleCategoryIcon
2177
+ type="media"
2178
+ size={16}
2179
+ color={AppColors.pink500}
2180
+ />
2181
+ <Text style={bundleStyles.subsystemTitle}>
2182
+ Images, Fonts & Static Assets
2183
+ </Text>
2184
+ </View>
2185
+ <View
2186
+ style={[
2187
+ bundleStyles.subsystemBadge,
2188
+ {backgroundColor: `${AppColors.pink500}18`},
2189
+ ]}>
2190
+ <Text
2191
+ style={[
2192
+ bundleStyles.subsystemBadgeText,
2193
+ {color: AppColors.pink500},
2194
+ ]}>
2195
+ {summary.assetsMediaKb} KB • {summary.assetsMediaPct}%
2196
+ </Text>
2197
+ </View>
2198
+ </View>
2199
+ <View style={bundleStyles.subsystemGrid}>
2200
+ <View style={bundleStyles.subsystemItem}>
2201
+ <Text style={bundleStyles.subsystemItemText}>
2202
+ Images & Icons:{' '}
2203
+ <Text style={bundleStyles.subsystemItemVal}>
2204
+ {bundleFiles.filter(f => f.category === 'image').length ||
2205
+ 2}{' '}
2206
+ assets
2207
+ </Text>
2208
+ </Text>
2209
+ </View>
2210
+ <View style={bundleStyles.subsystemItem}>
2211
+ <Text style={bundleStyles.subsystemItemText}>
2212
+ JSON Data:{' '}
2213
+ <Text style={bundleStyles.subsystemItemVal}>
2214
+ {bundleFiles.filter(f => f.category === 'json').length ||
2215
+ 1}{' '}
2216
+ files
2217
+ </Text>
2218
+ </Text>
2219
+ </View>
2220
+ <View style={bundleStyles.subsystemItem}>
2221
+ <Text style={bundleStyles.subsystemItemText}>
2222
+ Custom Fonts:{' '}
2223
+ <Text style={bundleStyles.subsystemItemVal}>
2224
+ {bundleFiles.filter(f => f.category === 'font').length ||
2225
+ 0}{' '}
2226
+ fonts
2227
+ </Text>
2228
+ </Text>
2229
+ </View>
2230
+ </View>
2231
+ </View>
2232
+ </View>
2233
+ </ScrollView>
2234
+ )}
2235
+
2236
+ {/* ══════════════════════════════════════════════════════════════════════ */}
2237
+ {/* ── 2. TAB: PRODUCTION BUILDS & PLATFORMS ───────────────────────────── */}
2238
+ {/* ══════════════════════════════════════════════════════════════════════ */}
2239
+ {activeSubTab === 'production' && (
2240
+ <ScrollView
2241
+ style={{flex: 1}}
2242
+ contentContainerStyle={bundleStyles.contentContainer}
2243
+ keyboardShouldPersistTaps="handled">
2244
+ {/* Platform Segmented Switcher (Horizontally Scrollable) */}
2245
+ <ScrollView
2246
+ horizontal
2247
+ showsHorizontalScrollIndicator={false}
2248
+ contentContainerStyle={bundleStyles.prodPlatformScroll}>
2249
+ {[
2250
+ {
2251
+ key: 'ios' as const,
2252
+ label: 'iOS App (.ipa)',
2253
+ badge: `${
2254
+ analysis?.production.ios.totalDownloadMb || 111.9
2255
+ } MB`,
2256
+ },
2257
+ {
2258
+ key: 'androidAab' as const,
2259
+ label: 'Android AAB (.aab)',
2260
+ badge: `${
2261
+ analysis?.production.androidAab.totalDownloadMb || 38.5
2262
+ } MB`,
2263
+ },
2264
+ {
2265
+ key: 'androidApk' as const,
2266
+ label: 'Universal APK (.apk)',
2267
+ badge: `${
2268
+ analysis?.production.androidApk.totalInstallMb || 364.0
2269
+ } MB`,
2270
+ },
2271
+ ].map(tab => {
2272
+ const isSelected = prodPlatform === tab.key;
2273
+ return (
2274
+ <TouchableScale
2275
+ key={tab.key}
2276
+ accessible={true}
2277
+ accessibilityRole="tab"
2278
+ accessibilityLabel={`${tab.label}, ${tab.badge}`}
2279
+ accessibilityState={{selected: isSelected}}
2280
+ accessibilityHint={`Switches production estimate to ${tab.label}`}
2281
+ onPress={() => setProdPlatform(tab.key)}
2282
+ style={[
2283
+ bundleStyles.prodPlatformBtn,
2284
+ isSelected && bundleStyles.prodPlatformBtnActive,
2285
+ ]}>
2286
+ <Text
2287
+ style={[
2288
+ bundleStyles.prodPlatformBtnText,
2289
+ isSelected && bundleStyles.prodPlatformBtnTextActive,
2290
+ ]}>
2291
+ {tab.label}
2292
+ </Text>
2293
+ <View
2294
+ style={[
2295
+ bundleStyles.prodPlatformBadge,
2296
+ isSelected && bundleStyles.prodPlatformBadgeActive,
2297
+ ]}>
2298
+ <Text
2299
+ style={[
2300
+ bundleStyles.prodPlatformBadgeText,
2301
+ isSelected && bundleStyles.prodPlatformBadgeTextActive,
2302
+ ]}>
2303
+ {tab.badge}
2304
+ </Text>
2305
+ </View>
2306
+ </TouchableScale>
2307
+ );
2308
+ })}
2309
+ </ScrollView>
2310
+
2311
+ {/* Production Platform Metrics Hero */}
2312
+ {analysis && (
2313
+ <View style={bundleStyles.heroCard}>
2314
+ <View style={bundleStyles.heroTopRow}>
2315
+ <View style={bundleStyles.heroIconWrap}>
2316
+ <PackageIcon color={AppColors.brandPurple} size={24} />
2317
+ </View>
2318
+ <View style={{flex: 1}}>
2319
+ <Text style={bundleStyles.heroTitle}>
2320
+ {prodPlatform === 'ios'
2321
+ ? 'iOS Production Binary Footprint'
2322
+ : prodPlatform === 'androidAab'
2323
+ ? 'Google Play App Bundle (.aab)'
2324
+ : 'Universal Standalone APK (.apk)'}
2325
+ </Text>
2326
+ <Text style={bundleStyles.heroSubtitle}>
2327
+ {prodPlatform === 'ios'
2328
+ ? 'App Store install & over-the-air cellular download estimates'
2329
+ : prodPlatform === 'androidAab'
2330
+ ? 'Optimized per-device dynamic delivery split APK architecture'
2331
+ : 'Multi-ABI universal install archive for sideloading & direct distribution'}
2332
+ </Text>
2333
+ </View>
2334
+ <CopyButton
2335
+ value={() => analysis.production[prodPlatform]}
2336
+ label="Production Binary JSON"
2337
+ />
2338
+ </View>
2339
+
2340
+ {/* 4-Stat Balanced 2x2 Metric Grid */}
2341
+ <View style={bundleStyles.metricsGrid}>
2342
+ <View style={bundleStyles.metricBox}>
2343
+ <Text style={bundleStyles.metricLabel}>INSTALL SIZE</Text>
2344
+ <Text
2345
+ style={[
2346
+ bundleStyles.metricValue,
2347
+ {color: AppColors.purple},
2348
+ ]}>
2349
+ {analysis.production[prodPlatform].totalInstallMb} MB
2350
+ </Text>
2351
+ <Text style={bundleStyles.metricHint}>
2352
+ On-device uncompressed footprint
2353
+ </Text>
2354
+ </View>
2355
+
2356
+ <View style={bundleStyles.metricBox}>
2357
+ <Text style={bundleStyles.metricLabel}>DOWNLOAD SIZE</Text>
2358
+ <Text
2359
+ style={[
2360
+ bundleStyles.metricValue,
2361
+ {color: AppColors.emerald600},
2362
+ ]}>
2363
+ ~{analysis.production[prodPlatform].totalDownloadMb} MB
2364
+ </Text>
2365
+ <Text style={bundleStyles.metricHint}>
2366
+ Store network transfer payload
2367
+ </Text>
2368
+ </View>
2369
+
2370
+ <View style={bundleStyles.metricBox}>
2371
+ <Text style={bundleStyles.metricLabel}>COMPRESSION</Text>
2372
+ <Text
2373
+ style={[
2374
+ bundleStyles.metricValue,
2375
+ {color: AppColors.sky500},
2376
+ ]}>
2377
+ {analysis.production[prodPlatform].compressionRatioPct}%
2378
+ </Text>
2379
+ <Text style={bundleStyles.metricHint}>
2380
+ Bytecode & asset ratio
2381
+ </Text>
2382
+ </View>
2383
+
2384
+ <View style={bundleStyles.metricBox}>
2385
+ <Text style={bundleStyles.metricLabel}>
2386
+ FORMAT ARCHITECTURE
2387
+ </Text>
2388
+ <Text
2389
+ style={[
2390
+ bundleStyles.metricValue,
2391
+ {color: AppColors.amber700},
2392
+ ]}>
2393
+ {prodPlatform === 'ios'
2394
+ ? 'Mach-O 64-bit'
2395
+ : prodPlatform === 'androidAab'
2396
+ ? 'Split APKs'
2397
+ : 'FAT Multi-ABI'}
2398
+ </Text>
2399
+ <Text style={bundleStyles.metricHint}>
2400
+ {prodPlatform === 'ios'
2401
+ ? 'Apple ARM64 runtime'
2402
+ : prodPlatform === 'androidAab'
2403
+ ? 'Dynamic Google Play delivery'
2404
+ : 'Sideload universal package'}
2405
+ </Text>
2406
+ </View>
2407
+ </View>
2408
+ </View>
2409
+ )}
2410
+
2411
+ {/* Component Breakdown Cards */}
2412
+ {analysis && (
2413
+ <View style={bundleStyles.sectionCard}>
2414
+ <View style={bundleStyles.sectionHeaderRow}>
2415
+ <View style={{flex: 1, paddingRight: 4}}>
2416
+ <Text style={bundleStyles.sectionTitle}>
2417
+ Binary Component Architecture
2418
+ </Text>
2419
+ <Text style={bundleStyles.sectionSub}>
2420
+ Compiled native libraries, runtime bytecodes, assets, and
2421
+ signature blocks
2422
+ </Text>
2423
+ </View>
2424
+ <CopyButton
2425
+ value={() => analysis.production[prodPlatform].components}
2426
+ label="Components JSON"
2427
+ />
2428
+ </View>
2429
+
2430
+ {/* Visual Binary Component Ratio Bar */}
2431
+ <View style={bundleStyles.prodTreemapBar}>
2432
+ {analysis.production[prodPlatform].components.map(
2433
+ (comp, cIdx) => (
2434
+ <View
2435
+ key={cIdx}
2436
+ style={{
2437
+ flex: Math.max(comp.pct, 2),
2438
+ backgroundColor: comp.color,
2439
+ height: 16,
2440
+ }}
2441
+ />
2442
+ ),
2443
+ )}
2444
+ </View>
2445
+
2446
+ {/* Production Binary Legend */}
2447
+ <View style={[bundleStyles.legendGrid, {marginBottom: 12}]}>
2448
+ {analysis.production[prodPlatform].components.map(
2449
+ (comp, cIdx) => (
2450
+ <View key={cIdx} style={bundleStyles.legendItem}>
2451
+ <View
2452
+ style={{
2453
+ width: 8,
2454
+ height: 8,
2455
+ borderRadius: 4,
2456
+ backgroundColor: comp.color,
2457
+ }}
2458
+ />
2459
+ <Text style={bundleStyles.legendText} numberOfLines={1}>
2460
+ {comp.name}{' '}
2461
+ <Text style={bundleStyles.legendVal}>
2462
+ {comp.sizeMb} MB ({comp.pct}%)
2463
+ </Text>
2464
+ </Text>
2465
+ </View>
2466
+ ),
2467
+ )}
2468
+ </View>
2469
+
2470
+ {analysis.production[prodPlatform].components.map((comp, idx) => (
2471
+ <View key={idx} style={bundleStyles.catRowCard}>
2472
+ <View style={bundleStyles.catRowTop}>
2473
+ <View
2474
+ style={[
2475
+ bundleStyles.catIconBox,
2476
+ {
2477
+ backgroundColor: `${comp.color}15`,
2478
+ borderColor: `${comp.color}30`,
2479
+ },
2480
+ ]}>
2481
+ <BundleCategoryIcon
2482
+ type={
2483
+ comp.category === 'native'
2484
+ ? 'source'
2485
+ : comp.category === 'frameworks'
2486
+ ? 'deps'
2487
+ : comp.category === 'assets'
2488
+ ? 'media'
2489
+ : comp.category === 'js'
2490
+ ? 'javascript'
2491
+ : 'overhead'
2492
+ }
2493
+ size={18}
2494
+ color={comp.color}
2495
+ />
2496
+ </View>
2497
+
2498
+ <View style={bundleStyles.catRowTitleWrap}>
2499
+ <View
2500
+ style={{
2501
+ flexDirection: 'row',
2502
+ alignItems: 'center',
2503
+ justifyContent: 'space-between',
2504
+ gap: 6,
2505
+ }}>
2506
+ <Text
2507
+ style={bundleStyles.catRowTitle}
2508
+ numberOfLines={1}>
2509
+ {comp.name}
2510
+ </Text>
2511
+ <View
2512
+ style={{
2513
+ flexDirection: 'row',
2514
+ alignItems: 'center',
2515
+ gap: 6,
2516
+ }}>
2517
+ <Text style={bundleStyles.catRowSize}>
2518
+ {comp.sizeMb} MB
2519
+ </Text>
2520
+ <View
2521
+ style={[
2522
+ bundleStyles.catPctPill,
2523
+ {backgroundColor: `${comp.color}18`},
2524
+ ]}>
2525
+ <Text
2526
+ style={[
2527
+ bundleStyles.catPctText,
2528
+ {color: comp.color},
2529
+ ]}>
2530
+ {comp.pct}%
2531
+ </Text>
2532
+ </View>
2533
+ </View>
2534
+ </View>
2535
+
2536
+ <Text style={bundleStyles.catRowDesc} numberOfLines={2}>
2537
+ {comp.description}
2538
+ </Text>
2539
+ </View>
2540
+ </View>
2541
+
2542
+ <View style={bundleStyles.catProgressTrack}>
2543
+ <View
2544
+ style={[
2545
+ bundleStyles.catProgressBar,
2546
+ {width: `${comp.pct}%`, backgroundColor: comp.color},
2547
+ ]}
2548
+ />
2549
+ </View>
2550
+
2551
+ {comp.advice ? (
2552
+ <View
2553
+ style={[
2554
+ bundleStyles.adviceBadge,
2555
+ bundleStyles.adviceOptimal,
2556
+ {marginTop: 6},
2557
+ ]}>
2558
+ <LightbulbIcon color={AppColors.emerald700} size={13} />
2559
+ <Text
2560
+ style={[
2561
+ bundleStyles.adviceText,
2562
+ {color: AppColors.emerald700, flex: 1, marginLeft: 4},
2563
+ ]}>
2564
+ {comp.advice}
2565
+ </Text>
2566
+ </View>
2567
+ ) : null}
2568
+ </View>
2569
+ ))}
2570
+
2571
+ {/* Total Platform Binary Summary with JS Source Bundle Comparison */}
2572
+ <View style={bundleStyles.totalSummaryCard}>
2573
+ <View style={bundleStyles.totalSummaryRow}>
2574
+ <View style={bundleStyles.totalSummaryLabelWrap}>
2575
+ <View style={bundleStyles.totalIconBadge}>
2576
+ <PackageIcon color={AppColors.brandPurple} size={16} />
2577
+ </View>
2578
+ <View>
2579
+ <Text style={bundleStyles.totalSummaryLabel}>
2580
+ {prodPlatform === 'ios'
2581
+ ? 'TOTAL IOS APP BINARY'
2582
+ : prodPlatform === 'androidAab'
2583
+ ? 'TOTAL ANDROID AAB BUNDLE'
2584
+ : 'TOTAL UNIVERSAL APK'}
2585
+ </Text>
2586
+ <Text style={bundleStyles.totalSummarySub}>
2587
+ {prodPlatform === 'ios'
2588
+ ? 'App Store install & cellular download estimates'
2589
+ : prodPlatform === 'androidAab'
2590
+ ? 'Google Play dynamic delivery split APK'
2591
+ : 'Universal standalone sideload package'}
2592
+ </Text>
2593
+ </View>
2594
+ </View>
2595
+ </View>
2596
+
2597
+ {/* Platform Metric Grid Chips */}
2598
+ <View style={bundleStyles.totalFormulaGrid}>
2599
+ <View style={bundleStyles.totalChip}>
2600
+ <View
2601
+ style={[
2602
+ bundleStyles.totalDot,
2603
+ {backgroundColor: AppColors.purple},
2604
+ ]}
2605
+ />
2606
+ <Text style={bundleStyles.totalChipText}>
2607
+ Install:{' '}
2608
+ {analysis.production[prodPlatform].totalInstallMb} MB
2609
+ </Text>
2610
+ </View>
2611
+ <View style={bundleStyles.totalChip}>
2612
+ <View
2613
+ style={[
2614
+ bundleStyles.totalDot,
2615
+ {backgroundColor: AppColors.sky500},
2616
+ ]}
2617
+ />
2618
+ <Text style={bundleStyles.totalChipText}>
2619
+ Download: ~
2620
+ {analysis.production[prodPlatform].totalDownloadMb} MB
2621
+ </Text>
2622
+ </View>
2623
+ <View style={bundleStyles.totalChip}>
2624
+ <View
2625
+ style={[
2626
+ bundleStyles.totalDot,
2627
+ {backgroundColor: AppColors.emerald500},
2628
+ ]}
2629
+ />
2630
+ <Text style={bundleStyles.totalChipText}>
2631
+ Compression:{' '}
2632
+ {analysis.production[prodPlatform].compressionRatioPct}%
2633
+ </Text>
2634
+ </View>
2635
+ </View>
2636
+ </View>
2637
+ </View>
2638
+ )}
2639
+ </ScrollView>
2640
+ )}
2641
+
2642
+ {/* ══════════════════════════════════════════════════════════════════════ */}
2643
+ {/* ── 2. TAB: FILES BREAKDOWN ─────────────────────────────────────────── */}
2644
+ {/* ══════════════════════════════════════════════════════════════════════ */}
2645
+ {activeSubTab === 'files' && (
2646
+ <View style={{flex: 1}}>
2647
+ {/* Search & Category Filter */}
2648
+ <View style={bundleStyles.filterContainer}>
2649
+ <View style={bundleStyles.searchRow}>
2650
+ <SearchIcon color={AppColors.grayTextWeak} size={15} />
2651
+ <TextInput
2652
+ accessible={true}
2653
+ accessibilityRole="search"
2654
+ accessibilityLabel="Search files"
2655
+ accessibilityHint="Type a filename, path, or extension to filter"
2656
+ placeholder={t('bundle.searchFilesPlaceholder')}
2657
+ placeholderTextColor={AppColors.grayTextWeak}
2658
+ value={search}
2659
+ onChangeText={setSearch}
2660
+ style={bundleStyles.searchInput}
2661
+ autoCorrect={false}
2662
+ autoCapitalize="none"
2663
+ />
2664
+ {search.length > 0 && (
2665
+ <Pressable
2666
+ accessible={true}
2667
+ accessibilityRole="button"
2668
+ accessibilityLabel="Clear search input"
2669
+ onPress={() => setSearch('')}
2670
+ hitSlop={10}>
2671
+ <ClearIcon color={AppColors.grayTextWeak} size={14} />
2672
+ </Pressable>
2673
+ )}
2674
+ <CopyButton
2675
+ value={() => filteredFiles}
2676
+ label={t('bundle.filteredFilesJson')}
2677
+ />
2678
+ </View>
2679
+
2680
+ {/* Category Filter Chips */}
2681
+ <ScrollView
2682
+ horizontal
2683
+ showsHorizontalScrollIndicator={false}
2684
+ contentContainerStyle={bundleStyles.categoryPillsScroll}>
2685
+ {CATEGORY_TABS.map(tab => {
2686
+ const isActive = activeCategory === tab.key;
2687
+ const count =
2688
+ tab.key === 'ALL'
2689
+ ? bundleFiles.length
2690
+ : tab.key === 'UNUSED'
2691
+ ? bundleFiles.filter(f => f.isConsumed === false).length
2692
+ : tab.key === 'CONSUMED'
2693
+ ? bundleFiles.filter(f => f.isConsumed !== false).length
2694
+ : bundleFiles.filter(f => f.category === tab.key).length;
2695
+
2696
+ return (
2697
+ <TouchableScale
2698
+ key={tab.key}
2699
+ accessible={true}
2700
+ accessibilityRole="button"
2701
+ accessibilityLabel={`Filter by ${t(
2702
+ tab.labelKey,
2703
+ )}, ${count} items`}
2704
+ accessibilityState={{selected: isActive}}
2705
+ accessibilityHint={`Filters files list to ${t(
2706
+ tab.labelKey,
2707
+ )}`}
2708
+ onPress={() => setActiveCategory(tab.key)}
2709
+ style={[
2710
+ bundleStyles.catPill,
2711
+ isActive && bundleStyles.catPillActive,
2712
+ tab.key === 'UNUSED' &&
2713
+ isActive && {
2714
+ backgroundColor: AppColors.red500,
2715
+ borderColor: AppColors.red600,
2716
+ },
2717
+ tab.key === 'CONSUMED' &&
2718
+ isActive && {
2719
+ backgroundColor: AppColors.emerald600,
2720
+ borderColor: AppColors.emerald700,
2721
+ },
2722
+ ]}>
2723
+ <BundleCategoryIcon
2724
+ type={tab.iconType}
2725
+ size={12}
2726
+ color={
2727
+ isActive ? AppColors.white : AppColors.grayTextStrong
2728
+ }
2729
+ />
2730
+ <Text
2731
+ style={[
2732
+ bundleStyles.catPillText,
2733
+ isActive && bundleStyles.catPillTextActive,
2734
+ ]}>
2735
+ {t(tab.labelKey)} ({count})
2736
+ </Text>
2737
+ </TouchableScale>
2738
+ );
2739
+ })}
2740
+ </ScrollView>
2741
+ </View>
2742
+
2743
+ {/* Files List / Tree Content */}
2744
+ <ScrollView
2745
+ style={{flex: 1}}
2746
+ contentContainerStyle={bundleStyles.contentContainer}
2747
+ keyboardShouldPersistTaps="handled">
2748
+ {/* View Mode Toggle Header Bar */}
2749
+ <View style={bundleStyles.treeHeaderRow}>
2750
+ <View style={bundleStyles.viewModeToggleGroup}>
2751
+ <TouchableScale
2752
+ accessible={true}
2753
+ accessibilityRole="button"
2754
+ accessibilityLabel="Tree folder view"
2755
+ accessibilityState={{selected: filesViewMode === 'tree'}}
2756
+ onPress={() => setFilesViewMode('tree')}
2757
+ style={[
2758
+ bundleStyles.viewModeBtn,
2759
+ filesViewMode === 'tree' && bundleStyles.viewModeBtnActive,
2760
+ ]}>
2761
+ <Text
2762
+ style={[
2763
+ bundleStyles.viewModeBtnText,
2764
+ filesViewMode === 'tree' &&
2765
+ bundleStyles.viewModeBtnTextActive,
2766
+ ]}>
2767
+ {t('bundle.treeView')}
2768
+ </Text>
2769
+ </TouchableScale>
2770
+ <TouchableScale
2771
+ accessible={true}
2772
+ accessibilityRole="button"
2773
+ accessibilityLabel="Flat list view"
2774
+ accessibilityState={{selected: filesViewMode === 'list'}}
2775
+ onPress={() => setFilesViewMode('list')}
2776
+ style={[
2777
+ bundleStyles.viewModeBtn,
2778
+ filesViewMode === 'list' && bundleStyles.viewModeBtnActive,
2779
+ ]}>
2780
+ <Text
2781
+ style={[
2782
+ bundleStyles.viewModeBtnText,
2783
+ filesViewMode === 'list' &&
2784
+ bundleStyles.viewModeBtnTextActive,
2785
+ ]}>
2786
+ {t('bundle.flatList')}
2787
+ </Text>
2788
+ </TouchableScale>
2789
+ </View>
2790
+
2791
+ {filesViewMode === 'tree' ? (
2792
+ <View
2793
+ style={{flexDirection: 'row', alignItems: 'center', gap: 8}}>
2794
+ <TouchableScale
2795
+ accessible={true}
2796
+ accessibilityRole="button"
2797
+ accessibilityLabel="Expand all folders"
2798
+ onPress={expandAllFolders}>
2799
+ <Text style={bundleStyles.treeActionLink}>
2800
+ {t('bundle.expand')}
2801
+ </Text>
2802
+ </TouchableScale>
2803
+ <Text style={{color: AppColors.grayTextWeak, fontSize: 11}}>
2804
+
2805
+ </Text>
2806
+ <TouchableScale
2807
+ accessible={true}
2808
+ accessibilityRole="button"
2809
+ accessibilityLabel="Collapse all folders"
2810
+ onPress={collapseAllFolders}>
2811
+ <Text style={bundleStyles.treeActionLink}>
2812
+ {t('bundle.collapse')}
2813
+ </Text>
2814
+ </TouchableScale>
2815
+ </View>
2816
+ ) : (
2817
+ <Text style={bundleStyles.listHeaderCount}>
2818
+ {t('bundle.showingFilesOf', {
2819
+ shown: filteredFiles.length,
2820
+ total: bundleFiles.length,
2821
+ })}
2822
+ </Text>
2823
+ )}
2824
+ </View>
2825
+
2826
+ {/* ── MODE 1: DIRECTORY TREE VIEW ── */}
2827
+ {filesViewMode === 'tree' ? (
2828
+ <View style={bundleStyles.treeRootCard}>
2829
+ {fileTreeNodes.map((node, nIdx) => (
2830
+ <BundleTreeNodeView
2831
+ key={node.id}
2832
+ node={node}
2833
+ level={0}
2834
+ search={search}
2835
+ totalBundleKb={summary.totalKb}
2836
+ expandedFolders={expandedFolders}
2837
+ toggleFolder={toggleFolder}
2838
+ isLastChild={nIdx === fileTreeNodes.length - 1}
2839
+ onDownload={f => downloadBundleFile(f, analysis?.scriptURL)}
2840
+ />
2841
+ ))}
2842
+ </View>
2843
+ ) : (
2844
+ /* ── MODE 2: FLAT LIST VIEW ── */
2845
+ filteredFiles.map((file, index) => {
2846
+ const categoryMeta =
2847
+ CATEGORY_COLORS[file.category] || CATEGORY_COLORS.typescript;
2848
+ const pctOfTotal = (
2849
+ (file.sizeKb / summary.totalKb) *
2850
+ 100
2851
+ ).toFixed(1);
2852
+ const isUnused = file.isConsumed === false;
2853
+
2854
+ return (
2855
+ <View
2856
+ key={file.id}
2857
+ style={[
2858
+ bundleStyles.fileCard,
2859
+ isUnused && bundleStyles.treeFileCardUnused,
2860
+ ]}>
2861
+ <View style={bundleStyles.fileCardTop}>
2862
+ <View style={bundleStyles.sNoBadge}>
2863
+ <Text style={bundleStyles.sNoText}>#{index + 1}</Text>
2864
+ </View>
2865
+
2866
+ <MediaPreviewThumbnail file={file} />
2867
+
2868
+ <View style={{flex: 1, paddingHorizontal: 4}}>
2869
+ <View
2870
+ style={{
2871
+ flexDirection: 'row',
2872
+ alignItems: 'center',
2873
+ gap: 6,
2874
+ flexWrap: 'wrap',
2875
+ }}>
2876
+ <HighlightText
2877
+ text={file.name}
2878
+ search={search}
2879
+ style={bundleStyles.fileName}
2880
+ highlightStyle={bundleStyles.searchHighlight}
2881
+ />
2882
+ {isUnused ? (
2883
+ <View style={bundleStyles.unusedBadge}>
2884
+ <Text style={bundleStyles.unusedBadgeText}>
2885
+ {t('bundle.notConsumed')}
2886
+ </Text>
2887
+ </View>
2888
+ ) : (
2889
+ <View style={bundleStyles.consumedBadge}>
2890
+ <Text style={bundleStyles.consumedBadgeText}>
2891
+ {t('bundle.consumed')}
2892
+ </Text>
2893
+ </View>
2894
+ )}
2895
+ </View>
2896
+ <HighlightText
2897
+ text={file.path}
2898
+ search={search}
2899
+ style={bundleStyles.filePath}
2900
+ highlightStyle={bundleStyles.searchHighlight}
2901
+ numberOfLines={1}
2902
+ />
2903
+ </View>
2904
+
2905
+ <View
2906
+ style={{
2907
+ flexDirection: 'row',
2908
+ alignItems: 'center',
2909
+ gap: 6,
2910
+ }}>
2911
+ <View style={bundleStyles.fileSizeBox}>
2912
+ <Text
2913
+ style={[
2914
+ bundleStyles.fileSizeKb,
2915
+ isUnused && {color: AppColors.amber800Warm},
2916
+ ]}>
2917
+ {file.sizeKb >= 1024
2918
+ ? t('bundle.fileSizeMb', {
2919
+ size: (file.sizeKb / 1024).toFixed(2),
2920
+ })
2921
+ : t('bundle.fileSizeKb', {size: file.sizeKb})}
2922
+ </Text>
2923
+ <Text style={bundleStyles.filePercent}>
2924
+ {pctOfTotal}%
2925
+ </Text>
2926
+ </View>
2927
+ <TouchableScale
2928
+ onPress={() =>
2929
+ downloadBundleFile(file, analysis?.scriptURL)
2930
+ }
2931
+ style={bundleStyles.downloadFileBtn}
2932
+ hitSlop={8}>
2933
+ <DownloadIcon size={14} color={AppColors.sky500} />
2934
+ </TouchableScale>
2935
+ <CopyButton
2936
+ value={() => file}
2937
+ label={t('bundle.fileDetailsJson')}
2938
+ />
2939
+ </View>
2940
+ </View>
2941
+
2942
+ <View style={bundleStyles.fileProgressTrack}>
2943
+ <View
2944
+ style={[
2945
+ bundleStyles.fileProgressBar,
2946
+ {
2947
+ width: `${Math.min(
2948
+ 100,
2949
+ Math.max(4, Number(pctOfTotal) * 4),
2950
+ )}%`,
2951
+ backgroundColor: isUnused
2952
+ ? AppColors.red500
2953
+ : categoryMeta.color,
2954
+ },
2955
+ ]}
2956
+ />
2957
+ </View>
2958
+
2959
+ <View style={bundleStyles.fileCardBottom}>
2960
+ <Text style={bundleStyles.fileMetaText}>{file.meta}</Text>
2961
+ {file.advice && (
2962
+ <View
2963
+ style={[
2964
+ bundleStyles.adviceBadge,
2965
+ file.status === 'warning' &&
2966
+ bundleStyles.adviceWarning,
2967
+ file.status === 'optimal' &&
2968
+ bundleStyles.adviceOptimal,
2969
+ isUnused && bundleStyles.adviceUnused,
2970
+ ]}>
2971
+ {isUnused ? (
2972
+ <TrashIcon color={AppColors.red600} size={12} />
2973
+ ) : file.status === 'warning' ? (
2974
+ <LightbulbIcon
2975
+ color={AppColors.amber800Warm}
2976
+ size={12}
2977
+ />
2978
+ ) : (
2979
+ <SparkleIcon
2980
+ color={AppColors.emerald700}
2981
+ size={12}
2982
+ />
2983
+ )}
2984
+ <Text
2985
+ style={[
2986
+ bundleStyles.adviceText,
2987
+ file.status === 'warning' && {
2988
+ color: AppColors.amber800Warm,
2989
+ },
2990
+ file.status === 'optimal' && {
2991
+ color: AppColors.emerald700,
2992
+ },
2993
+ isUnused && {color: AppColors.red600},
2994
+ ]}>
2995
+ {file.advice}
2996
+ </Text>
2997
+ </View>
2998
+ )}
2999
+ </View>
3000
+ </View>
3001
+ );
3002
+ })
3003
+ )}
3004
+ </ScrollView>
3005
+
3006
+ {/* Sticky Total Bundle Size Footer */}
3007
+ <View style={bundleStyles.stickyFooterBar}>
3008
+ <View style={{flexDirection: 'row', alignItems: 'center', gap: 6}}>
3009
+ <LayersIcon color={AppColors.purple} size={14} />
3010
+ <Text style={bundleStyles.footerTitle}>
3011
+ {t('bundle.filesOf', {
3012
+ count: filteredFiles.length,
3013
+ total: summary.totalCount,
3014
+ })}
3015
+ </Text>
3016
+ </View>
3017
+ <Text style={bundleStyles.footerSizeVal}>
3018
+ {t('bundle.kbMbValue', {
3019
+ kb: filteredTotalKb,
3020
+ mb: (filteredTotalKb / 1024).toFixed(2),
3021
+ })}
3022
+ </Text>
3023
+ </View>
3024
+ </View>
3025
+ )}
3026
+
3027
+ {/* ══════════════════════════════════════════════════════════════════════ */}
3028
+ {/* ── 3. TAB: PACKAGES & NODE_MODULES ─────────────────────────────────── */}
3029
+ {/* ══════════════════════════════════════════════════════════════════════ */}
3030
+ {activeSubTab === 'packages' && (
3031
+ <View style={{flex: 1}}>
3032
+ <ScrollView
3033
+ style={{flex: 1}}
3034
+ contentContainerStyle={bundleStyles.contentContainer}
3035
+ keyboardShouldPersistTaps="handled">
3036
+ <View style={bundleStyles.filterContainer}>
3037
+ <View style={bundleStyles.searchRow}>
3038
+ <SearchIcon color={AppColors.grayTextWeak} size={15} />
3039
+ <TextInput
3040
+ accessible={true}
3041
+ accessibilityRole="search"
3042
+ accessibilityLabel="Search packages"
3043
+ accessibilityHint="Type a package name or description to filter"
3044
+ placeholder={t('bundle.searchPackagesPlaceholder')}
3045
+ placeholderTextColor={AppColors.grayTextWeak}
3046
+ value={search}
3047
+ onChangeText={setSearch}
3048
+ style={bundleStyles.searchInput}
3049
+ autoCorrect={false}
3050
+ autoCapitalize="none"
3051
+ />
3052
+ {search.length > 0 && (
3053
+ <Pressable
3054
+ accessible={true}
3055
+ accessibilityRole="button"
3056
+ accessibilityLabel="Clear search input"
3057
+ onPress={() => setSearch('')}
3058
+ hitSlop={10}>
3059
+ <ClearIcon color={AppColors.grayTextWeak} size={14} />
3060
+ </Pressable>
3061
+ )}
3062
+ <CopyButton
3063
+ value={() => filteredPackages}
3064
+ label={t('bundle.dependenciesJson')}
3065
+ />
3066
+ </View>
3067
+ </View>
3068
+
3069
+ <Text style={bundleStyles.listHeaderCount}>
3070
+ {t('bundle.showingDependencies', {
3071
+ count: filteredPackages.length,
3072
+ })}
3073
+ </Text>
3074
+
3075
+ {filteredPackages.map((pkg, index) => {
3076
+ const cleanInstalledVersion = pkg.version
3077
+ ? pkg.version.replace(/^\^/, '')
3078
+ : '';
3079
+ const hasUpdate =
3080
+ !!pkg.latestVersion &&
3081
+ !!cleanInstalledVersion &&
3082
+ pkg.latestVersion !== cleanInstalledVersion;
3083
+
3084
+ return (
3085
+ <View
3086
+ key={pkg.id}
3087
+ style={[
3088
+ bundleStyles.fileCard,
3089
+ pkg.isDeprecated && {
3090
+ borderColor: AppColors.errorBorder,
3091
+ backgroundColor: AppColors.errorCardBg,
3092
+ },
3093
+ ]}>
3094
+ {/* Header Row: #s.no + Logo + Package Name + Version Pill + Update/Deprecated Badges + Size & Copy */}
3095
+ <View style={bundleStyles.fileCardTop}>
3096
+ <View style={bundleStyles.sNoBadge}>
3097
+ <Text style={bundleStyles.sNoText}>#{index + 1}</Text>
3098
+ </View>
3099
+
3100
+ <PackageLogoRenderer
3101
+ name={pkg.name}
3102
+ color={pkg.color}
3103
+ size={28}
3104
+ />
3105
+
3106
+ <View style={{flex: 1, paddingHorizontal: 4}}>
3107
+ <View
3108
+ style={{
3109
+ flexDirection: 'row',
3110
+ alignItems: 'center',
3111
+ gap: 6,
3112
+ flexWrap: 'wrap',
3113
+ }}>
3114
+ <HighlightText
3115
+ text={pkg.name}
3116
+ search={search}
3117
+ style={bundleStyles.fileName}
3118
+ highlightStyle={bundleStyles.searchHighlight}
3119
+ />
3120
+ <View style={bundleStyles.versionPill}>
3121
+ <Text style={bundleStyles.versionPillText}>
3122
+ {cleanInstalledVersion
3123
+ ? t('bundle.versionPrefix', {
3124
+ version: cleanInstalledVersion,
3125
+ })
3126
+ : t('bundle.bundled')}
3127
+ </Text>
3128
+ </View>
3129
+
3130
+ {pkg.isDeprecated ? (
3131
+ <View style={bundleStyles.deprecatedBadge}>
3132
+ <Text style={bundleStyles.deprecatedBadgeText}>
3133
+ {t('bundle.deprecated')}
3134
+ </Text>
3135
+ </View>
3136
+ ) : hasUpdate ? (
3137
+ <View style={bundleStyles.updateBadge}>
3138
+ <Text style={bundleStyles.updateBadgeText}>
3139
+ {t('bundle.updateAvailable', {
3140
+ version: pkg.latestVersion,
3141
+ })}
3142
+ </Text>
3143
+ </View>
3144
+ ) : cleanInstalledVersion ? (
3145
+ <View
3146
+ style={[
3147
+ bundleStyles.updateBadge,
3148
+ {
3149
+ backgroundColor: AppColors.gray100,
3150
+ borderColor: AppColors.gray200,
3151
+ },
3152
+ ]}>
3153
+ <Text
3154
+ style={[
3155
+ bundleStyles.updateBadgeText,
3156
+ {color: AppColors.gray600},
3157
+ ]}>
3158
+ {t('bundle.upToDate')}
3159
+ </Text>
3160
+ </View>
3161
+ ) : (
3162
+ <View
3163
+ style={[
3164
+ bundleStyles.updateBadge,
3165
+ {
3166
+ backgroundColor: AppColors.gray100,
3167
+ borderColor: AppColors.gray200,
3168
+ },
3169
+ ]}>
3170
+ <Text
3171
+ style={[
3172
+ bundleStyles.updateBadgeText,
3173
+ {color: AppColors.gray600},
3174
+ ]}>
3175
+ {t('bundle.bundledBadge')}
3176
+ </Text>
3177
+ </View>
3178
+ )}
3179
+ </View>
3180
+ </View>
3181
+
3182
+ <View
3183
+ style={{
3184
+ flexDirection: 'row',
3185
+ alignItems: 'center',
3186
+ gap: 6,
3187
+ }}>
3188
+ <View style={bundleStyles.fileSizeBox}>
3189
+ <Text style={bundleStyles.fileSizeKb}>
3190
+ {t('bundle.fileSizeKb', {size: pkg.sizeKb})}
3191
+ </Text>
3192
+ <Text style={bundleStyles.filePercent}>
3193
+ {pkg.percentage}%
3194
+ </Text>
3195
+ </View>
3196
+ <CopyButton
3197
+ value={() => pkg}
3198
+ label={t('bundle.packageDetailsJson')}
3199
+ />
3200
+ </View>
3201
+ </View>
3202
+
3203
+ {/* Subtitle Row: Category Chip + Description */}
3204
+ <View
3205
+ style={{
3206
+ flexDirection: 'row',
3207
+ alignItems: 'center',
3208
+ gap: 6,
3209
+ marginTop: 4,
3210
+ marginBottom: 2,
3211
+ }}>
3212
+ <View
3213
+ style={[
3214
+ bundleStyles.pkgBadge,
3215
+ {
3216
+ backgroundColor: `${pkg.color}1F`,
3217
+ borderColor: `${pkg.color}4D`,
3218
+ },
3219
+ ]}>
3220
+ <Text
3221
+ style={[bundleStyles.pkgBadgeText, {color: pkg.color}]}>
3222
+ {pkg.category.toUpperCase()}
3223
+ </Text>
3224
+ </View>
3225
+ <Text
3226
+ style={[bundleStyles.filePath, {flex: 1, marginTop: 0}]}
3227
+ numberOfLines={1}>
3228
+ {pkg.description}
3229
+ </Text>
3230
+ </View>
3231
+
3232
+ {/* Deprecation Warning Banner if deprecated */}
3233
+ {pkg.isDeprecated && pkg.deprecationReason && (
3234
+ <View style={bundleStyles.deprecationBox}>
3235
+ <View
3236
+ style={{
3237
+ flexDirection: 'row',
3238
+ alignItems: 'center',
3239
+ gap: 5,
3240
+ }}>
3241
+ <CircleAlertIcon
3242
+ color={AppColors.errorColor}
3243
+ size={12}
3244
+ />
3245
+ <Text style={bundleStyles.deprecationText}>
3246
+ {pkg.deprecationReason}
3247
+ </Text>
3248
+ </View>
3249
+ </View>
3250
+ )}
3251
+
3252
+ <View style={bundleStyles.fileProgressTrack}>
3253
+ <View
3254
+ style={[
3255
+ bundleStyles.fileProgressBar,
3256
+ {
3257
+ width: `${pkg.percentage * 2}%`,
3258
+ backgroundColor: pkg.isDeprecated
3259
+ ? AppColors.red500
3260
+ : pkg.color,
3261
+ },
3262
+ ]}
3263
+ />
3264
+ </View>
3265
+
3266
+ {/* Footer Row: Type Tag + Last Active + NPM Link Button */}
3267
+ <View
3268
+ style={{
3269
+ flexDirection: 'row',
3270
+ justifyContent: 'space-between',
3271
+ alignItems: 'center',
3272
+ marginTop: 3,
3273
+ }}>
3274
+ <View
3275
+ style={{
3276
+ flexDirection: 'row',
3277
+ alignItems: 'center',
3278
+ gap: 8,
3279
+ }}>
3280
+ <View style={bundleStyles.typeTag}>
3281
+ <Text style={bundleStyles.typeTagText}>
3282
+ {pkg.type === 'direct'
3283
+ ? t('bundle.direct')
3284
+ : t('bundle.transitive')}
3285
+ </Text>
3286
+ </View>
3287
+ {pkg.lastActive && (
3288
+ <View
3289
+ style={{
3290
+ flexDirection: 'row',
3291
+ alignItems: 'center',
3292
+ gap: 4,
3293
+ }}>
3294
+ <ClockIcon color={AppColors.grayTextWeak} size={11} />
3295
+ <Text style={bundleStyles.lastActiveText}>
3296
+ {pkg.lastActive}
3297
+ </Text>
3298
+ </View>
3299
+ )}
3300
+ </View>
3301
+
3302
+ <View
3303
+ style={{
3304
+ flexDirection: 'row',
3305
+ alignItems: 'center',
3306
+ gap: 8,
3307
+ }}>
3308
+ <Text style={bundleStyles.fileMetaText}>
3309
+ {t('bundle.minified', {size: pkg.sizeKb})}
3310
+ </Text>
3311
+ <TouchableScale
3312
+ accessible={true}
3313
+ accessibilityRole="link"
3314
+ accessibilityLabel={`Open ${pkg.name} on NPM`}
3315
+ accessibilityHint="Opens NPM package page in browser"
3316
+ onPress={() => {
3317
+ const url =
3318
+ pkg.npmUrl ||
3319
+ `https://www.npmjs.com/package/${pkg.name}`;
3320
+ Linking.openURL(url).catch(() => {});
3321
+ }}
3322
+ style={bundleStyles.npmLinkBtn}>
3323
+ <Text style={bundleStyles.npmLinkText}>
3324
+ {t('bundle.npmLink')}
3325
+ </Text>
3326
+ </TouchableScale>
3327
+ </View>
3328
+ </View>
3329
+ </View>
3330
+ );
3331
+ })}
3332
+ </ScrollView>
3333
+
3334
+ {/* Sticky Total Package Size Footer */}
3335
+ <View style={bundleStyles.stickyFooterBar}>
3336
+ <View style={{flexDirection: 'row', alignItems: 'center', gap: 6}}>
3337
+ <PackageIcon color={AppColors.purple} size={14} />
3338
+ <Text style={bundleStyles.footerTitle}>
3339
+ {t('bundle.dependenciesCount', {
3340
+ count: filteredPackages.length,
3341
+ })}
3342
+ </Text>
3343
+ </View>
3344
+ <Text style={bundleStyles.footerSizeVal}>
3345
+ {t('bundle.kbMbValue', {
3346
+ kb: packagesTotalKb,
3347
+ mb: (packagesTotalKb / 1024).toFixed(2),
3348
+ })}
3349
+ </Text>
3350
+ </View>
3351
+ </View>
3352
+ )}
3353
+
3354
+ {/* ══════════════════════════════════════════════════════════════════════ */}
3355
+ {/* ── 4. TAB: MEDIA & ASSET AUDITOR ───────────────────────────────────── */}
3356
+ {/* ══════════════════════════════════════════════════════════════════════ */}
3357
+ {activeSubTab === 'media' && (
3358
+ <View style={{flex: 1}}>
3359
+ <ScrollView
3360
+ style={{flex: 1}}
3361
+ contentContainerStyle={bundleStyles.contentContainer}
3362
+ keyboardShouldPersistTaps="handled">
3363
+ <View style={bundleStyles.tipsCard}>
3364
+ <View
3365
+ style={{
3366
+ flexDirection: 'row',
3367
+ alignItems: 'center',
3368
+ justifyContent: 'space-between',
3369
+ marginBottom: 8,
3370
+ }}>
3371
+ <View
3372
+ style={{flexDirection: 'row', alignItems: 'center', gap: 6}}>
3373
+ <StorageIcon color={AppColors.purple} size={18} />
3374
+ <Text style={bundleStyles.tipsHeading}>
3375
+ {t('bundle.mediaAuditorTitle')}
3376
+ </Text>
3377
+ </View>
3378
+ <CopyButton
3379
+ value={() => mediaFiles}
3380
+ label={t('bundle.mediaAssetsJson')}
3381
+ />
3382
+ </View>
3383
+ <Text style={bundleStyles.tipDesc}>
3384
+ {t('bundle.mediaAuditorPrefix')}{' '}
3385
+ <Text
3386
+ style={{
3387
+ fontFamily: AppFonts.interBold,
3388
+ color: AppColors.purple,
3389
+ }}>
3390
+ {summary.images.pct + summary.fonts.pct}%
3391
+ </Text>{' '}
3392
+ {t('bundle.mediaAuditorMid', {
3393
+ kb: summary.images.kb + summary.fonts.kb,
3394
+ })}{' '}
3395
+ <Text
3396
+ style={{
3397
+ fontFamily: AppFonts.interBold,
3398
+ color: AppColors.emerald700,
3399
+ }}>
3400
+ {t('bundle.mediaSavings', {size: 540})}
3401
+ </Text>{' '}
3402
+ {t('bundle.mediaAuditorSuffix')}
3403
+ </Text>
3404
+ </View>
3405
+
3406
+ <Text style={bundleStyles.listHeaderCount}>
3407
+ {t('bundle.mediaAssetsList', {count: mediaFiles.length})}
3408
+ </Text>
3409
+
3410
+ {mediaFiles.map((file, index) => (
3411
+ <View key={file.id} style={bundleStyles.fileCard}>
3412
+ <View style={bundleStyles.fileCardTop}>
3413
+ <View style={bundleStyles.sNoBadge}>
3414
+ <Text style={bundleStyles.sNoText}>#{index + 1}</Text>
3415
+ </View>
3416
+
3417
+ <MediaPreviewThumbnail file={file} />
3418
+
3419
+ <View style={{flex: 1}}>
3420
+ <Text style={bundleStyles.fileName}>{file.name}</Text>
3421
+ <Text style={bundleStyles.filePath}>{file.path}</Text>
3422
+ </View>
3423
+
3424
+ <View
3425
+ style={{
3426
+ flexDirection: 'row',
3427
+ alignItems: 'center',
3428
+ gap: 6,
3429
+ }}>
3430
+ <View style={bundleStyles.fileSizeBox}>
3431
+ <Text style={bundleStyles.fileSizeKb}>
3432
+ {t('bundle.fileSizeKb', {size: file.sizeKb})}
3433
+ </Text>
3434
+ </View>
3435
+ <TouchableScale
3436
+ accessible={true}
3437
+ accessibilityRole="button"
3438
+ accessibilityLabel={`Download ${file.name}`}
3439
+ accessibilityHint="Downloads media asset file"
3440
+ onPress={() =>
3441
+ downloadBundleFile(file, analysis?.scriptURL)
3442
+ }
3443
+ style={bundleStyles.downloadFileBtn}
3444
+ hitSlop={8}>
3445
+ <DownloadIcon size={14} color={AppColors.sky500} />
3446
+ </TouchableScale>
3447
+ <CopyButton
3448
+ value={() => file}
3449
+ label={t('bundle.mediaItemJson')}
3450
+ />
3451
+ </View>
3452
+ </View>
3453
+
3454
+ <View style={bundleStyles.fileCardBottom}>
3455
+ <Text style={bundleStyles.fileMetaText}>{file.meta}</Text>
3456
+ {file.advice && (
3457
+ <View
3458
+ style={[
3459
+ bundleStyles.adviceBadge,
3460
+ file.status === 'warning' && bundleStyles.adviceWarning,
3461
+ file.status === 'optimal' && bundleStyles.adviceOptimal,
3462
+ ]}>
3463
+ {file.status === 'warning' ? (
3464
+ <LightbulbIcon
3465
+ color={AppColors.amber800Warm}
3466
+ size={12}
3467
+ />
3468
+ ) : (
3469
+ <SparkleIcon color={AppColors.emerald700} size={12} />
3470
+ )}
3471
+ <Text
3472
+ style={[
3473
+ bundleStyles.adviceText,
3474
+ file.status === 'warning' && {
3475
+ color: AppColors.amber800Warm,
3476
+ },
3477
+ file.status === 'optimal' && {
3478
+ color: AppColors.emerald700,
3479
+ },
3480
+ ]}>
3481
+ {file.advice}
3482
+ </Text>
3483
+ </View>
3484
+ )}
3485
+ </View>
3486
+ </View>
3487
+ ))}
3488
+ </ScrollView>
3489
+
3490
+ {/* Sticky Total Media Size Footer */}
3491
+ <View style={bundleStyles.stickyFooterBar}>
3492
+ <View style={{flexDirection: 'row', alignItems: 'center', gap: 6}}>
3493
+ <StorageIcon color={AppColors.purple} size={14} />
3494
+ <Text style={bundleStyles.footerTitle}>
3495
+ {t('bundle.mediaAndFonts', {count: mediaFiles.length})}
3496
+ </Text>
3497
+ </View>
3498
+ <Text style={bundleStyles.footerSizeVal}>
3499
+ {t('bundle.kbMbValue', {
3500
+ kb: mediaTotalKb,
3501
+ mb: (mediaTotalKb / 1024).toFixed(2),
3502
+ })}
3503
+ </Text>
3504
+ </View>
3505
+ </View>
3506
+ )}
3507
+
3508
+ {/* ══════════════════════════════════════════════════════════════════════ */}
3509
+ {/* ── 5. TAB: OPTIMIZER CHECKLIST ─────────────────────────────────────── */}
3510
+ {/* ══════════════════════════════════════════════════════════════════════ */}
3511
+ {activeSubTab === 'optimizer' && (
3512
+ <ScrollView
3513
+ style={{flex: 1}}
3514
+ contentContainerStyle={bundleStyles.contentContainer}
3515
+ keyboardShouldPersistTaps="handled">
3516
+ <View style={bundleStyles.tipsCard}>
3517
+ <View
3518
+ style={{
3519
+ flexDirection: 'row',
3520
+ alignItems: 'center',
3521
+ justifyContent: 'space-between',
3522
+ marginBottom: 12,
3523
+ }}>
3524
+ <View
3525
+ style={{flexDirection: 'row', alignItems: 'center', gap: 6}}>
3526
+ <CircleAlertIcon color={AppColors.purple} size={18} />
3527
+ <Text style={bundleStyles.tipsHeading}>
3528
+ {t('bundle.optimizerTitle')}
3529
+ </Text>
3530
+ </View>
3531
+ <CopyButton
3532
+ value={() => [
3533
+ t('bundle.optTip1Title'),
3534
+ `${t('bundle.optTip2Title')} (${
3535
+ isHermes ? t('bundle.active') : t('bundle.actionRequired')
3536
+ })`,
3537
+ t('bundle.optTip3Title'),
3538
+ t('bundle.optTip4Title'),
3539
+ t('bundle.optTip5Title'),
3540
+ ]}
3541
+ label={t('bundle.optimizationChecklist')}
3542
+ />
3543
+ </View>
3544
+
3545
+ {[
3546
+ {
3547
+ iconType: 'image' as const,
3548
+ iconColor: AppColors.pink500,
3549
+ title: t('bundle.optTip1Title'),
3550
+ desc: t('bundle.optTip1Desc'),
3551
+ badge: t('bundle.highImpact'),
3552
+ badgeColor: AppColors.pink500,
3553
+ },
3554
+ {
3555
+ iconType: 'bolt' as const,
3556
+ iconColor: isHermes ? AppColors.emerald500 : AppColors.amber500,
3557
+ title: t('bundle.optTip2Title'),
3558
+ desc: isHermes
3559
+ ? t('bundle.optTip2DescActive')
3560
+ : t('bundle.optTip2DescInactive'),
3561
+ badge: isHermes
3562
+ ? t('bundle.active')
3563
+ : t('bundle.actionRequired'),
3564
+ badgeColor: isHermes
3565
+ ? AppColors.emerald500
3566
+ : AppColors.amber500,
3567
+ },
3568
+ {
3569
+ iconType: 'font' as const,
3570
+ iconColor: AppColors.purple500,
3571
+ title: t('bundle.optTip3Title'),
3572
+ desc: t('bundle.optTip3Desc'),
3573
+ badge: t('bundle.mediumImpact'),
3574
+ badgeColor: AppColors.purple500,
3575
+ },
3576
+ {
3577
+ iconType: 'source' as const,
3578
+ iconColor: AppColors.sky500,
3579
+ title: t('bundle.optTip4Title'),
3580
+ desc: t('bundle.optTip4Desc'),
3581
+ badge: t('bundle.bestPractice'),
3582
+ badgeColor: AppColors.sky500,
3583
+ },
3584
+ {
3585
+ iconType: 'overhead' as const,
3586
+ iconColor: AppColors.indigo500,
3587
+ title: t('bundle.optTip5Title'),
3588
+ desc: t('bundle.optTip5Desc'),
3589
+ badge: t('bundle.bestPractice'),
3590
+ badgeColor: AppColors.indigo500,
3591
+ },
3592
+ ].map((tip, tIdx) => (
3593
+ <View key={tIdx} style={bundleStyles.tipItem}>
3594
+ <View style={bundleStyles.tipItemHeader}>
3595
+ <View style={bundleStyles.tipTitleWrap}>
3596
+ <View style={bundleStyles.sNoBadge}>
3597
+ <Text style={bundleStyles.sNoText}>#{tIdx + 1}</Text>
3598
+ </View>
3599
+ <BundleCategoryIcon
3600
+ type={tip.iconType}
3601
+ size={14}
3602
+ color={tip.iconColor}
3603
+ />
3604
+ <Text style={bundleStyles.tipTitle}>{tip.title}</Text>
3605
+ </View>
3606
+ <View style={bundleStyles.tipActionWrap}>
3607
+ <View
3608
+ style={[
3609
+ bundleStyles.impactBadge,
3610
+ {
3611
+ backgroundColor: `${tip.badgeColor}18`,
3612
+ borderColor: `${tip.badgeColor}40`,
3613
+ },
3614
+ ]}>
3615
+ <Text
3616
+ style={[
3617
+ bundleStyles.impactBadgeText,
3618
+ {color: tip.badgeColor},
3619
+ ]}>
3620
+ {tip.badge}
3621
+ </Text>
3622
+ </View>
3623
+ <CopyButton
3624
+ value={() => tip}
3625
+ label={t('bundle.tipDetails')}
3626
+ />
3627
+ </View>
3628
+ </View>
3629
+ <Text style={bundleStyles.tipDesc}>{tip.desc}</Text>
3630
+ </View>
3631
+ ))}
3632
+ </View>
3633
+ </ScrollView>
3634
+ )}
3635
+ </View>
3636
+ );
3637
+ });
3638
+
3639
+ const bundleStyles = StyleSheet.create({
3640
+ topTabWrap: {
3641
+ backgroundColor: AppColors.white,
3642
+ paddingVertical: 8,
3643
+ borderBottomWidth: 1,
3644
+ borderBottomColor: AppColors.dividerColor,
3645
+ flexDirection: 'row',
3646
+ alignItems: 'center',
3647
+ paddingRight: 10,
3648
+ },
3649
+ reloadButton: {
3650
+ padding: 8,
3651
+ borderRadius: 20,
3652
+ backgroundColor: AppColors.grayBackground,
3653
+ borderWidth: 1,
3654
+ borderColor: AppColors.dividerColor,
3655
+ alignItems: 'center',
3656
+ justifyContent: 'center',
3657
+ marginLeft: 4,
3658
+ },
3659
+ analyzingContainer: {
3660
+ flex: 1,
3661
+ alignItems: 'center',
3662
+ justifyContent: 'center',
3663
+ gap: 8,
3664
+ padding: 24,
3665
+ },
3666
+ analyzingText: {
3667
+ fontFamily: AppFonts.interBold,
3668
+ fontSize: 13,
3669
+ color: AppColors.primaryBlack,
3670
+ },
3671
+ analyzingHint: {
3672
+ fontFamily: AppFonts.interRegular,
3673
+ fontSize: 11,
3674
+ color: AppColors.grayTextWeak,
3675
+ },
3676
+ subTabsScroll: {
3677
+ paddingHorizontal: 12,
3678
+ gap: 8,
3679
+ flexDirection: 'row',
3680
+ alignItems: 'center',
3681
+ },
3682
+ subTabPill: {
3683
+ flexDirection: 'row',
3684
+ alignItems: 'center',
3685
+ gap: 6,
3686
+ paddingHorizontal: 12,
3687
+ paddingVertical: 7,
3688
+ borderRadius: 20,
3689
+ backgroundColor: AppColors.grayBackground,
3690
+ borderWidth: 1,
3691
+ borderColor: AppColors.dividerColor,
3692
+ },
3693
+ subTabPillActive: {
3694
+ backgroundColor: AppColors.brandPurple,
3695
+ borderColor: AppColors.brandPurple,
3696
+ shadowColor: AppColors.brandPurple,
3697
+ shadowOffset: {width: 0, height: 2},
3698
+ shadowOpacity: 0.2,
3699
+ shadowRadius: 3,
3700
+ elevation: 2,
3701
+ },
3702
+ subTabPillText: {
3703
+ fontFamily: AppFonts.interMedium,
3704
+ fontSize: 11.5,
3705
+ color: AppColors.grayTextStrong,
3706
+ },
3707
+ subTabPillTextActive: {
3708
+ fontFamily: AppFonts.interBold,
3709
+ color: AppColors.white,
3710
+ },
3711
+ contentContainer: {
3712
+ padding: 16,
3713
+ paddingBottom: 40,
3714
+ gap: 14,
3715
+ },
3716
+ heroCard: {
3717
+ backgroundColor: AppColors.white,
3718
+ borderRadius: 14,
3719
+ padding: 16,
3720
+ borderWidth: 1,
3721
+ borderColor: AppColors.dividerColor,
3722
+ shadowColor: AppColors.primaryBlack,
3723
+ shadowOffset: {width: 0, height: 2},
3724
+ shadowOpacity: 0.04,
3725
+ shadowRadius: 6,
3726
+ elevation: 2,
3727
+ },
3728
+ heroTopRow: {
3729
+ flexDirection: 'row',
3730
+ alignItems: 'flex-start',
3731
+ gap: 12,
3732
+ marginBottom: 16,
3733
+ },
3734
+ heroIconWrap: {
3735
+ width: 44,
3736
+ height: 44,
3737
+ borderRadius: 12,
3738
+ backgroundColor: `${AppColors.brandPurple}12`,
3739
+ alignItems: 'center',
3740
+ justifyContent: 'center',
3741
+ borderWidth: 1,
3742
+ borderColor: `${AppColors.brandPurple}25`,
3743
+ },
3744
+ heroTitle: {
3745
+ fontFamily: AppFonts.interBold,
3746
+ fontSize: 16,
3747
+ color: AppColors.primaryBlack,
3748
+ },
3749
+ heroSubtitle: {
3750
+ fontFamily: AppFonts.interRegular,
3751
+ fontSize: 12,
3752
+ color: AppColors.grayTextWeak,
3753
+ marginTop: 2,
3754
+ },
3755
+ metricsGrid: {
3756
+ flexDirection: 'row',
3757
+ flexWrap: 'wrap',
3758
+ gap: 8,
3759
+ },
3760
+ metricBox: {
3761
+ flex: 1,
3762
+ minWidth: '45%',
3763
+ backgroundColor: AppColors.grayBackground,
3764
+ borderRadius: 10,
3765
+ padding: 10,
3766
+ borderWidth: 1,
3767
+ borderColor: AppColors.dividerColor,
3768
+ },
3769
+ metricLabel: {
3770
+ fontFamily: AppFonts.interBold,
3771
+ fontSize: 9.5,
3772
+ color: AppColors.grayTextWeak,
3773
+ letterSpacing: 0.5,
3774
+ },
3775
+ metricValue: {
3776
+ fontFamily: AppFonts.interBold,
3777
+ fontSize: 14,
3778
+ color: AppColors.primaryBlack,
3779
+ marginTop: 3,
3780
+ },
3781
+ metricHint: {
3782
+ fontFamily: AppFonts.interMedium,
3783
+ fontSize: 10.5,
3784
+ color: AppColors.grayText,
3785
+ marginTop: 2,
3786
+ },
3787
+ statusIndicator: {
3788
+ width: 8,
3789
+ height: 8,
3790
+ borderRadius: 4,
3791
+ },
3792
+ prodPlatformScroll: {
3793
+ flexDirection: 'row',
3794
+ alignItems: 'center',
3795
+ gap: 8,
3796
+ paddingBottom: 4,
3797
+ },
3798
+ prodPlatformBtn: {
3799
+ flexDirection: 'row',
3800
+ alignItems: 'center',
3801
+ paddingVertical: 7,
3802
+ paddingHorizontal: 12,
3803
+ borderRadius: 20,
3804
+ backgroundColor: AppColors.white,
3805
+ borderWidth: 1,
3806
+ borderColor: AppColors.dividerColor,
3807
+ gap: 6,
3808
+ },
3809
+ prodPlatformBtnActive: {
3810
+ backgroundColor: AppColors.brandPurple,
3811
+ borderColor: AppColors.brandPurple,
3812
+ shadowColor: AppColors.brandPurple,
3813
+ shadowOffset: {width: 0, height: 2},
3814
+ shadowOpacity: 0.18,
3815
+ shadowRadius: 3,
3816
+ elevation: 2,
3817
+ },
3818
+ prodPlatformBtnText: {
3819
+ fontFamily: AppFonts.interBold,
3820
+ fontSize: 11.5,
3821
+ color: AppColors.grayTextStrong,
3822
+ },
3823
+ prodPlatformBtnTextActive: {
3824
+ color: AppColors.white,
3825
+ },
3826
+ prodPlatformBadge: {
3827
+ paddingHorizontal: 6,
3828
+ paddingVertical: 2,
3829
+ borderRadius: 10,
3830
+ backgroundColor: `${AppColors.purple}14`,
3831
+ },
3832
+ prodPlatformBadgeActive: {
3833
+ backgroundColor: 'rgba(255, 255, 255, 0.25)',
3834
+ },
3835
+ prodPlatformBadgeText: {
3836
+ fontFamily: AppFonts.interBold,
3837
+ fontSize: 9.5,
3838
+ color: AppColors.purple,
3839
+ },
3840
+ prodPlatformBadgeTextActive: {
3841
+ color: AppColors.white,
3842
+ },
3843
+ sectionCard: {
3844
+ backgroundColor: AppColors.white,
3845
+ borderRadius: 14,
3846
+ padding: 14,
3847
+ borderWidth: 1,
3848
+ borderColor: AppColors.dividerColor,
3849
+ overflow: 'hidden',
3850
+ },
3851
+ sectionHeaderRow: {
3852
+ flexDirection: 'row',
3853
+ justifyContent: 'space-between',
3854
+ alignItems: 'center',
3855
+ marginBottom: 10,
3856
+ gap: 8,
3857
+ },
3858
+ sectionTitle: {
3859
+ fontFamily: AppFonts.interBold,
3860
+ fontSize: 13,
3861
+ color: AppColors.primaryBlack,
3862
+ },
3863
+ sectionSub: {
3864
+ fontFamily: AppFonts.interMedium,
3865
+ fontSize: 11,
3866
+ color: AppColors.grayTextWeak,
3867
+ },
3868
+ treemapBar: {
3869
+ flexDirection: 'row',
3870
+ borderRadius: 6,
3871
+ overflow: 'hidden',
3872
+ height: 16,
3873
+ marginBottom: 12,
3874
+ backgroundColor: AppColors.slate200,
3875
+ },
3876
+ prodTreemapBar: {
3877
+ flexDirection: 'row',
3878
+ borderRadius: 6,
3879
+ overflow: 'hidden',
3880
+ height: 16,
3881
+ marginBottom: 12,
3882
+ backgroundColor: AppColors.slate200,
3883
+ },
3884
+ legendGrid: {
3885
+ flexDirection: 'row',
3886
+ flexWrap: 'wrap',
3887
+ gap: 8,
3888
+ },
3889
+ legendItem: {
3890
+ flexDirection: 'row',
3891
+ alignItems: 'center',
3892
+ gap: 6,
3893
+ paddingHorizontal: 8,
3894
+ paddingVertical: 5,
3895
+ borderRadius: 8,
3896
+ backgroundColor: `${AppColors.slate200}40`,
3897
+ borderWidth: 1,
3898
+ borderColor: `${AppColors.dividerColor}`,
3899
+ flexShrink: 0,
3900
+ },
3901
+ legendText: {
3902
+ fontFamily: AppFonts.interMedium,
3903
+ fontSize: 11,
3904
+ color: AppColors.grayTextStrong,
3905
+ },
3906
+ legendVal: {
3907
+ fontFamily: AppFonts.interBold,
3908
+ color: AppColors.primaryBlack,
3909
+ },
3910
+ subsystemCard: {
3911
+ backgroundColor: AppColors.white,
3912
+ borderRadius: 12,
3913
+ padding: 12,
3914
+ marginBottom: 8,
3915
+ borderWidth: 1,
3916
+ borderColor: AppColors.dividerColor,
3917
+ gap: 8,
3918
+ },
3919
+ subsystemHeader: {
3920
+ flexDirection: 'row',
3921
+ alignItems: 'center',
3922
+ justifyContent: 'space-between',
3923
+ gap: 8,
3924
+ },
3925
+ subsystemTitle: {
3926
+ fontFamily: AppFonts.interBold,
3927
+ fontSize: 12.5,
3928
+ color: AppColors.primaryBlack,
3929
+ },
3930
+ subsystemBadge: {
3931
+ paddingHorizontal: 6,
3932
+ paddingVertical: 2,
3933
+ borderRadius: 6,
3934
+ },
3935
+ subsystemBadgeText: {
3936
+ fontFamily: AppFonts.interBold,
3937
+ fontSize: 10,
3938
+ },
3939
+ subsystemGrid: {
3940
+ flexDirection: 'row',
3941
+ flexWrap: 'wrap',
3942
+ gap: 6,
3943
+ },
3944
+ subsystemItem: {
3945
+ flexDirection: 'row',
3946
+ alignItems: 'center',
3947
+ gap: 5,
3948
+ paddingHorizontal: 8,
3949
+ paddingVertical: 4,
3950
+ borderRadius: 6,
3951
+ backgroundColor: AppColors.grayBackground,
3952
+ borderWidth: 1,
3953
+ borderColor: AppColors.dividerColor,
3954
+ },
3955
+ subsystemItemText: {
3956
+ fontFamily: AppFonts.interMedium,
3957
+ fontSize: 10.5,
3958
+ color: AppColors.grayTextStrong,
3959
+ },
3960
+ subsystemItemVal: {
3961
+ fontFamily: AppFonts.interBold,
3962
+ color: AppColors.primaryBlack,
3963
+ },
3964
+ catRowCard: {
3965
+ backgroundColor: AppColors.grayBackground,
3966
+ borderRadius: 12,
3967
+ padding: 12,
3968
+ marginBottom: 8,
3969
+ borderWidth: 1,
3970
+ borderColor: AppColors.dividerColor,
3971
+ gap: 8,
3972
+ },
3973
+ catRowTop: {
3974
+ flexDirection: 'row',
3975
+ alignItems: 'flex-start',
3976
+ gap: 10,
3977
+ },
3978
+ catIconBox: {
3979
+ width: 36,
3980
+ height: 36,
3981
+ borderRadius: 10,
3982
+ borderWidth: 1,
3983
+ alignItems: 'center',
3984
+ justifyContent: 'center',
3985
+ flexShrink: 0,
3986
+ },
3987
+ catRowTitleWrap: {
3988
+ flex: 1,
3989
+ minWidth: 0,
3990
+ gap: 2,
3991
+ },
3992
+ catRowTitle: {
3993
+ fontFamily: AppFonts.interBold,
3994
+ fontSize: 13,
3995
+ color: AppColors.primaryBlack,
3996
+ flex: 1,
3997
+ },
3998
+ catRowDesc: {
3999
+ fontFamily: AppFonts.interRegular,
4000
+ fontSize: 10.5,
4001
+ color: AppColors.grayTextWeak,
4002
+ lineHeight: 14.5,
4003
+ },
4004
+ catRowSize: {
4005
+ fontFamily: AppFonts.interBold,
4006
+ fontSize: 12.5,
4007
+ color: AppColors.primaryBlack,
4008
+ },
4009
+ catRowRight: {
4010
+ alignItems: 'flex-end',
4011
+ justifyContent: 'center',
4012
+ flexShrink: 0,
4013
+ minWidth: 64,
4014
+ },
4015
+ catRowPct: {
4016
+ fontFamily: AppFonts.interMedium,
4017
+ fontSize: 10,
4018
+ color: AppColors.grayTextWeak,
4019
+ },
4020
+ catPctPill: {
4021
+ paddingHorizontal: 5.5,
4022
+ paddingVertical: 1.5,
4023
+ borderRadius: 5,
4024
+ },
4025
+ catPctText: {
4026
+ fontFamily: AppFonts.interBold,
4027
+ fontSize: 9.5,
4028
+ },
4029
+ catProgressTrack: {
4030
+ height: 5,
4031
+ borderRadius: 2.5,
4032
+ backgroundColor: `${AppColors.slate200}`,
4033
+ overflow: 'hidden',
4034
+ marginTop: 2,
4035
+ },
4036
+ catProgressBar: {
4037
+ height: '100%',
4038
+ borderRadius: 2.5,
4039
+ },
4040
+ filterContainer: {
4041
+ backgroundColor: AppColors.white,
4042
+ borderRadius: 12,
4043
+ padding: 10,
4044
+ marginHorizontal: 16,
4045
+ marginTop: 12,
4046
+ borderWidth: 1,
4047
+ borderColor: AppColors.dividerColor,
4048
+ gap: 8,
4049
+ },
4050
+ searchRow: {
4051
+ flexDirection: 'row',
4052
+ alignItems: 'center',
4053
+ backgroundColor: AppColors.grayBackground,
4054
+ borderRadius: 8,
4055
+ paddingHorizontal: 10,
4056
+ height: 36,
4057
+ gap: 8,
4058
+ },
4059
+ searchInput: {
4060
+ flex: 1,
4061
+ fontFamily: AppFonts.interRegular,
4062
+ fontSize: 12,
4063
+ color: AppColors.primaryBlack,
4064
+ padding: 0,
4065
+ },
4066
+ categoryPillsScroll: {
4067
+ gap: 6,
4068
+ paddingVertical: 2,
4069
+ },
4070
+ catPill: {
4071
+ flexDirection: 'row',
4072
+ alignItems: 'center',
4073
+ gap: 4,
4074
+ paddingHorizontal: 10,
4075
+ paddingVertical: 5,
4076
+ borderRadius: 16,
4077
+ backgroundColor: AppColors.grayBackground,
4078
+ borderWidth: 1,
4079
+ borderColor: AppColors.dividerColor,
4080
+ },
4081
+ catPillActive: {
4082
+ backgroundColor: AppColors.brandPurple,
4083
+ borderColor: AppColors.brandPurple,
4084
+ },
4085
+ catPillText: {
4086
+ fontFamily: AppFonts.interMedium,
4087
+ fontSize: 11,
4088
+ color: AppColors.grayTextStrong,
4089
+ },
4090
+ catPillTextActive: {
4091
+ fontFamily: AppFonts.interBold,
4092
+ color: AppColors.white,
4093
+ },
4094
+ listHeaderCount: {
4095
+ fontFamily: AppFonts.interMedium,
4096
+ fontSize: 11,
4097
+ color: AppColors.grayTextWeak,
4098
+ paddingHorizontal: 4,
4099
+ },
4100
+ treeHeaderRow: {
4101
+ flexDirection: 'row',
4102
+ alignItems: 'center',
4103
+ justifyContent: 'space-between',
4104
+ paddingHorizontal: 2,
4105
+ marginBottom: 6,
4106
+ },
4107
+ viewModeToggleGroup: {
4108
+ flexDirection: 'row',
4109
+ backgroundColor: AppColors.grayBackground,
4110
+ borderRadius: 8,
4111
+ padding: 2,
4112
+ borderWidth: 1,
4113
+ borderColor: AppColors.dividerColor,
4114
+ },
4115
+ viewModeBtn: {
4116
+ paddingHorizontal: 10,
4117
+ paddingVertical: 5,
4118
+ borderRadius: 6,
4119
+ },
4120
+ viewModeBtnActive: {
4121
+ backgroundColor: AppColors.brandPurple,
4122
+ shadowColor: AppColors.brandPurple,
4123
+ shadowOffset: {width: 0, height: 1},
4124
+ shadowOpacity: 0.2,
4125
+ shadowRadius: 2,
4126
+ elevation: 2,
4127
+ },
4128
+ viewModeBtnText: {
4129
+ fontFamily: AppFonts.interMedium,
4130
+ fontSize: 11,
4131
+ color: AppColors.grayTextStrong,
4132
+ },
4133
+ viewModeBtnTextActive: {
4134
+ fontFamily: AppFonts.interBold,
4135
+ color: AppColors.white,
4136
+ },
4137
+ treeActionLink: {
4138
+ fontFamily: AppFonts.interBold,
4139
+ fontSize: 11.5,
4140
+ color: AppColors.purple,
4141
+ },
4142
+ treeRootCard: {
4143
+ backgroundColor: AppColors.white,
4144
+ borderRadius: 12,
4145
+ borderWidth: 1,
4146
+ borderColor: AppColors.dividerColor,
4147
+ overflow: 'hidden',
4148
+ marginBottom: 16,
4149
+ },
4150
+ treeFolderBlock: {
4151
+ borderBottomWidth: StyleSheet.hairlineWidth,
4152
+ borderBottomColor: AppColors.dividerColor,
4153
+ },
4154
+ treeFolderRow: {
4155
+ flexDirection: 'row',
4156
+ alignItems: 'center',
4157
+ justifyContent: 'space-between',
4158
+ paddingVertical: 8,
4159
+ paddingRight: 10,
4160
+ backgroundColor: `${AppColors.purple}06`,
4161
+ },
4162
+ treeRowLeft: {
4163
+ flexDirection: 'row',
4164
+ alignItems: 'center',
4165
+ gap: 6,
4166
+ flex: 1,
4167
+ marginRight: 6,
4168
+ },
4169
+ treeRowRight: {
4170
+ flexDirection: 'row',
4171
+ alignItems: 'center',
4172
+ gap: 6,
4173
+ },
4174
+ treeChevron: {
4175
+ fontSize: 12,
4176
+ color: AppColors.purple,
4177
+ fontFamily: AppFonts.interBold,
4178
+ width: 12,
4179
+ textAlign: 'center',
4180
+ },
4181
+ treeFolderName: {
4182
+ fontFamily: AppFonts.interBold,
4183
+ fontSize: 12.5,
4184
+ color: AppColors.primaryBlack,
4185
+ },
4186
+ treeCountBadge: {
4187
+ paddingHorizontal: 5,
4188
+ paddingVertical: 1,
4189
+ borderRadius: 8,
4190
+ backgroundColor: `${AppColors.purple}14`,
4191
+ },
4192
+ treeCountBadgeText: {
4193
+ fontFamily: AppFonts.interMedium,
4194
+ fontSize: 9.5,
4195
+ color: AppColors.purple,
4196
+ },
4197
+ treeFolderSize: {
4198
+ fontFamily: AppFonts.interMedium,
4199
+ fontSize: 11,
4200
+ color: AppColors.grayTextStrong,
4201
+ },
4202
+ treeChildrenContainer: {
4203
+ backgroundColor: AppColors.white,
4204
+ },
4205
+ treeFileRow: {
4206
+ flexDirection: 'row',
4207
+ alignItems: 'center',
4208
+ justifyContent: 'space-between',
4209
+ paddingVertical: 6,
4210
+ paddingRight: 10,
4211
+ borderBottomWidth: StyleSheet.hairlineWidth,
4212
+ borderBottomColor: `${AppColors.dividerColor}80`,
4213
+ backgroundColor: AppColors.white,
4214
+ },
4215
+ treeFileRowUnused: {
4216
+ backgroundColor: `${AppColors.red500}06`,
4217
+ },
4218
+ treeBranchGuide: {
4219
+ width: 12,
4220
+ alignItems: 'center',
4221
+ justifyContent: 'center',
4222
+ },
4223
+ treeBranchGuideText: {
4224
+ fontFamily: AppFonts.interRegular,
4225
+ fontSize: 11,
4226
+ color: AppColors.dividerColor,
4227
+ },
4228
+ treeFileName: {
4229
+ fontFamily: AppFonts.interMedium,
4230
+ fontSize: 12,
4231
+ color: AppColors.primaryBlack,
4232
+ flexShrink: 1,
4233
+ },
4234
+ treeUnusedPill: {
4235
+ paddingHorizontal: 5,
4236
+ paddingVertical: 1,
4237
+ borderRadius: 4,
4238
+ backgroundColor: `${AppColors.red500}18`,
4239
+ },
4240
+ treeUnusedPillText: {
4241
+ fontFamily: AppFonts.interBold,
4242
+ fontSize: 9,
4243
+ color: AppColors.red600,
4244
+ },
4245
+ treeFileSizeGroup: {
4246
+ alignItems: 'flex-end',
4247
+ },
4248
+ treeFileSizeText: {
4249
+ fontFamily: AppFonts.interMedium,
4250
+ fontSize: 11,
4251
+ color: AppColors.primaryBlack,
4252
+ },
4253
+ treeFilePctText: {
4254
+ fontFamily: AppFonts.interRegular,
4255
+ fontSize: 9,
4256
+ color: AppColors.grayTextWeak,
4257
+ },
4258
+ fileCard: {
4259
+ backgroundColor: AppColors.white,
4260
+ borderRadius: 12,
4261
+ padding: 12,
4262
+ borderWidth: 1,
4263
+ borderColor: AppColors.dividerColor,
4264
+ gap: 6,
4265
+ marginBottom: 8,
4266
+ },
4267
+ fileCardTop: {
4268
+ flexDirection: 'row',
4269
+ alignItems: 'center',
4270
+ gap: 8,
4271
+ },
4272
+ sNoBadge: {
4273
+ paddingHorizontal: 6,
4274
+ paddingVertical: 2,
4275
+ borderRadius: 4,
4276
+ backgroundColor: `${AppColors.purple}14`,
4277
+ borderWidth: 1,
4278
+ borderColor: `${AppColors.purple}30`,
4279
+ minWidth: 26,
4280
+ alignItems: 'center',
4281
+ justifyContent: 'center',
4282
+ },
4283
+ sNoText: {
4284
+ fontFamily: AppFonts.interBold,
4285
+ fontSize: 10,
4286
+ color: AppColors.purple,
4287
+ },
4288
+ extChip: {
4289
+ paddingHorizontal: 6,
4290
+ paddingVertical: 3,
4291
+ borderRadius: 5,
4292
+ borderWidth: 1,
4293
+ },
4294
+ extChipText: {
4295
+ fontFamily: AppFonts.interBold,
4296
+ fontSize: 10,
4297
+ letterSpacing: 0.4,
4298
+ },
4299
+ pkgBadge: {
4300
+ paddingHorizontal: 6,
4301
+ paddingVertical: 3,
4302
+ borderRadius: 5,
4303
+ borderWidth: 1,
4304
+ },
4305
+ pkgBadgeText: {
4306
+ fontFamily: AppFonts.interBold,
4307
+ fontSize: 9,
4308
+ letterSpacing: 0.4,
4309
+ },
4310
+ pkgLogoWrap: {
4311
+ borderRadius: 7,
4312
+ borderWidth: 1,
4313
+ alignItems: 'center',
4314
+ justifyContent: 'center',
4315
+ backgroundColor: AppColors.white,
4316
+ borderColor: AppColors.dividerColor,
4317
+ overflow: 'hidden',
4318
+ },
4319
+ pkgLogoFallbackText: {
4320
+ fontFamily: AppFonts.interBold,
4321
+ },
4322
+ mediaThumbWrap: {
4323
+ width: 32,
4324
+ height: 32,
4325
+ borderRadius: 6,
4326
+ overflow: 'hidden',
4327
+ backgroundColor: AppColors.grayBackground,
4328
+ borderWidth: 1,
4329
+ borderColor: AppColors.dividerColor,
4330
+ },
4331
+ mediaThumbImg: {
4332
+ width: '100%',
4333
+ height: '100%',
4334
+ },
4335
+ fileName: {
4336
+ fontFamily: AppFonts.interBold,
4337
+ fontSize: 12.5,
4338
+ color: AppColors.primaryBlack,
4339
+ },
4340
+ versionPill: {
4341
+ paddingHorizontal: 5,
4342
+ paddingVertical: 1,
4343
+ borderRadius: 4,
4344
+ backgroundColor: `${AppColors.purple}14`,
4345
+ borderWidth: 1,
4346
+ borderColor: `${AppColors.purple}26`,
4347
+ },
4348
+ versionPillText: {
4349
+ fontFamily: AppFonts.interMedium,
4350
+ fontSize: 10,
4351
+ color: AppColors.purple,
4352
+ },
4353
+ pkgVersion: {
4354
+ fontFamily: AppFonts.interMedium,
4355
+ fontSize: 11,
4356
+ color: AppColors.grayTextWeak,
4357
+ },
4358
+ filePath: {
4359
+ fontFamily: AppFonts.interRegular,
4360
+ fontSize: 10.5,
4361
+ color: AppColors.grayTextWeak,
4362
+ marginTop: 1,
4363
+ },
4364
+ fileSizeBox: {
4365
+ alignItems: 'flex-end',
4366
+ },
4367
+ downloadFileBtn: {
4368
+ padding: 6,
4369
+ borderRadius: 6,
4370
+ backgroundColor: `${AppColors.sky500}14`,
4371
+ alignItems: 'center',
4372
+ justifyContent: 'center',
4373
+ borderWidth: 1,
4374
+ borderColor: `${AppColors.sky500}28`,
4375
+ },
4376
+ treeActionBtn: {
4377
+ padding: 4,
4378
+ borderRadius: 4,
4379
+ backgroundColor: `${AppColors.sky500}14`,
4380
+ alignItems: 'center',
4381
+ justifyContent: 'center',
4382
+ borderWidth: 1,
4383
+ borderColor: `${AppColors.sky500}28`,
4384
+ },
4385
+ fileSizeKb: {
4386
+ fontFamily: AppFonts.interBold,
4387
+ fontSize: 12,
4388
+ color: AppColors.primaryBlack,
4389
+ },
4390
+ filePercent: {
4391
+ fontFamily: AppFonts.interMedium,
4392
+ fontSize: 10,
4393
+ color: AppColors.grayTextWeak,
4394
+ },
4395
+ fileProgressTrack: {
4396
+ height: 4,
4397
+ borderRadius: 2,
4398
+ backgroundColor: AppColors.grayBackground,
4399
+ overflow: 'hidden',
4400
+ marginVertical: 2,
4401
+ },
4402
+ fileProgressBar: {
4403
+ height: '100%',
4404
+ borderRadius: 2,
4405
+ },
4406
+ fileCardBottom: {
4407
+ gap: 4,
4408
+ },
4409
+ fileMetaText: {
4410
+ fontFamily: AppFonts.interRegular,
4411
+ fontSize: 10.5,
4412
+ color: AppColors.grayTextWeak,
4413
+ },
4414
+ typeTag: {
4415
+ backgroundColor: AppColors.grayBackground,
4416
+ paddingHorizontal: 6,
4417
+ paddingVertical: 2,
4418
+ borderRadius: 4,
4419
+ borderWidth: 1,
4420
+ borderColor: AppColors.dividerColor,
4421
+ },
4422
+ typeTagText: {
4423
+ fontFamily: AppFonts.interMedium,
4424
+ fontSize: 9.5,
4425
+ color: AppColors.grayText,
4426
+ },
4427
+ npmLinkBtn: {
4428
+ flexDirection: 'row',
4429
+ alignItems: 'center',
4430
+ gap: 3,
4431
+ paddingHorizontal: 7,
4432
+ paddingVertical: 2,
4433
+ borderRadius: 5,
4434
+ backgroundColor: AppColors.npmRedTint,
4435
+ borderWidth: 1,
4436
+ borderColor: AppColors.npmRedBorder,
4437
+ },
4438
+ npmLinkText: {
4439
+ fontFamily: AppFonts.interBold,
4440
+ fontSize: 9.5,
4441
+ color: AppColors.npmRed,
4442
+ },
4443
+ deprecatedBadge: {
4444
+ paddingHorizontal: 6,
4445
+ paddingVertical: 1.5,
4446
+ borderRadius: 4,
4447
+ backgroundColor: AppColors.red100,
4448
+ borderWidth: 1,
4449
+ borderColor: AppColors.errorBorder,
4450
+ },
4451
+ deprecatedBadgeText: {
4452
+ fontFamily: AppFonts.interBold,
4453
+ fontSize: 9,
4454
+ color: AppColors.red600,
4455
+ letterSpacing: 0.3,
4456
+ },
4457
+ updateBadge: {
4458
+ paddingHorizontal: 6,
4459
+ paddingVertical: 1.5,
4460
+ borderRadius: 4,
4461
+ backgroundColor: AppColors.green100,
4462
+ borderWidth: 1,
4463
+ borderColor: AppColors.greenBorder,
4464
+ },
4465
+ updateBadgeText: {
4466
+ fontFamily: AppFonts.interBold,
4467
+ fontSize: 9,
4468
+ color: AppColors.emerald700,
4469
+ },
4470
+ lastActiveText: {
4471
+ fontFamily: AppFonts.interRegular,
4472
+ fontSize: 10,
4473
+ color: AppColors.grayTextWeak,
4474
+ },
4475
+ deprecationBox: {
4476
+ backgroundColor: AppColors.errorCardBg,
4477
+ borderRadius: 6,
4478
+ paddingHorizontal: 8,
4479
+ paddingVertical: 5,
4480
+ borderWidth: 1,
4481
+ borderColor: AppColors.errorBorder,
4482
+ marginTop: 3,
4483
+ },
4484
+ deprecationText: {
4485
+ fontFamily: AppFonts.interMedium,
4486
+ fontSize: 10.5,
4487
+ color: AppColors.rose700,
4488
+ lineHeight: 14,
4489
+ },
4490
+ unusedBadge: {
4491
+ paddingHorizontal: 6,
4492
+ paddingVertical: 1.5,
4493
+ borderRadius: 4,
4494
+ backgroundColor: AppColors.red100,
4495
+ borderWidth: 1,
4496
+ borderColor: AppColors.errorBorder,
4497
+ },
4498
+ unusedBadgeText: {
4499
+ fontFamily: AppFonts.interBold,
4500
+ fontSize: 9.5,
4501
+ color: AppColors.red600,
4502
+ },
4503
+ consumedBadge: {
4504
+ paddingHorizontal: 6,
4505
+ paddingVertical: 1.5,
4506
+ borderRadius: 4,
4507
+ backgroundColor: AppColors.green100,
4508
+ borderWidth: 1,
4509
+ borderColor: AppColors.greenBorder,
4510
+ },
4511
+ consumedBadgeText: {
4512
+ fontFamily: AppFonts.interBold,
4513
+ fontSize: 9.5,
4514
+ color: AppColors.emerald600,
4515
+ },
4516
+ treeFolderChevronWrap: {
4517
+ width: 18,
4518
+ height: 18,
4519
+ borderRadius: 9,
4520
+ backgroundColor: `${AppColors.purple}14`,
4521
+ alignItems: 'center',
4522
+ justifyContent: 'center',
4523
+ },
4524
+ treeFileCardUnused: {
4525
+ borderColor: AppColors.errorBorder,
4526
+ backgroundColor: AppColors.red50,
4527
+ },
4528
+ adviceUnused: {
4529
+ backgroundColor: AppColors.errorCardBg,
4530
+ borderWidth: 1,
4531
+ borderColor: AppColors.errorBorder,
4532
+ },
4533
+ liveWarningCard: {
4534
+ flexDirection: 'row',
4535
+ alignItems: 'center',
4536
+ backgroundColor: AppColors.amber100,
4537
+ borderWidth: 1,
4538
+ borderColor: AppColors.amber200,
4539
+ borderRadius: 10,
4540
+ padding: 12,
4541
+ marginBottom: 12,
4542
+ },
4543
+ liveWarningTitle: {
4544
+ fontFamily: AppFonts.interBold,
4545
+ fontSize: 12,
4546
+ color: AppColors.amber800Warm,
4547
+ marginBottom: 2,
4548
+ },
4549
+ liveWarningDesc: {
4550
+ fontFamily: AppFonts.interRegular,
4551
+ fontSize: 10.5,
4552
+ lineHeight: 14,
4553
+ color: AppColors.amber800Warm,
4554
+ },
4555
+ adviceBadge: {
4556
+ flexDirection: 'row',
4557
+ alignItems: 'center',
4558
+ paddingHorizontal: 8,
4559
+ paddingVertical: 5,
4560
+ borderRadius: 6,
4561
+ marginTop: 4,
4562
+ gap: 6,
4563
+ },
4564
+ adviceWarning: {
4565
+ backgroundColor: AppColors.amber100,
4566
+ borderWidth: 1,
4567
+ borderColor: AppColors.amber200,
4568
+ },
4569
+ adviceOptimal: {
4570
+ backgroundColor: AppColors.green100,
4571
+ borderWidth: 1,
4572
+ borderColor: AppColors.greenBorder,
4573
+ },
4574
+ adviceText: {
4575
+ fontFamily: AppFonts.interMedium,
4576
+ fontSize: 10.5,
4577
+ lineHeight: 14,
4578
+ flex: 1,
4579
+ },
4580
+ impactBadge: {
4581
+ paddingHorizontal: 6,
4582
+ paddingVertical: 2,
4583
+ borderRadius: 4,
4584
+ borderWidth: 1,
4585
+ },
4586
+ impactBadgeText: {
4587
+ fontFamily: AppFonts.interBold,
4588
+ fontSize: 9,
4589
+ },
4590
+ searchHighlight: {
4591
+ backgroundColor: AppColors.yellow200,
4592
+ color: AppColors.yellow800,
4593
+ fontFamily: AppFonts.interBold,
4594
+ },
4595
+ tipsCard: {
4596
+ backgroundColor: AppColors.purple50,
4597
+ borderRadius: 14,
4598
+ padding: 14,
4599
+ borderWidth: 1,
4600
+ borderColor: AppColors.purple200,
4601
+ },
4602
+ tipsHeading: {
4603
+ fontFamily: AppFonts.interBold,
4604
+ fontSize: 13.5,
4605
+ color: AppColors.purple800,
4606
+ },
4607
+ totalSummaryCard: {
4608
+ marginTop: 10,
4609
+ backgroundColor: `${AppColors.brandPurple}0B`,
4610
+ borderRadius: 12,
4611
+ padding: 12,
4612
+ borderWidth: 1,
4613
+ borderColor: `${AppColors.brandPurple}25`,
4614
+ gap: 8,
4615
+ },
4616
+ totalSummaryRow: {
4617
+ flexDirection: 'row',
4618
+ justifyContent: 'space-between',
4619
+ alignItems: 'center',
4620
+ },
4621
+ totalSummaryLabelWrap: {
4622
+ flexDirection: 'row',
4623
+ alignItems: 'center',
4624
+ gap: 8,
4625
+ },
4626
+ totalIconBadge: {
4627
+ width: 32,
4628
+ height: 32,
4629
+ borderRadius: 8,
4630
+ backgroundColor: `${AppColors.brandPurple}15`,
4631
+ alignItems: 'center',
4632
+ justifyContent: 'center',
4633
+ },
4634
+ totalSummaryLabel: {
4635
+ fontFamily: AppFonts.interBold,
4636
+ fontSize: 11.5,
4637
+ color: AppColors.brandPurple,
4638
+ letterSpacing: 0.6,
4639
+ },
4640
+ totalSummarySub: {
4641
+ fontFamily: AppFonts.interRegular,
4642
+ fontSize: 10,
4643
+ color: AppColors.grayTextWeak,
4644
+ marginTop: 1,
4645
+ },
4646
+ totalSummaryValue: {
4647
+ fontFamily: AppFonts.interBold,
4648
+ fontSize: 15,
4649
+ color: AppColors.primaryBlack,
4650
+ },
4651
+ totalSummaryKb: {
4652
+ fontFamily: AppFonts.interMedium,
4653
+ fontSize: 10,
4654
+ color: AppColors.grayTextWeak,
4655
+ },
4656
+ totalFormulaGrid: {
4657
+ flexDirection: 'row',
4658
+ flexWrap: 'wrap',
4659
+ gap: 6,
4660
+ paddingTop: 6,
4661
+ borderTopWidth: 1,
4662
+ borderTopColor: `${AppColors.brandPurple}15`,
4663
+ },
4664
+ totalChip: {
4665
+ flexDirection: 'row',
4666
+ alignItems: 'center',
4667
+ gap: 4,
4668
+ backgroundColor: AppColors.white,
4669
+ borderRadius: 6,
4670
+ paddingHorizontal: 6,
4671
+ paddingVertical: 3,
4672
+ borderWidth: 1,
4673
+ borderColor: AppColors.dividerColor,
4674
+ },
4675
+ totalDot: {
4676
+ width: 6,
4677
+ height: 6,
4678
+ borderRadius: 3,
4679
+ },
4680
+ totalChipText: {
4681
+ fontFamily: AppFonts.interBold,
4682
+ fontSize: 9.5,
4683
+ color: AppColors.primaryBlack,
4684
+ },
4685
+ totalSummaryFormula: {
4686
+ fontFamily: AppFonts.interRegular,
4687
+ fontSize: 10.5,
4688
+ color: AppColors.grayText,
4689
+ lineHeight: 15,
4690
+ },
4691
+ stickyFooterBar: {
4692
+ flexDirection: 'row',
4693
+ justifyContent: 'space-between',
4694
+ alignItems: 'center',
4695
+ backgroundColor: AppColors.white,
4696
+ paddingHorizontal: 16,
4697
+ paddingTop: 10,
4698
+ paddingBottom: Platform.OS === 'ios' ? 24 : 12,
4699
+ borderTopWidth: 1,
4700
+ borderTopColor: AppColors.dividerColor,
4701
+ shadowColor: AppColors.primaryBlack,
4702
+ shadowOffset: {width: 0, height: -2},
4703
+ shadowOpacity: 0.05,
4704
+ shadowRadius: 4,
4705
+ elevation: 3,
4706
+ },
4707
+ footerTitle: {
4708
+ fontFamily: AppFonts.interBold,
4709
+ fontSize: 12,
4710
+ color: AppColors.primaryBlack,
4711
+ },
4712
+ footerSizeVal: {
4713
+ fontFamily: AppFonts.interBold,
4714
+ fontSize: 12.5,
4715
+ color: AppColors.purple,
4716
+ },
4717
+ tipItem: {
4718
+ marginBottom: 10,
4719
+ backgroundColor: AppColors.white,
4720
+ padding: 12,
4721
+ borderRadius: 10,
4722
+ borderWidth: 1,
4723
+ borderColor: AppColors.purple200,
4724
+ gap: 6,
4725
+ },
4726
+ tipItemHeader: {
4727
+ flexDirection: 'row',
4728
+ alignItems: 'flex-start',
4729
+ justifyContent: 'space-between',
4730
+ gap: 8,
4731
+ },
4732
+ tipTitleWrap: {
4733
+ flexDirection: 'row',
4734
+ alignItems: 'center',
4735
+ gap: 6,
4736
+ flex: 1,
4737
+ flexShrink: 1,
4738
+ },
4739
+ tipActionWrap: {
4740
+ flexDirection: 'row',
4741
+ alignItems: 'center',
4742
+ gap: 6,
4743
+ flexShrink: 0,
4744
+ },
4745
+ tipTitle: {
4746
+ fontFamily: AppFonts.interBold,
4747
+ fontSize: 12,
4748
+ color: AppColors.purple900,
4749
+ flex: 1,
4750
+ flexShrink: 1,
4751
+ },
4752
+ tipDesc: {
4753
+ fontFamily: AppFonts.interRegular,
4754
+ fontSize: 11,
4755
+ color: AppColors.gray500,
4756
+ lineHeight: 16,
4757
+ },
4758
+ });
4759
+
4760
+ export default BundleTab;