Files
image-studio/view/bottom-controls/ColorPicker.tsx
syntaxbullet 5915c62a9a refactor: Update bottom controls for improved styling and functionality
- Adjusted button styles across various components for consistency and better UX.
- Enhanced layout of action controls to utilize whitespace more effectively.
- Updated slider styles for a more modern appearance and improved usability.
- Refined input fields and labels for better accessibility and readability.
- Introduced new app surface styles for a cohesive design across the application.
- Added tests for canvas cursor behavior to ensure correct cursor display during operations.
2026-07-11 14:55:32 +02:00

83 lines
2.5 KiB
TypeScript

import { useEffect, useRef, useState } from "react";
export type BottomControlColorPickerProps = {
value: string;
"aria-label": string;
onValueChange: (value: string) => void;
};
export function BottomControlColorPicker({ value, onValueChange, ...props }: BottomControlColorPickerProps) {
const inputRef = useRef<HTMLInputElement>(null);
const [draft, setDraft] = useState(value);
useEffect(() => {
setDraft(value);
}, [value]);
const commitDraft = () => {
const normalized = normalizeHexColor(draft);
if (!normalized) {
setDraft(value);
return;
}
setDraft(normalized);
if (normalized !== value) onValueChange(normalized);
};
return (
<div className="flex items-center gap-3">
<button
type="button"
className="grid size-8 place-items-center rounded-md border border-white/20 bg-white/[0.03] text-white transition hover:border-white/40 focus:outline-none focus-visible:ring-2 focus-visible:ring-sky-300/50"
aria-label={props["aria-label"]}
title={value}
onClick={() => inputRef.current?.click()}
>
<span className="size-5 rounded border border-white/20" style={{ backgroundColor: value }} />
</button>
<input
ref={inputRef}
type="color"
className="sr-only"
value={value}
aria-label={props["aria-label"]}
onChange={(event) => onValueChange(event.target.value)}
/>
<input
className="h-8 w-20 bg-transparent px-2 font-mono text-xs uppercase text-white outline-none transition placeholder:text-white/25 focus:text-white"
value={draft}
aria-label={`${props["aria-label"]} hex value`}
spellCheck={false}
onChange={(event) => setDraft(event.target.value)}
onBlur={commitDraft}
onFocus={(event) => event.currentTarget.select()}
onKeyDown={(event) => {
event.stopPropagation();
if (event.key === "Enter") {
commitDraft();
event.currentTarget.blur();
}
if (event.key === "Escape") {
setDraft(value);
event.currentTarget.blur();
}
}}
/>
</div>
);
}
function normalizeHexColor(value: string): string | undefined {
const trimmed = value.trim();
const hex = trimmed.startsWith("#") ? trimmed.slice(1) : trimmed;
if (/^[0-9a-fA-F]{3}$/.test(hex)) {
const [r, g, b] = hex;
return `#${r}${r}${g}${g}${b}${b}`.toLowerCase();
}
if (/^[0-9a-fA-F]{6}$/.test(hex)) return `#${hex}`.toLowerCase();
return undefined;
}