chore: ignore and remove throwaway scratch files (#1439)

Agent working notes (HANDOVER/handoff) and one-off UI prototype HTML files were committed by mistake. Remove the already-tracked ones, add .gitignore patterns for these classes of files and a .tmp/ scratch dir, and document the rule plus a pre-commit self-check in AGENTS.md so future mistakes are caught mechanically.

No publishable package is affected, so this PR needs no changeset.
This commit is contained in:
qer 2026-07-06 22:12:14 +08:00 committed by GitHub
parent c5e3e80041
commit 0824e7b668
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 16 additions and 2041 deletions

12
.gitignore vendored
View file

@ -25,3 +25,15 @@ docs/superpowers/
reports/
.superpowers/
/plan/
# Agent scratch / throwaway files - do not commit
.tmp/
HANDOVER*.md
HANDOFF*.md
handoff.md
handover.md
*-designs.html
*-design.html
*-mockup.html
*-demo.html
*-demos.html

View file

@ -77,3 +77,7 @@ This is a TypeScript monorepo built for agent-assisted development. Keep the roo
- After finishing a task and before submitting a PR, you must run the `gen-changesets` skill (see `.agents/skills/gen-changesets/SKILL.md`) and generate a changeset under `.changeset/` according to its rules.
- When generating a changeset, **never** decide on a `major` bump on your own. When you judge a change to meet the major criteria (breaking changes, incompatible user configuration, renamed or removed commands/arguments, changed behavior semantics, etc.), you must stop and explain it to the user and ask for confirmation. **Only write `major` after the user has explicitly agreed.** Otherwise default to `minor` (and fall back to `patch` if `minor` is unclear). See the "Hard rule: confirm with the user before writing `major`" section in `.agents/skills/gen-changesets/SKILL.md` for details.
- Prefer importing via `import ... from '#/...'`, which serves the same purpose as `import ... from '@/...'`.
- Do not commit throwaway scratch or exploratory files. Never stage:
- Agent working notes or handoff/summary documents (e.g. `HANDOVER-*.md`, `HANDOFF-*.md`, `handoff.md`).
- Throwaway UI/UX prototypes or design mockups (e.g. `*-designs.html`, `*-mockup.html`, `*-demo(s).html`) at the repo root or under a `design/` folder. The only tracked `.html` files should be Vite `index.html` entrypoints.
Before committing or opening a PR, run `git status` and `git diff --staged --stat` and remove anything matching these patterns. Put scratch work under `.tmp/` (gitignored) instead of the repo root or the source tree.

View file

@ -1,149 +0,0 @@
# 接手文档kimi-web 聊天表格宽度
> **当前状态(未提交,用户先用着)**表格在阅读列≈760px内**单元格换行**显示——
> 不横向滚动、不溢出、不裁切,但**没有比正文更宽**。这是目前效果最干净、可以先用的版本。
>
> **仍未达成的目标**:让宽表格像 Manus 那样**突破到比正文更宽的列**再换行。两次突破尝试
> (容器单位 `cqw`、固定 px `@container` 断点)都没做出满意效果,已回退/停用,见 §4、§6。
>
> **只改了 1 个文件**`apps/kimi-web/src/components/chat/Markdown.vue`(表格样式块)。
> typecheck / check:style 通过。
## 1. 背景与目标
`apps/kimi-web` 聊天里 markstream 渲染的 markdown 表格,默认被钉在阅读列宽内
`width:100%` + `table-layout:fixed`),宽表格要么被挤成很窄的列、要么表格内横向滚动。
用户想要的最终效果(参照 Manus 截图):
1. 表格**比正文阅读列更宽**(横向铺开),但
2. 单元格**换行**,所以表格**永不横向滚动**(表格自身和对话面板都不滚),且
3. 正文 / 代码 / diff / 工具块 / thinking 等其它块仍限在 760px 阅读列内。
> 第 1 点(撑得比正文宽)目前**没做到**——见 §6。当前上线的是"列内换行"的降级版。
## 2. 当前实现(列内换行版,正在用)
`Markdown.vue` `<style scoped>` 表格块:
```css
.md :deep(.table-node) { /* <table>:列宽按内容、可换行 */
width: auto !important;
max-width: 100% !important;
table-layout: auto !important;
}
/* 表格在阅读列内换行;装不下(不可断的长内容)才滚动兜底 */
.md :deep(.table-node-wrapper) {
width: max-content;
max-width: 100% !important; /* 100% = 阅读列宽,封顶 → 换行 */
overflow-x: auto !important;
}
.md :deep(.table-node th),
.md :deep(.table-node td) {
text-align: left;
vertical-align: top;
}
```
要点:
- **换行靠的是"去掉旧代码的 `white-space:nowrap`"**。markstream 单元格默认就是
`white-space:normal`(见 `node_modules/markstream-vue/dist/index.px.css`),旧代码特意加
`nowrap !important` 才不换行;现在不设 white-space就回到 markstream 默认的换行。
- `max-content` + `max-width:100%`:列宽按内容撑,但整体不超过阅读列 → 到列宽就换行。
- `.a-msg` 保持 HEAD 的 `width:94%``.content-wrap` 保持 `max-width:760px`,都没动。
## 3. DOM / CSS 结构与关键约束
```
.con (container-type:inline-size; position:relative) ← 不裁剪;@container 的查询根
└ .chat-layout
└ .panes.chat-scroll { overflow-y:auto } ← 决定性裁剪点
└ .content-wrap { max-width:760px; margin:auto } ← 居中阅读列(桌面恒 align-center
└ .chat (flex column; padding:16px 14px)
└ .a-msg (width:94%; align-self:flex-start) ← 左对齐
├ .msg → Markdown(.md) → .markdown-renderer
│ └ .node-slot { width:100% } ← markstream不裁剪
│ └ .node-content { width:100% } ← markstream不裁剪
│ └ .table-node-wrapper ← 表格(突破点,若要撑宽)
│ └ table.table-node
└ .think / .tool-group / .tool / .a-msg-ft
```
**两条硬约束(踩过的坑):**
1. **`.panes` 是裁剪点。** 它有 `overflow-y:auto`,按 CSS 规则另一方向的 `visible` 会被强制算成
`auto`,所以 `.panes` 实际两个方向都裁剪。表格若比面板宽,`.panes` 就会横向滚动
macOS 悬浮滚动条静止时看不见,容易误以为是"被裁掉")。后代无法靠 CSS 逃出它
`position:fixed` 除外,但脱流、不随消息滚动,不可用)。
2. **`.node-content` / `.node-slot` 不裁剪**(只有 `width:100%`,无 overflow所以更宽的表格
wrapper 能正常"顶出"它们向右显示——如果 wrapper 宽度真的生效的话(见 §4 两次失败)。
markstream 的 `.table-node-wrapper` 原始样式:
`{ position:relative; max-width:100%; overflow-x:auto; ... }`,所以覆盖它的 `max-width` / `overflow-x`
需要 `!important`
## 4. 撑宽尝试为什么失败(避免重复试错)
目标是"表格突破 760 列、显示得比正文宽、再换行"。试过两种给 wrapper 更大宽度的写法,都不行:
1. **容器单位 `cqw`**`max-width: calc((100cqw + 760px)/2 - 48px)`
`cqw`(容器宽度单位)在本仓库**别处从没用过**,实测**没生效**——`max-width` 没卡住,
`width:max-content` 就一路撑到不换行的完整宽度、溢出面板(`.panes` 横滚)。
(注意:更早还踩过 `(100cqw+760)/2``760` 不带单位 → `长度+纯数字` 非法 → 整条声明被丢弃,
但即便修正带单位,`cqw` 依旧没做出效果。)
2. **固定 px `@container (min-width)` 断点**`@container (min-width:960px){ ... max-width:800px }`
等按面板宽分档给固定上限。容器查询本身在本仓库可靠Sidebar 等在用),语法也过了 lint
但实际视觉效果用户仍不满意(表格没有明显变宽 / 或档位不合适),已撤掉。
> 结论:单纯改 `.table-node-wrapper``max-width` 这条路,没能稳定做出"比正文宽且换行填满"的效果。
> 下次重做建议换思路(见 §6
## 5. 涉及文件
| 文件 | 关键位置 |
|---|---|
| `apps/kimi-web/src/components/chat/Markdown.vue` | `<style scoped>``.table-node` / `.table-node-wrapper` / 单元格块(**本任务唯一改动** |
| `apps/kimi-web/src/components/chat/ConversationPane.vue` | `.con { container-type:inline-size; --read-max:760px }``.content-wrap``.panes`(裁剪源)——**未改** |
| `apps/kimi-web/src/components/chat/ChatPane.vue` | `.a-msg { width:94% }`——**未改**(本任务) |
## 6. 若要重做"撑得比正文宽"
难点:让表格 wrapper 拿到一个"比 760 列宽、但不超过面板"的宽度,且这个宽度要**真的生效**并触发换行。
几个可试方向(按推荐度):
| 方向 | 说明 | 风险 |
|---|---|---|
| A. 撑开 `.a-msg` + 让表格 `max-content` | `.a-msg``width:fit-content; min-width:100%`,其余直接子元素限 760px表格 `max-content` 顶宽。曾实现过,问题是超面板时 `.panes` 横滚、且正文与宽表格同屏排版怪。 | 中:需解决"超面板横滚"和排版 |
| B. JS 量面板宽写进 CSS 变量 | `ResizeObserver``.con` 宽度,写 `--pane-w`,表格 `max-width: calc((var(--pane-w)+760px)/2 - …)`。绕开 `cqw` 不生效的问题。 | 中:引入 JS但最可控 |
| C. Teleport 到 `.panes` 外 | 宽表格用 Vue Teleport 渲到滚动容器外、JS 定位覆盖到消息处。 | 高:定位+滚动同步脆;且表格是 markstream 第三方 DOM流式重渲染会打架 |
| D. 页面级滚动重构 | 去掉 `.panes` 内滚动,整页纵向滚。 | 高:要重写滚动到底/加载更多/新消息提示等 |
**重做第一步**:先在浏览器量清楚——用户常见宽表格的自然宽度、以及侧栏展开/收起时
`.con`(面板)实际多宽。若表格自然宽度基本 ≤ 面板宽,方向 A/B 就够;否则必然要在
"表格内滚动 / 面板滚动 / 换行压缩"三者里选一个让步§3 硬约束决定纯 CSS 无法三者全免)。
## 7. 验证
```bash
export PATH="$HOME/.nvm/versions/node/v24.16.0/bin:$PATH"
pnpm --filter kimi-web typecheck # 通过
pnpm --filter kimi-web check:style # baseline本文件无新增告警
```
浏览器肉眼(本环境不能渲染):
- 让 agent 输出一个多列宽表格。
- 当前版本预期:表格在阅读列内**换行**显示,不横向滚动、不溢出;但**不会比正文宽**。
- macOS 是悬浮滚动条,判断"有没有横滚"别只看静止截图,拖一下或缩小窗口确认。
## 8. 与本任务无关的未提交改动(不要误改 / 误回退)
`git diff` 里还有这些**非本任务**的改动:
- `ConversationPane.vue`:平滑滚动守卫(`smoothScrollUntil` / `handleEditMessage` / `scrollToBottom` 调整)。
- `ChatPane.vue``.u-copy``justify-content:center` + `.u-copy svg``translateY(1.3px)`
- `apps/kimi-web/src/i18n/…``lib/desktopFlag.ts``components/chat/ConversationToc.vue` 等仓库里早已存在的杂项。
## 9. 收尾
表格改动只在 `Markdown.vue` 一个文件。若就用当前"列内换行"版收尾,按 `gen-changesets` 技能
生成 changesetscope: web描述markdown 表格单元格换行、避免被挤成窄列)即可。

View file

