ltmarx / web /src /components /StrengthSlider.tsx
harelcain's picture
Upload 37 files
f2f99a3 verified
import type { PresetName } from '@core/types.js';
const PRESET_MARKS: Array<{ value: number; label: string; emoji: string; preset: PresetName }> = [
{ value: 0.00, label: 'Light', emoji: '🌤️', preset: 'light' },
{ value: 0.33, label: 'Moderate', emoji: '⚡', preset: 'moderate' },
{ value: 0.67, label: 'Strong', emoji: '🛡️', preset: 'strong' },
{ value: 1.00, label: 'Fortress', emoji: '🏰', preset: 'fortress' },
];
interface StrengthSliderProps {
value: number;
onChange: (value: number, preset: PresetName) => void;
disabled?: boolean;
}
export default function StrengthSlider({ value, onChange, disabled }: StrengthSliderProps) {
const nearestPreset = PRESET_MARKS.reduce((best, mark) =>
Math.abs(mark.value - value) < Math.abs(best.value - value) ? mark : best
);
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<label className="text-sm font-medium text-zinc-300">Strength</label>
<span className="text-xs tabular-nums text-zinc-500">
{nearestPreset.emoji} {nearestPreset.label}
</span>
</div>
<div className="relative">
<input
type="range"
min="0"
max="1"
step="0.01"
value={value}
disabled={disabled}
onChange={(e) => {
const v = parseFloat(e.target.value);
const snap = PRESET_MARKS.find((m) => Math.abs(m.value - v) < 0.04);
if (snap) {
onChange(snap.value, snap.preset);
} else {
// Snap to nearest preset (no custom mode)
const nearest = PRESET_MARKS.reduce((best, mark) =>
Math.abs(mark.value - v) < Math.abs(best.value - v) ? mark : best
);
onChange(nearest.value, nearest.preset);
}
}}
className="w-full h-1.5 rounded-full appearance-none cursor-pointer
bg-zinc-700 accent-blue-500
disabled:opacity-50 disabled:cursor-not-allowed
[&::-webkit-slider-thumb]:appearance-none
[&::-webkit-slider-thumb]:w-4
[&::-webkit-slider-thumb]:h-4
[&::-webkit-slider-thumb]:rounded-full
[&::-webkit-slider-thumb]:bg-blue-500
[&::-webkit-slider-thumb]:shadow-lg
[&::-webkit-slider-thumb]:shadow-blue-500/20
[&::-webkit-slider-thumb]:transition-transform
[&::-webkit-slider-thumb]:hover:scale-110"
/>
<div className="relative mt-2 h-5">
{PRESET_MARKS.map((mark, i) => {
const pct = mark.value * 100;
const isFirst = i === 0;
const isLast = i === PRESET_MARKS.length - 1;
const align = isFirst ? 'left-0' : isLast ? 'right-0' : '-translate-x-1/2';
return (
<button
key={mark.preset}
onClick={() => onChange(mark.value, mark.preset)}
disabled={disabled}
style={isFirst || isLast ? undefined : { left: `${pct}%` }}
className={`absolute whitespace-nowrap text-[10px] transition-colors ${align} ${
nearestPreset.preset === mark.preset
? 'text-blue-400 font-medium'
: 'text-zinc-600 hover:text-zinc-400'
} disabled:opacity-50`}
>
{mark.emoji} {mark.label}
</button>
);
})}
</div>
</div>
</div>
);
}