返回 ppt-master
index.html
根目录 / index.html
1 <!DOCTYPE html>
2 <html lang="en">
3
4 <head>
5 <meta charset="UTF-8">
6 <meta name="viewport" content="width=device-width, initial-scale=1.0">
7 <title>PPT Master — AI generates natively editable PPTX from any document</title>
8 <meta name="description" content="Drop in a PDF, DOCX, URL, or Markdown — get back a natively editable PowerPoint with real shapes, not images. Open source templates and live examples.">
9 <meta property="og:title" content="PPT Master — AI generates natively editable PPTX">
10 <meta property="og:description" content="Drop in any document, get back a natively editable PowerPoint with real shapes, real text boxes, and real charts. Not images. Click anything and edit it.">
11 <meta property="og:image" content="https://hugohe3.github.io/ppt-master/docs/assets/screenshots/preview_pritzker_2026.png">
12 <meta property="og:url" content="https://hugohe3.github.io/ppt-master/">
13 <meta property="og:type" content="website">
14 <meta name="twitter:card" content="summary_large_image">
15 <meta name="twitter:title" content="PPT Master — AI generates natively editable PPTX">
16 <meta name="twitter:description" content="Drop in any document, get back a natively editable PowerPoint with real shapes. Not images. Open source.">
17 <meta name="twitter:image" content="https://hugohe3.github.io/ppt-master/docs/assets/screenshots/preview_pritzker_2026.png">
18 <link rel="icon"
19 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>">
20 <style>
21 :root {
22 /* Editorial dark — single accent, neutral surfaces */
23 --primary: #5b8def;
24 --primary-light: #88aaff;
25 --secondary: #5b8def;
26 --dark: #161618;
27 --darker: #0d0d0f;
28 --text: #e6e6e9;
29 --text-muted: #8a8a93;
30 --surface: #16161a;
31 --surface-hover: #1d1d20;
32 --border: #26262a;
33 --success: #5b8def;
34 --warning: #5b8def;
35 --accent: #5b8def;
36 }
37
38 * {
39 margin: 0;
40 padding: 0;
41 box-sizing: border-box;
42 }
43
44 body {
45 font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
46 background: var(--darker);
47 color: var(--text);
48 min-height: 100vh;
49 line-height: 1.6;
50 -webkit-font-smoothing: antialiased;
51 -moz-osx-font-smoothing: grayscale;
52 font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
53 }
54
55 /* Header */
56 .header {
57 background: var(--darker);
58 padding: 120px 20px 80px;
59 text-align: center;
60 position: relative;
61 overflow: hidden;
62 border-bottom: 1px solid var(--border);
63 }
64
65 .header::before {
66 content: '';
67 position: absolute;
68 top: -300px;
69 left: 50%;
70 transform: translateX(-50%);
71 width: 1100px;
72 height: 700px;
73 background: radial-gradient(circle, rgba(91, 141, 239, 0.12) 0%, transparent 60%);
74 pointer-events: none;
75 }
76
77 .header-content {
78 position: relative;
79 z-index: 1;
80 max-width: 820px;
81 margin: 0 auto;
82 }
83
84 .logo {
85 display: inline-flex;
86 align-items: center;
87 justify-content: center;
88 color: var(--primary);
89 margin-bottom: 28px;
90 opacity: 0.95;
91 }
92
93 .logo svg {
94 width: 56px;
95 height: auto;
96 }
97
98 .eyebrow {
99 display: inline-block;
100 font-size: 0.78rem;
101 font-weight: 500;
102 letter-spacing: 0.12em;
103 text-transform: uppercase;
104 color: var(--text-muted);
105 margin-bottom: 20px;
106 padding: 6px 14px;
107 border: 1px solid var(--border);
108 border-radius: 999px;
109 background: var(--surface);
110 }
111
112 .header h1 {
113 font-size: 3.75rem;
114 font-weight: 700;
115 line-height: 1.05;
116 letter-spacing: -0.025em;
117 margin-bottom: 20px;
118 color: var(--text);
119 }
120
121 .header p {
122 font-size: 1.15rem;
123 color: var(--text-muted);
124 max-width: 620px;
125 margin: 0 auto;
126 line-height: 1.55;
127 }
128
129 .header-cta {
130 display: flex;
131 justify-content: center;
132 gap: 12px;
133 margin-top: 36px;
134 flex-wrap: wrap;
135 }
136
137 .cta-btn {
138 display: inline-flex;
139 align-items: center;
140 gap: 8px;
141 padding: 11px 22px;
142 border-radius: 8px;
143 font-size: 0.95rem;
144 font-weight: 500;
145 text-decoration: none;
146 transition: background 0.18s ease, border-color 0.18s ease, transform 0.18s ease;
147 border: 1px solid transparent;
148 cursor: pointer;
149 letter-spacing: -0.01em;
150 }
151
152 .cta-btn.primary {
153 background: var(--primary);
154 color: #fff;
155 }
156
157 .cta-btn.primary:hover {
158 background: #4a7be0;
159 transform: translateY(-1px);
160 }
161
162 .cta-btn.secondary {
163 background: transparent;
164 color: var(--text);
165 border-color: var(--border);
166 }
167
168 .cta-btn.secondary:hover {
169 background: var(--surface);
170 border-color: #3a3a40;
171 transform: translateY(-1px);
172 }
173
174 .stats {
175 display: inline-flex;
176 justify-content: center;
177 gap: 0;
178 margin-top: 56px;
179 border: 1px solid var(--border);
180 border-radius: 12px;
181 background: var(--surface);
182 overflow: hidden;
183 }
184
185 .stat {
186 text-align: center;
187 padding: 18px 32px;
188 border-right: 1px solid var(--border);
189 }
190
191 .stat:last-child {
192 border-right: none;
193 }
194
195 .stat-value {
196 font-size: 1.75rem;
197 font-weight: 600;
198 color: var(--text);
199 letter-spacing: -0.02em;
200 line-height: 1.1;
201 }
202
203 .stat-label {
204 font-size: 0.78rem;
205 color: var(--text-muted);
206 letter-spacing: 0.08em;
207 text-transform: uppercase;
208 margin-top: 4px;
209 }
210
211 /* Navigation */
212 .nav {
213 background: rgba(13, 13, 15, 0.85);
214 backdrop-filter: blur(12px);
215 -webkit-backdrop-filter: blur(12px);
216 border-bottom: 1px solid var(--border);
217 padding: 14px 20px;
218 position: sticky;
219 top: 0;
220 z-index: 100;
221 }
222
223 .nav-content {
224 max-width: 1400px;
225 margin: 0 auto;
226 display: flex;
227 justify-content: center;
228 gap: 4px;
229 flex-wrap: wrap;
230 }
231
232 .nav-btn {
233 background: transparent;
234 border: 1px solid transparent;
235 color: var(--text-muted);
236 padding: 7px 16px;
237 border-radius: 8px;
238 cursor: pointer;
239 font-size: 0.9rem;
240 font-weight: 500;
241 transition: color 0.18s ease, background 0.18s ease, border-color 0.18s ease;
242 letter-spacing: -0.005em;
243 }
244
245 .nav-btn:hover {
246 color: var(--text);
247 background: var(--surface);
248 }
249
250 .nav-btn.active {
251 color: var(--text);
252 background: var(--surface);
253 border-color: var(--border);
254 }
255
256 /* Main Content */
257 .main {
258 max-width: 1400px;
259 margin: 0 auto;
260 padding: 56px 20px 80px;
261 }
262
263 .section-title {
264 display: flex;
265 align-items: baseline;
266 justify-content: space-between;
267 gap: 10px;
268 font-size: 1.25rem;
269 font-weight: 600;
270 margin-bottom: 28px;
271 color: var(--text);
272 letter-spacing: -0.015em;
273 }
274
275 .section-title .count {
276 font-size: 0.85rem;
277 font-weight: 400;
278 color: var(--text-muted);
279 letter-spacing: 0;
280 }
281
282 /* Project Grid */
283 .projects-grid {
284 display: grid;
285 grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
286 gap: 20px;
287 margin-bottom: 50px;
288 }
289
290 .project-card {
291 display: block;
292 background: var(--surface);
293 border: 1px solid var(--border);
294 border-radius: 12px;
295 overflow: hidden;
296 transition: border-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
297 cursor: pointer;
298 color: inherit;
299 text-decoration: none;
300 }
301
302 .project-card:hover {
303 transform: translateY(-3px);
304 border-color: #3a3a40;
305 box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35);
306 }
307
308 .card-preview {
309 aspect-ratio: 16 / 9;
310 background: var(--darker);
311 position: relative;
312 overflow: hidden;
313 display: flex;
314 align-items: center;
315 justify-content: center;
316 border-bottom: 1px solid var(--border);
317 }
318
319 .card-preview img {
320 width: 100%;
321 height: 100%;
322 object-fit: contain;
323 background: var(--darker);
324 }
325
326 .card-preview .placeholder {
327 font-size: 2.5rem;
328 opacity: 0.25;
329 color: var(--text-muted);
330 }
331
332 .card-badge {
333 position: absolute;
334 top: 12px;
335 right: 12px;
336 background: var(--primary);
337 color: white;
338 padding: 3px 10px;
339 border-radius: 6px;
340 font-size: 0.7rem;
341 font-weight: 600;
342 letter-spacing: 0.04em;
343 text-transform: uppercase;
344 }
345
346 .card-content {
347 padding: 18px 20px 20px;
348 }
349
350 .card-title {
351 font-size: 1.05rem;
352 font-weight: 600;
353 margin-bottom: 10px;
354 color: var(--text);
355 line-height: 1.35;
356 letter-spacing: -0.01em;
357 }
358
359 .card-meta {
360 display: flex;
361 gap: 14px;
362 margin-bottom: 10px;
363 font-size: 0.8rem;
364 color: var(--text-muted);
365 }
366
367 .card-meta span {
368 display: flex;
369 align-items: center;
370 gap: 5px;
371 }
372
373 .card-desc {
374 font-size: 0.875rem;
375 color: var(--text-muted);
376 line-height: 1.55;
377 display: -webkit-box;
378 -webkit-line-clamp: 2;
379 -webkit-box-orient: vertical;
380 overflow: hidden;
381 }
382
383 .card-tags {
384 display: flex;
385 flex-wrap: wrap;
386 gap: 6px;
387 margin-top: 14px;
388 }
389
390 .tag {
391 background: transparent;
392 padding: 3px 9px;
393 border-radius: 6px;
394 border: 1px solid var(--border);
395 font-size: 0.72rem;
396 color: var(--text-muted);
397 letter-spacing: -0.005em;
398 }
399
400 .tag.style-consulting,
401 .tag.style-general,
402 .tag.style-creative,
403 .tag.style-editorial {
404 background: rgba(91, 141, 239, 0.08);
405 color: var(--primary-light);
406 border-color: rgba(91, 141, 239, 0.22);
407 }
408
409 /* Footer */
410 .footer {
411 background: var(--darker);
412 border-top: 1px solid var(--border);
413 padding: 48px 20px 40px;
414 text-align: center;
415 }
416
417 .footer-links {
418 display: flex;
419 justify-content: center;
420 gap: 28px;
421 margin-bottom: 24px;
422 flex-wrap: wrap;
423 }
424
425 .footer-links a {
426 color: var(--text-muted);
427 text-decoration: none;
428 font-size: 0.9rem;
429 transition: color 0.18s ease;
430 }
431
432 .footer-links a:hover {
433 color: var(--text);
434 }
435
436 .footer-copy {
437 color: var(--text-muted);
438 font-size: 0.85rem;
439 }
440
441 .footer-copy a {
442 color: var(--text);
443 text-decoration: none;
444 border-bottom: 1px solid var(--border);
445 transition: border-color 0.18s ease;
446 }
447
448 .footer-copy a:hover {
449 border-color: var(--text-muted);
450 }
451
452 /* Responsive */
453 @media (max-width: 768px) {
454 .header {
455 padding: 80px 20px 60px;
456 }
457
458 .header h1 {
459 font-size: 2.25rem;
460 }
461
462 .header p {
463 font-size: 1rem;
464 }
465
466 .header-cta {
467 flex-direction: column;
468 align-items: stretch;
469 width: 100%;
470 max-width: 280px;
471 margin-left: auto;
472 margin-right: auto;
473 }
474
475 .stats {
476 margin-top: 36px;
477 flex-wrap: nowrap;
478 }
479
480 .stat {
481 padding: 14px 18px;
482 }
483
484 .stat-value {
485 font-size: 1.4rem;
486 }
487
488 .projects-grid {
489 grid-template-columns: 1fr;
490 }
491 }
492
493 /* Hidden class for filtering */
494 .hidden {
495 display: none !important;
496 }
497 </style>
498 </head>
499
500 <body>
501 <header class="header">
502 <div class="header-content">
503 <div class="logo" aria-hidden="true">
504 <svg viewBox="0 0 48 32" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
505 <rect x="2" y="2" width="44" height="28" rx="2.5"/>
506 <line x1="9" y1="11" x2="22" y2="11"/>
507 <line x1="9" y1="17" x2="32" y2="17"/>
508 <line x1="9" y1="23" x2="26" y2="23"/>
509 </svg>
510 </div>
511 <div class="eyebrow">Open source · MIT</div>
512 <h1>PPT Master</h1>
513 <p>AI generates natively editable PPTX from any document — real PowerPoint shapes, not images.</p>
514 <div class="header-cta">
515 <a href="https://github.com/hugohe3/ppt-master" class="cta-btn primary" target="_blank">Star on GitHub</a>
516 <a href="https://github.com/hugohe3/ppt-master#quick-start" class="cta-btn secondary" target="_blank">Get Started</a>
517 </div>
518 <div class="stats">
519 <div class="stat">
520 <div class="stat-value" id="examples-count">...</div>
521 <div class="stat-label">Examples</div>
522 </div>
523 <div class="stat">
524 <div class="stat-value" id="pages-count">...</div>
525 <div class="stat-label">Pages</div>
526 </div>
527 <div class="stat">
528 <div class="stat-value" id="templates-count">...</div>
529 <div class="stat-label">Templates</div>
530 </div>
531 </div>
532 </div>
533 </header>
534
535 <nav class="nav">
536 <div class="nav-content">
537 <button class="nav-btn active" data-filter="all">All</button>
538 <button class="nav-btn" data-filter="consulting">Consulting</button>
539 <button class="nav-btn" data-filter="general">General</button>
540 <button class="nav-btn" data-filter="creative">Creative</button>
541 <button class="nav-btn" data-filter="editorial">Editorial</button>
542 </div>
543 </nav>
544
545 <main class="main">
546 <div class="projects-grid" id="projects-grid">
547 <!-- Projects will be inserted here by JavaScript -->
548 </div>
549 </main>
550
551 <footer class="footer">
552 <div class="footer-links">
553 <a href="https://github.com/hugohe3/ppt-master" target="_blank">GitHub</a>
554 <a href="https://github.com/hugohe3/ppt-master#quick-start" target="_blank">Quick Start</a>
555 <a href="https://github.com/hugohe3/ppt-master/blob/main/docs/faq.md" target="_blank">FAQ</a>
556 <a href="https://github.com/hugohe3/ppt-master/discussions" target="_blank">Discussions</a>
557 </div>
558 <p class="footer-copy">PPT Master © 2025–2026 · Built by <a href="https://www.hehugo.com/" target="_blank">Hugo He</a></p>
559 </footer>
560
561 <script>
562 const EXAMPLES_DATA_URL = 'examples/examples.json';
563 let projects = [];
564 let activeFilter = 'all';
565
566 async function initProjects() {
567 try {
568 const data = await loadExamplesData();
569 projects = data.projects || [];
570 updateStats(data.stats);
571 renderProjects(activeFilter);
572 } catch (error) {
573 showLoadError(error);
574 }
575 }
576
577 async function loadExamplesData() {
578 const response = await fetch(EXAMPLES_DATA_URL, { cache: 'no-cache' });
579 if (!response.ok) {
580 throw new Error(`Failed to load ${EXAMPLES_DATA_URL}: ${response.status}`);
581 }
582 return response.json();
583 }
584
585 function updateStats(stats = {}) {
586 document.getElementById('examples-count').textContent = stats.examples ?? projects.length;
587 document.getElementById('pages-count').textContent = stats.pages ?? projects.reduce((sum, project) => sum + (project.slides?.length || 0), 0);
588 document.getElementById('templates-count').textContent = stats.templates ?? '...';
589 }
590
591 function showLoadError(error) {
592 console.error(error);
593 const grid = document.getElementById('projects-grid');
594 grid.innerHTML = '<div class="project-card"><div class="card-content"><h3 class="card-title">Unable to load examples</h3><p class="card-desc">Start a local web server or check examples/examples.json.</p></div></div>';
595 }
596
597 // Render projects
598 function renderProjects(filter = 'all') {
599 const grid = document.getElementById('projects-grid');
600 grid.innerHTML = '';
601
602 const filtered = filter === 'all'
603 ? projects
604 : projects.filter(p => p.style === filter);
605
606 filtered.forEach(project => {
607 const card = document.createElement('a');
608 card.className = 'project-card';
609 card.href = `viewer.html?project=${encodeURIComponent(project.id)}`;
610
611 const styleClass = `style-${project.style || 'general'}`;
612 const tags = project.tags || [];
613 const pages = project.slides?.length || 0;
614 const cover = project.cover || project.slides?.[0]?.file || '';
615 const folder = project.folder || `${project.id}/svg_final`;
616 const coverUrl = encodeURI(`examples/${folder}/${cover}`);
617 const img = document.createElement('img');
618 img.src = coverUrl;
619 img.alt = project.title;
620 img.onerror = function () {
621 this.onerror = null;
622 this.parentElement.innerHTML = '<div class="placeholder">—</div>';
623 };
624
625 card.innerHTML = `
626 <div class="card-preview">
627 ${project.isNew ? '<span class="card-badge new">New</span>' : ''}
628 </div>
629 <div class="card-content">
630 <h3 class="card-title">${project.title}</h3>
631 <div class="card-meta">
632 <span>${pages} pages</span>
633 <span>·</span>
634 <span>${project.styleName}</span>
635 </div>
636 <p class="card-desc">${project.desc}</p>
637 <div class="card-tags">
638 <span class="tag ${styleClass}">${project.styleName}</span>
639 ${tags.map(t => `<span class="tag">${t}</span>`).join('')}
640 </div>
641 </div>
642 `;
643
644 // Insert image into card-preview
645 const preview = card.querySelector('.card-preview');
646 preview.insertBefore(img, preview.firstChild);
647
648 grid.appendChild(card);
649 });
650 }
651
652 // Filter buttons
653 document.querySelectorAll('.nav-btn').forEach(btn => {
654 btn.addEventListener('click', () => {
655 document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
656 btn.classList.add('active');
657 activeFilter = btn.dataset.filter;
658 renderProjects(activeFilter);
659 });
660 });
661
662 // Initial render
663 initProjects();
664 </script>
665 </body>
666
667 </html>
668
668 lines HTML