95 lines
2.5 KiB
Svelte
95 lines
2.5 KiB
Svelte
<script lang="ts">
|
|
interface Props {
|
|
value: number;
|
|
min?: number;
|
|
max?: number;
|
|
step?: number;
|
|
formatValue?: (v: number) => string;
|
|
onchange?: (value: number) => void;
|
|
label?: string;
|
|
}
|
|
|
|
let {
|
|
value = $bindable(),
|
|
min = 0,
|
|
max = 100,
|
|
step = 1,
|
|
formatValue = (v: number) => String(v),
|
|
onchange,
|
|
label = "Value",
|
|
}: Props = $props();
|
|
|
|
let holdTimer: ReturnType<typeof setTimeout> | null = null;
|
|
let holdInterval: ReturnType<typeof setInterval> | null = null;
|
|
|
|
function decrement() {
|
|
if (value > min) {
|
|
value = Math.max(min, value - step);
|
|
onchange?.(value);
|
|
}
|
|
}
|
|
|
|
function increment() {
|
|
if (value < max) {
|
|
value = Math.min(max, value + step);
|
|
onchange?.(value);
|
|
}
|
|
}
|
|
|
|
function startHold(fn: () => void) {
|
|
fn();
|
|
// Initial delay before repeating
|
|
holdTimer = setTimeout(() => {
|
|
// Start repeating, accelerate over time
|
|
let delay = 150;
|
|
function tick() {
|
|
fn();
|
|
delay = Math.max(40, delay * 0.85);
|
|
holdInterval = setTimeout(tick, delay) as unknown as ReturnType<typeof setInterval>;
|
|
}
|
|
tick();
|
|
}, 400);
|
|
}
|
|
|
|
function stopHold() {
|
|
if (holdTimer) { clearTimeout(holdTimer); holdTimer = null; }
|
|
if (holdInterval) { clearTimeout(holdInterval as unknown as ReturnType<typeof setTimeout>); holdInterval = null; }
|
|
}
|
|
</script>
|
|
|
|
<div class="flex items-center gap-1.5" role="group" aria-label={label}>
|
|
<button
|
|
type="button"
|
|
aria-label="Decrease"
|
|
class="flex h-7 w-7 items-center justify-center rounded-lg
|
|
bg-[#141414] text-[#8a8a8a] transition-colors
|
|
hover:bg-[#1c1c1c] hover:text-white
|
|
disabled:opacity-20"
|
|
onmousedown={() => startHold(decrement)}
|
|
onmouseup={stopHold}
|
|
onmouseleave={stopHold}
|
|
onclick={(e) => { if (e.detail === 0) decrement(); }}
|
|
disabled={value <= min}
|
|
>
|
|
−
|
|
</button>
|
|
<span class="min-w-[38px] text-center text-[13px] tabular-nums text-white" aria-live="polite" aria-atomic="true">
|
|
{formatValue(value)}
|
|
</span>
|
|
<button
|
|
type="button"
|
|
aria-label="Increase"
|
|
class="flex h-7 w-7 items-center justify-center rounded-lg
|
|
bg-[#141414] text-[#8a8a8a] transition-colors
|
|
hover:bg-[#1c1c1c] hover:text-white
|
|
disabled:opacity-20"
|
|
onmousedown={() => startHold(increment)}
|
|
onmouseup={stopHold}
|
|
onmouseleave={stopHold}
|
|
onclick={(e) => { if (e.detail === 0) increment(); }}
|
|
disabled={value >= max}
|
|
>
|
|
+
|
|
</button>
|
|
</div>
|