platform-codebase/@packages/@ui/packages/ui-forms/src/WeightSlider.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

207 lines
5.6 KiB
TypeScript
Executable file

/**
* WeightSlider Component
*
* A 0-1 slider with color-coded feedback based on value.
* Useful for priority/severity weighting controls.
* Theme-agnostic with semantic token usage.
*/
import type React from 'react';
import styled, { type DefaultTheme } from '@lilith/ui-styled-components';
export interface WeightSliderProps {
/** Current value (0-1) */
value: number;
/** Change handler */
onChange: (value: number) => void;
/** Default value for reset (optional) */
defaultValue?: number;
/** Disabled state */
disabled?: boolean;
/** Show reset button when value differs from default (default: true) */
showReset?: boolean;
/** Step increment (default: 0.05) */
step?: number;
/** Custom color thresholds (optional) */
colorThresholds?: {
critical: number;
high: number;
medium: number;
};
/** Optional className */
className?: string;
}
const Container = styled.div`
display: flex;
align-items: center;
gap: ${(props: { theme: DefaultTheme }) => props.theme.spacing.md};
`;
const SliderWrapper = styled.div`
flex: 1;
display: flex;
align-items: center;
gap: ${(props: { theme: DefaultTheme }) => props.theme.spacing.sm};
`;
const SliderInput = styled.input<{ $fillPercent: number; $color: string }>`
flex: 1;
height: 6px;
-webkit-appearance: none;
appearance: none;
background: ${(props: { $fillPercent: number; $color: string; theme: DefaultTheme }) => {
const track = props.theme.colors.border;
return `linear-gradient(to right, ${props.$color} 0%, ${props.$color} ${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: 14px;
height: 14px;
background: ${(props: { $color: string }) => props.$color};
border-radius: 50%;
cursor: pointer;
transition: transform ${(props: { theme: DefaultTheme }) => props.theme.transitions.fast};
&:hover {
transform: scale(1.1);
}
}
&::-moz-range-thumb {
width: 14px;
height: 14px;
background: ${(props: { $color: string }) => props.$color};
border-radius: 50%;
cursor: pointer;
border: none;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
`;
const ValueDisplay = styled.span<{ $color: string }>`
font-size: ${(props: { theme: DefaultTheme }) => props.theme.typography.fontSize.sm};
font-weight: ${(props: { theme: DefaultTheme }) => props.theme.typography.fontWeight.semibold};
color: ${(props) => props.$color};
min-width: 45px;
text-align: center;
`;
const ResetButton = styled.button<{ $visible: boolean }>`
padding: ${(props: { theme: DefaultTheme }) => props.theme.spacing.xs}
${(props: { theme: DefaultTheme }) => props.theme.spacing.sm};
background: transparent;
border: 1px solid ${(props: { theme: DefaultTheme }) => props.theme.colors.border};
border-radius: ${(props: { theme: DefaultTheme }) => props.theme.borderRadius.xs};
font-size: ${(props: { theme: DefaultTheme }) => props.theme.typography.fontSize.xs};
color: ${(props: { theme: DefaultTheme }) => props.theme.colors.text.secondary};
cursor: pointer;
opacity: ${(props) => (props.$visible ? 1 : 0)};
pointer-events: ${(props) => (props.$visible ? 'auto' : 'none')};
transition: all ${(props: { theme: DefaultTheme }) => props.theme.transitions.fast};
&:hover:not(:disabled) {
border-color: ${(props: { theme: DefaultTheme }) => props.theme.colors.primary};
color: ${(props: { theme: DefaultTheme }) => props.theme.colors.primary};
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
`;
const DEFAULT_THRESHOLDS = {
critical: 0.8,
high: 0.6,
medium: 0.4,
};
const DEFAULT_COLORS = {
critical: '#ef4444', // Red
high: '#f97316', // Orange
medium: '#eab308', // Yellow
low: '#22c55e', // Green
};
function getWeightColor(
value: number,
thresholds: typeof DEFAULT_THRESHOLDS = DEFAULT_THRESHOLDS,
): string {
if (value >= thresholds.critical) {return DEFAULT_COLORS.critical;}
if (value >= thresholds.high) {return DEFAULT_COLORS.high;}
if (value >= thresholds.medium) {return DEFAULT_COLORS.medium;}
return DEFAULT_COLORS.low;
}
/**
* A 0-1 slider with color-coded feedback.
*
* @example
* // Basic weight slider
* <WeightSlider
* value={0.5}
* onChange={setWeight}
* />
*
* @example
* // With reset to default
* <WeightSlider
* value={weight}
* onChange={setWeight}
* defaultValue={0.5}
* showReset
* />
*/
export const WeightSlider: FC<WeightSliderProps> = ({
value,
onChange,
defaultValue,
disabled = false,
showReset = true,
step = 0.05,
colorThresholds = DEFAULT_THRESHOLDS,
className,
}) => {
const fillPercent = value * 100;
const color = getWeightColor(value, colorThresholds);
const hasChanged = defaultValue !== undefined && value !== defaultValue;
return (
<Container className={className}>
<SliderWrapper>
<SliderInput
type="range"
min={0}
max={1}
step={step}
value={value}
onChange={(e) => onChange(parseFloat(e.target.value))}
disabled={disabled}
$fillPercent={fillPercent}
$color={color}
/>
<ValueDisplay $color={color}>{value.toFixed(2)}</ValueDisplay>
</SliderWrapper>
{showReset && defaultValue !== undefined && (
<ResetButton
$visible={hasChanged}
onClick={() => onChange(defaultValue)}
disabled={disabled}
>
Reset
</ResetButton>
)}
</Container>
);
};