b588a70ecd
- Listview: Toolbar-Rahmen #407BFF, Fokus 2px ring-inset #235CC8 - Listview: Pfeil-Buttons Monatsauswahl in #85B7D7 - Listview: Klimawerte werden bei allen-0 ausgeblendet - Zeiteingabe: nur Stunden + Tab setzt Minuten automatisch auf :00 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
91 lines
2.3 KiB
TypeScript
91 lines
2.3 KiB
TypeScript
'use client';
|
||
|
||
import { useEffect, useState } from 'react';
|
||
|
||
interface Props {
|
||
value: string; // "HH:MM"
|
||
onChange: (value: string) => void;
|
||
className?: string;
|
||
clearOnFocus?: boolean;
|
||
autoFocus?: boolean;
|
||
}
|
||
|
||
function isValid(t: string): boolean {
|
||
if (!/^\d{1,2}:\d{2}$/.test(t)) return false;
|
||
const [h, m] = t.split(':').map(Number);
|
||
return h >= 0 && h <= 23 && m >= 0 && m <= 59;
|
||
}
|
||
|
||
function normalize(t: string): string {
|
||
const [h, m] = t.split(':').map(Number);
|
||
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
|
||
}
|
||
|
||
export default function TimeInput({ value, onChange, className = '', clearOnFocus = false, autoFocus = false }: Props) {
|
||
const [local, setLocal] = useState(value);
|
||
const [error, setError] = useState(false);
|
||
|
||
useEffect(() => {
|
||
setLocal(value);
|
||
setError(false);
|
||
}, [value]);
|
||
|
||
function handleChange(raw: string) {
|
||
setError(false);
|
||
// Auto-insert colon after the 2nd digit (only when adding, not deleting)
|
||
if (/^\d{2}$/.test(raw) && /^\d$/.test(local)) {
|
||
setLocal(raw + ':');
|
||
return;
|
||
}
|
||
setLocal(raw);
|
||
}
|
||
|
||
function handleFocus() {
|
||
if (clearOnFocus) {
|
||
setLocal('');
|
||
setError(false);
|
||
}
|
||
}
|
||
|
||
function handleBlur() {
|
||
if (local === '') {
|
||
setLocal(value);
|
||
setError(false);
|
||
return;
|
||
}
|
||
const expanded = /^\d{1,2}:?$/.test(local) ? local.replace(/:$/, '') + ':00' : local;
|
||
if (isValid(expanded)) {
|
||
const norm = normalize(expanded);
|
||
setLocal(norm);
|
||
setError(false);
|
||
onChange(norm);
|
||
} else {
|
||
setError(true);
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className={`relative ${className}`}>
|
||
<input
|
||
type="text"
|
||
inputMode="numeric"
|
||
value={local}
|
||
onChange={(e) => handleChange(e.target.value)}
|
||
onFocus={handleFocus}
|
||
onBlur={handleBlur}
|
||
autoFocus={autoFocus}
|
||
placeholder="HH:MM"
|
||
maxLength={5}
|
||
className={`w-full px-2 py-1 border-2 rounded-lg bg-white text-sm text-gray-900 font-mono text-center focus:outline-none ${
|
||
error ? 'border-red-500 focus:border-red-500' : 'border-gray-400 focus:border-blue-500'
|
||
}`}
|
||
/>
|
||
{error && (
|
||
<p className="absolute left-0 top-full mt-0.5 text-xs text-red-600 whitespace-nowrap z-10">
|
||
Ungültig (00:00 – 23:59)
|
||
</p>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|