@buoy-gg/images 7.0.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 (195) hide show
  1. package/LICENSE +58 -0
  2. package/README.md +58 -0
  3. package/lib/commonjs/capture/actions.js +227 -0
  4. package/lib/commonjs/capture/actions.js.map +1 -0
  5. package/lib/commonjs/capture/install.js +41 -0
  6. package/lib/commonjs/capture/install.js.map +1 -0
  7. package/lib/commonjs/capture/installExpoImage.js +99 -0
  8. package/lib/commonjs/capture/installExpoImage.js.map +1 -0
  9. package/lib/commonjs/capture/installRnImage.js +128 -0
  10. package/lib/commonjs/capture/installRnImage.js.map +1 -0
  11. package/lib/commonjs/capture/overrides.js +286 -0
  12. package/lib/commonjs/capture/overrides.js.map +1 -0
  13. package/lib/commonjs/capture/savings.js +193 -0
  14. package/lib/commonjs/capture/savings.js.map +1 -0
  15. package/lib/commonjs/capture/shared.js +367 -0
  16. package/lib/commonjs/capture/shared.js.map +1 -0
  17. package/lib/commonjs/components/ImageDetail.js +444 -0
  18. package/lib/commonjs/components/ImageDetail.js.map +1 -0
  19. package/lib/commonjs/components/ImageDetailFooter.js +232 -0
  20. package/lib/commonjs/components/ImageDetailFooter.js.map +1 -0
  21. package/lib/commonjs/components/ImageRow.js +164 -0
  22. package/lib/commonjs/components/ImageRow.js.map +1 -0
  23. package/lib/commonjs/components/ImagesHeader.js +63 -0
  24. package/lib/commonjs/components/ImagesHeader.js.map +1 -0
  25. package/lib/commonjs/components/ImagesIcon.js +59 -0
  26. package/lib/commonjs/components/ImagesIcon.js.map +1 -0
  27. package/lib/commonjs/components/ImagesListFooter.js +194 -0
  28. package/lib/commonjs/components/ImagesListFooter.js.map +1 -0
  29. package/lib/commonjs/components/ImagesModal.js +173 -0
  30. package/lib/commonjs/components/ImagesModal.js.map +1 -0
  31. package/lib/commonjs/components/ImagesRoot.js +24 -0
  32. package/lib/commonjs/components/ImagesRoot.js.map +1 -0
  33. package/lib/commonjs/components/InsightsBar.js +59 -0
  34. package/lib/commonjs/components/InsightsBar.js.map +1 -0
  35. package/lib/commonjs/export.js +81 -0
  36. package/lib/commonjs/export.js.map +1 -0
  37. package/lib/commonjs/format.js +117 -0
  38. package/lib/commonjs/format.js.map +1 -0
  39. package/lib/commonjs/hooks/useImageRecords.js +20 -0
  40. package/lib/commonjs/hooks/useImageRecords.js.map +1 -0
  41. package/lib/commonjs/index.js +236 -0
  42. package/lib/commonjs/index.js.map +1 -0
  43. package/lib/commonjs/package.json +1 -0
  44. package/lib/commonjs/preset.js +45 -0
  45. package/lib/commonjs/preset.js.map +1 -0
  46. package/lib/commonjs/register.js +19 -0
  47. package/lib/commonjs/register.js.map +1 -0
  48. package/lib/commonjs/state.js +54 -0
  49. package/lib/commonjs/state.js.map +1 -0
  50. package/lib/commonjs/store/imageEventStore.js +160 -0
  51. package/lib/commonjs/store/imageEventStore.js.map +1 -0
  52. package/lib/commonjs/store/insights.js +76 -0
  53. package/lib/commonjs/store/insights.js.map +1 -0
  54. package/lib/commonjs/sync/imagesSyncAdapter.js +315 -0
  55. package/lib/commonjs/sync/imagesSyncAdapter.js.map +1 -0
  56. package/lib/commonjs/types.js +2 -0
  57. package/lib/commonjs/types.js.map +1 -0
  58. package/lib/module/capture/actions.js +214 -0
  59. package/lib/module/capture/actions.js.map +1 -0
  60. package/lib/module/capture/install.js +36 -0
  61. package/lib/module/capture/install.js.map +1 -0
  62. package/lib/module/capture/installExpoImage.js +94 -0
  63. package/lib/module/capture/installExpoImage.js.map +1 -0
  64. package/lib/module/capture/installRnImage.js +122 -0
  65. package/lib/module/capture/installRnImage.js.map +1 -0
  66. package/lib/module/capture/overrides.js +269 -0
  67. package/lib/module/capture/overrides.js.map +1 -0
  68. package/lib/module/capture/savings.js +189 -0
  69. package/lib/module/capture/savings.js.map +1 -0
  70. package/lib/module/capture/shared.js +359 -0
  71. package/lib/module/capture/shared.js.map +1 -0
  72. package/lib/module/components/ImageDetail.js +440 -0
  73. package/lib/module/components/ImageDetail.js.map +1 -0
  74. package/lib/module/components/ImageDetailFooter.js +228 -0
  75. package/lib/module/components/ImageDetailFooter.js.map +1 -0
  76. package/lib/module/components/ImageRow.js +160 -0
  77. package/lib/module/components/ImageRow.js.map +1 -0
  78. package/lib/module/components/ImagesHeader.js +59 -0
  79. package/lib/module/components/ImagesHeader.js.map +1 -0
  80. package/lib/module/components/ImagesIcon.js +55 -0
  81. package/lib/module/components/ImagesIcon.js.map +1 -0
  82. package/lib/module/components/ImagesListFooter.js +190 -0
  83. package/lib/module/components/ImagesListFooter.js.map +1 -0
  84. package/lib/module/components/ImagesModal.js +169 -0
  85. package/lib/module/components/ImagesModal.js.map +1 -0
  86. package/lib/module/components/ImagesRoot.js +20 -0
  87. package/lib/module/components/ImagesRoot.js.map +1 -0
  88. package/lib/module/components/InsightsBar.js +55 -0
  89. package/lib/module/components/InsightsBar.js.map +1 -0
  90. package/lib/module/export.js +76 -0
  91. package/lib/module/export.js.map +1 -0
  92. package/lib/module/format.js +108 -0
  93. package/lib/module/format.js.map +1 -0
  94. package/lib/module/hooks/useImageRecords.js +16 -0
  95. package/lib/module/hooks/useImageRecords.js.map +1 -0
  96. package/lib/module/index.js +73 -0
  97. package/lib/module/index.js.map +1 -0
  98. package/lib/module/preset.js +40 -0
  99. package/lib/module/preset.js.map +1 -0
  100. package/lib/module/register.js +19 -0
  101. package/lib/module/register.js.map +1 -0
  102. package/lib/module/state.js +50 -0
  103. package/lib/module/state.js.map +1 -0
  104. package/lib/module/store/imageEventStore.js +146 -0
  105. package/lib/module/store/imageEventStore.js.map +1 -0
  106. package/lib/module/store/insights.js +71 -0
  107. package/lib/module/store/insights.js.map +1 -0
  108. package/lib/module/sync/imagesSyncAdapter.js +311 -0
  109. package/lib/module/sync/imagesSyncAdapter.js.map +1 -0
  110. package/lib/module/types.js +2 -0
  111. package/lib/module/types.js.map +1 -0
  112. package/lib/typescript/capture/actions.d.ts +49 -0
  113. package/lib/typescript/capture/actions.d.ts.map +1 -0
  114. package/lib/typescript/capture/install.d.ts +15 -0
  115. package/lib/typescript/capture/install.d.ts.map +1 -0
  116. package/lib/typescript/capture/installExpoImage.d.ts +23 -0
  117. package/lib/typescript/capture/installExpoImage.d.ts.map +1 -0
  118. package/lib/typescript/capture/installRnImage.d.ts +24 -0
  119. package/lib/typescript/capture/installRnImage.d.ts.map +1 -0
  120. package/lib/typescript/capture/overrides.d.ts +83 -0
  121. package/lib/typescript/capture/overrides.d.ts.map +1 -0
  122. package/lib/typescript/capture/savings.d.ts +37 -0
  123. package/lib/typescript/capture/savings.d.ts.map +1 -0
  124. package/lib/typescript/capture/shared.d.ts +61 -0
  125. package/lib/typescript/capture/shared.d.ts.map +1 -0
  126. package/lib/typescript/components/ImageDetail.d.ts +13 -0
  127. package/lib/typescript/components/ImageDetail.d.ts.map +1 -0
  128. package/lib/typescript/components/ImageDetailFooter.d.ts +19 -0
  129. package/lib/typescript/components/ImageDetailFooter.d.ts.map +1 -0
  130. package/lib/typescript/components/ImageRow.d.ts +15 -0
  131. package/lib/typescript/components/ImageRow.d.ts.map +1 -0
  132. package/lib/typescript/components/ImagesHeader.d.ts +25 -0
  133. package/lib/typescript/components/ImagesHeader.d.ts.map +1 -0
  134. package/lib/typescript/components/ImagesIcon.d.ts +13 -0
  135. package/lib/typescript/components/ImagesIcon.d.ts.map +1 -0
  136. package/lib/typescript/components/ImagesListFooter.d.ts +8 -0
  137. package/lib/typescript/components/ImagesListFooter.d.ts.map +1 -0
  138. package/lib/typescript/components/ImagesModal.d.ts +17 -0
  139. package/lib/typescript/components/ImagesModal.d.ts.map +1 -0
  140. package/lib/typescript/components/ImagesRoot.d.ts +11 -0
  141. package/lib/typescript/components/ImagesRoot.d.ts.map +1 -0
  142. package/lib/typescript/components/InsightsBar.d.ts +13 -0
  143. package/lib/typescript/components/InsightsBar.d.ts.map +1 -0
  144. package/lib/typescript/export.d.ts +12 -0
  145. package/lib/typescript/export.d.ts.map +1 -0
  146. package/lib/typescript/format.d.ts +26 -0
  147. package/lib/typescript/format.d.ts.map +1 -0
  148. package/lib/typescript/hooks/useImageRecords.d.ts +11 -0
  149. package/lib/typescript/hooks/useImageRecords.d.ts.map +1 -0
  150. package/lib/typescript/index.d.ts +33 -0
  151. package/lib/typescript/index.d.ts.map +1 -0
  152. package/lib/typescript/preset.d.ts +38 -0
  153. package/lib/typescript/preset.d.ts.map +1 -0
  154. package/lib/typescript/register.d.ts +15 -0
  155. package/lib/typescript/register.d.ts.map +1 -0
  156. package/lib/typescript/state.d.ts +54 -0
  157. package/lib/typescript/state.d.ts.map +1 -0
  158. package/lib/typescript/store/imageEventStore.d.ts +34 -0
  159. package/lib/typescript/store/imageEventStore.d.ts.map +1 -0
  160. package/lib/typescript/store/insights.d.ts +11 -0
  161. package/lib/typescript/store/insights.d.ts.map +1 -0
  162. package/lib/typescript/sync/imagesSyncAdapter.d.ts +205 -0
  163. package/lib/typescript/sync/imagesSyncAdapter.d.ts.map +1 -0
  164. package/lib/typescript/types.d.ts +121 -0
  165. package/lib/typescript/types.d.ts.map +1 -0
  166. package/package.json +67 -0
  167. package/register.d.ts +1 -0
  168. package/register.js +15 -0
  169. package/src/capture/actions.ts +206 -0
  170. package/src/capture/install.ts +36 -0
  171. package/src/capture/installExpoImage.ts +115 -0
  172. package/src/capture/installRnImage.tsx +133 -0
  173. package/src/capture/overrides.ts +282 -0
  174. package/src/capture/savings.ts +193 -0
  175. package/src/capture/shared.ts +387 -0
  176. package/src/components/ImageDetail.tsx +456 -0
  177. package/src/components/ImageDetailFooter.tsx +270 -0
  178. package/src/components/ImageRow.tsx +148 -0
  179. package/src/components/ImagesHeader.tsx +77 -0
  180. package/src/components/ImagesIcon.tsx +66 -0
  181. package/src/components/ImagesListFooter.tsx +193 -0
  182. package/src/components/ImagesModal.tsx +195 -0
  183. package/src/components/ImagesRoot.tsx +18 -0
  184. package/src/components/InsightsBar.tsx +51 -0
  185. package/src/export.ts +100 -0
  186. package/src/format.ts +85 -0
  187. package/src/hooks/useImageRecords.ts +24 -0
  188. package/src/index.tsx +108 -0
  189. package/src/preset.tsx +35 -0
  190. package/src/register.ts +17 -0
  191. package/src/state.ts +88 -0
  192. package/src/store/imageEventStore.ts +162 -0
  193. package/src/store/insights.ts +66 -0
  194. package/src/sync/imagesSyncAdapter.ts +261 -0
  195. package/src/types.ts +111 -0
