diff --git a/docs/assets/site.css b/docs/assets/site.css
index 6d62f92..1e95bec 100644
--- a/docs/assets/site.css
+++ b/docs/assets/site.css
@@ -62,8 +62,17 @@
--ph-line: rgba(15, 18, 20, 0.1);
--ph-in: #e9ecef;
--ph-out: #2c8a52;
- --ph-kb: #d9dde3;
+
+ /* 键盘复刻色:取自 App 源码 ColorLibrary(brandAccent / aiTeal / recordRed)与原生键面色 */
+ --kb-accent: #3aa05a;
+ --kb-teal: #2bafa4;
+ --kb-red: #ff3b30;
+ --ph-kb: #efeeec;
--ph-key: #ffffff;
+ --ph-key-ink: #1c1c1e;
+ --ph-key-off: #d6d6d6;
+ --ph-track: rgba(0, 0, 0, 0.1);
+ --ph-glass: rgba(255, 255, 255, 0.66);
}
html[data-theme="dark"] {
@@ -88,8 +97,12 @@ html[data-theme="dark"] {
--ph-line: rgba(255, 255, 255, 0.1);
--ph-in: #23262b;
--ph-out: #2f9a5b;
- --ph-kb: #1a1d21;
- --ph-key: #34383e;
+ --ph-kb: #232324;
+ --ph-key: #525252;
+ --ph-key-ink: #ffffff;
+ --ph-key-off: #3b3b3b;
+ --ph-track: #1a1a1a;
+ --ph-glass: rgba(255, 255, 255, 0.09);
}
/* --------------------------------------------------------------------------
@@ -748,7 +761,7 @@ html[data-lang="zh"] h3 {
max-width: 92%;
align-items: center;
gap: 14px;
- padding: 9px 22px 9px 9px;
+ padding: 9px 22px 9px 10px;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 999px;
background: rgba(14, 16, 19, 0.74);
@@ -759,69 +772,115 @@ html[data-lang="zh"] h3 {
translate: -50% 0;
}
-.capsule-orb {
+/*
+ * 麦克风胶囊:与键盘真实组件同色同形(156×60pt 的胶囊按比例缩小)。
+ * 待机 = 品牌绿 + 麦克风;录音 = 红色 + 7 条声波;长按问 AI = 青色 + 声波;
+ * 处理中 = 半透明红 + 转圈;插入成功 = 品牌绿 + 对勾。状态类由 site.js 切换。
+ */
+.capsule-mic {
position: relative;
display: grid;
- width: 42px;
- height: 42px;
+ width: 78px;
+ height: 32px;
flex: none;
place-items: center;
- border-radius: 50%;
- background: radial-gradient(circle at 35% 30%, #a8e6c1, var(--stage-accent) 55%, #3c9c63);
- color: #0c0e12;
+ border-radius: 999px;
+ background: var(--kb-accent);
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.28), 0 6px 18px -6px color-mix(in srgb, var(--kb-accent) 80%, transparent);
+ color: #fff;
+ transition: background-color 0.35s ease, box-shadow 0.35s ease, transform 0.5s var(--ease-spring);
}
-.capsule-orb .i {
- width: 19px;
- height: 19px;
- stroke-width: 2;
+.capsule-mic > * {
+ grid-area: 1 / 1;
+ transition: opacity 0.25s ease, transform 0.45s var(--ease-spring);
}
-/* 录音态:橙绿色涟漪从按钮向外扩散 */
-.capsule-orb::before,
-.capsule-orb::after {
- position: absolute;
- inset: 0;
- border: 1.5px solid var(--stage-accent);
- border-radius: 50%;
- content: "";
+.cap-mic {
+ width: 17px;
+ height: 17px;
+ stroke-width: 2.2;
+}
+
+.cap-check {
+ width: 18px;
+ height: 18px;
+ stroke-width: 3;
+}
+
+.capsule-wave,
+.capsule-spin,
+.cap-check {
opacity: 0;
+ transform: scale(0.6);
}
-.voice-capsule.is-listening .capsule-orb::before,
-.voice-capsule.is-listening .capsule-orb::after {
- animation: orb-ripple 1.8s cubic-bezier(0.2, 0.6, 0.3, 1) infinite;
+.voice-capsule.is-rec .capsule-mic {
+ background: var(--kb-red);
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.28), 0 6px 22px -6px color-mix(in srgb, var(--kb-red) 85%, transparent);
}
-.voice-capsule.is-listening .capsule-orb::after {
- animation-delay: 0.9s;
+.voice-capsule.is-ai .capsule-mic {
+ background: var(--kb-teal);
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.28), 0 6px 22px -6px color-mix(in srgb, var(--kb-teal) 85%, transparent);
+ transform: scale(1.04);
}
-@keyframes orb-ripple {
- 0% { opacity: 0.9; transform: scale(1); }
- 100% { opacity: 0; transform: scale(1.9); }
+.voice-capsule.is-busy .capsule-mic {
+ background: color-mix(in srgb, var(--kb-red) 72%, #2a2d31);
+}
+
+.voice-capsule:is(.is-rec, .is-ai, .is-busy, .is-done) .cap-mic {
+ opacity: 0;
+ transform: scale(0.6);
+}
+
+.voice-capsule:is(.is-rec, .is-ai) .capsule-wave,
+.voice-capsule.is-busy .capsule-spin,
+.voice-capsule.is-done .cap-check {
+ opacity: 1;
+ transform: none;
}
/* 声波条:site.js 每帧写 scaleY,只动 transform,不触发布局 */
.capsule-wave,
-.kb-wave {
+.mic-wave {
display: flex;
align-items: center;
}
.capsule-wave {
- height: 28px;
- gap: 3px;
+ height: 16px;
+ gap: 2.5px;
}
.capsule-wave i {
- width: 3px;
+ width: 2.5px;
height: 100%;
- border-radius: 3px;
- background: linear-gradient(180deg, #c9f3da, var(--stage-accent));
+ border-radius: 2px;
+ background: #ffc7c7;
transform: scaleY(0.14);
}
+.voice-capsule.is-ai .capsule-wave i {
+ background: #fff;
+}
+
+/* 系统风格的转圈:处理中状态 */
+.capsule-spin,
+.mic-spin {
+ width: 15px;
+ height: 15px;
+ border: 2px solid rgba(255, 255, 255, 0.35);
+ border-top-color: #fff;
+ border-radius: 50%;
+ animation: spin 0.8s linear infinite;
+}
+
+@keyframes spin {
+ to { rotate: 1turn; }
+}
+
.capsule-body {
display: flex;
min-width: min(400px, 52vw);
@@ -844,9 +903,10 @@ html[data-lang="zh"] h3 {
transition: background 0.3s ease, color 0.3s ease;
}
-.capsule-tag.is-agent {
- background: var(--stage-accent);
- color: #0c0e12;
+/* 标签颜色跟随胶囊状态:问 AI 用青色,与麦克风胶囊呼应 */
+.voice-capsule:is(.is-ai, .is-ai-busy) .capsule-tag {
+ background: color-mix(in srgb, var(--kb-teal) 22%, transparent);
+ color: #7fded4;
}
.capsule-line {
@@ -1219,7 +1279,7 @@ h3 {
/* --------------------------------------------------------------------------
* 9. 滚动叙事(三幕剧)
- * 状态类 s1…s8 由 site.js 依滚动进度累加到 .story 上;
+ * 消息类 m1/o1/m2/c2/o2/m3/c3/ev 与键盘 data-* 状态由 site.js 依滚动进度写到 .story 上;
* 只有 .story.is-live(脚本已接管)时才进入「未发生 → 隐藏」的初始态,
* 无 JavaScript 时手机直接展示最终画面。
* -------------------------------------------------------------------------- */
@@ -1351,7 +1411,7 @@ h3 {
transition: opacity 0.6s ease, transform 0.8s var(--ease-out);
}
-.story.s2:not(.s3) .story-device::before {
+.story:is([data-mic="rec"], [data-mic="busy"]) .story-device::before {
opacity: 1;
transform: scale(1.08);
}
@@ -1461,22 +1521,27 @@ h3 {
font-size: 2.7cqw;
}
+/* 底部对齐:新消息出现时把旧消息向上推,和真实聊天一样 */
.chat-body {
display: flex;
+ overflow: hidden;
min-height: 0;
flex: 1;
flex-direction: column;
- gap: 2.6cqw;
- padding: 4cqw 4.4cqw;
+ justify-content: flex-end;
+ padding: 1cqw 4.4cqw 3cqw;
}
.bubble {
position: relative;
+ flex: none;
max-width: 76%;
- padding: 2.3cqw 3.6cqw;
+ margin-top: 2.4cqw;
+ padding: 2.1cqw 3.4cqw;
border-radius: 5cqw;
- font-size: 3.85cqw;
- transition: opacity 0.35s ease, transform 0.7s var(--ease-spring), box-shadow 0.4s ease;
+ font-size: 3.6cqw;
+ transition: opacity 0.35s ease, transform 0.7s var(--ease-spring), box-shadow 0.4s ease,
+ max-height 0.5s var(--ease-out), margin 0.5s var(--ease-out), padding 0.5s var(--ease-out);
}
.bubble.in {
@@ -1494,21 +1559,32 @@ h3 {
transform-origin: 100% 100%;
}
+/* 未出现的气泡不占位(高度收为 0),出现时弹簧放大 */
.story.is-live .bubble {
+ max-height: 0;
+ margin-top: 0;
+ padding-block: 0;
opacity: 0;
transform: translateY(3cqw) scale(0.6);
}
-.story.is-live.s1 .b-ask,
-.story.is-live.s4 .b-sent,
-.story.is-live.s5 .b-addr {
+.story.is-live.m1 .b-m1,
+.story.is-live.o1 .b-o1,
+.story.is-live.m2 .b-m2,
+.story.is-live.o2 .b-o2,
+.story.is-live.m3 .b-m3 {
+ max-height: 40cqw;
+ margin-top: 2.4cqw;
+ padding-block: 2.1cqw;
opacity: 1;
transform: none;
}
-/* 第三幕:复制地址 → 气泡高亮 + 「已复制」提示 */
-.story.s6 .b-addr {
- box-shadow: 0 0 0 0.8cqw color-mix(in srgb, var(--ph-out) 40%, transparent);
+/* 长按复制:气泡描边高亮 + 「已复制」提示 */
+.story.c2 .b-m2,
+.story.c3 .b-m3,
+.skills-demo.is-copied .sd-msg {
+ box-shadow: 0 0 0 0.8cqw color-mix(in srgb, var(--kb-accent) 40%, transparent);
}
.copy-toast {
@@ -1535,7 +1611,9 @@ h3 {
stroke-width: 2.4;
}
-.story.s6:not(.s7) .copy-toast {
+.story.c2 .b-m2 .copy-toast,
+.story.c3 .b-m3 .copy-toast,
+.skills-demo.is-copied .copy-toast {
opacity: 1;
transform: none;
}
@@ -1564,7 +1642,7 @@ h3 {
transform: translateY(-130%) scale(0.92);
}
-.story.is-live.s8 .event-banner {
+.story.is-live.ev .event-banner {
opacity: 1;
transform: none;
}
@@ -1631,7 +1709,7 @@ h3 {
transform: scale(0);
}
-.story.is-live.s8 .event-check {
+.story.is-live.ev .event-check {
transform: none;
}
@@ -1685,7 +1763,7 @@ h3 {
visibility: hidden;
}
-.story.s2:not(.s4) .compose .caret {
+.story.has-text .compose .caret {
visibility: visible;
}
@@ -1712,215 +1790,423 @@ h3 {
transform: scale(1.05);
}
-/* 键盘:顶部条在「聆听 / 语气 / 技能」三种状态间切换 */
+/*
+ * 键盘:按 App 真实的「AI 助手」界面复刻(AIKeyboardView)。
+ * 尺寸以 cqw 表示,等比映射原生 pt 尺寸(顶栏 44pt、麦克风 156×60pt、圆键 48pt)。
+ * 所有状态都由祖先元素上的 data-* 属性驱动,故事区与技能区共用同一套样式:
+ * data-bar 顶栏:tabs 常规 / cancel 录音中 / paste AI 结果 / skills 复制后
+ * data-mic 麦克风:idle / rec 听写 / ai 问 AI / busy 处理中 / done 已插入
+ * data-ctx 上下文区:hint / live / think / extract / tip / none
+ * data-surface 主体:main 常规 / variants 回复候选
+ * data-pick 被点按的元素:0-2 回复卡片 / event 日程胶囊
+ * data-undo 插入后出现撤销与改写按钮;data-action 回车键可用
+ */
.kb {
display: flex;
flex: none;
flex-direction: column;
- gap: 2.4cqw;
- padding: 2.6cqw 2.6cqw 6.4cqw;
+ padding: 1cqw 3cqw 7cqw;
background: var(--ph-kb);
+ color: var(--ph-key-ink);
+ transition: background 0.4s ease;
+}
+
+.kb .i {
+ stroke-width: 2;
+}
+
+.kb-top {
+ position: relative;
+ height: 10.5cqw;
+ flex: none;
}
.kb-bar {
- position: relative;
- height: 8.6cqw;
-}
-
-.kb-status,
-.kb-tones,
-.kb-skills {
position: absolute;
inset: 0;
display: flex;
align-items: center;
+ gap: 1.6cqw;
opacity: 0;
pointer-events: none;
- transform: translateY(2cqw);
- transition: opacity 0.35s ease, transform 0.6s var(--ease-spring);
+ transform: translateY(1.5cqw);
+ transition: opacity 0.3s ease, transform 0.55s var(--ease-spring);
}
-.story.s2:not(.s3) .kb-status,
-.story.s3:not(.s4) .kb-tones,
-.story.s7 .kb-skills,
-.story:not(.is-live) .kb-skills {
+[data-bar="tabs"] .kb-bar-tabs,
+[data-bar="cancel"] .kb-bar-cancel,
+[data-bar="paste"] .kb-bar-paste,
+[data-bar="skills"] .kb-bar-skills {
opacity: 1;
transform: none;
}
-.kb-status {
- justify-content: center;
- gap: 2cqw;
- color: var(--ph-out);
- font-size: 3.4cqw;
- font-weight: 650;
+.kb-bar-tabs,
+.kb-bar-paste {
+ justify-content: space-between;
}
-.kb-status i {
- width: 2cqw;
- height: 2cqw;
+.kb-bar-cancel {
+ justify-content: flex-end;
+}
+
+/* OSG 标志:两实一空的三个圆点 */
+.kb-logo {
+ display: flex;
+ gap: 0.8cqw;
+}
+
+.kb-logo i {
+ width: 3.4cqw;
+ height: 3.4cqw;
border-radius: 50%;
background: currentColor;
- animation: caret-blink 1s steps(1) infinite;
}
-.kb-tones {
- gap: 1.8cqw;
+.kb-logo i:nth-child(2) {
+ background: transparent;
+ box-shadow: inset 0 0 0 0.45cqw currentColor;
}
-.kb-tones > span,
-.kb-skills > span {
- display: inline-flex;
- align-items: center;
- gap: 1cqw;
- padding: 1.5cqw 3cqw;
+/* AI / 中文 / EN 分段切换:固定在键盘水平中心,与真实界面一致 */
+.kb-seg {
+ position: absolute;
+ left: 50%;
+ display: flex;
+ gap: 0.5cqw;
+ padding: 0.5cqw;
border-radius: 99px;
+ background: var(--ph-track);
+ translate: -50% 0;
+}
+
+.kb-seg > span {
+ display: grid;
+ width: 11.4cqw;
+ height: 7.2cqw;
+ place-items: center;
+ border-radius: 99px;
+ color: color-mix(in srgb, var(--ph-key-ink) 55%, transparent);
+ font-size: 2.85cqw;
+ font-weight: 500;
+}
+
+.kb-seg > .is-on {
background: var(--ph-key);
- font-size: 3.2cqw;
- font-weight: 600;
- transition: background 0.3s ease, color 0.3s ease, transform 0.6s var(--ease-spring),
- opacity 0.3s ease;
+ box-shadow: 0 0 0 0.25cqw var(--ph-line);
+ color: var(--ph-key-ink);
}
-.story.tone-0 [data-tone="0"],
-.story.tone-1 [data-tone="1"],
-.story.tone-2 [data-tone="2"] {
- background: var(--ph-out);
+.kb-seg .i {
+ width: 3.7cqw;
+ height: 3.7cqw;
+}
+
+.kb-chips {
+ display: flex;
+ gap: 1.4cqw;
+}
+
+.kb-chip {
+ display: grid;
+ width: 8.2cqw;
+ height: 8.2cqw;
+ flex: none;
+ place-items: center;
+ border-radius: 50%;
+ background: var(--ph-key);
+ box-shadow: 0 0 0 0.25cqw var(--ph-line);
+ color: color-mix(in srgb, var(--ph-key-ink) 60%, transparent);
+}
+
+.kb-chip .i {
+ width: 3.8cqw;
+ height: 3.8cqw;
+}
+
+/* 玻璃胶囊:粘贴与技能 */
+.kb-cap {
+ display: inline-flex;
+ height: 7.2cqw;
+ flex: none;
+ align-items: center;
+ padding: 0 3cqw;
+ border-radius: 99px;
+ background: var(--ph-glass);
+ box-shadow: 0 0.6cqw 1.6cqw -0.6cqw rgba(0, 0, 0, 0.2), inset 0 0 0 0.25cqw var(--ph-line);
+ font-size: 3cqw;
+ font-weight: 650;
+ white-space: nowrap;
+ transition: background 0.25s ease, color 0.25s ease, transform 0.45s var(--ease-spring);
+}
+
+.kb-caps {
+ display: flex;
+ overflow: hidden;
+ min-width: 0;
+ flex: 1;
+ gap: 1.4cqw;
+ padding: 1cqw 0;
+ -webkit-mask-image: linear-gradient(90deg, #000 86%, transparent);
+ mask-image: linear-gradient(90deg, #000 86%, transparent);
+}
+
+/* 复制后技能胶囊依次弹出:模拟「按相关度挑出能用的技能」 */
+[data-bar="skills"] .kb-caps .kb-cap {
+ animation: cap-in 0.55s var(--ease-spring) both;
+}
+
+.kb-caps .kb-cap:nth-child(2) { animation-delay: 0.06s; }
+.kb-caps .kb-cap:nth-child(3) { animation-delay: 0.12s; }
+.kb-caps .kb-cap:nth-child(4) { animation-delay: 0.18s; }
+.kb-caps .kb-cap:nth-child(5) { animation-delay: 0.24s; }
+
+@keyframes cap-in {
+ from { opacity: 0; transform: translateY(2cqw) scale(0.7); }
+ to { opacity: 1; transform: none; }
+}
+
+[data-pick="event"] .cap-event {
+ background: var(--kb-accent);
color: #fff;
- transform: scale(1.07);
+ transform: scale(0.94);
}
-.kb-tones em {
- margin-left: auto;
- color: var(--ph-muted);
- font-size: 2.6cqw;
- font-style: normal;
-}
-
-.kb-skills {
- gap: 1.6cqw;
-}
-
-.kb-skills .i {
- width: 3.6cqw;
- height: 3.6cqw;
-}
-
-/* 技能芯片依次弹出,模拟「从技能目录里挑出相关操作」 */
-.story.is-live .kb-skills > span {
- opacity: 0;
- transform: translateY(3cqw) scale(0.7);
-}
-
-.story.is-live.s7 .kb-skills > span {
- opacity: 1;
- transform: none;
-}
-
-.kb-skills > span:nth-child(2) { transition-delay: 0.06s; }
-.kb-skills > span:nth-child(3) { transition-delay: 0.12s; }
-.kb-skills > span:nth-child(4) { transition-delay: 0.18s; }
-
-.story.is-live.s8 .kb-skills .skill-event,
-.story:not(.is-live) .kb-skills .skill-event {
- background: var(--ph-out);
- color: #fff;
- transform: scale(1.1);
- transition-delay: 0s;
+/* 主体:上下文区 + 两排操作键;回复候选出现时整体让位 */
+.kb-stage {
+ position: relative;
+ height: 51cqw;
+ flex: none;
}
.kb-main {
+ position: absolute;
+ inset: 0;
display: flex;
- height: 21cqw;
+ flex-direction: column;
+ transition: opacity 0.3s ease, transform 0.5s var(--ease-out);
+}
+
+[data-surface="variants"] .kb-main {
+ opacity: 0;
+ transform: scale(0.97);
+}
+
+.kb-ctx {
+ position: relative;
+ flex: 1;
+}
+
+.ctx {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ max-width: 88%;
+ font-size: 3.3cqw;
+ opacity: 0;
+ text-align: center;
+ transform: translate(-50%, -30%);
+ transition: opacity 0.3s ease, transform 0.5s var(--ease-out);
+ white-space: nowrap;
+}
+
+[data-ctx="hint"] .ctx-hint,
+[data-ctx="live"] .ctx-live,
+[data-ctx="think"] .ctx-think,
+[data-ctx="extract"] .ctx-extract,
+[data-ctx="tip"] .ctx-tip {
+ opacity: 1;
+ transform: translate(-50%, -50%);
+}
+
+.ctx-hint {
+ padding: 1.2cqw 3.2cqw;
+ border-radius: 99px;
+ box-shadow: inset 0 0 0 0.3cqw color-mix(in srgb, var(--ph-key-ink) 18%, transparent);
+ color: color-mix(in srgb, var(--ph-key-ink) 55%, transparent);
+ font-weight: 500;
+}
+
+/* 实时转写:听写时边说边出字 */
+.ctx-live {
+ width: 88%;
+ font-size: 3.7cqw;
+ line-height: 1.35;
+ white-space: normal;
+}
+
+.ctx-think,
+.ctx-extract {
+ color: color-mix(in srgb, var(--ph-key-ink) 55%, transparent);
+ font-size: 3.6cqw;
+}
+
+.ctx-tip {
+ padding: 1.6cqw 3.6cqw;
+ border-radius: 99px;
+ background: var(--ph-glass);
+ box-shadow: 0 1cqw 3cqw -1cqw rgba(0, 0, 0, 0.22), inset 0 0 0 0.25cqw var(--ph-line);
+ font-size: 3.6cqw;
+}
+
+.kb-row {
+ display: flex;
+ height: 14.3cqw;
+ flex: none;
align-items: center;
- justify-content: center;
- gap: 4cqw;
+ justify-content: space-between;
+ padding: 0 2cqw;
}
-.kb-wave {
- height: 12cqw;
- gap: 1.2cqw;
+.kb-row-low {
+ height: 11.4cqw;
+ margin-top: 5.2cqw;
}
-.kb-wave i {
- width: 1.1cqw;
- height: 100%;
- border-radius: 1cqw;
- background: var(--ph-out);
- opacity: 0.75;
- transform: scaleY(0.1);
+.kb-circle {
+ display: grid;
+ width: 11.4cqw;
+ height: 11.4cqw;
+ flex: none;
+ place-items: center;
+ border-radius: 50%;
+ background: var(--ph-key);
+ box-shadow: 0 1cqw 2.4cqw -1cqw rgba(0, 0, 0, 0.25), inset 0 0 0 0.25cqw var(--ph-line);
+ transition: opacity 0.35s ease, scale 0.5s var(--ease-spring);
+}
+
+.kb-circle .i {
+ width: 5cqw;
+ height: 5cqw;
+}
+
+/* 撤销与口述改写只在刚插入过文字后出现 */
+.kb-undo,
+.kb-edit {
+ opacity: 0;
+ scale: 0.7;
+}
+
+[data-undo="1"] .kb-undo,
+[data-undo="1"] .kb-edit {
+ opacity: 1;
+ scale: 1;
}
.kb-mic {
position: relative;
display: grid;
- width: 17cqw;
- height: 17cqw;
+ width: 37cqw;
+ height: 14.3cqw;
+ flex: none;
place-items: center;
- border-radius: 50%;
- background: var(--ph-out);
- box-shadow: 0 2cqw 6cqw -1cqw color-mix(in srgb, var(--ph-out) 70%, transparent);
+ border-radius: 99px;
+ background: var(--kb-accent);
+ box-shadow: inset 0 0.3cqw 0 rgba(255, 255, 255, 0.3),
+ 0 1.6cqw 4cqw -1.6cqw color-mix(in srgb, var(--kb-accent) 85%, transparent);
color: #fff;
- transition: transform 0.6s var(--ease-spring);
+ transition: background-color 0.35s ease, box-shadow 0.35s ease, transform 0.5s var(--ease-spring);
}
-.kb-mic .i {
- width: 7.4cqw;
- height: 7.4cqw;
- stroke-width: 2;
+.kb-mic > * {
+ grid-area: 1 / 1;
+ transition: opacity 0.25s ease, transform 0.45s var(--ease-spring);
}
-.kb-mic::before,
-.kb-mic::after {
- position: absolute;
- inset: 0;
- border: 0.5cqw solid var(--ph-out);
- border-radius: 50%;
- content: "";
+.mic-icon {
+ width: 6.6cqw;
+ height: 6.6cqw;
+}
+
+.kb .mic-check {
+ width: 7cqw;
+ height: 7cqw;
+ stroke-width: 3;
+}
+
+.mic-wave {
+ height: 6.2cqw;
+ gap: 1cqw;
+}
+
+.mic-wave i {
+ width: 1cqw;
+ height: 100%;
+ border-radius: 1cqw;
+ background: #ffc7c7;
+ transform: scaleY(0.14);
+}
+
+.kb .mic-spin {
+ width: 5.2cqw;
+ height: 5.2cqw;
+ border-width: 0.7cqw;
+}
+
+.mic-wave,
+.mic-spin,
+.mic-check {
opacity: 0;
+ transform: scale(0.6);
}
-.story.s2:not(.s3) .kb-mic {
- transform: scale(1.08);
+[data-mic="rec"] .kb-mic {
+ background: var(--kb-red);
+ box-shadow: inset 0 0.3cqw 0 rgba(255, 255, 255, 0.3),
+ 0 1.6cqw 4.4cqw -1.6cqw color-mix(in srgb, var(--kb-red) 85%, transparent);
}
-.story.s2:not(.s3) .kb-mic::before,
-.story.s2:not(.s3) .kb-mic::after {
- animation: orb-ripple 1.6s cubic-bezier(0.2, 0.6, 0.3, 1) infinite;
+[data-mic="ai"] .kb-mic {
+ background: var(--kb-teal);
+ transform: scale(1.03);
}
-.story.s2:not(.s3) .kb-mic::after {
- animation-delay: 0.8s;
+[data-mic="ai"] .mic-wave i {
+ background: #fff;
}
-.kb-keys {
+[data-mic="busy"] .kb-mic {
+ background: color-mix(in srgb, var(--kb-red) 72%, var(--ph-kb));
+ box-shadow: none;
+}
+
+:is([data-mic="rec"], [data-mic="ai"], [data-mic="busy"], [data-mic="done"]) .mic-icon {
+ opacity: 0;
+ transform: scale(0.6);
+}
+
+:is([data-mic="rec"], [data-mic="ai"]) .mic-wave,
+[data-mic="busy"] .mic-spin,
+[data-mic="done"] .mic-check {
+ opacity: 1;
+ transform: none;
+}
+
+/* 回车 / 发送:无可发送内容时为浅灰禁用态 */
+.kb-action {
display: grid;
- gap: 1.8cqw;
- grid-template-columns: 1fr 3.4fr 1.5fr;
-}
-
-.kb-key {
- display: grid;
- height: 10cqw;
+ width: 31.4cqw;
+ height: 11.4cqw;
+ flex: none;
place-items: center;
- border-radius: 2.4cqw;
+ border-radius: 99px;
+ background: var(--ph-key-off);
+ color: color-mix(in srgb, var(--ph-key-ink) 45%, transparent);
+ transition: background 0.3s ease, color 0.3s ease, transform 0.45s var(--ease-spring);
+}
+
+.kb-action .i {
+ width: 5.4cqw;
+ height: 5.4cqw;
+}
+
+[data-action="on"] .kb-action {
background: var(--ph-key);
- box-shadow: 0 0.4cqw 0 rgba(0, 0, 0, 0.16);
- font-size: 3.4cqw;
- transition: transform 0.4s var(--ease-spring), background 0.3s ease;
+ box-shadow: 0 1cqw 2.4cqw -1cqw rgba(0, 0, 0, 0.25), inset 0 0 0 0.25cqw var(--ph-line);
+ color: var(--ph-key-ink);
}
-.kb-key .i {
- width: 4.8cqw;
- height: 4.8cqw;
-}
-
-.kb-return {
- background: var(--ph-out);
- color: #fff;
-}
-
-.story.s4:not(.s5) .kb-return {
+.story.press-send .kb-action {
animation: key-press 0.5s var(--ease-out);
}
@@ -1928,6 +2214,151 @@ h3 {
30% { transform: scale(0.9); }
}
+/* 回复候选:三张玻璃卡片(普通 / 正式 / 轻松趣味),自下而上依次落定 */
+.kb-variants {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 1.8cqw;
+ padding: 1.2cqw 0.6cqw 0;
+ pointer-events: none;
+}
+
+.kb-card {
+ display: flex;
+ align-items: flex-start;
+ gap: 2.6cqw;
+ padding: 2cqw 3cqw;
+ border-radius: 3.6cqw;
+ background: var(--ph-glass);
+ box-shadow: 0 0.8cqw 2.4cqw -1.2cqw rgba(0, 0, 0, 0.2), inset 0 0 0 0.25cqw var(--ph-line);
+ opacity: 0;
+ transform: translateY(5cqw) scale(0.96);
+ transition: opacity 0.35s ease, transform 0.6s var(--ease-spring), box-shadow 0.3s ease;
+}
+
+.kb-card:nth-child(2) { transition-delay: 0.07s; }
+.kb-card:nth-child(3) { transition-delay: 0.14s; }
+
+[data-surface="variants"] .kb-card {
+ opacity: 1;
+ transform: none;
+}
+
+.kb-card .i-solid {
+ width: 4.6cqw;
+ height: 4.6cqw;
+ flex: none;
+ margin-top: 0.4cqw;
+ fill: currentColor;
+ stroke: none;
+}
+
+.kb-card > span {
+ display: grid;
+ gap: 0.3cqw;
+ line-height: 1.32;
+}
+
+.kb-card small {
+ color: color-mix(in srgb, var(--ph-key-ink) 55%, transparent);
+ font-size: 2.7cqw;
+}
+
+.kb-card b {
+ font-size: 3.2cqw;
+ font-weight: 500;
+}
+
+[data-pick="0"] .kb-card[data-card="0"],
+[data-pick="1"] .kb-card[data-card="1"],
+[data-pick="2"] .kb-card[data-card="2"] {
+ box-shadow: 0 0 0 0.7cqw color-mix(in srgb, var(--kb-accent) 50%, transparent);
+ transform: scale(0.97);
+ transition-delay: 0s;
+}
+
+/* --------------------------------------------------------------------------
+ * 技能区演示:复用键盘复刻组件,独立的容器查询让它按自身宽度缩放
+ * -------------------------------------------------------------------------- */
+.skills-frame {
+ display: grid;
+ place-items: center;
+ padding: clamp(18px, 3vw, 34px);
+ background: radial-gradient(120% 90% at 50% 0%, color-mix(in srgb, var(--accent) 14%, var(--surface)), var(--surface-soft));
+}
+
+.skills-demo {
+ display: flex;
+ overflow: hidden;
+ width: min(100%, 430px);
+ flex-direction: column;
+ border-radius: 22px;
+ background: var(--ph-bg);
+ box-shadow: 0 30px 60px -30px rgba(0, 0, 0, 0.45), inset 0 0 0 1px var(--ph-line);
+ color: var(--ph-ink);
+ container-type: inline-size;
+ text-align: left;
+}
+
+/* 消息来源区:被复制的那条消息,最后被「已添加到日历」横幅替换 */
+.sd-source {
+ position: relative;
+ display: grid;
+ min-height: 34cqw;
+ align-items: center;
+ padding: 7cqw 5cqw;
+}
+
+.sd-msg {
+ position: relative;
+ justify-self: start;
+ max-width: 82%;
+ padding: 2.4cqw 3.8cqw;
+ border-radius: 5cqw 5cqw 5cqw 1.4cqw;
+ background: var(--ph-in);
+ font-size: 3.7cqw;
+ line-height: 1.4;
+ transition: box-shadow 0.4s ease, opacity 0.4s ease, transform 0.6s var(--ease-out);
+}
+
+.skills-demo .event-banner {
+ top: 50%;
+ right: 5cqw;
+ left: 5cqw;
+ translate: 0 -50%;
+}
+
+.skills-demo .event-banner,
+.skills-demo .event-check {
+ opacity: 0;
+ transform: translateY(-40%) scale(0.92);
+}
+
+.skills-demo .event-check {
+ transform: scale(0);
+}
+
+.skills-demo.is-done .event-banner {
+ opacity: 1;
+ transform: none;
+}
+
+.skills-demo.is-done .event-check {
+ opacity: 1;
+ transform: none;
+}
+
+.skills-demo.is-done .sd-msg {
+ opacity: 0;
+ transform: translateY(6cqw) scale(0.94);
+}
+
+.skills-demo .kb {
+ padding-bottom: 5cqw;
+}
+
/* --------------------------------------------------------------------------
* 10. 数字带
* -------------------------------------------------------------------------- */
@@ -3068,13 +3499,9 @@ html.vt-theme::view-transition-new(root) {
padding: 6px 14px 6px 6px;
}
- .capsule-orb {
- width: 34px;
- height: 34px;
- }
-
- .capsule-wave {
- display: none;
+ .capsule-mic {
+ width: 62px;
+ height: 28px;
}
.capsule-body {
diff --git a/docs/assets/site.js b/docs/assets/site.js
index 004c5d0..8294ba9 100644
--- a/docs/assets/site.js
+++ b/docs/assets/site.js
@@ -368,19 +368,23 @@
/* ---------------- Hero 语音胶囊 ---------------- */
+ // 复刻键盘真实的麦克风状态流:轻点听写(红色声波 → 打勾)、长按问 AI(青色声波 → 转圈 → 回答)、
+ // 复制消息后的嘴替回复(转圈 → 回复)。文案取自 App 的实际界面字符串与演示素材。
const CAPSULE_SCRIPT = {
zh: [
- { tag: "听写", text: "帮我订周五晚上七点的位子,四个人" },
- { tag: "嘴替 · 轻松", text: "这周有点累,先不去啦,下次我请!" },
- { tag: "替你做", text: "已建日程:周五 19:00 · 四人晚餐", agent: true }
+ { tag: "轻点听写", mode: "rec", text: "帮我订周五晚上七点的位子,四个人" },
+ { tag: "长按问 AI", mode: "ai", text: "周末去哪儿玩比较合适?", busy: "AI 正在思考…", answer: "可以去近郊走走:上午逛古镇,下午找家咖啡馆。" },
+ { tag: "复制 · 嘴替回复", busy: "AI 正在思考…", answer: "好的,我整理一下预算版,明天上午发你。" }
],
en: [
- { tag: "Dictate", text: "Book a table for four, Friday at seven" },
- { tag: "Your voice", text: "Bit wiped this week — rain check? Next one’s on me!" },
- { tag: "Done for you", text: "Event added: Fri 7:00 PM · Dinner for four", agent: true }
+ { tag: "Tap to dictate", mode: "rec", text: "Book a table for four, Friday at seven" },
+ { tag: "Hold to ask AI", mode: "ai", text: "Where should we go this weekend?", busy: "AI is thinking…", answer: "Try a day trip: an old town in the morning, a café in the afternoon." },
+ { tag: "Copy · Reply in your voice", busy: "AI is thinking…", answer: "Sure, I’ll put the budget version together and send it over tomorrow morning." }
]
};
+ const CAPSULE_STATES = ["is-rec", "is-ai", "is-busy", "is-ai-busy", "is-done"];
+
const hero = document.getElementById("top");
const capsule = document.querySelector(".voice-capsule");
const capsuleTag = capsule?.querySelector(".capsule-tag");
@@ -400,6 +404,24 @@
return run === capsuleRun;
}
+ function setCapsuleState(...states) {
+ CAPSULE_STATES.forEach((name) => capsule.classList.toggle(name, states.includes(name)));
+ const listening = states.includes("is-rec") || states.includes("is-ai");
+ if (capsuleWave) capsuleWave.level = listening ? 1 : 0;
+ if (listening) wakeWaves();
+ }
+
+ // 逐字写入;返回 false 表示本轮已被新一轮(如切换语言)取代
+ async function typeLine(run, text, perChar) {
+ const characters = Array.from(text);
+ for (let i = 1; i <= characters.length; i += 1) {
+ if (!(await whenActive(run))) return false;
+ capsuleLine.textContent = characters.slice(0, i).join("");
+ await wait(perChar());
+ }
+ return true;
+ }
+
async function playCapsule() {
if (!capsule || !capsuleLine || !capsuleTag) return;
const run = ++capsuleRun;
@@ -407,39 +429,50 @@
const script = CAPSULE_SCRIPT[lang];
if (reduceMotion) {
+ setCapsuleState();
capsuleTag.textContent = script[0].tag;
capsuleLine.textContent = script[0].text;
return;
}
+ const base = lang === "zh" ? 72 : 34;
+ const speech = () => base + Math.random() * base * 0.8;
let index = 0;
while (run === capsuleRun) {
const item = script[index % script.length];
+ setCapsuleState();
capsuleTag.textContent = item.tag;
- capsuleTag.classList.toggle("is-agent", Boolean(item.agent));
capsuleLine.textContent = "";
- await wait(260);
+ await wait(650);
- // 听写与嘴替模拟「边说边出字」,Agentic 结果直接以完成态弹出
- const listening = !item.agent;
- capsule.classList.toggle("is-listening", listening);
- if (capsuleWave) capsuleWave.level = listening ? 1 : 0.15;
- wakeWaves();
-
- const characters = Array.from(item.text);
- const base = lang === "zh" ? 70 : 34;
- for (let i = 1; i <= characters.length; i += 1) {
- if (!(await whenActive(run))) return;
- capsuleLine.textContent = characters.slice(0, i).join("");
- await wait(item.agent ? 14 : base + Math.random() * base * 0.8);
+ // 1. 说话:红色(听写)或青色(问 AI)胶囊 + 声波,边说边出字
+ if (item.text) {
+ setCapsuleState(item.mode === "ai" ? "is-ai" : "is-rec");
+ if (!(await typeLine(run, item.text, speech))) return;
+ await wait(420);
}
- capsule.classList.remove("is-listening");
- if (capsuleWave) capsuleWave.level = 0.12;
- await wait(item.agent ? 2400 : 1900);
+ // 2. 处理中:胶囊转圈,显示真实界面的状态文案
+ if (item.busy) {
+ setCapsuleState("is-busy", item.mode === "ai" ? "is-ai-busy" : "");
+ capsuleLine.textContent = item.busy;
+ await wait(1300);
+ if (!(await whenActive(run))) return;
+ }
+
+ // 3. 结果:回答快速流出
+ if (item.answer) {
+ capsuleLine.textContent = "";
+ if (!(await typeLine(run, item.answer, () => 16))) return;
+ }
+
+ // 4. 插入成功:胶囊打勾
+ setCapsuleState("is-done");
+ await wait(2100);
if (!(await whenActive(run))) return;
- // 快速回删,像是把这一句交给下一个能力
+ // 快速回删,交给下一个能力
+ const characters = Array.from(capsuleLine.textContent);
for (let i = characters.length; i >= 0; i -= 2) {
if (run !== capsuleRun) return;
capsuleLine.textContent = characters.slice(0, i).join("");
@@ -451,43 +484,69 @@
languageHooks.push(() => playCapsule());
+ /* ---------------- 键盘复刻:共用状态写入 ---------------- */
+
+ const KB_DEFAULTS = { bar: "tabs", mic: "idle", ctx: "hint", surface: "main", pick: "", undo: "0", action: "off" };
+
+ // 把键盘状态写到容器的 data-* 上(只写变化的值,避免无谓的样式重算)
+ function applyKeyboardState(element, state) {
+ Object.keys(KB_DEFAULTS).forEach((key) => {
+ const value = state[key] ?? KB_DEFAULTS[key];
+ if (element.dataset[key] !== value) element.dataset[key] = value;
+ });
+ }
+
/* ---------------- 三幕滚动叙事 ---------------- */
+ // 与气泡文案保持一致:听写结果、选中的「普通」回复
const STORY_TEXT = {
zh: {
- draft: "可以啊 周五七点 位子我来订",
- tones: [
- "可以,周五七点,位子我来订。",
- "好的,周五晚七点见,座位由我来预订。",
- "好呀!周五七点不见不散,位子包在我身上~"
- ]
+ dictation: "收到了,我下午看完给你反馈。",
+ reply: "好的,我整理一下预算版,明天上午发你。"
},
en: {
- draft: "sure friday at seven ill book a table",
- tones: [
- "Sure — Friday at seven. I’ll book a table.",
- "Sounds good. Friday at 7 p.m.; I’ll make the reservation.",
- "Yes! Friday at 7 — leave the table to me."
- ]
+ dictation: "Got it — I’ll review it this afternoon and get back to you.",
+ reply: "Sure, I’ll put the budget version together and send it over tomorrow morning."
}
};
- // 进度阈值(0 → 1):s1 好友来信 · s2 开始聆听 · s3 嘴替改写 · s4 发送
- // s5 好友回地址 · s6 复制 · s7 技能弹出 · s8 写入日历
- const STORY_STEPS = [0.03, 0.08, 0.31, 0.585, 0.66, 0.735, 0.805, 0.885];
- const TYPE_START = 0.08;
- const TYPE_END = 0.29;
- const TONE_STEPS = [0.36, 0.44, 0.515];
- const ACT_RANGES = [[0, 0.31], [0.31, 0.66], [0.66, 1]];
+ // 键盘时间轴(滚动进度 0 → 1),每段是完整状态,取最后一个 at ≤ p 的段
+ // 第一幕 听写:录音 → 打勾插入 → 发送
+ // 第二幕 嘴替:复制消息 → AI 思考 → 三条回复 → 点「普通」插入 → 发送
+ // 第三幕 Agentic:复制会议通知 → 顶栏技能 → 点「日程」→ 提取 → 写入日历
+ const STORY_PHASES = [
+ { at: 0 },
+ { at: 0.065, bar: "cancel", mic: "rec", ctx: "live" },
+ { at: 0.235, mic: "done", ctx: "none", undo: "1", action: "on", compose: "dictation" },
+ { at: 0.27, undo: "1", action: "on", compose: "dictation", send: true },
+ { at: 0.29 },
+ { at: 0.375, bar: "paste", mic: "busy", ctx: "think" },
+ { at: 0.44, bar: "paste", surface: "variants", ctx: "none" },
+ { at: 0.545, bar: "paste", surface: "variants", ctx: "none", pick: "0" },
+ { at: 0.575, mic: "done", ctx: "none", undo: "1", action: "on", compose: "reply" },
+ { at: 0.6, undo: "1", action: "on", compose: "reply", send: true },
+ { at: 0.62 },
+ { at: 0.715, bar: "skills", ctx: "none" },
+ { at: 0.775, bar: "skills", ctx: "extract", mic: "busy", pick: "event" },
+ { at: 0.84, ctx: "tip" },
+ { at: 0.93 }
+ ];
+
+ // 对话气泡:到达阈值即出现;复制提示只在区间内显示
+ const STORY_MESSAGES = { m1: 0.02, o1: 0.29, m2: 0.33, o2: 0.62, m3: 0.67, ev: 0.885 };
+ const STORY_COPIES = { c2: [0.36, 0.44], c3: [0.705, 0.775] };
+ const LIVE_RANGE = [0.075, 0.225];
+ const ACT_RANGES = [[0, 0.31], [0.31, 0.655], [0.655, 1]];
const story = document.getElementById("storyStage");
const storyText = document.getElementById("storyText");
+ const storyLive = document.getElementById("storyLive");
const storyActs = story ? Array.from(story.querySelectorAll(".story-act")) : [];
const storyWaves = story
- ? Array.from(story.querySelectorAll(".kb-wave")).map((host, i) => createWave(host, { mirror: i === 0 }))
+ ? Array.from(story.querySelectorAll(".mic-wave")).map((host) => createWave(host))
: [];
let storyLastText = null;
- let storyLastTone = -1;
+ let storyLastLive = null;
let storyLastAct = -1;
function updateStory(force) {
@@ -500,43 +559,39 @@
const total = Math.max(1, story.offsetHeight - viewport);
const p = clamp(-rect.top / total);
- const lang = root.dataset.lang === "en" ? "en" : "zh";
- const copy = STORY_TEXT[lang];
+ const copy = STORY_TEXT[root.dataset.lang === "en" ? "en" : "zh"];
- const reached = STORY_STEPS.map((step) => p >= step);
- reached.forEach((on, i) => story.classList.toggle(`s${i + 1}`, on));
- const [, listening, rewriting, sent] = reached;
+ let phase = STORY_PHASES[0];
+ STORY_PHASES.forEach((item) => { if (p >= item.at) phase = item; });
+ applyKeyboardState(story, phase);
+ story.classList.toggle("press-send", Boolean(phase.send));
- // 语气:仅在「改写中」阶段可见,按进度依次切到 普通 → 正式 → 轻松
- let tone = -1;
- if (rewriting && !sent) {
- TONE_STEPS.forEach((step, i) => { if (p >= step) tone = i; });
+ Object.entries(STORY_MESSAGES).forEach(([name, at]) => story.classList.toggle(name, p >= at));
+ Object.entries(STORY_COPIES).forEach(([name, [from, to]]) => story.classList.toggle(name, p >= from && p < to));
+
+ // 实时转写:录音阶段按滚动进度逐字出现
+ let live = "";
+ if (phase.ctx === "live") {
+ const characters = Array.from(copy.dictation);
+ const count = Math.round(clamp((p - LIVE_RANGE[0]) / (LIVE_RANGE[1] - LIVE_RANGE[0])) * characters.length);
+ live = characters.slice(0, count).join("");
}
- if (tone !== storyLastTone) {
- [0, 1, 2].forEach((i) => story.classList.toggle(`tone-${i}`, i === tone));
- }
-
- // 输入框:聆听阶段按滚动进度逐字「听写」,改写阶段显示当前语气的版本
- let text = "";
- if (listening && !rewriting) {
- const characters = Array.from(copy.draft);
- const count = Math.round(clamp((p - TYPE_START) / (TYPE_END - TYPE_START)) * characters.length);
- text = characters.slice(0, count).join("");
- } else if (rewriting && !sent) {
- text = tone >= 0 ? copy.tones[tone] : copy.draft;
+ if (storyLive && live !== storyLastLive) {
+ storyLive.textContent = live;
+ storyLastLive = live;
}
+ // 输入框:插入时整句落入(与真实插入行为一致),发送后清空
+ const text = phase.compose ? copy[phase.compose] : "";
if (storyText && text !== storyLastText) {
- const isRewrite = tone !== storyLastTone && text && storyLastText;
storyText.textContent = text;
- if (isRewrite && !reduceMotion) {
+ if (text && !storyLastText && !reduceMotion) {
storyText.classList.remove("is-swapping");
void storyText.offsetWidth;
storyText.classList.add("is-swapping");
}
storyLastText = text;
}
- storyLastTone = tone;
story.classList.toggle("has-text", text.length > 0);
// 左侧三幕:高亮当前幕,幕内进度条随滚动填充
@@ -551,7 +606,7 @@
element.style.setProperty("--ap", clamp((p - start) / (end - start)).toFixed(3));
});
- storyWaves.forEach((wave) => { wave.level = listening && !rewriting ? 1 : 0.06; });
+ storyWaves.forEach((wave) => { wave.level = phase.mic === "rec" ? 1 : 0; });
if (inView) wakeWaves();
}
@@ -559,11 +614,56 @@
story.classList.add("is-live");
languageHooks.push(() => {
storyLastText = null;
- storyLastTone = -2;
+ storyLastLive = null;
updateStory(true);
});
}
+ /* ---------------- 技能区循环演示 ---------------- */
+
+ // 复制 → 顶栏换成技能胶囊 → 点「日程」→ 提取 → 加入日历 → 横幅确认,然后重来
+ const SKILL_DEMO_STEPS = [
+ { ms: 1300 },
+ { ms: 1900, bar: "skills", ctx: "none", copied: true },
+ { ms: 550, bar: "skills", ctx: "none", pick: "event", copied: true },
+ { ms: 1500, bar: "skills", ctx: "extract", mic: "busy", pick: "event" },
+ { ms: 1200, ctx: "tip" },
+ { ms: 2800, done: true }
+ ];
+
+ const skillsDemo = document.getElementById("skillsDemo");
+ let skillsDemoVisible = false;
+ let skillsDemoTimer = 0;
+ let skillsDemoIndex = 0;
+
+ function showSkillsStep(step) {
+ applyKeyboardState(skillsDemo, step);
+ skillsDemo.classList.toggle("is-copied", Boolean(step.copied));
+ skillsDemo.classList.toggle("is-done", Boolean(step.done));
+ }
+
+ function tickSkillsDemo() {
+ clearTimeout(skillsDemoTimer);
+ if (!skillsDemoVisible || document.hidden) return;
+ const step = SKILL_DEMO_STEPS[skillsDemoIndex];
+ showSkillsStep(step);
+ skillsDemoIndex = (skillsDemoIndex + 1) % SKILL_DEMO_STEPS.length;
+ skillsDemoTimer = setTimeout(tickSkillsDemo, step.ms);
+ }
+
+ if (skillsDemo) {
+ if (reduceMotion) {
+ // 减弱动态效果:停在「技能胶囊已出现」这一帧
+ showSkillsStep(SKILL_DEMO_STEPS[1]);
+ } else {
+ new IntersectionObserver((entries) => {
+ skillsDemoVisible = entries[0].isIntersecting;
+ tickSkillsDemo();
+ }, { threshold: 0.35 }).observe(skillsDemo);
+ document.addEventListener("visibilitychange", tickSkillsDemo);
+ }
+ }
+
/* ---------------- 滚动引擎 ---------------- */
const progressBar = document.querySelector(".scroll-progress");
diff --git a/docs/index.html b/docs/index.html
index 25949fa..07c4b64 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -50,7 +50,7 @@
-
+
+