Some checks failed
🚀 Lint & Test & Deploy / release (push) Has been cancelled
182 lines
4.3 KiB
Svelte
182 lines
4.3 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from 'svelte';
|
|
|
|
interface Props {
|
|
value?: number;
|
|
step?: number;
|
|
min?: number;
|
|
max?: number;
|
|
id?: string;
|
|
}
|
|
|
|
let {
|
|
value = $bindable(0.5),
|
|
step,
|
|
min = $bindable(0),
|
|
max = $bindable(1),
|
|
id
|
|
}: Props = $props();
|
|
|
|
// normalize bounds
|
|
if (min > max) [min, max] = [max, min];
|
|
|
|
let inputEl: HTMLInputElement | undefined = $state();
|
|
|
|
function clamp(v: number) {
|
|
return Math.min(max, Math.max(min, v));
|
|
}
|
|
|
|
function snap(v: number, s = step) {
|
|
if (s) v = Math.round(v / s) * s;
|
|
return +v.toFixed(3);
|
|
}
|
|
|
|
function getAutoStep(v: number): number {
|
|
const abs = Math.abs(v);
|
|
if (abs === 0) return 0.1; // fallback for 0
|
|
const exponent = Math.floor(Math.log10(abs));
|
|
return Math.pow(10, exponent);
|
|
}
|
|
|
|
let dragging = $state(false);
|
|
let startValue = 0;
|
|
let rect: DOMRect;
|
|
|
|
function onMouseDown(e: MouseEvent) {
|
|
if (!inputEl) return;
|
|
|
|
e.preventDefault();
|
|
|
|
dragging = true;
|
|
startValue = value;
|
|
rect = inputEl.getBoundingClientRect();
|
|
|
|
document.body.style.cursor = 'ew-resize';
|
|
window.addEventListener('mousemove', onMouseMove);
|
|
window.addEventListener('mouseup', onMouseUp);
|
|
}
|
|
|
|
function onMouseMove(e: MouseEvent) {
|
|
const ratio = (e.clientX - rect.left) / rect.width;
|
|
let delta = (min + (max - min) * ratio) - startValue;
|
|
|
|
if (e.shiftKey) delta /= 5;
|
|
|
|
value = snap(
|
|
e.ctrlKey
|
|
? startValue + delta
|
|
: clamp(startValue + delta),
|
|
(e.altKey && !step) ? getAutoStep(value) : step
|
|
);
|
|
}
|
|
|
|
function onMouseUp() {
|
|
dragging = false;
|
|
document.body.style.cursor = '';
|
|
window.removeEventListener('mousemove', onMouseMove);
|
|
window.removeEventListener('mouseup', onMouseUp);
|
|
if (startValue === value) {
|
|
inputEl?.focus();
|
|
}
|
|
|
|
min = Math.min(min, value);
|
|
max = Math.max(max, value);
|
|
}
|
|
|
|
function onKeyDown(e: KeyboardEvent) {
|
|
if (e.key === 'Escape' || e.key === 'Enter') {
|
|
onMouseUp();
|
|
inputEl?.blur();
|
|
}
|
|
}
|
|
|
|
function windowKeyDown(e: KeyboardEvent) {
|
|
if (!dragging) return;
|
|
if (e.shiftKey) {
|
|
startValue = value;
|
|
}
|
|
}
|
|
|
|
const width = $derived(
|
|
Number.isFinite(value)
|
|
? `${Math.max(value.toString().length * 8, 50)}px`
|
|
: '20px'
|
|
);
|
|
|
|
function stepUp(e: MouseEvent) {
|
|
value = snap(value + (e.shiftKey ? step! * 10 : step!));
|
|
}
|
|
|
|
function stepDown(e: MouseEvent) {
|
|
value = snap(value - (e.shiftKey ? step! * 10 : step!));
|
|
}
|
|
|
|
onMount(() => {
|
|
value = snap(value);
|
|
});
|
|
</script>
|
|
|
|
<svelte:window onkeydown={windowKeyDown} />
|
|
|
|
<div
|
|
class="component-wrapper relative flex items-stretch overflow-hidden rounded-sm border border-outline bg-layer-2 select-none transition-shadow"
|
|
class:cursor-ew-resize={dragging}
|
|
>
|
|
{#if step}
|
|
<button
|
|
aria-label="step down"
|
|
onmousedown={stepDown}
|
|
class="cursor-pointer w-4 bg-layer-3 opacity-30 hover:opacity-50"
|
|
>
|
|
<span class="i-[tabler--chevron-compact-left] block h-full w-full text-outline!"></span>
|
|
</button>
|
|
<div class="w-px bg-outline"></div>
|
|
{/if}
|
|
|
|
<div class="relative grow">
|
|
<div
|
|
class="absolute inset-y-0 left-0 bg-layer-3 opacity-30 pointer-events-none transition-[width]"
|
|
class:transition-none={dragging}
|
|
style={`width: ${Math.max(0, Math.min((value - min) / (max - min), 1) * 100)}%`}
|
|
>
|
|
</div>
|
|
|
|
<input
|
|
bind:this={inputEl}
|
|
bind:value
|
|
{id}
|
|
{step}
|
|
{min}
|
|
{max}
|
|
type="number"
|
|
onkeydown={onKeyDown}
|
|
onmousedown={onMouseDown}
|
|
class="w-full min-w-full cursor-pointer bg-transparent px-2 py-1 text-center font-tabular text-text outline-none appearance-none"
|
|
style:width={width}
|
|
/>
|
|
</div>
|
|
|
|
{#if step}
|
|
<div class="w-px bg-outline"></div>
|
|
<button
|
|
aria-label="step up"
|
|
onmousedown={stepUp}
|
|
class="cursor-pointer w-4 bg-layer-3 opacity-30 hover:opacity-50"
|
|
>
|
|
<span class="i-[tabler--chevron-compact-right] block h-full w-full text-outline!"></span>
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
|
|
<style>
|
|
input[type="number"] {
|
|
-webkit-appearance: textfield;
|
|
-moz-appearance: textfield;
|
|
appearance: textfield;
|
|
}
|
|
input[type="number"]::-webkit-inner-spin-button,
|
|
input[type="number"]::-webkit-outer-spin-button {
|
|
-webkit-appearance: none;
|
|
}
|
|
</style>
|