Removed static content from template.

This commit is contained in:
Michael Mainguy 2025-09-28 07:18:53 -05:00
parent d25eb56794
commit 1c2bb703f3
16 changed files with 1079 additions and 1134 deletions

247
RESTYLE.md Normal file
View File

@ -0,0 +1,247 @@
# Semantic Styling Framework Refactoring Checklist
This document tracks the refactoring of our CSS and TSX files to use the semantic styling framework, eliminating duplicate CSS and creating consistent component patterns.
## Framework Overview
Our semantic styling framework consists of:
- **`colors.css`** - CSS variables and color system
- **`semantic-components.css`** - Semantic component classes (cards, badges, actions, drag-and-drop, etc.)
- **`application.css`** - Utility classes and base component styles
- **`aspectRatios.css`** - Dedicated aspect ratio styling for slides (preserved)
## Refactoring Principles
1. **Use semantic classes** instead of component-specific CSS
2. **Leverage CSS variables** for colors, spacing, and typography
3. **Eliminate duplicate styles** by consolidating common patterns
4. **Preserve presentation/theme CSS** - slides should NOT use semantic framework
5. **Only keep minimal component-specific CSS** when absolutely necessary
---
## ✅ COMPLETED REFACTORING
### Core Framework Files
- **`src/styles/colors.css`** ✅ - Comprehensive CSS variable system
- **`src/styles/semantic-components.css`** ✅ - Semantic component classes
- **`src/styles/application.css`** ✅ - Utility classes and base styles
- **`src/styles/aspectRatios.css`** ✅ - Preserved dedicated slide aspect ratios
### Presentation Components
- **`src/components/presentations/PresentationEditor.tsx`** ✅ - Uses semantic classes
- **`src/components/presentations/PresentationEditor.css`** ✅ - Reduced by ~85%, only layout-specific CSS
- **`src/components/presentations/SlidesSidebar.tsx`** ✅ - Uses semantic classes
- **`src/components/presentations/SlidesSidebar.css`** ✅ - Minimal component-specific CSS
- **`src/components/presentations/shared/SlideThumbnail.tsx`** ✅ - Uses semantic classes
- **`src/components/presentations/shared/SlideThumbnail.css`** ✅ - Minimal component overrides
- **`src/components/presentations/PresentationMode.tsx`** ✅ - Uses semantic classes for UI
- **`src/components/presentations/PresentationMode.css`** ✅ - Isolated slide content from semantic framework
- **`src/components/presentations/shared/LoadingState.tsx`** ✅ - Uses semantic classes
- **`src/components/presentations/shared/LoadingState.css`** ✅ - Minimal overrides
- **`src/components/presentations/shared/ErrorState.tsx`** ✅ - Uses semantic classes (slide-editor version)
- **`src/components/presentations/shared/ErrorState.css`** ✅ - Minimal overrides
### Slide Operations
- **`src/hooks/useSlideOperations.ts`** ✅ - Added drag-and-drop reordering functionality
---
## 🚧 NEEDS REFACTORING
### High Priority - Presentation Components
#### **`src/components/presentations/NewPresentationPage.tsx`**
- **Status**: Partially refactored (documented in CSS)
- **Needs**: Full TSX refactoring to use semantic classes
- **Current Issues**: Uses custom classes that could be semantic utilities
#### **`src/components/presentations/NewPresentationPage.css`**
- **Status**: Has some semantic references but needs full refactoring
- **Needs**: Remove duplicate styles, use semantic framework
- **Current Issues**: Custom `.creation-form`, `.aspect-ratio-card` styles
#### **`src/components/presentations/ThemeSelector.tsx`**
- **Status**: Not refactored
- **Needs**: Use semantic classes for cards, grids, badges
- **Current Issues**: Likely uses hardcoded CSS classes
#### **`src/components/presentations/ThemeSelector.css`**
- **Status**: Partially refactored (hardcoded colors removed)
- **Needs**: Complete semantic class integration
- **Current Issues**: Custom `.theme-card`, `.themes-grid` could use semantic classes
#### **`src/components/presentations/PresentationsList.tsx`**
- **Status**: Not examined/refactored
- **Needs**: Full refactoring assessment
- **Likely Issues**: Custom card layouts, button styles
#### **`src/components/presentations/PresentationsList.css`**
- **Status**: Not examined/refactored
- **Needs**: Full refactoring assessment
- **Likely Issues**: Duplicate card, button, grid styles
### Medium Priority - Form Components
#### **`src/components/presentations/PresentationDetailsForm.tsx`**
- **Status**: Not examined/refactored
- **Needs**: Use semantic form classes
- **Likely Issues**: Custom form styling
#### **`src/components/presentations/PresentationDetailsForm.css`**
- **Status**: Not examined/refactored
- **Needs**: Use `.form-group`, `.form-input`, `.form-label` semantic classes
- **Likely Issues**: Duplicate form styles
#### **`src/components/presentations/AspectRatioSelector.tsx`**
- **Status**: Not examined/refactored
- **Needs**: Use semantic card and selection classes
- **Likely Issues**: Custom card selection UI
#### **`src/components/presentations/AspectRatioSelector.css`**
- **Status**: Not examined/refactored
- **Needs**: Use semantic `.card-interactive` and selection states
- **Likely Issues**: Custom card hover/selection styles
### Lower Priority - Supporting Components
#### **`src/components/presentations/CreationActions.tsx`**
- **Status**: Not examined/refactored
- **Needs**: Use semantic button classes
- **Likely Issues**: Custom button layouts
#### **`src/components/presentations/CreationActions.css`**
- **Status**: Not examined/refactored
- **Needs**: Use `.btn`, `.btn-primary`, `.btn-secondary` classes
- **Likely Issues**: Duplicate button styles
#### **`src/components/presentations/ThemeSelectionSection.tsx`**
- **Status**: Not examined/refactored
- **Needs**: Use semantic layout and card classes
- **Likely Issues**: Custom section layouts
#### **`src/components/presentations/ThemeSelectionSection.css`**
- **Status**: Not examined/refactored
- **Needs**: Use semantic grid and card classes
- **Likely Issues**: Custom grid layouts
#### **`src/components/presentations/EmptyPresentationState.tsx`**
- **Status**: Not examined/refactored
- **Needs**: Use semantic empty state classes
- **Likely Issues**: Custom empty state styling
#### **`src/components/presentations/EmptyPresentationState.css`**
- **Status**: Not examined/refactored
- **Needs**: Use `.empty-content` semantic class
- **Likely Issues**: Duplicate empty state styles
### Slide Editor Components (Lower Priority)
#### **`src/components/slide-editor/SlideEditor.tsx`**
- **Status**: Not examined/refactored
- **Needs**: Assessment for semantic class usage
- **Note**: May have complex layout requirements
#### **`src/components/slide-editor/SlideEditor.css`**
- **Status**: Not examined/refactored
- **Needs**: Assessment for consolidation opportunities
- **Note**: May need to preserve slide-specific styling
### Theme Components
#### **`src/components/themes/` (Multiple files)**
- **Status**: Not examined for this refactoring
- **Priority**: Low (theme browsing is separate from presentations)
- **Needs**: Future assessment
### UI Components
#### **`src/components/ui/` (Multiple files)**
- **Status**: Not examined for this refactoring
- **Priority**: Medium (shared components should use semantic framework)
- **Needs**: Future assessment for modal, dialog, form components
---
## 🎯 NEXT STEPS
### Immediate Priority (Sprint 1)
1. **NewPresentationPage.tsx** - Full semantic refactoring
2. **ThemeSelector.tsx** - Use semantic card and grid classes
3. **PresentationsList.tsx** - Assessment and refactoring
### Short Term (Sprint 2)
1. **Form Components** - PresentationDetailsForm, AspectRatioSelector
2. **Supporting Components** - CreationActions, ThemeSelectionSection
3. **Empty States** - EmptyPresentationState
### Long Term (Sprint 3)
1. **Slide Editor Components** - Assessment and selective refactoring
2. **UI Components** - Modal, dialog, form utilities
3. **Theme Components** - If needed for consistency
---
## 📋 REFACTORING CHECKLIST
For each component refactoring, ensure:
### TSX File Updates
- [ ] Replace custom CSS classes with semantic classes
- [ ] Use utility classes for layout (`flex`, `gap-4`, `items-center`, etc.)
- [ ] Use semantic typography classes (`text-lg`, `font-semibold`, etc.)
- [ ] Use semantic color classes (`text-primary`, `bg-secondary`, etc.)
- [ ] Use semantic component classes (`.card-interactive`, `.btn`, `.badge-secondary`, etc.)
- [ ] Remove imports of CSS files that become empty
### CSS File Updates
- [ ] Remove duplicate styles covered by semantic framework
- [ ] Replace hardcoded colors with CSS variables
- [ ] Keep only truly component-specific styling
- [ ] Use semantic classes as base, add minimal overrides only
- [ ] Document what semantic classes replace in comments
- [ ] Delete CSS file if it becomes empty/unnecessary
### Testing Requirements
- [ ] Visual consistency with existing design
- [ ] Interactive states work correctly (hover, focus, disabled)
- [ ] Responsive behavior maintained
- [ ] No regression in functionality
- [ ] Accessibility maintained (focus states, etc.)
---
## 🚨 SPECIAL CONSIDERATIONS
### Presentation/Slide Content Isolation
- **Theme CSS** (`public/themes/default/style.css`) must remain completely isolated
- **Slide content** should NEVER use semantic framework classes
- **PresentationMode** must prevent semantic class interference with slide rendering
- **AspectRatios.css** preserved for slide-specific aspect ratio handling
### Performance Considerations
- **Bundle size reduction** achieved through elimination of duplicate CSS
- **CSS loading order** maintained (colors → semantic-components → application)
- **Critical CSS** identified and preserved for above-the-fold content
### Drag-and-Drop Functionality
- **Semantic drag classes** implemented and working (`.draggable`, `.dragged`, `.drag-over`, `.drag-handle`)
- **Slide reordering** functionality preserved and enhanced
- **Visual feedback** consistent across all drag operations
---
## 📊 PROGRESS SUMMARY
- **✅ Completed**: 14 files (Core framework + key presentation components)
- **🚧 Needs Refactoring**: ~15-20 files (High and medium priority)
- **📈 CSS Reduction**: ~85% reduction achieved in refactored files
- **🎨 Consistency**: Semantic framework provides consistent design patterns
- **⚡ Performance**: Reduced CSS bundle size and eliminated duplicates
**Estimated Completion**: 2-3 sprints for high/medium priority items
---
*Last Updated: September 28, 2025*
*Framework Version: 1.0*

