@buoy-gg/assets 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.
- package/lib/commonjs/components/AssetRow.js +13 -4
- package/lib/commonjs/components/AssetRow.js.map +1 -1
- package/lib/commonjs/utils/testIDs.js +114 -0
- package/lib/commonjs/utils/testIDs.js.map +1 -0
- package/lib/module/components/AssetRow.js +13 -4
- package/lib/module/components/AssetRow.js.map +1 -1
- package/lib/module/utils/testIDs.js +103 -0
- package/lib/module/utils/testIDs.js.map +1 -0
- package/lib/typescript/components/AssetRow.d.ts.map +1 -1
- package/lib/typescript/utils/testIDs.d.ts +75 -0
- package/lib/typescript/utils/testIDs.d.ts.map +1 -0
- package/package.json +5 -6
- package/src/capture/expo.ts +0 -45
- package/src/capture/install.ts +0 -73
- package/src/capture/measure.ts +0 -167
- package/src/capture/metroGraph.ts +0 -146
- package/src/capture/registry.ts +0 -137
- package/src/components/AssetDetail.tsx +0 -207
- package/src/components/AssetRow.tsx +0 -243
- package/src/components/AssetsFilterBar.tsx +0 -99
- package/src/components/AssetsHeader.tsx +0 -75
- package/src/components/AssetsIcon.tsx +0 -9
- package/src/components/AssetsListControls.tsx +0 -101
- package/src/components/AssetsModal.tsx +0 -371
- package/src/components/BaselineBar.tsx +0 -98
- package/src/export.ts +0 -110
- package/src/format.ts +0 -53
- package/src/hooks/useAssetRecords.ts +0 -26
- package/src/index.tsx +0 -87
- package/src/preset.tsx +0 -35
- package/src/remote.ts +0 -65
- package/src/state.ts +0 -84
- package/src/store/assetStore.ts +0 -209
- package/src/store/baseline.ts +0 -152
- package/src/store/insights.ts +0 -145
- package/src/sync/assetsSyncAdapter.ts +0 -151
- package/src/types.ts +0 -160
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AssetsListControls — one compact row: name search, sort cycle (SIZE →
|
|
3
|
-
* NAME → TYPE), and — when no baseline exists yet — the BASE button that
|
|
4
|
-
* saves one. Once a baseline exists the BaselineBar takes over the diff UI,
|
|
5
|
-
* so this row never wastes space on a hint sentence.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { StyleSheet, Text, TextInput, TouchableOpacity, View } from "react-native";
|
|
9
|
-
import { night } from "@buoy-gg/shared-ui";
|
|
10
|
-
import { saveBaseline } from "../store/baseline";
|
|
11
|
-
|
|
12
|
-
export type AssetSort = "size" | "name" | "type";
|
|
13
|
-
|
|
14
|
-
const NEXT_SORT: Record<AssetSort, AssetSort> = {
|
|
15
|
-
size: "name",
|
|
16
|
-
name: "type",
|
|
17
|
-
type: "size",
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
interface AssetsListControlsProps {
|
|
21
|
-
query: string;
|
|
22
|
-
onQueryChange: (query: string) => void;
|
|
23
|
-
sort: AssetSort;
|
|
24
|
-
onSortChange: (sort: AssetSort) => void;
|
|
25
|
-
/** Show the save-baseline button (no baseline saved yet). */
|
|
26
|
-
showBaselineButton: boolean;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function AssetsListControls({
|
|
30
|
-
query,
|
|
31
|
-
onQueryChange,
|
|
32
|
-
sort,
|
|
33
|
-
onSortChange,
|
|
34
|
-
showBaselineButton,
|
|
35
|
-
}: AssetsListControlsProps) {
|
|
36
|
-
return (
|
|
37
|
-
<View style={styles.row}>
|
|
38
|
-
<TextInput
|
|
39
|
-
style={styles.input}
|
|
40
|
-
value={query}
|
|
41
|
-
onChangeText={onQueryChange}
|
|
42
|
-
placeholder="Search assets…"
|
|
43
|
-
placeholderTextColor={night.color.placeholder}
|
|
44
|
-
autoCapitalize="none"
|
|
45
|
-
autoCorrect={false}
|
|
46
|
-
testID="buoy-assets-search"
|
|
47
|
-
/>
|
|
48
|
-
<TouchableOpacity
|
|
49
|
-
style={styles.button}
|
|
50
|
-
onPress={() => onSortChange(NEXT_SORT[sort])}
|
|
51
|
-
testID="buoy-assets-sort"
|
|
52
|
-
>
|
|
53
|
-
<Text style={styles.buttonText}>↓ {sort.toUpperCase()}</Text>
|
|
54
|
-
</TouchableOpacity>
|
|
55
|
-
{showBaselineButton ? (
|
|
56
|
-
<TouchableOpacity
|
|
57
|
-
style={styles.button}
|
|
58
|
-
onPress={() => void saveBaseline()}
|
|
59
|
-
testID="buoy-assets-save-baseline"
|
|
60
|
-
>
|
|
61
|
-
<Text style={styles.buttonText}>BASE</Text>
|
|
62
|
-
</TouchableOpacity>
|
|
63
|
-
) : null}
|
|
64
|
-
</View>
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const styles = StyleSheet.create({
|
|
69
|
-
row: {
|
|
70
|
-
flexDirection: "row",
|
|
71
|
-
alignItems: "center",
|
|
72
|
-
gap: 8,
|
|
73
|
-
paddingHorizontal: 10,
|
|
74
|
-
paddingBottom: 8,
|
|
75
|
-
},
|
|
76
|
-
input: {
|
|
77
|
-
flex: 1,
|
|
78
|
-
backgroundColor: night.color.surfaceElevated,
|
|
79
|
-
borderWidth: 1,
|
|
80
|
-
borderColor: night.color.border,
|
|
81
|
-
borderRadius: 8,
|
|
82
|
-
paddingHorizontal: 10,
|
|
83
|
-
paddingVertical: 6,
|
|
84
|
-
color: night.color.text,
|
|
85
|
-
fontSize: 12,
|
|
86
|
-
},
|
|
87
|
-
button: {
|
|
88
|
-
backgroundColor: night.color.surface,
|
|
89
|
-
borderWidth: 1,
|
|
90
|
-
borderColor: night.color.border,
|
|
91
|
-
borderRadius: 8,
|
|
92
|
-
paddingHorizontal: 10,
|
|
93
|
-
paddingVertical: 7,
|
|
94
|
-
},
|
|
95
|
-
buttonText: {
|
|
96
|
-
color: night.color.textSecondary,
|
|
97
|
-
fontSize: 10,
|
|
98
|
-
fontWeight: "700",
|
|
99
|
-
letterSpacing: 0.5,
|
|
100
|
-
},
|
|
101
|
-
});
|
|
@@ -1,371 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AssetsModal — bottom-sheet wrapper rendered by FloatingDevTools when the
|
|
3
|
-
* Assets tool is opened. Inventory list (largest first) → tap for detail.
|
|
4
|
-
* Opening triggers a full rescan (registry walk + Metro graph + size
|
|
5
|
-
* measurement); merely installing the package never hits the network.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { useEffect, useState } from "react";
|
|
9
|
-
import { FlatList, StyleSheet, Text, View } from "react-native";
|
|
10
|
-
import { EventStepperFooter, JsModal, macOSColors, night } from "@buoy-gg/shared-ui";
|
|
11
|
-
import type { ModalState } from "@buoy-gg/shared-ui";
|
|
12
|
-
import { getScanStatus, installAssetCapture, rescan } from "../capture/install";
|
|
13
|
-
import { formatAssetMarkdown, formatInventoryMarkdown } from "../export";
|
|
14
|
-
import { useAssetRecords } from "../hooks/useAssetRecords";
|
|
15
|
-
import { remoteAssetsDispatch } from "../remote";
|
|
16
|
-
import { clearRecords, estDecodedBytes, getRecord } from "../store/assetStore";
|
|
17
|
-
import { getDiff, loadBaseline } from "../store/baseline";
|
|
18
|
-
import { AssetDetail } from "./AssetDetail";
|
|
19
|
-
import { AssetRow } from "./AssetRow";
|
|
20
|
-
import { AssetsFilterBar, type AssetFilter } from "./AssetsFilterBar";
|
|
21
|
-
import { AssetsHeader } from "./AssetsHeader";
|
|
22
|
-
import { AssetsListControls, type AssetSort } from "./AssetsListControls";
|
|
23
|
-
import { BaselineBar } from "./BaselineBar";
|
|
24
|
-
import type { AssetRecord } from "../types";
|
|
25
|
-
|
|
26
|
-
interface AssetsModalProps {
|
|
27
|
-
visible: boolean;
|
|
28
|
-
onClose: () => void;
|
|
29
|
-
onBack?: () => void;
|
|
30
|
-
onMinimize?: (modalState: ModalState) => void;
|
|
31
|
-
persistenceKey?: string;
|
|
32
|
-
/** Render as a plain header-bar + body stack instead of a JsModal (desktop). */
|
|
33
|
-
inline?: boolean;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function ScanNotice() {
|
|
37
|
-
const status = getScanStatus();
|
|
38
|
-
const notices: string[] = [];
|
|
39
|
-
if (!status.registryAvailable) {
|
|
40
|
-
notices.push(
|
|
41
|
-
"Asset registry unavailable — this environment doesn't expose @react-native/assets-registry."
|
|
42
|
-
);
|
|
43
|
-
} else if (!status.graphSupported && status.graphError) {
|
|
44
|
-
notices.push(
|
|
45
|
-
"No Metro dev server — showing runtime-registered assets only. The full bundle inventory and byte sizes need a dev build."
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
if (status.measuring) notices.push("Measuring sizes from the dev server…");
|
|
49
|
-
if (notices.length === 0) return null;
|
|
50
|
-
return (
|
|
51
|
-
<View style={styles.notice}>
|
|
52
|
-
{notices.map((n) => (
|
|
53
|
-
<Text key={n} style={styles.noticeText}>
|
|
54
|
-
{n}
|
|
55
|
-
</Text>
|
|
56
|
-
))}
|
|
57
|
-
</View>
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function FontFamilies() {
|
|
62
|
-
const families = getScanStatus().fontFamilies;
|
|
63
|
-
if (!families.length) return null;
|
|
64
|
-
return (
|
|
65
|
-
<View style={styles.fontsBox}>
|
|
66
|
-
<Text style={styles.fontsTitle}>LOADED FONT FAMILIES ({families.length})</Text>
|
|
67
|
-
<View style={styles.fontsWrap}>
|
|
68
|
-
{families.map((family) => (
|
|
69
|
-
<Text key={family} style={styles.fontChip}>
|
|
70
|
-
{family}
|
|
71
|
-
</Text>
|
|
72
|
-
))}
|
|
73
|
-
</View>
|
|
74
|
-
</View>
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function EmptyState() {
|
|
79
|
-
return (
|
|
80
|
-
<View style={styles.empty}>
|
|
81
|
-
<Text style={styles.emptyTitle}>No assets found yet</Text>
|
|
82
|
-
<Text style={styles.emptyText}>
|
|
83
|
-
Every bundled asset — images, fonts, video, audio — appears here with
|
|
84
|
-
sizes, scale coverage, and never-loaded detection. Try Rescan after
|
|
85
|
-
navigating around the app.
|
|
86
|
-
</Text>
|
|
87
|
-
</View>
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Largest first — measured bytes, falling back to decoded estimate. */
|
|
92
|
-
function bySizeDesc(a: AssetRecord, b: AssetRecord): number {
|
|
93
|
-
return (b.sizeBytes ?? estDecodedBytes(b)) - (a.sizeBytes ?? estDecodedBytes(a));
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const COMPARATORS: Record<AssetSort, (a: AssetRecord, b: AssetRecord) => number> = {
|
|
97
|
-
size: bySizeDesc,
|
|
98
|
-
name: (a, b) => a.name.localeCompare(b.name) || a.type.localeCompare(b.type),
|
|
99
|
-
type: (a, b) => a.type.localeCompare(b.type) || bySizeDesc(a, b),
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
export function AssetsModal({
|
|
103
|
-
visible,
|
|
104
|
-
onClose,
|
|
105
|
-
onBack,
|
|
106
|
-
onMinimize,
|
|
107
|
-
persistenceKey,
|
|
108
|
-
inline = false,
|
|
109
|
-
}: AssetsModalProps) {
|
|
110
|
-
// Safety net: the index import normally installed this already.
|
|
111
|
-
installAssetCapture();
|
|
112
|
-
|
|
113
|
-
const { records, stats, insights } = useAssetRecords();
|
|
114
|
-
const [selectedId, setSelectedId] = useState<number | null>(null);
|
|
115
|
-
const [filter, setFilter] = useState<AssetFilter>("all");
|
|
116
|
-
const [query, setQuery] = useState("");
|
|
117
|
-
const [sort, setSort] = useState<AssetSort>("size");
|
|
118
|
-
const selected = selectedId != null ? getRecord(selectedId) : undefined;
|
|
119
|
-
|
|
120
|
-
// Full scan on open — locally on device, forwarded on dashboard mirrors.
|
|
121
|
-
// The baseline load is async and re-renders through the store emit.
|
|
122
|
-
useEffect(() => {
|
|
123
|
-
if (!visible) return;
|
|
124
|
-
void loadBaseline();
|
|
125
|
-
const remote = remoteAssetsDispatch();
|
|
126
|
-
if (remote) {
|
|
127
|
-
remote("rescan").catch(() => {});
|
|
128
|
-
} else {
|
|
129
|
-
void rescan();
|
|
130
|
-
}
|
|
131
|
-
}, [visible]);
|
|
132
|
-
|
|
133
|
-
const sorted = [...records].sort(COMPARATORS[sort]);
|
|
134
|
-
const trimmedQuery = query.trim().toLowerCase();
|
|
135
|
-
const filtered = sorted.filter((r) => {
|
|
136
|
-
if (filter === "unused" && r.loaded) return false;
|
|
137
|
-
if (filter !== "all" && filter !== "unused" && r.kind !== filter) return false;
|
|
138
|
-
if (trimmedQuery && !`${r.name}.${r.type}`.toLowerCase().includes(trimmedQuery)) {
|
|
139
|
-
return false;
|
|
140
|
-
}
|
|
141
|
-
return true;
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
const selectedIndex = selected
|
|
145
|
-
? filtered.findIndex((r) => r.id === selected.id)
|
|
146
|
-
: -1;
|
|
147
|
-
const stepTo = (index: number) => {
|
|
148
|
-
const target = filtered[index];
|
|
149
|
-
if (target) setSelectedId(target.id);
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
// Set lookups so each row's badge checks are O(1).
|
|
153
|
-
const flagSets = {
|
|
154
|
-
scaleGaps: new Set(insights.scaleGaps),
|
|
155
|
-
scaleAnomalies: new Set(insights.scaleAnomalies),
|
|
156
|
-
duplicates: new Set(insights.duplicates.flatMap((d) => d.ids)),
|
|
157
|
-
webpCandidates: new Set(insights.webpCandidates),
|
|
158
|
-
overDecode: new Set(insights.overDecode),
|
|
159
|
-
heavy: new Set([...insights.heavyGifs, ...insights.heavyFonts]),
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
const diff = getDiff();
|
|
163
|
-
|
|
164
|
-
// The proportional size bar is relative to the largest asset in view.
|
|
165
|
-
let maxBytes = 0;
|
|
166
|
-
for (const r of filtered) {
|
|
167
|
-
const bytes = r.sizeBytes ?? estDecodedBytes(r);
|
|
168
|
-
if (bytes > maxBytes) maxBytes = bytes;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const getReport = () =>
|
|
172
|
-
selected
|
|
173
|
-
? formatAssetMarkdown(selected)
|
|
174
|
-
: formatInventoryMarkdown(sorted, stats, insights, getScanStatus(), getDiff());
|
|
175
|
-
|
|
176
|
-
const headerContent = (
|
|
177
|
-
<AssetsHeader
|
|
178
|
-
stats={stats}
|
|
179
|
-
measuring={getScanStatus().measuring}
|
|
180
|
-
detailOpen={selected != null}
|
|
181
|
-
detailIndex={selectedIndex}
|
|
182
|
-
detailTotal={filtered.length}
|
|
183
|
-
onCloseDetail={() => setSelectedId(null)}
|
|
184
|
-
onClear={clearRecords}
|
|
185
|
-
getReport={getReport}
|
|
186
|
-
onBack={onBack}
|
|
187
|
-
/>
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
const body = selected ? (
|
|
191
|
-
<View style={styles.detailContainer}>
|
|
192
|
-
<AssetDetail record={selected} insights={insights} />
|
|
193
|
-
{/* Same stepper the network tool uses — steps through the FILTERED
|
|
194
|
-
list, so search/kind filters re-scope prev/next in place. */}
|
|
195
|
-
{selectedIndex >= 0 ? (
|
|
196
|
-
<EventStepperFooter
|
|
197
|
-
currentIndex={selectedIndex}
|
|
198
|
-
totalItems={filtered.length}
|
|
199
|
-
itemLabel="Asset"
|
|
200
|
-
onPrevious={() => stepTo(selectedIndex - 1)}
|
|
201
|
-
onNext={() => stepTo(selectedIndex + 1)}
|
|
202
|
-
/>
|
|
203
|
-
) : null}
|
|
204
|
-
</View>
|
|
205
|
-
) : (
|
|
206
|
-
<View style={styles.listContainer}>
|
|
207
|
-
<AssetsFilterBar stats={stats} filter={filter} onChange={setFilter} />
|
|
208
|
-
<AssetsListControls
|
|
209
|
-
query={query}
|
|
210
|
-
onQueryChange={setQuery}
|
|
211
|
-
sort={sort}
|
|
212
|
-
onSortChange={setSort}
|
|
213
|
-
showBaselineButton={diff == null}
|
|
214
|
-
/>
|
|
215
|
-
<BaselineBar diff={diff} />
|
|
216
|
-
<ScanNotice />
|
|
217
|
-
{filter === "font" ? <FontFamilies /> : null}
|
|
218
|
-
<FlatList
|
|
219
|
-
data={filtered}
|
|
220
|
-
keyExtractor={(item: AssetRecord) => String(item.id)}
|
|
221
|
-
contentContainerStyle={styles.listContent}
|
|
222
|
-
renderItem={({ item }) => (
|
|
223
|
-
<AssetRow
|
|
224
|
-
record={item}
|
|
225
|
-
flags={{
|
|
226
|
-
scaleGap: flagSets.scaleGaps.has(item.id),
|
|
227
|
-
scaleAnomaly: flagSets.scaleAnomalies.has(item.id),
|
|
228
|
-
duplicate: flagSets.duplicates.has(item.id),
|
|
229
|
-
webp: flagSets.webpCandidates.has(item.id),
|
|
230
|
-
oversize: flagSets.overDecode.has(item.id),
|
|
231
|
-
heavy: flagSets.heavy.has(item.id),
|
|
232
|
-
}}
|
|
233
|
-
sizeFraction={
|
|
234
|
-
maxBytes > 0
|
|
235
|
-
? (item.sizeBytes ?? estDecodedBytes(item)) / maxBytes
|
|
236
|
-
: 0
|
|
237
|
-
}
|
|
238
|
-
// Guarded: programmatic invocations (automation) can call this
|
|
239
|
-
// with an event object instead of an id.
|
|
240
|
-
onPress={(id) => setSelectedId(typeof id === "number" ? id : null)}
|
|
241
|
-
/>
|
|
242
|
-
)}
|
|
243
|
-
ListEmptyComponent={<EmptyState />}
|
|
244
|
-
/>
|
|
245
|
-
</View>
|
|
246
|
-
);
|
|
247
|
-
|
|
248
|
-
// The ONLY difference between phone and desktop is which shell the header
|
|
249
|
-
// and body go in (same pattern as ImagesModal).
|
|
250
|
-
if (inline) {
|
|
251
|
-
return (
|
|
252
|
-
<View style={styles.inlineRoot}>
|
|
253
|
-
<View style={styles.inlineHeader}>{headerContent}</View>
|
|
254
|
-
{body}
|
|
255
|
-
</View>
|
|
256
|
-
);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
return (
|
|
260
|
-
<JsModal
|
|
261
|
-
visible={visible}
|
|
262
|
-
onClose={onClose}
|
|
263
|
-
onMinimize={onMinimize}
|
|
264
|
-
persistenceKey={persistenceKey}
|
|
265
|
-
header={{
|
|
266
|
-
showToggleButton: true,
|
|
267
|
-
customContent: headerContent,
|
|
268
|
-
}}
|
|
269
|
-
enablePersistence
|
|
270
|
-
initialMode="bottomSheet"
|
|
271
|
-
disableScrollWrapper
|
|
272
|
-
>
|
|
273
|
-
{body}
|
|
274
|
-
</JsModal>
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* The same Assets tool rendered INLINE for hosts that place tools in a panel
|
|
280
|
-
* rather than a modal (the desktop dashboard). One implementation only.
|
|
281
|
-
*/
|
|
282
|
-
export function AssetsToolView(
|
|
283
|
-
props: Omit<AssetsModalProps, "visible" | "onClose" | "inline"> = {}
|
|
284
|
-
) {
|
|
285
|
-
return <AssetsModal {...props} inline visible onClose={() => {}} />;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const styles = StyleSheet.create({
|
|
289
|
-
inlineRoot: {
|
|
290
|
-
flex: 1,
|
|
291
|
-
minHeight: 0,
|
|
292
|
-
// Transparent so the desktop workbench's hoisted sky (one field spanning
|
|
293
|
-
// sidebar + panel) shows through — the shell paints its base beneath it.
|
|
294
|
-
backgroundColor: "transparent",
|
|
295
|
-
},
|
|
296
|
-
inlineHeader: {
|
|
297
|
-
paddingHorizontal: 12,
|
|
298
|
-
paddingVertical: 8,
|
|
299
|
-
borderBottomWidth: 1,
|
|
300
|
-
borderBottomColor: macOSColors.border.default,
|
|
301
|
-
// Clear, like Bench — the host's hoisted sky shows through the header.
|
|
302
|
-
backgroundColor: "transparent",
|
|
303
|
-
},
|
|
304
|
-
listContainer: {
|
|
305
|
-
flex: 1,
|
|
306
|
-
},
|
|
307
|
-
listContent: {
|
|
308
|
-
paddingBottom: 16,
|
|
309
|
-
},
|
|
310
|
-
detailContainer: {
|
|
311
|
-
flex: 1,
|
|
312
|
-
},
|
|
313
|
-
notice: {
|
|
314
|
-
marginHorizontal: 10,
|
|
315
|
-
marginBottom: 8,
|
|
316
|
-
padding: 10,
|
|
317
|
-
borderRadius: 8,
|
|
318
|
-
borderWidth: 1,
|
|
319
|
-
borderColor: night.color.border,
|
|
320
|
-
backgroundColor: night.color.surface,
|
|
321
|
-
gap: 4,
|
|
322
|
-
},
|
|
323
|
-
noticeText: {
|
|
324
|
-
color: night.color.textSecondary,
|
|
325
|
-
fontSize: 11,
|
|
326
|
-
},
|
|
327
|
-
fontsBox: {
|
|
328
|
-
marginHorizontal: 10,
|
|
329
|
-
marginTop: 8,
|
|
330
|
-
padding: 10,
|
|
331
|
-
borderRadius: 8,
|
|
332
|
-
backgroundColor: night.color.surface,
|
|
333
|
-
gap: 6,
|
|
334
|
-
},
|
|
335
|
-
fontsTitle: {
|
|
336
|
-
color: night.color.textTertiary,
|
|
337
|
-
fontSize: 9,
|
|
338
|
-
fontWeight: "700",
|
|
339
|
-
letterSpacing: 0.5,
|
|
340
|
-
},
|
|
341
|
-
fontsWrap: {
|
|
342
|
-
flexDirection: "row",
|
|
343
|
-
flexWrap: "wrap",
|
|
344
|
-
gap: 6,
|
|
345
|
-
},
|
|
346
|
-
fontChip: {
|
|
347
|
-
color: night.color.textSecondary,
|
|
348
|
-
fontSize: 10,
|
|
349
|
-
backgroundColor: night.color.bg,
|
|
350
|
-
borderRadius: 999,
|
|
351
|
-
paddingHorizontal: 8,
|
|
352
|
-
paddingVertical: 3,
|
|
353
|
-
overflow: "hidden",
|
|
354
|
-
},
|
|
355
|
-
empty: {
|
|
356
|
-
alignItems: "center",
|
|
357
|
-
padding: 32,
|
|
358
|
-
gap: 8,
|
|
359
|
-
},
|
|
360
|
-
emptyTitle: {
|
|
361
|
-
color: night.color.text,
|
|
362
|
-
fontSize: 13,
|
|
363
|
-
fontWeight: "600",
|
|
364
|
-
},
|
|
365
|
-
emptyText: {
|
|
366
|
-
color: night.color.textSecondary,
|
|
367
|
-
fontSize: 11,
|
|
368
|
-
textAlign: "center",
|
|
369
|
-
lineHeight: 16,
|
|
370
|
-
},
|
|
371
|
-
});
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* BaselineBar — save/compare-against-baseline controls.
|
|
3
|
-
*
|
|
4
|
-
* No baseline: one "SAVE BASELINE" button. With a baseline: the diff summary
|
|
5
|
-
* ("+3 new · 1 removed · +1.2 MB since 2h ago") plus UPDATE and ✕ (clear).
|
|
6
|
-
* Growth renders in warning color, shrinkage in success — that's the whole
|
|
7
|
-
* point of the feature.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
|
|
11
|
-
import { macOSColors, night } from "@buoy-gg/shared-ui";
|
|
12
|
-
import { formatBytes } from "../format";
|
|
13
|
-
import { clearBaseline, saveBaseline } from "../store/baseline";
|
|
14
|
-
import type { AssetsDiff } from "../types";
|
|
15
|
-
|
|
16
|
-
function formatAge(savedAt: number): string {
|
|
17
|
-
const minutes = Math.max(1, Math.round((Date.now() - savedAt) / 60000));
|
|
18
|
-
if (minutes < 60) return `${minutes}m ago`;
|
|
19
|
-
const hours = Math.round(minutes / 60);
|
|
20
|
-
if (hours < 48) return `${hours}h ago`;
|
|
21
|
-
return `${Math.round(hours / 24)}d ago`;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function BaselineBar({ diff }: { diff: AssetsDiff | null }) {
|
|
25
|
-
// No baseline → no bar; the BASE button in AssetsListControls creates one.
|
|
26
|
-
if (!diff) return null;
|
|
27
|
-
|
|
28
|
-
const parts: string[] = [];
|
|
29
|
-
if (diff.added.length) parts.push(`+${diff.added.length} new`);
|
|
30
|
-
if (diff.removed.length) parts.push(`${diff.removed.length} removed`);
|
|
31
|
-
if (diff.grown.length) parts.push(`${diff.grown.length} grew`);
|
|
32
|
-
const net =
|
|
33
|
-
diff.netBytes != null && Math.abs(diff.netBytes) >= 1024
|
|
34
|
-
? `${diff.netBytes > 0 ? "+" : "−"}${formatBytes(Math.abs(diff.netBytes))}`
|
|
35
|
-
: null;
|
|
36
|
-
const changed = parts.length > 0 || net != null;
|
|
37
|
-
const netColor =
|
|
38
|
-
diff.netBytes != null && diff.netBytes > 0
|
|
39
|
-
? macOSColors.semantic.warning
|
|
40
|
-
: macOSColors.semantic.success;
|
|
41
|
-
|
|
42
|
-
return (
|
|
43
|
-
<View style={styles.bar}>
|
|
44
|
-
<Text style={styles.summary} numberOfLines={1}>
|
|
45
|
-
{changed ? parts.join(" · ") : "No changes"}
|
|
46
|
-
{net ? " · " : ""}
|
|
47
|
-
{net ? <Text style={{ color: netColor }}>{net}</Text> : null}
|
|
48
|
-
<Text style={styles.age}> vs {formatAge(diff.baselineSavedAt)}</Text>
|
|
49
|
-
</Text>
|
|
50
|
-
<TouchableOpacity style={styles.button} onPress={() => void saveBaseline()}>
|
|
51
|
-
<Text style={styles.buttonText}>UPDATE</Text>
|
|
52
|
-
</TouchableOpacity>
|
|
53
|
-
<TouchableOpacity style={styles.button} onPress={() => void clearBaseline()}>
|
|
54
|
-
<Text style={styles.buttonText}>✕</Text>
|
|
55
|
-
</TouchableOpacity>
|
|
56
|
-
</View>
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const styles = StyleSheet.create({
|
|
61
|
-
bar: {
|
|
62
|
-
flexDirection: "row",
|
|
63
|
-
alignItems: "center",
|
|
64
|
-
gap: 8,
|
|
65
|
-
marginHorizontal: 10,
|
|
66
|
-
marginBottom: 8,
|
|
67
|
-
paddingHorizontal: 10,
|
|
68
|
-
paddingVertical: 6,
|
|
69
|
-
borderWidth: 1,
|
|
70
|
-
borderColor: night.color.border,
|
|
71
|
-
borderRadius: 8,
|
|
72
|
-
backgroundColor: night.color.surface,
|
|
73
|
-
},
|
|
74
|
-
summary: {
|
|
75
|
-
flex: 1,
|
|
76
|
-
color: night.color.textSecondary,
|
|
77
|
-
fontSize: 10,
|
|
78
|
-
fontWeight: "600",
|
|
79
|
-
},
|
|
80
|
-
age: {
|
|
81
|
-
color: night.color.textTertiary,
|
|
82
|
-
fontWeight: "400",
|
|
83
|
-
},
|
|
84
|
-
button: {
|
|
85
|
-
backgroundColor: night.color.surfaceElevated,
|
|
86
|
-
borderWidth: 1,
|
|
87
|
-
borderColor: night.color.border,
|
|
88
|
-
borderRadius: 6,
|
|
89
|
-
paddingHorizontal: 8,
|
|
90
|
-
paddingVertical: 5,
|
|
91
|
-
},
|
|
92
|
-
buttonText: {
|
|
93
|
-
color: night.color.textSecondary,
|
|
94
|
-
fontSize: 9,
|
|
95
|
-
fontWeight: "700",
|
|
96
|
-
letterSpacing: 0.5,
|
|
97
|
-
},
|
|
98
|
-
});
|
package/src/export.ts
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Markdown reports for the shared ToolbarCopyButton — the paste-into-your-
|
|
3
|
-
* issue-tracker view of the inventory.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { formatBytes, formatDimensions, formatScales } from "./format";
|
|
7
|
-
import { estDecodedBytes } from "./store/assetStore";
|
|
8
|
-
import type {
|
|
9
|
-
AssetRecord,
|
|
10
|
-
AssetsDiff,
|
|
11
|
-
AssetsInsights,
|
|
12
|
-
AssetsScanStatus,
|
|
13
|
-
AssetsStats,
|
|
14
|
-
} from "./types";
|
|
15
|
-
|
|
16
|
-
export function formatAssetMarkdown(record: AssetRecord): string {
|
|
17
|
-
const lines = [
|
|
18
|
-
`# Asset: ${record.name}.${record.type}`,
|
|
19
|
-
"",
|
|
20
|
-
`- Kind: ${record.kind}`,
|
|
21
|
-
`- Status: ${record.loaded ? "loaded at runtime" : "in bundle, never loaded"}`,
|
|
22
|
-
];
|
|
23
|
-
if (record.kind === "image") lines.push(`- Dimensions: ${formatDimensions(record)} (logical)`);
|
|
24
|
-
if (record.scales.length) lines.push(`- Scales: ${formatScales(record.scales)}`);
|
|
25
|
-
if (record.sizeBytes != null && record.sizeSource === "measured") {
|
|
26
|
-
lines.push(`- Size (all variants): ${formatBytes(record.sizeBytes)}`);
|
|
27
|
-
}
|
|
28
|
-
const estimate = estDecodedBytes(record);
|
|
29
|
-
if (estimate > 0) lines.push(`- Decoded memory: ~${formatBytes(estimate)}`);
|
|
30
|
-
if (record.hash) lines.push(`- Hash: ${record.hash}`);
|
|
31
|
-
if (record.httpServerLocation) lines.push(`- Location: ${record.httpServerLocation}`);
|
|
32
|
-
if (record.fileSystemLocation) lines.push(`- Source dir: ${record.fileSystemLocation}`);
|
|
33
|
-
if (record.resolvedUri) lines.push(`- Resolved URI: ${record.resolvedUri}`);
|
|
34
|
-
return lines.join("\n");
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function formatInventoryMarkdown(
|
|
38
|
-
records: AssetRecord[],
|
|
39
|
-
stats: AssetsStats,
|
|
40
|
-
insights: AssetsInsights,
|
|
41
|
-
scanStatus: AssetsScanStatus,
|
|
42
|
-
diff?: AssetsDiff | null
|
|
43
|
-
): string {
|
|
44
|
-
const lines = [
|
|
45
|
-
"# Asset inventory",
|
|
46
|
-
"",
|
|
47
|
-
`${stats.total} assets · ${stats.loaded} loaded · ${stats.neverLoaded} never loaded` +
|
|
48
|
-
(stats.totalBytes > 0
|
|
49
|
-
? ` · ${formatBytes(stats.totalBytes)} measured (${stats.measuredCount}/${stats.total})`
|
|
50
|
-
: ""),
|
|
51
|
-
];
|
|
52
|
-
if (scanStatus.fontFamilies.length) {
|
|
53
|
-
lines.push(`Fonts loaded: ${scanStatus.fontFamilies.join(", ")}`);
|
|
54
|
-
}
|
|
55
|
-
if (!scanStatus.graphSupported) {
|
|
56
|
-
lines.push(
|
|
57
|
-
scanStatus.graphError
|
|
58
|
-
? `_Metro bundle graph unavailable (${scanStatus.graphError}) — runtime-registered assets only._`
|
|
59
|
-
: "_No Metro bundle graph — runtime-registered assets only (release build)._"
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (diff) {
|
|
64
|
-
const net =
|
|
65
|
-
diff.netBytes != null
|
|
66
|
-
? `${diff.netBytes >= 0 ? "+" : "−"}${formatBytes(Math.abs(diff.netBytes))} net`
|
|
67
|
-
: "net size unknown";
|
|
68
|
-
lines.push(
|
|
69
|
-
"",
|
|
70
|
-
`## Since baseline (${new Date(diff.baselineSavedAt).toISOString()})`,
|
|
71
|
-
`- ${diff.added.length} added · ${diff.removed.length} removed · ${diff.grown.length} grew · ${net}`
|
|
72
|
-
);
|
|
73
|
-
for (const gone of diff.removed.slice(0, 20)) {
|
|
74
|
-
lines.push(`- removed: ${gone.name}.${gone.type}`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const findings: string[] = [];
|
|
79
|
-
if (insights.neverLoaded.length) findings.push(`- ${insights.neverLoaded.length} shipped but never loaded`);
|
|
80
|
-
if (insights.duplicates.length) findings.push(`- ${insights.duplicates.length} duplicate content groups`);
|
|
81
|
-
if (insights.webpCandidates.length) findings.push(`- ${insights.webpCandidates.length} WebP conversion candidates`);
|
|
82
|
-
if (insights.scaleGaps.length) findings.push(`- ${insights.scaleGaps.length} scale-coverage gaps`);
|
|
83
|
-
if (insights.overDecode.length) findings.push(`- ${insights.overDecode.length} images decoding larger than 2× the screen`);
|
|
84
|
-
if (insights.heavyGifs.length) findings.push(`- ${insights.heavyGifs.length} heavy GIFs (convert to video/Lottie)`);
|
|
85
|
-
if (insights.heavyFonts.length) findings.push(`- ${insights.heavyFonts.length} heavy fonts (subset candidates)`);
|
|
86
|
-
if (insights.scaleAnomalies.length) findings.push(`- ${insights.scaleAnomalies.length} scale-variant anomalies (check exports)`);
|
|
87
|
-
if (findings.length) lines.push("", "## Findings", ...findings);
|
|
88
|
-
|
|
89
|
-
lines.push(
|
|
90
|
-
"",
|
|
91
|
-
"## Assets (largest first)",
|
|
92
|
-
"",
|
|
93
|
-
"| Asset | Kind | Dimensions | Scales | Size | Loaded |",
|
|
94
|
-
"| --- | --- | --- | --- | --- | --- |"
|
|
95
|
-
);
|
|
96
|
-
for (const r of records) {
|
|
97
|
-
const size =
|
|
98
|
-
r.sizeBytes != null && r.sizeSource === "measured"
|
|
99
|
-
? formatBytes(r.sizeBytes)
|
|
100
|
-
: estDecodedBytes(r) > 0
|
|
101
|
-
? `~${formatBytes(estDecodedBytes(r))}`
|
|
102
|
-
: "—";
|
|
103
|
-
lines.push(
|
|
104
|
-
`| ${r.name}.${r.type} | ${r.kind} | ${
|
|
105
|
-
r.kind === "image" ? formatDimensions(r) : "—"
|
|
106
|
-
} | ${formatScales(r.scales)} | ${size} | ${r.loaded ? "yes" : "NO"} |`
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
return lines.join("\n");
|
|
110
|
-
}
|