mirror of
https://github.com/Donkie/Spoolman.git
synced 2026-10-22 01:52:17 -05:00
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:
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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user