react-native-inapp-inspector 2.2.2 → 2.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/dist/commonjs/constants/version.d.ts +1 -1
  2. package/dist/commonjs/constants/version.js +1 -1
  3. package/dist/esm/constants/version.d.ts +1 -1
  4. package/dist/esm/constants/version.js +1 -1
  5. package/package.json +2 -1
  6. package/src/analytics.ts +35 -0
  7. package/src/bundle.ts +25 -0
  8. package/src/components/AnalyticsDetail.tsx +865 -0
  9. package/src/components/AnalyticsEventCard.tsx +441 -0
  10. package/src/components/AnalyticsGraph.tsx +583 -0
  11. package/src/components/AnimatedEntrance.tsx +64 -0
  12. package/src/components/AppHeaderLogo.tsx +109 -0
  13. package/src/components/BrandCircleIcon.tsx +144 -0
  14. package/src/components/BrandSquareIcon.tsx +144 -0
  15. package/src/components/CodeSnippet.tsx +725 -0
  16. package/src/components/ConsoleLogCard.tsx +628 -0
  17. package/src/components/CopyButton.tsx +87 -0
  18. package/src/components/DiffViewer.tsx +82 -0
  19. package/src/components/DomainHeader.tsx +237 -0
  20. package/src/components/EmptyState.tsx +73 -0
  21. package/src/components/EndOfListFooter.tsx +100 -0
  22. package/src/components/ErrorBoundary.tsx +688 -0
  23. package/src/components/HeadersSection.tsx +192 -0
  24. package/src/components/HighlightText.tsx +100 -0
  25. package/src/components/Inspector/AnalyticsFilterModal.tsx +1250 -0
  26. package/src/components/Inspector/AnalyticsTab.tsx +1336 -0
  27. package/src/components/Inspector/BundleTab.tsx +4731 -0
  28. package/src/components/Inspector/ConsoleTab.tsx +702 -0
  29. package/src/components/Inspector/CrashDetail.tsx +941 -0
  30. package/src/components/Inspector/CrashFilterModal.tsx +721 -0
  31. package/src/components/Inspector/CrashTab.tsx +871 -0
  32. package/src/components/Inspector/FabLauncher.tsx +80 -0
  33. package/src/components/Inspector/InspectorContext.tsx +28 -0
  34. package/src/components/Inspector/InspectorHeader.tsx +973 -0
  35. package/src/components/Inspector/LogDetail.tsx +1427 -0
  36. package/src/components/Inspector/MainScreen.tsx +258 -0
  37. package/src/components/Inspector/NavigationTracker.tsx +13 -0
  38. package/src/components/Inspector/NetworkDetail.tsx +794 -0
  39. package/src/components/Inspector/NetworkTab.tsx +1095 -0
  40. package/src/components/Inspector/NpmUpdateToast.tsx +392 -0
  41. package/src/components/Inspector/PerformanceTab.tsx +1894 -0
  42. package/src/components/Inspector/ReduxDetail.tsx +1651 -0
  43. package/src/components/Inspector/ReduxTab.tsx +954 -0
  44. package/src/components/Inspector/SettingsPanel.tsx +3181 -0
  45. package/src/components/Inspector/TabBar.tsx +187 -0
  46. package/src/components/Inspector/TelemetryConsentModal.tsx +392 -0
  47. package/src/components/Inspector/UpdateAvailableModal.tsx +545 -0
  48. package/src/components/JsonViewer.tsx +486 -0
  49. package/src/components/LogCard.tsx +491 -0
  50. package/src/components/LogSyntaxHighlighter.tsx +178 -0
  51. package/src/components/MetaAccordion.tsx +340 -0
  52. package/src/components/MiniBarChart.tsx +42 -0
  53. package/src/components/MiniLineChart.tsx +33 -0
  54. package/src/components/NetworkIcons.tsx +2118 -0
  55. package/src/components/SectionHeader.tsx +113 -0
  56. package/src/components/SegmentedTabs.tsx +83 -0
  57. package/src/components/Slider.tsx +300 -0
  58. package/src/components/SourcePageCard.tsx +148 -0
  59. package/src/components/Toast.tsx +131 -0
  60. package/src/components/TouchableScale.tsx +91 -0
  61. package/src/components/TreeNode.tsx +186 -0
  62. package/src/console.ts +24 -0
  63. package/src/constants/index.ts +38 -0
  64. package/src/constants/version.ts +3 -0
  65. package/src/crash.ts +32 -0
  66. package/src/customHooks/analyticsLogger.ts +336 -0
  67. package/src/customHooks/bundleAnalyzer.ts +1231 -0
  68. package/src/customHooks/consoleLogger.ts +497 -0
  69. package/src/customHooks/crashHandler.ts +944 -0
  70. package/src/customHooks/logFilters.ts +32 -0
  71. package/src/customHooks/networkLogger.ts +419 -0
  72. package/src/customHooks/performanceTracker.ts +1014 -0
  73. package/src/customHooks/reduxLogger.ts +406 -0
  74. package/src/customHooks/useAccordion.tsx +60 -0
  75. package/src/decorators/index.ts +184 -0
  76. package/src/helpers/gaAnalyticsRegistry.ts +204 -0
  77. package/src/helpers/index.ts +860 -0
  78. package/src/helpers/memoryManager.ts +138 -0
  79. package/src/helpers/searchQueryParser.ts +283 -0
  80. package/src/helpers/settingsStore.ts +171 -0
  81. package/src/helpers/telemetry.ts +505 -0
  82. package/src/helpers/toast.ts +17 -0
  83. package/src/i18n/index.ts +69 -0
  84. package/src/i18n/locales/en.json +1052 -0
  85. package/src/index.tsx +2336 -0
  86. package/src/native/NativeInspector.ts +397 -0
  87. package/src/native/NativeNetworkInspector.ts +24 -0
  88. package/src/network.ts +22 -0
  89. package/src/performance.ts +38 -0
  90. package/src/redux.ts +23 -0
  91. package/src/styles/AppColors.ts +408 -0
  92. package/src/styles/AppFonts.ts +8 -0
  93. package/src/styles/common.ts +209 -0
  94. package/src/styles/index.ts +1570 -0
  95. package/src/types/enums.ts +194 -0
  96. package/src/types/index.ts +34 -0
  97. package/src/types/interfaces.ts +515 -0