@ -1,547 +0,0 @@
<!doctype html>
<html lang="zh-CN" data-color-scheme="light">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>kimi-web · 消息队列 UX 高保真原型</title>
<style>
/* -----------------------------------------------------------------------
设计 token抄录自 apps/kimi-web/src/style.css v2 层;浅色 + 深色)。
真实落地请直接用项目 token本沙盒为自包含而内联。
----------------------------------------------------------------------- */
:root {
--color-bg: #ffffff;
--color-surface: #fafbfc;
--color-surface-raised: #ffffff;
--color-surface-sunken: #f3f5f8;
--color-text: rgba(0, 0, 0, 0.9);
--color-text-muted: #6b7280;
--color-text-faint: #9aa3af;
--color-line: #e7eaee;
--color-line-strong: #d4d9e0;
--color-accent: #1783ff;
--color-accent-hover: #0f6fe0;
--color-accent-soft: #e8f3ff;
--color-accent-bd: #cfe6ff;
--color-success: #0e7a38;
--color-warning: #a9610a;
--color-danger: #c0392b;
--color-danger-soft: #fbeaea;
--space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px;
--radius-xs: 4px; --radius-sm: 6px; --radius-md: 8px; --radius-lg: 12px;
--radius-xl: 16px; --radius-2xl: 20px; --radius-full: 999px;
--shadow-xs: 0 1px 2px rgba(16, 24, 40, 0.04);
--shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.05), 0 1px 3px rgba(16, 24, 40, 0.06);
--shadow-md: 0 4px 12px rgba(16, 24, 40, 0.07), 0 2px 4px rgba(16, 24, 40, 0.05);
--shadow-lg: 0 12px 32px rgba(16, 24, 40, 0.12), 0 4px 10px rgba(16, 24, 40, 0.08);
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--duration-fast: 120ms; --duration-base: 160ms;
--font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
"Hiragino Sans GB", "Microsoft YaHei", sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
--text-xs: 12px; --text-sm: 13px; --text-base: 14px; --text-lg: 16px;
--leading-normal: 1.5; --leading-relaxed: 1.7;
--weight-medium: 500; --weight-semibold: 700;
}
html[data-color-scheme="dark"] {
--color-bg: #0d1117;
--color-surface: #161b22;
--color-surface-raised: #1c2128;
--color-surface-sunken: #0d1117;
--color-text: #c9cdd4;
--color-text-muted: #9aa0a8;
--color-text-faint: #6b7280;
--color-line: #2d333b;
--color-line-strong: #3d444d;
--color-accent: #58a6ff;
--color-accent-hover: #79b8ff;
--color-accent-soft: rgba(88, 166, 255, 0.14);
--color-accent-bd: rgba(88, 166, 255, 0.28);
--color-success: #3fb950;
--color-warning: #d29922;
--color-danger: #f85149;
--color-danger-soft: rgba(248, 81, 73, 0.14);
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.2);
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.22), 0 1px 3px rgba(0, 0, 0, 0.18);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.24);
--shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.34), 0 4px 10px rgba(0, 0, 0, 0.28);
}
* { box-sizing: border-box; }
html, body { margin: 0; }
body {
background: var(--color-bg);
color: var(--color-text);
font-family: var(--font-ui);
font-size: var(--text-base);
line-height: var(--leading-normal);
-webkit-font-smoothing: antialiased;
transition: background var(--duration-base) var(--ease-out), color var(--duration-base) var(--ease-out);
}
svg { display: block; }
/* ---- 页面框架 ---- */
header {
max-width: 1180px; margin: 0 auto; padding: 40px 24px 8px;
display: flex; align-items: flex-start; justify-content: space-between; gap: 16px;
}
header h1 { font-size: 20px; font-weight: 650; margin: 0 0 6px; letter-spacing: -0.01em; }
header p { margin: 0; color: var(--color-text-muted); font-size: var(--text-sm); max-width: 60ch; }
.theme-toggle {
flex: none; display: inline-flex; align-items: center; gap: 6px; height: 30px; padding: 0 12px;
border-radius: var(--radius-full); border: 1px solid var(--color-line);
background: var(--color-surface); color: var(--color-text-muted); font: inherit; font-size: var(--text-sm);
cursor: pointer; transition: background var(--duration-fast) var(--ease-out);
}
.theme-toggle:hover { background: var(--color-surface-sunken); }
.wrap { max-width: 1180px; margin: 0 auto; padding: 18px 24px 80px; }
.legend {
display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px;
margin: 0 0 22px; padding: 14px 16px;
border: 1px solid var(--color-line); border-radius: var(--radius-lg); background: var(--color-surface);
}
.legend h2 { grid-column: 1 / -1; font-size: var(--text-sm); font-weight: var(--weight-semibold); margin: 0 0 2px; }
.legend .item { display: flex; gap: 10px; align-items: flex-start; font-size: var(--text-sm); color: var(--color-text-muted); }
.legend .item b { color: var(--color-text); font-weight: var(--weight-medium); }
.legend .tag { flex: none; font-size: 11px; font-weight: 700; color: var(--color-accent-hover); background: var(--color-accent-soft); border: 1px solid var(--color-accent-bd); border-radius: var(--radius-full); padding: 1px 7px; margin-top: 1px; }
.stage-label { font-size: var(--text-xs); font-weight: var(--weight-semibold); letter-spacing: .03em; text-transform: uppercase; color: var(--color-text-faint); margin: 26px 0 8px; }
/* 带侧栏注解的舞台:手机列 + 注解列 */
.annotated { display: grid; grid-template-columns: minmax(0, 760px) 1fr; gap: 22px; align-items: start; }
.notes { display: flex; flex-direction: column; gap: 12px; position: sticky; top: 16px; padding-top: 34px; }
.note-card { padding: 11px 13px; border: 1px solid var(--color-line); border-left: 3px solid var(--color-accent-bd); border-radius: var(--radius-md); background: var(--color-surface); font-size: var(--text-xs); color: var(--color-text-muted); line-height: 1.5; }
.note-card b { display: block; color: var(--color-text); font-weight: var(--weight-semibold); margin-bottom: 3px; font-size: var(--text-sm); }
/* ---- 聊天舞台 ---- */
.phone {
border: 1px solid var(--color-line); border-radius: var(--radius-2xl); background: var(--color-bg);
box-shadow: var(--shadow-lg); overflow: hidden;
}
.chat {
display: flex; flex-direction: column; gap: 14px;
padding: 22px 18px 14px;
max-height: 560px; overflow-y: auto;
}
/* 用户气泡(与 design-system §04 一致) */
.u-bub {
align-self: flex-end; max-width: 78%;
background: var(--color-accent-soft); border: 1px solid var(--color-accent-bd);
color: var(--color-text); border-radius: 18px 18px 5px 18px; padding: 11px 15px;
font-size: 15px; line-height: var(--leading-normal); box-shadow: var(--shadow-xs);
}
.u-bub.faint { opacity: .55; } /* 历史已发送、正在处理的那条 */
/* assistant prose */
.a-msg { max-width: 760px; font-size: 15px; line-height: var(--leading-relaxed); color: var(--color-text); }
.a-msg p { margin: 0; }
.a-msg code { font-family: var(--font-mono); background: var(--color-surface-sunken); border: 1px solid var(--color-line); color: var(--color-accent-hover); padding: 1px 6px; border-radius: 5px; font-size: .9em; }
/* thinking */
.thinking { display: inline-flex; align-items: center; gap: 9px; font-size: var(--text-sm); color: var(--color-text-muted); }
.moon { font-size: 15px; line-height: 1; }
/* 紧凑工具行 */
.tool-row { display: flex; align-items: center; gap: 8px; height: 30px; padding: 0 11px; border: 1px solid var(--color-line); border-radius: var(--radius-md); font-family: var(--font-mono); font-size: var(--text-sm); color: var(--color-text); max-width: 520px; }
.tool-row .tr-name { font-weight: 600; }
.tool-row .tr-arg { color: var(--color-text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.tool-row .tr-time { margin-left: auto; color: var(--color-text-faint); font-size: var(--text-xs); }
.dot { width: 7px; height: 7px; border-radius: 50%; flex: none; background: var(--color-text-faint); }
.dot.done { background: var(--color-success); }
.dot.running { background: var(--color-accent); box-shadow: 0 0 0 0 var(--color-accent-soft); animation: pulse 1.4s ease-out infinite; }
@keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(23,131,255,.4); } 100% { box-shadow: 0 0 0 6px rgba(23,131,255,0); } }
/* ===================================================================
新增:内联队列(核心提案)
=================================================================== */
.q-stack { align-self: flex-end; width: min(78%, 520px); display: flex; flex-direction: column; gap: 7px; }
.q-head {
display: flex; align-items: center; justify-content: flex-end; gap: 8px;
padding: 0 4px; color: var(--color-text-faint); font-size: var(--text-xs);
}
.q-head .q-title { display: inline-flex; align-items: center; gap: 6px; }
.q-head .q-hint { color: var(--color-text-faint); }
.q-head .q-count { font-weight: var(--weight-semibold); color: var(--color-accent-hover); }
.q-bubble {
position: relative; display: flex; align-items: center; gap: 8px;
padding: 9px 8px 9px 6px;
background: var(--color-surface-raised);
border: 1px dashed var(--color-accent-bd);
border-radius: 16px 16px 6px 16px;
box-shadow: var(--shadow-xs);
cursor: pointer; text-align: left;
transition: border-color var(--duration-fast) var(--ease-out),
background var(--duration-fast) var(--ease-out),
transform var(--duration-fast) var(--ease-out);
animation: q-in .22s var(--ease-out);
}
@keyframes q-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.q-bubble:hover { border-color: var(--color-accent); background: var(--color-accent-soft); }
.q-bubble:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--color-accent-soft); border-color: var(--color-accent); }
.q-bubble.leaving { animation: q-out .18s var(--ease-out) forwards; }
@keyframes q-out { to { opacity: 0; transform: translateY(-4px); } }
.q-grip { flex: none; width: 16px; display: grid; place-items: center; color: var(--color-text-faint); opacity: .7; cursor: grab; }
.q-bubble:hover .q-grip { opacity: 1; }
.q-body { flex: 1; min-width: 0; font-size: 15px; line-height: var(--leading-normal); color: var(--color-text); opacity: .82; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.q-bubble:hover .q-body { opacity: 1; }
.q-body .q-img { display: inline-flex; align-items: center; gap: 4px; color: var(--color-text-muted); }
.q-tag { flex: none; font-size: 10.5px; font-weight: 700; letter-spacing: .02em; padding: 1px 6px; border-radius: var(--radius-full); white-space: nowrap; }
.q-tag.next { color: var(--color-accent-hover); background: var(--color-accent-soft); border: 1px solid var(--color-accent-bd); }
.q-tag.idx { color: var(--color-text-faint); background: var(--color-surface-sunken); border: 1px solid var(--color-line); }
.q-rm {
flex: none; width: 22px; height: 22px; display: grid; place-items: center;
border: none; background: transparent; color: var(--color-text-faint); border-radius: var(--radius-sm);
cursor: pointer; opacity: 0; transition: opacity var(--duration-fast) var(--ease-out), background var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out);
}
.q-bubble:hover .q-rm, .q-bubble:focus-within .q-rm, .q-rm:focus-visible { opacity: 1; }
.q-rm:hover { background: var(--color-danger-soft); color: var(--color-danger); }
.q-rm:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--color-accent-soft); }
/* ===================================================================
Composer含新增的独立 Stop 按钮 + 运行态 placeholder
=================================================================== */
.dock { padding: 6px 18px 18px; background: var(--color-bg); }
.composer { background: var(--color-surface-raised); border: 1px solid var(--color-line); border-radius: var(--radius-xl); box-shadow: var(--shadow-md); overflow: hidden; }
.composer:focus-within { border-color: var(--color-accent); box-shadow: var(--shadow-md), 0 0 0 3px var(--color-accent-soft); }
.composer-ta {
display: block; width: 100%; resize: none; border: none; outline: none; background: transparent;
padding: 14px 16px 8px; font: inherit; font-size: 15px; color: var(--color-text); line-height: var(--leading-normal);
min-height: 24px; max-height: 160px;
}
.composer-ta::placeholder { color: var(--color-text-faint); }
.composer-bar { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 8px 8px; }
.composer-left, .composer-right { display: flex; align-items: center; gap: 2px; }
.icon-btn { width: 30px; height: 30px; display: grid; place-items: center; border: none; background: transparent; color: var(--color-text-muted); border-radius: var(--radius-md); cursor: pointer; }
.icon-btn:hover { background: var(--color-surface-sunken); color: var(--color-text); }
.pill { display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 10px; border-radius: var(--radius-md); border: 1px solid transparent; background: transparent; font-size: var(--text-sm); font-weight: var(--weight-medium); color: var(--color-text); cursor: pointer; }
.pill:hover { background: var(--color-surface-sunken); }
.pill .pp-strong { font-weight: 700; }
.pill .pp-sub { color: var(--color-accent); font-weight: 600; }
.pill svg { color: var(--color-text-faint); }
.send { width: 32px; height: 32px; border-radius: 50%; display: grid; place-items: center; background: var(--color-accent); color: #fff; border: none; cursor: pointer; box-shadow: var(--shadow-xs); transition: transform var(--duration-fast) var(--ease-out), background var(--duration-base) var(--ease-out); }
.send:hover { background: var(--color-accent-hover); }
.send:active { transform: scale(.92); }
/* 独立的 Stop 按钮(新增):与 Send 平级,运行时显示 */
.stop {
width: 32px; height: 32px; border-radius: 50%; display: grid; place-items: center;
background: var(--color-surface-raised); color: var(--color-text);
border: 1px solid var(--color-line-strong); box-shadow: var(--shadow-xs);
cursor: pointer; transition: transform var(--duration-fast) var(--ease-out), background var(--duration-base) var(--ease-out), color var(--duration-base) var(--ease-out), border-color var(--duration-base) var(--ease-out);
}
.stop:hover { background: var(--color-danger-soft); color: var(--color-danger); border-color: var(--color-danger-soft); }
.stop:active { transform: scale(.92); }
/* 运行态 hint输入框下方一行小字可关 */
.composer-hint { padding: 0 16px 8px; font-size: var(--text-xs); color: var(--color-text-faint); display: flex; align-items: center; gap: 6px; }
.composer-hint kbd { font-family: var(--font-mono); font-size: 11px; padding: 0 5px; border: 1px solid var(--color-line); border-radius: var(--radius-xs); background: var(--color-surface-sunken); color: var(--color-text-muted); }
/* 演示控制条 */
.demo-ctrl { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 10px; padding: 0 2px; }
.demo-ctrl button { font: inherit; font-size: var(--text-sm); border: 1px solid var(--color-line); background: var(--color-surface); color: var(--color-text-muted); border-radius: var(--radius-full); padding: 5px 12px; cursor: pointer; }
.demo-ctrl button:hover { background: var(--color-surface-sunken); color: var(--color-text); }
.demo-ctrl .spacer { flex: 1; }
.demo-ctrl .note { font-size: var(--text-xs); color: var(--color-text-faint); }
.footer-note { margin-top: 22px; font-size: var(--text-sm); color: var(--color-text-muted); max-width: 760px; }
.footer-note h3 { font-size: var(--text-base); color: var(--color-text); margin: 18px 0 6px; }
.footer-note ul { margin: 6px 0; padding-left: 20px; }
.footer-note li { margin: 4px 0; }
.footer-note code { font-family: var(--font-mono); font-size: 12px; background: var(--color-surface-sunken); border: 1px solid var(--color-line); border-radius: 5px; padding: 1px 5px; color: var(--color-accent-hover); }
@media (max-width: 980px) {
.legend { grid-template-columns: 1fr; }
.annotated { grid-template-columns: minmax(0, 760px); }
.notes { position: static; padding-top: 8px; }
header { flex-direction: column; }
}
</style>
</head>
<body>
<header>
<div>
<h1>消息队列 UX · 高保真原型</h1>
<p>把「发送」和「中断」解耦,把队列从隐藏面板搬进对话流。下面是运行中 + 排队的核心场景,可交互(试着输入回车、点 ×、点排队项编辑、点“出队一条”)。</p>
</div>
<button class="theme-toggle" id="themeToggle" type="button">◐ 切换深 / 浅</button>
</header>
<div class="wrap">
<div class="legend">
<h2>相对现在的改动4 点)</h2>
<div class="item"><span class="tag">A</span><div><b>发送永远是发送</b> — 运行中也不再变成“中断”Enter / 按钮都是入队。</div></div>
<div class="item"><span class="tag">B</span><div><b>Stop 独立按钮</b> — 与发送平级、运行时固定显示,急停仍一键可达。</div></div>
<div class="item"><span class="tag">C</span><div><b>队列内联进对话流</b> — 作为半透明待发送气泡,看得见、点得动。</div></div>
<div class="item"><span class="tag">D</span><div><b>可编辑 / 可排序 / 可删除</b> — 点气泡编辑、拖拽排序、× 删除。</div></div>
</div>
<div class="stage-label">场景 · 运行中agent 正在改写中间件,你又追加了 3 条)</div>
<div class="annotated">
<div class="phone">
<div class="chat" id="chat">
<!-- 历史:已被处理的用户消息 -->
<div class="u-bub faint">帮我把登录接口改成 JWT并补上对应的单元测试。</div>
<div class="a-msg"><p>我看了一下 <code>src/auth</code> 的结构,当前是基于 session cookie 的实现,先读取相关文件。</p></div>
<div class="tool-row">
<span class="dot done"></span>
<span class="tr-name">read_file</span>
<span class="tr-arg">src/auth/session.ts</span>
<span class="tr-time">0.2s</span>
</div>
<div class="tool-row">
<span class="dot running"></span>
<span class="tr-name">read_file</span>
<span class="tr-arg">src/auth/middleware.ts</span>
<span class="tr-time">运行中…</span>
</div>
<div class="thinking"><span class="moon">🌔</span>正在改写认证中间件…</div>
<!-- ===== 内联队列(新增) ===== -->
<div class="q-stack" id="qStack">
<div class="q-head">
<span class="q-title">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none"><path d="M2 4h12M2 8h12M2 12h8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
队列 · <span class="q-count" id="qCount">3</span>
</span>
<span class="q-hint">当前回合结束后自动逐条发送</span>
</div>
<div id="qItems"></div>
</div>
</div>
<!-- ===== Composer含独立 Stop ===== -->
<div class="dock">
<div class="composer" id="composer">
<textarea class="composer-ta" id="ta" rows="1" placeholder="输入会加入队列 · Ctrl+S 立即插入运行中的回合"></textarea>
<div class="composer-bar">
<div class="composer-left">
<button class="icon-btn" type="button" title="添加附件">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>
</button>
<span class="pill"><span style="width:7px;height:7px;border-radius:50%;background:var(--color-warning)"></span>yolo</span>
<span class="pill">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M2 4h12M2 8h8M2 12h5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>plan
</span>
</div>
<div class="composer-right">
<span class="pill"><span class="pp-strong">kimi-k2</span><span class="pp-sub">· thinking</span>
<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
<!-- 新增:独立的 Stop仅在 running 时显示 -->
<button class="stop" id="stopBtn" type="button" title="中断当前回合">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><rect x="4" y="4" width="8" height="8" rx="1.5" fill="currentColor"/></svg>
</button>
<!-- Send 永远是 Send -->
<button class="send" id="sendBtn" type="button" title="发送(运行中时加入队列)">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 13V3m0 0L4.5 6.5M8 3l3.5 3.5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</div>
</div>
<div class="composer-hint">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6.5" stroke="currentColor" stroke-width="1.4"/><path d="M8 7.2v3.6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="8" cy="5.2" r=".7" fill="currentColor"/></svg>
运行中:<kbd>Enter</kbd> 加入队列 · <kbd>Ctrl</kbd>+<kbd>S</kbd> 立即插入当前回合
</div>
</div>
<div class="demo-ctrl">
<button id="drainBtn" type="button">▶ 出队一条(模拟回合结束)</button>
<button id="resetBtn" type="button">↺ 重置</button>
<span class="spacer"></span>
<span class="note">提示:在输入框里打点什么按 Enter会作为新气泡进队列。</span>
</div>
</div>
</div>
<aside class="notes">
<div class="note-card">
<b>队列内联进对话流</b>
跟在当前 turn 后面,时间线一目了然。第一条标“下一条”,其余按 FIFO 编号。点气泡 = 载入输入框编辑;⋮⋮ 拖拽排序;× 删除hover / 聚焦时显示,键盘可达)。
</div>
<div class="note-card">
<b>发送 / 中断 解耦</b>
发送按钮永远是发送(运行中 = 入队),不会误触 Stop。Stop 独立成一个 32px 圆按钮hover 变红,和发送平级,急停依然一键可达。
</div>
<div class="note-card">
<b>出队可见</b>
点“▶ 出队一条”看效果:队头气泡上浮为正式用户气泡,队列整体前移,“下一条”标签自动易主。
</div>
</aside>
</div>
<div class="stage-label">对照 · 空闲态(没有队列、没有运行中的 turn</div>
<div class="phone" style="max-width:760px">
<div class="chat" style="max-height:none; padding-bottom:18px">
<div class="u-bub faint">这个项目用的是什么测试框架?</div>
<div class="a-msg"><p>用的是 <code>vitest</code>,纯逻辑测试,没有 jsdom / 组件测试。测试文件跟被测模块放在一起。</p></div>
</div>
<div class="dock">
<div class="composer">
<textarea class="composer-ta" rows="1" placeholder="给 Kimi 发消息,/ 调用命令,@ 引用文件…"></textarea>
<div class="composer-bar">
<div class="composer-left">
<button class="icon-btn" type="button"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg></button>
<span class="pill"><span style="width:7px;height:7px;border-radius:50%;background:var(--color-warning)"></span>yolo</span>
</div>
<div class="composer-right">
<span class="pill"><span class="pp-strong">kimi-k2</span><span class="pp-sub">· thinking</span>
<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
<!-- 空闲:没有 Stop只有 Send -->
<button class="send" type="button"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 13V3m0 0L4.5 6.5M8 3l3.5 3.5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
</div>
</div>
</div>
</div>
</div>
<div class="footer-note">
<h3>这套设计如何回应之前的几个问题</h3>
<ul>
<li><b>误触中断</b> → Stop 与 Send 物理分离Send 永不变成 Stop。</li>
<li><b>队列看不见</b> → 直接出现在对话流末尾,半透明 + 虚线边框表达“尚未发送”,不再需要点开 Dock 面板。</li>
<li><b>不能重排 / 图片不可见</b> → ⋮⋮ 拖拽排序;带图片的项显示缩略图(示例里的 <code>图片 ×2</code> 可换为真实缩略图)。</li>
<li><b>出队不可见</b> → 点“▶ 出队一条”看效果:队头气泡上浮为正式用户气泡,队列整体前移、“下一条”标签易主。</li>
<li><b>Steer 退居二级</b> → 保留 <code>Ctrl+S</code>,在输入框 hint 里提示,不抢主路径。</li>
</ul>
<p style="margin-top:10px">文件名:<code>apps/kimi-web/design/queue-composer-mockup.html</code> · 纯静态沙盒,未触碰任何应用代码。</p>
</div>
</div>
<script>
// ----- 演示数据 -----
const initialQueue = [
{ text: '过期时间设成 7 天,刷新令牌 30 天' },
{ text: '', img: 2 }, // 纯图片排队项
{ text: '另外把 refresh token 的 rotation 也加上' },
];
let queue = structuredClone(initialQueue);
const qItems = document.getElementById('qItems');
const qCount = document.getElementById('qCount');
const ta = document.getElementById('ta');
const sendBtn = document.getElementById('sendBtn');
const drainBtn = document.getElementById('drainBtn');
const resetBtn = document.getElementById('resetBtn');
const chat = document.getElementById('chat');
const icGrip = '<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><circle cx="5.5" cy="4" r="1.1" fill="currentColor"/><circle cx="10.5" cy="4" r="1.1" fill="currentColor"/><circle cx="5.5" cy="8" r="1.1" fill="currentColor"/><circle cx="10.5" cy="8" r="1.1" fill="currentColor"/><circle cx="5.5" cy="12" r="1.1" fill="currentColor"/><circle cx="10.5" cy="12" r="1.1" fill="currentColor"/></svg>';
const icClose = '<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>';
const icImg = '<svg width="13" height="13" viewBox="0 0 16 16" fill="none"><rect x="2" y="3" width="12" height="10" rx="1.5" stroke="currentColor" stroke-width="1.4"/><circle cx="5.5" cy="6.5" r="1" fill="currentColor"/><path d="m3 11 3-2.5L9 11" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/></svg>';
function renderQueue() {
qItems.innerHTML = '';
qCount.textContent = queue.length;
queue.forEach((item, i) => {
const b = document.createElement('button');
b.className = 'q-bubble';
b.type = 'button';
b.title = '点击载入输入框编辑';
const tag = i === 0
? '<span class="q-tag next">下一条</span>'
: `<span class="q-tag idx">#${i + 1}</span>`;
const body = item.img
? `<span class="q-img">${icImg}图片 ×${item.img}</span>`
: escapeHtml(item.text);
b.innerHTML = `
<span class="q-grip" title="拖拽排序">${icGrip}</span>
<span class="q-body">${body}</span>
${tag}
<span class="q-rm" role="button" tabindex="0" title="移除" aria-label="移除">${icClose}</span>`;
b.addEventListener('click', (e) => {
if (e.target.closest('.q-rm') || e.target.closest('.q-grip')) return;
editItem(i);
});
b.querySelector('.q-rm').addEventListener('click', (e) => { e.stopPropagation(); removeItem(i); });
qItems.appendChild(b);
});
}
function escapeHtml(s){ return s.replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); }
function enqueue(text, img) {
queue.push({ text, img });
renderQueue();
scrollChat();
}
function removeItem(i) {
const el = qItems.children[i];
if (el) { el.classList.add('leaving'); setTimeout(() => { queue.splice(i,1); renderQueue(); }, 160); }
else { queue.splice(i,1); renderQueue(); }
}
function editItem(i) {
const item = queue[i];
if (!item || item.img) return; // 图片项不可编辑
ta.value = item.text;
autoGrow();
removeItem(i);
ta.focus();
}
function drainOne() {
if (queue.length === 0) return;
const head = queue.shift();
renderQueue();
const sent = document.createElement('div');
sent.className = 'u-bub';
sent.style.animation = 'q-in .22s var(--ease-out)';
sent.textContent = head.img ? `(图片 ×${head.img}` : head.text;
chat.insertBefore(sent, document.getElementById('qStack'));
scrollChat();
}
function scrollChat(){ requestAnimationFrame(() => { chat.scrollTop = chat.scrollHeight; }); }
function autoGrow() {
ta.style.height = 'auto';
ta.style.height = Math.min(ta.scrollHeight, 160) + 'px';
}
// 输入框Enter 入队
ta.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) {
e.preventDefault();
const v = ta.value.trim();
if (!v) return;
enqueue(v);
ta.value = ''; autoGrow();
}
});
ta.addEventListener('input', autoGrow);
sendBtn.addEventListener('click', () => {
const v = ta.value.trim();
if (!v) return;
enqueue(v); ta.value = ''; autoGrow(); ta.focus();
});
drainBtn.addEventListener('click', drainOne);
resetBtn.addEventListener('click', () => {
queue = structuredClone(initialQueue);
[...chat.querySelectorAll('.u-bub:not(.faint)')].forEach(n => n.remove());
renderQueue();
});
// 深 / 浅切换
document.getElementById('themeToggle').addEventListener('click', () => {
const cur = document.documentElement.getAttribute('data-color-scheme');
document.documentElement.setAttribute('data-color-scheme', cur === 'dark' ? 'light' : 'dark');
});
renderQueue();
</script>
</body>
</html>

