Remove iframe previews and improve theme hot reload with SVG demo content

- Remove iframe previews from theme detail and layout detail pages for cleaner UI
- Replace with informative layout cards showing descriptions and slot type badges
- Fix theme hot reload by switching from custom HMR to full page reload
- Update template renderer to use slot names as demo content
- Add SVG pattern generation for image slots with grid background and slot labels
- Improve demo content for all slot types (code, lists, tables, etc.)
- Clean up HMR listeners and simplify theme change detection

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Michael Mainguy 2025-08-20 14:11:51 -05:00
parent 15d3789bb4
commit 98958b2cb3
9 changed files with 136 additions and 113 deletions

View File

@ -11,5 +11,5 @@
"hasMasterSlide": true "hasMasterSlide": true
} }
}, },
"generated": "2025-08-20T18:06:52.256Z" "generated": "2025-08-20T18:50:35.588Z"
} }

View File

@ -2,7 +2,8 @@
<h1 class="slot title-slot" data-slot="title" data-placeholder="Presentation Title" data-required> <h1 class="slot title-slot" data-slot="title" data-placeholder="Presentation Title" data-required>
{{title}} {{title}}
</h1> </h1>
<h2 class="slot subtitle-slot" data-slot="subtitle" data-placeholder="Subtitle or tagline"> <div class="slot diagram-slot" data-slot="diagram" data-placeholder="Diagram or Image" data-required>
{{subtitle}} {{diagram}}
</h2> </div>
<h3>Static Content</h3>
</div> </div>

View File

@ -126,6 +126,8 @@
.layout-title-slide .slot[data-slot="title"] { .layout-title-slide .slot[data-slot="title"] {
font-size: clamp(2rem, 5vw, 4rem); font-size: clamp(2rem, 5vw, 4rem);
margin-bottom: 2rem; margin-bottom: 2rem;
width: 80%;
color: var(--theme-primary); color: var(--theme-primary);
} }
@ -189,4 +191,4 @@
.slot.empty::before { .slot.empty::before {
display: none; display: none;
} }
} }/* Test change Wed Aug 20 13:55:27 CDT 2025 */

View File

