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:
@@ -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>
|
||||||
|
|||||||
@@ -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 }) => {
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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">
|
|
||||||
<{inspectedElementInfo.tagName}>
|
|
||||||
</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;
|
|
||||||
@@ -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;
|
|
||||||
Reference in New Issue
Block a user