@buoy-gg/storage 2.1.14 → 2.1.16

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.
@@ -9,185 +9,91 @@ var _reactNative = require("react-native");
9
9
  var _lineDiff = require("../../../utils/lineDiff");
10
10
  var _DiffSummary = require("../components/DiffSummary");
11
11
  var _jsxRuntime = require("react/jsx-runtime");
12
+ const DEFAULT_OPTIONS = {
13
+ hideLineNumbers: false,
14
+ disableWordDiff: false,
15
+ showDiffOnly: false,
16
+ compareMethod: "words",
17
+ contextLines: 3,
18
+ lineOffset: 0
19
+ };
12
20
  function ThemedSplitView({
13
21
  oldValue,
14
22
  newValue,
15
23
  theme,
16
- options = {
17
- hideLineNumbers: false,
18
- disableWordDiff: false,
19
- showDiffOnly: false,
20
- compareMethod: "words",
21
- contextLines: 3,
22
- lineOffset: 0
23
- },
24
+ options = DEFAULT_OPTIONS,
24
25
  showThemeName = false
25
26
  }) {
26
- // Compute line-by-line diff with options
27
- const diffComputeOptions = {
27
+ const dynamicStyles = (0, _react.useMemo)(() => createDynamicStyles(theme), [theme]);
28
+ const diffComputeOptions = (0, _react.useMemo)(() => ({
28
29
  compareMethod: options.compareMethod,
29
30
  disableWordDiff: options.disableWordDiff,
30
31
  showDiffOnly: options.showDiffOnly,
31
32
  contextLines: options.contextLines
32
- };
33
- const lineDiffs = (0, _lineDiff.computeLineDiff)(oldValue, newValue, diffComputeOptions);
33
+ }), [options.compareMethod, options.disableWordDiff, options.showDiffOnly, options.contextLines]);
34
+ const lineDiffs = (0, _react.useMemo)(() => (0, _lineDiff.computeLineDiff)(oldValue, newValue, diffComputeOptions), [oldValue, newValue, diffComputeOptions]);
34
35
 
35
- // Create dynamic styles based on theme
36
- const dynamicStyles = createDynamicStyles(theme);
36
+ // One pass for the summary instead of three .filter() calls per render.
37
+ const summary = (0, _react.useMemo)(() => {
38
+ let added = 0;
39
+ let removed = 0;
40
+ let modified = 0;
41
+ for (const d of lineDiffs) {
42
+ if (d.type === _lineDiff.DiffType.ADDED) added++;else if (d.type === _lineDiff.DiffType.REMOVED) removed++;else if (d.type === _lineDiff.DiffType.MODIFIED) modified++;
43
+ }
44
+ return {
45
+ added,
46
+ removed,
47
+ modified
48
+ };
49
+ }, [lineDiffs]);
37
50
 
38
- // Render word diff content
39
- const renderWordDiff = wordDiffs => {
40
- return wordDiffs.map((word, idx) => {
41
- let backgroundColor = "transparent";
42
- if (word.type === _lineDiff.DiffType.ADDED) {
43
- backgroundColor = theme.addedWordHighlight;
44
- } else if (word.type === _lineDiff.DiffType.REMOVED) {
45
- backgroundColor = theme.removedWordHighlight;
46
- }
47
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
48
- style: [dynamicStyles.wordDiff, {
49
- backgroundColor
50
- }],
51
- children: word.value
52
- }, idx);
53
- });
54
- };
51
+ // Auto-collapse runs of unchanged lines that aren't near a change. Keeps
52
+ // CONTEXT_LINES on either side of every change visible (GitHub-style), folds
53
+ // the rest into a tappable header.
54
+ const [expandedFolds, setExpandedFolds] = (0, _react.useState)(() => new Set());
55
55
 
