- 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>
207 lines
7.1 KiB
TypeScript
207 lines
7.1 KiB
TypeScript
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>
|
||
);
|
||
}; |