diff --git a/.windsurf/rules/developer-tools.md b/.windsurf/rules/developer-tools.md
new file mode 100644
index 00000000..412d103b
--- /dev/null
+++ b/.windsurf/rules/developer-tools.md
@@ -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
\ No newline at end of file
diff --git a/src/App.js b/src/App.js
index ffbbe3a0..1cc3178c 100644
--- a/src/App.js
+++ b/src/App.js
@@ -9,6 +9,7 @@ import Base64Tool from './pages/Base64Tool';
import CsvJsonTool from './pages/CsvJsonTool';
import BeautifierTool from './pages/BeautifierTool';
import DiffTool from './pages/DiffTool';
+import HtmlPreviewTool from './pages/HtmlPreviewTool';
import './index.css';
function App() {
@@ -24,6 +25,7 @@ function App() {
} />
} />
} />
+ } />
diff --git a/src/components/Layout.js b/src/components/Layout.js
index 06dbd852..4a283392 100644
--- a/src/components/Layout.js
+++ b/src/components/Layout.js
@@ -1,6 +1,6 @@
import React, { useState, useEffect, useRef } from 'react';
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';
const Layout = ({ children }) => {
@@ -40,7 +40,8 @@ const Layout = ({ children }) => {
{ 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: '/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 (
diff --git a/src/pages/Base64Tool.js b/src/pages/Base64Tool.js
index 9a55c9e0..f441c418 100644
--- a/src/pages/Base64Tool.js
+++ b/src/pages/Base64Tool.js
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
-import { Hash, Upload, Download } from 'lucide-react';
+import { Hash, Upload } from 'lucide-react';
import ToolLayout from '../components/ToolLayout';
import CopyButton from '../components/CopyButton';
diff --git a/src/pages/BeautifierTool.js b/src/pages/BeautifierTool.js
index b502952d..f27df38a 100644
--- a/src/pages/BeautifierTool.js
+++ b/src/pages/BeautifierTool.js
@@ -144,6 +144,9 @@ const BeautifierTool = () => {
};
const beautifyHtml = (text) => {
+ // Declare formatted variable outside try/catch block
+ let formatted = '';
+
try {
// Clean input text first
let cleanText = text.trim();
@@ -154,14 +157,9 @@ const BeautifierTool = () => {
// 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'];
- // 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;
const tab = ' ';
-
- // Better HTML parsing
+ let formatted = ''; // Better HTML parsing
const tokens = cleanText.match(/<\/?[^>]+>|[^<]+/g) || [];
for (let i = 0; i < tokens.length; i++) {
@@ -203,7 +201,7 @@ const BeautifierTool = () => {
return formatted.trim();
} catch (err) {
// Fallback to simple formatting if advanced parsing fails
- let formatted = '';
+ formatted = '';
let indent = 0;
const tab = ' ';
diff --git a/src/pages/HtmlPreviewTool.js b/src/pages/HtmlPreviewTool.js
new file mode 100644
index 00000000..e162c97b
--- /dev/null
+++ b/src/pages/HtmlPreviewTool.js
@@ -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(' {
+ 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(' {
+ // 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 (
+
+ {/* Main content area */}
+
+ {/* Left sidebar - Code inputs */}
+ {showSidebar && (
+
+
+
Code Editor
+
+
+
+
+
+ )}
+
+ {/* Center - Preview */}
+
+
+ {/* Inspector Sidebar */}
+ {inspectedElementInfo && (
+
+ )}
+
+
+ {/* Bottom toolbar */}
+
+
+ );
+ }
+
+ return (
+
+
+ {/* Left column - Code inputs */}
+
+
+
+
+ {/* Middle column - Preview */}
+
+
+ {/* ENHANCED OPTION A: Inspector Sidebar */}
+ {inspectedElementInfo && (
+
+
+
+ )}
+
+
+ {/* Bottom toolbar */}
+
+
+
+
+ );
+};
+
+export default HtmlPreviewTool;
diff --git a/src/pages/HtmlPreviewTool.js.backup b/src/pages/HtmlPreviewTool.js.backup
new file mode 100644
index 00000000..3d802ed1
--- /dev/null
+++ b/src/pages/HtmlPreviewTool.js.backup
@@ -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(' {
+ 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 (
+
+ {/* Left Sidebar - Code Input */}
+
+
+
+
+
+
+
+
+
+ {showCss && (
+
+
+
+
+ )}
+ {showJs && (
+
+
+
+
+ )}
+
+
+ {/* Preview Area */}
+
+ {!isFullscreen && (
+
+
Preview
+
+ )}
+
+
+ {/* Preview Tools - Bottom Toolbar */}
+
+
+ {isFullscreen && (
+
+ )}
+
+
+
+ {isFullscreen && (
+
+ )}
+ {Object.entries(devices).map(([key, { icon: DeviceIcon }]) => (
+
+ ))}
+
+
+
+
+
+
+
+ {isFullscreen && inspectedElementInfo && (
+
+ )}
+
+ }
+ />
+ );
+};
+
+// ##################################################################################
+// # 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 Loading editor...
;
+
+ const otherAttributes = Object.keys(edited).filter(
+ key => key !== 'tagName' && key !== 'id' && key !== 'className' && key !== 'innerText'
+ );
+
+ return (
+
+ {['tagName', 'id', 'className'].map(field => (
+
+
+ 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"
+ />
+
+ ))}
+
+
+
+ {otherAttributes.map(attr => (
+
+
+ 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"
+ />
+
+ ))}
+
+
+
+
+
+ );
+};
+
+export default HtmlPreviewTool;
diff --git a/src/pages/components/CodeInputs.js b/src/pages/components/CodeInputs.js
new file mode 100644
index 00000000..b8267b70
--- /dev/null
+++ b/src/pages/components/CodeInputs.js
@@ -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 (
+
+ {/* HTML Input */}
+
+
+
+
+
+
+
+ {/* CSS Input */}
+ {showCss && (
+
+
+
+
+
+
+ )}
+
+ {/* JS Input */}
+ {showJs && (
+
+
+
+
+
+
+ )}
+
+ );
+};
+
+export default CodeInputs;
diff --git a/src/pages/components/ElementEditor.js b/src/pages/components/ElementEditor.js
new file mode 100644
index 00000000..2a32068f
--- /dev/null
+++ b/src/pages/components/ElementEditor.js
@@ -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(/\n`;
+ content = content.slice(0, headEndIndex) + styleTag + content.slice(headEndIndex);
+ }
+
+ if (jsInput && jsInput.trim()) {
+ const bodyEndIndex = content.toLowerCase().lastIndexOf('');
+ if (bodyEndIndex !== -1) {
+ const scriptTag = `\n\n`;
+ content = content.slice(0, bodyEndIndex) + scriptTag + content.slice(bodyEndIndex);
+ }
+ }
+ return content;
+ } else {
+ return `
+
+
+
+
+
+ Preview
+
+
+
+ ${htmlInput}
+
+
+
+ `;
+ }
+ }, [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 (
+
+ );
+ }
+
+ // Render without device frame (desktop or non-fullscreen)
+ return (
+
+
+
+ );
+};
+
+export default PreviewFrame;
diff --git a/src/pages/components/PreviewFrame.experimental.js b/src/pages/components/PreviewFrame.experimental.js
new file mode 100644
index 00000000..f480ec9e
--- /dev/null
+++ b/src/pages/components/PreviewFrame.experimental.js
@@ -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('');
+ if (headEndIndex !== -1) {
+ const styleTag = `\n\n`;
+ content = content.slice(0, headEndIndex) + styleTag + content.slice(headEndIndex);
+ }
+ return content;
+ } else {
+ return `
+
+
+
+
+
+ Preview
+
+
+
+ ${htmlInput}
+
+
+ `;
+ }
+ }, [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(' {
+ 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 (
+
+ );
+ }
+
+ // Render without device frame (desktop or non-fullscreen)
+ return (
+
+
+
+ );
+};
+
+export default PreviewFrameExperimental;
diff --git a/src/pages/components/PreviewFrame.fresh.js b/src/pages/components/PreviewFrame.fresh.js
new file mode 100644
index 00000000..e68296e8
--- /dev/null
+++ b/src/pages/components/PreviewFrame.fresh.js
@@ -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('`;
+ const customStyles = ``;
+ const scripts = ``;
+
+ if (isFullHtml) {
+ let processedHtml = htmlInput;
+ if (!processedHtml.includes(cssLink)) {
+ processedHtml = processedHtml.replace('', `${cssLink}`);
+ }
+ if (!processedHtml.includes(customStyles)) {
+ processedHtml = processedHtml.replace('', `${customStyles}`);
+ }
+ if (!processedHtml.includes(jsInput)) {
+ processedHtml = processedHtml.replace('', `${scripts}
+ ${htmlInput}
+ ${scripts}
+ `);
+ }
+ return processedHtml;
+ } else {
+ return `
+
+
+