@buoy-gg/assets 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.
- package/lib/commonjs/components/AssetRow.js +13 -4
- package/lib/commonjs/components/AssetRow.js.map +1 -1
- package/lib/commonjs/components/AssetsModal.js +2 -1
- package/lib/commonjs/components/AssetsModal.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/components/AssetsModal.js +2 -1
- package/lib/module/components/AssetsModal.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 -370
- 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
package/src/capture/registry.ts
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Runtime access to @react-native/assets-registry.
|
|
3
|
-
*
|
|
4
|
-
* Asset ids are contiguous 1..N (`assets.push` return value), so a plain
|
|
5
|
-
* getAssetByID walk enumerates every asset registered so far — including
|
|
6
|
-
* everything registered BEFORE this tool was evaluated. That's why, unlike
|
|
7
|
-
* the images tool, there is no "must be the first import" requirement here:
|
|
8
|
-
* the walk backfills whatever the patch missed.
|
|
9
|
-
*
|
|
10
|
-
* registerAsset is patched (plain non-frozen CommonJS export, looked up at
|
|
11
|
-
* call time by Metro's generated asset modules) purely for LIVE updates as
|
|
12
|
-
* lazily-required assets register later.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { upsertRecord, assetKey } from "../store/assetStore";
|
|
16
|
-
import type { AssetRecord } from "../types";
|
|
17
|
-
|
|
18
|
-
/** Runtime shape of a registry entry (fileSystemLocation/files are stripped by Metro). */
|
|
19
|
-
export interface PackagerAsset {
|
|
20
|
-
__packager_asset: boolean;
|
|
21
|
-
httpServerLocation: string;
|
|
22
|
-
width?: number | null;
|
|
23
|
-
height?: number | null;
|
|
24
|
-
scales: number[];
|
|
25
|
-
hash: string;
|
|
26
|
-
name: string;
|
|
27
|
-
type: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface RegistryModule {
|
|
31
|
-
registerAsset: (asset: PackagerAsset) => number;
|
|
32
|
-
getAssetByID: (assetId: number) => PackagerAsset | undefined;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
let cachedRegistry: RegistryModule | null | undefined;
|
|
36
|
-
|
|
37
|
-
export function getRegistryModule(): RegistryModule | null {
|
|
38
|
-
if (cachedRegistry !== undefined) return cachedRegistry;
|
|
39
|
-
let mod: RegistryModule | null = null;
|
|
40
|
-
try {
|
|
41
|
-
mod = require("@react-native/assets-registry/registry");
|
|
42
|
-
} catch {
|
|
43
|
-
// Old RN layouts (< 0.72) don't ship @react-native/assets-registry as a
|
|
44
|
-
// resolvable package — the registry only exists at this deep path. RN's
|
|
45
|
-
// deep-import deprecation warning fires when this executes, but it only
|
|
46
|
-
// executes on those old layouts, where there is no other way in.
|
|
47
|
-
try {
|
|
48
|
-
mod = require("react-native/Libraries/Image/AssetRegistry");
|
|
49
|
-
} catch {
|
|
50
|
-
mod = null;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
cachedRegistry =
|
|
54
|
-
mod && typeof mod.getAssetByID === "function" ? mod : null;
|
|
55
|
-
return cachedRegistry;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** resolveAssetSource(id) → this environment's URI (dev URL / file:// / res id). */
|
|
59
|
-
export function resolveRegistryAsset(
|
|
60
|
-
registryId: number
|
|
61
|
-
): { uri: string; scale: number } | null {
|
|
62
|
-
try {
|
|
63
|
-
const { Image } = require("react-native");
|
|
64
|
-
const resolved = Image.resolveAssetSource(registryId);
|
|
65
|
-
if (resolved && typeof resolved.uri === "string") {
|
|
66
|
-
return { uri: resolved.uri, scale: resolved.scale ?? 1 };
|
|
67
|
-
}
|
|
68
|
-
} catch {
|
|
69
|
-
// resolution is enrichment — never let it break the walk
|
|
70
|
-
}
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function upsertFromPackagerAsset(
|
|
75
|
-
registryId: number,
|
|
76
|
-
asset: PackagerAsset
|
|
77
|
-
): AssetRecord {
|
|
78
|
-
const resolved = resolveRegistryAsset(registryId);
|
|
79
|
-
return upsertRecord({
|
|
80
|
-
key: assetKey(asset.httpServerLocation, asset.name, asset.type),
|
|
81
|
-
name: asset.name,
|
|
82
|
-
type: asset.type,
|
|
83
|
-
origin: "registry",
|
|
84
|
-
registryId,
|
|
85
|
-
hash: asset.hash,
|
|
86
|
-
httpServerLocation: asset.httpServerLocation,
|
|
87
|
-
width: asset.width ?? undefined,
|
|
88
|
-
height: asset.height ?? undefined,
|
|
89
|
-
scales: asset.scales,
|
|
90
|
-
resolvedUri: resolved?.uri,
|
|
91
|
-
resolvedScale: resolved?.scale,
|
|
92
|
-
loaded: true,
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** Enumerate every asset registered so far. Returns the highest id seen. */
|
|
97
|
-
export function walkRegistry(): number {
|
|
98
|
-
const registry = getRegistryModule();
|
|
99
|
-
if (!registry) return 0;
|
|
100
|
-
let id = 0;
|
|
101
|
-
for (;;) {
|
|
102
|
-
const asset = registry.getAssetByID(id + 1);
|
|
103
|
-
if (!asset) break;
|
|
104
|
-
id++;
|
|
105
|
-
upsertFromPackagerAsset(id, asset);
|
|
106
|
-
}
|
|
107
|
-
return id;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const PATCH_MARKER = "__buoyAssetsPatched";
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Wrap registerAsset so future registrations stream in live. Safe to call
|
|
114
|
-
* more than once (marker guard); the walk covers anything registered before.
|
|
115
|
-
*/
|
|
116
|
-
export function patchRegisterAsset(): boolean {
|
|
117
|
-
const registry = getRegistryModule();
|
|
118
|
-
if (!registry) return false;
|
|
119
|
-
const original = registry.registerAsset;
|
|
120
|
-
if ((original as unknown as Record<string, unknown>)[PATCH_MARKER]) return true;
|
|
121
|
-
const wrapped = (asset: PackagerAsset): number => {
|
|
122
|
-
const id = original.call(registry, asset);
|
|
123
|
-
try {
|
|
124
|
-
upsertFromPackagerAsset(id, asset);
|
|
125
|
-
} catch {
|
|
126
|
-
// never break app asset registration
|
|
127
|
-
}
|
|
128
|
-
return id;
|
|
129
|
-
};
|
|
130
|
-
(wrapped as unknown as Record<string, unknown>)[PATCH_MARKER] = true;
|
|
131
|
-
try {
|
|
132
|
-
registry.registerAsset = wrapped;
|
|
133
|
-
} catch {
|
|
134
|
-
return false;
|
|
135
|
-
}
|
|
136
|
-
return true;
|
|
137
|
-
}
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AssetDetail — full metadata for one asset: preview, identity, dimensions,
|
|
3
|
-
* per-scale sizes, resolution info, and which audit findings apply.
|
|
4
|
-
*
|
|
5
|
-
* Image previews render through the numeric registry id (works in every
|
|
6
|
-
* environment); fonts render a pangram with fontFamily set to the asset name
|
|
7
|
-
* — a best-effort guess (family ≠ filename for many fonts) that falls back
|
|
8
|
-
* to the system font silently when it misses.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { Image, ScrollView, StyleSheet, Text, View } from "react-native";
|
|
12
|
-
import { night } from "@buoy-gg/shared-ui";
|
|
13
|
-
import { displaySource } from "../capture/measure";
|
|
14
|
-
import { formatBytes, formatDimensions, formatScales } from "../format";
|
|
15
|
-
import { estDecodedBytes } from "../store/assetStore";
|
|
16
|
-
import type { AssetRecord, AssetsInsights } from "../types";
|
|
17
|
-
|
|
18
|
-
interface AssetDetailProps {
|
|
19
|
-
record: AssetRecord;
|
|
20
|
-
insights: AssetsInsights;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function Row({ label, value }: { label: string; value: string }) {
|
|
24
|
-
return (
|
|
25
|
-
<View style={styles.kvRow}>
|
|
26
|
-
<Text style={styles.kvLabel}>{label}</Text>
|
|
27
|
-
<Text style={styles.kvValue} selectable>
|
|
28
|
-
{value}
|
|
29
|
-
</Text>
|
|
30
|
-
</View>
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function Preview({ record }: { record: AssetRecord }) {
|
|
35
|
-
if (record.kind === "image") {
|
|
36
|
-
const source = displaySource(record);
|
|
37
|
-
if (source == null) return null;
|
|
38
|
-
return (
|
|
39
|
-
<View style={styles.previewBox}>
|
|
40
|
-
<Image
|
|
41
|
-
testID="buoy-internal-assets-preview"
|
|
42
|
-
source={source}
|
|
43
|
-
style={styles.previewImage}
|
|
44
|
-
resizeMode="contain"
|
|
45
|
-
/>
|
|
46
|
-
</View>
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
if (record.kind === "font") {
|
|
50
|
-
return (
|
|
51
|
-
<View style={styles.previewBox}>
|
|
52
|
-
<Text style={[styles.fontPreview, { fontFamily: record.name }]}>
|
|
53
|
-
The quick brown fox jumps over the lazy dog 0123456789
|
|
54
|
-
</Text>
|
|
55
|
-
</View>
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function AssetDetail({ record, insights }: AssetDetailProps) {
|
|
62
|
-
const findings: string[] = [];
|
|
63
|
-
if (insights.neverLoaded.includes(record.id)) {
|
|
64
|
-
findings.push("Never loaded — shipped in the bundle but no code required it");
|
|
65
|
-
}
|
|
66
|
-
if (insights.scaleGaps.includes(record.id)) {
|
|
67
|
-
findings.push("Scale gap — no variant covers this device's pixel ratio");
|
|
68
|
-
}
|
|
69
|
-
if (insights.webpCandidates.includes(record.id)) {
|
|
70
|
-
findings.push("WebP candidate — large png/jpeg, converting typically saves 20–40%");
|
|
71
|
-
}
|
|
72
|
-
const duplicate = insights.duplicates.find((d) => d.ids.includes(record.id));
|
|
73
|
-
if (duplicate) {
|
|
74
|
-
findings.push(
|
|
75
|
-
`Duplicate content — identical bytes shipped ${duplicate.ids.length} times`
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
if (insights.overDecode.includes(record.id)) {
|
|
79
|
-
findings.push(
|
|
80
|
-
"Decodes larger than 2× this entire screen — resize the source, no view can need this many pixels"
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
if (insights.heavyGifs.includes(record.id)) {
|
|
84
|
-
findings.push(
|
|
85
|
-
"Heavy GIF — every frame decodes into memory; a video or Lottie is typically ~90% smaller"
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
if (insights.heavyFonts.includes(record.id)) {
|
|
89
|
-
findings.push(
|
|
90
|
-
"Heavy font — likely an unsubset icon font; subsetting to used glyphs usually saves most of it"
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
if (insights.scaleAnomalies.includes(record.id)) {
|
|
94
|
-
findings.push(
|
|
95
|
-
"Scale anomaly — a higher-resolution variant is smaller in bytes than a lower one; check the exports"
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const estimate = estDecodedBytes(record);
|
|
100
|
-
|
|
101
|
-
return (
|
|
102
|
-
<ScrollView style={styles.scroll} contentContainerStyle={styles.content}>
|
|
103
|
-
<Preview record={record} />
|
|
104
|
-
|
|
105
|
-
{findings.length > 0 ? (
|
|
106
|
-
<View style={styles.findings}>
|
|
107
|
-
{findings.map((finding) => (
|
|
108
|
-
<Text key={finding} style={styles.findingText}>
|
|
109
|
-
{finding}
|
|
110
|
-
</Text>
|
|
111
|
-
))}
|
|
112
|
-
</View>
|
|
113
|
-
) : null}
|
|
114
|
-
|
|
115
|
-
<Row label="Name" value={`${record.name}.${record.type}`} />
|
|
116
|
-
<Row label="Kind" value={record.kind} />
|
|
117
|
-
<Row
|
|
118
|
-
label="Status"
|
|
119
|
-
value={record.loaded ? "Loaded (registered at runtime)" : "In bundle, never loaded"}
|
|
120
|
-
/>
|
|
121
|
-
{record.kind === "image" ? (
|
|
122
|
-
<Row label="Dimensions" value={`${formatDimensions(record)} (logical)`} />
|
|
123
|
-
) : null}
|
|
124
|
-
{record.kind === "image" && record.scales.length ? (
|
|
125
|
-
<Row label="Scales" value={formatScales(record.scales)} />
|
|
126
|
-
) : null}
|
|
127
|
-
|
|
128
|
-
{record.sizesByScale
|
|
129
|
-
? Object.entries(record.sizesByScale).map(([scale, bytes]) => (
|
|
130
|
-
<Row key={scale} label={`Size @${scale}x`} value={formatBytes(bytes)} />
|
|
131
|
-
))
|
|
132
|
-
: null}
|
|
133
|
-
{record.sizeBytes != null && record.sizeSource === "measured" ? (
|
|
134
|
-
<Row label="Total size" value={formatBytes(record.sizeBytes)} />
|
|
135
|
-
) : null}
|
|
136
|
-
{estimate > 0 ? (
|
|
137
|
-
<Row label="Decoded memory" value={`~${formatBytes(estimate)} (RGBA)`} />
|
|
138
|
-
) : null}
|
|
139
|
-
|
|
140
|
-
{record.registryId != null ? (
|
|
141
|
-
<Row label="Registry id" value={String(record.registryId)} />
|
|
142
|
-
) : null}
|
|
143
|
-
{record.hash ? <Row label="Hash" value={record.hash} /> : null}
|
|
144
|
-
{record.httpServerLocation ? (
|
|
145
|
-
<Row label="Location" value={record.httpServerLocation} />
|
|
146
|
-
) : null}
|
|
147
|
-
{record.fileSystemLocation ? (
|
|
148
|
-
<Row label="Source dir" value={record.fileSystemLocation} />
|
|
149
|
-
) : null}
|
|
150
|
-
{record.resolvedUri ? <Row label="Resolved URI" value={record.resolvedUri} /> : null}
|
|
151
|
-
</ScrollView>
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const styles = StyleSheet.create({
|
|
156
|
-
scroll: {
|
|
157
|
-
flex: 1,
|
|
158
|
-
},
|
|
159
|
-
content: {
|
|
160
|
-
padding: 12,
|
|
161
|
-
gap: 8,
|
|
162
|
-
},
|
|
163
|
-
previewBox: {
|
|
164
|
-
borderRadius: 8,
|
|
165
|
-
backgroundColor: night.color.bg,
|
|
166
|
-
padding: 12,
|
|
167
|
-
alignItems: "center",
|
|
168
|
-
justifyContent: "center",
|
|
169
|
-
minHeight: 96,
|
|
170
|
-
},
|
|
171
|
-
previewImage: {
|
|
172
|
-
width: "100%",
|
|
173
|
-
height: 140,
|
|
174
|
-
},
|
|
175
|
-
fontPreview: {
|
|
176
|
-
color: night.color.text,
|
|
177
|
-
fontSize: 16,
|
|
178
|
-
textAlign: "center",
|
|
179
|
-
},
|
|
180
|
-
findings: {
|
|
181
|
-
borderRadius: 8,
|
|
182
|
-
borderWidth: 1,
|
|
183
|
-
borderColor: night.color.border,
|
|
184
|
-
backgroundColor: night.color.surface,
|
|
185
|
-
padding: 10,
|
|
186
|
-
gap: 6,
|
|
187
|
-
},
|
|
188
|
-
findingText: {
|
|
189
|
-
color: night.color.textSecondary,
|
|
190
|
-
fontSize: 11,
|
|
191
|
-
lineHeight: 15,
|
|
192
|
-
},
|
|
193
|
-
kvRow: {
|
|
194
|
-
gap: 2,
|
|
195
|
-
},
|
|
196
|
-
kvLabel: {
|
|
197
|
-
color: night.color.textTertiary,
|
|
198
|
-
fontSize: 9,
|
|
199
|
-
fontWeight: "700",
|
|
200
|
-
letterSpacing: 0.5,
|
|
201
|
-
textTransform: "uppercase",
|
|
202
|
-
},
|
|
203
|
-
kvValue: {
|
|
204
|
-
color: night.color.text,
|
|
205
|
-
fontSize: 12,
|
|
206
|
-
},
|
|
207
|
-
});
|
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AssetRow — one bundled asset as a family-style card (matching the network
|
|
3
|
-
* tool's rows): rounded card, colored accent edge per kind, thumbnail, name,
|
|
4
|
-
* quiet metadata, bold right-aligned size, and finding badges ONLY when a
|
|
5
|
-
* finding exists. A thin proportional bar along the card bottom makes the
|
|
6
|
-
* heavy assets feel heavy at a glance.
|
|
7
|
-
*
|
|
8
|
-
* Thumbnails render through the NUMERIC registry id when available — that
|
|
9
|
-
* routes through resolveAssetSource natively, so it works in dev, iOS
|
|
10
|
-
* release (file://) and Android release (bare resource id) alike. They carry
|
|
11
|
-
* the buoy-internal testID so the images tool never records our own UI.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { Image, StyleSheet, Text, TouchableOpacity, View } from "react-native";
|
|
15
|
-
import { macOSColors, night } from "@buoy-gg/shared-ui";
|
|
16
|
-
import { displaySource } from "../capture/measure";
|
|
17
|
-
import {
|
|
18
|
-
formatDimensions,
|
|
19
|
-
formatScales,
|
|
20
|
-
formatSize,
|
|
21
|
-
KIND_COLORS,
|
|
22
|
-
KIND_LABELS,
|
|
23
|
-
} from "../format";
|
|
24
|
-
import { estDecodedBytes } from "../store/assetStore";
|
|
25
|
-
import type { AssetRecord } from "../types";
|
|
26
|
-
|
|
27
|
-
/** Which audit findings apply to this record (drives the row badges). */
|
|
28
|
-
export interface AssetRowFlags {
|
|
29
|
-
scaleGap: boolean;
|
|
30
|
-
scaleAnomaly: boolean;
|
|
31
|
-
duplicate: boolean;
|
|
32
|
-
webp: boolean;
|
|
33
|
-
oversize: boolean;
|
|
34
|
-
heavy: boolean;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface AssetRowProps {
|
|
38
|
-
record: AssetRecord;
|
|
39
|
-
flags: AssetRowFlags;
|
|
40
|
-
/** This record's size relative to the largest in the list (0–1). */
|
|
41
|
-
sizeFraction: number;
|
|
42
|
-
onPress: (id: number) => void;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function Badge({ label, color }: { label: string; color?: string }) {
|
|
46
|
-
// Quiet chip: legibility from light-gray text on a neutral fill. Only the
|
|
47
|
-
// serious tiers (red) and WEBP (blue) carry a small dot; the rest are plain.
|
|
48
|
-
return (
|
|
49
|
-
<View style={styles.badgePill}>
|
|
50
|
-
{color ? <View style={[styles.badgeDot, { backgroundColor: color }]} /> : null}
|
|
51
|
-
<Text style={styles.badgePillText}>{label}</Text>
|
|
52
|
-
</View>
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function AssetRow({ record, flags, sizeFraction, onPress }: AssetRowProps) {
|
|
57
|
-
const accent = KIND_COLORS[record.kind];
|
|
58
|
-
const thumbSource = record.kind === "image" ? displaySource(record) : null;
|
|
59
|
-
|
|
60
|
-
const badges: Array<{ label: string; color?: string }> = [];
|
|
61
|
-
if (!record.loaded) badges.push({ label: "UNUSED" });
|
|
62
|
-
if (flags.oversize) badges.push({ label: "OVERSIZE", color: macOSColors.semantic.error });
|
|
63
|
-
if (flags.duplicate) badges.push({ label: "DUP" });
|
|
64
|
-
if (flags.heavy) badges.push({ label: "HEAVY" });
|
|
65
|
-
if (flags.scaleAnomaly) badges.push({ label: "BAD SCALE", color: macOSColors.semantic.error });
|
|
66
|
-
else if (flags.scaleGap) badges.push({ label: "LOW-RES" });
|
|
67
|
-
if (flags.webp) badges.push({ label: "WEBP", color: macOSColors.semantic.info });
|
|
68
|
-
|
|
69
|
-
return (
|
|
70
|
-
<TouchableOpacity
|
|
71
|
-
style={styles.card}
|
|
72
|
-
testID={`buoy-asset-row-${record.id}`}
|
|
73
|
-
onPress={() => onPress(record.id)}
|
|
74
|
-
activeOpacity={0.7}
|
|
75
|
-
>
|
|
76
|
-
<View style={styles.body}>
|
|
77
|
-
<View style={styles.thumbBox}>
|
|
78
|
-
{thumbSource != null ? (
|
|
79
|
-
<Image
|
|
80
|
-
testID="buoy-internal-assets-thumb"
|
|
81
|
-
source={thumbSource}
|
|
82
|
-
style={styles.thumb}
|
|
83
|
-
resizeMode="contain"
|
|
84
|
-
/>
|
|
85
|
-
) : (
|
|
86
|
-
<Text style={[styles.thumbFallback, { color: accent }]}>
|
|
87
|
-
{KIND_LABELS[record.kind]}
|
|
88
|
-
</Text>
|
|
89
|
-
)}
|
|
90
|
-
</View>
|
|
91
|
-
|
|
92
|
-
<View style={styles.main}>
|
|
93
|
-
<Text style={styles.name} numberOfLines={1}>
|
|
94
|
-
{record.name}
|
|
95
|
-
<Text style={styles.ext}>.{record.type}</Text>
|
|
96
|
-
</Text>
|
|
97
|
-
<View style={styles.metaRow}>
|
|
98
|
-
{record.kind === "image" && record.width != null ? (
|
|
99
|
-
<Text style={styles.meta}>{formatDimensions(record)}</Text>
|
|
100
|
-
) : null}
|
|
101
|
-
{/* Scale variants only mean something for images. */}
|
|
102
|
-
{record.kind === "image" && record.scales.length ? (
|
|
103
|
-
<Text style={styles.meta}>{formatScales(record.scales)}</Text>
|
|
104
|
-
) : null}
|
|
105
|
-
</View>
|
|
106
|
-
</View>
|
|
107
|
-
|
|
108
|
-
<View style={styles.right}>
|
|
109
|
-
<Text style={styles.size}>
|
|
110
|
-
{formatSize(record, estDecodedBytes(record))}
|
|
111
|
-
</Text>
|
|
112
|
-
{badges.length ? (
|
|
113
|
-
<View style={styles.badgeRow}>
|
|
114
|
-
{badges.slice(0, 3).map((b) => (
|
|
115
|
-
<Badge key={b.label} label={b.label} color={b.color} />
|
|
116
|
-
))}
|
|
117
|
-
</View>
|
|
118
|
-
) : null}
|
|
119
|
-
</View>
|
|
120
|
-
</View>
|
|
121
|
-
|
|
122
|
-
{sizeFraction > 0 ? (
|
|
123
|
-
<View style={styles.sizeTrack}>
|
|
124
|
-
<View
|
|
125
|
-
style={[
|
|
126
|
-
styles.sizeFill,
|
|
127
|
-
{
|
|
128
|
-
backgroundColor: accent,
|
|
129
|
-
width: `${Math.max(2, Math.round(sizeFraction * 100))}%`,
|
|
130
|
-
},
|
|
131
|
-
]}
|
|
132
|
-
/>
|
|
133
|
-
</View>
|
|
134
|
-
) : null}
|
|
135
|
-
</TouchableOpacity>
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const styles = StyleSheet.create({
|
|
140
|
-
card: {
|
|
141
|
-
marginHorizontal: 10,
|
|
142
|
-
marginTop: 6,
|
|
143
|
-
borderRadius: 10,
|
|
144
|
-
backgroundColor: night.color.surface,
|
|
145
|
-
borderWidth: 1,
|
|
146
|
-
borderColor: night.color.border,
|
|
147
|
-
overflow: "hidden",
|
|
148
|
-
},
|
|
149
|
-
body: {
|
|
150
|
-
flexDirection: "row",
|
|
151
|
-
alignItems: "center",
|
|
152
|
-
paddingVertical: 8,
|
|
153
|
-
paddingHorizontal: 12,
|
|
154
|
-
gap: 10,
|
|
155
|
-
},
|
|
156
|
-
thumbBox: {
|
|
157
|
-
width: 40,
|
|
158
|
-
height: 40,
|
|
159
|
-
borderRadius: 8,
|
|
160
|
-
// Light plate so dark glyph assets stay visible on the dark card.
|
|
161
|
-
backgroundColor: "rgba(255,255,255,0.10)",
|
|
162
|
-
borderWidth: 1,
|
|
163
|
-
borderColor: night.color.border,
|
|
164
|
-
alignItems: "center",
|
|
165
|
-
justifyContent: "center",
|
|
166
|
-
overflow: "hidden",
|
|
167
|
-
},
|
|
168
|
-
thumb: {
|
|
169
|
-
width: 34,
|
|
170
|
-
height: 34,
|
|
171
|
-
},
|
|
172
|
-
thumbFallback: {
|
|
173
|
-
fontSize: 9,
|
|
174
|
-
fontWeight: "700",
|
|
175
|
-
letterSpacing: 0.5,
|
|
176
|
-
},
|
|
177
|
-
main: {
|
|
178
|
-
flex: 1,
|
|
179
|
-
gap: 3,
|
|
180
|
-
},
|
|
181
|
-
name: {
|
|
182
|
-
color: night.color.text,
|
|
183
|
-
fontSize: 12,
|
|
184
|
-
fontWeight: "600",
|
|
185
|
-
},
|
|
186
|
-
ext: {
|
|
187
|
-
color: night.color.textTertiary,
|
|
188
|
-
fontWeight: "400",
|
|
189
|
-
},
|
|
190
|
-
metaRow: {
|
|
191
|
-
flexDirection: "row",
|
|
192
|
-
alignItems: "center",
|
|
193
|
-
gap: 8,
|
|
194
|
-
},
|
|
195
|
-
meta: {
|
|
196
|
-
color: night.color.textSecondary,
|
|
197
|
-
fontSize: 10,
|
|
198
|
-
fontVariant: ["tabular-nums"],
|
|
199
|
-
},
|
|
200
|
-
right: {
|
|
201
|
-
alignItems: "flex-end",
|
|
202
|
-
gap: 4,
|
|
203
|
-
},
|
|
204
|
-
size: {
|
|
205
|
-
color: night.color.text,
|
|
206
|
-
fontSize: 12,
|
|
207
|
-
fontWeight: "700",
|
|
208
|
-
fontVariant: ["tabular-nums"],
|
|
209
|
-
},
|
|
210
|
-
badgeRow: {
|
|
211
|
-
flexDirection: "row",
|
|
212
|
-
gap: 4,
|
|
213
|
-
},
|
|
214
|
-
badgePill: {
|
|
215
|
-
flexDirection: "row",
|
|
216
|
-
alignItems: "center",
|
|
217
|
-
gap: 4,
|
|
218
|
-
backgroundColor: "rgba(255,255,255,0.06)",
|
|
219
|
-
borderRadius: 4,
|
|
220
|
-
paddingHorizontal: 6,
|
|
221
|
-
paddingVertical: 2.5,
|
|
222
|
-
},
|
|
223
|
-
badgeDot: {
|
|
224
|
-
width: 5,
|
|
225
|
-
height: 5,
|
|
226
|
-
borderRadius: 2.5,
|
|
227
|
-
},
|
|
228
|
-
badgePillText: {
|
|
229
|
-
color: night.color.textSecondary,
|
|
230
|
-
fontSize: 9,
|
|
231
|
-
fontWeight: "600",
|
|
232
|
-
letterSpacing: 0.3,
|
|
233
|
-
},
|
|
234
|
-
sizeTrack: {
|
|
235
|
-
height: 2,
|
|
236
|
-
backgroundColor: "transparent",
|
|
237
|
-
},
|
|
238
|
-
sizeFill: {
|
|
239
|
-
height: 2,
|
|
240
|
-
opacity: 0.45,
|
|
241
|
-
borderTopRightRadius: 2,
|
|
242
|
-
},
|
|
243
|
-
});
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AssetsFilterBar — kind filter pills with counts, styled like the network
|
|
3
|
-
* tool's bordered pills: outlined, kind-accent-colored when active. Wrapping
|
|
4
|
-
* row, not a horizontal ScrollView (same height-measurement pitfall the
|
|
5
|
-
* images tool documents).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
|
|
9
|
-
import { night } from "@buoy-gg/shared-ui";
|
|
10
|
-
import { KIND_COLORS, KIND_LABELS } from "../format";
|
|
11
|
-
import type { AssetKind, AssetsStats } from "../types";
|
|
12
|
-
|
|
13
|
-
export type AssetFilter = "all" | "unused" | AssetKind;
|
|
14
|
-
|
|
15
|
-
interface AssetsFilterBarProps {
|
|
16
|
-
stats: AssetsStats;
|
|
17
|
-
filter: AssetFilter;
|
|
18
|
-
onChange: (filter: AssetFilter) => void;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function AssetsFilterBar({ stats, filter, onChange }: AssetsFilterBarProps) {
|
|
22
|
-
const chips: Array<{ key: AssetFilter; label: string; accent: string }> = [
|
|
23
|
-
{ key: "all", label: `ALL ${stats.total}`, accent: night.color.textSecondary },
|
|
24
|
-
];
|
|
25
|
-
for (const kind of Object.keys(stats.byKind) as AssetKind[]) {
|
|
26
|
-
const entry = stats.byKind[kind];
|
|
27
|
-
if (entry.count > 0) {
|
|
28
|
-
chips.push({
|
|
29
|
-
key: kind,
|
|
30
|
-
label: `${KIND_LABELS[kind]} ${entry.count}`,
|
|
31
|
-
accent: KIND_COLORS[kind],
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
if (stats.neverLoaded > 0) {
|
|
36
|
-
// Amber, not semantic.warning — the bright yellow reads as an alarm.
|
|
37
|
-
chips.push({
|
|
38
|
-
key: "unused",
|
|
39
|
-
label: `UNUSED ${stats.neverLoaded}`,
|
|
40
|
-
accent: "#FF9F1C",
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
if (chips.length <= 1) return null;
|
|
44
|
-
|
|
45
|
-
return (
|
|
46
|
-
<View style={styles.bar}>
|
|
47
|
-
{chips.map((chip) => {
|
|
48
|
-
const active = chip.key === filter;
|
|
49
|
-
return (
|
|
50
|
-
<TouchableOpacity
|
|
51
|
-
key={chip.key}
|
|
52
|
-
style={[
|
|
53
|
-
styles.chip,
|
|
54
|
-
active && {
|
|
55
|
-
borderColor: chip.accent,
|
|
56
|
-
backgroundColor: `${chip.accent}1A`,
|
|
57
|
-
},
|
|
58
|
-
]}
|
|
59
|
-
// Tapping the active chip again untoggles back to ALL.
|
|
60
|
-
onPress={() => onChange(active ? "all" : chip.key)}
|
|
61
|
-
>
|
|
62
|
-
<Text
|
|
63
|
-
style={[
|
|
64
|
-
styles.chipText,
|
|
65
|
-
{ color: active ? chip.accent : night.color.textSecondary },
|
|
66
|
-
]}
|
|
67
|
-
>
|
|
68
|
-
{chip.label}
|
|
69
|
-
</Text>
|
|
70
|
-
</TouchableOpacity>
|
|
71
|
-
);
|
|
72
|
-
})}
|
|
73
|
-
</View>
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const styles = StyleSheet.create({
|
|
78
|
-
bar: {
|
|
79
|
-
flexDirection: "row",
|
|
80
|
-
flexWrap: "wrap",
|
|
81
|
-
gap: 6,
|
|
82
|
-
paddingHorizontal: 10,
|
|
83
|
-
paddingVertical: 8,
|
|
84
|
-
},
|
|
85
|
-
chip: {
|
|
86
|
-
borderWidth: 1,
|
|
87
|
-
borderColor: night.color.border,
|
|
88
|
-
backgroundColor: night.color.surface,
|
|
89
|
-
borderRadius: 999,
|
|
90
|
-
paddingHorizontal: 10,
|
|
91
|
-
paddingVertical: 5,
|
|
92
|
-
},
|
|
93
|
-
chipText: {
|
|
94
|
-
fontSize: 10,
|
|
95
|
-
fontWeight: "700",
|
|
96
|
-
letterSpacing: 0.4,
|
|
97
|
-
lineHeight: 13,
|
|
98
|
-
},
|
|
99
|
-
});
|