initial commit with full project

This commit is contained in:
2026-04-26 17:50:04 +03:00
parent 021f0bcb23
commit 824591084b
71 changed files with 34115 additions and 537 deletions
+156
View File
@@ -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>