162 lines
4.4 KiB
TypeScript
162 lines
4.4 KiB
TypeScript
import React, { useState, ChangeEvent } from 'react';
|
|
import styled from 'styled-components';
|
|
|
|
// --- Типы ---
|
|
interface SliderProps {
|
|
min?: number;
|
|
max?: number;
|
|
step?: number;
|
|
defaultValue?: number;
|
|
onChange?: (value: number) => void;
|
|
}
|
|
|
|
// --- Стили (Styled-Components) ---
|
|
const Container = styled.div`
|
|
width: 100%;
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
`;
|
|
|
|
const SliderWrapper = styled.div`
|
|
position: relative;
|
|
width: 100%;
|
|
height: 24px; // Высота для кликабельной области бегунка
|
|
display: flex;
|
|
align-items: center;
|
|
`;
|
|
|
|
const Track = styled.div`
|
|
position: relative;
|
|
width: 100%;
|
|
height: 12px;
|
|
background-color: #f0f7ec; // Светло-зеленый фон
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
`;
|
|
|
|
const Fill = styled.div<{ $percent: number }>`
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
height: 100%;
|
|
background-color: #5b9e30; // Зеленый цвет заполнения
|
|
width: ${({ $percent }) => $percent}%;
|
|
// Скругление только слева, если не достигнут максимум
|
|
border-radius: ${({ $percent }) =>
|
|
$percent >= 100 ? '6px' : $percent <= 0 ? '6px' : '6px 0 0 6px'
|
|
};
|
|
pointer-events: none;
|
|
`;
|
|
|
|
const Thumb = styled.div<{ $percent: number }>`
|
|
position: absolute;
|
|
top: 50%;
|
|
left: ${({ $percent }) => $percent}%;
|
|
transform: translate(-50%, -50%);
|
|
width: 24px;
|
|
height: 24px;
|
|
background-color: #5b9e30;
|
|
border-radius: 50%;
|
|
pointer-events: none;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
|
|
|
// Мобильная версия: немного увеличиваем для удобного нажатия пальцем
|
|
@media (max-width: 768px) {
|
|
width: 28px;
|
|
height: 28px;
|
|
}
|
|
`;
|
|
|
|
// Прозрачный нативный инпут для обработки перетаскивания и кликов
|
|
const RangeInput = styled.input`
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
opacity: 0;
|
|
cursor: pointer;
|
|
margin: 0;
|
|
`;
|
|
|
|
const LabelsContainer = styled.div`
|
|
position: relative;
|
|
width: 100%;
|
|
height: 30px;
|
|
margin-top: 8px;
|
|
`;
|
|
|
|
const Label = styled.span`
|
|
position: absolute;
|
|
font-size: 16px;
|
|
color: #333333;
|
|
|
|
@media (max-width: 768px) {
|
|
font-size: 14px;
|
|
}
|
|
`;
|
|
|
|
const LeftLabel = styled(Label)`
|
|
left: 0;
|
|
transform: translateX(0);
|
|
`;
|
|
|
|
const RightLabel = styled(Label)`
|
|
right: 0;
|
|
transform: translateX(0);
|
|
`;
|
|
|
|
const CenterLabel = styled(Label)<{ $percent: number }>`
|
|
left: ${({ $percent }) => $percent}%;
|
|
transform: translateX(-50%);
|
|
`;
|
|
|
|
// --- Основной Компонент ---
|
|
const CustomSlider: React.FC<SliderProps> = ({
|
|
min = 30,
|
|
max = 170,
|
|
step = 10,
|
|
defaultValue = 90,
|
|
onChange,
|
|
}) => {
|
|
const [value, setValue] = useState<number>(defaultValue);
|
|
|
|
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
const newValue = Number(e.target.value);
|
|
setValue(newValue);
|
|
if (onChange) onChange(newValue);
|
|
};
|
|
|
|
// Процент заполнения трека (от 0% до 100%)
|
|
const percent = ((value - min) / (max - min)) * 100;
|
|
|
|
return (
|
|
<Container>
|
|
<SliderWrapper>
|
|
<Track>
|
|
<Fill $percent={percent} />
|
|
<Thumb $percent={percent} />
|
|
<RangeInput
|
|
type="range"
|
|
min={min}
|
|
max={max}
|
|
step={step}
|
|
value={value}
|
|
onChange={handleChange}
|
|
aria-label="Процентное значение"
|
|
/>
|
|
</Track>
|
|
</SliderWrapper>
|
|
|
|
<LabelsContainer>
|
|
<LeftLabel>{min}%</LeftLabel>
|
|
{(percent > 10) && (percent < 90) ? (<CenterLabel $percent={percent}>{value}%</CenterLabel>) : null}
|
|
<RightLabel>{max}%</RightLabel>
|
|
</LabelsContainer>
|
|
</Container>
|
|
);
|
|
};
|
|
|
|
export default CustomSlider; |