Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
e6184ce
feat: Improve slide deck mobile responsiveness and fullscreen behavior
osterman Dec 31, 2025
902489e
feat: Add responsive scaling for desktop fullscreen mode
osterman Dec 31, 2025
daadd15
chore: Increase bomb image width from 180px to 280px
osterman Dec 31, 2025
c96eb4b
fix: Allow vertical scrolling in fullscreen slides for long content
osterman Dec 31, 2025
7297817
fix: Make mobile fullscreen fill entire viewport without black borders
osterman Dec 31, 2025
1bdc5d6
fix: Remove dark borders on mobile fullscreen by making containers tr…
osterman Dec 31, 2025
2650651
fix: Restore solid background and visible controls on mobile fullscreen
osterman Dec 31, 2025
64eab82
fix: Hide left-area container in mobile fullscreen mode
osterman Dec 31, 2025
337b03e
fix: Improve vertical centering in mobile fullscreen mode
osterman Dec 31, 2025
89c1328
fix: Keep prev navigation button visible in mobile fullscreen
osterman Dec 31, 2025
cec76d8
fix: Ensure vertical centering for content layout slides on mobile
osterman Dec 31, 2025
21e1809
fix: Enable vertical scrolling on mobile fullscreen slides
osterman Dec 31, 2025
4b59ca8
fix: Use absolute positioning for mobile slide to enable scrolling
osterman Dec 31, 2025
68b9371
fix: Use 'justify-content: safe center' for vertical centering with s…
osterman Dec 31, 2025
a60afd3
fix: Use margin:auto on slide__inner for vertical centering
osterman Dec 31, 2025
333904c
fix: Remove top padding from mobile fullscreen slide
osterman Dec 31, 2025
e888c9a
fix: Remove all top padding from mobile fullscreen slides
osterman Dec 31, 2025
26fea4b
fix: Add horizontal padding to slide__inner on mobile fullscreen
osterman Dec 31, 2025
5a0dbd4
feat: Add customizable speaker notes with position, display mode, and…
osterman Dec 31, 2025
2286456
refactor: Move notes preference controls to SlideNotesPanel header
osterman Dec 31, 2025
33fc4a6
fix: Add horizontal padding to bottom position speaker notes
osterman Dec 31, 2025
0959954
fix: Extend progress bar to full width in page mode
osterman Dec 31, 2025
a73a96f
fix: Address CodeRabbit review comments for SlideDeck
osterman Dec 31, 2025
c4afb3b
fix: Improve popout window slide state synchronization
osterman Dec 31, 2025
6f0f53e
feat: Add slide-notes-extractor plugin for TTS export
osterman Dec 31, 2025
2579316
feat: Add TTS player for speaker notes
osterman Dec 31, 2025
b520413
fix: Address security and accessibility review comments
osterman Dec 31, 2025
6d51c9b
[autofix.ci] apply automated fixes
autofix-ci[bot] Dec 31, 2025
a8fcfcc
[autofix.ci] apply automated fixes (attempt 2/3)
autofix-ci[bot] Dec 31, 2025
5ff39f1
Merge branch 'main' into osterman/fullscreen-white-corners
osterman Dec 31, 2025
be42744
fix: Auto-play TTS continues after slide advance
osterman Dec 31, 2025
0573a2f
fix: Use text extraction approach for HTML sanitization
osterman Dec 31, 2025
675eae0
feat: Add 2-second delay between slides during TTS auto-play
osterman Dec 31, 2025
8c3e722
refactor: Split TTS auto-advance delay into 1s after + 1s before
osterman Dec 31, 2025
0e92759
refactor: Rename TTS delay constants for clarity
osterman Dec 31, 2025
659ca5b
fix: Keep TTS player bar visible during slide transitions
osterman Dec 31, 2025
66673dc
fix: Show loading spinner during TTS slide transitions
osterman Dec 31, 2025
79a9854
fix: Reuse Audio element for iOS autoplay compatibility
osterman Jan 1, 2026
6ad42f6
feat: Prefetch TTS audio in parallel with delay
osterman Jan 1, 2026
e423412
feat: Prefetch next slide audio while current slide plays
osterman Jan 1, 2026
b991744
fix: Handle unhandled promise rejection in TTS resume
osterman Jan 1, 2026
a178c24
fix: Improve mobile portrait fullscreen layout for slides
osterman Jan 1, 2026
3bd41cf
fix: Center slide content vertically in mobile portrait mode
osterman Jan 1, 2026
9590f18
Merge branch 'main' into osterman/fullscreen-white-corners
aknysh Jan 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion website/docs/slides/atmos-intro.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ import { MetallicIcon } from '@site/src/components/MetallicIcon';
<li>Terraform lacks built-in guardrails & policy controls that enterprises need</li>
</SlideList>
</SlideContent>
<SlideImage src="/img/slides/atmos-intro/bomb.png" alt="Bomb" width={180} />
<SlideImage src="/img/slides/atmos-intro/bomb.png" alt="Bomb" width={280} />
<SlideNotes>
Let me enumerate the specific challenges with Terraform. GitOps and CI/CD
for teams is non-trivial to set up correctly. Configuration is not DRY -
Expand Down
3 changes: 3 additions & 0 deletions website/docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,9 @@ const config = {
'cli/*',
],
},
],
[
path.resolve(__dirname, 'plugins', 'slide-notes-extractor'), {}
]
],

