feat: 导出 SproutClaw .sproutclaw 配置

包含 extensions、skills、prompts、settings、auth、models、mcp 等配置。
排除 node_modules、npm 缓存、sessions 等运行时数据。
This commit is contained in:
2026-06-26 15:48:56 +08:00
commit 50edff80f5
13904 changed files with 411646 additions and 0 deletions

View File

@@ -0,0 +1,930 @@
<!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>