- Add React Router with theme browser, theme detail, and layout detail pages - Implement manifest-based theme discovery for better performance - Add Welcome component as home page with feature overview - Fix layout and styling issues with proper CSS centering - Implement introspective theme browsing (dynamically discover colors/variables) - Add layout preview system with iframe scaling - Create comprehensive theme detail page with color palette display - Fix TypeScript errors and build issues - Remove hardcoded theme assumptions in favor of dynamic discovery 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
162 lines
5.2 KiB
TypeScript
162 lines
5.2 KiB
TypeScript
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 = () => {
|
|
const { themeId } = useParams<{ themeId: string }>();
|
|
const [theme, setTheme] = useState<Theme | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
const loadTheme = async () => {
|
|
if (!themeId) {
|
|
setError('No theme ID provided');
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setLoading(true);
|
|
const themeData = await getTheme(themeId);
|
|
if (!themeData) {
|
|
setError(`Theme "${themeId}" not found`);
|
|
return;
|
|
}
|
|
setTheme(themeData);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to load theme');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
loadTheme();
|
|
}, [themeId]);
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="theme-detail-page">
|
|
<div className="loading-spinner">Loading theme...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="theme-detail-page">
|
|
<div className="error-message">
|
|
<h2>Error</h2>
|
|
<p>{error}</p>
|
|
<Link to="/themes" className="back-link">← Back to Themes</Link>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!theme) {
|
|
return (
|
|
<div className="theme-detail-page">
|
|
<div className="not-found">
|
|
<h2>Theme Not Found</h2>
|
|
<p>The requested theme could not be found.</p>
|
|
<Link to="/themes" className="back-link">← Back to Themes</Link>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="theme-detail-page">
|
|
<header className="theme-detail-header">
|
|
<Link to="/themes" className="back-link">← Back to Themes</Link>
|
|
<div className="theme-info">
|
|
<h1 className="theme-name">{theme.name}</h1>
|
|
<p className="theme-description">{theme.description}</p>
|
|
<div className="theme-meta">
|
|
{theme.author && <span className="theme-author">by {theme.author}</span>}
|
|
{theme.version && <span className="theme-version">v{theme.version}</span>}
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{theme.variables && Object.keys(theme.variables).length > 0 && (
|
|
<section className="theme-colors">
|
|
<h2>Color Palette</h2>
|
|
<div className="color-palette-large">
|
|
{Object.entries(theme.variables)
|
|
.filter(([_, value]) => value.startsWith('#') || value.includes('rgb') || value.includes('hsl'))
|
|
.map(([key, value]) => (
|
|
<div key={key} className="color-swatch-large">
|
|
<div
|
|
className="color-preview"
|
|
style={{ backgroundColor: value }}
|
|
title={`--${key}: ${value}`}
|
|
></div>
|
|
<div className="color-details">
|
|
<div className="color-name">--{key}</div>
|
|
<div className="color-value">{value}</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
<section className="theme-layouts">
|
|
<h2>Available Layouts ({theme.layouts.length})</h2>
|
|
<div className="layouts-grid">
|
|
{theme.layouts.map((layout) => (
|
|
<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>
|
|
<Link
|
|
to={`/themes/${theme.id}/layouts/${layout.id}`}
|
|
className="layout-detail-link"
|
|
>
|
|
View Details →
|
|
</Link>
|
|
</div>
|
|
<div className="layout-preview-container">
|
|
<LayoutPreview layout={layout} theme={theme} />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
{theme.variables && (
|
|
<section className="theme-variables">
|
|
<h2>CSS Variables</h2>
|
|
<div className="variables-grid">
|
|
{Object.entries(theme.variables).map(([key, value]) => (
|
|
<div key={key} className="variable-card">
|
|
<code className="variable-name">--{key}</code>
|
|
<span className="variable-value">{value}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
<section className="theme-technical">
|
|
<h2>Technical Details</h2>
|
|
<div className="tech-details">
|
|
<div className="tech-item">
|
|
<strong>Path:</strong> <code>{theme.basePath}</code>
|
|
</div>
|
|
<div className="tech-item">
|
|
<strong>CSS File:</strong> <code>{theme.cssFile}</code>
|
|
</div>
|
|
<div className="tech-item">
|
|
<strong>Master Slide:</strong> {theme.masterSlideTemplate ? 'Yes' : 'No'}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
);
|
|
}; |