Expand Down
123 changes: 123 additions & 0 deletions website/plugins/slide-notes-extractor/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
const fs = require('fs');
const path = require('path');

module.exports = function slideNotesExtractorPlugin(context, options) {
return {
name: 'slide-notes-extractor',

async postBuild({ outDir }) {
const slidesDir = path.join(context.siteDir, 'docs/slides');

// Check if slides directory exists.
if (!fs.existsSync(slidesDir)) {
console.log('[slide-notes-extractor] No docs/slides directory found, skipping');
return;
}

// Find all MDX files in docs/slides/.
const mdxFiles = fs.readdirSync(slidesDir)
.filter(f => f.endsWith('.mdx'));

if (mdxFiles.length === 0) {
console.log('[slide-notes-extractor] No MDX files found in docs/slides/');
return;
}

let totalNotes = 0;

for (const file of mdxFiles) {
const content = fs.readFileSync(
path.join(slidesDir, file),
'utf-8'
);

// Extract notes from each slide.
const notes = extractSlideNotes(content);

// Create output directory.
const deckName = path.basename(file, '.mdx');
const outputDir = path.join(outDir, 'slides', deckName);
fs.mkdirSync(outputDir, { recursive: true });

// Write individual .txt files (only for slides with notes).
let notesCount = 0;
notes.forEach((noteText, index) => {
if (noteText.trim()) {
fs.writeFileSync(
path.join(outputDir, `slide${index + 1}.txt`),
noteText.trim()
);
notesCount++;
}
});

totalNotes += notesCount;
console.log(`[slide-notes-extractor] ${deckName}: ${notesCount} notes from ${notes.length} slides`);
}

console.log(`[slide-notes-extractor] Total: ${totalNotes} speaker notes extracted`);
}
};
};

/**
* Extract speaker notes from MDX content.
* Returns an array of note strings, one per slide (empty string if no notes).
*/
function extractSlideNotes(mdxContent) {
const notes = [];

// Regex to match <Slide>...</Slide> blocks (handles attributes and multiline).
const slideRegex = /<Slide[^>]*>([\s\S]*?)<\/Slide>/g;

let match;
while ((match = slideRegex.exec(mdxContent)) !== null) {
const slideContent = match[1];

// Extract <SlideNotes>...</SlideNotes> content.
const notesMatch = slideContent.match(/<SlideNotes>([\s\S]*?)<\/SlideNotes>/);

if (notesMatch) {
// Strip JSX/HTML tags, normalize whitespace for plain text output.
let text = notesMatch[1];

// Remove JSX comments first.
text = text.replace(/\{\/\*[\s\S]*?\*\/\}/g, '');

// Use a simple, secure approach: extract only text content.
// Replace all HTML/JSX tags with spaces (to preserve word boundaries),
// then clean up. This avoids multi-character sanitization issues.
//
// Strategy: Rather than trying to strip tags iteratively (which can
// leave fragments), we extract text between tags and join with spaces.
const textParts = [];
let lastIndex = 0;
const tagRegex = /<[^>]*>/g;
let tagMatch;

while ((tagMatch = tagRegex.exec(text)) !== null) {
// Extract text before this tag.
if (tagMatch.index > lastIndex) {
textParts.push(text.slice(lastIndex, tagMatch.index));
}
lastIndex = tagMatch.index + tagMatch[0].length;
}
// Extract remaining text after last tag.
if (lastIndex < text.length) {
textParts.push(text.slice(lastIndex));
}

// Join parts and clean up any stray angle brackets (from malformed input).
text = textParts.join(' ').replace(/[<>]/g, '');

// Normalize whitespace.
text = text.replace(/\s+/g, ' ').trim();

notes.push(text);
} else {
notes.push(''); // Empty string for slides without notes.
}
}

return notes;
}
153 changes: 147 additions & 6 deletions website/src/components/SlideDeck/Slide.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
box-sizing: border-box;
background: var(--ifm-background-color);
color: var(--ifm-font-color-base);
overflow-y: auto;
}

