## New Feature: Full Screen Slide Preview - Add SlidePreviewModal component for full screen slide preview in SlideEditor - ESC key support and temporary hint for user guidance - Proper aspect ratio handling with theme CSS inheritance - Modal follows existing UI patterns for consistency ## Import Standards Compliance (31 files updated) - Fix all imports to use explicit .tsx/.ts extensions per IMPORT_STANDARDS.md - Eliminate barrel imports in App.tsx for better Vite tree shaking - Add direct imports with explicit paths across entire codebase - Preserve CSS imports and external library imports unchanged ## Code Architecture Improvements - Add comprehensive CSS & Component Architecture Guidelines to CLAUDE.md - Document modal patterns, aspect ratio handling, and CSS reuse principles - Reference all coding standards files for consistent development workflow - Prevent future CSS overcomplication and component duplication ## Performance Optimizations - Enable Vite tree shaking with proper import structure - Improve module resolution speed with explicit extensions - Optimize build performance through direct imports ## Files Changed - 31 TypeScript/React files with import fixes - 2 new SlidePreviewModal files (component + CSS) - Updated project documentation and coding guidelines - Fixed aspect ratio CSS patterns across components 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
193 lines
6.4 KiB
TypeScript
193 lines
6.4 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
||
import { useParams, Link } from 'react-router-dom';
|
||
import type { Theme, SlideLayout } from '../../types/theme.ts';
|
||
import { getTheme } from '../../themes/index.ts';
|
||
import './LayoutDetailPage.css';
|
||
|
||
export const LayoutDetailPage: React.FC = () => {
|
||
const { themeId, layoutId } = useParams<{ themeId: string; layoutId: string }>();
|
||
const [theme, setTheme] = useState<Theme | null>(null);
|
||
const [layout, setLayout] = useState<SlideLayout | null>(null);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [viewMode, setViewMode] = useState<'template' | 'slots'>('slots');
|
||
|
||
useEffect(() => {
|
||
const loadThemeAndLayout = async () => {
|
||
if (!themeId || !layoutId) {
|
||
setError('Missing theme ID or layout ID');
|
||
setLoading(false);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
setLoading(true);
|
||
const themeData = await getTheme(themeId);
|
||
if (!themeData) {
|
||
setError(`Theme "${themeId}" not found`);
|
||
return;
|
||
}
|
||
|
||
const layoutData = themeData.layouts.find((l: SlideLayout) => l.id === layoutId);
|
||
if (!layoutData) {
|
||
setError(`Layout "${layoutId}" not found in theme "${themeId}"`);
|
||
return;
|
||
}
|
||
|
||
setTheme(themeData);
|
||
setLayout(layoutData);
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : 'Failed to load theme and layout');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
loadThemeAndLayout();
|
||
}, [themeId, layoutId]);
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="layout-detail-page">
|
||
<div className="loading-spinner">Loading layout...</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (error) {
|
||
return (
|
||
<div className="layout-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 || !layout) {
|
||
return (
|
||
<div className="layout-detail-page">
|
||
<div className="not-found">
|
||
<h2>Layout Not Found</h2>
|
||
<p>The requested layout could not be found.</p>
|
||
<Link to="/themes" className="back-link">← Back to Themes</Link>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="layout-detail-page">
|
||
<header className="layout-detail-header">
|
||
<nav className="breadcrumb">
|
||
<Link to="/themes">Themes</Link>
|
||
<span className="separator">›</span>
|
||
<Link to={`/themes/${theme.id}`}>{theme.name}</Link>
|
||
<span className="separator">›</span>
|
||
<span className="current">{layout.name}</span>
|
||
</nav>
|
||
|
||
<div className="layout-info">
|
||
<h1 className="layout-name">{layout.name}</h1>
|
||
<p className="layout-description">{layout.description}</p>
|
||
<div className="layout-stats">
|
||
<span className="slot-count">{layout.slots.length} slots</span>
|
||
<span className="theme-name">from {theme.name}</span>
|
||
<Link
|
||
to={`/themes/${theme.id}/layouts/${layout.id}/preview`}
|
||
className="preview-link"
|
||
>
|
||
View Full Preview →
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<div className="view-mode-selector">
|
||
<button
|
||
type="button"
|
||
className={`mode-button ${viewMode === 'slots' ? 'active' : ''}`}
|
||
onClick={() => setViewMode('slots')}
|
||
>
|
||
Slots
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className={`mode-button ${viewMode === 'template' ? 'active' : ''}`}
|
||
onClick={() => setViewMode('template')}
|
||
>
|
||
Template
|
||
</button>
|
||
</div>
|
||
|
||
<main className="layout-content">
|
||
{viewMode === 'template' && (
|
||
<section className="template-section">
|
||
<h2>HTML Template</h2>
|
||
<div className="template-code">
|
||
<pre><code>{layout.htmlTemplate}</code></pre>
|
||
</div>
|
||
</section>
|
||
)}
|
||
|
||
{viewMode === 'slots' && (
|
||
<section className="slots-section">
|
||
<h2>Slot Configuration</h2>
|
||
<div className="slots-grid">
|
||
{layout.slots.map((slot) => (
|
||
<div key={slot.id} className="slot-card">
|
||
<div className="slot-header">
|
||
<h3 className="slot-id">{slot.id}</h3>
|
||
<span className={`slot-type ${slot.type}`}>{slot.type}</span>
|
||
{slot.required && <span className="required-badge">Required</span>}
|
||
</div>
|
||
|
||
<div className="slot-details">
|
||
{slot.placeholder && (
|
||
<div className="slot-field">
|
||
<strong>Placeholder:</strong> {slot.placeholder}
|
||
</div>
|
||
)}
|
||
|
||
{slot.defaultContent && (
|
||
<div className="slot-field">
|
||
<strong>Default Content:</strong> {slot.defaultContent}
|
||
</div>
|
||
)}
|
||
|
||
{slot.className && (
|
||
<div className="slot-field">
|
||
<strong>CSS Class:</strong> <code>{slot.className}</code>
|
||
</div>
|
||
)}
|
||
|
||
{slot.style && (
|
||
<div className="slot-field">
|
||
<strong>Inline Style:</strong> <code>{slot.style}</code>
|
||
</div>
|
||
)}
|
||
|
||
{slot.attributes && Object.keys(slot.attributes).length > 0 && (
|
||
<div className="slot-field">
|
||
<strong>Attributes:</strong>
|
||
<div className="attributes-list">
|
||
{Object.entries(slot.attributes).map(([key, value]) => (
|
||
<div key={key} className="attribute-item">
|
||
<code>{key}="{value}"</code>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</section>
|
||
)}
|
||
</main>
|
||
</div>
|
||
);
|
||
}; |