Compare commits
3 Commits
7bd25e1a7a
...
1a3b096cf9
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a3b096cf9 | |||
| 1c2bb703f3 | |||
| d25eb56794 |
40
.github/workflows/build.yml
vendored
Normal file
40
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: linux_amd64
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=2048'
|
||||
|
||||
- name: Extract hostname from package.json
|
||||
id: get-hostname
|
||||
run: |
|
||||
HOSTNAME=$(node -p "require('./package.json').deployHostname")
|
||||
echo "hostname=$HOSTNAME" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Deploy to nginx
|
||||
run: |
|
||||
DEPLOY_PATH="/var/www/localhost/${{ steps.get-hostname.outputs.hostname }}"
|
||||
|
||||
# Create directory if it doesn't exist
|
||||
mkdir -p "$DEPLOY_PATH"
|
||||
|
||||
# Copy built files (overwrite existing)
|
||||
cp -r dist/* "$DEPLOY_PATH/"
|
||||
echo "Deployed to $DEPLOY_PATH"
|
||||
247
RESTYLE.md
Normal file
247
RESTYLE.md
Normal 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*
|
||||
@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "slideshare",
|
||||
"deployHostname": "slides.digital-experiment.com",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "npm run generate-manifest && vite",
|
||||
|
||||
@ -15,5 +15,5 @@
|
||||
"hasMasterSlide": true
|
||||
}
|
||||
},
|
||||
"generated": "2025-09-12T14:15:01.593Z"
|
||||
"generated": "2025-09-24T21:37:00.344Z"
|
||||
}
|
||||
@ -1,3 +1,5 @@
|
||||
/* Presentation Editor - Minimal component-specific styles only */
|
||||
|
||||
.presentation-editor {
|
||||
min-height: 100vh;
|
||||
background: var(--bg-primary);
|
||||
@ -5,161 +7,28 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.editor-header {
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border-primary);
|
||||
padding: 1rem 2rem;
|
||||
/* Component-specific layout overrides only */
|
||||
.editor-layout {
|
||||
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;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
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 {
|
||||
.slide-editor-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-presentation {
|
||||
.slide-editor {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
flex-direction: column;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.empty-content {
|
||||
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 in empty state - component specific */
|
||||
.theme-preview {
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-primary);
|
||||
@ -173,19 +42,6 @@
|
||||
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 {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
@ -193,14 +49,6 @@
|
||||
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 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
@ -229,361 +77,12 @@
|
||||
background: var(--color-slate-400);
|
||||
}
|
||||
|
||||
.layout-preview-card {
|
||||
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 positioning in editor context */
|
||||
.slide-thumbnail {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Layout-related thumbnail styles moved to SlideThumbnail.css */
|
||||
|
||||
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.editor-layout {
|
||||
flex-direction: column;
|
||||
@ -592,32 +91,5 @@
|
||||
.slide-sidebar {
|
||||
width: 100%;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -40,7 +40,7 @@ export const PresentationEditor: React.FC = () => {
|
||||
confirmDelete
|
||||
} = useDialog();
|
||||
|
||||
const { duplicateSlide, deleteSlide, saving } = useSlideOperations({
|
||||
const { duplicateSlide, deleteSlide, reorderSlides, saving } = useSlideOperations({
|
||||
presentation,
|
||||
presentationId: presentationId || '',
|
||||
onPresentationUpdate: setPresentation,
|
||||
@ -140,25 +140,23 @@ export const PresentationEditor: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="presentation-editor">
|
||||
<header className="editor-header">
|
||||
<div className="presentation-info">
|
||||
<Link to="/presentations" className="back-link">← Back to Presentations</Link>
|
||||
<div className="presentation-title">
|
||||
<span>{presentation.metadata.name}</span>
|
||||
{presentation.metadata.description && (
|
||||
<span className="presentation-description">{presentation.metadata.description}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="presentation-meta">
|
||||
<span className="slide-counter">
|
||||
{totalSlides === 0 ? 'No slides' : `Slide ${currentSlideIndex + 1} of ${totalSlides}`}
|
||||
</span>
|
||||
{saving && <span className="saving-indicator">Saving...</span>}
|
||||
</div>
|
||||
<header className="page-header">
|
||||
<Link to="/presentations" className="back-link">← Back to Presentations</Link>
|
||||
<div className="flex-1">
|
||||
<h1 className="text-xl font-semibold text-primary m-0">{presentation.metadata.name}</h1>
|
||||
{presentation.metadata.description && (
|
||||
<p className="text-sm text-secondary mt-1 m-0">{presentation.metadata.description}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-4 items-center">
|
||||
<span className="badge-secondary">
|
||||
{totalSlides === 0 ? 'No slides' : `Slide ${currentSlideIndex + 1} of ${totalSlides}`}
|
||||
</span>
|
||||
{saving && <span className="text-sm text-warning font-medium">Saving...</span>}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="editor-content">
|
||||
<main className="flex-1 flex flex-col">
|
||||
{totalSlides === 0 ? (
|
||||
<EmptyPresentationState
|
||||
theme={theme}
|
||||
@ -176,54 +174,55 @@ export const PresentationEditor: React.FC = () => {
|
||||
onEditSlide={(slideId) => navigate(`/presentations/${presentationId}/slide/${slideId}/edit`)}
|
||||
onDuplicateSlide={duplicateSlide}
|
||||
onDeleteSlide={deleteSlide}
|
||||
onReorderSlides={reorderSlides}
|
||||
/>
|
||||
|
||||
<div className="slide-editor-area">
|
||||
{currentSlide ? (
|
||||
<div className="slide-editor">
|
||||
<div className="slide-header">
|
||||
<h3>Slide {currentSlideIndex + 1}</h3>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-semibold text-primary m-0">Slide {currentSlideIndex + 1}</h3>
|
||||
</div>
|
||||
<div className="slide-content-editor">
|
||||
<div className="content-preview">
|
||||
<div className="flex-1">
|
||||
<div className="card p-6">
|
||||
{/* TODO: Render actual slide content with editing capabilities */}
|
||||
<div className="editor-placeholder">
|
||||
<div className="content-slots">
|
||||
<div className="text-center">
|
||||
<div className="flex flex-col gap-3 mb-6">
|
||||
{Object.entries(currentSlide.content).map(([slotId, content]) => (
|
||||
<div key={slotId} className="content-slot">
|
||||
<label>{slotId}:</label>
|
||||
<div className="slot-content">{content || '(empty)'}</div>
|
||||
<div key={slotId} className="flex justify-between items-center p-3 bg-muted rounded">
|
||||
<label className="font-medium text-secondary">{slotId}:</label>
|
||||
<div className="text-tertiary">{content || '(empty)'}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="placeholder-note">
|
||||
<p className="text-tertiary text-sm">
|
||||
Interactive slide editor will be implemented next
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{currentSlide.notes && (
|
||||
<div className="slide-notes-editor">
|
||||
<h4>Speaker Notes</h4>
|
||||
<textarea
|
||||
<div className="mt-4">
|
||||
<h4 className="text-base font-semibold text-primary mb-2">Speaker Notes</h4>
|
||||
<textarea
|
||||
value={currentSlide.notes}
|
||||
onChange={(e) => {
|
||||
// TODO: Update slide notes
|
||||
loggers.presentation.debug('Slide notes updated', { slideId: currentSlide.id, notesLength: e.target.value.length });
|
||||
}}
|
||||
placeholder="Add speaker notes for this slide..."
|
||||
className="notes-textarea"
|
||||
className="form-textarea"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="slide-error">
|
||||
<p>Invalid slide number</p>
|
||||
<button
|
||||
<div className="error-content">
|
||||
<h2>Invalid slide number</h2>
|
||||
<button
|
||||
type="button"
|
||||
className="action-button secondary"
|
||||
className="btn btn-secondary"
|
||||
onClick={() => goToSlide(0)}
|
||||
>
|
||||
Go to First Slide
|
||||
@ -258,4 +257,5 @@ export const PresentationEditor: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
/* Fullscreen presentation mode styles */
|
||||
/* Presentation Mode - Surgical override approach to preserve theme styling */
|
||||
|
||||
/* Fullscreen presentation mode container */
|
||||
.presentation-mode {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: #000;
|
||||
background: var(--color-black);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -13,150 +15,114 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.presentation-mode.loading,
|
||||
.presentation-mode.error {
|
||||
flex-direction: column;
|
||||
background: #1a1a1a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.presentation-mode .loading-spinner {
|
||||
font-size: 1.2rem;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.presentation-mode .error-content {
|
||||
text-align: center;
|
||||
max-width: 600px;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.presentation-mode .error-content h2 {
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 2rem;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.presentation-mode .error-content p {
|
||||
margin: 0 0 2rem 0;
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.8;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.presentation-mode .exit-button {
|
||||
background: #fff;
|
||||
color: #000;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
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;
|
||||
/* ===================================================================
|
||||
SLIDE CONTENT - Minimal interference with theme styling
|
||||
=================================================================== */
|
||||
|
||||
/* Override only the specific semantic framework classes that conflict */
|
||||
.presentation-mode .slide-content .text-primary,
|
||||
.presentation-mode .slide-content .text-secondary,
|
||||
.presentation-mode .slide-content .text-tertiary,
|
||||
.presentation-mode .slide-content .text-white,
|
||||
.presentation-mode .slide-content .bg-primary,
|
||||
.presentation-mode .slide-content .bg-secondary,
|
||||
.presentation-mode .slide-content .bg-muted,
|
||||
.presentation-mode .slide-content .bg-accent,
|
||||
.presentation-mode .slide-content .flex,
|
||||
.presentation-mode .slide-content .flex-col,
|
||||
.presentation-mode .slide-content .flex-row,
|
||||
.presentation-mode .slide-content .items-center,
|
||||
.presentation-mode .slide-content .justify-center,
|
||||
.presentation-mode .slide-content .gap-1,
|
||||
.presentation-mode .slide-content .gap-2,
|
||||
.presentation-mode .slide-content .gap-3,
|
||||
.presentation-mode .slide-content .gap-4,
|
||||
.presentation-mode .slide-content .p-1,
|
||||
.presentation-mode .slide-content .p-2,
|
||||
.presentation-mode .slide-content .p-3,
|
||||
.presentation-mode .slide-content .p-4,
|
||||
.presentation-mode .slide-content .m-1,
|
||||
.presentation-mode .slide-content .m-2,
|
||||
.presentation-mode .slide-content .m-3,
|
||||
.presentation-mode .slide-content .m-4,
|
||||
.presentation-mode .slide-content .rounded,
|
||||
.presentation-mode .slide-content .rounded-lg,
|
||||
.presentation-mode .slide-content .shadow,
|
||||
.presentation-mode .slide-content .btn,
|
||||
.presentation-mode .slide-content .card {
|
||||
/* Disable semantic classes within slide content */
|
||||
all: revert !important;
|
||||
}
|
||||
|
||||
/* Ensure theme variables take precedence over semantic framework */
|
||||
.presentation-mode .slide-content {
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
background: #fff;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
/* Let theme CSS control fonts and colors */
|
||||
font-family: var(--theme-font-body, system-ui, sans-serif) !important;
|
||||
background: var(--theme-background, var(--color-black)) !important;
|
||||
color: var(--theme-text, white) !important;
|
||||
}
|
||||
|
||||
/* Aspect ratio classes for slides */
|
||||
.presentation-mode .slide-content.aspect-16-9 {
|
||||
aspect-ratio: 16/9;
|
||||
width: min(90vw, calc(90vh * 16/9));
|
||||
/* Force theme slot styling to override semantic framework */
|
||||
.presentation-mode .slide-content .slot[data-slot="title"] {
|
||||
color: var(--theme-primary, white) !important;
|
||||
font-family: var(--theme-font-heading, var(--theme-font-body, system-ui, sans-serif)) !important;
|
||||
}
|
||||
|
||||
.presentation-mode .slide-content.aspect-4-3 {
|
||||
aspect-ratio: 4/3;
|
||||
width: min(90vw, calc(90vh * 4/3));
|
||||
.presentation-mode .slide-content .slot[data-slot="subtitle"],
|
||||
.presentation-mode .slide-content .slot[data-slot="content"],
|
||||
.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 {
|
||||
aspect-ratio: 16/10;
|
||||
width: min(90vw, calc(90vh * 16/10));
|
||||
/* Ensure code blocks work properly */
|
||||
.presentation-mode .slide-content .slide-code,
|
||||
.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 */
|
||||
.presentation-mode .navigation-indicator {
|
||||
/* Ensure mermaid diagrams work properly */
|
||||
.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;
|
||||
bottom: 2rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
color: #fff;
|
||||
color: var(--color-white);
|
||||
padding: 1rem 2rem;
|
||||
border-radius: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
z-index: 10000;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.presentation-mode:hover .navigation-indicator {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.presentation-mode .slide-counter {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
/* Ensure navigation text uses white color and proper fonts */
|
||||
.navigation-indicator,
|
||||
.navigation-indicator * {
|
||||
color: var(--color-white) !important;
|
||||
font-family: system-ui, -apple-system, sans-serif !important;
|
||||
}
|
||||
|
||||
/* Hide scrollbars in presentation mode */
|
||||
@ -169,8 +135,11 @@
|
||||
display: none; /* Chrome/Safari */
|
||||
}
|
||||
|
||||
/* Ensure theme styles work in fullscreen */
|
||||
.presentation-mode .slide-content > * {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.navigation-indicator {
|
||||
bottom: 1rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,19 +123,21 @@ export const PresentationMode: React.FC = () => {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="presentation-mode loading">
|
||||
<div className="loading-spinner">Loading presentation...</div>
|
||||
<div className="presentation-mode">
|
||||
<div className="loading-content text-white">
|
||||
<div className="loading-spinner">Loading presentation...</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="presentation-mode error">
|
||||
<div className="error-content">
|
||||
<div className="presentation-mode">
|
||||
<div className="error-content text-white">
|
||||
<h2>Error Loading Presentation</h2>
|
||||
<p>{error}</p>
|
||||
<button onClick={() => navigate(-1)} className="exit-button">
|
||||
<button onClick={() => navigate(-1)} className="btn btn-primary">
|
||||
Exit Presentation Mode
|
||||
</button>
|
||||
</div>
|
||||
@ -145,10 +147,10 @@ export const PresentationMode: React.FC = () => {
|
||||
|
||||
if (!presentation || !theme) {
|
||||
return (
|
||||
<div className="presentation-mode error">
|
||||
<div className="error-content">
|
||||
<div className="presentation-mode">
|
||||
<div className="error-content text-white">
|
||||
<h2>Presentation Not Found</h2>
|
||||
<button onClick={() => navigate(-1)} className="exit-button">
|
||||
<button onClick={() => navigate(-1)} className="btn btn-primary">
|
||||
Exit Presentation Mode
|
||||
</button>
|
||||
</div>
|
||||
@ -158,11 +160,11 @@ export const PresentationMode: React.FC = () => {
|
||||
|
||||
if (presentation.slides.length === 0) {
|
||||
return (
|
||||
<div className="presentation-mode error">
|
||||
<div className="error-content">
|
||||
<div className="presentation-mode">
|
||||
<div className="error-content text-white">
|
||||
<h2>No Slides Available</h2>
|
||||
<p>This presentation is empty.</p>
|
||||
<button onClick={() => navigate(-1)} className="exit-button">
|
||||
<button onClick={() => navigate(-1)} className="btn btn-primary">
|
||||
Exit Presentation Mode
|
||||
</button>
|
||||
</div>
|
||||
@ -173,16 +175,16 @@ export const PresentationMode: React.FC = () => {
|
||||
const totalSlides = presentation.slides.length;
|
||||
|
||||
return (
|
||||
<div className="presentation-mode fullscreen">
|
||||
<div className="slide-container">
|
||||
<div className="presentation-mode">
|
||||
<div className={`slide-container ${presentation.metadata.aspectRatio ? `aspect-${presentation.metadata.aspectRatio.replace(':', '-')}` : 'aspect-16-9'}`}>
|
||||
{isRenderingSlide && (
|
||||
<div className="slide-loading">
|
||||
<div className="loading-spinner"></div>
|
||||
<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 text-xl opacity-80"></div>
|
||||
<span>Rendering slide...</span>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`slide-content ${presentation.metadata.aspectRatio ? `aspect-${presentation.metadata.aspectRatio.replace(':', '-')}` : 'aspect-16-9'}`}
|
||||
className="slide-content"
|
||||
dangerouslySetInnerHTML={{ __html: renderedSlideContent }}
|
||||
style={{ opacity: isRenderingSlide ? 0.5 : 1 }}
|
||||
/>
|
||||
@ -190,13 +192,15 @@ export const PresentationMode: React.FC = () => {
|
||||
|
||||
{/* Navigation indicator */}
|
||||
<div className="navigation-indicator">
|
||||
<span className="slide-counter">
|
||||
{currentSlideIndex + 1} / {totalSlides}
|
||||
</span>
|
||||
|
||||
<div className="navigation-hints">
|
||||
<span>← → Space: Navigate</span>
|
||||
<span>Esc: Exit</span>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<span className="text-lg font-medium">
|
||||
{currentSlideIndex + 1} / {totalSlides}
|
||||
</span>
|
||||
|
||||
<div className="flex gap-4 text-sm opacity-80">
|
||||
<span>← → Space: Navigate</span>
|
||||
<span>Esc: Exit</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,53 +1,21 @@
|
||||
/* Slides Sidebar Component - Minimal overrides using semantic classes */
|
||||
|
||||
/* Component-specific layout only */
|
||||
.slide-sidebar {
|
||||
width: 240px;
|
||||
background: #ffffff;
|
||||
border-right: 1px solid #e5e7eb;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
background: #f9fafb;
|
||||
/* Custom hover state for accent button */
|
||||
.slide-sidebar .bg-accent:hover:not(:disabled) {
|
||||
background: var(--text-link-hover);
|
||||
}
|
||||
|
||||
.sidebar-header h3 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin: 0;
|
||||
/* Responsive behavior for mobile */
|
||||
@media (max-width: 768px) {
|
||||
.slide-sidebar {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import type { Slide } from '../../types/presentation.ts';
|
||||
import { SlideThumbnail } from './shared/SlideThumbnail.tsx';
|
||||
import './SlidesSidebar.css';
|
||||
@ -13,6 +13,7 @@ interface SlidesSidebarProps {
|
||||
onEditSlide: (slideId: string) => void;
|
||||
onDuplicateSlide: (index: number) => void;
|
||||
onDeleteSlide: (index: number) => void;
|
||||
onReorderSlides: (fromIndex: number, toIndex: number) => void;
|
||||
}
|
||||
|
||||
export const SlidesSidebar: React.FC<SlidesSidebarProps> = ({
|
||||
@ -24,23 +25,74 @@ export const SlidesSidebar: React.FC<SlidesSidebarProps> = ({
|
||||
onAddSlide,
|
||||
onEditSlide,
|
||||
onDuplicateSlide,
|
||||
onDeleteSlide
|
||||
onDeleteSlide,
|
||||
onReorderSlides
|
||||
}) => {
|
||||
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
|
||||
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
|
||||
|
||||
const handleDragStart = (e: React.DragEvent, index: number) => {
|
||||
if (saving) return;
|
||||
|
||||
setDraggedIndex(index);
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/plain', index.toString());
|
||||
|
||||
// Add drag image
|
||||
if (e.currentTarget instanceof HTMLElement) {
|
||||
e.currentTarget.style.opacity = '0.5';
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragEnd = (e: React.DragEvent) => {
|
||||
if (e.currentTarget instanceof HTMLElement) {
|
||||
e.currentTarget.style.opacity = '';
|
||||
}
|
||||
setDraggedIndex(null);
|
||||
setDragOverIndex(null);
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent, index: number) => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
|
||||
if (draggedIndex !== null && index !== draggedIndex) {
|
||||
setDragOverIndex(index);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragLeave = () => {
|
||||
setDragOverIndex(null);
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent, toIndex: number) => {
|
||||
e.preventDefault();
|
||||
|
||||
const fromIndex = draggedIndex;
|
||||
if (fromIndex !== null && fromIndex !== toIndex && !saving) {
|
||||
onReorderSlides(fromIndex, toIndex);
|
||||
}
|
||||
|
||||
setDraggedIndex(null);
|
||||
setDragOverIndex(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<aside className="slide-sidebar">
|
||||
<div className="sidebar-header">
|
||||
<h3>Slides</h3>
|
||||
<button
|
||||
<header className="flex justify-between items-center p-4 border-b bg-muted">
|
||||
<h3 className="text-base font-semibold text-primary m-0">Slides</h3>
|
||||
<button
|
||||
type="button"
|
||||
className="add-slide-button"
|
||||
className="btn-icon rounded-full bg-accent text-white hover:bg-accent-hover"
|
||||
onClick={onAddSlide}
|
||||
title="Add new slide"
|
||||
disabled={saving}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="slides-list">
|
||||
</header>
|
||||
|
||||
<div className="flex-1 p-4 flex flex-col gap-4 overflow-y-auto">
|
||||
{slides.map((slide, index) => (
|
||||
<SlideThumbnail
|
||||
key={slide.id}
|
||||
@ -48,11 +100,18 @@ export const SlidesSidebar: React.FC<SlidesSidebarProps> = ({
|
||||
index={index}
|
||||
isActive={index === currentSlideIndex}
|
||||
isDisabled={saving}
|
||||
isDragged={draggedIndex === index}
|
||||
isDraggedOver={dragOverIndex === index}
|
||||
onClick={() => onSlideClick(index)}
|
||||
onDoubleClick={() => onSlideDoubleClick(slide.id)}
|
||||
onEdit={() => onEditSlide(slide.id)}
|
||||
onDuplicate={() => onDuplicateSlide(index)}
|
||||
onDelete={() => onDeleteSlide(index)}
|
||||
onDragStart={(e) => handleDragStart(e, index)}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragOver={(e) => handleDragOver(e, index)}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={(e) => handleDrop(e, index)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -2,73 +2,18 @@
|
||||
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 {
|
||||
position: relative;
|
||||
height: 120px;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
|
||||
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-muted) 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
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 {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
@ -77,42 +22,27 @@
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.theme-description {
|
||||
margin: 0 0 1rem 0;
|
||||
color: #64748b;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.875rem;
|
||||
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 {
|
||||
font-weight: 500;
|
||||
color: #475569;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.theme-author {
|
||||
color: #6b7280;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.theme-version {
|
||||
color: #9ca3af;
|
||||
color: var(--text-tertiary);
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
@ -121,31 +51,16 @@
|
||||
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 {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: #6b7280;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: #374151;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
|
||||
@ -1,67 +1,96 @@
|
||||
/* Error State Component - Fully refactored to use semantic classes */
|
||||
|
||||
/* Use semantic .error-content class from application.css */
|
||||
.error-content {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
min-height: 200px;
|
||||
/* Base styling already defined in application.css */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 40vh;
|
||||
text-align: center;
|
||||
gap: 1rem;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.error-content h2 {
|
||||
color: #dc2626;
|
||||
color: var(--text-error);
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.error-content p {
|
||||
color: #6b7280;
|
||||
margin-bottom: 2rem;
|
||||
max-width: 400px;
|
||||
line-height: 1.5;
|
||||
color: var(--text-secondary);
|
||||
margin: 0 0 2rem 0;
|
||||
max-width: 500px;
|
||||
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 {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
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;
|
||||
}
|
||||
/* Responsive design for mobile */
|
||||
@media (max-width: 768px) {
|
||||
.error-content {
|
||||
padding: 1rem;
|
||||
min-height: 30vh;
|
||||
}
|
||||
|
||||
.button.secondary {
|
||||
background: #f9fafb;
|
||||
color: #374151;
|
||||
border: 1px solid #d1d5db;
|
||||
}
|
||||
.error-content h2 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.button.secondary:hover {
|
||||
background: #f3f4f6;
|
||||
border-color: #9ca3af;
|
||||
}
|
||||
.error-content p {
|
||||
font-size: 0.875rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
color: #3b82f6;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
.error-actions {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
color: #2563eb;
|
||||
text-decoration: underline;
|
||||
.error-actions .btn {
|
||||
width: 100%;
|
||||
max-width: 280px;
|
||||
}
|
||||
}
|
||||
@ -1,31 +1,39 @@
|
||||
/* Loading State Component - Refactored to use semantic classes */
|
||||
|
||||
/* Use semantic .loading-content class from application.css */
|
||||
.loading-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
/* Already defined in application.css with proper semantic styling */
|
||||
/* Override min-height if needed for this specific component */
|
||||
min-height: 200px;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
/* Use semantic .loading-spinner class from application.css */
|
||||
.loading-spinner {
|
||||
color: #6b7280;
|
||||
/* Base styling already defined in application.css */
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* Spinner icon using semantic animation and CSS variables */
|
||||
.loading-spinner::after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-left: 10px;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-top: 2px solid #3b82f6;
|
||||
border: 2px solid var(--border-secondary);
|
||||
border-top: 2px solid var(--color-blue-500);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Use semantic animation from application.css */
|
||||
@keyframes spin {
|
||||
/* Animation already defined in application.css */
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
@ -4,15 +4,32 @@ import './LoadingState.css';
|
||||
interface LoadingStateProps {
|
||||
message?: string;
|
||||
className?: string;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
centered?: boolean;
|
||||
}
|
||||
|
||||
export const LoadingState: React.FC<LoadingStateProps> = ({
|
||||
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 (
|
||||
<div className={`loading-content ${className}`.trim()}>
|
||||
<div className="loading-spinner">{message}</div>
|
||||
<div className={containerClasses}>
|
||||
<div className={spinnerClasses}>{message}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,117 +1,56 @@
|
||||
/* Slide Thumbnail Component */
|
||||
/* Slide Thumbnail Component - Minimal overrides using semantic classes */
|
||||
|
||||
/* Component-specific layout for slide thumbnails */
|
||||
.slide-thumbnail {
|
||||
/* Override card-interactive to use flex column layout */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 2px solid var(--border-primary);
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
background: var(--bg-secondary);
|
||||
min-height: 160px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Interactive States - shared box-shadow */
|
||||
.slide-thumbnail:hover,
|
||||
.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 {
|
||||
/* Positioning for the badge number */
|
||||
.slide-thumbnail .badge-number {
|
||||
position: absolute;
|
||||
top: 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;
|
||||
}
|
||||
|
||||
.slide-thumbnail.active .thumbnail-number {
|
||||
background: var(--color-blue-500);
|
||||
}
|
||||
|
||||
/* Preview Area */
|
||||
.thumbnail-preview {
|
||||
/* Preview area specific styling */
|
||||
.slide-thumbnail .preview-container {
|
||||
flex: 1;
|
||||
padding: 2rem 1rem 1rem;
|
||||
min-height: 80px;
|
||||
background: var(--bg-muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.thumbnail-content {
|
||||
text-align: center;
|
||||
/* Component-specific drag handle positioning */
|
||||
.slide-thumbnail .drag-handle {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0.5rem;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
/* Content Labels - shared text styling */
|
||||
.thumbnail-content span {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
/* Show drag handle on hover */
|
||||
.slide-thumbnail:hover .drag-handle {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.layout-name {
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 0.25rem;
|
||||
/* Hide drag handle when not draggable */
|
||||
.slide-thumbnail:not(.draggable) .drag-handle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.content-count {
|
||||
font-size: 0.625rem; /* Override smaller size */
|
||||
color: var(--text-tertiary);
|
||||
/* Custom drop indicator line for slide thumbnails */
|
||||
.slide-thumbnail.drag-over::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: var(--color-blue-500);
|
||||
border-radius: 1px;
|
||||
z-index: 15;
|
||||
}
|
||||
|
||||
/* 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);
|
||||
}
|
||||
@ -7,11 +7,18 @@ interface SlideThumbnailProps {
|
||||
index: number;
|
||||
isActive: boolean;
|
||||
isDisabled?: boolean;
|
||||
isDragged?: boolean;
|
||||
isDraggedOver?: boolean;
|
||||
onClick: () => void;
|
||||
onDoubleClick?: () => void;
|
||||
onEdit: () => void;
|
||||
onDuplicate: () => void;
|
||||
onDelete: () => void;
|
||||
onDragStart?: (e: React.DragEvent) => void;
|
||||
onDragEnd?: (e: React.DragEvent) => void;
|
||||
onDragOver?: (e: React.DragEvent) => void;
|
||||
onDragLeave?: (e: React.DragEvent) => void;
|
||||
onDrop?: (e: React.DragEvent) => void;
|
||||
}
|
||||
|
||||
export const SlideThumbnail: React.FC<SlideThumbnailProps> = ({
|
||||
@ -19,31 +26,76 @@ export const SlideThumbnail: React.FC<SlideThumbnailProps> = ({
|
||||
index,
|
||||
isActive,
|
||||
isDisabled = false,
|
||||
isDragged = false,
|
||||
isDraggedOver = false,
|
||||
onClick,
|
||||
onDoubleClick,
|
||||
onEdit,
|
||||
onDuplicate,
|
||||
onDelete
|
||||
onDelete,
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
onDragOver,
|
||||
onDragLeave,
|
||||
onDrop
|
||||
}) => {
|
||||
const handleClick = () => {
|
||||
// Don't trigger click during drag operations
|
||||
if (isDragged) return;
|
||||
onClick();
|
||||
};
|
||||
|
||||
const handleDoubleClick = () => {
|
||||
// Don't trigger double-click during drag operations
|
||||
if (isDragged || !onDoubleClick) return;
|
||||
onDoubleClick();
|
||||
};
|
||||
|
||||
// Build semantic class names
|
||||
const containerClasses = [
|
||||
'card-interactive',
|
||||
'slide-thumbnail', // Keep for component-specific overrides
|
||||
isActive && 'selected',
|
||||
isDragged && 'dragged',
|
||||
isDraggedOver && 'drag-over',
|
||||
!isDisabled && 'draggable'
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
const badgeClasses = [
|
||||
'badge-number',
|
||||
isActive && 'active'
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`slide-thumbnail ${isActive ? 'active' : ''}`}
|
||||
onClick={onClick}
|
||||
onDoubleClick={onDoubleClick}
|
||||
className={containerClasses}
|
||||
draggable={!isDisabled}
|
||||
onClick={handleClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
onDragStart={onDragStart}
|
||||
onDragEnd={onDragEnd}
|
||||
onDragOver={onDragOver}
|
||||
onDragLeave={onDragLeave}
|
||||
onDrop={onDrop}
|
||||
title={`Slide ${index + 1}${isDragged ? ' (dragging...)' : ''}`}
|
||||
>
|
||||
<div className="thumbnail-number">{index + 1}</div>
|
||||
<div className="thumbnail-preview">
|
||||
<div className="thumbnail-content">
|
||||
<span className="layout-name">{slide.layoutId}</span>
|
||||
<span className="content-count">
|
||||
{Object.keys(slide.content).length} items
|
||||
</span>
|
||||
<div className={badgeClasses}>{index + 1}</div>
|
||||
|
||||
<div className="preview-container">
|
||||
<div className="preview-content">
|
||||
<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
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="thumbnail-actions">
|
||||
|
||||
<div className="action-bar">
|
||||
<button
|
||||
type="button"
|
||||
className="thumbnail-action edit"
|
||||
className="action-btn edit"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit();
|
||||
@ -55,7 +107,7 @@ export const SlideThumbnail: React.FC<SlideThumbnailProps> = ({
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="thumbnail-action"
|
||||
className="action-btn"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDuplicate();
|
||||
@ -67,7 +119,7 @@ export const SlideThumbnail: React.FC<SlideThumbnailProps> = ({
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="thumbnail-action delete"
|
||||
className="action-btn delete"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete();
|
||||
@ -78,6 +130,12 @@ export const SlideThumbnail: React.FC<SlideThumbnailProps> = ({
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{!isDisabled && (
|
||||
<div className="drag-handle" title="Drag to reorder">
|
||||
⋮⋮
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,62 +1,67 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
interface ErrorStateProps {
|
||||
error: 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 (
|
||||
<div className="slide-editor">
|
||||
<header className="slide-editor-header">
|
||||
<div className="editor-info">
|
||||
{presentationId ? (
|
||||
<Link to={`/presentations/${presentationId}/edit/slides/1`} className="back-button">
|
||||
← Back to Presentation
|
||||
</Link>
|
||||
) : (
|
||||
<Link to="/presentations" className="back-button">
|
||||
← Back to Presentations
|
||||
</Link>
|
||||
)}
|
||||
<div className="editor-title">
|
||||
<h1>Slide Editor Error</h1>
|
||||
</div>
|
||||
<div className="page-container">
|
||||
<header className="page-header">
|
||||
<a href={href} className="back-link">
|
||||
{text}
|
||||
</a>
|
||||
<div className="flex-1">
|
||||
<h1 className="text-xl font-semibold text-primary m-0">{title}</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="slide-editor-content">
|
||||
<div className="error-container">
|
||||
<div className="error-content">
|
||||
<h2>Unable to Load Slide Editor</h2>
|
||||
<p>{error}</p>
|
||||
|
||||
<div className="error-actions">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => window.location.reload()}
|
||||
className="action-button primary"
|
||||
>
|
||||
Reload Page
|
||||
</button>
|
||||
|
||||
{presentationId ? (
|
||||
<Link
|
||||
to={`/presentations/${presentationId}/edit/slides/1`}
|
||||
className="action-button secondary"
|
||||
>
|
||||
Return to Presentation
|
||||
</Link>
|
||||
) : (
|
||||
<Link
|
||||
to="/presentations"
|
||||
className="action-button secondary"
|
||||
>
|
||||
Back to Presentations
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<main className="page-content">
|
||||
<div className="error-content">
|
||||
<h2>Unable to Load Content</h2>
|
||||
<p>{error}</p>
|
||||
|
||||
<div className="flex gap-4 justify-center flex-wrap">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => window.location.reload()}
|
||||
className="btn btn-primary"
|
||||
>
|
||||
Reload Page
|
||||
</button>
|
||||
|
||||
<a
|
||||
href={href}
|
||||
className="btn btn-secondary"
|
||||
>
|
||||
{presentationId ? "Return to Presentation" : "Back to Presentations"}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@ -136,9 +136,48 @@ export const useSlideOperations = ({
|
||||
}
|
||||
};
|
||||
|
||||
const reorderSlides = async (fromIndex: number, toIndex: number) => {
|
||||
if (!presentation || fromIndex === toIndex) return;
|
||||
|
||||
try {
|
||||
setSaving(true);
|
||||
onError('');
|
||||
|
||||
// Create updated presentation with reordered slides
|
||||
const updatedPresentation = { ...presentation };
|
||||
const newSlides = [...presentation.slides];
|
||||
|
||||
// Remove the slide from its current position
|
||||
const [movedSlide] = newSlides.splice(fromIndex, 1);
|
||||
|
||||
// Insert it at the new position
|
||||
newSlides.splice(toIndex, 0, movedSlide);
|
||||
|
||||
// Update slide order for all slides
|
||||
newSlides.forEach((slide, index) => {
|
||||
slide.order = index;
|
||||
});
|
||||
|
||||
updatedPresentation.slides = newSlides;
|
||||
|
||||
// Save the updated presentation
|
||||
await updatePresentation(updatedPresentation);
|
||||
|
||||
// Update local state
|
||||
onPresentationUpdate(updatedPresentation);
|
||||
|
||||
} catch (err) {
|
||||
loggers.presentation.error('Failed to reorder slides', err instanceof Error ? err : new Error(String(err)));
|
||||
onError(err instanceof Error ? err.message : 'Failed to reorder slides');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
duplicateSlide,
|
||||
deleteSlide,
|
||||
reorderSlides,
|
||||
saving
|
||||
};
|
||||
};
|
||||
@ -1,6 +1,8 @@
|
||||
/* Application CSS - Consolidated Styles */
|
||||
/* Import color system first */
|
||||
@import './colors.css';
|
||||
/* Import semantic component classes */
|
||||
@import './semantic-components.css';
|
||||
|
||||
/* =================================================================
|
||||
BASE APP LAYOUT
|
||||
@ -85,6 +87,15 @@
|
||||
.fixed { position: fixed; }
|
||||
.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
|
||||
================================================================= */
|
||||
@ -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-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
|
||||
================================================================= */
|
||||
@ -778,4 +780,34 @@
|
||||
.page-title-section {
|
||||
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; }
|
||||
|
||||
384
src/styles/semantic-components.css
Normal file
384
src/styles/semantic-components.css
Normal 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;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user