56
- // Get colors for diff type
57
- const getDiffColors = (type, isModified = false) => {
58
- if (isModified) {
59
- return {
60
- background: theme.modifiedBackground,
61
- text: theme.modifiedText,
62
- markerBg: theme.markerModifiedBackground
63
- };
56
+ // Reset fold expansion when the user actually switches comparisons. Parent
57
+ // memoizes oldValue/newValue, so this only fires on a real change — not on
58
+ // every re-render. First run is a no-op (initial state is already empty).
59
+ const hasMountedRef = (0, _react.useRef)(false);
60
+ (0, _react.useEffect)(() => {
61
+ if (!hasMountedRef.current) {
62
+ hasMountedRef.current = true;
63
+ return;
64
64
  }
65
- switch (type) {
66
- case _lineDiff.DiffType.ADDED:
67
- return {
68
- background: theme.addedBackground,
69
- text: theme.addedText,
70
- markerBg: theme.markerAddedBackground
71
- };
72
- case _lineDiff.DiffType.REMOVED:
73
- return {
74
- background: theme.removedBackground,
75
- text: theme.removedText,
76
- markerBg: theme.markerRemovedBackground
77
- };
78
- case _lineDiff.DiffType.DEFAULT:
79
- return {
80
- background: theme.unchangedBackground,
81
- text: theme.unchangedText,
82
- markerBg: "transparent"
83
- };
84
- default:
85
- return {
86
- background: theme.unchangedBackground,
87
- text: theme.unchangedText,
88
- markerBg: "transparent"
89
- };
90
- }
91
- };
92
-
93
- // Render a single line side (left or right)
94
- const renderLineSide = (lineNumber, content, type, marker, isEmpty = false) => {
95
- if (isEmpty) {
96
- return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
97
- children: [!options.hideLineNumbers && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
98
- style: [dynamicStyles.gutter, dynamicStyles.emptyGutter],
99
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
100
- style: dynamicStyles.lineNumber,
101
- children: " "
102
- })
103
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
104
- style: [dynamicStyles.marker, dynamicStyles.emptyMarker],
105
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
106
- style: dynamicStyles.markerText,
107
- children: " "
108
- })
109
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
110
- style: [dynamicStyles.contentCell, dynamicStyles.emptyContent],
111
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
112
- style: dynamicStyles.content,
113
- children: " "
114
- })
115
- })]
65
+ setExpandedFolds(new Set());
66
+ }, [oldValue, newValue]);
67
+ const items = (0, _react.useMemo)(() => buildSplitItems(lineDiffs, expandedFolds), [lineDiffs, expandedFolds]);
68
+ const toggleFold = (0, _react.useCallback)(foldId => {
69
+ setExpandedFolds(prev => {
70
+ const next = new Set(prev);
71
+ if (next.has(foldId)) next.delete(foldId);else next.add(foldId);
72
+ return next;
73
+ });
74
+ }, []);
75
+ const keyExtractor = (0, _react.useCallback)(item => item.kind === "row" ? `r:${item.diffIndex}` : `f:${item.foldId}`, []);
76
+ const renderItem = (0, _react.useCallback)(({
77
+ item
78
+ }) => {
79
+ if (item.kind === "fold") {
80
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(FoldHeader, {
81
+ foldId: item.foldId,
82
+ count: item.count,
83
+ expanded: item.expanded,
84
+ theme: theme,
85
+ styles: dynamicStyles,
86
+ onToggle: toggleFold
116
87
  });
117
88
  }
118
- const colors = getDiffColors(type);
119
- return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
120
- children: [!options.hideLineNumbers && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
121
- style: [dynamicStyles.gutter, {
122
- backgroundColor: theme.lineNumberBackground
123
- }],
124
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
125
- style: dynamicStyles.lineNumber,
126
- children: lineNumber || " "
127
- })
128
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
129
- style: [dynamicStyles.marker, {
130
- backgroundColor: colors.markerBg
131
- }],
132
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
133
- style: [dynamicStyles.markerText, {
134
- color: theme.markerText
135
- }],
136
- children: marker
137
- })
138
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
139
- style: [dynamicStyles.contentCell, {
140
- backgroundColor: colors.background
141
- }],
142
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
143
- style: [dynamicStyles.content, {
144
- color: colors.text
145
- }],
146
- children: Array.isArray(content) ? renderWordDiff(content) : content || " "
147
- })
148
- })]
89
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(DiffRow, {
90
+ diff: item.diff,
91
+ showSeparator: false,
92
+ theme: theme,
93
+ options: options,
94
+ styles: dynamicStyles
149
95
  });
