slideshare/src/components/themes/ThemeBrowser.tsx
Michael Mainguy 98958b2cb3 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>
2025-08-20 14:11:51 -05:00

207 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import type { Theme } from '../../types/theme';
import { getThemes } from '../../themes';
import { LayoutPreview } from './LayoutPreview';
export const ThemeBrowser: React.FC = () => {
const navigate = useNavigate();
const [themes, setThemes] = useState<Theme[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [expandedTheme, setExpandedTheme] = useState<string | null>(null);
const [showLayoutPreviews, setShowLayoutPreviews] = useState<Record<string, boolean>>({});
useEffect(() => {
const loadThemes = async () => {
try {
setLoading(true);
const discoveredThemes = await getThemes();
setThemes(discoveredThemes);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load themes');
} finally {
setLoading(false);
}
};
loadThemes();
}, []);
const handleThemeClick = (theme: Theme) => {
navigate(`/themes/${theme.id}`);
};
const handleThemeExpand = (themeId: string) => {
setExpandedTheme(expandedTheme === themeId ? null : themeId);
};
const handleLayoutPreviewsToggle = (themeId: string) => {
setShowLayoutPreviews(prev => ({
...prev,
[themeId]: !prev[themeId]
}));
};
if (loading) {
return (
<div className="theme-browser loading">
<div className="loading-spinner">Loading themes...</div>
</div>
);
}
if (error) {
return (
<div className="theme-browser error">
<div className="error-message">
<h3>Error loading themes</h3>
<p>{error}</p>
</div>
</div>
);
}
if (themes.length === 0) {
return (
<div className="theme-browser empty">
<div className="empty-state">
<h3>No themes found</h3>
<p>No themes were discovered. Check your themes directory.</p>
</div>
</div>
);
}
return (
<div className="theme-browser">
<header className="theme-browser-header">
<h2>Available Themes</h2>
<span className="theme-count">{themes.length} theme{themes.length !== 1 ? 's' : ''}</span>
</header>
<div className="theme-list">
{themes.map((theme) => (
<div
key={theme.id}
className="theme-card"
onClick={() => handleThemeClick(theme)}
>
<div className="theme-header">
<div className="theme-info">
<h3 className="theme-name">{theme.name}</h3>
<p className="theme-description">{theme.description}</p>
{theme.author && (
<span className="theme-author">by {theme.author}</span>
)}
{theme.version && (
<span className="theme-version">v{theme.version}</span>
)}
</div>
<div className="theme-actions">
<button
type="button"
className="expand-button"
onClick={(e) => {
e.stopPropagation();
handleThemeExpand(theme.id);
}}
aria-label={`${expandedTheme === theme.id ? 'Collapse' : 'Expand'} ${theme.name}`}
>
{expandedTheme === theme.id ? '' : '+'}
</button>
</div>
</div>
{theme.variables && Object.keys(theme.variables).length > 0 && (
<div className="theme-preview">
<div className="color-palette">
{Object.entries(theme.variables)
.filter(([_, value]) => value.startsWith('#') || value.includes('rgb') || value.includes('hsl'))
.slice(0, 6) // Show max 6 color swatches to avoid overcrowding
.map(([key, value]) => (
<div
key={key}
className="color-swatch"
style={{ backgroundColor: value }}
title={`--${key}: ${value}`}
></div>
))}
</div>
</div>
)}
{expandedTheme === theme.id && (
<div className="theme-details">
<div className="theme-layouts">
<h4>
Layouts ({theme.layouts.length})
<button
type="button"
className={`layouts-toggle ${showLayoutPreviews[theme.id] ? 'active' : ''}`}
onClick={() => handleLayoutPreviewsToggle(theme.id)}
>
{showLayoutPreviews[theme.id] ? 'Hide Previews' : 'Show Previews'}
</button>
</h4>
{showLayoutPreviews[theme.id] ? (
<div className="layout-previews-grid">
{theme.layouts.map((layout) => (
<LayoutPreview
key={layout.id}
layout={layout}
theme={theme}
/>
))}
</div>
) : (
<div className="layout-list">
{theme.layouts.map((layout) => (
<div key={layout.id} className="layout-item">
<span className="layout-name">{layout.name}</span>
<span className="layout-slots">
{layout.slots.length} slot{layout.slots.length !== 1 ? 's' : ''}
</span>
</div>
))}
</div>
)}
</div>
{theme.variables && (
<div className="theme-variables">
<h4>CSS Variables</h4>
<div className="variable-list">
{Object.entries(theme.variables).map(([key, value]) => (
<div key={key} className="variable-item">
<code className="variable-name">--{key}</code>
<span className="variable-value">{value}</span>
</div>
))}
</div>
</div>
)}
<div className="theme-meta">
<div className="meta-item">
<strong>Path:</strong> {theme.basePath}
</div>
<div className="meta-item">
<strong>CSS File:</strong> {theme.cssFile}
</div>
{theme.masterSlideTemplate && (
<div className="meta-item">
<strong>Master Slide:</strong> Yes
</div>
)}
</div>
</div>
)}
</div>
))}
</div>
</div>
);
};