Add reusable UI components: ConfirmDialog, EditableSlider, Vite type declarations
- ConfirmDialog: modal confirm with Escape/backdrop dismiss, danger styling, i18n labels - EditableSlider: click-to-edit numeric slider with clamping and keyboard support - vite-env.d.ts: type declarations for .txt raw imports - Fixed missing title prop in EditableSlider interface from PR
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
interface EditableSliderProps {
|
||||
label: string;
|
||||
value: number;
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
onChange: (value: number) => void;
|
||||
formatDisplay?: (value: number) => string;
|
||||
helpText?: string;
|
||||
title?: string;
|
||||
autoLabel?: string;
|
||||
}
|
||||
|
||||
export const EditableSlider: React.FC<EditableSliderProps> = ({
|
||||
label,
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
onChange,
|
||||
formatDisplay,
|
||||
helpText,
|
||||
title = '',
|
||||
autoLabel = 'Auto',
|
||||
}) => {
|
||||
const [inputValue, setInputValue] = useState(value.toString());
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEditing) {
|
||||
setInputValue(value.toString());
|
||||
}
|
||||
}, [value, isEditing]);
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(e.target.value);
|
||||
};
|
||||
|
||||
const handleInputBlur = () => {
|
||||
setIsEditing(false);
|
||||
const numValue = parseFloat(inputValue);
|
||||
if (!isNaN(numValue)) {
|
||||
const clampedValue = Math.max(min, Math.min(max, numValue));
|
||||
onChange(clampedValue);
|
||||
setInputValue(clampedValue.toString());
|
||||
} else {
|
||||
setInputValue(value.toString());
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleInputBlur();
|
||||
} else if (e.key === 'Escape') {
|
||||
setInputValue(value.toString());
|
||||
setIsEditing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const displayValue = formatDisplay ? formatDisplay(value) : (value === min && autoLabel ? autoLabel : value.toString());
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-xs font-medium text-zinc-600 dark:text-zinc-400" title={title}>{label}</label>
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="number"
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
onBlur={handleInputBlur}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
onFocus={() => setIsEditing(true)}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
autoFocus
|
||||
className="text-xs font-mono text-zinc-900 dark:text-white bg-zinc-100 dark:bg-black/20 px-2 py-0.5 rounded w-20 text-right border border-pink-500 focus:outline-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
onClick={() => setIsEditing(true)}
|
||||
className="text-xs font-mono text-zinc-900 dark:text-white bg-zinc-100 dark:bg-black/20 px-2 py-0.5 rounded cursor-pointer hover:bg-zinc-200 dark:hover:bg-black/30 transition-colors"
|
||||
>
|
||||
{displayValue}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
value={value}
|
||||
onChange={(e) => onChange(Number(e.target.value))}
|
||||
className="w-full h-2 bg-zinc-200 dark:bg-zinc-700 rounded-lg appearance-none cursor-pointer accent-pink-500"
|
||||
/>
|
||||
{helpText && (
|
||||
<p className="text-[10px] text-zinc-500">{helpText}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user