From ab38eb73ad8e09f33f140c8f3df5973e93d48dec Mon Sep 17 00:00:00 2001 From: Donkie Date: Fri, 17 Jul 2026 17:48:47 +0200 Subject: [PATCH] client_v2: add unit-box numeric input and apply it across forms & inspectors Give NumberInput a `unit` prop that renders inside the input (left of the steppers), plus `dense` sizing, a `disabled` state, a numeric/commit `onchange` mode, and an `onclear` hook for nullable fields. Fix a min-content overflow so the flex control shrinks inside grid/flex cells. Apply the unit input to: - add-spool form (density, diameter, temps, weights) - label designer, element inspector, print layout panel (mm) - library adjust-weight field, and the filament/vendor/spool inspector detail grids (diameter, density, temps, price, empty-spool-weight) Move the units out of the English label strings into the inputs; locale keys are kept so Weblate translations aren't orphaned. --- .../src/lib/components/AddSpoolModal.svelte | 23 +-- .../src/lib/components/NumberInput.svelte | 130 ++++++++++++++--- .../components/labels/ElementInspector.svelte | 135 ++++++++++-------- .../components/labels/LabelDesigner.svelte | 33 ++--- .../components/labels/PrintLayoutPanel.svelte | 93 ++++++------ .../library/FilamentInspector.svelte | 55 +++++-- .../components/library/SpoolInspector.svelte | 73 +++------- .../components/library/VendorInspector.svelte | 11 +- client_v2/static/locales/en/common.json | 28 ++-- 9 files changed, 338 insertions(+), 243 deletions(-) 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}