deepsite-project-lytad / editor.html
LPX55's picture
🐳 12/03 - 07:11 - design is broken again
d71e6ae verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Slide Deck Editor</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
* {
font-family: 'Inter', sans-serif;
}
.slide-thumb {
transition: all 0.2s ease;
cursor: pointer;
}
.slide-thumb:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.slide-thumb.active {
ring: 2px solid #6366f1;
transform: scale(1.02);
}
.editor-input {
transition: all 0.2s ease;
}
.editor-input:focus {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.1);
}
.preview-container {
aspect-ratio: 16/9;
transition: transform 0.3s ease;
}
.tool-btn {
transition: all 0.2s ease;
}
.tool-btn:hover {
transform: translateY(-1px);
}
.tool-btn.active {
background: #e0e7ff;
color: #4338ca;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
.gradient-text {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
</style>
</head>
<body class="bg-gray-50 h-screen w-screen overflow-hidden flex text-gray-900">
<!-- Sidebar: Slides Panel -->
<aside class="w-72 bg-white border-r border-gray-200 flex flex-col h-full shrink-0">
<!-- Header -->
<div class="p-4 border-b border-gray-200 flex items-center justify-between bg-gray-50/50">
<div class="flex items-center gap-2">
<div class="w-8 h-8 rounded-lg bg-indigo-600 flex items-center justify-center text-white">
<i data-lucide="layers" class="w-4 h-4"></i>
</div>
<span class="font-semibold text-gray-900">Deck Editor</span>
</div>
<button onclick="editor.addSlide()" class="w-8 h-8 rounded-lg bg-indigo-50 text-indigo-600 flex items-center justify-center hover:bg-indigo-100 transition-colors" title="Add Slide">
<i data-lucide="plus" class="w-4 h-4"></i>
</button>
</div>
<!-- Slides List -->
<div id="slides-list" class="flex-1 overflow-y-auto p-4 space-y-3">
<!-- Slides rendered here -->
</div>
<!-- Footer Actions -->
<div class="p-4 border-t border-gray-200 space-y-2 bg-gray-50/50">
<button onclick="editor.preview()" class="w-full py-2.5 px-4 bg-indigo-600 text-white rounded-lg font-medium hover:bg-indigo-700 transition-colors flex items-center justify-center gap-2">
<i data-lucide="play" class="w-4 h-4"></i>
Preview Deck
</button>
<div class="flex gap-2">
<button onclick="editor.exportJSON()" class="flex-1 py-2 px-3 border border-gray-300 rounded-lg text-sm font-medium hover:bg-gray-50 transition-colors flex items-center justify-center gap-1">
<i data-lucide="download" class="w-4 h-4"></i>
Export
</button>
<label class="flex-1 py-2 px-3 border border-gray-300 rounded-lg text-sm font-medium hover:bg-gray-50 transition-colors flex items-center justify-center gap-1 cursor-pointer">
<i data-lucide="upload" class="w-4 h-4"></i>
Import
<input type="file" id="import-file" accept=".json" class="hidden" onchange="editor.importJSON(this)">
</label>
</div>
</div>
</aside>
<!-- Main Editor Area -->
<main class="flex-1 flex flex-col h-full overflow-hidden">
<!-- Toolbar -->
<div class="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-6 shrink-0">
<div class="flex items-center gap-4">
<div class="relative">
<button onclick="editor.toggleLayoutMenu()" id="layout-dropdown-btn" class="tool-btn px-4 py-1.5 rounded-lg text-sm font-medium text-gray-700 flex items-center gap-2 bg-white border border-gray-300 shadow-sm hover:bg-gray-50">
<i data-lucide="layout" class="w-4 h-4"></i>
<span id="current-layout-label">Title</span>
<i data-lucide="chevron-down" class="w-4 h-4 ml-1"></i>
</button>
<div id="layout-menu" class="hidden absolute top-full left-0 mt-2 w-56 bg-white rounded-xl shadow-xl border border-gray-200 py-2 z-50">
<div class="px-3 py-1.5 text-xs font-semibold text-gray-500 uppercase tracking-wider">Content</div>
<button onclick="editor.setLayout('title')" class="w-full px-4 py-2 text-sm text-gray-700 hover:bg-indigo-50 hover:text-indigo-600 flex items-center gap-3 transition-colors" data-layout="title">
<i data-lucide="type" class="w-4 h-4"></i>
Title
</button>
<button onclick="editor.setLayout('split')" class="w-full px-4 py-2 text-sm text-gray-700 hover:bg-indigo-50 hover:text-indigo-600 flex items-center gap-3 transition-colors" data-layout="split">
<i data-lucide="columns" class="w-4 h-4"></i>
Split
</button>
<button onclick="editor.setLayout('image')" class="w-full px-4 py-2 text-sm text-gray-700 hover:bg-indigo-50 hover:text-indigo-600 flex items-center gap-3 transition-colors" data-layout="image">
<i data-lucide="image" class="w-4 h-4"></i>
Image
</button>
<div class="border-t border-gray-100 my-1">
<div class="px-3 py-1.5 text-xs font-semibold text-gray-500 uppercase tracking-wider mt-1">Grid</div>
</div>
<button onclick="editor.setLayout('grid')" class="w-full px-4 py-2 text-sm text-gray-700 hover:bg-indigo-50 hover:text-indigo-600 flex items-center gap-3 transition-colors" data-layout="grid">
<i data-lucide="layout-grid" class="w-4 h-4"></i>
Grid Cards
</button>
<div class="border-t border-gray-100 my-1">
<div class="px-3 py-1.5 text-xs font-semibold text-gray-500 uppercase tracking-wider mt-1">Compare</div>
</div>
<button onclick="editor.setLayout('comparison')" class="w-full px-4 py-2 text-sm text-gray-700 hover:bg-indigo-50 hover:text-indigo-600 flex items-center gap-3 transition-colors" data-layout="comparison">
<i data-lucide="table" class="w-4 h-4"></i>
2-Column Table
</button>
<button onclick="editor.setLayout('compare3')" class="w-full px-4 py-2 text-sm text-gray-700 hover:bg-indigo-50 hover:text-indigo-600 flex items-center gap-3 transition-colors" data-layout="compare3">
<i data-lucide="columns-3" class="w-4 h-4"></i>
3-Column Table
</button>
<div class="border-t border-gray-100 my-1">
<div class="px-3 py-1.5 text-xs font-semibold text-gray-500 uppercase tracking-wider mt-1">Advanced</div>
</div>
<button onclick="editor.setLayout('custom-html')" class="w-full px-4 py-2 text-sm text-gray-700 hover:bg-indigo-50 hover:text-indigo-600 flex items-center gap-3 transition-colors" data-layout="custom-html">
<i data-lucide="code-2" class="w-4 h-4"></i>
Custom HTML
</button>
</div>
</div>
<!-- View Toggle -->
<div class="flex items-center gap-1 bg-indigo-50 rounded-lg p-1 ml-4">
<button onclick="editor.setViewMode('visual')" id="view-visual" class="view-btn active px-3 py-1.5 rounded-md text-sm font-medium text-indigo-700 flex items-center gap-1">
<i data-lucide="eye" class="w-4 h-4"></i>
Visual
</button>
<button onclick="editor.setViewMode('json')" id="view-json" class="view-btn px-3 py-1.5 rounded-md text-sm font-medium text-gray-600 flex items-center gap-1">
<i data-lucide="code" class="w-4 h-4"></i>
JSON
</button>
</div>
</div>
<div class="flex items-center gap-2">
<button onclick="editor.deleteSlide()" class="w-9 h-9 rounded-lg text-red-600 hover:bg-red-50 transition-colors flex items-center justify-center" title="Delete Slide">
<i data-lucide="trash-2" class="w-4 h-4"></i>
</button>
<button onclick="editor.duplicateSlide()" class="w-9 h-9 rounded-lg text-gray-600 hover:bg-gray-100 transition-colors flex items-center justify-center" title="Duplicate Slide">
<i data-lucide="copy" class="w-4 h-4"></i>
</button>
<button onclick="editor.moveSlide(-1)" class="w-9 h-9 rounded-lg text-gray-600 hover:bg-gray-100 transition-colors flex items-center justify-center" title="Move Up">
<i data-lucide="arrow-up" class="w-4 h-4"></i>
</button>
<button onclick="editor.moveSlide(1)" class="w-9 h-9 rounded-lg text-gray-600 hover:bg-gray-100 transition-colors flex items-center justify-center" title="Move Down">
<i data-lucide="arrow-down" class="w-4 h-4"></i>
</button>
</div>
</div>
<!-- Editor Content -->
<div class="flex-1 overflow-y-auto p-8">
<div class="max-w-4xl mx-auto space-y-6">
<!-- Slide Preview -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div class="bg-gray-100 px-4 py-2 border-b border-gray-200 flex items-center justify-between">
<span class="text-xs font-medium text-gray-500 uppercase tracking-wider">Preview</span>
<span class="text-xs text-gray-400" id="resolution-display">1920 × 1080</span>
</div>
<div class="p-6 bg-gray-50 flex items-center justify-center">
<div id="slide-preview" class="preview-container w-full max-w-2xl bg-white rounded-lg shadow-lg overflow-hidden relative">
<!-- Preview content rendered here -->
</div>
</div>
</div>
<!-- Edit Form -->
<div id="edit-form" class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-6">
<!-- Dynamic form fields based on layout -->
</div>
</div>
</div>
</main>
<!-- Preview Modal -->
<div id="preview-modal" class="fixed inset-0 bg-black/90 z-50 hidden items-center justify-center">
<div class="w-full h-full max-w-6xl max-h-screen p-4 flex flex-col">
<div class="flex items-center justify-between mb-4">
<h3 class="text-white font-semibold">Presentation Preview</h3>
<button onclick="editor.closePreview()" class="text-white/70 hover:text-white transition-colors">
<i data-lucide="x" class="w-6 h-6"></i>
</button>
</div>
<div class="flex-1 flex items-center justify-center">
<div id="preview-container" class="w-full aspect-video bg-white rounded-lg overflow-hidden shadow-2xl">
<!-- Full preview rendered here -->
</div>
</div>
<div class="mt-4 flex items-center justify-center gap-4">
<button onclick="editor.previewPrev()" class="px-4 py-2 bg-white/10 text-white rounded-lg hover:bg-white/20 transition-colors">
<i data-lucide="chevron-left" class="w-5 h-5"></i>
</button>
<span id="preview-counter" class="text-white font-medium">1 / 1</span>
<button onclick="editor.previewNext()" class="px-4 py-2 bg-white/10 text-white rounded-lg hover:bg-white/20 transition-colors">
<i data-lucide="chevron-right" class="w-5 h-5"></i>
</button>
</div>
</div>
</div>
<script>
// Initialize Lucide icons
lucide.createIcons();
class SlideEditor {
constructor() {
this.validationPaused = false;
this.pendingJSON = null;
this.slides = [
{
id: 1,
layout: 'title',
title: 'Digital Innovation',
subtitle: 'Transforming ideas into reality through modern design and cutting-edge technology',
theme: 'gradient',
badge: 'Presentation Deck',
metadata: ['2024', '5 min read']
},
{
id: 2,
layout: 'split',
title: 'Strategic Vision',
content: 'Our approach combines data-driven insights with creative excellence. We believe in building sustainable solutions that stand the test of time.',
points: [
'User-centered design methodology',
'Agile development processes',
'Continuous integration & delivery'
],
image: 'office'
},
{
id: 3,
layout: 'grid',
title: 'Core Features',
subtitle: 'Everything you need to succeed',
items: [
{ icon: 'zap', title: 'Lightning Fast', desc: 'Optimized performance', color: 'blue' },
{ icon: 'shield', title: 'Secure by Design', desc: 'Enterprise-grade security', color: 'purple' },
{ icon: 'heart', title: 'User Friendly', desc: 'Intuitive interfaces', color: 'pink' }
]
}
];
this.currentSlide = 0;
this.previewIndex = 0;
this.viewMode = 'visual';
this.jsonError = null;
this.init();
}
init() {
this.renderSlidesList();
this.renderEditor();
this.updatePreview();
// Close layout dropdown when clicking outside
document.addEventListener('click', (e) => {
const menu = document.getElementById('layout-menu');
const btn = document.getElementById('layout-dropdown-btn');
if (menu && btn && !menu.contains(e.target) && !btn.contains(e.target)) {
menu.classList.add('hidden');
}
});
}
toggleLayoutMenu() {
document.getElementById('layout-menu').classList.toggle('hidden');
}
generateId() {
return Date.now();
}
addSlide() {
const newSlide = {
id: this.generateId(),
layout: 'title',
title: 'New Slide',
subtitle: 'Add your subtitle here',
theme: 'default',
badge: '',
metadata: []
};
this.slides.push(newSlide);
this.currentSlide = this.slides.length - 1;
this.renderSlidesList();
this.renderEditor();
this.updatePreview();
}
duplicateSlide() {
const slide = this.slides[this.currentSlide];
const newSlide = { ...slide, id: this.generateId() };
this.slides.splice(this.currentSlide + 1, 0, newSlide);
this.currentSlide++;
this.renderSlidesList();
this.renderEditor();
this.updatePreview();
}
deleteSlide() {
if (this.slides.length <= 1) {
alert('You must have at least one slide');
return;
}
if (confirm('Delete this slide?')) {
this.slides.splice(this.currentSlide, 1);
this.currentSlide = Math.max(0, this.currentSlide - 1);
this.renderSlidesList();
this.renderEditor();
this.updatePreview();
}
}
moveSlide(direction) {
const newIndex = this.currentSlide + direction;
if (newIndex >= 0 && newIndex < this.slides.length) {
[this.slides[this.currentSlide], this.slides[newIndex]] =
[this.slides[newIndex], this.slides[this.currentSlide]];
this.currentSlide = newIndex;
this.renderSlidesList();
}
}
selectSlide(index) {
this.currentSlide = index;
this.renderSlidesList();
this.renderEditor();
this.updatePreview();
}
setLayout(layout) {
this.slides[this.currentSlide].layout = layout;
// Add default data for new layouts
if (layout === 'grid' && !this.slides[this.currentSlide].items) {
this.slides[this.currentSlide].items = [
{ icon: 'zap', title: 'Feature 1', desc: 'Description here', color: 'blue' }
];
}
if (layout === 'image' && !this.slides[this.currentSlide].imageTitle) {
this.slides[this.currentSlide].imageTitle = 'Image Slide';
this.slides[this.currentSlide].image = 'technology';
}
if (layout === 'comparison' && !this.slides[this.currentSlide].columns) {
this.slides[this.currentSlide].columns = ['Feature', 'Our Solution', 'Competitors'];
this.slides[this.currentSlide].rows = [
{ feature: 'Performance', col1: '✓ Superior', col2: '✗ Limited' },
{ feature: 'Pricing', col1: '✓ Affordable', col2: '✗ Expensive' },
{ feature: 'Support', col1: '✓ 24/7', col2: '✗ Business hours' }
];
}
if (layout === 'compare3' && !this.slides[this.currentSlide].headers) {
this.slides[this.currentSlide].headers = ['Option A', 'Option B', 'Option C'];
this.slides[this.currentSlide].rows = [
{ feature: 'Speed', col1: 'Fast', col2: 'Medium', col3: 'Slow' },
{ feature: 'Price', col1: '$99/mo', col2: '$149/mo', col3: '$199/mo' },
{ feature: 'Support', col1: '24/7', col2: 'Business', col3: 'Email' }
];
}
if (layout === 'custom-html' && !this.slides[this.currentSlide].htmlContent) {
this.slides[this.currentSlide].htmlContent = '<div class="flex items-center justify-center h-full bg-gradient-to-br from-indigo-50 to-purple-50">\n <div class="text-center p-8">\n <h2 class="text-4xl font-bold text-indigo-600 mb-4">Custom HTML Slide</h2>\n <p class="text-lg text-gray-600">Edit this content in the HTML editor below</p>\n </div>\n</div>';
}
this.renderEditor();
this.updatePreview();
// Update layout button label
const layoutLabels = {
'title': 'Title',
'split': 'Split',
'image': 'Image',
'grid': 'Grid Cards',
'comparison': '2-Column Table',
'compare3': '3-Column Table',
'custom-html': 'Custom HTML'
};
document.getElementById('current-layout-label').textContent = layoutLabels[layout] || layout;
// Close menu if open
document.getElementById('layout-menu').classList.add('hidden');
}
updateField(field, value) {
this.slides[this.currentSlide][field] = value;
this.updatePreview();
}
updatePoint(index, value) {
if (!this.slides[this.currentSlide].points) {
this.slides[this.currentSlide].points = [];
}
this.slides[this.currentSlide].points[index] = value;
this.updatePreview();
}
addPoint() {
if (!this.slides[this.currentSlide].points) {
this.slides[this.currentSlide].points = [];
}
this.slides[this.currentSlide].points.push('New point');
this.renderEditor();
this.updatePreview();
}
removePoint(index) {
this.slides[this.currentSlide].points.splice(index, 1);
this.renderEditor();
this.updatePreview();
}
renderSlidesList() {
const container = document.getElementById('slides-list');
container.innerHTML = this.slides.map((slide, index) => `
<div onclick="editor.selectSlide(${index})"
class="slide-thumb ${index === this.currentSlide ? 'active' : ''} bg-white rounded-lg border border-gray-200 p-3 ${index === this.currentSlide ? 'ring-2 ring-indigo-600' : ''}">
<div class="flex items-center gap-3 mb-2">
<span class="text-xs font-medium text-gray-400 w-5">${index + 1}</span>
<span class="font-medium text-sm text-gray-900 truncate flex-1">${slide.title || 'Untitled'}</span>
</div>
<div class="h-16 bg-gray-100 rounded border border-gray-200 overflow-hidden flex items-center justify-center text-xs text-gray-400">
${slide.layout}
</div>
</div>
`).join('');
}
setViewMode(mode) {
this.viewMode = mode;
document.querySelectorAll('.view-btn').forEach(btn => {
btn.classList.remove('active', 'bg-white', 'shadow-sm', 'text-indigo-700');
btn.classList.add('text-gray-600');
});
document.getElementById(`view-${mode}`).classList.add('active', 'bg-white', 'shadow-sm', 'text-indigo-700');
document.getElementById(`view-${mode}`).classList.remove('text-gray-600');
this.renderEditor();
}
renderEditor() {
const slide = this.slides[this.currentSlide];
const form = document.getElementById('edit-form');
// JSON View Mode
if (this.viewMode === 'json') {
form.innerHTML = `
<div class="space-y-4">
<div class="flex items-center justify-between">
<div>
<h3 class="text-sm font-semibold text-gray-900">JSON Editor</h3>
<p class="text-xs text-gray-500 mt-1">Edit the raw JSON data for this slide</p>
</div>
<div class="flex items-center gap-2">
<button onclick="editor.toggleValidationPause()" class="px-3 py-1.5 text-xs font-medium ${this.validationPaused ? 'text-orange-600 bg-orange-50 hover:bg-orange-100' : 'text-gray-600 bg-gray-100 hover:bg-gray-200'} rounded-lg transition-colors flex items-center gap-1" title="${this.validationPaused ? 'Resume real-time validation' : 'Pause real-time validation'}">
<i data-lucide="${this.validationPaused ? 'play' : 'pause'}" class="w-3 h-3"></i>
${this.validationPaused ? 'Resume' : 'Pause'}
</button>
<button onclick="editor.formatJSON()" class="px-3 py-1.5 text-xs font-medium text-indigo-600 bg-indigo-50 rounded-lg hover:bg-indigo-100 transition-colors flex items-center gap-1">
<i data-lucide="sparkles" class="w-3 h-3"></i>
Format
</button>
</div>
</div>
<div class="relative">
<textarea id="json-editor"
oninput="editor.validateJSON(this.value)"
class="w-full h-96 px-4 py-3 font-mono text-xs leading-relaxed border ${this.jsonError ? 'border-red-300 focus:ring-red-500 focus:border-red-500' : 'border-gray-300 focus:ring-indigo-500 focus:border-indigo-500'} rounded-lg outline-none resize-none"
spellcheck="false">${JSON.stringify(slide, null, 2)}</textarea>
${this.jsonError ? `<div class="absolute bottom-3 left-3 right-3 px-3 py-2 bg-red-50 border border-red-200 rounded-lg text-xs text-red-600 flex items-center gap-2"><i data-lucide="alert-circle" class="w-4 h-4"></i>${this.jsonError}</div>` : ''}
</div>
<button onclick="editor.applyJSON()" class="w-full py-3 px-4 bg-indigo-600 text-white rounded-lg font-medium hover:bg-indigo-700 transition-colors flex items-center justify-center gap-2">
<i data-lucide="save" class="w-4 h-4"></i>
Apply Changes
</button>
</div>
`;
lucide.createIcons();
return;
}
// Visual View Mode
let html = `
<div class="grid grid-cols-2 gap-4">
<div class="col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1">Slide Title</label>
<input type="text" value="${slide.title || ''}"
oninput="editor.updateField('title', this.value)"
class="editor-input w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none">
</div>
`;
if (slide.layout === 'title') {
html += `
<div class="col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1">Subtitle</label>
<textarea oninput="editor.updateField('subtitle', this.value)" rows="3"
class="editor-input w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none">${slide.subtitle || ''}</textarea>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Badge</label>
<input type="text" value="${slide.badge || ''}"
oninput="editor.updateField('badge', this.value)"
class="editor-input w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Theme</label>
<select onchange="editor.updateField('theme', this.value)"
class="editor-input w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none">
<option value="default" ${slide.theme === 'default' ? 'selected' : ''}>Default</option>
<option value="gradient" ${slide.theme === 'gradient' ? 'selected' : ''}>Gradient</option>
<option value="dark" ${slide.theme === 'dark' ? 'selected' : ''}>Dark</option>
</select>
</div>
`;
} else if (slide.layout === 'split') {
html += `
<div class="col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1">Content</label>
<textarea oninput="editor.updateField('content', this.value)" rows="4"
class="editor-input w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none">${slide.content || ''}</textarea>
</div>
<div class="col-span-2">
<div class="flex items-center justify-between mb-2">
<label class="block text-sm font-medium text-gray-700">Bullet Points</label>
<button onclick="editor.addPoint()" class="text-sm text-indigo-600 hover:text-indigo-700 font-medium">+ Add Point</button>
</div>
<div class="space-y-2">
${(slide.points || []).map((point, i) => `
<div class="flex items-center gap-2">
<i data-lucide="check-circle-2" class="w-4 h-4 text-indigo-600 shrink-0"></i>
<input type="text" value="${point}"
oninput="editor.updatePoint(${i}, this.value)"
class="editor-input flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none text-sm">
<button onclick="editor.removePoint(${i})" class="text-red-500 hover:text-red-700">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
`).join('')}
</div>
</div>
<div class="col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1">Image Category</label>
<select onchange="editor.updateField('image', this.value)"
class="editor-input w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none">
<option value="office" ${slide.image === 'office' ? 'selected' : ''}>Office</option>
<option value="technology" ${slide.image === 'technology' ? 'selected' : ''}>Technology</option>
<option value="people" ${slide.image === 'people' ? 'selected' : ''}>People</option>
<option value="nature" ${slide.image === 'nature' ? 'selected' : ''}>Nature</option>
</select>
</div>
`;
} else if (slide.layout === 'image') {
html += `
<div class="col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1">Image Title</label>
<input type="text" value="${slide.imageTitle || ''}"
oninput="editor.updateField('imageTitle', this.value)"
class="editor-input w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none">
</div>
<div class="col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1">Image Description</label>
<textarea oninput="editor.updateField('imageDesc', this.value)" rows="3"
class="editor-input w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none">${slide.imageDesc || ''}</textarea>
</div>
<div class="col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1">Image Category</label>
<select onchange="editor.updateField('image', this.value)"
class="editor-input w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none">
<option value="technology" ${slide.image === 'technology' ? 'selected' : ''}>Technology</option>
<option value="office" ${slide.image === 'office' ? 'selected' : ''}>Office</option>
<option value="cityscape" ${slide.image === 'cityscape' ? 'selected' : ''}>Cityscape</option>
<option value="nature" ${slide.image === 'nature' ? 'selected' : ''}>Nature</option>
</select>
</div>
`;
} else if (slide.layout === 'grid') {
html += `
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Subtitle</label>
<input type="text" value="${slide.subtitle || ''}"
oninput="editor.updateField('subtitle', this.value)"
class="editor-input w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none">
</div>
<div class="col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-2">Grid Items (first 3 shown)</label>
<div class="grid grid-cols-3 gap-4">
${(slide.items || []).slice(0, 3).map((item, i) => `
<div class="space-y-2 p-3 bg-gray-50 rounded-lg border border-gray-200">
<input type="text" value="${item.title}" placeholder="Title"
oninput="editor.updateItem(${i}, 'title', this.value)"
class="editor-input w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-indigo-500 outline-none">
<input type="text" value="${item.desc}" placeholder="Description"
oninput="editor.updateItem(${i}, 'desc', this.value)"
class="editor-input w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-indigo-500 outline-none">
</div>
`).join('')}
</div>
</div>
`;
} else if (slide.layout === 'comparison') {
html += `
<div class="col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1">Subtitle</label>
<input type="text" value="${slide.subtitle || ''}"
oninput="editor.updateField('subtitle', this.value)"
class="editor-input w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none">
</div>
<div class="col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-2">Column Headers</label>
<div class="grid grid-cols-3 gap-3">
${(slide.columns || ['Feature', 'Our Solution', 'Competitors']).map((col, i) => `
<input type="text" value="${col}"
oninput="editor.updateColumn(${i}, this.value)"
class="editor-input w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none">
`).join('')}
</div>
</div>
<div class="col-span-2">
<div class="flex items-center justify-between mb-2">
<label class="block text-sm font-medium text-gray-700">Comparison Rows</label>
<button onclick="editor.addComparisonRow()" class="text-sm text-indigo-600 hover:text-indigo-700 font-medium">+ Add Row</button>
</div>
<div class="space-y-2">
${(slide.rows || []).map((row, i) => `
<div class="grid grid-cols-3 gap-3 p-3 bg-gray-50 rounded-lg border border-gray-200">
<input type="text" value="${row.feature}" placeholder="Feature name"
oninput="editor.updateComparisonRow(${i}, 'feature', this.value)"
class="editor-input w-full px-3 py-2 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-indigo-500 outline-none">
<input type="text" value="${row.col1}" placeholder="Option 1"
oninput="editor.updateComparisonRow(${i}, 'col1', this.value)"
class="editor-input w-full px-3 py-2 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-indigo-500 outline-none">
<div class="flex items-center gap-2">
<input type="text" value="${row.col2}" placeholder="Option 2"
oninput="editor.updateComparisonRow(${i}, 'col2', this.value)"
class="editor-input flex-1 px-3 py-2 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-indigo-500 outline-none">
<button onclick="editor.removeComparisonRow(${i})" class="text-red-500 hover:text-red-700 p-1">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
</div>
`).join('')}
</div>
</div>
`;
} else if (slide.layout === 'compare3') {
html += `
<div class="col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1">Subtitle</label>
<input type="text" value="${slide.subtitle || ''}"
oninput="editor.updateField('subtitle', this.value)"
class="editor-input w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none">
</div>
<div class="col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-2">Three Column Headers</label>
<div class="grid grid-cols-3 gap-3">
${(slide.headers || ['Option A', 'Option B', 'Option C']).map((header, i) => `
<input type="text" value="${header}"
oninput="editor.updateHeader(${i}, this.value)"
class="editor-input w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none">
`).join('')}
</div>
</div>
<div class="col-span-2">
<div class="flex items-center justify-between mb-2">
<label class="block text-sm font-medium text-gray-700">Comparison Rows</label>
<button onclick="editor.addCompare3Row()" class="text-sm text-indigo-600 hover:text-indigo-700 font-medium">+ Add Row</button>
</div>
<div class="space-y-2">
${(slide.rows || []).map((row, i) => `
<div class="grid grid-cols-4 gap-3 p-3 bg-gray-50 rounded-lg border border-gray-200">
<input type="text" value="${row.feature}" placeholder="Feature name"
oninput="editor.updateCompare3Row(${i}, 'feature', this.value)"
class="editor-input w-full px-3 py-2 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-indigo-500 outline-none">
<input type="text" value="${row.col1}" placeholder="Column 1"
oninput="editor.updateCompare3Row(${i}, 'col1', this.value)"
class="editor-input w-full px-3 py-2 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-indigo-500 outline-none">
<input type="text" value="${row.col2}" placeholder="Column 2"
oninput="editor.updateCompare3Row(${i}, 'col2', this.value)"
class="editor-input w-full px-3 py-2 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-indigo-500 outline-none">
<div class="flex items-center gap-2">
<input type="text" value="${row.col3}" placeholder="Column 3"
oninput="editor.updateCompare3Row(${i}, 'col3', this.value)"
class="editor-input flex-1 px-3 py-2 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-indigo-500 outline-none">
<button onclick="editor.removeCompare3Row(${i})" class="text-red-500 hover:text-red-700 p-1">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
</div>
`).join('')}
</div>
</div>
`;
} else if (slide.layout === 'custom-html') {
html += `
<div class="col-span-2">
<div class="flex items-center justify-between mb-2">
<label class="block text-sm font-medium text-gray-700">HTML Content</label>
<span class="text-xs text-gray-500">Supports Tailwind CSS classes</span>
</div>
<textarea id="html-editor" oninput="editor.updateField('htmlContent', this.value)"
class="editor-input w-full px-4 py-3 font-mono text-xs leading-relaxed border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none resize-none"
rows="12"
spellcheck="false">${slide.htmlContent || ''}</textarea>
<p class="text-xs text-gray-500 mt-1">Write raw HTML. The content will be rendered inside a 16:9 slide container.</p>
</div>
`;
}
html += `</div>`;
form.innerHTML = html;
lucide.createIcons();
}
updateItem(index, field, value) {
this.slides[this.currentSlide].items[index][field] = value;
this.updatePreview();
}
updateColumn(index, value) {
if (!this.slides[this.currentSlide].columns) {
this.slides[this.currentSlide].columns = ['Feature', 'Our Solution', 'Competitors'];
}
this.slides[this.currentSlide].columns[index] = value;
this.updatePreview();
}
updateComparisonRow(index, field, value) {
if (!this.slides[this.currentSlide].rows) {
this.slides[this.currentSlide].rows = [];
}
if (!this.slides[this.currentSlide].rows[index]) {
this.slides[this.currentSlide].rows[index] = { feature: '', col1: '', col2: '' };
}
this.slides[this.currentSlide].rows[index][field] = value;
this.updatePreview();
}
addComparisonRow() {
if (!this.slides[this.currentSlide].rows) {
this.slides[this.currentSlide].rows = [];
}
this.slides[this.currentSlide].rows.push({ feature: 'New Feature', col1: '✓ Yes', col2: '✗ No' });
this.renderEditor();
this.updatePreview();
}
removeComparisonRow(index) {
this.slides[this.currentSlide].rows.splice(index, 1);
this.renderEditor();
this.updatePreview();
}
updateHeader(index, value) {
if (!this.slides[this.currentSlide].headers) {
this.slides[this.currentSlide].headers = ['Option A', 'Option B', 'Option C'];
}
this.slides[this.currentSlide].headers[index] = value;
this.updatePreview();
}
updateCompare3Row(index, field, value) {
if (!this.slides[this.currentSlide].rows) {
this.slides[this.currentSlide].rows = [];
}
if (!this.slides[this.currentSlide].rows[index]) {
this.slides[this.currentSlide].rows[index] = { feature: '', col1: '', col2: '', col3: '' };
}
this.slides[this.currentSlide].rows[index][field] = value;
this.updatePreview();
}
addCompare3Row() {
if (!this.slides[this.currentSlide].rows) {
this.slides[this.currentSlide].rows = [];
}
this.slides[this.currentSlide].rows.push({ feature: 'New Feature', col1: '-', col2: '-', col3: '-' });
this.renderEditor();
this.updatePreview();
}
removeCompare3Row(index) {
this.slides[this.currentSlide].rows.splice(index, 1);
this.renderEditor();
this.updatePreview();
}
validateJSON(jsonString) {
// Skip validation if paused (but still track for manual validation)
if (this.validationPaused) {
this.pendingJSON = jsonString;
return;
}
try {
JSON.parse(jsonString);
this.jsonError = null;
document.getElementById('json-editor').classList.remove('border-red-300', 'focus:ring-red-500', 'focus:border-red-500');
document.getElementById('json-editor').classList.add('border-gray-300', 'focus:ring-indigo-500', 'focus:border-indigo-500');
} catch (err) {
this.jsonError = err.message;
document.getElementById('json-editor').classList.add('border-red-300', 'focus:ring-red-500', 'focus:border-red-500');
document.getElementById('json-editor').classList.remove('border-gray-300', 'focus:ring-indigo-500', 'focus:border-indigo-500');
}
// Re-render to show/hide error message
const textarea = document.getElementById('json-editor');
const cursorPosition = textarea.selectionStart;
this.renderEditor();
// Restore cursor position
const newTextarea = document.getElementById('json-editor');
if (newTextarea) {
newTextarea.focus();
newTextarea.setSelectionRange(cursorPosition, cursorPosition);
}
}
toggleValidationPause() {
this.validationPaused = !this.validationPaused;
// When unpausing, validate the pending content
if (!this.validationPaused && this.pendingJSON) {
this.validateJSON(this.pendingJSON);
this.pendingJSON = null;
}
this.renderEditor();
}
formatJSON() {
const textarea = document.getElementById('json-editor');
try {
const parsed = JSON.parse(textarea.value);
textarea.value = JSON.stringify(parsed, null, 2);
this.jsonError = null;
this.renderEditor();
} catch (err) {
this.jsonError = err.message;
this.renderEditor();
}
}
applyJSON() {
const textarea = document.getElementById('json-editor');
try {
const parsed = JSON.parse(textarea.value);
// Preserve the ID
parsed.id = this.slides[this.currentSlide].id;
this.slides[this.currentSlide] = parsed;
this.jsonError = null;
this.renderSlidesList();
this.updatePreview();
// Show success feedback
const btn = document.querySelector('button[onclick="editor.applyJSON()"]');
const originalText = btn.innerHTML;
btn.innerHTML = '<i data-lucide="check" class="w-4 h-4"></i> Saved!';
btn.classList.add('bg-green-600', 'hover:bg-green-700');
btn.classList.remove('bg-indigo-600', 'hover:bg-indigo-700');
lucide.createIcons();
setTimeout(() => {
btn.innerHTML = originalText;
btn.classList.remove('bg-green-600', 'hover:bg-green-700');
btn.classList.add('bg-indigo-600', 'hover:bg-indigo-700');
lucide.createIcons();
}, 2000);
} catch (err) {
this.jsonError = err.message;
this.renderEditor();
}
}
updatePreview() {
const slide = this.slides[this.currentSlide];
const preview = document.getElementById('slide-preview');
let content = '';
if (slide.layout === 'title') {
const isGradient = slide.theme === 'gradient';
content = `
<div class="w-full h-full flex flex-col items-center justify-center p-8 ${isGradient ? 'bg-gradient-to-br from-white to-gray-50' : 'bg-white'}">
<div class="text-center space-y-4">
${slide.badge ? `<div class="inline-flex items-center gap-1 px-3 py-1 rounded-full bg-indigo-50 text-indigo-600 text-xs font-medium"><i data-lucide="sparkles" class="w-3 h-3"></i><span>${slide.badge}</span></div>` : ''}
<h1 class="text-3xl font-bold ${isGradient ? 'gradient-text' : 'text-gray-900'} leading-tight">${slide.title}</h1>
<p class="text-sm text-gray-600 max-w-md mx-auto">${slide.subtitle}</p>
</div>
</div>
`;
} else if (slide.layout === 'split') {
content = `
<div class="w-full h-full flex flex-col md:flex-row">
<div class="w-full md:w-1/2 p-6 flex flex-col justify-center bg-white">
<div class="space-y-4">
<h2 class="text-2xl font-bold text-gray-900">${slide.title}</h2>
<p class="text-sm text-gray-600 leading-relaxed">${slide.content}</p>
<ul class="space-y-2">
${(slide.points || []).map(point => `<li class="flex items-start gap-2 text-xs text-gray-600"><i data-lucide="check-circle-2" class="w-4 h-4 text-indigo-600 mt-0.5 shrink-0"></i><span>${point}</span></li>`).join('')}
</ul>
</div>
</div>
<div class="w-full md:w-1/2 bg-gray-100 flex items-center justify-center text-gray-400">
<i data-lucide="image" class="w-12 h-12"></i>
</div>
</div>
`;
} else if (slide.layout === 'image') {
content = `
<div class="w-full h-full relative bg-gray-900 flex items-center justify-center overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-t from-black/80 via-black/40 to-transparent"></div>
<div class="absolute bottom-0 left-0 right-0 p-6 text-white z-10">
<h2 class="text-2xl font-bold mb-2">${slide.title}</h2>
<p class="text-sm text-gray-200">${slide.subtitle}</p>
</div>
<i data-lucide="image" class="w-16 h-16 text-white/20"></i>
</div>
`;
} else if (slide.layout === 'grid') {
content = `
<div class="w-full h-full p-6 bg-gray-50 flex flex-col justify-center">
<div class="text-center space-y-1 mb-4">
<h2 class="text-xl font-bold text-gray-900">${slide.title}</h2>
<p class="text-xs text-gray-600">${slide.subtitle}</p>
</div>
<div class="grid grid-cols-3 gap-3">
${(slide.items || []).slice(0, 3).map(item => `
<div class="bg-white p-3 rounded-lg shadow-sm border border-gray-100">
<div class="w-8 h-8 rounded-lg bg-${item.color}-100 text-${item.color}-600 flex items-center justify-center mb-2">
<i data-lucide="${item.icon}" class="w-4 h-4"></i>
</div>
<h3 class="font-semibold text-gray-900 text-xs mb-1">${item.title}</h3>
<p class="text-xs text-gray-600 leading-tight">${item.desc}</p>
</div>
`).join('')}
</div>
</div>
`;
} else if (slide.layout === 'comparison') {
const cols = slide.columns || ['Feature', 'Our Solution', 'Competitors'];
content = `
<div class="w-full h-full p-6 bg-white flex flex-col justify-center">
<div class="text-center space-y-1 mb-4">
<h2 class="text-xl font-bold text-gray-900">${slide.title}</h2>
${slide.subtitle ? `<p class="text-xs text-gray-600">${slide.subtitle}</p>` : ''}
</div>
<div class="overflow-hidden rounded-lg border border-gray-200">
<table class="w-full text-xs">
<thead>
<tr class="bg-indigo-50">
<th class="px-3 py-2 text-left font-semibold text-gray-900 border-b border-indigo-100">${cols[0]}</th>
<th class="px-3 py-2 text-center font-semibold text-indigo-700 border-b border-indigo-100">${cols[1]}</th>
<th class="px-3 py-2 text-center font-semibold text-gray-600 border-b border-indigo-100">${cols[2]}</th>
</tr>
</thead>
<tbody>
${(slide.rows || []).map((row, i) => `
<tr class="${i % 2 === 0 ? 'bg-white' : 'bg-gray-50'}">
<td class="px-3 py-2 font-medium text-gray-900 border-b border-gray-100">${row.feature}</td>
<td class="px-3 py-2 text-center text-indigo-600 border-b border-gray-100">${row.col1}</td>
<td class="px-3 py-2 text-center text-gray-500 border-b border-gray-100">${row.col2}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
`;
} else if (slide.layout === 'compare3') {
const headers = slide.headers || ['Option A', 'Option B', 'Option C'];
content = `
<div class="w-full h-full p-6 bg-white flex flex-col justify-center">
<div class="text-center space-y-1 mb-4">
<h2 class="text-xl font-bold text-gray-900">${slide.title}</h2>
${slide.subtitle ? `<p class="text-xs text-gray-600">${slide.subtitle}</p>` : ''}
</div>
<div class="overflow-hidden rounded-lg border border-gray-200">
<table class="w-full text-xs">
<thead>
<tr class="bg-gradient-to-r from-indigo-50 via-purple-50 to-pink-50">
<th class="px-2 py-3 text-left font-semibold text-gray-900 border-b border-gray-200 w-1/4"></th>
<th class="px-2 py-3 text-center font-semibold text-indigo-700 border-b border-gray-200">${headers[0]}</th>
<th class="px-2 py-3 text-center font-semibold text-purple-700 border-b border-gray-200">${headers[1]}</th>
<th class="px-2 py-3 text-center font-semibold text-pink-700 border-b border-gray-200">${headers[2]}</th>
</tr>
</thead>
<tbody>
${(slide.rows || []).map((row, i) => `
<tr class="${i % 2 === 0 ? 'bg-white' : 'bg-gray-50'}">
<td class="px-2 py-3 font-medium text-gray-900 border-b border-gray-100">${row.feature}</td>
<td class="px-2 py-3 text-center text-sm text-gray-700 border-b border-gray-100">${row.col1}</td>
<td class="px-2 py-3 text-center text-sm text-gray-700 border-b border-gray-100">${row.col2}</td>
<td class="px-2 py-3 text-center text-sm text-gray-700 border-b border-gray-100">${row.col3}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
`;
} else if (slide.layout === 'custom-html') {
content = slide.htmlContent || '<div class="flex items-center justify-center h-full text-gray-400">No HTML content</div>';
}
preview.innerHTML = content;
lucide.createIcons();
}
preview() {
this.previewIndex = 0;
document.getElementById('preview-modal').classList.remove('hidden');
document.getElementById('preview-modal').classList.add('flex');
this.renderFullPreview();
}
closePreview() {
document.getElementById('preview-modal').classList.add('hidden');
document.getElementById('preview-modal').classList.remove('flex');
}
renderFullPreview() {
const slide = this.slides[this.previewIndex];
const container = document.getElementById('preview-container');
// Use same rendering logic but full size
let content = '';
if (slide.layout === 'title') {
const isGradient = slide.theme === 'gradient';
content = `
<div class="w-full h-full flex flex-col items-center justify-center p-12 ${isGradient ? 'bg-gradient-to-br from-white to-gray-50' : 'bg-white'}">
<div class="text-center space-y-6">
${slide.badge ? `<div class="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-indigo-50 text-indigo-600 text-sm font-medium mb-4"><i data-lucide="sparkles" class="w-4 h-4"></i><span>${slide.badge}</span></div>` : ''}
<h1 class="text-5xl font-bold ${isGradient ? 'gradient-text' : 'text-gray-900'} leading-tight">${slide.title}</h1>
<p class="text-xl text-gray-600 max-w-2xl mx-auto">${slide.subtitle}</p>
</div>
</div>
`;
} else if (slide.layout === 'split') {
content = `
<div class="w-full h-full flex">
<div class="w-1/2 p-12 flex flex-col justify-center bg-white">
<div class="space-y-6">
<h2 class="text-4xl font-bold text-gray-900">${slide.title}</h2>
<p class="text-lg text-gray-600 leading-relaxed">${slide.content}</p>
<ul class="space-y-3">
${(slide.points || []).map(point => `<li class="flex items-start gap-3 text-gray-600"><i data-lucide="check-circle-2" class="w-5 h-5 text-indigo-600 mt-0.5 shrink-0"></i><span>${point}</span></li>`).join('')}
</ul>
</div>
</div>
<div class="w-1/2 bg-gray-100 flex items-center justify-center">
<img src="http://static.photos/${slide.image || 'office'}/800x450/${slide.id}" class="w-full h-full object-cover" alt="">
</div>
</div>
`;
} else if (slide.layout === 'image') {
content = `
<div class="w-full h-full relative">
<img src="http://static.photos/${slide.image || 'technology'}/1200x630/${slide.id}" class="w-full h-full object-cover" alt="">
<div class="absolute inset-0 bg-gradient-to-t from-black/80 via-black/40 to-transparent"></div>
<div class="absolute bottom-0 left-0 right-0 p-12 text-white">
<h2 class="text-4xl font-bold mb-4">${slide.title}</h2>
<p class="text-xl text-gray-200">${slide.subtitle}</p>
</div>
</div>
`;
} else if (slide.layout === 'grid') {
content = `
<div class="w-full h-full p-12 bg-gray-50 flex flex-col justify-center">
<div class="text-center space-y-2 mb-8">
<h2 class="text-3xl font-bold text-gray-900">${slide.title}</h2>
<p class="text-gray-600">${slide.subtitle}</p>
</div>
<div class="grid grid-cols-3 gap-6 max-w-4xl mx-auto w-full">
${(slide.items || []).slice(0, 3).map(item => `
<div class="bg-white p-6 rounded-2xl shadow-sm border border-gray-100">
<div class="w-12 h-12 rounded-xl bg-${item.color}-100 text-${item.color}-600 flex items-center justify-center mb-4">
<i data-lucide="${item.icon}" class="w-6 h-6"></i>
</div>
<h3 class="font-semibold text-gray-900 mb-2">${item.title}</h3>
<p class="text-sm text-gray-600">${item.desc}</p>
</div>
`).join('')}
</div>
</div>
`;
} else if (slide.layout === 'comparison') {
const cols = slide.columns || ['Feature', 'Our Solution', 'Competitors'];
content = `
<div class="w-full h-full p-12 bg-white flex flex-col justify-center">
<div class="text-center space-y-2 mb-8">
<h2 class="text-4xl font-bold text-gray-900">${slide.title}</h2>
${slide.subtitle ? `<p class="text-xl text-gray-600">${slide.subtitle}</p>` : ''}
</div>
<div class="max-w-4xl mx-auto w-full">
<div class="overflow-hidden rounded-2xl border border-gray-200 shadow-sm">
<table class="w-full">
<thead>
<tr class="bg-indigo-50">
<th class="px-6 py-4 text-left text-sm font-semibold text-gray-900 border-b border-indigo-100 w-1/3">${cols[0]}</th>
<th class="px-6 py-4 text-center text-sm font-bold text-indigo-700 border-b border-indigo-100 w-1/3">${cols[1]}</th>
<th class="px-6 py-4 text-center text-sm font-semibold text-gray-600 border-b border-indigo-100 w-1/3">${cols[2]}</th>
</tr>
</thead>
<tbody>
${(slide.rows || []).map((row, i) => `
<tr class="${i % 2 === 0 ? 'bg-white' : 'bg-gray-50'} hover:bg-indigo-50/30 transition-colors">
<td class="px-6 py-4 text-sm font-medium text-gray-900 border-b border-gray-100">${row.feature}</td>
<td class="px-6 py-4 text-center text-sm font-semibold text-indigo-600 border-b border-gray-100">${row.col1}</td>
<td class="px-6 py-4 text-center text-sm text-gray-500 border-b border-gray-100">${row.col2}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
</div>
`;
} else if (slide.layout === 'compare3') {
const headers = slide.headers || ['Option A', 'Option B', 'Option C'];
content = `
<div class="w-full h-full p-12 bg-white flex flex-col justify-center">
<div class="text-center space-y-2 mb-8">
<h2 class="text-4xl font-bold text-gray-900">${slide.title}</h2>
${slide.subtitle ? `<p class="text-xl text-gray-600">${slide.subtitle}</p>` : ''}
</div>
<div class="max-w-5xl mx-auto w-full">
<div class="overflow-hidden rounded-2xl border border-gray-200 shadow-sm">
<table class="w-full">
<thead>
<tr class="bg-gradient-to-r from-indigo-50 via-purple-50 to-pink-50">
<th class="px-4 py-4 text-left text-sm font-semibold text-gray-900 border-b border-gray-200 w-1/4"></th>
<th class="px-4 py-4 text-center text-sm font-bold text-indigo-700 border-b border-gray-200">${headers[0]}</th>
<th class="px-4 py-4 text-center text-sm font-bold text-purple-700 border-b border-gray-200">${headers[1]}</th>
<th class="px-4 py-4 text-center text-sm font-bold text-pink-700 border-b border-gray-200">${headers[2]}</th>
</tr>
</thead>
<tbody>
${(slide.rows || []).map((row, i) => `
<tr class="${i % 2 === 0 ? 'bg-white' : 'bg-gray-50'} hover:bg-gray-50/50 transition-colors">
<td class="px-4 py-4 text-sm font-medium text-gray-900 border-b border-gray-100">${row.feature}</td>
<td class="px-4 py-4 text-center text-sm text-gray-700 border-b border-gray-100">${row.col1}</td>
<td class="px-4 py-4 text-center text-sm text-gray-700 border-b border-gray-100">${row.col2}</td>
<td class="px-4 py-4 text-center text-sm text-gray-700 border-b border-gray-100">${row.col3}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
</div>
`;
} else if (slide.layout === 'custom-html') {
content = slide.htmlContent || '<div class="flex items-center justify-center h-full text-gray-400 text-xl">No HTML content defined</div>';
}
container.innerHTML = content;
document.getElementById('preview-counter').textContent = `${this.previewIndex + 1} / ${this.slides.length}`;
lucide.createIcons();
}
previewNext() {
if (this.previewIndex < this.slides.length - 1) {
this.previewIndex++;
this.renderFullPreview();
}
}
previewPrev() {
if (this.previewIndex > 0) {
this.previewIndex--;
this.renderFullPreview();
}
}
exportJSON() {
const data = JSON.stringify(this.slides, null, 2);
const blob = new Blob([data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'presentation.json';
a.click();
URL.revokeObjectURL(url);
}
importJSON(input) {
const file = input.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
this.slides = JSON.parse(e.target.result);
this.currentSlide = 0;
this.renderSlidesList();
this.renderEditor();
this.updatePreview();
} catch (err) {
alert('Invalid JSON file');
}
};
reader.readAsText(file);
input.value = '';
}
}
// Initialize editor
const editor = new SlideEditor();
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
if (editor.currentSlide < editor.slides.length - 1) {
editor.selectSlide(editor.currentSlide + 1);
}
}
if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
if (editor.currentSlide > 0) {
editor.selectSlide(editor.currentSlide - 1);
}
}
});
</script>
</body>
</html>