Add full-screen layout preview route and fix iframe sandbox issue

- Create LayoutPreviewPage component for full-screen layout previews
- Add preview route /themes/:themeId/layouts/:layoutId/preview to App routing
- Update theme components with preview links and improved navigation
- Fix iframe sandbox error by adding allow-scripts permission
- Enhance template renderer with layout metadata support
- Replace PostCSS with regex-only CSS parsing for browser compatibility
- Add comprehensive standards documentation for code quality
- Clean up CSS slot indicators to be always visible with descriptions

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Michael Mainguy 2025-08-20 13:48:13 -05:00
parent 3f1c299850
commit 15d3789bb4
23 changed files with 1172 additions and 564 deletions

View File

@ -50,12 +50,10 @@
- Theme structure should not repeat the same information in multiple places
- Theme structure should be flexible and allow for custom themes to be created using only html templates and css
- Metadata for themes should be stored as comments in the html and css files
- React compoonents and code should be modular and reusable and abide by [REACT19.md](REACT19.md)
- We should refer to [VITEBESTPRACTICES.md](VITEBESTPRACTICES.md) for best practices in Vite
-
# General Claude Guidelines
- Don't run npm commands, just tell me what to run and I'll run them myself
- When building componentns, refer to https://www.vite.dev
# Architecture
## Themes
- Themes should be stored in a directory structure under public directory that allows for easy access and modification by browser

240
ERROR_HANDLING_STANDARDS.md Normal file
View File

@ -0,0 +1,240 @@
# Error Handling Standards
## Current State: INCONSISTENT
Error handling varies across the codebase - some functions have comprehensive error handling while others are missing it entirely.
## Required Error Handling Patterns
### 1. Error Boundaries (MISSING)
Implement React error boundaries for component-level error handling:
```typescript
// ✅ IMPLEMENT - ThemeErrorBoundary.tsx
import React, { Component, ErrorInfo, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
export class ThemeErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Theme component error:', error, errorInfo);
// Log to error reporting service in production
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<div className="error-fallback">
<h2>Something went wrong</h2>
<p>There was an error loading the theme system.</p>
<button onClick={() => window.location.reload()}>
Reload Page
</button>
</div>
);
}
return this.props.children;
}
}
```
### 2. Async Error Handling (STANDARDIZE)
Consistent pattern for all async operations:
```typescript
// ✅ STANDARD PATTERN
const performAsyncOperation = async (id: string) => {
try {
setLoading(true);
setError(null);
const result = await riskyOperation(id);
if (!result) {
throw new Error(`Operation failed for ${id}`);
}
return result;
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error occurred';
setError(message);
console.error(`Async operation failed:`, error);
throw error; // Re-throw if caller needs to handle
} finally {
setLoading(false);
}
};
```
### 3. Hook Error Handling (IMPLEMENT)
Custom hook for consistent error states:
```typescript
// ✅ IMPLEMENT - useAsyncOperation.ts
import { useState, useCallback } from 'react';
interface AsyncState<T> {
data: T | null;
loading: boolean;
error: string | null;
}
export function useAsyncOperation<T>() {
const [state, setState] = useState<AsyncState<T>>({
data: null,
loading: false,
error: null
});
const execute = useCallback(async (operation: () => Promise<T>) => {
setState(prev => ({ ...prev, loading: true, error: null }));
try {
const data = await operation();
setState({ data, loading: false, error: null });
return data;
} catch (error) {
const message = error instanceof Error ? error.message : 'Operation failed';
setState(prev => ({ ...prev, loading: false, error: message }));
throw error;
}
}, []);
return { ...state, execute };
}
```
### 4. Network Error Handling (ENHANCE)
Standardized fetch error handling:
```typescript
// ✅ STANDARD PATTERN
const fetchWithErrorHandling = async (url: string, options?: RequestInit) => {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response;
} catch (error) {
if (error instanceof TypeError) {
throw new Error('Network error - check your connection');
}
throw error;
}
};
```
## Error Types & Classifications
### 1. User Errors (Recoverable)
- Invalid theme selection
- Missing form data
- File upload issues
**Handling**: Show user-friendly message, allow retry
### 2. System Errors (Technical)
- Network failures
- Parse errors
- Configuration issues
**Handling**: Log details, show generic message, provide fallback
### 3. Critical Errors (Unrecoverable)
- Dependency failures
- Memory issues
- Corrupt data
**Handling**: Error boundary, full page reload option
## Logging Standards
### Development
```typescript
// ✅ PATTERN
console.group(`🔥 ${operation} Error`);
console.error('Details:', error);
console.error('Context:', context);
console.groupEnd();
```
### Production
```typescript
// ✅ PATTERN (when error service added)
errorReportingService.captureException(error, {
context,
user: getCurrentUser(),
timestamp: new Date().toISOString()
});
```
## Error Message Standards
### User-Facing Messages
- **Clear & Actionable**: "Could not load theme. Please try again."
- **No technical jargon**: Avoid error codes, stack traces
- **Suggest solutions**: "Check your connection and retry"
### Developer Messages
- **Detailed context**: Include operation, parameters, state
- **Stack traces**: Preserve for debugging
- **Structured data**: Consistent error object format
## Implementation Checklist
### HIGH PRIORITY
- [ ] Add ThemeErrorBoundary to App.tsx
- [ ] Implement useAsyncOperation hook
- [ ] Standardize theme loading error handling
- [ ] Add network error handling to fetch operations
### MEDIUM PRIORITY
- [ ] Create error fallback components
- [ ] Add error recovery mechanisms (retry buttons)
- [ ] Implement error state consistency across components
- [ ] Add loading state management standards
### LOW PRIORITY
- [ ] Add error reporting service integration
- [ ] Implement error analytics tracking
- [ ] Add error rate monitoring
- [ ] Create error handling documentation
## Testing Requirements
- **Error boundary testing**: Verify fallback rendering
- **Async error testing**: Test network failures, timeouts
- **Recovery testing**: Ensure retry mechanisms work
- **Error state testing**: Verify error messages display correctly
## Current Problem Areas
### Missing Error Boundaries
No React error boundaries implemented - component errors crash the app
### Inconsistent Async Patterns
Some async operations handle errors, others don't:
- `themeLoader.ts` ✅ Has good error handling
- Some components ❌ Missing error handling
### No Error Recovery
Users cannot recover from errors - no retry buttons or reload options
### Logging Inconsistency
Mix of `console.warn`, `console.error`, and no logging

