138 lines
2.9 KiB
TypeScript
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>
|
|
);
|
|
};
|