@@ -0,0 +1,725 @@
1
+ import React, {useMemo, useState, useRef, useEffect} from 'react';
2
+ import {StyleSheet, Text, View, Platform, FlatList, TextInput, Pressable} from 'react-native';
3
+ import Svg, {Path} from 'react-native-svg';
4
+ import {SearchIcon, ClearIcon} from './NetworkIcons';
5
+ import CopyButton from './CopyButton';
6
+ import TouchableScale from './TouchableScale';
7
+ import {AppColors} from '../styles/AppColors';
8
+ import {AppFonts} from '../styles/AppFonts';
9
+ import {CodeSnippetProps} from '../types';
10
+
11
+ const escapeRegExp = (string: string) => {
12
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
13
+ };
14
+
15
+ // HTML pretty printer
16
+ const formatHTML = (html: string): string => {
17
+ if (!html) return '';
18
+ let indent = 0;
19
+ const cleaned = html
20
+ .replace(/<([a-zA-Z0-9:-]+)([^>]*)>\s*<\/([a-zA-Z0-9:-]+)>/g, '<$1$2></$3>')
21
+ .replace(/(<[^>]+>)/g, '\n$1\n')
22
+ .replace(/\n+/g, '\n');
23
+
24
+ const lines = cleaned.split('\n');
25
+ const formatted: string[] = [];
26
+ const selfClosingTags =
27
+ /^(?:area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/i;
28
+
29
+ for (let i = 0; i < lines.length; i++) {
30
+ const line = lines[i].trim();
31
+ if (!line) continue;
32
+
33
+ if (line.startsWith('</')) {
34
+ indent = Math.max(0, indent - 1);
35
+ }
36
+
37
+ formatted.push(' '.repeat(indent) + line);
38
+
39
+ if (
40
+ line.startsWith('<') &&
41
+ !line.startsWith('</') &&
42
+ !line.startsWith('<!') &&
43
+ !line.endsWith('/>')
44
+ ) {
45
+ const tagNameMatch = line.match(/^<([a-zA-Z0-9:-]+)/);
46
+ if (tagNameMatch) {
47
+ const tagName = tagNameMatch[1];
48
+ if (!selfClosingTags.test(tagName)) {
49
+ const closeTagRegex = new RegExp(`</${escapeRegExp(tagName)}>`);
50
+ if (!closeTagRegex.test(line)) {
51
+ indent++;
52
+ }
53
+ }
54
+ }
55
+ }
56
+ }
57
+ return formatted.join('\n');
58
+ };
59
+
60
+ // CSS pretty printer
61
+ const formatCSS = (css: string): string => {
62
+ if (!css) return '';
63
+ const cleaned = css
64
+ .replace(/\s*\{\s*/g, ' {\n')
65
+ .replace(/\s*;\s*/g, ';\n')
66
+ .replace(/\s*\}\s*/g, '\n}\n')
67
+ .replace(/\n+/g, '\n');
68
+
69
+ const lines = cleaned.split('\n');
70
+ let indent = 0;
71
+ const formatted: string[] = [];
72
+
73
+ for (let i = 0; i < lines.length; i++) {
74
+ const line = lines[i].trim();
75
+ if (!line) continue;
76
+
77
+ if (line.startsWith('}')) {
78
+ indent = Math.max(0, indent - 1);
79
+ }
80
+
81
+ formatted.push(' '.repeat(indent) + line);
82
+
83
+ if (line.endsWith('{')) {
84
+ indent++;
85
+ }
86
+ }
87
+ return formatted.join('\n');
88
+ };
89
+
90
+ // JS pretty printer
91
+ const formatJS = (js: string): string => {
92
+ if (!js) return '';
93
+ const lines = js.split(/\r?\n/);
94
+ let indent = 0;
95
+ const formatted: string[] = [];
96
+
97
+ for (let i = 0; i < lines.length; i++) {
98
+ const line = lines[i].trim();
99
+ if (!line) continue;
100
+
101
+ let openCount = (line.match(/\{|\[/g) || []).length;
102
+ let closeCount = (line.match(/\}|\]/g) || []).length;
103
+
104
+ if (line.startsWith('}') || line.startsWith(']')) {
105
+ indent = Math.max(0, indent - 1);
106
+ } else if (closeCount > openCount) {
107
+ indent = Math.max(0, indent - (closeCount - openCount));
108
+ }
109
+
110
+ formatted.push(' '.repeat(indent) + line);
111
+
112
+ if (
113
+ openCount > closeCount &&
114
+ !line.startsWith('}') &&
115
+ !line.startsWith(']')
116
+ ) {
117
+ indent += openCount - closeCount;
118
+ } else if (line.endsWith('{') || line.endsWith('[')) {
119
+ indent++;
120
+ }
121
+ }
122
+ return formatted.join('\n');
123
+ };
124
+
125
+ const tokenizeJS = (text: string) => {
126
+ const tokens: {text: string; type: string}[] = [];
127
+ const regex =
128
+ /(\/\/.*|\/\*[\s\S]*?\*\/)|(\"(?:\\.|[^\"\\])*\"|\'(?:\\.|[^\'\\])*\'|\`(?:\\.|[^\`\\])*\`)|(\b(const|let|var|function|return|if|else|for|while|import|export|class|try|catch|new|this|async|await|break|continue|switch|case|default|throw|typeof|yield)\b)|(\b(console|window|document|fetch|global|require|module|exports|Promise|Map|Set|Array|Object|String|Number|Boolean)\b)|(\b\d+(?:\.\d+)?\b)/g;
129
+
130
+ let lastIndex = 0;
131
+ let match;
132
+ while ((match = regex.exec(text)) !== null) {
133
+ if (regex.lastIndex === match.index) {
134
+ regex.lastIndex++;
135
+ }
136
+ if (match.index > lastIndex) {
137
+ tokens.push({
138
+ text: text.substring(lastIndex, match.index),
139
+ type: 'plain',
140
+ });
141
+ }
142
+ const matchedText = match[0];
143
+ if (match[1]) tokens.push({text: matchedText, type: 'comment'});
144
+ else if (match[2]) tokens.push({text: matchedText, type: 'string'});
145
+ else if (match[3]) tokens.push({text: matchedText, type: 'keyword'});
146
+ else if (match[4]) tokens.push({text: matchedText, type: 'builtin'});
147
+ else if (match[5]) tokens.push({text: matchedText, type: 'number'});
148
+
149
+ lastIndex = regex.lastIndex;
150
+ }
151
+ if (lastIndex < text.length) {
152
+ tokens.push({text: text.substring(lastIndex), type: 'plain'});
153
+ }
154
+ return tokens;
155
+ };
156
+
157
+ const tokenizeCSS = (text: string) => {
158
+ const tokens: {text: string; type: string}[] = [];
159
+ const regex =
160
+ /(\/\*[\s\S]*?\*\/)|(\.[a-zA-Z0-9_-]+|#[a-zA-Z0-9_-]+|[a-zA-Z0-9_-]+(?=\s*\{))|([a-zA-Z0-9_-]+(?=\s*:))|(:[^;]+;)/g;
161
+
162
+ let lastIndex = 0;
163
+ let match;
164
+ while ((match = regex.exec(text)) !== null) {
165
+ if (regex.lastIndex === match.index) {
166
+ regex.lastIndex++;
167
+ }
168
+ if (match.index > lastIndex) {
169
+ tokens.push({
170
+ text: text.substring(lastIndex, match.index),
171
+ type: 'plain',
172
+ });
173
+ }
174
+ const matchedText = match[0];
175
+ if (match[1]) tokens.push({text: matchedText, type: 'comment'});
176
+ else if (match[2]) tokens.push({text: matchedText, type: 'selector'});
177
+ else if (match[3]) tokens.push({text: matchedText, type: 'property'});
178
+ else if (match[4]) {
179
+ tokens.push({text: ':', type: 'plain'});
180
+ tokens.push({text: matchedText.substring(1), type: 'value'});
181
+ }
182
+
183
+ lastIndex = regex.lastIndex;
184
+ }
185
+ if (lastIndex < text.length) {
186
+ tokens.push({text: text.substring(lastIndex), type: 'plain'});
187
+ }
188
+ return tokens;
189
+ };
190
+
191
+ const tokenizeHTML = (text: string) => {
192
+ const tokens: {text: string; type: string}[] = [];
193
+ const regex =
194
+ /(<!--[\s\S]*?-->)|(<\/?[a-zA-Z0-9:-]+>?)|(\"(?:\\.|[^\"\\])*\"|\'(?:\\.|[^\'\\])*\')|([a-zA-Z0-9:-]+(?=\s*=))/g;
195
+
196
+ let lastIndex = 0;
197
+ let match;
198
+ while ((match = regex.exec(text)) !== null) {
199
+ if (regex.lastIndex === match.index) {
200
+ regex.lastIndex++;
201
+ }
202
+ if (match.index > lastIndex) {
203
+ tokens.push({
204
+ text: text.substring(lastIndex, match.index),
205
+ type: 'plain',
206
+ });
207
+ }
208
+ const matchedText = match[0];
209
+ if (match[1]) tokens.push({text: matchedText, type: 'comment'});
210
+ else if (match[2]) tokens.push({text: matchedText, type: 'tag'});
211
+ else if (match[3]) tokens.push({text: matchedText, type: 'string'});
212
+ else if (match[4]) tokens.push({text: matchedText, type: 'attribute'});
213
+
214
+ lastIndex = regex.lastIndex;
215
+ }
216
+ if (lastIndex < text.length) {
217
+ tokens.push({text: text.substring(lastIndex), type: 'plain'});
218
+ }
219
+ return tokens;
220
+ };
221
+
222
+ const getStyleForType = (type: string) => {
223
+ switch (type) {
224
+ case 'comment':
225
+ return styles.comment;
226
+ case 'string':
227
+ return styles.string;
228
+ case 'keyword':
229
+ return styles.keyword;
230
+ case 'builtin':
231
+ return styles.builtin;
232
+ case 'number':
233
+ return styles.number;
234
+ case 'tag':
235
+ return styles.tag;
236
+ case 'attribute':
237
+ return styles.attribute;
238
+ case 'selector':
239
+ return styles.selector;
240
+ case 'property':
241
+ return styles.property;
242
+ case 'value':
243
+ return styles.value;
244
+ default:
245
+ return [styles.plain, {color: AppColors.primaryBlack}];
246
+ }
247
+ };
248
+
249
+ const ArrowUpIcon = ({color = AppColors.slate500, size = 16}: {color?: string; size?: number}) => (
250
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
251
+ <Path d="M18 15l-6-6-6 6" stroke={color} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
252
+ </Svg>
253
+ );
254
+
255
+ const ArrowDownIcon = ({color = AppColors.slate500, size = 16}: {color?: string; size?: number}) => (
256
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
257
+ <Path d="M6 9l6 6 6-6" stroke={color} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
258
+ </Svg>
259
+ );
260
+
261
+ const CodeSnippetLine = React.memo(
262
+ ({
263
+ line,
264
+ lineIndex,
265
+ language,
266
+ search,
267
+ isActiveMatch,
268
+ }: {
269
+ line: string;
270
+ lineIndex: number;
271
+ language: 'html' | 'css' | 'javascript';
272
+ search: string;
273
+ isActiveMatch: boolean;
274
+ }) => {
275
+ const tokens = useMemo(() => {
276
+ if (language === 'html') return tokenizeHTML(line);
277
+ if (language === 'css') return tokenizeCSS(line);
278
+ return tokenizeJS(line);
279
+ }, [line, language]);
280
+
281
+ const renderTokenText = (text: string, type: string) => {
282
+ const baseStyle = getStyleForType(type);
283
+ if (!search) {
284
+ return <Text style={baseStyle}>{text}</Text>;
285
+ }
286
+
287
+ const parts = text.split(new RegExp(`(${escapeRegExp(search)})`, 'gi'));
288
+ return (
289
+ <Text style={baseStyle}>
290
+ {parts.map((part, index) => {
291
+ const isMatch = part.toLowerCase() === search.toLowerCase();
292
+ return isMatch ? (
293
+ <Text key={index} style={styles.highlight}>
294
+ {part}
295
+ </Text>
296
+ ) : (
297
+ part
298
+ );
299
+ })}
300
+ </Text>
301
+ );
302
+ };
303
+
304
+ return (
305
+ <View style={[styles.lineRow, isActiveMatch && styles.activeMatchRow]}>
306
+ {/* Gutter Line Number */}
307
+ <View style={[styles.gutter, {backgroundColor: AppColors.grayBackground, borderRightColor: AppColors.grayBorderSecondary}, isActiveMatch && styles.activeMatchGutter]}>
308
+ <Text style={[styles.lineNumber, {color: AppColors.grayTextWeak}, isActiveMatch && styles.activeMatchLineNumber]}>{lineIndex + 1}</Text>
309
+ </View>
310
+
311
+ {/* Highlighted Code Line */}
312
+ <View style={styles.codeLine}>
313
+ <Text style={[styles.codeLineText, {color: AppColors.primaryBlack}]}>
314
+ {tokens.length === 0 ? (
315
+ <Text style={[styles.plain, {color: AppColors.primaryBlack}]}> </Text>
316
+ ) : (
317
+ tokens.map((token, tokenIdx) => (
318
+ <React.Fragment key={tokenIdx}>
319
+ {renderTokenText(token.text, token.type)}
320
+ </React.Fragment>
321
+ ))
322
+ )}
323
+ </Text>
324
+ </View>
325
+ </View>
326
+ );
327
+ },
328
+ );
329
+
330
+ const CodeSnippet: React.FC<CodeSnippetProps> = ({
331
+ code,
332
+ language,
333
+ search,
334
+ }) => {
335
+ const [searchQuery, setSearchQuery] = useState('');
336
+ const [debouncedQuery, setDebouncedQuery] = useState('');
337
+ const [currentMatchIdx, setCurrentMatchIdx] = useState(-1);
338
+ const [visibleLinesCount, setVisibleLinesCount] = useState(200);
339
+
340
+ const flatListRef = useRef<FlatList>(null);
341
+
342
+ // Sync prop search to searchQuery
343
+ useEffect(() => {
344
+ if (search !== undefined) {
345
+ setSearchQuery(search);
346
+ }
347
+ }, [search]);
348
+
349
+ // Debounce search query updates
350
+ useEffect(() => {
351
+ const timer = setTimeout(() => {
352
+ setDebouncedQuery(searchQuery);
353
+ }, 300);
354
+ return () => clearTimeout(timer);
355
+ }, [searchQuery]);
356
+
357
+ // If code is extremely large (e.g. > 150KB), skip the pretty printers
358
+ // to avoid blocking the single JS thread.
359
+ const formattedCode = useMemo(() => {
360
+ if (!code) return '';
361
+ if (code.length > 150000) {
362
+ return code;
363
+ }
364
+ if (language === 'html') return formatHTML(code);
365
+ if (language === 'css') return formatCSS(code);
366
+ if (language === 'javascript') return formatJS(code);
367
+ return code;
368
+ }, [code, language]);
369
+
370
+ const lines = useMemo(() => {
371
+ if (!formattedCode) return [];
372
+ return formattedCode.split(/\r?\n/);
373
+ }, [formattedCode]);
374
+
375
+ // Find all line indices matching the search query
376
+ const matches = useMemo(() => {
377
+ if (!debouncedQuery.trim() || debouncedQuery.length < 2) return [];
378
+ const query = debouncedQuery.toLowerCase();
379
+ const indices: number[] = [];
380
+ for (let i = 0; i < lines.length; i++) {
381
+ if (lines[i].toLowerCase().includes(query)) {
382
+ indices.push(i);
383
+ }
384
+ }
385
+ return indices;
386
+ }, [debouncedQuery, lines]);
387
+
388
+ // Reset scroll and states on code/language changes
389
+ useEffect(() => {
390
+ setVisibleLinesCount(200);
391
+ setSearchQuery(search || '');
392
+ setCurrentMatchIdx(-1);
393
+ }, [code, language]);
394
+
395
+ // Auto-scroll to the first match when search query returns matches
396
+ useEffect(() => {
397
+ if (matches.length > 0) {
398
+ setCurrentMatchIdx(0);
399
+ scrollToMatch(0);
400
+ } else {
401
+ setCurrentMatchIdx(-1);
402
+ }
403
+ }, [matches]);
404
+
405
+ const scrollToMatch = (matchIdx: number) => {
406
+ if (matches.length === 0 || matchIdx < 0 || matchIdx >= matches.length) return;
407
+ const lineIdx = matches[matchIdx];
408
+
409
+ // Ensure the targeted index is loaded in FlatList
410
+ if (lineIdx >= visibleLinesCount) {
411
+ setVisibleLinesCount(lineIdx + 100);
412
+ }
413
+
414
+ // Schedule scroll after state has applied
415
+ setTimeout(() => {
416
+ try {
417
+ flatListRef.current?.scrollToIndex({
418
+ index: lineIdx,
419
+ animated: false,
420
+ viewPosition: 0.3,
421
+ });
422
+ } catch (e) {
423
+ // Fallback recovery is handled by onScrollToIndexFailed
424
+ }
425
+ }, 50);
426
+ };
427
+
428
+ const onScrollToIndexFailed = (error: any) => {
429
+ flatListRef.current?.scrollToOffset({
430
+ offset: error.averageItemLength * error.index,
431
+ animated: false,
432
+ });
433
+ setTimeout(() => {
434
+ try {
435
+ flatListRef.current?.scrollToIndex({
436
+ index: error.index,
437
+ animated: false,
438
+ viewPosition: 0.3,
439
+ });
440
+ } catch (err) {
441
+ console.warn('Scroll to line index failed after fallback retry:', err);
442
+ }
443
+ }, 60);
444
+ };
445
+
446
+ const handleNextMatch = () => {
447
+ if (matches.length === 0) return;
448
+ const nextIdx = (currentMatchIdx + 1) % matches.length;
449
+ setCurrentMatchIdx(nextIdx);
450
+ scrollToMatch(nextIdx);
451
+ };
452
+
453
+ const handlePrevMatch = () => {
454
+ if (matches.length === 0) return;
455
+ const prevIdx = (currentMatchIdx - 1 + matches.length) % matches.length;
456
+ setCurrentMatchIdx(prevIdx);
457
+ scrollToMatch(prevIdx);
458
+ };
459
+
460
+ const visibleLines = useMemo(() => {
461
+ return lines.slice(0, visibleLinesCount);
462
+ }, [lines, visibleLinesCount]);
463
+
464
+ const handleEndReached = () => {
465
+ if (visibleLinesCount < lines.length) {
466
+ setVisibleLinesCount(prev => Math.min(prev + 150, lines.length));
467
+ }
468
+ };
469
+
470
+ const renderLine = ({item, index}: {item: string; index: number}) => {
471
+ const isActiveMatch = matches.length > 0 && currentMatchIdx >= 0 && index === matches[currentMatchIdx];
472
+ return (
473
+ <CodeSnippetLine
474
+ line={item}
475
+ lineIndex={index}
476
+ language={language}
477
+ search={debouncedQuery}
478
+ isActiveMatch={isActiveMatch}
479
+ />
480
+ );
481
+ };
482
+
483
+ return (
484
+ <View style={{flex: 1}}>
485
+ {/* Search Header Row */}
486
+ <View style={styles.searchRow}>
487
+ <View style={[styles.searchBar, {backgroundColor: AppColors.primaryLight, borderColor: AppColors.grayBorderSecondary}]}>
488
+ <SearchIcon color={AppColors.grayTextWeak} size={15} />
489
+ <TextInput
490
+ placeholder={`Search ${language.toUpperCase()}...`}
491
+ placeholderTextColor={AppColors.grayTextWeak}
492
+ value={searchQuery}
493
+ onChangeText={setSearchQuery}
494
+ style={[styles.searchInput, {color: AppColors.primaryBlack}]}
495
+ autoCorrect={false}
496
+ autoCapitalize="none"
497
+ />
498
+
499
+ {/* Matches Info */}
500
+ {debouncedQuery.length >= 2 && (
501
+ <Text style={[styles.matchCountText, {backgroundColor: AppColors.grayBackground, color: AppColors.grayTextWeak}]}>
502
+ {matches.length > 0 ? `${currentMatchIdx + 1}/${matches.length}` : '0/0'}
503
+ </Text>
504
+ )}
505
+
506
+ {searchQuery.length > 0 && (
507
+ <Pressable onPress={() => setSearchQuery('')} hitSlop={8} style={styles.clearBtn}>
508
+ <ClearIcon color={AppColors.grayTextWeak} size={12} />
509
+ </Pressable>
510
+ )}
511
+ </View>
512
+
513
+ {/* Up / Down Arrow Navigation Buttons */}
514
+ {matches.length > 0 && (
515
+ <View style={[styles.navArrowsGroup, {backgroundColor: AppColors.primaryLight, borderColor: AppColors.grayBorderSecondary}]}>
516
+ <TouchableScale onPress={handlePrevMatch} hitSlop={8} style={styles.navArrowBtn}>
517
+ <ArrowUpIcon color={AppColors.grayTextStrong} size={14} />
518
+ </TouchableScale>
519
+ <TouchableScale onPress={handleNextMatch} hitSlop={8} style={styles.navArrowBtn}>
520
+ <ArrowDownIcon color={AppColors.grayTextStrong} size={14} />
521
+ </TouchableScale>
522
+ </View>
523
+ )}
524
+
525
+ <CopyButton value={code} label={`${language.toUpperCase()} Source`} />
526
+ </View>
527
+
528
+ {/* Code Snippet list container */}
529
+ <View style={[styles.container, {backgroundColor: AppColors.grayBackground, borderColor: AppColors.grayBorderSecondary}]}>
530
+ <FlatList
531
+ ref={flatListRef}
532
+ data={visibleLines}
533
+ renderItem={renderLine}
534
+ keyExtractor={(_, index) => String(index)}
535
+ maxToRenderPerBatch={30}
536
+ windowSize={5}
537
+ initialNumToRender={50}
538
+ removeClippedSubviews={Platform.OS === 'android'}
539
+ onEndReached={handleEndReached}
540
+ onEndReachedThreshold={0.5}
541
+ onScrollToIndexFailed={onScrollToIndexFailed}
542
+ />
543
+ </View>
544
+ </View>
545
+ );
546
+ };
547
+
548
+ const monoFont = AppFonts.interRegular;
549
+
550
+ const styles = StyleSheet.create({
551
+ container: {
552
+ flex: 1,
553
+ backgroundColor: AppColors.slate50,
554
+ borderRadius: 8,
555
+ borderWidth: 1,
556
+ borderColor: AppColors.slate200,
557
+ overflow: 'hidden',
558
+ },
559
+ searchRow: {
560
+ flexDirection: 'row',
561
+ alignItems: 'center',
562
+ marginBottom: 6,
563
+ gap: 8,
564
+ },
565
+ searchBar: {
566
+ flex: 1,
567
+ flexDirection: 'row',
568
+ alignItems: 'center',
569
+ backgroundColor: AppColors.white,
570
+ borderRadius: 8,
571
+ borderWidth: 1,
572
+ borderColor: AppColors.slate200,
573
+ paddingHorizontal: 8,
574
+ height: 32,
575
+ },
576
+ searchInput: {
577
+ flex: 1,
578
+ fontFamily: AppFonts.interRegular,
579
+ fontSize: 12,
580
+ color: AppColors.slate900,
581
+ marginLeft: 6,
582
+ paddingVertical: 0,
583
+ },
584
+ matchCountText: {
585
+ fontFamily: AppFonts.interMedium,
586
+ fontSize: 10,
587
+ color: AppColors.slate500,
588
+ marginHorizontal: 6,
589
+ backgroundColor: AppColors.slate100,
590
+ paddingHorizontal: 6,
591
+ paddingVertical: 2,
592
+ borderRadius: 4,
593
+ },
594
+ clearBtn: {
595
+ padding: 4,
596
+ },
597
+ navArrowsGroup: {
598
+ flexDirection: 'row',
599
+ alignItems: 'center',
600
+ backgroundColor: AppColors.white,
601
+ borderRadius: 8,
602
+ borderWidth: 1,
603
+ borderColor: AppColors.slate200,
604
+ height: 32,
605
+ paddingHorizontal: 2,
606
+ },
607
+ navArrowBtn: {
608
+ padding: 5,
609
+ borderRadius: 6,
610
+ alignItems: 'center',
611
+ justifyContent: 'center',
612
+ },
613
+ lineRow: {
614
+ flexDirection: 'row',
615
+ alignItems: 'stretch',
616
+ minHeight: 20,
617
+ paddingVertical: 1,
618
+ },
619
+ activeMatchRow: {
620
+ backgroundColor: `${AppColors.amber600}38`,
621
+ borderLeftWidth: 4,
622
+ borderLeftColor: AppColors.amber600,
623
+ },
624
+ gutter: {
625
+ width: 40,
626
+ backgroundColor: AppColors.slate100,
627
+ borderRightWidth: 1,
628
+ borderRightColor: AppColors.slate200,
629
+ justifyContent: 'flex-start',
630
+ alignItems: 'flex-end',
631
+ paddingRight: 6,
632
+ paddingTop: 1,
633
+ },
634
+ activeMatchGutter: {
635
+ backgroundColor: `${AppColors.amber600}59`,
636
+ borderRightColor: `${AppColors.amber600}80`,
637
+ },
638
+ lineNumber: {
639
+ fontFamily: monoFont,
640
+ fontSize: 9,
641
+ color: AppColors.slate400,
642
+ },
643
+ activeMatchLineNumber: {
644
+ color: AppColors.amber800,
645
+ fontWeight: 'bold',
646
+ },
647
+ codeLine: {
648
+ flex: 1,
649
+ paddingLeft: 10,
650
+ paddingRight: 12,
651
+ justifyContent: 'center',
652
+ },
653
+ codeLineText: {
654
+ fontFamily: monoFont,
655
+ fontSize: 10.5,
656
+ color: AppColors.slate700,
657
+ flexWrap: 'wrap',
658
+ },
659
+ plain: {
660
+ fontFamily: monoFont,
661
+ fontSize: 10.5,
662
+ color: AppColors.slate700,
663
+ },
664
+ comment: {
665
+ fontFamily: monoFont,
666
+ fontSize: 10.5,
667
+ color: AppColors.slate500,
668
+ fontStyle: 'italic',
669
+ },
670
+ string: {
671
+ fontFamily: monoFont,
672
+ fontSize: 10.5,
673
+ color: AppColors.amber700,
674
+ },
675
+ keyword: {
676
+ fontFamily: monoFont,
677
+ fontSize: 10.5,
678
+ color: AppColors.violet600,
679
+ fontWeight: 'bold',
680
+ },
681
+ builtin: {
682
+ fontFamily: monoFont,
683
+ fontSize: 10.5,
684
+ color: AppColors.teal600,
685
+ },
686
+ number: {
687
+ fontFamily: monoFont,
688
+ fontSize: 10.5,
689
+ color: AppColors.blue600,
690
+ },
691
+ tag: {
692
+ fontFamily: monoFont,
693
+ fontSize: 10.5,
694
+ color: AppColors.indigo600,
695
+ fontWeight: 'bold',
696
+ },
697
+ attribute: {
698
+ fontFamily: monoFont,
699
+ fontSize: 10.5,
700
+ color: AppColors.teal600,
701
+ },
702
+ selector: {
703
+ fontFamily: monoFont,
704
+ fontSize: 10.5,
705
+ color: AppColors.violet600,
706
+ fontWeight: 'bold',
707
+ },
708
+ property: {
709
+ fontFamily: monoFont,
710
+ fontSize: 10.5,
711
+ color: AppColors.teal600,
712
+ },
713
+ value: {
714
+ fontFamily: monoFont,
715
+ fontSize: 10.5,
716
+ color: AppColors.orange600,
717
+ },
718
+ highlight: {
719
+ backgroundColor: AppColors.highlightYellow,
720
+ color: AppColors.black,
721
+ },
722
+ });
723
+
724
+ export default CodeSnippet;
725
+