/** * 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 * * * @example * // Custom value format * `${v}%`} * /> */ export const LabeledSlider: FC = ({ 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 ( {leftLabel && {leftLabel}} onChange(parseFloat(e.target.value))} disabled={disabled} $fillPercent={fillPercent} /> {rightLabel && {rightLabel}} {showValue && {formatValue(value)}} ); };