View file

@ -1,649 +0,0 @@
<!doctype html>
<html lang="zh" data-color-scheme="light">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>kimi-web · 侧栏「展开更多」Demo</title>
<style>
/* =========================================================================
Tokens — copied verbatim from apps/kimi-web/src/style.css (light + dark)
so this demo matches the real app pixel-for-pixel.
========================================================================= */
:root {
--line: #e9edf3;
--panel: #fafbfc;
--panel2: #f3f5f8;
--bg: #ffffff;
--dim: #595959;
--faint: #b5b5b5;
--color-bg: #ffffff;
--color-surface: #fafbfc;
--color-surface-raised: #ffffff;
--color-surface-sunken: #f3f5f8;
--color-text: rgba(0, 0, 0, 0.9);
--color-text-muted: #6b7280;
--color-text-faint: #9aa3af;
--color-line: #e7eaee;
--color-line-strong: #d4d9e0;
--color-accent: #1783ff;
--color-accent-hover: #0f6fe0;
--color-accent-soft: #e8f3ff;
--color-accent-bd: #cfe6ff;
--color-warning: #a9610a;
--color-warning-soft: #fbf1e0;
--color-info: #1783ff;
--space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px;
--space-5: 20px; --space-6: 24px; --space-8: 32px;
--radius-xs: 4px; --radius-sm: 6px; --radius-md: 8px; --radius-lg: 12px;
--radius-xl: 16px; --radius-full: 999px;
--text-xs: 12px; --text-sm: 13px; --text-base: 14px; --text-lg: 16px;
--weight-regular: 400; --weight-medium: 500; --weight-semibold: 700;
--leading-tight: 1.25; --leading-normal: 1.5;
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--duration-base: 160ms;
--font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
"Hiragino Sans GB", "Microsoft YaHei", "Source Han Sans SC", "Noto Sans SC",
Roboto, Ubuntu, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji",
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-mono: "JetBrains Mono Variable", "JetBrains Mono", ui-monospace,
"SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
--p-focus-ring: 0 0 0 3px var(--color-accent-soft);
--shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.05), 0 1px 3px rgba(16, 24, 40, 0.06);
--shadow-xl: 0 24px 64px rgba(16, 24, 40, 0.18), 0 8px 20px rgba(16, 24, 40, 0.1);
}
html[data-color-scheme="dark"] {
--line: #2d333b;
--panel: #1c2128;
--panel2: #161b22;
--bg: #0d1117;
--dim: #9aa0a8;
--faint: #525960;
--color-bg: #0d1117;
--color-surface: #161b22;
--color-surface-raised: #1c2128;
--color-surface-sunken: #0d1117;
--color-text: #c9cdd4;
--color-text-muted: #9aa0a8;
--color-text-faint: #6b7280;
--color-line: #2d333b;
--color-line-strong: #3d444d;
--color-accent: #58a6ff;
--color-accent-hover: #79b8ff;
--color-accent-soft: rgba(88, 166, 255, 0.14);
--color-accent-bd: rgba(88, 166, 255, 0.28);
--color-warning: #d29922;
--color-warning-soft: rgba(210, 153, 34, 0.14);
--color-info: #58a6ff;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.22), 0 1px 3px rgba(0, 0, 0, 0.18);
--shadow-xl: 0 24px 64px rgba(0, 0, 0, 0.42), 0 8px 20px rgba(0, 0, 0, 0.32);
}
/* =========================================================================
Demo page chrome (not part of the app)
========================================================================= */
* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body {
background: var(--bg);
color: var(--color-text);
font-family: var(--font-ui);
font-size: var(--text-base);
line-height: var(--leading-normal);
display: flex;
flex-direction: column;
}
.bar {
flex: none;
display: flex;
align-items: center;
gap: 16px;
padding: 10px 16px;
border-bottom: 1px solid var(--color-line);
background: var(--color-surface);
flex-wrap: wrap;
}
.bar h1 {
font-size: var(--text-base);
font-weight: var(--weight-semibold);
margin: 0;
margin-right: auto;
}
.ctl { display: flex; align-items: center; gap: 6px; font-size: var(--text-sm); color: var(--color-text-muted); }
.seg {
display: inline-flex;
border: 1px solid var(--color-line);
border-radius: var(--radius-md);
overflow: hidden;
background: var(--color-bg);
}
.seg button {
border: none;
background: transparent;
color: var(--color-text-muted);
font-family: var(--font-ui);
font-size: var(--text-sm);
padding: 4px 10px;
cursor: pointer;
}
.seg button.on { background: var(--color-accent-soft); color: var(--color-accent-hover); }
.seg button:focus-visible { outline: none; box-shadow: var(--p-focus-ring); }
.reset {
border: 1px solid var(--color-line);
background: var(--color-bg);
color: var(--color-text);
border-radius: var(--radius-md);
font-family: var(--font-ui);
font-size: var(--text-sm);
padding: 4px 12px;
cursor: pointer;
}
.reset:hover { background: var(--color-surface-sunken); }
.reset:focus-visible { outline: none; box-shadow: var(--p-focus-ring); }
.panes {
flex: 1;
min-height: 0;
display: flex;
align-items: stretch;
gap: 24px;
padding: 24px;
overflow: auto;
}
.pane { display: flex; flex-direction: column; align-items: center; gap: 12px; }
.pane-label {
font-size: var(--text-sm);
font-weight: var(--weight-medium);
color: var(--color-text-muted);
text-align: center;
max-width: 300px;
}
.pane-label b { color: var(--color-text); }
.notes {
flex: 1;
min-width: 280px;
max-width: 420px;
font-size: var(--text-sm);
color: var(--color-text-muted);
align-self: flex-start;
padding: 16px 18px;
border: 1px solid var(--color-line);
border-radius: var(--radius-lg);
background: var(--color-surface);
}
.notes h3 { margin: 0 0 8px; font-size: var(--text-base); color: var(--color-text); }
.notes h4 { margin: 18px 0 6px; font-size: var(--text-sm); color: var(--color-text); }
.notes ul { margin: 6px 0; padding-left: 18px; }
.notes li { margin: 4px 0; }
.notes code {
font-family: var(--font-mono);
font-size: var(--text-xs);
background: var(--color-surface-sunken);
padding: 1px 5px;
border-radius: var(--radius-xs);
color: var(--color-text);
}
.notes .bad { color: var(--color-warning); }
.notes .ok { color: var(--color-accent-hover); }
/* =========================================================================
Sidebar shell — faithful copy from Sidebar.vue / WorkspaceGroup.vue /
SessionRow.vue. Only the structure the demo needs is reproduced.
========================================================================= */
.side {
width: 300px;
height: min(720px, 100%);
border: 1px solid var(--line);
border-radius: var(--radius-lg);
background: var(--panel);
display: flex;
flex-direction: column;
min-width: 0;
overflow: hidden;
box-shadow: var(--shadow-sm);
/* Alignment contract (Sidebar.vue) */
--sb-pad-x: var(--space-4);
--sb-gutter: 20px;
--sb-gap: var(--space-2);
--ui-font-size: 15px;
}
.side .col { display: flex; flex-direction: column; min-height: 0; width: 100%; height: 100%; }
.ch {
display: flex; align-items: center; justify-content: space-between;
gap: 8px; padding: var(--space-3) var(--space-3) var(--space-2);
}
.ch-brand { display: flex; align-items: center; gap: 8px; min-width: 0; flex: 1; }
.ch-logo { width: 32px; height: 22px; flex: none; }
.ch-name { font-size: 15px; font-weight: 500; color: var(--color-text); }
.ch-actions { display: flex; align-items: center; gap: 2px; }
.icon-btn {
display: inline-flex; align-items: center; justify-content: center;
width: 26px; height: 26px; border: none; border-radius: var(--radius-sm);
background: transparent; color: var(--color-text-muted); cursor: pointer;
}
.icon-btn:hover { background: var(--color-surface-sunken); color: var(--color-text); }
.icon-btn:focus-visible { outline: none; box-shadow: var(--p-focus-ring); }
.icon-btn svg { width: 16px; height: 16px; }
.search {
display: flex; align-items: center; gap: 12px;
min-height: 26px; margin: 0 var(--space-2) var(--space-2);
padding: var(--space-1) calc(var(--sb-pad-x) - var(--space-2));
border: none; border-radius: var(--radius-md); background: transparent;
color: var(--color-text); font: inherit; text-align: left; cursor: pointer;
width: calc(100% - 16px);
}
.search:hover { background: var(--color-surface-sunken); }
.search svg { flex: none; width: 16px; height: 16px; color: var(--color-text-muted); }
.search-input { flex: 1; color: var(--color-text-muted); font-family: var(--font-mono); font-size: var(--ui-font-size); }
.btn-wrap { display: flex; align-items: center; gap: 8px; padding: 0 var(--space-2) var(--space-2); }
.btn-new-chat {
display: flex; align-items: center; gap: 12px; flex: 1; min-width: 0; min-height: 26px;
padding: var(--space-1) calc(var(--sb-pad-x) - var(--space-2));
border: none; border-radius: var(--radius-md); background: transparent;
color: var(--color-text); font-family: var(--font-ui); font-size: var(--ui-font-size);
cursor: pointer; text-align: left;
}
.btn-new-chat:hover { background: var(--color-surface-sunken); }
.btn-new-chat:focus-visible { outline: none; box-shadow: var(--p-focus-ring); }
.btn-new-chat svg { flex: none; width: 16px; height: 16px; }
.sessions { flex: 1; overflow-y: auto; padding: 0 var(--space-2) var(--space-2); min-height: 0; }
.side-section-label {
display: flex; align-items: center; justify-content: space-between; gap: 8px;
padding: 0 var(--space-3) var(--space-1) var(--space-2);
font-size: var(--text-sm); font-weight: 500; letter-spacing: .08em;
text-transform: uppercase; color: var(--faint); user-select: none;
}
.side-section-actions { display: flex; align-items: center; gap: 2px; }
.side-section-actions .icon-btn { width: 22px; height: 22px; color: var(--faint); }
.side-section-actions .icon-btn svg { width: 13px; height: 13px; }
/* Workspace group header */
.group { padding-bottom: var(--space-2); }
.gh {
display: flex; flex-direction: column; gap: 2px; margin: 0;
padding: var(--space-1) var(--space-2); border-radius: var(--radius-sm);
font-family: var(--font-ui); font-size: var(--text-xs); color: var(--color-text);
user-select: none; position: relative; cursor: pointer;
}
.gh:hover { background: var(--color-surface-sunken); }
.gh-top { display: flex; align-items: center; gap: var(--sb-gap); }
.gh-folder { flex: none; color: var(--color-text-muted); margin-right: calc(var(--sb-gutter) - 14px); width: 14px; height: 14px; }
.gh-name {
font-size: var(--text-lg); font-weight: var(--weight-medium); color: var(--color-text);
flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.gh-meta { color: var(--color-text-faint); font-size: var(--text-xs); font-family: var(--font-mono); font-weight: var(--weight-regular); flex: none; }
.gh-path {
color: var(--color-text-faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
padding-left: calc(var(--sb-gutter) + var(--sb-gap)); font-size: var(--text-xs);
}
/* Session list + collapse transition */
.group-sessions { height: auto; overflow: hidden; transition: height var(--duration-base) var(--ease-out); }
.group-sessions.collapsed { height: 0; }
/* Session row */
.se {
display: block; margin: 0; padding: var(--space-1) calc(var(--sb-pad-x) - var(--space-2));
border-radius: var(--radius-md); font-family: var(--font-ui); color: var(--color-text);
cursor: pointer; position: relative;
}
.se:hover { background: var(--panel2); }
.se.on { background: var(--color-accent-soft); color: var(--color-accent-hover); box-shadow: inset 0 0 0 1px var(--color-accent-bd); }
.row { display: flex; align-items: center; gap: var(--sb-gap); min-width: 0; min-height: 26px; }
.lead { width: var(--sb-gutter); flex: none; display: inline-flex; align-items: center; justify-content: center; }
.unread-dot { width: 7px; height: 7px; border-radius: var(--radius-full); background: var(--color-accent); }
.left { display: flex; align-items: center; flex: 1; min-width: 0; }
.t { color: inherit; font-size: 15px; font-weight: var(--weight-regular); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ts { color: var(--color-text-faint); font-size: var(--text-xs); font-family: var(--font-mono); flex: none; }
.se:hover .ts { display: none; }
.badge {
flex: none; font-family: var(--font-ui); font-size: 10px; font-weight: var(--weight-medium);
padding: 1px 6px; border-radius: var(--radius-full); line-height: 16px;
}
.badge.info { color: var(--color-info); background: var(--color-accent-soft); }
.badge.warn { color: var(--color-warning); background: var(--color-warning-soft); }
/* ---- The control under review: show-more / collapse --------------------
Designed per §07: a compact "list" control that mirrors the session-row
geometry, so its label text starts at the exact same x as the session
titles (--sb-pad-x + --sb-gutter + --sb-gap from the sidebar edge). A
small leading glyph sits in the gutter slot (like New chat's icon). */
.show-more {
width: 100%;
border: none;
cursor: pointer;
font-size: var(--text-xs);
text-align: left;
}
.show-more .sm-lead { display: none; }
/* Variant A: CURRENT (non-conformant) — mono, no radius, accent-on-hover,
no focus ring, and text 8px right of the session titles. */
body[data-style="current"] .show-more {
display: block;
padding: var(--space-1) var(--space-2) var(--space-1) calc(var(--sb-pad-x) + var(--sb-gutter) + var(--sb-gap));
background: none;
color: var(--color-text);
font-family: var(--font-mono);
}
body[data-style="current"] .show-more:hover {
color: var(--color-accent-hover);
background: var(--color-surface-sunken);
}
/* Variant B: DESIGNED — session-row-shaped pill; leading glyph in the
gutter; label aligned under session titles; font-ui; muted→text; bg-only
hover; focus ring. */
body[data-style="spec"] .show-more {
display: flex;
align-items: center;
gap: var(--sb-gap);
min-height: 26px;
margin: 0;
padding: var(--space-1) calc(var(--sb-pad-x) - var(--space-2));
background: transparent;
color: var(--color-text-muted);
font-family: var(--font-ui);
border-radius: var(--radius-md);
}
body[data-style="spec"] .show-more .sm-lead {
display: inline-block;
width: var(--sb-gutter);
flex: none;
}
body[data-style="spec"] .show-more .sm-label { flex: 1; min-width: 0; }
body[data-style="spec"] .show-more:hover {
background: var(--color-surface-sunken);
color: var(--color-text);
}
body[data-style="spec"] .show-more:focus-visible {
outline: none;
box-shadow: var(--p-focus-ring);
}
@media (max-width: 760px) {
.panes { flex-direction: column; }
.side { height: 600px; }
}
</style>
</head>
<body data-style="spec">
<div class="bar">
<h1>侧栏「展开更多」Demo</h1>
<label class="ctl">
按钮样式
<span class="seg" id="styleSeg">
<button data-v="spec" class="on">规范合规</button>
<button data-v="current">当前实现</button>
</span>
</label>
<label class="ctl">
主题
<span class="seg" id="schemeSeg">
<button data-v="light" class="on">浅色</button>
<button data-v="dark">深色</button>
</span>
</label>
<button class="reset" id="reset">重置演示</button>
</div>
<div class="panes">
<div class="pane">
<div class="pane-label"><b>方案 A</b> · 加载更多 + 整组折叠<br>(点工作区标题折叠整组)</div>
<aside class="side" data-pane="A"></aside>
</div>
<div class="pane">
<div class="pane-label"><b>方案 B</b> · 加载更多 + 组内展开/收起<br>(收回到第一页)</div>
<aside class="side" data-pane="B"></aside>
</div>
<section class="notes">
<h3>怎么看这个 demo</h3>
<ul>
<li>左右两个侧栏是同一组数据(<code>kimi-code</code> 工作区共 23 个会话,每页 5 条),可独立点按。</li>
<li>顶部 <b>按钮样式</b> 切换「当前实现」与「规范合规」,两套样式同时作用在两个方案上,方便对比。</li>
<li>第二个工作区 <code>docs</code> 用来看「上面的列表展开后会把下面的工作区顶下去」——这就是想收起的动机。</li>
</ul>
<h4>方案 A加载更多 + 整组折叠</h4>
<ul>
<li>只有 <code>加载更多</code> 一个按钮,拉下一页追加。</li>
<li>收起靠 <b>点工作区标题</b>(整组会话全部隐藏)。这是 §07 已经定义的折叠。</li>
<li class="ok">改动最小,完全符合现有规范。</li>
<li class="bad">一折就整组全收,连第一页也看不见。</li>
</ul>
<h4>方案 B加载更多 + 组内展开/收起</h4>
<ul>
<li>加载超过第一页后,多一个 <code>收起</code> 按钮;收回后变成 <code>展开</code></li>
<li>收起只回到第一页(前 5 条),数据不丢、不重新请求。</li>
<li class="ok">能收回第一页之外的会话,缓解把下面工作区顶下去的问题。</li>
<li class="bad">§07 目前没有定义这个组内控件,需要扩展规范。</li>
</ul>
<h4>样式差异(切到「当前实现」看)</h4>
<ul>
<li class="bad">当前:裸 button —— <code>mono</code> 字体、无圆角、hover 文字变 accent、无焦点环且文字比 session 标题<b>偏右 8px</b></li>
<li class="ok">设计稿:做成「会话行同款」的紧凑列表控件 —— 行首留空使文字<b>与 session 标题精确对齐</b><code>font-ui</code><code>radius-md</code>、26px 行高、hover 只出 sunken 底、<code>:focus-visible</code> 焦点环。</li>
</ul>
<p style="margin-top:14px">规范参考:<code>design-system.html</code> §07侧栏/ §02token / 字体)/ §08焦点环</p>
</section>
</div>
<script>
/* -------------------------------------------------------------------------
Data
------------------------------------------------------------------------- */
const PAGE = 5;
const SESSIONS = [
{ t: '修复登录页 token 刷新', ts: '刚刚', unread: true },
{ t: '重构 session 列表分页逻辑', ts: '5 分钟', badge: ['warn', '待审批'] },
{ t: '调整侧栏 hover 样式', ts: '12 分钟' },
{ t: '排查 WebSocket 断连重试', ts: '30 分钟', badge: ['info', '待回答'] },
{ t: '更新 design-system §07', ts: '1 小时' },
{ t: '优化首屏加载速度', ts: '2 小时' },
{ t: '修复暗色模式对比度', ts: '3 小时' },
{ t: '添加工作区拖拽排序', ts: '昨天' },
{ t: '重写 kebab 菜单定位', ts: '昨天' },
{ t: '合并 release 分支冲突', ts: '2 天' },
{ t: '接入新的 telemetry 事件', ts: '2 天' },
{ t: '修复搜索框焦点环', ts: '3 天' },
{ t: '清理废弃 CSS 变量', ts: '3 天' },
{ t: '迁移到 pnpm 10', ts: '4 天' },
{ t: '更新 i18n 文案', ts: '5 天' },
{ t: '修复移动端 sheet 滚动', ts: '6 天' },
{ t: '调整图标尺寸 token', ts: '1 周' },
{ t: '重构 useKimiWebClient', ts: '1 周' },
{ t: '删除 legacy 命令', ts: '2 周' },
{ t: '升级 Vue 3.5', ts: '2 周' },
{ t: '补充 e2e 用例', ts: '3 周' },
{ t: '文档站点 vitepress 升级', ts: '3 周' },
{ t: '初始化项目脚手架', ts: '1 月' },
];
const DOCS = [
{ t: '同步 release notes', ts: '1 小时' },
{ t: '翻译 zh 文档', ts: '昨天' },
{ t: '更新 README', ts: '3 天' },
{ t: '添加 AGENTS.md', ts: '1 周' },
];
/* -------------------------------------------------------------------------
Icons (Remix-Icon-ish, 24x24, currentColor)
------------------------------------------------------------------------- */
const I = {
folder: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 7a2 2 0 0 1 2-2h4.6a2 2 0 0 1 1.4.6L13 7h6a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7z"/></svg>',
folderOpen: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 7a2 2 0 0 1 2-2h4.6a2 2 0 0 1 1.4.6L13 7h6a2 2 0 0 1 2 2l-2 8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7z" opacity=".85"/></svg>',
search: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M10.5 4a6.5 6.5 0 1 0 4.1 11.6l4.4 4.4 1.4-1.4-4.4-4.4A6.5 6.5 0 0 0 10.5 4zm0 2a4.5 4.5 0 1 1 0 9 4.5 4.5 0 0 1 0-9z"/></svg>',
chatNew: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M5 5a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h3l3 3 3-3h5a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2H5zm7 4h2v3h3v2h-3v3h-2v-3H9v-2h3V9z"/></svg>',
dots: '<svg viewBox="0 0 24 24" fill="currentColor"><circle cx="5" cy="12" r="1.8"/><circle cx="12" cy="12" r="1.8"/><circle cx="19" cy="12" r="1.8"/></svg>',
collapse: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M7 14l5-5 5 5z"/></svg>',
expand: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M7 10l5 5 5-5z"/></svg>',
panel: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M4 5a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V5zm2 0v14h5V5H6zm7 0v14h5V5h-5z"/></svg>',
settings: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8zm0 2a2 2 0 1 1 0 4 2 2 0 0 1 0-4z"/><path d="M11 3h2l.4 2.1a6.9 6.9 0 0 1 1.6.9l2-.7 1 1.7-1.6 1.3c.1.3.1.7.1 1s0 .7-.1 1l1.6 1.3-1 1.7-2-.7a6.9 6.9 0 0 1-1.6.9L13 21h-2l-.4-2.1a6.9 6.9 0 0 1-1.6-.9l-2 .7-1-1.7 1.6-1.3A4.6 4.6 0 0 1 7.5 12c0-.3 0-.7.1-1L6 9.7l1-1.7 2 .7c.5-.4 1-.7 1.6-.9L11 3z" opacity=".55"/></svg>',
plus: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5h2v6h6v2h-6v6h-2v-6H5v-2h6z"/></svg>',
};
const logo = '<svg class="ch-logo" viewBox="0 0 32 22" fill="none"><rect x="1" y="1" width="30" height="20" rx="6" fill="var(--color-accent)"/><circle cx="11" cy="11" r="2.4" fill="#fff"/><circle cx="21" cy="11" r="2.4" fill="#fff"/></svg>';
/* -------------------------------------------------------------------------
Per-pane state
------------------------------------------------------------------------- */
const state = {
A: { loaded: PAGE, groupCollapsed: false },
B: { loaded: PAGE, expanded: true, groupCollapsed: false },
};
function specStyle() { return document.body.dataset.style === 'spec'; }
function row(s, idx) {
const lead = s.unread ? '<span class="unread-dot"></span>' : '';
const badge = s.badge ? `<span class="badge ${s.badge[0]}">${s.badge[1]}</span>` : '';
const on = idx === 1 ? ' on' : '';
return `<div class="se${on}"><div class="row"><span class="lead">${lead}</span><div class="left"><span class="t">${s.t}</span></div><span class="ts">${s.ts}</span>${badge}</div></div>`;
}
function groupHeader(name, meta, path, collapsed) {
return `<div class="gh" data-action="toggleGroup">
<div class="gh-top">
<span class="gh-folder">${collapsed ? I.folder : I.folderOpen}</span>
<span class="gh-name">${name}</span>
<span class="gh-meta">${meta}</span>
</div>
<div class="gh-path">${path}</div>
</div>`;
}
function smButton(action, label) {
return `<button class="show-more" data-action="${action}"><span class="sm-lead" aria-hidden="true"></span><span class="sm-label">${label}</span></button>`;
}
function render(pane) {
const root = document.querySelector(`.side[data-pane="${pane}"]`);
const st = state[pane];
const loaded = st.loaded;
const hasMore = loaded < SESSIONS.length;
const extra = loaded - PAGE;
// Mode A shows everything loaded; mode B shows only first PAGE when collapsed-in-group.
const visibleCount = (pane === 'B' && !st.expanded) ? Math.min(PAGE, loaded) : loaded;
const rows = SESSIONS.slice(0, visibleCount).map((s, i) => row(s, i)).join('');
const loadLabel = specStyle() ? '加载更多' : '展开更多';
const loadBtn = hasMore
? smButton('loadMore', `${loadLabel} (${SESSIONS.length - loaded})`)
: '';
let toggleBtn = '';
if (pane === 'B' && loaded > PAGE) {
toggleBtn = st.expanded
? smButton('collapseIn', '收起')
: smButton('expandIn', `展开 (${extra})`);
}
const collapsedCls = st.groupCollapsed ? ' collapsed' : '';
const docsRows = DOCS.map((s, i) => row(s, 99)).join('');
root.innerHTML = `<div class="col">
<div class="ch">
<div class="ch-brand">${logo}<span class="ch-name">Kimi Code</span></div>
<div class="ch-actions">
<button class="icon-btn" title="收起侧边栏">${I.panel}</button>
<button class="icon-btn" title="设置">${I.settings}</button>
</div>
</div>
<button class="search" type="button">${I.search}<span class="search-input">搜索会话 (⌘K)</span></button>
<div class="btn-wrap">
<button class="btn-new-chat" type="button">${I.chatNew}<span>新建对话</span></button>
<button class="icon-btn" title="新建工作区">${I.plus}</button>
</div>
<div class="sessions">
<div class="side-section-label">
<span>Workspaces</span>
<div class="side-section-actions">
<button class="icon-btn" title="折叠全部">${I.collapse}</button>
<button class="icon-btn" title="选项">${I.dots}</button>
</div>
</div>
<div class="group" data-role="target">
${groupHeader('kimi-code', `${SESSIONS.length}`, '~/code/kimi-code', st.groupCollapsed)}
<div class="group-sessions${collapsedCls}">
${rows}
${loadBtn}
${toggleBtn}
</div>
</div>
<div class="group">
${groupHeader('docs', `${DOCS.length}`, '~/code/docs', false)}
<div class="group-sessions">${docsRows}</div>
</div>
</div>
</div>`;
}
function renderAll() { render('A'); render('B'); }
document.querySelector('.panes').addEventListener('click', (e) => {
const el = e.target.closest('[data-action]');
if (!el) return;
const pane = el.closest('.side').dataset.pane;
const st = state[pane];
const action = el.dataset.action;
if (action === 'loadMore') {
st.loaded = Math.min(SESSIONS.length, st.loaded + PAGE);
if (pane === 'B') st.expanded = true;
} else if (action === 'collapseIn') {
st.expanded = false;
} else if (action === 'expandIn') {
st.expanded = true;
} else if (action === 'toggleGroup') {
st.groupCollapsed = !st.groupCollapsed;
}
render(pane);
});
// Style toggle
document.getElementById('styleSeg').addEventListener('click', (e) => {
const b = e.target.closest('button'); if (!b) return;
document.body.dataset.style = b.dataset.v;
for (const x of e.currentTarget.children) x.classList.toggle('on', x === b);
renderAll();
});
// Scheme toggle
document.getElementById('schemeSeg').addEventListener('click', (e) => {
const b = e.target.closest('button'); if (!b) return;
document.documentElement.dataset.colorScheme = b.dataset.v;
for (const x of e.currentTarget.children) x.classList.toggle('on', x === b);
});
// Reset
document.getElementById('reset').addEventListener('click', () => {
state.A = { loaded: PAGE, groupCollapsed: false };
state.B = { loaded: PAGE, expanded: true, groupCollapsed: false };
renderAll();
});
renderAll();
</script>
</body>
</html>

View file

@ -1,398 +0,0 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>用户消息 undo 退场动画 · 10 个候选</title>
<style>
/* -----------------------------------------------------------------------
设计系统 token从 apps/kimi-web/src/style.css 默认浅色 / 蓝色 accent 抄录,
仅用于本沙盒;真实落地时请直接用 var(--xxx) 引用项目 token。
----------------------------------------------------------------------- */
:root {
--bg: #ffffff;
--color-text: rgba(0, 0, 0, 0.9);
--muted: #8a8a8a;
--color-accent: #1783ff;
--color-accent-soft: #e8f3ff;
--color-accent-bd: #cfe6ff;
--radius-sm: 6px;
--radius-xl: 16px;
--shadow-xs: 0 1px 2px rgba(16, 24, 40, 0.04);
--shadow-md: 0 4px 12px rgba(16, 24, 40, 0.07), 0 2px 4px rgba(16, 24, 40, 0.05);
--shadow-lg: 0 12px 32px rgba(16, 24, 40, 0.12), 0 4px 10px rgba(16, 24, 40, 0.08);
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--bg);
color: var(--color-text);
font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
font-size: 15px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
header {
max-width: 880px;
margin: 0 auto;
padding: 40px 24px 8px;
}
header h1 { font-size: 20px; font-weight: 650; margin: 0 0 6px; letter-spacing: -0.01em; }
header p { margin: 0; color: var(--muted); font-size: 14px; }
.toolbar {
max-width: 880px;
margin: 18px auto 0;
padding: 0 24px;
display: flex;
gap: 10px;
align-items: center;
}
.btn {
appearance: none;
border: 1px solid var(--color-accent-bd);
background: var(--color-accent-soft);
color: var(--color-text);
border-radius: 999px;
padding: 6px 14px;
font: inherit;
font-size: 13px;
cursor: pointer;
transition: background 0.12s, border-color 0.12s;
}
.btn:hover { background: #d8ecff; }
.list {
max-width: 880px;
margin: 22px auto 64px;
padding: 0 24px;
display: flex;
flex-direction: column;
gap: 14px;
}
.card {
display: grid;
grid-template-columns: 1fr minmax(220px, 320px);
gap: 18px;
align-items: center;
border: 1px solid #ececf0;
border-radius: 14px;
padding: 16px 18px;
background: #fff;
}
.meta h2 {
font-size: 15px;
font-weight: 600;
margin: 0 0 6px;
display: flex;
align-items: center;
gap: 8px;
}
.meta .num { color: var(--muted); font-weight: 500; font-variant-numeric: tabular-nums; }
.meta p { margin: 0; color: var(--muted); font-size: 13px; }
.meta code {
display: inline-block;
margin-top: 8px;
font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
font-size: 11.5px;
color: #555;
background: #f5f5f7;
border-radius: 6px;
padding: 3px 7px;
}
.badge {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.02em;
padding: 2px 8px;
border-radius: 999px;
border: 1px solid transparent;
white-space: nowrap;
}
.badge.low { color: #1a7f4b; background: #e6f7ee; border-color: #bfe8d0; }
.badge.mid { color: #9a5b00; background: #fff3df; border-color: #f6dcae; }
.badge.high { color: #b4232a; background: #fdeaea; border-color: #f6c2c2; }
/* 演示舞台:右对齐气泡,与真实 .u-bub 一致 */
.stage {
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: flex-start;
gap: 6px;
min-height: 48px;
/* perspective 仅供需要 3D 的候选使用,对其余候选无影响 */
perspective: 900px;
}
.bubble {
max-width: 78%;
background: var(--color-accent-soft);
border: 1px solid var(--color-accent-bd);
color: var(--color-text);
border-radius: var(--radius-xl) var(--radius-xl) var(--radius-sm) var(--radius-xl);
padding: 11px 15px;
box-shadow: var(--shadow-xs);
cursor: pointer;
user-select: none;
transform-origin: right center;
/* 入场,让重置后有一个轻微出现感 */
animation: bubble-in 0.22s var(--ease-out) both;
}
.bubble:hover { border-color: #bcd9fb; }
.bubble.done { visibility: hidden; pointer-events: none; }
.hint { display: block; margin-top: 8px; color: var(--muted); font-size: 11.5px; text-align: right; }
@keyframes bubble-in {
0% { opacity: 0; transform: translateY(4px) scale(0.98); }
100% { opacity: 1; transform: none; }
}
/* =====================================================================
候选动画:触发方式均为在 .bubble 上添加 .exiting 类。
落地到 ChatPane.vue 时,把对应 @keyframes + .u-bub.undoing 规则替换即可。
===================================================================== */
/* 1) 柔雾消散 —— 低 */
.ex-1.exiting { animation: ux-1 0.28s var(--ease-out) forwards; }
@keyframes ux-1 {
0% { opacity: 1; filter: blur(0); transform: scale(1); }
100% { opacity: 0; filter: blur(6px); transform: scale(0.985); }
}
/* 2) 向右滑出 —— 低 */
.ex-2.exiting { animation: ux-2 0.26s var(--ease-out) forwards; }
@keyframes ux-2 {
0% { opacity: 1; transform: translateX(0); }
100% { opacity: 0; transform: translateX(52px); }
}
/* 3) 缩小淡出 —— 低(向右锚点缩小) */
.ex-3.exiting { animation: ux-3 0.24s var(--ease-out) forwards; }
@keyframes ux-3 {
0% { opacity: 1; transform: scale(1); }
100% { opacity: 0; transform: scale(0.82); }
}
/* 4) 横向收束 —— 中(沿右边缘压扁) */
.ex-4.exiting { animation: ux-4 0.26s var(--ease-out) forwards; }
@keyframes ux-4 {
0% { opacity: 1; transform: scaleX(1); }
60% { opacity: 0.7; }
100% { opacity: 0; transform: scaleX(0); }
}
/* 5) 向上折起 —— 中(需要父级 perspective */
.ex-5.exiting {
transform-origin: top center;
animation: ux-5 0.32s var(--ease-in-out) forwards;
}
@keyframes ux-5 {
0% { opacity: 1; transform: rotateX(0deg) translateY(0); }
100% { opacity: 0; transform: rotateX(-82deg) translateY(-6px); }
}
/* 6) 裁切擦除 —— 中clip-path 从右向左裁掉) */
.ex-6.exiting { animation: ux-6 0.3s var(--ease-out) forwards; }
@keyframes ux-6 {
0% { opacity: 1; clip-path: inset(0 0 0 0 round 16px); }
70% { opacity: 0.9; }
100% { opacity: 0; clip-path: inset(0 0 0 100% round 16px); }
}
/* 7) 浮起消散 —— 低~中(上浮 + 阴影放大 + 淡出) */
.ex-7.exiting { animation: ux-7 0.3s var(--ease-out) forwards; }
@keyframes ux-7 {
0% { opacity: 1; transform: translateY(0); box-shadow: var(--shadow-xs); }
100% { opacity: 0; transform: translateY(-14px); box-shadow: var(--shadow-lg); }
}
/* 8) 墨迹晕开 —— 中(失焦 + 提亮 + 去饱和,像墨迹在水里散开) */
.ex-8.exiting { animation: ux-8 0.34s var(--ease-out) forwards; }
@keyframes ux-8 {
0% { opacity: 1; filter: blur(0) brightness(1) grayscale(0); transform: scale(1); }
100% { opacity: 0; filter: blur(5px) brightness(1.45) grayscale(0.5); transform: scale(1.02); }
}
/* 9) 粒子点阵消散 —— 高mask 点阵 + @property 让点阵逐点收缩消失)
需要 @property 与 mask 支持Chrome/Edge 85+、Safari 16.4+、Firefox 128+)。 */
@property --dot {
syntax: "<length>";
inherits: false;
initial-value: 4px;
}
.ex-9.exiting {
-webkit-mask: radial-gradient(circle, #000 var(--dot), transparent calc(var(--dot) + 0.6px)) 0 0 / 9px 9px;
mask: radial-gradient(circle, #000 var(--dot), transparent calc(var(--dot) + 0.6px)) 0 0 / 9px 9px;
animation: ux-9 0.55s var(--ease-out) forwards;
}
@keyframes ux-9 {
0% { --dot: 4px; opacity: 1; }
55% { opacity: 0.9; }
100% { --dot: 0px; opacity: 0; }
}
/* 10) 散焦放大 —— 中(轻微放大 + 强失焦,像被吹散) */
.ex-10.exiting { animation: ux-10 0.3s var(--ease-out) forwards; }
@keyframes ux-10 {
0% { opacity: 1; filter: blur(0); transform: scale(1); }
100% { opacity: 0; filter: blur(11px); transform: scale(1.07); }
}
</style>
</head>
<body>
<header>
<h1>用户消息 undo 退场动画 · 10 个候选</h1>
<p>点击每条气泡预览退场效果;点击「重播」或「全部重置」可再次观看。气泡样式对齐真实 <code>.u-bub</code>右对齐、accent-soft、非对称圆角</p>
</header>
<div class="toolbar">
<button class="btn" id="resetAll">全部重置</button>
<span style="color:var(--muted);font-size:13px">复杂度:低 = 纯 transform/opacity/filter中 = 需 clip-path / perspective / 多 filter高 = 需 @property + mask。</span>
</div>
<div class="list" id="list">
<div class="card" data-ex="ex-1">
<div class="meta">
<h2><span class="num">01</span> 柔雾消散 <span class="badge low"></span></h2>
<p>原地失焦 + 轻微缩小。最克制、最贴设计系统的「安静」气质。</p>
<code>opacity · blur · scale</code>
</div>
<div class="stage"><div class="bubble">撤销这条消息,看看效果</div></div>
</div>
<div class="card" data-ex="ex-2">
<div class="meta">
<h2><span class="num">02</span> 向右滑出 <span class="badge low"></span></h2>
<p>向右平移并淡出,方向明确(与「撤回」语义一致)。性能最好。</p>
<code>translateX · opacity</code>
</div>
<div class="stage"><div class="bubble">撤销这条消息,看看效果</div></div>
</div>
<div class="card" data-ex="ex-3">
<div class="meta">
<h2><span class="num">03</span> 缩小淡出 <span class="badge low"></span></h2>
<p>向右锚点缩小并淡出。比柔雾多一点「被收走」的感觉,但仍然克制。</p>
<code>scale · opacityorigin right</code>
</div>
<div class="stage"><div class="bubble">撤销这条消息,看看效果</div></div>
</div>
<div class="card" data-ex="ex-4">
<div class="meta">
<h2><span class="num">04</span> 横向收束 <span class="badge mid"></span></h2>
<p>沿右边缘把气泡压扁到 0。像把消息「抽走」方向感强。</p>
<code>scaleX · opacityorigin right</code>
</div>
<div class="stage"><div class="bubble">撤销这条消息,看看效果</div></div>
</div>
<div class="card" data-ex="ex-5">
<div class="meta">
<h2><span class="num">05</span> 向上折起 <span class="badge mid"></span></h2>
<p>3D 向上翻折消失,立体、有层次。需父级 perspective。</p>
<code>rotateX · opacity · perspective</code>
</div>
<div class="stage"><div class="bubble">撤销这条消息,看看效果</div></div>
</div>
<div class="card" data-ex="ex-6">
<div class="meta">
<h2><span class="num">06</span> 裁切擦除 <span class="badge mid"></span></h2>
<p>用 clip-path 从右向左裁掉。干净、利落,无明显位移。</p>
<code>clip-path inset · opacity</code>
</div>
<div class="stage"><div class="bubble">撤销这条消息,看看效果</div></div>
</div>
<div class="card" data-ex="ex-7">
<div class="meta">
<h2><span class="num">07</span> 浮起消散 <span class="badge low">低~中</span></h2>
<p>向上浮起、阴影放大、淡出。像消息「飘走」,轻盈。</p>
<code>translateY · box-shadow · opacity</code>
</div>
<div class="stage"><div class="bubble">撤销这条消息,看看效果</div></div>
</div>
<div class="card" data-ex="ex-8">
<div class="meta">
<h2><span class="num">08</span> 墨迹晕开 <span class="badge mid"></span></h2>
<p>失焦 + 提亮 + 去饱和,像墨迹在水里晕开。柔和、有质感。</p>
<code>blur · brightness · grayscale · opacity</code>
</div>
<div class="stage"><div class="bubble">撤销这条消息,看看效果</div></div>
</div>
<div class="card" data-ex="ex-9">
<div class="meta">
<h2><span class="num">09</span> 粒子点阵消散 <span class="badge high"></span></h2>
<p>mask 点阵逐点收缩,气泡「碎成点」再消失。最接近「粒子消散」。</p>
<code>@property --k · radial-gradient mask</code>
</div>
<div class="stage"><div class="bubble">撤销这条消息,看看效果</div></div>
</div>
<div class="card" data-ex="ex-10">
<div class="meta">
<h2><span class="num">10</span> 散焦放大 <span class="badge mid"></span></h2>
<p>轻微放大 + 强失焦,像被一阵风吹散。比柔雾更「爆」一点。</p>
<code>scale · blur · opacity</code>
</div>
<div class="stage"><div class="bubble">撤销这条消息,看看效果</div></div>
</div>
</div>
<script>
// 交互:点击气泡触发退场;动画结束后标记 done 隐藏。
// 再次观看请用「重播」或「全部重置」。
document.querySelectorAll('.card').forEach((card) => {
const ex = card.dataset.ex;
const bubble = card.querySelector('.bubble');
bubble.classList.add(ex);
function replay() {
bubble.classList.remove('exiting', 'done');
// 强制重排,让 animation 能重新触发
void bubble.offsetWidth;
}
bubble.addEventListener('click', () => {
if (bubble.classList.contains('exiting')) return;
replay();
bubble.classList.add('exiting');
});
bubble.addEventListener('animationend', (e) => {
if (e.animationName === 'bubble-in') return;
bubble.classList.add('done');
});
// 给每个 stage 追加一个「重播」提示/按钮
const stage = card.querySelector('.stage');
const hint = document.createElement('span');
hint.className = 'hint';
hint.textContent = '点击气泡播放';
const again = document.createElement('button');
again.className = 'btn';
again.style.marginLeft = '8px';
again.style.padding = '2px 10px';
again.textContent = '重播';
again.addEventListener('click', (e) => { e.stopPropagation(); replay(); });
const wrap = document.createElement('span');
wrap.appendChild(hint);
wrap.appendChild(again);
stage.appendChild(wrap);
});
document.getElementById('resetAll').addEventListener('click', () => {
document.querySelectorAll('.bubble').forEach((b) => {
b.classList.remove('exiting', 'done');
void b.offsetWidth;
});
});
</script>
</body>
</html>

View file

@ -1,298 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>kimi-web · Composer 左下角按钮设计方案 v2</title>
<style>
:root {
--page: #fafafa;
--surface: #ffffff;
--border: #ebebed;
--border-strong: #dedee2;
--text: #18181b;
--muted: #71717a;
--faint: #a1a1aa;
--hover: #f4f4f5;
--active: #eeeeef;
--accent: #18181b;
--dot-manual: #a1a1aa;
--dot-yolo: #f59e0b;
--dot-auto: #ef4444;
--shadow: 0 1px 2px rgba(0,0,0,.04), 0 12px 32px rgba(0,0,0,.06);
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
[data-theme="dark"] {
--page: #0c0c0e;
--surface: #141417;
--border: #25252a;
--border-strong: #303037;
--text: #e4e4e7;
--muted: #a1a1aa;
--faint: #71717a;
--hover: #1d1d22;
--active: #26262c;
--accent: #e4e4e7;
--dot-manual: #71717a;
--shadow: 0 1px 2px rgba(0,0,0,.3), 0 16px 40px rgba(0,0,0,.45);
}
* { box-sizing: border-box; }
body {
margin: 0; padding: 36px 24px 90px;
background: var(--page); color: var(--text);
font-family: var(--sans); font-size: 14px; line-height: 1.5;
transition: background .25s, color .25s;
}
.wrap { max-width: 720px; margin: 0 auto; }
.topbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
h1 { font-size: 21px; font-weight: 650; margin: 0; letter-spacing: -.01em; }
.theme-toggle {
display: inline-flex; align-items: center; gap: 8px; cursor: pointer;
font-size: 12.5px; color: var(--muted); user-select: none;
}
.theme-toggle .track {
width: 38px; height: 22px; border-radius: 999px; background: var(--border-strong); position: relative;
transition: background .2s;
}
.theme-toggle .track::after {
content: ""; position: absolute; top: 3px; left: 3px; width: 16px; height: 16px;
border-radius: 50%; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.2); transition: transform .2s;
}
[data-theme="dark"] .theme-toggle .track { background: #3f3f46; }
[data-theme="dark"] .theme-toggle .track::after { transform: translateX(16px); }
.lead { color: var(--muted); margin: 6px 0 34px; font-size: 13.5px; }
.section { margin: 0 0 46px; }
.sh { display: flex; align-items: center; gap: 9px; margin: 0 0 6px; }
.sh h2 { font-size: 14.5px; font-weight: 650; margin: 0; letter-spacing: -.01em; }
.num { font-family: var(--mono); font-size: 11px; color: var(--faint); }
.rec { font-size: 10.5px; font-weight: 600; color: #fff; background: var(--text); border-radius: 999px; padding: 1.5px 8px; }
[data-theme="dark"] .rec { background: var(--text); color: #111; }
.desc { color: var(--muted); font-size: 13px; margin: 0 0 15px; }
/* ===== composer shell ===== */
.composer { background: var(--surface); border: 1px solid var(--border); border-radius: 18px; box-shadow: var(--shadow); overflow: hidden; }
.input { padding: 15px 17px 6px; min-height: 54px; color: var(--faint); font-size: 14.5px; }
.toolbar { display: flex; align-items: center; justify-content: space-between; padding: 8px 10px 10px; gap: 8px; }
.left { display: flex; align-items: center; gap: 4px; min-width: 0; }
.right { display: flex; align-items: center; gap: 6px; margin-left: auto; }
.ic { width: 15px; height: 15px; display: inline-block; vertical-align: -2.5px; flex: none;
fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.ic.sm { width: 12.5px; height: 12.5px; }
.send { width: 32px; height: 32px; border-radius: 50%; border: none; background: var(--accent); color: var(--surface);
display: inline-flex; align-items: center; justify-content: center; cursor: pointer; }
/* model pill (right, contextual) */
.model { display: inline-flex; align-items: center; gap: 5px; height: 30px; padding: 0 10px; border-radius: 9px; font-size: 13px; color: var(--muted); cursor: pointer; }
.model:hover { background: var(--hover); color: var(--text); }
/* ============================================================
方向 A静谧线性Quiet Linear
============================================================ */
.a-strip {
display: inline-flex; align-items: center; height: 32px;
border: 1px solid var(--border); border-radius: 10px; padding: 0 3px;
background: var(--surface);
}
.a-strip .it {
display: inline-flex; align-items: center; gap: 6.5px; height: 26px; padding: 0 9px;
border-radius: 7px; font-size: 13px; color: var(--muted); cursor: pointer; border: none; background: none; font: inherit;
}
.a-strip .it:hover { background: var(--hover); color: var(--text); }
.a-strip .sep { width: 1px; height: 14px; background: var(--border); }
.sd { width: 6.5px; height: 6.5px; border-radius: 50%; flex: none; }
.sd.auto { background: var(--dot-auto); }
.sd.yolo { background: var(--dot-yolo); }
.sd.manual { background: var(--dot-manual); }
/* ============================================================
方向 B浮动胶囊 DockFloating Pill
============================================================ */
.b-composer { border-radius: 22px; }
.b-dock {
display: inline-flex; align-items: center; height: 34px; padding: 0 5px; gap: 1px;
border: 1px solid var(--border); border-radius: 999px;
background: color-mix(in srgb, var(--surface) 82%, transparent);
backdrop-filter: blur(14px) saturate(1.4);
box-shadow: 0 4px 16px rgba(0,0,0,.05);
}
[data-theme="dark"] .b-dock { box-shadow: 0 6px 20px rgba(0,0,0,.4); }
.b-dock .it {
display: inline-flex; align-items: center; gap: 6px; height: 26px; padding: 0 10px;
border-radius: 999px; font-size: 12.5px; color: var(--muted); cursor: pointer; border: none; background: none; font: inherit;
}
.b-dock .it:hover { background: var(--hover); color: var(--text); }
.b-dock .it.on { background: var(--text); color: var(--surface); }
[data-theme="dark"] .b-dock .it.on { background: var(--text); color: #111; }
/* ============================================================
方向 C毛玻璃浮层Frosted Glass
============================================================ */
.c-composer {
border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
background: linear-gradient(180deg, color-mix(in srgb, var(--surface) 92%, transparent), var(--surface));
}
.c-glass {
display: inline-flex; align-items: center; height: 32px; padding: 0 4px; gap: 2px;
border: 1px solid color-mix(in srgb, var(--border) 80%, transparent); border-radius: 11px;
background: color-mix(in srgb, var(--surface) 55%, transparent);
backdrop-filter: blur(18px) saturate(1.6);
}
.c-glass .it {
display: inline-flex; align-items: center; gap: 6.5px; height: 25px; padding: 0 9px;
border-radius: 8px; font-size: 12.5px; color: var(--muted); cursor: pointer; border: none; background: none; font: inherit;
}
.c-glass .it:hover { background: color-mix(in srgb, var(--hover) 80%, transparent); color: var(--text); }
.c-pip { width: 7px; height: 7px; border-radius: 50%; flex: none; box-shadow: 0 0 0 2.5px color-mix(in srgb, var(--dot-auto) 22%, transparent); }
.c-pip.auto { background: var(--dot-auto); }
/* ============================================================
方向 D编辑式纯文字Editorial
============================================================ */
.d-composer { border: none; box-shadow: none; background: transparent; }
.d-input { padding: 4px 2px 10px; }
.d-toolbar { padding: 6px 2px 0; border-top: 1px solid var(--border); }
.d-left { gap: 2px; }
.d-left .it {
display: inline-flex; align-items: center; gap: 7px; height: 28px; padding: 0 4px;
border: none; background: none; border-radius: 6px; font-size: 13px; color: var(--muted); cursor: pointer; font: inherit;
}
.d-left .it:hover { color: var(--text); }
.d-left .it .ic { opacity: .7; }
.d-left .it:hover .ic { opacity: 1; }
.d-left .it b { font-weight: 500; color: var(--text); }
.d-sep { color: var(--border-strong); padding: 0 2px; }
.note { margin-top: 11px; font-size: 12.5px; color: var(--faint); display: flex; gap: 7px; align-items: flex-start; }
.note .d { width: 4px; height: 4px; border-radius: 50%; background: var(--muted); margin-top: 7px; flex: none; }
</style>
</head>
<body>
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<symbol id="i-image" viewBox="0 0 16 16"><rect x="2" y="3" width="12" height="10" rx="1.5"/><circle cx="5" cy="6" r="1.2"/><path d="M2 10.5l3-2.5L8 11l2.5-2L14 11"/></symbol>
<symbol id="i-plus" viewBox="0 0 16 16"><path d="M8 3v10M3 8h10"/></symbol>
<symbol id="i-sliders" viewBox="0 0 24 24"><line x1="4" y1="8" x2="20" y2="8"/><circle cx="10" cy="8" r="2.5" fill="currentColor" stroke="none"/><line x1="4" y1="16" x2="20" y2="16"/><circle cx="15" cy="16" r="2.5" fill="currentColor" stroke="none"/></symbol>
<symbol id="i-shield" viewBox="0 0 16 16"><path d="M8 1.5 12.5 3v4.2c0 3-2 5.2-4.5 6.3C5.5 12.4 3.5 10.2 3.5 7.2V3L8 1.5z"/></symbol>
<symbol id="i-sparkles" viewBox="0 0 16 16"><path d="M8 2l1.5 4.5L14 8l-4.5 1.5L8 14l-1.5-4.5L2 8l4.5-1.5z"/></symbol>
<symbol id="i-chevron" viewBox="0 0 16 16"><path d="M4 6l4 4 4-4"/></symbol>
<symbol id="i-send" viewBox="0 0 16 16"><path d="M8 3l6 5.5M8 3L2 8.5M8 3v10"/></symbol>
</svg>
<div class="wrap">
<div class="topbar">
<h1>Composer 左下角 · v2</h1>
<label class="theme-toggle" id="themeToggle"><span>深色</span><span class="track"></span></label>
</div>
<p class="lead">统一目标:减少噪点(去掉彩色胶囊、合并视觉块),并用完全不同的设计语言。右上角可切换深色预览。</p>
<!-- ===== 方向 A ===== -->
<div class="section">
<div class="sh"><span class="num">A</span><h2>静谧线性Quiet Linear</h2><span class="rec">推荐</span></div>
<p class="desc">参考 Linear / Vercel。三个控件收进一条发丝边框的容器黑白灰 + 精致留白;权限仅用一个 6px 状态点表达,去掉所有彩色胶囊。</p>
<div class="composer">
<div class="input">给 Kimi 发送消息…</div>
<div class="toolbar">
<div class="left">
<div class="a-strip">
<button class="it"><svg class="ic"><use href="#i-image"/></svg>附件</button>
<span class="sep"></span>
<button class="it"><span class="sd auto"></span>自动<svg class="ic sm" style="opacity:.5"><use href="#i-chevron"/></svg></button>
<span class="sep"></span>
<button class="it"><svg class="ic"><use href="#i-sliders"/></svg>模式</button>
</div>
</div>
<div class="right">
<span class="model">kimi-k2 · thinking<svg class="ic sm"><use href="#i-chevron"/></svg></span>
<button class="send"><svg class="ic" style="stroke:currentColor"><use href="#i-send"/></svg></button>
</div>
</div>
</div>
<div class="note"><span class="d"></span><span>整条读作"一个对象"而非 3~5 个碎片;权限颜色从大字红/橙降级为一个小点,安静但仍有语义。</span></div>
</div>
<!-- ===== 方向 B ===== -->
<div class="section">
<div class="sh"><span class="num">B</span><h2>浮动胶囊 DockFloating Pill</h2></div>
<p class="desc">参考 iOS / Arc 浏览器。所有控件放进一枚圆角 999px 的"药丸",半透明 + 背景模糊,像悬浮在输入框底部的工具条。激活项用反色胶囊。</p>
<div class="composer b-composer">
<div class="input">给 Kimi 发送消息…</div>
<div class="toolbar">
<div class="left">
<div class="b-dock">
<button class="it"><svg class="ic"><use href="#i-plus"/></svg></button>
<button class="it"><span class="sd auto"></span>自动</button>
<button class="it on"><svg class="ic"><use href="#i-sparkles"/></svg>Plan</button>
</div>
</div>
<div class="right">
<span class="model">kimi-k2<svg class="ic sm"><use href="#i-chevron"/></svg></span>
<button class="send"><svg class="ic" style="stroke:currentColor"><use href="#i-send"/></svg></button>
</div>
</div>
</div>
<div class="note"><span class="d"></span><span>把"当前启用的模式"直接做成反色胶囊(这里是 Plan状态一目了然附件退化为单个 +,节省空间。</span></div>
</div>
<!-- ===== 方向 C ===== -->
<div class="section">
<div class="sh"><span class="num">C</span><h2>毛玻璃浮层Frosted Glass</h2></div>
<p class="desc">半透明磨砂 + 柔和阴影,工具条轻飘飘地浮在输入区上方,整体更"现代、通透"。权限点带一圈柔光 halo。</p>
<div class="composer c-composer">
<div class="input">给 Kimi 发送消息…</div>
<div class="toolbar">
<div class="left">
<div class="c-glass">
<button class="it"><svg class="ic"><use href="#i-image"/></svg>附件</button>
<button class="it"><span class="c-pip auto"></span>自动<svg class="ic sm" style="opacity:.5"><use href="#i-chevron"/></svg></button>
<button class="it"><svg class="ic"><use href="#i-sliders"/></svg>模式</button>
</div>
</div>
<div class="right">
<span class="model">kimi-k2 · thinking<svg class="ic sm"><use href="#i-chevron"/></svg></span>
<button class="send"><svg class="ic" style="stroke:currentColor"><use href="#i-send"/></svg></button>
</div>
</div>
</div>
<div class="note"><span class="d"></span><span>通透感最强,适合配浅色/渐变背景;代价是 <code>backdrop-filter</code> 在低端机上略耗性能。</span></div>
</div>
<!-- ===== 方向 D ===== -->
<div class="section">
<div class="sh"><span class="num">D</span><h2>编辑式纯文字Editorial</h2></div>
<p class="desc">参考 Notion / 写作应用。彻底去掉边框、填充、胶囊,只剩一条分隔线 + 精心排版的文字和小图标hover 才显出交互。最克制、最"轻"。</p>
<div class="composer d-composer">
<div class="input d-input">给 Kimi 发送消息…</div>
<div class="toolbar d-toolbar">
<div class="left d-left">
<button class="it"><svg class="ic"><use href="#i-image"/></svg><b>图片</b></button>
<span class="d-sep">/</span>
<button class="it"><span class="sd auto"></span><b>自动</b></button>
<span class="d-sep">/</span>
<button class="it"><svg class="ic"><use href="#i-sliders"/></svg><b>模式</b></button>
</div>
<div class="right">
<span class="model">kimi-k2 · thinking<svg class="ic sm"><use href="#i-chevron"/></svg></span>
<button class="send"><svg class="ic" style="stroke:currentColor"><use href="#i-send"/></svg></button>
</div>
</div>
</div>
<div class="note"><span class="d"></span><span>信息密度最低,几乎"隐形";适合强调输入本身、不想被工具条抢戏的场景。</span></div>
</div>
</div>
<script>
const toggle = document.getElementById('themeToggle');
toggle.addEventListener('click', () => {
const cur = document.documentElement.getAttribute('data-theme');
document.documentElement.setAttribute('data-theme', cur === 'dark' ? 'light' : 'dark');
});
</script>
</body>
</html>