keynot
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseKeynot
Keynot
A skill for building browser-based slide presentations as single, self-contained HTML files. Keyboard navigation, swipe, fullscreen, animated reveals, embedded images, brand-accurate design. No external dependencies required at runtime.
一款用于构建基于浏览器的幻灯片演示文稿的技能,生成的演示文稿为单一、独立的HTML文件。支持键盘导航、滑动操作、全屏显示、动画展示、嵌入图片以及符合品牌规范的设计。运行时无需任何外部依赖。
When to Use This Skill
何时使用此技能
- User asks for a "presentation", "deck", "slides", "one-pager to present"
- User has a brand style guide and wants slides that match it
- User wants to iterate on slides quickly in the browser
- User wants a file they can open anywhere without software
Prefer HTML over PPTX unless:
- User explicitly asks for
.pptx - User needs to edit slides in PowerPoint
- User needs to share with someone who must edit in Office
- 用户要求制作“演示文稿”“幻灯片组”“幻灯片”“用于展示的单页内容”
- 用户拥有品牌风格指南,并希望幻灯片与之匹配
- 用户希望在浏览器中快速迭代修改幻灯片
- 用户希望获得一个无需软件即可在任意设备打开的文件
优先选择HTML而非PPTX,除非:
- 用户明确要求格式
.pptx - 用户需要在PowerPoint中编辑幻灯片
- 用户需要与必须使用Office编辑的人分享
Step 1: Extract the Brand Guide
步骤1:提取品牌指南信息
If the user provides a brand PDF, URL, or describes their brand, extract these before writing any code. Be explicit and precise.
如果用户提供品牌PDF、URL或描述其品牌风格,在编写任何代码前先提取这些信息。提取时需明确且精准。
What to Extract
需要提取的内容
Colors
- Primary (dominant background/text color)
- Accent (calls to action, rules, highlights)
- Neutrals (background, cream, gray)
- Document exact hex codes — never approximate
Typography
- Serif font (display headings, pull quotes)
- Sans-serif font (body, labels, UI)
- If proprietary fonts are specified (e.g., ITC Stone Serif, Acumin), find the closest Google Fonts equivalent
- Document the pairing explicitly
Layout Language
- Dominant color usage ratio (e.g., "blue should always dominate, red as accent only")
- Typical layout patterns visible in the guide (split panels, rules, dividers, margins)
- Tone: formal/editorial, warm/approachable, minimal/dense
Logo and Mark Rules
- Whether the logo/wordmark may be reproduced
- Clear space requirements
- Approved color variants (full color, white, black)
- Note: for institutional brands (universities, corporations), do NOT reproduce trademarked logos — use a text-based wordmark instead
颜色
- 主色调(主要背景/文本颜色)
- 强调色(用于号召性按钮、分隔线、高亮内容)
- 中性色(背景色、米白色、灰色)
- 记录精确的十六进制代码——绝不近似
排版
- 衬线字体(用于标题、引用文本)
- 无衬线字体(用于正文、标签、UI)
- 如果指定了专有字体(如ITC Stone Serif、Acumin),找到最接近的Google Fonts替代字体
- 明确记录字体搭配
布局规范
- 主色调使用比例(例如:“蓝色应始终为主色调,红色仅作为强调色”)
- 指南中可见的典型布局模式(分栏面板、分隔线、边距)
- 风格基调:正式/编辑风、温暖/亲和风、极简/密集风
Logo及标识规则
- 是否允许复制Logo/文字标识
- 留白要求
- 批准使用的颜色变体(全彩、白色、黑色)
- 注意:对于机构品牌(大学、企业),请勿复制商标Logo——改用基于文本的文字标识
Example Brand Record
品牌信息记录示例
Once extracted, record the brand parameters in a compact block at the top of the deck's so they're easy to audit and swap:
<style>Primary Colors:
Primary: #HEX (dominant — backgrounds, headings)
Accent: #HEX (rules, CTAs, status — used sparingly)
Cream: #HEX (light slide backgrounds)
Usage Rule: One color dominates. Accent appears sparingly —
rules, call-to-action bars, status indicators.
Never use the accent color as a full background.
Typography:
Serif: <Brand serif> → <closest Google Font>
Sans-serif: <Brand sans-serif> → <closest Google Font>
Layout Language:
- Which colors own which regions (dark panels vs. light panels)
- Any spine bars, rules, or dividers the brand uses
- Layout motifs (split panels, stat columns, card grids)
- Tone: editorial / warm / minimal / dense
Logo Rule: For institutional or trademarked brands, do NOT
reproduce the logo. Use a text-based wordmark instead:
<div class="wordmark">
<div class="w-line-1">Organization</div>
<div class="w-line-2">Subtitle or parent entity</div>
</div>提取完成后,将品牌参数记录在演示文稿标签顶部的紧凑区块中,以便于审核和替换:
<style>Primary Colors:
Primary: #HEX (dominant — backgrounds, headings)
Accent: #HEX (rules, CTAs, status — used sparingly)
Cream: #HEX (light slide backgrounds)
Usage Rule: One color dominates. Accent appears sparingly —
rules, call-to-action bars, status indicators.
Never use the accent color as a full background.
Typography:
Serif: <Brand serif> → <closest Google Font>
Sans-serif: <Brand sans-serif> → <closest Google Font>
Layout Language:
- Which colors own which regions (dark panels vs. light panels)
- Any spine bars, rules, or dividers the brand uses
- Layout motifs (split panels, stat columns, card grids)
- Tone: editorial / warm / minimal / dense
Logo Rule: For institutional or trademarked brands, do NOT
reproduce the logo. Use a text-based wordmark instead:
<div class="wordmark">
<div class="w-line-1">Organization</div>
<div class="w-line-2">Subtitle or parent entity</div>
</div>Step 2: Deck Architecture
步骤2:演示文稿架构
Every deck is a single HTML file. All assets (fonts via CDN, images via base64) are embedded.
每个演示文稿都是单一HTML文件。所有资源(通过CDN引入的字体、通过base64嵌入的图片)均内嵌在文件中。
Shell Structure
基础结构
html
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Google Fonts import -->
<!-- All CSS in <style> tag -->
</head>
<body>
<div class="deck" id="deck">
<div class="slide" id="s1">...</div>
<div class="slide" id="s2">...</div>
<!-- more slides -->
</div>
<!-- Navigation -->
<div class="nav">
<button id="prev">←</button>
<div class="dots" id="dots"></div>
<button id="next">→</button>
<span class="counter" id="counter">1 / N</span>
<button id="fsBtn" onclick="toggleFS()">⛶</button>
</div>
<!-- Portrait/mobile rotate hint (see Portrait / Mobile Handling below) -->
<div class="rotate-overlay" aria-hidden="true">
<svg class="rotate-icon" width="56" height="56" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="1.4"
stroke-linecap="round" stroke-linejoin="round">
<rect x="5" y="2" width="14" height="20" rx="2.5" />
<line x1="12" y1="18.5" x2="12.01" y2="18.5" />
</svg>
<h2>Rotate your device</h2>
<p>This deck is designed for landscape viewing. Turn your phone sideways to continue.</p>
</div>
<script><!-- All JS inline --></script>
</body>
</html>html
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Google Fonts import -->
<!-- All CSS in <style> tag -->
</head>
<body>
<div class="deck" id="deck">
<div class="slide" id="s1">...</div>
<div class="slide" id="s2">...</div>
<!-- more slides -->
</div>
<!-- Navigation -->
<div class="nav">
<button id="prev">←</button>
<div class="dots" id="dots"></div>
<button id="next">→</button>
<span class="counter" id="counter">1 / N</span>
<button id="fsBtn" onclick="toggleFS()">⛶</button>
</div>
<!-- Portrait/mobile rotate hint (see Portrait / Mobile Handling below) -->
<div class="rotate-overlay" aria-hidden="true">
<svg class="rotate-icon" width="56" height="56" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="1.4"
stroke-linecap="round" stroke-linejoin="round">
<rect x="5" y="2" width="14" height="20" rx="2.5" />
<line x1="12" y1="18.5" x2="12.01" y2="18.5" />
</svg>
<h2>Rotate your device</h2>
<p>This deck is designed for landscape viewing. Turn your phone sideways to continue.</p>
</div>
<script><!-- All JS inline --></script>
</body>
</html>Core CSS Patterns
核心CSS模式
css
:root {
/* Always use CSS variables for brand colors — swap these per brand */
--primary: #1a1a2e; /* dominant: dark panels, headings */
--accent: #e94560; /* sparingly: rules, CTAs, status */
--white: #ffffff;
--cream: #f7f4ee; /* light slide backgrounds */
--serif: 'Cormorant Garamond', Georgia, serif;
--sans: 'DM Sans', system-ui, sans-serif;
}
/* Deck shell */
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; }
.deck { width: 100vw; height: 100vh; position: relative; }
/* Slides: opacity transition, stacked absolutely */
.slide {
position: absolute; inset: 0;
opacity: 0; pointer-events: none;
transition: opacity .55s cubic-bezier(.4,0,.2,1);
}
.slide.active { opacity: 1; pointer-events: all; }
/* Staggered reveal animation on active slide children */
.slide.active .reveal { animation: fadeUp .6s both; }
.slide.active .reveal:nth-child(2) { animation-delay: .1s; }
.slide.active .reveal:nth-child(3) { animation-delay: .2s; }
/* ...continue for as many children as needed */
@keyframes fadeUp {
from { opacity: 0; transform: translateY(22px); }
to { opacity: 1; transform: translateY(0); }
}css
:root {
/* Always use CSS variables for brand colors — swap these per brand */
--primary: #1a1a2e; /* dominant: dark panels, headings */
--accent: #e94560; /* sparingly: rules, CTAs, status */
--white: #ffffff;
--cream: #f7f4ee; /* light slide backgrounds */
--serif: 'Cormorant Garamond', Georgia, serif;
--sans: 'DM Sans', system-ui, sans-serif;
}
/* Deck shell */
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; }
.deck { width: 100vw; height: 100vh; position: relative; }
/* Slides: opacity transition, stacked absolutely */
.slide {
position: absolute; inset: 0;
opacity: 0; pointer-events: none;
transition: opacity .55s cubic-bezier(.4,0,.2,1);
}
.slide.active { opacity: 1; pointer-events: all; }
/* Staggered reveal animation on active slide children */
.slide.active .reveal { animation: fadeUp .6s both; }
.slide.active .reveal:nth-child(2) { animation-delay: .1s; }
.slide.active .reveal:nth-child(3) { animation-delay: .2s; }
/* ...continue for as many children as needed */
@keyframes fadeUp {
from { opacity: 0; transform: translateY(22px); }
to { opacity: 1; transform: translateY(0); }
}Navigation CSS
导航CSS
css
.nav {
position: fixed; bottom: 28px; left: 50%;
transform: translateX(-50%);
z-index: 999;
display: flex; align-items: center; gap: 14px;
background: rgba(26,26,46,.75); /* tint from --primary */
backdrop-filter: blur(10px);
padding: 9px 20px; border-radius: 40px;
border: 1px solid rgba(255,255,255,.14);
opacity: 1;
transition: opacity .4s ease; /* required for auto-hide */
}
.nav-hidden { opacity: 0; pointer-events: none; } /* auto-hide state */
.nav button {
background: none; border: none; cursor: pointer;
color: rgba(255,255,255,.65); font-size: 16px;
width: 30px; height: 30px;
display: flex; align-items: center; justify-content: center;
border-radius: 50%;
transition: color .2s, background .2s;
}
.nav button:hover { color: #fff; background: rgba(255,255,255,.12); }
.dot {
width: 7px; height: 7px; border-radius: 50%;
background: rgba(255,255,255,.3); cursor: pointer;
transition: background .2s, transform .2s;
}
.dot.active { background: var(--accent); transform: scale(1.35); }css
.nav {
position: fixed; bottom: 28px; left: 50%;
transform: translateX(-50%);
z-index: 999;
display: flex; align-items: center; gap: 14px;
background: rgba(26,26,46,.75); /* tint from --primary */
backdrop-filter: blur(10px);
padding: 9px 20px; border-radius: 40px;
border: 1px solid rgba(255,255,255,.14);
opacity: 1;
transition: opacity .4s ease; /* required for auto-hide */
}
.nav-hidden { opacity: 0; pointer-events: none; } /* auto-hide state */
.nav button {
background: none; border: none; cursor: pointer;
color: rgba(255,255,255,.65); font-size: 16px;
width: 30px; height: 30px;
display: flex; align-items: center; justify-content: center;
border-radius: 50%;
transition: color .2s, background .2s;
}
.nav button:hover { color: #fff; background: rgba(255,255,255,.12); }
.dot {
width: 7px; height: 7px; border-radius: 50%;
background: rgba(255,255,255,.3); cursor: pointer;
transition: background .2s, transform .2s;
}
.dot.active { background: var(--accent); transform: scale(1.35); }JavaScript (complete — do not modify arithmetic)
JavaScript (complete — do not modify arithmetic)
javascript
// Fullscreen
function toggleFS() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(() => {});
document.getElementById('fsBtn').innerHTML = '✕';
} else {
document.exitFullscreen();
document.getElementById('fsBtn').innerHTML = '⛶';
}
}
document.addEventListener('fullscreenchange', () => {
if (!document.fullscreenElement)
document.getElementById('fsBtn').innerHTML = '⛶';
});
// Deep-link & session persistence helpers
function parseHash() {
const m = location.hash.match(/^#slide-(\d+)$/);
if (!m) return -1;
const idx = parseInt(m[1], 10) - 1; // hash is 1-based, array is 0-based
return (idx >= 0 && idx < document.querySelectorAll('.slide').length) ? idx : -1;
}
function initialSlide() {
const fromHash = parseHash();
if (fromHash >= 0) return fromHash;
const stored = sessionStorage.getItem('keynot-slide');
if (stored !== null) {
const idx = parseInt(stored, 10);
if (idx >= 0 && idx < document.querySelectorAll('.slide').length) return idx;
}
return 0;
}
function persistSlide(idx) {
sessionStorage.setItem('keynot-slide', idx);
history.replaceState(null, '', '#slide-' + (idx + 1));
}
// Navigation
const slides = document.querySelectorAll('.slide'); // REQUIRED — do not omit
const dotsEl = document.getElementById('dots');
const counter = document.getElementById('counter');
let cur = initialSlide();
// Remove default 'active' class from slide 1 if restoring a different slide
slides.forEach(s => s.classList.remove('active'));
slides.forEach((_, i) => {
const d = document.createElement('div');
d.className = 'dot';
d.addEventListener('click', () => go(i));
dotsEl.appendChild(d);
});
// Activate the initial slide
slides[cur].classList.add('active');
dotsEl.children[cur].classList.add('active');
counter.textContent = `${cur + 1} / ${slides.length}`;
persistSlide(cur);
function go(n) {
slides[cur].classList.remove('active');
dotsEl.children[cur].classList.remove('active');
cur = (n + slides.length) % slides.length;
slides[cur].classList.add('active');
dotsEl.children[cur].classList.add('active');
counter.textContent = `${cur + 1} / ${slides.length}`;
persistSlide(cur);
}
// Deep-link: respond to manual hash changes or pasted URLs
window.addEventListener('hashchange', () => {
const idx = parseHash();
if (idx >= 0 && idx !== cur) go(idx);
});
document.getElementById('next').addEventListener('click', () => go(cur + 1));
document.getElementById('prev').addEventListener('click', () => go(cur - 1));
document.addEventListener('keydown', e => {
if (e.key === 'ArrowRight' || e.key === 'ArrowDown' || e.key === ' ') go(cur + 1);
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') go(cur - 1);
if (e.key === 'f' || e.key === 'F') toggleFS();
});
// Auto-hide nav after 1.8s of inactivity; reappear on any interaction
const nav = document.querySelector('.nav');
let hideTimer;
function showNav() {
nav.classList.remove('nav-hidden');
clearTimeout(hideTimer);
hideTimer = setTimeout(() => nav.classList.add('nav-hidden'), 1800);
}
document.addEventListener('mousemove', showNav);
document.addEventListener('touchstart', showNav, { passive: true });
document.addEventListener('keydown', showNav);
showNav(); // start timer on load
// Swipe
let tx = 0;
document.addEventListener('touchstart', e => { tx = e.touches[0].clientX; }, { passive: true });
document.addEventListener('touchend', e => {
const dx = e.changedTouches[0].clientX - tx;
if (Math.abs(dx) > 50) go(dx < 0 ? cur + 1 : cur - 1);
}, { passive: true });javascript
// Fullscreen
function toggleFS() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(() => {});
document.getElementById('fsBtn').innerHTML = '✕';
} else {
document.exitFullscreen();
document.getElementById('fsBtn').innerHTML = '⛶';
}
}
document.addEventListener('fullscreenchange', () => {
if (!document.fullscreenElement)
document.getElementById('fsBtn').innerHTML = '⛶';
});
// Deep-link & session persistence helpers
function parseHash() {
const m = location.hash.match(/^#slide-(\d+)$/);
if (!m) return -1;
const idx = parseInt(m[1], 10) - 1; // hash is 1-based, array is 0-based
return (idx >= 0 && idx < document.querySelectorAll('.slide').length) ? idx : -1;
}
function initialSlide() {
const fromHash = parseHash();
if (fromHash >= 0) return fromHash;
const stored = sessionStorage.getItem('keynot-slide');
if (stored !== null) {
const idx = parseInt(stored, 10);
if (idx >= 0 && idx < document.querySelectorAll('.slide').length) return idx;
}
return 0;
}
function persistSlide(idx) {
sessionStorage.setItem('keynot-slide', idx);
history.replaceState(null, '', '#slide-' + (idx + 1));
}
// Navigation
const slides = document.querySelectorAll('.slide'); // REQUIRED — do not omit
const dotsEl = document.getElementById('dots');
const counter = document.getElementById('counter');
let cur = initialSlide();
// Remove default 'active' class from slide 1 if restoring a different slide
slides.forEach(s => s.classList.remove('active'));
slides.forEach((_, i) => {
const d = document.createElement('div');
d.className = 'dot';
d.addEventListener('click', () => go(i));
dotsEl.appendChild(d);
});
// Activate the initial slide
slides[cur].classList.add('active');
dotsEl.children[cur].classList.add('active');
counter.textContent = `${cur + 1} / ${slides.length}`;
persistSlide(cur);
function go(n) {
slides[cur].classList.remove('active');
dotsEl.children[cur].classList.remove('active');
cur = (n + slides.length) % slides.length;
slides[cur].classList.add('active');
dotsEl.children[cur].classList.add('active');
counter.textContent = `${cur + 1} / ${slides.length}`;
persistSlide(cur);
}
// Deep-link: respond to manual hash changes or pasted URLs
window.addEventListener('hashchange', () => {
const idx = parseHash();
if (idx >= 0 && idx !== cur) go(idx);
});
document.getElementById('next').addEventListener('click', () => go(cur + 1));
document.getElementById('prev').addEventListener('click', () => go(cur - 1));
document.addEventListener('keydown', e => {
if (e.key === 'ArrowRight' || e.key === 'ArrowDown' || e.key === ' ') go(cur + 1);
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') go(cur - 1);
if (e.key === 'f' || e.key === 'F') toggleFS();
});
// Auto-hide nav after 1.8s of inactivity; reappear on any interaction
const nav = document.querySelector('.nav');
let hideTimer;
function showNav() {
nav.classList.remove('nav-hidden');
clearTimeout(hideTimer);
hideTimer = setTimeout(() => nav.classList.add('nav-hidden'), 1800);
}
document.addEventListener('mousemove', showNav);
document.addEventListener('touchstart', showNav, { passive: true });
document.addEventListener('keydown', showNav);
showNav(); // start timer on load
// Swipe
let tx = 0;
document.addEventListener('touchstart', e => { tx = e.touches[0].clientX; }, { passive: true });
document.addEventListener('touchend', e => {
const dx = e.changedTouches[0].clientX - tx;
if (Math.abs(dx) > 50) go(dx < 0 ? cur + 1 : cur - 1);
}, { passive: true });Portrait / Mobile Handling
竖屏/移动端适配
Slide decks are a landscape medium. Rather than reflowing split-panel layouts into unreadable mobile stacks, show a "rotate your device" overlay when the viewport is portrait + narrow. This covers iPhones held vertically without doubling the CSS for every layout pattern. Landscape phone viewing keeps working because font sizing already scales down.
clamp()Add this CSS to every deck, near the bottom of the block so it overrides nothing:
<style>css
/* Rotate-to-landscape overlay — only visible on portrait phones */
.rotate-overlay {
display: none;
position: fixed; inset: 0;
z-index: 9999;
background: var(--primary);
color: var(--white);
flex-direction: column;
align-items: center; justify-content: center;
text-align: center; padding: 40px;
font-family: var(--sans);
}
.rotate-overlay .rotate-icon {
margin-bottom: 24px;
color: var(--accent);
animation: rotate-hint 2.4s ease-in-out infinite;
}
.rotate-overlay h2 {
font-family: var(--serif);
font-size: 30px; font-weight: 400;
margin-bottom: 12px; letter-spacing: -.01em;
}
.rotate-overlay p {
font-size: 14px; line-height: 1.6;
opacity: .72; max-width: 280px;
}
@keyframes rotate-hint {
0%, 40%, 100% { transform: rotate(0deg); }
60%, 80% { transform: rotate(90deg); }
}
@media (orientation: portrait) and (max-width: 900px) {
.rotate-overlay { display: flex; }
}The overlay HTML is included in the Shell Structure above. Drop both in and the deck handles portrait phones gracefully without any per-slide changes.
Why not full mobile responsiveness? Split panels, stat columns, and photo backgrounds all rely on horizontal real estate to communicate. A reflowed mobile version is effectively a different deck — and a worse one. PowerPoint and Google Slides also fail on portrait mobile; the audience already accepts this for the format.
幻灯片是横屏媒介。与其将分栏布局重排为难以阅读的移动端堆叠布局,不如在视口为竖屏且较窄时显示“旋转设备”提示层。这可以覆盖竖屏持握的iPhone,而无需为每种布局模式加倍编写CSS。横屏手机仍可正常查看,因为字体大小已自动缩放。
clamp()将以下CSS添加到每个演示文稿的标签底部,确保不会覆盖其他样式:
<style>css
/* Rotate-to-landscape overlay — only visible on portrait phones */
.rotate-overlay {
display: none;
position: fixed; inset: 0;
z-index: 9999;
background: var(--primary);
color: var(--white);
flex-direction: column;
align-items: center; justify-content: center;
text-align: center; padding: 40px;
font-family: var(--sans);
}
.rotate-overlay .rotate-icon {
margin-bottom: 24px;
color: var(--accent);
animation: rotate-hint 2.4s ease-in-out infinite;
}
.rotate-overlay h2 {
font-family: var(--serif);
font-size: 30px; font-weight: 400;
margin-bottom: 12px; letter-spacing: -.01em;
}
.rotate-overlay p {
font-size: 14px; line-height: 1.6;
opacity: .72; max-width: 280px;
}
@keyframes rotate-hint {
0%, 40%, 100% { transform: rotate(0deg); }
60%, 80% { transform: rotate(90deg); }
}
@media (orientation: portrait) and (max-width: 900px) {
.rotate-overlay { display: flex; }
}提示层HTML已包含在上方的【基础结构】中。同时添加这两部分,演示文稿即可优雅适配竖屏手机,无需对每张幻灯片进行修改。
**为何不做全移动端响应式?**分栏、统计列和图片背景都依赖横向空间来传递信息。重排后的移动端版本实际上是另一个演示文稿——且效果更差。PowerPoint和Google Slides在竖屏移动端也表现不佳,受众已经接受这种格式的局限性。
Print / PDF Export
打印/PDF导出
Users should be able to hit → "Save as PDF" and get a clean one-slide-per-page PDF. The screen deck stacks slides absolutely and hides inactive ones via , so a naive print captures only the active slide. The fix: a block that un-stacks every slide, kills animations, hides nav/overlays, and forces landscape page size.
Cmd+Popacity: 0@media printAdd this near the bottom of the block:
<style>css
/* Print / PDF export — Cmd+P → Save as PDF gives one slide per page */
@media print {
@page {
size: 1600px 1000px; /* native deck aspect ratio; user can fit-to-page */
margin: 0;
}
html, body {
width: 1600px; height: auto;
overflow: visible !important;
background: #fff;
}
.deck {
width: 1600px; height: auto;
position: static !important;
}
.slide {
position: relative !important;
inset: auto !important;
width: 1600px; height: 1000px;
opacity: 1 !important;
pointer-events: auto !important;
page-break-after: always;
break-after: page;
transform: none !important;
}
/* Kill all animations and transitions so content is in final state */
*, *::before, *::after {
animation: none !important;
animation-duration: 0s !important;
animation-delay: 0s !important;
transition: none !important;
}
.slide .reveal {
opacity: 1 !important;
transform: none !important;
}
/* Hide interactive chrome */
.nav, .rotate-overlay { display: none !important; }
/* Preserve backgrounds and colors in print output */
* {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
}Usage: user opens the deck in a browser, hits (or ), picks "Save as PDF" as the destination, and optionally toggles "Background graphics" on if the print dialog offers it. The default paper size in the dialog doesn't matter much — the rule sets the content frame, and browsers scale to fit. For pixel-perfect output, users can pick "Custom" paper size matching in the dialog.
Cmd+PCtrl+P@page1600 × 1000Gotchas:
- Gradients and some photo-panel rasterization can look slightly different from screen (Chrome and Safari differ). If the deck must look identical in print, keep backgrounds solid.
- Slides taller than 1000px will clip. Don't let content overflow the slide bounds on screen and it won't clip in print.
- Firefox honors less reliably than Chrome/Safari. For critical Firefox users, swap to
@page size: <pixels>(same ratio, physical units).size: 10in 6.25in
用户应能通过按下 → “另存为PDF”来获取每页一张幻灯片的清晰PDF。屏幕版演示文稿将幻灯片绝对定位堆叠,并通过隐藏非活动幻灯片,因此直接打印只会捕获当前活动幻灯片。解决方法:添加区块,取消幻灯片堆叠、禁用动画、隐藏导航/提示层,并强制页面为横屏。
Cmd+Popacity: 0@media print将以下代码添加到标签底部:
<style>css
/* Print / PDF export — Cmd+P → Save as PDF gives one slide per page */
@media print {
@page {
size: 1600px 1000px; /* native deck aspect ratio; user can fit-to-page */
margin: 0;
}
html, body {
width: 1600px; height: auto;
overflow: visible !important;
background: #fff;
}
.deck {
width: 1600px; height: auto;
position: static !important;
}
.slide {
position: relative !important;
inset: auto !important;
width: 1600px; height: 1000px;
opacity: 1 !important;
pointer-events: auto !important;
page-break-after: always;
break-after: page;
transform: none !important;
}
/* Kill all animations and transitions so content is in final state */
*, *::before, *::after {
animation: none !important;
animation-duration: 0s !important;
animation-delay: 0s !important;
transition: none !important;
}
.slide .reveal {
opacity: 1 !important;
transform: none !important;
}
/* Hide interactive chrome */
.nav, .rotate-overlay { display: none !important; }
/* Preserve backgrounds and colors in print output */
* {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
}**使用方法:**用户在浏览器中打开演示文稿,按下(或),选择“另存为PDF”作为目标,若打印对话框提供“背景图形”选项,可选择开启。对话框中的默认纸张大小影响不大——规则已设置内容框架,浏览器会自动缩放以适配。如需像素级完美输出,用户可在对话框中选择“自定义”纸张大小,设置为。
Cmd+PCtrl+P@page1600 × 1000注意事项:
- 渐变和部分图片面板的光栅化效果可能与屏幕显示略有不同(Chrome和Safari表现不同)。若演示文稿打印时必须与屏幕显示完全一致,请使用纯色背景。
- 高度超过1000px的幻灯片会被裁剪。确保屏幕上的内容不超出幻灯片边界,打印时就不会被裁剪。
- Firefox对的支持不如Chrome/Safari可靠。针对依赖Firefox的重要用户,可改为
@page size: <pixels>(相同比例,使用物理单位)。size: 10in 6.25in
Step 3: Layout Patterns
步骤3:布局模式
Use these as building blocks. Mix and match per slide.
将这些作为构建模块,可根据幻灯片需求混合搭配。
Split Panel (most versatile)
分栏面板(最通用)
Dark panel left (brand primary color), light panel right (white or cream). Diagonal slice or hard edge between them.
html
<div class="slide" id="sN" style="display:flex;">
<div class="left-panel"><!-- headline, statement, quote --></div>
<div class="right-panel"><!-- list, stats, details --></div>
</div>css
.left-panel {
width: 50%; background: var(--primary);
display: flex; flex-direction: column; justify-content: center;
padding: 72px 64px; position: relative; overflow: hidden;
}
.right-panel {
flex: 1;
display: flex; flex-direction: column; justify-content: center;
padding: 72px 56px 72px 64px; background: var(--cream);
}左侧深色面板(品牌主色调),右侧浅色面板(白色或米白色)。面板间可采用斜切或硬边分隔。
html
<div class="slide" id="sN" style="display:flex;">
<div class="left-panel"><!-- headline, statement, quote --></div>
<div class="right-panel"><!-- list, stats, details --></div>
</div>css
.left-panel {
width: 50%; background: var(--primary);
display: flex; flex-direction: column; justify-content: center;
padding: 72px 64px; position: relative; overflow: hidden;
}
.right-panel {
flex: 1;
display: flex; flex-direction: column; justify-content: center;
padding: 72px 56px 72px 64px; background: var(--cream);
}Stat Column (credentials, metrics)
统计列(资质、指标)
Right-side column of large serif numbers/words with small uppercase labels. Works over photo backgrounds.
html
<div class="stats-col">
<div class="stat">
<div class="stat-n">13+</div>
<div class="stat-l">Years at institution</div>
</div>
<!-- divider between stats: border-top: 1px solid rgba(255,255,255,.09) -->
</div>右侧大字号衬线数字/文字搭配小字号大写标签。适用于图片背景之上。
html
<div class="stats-col">
<div class="stat">
<div class="stat-n">13+</div>
<div class="stat-l">Years at institution</div>
</div>
<!-- divider between stats: border-top: 1px solid rgba(255,255,255,.09) -->
</div>Value Cards (principles, pillars)
价值卡片(原则、支柱)
Left colored bar accent, title + description. One bar color per value for visual differentiation.
html
<div class="value-card">
<div class="value-bar" style="background:#2e7d32"></div>
<div>
<div class="value-title">Principle Name</div>
<div class="value-text">Description of the principle.</div>
</div>
</div>左侧彩色强调条,搭配标题+描述。每个价值项使用不同的条色以实现视觉区分。
html
<div class="value-card">
<div class="value-bar" style="background:#2e7d32"></div>
<div>
<div class="value-title">Principle Name</div>
<div class="value-text">Description of the principle.</div>
</div>
</div>Approach Rows (process, steps)
流程行(流程、步骤)
Large ghost number, vertical rule, then body content. Good for 3-step processes.
html
<div class="approach">
<div class="approach-num">01</div>
<div class="approach-body">
<div class="approach-title">Step Title</div>
<div class="approach-text">Detail text.</div>
<div class="approach-tags"><span>Tag 1</span><span>Tag 2</span></div>
</div>
</div>大号空心数字、垂直分隔线,然后是正文内容。适用于三步流程。
html
<div class="approach">
<div class="approach-num">01</div>
<div class="approach-body">
<div class="approach-title">Step Title</div>
<div class="approach-text">Detail text.</div>
<div class="approach-tags"><span>Tag 1</span><span>Tag 2</span></div>
</div>
</div>Tool/Status Cards
工具/状态卡片
Name + status badge + description. Status badge variants: live (green), in-progress (amber), planned (neutral).
html
<div class="tool-card">
<div class="tool-body">
<div class="tool-name">Tool Name</div>
<span class="status status-live">Live</span>
<div class="tool-desc">What it does and who it's for.</div>
</div>
</div>名称+状态徽章+描述。状态徽章变体:已上线(绿色)、进行中(琥珀色)、计划中(中性色)。
html
<div class="tool-card">
<div class="tool-body">
<div class="tool-name">Tool Name</div>
<span class="status status-live">Live</span>
<div class="tool-desc">What it does and who it's for.</div>
</div>
</div>Photo Background Panel
图片背景面板
Embed image as base64, apply gradient overlay so text stays readable.
html
<div class="photo-panel" style="
position:absolute; top:0; right:0; bottom:0; width:42%;
background: url('data:image/png;base64,...') center/cover no-repeat;
z-index:1;
">
<div style="position:absolute;inset:0;
background:linear-gradient(to right, var(--primary) 0%, rgba(0,0,0,.55) 40%, rgba(0,0,0,.1) 100%);
"></div>
</div>To embed an image as base64:
python
import base64
data = 'data:image/png;base64,' + base64.b64encode(open('image.png','rb').read()).decode()将图片以base64格式嵌入,添加渐变覆盖层以确保文本可读性。
html
<div class="photo-panel" style="
position:absolute; top:0; right:0; bottom:0; width:42%;
background: url('data:image/png;base64,...') center/cover no-repeat;
z-index:1;
">
<div style="position:absolute;inset:0;
background:linear-gradient(to right, var(--primary) 0%, rgba(0,0,0,.55) 40%, rgba(0,0,0,.1) 100%);
"></div>
</div>将图片嵌入为base64格式的方法:
python
import base64
data = 'data:image/png;base64,' + base64.b64encode(open('image.png','rb').read()).decode()Step 4: Content Slide Sequence
步骤4:内容幻灯片序列
A strong 5-slide deck structure for an institutional introduction:
| Slide | Purpose | Layout |
|---|---|---|
| 1 | Who you are + elevator pitch | Full bleed dark + photo + stat column |
| 2 | Values / principles | Split panel + value cards |
| 3 | Approach / framework | Light background + approach rows |
| 4 | Tools / enablement | Dark background + two columns |
| 5 | Close + discussion opener | Split panel + photo background |
Adapt freely. The key is each slide has ONE job and ONE dominant visual idea.
适用于机构介绍的5页优质演示文稿结构:
| 幻灯片 | 用途 | 布局 |
|---|---|---|
| 1 | 自我介绍 + 电梯演讲 | 全幅深色背景+图片+统计列 |
| 2 | 价值观/原则 | 分栏面板+价值卡片 |
| 3 | 方法/框架 | 浅色背景+流程行 |
| 4 | 工具/赋能 | 深色背景+两列布局 |
| 5 | 收尾+讨论开场 | 分栏面板+图片背景 |
可自由调整。关键在于每张幻灯片只有一个核心任务和一个主导视觉元素。
Step 5: Typography Rules
步骤5:排版规则
css
/* Display heading — large, light weight, letter-spaced */
.display {
font-family: var(--serif);
font-size: clamp(52px, 7vw, 88px);
font-weight: 300; line-height: .95;
letter-spacing: -.01em;
}
/* Section heading */
.heading {
font-family: var(--serif);
font-size: clamp(32px, 4vw, 50px);
font-weight: 400; line-height: 1.1;
}
/* Eyebrow label */
.label {
font-family: var(--sans);
font-size: 10px; font-weight: 600;
letter-spacing: .2em; text-transform: uppercase;
}
/* Body */
.body {
font-family: var(--sans);
font-size: 13px; font-weight: 300; line-height: 1.65;
}
/* Use clamp() for all font sizes to scale with viewport */css
/* Display heading — large, light weight, letter-spaced */
.display {
font-family: var(--serif);
font-size: clamp(52px, 7vw, 88px);
font-weight: 300; line-height: .95;
letter-spacing: -.01em;
}
/* Section heading */
.heading {
font-family: var(--serif);
font-size: clamp(32px, 4vw, 50px);
font-weight: 400; line-height: 1.1;
}
/* Eyebrow label */
.label {
font-family: var(--sans);
font-size: 10px; font-weight: 600;
letter-spacing: .2em; text-transform: uppercase;
}
/* Body */
.body {
font-family: var(--sans);
font-size: 13px; font-weight: 300; line-height: 1.65;
}
/* Use clamp() for all font sizes to scale with viewport */Critical Pitfalls
关键注意事项
NEVER use blanket string/regex replacement on files containing JavaScript
切勿对包含JavaScript的文件使用全局字符串/正则替换
This will corrupt arithmetic operators. Example of what goes wrong:
python
undefined这会破坏算术运算符。错误示例:
python
undefinedDANGEROUS — corrupts JS
DANGEROUS — corrupts JS
content = content.replace(' - ', ', ')
content = content.replace(' - ', ', ')
cur - 1 becomes cur, 1 (prev button breaks)
cur - 1 becomes cur, 1 (prev button breaks)
clientX - tx becomes clientX, tx (swipe breaks)
clientX - tx becomes clientX, tx (swipe breaks)
**Safe approach:** Use targeted `str_replace` with unique, specific strings. Always view the JS section after any automated replacement to verify arithmetic operators are intact.
**安全方法:**使用针对性的`str_replace`,替换唯一、特定的字符串。在任何自动替换后,务必查看JavaScript部分,确认算术运算符完好无损。Always declare const slides
before using it
const slides务必在使用前声明const slides
const slidesIf inserting a new function before the navigation block, check that is still present and comes before .
const slides = document.querySelectorAll('.slide')slides.forEach(...)如果在导航区块前插入新函数,请检查是否仍然存在,且位于之前。
const slides = document.querySelectorAll('.slide')slides.forEach(...)str_replace
requires unique strings
str_replacestr_replace
需要唯一字符串
str_replaceBefore using str_replace, grep for the target string to confirm it appears exactly once. If it appears multiple times, add more context lines to make it unique.
使用str_replace前,先搜索目标字符串,确认它只出现一次。如果出现多次,添加更多上下文内容使其唯一。
Base64 images make files large but self-contained
Base64图片会增大文件体积但保证独立性
A single high-res PNG can add 150-200KB to the file. This is fine for a presentation deck. Use sizing and positioning. Always add a gradient overlay div for text legibility.
covercenter center一张高分辨率PNG图片会使文件增大150-200KB。这对于演示文稿来说是可接受的。使用尺寸和定位。务必添加渐变覆盖层以确保文本可读性。
covercenter centerEmoji in content
内容中的表情符号
Avoid leading emoji in list items and card titles — they read as AI-generated and date quickly. If the user asks to remove emoji, use Python unicode replacement rather than sed:
python
import re
content = re.sub(r'[\U00002600-\U0001FAFF]+', '', content)
content = content.replace('\ufe0f', '') # variation selector避免在列表项和卡片标题前使用表情符号——它们会显得是AI生成的,且很快过时。如果用户要求移除表情符号,请使用Python unicode替换而非sed:
python
import re
content = re.sub(r'[\U00002600-\U0001FAFF]+', '', content)
content = content.replace('\ufe0f', '') # variation selectorIterative Editing Workflow
迭代编辑工作流程
When making changes after initial build:
- View the relevant section first — tool with line range
view - Use for surgical edits — never rewrite the whole file for small changes
str_replace - Verify JS after any automated text processing — check lines with ,
cur,dx,+operators- - Present fresh file after changes — user may be viewing cached version
When the user says things like "slide 3: change X" — make ONLY that change. Resist the urge to improve adjacent content.
初始构建后进行修改时:
- 先查看相关部分——使用工具查看指定行范围
view - 使用进行精准编辑——切勿因小改动重写整个文件
str_replace - 任何自动文本处理后验证JavaScript——检查包含、
cur、dx、+运算符的行- - 修改后展示新文件——用户可能正在查看缓存版本
当用户说“第3页:修改X”时——只做该修改。不要试图改进相邻内容。
Codex / API Usage Notes
Codex / API使用说明
This skill is compatible with Claude API (Codex) usage. Key considerations:
- The complete JS block (navigation + fullscreen) should be treated as a unit — include it verbatim and do not modify arithmetic
- When generating via API, stream the file to disk and open in browser for preview
- The base64 embedding approach works identically in API context
- CSS variables (,
--primary, etc.) make theming programmatic — pass brand colors as parameters and substitute into the template--accent
此技能兼容Claude API(Codex)使用。关键注意事项:
- 完整的JavaScript区块(导航+全屏)应作为一个单元处理——完整包含,切勿修改算术部分
- 通过API生成时,将文件流式传输到磁盘并在浏览器中打开预览
- Base64嵌入方法在API环境中同样有效
- CSS变量(、
--primary等)使主题设置可编程——将品牌颜色作为参数传入并替换到模板中--accent
PPTX Export (PowerPoint)
PPTX导出(PowerPoint)
When the user explicitly asks for a file, generate a Python script that uses to build the deck programmatically. The user runs it with:
.pptxpython-pptxbash
uv run --with python-pptx <script>.pyNo system-wide installs required — handles the dependency inline.
uv当用户明确要求文件时,生成一个使用以编程方式构建演示文稿的Python脚本。用户通过以下命令运行:
.pptxpython-pptxbash
uv run --with python-pptx <script>.py无需全局安装依赖——会自动处理依赖。
uvWhen to use PPTX instead of HTML
何时使用PPTX而非HTML
- User explicitly asks for or "PowerPoint"
.pptx - User needs recipients to edit slides in Office
- User says "I need to share this with someone who uses PowerPoint"
- 用户明确要求或“PowerPoint”
.pptx - 用户需要收件人在Office中编辑幻灯片
- 用户说“我需要和使用PowerPoint的人分享这个”
How it works
工作原理
Same content extraction and brand logic as HTML — extract colors, fonts, layout intent from the prompt. Then emit a self-contained script instead of an file.
.py.html与HTML版本使用相同的内容提取和品牌逻辑——从提示中提取颜色、字体、布局意图。然后生成独立的脚本而非文件。
.py.htmlPPTX Generation Pattern
PPTX生成模式
python
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.enum.shapes import MSO_SHAPEpython
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.enum.shapes import MSO_SHAPEBrand colors mapped from CSS variables
Brand colors mapped from CSS variables
PRIMARY = RGBColor(0x1A, 0x1A, 0x2E)
ACCENT = RGBColor(0xE9, 0x45, 0x60)
prs = Presentation()
prs.slide_width = Inches(16)
prs.slide_height = Inches(10)
blank_layout = prs.slide_layouts[6] # blank — full control
def set_slide_bg(slide, color):
fill = slide.background.fill
fill.solid()
fill.fore_color.rgb = color
def add_text_box(slide, left, top, width, height, text,
font_size=18, bold=False, italic=False,
color=RGBColor(0xFF,0xFF,0xFF), font_name="Arial"):
txBox = slide.shapes.add_textbox(left, top, width, height)
tf = txBox.text_frame
tf.word_wrap = True
p = tf.paragraphs[0]
p.text = text
p.font.size = Pt(font_size)
p.font.bold = bold
p.font.italic = italic
p.font.color.rgb = color
p.font.name = font_name
PRIMARY = RGBColor(0x1A, 0x1A, 0x2E)
ACCENT = RGBColor(0xE9, 0x45, 0x60)
prs = Presentation()
prs.slide_width = Inches(16)
prs.slide_height = Inches(10)
blank_layout = prs.slide_layouts[6] # blank — full control
def set_slide_bg(slide, color):
fill = slide.background.fill
fill.solid()
fill.fore_color.rgb = color
def add_text_box(slide, left, top, width, height, text,
font_size=18, bold=False, italic=False,
color=RGBColor(0xFF,0xFF,0xFF), font_name="Arial"):
txBox = slide.shapes.add_textbox(left, top, width, height)
tf = txBox.text_frame
tf.word_wrap = True
p = tf.paragraphs[0]
p.text = text
p.font.size = Pt(font_size)
p.font.bold = bold
p.font.italic = italic
p.font.color.rgb = color
p.font.name = font_name
Build slides...
Build slides...
slide1 = prs.slides.add_slide(blank_layout)
set_slide_bg(slide1, PRIMARY)
add_text_box(slide1, Inches(0.8), Inches(3), Inches(10), Inches(3),
"Title Here", font_size=72, font_name="Georgia")
prs.save("output.pptx")
print("Saved: output.pptx")
undefinedslide1 = prs.slides.add_slide(blank_layout)
set_slide_bg(slide1, PRIMARY)
add_text_box(slide1, Inches(0.8), Inches(3), Inches(10), Inches(3),
"Title Here", font_size=72, font_name="Georgia")
prs.save("output.pptx")
print("Saved: output.pptx")
undefinedMapping HTML layouts to PPTX
HTML布局映射到PPTX
| HTML Pattern | PPTX Equivalent |
|---|---|
| Split panel (dark left / light right) | Rectangle shape as left bg + text boxes on each side |
| Stat columns | Large-font text boxes with small label text boxes below |
| Value cards with colored bars | Thin rectangle shapes as accent bars + adjacent text boxes |
| Approach rows (numbered steps) | Large number text box + title/body text boxes offset right |
| Full-bleed background | |
| HTML模式 | PPTX等效实现 |
|---|---|
| 分栏面板(左深/右浅) | 矩形形状作为左侧背景 + 两侧文本框 |
| 统计列 | 大字号文本框 + 下方小字号标签文本框 |
| 带彩色条的价值卡片 | 细矩形形状作为强调条 + 相邻文本框 |
| 流程行(编号步骤) | 大号数字文本框 + 右侧偏移的标题/正文文本框 |
| 全幅背景 | 使用 |
Important caveats
重要提示
Warn the user: PPTX output is an approximation of the HTML deck, not a pixel-perfect replica. python-pptx does not support CSS, gradients, scaling, or web fonts. The generated deck will:
clamp()- Use system fonts (Arial, Georgia) as fallbacks for Google Fonts
- Approximate gradients with solid colors
- Skip animations and reveals (static slides)
- Require manual touch-up for precise alignment
Always tell the user: "The .pptx is a solid starting point but may need minor adjustments in PowerPoint — check alignment and font rendering before presenting."
**告知用户:**PPTX输出是HTML演示文稿的近似版本,而非像素级完美复刻。python-pptx不支持CSS、渐变、缩放或网页字体。生成的演示文稿将:
clamp()- 使用系统字体(Arial、Georgia)作为Google Fonts的替代
- 用纯色近似渐变效果
- 跳过动画和逐步展示(静态幻灯片)
- 需要手动调整以实现精准对齐
务必告知用户:“此.pptx文件是良好的起点,但在PowerPoint中可能需要微调——展示前请检查对齐和字体渲染效果。”
Font mapping
字体映射
| HTML (Google Font) | PPTX fallback |
|---|---|
| Cormorant Garamond | Georgia |
| DM Sans | Arial |
| Playfair Display | Georgia |
| Inter | Arial |
| Any serif | Georgia or Times New Roman |
| Any sans-serif | Arial or Calibri |
| HTML(Google Font) | PPTX替代字体 |
|---|---|
| Cormorant Garamond | Georgia |
| DM Sans | Arial |
| Playfair Display | Georgia |
| Inter | Arial |
| 任意衬线字体 | Georgia或Times New Roman |
| 任意无衬线字体 | Arial或Calibri |
Quick Reference: Palette Slots
快速参考:调色板插槽
Every deck fills these slots. Populate from the brand guide, then theme programmatically via CSS variables.
--primary Dominant backgrounds and dark panels
--accent Rules, spine bars, CTAs, status dots, hover states
--cream Light slide backgrounds (warmer than pure white)
--stone Dividers, subtle borders (one tint above cream)
--ink Body text on light backgrounds (softer than black)
--muted Labels, metadata, secondary text
--go Positive / live / green status
--warn Review / caution / amber status
--alt Fifth accent for value/pillar differentiationFill with real hex codes at the top of and reference everywhere via .
<style>var(--name)每个演示文稿都会填充这些插槽。从品牌指南中获取内容,然后通过CSS变量进行主题设置。
--primary 主要背景和深色面板
--accent 分隔线、侧边栏、号召性按钮、状态点、悬停状态
--cream 浅色幻灯片背景(比纯白更温暖)
--stone 分隔线、细微边框(比cream深一个色调)
--ink 浅色背景上的正文文本(比黑色柔和)
--muted 标签、元数据、次要文本
--go 积极/已上线/绿色状态
--warn 审核/警告/琥珀色状态
--alt 用于价值/支柱区分的第五种强调色在标签顶部填入真实的十六进制代码,并通过在各处引用。
<style>var(--name)