From 98958b2cb3851f8d86203cb1f8060156d95e481a Mon Sep 17 00:00:00 2001 From: Michael Mainguy Date: Wed, 20 Aug 2025 14:11:51 -0500 Subject: [PATCH] Remove iframe previews and improve theme hot reload with SVG demo content MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- public/themes-manifest.json | 2 +- .../themes/default/layouts/title-slide.html | 7 +- public/themes/default/style.css | 4 +- src/components/themes/LayoutDetailPage.tsx | 25 +------ src/components/themes/ThemeBrowser.tsx | 28 ------- src/components/themes/ThemeDetailPage.css | 74 ++++++++++++++++--- src/components/themes/ThemeDetailPage.tsx | 16 ++-- src/plugins/themeWatcher.ts | 24 +++--- src/utils/templateRenderer.ts | 69 +++++++++-------- 9 files changed, 136 insertions(+), 113 deletions(-) diff --git a/public/themes-manifest.json b/public/themes-manifest.json index 4d94958..ce5c747 100644 --- a/public/themes-manifest.json +++ b/public/themes-manifest.json @@ -11,5 +11,5 @@ "hasMasterSlide": true } }, - "generated": "2025-08-20T18:06:52.256Z" + "generated": "2025-08-20T18:50:35.588Z" } \ No newline at end of file diff --git a/public/themes/default/layouts/title-slide.html b/public/themes/default/layouts/title-slide.html index 753aa3f..4d124bc 100644 --- a/public/themes/default/layouts/title-slide.html +++ b/public/themes/default/layouts/title-slide.html @@ -2,7 +2,8 @@

{{title}}

-

- {{subtitle}} -

+
+ {{diagram}} +
+

Static Content

\ No newline at end of file diff --git a/public/themes/default/style.css b/public/themes/default/style.css index 16b0d5a..8e19167 100644 --- a/public/themes/default/style.css +++ b/public/themes/default/style.css @@ -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; } -} \ No newline at end of file +}/* Test change Wed Aug 20 13:55:27 CDT 2025 */ diff --git a/src/components/themes/LayoutDetailPage.tsx b/src/components/themes/LayoutDetailPage.tsx index 344f399..fda4790 100644 --- a/src/components/themes/LayoutDetailPage.tsx +++ b/src/components/themes/LayoutDetailPage.tsx @@ -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(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(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 = () => {
-
- {viewMode === 'preview' && ( -
-

Layout Preview

-
- -
-
- )} - {viewMode === 'template' && (

HTML Template

diff --git a/src/components/themes/ThemeBrowser.tsx b/src/components/themes/ThemeBrowser.tsx index 9c68b56..2df7e76 100644 --- a/src/components/themes/ThemeBrowser.tsx +++ b/src/components/themes/ThemeBrowser.tsx @@ -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}`); diff --git a/src/components/themes/ThemeDetailPage.css b/src/components/themes/ThemeDetailPage.css index 474395d..4395951 100644 --- a/src/components/themes/ThemeDetailPage.css +++ b/src/components/themes/ThemeDetailPage.css @@ -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 { diff --git a/src/components/themes/ThemeDetailPage.tsx b/src/components/themes/ThemeDetailPage.tsx index 3c3d30f..861e06e 100644 --- a/src/components/themes/ThemeDetailPage.tsx +++ b/src/components/themes/ThemeDetailPage.tsx @@ -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 = () => {

{layout.name}

- {layout.slots.length} slots +

{layout.description}

+
+ {layout.slots.length} slots +
+ {Array.from(new Set(layout.slots.map(slot => slot.type))).map(type => ( + + {type} + + ))} +
+
{
-
- -
))} diff --git a/src/plugins/themeWatcher.ts b/src/plugins/themeWatcher.ts index 4396d4e..b6f0ee2 100644 --- a/src/plugins/themeWatcher.ts +++ b/src/plugins/themeWatcher.ts @@ -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() - } - }); } } ); diff --git a/src/utils/templateRenderer.ts b/src/utils/templateRenderer.ts index 370c7c8..1bacf54 100644 --- a/src/utils/templateRenderer.ts +++ b/src/utils/templateRenderer.ts @@ -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(` + + + + + + + + + + + ${slotName} + + + `); + 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; } };