;
}
export function BrowserDeviceToolbar({
setting,
width,
aspectRatio,
onAspectRatioChange,
onChange,
}: Props) {
const [pending, setPending] = useState(false);
const [customSize, setCustomSize] = useState<{
readonly width: string;
readonly height: string;
} | null>(null);
const presentedSize = customSize ?? {
width: String(setting.width),
height: String(setting.height),
};
const selectedValue =
setting._tag === "preset" &&
PREVIEW_VIEWPORT_PRESETS.some((preset) => preset.id === setting.presetId)
? setting.presetId
: RESPONSIVE_VALUE;
const customWidth = Number(presentedSize.width);
const customHeight = Number(presentedSize.height);
const customValid =
Number.isInteger(customWidth) &&
Number.isInteger(customHeight) &&
customWidth >= PREVIEW_VIEWPORT_MIN_DIMENSION &&
customWidth <= PREVIEW_VIEWPORT_MAX_DIMENSION &&
customHeight >= PREVIEW_VIEWPORT_MIN_DIMENSION &&
customHeight <= PREVIEW_VIEWPORT_MAX_DIMENSION &&
customWidth * customHeight <= PREVIEW_VIEWPORT_MAX_AREA;
const apply = (next: PreviewViewportSetting, nextAspectRatio = aspectRatio) => {
setPending(true);
void commitViewportAndAspectRatio(next, nextAspectRatio, onChange, onAspectRatioChange).then(
() => {
setPending(false);
setCustomSize(null);
},
() => setPending(false),
);
};
const applyCustomSize = () => {
if (!customValid || (customWidth === setting.width && customHeight === setting.height)) {
setCustomSize(null);
return;
}
apply({ _tag: "freeform", width: customWidth, height: customHeight });
};
const updateCustomDimension = (axis: "width" | "height", value: string) => {
setCustomSize((current) => {
const next = {
width: axis === "width" ? value : (current?.width ?? String(setting.width)),
height: axis === "height" ? value : (current?.height ?? String(setting.height)),
};
const numeric = Number(value);
if (
aspectRatio === null ||
!Number.isInteger(numeric) ||
numeric < PREVIEW_VIEWPORT_MIN_DIMENSION ||
numeric > PREVIEW_VIEWPORT_MAX_DIMENSION
) {
return next;
}
const resized = resizeFreeformViewport(
setting,
axis === "width"
? { x: numeric - setting.width, y: 0 }
: { x: 0, y: numeric - setting.height },
1,
axis === "width" ? "east" : "south",
aspectRatio,
);
return { width: String(resized.width), height: String(resized.height) };
});
};
const selectViewport = (value: string | null) => {
if (!value) return;
if (value === RESPONSIVE_VALUE) {
if (setting._tag === "freeform") return;
apply({ _tag: "freeform", width: setting.width, height: setting.height });
return;
}
const preset = PREVIEW_VIEWPORT_PRESETS.find((candidate) => candidate.id === value);
if (!preset) return;
apply(
resolvePreviewViewport({ mode: "preset", preset: preset.id }),
aspectRatio === null ? null : preset.width / preset.height,
);
};
const rotate = () => {
const hasCustomSize =
customValid && (customWidth !== setting.width || customHeight !== setting.height);
const source = hasCustomSize
? ({ _tag: "freeform", width: customWidth, height: customHeight } as const)
: setting;
apply(
{ ...source, width: source.height, height: source.width },
aspectRatio === null ? null : 1 / aspectRatio,
);
};
const toggleAspectRatio = () => {
onAspectRatioChange(aspectRatio === null ? customWidth / customHeight : null);
};
return (
{
const nextTarget = event.relatedTarget;
if (nextTarget instanceof Node && event.currentTarget.contains(nextTarget)) return;
const eventTarget = event.target;
if (
(nextTarget instanceof HTMLElement &&
nextTarget.closest('[data-slot="select-positioner"]')) ||
(eventTarget instanceof HTMLElement &&
eventTarget.closest('[data-slot="select-positioner"]'))
) {
return;
}
applyCustomSize();
}}
>
{width >= 560 ? (
Dimensions
) : null}
);
}