"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
import type { Operation, Spec, SpecProblem, SpecSound } from "@/lib/spec";
// The spec sheet, edited.
//
// This is the same file song/spec.mjs writes, and the same file the CLI reads --
// which is the point of it existing at all. Every field here is one line in a
// small JSON document a human can open, so "point Metal Slug at the music
// background" is one edit, visible from both sides.
//
// The OPERATIONS list is the payoff and sits at the top: it is what the declared
// structures let you do. An operation that is unavailable says which structure it
// wanted rather than quietly disappearing, because "why is there no trim button
// on this song" is exactly the question the sheet exists to answer.
type Props = {
song: string;
initial: Spec;
problems: SpecProblem[];
operations: Operation[];
plans: string[];
trimSets: { id: string; label: string }[];
};
const field =
"w-full rounded border border-[var(--color-line)] bg-[var(--color-ink)] px-1.5 py-1 text-[12px] text-[var(--color-text)] outline-none focus:border-[var(--color-sel)]";
function Row({ label, children }: { label: string; children: React.ReactNode }) {
return (
);
}
/** "" -> undefined, so an emptied box removes the key rather than storing "". */
const opt = (v: string) => (v.trim() ? v : undefined);
const optNum = (v: string) => {
const n = Number(v);
return v.trim() && Number.isFinite(n) ? n : undefined;
};
export default function SpecSheet({
song,
initial,
problems: initialProblems,
operations: initialOps,
plans,
trimSets,
}: Props) {
const [spec, setSpec] = useState(initial);
const [problems, setProblems] = useState(initialProblems);
const [ops, setOps] = useState(initialOps);
const [dirty, setDirty] = useState(false);
const [saving, setSaving] = useState(false);
const [error, setError] = useState(null);
const [ready, setReady] = useState(false);
useEffect(() => setReady(true), []);
const edit = (fn: (draft: Spec) => void) => {
setSpec((prev) => {
const next = structuredClone(prev);
fn(next);
return next;
});
setDirty(true);
};
const save = async () => {
setSaving(true);
setError(null);
try {
const res = await fetch("/api/browse/spec", {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({ song, spec }),
cache: "no-store",
});
const j = await res.json();
if (!res.ok) throw new Error(j.error || `HTTP ${res.status}`);
setSpec(j.spec);
setProblems(j.problems);
setOps(j.operations);
setDirty(false);
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
} finally {
setSaving(false);
}
};
const sounds: SpecSound[] = spec.sounds ?? [];
return (