Remove abandoned HTML Preview Tool files and fix ESLint warnings

- Removed unused imports (Key, Palette, QrCode) from Layout.js
- Deleted all HTML Preview Tool files from repository
- Removed HtmlPreviewTool import and route from App.js
- Build now completes successfully without ESLint warnings
- Ready for deployment in CI environment
This commit is contained in:
dwindown
2025-08-04 13:27:19 +07:00
parent 45ddccc2f6
commit bc7e2a8986
7 changed files with 3 additions and 2045 deletions

View File

@@ -9,7 +9,7 @@ import Base64Tool from './pages/Base64Tool';
import CsvJsonTool from './pages/CsvJsonTool'; import CsvJsonTool from './pages/CsvJsonTool';
import BeautifierTool from './pages/BeautifierTool'; import BeautifierTool from './pages/BeautifierTool';
import DiffTool from './pages/DiffTool'; import DiffTool from './pages/DiffTool';
import HtmlPreviewTool from './pages/HtmlPreviewTool';
import './index.css'; import './index.css';
function App() { function App() {
@@ -25,7 +25,7 @@ function App() {
<Route path="/csv-json" element={<CsvJsonTool />} /> <Route path="/csv-json" element={<CsvJsonTool />} />
<Route path="/beautifier" element={<BeautifierTool />} /> <Route path="/beautifier" element={<BeautifierTool />} />
<Route path="/diff" element={<DiffTool />} /> <Route path="/diff" element={<DiffTool />} />
<Route path="/html-preview" element={<HtmlPreviewTool />} />
</Routes> </Routes>
</Layout> </Layout>
</Router> </Router>

View File

@@ -1,6 +1,6 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import { Home, Hash, FileText, Key, Palette, QrCode, FileSpreadsheet, Wand2, GitCompare, Menu, X, Database, LinkIcon, Code2, ChevronDown } from 'lucide-react'; import { Home, Hash, FileText, FileSpreadsheet, Wand2, GitCompare, Menu, X, Database, LinkIcon, Code2, ChevronDown } from 'lucide-react';
import ThemeToggle from './ThemeToggle'; import ThemeToggle from './ThemeToggle';
const Layout = ({ children }) => { const Layout = ({ children }) => {

View File

@@ -1,451 +0,0 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import ToolLayout from '../components/ToolLayout';
import PreviewFrame from './components/PreviewFrame.fresh';
import Toolbar from './components/Toolbar';
import CodeInputs from './components/CodeInputs';
import InspectorSidebar from './components/InspectorSidebar';
import ElementEditor from './components/ElementEditor';
import '../styles/device-frames.css';
const HtmlPreviewTool = () => {
const [htmlInput, setHtmlInput] = useState('');
const [cssInput, setCssInput] = useState('');
const [jsInput, setJsInput] = useState('');
const [selectedDevice, setSelectedDevice] = useState('mobile');
const [inspectMode, setInspectMode] = useState(false);
const [inspectedElementInfo, setInspectedElementInfo] = useState(null);
const [isFullscreen, setIsFullscreen] = useState(false);
const [showSidebar, setShowSidebar] = useState(true);
const [forceRender, setForceRender] = useState(0);
// Separate inspector state to prevent iframe updates during inspector operations
const [inspectorHtmlState, setInspectorHtmlState] = useState('');
const [isInspectorActive, setIsInspectorActive] = useState(false);
// ENHANCED OPTION A: PreviewFrame API reference
const previewFrameRef = useRef(null);
// Debug: Monitor inspectedElementInfo changes and force re-render
useEffect(() => {
console.log('🔍 STATE CHANGE: inspectedElementInfo updated to:', inspectedElementInfo);
if (inspectedElementInfo) {
console.log('🔄 FORCING COMPONENT RE-RENDER for inspector sidebar');
// Force a re-render by updating a dummy state
setForceRender(prev => prev + 1);
}
}, [inspectedElementInfo, forceRender]);
const handleElementClick = useCallback((elementInfo) => {
console.log('🔎 ENHANCED ELEMENT CLICK:', elementInfo);
if (elementInfo) {
console.log('✅ ENHANCED INSPECTOR: Activating with cascade-id:', elementInfo.cascadeId);
setInspectedElementInfo(elementInfo);
setIsInspectorActive(true);
console.log('🎯 ENHANCED INSPECTOR: Sidebar activated, iframe DOM is source of truth');
// Debug: Force re-render check
setTimeout(() => {
console.log('🔍 POST-SET DEBUG: inspectedElementInfo should now be:', elementInfo);
setForceRender(prev => prev + 1); // Force re-render after state is set
}, 10);
}
}, []);
const cleanupInspectorState = useCallback(() => {
console.log('🧹 ENHANCED OPTION A: Cleaning up inspector state without triggering iframe refresh');
console.log('🚨 DEBUG: cleanupInspectorState called - clearing inspectedElementInfo');
console.trace('🔍 STACK TRACE: cleanupInspectorState called from:');
setInspectedElementInfo(null);
setInspectMode(false);
// ENHANCED OPTION A: Don't call setHtmlInput during cleanup
// The iframe DOM cleanup will be handled by PreviewFrame directly
// Only clean up React state, not HTML input
console.log('✅ ENHANCED CLEANUP: Inspector state cleared without iframe refresh');
}, []);
// ESC key handler to deactivate inspect mode
useEffect(() => {
const handleKeyDown = (event) => {
if (event.key === 'Escape' && inspectMode) {
console.log('⌨️ ESC key pressed - deactivating inspect mode');
cleanupInspectorState();
}
};
// Add event listener
document.addEventListener('keydown', handleKeyDown);
// Cleanup
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [inspectMode, cleanupInspectorState]);
useEffect(() => {
// ENHANCED OPTION A: Skip cascade ID injection during inspector operations
if (inspectedElementInfo) {
console.log('🚫 ENHANCED OPTION A: Skipping cascade ID injection during inspector operations');
return;
}
if (!htmlInput.trim()) return;
const isFullDocument = htmlInput.trim().toLowerCase().includes('<html');
console.log('🔧 HTML Processing Start:', { isFullDocument, inputLength: htmlInput.length });
// If it's already a full document, don't modify it unless we need to add cascade IDs
if (isFullDocument) {
const parser = new DOMParser();
const doc = parser.parseFromString(htmlInput, 'text/html');
let modified = false;
let idCounter = 0;
// Count existing cascade IDs
doc.querySelectorAll('[data-cascade-id]').forEach(el => {
const idNum = parseInt(el.getAttribute('data-cascade-id').split('-')[1], 10);
if (!isNaN(idNum) && idNum >= idCounter) {
idCounter = idNum + 1;
}
});
// Add cascade IDs to elements that don't have them
doc.querySelectorAll('*').forEach(el => {
if (!el.hasAttribute('data-cascade-id') &&
el.tagName.toLowerCase() !== 'body' &&
el.tagName.toLowerCase() !== 'html' &&
el.tagName.toLowerCase() !== 'head' &&
el.tagName.toLowerCase() !== 'title' &&
el.tagName.toLowerCase() !== 'meta' &&
el.tagName.toLowerCase() !== 'link' &&
el.tagName.toLowerCase() !== 'style' &&
el.tagName.toLowerCase() !== 'script') {
el.setAttribute('data-cascade-id', `cascade-${idCounter++}`);
modified = true;
}
});
if (modified) {
const newHtml = doc.documentElement.outerHTML;
console.log('✅ Full document processed, cascade IDs added');
console.log('🚨 CASCADE ID INJECTION: About to call setHtmlInput (POTENTIAL IFRAME REFRESH TRIGGER)');
setHtmlInput(newHtml);
}
} else {
// It's a fragment, process normally
const parser = new DOMParser();
const doc = parser.parseFromString(htmlInput, 'text/html');
let modified = false;
let idCounter = 0;
doc.body.querySelectorAll('[data-cascade-id]').forEach(el => {
const idNum = parseInt(el.getAttribute('data-cascade-id').split('-')[1], 10);
if (!isNaN(idNum) && idNum >= idCounter) {
idCounter = idNum + 1;
}
});
doc.body.querySelectorAll('*').forEach(el => {
if (!el.hasAttribute('data-cascade-id') && el.tagName.toLowerCase() !== 'body' && el.tagName.toLowerCase() !== 'html' && el.tagName.toLowerCase() !== 'head') {
el.setAttribute('data-cascade-id', `cascade-${idCounter++}`);
modified = true;
}
});
if (modified) {
const newHtml = doc.body.innerHTML;
console.log('✅ Fragment processed, cascade IDs added');
console.log('🚨 CASCADE ID INJECTION: About to call setHtmlInput (POTENTIAL IFRAME REFRESH TRIGGER)');
setHtmlInput(newHtml);
}
}
}, [htmlInput]);
const createDuplicateInCodeBox = useCallback((elementInfo) => {
const cascadeId = elementInfo.attributes['data-cascade-id'];
if (!cascadeId) {
console.error('❌ Cannot create duplicate: Element is missing data-cascade-id.');
return false;
}
// Use stored iframe DOM that contains the cascade-id, fallback to inspector state
const currentHtml = window.currentIframeDom || inspectorHtmlState || htmlInput;
console.log('🎯 INSPECTOR: Creating duplicates using iframe DOM with cascade-id');
if (window.currentIframeDom) {
console.log('✅ Using current iframe DOM with cascade-id');
} else {
console.log('⚠️ Fallback to inspector state (cascade-id may be missing)');
}
const processHtml = (currentHtml) => {
const parser = new DOMParser();
const doc = parser.parseFromString(currentHtml, 'text/html');
const originalElement = doc.querySelector(`[data-cascade-id="${cascadeId}"]`);
if (!originalElement) {
console.error(`❌ Could not find element with ${cascadeId} in HTML.`);
return currentHtml;
}
const hiddenElement = originalElement.cloneNode(true);
hiddenElement.setAttribute('data-original', 'true');
hiddenElement.style.display = 'none';
const visibleElement = originalElement.cloneNode(true);
visibleElement.setAttribute('data-original', 'false');
originalElement.parentNode.insertBefore(hiddenElement, originalElement);
originalElement.parentNode.insertBefore(visibleElement, originalElement);
originalElement.remove();
console.log(`✅ Successfully created duplicates for ${cascadeId}`);
// Preserve the original HTML structure (full document vs fragment)
const isFragment = !currentHtml.trim().toLowerCase().startsWith('<html');
return isFragment ? doc.body.innerHTML : doc.documentElement.outerHTML;
};
// ENHANCED OPTION A: Process HTML and activate inspector (iframe DOM becomes source of truth)
const processedHtml = processHtml(currentHtml);
setInspectorHtmlState(processedHtml);
setIsInspectorActive(true);
window.isInspectorActive = true;
console.log('🔍 INSPECTOR ACTIVATED: Iframe DOM is now source of truth, refreshes disabled');
return true;
}, []);
const handleRefresh = useCallback(() => {
// Refresh is handled by PreviewFrame component
console.log('🔄 Refreshing preview...');
}, []);
const toggleFullscreen = useCallback((targetDevice = null) => {
setIsFullscreen(prev => {
const newFullscreen = !prev;
// When exiting fullscreen (going to non-fullscreen), always switch to mobile
if (!newFullscreen) {
setSelectedDevice('mobile');
console.log('📱 Exiting fullscreen: Switched to mobile view');
}
return newFullscreen;
});
if (targetDevice) {
setSelectedDevice(targetDevice);
}
}, []);
const toggleSidebar = useCallback(() => {
setShowSidebar(prev => !prev);
}, []);
// ENHANCED OPTION A: Commit iframe DOM changes to HTML input using new API
const saveInspectorChanges = useCallback(() => {
console.log('💾 ENHANCED COMMIT: Using PreviewFrame API to commit changes');
if (!previewFrameRef.current) {
console.error('❌ COMMIT FAILED: PreviewFrame ref not available');
return;
}
try {
// Use Enhanced Option A API to get iframe DOM content
const committedHtml = previewFrameRef.current.getIframeContent();
if (committedHtml) {
// ENHANCED OPTION A: Update HTML input with committed changes
// This is an EXPLICIT SAVE operation, so iframe refresh is expected and correct
setHtmlInput(committedHtml);
console.log('✅ ENHANCED COMMIT: Changes committed successfully');
console.log('📊 COMMIT: HTML updated with iframe DOM content');
// Close inspector and reset state
console.log('🚨 DEBUG: saveInspectorChanges called - clearing inspectedElementInfo');
setInspectedElementInfo(null);
setInspectMode(false);
setIsInspectorActive(false);
console.log('🔄 ENHANCED COMMIT: Inspector closed, iframe will refresh with new content');
} else {
console.error('❌ ENHANCED COMMIT: Failed to extract iframe DOM content');
}
} catch (error) {
console.error('❌ ENHANCED COMMIT ERROR:', error);
}
}, [previewFrameRef]);
// ENHANCED OPTION A: Close inspector and reset state
const closeInspector = useCallback(() => {
console.log('❌ ENHANCED CLOSE: Closing inspector and resetting state');
// Reset all inspector state
console.log('🚨 DEBUG: closeInspector called - clearing inspectedElementInfo');
setInspectedElementInfo(null);
setInspectMode(false);
setIsInspectorActive(false);
setInspectorHtmlState('');
// Use PreviewFrame API to cancel changes if available
if (previewFrameRef?.current?.cancelChanges) {
previewFrameRef.current.cancelChanges();
}
console.log('✅ ENHANCED CLOSE: Inspector closed, iframe DOM reset');
}, [previewFrameRef]);
const closeInspectorLegacy = useCallback(() => {
// ENHANCED OPTION A: Close inspector and clear active flag (legacy)
setIsInspectorActive(false);
setInspectorHtmlState('');
window.isInspectorActive = false;
console.log('❌ INSPECTOR CLOSED: Iframe refreshes re-enabled');
cleanupInspectorState();
}, [cleanupInspectorState]);
if (isFullscreen) {
return (
<div className="fixed inset-0 bg-gray-100 dark:bg-gray-900 z-50 flex flex-col">
{/* Main content area */}
<div className="flex-1 flex overflow-hidden">
{/* Left sidebar - Code inputs */}
{showSidebar && (
<div className="w-96 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 flex flex-col">
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Code Editor</h2>
</div>
<div className="flex-1 flex flex-col p-4">
<CodeInputs
htmlInput={htmlInput}
setHtmlInput={setHtmlInput}
cssInput={cssInput}
setCssInput={setCssInput}
jsInput={jsInput}
setJsInput={setJsInput}
isFullscreen={isFullscreen}
/>
</div>
</div>
)}
{/* Center - Preview */}
<div className="flex-1 flex flex-col bg-gray-50 dark:bg-gray-900">
<div className="flex-1 p-4 overflow-hidden">
<PreviewFrame
ref={previewFrameRef}
htmlInput={isInspectorActive ? inspectorHtmlState : htmlInput}
cssInput={cssInput}
jsInput={jsInput}
selectedDevice={selectedDevice}
isInspectModeActive={inspectMode}
onElementClick={handleElementClick}
isFullscreen={isFullscreen}
/>
</div>
</div>
{/* Inspector Sidebar */}
{inspectedElementInfo && (
<InspectorSidebar
inspectedElementInfo={inspectedElementInfo}
htmlInput={htmlInput}
setHtmlInput={setHtmlInput}
onClose={closeInspector}
onSave={saveInspectorChanges}
previewFrameRef={previewFrameRef}
/>
)}
</div>
{/* Bottom toolbar */}
<Toolbar
selectedDevice={selectedDevice}
setSelectedDevice={setSelectedDevice}
isInspectModeActive={inspectMode}
setInspectMode={setInspectMode}
isFullscreen={isFullscreen}
onRefresh={handleRefresh}
onToggleFullscreen={toggleFullscreen}
onToggleSidebar={toggleSidebar}
showSidebar={showSidebar}
cleanupInspectorState={cleanupInspectorState}
inspectedElementInfo={inspectedElementInfo}
/>
</div>
);
}
return (
<ToolLayout title="HTML Preview Tool">
<div className={`flex h-full ${inspectedElementInfo ? 'gap-4' : 'gap-6'}`}>
{/* Left column - Code inputs */}
<div className={`flex flex-col transition-all duration-300 ${
inspectedElementInfo ? 'flex-1' : 'w-1/2'
}`}>
<CodeInputs
htmlInput={htmlInput}
setHtmlInput={setHtmlInput}
cssInput={cssInput}
setCssInput={setCssInput}
jsInput={jsInput}
setJsInput={setJsInput}
isFullscreen={isFullscreen}
/>
</div>
{/* Middle column - Preview */}
<div className={`flex flex-col transition-all duration-300 ${
inspectedElementInfo ? 'flex-1' : 'w-1/2'
}`}>
<div className="flex-1 bg-gray-50 dark:bg-gray-900 rounded-lg overflow-hidden">
<PreviewFrame
ref={previewFrameRef}
htmlInput={htmlInput}
cssInput={cssInput}
jsInput={jsInput}
selectedDevice={selectedDevice}
isInspectModeActive={inspectMode}
onElementClick={handleElementClick}
isFullscreen={isFullscreen}
/>
</div>
</div>
{/* ENHANCED OPTION A: Inspector Sidebar */}
{inspectedElementInfo && (
<div className="w-80 flex flex-col bg-gray-50 dark:bg-gray-900 rounded-lg overflow-hidden">
<InspectorSidebar
inspectedElementInfo={inspectedElementInfo}
htmlInput={htmlInput}
setHtmlInput={setHtmlInput}
onClose={closeInspector}
onSave={saveInspectorChanges}
previewFrameRef={previewFrameRef}
/>
</div>
)}
</div>
{/* Bottom toolbar */}
<div className="mt-6">
<Toolbar
selectedDevice={selectedDevice}
setSelectedDevice={setSelectedDevice}
isInspectModeActive={inspectMode}
setInspectMode={setInspectMode}
isFullscreen={isFullscreen}
onRefresh={handleRefresh}
onToggleFullscreen={toggleFullscreen}
onToggleSidebar={toggleSidebar}
showSidebar={showSidebar}
cleanupInspectorState={cleanupInspectorState}
inspectedElementInfo={inspectedElementInfo}
/>
</div>
</ToolLayout>
);
};
export default HtmlPreviewTool;

View File

@@ -1,461 +0,0 @@
import React, { useState, useEffect, useCallback } from 'react';
import ToolLayout from '../components/ToolLayout';
import PreviewFrame from './components/PreviewFrame';
import Toolbar from './components/Toolbar';
import CodeInputs from './components/CodeInputs';
import InspectorSidebar from './components/InspectorSidebar';
import '../styles/device-frames.css';
const HtmlPreviewTool = () => {
const [htmlInput, setHtmlInput] = useState('');
const [cssInput, setCssInput] = useState('');
const [jsInput, setJsInput] = useState('');
const [selectedDevice, setSelectedDevice] = useState('mobile');
const [inspectMode, setInspectMode] = useState(false);
const [inspectedElementInfo, setInspectedElementInfo] = useState(null);
const [isFullscreen, setIsFullscreen] = useState(false);
const [showSidebar, setShowSidebar] = useState(true);
const cleanupInspectorState = useCallback(() => {
console.log('🧹 Cleaning up inspector state and data-original attributes');
setInspectedElementInfo(null);
setInspectMode(false);
setHtmlInput(currentHtml => {
const parser = new DOMParser();
const doc = parser.parseFromString(currentHtml, 'text/html');
let modified = false;
// Remove hidden backup elements
doc.querySelectorAll('[data-original="true"]').forEach(el => {
el.remove();
modified = true;
});
// Clean attributes from visible elements
doc.querySelectorAll('[data-original="false"]').forEach(el => {
el.removeAttribute('data-original');
modified = true;
});
if (modified) {
const newHtml = doc.body.innerHTML;
console.log('🧹 Cleaned HTML from', currentHtml.length, 'to', newHtml.length, 'chars');
return newHtml;
}
return currentHtml;
});
}, []);
useEffect(() => {
if (inspectedElementInfo) return;
const parser = new DOMParser();
const doc = parser.parseFromString(htmlInput, 'text/html');
let modified = false;
let idCounter = 0;
doc.body.querySelectorAll('[data-cascade-id]').forEach(el => {
const idNum = parseInt(el.getAttribute('data-cascade-id').split('-')[1], 10);
if (!isNaN(idNum) && idNum >= idCounter) {
idCounter = idNum + 1;
}
});
doc.body.querySelectorAll('*').forEach(el => {
if (!el.hasAttribute('data-cascade-id') && el.tagName.toLowerCase() !== 'body' && el.tagName.toLowerCase() !== 'html' && el.tagName.toLowerCase() !== 'head') {
el.setAttribute('data-cascade-id', `cascade-${idCounter++}`);
modified = true;
}
});
if (modified) {
const isFragment = !htmlInput.trim().toLowerCase().startsWith('<html');
const newHtml = isFragment ? doc.body.innerHTML : doc.documentElement.outerHTML;
if (newHtml !== htmlInput) {
setHtmlInput(newHtml);
}
}
}, [htmlInput, inspectedElementInfo]);
const createDuplicateInCodeBox = useCallback((elementInfo) => {
const cascadeId = elementInfo.attributes['data-cascade-id'];
if (!cascadeId) {
console.error('❌ Cannot create duplicate: Element is missing data-cascade-id.');
return false;
}
setHtmlInput(currentHtml => {
const parser = new DOMParser();
const doc = parser.parseFromString(currentHtml, 'text/html');
const originalElement = doc.querySelector(`[data-cascade-id="${cascadeId}"]`);
if (!originalElement) {
console.error(`❌ Could not find element with ${cascadeId} in HTML.`);
return currentHtml;
}
const hiddenElement = originalElement.cloneNode(true);
hiddenElement.setAttribute('data-original', 'true');
hiddenElement.style.display = 'none';
const visibleElement = originalElement.cloneNode(true);
visibleElement.setAttribute('data-original', 'false');
originalElement.parentNode.insertBefore(hiddenElement, originalElement);
originalElement.parentNode.insertBefore(visibleElement, originalElement);
console.log(`✅ Successfully created duplicates for ${cascadeId}`);
return doc.body.innerHTML;
});
return true;
}, []);
const handleElementClick = useCallback((elementInfo) => {
console.log('🔍 Element selected for inspection:', elementInfo);
if (createDuplicateInCodeBox(elementInfo)) {
setInspectedElementInfo(elementInfo);
setInspectMode(false);
}
}, [createDuplicateInCodeBox]);
doc.removeEventListener('click', handleIframeClick, true);
};
}
}
}, [htmlInput, cssInput, jsInput, inspectMode, createDuplicateInCodeBox, cleanupInspectorState]);
const handleRefresh = useCallback(() => {
const handleRefresh = () => {
setHtmlInput(prev => prev);
setCssInput(prev => prev);
setJsInput(prev => prev);
};
const toggleFullscreen = (targetDevice = null) => {
setIsFullscreen(!isFullscreen);
if (!isFullscreen) {
setSidebarCollapsed(false);
if (targetDevice) {
setSelectedDevice(targetDevice);
}
}
};
const toggleSidebar = () => {
setSidebarCollapsed(!sidebarCollapsed);
};
return (
<ToolLayout
title="HTML Preview & Inspector"
mainContent={
<div className={`flex h-full ${isFullscreen ? 'flex-row' : 'flex-col'}`}>
{/* Left Sidebar - Code Input */}
<div className={`flex flex-col space-y-4 transition-all duration-300 ${
isFullscreen
? `${sidebarCollapsed ? 'hidden' : 'w-80'} bg-gray-50 dark:bg-gray-800 p-4 border-r border-gray-200 dark:border-gray-700 overflow-y-auto h-[calc(100vh-4rem)]`
: 'p-4'
}`}>
<div>
<label htmlFor="html-input" className="block text-lg font-medium text-gray-900 dark:text-gray-100 mb-1">HTML Code</label>
<textarea
id="html-input"
className="w-full p-3 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-blue-500 focus:border-blue-500 font-mono text-sm"
rows={isFullscreen ? "15" : (showCss || showJs ? "10" : "30")}
value={htmlInput}
onChange={(e) => setHtmlInput(e.target.value)}
placeholder="<!-- Your HTML code here -->"
></textarea>
<div className="flex gap-2 mt-2">
<button
onClick={() => setShowCss(!showCss)}
className={`px-3 py-1 text-sm rounded-md transition-colors ${showCss ? 'bg-blue-500 text-white hover:bg-blue-600' : 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'}`}>
{showCss ? 'Hide' : 'Show'} CSS
</button>
<button
onClick={() => setShowJs(!showJs)}
className={`px-3 py-1 text-sm rounded-md transition-colors ${showJs ? 'bg-blue-500 text-white hover:bg-blue-600' : 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'}`}>
{showJs ? 'Hide' : 'Show'} JS
</button>
</div>
</div>
{showCss && (
<div>
<label htmlFor="css-input" className="block text-lg font-medium text-gray-900 dark:text-gray-100 mb-1">CSS Styles (Optional)</label>
<textarea
id="css-input"
className="w-full p-3 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-blue-500 focus:border-blue-500 font-mono text-sm"
rows="8"
value={cssInput}
onChange={(e) => setCssInput(e.target.value)}
placeholder="/* Your CSS styles here */"
></textarea>
</div>
)}
{showJs && (
<div>
<label htmlFor="js-input" className="block text-lg font-medium text-gray-900 dark:text-gray-100 mb-1">JavaScript (Optional)</label>
<textarea
id="js-input"
className="w-full p-3 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-blue-500 focus:border-blue-500 font-mono text-sm"
rows="8"
value={jsInput}
onChange={(e) => setJsInput(e.target.value)}
placeholder="// Your JavaScript code here"
></textarea>
</div>
)}
</div>
{/* Preview Area */}
<div className={`flex flex-col transition-all duration-300 ${isFullscreen ? `flex-1 h-[calc(100vh-4rem)]` : 'flex-1'}`}>
{!isFullscreen && (
<div className="px-4 pt-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-1">Preview</h3>
</div>
)}
<div className={`flex justify-center items-center bg-gray-100 dark:bg-gray-900 ${isFullscreen ? 'flex-1 rounded-none p-4' : 'flex-grow rounded-md m-4'}`}>
<div
className={`relative transition-all duration-300 ease-in-out shadow-lg`}
style={{
width: isFullscreen ? devices[selectedDevice].width : devices['mobile'].width,
height: isFullscreen ? devices[selectedDevice].height : devices['mobile'].height,
}}
>
<iframe
ref={iframeRef}
title="HTML Preview"
className="w-full h-full border-none bg-white dark:bg-gray-800 rounded-lg"
sandbox="allow-scripts allow-forms allow-popups allow-modals allow-same-origin"
></iframe>
</div>
</div>
{/* Preview Tools - Bottom Toolbar */}
<div className={`flex justify-between items-center p-4 border-t border-gray-200 dark:border-gray-700 ${isFullscreen ? 'fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-900 z-50' : ''}`}>
<div className="flex items-center space-x-2">
{isFullscreen && (
<button onClick={toggleSidebar} className={`p-2 rounded-md ${!sidebarCollapsed ? 'bg-green-500 text-white' : 'bg-gray-200 dark:bg-gray-700'} hover:bg-green-600`} title="Toggle Code Sidebar">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" /></svg>
</button>
)}
</div>
<div className="flex justify-center items-center space-x-2">
<button onClick={handleRefresh} className="p-2 rounded-md bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600" title="Refresh Preview"><RefreshCw size={20} /></button>
{isFullscreen && (
<button onClick={() => setInspectMode(!inspectMode)} className={`p-2 rounded-md ${inspectMode ? 'bg-purple-500 text-white' : 'bg-gray-200 dark:bg-gray-700'} hover:bg-purple-600`} title="Toggle Inspect Mode"><Inspect size={20} /></button>
)}
{Object.entries(devices).map(([key, { icon: DeviceIcon }]) => (
<button
key={key}
onClick={() => isFullscreen ? setSelectedDevice(key) : toggleFullscreen(key)}
className={`p-2 rounded-md ${selectedDevice === key && isFullscreen ? 'bg-blue-500 text-white' : 'bg-gray-200 dark:bg-gray-700'} hover:bg-blue-600`}
title={`${key.charAt(0).toUpperCase() + key.slice(1)} View`}>
<DeviceIcon size={20} />
</button>
))}
</div>
<div className="flex items-center space-x-2">
<button onClick={() => toggleFullscreen()} className={`px-4 py-2 rounded-lg transition-colors flex items-center gap-2 ${isFullscreen ? 'bg-red-600 text-white hover:bg-red-700' : 'bg-blue-600 text-white hover:bg-blue-700'}`}>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{isFullscreen ? <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> : <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />}
</svg>
{isFullscreen ? 'Exit' : 'Fullscreen'}
</button>
</div>
</div>
</div>
{isFullscreen && inspectedElementInfo && (
<div className="w-80 bg-gray-50 dark:bg-gray-800 p-4 border-l border-gray-200 dark:border-gray-700 overflow-y-auto flex-shrink-0 h-[calc(100vh-4rem)]">
<div className="flex justify-between items-center mb-4">
<h4 className="text-lg font-medium text-gray-900 dark:text-gray-100">Element Editor</h4>
<button onClick={cleanupInspectorState} className="text-gray-600 dark:text-gray-400 hover:text-gray-800" title="Close">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
<ElementEditor
key={inspectedElementInfo.attributes['data-cascade-id']}
htmlInput={htmlInput}
setHtmlInput={setHtmlInput}
onClose={cleanupInspectorState}
/>
</div>
)}
</div>
}
/>
);
};
// ##################################################################################
// # ELEMENT EDITOR COMPONENT (REWRITTEN BASED ON USER'S EXACT LOGIC)
// ##################################################################################
const ElementEditor = ({ htmlInput, setHtmlInput, onClose }) => {
const [edited, setEdited] = useState(null);
// On mount, parse the element being edited (the one with data-original="false")
useEffect(() => {
const editableElementRegex = /<([a-zA-Z0-9]+)((?:\s+[\w-]+(?:="[^"]*")?)*?)\s+data-original="false"((?:\s+[\w-]+(?:="[^"]*")?)*?)>(.*?)<\/\1>|<([a-zA-Z0-9]+)((?:\s+[\w-]+(?:="[^"]*")?)*?)\s+data-original="false"((?:\s+[\w-]+(?:="[^"]*")?)*?)\/>/s;
const match = htmlInput.match(editableElementRegex);
if (match) {
const isSelfClosing = !!match[5];
const tagName = isSelfClosing ? match[5] : match[1];
const allAttributesString = (isSelfClosing ? (match[6] || '') + (match[7] || '') : (match[2] || '') + (match[3] || ''));
const innerHTML = isSelfClosing ? '' : match[4] || '';
const attributes = {};
const attrRegex = /([\w-]+)(?:="([^"]*)")?/g;
let attrMatch;
while ((attrMatch = attrRegex.exec(allAttributesString)) !== null) {
attributes[attrMatch[1]] = attrMatch[2] === undefined ? true : attrMatch[2];
}
const tempDiv = document.createElement('div');
tempDiv.innerHTML = innerHTML;
const initialState = {
tagName: tagName,
id: attributes.id || '',
className: attributes.class || '',
innerText: tempDiv.textContent || '',
...Object.fromEntries(Object.entries(attributes).filter(([key]) => key !== 'id' && key !== 'class')),
};
setEdited(initialState);
}
}, [htmlInput]);
// 3.A: On field change, update the element with data-original="false" in the code box
const handleFieldChange = (field, value) => {
const newEditedState = { ...edited, [field]: value };
setEdited(newEditedState);
setHtmlInput(currentHtml => {
const newElementHtml = buildElementHtml(newEditedState);
const editableElementRegex = /<[^>]+data-original="false"[^>]*>.*?<\/[^>]+>|<[^>]+data-original="false"[^>]*\/>/s;
if (!editableElementRegex.test(currentHtml)) {
console.error("Live Update Error: Cannot find element with data-original='false' to replace.");
return currentHtml;
}
return currentHtml.replace(editableElementRegex, newElementHtml);
});
};
const buildElementHtml = (state) => {
const { tagName, id, className, innerText, ...otherAttrs } = state;
const isSelfClosing = ['img', 'input', 'br', 'hr', 'meta', 'link'].includes(tagName.toLowerCase());
let attrs = 'data-original="false"';
if (id) attrs += ` id="${id}"`;
if (className) attrs += ` class="${className}"`;
for (const [key, value] of Object.entries(otherAttrs)) {
if (value === true) {
attrs += ` ${key}`;
} else if (value) {
attrs += ` ${key}="${value}"`;
}
}
if (isSelfClosing) {
return `<${tagName} ${attrs.trim()} />`;
} else {
const tempDiv = document.createElement('div');
tempDiv.innerText = innerText || '';
return `<${tagName} ${attrs.trim()}>${tempDiv.innerHTML}</${tagName}>`;
}
};
// 4. On Save, finalize the changes
const handleSave = () => {
setHtmlInput(currentHtml => {
const pairRegex = /(<[^>]+data-original="true"[^>]*style="display:none;"[^>]*>.*?<\/[^>]+>|<[^>]+data-original="true"[^>]*style="display:none;"[^>]*\/>)\s*(<[^>]+data-original="false"[^>]*>.*?<\/[^>]+>|<[^>]+data-original="false"[^>]*\/>)/s;
const pairMatch = currentHtml.match(pairRegex);
if (!pairMatch) {
console.error("Save Error: Could not find the hidden/visible element pair.");
return currentHtml;
}
const visibleElement = pairMatch[2];
const savedElement = visibleElement.replace(/\s*data-original="false"\s*/, ' ').replace(/\s{2,}/g, ' ');
return currentHtml.replace(pairRegex, savedElement);
});
onClose();
};
// 5. On Cancel, revert the changes
const handleCancel = () => {
setHtmlInput(currentHtml => {
const pairRegex = /(<[^>]+data-original="true"[^>]*style="display:none;"[^>]*>.*?<\/[^>]+>|<[^>]+data-original="true"[^>]*style="display:none;"[^>]*\/>)\s*(<[^>]+data-original="false"[^>]*>.*?<\/[^>]+>|<[^>]+data-original="false"[^>]*\/>)/s;
const pairMatch = currentHtml.match(pairRegex);
if (!pairMatch) {
console.error("Cancel Error: Could not find the hidden/visible element pair.");
return currentHtml;
}
const hiddenElement = pairMatch[1];
const unhiddenElement = hiddenElement
.replace(/\s*data-original="true"\s*/, ' ')
.replace(/\s*style="display:none;"\s*/, ' ')
.replace(/\s{2,}/g, ' ');
return currentHtml.replace(pairRegex, unhiddenElement);
});
onClose();
};
if (!edited) return <p className="dark:text-gray-300">Loading editor...</p>;
const otherAttributes = Object.keys(edited).filter(
key => key !== 'tagName' && key !== 'id' && key !== 'className' && key !== 'innerText'
);
return (
<div className="space-y-4">
{['tagName', 'id', 'className'].map(field => (
<div key={field} className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">{field.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}</label>
<input
type="text"
value={edited[field] || ''}
onChange={(e) => handleFieldChange(field, e.target.value)}
className="w-full p-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 font-mono text-sm"
/>
</div>
))}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">Inner Text</label>
<textarea
value={edited.innerText || ''}
onChange={(e) => handleFieldChange('innerText', e.target.value)}
rows="4"
className="w-full p-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 font-mono text-sm"
/>
</div>
{otherAttributes.map(attr => (
<div key={attr} className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">{attr}</label>
<input
type="text"
value={edited[attr] === true ? "" : edited[attr] || ''}
placeholder={edited[attr] === true ? "(boolean attribute)" : ""}
onChange={(e) => handleFieldChange(attr, e.target.value)}
className="w-full p-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 font-mono text-sm"
/>
</div>
))}
<div className="flex flex-col space-y-2 pt-4">
<button onClick={handleSave} className="w-full px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors">Save Changes</button>
<button onClick={handleCancel} className="w-full px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors">Cancel</button>
</div>
</div>
);
};
export default HtmlPreviewTool;

View File

@@ -1,213 +0,0 @@
import React, { useState, useRef } from 'react';
import { Search, Copy, Download } from 'lucide-react';
import { Controlled as CodeMirror } from 'react-codemirror2';
import 'codemirror/lib/codemirror.css';
import 'codemirror/theme/material.css';
import 'codemirror/mode/xml/xml';
import 'codemirror/mode/css/css';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/addon/search/search';
import 'codemirror/addon/search/searchcursor';
import 'codemirror/addon/dialog/dialog';
import 'codemirror/addon/dialog/dialog.css';
const CodeInputs = ({
htmlInput,
setHtmlInput,
cssInput,
setCssInput,
jsInput,
setJsInput,
isFullscreen
}) => {
const [activeTab, setActiveTab] = useState('html');
const htmlEditorRef = useRef(null);
const cssEditorRef = useRef(null);
const jsEditorRef = useRef(null);
// Handle search functionality
const handleSearch = (editorRef) => {
if (editorRef.current && editorRef.current.editor) {
editorRef.current.editor.execCommand('find');
}
};
// Handle copy functionality
const handleCopy = async (content) => {
try {
await navigator.clipboard.writeText(content);
} catch (err) {
console.error('Failed to copy:', err);
}
};
// Handle export functionality
const handleExport = (content, filename) => {
const blob = new Blob([content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
// Get current editor ref based on active tab
const getCurrentEditorRef = () => {
switch (activeTab) {
case 'html': return htmlEditorRef;
case 'css': return cssEditorRef;
case 'js': return jsEditorRef;
default: return htmlEditorRef;
}
};
// Get current content based on active tab
const getCurrentContent = () => {
switch (activeTab) {
case 'html': return htmlInput;
case 'css': return cssInput;
case 'js': return jsInput;
default: return htmlInput;
}
};
// Get filename for export based on active tab
const getExportFilename = () => {
switch (activeTab) {
case 'html': return 'code.html';
case 'css': return 'styles.css';
case 'js': return 'script.js';
default: return 'code.txt';
}
};
return (
<div className="flex flex-col h-full">
{/* Tab Navigation */}
<div className="flex border-b border-gray-200 dark:border-gray-700">
{[
{ id: 'html', label: 'HTML' },
{ id: 'css', label: 'CSS' },
{ id: 'js', label: 'JavaScript' }
].map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
activeTab === tab.id
? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
}`}
>
{tab.label}
</button>
))}
</div>
{/* Action buttons above editor */}
<div className="flex items-center justify-end space-x-2 p-2 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<button
onClick={() => handleSearch(getCurrentEditorRef())}
className="flex items-center gap-1 px-2 py-1 text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
title="Search"
>
<Search className="w-3 h-3" />
Search
</button>
<button
onClick={() => handleCopy(getCurrentContent())}
className="flex items-center gap-1 px-2 py-1 text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
title="Copy"
>
<Copy className="w-3 h-3" />
Copy
</button>
<button
onClick={() => handleExport(getCurrentContent(), getExportFilename())}
className="flex items-center gap-1 px-2 py-1 text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
title="Export"
>
<Download className="w-3 h-3" />
Export
</button>
</div>
{/* Code Editor */}
<div className="flex-1">
{activeTab === 'html' && (
<CodeMirror
ref={htmlEditorRef}
value={htmlInput}
onBeforeChange={(editor, data, value) => setHtmlInput(value)}
options={{
mode: 'xml',
theme: 'material',
lineNumbers: true,
lineWrapping: true,
autoCloseTags: true,
matchBrackets: true,
indentUnit: 2,
tabSize: 2,
extraKeys: {
'Ctrl-F': 'findPersistent',
'Cmd-F': 'findPersistent'
}
}}
className="h-full"
/>
)}
{activeTab === 'css' && (
<CodeMirror
ref={cssEditorRef}
value={cssInput}
onBeforeChange={(editor, data, value) => setCssInput(value)}
options={{
mode: 'css',
theme: 'material',
lineNumbers: true,
lineWrapping: true,
autoCloseBrackets: true,
matchBrackets: true,
indentUnit: 2,
tabSize: 2,
extraKeys: {
'Ctrl-F': 'findPersistent',
'Cmd-F': 'findPersistent'
}
}}
className="h-full"
/>
)}
{activeTab === 'js' && (
<CodeMirror
ref={jsEditorRef}
value={jsInput}
onBeforeChange={(editor, data, value) => setJsInput(value)}
options={{
mode: 'javascript',
theme: 'material',
lineNumbers: true,
lineWrapping: true,
autoCloseBrackets: true,
matchBrackets: true,
indentUnit: 2,
tabSize: 2,
extraKeys: {
'Ctrl-F': 'findPersistent',
'Cmd-F': 'findPersistent'
}
}}
className="h-full"
/>
)}
</div>
</div>
);
};
export default CodeInputs;

View File

@@ -1,65 +0,0 @@
import React from 'react';
import { X } from 'lucide-react';
import ElementEditor from './ElementEditor';
const InspectorSidebar = ({
inspectedElementInfo,
htmlInput,
setHtmlInput,
onClose,
onSave,
previewFrameRef
}) => {
if (!inspectedElementInfo) return null;
return (
<div className="w-80 bg-white dark:bg-gray-800 border-l border-gray-200 dark:border-gray-700 flex flex-col">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
Inspector
</h3>
<button
onClick={onClose}
className="p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Content */}
<div className="flex-1 p-4 overflow-y-auto">
<div className="space-y-4">
{/* Element Info */}
<div className="bg-gray-50 dark:bg-gray-700 p-3 rounded-md">
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Selected Element
</h4>
<p className="text-sm text-gray-600 dark:text-gray-400">
<span className="font-mono bg-gray-200 dark:bg-gray-600 px-1 rounded">
&lt;{inspectedElementInfo.tagName}&gt;
</span>
</p>
</div>
{/* Element Editor */}
<div>
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
Edit Properties
</h4>
<ElementEditor
htmlInput={htmlInput}
setHtmlInput={setHtmlInput}
onClose={onClose}
onSave={onSave}
selectedElementInfo={inspectedElementInfo}
previewFrameRef={previewFrameRef}
/>
</div>
</div>
</div>
</div>
);
};
export default InspectorSidebar;

View File

@@ -1,852 +0,0 @@
import React, { useState, useEffect, useRef, useCallback, forwardRef, useImperativeHandle } from 'react';
// Device Frame CSS - Converted from SCSS
const deviceFrameCSS = `
/* iPhone 14 Pro Device Frame */
.device-iphone-14-pro {
height: 780px;
width: 384px;
transform-origin: center;
position: relative;
margin: 0 auto;
}
.device-iphone-14-pro .device-frame {
background: #010101;
border: 1px solid #2a242f;
border-radius: 61px;
box-shadow: inset 0 0 4px 2px #a8a4b0, inset 0 0 0 5px #342C3F;
height: 780px;
padding: 17px;
width: 384px;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.device-iphone-14-pro .device-screen {
border-radius: 56px;
height: 746px;
width: 350px;
overflow: hidden;
scale: 0.75;
min-width: 130%;
height: 130%;
}
.device-iphone-14-pro .device-screen iframe {
width: 130%; /* 100% / 0.75 = 133.33% to compensate for 0.75 scale */
height: 130%;
transform: scale(0.75);
transform-origin: top left;
}
/* Mobile scrollbar styling for iPhone */
.device-iphone-14-pro .device-screen::-webkit-scrollbar {
width: 2px;
}
.device-iphone-14-pro .device-screen::-webkit-scrollbar-track {
background: transparent;
}
.device-iphone-14-pro .device-screen::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 1px;
}
.device-iphone-14-pro .device-stripe::after,
.device-iphone-14-pro .device-stripe::before {
border: solid rgba(1, 1, 1, 0.25);
border-width: 0 7px;
content: "";
height: 7px;
left: 0;
position: absolute;
width: 100%;
z-index: 9;
}
.device-iphone-14-pro .device-stripe::after {
top: 77px;
}
.device-iphone-14-pro .device-stripe::before {
bottom: 77px;
}
.device-iphone-14-pro .device-header {
background: #010101;
border-radius: 18px;
height: 31px;
left: 50%;
margin-left: -54px;
position: absolute;
top: 32px;
width: 108px;
z-index: 10;
}
.device-iphone-14-pro .device-sensors::after,
.device-iphone-14-pro .device-sensors::before {
content: "";
position: absolute;
}
.device-iphone-14-pro .device-sensors::after {
background: #010101;
border-radius: 16px;
height: 30px;
left: 50%;
margin-left: -54px;
top: 33px;
width: 67px;
z-index: 10;
}
.device-iphone-14-pro .device-sensors::before {
background: radial-gradient(farthest-corner at 20% 20%, #6074BF 0, transparent 40%),
radial-gradient(farthest-corner at 80% 80%, #513785 0, #24555E 20%, transparent 50%);
box-shadow: 0 0 1px 1px rgba(255, 255, 255, 0.05);
border-radius: 50%;
height: 8px;
left: 50%;
margin-left: 24px;
top: 44px;
width: 8px;
z-index: 10;
}
.device-iphone-14-pro .device-btns {
background: #2a242f;
border-radius: 1px;
height: 24px;
left: -2px;
position: absolute;
top: 86px;
width: 2px;
}
.device-iphone-14-pro .device-btns::after,
.device-iphone-14-pro .device-btns::before {
background: #2a242f;
border-radius: 1px;
content: "";
height: 46px;
left: 0;
position: absolute;
}
.device-iphone-14-pro .device-btns::after {
top: 45px;
}
.device-iphone-14-pro .device-btns::before {
top: 105px;
}
.device-iphone-14-pro .device-power {
background: #2a242f;
border-radius: 1px;
height: 75px;
right: -2px;
position: absolute;
top: 150px;
width: 2px;
}
/* iPad Pro Device Frame */
.device-ipad-pro {
height: 840px;
width: 600px;
transform-origin: center;
margin-top: 40px;
position: relative;
margin-left: auto;
margin-right: auto;
}
.device-ipad-pro .device-frame {
background: #0d0d0d;
border-radius: 32px;
box-shadow: inset 0 0 0 1px #c1c2c3, inset 0 0 1px 2px #e2e3e4;
height: 800px;
padding: 24px;
width: 576px;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.device-ipad-pro .device-screen {
border: 2px solid #0f0f0f;
border-radius: 10px;
overflow: hidden;
min-width: 200%;
height: 200%;
scale: 0.5;
}
.device-ipad-pro .device-screen iframe {
/* Set the iframe to the actual device resolution and scale it down */
width: 834px; /* iPad Pro 11" logical width */
height: 1194px; /* iPad Pro 11" logical height */
transform: scale(0.6331); /* 528px (screen width) / 834px (logical width) */
transform-origin: top left;
background: #fff; /* Ensure bg color for content */
}
/* Mobile scrollbar styling for iPad */
.device-ipad-pro .device-screen::-webkit-scrollbar {
width: 3px;
}
.device-ipad-pro .device-screen::-webkit-scrollbar-track {
background: transparent;
}
.device-ipad-pro .device-screen::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 2px;
}
.device-ipad-pro .device-power {
background: #2a242f;
border-radius: 2px;
height: 2px;
width: 38px;
right: 76px;
top: -2px;
position: absolute;
}
/* Reposition buttons specifically for iPad Pro */
.device-ipad-pro .device-btns {
background: #2a242f;
border-radius: 2px;
height: 30px; /* Volume up */
width: 2px;
right: 22px;
top: 90px;
position: absolute;
}
.device-ipad-pro .device-btns::after {
content: "";
background: #2a242f;
border-radius: 2px;
height: 30px; /* Volume down */
width: 2px;
left: 0;
top: 40px; /* Space between buttons */
position: absolute;
}
.device-ipad-pro .device-btns::before {
display: none; /* Hide the third button from iPhone */
}
.device-ipad-pro .device-sensors::after,
.device-ipad-pro .device-sensors::before {
content: "";
position: absolute;
}
.device-ipad-pro .device-sensors::after {
background: #141414;
border-radius: 16px;
box-shadow: -18px 0 #141414, 64px 0 #141414;
height: 10px;
left: 50%;
margin-left: -28px;
top: 11px;
width: 10px;
}
.device-ipad-pro .device-sensors::before {
background: radial-gradient(farthest-corner at 20% 20%, #6074BF 0, transparent 40%),
radial-gradient(farthest-corner at 80% 80%, #513785 0, #24555E 20%, transparent 50%);
box-shadow: 0 0 1px 1px rgba(255, 255, 255, 0.05);
border-radius: 50%;
height: 6px;
left: 50%;
margin-left: -3px;
top: 13px;
width: 5px;
}
/* Enable smooth scrolling on iOS */
.device-iphone-14-pro .device-screen,
.device-ipad-pro .device-screen {
-webkit-overflow-scrolling: touch; /* smooth momentum scroll on iOS */
overflow-y: auto;
}
/* Mobile custom scrollbar */
.device-iphone-14-pro .device-screen::-webkit-scrollbar,
.device-ipad-pro .device-screen::-webkit-scrollbar {
width: 4px;
height: 4px;
}
.device-iphone-14-pro .device-screen::-webkit-scrollbar-track,
.device-ipad-pro .device-screen::-webkit-scrollbar-track {
background: transparent;
}
.device-iphone-14-pro .device-screen::-webkit-scrollbar-thumb,
.device-ipad-pro .device-screen::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.15);
border-radius: 8px;
border: 1px solid rgba(0, 0, 0, 0.05);
}
/* Optional: Hide scrollbar on larger screens for desktop */
/* This media query hides the scrollbar on desktops where touch scrolling is not needed */
@media (pointer: fine) and (hover: hover) {
.device-iphone-14-pro .device-screen::-webkit-scrollbar,
.device-ipad-pro .device-screen::-webkit-scrollbar {
display: none;
}
}
`;
const injectCascadeIds = (rootElement) => {
if (!rootElement) return;
let idCounter = 0;
const elements = rootElement.querySelectorAll('*');
elements.forEach(el => {
if (!el.hasAttribute('data-cascade-id')) {
el.setAttribute('data-cascade-id', `cascade-${idCounter++}`);
}
});
};
// Inspector mode CSS
const domSelectorCSS = `
/* Hover effect for all elements in inspect mode */
body[cascade-inspect-mode] *:hover {
outline: 2px solid #0066ff !important;
outline-offset: 2px !important;
box-shadow: 0 0 0 2px rgba(0, 102, 255, 0.3) !important;
cursor: crosshair !important;
transition: all 0.1s ease !important;
}
/* Selected element styling */
.cascade-selected {
outline: 3px solid #00cc66 !important;
outline-offset: 3px !important;
box-shadow: 0 0 0 3px rgba(0, 204, 102, 0.3) !important;
position: relative !important;
}
/* Ensure inspector styles override any existing styles */
body[cascade-inspect-mode] * {
cursor: crosshair !important;
}
`;
const cursorCSS = `
/* Additional cursor styling for inspect mode */
body[cascade-inspect-mode] {
cursor: crosshair !important;
}
`;
const PreviewFrame = forwardRef((props, ref) => {
const {
htmlInput,
cssInput,
jsInput,
onElementClick,
isInspectModeActive,
selectedDevice,
isFullscreen
} = props;
const iframeRef = useRef(null);
const [originalScrollPosition, setOriginalScrollPosition] = useState({ x: 0, y: 0 });
const storeScrollPosition = useCallback(() => {
const iframe = iframeRef.current;
if (!iframe?.contentWindow) return;
const scrollX = iframe.contentWindow.scrollX || 0;
const scrollY = iframe.contentWindow.scrollY || 0;
setOriginalScrollPosition({ x: scrollX, y: scrollY });
console.log('📍 SCROLL STORED:', { x: scrollX, y: scrollY });
}, []);
const restoreScrollPosition = useCallback(() => {
const iframe = iframeRef.current;
if (!iframe) return;
const attemptRestore = (retryCount = 0) => {
if (retryCount > 3) {
console.warn('⚠️ SCROLL RESTORE: Max retries reached, giving up');
return;
}
if (iframe.contentWindow && iframe.contentDocument) {
try {
iframe.contentWindow.scrollTo(originalScrollPosition.x, originalScrollPosition.y);
console.log('🔄 SCROLL RESTORED:', originalScrollPosition);
} catch (error) {
console.error('❌ SCROLL RESTORE ERROR:', error);
}
} else {
console.log(`🔄 SCROLL RESTORE: Iframe not ready, retrying... (${retryCount + 1}/3)`);
setTimeout(() => attemptRestore(retryCount + 1), 100);
}
};
attemptRestore();
}, [originalScrollPosition]);
const handleIframeClick = useCallback((e) => {
if (!isInspectModeActive) return;
e.preventDefault();
e.stopPropagation();
const iframe = iframeRef.current;
if (!iframe) return;
const doc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document);
if (!doc) return;
storeScrollPosition();
const target = e.target;
if (!target) return;
let cascadeId = target.getAttribute('data-cascade-id');
if (!cascadeId) {
cascadeId = `cascade-${Date.now()}`;
target.setAttribute('data-cascade-id', cascadeId);
}
const elementInfo = {
tagName: target.tagName.toLowerCase(),
attributes: {},
textContent: target.textContent,
innerHTML: target.innerHTML,
cascadeId: cascadeId,
};
Array.from(target.attributes).forEach(attr => {
elementInfo.attributes[attr.name] = attr.value;
});
// Ensure className is properly mapped for ElementEditor compatibility
if (target.className) {
elementInfo.attributes.class = target.className;
}
console.log('🔍 ENHANCED SELECTION:', elementInfo);
console.log('🎯 INSPECTOR ACTIVATED: DOM manipulation mode enabled');
setTimeout(() => restoreScrollPosition(), 10);
onElementClick(elementInfo);
}, [isInspectModeActive, onElementClick, storeScrollPosition, restoreScrollPosition]);
const setupInspectModeStyles = useCallback(() => {
const iframe = iframeRef.current;
if (!iframe) return;
const doc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document);
if (!doc) return;
// Declare variables outside if block for cleanup function access
let styleElement = null;
let cursorStyleElement = null;
// Add inspector styles if not already present
if (!doc.getElementById('dom-selector-styles')) {
styleElement = doc.createElement('style');
styleElement.id = 'dom-selector-styles';
styleElement.textContent = domSelectorCSS;
doc.head.appendChild(styleElement);
// Add cursor styles
cursorStyleElement = doc.createElement('style');
cursorStyleElement.id = 'cursor-styles';
cursorStyleElement.textContent = cursorCSS;
doc.head.appendChild(cursorStyleElement);
console.log('✅ DEBUG: Inspector styles injected');
} else {
console.log('✅ DEBUG: Inspector styles already present');
// Get references to existing elements for cleanup
styleElement = doc.getElementById('dom-selector-styles');
cursorStyleElement = doc.getElementById('cursor-styles');
}
// ALWAYS attach click event listener (this was the bug - it was being skipped)
if (doc.body) {
// Remove any existing listener first to prevent duplicates
doc.body.removeEventListener('click', handleIframeClick, true);
doc.body.addEventListener('click', handleIframeClick, true);
console.log('✅ DEBUG: Click handler attached successfully');
} else {
console.error('❌ DEBUG: No iframe body found, cannot attach click handler');
}
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
injectCascadeIds(node);
}
});
}
}
});
// Observe the iframe document for changes
observer.observe(doc.body || doc, { childList: true, subtree: true });
// Cleanup function
return () => {
try {
observer.disconnect();
if (styleElement && styleElement.parentNode) {
styleElement.remove();
}
if (cursorStyleElement && cursorStyleElement.parentNode) {
cursorStyleElement.remove();
}
console.log('🧹 Inspector styles and listeners cleaned up');
} catch (error) {
console.warn('⚠️ Cleanup warning (safe to ignore):', error.message);
}
};
}, [handleIframeClick]);
const generateHtmlContent = useCallback(() => {
// Always generate content - the parent component controls when to refresh
console.log('🔄 GENERATING HTML CONTENT for iframe');
const isFullHtml = htmlInput.trim().toLowerCase().startsWith('<html');
const cssLink = `<link rel="stylesheet" href="https://cdn.tailwindcss.com">`;
const customStyles = `<style>${cssInput}</style>`;
const scripts = `<script>${jsInput}</script>`;
if (isFullHtml) {
let processedHtml = htmlInput;
if (!processedHtml.includes(cssLink)) {
processedHtml = processedHtml.replace('</head>', `${cssLink}</head>`);
}
if (!processedHtml.includes(customStyles)) {
processedHtml = processedHtml.replace('</head>', `${customStyles}</head>`);
}
if (!processedHtml.includes(jsInput)) {
processedHtml = processedHtml.replace('</body>', `${scripts}</body>`);
}
return processedHtml;
} else {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Preview</title>
${cssLink}
${customStyles}
</head>
<body>
${htmlInput}
${scripts}
</body>
</html>
`;
}
}, [htmlInput, cssInput, jsInput]);
useEffect(() => {
const iframe = iframeRef.current;
if (!iframe) return;
// ENHANCED OPTION A: Skip iframe refresh during inspector operations
if (window.isInspectorActive) {
console.log('🚫 ENHANCED OPTION A: Skipping iframe refresh during inspector operations');
return;
}
// Additional guard: Check if inspect mode is active
if (isInspectModeActive) {
console.log('🚫 ENHANCED OPTION A: Skipping iframe refresh - inspect mode is active');
return;
}
console.log('🔄 GENERATING HTML CONTENT for iframe');
const htmlContent = generateHtmlContent();
console.log('🔍 DEBUG: Generated HTML content length:', htmlContent.length);
// Always write content to iframe - Enhanced Option A uses DOM manipulation after content is loaded
const doc = iframe.contentDocument || iframe.contentWindow.document;
doc.open();
doc.write(htmlContent);
doc.close();
// The onload event ensures that the content is fully parsed and the DOM is ready
iframe.onload = () => {
// ENHANCED OPTION A: Inject cascade IDs immediately after content load
const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
if (iframeDoc?.body) {
injectCascadeIds(iframeDoc.body);
console.log('🏷️ CASCADE IDS: Injected into fresh iframe content');
}
if (isInspectModeActive) {
console.log('🎨 Applying inspect mode styles to fresh content.');
setupInspectModeStyles();
}
// Restore scroll position only after content is fully loaded
restoreScrollPosition();
};
console.log('🔄 IFRAME REFRESHED: New content written');
console.log('🔍 IFRAME REFRESH TRIGGERED BY:', {
htmlInputLength: htmlInput.length,
cssInputLength: cssInput.length,
jsInputLength: jsInput.length,
selectedDevice,
isFullscreen
});
}, [htmlInput, cssInput, jsInput, selectedDevice, isFullscreen, generateHtmlContent, isInspectModeActive]);
// Dedicated useEffect for inspect mode activation/deactivation
useEffect(() => {
const iframe = iframeRef.current;
if (!iframe) return;
const doc = iframe.contentDocument || iframe.contentWindow?.document;
if (!doc) return;
if (isInspectModeActive) {
console.log('🎯 ACTIVATING inspect mode - setting up click handlers');
setupInspectModeStyles();
} else {
console.log('🚫 DEACTIVATING inspect mode - cleaning up');
try {
// Remove inspect styles
const styleElement = doc.getElementById('inspector-styles');
if (styleElement) {
styleElement.remove();
}
// Remove click handler
if (doc?.body) {
doc.body.removeEventListener('click', handleIframeClick, true);
}
// Remove selected class from any elements
const selectedElements = doc.querySelectorAll('.cascade-selected');
selectedElements.forEach(el => el.classList.remove('cascade-selected'));
} catch (error) {
console.warn('⚠️ Inspect mode cleanup warning (safe to ignore):', error.message);
}
}
}, [isInspectModeActive, setupInspectModeStyles, handleIframeClick]);
useEffect(() => {
const styleId = 'device-frame-styles';
if (!document.getElementById(styleId)) {
const style = document.createElement('style');
style.id = styleId;
style.textContent = deviceFrameCSS;
document.head.appendChild(style);
}
return () => {
const style = document.getElementById(styleId);
if (style) {
style.remove();
}
};
}, []);
const getDeviceWrapper = () => {
console.log('🔧 Device Frame Debug:', { isFullscreen, selectedDevice });
if (!isFullscreen) {
console.log('📱 Non-fullscreen: Using iPhone 14 Pro frame');
return {
wrapperClass: 'flex justify-center items-center w-full h-full',
deviceFrame: 'iphone-14-pro'
};
}
if (selectedDevice === 'desktop') {
console.log('🖥️ Desktop fullscreen: No device frame');
return {
wrapperClass: 'w-full h-full max-w-full overflow-hidden',
deviceFrame: null
};
}
switch (selectedDevice) {
case 'tablet':
console.log('📟 Rendering iPad Pro frame');
return {
wrapperClass: 'flex justify-center items-center w-full h-full',
deviceFrame: 'ipad-pro'
};
case 'mobile':
console.log('📱 Rendering iPhone 14 Pro frame');
return {
wrapperClass: 'flex justify-center items-center w-full h-full',
deviceFrame: 'iphone-14-pro'
};
default:
console.log('❓ Unknown device, no frame');
return {
wrapperClass: 'w-full h-full max-w-full overflow-hidden',
deviceFrame: null,
};
}
};
const { wrapperClass, deviceFrame } = getDeviceWrapper();
useImperativeHandle(ref, () => ({
updateElementStyle: (cascadeId, property, value) => {
try {
const iframe = iframeRef.current;
if (!iframe?.contentDocument) {
console.error('❌ [PreviewFrame] Iframe or contentDocument not available');
return false;
}
const element = iframe.contentDocument.querySelector(`[data-cascade-id="${cascadeId}"]`);
if (element) {
element.style[property] = value;
console.log(`🎨 [PreviewFrame] Updated style '${property}' for cascade-id: ${cascadeId}`);
return true;
} else {
console.error(`❌ [PreviewFrame] Element with cascade-id '${cascadeId}' not found for style update.`);
return false;
}
} catch (error) {
console.error(`❌ [PreviewFrame] Error updating style:`, error);
return false;
}
},
updateElementAttribute: (cascadeId, attribute, value) => {
try {
const iframe = iframeRef.current;
if (!iframe?.contentDocument) {
console.error('❌ [PreviewFrame] Iframe or contentDocument not available');
return false;
}
const element = iframe.contentDocument.querySelector(`[data-cascade-id="${cascadeId}"]`);
if (element) {
if (value === null || value === undefined || value === '') {
element.removeAttribute(attribute);
console.log(`🗑️ [PreviewFrame] Removed attribute '${attribute}' for cascade-id: ${cascadeId}`);
} else {
element.setAttribute(attribute, value);
console.log(`🏷️ [PreviewFrame] Updated attribute '${attribute}' for cascade-id: ${cascadeId}`);
}
return true;
} else {
console.error(`❌ [PreviewFrame] Element with cascade-id '${cascadeId}' not found for attribute update.`);
return false;
}
} catch (error) {
console.error(`❌ [PreviewFrame] Error updating attribute:`, error);
return false;
}
},
updateElementText: (cascadeId, textContent) => {
try {
const iframe = iframeRef.current;
if (!iframe?.contentDocument) {
console.error('❌ [PreviewFrame] Iframe or contentDocument not available');
return false;
}
const element = iframe.contentDocument.querySelector(`[data-cascade-id="${cascadeId}"]`);
if (element) {
element.textContent = textContent;
console.log(`📝 [PreviewFrame] Updated textContent for cascade-id: ${cascadeId}`);
return true;
} else {
console.error(`❌ [PreviewFrame] Element with cascade-id '${cascadeId}' not found for text update.`);
return false;
}
} catch (error) {
console.error(`❌ [PreviewFrame] Error updating text:`, error);
return false;
}
},
updateElementClass: (cascadeId, className) => {
try {
const iframe = iframeRef.current;
if (!iframe?.contentDocument) {
console.error('❌ [PreviewFrame] Iframe or contentDocument not available');
return false;
}
const element = iframe.contentDocument.querySelector(`[data-cascade-id="${cascadeId}"]`);
if (element) {
element.className = className;
console.log(`🎨 [PreviewFrame] Updated className for cascade-id: ${cascadeId}`);
return true;
} else {
console.error(`❌ [PreviewFrame] Element with cascade-id '${cascadeId}' not found for class update.`);
return false;
}
} catch (error) {
console.error(`❌ [PreviewFrame] Error updating class:`, error);
return false;
}
},
getIframeContent: () => {
if (!iframeRef.current || !iframeRef.current.contentWindow) return '';
// Ensure IDs are injected before returning content
const iframeDoc = iframeRef.current.contentWindow.document;
injectCascadeIds(iframeDoc.body);
return iframeDoc.documentElement.outerHTML;
},
// Add other exposed methods here if needed
}), []);
if (deviceFrame) {
return (
<div className={wrapperClass}>
<div className={`device device-${deviceFrame}`}>
<div className="device-frame">
<iframe
ref={iframeRef}
key={`device-${deviceFrame}-${selectedDevice}-${isFullscreen}`}
className="device-screen w-full h-full border-0"
title="HTML Preview"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals"
/>
</div>
<div className="device-stripe"></div>
<div className="device-header"></div>
<div className="device-sensors"></div>
<div className="device-btns"></div>
<div className="device-power"></div>
</div>
</div>
);
}
return (
<div className={`${wrapperClass} bg-white rounded-lg shadow-lg`}>
<iframe
ref={iframeRef}
key={`no-device-${selectedDevice}-${isFullscreen}`}
className="w-full h-full border-0 overflow-hidden"
title="HTML Preview"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals"
style={{ maxWidth: '100%', maxHeight: '100%' }}
/>
</div>
);
});
export default PreviewFrame;