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.
This commit is contained in:
82
view/bottom-controls/ColorPicker.tsx
Normal file
82
view/bottom-controls/ColorPicker.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user