init
This commit is contained in:
commit
c6863f2bfc
34 files changed
+6909
No files matched your search
@@ -0,0 +1,228 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: number
|
||||
min?: number
|
||||
max?: number
|
||||
step?: number
|
||||
format?: (v: number) => string
|
||||
disabled?: boolean
|
||||
}>(),
|
||||
{
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.01,
|
||||
format: undefined,
|
||||
disabled: false,
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: number] }>()
|
||||
|
||||
const trackRef = ref<HTMLDivElement | null>(null)
|
||||
const dragging = ref(false)
|
||||
|
||||
const fillPercent = computed(() => {
|
||||
const range = props.max - props.min
|
||||
if (range <= 0) return 0
|
||||
const clamped = Math.max(props.min, Math.min(props.max, props.modelValue))
|
||||
return ((clamped - props.min) / range) * 100
|
||||
})
|
||||
|
||||
const formattedValue = computed(() => {
|
||||
if (props.format) return props.format(props.modelValue)
|
||||
return String(props.modelValue)
|
||||
})
|
||||
|
||||
function snap(value: number): number {
|
||||
const s = props.step
|
||||
if (s <= 0) return value
|
||||
const snapped = Math.round(value / s) * s
|
||||
const decimals = (s.toString().split('.')[1] || '').length
|
||||
return decimals > 0 ? parseFloat(snapped.toFixed(decimals)) : snapped
|
||||
}
|
||||
|
||||
function clamp(value: number): number {
|
||||
return Math.max(props.min, Math.min(props.max, value))
|
||||
}
|
||||
|
||||
function updateFromClientX(clientX: number) {
|
||||
const track = trackRef.value
|
||||
if (!track) return
|
||||
const rect = track.getBoundingClientRect()
|
||||
if (rect.width <= 0) return
|
||||
const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width))
|
||||
const raw = props.min + ratio * (props.max - props.min)
|
||||
emit('update:modelValue', clamp(snap(raw)))
|
||||
}
|
||||
|
||||
function onPointerDown(e: PointerEvent) {
|
||||
if (props.disabled) return
|
||||
if (e.button !== 0) return
|
||||
const el = e.currentTarget as HTMLElement
|
||||
el.setPointerCapture(e.pointerId)
|
||||
el.focus()
|
||||
dragging.value = true
|
||||
updateFromClientX(e.clientX)
|
||||
}
|
||||
|
||||
function onPointerMove(e: PointerEvent) {
|
||||
const el = e.currentTarget as HTMLElement
|
||||
if (!el.hasPointerCapture(e.pointerId)) return
|
||||
updateFromClientX(e.clientX)
|
||||
}
|
||||
|
||||
function onPointerUp(e: PointerEvent) {
|
||||
const el = e.currentTarget as HTMLElement
|
||||
if (el.hasPointerCapture(e.pointerId)) el.releasePointerCapture(e.pointerId)
|
||||
dragging.value = false
|
||||
}
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (props.disabled) return
|
||||
const big = e.shiftKey ? 10 : 1
|
||||
let next = props.modelValue
|
||||
switch (e.key) {
|
||||
case 'ArrowLeft':
|
||||
case 'ArrowDown':
|
||||
next -= props.step * big
|
||||
break
|
||||
case 'ArrowRight':
|
||||
case 'ArrowUp':
|
||||
next += props.step * big
|
||||
break
|
||||
case 'Home':
|
||||
next = props.min
|
||||
break
|
||||
case 'End':
|
||||
next = props.max
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
e.preventDefault()
|
||||
emit('update:modelValue', clamp(snap(next)))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="slider"
|
||||
:class="{ disabled, dragging }"
|
||||
:tabindex="disabled ? -1 : 0"
|
||||
role="slider"
|
||||
:aria-valuemin="min"
|
||||
:aria-valuemax="max"
|
||||
:aria-valuenow="modelValue"
|
||||
:aria-disabled="disabled"
|
||||
@pointerdown="onPointerDown"
|
||||
@pointermove="onPointerMove"
|
||||
@pointerup="onPointerUp"
|
||||
@pointercancel="onPointerUp"
|
||||
@keydown="onKeyDown"
|
||||
>
|
||||
<div ref="trackRef" class="track">
|
||||
<div class="track-bg"></div>
|
||||
<div class="track-fill" :style="{ width: fillPercent + '%' }"></div>
|
||||
<div class="thumb" :style="{ left: fillPercent + '%' }">
|
||||
<div class="thumb-dot"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="value">{{ formattedValue }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.slider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 6px 2px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.slider.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.track {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
height: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.track-bg {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: var(--border);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.track-fill {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
height: 4px;
|
||||
background: var(--accent);
|
||||
border-radius: 999px;
|
||||
transition: background 140ms var(--ease);
|
||||
}
|
||||
|
||||
.thumb {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.thumb-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--text);
|
||||
border: 2px solid var(--accent);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
|
||||
transition: transform 140ms var(--ease), box-shadow 140ms var(--ease),
|
||||
background 140ms var(--ease);
|
||||
}
|
||||
|
||||
.slider:hover:not(.disabled) .thumb-dot {
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
.slider:focus-visible:not(.disabled) .thumb-dot {
|
||||
box-shadow: 0 0 0 3px var(--accent-soft-strong), 0 1px 4px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.slider.dragging .thumb-dot {
|
||||
transform: scale(1.15);
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.value {
|
||||
min-width: 44px;
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-size: 12px;
|
||||
color: var(--text-dim-strong);
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.slider:hover:not(.disabled) .value {
|
||||
color: var(--text);
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user