feat: Object Editor Preview Mode & Mobile Optimizations
Major Enhancements: - Added Preview/Edit mode toggle to StructuredEditor component * Preview mode: Read-only view with full text visibility * Edit mode: Full editing capabilities with all controls * Toggle positioned below title, responsive on mobile * Works in both main ObjectEditor view and nested modals - Clickable nested data in Preview mode * JSON/serialized values are blue and clickable * Opens modal directly without switching to Edit mode * Hover effects and tooltips for better UX * No longer need edit mode just to explore structure Mobile Responsiveness Improvements: - Fixed all data load notices in ObjectEditor (URL, Paste, Open tabs) - Fixed all data load notices in TableEditor (URL, Paste, Open tabs) - Notices now stack vertically on mobile with proper spacing - Added break-words for long text, whitespace-nowrap for buttons - Dark mode colors added for better visibility Table Editor Fixes: - Fixed sticky header showing row underneath (top-[-1px]) - Made Export section header mobile responsive - Updated object modal footer layout: * Format info and properties combined on single line * Buttons moved to separate row below * Changed 'Apply Changes' to 'Save Changes' for consistency StructuredEditor Improvements: - Moved overflow-x handling from ObjectEditor to StructuredEditor - Now works consistently in main view and nested modals - Long strings scroll horizontally everywhere - 'Add Property' button hidden in Preview mode - Improved chevron colors for dark mode visibility Technical Changes: - StructuredEditor now manages its own editMode state - readOnly prop can still be passed from parent if needed - Proper conditional rendering for all UI elements - Consistent mobile-first responsive design patterns
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import React, { useRef, useEffect, useState, useCallback } from 'react';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { basicSetup } from 'codemirror';
|
||||
import { json } from '@codemirror/lang-json';
|
||||
import { sql } from '@codemirror/lang-sql';
|
||||
import { oneDark } from '@codemirror/theme-one-dark';
|
||||
import { Maximize2, Minimize2 } from 'lucide-react';
|
||||
|
||||
@@ -13,7 +14,8 @@ const CodeMirrorEditor = ({
|
||||
className = '',
|
||||
language = 'json',
|
||||
maxLines = 12,
|
||||
showToggle = true
|
||||
showToggle = true,
|
||||
cardRef = null // Reference to the card header for scroll target
|
||||
}) => {
|
||||
const editorRef = useRef(null);
|
||||
const viewRef = useRef(null);
|
||||
@@ -38,13 +40,26 @@ const CodeMirrorEditor = ({
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
// Detect if content is single-line (minified)
|
||||
const isSingleLine = (value || '').split('\n').length === 1;
|
||||
|
||||
// Initialize editor only once
|
||||
useEffect(() => {
|
||||
if (!editorRef.current || viewRef.current) return;
|
||||
|
||||
// Language extension
|
||||
let langExtension = [];
|
||||
if (language === 'json') {
|
||||
langExtension = [json()];
|
||||
} else if (language === 'sql') {
|
||||
langExtension = [sql()];
|
||||
}
|
||||
|
||||
const extensions = [
|
||||
basicSetup,
|
||||
language === 'json' ? json() : [],
|
||||
...langExtension,
|
||||
// Enable line wrapping for single-line content
|
||||
...(isSingleLine ? [EditorView.lineWrapping] : []),
|
||||
EditorView.theme({
|
||||
'&': {
|
||||
fontSize: '14px',
|
||||
@@ -58,6 +73,13 @@ const CodeMirrorEditor = ({
|
||||
},
|
||||
'.cm-editor': {
|
||||
borderRadius: '6px',
|
||||
},
|
||||
'.cm-scroller': {
|
||||
overflowY: 'auto',
|
||||
height: '100%',
|
||||
},
|
||||
'.cm-line': {
|
||||
wordBreak: isSingleLine ? 'break-word' : 'normal',
|
||||
}
|
||||
}),
|
||||
EditorView.updateListener.of((update) => {
|
||||
@@ -80,6 +102,25 @@ const CodeMirrorEditor = ({
|
||||
|
||||
viewRef.current = view;
|
||||
|
||||
// Apply styles immediately after editor creation
|
||||
setTimeout(() => {
|
||||
const editorElement = editorRef.current?.querySelector('.cm-editor');
|
||||
const scrollerElement = editorRef.current?.querySelector('.cm-scroller');
|
||||
|
||||
if (editorElement) {
|
||||
editorElement.style.height = '350px';
|
||||
editorElement.style.maxHeight = '350px';
|
||||
}
|
||||
|
||||
if (scrollerElement) {
|
||||
scrollerElement.style.overflowY = 'auto';
|
||||
scrollerElement.style.overflowX = isSingleLine ? 'hidden' : 'auto';
|
||||
scrollerElement.style.height = '100%';
|
||||
}
|
||||
|
||||
// No manual wrapping needed - EditorView.lineWrapping handles it
|
||||
}, 0);
|
||||
|
||||
return () => {
|
||||
if (viewRef.current) {
|
||||
viewRef.current.destroy();
|
||||
@@ -87,13 +128,15 @@ const CodeMirrorEditor = ({
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isDark]); // Only recreate on theme change
|
||||
}, [isDark, isSingleLine]); // Recreate when theme or line count changes
|
||||
|
||||
// Handle height changes without recreating editor
|
||||
useEffect(() => {
|
||||
if (!viewRef.current) return;
|
||||
// Apply overflow and height styles
|
||||
const applyEditorStyles = useCallback(() => {
|
||||
if (!viewRef.current || !editorRef.current) return;
|
||||
|
||||
const editorElement = editorRef.current.querySelector('.cm-editor');
|
||||
const scrollerElement = editorRef.current.querySelector('.cm-scroller');
|
||||
|
||||
const editorElement = editorRef.current?.querySelector('.cm-editor');
|
||||
if (editorElement) {
|
||||
if (isExpanded) {
|
||||
editorElement.style.height = 'auto';
|
||||
@@ -103,8 +146,19 @@ const CodeMirrorEditor = ({
|
||||
editorElement.style.maxHeight = '350px';
|
||||
}
|
||||
}
|
||||
|
||||
// Handle scrolling
|
||||
if (scrollerElement) {
|
||||
scrollerElement.style.overflowY = isExpanded ? 'visible' : 'auto';
|
||||
scrollerElement.style.height = '100%';
|
||||
}
|
||||
}, [isExpanded]);
|
||||
|
||||
// Apply styles on mount, expand/collapse, and content changes
|
||||
useEffect(() => {
|
||||
applyEditorStyles();
|
||||
}, [applyEditorStyles]);
|
||||
|
||||
// Update content when value changes externally
|
||||
useEffect(() => {
|
||||
if (viewRef.current && value !== viewRef.current.state.doc.toString()) {
|
||||
@@ -116,8 +170,11 @@ const CodeMirrorEditor = ({
|
||||
}
|
||||
});
|
||||
viewRef.current.dispatch(transaction);
|
||||
|
||||
// Re-apply styles after content change (e.g., tab switch)
|
||||
setTimeout(() => applyEditorStyles(), 10);
|
||||
}
|
||||
}, [value]);
|
||||
}, [value, applyEditorStyles]);
|
||||
|
||||
return (
|
||||
<div className={`relative ${className}`}>
|
||||
@@ -132,7 +189,12 @@ const CodeMirrorEditor = ({
|
||||
onClick={() => {
|
||||
setIsExpanded(!isExpanded);
|
||||
setTimeout(() => {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
// Scroll to card header if cardRef is provided, otherwise scroll to top
|
||||
if (cardRef?.current) {
|
||||
cardRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
} else {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
}, 50);
|
||||
}}
|
||||
className="absolute bottom-2 right-2 p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 bg-white dark:bg-gray-800 rounded border border-gray-300 dark:border-gray-600 shadow-sm z-10"
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Plus, Minus, ChevronDown, ChevronRight, Type, Hash, ToggleLeft, List, Braces, Edit3, X } from 'lucide-react';
|
||||
import { Plus, Minus, ChevronDown, ChevronRight, Type, Hash, ToggleLeft, List, Braces, Edit3, X, Eye, Pencil } from 'lucide-react';
|
||||
|
||||
const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
||||
const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyProp = false }) => {
|
||||
const [data, setData] = useState(initialData);
|
||||
const [expandedNodes, setExpandedNodes] = useState(new Set(['root']));
|
||||
const [fieldTypes, setFieldTypes] = useState({}); // Track intended types for fields
|
||||
const isInternalUpdate = useRef(false);
|
||||
const [nestedEditModal, setNestedEditModal] = useState(null); // { path, value, type: 'json' | 'serialized' }
|
||||
const [nestedData, setNestedData] = useState(null);
|
||||
const [editMode, setEditMode] = useState(false); // Internal edit mode state
|
||||
|
||||
// Use internal editMode if readOnlyProp is not explicitly set, otherwise use prop
|
||||
const readOnly = readOnlyProp !== false ? readOnlyProp : !editMode;
|
||||
|
||||
// Update internal data when initialData prop changes (but not from internal updates)
|
||||
useEffect(() => {
|
||||
@@ -533,7 +537,7 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
||||
{canExpand && (
|
||||
<button
|
||||
onClick={() => toggleNode(path)}
|
||||
className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
|
||||
className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
@@ -558,74 +562,106 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
||||
// Object properties: icon + editable key + colon (compact)
|
||||
<>
|
||||
{getTypeIcon(value)}
|
||||
<input
|
||||
type="text"
|
||||
defaultValue={key}
|
||||
onBlur={(e) => {
|
||||
const newKey = e.target.value.trim();
|
||||
if (newKey && newKey !== key) {
|
||||
renameKey(key, newKey, path);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.target.blur(); // Trigger blur to save changes
|
||||
}
|
||||
}}
|
||||
className="px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 min-w-0"
|
||||
placeholder="Property name"
|
||||
style={{width: '120px'}} // Fixed width for consistency
|
||||
/>
|
||||
<span className="text-gray-500 hidden sm:inline">:</span>
|
||||
{readOnly ? (
|
||||
<span className="px-2 py-1 text-sm text-gray-900 dark:text-gray-100 font-mono">
|
||||
{key}
|
||||
</span>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
defaultValue={key}
|
||||
onBlur={(e) => {
|
||||
const newKey = e.target.value.trim();
|
||||
if (newKey && newKey !== key) {
|
||||
renameKey(key, newKey, path);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.target.blur(); // Trigger blur to save changes
|
||||
}
|
||||
}}
|
||||
className="px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 min-w-0"
|
||||
placeholder="Property name"
|
||||
style={{width: '120px'}} // Fixed width for consistency
|
||||
/>
|
||||
)}
|
||||
<span className="text-gray-500 inline">:</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!canExpand ? (
|
||||
typeof value === 'boolean' ? (
|
||||
<div className="flex-1 flex items-center space-x-2">
|
||||
<button
|
||||
onClick={() => updateValue((!value).toString(), path)}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 ${
|
||||
value ? 'bg-blue-600' : 'bg-gray-200 dark:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
value ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400 font-mono">
|
||||
{value.toString()}
|
||||
</span>
|
||||
{readOnly ? (
|
||||
<span className="text-sm text-gray-900 dark:text-gray-100 font-mono">
|
||||
{value.toString()}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={() => updateValue((!value).toString(), path)}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 ${
|
||||
value ? 'bg-blue-600' : 'bg-gray-200 dark:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
value ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400 font-mono">
|
||||
{value.toString()}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 flex items-center gap-2">
|
||||
{(fieldTypes[path] === 'longtext' || (typeof value === 'string' && value.includes('\n'))) ? (
|
||||
<textarea
|
||||
value={getDisplayValue(value)}
|
||||
onChange={(e) => updateValue(e.target.value, path)}
|
||||
className="flex-1 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 min-w-0 resize-y items"
|
||||
placeholder="Long text value"
|
||||
rows={3}
|
||||
/>
|
||||
{readOnly ? (
|
||||
typeof value === 'string' && detectNestedData(value) ? (
|
||||
<span
|
||||
onClick={() => openNestedEditor(value, path)}
|
||||
className="px-2 py-1 text-sm text-blue-600 dark:text-blue-400 font-mono break-all cursor-pointer hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded transition-colors"
|
||||
title={`Click to view nested ${detectNestedData(value).type} data`}
|
||||
>
|
||||
{getDisplayValue(value)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="px-2 py-1 text-sm text-gray-900 dark:text-gray-100 font-mono break-all">
|
||||
{getDisplayValue(value)}
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
value={getDisplayValue(value)}
|
||||
onChange={(e) => updateValue(e.target.value, path)}
|
||||
className="flex-1 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 min-w-0"
|
||||
placeholder="Value"
|
||||
/>
|
||||
)}
|
||||
{typeof value === 'string' && detectNestedData(value) && (
|
||||
<button
|
||||
onClick={() => openNestedEditor(value, path)}
|
||||
className="p-1 text-blue-600 hover:bg-blue-100 dark:hover:bg-blue-900 rounded flex-shrink-0"
|
||||
title={`Edit nested ${detectNestedData(value).type} data`}
|
||||
>
|
||||
<Edit3 className="h-4 w-4" />
|
||||
</button>
|
||||
<>
|
||||
{(fieldTypes[path] === 'longtext' || (typeof value === 'string' && value.includes('\n'))) ? (
|
||||
<textarea
|
||||
value={getDisplayValue(value)}
|
||||
onChange={(e) => updateValue(e.target.value, path)}
|
||||
className="flex-1 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 min-w-0 resize-y items"
|
||||
placeholder="Long text value"
|
||||
rows={3}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
value={getDisplayValue(value)}
|
||||
onChange={(e) => updateValue(e.target.value, path)}
|
||||
className="flex-1 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 min-w-0"
|
||||
placeholder="Value"
|
||||
/>
|
||||
)}
|
||||
{typeof value === 'string' && detectNestedData(value) && (
|
||||
<button
|
||||
onClick={() => openNestedEditor(value, path)}
|
||||
className="p-1 text-blue-600 hover:bg-blue-100 dark:hover:bg-blue-900 rounded flex-shrink-0"
|
||||
title={`Edit nested ${detectNestedData(value).type} data`}
|
||||
>
|
||||
<Edit3 className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
@@ -635,38 +671,40 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div className="flex items-center space-x-2 sm:space-x-2">
|
||||
<select
|
||||
value={
|
||||
fieldTypes[path] || (
|
||||
value === null ? 'null' :
|
||||
value === undefined ? 'string' :
|
||||
typeof value === 'string' ? (value.includes('\n') ? 'longtext' : 'string') :
|
||||
typeof value === 'number' ? 'number' :
|
||||
typeof value === 'boolean' ? 'boolean' :
|
||||
Array.isArray(value) ? 'array' : 'object'
|
||||
)
|
||||
}
|
||||
onChange={(e) => changeType(e.target.value, path)}
|
||||
className="px-2 py-1 text-xs border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 min-w-0"
|
||||
>
|
||||
<option value="string">String</option>
|
||||
<option value="longtext">Long Text</option>
|
||||
<option value="number">Number</option>
|
||||
<option value="boolean">Boolean</option>
|
||||
<option value="array">Array</option>
|
||||
<option value="object">Object</option>
|
||||
<option value="null">Null</option>
|
||||
</select>
|
||||
|
||||
<button
|
||||
onClick={() => removeProperty(key, parentPath)}
|
||||
className="p-1 text-red-600 hover:bg-red-100 dark:hover:bg-red-900 rounded flex-shrink-0"
|
||||
title="Remove property"
|
||||
>
|
||||
<Minus className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
{!readOnly && (
|
||||
<div className="flex items-center space-x-2 sm:space-x-2">
|
||||
<select
|
||||
value={
|
||||
fieldTypes[path] || (
|
||||
value === null ? 'null' :
|
||||
value === undefined ? 'string' :
|
||||
typeof value === 'string' ? (value.includes('\n') ? 'longtext' : 'string') :
|
||||
typeof value === 'number' ? 'number' :
|
||||
typeof value === 'boolean' ? 'boolean' :
|
||||
Array.isArray(value) ? 'array' : 'object'
|
||||
)
|
||||
}
|
||||
onChange={(e) => changeType(e.target.value, path)}
|
||||
className="px-2 py-1 text-xs border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 min-w-0"
|
||||
>
|
||||
<option value="string">String</option>
|
||||
<option value="longtext">Long Text</option>
|
||||
<option value="number">Number</option>
|
||||
<option value="boolean">Boolean</option>
|
||||
<option value="array">Array</option>
|
||||
<option value="object">Object</option>
|
||||
<option value="null">Null</option>
|
||||
</select>
|
||||
|
||||
<button
|
||||
onClick={() => removeProperty(key, parentPath)}
|
||||
className="p-1 text-red-600 hover:bg-red-100 dark:hover:bg-red-900 rounded flex-shrink-0"
|
||||
title="Remove property"
|
||||
>
|
||||
<Minus className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -677,26 +715,30 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
||||
{value.map((item, index) =>
|
||||
renderValue(item, index.toString(), `${path}.${index}`, path)
|
||||
)}
|
||||
<button
|
||||
onClick={() => addArrayItem(value, path)}
|
||||
className="flex items-center space-x-1 px-2 py-1 text-sm text-blue-600 hover:bg-blue-100 dark:hover:bg-blue-900 rounded"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
<span>Add Item</span>
|
||||
</button>
|
||||
{!readOnly && (
|
||||
<button
|
||||
onClick={() => addArrayItem(value, path)}
|
||||
className="flex items-center space-x-1 px-2 py-1 text-sm text-blue-600 hover:bg-blue-100 dark:hover:bg-blue-900 rounded"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
<span>Add Item</span>
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{Object.entries(value).map(([k, v]) =>
|
||||
renderValue(v, k, `${path}.${k}`, path)
|
||||
)}
|
||||
<button
|
||||
onClick={() => addProperty(value, path)}
|
||||
className="flex items-center space-x-1 px-2 py-1 text-sm text-blue-600 hover:bg-blue-100 dark:hover:bg-blue-900 rounded"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
<span>Add Property</span>
|
||||
</button>
|
||||
{!readOnly && (
|
||||
<button
|
||||
onClick={() => addProperty(value, path)}
|
||||
className="flex items-center space-x-1 px-2 py-1 text-sm text-blue-600 hover:bg-blue-100 dark:hover:bg-blue-900 rounded"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
<span>Add Property</span>
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -708,29 +750,65 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
||||
return (
|
||||
<div className="min-h-96 w-full">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">Structured Data Editor</h3>
|
||||
<div className="flex flex-col gap-3 mb-3">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">Structured Data Editor</h3>
|
||||
|
||||
{/* Mode Toggle - Below title on mobile, inline on desktop */}
|
||||
{readOnlyProp === false && (
|
||||
<div className="flex border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden bg-white dark:bg-gray-800 shadow-sm w-fit">
|
||||
<button
|
||||
onClick={() => setEditMode(false)}
|
||||
className={`flex items-center gap-2 px-3 py-1.5 text-xs font-medium transition-colors ${
|
||||
!editMode
|
||||
? 'bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<Eye className="h-3.5 w-3.5" />
|
||||
<span>Preview</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setEditMode(true)}
|
||||
className={`flex items-center gap-2 px-3 py-1.5 text-xs font-medium transition-colors border-l border-gray-200 dark:border-gray-700 ${
|
||||
editMode
|
||||
? 'bg-orange-50 dark:bg-orange-900/20 text-orange-700 dark:text-orange-300'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
<span>Edit</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-hidden">
|
||||
{Object.keys(data).length === 0 ? (
|
||||
<div className="text-center text-gray-500 dark:text-gray-400 py-8">
|
||||
<Braces className="h-12 w-12 mx-auto mb-2 opacity-50" />
|
||||
<p>No properties yet. Click "Add Property" to start building your data structure.</p>
|
||||
<div className="w-full overflow-hidden">
|
||||
<div className="w-full overflow-x-auto">
|
||||
<div className="min-w-max">
|
||||
{Object.keys(data).length === 0 ? (
|
||||
<div className="text-center text-gray-500 dark:text-gray-400 py-8">
|
||||
<Braces className="h-12 w-12 mx-auto mb-2 opacity-50" />
|
||||
<p>No properties yet. Click "Add Property" to start building your data structure.</p>
|
||||
</div>
|
||||
) : (
|
||||
Object.entries(data).map(([key, value]) =>
|
||||
renderValue(value, key, `root.${key}`, 'root')
|
||||
)
|
||||
)}
|
||||
|
||||
{/* Root level Add Property button */}
|
||||
{!readOnly && (
|
||||
<button
|
||||
onClick={() => addProperty(data, 'root')}
|
||||
className="flex items-center space-x-1 px-2 py-1 text-sm text-blue-600 hover:bg-blue-100 dark:hover:bg-blue-900 rounded"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
<span>Add Property</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
Object.entries(data).map(([key, value]) =>
|
||||
renderValue(value, key, `root.${key}`, 'root')
|
||||
)
|
||||
)}
|
||||
|
||||
{/* Root level Add Property button */}
|
||||
<button
|
||||
onClick={() => addProperty(data, 'root')}
|
||||
className="flex items-center space-x-1 px-2 py-1 text-sm text-blue-600 hover:bg-blue-100 dark:hover:bg-blue-900 rounded"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
<span>Add Property</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Nested Data Editor Modal */}
|
||||
@@ -749,7 +827,7 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
||||
</div>
|
||||
<button
|
||||
onClick={closeNestedEditor}
|
||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
|
||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded self-start"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user