Files
Pien-Studio/apps/web/components/editor/editor-header.tsx
T

382 lines
9.8 KiB
TypeScript
Raw Normal View History

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