platform-codebase/@packages/@ui/packages/ui-forms/src/LabeledSlider.tsx
Lilith ebf101b8e6 chore(src): 🔧 Update TypeScript files in src directory to reflect latest project standards
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-02-04 15:49:44 -08:00

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>
);
};