1+ import { useState , useRef , useEffect } from "react" ;
2+ import { createPortal } from "react-dom" ;
3+
14interface 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}
0 commit comments