feat(catalog): inspect Nitro scales and preview original sprites
CI / check (push) Successful in 1m47s
CI / deploy (push) Successful in 1m30s
CI / e2e (push) Successful in 22s

This commit is contained in:
Simo committed 2026-09-05 17:22:27 +02:00
1 parent 2619bec165
commit 08b0b5021e
6 files changed
+544 -3

No files matched your search

@@ -0,0 +1,147 @@
"use client";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { adminFetch } from "@/lib/admin-fetch";
import type { NitroQuality, ScaleReport } from "@/lib/furni/nitro-quality";
function ScalePanel({
report,
classname,
revision,
}: {
report: ScaleReport;
classname: string;
revision: number;
}) {
const [asset, setAsset] = useState(report.assets[0] ?? ""),
[failed, setFailed] = useState(false);
return (
<section className="min-w-0 space-y-3 rounded-lg border p-3">
<h3 className="font-semibold">
Scale {report.size} ·{" "}
{report.state === "missing"
? "Not included"
: report.state === "issues"
? "Needs review"
: "Present"}
</h3>
<p className="text-xs text-muted-foreground">
{report.assets.length} sprites · {report.animations.length} animation
definitions · directions {report.directions.join(", ") || "—"}
</p>
{report.state === "missing" && (
<p className="text-sm">
This bundle contains no sprites at this scale. Missing scale 32 does
not by itself mean the furniture is unusable.
</p>
)}
{report.assets.length > 0 && (
<>
<label className="block text-sm">
Sprite
<select
className="mt-1 block w-full rounded border bg-background p-2"
value={asset}
onChange={(event) => {
setAsset(event.target.value);
setFailed(false);
}}
>
{report.assets.map((name) => (
<option key={name} value={name}>
{name}
</option>
))}
</select>
</label>
<div className="flex h-56 items-center justify-center overflow-auto rounded border bg-muted/30">
{failed ? (
<p role="alert">This sprite could not be rendered.</p>
) : (
<img
key={`${asset}-${revision}`}
src={`/api/admin/studio/nitro-quality?classname=${encodeURIComponent(classname)}&asset=${encodeURIComponent(asset)}&v=${revision}`}
alt={asset}
style={{
imageRendering: "pixelated",
maxWidth: "none",
flexShrink: 0,
}}
onError={() => setFailed(true)}
/>
)}
</div>
</>
)}
{report.issues.length > 0 && (
<ul className="max-h-40 overflow-auto space-y-2 text-xs">
{report.issues.map((issue) => (
<li key={`${issue.asset ?? ""}-${issue.message}`}>
<strong className="break-all">{issue.asset}</strong>{" "}
{issue.message}
</li>
))}
</ul>
)}
</section>
);
}
export function NitroQualityPanel({ classname }: { classname: string }) {
const [report, setReport] = useState<NitroQuality | null>(null),
[error, setError] = useState(""),
[revision, setRevision] = useState(0);
useEffect(() => {
const controller = new AbortController();
setReport(null);
setError("");
void (async () => {
try {
const response = await adminFetch(
`/api/admin/studio/nitro-quality?classname=${encodeURIComponent(classname)}&v=${revision}`,
{ signal: controller.signal },
);
const data = await response.json();
if (!response.ok) throw Error(data.error || "Inspection failed");
if (!controller.signal.aborted) setReport(data.report);
} catch (error) {
if (!controller.signal.aborted)
setError(
error instanceof Error ? error.message : "Inspection failed",
);
}
})();
return () => controller.abort();
}, [classname, revision]);
return (
<div className="space-y-3">
<div className="flex items-center justify-between gap-3">
<p className="text-sm">
Inspect the saved bundle at both scales. Previews show individual
sprites, not the assembled furniture or animation playback.
</p>
<Button
variant="outline"
onClick={() => setRevision((value) => value + 1)}
>
Recheck
</Button>
</div>
{error ? (
<p role="alert">{error}</p>
) : !report ? (
<p role="status">Checking scales and texture references…</p>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{report.scales.map((scale) => (
<ScalePanel
key={`${scale.size}-${revision}`}
report={scale}
classname={classname}
revision={revision}
/>
))}
</div>
)}
</div>
);
}