feat: add bundled OpenCode skills and refine module flow
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

This commit is contained in:
inman
2026-08-14 18:15:55 +08:00
parent 88f9ee8708
commit c809002180
31 changed files with 1153 additions and 241 deletions

View File

@@ -0,0 +1,47 @@
# 动画参考
动画服务于讲解顺序,不应成为内容的前提。每页最多使用一到两种运动方式,并保证减少动态效果时仍然完整可读。
## 情绪与选择
| 情绪 | 推荐 | 避免 |
| --- | --- | --- |
| 活泼友好 | 轻微弹性、错峰出现、短距离上移 | 连续旋转、强闪烁 |
| 科学探索 | 线条绘制、步骤依次出现、数字轻微递进 | 粒子铺满屏幕 |
| 故事叙述 | 淡入、横向推进、图片轻微放大 | 每个元素都飞入 |
| 清楚正式 | 200350ms 淡入或位移 | 复杂转场、视差 |
| 安静专注 | 500800ms 淡入、很小的位移 | 夸张弹跳 |
## 通用 CSS
```css
.reveal {
opacity: 0;
transform: translateY(28px);
transition: opacity .55s ease, transform .55s ease;
}
.slide.visible .reveal {
opacity: 1;
transform: translateY(0);
}
.slide.visible .reveal:nth-child(2) { transition-delay: .08s; }
.slide.visible .reveal:nth-child(3) { transition-delay: .16s; }
@media (prefers-reduced-motion: reduce) {
.reveal,
.slide.visible .reveal {
opacity: 1;
transform: none;
transition: none;
}
}
```
## 验收重点
- 切页时先切换可见状态,再触发内容动画;
- 不用 `display: none` 作为幻灯片切换机制;
- 不在每页添加同样的装饰动画;
- 检查动画结束后文字仍在原位置,不能出现溢出或重叠。

View 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。

View File

@@ -0,0 +1,48 @@
# 项目演示风格参考
先根据项目主题和观众选择一个清晰的视觉方向,再扩展到完整演示。默认优先浅色、明亮和高对比度方案;不要使用大量渐变、紫色科技卡片或无法解释的装饰。
## 适合孩子项目的方向
### 纸张与贴纸
- 感受:像项目手册,亲切、清楚、有一点手作感。
- 颜色:暖白底、墨绿文字、珊瑚和柠檬黄点缀。
- 结构:大标题、纸张边缘、少量标签和手绘感 CSS 线条。
- 适合:校园项目、生活观察、故事类作品。
### 彩色积木
- 感受:活泼、简单、适合快速讲解。
- 颜色:奶油白底,天蓝、草绿、橙色和粉色作为区块色。
- 结构:大色块、圆角但不过度、每页一个核心卡片或图形。
- 适合:小游戏、机器人、互动小程序。
### 小小实验室
- 感受:清楚、好奇、像科学展板。
- 颜色:浅灰白底、深蓝文字、青绿色和橙色高亮。
- 结构:步骤编号、观察结果、前后对比和简短结论。
- 适合实验、数据观察、AI 探索、制作过程。
### 故事地图
- 感受:有起点、过程和结果,帮助孩子按故事讲项目。
- 颜色:浅米色底、森林绿、砖红和天蓝。
- 结构:时间线、路线、角色或场景卡片;每页最多一个复杂图。
- 适合:问题解决、公益主题、创意产品。
### 清爽展板
- 感受:更正式但不严肃,适合学校汇报。
- 颜色:白底、深灰文字、单一品牌色和浅色辅助线。
- 结构:标题、证据、截图、结论,留出充足空白。
- 适合:阅读型报告、比赛答辩、作品集。
## 选择规则
- 主题和已有素材优先于个人偏好;有真实截图时围绕截图建立布局。
- 三个草图必须在字体、构图和色彩关系上真正不同,不能只是换背景色。
- 文字多时增加页数,不要压缩字号;标题要能在教室屏幕上读清楚。
- 每页最多 3 个重点;过程类项目优先使用“问题 → 尝试 → 结果 → 下一步”。
- 动画只用来强调顺序和变化,不能依赖动画才能读懂内容。

View File

@@ -0,0 +1,123 @@
/*
* Fixed 16:9 stage. Include this whole file in every generated deck.
* Slides are authored at 1920x1080 and scaled as one unit.
*/
html,
body {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
background: var(--stage-bg, #f4f7f5);
}
.deck-viewport {
position: fixed;
inset: 0;
overflow: hidden;
background: var(--stage-bg, #f4f7f5);
}
.deck-stage {
position: absolute;
left: 0;
top: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
transform-origin: 0 0;
background: var(--slide-bg, #fffdf7);
}
.slide {
position: absolute;
inset: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
display: block;
visibility: hidden;
opacity: 0;
pointer-events: none;
background: var(--slide-bg, #fffdf7);
}
.slide.active,
.slide.visible {
visibility: visible;
opacity: 1;
pointer-events: auto;
z-index: 1;
}
img,
video,
canvas,
svg {
max-width: 100%;
max-height: 100%;
}
.deck-controls {
position: fixed;
left: 50%;
bottom: 22px;
transform: translateX(-50%);
z-index: 1000;
}
@media print {
html,
body {
width: 1920px;
height: auto;
overflow: visible;
background: #fff;
}
.deck-viewport {
position: static;
overflow: visible;
background: #fff;
}
.deck-stage {
position: static;
width: auto;
height: auto;
transform: none !important;
background: none;
}
.slide {
position: relative;
display: block !important;
visibility: visible !important;
opacity: 1 !important;
pointer-events: auto !important;
width: 1920px;
height: 1080px;
break-after: page;
page-break-after: always;
}
.slide:last-child {
break-after: auto;
page-break-after: auto;
}
.deck-controls {
display: none !important;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}