Files
makelore/.opencode/skills/frontend-slides/references/html-template.md
inman c809002180
Some checks failed
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled
feat: add bundled OpenCode skills and refine module flow
2026-08-14 18:15:55 +08:00

133 lines
4.8 KiB
Markdown

# HTML 幻灯片结构参考
每份演示都是一个可独立打开的 HTML 文件。把 [viewport-base.css](viewport-base.css) 的完整内容放在 `<style>` 中,再加入项目自己的变量和组件样式。
## 最小结构
```html
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>项目演示</title>
<style>
:root {
--stage-bg: #eef3ef;
--slide-bg: #fffdf7;
--ink: #17332a;
--muted: #5c6f66;
--accent: #f08a63;
--display-font: "Trebuchet MS", "PingFang SC", sans-serif;
--body-font: "Avenir Next", "PingFang SC", sans-serif;
}
* { box-sizing: border-box; }
/* 在此处粘贴 viewport-base.css 的完整内容。 */
.slide-inner {
width: 100%;
height: 100%;
padding: 92px;
color: var(--ink);
font-family: var(--body-font);
}
.reveal {
opacity: 0;
transform: translateY(28px);
transition: opacity .55s ease, transform .55s ease;
}
.slide.visible .reveal {
opacity: 1;
transform: translateY(0);
}
.reveal:nth-child(2) { transition-delay: .08s; }
.reveal:nth-child(3) { transition-delay: .16s; }
</style>
</head>
<body>
<div class="deck-viewport" aria-label="项目演示">
<main class="deck-stage" id="deckStage">
<section class="slide title-slide active visible" aria-label="第 1 页">
<div class="slide-inner">
<h1 class="reveal">项目标题</h1>
<p class="reveal">一句话说明项目解决了什么问题</p>
</div>
</section>
<!-- 其余页使用同样的 .slide 结构。 -->
</main>
</div>
<div class="deck-controls" aria-label="演示控制">
<span id="pageCount">1 / 1</span>
</div>
<script>
class SlidePresentation {
constructor() {
this.slides = [...document.querySelectorAll('.slide')];
this.stage = document.querySelector('#deckStage');
this.pageCount = document.querySelector('#pageCount');
this.index = 0;
this.startX = null;
this.scaleStage();
window.addEventListener('resize', () => this.scaleStage());
window.addEventListener('keydown', (event) => this.onKey(event));
window.addEventListener('wheel', (event) => {
if (Math.abs(event.deltaY) > 12) this.show(this.index + Math.sign(event.deltaY));
}, { passive: true });
window.addEventListener('touchstart', (event) => {
this.startX = event.touches[0]?.clientX ?? null;
}, { passive: true });
window.addEventListener('touchend', (event) => {
if (this.startX === null) return;
const endX = event.changedTouches[0]?.clientX ?? this.startX;
if (Math.abs(endX - this.startX) > 40) this.show(this.index + (endX < this.startX ? 1 : -1));
this.startX = null;
}, { passive: true });
this.show(0);
}
scaleStage() {
const factor = Math.min(window.innerWidth / 1920, window.innerHeight / 1080);
const x = (window.innerWidth - 1920 * factor) / 2;
const y = (window.innerHeight - 1080 * factor) / 2;
this.stage.style.transform = `translate(${x}px, ${y}px) scale(${factor})`;
}
onKey(event) {
if (['ArrowRight', 'ArrowDown', 'PageDown', ' '].includes(event.key)) {
event.preventDefault(); this.show(this.index + 1);
}
if (['ArrowLeft', 'ArrowUp', 'PageUp'].includes(event.key)) {
event.preventDefault(); this.show(this.index - 1);
}
}
show(index) {
this.index = Math.max(0, Math.min(index, this.slides.length - 1));
this.slides.forEach((slide, slideIndex) => {
const active = slideIndex === this.index;
slide.classList.toggle('active', active);
slide.classList.toggle('visible', active);
slide.setAttribute('aria-hidden', String(!active));
});
if (this.pageCount) this.pageCount.textContent = `${this.index + 1} / ${this.slides.length}`;
}
}
new SlidePresentation();
</script>
</body>
</html>
```
## 内容和可访问性
- 使用一个明确的 `<h1>`,后续页面使用有顺序的 `<h2>`;图片提供简短 `alt` 文本。
- 不把重要内容只放在动画、颜色或悬停状态里;对比度要足够。
- 为每个 `.slide``aria-label`,并在切换时同步 `aria-hidden`
- 不在幻灯片内放调试按钮、文件路径、模板名称、内部提示或生成过程文字。
- 通过相对路径引用 `assets/`,不要把用户机器路径写入 HTML。