diff --git a/public/themes-manifest.json b/public/themes-manifest.json
index 7471d4e..e9dd450 100644
--- a/public/themes-manifest.json
+++ b/public/themes-manifest.json
@@ -7,6 +7,7 @@
"2-content-blocks",
"code-slide",
"content-slide",
+ "diagram-slide",
"image-slide",
"markdown-slide",
"title-slide"
@@ -14,5 +15,5 @@
"hasMasterSlide": true
}
},
- "generated": "2025-08-22T02:19:52.970Z"
+ "generated": "2025-08-22T02:33:14.669Z"
}
\ No newline at end of file
diff --git a/public/themes/default/layouts/diagram-slide.html b/public/themes/default/layouts/diagram-slide.html
new file mode 100644
index 0000000..eb1cd8e
--- /dev/null
+++ b/public/themes/default/layouts/diagram-slide.html
@@ -0,0 +1,23 @@
+
+
+ {{title}}
+
+
+
{{diagram}}
+
+
+ {{notes}}
+
+
\ No newline at end of file
diff --git a/public/themes/default/style.css b/public/themes/default/style.css
index 48a3619..219afc9 100644
--- a/public/themes/default/style.css
+++ b/public/themes/default/style.css
@@ -720,6 +720,143 @@ pre.code-content::before {
font-style: italic;
}
+/* Diagram editor textarea styling */
+.field-textarea.diagram-field {
+ font-family: var(--theme-font-code);
+ font-size: 0.9rem;
+ line-height: 1.4;
+ background: #0f1419;
+ color: #e6e6e6;
+ border: 1px solid var(--theme-accent);
+ border-radius: 4px;
+ padding: 0.75rem;
+ white-space: pre;
+ tab-size: 2;
+}
+
+.field-textarea.diagram-field::placeholder {
+ color: #6a6a6a;
+ font-style: italic;
+}
+
+/* Diagram slide layout */
+.layout-diagram-slide,
+.slide-container .layout-diagram-slide {
+ justify-content: flex-start;
+ align-items: stretch;
+ text-align: center;
+ padding: 2rem;
+}
+
+.layout-diagram-slide .slot[data-slot="title"] {
+ font-size: clamp(1.5rem, 4vw, 2rem);
+ margin-bottom: 1.5rem;
+ text-align: center;
+ color: var(--theme-text);
+}
+
+.layout-diagram-slide .slot[data-slot="diagram"] {
+ flex: 1;
+ margin-bottom: 1rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.layout-diagram-slide .slot[data-slot="notes"] {
+ font-size: clamp(0.9rem, 2vw, 1rem);
+ color: var(--theme-text-secondary);
+ line-height: 1.5;
+ text-align: center;
+ max-height: 20vh;
+ overflow-y: auto;
+}
+
+/* Enhanced Mermaid diagram styling */
+.mermaid-container {
+ margin: 1rem 0;
+ padding: 1.5rem;
+ background: rgba(255, 255, 255, 0.02);
+ border-radius: 12px;
+ border: 1px solid var(--theme-secondary);
+ text-align: center;
+ overflow: hidden;
+ width: 100%;
+ max-width: 100%;
+}
+
+.mermaid-diagram {
+ max-width: 100%;
+ height: auto;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.mermaid-diagram svg {
+ max-width: 100%;
+ height: auto;
+ background: transparent;
+ border-radius: 8px;
+}
+
+.diagram-error {
+ color: #ff6b6b;
+ background: rgba(255, 107, 107, 0.1);
+ padding: 1.5rem;
+ border-radius: 8px;
+ border: 1px solid #ff6b6b;
+ font-family: var(--theme-font-code);
+ font-size: 0.9rem;
+ text-align: left;
+}
+
+.diagram-error strong {
+ display: block;
+ margin-bottom: 0.5rem;
+ color: #ff8a8a;
+}
+
+.error-content {
+ background: rgba(0, 0, 0, 0.2);
+ padding: 0.75rem;
+ border-radius: 4px;
+ margin-top: 0.75rem;
+ white-space: pre-wrap;
+ font-size: 0.8rem;
+ overflow-x: auto;
+}
+
+/* Preview loading states */
+.preview-loading,
+.slide-loading {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 1rem;
+ color: var(--theme-text-secondary);
+ font-size: 0.9rem;
+ z-index: 10;
+}
+
+.loading-spinner {
+ width: 2rem;
+ height: 2rem;
+ border: 2px solid var(--theme-secondary);
+ border-top: 2px solid var(--theme-accent);
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
/* Responsive adjustments for enhanced features */
@media (max-width: 768px) {
.layout-code-slide {
diff --git a/src/components/presentations/PresentationMode.tsx b/src/components/presentations/PresentationMode.tsx
index fcf7d7a..ca74f0e 100644
--- a/src/components/presentations/PresentationMode.tsx
+++ b/src/components/presentations/PresentationMode.tsx
@@ -5,6 +5,7 @@ import { renderTemplateWithSampleData } from '../../utils/templateRenderer.ts';
import { sanitizeSlideTemplate } from '../../utils/htmlSanitizer.ts';
import { renderSlideMarkdown, isMarkdownContent } from '../../utils/markdownProcessor.ts';
import { highlightCode } from '../../utils/codeHighlighter.ts';
+import { renderDiagram } from '../../utils/diagramProcessor.ts';
import { loggers } from '../../utils/logger.ts';
import './PresentationMode.css';
import {usePresentationLoader} from "./hooks/usePresentationLoader.ts";
@@ -21,6 +22,8 @@ export const PresentationMode: React.FC = () => {
const [currentSlideIndex, setCurrentSlideIndex] = useState(
slideNumber ? parseInt(slideNumber, 10) - 1 : 0
);
+ const [renderedSlideContent, setRenderedSlideContent] = useState('');
+ const [isRenderingSlide, setIsRenderingSlide] = useState(false);
// Load presentation and theme
const { presentation, theme, loading, error } = usePresentationLoader(presentationId);
@@ -52,7 +55,31 @@ export const PresentationMode: React.FC = () => {
}
}, [slideNumber]);
- const renderSlideContent = (slide: SlideContent): string => {
+ // Render current slide content when slide changes
+ useEffect(() => {
+ const renderCurrentSlide = async () => {
+ if (!presentation || !theme || currentSlideIndex < 0 || currentSlideIndex >= presentation.slides.length) {
+ setRenderedSlideContent('');
+ return;
+ }
+
+ setIsRenderingSlide(true);
+ try {
+ const slide = presentation.slides[currentSlideIndex];
+ const rendered = await renderSlideContent(slide);
+ setRenderedSlideContent(rendered);
+ } catch (error) {
+ console.error('Failed to render slide:', error);
+ setRenderedSlideContent('Failed to render slide
');
+ } finally {
+ setIsRenderingSlide(false);
+ }
+ };
+
+ renderCurrentSlide();
+ }, [presentation, theme, currentSlideIndex]);
+
+ const renderSlideContent = async (slide: SlideContent): Promise => {
if (!theme) return '';
const layout = theme.layouts.find(l => l.id === slide.layoutId);
@@ -64,7 +91,7 @@ export const PresentationMode: React.FC = () => {
let renderedTemplate = layout.htmlTemplate;
// Replace template variables with slide content
- Object.entries(slide.content).forEach(([slotId, content]) => {
+ for (const [slotId, content] of Object.entries(slide.content)) {
const regex = new RegExp(`\\{\\{${slotId}\\}\\}`, 'g');
// Find the corresponding slot to determine processing type
@@ -77,13 +104,16 @@ export const PresentationMode: React.FC = () => {
// Handle code highlighting
const language = slot.attributes?.['data-language'] || 'javascript';
processedContent = highlightCode(content, language);
+ } else if (slot?.type === 'diagram') {
+ // Handle diagram rendering
+ processedContent = await renderDiagram(content);
} else if (slot?.type === 'markdown' || (slot?.type === 'text' && isMarkdownContent(content))) {
// Handle markdown processing
processedContent = renderSlideMarkdown(content, slot?.type);
}
renderedTemplate = renderedTemplate.replace(regex, processedContent);
- });
+ }
// Handle conditional blocks and clean up remaining variables
renderedTemplate = renderTemplateWithSampleData(renderedTemplate, layout);
@@ -140,16 +170,21 @@ export const PresentationMode: React.FC = () => {
);
}
- const currentSlide = presentation.slides[currentSlideIndex];
const totalSlides = presentation.slides.length;
- const renderedSlideContent = renderSlideContent(currentSlide);
return (
+ {isRenderingSlide && (
+
+ )}
diff --git a/src/components/slide-editor/ContentEditor.tsx b/src/components/slide-editor/ContentEditor.tsx
index 7ac6eb6..99f8acc 100644
--- a/src/components/slide-editor/ContentEditor.tsx
+++ b/src/components/slide-editor/ContentEditor.tsx
@@ -1,11 +1,10 @@
import React from 'react';
import type { Presentation } from '../../types/presentation.ts';
import type { SlideLayout } from '../../types/theme.ts';
-import { renderTemplateWithContent } from './utils.ts';
-import { sanitizeSlideTemplate } from '../../utils/htmlSanitizer.ts';
import { ImageUploadField } from '../ui/ImageUploadField.tsx';
import { CancelLink } from '../ui/buttons/CancelLink.tsx';
import { ActionButton } from '../ui/buttons/ActionButton.tsx';
+import { SlidePreview } from './SlidePreview.tsx';
interface ContentEditorProps {
presentation: Presentation;
@@ -45,11 +44,14 @@ export const ContentEditor: React.FC
= ({
);
}
- if (slot.type === 'code' || slot.type === 'markdown' || (slot.type === 'text' && slot.id.includes('content'))) {
+ if (slot.type === 'code' || slot.type === 'diagram' || slot.type === 'markdown' || (slot.type === 'text' && slot.id.includes('content'))) {
const getPlaceholder = () => {
if (slot.type === 'code') {
return slot.placeholder || 'Enter your JavaScript code here...';
}
+ if (slot.type === 'diagram') {
+ return slot.placeholder || 'Enter Mermaid diagram syntax here...';
+ }
if (slot.type === 'markdown') {
return slot.placeholder || 'Enter markdown content (e.g., **bold**, *italic*, - list items)';
}
@@ -58,6 +60,7 @@ export const ContentEditor: React.FC = ({
const getRows = () => {
if (slot.type === 'code') return 8;
+ if (slot.type === 'diagram') return 10;
if (slot.type === 'markdown') return 6;
return 4;
};
@@ -68,7 +71,11 @@ export const ContentEditor: React.FC = ({
value={slideContent[slot.id] || ''}
onChange={(e) => onSlotContentChange(slot.id, e.target.value)}
placeholder={getPlaceholder()}
- className={`field-textarea ${slot.type === 'code' ? 'code-field' : slot.type === 'markdown' ? 'markdown-field' : ''}`}
+ className={`field-textarea ${
+ slot.type === 'code' ? 'code-field' :
+ slot.type === 'diagram' ? 'diagram-field' :
+ slot.type === 'markdown' ? 'markdown-field' : ''
+ }`}
rows={getRows()}
style={slot.type === 'code' ? { fontFamily: 'var(--theme-font-code)' } : undefined}
/>
@@ -149,31 +156,11 @@ export const ContentEditor: React.FC = ({
-
-
Live Preview
-
- Updates automatically as you type
-
-
-
-
- {selectedLayout.name}
-
- {presentation.metadata.aspectRatio || '16:9'} aspect ratio
-
-
- {Object.values(slideContent).filter(v => v.trim()).length} / {selectedLayout.slots.length} slots filled
-
-
-
-
+
);
diff --git a/src/components/slide-editor/SlidePreview.tsx b/src/components/slide-editor/SlidePreview.tsx
new file mode 100644
index 0000000..0081e4c
--- /dev/null
+++ b/src/components/slide-editor/SlidePreview.tsx
@@ -0,0 +1,71 @@
+import React, { useState, useEffect } from 'react';
+import type { Presentation } from '../../types/presentation.ts';
+import type { SlideLayout } from '../../types/theme.ts';
+import { renderTemplateWithContent } from './utils.ts';
+import { sanitizeSlideTemplate } from '../../utils/htmlSanitizer.ts';
+
+interface SlidePreviewProps {
+ presentation: Presentation;
+ selectedLayout: SlideLayout;
+ slideContent: Record;
+}
+
+export const SlidePreview: React.FC = ({
+ presentation,
+ selectedLayout,
+ slideContent,
+}) => {
+ const [previewHtml, setPreviewHtml] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+
+ useEffect(() => {
+ const updatePreview = async () => {
+ setIsLoading(true);
+ try {
+ const rendered = await renderTemplateWithContent(selectedLayout, slideContent);
+ const sanitized = sanitizeSlideTemplate(rendered);
+ setPreviewHtml(sanitized);
+ } catch (error) {
+ console.error('Preview rendering failed:', error);
+ setPreviewHtml('Preview unavailable
');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ updatePreview();
+ }, [selectedLayout, slideContent]);
+
+ return (
+
+
Live Preview
+
+ Updates automatically as you type
+
+
+
+ {isLoading && (
+
+
+
Rendering preview...
+
+ )}
+
+
+
+ {selectedLayout.name}
+
+ {presentation.metadata.aspectRatio || '16:9'} aspect ratio
+
+
+ {Object.values(slideContent).filter(v => v.trim()).length} / {selectedLayout.slots.length} slots filled
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/components/slide-editor/utils.ts b/src/components/slide-editor/utils.ts
index cccc324..632b1b5 100644
--- a/src/components/slide-editor/utils.ts
+++ b/src/components/slide-editor/utils.ts
@@ -1,13 +1,14 @@
import type { SlideLayout } from '../../types/theme.ts';
import { renderSlideMarkdown, isMarkdownContent } from '../../utils/markdownProcessor.ts';
import { highlightCode } from '../../utils/codeHighlighter.ts';
+import { renderDiagram } from '../../utils/diagramProcessor.ts';
// Helper function to render template with actual content
-export const renderTemplateWithContent = (layout: SlideLayout, content: Record): string => {
+export const renderTemplateWithContent = async (layout: SlideLayout, content: Record): Promise => {
let rendered = layout.htmlTemplate;
// Replace content placeholders
- Object.entries(content).forEach(([slotId, value]) => {
+ for (const [slotId, value] of Object.entries(content)) {
const placeholder = new RegExp(`\\{\\{${slotId}\\}\\}`, 'g');
// Find the corresponding slot to determine processing type
@@ -20,13 +21,16 @@ export const renderTemplateWithContent = (layout: SlideLayout, content: Record => {
+ if (!diagramText || typeof diagramText !== 'string') {
+ return 'No diagram content provided
';
+ }
+
+ try {
+ // Clean and validate the diagram text
+ const cleanedText = diagramText.trim();
+
+ if (!cleanedText) {
+ return 'Empty diagram content
';
+ }
+
+ // Generate unique ID for this diagram
+ const diagramId = `mermaid-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+
+ console.log('Rendering Mermaid diagram:', cleanedText);
+
+ // Render the diagram directly without parse validation (parse might be causing issues)
+ const { svg } = await mermaid.render(diagramId, cleanedText);
+
+ console.log('Mermaid SVG generated:', svg.substring(0, 100) + '...');
+
+ // Wrap in container with proper styling
+ const result = ``;
+
+ console.log('Final HTML result:', result.substring(0, 200) + '...');
+
+ return result;
+
+ } catch (error) {
+ console.error('Mermaid diagram rendering failed:', error);
+
+ // Return error message with the original text for debugging
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
+ return `
+
Diagram Error: ${errorMessage}
+
${diagramText.replace(/[<>&"']/g, (char) => {
+ const escapeMap: Record = {
+ '<': '<',
+ '>': '>',
+ '&': '&',
+ '"': '"',
+ "'": '''
+ };
+ return escapeMap[char];
+ })}
+
`;
+ }
+};
+
+/**
+ * Checks if content appears to be Mermaid diagram syntax
+ * Used to determine whether to process content as a diagram
+ */
+export const isDiagramContent = (content: string): boolean => {
+ if (!content || typeof content !== 'string') {
+ return false;
+ }
+
+ const trimmed = content.trim();
+
+ // Common Mermaid diagram types
+ const mermaidPatterns = [
+ /^graph\s+(TD|TB|BT|RL|LR)/i, // Flowchart
+ /^flowchart\s+(TD|TB|BT|RL|LR)/i, // Flowchart (newer syntax)
+ /^sequenceDiagram/i, // Sequence diagram
+ /^classDiagram/i, // Class diagram
+ /^stateDiagram/i, // State diagram
+ /^erDiagram/i, // Entity relationship diagram
+ /^gantt/i, // Gantt chart
+ /^pie/i, // Pie chart
+ /^journey/i, // User journey
+ /^gitgraph/i, // Git graph
+ /^mindmap/i, // Mind map
+ /^timeline/i, // Timeline
+ ];
+
+ return mermaidPatterns.some(pattern => pattern.test(trimmed));
+};
+
+/**
+ * Sample diagram content for testing and previews
+ */
+export const SAMPLE_DIAGRAM_CONTENT = {
+ flowchart: `graph TD
+ A[Start] --> B{Is it working?}
+ B -->|Yes| C[Great!]
+ B -->|No| D[Debug]
+ D --> B
+ C --> E[End]`,
+
+ sequence: `sequenceDiagram
+ participant A as Alice
+ participant B as Bob
+ participant C as Charlie
+ A->>B: Hello Bob!
+ B->>C: Hello Charlie!
+ C->>A: Hello Alice!`,
+
+ pie: `pie title Project Status
+ "Completed" : 65
+ "In Progress" : 25
+ "Planning" : 10`,
+
+ gantt: `gantt
+ title Project Timeline
+ dateFormat YYYY-MM-DD
+ section Planning
+ Research :done, des1, 2024-01-01, 2024-01-15
+ Design :done, des2, 2024-01-10, 2024-01-25
+ section Development
+ Frontend :active, dev1, 2024-01-20, 2024-02-15
+ Backend :dev2, 2024-02-01, 2024-02-28
+ Testing :test1, 2024-02-20, 2024-03-10`
+};
\ No newline at end of file
diff --git a/src/utils/htmlSanitizer.ts b/src/utils/htmlSanitizer.ts
index a837c19..a92c74e 100644
--- a/src/utils/htmlSanitizer.ts
+++ b/src/utils/htmlSanitizer.ts
@@ -49,10 +49,18 @@ const DEFAULT_SLIDE_CONFIG: Required = {
// Quotes
'blockquote', 'cite',
// Code elements
- 'pre', 'code'
+ 'pre', 'code',
+ // SVG elements for diagrams
+ 'svg', 'g', 'path', 'rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon',
+ 'text', 'tspan', 'defs', 'marker', 'pattern', 'foreignObject'
],
allowedAttributes: [
- 'class', 'id', 'style', 'data-*'
+ 'class', 'id', 'style', 'data-*',
+ // SVG attributes
+ 'xmlns', 'viewBox', 'width', 'height', 'd', 'x', 'y', 'x1', 'y1', 'x2', 'y2',
+ 'cx', 'cy', 'r', 'rx', 'ry', 'fill', 'stroke', 'stroke-width', 'stroke-dasharray',
+ 'transform', 'points', 'font-family', 'font-size', 'text-anchor', 'dominant-baseline',
+ 'markerWidth', 'markerHeight', 'orient', 'refX', 'refY'
]
};