- 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.
81 lines
3.1 KiB
TypeScript
81 lines
3.1 KiB
TypeScript
import { Check, CaretDown } from "@phosphor-icons/react";
|
|
import { useEffect, useRef, useState } from "react";
|
|
|
|
export type BottomControlSelectOption<TValue extends string> = {
|
|
value: TValue;
|
|
label: string;
|
|
};
|
|
|
|
export type BottomControlSelectMenuProps<TValue extends string> = {
|
|
value: TValue;
|
|
options: readonly BottomControlSelectOption<TValue>[];
|
|
"aria-label": string;
|
|
onValueChange: (value: TValue) => void;
|
|
};
|
|
|
|
export function BottomControlSelectMenu<TValue extends string>({ value, options, onValueChange, ...props }: BottomControlSelectMenuProps<TValue>) {
|
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
const [open, setOpen] = useState(false);
|
|
const selectedOption = options.find((option) => option.value === value) ?? options[0];
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
|
|
const handlePointerDown = (event: PointerEvent) => {
|
|
if (!rootRef.current?.contains(event.target as Node)) setOpen(false);
|
|
};
|
|
|
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
if (event.key === "Escape") setOpen(false);
|
|
};
|
|
|
|
window.addEventListener("pointerdown", handlePointerDown);
|
|
window.addEventListener("keydown", handleKeyDown);
|
|
return () => {
|
|
window.removeEventListener("pointerdown", handlePointerDown);
|
|
window.removeEventListener("keydown", handleKeyDown);
|
|
};
|
|
}, [open]);
|
|
|
|
return (
|
|
<div ref={rootRef} className="relative">
|
|
<button
|
|
type="button"
|
|
className="inline-flex h-10 min-w-32 items-center rounded-full text-base text-white/85 transition hover:bg-white/10 hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-white/30"
|
|
aria-label={props["aria-label"]}
|
|
aria-haspopup="listbox"
|
|
aria-expanded={open}
|
|
onClick={() => setOpen((current) => !current)}
|
|
>
|
|
<span className="min-w-0 flex-1 px-4 text-left">{selectedOption?.label}</span>
|
|
<span className="grid size-10 flex-none place-items-center text-white/60">
|
|
<CaretDown size={18} weight="bold" />
|
|
</span>
|
|
</button>
|
|
{open ? (
|
|
<div className="absolute bottom-full left-0 z-30 mb-3 min-w-full rounded-[1.5rem] p-1 text-white backdrop-blur-xl" role="listbox" aria-label={props["aria-label"]}>
|
|
{options.map((option) => {
|
|
const selected = option.value === value;
|
|
return (
|
|
<button
|
|
key={option.value}
|
|
type="button"
|
|
className={`flex h-10 w-full items-center gap-3 rounded-full px-3 text-left text-sm transition ${selected ? "bg-white text-black" : "text-white/80 hover:bg-white/10 hover:text-white"}`}
|
|
role="option"
|
|
aria-selected={selected}
|
|
onClick={() => {
|
|
onValueChange(option.value);
|
|
setOpen(false);
|
|
}}
|
|
>
|
|
<span className="grid size-5 place-items-center">{selected ? <Check size={16} weight="bold" /> : null}</span>
|
|
<span className="min-w-0 flex-1 whitespace-nowrap">{option.label}</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|