feat: add editor layout components, welcome screen, and editor page

Create the four-zone editor layout shell (toolbar, options bar, status
bar, right panel), welcome screen with drag-and-drop and new document
dialog, and the main editor page with clipboard paste and URL loading.
This commit is contained in:
SnapOtter
2026-05-06 23:12:59 +08:00
parent 155783ca86
commit 08a2119564
7 changed files with 653 additions and 0 deletions
+76
View File
@@ -0,0 +1,76 @@
// apps/web/src/pages/editor-page.tsx
import { useCallback, useEffect } from "react";
import { WelcomeScreen } from "@/components/editor/common/welcome-screen";
import { EditorCanvas } from "@/components/editor/editor-canvas";
import { EditorOptionsBar } from "@/components/editor/editor-options-bar";
import { EditorRightPanel } from "@/components/editor/editor-right-panel";
import { EditorStatusBar } from "@/components/editor/editor-status-bar";
import { EditorToolbar } from "@/components/editor/editor-toolbar";
import { useEditorStore } from "@/stores/editor-store";
export function EditorPage() {
const sourceImageUrl = useEditorStore((s) => s.sourceImageUrl);
const isDirty = useEditorStore((s) => s.isDirty);
const loadImage = useEditorStore((s) => s.loadImage);
useEffect(() => {
const handler = (e: BeforeUnloadEvent) => {
if (isDirty) {
e.preventDefault();
}
};
window.addEventListener("beforeunload", handler);
return () => window.removeEventListener("beforeunload", handler);
}, [isDirty]);
const handlePaste = useCallback(
(e: ClipboardEvent) => {
const items = e.clipboardData?.items;
if (!items) return;
for (const item of items) {
if (item.type.startsWith("image/")) {
e.preventDefault();
const blob = item.getAsFile();
if (!blob) return;
const url = URL.createObjectURL(blob);
const img = new Image();
img.onload = () => loadImage(url, img.naturalWidth, img.naturalHeight);
img.src = url;
return;
}
}
},
[loadImage],
);
useEffect(() => {
document.addEventListener("paste", handlePaste);
return () => document.removeEventListener("paste", handlePaste);
}, [handlePaste]);
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const url = params.get("url");
if (url) {
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => loadImage(url, img.naturalWidth, img.naturalHeight);
img.src = url;
}
}, [loadImage]);
return (
<div className="flex flex-col h-full overflow-hidden">
<EditorOptionsBar />
<div className="flex flex-1 overflow-hidden">
<EditorToolbar />
<div className="relative flex-1 overflow-hidden bg-muted/30">
<EditorCanvas />
{!sourceImageUrl && <WelcomeScreen />}
</div>
<EditorRightPanel />
</div>
<EditorStatusBar />
</div>
);
}