@buoy-gg/images 7.0.29 → 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,266 +0,0 @@
1
- /**
2
- * ImagesModal — bottom-sheet wrapper rendered by FloatingDevTools when the
3
- * Images tool is opened. List of captured images → tap for detail. Also
4
- * surfaces the capture status so a mis-ordered install is self-diagnosing
5
- * instead of silently empty.
6
- */
7
-
8
- import { useEffect, useState } from "react";
9
- import { FlatList, StyleSheet, Text, View } from "react-native";
10
- import { JsModal, macOSColors, night } from "@buoy-gg/shared-ui";
11
- import type { ModalState } from "@buoy-gg/shared-ui";
12
- import { getCaptureStatus, installImageCapture } from "../capture/install";
13
- import { getGlobalModes } from "../capture/overrides";
14
- import { formatRecordMarkdown, formatRegistryMarkdown } from "../export";
15
- import { useImageRecords } from "../hooks/useImageRecords";
16
- import { remoteImagesDispatch } from "../remote";
17
- import { clearRecords, getRecord, updateRecord } from "../store/imageEventStore";
18
- import { ImageDetail } from "./ImageDetail";
19
- import { ImageDetailFooter } from "./ImageDetailFooter";
20
- import { ImageRow } from "./ImageRow";
21
- import { ImagesHeader } from "./ImagesHeader";
22
- import { ImagesListFooter } from "./ImagesListFooter";
23
- import { InsightsBar } from "./InsightsBar";
24
- import { computeInsights } from "../store/insights";
25
- import type { ImageRecord } from "../types";
26
-
27
- interface ImagesModalProps {
28
- visible: boolean;
29
- onClose: () => void;
30
- onBack?: () => void;
31
- onMinimize?: (modalState: ModalState) => void;
32
- persistenceKey?: string;
33
- /** Render as a plain header-bar + body stack instead of a JsModal (desktop). */
34
- inline?: boolean;
35
- }
36
-
37
- function CaptureNotice() {
38
- const status = getCaptureStatus();
39
- const notices: string[] = [];
40
- if (status.rnDecoratorTooLate) {
41
- notices.push(
42
- 'RN <Image> instrumentation missed the module-load race — add `import "@buoy-gg/images/register"` as the FIRST import of your entry file.'
43
- );
44
- }
45
- if (!status.expoAvailable && !status.rnDecoratorActive && !status.rnDecoratorTooLate) {
46
- notices.push("Waiting for images… render some <Image> components to populate the list.");
47
- }
48
- if (notices.length === 0) return null;
49
- return (
50
- <View style={styles.notice}>
51
- {notices.map((n) => (
52
- <Text key={n} style={styles.noticeText}>
53
- {n}
54
- </Text>
55
- ))}
56
- </View>
57
- );
58
- }
59
-
60
- function EmptyState() {
61
- return (
62
- <View style={styles.empty}>
63
- <Text style={styles.emptyTitle}>No images captured yet</Text>
64
- <Text style={styles.emptyText}>
65
- Every RN {"<Image>"} and expo-image load is recorded here with cache
66
- verdicts, timings, and oversize checks.
67
- </Text>
68
- </View>
69
- );
70
- }
71
-
72
- export function ImagesModal({
73
- visible,
74
- onClose,
75
- onBack,
76
- onMinimize,
77
- persistenceKey,
78
- inline = false,
79
- }: ImagesModalProps) {
80
- // Safety net: ensures capture is live even without the early register
81
- // import (expo-image capture is timing-immune, so this still gets most data).
82
- installImageCapture();
83
-
84
- const { records, stats } = useImageRecords();
85
- const [selectedId, setSelectedId] = useState<number | null>(null);
86
- const selected = selectedId != null ? getRecord(selectedId) : undefined;
87
-
88
- // Mirror mode: the synced wire records omit errorHeaders (detail-only data)
89
- // — pull them from the device the first time a failed record is opened.
90
- const wantHeadersFor =
91
- selected?.status === "error" && selected.errorHeaders == null
92
- ? selected.id
93
- : null;
94
- useEffect(() => {
95
- const remote = remoteImagesDispatch();
96
- if (wantHeadersFor == null || !remote) return;
97
- let cancelled = false;
98
- remote("getDetail", { id: wantHeadersFor })
99
- .then((detail) => {
100
- if (cancelled) return;
101
- const headers = (detail as { errorHeaders?: Record<string, string> | null })
102
- ?.errorHeaders;
103
- // `{}` (not undefined) so a headerless error doesn't refetch forever.
104
- updateRecord(wantHeadersFor, { errorHeaders: headers ?? {} });
105
- })
106
- .catch(() => {});
107
- return () => {
108
- cancelled = true;
109
- };
110
- }, [wantHeadersFor]);
111
-
112
- // Stepping is relative to the live list (newest first): previous = newer.
113
- const selectedIndex = selected
114
- ? records.findIndex((r) => r.id === selected.id)
115
- : -1;
116
- const stepTo = (index: number) => {
117
- const target = records[index];
118
- if (target) setSelectedId(target.id);
119
- };
120
-
121
- // Mode-appropriate report: single record in detail, full registry (stats +
122
- // insights + table) in the list. The shared ToolbarCopyButton resolves this
123
- // lazily on press and handles clipboard + feedback + gating.
124
- const getReport = () =>
125
- selected
126
- ? formatRecordMarkdown(selected)
127
- : formatRegistryMarkdown(
128
- records,
129
- stats,
130
- computeInsights(records),
131
- getCaptureStatus(),
132
- getGlobalModes()
133
- );
134
-
135
- const headerContent = (
136
- <ImagesHeader
137
- stats={stats}
138
- detailOpen={selected != null}
139
- detailIndex={selectedIndex}
140
- detailTotal={records.length}
141
- onCloseDetail={() => setSelectedId(null)}
142
- onClear={clearRecords}
143
- getReport={getReport}
144
- onBack={onBack}
145
- />
146
- );
147
-
148
- const body = selected ? (
149
- <View style={styles.detailContainer}>
150
- <ImageDetail record={selected} />
151
- <ImageDetailFooter
152
- key={selected.id}
153
- record={selected}
154
- hasPrevious={selectedIndex > 0}
155
- hasNext={selectedIndex >= 0 && selectedIndex < records.length - 1}
156
- onPrevious={() => stepTo(selectedIndex - 1)}
157
- onNext={() => stepTo(selectedIndex + 1)}
158
- />
159
- </View>
160
- ) : (
161
- <View style={styles.listContainer}>
162
- <InsightsBar insights={computeInsights(records)} />
163
- <CaptureNotice />
164
- <FlatList
165
- data={records}
166
- keyExtractor={(item: ImageRecord) => String(item.id)}
167
- renderItem={({ item }) => <ImageRow record={item} onPress={setSelectedId} />}
168
- ListEmptyComponent={<EmptyState />}
169
- />
170
- <ImagesListFooter />
171
- </View>
172
- );
173
-
174
- // The ONLY difference between the phone and the desktop dashboard is which
175
- // shell the header and body go in. Everything above this is shared.
176
- if (inline) {
177
- return (
178
- <View style={styles.inlineRoot}>
179
- {/* Flex-less wrapper — `ModalHeader` is `flex: 1` for JsModal's
180
- ROW-direction header slot and would grow vertically here. */}
181
- <View style={styles.inlineHeader}>{headerContent}</View>
182
- {body}
183
- </View>
184
- );
185
- }
186
-
187
- return (
188
- <JsModal
189
- visible={visible}
190
- onClose={onClose}
191
- onMinimize={onMinimize}
192
- persistenceKey={persistenceKey}
193
- header={{
194
- showToggleButton: true,
195
- customContent: headerContent,
196
- }}
197
- enablePersistence
198
- initialMode="bottomSheet"
199
- disableScrollWrapper
200
- >
201
- {body}
202
- </JsModal>
203
- );
204
- }
205
-
206
- /**
207
- * The same Images tool as `ImagesModal`, rendered INLINE for hosts that place
208
- * tools in a panel rather than a modal (the desktop dashboard). Exists so
209
- * there is exactly one implementation of the tool's header, list and detail.
210
- */
211
- export function ImagesToolView(
212
- props: Omit<ImagesModalProps, "visible" | "onClose" | "inline"> = {}
213
- ) {
214
- return <ImagesModal {...props} inline visible onClose={() => {}} />;
215
- }
216
-
217
- const styles = StyleSheet.create({
218
- inlineRoot: {
219
- flex: 1,
220
- minHeight: 0,
221
- // Transparent so the desktop workbench's hoisted sky (one field spanning
222
- // sidebar + panel) shows through — the shell paints its base beneath it.
223
- backgroundColor: "transparent",
224
- },
225
- inlineHeader: {
226
- paddingHorizontal: 12,
227
- paddingVertical: 8,
228
- borderBottomWidth: 1,
229
- borderBottomColor: macOSColors.border.default,
230
- // Clear, like Bench — the host's hoisted sky shows through the header.
231
- backgroundColor: "transparent",
232
- },
233
- listContainer: {
234
- flex: 1,
235
- },
236
- detailContainer: {
237
- flex: 1,
238
- },
239
- notice: {
240
- margin: 10,
241
- padding: 10,
242
- borderRadius: 8,
243
- backgroundColor: night.color.warningSoft,
244
- gap: 4,
245
- },
246
- noticeText: {
247
- color: night.color.warning,
248
- fontSize: 11,
249
- },
250
- empty: {
251
- alignItems: "center",
252
- padding: 32,
253
- gap: 8,
254
- },
255
- emptyTitle: {
256
- color: night.color.text,
257
- fontSize: 13,
258
- fontWeight: "600",
259
- },
260
- emptyText: {
261
- color: night.color.textSecondary,
262
- fontSize: 11,
263
- textAlign: "center",
264
- lineHeight: 16,
265
- },
266
- });
@@ -1,18 +0,0 @@
1
- /**
2
- * Mount once near the app root to start image capture immediately.
3
- *
4
- * NOTE: for full RN-core <Image> coverage prefer the side-effect import
5
- * `import "@buoy-gg/images/register"` as the first import of your entry file —
6
- * the RN decorator must be installed before React Native's Image module first
7
- * evaluates. ImagesRoot alone still captures every expo-image instance
8
- * (timing-immune) and reports whether the RN hook landed.
9
- */
10
-
11
- import { installImageCapture } from "../capture/install";
12
-
13
- export function ImagesRoot(): null {
14
- // Synchronous on first render — earlier than an effect, though still later
15
- // than the register import. Idempotent.
16
- installImageCapture();
17
- return null;
18
- }
@@ -1,51 +0,0 @@
1
- /**
2
- * InsightsBar — compact warning chips above the list when cross-record
3
- * findings exist (duplicates, retry storms, queue saturation, missing alt,
4
- * layout shifts). Renders nothing when all clear.
5
- *
6
- * A wrapping row (not a horizontal ScrollView — that under-measures its
7
- * height inside the modal's flex column and clips the chips).
8
- */
9
-
10
- import { StyleSheet, Text, View } from "react-native";
11
- import { night } from "@buoy-gg/shared-ui";
12
- import { insightChips } from "../store/insights";
13
- import type { ImageInsights } from "../types";
14
-
15
- export function InsightsBar({ insights }: { insights: ImageInsights }) {
16
- const chips = insightChips(insights);
17
- if (chips.length === 0) return null;
18
- return (
19
- <View style={styles.bar}>
20
- {chips.map((chip) => (
21
- <View key={chip} style={styles.chip}>
22
- <Text style={styles.chipText}>{chip}</Text>
23
- </View>
24
- ))}
25
- </View>
26
- );
27
- }
28
-
29
- const styles = StyleSheet.create({
30
- bar: {
31
- flexDirection: "row",
32
- flexWrap: "wrap",
33
- gap: 6,
34
- paddingHorizontal: 10,
35
- paddingVertical: 8,
36
- borderBottomWidth: StyleSheet.hairlineWidth,
37
- borderBottomColor: night.color.border,
38
- },
39
- chip: {
40
- backgroundColor: night.color.warningSoft,
41
- borderRadius: 999,
42
- paddingHorizontal: 10,
43
- paddingVertical: 5,
44
- },
45
- chipText: {
46
- color: night.color.warning,
47
- fontSize: 10,
48
- fontWeight: "600",
49
- lineHeight: 13,
50
- },
51
- });
package/src/export.ts DELETED
@@ -1,100 +0,0 @@
1
- /**
2
- * Copy-friendly markdown reports — paste-ready context for an issue, PR, or
3
- * an AI chat. One for the whole registry (list header copy) and one for a
4
- * single record (detail header copy).
5
- */
6
-
7
- import { PixelRatio } from "react-native";
8
- import {
9
- estDecodedBytes,
10
- estWastedBytes,
11
- neededPixels,
12
- } from "./store/imageEventStore";
13
- import { insightChips } from "./store/insights";
14
- import { cacheBadge, formatBytes, formatMs, sizeVerdict, uriTail } from "./format";
15
- import type { CaptureStatus, ImageInsights, ImageRecord, ImageStats } from "./types";
16
-
17
- export function formatRecordMarkdown(record: ImageRecord): string {
18
- const verdict = sizeVerdict(record);
19
- const cache = cacheBadge(record);
20
- const needed = neededPixels(record);
21
- const decoded = estDecodedBytes(record);
22
- const wasted = estWastedBytes(record);
23
-
24
- const lines: string[] = [];
25
- lines.push(`## Image report — ${uriTail(record.uri)}`);
26
- lines.push("");
27
- lines.push(`- URI: ${record.uri || "(none)"}`);
28
- lines.push(`- Library: ${record.lib === "expo" ? "expo-image" : "RN <Image>"} · kind: ${record.sourceKind} · mounted: ${record.mounted ? "yes" : "no"}`);
29
- lines.push(`- Status: ${record.status}${record.errorCode ? ` (HTTP ${record.errorCode})` : ""} · load cycles: ${record.loadCount}`);
30
- lines.push(`- Cache: ${cache.label}${record.cacheVerdictSource ? ` (via ${record.cacheVerdictSource === "event" ? "expo onLoad" : "Image.queryCache"})` : ""}`);
31
- lines.push(`- Load time: ${formatMs(record.durationMs)}${record.displayedAt && record.loadedAt ? ` · decode→display ${formatMs(Math.max(0, record.displayedAt - record.loadedAt))}` : ""}`);
32
- if (record.intrinsic) {
33
- lines.push(`- Decoded: ${record.intrinsic.width}×${record.intrinsic.height}px${record.mediaType ? ` (${record.mediaType})` : ""}${record.isAnimated ? " animated" : ""} ≈ ${formatBytes(decoded)} decoded memory`);
34
- }
35
- if (record.layout) {
36
- lines.push(`- Displayed: ${Math.round(record.layout.width)}×${Math.round(record.layout.height)}dp @${PixelRatio.get()}x (needs ${needed ? `${needed.width}×${needed.height}px` : "?"})`);
37
- }
38
- if (verdict.label) {
39
- lines.push(`- Size verdict: ${verdict.label}${wasted > 0 && needed ? ` — wasting ≈${formatBytes(wasted)}; serve ${needed.width}×${needed.height}` : ""}`);
40
- }
41
- if (record.progressSeen) {
42
- lines.push(`- Network transfer: ${record.bytesTotal ? formatBytes(record.bytesTotal) : "observed"}`);
43
- }
44
- if (record.hasAltText === false) lines.push(`- Accessibility: MISSING alt/accessibilityLabel`);
45
- if ((record.layoutShifts ?? 0) > 0) lines.push(`- Layout shifts after load: ${record.layoutShifts} (reserve explicit dimensions)`);
46
- if (record.error) {
47
- lines.push(`- Error: ${record.error}`);
48
- if (record.errorHeaders) {
49
- lines.push(`- Response headers: ${JSON.stringify(record.errorHeaders)}`);
50
- }
51
- }
52
- if (record.overrideLabel) lines.push(`- Simulation active: ${record.overrideLabel}`);
53
- return lines.join("\n");
54
- }
55
-
56
- export function formatRegistryMarkdown(
57
- records: ImageRecord[],
58
- stats: ImageStats,
59
- insights: ImageInsights,
60
- captureStatus: CaptureStatus,
61
- modes: { network: string; blank: boolean }
62
- ): string {
63
- const lines: string[] = [];
64
- lines.push(`## Buoy Images registry (${stats.total} records)`);
65
- lines.push("");
66
- lines.push(
67
- `${stats.loaded} loaded · ${stats.errors} failed · ${stats.loading} in flight · ≈${formatBytes(stats.estDecodedBytes)} decoded` +
68
- (stats.estWastedBytes > 0 ? ` · ${formatBytes(stats.estWastedBytes)} wasted on oversized sources` : "")
69
- );
70
- const chips = insightChips(insights);
71
- if (chips.length > 0) lines.push(`Insights: ${chips.join(" · ")}`);
72
- if (modes.network !== "normal" || modes.blank) {
73
- lines.push(`Simulation active: network=${modes.network}${modes.blank ? ", blank images" : ""}`);
74
- }
75
- if (captureStatus.rnDecoratorTooLate) {
76
- lines.push(`⚠ RN <Image> capture inactive (register import too late)`);
77
- }
78
- lines.push("");
79
- lines.push("| image | lib | status | cache | ms | decoded px | shown dp | verdict | notes |");
80
- lines.push("|---|---|---|---|---|---|---|---|---|");
81
- for (const r of records) {
82
- const verdict = sizeVerdict(r);
83
- const cache = cacheBadge(r);
84
- const dims = r.intrinsic ? `${r.intrinsic.width}×${r.intrinsic.height}` : "–";
85
- const layout = r.layout ? `${Math.round(r.layout.width)}×${Math.round(r.layout.height)}` : "–";
86
- const notes = [
87
- r.error ? `error: ${r.error}` : null,
88
- r.overrideLabel ? `sim: ${r.overrideLabel}` : null,
89
- r.hasAltText === false ? "no alt" : null,
90
- (r.layoutShifts ?? 0) > 0 ? `${r.layoutShifts} layout shifts` : null,
91
- r.mounted ? null : "unmounted",
92
- ]
93
- .filter(Boolean)
94
- .join("; ");
95
- lines.push(
96
- `| ${uriTail(r.uri)} | ${r.lib} | ${r.status} | ${cache.label} | ${r.durationMs != null ? Math.round(r.durationMs) : "–"} | ${dims} | ${layout} | ${verdict.label || "–"} | ${notes || "–"} |`
97
- );
98
- }
99
- return lines.join("\n");
100
- }
package/src/format.ts DELETED
@@ -1,85 +0,0 @@
1
- /**
2
- * Formatting + verdict helpers shared by list rows and the detail view.
3
- */
4
-
5
- import { macOSColors, night } from "@buoy-gg/shared-ui";
6
- import { oversizeFactor } from "./store/imageEventStore";
7
- import type { ImageRecord } from "./types";
8
-
9
- export function formatBytes(bytes: number): string {
10
- if (bytes <= 0) return "0 B";
11
- if (bytes < 1024) return `${bytes} B`;
12
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
13
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
14
- }
15
-
16
- export function formatMs(ms: number | undefined): string {
17
- if (ms == null) return "—";
18
- if (ms < 1000) return `${Math.round(ms)}ms`;
19
- return `${(ms / 1000).toFixed(2)}s`;
20
- }
21
-
22
- /** Last path segment of a URI, for compact row display. */
23
- export function uriTail(uri: string): string {
24
- if (!uri) return "(no source)";
25
- // Metro dev-server asset URLs put the real path in ?unstable_path=…
26
- const unstable = uri.match(/[?&]unstable_path=([^&?]+)/);
27
- const effective = unstable ? decodeURIComponent(unstable[1]!) : uri;
28
- const withoutQuery = effective.split("?")[0] ?? effective;
29
- const segments = withoutQuery.split("/").filter(Boolean);
30
- return segments[segments.length - 1] ?? effective;
31
- }
32
-
33
- export interface SizeVerdict {
34
- label: string;
35
- color: string;
36
- /** Linear factor (2 = twice as wide/tall as needed). */
37
- factor: number | null;
38
- }
39
-
40
- /**
41
- * Fresco-style fit verdict: green within 10%, yellow within 50%, red beyond.
42
- * Undersized sources (upscaled, likely blurry) get their own callout.
43
- */
44
- export function sizeVerdict(record: ImageRecord): SizeVerdict {
45
- const factor = oversizeFactor(record);
46
- if (factor == null) return { label: "", color: night.color.textTertiary, factor };
47
- if (factor < 0.9) {
48
- return { label: `${factor.toFixed(1)}× upscaled`, color: macOSColors.semantic.info, factor };
49
- }
50
- if (factor <= 1.1) {
51
- return { label: "right-sized", color: macOSColors.semantic.success, factor };
52
- }
53
- if (factor <= 1.5) {
54
- return { label: `${factor.toFixed(1)}× oversized`, color: macOSColors.semantic.warning, factor };
55
- }
56
- return { label: `${factor.toFixed(1)}× oversized`, color: macOSColors.semantic.error, factor };
57
- }
58
-
59
- export function statusColor(record: ImageRecord): string {
60
- switch (record.status) {
61
- case "loaded":
62
- return macOSColors.semantic.success;
63
- case "error":
64
- return macOSColors.semantic.error;
65
- case "loading":
66
- return macOSColors.semantic.warning;
67
- default:
68
- return night.color.textTertiary;
69
- }
70
- }
71
-
72
- /** Cache verdict badge text + tint (memory=green, disk=yellow, network=red). */
73
- export function cacheBadge(record: ImageRecord): { label: string; color: string } {
74
- const verdict = record.cacheVerdict;
75
- if (verdict === "memory" || verdict === "disk/memory") {
76
- return { label: verdict.toUpperCase(), color: macOSColors.semantic.success };
77
- }
78
- if (verdict === "disk") {
79
- return { label: "DISK", color: macOSColors.semantic.warning };
80
- }
81
- if (verdict === "none" || record.progressSeen) {
82
- return { label: "NETWORK", color: macOSColors.semantic.error };
83
- }
84
- return { label: "?", color: night.color.textTertiary };
85
- }
@@ -1,24 +0,0 @@
1
- /**
2
- * React bindings for the image event store.
3
- */
4
-
5
- import { useMemo, useSyncExternalStore } from "react";
6
- import { computeStats, getSnapshot, subscribe } from "../store/imageEventStore";
7
- import type { ImageRecord, ImageStats } from "../types";
8
-
9
- export interface UseImageRecordsResult {
10
- /** Newest first. */
11
- records: ImageRecord[];
12
- stats: ImageStats;
13
- }
14
-
15
- export function useImageRecords(): UseImageRecordsResult {
16
- const snapshot = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
17
- return useMemo(
18
- () => ({
19
- records: [...snapshot].reverse(),
20
- stats: computeStats(snapshot),
21
- }),
22
- [snapshot]
23
- );
24
- }
package/src/index.tsx DELETED
@@ -1,120 +0,0 @@
1
- /**
2
- * @buoy-gg/images
3
- *
4
- * Image debugger for React Native: a live registry of every image the app
5
- * loads — RN core <Image> (via the official component-decorator hook) and
6
- * expo-image (via a timing-immune class patch) — with cache verdicts
7
- * (memory/disk/network), load timings, decoded-vs-displayed oversize checks,
8
- * estimated decoded/wasted memory, and a failure log with HTTP details where
9
- * the platform provides them.
10
- *
11
- * Image HTTP traffic never touches the JS network stack, so network devtools
12
- * cannot see it — this tool is the visibility layer for images.
13
- */
14
-
15
- // Side-effect: install capture as early as this package is imported. For full
16
- // RN-core coverage, import "@buoy-gg/images/register" FIRST in your entry file
17
- // (the RN decorator must beat React Native's Image module evaluation).
18
- import "./register";
19
-
20
- // =============================================================================
21
- // PRESET (Primary entry point — pass to <FloatingDevTools apps={[...]} />)
22
- // =============================================================================
23
- export { imagesToolPreset, createImagesTool } from "./preset";
24
-
25
- // =============================================================================
26
- // ROOT (optional; capture also auto-installs on package import)
27
- // =============================================================================
28
- export { ImagesRoot } from "./components/ImagesRoot";
29
-
30
- // =============================================================================
31
- // COMPONENTS
32
- // =============================================================================
33
- export { ImagesModal } from "./components/ImagesModal";
34
- export { ImagesIcon, IMAGES_ICON_COLOR } from "./components/ImagesIcon";
35
-
36
- // =============================================================================
37
- // CAPTURE
38
- // =============================================================================
39
- export { installImageCapture, getCaptureStatus } from "./capture/install";
40
-
41
- // =============================================================================
42
- // ACTIONS
43
- // =============================================================================
44
- export {
45
- hardReloadRecord,
46
- retryRecord,
47
- evictExpoDiskEntry,
48
- } from "./capture/actions";
49
- export type { ActionResult } from "./capture/actions";
50
-
51
- // =============================================================================
52
- // SIMULATION OVERRIDES
53
- // =============================================================================
54
- export {
55
- setOverride,
56
- clearOverride,
57
- getOverride,
58
- flashRecord,
59
- setNetworkMode,
60
- setBlankImages,
61
- getGlobalModes,
62
- setOverrideForAllMounted,
63
- clearAllOverrides,
64
- flashAllMounted,
65
- } from "./capture/overrides";
66
- export { hardReloadAllMounted } from "./capture/actions";
67
- export type { ImageOverride, OverrideSource, NetworkMode } from "./capture/overrides";
68
-
69
- // =============================================================================
70
- // SAVINGS PROVER + INSIGHTS
71
- // =============================================================================
72
- export { proveSavings } from "./capture/savings";
73
- export type { SavingsResult } from "./capture/savings";
74
- export { computeInsights, insightChips } from "./store/insights";
75
- export { formatRecordMarkdown, formatRegistryMarkdown } from "./export";
76
-
77
- // =============================================================================
78
- // EXTERNAL SYNC (device → desktop dashboard / MCP)
79
- // =============================================================================
80
- export { imagesSyncAdapter } from "./sync/imagesSyncAdapter";
81
-
82
- // =============================================================================
83
- // DASHBOARD MIRROR (Buoy Desktop renders ImagesToolView fed by these)
84
- // =============================================================================
85
- export { ImagesToolView } from "./components/ImagesModal";
86
- export {
87
- disableImagesCapture,
88
- replaceImagesFromSnapshot,
89
- } from "./store/imageEventStore";
90
- export { setRemoteImagesHandler } from "./remote";
91
- export type { ImagesRemoteDispatch } from "./remote";
92
- export type { ImageRecordWire, ImagesSnapshotPayload } from "./types";
93
-
94
- // =============================================================================
95
- // STORE + HOOKS
96
- // =============================================================================
97
- export { useImageRecords } from "./hooks/useImageRecords";
98
- export {
99
- clearRecords,
100
- getSnapshot as getImageRecords,
101
- subscribe as subscribeImageRecords,
102
- computeStats,
103
- oversizeFactor,
104
- estDecodedBytes,
105
- estWastedBytes,
106
- } from "./store/imageEventStore";
107
-
108
- // =============================================================================
109
- // TYPES
110
- // =============================================================================
111
- export type {
112
- ImageRecord,
113
- ImageStats,
114
- ImageInsights,
115
- ImageLib,
116
- ImageStatus,
117
- CacheVerdict,
118
- SourceKind,
119
- CaptureStatus,
120
- } from "./types";