diff --git a/components/ConfirmDialog.tsx b/components/ConfirmDialog.tsx new file mode 100644 index 0000000..9e40e45 --- /dev/null +++ b/components/ConfirmDialog.tsx @@ -0,0 +1,90 @@ +import React, { useEffect, useRef } from 'react'; +import { AlertTriangle } from 'lucide-react'; +import { useI18n } from '../context/I18nContext'; + +interface ConfirmDialogProps { + isOpen: boolean; + title: string; + message: string; + confirmLabel?: string; + cancelLabel?: string; + danger?: boolean; + onConfirm: () => void; + onCancel: () => void; +} + +export const ConfirmDialog: React.FC = ({ + isOpen, + title, + message, + confirmLabel, + cancelLabel, + danger = true, + onConfirm, + onCancel, +}) => { + const { t } = useI18n(); + const dialogRef = useRef(null); + + useEffect(() => { + if (!isOpen) return; + const handleEscape = (e: KeyboardEvent) => { + if (e.key === 'Escape') onCancel(); + }; + document.addEventListener('keydown', handleEscape); + return () => document.removeEventListener('keydown', handleEscape); + }, [isOpen, onCancel]); + + if (!isOpen) return null; + + return ( +
{ if (e.target === e.currentTarget) onCancel(); }} + > +
+
+ {danger && ( +
+ +
+ )} +
+

+ {title} +

+

+ {message} +

+
+
+ +
+ + +
+
+
+ ); +}; diff --git a/components/EditableSlider.tsx b/components/EditableSlider.tsx new file mode 100644 index 0000000..91e3941 --- /dev/null +++ b/components/EditableSlider.tsx @@ -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 = ({ + 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) => { + 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) => { + 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 ( +
+
+ + {isEditing ? ( + 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" + /> + ) : ( + 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} + + )} +
+ 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 && ( +

{helpText}

+ )} +
+ ); +}; diff --git a/vite-env.d.ts b/vite-env.d.ts new file mode 100644 index 0000000..8fc53b8 --- /dev/null +++ b/vite-env.d.ts @@ -0,0 +1,12 @@ +/// + +// Declare .txt file imports as raw text +declare module '*.txt' { + const content: string; + export default content; +} + +declare module '*.txt?raw' { + const content: string; + export default content; +}