- 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.
83 lines
2.5 KiB
TypeScript
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;
|
|
}
|