initial commit with full project
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
value: number;
|
||||
labels?: { value: number; label: string }[];
|
||||
onChange: (value: number) => void;
|
||||
}
|
||||
|
||||
let { min, max, step, value, labels, onChange }: Props = $props();
|
||||
|
||||
let trackRef: HTMLDivElement | undefined = $state();
|
||||
let dragging = $state(false);
|
||||
|
||||
let fillPercent = $derived(((value - min) / (max - min)) * 100);
|
||||
|
||||
let currentLabel = $derived.by(() => {
|
||||
if (!labels) return null;
|
||||
let closest = labels[0];
|
||||
let dist = Math.abs(value - closest.value);
|
||||
for (const l of labels) {
|
||||
const d = Math.abs(value - l.value);
|
||||
if (d < dist) { closest = l; dist = d; }
|
||||
}
|
||||
return closest?.label ?? null;
|
||||
});
|
||||
|
||||
function valueFromEvent(e: MouseEvent | PointerEvent) {
|
||||
if (!trackRef) return value;
|
||||
const rect = trackRef.getBoundingClientRect();
|
||||
let pct = (e.clientX - rect.left) / rect.width;
|
||||
pct = Math.max(0, Math.min(1, pct));
|
||||
let raw = min + pct * (max - min);
|
||||
|
||||
if (labels && labels.length > 0) {
|
||||
let closest = labels[0].value;
|
||||
let dist = Math.abs(raw - closest);
|
||||
for (const l of labels) {
|
||||
const d = Math.abs(raw - l.value);
|
||||
if (d < dist) { closest = l.value; dist = d; }
|
||||
}
|
||||
const snapThreshold = (max - min) * 0.08;
|
||||
if (dist < snapThreshold) raw = closest;
|
||||
}
|
||||
|
||||
raw = Math.round(raw / step) * step;
|
||||
return Math.max(min, Math.min(max, raw));
|
||||
}
|
||||
|
||||
function handlePointerDown(e: PointerEvent) {
|
||||
dragging = true;
|
||||
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
||||
onChange(valueFromEvent(e));
|
||||
}
|
||||
|
||||
function handlePointerMove(e: PointerEvent) {
|
||||
if (!dragging) return;
|
||||
onChange(valueFromEvent(e));
|
||||
}
|
||||
|
||||
function handlePointerUp() {
|
||||
dragging = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="slider-wrap">
|
||||
{#if currentLabel}
|
||||
<div class="slider-value">{currentLabel}</div>
|
||||
{/if}
|
||||
<div
|
||||
bind:this={trackRef}
|
||||
class="slider-track"
|
||||
role="slider"
|
||||
aria-valuemin={min}
|
||||
aria-valuemax={max}
|
||||
aria-valuenow={value}
|
||||
tabindex="0"
|
||||
onpointerdown={handlePointerDown}
|
||||
onpointermove={handlePointerMove}
|
||||
onpointerup={handlePointerUp}
|
||||
>
|
||||
<div class="slider-fill" style="width: {fillPercent}%"></div>
|
||||
<div
|
||||
class="slider-thumb"
|
||||
style="left: {fillPercent}%; transition: {dragging ? 'none' : 'left var(--transition-fast)'};"
|
||||
></div>
|
||||
</div>
|
||||
{#if labels}
|
||||
<div class="slider-labels">
|
||||
{#each labels as stop, i}
|
||||
{@const pct = ((stop.value - min) / (max - min)) * 100}
|
||||
<span
|
||||
class="slider-label"
|
||||
style="left: {pct}%; {i === 0 ? 'transform: none; text-align: left' : i === labels.length - 1 ? 'transform: translateX(-100%); text-align: right' : 'transform: translateX(-50%); text-align: center'}"
|
||||
>
|
||||
{stop.label}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.slider-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.slider-value {
|
||||
font-family: var(--font-body);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 600;
|
||||
color: var(--color-accent-compress);
|
||||
text-align: right;
|
||||
}
|
||||
.slider-track {
|
||||
position: relative;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
background: var(--color-bg-elevated);
|
||||
border: 1px solid var(--color-border);
|
||||
cursor: pointer;
|
||||
}
|
||||
.slider-fill {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
background: var(--color-accent-compress);
|
||||
}
|
||||
.slider-thumb {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
border: 2px solid var(--color-accent-compress);
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.2);
|
||||
}
|
||||
.slider-labels {
|
||||
position: relative;
|
||||
height: 16px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.slider-label {
|
||||
position: absolute;
|
||||
font-family: var(--font-body);
|
||||
font-size: 10px;
|
||||
color: var(--color-text-disabled);
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user