Files
wehub-resource-sync 8ef9db2513
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Deploy to GitHub Pages / build (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 12:56:49 +08:00

931 lines
38 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="PPT Master Slide Gallery — Browse and view SVG presentation slides across various topics">
<meta name="keywords" content="slides, SVG, presentation, PPT, PowerPoint">
<meta property="og:title" content="Slide Gallery — PPT Master">
<meta property="og:description" content="Browse example presentations — AI-generated, natively editable PowerPoint from any document.">
<meta property="og:url" content="https://hugohe3.github.io/ppt-master/viewer.html">
<meta property="og:type" content="website">
<title>Slide Gallery — PPT Master</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
brand: {
50: '#eef3ff',
100: '#dde7ff',
200: '#c0d2ff',
300: '#9bb6ff',
400: '#7da0ff',
500: '#5b8def',
600: '#4a7be0',
700: '#3b65c2',
800: '#2f4f99',
900: '#243d75',
}
},
fontFamily: {
sans: ['Inter', 'system-ui', '-apple-system', 'PingFang SC', 'Microsoft YaHei', 'sans-serif'],
}
}
}
}
</script>
<!-- Fonts & Icons -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<!-- Favicon -->
<link rel="icon"
href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 32' fill='none' stroke='%235b8def' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'><rect x='2' y='2' width='44' height='28' rx='2.5'/><line x1='9' y1='11' x2='22' y2='11'/><line x1='9' y1='17' x2='32' y2='17'/><line x1='9' y1='23' x2='26' y2='23'/></svg>">
<style>
:root {
--primary: #5b8def;
--primary-light: #88aaff;
--darker: #0d0d0f;
--surface: #16161a;
--surface-hover: #1d1d20;
--border: #26262a;
--text: #e6e6e9;
--text-muted: #8a8a93;
}
body {
font-family: 'Inter', system-ui, -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif;
background: var(--darker);
color: var(--text);
min-height: 100vh;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Subtle ambient glow at the top of the page */
body::before {
content: '';
position: fixed;
top: -300px;
left: 50%;
transform: translateX(-50%);
width: 1200px;
height: 700px;
background: radial-gradient(circle, rgba(91, 141, 239, 0.08) 0%, transparent 60%);
pointer-events: none;
z-index: 0;
}
.glass {
background: var(--surface);
border: 1px solid var(--border);
backdrop-filter: blur(10px);
}
/* Override Tailwind utility colors for dark theme */
.text-gray-800,
.text-gray-700 { color: var(--text) !important; }
.text-gray-600,
.text-gray-500,
.text-gray-400 { color: var(--text-muted) !important; }
.bg-white { background: var(--surface) !important; }
.bg-gray-50 { background: var(--surface-hover) !important; }
.bg-gray-100 { background: var(--surface) !important; }
.border-gray-200,
.border-gray-100 { border-color: var(--border) !important; }
.hover\:bg-gray-50:hover { background: var(--surface-hover) !important; }
.bg-brand-50 { background: rgba(91, 141, 239, 0.08) !important; }
.drop-shadow-lg,
.drop-shadow { filter: none !important; }
kbd {
background: var(--surface) !important;
color: var(--text-muted) !important;
border: 1px solid var(--border) !important;
}
.collection-card {
display: block;
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
cursor: pointer;
border: 1px solid var(--border);
color: inherit;
text-decoration: none;
}
.collection-card:hover {
transform: translateY(-3px);
border-color: #3a3a40;
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.4);
}
.slide-container {
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.4);
transition: transform 0.3s ease;
}
.thumbnail {
transition: transform 0.18s ease, box-shadow 0.18s ease;
cursor: pointer;
}
.thumbnail:hover {
transform: scale(1.04);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
.nav-btn {
transition: transform 0.18s ease, background 0.18s ease, color 0.18s ease, border-color 0.18s ease;
}
.nav-btn:hover:not(:disabled) {
transform: scale(1.06);
}
.nav-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: var(--darker);
border-radius: 3px;
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #3a3a40;
}
.fade-in {
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: scale(0.98);
}
to {
opacity: 1;
transform: scale(1);
}
}
.slide-enter {
animation: slideEnter 0.4s ease-out;
}
@keyframes slideEnter {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Fullscreen styles */
.fullscreen-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.95);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
}
.fullscreen-overlay.hidden {
display: none;
}
.fullscreen-overlay object,
.fullscreen-overlay img {
width: 95vw;
height: 90vh;
max-width: 95vw;
max-height: 90vh;
object-fit: contain;
}
.progress-bar {
height: 4px;
transition: width 0.3s ease;
}
.dropdown-menu {
max-height: min(70vh, 560px);
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
}
/* Expanded view for slide viewer */
.viewer-expanded {
max-width: 95vw !important;
}
/* Theater mode - hide sidebar */
.theater-mode .sidebar-panel {
display: none;
}
.theater-mode .main-slide-area {
flex: 1;
max-width: 100%;
}
.theater-mode .slide-container object {
max-height: 75vh;
width: auto;
margin: 0 auto;
}
/* Better slide sizing for large screens */
@media (min-width: 1920px) {
.viewer-expanded {
max-width: 90vw !important;
}
.slide-container object {
max-height: 70vh;
}
}
@media (min-width: 2560px) {
.viewer-expanded {
max-width: 85vw !important;
}
}
/* View mode toggle button */
.view-mode-btn {
transition: background 0.18s ease, color 0.18s ease;
}
.view-mode-btn.active {
background: var(--primary);
color: #fff;
}
/* SVG object styling */
.slide-container object {
width: 100%;
height: auto;
min-height: 400px;
}
</style>
</head>
<body class="antialiased">
<!-- Header -->
<header class="glass shadow-lg sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-4 py-4">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-4">
<a href="index.html" class="text-gray-600 hover:text-brand-600 transition-colors">
<i class="fas fa-arrow-left"></i>
<span class="ml-2">Back to Projects</span>
</a>
</div>
<!-- Collection Selector (shown when viewing slides) -->
<div id="collectionSelector" class="hidden relative">
<button onclick="toggleDropdown()"
class="flex items-center space-x-2 px-4 py-2 bg-white rounded-lg shadow border border-gray-200 hover:border-brand-400 transition-colors">
<span id="currentCollectionIcon" class="text-xl">🎴</span>
<span id="currentCollectionName" class="font-medium text-gray-700">Select Collection</span>
<i class="fas fa-chevron-down text-gray-400"></i>
</button>
<div id="dropdownMenu"
class="dropdown-menu hidden absolute top-full left-0 mt-2 w-80 bg-white rounded-xl shadow-xl border border-gray-100 z-50">
<!-- Dropdown items will be generated by JavaScript -->
</div>
</div>
<div id="keyboardHints" class="hidden items-center space-x-2 text-sm">
<!-- View Mode Toggle -->
<div class="flex items-center space-x-1 mr-4 bg-gray-100 rounded-lg p-1">
<button onclick="setViewMode('normal')" id="normalModeBtn"
class="view-mode-btn px-3 py-1 rounded-md text-xs font-medium text-gray-600 hover:bg-white"
title="Normal View">
<i class="fas fa-columns"></i>
</button>
<button onclick="setViewMode('theater')" id="theaterModeBtn"
class="view-mode-btn px-3 py-1 rounded-md text-xs font-medium text-gray-600 hover:bg-white"
title="Theater Mode (T)">
<i class="fas fa-tv"></i>
</button>
</div>
<span class="text-gray-500">Shortcuts:</span>
<kbd class="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded border"></kbd>
<kbd class="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded border"></kbd>
<span class="text-gray-500">Navigate</span>
<kbd class="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded border ml-2">T</kbd>
<span class="text-gray-500">Theater</span>
<kbd class="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded border ml-2">F</kbd>
<span class="text-gray-500">Fullscreen</span>
<kbd class="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded border ml-2">ESC</kbd>
<span class="text-gray-500">Back</span>
</div>
</div>
</div>
<!-- Progress bar -->
<div id="progressBar" class="progress-bar bg-brand-500" style="width: 0%"></div>
</header>
<!-- Main Content -->
<main id="mainContent" class="max-w-7xl mx-auto px-4 py-8 transition-all duration-300">
<!-- Library View (Collection Selection) -->
<div id="libraryView">
<div class="text-center mb-12 pt-6">
<h1 class="text-4xl font-bold mb-3" style="color: var(--text); letter-spacing: -0.025em;">
Slide Gallery
</h1>
<p style="color: var(--text-muted);" class="text-lg">
Select a collection to start browsing
</p>
</div>
<!-- Collection Cards Grid -->
<div id="collectionsGrid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Cards will be generated by JavaScript -->
</div>
<!-- Empty State -->
<div id="emptyState" class="hidden text-center py-20">
<h3 class="text-xl font-medium" style="color: var(--text);">No slide collections found</h3>
<p style="color: var(--text-muted);" class="mt-2">Please add slide configurations</p>
</div>
</div>
<!-- Slide Viewer (Hidden by default) -->
<div id="viewerView" class="hidden">
<!-- Title Section -->
<div class="text-center mb-8 slide-enter">
<button onclick="backToLibrary()"
class="inline-flex items-center px-4 py-2 mb-4 transition-colors"
style="color: var(--text-muted);"
onmouseover="this.style.color='var(--text)'"
onmouseout="this.style.color='var(--text-muted)'">
<i class="fas fa-th-large mr-2"></i>
Back to Gallery
</button>
<h1 id="viewerTitle" class="text-3xl font-bold mb-2" style="color: var(--text); letter-spacing: -0.02em;">
Slide Title
</h1>
<p id="viewerDescription" style="color: var(--text-muted);">
Slide Description
</p>
</div>
<!-- Slide Viewer -->
<div id="slideViewerContainer" class="flex flex-col lg:flex-row gap-6 slide-enter">
<!-- Main Slide -->
<div class="main-slide-area flex-1">
<div class="glass rounded-2xl p-4 slide-container">
<object id="mainSlide" data="" type="image/svg+xml"
class="w-full rounded-lg fade-in cursor-pointer" onclick="toggleFullscreen()">
<p class="text-center text-gray-500 py-10">Loading SVG...</p>
</object>
</div>
<!-- Navigation Controls -->
<div class="flex items-center justify-center mt-6 space-x-4">
<button id="prevBtn" onclick="prevSlide()"
class="nav-btn w-12 h-12 rounded-full glass shadow-md flex items-center justify-center text-gray-600 border border-gray-200 hover:bg-brand-500 hover:text-white hover:border-brand-500">
<i class="fas fa-chevron-left"></i>
</button>
<div class="flex items-center space-x-2 px-6 py-2 glass rounded-full shadow-md">
<span id="currentPage" class="text-brand-600 font-bold">1</span>
<span class="text-gray-400">/</span>
<span id="totalPages" class="text-gray-600">1</span>
</div>
<button id="nextBtn" onclick="nextSlide()"
class="nav-btn w-12 h-12 rounded-full glass shadow-md flex items-center justify-center text-gray-600 border border-gray-200 hover:bg-brand-500 hover:text-white hover:border-brand-500">
<i class="fas fa-chevron-right"></i>
</button>
<button onclick="toggleFullscreen()"
class="nav-btn w-12 h-12 rounded-full glass shadow-md flex items-center justify-center text-gray-600 border border-gray-200 hover:bg-brand-500 hover:text-white hover:border-brand-500 ml-4">
<i class="fas fa-expand"></i>
</button>
</div>
</div>
<!-- Thumbnail Sidebar -->
<div class="sidebar-panel lg:w-64 xl:w-72">
<div class="glass rounded-2xl p-4 shadow-lg">
<h3 class="text-sm font-semibold text-gray-600 mb-3 flex items-center">
<i class="fas fa-th-large mr-2 text-brand-600"></i>
Slide Index
</h3>
<div id="thumbnailContainer" class="space-y-3 max-h-[60vh] overflow-y-auto pr-2">
<!-- Thumbnails will be generated by JavaScript -->
</div>
</div>
<!-- Slide Info -->
<div class="glass rounded-2xl p-4 shadow-lg mt-4">
<h3 class="text-sm font-semibold text-gray-600 mb-3 flex items-center">
<i class="fas fa-info-circle mr-2 text-brand-600"></i>
Current Slide
</h3>
<p id="slideTitle" class="text-gray-800 font-medium">Title</p>
<p id="slideDesc" class="text-sm text-gray-500 mt-1">Description</p>
</div>
</div>
</div>
<!-- Slide Overview -->
<div class="mt-12 slide-enter">
<h2 class="text-xl font-bold mb-6 flex items-center" style="color: var(--text); letter-spacing: -0.015em;">
<i class="fas fa-list-ul mr-3" style="color: var(--text-muted);"></i>
Overview
</h2>
<div id="overviewGrid" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-4">
<!-- Overview thumbnails will be generated by JavaScript -->
</div>
</div>
</div>
</main>
<!-- Footer -->
<footer class="mt-12 py-6" style="border-top: 1px solid var(--border);">
<div class="max-w-7xl mx-auto px-4 text-center">
<p class="text-sm" style="color: var(--text-muted);">
PPT Master © 20252026 · Built by <a href="https://www.hehugo.com/" target="_blank" style="color: var(--text); text-decoration: none; border-bottom: 1px solid var(--border);">Hugo He</a>
</p>
</div>
</footer>
<!-- Fullscreen Overlay -->
<div id="fullscreenOverlay" class="fullscreen-overlay hidden" onclick="toggleFullscreen()">
<button class="absolute top-4 right-4 text-white text-2xl hover:text-gray-300 transition-colors">
<i class="fas fa-times"></i>
</button>
<button onclick="event.stopPropagation(); prevSlide()"
class="absolute left-4 top-1/2 -translate-y-1/2 w-12 h-12 rounded-full bg-white/20 hover:bg-white/30 flex items-center justify-center text-white transition-colors">
<i class="fas fa-chevron-left"></i>
</button>
<object id="fullscreenImage" data="" type="image/svg+xml" class="fade-in">
<p>Loading SVG...</p>
</object>
<button onclick="event.stopPropagation(); nextSlide()"
class="absolute right-4 top-1/2 -translate-y-1/2 w-12 h-12 rounded-full bg-white/20 hover:bg-white/30 flex items-center justify-center text-white transition-colors">
<i class="fas fa-chevron-right"></i>
</button>
<div class="absolute bottom-4 left-1/2 -translate-x-1/2 px-4 py-2 bg-black/50 rounded-full text-white text-sm">
<span id="fullscreenPage">1</span> / <span id="fullscreenTotal">1</span>
</div>
</div>
<script>
const EXAMPLES_DATA_URL = 'examples/examples.json';
// State
let collections = [];
let currentCollection = null;
let currentSlide = 0;
let isFullscreen = false;
let dropdownOpen = false;
let viewMode = 'normal';
// Initialize
document.addEventListener('DOMContentLoaded', initViewer);
async function initViewer() {
try {
collections = await loadCollections();
renderCollections();
// Check URL for collection parameter
const urlParams = new URLSearchParams(window.location.search);
const collectionId = urlParams.get('project');
if (collectionId) {
const collection = collections.find(c => c.id === collectionId);
if (collection) {
openCollection(collection);
}
}
} catch (error) {
showLoadError(error);
}
}
async function loadCollections() {
const response = await fetch(EXAMPLES_DATA_URL, { cache: 'no-cache' });
if (!response.ok) {
throw new Error(`Failed to load ${EXAMPLES_DATA_URL}: ${response.status}`);
}
const data = await response.json();
return (data.projects || []).map(project => ({
...project,
slides: project.slides || [],
cover: project.cover || project.slides?.[0]?.file || ''
}));
}
function showLoadError(error) {
console.error(error);
document.getElementById('emptyState').classList.remove('hidden');
document.querySelector('#emptyState h3').textContent = 'Unable to load slide collections';
document.querySelector('#emptyState p').textContent = 'Start a local web server or check examples/examples.json.';
}
// Render collection cards
function renderCollections() {
const grid = document.getElementById('collectionsGrid');
if (collections.length === 0) {
document.getElementById('emptyState').classList.remove('hidden');
return;
}
grid.innerHTML = collections.map(collection => {
const cover = collection.cover || collection.slides[0]?.file || '';
const coverPath = encodeURI(`examples/${collection.folder}/${cover}`);
const collectionUrl = `viewer.html?project=${encodeURIComponent(collection.id)}`;
return `
<a href="${collectionUrl}" data-collection-id="${collection.id}" class="collection-card rounded-xl overflow-hidden" style="background: var(--surface);">
<div class="relative" style="aspect-ratio: 16 / 9; background: var(--darker); border-bottom: 1px solid var(--border);">
<object data="${coverPath}" type="image/svg+xml" class="w-full h-full object-contain" style="pointer-events:none">
<div class="w-full h-full flex items-center justify-center text-3xl" style="opacity: 0.5;">${collection.icon}</div>
</object>
<div class="absolute bottom-3 right-3 px-2.5 py-1 rounded-md text-xs font-medium"
style="background: rgba(13, 13, 15, 0.85); color: var(--text); border: 1px solid var(--border); backdrop-filter: blur(8px);">
${collection.slides.length} slides
</div>
</div>
<div class="p-5" style="border-left: 3px solid ${collection.color};">
<div class="flex items-center gap-2 mb-2">
<span class="text-base">${collection.icon}</span>
<h3 class="font-semibold text-base" style="color: var(--text); letter-spacing: -0.01em;">${collection.title}</h3>
</div>
<p class="text-sm line-clamp-2" style="color: var(--text-muted); line-height: 1.55;">${collection.description}</p>
</div>
</a>
`}).join('');
grid.querySelectorAll('.collection-card').forEach(card => {
card.addEventListener('click', handleCollectionCardClick);
});
renderDropdown();
}
function handleCollectionCardClick(event) {
if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
return;
}
event.preventDefault();
const collection = collections.find(item => item.id === event.currentTarget.dataset.collectionId);
if (collection) {
openCollection(collection);
}
}
// Render dropdown menu
function renderDropdown() {
const dropdown = document.getElementById('dropdownMenu');
dropdown.innerHTML = collections.map(collection => `
<div class="flex items-center px-4 py-3 hover:bg-gray-50 cursor-pointer transition-colors ${currentCollection?.id === collection.id ? 'bg-brand-50' : ''}"
onclick='switchCollection(${JSON.stringify(collection).replace(/'/g, "&#39;")})'>
<span class="text-2xl mr-3">${collection.icon}</span>
<div class="flex-1 min-w-0">
<p class="font-medium text-gray-800 truncate">${collection.title}</p>
<p class="text-xs text-gray-500">${collection.slides.length} slides</p>
</div>
${currentCollection?.id === collection.id ? '<i class="fas fa-check text-brand-500"></i>' : ''}
</div>
`).join('');
}
// Toggle dropdown
function toggleDropdown() {
dropdownOpen = !dropdownOpen;
document.getElementById('dropdownMenu').classList.toggle('hidden', !dropdownOpen);
}
// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
if (!e.target.closest('#collectionSelector')) {
dropdownOpen = false;
document.getElementById('dropdownMenu').classList.add('hidden');
}
});
// Open a collection
function openCollection(collection) {
currentCollection = collection;
currentSlide = 0;
// Update URL
const url = new URL(window.location);
url.searchParams.set('project', collection.id);
window.history.pushState({}, '', url);
// Update UI
document.getElementById('libraryView').classList.add('hidden');
document.getElementById('viewerView').classList.remove('hidden');
document.getElementById('collectionSelector').classList.remove('hidden');
document.getElementById('keyboardHints').classList.remove('hidden');
document.getElementById('keyboardHints').classList.add('flex');
document.getElementById('mainContent').classList.add('viewer-expanded');
setViewMode('normal');
// Update header
document.getElementById('currentCollectionIcon').textContent = collection.icon;
document.getElementById('currentCollectionName').textContent = collection.title;
// Update viewer
document.getElementById('viewerTitle').innerHTML = `${collection.icon} ${collection.title}`;
document.getElementById('viewerDescription').textContent = collection.description;
document.getElementById('totalPages').textContent = collection.slides.length;
document.getElementById('fullscreenTotal').textContent = collection.slides.length;
// Set theme color
document.getElementById('progressBar').style.background = `linear-gradient(90deg, ${collection.color} 0%, ${collection.color}cc 100%)`;
generateThumbnails();
generateOverview();
updateSlide();
renderDropdown();
window.scrollTo({ top: 0, behavior: 'smooth' });
}
// Switch collection from dropdown
function switchCollection(collection) {
toggleDropdown();
openCollection(collection);
}
// Back to library
function backToLibrary() {
currentCollection = null;
const url = new URL(window.location);
url.searchParams.delete('project');
window.history.pushState({}, '', url);
document.getElementById('libraryView').classList.remove('hidden');
document.getElementById('viewerView').classList.add('hidden');
document.getElementById('collectionSelector').classList.add('hidden');
document.getElementById('keyboardHints').classList.add('hidden');
document.getElementById('keyboardHints').classList.remove('flex');
document.getElementById('progressBar').style.width = '0%';
document.getElementById('mainContent').classList.remove('viewer-expanded');
document.getElementById('slideViewerContainer').classList.remove('theater-mode');
window.scrollTo({ top: 0, behavior: 'smooth' });
}
// Set view mode
function setViewMode(mode) {
viewMode = mode;
const container = document.getElementById('slideViewerContainer');
const normalBtn = document.getElementById('normalModeBtn');
const theaterBtn = document.getElementById('theaterModeBtn');
if (mode === 'theater') {
container.classList.add('theater-mode');
normalBtn.classList.remove('active');
theaterBtn.classList.add('active');
} else {
container.classList.remove('theater-mode');
normalBtn.classList.add('active');
theaterBtn.classList.remove('active');
}
}
function toggleViewMode() {
setViewMode(viewMode === 'normal' ? 'theater' : 'normal');
}
// Generate thumbnail sidebar
function generateThumbnails() {
if (!currentCollection) return;
const container = document.getElementById('thumbnailContainer');
const basePath = `examples/${currentCollection.folder}/`;
container.innerHTML = currentCollection.slides.map((slide, index) => {
const slidePath = encodeURI(basePath + slide.file);
return `
<div class="thumbnail rounded-lg overflow-hidden border-2 ${index === 0 ? 'border-brand-500' : 'border-transparent'}"
onclick="goToSlide(${index})"
id="thumb-${index}">
<object data="${slidePath}" type="image/svg+xml" class="w-full" style="pointer-events:none">
<div class="w-full h-16 flex items-center justify-center bg-gray-100">📄</div>
</object>
<div class="px-2 py-1 bg-gray-50 text-xs text-gray-600 truncate">
${index + 1}. ${slide.title}
</div>
</div>
`}).join('');
}
// Generate overview grid
function generateOverview() {
if (!currentCollection) return;
const container = document.getElementById('overviewGrid');
const basePath = `examples/${currentCollection.folder}/`;
container.innerHTML = currentCollection.slides.map((slide, index) => {
const slidePath = encodeURI(basePath + slide.file);
return `
<div class="glass rounded-xl p-2 shadow-md hover:shadow-lg transition-shadow cursor-pointer"
onclick="goToSlide(${index}); window.scrollTo({top: 0, behavior: 'smooth'})">
<object data="${slidePath}" type="image/svg+xml" class="w-full rounded-lg" style="pointer-events:none">
<div class="w-full h-24 flex items-center justify-center bg-gray-100">📄</div>
</object>
<div class="mt-2 px-1">
<p class="text-sm font-medium text-gray-800 truncate">${index + 1}. ${slide.title}</p>
<p class="text-xs text-gray-500 truncate">${slide.desc}</p>
</div>
</div>
`}).join('');
}
// Update slide display
function updateSlide() {
if (!currentCollection) return;
const slide = currentCollection.slides[currentSlide];
const basePath = `examples/${currentCollection.folder}/`;
const slidePath = encodeURI(basePath + slide.file);
const mainSlide = document.getElementById('mainSlide');
const fullscreenImage = document.getElementById('fullscreenImage');
// Update main slide
mainSlide.classList.remove('fade-in');
void mainSlide.offsetWidth;
mainSlide.classList.add('fade-in');
mainSlide.data = slidePath;
// Update fullscreen
fullscreenImage.data = slidePath;
// Update page numbers
document.getElementById('currentPage').textContent = currentSlide + 1;
document.getElementById('fullscreenPage').textContent = currentSlide + 1;
// Update slide info
document.getElementById('slideTitle').textContent = slide.title;
document.getElementById('slideDesc').textContent = slide.desc;
// Update progress bar
const progress = ((currentSlide + 1) / currentCollection.slides.length) * 100;
document.getElementById('progressBar').style.width = progress + '%';
// Update navigation buttons
document.getElementById('prevBtn').disabled = currentSlide === 0;
document.getElementById('nextBtn').disabled = currentSlide === currentCollection.slides.length - 1;
// Update thumbnail highlights
currentCollection.slides.forEach((_, i) => {
const thumb = document.getElementById(`thumb-${i}`);
if (thumb) {
if (i === currentSlide) {
thumb.classList.add('border-brand-500');
thumb.classList.remove('border-transparent');
thumb.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
} else {
thumb.classList.remove('border-brand-500');
thumb.classList.add('border-transparent');
}
}
});
}
// Navigation
function nextSlide() {
if (currentCollection && currentSlide < currentCollection.slides.length - 1) {
currentSlide++;
updateSlide();
}
}
function prevSlide() {
if (currentSlide > 0) {
currentSlide--;
updateSlide();
}
}
function goToSlide(index) {
currentSlide = index;
updateSlide();
}
// Fullscreen toggle
function toggleFullscreen() {
const overlay = document.getElementById('fullscreenOverlay');
isFullscreen = !isFullscreen;
if (isFullscreen) {
overlay.classList.remove('hidden');
document.body.style.overflow = 'hidden';
} else {
overlay.classList.add('hidden');
document.body.style.overflow = '';
}
}
// Keyboard navigation
document.addEventListener('keydown', (e) => {
if (!currentCollection && e.key !== 'Escape') return;
switch (e.key) {
case 'ArrowLeft':
prevSlide();
break;
case 'ArrowRight':
nextSlide();
break;
case 'Escape':
if (isFullscreen) {
toggleFullscreen();
} else if (currentCollection) {
backToLibrary();
}
break;
case 'f':
case 'F':
if (currentCollection) toggleFullscreen();
break;
case 't':
case 'T':
if (currentCollection && !isFullscreen) toggleViewMode();
break;
}
});
// Handle browser back/forward
window.addEventListener('popstate', () => {
const urlParams = new URLSearchParams(window.location.search);
const collectionId = urlParams.get('project');
if (collectionId) {
const collection = collections.find(c => c.id === collectionId);
if (collection) {
openCollection(collection);
}
} else {
backToLibrary();
}
});
// Touch/swipe support
let touchStartX = 0;
let touchEndX = 0;
document.addEventListener('touchstart', (e) => {
touchStartX = e.changedTouches[0].screenX;
});
document.addEventListener('touchend', (e) => {
touchEndX = e.changedTouches[0].screenX;
const diff = touchStartX - touchEndX;
if (Math.abs(diff) > 50 && currentCollection) {
if (diff > 0) nextSlide();
else prevSlide();
}
});
</script>
</body>
</html>