Files
image-studio/view/bottom-controls/ColorPicker.tsx
syntaxbullet e8073e6146 feat: enhance bottom controls and UI components
- Updated BottomControlsIsland for improved layout and styling.
- Refactored LayersSheet to enhance usability and visual consistency.
- Modified ToolOverlay for better tool selection experience.
- Improved BrushControls with new ColorPicker and Slider components.
- Enhanced PanControls and TransformControls for better user interaction.
- Updated ZoomControls for consistent button sizes and styles.
- Introduced new styles for bottom controls in styles.ts and index.css.
- Added BottomControlColorPicker, BottomControlSelectMenu, and BottomControlSlider components for better modularity and reusability.
2026-07-03 22:28:34 +02:00

83 lines
2.6 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-10 place-items-center rounded-full border border-white/40 bg-transparent text-white transition hover:border-white focus:outline-none focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-white"
aria-label={props["aria-label"]}
title={value}
onClick={() => inputRef.current?.click()}
>
<span className="size-6 rounded-full border border-white/25" 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-10 w-24 bg-transparent px-2 font-mono text-base uppercase text-white outline-none transition placeholder:text-white/30 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;
}