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.
This commit is contained in:
Donkie committed 2026-07-17 17:48:47 +02:00
1 parent 287d3010ac
commit ab38eb73ad
9 files changed
+338 -243

No files matched your search

@@ -589,33 +589,36 @@
{#if showAdvanced}
<div class="form">
<label
>{m['filament.fields.density']()} <span class="u">g/cm³</span> <span class="req">*</span>
>{m['filament.fields.density']()} <span class="req">*</span>
<NumberInput
bind:value={nf.density}
min={0}
step={0.01}
unit="g/cm³"
spaced
invalid={!!errors.density}
/>
{#if errors.density}<span class="err">{errors.density}</span>{/if}
</label>
<label
>{m['filament.fields.diameter']()} <span class="u">mm</span> <span class="req">*</span>
>{m['filament.fields.diameter']()} <span class="req">*</span>
<NumberInput
bind:value={nf.diameter}
min={0}
step={0.05}
unit="mm"
spaced
invalid={!!errors.diameter}
/>
{#if errors.diameter}<span class="err">{errors.diameter}</span>{/if}
</label>
<label
>{m['filament.fields.settingsBedTemp']()} <span class="u">°C</span>
>{m['filament.fields.settingsBedTemp']()}
<NumberInput
bind:value={nf.nozzleTemp}
min={0}
step={5}
unit="°C"
placeholder="—"
spaced
invalid={!!errors.nozzleTemp}
@@ -623,11 +626,12 @@
{#if errors.nozzleTemp}<span class="err">{errors.nozzleTemp}</span>{/if}
</label>
<label
>{m['filament.fields.settingsExtruderTemp']()} <span class="u">°C</span>
>{m['filament.fields.settingsExtruderTemp']()}
<NumberInput
bind:value={nf.bedTemp}
min={0}
step={5}
unit="°C"
placeholder="—"
spaced
invalid={!!errors.bedTemp}
@@ -663,7 +667,7 @@
{#if errors.count}<span class="err">{errors.count}</span>{/if}
</label>
<label
>{m['filament.fields.weight']()} <span class="u">g</span>
>{m['filament.fields.weight']()}
<button
type="button"
class="help-toggle"
@@ -672,7 +676,7 @@
aria-expanded={openHelp === 'weight'}
onclick={() => (openHelp = openHelp === 'weight' ? null : 'weight')}>ⓘ</button
>
<NumberInput bind:value={netWeight} min={0} step={50} spaced invalid={!!errors.netWeight} />
<NumberInput bind:value={netWeight} min={0} step={50} unit="g" spaced invalid={!!errors.netWeight} />
{#if openHelp === 'weight'}
<span class="help-popup" id="weight-help" role="note"
>{m['filament.fieldsHelp.weight']()}</span
@@ -681,7 +685,7 @@
{#if errors.netWeight}<span class="err">{errors.netWeight}</span>{/if}
</label>
<label
>{m['filament.fields.spoolWeight']()} <span class="u">g</span>
>{m['filament.fields.spoolWeight']()}
<button
type="button"
class="help-toggle"
@@ -694,6 +698,7 @@
bind:value={spoolWeight}
min={0}
step={10}
unit="g"
placeholder="—"
spaced
invalid={!!errors.spoolWeight}
@@ -738,11 +743,11 @@
bind:value={fillWeight}
min={0}
step={10}
unit="g"
placeholder="0"
width="110px"
width="130px"
invalid={!!errors.fillWeight}
/>
<span class="u">g</span>
<span class="fill-help">{errors.fillWeight || fillHelp}</span>
</div>
{/if}
+109 -21
View File
@@ -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);
}
}
</script>
<div class="ni" class:spaced class:invalid style:width>
<div class="ni" class:spaced class:invalid class:dense class:disabled style:width>
<!-- value/oninput (not bind:value) so the value stays a string — Svelte would
otherwise coerce a type=number binding to a number. -->
<input
class="mono"
type="number"
{value}
oninput={(e) => (value = e.currentTarget.value)}
value={shown}
oninput={(e) => onInput(e.currentTarget.value)}
onchange={commit}
{min}
{max}
{step}
{placeholder}
{disabled}
inputmode="decimal"
/>
{#if unit}<span class="unit">{unit}</span>{/if}
<div class="spin">
<button
type="button"
tabindex="-1"
{disabled}
aria-label={m['common.increment']()}
onmousedown={(e) => (e.preventDefault(), bump(1))}
>
@@ -75,6 +123,7 @@
<button
type="button"
tabindex="-1"
{disabled}
aria-label={m['common.decrement']()}
onmousedown={(e) => (e.preventDefault(), bump(-1))}
>
@@ -94,40 +143,79 @@
<style>
.ni {
position: relative;
display: block;
display: flex;
align-items: stretch;
border: 1px solid var(--border-strong);
background: none;
border-radius: 7px;
overflow: hidden;
/* Allow shrinking below the native input's preferred width when placed in
grid/flex cells (e.g. the label print panel's margin grid). */
min-width: 0;
}
.ni.spaced {
margin-top: 5px;
}
.ni:focus-within {
border-color: var(--accent);
}
.ni.invalid {
border-color: var(--danger);
}
.ni.dense {
border-radius: 6px;
}
.ni.disabled {
opacity: 0.55;
}
.ni.disabled input,
.ni.disabled .spin button {
cursor: not-allowed;
}
.ni input {
width: 100%;
border: 1px solid var(--border-strong);
flex: 1;
/* width:0 (+ flex-grow) keeps the native input's intrinsic width from
forcing the whole control wider than its grid/flex cell. */
width: 0;
min-width: 0;
border: none;
background: none;
border-radius: 7px;
padding: 9px 30px 9px 12px;
padding: 9px 4px 9px 12px;
color: var(--text);
font-size: 13px;
}
.ni.dense input {
padding: 7px 2px 7px 9px;
font-size: 12.5px;
}
.ni input:focus {
outline: none;
border-color: var(--accent);
}
.ni.invalid input {
border-color: var(--danger);
.unit {
display: flex;
align-items: center;
padding: 0 8px 0 2px;
color: var(--text-muted);
font-size: 12px;
white-space: nowrap;
pointer-events: none;
user-select: none;
}
.ni.dense .unit {
padding: 0 6px 0 2px;
font-size: 11px;
}
.spin {
position: absolute;
top: 1px;
right: 1px;
bottom: 1px;
flex: none;
width: 22px;
display: flex;
flex-direction: column;
border-left: 1px solid var(--border-strong);
border-radius: 0 7px 7px 0;
overflow: hidden;
}
.ni.dense .spin {
width: 18px;
}
.spin button {
flex: 1;
display: flex;
@@ -1,6 +1,7 @@
<script lang="ts">
import type { LabelElement, ElementType } from '$lib/labels/types';
import type { PlaceholderGroup } from '$lib/labels/template';
import NumberInput from '../NumberInput.svelte';
import * as m from '$lib/paraglide/messages';
interface Props {
@@ -22,9 +23,6 @@
function update(patch: Record<string, unknown>) {
if (el) onchange({ ...el, ...patch } as LabelElement);
}
function num(e: Event): number {
return parseFloat((e.currentTarget as HTMLInputElement).value) || 0;
}
function insertField(token: string) {
if (el?.type === 'text') update({ template: `${el.template}{${token}}` });
}
@@ -38,31 +36,34 @@
<div class="row2">
<label
>{m['labels.xMm']()}<input
type="number"
step="0.5"
>{m['labels.xMm']()}<NumberInput
dense
unit="mm"
step={0.5}
value={el.x}
onchange={(e) => update({ x: num(e) })}
onchange={(v) => update({ x: v })}
/></label
>
<label
>{m['labels.yMm']()}<input
type="number"
step="0.5"
>{m['labels.yMm']()}<NumberInput
dense
unit="mm"
step={0.5}
value={el.y}
onchange={(e) => update({ y: num(e) })}
onchange={(v) => update({ y: v })}
/></label
>
</div>
{#if el.type === 'qr'}
<label
>{m['labels.sizeMm']()}<input
type="number"
step="0.5"
min="5"
>{m['labels.sizeMm']()}<NumberInput
dense
unit="mm"
step={0.5}
min={5}
value={el.size}
onchange={(e) => update({ size: num(e) })}
onchange={(v) => update({ size: v })}
/></label
>
<label class="check"
@@ -90,21 +91,23 @@
{:else if el.type === 'text'}
<div class="row2">
<label
>{m['labels.widthMm']()}<input
type="number"
step="0.5"
min="2"
>{m['labels.widthMm']()}<NumberInput
dense
unit="mm"
step={0.5}
min={2}
value={el.w}
onchange={(e) => update({ w: num(e) })}
onchange={(v) => update({ w: v })}
/></label
>
<label
>{m['labels.fontMm']()}<input
type="number"
step="0.25"
min="1"
>{m['labels.fontMm']()}<NumberInput
dense
unit="mm"
step={0.25}
min={1}
value={el.fontSize}
onchange={(e) => update({ fontSize: num(e) })}
onchange={(v) => update({ fontSize: v })}
/></label
>
</div>
@@ -173,72 +176,79 @@
{:else if el.type === 'swatch'}
<div class="row2">
<label
>{m['labels.widthMm']()}<input
type="number"
step="0.5"
min="2"
>{m['labels.widthMm']()}<NumberInput
dense
unit="mm"
step={0.5}
min={2}
value={el.w}
onchange={(e) => update({ w: num(e) })}
onchange={(v) => update({ w: v })}
/></label
>
<label
>{m['labels.heightMm']()}<input
type="number"
step="0.5"
min="2"
>{m['labels.heightMm']()}<NumberInput
dense
unit="mm"
step={0.5}
min={2}
value={el.h}
onchange={(e) => update({ h: num(e) })}
onchange={(v) => update({ h: v })}
/></label
>
</div>
<label
>{m['labels.cornerRadiusMm']()}<input
type="number"
step="0.5"
min="0"
>{m['labels.cornerRadiusMm']()}<NumberInput
dense
unit="mm"
step={0.5}
min={0}
value={el.radius}
onchange={(e) => update({ radius: num(e) })}
onchange={(v) => update({ radius: v })}
/></label
>
<p class="hint">{m['labels.hintSwatch']()}</p>
{:else if el.type === 'rect'}
<div class="row2">
<label
>{m['labels.widthMm']()}<input
type="number"
step="0.5"
min="1"
>{m['labels.widthMm']()}<NumberInput
dense
unit="mm"
step={0.5}
min={1}
value={el.w}
onchange={(e) => update({ w: num(e) })}
onchange={(v) => update({ w: v })}
/></label
>
<label
>{m['labels.heightMm']()}<input
type="number"
step="0.5"
min="1"
>{m['labels.heightMm']()}<NumberInput
dense
unit="mm"
step={0.5}
min={1}
value={el.h}
onchange={(e) => update({ h: num(e) })}
onchange={(v) => update({ h: v })}
/></label
>
</div>
<div class="row2">
<label
>{m['labels.radiusMm']()}<input
type="number"
step="0.5"
min="0"
>{m['labels.radiusMm']()}<NumberInput
dense
unit="mm"
step={0.5}
min={0}
value={el.radius}
onchange={(e) => update({ radius: num(e) })}
onchange={(v) => update({ radius: v })}
/></label
>
<label
>{m['labels.strokeMm']()}<input
type="number"
step="0.1"
min="0"
>{m['labels.strokeMm']()}<NumberInput
dense
unit="mm"
step={0.1}
min={0}
value={el.strokeWidth}
onchange={(e) => update({ strokeWidth: num(e) })}
onchange={(v) => update({ strokeWidth: v })}
/></label
>
</div>
@@ -294,10 +304,9 @@
}
.row2 {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 8px;
}
input[type='number'],
select,
textarea {
width: 100%;
@@ -1,6 +1,7 @@
<script lang="ts">
import LabelCanvas from './LabelCanvas.svelte';
import ElementInspector from './ElementInspector.svelte';
import NumberInput from '../NumberInput.svelte';
import type { LabelDesign, LabelElement } from '$lib/labels/types';
import { getPlaceholderGroups, type LabelBinding } from '$lib/labels/template';
import { fields } from '$lib/stores/fields.svelte';
@@ -145,19 +146,23 @@
<div class="p-head">{m['printing.generic.dimensions']()}</div>
<label class="sz"
>{m['labels.wMm']()}<input
type="number"
min="5"
>{m['labels.wMm']()}<NumberInput
dense
unit="mm"
min={5}
step={0.5}
value={design.label.w}
onchange={(e) => setLabelSize('w', parseFloat(e.currentTarget.value) || 5)}
onchange={(v) => setLabelSize('w', v || 5)}
/></label
>
<label class="sz"
>{m['labels.hMm']()}<input
type="number"
min="5"
>{m['labels.hMm']()}<NumberInput
dense
unit="mm"
min={5}
step={0.5}
value={design.label.h}
onchange={(e) => setLabelSize('h', parseFloat(e.currentTarget.value) || 5)}
onchange={(v) => setLabelSize('h', v || 5)}
/></label
>
</div>
@@ -241,18 +246,6 @@
font-size: 11px;
color: var(--text-dim);
}
.sz input {
border: 1px solid var(--border-strong);
background: none;
border-radius: 6px;
padding: 6px 8px;
color: var(--text);
font-size: 12.5px;
}
.sz input:focus {
outline: none;
border-color: var(--accent);
}
.stage-area {
display: flex;
flex-direction: column;
@@ -1,6 +1,7 @@
<script lang="ts">
import { untrack } from 'svelte';
import Button from '$components/Button.svelte';
import NumberInput from '../NumberInput.svelte';
import LabelCanvas from './LabelCanvas.svelte';
import type { LabelDesign } from '$lib/labels/types';
import type { LabelBinding } from '$lib/labels/template';
@@ -103,9 +104,6 @@
function setMargin(k: 't' | 'b' | 'l' | 'r', v: number) {
layout.margin = { ...layout.margin, [k]: v };
}
function n(e: Event): number {
return parseFloat((e.currentTarget as HTMLInputElement).value) || 0;
}
</script>
<div class="print-panel">
@@ -159,17 +157,19 @@
{#if layout.paper === 'custom'}
<div class="row2">
<label class="fld"
>{m['labels.widthMm']()}<input
type="number"
>{m['labels.widthMm']()}<NumberInput
dense
unit="mm"
value={layout.custom.w}
onchange={(e) => (layout.custom = { ...layout.custom, w: n(e) })}
onchange={(v) => (layout.custom = { ...layout.custom, w: v })}
/></label
>
<label class="fld"
>{m['labels.heightMm']()}<input
type="number"
>{m['labels.heightMm']()}<NumberInput
dense
unit="mm"
value={layout.custom.h}
onchange={(e) => (layout.custom = { ...layout.custom, h: n(e) })}
onchange={(v) => (layout.custom = { ...layout.custom, h: v })}
/></label
>
</div>
@@ -180,65 +180,71 @@
<div class="row4">
<label class="fld"
>{m['labels.marginTop']()}<input
type="number"
>{m['labels.marginTop']()}<NumberInput
dense
unit="mm"
value={layout.margin.t}
onchange={(e) => setMargin('t', n(e))}
onchange={(v) => setMargin('t', v)}
/></label
>
<label class="fld"
>{m['labels.marginBottom']()}<input
type="number"
>{m['labels.marginBottom']()}<NumberInput
dense
unit="mm"
value={layout.margin.b}
onchange={(e) => setMargin('b', n(e))}
onchange={(v) => setMargin('b', v)}
/></label
>
<label class="fld"
>{m['labels.marginLeft']()}<input
type="number"
>{m['labels.marginLeft']()}<NumberInput
dense
unit="mm"
value={layout.margin.l}
onchange={(e) => setMargin('l', n(e))}
onchange={(v) => setMargin('l', v)}
/></label
>
<label class="fld"
>{m['labels.marginRight']()}<input
type="number"
>{m['labels.marginRight']()}<NumberInput
dense
unit="mm"
value={layout.margin.r}
onchange={(e) => setMargin('r', n(e))}
onchange={(v) => setMargin('r', v)}
/></label
>
</div>
<div class="row2">
<label class="fld"
>{m['printing.generic.horizontalSpacing']()}<input
type="number"
>{m['printing.generic.horizontalSpacing']()}<NumberInput
dense
unit="mm"
value={layout.spacing.h}
onchange={(e) => (layout.spacing = { ...layout.spacing, h: n(e) })}
onchange={(v) => (layout.spacing = { ...layout.spacing, h: v })}
/></label
>
<label class="fld"
>{m['printing.generic.verticalSpacing']()}<input
type="number"
>{m['printing.generic.verticalSpacing']()}<NumberInput
dense
unit="mm"
value={layout.spacing.v}
onchange={(e) => (layout.spacing = { ...layout.spacing, v: n(e) })}
onchange={(v) => (layout.spacing = { ...layout.spacing, v: v })}
/></label
>
</div>
<div class="row2">
<label class="fld"
>{m['printing.generic.skipItems']()}<input
type="number"
min="0"
>{m['printing.generic.skipItems']()}<NumberInput
dense
min={0}
value={layout.skip}
onchange={(e) => (layout.skip = n(e))}
onchange={(v) => (layout.skip = v)}
/></label
>
<label class="fld"
>{m['printing.generic.itemCopies']()}<input
type="number"
min="1"
>{m['printing.generic.itemCopies']()}<NumberInput
dense
min={1}
value={layout.copies}
onchange={(e) => (layout.copies = Math.max(1, n(e)))}
onchange={(v) => (layout.copies = Math.max(1, v))}
/></label
>
</div>
@@ -256,11 +262,11 @@
{:else}
<div class="row2">
<label class="fld"
>{m['labels.copies']()}<input
type="number"
min="1"
>{m['labels.copies']()}<NumberInput
dense
min={1}
value={layout.copies}
onchange={(e) => (layout.copies = Math.max(1, n(e)))}
onchange={(v) => (layout.copies = Math.max(1, v))}
/></label
>
</div>
@@ -326,7 +332,6 @@
padding: 2px 4px;
}
.search,
.fld input,
.fld select {
width: 100%;
border: 1px solid var(--border-strong);
@@ -381,13 +386,15 @@
}
.row2 {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 8px;
}
.row4 {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 6px;
/* 2×2 so each margin field has room for a 3-digit value plus the mm unit
and steppers. */
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 6px 8px;
}
.seg {
display: flex;
@@ -2,6 +2,7 @@
import Swatch from '../Swatch.svelte';
import Button from '../Button.svelte';
import EditableField from '../EditableField.svelte';
import NumberInput from '../NumberInput.svelte';
import ProgressBar from '../ProgressBar.svelte';
import SectionLabel from '../SectionLabel.svelte';
import ExtraFieldsSection from '../ExtraFieldsSection.svelte';
@@ -135,36 +136,60 @@
<Field label={m['inspector.colorHex']()}>
<EditableField value={filament.colors[0]} mono oninput={(v) => set({ colors: [v] })} />
</Field>
<Field label={m['inspector.diameterMm']()}>
<EditableField
<Field label={m['filament.fields.diameter']()}>
<NumberInput
dense
width="200px"
unit="mm"
step={0.05}
min={0}
value={filament.diameter}
mono
oninput={(v) => set({ diameter: parseFloat(v) || filament.diameter })}
onchange={(v) => set({ diameter: v || filament.diameter })}
/>
</Field>
<Field label={m['filament.fields.density']()}>
<EditableField
<NumberInput
dense
width="200px"
unit="g/cm³"
step={0.01}
min={0}
value={filament.density}
mono
oninput={(v) => set({ density: parseFloat(v) || filament.density })}
onchange={(v) => set({ density: v || filament.density })}
/>
</Field>
<Field label={m['inspector.nozzleC']()}>
<EditableField
<NumberInput
dense
width="200px"
unit="°C"
step={5}
min={0}
value={filament.nozzleTemp}
mono
oninput={(v) => set({ nozzleTemp: parseInt(v, 10) || filament.nozzleTemp })}
onchange={(v) => set({ nozzleTemp: Math.round(v) })}
/>
</Field>
<Field label={m['inspector.bedC']()}>
<EditableField
<NumberInput
dense
width="200px"
unit="°C"
step={5}
min={0}
value={filament.bedTemp}
mono
oninput={(v) => set({ bedTemp: parseInt(v, 10) || filament.bedTemp })}
onchange={(v) => set({ bedTemp: Math.round(v) })}
/>
</Field>
<Field label={m['inspector.defaultPrice']({ currency: settings.currency })}>
<EditableField value={filament.price} mono oninput={(v) => set({ price: parseFloat(v) || 0 })} />
<Field label={m['filament.fields.price']()}>
<NumberInput
dense
width="200px"
unit={settings.currency}
step={1}
min={0}
value={filament.price}
onchange={(v) => set({ price: v })}
/>
</Field>
</FieldGrid>
@@ -1,6 +1,7 @@
<script lang="ts">
import Swatch from '../Swatch.svelte';
import Button from '../Button.svelte';
import NumberInput from '../NumberInput.svelte';
import EditableField from '../EditableField.svelte';
import Combobox from '../Combobox.svelte';
import DateTimeField from '../DateTimeField.svelte';
@@ -158,16 +159,6 @@
saver.push(spool.id, patch);
}
function setPrice(raw: string) {
const trimmed = raw.trim();
if (trimmed === '') {
set({ price: undefined });
return;
}
const v = parseFloat(trimmed);
if (!isNaN(v) && v >= 0) set({ price: v });
}
const extraSaver = makeExtraSaver(
(e) => inventory.patchSpool(spool.id, { extra: e }),
(p) => spoolSource.saveSpool(spool.id, { extra: p }).catch((err) => console.error('Save failed', err)),
@@ -237,15 +228,13 @@
</div>
<div class="adjust-row">
<span class="adj-label">{adjustInfo.fieldLabelKey()}</span>
<input
class="adj-input mono"
type="number"
step="any"
value={adjustVal}
oninput={(e) => (adjustVal = e.currentTarget.value)}
<NumberInput
bind:value={adjustVal}
unit={adjustInfo.unit}
step={0.01}
disabled={adjustBusy}
width="130px"
/>
<span class="adj-unit">{adjustInfo.unit}</span>
<Button onclick={applyAdjust} disabled={adjustBusy}
>{adjustBusy ? m['inspector.applying']() : m['inspector.apply']()}</Button
>
@@ -276,17 +265,18 @@
<EditableField value={spool.lot} mono oninput={(v) => set({ lot: v })} />
</Field>
<Field label={m['spool.fields.price']()}>
<div class="price-row">
<EditableField
value={spool.price != null ? String(spool.price) : ''}
placeholder={m['inspector.filamentDefault']({
price: settings.formatPriceValue(filament.price)
})}
mono
oninput={setPrice}
/>
<span class="price-unit">{settings.currencySymbol}</span>
</div>
<NumberInput
dense
width="240px"
unit={settings.currencySymbol}
min={0}
placeholder={m['inspector.filamentDefault']({
price: settings.formatPriceValue(filament.price)
})}
value={spool.price ?? ''}
onchange={(v) => set({ price: v })}
onclear={() => set({ price: undefined })}
/>
</Field>
<Field label={m['spool.fields.registered']()}>{spool.registeredLabel}</Field>
<Field label={m['spool.fields.firstUsed']()}>
@@ -456,19 +446,6 @@
font-size: 12.5px;
color: var(--accent-muted-2);
}
.adj-input {
width: 90px;
background: var(--input-bg);
border: 1px solid var(--border-input);
border-radius: var(--radius);
padding: 6px 10px;
color: var(--text);
font-size: 13px;
}
.adj-unit {
font-size: 12px;
color: var(--text-dim);
}
.adj-help {
font-size: 11px;
color: var(--text-faint);
@@ -489,20 +466,6 @@
align-items: center;
gap: 7px;
}
.price-row {
display: flex;
align-items: center;
gap: 6px;
}
.price-row :global(.edit) {
flex: 1;
min-width: 0;
}
.price-unit {
color: var(--text-dim);
font-size: 12px;
flex: none;
}
.link {
font-size: 11.5px;
color: var(--accent-link);
@@ -1,6 +1,7 @@
<script lang="ts">
import Swatch from '../Swatch.svelte';
import EditableField from '../EditableField.svelte';
import NumberInput from '../NumberInput.svelte';
import SectionLabel from '../SectionLabel.svelte';
import ExtraFieldsSection from '../ExtraFieldsSection.svelte';
import FieldGrid from '../FieldGrid.svelte';
@@ -81,10 +82,14 @@
<EditableField value={vendor.name} oninput={(v) => set({ name: v })} />
</Field>
<Field label={m['inspector.emptySpoolWt']()}>
<EditableField
<NumberInput
dense
unit="g"
step={10}
min={0}
width="200px"
value={vendor.emptyWeight}
mono
oninput={(v) => set({ emptyWeight: parseInt(v, 10) || vendor.emptyWeight })}
onchange={(v) => set({ emptyWeight: Math.round(v) })}
/>
</Field>
</FieldGrid>
+14 -14
View File
@@ -167,13 +167,13 @@
"inspector.adjustWeight": "Adjust weight",
"inspector.apply": "Apply",
"inspector.applying": "Applying...",
"inspector.bedC": "Bed °C",
"inspector.bedC": "Bed",
"inspector.colorHex": "Color hex",
"inspector.consumeAmount": "Consume amount:",
"inspector.defaultPrice": "Default price ({{currency}})",
"inspector.diameterMm": "Diameter mm",
"inspector.empty": "Select a spool, filament or manufacturer from the list to see its details here.",
"inspector.emptySpoolWt": "Empty spool wt. g",
"inspector.emptySpoolWt": "Empty spool wt.",
"inspector.enterValidLength": "Enter a valid length.",
"inspector.enterValidWeight": "Enter a valid weight.",
"inspector.filamentDefault": "{{price}} · filament default",
@@ -182,7 +182,7 @@
"inspector.multi": "multi ×{{count}}",
"inspector.newGross": "New gross weight:",
"inspector.nozzleBed": "Nozzle / bed",
"inspector.nozzleC": "Nozzle °C",
"inspector.nozzleC": "Nozzle",
"inspector.ofRemaining": "of {{weight}} g remaining · {{length}} m",
"inspector.openFilament": "Open filament →",
"inspector.printLabel": "Print label",
@@ -208,7 +208,7 @@
"labels.chooseField": "Choose a field...",
"labels.clearFill": "Clear fill (outline only)",
"labels.copies": "Copies",
"labels.cornerRadiusMm": "Corner radius (mm)",
"labels.cornerRadiusMm": "Corner radius",
"labels.createFirst": "Create your first label",
"labels.cutGuides": "Cut guides",
"labels.deleteConfirm": "Delete this label design?",
@@ -227,12 +227,12 @@
"labels.fields.netWeightG": "Net weight (g)",
"labels.fields.remainingG": "Remaining (g)",
"labels.fill": "Fill",
"labels.fontMm": "Font (mm)",
"labels.fontMm": "Font",
"labels.gapH": "Gap H (mm)",
"labels.gapV": "Gap V (mm)",
"labels.gridInfo": "{{cols}} × {{rows}} = {{perPage}} labels / page",
"labels.hMm": "H (mm)",
"labels.heightMm": "Height (mm)",
"labels.hMm": "H",
"labels.heightMm": "Height",
"labels.hintEncScheme": "A compact Spoolman code — scan it with the Spoolman app's built-in camera. Most generic scanners won't open it.",
"labels.hintEncUrl": "A full web link — scannable by any QR reader or phone camera. Uses the External URL from Settings (falls back to this site's address).",
"labels.hintNowrap": "Text stays on one line; anything past the width is clipped.",
@@ -263,7 +263,7 @@
"labels.previewWith": "Preview with",
"labels.printN_one": "Print {{count}} label",
"labels.printN_other": "Print {{count}} labels",
"labels.radiusMm": "Radius (mm)",
"labels.radiusMm": "Radius",
"labels.sampleData": "Sample data (no spool)",
"labels.saved": "Saved",
"labels.saving": "Saving...",
@@ -275,19 +275,19 @@
"labels.showingReal": "Showing real spool data.",
"labels.showingSpool": "Showing spool #{{id}}",
"labels.showingTags": "Fields show as tags until you pick a spool above.",
"labels.sizeMm": "Size (mm)",
"labels.sizeMm": "Size",
"labels.skipCells": "Skip cells",
"labels.stroke": "Stroke",
"labels.strokeMm": "Stroke (mm)",
"labels.strokeMm": "Stroke",
"labels.tabDesign": "Design",
"labels.tabPrint": "Print",
"labels.textTemplate": "Text / template",
"labels.unknownFilament": "Unknown filament",
"labels.wMm": "W (mm)",
"labels.widthMm": "Width (mm)",
"labels.wMm": "W",
"labels.widthMm": "Width",
"labels.wrapText": "Wrap text",
"labels.xMm": "X (mm)",
"labels.yMm": "Y (mm)",
"labels.xMm": "X",
"labels.yMm": "Y",
"library.apiError": "Couldn't reach the Spoolman API. Is the backend running?",
"library.emptyFiltered": "No {{unit}} match the current filters.",
"library.filterBy": "Filter by",