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:
parent
15d3789bb4
commit
98958b2cb3
@ -11,5 +11,5 @@
|
||||
"hasMasterSlide": true
|
||||
}
|
||||
},
|
||||
"generated": "2025-08-20T18:06:52.256Z"
|
||||
"generated": "2025-08-20T18:50:35.588Z"
|
||||
}
|
@ -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>
|
@ -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 */
|
||||
|
@ -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>
|
||||
|
@ -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}`);
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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()
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user