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
}
},
"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>
{{title}}
</h1>
<h2 class="slot subtitle-slot" data-slot="subtitle" data-placeholder="Subtitle or tagline">
{{subtitle}}
</h2>
<div class="slot diagram-slot" data-slot="diagram" data-placeholder="Diagram or Image" data-required>
{{diagram}}
</div>
<h3>Static Content</h3>
</div>

View File

@ -126,6 +126,8 @@
.layout-title-slide .slot[data-slot="title"] {
font-size: clamp(2rem, 5vw, 4rem);
margin-bottom: 2rem;
width: 80%;
color: var(--theme-primary);
}
@ -189,4 +191,4 @@
.slot.empty::before {
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 type { Theme, SlideLayout } from '../../types/theme';
import { getTheme } from '../../themes';
import { LayoutPreview } from './LayoutPreview';
import './LayoutDetailPage.css';
export const LayoutDetailPage: React.FC = () => {
@ -11,7 +10,7 @@ export const LayoutDetailPage: React.FC = () => {
const [layout, setLayout] = useState<SlideLayout | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [viewMode, setViewMode] = useState<'preview' | 'template' | 'slots'>('preview');
const [viewMode, setViewMode] = useState<'template' | 'slots'>('slots');
useEffect(() => {
const loadThemeAndLayout = async () => {
@ -109,10 +108,10 @@ export const LayoutDetailPage: React.FC = () => {
<div className="view-mode-selector">
<button
type="button"
className={`mode-button ${viewMode === 'preview' ? 'active' : ''}`}
onClick={() => setViewMode('preview')}
className={`mode-button ${viewMode === 'slots' ? 'active' : ''}`}
onClick={() => setViewMode('slots')}
>
Preview
Slots
</button>
<button
type="button"
@ -121,25 +120,9 @@ export const LayoutDetailPage: React.FC = () => {
>
Template
</button>
<button
type="button"
className={`mode-button ${viewMode === 'slots' ? 'active' : ''}`}
onClick={() => setViewMode('slots')}
>
Slots
</button>
</div>
<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' && (
<section className="template-section">
<h2>HTML Template</h2>

View File

@ -28,34 +28,6 @@ export const ThemeBrowser: React.FC = () => {
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) => {
navigate(`/themes/${theme.id}`);

View File

@ -130,8 +130,8 @@
.layout-card {
border: 1px solid #e5e7eb;
border-radius: 0.75rem;
overflow: hidden;
background: white;
padding: 1.5rem;
transition: all 0.2s ease;
}
@ -141,23 +141,76 @@
}
.layout-header {
padding: 1rem;
border-bottom: 1px solid #e5e7eb;
background: #f9fafb;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
flex-direction: column;
gap: 1rem;
}
.layout-name {
margin: 0;
font-size: 1rem;
font-size: 1.125rem;
font-weight: 600;
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 {
font-size: 0.75rem;
color: #6b7280;
@ -197,9 +250,6 @@
background-color: #bfdbfe;
}
.layout-preview-container {
height: 200px;
}
/* Variables Section */
.theme-variables {

View File

@ -2,7 +2,6 @@ import React, { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import type { Theme } from '../../types/theme';
import { getTheme } from '../../themes';
import { LayoutPreview } from './LayoutPreview';
import './ThemeDetailPage.css';
export const ThemeDetailPage: React.FC = () => {
@ -113,7 +112,17 @@ export const ThemeDetailPage: React.FC = () => {
<div key={layout.id} className="layout-card">
<div className="layout-header">
<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">
<Link
to={`/themes/${theme.id}/layouts/${layout.id}`}
@ -129,9 +138,6 @@ export const ThemeDetailPage: React.FC = () => {
</Link>
</div>
</div>
<div className="layout-preview-container">
<LayoutPreview layout={layout} theme={theme} />
</div>
</div>
))}
</div>

View File

@ -21,23 +21,23 @@ export function themeWatcherPlugin(): Plugin {
if (filename && (filename.endsWith('.css') || filename.endsWith('.html'))) {
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 module = server.moduleGraph.getModuleById(moduleId);
if (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';
/**
* 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
*/
@ -11,51 +34,37 @@ export const generateSampleDataForSlot = (slot: SlotConfig): string => {
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
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':
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':
return 'https://www.example.com/sample-video.mp4';
return createImageSVG(`${slotDisplayName} (Video)`, 640, 360);
case 'audio':
return 'https://www.example.com/sample-audio.mp3';
return `[${slotDisplayName} Audio]`;
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':
return `function example() {\n console.log("Sample code");\n return true;\n}`;
return `// ${slotDisplayName}\nfunction ${id.replace(/-/g, '')}() {\n return "${slotDisplayName}";\n}`;
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:
// Use placeholder as fallback, or generate from slot ID
if (placeholder && placeholder !== `Enter ${id}`) {
return placeholder.replace(/^(Enter|Add|Click to add)\s*/i, 'Sample ');
}
return `Sample ${id.replace(/-/g, ' ')} content`;
return slotDisplayName;
}
};