Files
image-studio/view/bottom-controls/TransformControls.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

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)),
};
}