Skip to content

Commit 0aa20e4

Browse files
committed
feat: wire up exportHTML handler in Layout component
1 parent cceee18 commit 0aa20e4

File tree

2 files changed

+113
-12
lines changed

2 files changed

+113
-12
lines changed

src/components/Header/Header.tsx

Lines changed: 108 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
import { useState, useRef, useEffect } from "react";
2+
import { createPortal } from "react-dom";
3+
14
interface HeaderProps {
25
title?: string;
36
onFormat?: () => void;
47
onSettingsOpen?: () => void;
58
onDownload?: () => void;
9+
onExportHTML?: () => void;
610
onShare?: () => void;
711
onZenMode?: () => void;
812
isZenMode?: boolean;
@@ -13,11 +17,51 @@ function Header({
1317
onFormat,
1418
onSettingsOpen,
1519
onDownload,
20+
onExportHTML,
1621
onShare,
1722
onZenMode,
1823
}: HeaderProps) {
24+
const [isDownloadMenuOpen, setIsDownloadMenuOpen] = useState(false);
25+
const [menuPosition, setMenuPosition] = useState({ top: 0, right: 0 });
26+
const downloadMenuRef = useRef<HTMLDivElement>(null);
27+
const buttonRef = useRef<HTMLButtonElement>(null);
28+
29+
// Toggle dropdown and calculate position synchronously
30+
const toggleDropdown = () => {
31+
if (!isDownloadMenuOpen && buttonRef.current) {
32+
const rect = buttonRef.current.getBoundingClientRect();
33+
setMenuPosition({
34+
top: rect.bottom + 4,
35+
right: window.innerWidth - rect.right,
36+
});
37+
}
38+
setIsDownloadMenuOpen(!isDownloadMenuOpen);
39+
};
40+
41+
// Close dropdown when clicking outside
42+
useEffect(() => {
43+
const handleClickOutside = (event: MouseEvent) => {
44+
if (
45+
downloadMenuRef.current &&
46+
!downloadMenuRef.current.contains(event.target as Node) &&
47+
buttonRef.current &&
48+
!buttonRef.current.contains(event.target as Node)
49+
) {
50+
setIsDownloadMenuOpen(false);
51+
}
52+
};
53+
54+
if (isDownloadMenuOpen) {
55+
document.addEventListener("mousedown", handleClickOutside);
56+
}
57+
58+
return () => {
59+
document.removeEventListener("mousedown", handleClickOutside);
60+
};
61+
}, [isDownloadMenuOpen]);
62+
1963
return (
20-
<header className="sticky top-0 z-50 w-full bg-slate-800 border-b border-slate-700 shadow-lg flex-shrink-0 select-none">
64+
<header className="sticky top-0 z-[9999] w-full bg-slate-800 border-b border-slate-700 shadow-lg flex-shrink-0 select-none">
2165
<div className="max-w-full mx-auto px-4 sm:px-6 lg:px-8">
2266
<div className="flex items-center justify-between h-14">
2367
{/* Logo / Title */}
@@ -54,16 +98,25 @@ function Header({
5498
</svg>
5599
<span className="hidden sm:inline">Prettier</span>
56100
</button>
57-
<button
58-
onClick={onDownload}
59-
className="px-2 sm:px-3 py-1.5 text-sm font-medium text-slate-300 hover:text-white hover:bg-slate-700 rounded-md transition-colors flex items-center gap-1.5"
60-
title="Download project as ZIP"
61-
>
62-
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
63-
<path strokeLinecap="round" strokeLinejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
64-
</svg>
65-
<span className="hidden sm:inline">Download</span>
66-
</button>
101+
102+
{/* Download Dropdown */}
103+
<div className="relative">
104+
<button
105+
ref={buttonRef}
106+
onClick={toggleDropdown}
107+
className="px-2 sm:px-3 py-1.5 text-sm font-medium text-slate-300 hover:text-white hover:bg-slate-700 rounded-md transition-colors flex items-center gap-1.5"
108+
title="Export options"
109+
>
110+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
111+
<path strokeLinecap="round" strokeLinejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
112+
</svg>
113+
<span className="hidden sm:inline">Export</span>
114+
<svg className="w-3 h-3 ml-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
115+
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
116+
</svg>
117+
</button>
118+
</div>
119+
67120
<button
68121
onClick={onShare}
69122
className="px-2 sm:px-3 py-1.5 text-sm font-medium text-slate-300 hover:text-white hover:bg-slate-700 rounded-md transition-colors flex items-center gap-1.5"
@@ -85,10 +138,53 @@ function Header({
85138
</svg>
86139
<span className="hidden sm:inline">Settings</span>
87140
</button>
88-
89141
</nav>
90142
</div>
91143
</div>
144+
145+
{/* Dropdown Menu - Rendered via Portal */}
146+
{isDownloadMenuOpen && createPortal(
147+
<div
148+
ref={downloadMenuRef}
149+
className="fixed w-52 bg-slate-700 rounded-lg shadow-xl border border-slate-600 py-1 z-[99999] animate-in fade-in slide-in-from-top-2 duration-150"
150+
style={{
151+
top: menuPosition.top,
152+
right: menuPosition.right,
153+
}}
154+
>
155+
<button
156+
onClick={() => {
157+
onDownload?.();
158+
setIsDownloadMenuOpen(false);
159+
}}
160+
className="w-full px-4 py-2.5 text-sm text-left text-slate-200 hover:bg-slate-600 flex items-center gap-3 transition-colors"
161+
>
162+
<svg className="w-4 h-4 text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
163+
<path strokeLinecap="round" strokeLinejoin="round" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
164+
</svg>
165+
<div>
166+
<div className="font-medium">Download as ZIP</div>
167+
<div className="text-xs text-slate-400">Separate HTML, CSS, JS files</div>
168+
</div>
169+
</button>
170+
<button
171+
onClick={() => {
172+
onExportHTML?.();
173+
setIsDownloadMenuOpen(false);
174+
}}
175+
className="w-full px-4 py-2.5 text-sm text-left text-slate-200 hover:bg-slate-600 flex items-center gap-3 transition-colors"
176+
>
177+
<svg className="w-4 h-4 text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
178+
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
179+
</svg>
180+
<div>
181+
<div className="font-medium">Export as Single HTML</div>
182+
<div className="text-xs text-slate-400">CSS & JS embedded inline</div>
183+
</div>
184+
</button>
185+
</div>,
186+
document.body
187+
)}
92188
</header>
93189
);
94190
}

src/components/Layout/Layout.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ function Layout() {
2020
mainContentRef.current?.downloadProject();
2121
};
2222

23+
const handleExportHTML = () => {
24+
mainContentRef.current?.exportHTML();
25+
};
26+
2327
const handleShare = () => {
2428
mainContentRef.current?.shareCode();
2529
};
@@ -39,6 +43,7 @@ function Layout() {
3943
onFormat={handleFormat}
4044
onSettingsOpen={handleSettingsOpen}
4145
onDownload={handleDownload}
46+
onExportHTML={handleExportHTML}
4247
onShare={handleShare}
4348
onZenMode={toggleZenMode}
4449
isZenMode={isZenMode}

0 commit comments

Comments
 (0)