diff --git a/client/src/components/BatchPanel.tsx b/client/src/components/BatchPanel.tsx index 5846254..fe3cb6c 100644 --- a/client/src/components/BatchPanel.tsx +++ b/client/src/components/BatchPanel.tsx @@ -180,7 +180,7 @@ export function BatchPanel() { ))} -
+
+ {}} + /> + + ); + } + const user = userEvent.setup(); + render(); + const input = screen.getByRole("spinbutton") as HTMLInputElement; + expect(input.value).toBe("90"); + + await user.click(screen.getByText("set")); + + expect(input.value).toBe("120"); + }); +}); diff --git a/client/src/components/NumberField.tsx b/client/src/components/NumberField.tsx new file mode 100644 index 0000000..8d5cf74 --- /dev/null +++ b/client/src/components/NumberField.tsx @@ -0,0 +1,66 @@ +import { useEffect, useState } from "react"; + +type NumberFieldProps = { + label: string; + value: number; + min: number; + max: number; + className?: string; + onChange: (value: number) => void; +}; + +/** + * A numeric input that's pleasant to hand-edit. + * + * A plain controlled `` fights the user: + * clearing it yields `Number("") === 0` (so the field jumps to 0, and the + * server then has to defend against a 0-size font), and the leftover leading + * digit makes the next keystroke read as e.g. "06". Here we keep the visible + * text as a free-form draft so the field can be emptied and retyped, commit a + * clamped number to the parent only when the draft parses, and resync the draft + * to the committed value on blur — never while typing, so clamping can't snap a + * half-entered number out from under the cursor. + */ +export function NumberField({ + label, + value, + min, + max, + className, + onChange, +}: NumberFieldProps) { + const [draft, setDraft] = useState(String(value)); + const [focused, setFocused] = useState(false); + + // Mirror an external value change (e.g. loading a label file) into the field, + // but stay out of the way while the user is actively editing it. + useEffect(() => { + if (!focused) setDraft(String(value)); + }, [value, focused]); + + const clamp = (n: number) => Math.min(max, Math.max(min, n)); + + return ( + + ); +} diff --git a/client/src/components/TextWidgetEditor.tsx b/client/src/components/TextWidgetEditor.tsx index ba51a8f..7f7df8d 100644 --- a/client/src/components/TextWidgetEditor.tsx +++ b/client/src/components/TextWidgetEditor.tsx @@ -1,5 +1,6 @@ import { useLabelStore } from "../state/useLabelStore"; import type { TextWidget, FontStyle, Alignment } from "../types/label"; +import { NumberField } from "./NumberField"; export function TextWidgetEditor({ widget }: { widget: TextWidget }) { const update = useLabelStore((s) => s.updateWidget); @@ -30,33 +31,23 @@ export function TextWidgetEditor({ widget }: { widget: TextWidget }) { - + update(widget.id, { fontScale })} + /> - + update(widget.id, { frameWidthPx })} + />