feat: add bundled OpenCode skills and refine module flow
This commit is contained in:
132
.opencode/skills/frontend-slides/references/html-template.md
Normal file
132
.opencode/skills/frontend-slides/references/html-template.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# 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。
|
||||
Reference in New Issue
Block a user