@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,270 @@
1
+ /**
2
+ * ImageDetailFooter — bottom navbar for the detail view: previous/next
3
+ * stepping through the captured list at the edges, per-image actions in the
4
+ * middle (hard reload / retry), with inline action feedback. Styling matches
5
+ * shared-ui's EventStepperFooter so it reads like the rest of Buoy.
6
+ *
7
+ * Mount with key={record.id} so action feedback resets when stepping.
8
+ */
9
+
10
+ import { useState } from "react";
11
+ import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
12
+ import {
13
+ ChevronLeft,
14
+ ChevronRight,
15
+ RefreshCw,
16
+ macOSColors,
17
+ useSafeAreaInsets,
18
+ } from "@buoy-gg/shared-ui";
19
+ import { hardReloadRecord, retryRecord } from "../capture/actions";
20
+ import type { ActionResult } from "../capture/actions";
21
+ import { clearOverride, flashRecord, setOverride } from "../capture/overrides";
22
+ import type { ImageRecord } from "../types";
23
+
24
+ interface ImageDetailFooterProps {
25
+ record: ImageRecord;
26
+ hasPrevious: boolean;
27
+ hasNext: boolean;
28
+ onPrevious: () => void;
29
+ onNext: () => void;
30
+ }
31
+
32
+ export function ImageDetailFooter({
33
+ record,
34
+ hasPrevious,
35
+ hasNext,
36
+ onPrevious,
37
+ onNext,
38
+ }: ImageDetailFooterProps) {
39
+ const insets = useSafeAreaInsets();
40
+ const [message, setMessage] = useState<string | null>(null);
41
+
42
+ const runAction = (action: () => Promise<ActionResult>) => {
43
+ setMessage("…");
44
+ action()
45
+ .then((result) => setMessage(result.message))
46
+ .catch((error) => setMessage(String(error?.message ?? error)));
47
+ };
48
+
49
+ const actionsDisabled = !record.mounted;
50
+ const overrideActive = record.overrideLabel != null;
51
+
52
+ const simulate = (
53
+ source: { type: "error" } | { type: "hang" } | { type: "blank" },
54
+ label: string
55
+ ) => {
56
+ setOverride(record.id, { source, label });
57
+ setMessage(`${label} — Restore to undo`);
58
+ };
59
+
60
+ return (
61
+ <View style={[styles.footer, { paddingBottom: 12 + insets.bottom }]}>
62
+ {message ? (
63
+ <Text style={styles.message} numberOfLines={1}>
64
+ {message}
65
+ </Text>
66
+ ) : actionsDisabled ? (
67
+ <Text style={styles.hint} numberOfLines={1}>
68
+ Instance unmounted — reload actions need the image on screen.
69
+ </Text>
70
+ ) : null}
71
+
72
+ <View style={styles.row}>
73
+ <TouchableOpacity
74
+ onPress={onPrevious}
75
+ disabled={!hasPrevious}
76
+ style={[styles.navButton, !hasPrevious && styles.disabled]}
77
+ activeOpacity={0.7}
78
+ accessibilityLabel="Previous image"
79
+ >
80
+ <ChevronLeft
81
+ size={20}
82
+ color={hasPrevious ? macOSColors.text.primary : macOSColors.text.muted}
83
+ />
84
+ </TouchableOpacity>
85
+
86
+ <TouchableOpacity
87
+ onPress={() => runAction(() => hardReloadRecord(record))}
88
+ disabled={actionsDisabled}
89
+ style={[styles.actionButton, actionsDisabled && styles.disabled]}
90
+ activeOpacity={0.7}
91
+ >
92
+ <RefreshCw
93
+ size={13}
94
+ color={actionsDisabled ? macOSColors.text.muted : macOSColors.semantic.info}
95
+ />
96
+ <Text style={[styles.actionText, actionsDisabled && styles.actionTextDisabled]}>
97
+ Hard reload
98
+ </Text>
99
+ </TouchableOpacity>
100
+
101
+ <TouchableOpacity
102
+ onPress={() => runAction(() => retryRecord(record))}
103
+ disabled={actionsDisabled}
104
+ style={[styles.actionButton, actionsDisabled && styles.disabled]}
105
+ activeOpacity={0.7}
106
+ >
107
+ <Text style={[styles.actionText, actionsDisabled && styles.actionTextDisabled]}>
108
+ {record.status === "error" ? "Retry" : "Reload"}
109
+ </Text>
110
+ </TouchableOpacity>
111
+
112
+ <TouchableOpacity
113
+ onPress={onNext}
114
+ disabled={!hasNext}
115
+ style={[styles.navButton, !hasNext && styles.disabled]}
116
+ activeOpacity={0.7}
117
+ accessibilityLabel="Next image"
118
+ >
119
+ <ChevronRight
120
+ size={20}
121
+ color={hasNext ? macOSColors.text.primary : macOSColors.text.muted}
122
+ />
123
+ </TouchableOpacity>
124
+ </View>
125
+
126
+ <View style={styles.row}>
127
+ <SimButton
128
+ label="Error"
129
+ disabled={actionsDisabled}
130
+ onPress={() => simulate({ type: "error" }, "Forced error")}
131
+ />
132
+ <SimButton
133
+ label="Loading"
134
+ disabled={actionsDisabled}
135
+ onPress={() => simulate({ type: "hang" }, "Forced loading")}
136
+ />
137
+ <SimButton
138
+ label="Blank"
139
+ disabled={actionsDisabled}
140
+ onPress={() => simulate({ type: "blank" }, "Blanked")}
141
+ />
142
+ <SimButton
143
+ label="Find"
144
+ disabled={actionsDisabled}
145
+ onPress={() => {
146
+ flashRecord(record.id);
147
+ setMessage("Flashing a red border on the image (2.5s)");
148
+ }}
149
+ />
150
+ <SimButton
151
+ label="Restore"
152
+ accent
153
+ disabled={!overrideActive}
154
+ onPress={() => {
155
+ clearOverride(record.id);
156
+ setMessage("Override removed — original source restored");
157
+ }}
158
+ />
159
+ </View>
160
+ </View>
161
+ );
162
+ }
163
+
164
+ function SimButton({
165
+ label,
166
+ onPress,
167
+ disabled,
168
+ accent,
169
+ }: {
170
+ label: string;
171
+ onPress: () => void;
172
+ disabled?: boolean;
173
+ accent?: boolean;
174
+ }) {
175
+ return (
176
+ <TouchableOpacity
177
+ onPress={onPress}
178
+ disabled={disabled}
179
+ style={[styles.simButton, disabled && styles.disabled]}
180
+ activeOpacity={0.7}
181
+ >
182
+ <Text
183
+ style={[
184
+ styles.simText,
185
+ accent && !disabled && styles.simTextAccent,
186
+ disabled && styles.actionTextDisabled,
187
+ ]}
188
+ >
189
+ {label}
190
+ </Text>
191
+ </TouchableOpacity>
192
+ );
193
+ }
194
+
195
+ const styles = StyleSheet.create({
196
+ footer: {
197
+ paddingHorizontal: 12,
198
+ paddingTop: 10,
199
+ backgroundColor: macOSColors.background.base,
200
+ borderTopWidth: 1,
201
+ borderTopColor: macOSColors.border.default,
202
+ gap: 8,
203
+ },
204
+ row: {
205
+ flexDirection: "row",
206
+ alignItems: "center",
207
+ gap: 8,
208
+ },
209
+ navButton: {
210
+ paddingHorizontal: 10,
211
+ paddingVertical: 8,
212
+ borderRadius: 6,
213
+ backgroundColor: macOSColors.background.card,
214
+ alignItems: "center",
215
+ justifyContent: "center",
216
+ },
217
+ actionButton: {
218
+ flex: 1,
219
+ flexDirection: "row",
220
+ alignItems: "center",
221
+ justifyContent: "center",
222
+ gap: 6,
223
+ paddingVertical: 8,
224
+ borderRadius: 6,
225
+ backgroundColor: macOSColors.background.card,
226
+ borderWidth: StyleSheet.hairlineWidth,
227
+ borderColor: macOSColors.border.default,
228
+ },
229
+ actionText: {
230
+ fontSize: 11,
231
+ fontWeight: "600",
232
+ color: macOSColors.text.primary,
233
+ fontFamily: "monospace",
234
+ textTransform: "uppercase",
235
+ },
236
+ actionTextDisabled: {
237
+ color: macOSColors.text.muted,
238
+ },
239
+ disabled: {
240
+ opacity: 0.4,
241
+ },
242
+ simButton: {
243
+ flex: 1,
244
+ alignItems: "center",
245
+ paddingVertical: 7,
246
+ borderRadius: 6,
247
+ backgroundColor: macOSColors.background.card,
248
+ borderWidth: StyleSheet.hairlineWidth,
249
+ borderColor: macOSColors.border.default,
250
+ },
251
+ simText: {
252
+ fontSize: 10,
253
+ fontWeight: "600",
254
+ color: macOSColors.text.secondary,
255
+ fontFamily: "monospace",
256
+ textTransform: "uppercase",
257
+ },
258
+ simTextAccent: {
259
+ color: macOSColors.semantic.warning,
260
+ },
261
+ message: {
262
+ color: macOSColors.semantic.info,
263
+ fontSize: 10,
264
+ fontFamily: "monospace",
265
+ },
266
+ hint: {
267
+ color: macOSColors.text.muted,
268
+ fontSize: 10,
269
+ },
270
+ });
@@ -0,0 +1,148 @@
1
+ /**
2
+ * ImageRow — one captured image in the list: thumbnail, filename, library +
3
+ * cache badges, load time, and the size verdict.
4
+ *
5
+ * Thumbnails carry the buoy-internal testID so the capture layer skips them —
6
+ * the tool must never appear in its own registry.
7
+ */
8
+
9
+ import { Image, StyleSheet, Text, TouchableOpacity, View } from "react-native";
10
+ import { macOSColors } from "@buoy-gg/shared-ui";
11
+ import { cacheBadge, formatMs, sizeVerdict, statusColor, uriTail } from "../format";
12
+ import type { ImageRecord } from "../types";
13
+
14
+ interface ImageRowProps {
15
+ record: ImageRecord;
16
+ onPress: (id: number) => void;
17
+ }
18
+
19
+ export function ImageRow({ record, onPress }: ImageRowProps) {
20
+ const verdict = sizeVerdict(record);
21
+ const cache = cacheBadge(record);
22
+ const showThumb =
23
+ record.sourceKind === "network" ||
24
+ record.sourceKind === "asset" ||
25
+ record.sourceKind === "file";
26
+
27
+ return (
28
+ <TouchableOpacity
29
+ style={styles.row}
30
+ testID={`buoy-image-row-${record.id}`}
31
+ onPress={() => onPress(record.id)}
32
+ >
33
+ <View style={styles.thumbBox}>
34
+ {showThumb && record.uri ? (
35
+ <Image
36
+ testID="buoy-internal-images-thumb"
37
+ source={{ uri: record.uri }}
38
+ style={styles.thumb}
39
+ resizeMode="cover"
40
+ />
41
+ ) : (
42
+ <Text style={styles.thumbFallback}>?</Text>
43
+ )}
44
+ <View style={[styles.statusDot, { backgroundColor: statusColor(record) }]} />
45
+ </View>
46
+
47
+ <View style={styles.main}>
48
+ <Text style={styles.name} numberOfLines={1}>
49
+ {uriTail(record.uri)}
50
+ </Text>
51
+ <View style={styles.metaRow}>
52
+ <Text style={[styles.badge, { color: macOSColors.text.muted }]}>
53
+ {record.lib === "expo" ? "EXPO" : "RN"}
54
+ </Text>
55
+ <Text style={[styles.badge, { color: cache.color }]}>{cache.label}</Text>
56
+ {record.overrideLabel ? (
57
+ <Text style={[styles.badge, { color: macOSColors.semantic.warning }]}>SIM</Text>
58
+ ) : null}
59
+ <Text style={styles.meta}>{formatMs(record.durationMs)}</Text>
60
+ {record.intrinsic ? (
61
+ <Text style={styles.meta}>
62
+ {record.intrinsic.width}×{record.intrinsic.height}
63
+ </Text>
64
+ ) : null}
65
+ </View>
66
+ </View>
67
+
68
+ <View style={styles.right}>
69
+ {record.status === "error" ? (
70
+ <Text style={[styles.verdict, { color: macOSColors.semantic.error }]}>FAILED</Text>
71
+ ) : verdict.label ? (
72
+ <Text style={[styles.verdict, { color: verdict.color }]}>{verdict.label}</Text>
73
+ ) : null}
74
+ </View>
75
+ </TouchableOpacity>
76
+ );
77
+ }
78
+
79
+ const styles = StyleSheet.create({
80
+ row: {
81
+ flexDirection: "row",
82
+ alignItems: "center",
83
+ paddingHorizontal: 12,
84
+ paddingVertical: 8,
85
+ borderBottomWidth: StyleSheet.hairlineWidth,
86
+ borderBottomColor: macOSColors.border.default,
87
+ gap: 10,
88
+ },
89
+ thumbBox: {
90
+ width: 40,
91
+ height: 40,
92
+ borderRadius: 6,
93
+ backgroundColor: macOSColors.background.input,
94
+ alignItems: "center",
95
+ justifyContent: "center",
96
+ overflow: "hidden",
97
+ },
98
+ thumb: {
99
+ width: 40,
100
+ height: 40,
101
+ },
102
+ thumbFallback: {
103
+ color: macOSColors.text.muted,
104
+ fontSize: 16,
105
+ fontWeight: "700",
106
+ },
107
+ statusDot: {
108
+ position: "absolute",
109
+ right: 2,
110
+ bottom: 2,
111
+ width: 8,
112
+ height: 8,
113
+ borderRadius: 4,
114
+ borderWidth: 1,
115
+ borderColor: macOSColors.background.base,
116
+ },
117
+ main: {
118
+ flex: 1,
119
+ gap: 2,
120
+ },
121
+ name: {
122
+ color: macOSColors.text.primary,
123
+ fontSize: 12,
124
+ fontWeight: "600",
125
+ },
126
+ metaRow: {
127
+ flexDirection: "row",
128
+ alignItems: "center",
129
+ gap: 8,
130
+ },
131
+ badge: {
132
+ fontSize: 9,
133
+ fontWeight: "700",
134
+ letterSpacing: 0.5,
135
+ },
136
+ meta: {
137
+ color: macOSColors.text.secondary,
138
+ fontSize: 10,
139
+ fontVariant: ["tabular-nums"],
140
+ },
141
+ right: {
142
+ alignItems: "flex-end",
143
+ },
144
+ verdict: {
145
+ fontSize: 10,
146
+ fontWeight: "700",
147
+ },
148
+ });
@@ -0,0 +1,77 @@
1
+ /**
2
+ * ImagesHeader — shared ModalHeader wiring for the Images tool, matching the
3
+ * Console/Network header pattern. List mode shows aggregate stats, the shared
4
+ * ToolbarCopyButton (full registry report) and the shared ToolbarClearButton;
5
+ * detail mode shows back navigation plus the copy button for that record's
6
+ * report. Copy behavior (Pro gating, hint banner, success/error feedback)
7
+ * comes from shared-ui — identical to every other tool.
8
+ */
9
+
10
+ import {
11
+ ModalHeader,
12
+ ToolbarClearButton,
13
+ ToolbarCopyButton,
14
+ } from "@buoy-gg/shared-ui";
15
+ import { formatBytes } from "../format";
16
+ import type { ImageStats } from "../types";
17
+
18
+ interface ImagesHeaderProps {
19
+ stats: ImageStats;
20
+ detailOpen: boolean;
21
+ /** Index of the open record in the list (-1 when unknown). */
22
+ detailIndex?: number;
23
+ detailTotal?: number;
24
+ onCloseDetail: () => void;
25
+ onClear: () => void;
26
+ /** Builds the mode-appropriate report (registry or single record). */
27
+ getReport: () => string;
28
+ /** App-stack back (from the floating menu's AppHost). */
29
+ onBack?: () => void;
30
+ }
31
+
32
+ export function ImagesHeader({
33
+ stats,
34
+ detailOpen,
35
+ detailIndex = -1,
36
+ detailTotal = 0,
37
+ onCloseDetail,
38
+ onClear,
39
+ getReport,
40
+ onBack,
41
+ }: ImagesHeaderProps) {
42
+ if (detailOpen) {
43
+ const counter =
44
+ detailIndex >= 0 && detailTotal > 0
45
+ ? `${detailIndex + 1} of ${detailTotal}`
46
+ : undefined;
47
+ return (
48
+ <ModalHeader>
49
+ <ModalHeader.Navigation onBack={onCloseDetail} />
50
+ <ModalHeader.Content title="Image detail" subtitle={counter} />
51
+ <ModalHeader.Actions>
52
+ <ToolbarCopyButton value={getReport} />
53
+ </ModalHeader.Actions>
54
+ </ModalHeader>
55
+ );
56
+ }
57
+
58
+ const subtitleParts = [
59
+ `${stats.total} images`,
60
+ `${stats.errors} failed`,
61
+ `≈${formatBytes(stats.estDecodedBytes)} decoded`,
62
+ ];
63
+ if (stats.estWastedBytes > 0) {
64
+ subtitleParts.push(`${formatBytes(stats.estWastedBytes)} wasted`);
65
+ }
66
+
67
+ return (
68
+ <ModalHeader>
69
+ {onBack ? <ModalHeader.Navigation onBack={onBack} /> : null}
70
+ <ModalHeader.Content title="Images" subtitle={subtitleParts.join(" · ")} />
71
+ <ModalHeader.Actions>
72
+ <ToolbarCopyButton value={getReport} />
73
+ <ToolbarClearButton onPress={onClear} />
74
+ </ModalHeader.Actions>
75
+ </ModalHeader>
76
+ );
77
+ }
@@ -0,0 +1,66 @@
1
+ import { View, Text, StyleSheet } from "react-native";
2
+
3
+ /** Tool accent color (photo violet). */
4
+ export const IMAGES_ICON_COLOR = "#C084FC";
5
+
6
+ interface ImagesIconProps {
7
+ size: number;
8
+ color?: string;
9
+ }
10
+
11
+ /**
12
+ * Dependency-free "picture" glyph: rounded frame with a sun dot and a
13
+ * mountain-ish baseline, rendered from plain views.
14
+ */
15
+ export function ImagesIcon({ size, color = IMAGES_ICON_COLOR }: ImagesIconProps) {
16
+ return (
17
+ <View
18
+ style={[
19
+ styles.frame,
20
+ {
21
+ width: size,
22
+ height: size,
23
+ borderRadius: size * 0.2,
24
+ borderColor: color,
25
+ },
26
+ ]}
27
+ >
28
+ <View
29
+ style={[
30
+ styles.sun,
31
+ {
32
+ width: size * 0.18,
33
+ height: size * 0.18,
34
+ borderRadius: size * 0.09,
35
+ backgroundColor: color,
36
+ top: size * 0.18,
37
+ right: size * 0.2,
38
+ },
39
+ ]}
40
+ />
41
+ <Text
42
+ style={{
43
+ color,
44
+ fontSize: size * 0.52,
45
+ fontWeight: "700",
46
+ lineHeight: size * 0.62,
47
+ marginTop: size * 0.28,
48
+ }}
49
+ >
50
+ {"▲"}
51
+ </Text>
52
+ </View>
53
+ );
54
+ }
55
+
56
+ const styles = StyleSheet.create({
57
+ frame: {
58
+ borderWidth: 1.5,
59
+ alignItems: "center",
60
+ justifyContent: "flex-end",
61
+ overflow: "hidden",
62
+ },
63
+ sun: {
64
+ position: "absolute",
65
+ },
66
+ });