@ -2,7 +2,6 @@ import React, { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom'; import { useParams, Link } from 'react-router-dom';
import type { Theme, SlideLayout } from '../../types/theme'; import type { Theme, SlideLayout } from '../../types/theme';
import { getTheme } from '../../themes'; import { getTheme } from '../../themes';
import { LayoutPreview } from './LayoutPreview';
import './LayoutDetailPage.css'; import './LayoutDetailPage.css';
export const LayoutDetailPage: React.FC = () => { export const LayoutDetailPage: React.FC = () => {
@ -11,7 +10,7 @@ export const LayoutDetailPage: React.FC = () => {
const [layout, setLayout] = useState<SlideLayout | null>(null); const [layout, setLayout] = useState<SlideLayout | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [viewMode, setViewMode] = useState<'preview' | 'template' | 'slots'>('preview'); const [viewMode, setViewMode] = useState<'template' | 'slots'>('slots');
useEffect(() => { useEffect(() => {
const loadThemeAndLayout = async () => { const loadThemeAndLayout = async () => {
@ -109,10 +108,10 @@ export const LayoutDetailPage: React.FC = () => {
<div className="view-mode-selector"> <div className="view-mode-selector">
<button <button
type="button" type="button"
className={`mode-button ${viewMode === 'preview' ? 'active' : ''}`} className={`mode-button ${viewMode === 'slots' ? 'active' : ''}`}
onClick={() => setViewMode('preview')} onClick={() => setViewMode('slots')}
> >
Preview Slots
</button> </button>
<button <button
type="button" type="button"
@ -121,25 +120,9 @@ export const LayoutDetailPage: React.FC = () => {
> >
Template Template
</button> </button>
<button
type="button"
className={`mode-button ${viewMode === 'slots' ? 'active' : ''}`}
onClick={() => setViewMode('slots')}
>
Slots
</button>
</div> </div>
<main className="layout-content"> <main className="layout-content">
{viewMode === 'preview' && (
<section className="preview-section">
<h2>Layout Preview</h2>
<div className="large-preview">
<LayoutPreview layout={layout} theme={theme} />
</div>
</section>
)}
{viewMode === 'template' && ( {viewMode === 'template' && (
<section className="template-section"> <section className="template-section">
<h2>HTML Template</h2> <h2>HTML Template</h2>

View File

@ -28,34 +28,6 @@ export const ThemeBrowser: React.FC = () => {
loadThemes(); loadThemes();
}, []); }, []);
// Listen for theme changes via HMR
useEffect(() => {
if (import.meta.hot) {
const handleThemeChange = (data: { filename: string; eventType: string; timestamp: number }) => {
console.log('Theme changed, reloading themes:', data);
// Clear theme cache and reload
setLoading(true);
setTimeout(async () => {
try {
const discoveredThemes = await getThemes(true); // Force cache bust
setThemes(discoveredThemes);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to reload themes');
} finally {
setLoading(false);
}
}, 100); // Small delay to ensure file is fully written
};
import.meta.hot.on('theme-changed', handleThemeChange);
return () => {
import.meta.hot?.off('theme-changed', handleThemeChange);
};
}
}, []);
const handleThemeClick = (theme: Theme) => { const handleThemeClick = (theme: Theme) => {
navigate(`/themes/${theme.id}`); navigate(`/themes/${theme.id}`);

View File

@ -130,8 +130,8 @@
.layout-card { .layout-card {
border: 1px solid #e5e7eb; border: 1px solid #e5e7eb;
border-radius: 0.75rem; border-radius: 0.75rem;
overflow: hidden;
background: white; background: white;
padding: 1.5rem;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
@ -141,23 +141,76 @@
} }
.layout-header { .layout-header {
padding: 1rem;
border-bottom: 1px solid #e5e7eb;
background: #f9fafb;
display: flex; display: flex;
justify-content: space-between; flex-direction: column;
align-items: center; gap: 1rem;
flex-wrap: wrap;
gap: 0.5rem;
} }
.layout-name { .layout-name {
margin: 0; margin: 0;
font-size: 1rem; font-size: 1.125rem;
font-weight: 600; font-weight: 600;
color: #1f2937; color: #1f2937;
} }
.layout-description {
margin: 0;
font-size: 0.875rem;
color: #6b7280;
line-height: 1.4;
}
.layout-info {
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}
.slot-types {
display: flex;
gap: 0.25rem;
flex-wrap: wrap;
}
.slot-type-badge {
font-size: 0.625rem;
font-weight: 500;
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
text-transform: capitalize;
}
.slot-type-title {
background-color: #fef3c7;
color: #92400e;
}
.slot-type-subtitle {
background-color: #e0e7ff;
color: #3730a3;
}
.slot-type-text {
background-color: #d1fae5;
color: #047857;
}
.slot-type-image {
background-color: #fce7f3;
color: #be185d;
}
.slot-type-video {
background-color: #ddd6fe;
color: #6b21a8;
}
.slot-type-list {
background-color: #fed7d7;
color: #c53030;
}
.layout-slots { .layout-slots {
font-size: 0.75rem; font-size: 0.75rem;
color: #6b7280; color: #6b7280;
@ -197,9 +250,6 @@
background-color: #bfdbfe; background-color: #bfdbfe;
} }
.layout-preview-container {
height: 200px;
}
/* Variables Section */ /* Variables Section */
.theme-variables { .theme-variables {

View File

@ -2,7 +2,6 @@ import React, { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom'; import { useParams, Link } from 'react-router-dom';
import type { Theme } from '../../types/theme'; import type { Theme } from '../../types/theme';
import { getTheme } from '../../themes'; import { getTheme } from '../../themes';
import { LayoutPreview } from './LayoutPreview';
import './ThemeDetailPage.css'; import './ThemeDetailPage.css';
export const ThemeDetailPage: React.FC = () => { export const ThemeDetailPage: React.FC = () => {
@ -113,7 +112,17 @@ export const ThemeDetailPage: React.FC = () => {
<div key={layout.id} className="layout-card"> <div key={layout.id} className="layout-card">
<div className="layout-header"> <div className="layout-header">
<h3 className="layout-name">{layout.name}</h3> <h3 className="layout-name">{layout.name}</h3>
<span className="layout-slots">{layout.slots.length} slots</span> <p className="layout-description">{layout.description}</p>
<div className="layout-info">
<span className="layout-slots">{layout.slots.length} slots</span>
<div className="slot-types">
{Array.from(new Set(layout.slots.map(slot => slot.type))).map(type => (
<span key={type} className={`slot-type-badge slot-type-${type}`}>
{type}
</span>
))}
</div>
</div>
<div className="layout-actions"> <div className="layout-actions">
<Link <Link
to={`/themes/${theme.id}/layouts/${layout.id}`} to={`/themes/${theme.id}/layouts/${layout.id}`}
@ -129,9 +138,6 @@ export const ThemeDetailPage: React.FC = () => {
</Link> </Link>
</div> </div>
</div> </div>
<div className="layout-preview-container">
<LayoutPreview layout={layout} theme={theme} />
</div>
</div> </div>
))} ))}
</div> </div>

View File

@ -21,23 +21,23 @@ export function themeWatcherPlugin(): Plugin {
if (filename && (filename.endsWith('.css') || filename.endsWith('.html'))) { if (filename && (filename.endsWith('.css') || filename.endsWith('.html'))) {
console.log(`Theme file changed: ${filename}`); console.log(`Theme file changed: ${filename}`);
// Invalidate theme cache and send HMR update // For theme files, we need a full reload since they affect:
// - Dynamically loaded CSS files
// - Theme manifest caching
// - Layout template caching
// - CSS variables that might be applied globally
// Send full reload command to browser
server.ws.send({
type: 'full-reload'
});
// Also invalidate theme cache for good measure
const moduleId = '/src/utils/themeLoader.ts'; const moduleId = '/src/utils/themeLoader.ts';
const module = server.moduleGraph.getModuleById(moduleId); const module = server.moduleGraph.getModuleById(moduleId);
if (module) { if (module) {
server.reloadModule(module); server.reloadModule(module);
} }
// Send custom HMR event to notify components
server.ws.send({
type: 'custom',
event: 'theme-changed',
data: {
filename,
eventType,
timestamp: Date.now()
}
});
} }
} }
); );

View File

@ -1,5 +1,28 @@
import type { SlideLayout, SlotConfig } from '../types/theme'; import type { SlideLayout, SlotConfig } from '../types/theme';
/**
* Creates a simple SVG pattern for image slots
*/
const createImageSVG = (slotName: string, width: number = 400, height: number = 300): string => {
const encodedSVG = encodeURIComponent(`
<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
<defs>
<pattern id="grid" width="20" height="20" patternUnits="userSpaceOnUse">
<path d="M 20 0 L 0 0 0 20" fill="none" stroke="#e2e8f0" stroke-width="1"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="#f8fafc"/>
<rect width="100%" height="100%" fill="url(#grid)"/>
<rect x="2" y="2" width="${width-4}" height="${height-4}" fill="none" stroke="#cbd5e1" stroke-width="2" stroke-dasharray="8,4"/>
<text x="50%" y="50%" text-anchor="middle" dominant-baseline="middle"
font-family="system-ui, sans-serif" font-size="16" font-weight="500" fill="#64748b">
${slotName}
</text>
</svg>
`);
return `data:image/svg+xml,${encodedSVG}`;
};
/** /**
* Generates sample data for a slot based on its configuration * Generates sample data for a slot based on its configuration
*/ */
@ -11,51 +34,37 @@ export const generateSampleDataForSlot = (slot: SlotConfig): string => {
return defaultContent; return defaultContent;
} }
// Clean up slot name for display
const slotDisplayName = id.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
// Generate sample data based on slot type // Generate sample data based on slot type
switch (type) { switch (type) {
case 'title':
if (id.includes('presentation') || id.includes('main')) {
return 'Sample Presentation Title';
}
return 'Slide Title Example';
case 'subtitle':
if (id.includes('presentation') || id.includes('main')) {
return 'A compelling subtitle for your presentation';
}
return 'Slide subtitle or description';
case 'text':
if (id.includes('content') || id.includes('body')) {
return `This is sample content for the ${id} slot. It demonstrates how text will appear in this layout with multiple sentences to show text flow and formatting.`;
}
return `Sample ${id} text content`;
case 'image': case 'image':
return 'https://via.placeholder.com/400x300/e2e8f0/64748b?text=Sample+Image'; return createImageSVG(slotDisplayName);
case 'title':
case 'subtitle':
case 'text':
case 'heading':
return slotDisplayName;
case 'video': case 'video':
return 'https://www.example.com/sample-video.mp4'; return createImageSVG(`${slotDisplayName} (Video)`, 640, 360);
case 'audio': case 'audio':
return 'https://www.example.com/sample-audio.mp3'; return `[${slotDisplayName} Audio]`;
case 'list': case 'list':
return '• First item example\n• Second item example\n• Third item example'; return `${slotDisplayName} Item 1\n• ${slotDisplayName} Item 2\n• ${slotDisplayName} Item 3`;
case 'code': case 'code':
return `function example() {\n console.log("Sample code");\n return true;\n}`; return `// ${slotDisplayName}\nfunction ${id.replace(/-/g, '')}() {\n return "${slotDisplayName}";\n}`;
case 'table': case 'table':
return 'Header 1 | Header 2\n--- | ---\nData 1 | Data 2\nData 3 | Data 4'; return `${slotDisplayName} Col 1 | ${slotDisplayName} Col 2\n--- | ---\nRow 1 Data | Row 1 Data\nRow 2 Data | Row 2 Data`;
default: default:
// Use placeholder as fallback, or generate from slot ID return slotDisplayName;
if (placeholder && placeholder !== `Enter ${id}`) {
return placeholder.replace(/^(Enter|Add|Click to add)\s*/i, 'Sample ');
}
return `Sample ${id.replace(/-/g, ' ')} content`;
} }
}; };