Spaces:
Running
Running
| 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> | |
| ); | |
| } | |