Files
Pien-Studio/apps/web/components/canvas-size-modal.tsx
T
2026-05-10 03:06:54 +07:00

174 lines
7.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import React from "react";
import type { AspectRatio } from "@pien-studio/types";
import { useTranslations } from "../hooks/use-translations";
interface CanvasSizeModalProps {
isOpen: boolean;
onClose: () => void;
currentWidth: number;
currentHeight: number;
currentAspect: AspectRatio;
onApply: (width: number, height: number, aspect: AspectRatio) => void;
isDark: boolean;
}
const PRESETS = [
{ labelKey: "editor.presetSquare", sublabel: "1080 × 1080", aspect: "1:1" as AspectRatio, width: 1080, height: 1080 },
{ labelKey: "editor.presetPortrait45", sublabel: "1080 × 1350", aspect: "4:5" as AspectRatio, width: 1080, height: 1350 },
{ labelKey: "editor.presetStory916", sublabel: "1080 × 1920", aspect: "9:16" as AspectRatio, width: 1080, height: 1920 },
{ labelKey: "editor.presetWidescreen", sublabel: "1920 × 1080", aspect: "16:9" as AspectRatio, width: 1920, height: 1080 },
{ labelKey: "editor.presetPhoto43", sublabel: "1440 × 1080", aspect: "4:3" as AspectRatio, width: 1440, height: 1080 },
{ labelKey: "editor.presetClassic32", sublabel: "1620 × 1080", aspect: "3:2" as AspectRatio, width: 1620, height: 1080 },
];
export function CanvasSizeModal({
isOpen,
onClose,
currentWidth,
currentHeight,
currentAspect,
onApply,
isDark,
}: CanvasSizeModalProps) {
const { t } = useTranslations();
const [mode, setMode] = React.useState<"preset" | "custom">(
PRESETS.some((p) => p.aspect === currentAspect) ? "preset" : "custom",
);
const [customWidth, setCustomWidth] = React.useState(currentWidth.toString());
const [customHeight, setCustomHeight] = React.useState(currentHeight.toString());
const [selectedPreset, setSelectedPreset] = React.useState<AspectRatio>(currentAspect);
if (!isOpen) return null;
function handleApply() {
if (mode === "preset") {
const preset = PRESETS.find((p) => p.aspect === selectedPreset)!;
onApply(preset.width, preset.height, preset.aspect);
} else {
const w = parseInt(customWidth, 10);
const h = parseInt(customHeight, 10);
if (!isNaN(w) && !isNaN(h) && w > 0 && h > 0) {
onApply(w, h, "free");
}
}
onClose();
}
const overlay = "fixed inset-0 z-50 flex items-center justify-center bg-black/40";
const panel = `w-full max-w-sm rounded-2xl border p-5 shadow-2xl ${
isDark ? "border-white/15 bg-[#2b2d31]" : "border-black/15 bg-white"
}`;
return (
<div className={overlay} onClick={onClose}>
<div className={panel} onClick={(e) => e.stopPropagation()}>
<div className="mb-4 flex items-center justify-between">
<h2 className={`text-base font-semibold ${isDark ? "text-[#f5f7fa]" : "text-[#1f2430]"}`}>
{t("editor.canvasSizeTitle")}
</h2>
<button
onClick={onClose}
className={`rounded border px-2 py-0.5 text-xs ${isDark ? "border-white/20 text-[#d7dae0]" : "border-black/20 text-[#1f2430]"}`}
>
</button>
</div>
<div className="mb-4 flex gap-2">
{(["preset", "custom"] as const).map((m) => (
<button
key={m}
onClick={() => setMode(m)}
className={`flex-1 rounded border py-1.5 text-xs font-semibold capitalize ${
mode === m
? "border-[var(--color-accent-strong)] bg-[var(--color-accent-strong)] text-white"
: isDark
? "border-white/20 text-[#d7dae0]"
: "border-black/20 text-[#1f2430]"
}`}
>
{m === "preset" ? t("editor.modePreset") : t("editor.modeCustom")}
</button>
))}
</div>
{mode === "preset" ? (
<div className="grid grid-cols-2 gap-2">
{PRESETS.map((p) => (
<button
key={p.aspect}
onClick={() => setSelectedPreset(p.aspect)}
className={`rounded border p-3 text-left ${
selectedPreset === p.aspect
? "border-[var(--color-accent-strong)] bg-[var(--color-accent-strong)] text-white"
: isDark
? "border-white/10 bg-[#343841] text-[#d7dae0]"
: "border-black/10 bg-[#f6f8fb] text-[#1f2430]"
}`}
>
<div className="text-xs font-semibold">{t(p.labelKey)}</div>
<div className={`text-[10px] ${selectedPreset === p.aspect ? "text-white/70" : isDark ? "text-[#aeb3bc]" : "text-[#6c7382]"}`}>
{p.sublabel}
</div>
</button>
))}
</div>
) : (
<div className="space-y-3">
<div className="flex items-center gap-2">
<label className={`w-16 text-xs ${isDark ? "text-[#aeb3bc]" : "text-[#5f6672]"}`}>{t("editor.widthShort")}</label>
<input
type="number"
value={customWidth}
onChange={(e) => setCustomWidth(e.target.value)}
min={1}
className={`flex-1 rounded border px-2 py-1.5 text-sm ${
isDark ? "border-white/20 bg-[#25272b] text-[#e8eaed]" : "border-black/20 bg-[#f6f8fb] text-[#1f2430]"
}`}
/>
<span className={`text-xs ${isDark ? "text-[#aeb3bc]" : "text-[#5f6672]"}`}>px</span>
</div>
<div className="flex items-center gap-2">
<label className={`w-16 text-xs ${isDark ? "text-[#aeb3bc]" : "text-[#5f6672]"}`}>{t("editor.heightShort")}</label>
<input
type="number"
value={customHeight}
onChange={(e) => setCustomHeight(e.target.value)}
min={1}
className={`flex-1 rounded border px-2 py-1.5 text-sm ${
isDark ? "border-white/20 bg-[#25272b] text-[#e8eaed]" : "border-black/20 bg-[#f6f8fb] text-[#1f2430]"
}`}
/>
<span className={`text-xs ${isDark ? "text-[#aeb3bc]" : "text-[#5f6672]"}`}>px</span>
</div>
<div className="rounded border border-dashed border-black/20 p-2 text-center">
<span className={`text-xs ${isDark ? "text-[#aeb3bc]" : "text-[#5f6672]"}`}>
{parseInt(customWidth) || 0} × {parseInt(customHeight) || 0} px
</span>
</div>
</div>
)}
<div className="mt-5 flex justify-end gap-2">
<button
onClick={onClose}
className={`rounded border px-3 py-1.5 text-xs font-medium ${
isDark ? "border-white/20 text-[#d7dae0]" : "border-black/20 text-[#1f2430]"
}`}
>
{t("editor.cancel")}
</button>
<button
onClick={handleApply}
className="rounded border border-[var(--color-accent-strong)] bg-[var(--color-accent-strong)] px-3 py-1.5 text-xs font-semibold text-white"
>
{t("editor.apply")}
</button>
</div>
</div>
</div>
);
}