150
- };
151
-
152
- // Check if we should show a separator (gap in line numbers)
153
- const shouldShowSeparator = (idx, diffs) => {
154
- if (!options.showDiffOnly || idx === 0) return false;
155
- const prevDiff = diffs[idx - 1];
156
- const currDiff = diffs[idx];
157
-
158
- // Check for gap in line numbers
159
- const leftGap = currDiff.leftLineNumber && prevDiff.leftLineNumber && currDiff.leftLineNumber - prevDiff.leftLineNumber > 1;
160
- const rightGap = currDiff.rightLineNumber && prevDiff.rightLineNumber && currDiff.rightLineNumber - prevDiff.rightLineNumber > 1;
161
- return leftGap || rightGap;
162
- };
163
-
164
- // Render a complete row with both left and right sides
165
- const renderDiffRow = (diff, idx, diffs) => {
166
- const isRemoved = diff.type === _lineDiff.DiffType.REMOVED;
167
- const isAdded = diff.type === _lineDiff.DiffType.ADDED;
168
- const isModified = diff.type === _lineDiff.DiffType.MODIFIED;
169
- const isDefault = diff.type === _lineDiff.DiffType.DEFAULT;
170
- return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_react.Fragment, {
171
- children: [shouldShowSeparator(idx, diffs) && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
172
- style: dynamicStyles.separator,
173
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
174
- style: dynamicStyles.separatorText,
175
- children: "\u2022 \u2022 \u2022"
176
- })
177
- }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
178
- style: dynamicStyles.row,
179
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
180
- style: dynamicStyles.leftSide,
181
- children: isRemoved || isModified || isDefault ? renderLineSide(diff.leftLineNumber, diff.leftContent, isModified ? _lineDiff.DiffType.REMOVED : diff.type, isRemoved || isModified ? "-" : " ") : renderLineSide(undefined, undefined, _lineDiff.DiffType.DEFAULT, " ", true)
182
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
183
- style: dynamicStyles.centerDivider
184
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
185
- style: dynamicStyles.rightSide,
186
- children: isAdded || isModified || isDefault ? renderLineSide(diff.rightLineNumber, diff.rightContent, isModified ? _lineDiff.DiffType.ADDED : diff.type, isAdded || isModified ? "+" : " ") : renderLineSide(undefined, undefined, _lineDiff.DiffType.DEFAULT, " ", true)
187
- })]
188
- })]
189
- }, idx);
190
- };
96
+ }, [theme, options, dynamicStyles, toggleFold]);
191
97
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
192
98
  style: [dynamicStyles.container, theme.glowColor && {
193
99
  shadowColor: theme.glowColor,
@@ -225,24 +131,276 @@ function ThemedSplitView({
225
131
  })
226
132
  })]
