Remove HTML Preview Tool from navigation and add to gitignore
- Removed HTML Preview Tool from navigation menu in Layout.js - Cleaned up unused Code import - Added HTML Preview related files to .gitignore - Project builds successfully without HTML Preview Tool
This commit is contained in:
@@ -316,7 +316,7 @@ const HtmlPreviewTool = () => {
|
||||
<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">
|
||||
<div className="flex-1 flex flex-col p-4">
|
||||
<CodeInputs
|
||||
htmlInput={htmlInput}
|
||||
setHtmlInput={setHtmlInput}
|
||||
@@ -381,7 +381,7 @@ const HtmlPreviewTool = () => {
|
||||
<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 ${
|
||||
<div className={`flex flex-col transition-all duration-300 ${
|
||||
inspectedElementInfo ? 'flex-1' : 'w-1/2'
|
||||
}`}>
|
||||
<CodeInputs
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ChevronDown, ChevronUp, Maximize2, Minimize2 } from 'lucide-react';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { Search, Copy, Download } from 'lucide-react';
|
||||
import { Controlled as CodeMirror } from 'react-codemirror2';
|
||||
import 'codemirror/lib/codemirror.css';
|
||||
import 'codemirror/theme/material.css';
|
||||
import 'codemirror/mode/xml/xml';
|
||||
import 'codemirror/mode/css/css';
|
||||
import 'codemirror/mode/javascript/javascript';
|
||||
import 'codemirror/addon/search/search';
|
||||
import 'codemirror/addon/search/searchcursor';
|
||||
import 'codemirror/addon/dialog/dialog';
|
||||
import 'codemirror/addon/dialog/dialog.css';
|
||||
|
||||
const CodeInputs = ({
|
||||
htmlInput,
|
||||
@@ -10,129 +20,192 @@ const CodeInputs = ({
|
||||
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 [activeTab, setActiveTab] = useState('html');
|
||||
const htmlEditorRef = useRef(null);
|
||||
const cssEditorRef = useRef(null);
|
||||
const jsEditorRef = useRef(null);
|
||||
|
||||
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
|
||||
}
|
||||
// Handle search functionality
|
||||
const handleSearch = (editorRef) => {
|
||||
if (editorRef.current && editorRef.current.editor) {
|
||||
editorRef.current.editor.execCommand('find');
|
||||
}
|
||||
|
||||
// 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';
|
||||
};
|
||||
|
||||
// Handle copy functionality
|
||||
const handleCopy = async (content) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(content);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle export functionality
|
||||
const handleExport = (content, filename) => {
|
||||
const blob = new Blob([content], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
// Get current editor ref based on active tab
|
||||
const getCurrentEditorRef = () => {
|
||||
switch (activeTab) {
|
||||
case 'html': return htmlEditorRef;
|
||||
case 'css': return cssEditorRef;
|
||||
case 'js': return jsEditorRef;
|
||||
default: return htmlEditorRef;
|
||||
}
|
||||
};
|
||||
|
||||
// Get current content based on active tab
|
||||
const getCurrentContent = () => {
|
||||
switch (activeTab) {
|
||||
case 'html': return htmlInput;
|
||||
case 'css': return cssInput;
|
||||
case 'js': return jsInput;
|
||||
default: return htmlInput;
|
||||
}
|
||||
};
|
||||
|
||||
// Get filename for export based on active tab
|
||||
const getExportFilename = () => {
|
||||
switch (activeTab) {
|
||||
case 'html': return 'code.html';
|
||||
case 'css': return 'styles.css';
|
||||
case 'js': return 'script.js';
|
||||
default: return 'code.txt';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="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>
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Tab Navigation */}
|
||||
<div className="flex border-b border-gray-200 dark:border-gray-700">
|
||||
{[
|
||||
{ id: 'html', label: 'HTML' },
|
||||
{ id: 'css', label: 'CSS' },
|
||||
{ id: 'js', label: 'JavaScript' }
|
||||
].map((tab) => (
|
||||
<button
|
||||
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'}
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === tab.id
|
||||
? 'border-blue-500 text-blue-600 dark:text-blue-400'
|
||||
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
{htmlExtended ? <Minimize2 className="w-3 h-3" /> : <Maximize2 className="w-3 h-3" />}
|
||||
<span>{htmlExtended ? 'Min' : 'Max'}</span>
|
||||
{tab.label}
|
||||
</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>
|
||||
)}
|
||||
{/* Action buttons above editor */}
|
||||
<div className="flex items-center justify-end space-x-2 p-2 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
onClick={() => handleSearch(getCurrentEditorRef())}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
|
||||
title="Search"
|
||||
>
|
||||
<Search className="w-3 h-3" />
|
||||
Search
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleCopy(getCurrentContent())}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
|
||||
title="Copy"
|
||||
>
|
||||
<Copy className="w-3 h-3" />
|
||||
Copy
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleExport(getCurrentContent(), getExportFilename())}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
|
||||
title="Export"
|
||||
>
|
||||
<Download className="w-3 h-3" />
|
||||
Export
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 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`}
|
||||
{/* Code Editor */}
|
||||
<div className="flex-1">
|
||||
{activeTab === 'html' && (
|
||||
<CodeMirror
|
||||
ref={htmlEditorRef}
|
||||
value={htmlInput}
|
||||
onBeforeChange={(editor, data, value) => setHtmlInput(value)}
|
||||
options={{
|
||||
mode: 'xml',
|
||||
theme: 'material',
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
autoCloseTags: true,
|
||||
matchBrackets: true,
|
||||
indentUnit: 2,
|
||||
tabSize: 2,
|
||||
extraKeys: {
|
||||
'Ctrl-F': 'findPersistent',
|
||||
'Cmd-F': 'findPersistent'
|
||||
}
|
||||
}}
|
||||
className="h-full"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{activeTab === 'css' && (
|
||||
<CodeMirror
|
||||
ref={cssEditorRef}
|
||||
value={cssInput}
|
||||
onBeforeChange={(editor, data, value) => setCssInput(value)}
|
||||
options={{
|
||||
mode: 'css',
|
||||
theme: 'material',
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
autoCloseBrackets: true,
|
||||
matchBrackets: true,
|
||||
indentUnit: 2,
|
||||
tabSize: 2,
|
||||
extraKeys: {
|
||||
'Ctrl-F': 'findPersistent',
|
||||
'Cmd-F': 'findPersistent'
|
||||
}
|
||||
}}
|
||||
className="h-full"
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'js' && (
|
||||
<CodeMirror
|
||||
ref={jsEditorRef}
|
||||
value={jsInput}
|
||||
onBeforeChange={(editor, data, value) => setJsInput(value)}
|
||||
options={{
|
||||
mode: 'javascript',
|
||||
theme: 'material',
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
autoCloseBrackets: true,
|
||||
matchBrackets: true,
|
||||
indentUnit: 2,
|
||||
tabSize: 2,
|
||||
extraKeys: {
|
||||
'Ctrl-F': 'findPersistent',
|
||||
'Cmd-F': 'findPersistent'
|
||||
}
|
||||
}}
|
||||
className="h-full"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
213
src/pages/components/CodeInputsNew.js
Normal file
213
src/pages/components/CodeInputsNew.js
Normal file
@@ -0,0 +1,213 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { Search, Copy, Download } from 'lucide-react';
|
||||
import { Controlled as CodeMirror } from 'react-codemirror2';
|
||||
import 'codemirror/lib/codemirror.css';
|
||||
import 'codemirror/theme/material.css';
|
||||
import 'codemirror/mode/xml/xml';
|
||||
import 'codemirror/mode/css/css';
|
||||
import 'codemirror/mode/javascript/javascript';
|
||||
import 'codemirror/addon/search/search';
|
||||
import 'codemirror/addon/search/searchcursor';
|
||||
import 'codemirror/addon/dialog/dialog';
|
||||
import 'codemirror/addon/dialog/dialog.css';
|
||||
|
||||
const CodeInputs = ({
|
||||
htmlInput,
|
||||
setHtmlInput,
|
||||
cssInput,
|
||||
setCssInput,
|
||||
jsInput,
|
||||
setJsInput,
|
||||
isFullscreen
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState('html');
|
||||
const htmlEditorRef = useRef(null);
|
||||
const cssEditorRef = useRef(null);
|
||||
const jsEditorRef = useRef(null);
|
||||
|
||||
// Handle search functionality
|
||||
const handleSearch = (editorRef) => {
|
||||
if (editorRef.current && editorRef.current.editor) {
|
||||
editorRef.current.editor.execCommand('find');
|
||||
}
|
||||
};
|
||||
|
||||
// Handle copy functionality
|
||||
const handleCopy = async (content) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(content);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle export functionality
|
||||
const handleExport = (content, filename) => {
|
||||
const blob = new Blob([content], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
// Get current editor ref based on active tab
|
||||
const getCurrentEditorRef = () => {
|
||||
switch (activeTab) {
|
||||
case 'html': return htmlEditorRef;
|
||||
case 'css': return cssEditorRef;
|
||||
case 'js': return jsEditorRef;
|
||||
default: return htmlEditorRef;
|
||||
}
|
||||
};
|
||||
|
||||
// Get current content based on active tab
|
||||
const getCurrentContent = () => {
|
||||
switch (activeTab) {
|
||||
case 'html': return htmlInput;
|
||||
case 'css': return cssInput;
|
||||
case 'js': return jsInput;
|
||||
default: return htmlInput;
|
||||
}
|
||||
};
|
||||
|
||||
// Get filename for export based on active tab
|
||||
const getExportFilename = () => {
|
||||
switch (activeTab) {
|
||||
case 'html': return 'code.html';
|
||||
case 'css': return 'styles.css';
|
||||
case 'js': return 'script.js';
|
||||
default: return 'code.txt';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Tab Navigation */}
|
||||
<div className="flex border-b border-gray-200 dark:border-gray-700">
|
||||
{[
|
||||
{ id: 'html', label: 'HTML' },
|
||||
{ id: 'css', label: 'CSS' },
|
||||
{ id: 'js', label: 'JavaScript' }
|
||||
].map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === tab.id
|
||||
? 'border-blue-500 text-blue-600 dark:text-blue-400'
|
||||
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Action buttons above editor */}
|
||||
<div className="flex items-center justify-end space-x-2 p-2 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
onClick={() => handleSearch(getCurrentEditorRef())}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
|
||||
title="Search"
|
||||
>
|
||||
<Search className="w-3 h-3" />
|
||||
Search
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleCopy(getCurrentContent())}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
|
||||
title="Copy"
|
||||
>
|
||||
<Copy className="w-3 h-3" />
|
||||
Copy
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleExport(getCurrentContent(), getExportFilename())}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
|
||||
title="Export"
|
||||
>
|
||||
<Download className="w-3 h-3" />
|
||||
Export
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Code Editor */}
|
||||
<div className="flex-1">
|
||||
{activeTab === 'html' && (
|
||||
<CodeMirror
|
||||
ref={htmlEditorRef}
|
||||
value={htmlInput}
|
||||
onBeforeChange={(editor, data, value) => setHtmlInput(value)}
|
||||
options={{
|
||||
mode: 'xml',
|
||||
theme: 'material',
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
autoCloseTags: true,
|
||||
matchBrackets: true,
|
||||
indentUnit: 2,
|
||||
tabSize: 2,
|
||||
extraKeys: {
|
||||
'Ctrl-F': 'findPersistent',
|
||||
'Cmd-F': 'findPersistent'
|
||||
}
|
||||
}}
|
||||
className="h-full"
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'css' && (
|
||||
<CodeMirror
|
||||
ref={cssEditorRef}
|
||||
value={cssInput}
|
||||
onBeforeChange={(editor, data, value) => setCssInput(value)}
|
||||
options={{
|
||||
mode: 'css',
|
||||
theme: 'material',
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
autoCloseBrackets: true,
|
||||
matchBrackets: true,
|
||||
indentUnit: 2,
|
||||
tabSize: 2,
|
||||
extraKeys: {
|
||||
'Ctrl-F': 'findPersistent',
|
||||
'Cmd-F': 'findPersistent'
|
||||
}
|
||||
}}
|
||||
className="h-full"
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'js' && (
|
||||
<CodeMirror
|
||||
ref={jsEditorRef}
|
||||
value={jsInput}
|
||||
onBeforeChange={(editor, data, value) => setJsInput(value)}
|
||||
options={{
|
||||
mode: 'javascript',
|
||||
theme: 'material',
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
autoCloseBrackets: true,
|
||||
matchBrackets: true,
|
||||
indentUnit: 2,
|
||||
tabSize: 2,
|
||||
extraKeys: {
|
||||
'Ctrl-F': 'findPersistent',
|
||||
'Cmd-F': 'findPersistent'
|
||||
}
|
||||
}}
|
||||
className="h-full"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeInputs;
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Copy } from 'lucide-react';
|
||||
|
||||
const ElementEditor = ({ htmlInput, setHtmlInput, onClose, onSave, previewFrameRef, selectedElementInfo }) => {
|
||||
console.log('🔍 ELEMENT EDITOR: Received props:', { selectedElementInfo, previewFrameRef: !!previewFrameRef });
|
||||
@@ -10,14 +11,14 @@ const ElementEditor = ({ htmlInput, setHtmlInput, onClose, onSave, previewFrameR
|
||||
if (selectedElementInfo) {
|
||||
const elementInfo = {
|
||||
tagName: selectedElementInfo.tagName,
|
||||
innerText: selectedElementInfo.textContent || '',
|
||||
innerHTML: selectedElementInfo.innerHTML || 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
|
||||
// Add all other attributes (excluding cascadeId from display)
|
||||
Object.entries(selectedElementInfo.attributes).forEach(([name, value]) => {
|
||||
if (!['id', 'class', 'data-original', 'data-cascade-id'].includes(name)) {
|
||||
elementInfo[name] = value;
|
||||
@@ -68,7 +69,7 @@ const ElementEditor = ({ htmlInput, setHtmlInput, onClose, onSave, previewFrameR
|
||||
if (previewFrameRef?.current && edited?.cascadeId) {
|
||||
let success = false;
|
||||
|
||||
if (field === 'innerText') {
|
||||
if (field === 'innerHTML') {
|
||||
success = previewFrameRef.current.updateElementText(edited.cascadeId, value);
|
||||
} else if (field === 'className') {
|
||||
success = previewFrameRef.current.updateElementClass(edited.cascadeId, value);
|
||||
@@ -143,12 +144,48 @@ const ElementEditor = ({ htmlInput, setHtmlInput, onClose, onSave, previewFrameR
|
||||
|
||||
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'
|
||||
// Filter out system attributes for display (hide cascadeId)
|
||||
const otherAttributes = Object.keys(edited).filter(key =>
|
||||
!['tagName', 'innerHTML', 'id', 'className', 'cascadeId', 'isContainer'].includes(key)
|
||||
);
|
||||
|
||||
// Handle copy element functionality
|
||||
const handleCopyElement = async () => {
|
||||
if (!edited) return;
|
||||
|
||||
try {
|
||||
// Create a clean element string without cascade attributes
|
||||
const elementString = `<${edited.tagName}${edited.id ? ` id="${edited.id}"` : ''}${edited.className ? ` class="${edited.className}"` : ''}${otherAttributes.map(attr => ` ${attr}="${edited[attr]}"`).join('')}>${edited.innerHTML || ''}</${edited.tagName}>`;
|
||||
|
||||
await navigator.clipboard.writeText(elementString);
|
||||
console.log('✅ Element copied to clipboard');
|
||||
} catch (err) {
|
||||
console.error('❌ Failed to copy element:', err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||
Edit {edited.tagName.toUpperCase()}
|
||||
</h3>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={handleCopyElement}
|
||||
className="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors"
|
||||
title="Copy element"
|
||||
>
|
||||
<Copy className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">Tag Name</label>
|
||||
<textarea
|
||||
@@ -189,34 +226,26 @@ const ElementEditor = ({ htmlInput, setHtmlInput, onClose, onSave, previewFrameR
|
||||
/>
|
||||
</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 innerHTML field for all elements */}
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Inner HTML
|
||||
<span className="text-xs text-gray-500 ml-2">(HTML content inside element)</span>
|
||||
</label>
|
||||
<textarea
|
||||
ref={(el) => textareaRefs.current.innerHTML = el}
|
||||
value={edited.innerHTML || ''}
|
||||
onChange={(e) => {
|
||||
handleFieldChange('innerHTML', e.target.value);
|
||||
autoResizeTextarea(e.target);
|
||||
}}
|
||||
rows="3"
|
||||
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"
|
||||
placeholder="Enter HTML content or text..."
|
||||
/>
|
||||
</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>
|
||||
|
||||
90
src/pages/components/SimpleToolbar.js
Normal file
90
src/pages/components/SimpleToolbar.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import { Smartphone, Tablet, Monitor, RotateCcw, Maximize, Minimize, Eye, EyeOff } from 'lucide-react';
|
||||
|
||||
const SimpleToolbar = ({
|
||||
selectedDevice,
|
||||
setSelectedDevice,
|
||||
isFullscreen,
|
||||
onRefresh,
|
||||
onToggleFullscreen,
|
||||
onToggleSidebar,
|
||||
showSidebar,
|
||||
isSidebarExpanded
|
||||
}) => {
|
||||
const devices = [
|
||||
{ id: 'mobile', label: 'Mobile', icon: Smartphone },
|
||||
{ id: 'tablet', label: 'Tablet', icon: Tablet },
|
||||
{ id: 'desktop', label: 'Desktop', icon: Monitor }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Device selection */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300 mr-3">
|
||||
Device:
|
||||
</span>
|
||||
{devices.map((device) => {
|
||||
const Icon = device.icon;
|
||||
const isDisabled = isSidebarExpanded && device.id !== 'desktop';
|
||||
|
||||
return (
|
||||
<button
|
||||
key={device.id}
|
||||
onClick={() => !isDisabled && setSelectedDevice(device.id)}
|
||||
disabled={isDisabled}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
selectedDevice === device.id
|
||||
? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400'
|
||||
: isDisabled
|
||||
? 'text-gray-400 dark:text-gray-600 cursor-not-allowed'
|
||||
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
title={isDisabled ? 'Disabled when sidebar is expanded' : `Switch to ${device.label} view`}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
{device.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex items-center space-x-2">
|
||||
{/* Refresh button */}
|
||||
<button
|
||||
onClick={onRefresh}
|
||||
className="flex items-center gap-2 px-3 py-2 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors"
|
||||
title="Refresh preview"
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
Refresh
|
||||
</button>
|
||||
|
||||
{/* Sidebar toggle */}
|
||||
<button
|
||||
onClick={onToggleSidebar}
|
||||
className="flex items-center gap-2 px-3 py-2 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors"
|
||||
title={showSidebar ? 'Hide sidebar' : 'Show sidebar'}
|
||||
>
|
||||
{showSidebar ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||
{showSidebar ? 'Hide' : 'Show'} Sidebar
|
||||
</button>
|
||||
|
||||
{/* Fullscreen toggle */}
|
||||
<button
|
||||
onClick={onToggleFullscreen}
|
||||
className="flex items-center gap-2 px-3 py-2 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors"
|
||||
title={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
|
||||
>
|
||||
{isFullscreen ? <Minimize className="w-4 h-4" /> : <Maximize className="w-4 h-4" />}
|
||||
{isFullscreen ? 'Exit' : 'Enter'} Fullscreen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SimpleToolbar;
|
||||
Reference in New Issue
Block a user