Files
Vectrace/src/components/vertical-slider.tsx
T
2026-06-29 09:41:02 +02:00

138 lines
2.9 KiB
TypeScript

import React, { useState } from "react";
import styled from "styled-components";
const SliderWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 120px;
width: 40px;
gap: 8px;
`;
interface SliderProps {
min?: number;
max?: number;
step?: number;
value?: number;
onChange?: (value: number) => void;
}
interface TrackTransientProps {
$fillPercent: number;
}
const Track = styled.input.attrs({ type: "range" }) <TrackTransientProps>`
appearance: none;
-webkit-appearance: none;
writing-mode: vertical-lr;
direction: rtl;
width: 6px;
height: 100%;
background: linear-gradient(
to top,
white ${({ $fillPercent }) => $fillPercent}%,
var(--secondary-color) ${({ $fillPercent }) => $fillPercent}%
);
border-radius: 0;
outline: none;
cursor: pointer;
transition: background 0.15s ease;
&::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 22px;
height: 22px;
border-radius: 0;
background: #ffffff;
border: 3px solid var(--secondary-color);
box-shadow: 0 0 6px rgba(255, 68, 68, 0.6);
cursor: grab;
transition: transform 0.1s ease, box-shadow 0.1s ease;
&:active {
cursor: grabbing;
transform: scale(1.2);
box-shadow: 0 0 12px rgba(255, 68, 68, 0.9);
}
}
&::-moz-range-thumb {
width: 22px;
height: 22px;
border-radius: 0;
background: #ffffff;
border: 3px solid #ff4444;
box-shadow: 0 0 6px rgba(255, 68, 68, 0.6);
cursor: grab;
transition: transform 0.1s ease;
&:active {
cursor: grabbing;
transform: scale(1.2);
}
}
&::-moz-range-track {
background: transparent;
}
`;
const ValueLabel = styled.span`
font-size: 13px;
color: white;
letter-spacing: 0.05em;
`;
interface SliderProps {
min?: number;
max?: number;
step?: number;
value?: number;
onLabelClick?: () => void;
onChange?: (value: number) => void;
}
export function VerticalSlider({
min = 0,
max = 100,
step = 1,
onLabelClick,
value: controlledValue,
onChange,
}: SliderProps) {
const [internalValue, setInternalValue] = useState(
controlledValue ?? Math.floor((max - min) / 2)
);
const value = controlledValue ?? internalValue;
const fillPercent = ((value - min) / (max - min)) * 100;
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const next = Number(e.target.value);
setInternalValue(next);
onChange?.(next);
};
return (
<SliderWrapper>
<ValueLabel onDoubleClick={() => {
onLabelClick?.();
}}>{value}</ValueLabel>
<Track
min={min}
max={max}
step={step}
value={value}
$fillPercent={fillPercent}
onChange={handleChange}
/>
</SliderWrapper>
);
};