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:
syntaxbullet
2026-07-03 22:28:34 +02:00
parent 4590b47e19
commit e8073e6146
16 changed files with 409 additions and 155 deletions

View File

@@ -0,0 +1,80 @@
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>
);
}