26
IMPORT_STANDARDS.md Normal file
View File

@ -0,0 +1,26 @@
# Import Standards
## Direct Imports Required
- **NEVER use barrel files** for component imports (violates Vite best practices)
- **ALWAYS use direct file imports** with explicit extensions for better Vite performance
- **AVOID** `import { Component } from './components'`
- **USE** `import { Component } from './components/Component.tsx'`
## Examples
```typescript
// ❌ BAD - Barrel file usage
import { ThemeBrowser, ThemeDetailPage } from './components/themes'
// ✅ GOOD - Direct imports
import { ThemeBrowser } from './components/themes/ThemeBrowser.tsx'
import { ThemeDetailPage } from './components/themes/ThemeDetailPage.tsx'
```
## Performance Impact
- Barrel files prevent Vite's tree shaking optimization
- Direct imports enable faster module resolution
- Explicit extensions reduce lookup overhead
## Exceptions
- CSS imports don't require extensions: `import './Component.css'`
- Type-only imports: `import type { Theme } from './types/theme.ts'`

View File

@ -1,311 +0,0 @@
# React 19 Guidelines & Best Practices
*Based on [React 19 Release Blog](https://react.dev/blog/2024/12/05/react-19)*
## Core Philosophy
**"React 19 provides powerful tools to create more responsive, efficient web applications with simplified state management and rendering strategies."**
## Key New Features
### 1. Actions
Actions simplify data mutations and state updates by automatically managing:
- **Pending states** - know when operations are in progress
- **Optimistic updates** - update UI before server confirms
- **Error handling** - graceful error recovery
- **Form submissions** - streamlined form processing
```jsx
// Example: Using Actions for form submission
function UpdateName({ name, updateName }) {
const [isPending, startTransition] = useTransition();
return (
<form action={updateName}>
<input name="name" defaultValue={name} />
<button type="submit" disabled={isPending}>
{isPending ? 'Updating...' : 'Update'}
</button>
</form>
);
}
```
### 2. New Hooks
#### `useActionState`
Simplifies action handling with built-in state management:
```jsx
function MyComponent() {
const [state, formAction] = useActionState(actionFunction, initialState);
return (
<form action={formAction}>
{/* Form elements */}
</form>
);
}
```
#### `useOptimistic`
Enables optimistic UI updates for responsive user experience:
```jsx
function TodoList({ todos, addTodo }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo) => [...state, { ...newTodo, sending: true }]
);
async function formAction(formData) {
addOptimisticTodo({ name: formData.get("name") });
await addTodo(formData);
}
return (
<form action={formAction}>
{optimisticTodos.map(todo => (
<div key={todo.id} className={todo.sending ? "sending" : ""}>
{todo.name}
</div>
))}
</form>
);
}
```
#### `useFormStatus`
Provides form submission status:
```jsx
function SubmitButton() {
const { pending, data, method, action } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Submitting...' : 'Submit'}
</button>
);
}
```
#### `use`
Reads resources during rendering:
```jsx
function Profile({ userPromise }) {
const user = use(userPromise);
return <h1>{user.name}</h1>;
}
```
### 3. Improved Component Rendering
#### `ref` as a Prop
Function components can now receive `ref` as a regular prop:
```jsx
function MyInput({ placeholder, ref }) {
return <input placeholder={placeholder} ref={ref} />;
}
// Usage
<MyInput ref={inputRef} placeholder="Enter text" />
```
#### Direct Context Rendering
Context can be rendered directly without `.Provider`:
```jsx
// Before React 19
<ThemeContext.Provider value={theme}>
<App />
</ThemeContext.Provider>
// React 19
<ThemeContext value={theme}>
<App />
</ThemeContext>
```
#### Cleanup Functions for Refs
```jsx
function MyComponent() {
const ref = useCallback((node) => {
if (node) {
// Setup
node.focus();
// Cleanup function
return () => {
node.blur();
};
}
}, []);
return <input ref={ref} />;
}
```
#### Metadata Support
Components can render metadata tags directly:
```jsx
function BlogPost({ post }) {
return (
<article>
<title>{post.title}</title>
<meta name="description" content={post.excerpt} />
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
```
### 4. Performance Enhancements
#### Resource Preloading
```jsx
import { preload, preinit } from 'react-dom';
// Preload resources
preload('/api/user', { as: 'fetch' });
preinit('/styles.css', { as: 'style' });
```
#### Better Hydration Error Reporting
- More detailed error messages
- Better debugging information
- Improved compatibility with third-party scripts
### 5. Server Components
#### Server-Side Rendering
```jsx
// Server Component
async function BlogPost({ slug }) {
const post = await fetchPost(slug);
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
```
#### Server Actions
```jsx
// actions.js
'use server'
export async function updateUser(formData) {
const name = formData.get('name');
// Update user on server
await database.updateUser({ name });
}
// Component
import { updateUser } from './actions.js';
function UserForm() {
return (
<form action={updateUser}>
<input name="name" />
<button type="submit">Update</button>
</form>
);
}
```
## Best Practices for Our Project
### 1. Forms and Data Management
- **Use Actions** for theme selection and configuration
- **Implement useOptimistic** for responsive theme preview
- **Leverage useFormStatus** for loading states
### 2. Component Design
- **Use ref as prop** for theme slot components
- **Implement cleanup functions** for theme CSS loading
- **Render metadata** for theme preview information
### 3. Performance
- **Preload theme assets** using new preloading APIs
- **Use Server Components** for theme discovery if server-side rendering is added
- **Implement error boundaries** with improved error handling
### 4. State Management
- **Use useActionState** for complex theme operations
- **Implement optimistic updates** for theme switching
- **Leverage new Context syntax** for theme providers
## Implementation Guidelines
### Theme Selection with Actions
```jsx
function ThemeBrowser({ themes }) {
const [selectedTheme, selectTheme] = useActionState(
async (currentState, formData) => {
const themeId = formData.get('themeId');
const theme = await loadTheme(themeId);
return theme;
},
null
);
return (
<form>
{themes.map(theme => (
<button
key={theme.id}
formAction={() => selectTheme(new FormData([['themeId', theme.id]]))}
>
{theme.name}
</button>
))}
</form>
);
}
```
### Optimistic Theme Switching
```jsx
function ThemePreview({ currentTheme, onThemeChange }) {
const [optimisticTheme, setOptimisticTheme] = useOptimistic(
currentTheme,
(state, newTheme) => newTheme
);
async function switchTheme(newTheme) {
setOptimisticTheme(newTheme);
await onThemeChange(newTheme);
}
return (
<div className={`theme-preview theme-${optimisticTheme.id}`}>
<h3>{optimisticTheme.name}</h3>
{/* Theme preview content */}
</div>
);
}
```
### Theme Context (New Syntax)
```jsx
function App() {
const [theme, setTheme] = useState(defaultTheme);
return (
<ThemeContext value={{ theme, setTheme }}>
<ThemeBrowser />
<SlideEditor />
</ThemeContext>
);
}
```
## Key Takeaways
1. **Embrace Actions** - Simplify form handling and state management
2. **Use Optimistic Updates** - Create responsive user interfaces
3. **Leverage New Hooks** - Reduce boilerplate code
4. **Improve Performance** - Utilize preloading and better error handling
5. **Simplify Context** - Use direct rendering without `.Provider`
6. **Handle Refs Better** - Pass refs as regular props to function components

112
REACT19_IMPLEMENTATION.md Normal file
View File

@ -0,0 +1,112 @@
# React 19 Implementation Standards
## Current Status: NOT IMPLEMENTED
The codebase uses React 19.1.1 but implements ZERO React 19 features. This is a significant gap.
This might be OK if the features aren't needed, are complex or costly, or there is a better way that
doesn't violate react standard approaches.
## Required React 19 Features to Implement
### 1. Actions for Form Handling
Replace manual form state management with React 19 Actions:
```typescript
// ❌ CURRENT - Manual state management
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const handleSubmit = async (e) => {
setLoading(true);
try {
await submitForm(e);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
// ✅ REACT 19 - Actions
const [state, submitAction] = useActionState(async (prevState, formData) => {
// Action automatically handles loading/error states
return await submitForm(formData);
}, initialState);
```
### 2. useOptimistic for Theme Previews
Implement optimistic updates for theme switching:
```typescript
// ✅ IMPLEMENT
function ThemePreview({ currentTheme }) {
const [optimisticTheme, setOptimisticTheme] = useOptimistic(
currentTheme,
(currentTheme, optimisticValue) => ({ ...currentTheme, ...optimisticValue })
);
const switchTheme = async (newTheme) => {
setOptimisticTheme(newTheme); // Immediate UI update
await saveTheme(newTheme); // Actual save
};
}
```
### 3. Modern Context Syntax
Replace Provider wrapper with direct Context rendering:
```typescript
// ❌ OLD
<ThemeContext.Provider value={themeValue}>
<App />
</ThemeContext.Provider>
// ✅ REACT 19
<ThemeContext value={themeValue}>
<App />
</ThemeContext>
```
### 4. Error Boundaries with Actions
Implement error boundaries that work with Actions:
```typescript
// ✅ IMPLEMENT
class ThemeErrorBoundary extends React.Component {
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// Log to error reporting service
console.error('Theme loading error:', error, errorInfo);
}
}
```
## Implementation Priority
### HIGH PRIORITY
1. **Theme selection Actions** - Replace manual form handling
2. **Error boundaries** - Add proper error handling
3. **useOptimistic for theme previews** - Better UX
### MEDIUM PRIORITY
1. **Context syntax modernization** - Update provider patterns
2. **Form Actions for theme creation** - When that feature is added
3. **Server Actions integration** - If backend is added
### LOW PRIORITY
1. **Preloading optimization** - Use React 19 preloading APIs
2. **Streaming SSR** - If SSR is implemented
3. **Advanced concurrent features** - As needed
## Migration Strategy
1. Start with theme selection Actions (highest impact)
2. Add error boundaries for robustness
3. Implement optimistic updates for better UX
4. Gradually modernize Context usage
5. Add advanced features as needed
## Resources
- [React 19 Actions Documentation](https://react.dev/reference/react/useActionState)
- [useOptimistic Hook](https://react.dev/reference/react/useOptimistic)
- [Error Boundaries](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary)

View File

@ -1,137 +0,0 @@
# Vite Best Practices & Performance Guide
*Based on [Vite Performance Documentation](https://vite.dev/guide/performance)*
## Core Performance Principle
**"Reduce the amount of work for source files (JS/TS/CSS)"** to maintain performance as projects grow.
## Browser Setup
### Development Environment
- **Create a dev-only browser profile** without extensions
- **Use incognito mode** for faster performance
- **Don't disable browser cache** while using dev tools (it significantly slows down requests and HMR)
## Plugin Management
### Best Practices
- **Be cautious with community plugins** - they may impact performance
- **Dynamically import large dependencies** instead of importing them statically
- **Avoid long-running operations** in plugin hooks
- **Minimize file transformation time**
## Import and Resolve Optimization
### File Extensions
- **Be explicit with import paths** - use `import './Component.jsx'` instead of `import './Component'`
- **Narrow down `resolve.extensions` list** - don't include unnecessary file types
- Use common extensions first: `['.js', '.ts', '.jsx', '.tsx']`
### Module Imports
- **Avoid barrel files** that re-export multiple modules from index files
- **Import individual APIs directly** instead of from index files
- Example: Use `import { debounce } from 'lodash-es/debounce'` instead of `import { debounce } from 'lodash-es'`
## File Transformation
### Warmup Strategy
- **Use `server.warmup`** for frequently used files
- Pre-transform commonly accessed modules during dev server startup
### Native Tooling
- **Use native tooling when possible** for better performance
- Consider these alternatives:
- **Rolldown** instead of Rollup
- **LightningCSS** for CSS processing
- **`@vitejs/plugin-react-swc`** for React projects
## CSS and Styling
### CSS Optimization
- **Use CSS instead of preprocessors** when possible (native CSS is faster)
- **Import SVGs as strings/URLs** rather than components when appropriate
- **Minimize unnecessary transformations**
## Performance Profiling
### Diagnostic Tools
- **Use `vite --profile`** to generate performance profiles
- **Use `vite --debug plugin-transform`** to inspect transformation times
- **Use tools like speedscope** to analyze bottlenecks
### Monitoring
- Track build times and identify slow transformations
- Monitor bundle sizes and chunk splitting effectiveness
- Profile HMR performance during development
## Import Strategies
### Dynamic Imports
```typescript
// Good: Dynamic import for large dependencies
const heavyLibrary = await import('heavy-library');
// Bad: Static import of large library
import heavyLibrary from 'heavy-library';
```
### Explicit Extensions
```typescript
// Good: Explicit file extension
import Component from './Component.jsx';
// Bad: Missing extension (requires resolution)
import Component from './Component';
```
### Direct API Imports
```typescript
// Good: Direct import
import { debounce } from 'lodash-es/debounce';
// Bad: Barrel import (imports entire library)
import { debounce } from 'lodash-es';
```
## Configuration Recommendations
### Resolve Extensions (in order of frequency)
```typescript
export default {
resolve: {
extensions: ['.js', '.ts', '.jsx', '.tsx', '.json']
}
}
```
### Server Warmup
```typescript
export default {
server: {
warmup: {
clientFiles: ['./src/components/**/*.tsx', './src/utils/**/*.ts']
}
}
}
```
## Development vs Production
### Development Focus
- Fast HMR and dev server startup
- Minimal transformations
- Browser-friendly module resolution
### Production Focus
- Optimized bundle sizes
- Tree shaking effectiveness
- Code splitting strategies
## Key Takeaways
1. **Minimize transformations** - every transformation adds overhead
2. **Be explicit** - help Vite resolve files faster with explicit paths
3. **Use native tools** - they're typically faster than JavaScript alternatives
4. **Profile regularly** - identify and fix performance bottlenecks early
5. **Optimize imports** - direct imports are faster than barrel imports
6. **Consider browser setup** - development environment affects performance significantly

131
VITE_PERFORMANCE.md Normal file
View File

@ -0,0 +1,131 @@
# Vite Performance Standards
## Missing Configuration
The current `vite.config.ts` is missing critical performance optimizations.
## Required Vite Configuration Updates
### 1. Resolve Extensions (CRITICAL)
```typescript
// ADD to vite.config.ts
export default defineConfig({
resolve: {
extensions: ['.js', '.ts', '.jsx', '.tsx', '.json']
}
});
```
**Impact**: Faster module resolution, explicit extension requirements
### 2. Server Warmup (HIGH IMPACT)
```typescript
// ADD to vite.config.ts
export default defineConfig({
server: {
warmup: {
clientFiles: [
'./src/components/**/*.tsx',
'./src/utils/**/*.ts',
'./src/types/**/*.ts',
'./src/themes/**/*.ts'
]
}
}
});
```
**Impact**: Faster initial load times, better dev experience
### 3. Build Optimizations
```typescript
// ADD to vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
router: ['react-router-dom'],
themes: ['./src/utils/themeLoader', './src/utils/cssParser']
}
}
}
}
});
```
**Impact**: Better code splitting, improved cache utilization
### 4. Dependency Pre-bundling
```typescript
// ADD to vite.config.ts
export default defineConfig({
optimizeDeps: {
include: ['react', 'react-dom', 'react-router-dom'],
exclude: ['@vite/client', '@vite/env']
}
});
```
**Impact**: Faster cold starts, consistent dependency handling
## Current Performance Issues
### 1. Barrel File Usage
**Problem**: Two barrel files present
- `src/components/themes/index.ts`
- `src/themes/index.ts`
**Impact**: Prevents tree-shaking, slower builds
**Solution**: Remove barrel files, use direct imports
### 2. Missing File Extensions
**Problem**: Import statements lack explicit extensions
```typescript
// ❌ CURRENT
import App from './App.tsx'
import { getThemes } from '../../themes'
// ✅ SHOULD BE
import App from './App.tsx'
import { getThemes } from '../../themes/index.ts'
```
### 3. No Warmup Configuration
**Problem**: Cold start performance is suboptimal
**Solution**: Configure server warmup for frequently accessed files
## Implementation Priority
### IMMEDIATE (Critical Performance Impact)
1. **Remove barrel file imports** - Replace with direct imports
2. **Add resolve extensions** - Configure explicit extension handling
3. **Configure server warmup** - Improve dev server startup
### SHORT TERM (Build Performance)
1. **Add manual chunk configuration** - Optimize bundle splitting
2. **Configure dependency pre-bundling** - Improve cold start times
3. **Add explicit file extensions** - All import statements
### LONG TERM (Advanced Optimizations)
1. **Dynamic imports for themes** - Code splitting for theme system
2. **Service worker integration** - Cache theme assets
3. **Module federation** - If theme system becomes shared
## Monitoring Standards
- **Bundle size analysis**: Use `npm run build -- --analyze`
- **Dev server startup time**: Should be < 1 second after warmup
- **Hot reload performance**: Changes should reflect in < 200ms
- **Build time targets**: Production build < 30 seconds
## File Size Targets
- **Main bundle**: < 200KB gzipped
- **Theme chunks**: < 50KB each
- **CSS bundles**: < 100KB total
- **Asset optimization**: Images < 500KB each
## Implementation Checklist
- [ ] Remove barrel file exports
- [ ] Add resolve extensions configuration
- [ ] Configure server warmup paths
- [ ] Add manual chunk configuration
- [ ] Configure dependency pre-bundling
- [ ] Add explicit import extensions
- [ ] Set up bundle analysis
- [ ] Monitor performance metrics

View File

@ -11,5 +11,5 @@
"hasMasterSlide": true
}
},
"generated": "2025-08-20T15:25:28.729Z"
"generated": "2025-08-20T18:06:52.256Z"
}

View File

@ -2,6 +2,9 @@
<h1 class="slot title-slot" data-slot="title" data-placeholder="Slide Title" data-required>
{{title}}
</h1>
<h1 class="slot subtitle-slot" data-slot="subtitle" data-placeholder="Slide Subtitle" data-required>
{{subtitle}}
</h1>
<div class="slot content-area" data-slot="content" data-placeholder="Your content here..." data-multiline="true">
{{content}}
</div>

View File

@ -1,5 +1,5 @@
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import { ThemeBrowser, ThemeDetailPage, LayoutDetailPage } from './components/themes'
import { ThemeBrowser, ThemeDetailPage, LayoutDetailPage, LayoutPreviewPage } from './components/themes'
import { AppHeader } from './components/AppHeader'
import { Welcome } from './components/Welcome'
import './App.css'
@ -16,6 +16,7 @@ function App() {
<Route path="/themes" element={<ThemeBrowser />} />
<Route path="/themes/:themeId" element={<ThemeDetailPage />} />
<Route path="/themes/:themeId/layouts/:layoutId" element={<LayoutDetailPage />} />
<Route path="/themes/:themeId/layouts/:layoutId/preview" element={<LayoutPreviewPage />} />
</Routes>
</main>
</div>

View File

@ -0,0 +1,25 @@
/* Additional styles for LayoutDetailPage preview link */
.layout-stats {
display: flex;
align-items: center;
gap: 1rem;
margin-top: 0.5rem;
flex-wrap: wrap;
}
.preview-link {
color: #3b82f6;
text-decoration: none;
font-size: 0.875rem;
font-weight: 500;
padding: 0.375rem 0.75rem;
background: #dbeafe;
border-radius: 0.375rem;
transition: all 0.2s ease;
}
.preview-link:hover {
background: #bfdbfe;
text-decoration: none;
transform: translateY(-1px);
}

View File

@ -3,6 +3,7 @@ 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 = () => {
const { themeId, layoutId } = useParams<{ themeId: string; layoutId: string }>();
@ -95,6 +96,12 @@ export const LayoutDetailPage: React.FC = () => {
<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>

View File

@ -50,7 +50,7 @@ export const LayoutPreview: React.FC<LayoutPreviewProps> = ({
ref={iframeRef}
className="layout-preview-frame"
title={`Preview of ${layout.name}`}
sandbox="allow-same-origin"
sandbox="allow-same-origin allow-scripts"
/>
</div>

View File

@ -0,0 +1,372 @@
.layout-preview-page {
min-height: 100vh;
display: flex;
flex-direction: column;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
position: relative;
}
/* Header */
.preview-header {
background: white;
border-bottom: 1px solid #e5e7eb;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
.preview-nav {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
}
.nav-link {
color: #3b82f6;
text-decoration: none;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
transition: background-color 0.2s ease;
}
.nav-link:hover {
background-color: #eff6ff;
text-decoration: underline;
}
.nav-separator {
color: #9ca3af;
font-weight: 500;
}
.nav-current {
color: #6b7280;
font-weight: 500;
}
.preview-actions {
display: flex;
gap: 0.75rem;
}
.action-button {
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-size: 0.875rem;
font-weight: 500;
text-decoration: none;
border: none;
cursor: pointer;
transition: all 0.2s ease;
}
.action-button.primary {
background: #3b82f6;
color: white;
}
.action-button.primary:hover {
background: #2563eb;
}
.action-button.secondary {
background: #f3f4f6;
color: #374151;
border: 1px solid #d1d5db;
}
.action-button.secondary:hover {
background: #e5e7eb;
}
/* Main Content */
.preview-content {
flex: 1;
background: #f8fafc;
position: relative;
overflow: hidden;
}
.layout-description-banner {
position: fixed;
top: 10%;
left: calc(50% - 600px - 15rem);
width: 14rem;
background: white;
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
padding: 1rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
z-index: 15;
}
.layout-description-banner h3 {
margin: 0 0 0.5rem 0;
font-size: 1rem;
font-weight: 600;
color: #1f2937;
}
.layout-description-banner p {
margin: 0 0 1rem 0;
font-size: 0.875rem;
color: #6b7280;
line-height: 1.4;
}
.slot-count-badge {
background: #dbeafe;
color: #1e40af;
font-size: 0.75rem;
font-weight: 500;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
display: inline-block;
}
.layout-rendered-content {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
border-radius: 0.5rem;
min-height: 600px;
min-width: 800px;
max-width: 1200px;
padding: 2rem;
z-index: 10;
max-height: 80vh;
overflow-y: auto;
}
/* Make slot indicators always visible with blue dotted borders */
.layout-rendered-content [data-slot] {
border: 2px dotted #3b82f6 !important;
position: relative;
min-height: 1.5rem;
padding: 0.5rem;
}
/* Add slot labels */
.layout-rendered-content [data-slot]::before {
content: "Slot: " attr(data-slot) " (" attr(data-type) ")";
position: absolute;
top: -1.5rem;
left: 0;
background: #3b82f6;
color: white;
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
z-index: 1;
font-weight: 500;
}
/* Ensure slot content is visible */
.layout-rendered-content [data-slot]:empty::after {
content: "[Empty slot - " attr(data-slot) "]";
color: #9ca3af;
font-style: italic;
font-size: 0.875rem;
}
.close-preview-button {
position: fixed;
top: 10%;
right: calc(50% - 600px - 3rem);
width: 3rem;
height: 3rem;
background: #ef4444;
color: white;
border: none;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
font-weight: bold;
cursor: pointer;
z-index: 20;
text-decoration: none;
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
transition: all 0.2s ease;
}
.close-preview-button:hover {
background: #dc2626;
transform: scale(1.05);
box-shadow: 0 6px 16px rgba(239, 68, 68, 0.4);
}
.close-preview-button:active {
transform: scale(0.95);
}
/* Footer */
.preview-footer {
background: white;
border-top: 1px solid #e5e7eb;
padding: 1rem 2rem;
flex-shrink: 0;
}
.preview-info {
display: flex;
gap: 2rem;
justify-content: center;
font-size: 0.75rem;
color: #6b7280;
}
.preview-info span {
display: flex;
align-items: center;
}
.theme-info::before {
content: "🎨";
margin-right: 0.5rem;
}
.layout-info::before {
content: "📐";
margin-right: 0.5rem;
}
.slots-info::before {
content: "🔧";
margin-right: 0.5rem;
}
/* Loading and Error States */
.layout-preview-page.loading,
.layout-preview-page.error,
.layout-preview-page.not-found {
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
}
.loading-content,
.error-content,
.not-found-content {
max-width: 500px;
padding: 2rem;
}
.loading-spinner {
font-size: 1.125rem;
color: #6b7280;
margin-bottom: 1.5rem;
}
.error-content h2,
.not-found-content h2 {
color: #ef4444;
margin-bottom: 1rem;
}
.back-link {
display: inline-block;
margin-top: 1rem;
color: #3b82f6;
text-decoration: none;
font-size: 0.875rem;
}
.back-link:hover {
text-decoration: underline;
}
/* Print Styles */
@media print {
.preview-header,
.preview-footer {
display: none;
}
.preview-content {
padding: 0;
background: white;
}
.layout-rendered-content {
box-shadow: none;
max-width: none;
width: 100%;
}
}
/* Responsive */
@media (max-width: 1024px) {
.layout-rendered-content {
min-width: 90vw;
max-width: 90vw;
min-height: 500px;
padding: 1.5rem;
}
.close-preview-button {
right: 5%;
top: 12%;
}
.layout-description-banner {
left: 5%;
width: 12rem;
}
}
@media (max-width: 768px) {
.preview-header {
padding: 1rem;
flex-direction: column;
gap: 1rem;
align-items: stretch;
}
.preview-nav {
justify-content: center;
}
.preview-actions {
justify-content: center;
}
.layout-rendered-content {
min-width: 95vw;
max-width: 95vw;
min-height: 400px;
padding: 1rem;
border-radius: 0.25rem;
}
.close-preview-button {
right: 2.5%;
top: 15%;
width: 2.5rem;
height: 2.5rem;
font-size: 1rem;
}
.layout-description-banner {
position: relative;
left: auto;
top: auto;
width: 100%;
margin: 1rem;
transform: none;
}
.preview-info {
flex-direction: column;
gap: 0.5rem;
align-items: center;
}
}

View File

@ -0,0 +1,175 @@
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 { renderTemplateWithSampleData } from '../../utils/templateRenderer';
import './LayoutPreviewPage.css';
export const LayoutPreviewPage: 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 [renderedContent, setRenderedContent] = useState<string>('');
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);
// Render the template with sample data
const rendered = renderTemplateWithSampleData(layoutData.htmlTemplate, layoutData);
setRenderedContent(rendered);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load theme and layout');
} finally {
setLoading(false);
}
};
loadThemeAndLayout();
}, [themeId, layoutId]);
useEffect(() => {
if (theme) {
// Dynamically load theme CSS
const themeStyleId = 'theme-preview-style';
let existingStyle = document.getElementById(themeStyleId);
if (existingStyle) {
existingStyle.remove();
}
const link = document.createElement('link');
link.id = themeStyleId;
link.rel = 'stylesheet';
link.href = `${theme.basePath}/${theme.cssFile}`;
document.head.appendChild(link);
// Cleanup function
return () => {
const styleToRemove = document.getElementById(themeStyleId);
if (styleToRemove) {
styleToRemove.remove();
}
};
}
}, [theme]);
if (loading) {
return (
<div className="layout-preview-page loading">
<div className="loading-content">
<div className="loading-spinner">Loading preview...</div>
<Link to="/themes" className="back-link"> Back to Themes</Link>
</div>
</div>
);
}
if (error) {
return (
<div className="layout-preview-page error">
<div className="error-content">
<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-preview-page not-found">
<div className="not-found-content">
<h2>Preview Not Found</h2>
<p>The requested layout preview could not be found.</p>
<Link to="/themes" className="back-link"> Back to Themes</Link>
</div>
</div>
);
}
return (
<div className="layout-preview-page">
<header className="preview-header">
<nav className="preview-nav">
<Link to="/themes" className="nav-link">Themes</Link>
<span className="nav-separator"></span>
<Link to={`/themes/${theme.id}`} className="nav-link">{theme.name}</Link>
<span className="nav-separator"></span>
<Link to={`/themes/${theme.id}/layouts/${layout.id}`} className="nav-link">{layout.name}</Link>
<span className="nav-separator"></span>
<span className="nav-current">Preview</span>
</nav>
<div className="preview-actions">
<Link
to={`/themes/${theme.id}/layouts/${layout.id}`}
className="action-button secondary"
>
Layout Details
</Link>
<button
type="button"
className="action-button primary"
onClick={() => window.print()}
>
Print
</button>
</div>
</header>
<main className="preview-content">
<div className="layout-description-banner">
<h3>{layout.name} Layout</h3>
<p>{layout.description}</p>
<div className="slot-count-badge">
{layout.slots.length} slot{layout.slots.length !== 1 ? 's' : ''}
</div>
</div>
<div
className="layout-rendered-content"
dangerouslySetInnerHTML={{ __html: renderedContent }}
/>
<Link
to={`/themes/${theme.id}/layouts/${layout.id}`}
className="close-preview-button"
title="Close Preview"
>
</Link>
</main>
<footer className="preview-footer">
<div className="preview-info">
<span className="theme-info">Theme: {theme.name}</span>
<span className="layout-info">Layout: {layout.name}</span>
<span className="slots-info">{layout.slots.length} slots</span>
</div>
</footer>
</div>
);
};

View File

@ -147,6 +147,8 @@
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
}
.layout-name {
@ -164,17 +166,37 @@
border-radius: 0.25rem;
}
.layout-detail-link {
color: #3b82f6;
text-decoration: none;
font-size: 0.875rem;
font-weight: 500;
.layout-actions {
display: flex;
gap: 0.5rem;
}
.layout-detail-link:hover {
.layout-detail-link,
.layout-preview-link {
color: #3b82f6;
text-decoration: none;
font-size: 0.75rem;
font-weight: 500;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
transition: background-color 0.2s ease;
}
.layout-detail-link:hover,
.layout-preview-link:hover {
background-color: #eff6ff;
text-decoration: underline;
}
.layout-preview-link {
background-color: #dbeafe;
color: #1e40af;
}
.layout-preview-link:hover {
background-color: #bfdbfe;
}
.layout-preview-container {
height: 200px;
}

View File

@ -114,12 +114,20 @@ export const ThemeDetailPage: React.FC = () => {
<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 className="layout-actions">
<Link
to={`/themes/${theme.id}/layouts/${layout.id}`}
className="layout-detail-link"
>
Details
</Link>
<Link
to={`/themes/${theme.id}/layouts/${layout.id}/preview`}
className="layout-preview-link"
>
Preview
</Link>
</div>
</div>
<div className="layout-preview-container">
<LayoutPreview layout={layout} theme={theme} />

View File

@ -2,4 +2,5 @@ export { ThemeBrowser } from './ThemeBrowser';
export { LayoutPreview } from './LayoutPreview';
export { ThemeDetailPage } from './ThemeDetailPage';
export { LayoutDetailPage } from './LayoutDetailPage';
export { LayoutPreviewPage } from './LayoutPreviewPage';
export type { Theme } from '../../types/theme';

View File

@ -1,6 +1,4 @@
import type { Plugin } from 'vite';
import { watch } from 'fs';
import path from 'path';
/**
* Vite plugin to watch theme files in public directory and trigger HMR
@ -8,11 +6,15 @@ import path from 'path';
export function themeWatcherPlugin(): Plugin {
return {
name: 'theme-watcher',
configureServer(server) {
async configureServer(server) {
// Dynamic imports for Node.js modules in server context
const fs = await import('fs');
const path = await import('path');
const publicThemesPath = path.resolve(process.cwd(), 'public/themes');
// Watch for changes in the themes directory
const watcher = watch(
const watcher = fs.watch(
publicThemesPath,
{ recursive: true },
(eventType: string, filename: string | null) => {

View File

@ -1,5 +1,3 @@
import postcss from 'postcss';
export interface CSSVariables {
[key: string]: string;
}
@ -13,44 +11,18 @@ export interface ThemeMetadata {
}
/**
* Parses CSS variables from CSS text content using PostCSS
* Parses CSS variables from CSS text content using regex
*/
export const parseCSSVariables = (cssContent: string): CSSVariables => {
const variables: CSSVariables = {};
try {
const ast = postcss.parse(cssContent);
// Find :root rule and extract custom properties
ast.walkRules(':root', (rule) => {
rule.walkDecls((decl) => {
if (decl.prop.startsWith('--')) {
const name = decl.prop.substring(2); // Remove '--' prefix
variables[name] = decl.value.trim();
}
});
});
} catch (error) {
console.warn('Error parsing CSS with PostCSS:', error);
// Fallback to regex parsing if PostCSS fails
return parseCSSVariablesRegex(cssContent);
}
// Parse :root rules and extract CSS variables
const rootRegex = /:root\s*\{([^}]+)\}/gs;
let rootMatch;
return variables;
};
/**
* Fallback regex-based CSS variable parsing
*/
const parseCSSVariablesRegex = (cssContent: string): CSSVariables => {
const variables: CSSVariables = {};
const rootRegex = /:root\s*\{([^}]+)\}/g;
const variableRegex = /--([^:]+):\s*([^;]+);/g;
const rootMatch = rootRegex.exec(cssContent);
if (rootMatch) {
while ((rootMatch = rootRegex.exec(cssContent)) !== null) {
const rootContent = rootMatch[1];
const variableRegex = /--([^:]+):\s*([^;]+);/g;
let variableMatch;
while ((variableMatch = variableRegex.exec(rootContent)) !== null) {
@ -62,6 +34,7 @@ const parseCSSVariablesRegex = (cssContent: string): CSSVariables => {
return variables;
};
/**
* Loads CSS file and extracts variables
*/
@ -96,65 +69,9 @@ export const setCSSVariable = (variableName: string, value: string, element?: HT
};
/**
* Parses theme metadata from CSS comment block using PostCSS
* Parses theme metadata from CSS comment block using regex
*/
export const parseThemeMetadata = (cssContent: string): ThemeMetadata | null => {
try {
const ast = postcss.parse(cssContent);
const metadata: Partial<ThemeMetadata> = {};
// Find the first comment that contains theme metadata
ast.walkComments((comment) => {
if (!metadata.id) { // Only process the first metadata comment
const commentContent = comment.text;
// Parse each line in the comment
const lines = commentContent.split('\n');
for (const line of lines) {
const cleanLine = line.replace(/^\s*\*\s?/, '').trim();
if (cleanLine.includes(':')) {
const [key, ...valueParts] = cleanLine.split(':');
const value = valueParts.join(':').trim();
switch (key.toLowerCase().trim()) {
case 'theme':
metadata.id = value.toLowerCase().replace(/\s+/g, '-');
break;
case 'name':
metadata.name = value;
break;
case 'description':
metadata.description = value;
break;
case 'author':
metadata.author = value;
break;
case 'version':
metadata.version = value;
break;
}
}
}
}
});
// Ensure required fields
if (!metadata.id || !metadata.name || !metadata.description) {
return null;
}
return metadata as ThemeMetadata;
} catch (error) {
console.warn('Error parsing CSS metadata with PostCSS:', error);
// Fallback to regex parsing
return parseThemeMetadataRegex(cssContent);
}
};
/**
* Fallback regex-based metadata parsing
*/
const parseThemeMetadataRegex = (cssContent: string): ThemeMetadata | null => {
const commentRegex = /\/\*\s*([\s\S]*?)\s*\*\//;
const match = commentRegex.exec(cssContent);
@ -199,6 +116,7 @@ const parseThemeMetadataRegex = (cssContent: string): ThemeMetadata | null => {
return metadata as ThemeMetadata;
};
/**
* Gets all CSS variables from computed styles
*/

View File

@ -79,6 +79,10 @@ export const generateSampleDataForLayout = (layout: SlideLayout): Record<string,
sampleData.author = 'Sample Author';
sampleData.date = new Date().toLocaleDateString();
// Add layout metadata for templates that might use it
sampleData['layout-description'] = layout.description || `This is the ${layout.name} layout`;
sampleData['layout-name'] = layout.name;
return sampleData;
};

View File

@ -200,9 +200,11 @@ const extractSlotsFromTemplate = (htmlTemplate: string) => {
});
// Clean up empty attributes
if (Object.keys(slotConfig.attributes!).length === 0) {
/*if (Object.keys(slotConfig.attributes!).length === 0) {
delete (slotConfig as any).attributes;
}
*/
slots.push(slotConfig);
});

View File

@ -8,10 +8,19 @@ export default defineConfig({
react(),
themeWatcherPlugin()
],
define: {
global: 'globalThis',
process: { env: {} }
},
optimizeDeps: {
exclude: ['fs', 'path']
},
server: {
watch: {
// Watch public directory for theme changes
ignored: ['!**/public/themes/**']
}
}
},
// Ensure static assets are served correctly
publicDir: 'public'
})