Fix HTML Preview Tool critical bugs: PreviewFrame ref and click handler
- Fixed missing ref={previewFrameRef} on first PreviewFrame component (line 336)
* This was causing 'PreviewFrame API not available' errors during save operations
* Both fullscreen and normal mode PreviewFrame instances now have proper ref connection
- Fixed click handler attachment bug in setupInspectModeStyles function
* Click handler was being skipped when styles were already injected
* Now always attaches click handler when inspect mode is activated
* Added proper cleanup to prevent duplicate event listeners
- Fixed variable scope issues in PreviewFrame.fresh.js
* styleElement and cursorStyleElement now properly scoped for cleanup function
* Added references to existing elements when styles already present
- Removed unused variables and fixed eslint warnings
* Removed unused indentSize variable in BeautifierTool.js
* Removed unused onSave and onDomUpdate props in PreviewFrame.fresh.js
* Fixed unnecessary escape character in script tag
These fixes restore the Enhanced Option A DOM manipulation architecture:
- Inspector sidebar should now appear when clicking elements in inspect mode
- Save functionality should work without 'PreviewFrame ref not available' errors
- Live editing of element properties should work through PreviewFrame API
- Iframe refresh prevention during inspector operations maintained
This commit is contained in:
13
.windsurf/rules/developer-tools.md
Normal file
13
.windsurf/rules/developer-tools.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
trigger: always_on
|
||||||
|
---
|
||||||
|
|
||||||
|
keep look the issue globally, not narrow. we are done chasing symtomp with narrow sight, we have things to be achieved:
|
||||||
|
A. main goal: having a working HTML Preview with element inspector and editor feature, and
|
||||||
|
B. sub goal: implementing the "stable option A DOM Manipulation" properly to reach the main goal (A)
|
||||||
|
|
||||||
|
In every reported issue, check if that prevent us to achieved the sub goal. Failing sub goal means fail to reach the main goal. So pivot everything to make a success sub goal, to achieve main goal.
|
||||||
|
|
||||||
|
I believe promised sub goal is the way to get succeed on the main goal.
|
||||||
|
|
||||||
|
Avoid any looping thought
|
||||||
@@ -9,6 +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() {
|
||||||
@@ -24,6 +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 { Code2, Home, ChevronDown, Menu, X, Database, FileText, Link as LinkIcon, Hash, FileSpreadsheet, Wand2, GitCompare } from 'lucide-react';
|
import { Code2, Home, ChevronDown, Menu, X, Database, FileText, Link as LinkIcon, Hash, FileSpreadsheet, Wand2, GitCompare, Code } from 'lucide-react';
|
||||||
import ThemeToggle from './ThemeToggle';
|
import ThemeToggle from './ThemeToggle';
|
||||||
|
|
||||||
const Layout = ({ children }) => {
|
const Layout = ({ children }) => {
|
||||||
@@ -40,7 +40,8 @@ const Layout = ({ children }) => {
|
|||||||
{ path: '/base64', name: 'Base64 Tool', icon: Hash, description: 'Base64 encode/decode' },
|
{ path: '/base64', name: 'Base64 Tool', icon: Hash, description: 'Base64 encode/decode' },
|
||||||
{ path: '/csv-json', name: 'CSV/JSON Tool', icon: FileSpreadsheet, description: 'Convert CSV ↔ JSON' },
|
{ path: '/csv-json', name: 'CSV/JSON Tool', icon: FileSpreadsheet, description: 'Convert CSV ↔ JSON' },
|
||||||
{ path: '/beautifier', name: 'Beautifier Tool', icon: Wand2, description: 'Beautify/minify code' },
|
{ path: '/beautifier', name: 'Beautifier Tool', icon: Wand2, description: 'Beautify/minify code' },
|
||||||
{ path: '/diff', name: 'Diff Tool', icon: GitCompare, description: 'Compare text differences' }
|
{ path: '/diff', name: 'Diff Tool', icon: GitCompare, description: 'Compare text differences' },
|
||||||
|
{ path: '/html-preview', name: 'HTML Preview', icon: Code, description: 'Render HTML, CSS, JS' }
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Hash, Upload, Download } from 'lucide-react';
|
import { Hash, Upload } from 'lucide-react';
|
||||||
import ToolLayout from '../components/ToolLayout';
|
import ToolLayout from '../components/ToolLayout';
|
||||||
import CopyButton from '../components/CopyButton';
|
import CopyButton from '../components/CopyButton';
|
||||||
|
|
||||||
|
|||||||
@@ -144,6 +144,9 @@ const BeautifierTool = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const beautifyHtml = (text) => {
|
const beautifyHtml = (text) => {
|
||||||
|
// Declare formatted variable outside try/catch block
|
||||||
|
let formatted = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Clean input text first
|
// Clean input text first
|
||||||
let cleanText = text.trim();
|
let cleanText = text.trim();
|
||||||
@@ -154,14 +157,9 @@ const BeautifierTool = () => {
|
|||||||
// Self-closing tags that don't need closing tags
|
// Self-closing tags that don't need closing tags
|
||||||
const selfClosingTags = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
|
const selfClosingTags = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
|
||||||
|
|
||||||
// Inline tags that should stay on same line
|
|
||||||
const inlineTags = ['a', 'abbr', 'acronym', 'b', 'bdo', 'big', 'br', 'button', 'cite', 'code', 'dfn', 'em', 'i', 'img', 'input', 'kbd', 'label', 'map', 'object', 'q', 'samp', 'script', 'select', 'small', 'span', 'strong', 'sub', 'sup', 'textarea', 'tt', 'var'];
|
|
||||||
|
|
||||||
let formatted = '';
|
|
||||||
let indent = 0;
|
let indent = 0;
|
||||||
const tab = ' ';
|
const tab = ' ';
|
||||||
|
let formatted = ''; // Better HTML parsing
|
||||||
// Better HTML parsing
|
|
||||||
const tokens = cleanText.match(/<\/?[^>]+>|[^<]+/g) || [];
|
const tokens = cleanText.match(/<\/?[^>]+>|[^<]+/g) || [];
|
||||||
|
|
||||||
for (let i = 0; i < tokens.length; i++) {
|
for (let i = 0; i < tokens.length; i++) {
|
||||||
@@ -203,7 +201,7 @@ const BeautifierTool = () => {
|
|||||||
return formatted.trim();
|
return formatted.trim();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Fallback to simple formatting if advanced parsing fails
|
// Fallback to simple formatting if advanced parsing fails
|
||||||
let formatted = '';
|
formatted = '';
|
||||||
let indent = 0;
|
let indent = 0;
|
||||||
const tab = ' ';
|
const tab = ' ';
|
||||||
|
|
||||||
|
|||||||
451
src/pages/HtmlPreviewTool.js
Normal file
451
src/pages/HtmlPreviewTool.js
Normal file
@@ -0,0 +1,451 @@
|
|||||||
|
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 p-4 overflow-y-auto">
|
||||||
|
<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={`space-y-6 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;
|
||||||
461
src/pages/HtmlPreviewTool.js.backup
Normal file
461
src/pages/HtmlPreviewTool.js.backup
Normal file
@@ -0,0 +1,461 @@
|
|||||||
|
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;
|
||||||
140
src/pages/components/CodeInputs.js
Normal file
140
src/pages/components/CodeInputs.js
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { ChevronDown, ChevronUp, Maximize2, Minimize2 } from 'lucide-react';
|
||||||
|
|
||||||
|
const CodeInputs = ({
|
||||||
|
htmlInput,
|
||||||
|
setHtmlInput,
|
||||||
|
cssInput,
|
||||||
|
setCssInput,
|
||||||
|
jsInput,
|
||||||
|
setJsInput,
|
||||||
|
isFullscreen
|
||||||
|
}) => {
|
||||||
|
const [showCss, setShowCss] = useState(false);
|
||||||
|
const [showJs, setShowJs] = useState(false);
|
||||||
|
const [htmlExtended, setHtmlExtended] = useState(true); // Default: HTML box extended
|
||||||
|
const [cssExtended, setCssExtended] = useState(false);
|
||||||
|
const [jsExtended, setJsExtended] = useState(false);
|
||||||
|
|
||||||
|
const getTextareaHeight = (type, isExtended) => {
|
||||||
|
if (isFullscreen) {
|
||||||
|
// In fullscreen, give HTML much more space as main script area
|
||||||
|
if (type === 'html') {
|
||||||
|
return isExtended ? 'h-[32rem]' : 'h-96'; // Much taller for HTML in fullscreen to balance with iPhone frame
|
||||||
|
} else {
|
||||||
|
return isExtended ? 'h-64' : 'h-32'; // CSS/JS remain smaller
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In non-fullscreen, give much more space for HTML as main script area
|
||||||
|
if (type === 'html') {
|
||||||
|
if (!showCss && !showJs) {
|
||||||
|
// When CSS/JS hidden, HTML gets maximum space
|
||||||
|
return isExtended ? 'h-[36rem]' : 'h-[28rem]'; // Much taller for main script to balance with iPhone frame
|
||||||
|
} else {
|
||||||
|
// When CSS/JS visible, HTML still gets substantial space
|
||||||
|
return isExtended ? 'h-[28rem]' : 'h-96';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// CSS and JS boxes remain smaller
|
||||||
|
return isExtended ? 'h-64' : 'h-32';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* HTML Input */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
HTML
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
onClick={() => setHtmlExtended(!htmlExtended)}
|
||||||
|
className="flex items-center space-x-1 px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
|
||||||
|
title={htmlExtended ? 'Minimize HTML box' : 'Maximize HTML box'}
|
||||||
|
>
|
||||||
|
{htmlExtended ? <Minimize2 className="w-3 h-3" /> : <Maximize2 className="w-3 h-3" />}
|
||||||
|
<span>{htmlExtended ? 'Min' : 'Max'}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
value={htmlInput}
|
||||||
|
onChange={(e) => setHtmlInput(e.target.value)}
|
||||||
|
placeholder="Enter your HTML code here..."
|
||||||
|
className={`w-full ${getTextareaHeight('html', htmlExtended)} p-3 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 resize-none focus:ring-2 focus:ring-blue-500 focus:border-transparent`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Toggle buttons for CSS and JS */}
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowCss(!showCss)}
|
||||||
|
className="flex items-center space-x-1 px-3 py-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
{showCss ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
|
||||||
|
<span>CSS</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowJs(!showJs)}
|
||||||
|
className="flex items-center space-x-1 px-3 py-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
{showJs ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
|
||||||
|
<span>JS</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CSS Input */}
|
||||||
|
{showCss && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
CSS
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
onClick={() => setCssExtended(!cssExtended)}
|
||||||
|
className="flex items-center space-x-1 px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
|
||||||
|
title={cssExtended ? 'Minimize CSS box' : 'Maximize CSS box'}
|
||||||
|
>
|
||||||
|
{cssExtended ? <Minimize2 className="w-3 h-3" /> : <Maximize2 className="w-3 h-3" />}
|
||||||
|
<span>{cssExtended ? 'Min' : 'Max'}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
value={cssInput}
|
||||||
|
onChange={(e) => setCssInput(e.target.value)}
|
||||||
|
placeholder="Enter your CSS code here..."
|
||||||
|
className={`w-full ${getTextareaHeight('css', cssExtended)} p-3 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 resize-none focus:ring-2 focus:ring-blue-500 focus:border-transparent`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* JS Input */}
|
||||||
|
{showJs && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
JavaScript
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
onClick={() => setJsExtended(!jsExtended)}
|
||||||
|
className="flex items-center space-x-1 px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
|
||||||
|
title={jsExtended ? 'Minimize JS box' : 'Maximize JS box'}
|
||||||
|
>
|
||||||
|
{jsExtended ? <Minimize2 className="w-3 h-3" /> : <Maximize2 className="w-3 h-3" />}
|
||||||
|
<span>{jsExtended ? 'Min' : 'Max'}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
value={jsInput}
|
||||||
|
onChange={(e) => setJsInput(e.target.value)}
|
||||||
|
placeholder="Enter your JavaScript code here..."
|
||||||
|
className={`w-full ${getTextareaHeight('js', jsExtended)} p-3 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 resize-none focus:ring-2 focus:ring-blue-500 focus:border-transparent`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CodeInputs;
|
||||||
253
src/pages/components/ElementEditor.js
Normal file
253
src/pages/components/ElementEditor.js
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
const ElementEditor = ({ htmlInput, setHtmlInput, onClose, onSave, previewFrameRef, selectedElementInfo }) => {
|
||||||
|
console.log('🔍 ELEMENT EDITOR: Received props:', { selectedElementInfo, previewFrameRef: !!previewFrameRef });
|
||||||
|
const [edited, setEdited] = useState(null);
|
||||||
|
const textareaRefs = useRef({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// ENHANCED OPTION A: Use selectedElementInfo directly from props
|
||||||
|
if (selectedElementInfo) {
|
||||||
|
const elementInfo = {
|
||||||
|
tagName: selectedElementInfo.tagName,
|
||||||
|
innerText: selectedElementInfo.textContent || '',
|
||||||
|
id: selectedElementInfo.attributes.id || '',
|
||||||
|
className: selectedElementInfo.attributes.class || '',
|
||||||
|
cascadeId: selectedElementInfo.cascadeId,
|
||||||
|
isContainer: selectedElementInfo.attributes.children?.length > 0 || isContainerElement(selectedElementInfo.tagName),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add all other attributes
|
||||||
|
Object.entries(selectedElementInfo.attributes).forEach(([name, value]) => {
|
||||||
|
if (!['id', 'class', 'data-original', 'data-cascade-id'].includes(name)) {
|
||||||
|
elementInfo[name] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setEdited(elementInfo);
|
||||||
|
console.log('🎯 ENHANCED EDITOR: Initialized with selected element:', elementInfo);
|
||||||
|
} else {
|
||||||
|
// Clear the editor when no element is selected
|
||||||
|
setEdited(null);
|
||||||
|
}
|
||||||
|
}, [selectedElementInfo]);
|
||||||
|
|
||||||
|
// Helper function to detect container elements
|
||||||
|
const isContainerElement = (tagName) => {
|
||||||
|
const containerTags = [
|
||||||
|
'div', 'section', 'article', 'header', 'footer', 'nav', 'main', 'aside',
|
||||||
|
'ul', 'ol', 'li', 'table', 'thead', 'tbody', 'tr', 'td', 'th',
|
||||||
|
'form', 'fieldset', 'figure', 'details', 'summary',
|
||||||
|
// Media elements should not have innerText
|
||||||
|
'img', 'video', 'audio', 'svg', 'canvas', 'iframe', 'embed', 'object'
|
||||||
|
];
|
||||||
|
return containerTags.includes(tagName);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-resize textarea function
|
||||||
|
const autoResizeTextarea = (textarea) => {
|
||||||
|
if (textarea) {
|
||||||
|
textarea.style.height = 'auto';
|
||||||
|
textarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px'; // Max height 200px
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Effect to auto-resize textareas when content changes
|
||||||
|
useEffect(() => {
|
||||||
|
Object.values(textareaRefs.current).forEach(textarea => {
|
||||||
|
autoResizeTextarea(textarea);
|
||||||
|
});
|
||||||
|
}, [edited]);
|
||||||
|
|
||||||
|
// ENHANCED OPTION A: Field change handler using PreviewFrame API
|
||||||
|
const handleFieldChange = (field, value) => {
|
||||||
|
console.log(`⌨️ ENHANCED: Field '${field}' changed to '${value}'`);
|
||||||
|
setEdited(prev => ({ ...prev, [field]: value }));
|
||||||
|
|
||||||
|
// Use Enhanced Option A API for direct DOM manipulation
|
||||||
|
if (previewFrameRef?.current && edited?.cascadeId) {
|
||||||
|
let success = false;
|
||||||
|
|
||||||
|
if (field === 'innerText') {
|
||||||
|
success = previewFrameRef.current.updateElementText(edited.cascadeId, value);
|
||||||
|
} else if (field === 'className') {
|
||||||
|
success = previewFrameRef.current.updateElementClass(edited.cascadeId, value);
|
||||||
|
} else if (field === 'id' || field === 'src' || field === 'href' || field === 'alt' || field === 'title') {
|
||||||
|
success = previewFrameRef.current.updateElementAttribute(edited.cascadeId, field, value);
|
||||||
|
} else {
|
||||||
|
// For CSS properties, use updateElementStyle
|
||||||
|
success = previewFrameRef.current.updateElementStyle(edited.cascadeId, field, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
console.log(`✅ ENHANCED UPDATE: ${field} updated in iframe DOM (scroll preserved)`);
|
||||||
|
} else {
|
||||||
|
console.error(`❌ ENHANCED UPDATE: Failed to update ${field}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ ENHANCED UPDATE: PreviewFrame ref or cascadeId not available');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
console.log('💾 ENHANCED OPTION A SAVE: Using PreviewFrame API to get iframe content');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use Enhanced Option A API to get iframe content
|
||||||
|
if (previewFrameRef?.current?.getIframeContent) {
|
||||||
|
const currentIframeHtml = previewFrameRef.current.getIframeContent();
|
||||||
|
|
||||||
|
if (currentIframeHtml) {
|
||||||
|
// Clean up inspector-specific attributes
|
||||||
|
const cleanedHtml = currentIframeHtml
|
||||||
|
.replace(/\s*data-selected="true"/g, '')
|
||||||
|
.replace(/\s*data-cascade-id="[^"]*"/g, '')
|
||||||
|
.replace(/\s*id="inspector-styles"/g, '')
|
||||||
|
.replace(/<style[^>]*id="inspector-styles"[^>]*>.*?<\/style>/gs, '')
|
||||||
|
.replace(/\s{2,}/g, ' ');
|
||||||
|
|
||||||
|
// ENHANCED OPTION A: Update htmlInput with the cleaned iframe content
|
||||||
|
// This is the ONLY allowed setHtmlInput call during inspector operations (explicit save)
|
||||||
|
setHtmlInput(cleanedHtml);
|
||||||
|
|
||||||
|
console.log('✅ ENHANCED SAVE: Successfully extracted and cleaned iframe DOM via API');
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ Save: No content returned from PreviewFrame API');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('❌ Save: PreviewFrame API not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Enhanced Option A Save failed:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger parent save callback
|
||||||
|
if (onSave) {
|
||||||
|
onSave();
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 5. ENHANCED OPTION A: Cancel reverts changes in iframe DOM directly
|
||||||
|
const handleCancel = () => {
|
||||||
|
console.log('🔄 ENHANCED CANCEL: Reverting changes in iframe DOM without triggering refresh');
|
||||||
|
|
||||||
|
// For Enhanced Option A, we don't need to revert HTML input
|
||||||
|
// The iframe DOM changes will be discarded when the inspector closes
|
||||||
|
// Just close the inspector without syncing changes
|
||||||
|
console.log('✅ ENHANCED CANCEL: Changes discarded, iframe DOM remains stable');
|
||||||
|
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' && key !== 'isContainer'
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">Tag Name</label>
|
||||||
|
<textarea
|
||||||
|
ref={(el) => textareaRefs.current.tagName = el}
|
||||||
|
value={edited.tagName || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
handleFieldChange('tagName', e.target.value);
|
||||||
|
autoResizeTextarea(e.target);
|
||||||
|
}}
|
||||||
|
rows="1"
|
||||||
|
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 resize-none overflow-hidden"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">Class</label>
|
||||||
|
<textarea
|
||||||
|
ref={(el) => textareaRefs.current.className = el}
|
||||||
|
value={edited.className || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
handleFieldChange('className', e.target.value);
|
||||||
|
autoResizeTextarea(e.target);
|
||||||
|
}}
|
||||||
|
rows="1"
|
||||||
|
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 resize-none overflow-hidden"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">ID</label>
|
||||||
|
<textarea
|
||||||
|
ref={(el) => textareaRefs.current.id = el}
|
||||||
|
value={edited.id || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
handleFieldChange('id', e.target.value);
|
||||||
|
autoResizeTextarea(e.target);
|
||||||
|
}}
|
||||||
|
rows="1"
|
||||||
|
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 resize-none overflow-hidden"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Only show innerText field for non-container elements */}
|
||||||
|
{!edited.isContainer && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
Inner Text
|
||||||
|
<span className="text-xs text-gray-500 ml-2">(text content)</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
ref={(el) => textareaRefs.current.innerText = el}
|
||||||
|
value={edited.innerText || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
handleFieldChange('innerText', e.target.value);
|
||||||
|
autoResizeTextarea(e.target);
|
||||||
|
}}
|
||||||
|
rows="2"
|
||||||
|
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 resize-none overflow-hidden"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Show container hint for container elements */}
|
||||||
|
{edited.isContainer && (
|
||||||
|
<div className="p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md">
|
||||||
|
<p className="text-sm text-blue-700 dark:text-blue-300">
|
||||||
|
📦 This is a container element. Edit its ID, class, or other attributes instead of text content.
|
||||||
|
</p>
|
||||||
|
</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>
|
||||||
|
<textarea
|
||||||
|
ref={(el) => textareaRefs.current[attr] = el}
|
||||||
|
value={edited[attr] === true ? "" : edited[attr] || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
handleFieldChange(attr, e.target.value);
|
||||||
|
autoResizeTextarea(e.target);
|
||||||
|
}}
|
||||||
|
rows="1"
|
||||||
|
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 resize-none overflow-hidden"
|
||||||
|
/>
|
||||||
|
</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 ElementEditor;
|
||||||
65
src/pages/components/InspectorSidebar.js
Normal file
65
src/pages/components/InspectorSidebar.js
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
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;
|
||||||
674
src/pages/components/PreviewFrame.backup.js
Normal file
674
src/pages/components/PreviewFrame.backup.js
Normal file
@@ -0,0 +1,674 @@
|
|||||||
|
import React, { useRef, useEffect, useCallback, useMemo, useState } 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;
|
||||||
|
width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 PreviewFrame = ({
|
||||||
|
htmlInput,
|
||||||
|
cssInput,
|
||||||
|
jsInput,
|
||||||
|
selectedDevice,
|
||||||
|
inspectMode,
|
||||||
|
onElementClick,
|
||||||
|
isFullscreen
|
||||||
|
}) => {
|
||||||
|
const iframeRef = useRef(null);
|
||||||
|
|
||||||
|
// Handle iframe click for element selection - defined first to avoid initialization errors
|
||||||
|
const handleIframeClick = useCallback((e) => {
|
||||||
|
if (!inspectMode) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const clickedElement = e.target;
|
||||||
|
const elementInfo = {
|
||||||
|
tagName: clickedElement.tagName.toLowerCase(),
|
||||||
|
innerText: clickedElement.innerText || clickedElement.textContent || '',
|
||||||
|
attributes: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
Array.from(clickedElement.attributes).forEach(attr => {
|
||||||
|
elementInfo.attributes[attr.name] = attr.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
onElementClick(elementInfo);
|
||||||
|
}, [inspectMode, onElementClick]);
|
||||||
|
|
||||||
|
// Function to setup inspect mode styles and event handlers with MutationObserver
|
||||||
|
const setupInspectModeStyles = useCallback((iframeDoc) => {
|
||||||
|
console.log('🎨 PreviewFrame: Setting up robust inspect mode with MutationObserver');
|
||||||
|
|
||||||
|
// Remove existing inspect styles and observers
|
||||||
|
const existingStyle = iframeDoc.getElementById('inspect-mode-styles');
|
||||||
|
if (existingStyle) existingStyle.remove();
|
||||||
|
|
||||||
|
// Clean up any existing observer
|
||||||
|
if (iframeDoc._inspectObserver) {
|
||||||
|
iframeDoc._inspectObserver.disconnect();
|
||||||
|
delete iframeDoc._inspectObserver;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add inspect mode styles with better hover highlights
|
||||||
|
const style = iframeDoc.createElement('style');
|
||||||
|
style.id = 'inspect-mode-styles';
|
||||||
|
style.textContent = `
|
||||||
|
/* High specificity selectors for inspect mode */
|
||||||
|
html * {
|
||||||
|
cursor: crosshair !important;
|
||||||
|
pointer-events: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover highlights with maximum specificity */
|
||||||
|
html body *:hover {
|
||||||
|
outline: 2px solid #3b82f6 !important;
|
||||||
|
outline-offset: 1px !important;
|
||||||
|
background-color: rgba(59, 130, 246, 0.1) !important;
|
||||||
|
transition: all 0.1s ease !important;
|
||||||
|
box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Selected element styles */
|
||||||
|
html body [data-original="false"] {
|
||||||
|
outline: 2px solid #10b981 !important;
|
||||||
|
outline-offset: 1px !important;
|
||||||
|
background-color: rgba(16, 185, 129, 0.1) !important;
|
||||||
|
box-shadow: 0 0 0 1px rgba(16, 185, 129, 0.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Selected element hover */
|
||||||
|
html body [data-original="false"]:hover {
|
||||||
|
outline: 2px solid #059669 !important;
|
||||||
|
outline-offset: 1px !important;
|
||||||
|
background-color: rgba(5, 150, 105, 0.15) !important;
|
||||||
|
box-shadow: 0 0 0 1px rgba(5, 150, 105, 0.4) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prevent text selection during inspect */
|
||||||
|
html * {
|
||||||
|
-webkit-user-select: none !important;
|
||||||
|
-moz-user-select: none !important;
|
||||||
|
-ms-user-select: none !important;
|
||||||
|
user-select: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override any existing hover styles */
|
||||||
|
html body *:hover {
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
iframeDoc.head.appendChild(style);
|
||||||
|
|
||||||
|
// Add event handlers
|
||||||
|
const preventInteraction = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add click handler for element selection
|
||||||
|
iframeDoc.addEventListener('click', handleIframeClick, true);
|
||||||
|
|
||||||
|
// Prevent other interactions during inspect mode
|
||||||
|
iframeDoc.addEventListener('mousedown', preventInteraction, true);
|
||||||
|
iframeDoc.addEventListener('mouseup', preventInteraction, true);
|
||||||
|
iframeDoc.addEventListener('contextmenu', preventInteraction, true);
|
||||||
|
iframeDoc.addEventListener('dragstart', preventInteraction, true);
|
||||||
|
iframeDoc.addEventListener('selectstart', preventInteraction, true);
|
||||||
|
|
||||||
|
// Setup MutationObserver to reapply styles when DOM changes
|
||||||
|
const observer = new MutationObserver((mutations) => {
|
||||||
|
let needsReapply = false;
|
||||||
|
mutations.forEach((mutation) => {
|
||||||
|
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
|
||||||
|
needsReapply = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (needsReapply) {
|
||||||
|
console.log('🔄 PreviewFrame: DOM changed, reapplying inspect styles');
|
||||||
|
// Reapply styles after a short delay to ensure new elements are rendered
|
||||||
|
setTimeout(() => {
|
||||||
|
const currentStyle = iframeDoc.getElementById('inspect-mode-styles');
|
||||||
|
if (!currentStyle) {
|
||||||
|
// Style was removed, reapply it
|
||||||
|
const newStyle = iframeDoc.createElement('style');
|
||||||
|
newStyle.id = 'inspect-mode-styles';
|
||||||
|
newStyle.textContent = style.textContent;
|
||||||
|
iframeDoc.head.appendChild(newStyle);
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start observing DOM changes
|
||||||
|
observer.observe(iframeDoc.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
attributes: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store observer for cleanup
|
||||||
|
iframeDoc._inspectObserver = observer;
|
||||||
|
|
||||||
|
console.log('✅ PreviewFrame: Robust inspect mode with MutationObserver applied');
|
||||||
|
}, [handleIframeClick]);
|
||||||
|
|
||||||
|
const generateHtmlContent = useCallback(() => {
|
||||||
|
const scrollbarCSS = `
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
html, body {
|
||||||
|
overflow-y: overlay; /* scrollbar overlays content, no space reserved */
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
padding-right: 0; /* No padding, since scrollbar overlays */
|
||||||
|
box-sizing: border-box; /* Use border-box for correct sizing */
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(0,0,0,0.2) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
body::-webkit-scrollbar {
|
||||||
|
width: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar styles */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgba(0,0,0,0.2);
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Firefox custom scrollbar */
|
||||||
|
html {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(0,0,0,0.2) transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const finalCss = `${scrollbarCSS}\n${cssInput || ''}`;
|
||||||
|
const isFullDocument = htmlInput.trim().toLowerCase().includes('<html');
|
||||||
|
|
||||||
|
if (isFullDocument) {
|
||||||
|
let content = htmlInput;
|
||||||
|
const headEndIndex = content.toLowerCase().indexOf('</head>');
|
||||||
|
if (headEndIndex !== -1) {
|
||||||
|
const styleTag = `\n<style>\n${finalCss}\n</style>\n`;
|
||||||
|
content = content.slice(0, headEndIndex) + styleTag + content.slice(headEndIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsInput && jsInput.trim()) {
|
||||||
|
const bodyEndIndex = content.toLowerCase().lastIndexOf('</body>');
|
||||||
|
if (bodyEndIndex !== -1) {
|
||||||
|
const scriptTag = `\n<script>\n(function() {\n${jsInput}\n})();\n</script>\n`;
|
||||||
|
content = content.slice(0, bodyEndIndex) + scriptTag + content.slice(bodyEndIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
} 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>
|
||||||
|
<style>${finalCss}</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${htmlInput}
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
${jsInput || ''}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}, [htmlInput, cssInput, jsInput]);
|
||||||
|
|
||||||
|
// Effect for loading content and managing inspect mode
|
||||||
|
useEffect(() => {
|
||||||
|
const iframe = iframeRef.current;
|
||||||
|
if (!iframe) return;
|
||||||
|
|
||||||
|
const content = generateHtmlContent();
|
||||||
|
const doc = iframe.contentDocument || iframe.contentWindow.document;
|
||||||
|
|
||||||
|
// Write content
|
||||||
|
doc.open();
|
||||||
|
doc.write(content);
|
||||||
|
doc.close();
|
||||||
|
|
||||||
|
// Apply inspect mode immediately after writing content
|
||||||
|
if (inspectMode) {
|
||||||
|
console.log('🎨 Applying inspect mode styles.');
|
||||||
|
setupInspectModeStyles(doc);
|
||||||
|
}
|
||||||
|
}, [htmlInput, cssInput, jsInput, inspectMode, selectedDevice, isFullscreen, generateHtmlContent, setupInspectModeStyles]);
|
||||||
|
|
||||||
|
// Effect for injecting device frame CSS into the main document
|
||||||
|
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 });
|
||||||
|
|
||||||
|
// Non-fullscreen always uses iPhone frame (mobile view)
|
||||||
|
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'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fullscreen desktop mode: no frame
|
||||||
|
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();
|
||||||
|
|
||||||
|
if (deviceFrame) {
|
||||||
|
// Render with device frame (iPhone 14 Pro or iPad Pro)
|
||||||
|
console.log(`🎨 Rendering device frame: device-${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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render without device frame (desktop or non-fullscreen)
|
||||||
|
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;
|
||||||
720
src/pages/components/PreviewFrame.experimental.js
Normal file
720
src/pages/components/PreviewFrame.experimental.js
Normal file
@@ -0,0 +1,720 @@
|
|||||||
|
import React, { useRef, useEffect, useCallback, useMemo, useState } 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;
|
||||||
|
width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 PreviewFrameExperimental = ({
|
||||||
|
htmlInput,
|
||||||
|
cssInput,
|
||||||
|
jsInput,
|
||||||
|
selectedDevice,
|
||||||
|
inspectMode,
|
||||||
|
onElementClick,
|
||||||
|
isFullscreen
|
||||||
|
}) => {
|
||||||
|
const iframeRef = useRef(null);
|
||||||
|
const [isInitialized, setIsInitialized] = useState(false);
|
||||||
|
const [lastJsInput, setLastJsInput] = useState('');
|
||||||
|
|
||||||
|
// Handle iframe click for element selection
|
||||||
|
const handleIframeClick = useCallback((e) => {
|
||||||
|
if (!inspectMode) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const clickedElement = e.target;
|
||||||
|
const elementInfo = {
|
||||||
|
tagName: clickedElement.tagName.toLowerCase(),
|
||||||
|
innerText: clickedElement.innerText || clickedElement.textContent || '',
|
||||||
|
attributes: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
Array.from(clickedElement.attributes).forEach(attr => {
|
||||||
|
elementInfo.attributes[attr.name] = attr.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
onElementClick(elementInfo);
|
||||||
|
}, [inspectMode, onElementClick]);
|
||||||
|
|
||||||
|
// Function to setup inspect mode styles and event handlers
|
||||||
|
const setupInspectModeStyles = useCallback((iframeDoc) => {
|
||||||
|
console.log('🎨 PreviewFrame Experimental: Setting up inspect mode');
|
||||||
|
|
||||||
|
// Remove existing inspect styles and observers
|
||||||
|
const existingStyle = iframeDoc.getElementById('inspect-mode-styles');
|
||||||
|
if (existingStyle) existingStyle.remove();
|
||||||
|
|
||||||
|
// Clean up any existing observer
|
||||||
|
if (iframeDoc._inspectObserver) {
|
||||||
|
iframeDoc._inspectObserver.disconnect();
|
||||||
|
delete iframeDoc._inspectObserver;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add inspect mode styles with better hover highlights
|
||||||
|
const style = iframeDoc.createElement('style');
|
||||||
|
style.id = 'inspect-mode-styles';
|
||||||
|
style.textContent = `
|
||||||
|
/* High specificity selectors for inspect mode */
|
||||||
|
html * {
|
||||||
|
cursor: crosshair !important;
|
||||||
|
pointer-events: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover highlights with maximum specificity */
|
||||||
|
html body *:hover {
|
||||||
|
outline: 2px solid #3b82f6 !important;
|
||||||
|
outline-offset: 1px !important;
|
||||||
|
background-color: rgba(59, 130, 246, 0.1) !important;
|
||||||
|
transition: all 0.1s ease !important;
|
||||||
|
box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Selected element styles */
|
||||||
|
html body [data-selected="true"] {
|
||||||
|
outline: 2px solid #10b981 !important;
|
||||||
|
outline-offset: 1px !important;
|
||||||
|
background-color: rgba(16, 185, 129, 0.1) !important;
|
||||||
|
box-shadow: 0 0 0 1px rgba(16, 185, 129, 0.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Selected element hover */
|
||||||
|
html body [data-selected="true"]:hover {
|
||||||
|
outline: 2px solid #059669 !important;
|
||||||
|
outline-offset: 1px !important;
|
||||||
|
background-color: rgba(5, 150, 105, 0.15) !important;
|
||||||
|
box-shadow: 0 0 0 1px rgba(5, 150, 105, 0.4) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prevent text selection during inspect */
|
||||||
|
html * {
|
||||||
|
-webkit-user-select: none !important;
|
||||||
|
-moz-user-select: none !important;
|
||||||
|
-ms-user-select: none !important;
|
||||||
|
user-select: none !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
iframeDoc.head.appendChild(style);
|
||||||
|
|
||||||
|
// Add event handlers
|
||||||
|
const preventInteraction = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Enhanced click handler that marks selected elements
|
||||||
|
const enhancedClickHandler = (e) => {
|
||||||
|
if (!inspectMode) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
// Remove previous selection
|
||||||
|
const previousSelected = iframeDoc.querySelector('[data-selected="true"]');
|
||||||
|
if (previousSelected) {
|
||||||
|
previousSelected.removeAttribute('data-selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark new selection
|
||||||
|
const clickedElement = e.target;
|
||||||
|
clickedElement.setAttribute('data-selected', 'true');
|
||||||
|
|
||||||
|
// Call the original handler
|
||||||
|
handleIframeClick(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add click handler for element selection
|
||||||
|
iframeDoc.addEventListener('click', enhancedClickHandler, true);
|
||||||
|
|
||||||
|
// Prevent other interactions during inspect mode
|
||||||
|
iframeDoc.addEventListener('mousedown', preventInteraction, true);
|
||||||
|
iframeDoc.addEventListener('mouseup', preventInteraction, true);
|
||||||
|
iframeDoc.addEventListener('contextmenu', preventInteraction, true);
|
||||||
|
iframeDoc.addEventListener('dragstart', preventInteraction, true);
|
||||||
|
iframeDoc.addEventListener('selectstart', preventInteraction, true);
|
||||||
|
|
||||||
|
console.log('✅ PreviewFrame Experimental: Inspect mode applied');
|
||||||
|
}, [handleIframeClick, inspectMode]);
|
||||||
|
|
||||||
|
// Generate base HTML structure (without user JS)
|
||||||
|
const generateBaseHtml = useCallback(() => {
|
||||||
|
const scrollbarCSS = `
|
||||||
|
/* Custom Scrollbar Styles */
|
||||||
|
::-webkit-scrollbar { width: 8px; height: 8px; }
|
||||||
|
::-webkit-scrollbar-track { background: #f1f1f1; }
|
||||||
|
::-webkit-scrollbar-thumb { background: #888; border-radius: 4px; }
|
||||||
|
::-webkit-scrollbar-thumb:hover { background: #555; }
|
||||||
|
html { scrollbar-width: thin; scrollbar-color: #888 #f1f1f1; }
|
||||||
|
`;
|
||||||
|
const finalCss = `${scrollbarCSS}\n${cssInput || ''}`;
|
||||||
|
const isFullDocument = htmlInput.trim().toLowerCase().includes('<html');
|
||||||
|
|
||||||
|
if (isFullDocument) {
|
||||||
|
let content = htmlInput;
|
||||||
|
const headEndIndex = content.toLowerCase().indexOf('</head>');
|
||||||
|
if (headEndIndex !== -1) {
|
||||||
|
const styleTag = `\n<style>\n${finalCss}\n</style>\n`;
|
||||||
|
content = content.slice(0, headEndIndex) + styleTag + content.slice(headEndIndex);
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
} 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>
|
||||||
|
<style>${finalCss}</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${htmlInput}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}, [htmlInput, cssInput]);
|
||||||
|
|
||||||
|
// Initialize iframe with base content (runs once or when device changes)
|
||||||
|
const initializeIframe = useCallback(() => {
|
||||||
|
const iframe = iframeRef.current;
|
||||||
|
if (!iframe) return;
|
||||||
|
|
||||||
|
console.log('🚀 PreviewFrame Experimental: Initializing iframe');
|
||||||
|
const content = generateBaseHtml();
|
||||||
|
const doc = iframe.contentDocument || iframe.contentWindow.document;
|
||||||
|
|
||||||
|
// Write base content
|
||||||
|
doc.open();
|
||||||
|
doc.write(content);
|
||||||
|
doc.close();
|
||||||
|
|
||||||
|
setIsInitialized(true);
|
||||||
|
setLastJsInput(''); // Reset JS tracking
|
||||||
|
|
||||||
|
console.log('✅ PreviewFrame Experimental: Base iframe initialized');
|
||||||
|
}, [generateBaseHtml]);
|
||||||
|
|
||||||
|
// Update only JavaScript content (without full page reload)
|
||||||
|
const updateJavaScript = useCallback(() => {
|
||||||
|
if (!isInitialized || !jsInput || jsInput === lastJsInput) return;
|
||||||
|
|
||||||
|
const iframe = iframeRef.current;
|
||||||
|
if (!iframe) return;
|
||||||
|
|
||||||
|
console.log('📝 PreviewFrame Experimental: Updating JavaScript only');
|
||||||
|
const doc = iframe.contentDocument || iframe.contentWindow.document;
|
||||||
|
|
||||||
|
// Remove previous script if exists
|
||||||
|
const existingScript = doc.getElementById('user-script');
|
||||||
|
if (existingScript) {
|
||||||
|
existingScript.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new script with IIFE wrapper
|
||||||
|
if (jsInput.trim()) {
|
||||||
|
const script = doc.createElement('script');
|
||||||
|
script.id = 'user-script';
|
||||||
|
script.textContent = `
|
||||||
|
(function() {
|
||||||
|
try {
|
||||||
|
${jsInput}
|
||||||
|
} catch(e) {
|
||||||
|
console.error('User script error:', e);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
`;
|
||||||
|
doc.body.appendChild(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLastJsInput(jsInput);
|
||||||
|
console.log('✅ PreviewFrame Experimental: JavaScript updated');
|
||||||
|
}, [isInitialized, jsInput, lastJsInput]);
|
||||||
|
|
||||||
|
// Update HTML content (requires partial reload)
|
||||||
|
const updateHtmlContent = useCallback(() => {
|
||||||
|
if (!isInitialized) return;
|
||||||
|
|
||||||
|
const iframe = iframeRef.current;
|
||||||
|
if (!iframe) return;
|
||||||
|
|
||||||
|
console.log('🔄 PreviewFrame Experimental: Updating HTML content');
|
||||||
|
const doc = iframe.contentDocument || iframe.contentWindow.document;
|
||||||
|
|
||||||
|
// Update body content only (preserve head and scripts)
|
||||||
|
const isFullDocument = htmlInput.trim().toLowerCase().includes('<html');
|
||||||
|
|
||||||
|
if (!isFullDocument) {
|
||||||
|
doc.body.innerHTML = htmlInput;
|
||||||
|
|
||||||
|
// Re-add user script after HTML update
|
||||||
|
updateJavaScript();
|
||||||
|
|
||||||
|
// Re-apply inspect mode if enabled
|
||||||
|
if (inspectMode) {
|
||||||
|
setupInspectModeStyles(doc);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For full documents, we need to reinitialize
|
||||||
|
initializeIframe();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ PreviewFrame Experimental: HTML content updated');
|
||||||
|
}, [isInitialized, htmlInput, inspectMode, updateJavaScript, setupInspectModeStyles, initializeIframe]);
|
||||||
|
|
||||||
|
// Effect for iframe initialization (runs when device changes)
|
||||||
|
useEffect(() => {
|
||||||
|
initializeIframe();
|
||||||
|
}, [selectedDevice, isFullscreen, initializeIframe]);
|
||||||
|
|
||||||
|
// Effect for HTML content updates
|
||||||
|
useEffect(() => {
|
||||||
|
if (isInitialized) {
|
||||||
|
updateHtmlContent();
|
||||||
|
}
|
||||||
|
}, [htmlInput, cssInput, updateHtmlContent, isInitialized]);
|
||||||
|
|
||||||
|
// Effect for JavaScript updates
|
||||||
|
useEffect(() => {
|
||||||
|
updateJavaScript();
|
||||||
|
}, [jsInput, updateJavaScript]);
|
||||||
|
|
||||||
|
// Effect for inspect mode
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isInitialized) return;
|
||||||
|
|
||||||
|
const iframe = iframeRef.current;
|
||||||
|
if (!iframe) return;
|
||||||
|
|
||||||
|
const doc = iframe.contentDocument || iframe.contentWindow.document;
|
||||||
|
|
||||||
|
if (inspectMode) {
|
||||||
|
console.log('🎨 PreviewFrame Experimental: Enabling inspect mode');
|
||||||
|
setupInspectModeStyles(doc);
|
||||||
|
} else {
|
||||||
|
console.log('🎨 PreviewFrame Experimental: Disabling inspect mode');
|
||||||
|
const existingStyle = doc.getElementById('inspect-mode-styles');
|
||||||
|
if (existingStyle) existingStyle.remove();
|
||||||
|
|
||||||
|
// Remove selected attributes
|
||||||
|
const selectedElements = doc.querySelectorAll('[data-selected="true"]');
|
||||||
|
selectedElements.forEach(el => el.removeAttribute('data-selected'));
|
||||||
|
}
|
||||||
|
}, [inspectMode, isInitialized, setupInspectModeStyles]);
|
||||||
|
|
||||||
|
// Effect for injecting device frame CSS into the main document
|
||||||
|
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 (Experimental):', { isFullscreen, selectedDevice });
|
||||||
|
|
||||||
|
// Non-fullscreen always uses iPhone frame (mobile view)
|
||||||
|
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'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fullscreen desktop mode: no frame
|
||||||
|
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();
|
||||||
|
|
||||||
|
if (deviceFrame) {
|
||||||
|
// Render with device frame (iPhone 14 Pro or iPad Pro)
|
||||||
|
console.log(`🎨 Rendering device frame (Experimental): device-${deviceFrame}`);
|
||||||
|
return (
|
||||||
|
<div className={wrapperClass}>
|
||||||
|
<div className={`device device-${deviceFrame}`}>
|
||||||
|
<div className="device-frame">
|
||||||
|
<iframe
|
||||||
|
ref={iframeRef}
|
||||||
|
key={`experimental-device-${deviceFrame}-${selectedDevice}-${isFullscreen}`}
|
||||||
|
className="device-screen w-full h-full border-0"
|
||||||
|
title="HTML Preview (Experimental)"
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render without device frame (desktop or non-fullscreen)
|
||||||
|
return (
|
||||||
|
<div className={`${wrapperClass} bg-white rounded-lg shadow-lg`}>
|
||||||
|
<iframe
|
||||||
|
ref={iframeRef}
|
||||||
|
key={`experimental-no-device-${selectedDevice}-${isFullscreen}`}
|
||||||
|
className="w-full h-full border-0 overflow-hidden"
|
||||||
|
title="HTML Preview (Experimental)"
|
||||||
|
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals"
|
||||||
|
style={{ maxWidth: '100%', maxHeight: '100%' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PreviewFrameExperimental;
|
||||||
852
src/pages/components/PreviewFrame.fresh.js
Normal file
852
src/pages/components/PreviewFrame.fresh.js
Normal file
@@ -0,0 +1,852 @@
|
|||||||
|
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;
|
||||||
1283
src/pages/components/PreviewFrame.js
Normal file
1283
src/pages/components/PreviewFrame.js
Normal file
File diff suppressed because it is too large
Load Diff
105
src/pages/components/PreviewServer.js
Normal file
105
src/pages/components/PreviewServer.js
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preview Server - A completely different approach that avoids iframe JS issues
|
||||||
|
* Instead of injecting content into iframe, we create blob URLs that serve as actual web pages
|
||||||
|
*/
|
||||||
|
class PreviewServer {
|
||||||
|
constructor() {
|
||||||
|
this.currentBlobUrl = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a blob URL that serves as a real web page
|
||||||
|
createPreviewUrl(htmlInput, cssInput, jsInput) {
|
||||||
|
// Clean up previous blob URL
|
||||||
|
if (this.currentBlobUrl) {
|
||||||
|
URL.revokeObjectURL(this.currentBlobUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate complete HTML document
|
||||||
|
const fullHtml = this.generateCompleteHtml(htmlInput, cssInput, jsInput);
|
||||||
|
|
||||||
|
// Create blob with proper MIME type
|
||||||
|
const blob = new Blob([fullHtml], { type: 'text/html' });
|
||||||
|
|
||||||
|
// Create object URL
|
||||||
|
this.currentBlobUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
return this.currentBlobUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
generateCompleteHtml(htmlInput, cssInput, jsInput) {
|
||||||
|
// Check if user provided full HTML document
|
||||||
|
const isFullDocument = htmlInput.trim().toLowerCase().includes('<!doctype') ||
|
||||||
|
htmlInput.trim().toLowerCase().includes('<html');
|
||||||
|
|
||||||
|
if (isFullDocument) {
|
||||||
|
// User provided full document - inject CSS and JS appropriately
|
||||||
|
let modifiedHtml = htmlInput;
|
||||||
|
|
||||||
|
// Inject CSS into head if provided
|
||||||
|
if (cssInput && cssInput.trim()) {
|
||||||
|
const cssTag = `<style>${cssInput}</style>`;
|
||||||
|
if (modifiedHtml.includes('</head>')) {
|
||||||
|
modifiedHtml = modifiedHtml.replace('</head>', `${cssTag}\n</head>`);
|
||||||
|
} else {
|
||||||
|
// Add head section if missing
|
||||||
|
modifiedHtml = modifiedHtml.replace('<html>', '<html>\n<head>\n' + cssTag + '\n</head>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject JS before closing body if provided
|
||||||
|
if (jsInput && jsInput.trim()) {
|
||||||
|
const jsTag = `<script>${jsInput}</script>`;
|
||||||
|
if (modifiedHtml.includes('</body>')) {
|
||||||
|
modifiedHtml = modifiedHtml.replace('</body>', `${jsTag}\n</body>`);
|
||||||
|
} else {
|
||||||
|
// Add script at the end
|
||||||
|
modifiedHtml += `\n${jsTag}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return modifiedHtml;
|
||||||
|
} else {
|
||||||
|
// User provided fragment - create complete document
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>HTML Preview</title>
|
||||||
|
${cssInput ? `<style>${cssInput}</style>` : ''}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${htmlInput}
|
||||||
|
${jsInput ? `<script>${jsInput}</script>` : ''}
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up resources
|
||||||
|
cleanup() {
|
||||||
|
if (this.currentBlobUrl) {
|
||||||
|
URL.revokeObjectURL(this.currentBlobUrl);
|
||||||
|
this.currentBlobUrl = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hook for using the preview server
|
||||||
|
export const usePreviewServer = () => {
|
||||||
|
const [previewServer] = useState(() => new PreviewServer());
|
||||||
|
|
||||||
|
const createPreviewUrl = useCallback((htmlInput, cssInput, jsInput) => {
|
||||||
|
return previewServer.createPreviewUrl(htmlInput, cssInput, jsInput);
|
||||||
|
}, [previewServer]);
|
||||||
|
|
||||||
|
const cleanup = useCallback(() => {
|
||||||
|
previewServer.cleanup();
|
||||||
|
}, [previewServer]);
|
||||||
|
|
||||||
|
return { createPreviewUrl, cleanup };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PreviewServer;
|
||||||
132
src/pages/components/Toolbar.js
Normal file
132
src/pages/components/Toolbar.js
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { RefreshCw, Monitor, Tablet, Smartphone, Inspect, Maximize2, Minimize2, Code } from 'lucide-react';
|
||||||
|
|
||||||
|
const Toolbar = ({
|
||||||
|
selectedDevice,
|
||||||
|
setSelectedDevice,
|
||||||
|
isInspectModeActive,
|
||||||
|
setInspectMode,
|
||||||
|
isFullscreen,
|
||||||
|
onRefresh,
|
||||||
|
onToggleFullscreen,
|
||||||
|
onToggleSidebar,
|
||||||
|
showSidebar,
|
||||||
|
cleanupInspectorState,
|
||||||
|
inspectedElementInfo
|
||||||
|
}) => {
|
||||||
|
const handleDeviceChange = (device) => {
|
||||||
|
if (!isFullscreen && (device === 'desktop' || device === 'tablet')) {
|
||||||
|
onToggleFullscreen(device);
|
||||||
|
} else {
|
||||||
|
setSelectedDevice(device);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInspectToggle = () => {
|
||||||
|
if (isInspectModeActive) {
|
||||||
|
// If already in inspect mode, just turn it off
|
||||||
|
cleanupInspectorState();
|
||||||
|
} else {
|
||||||
|
// Activate inspect mode (no need to cleanup when activating)
|
||||||
|
console.log('🎯 TOOLBAR: Activating inspect mode');
|
||||||
|
setInspectMode(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 p-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
{/* Left side - Code toggle (fullscreen only) */}
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
{isFullscreen && (
|
||||||
|
<button
|
||||||
|
onClick={onToggleSidebar}
|
||||||
|
className={`p-2 rounded-md transition-colors ${
|
||||||
|
showSidebar
|
||||||
|
? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400'
|
||||||
|
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600'
|
||||||
|
}`}
|
||||||
|
title="Toggle Code Sidebar"
|
||||||
|
>
|
||||||
|
<Code className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Center - Preview controls */}
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={onRefresh}
|
||||||
|
className="p-2 bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
|
||||||
|
title="Refresh Preview"
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex bg-gray-100 dark:bg-gray-700 rounded-md p-1">
|
||||||
|
<button
|
||||||
|
onClick={() => handleDeviceChange('desktop')}
|
||||||
|
className={`p-2 rounded transition-colors ${
|
||||||
|
selectedDevice === 'desktop'
|
||||||
|
? 'bg-blue-500 text-white'
|
||||||
|
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600'
|
||||||
|
}`}
|
||||||
|
title="Desktop View"
|
||||||
|
>
|
||||||
|
<Monitor className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDeviceChange('tablet')}
|
||||||
|
className={`p-2 rounded transition-colors ${
|
||||||
|
selectedDevice === 'tablet'
|
||||||
|
? 'bg-blue-500 text-white'
|
||||||
|
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600'
|
||||||
|
}`}
|
||||||
|
title="Tablet View"
|
||||||
|
>
|
||||||
|
<Tablet className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDeviceChange('mobile')}
|
||||||
|
className={`p-2 rounded transition-colors ${
|
||||||
|
selectedDevice === 'mobile'
|
||||||
|
? 'bg-blue-500 text-white'
|
||||||
|
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600'
|
||||||
|
}`}
|
||||||
|
title="Mobile View"
|
||||||
|
>
|
||||||
|
<Smartphone className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isFullscreen && (
|
||||||
|
<button
|
||||||
|
onClick={handleInspectToggle}
|
||||||
|
className={`p-2 rounded-md transition-colors ${
|
||||||
|
isInspectModeActive
|
||||||
|
? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400'
|
||||||
|
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600'
|
||||||
|
}`}
|
||||||
|
title="Inspect Element"
|
||||||
|
>
|
||||||
|
<Inspect className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right side - Fullscreen toggle */}
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={() => onToggleFullscreen()}
|
||||||
|
className="p-2 bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
|
||||||
|
title={isFullscreen ? "Exit Fullscreen" : "Enter Fullscreen"}
|
||||||
|
>
|
||||||
|
{isFullscreen ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Toolbar;
|
||||||
178
src/styles/device-frames.css
Normal file
178
src/styles/device-frames.css
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
/* Device Frames CSS - Inspired by css-device-frames */
|
||||||
|
|
||||||
|
/* Mobile Device Frame */
|
||||||
|
.device-frame-mobile {
|
||||||
|
position: relative;
|
||||||
|
width: 375px;
|
||||||
|
height: 667px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 30px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-frame-mobile::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 60px;
|
||||||
|
height: 6px;
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-frame-mobile::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 8px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-frame-mobile iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% - 40px);
|
||||||
|
border: none;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: white;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Compact for Non-Fullscreen */
|
||||||
|
.device-frame-mobile-compact {
|
||||||
|
position: relative;
|
||||||
|
width: 375px;
|
||||||
|
height: 600px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 25px;
|
||||||
|
padding: 15px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-frame-mobile-compact::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 6px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 50px;
|
||||||
|
height: 4px;
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-frame-mobile-compact::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 6px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-frame-mobile-compact iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% - 30px);
|
||||||
|
border: none;
|
||||||
|
border-radius: 15px;
|
||||||
|
background: white;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablet Device Frame */
|
||||||
|
.device-frame-tablet {
|
||||||
|
position: relative;
|
||||||
|
width: 768px;
|
||||||
|
height: 1024px;
|
||||||
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
border-radius: 40px;
|
||||||
|
padding: 30px;
|
||||||
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-frame-tablet::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-frame-tablet iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% - 60px);
|
||||||
|
border: none;
|
||||||
|
border-radius: 25px;
|
||||||
|
background: white;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode adjustments */
|
||||||
|
.dark .device-frame-mobile,
|
||||||
|
.dark .device-frame-mobile-compact {
|
||||||
|
background: linear-gradient(135deg, #434343 0%, #000000 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .device-frame-tablet {
|
||||||
|
background: linear-gradient(135deg, #434343 0%, #000000 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .device-frame-mobile::before,
|
||||||
|
.dark .device-frame-mobile-compact::before,
|
||||||
|
.dark .device-frame-tablet::before,
|
||||||
|
.dark .device-frame-mobile::after,
|
||||||
|
.dark .device-frame-mobile-compact::after,
|
||||||
|
.dark .device-frame-tablet::after {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-color: rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.device-frame-mobile,
|
||||||
|
.device-frame-mobile-compact {
|
||||||
|
width: 300px;
|
||||||
|
height: 480px;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-frame-mobile iframe,
|
||||||
|
.device-frame-mobile-compact iframe {
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-top: 12px;
|
||||||
|
height: calc(100% - 24px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-frame-tablet {
|
||||||
|
width: 600px;
|
||||||
|
height: 800px;
|
||||||
|
border-radius: 30px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-frame-tablet iframe {
|
||||||
|
border-radius: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
height: calc(100% - 40px);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user