@buoy-gg/images 7.0.28 → 7.0.30

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.
@@ -1,271 +0,0 @@
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
- night,
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 ? night.color.accent : night.color.textTertiary}
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 ? night.color.textTertiary : night.color.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 ? night.color.accent : night.color.textTertiary}
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
- // Dark glass over the star field — matches EventStepperFooter's night bar.
200
- backgroundColor: "rgba(5, 5, 5, 0.92)",
201
- borderTopWidth: 1,
202
- borderTopColor: night.color.border,
203
- gap: 8,
204
- },
205
- row: {
206
- flexDirection: "row",
207
- alignItems: "center",
208
- gap: 8,
209
- },
210
- navButton: {
211
- paddingHorizontal: 10,
212
- paddingVertical: 8,
213
- borderRadius: 6,
214
- backgroundColor: night.color.button,
215
- alignItems: "center",
216
- justifyContent: "center",
217
- },
218
- actionButton: {
219
- flex: 1,
220
- flexDirection: "row",
221
- alignItems: "center",
222
- justifyContent: "center",
223
- gap: 6,
224
- paddingVertical: 8,
225
- borderRadius: 6,
226
- backgroundColor: night.color.button,
227
- borderWidth: StyleSheet.hairlineWidth,
228
- borderColor: night.color.border,
229
- },
230
- actionText: {
231
- fontSize: 11,
232
- fontWeight: "600",
233
- color: night.color.text,
234
- fontFamily: "monospace",
235
- textTransform: "uppercase",
236
- },
237
- actionTextDisabled: {
238
- color: night.color.textTertiary,
239
- },
240
- disabled: {
241
- opacity: 0.4,
242
- },
243
- simButton: {
244
- flex: 1,
245
- alignItems: "center",
246
- paddingVertical: 7,
247
- borderRadius: 6,
248
- backgroundColor: night.color.button,
249
- borderWidth: StyleSheet.hairlineWidth,
250
- borderColor: night.color.border,
251
- },
252
- simText: {
253
- fontSize: 10,
254
- fontWeight: "600",
255
- color: night.color.textSecondary,
256
- fontFamily: "monospace",
257
- textTransform: "uppercase",
258
- },
259
- simTextAccent: {
260
- color: night.color.warning,
261
- },
262
- message: {
263
- color: night.color.info,
264
- fontSize: 10,
265
- fontFamily: "monospace",
266
- },
267
- hint: {
268
- color: night.color.textTertiary,
269
- fontSize: 10,
270
- },
271
- });
@@ -1,148 +0,0 @@
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, night } 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: night.color.textTertiary }]}>
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: night.color.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: night.color.border,
87
- gap: 10,
88
- },
89
- thumbBox: {
90
- width: 40,
91
- height: 40,
92
- borderRadius: 6,
93
- backgroundColor: night.color.bg,
94
- alignItems: "center",
95
- justifyContent: "center",
96
- overflow: "hidden",
97
- },
98
- thumb: {
99
- width: 40,
100
- height: 40,
101
- },
102
- thumbFallback: {
103
- color: night.color.textTertiary,
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: night.color.bg,
116
- },
117
- main: {
118
- flex: 1,
119
- gap: 2,
120
- },
121
- name: {
122
- color: night.color.text,
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: night.color.textSecondary,
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
- });
@@ -1,77 +0,0 @@
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
- }
@@ -1,9 +0,0 @@
1
- /**
2
- * ImagesIcon — re-exported from the Buoy Icon Format.
3
- *
4
- * The artwork lives in shared/icons/images.json and is generated into
5
- * @buoy-gg/floating-tools-core, so React Native, the desktop dashboard and
6
- * Flutter all render the same drawing. Edit the JSON, then `pnpm icons`.
7
- */
8
-
9
- export { ImagesIcon, IMAGES_ICON_COLOR } from "@buoy-gg/shared-ui";
@@ -1,194 +0,0 @@
1
- /**
2
- * ImagesListFooter — the list screen's action bar: mass simulations applied
3
- * to EVERY mounted image at once (error / loading / blank), mass hard-reload,
4
- * flash everything, restore all — plus the global network simulation modes
5
- * (Normal / Offline / Cold). Styled to match the detail footer.
6
- */
7
-
8
- import { useState, useSyncExternalStore } from "react";
9
- import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
10
- import { night, useSafeAreaInsets } from "@buoy-gg/shared-ui";
11
- import { hardReloadAllMounted, subscribeActions } from "../capture/actions";
12
- import {
13
- clearAllOverrides,
14
- flashAllMounted,
15
- getGlobalModes,
16
- setNetworkMode,
17
- setOverrideForAllMounted,
18
- } from "../capture/overrides";
19
- import type { NetworkMode, OverrideSource } from "../capture/overrides";
20
-
21
- const NETWORK_MODES: { key: NetworkMode; label: string }[] = [
22
- { key: "normal", label: "Normal" },
23
- { key: "offline", label: "Offline" },
24
- { key: "cold", label: "Cold" },
25
- ];
26
-
27
- export function ImagesListFooter() {
28
- const insets = useSafeAreaInsets();
29
- const [message, setMessage] = useState<string | null>(null);
30
- const network = useSyncExternalStore(
31
- subscribeActions,
32
- () => getGlobalModes().network
33
- );
34
-
35
- const massOverride = (source: OverrideSource, label: string) => {
36
- const count = setOverrideForAllMounted(source, label);
37
- setMessage(count > 0 ? `${label} on ${count} image${count === 1 ? "" : "s"} — Restore all to undo` : "No mounted images");
38
- };
39
-
40
- return (
41
- <View style={[styles.footer, { paddingBottom: 10 + insets.bottom }]}>
42
- {message ? (
43
- <Text style={styles.message} numberOfLines={1}>
44
- {message}
45
- </Text>
46
- ) : (
47
- <Text style={styles.caption}>MASS ACTIONS — every image on screen</Text>
48
- )}
49
- <View style={styles.row}>
50
- <MassButton label="Error" onPress={() => massOverride({ type: "error" }, "Forced error")} />
51
- <MassButton label="Loading" onPress={() => massOverride({ type: "hang" }, "Forced loading")} />
52
- <MassButton label="Blank" onPress={() => massOverride({ type: "blank" }, "Blanked")} />
53
- <MassButton
54
- label="Reload"
55
- onPress={() => {
56
- setMessage("…");
57
- hardReloadAllMounted().then((r) => setMessage(r.message));
58
- }}
59
- />
60
- <MassButton
61
- label="Find"
62
- onPress={() => {
63
- const count = flashAllMounted();
64
- setMessage(`Flashing ${count} image${count === 1 ? "" : "s"} (2.5s)`);
65
- }}
66
- />
67
- <MassButton
68
- label="Restore"
69
- accent
70
- onPress={() => {
71
- const count = clearAllOverrides();
72
- setMessage(count > 0 ? `Restored ${count} image${count === 1 ? "" : "s"}` : "No overrides active");
73
- }}
74
- />
75
- </View>
76
-
77
- <View style={styles.row}>
78
- <Text style={styles.netLabel}>NET</Text>
79
- {NETWORK_MODES.map((mode) => {
80
- const active = network === mode.key;
81
- return (
82
- <TouchableOpacity
83
- key={mode.key}
84
- onPress={() => {
85
- setNetworkMode(mode.key);
86
- // Modes only affect loads that happen AFTER they're set — an
87
- // on-screen image keeps its bitmap until something reloads it.
88
- setMessage(
89
- mode.key === "normal"
90
- ? "Network simulation off — tap RELOAD to load normally again"
91
- : mode.key === "offline"
92
- ? "Offline: network images FAIL (bundled assets still load) — tap RELOAD to apply to on-screen images"
93
- : "Cold (bypass caches) — tap RELOAD: everything refetches like first launch"
94
- );
95
- }}
96
- style={[styles.netButton, active && styles.netButtonActive]}
97
- activeOpacity={0.7}
98
- >
99
- <Text style={[styles.buttonText, active && styles.netTextActive]}>
100
- {mode.label}
101
- </Text>
102
- </TouchableOpacity>
103
- );
104
- })}
105
- </View>
106
- </View>
107
- );
108
- }
109
-
110
- function MassButton({
111
- label,
112
- onPress,
113
- accent,
114
- }: {
115
- label: string;
116
- onPress: () => void;
117
- accent?: boolean;
118
- }) {
119
- return (
120
- <TouchableOpacity onPress={onPress} style={styles.button} activeOpacity={0.7}>
121
- <Text style={[styles.buttonText, accent && styles.buttonTextAccent]}>{label}</Text>
122
- </TouchableOpacity>
123
- );
124
- }
125
-
126
- const styles = StyleSheet.create({
127
- footer: {
128
- paddingHorizontal: 12,
129
- paddingTop: 8,
130
- // Dark glass over the star field — matches EventStepperFooter's night bar.
131
- backgroundColor: "rgba(5, 5, 5, 0.92)",
132
- borderTopWidth: 1,
133
- borderTopColor: night.color.border,
134
- gap: 8,
135
- },
136
- caption: {
137
- color: night.color.textTertiary,
138
- fontSize: 9,
139
- fontWeight: "700",
140
- letterSpacing: 0.8,
141
- },
142
- message: {
143
- color: night.color.info,
144
- fontSize: 10,
145
- fontFamily: "monospace",
146
- },
147
- row: {
148
- flexDirection: "row",
149
- gap: 6,
150
- },
151
- button: {
152
- flex: 1,
153
- alignItems: "center",
154
- paddingVertical: 8,
155
- borderRadius: 6,
156
- backgroundColor: night.color.button,
157
- borderWidth: StyleSheet.hairlineWidth,
158
- borderColor: night.color.border,
159
- },
160
- buttonText: {
161
- fontSize: 10,
162
- fontWeight: "600",
163
- color: night.color.textSecondary,
164
- fontFamily: "monospace",
165
- textTransform: "uppercase",
166
- },
167
- buttonTextAccent: {
168
- color: night.color.warning,
169
- },
170
- netLabel: {
171
- color: night.color.textTertiary,
172
- fontSize: 9,
173
- fontWeight: "700",
174
- letterSpacing: 0.8,
175
- alignSelf: "center",
176
- marginRight: 2,
177
- },
178
- netButton: {
179
- flex: 1,
180
- alignItems: "center",
181
- paddingVertical: 6,
182
- borderRadius: 6,
183
- backgroundColor: night.color.button,
184
- borderWidth: StyleSheet.hairlineWidth,
185
- borderColor: night.color.border,
186
- },
187
- netButtonActive: {
188
- backgroundColor: night.color.accentSoft,
189
- borderColor: night.color.accentBorderStrong,
190
- },
191
- netTextActive: {
192
- color: night.color.accent,
193
- },
194
- });