feat(layers): add layer management panel
This commit is contained in:
@@ -1,36 +0,0 @@
|
||||
import { Stack } from "@phosphor-icons/react";
|
||||
import type { SelectionSummary } from "../selectionSummary";
|
||||
import { BottomControlDivider } from "./Divider";
|
||||
import { bottomControlButtonClass } from "./styles";
|
||||
|
||||
export type SelectionControlsProps = {
|
||||
selection: SelectionSummary;
|
||||
onOpenLayers: () => void;
|
||||
};
|
||||
|
||||
export function SelectionControls({ selection, onOpenLayers }: SelectionControlsProps) {
|
||||
const label = selectionLabel(selection);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button type="button" className={bottomControlButtonClass()} aria-label="Open layers" title="Open layers" onClick={onOpenLayers}>
|
||||
<Stack size={16} weight="regular" />
|
||||
</button>
|
||||
<BottomControlDivider />
|
||||
<span className="max-w-48 truncate px-1 text-white">{label}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function selectionLabel(selection: SelectionSummary) {
|
||||
switch (selection.type) {
|
||||
case "artboard":
|
||||
return `${selection.name} · ${selection.layerCount} layers`;
|
||||
case "layer":
|
||||
return selection.name;
|
||||
case "multi-layer":
|
||||
return `${selection.count} layers selected`;
|
||||
case "none":
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,100 @@
|
||||
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 { bottomControlIconSlotClass, bottomControlLabelClass, bottomControlValueClass } from "./styles";
|
||||
import { bottomControlIconSlotClass, bottomControlInputClass, bottomControlLabelClass } from "./styles";
|
||||
|
||||
export type TransformControlsProps = {
|
||||
bounds: Rect;
|
||||
target: TransformTarget;
|
||||
dispatch: AppStore["dispatch"];
|
||||
};
|
||||
|
||||
export function TransformControls({ bounds }: TransformControlsProps) {
|
||||
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="flex w-full items-center justify-center gap-2 tabular-nums">
|
||||
<span className={bottomControlIconSlotClass()}>
|
||||
<BoundingBox size={16} weight="regular" />
|
||||
</span>
|
||||
<BottomControlDivider />
|
||||
<span className={bottomControlLabelClass()}>X</span>
|
||||
<span className={bottomControlValueClass()}>{Math.round(bounds.x)}</span>
|
||||
<span className={bottomControlLabelClass()}>Y</span>
|
||||
<span className={bottomControlValueClass()}>{Math.round(bounds.y)}</span>
|
||||
<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 />
|
||||
<span className={bottomControlLabelClass()}>W</span>
|
||||
<span className={bottomControlValueClass()}>{Math.round(bounds.w)}</span>
|
||||
<span className={bottomControlLabelClass()}>H</span>
|
||||
<span className={bottomControlValueClass()}>{Math.round(bounds.h)}</span>
|
||||
<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="flex items-center gap-1">
|
||||
<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)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@ export function bottomControlValueClass() {
|
||||
return "min-w-10 text-center text-white";
|
||||
}
|
||||
|
||||
export function bottomControlInputClass() {
|
||||
return "h-7 w-14 px-1 text-center text-white outline-none transition placeholder:text-white/30 focus:text-white";
|
||||
}
|
||||
|
||||
export function bottomControlIconSlotClass() {
|
||||
return "grid size-7 place-items-center rounded-full text-white/80";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user