227
133
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_DiffSummary.DiffSummary, {
228
- added: lineDiffs.filter(d => d.type === _lineDiff.DiffType.ADDED).length,
229
- removed: lineDiffs.filter(d => d.type === _lineDiff.DiffType.REMOVED).length,
230
- modified: lineDiffs.filter(d => d.type === _lineDiff.DiffType.MODIFIED).length,
134
+ added: summary.added,
135
+ removed: summary.removed,
136
+ modified: summary.modified,
231
137
  theme: theme
232
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
138
+ }), lineDiffs.length === 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
139
+ style: dynamicStyles.emptyState,
140
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
141
+ style: dynamicStyles.emptyText,
142
+ children: options.showDiffOnly ? "No differences found" : "No content to display"
143
+ })
144
+ }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.FlatList, {
233
145
  style: dynamicStyles.scrollView,
234
- showsVerticalScrollIndicator: false,
235
146
  contentContainerStyle: dynamicStyles.scrollContent,
236
- children: lineDiffs.length === 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
237
- style: dynamicStyles.emptyState,
238
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
239
- style: dynamicStyles.emptyText,
240
- children: options.showDiffOnly ? "No differences found" : "No content to display"
147
+ data: items,
148
+ renderItem: renderItem,
149
+ keyExtractor: keyExtractor,
150
+ showsVerticalScrollIndicator: false,
151
+ initialNumToRender: 20,
152
+ maxToRenderPerBatch: 20,
153
+ windowSize: 7,
154
+ removeClippedSubviews: true
155
+ })]
156
+ });
157
+ }
158
+
159
+ // Number of context lines kept visible on either side of every change.
160
+ const CONTEXT_LINES = 3;
161
+ // Don't bother folding tiny gaps — toggling them is more friction than scroll.
162
+ const MIN_FOLD_SIZE = 4;
163
+ function buildSplitItems(lineDiffs, expandedFolds) {
164
+ const n = lineDiffs.length;
165
+ if (n === 0) return [];
166
+
167
+ // Mark every line that should always be visible: any change, plus
168
+ // CONTEXT_LINES on either side of it.
169
+ const visible = new Array(n).fill(false);
170
+ for (let i = 0; i < n; i++) {
171
+ if (lineDiffs[i].type !== _lineDiff.DiffType.DEFAULT) {
172
+ const start = Math.max(0, i - CONTEXT_LINES);
173
+ const end = Math.min(n - 1, i + CONTEXT_LINES);
174
+ for (let k = start; k <= end; k++) visible[k] = true;
175
+ }
176
+ }
177
+ const items = [];
178
+ let foldId = 0;
179
+ let i = 0;
180
+ while (i < n) {
181
+ if (visible[i]) {
182
+ items.push({
183
+ kind: "row",
184
+ diff: lineDiffs[i],
185
+ diffIndex: i
186
+ });
187
+ i++;
188
+ continue;
189
+ }
190
+ let j = i;
191
+ while (j < n && !visible[j]) j++;
192
+ const runLength = j - i;
193
+ if (runLength < MIN_FOLD_SIZE) {
194
+ for (let k = i; k < j; k++) {
195
+ items.push({
196
+ kind: "row",
197
+ diff: lineDiffs[k],
198
+ diffIndex: k
199
+ });
200
+ }
201
+ } else {
202
+ const id = foldId++;
203
+ const expanded = expandedFolds.has(id);
204
+ items.push({
205
+ kind: "fold",
206
+ foldId: id,
207
+ count: runLength,
208
+ expanded
209
+ });
210
+ if (expanded) {
211
+ for (let k = i; k < j; k++) {
212
+ items.push({
213
+ kind: "row",
214
+ diff: lineDiffs[k],
215
+ diffIndex: k
216
+ });
217
+ }
218
+ }
219
+ }
220
+ i = j;
221
+ }
222
+ return items;
223
+ }
224
+ const FoldHeader = /*#__PURE__*/(0, _react.memo)(function FoldHeader({
225
+ foldId,
226
+ count,
227
+ expanded,
228
+ theme,
229
+ styles,
230
+ onToggle
231
+ }) {
232
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
233
+ style: [styles.foldHeader, {
234
+ backgroundColor: theme.separatorBackground
235
+ }],
236
+ onPress: () => onToggle(foldId),
237
+ activeOpacity: 0.7,
238
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
239
+ style: [styles.foldHeaderText, {
240
+ color: theme.separatorText
241
+ }],
242
+ children: expanded ? `▼ ${count} unchanged line${count === 1 ? "" : "s"} — tap to hide` : `▶ ${count} unchanged line${count === 1 ? "" : "s"} — tap to expand`
243
+ })
244
+ });
245
+ });
246
+ const DiffRow = /*#__PURE__*/(0, _react.memo)(function DiffRow({
247
+ diff,
248
+ showSeparator,
249
+ theme,
250
+ options,
251
+ styles
252
+ }) {
253
+ const isRemoved = diff.type === _lineDiff.DiffType.REMOVED;
254
+ const isAdded = diff.type === _lineDiff.DiffType.ADDED;
255
+ const isModified = diff.type === _lineDiff.DiffType.MODIFIED;
256
+ const isDefault = diff.type === _lineDiff.DiffType.DEFAULT;
257
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_react.Fragment, {
258
+ children: [showSeparator && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
259
+ style: styles.separator,
260
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
261
+ style: styles.separatorText,
262
+ children: "\u2022 \u2022 \u2022"
263
+ })
264
+ }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
265
+ style: styles.row,
266
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
267
+ style: styles.leftSide,
268
+ children: isRemoved || isModified || isDefault ? /*#__PURE__*/(0, _jsxRuntime.jsx)(LineSide, {
269
+ lineNumber: diff.leftLineNumber,
270
+ content: diff.leftContent,
271
+ type: isModified ? _lineDiff.DiffType.REMOVED : diff.type,
272
+ marker: isRemoved || isModified ? "-" : " ",
273
+ theme: theme,
274
+ options: options,
275
+ styles: styles
276
+ }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(EmptyLineSide, {
277
+ options: options,
278
+ styles: styles
279
+ })
280
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
281
+ style: styles.centerDivider
282
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
283
+ style: styles.rightSide,
284
+ children: isAdded || isModified || isDefault ? /*#__PURE__*/(0, _jsxRuntime.jsx)(LineSide, {
285
+ lineNumber: diff.rightLineNumber,
286
+ content: diff.rightContent,
287
+ type: isModified ? _lineDiff.DiffType.ADDED : diff.type,
288
+ marker: isAdded || isModified ? "+" : " ",
289
+ theme: theme,
290
+ options: options,
291
+ styles: styles
292
+ }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(EmptyLineSide, {
293
+ options: options,
294
+ styles: styles
241
295
  })
242
- }) : lineDiffs.map((diff, idx) => renderDiffRow(diff, idx, lineDiffs))
296
+ })]
297
+ })]
298
+ });
299
+ });
300
+ function LineSide({
301
+ lineNumber,
302
+ content,
303
+ type,
304
+ marker,
305
+ theme,
306
+ options,
307
+ styles
308
+ }) {
309
+ const colors = getDiffColors(type, theme);
310
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_react.Fragment, {
311
+ children: [!options.hideLineNumbers && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
312
+ style: [styles.gutter, {
313
+ backgroundColor: theme.lineNumberBackground
314
+ }],
315
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
316
+ style: styles.lineNumber,
317
+ children: lineNumber || " "
318
+ })
319
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
320
+ style: [styles.marker, {
321
+ backgroundColor: colors.markerBg
322
+ }],
323
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
324
+ style: [styles.markerText, {
325
+ color: theme.markerText
326
+ }],
327
+ children: marker
328
+ })
329
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
330
+ style: [styles.contentCell, {
331
+ backgroundColor: colors.background
332
+ }],
333
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
334
+ style: [styles.content, {
335
+ color: colors.text
336
+ }],
337
+ children: Array.isArray(content) ? content.map((word, idx) => {
338
+ let backgroundColor = "transparent";
339
+ if (word.type === _lineDiff.DiffType.ADDED) {
340
+ backgroundColor = theme.addedWordHighlight;
341
+ } else if (word.type === _lineDiff.DiffType.REMOVED) {
342
+ backgroundColor = theme.removedWordHighlight;
343
+ }
344
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
345
+ style: [styles.wordDiff, {
346
+ backgroundColor
347
+ }],
348
+ children: word.value
349
+ }, idx);
350
+ }) : content || " "
351
+ })
243
352
  })]
