diff --git a/client_v2/src/lib/components/AddSpoolModal.svelte b/client_v2/src/lib/components/AddSpoolModal.svelte index b919eef2..19c473ed 100644 --- a/client_v2/src/lib/components/AddSpoolModal.svelte +++ b/client_v2/src/lib/components/AddSpoolModal.svelte @@ -589,33 +589,36 @@ {#if showAdvanced}
{/if} diff --git a/client_v2/src/lib/components/NumberInput.svelte b/client_v2/src/lib/components/NumberInput.svelte index 4ce97f53..af87af7b 100644 --- a/client_v2/src/lib/components/NumberInput.svelte +++ b/client_v2/src/lib/components/NumberInput.svelte @@ -2,8 +2,14 @@ import * as m from '$lib/paraglide/messages'; // A themed numeric input. Native spinners are hidden app-wide (see app.css); // this component supplies its own up/down steppers styled to match the dark UI. + // + // Two usage modes: + // 1. String/bindable (default): `bind:value` with a string; updates live on input. + // 2. Numeric/commit: pass `onchange` (and a number/string `value`); the value is + // controlled and `onchange` fires with the parsed number on blur/step. Handy for + // callers that keep numbers in an immutable object and commit on change. interface Props { - value: string; + value: string | number; min?: number; max?: number; step?: number; @@ -14,6 +20,17 @@ spaced?: boolean; /** Render with an error outline. */ invalid?: boolean; + /** Optional unit shown inside the input on the right (e.g. "mm", "g"). */ + unit?: string; + /** Compact sizing for dense side panels (smaller font/padding). */ + dense?: boolean; + /** Disable the input and steppers. */ + disabled?: boolean; + /** Commit callback. When set, the component runs in numeric/commit mode. */ + onchange?: (value: number) => void; + /** Commit-mode only: called instead of `onchange` when the field is left empty, + for fields where blank means "unset" (e.g. a price that falls back to a default). */ + onclear?: () => void; } let { value = $bindable(), @@ -23,41 +40,72 @@ placeholder, width = '100%', spaced = false, - invalid = false + invalid = false, + unit, + dense = false, + disabled = false, + onchange, + onclear }: Props = $props(); + // Commit mode keeps a local draft so keystrokes don't fire onchange; the controlled + // `value` re-seeds it whenever the parent changes it (e.g. a new element is selected). + let draft = $state(''); + $effect(() => { + if (onchange) draft = String(value ?? ''); + }); + const shown = $derived(onchange ? draft : String(value ?? '')); + function clamp(n: number): number { if (min != null && n < min) n = min; if (max != null && n > max) n = max; return n; } + function onInput(v: string) { + if (onchange) draft = v; + else value = v; + } + function commit() { + if (!onchange) return; + if (onclear && draft.trim() === '') onclear(); + else onchange(clamp(parseFloat(draft) || 0)); + } function bump(dir: 1 | -1) { - const cur = parseFloat(value); + const cur = parseFloat(shown); const base = Number.isFinite(cur) ? cur : (min ?? 0); let next = clamp(base + dir * step); next = Math.round(next * 1e6) / 1e6; // trim float noise - value = String(next); + if (onchange) { + draft = String(next); + onchange(next); + } else { + value = String(next); + } } -
+
(value = e.currentTarget.value)} + value={shown} + oninput={(e) => onInput(e.currentTarget.value)} + onchange={commit} {min} {max} {step} {placeholder} + {disabled} inputmode="decimal" /> + {#if unit}{unit}{/if}