- 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.
101 lines
3.2 KiB
TypeScript
101 lines
3.2 KiB
TypeScript
import { useEffect, useState, type Dispatch, type SetStateAction } from "react";
|
|
import { BoundingBox } from "@phosphor-icons/react";
|
|
import { commandIds } from "@commands/ids";
|
|
import type { Rect } from "@core/geometry";
|
|
import type { AppStore } from "@editor/store";
|
|
import type { TransformTarget } from "@editor/transform";
|
|
import { BottomControlDivider } from "./Divider";
|
|
import { bottomControlFieldClass, bottomControlIconSlotClass, bottomControlInputClass, bottomControlLabelClass, bottomControlMenuClass } from "./styles";
|
|
|
|
export type TransformControlsProps = {
|
|
bounds: Rect;
|
|
target: TransformTarget;
|
|
dispatch: AppStore["dispatch"];
|
|
};
|
|
|
|
type BoundsField = keyof Rect;
|
|
|
|
export function TransformControls({ bounds, target, dispatch }: TransformControlsProps) {
|
|
const [draft, setDraft] = useState(() => draftFromBounds(bounds));
|
|
|
|
useEffect(() => {
|
|
setDraft(draftFromBounds(bounds));
|
|
}, [bounds.x, bounds.y, bounds.w, bounds.h]);
|
|
|
|
const commitField = (field: BoundsField) => {
|
|
const value = Number.parseFloat(draft[field]);
|
|
if (!Number.isFinite(value)) {
|
|
setDraft(draftFromBounds(bounds));
|
|
return;
|
|
}
|
|
|
|
dispatch(commandIds.transformSetBounds, {
|
|
target,
|
|
bounds: {
|
|
...bounds,
|
|
[field]: field === "w" || field === "h" ? Math.max(1, value) : value,
|
|
},
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div className={bottomControlMenuClass()}>
|
|
<span className={bottomControlIconSlotClass()}>
|
|
<BoundingBox size={24} weight="regular" />
|
|
</span>
|
|
<BottomControlDivider />
|
|
<BoundsInput label="X" field="x" draft={draft.x} setDraft={setDraft} commitField={commitField} />
|
|
<BoundsInput label="Y" field="y" draft={draft.y} setDraft={setDraft} commitField={commitField} />
|
|
<BottomControlDivider />
|
|
<BoundsInput label="W" field="w" draft={draft.w} setDraft={setDraft} commitField={commitField} />
|
|
<BoundsInput label="H" field="h" draft={draft.h} setDraft={setDraft} commitField={commitField} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function BoundsInput({
|
|
label,
|
|
field,
|
|
draft,
|
|
setDraft,
|
|
commitField,
|
|
}: {
|
|
label: string;
|
|
field: BoundsField;
|
|
draft: string;
|
|
setDraft: Dispatch<SetStateAction<Record<BoundsField, string>>>;
|
|
commitField: (field: BoundsField) => void;
|
|
}) {
|
|
return (
|
|
<label className={bottomControlFieldClass()}>
|
|
<span className={bottomControlLabelClass()}>{label}</span>
|
|
<input
|
|
className={bottomControlInputClass()}
|
|
inputMode="decimal"
|
|
value={draft}
|
|
aria-label={label}
|
|
onChange={(event) => setDraft((current) => ({ ...current, [field]: event.target.value }))}
|
|
onBlur={() => commitField(field)}
|
|
onFocus={(event) => event.currentTarget.select()}
|
|
onKeyDown={(event) => {
|
|
event.stopPropagation();
|
|
if (event.key === "Enter") {
|
|
commitField(field);
|
|
event.currentTarget.blur();
|
|
}
|
|
if (event.key === "Escape") event.currentTarget.blur();
|
|
}}
|
|
/>
|
|
</label>
|
|
);
|
|
}
|
|
|
|
function draftFromBounds(bounds: Rect): Record<BoundsField, string> {
|
|
return {
|
|
x: String(Math.round(bounds.x)),
|
|
y: String(Math.round(bounds.y)),
|
|
w: String(Math.round(bounds.w)),
|
|
h: String(Math.round(bounds.h)),
|
|
};
|
|
}
|