Major improvements to Object Editor, Table Editor, and Invoice Editor: ## UX Enhancements: - Made export sections collapsible across all editors to reduce page height - Added comprehensive, collapsible usage tips with eye-catching design - Implemented consistent input method patterns (file auto-load, inline URL buttons) - Paste sections now collapse after successful parsing with data summaries ## Data Loss Prevention: - Added confirmation modals when switching input methods with existing data - Amber-themed warning design with specific data summaries - Suggests saving before proceeding with destructive actions - Prevents accidental data loss across all editor tools ## Consistency Improvements: - Standardized file input styling with 'tool-input' class - URL fetch buttons now inline (not below input) across all editors - Parse buttons positioned consistently on bottom-right - Auto-load behavior for file inputs matching across editors ## Bug Fixes: - Fixed Table Editor cell text overflow with proper truncation - Fixed Object Editor file input to auto-load content - Removed unnecessary parse buttons and checkboxes - Fixed Invoice Editor URL form layout ## Documentation: - Created EDITOR_TOOL_GUIDE.md with comprehensive patterns - Created EDITOR_CHECKLIST.md for quick reference - Created PROJECT_ROADMAP.md with future plans - Created TODO.md with detailed task lists - Documented data loss prevention patterns - Added code examples and best practices All editors now follow consistent UX patterns with improved user experience and data protection.
23 KiB
23 KiB
Editor Tool Building Guide
This guide ensures consistency across all editor tools in the Developer Tools application. Follow these patterns when creating new editor tools.
Table of Contents
- Input Section Patterns
- Main Editor Section
- Export/Output Section
- Usage Tips Section
- State Management
- Error Handling
- Complete Example
Input Section Patterns
Required Tabs
All editor tools must have these 4 input tabs:
- Create New - Start empty or load sample data
- URL - Fetch data from API endpoints
- Paste - Paste data directly
- Open - Upload files
Tab Structure
<div className="flex border-b border-gray-200 dark:border-gray-700">
<button onClick={() => handleTabChange('create')} className={...}>
<Plus className="h-4 w-4" />
Create New
</button>
<button onClick={() => handleTabChange('url')} className={...}>
<Globe className="h-4 w-4" />
URL
</button>
<button onClick={() => handleTabChange('paste')} className={...}>
<FileText className="h-4 w-4" />
Paste
</button>
<button onClick={() => handleTabChange('open')} className={...}>
<Upload className="h-4 w-4" />
Open
</button>
</div>
Create New Tab
{activeTab === 'create' && (
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Start Empty Button */}
<button onClick={handleStartEmpty} className="flex flex-col items-center p-6 border-2 border-dashed...">
<Plus className="h-8 w-8..." />
<span className="font-medium...">Start Empty</span>
<span className="text-xs...">Create a blank structure</span>
</button>
{/* Load Sample Button */}
<button onClick={handleLoadSample} className="flex flex-col items-center p-6 border-2 border-dashed...">
<FileText className="h-8 w-8..." />
<span className="font-medium...">Load Sample</span>
<span className="text-xs...">Start with example data</span>
</button>
</div>
{/* Tip */}
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-3">
<p className="text-xs text-blue-700 dark:text-blue-300">
💡 <strong>Tip:</strong> You can always import data later using the URL, Paste, or Open tabs.
</p>
</div>
</div>
)}
URL Tab (CONSISTENT LAYOUT)
IMPORTANT: Button must be INLINE on the right, not below!
{activeTab === 'url' && (
<div className="space-y-3">
<div className="flex gap-2">
<div className="relative flex-1">
<input
type="url"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="https://api.telegram.org/bot<token>/getMe"
className="tool-input w-full"
onKeyPress={(e) => e.key === 'Enter' && handleFetchData()}
/>
</div>
<button
onClick={handleFetchData}
disabled={isLoading || !url.trim()}
className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white font-medium px-4 py-2 rounded-md transition-colors flex items-center whitespace-nowrap"
>
{isLoading ? 'Fetching...' : 'Fetch Data'}
</button>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400">
Enter any URL that returns JSON data. Examples: Telegram Bot API, JSONPlaceholder, GitHub API, etc.
</p>
</div>
)}
Paste Tab (WITH COLLAPSE)
{activeTab === 'paste' && (
pasteCollapsed ? (
// Collapsed State
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<div className="flex items-center justify-between">
<span className="text-sm text-green-700 dark:text-green-300">
✓ Data loaded: {pasteDataSummary.format} ({pasteDataSummary.size.toLocaleString()} chars)
</span>
<button
onClick={() => setPasteCollapsed(false)}
className="text-sm text-blue-600 hover:underline"
>
Edit Input ▼
</button>
</div>
</div>
) : (
// Expanded State
<div className="space-y-3">
<div>
<CodeMirrorEditor
value={inputText}
onChange={setInputText}
language="json"
placeholder="Paste your data here..."
maxLines={12}
showToggle={true}
className="w-full"
/>
</div>
{error && (
<div className="p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<p className="text-sm text-red-600 dark:text-red-400">
<strong>Invalid Data:</strong> {error}
</p>
</div>
)}
<div className="flex items-center justify-between flex-shrink-0">
<div className="text-sm text-gray-600 dark:text-gray-400">
Auto-detects format
</div>
<button
onClick={handleParseData}
disabled={!inputValid || !inputText.trim()}
className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed text-white font-medium px-4 py-2 rounded-md transition-colors flex-shrink-0"
>
Parse Data
</button>
</div>
</div>
)
)}
Open Tab (CONSISTENT WITH TABLE EDITOR)
IMPORTANT: Use tool-input class and auto-load on file selection!
{activeTab === 'open' && (
<div className="space-y-3">
<input
ref={fileInputRef}
type="file"
accept=".json,.txt"
onChange={handleFileImport}
className="tool-input"
/>
<div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-md p-3">
<p className="text-xs text-green-700 dark:text-green-300">
🔒 <strong>Privacy:</strong> Your data stays in your browser. We don't store or upload anything - just help you open, edit, and export your files locally.
</p>
</div>
</div>
)}
Parse Button Behavior
- Valid data: Collapse input, show summary, display editor
- Invalid data: Show error, keep input expanded, hide editor
- Button disabled: When no data or invalid format
Main Editor Section
Structure
{(activeTab !== 'create' || createNewCompleted) && (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden mb-6">
{/* Editor Header */}
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<Edit3 className="h-5 w-5 text-blue-600 dark:text-blue-400" />
Your Editor Name
</h3>
{/* Optional: Settings or Actions */}
</div>
</div>
{/* Editor Content */}
<div className="p-4">
{/* Your editor implementation */}
</div>
</div>
)}
Fullscreen Support (Optional)
If your editor supports fullscreen:
const [isFullscreen, setIsFullscreen] = useState(false);
<div className={`... ${
isFullscreen
? "fixed inset-0 z-[99999] rounded-none border-0 shadow-none overflow-hidden !m-0"
: "..."
}`}
style={isFullscreen ? { marginTop: "0 !important" } : {}}
>
Export/Output Section
Collapsible Export (REQUIRED)
{data && (
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden mt-6">
{/* Collapsible Header */}
<div
onClick={() => setExportExpanded(!exportExpanded)}
className="px-4 py-3 border-b border-gray-200 dark:border-gray-700 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<Download className="h-5 w-5 text-green-600 dark:text-green-400" />
Export Results
{exportExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</h3>
<div className="text-sm text-gray-600 dark:text-gray-400">
{/* Summary info */}
</div>
</div>
</div>
{/* Export Content - Collapsible */}
{exportExpanded && (
<div>
{/* Export tabs and content */}
</div>
)}
</div>
)}
Export Tabs Pattern
<div className="flex border-b border-gray-200 dark:border-gray-700">
<button
onClick={() => setExportTab('json')}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
exportTab === 'json'
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
}`}
>
<Braces className="h-4 w-4" />
JSON
</button>
{/* More export format tabs */}
</div>
Usage Tips Section
Collapsible Usage Tips (REQUIRED)
{/* Usage Tips */}
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md overflow-hidden mt-6">
<div
onClick={() => setUsageTipsExpanded(!usageTipsExpanded)}
className="px-4 py-3 cursor-pointer hover:bg-blue-100 dark:hover:bg-blue-900/30 transition-colors flex items-center justify-between"
>
<h4 className="text-blue-800 dark:text-blue-200 font-medium flex items-center gap-2">
💡 Usage Tips
</h4>
{usageTipsExpanded ? <ChevronUp className="h-4 w-4 text-blue-600 dark:text-blue-400" /> : <ChevronDown className="h-4 w-4 text-blue-600 dark:text-blue-400" />}
</div>
{usageTipsExpanded && (
<div className="px-4 pb-4 text-blue-700 dark:text-blue-300 text-sm space-y-3">
<div>
<p className="font-medium mb-1">📝 Input Methods:</p>
<ul className="list-disc list-inside space-y-1 ml-2">
<li><strong>Create New:</strong> Description</li>
<li><strong>URL Import:</strong> Description</li>
<li><strong>Paste Data:</strong> Description</li>
<li><strong>Open Files:</strong> Description</li>
</ul>
</div>
<div>
<p className="font-medium mb-1">✏️ Editing Features:</p>
<ul className="list-disc list-inside space-y-1 ml-2">
{/* Your editor-specific features */}
</ul>
</div>
<div>
<p className="font-medium mb-1">📤 Export Options:</p>
<ul className="list-disc list-inside space-y-1 ml-2">
{/* Your export formats */}
</ul>
</div>
<div>
<p className="font-medium mb-1">💾 Data Privacy:</p>
<ul className="list-disc list-inside space-y-1 ml-2">
<li><strong>Local Processing:</strong> All data stays in your browser</li>
<li><strong>No Upload:</strong> We don't store or transmit your data</li>
<li><strong>Secure:</strong> Your information remains private</li>
</ul>
</div>
</div>
)}
</div>
Data Loss Prevention (CRITICAL!)
Confirmation Modal for Tab Changes
IMPORTANT: Always confirm before clearing user data when switching input methods!
When to Show Confirmation
- User has entered/loaded data
- User tries to switch to a different input tab
- User tries to use Create New buttons (Start Empty/Load Sample)
Implementation Pattern
1. Check if User Has Data
const hasUserData = () => {
// Check if there's meaningful data
return Object.keys(editorData).length > 0;
};
const hasModifiedData = () => {
// Check if data has been modified from initial state
// Return false for empty or sample data
// Return true for user-entered data
};
2. Handle Tab Change with Confirmation
const handleTabChange = (newTab) => {
if (hasModifiedData() && activeTab !== newTab) {
setPendingTabChange(newTab);
setShowInputChangeModal(true);
} else {
setActiveTab(newTab);
}
};
3. Confirmation Modal Component
const InputChangeConfirmationModal = ({
editorData,
currentMethod,
newMethod,
onConfirm,
onCancel
}) => {
const getMethodName = (method) => {
switch (method) {
case 'create': return 'Create New';
case 'create_empty': return 'Start Empty';
case 'create_sample': return 'Load Sample';
case 'url': return 'URL Import';
case 'paste': return 'Paste Data';
case 'open': return 'File Upload';
default: return method;
}
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full overflow-hidden">
{/* Header - Amber Warning */}
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700 bg-amber-50 dark:bg-amber-900/20">
<div className="flex items-center gap-3">
<div className="flex-shrink-0">
<AlertTriangle className="h-6 w-6 text-amber-600 dark:text-amber-400" />
</div>
<div>
<h3 className="text-lg font-semibold text-amber-800 dark:text-amber-200">
Confirm Action
</h3>
<p className="text-sm text-amber-700 dark:text-amber-300">
{newMethod === 'create_empty' || newMethod === 'create_sample'
? `Using ${getMethodName(newMethod)} will clear your current data.`
: `Switching from ${getMethodName(currentMethod)} to ${getMethodName(newMethod)} will clear your current data.`
}
</p>
</div>
</div>
</div>
{/* Body - Show Current Data Summary */}
<div className="px-6 py-4">
<div className="space-y-3">
<p className="text-sm text-gray-600 dark:text-gray-400">
You currently have:
</p>
<ul className="text-sm text-gray-700 dark:text-gray-300 space-y-1 ml-4">
{/* List current data items */}
<li>• [Data summary item 1]</li>
<li>• [Data summary item 2]</li>
<li>• [Data summary item 3]</li>
</ul>
{/* Blue Tip Box */}
<div className="mt-4 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
<p className="text-xs text-blue-700 dark:text-blue-300">
<strong>Tip:</strong> Consider downloading your current data as JSON before proceeding to save your work.
</p>
</div>
</div>
</div>
{/* Footer - Action Buttons */}
<div className="px-6 py-4 bg-gray-50 dark:bg-gray-700/50 flex justify-end gap-3">
<button
onClick={onCancel}
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded-md hover:bg-gray-50 dark:hover:bg-gray-500 transition-colors"
>
Cancel
</button>
<button
onClick={onConfirm}
className="px-4 py-2 text-sm font-medium text-white bg-amber-600 hover:bg-amber-700 rounded-md transition-colors flex items-center gap-2"
>
<AlertTriangle className="h-4 w-4" />
Switch & Clear Data
</button>
</div>
</div>
</div>
);
};
4. Usage in Component
{/* Confirmation Modal */}
{showInputChangeModal && (
<InputChangeConfirmationModal
editorData={editorData}
currentMethod={activeTab}
newMethod={pendingTabChange}
onConfirm={() => {
clearAllData();
setActiveTab(pendingTabChange);
setShowInputChangeModal(false);
setPendingTabChange(null);
}}
onCancel={() => {
setShowInputChangeModal(false);
setPendingTabChange(null);
}}
/>
)}
Visual Design
- Header: Amber background with AlertTriangle icon
- Body: White/dark background with data summary
- Tip Box: Blue background with save suggestion
- Footer: Gray background with Cancel and Warning buttons
- Warning Button: Amber color with AlertTriangle icon
Key Points
- Always show when user has meaningful data
- List specific data user will lose (not generic message)
- Suggest saving before proceeding (blue tip box)
- Clear button text - "Switch & Clear Data" (not ambiguous)
- z-50 to appear above everything
- Amber theme for warning (not red, not blue)
State Management
Required State Variables
// Tab management
const [activeTab, setActiveTab] = useState('create');
const [createNewCompleted, setCreateNewCompleted] = useState(false);
// Input data
const [inputText, setInputText] = useState('');
const [url, setUrl] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
// Collapse states
const [pasteCollapsed, setPasteCollapsed] = useState(false);
const [pasteDataSummary, setPasteDataSummary] = useState(null);
const [exportExpanded, setExportExpanded] = useState(false);
const [usageTipsExpanded, setUsageTipsExpanded] = useState(false);
// Confirmation modals
const [showInputChangeModal, setShowInputChangeModal] = useState(false);
const [pendingTabChange, setPendingTabChange] = useState(null);
// Refs
const fileInputRef = useRef(null);
// Your editor-specific state
const [editorData, setEditorData] = useState(null);
Tab Change with Confirmation
const handleTabChange = (newTab) => {
if (hasModifiedData() && activeTab !== newTab) {
setPendingTabChange(newTab);
setShowInputChangeModal(true);
} else {
setActiveTab(newTab);
}
};
const confirmInputChange = () => {
clearAllData();
setActiveTab(pendingTabChange);
setShowInputChangeModal(false);
setPendingTabChange(null);
};
Error Handling
Parse Data with Error Handling
const handleParseData = () => {
try {
const parsed = parseYourData(inputText);
if (parsed.valid) {
setEditorData(parsed.data);
setError('');
setCreateNewCompleted(true);
setPasteDataSummary({
format: parsed.format,
size: inputText.length,
items: parsed.data.length
});
setPasteCollapsed(true);
} else {
// Show error, keep input expanded
setError(parsed.error || 'Invalid data format');
setPasteCollapsed(false);
}
} catch (err) {
setError(err.message || 'Failed to parse data');
setPasteCollapsed(false);
}
};
Error Display
{error && (
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
<div className="flex items-center gap-2">
<AlertTriangle className="h-5 w-5 text-red-600 dark:text-red-400" />
<p className="text-red-700 dark:text-red-300">{error}</p>
</div>
</div>
)}
Complete Example
See existing editors for complete implementations:
- Object Editor (
/src/pages/ObjectEditor.js) - Best for JSON/structured data - Table Editor (
/src/pages/TableEditor.js) - Best for tabular data - Invoice Editor (
/src/pages/InvoiceEditor.js) - Best for form-based editors
Checklist for New Editor Tools
Input Section
- All 4 tabs implemented (Create New, URL, Paste, Open)
- URL tab has inline button (not below)
- Open tab uses
tool-inputclass - Open tab auto-loads on file selection
- Paste tab has parse button on bottom-right
- Paste tab collapses after successful parse
- Error handling shows errors and keeps input expanded
Main Editor
- Only shows after data is loaded
- Has proper header with icon
- Implements your editor functionality
- Optional: Fullscreen support with proper z-index
Export Section
- Collapsible by default
- Header shows summary info
- Chevron icon indicates state
- Hover effect on header
- Export tabs if multiple formats
Usage Tips
- Collapsible by default
- 💡 emoji in header
- Comprehensive tips organized by category
- Includes Input Methods, Editing Features, Export Options, Data Privacy
State & Behavior
- Tab change confirmation when data exists
- Proper error handling
- Loading states for async operations
- Dark mode support
Styling
- Uses Tailwind CSS classes
- Consistent with other editors
- Responsive design
- Proper spacing and layout
Common Patterns
Button Styling
// Primary action button
className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed text-white font-medium px-4 py-2 rounded-md transition-colors flex-shrink-0"
// Secondary button
className="bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 px-4 py-2 rounded-md transition-colors"
Input Styling
// Standard input
className="tool-input w-full"
// Or explicit:
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
Card/Section Styling
className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden"
Import Requirements
Required Icons (from lucide-react)
import {
Plus, // Create New
Globe, // URL
FileText, // Paste
Upload, // Open
Download, // Export
Edit3, // Editor
AlertTriangle, // Errors
ChevronUp, // Collapse indicators
ChevronDown, // Collapse indicators
// Add your specific icons
} from 'lucide-react';
Required Components
import ToolLayout from '../components/ToolLayout';
import CodeMirrorEditor from '../components/CodeMirrorEditor';
import CodeEditor from '../components/CodeEditor'; // For read-only code display
Best Practices
- Consistency First: Always follow existing patterns before innovating
- User Data Safety: Always confirm before clearing user data
- Error Messages: Be specific and helpful
- Loading States: Show loading indicators for async operations
- Accessibility: Use semantic HTML and proper ARIA labels
- Dark Mode: Test in both light and dark modes
- Mobile: Ensure responsive design works on small screens
- Performance: Lazy load heavy components when possible
Testing Your Editor
- Input Methods: Test all 4 tabs work correctly
- Parse Valid Data: Ensure data loads and displays
- Parse Invalid Data: Ensure errors show and input stays expanded
- Tab Switching: Confirm data loss prevention works
- Export: Test all export formats
- Collapse/Expand: Test all collapsible sections
- Dark Mode: Toggle and verify styling
- Mobile: Test on small screen sizes
- Edge Cases: Empty data, very large data, special characters
Questions or Issues?
Refer to existing editors:
- Object Editor: JSON/PHP serialized data handling
- Table Editor: Tabular data with inline editing
- Invoice Editor: Form-based data entry
Follow these patterns and your new editor will be consistent with the rest of the application!