@@ -0,0 +1,456 @@
1
+ /**
2
+ * ImageDetail — full record view: preview, source, load timeline, dimensions
3
+ * with the oversize verdict + estimated decoded/wasted memory, cache verdict,
4
+ * and error details (including HTTP status/headers when the platform provides
5
+ * them). Cache actions live here too (expo-image global clears).
6
+ */
7
+
8
+ import { useState, type ReactNode } from "react";
9
+ import {
10
+ Image,
11
+ PixelRatio,
12
+ ScrollView,
13
+ StyleSheet,
14
+ Text,
15
+ TextInput,
16
+ TouchableOpacity,
17
+ View,
18
+ } from "react-native";
19
+ import { macOSColors } from "@buoy-gg/shared-ui";
20
+ import { clearOverride, setOverride } from "../capture/overrides";
21
+ import { proveSavings } from "../capture/savings";
22
+ import type { SavingsResult } from "../capture/savings";
23
+ import {
24
+ estDecodedBytes,
25
+ estWastedBytes,
26
+ neededPixels,
27
+ } from "../store/imageEventStore";
28
+ import { cacheBadge, formatBytes, formatMs, sizeVerdict } from "../format";
29
+ import type { ImageRecord } from "../types";
30
+
31
+ interface ImageDetailProps {
32
+ record: ImageRecord;
33
+ }
34
+
35
+ function Section({ title, children }: { title: string; children: ReactNode }) {
36
+ return (
37
+ <View style={styles.section}>
38
+ <Text style={styles.sectionTitle}>{title}</Text>
39
+ <View style={styles.sectionBody}>{children}</View>
40
+ </View>
41
+ );
42
+ }
43
+
44
+ function Row({ label, value, color }: { label: string; value: string; color?: string }) {
45
+ return (
46
+ <View style={styles.kvRow}>
47
+ <Text style={styles.kvLabel}>{label}</Text>
48
+ <Text style={[styles.kvValue, color ? { color } : null]} selectable>
49
+ {value}
50
+ </Text>
51
+ </View>
52
+ );
53
+ }
54
+
55
+ /** expo-image global cache clears (no-ops if expo-image is absent). */
56
+ function clearExpoCache(kind: "memory" | "disk"): void {
57
+ try {
58
+ const expo = require("expo-image");
59
+ if (kind === "memory") void expo.Image.clearMemoryCache();
60
+ else void expo.Image.clearDiskCache();
61
+ } catch {
62
+ // expo-image not installed
63
+ }
64
+ }
65
+
66
+ /** Free-text source swap: test another URL/format/CDN param on the live view. */
67
+ function SwapUrlSection({ record }: { record: ImageRecord }) {
68
+ const [url, setUrl] = useState(record.uri);
69
+ const trimmed = url.trim();
70
+ const canApply = record.mounted && trimmed.length > 0 && trimmed !== record.uri;
71
+
72
+ return (
73
+ <Section title="SWAP SOURCE URL">
74
+ <TextInput
75
+ style={styles.urlInput}
76
+ value={url}
77
+ onChangeText={setUrl}
78
+ placeholder="https://…"
79
+ placeholderTextColor={macOSColors.text.muted}
80
+ autoCapitalize="none"
81
+ autoCorrect={false}
82
+ multiline
83
+ />
84
+ <View style={styles.actionsRow}>
85
+ <TouchableOpacity
86
+ style={[styles.actionButton, !canApply && styles.actionDisabled]}
87
+ disabled={!canApply}
88
+ onPress={() =>
89
+ setOverride(record.id, {
90
+ source: { type: "url", uri: trimmed },
91
+ label: "URL swapped",
92
+ })
93
+ }
94
+ >
95
+ <Text style={styles.actionText}>Apply</Text>
96
+ </TouchableOpacity>
97
+ <TouchableOpacity
98
+ style={styles.actionButton}
99
+ onPress={() => setUrl(record.uri)}
100
+ >
101
+ <Text style={styles.actionText}>Reset field</Text>
102
+ </TouchableOpacity>
103
+ </View>
104
+ </Section>
105
+ );
106
+ }
107
+
108
+ /**
109
+ * Real, on-device savings proof: re-encode at displayed size as WebP via
110
+ * expo-image-manipulator, report actual bytes, offer an in-place preview.
111
+ */
112
+ function SavingsSection({ record }: { record: ImageRecord }) {
113
+ const [busy, setBusy] = useState(false);
114
+ const [result, setResult] = useState<SavingsResult | null>(null);
115
+
116
+ const run = () => {
117
+ setBusy(true);
118
+ proveSavings(record)
119
+ .then(setResult)
120
+ .catch((error) => setResult({ ok: false, message: String(error?.message ?? error) }))
121
+ .finally(() => setBusy(false));
122
+ };
123
+
124
+ return (
125
+ <Section title="PROVE SAVINGS (re-encode on device)">
126
+ {result == null ? (
127
+ <View style={styles.actionsRow}>
128
+ <TouchableOpacity style={styles.actionButton} onPress={run} disabled={busy}>
129
+ <Text style={styles.actionText}>
130
+ {busy ? "Re-encoding…" : "Re-encode at displayed size (WebP)"}
131
+ </Text>
132
+ </TouchableOpacity>
133
+ </View>
134
+ ) : (
135
+ <>
136
+ <Row
137
+ label="Result"
138
+ value={result.message || (result.ok ? "done" : "failed")}
139
+ color={result.ok ? macOSColors.semantic.success : macOSColors.semantic.error}
140
+ />
141
+ {result.originalBytes != null ? (
142
+ <Row
143
+ label="Original"
144
+ value={`${formatBytes(result.originalBytes)} (${result.originalBytesSource})`}
145
+ />
146
+ ) : null}
147
+ {result.optimizedBytes != null ? (
148
+ <Row
149
+ label="Optimized"
150
+ value={`${formatBytes(result.optimizedBytes)} — ${result.optimizedDims?.width}×${result.optimizedDims?.height} WebP`}
151
+ color={macOSColors.semantic.success}
152
+ />
153
+ ) : null}
154
+ {result.savedBytes != null ? (
155
+ <Row
156
+ label="Saved"
157
+ value={`${formatBytes(result.savedBytes)} (${result.savedPct}%)`}
158
+ color={macOSColors.semantic.success}
159
+ />
160
+ ) : null}
161
+ <View style={styles.actionsRow}>
162
+ {result.ok && result.optimizedUri && record.mounted ? (
163
+ <TouchableOpacity
164
+ style={styles.actionButton}
165
+ onPress={() =>
166
+ setOverride(record.id, {
167
+ source: { type: "url", uri: result.optimizedUri! },
168
+ label: "Optimized preview",
169
+ })
170
+ }
171
+ >
172
+ <Text style={styles.actionText}>Preview in place</Text>
173
+ </TouchableOpacity>
174
+ ) : null}
175
+ <TouchableOpacity style={styles.actionButton} onPress={run} disabled={busy}>
176
+ <Text style={styles.actionText}>{busy ? "Re-encoding…" : "Re-run"}</Text>
177
+ </TouchableOpacity>
178
+ </View>
179
+ </>
180
+ )}
181
+ </Section>
182
+ );
183
+ }
184
+
185
+ export function ImageDetail({ record }: ImageDetailProps) {
186
+ const verdict = sizeVerdict(record);
187
+ const cache = cacheBadge(record);
188
+ const needed = neededPixels(record);
189
+ const decoded = estDecodedBytes(record);
190
+ const wasted = estWastedBytes(record);
191
+
192
+ return (
193
+ <ScrollView style={styles.container} contentContainerStyle={styles.content}>
194
+ {record.overrideLabel ? (
195
+ <View style={styles.overrideBanner}>
196
+ <Text style={styles.overrideText}>
197
+ Simulation active: {record.overrideLabel} — stats below describe the
198
+ override, not the original source.
199
+ </Text>
200
+ <TouchableOpacity onPress={() => clearOverride(record.id)}>
201
+ <Text style={styles.overrideRestore}>RESTORE</Text>
202
+ </TouchableOpacity>
203
+ </View>
204
+ ) : null}
205
+
206
+ {record.uri && record.sourceKind !== "data" ? (
207
+ <View style={styles.previewBox}>
208
+ <Image
209
+ testID="buoy-internal-images-preview"
210
+ source={{ uri: record.uri }}
211
+ style={styles.preview}
212
+ resizeMode="contain"
213
+ />
214
+ </View>
215
+ ) : null}
216
+
217
+ <Section title="SOURCE">
218
+ <Row label="URI" value={record.uri || "(none)"} />
219
+ <Row label="Kind" value={record.sourceKind} />
220
+ <Row label="Library" value={record.lib === "expo" ? "expo-image" : "RN <Image>"} />
221
+ {record.hasAltText === false ? (
222
+ <Row
223
+ label="Alt text"
224
+ value="missing — add alt/accessibilityLabel"
225
+ color={macOSColors.semantic.warning}
226
+ />
227
+ ) : null}
228
+ {record.mediaType != null ? <Row label="Media type" value={String(record.mediaType)} /> : null}
229
+ {record.isAnimated != null ? (
230
+ <Row label="Animated" value={record.isAnimated ? "yes" : "no"} />
231
+ ) : null}
232
+ </Section>
233
+
234
+ <Section title="LOAD">
235
+ <Row
236
+ label="Status"
237
+ value={record.status}
238
+ color={
239
+ record.status === "error"
240
+ ? macOSColors.semantic.error
241
+ : record.status === "loaded"
242
+ ? macOSColors.semantic.success
243
+ : macOSColors.semantic.warning
244
+ }
245
+ />
246
+ <Row label="Duration" value={formatMs(record.durationMs)} />
247
+ {record.displayedAt && record.loadedAt ? (
248
+ <Row
249
+ label="Decode → display"
250
+ value={formatMs(Math.max(0, record.displayedAt - record.loadedAt))}
251
+ />
252
+ ) : null}
253
+ <Row label="Cache" value={`${cache.label}`} color={cache.color} />
254
+ {record.cacheVerdictSource ? (
255
+ <Row
256
+ label="Cache source"
257
+ value={
258
+ record.cacheVerdictSource === "event"
259
+ ? "reported by expo-image onLoad"
260
+ : "Image.queryCache after load"
261
+ }
262
+ />
263
+ ) : null}
264
+ {record.progressSeen ? (
265
+ <Row
266
+ label="Downloaded"
267
+ value={
268
+ record.bytesTotal
269
+ ? `${formatBytes(record.bytesLoaded ?? 0)} / ${formatBytes(record.bytesTotal)}`
270
+ : "network transfer observed"
271
+ }
272
+ />
273
+ ) : null}
274
+ <Row label="Load cycles" value={String(record.loadCount)} />
275
+ <Row label="Still mounted" value={record.mounted ? "yes" : "no"} />
276
+ </Section>
277
+
278
+ <Section title="DIMENSIONS">
279
+ {record.intrinsic ? (
280
+ <Row
281
+ label="Decoded"
282
+ value={`${record.intrinsic.width}×${record.intrinsic.height} px`}
283
+ />
284
+ ) : (
285
+ <Row label="Decoded" value="unknown" />
286
+ )}
287
+ {record.layout ? (
288
+ <Row
289
+ label="Displayed"
290
+ value={`${Math.round(record.layout.width)}×${Math.round(record.layout.height)} dp (needs ${
291
+ needed ? `${needed.width}×${needed.height}` : "?"
292
+ } px @${PixelRatio.get()}x)`}
293
+ />
294
+ ) : (
295
+ <Row label="Displayed" value="no layout yet" />
296
+ )}
297
+ {verdict.label ? <Row label="Verdict" value={verdict.label} color={verdict.color} /> : null}
298
+ {decoded > 0 ? <Row label="Est. decoded memory" value={formatBytes(decoded)} /> : null}
299
+ {wasted > 0 ? (
300
+ <Row
301
+ label="Est. wasted"
302
+ value={`${formatBytes(wasted)} — resize the source to ${
303
+ needed ? `${needed.width}×${needed.height}` : "the displayed size"
304
+ }`}
305
+ color={macOSColors.semantic.warning}
306
+ />
307
+ ) : null}
308
+ {(record.layoutShifts ?? 0) > 0 ? (
309
+ <Row
310
+ label="Layout shifts"
311
+ value={`${record.layoutShifts} — box resized after load; reserve explicit width/height`}
312
+ color={macOSColors.semantic.warning}
313
+ />
314
+ ) : null}
315
+ </Section>
316
+
317
+ {record.sourceKind === "network" || record.sourceKind === "asset" ? (
318
+ <SavingsSection key={`savings-${record.id}`} record={record} />
319
+ ) : null}
320
+
321
+ {record.status === "error" ? (
322
+ <Section title="ERROR">
323
+ <Row label="Message" value={record.error ?? "unknown"} color={macOSColors.semantic.error} />
324
+ {record.errorCode != null ? <Row label="HTTP status" value={String(record.errorCode)} /> : null}
325
+ {record.errorHeaders
326
+ ? Object.entries(record.errorHeaders).map(([k, v]) => (
327
+ <Row key={k} label={k} value={String(v)} />
328
+ ))
329
+ : null}
330
+ </Section>
331
+ ) : null}
332
+
333
+ {record.sourceKind === "network" && record.mounted ? (
334
+ <SwapUrlSection key={record.id} record={record} />
335
+ ) : null}
336
+
337
+ {record.lib === "expo" ? (
338
+ <Section title="CACHE ACTIONS (expo-image, global)">
339
+ <View style={styles.actionsRow}>
340
+ <TouchableOpacity style={styles.actionButton} onPress={() => clearExpoCache("memory")}>
341
+ <Text style={styles.actionText}>Clear memory cache</Text>
342
+ </TouchableOpacity>
343
+ <TouchableOpacity style={styles.actionButton} onPress={() => clearExpoCache("disk")}>
344
+ <Text style={styles.actionText}>Clear disk cache</Text>
345
+ </TouchableOpacity>
346
+ </View>
347
+ </Section>
348
+ ) : null}
349
+ </ScrollView>
350
+ );
351
+ }
352
+
353
+ const styles = StyleSheet.create({
354
+ container: {
355
+ flex: 1,
356
+ },
357
+ content: {
358
+ padding: 12,
359
+ gap: 12,
360
+ },
361
+ previewBox: {
362
+ height: 140,
363
+ borderRadius: 8,
364
+ backgroundColor: macOSColors.background.input,
365
+ overflow: "hidden",
366
+ },
367
+ preview: {
368
+ width: "100%",
369
+ height: "100%",
370
+ },
371
+ section: {
372
+ gap: 6,
373
+ },
374
+ sectionTitle: {
375
+ color: macOSColors.text.muted,
376
+ fontSize: 10,
377
+ fontWeight: "700",
378
+ letterSpacing: 0.8,
379
+ },
380
+ sectionBody: {
381
+ backgroundColor: macOSColors.background.card,
382
+ borderRadius: 8,
383
+ borderWidth: StyleSheet.hairlineWidth,
384
+ borderColor: macOSColors.border.default,
385
+ paddingHorizontal: 10,
386
+ paddingVertical: 4,
387
+ },
388
+ kvRow: {
389
+ flexDirection: "row",
390
+ paddingVertical: 5,
391
+ gap: 12,
392
+ },
393
+ kvLabel: {
394
+ color: macOSColors.text.secondary,
395
+ fontSize: 11,
396
+ width: 110,
397
+ },
398
+ kvValue: {
399
+ color: macOSColors.text.primary,
400
+ fontSize: 11,
401
+ flex: 1,
402
+ },
403
+ actionsRow: {
404
+ flexDirection: "row",
405
+ gap: 8,
406
+ paddingVertical: 8,
407
+ },
408
+ actionButton: {
409
+ backgroundColor: macOSColors.background.hover,
410
+ borderRadius: 6,
411
+ paddingHorizontal: 10,
412
+ paddingVertical: 6,
413
+ borderWidth: StyleSheet.hairlineWidth,
414
+ borderColor: macOSColors.border.default,
415
+ },
416
+ actionText: {
417
+ color: macOSColors.text.primary,
418
+ fontSize: 11,
419
+ fontWeight: "600",
420
+ },
421
+ actionDisabled: {
422
+ opacity: 0.4,
423
+ },
424
+ urlInput: {
425
+ color: macOSColors.text.primary,
426
+ fontSize: 11,
427
+ fontFamily: "monospace",
428
+ backgroundColor: macOSColors.background.input,
429
+ borderRadius: 6,
430
+ borderWidth: StyleSheet.hairlineWidth,
431
+ borderColor: macOSColors.border.input,
432
+ paddingHorizontal: 8,
433
+ paddingVertical: 6,
434
+ marginTop: 8,
435
+ },
436
+ overrideBanner: {
437
+ flexDirection: "row",
438
+ alignItems: "center",
439
+ gap: 10,
440
+ padding: 10,
441
+ borderRadius: 8,
442
+ backgroundColor: macOSColors.semantic.warningBackground,
443
+ },
444
+ overrideText: {
445
+ flex: 1,
446
+ color: macOSColors.semantic.warning,
447
+ fontSize: 11,
448
+ lineHeight: 15,
449
+ },
450
+ overrideRestore: {
451
+ color: macOSColors.semantic.warning,
452
+ fontSize: 11,
453
+ fontWeight: "700",
454
+ letterSpacing: 0.5,
455
+ },
456
+ });