mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
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:
parent
c5e3e80041
commit
0824e7b668
7 changed files with 16 additions and 2041 deletions
12
.gitignore
vendored
12
.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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` 技能
|
||||
生成 changeset(scope: web,描述:markdown 表格单元格换行、避免被挤成窄列)即可。
|
||||
|
|
@ -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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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>
|
||||
|
|
@ -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(侧栏)/ §02(token / 字体)/ §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>
|
||||
|
|
@ -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 · opacity(origin 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 · opacity(origin 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>
|
||||
|
|
@ -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:浮动胶囊 Dock(Floating 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>浮动胶囊 Dock(Floating 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue