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"
/>
)}