167 lines
4.2 KiB
TypeScript
Executable file
167 lines
4.2 KiB
TypeScript
Executable file
/**
|
|
* LabeledSlider Component
|
|
*
|
|
* A range slider with optional left/right labels and value display.
|
|
* Theme-agnostic with semantic token usage.
|
|
*/
|
|
|
|
import type React from 'react';
|
|
|
|
import styled, { type DefaultTheme } from '@lilith/ui-styled-components';
|
|
|
|
|
|
export interface LabeledSliderProps {
|
|
/** Current value */
|
|
value: number;
|
|
/** Change handler */
|
|
onChange: (value: number) => void;
|
|
/** Minimum value (default: 0) */
|
|
min?: number;
|
|
/** Maximum value (default: 1) */
|
|
max?: number;
|
|
/** Step increment (default: 0.1) */
|
|
step?: number;
|
|
/** Label shown on the left */
|
|
leftLabel?: string;
|
|
/** Label shown on the right */
|
|
rightLabel?: string;
|
|
/** Show current value (default: true) */
|
|
showValue?: boolean;
|
|
/** Format function for value display */
|
|
formatValue?: (value: number) => string;
|
|
/** Disabled state */
|
|
disabled?: boolean;
|
|
/** Optional className */
|
|
className?: string;
|
|
}
|
|
|
|
const Container = styled.div`
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: ${(props: { theme: DefaultTheme }) => props.theme.spacing.sm};
|
|
`;
|
|
|
|
const SliderRow = styled.div`
|
|
display: flex;
|
|
align-items: center;
|
|
gap: ${(props: { theme: DefaultTheme }) => props.theme.spacing.md};
|
|
`;
|
|
|
|
const SliderLabel = styled.span`
|
|
font-size: ${(props: { theme: DefaultTheme }) => props.theme.typography.fontSize.xs};
|
|
color: ${(props: { theme: DefaultTheme }) => props.theme.colors.text.secondary};
|
|
min-width: 60px;
|
|
|
|
&:last-of-type {
|
|
text-align: right;
|
|
}
|
|
`;
|
|
|
|
const SliderInput = styled.input<{ $fillPercent: number }>`
|
|
flex: 1;
|
|
height: 6px;
|
|
-webkit-appearance: none;
|
|
appearance: none;
|
|
background: ${(props: { $fillPercent: number; theme: DefaultTheme }) => {
|
|
const fill = props.theme.colors.primary;
|
|
const track = props.theme.colors.border;
|
|
return `linear-gradient(to right, ${fill} 0%, ${fill} ${props.$fillPercent}%, ${track} ${props.$fillPercent}%, ${track} 100%)`;
|
|
}};
|
|
border-radius: ${(props: { theme: DefaultTheme }) => props.theme.borderRadius.full};
|
|
cursor: pointer;
|
|
|
|
&::-webkit-slider-thumb {
|
|
-webkit-appearance: none;
|
|
appearance: none;
|
|
width: 16px;
|
|
height: 16px;
|
|
background: ${(props: { theme: DefaultTheme }) => props.theme.colors.primary};
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
transition: transform ${(props: { theme: DefaultTheme }) => props.theme.transitions.fast};
|
|
|
|
&:hover {
|
|
transform: scale(1.1);
|
|
}
|
|
}
|
|
|
|
&::-moz-range-thumb {
|
|
width: 16px;
|
|
height: 16px;
|
|
background: ${(props: { theme: DefaultTheme }) => props.theme.colors.primary};
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
border: none;
|
|
}
|
|
|
|
&:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
`;
|
|
|
|
const ValueDisplay = styled.span`
|
|
font-size: ${(props: { theme: DefaultTheme }) => props.theme.typography.fontSize.sm};
|
|
font-weight: ${(props: { theme: DefaultTheme }) => props.theme.typography.fontWeight.medium};
|
|
color: ${(props: { theme: DefaultTheme }) => props.theme.colors.text.primary};
|
|
min-width: 40px;
|
|
text-align: center;
|
|
`;
|
|
|
|
/**
|
|
* A range slider with optional labels and value display.
|
|
*
|
|
* @example
|
|
* // Basic slider with percentage display
|
|
* <LabeledSlider
|
|
* value={0.5}
|
|
* onChange={setValue}
|
|
* leftLabel="Low"
|
|
* rightLabel="High"
|
|
* />
|
|
*
|
|
* @example
|
|
* // Custom value format
|
|
* <LabeledSlider
|
|
* value={50}
|
|
* onChange={setValue}
|
|
* min={0}
|
|
* max={100}
|
|
* formatValue={(v) => `${v}%`}
|
|
* />
|
|
*/
|
|
export const LabeledSlider: FC<LabeledSliderProps> = ({
|
|
value,
|
|
onChange,
|
|
min = 0,
|
|
max = 1,
|
|
step = 0.1,
|
|
leftLabel,
|
|
rightLabel,
|
|
showValue = true,
|
|
formatValue = (v) => `${Math.round(v * 100)}%`,
|
|
disabled = false,
|
|
className,
|
|
}) => {
|
|
const fillPercent = ((value - min) / (max - min)) * 100;
|
|
|
|
return (
|
|
<Container className={className}>
|
|
<SliderRow>
|
|
{leftLabel && <SliderLabel>{leftLabel}</SliderLabel>}
|
|
<SliderInput
|
|
type="range"
|
|
min={min}
|
|
max={max}
|
|
step={step}
|
|
value={value}
|
|
onChange={(e) => onChange(parseFloat(e.target.value))}
|
|
disabled={disabled}
|
|
$fillPercent={fillPercent}
|
|
/>
|
|
{rightLabel && <SliderLabel>{rightLabel}</SliderLabel>}
|
|
{showValue && <ValueDisplay>{formatValue(value)}</ValueDisplay>}
|
|
</SliderRow>
|
|
</Container>
|
|
);
|
|
};
|