import { useCallback, useEffect, useRef, useState } from "react"; import { ArrowDown, ArrowUp } from "lucide-react"; import { useTranslation } from "react-i18next"; import Tooltip from "./Tooltip"; function PriorityStepButton({ delta, Icon, label, onHoldStart, onStop, onTap, }: { delta: number; Icon: typeof ArrowUp; label: string; onHoldStart: (delta: number) => void; onStop: () => void; onTap: (delta: number) => void; }) { return ( ); } /** * A press-and-hold-repeat priority control with an inline-editable value (negatives allowed). * Shared by the channel manager table (a cell) and the channel detail page (a header control), so * the two never drift. The caller owns persistence: `onPriority(delta)` nudges by ±1 (with hold * auto-repeat), `onSet(value)` writes a typed-in absolute value. */ export function PriorityStepper({ priority, onPriority, onSet, }: { priority: number; onPriority: (delta: number) => void; onSet: (value: number) => void; }) { const { t } = useTranslation(); const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(""); const holdDelay = useRef | null>(null); const holdRepeat = useRef | null>(null); const suppressClick = useRef(false); const stop = useCallback(() => { if (holdDelay.current) clearTimeout(holdDelay.current); if (holdRepeat.current) clearTimeout(holdRepeat.current); holdDelay.current = holdRepeat.current = null; }, []); useEffect(() => stop, [stop]); // clear any running timers on unmount // Hold past a threshold → auto-repeat at an accelerating rate, and suppress the trailing click so // a hold doesn't double-count its final notch. A quick tap never crosses the threshold, so it // falls through to onClick — which also keeps the keyboard path (Enter/Space) working. const start = (delta: number) => { suppressClick.current = false; holdDelay.current = setTimeout(() => { suppressClick.current = true; let rate = 140; const tick = () => { onPriority(delta); rate = Math.max(40, rate - 12); holdRepeat.current = setTimeout(tick, rate); }; tick(); }, 350); }; const onTap = (delta: number) => { if (suppressClick.current) { suppressClick.current = false; return; } onPriority(delta); }; const commitEdit = () => { const parsed = parseInt(draft, 10); if (!Number.isNaN(parsed) && parsed !== priority) onSet(parsed); setEditing(false); }; const beginEdit = () => { setDraft(String(priority)); setEditing(true); }; // The value sits to the LEFT of the arrows (not between them) so the cursor — which rests on the // arrows while you press-and-hold — never covers the live value you're setting. Clicking the value // opens an inline edit to type any integer directly (negatives allowed). return (
{/* The number keeps its footprint (w-6) whether displayed or editing; the input is absolutely positioned OVER it (out of flow) so opening the editor never changes the row height/width — it just overlays, extending left for a longer number. */} {editing && ( e.currentTarget.select()} onChange={(e) => setDraft(e.target.value)} onBlur={commitEdit} onKeyDown={(e) => { if (e.key === "Enter") commitEdit(); else if (e.key === "Escape") setEditing(false); }} aria-label={t("channels.row.priorityHint")} className="absolute top-1/2 right-0 -translate-y-1/2 w-14 h-6 text-sm font-medium tabular-nums text-right bg-card border border-accent rounded-sm px-1 outline-hidden z-popover [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none" /> )}
); }