From a84546b30b39a7ecf8c991c5e0e182885ff99a02 Mon Sep 17 00:00:00 2001 From: Michael Mainguy Date: Thu, 21 Aug 2025 13:30:58 -0500 Subject: [PATCH] Remove PresentationViewer component and view functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove view buttons from presentations list (both icon and card actions) - Delete PresentationViewer component and CSS files - Remove view route from App.tsx routing - Streamline presentation actions to Edit, Present, and Delete only 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- public/themes-manifest.json | 2 +- public/themes/CLAUDE.md | 360 ++++++++++++++++ src/App.tsx | 2 - .../presentations/PresentationViewer.css | 390 ------------------ .../presentations/PresentationViewer.tsx | 249 ----------- .../presentations/PresentationsList.tsx | 19 - 6 files changed, 361 insertions(+), 661 deletions(-) delete mode 100644 src/components/presentations/PresentationViewer.css delete mode 100644 src/components/presentations/PresentationViewer.tsx 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 ( -
-
-
- ← Back -
-

{presentation.metadata.name}

- {presentation.metadata.description && ( -

{presentation.metadata.description}

- )} -
-
- Theme: {theme.name} - - {totalSlides === 0 ? 'No slides' : `Slide ${currentSlideIndex + 1} of ${totalSlides}`} - -
-
- -
- - -
-
- -
- {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}

-
- )} -
- ) : ( -
-

Invalid slide number

-
- )} -
-
- -
- - -
- {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 && (