244
353
  });
245
354
  }
355
+ function EmptyLineSide({
356
+ options,
357
+ styles
358
+ }) {
359
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_react.Fragment, {
360
+ children: [!options.hideLineNumbers && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
361
+ style: [styles.gutter, styles.emptyGutter],
362
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
363
+ style: styles.lineNumber,
364
+ children: " "
365
+ })
366
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
367
+ style: [styles.marker, styles.emptyMarker],
368
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
369
+ style: styles.markerText,
370
+ children: " "
371
+ })
372
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
373
+ style: [styles.contentCell, styles.emptyContent],
374
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
375
+ style: styles.content,
376
+ children: " "
377
+ })
378
+ })]
379
+ });
380
+ }
381
+ function getDiffColors(type, theme) {
382
+ switch (type) {
383
+ case _lineDiff.DiffType.ADDED:
384
+ return {
385
+ background: theme.addedBackground,
386
+ text: theme.addedText,
387
+ markerBg: theme.markerAddedBackground
388
+ };
389
+ case _lineDiff.DiffType.REMOVED:
390
+ return {
391
+ background: theme.removedBackground,
392
+ text: theme.removedText,
393
+ markerBg: theme.markerRemovedBackground
394
+ };
395
+ case _lineDiff.DiffType.DEFAULT:
396
+ default:
397
+ return {
398
+ background: theme.unchangedBackground,
399
+ text: theme.unchangedText,
400
+ markerBg: "transparent"
401
+ };
402
+ }
403
+ }
246
404
 