.slide__inner {
Expand Down Expand Up @@ -87,14 +88,19 @@ html[data-theme='dark'] .slide--title {
max-width: 700px;
}

/* Fullscreen adjustments */
.slide-deck--fullscreen .slide {
background: #1a1a2e;
color: #fff;
/* Fullscreen adjustments - slides inherit normal theme styling */

/* Desktop fullscreen - scale content to fill the larger slide area */
.slide-deck--fullscreen .slide__inner {
max-width: 85%;
}

.slide-deck--fullscreen .slide--split .slide__inner {
max-width: 90%;
}

.slide-deck--fullscreen .slide--title {
background: linear-gradient(135deg, rgba(30, 91, 184, 0.3) 0%, #1a1a2e 100%);
.slide-deck--fullscreen .slide--code .slide__inner {
max-width: 90%;
}

/* Responsive */
Expand Down Expand Up @@ -126,3 +132,138 @@ html[data-theme='dark'] .slide--title {
max-width: 100%;
}
}

/* Small Mobile */
@media screen and (max-width: 480px) {
.slide {
padding: 0.75rem 1rem;
}

.slide--title {
padding: 0.75rem 1rem;
}

.slide--code {
padding: 0.5rem;
}

.slide--split {
padding: 0.75rem 1rem;
}
}

/* Mobile fullscreen - scale content to fit viewport */
@media screen and (max-width: 996px) and (max-height: 500px),
screen and (max-width: 768px) {
.slide-deck--fullscreen .slide {
padding: 0 1.5rem !important;
overflow-y: auto;
}

.slide-deck--fullscreen .slide__inner {
max-width: 95vw;
width: 100%;
margin: auto;
padding: 0 1rem;
}

/* Override content layout for mobile fullscreen */
.slide-deck--fullscreen .slide--content .slide__inner {
text-align: left;
align-items: flex-start;
}

/* Scale text for mobile fullscreen */
.slide-deck--fullscreen .slide-title {
font-size: clamp(1.25rem, 6vw, 2rem);
margin-bottom: 0.75rem;
}

.slide-deck--fullscreen .slide-subtitle {
font-size: clamp(1rem, 4vw, 1.5rem);
}

.slide-deck--fullscreen .slide-content,
.slide-deck--fullscreen .slide-list {
font-size: clamp(0.9rem, 3.5vw, 1.1rem);
}

.slide-deck--fullscreen .slide-list li {
margin-bottom: 0.4em;
}

/* Scale images proportionally */
.slide-deck--fullscreen .slide-image img {
max-height: 40vh;
max-width: 100%;
width: auto;
height: auto;
object-fit: contain;
}

/* For split layouts on mobile, keep 2-column layout */
.slide-deck--fullscreen .slide--split .slide__inner {
flex-direction: row;
gap: 1.5rem;
align-items: center;
}

.slide-deck--fullscreen .slide--split .slide__inner > *:first-child {
flex: 2;
min-width: 0;
}

.slide-deck--fullscreen .slide--split .slide__inner > *:last-child {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
justify-content: center;
}

.slide-deck--fullscreen .slide--split .slide-image img,
.slide-deck--fullscreen .slide--split img {
max-height: 35vh;
max-width: 100%;
width: auto;
height: auto;
object-fit: contain;
}

/* Split layout text sizes */
.slide-deck--fullscreen .slide--split .slide-title {
font-size: clamp(1rem, 5vw, 1.75rem);
margin-bottom: 0.5rem;
}

.slide-deck--fullscreen .slide--split .slide-list {
font-size: clamp(0.75rem, 3vw, 1rem);
}

.slide-deck--fullscreen .slide--split .slide-list li {
margin-bottom: 0.3em;
}
}

/* Mobile Portrait Fullscreen - adjust layout for narrow tall viewports */
@media screen and (max-width: 768px) and (orientation: portrait) {
.slide-deck--fullscreen .slide {
padding: 2rem 1.5rem 5rem !important;
/* Center content vertically */
align-items: center;
justify-content: center;
}

.slide-deck--fullscreen .slide__inner {
max-width: 100%;
width: 100%;
margin: 0 auto;
padding: 0;
}

/* Content slides keep left text alignment */
.slide-deck--fullscreen .slide--content .slide__inner {
text-align: left;
align-items: flex-start;
}
}
Loading
Loading