View File

@ -1,3 +1,5 @@
/* Presentation Editor - Minimal component-specific styles only */
.presentation-editor { .presentation-editor {
min-height: 100vh; min-height: 100vh;
background: var(--bg-primary); background: var(--bg-primary);
@ -5,161 +7,28 @@
flex-direction: column; flex-direction: column;
} }
/* Header */ /* Component-specific layout overrides only */
.editor-header { .editor-layout {
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-primary);
padding: 1rem 2rem;
display: flex; 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; flex: 1;
min-width: 0; min-height: 0;
} }
.back-link { .slide-editor-area {
color: var(--text-secondary);
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: var(--bg-hover);
color: var(--text-primary);
}
.presentation-title span {
color: var(--text-secondary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.presentation-meta {
display: flex;
gap: 1rem;
align-items: center;
flex-shrink: 0;
flex-wrap: wrap;
}
.theme-badge {
background: var(--color-blue-100);
color: var(--color-blue-800);
padding: 0.25rem 0.75rem;
border-radius: 0.375rem;
font-size: 0.75rem;
font-weight: 500;
}
.slide-counter {
color: var(--text-tertiary);
font-size: 0.875rem;
font-weight: 500;
}
.saving-indicator {
color: var(--text-warning);
font-size: 0.875rem;
font-weight: 500;
}
.editor-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: var(--btn-primary-bg);
color: var(--btn-primary-text);
}
.action-button.primary:hover:not(:disabled) {
background: var(--btn-primary-bg-hover);
}
.action-button.secondary {
background: var(--btn-secondary-bg);
color: var(--btn-secondary-text);
border: 1px solid var(--btn-secondary-border);
}
.action-button.secondary:hover:not(:disabled) {
background: var(--btn-secondary-bg-hover);
color: var(--btn-secondary-text-hover);
}
.action-button.large {
padding: 0.75rem 1.5rem;
font-size: 1rem;
}
.action-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Main Content */
.editor-content {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: 0;
} }
/* Empty State */ .slide-editor {
.empty-presentation {
flex: 1; flex: 1;
display: flex; display: flex;
align-items: center; flex-direction: column;
justify-content: center; padding: 1rem;
padding: 2rem;
} }
.empty-content { /* Theme preview in empty state - component specific */
text-align: center;
max-width: 800px;
}
.empty-content h2 {
margin: 0 0 0.5rem 0;
color: var(--text-secondary);
font-size: 1.5rem;
}
.empty-content p {
margin: 0 0 2rem 0;
color: var(--text-tertiary);
font-size: 1rem;
}
/* Theme Preview in Empty State */
.theme-preview { .theme-preview {
background: var(--bg-primary); background: var(--bg-primary);
border: 1px solid var(--border-primary); border: 1px solid var(--border-primary);
@ -173,19 +42,6 @@
flex-direction: column; flex-direction: column;
} }
.theme-preview h3 {
margin: 0 0 0.5rem 0;
color: var(--text-primary);
font-size: 1.25rem;
font-weight: 600;
}
.theme-description {
margin: 0 0 1.5rem 0;
color: var(--text-secondary);
font-size: 0.875rem;
}
.available-layouts { .available-layouts {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
@ -193,14 +49,6 @@
flex-direction: column; flex-direction: column;
} }
.available-layouts h4 {
margin: 0 0 1rem 0;
color: var(--text-secondary);
font-size: 1rem;
font-weight: 600;
flex-shrink: 0;
}
.layouts-grid { .layouts-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
@ -229,361 +77,12 @@
background: var(--color-slate-400); background: var(--color-slate-400);
} }
.layout-preview-card { /* Slide thumbnail positioning in editor context */
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: 0.5rem;
padding: 1rem;
transition: all 0.2s ease;
}
.layout-preview-card:hover {
border-color: var(--border-hover);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.layout-name {
font-weight: 600;
color: var(--text-primary);
font-size: 0.875rem;
margin-bottom: 0.25rem;
}
.layout-description {
color: var(--text-secondary);
font-size: 0.75rem;
margin-bottom: 0.5rem;
line-height: 1.4;
}
.slot-count {
color: var(--color-emerald-600);
font-size: 0.75rem;
font-weight: 500;
}
.more-layouts {
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-tertiary);
border: 1px dashed var(--border-hover);
border-radius: 0.5rem;
padding: 1rem;
color: var(--text-secondary);
font-size: 0.875rem;
font-weight: 500;
}
/* Editor Layout */
.editor-layout {
display: flex;
flex: 1;
min-height: 0;
}
/* Slide Sidebar */
.slide-sidebar {
width: 280px;
background: var(--bg-secondary);
border-right: 1px solid var(--border-primary);
display: flex;
flex-direction: column;
}
.sidebar-header {
padding: 1rem;
border-bottom: 1px solid var(--border-primary);
display: flex;
justify-content: space-between;
align-items: center;
}
.sidebar-header h3 {
margin: 0;
font-size: 1rem;
font-weight: 600;
color: var(--text-primary);
}
.add-slide-button {
width: 32px;
height: 32px;
border: 1px solid var(--border-primary);
background: var(--bg-secondary);
border-radius: 0.375rem;
cursor: pointer;
font-size: 1.25rem;
color: var(--text-accent);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.add-slide-button:hover:not(:disabled) {
background: var(--bg-hover);
border-color: var(--border-accent);
}
.add-slide-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.slides-list {
flex: 1;
overflow-y: auto;
padding: 0.5rem;
}
/* Slide thumbnails specific to editor layout - positioning only */
.slide-thumbnail { .slide-thumbnail {
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
/* Layout-related thumbnail styles moved to SlideThumbnail.css */ /* Responsive design */
/* Slide Editor Area */
.slide-editor-area {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
.slide-editor {
flex: 1;
display: flex;
flex-direction: column;
padding: 1rem;
}
.slide-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
gap: 1rem;
flex-wrap: wrap;
}
.slide-header h3 {
margin: 0;
font-size: 1.125rem;
font-weight: 600;
color: var(--text-primary);
}
.slide-controls {
display: flex;
gap: 0.5rem;
}
.control-button {
padding: 0.375rem 0.75rem;
border: 1px solid var(--border-primary);
background: var(--bg-secondary);
color: var(--text-secondary);
border-radius: 0.375rem;
cursor: pointer;
font-weight: 500;
font-size: 0.875rem;
transition: all 0.2s ease;
}
.control-button:hover:not(:disabled) {
background: var(--bg-hover);
border-color: var(--border-hover);
}
.control-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.control-button.edit-slide-button {
background: var(--btn-primary-bg);
color: var(--btn-primary-text);
border-color: var(--btn-primary-border);
font-weight: 600;
}
.control-button.edit-slide-button:hover:not(:disabled) {
background: var(--btn-primary-bg-hover);
border-color: var(--btn-primary-border-hover);
}
.slide-content-editor {
flex: 1;
display: flex;
flex-direction: column;
gap: 1rem;
}
.content-preview {
flex: 1;
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: 0.75rem;
padding: 2rem;
min-height: 400px;
}
.editor-placeholder {
text-align: center;
color: var(--text-tertiary);
}
.editor-placeholder h4 {
margin: 0 0 1rem 0;
color: var(--text-secondary);
}
.editor-placeholder p {
margin: 0.5rem 0;
}
.content-slots {
margin: 1.5rem 0;
text-align: left;
}
.content-slot {
margin-bottom: 1rem;
padding: 0.75rem;
background: var(--bg-primary);
border-radius: 0.375rem;
}
.content-slot label {
display: block;
font-weight: 500;
color: var(--text-secondary);
margin-bottom: 0.25rem;
font-size: 0.875rem;
}
.slot-content {
color: var(--text-tertiary);
font-size: 0.875rem;
}
.placeholder-note {
font-style: italic;
color: var(--text-muted);
margin-top: 1.5rem;
}
.slide-notes-editor {
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: 0.75rem;
padding: 1rem;
}
.slide-notes-editor h4 {
margin: 0 0 0.75rem 0;
font-size: 0.875rem;
font-weight: 600;
color: var(--text-secondary);
}
.notes-textarea {
width: 100%;
min-height: 80px;
padding: 0.75rem;
border: 1px solid var(--border-secondary);
border-radius: 0.5rem;
font-size: 0.875rem;
color: var(--text-secondary);
background: var(--bg-secondary);
box-sizing: border-box;
resize: vertical;
font-family: inherit;
}
.notes-textarea:focus {
outline: none;
border-color: var(--border-focus);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.slide-error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem;
text-align: center;
color: var(--text-error);
}
/* 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: var(--text-secondary);
font-size: 1.125rem;
}
.error-content h2,
.not-found-content h2 {
color: var(--text-error);
margin: 0;
}
.error-content p,
.not-found-content p {
color: var(--text-secondary);
margin: 0.5rem 0 1.5rem 0;
}
/* Responsive Design */
@media (max-width: 1024px) {
.slide-sidebar {
width: 240px;
}
.editor-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;
}
.editor-actions {
justify-content: stretch;
}
.action-button {
flex: 1;
}
}
@media (max-width: 768px) { @media (max-width: 768px) {
.editor-layout { .editor-layout {
flex-direction: column; flex-direction: column;
@ -592,32 +91,5 @@
.slide-sidebar { .slide-sidebar {
width: 100%; width: 100%;
max-height: 200px; max-height: 200px;
border-right: none;
border-bottom: 1px solid var(--border-primary);
}
.slides-list {
display: flex;
gap: 0.5rem;
overflow-x: auto;
padding: 0.5rem;
}
.slide-thumbnail {
min-width: 120px;
margin-bottom: 0;
}
.slide-header {
flex-direction: column;
align-items: stretch;
}
.slide-controls {
justify-content: stretch;
}
.control-button {
flex: 1;
} }
} }

View File

@ -140,25 +140,23 @@ export const PresentationEditor: React.FC = () => {
return ( return (
<div className="presentation-editor"> <div className="presentation-editor">
<header className="editor-header"> <header className="page-header">
<div className="presentation-info">
<Link to="/presentations" className="back-link"> Back to Presentations</Link> <Link to="/presentations" className="back-link"> Back to Presentations</Link>
<div className="presentation-title"> <div className="flex-1">
<span>{presentation.metadata.name}</span> <h1 className="text-xl font-semibold text-primary m-0">{presentation.metadata.name}</h1>
{presentation.metadata.description && ( {presentation.metadata.description && (
<span className="presentation-description">{presentation.metadata.description}</span> <p className="text-sm text-secondary mt-1 m-0">{presentation.metadata.description}</p>
)} )}
</div> </div>
<div className="presentation-meta"> <div className="flex gap-4 items-center">
<span className="slide-counter"> <span className="badge-secondary">
{totalSlides === 0 ? 'No slides' : `Slide ${currentSlideIndex + 1} of ${totalSlides}`} {totalSlides === 0 ? 'No slides' : `Slide ${currentSlideIndex + 1} of ${totalSlides}`}
</span> </span>
{saving && <span className="saving-indicator">Saving...</span>} {saving && <span className="text-sm text-warning font-medium">Saving...</span>}
</div>
</div> </div>
</header> </header>
<main className="editor-content"> <main className="flex-1 flex flex-col">
{totalSlides === 0 ? ( {totalSlides === 0 ? (
<EmptyPresentationState <EmptyPresentationState
theme={theme} theme={theme}
@ -182,30 +180,30 @@ export const PresentationEditor: React.FC = () => {
<div className="slide-editor-area"> <div className="slide-editor-area">
{currentSlide ? ( {currentSlide ? (
<div className="slide-editor"> <div className="slide-editor">
<div className="slide-header"> <div className="flex justify-between items-center mb-4">
<h3>Slide {currentSlideIndex + 1}</h3> <h3 className="text-lg font-semibold text-primary m-0">Slide {currentSlideIndex + 1}</h3>
</div> </div>
<div className="slide-content-editor"> <div className="flex-1">
<div className="content-preview"> <div className="card p-6">
{/* TODO: Render actual slide content with editing capabilities */} {/* TODO: Render actual slide content with editing capabilities */}
<div className="editor-placeholder"> <div className="text-center">
<div className="content-slots"> <div className="flex flex-col gap-3 mb-6">
{Object.entries(currentSlide.content).map(([slotId, content]) => ( {Object.entries(currentSlide.content).map(([slotId, content]) => (
<div key={slotId} className="content-slot"> <div key={slotId} className="flex justify-between items-center p-3 bg-muted rounded">
<label>{slotId}:</label> <label className="font-medium text-secondary">{slotId}:</label>
<div className="slot-content">{content || '(empty)'}</div> <div className="text-tertiary">{content || '(empty)'}</div>
</div> </div>
))} ))}
</div> </div>
<p className="placeholder-note"> <p className="text-tertiary text-sm">
Interactive slide editor will be implemented next Interactive slide editor will be implemented next
</p> </p>
</div> </div>
</div> </div>
{currentSlide.notes && ( {currentSlide.notes && (
<div className="slide-notes-editor"> <div className="mt-4">
<h4>Speaker Notes</h4> <h4 className="text-base font-semibold text-primary mb-2">Speaker Notes</h4>
<textarea <textarea
value={currentSlide.notes} value={currentSlide.notes}
onChange={(e) => { onChange={(e) => {
@ -213,18 +211,18 @@ export const PresentationEditor: React.FC = () => {
loggers.presentation.debug('Slide notes updated', { slideId: currentSlide.id, notesLength: e.target.value.length }); loggers.presentation.debug('Slide notes updated', { slideId: currentSlide.id, notesLength: e.target.value.length });
}} }}
placeholder="Add speaker notes for this slide..." placeholder="Add speaker notes for this slide..."
className="notes-textarea" className="form-textarea"
/> />
</div> </div>
)} )}
</div> </div>
</div> </div>
) : ( ) : (
<div className="slide-error"> <div className="error-content">
<p>Invalid slide number</p> <h2>Invalid slide number</h2>
<button <button
type="button" type="button"
className="action-button secondary" className="btn btn-secondary"
onClick={() => goToSlide(0)} onClick={() => goToSlide(0)}
> >
Go to First Slide Go to First Slide
@ -260,3 +258,4 @@ export const PresentationEditor: React.FC = () => {
</div> </div>
); );
}; };

View File

@ -1,11 +1,13 @@
/* Fullscreen presentation mode styles */ /* Presentation Mode - Surgical override approach to preserve theme styling */
/* Fullscreen presentation mode container */
.presentation-mode { .presentation-mode {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
background: #000; background: var(--color-black);
z-index: 9999; z-index: 9999;
display: flex; display: flex;
align-items: center; align-items: center;
@ -13,150 +15,114 @@
overflow: hidden; overflow: hidden;
} }
.presentation-mode.loading, /* ===================================================================
.presentation-mode.error { SLIDE CONTENT - Minimal interference with theme styling
flex-direction: column; =================================================================== */
background: #1a1a1a;
color: #fff; /* Override only the specific semantic framework classes that conflict */
} .presentation-mode .slide-content .text-primary,
.presentation-mode .slide-content .text-secondary,
.presentation-mode .loading-spinner { .presentation-mode .slide-content .text-tertiary,
font-size: 1.2rem; .presentation-mode .slide-content .text-white,
padding: 2rem; .presentation-mode .slide-content .bg-primary,
text-align: center; .presentation-mode .slide-content .bg-secondary,
} .presentation-mode .slide-content .bg-muted,
.presentation-mode .slide-content .bg-accent,
.presentation-mode .error-content { .presentation-mode .slide-content .flex,
text-align: center; .presentation-mode .slide-content .flex-col,
max-width: 600px; .presentation-mode .slide-content .flex-row,
padding: 2rem; .presentation-mode .slide-content .items-center,
} .presentation-mode .slide-content .justify-center,
.presentation-mode .slide-content .gap-1,
.presentation-mode .error-content h2 { .presentation-mode .slide-content .gap-2,
margin: 0 0 1rem 0; .presentation-mode .slide-content .gap-3,
font-size: 2rem; .presentation-mode .slide-content .gap-4,
font-weight: 300; .presentation-mode .slide-content .p-1,
} .presentation-mode .slide-content .p-2,
.presentation-mode .slide-content .p-3,
.presentation-mode .error-content p { .presentation-mode .slide-content .p-4,
margin: 0 0 2rem 0; .presentation-mode .slide-content .m-1,
font-size: 1.1rem; .presentation-mode .slide-content .m-2,
opacity: 0.8; .presentation-mode .slide-content .m-3,
line-height: 1.5; .presentation-mode .slide-content .m-4,
} .presentation-mode .slide-content .rounded,
.presentation-mode .slide-content .rounded-lg,
.presentation-mode .exit-button { .presentation-mode .slide-content .shadow,
background: #fff; .presentation-mode .slide-content .btn,
color: #000; .presentation-mode .slide-content .card {
border: none; /* Disable semantic classes within slide content */
padding: 0.75rem 1.5rem; all: revert !important;
font-size: 1rem;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s ease;
}
.presentation-mode .exit-button:hover {
background: #f0f0f0;
}
/* Fullscreen slide container */
.presentation-mode.fullscreen {
background: #000;
}
.presentation-mode .slide-container {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
position: relative;
} }
/* Ensure theme variables take precedence over semantic framework */
.presentation-mode .slide-content { .presentation-mode .slide-content {
max-width: 90vw; /* Let theme CSS control fonts and colors */
max-height: 90vh; font-family: var(--theme-font-body, system-ui, sans-serif) !important;
background: #fff; background: var(--theme-background, var(--color-black)) !important;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); color: var(--theme-text, white) !important;
position: relative;
overflow: hidden;
} }
/* Aspect ratio classes for slides */ /* Force theme slot styling to override semantic framework */
.presentation-mode .slide-content.aspect-16-9 { .presentation-mode .slide-content .slot[data-slot="title"] {
aspect-ratio: 16/9; color: var(--theme-primary, white) !important;
width: min(90vw, calc(90vh * 16/9)); font-family: var(--theme-font-heading, var(--theme-font-body, system-ui, sans-serif)) !important;
} }
.presentation-mode .slide-content.aspect-4-3 { .presentation-mode .slide-content .slot[data-slot="subtitle"],
aspect-ratio: 4/3; .presentation-mode .slide-content .slot[data-slot="content"],
width: min(90vw, calc(90vh * 4/3)); .presentation-mode .slide-content .slot[data-slot="content1"],
.presentation-mode .slide-content .slot[data-slot="content2"] {
color: var(--theme-text-secondary, #94a4ab) !important;
font-family: var(--theme-font-body, system-ui, sans-serif) !important;
} }
.presentation-mode .slide-content.aspect-16-10 { /* Ensure code blocks work properly */
aspect-ratio: 16/10; .presentation-mode .slide-content .slide-code,
width: min(90vw, calc(90vh * 16/10)); .presentation-mode .slide-content pre.slot[data-type="code"],
.presentation-mode .slide-content .language-javascript,
.presentation-mode .slide-content .hljs-code {
background: #1e1e1e !important;
color: #e6e6e6 !important;
font-family: var(--theme-font-code, 'Consolas', monospace) !important;
} }
/* Navigation indicator */ /* Ensure mermaid diagrams work properly */
.presentation-mode .navigation-indicator { .presentation-mode .slide-content .mermaid-container,
.presentation-mode .slide-content .mermaid-diagram {
background: transparent !important;
}
/* ===================================================================
NAVIGATION INDICATOR - Uses semantic framework (outside slides)
=================================================================== */
/* Navigation indicator overlay */
.navigation-indicator {
position: fixed; position: fixed;
bottom: 2rem; bottom: 2rem;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
background: rgba(0, 0, 0, 0.7); background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
color: #fff; color: var(--color-white);
padding: 1rem 2rem; padding: 1rem 2rem;
border-radius: 2rem; border-radius: 2rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
opacity: 0; opacity: 0;
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
pointer-events: none; pointer-events: none;
z-index: 10000;
border: 1px solid rgba(255, 255, 255, 0.1);
} }
.presentation-mode:hover .navigation-indicator { .presentation-mode:hover .navigation-indicator {
opacity: 1; opacity: 1;
} }
.presentation-mode .slide-counter { /* Ensure navigation text uses white color and proper fonts */
font-size: 1.1rem; .navigation-indicator,
font-weight: 500; .navigation-indicator * {
} color: var(--color-white) !important;
font-family: system-ui, -apple-system, sans-serif !important;
.presentation-mode .navigation-hints {
display: flex;
gap: 1rem;
font-size: 0.9rem;
opacity: 0.8;
}
.presentation-mode .navigation-hints span {
white-space: nowrap;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.presentation-mode .navigation-indicator {
bottom: 1rem;
padding: 0.75rem 1.5rem;
font-size: 0.9rem;
}
.presentation-mode .navigation-hints {
flex-direction: column;
gap: 0.25rem;
text-align: center;
}
.presentation-mode .slide-content {
max-width: 95vw;
max-height: 95vh;
}
} }
/* Hide scrollbars in presentation mode */ /* Hide scrollbars in presentation mode */
@ -169,8 +135,11 @@
display: none; /* Chrome/Safari */ display: none; /* Chrome/Safari */
} }
/* Ensure theme styles work in fullscreen */ /* Responsive adjustments */
.presentation-mode .slide-content > * { @media (max-width: 768px) {
width: 100%; .navigation-indicator {
height: 100%; bottom: 1rem;
padding: 0.75rem 1.5rem;
font-size: 0.9rem;
}
} }

View File

@ -123,19 +123,21 @@ export const PresentationMode: React.FC = () => {
if (loading) { if (loading) {
return ( return (
<div className="presentation-mode loading"> <div className="presentation-mode">
<div className="loading-content text-white">
<div className="loading-spinner">Loading presentation...</div> <div className="loading-spinner">Loading presentation...</div>
</div> </div>
</div>
); );
} }
if (error) { if (error) {
return ( return (
<div className="presentation-mode error"> <div className="presentation-mode">
<div className="error-content"> <div className="error-content text-white">
<h2>Error Loading Presentation</h2> <h2>Error Loading Presentation</h2>
<p>{error}</p> <p>{error}</p>
<button onClick={() => navigate(-1)} className="exit-button"> <button onClick={() => navigate(-1)} className="btn btn-primary">
Exit Presentation Mode Exit Presentation Mode
</button> </button>
</div> </div>
@ -145,10 +147,10 @@ export const PresentationMode: React.FC = () => {
if (!presentation || !theme) { if (!presentation || !theme) {
return ( return (
<div className="presentation-mode error"> <div className="presentation-mode">
<div className="error-content"> <div className="error-content text-white">
<h2>Presentation Not Found</h2> <h2>Presentation Not Found</h2>
<button onClick={() => navigate(-1)} className="exit-button"> <button onClick={() => navigate(-1)} className="btn btn-primary">
Exit Presentation Mode Exit Presentation Mode
</button> </button>
</div> </div>
@ -158,11 +160,11 @@ export const PresentationMode: React.FC = () => {
if (presentation.slides.length === 0) { if (presentation.slides.length === 0) {
return ( return (
<div className="presentation-mode error"> <div className="presentation-mode">
<div className="error-content"> <div className="error-content text-white">
<h2>No Slides Available</h2> <h2>No Slides Available</h2>
<p>This presentation is empty.</p> <p>This presentation is empty.</p>
<button onClick={() => navigate(-1)} className="exit-button"> <button onClick={() => navigate(-1)} className="btn btn-primary">
Exit Presentation Mode Exit Presentation Mode
</button> </button>
</div> </div>
@ -173,16 +175,16 @@ export const PresentationMode: React.FC = () => {
const totalSlides = presentation.slides.length; const totalSlides = presentation.slides.length;
return ( return (
<div className="presentation-mode fullscreen"> <div className="presentation-mode">
<div className="slide-container"> <div className={`slide-container ${presentation.metadata.aspectRatio ? `aspect-${presentation.metadata.aspectRatio.replace(':', '-')}` : 'aspect-16-9'}`}>
{isRenderingSlide && ( {isRenderingSlide && (
<div className="slide-loading"> <div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 flex flex-col items-center gap-4 text-white text-lg z-10">
<div className="loading-spinner"></div> <div className="loading-spinner text-xl opacity-80"></div>
<span>Rendering slide...</span> <span>Rendering slide...</span>
</div> </div>
)} )}
<div <div
className={`slide-content ${presentation.metadata.aspectRatio ? `aspect-${presentation.metadata.aspectRatio.replace(':', '-')}` : 'aspect-16-9'}`} className="slide-content"
dangerouslySetInnerHTML={{ __html: renderedSlideContent }} dangerouslySetInnerHTML={{ __html: renderedSlideContent }}
style={{ opacity: isRenderingSlide ? 0.5 : 1 }} style={{ opacity: isRenderingSlide ? 0.5 : 1 }}
/> />
@ -190,15 +192,17 @@ export const PresentationMode: React.FC = () => {
{/* Navigation indicator */} {/* Navigation indicator */}
<div className="navigation-indicator"> <div className="navigation-indicator">
<span className="slide-counter"> <div className="flex flex-col items-center gap-2">
<span className="text-lg font-medium">
{currentSlideIndex + 1} / {totalSlides} {currentSlideIndex + 1} / {totalSlides}
</span> </span>
<div className="navigation-hints"> <div className="flex gap-4 text-sm opacity-80">
<span> Space: Navigate</span> <span> Space: Navigate</span>
<span>Esc: Exit</span> <span>Esc: Exit</span>
</div> </div>
</div> </div>
</div> </div>
</div>
); );
}; };

View File

@ -1,53 +1,21 @@
/* Slides Sidebar Component - Minimal overrides using semantic classes */
/* Component-specific layout only */
.slide-sidebar { .slide-sidebar {
width: 240px; width: 240px;
background: #ffffff;
border-right: 1px solid #e5e7eb;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 100%; min-height: 100%;
} }
.sidebar-header { /* Custom hover state for accent button */
display: flex; .slide-sidebar .bg-accent:hover:not(:disabled) {
justify-content: space-between; background: var(--text-link-hover);
align-items: center;
padding: 1rem;
border-bottom: 1px solid #e5e7eb;
background: #f9fafb;
} }
.sidebar-header h3 { /* Responsive behavior for mobile */
font-size: 1rem; @media (max-width: 768px) {
font-weight: 600; .slide-sidebar {
color: #1f2937; width: 200px;
margin: 0;
} }
.add-slide-button {
background: #3b82f6;
color: white;
border: none;
width: 32px;
height: 32px;
border-radius: 50%;
font-size: 1.25rem;
font-weight: bold;
cursor: pointer;
transition: background-color 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.add-slide-button:hover {
background: #2563eb;
}
.slides-list {
flex: 1;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
overflow-y: auto;
} }

View File

@ -79,19 +79,20 @@ export const SlidesSidebar: React.FC<SlidesSidebarProps> = ({
return ( return (
<aside className="slide-sidebar"> <aside className="slide-sidebar">
<div className="sidebar-header"> <header className="flex justify-between items-center p-4 border-b bg-muted">
<h3>Slides</h3> <h3 className="text-base font-semibold text-primary m-0">Slides</h3>
<button <button
type="button" type="button"
className="add-slide-button" className="btn-icon rounded-full bg-accent text-white hover:bg-accent-hover"
onClick={onAddSlide} onClick={onAddSlide}
title="Add new slide" title="Add new slide"
disabled={saving}
> >
+ +
</button> </button>
</div> </header>
<div className="slides-list"> <div className="flex-1 p-4 flex flex-col gap-4 overflow-y-auto">
{slides.map((slide, index) => ( {slides.map((slide, index) => (
<SlideThumbnail <SlideThumbnail
key={slide.id} key={slide.id}

View File

@ -2,73 +2,18 @@
width: 100%; width: 100%;
} }
.themes-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 1.5rem;
}
.theme-card {
background: white;
border: 2px solid #e2e8f0;
border-radius: 0.75rem;
overflow: hidden;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
}
.theme-card:hover {
border-color: #cbd5e1;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.theme-card.selected {
border-color: #22c55e;
box-shadow: 0 4px 6px -1px rgba(34, 197, 94, 0.2);
}
/* Theme preview uses .preview-container semantic class */
.theme-preview { .theme-preview {
position: relative; position: relative;
height: 120px; height: 120px;
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-muted) 100%);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 1rem; padding: 1rem;
} }
.color-preview-strip {
display: flex;
gap: 0.5rem;
align-items: center;
flex-wrap: wrap;
justify-content: center;
}
.color-swatch {
width: 48px;
height: 48px;
border-radius: 50%;
border: 3px solid white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
flex-shrink: 0;
}
.no-colors {
color: #9ca3af;
font-size: 0.875rem;
text-align: center;
font-style: italic;
}
.selection-indicator {
position: absolute;
top: 0.75rem;
right: 0.75rem;
z-index: 10;
}
.theme-info { .theme-info {
padding: 1.5rem; padding: 1.5rem;
} }
@ -77,42 +22,27 @@
margin: 0 0 0.5rem 0; margin: 0 0 0.5rem 0;
font-size: 1.125rem; font-size: 1.125rem;
font-weight: 600; font-weight: 600;
color: #1e293b; color: var(--text-primary);
} }
.theme-description { .theme-description {
margin: 0 0 1rem 0; margin: 0 0 1rem 0;
color: #64748b; color: var(--text-secondary);
font-size: 0.875rem; font-size: 0.875rem;
line-height: 1.4; line-height: 1.4;
} }
.theme-meta {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.theme-stats {
display: flex;
flex-direction: column;
gap: 0.25rem;
font-size: 0.75rem;
color: #6b7280;
}
.layouts-count { .layouts-count {
font-weight: 500; font-weight: 500;
color: #475569; color: var(--text-secondary);
} }
.theme-author { .theme-author {
color: #6b7280; color: var(--text-tertiary);
} }
.theme-version { .theme-version {
color: #9ca3af; color: var(--text-tertiary);
font-family: 'Courier New', monospace; font-family: 'Courier New', monospace;
} }
@ -121,31 +51,16 @@
gap: 0.5rem; gap: 0.5rem;
} }
.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;
}
.preview-link:hover {
background: #eff6ff;
text-decoration: none;
}
/* Empty State */ /* Empty State */
.empty-state { .empty-state {
text-align: center; text-align: center;
padding: 3rem; padding: 3rem;
color: #6b7280; color: var(--text-tertiary);
} }
.empty-state h3 { .empty-state h3 {
margin: 0 0 0.5rem 0; margin: 0 0 0.5rem 0;
color: #374151; color: var(--text-primary);
} }
.empty-state p { .empty-state p {

View File

@ -1,67 +1,96 @@
/* Error State Component - Fully refactored to use semantic classes */
/* Use semantic .error-content class from application.css */
.error-content { .error-content {
text-align: center; /* Base styling already defined in application.css */
padding: 2rem;
min-height: 200px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center;
align-items: center; align-items: center;
justify-content: center;
min-height: 40vh;
text-align: center;
gap: 1rem;
padding: 2rem;
} }
.error-content h2 { .error-content h2 {
color: #dc2626; color: var(--text-error);
font-size: 1.5rem; font-size: 1.5rem;
font-weight: 600; font-weight: 600;
margin-bottom: 1rem; margin: 0 0 1rem 0;
} }
.error-content p { .error-content p {
color: #6b7280; color: var(--text-secondary);
margin-bottom: 2rem; margin: 0 0 2rem 0;
max-width: 400px; max-width: 500px;
line-height: 1.5; line-height: 1.6;
font-size: 0.9375rem;
} }
/* Error icon styling using semantic classes */
.error-content::before {
content: '⚠️';
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.8;
}
/* Compact error state for inline usage */
.error-content.compact {
min-height: auto;
padding: 1.5rem;
gap: 0.75rem;
}
.error-content.compact h2 {
font-size: 1.25rem;
margin-bottom: 0.5rem;
}
.error-content.compact p {
margin-bottom: 1.5rem;
font-size: 0.875rem;
}
.error-content.compact::before {
font-size: 2rem;
margin-bottom: 0.5rem;
}
/* Error actions container using semantic flex utilities */
.error-actions { .error-actions {
display: flex; display: flex;
gap: 1rem; gap: 1rem;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex-wrap: wrap;
margin-top: 1rem;
} }
.button { /* Responsive design for mobile */
padding: 0.75rem 1.5rem; @media (max-width: 768px) {
border-radius: 0.5rem; .error-content {
font-weight: 500; padding: 1rem;
min-height: 30vh;
}
.error-content h2 {
font-size: 1.25rem;
}
.error-content p {
font-size: 0.875rem; font-size: 0.875rem;
border: none; max-width: 100%;
cursor: pointer;
transition: all 0.2s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
} }
.button.secondary { .error-actions {
background: #f9fafb; flex-direction: column;
color: #374151; width: 100%;
border: 1px solid #d1d5db;
} }
.button.secondary:hover { .error-actions .btn {
background: #f3f4f6; width: 100%;
border-color: #9ca3af; max-width: 280px;
} }
.back-link {
color: #3b82f6;
text-decoration: none;
font-weight: 500;
transition: color 0.2s ease;
}
.back-link:hover {
color: #2563eb;
text-decoration: underline;
} }

View File

@ -1,31 +1,39 @@
/* Loading State Component - Refactored to use semantic classes */
/* Use semantic .loading-content class from application.css */
.loading-content { .loading-content {
display: flex; /* Already defined in application.css with proper semantic styling */
justify-content: center; /* Override min-height if needed for this specific component */
align-items: center;
min-height: 200px; min-height: 200px;
padding: 2rem; padding: 2rem;
} }
/* Use semantic .loading-spinner class from application.css */
.loading-spinner { .loading-spinner {
color: #6b7280; /* Base styling already defined in application.css */
color: var(--text-secondary);
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 500; font-weight: 500;
display: inline-flex;
align-items: center;
gap: 0.75rem;
} }
/* Spinner icon using semantic animation and CSS variables */
.loading-spinner::after { .loading-spinner::after {
content: ''; content: '';
display: inline-block; display: inline-block;
width: 20px; width: 20px;
height: 20px; height: 20px;
margin-left: 10px; border: 2px solid var(--border-secondary);
border: 2px solid #e5e7eb; border-top: 2px solid var(--color-blue-500);
border-top: 2px solid #3b82f6;
border-radius: 50%; border-radius: 50%;
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
vertical-align: middle;
} }
/* Use semantic animation from application.css */
@keyframes spin { @keyframes spin {
/* Animation already defined in application.css */
0% { transform: rotate(0deg); } 0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); } 100% { transform: rotate(360deg); }
} }

View File

@ -4,15 +4,32 @@ import './LoadingState.css';
interface LoadingStateProps { interface LoadingStateProps {
message?: string; message?: string;
className?: string; className?: string;
size?: 'sm' | 'md' | 'lg';
centered?: boolean;
} }
export const LoadingState: React.FC<LoadingStateProps> = ({ export const LoadingState: React.FC<LoadingStateProps> = ({
message = "Loading...", message = "Loading...",
className = "" className = "",
size = 'md',
centered = true
}) => { }) => {
const containerClasses = [
centered ? 'loading-content' : 'flex items-center gap-3',
size === 'sm' && 'p-4 min-h-20',
size === 'lg' && 'p-8 min-h-32',
className
].filter(Boolean).join(' ');
const spinnerClasses = [
'loading-spinner',
size === 'sm' && 'text-xs',
size === 'lg' && 'text-base'
].filter(Boolean).join(' ');
return ( return (
<div className={`loading-content ${className}`.trim()}> <div className={containerClasses}>
<div className="loading-spinner">{message}</div> <div className={spinnerClasses}>{message}</div>
</div> </div>
); );
}; };

View File

@ -1,143 +1,48 @@
/* Slide Thumbnail Component */ /* Slide Thumbnail Component - Minimal overrides using semantic classes */
/* Component-specific layout for slide thumbnails */
.slide-thumbnail { .slide-thumbnail {
/* Override card-interactive to use flex column layout */
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border: 2px solid var(--border-primary); min-height: 160px;
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.2s ease;
background: var(--bg-secondary);
overflow: hidden; overflow: hidden;
position: relative;
} }
/* Interactive States - shared box-shadow */ /* Positioning for the badge number */
.slide-thumbnail:hover, .slide-thumbnail .badge-number {
.slide-thumbnail.active {
border-color: var(--border-accent);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15);
}
.slide-thumbnail.active {
background: var(--bg-accent);
}
/* Thumbnail Number Badge */
.thumbnail-number {
position: absolute; position: absolute;
top: 0.5rem; top: 0.5rem;
left: 0.5rem; left: 0.5rem;
background: var(--color-gray-700);
color: var(--color-white);
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 600;
z-index: 10; z-index: 10;
} }
.slide-thumbnail.active .thumbnail-number { /* Preview area specific styling */
background: var(--color-blue-500); .slide-thumbnail .preview-container {
} flex: 1;
/* Preview Area */
.thumbnail-preview {
padding: 2rem 1rem 1rem; padding: 2rem 1rem 1rem;
min-height: 80px; min-height: 80px;
background: var(--bg-muted);
display: flex;
align-items: center;
justify-content: center;
} }
.thumbnail-content { /* Component-specific drag handle positioning */
text-align: center; .slide-thumbnail .drag-handle {
position: absolute;
top: 50%;
right: 0.5rem;
transform: translateY(-50%);
} }
/* Content Labels - shared text styling */ /* Show drag handle on hover */
.thumbnail-content span { .slide-thumbnail:hover .drag-handle {
display: block; opacity: 1;
font-size: 0.75rem;
} }
.layout-name { /* Hide drag handle when not draggable */
font-weight: 600; .slide-thumbnail:not(.draggable) .drag-handle {
color: var(--text-secondary); display: none;
margin-bottom: 0.25rem;
}
.content-count {
font-size: 0.625rem; /* Override smaller size */
color: var(--text-tertiary);
}
/* Action Bar */
.thumbnail-actions {
display: flex;
justify-content: center;
gap: 0.25rem;
padding: 0.5rem;
background: var(--bg-secondary);
border-top: 1px solid var(--border-primary);
}
/* Action Buttons - consolidated common properties */
.thumbnail-action {
background: none;
border: none;
padding: 0.25rem;
border-radius: 0.25rem;
cursor: pointer;
font-size: 0.875rem;
transition: background-color 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
min-width: 28px;
height: 28px;
}
/* Button States - cascading hover effects */
.thumbnail-action:hover:not(:disabled) {
background: var(--bg-hover);
}
.thumbnail-action:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Specific Action Types - override general hover */
.thumbnail-action.edit:hover:not(:disabled) {
background: var(--color-blue-100);
}
.thumbnail-action.delete:hover:not(:disabled) {
background: var(--color-red-100);
}
/* Drag and Drop Styles */
.slide-thumbnail[draggable="true"] {
cursor: grab;
}
.slide-thumbnail[draggable="true"]:active {
cursor: grabbing;
}
.slide-thumbnail.dragged {
opacity: 0.5;
transform: rotate(2deg);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
}
.slide-thumbnail.drag-over {
border-color: var(--color-blue-500);
background: var(--color-blue-50);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(59, 130, 246, 0.3);
} }
/* Custom drop indicator line for slide thumbnails */
.slide-thumbnail.drag-over::before { .slide-thumbnail.drag-over::before {
content: ''; content: '';
position: absolute; position: absolute;
@ -147,27 +52,5 @@
height: 2px; height: 2px;
background: var(--color-blue-500); background: var(--color-blue-500);
border-radius: 1px; border-radius: 1px;
} z-index: 15;
/* Drag Handle */
.drag-handle {
position: absolute;
top: 50%;
right: 0.5rem;
transform: translateY(-50%);
color: var(--text-tertiary);
font-size: 0.875rem;
font-weight: bold;
opacity: 0;
transition: opacity 0.2s ease;
pointer-events: none;
z-index: 5;
}
.slide-thumbnail:hover .drag-handle {
opacity: 1;
}
.slide-thumbnail[draggable="false"] .drag-handle {
display: none;
} }

View File

@ -51,16 +51,24 @@ export const SlideThumbnail: React.FC<SlideThumbnailProps> = ({
onDoubleClick(); onDoubleClick();
}; };
const className = [ // Build semantic class names
'slide-thumbnail', const containerClasses = [
isActive && 'active', 'card-interactive',
'slide-thumbnail', // Keep for component-specific overrides
isActive && 'selected',
isDragged && 'dragged', isDragged && 'dragged',
isDraggedOver && 'drag-over' isDraggedOver && 'drag-over',
!isDisabled && 'draggable'
].filter(Boolean).join(' ');
const badgeClasses = [
'badge-number',
isActive && 'active'
].filter(Boolean).join(' '); ].filter(Boolean).join(' ');
return ( return (
<div <div
className={className} className={containerClasses}
draggable={!isDisabled} draggable={!isDisabled}
onClick={handleClick} onClick={handleClick}
onDoubleClick={handleDoubleClick} onDoubleClick={handleDoubleClick}
@ -71,19 +79,23 @@ export const SlideThumbnail: React.FC<SlideThumbnailProps> = ({
onDrop={onDrop} onDrop={onDrop}
title={`Slide ${index + 1}${isDragged ? ' (dragging...)' : ''}`} title={`Slide ${index + 1}${isDragged ? ' (dragging...)' : ''}`}
> >
<div className="thumbnail-number">{index + 1}</div> <div className={badgeClasses}>{index + 1}</div>
<div className="thumbnail-preview">
<div className="thumbnail-content"> <div className="preview-container">
<span className="layout-name">{slide.layoutId}</span> <div className="preview-content">
<span className="content-count"> <div className="text-center">
<span className="font-semibold text-secondary text-xs block mb-1">{slide.layoutId}</span>
<span className="text-xs text-tertiary">
{Object.keys(slide.content).length} items {Object.keys(slide.content).length} items
</span> </span>
</div> </div>
</div> </div>
<div className="thumbnail-actions"> </div>
<div className="action-bar">
<button <button
type="button" type="button"
className="thumbnail-action edit" className="action-btn edit"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onEdit(); onEdit();
@ -95,7 +107,7 @@ export const SlideThumbnail: React.FC<SlideThumbnailProps> = ({
</button> </button>
<button <button
type="button" type="button"
className="thumbnail-action" className="action-btn"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onDuplicate(); onDuplicate();
@ -107,7 +119,7 @@ export const SlideThumbnail: React.FC<SlideThumbnailProps> = ({
</button> </button>
<button <button
type="button" type="button"
className="thumbnail-action delete" className="action-btn delete"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onDelete(); onDelete();

View File

@ -1,62 +1,67 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom';
interface ErrorStateProps { interface ErrorStateProps {
error: string; error: string;
presentationId?: string; presentationId?: string;
title?: string;
backText?: string;
backLink?: string;
} }
export const ErrorState: React.FC<ErrorStateProps> = ({ error, presentationId }) => { export const ErrorState: React.FC<ErrorStateProps> = ({
error,
presentationId,
title = "Slide Editor Error",
backText,
backLink
}) => {
// Determine back navigation
const getBackNavigation = () => {
if (backLink && backText) {
return { href: backLink, text: backText };
}
if (presentationId) {
return {
href: `/presentations/${presentationId}/edit/slides/1`,
text: "← Back to Presentation"
};
}
return { href: "/presentations", text: "← Back to Presentations" };
};
const { href, text } = getBackNavigation();
return ( return (
<div className="slide-editor"> <div className="page-container">
<header className="slide-editor-header"> <header className="page-header">
<div className="editor-info"> <a href={href} className="back-link">
{presentationId ? ( {text}
<Link to={`/presentations/${presentationId}/edit/slides/1`} className="back-button"> </a>
Back to Presentation <div className="flex-1">
</Link> <h1 className="text-xl font-semibold text-primary m-0">{title}</h1>
) : (
<Link to="/presentations" className="back-button">
Back to Presentations
</Link>
)}
<div className="editor-title">
<h1>Slide Editor Error</h1>
</div>
</div> </div>
</header> </header>
<main className="slide-editor-content"> <main className="page-content">
<div className="error-container">
<div className="error-content"> <div className="error-content">
<h2>Unable to Load Slide Editor</h2> <h2>Unable to Load Content</h2>
<p>{error}</p> <p>{error}</p>
<div className="error-actions"> <div className="flex gap-4 justify-center flex-wrap">
<button <button
type="button" type="button"
onClick={() => window.location.reload()} onClick={() => window.location.reload()}
className="action-button primary" className="btn btn-primary"
> >
Reload Page Reload Page
</button> </button>
{presentationId ? ( <a
<Link href={href}
to={`/presentations/${presentationId}/edit/slides/1`} className="btn btn-secondary"
className="action-button secondary"
> >
Return to Presentation {presentationId ? "Return to Presentation" : "Back to Presentations"}
</Link> </a>
) : (
<Link
to="/presentations"
className="action-button secondary"
>
Back to Presentations
</Link>
)}
</div>
</div> </div>
</div> </div>
</main> </main>

View File

@ -1,6 +1,8 @@
/* Application CSS - Consolidated Styles */ /* Application CSS - Consolidated Styles */
/* Import color system first */ /* Import color system first */
@import './colors.css'; @import './colors.css';
/* Import semantic component classes */
@import './semantic-components.css';
/* ================================================================= /* =================================================================
BASE APP LAYOUT BASE APP LAYOUT
@ -85,6 +87,15 @@
.fixed { position: fixed; } .fixed { position: fixed; }
.sticky { position: sticky; } .sticky { position: sticky; }
/* Positioning utilities */
.top-1\/2 { top: 50%; }
.left-1\/2 { left: 50%; }
/* Transform utilities */
.transform { transform: translateX(var(--tw-translate-x, 0)) translateY(var(--tw-translate-y, 0)) rotate(var(--tw-rotate, 0)) skewX(var(--tw-skew-x, 0)) skewY(var(--tw-skew-y, 0)) scaleX(var(--tw-scale-x, 1)) scaleY(var(--tw-scale-y, 1)); }
.-translate-x-1\/2 { --tw-translate-x: -50%; }
.-translate-y-1\/2 { --tw-translate-y: -50%; }
/* ================================================================= /* =================================================================
UTILITY CLASSES - Spacing UTILITY CLASSES - Spacing
================================================================= */ ================================================================= */
@ -229,15 +240,6 @@
.shadow-md { box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); } .shadow-md { box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); }
.shadow-lg { box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); } .shadow-lg { box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); }
/* =================================================================
UTILITY CLASSES - Aspect Ratios
================================================================= */
.aspect-16-9 { aspect-ratio: 16 / 9; }
.aspect-4-3 { aspect-ratio: 4 / 3; }
.aspect-16-10 { aspect-ratio: 16 / 10; }
.aspect-square { aspect-ratio: 1 / 1; }
/* ================================================================= /* =================================================================
UTILITY CLASSES - Transitions & Animations UTILITY CLASSES - Transitions & Animations
================================================================= */ ================================================================= */
@ -779,3 +781,33 @@
padding: 1.5rem 1rem; padding: 1.5rem 1rem;
} }
} }
/* =================================================================
UTILITY CLASSES - Background Colors
================================================================= */
.bg-primary { background-color: var(--bg-primary); }
.bg-secondary { background-color: var(--bg-secondary); }
.bg-muted { background-color: var(--bg-muted); }
.bg-accent { background-color: var(--text-accent); }
.bg-hover { background-color: var(--bg-hover); }
/* Background hover states */
.hover\:bg-accent-hover:hover { background-color: var(--text-link-hover); }
/* =================================================================
UTILITY CLASSES - Text Colors
================================================================= */
.text-primary { color: var(--text-primary); }
.text-secondary { color: var(--text-secondary); }
.text-tertiary { color: var(--text-tertiary); }
.text-white { color: var(--color-white); }
.text-warning { color: var(--text-warning); }
/* =================================================================
UTILITY CLASSES - Overflow
================================================================= */
.overflow-y-auto { overflow-y: auto; }
.overflow-hidden { overflow: hidden; }

View File

@ -0,0 +1,384 @@
/* Semantic Component Classes - Consolidation of common patterns */
/* =================================================================
CARD VARIANTS - Consolidate duplicate card patterns
================================================================= */
/* Interactive Card - Clickable cards with hover states */
.card-interactive {
background: var(--bg-secondary);
border: 2px solid var(--border-primary);
border-radius: 0.75rem;
cursor: pointer;
transition: all 0.2s ease;
overflow: hidden;
position: relative;
}
.card-interactive:hover {
border-color: var(--border-hover);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.card-interactive:focus {
outline: none;
border-color: var(--border-accent);
box-shadow: 0 0 0 3px var(--bg-accent);
}
.card-interactive.selected {
border-color: var(--border-accent);
background: var(--bg-accent);
box-shadow: 0 4px 6px -1px rgba(59, 130, 246, 0.2);
}
/* Selection Indicator */
.selection-indicator {
position: absolute;
top: 0.75rem;
right: 0.75rem;
z-index: 10;
color: var(--color-success);
font-size: 1.25rem;
}
/* =================================================================
BADGE SYSTEM - Consistent status indicators
================================================================= */
.badge {
display: inline-flex;
align-items: center;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 500;
line-height: 1;
}
.badge-primary {
background: var(--bg-accent);
color: var(--text-accent);
}
.badge-secondary {
background: var(--bg-muted);
color: var(--text-secondary);
}
.badge-success {
background: var(--color-success-bg, #dcfce7);
color: var(--color-success, #16a34a);
}
.badge-error {
background: var(--color-red-100);
color: var(--text-error);
}
/* Number Badge - For counts, slide numbers, etc. */
.badge-number {
background: var(--color-gray-700);
color: var(--color-white);
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 600;
min-width: 1.5rem;
text-align: center;
}
.badge-number.active {
background: var(--color-blue-500);
}
/* =================================================================
PREVIEW CONTAINERS - Consistent preview areas
================================================================= */
.preview-container {
position: relative;
background: var(--bg-muted);
border: 1px solid var(--border-primary);
border-radius: 0.5rem;
overflow: hidden;
}
.preview-content {
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
min-height: 80px;
}
.preview-placeholder {
color: var(--text-tertiary);
font-size: 0.875rem;
text-align: center;
font-style: italic;
}
/* Color Preview Strip */
.color-preview-strip {
display: flex;
gap: 0.5rem;
align-items: center;
flex-wrap: wrap;
justify-content: center;
}
.color-swatch {
width: 48px;
height: 48px;
border-radius: 50%;
border: 3px solid var(--bg-secondary);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
flex-shrink: 0;
}
@media (max-width: 768px) {
.color-swatch {
width: 36px;
height: 36px;
}
}
/* =================================================================
ACTION BARS - Consistent action button layouts
================================================================= */
.action-bar {
display: flex;
justify-content: center;
gap: 0.25rem;
padding: 0.5rem;
background: var(--bg-secondary);
border-top: 1px solid var(--border-primary);
}
.action-bar.justify-between {
justify-content: space-between;
}
.action-bar.justify-end {
justify-content: flex-end;
}
/* Action Button - Small buttons for action bars */
.action-btn {
background: none;
border: none;
padding: 0.25rem;
border-radius: 0.25rem;
cursor: pointer;
font-size: 0.875rem;
transition: background-color 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
min-width: 28px;
height: 28px;
color: var(--text-secondary);
}
.action-btn:hover:not(:disabled) {
background: var(--bg-hover);
color: var(--text-primary);
}
.action-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.action-btn.edit:hover:not(:disabled) {
background: var(--color-blue-100);
color: var(--color-blue-600);
}
.action-btn.delete:hover:not(:disabled) {
background: var(--color-red-100);
color: var(--text-error);
}
/* =================================================================
GRID LAYOUTS - Consistent responsive grids
================================================================= */
.grid-responsive {
display: grid;
gap: 1.5rem;
}
.grid-responsive-sm {
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}
.grid-responsive-md {
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
}
.grid-responsive-lg {
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
}
@media (max-width: 768px) {
.grid-responsive-sm,
.grid-responsive-md,
.grid-responsive-lg {
grid-template-columns: 1fr;
gap: 1rem;
}
}
/* =================================================================
METADATA DISPLAY - Consistent info layouts
================================================================= */
.meta-info {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
font-size: 0.75rem;
color: var(--text-tertiary);
}
.meta-stats {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.meta-stat {
display: flex;
align-items: center;
gap: 0.5rem;
}
.meta-stat-label {
font-weight: 500;
color: var(--text-secondary);
}
.meta-stat-value {
color: var(--text-tertiary);
font-family: 'Courier New', monospace;
}
@media (max-width: 768px) {
.meta-info {
flex-direction: column;
align-items: flex-start;
gap: 0.75rem;
}
}
/* =================================================================
DRAG & DROP STATES - Consistent drag feedback
================================================================= */
.draggable {
cursor: grab;
}
.draggable:active {
cursor: grabbing;
}
.dragged {
opacity: 0.5;
transform: rotate(2deg);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
}
.drag-over {
border-color: var(--color-blue-500);
background: var(--color-blue-50);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(59, 130, 246, 0.3);
}
.drag-over::before {
content: '';
position: absolute;
top: -2px;
left: 0;
right: 0;
height: 2px;
background: var(--color-blue-500);
border-radius: 1px;
}
.drag-handle {
position: absolute;
top: 50%;
right: 0.5rem;
transform: translateY(-50%);
color: var(--text-tertiary);
font-size: 0.875rem;
font-weight: bold;
opacity: 0;
transition: opacity 0.2s ease;
pointer-events: none;
z-index: 5;
}
.draggable:hover .drag-handle {
opacity: 1;
}
/* =================================================================
LINK STYLES - Consistent link appearances
================================================================= */
.link {
color: var(--text-link);
text-decoration: none;
transition: color 0.2s ease;
}
.link:hover {
color: var(--text-link-hover);
text-decoration: underline;
}
.link-subtle {
color: var(--text-secondary);
text-decoration: none;
font-size: 0.75rem;
font-weight: 500;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
transition: all 0.2s ease;
}
.link-subtle:hover {
background: var(--bg-hover);
text-decoration: none;
color: var(--text-primary);
}
/* Back Link - Consistent navigation */
.back-link {
background: transparent;
border: none;
color: var(--text-tertiary);
font-size: 0.875rem;
font-weight: 500;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
cursor: pointer;
transition: all 0.2s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.back-link:hover {
background: var(--bg-hover);
color: var(--text-primary);
text-decoration: none;
}