Files
threadcount-community/components/m/count/CountPanel.tsx
T
ThreadCount 0910bc32c1 ThreadCount Community edition
Uniform stock management for healthcare linen rooms: the coordinator app, the phone counter and the staff app, for your own server. Built from 38e16eb on 2026-09-15. Licensed under the Functional Source License (FSL-1.1-ALv2).
2026-09-16 07:57:54 +10:00

53 lines
3.1 KiB
TypeScript

"use client";
/* The ink "Now counting" panel (mockup .panel): garment · size, Counted / Expected / Gap, the
* Hands-free switch, and the inline "Type a count" field when it is open. The same pieces are the
* figure and control of the live camera overlay when hands-free falls back to it. */
import { useEffect, useRef, useState } from "react";
import { GROUND, INK, MFigRow, MKick, MSwitchRow } from "@/components/m";
import { signed } from "@/components/m/count/lines";
const OFF_GREY_TEXT = "#b5b1af"; // mockup .panel .kick
export function CountFigures({ name, counted, expected }: { name: string; counted: number; expected: number }) {
const d = counted - expected;
return (
<>
<div style={{ ["--tcx-kick" as string]: OFF_GREY_TEXT } as React.CSSProperties}><MKick>Now counting</MKick></div>
<div aria-live="polite" style={{ fontSize: 19, fontWeight: 800, marginTop: 2, lineHeight: 1.2 }}>{name}</div>
<MFigRow figs={[
{ label: "Counted", n: counted },
{ label: "Expected", n: expected },
{ label: "Gap", n: signed(d), tone: d === 0 ? "ok" : "accent" },
]} />
</>
);
}
export function HandsFree({ on, onToggle, disabled }: { on: boolean; onToggle: () => void; disabled?: boolean }) {
return <MSwitchRow dark tone="accent" title="Hands-free" on={on} onToggle={onToggle} disabled={disabled} />;
}
/** Inline count field: Enter or Set applies it. Keyed by the caller on the line, so it never shows
* the previous line's figure under the new line's name. */
export function TypeCount({ name, value, onSet }: { name: string; value: number; onSet: (n: number) => void }) {
const [v, setV] = useState(String(value));
const ref = useRef<HTMLInputElement | null>(null);
useEffect(() => { ref.current?.focus(); ref.current?.select(); }, []);
const set = () => { const n = parseInt(v, 10); if (Number.isFinite(n) && n >= 0) onSet(n); };
return (
<div style={{ display: "flex", gap: 8, marginTop: 10 }}>
<input ref={ref} type="text" inputMode="numeric" pattern="[0-9]*" aria-label={`Counted for ${name}`} value={v}
onChange={(e) => setV(e.target.value.replace(/[^0-9]/g, "").slice(0, 5))}
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); set(); } }}
style={{ flex: 1, minWidth: 0, height: 52, border: "2px solid #57534f", background: "#fff", color: INK, fontFamily: "inherit", fontSize: 16, padding: "0 14px", borderRadius: 0, boxSizing: "border-box" }} />
<button type="button" onClick={set}
style={{ minHeight: 52, padding: "0 18px", border: "2px solid " + GROUND, background: "transparent", color: GROUND, fontFamily: "inherit", fontSize: 14, fontWeight: 800, letterSpacing: "0.06em", textTransform: "uppercase", cursor: "pointer" }}>Set</button>
</div>
);
}
/** The panel as it sits at the top of the counting body (bleeds over MBody pad). */
export function CountPanel({ children }: { children: React.ReactNode }) {
return <section aria-label="Now counting" style={{ background: INK, color: GROUND, margin: "-16px -16px 12px", padding: "14px 16px 16px" }}>{children}</section>;
}