diff --git a/public/themes-manifest.json b/public/themes-manifest.json
index f8b4971..ab8303b 100644
--- a/public/themes-manifest.json
+++ b/public/themes-manifest.json
@@ -12,5 +12,5 @@
"hasMasterSlide": true
}
},
- "generated": "2025-08-21T18:19:19.993Z"
+ "generated": "2025-08-21T18:25:50.144Z"
}
\ No newline at end of file
diff --git a/public/themes/CLAUDE.md b/public/themes/CLAUDE.md
index e69de29..0494c5d 100644
--- a/public/themes/CLAUDE.md
+++ b/public/themes/CLAUDE.md
@@ -0,0 +1,360 @@
+# Theme Creation Guidelines
+
+## Theme Structure
+
+Each theme must follow this directory structure:
+```
+themes/
+ theme-name/
+ style.css # Theme styles and metadata
+ master-slide.html # Master slide template (optional)
+ layouts/ # Layout templates directory
+ layout1.html
+ layout2.html
+ ...
+```
+
+## CSS Theme Metadata
+
+Theme metadata MUST be included as comments at the top of `style.css`:
+
+```css
+/*
+ * Theme: [theme-id]
+ * Name: [Display Name]
+ * Description: [Theme description]
+ * Author: [Author Name] ([email])
+ * Version: [version]
+ */
+```
+
+## CSS Variables System
+
+### Required Theme Variables
+Define these CSS custom properties in `:root` for consistent theming:
+
+```css
+:root {
+ /* Colors */
+ --theme-primary: #color; /* Primary brand color */
+ --theme-secondary: #color; /* Secondary accent color */
+ --theme-accent: #color; /* Interactive accent color */
+ --theme-background: #color; /* Slide background */
+ --theme-text: #color; /* Primary text color */
+ --theme-text-secondary: #color; /* Secondary text color */
+
+ /* Typography */
+ --theme-font-heading: 'Font', fallback;
+ --theme-font-body: 'Font', fallback;
+ --theme-font-code: 'Font', fallback;
+
+ /* Layout */
+ --slide-padding: 5%; /* Slide edge padding */
+ --content-max-width: 90%; /* Max content width */
+}
+```
+
+### Required Base Slide Styling
+Always include this base styling that works with the global `.slide-container` classes:
+
+```css
+.slide-container .slide-content,
+.slide {
+ width: 100%;
+ height: 100%;
+ background: var(--theme-background);
+ color: var(--theme-text);
+ font-family: var(--theme-font-body);
+ padding: var(--slide-padding);
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+ position: relative;
+ overflow: hidden;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+}
+```
+
+## Layout HTML Templates
+
+### Required Layout Structure
+Each layout HTML file must:
+
+1. **Use semantic class naming**: `.layout-[layout-name]`
+2. **Include slot elements** for editable content
+3. **Use Handlebars syntax** for template variables
+
+### Slot System
+Slots are editable areas defined with specific data attributes:
+
+```html
+
+ {{slot-id}}
+
+```
+
+#### Slot Data Attributes:
+- `data-slot="[id]"`: Unique identifier for the slot
+- `data-placeholder="[text]"`: Placeholder text when empty
+- `data-required`: Mark slot as required (optional)
+- `data-multiline="true"`: Allow multiline text input (optional)
+- `data-accept="image/*"`: For image slots (optional)
+- `data-hidden="true"`: Hide from direct editing (optional)
+
+#### Common Slot Types:
+- **Title slots**: `data-slot="title"`
+- **Text content**: `data-slot="content"`
+- **Images**: `data-slot="image"` with `data-accept="image/*"`
+- **Subtitles**: `data-slot="subtitle"`
+
+### Layout CSS Naming Convention
+Style layouts using the pattern: `.layout-[layout-name]`
+
+```css
+.layout-my-layout,
+.slide-container .layout-my-layout {
+ /* Layout-specific styles */
+}
+
+.layout-my-layout .slot[data-slot="title"] {
+ /* Slot-specific styles */
+}
+```
+
+## Creating New Layouts
+
+### 1. Create Layout HTML Template
+Create `themes/[theme]/layouts/my-layout.html`:
+
+```html
+
+
+ {{title}}
+
+
+
+ {{content}}
+
+
+```
+
+### 2. Add Layout Styles to theme CSS
+Add to the theme's `style.css`:
+
+```css
+/* My Layout */
+.layout-my-layout,
+.slide-container .layout-my-layout {
+ justify-content: flex-start;
+ align-items: stretch;
+}
+
+.layout-my-layout .slot[data-slot="title"] {
+ font-size: clamp(1.5rem, 6vw, 2.5rem);
+ margin-bottom: 2rem;
+ text-align: center;
+}
+
+.layout-my-layout .slot[data-slot="content"] {
+ flex: 1;
+ font-size: clamp(1rem, 2.5vw, 1.25rem);
+ text-align: left;
+}
+```
+
+## Standard Slot Styles
+
+### Required Slot Base Styles
+```css
+.slot {
+ position: relative;
+ border: 2px dashed transparent;
+ min-height: 2rem;
+ transition: border-color 0.2s ease;
+ width: 100%;
+ max-width: var(--content-max-width);
+ margin: 0 auto;
+ text-align: inherit;
+}
+
+.slot:hover,
+.slot.editing {
+ border-color: var(--theme-accent);
+ border-radius: 4px;
+}
+
+.slot.empty {
+ border-color: var(--theme-secondary);
+ opacity: 0.5;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.slot.empty::before {
+ content: attr(data-placeholder);
+ color: var(--theme-text-secondary);
+ font-style: italic;
+}
+```
+
+### Slot Type Styles
+```css
+/* Text slots */
+.slot[data-type="title"] {
+ font-family: var(--theme-font-heading);
+ font-weight: bold;
+ line-height: 1.2;
+ text-align: center;
+}
+
+.slot[data-type="subtitle"] {
+ font-family: var(--theme-font-heading);
+ line-height: 1.4;
+ opacity: 0.8;
+ text-align: center;
+}
+
+.slot[data-type="text"] {
+ line-height: 1.6;
+ text-align: left;
+}
+
+/* Image slots */
+.slot[data-type="image"] {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: #f8fafc;
+ border-radius: 8px;
+}
+
+.slot[data-type="image"] img {
+ max-width: 100%;
+ max-height: 100%;
+ object-fit: contain;
+ border-radius: 4px;
+}
+```
+
+## Master Slide Templates
+
+Master slides provide unchangeable content that appears on all slides. Create `master-slide.html`:
+
+```html
+
+```
+
+Style master slide elements:
+```css
+.master-slide {
+ position: absolute;
+ z-index: 1;
+}
+
+.master-slide.footer {
+ bottom: 0;
+ left: 0;
+ right: 0;
+ padding: 1rem;
+ text-align: center;
+ font-size: 0.875rem;
+ opacity: 0.6;
+}
+```
+
+## Responsive Design
+
+### Aspect Ratio Support
+Include aspect ratio specific adjustments:
+
+```css
+.slide-container.aspect-16-9 .slide-content,
+.slide-container.aspect-16-9 .slide {
+ --slide-padding: 4%;
+ --content-max-width: 85%;
+}
+
+.slide-container.aspect-4-3 .slide-content,
+.slide-container.aspect-4-3 .slide {
+ --slide-padding: 6%;
+ --content-max-width: 80%;
+}
+```
+
+### Mobile Responsive
+Add mobile breakpoints:
+
+```css
+@media (max-width: 768px) {
+ :root {
+ --slide-padding: 3%;
+ --content-max-width: 95%;
+ }
+
+ .layout-my-layout .slot[data-slot="title"] {
+ font-size: clamp(1.2rem, 5vw, 2rem);
+ }
+}
+```
+
+## Print Support
+Include print styles for presentation export:
+
+```css
+@media print {
+ .slide {
+ page-break-after: always;
+ width: 100%;
+ height: 100vh;
+ }
+
+ .slot {
+ border: none !important;
+ }
+
+ .slot.empty::before {
+ display: none;
+ }
+}
+```
+
+## Best Practices
+
+1. **Use clamp() for responsive typography**: `font-size: clamp(min, preferred, max)`
+2. **Leverage CSS custom properties**: Makes themes easily customizable
+3. **Follow existing naming conventions**: `.layout-[name]`, `.slot[data-slot="name"]`
+4. **Test across aspect ratios**: Ensure layouts work in 16:9, 4:3, and 16:10
+5. **Consider accessibility**: Use sufficient color contrast and readable fonts
+6. **Use semantic HTML**: Proper heading hierarchy and meaningful class names
+7. **Keep layouts flexible**: Use flexbox/grid for responsive behavior
+
+## Template Variables (Handlebars)
+
+Use Handlebars syntax for dynamic content:
+- `{{variableName}}` - Simple variable
+- `{{#if condition}}...{{/if}}` - Conditional rendering
+- `{{#image}}...{{/image}}` - Check if image exists
+
+Common variables:
+- `{{title}}` - Slide title
+- `{{content}}` - Main content
+- `{{image}}` - Image source
+- `{{imageAlt}}` - Image alt text
+- `{{footerText}}` - Footer text
\ No newline at end of file
diff --git a/src/App.tsx b/src/App.tsx
index 639809d..d046a6a 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -4,7 +4,6 @@ import { ThemeDetailPage } from './components/themes/ThemeDetailPage.tsx';
import { LayoutDetailPage } from './components/themes/LayoutDetailPage.tsx';
import { LayoutPreviewPage } from './components/themes/LayoutPreviewPage.tsx';
import { NewPresentationPage } from './components/presentations/NewPresentationPage.tsx';
-import { PresentationViewer } from './components/presentations/PresentationViewer.tsx';
import { PresentationMode } from './components/presentations/PresentationMode.tsx';
import { PresentationEditor } from './components/presentations/PresentationEditor.tsx';
import { SlideEditor } from './components/slide-editor/SlideEditor.tsx';
@@ -25,7 +24,6 @@ function App() {
} />
} />
} />
- } />
} />
} />
} />
diff --git a/src/components/presentations/PresentationViewer.css b/src/components/presentations/PresentationViewer.css
deleted file mode 100644
index a170a22..0000000
--- a/src/components/presentations/PresentationViewer.css
+++ /dev/null
@@ -1,390 +0,0 @@
-.presentation-viewer {
- min-height: 100vh;
- background: #f8fafc;
- display: flex;
- flex-direction: column;
-}
-
-/* Header */
-.viewer-header {
- background: white;
- border-bottom: 1px solid #e2e8f0;
- padding: 1rem 2rem;
- display: flex;
- justify-content: space-between;
- align-items: center;
- gap: 2rem;
- flex-wrap: wrap;
-}
-
-.presentation-info {
- display: flex;
- align-items: center;
- gap: 1.5rem;
- flex: 1;
- min-width: 0;
-}
-
-.back-link {
- color: #64748b;
- text-decoration: none;
- font-weight: 500;
- font-size: 0.875rem;
- padding: 0.5rem 0.75rem;
- border-radius: 0.375rem;
- transition: all 0.2s ease;
- flex-shrink: 0;
-}
-
-.back-link:hover {
- background: #f1f5f9;
- color: #334155;
-}
-
-.presentation-title {
- flex: 1;
- min-width: 0;
-}
-
-.presentation-title h1 {
- margin: 0;
- font-size: 1.5rem;
- font-weight: 600;
- color: #1e293b;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.presentation-description {
- margin: 0.25rem 0 0 0;
- color: #64748b;
- font-size: 0.875rem;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.presentation-meta {
- display: flex;
- gap: 1rem;
- align-items: center;
- flex-shrink: 0;
-}
-
-.theme-badge {
- background: #dbeafe;
- color: #1e40af;
- padding: 0.25rem 0.75rem;
- border-radius: 0.375rem;
- font-size: 0.75rem;
- font-weight: 500;
-}
-
-.slide-counter {
- color: #6b7280;
- font-size: 0.875rem;
- font-weight: 500;
-}
-
-.viewer-actions {
- display: flex;
- gap: 0.75rem;
- flex-shrink: 0;
-}
-
-.action-button {
- padding: 0.5rem 1rem;
- border-radius: 0.375rem;
- font-weight: 500;
- font-size: 0.875rem;
- border: none;
- cursor: pointer;
- transition: all 0.2s ease;
- text-decoration: none;
- display: inline-flex;
- align-items: center;
- justify-content: center;
-}
-
-.action-button.primary {
- background: #3b82f6;
- color: white;
-}
-
-.action-button.primary:hover {
- background: #2563eb;
-}
-
-.action-button.secondary {
- background: #f8fafc;
- color: #64748b;
- border: 1px solid #e2e8f0;
-}
-
-.action-button.secondary:hover {
- background: #f1f5f9;
- color: #475569;
-}
-
-.action-button.large {
- padding: 0.75rem 1.5rem;
- font-size: 1rem;
-}
-
-/* Main Content */
-.viewer-content {
- flex: 1;
- display: flex;
- flex-direction: column;
- padding: 2rem;
- gap: 2rem;
-}
-
-/* Empty State */
-.empty-presentation {
- flex: 1;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.empty-content {
- text-align: center;
- max-width: 400px;
-}
-
-.empty-content h2 {
- margin: 0 0 0.5rem 0;
- color: #374151;
- font-size: 1.5rem;
-}
-
-.empty-content p {
- margin: 0 0 2rem 0;
- color: #6b7280;
- font-size: 1rem;
-}
-
-/* Slide Area */
-.slide-area {
- flex: 1;
- display: flex;
- justify-content: center;
- align-items: center;
- min-height: 400px;
-}
-
-.slide-container {
- background: white;
- border-radius: 0.75rem;
- border: 1px solid #e2e8f0;
- padding: 2rem;
- max-width: 800px;
- width: 100%;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
-}
-
-.slide-content h3 {
- margin: 0 0 1rem 0;
- color: #1e293b;
- font-size: 1.25rem;
-}
-
-.slide-content p {
- margin: 0 0 1rem 0;
- color: #64748b;
- font-size: 0.875rem;
-}
-
-.slide-preview {
- background: #f8fafc;
- border: 2px dashed #cbd5e1;
- border-radius: 0.5rem;
- padding: 3rem;
- text-align: center;
- margin: 1.5rem 0;
-}
-
-.slide-placeholder {
- color: #6b7280;
-}
-
-.slide-placeholder p {
- margin: 0.5rem 0;
-}
-
-.slide-notes {
- margin-top: 1.5rem;
- padding: 1rem;
- background: #f8fafc;
- border-radius: 0.5rem;
- border: 1px solid #e2e8f0;
-}
-
-.slide-notes h4 {
- margin: 0 0 0.5rem 0;
- color: #374151;
- font-size: 0.875rem;
- font-weight: 600;
-}
-
-.slide-notes p {
- margin: 0;
- color: #6b7280;
- font-size: 0.875rem;
- line-height: 1.4;
-}
-
-.slide-error {
- text-align: center;
- padding: 2rem;
- color: #dc2626;
-}
-
-/* Slide Navigation */
-.slide-navigation {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 1rem;
- padding: 1rem;
- background: white;
- border-radius: 0.75rem;
- border: 1px solid #e2e8f0;
-}
-
-.nav-button {
- padding: 0.5rem 1rem;
- border: 1px solid #e2e8f0;
- background: white;
- color: #374151;
- border-radius: 0.375rem;
- cursor: pointer;
- font-weight: 500;
- font-size: 0.875rem;
- transition: all 0.2s ease;
-}
-
-.nav-button:hover:not(:disabled) {
- background: #f8fafc;
- border-color: #cbd5e1;
-}
-
-.nav-button:disabled {
- opacity: 0.5;
- cursor: not-allowed;
-}
-
-.slide-thumbnails {
- display: flex;
- gap: 0.5rem;
- max-width: 300px;
- overflow-x: auto;
- padding: 0.25rem;
-}
-
-.thumbnail {
- min-width: 40px;
- height: 40px;
- border: 2px solid #e2e8f0;
- background: white;
- border-radius: 0.375rem;
- cursor: pointer;
- font-weight: 500;
- font-size: 0.875rem;
- color: #6b7280;
- transition: all 0.2s ease;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.thumbnail:hover {
- border-color: #cbd5e1;
- background: #f8fafc;
-}
-
-.thumbnail.active {
- border-color: #3b82f6;
- background: #dbeafe;
- color: #1e40af;
-}
-
-/* Loading and Error States */
-.loading-content,
-.error-content,
-.not-found-content {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- min-height: 50vh;
- text-align: center;
- gap: 1rem;
-}
-
-.loading-spinner {
- color: #64748b;
- font-size: 1.125rem;
-}
-
-.error-content h2,
-.not-found-content h2 {
- color: #dc2626;
- margin: 0;
-}
-
-.error-content p,
-.not-found-content p {
- color: #64748b;
- margin: 0.5rem 0 1.5rem 0;
-}
-
-/* Responsive Design */
-@media (max-width: 768px) {
- .viewer-header {
- padding: 1rem;
- flex-direction: column;
- align-items: stretch;
- gap: 1rem;
- }
-
- .presentation-info {
- flex-direction: column;
- align-items: flex-start;
- gap: 1rem;
- }
-
- .presentation-meta {
- flex-direction: column;
- align-items: flex-start;
- gap: 0.5rem;
- }
-
- .viewer-actions {
- justify-content: stretch;
- }
-
- .action-button {
- flex: 1;
- }
-
- .viewer-content {
- padding: 1rem;
- }
-
- .slide-container {
- padding: 1rem;
- }
-
- .slide-navigation {
- flex-direction: column;
- gap: 1rem;
- }
-
- .slide-thumbnails {
- justify-content: center;
- max-width: none;
- }
-}
\ No newline at end of file
diff --git a/src/components/presentations/PresentationViewer.tsx b/src/components/presentations/PresentationViewer.tsx
deleted file mode 100644
index 7775c46..0000000
--- a/src/components/presentations/PresentationViewer.tsx
+++ /dev/null
@@ -1,249 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { useParams, useNavigate, Link } from 'react-router-dom';
-import type { Presentation } from '../../types/presentation.ts';
-import type { Theme } from '../../types/theme.ts';
-import { getPresentationById } from '../../utils/presentationStorage.ts';
-import { loadTheme } from '../../utils/themeLoader.ts';
-import './PresentationViewer.css';
-
-export const PresentationViewer: React.FC = () => {
- const { presentationId, slideNumber } = useParams<{
- presentationId: string;
- slideNumber: string;
- }>();
- const navigate = useNavigate();
-
- const [presentation, setPresentation] = useState(null);
- const [theme, setTheme] = useState(null);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
-
- const currentSlideIndex = slideNumber ? parseInt(slideNumber, 10) - 1 : 0;
-
- useEffect(() => {
- const loadPresentationAndTheme = async () => {
- if (!presentationId) {
- setError('No presentation ID provided');
- setLoading(false);
- return;
- }
-
- try {
- setLoading(true);
-
- // Load presentation
- const presentationData = await getPresentationById(presentationId);
- if (!presentationData) {
- setError(`Presentation not found: ${presentationId}`);
- return;
- }
-
- setPresentation(presentationData);
-
- // Load theme
- const themeData = await loadTheme(presentationData.metadata.theme, false);
- if (!themeData) {
- setError(`Theme not found: ${presentationData.metadata.theme}`);
- return;
- }
-
- setTheme(themeData);
- } catch (err) {
- setError(err instanceof Error ? err.message : 'Failed to load presentation');
- } finally {
- setLoading(false);
- }
- };
-
- loadPresentationAndTheme();
- }, [presentationId]);
-
- const goToSlide = (slideIndex: number) => {
- if (!presentation) return;
-
- const slideNum = slideIndex + 1;
- navigate(`/presentations/${presentationId}/view/slides/${slideNum}`);
- };
-
- const goToPreviousSlide = () => {
- if (currentSlideIndex > 0) {
- goToSlide(currentSlideIndex - 1);
- }
- };
-
- const goToNextSlide = () => {
- if (presentation && currentSlideIndex < presentation.slides.length - 1) {
- goToSlide(currentSlideIndex + 1);
- }
- };
-
- const editPresentation = () => {
- if (!presentation) return;
-
- const slideNum = Math.max(1, currentSlideIndex + 1);
- navigate(`/presentations/${presentationId}/edit/slides/${slideNum}`);
- };
-
- const enterPresentationMode = () => {
- if (!presentation) return;
- navigate(`/presentations/${presentationId}/present/${currentSlideIndex + 1}`);
- };
-
- if (loading) {
- return (
-
-
-
Loading presentation...
-
-
- );
- }
-
- if (error) {
- return (
-
-
-
Error Loading Presentation
-
{error}
-
← Back to Themes
-
-
- );
- }
-
- if (!presentation || !theme) {
- return (
-
-
-
Presentation Not Found
-
The requested presentation could not be found.
-
← Back to Themes
-
-
- );
- }
-
- const currentSlide = presentation.slides[currentSlideIndex];
- const totalSlides = presentation.slides.length;
-
- return (
-
-
-
-
- {totalSlides === 0 ? (
-
-
-
This presentation is empty
-
Switch to edit mode to add slides
-
-
-
- ) : (
- <>
-
-
- {currentSlide ? (
-
-
Slide {currentSlideIndex + 1}
-
Layout: {currentSlide.layoutId}
-
- {/* TODO: Render actual slide content based on layout */}
-
-
Slide content will be rendered here
-
Layout: {currentSlide.layoutId}
-
Content slots: {Object.keys(currentSlide.content).length}
-
-
- {currentSlide.notes && (
-
-
Notes:
-
{currentSlide.notes}
-
- )}
-
- ) : (
-
- )}
-
-
-
-
-
-
-
- {presentation.slides.map((slide, index) => (
-
- ))}
-
-
-
-
- >
- )}
-
-
- );
-};
\ No newline at end of file
diff --git a/src/components/presentations/PresentationsList.tsx b/src/components/presentations/PresentationsList.tsx
index 30997f5..82cf90d 100644
--- a/src/components/presentations/PresentationsList.tsx
+++ b/src/components/presentations/PresentationsList.tsx
@@ -83,10 +83,6 @@ export const PresentationsList: React.FC = () => {
navigate(`/presentations/${id}/edit/slides/${slideNumber}`);
};
- const handleViewPresentation = (id: string, slideCount: number) => {
- const slideNumber = slideCount > 0 ? 1 : 1; // Always go to slide 1, or empty state
- navigate(`/presentations/${id}/view/slides/${slideNumber}`);
- };
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('en-US', {
@@ -171,14 +167,6 @@ export const PresentationsList: React.FC = () => {
>
✏️
-
-
{presentation.slides.length > 0 && (