File size: 4,406 Bytes
b2d4d0e 200e3fe b2d4d0e 200e3fe 73c8079 97db435 200e3fe 14c6f28 97db435 14c6f28 97db435 73c8079 200e3fe 73c8079 200e3fe 73c8079 200e3fe 73c8079 200e3fe 73c8079 b2d4d0e 73c8079 b2d4d0e 65949d0 73c8079 65949d0 73c8079 b2d4d0e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | import type { ParamSpec } from "@/lib/api";
import InfoTip from "@/components/InfoTip";
type Props = {
specs: ParamSpec[];
values: Record<string, unknown>;
onChange: (next: Record<string, unknown>) => void;
};
function ParamLabel({ id, label, help }: { id: string; label: string; help?: string }) {
return (
<span className="inline-flex items-center gap-1.5">
<label htmlFor={id} className="label-mono">{label}</label>
{help && <InfoTip text={help} />}
</span>
);
}
function renderControl(
s: ParamSpec,
values: Record<string, unknown>,
set: (name: string, v: unknown) => void,
) {
const id = `param-${s.name}`;
const current: unknown = values[s.name] ?? s.default;
if (s.name === "seed") {
const v = (values[s.name] ?? s.default) as number;
return (
<div key={s.name} className="space-y-1.5">
<ParamLabel id={id} label={s.label} help={s.help} />
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
<input
id={id}
aria-label={s.label}
type="number"
min={s.min}
step={s.step ?? 1}
value={v}
onChange={(e) => set(s.name, Number(e.target.value))}
className="field-input !w-40 sm:!w-44 font-mono text-[12px] py-1"
/>
<button
type="button"
onClick={() => set(s.name, -1)}
className="label-mono hover:text-foreground transition-colors"
>
↻ random
</button>
{v === -1 && (
<span className="label-mono text-muted-foreground">(random per generate)</span>
)}
</div>
</div>
);
}
if (s.type === "float" || s.type === "int") {
const n = typeof current === "number" ? current : Number(current);
return (
<div key={s.name} className="space-y-1.5">
<div className="flex items-baseline justify-between">
<ParamLabel id={id} label={s.label} help={s.help} />
<span className="font-mono text-[12px] text-foreground tracking-wider">
{Number.isFinite(n) ? n.toFixed(2) : String(current)}
</span>
</div>
<input
id={id}
aria-label={s.label}
type="range"
min={s.min}
max={s.max}
step={s.step ?? 0.01}
value={Number.isFinite(n) ? n : 0}
onChange={(e) => set(s.name, Number(e.target.value))}
className="w-full accent-[hsl(var(--ember))]"
/>
</div>
);
}
if (s.type === "bool") {
return (
<div key={s.name} className="flex items-center justify-between">
<ParamLabel id={id} label={s.label} help={s.help} />
<input
id={id}
aria-label={s.label}
type="checkbox"
checked={!!current}
onChange={(e) => set(s.name, e.target.checked)}
className="accent-[hsl(var(--ember))]"
/>
</div>
);
}
return (
<div key={s.name} className="space-y-1.5">
<ParamLabel id={id} label={s.label} help={s.help} />
<select
id={id}
aria-label={s.label}
value={String(current)}
onChange={(e) => set(s.name, e.target.value)}
className="field-input font-mono text-[12px]"
>
{(s.choices ?? []).map((c) => (
<option key={c} value={c}>{c}</option>
))}
</select>
</div>
);
}
export default function ParamsPanel({ specs, values, onChange }: Props) {
function set(name: string, v: unknown) {
onChange({ ...values, [name]: v });
}
const basic = specs.filter((s) => (s.group ?? "basic") === "basic");
const advanced = specs.filter((s) => s.group === "advanced");
return (
<div className="space-y-5">
{basic.map((s) => renderControl(s, values, set))}
{advanced.length > 0 && (
<details className="card-paper p-3 [&_summary::-webkit-details-marker]:hidden">
<summary className="label-mono cursor-pointer select-none flex items-center gap-2">
<span className="inline-block transition-transform [details[open]>summary>&]:rotate-90">▸</span>
advanced · {advanced.length} params
</summary>
<div className="mt-4 space-y-5">
{advanced.map((s) => renderControl(s, values, set))}
</div>
</details>
)}
</div>
);
}
|