247
405
  // Create dynamic styles based on theme
248
406
  function createDynamicStyles(theme) {
@@ -404,6 +562,20 @@ function createDynamicStyles(theme) {
404
562
  fontFamily: "monospace",
405
563
  letterSpacing: 2
406
564
  },
565
+ foldHeader: {
566
+ paddingVertical: 6,
567
+ paddingHorizontal: 12,
568
+ alignItems: "center",
569
+ justifyContent: "center",
570
+ borderTopWidth: 1,
571
+ borderBottomWidth: 1,
572
+ borderColor: theme.borderColor
573
+ },
574
+ foldHeaderText: {
575
+ fontSize: 10,
576
+ fontFamily: "monospace",
577
+ letterSpacing: 0.5
578
+ },
407
579
  emptyState: {
408
580
  padding: 40,
409
581
  alignItems: "center",
@@ -67,6 +67,15 @@ function formatTimeWithMs(date) {
67
67
  const ms = String(date.getMilliseconds()).padStart(3, "0");
68
68
  return `${h}:${m}:${s}.${ms}`;
69
69
  }
70
+ const SPLIT_VIEW_OPTIONS = {
71
+ hideLineNumbers: false,
72
+ disableWordDiff: false,
73
+ showDiffOnly: false,
74
+ compareMethod: "words",
75
+ contextLines: 3,
76
+ lineOffset: 0
77
+ };
78
+ const SPLIT_VIEW_DIFFERENCES = [];
70
79
  function StorageEventDetailContent({
71
80
  conversation,
72
81
  selectedEventIndex = 0,
@@ -263,38 +272,30 @@ function StorageEventDetailContent({
263
272
  });
264
273
  }, [valueToShow, action, actionColor, valueType]);
265
274
 
275
+ // Memoized so the diff viewers don't see new oldValue/newValue refs on every
276
+ // re-render. parseValue runs JSON.parse, which would otherwise produce fresh
277
+ // object identities every time and force expensive recomputations downstream
278
+ // (line diff, tree diff, expansion-state effects, etc.).
279
+ const previousValue = (0, _react.useMemo)(() => (0, _sharedUi.parseValue)(leftEvent?.data?.value ?? null), [leftEvent]);
280
+ const currentValue = (0, _react.useMemo)(() => (0, _sharedUi.parseValue)(rightEvent?.data?.value), [rightEvent]);
281
+
266
282
  // Render diff content
267
283
  const renderDiffContent = (0, _react.useCallback)(() => {
268
- const previousValue = (0, _sharedUi.parseValue)(leftEvent?.data?.value ?? null);
269
- const currentValue = (0, _sharedUi.parseValue)(rightEvent?.data?.value);
270
284
  if (diffMode === "split") {
271
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
272
- style: {
273
- flex: 1
274
- },
275
- showsVerticalScrollIndicator: true,
276
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_ThemedSplitView.ThemedSplitView, {
277
- oldValue: previousValue,
278
- newValue: currentValue,
279
- differences: [],
280
- theme: _diffThemes.diffThemes.devToolsDefault,
281
- options: {
282
- hideLineNumbers: false,
283
- disableWordDiff: false,
284
- showDiffOnly: false,
285
- compareMethod: "words",
286
- contextLines: 3,
287
- lineOffset: 0
288
- },
289
- showThemeName: false
290
- })
285
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_ThemedSplitView.ThemedSplitView, {
286
+ oldValue: previousValue,
287
+ newValue: currentValue,
288
+ differences: SPLIT_VIEW_DIFFERENCES,
289
+ theme: _diffThemes.diffThemes.devToolsDefault,
290
+ options: SPLIT_VIEW_OPTIONS,
291
+ showThemeName: false
291
292
  });
292
293
  }
293
294
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_TreeDiffViewer.TreeDiffViewer, {
294
295
  oldValue: previousValue,
295
296
  newValue: currentValue
296
297
  });
297
- }, [leftEvent, rightEvent, diffMode]);
298
+ }, [previousValue, currentValue, diffMode]);
298
299
 
299
300
  // Footer navigation handlers
300
301
  const handleFooterPrevious = (0, _react.useCallback)(() => {