techfreakworm's picture
feat(web): info icon on every param with hover/click tooltip; add help text to all params
200e3fe unverified
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>
);
}