-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathApp.tsx
More file actions
211 lines (186 loc) · 7.88 KB
/
Copy pathApp.tsx
File metadata and controls
211 lines (186 loc) · 7.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
import { useEffect, useState, useCallback } from "react";
import { Upload, Minus, Plus, Delete, Command } from "lucide-react";
import { useEditor, ZOOM_MAX, ZOOM_MIN } from "@/store/editor";
import { TopBar } from "@/components/TopBar";
import { LeftRail } from "@/components/LeftRail";
import { PanelHost } from "@/components/panels";
import { CanvasStage } from "@/components/canvas/CanvasStage";
import { GridControl } from "@/components/canvas/GridControl";
import { SocialPreviewModal } from "@/components/SocialPreviewModal";
import { CustomizeSidebarModal } from "@/components/CustomizeSidebarModal";
import { KeyBadge } from "@/components/ui/key-badge";
import { importImageFile } from "@/lib/importers";
import {
exportCanvas,
copyCanvasToClipboard,
renderCanvasDataURL,
} from "@/lib/exporters";
/* ------------------------------------------------------------------------------------------ */
export const App = () => {
const activeTool = useEditor((s) => s.activeTool);
const zoom = useEditor((s) => s.zoom);
const setZoom = useEditor((s) => s.setZoom);
const addImage = useEditor((s) => s.addImage);
const elements = useEditor((s) => s.elements);
const [previewOpen, setPreviewOpen] = useState(false);
const [previewDataUrl, setPreviewDataUrl] = useState<string | null>(null);
const [customizeOpen, setCustomizeOpen] = useState(false);
const [hiddenTools, setHiddenTools] = useState<Record<string, boolean>>({});
const [dragOver, setDragOver] = useState(false);
const toggleVisible = useCallback((k: string) => {
setHiddenTools((h) => ({ ...h, [k]: !h[k] }));
}, []);
const openPreview = useCallback(async () => {
setPreviewDataUrl(await renderCanvasDataURL("png", 1));
setPreviewOpen(true);
}, []);
// Hotkeys
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
const target = e.target as HTMLElement | null;
const inField =
!!target &&
(target.tagName === "INPUT" || target.tagName === "TEXTAREA");
if (inField) return;
if ((e.metaKey || e.ctrlKey) && e.key === "e") {
e.preventDefault();
exportCanvas("png", 2);
} else if ((e.metaKey || e.ctrlKey) && e.key === "c") {
if (useEditor.getState().selectedIds.length > 0) {
e.preventDefault();
copyCanvasToClipboard();
}
} else if (e.shiftKey && !e.metaKey && !e.ctrlKey && e.key.toLowerCase() === "g") {
e.preventDefault();
useEditor.getState().toggleGrid();
} else if (e.key === "Escape") {
setPreviewOpen(false);
setCustomizeOpen(false);
}
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, []);
// Upload image on drag
const onDragOver = (e: React.DragEvent) => {
if (Array.from(e.dataTransfer.types).includes("Files")) {
e.preventDefault();
setDragOver(true);
}
};
const onDrop = async (e: React.DragEvent) => {
e.preventDefault();
setDragOver(false);
for (const file of Array.from(e.dataTransfer.files)) {
if (file.type.startsWith("image/")) await importImageFile(file, addImage);
}
};
return (
<div
className="relative flex h-screen flex-col bg-chrome"
onDragOver={onDragOver}
onDragLeave={() => setDragOver(false)}
onDrop={onDrop}
>
<TopBar onPreview={openPreview} />
<div className="flex min-h-0 flex-1">
<LeftRail hiddenTools={hiddenTools} />
<aside className="z-10 w-72.5 shrink-0 overflow-y-auto border-r border-hairline bg-panel scrollbar-thin">
<PanelHost
activeTool={activeTool}
onOpenCustomizeModal={() => setCustomizeOpen(true)}
/>
</aside>
<main className="relative min-w-0 flex-1 overflow-hidden bg-canvas">
<div className="size-full bg-dot-grid absolute inset-0 bottom-20 flex items-center justify-center">
<CanvasStage />
{elements.length === 0 && (
<div className="pointer-events-none absolute inset-0 flex flex-col items-center justify-center gap-2.5 text-text-muted">
<div className="pointer-events-auto flex h-14 w-14 items-center justify-center rounded-full bg-p-50 text-p-600">
<Upload className="h-5.5 w-5.5" />
</div>
<div className="font-display text-[18px] font-semibold tracking-[-0.01em] text-gray-100 cursor-pointer!">
<span>Drag and drop image to begin</span>
</div>
<div className="flex items-center gap-1.5 text-[13px] text-gray-400">
paste{" "}
<KeyBadge className="border-p-100 bg-p-50 text-p-600">
⌘V
</KeyBadge>{" "}
from your clipboard
</div>
</div>
)}
</div>
{dragOver && (
<div className="pointer-events-none absolute inset-3 z-50 flex items-center justify-center rounded-2xl border-2 border-dashed border-p-300 bg-p-600/8 text-sm font-medium text-text" />
)}
{/* Zoom buttons */}
<div className="absolute bottom-23 left-4 z-6 flex items-center gap-1 rounded-lg border border-hairline bg-panel p-1">
<button
type="button"
title="Zoom out"
aria-label="Zoom out"
disabled={zoom <= ZOOM_MIN}
onClick={() => setZoom(zoom - 10)}
className="flex h-6 w-6 items-center justify-center rounded text-sm text-text-muted hover:bg-p-200/10 hover:text-text disabled:pointer-events-none disabled:opacity-30"
>
<Minus className="h-3 w-3" />
</button>
<span className="min-w-8 px-1 text-center font-mono text-[11px] text-text">
{zoom}%
</span>
<button
type="button"
title="Zoom in"
aria-label="Zoom in"
disabled={zoom >= ZOOM_MAX}
onClick={() => setZoom(zoom + 10)}
className="flex h-6 w-6 items-center justify-center rounded text-sm text-text-muted hover:bg-p-200/10 hover:text-text disabled:pointer-events-none disabled:opacity-30"
>
<Plus className="h-3 w-3" />
</button>
{/* View aids live with zoom: both describe how you're looking at
the canvas, and neither appears in an export. */}
<div className="mx-0.5 h-4.5 w-px bg-hairline" />
<GridControl />
</div>
{/* Paste, Export and Delete */}
<div className="absolute bottom-23 right-4 z-6 flex items-center gap-3.5 whitespace-nowrap">
<div className="flex items-center gap-1.5 text-[10.5px] text-text-faint">
<KeyBadge className="size-fit px-1.5 py-0.5">
<div className="text-[10.5px] flex items-center justify-center">
<Command size={10.5} />V
</div>
</KeyBadge>{" "}
paste
<KeyBadge className="size-fit px-1.5 py-0.5">
<div className="text-[10.5px] flex items-center justify-center">
<Command size={10.5} />E
</div>
</KeyBadge>{" "}
export
<KeyBadge className="size-fit px-1.5 py-0.5">
<div className="text-[10.5px] flex item-center justify-center">
<Delete size={10.5} />
</div>
</KeyBadge>{" "}
delete
</div>
</div>
</main>
</div>
<SocialPreviewModal
open={previewOpen}
onOpenChange={setPreviewOpen}
previewDataUrl={previewDataUrl}
/>
<CustomizeSidebarModal
open={customizeOpen}
onOpenChange={setCustomizeOpen}
hiddenTools={hiddenTools}
toggleVisible={toggleVisible}
/>
</div>
);
};