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

668 lines
21 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">
<title>PPT Master — AI generates natively editable PPTX from any document</title>
<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.">
<meta property="og:title" content="PPT Master — AI generates natively editable PPTX">
<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.">
<meta property="og:image" content="https://hugohe3.github.io/ppt-master/docs/assets/screenshots/preview_pritzker_2026.png">
<meta property="og:url" content="https://hugohe3.github.io/ppt-master/">
<meta property="og:type" content="website">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="PPT Master — AI generates natively editable PPTX">
<meta name="twitter:description" content="Drop in any document, get back a natively editable PowerPoint with real shapes. Not images. Open source.">
<meta name="twitter:image" content="https://hugohe3.github.io/ppt-master/docs/assets/screenshots/preview_pritzker_2026.png">
<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 {
/* Editorial dark — single accent, neutral surfaces */
--primary: #5b8def;
--primary-light: #88aaff;
--secondary: #5b8def;
--dark: #161618;
--darker: #0d0d0f;
--text: #e6e6e9;
--text-muted: #8a8a93;
--surface: #16161a;
--surface-hover: #1d1d20;
--border: #26262a;
--success: #5b8def;
--warning: #5b8def;
--accent: #5b8def;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
background: var(--darker);
color: var(--text);
min-height: 100vh;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
}
/* Header */
.header {
background: var(--darker);
padding: 120px 20px 80px;
text-align: center;
position: relative;
overflow: hidden;
border-bottom: 1px solid var(--border);
}
.header::before {
content: '';
position: absolute;
top: -300px;
left: 50%;
transform: translateX(-50%);
width: 1100px;
height: 700px;
background: radial-gradient(circle, rgba(91, 141, 239, 0.12) 0%, transparent 60%);
pointer-events: none;
}
.header-content {
position: relative;
z-index: 1;
max-width: 820px;
margin: 0 auto;
}
.logo {
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--primary);
margin-bottom: 28px;
opacity: 0.95;
}
.logo svg {
width: 56px;
height: auto;
}
.eyebrow {
display: inline-block;
font-size: 0.78rem;
font-weight: 500;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 20px;
padding: 6px 14px;
border: 1px solid var(--border);
border-radius: 999px;
background: var(--surface);
}
.header h1 {
font-size: 3.75rem;
font-weight: 700;
line-height: 1.05;
letter-spacing: -0.025em;
margin-bottom: 20px;
color: var(--text);
}
.header p {
font-size: 1.15rem;
color: var(--text-muted);
max-width: 620px;
margin: 0 auto;
line-height: 1.55;
}
.header-cta {
display: flex;
justify-content: center;
gap: 12px;
margin-top: 36px;
flex-wrap: wrap;
}
.cta-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 11px 22px;
border-radius: 8px;
font-size: 0.95rem;
font-weight: 500;
text-decoration: none;
transition: background 0.18s ease, border-color 0.18s ease, transform 0.18s ease;
border: 1px solid transparent;
cursor: pointer;
letter-spacing: -0.01em;
}
.cta-btn.primary {
background: var(--primary);
color: #fff;
}
.cta-btn.primary:hover {
background: #4a7be0;
transform: translateY(-1px);
}
.cta-btn.secondary {
background: transparent;
color: var(--text);
border-color: var(--border);
}
.cta-btn.secondary:hover {
background: var(--surface);
border-color: #3a3a40;
transform: translateY(-1px);
}
.stats {
display: inline-flex;
justify-content: center;
gap: 0;
margin-top: 56px;
border: 1px solid var(--border);
border-radius: 12px;
background: var(--surface);
overflow: hidden;
}
.stat {
text-align: center;
padding: 18px 32px;
border-right: 1px solid var(--border);
}
.stat:last-child {
border-right: none;
}
.stat-value {
font-size: 1.75rem;
font-weight: 600;
color: var(--text);
letter-spacing: -0.02em;
line-height: 1.1;
}
.stat-label {
font-size: 0.78rem;
color: var(--text-muted);
letter-spacing: 0.08em;
text-transform: uppercase;
margin-top: 4px;
}
/* Navigation */
.nav {
background: rgba(13, 13, 15, 0.85);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border);
padding: 14px 20px;
position: sticky;
top: 0;
z-index: 100;
}
.nav-content {
max-width: 1400px;
margin: 0 auto;
display: flex;
justify-content: center;
gap: 4px;
flex-wrap: wrap;
}
.nav-btn {
background: transparent;
border: 1px solid transparent;
color: var(--text-muted);
padding: 7px 16px;
border-radius: 8px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
transition: color 0.18s ease, background 0.18s ease, border-color 0.18s ease;
letter-spacing: -0.005em;
}
.nav-btn:hover {
color: var(--text);
background: var(--surface);
}
.nav-btn.active {
color: var(--text);
background: var(--surface);
border-color: var(--border);
}
/* Main Content */
.main {
max-width: 1400px;
margin: 0 auto;
padding: 56px 20px 80px;
}
.section-title {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 10px;
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 28px;
color: var(--text);
letter-spacing: -0.015em;
}
.section-title .count {
font-size: 0.85rem;
font-weight: 400;
color: var(--text-muted);
letter-spacing: 0;
}
/* Project Grid */
.projects-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
gap: 20px;
margin-bottom: 50px;
}
.project-card {
display: block;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
overflow: hidden;
transition: border-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
cursor: pointer;
color: inherit;
text-decoration: none;
}
.project-card:hover {
transform: translateY(-3px);
border-color: #3a3a40;
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35);
}
.card-preview {
aspect-ratio: 16 / 9;
background: var(--darker);
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 1px solid var(--border);
}
.card-preview img {
width: 100%;
height: 100%;
object-fit: contain;
background: var(--darker);
}
.card-preview .placeholder {
font-size: 2.5rem;
opacity: 0.25;
color: var(--text-muted);
}
.card-badge {
position: absolute;
top: 12px;
right: 12px;
background: var(--primary);
color: white;
padding: 3px 10px;
border-radius: 6px;
font-size: 0.7rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.card-content {
padding: 18px 20px 20px;
}
.card-title {
font-size: 1.05rem;
font-weight: 600;
margin-bottom: 10px;
color: var(--text);
line-height: 1.35;
letter-spacing: -0.01em;
}
.card-meta {
display: flex;
gap: 14px;
margin-bottom: 10px;
font-size: 0.8rem;
color: var(--text-muted);
}
.card-meta span {
display: flex;
align-items: center;
gap: 5px;
}
.card-desc {
font-size: 0.875rem;
color: var(--text-muted);
line-height: 1.55;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.card-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 14px;
}
.tag {
background: transparent;
padding: 3px 9px;
border-radius: 6px;
border: 1px solid var(--border);
font-size: 0.72rem;
color: var(--text-muted);
letter-spacing: -0.005em;
}
.tag.style-consulting,
.tag.style-general,
.tag.style-creative,
.tag.style-editorial {
background: rgba(91, 141, 239, 0.08);
color: var(--primary-light);
border-color: rgba(91, 141, 239, 0.22);
}
/* Footer */
.footer {
background: var(--darker);
border-top: 1px solid var(--border);
padding: 48px 20px 40px;
text-align: center;
}
.footer-links {
display: flex;
justify-content: center;
gap: 28px;
margin-bottom: 24px;
flex-wrap: wrap;
}
.footer-links a {
color: var(--text-muted);
text-decoration: none;
font-size: 0.9rem;
transition: color 0.18s ease;
}
.footer-links a:hover {
color: var(--text);
}
.footer-copy {
color: var(--text-muted);
font-size: 0.85rem;
}
.footer-copy a {
color: var(--text);
text-decoration: none;
border-bottom: 1px solid var(--border);
transition: border-color 0.18s ease;
}
.footer-copy a:hover {
border-color: var(--text-muted);
}
/* Responsive */
@media (max-width: 768px) {
.header {
padding: 80px 20px 60px;
}
.header h1 {
font-size: 2.25rem;
}
.header p {
font-size: 1rem;
}
.header-cta {
flex-direction: column;
align-items: stretch;
width: 100%;
max-width: 280px;
margin-left: auto;
margin-right: auto;
}
.stats {
margin-top: 36px;
flex-wrap: nowrap;
}
.stat {
padding: 14px 18px;
}
.stat-value {
font-size: 1.4rem;
}
.projects-grid {
grid-template-columns: 1fr;
}
}
/* Hidden class for filtering */
.hidden {
display: none !important;
}
</style>
</head>
<body>
<header class="header">
<div class="header-content">
<div class="logo" aria-hidden="true">
<svg viewBox="0 0 48 32" fill="none" stroke="currentColor" stroke-width="2" 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>
</div>
<div class="eyebrow">Open source · MIT</div>
<h1>PPT Master</h1>
<p>AI generates natively editable PPTX from any document — real PowerPoint shapes, not images.</p>
<div class="header-cta">
<a href="https://github.com/hugohe3/ppt-master" class="cta-btn primary" target="_blank">Star on GitHub</a>
<a href="https://github.com/hugohe3/ppt-master#quick-start" class="cta-btn secondary" target="_blank">Get Started</a>
</div>
<div class="stats">
<div class="stat">
<div class="stat-value" id="examples-count">...</div>
<div class="stat-label">Examples</div>
</div>
<div class="stat">
<div class="stat-value" id="pages-count">...</div>
<div class="stat-label">Pages</div>
</div>
<div class="stat">
<div class="stat-value" id="templates-count">...</div>
<div class="stat-label">Templates</div>
</div>
</div>
</div>
</header>
<nav class="nav">
<div class="nav-content">
<button class="nav-btn active" data-filter="all">All</button>
<button class="nav-btn" data-filter="consulting">Consulting</button>
<button class="nav-btn" data-filter="general">General</button>
<button class="nav-btn" data-filter="creative">Creative</button>
<button class="nav-btn" data-filter="editorial">Editorial</button>
</div>
</nav>
<main class="main">
<div class="projects-grid" id="projects-grid">
<!-- Projects will be inserted here by JavaScript -->
</div>
</main>
<footer class="footer">
<div class="footer-links">
<a href="https://github.com/hugohe3/ppt-master" target="_blank">GitHub</a>
<a href="https://github.com/hugohe3/ppt-master#quick-start" target="_blank">Quick Start</a>
<a href="https://github.com/hugohe3/ppt-master/blob/main/docs/faq.md" target="_blank">FAQ</a>
<a href="https://github.com/hugohe3/ppt-master/discussions" target="_blank">Discussions</a>
</div>
<p class="footer-copy">PPT Master © 20252026 · Built by <a href="https://www.hehugo.com/" target="_blank">Hugo He</a></p>
</footer>
<script>
const EXAMPLES_DATA_URL = 'examples/examples.json';
let projects = [];
let activeFilter = 'all';
async function initProjects() {
try {
const data = await loadExamplesData();
projects = data.projects || [];
updateStats(data.stats);
renderProjects(activeFilter);
} catch (error) {
showLoadError(error);
}
}
async function loadExamplesData() {
const response = await fetch(EXAMPLES_DATA_URL, { cache: 'no-cache' });
if (!response.ok) {
throw new Error(`Failed to load ${EXAMPLES_DATA_URL}: ${response.status}`);
}
return response.json();
}
function updateStats(stats = {}) {
document.getElementById('examples-count').textContent = stats.examples ?? projects.length;
document.getElementById('pages-count').textContent = stats.pages ?? projects.reduce((sum, project) => sum + (project.slides?.length || 0), 0);
document.getElementById('templates-count').textContent = stats.templates ?? '...';
}
function showLoadError(error) {
console.error(error);
const grid = document.getElementById('projects-grid');
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>';
}
// Render projects
function renderProjects(filter = 'all') {
const grid = document.getElementById('projects-grid');
grid.innerHTML = '';
const filtered = filter === 'all'
? projects
: projects.filter(p => p.style === filter);
filtered.forEach(project => {
const card = document.createElement('a');
card.className = 'project-card';
card.href = `viewer.html?project=${encodeURIComponent(project.id)}`;
const styleClass = `style-${project.style || 'general'}`;
const tags = project.tags || [];
const pages = project.slides?.length || 0;
const cover = project.cover || project.slides?.[0]?.file || '';
const folder = project.folder || `${project.id}/svg_final`;
const coverUrl = encodeURI(`examples/${folder}/${cover}`);
const img = document.createElement('img');
img.src = coverUrl;
img.alt = project.title;
img.onerror = function () {
this.onerror = null;
this.parentElement.innerHTML = '<div class="placeholder">—</div>';
};
card.innerHTML = `
<div class="card-preview">
${project.isNew ? '<span class="card-badge new">New</span>' : ''}
</div>
<div class="card-content">
<h3 class="card-title">${project.title}</h3>
<div class="card-meta">
<span>${pages} pages</span>
<span>·</span>
<span>${project.styleName}</span>
</div>
<p class="card-desc">${project.desc}</p>
<div class="card-tags">
<span class="tag ${styleClass}">${project.styleName}</span>
${tags.map(t => `<span class="tag">${t}</span>`).join('')}
</div>
</div>
`;
// Insert image into card-preview
const preview = card.querySelector('.card-preview');
preview.insertBefore(img, preview.firstChild);
grid.appendChild(card);
});
}
// Filter buttons
document.querySelectorAll('.nav-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
activeFilter = btn.dataset.filter;
renderProjects(activeFilter);
});
});
// Initial render
initProjects();
</script>
</body>
</html>