2026-05-10 03:06:54 +07:00
|
|
|
import React from "react";
|
|
|
|
|
import Link from "next/link";
|
|
|
|
|
import { Redo2, Undo2 } from "lucide-react";
|
|
|
|
|
import { UiPreferences } from "../ui-preferences";
|
|
|
|
|
import { dividerClass, hoverSubtleClass } from "../../lib/theme";
|
|
|
|
|
|
|
|
|
|
type EditorHeaderProps = {
|
|
|
|
|
isDark: boolean;
|
|
|
|
|
projectTitle: string;
|
|
|
|
|
canvasWidth: number;
|
|
|
|
|
canvasHeight: number;
|
|
|
|
|
canUndo: boolean;
|
|
|
|
|
canRedo: boolean;
|
|
|
|
|
isDirty: boolean;
|
|
|
|
|
labels: {
|
|
|
|
|
file: string;
|
|
|
|
|
edit: string;
|
|
|
|
|
view: string;
|
|
|
|
|
settings: string;
|
|
|
|
|
save: string;
|
|
|
|
|
exportPng: string;
|
|
|
|
|
exportProjectFile: string;
|
|
|
|
|
importImage: string;
|
|
|
|
|
canvasSize: string;
|
|
|
|
|
undo: string;
|
|
|
|
|
redo: string;
|
|
|
|
|
copy: string;
|
|
|
|
|
cut: string;
|
|
|
|
|
paste: string;
|
|
|
|
|
preferences: string;
|
|
|
|
|
panTool: string;
|
|
|
|
|
pointerTool: string;
|
|
|
|
|
unsavedChanges: string;
|
|
|
|
|
saved: string;
|
|
|
|
|
};
|
|
|
|
|
onSave: () => void;
|
|
|
|
|
onExportPng: () => void;
|
|
|
|
|
onExportProjectFile: () => void;
|
|
|
|
|
onImportImage: () => void;
|
|
|
|
|
onOpenCanvasSize: () => void;
|
|
|
|
|
onUndo: () => void;
|
|
|
|
|
onRedo: () => void;
|
|
|
|
|
onCopy: () => void;
|
|
|
|
|
onCut: () => void;
|
|
|
|
|
onPaste: () => void;
|
|
|
|
|
onSetHandTool: () => void;
|
|
|
|
|
onSetPointerTool: () => void;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function EditorHeader({
|
|
|
|
|
isDark,
|
|
|
|
|
projectTitle,
|
|
|
|
|
canvasWidth,
|
|
|
|
|
canvasHeight,
|
|
|
|
|
canUndo,
|
|
|
|
|
canRedo,
|
|
|
|
|
isDirty,
|
|
|
|
|
labels,
|
|
|
|
|
onSave,
|
|
|
|
|
onExportPng,
|
|
|
|
|
onExportProjectFile,
|
|
|
|
|
onImportImage,
|
|
|
|
|
onOpenCanvasSize,
|
|
|
|
|
onUndo,
|
|
|
|
|
onRedo,
|
|
|
|
|
onCopy,
|
|
|
|
|
onCut,
|
|
|
|
|
onPaste,
|
|
|
|
|
onSetHandTool,
|
|
|
|
|
onSetPointerTool,
|
|
|
|
|
}: EditorHeaderProps) {
|
|
|
|
|
const menuClass = `absolute left-0 top-full z-30 min-w-[180px] rounded border p-1 text-[11px] opacity-0 shadow-xl transition-opacity pointer-events-none group-hover:pointer-events-auto group-hover:opacity-100 hover:pointer-events-auto hover:opacity-100 ${
|
2026-05-31 03:03:49 +07:00
|
|
|
isDark
|
|
|
|
|
? "border-white/10 bg-[#2a2c31] text-[#e2e5ea]"
|
|
|
|
|
: "border-black/10 bg-white text-[#1f2430]"
|
2026-05-10 03:06:54 +07:00
|
|
|
}`;
|
|
|
|
|
|
|
|
|
|
return (
|
2026-05-31 03:03:49 +07:00
|
|
|
<header
|
|
|
|
|
className={`flex h-14 items-center justify-between border-b px-4 ${isDark ? "border-white/10 bg-[#2b2d31]" : "border-black/10 bg-white"}`}
|
|
|
|
|
>
|
2026-05-10 03:06:54 +07:00
|
|
|
<div className="flex items-center gap-4">
|
|
|
|
|
<Link href="/" className="group">
|
2026-05-31 03:03:49 +07:00
|
|
|
<p
|
|
|
|
|
className={`text-[10px] uppercase tracking-[0.2em] group-hover:opacity-80 ${isDark ? "text-[#a8abb2]" : "text-[#6c7382]"}`}
|
|
|
|
|
>
|
2026-05-10 03:06:54 +07:00
|
|
|
pien.studio
|
|
|
|
|
</p>
|
2026-05-31 03:03:49 +07:00
|
|
|
<h1
|
|
|
|
|
className={`text-sm font-semibold group-hover:opacity-80 ${isDark ? "text-[#f5f7fa]" : "text-[#1f2430]"}`}
|
|
|
|
|
>
|
|
|
|
|
{projectTitle}
|
|
|
|
|
</h1>
|
2026-05-10 03:06:54 +07:00
|
|
|
</Link>
|
|
|
|
|
|
|
|
|
|
<nav className="flex items-center gap-2 text-xs font-medium">
|
|
|
|
|
<MenuShell isDark={isDark} label={labels.file} menuClass={menuClass}>
|
|
|
|
|
<FileMenu
|
|
|
|
|
isDark={isDark}
|
|
|
|
|
labels={labels}
|
|
|
|
|
canvasWidth={canvasWidth}
|
|
|
|
|
canvasHeight={canvasHeight}
|
|
|
|
|
onSave={onSave}
|
|
|
|
|
onExportPng={onExportPng}
|
|
|
|
|
onExportProjectFile={onExportProjectFile}
|
|
|
|
|
onImportImage={onImportImage}
|
|
|
|
|
onOpenCanvasSize={onOpenCanvasSize}
|
|
|
|
|
/>
|
|
|
|
|
</MenuShell>
|
|
|
|
|
<MenuShell isDark={isDark} label={labels.edit} menuClass={menuClass}>
|
2026-05-31 03:03:49 +07:00
|
|
|
<EditMenu
|
|
|
|
|
isDark={isDark}
|
|
|
|
|
labels={labels}
|
|
|
|
|
canUndo={canUndo}
|
|
|
|
|
canRedo={canRedo}
|
|
|
|
|
onUndo={onUndo}
|
|
|
|
|
onRedo={onRedo}
|
|
|
|
|
onCopy={onCopy}
|
|
|
|
|
onCut={onCut}
|
|
|
|
|
onPaste={onPaste}
|
|
|
|
|
/>
|
2026-05-10 03:06:54 +07:00
|
|
|
</MenuShell>
|
|
|
|
|
<MenuShell isDark={isDark} label={labels.view} menuClass={menuClass}>
|
2026-05-31 03:03:49 +07:00
|
|
|
<ViewMenu
|
|
|
|
|
isDark={isDark}
|
|
|
|
|
labels={labels}
|
|
|
|
|
onSetHandTool={onSetHandTool}
|
|
|
|
|
onSetPointerTool={onSetPointerTool}
|
|
|
|
|
/>
|
2026-05-10 03:06:54 +07:00
|
|
|
</MenuShell>
|
2026-05-31 03:03:49 +07:00
|
|
|
<MenuShell
|
|
|
|
|
isDark={isDark}
|
|
|
|
|
label={labels.settings}
|
|
|
|
|
menuClass={menuClass}
|
|
|
|
|
>
|
2026-05-10 03:06:54 +07:00
|
|
|
<SettingsMenu isDark={isDark} preferences={labels.preferences} />
|
|
|
|
|
</MenuShell>
|
|
|
|
|
</nav>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={onUndo}
|
|
|
|
|
disabled={!canUndo}
|
|
|
|
|
title={labels.undo}
|
|
|
|
|
className={`rounded border px-2.5 py-1.5 text-xs font-medium ${
|
2026-05-31 03:03:49 +07:00
|
|
|
isDark
|
|
|
|
|
? "border-white/15 bg-[#25272b] text-[#d7dae0]"
|
|
|
|
|
: "border-black/15 bg-[#f2f4f8] text-[#1f2430]"
|
2026-05-10 03:06:54 +07:00
|
|
|
} ${!canUndo ? "opacity-50" : "hover:opacity-90"}`}
|
|
|
|
|
>
|
|
|
|
|
<Undo2 className="h-3.5 w-3.5" />
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={onRedo}
|
|
|
|
|
disabled={!canRedo}
|
|
|
|
|
title={labels.redo}
|
|
|
|
|
className={`rounded border px-2.5 py-1.5 text-xs font-medium ${
|
2026-05-31 03:03:49 +07:00
|
|
|
isDark
|
|
|
|
|
? "border-white/15 bg-[#25272b] text-[#d7dae0]"
|
|
|
|
|
: "border-black/15 bg-[#f2f4f8] text-[#1f2430]"
|
2026-05-10 03:06:54 +07:00
|
|
|
} ${!canRedo ? "opacity-50" : "hover:opacity-90"}`}
|
|
|
|
|
>
|
|
|
|
|
<Redo2 className="h-3.5 w-3.5" />
|
|
|
|
|
</button>
|
2026-05-31 03:03:49 +07:00
|
|
|
<span
|
|
|
|
|
className={`text-[10px] font-semibold uppercase tracking-[0.2em] ${isDark ? "text-[#a8abb2]" : "text-[#6c7382]"}`}
|
|
|
|
|
>
|
2026-05-10 03:06:54 +07:00
|
|
|
{isDirty ? labels.unsavedChanges : labels.saved}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</header>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 03:03:49 +07:00
|
|
|
function MenuShell({
|
|
|
|
|
isDark,
|
|
|
|
|
label,
|
|
|
|
|
menuClass,
|
|
|
|
|
children,
|
|
|
|
|
}: {
|
|
|
|
|
isDark: boolean;
|
|
|
|
|
label: string;
|
|
|
|
|
menuClass: string;
|
|
|
|
|
children: React.ReactNode;
|
|
|
|
|
}) {
|
2026-05-10 03:06:54 +07:00
|
|
|
return (
|
|
|
|
|
<div className="relative group">
|
2026-05-31 03:03:49 +07:00
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className={`rounded px-2 py-1 ${isDark ? "text-[#d7dae0] hover:bg-white/10" : "text-[#1f2430] hover:bg-black/5"}`}
|
|
|
|
|
>
|
2026-05-10 03:06:54 +07:00
|
|
|
{label}
|
|
|
|
|
</button>
|
|
|
|
|
<div className={menuClass} onClick={(event) => event.stopPropagation()}>
|
|
|
|
|
{children}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function FileMenu({
|
|
|
|
|
isDark,
|
|
|
|
|
labels,
|
|
|
|
|
canvasWidth,
|
|
|
|
|
canvasHeight,
|
|
|
|
|
onSave,
|
|
|
|
|
onExportPng,
|
|
|
|
|
onExportProjectFile,
|
|
|
|
|
onImportImage,
|
|
|
|
|
onOpenCanvasSize,
|
|
|
|
|
}: {
|
|
|
|
|
isDark: boolean;
|
|
|
|
|
labels: EditorHeaderProps["labels"];
|
|
|
|
|
canvasWidth: number;
|
|
|
|
|
canvasHeight: number;
|
|
|
|
|
onSave: () => void;
|
|
|
|
|
onExportPng: () => void;
|
|
|
|
|
onExportProjectFile: () => void;
|
|
|
|
|
onImportImage: () => void;
|
|
|
|
|
onOpenCanvasSize: () => void;
|
|
|
|
|
}) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-1">
|
2026-05-31 03:03:49 +07:00
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={onImportImage}
|
|
|
|
|
className={`w-full rounded px-2 py-1 text-left ${hoverSubtleClass(isDark)}`}
|
|
|
|
|
>
|
|
|
|
|
{labels.importImage}
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={onSave}
|
|
|
|
|
className={`w-full rounded px-2 py-1 text-left ${hoverSubtleClass(isDark)}`}
|
|
|
|
|
>
|
|
|
|
|
{labels.save}
|
|
|
|
|
</button>
|
2026-05-10 03:06:54 +07:00
|
|
|
<div className={`my-1 h-px ${dividerClass(isDark)}`} />
|
2026-05-31 03:03:49 +07:00
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={onExportPng}
|
|
|
|
|
className={`w-full rounded px-2 py-1 text-left ${hoverSubtleClass(isDark)}`}
|
|
|
|
|
>
|
|
|
|
|
{labels.exportPng}
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={onExportProjectFile}
|
|
|
|
|
className={`w-full rounded px-2 py-1 text-left ${hoverSubtleClass(isDark)}`}
|
|
|
|
|
>
|
|
|
|
|
{labels.exportProjectFile}
|
|
|
|
|
</button>
|
2026-05-10 03:06:54 +07:00
|
|
|
<div className={`my-1 h-px ${dividerClass(isDark)}`} />
|
2026-05-31 03:03:49 +07:00
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={onOpenCanvasSize}
|
|
|
|
|
className={`w-full rounded px-2 py-1 text-left ${hoverSubtleClass(isDark)}`}
|
|
|
|
|
>
|
|
|
|
|
{labels.canvasSize} ({canvasWidth} x {canvasHeight})
|
|
|
|
|
</button>
|
2026-05-10 03:06:54 +07:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function EditMenu({
|
|
|
|
|
isDark,
|
|
|
|
|
labels,
|
|
|
|
|
canUndo,
|
|
|
|
|
canRedo,
|
|
|
|
|
onUndo,
|
|
|
|
|
onRedo,
|
|
|
|
|
onCopy,
|
|
|
|
|
onCut,
|
|
|
|
|
onPaste,
|
|
|
|
|
}: {
|
|
|
|
|
isDark: boolean;
|
|
|
|
|
labels: EditorHeaderProps["labels"];
|
|
|
|
|
canUndo: boolean;
|
|
|
|
|
canRedo: boolean;
|
|
|
|
|
onUndo: () => void;
|
|
|
|
|
onRedo: () => void;
|
|
|
|
|
onCopy: () => void;
|
|
|
|
|
onCut: () => void;
|
|
|
|
|
onPaste: () => void;
|
|
|
|
|
}) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-1">
|
2026-05-31 03:03:49 +07:00
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={onUndo}
|
|
|
|
|
disabled={!canUndo}
|
|
|
|
|
className={`w-full rounded px-2 py-1 text-left ${!canUndo ? "opacity-50" : hoverSubtleClass(isDark)}`}
|
|
|
|
|
>
|
|
|
|
|
{labels.undo}
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={onRedo}
|
|
|
|
|
disabled={!canRedo}
|
|
|
|
|
className={`w-full rounded px-2 py-1 text-left ${!canRedo ? "opacity-50" : hoverSubtleClass(isDark)}`}
|
|
|
|
|
>
|
|
|
|
|
{labels.redo}
|
|
|
|
|
</button>
|
2026-05-10 03:06:54 +07:00
|
|
|
<div className={`my-1 h-px ${dividerClass(isDark)}`} />
|
2026-05-31 03:03:49 +07:00
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={onCopy}
|
|
|
|
|
className={`w-full rounded px-2 py-1 text-left ${hoverSubtleClass(isDark)}`}
|
|
|
|
|
>
|
|
|
|
|
{labels.copy}
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={onCut}
|
|
|
|
|
className={`w-full rounded px-2 py-1 text-left ${hoverSubtleClass(isDark)}`}
|
|
|
|
|
>
|
|
|
|
|
{labels.cut}
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={onPaste}
|
|
|
|
|
className={`w-full rounded px-2 py-1 text-left ${hoverSubtleClass(isDark)}`}
|
|
|
|
|
>
|
|
|
|
|
{labels.paste}
|
|
|
|
|
</button>
|
2026-05-10 03:06:54 +07:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 03:03:49 +07:00
|
|
|
function ViewMenu({
|
|
|
|
|
isDark,
|
|
|
|
|
labels,
|
|
|
|
|
onSetHandTool,
|
|
|
|
|
onSetPointerTool,
|
|
|
|
|
}: {
|
|
|
|
|
isDark: boolean;
|
|
|
|
|
labels: EditorHeaderProps["labels"];
|
|
|
|
|
onSetHandTool: () => void;
|
|
|
|
|
onSetPointerTool: () => void;
|
|
|
|
|
}) {
|
2026-05-10 03:06:54 +07:00
|
|
|
return (
|
|
|
|
|
<div className="space-y-1">
|
2026-05-31 03:03:49 +07:00
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={onSetHandTool}
|
|
|
|
|
className={`w-full rounded px-2 py-1 text-left ${hoverSubtleClass(isDark)}`}
|
|
|
|
|
>
|
|
|
|
|
{labels.panTool}
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={onSetPointerTool}
|
|
|
|
|
className={`w-full rounded px-2 py-1 text-left ${hoverSubtleClass(isDark)}`}
|
|
|
|
|
>
|
|
|
|
|
{labels.pointerTool}
|
|
|
|
|
</button>
|
2026-05-10 03:06:54 +07:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 03:03:49 +07:00
|
|
|
function SettingsMenu({
|
|
|
|
|
isDark,
|
|
|
|
|
preferences,
|
|
|
|
|
}: {
|
|
|
|
|
isDark: boolean;
|
|
|
|
|
preferences: string;
|
|
|
|
|
}) {
|
2026-05-10 03:06:54 +07:00
|
|
|
return (
|
|
|
|
|
<div className="space-y-2 p-1">
|
2026-05-31 03:03:49 +07:00
|
|
|
<p
|
|
|
|
|
className={`px-2 text-[10px] font-semibold uppercase tracking-wide ${isDark ? "text-[#9aa1ad]" : "text-[#6c7382]"}`}
|
|
|
|
|
>
|
|
|
|
|
{preferences}
|
|
|
|
|
</p>
|
2026-05-10 03:06:54 +07:00
|
|
|
<UiPreferences />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|