kimi-code/apps/kimi-web/design/undo-exit-demos.html
qer b905dd4910
feat(web): redesign web UI and add design system (#1258)
* feat: redesign web ui & add design system

* feat(web): add motion to redesigned UI and add changesets

Animate toast enter/leave, dialog open, and workspace-list and tool-row expand/collapse instead of snapping, and add the changesets covering the web redesign.

* fix(web): remove undo message exit animation

* fix(web): route agent tools to AgentTool and focus dialog on open

- toolRegistry matched the raw 'agent' name, but normalizeToolName folds
  agent/subagent into 'task', so agent calls fell through to GenericTool
  and lost the inline Open button for the subagent detail panel.
- Dialog's focus watcher only fired on change; callers that mount with
  open already true (Login, Settings, ...) never moved focus into the
  modal. Run it immediately so initial focus and restore-on-close work.

* feat(web): add logo long-press design-system easter egg

Hold the sidebar logo for 3 seconds to open a dialog showing the design system page. Also trim and rebalance the redesign changesets.

* fix(web): silence Sidebar v-show warning by making it single-root

Nest the design-system Teleport inside the sidebar <aside> so the component has a single element root. App applies v-show to Sidebar, which needs an element root to attach to; the fragment root logged a "non-element root node" warning on every reactive update and the collapse did not take effect.

* fix(web): thinking toggle, tool-group i18n, agent detail button

- Show the default-thinking switch as on whenever thinking is effectively enabled (enabled !== false), matching the core resolver.

- Route the grouped tool-call header and status through vue-i18n.

- Hide the subagent "Open detail" button when no matching task exists (e.g. a completed foreground subagent after a refresh).

* fix(web): use strict equality in agent detail button guard

oxlint eqeqeq flagged the loose != null check; resolveAgentTaskId returns string | undefined, so compare with !== undefined.

* chore(web): remove stray design mockups and screenshots

Remove the design exploration mockups, screenshots, prompts, and notes that were accidentally committed under apps/kimi-web/design. Keep design-system.html, which the sidebar logo easter egg still references.

* fix(web): keep sessions on continuation failure, treat absent thinking as on

- Keep sessions already loaded from earlier pages when a continuation page fetch fails, instead of replacing the workspace with an empty page.

- Treat an absent thinking config as enabled in the settings toggle, matching the core resolver (thinking is on unless explicitly disabled).

* feat(web): open design-system easter egg on 10 logo clicks

Replace the 3-second long-press trigger with 10 consecutive clicks on the Kimi mark; the count resets after a short idle. The long-press was unreliable because pointerleave cancelled the timer on any drift.

* fix(web): treat cancelled swarm members as finished

phaseForTask now lets a terminal status (completed/failed/cancelled) override a stale subagentPhase, so a cancelled swarm member no longer stays live and suppresses the finished AgentSwarm card.

* feat(web): use 1s long-press for design-system easter egg

Switch the logo easter egg back to a long-press, shortened to 1 second, and make it reliable this time: use pointer capture plus touch-action:none so a slight drift no longer cancels the hold.

* fix(web): use plain Spinner for activity notices

ActivityNotice renders for non-chat loading states (e.g. compaction), so it must use Spinner per the design-system rule that reserves MoonSpinner for the chat first-response state.

* docs(web): add a11y guidance to design system

* docs(web): drop stale design README link

* fix(web): restore model search focus and define panel header weight

- Bind the model picker's search Input to searchRef so useDialogFocus moves focus into it on open instead of the dialog's close button.

- Use the defined --weight-semibold token for panel header titles (--weight-bold is not declared, so the shorthand was invalid).

* fix(web): let any open dialog own Escape over the side panel

Track open design-system Dialog instances in a shared count and include it in App.vue's anyOverlayOpen, so a dialog whose open state lives outside App.vue (such as the sidebar session search) captures Escape before the background side panel closes.

* fix(web): base dock-work flag on filtered dock task lists

Foreground subagents are excluded from the dock task lists, so a session whose only task is a foreground subagent no longer renders an empty workbar above the composer.

* fix(web): create subagent task before forwarding text deltas

A client that subscribes from a snapshot after subagent.spawned already fired never received the lifecycle taskCreated; the reducer only applies taskProgress to existing tasks, so assistant text deltas were dropped and the live subagent detail stayed blank. Emit taskCreated (via patchSubagent) before the text progress, mirroring the tool-progress path.

* fix(web): keep plan, swarm, and goal mode toggles per session

Plan, swarm, and goal modes were stored as global scalars on the web client and a single localStorage key each, so they leaked across sessions. Bind them to the active session via per-session maps, persist per session, and apply server status/events to the originating session so background sessions keep independent state.

* fix(web): keep subagent detail reachable for synthesized tasks

When the web client subscribes after a subagent already spawned, the synthesized subagent task has no parentToolCallId, so the Agent tool's Open-detail button was hidden and the panel would not open. Fall back to the single unmapped subagent task when resolving the detail target in both the button visibility and the panel open paths.

* fix(web): keep session kebab menu from being clipped

Teleport the SessionRow kebab menu to body and anchor it with fixed positioning so the collapsing group-sessions list's overflow:hidden no longer clips the dropdown.

* fix(web): apply staged modes to the created session by id

When starting the first prompt, apply the staged plan/swarm/goal modes to the just-created session's per-session maps by id instead of via the activeSessionId-based setters, so a session switch during the selectSession await can't drop the modes for this session or pollute another.

* refactor(web): unify confirmation dialogs into a single modal

Replace the inconsistent confirmation patterns (native confirm(),
two-step menu arming, hand-rolled inline strips, bare buttons) with one
modal ConfirmDialog driven by a global useConfirmDialog() composable,
and consolidate the duplicated confirm/cancel i18n keys.

* feat(web): inline message queue with separate stop button

- Send button always sends/enqueues; interrupt moves to a separate red Stop
  button shown only while running, so the two can no longer be confused.
- Queued prompts now render inline at the tail of the transcript (after the
  running turn) instead of behind the dock panel: click to edit, remove, drag
  the grip to reorder, with image thumbnails and a "next up" marker.
- Remove the dock queue panel and the QueuePane component; Steer stays on
  Ctrl/Cmd+S.

* chore: add changeset for web queue UX

* feat(web): prompt reliability, sidebar menu, and composer/markdown polish

- Fix spurious errors when question/approval/task actions were already complete
- Add loading feedback to question and approval prompts; block double-clicks
- Make the question "Other" option selectable by row click and let Enter advance/submit
- Consolidate workspace section actions into an overflow menu
- Tighten markdown prose line-height and block spacing
- Recall input history only when the caret is at text start
2026-07-01 20:47:12 +08:00

398 lines
15 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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