95 lines
2.7 KiB
TypeScript
95 lines
2.7 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from "react";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
interface ThemeSeedInputProps {
|
|
value: string;
|
|
onChange: (hex: string) => void;
|
|
className?: string;
|
|
}
|
|
|
|
function isValidHex(value: string): boolean {
|
|
return /^#[0-9a-fA-F]{6}$/.test(value);
|
|
}
|
|
|
|
export function ThemeSeedInput({ value, onChange, className }: ThemeSeedInputProps) {
|
|
const [hexText, setHexText] = useState(value);
|
|
const debounceRef = useRef<number | null>(null);
|
|
|
|
// Sync external value to internal text when it changes
|
|
useEffect(() => {
|
|
setHexText(value);
|
|
}, [value]);
|
|
|
|
const debouncedOnChange = useCallback(
|
|
(newHex: string) => {
|
|
if (debounceRef.current !== null) {
|
|
window.clearTimeout(debounceRef.current);
|
|
}
|
|
debounceRef.current = window.setTimeout(() => {
|
|
onChange(newHex);
|
|
debounceRef.current = null;
|
|
}, 150);
|
|
},
|
|
[onChange],
|
|
);
|
|
|
|
const handleColorChange = useCallback(
|
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const newHex = e.target.value;
|
|
setHexText(newHex);
|
|
debouncedOnChange(newHex);
|
|
},
|
|
[debouncedOnChange],
|
|
);
|
|
|
|
const handleTextChange = useCallback(
|
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const newVal = e.target.value;
|
|
setHexText(newVal);
|
|
if (isValidHex(newVal)) {
|
|
debouncedOnChange(newVal);
|
|
}
|
|
},
|
|
[debouncedOnChange],
|
|
);
|
|
|
|
const handleTextBlur = useCallback(() => {
|
|
// On blur, reset to valid value if text is invalid
|
|
if (!isValidHex(hexText)) {
|
|
setHexText(value);
|
|
}
|
|
}, [hexText, value]);
|
|
|
|
return (
|
|
<div className={cn("flex flex-col gap-2", className)}>
|
|
<Label htmlFor="seed-color" className="text-sm font-medium">
|
|
Seed color
|
|
</Label>
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
id="seed-color"
|
|
type="color"
|
|
value={isValidHex(hexText) ? hexText : "#000000"}
|
|
onChange={handleColorChange}
|
|
className="h-12 w-12 cursor-pointer rounded-md border border-input bg-transparent p-1 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
|
aria-label="Seed color picker"
|
|
/>
|
|
<Input
|
|
type="text"
|
|
value={hexText}
|
|
onChange={handleTextChange}
|
|
onBlur={handleTextBlur}
|
|
placeholder="#000000"
|
|
maxLength={7}
|
|
className="h-12 w-32 font-mono text-sm"
|
|
aria-label="Seed color hex value"
|
|
/>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
We'll generate a full palette in OKLCH.
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|